summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.cirrus.yml7
-rw-r--r--.clang-format178
-rw-r--r--.clang-tidy162
-rw-r--r--.codespellrc6
-rw-r--r--.coveralls.yml2
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md17
-rw-r--r--.github/ISSUE_TEMPLATE/feature_request.md20
-rw-r--r--.github/actions/muslbuilder/Dockerfile162
-rwxr-xr-x.github/actions/muslbuilder/entrypoint.sh18
-rw-r--r--.github/mlc_config.json10
-rw-r--r--.github/workflows/bins.yml114
-rw-r--r--.github/workflows/c-cpp.yml194
-rw-r--r--.github/workflows/check-md-links.yml12
-rw-r--r--.github/workflows/coverity.yml48
-rw-r--r--.github/workflows/release.yml98
-rw-r--r--.github/workflows/rpmbuild.yml49
-rw-r--r--.github/workflows/tailer-ape.yml37
-rw-r--r--.gitignore104
-rw-r--r--.gitmodules3
-rw-r--r--.readthedocs.yaml20
-rw-r--r--ARCHITECTURE.md121
-rw-r--r--AUTHORS48
-rw-r--r--CMakeLists.txt59
-rw-r--r--CMakePresets.json142
-rw-r--r--CMakeUserPresets.json.example62
-rw-r--r--FUNDING.yml2
-rw-r--r--INSTALL229
-rw-r--r--LICENSE24
-rw-r--r--Makefile.am13
-rw-r--r--NEWS.md1037
-rw-r--r--README80
-rw-r--r--README.md157
-rw-r--r--TESTS_ENVIRONMENT.in277
-rw-r--r--appveyor.yml17
-rwxr-xr-xautogen.sh26
-rwxr-xr-xcleanup_expected.sh15
-rw-r--r--cmake/CodeCoverage.cmake438
-rw-r--r--cmake/Hunter/config.cmake14
-rw-r--r--cmake/HunterGate.cmake528
-rw-r--r--cmake/coverage.cmake33
-rw-r--r--cmake/dev-mode.cmake32
-rw-r--r--cmake/docs-ci.cmake112
-rw-r--r--cmake/docs.cmake46
-rw-r--r--cmake/folders.cmake21
-rw-r--r--cmake/install-rules.cmake44
-rw-r--r--cmake/install-script.cmake18
-rw-r--r--cmake/lint-targets.cmake32
-rw-r--r--cmake/lint.cmake50
-rw-r--r--cmake/open-cpp-coverage.cmake.example31
-rw-r--r--cmake/prelude.cmake10
-rw-r--r--cmake/project-is-top-level.cmake6
-rw-r--r--cmake/spell-targets.cmake22
-rw-r--r--cmake/spell.cmake29
-rw-r--r--cmake/variables.cmake28
-rw-r--r--conanfile.py58
-rw-r--r--configure.ac331
-rw-r--r--demo/Dockerfile44
-rw-r--r--demo/fly.toml35
-rwxr-xr-xdemo/loggen.py54
-rw-r--r--docs/.gitignore5
-rw-r--r--docs/02_downloads.md65
-rw-r--r--docs/03_features.md146
-rw-r--r--docs/04_tutorials.md30
-rw-r--r--docs/05_docs.md6
-rw-r--r--docs/06_changeblog.md6
-rw-r--r--docs/404.html25
-rw-r--r--docs/CNAME1
-rw-r--r--docs/Doxyfile.in32
-rw-r--r--docs/Gemfile31
-rw-r--r--docs/Gemfile.lock268
-rw-r--r--docs/Makefile153
-rw-r--r--docs/README.md4
-rw-r--r--docs/_config.yml62
-rw-r--r--docs/_includes/social.html121
-rw-r--r--docs/_layouts/post.html17
-rw-r--r--docs/_layouts/top.html78
-rw-r--r--docs/_posts/2013-09-10-json-encoded-logs.md56
-rw-r--r--docs/_posts/2013-09-13-four-years-on-github.md12
-rw-r--r--docs/_posts/2013-10-05-mini-review-in-linux-magazine.md8
-rw-r--r--docs/_posts/2013-10-06-competing-with-tail.md23
-rw-r--r--docs/_posts/2013-11-01-mini-review-linux-user-magazine.md9
-rw-r--r--docs/_posts/2014-02-22-changes-to-the-scrollbar.md21
-rw-r--r--docs/_posts/2014-11-11-lofi-mode.md23
-rw-r--r--docs/_posts/2015-04-11-pretty-print-view.md39
-rw-r--r--docs/_posts/2016-03-20-lnav-in-print.md10
-rw-r--r--docs/_posts/2018-03-27-reveal-file-paths.md16
-rw-r--r--docs/_posts/2018-04-05-linux-magazine-tutorial.md9
-rw-r--r--docs/_posts/2018-05-17-tags-and-comments.md22
-rw-r--r--docs/_posts/2018-11-9-visual-filter-editor.md35
-rw-r--r--docs/_posts/2019-05-08-themes.md28
-rw-r--r--docs/_posts/2020-12-23-xpath-sql-function.md39
-rw-r--r--docs/_posts/2021-05-03-tailing-remote-files.md32
-rw-r--r--docs/_posts/2022-05-01-regex101-integration.md73
-rw-r--r--docs/_posts/2022-08-04-pretty-errors.md46
-rw-r--r--docs/_posts/2022-08-06-markdown-support.md33
-rw-r--r--docs/_posts/2022-09-01-playground.md27
-rw-r--r--docs/_posts/2022-09-24-vscode-extension.md21
-rw-r--r--docs/_posts/2023-06-23-cursor-mode.md30
-rw-r--r--docs/assets/images/favicon.pngbin0 -> 1340 bytes
-rw-r--r--docs/assets/images/favicon.svg3
-rw-r--r--docs/assets/images/linux-user-and-dev-mag.jpegbin0 -> 381894 bytes
-rw-r--r--docs/assets/images/lnav-after-pretty.pngbin0 -> 161296 bytes
-rw-r--r--docs/assets/images/lnav-before-pretty.pngbin0 -> 186997 bytes
-rw-r--r--docs/assets/images/lnav-front-page.pngbin0 -> 1140435 bytes
-rw-r--r--docs/assets/images/lnav-hist.pngbin0 -> 79899 bytes
-rw-r--r--docs/assets/images/lnav-invalid-regex-error.pngbin0 -> 79410 bytes
-rw-r--r--docs/assets/images/lnav-multi-file2.pngbin0 -> 313849 bytes
-rw-r--r--docs/assets/images/lnav-query.pngbin0 -> 86674 bytes
-rw-r--r--docs/assets/images/lnav-sql-error-msg.pngbin0 -> 36847 bytes
-rw-r--r--docs/assets/images/lnav-syntax-highlight.gifbin0 -> 26534 bytes
-rw-r--r--docs/assets/images/lnav-syslog-thumb.pngbin0 -> 82417 bytes
-rw-r--r--docs/assets/images/lnav-syslog.pngbin0 -> 334859 bytes
-rw-r--r--docs/assets/images/lnav-tab-complete.gifbin0 -> 411818 bytes
-rw-r--r--docs/assets/images/lnav-theme-cycle.gifbin0 -> 600599 bytes
-rw-r--r--docs/assets/images/lnav-vscode-extension.pngbin0 -> 678830 bytes
-rw-r--r--docs/assets/images/scrollbar-change-2.pngbin0 -> 146624 bytes
-rw-r--r--docs/assets/js/codeblock.js23
-rw-r--r--docs/assets/main.scss83
-rw-r--r--docs/conf.py.in6
-rw-r--r--docs/index.markdown58
-rw-r--r--docs/lnav-architecture.pngbin0 -> 368248 bytes
-rw-r--r--docs/lnav-tui.pngbin0 -> 156641 bytes
-rw-r--r--docs/pages/about.dox7
-rw-r--r--docs/requirements.txt6
-rw-r--r--docs/schemas/config-v1.schema.json804
-rw-r--r--docs/schemas/event-file-format-detected-v1.schema.json26
-rw-r--r--docs/schemas/event-file-open-v1.schema.json21
-rw-r--r--docs/schemas/event-log-msg-detected-v1.schema.json57
-rw-r--r--docs/schemas/event-session-loaded-v1.schema.json16
-rw-r--r--docs/schemas/format-v1.schema.json638
-rw-r--r--docs/source/_ext/__init__.py0
-rw-r--r--docs/source/_ext/lnavlexer.py26
-rw-r--r--docs/source/_static/theme_overrides.css57
-rw-r--r--docs/source/cli.rst168
-rw-r--r--docs/source/commands.rst132
-rw-r--r--docs/source/conf.py461
-rw-r--r--docs/source/config.rst270
-rw-r--r--docs/source/cookbook.rst104
-rw-r--r--docs/source/data.rst193
-rw-r--r--docs/source/docutils.conf2
-rw-r--r--docs/source/events.rst56
-rw-r--r--docs/source/faq.rst85
-rw-r--r--docs/source/filter-out-preview.pngbin0 -> 690117 bytes
-rw-r--r--docs/source/formats.rst561
-rw-r--r--docs/source/group_concat-help.pngbin0 -> 60071 bytes
-rw-r--r--docs/source/hotkey-tips.pngbin0 -> 62825 bytes
-rw-r--r--docs/source/hotkeys.rst300
-rw-r--r--docs/source/howitworks.rst13
-rw-r--r--docs/source/index.rst35
-rw-r--r--docs/source/intro.rst164
-rw-r--r--docs/source/key-encoding-prompt.pngbin0 -> 88912 bytes
-rw-r--r--docs/source/lnav-breadcrumbs-help.pngbin0 -> 45782 bytes
-rw-r--r--docs/source/lnav-config-header.pngbin0 -> 35192 bytes
-rw-r--r--docs/source/lnav-files-panel.pngbin0 -> 70248 bytes
-rw-r--r--docs/source/lnav-filters-panel.pngbin0 -> 48867 bytes
-rw-r--r--docs/source/lnav-markdown-example.pngbin0 -> 922623 bytes
-rw-r--r--docs/source/lnav-pretty-view-after.pngbin0 -> 634459 bytes
-rw-r--r--docs/source/lnav-pretty-view-before.pngbin0 -> 645281 bytes
-rw-r--r--docs/source/lnav-spectro-cpu-pct.pngbin0 -> 558558 bytes
-rw-r--r--docs/source/lnav-ui.pngbin0 -> 923959 bytes
-rw-r--r--docs/source/open-error.pngbin0 -> 21053 bytes
-rw-r--r--docs/source/open-help.pngbin0 -> 98785 bytes
-rw-r--r--docs/source/open-preview.pngbin0 -> 53409 bytes
-rw-r--r--docs/source/query-results.pngbin0 -> 35595 bytes
-rw-r--r--docs/source/sessions.rst23
-rw-r--r--docs/source/sql-help.pngbin0 -> 29721 bytes
-rw-r--r--docs/source/sqlext.rst184
-rw-r--r--docs/source/sqltab.rst263
-rw-r--r--docs/source/ui.rst327
-rw-r--r--docs/source/usage.rst291
-rw-r--r--docs/tutorials/playground/index.md9
-rw-r--r--docs/tutorials/playground/logs/access_log.gzbin0 -> 36529 bytes
-rw-r--r--docs/tutorials/playground/logs/messages.gzbin0 -> 14034 bytes
-rwxr-xr-xdocs/tutorials/playground/run.sh16
-rw-r--r--docs/tutorials/playground/text/markdown-sample.md157
-rw-r--r--docs/tutorials/tutorial-lib/configs/tutorial1/config.json18
-rw-r--r--docs/tutorials/tutorial-lib/formats/tutorial-lib/lnav-tutorial-key-handler.lnav29
-rw-r--r--docs/tutorials/tutorial-lib/formats/tutorial-lib/tutorial.sql154
-rw-r--r--docs/tutorials/tutorial1/index.md145
-rwxr-xr-xdocs/tutorials/tutorial1/run.sh16
-rw-r--r--docs/tutorials/tutorial1/tutorial1.glog100
-rwxr-xr-xexample-scripts/clipboard.sh69
-rwxr-xr-xexample-scripts/log_to_csv.sh66
-rw-r--r--example-scripts/report-demo.lnav83
-rw-r--r--example-scripts/tag-ssh-msgs.lnav10
-rw-r--r--lnav.1126
-rw-r--r--lnav.cfg543
-rw-r--r--m4/ax_ac_append_to_file.m432
-rw-r--r--m4/ax_ac_print_to_file.m432
-rw-r--r--m4/ax_add_am_macro_static.m428
-rw-r--r--m4/ax_am_macros_static.m438
-rw-r--r--m4/ax_check_gnu_make.m495
-rw-r--r--m4/ax_check_link_flag.m474
-rw-r--r--m4/ax_check_pcre2.m4165
-rw-r--r--m4/ax_code_coverage.m4272
-rw-r--r--m4/ax_cxx_compile_stdcxx.m4962
-rw-r--r--m4/ax_cxx_compile_stdcxx_11.m439
-rw-r--r--m4/ax_cxx_compile_stdcxx_14.m434
-rw-r--r--m4/ax_cxx_compile_stdcxx_17.m435
-rw-r--r--m4/ax_file_escapes.m430
-rw-r--r--m4/ax_prog_cc_for_build.m4141
-rw-r--r--m4/ax_pthread.m4507
-rw-r--r--m4/ax_with_curses.m4517
-rw-r--r--m4/libcurl.m4301
-rw-r--r--m4/lnav_common.m422
-rw-r--r--m4/lnav_with_jemalloc.m481
-rw-r--r--m4/lnav_with_libarchive.m477
-rw-r--r--m4/lnav_with_readline.m495
-rw-r--r--m4/lnav_with_sqlite3.m4127
-rw-r--r--m4/lnav_with_yajl.m487
-rw-r--r--release/Makefile105
-rw-r--r--release/README.md4
-rw-r--r--release/lnav-screenshot.terminal453
-rw-r--r--release/lnav.spec.in93
-rwxr-xr-xrelease/loggen.py225
-rwxr-xr-xrelease/makespec.sh13
-rwxr-xr-xrelease/spectrolog.py114
-rwxr-xr-xrelease/tail-demo.sh19
-rw-r--r--release/vagrant-static/Vagrantfile110
-rwxr-xr-xrelease/vagrant-static/build-pkg.sh30
-rwxr-xr-xrelease/vagrant-static/build.sh84
-rw-r--r--release/vagrant-static/musl-pkg.sh24
-rwxr-xr-xrelease/vagrant-static/pkg.sh22
-rw-r--r--release/vagrant-static/provision-pkg.sh7
-rwxr-xr-xrelease/vagrant-static/provision.sh152
-rw-r--r--snapcraft.yaml91
-rw-r--r--src/CMakeLists.txt633
-rw-r--r--src/Makefile.am548
-rw-r--r--src/all_logs_vtab.cc99
-rw-r--r--src/all_logs_vtab.hh60
-rwxr-xr-xsrc/alpha-release.sh13
-rw-r--r--src/animals.json1
-rw-r--r--src/ansi-palette.json122
-rw-r--r--src/archive_manager.cc406
-rw-r--r--src/archive_manager.cfg.hh46
-rw-r--r--src/archive_manager.hh83
-rw-r--r--src/base/CMakeLists.txt83
-rw-r--r--src/base/Makefile.am110
-rw-r--r--src/base/README.md1
-rw-r--r--src/base/ansi_scrubber.cc388
-rw-r--r--src/base/ansi_scrubber.hh75
-rw-r--r--src/base/attr_line.builder.cc30
-rw-r--r--src/base/attr_line.builder.hh119
-rw-r--r--src/base/attr_line.cc537
-rw-r--r--src/base/attr_line.hh758
-rw-r--r--src/base/attr_line.tests.cc91
-rw-r--r--src/base/auto_fd.hh318
-rw-r--r--src/base/auto_mem.hh402
-rw-r--r--src/base/auto_pid.cc63
-rw-r--r--src/base/auto_pid.hh175
-rw-r--r--src/base/bus.hh68
-rw-r--r--src/base/date_time_scanner.cc308
-rw-r--r--src/base/date_time_scanner.hh137
-rw-r--r--src/base/enum_util.hh48
-rw-r--r--src/base/file_range.hh78
-rw-r--r--src/base/fs_util.cc183
-rw-r--r--src/base/fs_util.hh122
-rw-r--r--src/base/fs_util.tests.cc56
-rw-r--r--src/base/func_util.hh159
-rw-r--r--src/base/future_util.hh111
-rw-r--r--src/base/humanize.cc110
-rw-r--r--src/base/humanize.file_size.tests.cc51
-rw-r--r--src/base/humanize.hh58
-rw-r--r--src/base/humanize.network.cc76
-rw-r--r--src/base/humanize.network.hh109
-rw-r--r--src/base/humanize.network.tests.cc118
-rw-r--r--src/base/humanize.time.cc208
-rw-r--r--src/base/humanize.time.hh88
-rw-r--r--src/base/humanize.time.tests.cc125
-rw-r--r--src/base/injector.bind.hh173
-rw-r--r--src/base/injector.hh230
-rw-r--r--src/base/intern_string.cc303
-rw-r--r--src/base/intern_string.hh865
-rw-r--r--src/base/intern_string.tests.cc152
-rw-r--r--src/base/is_utf8.cc313
-rw-r--r--src/base/is_utf8.hh59
-rw-r--r--src/base/isc.cc147
-rw-r--r--src/base/isc.hh225
-rw-r--r--src/base/itertools.hh785
-rw-r--r--src/base/lnav.console.cc494
-rw-r--r--src/base/lnav.console.hh176
-rw-r--r--src/base/lnav.console.into.hh51
-rw-r--r--src/base/lnav.gzip.cc135
-rw-r--r--src/base/lnav.gzip.hh54
-rw-r--r--src/base/lnav.gzip.tests.cc67
-rw-r--r--src/base/lnav_log.cc686
-rw-r--r--src/base/lnav_log.hh188
-rw-r--r--src/base/log_level_enum.hh65
-rw-r--r--src/base/lrucache.hpp83
-rw-r--r--src/base/math_util.hh73
-rw-r--r--src/base/network.tcp.cc75
-rw-r--r--src/base/network.tcp.hh80
-rw-r--r--src/base/network.tcp.tests.cc50
-rw-r--r--src/base/opt_util.hh105
-rw-r--r--src/base/paths.cc130
-rw-r--r--src/base/paths.hh58
-rw-r--r--src/base/result.h1032
-rw-r--r--src/base/snippet_highlighters.cc344
-rw-r--r--src/base/snippet_highlighters.hh43
-rw-r--r--src/base/string_attr_type.cc51
-rw-r--r--src/base/string_attr_type.hh679
-rw-r--r--src/base/string_util.cc304
-rw-r--r--src/base/string_util.hh232
-rw-r--r--src/base/string_util.tests.cc90
-rw-r--r--src/base/strnatcmp.c302
-rw-r--r--src/base/strnatcmp.h41
-rw-r--r--src/base/test_base.cc33
-rw-r--r--src/base/time_util.cc239
-rw-r--r--src/base/time_util.hh206
-rw-r--r--src/big_array.hh136
-rw-r--r--src/bin2c.hh74
-rw-r--r--src/bookmarks.cc89
-rw-r--r--src/bookmarks.hh209
-rw-r--r--src/bottom_status_source.cc249
-rw-r--r--src/bottom_status_source.hh94
-rw-r--r--src/bound_tags.hh54
-rw-r--r--src/breadcrumb.hh134
-rw-r--r--src/breadcrumb_curses.cc461
-rw-r--r--src/breadcrumb_curses.hh107
-rw-r--r--src/byte_array.hh148
-rw-r--r--src/collation-functions.cc155
-rw-r--r--src/column_namer.cc114
-rw-r--r--src/column_namer.hh70
-rw-r--r--src/command_executor.cc1135
-rw-r--r--src/command_executor.hh262
-rw-r--r--src/config.cmake.h.in31
-rw-r--r--src/curl_looper.cc358
-rw-r--r--src/curl_looper.hh192
-rw-r--r--src/data_parser.cc1071
-rw-r--r--src/data_parser.hh431
-rw-r--r--src/data_scanner.cc265
-rw-r--r--src/data_scanner.hh211
-rw-r--r--src/data_scanner_re.cc36096
-rw-r--r--src/data_scanner_re.re277
-rw-r--r--src/db_sub_source.cc457
-rw-r--r--src/db_sub_source.hh144
-rw-r--r--src/diseases.json549
-rw-r--r--src/doc_status_source.hh81
-rw-r--r--src/document.sections.cc544
-rw-r--r--src/document.sections.hh116
-rw-r--r--src/dump_internals.cc93
-rw-r--r--src/dump_internals.hh41
-rw-r--r--src/elem_to_json.cc222
-rw-r--r--src/elem_to_json.hh41
-rw-r--r--src/emojis.json4036
-rw-r--r--src/environ_vtab.cc338
-rw-r--r--src/environ_vtab.hh39
-rw-r--r--src/extension-functions.cc2828
-rw-r--r--src/field_overlay_source.cc574
-rw-r--r--src/field_overlay_source.hh86
-rw-r--r--src/file_collection.cc649
-rw-r--r--src/file_collection.hh180
-rw-r--r--src/file_format.cc119
-rw-r--r--src/file_format.hh76
-rw-r--r--src/file_vtab.cc345
-rw-r--r--src/file_vtab.cfg.hh43
-rw-r--r--src/files_sub_source.cc432
-rw-r--r--src/files_sub_source.hh117
-rw-r--r--src/filter_observer.cc113
-rw-r--r--src/filter_observer.hh78
-rw-r--r--src/filter_status_source.cc332
-rw-r--r--src/filter_status_source.hh82
-rw-r--r--src/filter_sub_source.cc669
-rw-r--r--src/filter_sub_source.hh92
-rw-r--r--src/fmtlib/Makefile.am22
-rw-r--r--src/fmtlib/fmt/args.h234
-rw-r--r--src/fmtlib/fmt/chrono.h2267
-rw-r--r--src/fmtlib/fmt/color.h633
-rw-r--r--src/fmtlib/fmt/compile.h607
-rw-r--r--src/fmtlib/fmt/core.h2951
-rw-r--r--src/fmtlib/fmt/format-inl.h1681
-rw-r--r--src/fmtlib/fmt/format.h4735
-rw-r--r--src/fmtlib/fmt/locale.h2
-rw-r--r--src/fmtlib/fmt/os.h451
-rw-r--r--src/fmtlib/fmt/ostream.h209
-rw-r--r--src/fmtlib/fmt/printf.h679
-rw-r--r--src/fmtlib/fmt/ranges.h732
-rw-r--r--src/fmtlib/fmt/std.h349
-rw-r--r--src/fmtlib/fmt/xchar.h259
-rw-r--r--src/fmtlib/format.cc43
-rw-r--r--src/fmtlib/os.cc390
-rw-r--r--src/format2csv.py23
-rw-r--r--src/formats/README.md5
-rw-r--r--src/formats/access_log.json117
-rw-r--r--src/formats/alb_log.json133
-rw-r--r--src/formats/block_log.json23
-rw-r--r--src/formats/bunyan_log.json105
-rw-r--r--src/formats/candlepin_log.json49
-rw-r--r--src/formats/choose_repo_log.json24
-rw-r--r--src/formats/cloudflare_log.json236
-rw-r--r--src/formats/cloudvm_ram_log.json22
-rw-r--r--src/formats/cups_log.json43
-rw-r--r--src/formats/dpkg_log.json43
-rw-r--r--src/formats/elb_log.json109
-rw-r--r--src/formats/engine_log.json34
-rw-r--r--src/formats/error_log.json67
-rw-r--r--src/formats/esx_syslog_log.json66
-rw-r--r--src/formats/formats.am46
-rw-r--r--src/formats/fsck_hfs_log.json23
-rw-r--r--src/formats/glog_log.json52
-rw-r--r--src/formats/haproxy_log.json173
-rw-r--r--src/formats/java_log.json147
-rw-r--r--src/formats/journald_json_log.json84
-rw-r--r--src/formats/katello_log.json48
-rw-r--r--src/formats/logfmt/CMakeLists.txt40
-rw-r--r--src/formats/logfmt/Makefile.am41
-rw-r--r--src/formats/logfmt/logfmt.parser.cc266
-rw-r--r--src/formats/logfmt/logfmt.parser.hh91
-rw-r--r--src/formats/logfmt/logfmt.parser.test.cc221
-rw-r--r--src/formats/openam_log.json73
-rw-r--r--src/formats/openamdb_log.json21
-rw-r--r--src/formats/openstack_log.json65
-rw-r--r--src/formats/page_log.json67
-rw-r--r--src/formats/papertrail_log.json52
-rw-r--r--src/formats/pcap_log.json82
-rw-r--r--src/formats/procstate_log.json22
-rw-r--r--src/formats/s3_log.json158
-rw-r--r--src/formats/snaplogic_log.json55
-rw-r--r--src/formats/sssd_log.json38
-rw-r--r--src/formats/strace_log.json44
-rw-r--r--src/formats/sudo_log.json48
-rw-r--r--src/formats/syslog_log.json99
-rw-r--r--src/formats/tcf_log.json51
-rw-r--r--src/formats/tcsh_history.json18
-rw-r--r--src/formats/unifi_iptables_log.json154
-rw-r--r--src/formats/unifi_log.json204
-rw-r--r--src/formats/uwsgi_log.json108
-rw-r--r--src/formats/vdsm_log.json67
-rw-r--r--src/formats/vmk_log.json51
-rw-r--r--src/formats/vmw_log.json241
-rw-r--r--src/formats/vmw_py_log.json45
-rw-r--r--src/formats/vmw_vc_svc_log.json48
-rw-r--r--src/formats/xmlrpc_log.json43
-rw-r--r--src/fs-extension-functions.cc260
-rw-r--r--src/fstat_vtab.cc368
-rw-r--r--src/fstat_vtab.hh37
-rw-r--r--src/fts_fuzzy_match.cc227
-rw-r--r--src/fts_fuzzy_match.hh50
-rw-r--r--src/ghc/filesystem.hpp6049
-rw-r--r--src/ghc/fs_fwd.hpp38
-rw-r--r--src/ghc/fs_impl.hpp35
-rw-r--r--src/ghc/fs_std.hpp60
-rw-r--r--src/ghc/fs_std_fwd.hpp63
-rw-r--r--src/ghc/fs_std_impl.hpp46
-rw-r--r--src/grep_highlighter.hh39
-rw-r--r--src/grep_proc.cc426
-rw-r--r--src/grep_proc.hh307
-rw-r--r--src/help.md536
-rw-r--r--src/help.txt1005
-rw-r--r--src/help_text.cc105
-rw-r--r--src/help_text.hh225
-rw-r--r--src/help_text_formatter.cc691
-rw-r--r--src/help_text_formatter.hh61
-rw-r--r--src/highlighter.cc146
-rw-r--r--src/highlighter.hh123
-rw-r--r--src/hist_source.cc188
-rw-r--r--src/hist_source.hh408
-rw-r--r--src/hotkeys.cc988
-rw-r--r--src/hotkeys.hh36
-rw-r--r--src/init.sql138
-rw-r--r--src/input_dispatcher.cc178
-rw-r--r--src/input_dispatcher.hh79
-rw-r--r--src/internals/README.md4
-rw-r--r--src/internals/cmd-ref.rst1628
-rw-r--r--src/internals/sql-ref.rst3850
-rw-r--r--src/itertools.similar.hh133
-rw-r--r--src/json-extension-functions.cc915
-rw-r--r--src/k_merge_tree.h471
-rw-r--r--src/keymaps/README.md5
-rw-r--r--src/keymaps/de-keymap.json51
-rw-r--r--src/keymaps/default-keymap.json172
-rw-r--r--src/keymaps/fr-keymap.json51
-rw-r--r--src/keymaps/keymaps.am9
-rw-r--r--src/keymaps/sv-keymap.json27
-rw-r--r--src/keymaps/uk-keymap.json15
-rw-r--r--src/keymaps/us-keymap.json51
-rw-r--r--src/line_buffer.cc1413
-rw-r--r--src/line_buffer.hh370
-rw-r--r--src/listview_curses.cc733
-rw-r--r--src/listview_curses.hh570
-rw-r--r--src/lnav.cc3387
-rw-r--r--src/lnav.events.cc177
-rw-r--r--src/lnav.events.hh128
-rw-r--r--src/lnav.hh286
-rw-r--r--src/lnav.indexing.cc457
-rw-r--r--src/lnav.indexing.hh45
-rw-r--r--src/lnav.management_cli.cc910
-rw-r--r--src/lnav.management_cli.hh53
-rw-r--r--src/lnav_commands.cc5769
-rw-r--r--src/lnav_commands.hh42
-rw-r--r--src/lnav_config.cc1670
-rw-r--r--src/lnav_config.hh145
-rw-r--r--src/lnav_config_fwd.hh70
-rw-r--r--src/lnav_util.cc334
-rw-r--r--src/lnav_util.hh241
-rw-r--r--src/log.watch.cc388
-rw-r--r--src/log.watch.hh45
-rw-r--r--src/log_accel.cc106
-rw-r--r--src/log_accel.hh101
-rw-r--r--src/log_actions.cc237
-rw-r--r--src/log_actions.hh67
-rw-r--r--src/log_data_helper.cc214
-rw-r--r--src/log_data_helper.hh92
-rw-r--r--src/log_data_table.cc199
-rw-r--r--src/log_data_table.hh79
-rw-r--r--src/log_format.cc3336
-rw-r--r--src/log_format.hh579
-rw-r--r--src/log_format_ext.hh434
-rw-r--r--src/log_format_fwd.hh330
-rw-r--r--src/log_format_impls.cc1775
-rw-r--r--src/log_format_loader.cc1488
-rw-r--r--src/log_format_loader.hh81
-rw-r--r--src/log_gutter_source.hh77
-rw-r--r--src/log_level.cc111
-rw-r--r--src/log_level.hh51
-rw-r--r--src/log_level_re.cc718
-rw-r--r--src/log_level_re.re103
-rw-r--r--src/log_search_table.cc270
-rw-r--r--src/log_search_table.hh83
-rw-r--r--src/log_search_table_fwd.hh40
-rw-r--r--src/log_vtab_impl.cc2174
-rw-r--r--src/log_vtab_impl.hh344
-rw-r--r--src/logfile.cc1189
-rw-r--r--src/logfile.cfg.hh45
-rw-r--r--src/logfile.hh462
-rw-r--r--src/logfile_fwd.hh160
-rw-r--r--src/logfile_stats.hh40
-rw-r--r--src/logfile_sub_source.cc2467
-rw-r--r--src/logfile_sub_source.cfg.hh49
-rw-r--r--src/logfile_sub_source.hh1021
-rw-r--r--src/mapbox/optional.hpp74
-rw-r--r--src/mapbox/recursive_wrapper.hpp122
-rw-r--r--src/mapbox/variant.hpp1053
-rw-r--r--src/mapbox/variant_cast.hpp85
-rw-r--r--src/mapbox/variant_io.hpp45
-rw-r--r--src/mapbox/variant_visitor.hpp43
-rw-r--r--src/md2attr_line.cc635
-rw-r--r--src/md2attr_line.hh91
-rw-r--r--src/md4cpp.cc297
-rw-r--r--src/md4cpp.hh144
-rw-r--r--src/network-extension-functions.cc169
-rw-r--r--src/optional.hpp1808
-rw-r--r--src/pcap_manager.cc139
-rw-r--r--src/pcap_manager.hh54
-rw-r--r--src/pcrepp/CMakeLists.txt16
-rw-r--r--src/pcrepp/Makefile.am33
-rw-r--r--src/pcrepp/pcre2pp.cc473
-rw-r--r--src/pcrepp/pcre2pp.hh368
-rw-r--r--src/pcrepp/test_pcre2pp.cc260
-rw-r--r--src/piper_proc.cc237
-rw-r--r--src/piper_proc.hh86
-rw-r--r--src/plain_text_source.cc373
-rw-r--r--src/plain_text_source.hh137
-rw-r--r--src/plugins.hh48
-rw-r--r--src/pollable.cc125
-rw-r--r--src/pollable.hh86
-rw-r--r--src/pretty_printer.cc378
-rw-r--r--src/pretty_printer.hh137
-rw-r--r--src/preview_status_source.hh84
-rw-r--r--src/ptimec.c157
-rw-r--r--src/ptimec.hh1119
-rw-r--r--src/ptimec_rt.cc186
-rw-r--r--src/pugixml/Makefile.am9
-rw-r--r--src/pugixml/pugiconfig.hpp77
-rw-r--r--src/pugixml/pugixml.cpp13029
-rw-r--r--src/pugixml/pugixml.hpp1501
-rw-r--r--src/readline_callbacks.cc908
-rw-r--r--src/readline_callbacks.hh50
-rw-r--r--src/readline_context.hh183
-rw-r--r--src/readline_curses.cc1535
-rw-r--r--src/readline_curses.hh344
-rw-r--r--src/readline_highlighters.cc480
-rw-r--r--src/readline_highlighters.hh48
-rw-r--r--src/readline_possibilities.cc510
-rw-r--r--src/readline_possibilities.hh85
-rw-r--r--src/regex101.client.cc331
-rw-r--r--src/regex101.client.hh94
-rw-r--r--src/regex101.import.cc395
-rw-r--r--src/regex101.import.hh61
-rw-r--r--src/regexp_vtab.cc644
-rw-r--r--src/regexp_vtab.hh37
-rw-r--r--src/relative_time.cc1134
-rw-r--r--src/relative_time.hh263
-rw-r--r--src/remote/CMakeLists.txt5
-rw-r--r--src/remote/Makefile.am20
-rw-r--r--src/remote/remote.ssh.cc38
-rw-r--r--src/remote/remote.ssh.hh41
-rw-r--r--src/ring_span.hh946
-rw-r--r--src/root-config.json79
-rw-r--r--src/safe/accessmode.h49
-rw-r--r--src/safe/defaulttypes.h23
-rw-r--r--src/safe/mutableref.h40
-rw-r--r--src/safe/safe.h359
-rw-r--r--src/scripts/README.md5
-rw-r--r--src/scripts/dhclient-summary.lnav23
-rwxr-xr-xsrc/scripts/dump-pid.sh13
-rw-r--r--src/scripts/lnav-pop-view.lnav21
-rw-r--r--src/scripts/partition-by-boot.lnav12
-rw-r--r--src/scripts/rename-stdin.lnav12
-rw-r--r--src/scripts/scripts.am12
-rw-r--r--src/scripts/search-for.lnav7
-rw-r--r--src/sequence_matcher.cc79
-rw-r--r--src/sequence_matcher.hh102
-rw-r--r--src/sequence_sink.hh89
-rw-r--r--src/service_tags.hh48
-rw-r--r--src/session.export.cc484
-rw-r--r--src/session.export.hh43
-rw-r--r--src/session_data.cc1864
-rw-r--r--src/session_data.hh100
-rw-r--r--src/shared_buffer.cc203
-rw-r--r--src/shared_buffer.hh217
-rw-r--r--src/shlex.cc218
-rw-r--r--src/shlex.hh222
-rw-r--r--src/shlex.resolver.hh94
-rw-r--r--src/simdutf8check.h237
-rw-r--r--src/spectro_impls.cc518
-rw-r--r--src/spectro_impls.hh87
-rw-r--r--src/spectro_source.cc615
-rw-r--r--src/spectro_source.hh208
-rw-r--r--src/spookyhash/SpookyV2.cpp350
-rw-r--r--src/spookyhash/SpookyV2.h339
-rw-r--r--src/sql_commands.cc273
-rw-r--r--src/sql_help.hh59
-rw-r--r--src/sql_util.cc1220
-rw-r--r--src/sql_util.hh136
-rw-r--r--src/sqlite-extension-func.cc1167
-rw-r--r--src/sqlite-extension-func.hh109
-rw-r--r--src/sqlitepp.cc36
-rw-r--r--src/sqlitepp.client.hh209
-rw-r--r--src/sqlitepp.hh110
-rw-r--r--src/state-extension-functions.cc177
-rw-r--r--src/static_file_vtab.cc333
-rw-r--r--src/static_file_vtab.hh39
-rw-r--r--src/statusview_curses.cc240
-rw-r--r--src/statusview_curses.hh189
-rw-r--r--src/string-extension-functions.cc1251
-rw-r--r--src/strong_int.hh116
-rw-r--r--src/styling.cc246
-rw-r--r--src/styling.hh230
-rw-r--r--src/sysclip.cc163
-rw-r--r--src/sysclip.cfg.hh84
-rw-r--r--src/sysclip.hh57
-rw-r--r--src/tailer/CMakeLists.txt24
-rw-r--r--src/tailer/Makefile.am111
-rw-r--r--src/tailer/README.md35
-rw-r--r--src/tailer/drive_tailer.cc288
-rw-r--r--src/tailer/sha-256.c161
-rw-r--r--src/tailer/sha-256.h44
-rwxr-xr-xsrc/tailer/tailer.apebin0 -> 147456 bytes
-rw-r--r--src/tailer/tailer.c100
-rw-r--r--src/tailer/tailer.h77
-rw-r--r--src/tailer/tailer.looper.cc1192
-rw-r--r--src/tailer/tailer.looper.cfg.hh53
-rw-r--r--src/tailer/tailer.looper.hh158
-rw-r--r--src/tailer/tailer.main.c1072
-rw-r--r--src/tailer/tailerpp.cc146
-rw-r--r--src/tailer/tailerpp.hh315
-rwxr-xr-xsrc/tailer/test_tailer.sh50
-rw-r--r--src/term_extra.hh119
-rw-r--r--src/termios_guard.hh79
-rw-r--r--src/test_override.c56
-rw-r--r--src/text_anonymizer.cc520
-rw-r--r--src/text_anonymizer.hh75
-rw-r--r--src/text_format.cc152
-rw-r--r--src/text_format.hh125
-rw-r--r--src/textfile_highlighters.cc464
-rw-r--r--src/textfile_highlighters.hh37
-rw-r--r--src/textfile_sub_source.cc875
-rw-r--r--src/textfile_sub_source.hh173
-rw-r--r--src/textview_curses.cc1143
-rw-r--r--src/textview_curses.hh776
-rw-r--r--src/textview_curses_fwd.hh49
-rw-r--r--src/themes/README.md5
-rw-r--r--src/themes/default-theme.json245
-rw-r--r--src/themes/eldar.json200
-rw-r--r--src/themes/grayscale.json172
-rw-r--r--src/themes/monocai.json272
-rw-r--r--src/themes/night-owl.json223
-rw-r--r--src/themes/solarized-dark.json214
-rw-r--r--src/themes/solarized-light.json214
-rw-r--r--src/themes/themes.am10
-rw-r--r--src/third-party/ArenaAlloc/arenaalloc.h186
-rw-r--r--src/third-party/ArenaAlloc/arenaallocimpl.h290
-rw-r--r--src/third-party/ArenaAlloc/recyclealloc.h184
-rw-r--r--src/third-party/CLI/App.hpp3246
-rw-r--r--src/third-party/CLI/CLI.hpp36
-rw-r--r--src/third-party/CLI/Config.hpp396
-rw-r--r--src/third-party/CLI/ConfigFwd.hpp185
-rw-r--r--src/third-party/CLI/Error.hpp355
-rw-r--r--src/third-party/CLI/Formatter.hpp292
-rw-r--r--src/third-party/CLI/FormatterFwd.hpp184
-rw-r--r--src/third-party/CLI/Macros.hpp60
-rw-r--r--src/third-party/CLI/Option.hpp1362
-rw-r--r--src/third-party/CLI/Split.hpp143
-rw-r--r--src/third-party/CLI/StringTools.hpp430
-rw-r--r--src/third-party/CLI/Timer.hpp134
-rw-r--r--src/third-party/CLI/TypeTools.hpp1558
-rw-r--r--src/third-party/CLI/Validators.hpp1175
-rw-r--r--src/third-party/CLI/Version.hpp16
-rw-r--r--src/third-party/backward-cpp/backward.hpp4460
-rw-r--r--src/third-party/base64/LICENSE28
-rw-r--r--src/third-party/base64/include/libbase64.h133
-rw-r--r--src/third-party/base64/lib/Makefile.am23
-rw-r--r--src/third-party/base64/lib/arch/avx/codec.c42
-rw-r--r--src/third-party/base64/lib/arch/avx2/codec.c42
-rw-r--r--src/third-party/base64/lib/arch/avx2/dec_loop.c110
-rw-r--r--src/third-party/base64/lib/arch/avx2/dec_reshuffle.c34
-rw-r--r--src/third-party/base64/lib/arch/avx2/enc_loop.c89
-rw-r--r--src/third-party/base64/lib/arch/avx2/enc_reshuffle.c83
-rw-r--r--src/third-party/base64/lib/arch/avx2/enc_translate.c30
-rw-r--r--src/third-party/base64/lib/arch/generic/32/dec_loop.c86
-rw-r--r--src/third-party/base64/lib/arch/generic/32/enc_loop.c73
-rw-r--r--src/third-party/base64/lib/arch/generic/64/enc_loop.c77
-rw-r--r--src/third-party/base64/lib/arch/generic/codec.c39
-rw-r--r--src/third-party/base64/lib/arch/generic/dec_head.c37
-rw-r--r--src/third-party/base64/lib/arch/generic/dec_tail.c91
-rw-r--r--src/third-party/base64/lib/arch/generic/enc_head.c24
-rw-r--r--src/third-party/base64/lib/arch/generic/enc_tail.c34
-rw-r--r--src/third-party/base64/lib/arch/neon32/codec.c77
-rw-r--r--src/third-party/base64/lib/arch/neon32/dec_loop.c106
-rw-r--r--src/third-party/base64/lib/arch/neon32/enc_loop.c169
-rw-r--r--src/third-party/base64/lib/arch/neon32/enc_reshuffle.c31
-rw-r--r--src/third-party/base64/lib/arch/neon32/enc_translate.c57
-rw-r--r--src/third-party/base64/lib/arch/neon64/codec.c92
-rw-r--r--src/third-party/base64/lib/arch/neon64/dec_loop.c129
-rw-r--r--src/third-party/base64/lib/arch/neon64/enc_loop.c133
-rw-r--r--src/third-party/base64/lib/arch/neon64/enc_reshuffle.c31
-rw-r--r--src/third-party/base64/lib/arch/sse41/codec.c42
-rw-r--r--src/third-party/base64/lib/arch/sse42/codec.c42
-rw-r--r--src/third-party/base64/lib/arch/ssse3/codec.c42
-rw-r--r--src/third-party/base64/lib/arch/ssse3/dec_loop.c173
-rw-r--r--src/third-party/base64/lib/arch/ssse3/dec_reshuffle.c33
-rw-r--r--src/third-party/base64/lib/arch/ssse3/enc_loop.c67
-rw-r--r--src/third-party/base64/lib/arch/ssse3/enc_reshuffle.c48
-rw-r--r--src/third-party/base64/lib/arch/ssse3/enc_translate.c33
-rw-r--r--src/third-party/base64/lib/codec_choose.c281
-rw-r--r--src/third-party/base64/lib/codecs.h65
-rw-r--r--src/third-party/base64/lib/config.h7
-rw-r--r--src/third-party/base64/lib/env.h74
-rw-r--r--src/third-party/base64/lib/lib.c175
-rw-r--r--src/third-party/base64/lib/lib_openmp.c149
-rw-r--r--src/third-party/base64/lib/tables/table_dec_32bit.h393
-rw-r--r--src/third-party/base64/lib/tables/table_enc_12bit.h1031
-rw-r--r--src/third-party/base64/lib/tables/tables.c40
-rw-r--r--src/third-party/base64/lib/tables/tables.h23
-rw-r--r--src/third-party/doctest-root/doctest/doctest.h7019
-rw-r--r--src/third-party/intervaltree/IntervalTree.h346
-rw-r--r--src/third-party/md4c/md4c.c6410
-rw-r--r--src/third-party/md4c/md4c.h405
-rw-r--r--src/third-party/rapidyaml/ryml_all.hpp30945
-rw-r--r--src/third-party/robin_hood/robin_hood.h2544
-rw-r--r--src/third-party/scnlib/include/scn/all.h26
-rw-r--r--src/third-party/scnlib/include/scn/detail/args.h619
-rw-r--r--src/third-party/scnlib/include/scn/detail/config.h466
-rw-r--r--src/third-party/scnlib/include/scn/detail/context.h126
-rw-r--r--src/third-party/scnlib/include/scn/detail/error.h136
-rw-r--r--src/third-party/scnlib/include/scn/detail/file.h568
-rw-r--r--src/third-party/scnlib/include/scn/detail/fwd.h204
-rw-r--r--src/third-party/scnlib/include/scn/detail/locale.h595
-rw-r--r--src/third-party/scnlib/include/scn/detail/parse_context.h581
-rw-r--r--src/third-party/scnlib/include/scn/detail/range.h598
-rw-r--r--src/third-party/scnlib/include/scn/detail/result.h595
-rw-r--r--src/third-party/scnlib/include/scn/detail/vectored.h166
-rw-r--r--src/third-party/scnlib/include/scn/detail/visitor.h248
-rw-r--r--src/third-party/scnlib/include/scn/fwd.h23
-rw-r--r--src/third-party/scnlib/include/scn/istream.h23
-rw-r--r--src/third-party/scnlib/include/scn/ranges/custom_impl.h1632
-rw-r--r--src/third-party/scnlib/include/scn/ranges/ranges.h49
-rw-r--r--src/third-party/scnlib/include/scn/ranges/std_impl.h67
-rw-r--r--src/third-party/scnlib/include/scn/ranges/util.h419
-rw-r--r--src/third-party/scnlib/include/scn/reader/common.h1663
-rw-r--r--src/third-party/scnlib/include/scn/reader/float.h246
-rw-r--r--src/third-party/scnlib/include/scn/reader/int.h537
-rw-r--r--src/third-party/scnlib/include/scn/reader/reader.h111
-rw-r--r--src/third-party/scnlib/include/scn/reader/string.h1336
-rw-r--r--src/third-party/scnlib/include/scn/reader/types.h220
-rw-r--r--src/third-party/scnlib/include/scn/scan/common.h131
-rw-r--r--src/third-party/scnlib/include/scn/scan/getline.h186
-rw-r--r--src/third-party/scnlib/include/scn/scan/ignore.h189
-rw-r--r--src/third-party/scnlib/include/scn/scan/istream.h147
-rw-r--r--src/third-party/scnlib/include/scn/scan/list.h450
-rw-r--r--src/third-party/scnlib/include/scn/scan/scan.h444
-rw-r--r--src/third-party/scnlib/include/scn/scan/vscan.h208
-rw-r--r--src/third-party/scnlib/include/scn/scn.h26
-rw-r--r--src/third-party/scnlib/include/scn/tuple_return.h23
-rw-r--r--src/third-party/scnlib/include/scn/tuple_return/tuple_return.h123
-rw-r--r--src/third-party/scnlib/include/scn/tuple_return/util.h176
-rw-r--r--src/third-party/scnlib/include/scn/unicode/common.h139
-rw-r--r--src/third-party/scnlib/include/scn/unicode/unicode.h243
-rw-r--r--src/third-party/scnlib/include/scn/unicode/utf16.h139
-rw-r--r--src/third-party/scnlib/include/scn/unicode/utf8.h297
-rw-r--r--src/third-party/scnlib/include/scn/util/algorithm.h80
-rw-r--r--src/third-party/scnlib/include/scn/util/array.h105
-rw-r--r--src/third-party/scnlib/include/scn/util/expected.h158
-rw-r--r--src/third-party/scnlib/include/scn/util/math.h121
-rw-r--r--src/third-party/scnlib/include/scn/util/memory.h404
-rw-r--r--src/third-party/scnlib/include/scn/util/meta.h77
-rw-r--r--src/third-party/scnlib/include/scn/util/optional.h105
-rw-r--r--src/third-party/scnlib/include/scn/util/small_vector.h788
-rw-r--r--src/third-party/scnlib/include/scn/util/span.h240
-rw-r--r--src/third-party/scnlib/include/scn/util/string_view.h270
-rw-r--r--src/third-party/scnlib/include/scn/util/unique_ptr.h118
-rw-r--r--src/third-party/scnlib/licenses/README.md25
-rw-r--r--src/third-party/scnlib/licenses/fast_float-apache.txt190
-rw-r--r--src/third-party/scnlib/licenses/fast_float-mit.txt27
-rw-r--r--src/third-party/scnlib/licenses/fmt.rst27
-rw-r--r--src/third-party/scnlib/licenses/nanorange.txt23
-rw-r--r--src/third-party/scnlib/licenses/utfcpp.txt23
-rw-r--r--src/third-party/scnlib/src/Makefile.am65
-rw-r--r--src/third-party/scnlib/src/deps/fast_float/single_include/fast_float/fast_float.h2981
-rw-r--r--src/third-party/scnlib/src/file.cpp311
-rw-r--r--src/third-party/scnlib/src/locale.cpp668
-rw-r--r--src/third-party/scnlib/src/reader_float.cpp433
-rw-r--r--src/third-party/scnlib/src/reader_int.cpp372
-rw-r--r--src/third-party/scnlib/src/vscan.cpp80
-rw-r--r--src/third-party/sqlite/ext/dbdump.c730
-rw-r--r--src/third-party/sqlite/ext/series.c448
-rw-r--r--src/third-party/xxHash/xxh_x86dispatch.c770
-rw-r--r--src/third-party/xxHash/xxh_x86dispatch.h85
-rw-r--r--src/third-party/xxHash/xxhash.c43
-rw-r--r--src/third-party/xxHash/xxhash.h6075
-rw-r--r--src/time-extension-functions.cc266
-rw-r--r--src/time_T.hh50
-rw-r--r--src/time_formats.am63
-rw-r--r--src/timer.cc121
-rw-r--r--src/timer.hh68
-rw-r--r--src/top_status_source.cc122
-rw-r--r--src/top_status_source.cfg.hh37
-rw-r--r--src/top_status_source.hh79
-rw-r--r--src/top_sys_status_source.hh80
-rw-r--r--src/unique_path.cc129
-rw-r--r--src/unique_path.hh88
-rw-r--r--src/url_loader.hh150
-rw-r--r--src/view_curses.cc1247
-rw-r--r--src/view_curses.hh482
-rw-r--r--src/view_helpers.cc1214
-rw-r--r--src/view_helpers.crumbs.hh37
-rw-r--r--src/view_helpers.examples.hh41
-rw-r--r--src/view_helpers.hh101
-rw-r--r--src/view_helpers.hist.hh53
-rw-r--r--src/views_vtab.cc1113
-rw-r--r--src/views_vtab.hh39
-rw-r--r--src/vis_line.hh43
-rw-r--r--src/vt52_curses.cc314
-rw-r--r--src/vt52_curses.hh169
-rw-r--r--src/vtab_module.cc109
-rw-r--r--src/vtab_module.hh929
-rw-r--r--src/vtab_module_json.hh58
-rw-r--r--src/words.json1
-rw-r--r--src/ww898/cp_utf8.hpp171
-rw-r--r--src/xml-entities.json2233
-rw-r--r--src/xml_util.cc81
-rw-r--r--src/xml_util.hh45
-rw-r--r--src/xpath_vtab.cc393
-rw-r--r--src/xpath_vtab.hh37
-rw-r--r--src/xterm-palette.json3842
-rw-r--r--src/xterm_mouse.cc98
-rw-r--r--src/xterm_mouse.hh131
-rw-r--r--src/yajl/CMakeLists.txt27
-rw-r--r--src/yajl/Makefile.am37
-rw-r--r--src/yajl/api/yajl_common.h75
-rw-r--r--src/yajl/api/yajl_gen.h165
-rw-r--r--src/yajl/api/yajl_parse.h228
-rw-r--r--src/yajl/api/yajl_tree.h186
-rw-r--r--src/yajl/yajl.c189
-rw-r--r--src/yajl/yajl_alloc.c52
-rw-r--r--src/yajl/yajl_alloc.h34
-rw-r--r--src/yajl/yajl_buf.c103
-rw-r--r--src/yajl/yajl_buf.h57
-rw-r--r--src/yajl/yajl_bytestack.h69
-rw-r--r--src/yajl/yajl_common.h75
-rw-r--r--src/yajl/yajl_encode.c220
-rw-r--r--src/yajl/yajl_encode.h34
-rw-r--r--src/yajl/yajl_gen.c362
-rw-r--r--src/yajl/yajl_lex.c763
-rw-r--r--src/yajl/yajl_lex.h117
-rw-r--r--src/yajl/yajl_parser.c498
-rw-r--r--src/yajl/yajl_parser.h78
-rw-r--r--src/yajl/yajl_tree.c503
-rw-r--r--src/yajl/yajl_version.c7
-rw-r--r--src/yajl/yajl_version.h23
-rw-r--r--src/yajlpp/CMakeLists.txt27
-rw-r--r--src/yajlpp/Makefile.am81
-rw-r--r--src/yajlpp/README.md5
-rw-r--r--src/yajlpp/drive_json_op.cc210
-rw-r--r--src/yajlpp/drive_json_ptr_walk.cc103
-rw-r--r--src/yajlpp/json_op.cc316
-rw-r--r--src/yajlpp/json_op.hh82
-rw-r--r--src/yajlpp/json_ptr.cc521
-rw-r--r--src/yajlpp/json_ptr.hh136
-rwxr-xr-xsrc/yajlpp/test_json_op.sh114
-rw-r--r--src/yajlpp/test_json_ptr.cc73
-rw-r--r--src/yajlpp/test_json_ptr_walk.sh77
-rw-r--r--src/yajlpp/test_yajlpp.cc166
-rw-r--r--src/yajlpp/yajlpp.cc1574
-rw-r--r--src/yajlpp/yajlpp.hh682
-rw-r--r--src/yajlpp/yajlpp_def.hh1408
-rw-r--r--src/yaml-extension-functions.cc103
-rw-r--r--test/CMakeLists.txt88
-rw-r--r--test/Makefile.am523
-rw-r--r--test/UTF-8-test.txtbin0 -> 22781 bytes
-rw-r--r--test/aftest.cc63
-rw-r--r--test/ansi-colors.0.in22
-rw-r--r--test/bad-config-json/formats/invalid-json/format.json5
-rw-r--r--test/bad-config-json/formats/invalid-key/format.json27
-rw-r--r--test/bad-config/formats/invalid-json-format/format.json17
-rw-r--r--test/bad-config/formats/invalid-name/format.json8
-rw-r--r--test/bad-config/formats/invalid-properties/format.json45
-rw-r--r--test/bad-config/formats/invalid-regex/format.json29
-rw-r--r--test/bad-config/formats/invalid-sample/format.json49
-rw-r--r--test/bad-config/formats/invalid-schema/format.json3
-rw-r--r--test/bad-config/formats/invalid-sql/init.sql5
-rw-r--r--test/bad-config/formats/invalid-sql/init2.sql2
-rw-r--r--test/bad-config/formats/no-regexes/format.json7
-rw-r--r--test/bad-config/formats/no-samples/format.json17
-rw-r--r--test/bad-config2/formats/invalid-config/config.bad-schema.json3
-rw-r--r--test/bad-config2/formats/invalid-config/config.json5
-rw-r--r--test/bad-config2/formats/invalid-config/config.malformed.json5
-rw-r--r--test/bad-config2/formats/invalid-config/config.truncated.json2
-rw-r--r--test/books.xml120
-rw-r--r--test/datafile_ipaddr.013
-rw-r--r--test/datafile_simple.025
-rw-r--r--test/datafile_simple.113
-rw-r--r--test/datafile_simple.109
-rw-r--r--test/datafile_simple.1113
-rw-r--r--test/datafile_simple.1210
-rw-r--r--test/datafile_simple.1310
-rw-r--r--test/datafile_simple.1457
-rw-r--r--test/datafile_simple.1575
-rw-r--r--test/datafile_simple.169
-rw-r--r--test/datafile_simple.1722
-rw-r--r--test/datafile_simple.1823
-rw-r--r--test/datafile_simple.1953
-rw-r--r--test/datafile_simple.240
-rw-r--r--test/datafile_simple.2017
-rw-r--r--test/datafile_simple.2126
-rw-r--r--test/datafile_simple.229
-rw-r--r--test/datafile_simple.2388
-rw-r--r--test/datafile_simple.325
-rw-r--r--test/datafile_simple.410
-rw-r--r--test/datafile_simple.510
-rw-r--r--test/datafile_simple.625
-rw-r--r--test/datafile_simple.718
-rw-r--r--test/datafile_simple.844
-rw-r--r--test/datafile_simple.917
-rw-r--r--test/datafile_syslog.023
-rw-r--r--test/datafile_syslog.121
-rw-r--r--test/datafile_xml.026
-rw-r--r--test/dhcp-trunc.pcapngbin0 -> 253 bytes
-rw-r--r--test/dhcp.pcapngbin0 -> 1508 bytes
-rw-r--r--test/document.sections.tests.cc170
-rw-r--r--test/drive_data_scanner.cc312
-rw-r--r--test/drive_grep_proc.cc156
-rw-r--r--test/drive_line_buffer.cc246
-rw-r--r--test/drive_listview.cc152
-rw-r--r--test/drive_logfile.cc194
-rw-r--r--test/drive_mvwattrline.cc119
-rw-r--r--test/drive_readline_curses.cc153
-rw-r--r--test/drive_sequencer.cc150
-rw-r--r--test/drive_shlexer.cc96
-rw-r--r--test/drive_sql.cc78
-rw-r--r--test/drive_sql_anno.cc89
-rw-r--r--test/drive_view_colors.cc106
-rw-r--r--test/drive_vt52_curses.cc129
-rw-r--r--test/expected/expected.am1021
-rw-r--r--test/expected/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.err0
-rw-r--r--test/expected/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.out1
-rw-r--r--test/expected/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.err0
-rw-r--r--test/expected/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.out1
-rw-r--r--test/expected/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.err8
-rw-r--r--test/expected/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.out0
-rw-r--r--test/expected/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.err0
-rw-r--r--test/expected/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.out3
-rw-r--r--test/expected/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.err0
-rw-r--r--test/expected/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.out2
-rw-r--r--test/expected/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.err6
-rw-r--r--test/expected/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.out0
-rw-r--r--test/expected/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.err0
-rw-r--r--test/expected/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.out3
-rw-r--r--test/expected/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.err0
-rw-r--r--test/expected/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.out3
-rw-r--r--test/expected/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.err0
-rw-r--r--test/expected/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.out3
-rw-r--r--test/expected/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.err0
-rw-r--r--test/expected/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.out2
-rw-r--r--test/expected/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.err0
-rw-r--r--test/expected/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.out1
-rw-r--r--test/expected/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.err7
-rw-r--r--test/expected/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.out0
-rw-r--r--test/expected/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.err0
-rw-r--r--test/expected/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.out2
-rw-r--r--test/expected/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.err0
-rw-r--r--test/expected/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.out2
-rw-r--r--test/expected/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.err0
-rw-r--r--test/expected/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.out3
-rw-r--r--test/expected/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.err0
-rw-r--r--test/expected/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.out68
-rw-r--r--test/expected/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.err6
-rw-r--r--test/expected/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.out0
-rw-r--r--test/expected/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.err0
-rw-r--r--test/expected/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.out3
-rw-r--r--test/expected/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.err7
-rw-r--r--test/expected/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.out0
-rw-r--r--test/expected/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.err0
-rw-r--r--test/expected/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.out2
-rw-r--r--test/expected/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.err0
-rw-r--r--test/expected/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.out12
-rw-r--r--test/expected/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.err0
-rw-r--r--test/expected/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.out0
-rw-r--r--test/expected/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.err0
-rw-r--r--test/expected/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.out1
-rw-r--r--test/expected/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.err0
-rw-r--r--test/expected/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.out1
-rw-r--r--test/expected/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.err0
-rw-r--r--test/expected/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.out2
-rw-r--r--test/expected/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.err6
-rw-r--r--test/expected/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.out0
-rw-r--r--test/expected/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.err0
-rw-r--r--test/expected/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.out2
-rw-r--r--test/expected/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.err0
-rw-r--r--test/expected/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.out1
-rw-r--r--test/expected/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.err0
-rw-r--r--test/expected/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.out23
-rw-r--r--test/expected/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.err0
-rw-r--r--test/expected/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.out1
-rw-r--r--test/expected/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.err0
-rw-r--r--test/expected/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.out1
-rw-r--r--test/expected/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.err0
-rw-r--r--test/expected/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.out2
-rw-r--r--test/expected/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.err0
-rw-r--r--test/expected/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.out1
-rw-r--r--test/expected/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.err0
-rw-r--r--test/expected/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.out3
-rw-r--r--test/expected/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.err7
-rw-r--r--test/expected/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.out0
-rw-r--r--test/expected/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.err0
-rw-r--r--test/expected/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.out3
-rw-r--r--test/expected/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.err0
-rw-r--r--test/expected/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.out2
-rw-r--r--test/expected/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.err0
-rw-r--r--test/expected/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.out2
-rw-r--r--test/expected/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.err0
-rw-r--r--test/expected/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.out2
-rw-r--r--test/expected/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.err0
-rw-r--r--test/expected/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.out3
-rw-r--r--test/expected/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.err0
-rw-r--r--test/expected/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.out3
-rw-r--r--test/expected/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.err5
-rw-r--r--test/expected/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.out0
-rw-r--r--test/expected/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.err0
-rw-r--r--test/expected/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.out2
-rw-r--r--test/expected/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.err4
-rw-r--r--test/expected/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.out0
-rw-r--r--test/expected/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.err0
-rw-r--r--test/expected/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.out2
-rw-r--r--test/expected/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.err0
-rw-r--r--test/expected/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.out2
-rw-r--r--test/expected/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.err0
-rw-r--r--test/expected/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.out1
-rw-r--r--test/expected/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.err0
-rw-r--r--test/expected/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.out14
-rw-r--r--test/expected/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.err8
-rw-r--r--test/expected/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.out0
-rw-r--r--test/expected/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.err4
-rw-r--r--test/expected/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.out0
-rw-r--r--test/expected/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.err0
-rw-r--r--test/expected/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.out1
-rw-r--r--test/expected/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.err0
-rw-r--r--test/expected/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.out3
-rw-r--r--test/expected/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.err0
-rw-r--r--test/expected/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.out5
-rw-r--r--test/expected/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.err6
-rw-r--r--test/expected/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.out0
-rw-r--r--test/expected/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.err0
-rw-r--r--test/expected/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.out1
-rw-r--r--test/expected/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.err0
-rw-r--r--test/expected/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.out3
-rw-r--r--test/expected/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.err0
-rw-r--r--test/expected/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.out3
-rw-r--r--test/expected/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.err0
-rw-r--r--test/expected/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.out1
-rw-r--r--test/expected/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.err0
-rw-r--r--test/expected/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.out1
-rw-r--r--test/expected/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.err0
-rw-r--r--test/expected/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.out1
-rw-r--r--test/expected/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.err6
-rw-r--r--test/expected/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.out0
-rw-r--r--test/expected/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.err0
-rw-r--r--test/expected/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.out37
-rw-r--r--test/expected/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.err6
-rw-r--r--test/expected/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.out0
-rw-r--r--test/expected/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.err0
-rw-r--r--test/expected/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.out2
-rw-r--r--test/expected/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.err0
-rw-r--r--test/expected/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.out3
-rw-r--r--test/expected/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.err0
-rw-r--r--test/expected/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.out2
-rw-r--r--test/expected/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.err0
-rw-r--r--test/expected/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.out1
-rw-r--r--test/expected/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.err0
-rw-r--r--test/expected/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.out4
-rw-r--r--test/expected/test_cmds.sh_a0e6214b2a85c90d31aee12efde850441cca7eb3.err0
-rw-r--r--test/expected/test_cmds.sh_a0e6214b2a85c90d31aee12efde850441cca7eb3.out2
-rw-r--r--test/expected/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.err0
-rw-r--r--test/expected/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.out2
-rw-r--r--test/expected/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.err3
-rw-r--r--test/expected/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.out0
-rw-r--r--test/expected/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.err6
-rw-r--r--test/expected/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.out0
-rw-r--r--test/expected/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.err0
-rw-r--r--test/expected/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.out10
-rw-r--r--test/expected/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.err7
-rw-r--r--test/expected/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.out0
-rw-r--r--test/expected/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.err0
-rw-r--r--test/expected/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.out12
-rw-r--r--test/expected/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.err0
-rw-r--r--test/expected/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.out1
-rw-r--r--test/expected/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.err0
-rw-r--r--test/expected/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.out1
-rw-r--r--test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.err0
-rw-r--r--test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out4422
-rw-r--r--test/expected/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.err0
-rw-r--r--test/expected/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.out37
-rw-r--r--test/expected/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.err0
-rw-r--r--test/expected/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.out1
-rw-r--r--test/expected/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.err0
-rw-r--r--test/expected/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.out2
-rw-r--r--test/expected/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.err0
-rw-r--r--test/expected/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.out1
-rw-r--r--test/expected/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.err0
-rw-r--r--test/expected/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.out1
-rw-r--r--test/expected/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.err0
-rw-r--r--test/expected/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.out1
-rw-r--r--test/expected/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.err7
-rw-r--r--test/expected/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.out0
-rw-r--r--test/expected/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.err0
-rw-r--r--test/expected/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.out5
-rw-r--r--test/expected/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.err7
-rw-r--r--test/expected/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.out0
-rw-r--r--test/expected/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.err0
-rw-r--r--test/expected/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.out3
-rw-r--r--test/expected/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.err0
-rw-r--r--test/expected/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.out1
-rw-r--r--test/expected/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.err6
-rw-r--r--test/expected/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.out0
-rw-r--r--test/expected/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.err0
-rw-r--r--test/expected/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.out2
-rw-r--r--test/expected/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.err0
-rw-r--r--test/expected/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.out2
-rw-r--r--test/expected/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.err0
-rw-r--r--test/expected/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.out1
-rw-r--r--test/expected/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.err0
-rw-r--r--test/expected/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.out3
-rw-r--r--test/expected/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.err0
-rw-r--r--test/expected/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.out3
-rw-r--r--test/expected/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.err7
-rw-r--r--test/expected/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.out0
-rw-r--r--test/expected/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.err0
-rw-r--r--test/expected/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.out2
-rw-r--r--test/expected/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.err0
-rw-r--r--test/expected/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.out1
-rw-r--r--test/expected/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.err0
-rw-r--r--test/expected/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.out1
-rw-r--r--test/expected/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.err0
-rw-r--r--test/expected/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.out3
-rw-r--r--test/expected/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.err0
-rw-r--r--test/expected/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.out3
-rw-r--r--test/expected/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.err0
-rw-r--r--test/expected/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.out2
-rw-r--r--test/expected/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.err0
-rw-r--r--test/expected/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.out12
-rw-r--r--test/expected/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.err0
-rw-r--r--test/expected/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.out2
-rw-r--r--test/expected/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.err0
-rw-r--r--test/expected/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.out3
-rw-r--r--test/expected/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.err6
-rw-r--r--test/expected/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.out0
-rw-r--r--test/expected/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.err7
-rw-r--r--test/expected/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.out0
-rw-r--r--test/expected/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.err0
-rw-r--r--test/expected/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.out3
-rw-r--r--test/expected/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.err0
-rw-r--r--test/expected/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.out2
-rw-r--r--test/expected/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.err6
-rw-r--r--test/expected/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.out0
-rw-r--r--test/expected/test_config.sh_5105c29004e297521310ca0bd0fd560b01c2c549.err20
-rw-r--r--test/expected/test_config.sh_5105c29004e297521310ca0bd0fd560b01c2c549.out0
-rw-r--r--test/expected/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.err6
-rw-r--r--test/expected/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.out0
-rw-r--r--test/expected/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.err38
-rw-r--r--test/expected/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.out0
-rw-r--r--test/expected/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.err0
-rw-r--r--test/expected/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.out0
-rw-r--r--test/expected/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.err12
-rw-r--r--test/expected/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.out0
-rw-r--r--test/expected/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.err8
-rw-r--r--test/expected/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.out0
-rw-r--r--test/expected/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.err0
-rw-r--r--test/expected/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.out1
-rw-r--r--test/expected/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.err0
-rw-r--r--test/expected/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.out0
-rw-r--r--test/expected/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.err0
-rw-r--r--test/expected/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.out6
-rw-r--r--test/expected/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.err0
-rw-r--r--test/expected/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.out2
-rw-r--r--test/expected/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.err0
-rw-r--r--test/expected/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.out0
-rw-r--r--test/expected/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.err0
-rw-r--r--test/expected/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.out3
-rw-r--r--test/expected/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.err10
-rw-r--r--test/expected/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.out0
-rw-r--r--test/expected/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.err0
-rw-r--r--test/expected/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.out3
-rw-r--r--test/expected/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.err0
-rw-r--r--test/expected/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.out9
-rw-r--r--test/expected/test_format_loader.sh_3f1d6f35e8a9ae4fd3e91ffaa82a037b5a847ab7.err171
-rw-r--r--test/expected/test_format_loader.sh_3f1d6f35e8a9ae4fd3e91ffaa82a037b5a847ab7.out0
-rw-r--r--test/expected/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.err215
-rw-r--r--test/expected/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.out0
-rw-r--r--test/expected/test_format_loader.sh_a47f2b090a5d8a226783835c7ff7d1c8821f11ed.err61
-rw-r--r--test/expected/test_format_loader.sh_a47f2b090a5d8a226783835c7ff7d1c8821f11ed.out0
-rw-r--r--test/expected/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.err66
-rw-r--r--test/expected/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.out0
-rw-r--r--test/expected/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.err0
-rw-r--r--test/expected/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.out13
-rw-r--r--test/expected/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.err0
-rw-r--r--test/expected/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.out14
-rw-r--r--test/expected/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.err0
-rw-r--r--test/expected/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.out205
-rw-r--r--test/expected/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.err0
-rw-r--r--test/expected/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.out3
-rw-r--r--test/expected/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.err0
-rw-r--r--test/expected/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.out29
-rw-r--r--test/expected/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.err0
-rw-r--r--test/expected/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.out42
-rw-r--r--test/expected/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.err0
-rw-r--r--test/expected/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.out4
-rw-r--r--test/expected/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.err0
-rw-r--r--test/expected/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.out29
-rw-r--r--test/expected/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.err0
-rw-r--r--test/expected/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.out1
-rw-r--r--test/expected/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.err0
-rw-r--r--test/expected/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.out12
-rw-r--r--test/expected/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.err0
-rw-r--r--test/expected/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.out8
-rw-r--r--test/expected/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.err0
-rw-r--r--test/expected/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.out2
-rw-r--r--test/expected/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.err0
-rw-r--r--test/expected/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.out14
-rw-r--r--test/expected/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.err0
-rw-r--r--test/expected/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.out21
-rw-r--r--test/expected/test_json_format.sh_90a037c7d9d70ac4ca97158271ea242787313377.err0
-rw-r--r--test/expected/test_json_format.sh_90a037c7d9d70ac4ca97158271ea242787313377.out3
-rw-r--r--test/expected/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.err0
-rw-r--r--test/expected/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.out4
-rw-r--r--test/expected/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.err0
-rw-r--r--test/expected/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.out4
-rw-r--r--test/expected/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.err0
-rw-r--r--test/expected/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.out29
-rw-r--r--test/expected/test_json_format.sh_a6be47f1311ed92feaf303142fcb103deb80f456.err0
-rw-r--r--test/expected/test_json_format.sh_a6be47f1311ed92feaf303142fcb103deb80f456.out4
-rw-r--r--test/expected/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.err0
-rw-r--r--test/expected/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.out1
-rw-r--r--test/expected/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.err0
-rw-r--r--test/expected/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.out2
-rw-r--r--test/expected/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.err0
-rw-r--r--test/expected/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.out49
-rw-r--r--test/expected/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.err0
-rw-r--r--test/expected/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.out4
-rw-r--r--test/expected/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.err0
-rw-r--r--test/expected/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.out3
-rw-r--r--test/expected/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.err0
-rw-r--r--test/expected/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.out4
-rw-r--r--test/expected/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.err0
-rw-r--r--test/expected/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.out2
-rw-r--r--test/expected/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.err0
-rw-r--r--test/expected/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.out2
-rw-r--r--test/expected/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.err0
-rw-r--r--test/expected/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.out4
-rw-r--r--test/expected/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.err0
-rw-r--r--test/expected/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.out4
-rw-r--r--test/expected/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.err0
-rw-r--r--test/expected/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.out3
-rw-r--r--test/expected/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.err0
-rw-r--r--test/expected/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.out1
-rw-r--r--test/expected/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.err0
-rw-r--r--test/expected/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.out2
-rw-r--r--test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.err0
-rw-r--r--test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.out2
-rw-r--r--test/expected/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.err0
-rw-r--r--test/expected/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.out2
-rw-r--r--test/expected/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.err0
-rw-r--r--test/expected/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.out2
-rw-r--r--test/expected/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.err0
-rw-r--r--test/expected/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.out4
-rw-r--r--test/expected/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.err0
-rw-r--r--test/expected/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.out10
-rw-r--r--test/expected/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.err0
-rw-r--r--test/expected/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.out6
-rw-r--r--test/expected/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.err0
-rw-r--r--test/expected/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.out1
-rw-r--r--test/expected/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.err0
-rw-r--r--test/expected/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.out3
-rw-r--r--test/expected/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.err0
-rw-r--r--test/expected/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.out11
-rw-r--r--test/expected/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.err7
-rw-r--r--test/expected/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.out0
-rw-r--r--test/expected/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.err11
-rw-r--r--test/expected/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.out0
-rw-r--r--test/expected/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.err0
-rw-r--r--test/expected/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.out2
-rw-r--r--test/expected/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.err0
-rw-r--r--test/expected/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.out5
-rw-r--r--test/expected/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.err0
-rw-r--r--test/expected/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.out6
-rw-r--r--test/expected/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.err0
-rw-r--r--test/expected/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.out3
-rw-r--r--test/expected/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.err0
-rw-r--r--test/expected/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.out2
-rw-r--r--test/expected/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.err0
-rw-r--r--test/expected/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.out3
-rw-r--r--test/expected/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.err0
-rw-r--r--test/expected/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.out37
-rw-r--r--test/expected/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.err0
-rw-r--r--test/expected/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.out4
-rw-r--r--test/expected/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.err0
-rw-r--r--test/expected/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.out3
-rw-r--r--test/expected/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.err0
-rw-r--r--test/expected/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.out4
-rw-r--r--test/expected/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.err0
-rw-r--r--test/expected/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.out4
-rw-r--r--test/expected/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.err0
-rw-r--r--test/expected/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.out5
-rw-r--r--test/expected/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.err0
-rw-r--r--test/expected/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.out4
-rw-r--r--test/expected/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.err0
-rw-r--r--test/expected/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.out4
-rw-r--r--test/expected/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.err0
-rw-r--r--test/expected/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.out8
-rw-r--r--test/expected/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.err0
-rw-r--r--test/expected/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.out4
-rw-r--r--test/expected/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.err0
-rw-r--r--test/expected/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.out5
-rw-r--r--test/expected/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.err0
-rw-r--r--test/expected/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.out5
-rw-r--r--test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.err0
-rw-r--r--test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.out3
-rw-r--r--test/expected/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.err0
-rw-r--r--test/expected/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.out19
-rw-r--r--test/expected/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.err0
-rw-r--r--test/expected/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.out3
-rw-r--r--test/expected/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.err0
-rw-r--r--test/expected/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.out1
-rw-r--r--test/expected/test_regex101.sh_0fa3663a45aca6a328cb728872af7ed7ee896f1c.err2
-rw-r--r--test/expected/test_regex101.sh_0fa3663a45aca6a328cb728872af7ed7ee896f1c.out0
-rw-r--r--test/expected/test_regex101.sh_182ae9244db314a953af2bee969726e381bc5a32.err3
-rw-r--r--test/expected/test_regex101.sh_182ae9244db314a953af2bee969726e381bc5a32.out0
-rw-r--r--test/expected/test_regex101.sh_2158f1f011ba8e1b152396c072790c076fdb8ce8.err3
-rw-r--r--test/expected/test_regex101.sh_2158f1f011ba8e1b152396c072790c076fdb8ce8.out1
-rw-r--r--test/expected/test_regex101.sh_281af24141680330791db7f7c5fa70833ce08a6b.err1
-rw-r--r--test/expected/test_regex101.sh_281af24141680330791db7f7c5fa70833ce08a6b.out0
-rw-r--r--test/expected/test_regex101.sh_35703b13990785632cca82123fb3883797959c0b.err0
-rw-r--r--test/expected/test_regex101.sh_35703b13990785632cca82123fb3883797959c0b.out4
-rw-r--r--test/expected/test_regex101.sh_366730cac50b4a09b7de4b84641791470b1cb9a3.err0
-rw-r--r--test/expected/test_regex101.sh_366730cac50b4a09b7de4b84641791470b1cb9a3.out10
-rw-r--r--test/expected/test_regex101.sh_3d18474a3e472fff6e23e0c41337ec9188fee591.err34
-rw-r--r--test/expected/test_regex101.sh_3d18474a3e472fff6e23e0c41337ec9188fee591.out3
-rw-r--r--test/expected/test_regex101.sh_442cc58676590a3604d5c2183f5fe0a75c98351a.err0
-rw-r--r--test/expected/test_regex101.sh_442cc58676590a3604d5c2183f5fe0a75c98351a.out2
-rw-r--r--test/expected/test_regex101.sh_566fd88d216a44bc1c6e23f2d6f2d0caf99d42f9.err0
-rw-r--r--test/expected/test_regex101.sh_566fd88d216a44bc1c6e23f2d6f2d0caf99d42f9.out1
-rw-r--r--test/expected/test_regex101.sh_5f2f7ecb6ab9cbec4b41385b91bd038906b8a7b2.err3
-rw-r--r--test/expected/test_regex101.sh_5f2f7ecb6ab9cbec4b41385b91bd038906b8a7b2.out0
-rw-r--r--test/expected/test_regex101.sh_629bde30483e0a6461076e9058f3a5eb81ae0425.err3
-rw-r--r--test/expected/test_regex101.sh_629bde30483e0a6461076e9058f3a5eb81ae0425.out0
-rw-r--r--test/expected/test_regex101.sh_630db454054cf92ec9bd0f4e3e83300047f583ff.err0
-rw-r--r--test/expected/test_regex101.sh_630db454054cf92ec9bd0f4e3e83300047f583ff.out4
-rw-r--r--test/expected/test_regex101.sh_771af6f3d29b8350542d5c6e98bdbf4c223cd531.err1
-rw-r--r--test/expected/test_regex101.sh_771af6f3d29b8350542d5c6e98bdbf4c223cd531.out0
-rw-r--r--test/expected/test_regex101.sh_7991a5b617867cf37c9f7baa85ffa425f7d455a2.err5
-rw-r--r--test/expected/test_regex101.sh_7991a5b617867cf37c9f7baa85ffa425f7d455a2.out0
-rw-r--r--test/expected/test_regex101.sh_79ee3f5fe71ccec97b2619d8c1f74ca97ffd2243.err0
-rw-r--r--test/expected/test_regex101.sh_79ee3f5fe71ccec97b2619d8c1f74ca97ffd2243.out2
-rw-r--r--test/expected/test_regex101.sh_7de76c174c58d67bf93e8f01d6d55ebb6a023f10.err3
-rw-r--r--test/expected/test_regex101.sh_7de76c174c58d67bf93e8f01d6d55ebb6a023f10.out0
-rw-r--r--test/expected/test_regex101.sh_8a43e6657d4f60e68d31eb8302542ca28e80d077.err0
-rw-r--r--test/expected/test_regex101.sh_8a43e6657d4f60e68d31eb8302542ca28e80d077.out3
-rw-r--r--test/expected/test_regex101.sh_8e93a3b6b941847c71409a297779fbb0a6666a51.err3
-rw-r--r--test/expected/test_regex101.sh_8e93a3b6b941847c71409a297779fbb0a6666a51.out0
-rw-r--r--test/expected/test_regex101.sh_95c56a9d146ec9a7c2196559d316f928b2ae6ae9.err4
-rw-r--r--test/expected/test_regex101.sh_95c56a9d146ec9a7c2196559d316f928b2ae6ae9.out0
-rw-r--r--test/expected/test_regex101.sh_9d101ee29c45cdb8c0f117ad736c9a5dd5da5839.err1
-rw-r--r--test/expected/test_regex101.sh_9d101ee29c45cdb8c0f117ad736c9a5dd5da5839.out0
-rw-r--r--test/expected/test_regex101.sh_c43e07df9b3068696fdc8759c7561135db981b38.err2
-rw-r--r--test/expected/test_regex101.sh_c43e07df9b3068696fdc8759c7561135db981b38.out0
-rw-r--r--test/expected/test_regex101.sh_cbd859487e4ea011cd6e0f0f114d70158bfd8b43.err0
-rw-r--r--test/expected/test_regex101.sh_cbd859487e4ea011cd6e0f0f114d70158bfd8b43.out34
-rw-r--r--test/expected/test_regex101.sh_cf6c0a9f0f04e24ce1fae7a0a434830b14447f83.err1
-rw-r--r--test/expected/test_regex101.sh_cf6c0a9f0f04e24ce1fae7a0a434830b14447f83.out0
-rw-r--r--test/expected/test_regex101.sh_d84597760285c3964b258726341e018f6cd49954.err7
-rw-r--r--test/expected/test_regex101.sh_d84597760285c3964b258726341e018f6cd49954.out0
-rw-r--r--test/expected/test_regex101.sh_f23e393dbf23d0d8e276e9b7610c7b74d79980f8.err0
-rw-r--r--test/expected/test_regex101.sh_f23e393dbf23d0d8e276e9b7610c7b74d79980f8.out15
-rw-r--r--test/expected/test_regex101.sh_fc41b6ee90cbf038620151f16d164b361acf82dd.err1
-rw-r--r--test/expected/test_regex101.sh_fc41b6ee90cbf038620151f16d164b361acf82dd.out0
-rw-r--r--test/expected/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.err0
-rw-r--r--test/expected/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.out1
-rw-r--r--test/expected/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.err0
-rw-r--r--test/expected/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.out3
-rw-r--r--test/expected/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.err0
-rw-r--r--test/expected/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.out0
-rw-r--r--test/expected/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.err0
-rw-r--r--test/expected/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.out0
-rw-r--r--test/expected/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.err0
-rw-r--r--test/expected/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.out0
-rw-r--r--test/expected/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.err0
-rw-r--r--test/expected/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.out4
-rw-r--r--test/expected/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.err0
-rw-r--r--test/expected/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.out36
-rw-r--r--test/expected/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.err0
-rw-r--r--test/expected/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.out3
-rw-r--r--test/expected/test_sessions.sh_8732dad5481be991ca7f291d9c5451c7b016cea7.err0
-rw-r--r--test/expected/test_sessions.sh_8732dad5481be991ca7f291d9c5451c7b016cea7.out33
-rw-r--r--test/expected/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.err0
-rw-r--r--test/expected/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.out0
-rw-r--r--test/expected/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.err0
-rw-r--r--test/expected/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.out2
-rw-r--r--test/expected/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.err0
-rw-r--r--test/expected/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.out11
-rw-r--r--test/expected/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.err0
-rw-r--r--test/expected/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.out0
-rw-r--r--test/expected/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.err0
-rw-r--r--test/expected/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.out0
-rw-r--r--test/expected/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.err0
-rw-r--r--test/expected/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.out1
-rw-r--r--test/expected/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.err0
-rw-r--r--test/expected/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.out1
-rw-r--r--test/expected/test_sessions.sh_e39648f425c3f291c9d1c0d14595a019abd0cb48.err0
-rw-r--r--test/expected/test_sessions.sh_e39648f425c3f291c9d1c0d14595a019abd0cb48.out33
-rw-r--r--test/expected/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.err0
-rw-r--r--test/expected/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.out7
-rw-r--r--test/expected/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.err0
-rw-r--r--test/expected/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.out7
-rw-r--r--test/expected/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.err0
-rw-r--r--test/expected/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.out2
-rw-r--r--test/expected/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.err0
-rw-r--r--test/expected/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.out5
-rw-r--r--test/expected/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.err0
-rw-r--r--test/expected/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.out7
-rw-r--r--test/expected/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.err0
-rw-r--r--test/expected/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.out6
-rw-r--r--test/expected/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.err0
-rw-r--r--test/expected/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.out7
-rw-r--r--test/expected/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.err0
-rw-r--r--test/expected/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.out6
-rw-r--r--test/expected/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.err0
-rw-r--r--test/expected/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.out5
-rw-r--r--test/expected/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.err0
-rw-r--r--test/expected/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.out6
-rw-r--r--test/expected/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.err0
-rw-r--r--test/expected/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.out9
-rw-r--r--test/expected/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.err0
-rw-r--r--test/expected/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.out6
-rw-r--r--test/expected/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.err0
-rw-r--r--test/expected/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.out6
-rw-r--r--test/expected/test_sql.sh_02def66745b063518473df862987747909f56ccc.err4
-rw-r--r--test/expected/test_sql.sh_02def66745b063518473df862987747909f56ccc.out0
-rw-r--r--test/expected/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.err0
-rw-r--r--test/expected/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.out2
-rw-r--r--test/expected/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.err4
-rw-r--r--test/expected/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.out0
-rw-r--r--test/expected/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.err0
-rw-r--r--test/expected/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.out2
-rw-r--r--test/expected/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.err0
-rw-r--r--test/expected/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.out3
-rw-r--r--test/expected/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.err10
-rw-r--r--test/expected/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.out0
-rw-r--r--test/expected/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.err6
-rw-r--r--test/expected/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.out0
-rw-r--r--test/expected/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.err4
-rw-r--r--test/expected/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.out0
-rw-r--r--test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.err4
-rw-r--r--test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.out0
-rw-r--r--test/expected/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.err4
-rw-r--r--test/expected/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.out0
-rw-r--r--test/expected/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.err0
-rw-r--r--test/expected/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.out3
-rw-r--r--test/expected/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.err0
-rw-r--r--test/expected/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.out2
-rw-r--r--test/expected/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.err4
-rw-r--r--test/expected/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.out0
-rw-r--r--test/expected/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.err5
-rw-r--r--test/expected/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.out0
-rw-r--r--test/expected/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.err4
-rw-r--r--test/expected/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.out0
-rw-r--r--test/expected/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.err4
-rw-r--r--test/expected/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.out0
-rw-r--r--test/expected/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.err4
-rw-r--r--test/expected/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.out0
-rw-r--r--test/expected/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.err0
-rw-r--r--test/expected/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.out2
-rw-r--r--test/expected/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.err0
-rw-r--r--test/expected/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.out2
-rw-r--r--test/expected/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.err5
-rw-r--r--test/expected/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.out0
-rw-r--r--test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.err7
-rw-r--r--test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.out0
-rw-r--r--test/expected/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.err4
-rw-r--r--test/expected/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.out0
-rw-r--r--test/expected/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.err5
-rw-r--r--test/expected/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.out0
-rw-r--r--test/expected/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.err0
-rw-r--r--test/expected/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.out2
-rw-r--r--test/expected/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.err6
-rw-r--r--test/expected/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.out0
-rw-r--r--test/expected/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.err6
-rw-r--r--test/expected/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.out0
-rw-r--r--test/expected/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.err0
-rw-r--r--test/expected/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.out2
-rw-r--r--test/expected/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.err0
-rw-r--r--test/expected/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.out2
-rw-r--r--test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err6
-rw-r--r--test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.out0
-rw-r--r--test/expected/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.err0
-rw-r--r--test/expected/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.out4
-rw-r--r--test/expected/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.err0
-rw-r--r--test/expected/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.out4
-rw-r--r--test/expected/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.err6
-rw-r--r--test/expected/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.out0
-rw-r--r--test/expected/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.err0
-rw-r--r--test/expected/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.out4
-rw-r--r--test/expected/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.err6
-rw-r--r--test/expected/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.out0
-rw-r--r--test/expected/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.err4
-rw-r--r--test/expected/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.out0
-rw-r--r--test/expected/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.err4
-rw-r--r--test/expected/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.out0
-rw-r--r--test/expected/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.err0
-rw-r--r--test/expected/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.out2
-rw-r--r--test/expected/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.err0
-rw-r--r--test/expected/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.out4
-rw-r--r--test/expected/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.err0
-rw-r--r--test/expected/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.out4
-rw-r--r--test/expected/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.err4
-rw-r--r--test/expected/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.out0
-rw-r--r--test/expected/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.err0
-rw-r--r--test/expected/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.out2
-rw-r--r--test/expected/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.err0
-rw-r--r--test/expected/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.out5
-rw-r--r--test/expected/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.err0
-rw-r--r--test/expected/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.out2
-rw-r--r--test/expected/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.err4
-rw-r--r--test/expected/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.out0
-rw-r--r--test/expected/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.err0
-rw-r--r--test/expected/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.out4
-rw-r--r--test/expected/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.err0
-rw-r--r--test/expected/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.out3
-rw-r--r--test/expected/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.err4
-rw-r--r--test/expected/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.out0
-rw-r--r--test/expected/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.err0
-rw-r--r--test/expected/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.out2
-rw-r--r--test/expected/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.err0
-rw-r--r--test/expected/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.out2
-rw-r--r--test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.err10
-rw-r--r--test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.out0
-rw-r--r--test/expected/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.err0
-rw-r--r--test/expected/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.out1
-rw-r--r--test/expected/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.err0
-rw-r--r--test/expected/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.out3
-rw-r--r--test/expected/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.err0
-rw-r--r--test/expected/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.out5
-rw-r--r--test/expected/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.err0
-rw-r--r--test/expected/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.out4
-rw-r--r--test/expected/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.err0
-rw-r--r--test/expected/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.out6
-rw-r--r--test/expected/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.err0
-rw-r--r--test/expected/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.out2
-rw-r--r--test/expected/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.err0
-rw-r--r--test/expected/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.out5
-rw-r--r--test/expected/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.err0
-rw-r--r--test/expected/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.out7
-rw-r--r--test/expected/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.err0
-rw-r--r--test/expected/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.out7
-rw-r--r--test/expected/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.err0
-rw-r--r--test/expected/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.out6
-rw-r--r--test/expected/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.err0
-rw-r--r--test/expected/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.out12
-rw-r--r--test/expected/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.err0
-rw-r--r--test/expected/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.out7
-rw-r--r--test/expected/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.err0
-rw-r--r--test/expected/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.out10
-rw-r--r--test/expected/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.err0
-rw-r--r--test/expected/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.out6
-rw-r--r--test/expected/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.err0
-rw-r--r--test/expected/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.out7
-rw-r--r--test/expected/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.err0
-rw-r--r--test/expected/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.out10
-rw-r--r--test/expected/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.err0
-rw-r--r--test/expected/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.out6
-rw-r--r--test/expected/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.err0
-rw-r--r--test/expected/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.out11
-rw-r--r--test/expected/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.err0
-rw-r--r--test/expected/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.out10
-rw-r--r--test/expected/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.out2
-rw-r--r--test/expected/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.err0
-rw-r--r--test/expected/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.err1
-rw-r--r--test/expected/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.out0
-rw-r--r--test/expected/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.err1
-rw-r--r--test/expected/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.out0
-rw-r--r--test/expected/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.out2
-rw-r--r--test/expected/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.err0
-rw-r--r--test/expected/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.out2
-rw-r--r--test/expected/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.err0
-rw-r--r--test/expected/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.out0
-rw-r--r--test/expected/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.err0
-rw-r--r--test/expected/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.out3
-rw-r--r--test/expected/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.err0
-rw-r--r--test/expected/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.out5
-rw-r--r--test/expected/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.err0
-rw-r--r--test/expected/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.out2
-rw-r--r--test/expected/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.err0
-rw-r--r--test/expected/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.out5
-rw-r--r--test/expected/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.err0
-rw-r--r--test/expected/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.out3
-rw-r--r--test/expected/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.err0
-rw-r--r--test/expected/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.out2
-rw-r--r--test/expected/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.err0
-rw-r--r--test/expected/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.out2
-rw-r--r--test/expected/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.err0
-rw-r--r--test/expected/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.out5
-rw-r--r--test/expected/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.err0
-rw-r--r--test/expected/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.out2
-rw-r--r--test/expected/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.err4
-rw-r--r--test/expected/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.out0
-rw-r--r--test/expected/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.err0
-rw-r--r--test/expected/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.out3
-rw-r--r--test/expected/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.err0
-rw-r--r--test/expected/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.out2
-rw-r--r--test/expected/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.err0
-rw-r--r--test/expected/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.out2
-rw-r--r--test/expected/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.err0
-rw-r--r--test/expected/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.out2
-rw-r--r--test/expected/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.err0
-rw-r--r--test/expected/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.out2
-rw-r--r--test/expected/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.err0
-rw-r--r--test/expected/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.out2
-rw-r--r--test/expected/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.err0
-rw-r--r--test/expected/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.out2
-rw-r--r--test/expected/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.err0
-rw-r--r--test/expected/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.out2
-rw-r--r--test/expected/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.err1
-rw-r--r--test/expected/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.out0
-rw-r--r--test/expected/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.err0
-rw-r--r--test/expected/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.out2
-rw-r--r--test/expected/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.err1
-rw-r--r--test/expected/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.out0
-rw-r--r--test/expected/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.err0
-rw-r--r--test/expected/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.out2
-rw-r--r--test/expected/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.err0
-rw-r--r--test/expected/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.out3
-rw-r--r--test/expected/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.err0
-rw-r--r--test/expected/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.out2
-rw-r--r--test/expected/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.err0
-rw-r--r--test/expected/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.out2
-rw-r--r--test/expected/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.err0
-rw-r--r--test/expected/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.out2
-rw-r--r--test/expected/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.err0
-rw-r--r--test/expected/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.out2
-rw-r--r--test/expected/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.err0
-rw-r--r--test/expected/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.out2
-rw-r--r--test/expected/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.err1
-rw-r--r--test/expected/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.out0
-rw-r--r--test/expected/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.err0
-rw-r--r--test/expected/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.out2
-rw-r--r--test/expected/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.err0
-rw-r--r--test/expected/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.out2
-rw-r--r--test/expected/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.err0
-rw-r--r--test/expected/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.out2
-rw-r--r--test/expected/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.err0
-rw-r--r--test/expected/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.out2
-rw-r--r--test/expected/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.err0
-rw-r--r--test/expected/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.out2
-rw-r--r--test/expected/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.err0
-rw-r--r--test/expected/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.out2
-rw-r--r--test/expected/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.err0
-rw-r--r--test/expected/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.out2
-rw-r--r--test/expected/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.err0
-rw-r--r--test/expected/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.out2
-rw-r--r--test/expected/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.err0
-rw-r--r--test/expected/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.out2
-rw-r--r--test/expected/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.err0
-rw-r--r--test/expected/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.out2
-rw-r--r--test/expected/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.err0
-rw-r--r--test/expected/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.out2
-rw-r--r--test/expected/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.err0
-rw-r--r--test/expected/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.out2
-rw-r--r--test/expected/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.err1
-rw-r--r--test/expected/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.out0
-rw-r--r--test/expected/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.err0
-rw-r--r--test/expected/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.out2
-rw-r--r--test/expected/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.err0
-rw-r--r--test/expected/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.out2
-rw-r--r--test/expected/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.err0
-rw-r--r--test/expected/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.out2
-rw-r--r--test/expected/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.err0
-rw-r--r--test/expected/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.out3
-rw-r--r--test/expected/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.err4
-rw-r--r--test/expected/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.out0
-rw-r--r--test/expected/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.err0
-rw-r--r--test/expected/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.out4
-rw-r--r--test/expected/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.err0
-rw-r--r--test/expected/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.out2
-rw-r--r--test/expected/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.err0
-rw-r--r--test/expected/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.out27
-rw-r--r--test/expected/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.err0
-rw-r--r--test/expected/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.out2
-rw-r--r--test/expected/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.err9
-rw-r--r--test/expected/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.out0
-rw-r--r--test/expected/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.err0
-rw-r--r--test/expected/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.out2
-rw-r--r--test/expected/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.err0
-rw-r--r--test/expected/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.out2
-rw-r--r--test/expected/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.err0
-rw-r--r--test/expected/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.out2
-rw-r--r--test/expected/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.err0
-rw-r--r--test/expected/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.out12
-rw-r--r--test/expected/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.err0
-rw-r--r--test/expected/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.out4
-rw-r--r--test/expected/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.err0
-rw-r--r--test/expected/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.out24
-rw-r--r--test/expected/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.err0
-rw-r--r--test/expected/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.out6
-rw-r--r--test/expected/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.err0
-rw-r--r--test/expected/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.out2
-rw-r--r--test/expected/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.err0
-rw-r--r--test/expected/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.out2
-rw-r--r--test/expected/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.err0
-rw-r--r--test/expected/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.out2
-rw-r--r--test/expected/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.err0
-rw-r--r--test/expected/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.out64
-rw-r--r--test/expected/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.err0
-rw-r--r--test/expected/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.out198
-rw-r--r--test/expected/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.err0
-rw-r--r--test/expected/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.out2
-rw-r--r--test/expected/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.err0
-rw-r--r--test/expected/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.out8
-rw-r--r--test/expected/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.err1
-rw-r--r--test/expected/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.out0
-rw-r--r--test/expected/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.err0
-rw-r--r--test/expected/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.out2
-rw-r--r--test/expected/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.err0
-rw-r--r--test/expected/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.out8
-rw-r--r--test/expected/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.err0
-rw-r--r--test/expected/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.out2
-rw-r--r--test/expected/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.err0
-rw-r--r--test/expected/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.out2
-rw-r--r--test/expected/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.err0
-rw-r--r--test/expected/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.out2
-rw-r--r--test/expected/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.err1
-rw-r--r--test/expected/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.out0
-rw-r--r--test/expected/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.err0
-rw-r--r--test/expected/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.out2
-rw-r--r--test/expected/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.err0
-rw-r--r--test/expected/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.out2
-rw-r--r--test/expected/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.err0
-rw-r--r--test/expected/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.out2
-rw-r--r--test/expected/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.err0
-rw-r--r--test/expected/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.out2
-rw-r--r--test/expected/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.err0
-rw-r--r--test/expected/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.out2
-rw-r--r--test/expected/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.err0
-rw-r--r--test/expected/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.out2
-rw-r--r--test/expected/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.err0
-rw-r--r--test/expected/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.out24
-rw-r--r--test/expected/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.err0
-rw-r--r--test/expected/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.out2
-rw-r--r--test/expected/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.err0
-rw-r--r--test/expected/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.out3
-rw-r--r--test/expected/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.err0
-rw-r--r--test/expected/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.out2
-rw-r--r--test/expected/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.err0
-rw-r--r--test/expected/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.out2
-rw-r--r--test/expected/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.err0
-rw-r--r--test/expected/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.out6
-rw-r--r--test/expected/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.err0
-rw-r--r--test/expected/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.out2
-rw-r--r--test/expected/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.err0
-rw-r--r--test/expected/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.out16
-rw-r--r--test/expected/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.err0
-rw-r--r--test/expected/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.out3
-rw-r--r--test/expected/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.err1
-rw-r--r--test/expected/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.out0
-rw-r--r--test/expected/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.err0
-rw-r--r--test/expected/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.out2
-rw-r--r--test/expected/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.err0
-rw-r--r--test/expected/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.out2
-rw-r--r--test/expected/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.err0
-rw-r--r--test/expected/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.out2
-rw-r--r--test/expected/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.err0
-rw-r--r--test/expected/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.out2
-rw-r--r--test/expected/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.err0
-rw-r--r--test/expected/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.out2
-rw-r--r--test/expected/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.err1
-rw-r--r--test/expected/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.out0
-rw-r--r--test/expected/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.err0
-rw-r--r--test/expected/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.out2
-rw-r--r--test/expected/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.err0
-rw-r--r--test/expected/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.out2
-rw-r--r--test/expected/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.err0
-rw-r--r--test/expected/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.out2
-rw-r--r--test/expected/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.err1
-rw-r--r--test/expected/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.out0
-rw-r--r--test/expected/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.err0
-rw-r--r--test/expected/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.out2
-rw-r--r--test/expected/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.err0
-rw-r--r--test/expected/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.out2
-rw-r--r--test/expected/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.err0
-rw-r--r--test/expected/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.out2
-rw-r--r--test/expected/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.err0
-rw-r--r--test/expected/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.out2
-rw-r--r--test/expected/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.err0
-rw-r--r--test/expected/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.out2
-rw-r--r--test/expected/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.err1
-rw-r--r--test/expected/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.out0
-rw-r--r--test/expected/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.err0
-rw-r--r--test/expected/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.out3
-rw-r--r--test/expected/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.err0
-rw-r--r--test/expected/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.out2
-rw-r--r--test/expected/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.err0
-rw-r--r--test/expected/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.out2
-rw-r--r--test/expected/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.err0
-rw-r--r--test/expected/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.out16
-rw-r--r--test/expected/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.err0
-rw-r--r--test/expected/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.out3
-rw-r--r--test/expected/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.err0
-rw-r--r--test/expected/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.out2
-rw-r--r--test/expected/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.err0
-rw-r--r--test/expected/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.out2
-rw-r--r--test/expected/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.err0
-rw-r--r--test/expected/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.out2
-rw-r--r--test/expected/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.err1
-rw-r--r--test/expected/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.out0
-rw-r--r--test/expected/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.err0
-rw-r--r--test/expected/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.out2
-rw-r--r--test/expected/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.err0
-rw-r--r--test/expected/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.out2
-rw-r--r--test/expected/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.err0
-rw-r--r--test/expected/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.out2
-rw-r--r--test/expected/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.err0
-rw-r--r--test/expected/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.out2
-rw-r--r--test/expected/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.err0
-rw-r--r--test/expected/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.out0
-rw-r--r--test/expected/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.err1
-rw-r--r--test/expected/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.out0
-rw-r--r--test/expected/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.err0
-rw-r--r--test/expected/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.out2
-rw-r--r--test/expected/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.err0
-rw-r--r--test/expected/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.out47
-rw-r--r--test/expected/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.err0
-rw-r--r--test/expected/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.out2
-rw-r--r--test/expected/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.err0
-rw-r--r--test/expected/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.out0
-rw-r--r--test/expected/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.err0
-rw-r--r--test/expected/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.out2
-rw-r--r--test/expected/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.err0
-rw-r--r--test/expected/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.out2
-rw-r--r--test/expected/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.err0
-rw-r--r--test/expected/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.out2
-rw-r--r--test/expected/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.err0
-rw-r--r--test/expected/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.out2
-rw-r--r--test/expected/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.err0
-rw-r--r--test/expected/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.out2
-rw-r--r--test/expected/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.err0
-rw-r--r--test/expected/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.out2
-rw-r--r--test/expected/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.err0
-rw-r--r--test/expected/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.out2
-rw-r--r--test/expected/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.err0
-rw-r--r--test/expected/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.out2
-rw-r--r--test/expected/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.err0
-rw-r--r--test/expected/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.out2
-rw-r--r--test/expected/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.err0
-rw-r--r--test/expected/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.out2
-rw-r--r--test/expected/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.err1
-rw-r--r--test/expected/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.out0
-rw-r--r--test/expected/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.err0
-rw-r--r--test/expected/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.out2
-rw-r--r--test/expected/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.err0
-rw-r--r--test/expected/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.out2
-rw-r--r--test/expected/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.err0
-rw-r--r--test/expected/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.out2
-rw-r--r--test/expected/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.err0
-rw-r--r--test/expected/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.out2
-rw-r--r--test/expected/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.err1
-rw-r--r--test/expected/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.out0
-rw-r--r--test/expected/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.err1
-rw-r--r--test/expected/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.out0
-rw-r--r--test/expected/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.err0
-rw-r--r--test/expected/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.out2
-rw-r--r--test/expected/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.err0
-rw-r--r--test/expected/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.out2
-rw-r--r--test/expected/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.err0
-rw-r--r--test/expected/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.out2
-rw-r--r--test/expected/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.err0
-rw-r--r--test/expected/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.out2
-rw-r--r--test/expected/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.err0
-rw-r--r--test/expected/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.out2
-rw-r--r--test/expected/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.err0
-rw-r--r--test/expected/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.out2
-rw-r--r--test/expected/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.err0
-rw-r--r--test/expected/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.out2
-rw-r--r--test/expected/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.out0
-rw-r--r--test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.out19
-rw-r--r--test/expected/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.out1
-rw-r--r--test/expected/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.out3
-rw-r--r--test/expected/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.out1
-rw-r--r--test/expected/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.out2
-rw-r--r--test/expected/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.out0
-rw-r--r--test/expected/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.out2
-rw-r--r--test/expected/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.out2
-rw-r--r--test/expected/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.out0
-rw-r--r--test/expected/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.out2
-rw-r--r--test/expected/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.out2
-rw-r--r--test/expected/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.out101
-rw-r--r--test/expected/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.out2
-rw-r--r--test/expected/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.out0
-rw-r--r--test/expected/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.out0
-rw-r--r--test/expected/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.out0
-rw-r--r--test/expected/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.out1
-rw-r--r--test/expected/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.out1
-rw-r--r--test/expected/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.err0
-rw-r--r--test/expected/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.out3
-rw-r--r--test/expected/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.out0
-rw-r--r--test/expected/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.err4
-rw-r--r--test/expected/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.out0
-rw-r--r--test/expected/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.err0
-rw-r--r--test/expected/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.out0
-rw-r--r--test/expected/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.err0
-rw-r--r--test/expected/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.out6
-rw-r--r--test/expected/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.err0
-rw-r--r--test/expected/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.out6
-rw-r--r--test/expected/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.err1
-rw-r--r--test/expected/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.out0
-rw-r--r--test/expected/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.err1
-rw-r--r--test/expected/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.out0
-rw-r--r--test/expected/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.err4
-rw-r--r--test/expected/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.out0
-rw-r--r--test/expected/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.err4
-rw-r--r--test/expected/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.out0
-rw-r--r--test/expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.err3
-rw-r--r--test/expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.out8
-rw-r--r--test/expected/test_text_file.sh_2e69c22dcfa37b5c3e8490a6026eacb7ca953998.err2
-rw-r--r--test/expected/test_text_file.sh_2e69c22dcfa37b5c3e8490a6026eacb7ca953998.out0
-rw-r--r--test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.err0
-rw-r--r--test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.out178
-rw-r--r--test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.err0
-rw-r--r--test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.out149
-rw-r--r--test/expected/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.err0
-rw-r--r--test/expected/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.out10
-rw-r--r--test/expected/test_text_file.sh_801414c6bb6d3f9225973eafa3c6dfa49cd2081d.err0
-rw-r--r--test/expected/test_text_file.sh_801414c6bb6d3f9225973eafa3c6dfa49cd2081d.out111
-rw-r--r--test/expected/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.err0
-rw-r--r--test/expected/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.out111
-rw-r--r--test/expected/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.err0
-rw-r--r--test/expected/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.out2
-rw-r--r--test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.err0
-rw-r--r--test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.out4
-rw-r--r--test/expected/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.err0
-rw-r--r--test/expected/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.out74
-rw-r--r--test/expected/test_text_file.sh_c21295f131c221861568bda5014b76ef99bdd11f.err0
-rw-r--r--test/expected/test_text_file.sh_c21295f131c221861568bda5014b76ef99bdd11f.out159
-rw-r--r--test/expected/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.err0
-rw-r--r--test/expected/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.out58
-rw-r--r--test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.err0
-rw-r--r--test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out149
-rw-r--r--test/expected_help.txt4016
-rw-r--r--test/file_for_dot_read.sql4
-rw-r--r--test/formats/collision/format.json48
-rw-r--r--test/formats/customlevel/format.json25
-rw-r--r--test/formats/jsontest-subsec/format.json27
-rw-r--r--test/formats/jsontest/format.json47
-rw-r--r--test/formats/jsontest/lnav-logstash.json47
-rw-r--r--test/formats/jsontest/rewrite-user.lnav2
-rw-r--r--test/formats/jsontest2/format.json49
-rw-r--r--test/formats/jsontest3/format.json84
-rw-r--r--test/formats/nestedjson/format.json32
-rw-r--r--test/formats/scripts/multiline-echo.lnav3
-rw-r--r--test/formats/scripts/nested-redirecting.lnav5
-rw-r--r--test/formats/scripts/redirecting.lnav6
-rw-r--r--test/formats/sqldir/init.sql5
-rw-r--r--test/formats/timestamp/format.json26
-rw-r--r--test/formats/xmlmsg/format.json73
-rw-r--r--test/gp_test.cc103
-rw-r--r--test/lb_test.cc65
-rw-r--r--test/listview_output.070
-rw-r--r--test/listview_output.170
-rw-r--r--test/listview_output.270
-rw-r--r--test/listview_output.370
-rw-r--r--test/listview_output.468
-rw-r--r--test/listview_output.559
-rw-r--r--test/listview_output.659
-rw-r--r--test/listview_output_cursor.070
-rw-r--r--test/listview_output_cursor.170
-rw-r--r--test/listview_output_cursor.270
-rw-r--r--test/listview_output_cursor.325
-rw-r--r--test/listview_output_cursor.425
-rw-r--r--test/listview_output_cursor.535
-rw-r--r--test/listview_output_cursor.635
-rw-r--r--test/lnav_doctests.cc231
-rw-r--r--test/log-samples/sample-057d6c669632ef9d07b6adec605f6bdeae19af27.txt13
-rw-r--r--test/log-samples/sample-06aaa6f48a801f592558575d886864d6c3ab9ed4.txt40
-rw-r--r--test/log-samples/sample-1aeb47c0a97d19bb7418f0172480e05e49c6e53e.txt17
-rw-r--r--test/log-samples/sample-27353a72ba4025448f261dcfa6ea16e474187795.txt4
-rw-r--r--test/log-samples/sample-3856ad0f551a04fde41a020158d6b33ef97c870a.txt17
-rw-r--r--test/log-samples/sample-45364b3fd51af92a4ad8a309b5f4fd88.txt40
-rw-r--r--test/log-samples/sample-500c9e492e04f5f58862c8086ca301de0dd976ce.txt13
-rw-r--r--test/log-samples/sample-55ac97afae4b0650ccb62e2dbc8d89bb.txt15
-rw-r--r--test/log-samples/sample-6049d4309f26eefb1a3406d937a9ba8a0df592a7.txt13
-rw-r--r--test/log-samples/sample-62315d884afdc4155b35f905415c74bfcfd39fc2.txt17
-rw-r--r--test/log-samples/sample-70c906b3c1a1cf03f15bde92ee78edfa6f9b7960.txt4
-rw-r--r--test/log-samples/sample-9cf7fbb3546c676c686fac0ed096d026f46c875f.txt13
-rw-r--r--test/log-samples/sample-a74570613c082c7fe283672031e18e54e8887ffb.txt13
-rw-r--r--test/log-samples/sample-aca2878a2e50779c6697c0747ab1f60e4b368dcb.txt15
-rw-r--r--test/log-samples/sample-ad31f12d2adabd07e3ddda3ad5b0dbf6b49c4c99.txt21
-rw-r--r--test/log-samples/sample-bc6f6cf689fa5455616b4d9fbe121a48d3c9de59.txt25
-rw-r--r--test/log-samples/sample-c15acd32844669d23d0cbc88ec548129ed2c592e.txt87
-rw-r--r--test/log-samples/sample-c23f22c1b932b904203e018f78dead95fb89b15d.txt37
-rw-r--r--test/log-samples/sample-d0d6b3fc6766caac5ac3fac4a3754ceaab785eb8.txt33
-rw-r--r--test/log-samples/sample-d4a0aedc8350f64b22403eeef4eca71fbf749d2b.txt9
-rw-r--r--test/log-samples/sample-d714b5e8cd354321f376ed1c0a70ec9a2f58076d.txt9
-rw-r--r--test/log-samples/sample-dd7d406352ec6a11d966b6f015a9482b060f2b29.txt23
-rw-r--r--test/log-samples/sample-e779d1771e34f5203ae73e85802e78002be63db6.txt25
-rw-r--r--test/log-samples/sample-f5afbee90a8c054061c4e9ffe673293cce7761de.txt13
-rw-r--r--test/log-samples/sample-fc8923633e57bacd641d80dde3ff878212230552.txt13
-rw-r--r--test/log.clog2
-rw-r--r--test/logfile_access_log.03
-rw-r--r--test/logfile_access_log.11
-rw-r--r--test/logfile_ansi.01
-rw-r--r--test/logfile_ansi.110
-rw-r--r--test/logfile_bad_access_log.03
-rw-r--r--test/logfile_bad_syslog.04
-rw-r--r--test/logfile_block.14
-rw-r--r--test/logfile_block.22
-rw-r--r--test/logfile_blued.06
-rw-r--r--test/logfile_bro_conn.log.0101
-rw-r--r--test/logfile_bro_http.log.0206
-rw-r--r--test/logfile_bunyan.010
-rw-r--r--test/logfile_cloudflare.json1
-rw-r--r--test/logfile_crlf.02
-rw-r--r--test/logfile_cxx.01
-rw-r--r--test/logfile_empty.00
-rw-r--r--test/logfile_epoch.02
-rw-r--r--test/logfile_epoch.12
-rw-r--r--test/logfile_filter.013
-rw-r--r--test/logfile_for_join.010
-rw-r--r--test/logfile_generic.02
-rw-r--r--test/logfile_generic.12
-rw-r--r--test/logfile_generic.22
-rw-r--r--test/logfile_generic.32
-rw-r--r--test/logfile_generic_with_header.04
-rw-r--r--test/logfile_glog.07
-rw-r--r--test/logfile_haproxy.017
-rw-r--r--test/logfile_invalid_json.json5
-rw-r--r--test/logfile_invalid_json2.json3
-rw-r--r--test/logfile_journald.json2
-rw-r--r--test/logfile_json.json13
-rw-r--r--test/logfile_json2.json3
-rw-r--r--test/logfile_json3.json3
-rw-r--r--test/logfile_json_subsec.json2
-rw-r--r--test/logfile_leveltest.08
-rw-r--r--test/logfile_logfmt.05
-rw-r--r--test/logfile_mixed_json2.json18
-rw-r--r--test/logfile_multiline.03
-rw-r--r--test/logfile_nested_json.json13
-rw-r--r--test/logfile_openam.02
-rw-r--r--test/logfile_plain.03
-rw-r--r--test/logfile_pretty.023
-rw-r--r--test/logfile_procstate.043
-rw-r--r--test/logfile_rollover.06
-rw-r--r--test/logfile_rollover.15
-rw-r--r--test/logfile_strace_log.09
-rw-r--r--test/logfile_syslog.04
-rw-r--r--test/logfile_syslog.14
-rw-r--r--test/logfile_syslog.23
-rw-r--r--test/logfile_syslog.317
-rw-r--r--test/logfile_syslog_fr.01
-rw-r--r--test/logfile_syslog_with_access_log.05
-rw-r--r--test/logfile_syslog_with_header.06
-rw-r--r--test/logfile_syslog_with_mixed_times.013
-rw-r--r--test/logfile_tai64n.010
-rw-r--r--test/logfile_tcf.030
-rw-r--r--test/logfile_tcf.13
-rw-r--r--test/logfile_tcsh_history.04
-rw-r--r--test/logfile_uwsgi.019
-rw-r--r--test/logfile_vami.04
-rw-r--r--test/logfile_vdsm.016
-rw-r--r--test/logfile_vmw_log.04
-rw-r--r--test/logfile_vpxd.012
-rw-r--r--test/logfile_w3c.05
-rw-r--r--test/logfile_w3c.17
-rw-r--r--test/logfile_w3c.222
-rw-r--r--test/logfile_w3c.310
-rw-r--r--test/logfile_w3c.46
-rw-r--r--test/logfile_w3c.52
-rw-r--r--test/logfile_w3c.65
-rw-r--r--test/logfile_w3c_big.0254
-rw-r--r--test/logfile_with_a_really_long_name_to_test_a_bug_with_long_names.01
-rw-r--r--test/logfile_xml_msg.036
-rw-r--r--test/multiline.lnav14
-rw-r--r--test/mvwattrline_output.049
-rw-r--r--test/nested.lnav2
-rwxr-xr-xtest/parser_debugger.py249
-rw-r--r--test/remote-log-dir/logfile_access_log.03
-rw-r--r--test/remote-log-dir/logfile_access_log.11
-rw-r--r--test/rltest.cc216
-rw-r--r--test/scripty.cc1153
-rw-r--r--test/si_test.cc49
-rw-r--r--test/slicer.cc99
-rw-r--r--test/sql.0.in6
-rw-r--r--test/sql.0.out7
-rw-r--r--test/test_abbrev.cc60
-rw-r--r--test/test_ansi_scrubber.cc98
-rw-r--r--test/test_auto_fd.cc82
-rw-r--r--test/test_auto_mem.cc115
-rw-r--r--test/test_bookmarks.cc146
-rw-r--r--test/test_cli.sh31
-rw-r--r--test/test_cmds.sh519
-rw-r--r--test/test_column_namer.cc68
-rwxr-xr-xtest/test_config.sh39
-rw-r--r--test/test_curl.sh48
-rw-r--r--test/test_data_parser.sh16
-rw-r--r--test/test_date_time_scanner.cc192
-rw-r--r--test/test_events.sh31
-rw-r--r--test/test_format_installer.sh34
-rw-r--r--test/test_format_loader.sh17
-rw-r--r--test/test_grep_proc.sh57
-rw-r--r--test/test_grep_proc2.cc150
-rw-r--r--test/test_json_format.sh156
-rw-r--r--test/test_line_buffer.sh78
-rw-r--r--test/test_line_buffer2.cc157
-rw-r--r--test/test_listview.sh83
-rw-r--r--test/test_log_accel.cc70
-rw-r--r--test/test_logfile.sh707
-rw-r--r--test/test_md2attr_line.cc34
-rw-r--r--test/test_meta.sh110
-rw-r--r--test/test_mvwattrline.sh6
-rw-r--r--test/test_ncurses_unicode.cc40
-rw-r--r--test/test_pretty_print.sh45
-rw-r--r--test/test_regex101.sh77
-rw-r--r--test/test_reltime.cc427
-rw-r--r--test/test_remote.sh119
-rw-r--r--test/test_scripts.sh51
-rw-r--r--test/test_sessions.sh119
-rw-r--r--test/test_shlexer.sh32
-rw-r--r--test/test_sql.sh1097
-rw-r--r--test/test_sql_anno.sh50
-rw-r--r--test/test_sql_coll_func.sh29
-rw-r--r--test/test_sql_fs_func.sh53
-rw-r--r--test/test_sql_indexes.sh45
-rw-r--r--test/test_sql_json_func.sh132
-rw-r--r--test/test_sql_regexp.sh32
-rw-r--r--test/test_sql_search_table.sh28
-rw-r--r--test/test_sql_str_func.sh170
-rw-r--r--test/test_sql_time_func.sh71
-rw-r--r--test/test_sql_views_vtab.sh179
-rw-r--r--test/test_sql_xml_func.sh11
-rw-r--r--test/test_sql_yaml_func.sh5
-rw-r--r--test/test_stubs.cc93
-rw-r--r--test/test_text_anonymizer.cc131
-rw-r--r--test/test_text_file.sh36
-rw-r--r--test/test_top_status.cc90
-rw-r--r--test/test_tui.sh28
-rw-r--r--test/test_view_colors.sh6
-rw-r--r--test/test_vt52_curses.sh6
-rw-r--r--test/textfile_0.md9
-rw-r--r--test/textfile_ansi.01
-rw-r--r--test/textfile_ansi_expanding.010
-rw-r--r--test/textfile_json_indented.012
-rw-r--r--test/textfile_json_one_line.01
-rw-r--r--test/textfile_quoted_json.01
-rw-r--r--test/toplevel.lnav4
-rw-r--r--test/tui-captures/tui_echo.0229
-rw-r--r--test/tui-captures/tui_help.0179
-rwxr-xr-xtest/update_parser_output.sh13
-rw-r--r--test/view_colors_output.054
-rw-r--r--test/vt52_curses_input.049
-rw-r--r--test/vt52_curses_input.138
-rw-r--r--test/vt52_curses_output.039
-rw-r--r--test/vt52_curses_output.138
-rw-r--r--test/xpath_tui.0458
-rw-r--r--tools/Makefile.am16
-rw-r--r--tools/bin2c.c252
-rwxr-xr-xupdate_expected_output.sh99
2277 files changed, 375618 insertions, 0 deletions
diff --git a/.cirrus.yml b/.cirrus.yml
new file mode 100644
index 0000000..7861b40
--- /dev/null
+++ b/.cirrus.yml
@@ -0,0 +1,7 @@
+freebsd_instance:
+ image_family: freebsd-13-2
+task:
+ install_script: pkg install -y wget git m4 bash autoconf automake sqlite3 gmake curl libarchive pcre2 bzip2
+ build_script: ./autogen.sh && ./configure && gmake -j3
+ binaries_artifacts:
+ path: "src/lnav"
diff --git a/.clang-format b/.clang-format
new file mode 100644
index 0000000..5eeefee
--- /dev/null
+++ b/.clang-format
@@ -0,0 +1,178 @@
+---
+Language: Cpp
+# BasedOnStyle: Chromium
+AccessModifierOffset: -4
+AlignAfterOpenBracket: Align
+AlignConsecutiveMacros: Consecutive
+AlignConsecutiveAssignments: None
+AlignConsecutiveBitFields: None
+AlignConsecutiveDeclarations: None
+AlignEscapedNewlines: DontAlign
+AlignOperands: DontAlign
+AlignTrailingComments: false
+AllowAllArgumentsOnNextLine: true
+AllowAllConstructorInitializersOnNextLine: false
+AllowAllParametersOfDeclarationOnNextLine: true
+AllowShortEnumsOnASingleLine: false
+AllowShortBlocksOnASingleLine: Empty
+AllowShortCaseLabelsOnASingleLine: false
+AllowShortFunctionsOnASingleLine: Inline
+AllowShortLambdasOnASingleLine: All
+AllowShortIfStatementsOnASingleLine: Never
+AllowShortLoopsOnASingleLine: false
+# AlwaysBreakAfterDefinitionReturnType: None
+AlwaysBreakAfterReturnType: TopLevelDefinitions
+AlwaysBreakBeforeMultilineStrings: true
+AlwaysBreakTemplateDeclarations: Yes
+BinPackArguments: false
+BinPackParameters: false
+BraceWrapping:
+ AfterCaseLabel: false
+ AfterClass: false
+ AfterControlStatement: MultiLine
+ AfterEnum: false
+ AfterFunction: true
+ AfterNamespace: false
+ AfterObjCDeclaration: false
+ AfterStruct: false
+ AfterUnion: false
+ AfterExternBlock: true
+ BeforeCatch: false
+ BeforeElse: false
+ BeforeLambdaBody: false
+ BeforeWhile: false
+ IndentBraces: false
+ SplitEmptyFunction: true
+ SplitEmptyRecord: true
+ SplitEmptyNamespace: true
+BreakBeforeBinaryOperators: All
+BreakBeforeBraces: Custom
+# BreakBeforeInheritanceComma: true
+BreakInheritanceList: BeforeComma
+BreakBeforeTernaryOperators: true
+# BreakConstructorInitializersBeforeComma: true
+BreakConstructorInitializers: BeforeColon
+BreakAfterJavaFieldAnnotations: true
+BreakStringLiterals: true
+ColumnLimit: 80
+CommentPragmas: '^ IWYU pragma:'
+CompactNamespaces: false
+ConstructorInitializerAllOnOneLineOrOnePerLine: false
+ConstructorInitializerIndentWidth: 4
+ContinuationIndentWidth: 4
+Cpp11BracedListStyle: true
+DeriveLineEnding: false
+DerivePointerAlignment: false
+DisableFormat: false
+ExperimentalAutoDetectBinPacking: false
+FixNamespaceComments: true
+ForEachMacros:
+ - foreach
+ - Q_FOREACH
+ - BOOST_FOREACH
+IncludeBlocks: Regroup
+IncludeCategories:
+ # Standard library headers come before anything else
+ - Regex: '^<[a-z_]+>'
+ Priority: -1
+ - Regex: '^<.+\.h(pp)?>'
+ Priority: 1
+ - Regex: '^<.*'
+ Priority: 2
+ - Regex: '.*'
+ Priority: 3
+IncludeIsMainRegex: ''
+IncludeIsMainSourceRegex: ''
+IndentCaseLabels: true
+IndentCaseBlocks: false
+IndentGotoLabels: true
+IndentPPDirectives: AfterHash
+IndentExternBlock: NoIndent
+IndentWidth: 4
+IndentWrappedFunctionNames: false
+InsertTrailingCommas: Wrapped
+JavaScriptQuotes: Double
+JavaScriptWrapImports: true
+KeepEmptyLinesAtTheStartOfBlocks: false
+MacroBlockBegin: ''
+MacroBlockEnd: ''
+MaxEmptyLinesToKeep: 1
+NamespaceIndentation: None
+ObjCBinPackProtocolList: Never
+ObjCBlockIndentWidth: 4
+ObjCBreakBeforeNestedBlockParam: true
+ObjCSpaceAfterProperty: false
+ObjCSpaceBeforeProtocolList: true
+PenaltyBreakAssignment: 4
+PenaltyBreakBeforeFirstCallParameter: 1
+PenaltyBreakComment: 300
+PenaltyBreakFirstLessLess: 120
+PenaltyBreakString: 1000
+PenaltyBreakTemplateDeclaration: 10
+PenaltyExcessCharacter: 1000000
+PenaltyReturnTypeOnItsOwnLine: 200
+PointerAlignment: Left
+RawStringFormats:
+ - Language: Cpp
+ Delimiters:
+ - cc
+ - CC
+ - cpp
+ - Cpp
+ - CPP
+ - 'c++'
+ - 'C++'
+ CanonicalDelimiter: ''
+ BasedOnStyle: google
+ - Language: TextProto
+ Delimiters:
+ - pb
+ - PB
+ - proto
+ - PROTO
+ EnclosingFunctions:
+ - EqualsProto
+ - EquivToProto
+ - PARSE_PARTIAL_TEXT_PROTO
+ - PARSE_TEST_PROTO
+ - PARSE_TEXT_PROTO
+ - ParseTextOrDie
+ - ParseTextProtoOrDie
+ - ParseTestProto
+ - ParsePartialTestProto
+ CanonicalDelimiter: ''
+ BasedOnStyle: google
+ReflowComments: true
+SortIncludes: CaseInsensitive
+SortUsingDeclarations: true
+SpaceAfterCStyleCast: true
+SpaceAfterLogicalNot: false
+SpaceAfterTemplateKeyword: false
+SpaceBeforeAssignmentOperators: true
+SpaceBeforeCpp11BracedList: false
+SpaceBeforeCtorInitializerColon: true
+SpaceBeforeInheritanceColon: true
+SpaceBeforeParens: ControlStatementsExceptForEachMacros
+SpaceBeforeRangeBasedForLoopColon: true
+SpaceInEmptyBlock: false
+SpaceInEmptyParentheses: false
+SpacesBeforeTrailingComments: 2
+SpacesInAngles: Never
+SpacesInConditionalStatement: false
+SpacesInContainerLiterals: false
+SpacesInCStyleCastParentheses: false
+SpacesInParentheses: false
+SpacesInSquareBrackets: false
+SpaceBeforeSquareBrackets: false
+Standard: Auto
+StatementMacros:
+ - Q_UNUSED
+ - QT_REQUIRE_VERSION
+TabWidth: 8
+UseCRLF: false
+UseTab: Never
+WhitespaceSensitiveMacros:
+ - STRINGIZE
+ - PP_STRINGIZE
+ - BOOST_PP_STRINGIZE
+...
diff --git a/.clang-tidy b/.clang-tidy
new file mode 100644
index 0000000..138dd8a
--- /dev/null
+++ b/.clang-tidy
@@ -0,0 +1,162 @@
+---
+# Enable ALL the things! Except not really
+# misc-non-private-member-variables-in-classes: the options don't do anything
+Checks: >
+ *,
+ -google-readability-todo,
+ -altera-unroll-loops,
+ -altera-id-dependent-backward-branch,
+ -altera-struct-pack-align,
+ -fuchsia-*,
+ fuchsia-multiple-inheritance,
+ -llvm-header-guard,
+ -llvm-include-order,
+ -llvmlibc-*,
+ -modernize-use-trailing-return-type,
+ -misc-non-private-member-variables-in-classes,
+ -cppcoreguidelines-pro-type-vararg,
+ -hicpp-vararg,
+ -cppcoreguidelines-avoid-c-arrays,
+ -hicpp-avoid-c-arrays,
+ -modernize-avoid-c-arrays
+WarningsAsErrors: ''
+CheckOptions:
+ - key: 'bugprone-argument-comment.StrictMode'
+ value: 'true'
+# Prefer using enum classes with 2 values for parameters instead of bools
+ - key: 'bugprone-argument-comment.CommentBoolLiterals'
+ value: 'true'
+ - key: 'bugprone-misplaced-widening-cast.CheckImplicitCasts'
+ value: 'true'
+ - key: 'bugprone-sizeof-expression.WarnOnSizeOfIntegerExpression'
+ value: 'true'
+ - key: 'bugprone-suspicious-string-compare.WarnOnLogicalNotComparison'
+ value: 'true'
+ - key: 'readability-simplify-boolean-expr.ChainedConditionalReturn'
+ value: 'true'
+ - key: 'readability-simplify-boolean-expr.ChainedConditionalAssignment'
+ value: 'true'
+ - key: 'readability-uniqueptr-delete-release.PreferResetCall'
+ value: 'true'
+ - key: 'cppcoreguidelines-init-variables.MathHeader'
+ value: '<cmath>'
+ - key: 'cppcoreguidelines-narrowing-conversions.PedanticMode'
+ value: 'true'
+ - key: 'readability-else-after-return.WarnOnUnfixable'
+ value: 'true'
+ - key: 'readability-else-after-return.WarnOnConditionVariables'
+ value: 'true'
+ - key: 'readability-inconsistent-declaration-parameter-name.Strict'
+ value: 'true'
+ - key: 'readability-qualified-auto.AddConstToQualified'
+ value: 'true'
+ - key: 'readability-redundant-access-specifiers.CheckFirstDeclaration'
+ value: 'true'
+# These seem to be the most common identifier styles
+ - key: 'readability-identifier-naming.AbstractClassCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ClassCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ClassConstantCase'
+ value: 'UPPER_CASE'
+ - key: 'readability-identifier-naming.ClassMemberCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ClassMethodCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ConstantCase'
+ value: 'UPPER_CASE'
+ - key: 'readability-identifier-naming.ConstantMemberCase'
+ value: 'UPPER_CASE'
+ - key: 'readability-identifier-naming.ConstantParameterCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ConstantPointerParameterCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ConstexprFunctionCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ConstexprMethodCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ConstexprVariableCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.EnumCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.EnumConstantCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.FunctionCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.GlobalConstantCase'
+ value: 'UPPER_CASE'
+ - key: 'readability-identifier-naming.GlobalConstantPointerCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.GlobalFunctionCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.GlobalPointerCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.GlobalVariableCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.InlineNamespaceCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.LocalConstantCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.LocalConstantPointerCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.LocalPointerCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.LocalVariableCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.MacroDefinitionCase'
+ value: 'UPPER_CASE'
+ - key: 'readability-identifier-naming.MemberCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.MethodCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.NamespaceCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ParameterCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ParameterPackCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.PointerParameterCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.PrivateMemberCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.PrivateMethodCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ProtectedMemberCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ProtectedMethodCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.PublicMemberCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.PublicMethodCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ScopedEnumConstantCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.StaticConstantCase'
+ value: 'UPPER_CASE'
+ - key: 'readability-identifier-naming.StaticVariableCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.StructCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.TemplateParameterCase'
+ value: 'CamelCase'
+ - key: 'readability-identifier-naming.TemplateTemplateParameterCase'
+ value: 'CamelCase'
+ - key: 'readability-identifier-naming.TypeAliasCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.TypedefCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.TypeTemplateParameterCase'
+ value: 'CamelCase'
+ - key: 'readability-identifier-naming.UnionCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.ValueTemplateParameterCase'
+ value: 'CamelCase'
+ - key: 'readability-identifier-naming.VariableCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-naming.VirtualMethodCase'
+ value: 'lower_case'
+ - key: 'readability-identifier-length.MinimumVariableNameLength'
+ value: '2'
+ - key: 'readability-identifier-length.MinimumParameterNameLength'
+ value: '2'
+...
diff --git a/.codespellrc b/.codespellrc
new file mode 100644
index 0000000..d3634a3
--- /dev/null
+++ b/.codespellrc
@@ -0,0 +1,6 @@
+[codespell]
+builtin = clear,rare,en-GB_to_en-US,names,informal,code
+check-filenames =
+check-hidden =
+skip = */.git,*/build,*/prefix,*/conan
+quiet-level = 2
diff --git a/.coveralls.yml b/.coveralls.yml
new file mode 100644
index 0000000..38d8835
--- /dev/null
+++ b/.coveralls.yml
@@ -0,0 +1,2 @@
+service_name: github-actions
+
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..35976d4
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,17 @@
+---
+name: Bug report
+about: Create a report to help us improve
+title: ''
+labels: bug
+assignees: ''
+
+---
+
+**lnav version**
+v0.11.1 is the latest
+
+**Describe the bug**
+A clear and concise description of what the bug is.
+
+**To Reproduce**
+Steps to reproduce the behavior:
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..11fc491
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,20 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+title: ''
+labels: enhancement
+assignees: ''
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/actions/muslbuilder/Dockerfile b/.github/actions/muslbuilder/Dockerfile
new file mode 100644
index 0000000..41d0910
--- /dev/null
+++ b/.github/actions/muslbuilder/Dockerfile
@@ -0,0 +1,162 @@
+FROM alpine:3.16
+
+LABEL com.github.actions.name="C++ MUSL Builder Slim"
+LABEL com.github.actions.description="Provides a C++ MUSL environment"
+LABEL com.github.actions.icon="settings"
+LABEL com.github.actions.color="orange"
+
+RUN apk update && apk add --no-cache \
+ build-base \
+ binutils \
+ m4 \
+ git \
+ cmake \
+ make \
+ libgcc \
+ musl-dev \
+ gcc \
+ g++ \
+ lz4 \
+ lz4-dev \
+ lz4-static \
+ zip \
+ zstd \
+ zstd-dev \
+ zstd-static \
+ perl \
+ autoconf \
+ automake \
+ elfutils \
+ elfutils-dev \
+ libelf-static \
+ libexecinfo-dev \
+ libexecinfo-static \
+ libtool \
+ libunwind \
+ libunwind-dev \
+ libunwind-static \
+ linux-headers
+
+ADD https://www.libarchive.org/downloads/libarchive-3.6.2.tar.gz /
+ADD https://ftp.gnu.org/gnu/make/make-4.2.1.tar.gz /
+ADD https://ftp.gnu.org/gnu/ncurses/ncurses-6.4.tar.gz /
+ADD https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.42/pcre2-10.42.tar.gz /
+ADD https://ftp.gnu.org/gnu/readline/readline-6.3.tar.gz /
+ADD https://zlib.net/zlib-1.2.13.tar.gz /
+ADD https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz /
+ADD https://www.sqlite.org/2023/sqlite-autoconf-3420000.tar.gz /
+ADD https://www.openssl.org/source/openssl-1.0.2n.tar.gz /
+ADD https://www.libssh2.org/download/libssh2-1.11.0.tar.gz /
+ADD https://curl.se/download/curl-8.1.2.tar.gz /
+ADD https://tukaani.org/xz/xz-5.4.3.tar.gz /
+
+RUN mkdir -p /fake.root /packages /extract
+ENV SQLITE_CFLAGS="\
+ -DSQLITE_ENABLE_COLUMN_METADATA \
+ -DSQLITE_SOUNDEX \
+ -DSQLITE_ENABLE_DBSTAT_VTAB \
+ -DSQLITE_ENABLE_API_ARMOR \
+ -DSQLITE_ENABLE_JSON1 \
+ -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
+ "
+
+ENV NCURSES_FALLBACKS="\
+ansi,\
+cygwin,\
+Eterm,\
+Eterm-256color,\
+gnome,\
+gnome-256color,\
+konsole,\
+konsole-256color,\
+linux,\
+putty,\
+rxvt,\
+rxvt-256color,\
+screen,\
+screen-16color,\
+screen-256color,\
+tmux,\
+tmux-256color,\
+vt100,\
+vt220,\
+xterm,\
+xterm-256color\
+"
+
+RUN cd /extract && for pkg in /*.tar.gz; do tar xvfz "$pkg"; done
+
+RUN cd /extract/bzip2-1.0.8 && make install PREFIX=/fake.root && make clean
+
+RUN cd /extract/zlib-* && ./configure --prefix=/fake.root && make -j2 && make install && make clean
+
+RUN cd /extract/xz-* && \
+ ./configure --prefix=/fake.root \
+ --disable-shared \
+ "LDFLAGS=-L/fake.root/lib" \
+ "CPPFLAGS=-I/fake.root/include" \
+ && \
+ make -j2 && \
+ make install && make clean
+
+RUN cd /extract/libarchive-* && \
+ ./configure --prefix=/fake.root \
+ --disable-shared \
+ "LDFLAGS=-L/fake.root/lib" \
+ "CPPFLAGS=-I/fake.root/include" \
+ && \
+ make -j2 && \
+ make install && make clean
+
+RUN cd /extract/make-4.2.1 && ./configure --prefix=/fake.root && make -j2 && make install && make clean
+RUN cd /extract/readline-* && ./configure --prefix=/fake.root && make -j2 && make install && make clean
+
+RUN cd /extract/sqlite-* && \
+ ./configure --disable-editline --prefix=/fake.root \
+ CFLAGS="${SQLITE_CFLAGS}" \
+ && \
+ make -j2 && make install && make clean
+
+RUN cd /extract/openssl-* && \
+ ./config --prefix=/fake.root -fPIC && \
+ make -j2 && \
+ make install && make clean
+
+RUN cd /extract/ncurses-* && \
+ ./configure --prefix=/fake.root \
+ --enable-ext-mouse \
+ --enable-sigwinch \
+ --with-default-terminfo-dir=/usr/share/terminfo \
+ --enable-ext-colors \
+ --enable-widec \
+ --enable-termcap \
+ --with-fallbacks=$NCURSES_FALLBACKS \
+ && \
+ make -j2 && make install && make clean
+
+RUN cd /extract/pcre2-* && \
+ ./configure --prefix=/fake.root \
+ --enable-jit \
+ && \
+ make -j2 && make install && make clean
+
+RUN cd /extract/libssh2-* && \
+ ./configure --prefix=/fake.root \
+ --with-libssl-prefix=/fake.root \
+ --with-libz-prefix=/fake.root \
+ "CPPFLAGS=-I/fake.root/include" \
+ "LDFLAGS=-ldl -L/fake.root/lib" && \
+ make -j2 && \
+ make install && make clean
+
+RUN cd /extract/curl-* && \
+ ./configure --prefix=/fake.root \
+ --with-libssh2=/fake.root \
+ --with-ssl=/fake.root \
+ --with-zlib=/fake.root \
+ "LDFLAGS=-ldl" && \
+ make -j2 && \
+ make install && make clean
+
+COPY entrypoint.sh /entrypoint.sh
+ENTRYPOINT ["/entrypoint.sh"]
diff --git a/.github/actions/muslbuilder/entrypoint.sh b/.github/actions/muslbuilder/entrypoint.sh
new file mode 100755
index 0000000..7c40420
--- /dev/null
+++ b/.github/actions/muslbuilder/entrypoint.sh
@@ -0,0 +1,18 @@
+#!/bin/sh
+
+set -Eeuxo pipefail
+
+cd $GITHUB_WORKSPACE
+./autogen.sh
+mkdir lbuild
+cd lbuild
+../configure \
+ --with-libarchive=/fake.root \
+ CFLAGS='-static -g1 -gz=zlib -no-pie -O2' \
+ CXXFLAGS='-static -g1 -gz=zlib -U__unused -no-pie -O2' \
+ LDFLAGS="-L/fake.root/lib" \
+ CPPFLAGS="-I/fake.root/include" \
+ LIBS="-L/fake.root/lib -lexecinfo -lssh2 -llzma -lssl -lcrypto -lz -llz4" \
+ --enable-static \
+ PATH="/fake.root/bin:${PATH}"
+make -j2
diff --git a/.github/mlc_config.json b/.github/mlc_config.json
new file mode 100644
index 0000000..be01fdb
--- /dev/null
+++ b/.github/mlc_config.json
@@ -0,0 +1,10 @@
+{
+ "ignorePatterns": [
+ {
+ "pattern": "^/assets"
+ },
+ {
+ "pattern": "^https://archive.org"
+ }
+ ]
+} \ No newline at end of file
diff --git a/.github/workflows/bins.yml b/.github/workflows/bins.yml
new file mode 100644
index 0000000..27a9a04
--- /dev/null
+++ b/.github/workflows/bins.yml
@@ -0,0 +1,114 @@
+name: bins
+
+on:
+ push:
+ branches:
+ - master
+ paths-ignore:
+ - docs
+ - README.md
+ - NEWS.md
+ workflow_call:
+ inputs:
+ lnav_version_number:
+ description: The version number of the release
+ required: false
+ type: string
+ upload_url:
+ description: The URL to upload release assets
+ required: false
+ type: string
+
+jobs:
+ build-musl:
+ runs-on: ubuntu-latest
+ container:
+ image: tstack/lnav-build:1
+ env:
+ LNAV_BASENAME: lnav-${{ inputs.lnav_version_number }}
+ LNAV_ZIPNAME: lnav-${{ inputs.lnav_version_number }}-x86_64-linux-musl.zip
+ steps:
+ - name: checkout
+ uses: actions/checkout@v3
+ - name: make
+ run: /entrypoint.sh
+ - name: Build musl package
+ if: ${{ inputs.lnav_version_number != '' }}
+ run: >-
+ mkdir ${{ env.LNAV_BASENAME }} &&
+ cd ${{ env.LNAV_BASENAME }} &&
+ cp ../NEWS.md ../README . &&
+ cp ../lbuild/src/lnav . &&
+ cd .. &&
+ zip -r ${{ env.LNAV_ZIPNAME }} ${{ env.LNAV_BASENAME }}
+ - name: Upload a Build Artifact
+ uses: actions/upload-artifact@v3
+ with:
+ # Artifact name
+ name: lnav-linux-musl-64bit.zip
+ # A file, directory or wildcard pattern that describes what to upload
+ path: lbuild/src/lnav
+ - name: Upload musl-binary archive
+ uses: actions/upload-release-asset@v1.0.2
+ if: ${{ inputs.upload_url != '' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ inputs.upload_url }}
+ asset_path: ${{ env.LNAV_ZIPNAME }}
+ asset_name: ${{ env.LNAV_ZIPNAME }}
+ asset_content_type: application/octet-stream
+
+ build-macos:
+ runs-on: macos-12
+ env:
+ LNAV_BASENAME: lnav-${{ inputs.lnav_version_number }}
+ LNAV_ZIPNAME: lnav-${{ inputs.lnav_version_number }}-x86_64-macos.zip
+ steps:
+ - name: checkout
+ uses: actions/checkout@v3
+ - name: install packages
+ run: brew install pcre2 sqlite ncurses xz zstd readline libarchive curl autoconf automake
+ - name: autogen
+ run: ./autogen.sh
+ - name: configure
+ run: >-
+ ./configure --enable-static \
+ --with-libcurl=/usr \
+ --with-pcre2=$(brew --prefix pcre2) \
+ --with-sqlite3=$(brew --prefix sqlite3) \
+ "CXXFLAGS=-I$(brew --prefix ncurses)/include -g2 -O2" \
+ 'CFLAGS=-O2 -g2' \
+ "LDFLAGS=-L$(brew --prefix ncurses)/lib -L$(brew --prefix xz)/lib -L$(brew --prefix zstd)/lib/" \
+ --with-readline=$(brew --prefix readline) \
+ --with-libarchive=$(brew --prefix libarchive) \
+ "LIBS=-llzma -lzstd -lbrotlidec-static -liconv -llz4"
+ - name: make
+ run: make -j2
+ - name: Build macos package
+ if: ${{ inputs.lnav_version_number != '' }}
+ run: >-
+ mkdir ${{ env.LNAV_BASENAME }} &&
+ cd ${{ env.LNAV_BASENAME }} &&
+ cp ../NEWS.md ../README . &&
+ cp ../src/lnav . &&
+ cd .. &&
+ zip -r ${{ env.LNAV_ZIPNAME }} ${{ env.LNAV_BASENAME }}
+ - name: Upload config.log artifact
+ uses: actions/upload-artifact@v3
+ if: ${{ always() }}
+ with:
+ # Artifact name
+ name: config-log.zip
+ # A file, directory or wildcard pattern that describes what to upload
+ path: config.log
+ - name: Upload macos archive
+ uses: actions/upload-release-asset@v1.0.2
+ if: ${{ inputs.upload_url != '' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ inputs.upload_url }}
+ asset_path: ${{ env.LNAV_ZIPNAME }}
+ asset_name: ${{ env.LNAV_ZIPNAME }}
+ asset_content_type: application/octet-stream
diff --git a/.github/workflows/c-cpp.yml b/.github/workflows/c-cpp.yml
new file mode 100644
index 0000000..66188af
--- /dev/null
+++ b/.github/workflows/c-cpp.yml
@@ -0,0 +1,194 @@
+name: ci-build
+
+on:
+ push:
+ branches: [ master ]
+ paths-ignore:
+ - docs
+ - README.md
+ - NEWS.md
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ coverage:
+ runs-on: self-hosted
+ steps:
+ - uses: actions/checkout@v3
+# - name: Update apt
+# run: sudo apt-get update
+# - name: Install packages
+# run: sudo apt-get install libncursesw5-dev libpcre++-dev libsqlite3-dev libbz2-dev libcurl4-openssl-dev libreadline-dev zlib1g-dev lcov
+# - name: install cpp-coveralls
+# run: pip install --user cpp-coveralls
+ - name: autogen
+ run: ./autogen.sh
+ - name: configure
+ run: ./configure --disable-static --enable-code-coverage --enable-debug CFLAGS=-g3 CXXFLAGS=-g3
+ - name: make
+ run: make -j3
+ - name: make check
+ run: make check
+ - name: upload cover
+ env:
+ COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }}
+ TRAVIS_JOB_ID: ${{ github.run_id }}-${{ github.run_number }}
+ run: >-
+ coveralls
+ --exclude src/doctest.hh
+ --exclude src/fmtlib
+ --exclude src/ghc
+ --exclude src/k_merge_tree.h
+ --exclude src/mapbox
+ --exclude src/pugixml
+ --exclude src/base/result.h
+ --exclude src/safe
+ --exclude src/spookyhash
+ --exclude src/third-party
+ --exclude src/ww898
+ --exclude src/yajl
+ --exclude test
+ --exclude src/data_scanner_re.cc
+ --gcov-options '\-lp'
+
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Update apt
+ run: sudo apt-get update
+ - name: Install packages
+ run: >-
+ sudo apt-get install -y
+ make
+ automake
+ autoconf
+ g++
+ libpcre2-dev
+ libpcre3-dev
+ libncurses-dev
+ libsqlite3-dev
+ libbz2-dev
+ libcurl4-openssl-dev
+ libreadline-dev
+ tshark
+ zlib1g-dev
+ - name: autogen
+ run: ./autogen.sh
+ - name: configure
+ run: ./configure --disable-static
+ - name: make
+ run: make -j4
+ - name: make distcheck
+ run: make distcheck -j4 || (test -e lnav-*/_build/sub/src/tailer/test-suite.log && cat lnav-*/_build/sub/src/tailer/test-suite.log && false) || (test -e lnav-*/_build/sub/test/test-suite.log && cat lnav-*/_build/sub/test/test-suite.log && false)
+ - name: Upload a Build Artifact
+ uses: actions/upload-artifact@v3
+ with:
+ # Artifact name
+ name: lnav-tot-linux-64bit.zip
+ # A file, directory or wildcard pattern that describes what to upload
+ path: src/lnav
+
+ build-windows:
+ runs-on: windows-latest
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - { icon: '🟦', sys: MSYS }
+ name: ${{ matrix.icon }} ${{ matrix.sys }}
+ defaults:
+ run:
+ shell: msys2 {0}
+ steps:
+ - name: '🧰 Checkout'
+ uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: '${{ matrix.icon }} Setup MSYS2'
+ uses: msys2/setup-msys2@v2
+ with:
+ msystem: ${{matrix.sys}}
+ update: true
+ install: >-
+ autoconf
+ automake
+ gcc
+ git
+ make
+ zip
+ msys/libarchive-devel
+ msys/libbz2-devel
+ msys/libcurl-devel
+ msys/libidn2-devel
+ msys/liblzma-devel
+ msys/libreadline-devel
+ msys/libsqlite-devel
+ msys/libunistring-devel
+ msys/ncurses-devel
+ msys/pcre2-devel
+ msys/zlib-devel
+ - name: '🔧 Generate and configure'
+ run: |
+ set -x
+ ./autogen.sh
+ mkdir -p ../lnav-build
+ cd ../lnav-build
+ export PREFIX=$PWD/lnav
+ ../lnav/configure \
+ --enable-static \
+ LDFLAGS="-static" \
+ CPPFLAGS="-I../src -I../../lnav/src -I../../lnav/src/fmtlib -O2 -DNCURSES_STATIC" \
+ CXXFLAGS="-fPIC" \
+ CFLAGS="-fPIC" \
+ LIBS="-larchive -lssh2 -llzma -llz4 -lz -lzstd -lssl -lcrypto -liconv -lunistring -lbrotlicommon" \
+ --sysconfdir=/etc \
+ --prefix=$PREFIX || cat config.log
+ - name: '🚧 Make (do not use -j)'
+ run: |
+ set -x
+ cd ../lnav-build
+ make CFLAGS="-c"
+ strip -s src/lnav.exe
+ - name: '📦 Package for distribution'
+ run: |
+ set -x
+ cd ../lnav-build
+ export PREFIX=$PWD/lnav
+ make install
+ ldd $PREFIX/bin/lnav.exe | grep /usr | cut -d' ' -f3 | xargs -I {} cp {} $PREFIX/bin/
+ mkdir -p lib/terminfo/78
+ cp -r /usr/lib/terminfo/78/xterm-256color lib/terminfo/78/
+ zip -r ../lnav/lnav-${{ github.ref_name }}-windows-amd64.zip lnav lib
+ - name: '💉 Basic test'
+ run: |
+ set -x
+ cd ../lnav-build
+ export PREFIX=$PWD/lnav
+ $PREFIX/bin/lnav.exe -n ../lnav/test/logfile_multiline.0
+ - name: '⬆️ Upload a Build Artifact'
+ uses: actions/upload-artifact@v3
+ with:
+ name: lnav-${{ github.ref_name }}-windows-amd64.zip
+ path: lnav-${{ github.ref_name }}-windows-amd64.zip
+ if-no-files-found: error
+# - name: '🎁 Create Release'
+# id: create_release
+# uses: actions/create-release@v1
+# env:
+# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+# with:
+# tag_name: ${{ github.ref_name }}
+# release_name: Release ${{ github.ref_name }}
+# draft: false
+# prerelease: false
+# - name: '⬆️ Upload Release Asset'
+# id: upload-release-asset
+# uses: actions/upload-release-asset@v1
+# env:
+# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+# with:
+# upload_url: ${{ steps.create_release.outputs.upload_url }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
+# asset_path: ./lnav-${{ github.ref_name }}-windows-amd64.zip
+# asset_name: lnav-${{ github.ref_name }}-windows-amd64.zip
+# asset_content_type: application/zip
diff --git a/.github/workflows/check-md-links.yml b/.github/workflows/check-md-links.yml
new file mode 100644
index 0000000..f55b468
--- /dev/null
+++ b/.github/workflows/check-md-links.yml
@@ -0,0 +1,12 @@
+name: Check Markdown links
+
+on: push
+
+jobs:
+ markdown-link-check:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@master
+ - uses: gaurav-nelson/github-action-markdown-link-check@v1
+ with:
+ config-file: '.github/mlc_config.json'
diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml
new file mode 100644
index 0000000..a081f33
--- /dev/null
+++ b/.github/workflows/coverity.yml
@@ -0,0 +1,48 @@
+# GitHub actions workflow.
+# https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions
+
+name: Coverity Scan
+
+on:
+ push:
+ branches: [ main ]
+
+ schedule:
+ # The GH mirroring from Google GoB does not trigger push actions.
+ # Fire it once a week to provide some coverage.
+ - cron: '39 2 * * WED'
+
+ # Allow for manual triggers from the web.
+ workflow_dispatch:
+
+jobs:
+ coverity:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Update apt
+ run: sudo apt-get update
+ - name: Install packages
+ run: >-
+ sudo apt-get install -y
+ make
+ automake
+ autoconf
+ g++
+ libpcre3-dev
+ libncurses-dev
+ libsqlite3-dev
+ libbz2-dev
+ libcurl4-openssl-dev
+ libreadline-dev
+ tshark
+ zlib1g-dev
+ - name: autogen
+ run: ./autogen.sh
+ - name: configure
+ run: ./configure --disable-static
+ - uses: vapier/coverity-scan-action@v1
+ with:
+ command: make -j$(getconf _NPROCESSORS_CONF)
+ email: ${{ secrets.COVERITY_SCAN_EMAIL }}
+ token: ${{ secrets.COVERITY_SCAN_TOKEN }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..650f053
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,98 @@
+
+name: release
+on:
+ push:
+ tags:
+ - '*'
+jobs:
+ create-release:
+ name: create-release
+ runs-on: ubuntu-latest
+ outputs:
+ upload_url: ${{ steps.release.outputs.upload_url }}
+ lnav_version_number: ${{ env.LNAV_VERSION_NUMBER }}
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ - name: Make release notes
+ run: make -C release release-NEWS.md
+ - name: Get the release version from the tag
+ shell: bash
+ run: |
+ # Apparently, this is the right way to get a tag name. Really?
+ #
+ # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
+ echo "LNAV_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
+ echo "LNAV_VERSION_NUMBER=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
+ echo "version is: ${{ env.LNAV_VERSION }}"
+ - name: Create GitHub release
+ id: release
+ uses: actions/create-release@v1
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ tag_name: ${{ env.LNAV_VERSION }}
+ release_name: ${{ env.LNAV_VERSION }}
+ body_path: release/release-NEWS.md
+ draft: true
+
+ build-binaries:
+ name: build-binaries
+ needs: ['create-release']
+ uses: ./.github/workflows/bins.yml
+ secrets: inherit
+ with:
+ lnav_version_number: ${{ needs.create-release.outputs.lnav_version_number }}
+ upload_url: ${{ needs.create-release.outputs.upload_url }}
+
+ build-release:
+ name: build-release
+ needs: ['create-release']
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v3
+ - name: Update apt
+ run: sudo apt-get update
+ - name: Install packages
+ run: >-
+ sudo apt-get install -y
+ make
+ automake
+ autoconf
+ g++
+ libpcre2-dev
+ libpcre3-dev
+ libncurses-dev
+ libsqlite3-dev
+ libbz2-dev
+ libcurl4-openssl-dev
+ libreadline-dev
+ zlib1g-dev
+ - name: autogen
+ run: ./autogen.sh
+ - name: configure
+ run: ./configure --disable-static
+ - name: make dist
+ run: >-
+ make dist dist-bzip2 &&
+ mv -n lnav-*.tar.gz lnav-${{ needs.create-release.outputs.lnav_version_number }}.tar.gz &&
+ mv -n lnav-*.tar.bz2 lnav-${{ needs.create-release.outputs.lnav_version_number }}.tar.bz2
+ - name: Upload tgz archive
+ uses: actions/upload-release-asset@v1.0.2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ needs.create-release.outputs.upload_url }}
+ asset_path: lnav-${{ needs.create-release.outputs.lnav_version_number }}.tar.gz
+ asset_name: lnav-${{ needs.create-release.outputs.lnav_version_number }}.tar.gz
+ asset_content_type: application/octet-stream
+ - name: Upload tbz2 archive
+ uses: actions/upload-release-asset@v1.0.2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ with:
+ upload_url: ${{ needs.create-release.outputs.upload_url }}
+ asset_path: lnav-${{ needs.create-release.outputs.lnav_version_number }}.tar.bz2
+ asset_name: lnav-${{ needs.create-release.outputs.lnav_version_number }}.tar.bz2
+ asset_content_type: application/octet-stream
diff --git a/.github/workflows/rpmbuild.yml b/.github/workflows/rpmbuild.yml
new file mode 100644
index 0000000..67e6d83
--- /dev/null
+++ b/.github/workflows/rpmbuild.yml
@@ -0,0 +1,49 @@
+
+name: RPM Build
+
+on:
+ push:
+ branches: [ master ]
+ paths-ignore:
+ - docs
+ - README.md
+ - NEWS.md
+ tags:
+ - '*'
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+
+ - name: Get the release version from the tag
+ shell: bash
+ run: |
+ # Apparently, this is the right way to get a tag name. Really?
+ #
+ # See: https://github.community/t5/GitHub-Actions/How-to-get-just-the-tag-name/m-p/32167/highlight/true#M1027
+ echo "LNAV_VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
+ echo "LNAV_VERSION_NUMBER=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV
+ echo "version is: ${{ env.LNAV_VERSION }}"
+ make -C release lnav.spec
+ - name: build RPM package
+ id: rpm
+ uses: tstack/rpmbuild@master
+ with:
+ spec_file: "release/lnav.spec"
+
+ - name: Upload artifact
+ uses: actions/upload-artifact@v1.0.0
+ with:
+ name: Binary RPM
+ path: ${{ steps.rpm.outputs.rpm_dir_path }}
+
+ - name: Push to packagecloud.io
+ uses: tstack/upload-packagecloud@main
+ if: github.ref_type == 'tag'
+ with:
+ userrepo: tstack/lnav
+ apitoken: ${{ secrets.PACKAGECLOUD_TOKEN }}
+ packages: ${{ steps.rpm.outputs.rpm_dir_path }}x86_64
+ rpmdists: el/8
diff --git a/.github/workflows/tailer-ape.yml b/.github/workflows/tailer-ape.yml
new file mode 100644
index 0000000..e487507
--- /dev/null
+++ b/.github/workflows/tailer-ape.yml
@@ -0,0 +1,37 @@
+name: tailer-ape
+
+on:
+ push:
+ branches: [ master ]
+ paths-ignore:
+ - docs
+ - README.md
+ - NEWS.md
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: update apt
+ run: sudo apt-get update
+ - name: Install packages
+ run: >-
+ wget https://github.com/tstack/cosmopolitan/releases/download/v1.0stack/cosmopolitan-amalgamation-1.0-stack.zip &&
+ unzip cosmopolitan-amalgamation-1.0-stack.zip
+ - name: Build
+ run: >-
+ gcc -g -Os -static -nostdlib -nostdinc -fno-pie -no-pie -mno-red-zone
+ -fno-omit-frame-pointer -pg -mnop-mcount
+ -o tailer.dbg -I src/tailer
+ src/tailer/tailer.main.c src/tailer/tailer.c src/tailer/sha-256.c
+ -fuse-ld=bfd -Wl,-T,ape.lds
+ -include cosmopolitan.h crt.o ape.o cosmopolitan.a
+ - name: Objcopy
+ run: objcopy -S -O binary tailer.dbg src/tailer/tailer.ape
+ - uses: stefanzweifel/git-auto-commit-action@v4
+ with:
+ # Optional, but recommended
+ # Defaults to "Apply automatic changes"
+ commit_message: Update tailer
+ file_pattern: src/tailer/tailer.ape
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..100682c
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,104 @@
+.deps
+.lnav
+*.dat
+*.diff
+test/*.err
+*.index
+*.log
+*.o
+*.obj
+*.pyc
+*.tmp
+*.trs
+*~
+.idea/
+Makefile
+Makefile.in
+TESTS_ENVIRONMENT
+aclocal.m4
+aminclude_static.am
+ar-lib
+autom4te.cache
+compile
+config.guess
+config.log
+config.status
+config.sub
+configure
+depcomp
+install-sh
+missing
+mkinstalldirs
+test-driver
+docs/build
+release/release-NEWS.md
+release/linux-pkg/
+release/osx-build-dir/
+release/osx-pkg/
+release/outbox/
+release/vagrant-static-linux/.vagrant
+release/vagrant-static-linux/lnav
+cmake-build-debug/
+src/bin2c
+src/config.h
+src/config.h.in
+src/default-config-json.c
+src/default-log-formats-json.c
+src/dump-pid-sh.c
+src/help.c
+src/init-sql.c
+src/libdiag.a
+src/lnav
+src/lnav-test
+src/ptimec
+src/spookyhash/.dirstamp
+src/stamp-h1
+src/static-libs/
+src/time_fmts.cc
+src/yajl/.dirstamp
+test/drive_data_scanner
+test/drive_grep_proc
+test/drive_json_op
+test/drive_json_ptr_walk
+test/drive_line_buffer
+test/drive_listview
+test/drive_logfile
+test/drive_mvwattrline
+test/drive_readline_curses
+test/drive_sequencer
+test/drive_sql
+test/drive_view_colors
+test/drive_vt52_curses
+test/logfile_append.0
+test/logfile_syslog.1.bz2
+test/logfile_syslog.1.gz
+test/scanned.dpt
+test/scripty
+test/simple-db.db
+test/slicer
+test/test_ansi_scrubber
+test/test_auto_fd
+test/test_auto_mem
+test/test_bookmarks
+test/test_chunky_index
+test/test_concise
+test/test_date_time_scanner
+test/test_grep_proc2
+test/test_hist_source
+test/test_json_ptr
+test/test_line_buffer2
+test/test_log_accel
+test/test_pcrepp
+test/test_top_status
+test/test_yajlpp
+test/truncfile.0
+cmake-build/
+.vs/
+.vscode/
+build/
+cmake/open-cpp-coverage.cmake
+cmake-build-*/
+conan/
+prefix/
+CMakeLists.txt.user
+CMakeUserPresets.json
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..8bff71c
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "ext/pcapplusplus"]
+ path = ext/pcapplusplus
+ url = https://github.com/seladb/PcapPlusPlus.git
diff --git a/.readthedocs.yaml b/.readthedocs.yaml
new file mode 100644
index 0000000..fd30369
--- /dev/null
+++ b/.readthedocs.yaml
@@ -0,0 +1,20 @@
+# .readthedocs.yaml
+# Read the Docs configuration file
+# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
+
+# Required
+version: 2
+
+# Build documentation in the docs/ directory with Sphinx
+sphinx:
+ configuration: docs/source/conf.py
+
+# Optionally build your docs in additional formats such as PDF
+formats:
+ - pdf
+
+# Optionally set the version of Python and requirements required to build your docs
+python:
+ version: 3.7
+ install:
+ - requirements: docs/requirements.txt
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..3ef11cb
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,121 @@
+# Architecture
+
+This document covers the internal architecture of the Logfile Navigator (lnav),
+a terminal-based tool for viewing and analyzing log files.
+
+## Goals
+
+The following goals drive the design and implementation of lnav:
+
+- Don't make the user do something that can be done automatically.
+
+ Example: Automatically detect log formats for files instead of making them
+ specify the format for each file.
+
+- Be performant on low-spec hardware.
+
+ Example: Prefer single-threaded optimizations over trying to parallelize
+
+- Operations should be "live" and not block the user from continuing to work.
+
+ Example: Searches are run in the background.
+
+- Provide context-sensitive help.
+
+ Example: When the cursor is over a SQL keyword/function, the help text for
+ that is shown above.
+
+- Show a preview of operations so the user knows what is going to happen.
+
+ Example: When entering a `:filter-out` command, the matched parts of the
+ lines are highlighted in red.
+
+## Overview
+
+The whole of lnav consists of a
+[log file parser](https://docs.lnav.org/en/latest/formats.html),
+[text UI](https://docs.lnav.org/en/latest/ui.html),
+[integrations with SQLite](https://docs.lnav.org/en/latest/sqlext.html),
+[command-line interface](https://docs.lnav.org/en/latest/cli.html), and
+[commands for operating on logs](https://docs.lnav.org/en/latest/commands.html).
+Since the majority of lnav's operations center around logs, the core
+data-structure is the combined log message index. The message index is populated
+when new messages are read from log files. The text UI displays a subset of
+messages from the index. The SQLite virtual-tables allow for programmatic access
+to the messages and lnav's internal state.
+
+[![lnav architecture](docs/lnav-architecture.png)](https://whimsical.com/lnav-architecture-UM594Qo4G3nt2XWaSZA1mh)
+
+## File Monitoring
+
+Each file being monitored by lnav has an associated [`logfile`](src/logfile.hh)
+object, be they plaintext files or files with a recognized format. These
+objects are periodically polled by the main event loop to check if the file
+was deleted, truncated, or new lines added. While reading new lines, if no
+log format has matched yet, each line will be passed through the log format
+regular expressions to try and find a match. Each line that is read is added
+to an index
+
+#### Why is `mmap()` not used?
+
+Note that file contents are consumed using `pread(2)`/`read(2)` and not
+`mmap(2)` since `mmap(2)` does not react well to files changing out from
+underneath it. For example, a truncated file would likely result in a
+`SIGBUS`.
+
+## Log Messages
+
+As files are being indexed, if a matching format is found, the file is
+"promoted" from a plaintext file to a log file. When the file is promoted,
+it is added to the [logfile_sub_source](src/logfile_sub_source.hh), which
+collates all log messages together into a single index.
+
+### Timestamp Parsing
+
+Since all log messages need to have a timestamp, timestamp parsing needs to be
+very efficient. The standard `strptime()` function is quite expensive, so lnav
+includes an optimized custom parser and code-generator in the
+[ptimec](src/ptimec.hh) component. The code-generator is used at compile-time
+to generate parsers for several [common formats](src/time_formats.am).
+
+## Log Formats
+
+[log_format](src/log_format.hh) instances are used to parse lines from files
+into `logline` objects. The majority of log formats are
+[external_log_format](src/log_format_ext.hh) objects that are create from
+[JSON format definitions](https://docs.lnav.org/en/latest/formats.html). The
+built-in definitions are located in the [formats](src/formats) directory. Log
+formats that cannot be handled through a simple regular expression are
+implemented in the [log_format_impls.cc](src/log_format_impls.cc) file.
+
+## User Interface
+
+The lnav text-user-interface is built on top of
+[ncurses](https://invisible-island.net/ncurses/announce.html).
+However, the higher-level functionality of panels, widgets, and such is not
+used. Instead, the following custom components are built on top of the ncurses
+primitives:
+
+- [view_curses](src/view_curses.hh) - Provides the basics for text roles, which
+ allows for themes to color and style text. The `mvwattrline()` function does
+ all the heavy lifting of drawing ["attributed" lines](src/base/attr_line.hh),
+ which are strings that have attributes associated with a given range of
+ characters.
+- [listview_curses](src/listview_curses.hh) - Displays a list of items that are
+ provided by a source.
+- [textview_curses](src/textview_curses.hh) - Builds on the list view by adding
+ support for searching, filtering, bookmarks, etc... The main panel that
+ displays the logs/plaintext/help is a textview.
+- [statusview_curses](src/state-extension-functions.cc) - Draws the status bars
+ at the top and bottom of the TUI.
+- [vt52_curses](src/vt52_curses.hh) - Adapts vt52 escape codes to the ncurses
+ API.
+- [readline_curses](src/readline_curses.hh) - Provides access to the readline
+ library. The readline code is executed in a child process since readline
+ does not get along with ncurses. The child process and readline is set to
+ use a vt52 terminal and the vt52_curses view is uses to translate those
+ escape codes to ncurses.
+
+The following diagram shows the underlying components that make up the TUI:
+
+[![lnav TUI](docs/lnav-tui.png)](https://whimsical.com/lnav-tui-MQjXc7Vx23BxQTHrnuNp5F)
diff --git a/AUTHORS b/AUTHORS
new file mode 100644
index 0000000..c5bb74f
--- /dev/null
+++ b/AUTHORS
@@ -0,0 +1,48 @@
+
+Primary Author
+--------------
+
+Timothy Stack (timothyshanestack@gmail.com)
+
+Contributors
+------------
+
+Suresh Sundriyal (sureshsundriyal@gmail.com)
+Matt Dordal (matt@dordal.org)
+Christopher Meng
+Salvatore Bonaccorso
+Henrietta Stack
+Pablo Iranzo Gómez
+Brian Cain
+Paul Wayper
+Adam Spiers
+Kevin Pham
+Eli Young
+Victor Hooi
+Michael Bouvy
+Santiago Agüero
+Benny Zlotnik
+Thomas Hurst
+Justin Berger
+Jan Chren
+Geoff Crompton
+Medina Maza
+Phil Hord
+Tristan Ramseyer
+Aurélien Rouëné
+Emiliano Bonassi
+Darragh O'Reilly
+Stéphane Blondon
+Miguel Terron
+Enguerrand de Rochefort
+Nicolas Werner
+Matt Hayden
+Simos Xenitellis
+Finnegan Stack
+Amos Bird
+Cristian Chiru
+Peter Schiffer
+Pedro Pombeiro
+Florian Münchbach
+Fredrik Forséll
+Tobias Gruetzmacher
diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644
index 0000000..0234703
--- /dev/null
+++ b/CMakeLists.txt
@@ -0,0 +1,59 @@
+cmake_minimum_required(VERSION 3.14)
+
+include(cmake/prelude.cmake)
+
+set(CMAKE_CXX_STANDARD 14)
+project(
+ lnav
+ VERSION 0.11.2
+ DESCRIPTION "An advanced log file viewer for the small-scale."
+ HOMEPAGE_URL "https://lnav.org/"
+ LANGUAGES CXX C
+)
+
+include(cmake/project-is-top-level.cmake)
+include(cmake/variables.cmake)
+
+find_package(SQLite3 REQUIRED)
+find_package(BZip2 REQUIRED)
+find_package(LibArchive REQUIRED)
+find_package(ZLIB REQUIRED)
+find_package(pcre REQUIRED)
+find_package(pcre2 REQUIRED)
+find_package(readline REQUIRED)
+find_package(ncurses REQUIRED)
+find_package(CURL REQUIRED)
+
+set(lnav_LIBS
+ CURL::libcurl
+ SQLite::SQLite3
+ BZip2::BZip2
+ ncurses::libcurses
+ pcre::libpcre
+ pcre2::pcre2
+ readline::readline
+ LibArchive::LibArchive
+ ZLIB::ZLIB
+ )
+
+add_subdirectory(src)
+# add_subdirectory(test)
+
+# ---- Install rules ----
+
+if (NOT CMAKE_SKIP_INSTALL_RULES)
+ include(cmake/install-rules.cmake)
+endif ()
+
+# ---- Developer mode ----
+
+if (NOT lnav_DEVELOPER_MODE)
+ return()
+elseif (NOT PROJECT_IS_TOP_LEVEL)
+ message(
+ AUTHOR_WARNING
+ "Developer mode is intended for developers of lnav"
+ )
+endif ()
+
+include(cmake/dev-mode.cmake)
diff --git a/CMakePresets.json b/CMakePresets.json
new file mode 100644
index 0000000..d1adc00
--- /dev/null
+++ b/CMakePresets.json
@@ -0,0 +1,142 @@
+{
+ "version": 2,
+ "cmakeMinimumRequired": {
+ "major": 3,
+ "minor": 14,
+ "patch": 0
+ },
+ "configurePresets": [
+ {
+ "name": "cmake-pedantic",
+ "hidden": true,
+ "warnings": {
+ "dev": true,
+ "deprecated": true,
+ "uninitialized": true,
+ "unusedCli": true,
+ "systemVars": false
+ },
+ "errors": {
+ "dev": true,
+ "deprecated": true
+ }
+ },
+ {
+ "name": "dev-mode",
+ "hidden": true,
+ "inherits": "cmake-pedantic",
+ "cacheVariables": {
+ "lnav_DEVELOPER_MODE": "ON"
+ }
+ },
+ {
+ "name": "conan",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/conan/conan_toolchain.cmake"
+ }
+ },
+ {
+ "name": "cppcheck",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_CXX_CPPCHECK": "cppcheck;--inline-suppr"
+ }
+ },
+ {
+ "name": "clang-tidy",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_CXX_CLANG_TIDY": "clang-tidy;--header-filter=${sourceDir}/*"
+ }
+ },
+ {
+ "name": "ci-std",
+ "description": "This preset makes sure the project actually builds with at least the specified standard",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_CXX_EXTENSIONS": "OFF",
+ "CMAKE_CXX_STANDARD": "14",
+ "CMAKE_CXX_STANDARD_REQUIRED": "ON"
+ }
+ },
+ {
+ "name": "flags-unix",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_CXX_FLAGS": ""
+ }
+ },
+ {
+ "name": "flags-windows",
+ "hidden": true,
+ "cacheVariables": {
+ "CMAKE_CXX_FLAGS": "/W4 /permissive- /utf-8 /volatile:iso /EHsc /Zc:__cplusplus /Zc:throwingNew"
+ }
+ },
+ {
+ "name": "ci-unix",
+ "generator": "Unix Makefiles",
+ "hidden": true,
+ "inherits": ["flags-unix", "ci-std"],
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Release"
+ }
+ },
+ {
+ "name": "ci-win64",
+ "inherits": ["flags-windows", "ci-std"],
+ "generator": "Visual Studio 17 2022",
+ "architecture": "x64",
+ "hidden": true
+ },
+ {
+ "name": "coverage-unix",
+ "binaryDir": "${sourceDir}/build/coverage",
+ "inherits": "ci-unix",
+ "hidden": true,
+ "cacheVariables": {
+ "ENABLE_COVERAGE": "ON",
+ "CMAKE_BUILD_TYPE": "Coverage",
+ "CMAKE_CXX_FLAGS_COVERAGE": "-Og -g --coverage -fkeep-inline-functions -fkeep-static-functions",
+ "CMAKE_EXE_LINKER_FLAGS_COVERAGE": "--coverage",
+ "CMAKE_SHARED_LINKER_FLAGS_COVERAGE": "--coverage",
+ "CMAKE_MAP_IMPORTED_CONFIG_SANITIZE": "Coverage;RelWithDebInfo;Release;Debug;"
+ }
+ },
+ {
+ "name": "ci-coverage",
+ "inherits": ["coverage-unix", "dev-mode", "conan"],
+ "cacheVariables": {
+ "COVERAGE_HTML_COMMAND": ""
+ }
+ },
+ {
+ "name": "ci-sanitize",
+ "binaryDir": "${sourceDir}/build/sanitize",
+ "inherits": ["ci-unix", "dev-mode", "conan"],
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Sanitize",
+ "CMAKE_CXX_FLAGS_SANITIZE": "-O2 -g -fsanitize=address,undefined -fno-omit-frame-pointer -fno-common",
+ "CMAKE_MAP_IMPORTED_CONFIG_SANITIZE": "Sanitize;RelWithDebInfo;Release;Debug;"
+ }
+ },
+ {
+ "name": "ci-build",
+ "binaryDir": "${sourceDir}/build",
+ "hidden": true
+ },
+ {
+ "name": "ci-macos",
+ "inherits": ["ci-build", "ci-unix", "dev-mode", "conan"]
+ },
+ {
+ "name": "ci-ubuntu",
+ "inherits": ["ci-build", "ci-unix", "clang-tidy", "conan", "dev-mode"]
+ },
+ {
+ "name": "ci-windows",
+ "inherits": ["ci-build", "ci-win64", "dev-mode", "conan"]
+ }
+ ]
+}
diff --git a/CMakeUserPresets.json.example b/CMakeUserPresets.json.example
new file mode 100644
index 0000000..64a7918
--- /dev/null
+++ b/CMakeUserPresets.json.example
@@ -0,0 +1,62 @@
+{
+ "version": 2,
+ "cmakeMinimumRequired": {
+ "major": 3,
+ "minor": 14,
+ "patch": 0
+ },
+ "configurePresets": [
+ {
+ "name": "dev-common",
+ "hidden": true,
+ "inherits": ["conan"],
+ "cacheVariables": {
+ "BUILD_MCSS_DOCS": "ON"
+ }
+ },
+ {
+ "name": "dev-unix",
+ "binaryDir": "${sourceDir}/build/dev-unix",
+ "inherits": ["dev-common", "ci-unix", "dev-mode"],
+ "cacheVariables": {
+ "CMAKE_BUILD_TYPE": "Debug"
+ }
+ },
+ {
+ "name": "dev-win64",
+ "binaryDir": "${sourceDir}/build/dev-win64",
+ "inherits": ["dev-common", "ci-win64"]
+ },
+ {
+ "name": "dev",
+ "binaryDir": "${sourceDir}/build/dev",
+ "inherits": "dev-unix"
+ },
+ {
+ "name": "dev-coverage",
+ "binaryDir": "${sourceDir}/build/coverage",
+ "inherits": ["dev-mode", "coverage-unix", "conan"]
+ }
+ ],
+ "buildPresets": [
+ {
+ "name": "dev",
+ "configurePreset": "dev",
+ "configuration": "Debug",
+ "jobs": 4
+ }
+ ],
+ "testPresets": [
+ {
+ "name": "dev",
+ "configurePreset": "dev",
+ "configuration": "Debug",
+ "output": {
+ "outputOnFailure": true
+ },
+ "execution": {
+ "jobs": 4
+ }
+ }
+ ]
+}
diff --git a/FUNDING.yml b/FUNDING.yml
new file mode 100644
index 0000000..0f832f7
--- /dev/null
+++ b/FUNDING.yml
@@ -0,0 +1,2 @@
+# These are supported funding model platforms
+github: [tstack]
diff --git a/INSTALL b/INSTALL
new file mode 100644
index 0000000..a4b3414
--- /dev/null
+++ b/INSTALL
@@ -0,0 +1,229 @@
+Copyright 1994, 1995, 1996, 1999, 2000, 2001, 2002 Free Software
+Foundation, Inc.
+
+ This file is free documentation; the Free Software Foundation gives
+unlimited permission to copy, distribute and modify it.
+
+Basic Installation
+==================
+
+ These are generic installation instructions.
+
+ The `configure' shell script attempts to guess correct values for
+various system-dependent variables used during compilation. It uses
+those values to create a `Makefile' in each directory of the package.
+It may also create one or more `.h' files containing system-dependent
+definitions. Finally, it creates a shell script `config.status' that
+you can run in the future to recreate the current configuration, and a
+file `config.log' containing compiler output (useful mainly for
+debugging `configure').
+
+ It can also use an optional file (typically called `config.cache'
+and enabled with `--cache-file=config.cache' or simply `-C') that saves
+the results of its tests to speed up reconfiguring. (Caching is
+disabled by default to prevent problems with accidental use of stale
+cache files.)
+
+ If you need to do unusual things to compile the package, please try
+to figure out how `configure' could check whether to do them, and mail
+diffs or instructions to the address given in the `README' so they can
+be considered for the next release. If you are using the cache, and at
+some point `config.cache' contains results you don't want to keep, you
+may remove or edit it.
+
+ The file `configure.ac' (or `configure.in') is used to create
+`configure' by a program called `autoconf'. You only need
+`configure.ac' if you want to change it or regenerate `configure' using
+a newer version of `autoconf'.
+
+The simplest way to compile this package is:
+
+ 1. `cd' to the directory containing the package's source code and type
+ `./configure' to configure the package for your system. If you're
+ using `csh' on an old version of System V, you might need to type
+ `sh ./configure' instead to prevent `csh' from trying to execute
+ `configure' itself.
+
+ Running `configure' takes awhile. While running, it prints some
+ messages telling which features it is checking for.
+
+ 2. Type `make' to compile the package.
+
+ 3. Optionally, type `make check' to run any self-tests that come with
+ the package.
+
+ 4. Type `make install' to install the programs and any data files and
+ documentation.
+
+ 5. You can remove the program binaries and object files from the
+ source code directory by typing `make clean'. To also remove the
+ files that `configure' created (so you can compile the package for
+ a different kind of computer), type `make distclean'. There is
+ also a `make maintainer-clean' target, but that is intended mainly
+ for the package's developers. If you use it, you may have to get
+ all sorts of other programs in order to regenerate files that came
+ with the distribution.
+
+Compilers and Options
+=====================
+
+ Some systems require unusual options for compilation or linking that
+the `configure' script does not know about. Run `./configure --help'
+for details on some of the pertinent environment variables.
+
+ You can give `configure' initial values for configuration parameters
+by setting variables in the command line or in the environment. Here
+is an example:
+
+ ./configure CC=c89 CFLAGS=-O2 LIBS=-lposix
+
+ *Note Defining Variables::, for more details.
+
+Compiling For Multiple Architectures
+====================================
+
+ You can compile the package for more than one kind of computer at the
+same time, by placing the object files for each architecture in their
+own directory. To do this, you must use a version of `make' that
+supports the `VPATH' variable, such as GNU `make'. `cd' to the
+directory where you want the object files and executables to go and run
+the `configure' script. `configure' automatically checks for the
+source code in the directory that `configure' is in and in `..'.
+
+ If you have to use a `make' that does not support the `VPATH'
+variable, you have to compile the package for one architecture at a
+time in the source code directory. After you have installed the
+package for one architecture, use `make distclean' before reconfiguring
+for another architecture.
+
+Installation Names
+==================
+
+ By default, `make install' will install the package's files in
+`/usr/local/bin', `/usr/local/man', etc. You can specify an
+installation prefix other than `/usr/local' by giving `configure' the
+option `--prefix=PATH'.
+
+ You can specify separate installation prefixes for
+architecture-specific files and architecture-independent files. If you
+give `configure' the option `--exec-prefix=PATH', the package will use
+PATH as the prefix for installing programs and libraries.
+Documentation and other data files will still use the regular prefix.
+
+ In addition, if you use an unusual directory layout you can give
+options like `--bindir=PATH' to specify different values for particular
+kinds of files. Run `configure --help' for a list of the directories
+you can set and what kinds of files go in them.
+
+ If the package supports it, you can cause programs to be installed
+with an extra prefix or suffix on their names by giving `configure' the
+option `--program-prefix=PREFIX' or `--program-suffix=SUFFIX'.
+
+Optional Features
+=================
+
+ Some packages pay attention to `--enable-FEATURE' options to
+`configure', where FEATURE indicates an optional part of the package.
+They may also pay attention to `--with-PACKAGE' options, where PACKAGE
+is something like `gnu-as' or `x' (for the X Window System). The
+`README' should mention any `--enable-' and `--with-' options that the
+package recognizes.
+
+ For packages that use the X Window System, `configure' can usually
+find the X include and library files automatically, but if it doesn't,
+you can use the `configure' options `--x-includes=DIR' and
+`--x-libraries=DIR' to specify their locations.
+
+Specifying the System Type
+==========================
+
+ There may be some features `configure' cannot figure out
+automatically, but needs to determine by the type of machine the package
+will run on. Usually, assuming the package is built to be run on the
+_same_ architectures, `configure' can figure that out, but if it prints
+a message saying it cannot guess the machine type, give it the
+`--build=TYPE' option. TYPE can either be a short name for the system
+type, such as `sun4', or a canonical name which has the form:
+
+ CPU-COMPANY-SYSTEM
+
+where SYSTEM can have one of these forms:
+
+ OS KERNEL-OS
+
+ See the file `config.sub' for the possible values of each field. If
+`config.sub' isn't included in this package, then this package doesn't
+need to know the machine type.
+
+ If you are _building_ compiler tools for cross-compiling, you should
+use the `--target=TYPE' option to select the type of system they will
+produce code for.
+
+ If you want to _use_ a cross compiler, that generates code for a
+platform different from the build platform, you should specify the
+"host" platform (i.e., that on which the generated programs will
+eventually be run) with `--host=TYPE'.
+
+Sharing Defaults
+================
+
+ If you want to set default values for `configure' scripts to share,
+you can create a site shell script called `config.site' that gives
+default values for variables like `CC', `cache_file', and `prefix'.
+`configure' looks for `PREFIX/share/config.site' if it exists, then
+`PREFIX/etc/config.site' if it exists. Or, you can set the
+`CONFIG_SITE' environment variable to the location of the site script.
+A warning: not all `configure' scripts look for a site script.
+
+Defining Variables
+==================
+
+ Variables not defined in a site shell script can be set in the
+environment passed to `configure'. However, some packages may run
+configure again during the build, and the customized values of these
+variables may be lost. In order to avoid this problem, you should set
+them in the `configure' command line, using `VAR=value'. For example:
+
+ ./configure CC=/usr/local2/bin/gcc
+
+will cause the specified gcc to be used as the C compiler (unless it is
+overridden in the site shell script).
+
+`configure' Invocation
+======================
+
+ `configure' recognizes the following options to control how it
+operates.
+
+`--help'
+`-h'
+ Print a summary of the options to `configure', and exit.
+
+`--version'
+`-V'
+ Print the version of Autoconf used to generate the `configure'
+ script, and exit.
+
+`--cache-file=FILE'
+ Enable the cache: use and save the results of the tests in FILE,
+ traditionally `config.cache'. FILE defaults to `/dev/null' to
+ disable caching.
+
+`--config-cache'
+`-C'
+ Alias for `--cache-file=config.cache'.
+
+`--quiet'
+`--silent'
+`-q'
+ Do not print messages saying which checks are being made. To
+ suppress all normal output, redirect it to `/dev/null' (any error
+ messages will still be shown).
+
+`--srcdir=DIR'
+ Look for the package's source code in directory DIR. Usually
+ `configure' can determine that directory automatically.
+
+`configure' also accepts some other, not widely useful, options. Run
+`configure --help' for more details.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4ddb9f3
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,24 @@
+Copyright (c) 2010, Timothy Stack
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Makefile.am b/Makefile.am
new file mode 100644
index 0000000..d3f34d5
--- /dev/null
+++ b/Makefile.am
@@ -0,0 +1,13 @@
+
+ACLOCAL_AMFLAGS = -I .
+
+SUBDIRS = tools src test
+
+noinst_SCRIPTS = TESTS_ENVIRONMENT
+
+dist_man_MANS = lnav.1
+
+EXTRA_DIST = \
+ AUTHORS \
+ LICENSE \
+ README.md
diff --git a/NEWS.md b/NEWS.md
new file mode 100644
index 0000000..acd36f6
--- /dev/null
+++ b/NEWS.md
@@ -0,0 +1,1037 @@
+## lnav v0.11.2
+
+Features:
+* A "cursor" mode has been added to the main view that can
+ be toggled by pressing CTRL-X. While in cursor mode, any
+ operations that would normally work on the "top" line will
+ now operate on the focused line instead.
+* Added CTRL-D and CTRL-U hotkeys to move down/up by half
+ a page.
+* Added an `auto-width` flag to the elements of the
+ `line-format` array that indicates that the width of the
+ field should automatically be determined by the observed
+ values.
+* Added bunyan log format from Tobias Gruetzmacher.
+* Added cloudlare log format from @minusf.
+* Number fields used in a JSON log format `line-format`
+ array now default to being right-aligned. Also, added
+ `prefix` and `suffix` to `line-format` elements so a
+ string can optionally be prepended/appended if the value
+ is not empty.
+* JSON log format detection has been improved to not rely
+ on matching the file name. All possible formats are
+ tried and the one with the most available fields for a
+ given `line-format` is used. For example, if the first
+ log message has 8 fields and format A contains 5 of
+ those fields in its `line-format` while format B only
+ contains 2 of those fields in its `line-format`, format
+ A will be used for the file.
+
+Changes:
+* For JSON-lines logs, line-feeds at the end of a value are
+ automatically stripped.
+
+Bug Fixes:
+* Hidden values in JSON logs are now hidden by default.
+* Text with ANSI-escapes is now filtered properly.
+
+## lnav v0.11.1
+
+Features:
+* Additional validation checks for log formats have been
+ added and will result in warnings. Pass `-W` on the
+ command-line to view the warnings. The following new
+ check have been added:
+ - Each regex must have a corresponding sample log message
+ that it matches.
+ - Each sample must be matched by only one regex.
+* Added built-in support for anonymizing content. The
+ `:write-*` commands now accept an `--anonymize` option
+ and there is an `anonymize()` SQL function. The
+ anonymization process will try to replace identifying
+ information with random data. For example, IPv4 addresses
+ are replaced with addresses in the 10.0.0.0/8 range.
+ (This feature is mainly intended to help with providing
+ information to lnav support that does not have sensitive
+ values.)
+* Added `parse_url()` and `unparse_url()` SQL functions for
+ parsing URLs into a JSON object and then back again. Note
+ that the implementation relies on libcurl which has some
+ limitations, like not supporting all types of schemes
+ (e.g. `mailto:`).
+* Added the `subsecond-field` and `subsecond-units` log
+ format properties to allow for specifying a separate
+ field for the sub-second portion of a timestamp.
+* Added a keymap for Swedish keyboards.
+
+Breaking changes:
+* The `regexp_capture()` table-valued-function now returns NULL
+ instead of an empty string for the `capture_name` column if
+ the capture is not named.
+
+Fixes:
+* Reduce the "no patterns have a capture" error to a warning
+ so that it doesn't block lnav from starting up.
+* Some ANSI escape sequences will now be removed before testing
+ regexes against a log message.
+* If a line in a JSON-lines log file does not start with a
+ `{`, it will now be shown as-is and will not have the JSON
+ parse error.
+
+Cost of Doing Business:
+* Migrated from pcre to pcre2.
+
+## lnav v0.11.0
+
+Features:
+* Redesigned the top status area to allow for user-specified
+ messages and added a second line that displays an interactive
+ breadcrumb bar. The top status line now shows the clock and
+ the remaining area displays whatever messages are inserted
+ into the lnav_user_notifications table. The information that
+ was originally on top is now in a second line and organized
+ as breadcrumbs. Pressing `ENTER` will activate the breadcrumb bar
+ and the left/right cursor keys can be used to select a particular
+ crumb while the up/down keys can select a value to switch to.
+ While a crumb is selected, you can also type in some text to do
+ a fuzzy search on the possibilities or, if the crumb represents
+ an array of values, enter the index to jump to.
+* The pretty-print view will now show breadcrumbs that indicate the
+ location of the top line in the view with the prettified structure.
+* Markdown files (those with a .md extension) are now rendered in the
+ TEXT view. The breadcrumb bar at the top will also be updated
+ depending on the section of the document that you are in and you
+ can use it to jump to different parts of the doc.
+* The `:goto` command will now accept anchor links (i.e. `#section-id`)
+ as an argument when the text file being viewed has sections. You
+ can also specify an anchor when opening a file by appending
+ `#<link-name>`. For example, `README.md#screenshot`.
+* Log message comments are now treated as markdown and rendered
+ accordingly in the overlay. Multi-line comments are now supported
+ as well.
+* Metadata embedded in files can now be accessed by the
+ `lnav_file_metadata` table. Currently, only the front-matter in
+ Markdown files is supported.
+* Added an integration with regex101.com to make it easier to edit
+ log message regular expressions. Using the new "management CLI"
+ (activated by the `-m` option), a log format can be created from
+ a regular expression entry on regex101.com and existing patterns
+ can be edited.
+* In the spectrogram view, the selected value range is now shown by
+ an overlay that includes a summary of the range and the number of
+ values that fall in that range. There is also a detail panel at
+ the bottom that shows the log-messages/DB-rows whose values are in
+ that range. You can then press TAB to focus on the detail view
+ and scroll around.
+* Add initial support for pcap(3) files using tshark(1).
+* SQL statement execution can now be canceled by pressing `CTRL+]`
+ (same as canceling out of a prompt).
+* To make it possible to automate some operations, there is now an
+ `lnav_events` table that is updated when internal events occur
+ within lnav (e.g. opening a file, format is detected). You
+ can then add SQLite `TRIGGER`s to this table that can perform a
+ task by updating other tables.
+* Tags can automatically be added to messages by defining a pattern
+ in a log format. Under a format definition, add the tag name
+ into the "tags" object in a format definition. The "pattern"
+ property specifies the regular expression to match against a line
+ in a file that matches the format. If a match is found, the tag
+ will be applied to the log message. To restrict matches to
+ certain files, you can add a "paths" array whose object elements
+ contain a "glob" property that will be matched against file names.
+* Log messages can now be detected automatically via "watch
+ expressions". These are SQL expressions that are executed for
+ each log message. If the expressions evaluates to true, an
+ event is published to the `lnav_events` table that includes the
+ message contents.
+* Added the `regexp_capture_into_json()` table-valued-function that
+ is similar to `regexp_capture()`, but returns a single row with a
+ JSON value for each match instead of a row for each capture.
+* Added a `top_meta` column to the lnav_views table that contains
+ metadata related to the top line in the view.
+* Added a `log_opid` hidden column to all log tables that contains
+ the "operation ID" as specified in the log format.
+* Moved the `log_format` column from the all_logs table to a hidden
+ column on all tables.
+* Add format for UniFi gateway.
+* Added a `glob` property to search tables defined in log formats
+ to constrain searches to log messages from files that have a
+ matching log_path value.
+* Initial indexing of large files should be faster. Decompression
+ and searching for line-endings are now pipelined, so they happen
+ in a thread that is separate from the regular expression matcher.
+* Writing to the clipboard now falls back to OSC 52 escape sequence
+ if none of the clipboard commands could be detected. Your
+ terminal software will need to support the sequence and you may
+ need to explicitly enable it in the terminal.
+* Added the `:export-session-to <path>` command that writes the
+ current session state to a file as a list of commands/SQL
+ statements. This script file can be executed to restore the
+ majority of the current state.
+* Added the `echoln()` SQL function that behaves similarly to the
+ `:echo` command, writing its first argument to the current
+ output.
+* Added `encode()` and `decode()` SQL functions for transcoding
+ blobs or text values using one of the following algorithms:
+ base64, hex, or uri.
+* In regular expressions, capture group names are now semantically
+ highlighted (e.g. in the capture, `(?<name>\w+)`, "name" would
+ have a unique color). Also, operations or previews that use
+ that regular expression will highlight the matched data with
+ the same color.
+* Added an lnav_views_echo table that is a real SQLite table that
+ you can create TRIGGERs on in order to perform actions when
+ scrolling in a view.
+* Added a `yaml_to_json()` SQL function that converts a YAML
+ document to the equivalent JSON.
+
+Breaking Changes:
+* Formats definitions are now checked to ensure that values have a
+ corresponding capture in at least one pattern.
+* Added a 'language' column to the lnav_view_filters table that
+ specifies the language of the 'pattern' column, either 'regex'
+ or 'sql'.
+* Timestamps that do not have a day or month are rewritten to a
+ full timestamp like YYYY-MM-DD HH:MM:SS.
+* Removed the summary overlay at the bottom of the log view that
+ displayed things like "Error rate" and the time span. It doesn't
+ seem like anyone used it.
+* Removed the `log_msg_instance` column from the logline and search
+ tables since it causes problems with performance.
+* Search tables now search for multiple matches within a message
+ instead of stopping at the first hit. Each additional match is
+ returned as a separate row. A `match_index` column has been
+ added to capture the index of the match within the message.
+ The table regex is also compiled with the "multiline" flag enabled
+ so the meaning of the `^` and `$` metacharacters are changed
+ to match the start/end of a line instead of the start/end of
+ the entire message string.
+* Search tables defined in formats are now constrained to only
+ match log messages that are in that log format instead of all
+ log messages. As a benefit, the search table now includes
+ the columns that are defined as part of the format.
+* The lnav_view_filters table will treats the tuple of
+ (view_name, type, language, pattern) as a `UNIQUE` index and
+ will raise a conflict error on an `INSERT`. Use `REPLACE INTO`
+ instead of `INSERT INTO` to ignore conflict error.
+* The types of SQL values stored as local variables in scripts
+ is now preserved when used as bound variables at a later point
+ in the script.
+
+Fixes:
+* Toggling enabled/disabled filters when there is a SQL expression
+ no longer causes a crash.
+* Fix a crash related to long lines that are word wrapped.
+* Multiple SQL statements in a SQL block of a script are now
+ executed instead of just the first one.
+* In cases where there were many colors on screen, some text would
+ be colored incorrectly.
+* The pretty-print view now handles ANSI escape sequences.
+* The "overstrike" convention for doing bold and underline is now
+ supported. (Overstrike is a character followed by a backspace
+ and then the same character for bold or an underscore for
+ underline.)
+* The `:eval` command now works with searching (using the '/'
+ prefix).
+
+## lnav v0.10.1
+
+Features:
+* Added `:show-only-this-file` command that hides all files except the
+ one for the top line in the view.
+* The `:write-raw-to` command now accepts a `--view` flag that specifies
+ the source view for the data to write. For example, to write the
+ results of a SQL query, you would pass `--view=db` to the command.
+* The commands used to access the clipboard are now configured through
+ the "tuning" section of the configuration.
+* Added an `lnav_version()` SQL function that returns the current
+ version string.
+* Added basic support for the logfmt file format. Currently, only files
+ whose lines are entirely logfmt-encoded are supported. The lines
+ must also contain either a field named `time` or `ts` that contains
+ the timestamp.
+* Added the `logfmt2json()` SQL function to convert a string containing
+ a logfmt-encoded message into a JSON object that can be operated on
+ more easily.
+* Added the `gzip()` and `gunzip()` SQL functions to compress values
+ into a blob and decompress a blob into a string.
+Interface changes:
+* The xclip implementation for accessing the system clipboard now writes
+ to the "clipboard" selection instead of the "primary" selection.
+* The 'query' bookmark type and `y`/`Y` hotkeys have been removed due to
+ performance issues and the functionality is probably rarely used.
+Bug Fixes:
+* The text "send-input" would show up on some terminals instead of
+ ignoring the escape sequence. This control sequence was only
+ intended to be used in the test suite.
+* Remote file synchronization has been optimized a bit.
+* Configuration values loaded from the `~/.lnav/configs` directory
+ are now included in the default configuration, so they won't be
+ saved into the `~/.lnav/config.json` user configuration file.
+* Key handling in the visual filter editor will no longer swallow
+ certain key-presses when editing a filter.
+* Scrolling performance restored in the SQL view.
+* The `:redirect-to` command now works with `/dev/clipboard`
+* The field overlay (opened by pressing 'p') now shows `log_time`
+ for the timestamp field instead of the name defined in the format.
+* The search term in the bottom status bar will now update properly
+ when switching views.
+* The "Out-Of-Time-Order Message" overlay will be shown again.
+* The tab for the "Files" panel will be highlighted in red if there
+ is an issue opening a file.
+* Overwritten files should be reloaded again.
+* The `jget()` SQL function now returns numbers with the correct type.
+* The `json_contains()` SQL function now returns false if the first
+ argument is NULL instead of NULL.
+* The local copies of remote files are now cleaned up after a couple
+ days of the host not being accessed.
+* The initial loading and indexing phase has been optimized.
+
+## lnav v0.10.0
+
+Features:
+* Files on remote machines can be viewed/tailed if they are accessible
+ via SSH. The syntax for specifying the host and path is similar to
+ scp. For example, to view the files in the /var/log directory on the
+ machine `host1.example.org`:
+ ```console
+ $ lnav user@host1.example.org:/var/log
+ ```
+ Note that you must be able to log into the machine without any
+ interaction.
+* Added the `:filter-expr` command to filter log messages based on an SQL
+ expression. This command allows much greater control over filtering.
+* Added the `:mark-expr` command to mark log messages based on an SQL
+ expression. This command makes it easier to programmatically mark
+ log messages compared to using SQL.
+* Added support for archive files, like zip, and other compression formats,
+ like xz, when compiled with libarchive. When one of these types of
+ files is detected, they are unpacked into a temporary directory and
+ all the files are loaded into lnav.
+* Added an `xpath()` table-valued function for extracting values from
+ strings containing XML snippets.
+* Added the `:prompt` command to allow for more customization of prompts.
+ Combined with a custom keymapping, you can now open a prompt and prefill
+ it with a given value. For example, a key could be bound to the
+ following command to open the command prompt with `:filter-in `
+ already filled in:
+ ```lnav
+ :prompt command : 'filter-in '
+ ```
+* Added support for the W3C Extended Log File Format with the name
+ `w3c_log`. Similarly to the bro log format, the header is used to
+ determine the columns in a particular file. However, since the columns
+ can be different between files, the SQL table only has a well-known set
+ of columns and the remainder are accessible through JSON-objects stored
+ in columns like `cs_headers` and `sc_headers`.
+* Added support for the S3 Access File Format.
+* To jump to the first search hit above the top line in a view, you can
+ press `CTRL+J` instead of `ENTER` in the search prompt. Pressing `ENTER`
+ will jump to the first hit below the current window.
+* Filtering, as a whole, can be now disabled/enabled without affecting
+ the state of individual filters. This includes text and time-filters
+ (i.e. `:hide-lines-before`). You can enable/disable filtering by:
+ pressing `f` in the filter editor UI; executing the `:toggle-filtering`
+ command; or by doing an `UPDATE` on the "filtering" column of the
+ `lnav_views` SQLite table.
+* Themes can now include definitions for text highlights under:
+ `/ui/theme-defs/<theme_name>/highlights`
+* Added a "grayscale" theme that isn't so colorful.
+* Added the `humanize_file_size()` SQL function that converts a numeric size
+ to a human-friendly string.
+* Added the `sparkline()` SQL function that returns a "sparkline" bar made
+ out of unicode characters. It can be used with a single value or as
+ an aggregator.
+* Added a `log_time_msecs` hidden column to the log tables that returns
+ the timestamp as the number of milliseconds from the epoch.
+* Added an `lnav_top_file()` SQL function that can be used to get the
+ name of the top line in the top view or NULL if the line did not come
+ from a file.
+* Added a `mimetype` column to the lnav_file table that returns a guess as
+ to the MIME type of the file contents.
+* Added a `content` hidden column to the lnav_file table that can be used
+ to read the contents of the file. The contents can then be passed to
+ functions that operate on XML/JSON data, like `xpath()` or `json_tree()`.
+* Added an `lnav_top_view` SQL VIEW that returns the row for the top view
+ in the lnav_views table.
+* The `generate_series()` SQLite extension is now included by default.
+ One change from the standard implementation is that both the start and
+ stop are required parameters.
+* Added the `;.read` SQL command for executing a plain SQL file.
+* Added the `-N` flag so that lnav will run without opening the default
+ syslog file.
+
+Interface Changes:
+* When copying log lines, the file name and time offset will be included
+ in the copy if they are enabled.
+* Log messages that cannot be parsed properly will be given an "invalid"
+ log level and the invalid portions colored yellow.
+* The range_start and range_stop values of the `regexp_capture()` results
+ now start at 1 instead of zero to match with what the other SQL string
+ functions expect.
+* The `:write-cols-to` command has been renamed to `:write-table-to`.
+* The DB view will limit the maximum column width to 120 characters.
+* The `:echo` command now evaluates its message to do variable
+ substitution.
+* The `:write-raw-to` command has been changed to write the original
+ log file content of marked lines. For example, when viewing a JSON
+ log, the JSON-Line values from the log file will be written to the
+ output file. The `:write-view-to` command has been added to perform
+ the previous work of `:write-raw-to` where the raw content of the view
+ is written to the file.
+
+Fixes:
+* Unicode text can now be entered in prompts.
+* The `replicate()` SQL function would cause a crash if the number of
+ replications was zero.
+* Many internal improvements.
+
+## lnav v0.9.0
+
+Features:
+* Added support for themes and included a few as well: default, eldar,
+ monocai, night-owl, solarized-light, and solarized-dark. The theme
+ can be changed using the `:config` command, like so:
+ ```lnav
+ :config /ui/theme night-owl
+ ```
+ Consult the online documentation for defining a new theme at:
+ https://lnav.readthedocs.io/en/latest/config.html#theme-definitions
+* Added support for custom keymaps and included the following: de, fr,
+ uk, us. The keymap can be changed using the `:config` command, like so:
+ ```lnav
+ :config /ui/keymap uk
+ ```
+ Consult the online documentation for defining a new keymap at:
+ https://lnav.readthedocs.io/en/latest/config.html#keymap-definitions
+* The following JSON-Schemas have been published for the log format and
+ configuration JSON files:
+ - https://lnav.org/schemas/format-v1.schema.json
+ - https://lnav.org/schemas/config-v1.schema.json
+
+ Formats should be updated to reference the schema using the `$schema`
+ property.
+* Indexing of new data in log files can now be paused by pressing `=`
+ and unpaused by pressing it again. The bottom status bar will display
+ 'Paused' in the right corner while paused.
+* CMake is now a supported way to build.
+* When viewing data from the standard-input, a symbolic name can be used
+ to preserve session state. The name can be changed using the
+ `|rename-stdin` lnav script or by doing an `UPDATE` to the filepath
+ column of the lnav_file table. For example, to assign the name
+ "journald", the following SQL statement can be executed in lnav:
+ ```lnav
+ ;UPDATE lnav_file SET filepath='journald' WHERE filepath='stdin'
+ ```
+* The size of the terminal can be accessed in SQL using the `$LINES` and
+ `$COLS` variables.
+* The `raise_error(msg)` SQL function has been added to make it easier to
+ raise an error in an lnav script to stop execution and notify the user.
+* Added the `json_concat()` function to make it easier to append/concatenate
+ values onto arrays.
+* Added the `:write-jsonlines-to` command that writes the result of a SQL
+ query to a file in the JSON Lines format.
+
+Interface Changes:
+* Data piped into lnav is no longer dumped to the console after exit.
+ Instead, a file containing the data is left in `.lnav/stdin-captures/`
+ and a message is printed to the console indicating the file name.
+* In time-offset mode, the deltas for messages before the first mark
+ are now negative instead of relative to the start of the log.
+* The $XDG_CONFIG_HOME environment variable (or `~/.config` directory) are
+ now respected for storing lnav's configuration. If you have an existing
+ `~/.lnav` directory, that will continue to be used until you move it to
+ `$XDG_CONFIG_HOME/lnav` or `~/.config/lnav`.
+* Removed the `:save-config` command. Changes to the configuration are now
+ immediately saved.
+
+Fixes:
+* Added 'notice' log level.
+* If a `timestamp-format` is used in an element of a `line-format`, the
+ field name is ignored and a formatted timestamp is always used.
+* Ignore stdin when it is connected to `/dev/null`.
+
+## lnav v0.8.5
+
+Features:
+* Added a visual filter editor to make it easier to update existing
+ filters. The editor can be opened by pressing `TAB`. Once the editor
+ is opened, you can create/delete, enable/disable, and edit the patterns
+ with hotkeys.
+* Added an `lnav_view_filters` SQL table that can be used to
+ programmatically manipulate filters.
+* Added an `lnav_view_filter_stats` SQL table that contains the number of
+ times a given filter matched a line in the view.
+* Added a `log_filters` column to log tables that can be used to see what
+ filters matched the log message.
+* A history of locations in a view is now kept so that you can jump back
+ to where you were previously using the `{` and `}` keys. The location
+ history can also be accessed through the `:prev-location` and
+ `:next-location` commands.
+* The `:write-*` commands will now accept `/dev/clipboard` as a file name
+ that writes to the system clipboard.
+* The `:write-to` and `:write-raw-to` commands will now print out comments
+ and tags attached to the lines.
+* Added a `:redirect-to <path>` command to redirect command output to the
+ given file. This command is mostly useful in scripts where one might
+ want to redirect all output from commands like `:echo` and `:write-to -`
+ to a single file.
+* If a log file format has multiple patterns for matching log messages,
+ each pattern is now tried to match a message in a file. Previously,
+ only one pattern was ever used for an entire file.
+* Added haproxy log format from Peter Hoffmann.
+* Added `spooky_hash()` and `group_spooky_hash()` SQL functions to
+ generate a hash of their parameters.
+* Added `time_offset` to the `lnav_file` table so that the timestamps in
+ a file can be adjusted programmatically.
+
+Interface Changes:
+* The auto-complete behavior in the prompt has been modified to fall back
+ to a fuzzy search if the prefix search finds no matches. For example,
+ typing in `:fin` and pressing TAB would previously not do anything.
+ Now, the `:fin` will be completed to `:filter-in ` since that is a
+ strong fuzzy match. If there are multiple matches, as would happen
+ with `:dfil`, readline's menu-complete behavior will be engaged and
+ you can press `TAB` cycle through the options.
+* Added `CTRL+F` to toggle the enabled/disabled state of all filters for the
+ current view.
+* The `-r` flag is now for recursively loading files. The functionality
+ for loading rotated files is now under the `-R` flag.
+* The current search term is now shown in the bottom status bar.
+* Some initial help text is now shown for the search and SQL prompts to
+ refresh the memory.
+* When entering the `:comment` command for a line with a comment, the
+ command prompt will be filled in with the existing comment to make
+ editing easier.
+* Hidden fields now show up as a unicode vertical ellipsis (⋮) instead of
+ three-dot ellipsis to save space.
+* Pressing 7/8 will now move to the previous/next minute.
+* The `:write-raw-to` command has been changed to write the entire
+ contents of the current view and a `:write-screen-to` command has been
+ added to write only the current screen contents.
+* Disabled filters are now saved in sessions.
+* The `:adjust-log-time` command now accepts relative times as input.
+
+Fixes:
+* The `:write-json-to` command will now pass through JSON cells as their
+ JSON values instead of a JSON-encoded string.
+
+## lnav v0.8.4
+
+Features:
+* Added the `:comment` command that can be used to attach a comment to a
+ log line. The comment will be displayed below the line, like so:
+ ```
+ 2017-01-01T15:30:00 error: computer is on fire
+ + This is where it all went wrong
+ ```
+ The `:clear-comment` command will remove the attached comment. Comments
+ are searchable with the standard search mechanism and they are available
+ in SQL through the `log_comment` column.
+* Added the `:tag`, `:untag`, and `:delete-tags` commands that can be used
+ to attach/detach tags on the top log line and delete all instances of
+ a tag. Tags are also searchable and are available in SQL as a JSON
+ array in the `log_tags` column.
+* Pressing left-arrow while viewing log messages will reveal the source
+ file name for each line and the unique parts of the source path.
+ Pressing again will reveal the full path.
+* The file name section of the top status line will show only the unique
+ parts of the log file path if there is not enough room to show the full
+ path.
+* Added the `:hide-unmarked-lines` and `:show-unmarked-lines` commands
+ that hide/show lines based on whether they are bookmarked.
+* Added the `json_contains()` SQL function to check if a JSON value
+ contains a number of a string.
+* The relative time parser recognizes "next" at the beginning of the
+ input, for example, "next hour" or "next day". Handy for use in the
+ `:goto` command.
+* Added a "text-transform" option for formatting JSON log messages. The
+ supported options are: none, uppercase, lowercase, and capitalize.
+* Added a special `__level__` field name for formatting JSON messages so
+ that the lnav level name can be used instead of the internal value in
+ the JSON object.
+* Added a log format for journald JSON logs.
+
+Interface Changes:
+* When typing in a search, instead of moving the view to the first match
+ that was found, the first ten matches will be displayed in the preview
+ window.
+* The pretty-print view maintains highlighting from the log view.
+* The pretty-print view no longer tries to reverse lookup IP addresses.
+* The online help for commands and SQL functions now includes a 'See Also'
+ section that lists related commands/functions.
+
+Fixes:
+* The HOME key should now work in the command-prompt and move the cursor
+ to the beginning of the line.
+* The `:delete-filter` command should now tab-complete existing filters.
+* Milliseconds can now be used in relative times (e.g. 10:00:00.123)
+* The `J`/`K` hotkeys were not marking lines correctly when the bottom of
+ the view was reached.
+* The level field in JSON logs should now be recognized by the level
+ patterns in the format.
+
+## lnav v0.8.3
+
+Features:
+* Support for the Bro Network Security Monitor (https://www.bro.org) log
+ file format.
+* Added an `fstat()` table-valued function for querying the local
+ filesystem.
+* Added `readlink()` and `realpath()` SQL functions.
+* Highlights specified in log formats can now specify the colors to use
+ for the highlighted parts of the log message.
+* Added a `:quit` command.
+* Added a `/ui/default-colors` configuration option to specify that the
+ terminal's default background and foreground colors should be used
+ instead of black and white.
+
+Interface Changes:
+* Pressing delete at a command-prompt will exit the prompt if there is no
+ other input.
+
+Fixes:
+* The help view now includes all the command-help that would pop up as
+ you entered commands and SQL queries.
+* Hidden fields and lines hidden before/after times are now saved in the
+ current session and restored.
+* Unicode characters should now be displayed correctly (make sure you
+ have LANG set to a UTF-8 locale).
+
+## lnav v0.8.2
+
+Features:
+* The timestamp format for JSON log files can be specified with the
+ `timestamp-format` option in the `line-format` array.
+* Added "min-width", "max-width", "align", and "overflow" options to the
+ "line-format" in format definitions for JSON log files. These options
+ give you more control over how the displayed line looks.
+* Added a "hidden" option to log format values so that you can hide JSON
+ log fields from being displayed if they are not in the line format.
+* Added a `rewriter` field to log format value definitions that is a
+ command used to rewrite the field in the pretty-printed version of a
+ log message. For example, the HTTP access log format will rewrite the
+ status code field to include the textual version (e.g. 200 (OK)).
+* Log message fields can now be hidden using the `:hide-fields` command or
+ by setting the 'hidden' property in the log format. When hidden, the
+ fields will be replaced with a yellow ellipsis when displayed. Hiding
+ large fields that contain extra details can make the log easier to read.
+ The `x` hotkey can be used to quickly toggle whether these fields are
+ displayed or not.
+* Added a `:mark` command to bookmark the top line in the current view.
+* Added an `:alt-msg` command that can be used to set the text to be
+ displayed in the bottom right of the command line. This command is
+ mostly intended for use by hotkey maps to set the help text.
+* In lnav scripts, the first row of a SQL query result will now be turned
+ into local variables that can be referenced in other commands or
+ queries. For example, the following script will print the number one:
+ ```lnav
+ ;SELECT 1 as foobar
+ :eval :echo ${foobar}
+ ```
+* Added an `lnav_view_stack` SQL table that gives access to the view
+ stack.
+* Added a `top_time` column to the lnav_views table so that you can get
+ the timestamp for the top line in views that are time-based as well as
+ allowing you to move the view to a given time with an UPDATE statement.
+* Added a 'search' column to the lnav_views table so that you can perform
+ a text search programmatically.
+* Added a `regexp_capture(<string>, <pattern>)` table-valued function for
+ getting detailed results from matching a regular expression against a
+ string.
+* Added a `timediff(<time1>, <time2>)` SQL function for computing the
+ difference between two relative or absolute timestamps.
+* Log formats can now define a default set of highlights with the
+ "highlights" property.
+* Added a `|search-for <pattern>` built-in script that can be used to
+ start a search from the command-line.
+* Log format definitions can now specify the expected log level for a
+ sample line. This check should make it easier to validate the
+ definition.
+
+Interface Changes:
+* Command and SQL documentation is now displayed in a section at the
+ bottom of the screen when a command or query is being entered. Some
+ commands will also display a preview of the command results. For
+ example, the `:open` command will display the first ten lines of the
+ file to be opened and the `:filter-out` command will highlight text
+ that matches in the current view. The preview pane can be shown/hidden
+ by pressing `CTRL-P`.
+* The color used for text colored via `:highlight` is now based on the
+ the regex instead of randomly picked so that colors are consistent
+ across invocations.
+* The "graph" view has been removed since it's functionality has been
+ obsoleted by other features, like `:create-search-table`.
+* When doing a search, if a hit is found within a second after hitting
+ `<ENTER>`, the view will move to the matched line. The previous behavior
+ was to stay on the current line, which tended to be a surprise to new
+ users.
+* Pressing `n`/`N` to move through the next/previous search hit will now
+ skip adjacent lines, up to the vertical size of the view. This should
+ make scanning through clusters of hits much faster. Repeatedly
+ pressing these keys within a short time will also accelerate scanning
+ by moving the view at least a full page at a time.
+
+Breaking Changes:
+* The captured timestamp text in log files must fully match a known format
+ or an error will be reported. The previous behavior was to ignore any
+ text at the end of the line.
+
+Fixes:
+* You can now execute commands from the standard input by using a dash (`-`)
+ with the `-f` command-line argument. Reading commands from a file
+ descriptor should also work, for example, with the following bash
+ syntax:
+ ```console
+ $ lnav -f <(echo :open the-file-to-open)
+ ```
+* Programming language syntax highlighting should now only be applied to
+ source code files instead of everywhere.
+
+## lnav v0.8.1
+
+Features:
+* Added a spectrogram command and view that displays the values of a
+ numeric field over time. The view works for log message fields or
+ for database result columns.
+* Log formats can now create SQL views and execute other statements
+ by adding `.sql` files to their format directories. The SQL scripts
+ will be executed on startup.
+* Added `json_group_object` and `json_group_array` aggregate SQL
+ functions that collects values from a GROUP BY query into a JSON
+ object or array, respectively.
+* The SQL view will now graph values found in JSON objects/arrays in
+ addition to the regular columns in the result.
+* Added an `regexp_match(<re>, <str>)` SQL function that can be used to
+ extract values from a string using a regular expression.
+* Added an `extract(<str>)` SQL function that extracts values using the
+ same data discover/extraction parser used in the `logline` table.
+* Added a "summary" overlay line to the bottom of the log view that
+ displays how long ago the last message was received, along with the
+ total number of files and the error rate over the past five minutes.
+* Pressing `V` in the DB view will now check for a column with a
+ timestamp and move to the corresponding time in the log view.
+* Added `a`/`A` hotkeys to restore a view previously popped with `q`/`Q`.
+* Added `:hide-lines-before`, `:hide-lines-after`, and
+ `:show-lines-before-and-after` commands so that you can filter out
+ log lines based on time.
+* Scripts containing lnav commands/queries can now be executed using
+ the pipe (`|`) hotkey. See the documentation for more information.
+* Added an `:eval` command that can be used to execute a command or
+ query after performing environment variable substitution.
+* Added an `:echo` command that can be useful for scripts to message
+ the user.
+* The `log_part` column can now be set with an SQL `UPDATE` statement.
+* Added a `log_body` hidden column that returns the body of the log
+ message.
+* Added `:config`, `:reset-config`, and `:save-config` commands to change
+ configuration options, reset to default, and save them for future
+ executions.
+* Added a `/ui/clock-format` configuration option that controls the time
+ format in the top-left corner.
+* Added a `/ui/dim-text` configuration option that controls the brightness
+ of text in the UI.
+* Added support for TAI64 timestamps (http://cr.yp.to/libtai/tai64.html).
+* Added a safe execution mode. If the `LNAVSECURE` environment variable is
+ set before executing lnav, the following commands are disabled:
+ - `:open`
+ - `:pipe-to`
+ - `:pipe-line-to`
+ - `:write-*-to`
+
+ This makes it easier to run lnav with escalated privileges in restricted
+ environments, without the risk of users being able to use the above
+ mentioned commands to gain privileged access.
+
+Interface Changes:
+* The `o`/`O` hotkeys have been reassigned to navigate through log
+ messages that have a matching "opid" field. The old action of
+ moving forward and backward by 60 minutes can be simulated by
+ using the `:goto` command with a relative time and the `r`/`R`
+ hotkeys.
+* Log messages with timestamps that pre-date previous log messages will
+ have the timestamp highlighted in yellow and underlined. These out-
+ of-time-order messages will be assigned the time of the previous
+ message for sorting purposes. You can press the 'p' hotkey to examine
+ the 'Received Time' of the message as well as the time parsed from the
+ original message. A `log_actual_time` hidden field has also been
+ added to the SQLite virtual table so you can operate on the original
+ message time from the file.
+* The `A`/`B` hotkeys for moving forward/backward by 10% line increments
+ have been reassigned to `[` and `]`. The `a` and `A` hotkeys are now
+ used to return to the previously popped view while trying to preserve
+ the time range. For example, after leaving the spectrogram view with
+ 'q', you can press 'A' return to the view with the top time in the
+ spectrogram matching the top time in the log view.
+* The 'Q' hotkey now pops the current view off of the stack while
+ maintaining the top time between views.
+
+Fixes:
+* Issues with tailing JSON logs have been fixed.
+* The `jget()` SQL function should now work for objects nested in arrays.
+
+## lnav v0.8.0
+
+Features:
+* Integration with "papertrailapp.com" for querying and tailing
+ server log and syslog messages. See the Papertrail section in
+ the online help for more details.
+* Remote files can be opened when lnav is built with libcurl v7.23.0+
+* SQL queries can now be done on lines that match a regular expression
+ using the `log_search` table or by creating custom tables with the
+ `:create-search-table` command.
+* Log formats that are "containers" for other log formats, like
+ syslog, are now supported. See the online help for more
+ information.
+* Formats can be installed from git repositories using the `-i` option.
+ A standard set of extra formats can be installed by doing
+ `lnav -i extra`. (You must have git installed for this to work.)
+* Added support for 'VMware vSphere Auto Deploy' log format.
+* Added a 'sudo' log format.
+* Added hotkeys to move left/right by a smaller increment (H/L or
+ Shift+Left/Shift+Right).
+* A color-coded bar has been added to the left side to show where
+ messages from one file stop and messages from another file start.
+* The `-C` option will now try to check any specified log files to
+ make sure the format(s) match all of the lines.
+* Added an `all_logs` SQLite table that contains the message format
+ extracted from each log line. Also added a `;.msgformat` SQL command
+ that executes a query that returns the counts for each format and the
+ first line where the format was seen.
+* Added an `lnav_views` SQLite table that can be used to query and
+ change the lnav view state.
+* When typing in a command, the status bar will display a short
+ summary of the currently entered command.
+* Added a `:delete-filter` command.
+* Added a `log_msg_instance` column to the logline and log_search
+ tables to make it easier to join tables that are matching log
+ messages that are ordered.
+* Added a `timeslice()` function to SQLite so that it is easier to
+ group log messages by time buckets.
+* The `:goto` command now supports relative time values like
+ `a minute ago`, `an hour later`, and many more.
+
+Interface Changes:
+* The `r`/`R` hotkeys have been reassigned to navigate through the log
+ messages by the relative time value that was last used with the
+ `:goto` command.
+
+Fixes:
+* The pretty-print view should now work for text files.
+* Nested fields in JSON logs are now supported for levels, bodies, etc...
+* Tab-completion should work for quoted SQL identifiers.
+* 'lo-fi' mode key shortcut changed to `CTRL+L`.
+* 'redraw' shortcut removed. Relegated to just a command.
+* Fixed lnav hang in pretty-print mode while doing a dns lookup.
+* The generic log message parser used to extract data has been
+ optimized and should be a bit faster.
+
+## lnav v0.7.3
+
+Features:
+* Add `:pipe-to` and `:pipe-line-to` commands that pipe the currently
+ marked lines or the current log message to a shell command,
+ respectively.
+* Added a "pretty-print" view (P hotkey) that tries to reformat log
+ messages so that they are easier to read.
+* Added a `:redraw` command (CTRL+L hotkey) to redraw the window in
+ case it has been corrupted.
+* Added a `:relative-goto` command to move the current view relative
+ to its current position.
+* Experimental support for linking with jemalloc.
+* The plain text view now supports filtering.
+* Added `:next-mark` and `:prev-mark` commands to jump to the next or
+ previous bookmarked line (e.g. error, warning, ...)
+* Added a `:zoom-to` command to change the zoom level of the histogram
+ view.
+* Log formats can now define their own timestamp formats with the
+ `timestamp-format` field.
+
+Fixes:
+* Autotools scripts overhaul.
+* Added a configure option to disable linking with libtinfo. The newer
+ versions of ncurses don't require it, however the build silently pulls
+ it in as a dependency, if it is available on the system. This can be
+ explicitly disabled using the `--disable-tinfo` option during configure.
+* Fixed the configure script behavior to ignore the values specified using
+ the CFLAGS and LDFLAGS environment variables while searching for sqlite3
+ when `--with-sqlite3` switch was specified without the prefix.
+* The configure script now recognizes libeditline symlink'ed to masquerade
+ as libreadline. This previously used to cause problems at compile time,
+ specially on OS X. If you come across this error, use the
+ `--with-readline=prefix` switch to specify the path to the correct
+ location of libreadline.
+* The order that log formats are tried against a log file is now
+ automatically determined so that more specific formats are tested
+ before more general ones. The order is determined on startup based on
+ how each format matches each other formats sample lines.
+* Command files (i.e. those executed via the `-f` flag) now support
+ commands/queries that span more than one line.
+* Added more log levels: stats, debug2 - debug5.
+
+## lnav v0.7.2
+
+* Added log formats for vdsm, openstack, and the vmkernel.
+* Added a "lo-fi" mode (L hotkey) that dumps the displayed log lines
+ to the terminal without any decorations. The `:write-to`, `:write-json-to`,
+ and `:write-csv-to` commands will also write their output to the terminal
+ when passed `-` as the file name. This mode can be useful for copying
+ plain text lines to the clipboard.
+* (OS X) Text search strings are copied to the system's "find" clipboard.
+ Also, when starting a new search, the current value in the "find"
+ clipboard can be tab-completed.
+
+## lnav v0.7.1
+
+Features:
+* Added an `environ` SQL table that reflects lnav's environment
+ variables. The table can be read and written to using SQL
+ `SELECT`, `INSERT`, `UPDATE`, and `DELETE` statements. Setting
+ variables can be a way to use SQL query results in lnav commands.
+* Added a `jget()` SQLite function that can extract fields from a JSON-
+ encoded value.
+* Added log formats for the OpenAM identity provider.
+* Added a `:clear-highlight` command to clear previous calls to the
+ `:highlight` command.
+* Fixed some performance bugs in indexing JSON log formats. Loading
+ times should be at least five times faster.
+* Filtering performance should be improved so that enabling/disabling
+ filters should be almost instantaneous.
+* The `:filter-in`, `:filter-out`, and `:highlight` commands now support
+ tab-completion of text in the current view.
+* Add a `-i` flag that installs format files in: `~/.lnav/formats/installed`
+
+## lnav v0.7.0
+
+Features:
+* Add the '.schema' SQL command to open a view that displays the schema
+ for the internal tables and any attached databases. If lnav was only
+ executed with a SQLite database and no text files, this view will open
+ by default.
+* The scroll bar now indicates the location of errors/warnings, search
+ hits, and bookmarks.
+* The xterm title is update to reflect the file name for the top line
+ in the view.
+* Added a "headless" mode so that you can execute commands and run SQL
+ queries from the command-line without having to do it from the curses
+ UI.
+* When doing a search or SQL query, any text that is currently being
+ displayed can be tab-completed.
+* The `-H` option was added so you can view the internal help text.
+* Added the 'g/G' hotkeys to move to the top/bottom of the file.
+* Added a `log_mark` column to the log tables that indicates whether or
+ not a log message is bookmarked. The field is writable, so you can
+ bookmark lines using an SQL UPDATE query.
+* Added syntax-highlighting when editing SQL queries or search regexes.
+* Added a `:write-json-to` command that writes the result of a SQL query
+ to a JSON-formatted file.
+* The "elapsed time" column now uses red/green coloring to indicate
+ sharp changes in the message rate.
+* Added a `:set-min-log-level` command to filter out log messages that
+ are below a given level.
+
+Fixes:
+* Performance improvements.
+* Multi-line filtering has been fixed.
+* A collator has been added to the log_level column in the log tables
+ so that you can write expressions like `log_level > 'warning'`.
+* The log_time datetime format now matches what is returned by
+ `datetime('now')` so that collating works correctly.
+* If a search string is not valid PCRE syntax, a search is done for
+ the exact string instead of just returning an error.
+* Static-linking has been cleaned up.
+* OpenSSL is no longer a requirement.
+* Alpha support for Windows/cygwin.
+* Environment variables can now be accessed in SQL queries using
+ the syntax: `$VAR_NAME`
+* An internal log is kept and written out on a crash.
+* Partition bookmarks are now tracked separately from regular user
+ bookmarks. You can start a partition with the 'partition-name'
+ command and remove it with the 'clear-partition' command.
+* Improved display of possible matches during tab-completion in the
+ command-prompt. The matches are now shown in a separate view and
+ pressing tab repeatedly will scroll through the view.
+* The "open" command now does shell word expansion for file names.
+* More config directory paths have been added: `/etc/lnav`,
+ `$prefix/etc/lnav`, and directories passed on the command-line
+ with `-I`.
+
+## lnav v0.6.2
+
+Features:
+* Word-wrap support.
+
+Fixes:
+* Fix some OS X Mavericks build/runtime issues.
+
+## lnav v0.6.1
+Features:
+* Support for JSON-encoded log files.
+
+Fixes:
+* Some minor fixes and performance improvements.
+
+## lnav v0.6.0
+
+Features:
+* Custom log formats and more builtin formats
+* Automatic extraction of data from logs
+* UI improvements, support for 256 color terminals
+
+## lnav v0.5.1
+
+Features:
+* Added the `-t` and `-w` options which can be used to prepend a
+ timestamp to any data piped in on stdin and to specify a file to
+ write the contents of stdin to.
+
+Fixes:
+* Cleanup for packaging.
+
+## lnav v0.5.0
+
+Features:
+* Files can be specified on the command-line using wildcards so that
+ new files are automatically loaded. Directories can also be passed
+ as command-line arguments to read all of the files in the directory.
+* Builds on cygwin again.
+* Added the `C` hotkey to clear any existing user bookmarks.
+* Added experimental support for accepting input from mice.
+
+Fixes:
+* Internal cleanup.
+* Copying to the clipboard on OS X is now supported.
+* Many bug fixes.
+
+## lnav v0.4.0
+
+Features:
+* Files that are not recognized as containing log messages have been
+ broken out to a separate text files view. You can flip between the
+ log view and the text file view with the `t` hotkey. When viewing
+ text files, the `f` hotkey will switch between files.
+* Files compressed with bzip2 are recognized and decompressed on the
+ fly.
+* Added a "session" file and command for storing commands that should
+ be executed on startup. For example, if you always want some
+ highlighting to be done, you can add that command to the session
+ file.
+
+Fixes:
+* Add some more log file formats for generic log files.
+* Performance improvements for compressed files.
+* Works on OS X now.
+
+## lnav v0.3.0
+
+Changes:
+* The hotkey for the SQL view was changed to `v` and `V` from '.'.
+
+Features:
+* You can now switch between the SQL result view and the log view while
+ keeping the top of the views in sync with the `log_line` column.
+
+Fixes:
+* The `log_line` column is no longer included in the SQL result view's
+ stacked bar graph.
+* Added a "warnings" count to the histogram view.
diff --git a/README b/README
new file mode 100644
index 0000000..904ce9a
--- /dev/null
+++ b/README
@@ -0,0 +1,80 @@
+
+LNAV
+----
+
+The log file navigator, lnav, is an enhanced log file viewer that
+takes advantage of any semantic information that can be gleaned from
+the files being viewed, such as timestamps and log levels. Using this
+extra semantic information, lnav can do things like interleaving
+messages from different files, generate histograms of messages over
+time, and providing hotkeys for navigating through the file. It is
+hoped that these features will allow the user to quickly and
+efficiently zero in on problems.
+
+
+PREREQUISITES
+-------------
+
+The following software packages are required to build/run lnav:
+
+ gcc/clang - A C++14-compatible compiler.
+ libpcre2 - The Perl Compatible Regular Expression v2 (PCRE2) library.
+ sqlite - The SQLite database engine. Version 3.9.0 or higher is required.
+ ncurses - The ncurses text UI library.
+ readline - The readline line editing library.
+ zlib - The zlib compression library.
+ bz2 - The bzip2 compression library.
+ re2c - The re2c scanner generator.
+ libcurl - The cURL library for downloading files from URLs. Version
+ 7.23.0 or higher is required.
+ libarchive - The libarchive library for opening archive files, like zip/tgz.
+ wireshark - The 'tshark' program is used to interpret pcap files.
+
+
+INSTALLATION
+------------
+
+Lnav follows the usual GNU style for configuring and installing software:
+
+Run "./autogen.sh" if compiling from a cloned repository.
+
+ $ ./configure
+ $ make
+ $ sudo make install
+
+
+USING
+-----
+
+The only file installed is the executable, "lnav". You can execute it
+with no arguments to view the default set of files:
+
+ $ lnav
+
+You can view all the syslog messages by running:
+
+ $ lnav /var/log/messages*
+
+
+SUPPORT
+-------
+
+The lnav mailing list can be reached at:
+
+ lnav@googlegroups.com
+
+
+ACKNOWLEDGEMENTS
+----------------
+
+The xterm color database was copied from:
+
+ https://jonasjacek.github.io/colors/
+
+
+SEE ALSO
+--------
+
+The lnav website:
+
+ https://lnav.org
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..5f1185a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,157 @@
+<!-- This is a comment for testing purposes -->
+
+[![Build](https://github.com/tstack/lnav/workflows/ci-build/badge.svg)](https://github.com/tstack/lnav/actions?query=workflow%3Aci-build)
+[![Docs](https://readthedocs.org/projects/lnav/badge/?version=latest&style=plastic)](https://docs.lnav.org)
+[![Coverage Status](https://coveralls.io/repos/github/tstack/lnav/badge.svg?branch=master)](https://coveralls.io/github/tstack/lnav?branch=master)
+[![lnav](https://snapcraft.io/lnav/badge.svg)](https://snapcraft.io/lnav)
+
+[<img src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/62594fddd654fc29fcc07359_cb48d2a8d4991281d7a6a95d2f58195e.svg" height="20"/>](https://discord.gg/erBPnKwz7R)
+
+_This is the source repository for **lnav**, visit [https://lnav.org](https://lnav.org) for a high level overview._
+
+# LNAV -- The Logfile Navigator
+
+The Log File Navigator, **lnav** for short, is an advanced log file viewer
+for the small-scale. It is a terminal application that can understand
+your log files and make it easy for you to find problems with little to
+no setup.
+
+## Screenshot
+
+The following screenshot shows a syslog file. Log lines are displayed with
+highlights. Errors are red and warnings are yellow.
+
+[![Screenshot](docs/assets/images/lnav-syslog-thumb.png)](docs/assets/images/lnav-syslog.png)
+
+## Features
+
+- Log messages from different files are collated together into a single view
+- Automatic detection of log format
+- Automatic decompression of GZip and BZip2 files
+- Filter log messages based on regular expressions
+- Use SQL to analyze your logs
+- And more...
+
+## Installation
+
+[Download a statically-linked binary for Linux/MacOS from the release page](https://github.com/tstack/lnav/releases/latest#release-artifacts)
+
+## Usage
+
+The only file installed is the executable, `lnav`. You can execute it
+with no arguments to view the default set of files:
+
+```
+$ lnav
+```
+
+You can view all the syslog messages by running:
+
+```
+$ lnav /var/log/messages*
+```
+
+### Usage with `systemd-journald`
+
+On systems running `systemd-journald`, you can use `lnav` as the pager:
+
+```
+$ journalctl | lnav
+```
+
+or in follow mode:
+
+```
+$ journalctl -f | lnav
+```
+
+Since `journalctl`'s default output format omits the year, if you are
+viewing logs which span multiple years you will need to change the
+output format to include the year, otherwise `lnav` gets confused:
+
+```
+$ journalctl -o short-iso | lnav
+```
+
+It is also possible to use `journalctl`'s json output format and `lnav`
+will make use of additional fields such as PRIORITY and \_SYSTEMD_UNIT:
+
+```
+$ journalctl -o json | lnav
+```
+
+In case some MESSAGE fields contain special characters such as
+ANSI color codes which are considered as unprintable by journalctl,
+specifying `journalctl`'s `-a` option might be preferable in order
+to output those messages still in a non-binary representation:
+
+```
+$ journalctl -a -o json | lnav
+```
+
+If using systemd v236 or newer, the output fields can be limited to
+the ones actually recognized by `lnav` for increased efficiency:
+
+```
+$ journalctl -o json --output-fields=MESSAGE,PRIORITY,_PID,SYSLOG_IDENTIFIER,_SYSTEMD_UNIT | lnav
+```
+
+If your system has been running for a long time, for increased
+efficiency you may want to limit the number of log lines fed into
+`lnav`, e.g. via `journalctl`'s `-n` or `--since=...` options.
+
+In case of a persistent journal, you may want to limit the number
+of log lines fed into `lnav` via `journalctl`'s `-b` option.
+
+## Support
+
+Please file issues on this repository or use the discussions section.
+The following alternatives are also available:
+
+- [support@lnav.org](mailto:support@lnav.org)
+- [Discord](https://discord.gg/erBPnKwz7R)
+- [Google Groups](https://groups.google.com/g/lnav)
+
+## Links
+
+- [Main Site](https://lnav.org)
+- [**Documentation**](https://docs.lnav.org) on Read the Docs
+- [Internal Architecture](ARCHITECTURE.md)
+
+## Contributing
+
+- [Become a Sponsor on GitHub](https://github.com/sponsors/tstack)
+
+### Building From Source
+
+#### Prerequisites
+
+The following software packages are required to build lnav:
+
+- gcc/clang - A C++14-compatible compiler.
+- libpcre2 - The Perl Compatible Regular Expression v2 (PCRE2) library.
+- sqlite - The SQLite database engine. Version 3.9.0 or higher is required.
+- ncurses - The ncurses text UI library.
+- readline - The readline line editing library.
+- zlib - The zlib compression library.
+- bz2 - The bzip2 compression library.
+- libcurl - The cURL library for downloading files from URLs. Version 7.23.0 or higher is required.
+- libarchive - The libarchive library for opening archive files, like zip/tgz.
+- wireshark - The 'tshark' program is used to interpret pcap files.
+
+#### Build
+
+Lnav follows the usual GNU style for configuring and installing software:
+
+Run `./autogen.sh` if compiling from a cloned repository.
+
+```console
+$ ./configure
+$ make
+$ sudo make install
+```
+
+## See Also
+
+[Angle-grinder](https://github.com/rcoh/angle-grinder) is a tool to slice and dice log files on the command-line.
+If you're familiar with the SumoLogic query language, you might find this tool more comfortable to work with.
diff --git a/TESTS_ENVIRONMENT.in b/TESTS_ENVIRONMENT.in
new file mode 100644
index 0000000..42de497
--- /dev/null
+++ b/TESTS_ENVIRONMENT.in
@@ -0,0 +1,277 @@
+#! /bin/bash
+
+unset NO_COLOR
+
+top_srcdir="@abssrcdir@"
+export top_srcdir
+
+top_srcdir_parent=`dirname ${top_srcdir}`
+export top_srcdir_parent
+
+srcdir="@abssrcdir@/test"
+export srcdir
+
+# The top build directory, derived from the path to this script.
+top_builddir=`dirname $0`
+export top_builddir
+
+builddir=`pwd -P`
+export builddir
+
+test_dir="@abssrcdir@/test"
+export test_dir
+
+# Let the tests know whether bzip is supported or not.
+BZIP2_SUPPORT="@BZIP2_SUPPORT@"
+export BZIP2_SUPPORT
+
+BZIP2_CMD="@BZIP2_CMD@"
+export BZIP2_CMD
+
+XZ_CMD="@XZ_CMD@"
+export XZ_CMD
+
+TSHARK_CMD="@TSHARK_CMD@"
+export TSHARK_CMD
+
+LIBARCHIVE_LIBS="@LIBARCHIVE_LIBS@"
+export LIBARCHIVE_LIBS
+
+HOME="${top_builddir}/test"
+export HOME
+
+# The full path of the test case
+test_file=$1
+# The base name of the test case
+test_file_base=`basename $1`
+# The current test number for shell based tests.
+test_num=0
+
+test_hash=""
+
+lnav_test="${top_builddir}/src/lnav-test"
+export lnav_test
+
+lnav="${top_builddir}/src/lnav"
+export lnav
+
+LNAV_LOG_PATH="${top_builddir}/test/test.log"
+export LNAV_LOG_PATH
+
+SFTP_TEST_URL="@SFTP_TEST_URL@"
+export SFTP_TEST_URL
+
+HAVE_SQLITE3_VALUE_SUBTYPE="@HAVE_SQLITE3_VALUE_SUBTYPE@"
+export HAVE_SQLITE3_VALUE_SUBTYPE
+
+HAVE_SQLITE3_ERROR_OFFSET="@HAVE_SQLITE3_ERROR_OFFSET@"
+export HAVE_SQLITE3_ERROR_OFFSET
+
+## BEGIN Functions
+
+LAST_TEST=""
+
+LAST_CAP_TEST=()
+
+has_errors=""
+
+#
+# Run a test case and capture its standard out and standard err.
+#
+# Usage: run_test <utility> [<argument> ...]
+#
+# Example:
+#
+# To run rktimes and capture all of its stdio output:
+#
+# run_test rktimes -V
+#
+run_test() {
+ printf "%s \033[0;35m=============================================================\033[0m\n" $(date -Iseconds)
+ LAST_TEST=("test: $@")
+ echo "${LAST_TEST[@]}"
+ export test_num=`expr ${test_num} \+ 1`
+ "$@" > ${test_file_base}_${test_num}.tmp 2> ${test_file_base}_${test_num}.err
+}
+
+run_cap_test() {
+ LAST_CAP_TEST=("test: $@")
+ local full_cmd=$(echo "${LAST_CAP_TEST[@]}" | sed -e "s;${test_dir};{test_dir};g" -e "s;${top_srcdir};{top_srcdir};g")
+ export test_hash=$(echo "${full_cmd}" | shasum | cut -f 1 -d ' ')
+ echo "${full_cmd}" > ${test_file_base}_${test_hash}.cmd
+ "$@" > ${test_file_base}_${test_hash}.out 2> ${test_file_base}_${test_hash}.err
+
+ sed -ibak \
+ -e "s;${test_dir};{test_dir};g" \
+ -e "s;${builddir};{test_dir};g" \
+ -e "s;${top_srcdir};{top_srcdir};g" \
+ -e "s;${top_srcdir_parent};{top_srcdir_parent};g" \
+ ${test_file_base}_${test_hash}.out
+ echo
+ printf "%s \033[0;35m=============================================================\033[0m\n" $(date -Iseconds)
+ printf '\033[0;35mCommand\033[0m: %s\n' "${full_cmd}"
+ printf '\033[0;32mBEGIN\033[0m %s\n' "${test_file_base}_${test_hash}.out"
+ cat "${test_file_base}_${test_hash}.out"
+ printf '\033[0;32mEND\033[0m %s\n' "${test_file_base}_${test_hash}.out"
+ if test -f ${srcdir}/expected/${test_file_base}_${test_hash}.out; then
+ diff -w -u \
+ ${srcdir}/expected/${test_file_base}_${test_hash}.out \
+ ${test_file_base}_${test_hash}.out \
+ > ${test_file_base}_${test_hash}.diff
+ if test $? -ne 0; then
+ echo OUT: "${full_cmd}"
+ cat ${test_file_base}_${test_hash}.diff
+ echo "FAIL! EXPECTED OUT DIFF"
+ export has_errors="yes"
+ fi
+ else
+ echo "FAIL! EXPECTED OUT MISSING -- ${srcdir}/expected/${test_file_base}_${test_hash}.out"
+ export has_errors="yes"
+ fi
+
+ sed -ibak -E \
+ -e "s;${test_dir};{test_dir};g" \
+ -e "s;${builddir};{builddir};g" \
+ -e "s;${top_srcdir};{top_srcdir};g" \
+ -e 's;"errorId":".+";;g' \
+ ${test_file_base}_${test_hash}.err
+ printf '\033[0;31mBEGIN\033[0m %s\n' "${test_file_base}_${test_hash}.err"
+ cat "${test_file_base}_${test_hash}.err"
+ printf '\033[0;31mEND\033[0m %s\n' "${test_file_base}_${test_hash}.err"
+ if test -f ${srcdir}/expected/${test_file_base}_${test_hash}.err; then
+ diff -w -u ${srcdir}/expected/${test_file_base}_${test_hash}.err \
+ ${test_file_base}_${test_hash}.err \
+ > ${test_file_base}_${test_hash}.err.diff
+ if test $? -ne 0; then
+ echo ERR: "${full_cmd}"
+ cat ${test_file_base}_${test_hash}.err.diff
+ echo "FAIL! EXPECTED ERR DIFF"
+ export has_errors="yes"
+ fi
+ else
+ echo "FAIL! EXPECTED ERR MISSING"
+ export has_errors="yes"
+ fi
+}
+
+#
+# Check the output generated by a run_test() call.
+#
+# Usage: check_output <fail message> {Expected output on stdin}
+#
+# Example:
+#
+# To check the output of 'rktimes -V' and print out 'Unable to get version?'
+# if the output doesn't match:
+#
+# run_test rktimes -V
+# check_output "Unable to get version?" <<EOF
+# 0.5
+# EOF
+#
+check_output() {
+ sed -ibak \
+ -e "s;${test_dir};{test_dir};g" \
+ -e "s;${top_srcdir};{top_srcdir};g" \
+ ${test_file_base}_${test_num}.tmp
+ diff -w -u - ${test_file_base}_${test_num}.tmp > ${test_file_base}_${test_num}.diff
+ if test $? -ne 0; then
+ echo "${LAST_TEST[@]}"
+ echo $1
+ cat ${test_file_base}_${test_num}.diff
+ exit 1
+ fi
+}
+
+check_output_ws() {
+ diff -u - ${test_file_base}_${test_num}.tmp > ${test_file_base}_${test_num}.diff
+ if test $? -ne 0; then
+ echo "${LAST_TEST[@]}"
+ echo $1
+ cat ${test_file_base}_${test_num}.diff
+ exit 1
+ fi
+}
+
+test_filename() {
+ echo ${test_file_base}_${test_num}.tmp
+}
+
+test_err_filename() {
+ echo ${test_file_base}_${test_num}.err
+}
+
+check_error_output() {
+ sed -ibak \
+ -e "s;${test_dir};{test_dir};g" \
+ -e "s;${top_srcdir};{top_srcdir};g" \
+ ${test_file_base}_${test_num}.err
+ diff -w -u - ${test_file_base}_${test_num}.err \
+ > ${test_file_base}_${test_num}.err.diff
+ if test $? -ne 0; then
+ echo "${LAST_TEST[@]}"
+ echo $1
+ cat ${test_file_base}_${test_num}.err.diff
+ exit 1
+ fi
+}
+
+#
+# Grep for a string in the output generated by a run_test() call.
+#
+# Usage: grep_output_for <string> <fail message>
+#
+# Example:
+#
+# To check the output of 'cbhey -l' for 'IDL:Foobar:1.0' and print out
+# 'Unable to list supported interfaces?' if it is not found:
+#
+# run_test cbhey -l
+# grep_output_for "IDL:Foobar:1.0" "Unable to list supported interface?"
+#
+grep_output_for() {
+ if grep -q $1 ${test_file_base}_${test_num}.tmp > /dev/null 2>&1; then
+ :
+ else
+ echo "${test_file_base}_${test_num}.tmp: $2"
+ exit 1
+ fi
+}
+
+on_error_log() {
+ if test $? -ne 0; then
+ echo $1 > /dev/stderr
+ cat ${test_file_base}_${test_num}.tmp
+ cat ${test_file_base}_${test_num}.err
+ fi
+}
+
+on_error_fail_with() {
+ if test $? -ne 0; then
+ echo $1 > /dev/stderr
+ cat ${test_file_base}_${test_num}.tmp
+ cat ${test_file_base}_${test_num}.err
+ exit 1
+ fi
+}
+
+## END Functions
+
+
+# Finally, run the test...
+
+if test -x $1 && test `basename $1 .sh` == `basename $1`; then
+ exec $*
+else
+ # Shell script
+ shift
+ . ${test_file}
+fi
+
+cleanup() {
+ if test "${has_errors}"x = "yes"x; then
+ exit 1
+ fi
+}
+
+trap "cleanup" EXIT
diff --git a/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..196739e
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,17 @@
+version: "{build}"
+image: Visual Studio 2022
+
+environment:
+ matrix:
+ - cygwin: cygwin64
+ cygsetup: setup-x86_64.exe
+
+install:
+ - C:\%cygwin%\%cygsetup% -qnNdOX -R C:/%cygwin% -l C:/%cygwin%/var/cache/setup -P libpcre2-devel -P libncurses-devel -P libreadline-devel -P zlib-devel -P libbz2-devel -P libsqlite3-devel -P libcurl-devel -P libarchive-devel
+
+build_script:
+ - C:\%cygwin%\bin\sh -lc "uname -a && gcc --version && cd /cygdrive/c/projects/lnav && ./autogen.sh && ./configure && make && strip src/lnav.exe && ldd src/lnav.exe"
+
+artifacts:
+ - path: src\lnav.exe
+ name: lnav.exe
diff --git a/autogen.sh b/autogen.sh
new file mode 100755
index 0000000..c235230
--- /dev/null
+++ b/autogen.sh
@@ -0,0 +1,26 @@
+#! /bin/sh
+
+
+if test x"${AUTORECONF}" = x""; then
+ autoreconf -V 1>/dev/null 2>/dev/null
+ if test $? -eq 0; then
+ AUTORECONF=autoreconf
+ fi
+fi
+
+if test x"${AUTORECONF}" != x""; then
+ ${AUTORECONF} -vfi -I m4
+else
+ AUTOCONF=${AUTOCONF:-autoconf}
+ AUTOMAKE=${AUTOMAKE:-automake}
+ AUTOHEADER=${AUTOHEADER:-autoheader}
+ ACLOCAL=${ACLOCAL:-aclocal}
+
+ ${AUTOCONF} --version
+ ${AUTOMAKE} --version
+
+ ${ACLOCAL} -I m4 -I .
+ ${AUTOHEADER} -I .
+ ${AUTOMAKE} --add-missing --copy --force-missing --foreign
+ ${AUTOCONF}
+fi
diff --git a/cleanup_expected.sh b/cleanup_expected.sh
new file mode 100755
index 0000000..5be90be
--- /dev/null
+++ b/cleanup_expected.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+
+srcdir="$1"
+builddir="$2"
+
+for fname in "${srcdir}"/expected/*.out; do
+ stem=$(basename "$fname" | sed -e 's/.out$//')
+
+ if ! test -f "${builddir}/$stem.cmd"; then
+ echo "removing $fname"
+ guilt rm "$fname"
+ echo "removing ${srcdir}/expected/${stem}.err"
+ guilt rm "${srcdir}/expected/${stem}.err"
+ fi
+done
diff --git a/cmake/CodeCoverage.cmake b/cmake/CodeCoverage.cmake
new file mode 100644
index 0000000..2f54ee0
--- /dev/null
+++ b/cmake/CodeCoverage.cmake
@@ -0,0 +1,438 @@
+# Copyright (c) 2012 - 2017, Lars Bilke
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without modification,
+# are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors
+# may be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+#
+# CHANGES:
+#
+# 2012-01-31, Lars Bilke
+# - Enable Code Coverage
+#
+# 2013-09-17, Joakim Söderberg
+# - Added support for Clang.
+# - Some additional usage instructions.
+#
+# 2016-02-03, Lars Bilke
+# - Refactored functions to use named parameters
+#
+# 2017-06-02, Lars Bilke
+# - Merged with modified version from github.com/ufz/ogs
+#
+# 2019-05-06, Anatolii Kurotych
+# - Remove unnecessary --coverage flag
+#
+# 2019-12-13, FeRD (Frank Dana)
+# - Deprecate COVERAGE_LCOVR_EXCLUDES and COVERAGE_GCOVR_EXCLUDES lists in favor
+# of tool-agnostic COVERAGE_EXCLUDES variable, or EXCLUDE setup arguments.
+# - CMake 3.4+: All excludes can be specified relative to BASE_DIRECTORY
+# - All setup functions: accept BASE_DIRECTORY, EXCLUDE list
+# - Set lcov basedir with -b argument
+# - Add automatic --demangle-cpp in lcovr, if 'c++filt' is available (can be
+# overridden with NO_DEMANGLE option in setup_target_for_coverage_lcovr().)
+# - Delete output dir, .info file on 'make clean'
+# - Remove Python detection, since version mismatches will break gcovr
+# - Minor cleanup (lowercase function names, update examples...)
+#
+# 2019-12-19, FeRD (Frank Dana)
+# - Rename Lcov outputs, make filtered file canonical, fix cleanup for targets
+#
+# 2020-01-19, Bob Apthorpe
+# - Added gfortran support
+#
+# 2020-02-17, FeRD (Frank Dana)
+# - Make all add_custom_target()s VERBATIM to auto-escape wildcard characters
+# in EXCLUDEs, and remove manual escaping from gcovr targets
+#
+# USAGE:
+#
+# 1. Copy this file into your cmake modules path.
+#
+# 2. Add the following line to your CMakeLists.txt (best inside an if-condition
+# using a CMake option() to enable it just optionally):
+# include(CodeCoverage)
+#
+# 3. Append necessary compiler flags:
+# append_coverage_compiler_flags()
+#
+# 3.a (OPTIONAL) Set appropriate optimization flags, e.g. -O0, -O1 or -Og
+#
+# 4. If you need to exclude additional directories from the report, specify them
+# using full paths in the COVERAGE_EXCLUDES variable before calling
+# setup_target_for_coverage_*().
+# Example:
+# set(COVERAGE_EXCLUDES
+# '${PROJECT_SOURCE_DIR}/src/dir1/*'
+# '/path/to/my/src/dir2/*')
+# Or, use the EXCLUDE argument to setup_target_for_coverage_*().
+# Example:
+# setup_target_for_coverage_lcov(
+# NAME coverage
+# EXECUTABLE testrunner
+# EXCLUDE "${PROJECT_SOURCE_DIR}/src/dir1/*" "/path/to/my/src/dir2/*")
+#
+# 4.a NOTE: With CMake 3.4+, COVERAGE_EXCLUDES or EXCLUDE can also be set
+# relative to the BASE_DIRECTORY (default: PROJECT_SOURCE_DIR)
+# Example:
+# set(COVERAGE_EXCLUDES "dir1/*")
+# setup_target_for_coverage_gcovr_html(
+# NAME coverage
+# EXECUTABLE testrunner
+# BASE_DIRECTORY "${PROJECT_SOURCE_DIR}/src"
+# EXCLUDE "dir2/*")
+#
+# 5. Use the functions described below to create a custom make target which
+# runs your test executable and produces a code coverage report.
+#
+# 6. Build a Debug build:
+# cmake -DCMAKE_BUILD_TYPE=Debug ..
+# make
+# make my_coverage_target
+#
+
+include(CMakeParseArguments)
+
+# Check prereqs
+find_program( GCOV_PATH gcov )
+find_program( LCOV_PATH NAMES lcov lcov.bat lcov.exe lcov.perl)
+find_program( GENHTML_PATH NAMES genhtml genhtml.perl genhtml.bat )
+find_program( GCOVR_PATH gcovr PATHS ${CMAKE_SOURCE_DIR}/scripts/test)
+find_program( CPPFILT_PATH NAMES c++filt )
+
+if(NOT GCOV_PATH)
+ message(FATAL_ERROR "gcov not found! Aborting...")
+endif() # NOT GCOV_PATH
+
+message("foo ${CMAKE_CXX_COMPILER_ID}")
+
+if("${CMAKE_CXX_COMPILER_ID}" MATCHES "(Apple)?[Cc]lang")
+ if("${CMAKE_CXX_COMPILER_VERSION}" VERSION_LESS 3)
+ message(FATAL_ERROR "Clang version must be 3.0.0 or greater! Aborting...")
+ endif()
+elseif(NOT CMAKE_COMPILER_IS_GNUCXX)
+ if("${CMAKE_Fortran_COMPILER_ID}" MATCHES "[Ff]lang")
+ # Do nothing; exit conditional without error if true
+ elseif("${CMAKE_Fortran_COMPILER_ID}" MATCHES "GNU")
+ # Do nothing; exit conditional without error if true
+ else()
+ message(FATAL_ERROR "Compiler is not GNU gcc! Aborting...")
+ endif()
+endif()
+
+set(COVERAGE_COMPILER_FLAGS "-g -fprofile-arcs -ftest-coverage"
+ CACHE INTERNAL "")
+
+set(CMAKE_Fortran_FLAGS_COVERAGE
+ ${COVERAGE_COMPILER_FLAGS}
+ CACHE STRING "Flags used by the Fortran compiler during coverage builds."
+ FORCE )
+set(CMAKE_CXX_FLAGS_COVERAGE
+ ${COVERAGE_COMPILER_FLAGS}
+ CACHE STRING "Flags used by the C++ compiler during coverage builds."
+ FORCE )
+set(CMAKE_C_FLAGS_COVERAGE
+ ${COVERAGE_COMPILER_FLAGS}
+ CACHE STRING "Flags used by the C compiler during coverage builds."
+ FORCE )
+set(CMAKE_EXE_LINKER_FLAGS_COVERAGE
+ ""
+ CACHE STRING "Flags used for linking binaries during coverage builds."
+ FORCE )
+set(CMAKE_SHARED_LINKER_FLAGS_COVERAGE
+ ""
+ CACHE STRING "Flags used by the shared libraries linker during coverage builds."
+ FORCE )
+mark_as_advanced(
+ CMAKE_Fortran_FLAGS_COVERAGE
+ CMAKE_CXX_FLAGS_COVERAGE
+ CMAKE_C_FLAGS_COVERAGE
+ CMAKE_EXE_LINKER_FLAGS_COVERAGE
+ CMAKE_SHARED_LINKER_FLAGS_COVERAGE )
+
+if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
+ message(WARNING "Code coverage results with an optimised (non-Debug) build may be misleading")
+endif() # NOT CMAKE_BUILD_TYPE STREQUAL "Debug"
+
+if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_Fortran_COMPILER_ID STREQUAL "GNU")
+ link_libraries(gcov)
+endif()
+
+# Defines a target for running and collection code coverage information
+# Builds dependencies, runs the given executable and outputs reports.
+# NOTE! The executable should always have a ZERO as exit code otherwise
+# the coverage generation will not complete.
+#
+# setup_target_for_coverage_lcov(
+# NAME testrunner_coverage # New target name
+# EXECUTABLE testrunner -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
+# DEPENDENCIES testrunner # Dependencies to build first
+# BASE_DIRECTORY "../" # Base directory for report
+# # (defaults to PROJECT_SOURCE_DIR)
+# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative
+# # to BASE_DIRECTORY, with CMake 3.4+)
+# NO_DEMANGLE # Don't demangle C++ symbols
+# # even if c++filt is found
+# )
+function(setup_target_for_coverage_lcov)
+
+ set(options NO_DEMANGLE)
+ set(oneValueArgs BASE_DIRECTORY NAME)
+ set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES LCOV_ARGS GENHTML_ARGS)
+ cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ if(NOT LCOV_PATH)
+ message(FATAL_ERROR "lcov not found! Aborting...")
+ endif() # NOT LCOV_PATH
+
+ if(NOT GENHTML_PATH)
+ message(FATAL_ERROR "genhtml not found! Aborting...")
+ endif() # NOT GENHTML_PATH
+
+ # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
+ if(${Coverage_BASE_DIRECTORY})
+ get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
+ else()
+ set(BASEDIR ${PROJECT_SOURCE_DIR})
+ endif()
+
+ # Collect excludes (CMake 3.4+: Also compute absolute paths)
+ set(LCOV_EXCLUDES "")
+ foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_LCOV_EXCLUDES})
+ if(CMAKE_VERSION VERSION_GREATER 3.4)
+ get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
+ endif()
+ list(APPEND LCOV_EXCLUDES "${EXCLUDE}")
+ endforeach()
+ list(REMOVE_DUPLICATES LCOV_EXCLUDES)
+
+ # Conditional arguments
+ if(CPPFILT_PATH AND NOT ${Coverage_NO_DEMANGLE})
+ set(GENHTML_EXTRA_ARGS "--demangle-cpp")
+ endif()
+
+ # Setup target
+ add_custom_target(${Coverage_NAME}
+
+ # Cleanup lcov
+ COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -directory . -b ${BASEDIR} --zerocounters
+ # Create baseline to make sure untouched files show up in the report
+ COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -c -i -d . -b ${BASEDIR} -o ${Coverage_NAME}.base
+
+ # Run tests
+ COMMAND ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
+
+ # Capturing lcov counters and generating report
+ COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --directory . -b ${BASEDIR} --capture --output-file ${Coverage_NAME}.capture
+ # add baseline counters
+ COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} -a ${Coverage_NAME}.base -a ${Coverage_NAME}.capture --output-file ${Coverage_NAME}.total
+ # filter collected data to final coverage report
+ COMMAND ${LCOV_PATH} ${Coverage_LCOV_ARGS} --gcov-tool ${GCOV_PATH} --remove ${Coverage_NAME}.total ${LCOV_EXCLUDES} --output-file ${Coverage_NAME}.info
+
+ # Generate HTML output
+ COMMAND ${GENHTML_PATH} ${GENHTML_EXTRA_ARGS} ${Coverage_GENHTML_ARGS} -o ${Coverage_NAME} ${Coverage_NAME}.info
+
+ # Set output files as GENERATED (will be removed on 'make clean')
+ BYPRODUCTS
+ ${Coverage_NAME}.base
+ ${Coverage_NAME}.capture
+ ${Coverage_NAME}.total
+ ${Coverage_NAME}.info
+ ${Coverage_NAME} # report directory
+
+ WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
+ DEPENDS ${Coverage_DEPENDENCIES}
+ VERBATIM # Protect arguments to commands
+ COMMENT "Resetting code coverage counters to zero.\nProcessing code coverage counters and generating report."
+ )
+
+ # Show where to find the lcov info report
+ add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
+ COMMAND ;
+ COMMENT "Lcov code coverage info report saved in ${Coverage_NAME}.info."
+ )
+
+ # Show info where to find the report
+ add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
+ COMMAND ;
+ COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
+ )
+
+endfunction() # setup_target_for_coverage_lcov
+
+# Defines a target for running and collection code coverage information
+# Builds dependencies, runs the given executable and outputs reports.
+# NOTE! The executable should always have a ZERO as exit code otherwise
+# the coverage generation will not complete.
+#
+# setup_target_for_coverage_gcovr_xml(
+# NAME ctest_coverage # New target name
+# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
+# DEPENDENCIES executable_target # Dependencies to build first
+# BASE_DIRECTORY "../" # Base directory for report
+# # (defaults to PROJECT_SOURCE_DIR)
+# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative
+# # to BASE_DIRECTORY, with CMake 3.4+)
+# )
+function(setup_target_for_coverage_gcovr_xml)
+
+ set(options NONE)
+ set(oneValueArgs BASE_DIRECTORY NAME)
+ set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
+ cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ if(NOT GCOVR_PATH)
+ message(FATAL_ERROR "gcovr not found! Aborting...")
+ endif() # NOT GCOVR_PATH
+
+ # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
+ if(${Coverage_BASE_DIRECTORY})
+ get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
+ else()
+ set(BASEDIR ${PROJECT_SOURCE_DIR})
+ endif()
+
+ # Collect excludes (CMake 3.4+: Also compute absolute paths)
+ set(GCOVR_EXCLUDES "")
+ foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES})
+ if(CMAKE_VERSION VERSION_GREATER 3.4)
+ get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
+ endif()
+ list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
+ endforeach()
+ list(REMOVE_DUPLICATES GCOVR_EXCLUDES)
+
+ # Combine excludes to several -e arguments
+ set(GCOVR_EXCLUDE_ARGS "")
+ foreach(EXCLUDE ${GCOVR_EXCLUDES})
+ list(APPEND GCOVR_EXCLUDE_ARGS "-e")
+ list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}")
+ endforeach()
+
+ add_custom_target(${Coverage_NAME}
+ # Run tests
+ ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
+
+ # Running gcovr
+ COMMAND ${GCOVR_PATH} --xml
+ -r ${BASEDIR} ${GCOVR_EXCLUDE_ARGS}
+ --object-directory=${PROJECT_BINARY_DIR}
+ -o ${Coverage_NAME}.xml
+ BYPRODUCTS ${Coverage_NAME}.xml
+ WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
+ DEPENDS ${Coverage_DEPENDENCIES}
+ VERBATIM # Protect arguments to commands
+ COMMENT "Running gcovr to produce Cobertura code coverage report."
+ )
+
+ # Show info where to find the report
+ add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
+ COMMAND ;
+ COMMENT "Cobertura code coverage report saved in ${Coverage_NAME}.xml."
+ )
+endfunction() # setup_target_for_coverage_gcovr_xml
+
+# Defines a target for running and collection code coverage information
+# Builds dependencies, runs the given executable and outputs reports.
+# NOTE! The executable should always have a ZERO as exit code otherwise
+# the coverage generation will not complete.
+#
+# setup_target_for_coverage_gcovr_html(
+# NAME ctest_coverage # New target name
+# EXECUTABLE ctest -j ${PROCESSOR_COUNT} # Executable in PROJECT_BINARY_DIR
+# DEPENDENCIES executable_target # Dependencies to build first
+# BASE_DIRECTORY "../" # Base directory for report
+# # (defaults to PROJECT_SOURCE_DIR)
+# EXCLUDE "src/dir1/*" "src/dir2/*" # Patterns to exclude (can be relative
+# # to BASE_DIRECTORY, with CMake 3.4+)
+# )
+function(setup_target_for_coverage_gcovr_html)
+
+ set(options NONE)
+ set(oneValueArgs BASE_DIRECTORY NAME)
+ set(multiValueArgs EXCLUDE EXECUTABLE EXECUTABLE_ARGS DEPENDENCIES)
+ cmake_parse_arguments(Coverage "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN})
+
+ if(NOT GCOVR_PATH)
+ message(FATAL_ERROR "gcovr not found! Aborting...")
+ endif() # NOT GCOVR_PATH
+
+ # Set base directory (as absolute path), or default to PROJECT_SOURCE_DIR
+ if(${Coverage_BASE_DIRECTORY})
+ get_filename_component(BASEDIR ${Coverage_BASE_DIRECTORY} ABSOLUTE)
+ else()
+ set(BASEDIR ${PROJECT_SOURCE_DIR})
+ endif()
+
+ # Collect excludes (CMake 3.4+: Also compute absolute paths)
+ set(GCOVR_EXCLUDES "")
+ foreach(EXCLUDE ${Coverage_EXCLUDE} ${COVERAGE_EXCLUDES} ${COVERAGE_GCOVR_EXCLUDES})
+ if(CMAKE_VERSION VERSION_GREATER 3.4)
+ get_filename_component(EXCLUDE ${EXCLUDE} ABSOLUTE BASE_DIR ${BASEDIR})
+ endif()
+ list(APPEND GCOVR_EXCLUDES "${EXCLUDE}")
+ endforeach()
+ list(REMOVE_DUPLICATES GCOVR_EXCLUDES)
+
+ # Combine excludes to several -e arguments
+ set(GCOVR_EXCLUDE_ARGS "")
+ foreach(EXCLUDE ${GCOVR_EXCLUDES})
+ list(APPEND GCOVR_EXCLUDE_ARGS "-e")
+ list(APPEND GCOVR_EXCLUDE_ARGS "${EXCLUDE}")
+ endforeach()
+
+ add_custom_target(${Coverage_NAME}
+ # Run tests
+ ${Coverage_EXECUTABLE} ${Coverage_EXECUTABLE_ARGS}
+
+ # Create folder
+ COMMAND ${CMAKE_COMMAND} -E make_directory ${PROJECT_BINARY_DIR}/${Coverage_NAME}
+
+ # Running gcovr
+ COMMAND ${GCOVR_PATH} --html --html-details
+ -r ${BASEDIR} ${GCOVR_EXCLUDE_ARGS}
+ --object-directory=${PROJECT_BINARY_DIR}
+ -o ${Coverage_NAME}/index.html
+
+ BYPRODUCTS ${PROJECT_BINARY_DIR}/${Coverage_NAME} # report directory
+ WORKING_DIRECTORY ${PROJECT_BINARY_DIR}
+ DEPENDS ${Coverage_DEPENDENCIES}
+ VERBATIM # Protect arguments to commands
+ COMMENT "Running gcovr to produce HTML code coverage report."
+ )
+
+ # Show info where to find the report
+ add_custom_command(TARGET ${Coverage_NAME} POST_BUILD
+ COMMAND ;
+ COMMENT "Open ./${Coverage_NAME}/index.html in your browser to view the coverage report."
+ )
+
+endfunction() # setup_target_for_coverage_gcovr_html
+
+function(append_coverage_compiler_flags)
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
+ set(CMAKE_Fortran_FLAGS "${CMAKE_Fortran_FLAGS} ${COVERAGE_COMPILER_FLAGS}" PARENT_SCOPE)
+ message(STATUS "Appending code coverage compiler flags: ${COVERAGE_COMPILER_FLAGS}")
+endfunction() # append_coverage_compiler_flags
diff --git a/cmake/Hunter/config.cmake b/cmake/Hunter/config.cmake
new file mode 100644
index 0000000..f731b2d
--- /dev/null
+++ b/cmake/Hunter/config.cmake
@@ -0,0 +1,14 @@
+
+hunter_config(
+ libpcre
+ VERSION 8.41
+ CMAKE_ARGS
+ EXTRA_FLAGS=--enable-unicode-properties --enable-jit --enable-utf
+)
+
+hunter_config(
+ readline
+ VERSION 6.3
+ CMAKE_ARGS
+ EXTRA_FLAGS=CFLAGS=-Wno-implicit-function-declaration
+)
diff --git a/cmake/HunterGate.cmake b/cmake/HunterGate.cmake
new file mode 100644
index 0000000..e78d3e8
--- /dev/null
+++ b/cmake/HunterGate.cmake
@@ -0,0 +1,528 @@
+# Copyright (c) 2013-2019, Ruslan Baratov
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# This is a gate file to Hunter package manager.
+# Include this file using `include` command and add package you need, example:
+#
+# cmake_minimum_required(VERSION 3.2)
+#
+# include("cmake/HunterGate.cmake")
+# HunterGate(
+# URL "https://github.com/path/to/hunter/archive.tar.gz"
+# SHA1 "798501e983f14b28b10cda16afa4de69eee1da1d"
+# )
+#
+# project(MyProject)
+#
+# hunter_add_package(Foo)
+# hunter_add_package(Boo COMPONENTS Bar Baz)
+#
+# Projects:
+# * https://github.com/hunter-packages/gate/
+# * https://github.com/ruslo/hunter
+
+option(HUNTER_ENABLED "Enable Hunter package manager support" ON)
+
+if(HUNTER_ENABLED)
+ if(CMAKE_VERSION VERSION_LESS "3.2")
+ message(
+ FATAL_ERROR
+ "At least CMake version 3.2 required for Hunter dependency management."
+ " Update CMake or set HUNTER_ENABLED to OFF."
+ )
+ endif()
+endif()
+
+include(CMakeParseArguments) # cmake_parse_arguments
+
+option(HUNTER_STATUS_PRINT "Print working status" ON)
+option(HUNTER_STATUS_DEBUG "Print a lot info" OFF)
+option(HUNTER_TLS_VERIFY "Enable/disable TLS certificate checking on downloads" ON)
+
+set(HUNTER_ERROR_PAGE "https://docs.hunter.sh/en/latest/reference/errors")
+
+function(hunter_gate_status_print)
+ if(HUNTER_STATUS_PRINT OR HUNTER_STATUS_DEBUG)
+ foreach(print_message ${ARGV})
+ message(STATUS "[hunter] ${print_message}")
+ endforeach()
+ endif()
+endfunction()
+
+function(hunter_gate_status_debug)
+ if(HUNTER_STATUS_DEBUG)
+ foreach(print_message ${ARGV})
+ string(TIMESTAMP timestamp)
+ message(STATUS "[hunter *** DEBUG *** ${timestamp}] ${print_message}")
+ endforeach()
+ endif()
+endfunction()
+
+function(hunter_gate_error_page error_page)
+ message("------------------------------ ERROR ------------------------------")
+ message(" ${HUNTER_ERROR_PAGE}/${error_page}.html")
+ message("-------------------------------------------------------------------")
+ message("")
+ message(FATAL_ERROR "")
+endfunction()
+
+function(hunter_gate_internal_error)
+ message("")
+ foreach(print_message ${ARGV})
+ message("[hunter ** INTERNAL **] ${print_message}")
+ endforeach()
+ message("[hunter ** INTERNAL **] [Directory:${CMAKE_CURRENT_LIST_DIR}]")
+ message("")
+ hunter_gate_error_page("error.internal")
+endfunction()
+
+function(hunter_gate_fatal_error)
+ cmake_parse_arguments(hunter "" "ERROR_PAGE" "" "${ARGV}")
+ if("${hunter_ERROR_PAGE}" STREQUAL "")
+ hunter_gate_internal_error("Expected ERROR_PAGE")
+ endif()
+ message("")
+ foreach(x ${hunter_UNPARSED_ARGUMENTS})
+ message("[hunter ** FATAL ERROR **] ${x}")
+ endforeach()
+ message("[hunter ** FATAL ERROR **] [Directory:${CMAKE_CURRENT_LIST_DIR}]")
+ message("")
+ hunter_gate_error_page("${hunter_ERROR_PAGE}")
+endfunction()
+
+function(hunter_gate_user_error)
+ hunter_gate_fatal_error(${ARGV} ERROR_PAGE "error.incorrect.input.data")
+endfunction()
+
+function(hunter_gate_self root version sha1 result)
+ string(COMPARE EQUAL "${root}" "" is_bad)
+ if(is_bad)
+ hunter_gate_internal_error("root is empty")
+ endif()
+
+ string(COMPARE EQUAL "${version}" "" is_bad)
+ if(is_bad)
+ hunter_gate_internal_error("version is empty")
+ endif()
+
+ string(COMPARE EQUAL "${sha1}" "" is_bad)
+ if(is_bad)
+ hunter_gate_internal_error("sha1 is empty")
+ endif()
+
+ string(SUBSTRING "${sha1}" 0 7 archive_id)
+
+ set(
+ hunter_self
+ "${root}/_Base/Download/Hunter/${version}/${archive_id}/Unpacked"
+ )
+
+ set("${result}" "${hunter_self}" PARENT_SCOPE)
+endfunction()
+
+# Set HUNTER_GATE_ROOT cmake variable to suitable value.
+function(hunter_gate_detect_root)
+ # Check CMake variable
+ string(COMPARE NOTEQUAL "${HUNTER_ROOT}" "" not_empty)
+ if(not_empty)
+ set(HUNTER_GATE_ROOT "${HUNTER_ROOT}" PARENT_SCOPE)
+ hunter_gate_status_debug("HUNTER_ROOT detected by cmake variable")
+ return()
+ endif()
+
+ # Check environment variable
+ string(COMPARE NOTEQUAL "$ENV{HUNTER_ROOT}" "" not_empty)
+ if(not_empty)
+ set(HUNTER_GATE_ROOT "$ENV{HUNTER_ROOT}" PARENT_SCOPE)
+ hunter_gate_status_debug("HUNTER_ROOT detected by environment variable")
+ return()
+ endif()
+
+ # Check HOME environment variable
+ string(COMPARE NOTEQUAL "$ENV{HOME}" "" result)
+ if(result)
+ set(HUNTER_GATE_ROOT "$ENV{HOME}/.hunter" PARENT_SCOPE)
+ hunter_gate_status_debug("HUNTER_ROOT set using HOME environment variable")
+ return()
+ endif()
+
+ # Check SYSTEMDRIVE and USERPROFILE environment variable (windows only)
+ if(WIN32)
+ string(COMPARE NOTEQUAL "$ENV{SYSTEMDRIVE}" "" result)
+ if(result)
+ set(HUNTER_GATE_ROOT "$ENV{SYSTEMDRIVE}/.hunter" PARENT_SCOPE)
+ hunter_gate_status_debug(
+ "HUNTER_ROOT set using SYSTEMDRIVE environment variable"
+ )
+ return()
+ endif()
+
+ string(COMPARE NOTEQUAL "$ENV{USERPROFILE}" "" result)
+ if(result)
+ set(HUNTER_GATE_ROOT "$ENV{USERPROFILE}/.hunter" PARENT_SCOPE)
+ hunter_gate_status_debug(
+ "HUNTER_ROOT set using USERPROFILE environment variable"
+ )
+ return()
+ endif()
+ endif()
+
+ hunter_gate_fatal_error(
+ "Can't detect HUNTER_ROOT"
+ ERROR_PAGE "error.detect.hunter.root"
+ )
+endfunction()
+
+function(hunter_gate_download dir)
+ string(
+ COMPARE
+ NOTEQUAL
+ "$ENV{HUNTER_DISABLE_AUTOINSTALL}"
+ ""
+ disable_autoinstall
+ )
+ if(disable_autoinstall AND NOT HUNTER_RUN_INSTALL)
+ hunter_gate_fatal_error(
+ "Hunter not found in '${dir}'"
+ "Set HUNTER_RUN_INSTALL=ON to auto-install it from '${HUNTER_GATE_URL}'"
+ "Settings:"
+ " HUNTER_ROOT: ${HUNTER_GATE_ROOT}"
+ " HUNTER_SHA1: ${HUNTER_GATE_SHA1}"
+ ERROR_PAGE "error.run.install"
+ )
+ endif()
+ string(COMPARE EQUAL "${dir}" "" is_bad)
+ if(is_bad)
+ hunter_gate_internal_error("Empty 'dir' argument")
+ endif()
+
+ string(COMPARE EQUAL "${HUNTER_GATE_SHA1}" "" is_bad)
+ if(is_bad)
+ hunter_gate_internal_error("HUNTER_GATE_SHA1 empty")
+ endif()
+
+ string(COMPARE EQUAL "${HUNTER_GATE_URL}" "" is_bad)
+ if(is_bad)
+ hunter_gate_internal_error("HUNTER_GATE_URL empty")
+ endif()
+
+ set(done_location "${dir}/DONE")
+ set(sha1_location "${dir}/SHA1")
+
+ set(build_dir "${dir}/Build")
+ set(cmakelists "${dir}/CMakeLists.txt")
+
+ hunter_gate_status_debug("Locking directory: ${dir}")
+ file(LOCK "${dir}" DIRECTORY GUARD FUNCTION)
+ hunter_gate_status_debug("Lock done")
+
+ if(EXISTS "${done_location}")
+ # while waiting for lock other instance can do all the job
+ hunter_gate_status_debug("File '${done_location}' found, skip install")
+ return()
+ endif()
+
+ file(REMOVE_RECURSE "${build_dir}")
+ file(REMOVE_RECURSE "${cmakelists}")
+
+ file(MAKE_DIRECTORY "${build_dir}") # check directory permissions
+
+ # Disabling languages speeds up a little bit, reduces noise in the output
+ # and avoids path too long windows error
+ file(
+ WRITE
+ "${cmakelists}"
+ "cmake_minimum_required(VERSION 3.2)\n"
+ "project(HunterDownload LANGUAGES NONE)\n"
+ "include(ExternalProject)\n"
+ "ExternalProject_Add(\n"
+ " Hunter\n"
+ " URL\n"
+ " \"${HUNTER_GATE_URL}\"\n"
+ " URL_HASH\n"
+ " SHA1=${HUNTER_GATE_SHA1}\n"
+ " DOWNLOAD_DIR\n"
+ " \"${dir}\"\n"
+ " TLS_VERIFY\n"
+ " ${HUNTER_TLS_VERIFY}\n"
+ " SOURCE_DIR\n"
+ " \"${dir}/Unpacked\"\n"
+ " CONFIGURE_COMMAND\n"
+ " \"\"\n"
+ " BUILD_COMMAND\n"
+ " \"\"\n"
+ " INSTALL_COMMAND\n"
+ " \"\"\n"
+ ")\n"
+ )
+
+ if(HUNTER_STATUS_DEBUG)
+ set(logging_params "")
+ else()
+ set(logging_params OUTPUT_QUIET)
+ endif()
+
+ hunter_gate_status_debug("Run generate")
+
+ # Need to add toolchain file too.
+ # Otherwise on Visual Studio + MDD this will fail with error:
+ # "Could not find an appropriate version of the Windows 10 SDK installed on this machine"
+ if(EXISTS "${CMAKE_TOOLCHAIN_FILE}")
+ get_filename_component(absolute_CMAKE_TOOLCHAIN_FILE "${CMAKE_TOOLCHAIN_FILE}" ABSOLUTE)
+ set(toolchain_arg "-DCMAKE_TOOLCHAIN_FILE=${absolute_CMAKE_TOOLCHAIN_FILE}")
+ else()
+ # 'toolchain_arg' can't be empty
+ set(toolchain_arg "-DCMAKE_TOOLCHAIN_FILE=")
+ endif()
+
+ string(COMPARE EQUAL "${CMAKE_MAKE_PROGRAM}" "" no_make)
+ if(no_make)
+ set(make_arg "")
+ else()
+ # Test case: remove Ninja from PATH but set it via CMAKE_MAKE_PROGRAM
+ set(make_arg "-DCMAKE_MAKE_PROGRAM=${CMAKE_MAKE_PROGRAM}")
+ endif()
+
+ execute_process(
+ COMMAND
+ "${CMAKE_COMMAND}"
+ "-H${dir}"
+ "-B${build_dir}"
+ "-G${CMAKE_GENERATOR}"
+ "${toolchain_arg}"
+ ${make_arg}
+ WORKING_DIRECTORY "${dir}"
+ RESULT_VARIABLE download_result
+ ${logging_params}
+ )
+
+ if(NOT download_result EQUAL 0)
+ hunter_gate_internal_error(
+ "Configure project failed."
+ "To reproduce the error run: ${CMAKE_COMMAND} -H${dir} -B${build_dir} -G${CMAKE_GENERATOR} ${toolchain_arg} ${make_arg}"
+ "In directory ${dir}"
+ )
+ endif()
+
+ hunter_gate_status_print(
+ "Initializing Hunter workspace (${HUNTER_GATE_SHA1})"
+ " ${HUNTER_GATE_URL}"
+ " -> ${dir}"
+ )
+ execute_process(
+ COMMAND "${CMAKE_COMMAND}" --build "${build_dir}"
+ WORKING_DIRECTORY "${dir}"
+ RESULT_VARIABLE download_result
+ ${logging_params}
+ )
+
+ if(NOT download_result EQUAL 0)
+ hunter_gate_internal_error("Build project failed")
+ endif()
+
+ file(REMOVE_RECURSE "${build_dir}")
+ file(REMOVE_RECURSE "${cmakelists}")
+
+ file(WRITE "${sha1_location}" "${HUNTER_GATE_SHA1}")
+ file(WRITE "${done_location}" "DONE")
+
+ hunter_gate_status_debug("Finished")
+endfunction()
+
+# Must be a macro so master file 'cmake/Hunter' can
+# apply all variables easily just by 'include' command
+# (otherwise PARENT_SCOPE magic needed)
+macro(HunterGate)
+ if(HUNTER_GATE_DONE)
+ # variable HUNTER_GATE_DONE set explicitly for external project
+ # (see `hunter_download`)
+ set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES)
+ endif()
+
+ # First HunterGate command will init Hunter, others will be ignored
+ get_property(_hunter_gate_done GLOBAL PROPERTY HUNTER_GATE_DONE SET)
+
+ if(NOT HUNTER_ENABLED)
+ # Empty function to avoid error "unknown function"
+ function(hunter_add_package)
+ endfunction()
+
+ set(
+ _hunter_gate_disabled_mode_dir
+ "${CMAKE_CURRENT_LIST_DIR}/cmake/Hunter/disabled-mode"
+ )
+ if(EXISTS "${_hunter_gate_disabled_mode_dir}")
+ hunter_gate_status_debug(
+ "Adding \"disabled-mode\" modules: ${_hunter_gate_disabled_mode_dir}"
+ )
+ list(APPEND CMAKE_PREFIX_PATH "${_hunter_gate_disabled_mode_dir}")
+ endif()
+ elseif(_hunter_gate_done)
+ hunter_gate_status_debug("Secondary HunterGate (use old settings)")
+ hunter_gate_self(
+ "${HUNTER_CACHED_ROOT}"
+ "${HUNTER_VERSION}"
+ "${HUNTER_SHA1}"
+ _hunter_self
+ )
+ include("${_hunter_self}/cmake/Hunter")
+ else()
+ set(HUNTER_GATE_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}")
+
+ string(COMPARE NOTEQUAL "${PROJECT_NAME}" "" _have_project_name)
+ if(_have_project_name)
+ hunter_gate_fatal_error(
+ "Please set HunterGate *before* 'project' command. "
+ "Detected project: ${PROJECT_NAME}"
+ ERROR_PAGE "error.huntergate.before.project"
+ )
+ endif()
+
+ cmake_parse_arguments(
+ HUNTER_GATE "LOCAL" "URL;SHA1;GLOBAL;FILEPATH" "" ${ARGV}
+ )
+
+ string(COMPARE EQUAL "${HUNTER_GATE_SHA1}" "" _empty_sha1)
+ string(COMPARE EQUAL "${HUNTER_GATE_URL}" "" _empty_url)
+ string(
+ COMPARE
+ NOTEQUAL
+ "${HUNTER_GATE_UNPARSED_ARGUMENTS}"
+ ""
+ _have_unparsed
+ )
+ string(COMPARE NOTEQUAL "${HUNTER_GATE_GLOBAL}" "" _have_global)
+ string(COMPARE NOTEQUAL "${HUNTER_GATE_FILEPATH}" "" _have_filepath)
+
+ if(_have_unparsed)
+ hunter_gate_user_error(
+ "HunterGate unparsed arguments: ${HUNTER_GATE_UNPARSED_ARGUMENTS}"
+ )
+ endif()
+ if(_empty_sha1)
+ hunter_gate_user_error("SHA1 suboption of HunterGate is mandatory")
+ endif()
+ if(_empty_url)
+ hunter_gate_user_error("URL suboption of HunterGate is mandatory")
+ endif()
+ if(_have_global)
+ if(HUNTER_GATE_LOCAL)
+ hunter_gate_user_error("Unexpected LOCAL (already has GLOBAL)")
+ endif()
+ if(_have_filepath)
+ hunter_gate_user_error("Unexpected FILEPATH (already has GLOBAL)")
+ endif()
+ endif()
+ if(HUNTER_GATE_LOCAL)
+ if(_have_global)
+ hunter_gate_user_error("Unexpected GLOBAL (already has LOCAL)")
+ endif()
+ if(_have_filepath)
+ hunter_gate_user_error("Unexpected FILEPATH (already has LOCAL)")
+ endif()
+ endif()
+ if(_have_filepath)
+ if(_have_global)
+ hunter_gate_user_error("Unexpected GLOBAL (already has FILEPATH)")
+ endif()
+ if(HUNTER_GATE_LOCAL)
+ hunter_gate_user_error("Unexpected LOCAL (already has FILEPATH)")
+ endif()
+ endif()
+
+ hunter_gate_detect_root() # set HUNTER_GATE_ROOT
+
+ # Beautify path, fix probable problems with windows path slashes
+ get_filename_component(
+ HUNTER_GATE_ROOT "${HUNTER_GATE_ROOT}" ABSOLUTE
+ )
+ hunter_gate_status_debug("HUNTER_ROOT: ${HUNTER_GATE_ROOT}")
+ if(NOT HUNTER_ALLOW_SPACES_IN_PATH)
+ string(FIND "${HUNTER_GATE_ROOT}" " " _contain_spaces)
+ if(NOT _contain_spaces EQUAL -1)
+ hunter_gate_fatal_error(
+ "HUNTER_ROOT (${HUNTER_GATE_ROOT}) contains spaces."
+ "Set HUNTER_ALLOW_SPACES_IN_PATH=ON to skip this error"
+ "(Use at your own risk!)"
+ ERROR_PAGE "error.spaces.in.hunter.root"
+ )
+ endif()
+ endif()
+
+ string(
+ REGEX
+ MATCH
+ "[0-9]+\\.[0-9]+\\.[0-9]+[-_a-z0-9]*"
+ HUNTER_GATE_VERSION
+ "${HUNTER_GATE_URL}"
+ )
+ string(COMPARE EQUAL "${HUNTER_GATE_VERSION}" "" _is_empty)
+ if(_is_empty)
+ set(HUNTER_GATE_VERSION "unknown")
+ endif()
+
+ hunter_gate_self(
+ "${HUNTER_GATE_ROOT}"
+ "${HUNTER_GATE_VERSION}"
+ "${HUNTER_GATE_SHA1}"
+ _hunter_self
+ )
+
+ set(_master_location "${_hunter_self}/cmake/Hunter")
+ get_filename_component(_archive_id_location "${_hunter_self}/.." ABSOLUTE)
+ set(_done_location "${_archive_id_location}/DONE")
+ set(_sha1_location "${_archive_id_location}/SHA1")
+
+ # Check Hunter already downloaded by HunterGate
+ if(NOT EXISTS "${_done_location}")
+ hunter_gate_download("${_archive_id_location}")
+ endif()
+
+ if(NOT EXISTS "${_done_location}")
+ hunter_gate_internal_error("hunter_gate_download failed")
+ endif()
+
+ if(NOT EXISTS "${_sha1_location}")
+ hunter_gate_internal_error("${_sha1_location} not found")
+ endif()
+ file(READ "${_sha1_location}" _sha1_value)
+ string(COMPARE EQUAL "${_sha1_value}" "${HUNTER_GATE_SHA1}" _is_equal)
+ if(NOT _is_equal)
+ hunter_gate_internal_error(
+ "Short SHA1 collision:"
+ " ${_sha1_value} (from ${_sha1_location})"
+ " ${HUNTER_GATE_SHA1} (HunterGate)"
+ )
+ endif()
+ if(NOT EXISTS "${_master_location}")
+ hunter_gate_user_error(
+ "Master file not found:"
+ " ${_master_location}"
+ "try to update Hunter/HunterGate"
+ )
+ endif()
+ include("${_master_location}")
+ set_property(GLOBAL PROPERTY HUNTER_GATE_DONE YES)
+ endif()
+endmacro()
diff --git a/cmake/coverage.cmake b/cmake/coverage.cmake
new file mode 100644
index 0000000..c89cc16
--- /dev/null
+++ b/cmake/coverage.cmake
@@ -0,0 +1,33 @@
+# ---- Variables ----
+
+# We use variables separate from what CTest uses, because those have
+# customization issues
+set(
+ COVERAGE_TRACE_COMMAND
+ lcov -c -q
+ -o "${PROJECT_BINARY_DIR}/coverage.info"
+ -d "${PROJECT_BINARY_DIR}"
+ --include "${PROJECT_SOURCE_DIR}/*"
+ CACHE STRING
+ "; separated command to generate a trace for the 'coverage' target"
+)
+
+set(
+ COVERAGE_HTML_COMMAND
+ genhtml --legend -f -q
+ "${PROJECT_BINARY_DIR}/coverage.info"
+ -p "${PROJECT_SOURCE_DIR}"
+ -o "${PROJECT_BINARY_DIR}/coverage_html"
+ CACHE STRING
+ "; separated command to generate an HTML report for the 'coverage' target"
+)
+
+# ---- Coverage target ----
+
+add_custom_target(
+ coverage
+ COMMAND ${COVERAGE_TRACE_COMMAND}
+ COMMAND ${COVERAGE_HTML_COMMAND}
+ COMMENT "Generating coverage report"
+ VERBATIM
+)
diff --git a/cmake/dev-mode.cmake b/cmake/dev-mode.cmake
new file mode 100644
index 0000000..73a31fb
--- /dev/null
+++ b/cmake/dev-mode.cmake
@@ -0,0 +1,32 @@
+include(cmake/folders.cmake)
+
+include(CTest)
+if(BUILD_TESTING)
+ add_subdirectory(test)
+endif()
+
+add_custom_target(
+ run-exe
+ COMMAND lnav
+ VERBATIM
+)
+add_dependencies(run-exe lnav)
+
+option(BUILD_MCSS_DOCS "Build documentation using Doxygen and m.css" OFF)
+if(BUILD_MCSS_DOCS)
+ include(cmake/docs.cmake)
+endif()
+
+option(ENABLE_COVERAGE "Enable coverage support separate from CTest's" OFF)
+if(ENABLE_COVERAGE)
+ include(cmake/coverage.cmake)
+endif()
+
+if(CMAKE_HOST_SYSTEM_NAME STREQUAL "Windows")
+ include(cmake/open-cpp-coverage.cmake OPTIONAL)
+endif()
+
+include(cmake/lint-targets.cmake)
+include(cmake/spell-targets.cmake)
+
+add_folders(Project)
diff --git a/cmake/docs-ci.cmake b/cmake/docs-ci.cmake
new file mode 100644
index 0000000..ae7f0c7
--- /dev/null
+++ b/cmake/docs-ci.cmake
@@ -0,0 +1,112 @@
+cmake_minimum_required(VERSION 3.14)
+
+foreach(var IN ITEMS PROJECT_BINARY_DIR PROJECT_SOURCE_DIR)
+ if(NOT DEFINED "${var}")
+ message(FATAL_ERROR "${var} must be defined")
+ endif()
+endforeach()
+set(bin "${PROJECT_BINARY_DIR}")
+set(src "${PROJECT_SOURCE_DIR}")
+
+# ---- Dependencies ----
+
+set(mcss_SOURCE_DIR "${bin}/docs/.ci")
+if(NOT IS_DIRECTORY "${mcss_SOURCE_DIR}")
+ file(MAKE_DIRECTORY "${mcss_SOURCE_DIR}")
+ file(
+ DOWNLOAD
+ https://github.com/friendlyanon/m.css/releases/download/release-1/mcss.zip
+ "${mcss_SOURCE_DIR}/mcss.zip"
+ STATUS status
+ EXPECTED_MD5 00cd2757ebafb9bcba7f5d399b3bec7f
+ )
+ if(NOT status MATCHES "^0;")
+ message(FATAL_ERROR "Download failed with ${status}")
+ endif()
+ execute_process(
+ COMMAND "${CMAKE_COMMAND}" -E tar xf mcss.zip
+ WORKING_DIRECTORY "${mcss_SOURCE_DIR}"
+ RESULT_VARIABLE result
+ )
+ if(NOT result EQUAL "0")
+ message(FATAL_ERROR "Extraction failed with ${result}")
+ endif()
+ file(REMOVE "${mcss_SOURCE_DIR}/mcss.zip")
+endif()
+
+find_program(Python3_EXECUTABLE NAMES python3 python)
+if(NOT Python3_EXECUTABLE)
+ message(FATAL_ERROR "Python executable was not found")
+endif()
+
+# ---- Process project() call in CMakeLists.txt ----
+
+file(READ "${src}/CMakeLists.txt" content)
+
+string(FIND "${content}" "project(" index)
+if(index EQUAL "-1")
+ message(FATAL_ERROR "Could not find \"project(\"")
+endif()
+string(SUBSTRING "${content}" "${index}" -1 content)
+
+string(FIND "${content}" "\n)\n" index)
+if(index EQUAL "-1")
+ message(FATAL_ERROR "Could not find \"\\n)\\n\"")
+endif()
+string(SUBSTRING "${content}" 0 "${index}" content)
+
+file(WRITE "${bin}/docs-ci.project.cmake" "docs_${content}\n)\n")
+
+macro(list_pop_front list out)
+ list(GET "${list}" 0 "${out}")
+ list(REMOVE_AT "${list}" 0)
+endmacro()
+
+function(docs_project name)
+ cmake_parse_arguments(PARSE_ARGV 1 "" "" "VERSION;DESCRIPTION;HOMEPAGE_URL" LANGUAGES)
+ set(PROJECT_NAME "${name}" PARENT_SCOPE)
+ if(DEFINED _VERSION)
+ set(PROJECT_VERSION "${_VERSION}" PARENT_SCOPE)
+ string(REGEX MATCH "^[0-9]+(\\.[0-9]+)*" versions "${_VERSION}")
+ string(REPLACE . ";" versions "${versions}")
+ set(suffixes MAJOR MINOR PATCH TWEAK)
+ while(NOT versions STREQUAL "" AND NOT suffixes STREQUAL "")
+ list_pop_front(versions version)
+ list_pop_front(suffixes suffix)
+ set("PROJECT_VERSION_${suffix}" "${version}" PARENT_SCOPE)
+ endwhile()
+ endif()
+ if(DEFINED _DESCRIPTION)
+ set(PROJECT_DESCRIPTION "${_DESCRIPTION}" PARENT_SCOPE)
+ endif()
+ if(DEFINED _HOMEPAGE_URL)
+ set(PROJECT_HOMEPAGE_URL "${_HOMEPAGE_URL}" PARENT_SCOPE)
+ endif()
+endfunction()
+
+include("${bin}/docs-ci.project.cmake")
+
+# ---- Generate docs ----
+
+if(NOT DEFINED DOXYGEN_OUTPUT_DIRECTORY)
+ set(DOXYGEN_OUTPUT_DIRECTORY "${bin}/docs")
+endif()
+set(out "${DOXYGEN_OUTPUT_DIRECTORY}")
+
+foreach(file IN ITEMS Doxyfile conf.py)
+ configure_file("${src}/docs/${file}.in" "${bin}/docs/${file}" @ONLY)
+endforeach()
+
+set(mcss_script "${mcss_SOURCE_DIR}/documentation/doxygen.py")
+set(config "${bin}/docs/conf.py")
+
+file(REMOVE_RECURSE "${out}/html" "${out}/xml")
+
+execute_process(
+ COMMAND "${Python3_EXECUTABLE}" "${mcss_script}" "${config}"
+ WORKING_DIRECTORY "${bin}/docs"
+ RESULT_VARIABLE result
+)
+if(NOT result EQUAL "0")
+ message(FATAL_ERROR "m.css returned with ${result}")
+endif()
diff --git a/cmake/docs.cmake b/cmake/docs.cmake
new file mode 100644
index 0000000..286d027
--- /dev/null
+++ b/cmake/docs.cmake
@@ -0,0 +1,46 @@
+# ---- Dependencies ----
+
+set(extract_timestamps "")
+if (CMAKE_VERSION VERSION_GREATER_EQUAL "3.24")
+ set(extract_timestamps DOWNLOAD_EXTRACT_TIMESTAMP YES)
+endif ()
+
+include(FetchContent)
+FetchContent_Declare(
+ mcss URL
+ https://github.com/friendlyanon/m.css/releases/download/release-1/mcss.zip
+ URL_MD5 00cd2757ebafb9bcba7f5d399b3bec7f
+ SOURCE_DIR "${PROJECT_BINARY_DIR}/mcss"
+ UPDATE_DISCONNECTED YES
+ ${extract_timestamps}
+)
+FetchContent_MakeAvailable(mcss)
+
+find_package(Python3 3.6 REQUIRED)
+
+# ---- Declare documentation target ----
+
+set(
+ DOXYGEN_OUTPUT_DIRECTORY "${PROJECT_BINARY_DIR}/docs"
+ CACHE PATH "Path for the generated Doxygen documentation"
+)
+
+set(working_dir "${PROJECT_BINARY_DIR}/docs")
+
+foreach (file IN ITEMS Doxyfile conf.py)
+ configure_file("docs/${file}.in" "${working_dir}/${file}" @ONLY)
+endforeach ()
+
+set(mcss_script "${mcss_SOURCE_DIR}/documentation/doxygen.py")
+set(config "${working_dir}/conf.py")
+
+add_custom_target(
+ docs
+ COMMAND "${CMAKE_COMMAND}" -E remove_directory
+ "${DOXYGEN_OUTPUT_DIRECTORY}/html"
+ "${DOXYGEN_OUTPUT_DIRECTORY}/xml"
+ COMMAND "${Python3_EXECUTABLE}" "${mcss_script}" "${config}"
+ COMMENT "Building documentation using Doxygen and m.css"
+ WORKING_DIRECTORY "${working_dir}"
+ VERBATIM
+)
diff --git a/cmake/folders.cmake b/cmake/folders.cmake
new file mode 100644
index 0000000..da7bd33
--- /dev/null
+++ b/cmake/folders.cmake
@@ -0,0 +1,21 @@
+set_property(GLOBAL PROPERTY USE_FOLDERS YES)
+
+# Call this function at the end of a directory scope to assign a folder to
+# targets created in that directory. Utility targets will be assigned to the
+# UtilityTargets folder, otherwise to the ${name}Targets folder. If a target
+# already has a folder assigned, then that target will be skipped.
+function(add_folders name)
+ get_property(targets DIRECTORY PROPERTY BUILDSYSTEM_TARGETS)
+ foreach(target IN LISTS targets)
+ get_property(folder TARGET "${target}" PROPERTY FOLDER)
+ if(DEFINED folder)
+ continue()
+ endif()
+ set(folder Utility)
+ get_property(type TARGET "${target}" PROPERTY TYPE)
+ if(NOT type STREQUAL "UTILITY")
+ set(folder "${name}")
+ endif()
+ set_property(TARGET "${target}" PROPERTY FOLDER "${folder}Targets")
+ endforeach()
+endfunction()
diff --git a/cmake/install-rules.cmake b/cmake/install-rules.cmake
new file mode 100644
index 0000000..ed43e76
--- /dev/null
+++ b/cmake/install-rules.cmake
@@ -0,0 +1,44 @@
+include(CMakePackageConfigHelpers)
+include(GNUInstallDirs)
+
+# find_package(<package>) call for consumers to find this project
+set(package lnav)
+
+install(
+ TARGETS lnav
+ RUNTIME COMPONENT lnav_Runtime
+)
+
+write_basic_package_version_file(
+ "${package}ConfigVersion.cmake"
+ COMPATIBILITY SameMajorVersion
+)
+
+# Allow package maintainers to freely override the path for the configs
+set(
+ lnav_INSTALL_CMAKEDIR "${CMAKE_INSTALL_DATADIR}/${package}"
+ CACHE PATH "CMake package config location relative to the install prefix"
+)
+mark_as_advanced(lnav_INSTALL_CMAKEDIR)
+
+install(
+ FILES "${PROJECT_BINARY_DIR}/${package}ConfigVersion.cmake"
+ DESTINATION "${lnav_INSTALL_CMAKEDIR}"
+ COMPONENT lnav_Development
+)
+
+# Export variables for the install script to use
+install(CODE "
+set(lnav_NAME [[$<TARGET_FILE_NAME:lnav>]])
+set(lnav_INSTALL_CMAKEDIR [[${lnav_INSTALL_CMAKEDIR}]])
+set(CMAKE_INSTALL_BINDIR [[${CMAKE_INSTALL_BINDIR}]])
+" COMPONENT lnav_Development)
+
+install(
+ SCRIPT cmake/install-script.cmake
+ COMPONENT lnav_Development
+)
+
+if(PROJECT_IS_TOP_LEVEL)
+ include(CPack)
+endif()
diff --git a/cmake/install-script.cmake b/cmake/install-script.cmake
new file mode 100644
index 0000000..6bd0c6e
--- /dev/null
+++ b/cmake/install-script.cmake
@@ -0,0 +1,18 @@
+file(
+ RELATIVE_PATH relative_path
+ "/${lnav_INSTALL_CMAKEDIR}"
+ "/${CMAKE_INSTALL_BINDIR}/${lnav_NAME}"
+)
+
+get_filename_component(prefix "${CMAKE_INSTALL_PREFIX}" ABSOLUTE)
+set(config_dir "${prefix}/${lnav_INSTALL_CMAKEDIR}")
+set(config_file "${config_dir}/lnavConfig.cmake")
+
+message(STATUS "Installing: ${config_file}")
+file(WRITE "${config_file}" "\
+set(
+ LNAV_EXECUTABLE
+ \"\${CMAKE_CURRENT_LIST_DIR}/${relative_path}\"
+ CACHE FILEPATH \"Path to the lnav executable\"
+)
+")
diff --git a/cmake/lint-targets.cmake b/cmake/lint-targets.cmake
new file mode 100644
index 0000000..1ffd8c0
--- /dev/null
+++ b/cmake/lint-targets.cmake
@@ -0,0 +1,32 @@
+set(
+ FORMAT_PATTERNS
+ src/*.cc src/*.hh
+ test/*.cc test/*.hh
+ CACHE STRING
+ "; separated patterns relative to the project source dir to format"
+)
+
+set(FORMAT_COMMAND clang-format CACHE STRING "Formatter to use")
+
+add_custom_target(
+ format-check
+ COMMAND "${CMAKE_COMMAND}"
+ -D "FORMAT_COMMAND=${FORMAT_COMMAND}"
+ -D "PATTERNS=${FORMAT_PATTERNS}"
+ -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake"
+ WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
+ COMMENT "Linting the code"
+ VERBATIM
+)
+
+add_custom_target(
+ format-fix
+ COMMAND "${CMAKE_COMMAND}"
+ -D "FORMAT_COMMAND=${FORMAT_COMMAND}"
+ -D "PATTERNS=${FORMAT_PATTERNS}"
+ -D FIX=YES
+ -P "${PROJECT_SOURCE_DIR}/cmake/lint.cmake"
+ WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
+ COMMENT "Fixing the code"
+ VERBATIM
+)
diff --git a/cmake/lint.cmake b/cmake/lint.cmake
new file mode 100644
index 0000000..32bfdc4
--- /dev/null
+++ b/cmake/lint.cmake
@@ -0,0 +1,50 @@
+cmake_minimum_required(VERSION 3.14)
+
+macro(default name)
+ if(NOT DEFINED "${name}")
+ set("${name}" "${ARGN}")
+ endif()
+endmacro()
+
+default(FORMAT_COMMAND clang-format)
+default(
+ PATTERNS
+ src/*.cc src/*.hh
+ test/*.cc test/*.hh
+)
+default(FIX NO)
+
+set(flag --output-replacements-xml)
+set(args OUTPUT_VARIABLE output)
+if(FIX)
+ set(flag -i)
+ set(args "")
+endif()
+
+file(GLOB_RECURSE files ${PATTERNS})
+set(badly_formatted "")
+set(output "")
+string(LENGTH "${CMAKE_SOURCE_DIR}/" path_prefix_length)
+
+foreach(file IN LISTS files)
+ execute_process(
+ COMMAND "${FORMAT_COMMAND}" --style=file "${flag}" "${file}"
+ WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+ RESULT_VARIABLE result
+ ${args}
+ )
+ if(NOT result EQUAL "0")
+ message(FATAL_ERROR "'${file}': formatter returned with ${result}")
+ endif()
+ if(NOT FIX AND output MATCHES "\n<replacement offset")
+ string(SUBSTRING "${file}" "${path_prefix_length}" -1 relative_file)
+ list(APPEND badly_formatted "${relative_file}")
+ endif()
+ set(output "")
+endforeach()
+
+if(NOT badly_formatted STREQUAL "")
+ list(JOIN badly_formatted "\n" bad_list)
+ message("The following files are badly formatted:\n\n${bad_list}\n")
+ message(FATAL_ERROR "Run again with FIX=YES to fix these files.")
+endif()
diff --git a/cmake/open-cpp-coverage.cmake.example b/cmake/open-cpp-coverage.cmake.example
new file mode 100644
index 0000000..b00f086
--- /dev/null
+++ b/cmake/open-cpp-coverage.cmake.example
@@ -0,0 +1,31 @@
+# Example file to run OpenCppCoverage on Windows
+
+include(ProcessorCount)
+ProcessorCount(N)
+
+file(MAKE_DIRECTORY "${PROJECT_BINARY_DIR}/opencppcoverage")
+
+# Convert delimiters to Windows ones
+string(REPLACE "/" "\\" binary_dir "${PROJECT_BINARY_DIR}")
+string(REPLACE "/" "\\" source_dir "${PROJECT_SOURCE_DIR}")
+string(REPLACE "/" "\\" ctest "${CMAKE_CTEST_COMMAND}")
+
+add_custom_target(
+ win-cov
+ COMMAND OpenCppCoverage -q
+ # We want coverage from the child processes of CTest
+ --cover_children
+ # Subdirectory where the tests reside in the binary directory
+ --modules "${binary_dir}\\test"
+ # This command is for the developer, so export as html instead of cobertura
+ --export_type "html:${binary_dir}\\opencppcoverage"
+ # Source (not header) file locations
+ --sources "${source_dir}\\source"
+ --sources "${source_dir}\\test\\source"
+ # Working directory for CTest, which should be the binary directory
+ --working_dir "${binary_dir}"
+ # OpenCppCoverage should be run only with the Debug configuration tests
+ -- "${ctest}" -C Debug --output-on-failure -j "${N}"
+ WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
+ VERBATIM
+)
diff --git a/cmake/prelude.cmake b/cmake/prelude.cmake
new file mode 100644
index 0000000..c37d590
--- /dev/null
+++ b/cmake/prelude.cmake
@@ -0,0 +1,10 @@
+# ---- In-source guard ----
+
+if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
+ message(
+ FATAL_ERROR
+ "In-source builds are not supported. "
+ "Please read the BUILDING document before trying to build this project. "
+ "You may need to delete 'CMakeCache.txt' and 'CMakeFiles/' first."
+ )
+endif()
diff --git a/cmake/project-is-top-level.cmake b/cmake/project-is-top-level.cmake
new file mode 100644
index 0000000..3435fc0
--- /dev/null
+++ b/cmake/project-is-top-level.cmake
@@ -0,0 +1,6 @@
+# This variable is set by project() in CMake 3.21+
+string(
+ COMPARE EQUAL
+ "${CMAKE_SOURCE_DIR}" "${PROJECT_SOURCE_DIR}"
+ PROJECT_IS_TOP_LEVEL
+)
diff --git a/cmake/spell-targets.cmake b/cmake/spell-targets.cmake
new file mode 100644
index 0000000..0c21cab
--- /dev/null
+++ b/cmake/spell-targets.cmake
@@ -0,0 +1,22 @@
+set(SPELL_COMMAND codespell CACHE STRING "Spell checker to use")
+
+add_custom_target(
+ spell-check
+ COMMAND "${CMAKE_COMMAND}"
+ -D "SPELL_COMMAND=${SPELL_COMMAND}"
+ -P "${PROJECT_SOURCE_DIR}/cmake/spell.cmake"
+ WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
+ COMMENT "Checking spelling"
+ VERBATIM
+)
+
+add_custom_target(
+ spell-fix
+ COMMAND "${CMAKE_COMMAND}"
+ -D "SPELL_COMMAND=${SPELL_COMMAND}"
+ -D FIX=YES
+ -P "${PROJECT_SOURCE_DIR}/cmake/spell.cmake"
+ WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}"
+ COMMENT "Fixing spelling errors"
+ VERBATIM
+)
diff --git a/cmake/spell.cmake b/cmake/spell.cmake
new file mode 100644
index 0000000..e05ecd7
--- /dev/null
+++ b/cmake/spell.cmake
@@ -0,0 +1,29 @@
+cmake_minimum_required(VERSION 3.14)
+
+macro(default name)
+ if(NOT DEFINED "${name}")
+ set("${name}" "${ARGN}")
+ endif()
+endmacro()
+
+default(SPELL_COMMAND codespell)
+default(FIX NO)
+
+set(flag "")
+if(FIX)
+ set(flag -w)
+endif()
+
+execute_process(
+ COMMAND "${SPELL_COMMAND}" ${flag}
+ WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}"
+ RESULT_VARIABLE result
+)
+
+if(result EQUAL "65")
+ message(FATAL_ERROR "Run again with FIX=YES to fix these errors.")
+elseif(result EQUAL "64")
+ message(FATAL_ERROR "Spell checker printed the usage info. Bad arguments?")
+elseif(NOT result EQUAL "0")
+ message(FATAL_ERROR "Spell checker returned with ${result}")
+endif()
diff --git a/cmake/variables.cmake b/cmake/variables.cmake
new file mode 100644
index 0000000..d4497e7
--- /dev/null
+++ b/cmake/variables.cmake
@@ -0,0 +1,28 @@
+# ---- Developer mode ----
+
+# Developer mode enables targets and code paths in the CMake scripts that are
+# only relevant for the developer(s) of lnav
+# Targets necessary to build the project must be provided unconditionally, so
+# consumers can trivially build and package the project
+if(PROJECT_IS_TOP_LEVEL)
+ option(lnav_DEVELOPER_MODE "Enable developer mode" OFF)
+endif()
+
+# ---- Warning guard ----
+
+# target_include_directories with the SYSTEM modifier will request the compiler
+# to omit warnings from the provided paths, if the compiler supports that
+# This is to provide a user experience similar to find_package when
+# add_subdirectory or FetchContent is used to consume this project
+set(warning_guard "")
+if(NOT PROJECT_IS_TOP_LEVEL)
+ option(
+ lnav_INCLUDES_WITH_SYSTEM
+ "Use SYSTEM modifier for lnav's includes, disabling warnings"
+ ON
+ )
+ mark_as_advanced(lnav_INCLUDES_WITH_SYSTEM)
+ if(lnav_INCLUDES_WITH_SYSTEM)
+ set(warning_guard SYSTEM)
+ endif()
+endif()
diff --git a/conanfile.py b/conanfile.py
new file mode 100644
index 0000000..b9fa5c4
--- /dev/null
+++ b/conanfile.py
@@ -0,0 +1,58 @@
+from conans import ConanFile
+from conan.tools.cmake import CMake, CMakeToolchain, CMakeDeps
+
+
+class LnavConan(ConanFile):
+ name = "lnav"
+ version = "0.11.2"
+ homepage = "https://lnav.org"
+ url = "https://github.com/tstack/lnav.git"
+ license = "BSD-2-Clause"
+ description = (
+ "The Log File Navigator, lnav for short, is an advanced "
+ "log file viewer for the small-scale"
+ )
+ settings = "os", "compiler", "build_type", "arch"
+ exports_sources = "*"
+ no_copy_source = True
+ requires = (
+ "bzip2/1.0.8",
+ "libarchive/3.6.0",
+ "libcurl/7.85.0",
+ "ncurses/6.3",
+ "pcre2/10.40",
+ "readline/8.1.2",
+ "sqlite3/3.38.0",
+ "zlib/1.2.12",
+ )
+ generators = ("virtualrunenv",)
+ default_options = {
+ "libarchive:with_bzip2": True,
+ "libarchive:with_lz4": True,
+ "libarchive:with_lzo": True,
+ "libarchive:with_lzma": True,
+ "libarchive:with_zstd": True,
+ "pcre2:support_jit": True,
+ "pcre2:build_pcre2_8": True,
+ "sqlite3:enable_json1": True,
+ "sqlite3:enable_soundex": True,
+ "readline:with_library": "curses",
+ }
+
+ def generate(self):
+ CMakeToolchain(self).generate()
+ CMakeDeps(self).generate()
+
+ def build(self):
+ cmake = CMake(self)
+ cmake.configure()
+ if self.settings.os == "Macos" and self.settings.arch == "armv8":
+ cmake.definitions["CMAKE_SYSTEM_PROCESSOR"] = "arm64"
+ cmake.build()
+
+ def package(self):
+ cmake = CMake(self)
+ cmake.install()
+
+ def deploy(self):
+ self.copy("*", dst="bin", src="bin")
diff --git a/configure.ac b/configure.ac
new file mode 100644
index 0000000..abc8015
--- /dev/null
+++ b/configure.ac
@@ -0,0 +1,331 @@
+AC_INIT([lnav],[0.11.2],[lnav@googlegroups.com],[lnav],[http://lnav.org])
+AC_CONFIG_SRCDIR([src/lnav.cc])
+AC_CONFIG_MACRO_DIR([m4])
+AM_INIT_AUTOMAKE([foreign subdir-objects])
+AM_SILENT_RULES([yes])
+
+AC_PREFIX_DEFAULT(/usr)
+
+AC_CANONICAL_HOST
+AX_PROG_CC_FOR_BUILD
+
+AX_PTHREAD()
+LIBS="$PTHREAD_LIBS $LIBS"
+CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+CC="$PTHREAD_CC"
+
+AC_LANG(C++)
+AX_CXX_COMPILE_STDCXX_14([noext], [mandatory])
+
+dnl abssrcdir is the absolute path to the source base (regardless of where
+dnl you are building it)
+AS_CASE([x$srcdir],
+ [x/*],
+ AS_VAR_SET(abssrcdir, $srcdir),
+ AS_VAR_SET(abssrcdir, `cd $srcdir; pwd`)
+)
+
+AC_SUBST(abssrcdir)
+
+AC_PROG_CXX
+
+CPPFLAGS="$CPPFLAGS -D_ISOC99_SOURCE -D__STDC_LIMIT_MACROS -D_GNU_SOURCE"
+
+AC_ARG_ENABLE([debug],
+ AS_HELP_STRING([--enable-debug],
+ [Compile with symbols]))
+
+AS_VAR_IF([enable_profiling], [yes],
+ [CFLAGS=`echo $CFLAGS | sed 's/-O2//g'`
+ CXXFLAGS=`echo $CXXFLAGS | sed 's/-O2//g'`],
+ [enable_debug=no]dnl
+)
+
+AC_ARG_VAR(SFTP_TEST_URL)
+
+AC_PROG_INSTALL
+AC_PROG_RANLIB
+AM_PROG_AR
+AC_PROG_LN_S
+AC_PROG_MAKE_SET
+
+AC_PATH_PROG(BZIP2_CMD, [bzip2])
+AC_PATH_PROG(RE2C_CMD, [re2c])
+AM_CONDITIONAL(HAVE_RE2C, test x"$RE2C_CMD" != x"")
+AC_PATH_PROG(XZ_CMD, [xz])
+AC_PATH_PROG(TSHARK_CMD, [tshark])
+
+AC_CHECK_SIZEOF(off_t)
+AC_CHECK_SIZEOF(size_t)
+
+AC_STRUCT_TIMEZONE
+
+AC_ARG_ENABLE([static],
+ AS_HELP_STRING([--enable-static],
+ [Enable static linking]))
+
+AC_SEARCH_LIBS(openpty, util)
+AC_SEARCH_LIBS(gzseek, z, [], [AC_MSG_ERROR([libz required to build])])
+AC_SEARCH_LIBS(BZ2_bzopen, bz2,
+ AS_VAR_SET(BZIP2_SUPPORT, 1),
+ AS_VAR_SET(BZIP2_SUPPORT, 0))
+AC_SUBST(BZIP2_SUPPORT)
+AC_SEARCH_LIBS(dlopen, dl)
+AC_SEARCH_LIBS(backtrace, execinfo)
+LIBCURL_CHECK_CONFIG([], [7.23.0], [], [AC_MSG_ERROR([libcurl required to build])], [test x"${enable_static}" = x"yes"])
+
+# Sometimes, curses depends on these libraries being linked in...
+AC_ARG_ENABLE([tinfo],
+ AS_HELP_STRING([--disable-tinfo],
+ [Disable linking with tinfo, enabled by default]),
+ [], [enable_tinfo="yes"]
+)
+
+AS_IF([test "x${enable_tinfo}" != "xno"],
+ [dnl
+ AC_MSG_NOTICE([Trying to link with tinfo])
+ AC_SEARCH_LIBS(cur_term, [tinfow tinfo],
+ AC_MSG_NOTICE([Linking with tinfo]),
+ AC_MSG_WARN([libtinfo not found])
+ )
+ ],
+ AC_MSG_NOTICE([Building with tinfo linking disabled])
+)
+
+dnl libgpm is required on some systems where there is a misconfigured ncurses
+dnl and gpm libraries with interdependencies. This check is not required on OS X.
+AS_CASE(["$host_os"],
+ [darwin*],
+ [],
+ AC_SEARCH_LIBS(Gpm_Open, [gpm :libgpm.so.2],
+ AS_VAR_SET(HAVE_GPM, "1"),
+ AC_MSG_WARN(m4_join([ ],
+ [libgpm not found. If build fails later],
+ [consider installing gpm dev package])dnl
+ )
+ )
+)
+
+AC_CHECK_HEADERS(execinfo.h pty.h util.h zlib.h bzlib.h libutil.h sys/ttydefaults.h)
+
+dnl Experimental SIMD features.
+AC_ARG_ENABLE([simd],
+ AS_HELP_STRING([--enable-simd], [Try and enable simd optimizations]),
+ [
+ AS_CASE(["$enable_simd"],
+ [no], [enable_simd="no"],
+ [yes | ""],[enable_simd="yes"])
+ ],
+ []
+)
+
+AS_IF([test "x$enable_simd" = "xyes"], [
+ AC_CHECK_HEADERS(x86intrin.h)
+])
+
+
+AC_ARG_WITH([system_doctest],
+ AS_HELP_STRING(
+ [--with-system-doctest],
+ [Use the system provided doctest library rather than the bundled one]
+ ),
+ [], []
+)
+
+AS_IF([test "x$with_system_doctest" = "xyes"], [
+ AC_CHECK_HEADERS(doctest/doctest.h)
+ AS_IF([test "x$ac_cv_header_doctest_doctest_h" != "xyes"], [
+ AC_MSG_ERROR([system doctest not found])dnl
+ ])
+])
+AS_IF([test "x$with_system_doctest" != "xyes"], [
+ CPPFLAGS="-I\$(top_srcdir)/src/third-party/doctest-root $CPPFLAGS"
+])
+
+
+LNAV_WITH_JEMALLOC
+
+LNAV_WITH_LOCAL_YAJL
+
+AX_WITH_CURSES
+
+AS_VAR_IF([ax_cv_curses],[yes],[],
+ AC_MSG_ERROR([requires an X/Open-compatible Curses library with color])dnl
+)
+
+AX_PATH_LIB_ARCHIVE
+AX_CHECK_PCRE2([8], [], [AC_MSG_ERROR([pcre2 is required to build])])
+AX_PATH_LIB_READLINE
+
+AX_CODE_COVERAGE
+
+LNAV_WITH_SQLITE3("3.9.0")
+
+AC_ARG_ENABLE(
+ [system-paths],
+ AS_HELP_STRING([--disable-system-paths],
+ [Add extra system paths]),,[enable_system_paths=yes])
+
+if test x"${enable_system_paths}" != x"no"; then
+ for defdir in /opt/local /usr/local; do
+ if test -d "$defdir/include"; then
+ echo "Adding include path: $defdir/include"
+ CPPFLAGS="$CPPFLAGS -I$defdir/include"
+ fi
+ done
+
+ for defdir in /opt/local /usr/local /usr /; do
+
+ if test -d "$defdir/lib64"; then
+ LDFLAGS="$LDFLAGS -L$defdir/lib64"
+ fi
+ if test -d "$defdir/lib"; then
+ LDFLAGS="$LDFLAGS -L$defdir/lib"
+ fi
+
+ if test -d "$defdir/lib/x86_64-linux-gnu"; then
+ LDFLAGS="$LDFLAGS -L$defdir/lib/x86_64-linux-gnu"
+ fi
+ done
+else
+ echo "Not including extra system paths"
+fi
+
+dnl case "$host_os" in
+dnl *)
+dnl # AC_DEFINE([_XOPEN_SOURCE], [500], [Need pread])
+dnl AC_DEFINE([_DEFAULT_SOURCE], [1], [Need pread])
+dnl AC_DEFINE([_BSD_SOURCE], [1], [Need pread])
+dnl ;;
+dnl esac
+
+AC_DEFINE([_XOPEN_SOURCE_EXTENDED], [1], [Wide character support for ncurses])
+
+AS_VAR_SET(ALL_LDFLAGS, "$SQLITE3_LDFLAGS $READLINE_LDFLAGS $LIBARCHIVE_LDFLAGS $LIBCURL $LDFLAGS")
+
+AS_VAR_SET(static_lib_list,
+ ["libncurses.a libncursesw.a libreadline.a libsqlite3.a libz.a libtinfo.a libtinfow.a"])
+AS_VAR_SET(static_lib_list,
+ ["$static_lib_list libpcre2-8.a libncursesw.a libbz2.a"])
+AS_VAR_SET(static_lib_list,
+ ["$static_lib_list libgpm.a libcurl.a libcrypto.a libssl.a libssh2.a"])
+AS_VAR_SET(static_lib_list,
+ ["$static_lib_list libnghttp2.a liblzma.a libcrypto.a libzstd.a libldap.a"])
+AS_VAR_SET(static_lib_list,
+ ["$static_lib_list libarchive.a libidn2.a libgssapi_krb5.a libbrotlidec-static.a"])
+AS_VAR_SET(static_lib_list,
+ ["$static_lib_list librtmp.a libiconv.a liblz4.a liblber.a libunistring.a"])
+
+if test x"${enable_static}" = x"yes"; then
+ case "$host_os" in
+ darwin*)
+ STATIC_LDFLAGS="$STATIC_LDFLAGS -Wl,-search_paths_first"
+ ;;
+ esac
+
+ AX_CHECK_LINK_FLAG([-static-libgcc], [STATIC_LDFLAGS="$STATIC_LDFLAGS -static-libgcc"])
+
+ STATIC_LDFLAGS="$STATIC_LDFLAGS -L`pwd`/src/static-libs"
+ AX_CHECK_LINK_FLAG([-static-libstdc++],
+ [STATIC_LDFLAGS="$STATIC_LDFLAGS -static-libstdc++"])
+ # This is a hack to link against static libraries instead of shared
+ # so that we can build a mostly statically link exe that can
+ # be downloaded and used right away. This is required for OS X and
+ # will, hopefully, make a static binary that is compatible with
+ # many different versions of Linux.
+ AS_MKDIR_P(src/static-libs)
+ rm -f src/static-libs/*.a
+ for libflag in $ALL_LDFLAGS; do
+ case $libflag in
+ -Lstatic-libs)
+ ;;
+ -L*)
+ libdir=`echo $libflag | sed -e 's/-L//'`
+ for slib in $static_lib_list; do
+ if test -e "$libdir/$slib" -a ! -e "src/static-libs/$slib"; then
+ ln -sf "$libdir/$slib" src/static-libs/.
+ fi
+ done
+ ;;
+ esac
+ done
+
+ for slib in $static_lib_list; do
+ AC_MSG_CHECKING(for static library $slib)
+ if test -e "src/static-libs/$slib"; then
+ found_slib=`readlink src/static-libs/$slib`
+ AC_MSG_RESULT($found_slib)
+ else
+ AC_MSG_RESULT(not found)
+ fi
+ done
+fi
+AC_SUBST(STATIC_LDFLAGS)
+
+AS_CASE(["$host_os"],
+ [darwin*],
+ [],
+ [
+ curses_lib=$(echo $CURSES_LIB | sed -e 's/-l//')
+ AS_IF([test $? -eq 0],
+ [
+ AS_CASE(["$curses_lib"],
+ [ncurses*],
+ [AS_VAR_SET_IF(HAVE_GPM, [],
+ [
+ AC_MSG_NOTICE([Checking for libgpm dependency])
+ AS_VAR_SET(saved_LDFLAGS, $LDFLAGS)
+ AS_VAR_SET(LDFLAGS, "$STATIC_LDFLAGS $LDFLAGS")
+ AS_VAR_SET(saved_LIBS, $LIBS)
+ AC_CHECK_LIB($curses_lib, mousemask,
+ [
+ AS_VAR_SET(LDFLAGS, $saved_LDFLAGS)
+ AS_VAR_SET(LIBS, $saved_LIBS)
+ ],
+ AC_MSG_ERROR([libgpm development libraries are required to build]))dnl
+ ])dnl
+ ])
+ ],
+ [
+ AC_MSG_WARN([Unable to test for dependepncy on gpm.])
+ AC_MSG_WARN([If build fails during make consider installing libgpm development libraries.])
+ ])dnl
+ ]dnl
+)
+
+saved_location=$(pwd)
+cd $srcdir
+version=$(expr $(git describe --abbrev=7 --dirty --always --tags 2>/dev/null) : 'v\([[0-9]]*\.[[0-9]]*\.[[0-9]]*.*\)' 2>/dev/null)
+cd $saved_location
+AS_IF([test $? -eq 0],
+ [version=$(echo $version | tr -d '\n')
+ version=${version:-${PACKAGE_VERSION}}
+ AC_DEFINE_UNQUOTED([VCS_PACKAGE_STRING], ["$PACKAGE_NAME $version"],
+ [VCS package string])],
+ AC_DEFINE_UNQUOTED([VCS_PACKAGE_STRING], ["$PACKAGE_STRING"], [VCS package string]))
+
+AM_CONDITIONAL(USE_INCLUDED_YAJL, test $HAVE_LOCAL_YAJL -eq 0)
+AM_CONDITIONAL(HAVE_LIBCURL, test x"$LIBCURL" != x"")
+AM_CONDITIONAL([DISABLE_DOCUMENTATION], [ test x"$cross_compiling" != x"no" ])
+
+USER_CXXFLAGS="${CXXFLAGS}"
+AC_SUBST(USER_CXXFLAGS)
+
+AC_CONFIG_HEADERS([src/config.h])
+AC_CONFIG_FILES([Makefile])
+AC_CONFIG_FILES([TESTS_ENVIRONMENT])
+AC_CONFIG_FILES([tools/Makefile])
+AC_CONFIG_FILES([src/Makefile])
+AC_CONFIG_FILES([src/base/Makefile])
+AC_CONFIG_FILES([src/formats/logfmt/Makefile])
+AC_CONFIG_FILES([src/fmtlib/Makefile])
+AC_CONFIG_FILES([src/pcrepp/Makefile])
+AC_CONFIG_FILES([src/pugixml/Makefile])
+AC_CONFIG_FILES([src/tailer/Makefile])
+AC_CONFIG_FILES([src/yajl/Makefile])
+AC_CONFIG_FILES([src/yajlpp/Makefile])
+AC_CONFIG_FILES([src/third-party/base64/lib/Makefile])
+AC_CONFIG_FILES([src/third-party/scnlib/src/Makefile])
+AC_CONFIG_FILES([test/Makefile])
+
+AC_OUTPUT
diff --git a/demo/Dockerfile b/demo/Dockerfile
new file mode 100644
index 0000000..25ded84
--- /dev/null
+++ b/demo/Dockerfile
@@ -0,0 +1,44 @@
+
+FROM debian:11.3-slim
+
+RUN set -eux; \
+ export DEBIAN_FRONTEND=noninteractive; \
+ apt update; \
+ apt install --yes --no-install-recommends bind9-dnsutils iputils-ping iproute2 curl ca-certificates htop wget unzip openssh-server; \
+ apt clean autoclean; \
+ apt autoremove --yes; \
+ rm -rf /var/lib/{apt,dpkg,cache,log}/; \
+ echo "Installed base utils!"
+
+ADD https://github.com/tstack/lnav/releases/download/v0.11.0/lnav-0.11.0-musl-64bit.zip /
+RUN unzip lnav-0.11.0-musl-64bit.zip
+
+COPY docs/tutorials tutorials
+RUN gunzip /tutorials/playground/logs/*.gz
+
+RUN useradd -rm -d /home/playground -s /bin/bash playground
+RUN echo 'playground:playground' | chpasswd
+RUN passwd -d playground
+
+RUN useradd -rm -d /home/tutorial1 -s /bin/bash tutorial1
+RUN echo 'tutorial1:tutorial1' | chpasswd
+RUN passwd -d tutorial1
+
+USER playground
+RUN /lnav-0.11.0/lnav -nN -c ":config /ui/theme monocai"
+
+USER tutorial1
+RUN /lnav-0.11.0/lnav -nN -c ":config /ui/theme monocai"
+
+USER root
+
+RUN echo 'Match User playground' >> /etc/ssh/sshd_config
+RUN echo 'ForceCommand env PATH=/lnav-0.11.0:$PATH /tutorials/playground/run.sh' >> /etc/ssh/sshd_config
+RUN echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_config
+RUN echo 'Match User tutorial1' >> /etc/ssh/sshd_config
+RUN echo 'ForceCommand env PATH=/lnav-0.11.0:$PATH /tutorials/tutorial1/run.sh' >> /etc/ssh/sshd_config
+RUN echo 'PermitEmptyPasswords yes' >> /etc/ssh/sshd_config
+RUN service ssh start
+EXPOSE 22
+
+CMD ["/usr/sbin/sshd", "-D"]
diff --git a/demo/fly.toml b/demo/fly.toml
new file mode 100644
index 0000000..75affba
--- /dev/null
+++ b/demo/fly.toml
@@ -0,0 +1,35 @@
+# fly.toml file generated for lnav-demo on 2022-08-22T10:26:28-07:00
+
+app = "lnav-demo"
+kill_signal = "SIGINT"
+kill_timeout = 5
+processes = []
+
+[env]
+
+[experimental]
+ allowed_public_ports = []
+ auto_rollback = true
+
+[[services]]
+ http_checks = []
+ internal_port = 22
+ processes = ["app"]
+ protocol = "tcp"
+ script_checks = []
+ [services.concurrency]
+ hard_limit = 25
+ soft_limit = 20
+ type = "connections"
+
+ [[services.ports]]
+ port = 22
+
+ [[services.tcp_checks]]
+ grace_period = "1s"
+ interval = "15s"
+ restart_limit = 0
+ timeout = "2s"
+
+[build]
+image = "lnav-demo"
diff --git a/demo/loggen.py b/demo/loggen.py
new file mode 100755
index 0000000..6790a09
--- /dev/null
+++ b/demo/loggen.py
@@ -0,0 +1,54 @@
+import datetime
+import os
+import random
+import shutil
+import sys
+
+MSGS = [
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
+ "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
+ "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.",
+ "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.",
+]
+
+GLOG_DATE_FMT = "%Y%m%d %H:%M:%S"
+
+START_TIME = datetime.datetime.fromtimestamp(1490191111)
+
+try:
+ shutil.rmtree("/tmp/demo")
+ os.makedirs("/tmp/demo")
+except OSError:
+ pass
+
+PIDS = [
+ "123",
+ "123",
+ "123",
+ "121",
+ "124",
+ "123",
+ "61456",
+ "61456",
+ "61457",
+]
+
+LOG_LOCS = [
+ "demo.cc:123",
+ "demo.cc:352",
+ "loader.cc:13",
+ "loader.cc:552",
+ "blaster.cc:352",
+ "blaster.cc:112",
+ "blaster.cc:6782",
+]
+
+CURR_TIME = START_TIME
+for _index in range(0, int(sys.argv[1])):
+ CURR_TIME += datetime.timedelta(seconds=random.randrange(1, 22))
+ print("I%s.%06d %s %s] %s" % (
+ CURR_TIME.strftime(GLOG_DATE_FMT),
+ random.randrange(0, 100000),
+ random.choice(PIDS),
+ random.choice(LOG_LOCS),
+ random.choice(MSGS)))
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 0000000..f40fbd8
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,5 @@
+_site
+.sass-cache
+.jekyll-cache
+.jekyll-metadata
+vendor
diff --git a/docs/02_downloads.md b/docs/02_downloads.md
new file mode 100644
index 0000000..9c44192
--- /dev/null
+++ b/docs/02_downloads.md
@@ -0,0 +1,65 @@
+---
+layout: page
+title: Downloads
+permalink: /downloads
+---
+
+The latest **stable** release is [**v{{ site.version }}**](https://github.com/tstack/lnav/releases/latest).
+
+The following options are available for installing **lnav**:
+
+## Linux
+
+<!-- markdown-link-check-disable-next-line -->
+Download a [statically linked 64-bit binary](https://github.com/tstack/lnav/releases/download/v{{site.version}}/lnav-{{site.version}}-x86_64-linux-musl.zip).
+
+Install from the [Snap Store](https://snapcraft.io/lnav):
+
+```console
+$ sudo snap install lnav
+```
+
+Install RPMs from [Package Cloud](https://packagecloud.io/tstack/lnav):
+
+```console
+$ curl -s https://packagecloud.io/install/repositories/tstack/lnav/script.rpm.sh | sudo bash
+$ sudo yum install lnav
+```
+
+## MacOS
+
+<!-- markdown-link-check-disable-next-line -->
+Download a [statically linked 64-bit binary](https://github.com/tstack/lnav/releases/download/v{{site.version}}/lnav-{{site.version}}-x86_64-macos.zip)
+
+Install using [Homebrew](https://formulae.brew.sh/formula/lnav):
+
+```console
+$ brew install lnav
+```
+
+## Source
+
+<!-- markdown-link-check-disable-next-line -->
+Download the [source](https://github.com/tstack/lnav/releases/download/v{{site.version}}/lnav-{{site.version}}.tar.gz)
+and install any dependencies. The following commands will unpack the source
+tar ball, configure the build for your system, build, and then install:
+
+```console
+$ tar xvfz lnav-{{site.version}}.tar.gz
+$ cd lnav-{{site.version}}
+$ ./configure
+$ make
+$ make install
+```
+
+### GitHub
+
+If you would like to contribute to the development of lnav, visit our page on
+[GitHub](https://github.com/tstack/lnav).
+
+# VSCode Extension
+
+The [lnav VSCode Extension](https://marketplace.visualstudio.com/items?itemName=lnav.lnav)
+can be used to add syntax highlighting to lnav scripts.
+
+![Screenshot of an lnav script](/assets/images/lnav-vscode-extension.png)
diff --git a/docs/03_features.md b/docs/03_features.md
new file mode 100644
index 0000000..513cd2c
--- /dev/null
+++ b/docs/03_features.md
@@ -0,0 +1,146 @@
+---
+layout: page
+title: Features
+permalink: /features
+---
+
+* TOC
+{:toc}
+
+## Single Log View
+
+All log file contents are merged into a single view based on message timestamps.
+You no longer need to manually correlate timestamps across multiple windows or
+figure out the order in which to view rotated log files. The color bars on the
+left-hand side help to show which file a message belongs to.
+
+![Screenshot of lnav showing messages from multiple files](/assets/images/lnav-multi-file2.png)
+
+## Automatic Log Format Detection
+
+The log message format is automatically determined by lnav while scanning your
+files. The following are some of the [formats that are built in by default](https://docs.lnav.org/en/latest/formats.html):
+
+* Common Web Access Log format
+* W3C Extended Log File Format
+* logfmt
+* CUPS page_log
+* Syslog
+* Glog
+* VMware ESXi/vCenter Logs
+* dpkg.log
+* uwsgi
+* "Generic" - Any message that starts with a timestamp
+* Strace
+* sudo
+
+If your log file format is JSON-lines or can be matched by a PCRE regular
+expression, you can define your own format in a
+[JSON file](https://docs.lnav.org/en/latest/formats.html#defining-a-new-format).
+
+GZIP'ed and BZIP2'ed files are also detected automatically and decompressed on-the-fly.
+
+## Filters
+
+Display only lines that match or do not match a set of regular expressions.
+Useful for removing extraneous log lines that you are not interested in.
+
+## Timeline View
+
+The timeline view shows a histogram of messages over time. The number of
+warnings and errors are highlighted in the display so that you can easily see
+where problems have occurred. Once you have found a period of time that is of
+interest, a key-press will take you back to the log message view at the
+corresponding time.
+
+![Screenshot of timeline view](/assets/images/lnav-hist.png)
+
+## Pretty-Print View
+
+The pretty-print view will reformat structured data, like XML or JSON, so that
+it is easier to read. Simply press SHIFT+P in the log view to have all the
+currently displayed lines pretty-printed.
+
+The following screenshot shows an XML blob with no indentation:
+
+![A flat blob of XML](/assets/images/lnav-before-pretty.png)
+
+After pressing SHIFT+P, the XML is pretty-printed for easier viewing:
+
+![A pretty-printed blob of XML](/assets/images/lnav-after-pretty.png)
+
+## Query Logs Using SQL
+
+Log files are directly used as the backing for SQLite virtual tables. This
+means you can perform queries on messages without having to load the data into
+an SQL database. For example, the screenshot below shows the result of
+running the following query against an Apache access_log file:
+
+```sql
+SELECT c_ip, count(*), sum(sc_bytes) AS total FROM access_log
+ GROUP BY c_ip ORDER BY total DESC;
+```
+
+![The results of a SQL query](/assets/images/lnav-query.png)
+
+## "Live" Operation
+
+Searches are done as you type; new log lines are automatically loaded and
+searched as they are added; filters apply to lines as they are loaded; and, SQL
+queries are checked for correctness as you type.
+
+## Themes
+
+The UI can be [customized through themes](https://lnav.readthedocs.io/en/latest/config.html#theme-definitions).
+
+![Animation of the UI cycling through themes](/assets/images/lnav-theme-cycle.gif)
+
+## Syntax Highlighting
+
+Errors and warnings are colored in red and yellow, respectively. Highlights are
+also applied to: SQL keywords, XML tags, file and line numbers in Java
+backtraces, and quoted strings. The search and SQL query prompt are also
+highlighted as you type, making it easier to see errors and matching brackets.
+
+![Animation of syntax highlighting](/assets/images/lnav-syntax-highlight.gif)
+
+## Tab-completion
+
+The command prompt supports tab-completion for almost all operations. For
+example, when doing a search, you can tab-complete words that are displayed on
+screen rather than having to do a copy & paste.
+
+![Animation of TAB-completion](/assets/images/lnav-tab-complete.gif)
+
+## Custom Keymaps
+
+[Hotkeys can be customized](https://lnav.readthedocs.io/en/latest/config.html#keymap-definitions)
+to run lnav commands or scripts.
+
+## Sessions
+
+Session information is saved automatically and restored when you are viewing the
+same set of files. The current location in files, bookmarks, and applied filters
+are all saved as part of the session.
+
+## Headless Mode
+
+The log processing features of lnav can be used in scripts if you have a canned
+set of operations or queries that you want to perform regularly. You can enable
+headless mode with the '-n' switch on the command-line and then use the '-c'
+flag to specify the commands or queries you want to execute. For example, to get
+the top 10 client IP addresses from an apache access log file and write the
+results to standard out in CSV format:
+
+```console
+$ lnav -n \
+ -c ';SELECT c_ip, count(*) AS total FROM access_log GROUP BY c_ip ORDER BY total DESC LIMIT 10' \
+ -c ':write-csv-to -' \
+ access.log
+
+c_ip,total
+10.208.110.176,2989570
+10.178.4.102,11183
+10.32.110.197,2020
+10.29.165.250,443
+```
diff --git a/docs/04_tutorials.md b/docs/04_tutorials.md
new file mode 100644
index 0000000..1913e5a
--- /dev/null
+++ b/docs/04_tutorials.md
@@ -0,0 +1,30 @@
+---
+layout: page
+title: Tutorials
+permalink: /tutorials
+---
+
+These tutorials are provided to help you learn how **lnav** works
+without having to install anything. They are running on a shared
+[fly.io](https://fly.io) instance, so please be kind.
+
+The tutorials are implemented using features in **lnav** and not
+built in to the code itself. The tutorial text is
+[markdown](https://docs.lnav.org/en/latest/ui.html#markdown),
+the logic is written in [SQL](https://docs.lnav.org/en/latest/sqlext.html),
+and the reactions are triggered through
+[events](https://docs.lnav.org/en/latest/events.html).
+
+The source for the tutorials can be found [here](https://github.com/tstack/lnav/tree/master/docs/tutorials).
+
+# Tutorial 1
+
+<div id="playground-box">
+<h4>Learn how to navigate an example log file using lnav:</h4>
+
+<code>
+<span class="prompt">$</span>
+<a href="ssh://tutorial1@demo.lnav.org">ssh tutorial1@demo.lnav.org</a>
+</code>
+</div>
+
diff --git a/docs/05_docs.md b/docs/05_docs.md
new file mode 100644
index 0000000..3c19354
--- /dev/null
+++ b/docs/05_docs.md
@@ -0,0 +1,6 @@
+---
+title: Docs
+permalink: /docs
+redirect_to:
+ - https://docs.lnav.org
+---
diff --git a/docs/06_changeblog.md b/docs/06_changeblog.md
new file mode 100644
index 0000000..1f2bdc9
--- /dev/null
+++ b/docs/06_changeblog.md
@@ -0,0 +1,6 @@
+---
+title: ChangeBlog
+layout: home
+permalink: /blog/
+---
+
diff --git a/docs/404.html b/docs/404.html
new file mode 100644
index 0000000..086a5c9
--- /dev/null
+++ b/docs/404.html
@@ -0,0 +1,25 @@
+---
+permalink: /404.html
+layout: default
+---
+
+<style type="text/css" media="screen">
+ .container {
+ margin: 10px auto;
+ max-width: 600px;
+ text-align: center;
+ }
+ h1 {
+ margin: 30px 0;
+ font-size: 4em;
+ line-height: 1;
+ letter-spacing: -1px;
+ }
+</style>
+
+<div class="container">
+ <h1>404</h1>
+
+ <p><strong>Page not found :(</strong></p>
+ <p>The requested page could not be found.</p>
+</div>
diff --git a/docs/CNAME b/docs/CNAME
new file mode 100644
index 0000000..330a24a
--- /dev/null
+++ b/docs/CNAME
@@ -0,0 +1 @@
+lnav.org \ No newline at end of file
diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in
new file mode 100644
index 0000000..a6da763
--- /dev/null
+++ b/docs/Doxyfile.in
@@ -0,0 +1,32 @@
+# Configuration for Doxygen for use with CMake
+# Only options that deviate from the default are included
+# To create a new Doxyfile containing all available options, call `doxygen -g`
+
+# Get Project name and version from CMake
+PROJECT_NAME = "@PROJECT_NAME@"
+PROJECT_NUMBER = "@PROJECT_VERSION@"
+
+# Add sources
+INPUT = "@PROJECT_SOURCE_DIR@/README.md" "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@/source" "@PROJECT_SOURCE_DIR@/docs/pages"
+EXTRACT_ALL = YES
+RECURSIVE = YES
+OUTPUT_DIRECTORY = "@DOXYGEN_OUTPUT_DIRECTORY@"
+
+# Use the README as a main page
+USE_MDFILE_AS_MAINPAGE = "@PROJECT_SOURCE_DIR@/README.md"
+
+# set relative include paths
+FULL_PATH_NAMES = YES
+STRIP_FROM_PATH = "@PROJECT_SOURCE_DIR@/include" "@PROJECT_SOURCE_DIR@"
+STRIP_FROM_INC_PATH =
+
+# We use m.css to generate the html documentation, so we only need XML output
+GENERATE_XML = YES
+GENERATE_HTML = NO
+GENERATE_LATEX = NO
+XML_PROGRAMLISTING = NO
+CREATE_SUBDIRS = NO
+
+# Include all directories, files and namespaces in the documentation
+# Disable to include only explicitly documented objects
+M_SHOW_UNDOCUMENTED = YES
diff --git a/docs/Gemfile b/docs/Gemfile
new file mode 100644
index 0000000..10560d9
--- /dev/null
+++ b/docs/Gemfile
@@ -0,0 +1,31 @@
+source "https://rubygems.org"
+# Hello! This is where you manage which Jekyll version is used to run.
+# When you want to use a different version, change it below, save the
+# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
+#
+# bundle exec jekyll serve
+#
+# This will help ensure the proper Jekyll version is running.
+# Happy Jekylling!
+# gem "jekyll", "~> 4.2.0"
+# This is the default theme for new Jekyll sites. You may change this to anything you like.
+gem "minima", "~> 2.5"
+# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
+# uncomment the line below. To upgrade, run `bundle update github-pages`.
+gem "github-pages", group: :jekyll_plugins
+# If you have any plugins, put them here!
+group :jekyll_plugins do
+end
+
+# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
+# and associated library.
+platforms :mingw, :x64_mingw, :mswin, :jruby do
+ gem "tzinfo", "~> 1.2"
+ gem "tzinfo-data"
+end
+
+# Performance-booster for watching directories on Windows
+gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
+
+
+gem "webrick", "~> 1.7"
diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock
new file mode 100644
index 0000000..275c580
--- /dev/null
+++ b/docs/Gemfile.lock
@@ -0,0 +1,268 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ activesupport (7.0.5)
+ concurrent-ruby (~> 1.0, >= 1.0.2)
+ i18n (>= 1.6, < 2)
+ minitest (>= 5.1)
+ tzinfo (~> 2.0)
+ addressable (2.8.4)
+ public_suffix (>= 2.0.2, < 6.0)
+ coffee-script (2.4.1)
+ coffee-script-source
+ execjs
+ coffee-script-source (1.11.1)
+ colorator (1.1.0)
+ commonmarker (0.23.9)
+ concurrent-ruby (1.2.2)
+ dnsruby (1.70.0)
+ simpleidn (~> 0.2.1)
+ em-websocket (0.5.3)
+ eventmachine (>= 0.12.9)
+ http_parser.rb (~> 0)
+ ethon (0.16.0)
+ ffi (>= 1.15.0)
+ eventmachine (1.2.7)
+ execjs (2.8.1)
+ faraday (2.7.7)
+ faraday-net_http (>= 2.0, < 3.1)
+ ruby2_keywords (>= 0.0.4)
+ faraday-net_http (3.0.2)
+ ffi (1.15.5)
+ forwardable-extended (2.6.0)
+ gemoji (3.0.1)
+ github-pages (228)
+ github-pages-health-check (= 1.17.9)
+ jekyll (= 3.9.3)
+ jekyll-avatar (= 0.7.0)
+ jekyll-coffeescript (= 1.1.1)
+ jekyll-commonmark-ghpages (= 0.4.0)
+ jekyll-default-layout (= 0.1.4)
+ jekyll-feed (= 0.15.1)
+ jekyll-gist (= 1.5.0)
+ jekyll-github-metadata (= 2.13.0)
+ jekyll-include-cache (= 0.2.1)
+ jekyll-mentions (= 1.6.0)
+ jekyll-optional-front-matter (= 0.3.2)
+ jekyll-paginate (= 1.1.0)
+ jekyll-readme-index (= 0.3.0)
+ jekyll-redirect-from (= 0.16.0)
+ jekyll-relative-links (= 0.6.1)
+ jekyll-remote-theme (= 0.4.3)
+ jekyll-sass-converter (= 1.5.2)
+ jekyll-seo-tag (= 2.8.0)
+ jekyll-sitemap (= 1.4.0)
+ jekyll-swiss (= 1.0.0)
+ jekyll-theme-architect (= 0.2.0)
+ jekyll-theme-cayman (= 0.2.0)
+ jekyll-theme-dinky (= 0.2.0)
+ jekyll-theme-hacker (= 0.2.0)
+ jekyll-theme-leap-day (= 0.2.0)
+ jekyll-theme-merlot (= 0.2.0)
+ jekyll-theme-midnight (= 0.2.0)
+ jekyll-theme-minimal (= 0.2.0)
+ jekyll-theme-modernist (= 0.2.0)
+ jekyll-theme-primer (= 0.6.0)
+ jekyll-theme-slate (= 0.2.0)
+ jekyll-theme-tactile (= 0.2.0)
+ jekyll-theme-time-machine (= 0.2.0)
+ jekyll-titles-from-headings (= 0.5.3)
+ jemoji (= 0.12.0)
+ kramdown (= 2.3.2)
+ kramdown-parser-gfm (= 1.1.0)
+ liquid (= 4.0.4)
+ mercenary (~> 0.3)
+ minima (= 2.5.1)
+ nokogiri (>= 1.13.6, < 2.0)
+ rouge (= 3.26.0)
+ terminal-table (~> 1.4)
+ github-pages-health-check (1.17.9)
+ addressable (~> 2.3)
+ dnsruby (~> 1.60)
+ octokit (~> 4.0)
+ public_suffix (>= 3.0, < 5.0)
+ typhoeus (~> 1.3)
+ html-pipeline (2.14.3)
+ activesupport (>= 2)
+ nokogiri (>= 1.4)
+ http_parser.rb (0.8.0)
+ i18n (1.14.1)
+ concurrent-ruby (~> 1.0)
+ jekyll (3.9.3)
+ addressable (~> 2.4)
+ colorator (~> 1.0)
+ em-websocket (~> 0.5)
+ i18n (>= 0.7, < 2)
+ jekyll-sass-converter (~> 1.0)
+ jekyll-watch (~> 2.0)
+ kramdown (>= 1.17, < 3)
+ liquid (~> 4.0)
+ mercenary (~> 0.3.3)
+ pathutil (~> 0.9)
+ rouge (>= 1.7, < 4)
+ safe_yaml (~> 1.0)
+ jekyll-avatar (0.7.0)
+ jekyll (>= 3.0, < 5.0)
+ jekyll-coffeescript (1.1.1)
+ coffee-script (~> 2.2)
+ coffee-script-source (~> 1.11.1)
+ jekyll-commonmark (1.4.0)
+ commonmarker (~> 0.22)
+ jekyll-commonmark-ghpages (0.4.0)
+ commonmarker (~> 0.23.7)
+ jekyll (~> 3.9.0)
+ jekyll-commonmark (~> 1.4.0)
+ rouge (>= 2.0, < 5.0)
+ jekyll-default-layout (0.1.4)
+ jekyll (~> 3.0)
+ jekyll-feed (0.15.1)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-gist (1.5.0)
+ octokit (~> 4.2)
+ jekyll-github-metadata (2.13.0)
+ jekyll (>= 3.4, < 5.0)
+ octokit (~> 4.0, != 4.4.0)
+ jekyll-include-cache (0.2.1)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-mentions (1.6.0)
+ html-pipeline (~> 2.3)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-optional-front-matter (0.3.2)
+ jekyll (>= 3.0, < 5.0)
+ jekyll-paginate (1.1.0)
+ jekyll-readme-index (0.3.0)
+ jekyll (>= 3.0, < 5.0)
+ jekyll-redirect-from (0.16.0)
+ jekyll (>= 3.3, < 5.0)
+ jekyll-relative-links (0.6.1)
+ jekyll (>= 3.3, < 5.0)
+ jekyll-remote-theme (0.4.3)
+ addressable (~> 2.0)
+ jekyll (>= 3.5, < 5.0)
+ jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0)
+ rubyzip (>= 1.3.0, < 3.0)
+ jekyll-sass-converter (1.5.2)
+ sass (~> 3.4)
+ jekyll-seo-tag (2.8.0)
+ jekyll (>= 3.8, < 5.0)
+ jekyll-sitemap (1.4.0)
+ jekyll (>= 3.7, < 5.0)
+ jekyll-swiss (1.0.0)
+ jekyll-theme-architect (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-cayman (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-dinky (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-hacker (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-leap-day (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-merlot (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-midnight (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-minimal (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-modernist (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-primer (0.6.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-github-metadata (~> 2.9)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-slate (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-tactile (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-theme-time-machine (0.2.0)
+ jekyll (> 3.5, < 5.0)
+ jekyll-seo-tag (~> 2.0)
+ jekyll-titles-from-headings (0.5.3)
+ jekyll (>= 3.3, < 5.0)
+ jekyll-watch (2.2.1)
+ listen (~> 3.0)
+ jemoji (0.12.0)
+ gemoji (~> 3.0)
+ html-pipeline (~> 2.2)
+ jekyll (>= 3.0, < 5.0)
+ kramdown (2.3.2)
+ rexml
+ kramdown-parser-gfm (1.1.0)
+ kramdown (~> 2.0)
+ liquid (4.0.4)
+ listen (3.8.0)
+ rb-fsevent (~> 0.10, >= 0.10.3)
+ rb-inotify (~> 0.9, >= 0.9.10)
+ mercenary (0.3.6)
+ minima (2.5.1)
+ jekyll (>= 3.5, < 5.0)
+ jekyll-feed (~> 0.9)
+ jekyll-seo-tag (~> 2.1)
+ minitest (5.18.1)
+ nokogiri (1.15.2-arm64-darwin)
+ racc (~> 1.4)
+ nokogiri (1.15.2-x86_64-linux)
+ racc (~> 1.4)
+ octokit (4.25.1)
+ faraday (>= 1, < 3)
+ sawyer (~> 0.9)
+ pathutil (0.16.2)
+ forwardable-extended (~> 2.6)
+ public_suffix (4.0.7)
+ racc (1.7.1)
+ rb-fsevent (0.11.2)
+ rb-inotify (0.10.1)
+ ffi (~> 1.0)
+ rexml (3.2.5)
+ rouge (3.26.0)
+ ruby2_keywords (0.0.5)
+ rubyzip (2.3.2)
+ safe_yaml (1.0.5)
+ sass (3.7.4)
+ sass-listen (~> 4.0.0)
+ sass-listen (4.0.0)
+ rb-fsevent (~> 0.9, >= 0.9.4)
+ rb-inotify (~> 0.9, >= 0.9.7)
+ sawyer (0.9.2)
+ addressable (>= 2.3.5)
+ faraday (>= 0.17.3, < 3)
+ simpleidn (0.2.1)
+ unf (~> 0.1.4)
+ terminal-table (1.8.0)
+ unicode-display_width (~> 1.1, >= 1.1.1)
+ typhoeus (1.4.0)
+ ethon (>= 0.9.0)
+ tzinfo (2.0.6)
+ concurrent-ruby (~> 1.0)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.8.2)
+ unicode-display_width (1.8.0)
+ webrick (1.8.1)
+
+PLATFORMS
+ arm64-darwin-21
+ arm64-darwin-22
+ x86_64-linux
+
+DEPENDENCIES
+ github-pages
+ minima (~> 2.5)
+ tzinfo (~> 1.2)
+ tzinfo-data
+ wdm (~> 0.1.1)
+ webrick (~> 1.7)
+
+BUNDLED WITH
+ 2.3.20
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..b16b62c
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,153 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+PAPER =
+BUILDDIR = build
+
+# Internal variables.
+PAPEROPT_a4 = -D latex_paper_size=a4
+PAPEROPT_letter = -D latex_paper_size=letter
+ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+# the i18n builder cannot share the environment and doctrees with the others
+I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
+
+.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " dirhtml to make HTML files named index.html in directories"
+ @echo " singlehtml to make a single large HTML file"
+ @echo " pickle to make pickle files"
+ @echo " json to make JSON files"
+ @echo " htmlhelp to make HTML files and a HTML help project"
+ @echo " qthelp to make HTML files and a qthelp project"
+ @echo " devhelp to make HTML files and a Devhelp project"
+ @echo " epub to make an epub"
+ @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
+ @echo " latexpdf to make LaTeX files and run them through pdflatex"
+ @echo " text to make text files"
+ @echo " man to make manual pages"
+ @echo " texinfo to make Texinfo files"
+ @echo " info to make Texinfo files and run them through makeinfo"
+ @echo " gettext to make PO message catalogs"
+ @echo " changes to make an overview of all changed/added/deprecated items"
+ @echo " linkcheck to check all external links for integrity"
+ @echo " doctest to run all doctests embedded in the documentation (if enabled)"
+
+clean:
+ -rm -rf $(BUILDDIR)/*
+
+html:
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
+
+dirhtml:
+ $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
+ @echo
+ @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
+
+singlehtml:
+ $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
+ @echo
+ @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
+
+pickle:
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
+ @echo
+ @echo "Build finished; now you can process the pickle files."
+
+json:
+ $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
+ @echo
+ @echo "Build finished; now you can process the JSON files."
+
+htmlhelp:
+ $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
+ @echo
+ @echo "Build finished; now you can run HTML Help Workshop with the" \
+ ".hhp project file in $(BUILDDIR)/htmlhelp."
+
+qthelp:
+ $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
+ @echo
+ @echo "Build finished; now you can run "qcollectiongenerator" with the" \
+ ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
+ @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/lnav.qhcp"
+ @echo "To view the help file:"
+ @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/lnav.qhc"
+
+devhelp:
+ $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
+ @echo
+ @echo "Build finished."
+ @echo "To view the help file:"
+ @echo "# mkdir -p $$HOME/.local/share/devhelp/lnav"
+ @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/lnav"
+ @echo "# devhelp"
+
+epub:
+ $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
+ @echo
+ @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
+
+latex:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo
+ @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
+ @echo "Run \`make' in that directory to run these through (pdf)latex" \
+ "(use \`make latexpdf' here to do that automatically)."
+
+latexpdf:
+ $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
+ @echo "Running LaTeX files through pdflatex..."
+ $(MAKE) -C $(BUILDDIR)/latex all-pdf
+ @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
+
+text:
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
+ @echo
+ @echo "Build finished. The text files are in $(BUILDDIR)/text."
+
+man:
+ $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
+ @echo
+ @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
+
+texinfo:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo
+ @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
+ @echo "Run \`make' in that directory to run these through makeinfo" \
+ "(use \`make info' here to do that automatically)."
+
+info:
+ $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
+ @echo "Running Texinfo files through makeinfo..."
+ make -C $(BUILDDIR)/texinfo info
+ @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
+
+gettext:
+ $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
+ @echo
+ @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
+
+changes:
+ $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
+ @echo
+ @echo "The overview file is in $(BUILDDIR)/changes."
+
+linkcheck:
+ $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
+ @echo
+ @echo "Link check complete; look for any errors in the above output " \
+ "or in $(BUILDDIR)/linkcheck/output.txt."
+
+doctest:
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
+ @echo "Testing of doctests in the sources finished, look at the " \
+ "results in $(BUILDDIR)/doctest/output.txt."
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..c93d111
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,4 @@
+# Docs
+
+This directory contains the ReST documentation that is published to
+[docs.lnav.org](https://docs.lnav.org)
diff --git a/docs/_config.yml b/docs/_config.yml
new file mode 100644
index 0000000..6d116ee
--- /dev/null
+++ b/docs/_config.yml
@@ -0,0 +1,62 @@
+# Welcome to Jekyll!
+#
+# This config file is meant for settings that affect your whole blog, values
+# which you are expected to set up once and rarely edit after that. If you find
+# yourself editing this file very often, consider using Jekyll's data files
+# feature for the data you need to update frequently.
+#
+# For technical reasons, this file is *NOT* reloaded automatically when you use
+# 'bundle exec jekyll serve'. If you change this file, please restart the server process.
+#
+# If you need help with YAML syntax, here are some quick references for you:
+# https://learn-the-web.algonquindesign.ca/topics/markdown-yaml-cheat-sheet/#yaml
+# https://learnxinyminutes.com/docs/yaml/
+#
+# Site settings
+# These are used to personalize your new site. If you look in the HTML files,
+# you will see them accessed via {{ site.title }}, {{ site.email }}, and so on.
+# You can create any custom variable you would like, and they will be accessible
+# in the templates via {{ site.myvariable }}.
+
+title: The Logfile Navigator
+version: 0.11.1
+email: support@lnav.org
+description: >- # this means to ignore newlines until "baseurl:"
+ The Logfile Navigator, lnav for short, is an advanced log file viewer
+ for the small-scale.
+# baseurl: /lnav
+# url: "http://lnav.org" # the base hostname & protocol for your site, e.g. http://example.com
+twitter_username: lnavapp
+github: tstack/lnav
+
+# Build settings
+theme: minima
+plugins:
+ - jekyll-feed
+ - jekyll-redirect-from
+
+show_excerpts: true
+
+exclude:
+ - source
+ - tutorials
+
+# Exclude from processing.
+# The following items will not be processed, by default.
+# Any item listed under the `exclude:` key here will be automatically added to
+# the internal "default list".
+#
+# Excluded items can be processed by explicitly listing the directories or
+# their entries' file path in the `include:` list.
+#
+# exclude:
+# - .sass-cache/
+# - .jekyll-cache/
+# - gemfiles/
+# - Gemfile
+# - Gemfile.lock
+# - node_modules/
+# - vendor/bundle/
+# - vendor/cache/
+# - vendor/gems/
+# - vendor/ruby/
diff --git a/docs/_includes/social.html b/docs/_includes/social.html
new file mode 100644
index 0000000..15672c0
--- /dev/null
+++ b/docs/_includes/social.html
@@ -0,0 +1,121 @@
+<ul class="social-media-list">
+ {%- if site.dribbble_username -%}
+ <li>
+ <a href="https://dribbble.com/{{ site.dribbble_username| cgi_escape | escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#dribbble' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.dribbble_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- if site.facebook_username -%}
+ <li>
+ <a href="https://www.facebook.com/{{ site.facebook_username| cgi_escape | escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#facebook' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.facebook_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- if site.flickr_username -%}
+ <li>
+ <a href="https://www.flickr.com/photos/{{ site.flickr_username| cgi_escape | escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#flickr' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.flickr_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- if site.github_username -%}
+ <li>
+ <a href="https://github.com/{{ site.github_username| cgi_escape | escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#github' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.github_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- if site.github -%}
+ <li><a href="https://github.com/{{ site.github }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#github' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.github | escape }}</span></a></li>
+ {%- endif -%}
+ {%- if site.instagram_username -%}
+ <li>
+ <a href="https://instagram.com/{{ site.instagram_username| cgi_escape | escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#instagram' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.instagram_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- if site.linkedin_username -%}
+ <li>
+ <a href="https://www.linkedin.com/in/{{ site.linkedin_username| cgi_escape | escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#linkedin' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.linkedin_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- if site.pinterest_username -%}
+ <li>
+ <a href="https://www.pinterest.com/{{ site.pinterest_username| cgi_escape | escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#pinterest' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.pinterest_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- for mst in site.mastodon -%}{%- if mst.username and mst.instance -%}
+ <li>
+ <a href="https://{{ mst.instance| cgi_escape | escape}}/@{{mst.username}}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#mastodon' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ mst.username|escape }}</span></a></li>
+ {%- endif -%}{%- endfor -%}
+ {%- if site.twitter_username -%}
+ <li>
+ <a href="https://www.twitter.com/{{ site.twitter_username| cgi_escape | escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#twitter' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.twitter_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- if site.youtube_username -%}
+ <li>
+ <a href="https://youtube.com/{{ site.youtube_username| cgi_escape | escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#youtube' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.youtube_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- if site.googleplus_username -%}
+ <li>
+ <a href="https://plus.google.com/{{ site.googleplus_username| escape }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#googleplus' | relative_url }}"></use>
+ </svg>
+ <span class="username">{{ site.googleplus_username| escape }}</span></a>
+ </li>
+ {%- endif -%}
+ {%- if site.rss -%}
+ <li><a href="{{ 'feed.xml' | relative_url }}">
+ <svg class="svg-icon">
+ <use xlink:href="{{ '/assets/minima-social-icons.svg#rss' | relative_url }}"></use>
+ </svg>
+ <span>{{ site.rss | escape }}</span></a></li>
+ {%- endif -%}
+ <li><a href="https://discord.gg/erBPnKwz7R">
+ <img style="height: 1.75em"
+ src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/62594fdd4b264a31eafa1e5d_92ad040ed5143bfb541ea61f5c3bb18f.svg"/>
+ </a></li>
+</ul>
+
+<script data-goatcounter="https://lnav.goatcounter.com/count"
+ async src="//gc.zgo.at/count.js"></script>
diff --git a/docs/_layouts/post.html b/docs/_layouts/post.html
new file mode 100644
index 0000000..6deec42
--- /dev/null
+++ b/docs/_layouts/post.html
@@ -0,0 +1,17 @@
+---
+layout: default
+---
+
+<div class="post">
+
+ <header class="post-header">
+ <h1 class="post-title">{{ page.title }}</h1>
+ </header>
+
+ <article class="post-content">
+ {{ content }}
+ </article>
+
+ <script src="/assets/js/codeblock.js"></script>
+
+</div>
diff --git a/docs/_layouts/top.html b/docs/_layouts/top.html
new file mode 100644
index 0000000..75be485
--- /dev/null
+++ b/docs/_layouts/top.html
@@ -0,0 +1,78 @@
+<!DOCTYPE html>
+<html lang="{{ page.lang | default: site.lang | default: "en" }}">
+
+{%- include head.html -%}
+
+<link rel="icon" type="image/svg+xml" href="/assets/images/favicon.svg">
+<link rel="icon" type="image/png" href="/assets/images/favicon.png">
+
+<style>
+ main {
+ margin: 0 50px;
+ }
+
+ #top-description {
+ font-size: xx-large;
+ margin-top: 3em;
+ margin-left: 1em;
+ }
+
+ #intro {
+ font-size: x-large;
+ font-weight: lighter;
+ margin-top: 2em;
+ margin-left: 2em;
+ }
+
+ .dlrow {
+ display: grid;
+ align-items: start;
+ grid-template-columns: repeat(auto-fit, minmax(22em, 1fr));
+ width: 100%;
+ }
+
+ p:before, #top-description:before {
+ content: "";
+ width: 6em;
+ display: block;
+ overflow: hidden;
+ }
+
+ dl {
+ display: grid;
+ grid-template-rows: auto auto;
+ grid-auto-flow: column;
+ }
+ dl dt {
+ font-weight: normal;
+ font-size: x-large;
+ text-align: center;
+ border-bottom: 1px solid #8d8;
+ margin: 20px 30px;
+ height: 2em;
+ }
+ dl dd {
+ margin: 0 30px 20px 30px;
+ min-height: 3em;
+ color: #828282;
+ font-size: medium;
+ }
+</style>
+
+<body>
+
+<a href="https://github.com/tstack/lnav">
+ <img loading="lazy" width="149" style="position: absolute; z-index: 100" height="149" src="https://github.blog/wp-content/uploads/2008/12/forkme_left_green_007200.png?resize=149%2C149" class="attachment-full size-full" alt="Fork me on GitHub" data-recalc-dims="1">
+</a>
+
+{%- include header.html -%}
+
+<main class="page-content" aria-label="Content">
+ {{ content }}
+</main>
+
+{%- include footer.html -%}
+
+</body>
+
+</html>
diff --git a/docs/_posts/2013-09-10-json-encoded-logs.md b/docs/_posts/2013-09-10-json-encoded-logs.md
new file mode 100644
index 0000000..1fa36a4
--- /dev/null
+++ b/docs/_posts/2013-09-10-json-encoded-logs.md
@@ -0,0 +1,56 @@
+---
+layout: post
+title: "Support for JSON-encoded logs in v0.6.1"
+date: 2013-09-10 00:00:00
+excerpt: Turning JSON barf into something readable.
+---
+
+Making logs easily digestible by machines is becoming a concern as tools like
+elasticsearch become more popular. One of the popular strategies is to encode
+the whole log message in JSON and then write that as a single line to a file.
+For example:
+
+```json
+{"time": "2013-09-04T23:55:09.274041Z", "level" : "INFO", "body" : "Hello, World!" }
+{"time": "2013-09-04T23:56:00.285224Z", "level" : "ERROR", "body" : "Something terrible has happened!", "tb": " foo.c:12\n bar.y:33" }
+```
+
+Unfortunately, what is good for a machine is not so great for a human. To try to
+improve the situation, the latest release of lnav includes support for parsing
+JSON log messages and transforming them on-the-fly. The display format is
+specified in a log format configuration and can specify which fields should be
+displayed on the main message line. Any unused fields that are found in the
+message will be displayed below the main field so you don't miss anything.
+
+The above log lines can be transformed using the following format configuration:
+
+```json
+{
+ "json_ex_log": {
+ "title": "Example JSON Log",
+ "description": "An example log format configuration for JSON logs",
+ "json": true,
+ "file-pattern": "test-log\\.json.*",
+ "level-field": "level",
+ "line-format": [
+ {
+ "field": "time"
+ },
+ " ",
+ {
+ "field": "body"
+ }
+ ]
+ }
+}
+```
+
+After copying this config to `~/.lnav/formats/test/format.json`, the log messages
+will look like this when viewed in lnav:
+
+```
+2013-09-04T23:55:09.274041Z Hello, World!
+2013-09-04T23:56:00.285224Z Something terrible has happened!
+ tb: foo.c:12
+ tb: bar.y:33
+```
diff --git a/docs/_posts/2013-09-13-four-years-on-github.md b/docs/_posts/2013-09-13-four-years-on-github.md
new file mode 100644
index 0000000..875dc53
--- /dev/null
+++ b/docs/_posts/2013-09-13-four-years-on-github.md
@@ -0,0 +1,12 @@
+---
+layout: post
+title: "Four years on GitHub"
+date: 2013-09-13 00:00:00
+excerpt: Still going strong!
+---
+
+The [first commit](https://github.com/tstack/lnav/commit/b4ec432515e95e86ec9d711833b8cb34d0912546)
+to the [lnav repository](https://github.com/tstack/lnav) was four years ago
+today! I started working on lnav a couple of years before that, but this was
+its first public appearance. I still use it every day and plan to keep things
+going for years to come.
diff --git a/docs/_posts/2013-10-05-mini-review-in-linux-magazine.md b/docs/_posts/2013-10-05-mini-review-in-linux-magazine.md
new file mode 100644
index 0000000..19f9c73
--- /dev/null
+++ b/docs/_posts/2013-10-05-mini-review-in-linux-magazine.md
@@ -0,0 +1,8 @@
+---
+layout: post
+title: "Mini review of lnav in Linux Magazine"
+date: 2013-10-05 00:00:00
+---
+
+The [October issue of Linux Magazine](http://www.linux-magazine.com/Issues/2013/155/Tool-Tips)
+has a nice review of lnav v0.5.0, the author gave it four out of five stars!
diff --git a/docs/_posts/2013-10-06-competing-with-tail.md b/docs/_posts/2013-10-06-competing-with-tail.md
new file mode 100644
index 0000000..b93e7bd
--- /dev/null
+++ b/docs/_posts/2013-10-06-competing-with-tail.md
@@ -0,0 +1,23 @@
+---
+layout: post
+title: "Competing with 'tail -f'"
+date: 2013-09-10 00:00:00
+excerpt: The standard utilities are tough competition.
+---
+
+Probably the toughest competition for lnav is the standard Unix utilities like
+tail, grep, less, and emacs/vim. It can be hard trying to convince people that
+these built-in commands that they've used for forever can be improved upon. The
+advanced features of lnav might even work against it since folks are expecting
+to have to learn a bunch of stuff to see any benefits.
+
+The reality is that there are quite a few "passive" features in lnav that can
+provide value with no effort required by the user. For example, lnav can easily
+replace 'tail -f', it's even shorter to type! Beyond the basic task of
+displaying new lines appended to a log file, you also get to see log messages
+from multiple files interleaved, the ability to scroll backwards, syntax
+highlighting, live searching, and so on. These basic features do not have the
+same "wow" factor as executing a SQL query over data automatically extracted
+from a log file, but they're the features that get used 90% of the time.
+
+Anyways, I think I'm gaining a new appreciation for marketing/sales...
diff --git a/docs/_posts/2013-11-01-mini-review-linux-user-magazine.md b/docs/_posts/2013-11-01-mini-review-linux-user-magazine.md
new file mode 100644
index 0000000..02d67f6
--- /dev/null
+++ b/docs/_posts/2013-11-01-mini-review-linux-user-magazine.md
@@ -0,0 +1,9 @@
+---
+layout: post
+title: "Mini-review of lnav in Linux User Magazine"
+date: 2013-09-10 00:00:00
+---
+
+The german magazine, [Linux User](http://www.linux-user.de/), has a
+mini-review of lnav v0.6.0 in their
+[November issue](http://www.linux-user.de/Downloads/LUCE/2013/lu-ce_2013-11.pdf)!
diff --git a/docs/_posts/2014-02-22-changes-to-the-scrollbar.md b/docs/_posts/2014-02-22-changes-to-the-scrollbar.md
new file mode 100644
index 0000000..2c9a21f
--- /dev/null
+++ b/docs/_posts/2014-02-22-changes-to-the-scrollbar.md
@@ -0,0 +1,21 @@
+---
+layout: post
+title: "Changes To The Scrollbar"
+date: 2014-02-22 00:00:00
+excerpt: Packing more information into the right scrollbar.
+---
+
+I've made some changes to the scrollbar that is shown on the right side of the
+display based on some feedback from users. The scroll area now has a single
+vertical line extending from the top to the bottom. Previously, this area would
+show log message characters and it wasn't very clear that the scroll bar
+existed. The line is colored based whether there are errors or warnings in that
+part of the log. The coloring should make it easier to see the distribution of
+errors across the whole log. Similarly, there are notches added to the left and
+right side of the line to show search hits and bookmarks, respectively. See the
+following screenshot to get an idea of what it looks like:
+
+![Screenshot of the redesigned scrollbar](/assets/images/scrollbar-change-2.png)
+
+These changes are currently only in the latest code from git. I'll be playing
+with things a bit more before making a release.
diff --git a/docs/_posts/2014-11-11-lofi-mode.md b/docs/_posts/2014-11-11-lofi-mode.md
new file mode 100644
index 0000000..b6ca628
--- /dev/null
+++ b/docs/_posts/2014-11-11-lofi-mode.md
@@ -0,0 +1,23 @@
+---
+layout: post
+title: Added "lo-fi" mode
+excerpt: An alternative way to copy displayed text.
+---
+
+*(This change is in v0.7.2+)*
+
+Copying text to the clipboard can be done by marking lines with the
+[bookmark hotkeys](https://docs.lnav.org/en/latest/hotkeys.html#bookmarks),
+like `m`, and then pressing `c`. Commands that write to a file,
+like [`:write-csv-to`](https://docs.lnav.org/en/latest/commands.html#write-csv-to-path)
+accept `/dev/clipboard` as a way to write to the clipboard. However, if the
+native clipboard isn't supported, or you're on an SSH connection, you can
+now switch to "lo-fi" mode. In "lo-fi" mode, lnav drops out of the curses
+display and prints the raw text to the terminal. You can switch to "lo-fi"
+mode in a view by pressing `CTRL-L`. For commands, you can use a dash `-`
+to switch to "lo-fi" and print to standard out.
+
+<script id="asciicast-fH4cdgugIJVcPQnSwCmTdaA7f"
+ src="https://asciinema.org/a/fH4cdgugIJVcPQnSwCmTdaA7f.js"
+ async>
+</script>
diff --git a/docs/_posts/2015-04-11-pretty-print-view.md b/docs/_posts/2015-04-11-pretty-print-view.md
new file mode 100644
index 0000000..6ea31b1
--- /dev/null
+++ b/docs/_posts/2015-04-11-pretty-print-view.md
@@ -0,0 +1,39 @@
+---
+layout: post
+title: "Pretty-print view in v0.7.3"
+date: 2015-04-11 00:00:00
+excerpt: Automatically reformat structured data with "SHIFT+P".
+---
+
+I wanted to call out the pretty-print feature in the latest release of lnav.
+This idea came from a coworker of Suresh who was having a hard time trying to
+read some unformatted XML in a log. They wanted the XML pretty-printed and were
+hoping that could be done by just piping the message to xmlpp or the like. So,
+first we implemented the 'pipe-to' and 'pipe-line-to' commands that will let you
+pipe log messages to a command and then display the result inside of lnav. That
+worked well enough, but pretty-printing is such a frequent operation that having
+to execute a command was kind of a pain. It would also be nice if it worked for
+a variety of text, like JSON or Python data. The solution we came up with was to
+leverage the existing code for parsing log messages to create a simple
+pretty-printer that should work for most data formats. Another benefit is that
+the log message does not have to be well-formed for the printer to work, any
+leading or trailing garbage shouldn't confuse things.
+
+As an example, here is a screenshot of the log message with the unformatted XML
+text with word-wrapping turned on:
+
+![Screenshot of raw XML](/assets/images/lnav-before-pretty.png)
+
+That's not very easy to read and it's hard to figure out the structure of the
+message. Now, here is that same message after pressing SHIFT+P to switch to the
+pretty-print view of lnav:
+
+![Screenshot of pretty-printed XML](/assets/images/lnav-after-pretty.png)
+
+The XML text is indented nicely and the usual syntax highlighting is applied.
+Also notice that lnav will automatically try to lookup the DNS name for IP
+addresses. Overall, I think it's a major improvement over the raw view.
+
+This is a pretty simple feature but I have found it quite useful in the couple
+weeks that it has been implemented. It's so useful that I'm kicking myself for
+not having thought of it before.
diff --git a/docs/_posts/2016-03-20-lnav-in-print.md b/docs/_posts/2016-03-20-lnav-in-print.md
new file mode 100644
index 0000000..73e7722
--- /dev/null
+++ b/docs/_posts/2016-03-20-lnav-in-print.md
@@ -0,0 +1,10 @@
+---
+layout: post
+title: "lnav in print"
+date: 2016-03-20 00:00:00
+---
+
+A [review of lnav](https://archive.org/details/Linux_User_Developer_162_2016_UK/page/n87/mode/2up)
+is in Linux User & Developer magazine issue 162:
+
+![Picture of lnav story in the magazine](/assets/images/linux-user-and-dev-mag.jpeg)
diff --git a/docs/_posts/2018-03-27-reveal-file-paths.md b/docs/_posts/2018-03-27-reveal-file-paths.md
new file mode 100644
index 0000000..01b31d8
--- /dev/null
+++ b/docs/_posts/2018-03-27-reveal-file-paths.md
@@ -0,0 +1,16 @@
+---
+layout: post
+title: "Reveal log message source file paths by pressing left-arrow"
+excerpt: Find out where log messages are coming from.
+---
+
+*(This change is in v0.8.4+)*
+
+If you want to know which file log messages are coming from, you can press the
+left-arrow to reveal the unique name and then press it again to reveal the
+full path.
+
+<script id="asciicast-ATHHpQiHVaTVxVRkhCv4ED7wT"
+ src="https://asciinema.org/a/ATHHpQiHVaTVxVRkhCv4ED7wT.js"
+ async>
+</script>
diff --git a/docs/_posts/2018-04-05-linux-magazine-tutorial.md b/docs/_posts/2018-04-05-linux-magazine-tutorial.md
new file mode 100644
index 0000000..9d868b5
--- /dev/null
+++ b/docs/_posts/2018-04-05-linux-magazine-tutorial.md
@@ -0,0 +1,9 @@
+---
+layout: post
+title: "Tutorial for lnav in Linux Magazine"
+date: 2018-04-05 00:00:00
+---
+
+Looks like there was an in-depth
+[tutorial on lnav in Linux Magazine](http://www.linux-magazine.com/Issues/2017/196/Tutorials-lnav).
+Unfortunately, I didn't notice until now and missed out on a hardcopy.
diff --git a/docs/_posts/2018-05-17-tags-and-comments.md b/docs/_posts/2018-05-17-tags-and-comments.md
new file mode 100644
index 0000000..c6f2fef
--- /dev/null
+++ b/docs/_posts/2018-05-17-tags-and-comments.md
@@ -0,0 +1,22 @@
+---
+layout: post
+title: "Support for tagging and commenting on log messages"
+excerpt: Annotate log messages with your thoughts.
+---
+
+*(This change is in v0.8.4+)*
+
+If you have been wanting to add notes to log messages you might be interested
+in, the new [`:comment`](https://docs.lnav.org/en/latest/commands.html#comment-text)
+and [`:tag`](https://docs.lnav.org/en/latest/commands.html#tag-tag) commands.
+These commands add a comment or tag(s) to the top message in the log view.
+The comments and tags are saved in the session, so they will be restored
+automatically when the file is reopened. These annotations can be searched
+for using the regular search prompt and can be accessed in the
+[log tables](https://docs.lnav.org/en/latest/sqlext.html#log-tables) using the
+`log_tags` and `log_comment` columns.
+
+<script id="asciicast-yRTcQd2VMv3QZVs5597OyAAxI"
+ src="https://asciinema.org/a/yRTcQd2VMv3QZVs5597OyAAxI.js"
+ async>
+</script>
diff --git a/docs/_posts/2018-11-9-visual-filter-editor.md b/docs/_posts/2018-11-9-visual-filter-editor.md
new file mode 100644
index 0000000..8fb59a4
--- /dev/null
+++ b/docs/_posts/2018-11-9-visual-filter-editor.md
@@ -0,0 +1,35 @@
+---
+layout: post
+title: "Visual filter editor"
+excerpt: A friendlier way to interact with filters.
+---
+
+*(This change is in v0.8.5+)*
+
+A visual filter editor has been added to make it easier to create, edit,
+enable, disable, and delete filters. In the log or text views, pressing `TAB`
+will open the filter editor panel. While the panel is in focus, the following
+hotkeys can be used:
+
+- `i` - Create an IN filter that will only show lines that match the given
+ regular expression.
+- `o` - Create an OUT filter that will hide lines that match the given regular
+ expression.
+- `Space` - Toggle the filter between being enabled and disabled.
+- `Enter` - Edit the selected filter.
+- `Shift+D` - Delete the filter.
+- `t` - Switch a filter from an IN to an OUT or vice-versa.
+- `f` - Globally enable or disable filtering.
+
+When editing a filter, the main view will highlight lines that portion of the
+lines that match the given regular expression:
+
+- Lines that match an OUT filter are highlighted with red;
+- Lines that match an IN filter are highlighted with green.
+
+You can also press `TAB` to complete words that are visible in the main view.
+
+<script id="asciicast-tcHeLbqVImRVcxWTYIrm3v6bw"
+ src="https://asciinema.org/a/tcHeLbqVImRVcxWTYIrm3v6bw.js"
+ async>
+</script>
diff --git a/docs/_posts/2019-05-08-themes.md b/docs/_posts/2019-05-08-themes.md
new file mode 100644
index 0000000..a145ca6
--- /dev/null
+++ b/docs/_posts/2019-05-08-themes.md
@@ -0,0 +1,28 @@
+---
+layout: post
+title: Support for Themes
+excerpt: Change the user-interface to your liking
+---
+
+*(This change is in v0.9.0+)*
+
+The lnav user-interface can now be customized by selecting one of the
+builtin themes, like
+[monocai](https://github.com/tstack/lnav/blob/master/src/themes/monocai.json)
+and
+[night-owl](https://github.com/tstack/lnav/blob/master/src/themes/night-owl.json),
+or
+[by defining your own theme](https://lnav.readthedocs.io/en/latest/config.html#theme-definitions).
+
+Selecting a theme can be done through the
+[`:config`](https://docs.lnav.org/en/latest/commands.html#config-option-value)
+command, like so:
+
+```
+:config /ui/theme monocai
+```
+
+Pressing `TAB` after the `/ui/theme ` will cycle through the available themes,
+like so:
+
+![Animation of lnav cycling through themes](/assets/images/lnav-theme-cycle.gif)
diff --git a/docs/_posts/2020-12-23-xpath-sql-function.md b/docs/_posts/2020-12-23-xpath-sql-function.md
new file mode 100644
index 0000000..b0a7657
--- /dev/null
+++ b/docs/_posts/2020-12-23-xpath-sql-function.md
@@ -0,0 +1,39 @@
+---
+layout: post
+title: Drilling down into XML snippets
+excerpt: The new "xpath()" table-valued SQL function.
+---
+
+*(This change is in [**v0.10.0+**](https://github.com/tstack/lnav/releases/tag/v0.10.0))*
+
+XML snippets in log messages can now be queried using the
+[`xpath()`](https://docs.lnav.org/en/latest/sqlext.html#xpath-xpath-xmldoc)
+table-valued SQL function. The function takes an
+[XPath](https://developer.mozilla.org/en-US/docs/Web/XPath), the XML snippet
+to be queried, and returns a table with the results of the XPath query.
+For example, given following XML document:
+
+```xml
+<msg>Hello, World!</msg>
+```
+
+Extracting the text value from the `msg` node can be done using the following
+query:
+
+```sql
+SELECT result FROM xpath('/msg/text()', '<msg>Hello, World!</msg>')
+```
+
+Of course, you won't typically be passing XML values as string literals, you
+will be extracting them from log messages. Assuming your log format already
+extracts the XML data, you can do a `SELECT` on the log format table and join
+that with the `xpath()` call. Since it can be challenging to construct a
+correct `xpath()` call, lnav will suggest calls for the nodes it finds in any
+XML log message fields. The following asciicast demonstrates this flow:
+
+<script id="asciicast-x89mrk8JPHBmB4pTbaZvTt8Do"
+ src="https://asciinema.org/a/x89mrk8JPHBmB4pTbaZvTt8Do.js"
+ async>
+</script>
+
+The implementation uses the [pugixml](https://pugixml.org) library.
diff --git a/docs/_posts/2021-05-03-tailing-remote-files.md b/docs/_posts/2021-05-03-tailing-remote-files.md
new file mode 100644
index 0000000..9d1d15c
--- /dev/null
+++ b/docs/_posts/2021-05-03-tailing-remote-files.md
@@ -0,0 +1,32 @@
+---
+layout: post
+title: Tailing files on remote hosts
+excerpt: Native support for tailing logs on machines accessible via SSH
+---
+
+*(This change is in [**v0.10.0+**](https://github.com/tstack/lnav/releases/tag/v0.10.0))*
+
+One of the new features in the upcoming v0.10.0 release of lnav is support
+for tailing log files on remote hosts via SSH. This feature allows you to
+view local files and files from multiple remote hosts alongside each other
+in the log view. The only setup required is to ensure the machines can be
+accessed via SSH without any interaction, meaning the host key must have
+been previously accepted and public key authentication configured. Opening
+a remote file is then simply a matter of specifying the location using the
+common scp syntax (i.e. `user@host:/path/to/file`).
+
+When lnav accesses a remote host, it transfers an agent (called the
+"tailer") to the host to handle file system requests from lnav. The agent
+is an [αcτµαlly pδrταblε εxεcµταblε](https://justine.lol/ape.html) that
+should run on most X86 Operating Systems. The agent will monitor the
+files of interest and synchronize their contents back to the host machine.
+In addition, the agent can be used to satisfy interactive requests for
+TAB-completion of remote file paths and previewing directory and file
+contents.
+
+The following asciicast shows lnav opening log files on MacOS and FreeBSD:
+
+<script id="asciicast-fblzf1Ir5Rr0b5wMGEJBb95ye"
+ src="https://asciinema.org/a/fblzf1Ir5Rr0b5wMGEJBb95ye.js"
+ async>
+</script>
diff --git a/docs/_posts/2022-05-01-regex101-integration.md b/docs/_posts/2022-05-01-regex101-integration.md
new file mode 100644
index 0000000..02011ce
--- /dev/null
+++ b/docs/_posts/2022-05-01-regex101-integration.md
@@ -0,0 +1,73 @@
+---
+layout: post
+title: Integration with regex101.com
+excerpt: Create/edit format files using regex101.com
+---
+
+*(This change will be in the upcoming v0.11.0 release)*
+
+Creating and updating format files for **lnav** can be a bit tedious and
+error-prone. To help streamline the process, an integration with regex101.com
+has been added. Now, you can create regular expressions for plaintext log
+files on https://regex101.com and then create a skeleton format file with a
+simple command. If you already have a format file that needs to be updated,
+you can push the regexes up to regex101, edit them with their interface, and
+then pull the changes back down as a format patch file.
+
+To further improve the experience of developing with format files, there is
+also work underway to improve error messages. Many messages should be clearer,
+more context is provided, and they should look nicer as well. For example, the
+following error is displayed when a format regex is not valid:
+
+![Screenshot of an error message](/assets/images/lnav-invalid-regex-error.png)
+
+## Management CLI
+
+The regex101 integration can be accessed through the new "management-mode CLI".
+This mode can be accessed by passing `-m` as the first option to **lnav**. The
+management CLI is organized as a series of nested commands. If you're not sure
+what to do at a given level, run the command as-is and the CLI should print out
+help text to guide you through the hierarchy of commands and required
+parameters.
+
+### Create a format from a regular expression
+
+The `regex101 import` command can be used to import a regular expression from
+regex101.com and create or patch a format file. The command takes the URL of
+the regex, the format name, and the name of the regex in the log format (
+defaults to "std" if not given). For example, the following command can be used
+to import the regex at "https://regex101.com/r/zpEnjV/2" into the format named "
+re101_example_log":
+
+```console
+$ lnav -m regex101 import https://regex101.com/r/zpEnjV/2 re101_example_log
+```
+
+If the import was successful, the path to the skeleton format file will be
+printed. You will most likely need to edit the file to fill in more details
+about your log format.
+
+### Editing an existing regular expression
+
+If you have a log format with a regex that needs to be updated, you can push
+the regex to regex101.com for editing with a command like (replace
+"myformat_log"/"std" with the name of your format and regex):
+
+```console
+$ lnav -m format myformat_log regex std regex101 push
+```
+
+Along with the regex, the format's samples will be added to the entry to ensure
+changes won't break existing matches. If the push was successful, the URL for
+the new regex101.com entry will be printed out. You can use that URL to edit the
+regex to your needs. Once you're done editing the regex, you can pull the
+changes down to a "patch" file using the following command:
+
+```console
+$ lnav -m format myformat_log regex std regex101 pull
+```
+
+The patch file will be evaluated after the original format file and override
+the values from the original. Once you are satisfied with the changes, you
+can move the contents of the patch file to the original file and delete the
+patch.
diff --git a/docs/_posts/2022-08-04-pretty-errors.md b/docs/_posts/2022-08-04-pretty-errors.md
new file mode 100644
index 0000000..b04c9e8
--- /dev/null
+++ b/docs/_posts/2022-08-04-pretty-errors.md
@@ -0,0 +1,46 @@
+---
+layout: post
+title: Pretty error messages
+excerpt: Error message improvements
+---
+
+*(This change will be in the upcoming v0.11.0 release)*
+
+Taking a page from compilers like rustc, I've spent some time
+improving error messages to make them look nicer and be more
+helpful. Fortunately, SQLite has improved their error reporting
+as well by adding
+[sqlite3_error_offset()](http://sqlite.com/c3ref/errcode.html).
+This function can point to the part of the SQL statement that
+was in error. As an example of the improvement, a SQL file
+that contained the following content:
+
+```sql
+
+-- This is a test
+SELECT abc),
+rtrim(def)
+FROM mytable;
+```
+
+Would report an error like the following on startup in v0.10.1:
+
+```text
+error:/Users/tstack/.config/lnav/formats/installed/test.sql:2:near ")": syntax error
+```
+
+Now, you will get a clearer error message with a syntax-highlighted
+code snippet and a pointer to the part of the code that has the
+problem:
+
+![Screenshot of a SQL error](/assets/images/lnav-sql-error-msg.png)
+
+Inside the TUI, a panel has been added at the bottom to display these
+long-form error messages. The panel will disappear after a short
+time or when input is received. Here is an example showing an error
+for an invalid regular expression:
+
+<script id="asciicast-lmYMLZsB02WbSO8VEz4aVLXa1"
+ src="https://asciinema.org/a/lmYMLZsB02WbSO8VEz4aVLXa1.js"
+ async>
+</script>
diff --git a/docs/_posts/2022-08-06-markdown-support.md b/docs/_posts/2022-08-06-markdown-support.md
new file mode 100644
index 0000000..4504bbd
--- /dev/null
+++ b/docs/_posts/2022-08-06-markdown-support.md
@@ -0,0 +1,33 @@
+---
+layout: post
+title: Markdown Support
+excerpt: A side effect of fancier help text
+---
+
+*(This change will be in the upcoming v0.11.0 release)*
+
+As part of the effort to polish the lnav TUI, I wanted to make the builtin
+help text look a bit nicer. The current help text is a plain text file with
+some ANSI escape sequences for colors. It's not easy to write or read. Since
+Markdown has become a dominant way to write this type of document, I figured
+I could use that and have the side benefit of allowing lnav to read Markdown
+docs. Fortunately, the [MD4C](https://github.com/mity/md4c) library exists.
+This library provides a nice event-driven parser for documents instead of
+just converting directly to HTML. In addition, document structure is now
+shown/navigable through the new breadcrumb bar at the top. I think the
+result is pretty nice:
+
+<script id="asciicast-2hx3UiyzOHQXBQOBf31ztKvHc"
+ src="https://asciinema.org/a/2hx3UiyzOHQXBQOBf31ztKvHc.js"
+ async>
+</script>
+
+## Viewing Markdown Files
+
+Files with an `.md` suffix will be considered as Markdown and will be
+parsed as such. As an example, here is lnav displaying its README.md file:
+
+<script id="asciicast-iw4rwddZNGCe3v8DyOfItERG9"
+ src="https://asciinema.org/a/iw4rwddZNGCe3v8DyOfItERG9.js"
+ async>
+</script>
diff --git a/docs/_posts/2022-09-01-playground.md b/docs/_posts/2022-09-01-playground.md
new file mode 100644
index 0000000..3cbaa11
--- /dev/null
+++ b/docs/_posts/2022-09-01-playground.md
@@ -0,0 +1,27 @@
+---
+layout: post
+title: Playground and Tutorial
+excerpt: Try lnav without having to install anything
+---
+
+To make it easier to try out **lnav**, I've deployed an ssh-based playground
+and tutorial. You can SSH as follows to try them out:
+
+```console
+$ ssh playground@demo.lnav.org
+$ ssh tutorial1@demo.lnav.org
+```
+
+<script id="asciicast-HiiUMMmRKZh0uCVKm1Uw8WLlw"
+ src="https://asciinema.org/a/HiiUMMmRKZh0uCVKm1Uw8WLlw.js"
+ async>
+</script>
+
+The playground has a couple of example logs to play with. The tutorial
+tries to guide you through the basics of navigating log files with lnav.
+The server is running on the free-tier of [fly.io](https://fly.io), so
+please be kind.
+
+This effort was inspired by the `git.charm.sh` SSH server and by the
+[fasterthanli.me](https://fasterthanli.me/articles/remote-development-with-rust-on-fly-io)
+post on doing remote development on fly.io.
diff --git a/docs/_posts/2022-09-24-vscode-extension.md b/docs/_posts/2022-09-24-vscode-extension.md
new file mode 100644
index 0000000..6c7b78f
--- /dev/null
+++ b/docs/_posts/2022-09-24-vscode-extension.md
@@ -0,0 +1,21 @@
+---
+layout: post
+title: VSCode Extension for lnav
+excerpt: Syntax highlighting for lnav scripts
+---
+
+I've published a simple [Visual Studio Code extension for lnav](
+https://marketplace.visualstudio.com/items?itemName=lnav.lnav)
+that adds syntax highlighting for scripts. The following is a
+screenshot showing the `dhclient-summary.lnav` script that is
+builtin:
+
+![Screenshot of an lnav script](/assets/images/lnav-vscode-extension.png)
+
+The lnav commands, those prefixed with colons, are marked as
+keywords and the SQL blocks are treated as an embedded language
+and highlighted accordingly.
+
+If people find this useful, we can take it further and add
+support for running the current script/snippet in a new lnav
+process or even talking to an existing one.
diff --git a/docs/_posts/2023-06-23-cursor-mode.md b/docs/_posts/2023-06-23-cursor-mode.md
new file mode 100644
index 0000000..c0a9f1d
--- /dev/null
+++ b/docs/_posts/2023-06-23-cursor-mode.md
@@ -0,0 +1,30 @@
+---
+layout: post
+title: Cursor Mode
+excerpt: Move around the main view using a cursor
+---
+
+*(This change is in [**v0.11.2+**](https://github.com/tstack/lnav/releases/tag/v0.11.2-rc3))*
+
+The major change in the v0.11.2 release is the addition of a "cursor mode"
+for the main view. Instead of focusing on the top line for interacting
+with **lnav**, a cursor line is displayed and interactions focus on that.
+The arrow keys and the hotkeys that jump between bookmarks, like search
+hits and errors, now move the focused line instead of scrolling the view.
+To help provide context for what you're looking at, large jumps will keep
+the focused line in the middle of the view. Smaller movements, like
+moving the cursor above the top line, will scroll the view a small amount
+so as not to be jarring.
+
+You can enable/disable cursor mode interactively by pressing `CTRL` + `x`.
+Or, you can permanently enable cursor mode by running the following
+`:config` command:
+
+```
+:config /ui/movement/mode cursor
+```
+
+<script async
+ id="asciicast-d94CmxlGM01I0L5HNn9qDn917"
+ src="https://asciinema.org/a/d94CmxlGM01I0L5HNn9qDn917.js">
+</script>
diff --git a/docs/assets/images/favicon.png b/docs/assets/images/favicon.png
new file mode 100644
index 0000000..e126db3
--- /dev/null
+++ b/docs/assets/images/favicon.png
Binary files differ
diff --git a/docs/assets/images/favicon.svg b/docs/assets/images/favicon.svg
new file mode 100644
index 0000000..6c62941
--- /dev/null
+++ b/docs/assets/images/favicon.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="1000" height="1000"><g transform="matrix(38.46153846153846,0,0,38.46153846153846,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:svgjs="http://svgjs.com/svgjs" width="26" height="26"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 26 26">
+ <path d="M 13 0.1875 C 5.925781 0.1875 0.1875 5.925781 0.1875 13 C 0.1875 20.074219 5.925781 25.8125 13 25.8125 C 20.074219 25.8125 25.8125 20.074219 25.8125 13 C 25.8125 5.925781 20.074219 0.1875 13 0.1875 Z M 11.9375 3.25 C 11.785156 3.464844 11.6875 3.71875 11.6875 4 C 11.6875 4.734375 12.265625 5.3125 13 5.3125 C 13.734375 5.3125 14.3125 4.734375 14.3125 4 C 14.3125 3.71875 14.214844 3.464844 14.0625 3.25 C 18.625 3.746094 22.253906 7.375 22.75 11.9375 C 22.535156 11.785156 22.28125 11.6875 22 11.6875 C 21.265625 11.6875 20.6875 12.265625 20.6875 13 C 20.6875 13.734375 21.265625 14.3125 22 14.3125 C 22.28125 14.3125 22.535156 14.214844 22.75 14.0625 C 22.253906 18.625 18.625 22.253906 14.0625 22.75 C 14.214844 22.535156 14.3125 22.28125 14.3125 22 C 14.3125 21.265625 13.734375 20.6875 13 20.6875 C 12.265625 20.6875 11.6875 21.265625 11.6875 22 C 11.6875 22.28125 11.785156 22.535156 11.9375 22.75 C 7.375 22.253906 3.746094 18.625 3.25 14.0625 C 3.464844 14.214844 3.71875 14.3125 4 14.3125 C 4.734375 14.3125 5.3125 13.734375 5.3125 13 C 5.3125 12.265625 4.734375 11.6875 4 11.6875 C 3.71875 11.6875 3.464844 11.785156 3.25 11.9375 C 3.746094 7.375 7.375 3.746094 11.9375 3.25 Z M 18.3125 7.6875 L 11.375 11.375 L 7.6875 18.3125 L 14.625 14.625 Z M 13 11.75 C 13.695313 11.75 14.25 12.304688 14.25 13 C 14.25 13.695313 13.695313 14.25 13 14.25 C 12.304688 14.25 11.75 13.695313 11.75 13 C 11.75 12.304688 12.304688 11.75 13 11.75 Z"></path>
+</svg></svg></g></svg> \ No newline at end of file
diff --git a/docs/assets/images/linux-user-and-dev-mag.jpeg b/docs/assets/images/linux-user-and-dev-mag.jpeg
new file mode 100644
index 0000000..c61e8e0
--- /dev/null
+++ b/docs/assets/images/linux-user-and-dev-mag.jpeg
Binary files differ
diff --git a/docs/assets/images/lnav-after-pretty.png b/docs/assets/images/lnav-after-pretty.png
new file mode 100644
index 0000000..3983713
--- /dev/null
+++ b/docs/assets/images/lnav-after-pretty.png
Binary files differ
diff --git a/docs/assets/images/lnav-before-pretty.png b/docs/assets/images/lnav-before-pretty.png
new file mode 100644
index 0000000..3100091
--- /dev/null
+++ b/docs/assets/images/lnav-before-pretty.png
Binary files differ
diff --git a/docs/assets/images/lnav-front-page.png b/docs/assets/images/lnav-front-page.png
new file mode 100644
index 0000000..20483dd
--- /dev/null
+++ b/docs/assets/images/lnav-front-page.png
Binary files differ
diff --git a/docs/assets/images/lnav-hist.png b/docs/assets/images/lnav-hist.png
new file mode 100644
index 0000000..f3e92eb
--- /dev/null
+++ b/docs/assets/images/lnav-hist.png
Binary files differ
diff --git a/docs/assets/images/lnav-invalid-regex-error.png b/docs/assets/images/lnav-invalid-regex-error.png
new file mode 100644
index 0000000..d67d53c
--- /dev/null
+++ b/docs/assets/images/lnav-invalid-regex-error.png
Binary files differ
diff --git a/docs/assets/images/lnav-multi-file2.png b/docs/assets/images/lnav-multi-file2.png
new file mode 100644
index 0000000..3498fbc
--- /dev/null
+++ b/docs/assets/images/lnav-multi-file2.png
Binary files differ
diff --git a/docs/assets/images/lnav-query.png b/docs/assets/images/lnav-query.png
new file mode 100644
index 0000000..470fdc6
--- /dev/null
+++ b/docs/assets/images/lnav-query.png
Binary files differ
diff --git a/docs/assets/images/lnav-sql-error-msg.png b/docs/assets/images/lnav-sql-error-msg.png
new file mode 100644
index 0000000..526f6c3
--- /dev/null
+++ b/docs/assets/images/lnav-sql-error-msg.png
Binary files differ
diff --git a/docs/assets/images/lnav-syntax-highlight.gif b/docs/assets/images/lnav-syntax-highlight.gif
new file mode 100644
index 0000000..6f0ce13
--- /dev/null
+++ b/docs/assets/images/lnav-syntax-highlight.gif
Binary files differ
diff --git a/docs/assets/images/lnav-syslog-thumb.png b/docs/assets/images/lnav-syslog-thumb.png
new file mode 100644
index 0000000..e1f0533
--- /dev/null
+++ b/docs/assets/images/lnav-syslog-thumb.png
Binary files differ
diff --git a/docs/assets/images/lnav-syslog.png b/docs/assets/images/lnav-syslog.png
new file mode 100644
index 0000000..b676931
--- /dev/null
+++ b/docs/assets/images/lnav-syslog.png
Binary files differ
diff --git a/docs/assets/images/lnav-tab-complete.gif b/docs/assets/images/lnav-tab-complete.gif
new file mode 100644
index 0000000..de49539
--- /dev/null
+++ b/docs/assets/images/lnav-tab-complete.gif
Binary files differ
diff --git a/docs/assets/images/lnav-theme-cycle.gif b/docs/assets/images/lnav-theme-cycle.gif
new file mode 100644
index 0000000..f21281f
--- /dev/null
+++ b/docs/assets/images/lnav-theme-cycle.gif
Binary files differ
diff --git a/docs/assets/images/lnav-vscode-extension.png b/docs/assets/images/lnav-vscode-extension.png
new file mode 100644
index 0000000..f41760f
--- /dev/null
+++ b/docs/assets/images/lnav-vscode-extension.png
Binary files differ
diff --git a/docs/assets/images/scrollbar-change-2.png b/docs/assets/images/scrollbar-change-2.png
new file mode 100644
index 0000000..c7671c1
--- /dev/null
+++ b/docs/assets/images/scrollbar-change-2.png
Binary files differ
diff --git a/docs/assets/js/codeblock.js b/docs/assets/js/codeblock.js
new file mode 100644
index 0000000..b41e902
--- /dev/null
+++ b/docs/assets/js/codeblock.js
@@ -0,0 +1,23 @@
+var codeBlocks = document.querySelectorAll('pre.highlight');
+
+codeBlocks.forEach(function (codeBlock) {
+ var copyButton = document.createElement('button');
+ copyButton.className = 'copy';
+ copyButton.type = 'button';
+ copyButton.ariaLabel = 'Copy code to clipboard';
+ copyButton.innerText = 'Copy';
+
+ codeBlock.append(copyButton);
+
+ copyButton.addEventListener('click', function () {
+ var code = codeBlock.querySelector('code').innerText.trim();
+ window.navigator.clipboard.writeText(code);
+
+ copyButton.innerText = 'Copied';
+ var fourSeconds = 4000;
+
+ setTimeout(function () {
+ copyButton.innerText = 'Copy';
+ }, fourSeconds);
+ });
+});
diff --git a/docs/assets/main.scss b/docs/assets/main.scss
new file mode 100644
index 0000000..24d9dbd
--- /dev/null
+++ b/docs/assets/main.scss
@@ -0,0 +1,83 @@
+---
+---
+
+@import "{{ site.theme }}";
+
+#playground-box {
+ font-size: x-large;
+ border-radius: 25px;
+ background: #8d8;
+ padding: 20px;
+ display: inline-block;
+}
+
+#playground-box h4 {
+ margin-bottom: 0;
+}
+
+#playground-box code {
+ padding-left: 20px;
+ background: #444;
+ border-color: #444;
+}
+
+#playground-box code a {
+ padding-right: 20px;
+ color: white;
+}
+
+#playground-box .prompt {
+ color: #4f4;
+}
+
+
+pre.highlight {
+ padding: 8px 12px;
+ position: relative;
+
+ // override skeleton styles
+ > code {
+ border: 0;
+ overflow-x: auto;
+ padding-right: 0;
+ padding-left: 0;
+ }
+
+ &.highlight {
+ border-left: 15px solid #35383c;
+ color: #c1c2c3;
+ overflow: auto;
+ white-space: pre;
+ word-wrap: normal;
+
+ &,
+ code {
+ background-color: #222;
+ font-size: 14px;
+ }
+ }
+
+ // code to clipboard
+ .copy {
+ color: #4AF626;
+ position: absolute;
+ right: 1.2rem;
+ top: 1.2rem;
+ opacity: 0;
+
+ &:active,
+ &:focus,
+ &:hover {
+ background: rgba(0, 0, 0, 0.7);
+ opacity: 1;
+ }
+ }
+
+ &:active .copy,
+ &:focus .copy,
+ &:hover .copy {
+ background: rgba(0, 0, 0, 0.7);
+ opacity: 1;
+ }
+}
+
diff --git a/docs/conf.py.in b/docs/conf.py.in
new file mode 100644
index 0000000..b81e3d9
--- /dev/null
+++ b/docs/conf.py.in
@@ -0,0 +1,6 @@
+DOXYFILE = 'Doxyfile'
+
+LINKS_NAVBAR1 = [
+ (None, 'pages', [(None, 'about')]),
+ (None, 'namespaces', []),
+]
diff --git a/docs/index.markdown b/docs/index.markdown
new file mode 100644
index 0000000..eadb501
--- /dev/null
+++ b/docs/index.markdown
@@ -0,0 +1,58 @@
+---
+
+# Feel free to add content and custom Front Matter to this file.
+
+# To modify the layout, see https://jekyllrb.com/docs/themes/#overriding-theme-defaults
+
+layout: top
+---
+
+![Screenshot of lnav](/assets/images/lnav-front-page.png){:
+style="float: right; max-width: 50%"}
+
+<div id="top-description">
+An advanced log file viewer for the small-scale
+</div>
+
+<div id="intro">
+<p>Watch and analyze your log files from a terminal.</p>
+
+<p>No server. No setup. Still featureful.</p>
+
+<div id="playground-box">
+<h4>Try it out:</h4>
+
+<code>
+<span class="prompt">$</span>
+<a href="ssh://playground@demo.lnav.org">ssh playground@demo.lnav.org</a>
+</code>
+</div>
+</div>
+
+<div class="dlrow">
+<dl>
+<dt>In Your Terminal</dt>
+<dd>
+Many logging tools, like Splunk, provide great features but are optimized for
+large-scale deployments. They require installing and configuring servers
+before they can be effectively used. There is still a need for a robust log
+file analyzer for the terminal.
+</dd>
+</dl>
+
+<dl>
+<dt>Easy to Use</dt>
+<dd>
+Just point lnav to a directory and it will take care of the rest. File formats
+are automatically detected and compressed files are unpacked on the fly.
+</dd>
+</dl>
+
+<dl>
+<dt>Improved Presentation</dt>
+<dd>
+Log files are a wealth of information, lnav can help highlight the parts that
+are important and filter out the noise.
+</dd>
+</dl>
+</div>
diff --git a/docs/lnav-architecture.png b/docs/lnav-architecture.png
new file mode 100644
index 0000000..dc9fae9
--- /dev/null
+++ b/docs/lnav-architecture.png
Binary files differ
diff --git a/docs/lnav-tui.png b/docs/lnav-tui.png
new file mode 100644
index 0000000..bcb87ca
--- /dev/null
+++ b/docs/lnav-tui.png
Binary files differ
diff --git a/docs/pages/about.dox b/docs/pages/about.dox
new file mode 100644
index 0000000..2efbda9
--- /dev/null
+++ b/docs/pages/about.dox
@@ -0,0 +1,7 @@
+/**
+ * @page about About
+ * @section about-doxygen Doxygen documentation
+ * This page is auto generated using
+ * <a href="https://www.doxygen.nl/">Doxygen</a>, making use of some useful
+ * <a href="https://www.doxygen.nl/manual/commands.html">special commands</a>.
+ */
diff --git a/docs/requirements.txt b/docs/requirements.txt
new file mode 100644
index 0000000..1d29e34
--- /dev/null
+++ b/docs/requirements.txt
@@ -0,0 +1,6 @@
+sphinx>=4.3.0
+sphinx-copybutton
+sphinx-jsonschema
+sphinx-prompt
+pathlib
+sphinx-rtd-theme>=0.5.1
diff --git a/docs/schemas/config-v1.schema.json b/docs/schemas/config-v1.schema.json
new file mode 100644
index 0000000..e2285cc
--- /dev/null
+++ b/docs/schemas/config-v1.schema.json
@@ -0,0 +1,804 @@
+{
+ "$id": "https://lnav.org/schemas/config-v1.schema.json",
+ "title": "https://lnav.org/schemas/config-v1.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "$schema": {
+ "title": "/$schema",
+ "description": "The URI that specifies the schema that describes this type of file",
+ "type": "string",
+ "examples": [
+ "https://lnav.org/schemas/config-v1.schema.json"
+ ]
+ },
+ "tuning": {
+ "description": "Internal settings",
+ "title": "/tuning",
+ "type": "object",
+ "properties": {
+ "archive-manager": {
+ "description": "Settings related to opening archive files",
+ "title": "/tuning/archive-manager",
+ "type": "object",
+ "properties": {
+ "min-free-space": {
+ "title": "/tuning/archive-manager/min-free-space",
+ "description": "The minimum free space, in bytes, to maintain when unpacking archives",
+ "type": "integer",
+ "minimum": 0
+ },
+ "cache-ttl": {
+ "title": "/tuning/archive-manager/cache-ttl",
+ "description": "The time-to-live for unpacked archives, expressed as a duration (e.g. '3d' for three days)",
+ "type": "string",
+ "examples": [
+ "3d",
+ "12h"
+ ]
+ }
+ },
+ "additionalProperties": false
+ },
+ "file-vtab": {
+ "description": "Settings related to the lnav_file virtual-table",
+ "title": "/tuning/file-vtab",
+ "type": "object",
+ "properties": {
+ "max-content-size": {
+ "title": "/tuning/file-vtab/max-content-size",
+ "description": "The maximum allowed file size for the content column",
+ "type": "integer",
+ "minimum": 0
+ }
+ },
+ "additionalProperties": false
+ },
+ "logfile": {
+ "description": "Settings related to log files",
+ "title": "/tuning/logfile",
+ "type": "object",
+ "properties": {
+ "max-unrecognized-lines": {
+ "title": "/tuning/logfile/max-unrecognized-lines",
+ "description": "The maximum number of lines in a file to use when detecting the format",
+ "type": "integer",
+ "minimum": 1
+ }
+ },
+ "additionalProperties": false
+ },
+ "remote": {
+ "description": "Settings related to remote file support",
+ "title": "/tuning/remote",
+ "type": "object",
+ "properties": {
+ "cache-ttl": {
+ "title": "/tuning/remote/cache-ttl",
+ "description": "The time-to-live for files copied from remote hosts, expressed as a duration (e.g. '3d' for three days)",
+ "type": "string",
+ "examples": [
+ "3d",
+ "12h"
+ ]
+ },
+ "ssh": {
+ "description": "Settings related to the ssh command used to contact remote machines",
+ "title": "/tuning/remote/ssh",
+ "type": "object",
+ "properties": {
+ "command": {
+ "title": "/tuning/remote/ssh/command",
+ "description": "The SSH command to execute",
+ "type": "string"
+ },
+ "transfer-command": {
+ "title": "/tuning/remote/ssh/transfer-command",
+ "description": "Command executed on the remote host when transferring the file",
+ "type": "string"
+ },
+ "start-command": {
+ "title": "/tuning/remote/ssh/start-command",
+ "description": "Command executed on the remote host to start the tailer",
+ "type": "string"
+ },
+ "flags": {
+ "title": "/tuning/remote/ssh/flags",
+ "description": "The flags to pass to the SSH command",
+ "type": "string"
+ },
+ "options": {
+ "description": "The options to pass to the SSH command",
+ "title": "/tuning/remote/ssh/options",
+ "type": "object",
+ "patternProperties": {
+ "(\\w+)": {
+ "title": "/tuning/remote/ssh/options/<option_name>",
+ "description": "Set an option to be passed to the SSH command",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "config": {
+ "description": "The ssh_config options to pass to SSH with the -o option",
+ "title": "/tuning/remote/ssh/config",
+ "type": "object",
+ "patternProperties": {
+ "(\\w+)": {
+ "title": "/tuning/remote/ssh/config/<config_name>",
+ "description": "Set an SSH configuration value",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "clipboard": {
+ "description": "Settings related to the clipboard",
+ "title": "/tuning/clipboard",
+ "type": "object",
+ "properties": {
+ "impls": {
+ "description": "Clipboard implementations",
+ "title": "/tuning/clipboard/impls",
+ "type": "object",
+ "patternProperties": {
+ "([\\w\\-]+)": {
+ "description": "Clipboard implementation",
+ "title": "/tuning/clipboard/impls/<clipboard_impl_name>",
+ "type": "object",
+ "properties": {
+ "test": {
+ "title": "/tuning/clipboard/impls/<clipboard_impl_name>/test",
+ "description": "The command that checks",
+ "type": "string",
+ "examples": [
+ "command -v pbcopy"
+ ]
+ },
+ "general": {
+ "description": "Commands to work with the general clipboard",
+ "title": "/tuning/clipboard/impls/<clipboard_impl_name>/general",
+ "$ref": "#/definitions/clip-commands"
+ },
+ "find": {
+ "description": "Commands to work with the find clipboard",
+ "title": "/tuning/clipboard/impls/<clipboard_impl_name>/find",
+ "$ref": "#/definitions/clip-commands"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "ui": {
+ "description": "User-interface settings",
+ "title": "/ui",
+ "type": "object",
+ "properties": {
+ "clock-format": {
+ "title": "/ui/clock-format",
+ "description": "The format for the clock displayed in the top-left corner using strftime(3) conversions",
+ "type": "string",
+ "examples": [
+ "%a %b %d %H:%M:%S %Z"
+ ]
+ },
+ "dim-text": {
+ "title": "/ui/dim-text",
+ "description": "Reduce the brightness of text (useful for xterms). This setting can be useful when running in an xterm where the white color is very bright.",
+ "type": "boolean"
+ },
+ "default-colors": {
+ "title": "/ui/default-colors",
+ "description": "Use default terminal background and foreground colors instead of black and white for all text coloring. This setting can be useful when transparent background or alternate color theme terminal is used.",
+ "type": "boolean"
+ },
+ "keymap": {
+ "title": "/ui/keymap",
+ "description": "The name of the keymap to use.",
+ "type": "string"
+ },
+ "theme": {
+ "title": "/ui/theme",
+ "description": "The name of the theme to use.",
+ "type": "string"
+ },
+ "theme-defs": {
+ "description": "Theme definitions.",
+ "title": "/ui/theme-defs",
+ "type": "object",
+ "patternProperties": {
+ "([\\w\\-]+)": {
+ "description": "Theme definitions",
+ "title": "/ui/theme-defs/<theme_name>",
+ "type": "object",
+ "properties": {
+ "vars": {
+ "description": "Variables definitions that are used in this theme.",
+ "title": "/ui/theme-defs/<theme_name>/vars",
+ "type": "object",
+ "patternProperties": {
+ "(\\w+)": {
+ "title": "/ui/theme-defs/<theme_name>/vars/<var_name>",
+ "description": "A theme variable definition",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "styles": {
+ "description": "Styles for log messages.",
+ "title": "/ui/theme-defs/<theme_name>/styles",
+ "type": "object",
+ "properties": {
+ "identifier": {
+ "description": "Styling for identifiers in logs",
+ "title": "/ui/theme-defs/<theme_name>/styles/identifier",
+ "$ref": "#/definitions/style"
+ },
+ "text": {
+ "description": "Styling for plain text",
+ "title": "/ui/theme-defs/<theme_name>/styles/text",
+ "$ref": "#/definitions/style"
+ },
+ "alt-text": {
+ "description": "Styling for plain text when alternating",
+ "title": "/ui/theme-defs/<theme_name>/styles/alt-text",
+ "$ref": "#/definitions/style"
+ },
+ "error": {
+ "description": "Styling for error messages",
+ "title": "/ui/theme-defs/<theme_name>/styles/error",
+ "$ref": "#/definitions/style"
+ },
+ "ok": {
+ "description": "Styling for success messages",
+ "title": "/ui/theme-defs/<theme_name>/styles/ok",
+ "$ref": "#/definitions/style"
+ },
+ "info": {
+ "description": "Styling for informational messages",
+ "title": "/ui/theme-defs/<theme_name>/styles/info",
+ "$ref": "#/definitions/style"
+ },
+ "warning": {
+ "description": "Styling for warning messages",
+ "title": "/ui/theme-defs/<theme_name>/styles/warning",
+ "$ref": "#/definitions/style"
+ },
+ "hidden": {
+ "description": "Styling for hidden fields in logs",
+ "title": "/ui/theme-defs/<theme_name>/styles/hidden",
+ "$ref": "#/definitions/style"
+ },
+ "cursor-line": {
+ "description": "Styling for the cursor line in the main view",
+ "title": "/ui/theme-defs/<theme_name>/styles/cursor-line",
+ "$ref": "#/definitions/style"
+ },
+ "adjusted-time": {
+ "description": "Styling for timestamps that have been adjusted",
+ "title": "/ui/theme-defs/<theme_name>/styles/adjusted-time",
+ "$ref": "#/definitions/style"
+ },
+ "skewed-time": {
+ "description": "Styling for timestamps that are different from the received time",
+ "title": "/ui/theme-defs/<theme_name>/styles/skewed-time",
+ "$ref": "#/definitions/style"
+ },
+ "offset-time": {
+ "description": "Styling for hidden fields",
+ "title": "/ui/theme-defs/<theme_name>/styles/offset-time",
+ "$ref": "#/definitions/style"
+ },
+ "invalid-msg": {
+ "description": "Styling for invalid log messages",
+ "title": "/ui/theme-defs/<theme_name>/styles/invalid-msg",
+ "$ref": "#/definitions/style"
+ },
+ "popup": {
+ "description": "Styling for popup windows",
+ "title": "/ui/theme-defs/<theme_name>/styles/popup",
+ "$ref": "#/definitions/style"
+ },
+ "focused": {
+ "description": "Styling for a focused row in a list view",
+ "title": "/ui/theme-defs/<theme_name>/styles/focused",
+ "$ref": "#/definitions/style"
+ },
+ "disabled-focused": {
+ "description": "Styling for a disabled focused row in a list view",
+ "title": "/ui/theme-defs/<theme_name>/styles/disabled-focused",
+ "$ref": "#/definitions/style"
+ },
+ "scrollbar": {
+ "description": "Styling for scrollbars",
+ "title": "/ui/theme-defs/<theme_name>/styles/scrollbar",
+ "$ref": "#/definitions/style"
+ },
+ "h1": {
+ "description": "Styling for top-level headers",
+ "title": "/ui/theme-defs/<theme_name>/styles/h1",
+ "$ref": "#/definitions/style"
+ },
+ "h2": {
+ "description": "Styling for 2nd-level headers",
+ "title": "/ui/theme-defs/<theme_name>/styles/h2",
+ "$ref": "#/definitions/style"
+ },
+ "h3": {
+ "description": "Styling for 3rd-level headers",
+ "title": "/ui/theme-defs/<theme_name>/styles/h3",
+ "$ref": "#/definitions/style"
+ },
+ "h4": {
+ "description": "Styling for 4th-level headers",
+ "title": "/ui/theme-defs/<theme_name>/styles/h4",
+ "$ref": "#/definitions/style"
+ },
+ "h5": {
+ "description": "Styling for 5th-level headers",
+ "title": "/ui/theme-defs/<theme_name>/styles/h5",
+ "$ref": "#/definitions/style"
+ },
+ "h6": {
+ "description": "Styling for 6th-level headers",
+ "title": "/ui/theme-defs/<theme_name>/styles/h6",
+ "$ref": "#/definitions/style"
+ },
+ "hr": {
+ "description": "Styling for horizontal rules",
+ "title": "/ui/theme-defs/<theme_name>/styles/hr",
+ "$ref": "#/definitions/style"
+ },
+ "hyperlink": {
+ "description": "Styling for hyperlinks",
+ "title": "/ui/theme-defs/<theme_name>/styles/hyperlink",
+ "$ref": "#/definitions/style"
+ },
+ "list-glyph": {
+ "description": "Styling for glyphs that prefix a list item",
+ "title": "/ui/theme-defs/<theme_name>/styles/list-glyph",
+ "$ref": "#/definitions/style"
+ },
+ "breadcrumb": {
+ "description": "Styling for the separator between breadcrumbs",
+ "title": "/ui/theme-defs/<theme_name>/styles/breadcrumb",
+ "$ref": "#/definitions/style"
+ },
+ "table-border": {
+ "description": "Styling for table borders",
+ "title": "/ui/theme-defs/<theme_name>/styles/table-border",
+ "$ref": "#/definitions/style"
+ },
+ "table-header": {
+ "description": "Styling for table headers",
+ "title": "/ui/theme-defs/<theme_name>/styles/table-header",
+ "$ref": "#/definitions/style"
+ },
+ "quote-border": {
+ "description": "Styling for quoted-block borders",
+ "title": "/ui/theme-defs/<theme_name>/styles/quote-border",
+ "$ref": "#/definitions/style"
+ },
+ "quoted-text": {
+ "description": "Styling for quoted text blocks",
+ "title": "/ui/theme-defs/<theme_name>/styles/quoted-text",
+ "$ref": "#/definitions/style"
+ },
+ "footnote-border": {
+ "description": "Styling for footnote borders",
+ "title": "/ui/theme-defs/<theme_name>/styles/footnote-border",
+ "$ref": "#/definitions/style"
+ },
+ "footnote-text": {
+ "description": "Styling for footnote text",
+ "title": "/ui/theme-defs/<theme_name>/styles/footnote-text",
+ "$ref": "#/definitions/style"
+ },
+ "snippet-border": {
+ "description": "Styling for snippet borders",
+ "title": "/ui/theme-defs/<theme_name>/styles/snippet-border",
+ "$ref": "#/definitions/style"
+ }
+ },
+ "additionalProperties": false
+ },
+ "syntax-styles": {
+ "description": "Styles for syntax highlighting in text files.",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles",
+ "type": "object",
+ "properties": {
+ "quoted-code": {
+ "description": "Styling for quoted code blocks",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/quoted-code",
+ "$ref": "#/definitions/style"
+ },
+ "code-border": {
+ "description": "Styling for quoted-code borders",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/code-border",
+ "$ref": "#/definitions/style"
+ },
+ "keyword": {
+ "description": "Styling for keywords in source files",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/keyword",
+ "$ref": "#/definitions/style"
+ },
+ "string": {
+ "description": "Styling for single/double-quoted strings in text",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/string",
+ "$ref": "#/definitions/style"
+ },
+ "comment": {
+ "description": "Styling for comments in source files",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/comment",
+ "$ref": "#/definitions/style"
+ },
+ "doc-directive": {
+ "description": "Styling for documentation directives in source files",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/doc-directive",
+ "$ref": "#/definitions/style"
+ },
+ "variable": {
+ "description": "Styling for variables in text",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/variable",
+ "$ref": "#/definitions/style"
+ },
+ "symbol": {
+ "description": "Styling for symbols in source files",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/symbol",
+ "$ref": "#/definitions/style"
+ },
+ "number": {
+ "description": "Styling for numbers in source files",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/number",
+ "$ref": "#/definitions/style"
+ },
+ "re-special": {
+ "description": "Styling for special characters in regular expressions",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/re-special",
+ "$ref": "#/definitions/style"
+ },
+ "re-repeat": {
+ "description": "Styling for repeats in regular expressions",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/re-repeat",
+ "$ref": "#/definitions/style"
+ },
+ "diff-delete": {
+ "description": "Styling for deleted lines in diffs",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/diff-delete",
+ "$ref": "#/definitions/style"
+ },
+ "diff-add": {
+ "description": "Styling for added lines in diffs",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/diff-add",
+ "$ref": "#/definitions/style"
+ },
+ "diff-section": {
+ "description": "Styling for diffs",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/diff-section",
+ "$ref": "#/definitions/style"
+ },
+ "spectrogram-low": {
+ "description": "Styling for the lower threshold values in the spectrogram view",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/spectrogram-low",
+ "$ref": "#/definitions/style"
+ },
+ "spectrogram-medium": {
+ "description": "Styling for the medium threshold values in the spectrogram view",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/spectrogram-medium",
+ "$ref": "#/definitions/style"
+ },
+ "spectrogram-high": {
+ "description": "Styling for the high threshold values in the spectrogram view",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/spectrogram-high",
+ "$ref": "#/definitions/style"
+ },
+ "file": {
+ "description": "Styling for file names in source files",
+ "title": "/ui/theme-defs/<theme_name>/syntax-styles/file",
+ "$ref": "#/definitions/style"
+ }
+ },
+ "additionalProperties": false
+ },
+ "status-styles": {
+ "description": "Styles for the user-interface components.",
+ "title": "/ui/theme-defs/<theme_name>/status-styles",
+ "type": "object",
+ "properties": {
+ "text": {
+ "description": "Styling for status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/text",
+ "$ref": "#/definitions/style"
+ },
+ "warn": {
+ "description": "Styling for warnings in status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/warn",
+ "$ref": "#/definitions/style"
+ },
+ "alert": {
+ "description": "Styling for alerts in status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/alert",
+ "$ref": "#/definitions/style"
+ },
+ "active": {
+ "description": "Styling for activity in status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/active",
+ "$ref": "#/definitions/style"
+ },
+ "inactive-alert": {
+ "description": "Styling for inactive alert status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/inactive-alert",
+ "$ref": "#/definitions/style"
+ },
+ "inactive": {
+ "description": "Styling for inactive status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/inactive",
+ "$ref": "#/definitions/style"
+ },
+ "title-hotkey": {
+ "description": "Styling for hotkey highlights in titles",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/title-hotkey",
+ "$ref": "#/definitions/style"
+ },
+ "title": {
+ "description": "Styling for title sections of status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/title",
+ "$ref": "#/definitions/style"
+ },
+ "disabled-title": {
+ "description": "Styling for title sections of status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/disabled-title",
+ "$ref": "#/definitions/style"
+ },
+ "subtitle": {
+ "description": "Styling for subtitle sections of status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/subtitle",
+ "$ref": "#/definitions/style"
+ },
+ "info": {
+ "description": "Styling for informational messages in status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/info",
+ "$ref": "#/definitions/style"
+ },
+ "hotkey": {
+ "description": "Styling for hotkey highlights of status bars",
+ "title": "/ui/theme-defs/<theme_name>/status-styles/hotkey",
+ "$ref": "#/definitions/style"
+ }
+ },
+ "additionalProperties": false
+ },
+ "log-level-styles": {
+ "description": "Styles for each log message level.",
+ "title": "/ui/theme-defs/<theme_name>/log-level-styles",
+ "type": "object",
+ "patternProperties": {
+ "(trace|debug5|debug4|debug3|debug2|debug|info|stats|notice|warning|error|critical|fatal|invalid)": {
+ "title": "/ui/theme-defs/<theme_name>/log-level-styles/<level>",
+ "$ref": "#/definitions/style"
+ }
+ },
+ "additionalProperties": false
+ },
+ "highlights": {
+ "description": "Styles for text highlights.",
+ "title": "/ui/theme-defs/<theme_name>/highlights",
+ "type": "object",
+ "patternProperties": {
+ "([\\w\\-]+)": {
+ "title": "/ui/theme-defs/<theme_name>/highlights/<highlight_name>",
+ "type": "object",
+ "properties": {
+ "pattern": {
+ "title": "/ui/theme-defs/<theme_name>/highlights/<highlight_name>/pattern",
+ "description": "The regular expression to highlight",
+ "type": "string"
+ },
+ "style": {
+ "description": "The styling for the text that matches the associated pattern",
+ "title": "/ui/theme-defs/<theme_name>/highlights/<highlight_name>/style",
+ "$ref": "#/definitions/style"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "movement": {
+ "description": "Log file cursor movement mode settings",
+ "title": "/ui/movement",
+ "type": "object",
+ "properties": {
+ "mode": {
+ "title": "/ui/movement/mode",
+ "description": "The mode of cursor movement to use.",
+ "type": "string",
+ "enum": [
+ "top",
+ "cursor"
+ ],
+ "examples": [
+ "top",
+ "cursor"
+ ]
+ }
+ },
+ "additionalProperties": false
+ },
+ "keymap-defs": {
+ "description": "Keymap definitions.",
+ "title": "/ui/keymap-defs",
+ "type": "object",
+ "patternProperties": {
+ "([\\w\\-]+)": {
+ "description": "The keymap definitions",
+ "title": "/ui/keymap-defs/<keymap_name>",
+ "type": "object",
+ "patternProperties": {
+ "((?:x[0-9a-f]{2})+)": {
+ "description": "Map of key codes to commands to execute. The field names are the keys to be mapped using as a hexadecimal representation of the UTF-8 encoding. Each byte of the UTF-8 should start with an 'x' followed by the hexadecimal representation of the byte.",
+ "title": "/ui/keymap-defs/<keymap_name>/<key_seq>",
+ "type": "object",
+ "properties": {
+ "command": {
+ "title": "/ui/keymap-defs/<keymap_name>/<key_seq>/command",
+ "description": "The command to execute for the given key sequence. Use a script to execute more complicated operations.",
+ "type": "string",
+ "pattern": "^[:|;].*",
+ "examples": [
+ ":goto next hour"
+ ]
+ },
+ "alt-msg": {
+ "title": "/ui/keymap-defs/<keymap_name>/<key_seq>/alt-msg",
+ "description": "The help message to display after the key is pressed.",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "log": {
+ "description": "Log message settings",
+ "title": "/log",
+ "type": "object",
+ "properties": {
+ "watch-expressions": {
+ "description": "Log message watch expressions",
+ "title": "/log/watch-expressions",
+ "type": "object",
+ "patternProperties": {
+ "([\\w\\-]+)": {
+ "description": "A log message watch expression",
+ "title": "/log/watch-expressions/<watch_name>",
+ "type": "object",
+ "properties": {
+ "expr": {
+ "title": "/log/watch-expressions/<watch_name>/expr",
+ "description": "The SQL expression to execute for each input line. If expression evaluates to true, a 'log message detected' event will be published.",
+ "type": "string"
+ },
+ "enabled": {
+ "title": "/log/watch-expressions/<watch_name>/enabled",
+ "description": "Indicates whether or not this expression should be evaluated during log processing.",
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "global": {
+ "description": "Global variable definitions",
+ "title": "/global",
+ "type": "object",
+ "patternProperties": {
+ "(\\w+)": {
+ "title": "/global/<var_name>",
+ "description": "A global variable definition. Global variables can be referenced in scripts, SQL statements, or commands.",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false,
+ "definitions": {
+ "clip-commands": {
+ "title": "clip-commands",
+ "description": "Container for the commands used to read from and write to the system clipboard",
+ "type": "object",
+ "$$target": "#/definitions/clip-commands",
+ "properties": {
+ "write": {
+ "title": "/write",
+ "description": "The command used to write to the clipboard",
+ "type": "string",
+ "examples": [
+ "pbcopy"
+ ]
+ },
+ "read": {
+ "title": "/read",
+ "description": "The command used to read from the clipboard",
+ "type": "string",
+ "examples": [
+ "pbpaste"
+ ]
+ }
+ },
+ "additionalProperties": false
+ },
+ "style": {
+ "title": "style",
+ "type": "object",
+ "$$target": "#/definitions/style",
+ "properties": {
+ "color": {
+ "title": "/color",
+ "description": "The foreground color value for this style. The value can be the name of an xterm color, the hexadecimal value, or a theme variable reference.",
+ "type": "string",
+ "examples": [
+ "#fff",
+ "Green",
+ "$black"
+ ]
+ },
+ "background-color": {
+ "title": "/background-color",
+ "description": "The background color value for this style. The value can be the name of an xterm color, the hexadecimal value, or a theme variable reference.",
+ "type": "string",
+ "examples": [
+ "#2d2a2e",
+ "Green"
+ ]
+ },
+ "underline": {
+ "title": "/underline",
+ "description": "Indicates that the text should be underlined.",
+ "type": "boolean"
+ },
+ "bold": {
+ "title": "/bold",
+ "description": "Indicates that the text should be bolded.",
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ }
+ }
+}
diff --git a/docs/schemas/event-file-format-detected-v1.schema.json b/docs/schemas/event-file-format-detected-v1.schema.json
new file mode 100644
index 0000000..edb8531
--- /dev/null
+++ b/docs/schemas/event-file-format-detected-v1.schema.json
@@ -0,0 +1,26 @@
+{
+ "$id": "https://lnav.org/event-file-format-detected-v1.schema.json",
+ "title": "https://lnav.org/event-file-format-detected-v1.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Event fired when a log format is detected for a file.",
+ "properties": {
+ "$schema": {
+ "title": "/$schema",
+ "type": "string",
+ "examples": [
+ "https://lnav.org/event-file-format-detected-v1.schema.json"
+ ]
+ },
+ "filename": {
+ "title": "/filename",
+ "description": "The path of the file for which a matching format was found",
+ "type": "string"
+ },
+ "format": {
+ "title": "/format",
+ "description": "The name of the format",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/docs/schemas/event-file-open-v1.schema.json b/docs/schemas/event-file-open-v1.schema.json
new file mode 100644
index 0000000..76a6bc1
--- /dev/null
+++ b/docs/schemas/event-file-open-v1.schema.json
@@ -0,0 +1,21 @@
+{
+ "$id": "https://lnav.org/event-file-open-v1.schema.json",
+ "title": "https://lnav.org/event-file-open-v1.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Event fired when a file is opened.",
+ "properties": {
+ "$schema": {
+ "title": "/$schema",
+ "type": "string",
+ "examples": [
+ "https://lnav.org/event-file-open-v1.schema.json"
+ ]
+ },
+ "filename": {
+ "title": "/filename",
+ "description": "The path of the file that was opened",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/docs/schemas/event-log-msg-detected-v1.schema.json b/docs/schemas/event-log-msg-detected-v1.schema.json
new file mode 100644
index 0000000..30ec3a2
--- /dev/null
+++ b/docs/schemas/event-log-msg-detected-v1.schema.json
@@ -0,0 +1,57 @@
+{
+ "$id": "https://lnav.org/event-log-msg-detected-v1.schema.json",
+ "title": "https://lnav.org/event-log-msg-detected-v1.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Event fired when a log message is detected by a watch expression.",
+ "properties": {
+ "$schema": {
+ "title": "/$schema",
+ "type": "string",
+ "examples": [
+ "https://lnav.org/event-log-msg-detected-v1.schema.json"
+ ]
+ },
+ "watch-name": {
+ "title": "/watch-name",
+ "description": "The name of the watch expression that matched this log message",
+ "type": "string"
+ },
+ "filename": {
+ "title": "/filename",
+ "description": "The path of the file containing the log message",
+ "type": "string"
+ },
+ "line-number": {
+ "title": "/line-number",
+ "description": "The line number in the file, starting from zero",
+ "type": "integer"
+ },
+ "format": {
+ "title": "/format",
+ "description": "The name of the log format that matched this log message",
+ "type": "string"
+ },
+ "timestamp": {
+ "title": "/timestamp",
+ "description": "The timestamp of the log message",
+ "type": "string"
+ },
+ "values": {
+ "description": "The log message values captured by the log format",
+ "title": "/values",
+ "type": "object",
+ "patternProperties": {
+ "([\\w\\-]+)": {
+ "title": "/values/<name>",
+ "type": [
+ "boolean",
+ "integer",
+ "string"
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/docs/schemas/event-session-loaded-v1.schema.json b/docs/schemas/event-session-loaded-v1.schema.json
new file mode 100644
index 0000000..f64f051
--- /dev/null
+++ b/docs/schemas/event-session-loaded-v1.schema.json
@@ -0,0 +1,16 @@
+{
+ "$id": "https://lnav.org/event-session-loaded-v1.schema.json",
+ "title": "https://lnav.org/event-session-loaded-v1.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Event fired when a session is loaded.",
+ "properties": {
+ "$schema": {
+ "title": "/$schema",
+ "type": "string",
+ "examples": [
+ "https://lnav.org/event-session-loaded-v1.schema.json"
+ ]
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/docs/schemas/format-v1.schema.json b/docs/schemas/format-v1.schema.json
new file mode 100644
index 0000000..fe781f9
--- /dev/null
+++ b/docs/schemas/format-v1.schema.json
@@ -0,0 +1,638 @@
+{
+ "$id": "https://lnav.org/schemas/format-v1.schema.json",
+ "title": "https://lnav.org/schemas/format-v1.schema.json",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "properties": {
+ "$schema": {
+ "title": "/$schema",
+ "description": "Specifies the type of this file",
+ "type": "string"
+ }
+ },
+ "patternProperties": {
+ "(\\w+)": {
+ "description": "The definition of a log file format.",
+ "title": "/<format_name>",
+ "type": "object",
+ "properties": {
+ "regex": {
+ "description": "The set of regular expressions used to match log messages",
+ "title": "/<format_name>/regex",
+ "type": "object",
+ "patternProperties": {
+ "([^/]+)": {
+ "description": "The set of patterns used to match log messages",
+ "title": "/<format_name>/regex/<pattern_name>",
+ "type": "object",
+ "properties": {
+ "pattern": {
+ "title": "/<format_name>/regex/<pattern_name>/pattern",
+ "description": "The regular expression to match a log message and capture fields.",
+ "type": "string",
+ "minLength": 1
+ },
+ "module-format": {
+ "title": "/<format_name>/regex/<pattern_name>/module-format",
+ "description": "If true, this pattern will only be used to parse message bodies of container formats, like syslog",
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "json": {
+ "title": "/<format_name>/json",
+ "description": "Indicates that log files are JSON-encoded (deprecated, use \"file-type\": \"json\")",
+ "type": "boolean"
+ },
+ "convert-to-local-time": {
+ "title": "/<format_name>/convert-to-local-time",
+ "description": "Indicates that displayed timestamps should automatically be converted to local time",
+ "type": "boolean"
+ },
+ "hide-extra": {
+ "title": "/<format_name>/hide-extra",
+ "description": "Specifies whether extra values in JSON logs should be displayed",
+ "type": "boolean"
+ },
+ "multiline": {
+ "title": "/<format_name>/multiline",
+ "description": "Indicates that log messages can span multiple lines",
+ "type": "boolean"
+ },
+ "timestamp-divisor": {
+ "title": "/<format_name>/timestamp-divisor",
+ "description": "The value to divide a numeric timestamp by in a JSON log.",
+ "type": [
+ "integer",
+ "number"
+ ]
+ },
+ "file-pattern": {
+ "title": "/<format_name>/file-pattern",
+ "description": "A regular expression that restricts this format to log files with a matching name",
+ "type": "string"
+ },
+ "mime-types": {
+ "title": "/<format_name>/mime-types",
+ "description": "A list of mime-types this format should be used for",
+ "type": "array",
+ "items": {
+ "type": "string",
+ "enum": [
+ "application/vnd.tcpdump.pcap"
+ ]
+ }
+ },
+ "level-field": {
+ "title": "/<format_name>/level-field",
+ "description": "The name of the level field in the log message pattern",
+ "type": "string"
+ },
+ "level-pointer": {
+ "title": "/<format_name>/level-pointer",
+ "description": "A regular-expression that matches the JSON-pointer of the level property",
+ "type": "string"
+ },
+ "timestamp-field": {
+ "title": "/<format_name>/timestamp-field",
+ "description": "The name of the timestamp field in the log message pattern",
+ "type": "string"
+ },
+ "subsecond-field": {
+ "title": "/<format_name>/subsecond-field",
+ "description": "The path to the property in a JSON-lines log message that contains the sub-second time value",
+ "type": "string"
+ },
+ "subsecond-units": {
+ "title": "/<format_name>/subsecond-units",
+ "description": "The units of the subsecond-field property value",
+ "type": "string",
+ "enum": [
+ "milli",
+ "micro",
+ "nano"
+ ]
+ },
+ "time-field": {
+ "title": "/<format_name>/time-field",
+ "description": "The name of the time field in the log message pattern. This field should only be specified if the timestamp field only contains a date.",
+ "type": "string"
+ },
+ "body-field": {
+ "title": "/<format_name>/body-field",
+ "description": "The name of the body field in the log message pattern",
+ "type": "string"
+ },
+ "url": {
+ "title": "/<format_name>/url",
+ "description": "A URL with more information about this log format",
+ "type": [
+ "array",
+ "string"
+ ],
+ "items": {
+ "type": "string"
+ }
+ },
+ "title": {
+ "title": "/<format_name>/title",
+ "description": "The human-readable name for this log format",
+ "type": "string"
+ },
+ "description": {
+ "title": "/<format_name>/description",
+ "description": "A longer description of this log format",
+ "type": "string"
+ },
+ "timestamp-format": {
+ "title": "/<format_name>/timestamp-format",
+ "description": "An array of strptime(3)-like timestamp formats",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "module-field": {
+ "title": "/<format_name>/module-field",
+ "description": "The name of the module field in the log message pattern",
+ "type": "string"
+ },
+ "opid-field": {
+ "title": "/<format_name>/opid-field",
+ "description": "The name of the operation-id field in the log message pattern",
+ "type": "string"
+ },
+ "ordered-by-time": {
+ "title": "/<format_name>/ordered-by-time",
+ "description": "Indicates that the order of messages in the file is time-based.",
+ "type": "boolean"
+ },
+ "level": {
+ "description": "The map of level names to patterns or integer values",
+ "title": "/<format_name>/level",
+ "type": "object",
+ "patternProperties": {
+ "(trace|debug[2345]?|info|stats|notice|warning|error|critical|fatal)": {
+ "title": "/<format_name>/level/<level>",
+ "description": "The regular expression used to match the log text for this level. For JSON logs with numeric levels, this should be the number for the corresponding level.",
+ "type": [
+ "integer",
+ "string"
+ ]
+ }
+ },
+ "additionalProperties": false
+ },
+ "value": {
+ "description": "The set of value definitions",
+ "title": "/<format_name>/value",
+ "type": "object",
+ "patternProperties": {
+ "([^/]+)": {
+ "description": "The set of values captured by the log message patterns",
+ "title": "/<format_name>/value/<value_name>",
+ "type": "object",
+ "properties": {
+ "kind": {
+ "title": "/<format_name>/value/<value_name>/kind",
+ "description": "The type of data in the field",
+ "type": "string",
+ "enum": [
+ "string",
+ "integer",
+ "float",
+ "boolean",
+ "json",
+ "struct",
+ "quoted",
+ "xml"
+ ]
+ },
+ "collate": {
+ "title": "/<format_name>/value/<value_name>/collate",
+ "description": "The collating function to use for this column",
+ "type": "string"
+ },
+ "unit": {
+ "description": "Unit definitions for this field",
+ "title": "/<format_name>/value/<value_name>/unit",
+ "type": "object",
+ "properties": {
+ "field": {
+ "title": "/<format_name>/value/<value_name>/unit/field",
+ "description": "The name of the field that contains the units for this field",
+ "type": "string"
+ },
+ "scaling-factor": {
+ "description": "Transforms the numeric value by the given factor",
+ "title": "/<format_name>/value/<value_name>/unit/scaling-factor",
+ "type": "object",
+ "patternProperties": {
+ "([^/]+)": {
+ "title": "/<format_name>/value/<value_name>/unit/scaling-factor/<scale>",
+ "type": "object",
+ "properties": {
+ "op": {
+ "title": "/<format_name>/value/<value_name>/unit/scaling-factor/<scale>/op",
+ "type": "string",
+ "enum": [
+ "identity",
+ "multiply",
+ "divide"
+ ]
+ },
+ "value": {
+ "title": "/<format_name>/value/<value_name>/unit/scaling-factor/<scale>/value",
+ "type": "number"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "identifier": {
+ "title": "/<format_name>/value/<value_name>/identifier",
+ "description": "Indicates whether or not this field contains an identifier that should be highlighted",
+ "type": "boolean"
+ },
+ "foreign-key": {
+ "title": "/<format_name>/value/<value_name>/foreign-key",
+ "description": "Indicates whether or not this field should be treated as a foreign key for row in another table",
+ "type": "boolean"
+ },
+ "hidden": {
+ "title": "/<format_name>/value/<value_name>/hidden",
+ "description": "Indicates whether or not this field should be hidden",
+ "type": "boolean"
+ },
+ "action-list": {
+ "title": "/<format_name>/value/<value_name>/action-list",
+ "description": "Actions to execute when this field is clicked on",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "rewriter": {
+ "title": "/<format_name>/value/<value_name>/rewriter",
+ "description": "A command that will rewrite this field when pretty-printing",
+ "type": "string",
+ "examples": [
+ ";SELECT :sc_status || ' (' || (SELECT message FROM http_status_codes WHERE status = :sc_status) || ') '"
+ ]
+ },
+ "description": {
+ "title": "/<format_name>/value/<value_name>/description",
+ "description": "A description of the field",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "tags": {
+ "description": "The tags to automatically apply to log messages",
+ "title": "/<format_name>/tags",
+ "type": "object",
+ "patternProperties": {
+ "([\\w:;\\._\\-]+)": {
+ "description": "The name of the tag to apply",
+ "title": "/<format_name>/tags/<tag_name>",
+ "type": "object",
+ "properties": {
+ "paths": {
+ "description": "Restrict tagging to the given paths",
+ "title": "/<format_name>/tags/<tag_name>/paths",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "glob": {
+ "title": "/<format_name>/tags/<tag_name>/paths/glob",
+ "description": "The glob to match against file paths",
+ "type": "string",
+ "examples": [
+ "*/system.log*"
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "pattern": {
+ "title": "/<format_name>/tags/<tag_name>/pattern",
+ "description": "The regular expression to match against the body of the log message",
+ "type": "string",
+ "examples": [
+ "\\w+ is down"
+ ]
+ },
+ "description": {
+ "title": "/<format_name>/tags/<tag_name>/description",
+ "description": "A description of this tag",
+ "type": "string"
+ },
+ "level": {
+ "title": "/<format_name>/tags/<tag_name>/level",
+ "description": "Constrain hits to log messages with this level",
+ "type": "string",
+ "enum": [
+ "trace",
+ "debug5",
+ "debug4",
+ "debug3",
+ "debug2",
+ "debug",
+ "info",
+ "stats",
+ "notice",
+ "warning",
+ "error",
+ "critical",
+ "fatal"
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "action": {
+ "title": "/<format_name>/action",
+ "type": "object",
+ "patternProperties": {
+ "(\\w+)": {
+ "title": "/<format_name>/action/<action_name>",
+ "type": [
+ "string",
+ "object"
+ ],
+ "properties": {
+ "label": {
+ "title": "/<format_name>/action/<action_name>/label",
+ "type": "string"
+ },
+ "capture-output": {
+ "title": "/<format_name>/action/<action_name>/capture-output",
+ "type": "boolean"
+ },
+ "cmd": {
+ "title": "/<format_name>/action/<action_name>/cmd",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "sample": {
+ "description": "An array of sample log messages to be tested against the log message patterns",
+ "title": "/<format_name>/sample",
+ "type": "array",
+ "items": {
+ "type": "object",
+ "properties": {
+ "description": {
+ "title": "/<format_name>/sample/description",
+ "description": "A description of this sample.",
+ "type": "string"
+ },
+ "line": {
+ "title": "/<format_name>/sample/line",
+ "description": "A sample log line that should match a pattern in this format.",
+ "type": "string"
+ },
+ "level": {
+ "title": "/<format_name>/sample/level",
+ "description": "The expected level for this sample log line.",
+ "type": "string",
+ "enum": [
+ "trace",
+ "debug5",
+ "debug4",
+ "debug3",
+ "debug2",
+ "debug",
+ "info",
+ "stats",
+ "notice",
+ "warning",
+ "error",
+ "critical",
+ "fatal"
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "line-format": {
+ "description": "The display format for JSON-encoded log messages",
+ "title": "/<format_name>/line-format",
+ "type": "array",
+ "items": {
+ "type": [
+ "string",
+ "object"
+ ],
+ "properties": {
+ "field": {
+ "title": "/<format_name>/line-format/field",
+ "description": "The name of the field to substitute at this position",
+ "type": "string"
+ },
+ "default-value": {
+ "title": "/<format_name>/line-format/default-value",
+ "description": "The default value for this position if the field is null",
+ "type": "string"
+ },
+ "timestamp-format": {
+ "title": "/<format_name>/line-format/timestamp-format",
+ "description": "The strftime(3) format for this field",
+ "type": "string",
+ "minLength": 1
+ },
+ "min-width": {
+ "title": "/<format_name>/line-format/min-width",
+ "description": "The minimum width of the field",
+ "type": "integer",
+ "minimum": 0
+ },
+ "auto-width": {
+ "title": "/<format_name>/line-format/auto-width",
+ "description": "Automatically detect the necessary width of the field based on the observed values",
+ "type": "boolean"
+ },
+ "max-width": {
+ "title": "/<format_name>/line-format/max-width",
+ "description": "The maximum width of the field",
+ "type": "integer",
+ "minimum": 0
+ },
+ "align": {
+ "title": "/<format_name>/line-format/align",
+ "description": "Align the text in the column to the left or right side",
+ "type": "string",
+ "enum": [
+ "left",
+ "right"
+ ]
+ },
+ "overflow": {
+ "title": "/<format_name>/line-format/overflow",
+ "description": "Overflow style",
+ "type": "string",
+ "enum": [
+ "abbrev",
+ "truncate",
+ "dot-dot"
+ ]
+ },
+ "text-transform": {
+ "title": "/<format_name>/line-format/text-transform",
+ "description": "Text transformation",
+ "type": "string",
+ "enum": [
+ "none",
+ "uppercase",
+ "lowercase",
+ "capitalize"
+ ]
+ },
+ "prefix": {
+ "title": "/<format_name>/line-format/prefix",
+ "description": "Text to prepend to the value",
+ "type": "string"
+ },
+ "suffix": {
+ "title": "/<format_name>/line-format/suffix",
+ "description": "Text to append to the value",
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "search-table": {
+ "description": "Search tables to automatically define for this log format",
+ "title": "/<format_name>/search-table",
+ "type": "object",
+ "patternProperties": {
+ "(\\w+)": {
+ "description": "The set of search tables to be automatically defined",
+ "title": "/<format_name>/search-table/<table_name>",
+ "type": "object",
+ "properties": {
+ "pattern": {
+ "title": "/<format_name>/search-table/<table_name>/pattern",
+ "description": "The regular expression for this search table.",
+ "type": "string"
+ },
+ "glob": {
+ "title": "/<format_name>/search-table/<table_name>/glob",
+ "description": "Glob pattern used to constrain hits to messages that match the given pattern.",
+ "type": "string"
+ },
+ "level": {
+ "title": "/<format_name>/search-table/<table_name>/level",
+ "description": "Constrain hits to log messages with this level",
+ "type": "string",
+ "enum": [
+ "trace",
+ "debug5",
+ "debug4",
+ "debug3",
+ "debug2",
+ "debug",
+ "info",
+ "stats",
+ "notice",
+ "warning",
+ "error",
+ "critical",
+ "fatal"
+ ]
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "highlights": {
+ "description": "The set of highlight definitions",
+ "title": "/<format_name>/highlights",
+ "type": "object",
+ "patternProperties": {
+ "([^/]+)": {
+ "description": "The definition of a highlight",
+ "title": "/<format_name>/highlights/<highlight_name>",
+ "type": "object",
+ "properties": {
+ "pattern": {
+ "title": "/<format_name>/highlights/<highlight_name>/pattern",
+ "description": "A regular expression to highlight in logs of this format.",
+ "type": "string"
+ },
+ "color": {
+ "title": "/<format_name>/highlights/<highlight_name>/color",
+ "description": "The color to use when highlighting this pattern.",
+ "type": "string"
+ },
+ "background-color": {
+ "title": "/<format_name>/highlights/<highlight_name>/background-color",
+ "description": "The background color to use when highlighting this pattern.",
+ "type": "string"
+ },
+ "underline": {
+ "title": "/<format_name>/highlights/<highlight_name>/underline",
+ "description": "Highlight this pattern with an underline.",
+ "type": "boolean"
+ },
+ "blink": {
+ "title": "/<format_name>/highlights/<highlight_name>/blink",
+ "description": "Highlight this pattern by blinking.",
+ "type": "boolean"
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+ },
+ "file-type": {
+ "title": "/<format_name>/file-type",
+ "description": "The type of file that contains the log messages",
+ "type": "string",
+ "enum": [
+ "text",
+ "json",
+ "csv"
+ ]
+ },
+ "max-unrecognized-lines": {
+ "title": "/<format_name>/max-unrecognized-lines",
+ "description": "The maximum number of lines in a file to use when detecting the format",
+ "type": "integer",
+ "minimum": 1
+ }
+ },
+ "additionalProperties": false
+ }
+ },
+ "additionalProperties": false
+}
diff --git a/docs/source/_ext/__init__.py b/docs/source/_ext/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/docs/source/_ext/__init__.py
diff --git a/docs/source/_ext/lnavlexer.py b/docs/source/_ext/lnavlexer.py
new file mode 100644
index 0000000..2509a5f
--- /dev/null
+++ b/docs/source/_ext/lnavlexer.py
@@ -0,0 +1,26 @@
+
+__all__ = ['LnavCommandLexer']
+
+import re
+
+from pygments.token import Whitespace, Text, Keyword, Literal
+from pygments.lexers._mapping import LEXERS
+from pygments.lexers.python import RegexLexer
+
+class LnavCommandLexer(RegexLexer):
+ name = 'lnav'
+
+ flags = re.IGNORECASE
+ tokens = {
+ 'root': [
+ (r'\s+', Whitespace),
+ (r':[\w\-]+', Keyword),
+ (r'\<[\w\-]+\>', Literal.String.Doc),
+ (r'.', Text),
+ ]
+ }
+
+def setup(app):
+ LEXERS['LnavCommandLexer'] = (
+ '_ext.lnavlexer', 'lnav', ('lnav',), ('*.lnav',), ('text/lnav',))
+ app.add_lexer('lnav', LnavCommandLexer)
diff --git a/docs/source/_static/theme_overrides.css b/docs/source/_static/theme_overrides.css
new file mode 100644
index 0000000..c25d308
--- /dev/null
+++ b/docs/source/_static/theme_overrides.css
@@ -0,0 +1,57 @@
+/* override table width restrictions */
+@media screen and (min-width: 767px) {
+
+ .wy-table-responsive table td {
+ /* !important prevents the common CSS stylesheets from overriding
+ this as on RTD they are loaded after this stylesheet */
+ white-space: normal !important;
+ }
+
+}
+
+th p {
+ margin-bottom: 0px;
+}
+
+.wy-nav-content {
+ max-width: 900px;
+ overflow-x: auto;
+}
+
+table.query-results p {
+ font-size: 0.9em !important;
+}
+
+DL DT:target, :target > H2, :target > H3, span:target + H2, span:target + H3 {
+ border: 0 !important;
+ border-bottom: 1px solid #d3d381 !important;
+ background: #ffc !important;
+ /* padding: 1em !important;*/
+}
+
+DL DT {
+ border: 0 !important;
+ background: inherit !important;
+ border-bottom: 1px solid #c3c0ee !important;
+ /* display: block !important; */
+}
+
+kbd {
+ padding: 0.1em 0.6em;
+ border: 1px solid #ccc;
+ font-size: 11px;
+ font-family: Arial, Helvetica, sans-serif;
+ background-color: #f7f7f7;
+ color: #333;
+ -moz-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
+ -webkit-box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
+ box-shadow: 0 1px 0px rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset;
+ -moz-border-radius: 3px;
+ -webkit-border-radius: 3px;
+ border-radius: 3px;
+ display: inline-block;
+ margin: 0 0.1em 0.1em 0.1em;
+ text-shadow: 0 1px 0 #fff;
+ line-height: 1.4;
+ white-space: nowrap;
+}
diff --git a/docs/source/cli.rst b/docs/source/cli.rst
new file mode 100644
index 0000000..565e54f
--- /dev/null
+++ b/docs/source/cli.rst
@@ -0,0 +1,168 @@
+.. _cli:
+
+Command Line Interface
+======================
+
+There are two command-line interfaces provided by **lnav**, one for viewing
+files and one for managing **lnav**'s configuration. The file viewing mode is
+the default and is all that most people will need. The management mode can
+be useful for those that are developing log file formats and is activated by
+passing the :option:`-m` option as the first argument.
+
+File Viewing Mode
+-----------------
+
+The following options can be used when starting **lnav**. There are not
+many flags because the majority of the functionality is accessed using
+the :option:`-c` option to execute :ref:`commands<commands>` or
+:ref:`SQL queries<sql-ext>`.
+
+Options
+^^^^^^^
+
+.. option:: -h
+
+ Print these command-line options and exit.
+
+.. option:: -H
+
+ Start lnav and switch to the help view.
+
+.. option:: -C
+
+ Check the given files against the configuration, report any errors, and
+ exit. This option can be helpful for validating that a log format is
+ well-formed.
+
+.. option:: -c <command>
+
+ Execute the given lnav command, SQL query, or lnav script. The
+ argument must be prefixed with the character used to enter the prompt
+ to distinguish between the different types (i.e. ':', ';', '|').
+ This option can be given multiple times.
+
+.. option:: -f <path>
+
+ Execute the given command file. This option can be given multiple times.
+
+.. option:: -I <path>
+
+ Add a configuration directory.
+
+.. option:: -i
+
+ Install the format files in the :file:`.lnav/formats/` directory.
+ Individual files will be installed in the :file:`installed`
+ directory and git repositories will be cloned with a directory
+ name based on their repository URI.
+
+.. option:: -u
+
+ Update formats installed from git repositories.
+
+.. option:: -d <path>
+
+ Write debug messages to the given file.
+
+.. option:: -n
+
+ Run without the curses UI (headless mode).
+
+.. option:: -N
+
+ Do not open the default syslog file if no files are given.
+
+.. option:: -r
+
+ Recursively load files from the given base directories.
+
+.. option:: -t
+
+ Prepend timestamps to the lines of data being read in on the standard input.
+
+.. option:: -w <path>
+
+ Write the contents of the standard input to this file.
+
+.. option:: -V
+
+ Print the version of lnav.
+
+.. option:: -q
+
+ Do not print the log messages after executing all of the commands.
+
+
+Management Mode (v0.11.0+)
+--------------------------
+
+The management CLI mode provides functionality for query **lnav**'s log
+format definitions.
+
+Options
+^^^^^^^
+
+.. option:: -m
+
+ Switch to management mode. This must be the first option passed on the
+ command-line.
+
+Subcommands
+^^^^^^^^^^^
+
+.. option:: regex101 import <regex101-url> <format-name> [<regex-name>]
+
+ Convert a regex101.com entry into a skeleton log format file.
+
+.. option:: format <format-name> regex <regex-name> push
+
+ Push a log format regular expression to regex101.com .
+
+.. option:: format <format-name> regex <regex-name> pull
+
+ Pull changes to a regex that was previously pushed to regex101.com .
+
+Environment Variables
+---------------------
+
+.. envvar:: XDG_CONFIG_HOME
+
+ If this variable is set, lnav will use this directory to store its
+ configuration in a sub-directory named :file:`lnav`.
+
+.. envvar:: HOME
+
+ If :envvar:`XDG_CONFIG_HOME` is not set, lnav will use this directory
+ to store its configuration in a sub-directory named :file:`.lnav`.
+
+.. envvar:: APPDATA
+
+ On Windows, lnav will use this directory instead of HOME
+ to store its configuration in a sub-directory named :file:`.lnav`.
+
+.. envvar:: TZ
+
+ The timezone setting is used in some log formats to convert UTC timestamps
+ to the local timezone.
+
+
+Examples
+--------
+
+ To load and follow the system syslog file:
+
+ .. prompt:: bash
+
+ lnav
+
+ To load all of the files in :file:`/var/log`:
+
+ .. prompt:: bash
+
+ lnav /var/log
+
+ To watch the output of make with timestamps prepended:
+
+ .. prompt:: bash
+
+ make 2>&1 | lnav -t
diff --git a/docs/source/commands.rst b/docs/source/commands.rst
new file mode 100644
index 0000000..d83a07e
--- /dev/null
+++ b/docs/source/commands.rst
@@ -0,0 +1,132 @@
+.. role:: lnavcmd(code)
+ :language: lnav
+ :class: highlight
+
+.. _commands:
+
+Commands
+========
+
+Commands provide access to some of the more advanced features in **lnav**, like
+:ref:`filtering<filtering>` and
+:ref:`"search tables"<search_tables>`. You can activate the command
+prompt by pressing the :kbd:`:` key. At the prompt, you can start typing
+in the desired command and/or double-tap :kbd:`TAB` to activate
+auto-completion and show the available commands. To guide you in the usage of
+the commands, a help window will appear above the command prompt with an
+explanation of the command and its parameters (if it has any). For example,
+the screenshot below shows the help for the :code:`:open` command:
+
+.. figure:: open-help.png
+ :align: center
+ :figwidth: 50%
+
+ Screenshot of the online help for the :code:`:open` command.
+
+In addition to online help, many commands provide a preview of the effects that
+the command will have. This preview will activate shortly after you have
+finished typing, but before you have pressed :kbd:`Enter` to execute the
+command. For example, the :code:`:open` command will show a preview of the
+first few lines of the file given as its argument:
+
+.. figure:: open-preview.png
+ :align: center
+
+ Screenshot of the preview shown for the :code:`:open` command.
+
+The :lnavcmd:`:filter-out pattern` command is another instance where the preview behavior
+can help you craft the correct command-line. This command takes a PCRE2 regular
+expression that specifies the log messages that should be filtered out of the
+view. The preview for this command will highlight the portion of the log
+messages that match the expression in red. Thus, you can be certain that the
+regular expression is matching the log messages you are interested in before
+committing the filter. The following screenshot shows an example of this
+preview behavior for the string "launchd":
+
+.. figure:: filter-out-preview.png
+ :align: center
+
+ Screenshot showing the preview for the :code:`:filter-out` command.
+
+Any errors detected during preview will be shown in the status bar right above
+the command prompt. For example, an attempt to open an unknown file will show
+an error message in the status bar, like so:
+
+.. figure:: open-error.png
+ :align: center
+
+ Screenshot of the error shown when trying to open a non-existent file.
+
+.. tip::
+
+ Note that almost all commands support TAB-completion for their arguments.
+ So, if you are in doubt as to what to type for an argument, you can double-
+ tap the :kbd:`TAB` key to get suggestions. For example, the
+ TAB-completion for the :code:`filter-in` command will suggest words that are
+ currently displayed in the view.
+
+.. note:: The following commands can be disabled by setting the ``LNAVSECURE``
+ environment variable before executing the **lnav** binary:
+
+ - :code:`:open`
+ - :code:`:pipe-to`
+ - :code:`:pipe-line-to`
+ - :code:`:write-*-to`
+
+ This makes it easier to run **lnav** in restricted environments without the
+ risk of privilege escalation.
+
+I/O Commands
+------------
+
+Anonymization
+^^^^^^^^^^^^^
+
+Anonymization is the process of removing identifying information from content
+to make it safer for sharing with others. For example, an IP address can
+often be used to uniquely identify an entity. Substituting all instances of
+a particular IP with the same dummy value would remove the identifying data
+without losing statistical accuracy. **lnav** has built-in support for
+anonymization through the :code:`--anonymize` flag on the :code:`:write-*`
+collection of commands. While the anonymization process should catch most
+
+ :IPv4 Addresses: Are replaced with addresses in the :code:`10.0.0.0/8` range.
+
+ :IPv6 Addresses: Are replaced with addresses in the :code:`2001:db8::/32` range.
+
+ :URL User Names: Are replaced with a random animal name.
+
+ :URL Passwords: Are replaced with a hash of the input password.
+
+ :URL Hosts: Are replaced with a random name under the example.com domain.
+
+ :URL Paths: Are recursively examined for substitution.
+
+ :URL Query Strings: Are recursively examined for substitution.
+
+ :URL Fragments: Are recursively examined for substitution.
+
+ :Paths: Are recursively examined for substitution.
+
+ :Credit Card Numbers: Are replaced with a 16 digit hash of the input number.
+
+ :MAC Addresses: Are replaced with addresses in the :code:`00:00:5E:00:53:00` range.
+
+ :Hex Dumps: Are replaced with a hash of the input replicated to the size of input.
+
+ :Email User Names: Are replaced with a random animal name.
+
+ :Email Host Names: Are replaced with a random name under the example.com domain.
+
+ :Words: Are replaced with a random word with a matching case style.
+
+ :Quoted Strings: Are recursively examined for substitution.
+
+ :UUID: Are replaced with a hash of the input.
+
+ :XML Attribute Values: Are recursively examined for substitution.
+
+Reference
+---------
+
+.. include:: ../../src/internals/cmd-ref.rst
diff --git a/docs/source/conf.py b/docs/source/conf.py
new file mode 100644
index 0000000..3a2d8f2
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,461 @@
+# -*- coding: utf-8 -*-
+#
+# lnav documentation build configuration file, created by
+# sphinx-quickstart on Fri Jul 12 21:09:39 2013.
+#
+# This file is execfile()d with the current directory set to its containing dir.
+#
+# Note that not all possible configuration values are present in this
+# autogenerated file.
+#
+# All configuration values have a default; values that are commented out
+# serve to show the default.
+
+import sys, os
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+this_dir = os.path.abspath('.')
+src_dir = os.path.join(this_dir, "..", "..", "src")
+sys.path.insert(0, this_dir)
+sys.path.insert(0, src_dir)
+
+import format2csv
+
+format2csv.main(["",
+ os.path.join(this_dir, "format-table.csv"),
+ os.path.join(src_dir, "formats")])
+
+import re
+from pygments.lexer import RegexLexer, words
+from pygments.token import Punctuation, Whitespace, Text, Comment, Operator, \
+ Keyword, Name, String, Literal, Number, Generic
+from sphinx.highlighting import lexers
+
+
+class CustSqliteLexer(RegexLexer):
+ name = 'custsqlite'
+
+ flags = re.IGNORECASE
+ tokens = {
+ 'root': [
+ (r'\s+', Text),
+ (r'--.*\n?', Comment.Single),
+ (r'#.*\n?', Comment.Single),
+ (r'/\*', Comment.Multiline, 'multiline-comments'),
+ (words((
+ 'ABORT',
+ 'ACTION',
+ 'ADD',
+ 'AFTER',
+ 'ALL',
+ 'ALTER',
+ 'ALWAYS',
+ 'ANALYZE',
+ 'AND',
+ 'AS',
+ 'ASC',
+ 'ATTACH',
+ 'AUTOINCREMENT',
+ 'BEFORE',
+ 'BEGIN',
+ 'BETWEEN',
+ 'BY',
+ 'CASCADE',
+ 'CASE',
+ 'CAST',
+ 'CHECK',
+ 'COLLATE',
+ 'COLUMN',
+ 'COMMIT',
+ 'CONFLICT',
+ 'CONSTRAINT',
+ 'CREATE',
+ 'CROSS',
+ 'CURRENT',
+ 'CURRENT_DATE',
+ 'CURRENT_TIME',
+ 'CURRENT_TIMESTAMP',
+ 'DATABASE',
+ 'DEFAULT',
+ 'DEFERRABLE',
+ 'DEFERRED',
+ 'DELETE',
+ 'DESC',
+ 'DETACH',
+ 'DISTINCT',
+ 'DO',
+ 'DROP',
+ 'EACH',
+ 'ELSE',
+ 'END',
+ 'ESCAPE',
+ 'EXCEPT',
+ 'EXCLUDE',
+ 'EXCLUSIVE',
+ 'EXISTS',
+ 'EXPLAIN',
+ 'FAIL',
+ 'FILTER',
+ 'FIRST',
+ 'FOLLOWING',
+ 'FOR',
+ 'FOREIGN',
+ 'FROM',
+ 'FULL',
+ 'GENERATED',
+ 'GLOB',
+ 'GROUP',
+ 'GROUPS',
+ 'HAVING',
+ 'IF',
+ 'IGNORE',
+ 'IMMEDIATE',
+ 'IN',
+ 'INDEX',
+ 'INDEXED',
+ 'INITIALLY',
+ 'INNER',
+ 'INSERT',
+ 'INSTEAD',
+ 'INTERSECT',
+ 'INTO',
+ 'IS',
+ 'ISNULL',
+ 'JOIN',
+ 'KEY',
+ 'LAST',
+ 'LEFT',
+ 'LIKE',
+ 'LIMIT',
+ 'MATCH',
+ 'NATURAL',
+ 'NO',
+ 'NOT',
+ 'NOTHING',
+ 'NOTNULL',
+ 'NULL',
+ 'NULLS',
+ 'OF',
+ 'OFFSET',
+ 'ON',
+ 'OR',
+ 'ORDER',
+ 'OTHERS',
+ 'OUTER',
+ 'OVER',
+ 'PARTITION',
+ 'PLAN',
+ 'PRAGMA',
+ 'PRECEDING',
+ 'PRIMARY',
+ 'QUERY',
+ 'RAISE',
+ 'RANGE',
+ 'RECURSIVE',
+ 'REFERENCES',
+ 'REGEXP',
+ 'REINDEX',
+ 'RELEASE',
+ 'RENAME',
+ 'REPLACE',
+ 'RESTRICT',
+ 'RIGHT',
+ 'ROLLBACK',
+ 'ROW',
+ 'ROWS',
+ 'SAVEPOINT',
+ 'SELECT',
+ 'SET',
+ 'TABLE',
+ 'TEMP',
+ 'TEMPORARY',
+ 'THEN',
+ 'TIES',
+ 'TO',
+ 'TRANSACTION',
+ 'TRIGGER',
+ 'UNBOUNDED',
+ 'UNION',
+ 'UNIQUE',
+ 'UPDATE',
+ 'USING',
+ 'VACUUM',
+ 'VALUES',
+ 'VIEW',
+ 'VIRTUAL',
+ 'WHEN',
+ 'WHERE',
+ 'WINDOW',
+ 'WITH',
+ 'WITHOUT'), suffix=r'\b'),
+ Keyword),
+ (words((
+ 'ARRAY', 'BIGINT', 'BINARY', 'BIT', 'BLOB', 'BOOLEAN', 'CHAR',
+ 'CHARACTER', 'DATE', 'DEC', 'DECIMAL', 'FLOAT', 'INT', 'INTEGER',
+ 'INTERVAL', 'NUMBER', 'NUMERIC', 'REAL', 'SERIAL', 'SMALLINT',
+ 'VARCHAR', 'VARYING', 'INT8', 'SERIAL8', 'TEXT'), suffix=r'\b'),
+ Name.Builtin),
+ (r'[+*/<>=~!@#%^&|`?-]', Operator),
+ (r'[0-9]+', Number.Integer),
+ # TODO: Backslash escapes?
+ (r"'(''|[^'])*'", String.Single),
+ (r'"(""|[^"])*"', String.Symbol), # not a real string literal in ANSI SQL
+ (r'[a-z_][\w$]*', Name), # allow $s in strings for Oracle
+ (r'\$[a-z_]+', Name),
+ (r'[;:()\[\],.]', Punctuation)
+ ],
+ 'multiline-comments': [
+ (r'/\*', Comment.Multiline, 'multiline-comments'),
+ (r'\*/', Comment.Multiline, '#pop'),
+ (r'[^/*]+', Comment.Multiline),
+ (r'[/*]', Comment.Multiline)
+ ]
+ }
+
+ def analyse_text(text):
+ return 0.01
+
+
+lexers['custsqlite'] = CustSqliteLexer(startinline=True)
+
+# -- General configuration -----------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = [
+ 'sphinx.ext.coverage',
+ "sphinx_rtd_theme",
+ 'sphinx_copybutton',
+ 'sphinx-jsonschema',
+ 'sphinx-prompt',
+ '_ext.lnavlexer',
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix of source filenames.
+source_suffix = '.rst'
+
+# The encoding of source files.
+# source_encoding = 'utf-8-sig'
+
+# The master toctree document.
+master_doc = 'index'
+
+# General information about the project.
+project = u'lnav'
+copyright = u'2023, Tim Stack'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.11'
+# The full version, including alpha/beta/rc tags.
+release = '0.11.1'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+# language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+# today = ''
+# Else, today_fmt is used as the format for a strftime call.
+# today_fmt = '%B %d, %Y'
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+exclude_patterns = []
+
+# The reST default role (used for this markup: `text`) to use for all documents.
+# default_role = None
+
+# If true, '()' will be appended to :func: etc. cross-reference text.
+# add_function_parentheses = True
+
+# If true, the current module name will be prepended to all description
+# unit titles (such as .. function::).
+# add_module_names = True
+
+# If true, sectionauthor and moduleauthor directives will be shown in the
+# output. They are ignored by default.
+# show_authors = False
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = 'sphinx'
+
+# A list of ignored prefixes for module index sorting.
+# modindex_common_prefix = []
+
+# -- Options for HTML output ---------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages. See the documentation for
+# a list of builtin themes.
+html_theme = 'sphinx_rtd_theme'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+html_theme_options = {
+}
+
+# Add any paths that contain custom themes here, relative to this directory.
+# html_theme_path = []
+
+# The name for this set of Sphinx documents. If None, it defaults to
+# "<project> v<release> documentation".
+# html_title = None
+
+# A shorter title for the navigation bar. Default is the same as html_title.
+# html_short_title = None
+
+# The name of an image file (relative to this directory) to place at the top
+# of the sidebar.
+# html_logo = None
+
+# The name of an image file (within the static path) to use as favicon of the
+# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
+# pixels large.
+# html_favicon = None
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+html_static_path = ['_static']
+
+
+def setup(app):
+ app.add_css_file('theme_overrides.css')
+
+
+# html_context = {
+# 'css_files': [
+# '_static/theme_overrides.css', # override wide tables in RTD theme
+# ],
+# }
+
+# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
+# using the given strftime format.
+# html_last_updated_fmt = '%b %d, %Y'
+
+# If true, SmartyPants will be used to convert quotes and dashes to
+# typographically correct entities.
+# html_use_smartypants = True
+
+# Custom sidebar templates, maps document names to template names.
+# html_sidebars = {}
+
+# Additional templates that should be rendered to pages, maps page names to
+# template names.
+# html_additional_pages = {}
+
+# If false, no module index is generated.
+# html_domain_indices = True
+
+# If false, no index is generated.
+# html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+# html_split_index = False
+
+# If true, links to the reST sources are added to the pages.
+# html_show_sourcelink = True
+
+# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
+# html_show_sphinx = True
+
+# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
+# html_show_copyright = True
+
+# If true, an OpenSearch description file will be output, and all pages will
+# contain a <link> tag referring to it. The value of this option must be the
+# base URL from which the finished HTML is served.
+# html_use_opensearch = ''
+
+# This is the file name suffix for HTML files (e.g. ".xhtml").
+# html_file_suffix = None
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'lnavdoc'
+
+# -- Options for LaTeX output --------------------------------------------------
+
+latex_elements = {
+ # The paper size ('letterpaper' or 'a4paper').
+ # 'papersize': 'letterpaper',
+
+ # The font size ('10pt', '11pt' or '12pt').
+ # 'pointsize': '10pt',
+
+ # Additional stuff for the LaTeX preamble.
+ # 'preamble': '',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'lnav.tex', u'lnav Documentation',
+ u'Tim Stack', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+# latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+# latex_use_parts = False
+
+# If true, show page references after internal links.
+# latex_show_pagerefs = False
+
+# If true, show URL addresses after external links.
+# latex_show_urls = False
+
+# Documents to append as an appendix to all manuals.
+# latex_appendices = []
+
+# If false, no module index is generated.
+# latex_domain_indices = True
+
+
+# -- Options for manual page output --------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+ ('index', 'lnav', u'lnav Documentation',
+ [u'Tim Stack'], 1)
+]
+
+# If true, show URL addresses after external links.
+# man_show_urls = False
+
+
+# -- Options for Texinfo output ------------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+# dir menu entry, description, category)
+texinfo_documents = [
+ ('index', 'lnav', u'lnav Documentation',
+ u'Tim Stack', 'lnav', 'One line description of project.',
+ 'Miscellaneous'),
+]
+
+# Documents to append as an appendix to all manuals.
+# texinfo_appendices = []
+
+# If false, no module index is generated.
+# texinfo_domain_indices = True
+
+# How to display URL addresses: 'footnote', 'no', or 'inline'.
+# texinfo_show_urls = 'footnote'
diff --git a/docs/source/config.rst b/docs/source/config.rst
new file mode 100644
index 0000000..5294e52
--- /dev/null
+++ b/docs/source/config.rst
@@ -0,0 +1,270 @@
+
+.. _Configuration:
+
+Configuration
+=============
+
+The configuration for **lnav** is stored in the following JSON files in
+:file:`~/.lnav`:
+
+* :file:`config.json` -- Contains local customizations that were done using the
+ :code:`:config` command.
+* :file:`configs/default/*.json` -- The default configuration files that are
+ built into lnav are written to this directory with :file:`.sample` appended.
+ Removing the :file:`.sample` extension and editing the file will allow you to
+ do basic customizations.
+* :file:`configs/installed/*.json` -- Contains configuration files installed
+ using the :option:`-i` flag (e.g. :code:`$ lnav -i /path/to/config.json`).
+* :file:`configs/*/*.json` -- Other directories that contain :file:`*.json`
+ files will be loaded on startup. This structure is convenient for installing
+ **lnav** configurations, like from a git repository.
+
+A valid **lnav** configuration file must contain an object with the
+:code:`$schema` property, like so:
+
+.. code-block:: json
+
+ {
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json"
+ }
+
+.. note::
+
+ Log format definitions are stored separately in the :file:`~/.lnav/formats`
+ directly. See the :ref:`Log Formats<log_formats>` chapter for more
+ information.
+
+
+Options
+-------
+
+The following configuration options can be used to customize the **lnav** UI to
+your liking. The options can be changed using the :code:`:config` command.
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/keymap
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/theme
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/clock-format
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/dim-text
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/default-colors
+
+
+.. _themes:
+
+Theme Definitions
+-----------------
+
+User Interface themes are defined in a JSON configuration file. A theme is
+made up of the style definitions for different types of text in the UI. A
+:ref:`definition<theme_style>` can include the foreground/background colors
+and the bold/underline attributes. The style definitions are broken up into
+multiple categories for the sake of organization. To make it easier to write
+a definition, a theme can define variables that can be referenced as color
+values.
+
+Variables
+^^^^^^^^^
+
+The :code:`vars` object in a theme definition contains the mapping of variable
+names to color values. These variables can be referenced in style definitions
+by prefixing them with a dollar-sign (e.g. :code:`$black`). The following
+variables can also be defined to control the values of the ANSI colors that
+are log messages or plain text:
+
+.. csv-table:: ANSI colors
+ :header: "Variable Name", "ANSI Escape"
+
+ "black", "ESC[30m"
+ "red", "ESC[31m"
+ "green", "ESC[32m"
+ "yellow", "ESC[33m"
+ "blue", "ESC[34m"
+ "magenta", "ESC[35m"
+ "cyan", "ESC[36m"
+ "white", "ESC[37m"
+
+Specifying Colors
+^^^^^^^^^^^^^^^^^
+
+Colors can be specified using hexadecimal notation by starting with a hash
+(e.g. :code:`#aabbcc`) or using a color name as found at
+http://jonasjacek.github.io/colors/. If colors are not specified for a style,
+the values from the :code:`styles/text` definition.
+
+.. note::
+
+ When specifying colors in hexadecimal notation, you do not need to have an
+ exact match in the XTerm 256 color palette. A best approximation will be
+ picked based on the `CIEDE2000 <https://en.wikipedia.org/wiki/Color_difference#CIEDE2000>`_
+ color difference algorithm.
+
+
+
+Example
+^^^^^^^
+
+The following example sets the black/background color for text to a dark grey
+using a variable and sets the foreground to an off-white. This theme is
+incomplete, but it works enough to give you an idea of how a theme is defined.
+You can copy the code block, save it to a file in
+:file:`~/.lnav/configs/installed/` and then activate it by executing
+:code:`:config /ui/theme example` in lnav. For a more complete theme
+definition, see one of the definitions built into **lnav**, like
+`monocai <https://github.com/tstack/lnav/blob/master/src/themes/monocai.json>`_.
+
+ .. code-block:: json
+
+ {
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "theme-defs": {
+ "example1": {
+ "vars": {
+ "black": "#2d2a2e"
+ },
+ "styles": {
+ "text": {
+ "color": "#f6f6f6",
+ "background-color": "$black"
+ }
+ }
+ }
+ }
+ }
+ }
+
+Reference
+^^^^^^^^^
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/theme-defs/patternProperties/([\w\-]+)/properties/vars
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/theme-defs/patternProperties/([\w\-]+)/properties/styles
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/theme-defs/patternProperties/([\w\-]+)/properties/syntax-styles
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/theme-defs/patternProperties/([\w\-]+)/properties/status-styles
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/theme-defs/patternProperties/([\w\-]+)/properties/log-level-styles
+
+.. _theme_style:
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/definitions/style
+
+
+.. _keymaps:
+
+Keymap Definitions
+------------------
+
+Keymaps in **lnav** map a key sequence to a command to execute. When a key is
+pressed, it is converted into a hex-encoded string that is looked up in the
+keymap. The :code:`command` value associated with the entry in the keymap is
+then executed. Note that the "command" can be an **lnav**
+:ref:`command<commands>`, a :ref:`SQL statement/query<sql-ext>`, or an
+**lnav** script. If an :code:`alt-msg` value is included in the entry, the
+bottom-right section of the UI will be updated with the help text.
+
+.. note::
+
+ Not all functionality is available via commands or SQL at the moment. Also,
+ some hotkeys are not implemented via keymaps.
+
+Key Sequence Encoding
+^^^^^^^^^^^^^^^^^^^^^
+
+Key presses are converted into a hex-encoded string that is used to lookup an
+entry in the keymap. Each byte of the keypress value is formatted as an
+:code:`x` followed by the hex-encoding in lowercase. For example, the encoding
+for the £ key would be :code:`xc2xa3`. To make it easier to discover the
+encoding for unassigned keys, **lnav** will print in the command prompt the
+:code:`:config` command and
+`JSON-Pointer <https://tools.ietf.org/html/rfc6901>`_ for assigning a command
+to the key.
+
+.. figure:: key-encoding-prompt.png
+ :align: center
+
+ Screenshot of the command prompt when an unassigned key is pressed.
+
+.. note::
+
+ Since **lnav** is a terminal application, it can only receive keypresses that
+ can be represented as characters or escape sequences. For example, it cannot
+ handle the press of a modifier key.
+
+Reference
+^^^^^^^^^
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/ui/properties/keymap-defs/patternProperties/([\w\-]+)
+
+
+Log Handling
+------------
+
+The handling of logs is largely determined by the
+:ref:`log file formats<log_formats>`, this section covers options that are not
+specific to a particular format.
+
+Watch Expressions (v0.11.0+)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Watch expressions can be used to fire an event when a log message matches a
+condition. You can then install a listener for these events and trigger an
+action to be performed. For example, to automate filtering based on
+identifiers, a watch expression can match messages that mention the ID and then
+a trigger can install a filter for that ID. Creating a watch expression is
+done by adding an entry into the :code:`/log/watch-expressions` configuration
+tree. For example, to create a watch named "dhcpdiscover" that matches
+DHCPDISCOVER messages from the :code:`dhclient` daemon, you would run the
+following:
+
+.. code-block:: lnav
+
+ :config /log/watch-expressions/dhcpdiscover/expr :log_procname = 'dhclient' AND startswith(:log_body, 'DHCPDISCOVER')
+
+The watch expression can refer to column names in the log message by prefixing
+them with a colon. The expression is evaluated by passing the log message
+fields as bound parameters and not against a table. The easiest way to test
+out an expression is with the :ref:`mark_expr` command, since it will behave
+similarly. After changing the configuration, you'll need to restart lnav
+for the effect to take place. You can then query the :code:`lnav_events`
+table to see any generated
+:code:`https://lnav.org/event-log-msg-detected-v1.schema.json` events from the
+logs that were loaded:
+
+.. code-block:: custsqlite
+
+ ;SELECT * FROM lnav_events
+
+From there, you can create a SQLite trigger on the :code:`lnav_events` table
+that will examine the event contents and perform an action. See the
+:ref:`Events` section for more information on handling events.
+
+Reference
+^^^^^^^^^
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/log/properties/watch-expressions/patternProperties/([\w\-]+)
+
+.. _tuning:
+
+Tuning
+------
+
+The following configuration options can be used to tune the internals of
+**lnav** to your liking. The options can be changed using the :code:`:config`
+command.
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/tuning/properties/archive-manager
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/tuning/properties/clipboard
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/definitions/clip-commands
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/tuning/properties/file-vtab
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/tuning/properties/logfile
+
+.. jsonschema:: ../schemas/config-v1.schema.json#/properties/tuning/properties/remote/properties/ssh
diff --git a/docs/source/cookbook.rst b/docs/source/cookbook.rst
new file mode 100644
index 0000000..3eb0ffd
--- /dev/null
+++ b/docs/source/cookbook.rst
@@ -0,0 +1,104 @@
+
+.. _Cookbook:
+
+Cookbook
+========
+
+This chapter contains recipes for common tasks that can be done in **lnav**.
+These recipes can be used as a starting point for your own needs after some
+adaptation.
+
+
+Log Formats
+-----------
+
+TBD
+
+Defining a New Format
+^^^^^^^^^^^^^^^^^^^^^
+
+TBD
+
+
+Annotating Logs
+---------------
+
+Log messages can be annotated in a couple of different ways in **lnav** to help
+you get organized.
+
+Create partitions for Linux boots
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+When digging through logs that can be broken up into multiple sections,
+**lnav**'s :ref:`partitioning feature<taking_notes>` can be used to keep track
+of which section you are in. For example, if a collection of Linux logs
+covered multiple boots, the following script could be used to create partitions
+for each boot. After the partition name is set for the log messages, the
+current name will show up in the top status bar next to the current time.
+
+.. literalinclude:: ../../src/scripts/partition-by-boot.lnav
+ :language: custsqlite
+ :caption: partition-by-boot.lnav
+ :linenos:
+
+Tagging SSH log messages
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+Log messages can be tagged interactively with the :ref:`:tag<tag>` command or
+programmatically using the :ref:`sql-ext`. This example uses a script to
+search for interesting SSH messages and automatically adds an appropriate tag.
+
+.. literalinclude:: ../../example-scripts/tag-ssh-msgs.lnav
+ :language: custsqlite
+ :caption: tag-ssh-msgs.lnav
+ :linenos:
+
+Log Analysis
+------------
+
+Most log analysis within **lnav** is done through the :ref:`sql-ext`. The
+following examples should give you some ideas to start leveraging this
+functionality. One thing to keep in mind is that if a query gets to be too
+large or multiple statements need to be executed, you can create a
+:code:`.lnav` script that contains the statements and execute it using the
+:kbd:`\|` command prompt.
+
+Count client IPs in web access logs
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+To count the occurrences of an IP in web access logs and order the results
+from highest to lowest:
+
+ .. code-block:: custsqlite
+
+ ;SELECT c_ip, count(*) as hits FROM access_log GROUP BY c_ip ORDER BY hits DESC
+
+
+Show only lines where a numeric field is in a range
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The :ref:`:filter-expr<filter_expr>` command can be used to filter web access
+logs to only show lines where the number of bytes transferred to the client is
+between 10,000 and 40,000 bytes like so:
+
+ .. code-block:: custsqlite
+
+ :filter-expr :sc_bytes BETWEEN 10000 AND 40000
+
+
+Generating a Report
+^^^^^^^^^^^^^^^^^^^
+
+Reports can be generated by writing an **lnav** :ref:`script<scripts>` that
+uses SQL queries and commands to format a document. A basic script can simply
+execute a SQL query that is shown in the DB view. More sophisticated scripts
+can use the following commands to generate customized output for a report:
+
+* The :ref:`:echo<echo>` command to write plain text
+* :ref:`SQL queries<sql-ext>` followed by a "write" command, like
+ :ref:`:write-table-to<write_table_to>`.
+
+.. literalinclude:: ../../example-scripts/report-demo.lnav
+ :language: custsqlite
+ :caption: report-demo.lnav
+ :linenos:
diff --git a/docs/source/data.rst b/docs/source/data.rst
new file mode 100644
index 0000000..a7352e1
--- /dev/null
+++ b/docs/source/data.rst
@@ -0,0 +1,193 @@
+
+.. _data-ext:
+
+Extracting Data
+===============
+
+**Note**: This feature is still in **BETA**, you should expect bugs and
+incompatible changes in the future.
+
+Log messages contain a good deal of useful data, but it's not always easy to get
+at. The log parser built into **lnav** is able to extract data as described by
+:ref:`log_formats` as well as discovering data in plain text messages. This data
+can then be queried and processed using the SQLite front-end that is also
+incorporated into **lnav**. As an example, the following Syslog message from
+:code:`sudo` can be processed to extract several key/value pairs::
+
+ Jul 31 11:42:26 Example-MacBook-Pro.local sudo[87024]: testuser : TTY=ttys004 ; PWD=/Users/testuser/github/lbuild ; USER=root ; COMMAND=/usr/bin/make install
+
+The data that can be extracted by the parser is viewable directly in **lnav**
+by pressing the 'p' key. The results will be shown in an overlay like the
+following::
+
+ Current Time: 2013-07-31T11:42:26.000 Original Time: 2013-07-31T11:42:26.000 Offset: +0.000
+ Known message fields:
+ ├ log_hostname = Example-MacBook-Pro.local
+ ├ log_procname = sudo
+ ├ log_pid = 87024
+ Discovered message fields:
+ ├ col_0 = testuser
+ ├ TTY = ttys004
+ ├ PWD = /Users/testuser/github/lbuild
+ ├ USER = root
+ └ COMMAND = /usr/bin/make install
+
+Notice that the parser has detected pairs of the form '<key>=<value>'. The data
+parser will also look for pairs separated by a colon. If there are no clearly
+demarcated pairs, then the parser will extract anything that looks like data
+values and assign them keys of the form 'col_N'. For example, two data values,
+an IPv4 address and a symbol, will be extracted from the following log
+message::
+
+ Apr 29 08:13:43 sample-centos5 avahi-daemon[2467]: Registering new address record for 10.1.10.62 on eth0.
+
+Since there are no keys for the values in the message, the parser will assign
+'col_0' for the IP address and 'col_1' for the symbol, as seen here::
+
+ Current Time: 2013-04-29T08:13:43.000 Original Time: 2013-04-29T08:13:43.000 Offset: +0.000
+ Known message fields:
+ ├ log_hostname = sample-centos5
+ ├ log_procname = avahi-daemon
+ ├ log_pid = 2467
+ Discovered message fields:
+ ├ col_0 = 10.1.10.62
+ └ col_1 = eth0
+
+Now that you have an idea of how the parser works, you can begin to perform
+queries on the data that is being extracted. The SQLite database engine is
+embedded into **lnav** and its `Virtual Table
+<http://www.sqlite.org/vtab.html>`_ mechanism is used to provide a means to
+process this log data. Each log format has its own table that can be used to
+access all of the loaded messages that are in that format. For accessing log
+message content that is more free-form, like the examples given here, the
+**logline** table can be used. The **logline** table is recreated for each
+query and is based on the format and pairs discovered in the log message at
+the top of the display.
+
+Queries can be performed by pressing the semi-colon (;) key in **lnav**. After
+pressing the key, the overlay showing any known or discovered fields will be
+displayed to give you an idea of what data is available. The query can be any
+`SQL query <http://sqlite.org/lang.html>`_ supported by SQLite. To make
+analysis easier, **lnav** includes many extra functions for processing strings,
+paths, and IP addresses. See :ref:`sql-ext` for more information.
+
+As an example, the simplest query to perform initially would be a "select all",
+like so:
+
+.. code-block:: sql
+
+ SELECT * FROM logline
+
+When this query is run against the second example log message given above, the
+following results are received::
+
+ log_line log_part log_time log_idle_msecs log_level log_hostname log_procname log_pid col_0 col_1
+
+ 292 p.0 2013-04-11T16:42:51.000 0 info localhost avahi-daemon 2480 fe80::a00:27ff:fe98:7f6e eth0
+ 293 p.0 2013-04-11T16:42:51.000 0 info localhost avahi-daemon 2480 10.0.2.15 eth0
+ 330 p.0 2013-04-11T16:47:02.000 0 info localhost avahi-daemon 2480 fe80::a00:27ff:fe98:7f6e eth0
+ 336 p.0 2013-04-11T16:47:02.000 0 info localhost avahi-daemon 2480 10.1.10.75 eth0
+ 343 p.0 2013-04-11T16:47:02.000 0 info localhost avahi-daemon 2480 10.1.10.75 eth0
+ 370 p.0 2013-04-11T16:59:39.000 0 info localhost avahi-daemon 2480 10.1.10.75 eth0
+ 377 p.0 2013-04-11T16:59:39.000 0 info localhost avahi-daemon 2480 10.1.10.75 eth0
+ 382 p.0 2013-04-11T16:59:41.000 0 info localhost avahi-daemon 2480 fe80::a00:27ff:fe98:7f6e eth0
+ 401 p.0 2013-04-11T17:20:45.000 0 info localhost avahi-daemon 4247 fe80::a00:27ff:fe98:7f6e eth0
+ 402 p.0 2013-04-11T17:20:45.000 0 info localhost avahi-daemon 4247 10.1.10.75 eth0
+
+ 735 p.0 2013-04-11T17:41:46.000 0 info sample-centos5 avahi-daemon 2465 fe80::a00:27ff:fe98:7f6e eth0
+ 736 p.0 2013-04-11T17:41:46.000 0 info sample-centos5 avahi-daemon 2465 10.1.10.75 eth0
+ 781 p.0 2013-04-12T03:32:30.000 0 info sample-centos5 avahi-daemon 2465 10.1.10.64 eth0
+ 788 p.0 2013-04-12T03:32:30.000 0 info sample-centos5 avahi-daemon 2465 10.1.10.64 eth0
+ 1166 p.0 2013-04-25T10:56:00.000 0 info sample-centos5 avahi-daemon 2467 fe80::a00:27ff:fe98:7f6e eth0
+ 1167 p.0 2013-04-25T10:56:00.000 0 info sample-centos5 avahi-daemon 2467 10.1.10.111 eth0
+ 1246 p.0 2013-04-26T06:06:25.000 0 info sample-centos5 avahi-daemon 2467 10.1.10.49 eth0
+ 1253 p.0 2013-04-26T06:06:25.000 0 info sample-centos5 avahi-daemon 2467 10.1.10.49 eth0
+ 1454 p.0 2013-04-28T06:53:55.000 0 info sample-centos5 avahi-daemon 2467 10.1.10.103 eth0
+ 1461 p.0 2013-04-28T06:53:55.000 0 info sample-centos5 avahi-daemon 2467 10.1.10.103 eth0
+
+ 1497 p.0 2013-04-29T08:13:43.000 0 info sample-centos5 avahi-daemon 2467 10.1.10.62 eth0
+ 1504 p.0 2013-04-29T08:13:43.000 0 info sample-centos5 avahi-daemon 2467 10.1.10.62 eth0
+
+Note that **lnav** is not returning results for all messages that are in this
+syslog file. Rather, it searches for messages that match the format for the
+given line and returns only those messages in results. In this case, that
+format is "Registering new address record for <IP> on <symbol>", which
+corresponds to the parts of the message that were not recognized as data.
+
+More sophisticated queries can be done, of course. For example, to find out the
+frequency of IP addresses mentioned in these messages, you can run:
+
+.. code-block:: sql
+
+ SELECT col_0,count(*) FROM logline GROUP BY col_0
+
+The results for this query are::
+
+ col_0 count(*)
+
+ 10.0.2.15 1
+ 10.1.10.49 2
+ 10.1.10.62 2
+ 10.1.10.64 2
+ 10.1.10.75 6
+ 10.1.10.103 2
+ 10.1.10.111 1
+ fe80::a00:27ff:fe98:7f6e 6
+
+Since this type of query is fairly common, **lnav** includes a "summarize"
+command that will compute the frequencies of identifiers as well as min, max,
+average, median, and standard deviation for number columns. In this case, you
+can run the following to compute the frequencies and return an ordered set of
+results::
+
+ :summarize col_0
+
+
+Recognized Data Types
+---------------------
+
+When searching for data to extract from log messages, **lnav** looks for the
+following set of patterns:
+
+
+Strings
+ Single and double-quoted strings. Example: "The quick brown fox."
+
+URLs
+ URLs that contain the '://' separator. Example: http://example.com
+
+Paths
+ File system paths. Examples: /path/to/file, ./relative/path
+
+MAC Address
+ Ethernet MAC addresses. Example: c4:2c:03:0e:e4:4a
+
+Hex Dumps
+ A colon-separated string of hex numbers. Example: e8:06:88:ff
+
+Date/Time
+ Date and time stamps of the form "YYYY-mm-DD" and "HH:MM:SS".
+
+IP Addresses
+ IPv4 and IPv6 addresses. Examples: 127.0.0.1, fe80::c62c:3ff:fe0e:e44a%en0
+
+UUID
+ The common formatting for 128-bit UUIDs. Example:
+ 0E305E39-F1E9-4DE4-B10B-5829E5DF54D0
+
+Version Numbers
+ Dot-separated version numbers. Example: 3.7.17
+
+Numbers
+ Numbers in base ten, hex, and octal formats. Examples: 1234, 0xbeef, 0777
+
+E-Mail Address
+ Strings that look close to an e-mail address. Example: gary@example.com
+
+Constants
+ Common constants in languages, like: true, false, null, None.
+
+Symbols
+ Words that follow the common conventions for symbols in programming
+ languages. For example, containing all capital letters, or separated
+ by colons. Example: SOME_CONSTANT_VALUE, namespace::value
diff --git a/docs/source/docutils.conf b/docs/source/docutils.conf
new file mode 100644
index 0000000..1bf4d83
--- /dev/null
+++ b/docs/source/docutils.conf
@@ -0,0 +1,2 @@
+[restructuredtext parser]
+syntax_highlight = short
diff --git a/docs/source/events.rst b/docs/source/events.rst
new file mode 100644
index 0000000..8318abf
--- /dev/null
+++ b/docs/source/events.rst
@@ -0,0 +1,56 @@
+.. _Events:
+
+Events (v0.11.0+)
+=================
+
+The events mechanism allows **lnav** to be automated based on events that
+occur during processing. For example, filters could be added only when a
+particular log file format is detected instead of always installing them.
+Events are published through the :ref:`lnav_events<table_lnav_events>` SQLite
+table. Reacting to events can be done by creating a SQLite trigger on the
+table and inspecting the content of the event.
+
+Trigger Example
+---------------
+
+The following is an example of a trigger that adds an out filter when a
+syslog file is loaded. You can copy the code into an :file:`.sql` file and
+install it by running :code:`lnav -i my_trigger.sql`.
+
+.. code-block:: sql
+ :caption: my_trigger.sql
+ :linenos:
+
+ CREATE TRIGGER IF NOT EXISTS add_format_specific_filters
+ AFTER INSERT ON lnav_events WHEN
+ -- Check the event type
+ jget(NEW.content, '/$schema') =
+ 'https://lnav.org/event-file-format-detected-v1.schema.json' AND
+ -- Only create the filter when a given format is seen
+ jget(NEW.content, '/format') = 'syslog_log' AND
+ -- Don't create the filter if it's already there
+ NOT EXISTS (
+ SELECT 1 FROM lnav_view_filters WHERE pattern = 'noisy message')
+ BEGIN
+ INSERT INTO lnav_view_filters (view_name, enabled, type, pattern) VALUES
+ ('log', 1, 'OUT', 'noisy message');
+ END;
+
+.. _event_reference:
+
+Reference
+---------
+
+The following tables describe the schema of the event JSON objects.
+
+.. jsonschema:: ../schemas/event-file-open-v1.schema.json#
+ :lift_description:
+
+.. jsonschema:: ../schemas/event-file-format-detected-v1.schema.json#
+ :lift_description:
+
+.. jsonschema:: ../schemas/event-log-msg-detected-v1.schema.json#
+ :lift_description:
+
+.. jsonschema:: ../schemas/event-session-loaded-v1.schema.json#
+ :lift_description:
diff --git a/docs/source/faq.rst b/docs/source/faq.rst
new file mode 100644
index 0000000..013d1c9
--- /dev/null
+++ b/docs/source/faq.rst
@@ -0,0 +1,85 @@
+.. _faq:
+
+Frequently Asked Questions
+==========================
+
+Q: How can I copy & paste without decorations?
+----------------------------------------------
+
+:Answer: There are a couple ways to do this:
+
+ * Use the :ref:`bookmark<hotkeys_bookmarks>` hotkeys to mark lines and then
+ press :kbd:`c` to copy to the local system keyboard. The system clipboard
+ is accessed using commands like :code:`pbcopy` and :code:`xclip`. See the
+ :ref:`tuning` section for more details.
+
+ If a system clipboard is not available,
+ the `OSC 52 <https://www.reddit.com/r/vim/comments/k1ydpn/a_guide_on_how_to_copy_text_from_anywhere/>`_
+ terminal escape sequence will be tried. If your terminal supports this
+ escape sequence, the selected text will be copied to the clipboard, even
+ if you are on an SSH connection.
+
+ * Press :kbd:`CTRL` + :kbd:`l` to temporarily switch to "lo-fi"
+ mode where the contents of the current view are printed to the terminal.
+ This option is useful when you are logged into a remote host.
+
+
+Q: How can I force a format for a file?
+---------------------------------------
+
+:Answer: The log format for a file is automatically detected and cannot be
+ forced.
+
+:Solution: Add some of the log file lines to the :ref:`sample<format_sample>`
+ array and then startup lnav to get a detailed explanation of where the format
+ patterns are not matching the sample lines.
+
+:Details: The first lines of the file are matched against the
+ :ref:`regular expressions defined in the format definitions<format_regex>`.
+ The order of the formats is automatically determined so that more specific
+ formats are tried before more generic ones. Therefore, if the expected
+ format is not being chosen for a file, then it means the regular expressions
+ defined by that format are not matching the first few lines of the file.
+
+ See :ref:`format_order` for more information.
+
+
+Q: How can I search backwards, like pressing :kbd:`?` in less?
+--------------------------------------------------------------
+
+:Answer: Searches in **lnav** runs in the background and do not block input
+ waiting to find the first hit. While the search prompt is open, pressing
+ :kbd:`CTRL` + :kbd:`j` will jump to the previous hit that was found. A
+ preview panel is also opened that shows the hits that have been found so
+ far.
+
+ After pressing :kbd:`Enter` at the search prompt, the view will jump to
+ the first hit that was found. Then, you can press :kbd:`n` to move to
+ the next search hit and :kbd:`N` to move to the previous one. If you
+ would like to add a hotkey for jumping to the previous hit by default,
+ enter the following configuration command:
+
+ .. code-block:: lnav
+
+ :config /ui/keymap-defs/default/x3f/command :prompt --alt search ?
+
+
+Q: Why isn't my log file highlighted correctly?
+-----------------------------------------------
+
+TBD
+
+Q: Why isn't a file being displayed?
+------------------------------------
+
+:Answer: Plaintext files are displayed separately from log files in the TEXT
+ view.
+
+:Solution: Press the :kbd:`t` key to switch to the text view. Or, open the
+ files configuration panel by pressing :kbd:`TAB` to cycle through the
+ panels, and then press :kbd:`/` to search for the file you're interested in.
+ If the file is a log, a new :ref:`log format<log_formats>` will need to be
+ created or an existing one modified.
+
+:Details: If a file being monitored by lnav does not match a known log file
+ format, it is treated as plaintext and will be displayed in the TEXT view.
diff --git a/docs/source/filter-out-preview.png b/docs/source/filter-out-preview.png
new file mode 100644
index 0000000..8f9816f
--- /dev/null
+++ b/docs/source/filter-out-preview.png
Binary files differ
diff --git a/docs/source/formats.rst b/docs/source/formats.rst
new file mode 100644
index 0000000..b30713d
--- /dev/null
+++ b/docs/source/formats.rst
@@ -0,0 +1,561 @@
+.. _log_formats:
+
+Log Formats
+===========
+
+Log files loaded into **lnav** are parsed based on formats defined in
+configuration files. Many
+formats are already built in to the **lnav** binary and you can define your own
+using a JSON file. When loading files, each format is checked to see if it can
+parse the first few lines in the file. Once a match is found, that format will
+be considered that files format and used to parse the remaining lines in the
+file. If no match is found, the file is considered to be plain text and can
+be viewed in the "text" view that is accessed with the **t** key.
+
+The following log formats are built into **lnav**:
+
+.. csv-table::
+ :header: "Name", "Table Name", "Description"
+ :widths: 8 5 20
+ :file: format-table.csv
+
+In addition to the above formats, the following self-describing formats are
+supported:
+
+* The
+ `Bro Network Security Monitor <https://www.bro.org/sphinx/script-reference/log-files.html>`_
+ TSV log format is supported in lnav versions v0.8.3+. The Bro log format is
+ self-describing, so **lnav** will read the header to determine the shape of
+ the file.
+* The
+ `W3C Extended Log File Format <https://www.w3.org/TR/WD-logfile.html>`_
+ is supported in lnav versions v0.10.0+. The W3C log format is
+ self-describing, so **lnav** will read the header to determine the shape of
+ the file.
+
+There is also basic support for the `logfmt <https://brandur.org/logfmt>`_
+convention for formatting log messages. Files that use this format must
+have the entire line be key/value pairs and the timestamp contained in a
+field named :code:`time` or :code:`ts`. If the file you're using does not
+quite follow this formatting, but wraps logfmt data with another recognized
+format, you can use the :ref:`logfmt2json` SQL function to convert the data
+into JSON for further analysis.
+
+
+Defining a New Format
+---------------------
+
+New log formats can be defined by placing JSON configuration files in
+subdirectories of the :file:`/etc/lnav/formats` and :file:`~/.lnav/formats/`
+directories. The directories and files can be named anything you like, but the
+files must have the '.json' suffix. A sample file containing the builtin
+configuration will be written to this directory when **lnav** starts up.
+You can consult that file when writing your own formats or if you need to
+modify existing ones. Format directories can also contain '.sql' and '.lnav'
+script files that can be used automate log file analysis.
+
+Creating a Format Using Regex101.com (v0.11.0+)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+For plain-text log files, the easiest way to create a log format definition is
+to create the regular expression that recognizes log messages using
+https://regex101.com . Simply copy a log line into the test string input box
+on the site and then start editing the regular expression. When building the
+regular expression, you'll want to use named captures for the structured parts
+of the log message. Any raw message text should be matched by a captured named
+"body". Once you have a regex that matches the whole log message, you can use
+**lnav**'s "management CLI" to create a skeleton format file. The skeleton
+will be populated with the regular expression from the site and the test
+string, along with any unit tests, will be added to the "samples" list. The
+"regex101 import" management command is used to create the skeleton and has
+the following form:
+
+.. prompt:: bash
+
+ lnav -m regex101 import <regex101-url> <format-name> [<regex-name>]
+
+If the import was successful, the path to the new format file should be
+printed out. The skeleton will most likely need some changes to make it
+fully functional. For example, the :code:`kind` properties for captured values
+default to :code:`string`, but you'll want to change them to the appropriate
+type.
+
+Format File Reference
+^^^^^^^^^^^^^^^^^^^^^
+
+An **lnav** format file must contain a single JSON object, preferably with a
+:code:`$schema` property that refers to the
+`format-v1.schema <https://lnav.org/schemas/format-v1.schema.json>`_,
+like so:
+
+.. code-block:: json
+
+ {
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json"
+ }
+
+Each format to be defined in the file should be a separate field in the top-level
+object. The field name should be the symbolic name of the format and consist
+only of alphanumeric characters and underscores. This value will also be used
+as the SQL table name for the log. The value for each field should be another
+object with the following fields:
+
+:title: The short and human-readable name for the format.
+:description: A longer description of the format.
+:url: A URL to the definition of the format.
+
+:file-pattern: A regular expression used to match log file paths. Typically,
+ every file format will be tried during the detection process. This field
+ can be used to limit which files a format is applied to in case there is
+ a potential for conflicts.
+
+.. _format_regex:
+
+:regex: This object contains sub-objects that describe the message formats
+ to match in a plain-text log file. Each :code:`regex` MUST only match one
+ type of log message. It must not match log messages that are matched by
+ other regexes in this format. This uniqueness requirement is necessary
+ because **lnav** will "lock-on" to a regex and use it to match against
+ the next line in a file. So, if the regexes do not uniquely match each
+ type of log message, messages can be matched by the wrong regex. The
+ "lock-on" behavior is needed to avoid the performance hit of having to
+ try too many different regexes.
+
+ .. note:: Log files that contain JSON messages should not specify this field.
+
+ :pattern: The regular expression that should be used to match log messages.
+ The `PCRE2 <http://www.pcre.org>`_ library is used by **lnav** to do all
+ regular expression matching.
+
+ :module-format: If true, this regex will only be used to parse message
+ bodies for formats that can act as containers, such as syslog. Default:
+ false.
+
+:json: True if each log line is JSON-encoded.
+
+:line-format: An array that specifies the text format for JSON-encoded
+ log messages. Log files that are JSON-encoded will have each message
+ converted from the raw JSON encoding into this format. Each element
+ is either an object that defines which fields should be inserted into
+ the final message string and or a string constant that should be
+ inserted. For example, the following configuration will transform each
+ log message object into a string that contains the timestamp, followed
+ by a space, and then the message body:
+
+ .. code-block:: json
+
+ [ { "field": "ts" }, " ", { "field": "msg" } ]
+
+ .. note:: Line-feeds at the end of a value are automatically stripped.
+
+ :field: The name or `JSON-Pointer <https://tools.ietf.org/html/rfc6901>`_
+ of the message field that should be inserted at this point in the
+ message. The special :code:`__timestamp__` field name can be used to
+ insert a human-readable timestamp. The :code:`__level__` field can be
+ used to insert the level name as defined by lnav.
+
+ .. tip::
+
+ Use a JSON-Pointer to reference nested fields. For example, to include
+ a "procname" property that is nested in a "details" object, you would
+ write the field reference as :code:`/details/procname`.
+
+ :min-width: The minimum width for the field. If the value for the field
+ in a given log message is shorter, padding will be added as needed to
+ meet the minimum-width requirement. (v0.8.2+)
+ :max-width: The maximum width for the field. If the value for the field
+ in a given log message is longer, the overflow algorithm will be applied
+ to try and shorten the field. (v0.8.2+)
+ :auto-width: Flag that indicates that the width of the field should
+ automatically be set to the widest value seen. (v0.11.2)
+ :align: Specifies the alignment for the field, either "left" or "right".
+ If "left", padding to meet the minimum-width will be added on the right.
+ If "right", padding will be added on the left. (v0.8.2+)
+ :overflow: The algorithm used to shorten a field that is longer than
+ "max-width". The following algorithms are supported:
+
+ :abbrev: Removes all but the first letter in dotted text. For example,
+ "com.example.foo" would be shortened to "c.e.foo".
+ :truncate: Truncates any text past the maximum width.
+ :dot-dot: Cuts out the middle of the text and replaces it with two
+ dots (i.e. '..').
+
+ (v0.8.2+)
+ :timestamp-format: The timestamp format to use when displaying the time
+ for this log message. (v0.8.2+)
+ :default-value: The default value to use if the field could not be found
+ in the current log message. The built-in default is "-".
+ :text-transform: Transform the text in the field. Supported options are:
+ none, uppercase, lowercase, capitalize
+ :prefix: Text to prepend to the value. If the value is empty, this prefix
+ will not be added.
+ :suffix: Text to append to the value. If the value is empty, this suffix
+ will not be added.
+
+:timestamp-field: The name of the field that contains the log message
+ timestamp. Defaults to "timestamp".
+
+:timestamp-format: An array of timestamp formats using a subset of the
+ strftime conversion specification. The following conversions are
+ supported: %a, %b, %L, %M, %H, %I, %d, %e, %k, %l, %m, %p, %y, %Y, %S, %s,
+ %Z, %z. In addition, you can also use the following:
+
+ :%L: Milliseconds as a decimal number (range 000 to 999).
+ :%f: Microseconds as a decimal number (range 000000 to 999999).
+ :%N: Nanoseconds as a decimal number (range 000000000 to 999999999).
+ :%q: Seconds from the epoch as a hexidecimal number.
+ :%i: Milliseconds from the epoch.
+ :%6: Microseconds from the epoch.
+
+:timestamp-divisor: For JSON logs with numeric timestamps, this value is used
+ to divide the timestamp by to get the number of seconds and fractional
+ seconds.
+
+:subsecond-field: (v0.11.1+) The path to the property in a JSON-lines log
+ message that contains the sub-second time value
+
+:subsecond-units: (v0.11.1+) The units of the subsecond-field property value.
+ The following values are supported:
+
+ :milli: for milliseconds
+ :micro: for microseconds
+ :nano: for nanoseconds
+
+:ordered-by-time: (v0.8.3+) Indicates that the order of messages in the file
+ is time-based. Files that are not naturally ordered by time will be sorted
+ in order to display them in the correct order. Note that this sorting can
+ incur a performance penalty when tailing logs.
+
+:level-field: The name of the regex capture group that contains the log
+ message level. Defaults to "level".
+
+:body-field: The name of the field that contains the main body of the
+ message. Defaults to "body".
+
+:opid-field: The name of the field that contains the "operation ID" of the
+ message. An "operation ID" establishes a thread of messages that might
+ correspond to a particular operation/request/transaction. The user can
+ press the 'o' or 'Shift+O' hotkeys to move forward/backward through the
+ list of messages that have the same operation ID. Note: For JSON-encoded
+ logs, the opid field can be a path (e.g. "foo/bar/opid") if the field is
+ nested in an object and it MUST be included in the "line-format" for the
+ 'o' hotkeys to work.
+
+:module-field: The name of the field that contains the module identifier
+ that distinguishes messages from one log source from another. This field
+ should be used if this message format can act as a container for other
+ types of log messages. For example, an Apache access log can be sent to
+ syslog instead of written to a file. In this case, **lnav** will parse
+ the syslog message and then separately parse the body of the message to
+ determine the "sub" format. This module identifier is used to help
+ **lnav** quickly identify the format to use when parsing message bodies.
+
+:hide-extra: A boolean for JSON logs that indicates whether fields not
+ present in the line-format should be displayed on their own lines.
+
+:level: A mapping of error levels to regular expressions. During scanning
+ the contents of the capture group specified by *level-field* will be
+ checked against each of these regexes. Once a match is found, the log
+ message level will set to the corresponding level. The available levels,
+ in order of severity, are: **fatal**, **critical**, **error**,
+ **warning**, **stats**, **info**, **debug**, **debug2-5**, **trace**.
+ For JSON logs with exact numeric levels, the number for the corresponding
+ level can be supplied. If the JSON log format uses numeric ranges instead
+ of exact numbers, you can supply a pattern and the number found in the log
+ will be converted to a string for pattern-matching.
+
+ .. note:: The regular expression is not anchored to the start of the
+ string by default, so an expression like :code:`1` will match
+ :code:`-1`. If you want to exactly match :code:`1`, you would
+ use :code:`^1$` as the expression.
+
+:multiline: If false, **lnav** will consider any log lines that do not
+ match one of the message patterns to be in error when checking files with
+ the '-C' option. This flag will not affect normal viewing operation.
+ Default: true.
+
+:value: This object contains the definitions for the values captured by the
+ regexes.
+
+ :kind: The type of data that was captured **string**, **integer**,
+ **float**, **json**, **quoted**.
+ :collate: The name of the SQLite collation function for this value.
+ The standard SQLite collation functions can be used as well as the
+ ones defined by lnav, as described in :ref:`collators`.
+ :identifier: A boolean that indicates whether or not this field represents
+ an identifier and should be syntax colored.
+ :foreign-key: A boolean that indicates that this field is a key and should
+ not be graphed. This should only need to be set for integer fields.
+ :hidden: A boolean for log fields that indicates whether they should
+ be displayed. The behavior is slightly different for JSON logs and text
+ logs. For a JSON log, this property determines whether an extra line
+ will be added with the key/value pair. For text logs, this property
+ controls whether the value should be displayed by default or replaced
+ with an ellipsis.
+ :rewriter: A command to rewrite this field when pretty-printing log
+ messages containing this value. The command must start with ':', ';',
+ or '|' to signify whether it is a regular command, SQL query, or a script
+ to be executed. The other fields in the line are accessible in SQL by
+ using the ':' prefix. The text value of this field will then be replaced
+ with the result of the command when pretty-printing. For example, the
+ HTTP access log format will rewrite the status code field to include the
+ textual version (e.g. 200 (OK)) using the following SQL query:
+
+ .. code-block:: sql
+
+ ;SELECT :sc_status || ' (' || (
+ SELECT message FROM http_status_codes
+ WHERE status = :sc_status) || ') '
+
+:tags: This object contains the tags that should automatically be added to
+ log messages.
+
+ :pattern: The regular expression evaluated over a line in the log file as
+ it is read in. If there is a match, the log message the line is a part
+ of will have this tag added to it.
+ :paths: This array contains objects that define restrictions on the file
+ paths that the tags will be applied to. The objects in this array can
+ contain:
+
+ :glob: A glob pattern to check against the log files read by lnav.
+
+.. _format_sample:
+
+:sample: A list of objects that contain sample log messages. All formats
+ must include at least one sample and it must be matched by one of the
+ included regexes. Each object must contain the following field:
+
+ :line: The sample message.
+ :level: The expected error level. An error will be raised if this level
+ does not match the level parsed by lnav for this sample message.
+
+:highlights: This object contains the definitions for patterns to be
+ highlighted in a log message. Each entry should have a name and a
+ definition with the following fields:
+
+ :pattern: The regular expression to match in the log message body.
+ :color: The foreground color to use when highlighting the part of the
+ message that matched the pattern. If no color is specified, one will be
+ picked automatically. Colors can be specified using hexadecimal notation
+ by starting with a hash (e.g. #aabbcc) or using a color name as found
+ at http://jonasjacek.github.io/colors/.
+ :background-color: The background color to use when highlighting the part
+ of the message that matched the pattern. If no background color is
+ specified, black will be used. The background color is only considered
+ if a foreground color is specified.
+ :underline: If true, underline the part of the message that matched the
+ pattern.
+ :blink: If true, blink the part of the message that matched the pattern.
+
+Example format:
+
+.. code-block:: json
+
+ {
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "example_log" : {
+ "title" : "Example Log Format",
+ "description" : "Log format used in the documentation example.",
+ "url" : "http://example.com/log-format.html",
+ "regex" : {
+ "basic" : {
+ "pattern" : "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z)>>(?<level>\\w+)>>(?<component>\\w+)>>(?<body>.*)$"
+ }
+ },
+ "level-field" : "level",
+ "level" : {
+ "error" : "ERROR",
+ "warning" : "WARNING"
+ },
+ "value" : {
+ "component" : {
+ "kind" : "string",
+ "identifier" : true
+ }
+ },
+ "sample" : [
+ {
+ "line" : "2011-04-01T15:14:34.203Z>>ERROR>>core>>Shit's on fire yo!"
+ }
+ ]
+ }
+ }
+
+Patching an Existing Format
+---------------------------
+
+When loading log formats from files, **lnav** will overlay any new data over
+previously loaded data. This feature allows you to override existing value or
+append new ones to the format configurations. For example, you can separately
+add a new regex to the example log format given above by creating another file
+with the following contents:
+
+.. code-block:: json
+
+ {
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "example_log" : {
+ "regex" : {
+ "custom1" : {
+ "pattern" : "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z)<<(?<level>\\w+)--(?<component>\\w+)>>(?<body>.*)$"
+ }
+ },
+ "sample" : [
+ {
+ "line" : "2011-04-01T15:14:34.203Z<<ERROR--core>>Shit's on fire yo!"
+ }
+ ]
+ }
+ }
+
+
+This example overrides the default `syslog_log <https://github.com/tstack/lnav/blob/master/src/formats/syslog_log.json>`_
+error detection regex to **not** match the :code:`errors=` string.
+
+.. code-block:: json
+
+ {
+ "syslog_log": {
+ "level": {
+ "error": "(?:(?:(?<![a-zA-Z]))(?:(?i)error(?:s)?(?!=))(?:(?![a-zA-Z]))|failed|failure)"
+ }
+ }
+ }
+
+
+.. _scripts:
+
+Scripts
+-------
+
+Format directories may also contain :file:`.sql` and :file:`.lnav` files to help automate
+log file analysis. The SQL files are executed on startup to create any helper
+tables or views and the '.lnav' script files can be executed using the pipe
+hotkey :kbd:`|`. For example, **lnav** includes a "partition-by-boot" script that
+partitions the log view based on boot messages from the Linux kernel. A script
+can have a mix of SQL and **lnav** commands, as well as include other scripts.
+The type of statement to execute is determined by the leading character on a
+line: a semi-colon begins a SQL statement; a colon starts an **lnav** command;
+and a pipe :code:`|` denotes another script to be executed. Lines beginning with a
+hash are treated as comments. The following variables are defined in a script:
+
+.. envvar:: #
+
+ The number of arguments passed to the script.
+
+.. envvar:: __all__
+
+ A string containing all the arguments joined by a single space.
+
+.. envvar:: 0
+
+ The path to the script being executed.
+
+.. envvar:: 1-N
+
+ The arguments passed to the script.
+
+Remember that you need to use the :ref:`:eval<eval>` command when referencing
+variables in most **lnav** commands. Scripts can provide help text to be
+displayed during interactive usage by adding the following tags in a comment
+header:
+
+ :@synopsis: The synopsis should contain the name of the script and any
+ parameters to be passed. For example::
+
+ # @synopsis: hello-world <name1> [<name2> ... <nameN>]
+
+ :@description: A one-line description of what the script does. For example::
+
+ # @description: Say hello to the given names.
+
+
+
+.. tip::
+
+ The :ref:`:eval<eval>` command can be used to do variable substitution for
+ commands that do not natively support it. For example, to substitute the
+ variable, :code:`pattern`, in a :ref:`:filter-out<filter_out>` command:
+
+ .. code-block:: lnav
+
+ :eval :filter-out ${pattern}
+
+VSCode Extension
+^^^^^^^^^^^^^^^^
+
+The `lnav VSCode Extension <https://marketplace.visualstudio.com/items?itemName=lnav.lnav>`_
+can be installed to add syntax highlighting to lnav scripts.
+
+Installing Formats
+------------------
+
+File formats are loaded from subdirectories in :file:`/etc/lnav/formats` and
+:file:`~/.lnav/formats/`. You can manually create these subdirectories and
+copy the format files into there. Or, you can pass the '-i' option to **lnav**
+to automatically install formats from the command-line. For example:
+
+.. code-block:: bash
+
+ $ lnav -i myformat.json
+ info: installed: /home/example/.lnav/formats/installed/myformat_log.json
+
+Format files installed using this method will be placed in the :file:`installed`
+subdirectory and named based on the first format name found in the file.
+
+You can also install formats from git repositories by passing the repository's
+clone URL. A standard set of repositories is maintained at
+(https://github.com/tstack/lnav-config) and can be installed by passing 'extra'
+on the command line, like so:
+
+.. prompt:: bash
+
+ lnav -i extra
+
+These repositories can be updated by running **lnav** with the '-u' flag.
+
+Format files can also be made executable by adding a shebang (#!) line to the
+top of the file, like so::
+
+ #! /usr/bin/env lnav -i
+ {
+ "myformat_log" : ...
+ }
+
+Executing the format file should then install it automatically:
+
+.. code-block:: bash
+
+ $ chmod ugo+rx myformat.json
+ $ ./myformat.json
+ info: installed: /home/example/.lnav/formats/installed/myformat_log.json
+
+.. _format_order:
+
+Format Order When Scanning a File
+---------------------------------
+
+When **lnav** loads a file, it tries each log format against the first 15,000
+lines [#]_ of the file trying to find a match. When a match is found, that log
+format will be locked in and used for the rest of the lines in that file.
+Since there may be overlap between formats, **lnav** performs a test on
+startup to determine which formats match each others sample lines. Using
+this information it will create an ordering of the formats so that the more
+specific formats are tried before the more generic ones. For example, a
+format that matches certain syslog messages will match its own sample lines,
+but not the ones in the syslog samples. On the other hand, the syslog format
+will match its own samples and those in the more specific format. You can
+see the order of the format by enabling debugging and checking the **lnav**
+log file for the "Format order" message:
+
+.. prompt:: bash
+
+ lnav -d /tmp/lnav.log
+
+For JSON-lines log files, the log message must have the timestamp property
+specified in the format in order to match. If multiple formats match a
+message, the format that has the most matching :code:`line-format` elements
+will win.
+
+.. [#] The maximum number of lines to check can be configured. See the
+ :ref:`tuning` section for more details.
diff --git a/docs/source/group_concat-help.png b/docs/source/group_concat-help.png
new file mode 100644
index 0000000..f7b6f08
--- /dev/null
+++ b/docs/source/group_concat-help.png
Binary files differ
diff --git a/docs/source/hotkey-tips.png b/docs/source/hotkey-tips.png
new file mode 100644
index 0000000..5b78686
--- /dev/null
+++ b/docs/source/hotkey-tips.png
Binary files differ
diff --git a/docs/source/hotkeys.rst b/docs/source/hotkeys.rst
new file mode 100644
index 0000000..4bad1d9
--- /dev/null
+++ b/docs/source/hotkeys.rst
@@ -0,0 +1,300 @@
+.. _hotkeys:
+
+Hotkey Reference
+================
+
+This reference covers the keys used to control **lnav**. Consult the `built-in
+help <https://github.com/tstack/lnav/blob/master/src/help.txt>`_ in **lnav** for
+a more detailed explanation of each key.
+
+Spatial Navigation
+------------------
+
+The majority of these hotkeys should be available in all views.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 6 6 6 20
+
+ * - Keypress
+ -
+ -
+ - Command
+ * - :kbd:`Space`
+ - :kbd:`PgDn`
+ -
+ - Down a page
+ * - :kbd:`Ctrl` + :kbd:`d`
+ -
+ -
+ - Down by half a page
+ * - :kbd:`b`
+ - :kbd:`Backspace`
+ - :kbd:`PgUp`
+ - Up a page
+ * - :kbd:`Ctrl` + :kbd:`u`
+ -
+ -
+ - Up by half a page
+ * - :kbd:`j`
+ - :kbd:`↓`
+ -
+ - Down a line
+ * - :kbd:`k`
+ - :kbd:`↑`
+ -
+ - Up a line
+ * - :kbd:`h`
+ - :kbd:`←`
+ -
+ - Left half a page. In the log view, pressing left while at the start of
+ the message text will reveal the shortened source file name for each line.
+ Pressing again will reveal the full path.
+ * - :kbd:`Shift` + :kbd:`h`
+ - :kbd:`Shift` + :kbd:`←`
+ -
+ - Left ten columns
+ * - :kbd:`l`
+ - :kbd:`→`
+ -
+ - Right half a page
+ * - :kbd:`Shift` + :kbd:`l`
+ - :kbd:`Shift` + :kbd:`→`
+ -
+ - Right ten columns
+ * - :kbd:`Home`
+ - :kbd:`g`
+ -
+ - Top of the view
+ * - :kbd:`End`
+ - :kbd:`G`
+ -
+ - Bottom of the view
+ * - :kbd:`e`
+ - :kbd:`Shift` + :kbd:`e`
+ -
+ - Next/previous error
+ * - :kbd:`w`
+ - :kbd:`Shift` + :kbd:`w`
+ -
+ - Next/previous warning
+ * - :kbd:`n`
+ - :kbd:`Shift` + :kbd:`n`
+ -
+ - Next/previous search hit
+ * - :kbd:`>`
+ - :kbd:`<`
+ -
+ - Next/previous search hit (horizontal)
+ * - :kbd:`f`
+ - :kbd:`Shift` + :kbd:`f`
+ -
+ - Next/previous file
+ * - :kbd:`u`
+ - :kbd:`Shift` + :kbd:`u`
+ -
+ - Next/previous bookmark
+ * - :kbd:`o`
+ - :kbd:`Shift` + :kbd:`o`
+ -
+ - Forward/backward through log messages with a matching "opid" field
+ * - :kbd:`s`
+ - :kbd:`Shift` + :kbd:`s`
+ -
+ - Next/previous slow down in the log message rate
+ * - :kbd:`{`
+ - :kbd:`}`
+ -
+ - Previous/next location in history
+
+Chronological Navigation
+------------------------
+
+These hotkeys are only functional on views that are time-based, like the log
+view or the histogram view.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 5 5 20
+
+ * - Keypress
+ -
+ - Command
+ * - :kbd:`d`
+ - :kbd:`Shift` + :kbd:`d`
+ - Forward/backward 24 hours
+ * - :kbd:`1` - :kbd:`6`
+ - :kbd:`Shift` + :kbd:`1` - :kbd:`6`
+ - Next/previous n'th ten minute of the hour
+ * - :kbd:`7`
+ - :kbd:`8`
+ - Previous/next minute
+ * - :kbd:`0`
+ - :kbd:`Shift` + :kbd:`0`
+ - Next/previous day
+ * - :kbd:`r`
+ - :kbd:`Shift` + :kbd:`r`
+ - Forward/backward by the relative time that was last used with the goto command.
+
+Breadcrumb Navigation
+---------------------
+
+The following hotkeys are related to the breadcrumb bar that is below the top
+status bar.
+
+.. list-table::
+ :header-rows: 1
+ :widths: 5 20
+
+ * - Keypress
+ - Description
+ * - :kbd:`ENTER`
+ - Focus on the breadcrumb bar. Or, if the bar is currently focused,
+ accept the selected value and drop focus.
+ * - :kbd:`Escape`
+ - Drop focus on the breadcrumb bar.
+ * - :kbd:`←`
+ - Select the crumb to the left. If the first crumb is selected, the
+ selection will wrap around to the last crumb.
+ * - :kbd:`→`
+ - Accept the current value, which might mean navigating to the value in
+ the view, then selecting the crumb to the right.
+ * - :kbd:`Ctrl` + :kbd:`a`
+ - Select the first crumb.
+ * - :kbd:`Ctrl` + :kbd:`e`
+ - Select the last crumb.
+ * - :kbd:`↓`
+ - Select the next value in the crumb dropdown.
+ * - :kbd:`↑`
+ - Select the previous value in the crumb dropdown.
+ * - :kbd:`Home`
+ - Select the first value in the crumb dropdown.
+ * - :kbd:`End`
+ - Select the last value in the crumb dropdown.
+
+While a crumb is selected, you can perform a fuzzy search on the possible
+values by typing in the value you are interested in.
+
+.. _hotkeys_bookmarks:
+
+Bookmarks
+---------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 5 20
+
+ * - Keypress
+ - Command
+ * - :kbd:`m`
+ - Mark/unmark the top line or focused line when in cursor mode
+ * - :kbd:`Shift` + :kbd:`m`
+ - Mark/unmark the range of lines from the last marked to the top
+ * - :kbd:`Shift` + :kbd:`j`
+ - Mark/unmark the next line after the previously marked
+ * - :kbd:`Shift` + :kbd:`k`
+ - Mark/unmark the previous line
+ * - :kbd:`c`
+ - Copy marked lines to the clipboard
+ * - :kbd:`Shift` + :kbd:`c`
+ - Clear marked lines
+
+.. _hotkeys_display:
+
+Display
+-------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 5 20
+
+ * - Keypress
+ - Command
+ * - :kbd:`?`
+ - View/leave builtin help
+ * - :kbd:`q`
+ - Return to the previous view/quit
+ * - :kbd:`Shift` + :kbd:`q`
+ - Return to the previous view/quit while matching the top times of the two views
+ * - :kbd:`a`
+ - Restore the view that was previously popped with 'q/Q'
+ * - :kbd:`Shift` + :kbd:`a`
+ - Restore the view that was previously popped with 'q/Q' and match the top times of the views
+ * - :kbd:`Shift` + :kbd:`p`
+ - Switch to/from the pretty-printed view of the displayed log or text files
+ * - :kbd:`Shift` + :kbd:`t`
+ - Display elapsed time between lines
+ * - :kbd:`t`
+ - Switch to/from the text file view
+ * - :kbd:`i`
+ - Switch to/from the histogram view
+ * - :kbd:`Shift` + :kbd:`i`
+ - Switch to/from the histogram view
+ * - :kbd:`v`
+ - Switch to/from the SQL result view
+ * - :kbd:`Shift` + :kbd:`v`
+ - Switch to/from the SQL result view and move to the corresponding in the
+ log_line column
+ * - :kbd:`p`
+ - Toggle the display of the log parser results
+ * - :kbd:`Tab`
+ - In the log/text views, focus on the configuration panel for editing
+ filters and examining the list of loaded files. In the SQL result view,
+ cycle through columns to display as bar graphs
+ * - :kbd:`Ctrl` + :kbd:`l`
+ - Switch to lo-fi mode. The displayed log lines will be dumped to the
+ terminal without any decorations so they can be copied easily.
+ * - :kbd:`Ctrl` + :kbd:`w`
+ - Toggle word-wrap.
+ * - :kbd:`Ctrl` + :kbd:`p`
+ - Show/hide the data preview panel that may be opened when entering
+ commands or SQL queries.
+ * - :kbd:`Ctrl` + :kbd:`f`
+ - Toggle the enabled/disabled state of all filters in the current view.
+ * - :kbd:`x`
+ - Toggle the hiding of log message fields. The hidden fields will be
+ replaced with three bullets and highlighted in yellow.
+ * - :kbd:`Ctrl` + :kbd:`x`
+ - Toggle the cursor mode. Allows moving the focused line instead of
+ keeping it fixed at the top of the current screen.
+ * - :kbd:`=`
+ - Pause/unpause loading of new file data.
+
+Session
+-------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 5 20
+
+ * - Keypress
+ - Command
+ * - :kbd:`Ctrl` + :kbd:`R`
+ - Reset the current :ref:`session<sessions>` state. The session state
+ includes things like filters, bookmarks, and hidden fields.
+
+Query Prompts
+-------------
+
+.. list-table::
+ :header-rows: 1
+ :widths: 5 20
+
+ * - Keypress
+ - Command
+ * - :kbd:`/`
+ - Search for lines matching a regular expression
+ * - :kbd:`;`
+ - Open the :ref:`sql-ext` to execute SQL statements/queries
+ * - :kbd:`:`
+ - Execute an internal command, see :ref:`commands` for more information
+ * - :kbd:`\|`
+ - Execute an lnav script located in a format directory
+ * - :kbd:`Ctrl` + :kbd:`]`
+ - Abort the prompt
+
+Customizing
+-----------
+
+You can customize the behavior of hotkeys by defining your own keymaps.
+Consult the :ref:`Keymaps<keymaps>` configuration section for more information.
diff --git a/docs/source/howitworks.rst b/docs/source/howitworks.rst
new file mode 100644
index 0000000..d111fd0
--- /dev/null
+++ b/docs/source/howitworks.rst
@@ -0,0 +1,13 @@
+
+.. _howitworks:
+
+How It Works
+============
+
+"Magic"
+
+Internal Architecture
+---------------------
+
+The `ARCHITECTURE.md <https://github.com/tstack/lnav/blob/master/ARCHITECTURE.md>`_
+file in the source tree contains some information about lnav's internals.
diff --git a/docs/source/index.rst b/docs/source/index.rst
new file mode 100644
index 0000000..d94c0eb
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,35 @@
+Welcome to lnav's documentation!
+================================
+
+The `Log File Navigator <http://lnav.org>`_ (**lnav**) is an advanced log file
+viewer for the console.
+
+Contents:
+
+.. toctree::
+ :maxdepth: 2
+
+ intro
+ ui
+ hotkeys
+ cli
+ usage
+ cookbook
+ config
+ formats
+ sessions
+ commands
+ sqlext
+ sqltab
+ events
+ data
+ howitworks
+ faq
+
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`modindex`
+* :ref:`search`
diff --git a/docs/source/intro.rst b/docs/source/intro.rst
new file mode 100644
index 0000000..96b0612
--- /dev/null
+++ b/docs/source/intro.rst
@@ -0,0 +1,164 @@
+Introduction
+============
+
+The Log File Navigator, **lnav**, is an advanced log file viewer for the
+terminal. It provides an :ref:`easy-to-use interface<ui>` for monitoring and
+analyzing your log files with little to no setup. Simply point **lnav** at
+your log files and it will automatically detect the :ref:`log_formats`, index
+their contents, and display a combined view of all log messages. You can
+navigate through your logs using a variety of :ref:`hotkeys<hotkeys>`.
+:ref:`Commands<commands>` give you additional control over **lnav**'s behavior
+for doing things like applying filters, tagging messages, and more. You can
+then analyze your log messages using the :ref:`sql-ext`.
+
+Development
+-----------
+
+Development of lnav is hosted on `GitHub <https://github.com/tstack/lnav/>`_.
+
+`Issues <https://github.com/tstack/lnav/issues>`_ should be used for bugs
+and feature requests.
+
+`Discussions <https://github.com/tstack/lnav/discussions>`_ should be used
+for asking questions and sharing tips.
+
+Downloads
+---------
+
+Binaries and source code for lnav can be downloaded from the
+`releases page <https://github.com/tstack/lnav/releases>`_.
+
+When building from source, follow the steps below.
+
+Dependencies
+^^^^^^^^^^^^
+
+When compiling from source, the following dependencies are required:
+
+* `NCurses <http://www.gnu.org/s/ncurses/>`_
+* `PCRE2 <http://www.pcre.org>`_
+* `SQLite <http://www.sqlite.org>`_
+* `ZLib <http://wwww.zlib.net>`_
+* `Bzip2 <http://www.bzip.org>`_
+* `Readline <http://www.gnu.org/s/readline>`_
+* `libcurl <https://curl.haxx.se>`_
+* `libarchive <https://libarchive.org>`_
+
+Installation
+^^^^^^^^^^^^
+
+Check the `downloads page <http://lnav.org/downloads>`_ to see if there are
+packages for your operating system. To compile from source, use the following
+commands:
+
+.. prompt:: bash
+
+ ./configure
+ make
+ sudo make install
+
+
+Viewing Logs
+------------
+
+The arguments to **lnav** are the log files, directories, or URLs to be viewed.
+For example, to view all of the CUPS logs on your system:
+
+.. prompt:: bash
+
+ lnav /var/log/cups
+
+The formats of the logs are determined automatically and indexed on-the-fly.
+See :ref:`log_formats` for a listing of the predefined formats and how to
+define your own.
+
+If no arguments are given, **lnav** will try to open the syslog file on your
+system:
+
+.. prompt:: bash
+
+ lnav
+
+
+Setup
+-----
+
+After starting **lnav**, you might want to set the
+:ref:`configuration options<Configuration>` mentioned below. Configuration in
+**lnav** is done using the :code:`:config` command. To change a configuration
+option, start by pressing :kbd:`:` to enter the command prompt. Then,
+type "config" followed by the option name and value.
+
+.. note::
+
+ Tab-completion is available for these configuration options and, in some
+ cases, their values as well.
+
+
+Keymap
+^^^^^^
+
+The keymap defines the mapping from :ref:`hotkeys<hotkeys>` to commands to
+execute. The default mapping is for "U.S." keyboards. The following command
+can be used to change the keymap:
+
+.. code-block:: lnav
+
+ :config /ui/keymap <keymap-name>
+
+The builtin keymaps are:
+
+ :de: `German <https://github.com/tstack/lnav/blob/master/src/keymaps/de-keymap.json>`_
+ :fr: `French <https://github.com/tstack/lnav/blob/master/src/keymaps/fr-keymap.json>`_
+ :sv: `Swedish <https://github.com/tstack/lnav/blob/master/src/keymaps/sv-keymap.json>`_
+ :uk: `United Kingdom <https://github.com/tstack/lnav/blob/master/src/keymaps/uk-keymap.json>`_
+ :us: `United States <https://github.com/tstack/lnav/blob/master/src/keymaps/us-keymap.json>`_
+
+To create or customize a keymap, consult the :ref:`keymaps` section.
+
+
+Theme
+^^^^^
+
+The visual styling of **lnav** can be customized using a theme. The following
+command can be used to the change the theme:
+
+.. code-block:: lnav
+
+ :config /ui/theme <theme-name>
+
+The builtin themes are:
+`default <https://github.com/tstack/lnav/blob/master/src/themes/default-theme.json>`_,
+`eldar <https://github.com/tstack/lnav/blob/master/src/themes/eldar.json>`_,
+`grayscale <https://github.com/tstack/lnav/blob/master/src/themes/grayscale.json>`_,
+`monocai <https://github.com/tstack/lnav/blob/master/src/themes/monocai.json>`_,
+`night-owl <https://github.com/tstack/lnav/blob/master/src/themes/night-owl.json>`_,
+`solarized-dark <https://github.com/tstack/lnav/blob/master/src/themes/solarized-dark.json>`_,
+and
+`solarized-light <https://github.com/tstack/lnav/blob/master/src/themes/default-theme.json>`_.
+
+To create or customize a theme, consult the :ref:`themes` section.
+
+
+Cursor Mode (v0.11.2+)
+^^^^^^^^^^^^^^^^^^^^^^
+
+The default mode for scrolling in **lnav** is to move the contents of the
+main view when the arrow keys are pressed. Any interactions, such as
+jumping to a search hit, are then focused on the top line in the view.
+Alternatively, you can enable "cursor" mode where there is a cursor line
+in the view that is moved by the arrow keys and other interactions. You
+can enable cursor mode with the following command:
+
+.. code-block:: lnav
+
+ :config /ui/movement/mode cursor
+
+Log Formats
+^^^^^^^^^^^
+
+In order for **lnav** to understand your log files, it needs to told how to
+parse the log messages using a log format definition. There are many log
+formats builtin and **lnav** will automatically determine the best format to
+use. In case your log file is not recognized, consult the :ref:`log_formats`
+section for information on how to create a format.
diff --git a/docs/source/key-encoding-prompt.png b/docs/source/key-encoding-prompt.png
new file mode 100644
index 0000000..fe63412
--- /dev/null
+++ b/docs/source/key-encoding-prompt.png
Binary files differ
diff --git a/docs/source/lnav-breadcrumbs-help.png b/docs/source/lnav-breadcrumbs-help.png
new file mode 100644
index 0000000..b3b80d0
--- /dev/null
+++ b/docs/source/lnav-breadcrumbs-help.png
Binary files differ
diff --git a/docs/source/lnav-config-header.png b/docs/source/lnav-config-header.png
new file mode 100644
index 0000000..079b9c2
--- /dev/null
+++ b/docs/source/lnav-config-header.png
Binary files differ
diff --git a/docs/source/lnav-files-panel.png b/docs/source/lnav-files-panel.png
new file mode 100644
index 0000000..7aed793
--- /dev/null
+++ b/docs/source/lnav-files-panel.png
Binary files differ
diff --git a/docs/source/lnav-filters-panel.png b/docs/source/lnav-filters-panel.png
new file mode 100644
index 0000000..788e63f
--- /dev/null
+++ b/docs/source/lnav-filters-panel.png
Binary files differ
diff --git a/docs/source/lnav-markdown-example.png b/docs/source/lnav-markdown-example.png
new file mode 100644
index 0000000..e51bad0
--- /dev/null
+++ b/docs/source/lnav-markdown-example.png
Binary files differ
diff --git a/docs/source/lnav-pretty-view-after.png b/docs/source/lnav-pretty-view-after.png
new file mode 100644
index 0000000..7fcee79
--- /dev/null
+++ b/docs/source/lnav-pretty-view-after.png
Binary files differ
diff --git a/docs/source/lnav-pretty-view-before.png b/docs/source/lnav-pretty-view-before.png
new file mode 100644
index 0000000..e3a5a13
--- /dev/null
+++ b/docs/source/lnav-pretty-view-before.png
Binary files differ
diff --git a/docs/source/lnav-spectro-cpu-pct.png b/docs/source/lnav-spectro-cpu-pct.png
new file mode 100644
index 0000000..a9df2b6
--- /dev/null
+++ b/docs/source/lnav-spectro-cpu-pct.png
Binary files differ
diff --git a/docs/source/lnav-ui.png b/docs/source/lnav-ui.png
new file mode 100644
index 0000000..2dcad66
--- /dev/null
+++ b/docs/source/lnav-ui.png
Binary files differ
diff --git a/docs/source/open-error.png b/docs/source/open-error.png
new file mode 100644
index 0000000..6454b2d
--- /dev/null
+++ b/docs/source/open-error.png
Binary files differ
diff --git a/docs/source/open-help.png b/docs/source/open-help.png
new file mode 100644
index 0000000..a11656a
--- /dev/null
+++ b/docs/source/open-help.png
Binary files differ
diff --git a/docs/source/open-preview.png b/docs/source/open-preview.png
new file mode 100644
index 0000000..b888376
--- /dev/null
+++ b/docs/source/open-preview.png
Binary files differ
diff --git a/docs/source/query-results.png b/docs/source/query-results.png
new file mode 100644
index 0000000..14ae5b8
--- /dev/null
+++ b/docs/source/query-results.png
Binary files differ
diff --git a/docs/source/sessions.rst b/docs/source/sessions.rst
new file mode 100644
index 0000000..e97e598
--- /dev/null
+++ b/docs/source/sessions.rst
@@ -0,0 +1,23 @@
+
+.. _sessions:
+
+Sessions
+========
+
+Session information is stored automatically for the set of files that were
+passed in on the command-line and reloaded the next time **lnav** is executed.
+The information currently stored is:
+
+* Position within the files being viewed.
+* Active searches for each view.
+* :ref:`Log filters<filtering>`.
+* :ref:`Highlights<highlight>`.
+* :ref:`Hidden files<hide_file>`.
+* :ref:`Hidden fields<hide_fields>`.
+
+Bookmarks and log-time adjustments are stored separately on a per-file basis.
+Note that the bookmarks are associated with files based on the content of the
+first line of the file so that they are preserved even if the file has been
+moved from its current location.
+
+Session data is stored in the :file:`~/.lnav` directory.
diff --git a/docs/source/sql-help.png b/docs/source/sql-help.png
new file mode 100644
index 0000000..44901a4
--- /dev/null
+++ b/docs/source/sql-help.png
Binary files differ
diff --git a/docs/source/sqlext.rst b/docs/source/sqlext.rst
new file mode 100644
index 0000000..221b100
--- /dev/null
+++ b/docs/source/sqlext.rst
@@ -0,0 +1,184 @@
+.. _sql-ext:
+
+SQLite Interface
+================
+
+Log analysis in **lnav** can be done using the SQLite interface. Log messages
+can be accessed via `virtual tables <https://www.sqlite.org/vtab.html>`_ that
+are created for each file format. The tables have the same name as the log
+format and each message is its own row in the table. For example, given the
+following log message from an Apache access log::
+
+ 127.0.0.1 - frank [10/Oct/2000:13:55:36 -0700] "GET /apache_pb.gif HTTP/1.0" 200 2326
+
+These columns would be available for its row in the :code:`access_log` table:
+
+.. csv-table::
+ :class: query-results
+ :header-rows: 1
+
+ log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,c_ip,cs_method,cs_referer,cs_uri_query,cs_uri_stem,cs_user_agent,cs_username,cs_version,sc_bytes,sc_status
+ 0,<NULL>,2000-10-10 13:55:36.000,0,info,1,<NULL>,<NULL>,<NULL>,127.0.0.1,GET,<NULL>,<NULL>,/apache_pb.gif,<NULL>,frank,HTTP/1.0,2326,200
+
+.. note:: Some columns are hidden by default to reduce the amount of noise in
+ results, but they can still be accessed when explicitly used. The hidden
+ columns are: :code:`log_path`, :code:`log_text`, :code:`log_body`, and
+ :code:`log_raw_text`.
+
+You can activate the SQL prompt by pressing the :kbd:`;` key. At the
+prompt, you can start typing in the desired SQL statement and/or double-tap
+:kbd:`TAB` to activate auto-completion. A help window will appear above
+the prompt to guide you in the usage of SQL keywords and functions.
+
+.. figure:: sql-help.png
+ :align: center
+
+ Screenshot of the online help for the SQL prompt.
+
+.. figure:: group_concat-help.png
+ :align: center
+
+ Screenshot of the online help for the :code:`group_concat()` function.
+
+A simple query to perform on an Apache access log might be to get the average
+and maximum number of bytes returned by the server, grouped by IP address:
+
+.. code-block:: custsqlite
+
+ ;SELECT c_ip, avg(sc_bytes), max(sc_bytes) FROM access_log GROUP BY c_ip
+
+After pressing :kbd:`Enter`, SQLite will execute the query using **lnav**'s
+virtual table implementation to extract the data directly from the log files.
+Once the query has finished, the main window will switch to the DB view to
+show the results. Press :kbd:`q` to return to the log view and press :kbd:`v`
+to return to the log view. If the SQL results contain a
+:code:`log_line` column, you can press to :kbd:`Shift` + :kbd:`V` to
+switch between the DB view and the log
+
+.. figure:: query-results.png
+ :align: center
+
+ Screenshot of the SQL results view.
+
+The DB view has the following display features:
+
+* Column headers stick to the top of the view when scrolling.
+* A stacked bar chart of the numeric column values is displayed underneath the
+ rows. Pressing :kbd:`TAB` will cycle through displaying no columns, each
+ individual column, or all columns.
+* JSON columns in the top row can be pretty-printed by pressing :kbd:`p`.
+ The display will show the value and JSON-Pointer path that can be passed to
+ the `jget`_ function.
+
+
+Log Tables
+----------
+
+Each log format has its own database table that can be used to access log
+messages that match that format. The table name is the same as the format
+name, for example, the :code:`syslog_log` format will have a table that is
+also named :code:`syslog_log`. There is also an :code:`all_logs` table
+that provides access to all messages from all formats.
+
+.. note:: Only the displayed log messages are reflected in the SQLite
+ interface. Any log messages that have been filtered out are not
+ accessible.
+
+The columns in the log tables are made up of several builtins along with
+the values captured by the log format specification. Use the :code:`.schema`
+command in the SQL prompt to examine a dump of the current database schema.
+
+The following columns are builtin and included in a :code:`SELECT *`:
+
+ :log_line: The line number for the message in the log view.
+ :log_part: The partition the message is in. This column can be changed by
+ an :code:`UPDATE` or the :ref:`:parition-name<partition_name>` command.
+ :log_time: The adjusted timestamp for the log message. This time can differ
+ from the log message's time stamp if it arrived out-of-order and the log
+ format expects log files to be time-ordered.
+ :log_actual_time: The log messages original timestamp in the file.
+ :log_idle_msecs: The difference in time between this messages and the
+ previous. The unit of time is milliseconds.
+ :log_level: The log message level.
+ :log_mark: True if the log message was marked by the user.
+ :log_comment: The comment for the message. This column can be changed by
+ an :code:`UPDATE` or the :ref:`:comment<comment>` command.
+ :log_tags: A JSON list of tags for the message. This column can be changed
+ by an :code:`UPDATE` or the :ref:`:tag<tag>` command.
+ :log_filters: A JSON list of filter IDs that matched this message
+
+The following columns are builtin and are hidden, so they will *not* be
+included in a :code:`SELECT *`:
+
+ :log_time_msecs: The adjusted timestamp for the log message as the number of
+ milliseconds from the epoch. This column can be more efficient to use for
+ time-related operations, like :ref:`timeslice()<timeslice>`.
+ :log_path: The path to the log file this message is from.
+ :log_text: The full text of the log message.
+ :log_body: The body of the log message.
+ :log_raw_text: The raw text of this message from the log file. In this case
+ of JSON and CSV logs, this will be the exact line of JSON-Line and CSV
+ text from the file.
+
+Extensions
+----------
+
+To make it easier to analyze log data from within **lnav**, there are several
+built-in extensions that provide extra functions and collators beyond those
+`provided by SQLite <http://www.sqlite.org/lang_corefunc.html>`_. The majority
+of the functions are from the
+`extensions-functions.c <http://www.sqlite.org/contrib>`_ file available from
+the `sqlite.org <http://sqlite.org>`_ web site.
+
+.. tip:: You can include a SQLite database file on the command-line and use
+ **lnav**'s interface to perform queries. The database will be attached with
+ a name based on the database file name.
+
+Commands
+--------
+
+A SQL command is an internal macro implemented by lnav.
+
+* :code:`.schema` - Open the schema view. This view contains a dump of the
+ schema for the internal tables and any tables in attached databases.
+* :code:`.msgformats` - Executes a canned query that groups and counts log
+ messages by the format of their message bodies. This command can be useful
+ for quickly finding out the types of messages that are most common in a log
+ file.
+
+Variables
+---------
+
+The following variables are available in SQL statements:
+
+* :code:`$LINES` - The number of lines in the terminal window.
+* :code:`$COLS` - The number of columns in the terminal window.
+
+Environment
+-----------
+
+Environment variables can be accessed in queries using the usual syntax of
+:code:`$VAR_NAME`. For example, to read the value of the "USER" variable, you
+can write:
+
+.. code-block:: custsqlite
+
+ ;SELECT $USER
+
+.. _collators:
+
+Collators
+---------
+
+* **naturalcase** - Compare strings "naturally" so that number values in the
+ string are compared based on their numeric value and not their character
+ values. For example, "foo10" would be considered greater than "foo2".
+* **naturalnocase** - The same as naturalcase, but case-insensitive.
+* **ipaddress** - Compare IPv4/IPv6 addresses.
+
+Reference
+---------
+
+The following is a reference of the SQL syntax and functions that are available:
+
+.. include:: ../../src/internals/sql-ref.rst
diff --git a/docs/source/sqltab.rst b/docs/source/sqltab.rst
new file mode 100644
index 0000000..2531390
--- /dev/null
+++ b/docs/source/sqltab.rst
@@ -0,0 +1,263 @@
+.. _sql-tab:
+
+SQLite Tables Reference
+=======================
+
+In addition to the tables generated for each log format, **lnav** includes
+the following tables/views:
+
+* `environ`_
+* `lnav_events`_
+* `lnav_file`_
+* `lnav_file_metadata`_
+* `lnav_user_notifications`_
+* `lnav_views`_
+* `lnav_views_echo`_
+* `lnav_view_files`_
+* `lnav_view_stack`_
+* `lnav_view_filters`_
+* `lnav_view_filter_stats`_
+* `lnav_view_filters_and_stats`_
+* `all_logs`_
+* `http_status_codes`_
+* `regexp_capture(<string>, <regex>)`_
+
+These extra tables provide useful information and can let you manipulate
+**lnav**'s internal state. You can get a dump of the entire database schema
+by executing the '.schema' SQL command, like so::
+
+ ;.schema
+
+environ
+-------
+
+The **environ** table gives you access to the **lnav** process' environment
+variables. You can :code:`SELECT`, :code:`INSERT`, and :code:`UPDATE`
+environment variables, like so:
+
+.. code-block:: custsqlite
+
+ ;SELECT * FROM environ WHERE name = 'SHELL'
+ name value
+ SHELL /bin/tcsh
+
+ ;UPDATE environ SET value = '/bin/sh' WHERE name = 'SHELL'
+
+Environment variables can be used to store simple values or pass values
+from **lnav**'s SQL environment to **lnav**'s commands. For example, the
+:code:`:open` command will do variable substitution, so you can insert a variable
+named "FILENAME" and then open it in **lnav** by referencing it with
+"$FILENAME":
+
+.. code-block:: custsqlite
+
+ ;INSERT INTO environ VALUES ('FILENAME', '/path/to/file')
+ :open $FILENAME
+
+
+.. _table_lnav_events:
+
+lnav_events
+-----------
+
+The :code:`lnav_events` table allows you to react to events that occur while
+**lnav** is running using SQLite triggers. For example, when a file is
+opened, a row is inserted into the :code:`lnav_events` table that contains
+a timestamp and a JSON object with the event ID and the path of the file.
+The following columns are available in this table:
+
+ :ts: The timestamp of the event.
+ :content: A JSON object that contains the event information. See the
+ :ref:`event_reference` for more information about the types
+ of events that are available.
+
+lnav_file
+---------
+
+The :code:`lnav_file` table allows you to examine and perform limited updates to
+the metadata for the files that are currently loaded into **lnav**. The
+following columns are available in this table:
+
+ :device: The device the file is stored on.
+ :inode: The inode for the file on the device.
+ :filepath: If this is a real file, it will be the absolute path. Otherwise,
+ it is a symbolic name. If it is a symbolic name, it can be UPDATEd so that
+ this file will be considered when saving and loading session information.
+ :format: The log file format for the file.
+ :lines: The number of lines in the file.
+ :time_offset: The millisecond offset for timestamps. This column can be
+ UPDATEd to change the offset of timestamps in the file.
+
+lnav_file_metadata
+------------------
+
+The :code:`lnav_file_metadata` table gives access to metadata associated with a
+loaded file. Currently,
+
+:filepath: The path to the file.
+:descriptor: A descriptor that identifies the source of the metadata. The
+ following descriptors are supported:
+
+ :net.zlib.gzip.header: The header on a gzipped file. The content is a
+ JSON object with the following properties:
+
+ :name: The original name of the file.
+ :mtime: The last modified time of the file when it was compressed.
+ :comment: A text comment associated with the file.
+ :net.daringfireball.markdown.frontmatter: The frontmatter on a
+ markdown file. If the frontmatter is delimited by three dashes
+ (:code:`---`), the :code:`mimetype` will be :code:`application/yaml`.
+ If the frontmatter is delimited by three pluses (:code:`+++`) the
+ :code:`mimetype` will be :code:`application/toml`.
+:mimetype: The MIME type of the metadata.
+:content: The metadata itself.
+
+.. _table_lnav_user_notifications:
+
+lnav_user_notifications
+-----------------------
+
+The :code:`lnav_user_notifications` table allows you to display a custom message
+in the top-right corner of the UI. For example, to display "Hello, World!",
+you can enter:
+
+.. code-block:: custsqlite
+
+ ;REPLACE INTO lnav_user_notifications (message) VALUES ('Hello, World!')
+
+There are additional columns to have finer control of what is displayed and
+when:
+
+ :id: The unique ID for the message, defaults to "org.lnav.user". This is
+ the primary key for the table, so more than one type of message is not
+ allowed.
+ :priority: The priority of the message. Higher priority messages will be
+ displayed until they are cleared or are expired.
+ :created: The time the message was created.
+ :expiration: The time when the message should expire or NULL if it should
+ not automatically expire.
+ :views: A JSON array of view names where the message is applicable or NULL
+ if the message should be shown in all views.
+ :message: The message itself.
+
+This table will most likely be used in combination with :ref:`Events` and the
+`lnav_views_echo`_ table.
+
+lnav_views
+----------
+
+The :code:`lnav_views` table allows you to SELECT and UPDATE information related
+to **lnav**'s "views" (e.g. log, text, ...). The following columns are
+available in this table:
+
+:name: The name of the view.
+:top: The line number at the top of the view. This value can be UPDATEd to
+ move the view to the given line.
+:left: The left-most column number to display. This value can be UPDATEd to
+ move the view left or right.
+:height: The number of lines that are displayed on the screen.
+:inner_height: The number of lines of content being displayed.
+:top_time: The timestamp of the top line in the view or NULL if the view is
+ not time-based. This value can be UPDATEd to move the view to the given
+ time.
+:top_file: The file the top line in the view is from.
+:paused: Indicates if the view is paused and will not load new data.
+:search: The search string for this view. This value can be UPDATEd to
+ initiate a text search in this view.
+:filtering: Indicates if the view is applying filters.
+:movement: The movement mode, either 'top' or 'cursor'.
+:top_meta: A JSON object that contains metadata related to the top line
+ in the view.
+:selection: The number of the line that is focused for selection.
+
+lnav_views_echo
+---------------
+
+The :code:`lnav_views_echo` table is a real SQLite table that you can create
+TRIGGERs on in order to react to users moving around in a view.
+
+.. note::
+
+ The table is periodically updated to reflect the current state of the views.
+ The changes are *not* performed immediately after the user action.
+
+lnav_view_files
+---------------
+
+The :code:`lnav_view_files` table provides access to details about the files
+displayed in a particular view. The main purpose of this table is to allow
+you to programmatically control which files are shown / hidden in the view.
+The following columns are available in this table:
+
+:view_name: The name of the view.
+:filepath: The file's path.
+:visible: Determines whether the file is visible in the view. This column
+ can be changed using an :code:`UPDATE` statement to hide or show the file.
+
+lnav_view_stack
+---------------
+
+The :code:`lnav_view_stack` table allows you to :code:`SELECT` and :code:`DELETE`
+from the stack of **lnav** "views" (e.g. log, text, ...). The following columns
+are available in this table:
+
+ :name: The name of the view.
+
+.. _table_lnav_view_filters:
+
+lnav_view_filters
+-----------------
+
+The :code:`lnav_view_filters` table allows you to manipulate the filters in the
+**lnav** views. The following columns are available in this table:
+
+ :view_name: The name of the view the filter is applied to.
+ :filter_id: The filter identifier. This will be assigned on insertion.
+ :enabled: Indicates whether this filter is enabled or disabled.
+ :type: The type of filter, either 'in' or 'out'.
+ :pattern: The regular expression to filter on.
+
+This table supports :code:`SELECT`, :code:`INSERT`, :code:`UPDATE`, and
+:code:`DELETE` on the table rows to read, create, update, and delete
+filters for the views.
+
+lnav_view_filter_stats
+----------------------
+
+The :code:`lnav_view_filter_stats` table allows you to get information about how
+many lines matched a given filter. The following columns are available in
+this table:
+
+ :view_name: The name of the view.
+ :filter_id: The filter identifier.
+ :hits: The number of lines that matched this filter.
+
+This table is read-only.
+
+lnav_view_filters_and_stats
+---------------------------
+
+The :code:`lnav_view_filters_and_stats` view joins the :code:`lnav_view_filters`
+table with the :code:`lnav_view_filter_stats` table into a single view for ease of use.
+
+all_logs
+--------
+
+.. f0:sql.tables.all_logs
+
+The :code:`all_logs` table lets you query the format derived from the **lnav**
+log message parser that is used to automatically extract data, see
+:ref:`data-ext` for more details.
+
+http_status_codes
+-----------------
+
+The :code:`http_status_codes` table is a handy reference that can be used to turn
+HTTP status codes into human-readable messages.
+
+regexp_capture(<string>, <regex>)
+---------------------------------
+
+The :code:`regexp_capture()` table-valued function applies the regular expression
+to the given string and returns detailed results for the captured portions of
+the string.
diff --git a/docs/source/ui.rst b/docs/source/ui.rst
new file mode 100644
index 0000000..a718d06
--- /dev/null
+++ b/docs/source/ui.rst
@@ -0,0 +1,327 @@
+.. _ui:
+
+User Interface
+==============
+
+The **lnav** TUI displays the content of the current "view" in the middle,
+with status bars above and below, and the interactive prompt as the last line.
+
+.. figure:: lnav-ui.png
+ :align: center
+ :alt: Screenshot of lnav showing a mix of syslog and web access_log messages.
+
+ Screenshot of **lnav** viewing syslog and web access_log messages.
+
+The default view shows the log messages from the log files that have been
+loaded. There are other views for displaying content like plaintext files
+and SQL results. The :ref:`ui_views` section describes the characteristics of
+each view in more detail. You can switch to the different views using the
+hotkeys described in the :ref:`hotkeys_display` section or by pressing
+:kbd:`ENTER` to activate the breadcrumb bar, moving to the first crumb, and
+then selecting the desired view. You can switch back to the previous view by
+pressing :kbd:`q`. You can switch forward to the new view by pressing
+:kbd:`a`. If the views are time-based (e.g. log and histogram), pressing
+:kbd:`Shift` + :kbd:`q` and :kbd:`Shift` + :kbd:`a` will synchronize the top
+times in the views.
+
+**lnav** provides many operations to work with the log/text data in the
+main view. For example, you can add comments and tags to log messages.
+By default, the top line is used as the reference point to edit the
+comment or tags. Alternatively, you can press :kbd:`Ctrl` + :kbd:`x`
+to switch to "cursor" mode where the "focused" line is highlighted and
+most operations now work with that line. When in "cursor" mode, the
+:kbd:`↑` and :kbd:`↓` keys now move the focused line instead of scrolling
+the view. Jumping to bookmarks, like errors, will also move the focused
+line instead of moving the next error to the top of the view.
+
+The right side of the display has a proportionally sized 'scrollbar' that
+shows:
+
+* the current position in the file;
+* the locations of errors/warnings in the log files by using red or yellow
+ coloring;
+* the locations of search hits by using a tick-mark pointing to the left;
+* the locations of bookmarks by using a tick-mark pointing to the right.
+
+Top Status Bar
+--------------
+
+The top status bar shows the current time and messages stored in the
+:ref:`table_lnav_user_notifications` table.
+
+Below the top status bar is the breadcrumb bar that displays the semantic
+location of the focused line in the main view. For example, within a
+pretty-printed JSON document, it will show the path to property at the top
+of the view. The actual content of the bar depends on the current view and
+will be updated as you navigate around the main view. The bar can also be
+used to navigate around the document by focusing on it.
+
+Breadcrumb Bar
+--------------
+
+.. figure:: lnav-breadcrumbs-help.png
+ :align: center
+ :figwidth: 90%
+
+ Screenshot of the breadcrumb bar focused and navigating the help text
+
+To focus on the breadcrumb bar, press :kbd:`ENTER`. The :kbd:`←`/:kbd:`→`
+cursor keys can be used to select a crumb and the :kbd:`↑`/:kbd:`↓` keys can
+be used select a value of that crumb. To accept a value and drop focus on the
+bar, press :kbd:`ENTER`. To accept a value and move to the next crumb, press
+:kbd:`→`. Using :kbd:`→` makes it quicker to drill down into a document
+without having to constantly switch focus. To drop focus on the bar without
+accepting anything, press :kbd:`Escape`.
+
+There are three types of crumbs:
+
+* a dropdown where one of a limited set of values can be selected;
+* a combobox where a value can be entered directly or selected;
+* a numeric input for entering array indexes.
+
+When a dropdown or combobox is selected, you can type part of the desired value
+to filter the list of values. For example, the first crumb is always the
+current view, typing in "hi" will filter the list down to the "HIST" value.
+
+Configuration Panels
+--------------------
+
+.. figure:: lnav-config-header.png
+ :align: center
+ :figwidth: 90%
+
+ Screenshot of the header for the configuration panels when they are hidden.
+
+After the main view content, there is a header bar for two configuration
+panels: Files and Filters. These panels provide visual access to parts of
+lnav's configuration. To access the panels, press the :kbd:`TAB` key.
+To hide the panels again, press :kbd:`q`.
+
+.. figure:: lnav-files-panel.png
+ :align: center
+ :figwidth: 90%
+
+ Screenshot of the files panel showing the loaded files.
+
+The Files panel is open initially to display progress in loading files.
+The following information can be displayed for each file:
+
+* the "unique" portion of the path relative to the other files;
+* the amount of data that has been indexed;
+* the date range of log messages contained in the file;
+* the errors that were encountered while trying to index the file;
+* the notes recorded for files where some automatic action was taken,
+ like hiding the file if it was seen as a duplicate of another file.
+
+.. figure:: lnav-filters-panel.png
+ :align: center
+ :figwidth: 90%
+
+ Screenshot of the filters panel showing an OUT and a disabled IN filter.
+
+If the view supports filtering, there will be a status line showing the
+following:
+
+* the number of enabled filters and the total number of filters;
+* the number of lines that are **not** displayed because of filtering.
+
+To edit the filters, you can press TAB to change the focus from the main
+view to the filter editor. The editor allows you to create, enable/disable,
+and delete filters easily.
+
+Bottom Status Bar
+-----------------
+
+The second to last line is the bottom status bar, which shows the following:
+
+* the line number of the focused line, starting from zero;
+* the location within the view, as a percentage;
+* the current search hit, the total number of hits, and the search term;
+* the loading indicator.
+
+When the interactive prompt is active, this bar can show the prompt
+description, help text, or error message.
+
+Prompt
+------
+
+Finally, the last line on the display is where you can enter search
+patterns and execute internal commands, such as converting a
+unix-timestamp into a human-readable date. The following key-presses
+will activate a corresponding prompt:
+
+* :kbd:`/` - The search prompt. You can enter a PCRE2-flavored regular
+ expression to search for in the current view.
+* :kbd:`:` - The command prompt. Commands are used to perform common
+ operations.
+* :kbd:`;` - The SQL prompt. SQL queries can be used for log analysis
+ and manipulating **lnav**'s state.
+* :kbd:`|` - The script prompt. Enter a path to the lnav script to
+ execute, along with the arguments to pass in.
+
+The command-line is by the readline library, so the usual set of keyboard
+shortcuts can be used for editing and moving within the command-line.
+
+.. _ui_views:
+
+Views
+-----
+
+The accessible content within lnav is separated into the following views.
+
+LOG
+^^^
+
+The log view displays the log messages from any loaded log files in time
+order. This view will be shown by default if any log messages are available.
+
+On color displays, the log messages will be highlighted as follows:
+
+* Errors will be colored in red;
+* warnings will be yellow;
+* search hits are reverse video;
+* various color highlights will be applied to: IP addresses, SQL keywords,
+ XML tags, file and line numbers in Java backtraces, and quoted strings;
+* "identifiers" in the messages will be randomly assigned colors based on their
+ content (works best on "xterm-256color" terminals).
+
+.. note::
+
+ If the coloring is too much for your tastes, you can change to the
+ "grayscale" theme by entering the following command:
+
+ .. code-block:: lnav
+
+ :config /ui/theme grayscale
+
+.. note::
+
+ If a log message has a timestamp that is out-of-order with its neighboring
+ messages, the timestamp will be highlighted in yellow. When one of these
+ messages is at the top of the log view, an overlay will display the
+ difference between the "actual time" and the "received time". The "actual
+ time" is the original textual timestamp. The "received time" is the time
+ of an earlier message that is larger than this log message's time.
+
+The source file name for each message can be displayed by scrolling left.
+Scrolling left once will show the shortened version of the file name relative
+to the other files that are loaded. In the shortened version, the unique
+portion of the file name will be in square brackets. Scrolling left a second
+time will show the full path.
+
+The breadcrumb bar will show the following crumbs:
+
+* the timestamp for the focused line;
+* the log format for the focused line;
+* the name of the file the focused line was pulled from;
+* the "operation ID" of the focused log message, if it is supported by the log
+ format.
+
+These crumbs are interactive and can be used to navigate to different parts
+of the log view. For example, selecting a different value in the log format
+crumb will jump to the first message with that format.
+
+TEXT
+^^^^
+
+The text view displays files for which lnav could not detect any log messages.
+
+Press :kbd:`t` to switch to the text view. While in the text view, you can
+press :kbd:`f` or :kbd:`Shift` + :kbd:`F` to switch to the next / previous
+text file.
+
+Markdown
+""""""""
+
+Files with an :code:`.md` (or :code:`.markdown`) extension will be treated as
+Markdown files and rendered separately.
+
+ .. figure:: lnav-markdown-example.png
+ :align: center
+
+ Viewing the **lnav** :file:`README.md` file.
+
+
+DB
+^^
+
+The DB view shows the results of queries done through the SQLite interface.
+You can execute a query by pressing :kbd:`;` and then entering a SQL statement.
+
+Press :kbd:`v` to switch to the database result view.
+
+HELP
+^^^^
+
+The help view displays the builtin help text. While in the help view, the
+breadcrumb bar can be used to navigate to different sections of the document.
+
+Press :kbd:`?` to switch to the help view.
+
+HIST
+^^^^
+
+The histogram view displays a stacked bar chart of messages over time
+classified by their log level and whether they've been bookmarked.
+
+Press :kbd:`i` to switch back and forth to the histogram view. You
+can also press :kbd:`Shift` + :kbd:`i` to toggle the histogram view
+while synchronizing the top time. While in the histogram view,
+pressing :kbd:`z` / :kbd:`Shift` + :kbd:`z` will zoom in/out.
+
+PRETTY
+^^^^^^
+
+The pretty-print view takes the text displayed in the current view and shows
+the result of a pretty-printer run on that text. For example, if a log
+message contained an XML message on a single line, the pretty-printer would
+break the XML across multiple lines with appropriate indentation.
+
+.. figure:: lnav-pretty-view-before.png
+ :align: center
+ :figwidth: 90%
+
+ Screenshot of a log message with a flat JSON object.
+
+.. figure:: lnav-pretty-view-after.png
+ :align: center
+ :figwidth: 90%
+
+ Screenshot of the same log message in the PRETTY view. The JSON object
+ is now indented for easier reading.
+
+Press :kbd:`Shift` + :kbd:`P` to switch to the pretty-print view.
+
+SCHEMA
+^^^^^^
+
+The schema view displays the current schema of the builtin SQLite database.
+
+Press :kbd:`;` to enter the SQL prompt and then enter :code:`.schema` to
+open the schema view.
+
+SPECTRO
+^^^^^^^
+
+The spectrogram view is a "three"-dimensional display of data points of a log
+field or a SQL query column. The dimensions are time on the Y axis, the range
+of data point values on the X axis, and the number of data points as a color.
+For example, if you were to visualize process CPU usage over time, the range
+of values on the X axis would be CPU percentages and there would be colored
+blocks at each point on the line where a process had that CPU percentage, like
+so
+
+.. figure:: lnav-spectro-cpu-pct.png
+ :align: center
+
+ Screenshot of the **lnav** spectrogram view showing CPU usage of processes.
+
+The colors correspond to the relative number of data points in a bucket.
+The legend overlaid at the top line in the view shows the counts of data
+points that are in a particular color, with green having the fewest number of
+data points, yellow the middle, and red the most. You can select a particular
+bucket using the cursor keys to see the exact number of data points and the
+range of values. The panel at the bottom of the view shows the data points
+themselves from the original source, the log file or the SQL query results.
+You can press :kbd:`TAB` to focus on the details panel so you can scroll
+around and get a closer look at the values.
diff --git a/docs/source/usage.rst b/docs/source/usage.rst
new file mode 100644
index 0000000..d43ed52
--- /dev/null
+++ b/docs/source/usage.rst
@@ -0,0 +1,291 @@
+.. _usage:
+
+Usage
+=====
+
+This chapter contains an overview of how to use **lnav**.
+
+
+Basic Controls
+--------------
+
+Like most file viewers, scrolling through files can be done with the usual
+:ref:`hotkeys<hotkeys>`. For non-trivial operations, you can enter the
+:ref:`command<commands>` prompt by pressing :kbd:`:`. To analyze data in a
+log file, you can enter the :ref:`SQL prompt<sql-ext>` by pressing :kbd:`;`.
+
+.. tip::
+
+ Check the bottom right corner of the screen for tips on hotkeys that might
+ be useful in the current context.
+
+ .. figure:: hotkey-tips.png
+ :align: center
+
+ When **lnav** is first open, it suggests using :kbd:`e` and
+ :kbd:`Shift` + :kbd:`e` to jump to error messages.
+
+
+Viewing Files
+-------------
+
+The files to view in **lnav** can be given on the command-line or passed to the
+:ref:`:open<open>` command. A
+`glob pattern <https://en.wikipedia.org/wiki/Glob_(programming)>`_ can be given
+to watch for files with a common name. If the path is a directory, all of the
+files in the directory will be opened and the directory will be monitored for
+files to be added or removed from the view. If the path is an archive or
+compressed file (and lnav was built with libarchive), the archive will be
+extracted to a temporary location and the files within will be loaded. The
+files that are found will be scanned to identify their file format. Files
+that match a log format will be collated by time and displayed in the LOG
+view. Plain text files can be viewed in the TEXT view, which can be accessed
+by pressing :kbd:`t`.
+
+
+Archive Support
+^^^^^^^^^^^^^^^
+
+.. f0:archive
+
+If **lnav** is compiled with `libarchive <https://www.libarchive.org>`_,
+any files to be opened will be examined to see if they are a supported archive
+type. If so, the contents of the archive will be extracted to the
+:code:`$TMPDIR/lnav-user-${UID}-work/archives/` directory. Once extracted, the
+files within will be loaded into lnav. To speed up opening large amounts of
+files, any file that meets the following conditions will be automatically
+hidden and not indexed:
+
+* Binary files
+* Plain text files that are larger than 128KB
+* Duplicate log files
+
+The unpacked files will be left in the temporary directory after exiting
+**lnav** so that opening the same archive again will be faster. Unpacked
+archives that have not been accessed in the past two days will be automatically
+deleted the next time **lnav** is started.
+
+
+.. _remote:
+
+Remote Files
+^^^^^^^^^^^^
+
+Files on remote machines can be viewed and tailed if you have access to the
+machines via SSH. First, make sure you can SSH into the remote machine without
+any interaction by: 1) accepting the host key as known and 2) copying your
+identity's public key to the :file:`.ssh/authorized_keys` file on the remote
+machine. Once the setup is complete, you can open a file on a remote host
+using the same syntax as :command:`scp(1)` where the username and host are
+given, followed by a colon, and then the path to the files, like so::
+
+ [user@]host:/path/to/logs
+
+For example, to open :file:`/var/log/syslog.log` on "host1.example.com" as the
+user "dean", you would write:
+
+.. prompt:: bash
+
+ lnav dean@host1.example.com:/var/log/syslog.log
+
+Remote files can also be opened using the :ref:`:open<open>` command. Opening
+a remote file in the TUI has the advantage that the file path can be
+:kbd:`TAB`-completed and a preview is shown of the first few lines of the
+file.
+
+.. note::
+
+ If lnav is installed from the `snap <https://snapcraft.io/lnav>`_, you will
+ need to connect it to the
+ `ssh-keys plug <https://snapcraft.io/docs/ssh-keys-interface>`_ using the
+ following command:
+
+ .. prompt:: bash
+
+ sudo snap connect lnav:ssh-keys
+
+.. note::
+
+ Remote file access is implemented by transferring an
+ `αcτµαlly pδrταblε εxεcµταblε <https://justine.lol/ape.html>`_ to the
+ destination and invoking it. An APE binary can run on most any x86_64
+ machine and OS (i.e. MacOS, Linux, FreeBSD, Windows). The binary is
+ baked into the lnav executable itself, so there is no extra setup that
+ needs to be done on the remote machine.
+
+ The binary file is named ``tailer.bin.XXXXXX`` where *XXXXXX* is 6 random digits.
+ The file is, under normal circumstancies, deleted immediately.
+
+Searching
+---------
+
+Any log messages that are loaded into **lnav** are indexed by time and log
+level (e.g. error, warning) to make searching quick and easy with
+:ref:`hotkeys<hotkeys>`. For example, pressing :kbd:`e` will jump to the
+next error in the file and pressing :kbd:`Shift` + :kbd:`e` will jump to
+the previous error. Plain text searches can be done by pressing :kbd:`/`
+to enter the search prompt. A regular expression can be entered into the
+prompt to start a search through the current view.
+
+
+.. _filtering:
+
+Filtering
+---------
+
+To reduce the amount of noise in a log file, **lnav** can hide log messages
+that match certain criteria. The following sub-sections explain ways to go
+about that.
+
+
+Regular Expression Match
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+If there are log messages that you are not interested in, you can do a
+"filter out" to hide messages that match a pattern. A filter can be created
+using the interactive editor, the :ref:`:filter-out<filter_out>` command, or
+by doing an :code:`INSERT` into the
+:ref:`lnav_view_filters<table_lnav_view_filters>` table.
+
+If there are log messages that you are only interested in, you can do a
+"filter in" to only show messages that match a pattern. The filter can be
+created using the interactive editor, the :ref:`:filter-in<filter_in>` command,
+or by doing an :code:`INSERT` into the
+:ref:`lnav_view_filters<table_lnav_view_filters>` table.
+
+
+SQLite Expression
+^^^^^^^^^^^^^^^^^
+
+Complex filtering can be done by passing a SQLite expression to the
+:ref:`:filter-expr<filter_expr>` command. The expression will be executed for
+every log message and if it returns true, the line will be shown in the log
+view.
+
+
+Time
+^^^^
+
+To limit log messages to a given time frame, the
+:ref:`:hide-lines-before<hide_lines_before>` and
+:ref:`:hide-lines-after<hide_lines_after>` commands can be used to specify
+the beginning and end of the time frame.
+
+
+Log level
+^^^^^^^^^
+
+To hide messages below a certain log level, you can use the
+:ref:`:set-min-log-level<set_min_log_level>` command.
+
+.. _search_tables:
+
+Search Tables
+-------------
+
+Search tables allow you to access arbitrary data in log messages through
+SQLite virtual tables. If there is some data in a log message that you can
+match with a regular expression, you can create a search-table that matches
+that data and any capture groups will be plumbed through as columns in the
+search table.
+
+Creating a search table can be done interactively using the
+:ref:`:create-search-table<create_search_table>` command or by adding it to
+a :ref:`log format definition<log_formats>`. The main difference between
+the two is that tables defined as part of a format will only search messages
+from log files with that format and the tables will include log message
+columns defined in that format. Whereas a table created with the command
+will search messages from all different formats and no format-specific
+columns will be included in the table.
+
+.. _taking_notes:
+
+Taking Notes
+------------
+
+A few of the columns in the log tables can be updated on a row-by-row basis to
+allow you to take notes. The majority of the columns in a log table are
+read-only since they are backed by the log files themselves. However, the
+following columns can be changed by an :code:`UPDATE` statement:
+
+* **log_part** - The "partition" the log message belongs to. This column can
+ also be changed by the :ref:`:partition-name<partition_name>` command.
+* **log_mark** - Indicates whether the line has been bookmarked.
+* **log_comment** - A free-form text field for storing commentary. This
+ column can also be changed by the :ref:`:comment<comment>` command.
+* **log_tags** - A JSON list of tags associated with the log message. This
+ column can also be changed by the :ref:`:tag<tag>` command.
+
+While these columns can be updated by through other means, using the SQL
+interface allows you to make changes automatically and en masse. For example,
+to bookmark all lines that have the text "something interesting" in the log
+message body, you can execute:
+
+.. code-block:: custsqlite
+
+ ;UPDATE all_logs SET log_mark = 1 WHERE log_body LIKE '%something interesting%'
+
+As a more advanced example of the power afforded by SQL and **lnav**'s virtual
+tables, we will tag log messages where the IP address bound by dhclient has
+changed. For example, if dhclient reports "bound to 10.0.0.1" initially and
+then reports "bound to 10.0.0.2", we want to tag only the messages where the
+IP address was different from the previous message. While this can be done
+with a single SQL statement [#]_, we will break things down into a few steps for
+this example. First, we will use the :ref:`:create-search-table<create_search_table>`
+command to match the dhclient message and extract the IP address:
+
+.. code-block:: lnav
+
+ :create-search-table dhclient_ip bound to (?<ip>[^ ]+)
+
+The above command will create a new table named :code:`dhclient_ip` with the
+standard log columns and an :code:`ip` column that contains the IP address.
+Next, we will create a view over the :code:`dhclient_ip` table that returns
+the log message line number, the IP address from the current row and the IP
+address from the previous row:
+
+.. code-block:: custsqlite
+
+ ;CREATE VIEW IF NOT EXISTS dhclient_ip_changes AS SELECT log_line, ip, lag(ip) OVER (ORDER BY log_line) AS prev_ip FROM dhclient_ip
+
+Finally, the following :code:`UPDATE` statement will concatenate the tag
+"#ipchanged" onto the :code:`log_tags` column for any rows in the view where
+the current IP is different from the previous IP:
+
+.. code-block:: custsqlite
+
+ ;UPDATE syslog_log SET log_tags = json_concat(log_tags, '#ipchanged') WHERE log_line IN (SELECT log_line FROM dhclient_ip_changes WHERE ip != prev_ip)
+
+Since the above can be a lot to type out interactively, you can put these
+commands into a :ref:`script<scripts>` and execute that script with the
+:kbd:`\|` hotkey.
+
+.. [#] The expression :code:`regexp_match('bound to ([^ ]+)', log_body) as ip`
+ can be used to extract the IP address from the log message body.
+
+Sharing Sessions With Others
+----------------------------
+
+After setting up filters, bookmarks, and making notes, you might want to share
+your work with others. If they have access to the same log files, you can
+use the :ref:`:export-session-to<export_session_to>` command to write an
+executable **lnav** script that will recreate the current session state. The
+script contains various SQL statements and **lnav** commands that capture the
+current state. So, you should feel free to modify the script or use it as a
+reference to learn about more advanced uses of lnav.
+
+The script will capture the file paths that were explicitly specified and
+not the files that were actually opened. For example, if you specified
+"/var/log" on the command line, the script will include
+:code:`:open /var/log/*` and not an individual open for each file in that
+directory.
+
+Also, in order to support archives of log files, lnav will try to find the
+directory where the archive was unpacked and use that as the base for the
+:code:`:open` command. Currently, this is done by searching for the top
+"README" file in the directory hierarchy containing the files [1]_. The
+consumer of the session script can then set the :code:`LOG_DIR_0` (or 1, 2,
+...) environment variable to change where the log files will be loaded from.
+
+.. [1] It is assumed a log archive would have a descriptive README file.
+ Other heuristics may be added in the future.
diff --git a/docs/tutorials/playground/index.md b/docs/tutorials/playground/index.md
new file mode 100644
index 0000000..e1516b1
--- /dev/null
+++ b/docs/tutorials/playground/index.md
@@ -0,0 +1,9 @@
+
+# Playground
+
+Welcome to the **lnav** playground!
+
+There are some sample files loaded into the log and text views.
+Press `q` to switch back to the log view and start exploring.
+You can also press `f` in this view to switch to the other
+text files that are loaded, like a markdown sample.
diff --git a/docs/tutorials/playground/logs/access_log.gz b/docs/tutorials/playground/logs/access_log.gz
new file mode 100644
index 0000000..485cf4e
--- /dev/null
+++ b/docs/tutorials/playground/logs/access_log.gz
Binary files differ
diff --git a/docs/tutorials/playground/logs/messages.gz b/docs/tutorials/playground/logs/messages.gz
new file mode 100644
index 0000000..e6ad012
--- /dev/null
+++ b/docs/tutorials/playground/logs/messages.gz
Binary files differ
diff --git a/docs/tutorials/playground/run.sh b/docs/tutorials/playground/run.sh
new file mode 100755
index 0000000..4a6723d
--- /dev/null
+++ b/docs/tutorials/playground/run.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+export LNAVSECURE=1
+export TERM=xterm-256color
+
+timeout --foreground --kill-after=30s 10m lnav \
+ -d "/tmp/$(echo "playground."$(date "+%Y-%m-%dT%H-%M-%S")".$$.log")" \
+ /tutorials/playground/logs \
+ /tutorials/playground/text \
+ /tutorials/playground/index.md#playground
+
+if [ $? = 124 ]; then
+ echo "error: reached connection time limit, reconnect if you're not a bot."
+else
+ echo "Thanks for trying out lnav! Have a nice day!"
+fi
diff --git a/docs/tutorials/playground/text/markdown-sample.md b/docs/tutorials/playground/text/markdown-sample.md
new file mode 100644
index 0000000..4b56cae
--- /dev/null
+++ b/docs/tutorials/playground/text/markdown-sample.md
@@ -0,0 +1,157 @@
+An h1 header
+============
+
+Paragraphs are separated by a blank line.
+
+2nd paragraph. *Italic*, **bold**, and `monospace`. Itemized lists
+look like:
+
+ * this one
+ * that one
+ * the other one
+
+Note that --- not considering the asterisk --- the actual text
+content starts at 4-columns in.
+
+> Block quotes are
+> written like so.
+>
+> They can span multiple paragraphs,
+> if you like.
+
+Use 3 dashes for an em-dash. Use 2 dashes for ranges (ex., "it's all
+in chapters 12--14"). Three dots ... will be converted to an ellipsis.
+Unicode is supported. ☺
+
+
+
+An h2 header
+------------
+
+Here's a numbered list:
+
+ 1. first item
+ 2. second item
+ 3. third item
+
+Note again how the actual text starts at 4 columns in (4 characters
+from the left side). Here's a code sample:
+
+ # Let me re-iterate ...
+ for i in 1 .. 10 { do-something(i) }
+
+As you probably guessed, indented 4 spaces. By the way, instead of
+indenting the block, you can use delimited blocks, if you like:
+
+~~~
+define foobar() {
+ print "Welcome to flavor country!";
+}
+~~~
+
+(which makes copying & pasting easier). You can optionally mark the
+delimited block for Pandoc to syntax highlight it:
+
+~~~python
+import time
+# Quick, count to ten!
+for i in range(10):
+ # (but not *too* quick)
+ time.sleep(0.5)
+ print i
+~~~
+
+
+
+### An h3 header ###
+
+Now a nested list:
+
+ 1. First, get these ingredients:
+
+ * carrots
+ * celery
+ * lentils
+
+ 2. Boil some water.
+
+ 3. Dump everything in the pot and follow
+ this algorithm:
+
+ find wooden spoon
+ uncover pot
+ stir
+ cover pot
+ balance wooden spoon precariously on pot handle
+ wait 10 minutes
+ goto first step (or shut off burner when done)
+
+ Do not bump wooden spoon or it will fall.
+
+Notice again how text always lines up on 4-space indents (including
+that last line which continues item 3 above).
+
+Here's a link to [a website](https://lnav.org), to a [local
+doc](../index.md), and to a [section heading in the current
+doc](#an-h2-header). Here's a footnote [^1].
+
+[^1]: Footnote text goes here.
+
+Tables can look like this:
+
+size material color
+---- ------------ ------------
+9 leather brown
+10 hemp canvas natural
+11 glass transparent
+
+Table: Shoes, their sizes, and what they're made of
+
+(The above is the caption for the table.) Pandoc also supports
+multi-line tables:
+
+-------- -----------------------
+keyword text
+-------- -----------------------
+red Sunsets, apples, and
+ other red or reddish
+ things.
+
+green Leaves, grass, frogs
+ and other things it's
+ not easy being.
+-------- -----------------------
+
+A horizontal rule follows.
+
+***
+
+Here's a definition list:
+
+apples
+ : Good for making applesauce.
+oranges
+ : Citrus!
+tomatoes
+ : There's no "e" in tomatoe.
+
+Again, text is indented 4 spaces. (Put a blank line between each
+term/definition pair to spread things out more.)
+
+Here's a "line block":
+
+| Line one
+| Line too
+| Line tree
+
+and images can be specified like so:
+
+![example image](../../../assets/images/lnav-front-page.png "An exemplary image")
+
+Inline math equations go in like so: $\omega = d\phi / dt$. Display
+math should get its own line and be put in in double-dollarsigns:
+
+$$I = \int \rho R^{2} dV$$
+
+And note that you can backslash-escape any punctuation characters
+which you wish to be displayed literally, ex.: \`foo\`, \*bar\*, etc. \ No newline at end of file
diff --git a/docs/tutorials/tutorial-lib/configs/tutorial1/config.json b/docs/tutorials/tutorial-lib/configs/tutorial1/config.json
new file mode 100644
index 0000000..4407368
--- /dev/null
+++ b/docs/tutorials/tutorial-lib/configs/tutorial1/config.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "global": {
+ "lnav_tutorial_name": "tutorial1"
+ },
+ "ui": {
+ "keymap-defs": {
+ "default": {
+ "x79": {
+ "command": "|lnav-tutorial-key-handler next"
+ },
+ "x59": {
+ "command": "|lnav-tutorial-key-handler prev"
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/docs/tutorials/tutorial-lib/formats/tutorial-lib/lnav-tutorial-key-handler.lnav b/docs/tutorials/tutorial-lib/formats/tutorial-lib/lnav-tutorial-key-handler.lnav
new file mode 100644
index 0000000..becadec
--- /dev/null
+++ b/docs/tutorials/tutorial-lib/formats/tutorial-lib/lnav-tutorial-key-handler.lnav
@@ -0,0 +1,29 @@
+
+;SELECT filepath AS tutorial_path FROM lnav_file
+ WHERE filepath GLOB '*/tutorial1/index.md' LIMIT 1
+;SELECT CASE
+ WHEN $1 = 'next' AND
+ step < (SELECT max(step) FROM lnav_tutorial_steps WHERE name = $lnav_tutorial_name)
+ THEN step + 1
+ WHEN $1 = 'prev' AND step > 1 THEN step - 1
+ ELSE step
+ END AS new_step
+ FROM lnav_tutorial_step WHERE name = $lnav_tutorial_name
+;SELECT CASE
+ WHEN $1 = 'next' AND
+ step = (SELECT max(step) FROM lnav_tutorial_steps WHERE name = $lnav_tutorial_name)
+ THEN '#conclusion'
+ ELSE '#step-' || $new_step
+ END AS new_anchor
+ FROM lnav_tutorial_step WHERE name = $lnav_tutorial_name
+;UPDATE lnav_tutorial_step SET step = $new_step WHERE name = $lnav_tutorial_name
+;UPDATE lnav_views SET top_meta = json_object(
+ 'file', $tutorial_path,
+ 'anchor', $new_anchor
+)
+ WHERE name = 'text'
+:switch-to-view text
+;UPDATE lnav_views SET top = 0, left = 0
+ WHERE name = 'log'
+;REPLACE INTO lnav_user_notifications (id, views, message)
+ SELECT * FROM lnav_tutorial_log_notification; \ No newline at end of file
diff --git a/docs/tutorials/tutorial-lib/formats/tutorial-lib/tutorial.sql b/docs/tutorials/tutorial-lib/formats/tutorial-lib/tutorial.sql
new file mode 100644
index 0000000..2813357
--- /dev/null
+++ b/docs/tutorials/tutorial-lib/formats/tutorial-lib/tutorial.sql
@@ -0,0 +1,154 @@
+
+-- Tracks the current step in the tutorial
+CREATE TABLE lnav_tutorial_step
+(
+ name TEXT NOT NULL PRIMARY KEY,
+ step INTEGER NOT NULL
+);
+
+INSERT INTO lnav_tutorial_step
+ VALUES ('tutorial1', 1);
+
+-- A description of each step in the tutorial with its achievements
+CREATE TABLE lnav_tutorial_steps
+(
+ name TEXT NOT NULL,
+ step INTEGER NOT NULL,
+ achievements TEXT NOT NULL,
+ PRIMARY KEY (name, step)
+);
+
+-- Tracks the progress through the achievements in a step of the tutorial
+CREATE TABLE IF NOT EXISTS lnav_tutorial_progress
+(
+ name TEXT NOT NULL,
+ step INTEGER NOT NULL,
+ achieved TEXT NOT NULL,
+
+ PRIMARY KEY (name, step, achieved)
+);
+
+CREATE TABLE IF NOT EXISTS lnav_tutorial_lines
+(
+ name TEXT NOT NULL,
+ step INTEGER NOT NULL,
+ view_ptr TEXT NOT NULL,
+ view_value TEXT NOT NULL,
+ achievement TEXT NOT NULL,
+ log_comment TEXT
+);
+
+-- Copy the tutorial data from the markdown frontmatter to
+-- the appropriate tables.
+CREATE TRIGGER IF NOT EXISTS add_tutorial_data
+ AFTER INSERT
+ ON lnav_events
+ WHEN jget(new.content, '/$schema') = 'https://lnav.org/event-file-format-detected-v1.schema.json' AND
+ jget(new.content, '/format') = 'text/markdown'
+BEGIN
+ INSERT INTO lnav_tutorial_steps
+ SELECT jget(tutorial_meta, '/name'),
+ key + 1,
+ value
+ FROM (SELECT yaml_to_json(lnav_file_metadata.content) AS tutorial_meta
+ FROM lnav_file_metadata
+ WHERE filepath = jget(new.content, '/filename')) AS meta_content,
+ json_each(jget(meta_content.tutorial_meta, '/steps'));
+
+ REPLACE INTO lnav_tutorial_lines
+ SELECT name,
+ step,
+ jget(value, '/view_ptr'),
+ jget(value, '/view_value'),
+ key,
+ jget(value, '/comment')
+ FROM lnav_tutorial_steps,
+ json_each(achievements)
+ WHERE jget(value, '/view_ptr') IS NOT NULL;
+
+ REPLACE INTO lnav_user_notifications (id, views, message)
+ SELECT *
+ FROM lnav_tutorial_log_notification;
+END;
+
+CREATE TRIGGER IF NOT EXISTS tutorial_move_log_after_load
+ AFTER INSERT
+ ON lnav_events
+ WHEN jget(new.content, '/$schema') = 'https://lnav.org/event-session-loaded-v1.schema.json'
+BEGIN
+ UPDATE lnav_views SET top = 0 WHERE name = 'log';
+END;
+
+CREATE TRIGGER IF NOT EXISTS lnav_tutorial_view_listener UPDATE OF top
+ ON lnav_views_echo
+ WHEN new.name = 'log'
+BEGIN
+ INSERT OR IGNORE INTO lnav_tutorial_progress
+ SELECT lnav_tutorial_lines.name,
+ lnav_tutorial_lines.step,
+ achievement
+ FROM lnav_tutorial_step,
+ lnav_tutorial_lines
+ WHERE lnav_tutorial_step.step = lnav_tutorial_lines.step
+ AND jget(json_object('top', new.top,
+ 'left', new.left,
+ 'search', new.search),
+ view_ptr) = view_value;
+ UPDATE all_logs
+ SET log_comment = (SELECT log_comment
+ FROM lnav_tutorial_step,
+ lnav_tutorial_lines
+ WHERE lnav_tutorial_step.step = lnav_tutorial_lines.step
+ AND lnav_tutorial_lines.log_comment IS NOT NULL
+ AND jget(json_object('top', new.top,
+ 'left', new.left,
+ 'search', new.search), view_ptr) = view_value)
+ WHERE log_line = new.top
+ AND log_comment IS NULL;
+END;
+
+CREATE TABLE lnav_tutorial_message
+(
+ msgid INTEGER PRIMARY KEY,
+ msg TEXT
+);
+
+CREATE VIEW lnav_tutorial_current_achievements AS
+SELECT key AS achievement, value
+ FROM lnav_tutorial_step,
+ lnav_tutorial_steps, json_each(lnav_tutorial_steps.achievements)
+ WHERE lnav_tutorial_step.step = lnav_tutorial_steps.step;
+
+CREATE VIEW lnav_tutorial_current_progress AS
+SELECT achieved
+ FROM lnav_tutorial_step,
+ lnav_tutorial_progress
+ WHERE lnav_tutorial_step.step = lnav_tutorial_progress.step;
+
+CREATE VIEW lnav_tutorial_remaining_achievements AS
+SELECT *
+ FROM lnav_tutorial_current_achievements
+ WHERE achievement NOT IN (SELECT * FROM lnav_tutorial_current_progress);
+
+CREATE VIEW lnav_tutorial_log_notification AS
+SELECT *
+ FROM (SELECT 'org.lnav.tutorial.log' AS id, '["log"]' AS views, jget(value, '/notification') AS message
+ FROM lnav_tutorial_remaining_achievements
+ UNION ALL
+ SELECT 'org.lnav.tutorial.log' AS id,
+ '["log"]' AS views,
+ 'Press `y` to go to the next step in the tutorial' AS message)
+ LIMIT 1;
+
+CREATE TRIGGER IF NOT EXISTS lnav_tutorial_progress_listener
+ AFTER INSERT
+ ON lnav_tutorial_progress
+BEGIN
+ DELETE FROM lnav_user_notifications WHERE id = 'org.lnav.tutorial.log';
+ REPLACE INTO lnav_user_notifications (id, views, message)
+ SELECT *
+ FROM lnav_tutorial_log_notification;
+END;
+
+REPLACE INTO lnav_user_notifications (id, views, message)
+ VALUES ('org.lnav.tutorial.text', '["text"]', 'Press `q` to go to the log view')
diff --git a/docs/tutorials/tutorial1/index.md b/docs/tutorials/tutorial1/index.md
new file mode 100644
index 0000000..a608ba1
--- /dev/null
+++ b/docs/tutorials/tutorial1/index.md
@@ -0,0 +1,145 @@
+---
+name: tutorial1
+steps:
+ - move-to-error:
+ description: "Move to an error"
+ view_ptr: /top
+ view_value: 6
+ notification: |
+ Press `e`/`Shift+E` to move through the
+ <span class="-lnav_log-level-styles_error">errors</span>
+ comment: |
+ You found the error!
+ [Log formats](https://docs.lnav.org/en/latest/formats.html#format-file-reference)
+ can define the log levels for a given message.
+ The [theme](https://docs.lnav.org/en/latest/config.html#theme-definitions) defines
+ how the levels are displayed.
+ move-to-warning:
+ description: "Move to a warning"
+ notification: |
+ Press `w`/`Shift+W` to move through the
+ <span class="-lnav_log-level-styles_warning">warnings</span>
+ view_ptr: /top
+ view_value: 3
+ comment: |
+ You found the warning! The scrollbar on the right is highlighted
+ to show the position of
+ <span class="-lnav_log-level-styles_warning">warnings</span> and
+ <span class="-lnav_log-level-styles_error">errors</span> in this
+ view.
+ - search-for-term:
+ description: "Search for something"
+ notification: "Press `/` to search for '1AF9...'"
+ view_ptr: /search
+ view_value: 1AF9293A-F42D-4318-BCDF-60234B240955
+ move-to-next-hit:
+ description: "Move to the next hit"
+ notification: "Press `n`/`Shift+N` to move through the search hits"
+ view_ptr: /top
+ view_value: 53
+ comment: |
+ The matching text in a search is highlighted in
+ <span class="-lnav_styles_search">reverse-video</span>.
+ However, the text is not always on-screen, so the bar on the
+ left will also be highlighted. You can then press `>` to
+ move right to the next (horizontal) search hit. Pressing
+ `<` will move left to the previous (horizontal) hit or all
+ the way back to the start of the line.
+ move-right:
+ description: "Move to the right"
+ notification: "Press `>` to move horizontally to view the search hit"
+ view_ptr: /left
+ view_value: 582
+ - move-to-half-hour:
+ description: "Move to the next half-hour"
+ notification: "Press `3`/`Shift+3` to move through the half-hour marks"
+ view_ptr: /top
+ view_value: 34
+ comment: |
+ This file is in the _glog_ format and timestamps consist of the
+ year, month, and day squished together. This log message's
+ timestamp is March 22nd, 2017. You can see the timestamp for
+ the top line in the view in the breadcrumb bar. Next, go to the
+ log messages for the following day using `:goto March 23` or the
+ breadcrumb bar above.
+ move-to-timestamp:
+ description: "Move to a given timestamp"
+ notification: "Move to '**March 23**' using `:goto` or the breadcrumb bar"
+ view_ptr: /top
+ view_value: 79
+ comment: |
+ Many different timestamp formats are recognized as well as
+ relative times, like `+1h` or `-2h`.
+---
+# Tutorial 1
+
+Welcome to the first _interactive_ **lnav** tutorial!
+
+This tutorial will guide you through the basics of navigating log files.
+Pressing `q` will display an example log file to try out commands on.
+Pressing `y` will return you to the next step in the tutorial.
+
+## Step 1
+
+Finding errors quickly is one of the main use-cases for **lnav**. To
+make that quick and easy, **lnav** parses the log messages in log files
+as they are loaded and builds indexes of the errors and warnings. You
+can then use the following hotkeys to jump to them in the log view:
+
+| Key | Action |
+|-----------|----------------------------------------------------------------------------------|
+| `e` | Move to the next <span class="-lnav_log-level-styles_error">error</span> |
+| `Shift+E` | Move to the previous <span class="-lnav_log-level-styles_error">error</span> |
+| `w` | Move to the next <span class="-lnav_log-level-styles_warning">warning</span> |
+| `Shift+W` | Move to the previous <span class="-lnav_log-level-styles_warning">warning</span> |
+
+To complete this step in the tutorial, you'll need to navigate to the
+errors and warnings in the sample log file. You can check the upper-right
+↗↗↗ status bar for tips on what you need to do next. Now, press `q` to
+switch to the log view and begin navigating the sample log file.
+
+## Step 2
+
+To search for text in files, you can press `/` to enter the search
+prompt. To make it easier to search for text that is on-screen, you
+can press `TAB` to complete values that are shown on screen. For
+example, to search for the UUID "1AF9293A-F42D-4318-BCDF-60234B240955"
+that is in one of the error messages, you can enter "1AF9" and then
+press `TAB` to complete the rest of the UUID.
+
+Press `q` to switch to the log view and try searching for the UUID.
+
+## Step 3
+
+To move to a particular time in the logs, you have a few options:
+
+* The number keys can be used to move to messages at the ten-minute
+ marks within an hour. For example, pressing `2` will move to the
+ first message after the next twenty-minute mark, pressing `3`
+ will move to the next half-hour mark, and so on.
+* Pressing `ENTER` to focus on the breadcrumb bar, then you
+ can press `TAB` (or right-arrow) to move to the time crumb.
+ With the time crumb selected, you can then type in an absolute
+ or relative time. Or, you can use the up and down arrow keys
+ to select a preset relative time.
+* Pressing `:` will activate the command prompt, then you can use
+ the `:goto` command to move to a given timestamp (or line number).
+
+Press `q` to switch to the log view and try moving to different
+times.
+
+## Conclusion
+
+That's all for now, thanks for your time! Visit the
+[downloads](https://lnav.org/downloads) page to find out how to
+download or install **lnav** for your system. The full
+documentation is available at https://docs.lnav.org
+
+Press `q` to switch to the log view and then press `q` again to
+exit **lnav**.
+
+## Colophon
+
+The source for this tutorial is available here:
+
+https://github.com/tstack/lnav/tree/master/docs/tutorials/
diff --git a/docs/tutorials/tutorial1/run.sh b/docs/tutorials/tutorial1/run.sh
new file mode 100755
index 0000000..6785ac4
--- /dev/null
+++ b/docs/tutorials/tutorial1/run.sh
@@ -0,0 +1,16 @@
+#!/bin/bash
+
+export LNAVSECURE=1
+export TERM=xterm-256color
+
+timeout --foreground --kill-after=30s 5m lnav \
+ -d "/tmp/$(echo "tutorial1."$(date "+%Y-%m-%dT%H-%M-%S")".$$.log")" \
+ -I /tutorials/tutorial-lib \
+ /tutorials/tutorial1/tutorial1.glog \
+ /tutorials/tutorial1/index.md#tutorial-1
+
+if [ $? = 124 ]; then
+ echo "error: reached connection time limit, reconnect if you're not a bot."
+else
+ echo "Thanks for trying out lnav! Have a nice day!"
+fi
diff --git a/docs/tutorials/tutorial1/tutorial1.glog b/docs/tutorials/tutorial1/tutorial1.glog
new file mode 100644
index 0000000..b996d68
--- /dev/null
+++ b/docs/tutorials/tutorial1/tutorial1.glog
@@ -0,0 +1,100 @@
+I20170322 06:58:47.082758 61456 blaster.cc:112] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 06:58:58.019562 121 demo.cc:123] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 06:58:59.059175 123 blaster.cc:6782] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+W20170322 06:59:16.062826 61456 demo.cc:352] Ut enim ad minim veniam, quis nostrud exercitation 1AF9293A-F42D-4318-BCDF-60234B240955 ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 06:59:28.084062 124 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 06:59:32.053551 123 loader.cc:13] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+E20170322 06:59:53.084969 123 loader.cc:552] Excepteur sint occaecat cupidatat non proident 1AF9293A-F42D-4318-BCDF-60234B240955, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 07:00:00.096693 123 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:00:03.049849 123 demo.cc:352] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:00:08.070575 123 blaster.cc:6782] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 07:00:23.019849 123 blaster.cc:352] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:00:28.022692 61457 loader.cc:552] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 07:00:29.058438 61456 blaster.cc:352] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 07:00:30.028483 123 loader.cc:13] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 07:00:49.070676 123 demo.cc:352] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 07:00:56.095214 123 loader.cc:552] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+W20170322 07:01:14.042785 123 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:01:31.083704 123 blaster.cc:112] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 07:01:44.013733 121 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:01:55.024085 121 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:02:02.027811 121 blaster.cc:6782] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:02:14.022939 61456 blaster.cc:112] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 07:02:30.035925 123 loader.cc:13] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 07:02:49.024985 123 loader.cc:13] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 07:03:09.056478 121 blaster.cc:6782] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:03:15.023777 123 demo.cc:352] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 07:03:32.066107 123 blaster.cc:352] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 07:03:48.028662 124 blaster.cc:6782] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 07:03:54.027078 123 demo.cc:123] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 07:04:09.041478 123 demo.cc:123] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:04:14.068162 121 blaster.cc:6782] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:04:28.099513 124 blaster.cc:112] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 07:04:40.063473 124 loader.cc:552] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 07:04:50.024030 123 loader.cc:552] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 07:34:56.081415 121 blaster.cc:352] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 07:35:14.096304 123 blaster.cc:352] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 08:05:21.086331 123 demo.cc:352] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 08:05:33.039503 123 loader.cc:13] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 08:05:43.092657 124 blaster.cc:6782] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 08:05:59.002644 123 demo.cc:123] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 08:06:01.022102 123 demo.cc:352] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 08:06:22.005675 123 blaster.cc:6782] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 08:06:37.088974 123 blaster.cc:112] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 08:06:44.043938 61457 demo.cc:123] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 08:06:47.060703 123 loader.cc:13] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 08:06:49.052185 61456 demo.cc:123] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 08:06:52.074424 61457 demo.cc:352] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 08:07:02.063191 123 demo.cc:123] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 08:07:10.030327 61457 blaster.cc:112] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 08:07:11.011338 123 loader.cc:13] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 08:07:27.078391 123 blaster.cc:352] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 08:07:41.061684 123 blaster.cc:112] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 08:07:53.076558 121 blaster.cc:112] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 08:38:04.055174 121 blaster.cc:112] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur: 1AF9293A-F42D-4318-BCDF-60234B240955
+I20170322 08:38:18.046756 123 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 08:48:28.004198 123 loader.cc:552] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 08:48:36.032193 61457 blaster.cc:352] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 09:08:50.028964 61456 loader.cc:13] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 09:08:56.074576 124 blaster.cc:112] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 09:08:57.090258 123 loader.cc:13] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 09:09:00.067690 121 blaster.cc:352] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 09:09:19.036483 61457 blaster.cc:112] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 09:09:40.048046 123 blaster.cc:352] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 09:09:52.051526 123 loader.cc:13] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 09:10:11.003845 61456 loader.cc:552] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 09:10:27.094133 123 blaster.cc:6782] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 09:10:43.027892 121 blaster.cc:352] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 09:10:57.078489 124 demo.cc:352] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 09:11:09.014685 123 demo.cc:123] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 09:11:18.029203 61456 blaster.cc:352] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 09:11:24.067068 121 demo.cc:123] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 09:11:38.053891 61456 loader.cc:552] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 09:11:59.027292 61457 blaster.cc:112] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 09:12:10.069054 61457 loader.cc:13] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170322 09:12:22.018053 123 loader.cc:552] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170322 09:12:39.000436 123 demo.cc:352] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170322 09:12:53.009916 123 loader.cc:13] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 09:13:13.051890 121 demo.cc:123] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170322 09:13:24.076724 123 demo.cc:123] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170323 06:13:34.075980 123 blaster.cc:6782] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170323 06:13:35.096130 61456 blaster.cc:6782] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170323 06:13:49.087790 121 demo.cc:123] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170323 06:14:08.033671 61457 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170323 06:14:23.091358 61456 blaster.cc:112] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170323 06:14:35.088133 61456 demo.cc:123] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170323 06:14:55.005577 123 blaster.cc:352] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170323 06:14:58.008392 61457 demo.cc:123] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170323 06:15:05.004789 123 loader.cc:552] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170323 07:28:07.070013 123 blaster.cc:6782] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170323 07:32:08.012805 123 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170323 07:33:25.042509 61456 loader.cc:552] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+E20170323 08:15:32.027688 123 blaster.cc:6782] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170323 08:15:41.020299 61456 blaster.cc:6782] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170323 08:15:42.021039 124 loader.cc:552] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170323 08:15:59.063918 123 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
+I20170323 08:16:19.082250 123 loader.cc:552] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170323 08:16:20.026445 61457 loader.cc:13] Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
+I20170323 08:16:41.048447 123 blaster.cc:6782] Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
+I20170323 08:16:52.097215 61456 demo.cc:123] Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+I20170323 09:17:01.020663 61456 blaster.cc:112] Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
diff --git a/example-scripts/clipboard.sh b/example-scripts/clipboard.sh
new file mode 100755
index 0000000..2cdd016
--- /dev/null
+++ b/example-scripts/clipboard.sh
@@ -0,0 +1,69 @@
+#!/bin/sh
+# Wrapper for various clipboard I/O on Linux desktop environments and Windows emulations thereof
+
+if [ -z "$STDIN_COPY_COMMAND" ] || [ -z "$STDOUT_PASTE_COMMAND" ]
+then
+ if [ -n "$WAYLAND_DISPLAY" ]
+ then
+ STDIN_COPY_COMMAND="wl-copy --foreground --type text/plain"
+ STDOUT_PASTE_COMMAND="wl-paste --no-newline"
+ elif [ -n "$DISPLAY" ]
+ then
+ if command -v xclip
+ then
+ STDIN_COPY_COMMAND="xclip -quiet -i -selection clipboard"
+ STDOUT_PASTE_COMMAND="xclip -o -selection clipboard"
+ elif command -v xsel
+ then
+ STDIN_COPY_COMMAND="xsel --nodetach -i --clipboard"
+ STDOUT_PASTE_COMMAND="xsel -o --clipboard"
+ fi
+ elif command -v lemonade
+ then
+ STDIN_COPY_COMMAND="lemonade copy"
+ STDOUT_PASTE_COMMAND="lemonade paste"
+ elif command -v doitclient
+ then
+ STDIN_COPY_COMMAND="doitclient wclip"
+ STDOUT_PASTE_COMMAND="doitclient wclip -r"
+ elif command -v win32yank.exe
+ then
+ STDIN_COPY_COMMAND="win32yank.exe -i --crlf"
+ STDOUT_PASTE_COMMAND="win32yank.exe -o --lf"
+ elif command -v clip.exe
+ then
+ STDIN_COPY_COMMAND="clip.exe"
+ STDOUT_PASTE_COMMAND=":"
+ elif [ -n "$TMUX" ]
+ then
+ STDIN_COPY_COMMAND="tmux load-buffer -"
+ STDOUT_PASTE_COMMAND="tmux save-buffer -"
+ else
+ echo 'No clipboard command' >&2
+ exit 10
+ fi > /dev/null
+fi
+
+case $1 in
+ copy) exec $STDIN_COPY_COMMAND > /dev/null 2>/dev/null ;;
+ paste) exec $STDOUT_PASTE_COMMAND < /dev/null 2>/dev/null ;;
+ "") # Try to guess intention
+ if ! [ -t 0 ] # stdin is piped
+ then
+ exec $STDIN_COPY_COMMAND > /dev/null 2>/dev/null
+ elif ! [ -t 1 ] # stdout is piped
+ then
+ exec $STDOUT_PASTE_COMMAND < /dev/null 2>/dev/null
+ else
+ export STDIN_COPY_COMMAND STDOUT_PASTE_COMMAND
+ fi
+ ;;
+ *) cat << EOF
+Usage:
+ clipboard copy
+ clipboard paste
+ . clipboard
+EOF
+ exit 10
+ ;;
+esac
diff --git a/example-scripts/log_to_csv.sh b/example-scripts/log_to_csv.sh
new file mode 100755
index 0000000..70117dd
--- /dev/null
+++ b/example-scripts/log_to_csv.sh
@@ -0,0 +1,66 @@
+#!/usr/bin/env bash
+
+#
+# An example script that converts messages in a syslog file into a
+# CSV-formatted file. The CSV file is written to the current directory
+# with the same base name as the source file. If the script is run on
+# the same file multiple times, it will only convert newly added lines.
+#
+# NOTE: lnav is going to save some state in $HOME; you might want to change
+# $HOME to something else...
+#
+# NOTE 2: Unfortunately, this is pretty inefficient right now since lnav
+# is going to store the entire log file in memory when processing the
+# result of the SQL SELECT.
+#
+
+if test $# -lt 1; then
+ echo "usage: $0 <path>"
+ echo "Convert a syslog file into CSV format."
+ exit 1
+fi
+
+if test ! -f "$1"; then
+ echo "error: expecting a log file as the first argument"
+ exit 1
+fi
+
+# Figure out a unique file name.
+out_file_base=$(basename "$1")
+counter=0
+while test -e "${out_file_base}.${counter}.csv"; do
+ counter=$((counter + 1))
+done
+export OUT_FILE="${out_file_base}.${counter}.csv"
+
+# Here's a quick summary of what this is doing:
+#
+# 1. ':load-session' will load the session data which stores which lines
+# are bookmarked in a file. We're using bookmarks to keep track of the
+# last line that we converted in a previous run of this script.
+# 2. ';CREATE TABLE helper' creates a temporary table that we use to store
+# the range of messages that we'll be converting.
+# 3. ';INSERT INTO helper' will figure out the range of lines in syslog file
+# to convert.
+# 4. ';UPDATE syslog_log' will set a bookmark on the last line of the range
+# we computed in the previous step.
+# 5. ';SELECT *' will pull all of the log messages in the computed range.
+# 6. ':write-csv-to' will write out the log messages SELECTed in step #5.
+# 7. ':save-session' will save the bookmark we set so it can be loaded on
+# future runs of this script.
+
+lnav -nq -d /tmp/lnav.err \
+ -c ":load-session" \
+ -c ";CREATE TABLE helper ( start_line int, max_line int )" \
+ -c ";INSERT INTO helper ( start_line, max_line ) VALUES (\
+ (SELECT coalesce(\
+ (SELECT max(log_line) FROM syslog_log where log_mark = 1) + 1,\
+ 0)),\
+ (SELECT max(log_line) FROM syslog_log ))" \
+ -c ";UPDATE syslog_log SET log_mark = 1 where log_line = (\
+ SELECT max_line FROM helper)" \
+ -c ";SELECT *,log_text FROM syslog_log where log_line between (\
+ SELECT start_line FROM helper) and (SELECT max_line FROM helper)" \
+ -c ':write-csv-to $OUT_FILE' \
+ -c ":save-session" \
+ "$1"
diff --git a/example-scripts/report-demo.lnav b/example-scripts/report-demo.lnav
new file mode 100644
index 0000000..aeb0040
--- /dev/null
+++ b/example-scripts/report-demo.lnav
@@ -0,0 +1,83 @@
+#
+# @synopsis: report-demo [<output-path>]
+# @description: Generate a report for requests in access_log files
+#
+
+# Figure out the file path where the report should be written to, default is
+# stdout
+;SELECT CASE
+ WHEN $1 IS NULL THEN '-'
+ ELSE $1
+ END AS out_path
+
+# Redirect output from commands to $out_path
+:redirect-to $out_path
+
+# Print an introductory message
+;SELECT printf('\n%d total requests', count(1)) AS msg FROM access_log
+:echo $msg
+
+;WITH top_paths AS (
+ SELECT
+ cs_uri_stem,
+ count(1) AS total_hits,
+ sum(sc_bytes) as bytes,
+ count(distinct c_ip) as visitors
+ FROM access_log
+ WHERE sc_status BETWEEN 200 AND 300
+ GROUP BY cs_uri_stem
+ ORDER BY total_hits DESC
+ LIMIT 50),
+ weekly_hits_with_gaps AS (
+ SELECT timeslice(log_time_msecs, '1w') AS week,
+ cs_uri_stem,
+ count(1) AS weekly_hits
+ FROM access_log
+ WHERE cs_uri_stem IN (SELECT cs_uri_stem FROM top_paths) AND
+ sc_status BETWEEN 200 AND 300
+ GROUP BY week, cs_uri_stem),
+ all_weeks AS (
+ SELECT week
+ FROM weekly_hits_with_gaps
+ GROUP BY week
+ ORDER BY week ASC),
+ weekly_hits AS (
+ SELECT all_weeks.week,
+ top_paths.cs_uri_stem,
+ ifnull(weekly_hits, 0) AS hits
+ FROM all_weeks
+ CROSS JOIN top_paths
+ LEFT JOIN weekly_hits_with_gaps
+ ON all_weeks.week = weekly_hits_with_gaps.week AND
+ top_paths.cs_uri_stem = weekly_hits_with_gaps.cs_uri_stem)
+ SELECT weekly_hits.cs_uri_stem AS Path,
+ printf('%,9d', total_hits) AS Hits,
+ printf('%,9d', visitors) AS Visitors,
+ printf('%9s', humanize_file_size(bytes)) as Amount,
+ sparkline(hits) AS Weeks
+ FROM weekly_hits
+ LEFT JOIN top_paths ON top_paths.cs_uri_stem = weekly_hits.cs_uri_stem
+ GROUP BY weekly_hits.cs_uri_stem
+ ORDER BY Hits DESC
+ LIMIT 10
+
+:write-table-to -
+
+:echo
+:echo Failed Requests
+:echo
+
+;SELECT printf('%,9d', count(1)) AS Hits,
+ printf('%,9d', count(distinct c_ip)) AS Visitors,
+ sc_status AS Status,
+ cs_method AS Method,
+ group_concat(distinct cs_version) AS Versions,
+ cs_uri_stem AS Path,
+ replicate('|', (cast(count(1) AS REAL) / $total_requests) * 100.0) AS "% of Requests"
+ FROM access_log
+ WHERE sc_status >= 400
+ GROUP BY cs_method, cs_uri_stem
+ ORDER BY Hits DESC
+ LIMIT 10
+
+:write-table-to -
diff --git a/example-scripts/tag-ssh-msgs.lnav b/example-scripts/tag-ssh-msgs.lnav
new file mode 100644
index 0000000..9ffad6a
--- /dev/null
+++ b/example-scripts/tag-ssh-msgs.lnav
@@ -0,0 +1,10 @@
+#
+# @synopsis: tag-ssh-msgs
+# @description: Tag interesting SSH log messages
+#
+
+;UPDATE all_logs
+ SET log_tags = json_concat(log_tags, '#ssh.invalid-user')
+ WHERE log_text LIKE '%Invalid user from%'
+
+;SELECT 'Tagged ' || changes() || ' messages';
diff --git a/lnav.1 b/lnav.1
new file mode 100644
index 0000000..4a1366a
--- /dev/null
+++ b/lnav.1
@@ -0,0 +1,126 @@
+.\" DO NOT MODIFY THIS FILE! It was generated by help2man 1.43.3.
+.\" Some roff macros, for reference:
+.\" .nh disable hyphenation
+.\" .hy enable hyphenation
+.\" .ad l left justify
+.\" .ad b justify to both left and right margins
+.\" .nf disable filling
+.\" .fi enable filling
+.\" .br insert line break
+.\" .sp <n> insert n+1 empty lines
+.\" for manpage-specific macros, see man(7)
+.\" Define macros
+.de Vb \" Begin verbatim text
+.ft CW
+.nf
+.ne \\$1
+..
+.de Ve \" End verbatim text
+.ft R
+.fi
+..
+.TH LNAV "1" "August 2022"
+.SH NAME
+lnav \- ncurses-based log file viewer
+.SH SYNOPSIS
+.B lnav
+[\-hVsar] [logfile1 logfile2 ...]
+.SH DESCRIPTION
+The log file navigator, lnav, is an enhanced log file viewer that
+takes advantage of any semantic information that can be gleaned from
+the files being viewed, such as timestamps and log levels. Using this
+extra semantic information, lnav can do things like interleaving
+messages from different files, generate histograms of messages over
+time, and providing hotkeys for navigating through the file. It is
+hoped that these features will allow the user to quickly and
+efficiently zero in on problems.
+.SH KEY BINDINGS
+.TP
+?
+View/leave the online help text.
+.TP
+q
+Quit the program.
+.SH OPTIONS
+.TP
+\fB\-h\fR
+Print help and exit
+.TP
+\fB\-H\fR
+Display the internal help text.
+.TP
+\fB\-n\fR
+Run without the curses UI. (headless mode)
+.TP
+\fB\-c\fR cmd
+Execute a command after the files have been loaded.
+.TP
+\fB\-f\fR path
+Execute the commands in the given file.
+.TP
+\fB\-I\fR path
+Add the given configuration directory to the search path.
+.TP
+\fB\-n\fR
+Do not open the default syslog file if no files are given.
+.TP
+\fB\-q\fR
+Quiet mode. Do not print the log messages after executing all of the commands.
+.TP
+\fB\-i\fR
+Install the given format files in the $HOME/.lnav/formats/installed directory
+and exit.
+.TP
+\fB\-u\fR
+Update formats installed from git repositories.
+.TP
+\fB\-C\fR
+Check the configuration and exit. The log format files will be loaded and
+checked. Any files given on the command-line will be loaded checked to make
+sure they match a log format.
+.TP
+\fB\-d\fR file
+Write debug messages to the given file.
+.TP
+\fB\-V\fR
+Print version information.
+.TP
+\fB\-r\fR
+Recursively load files from the given directories.
+.TP
+\fB\-R\fR
+Load older rotated log files as well.
+.TP
+\fB\-t\fR
+Prepend timestamps to the lines of data being read in
+on the standard input.
+.TP
+\fB\-w\fR file
+Write the contents of the standard input to this file.
+.SS "Optional arguments:"
+.TP
+logfile1
+The log files or directories to view. If a
+directory is given, all of the files in the
+directory will be loaded.
+.SH EXAMPLES
+To load and follow the syslog file:
+.PP
+.Vb 1
+\& lnav
+.Ve
+.PP
+To load all of the files in /var/log:
+.PP
+.Vb 1
+\& lnav /var/log
+.Ve
+.PP
+To watch the output of make with timestamps prepended:
+.PP
+.Vb 1
+\& make 2>&1 | lnav \-t
+.Ve
+.SH AUTHOR
+This manual page was written by Salvatore Bonaccorso <carnil@debian.org>
+for the Debian system (but may be used by others).
diff --git a/lnav.cfg b/lnav.cfg
new file mode 100644
index 0000000..216b409
--- /dev/null
+++ b/lnav.cfg
@@ -0,0 +1,543 @@
+
+#
+# General options
+#
+
+# The type of line endings
+newlines = lf # auto/lf/crlf/cr
+
+# The original size of tabs in the input
+input_tab_size = 8 # number
+
+# The size of tabs in the output (only used if align_with_tabs=true)
+output_tab_size = 8 # number
+
+# The ascii value of the string escape char, usually 92 (\). (Pawn)
+string_escape_char = 92 # number
+
+#
+# Indenting
+#
+
+# The number of columns to indent per level (usually 2, 3, 4, or 8)
+indent_columns = 4 # number
+
+# How to use tabs when indenting code
+# 0=spaces only
+# 1=indent with tabs, align with spaces
+# 2=indent and align with tabs
+indent_with_tabs = 0 # number
+
+# Whether to indent strings broken by '\' so that they line up
+indent_align_string = true # false/true
+
+# Spaces to indent '{' from level
+indent_brace = 0 # number
+
+# Whether braces are indented to the body level
+indent_braces = false # false/true
+
+# Disabled indenting function braces if indent_braces is true
+indent_braces_no_func = false # false/true
+
+# Indent based on the size of the brace parent, ie 'if' => 3 spaces, 'for' => 4 spaces, etc.
+indent_brace_parent = false # false/true
+
+# Whether the 'namespace' body is indented
+indent_namespace = false # false/true
+
+# Whether the 'class' body is indented
+indent_class = true # false/true
+
+# Whether to indent the stuff after a leading class colon
+indent_class_colon = true # false/true
+
+# Whether to indent continued function call parameters one indent level (true) or aligns instead of indent (false)
+indent_func_call_param = false # false/true
+
+# The number of spaces to indent a continued '->' or '.'
+# Usually set to indent_columns.
+indent_member = 0 # number
+
+# Spaces to indent single line ('//') comments on lines before code
+indent_sing_line_comments = 0 # number
+
+# Spaces to indent 'case' from 'switch'
+indent_switch_case = 0 # number
+
+# Spaces to indent 'case' body from 'case'
+indent_case_body = 4 # number
+
+# Spaces to indent '{' from 'case'
+indent_case_brace = 0 # number
+
+# Whether to indent comments found in first column
+indent_col1_comment = false # false/true
+
+# How to indent goto labels (>0=absolute column where 1 is the leftmost column, <=0=subtract from brace indent)
+indent_label = 1 # number
+
+# If an open paren is followed by a newline, indent the next line so that it lines up after the open paren (not recommended)
+indent_paren_nl = false # false/true
+
+# If an open square is followed by a newline, indent the next line so that it lines up after the open square (not recommended)
+indent_square_nl = false # false/true
+
+#
+# Spacing options
+#
+
+# Add or remove space around arithmetic operator '+', '-', '/', '*', etc
+sp_arith = force # ignore/add/remove/force
+
+# Add or remove space around assignment operator '=', '+=', etc
+sp_assign = force # ignore/add/remove/force
+
+# Add or remove space around boolean operators '&&' and '||'
+sp_bool = force # ignore/add/remove/force
+
+# Add or remove space around compare operator '<', '>', '==', etc
+sp_compare = force # ignore/add/remove/force
+
+# Add or remove space inside '(' and ')'
+sp_inside_paren = remove # ignore/add/remove/force
+
+# Add or remove space between nested parens
+sp_paren_paren = remove # ignore/add/remove/force
+
+# Add or remove space between ')' and '{'
+sp_paren_brace = ignore # ignore/add/remove/force
+
+# Add or remove space before pointer star '*'
+sp_before_ptr_star = force # ignore/add/remove/force
+
+# Add or remove space between pointer stars '*'
+sp_between_ptr_star = remove # ignore/add/remove/force
+
+# Add or remove space after pointer star '*'
+sp_after_ptr_star = remove # ignore/add/remove/force
+
+# Add or remove space before reference sign '&'
+sp_before_byref = force # ignore/add/remove/force
+
+# Add or remove space after reference sign '&'
+sp_after_byref = ignore # ignore/add/remove/force
+
+# Add or remove space before '<>'
+sp_before_angle = remove # ignore/add/remove/force
+
+# Add or remove space after '<>'
+sp_after_angle = force # ignore/add/remove/force
+
+# Add or remove space before '(' of 'if', 'for', 'switch', and 'while'
+sp_before_sparen = force # ignore/add/remove/force
+
+# Add or remove space inside if-condition '(' and ')'
+sp_inside_sparen = remove # ignore/add/remove/force
+
+# Add or remove space after ')' of 'if', 'for', 'switch', and 'while'
+sp_after_sparen = force # ignore/add/remove/force
+
+# Add or remove space between ')' and '{' of 'if', 'for', 'switch', and 'while'
+sp_sparen_brace = add # ignore/add/remove/force
+
+# Add or remove space before empty statement ';' on 'if', 'for' and 'while'
+sp_special_semi = ignore # ignore/add/remove/force
+
+# Add or remove space before ';'
+sp_before_semi = remove # ignore/add/remove/force
+
+# Add or remove space before '[' (except '[]')
+sp_before_square = ignore # ignore/add/remove/force
+
+# Add or remove space before '[]'
+sp_before_squares = ignore # ignore/add/remove/force
+
+# Add or remove space inside '[' and ']'
+sp_inside_square = remove # ignore/add/remove/force
+
+# Add or remove space after ','
+sp_after_comma = force # ignore/add/remove/force
+
+# Add or remove space between 'operator' and operator sign
+sp_after_operator = ignore # ignore/add/remove/force
+
+# Add or remove space after cast
+sp_after_cast = remove # ignore/add/remove/force
+
+# Add or remove space between 'sizeof' and '('
+sp_sizeof_paren = remove # ignore/add/remove/force
+
+# Add or remove space after the tag keyword (Pawn)
+sp_after_tag = ignore # ignore/add/remove/force
+
+# Add or remove space inside enum '{' and '}'
+sp_inside_braces_enum = force # ignore/add/remove/force
+
+# Add or remove space inside struct/union '{' and '}'
+sp_inside_braces_struct = force # ignore/add/remove/force
+
+# Add or remove space inside '{' and '}'
+sp_inside_braces = force # ignore/add/remove/force
+
+# Add or remove space inside '<' and '>'
+sp_inside_angle = remove # ignore/add/remove/force
+
+# Add or remove space between return type and function name (a minimum of 1 is forced except for pointer return types)
+sp_type_func = ignore # ignore/add/remove/force
+
+# Add or remove space between function name and '(' on function declaration
+sp_func_proto_paren = remove # ignore/add/remove/force
+
+# Add or remove space between function name and '(' on function definition
+sp_func_def_paren = remove # ignore/add/remove/force
+
+# Add or remove space inside empty function '()'
+sp_inside_fparens = ignore # ignore/add/remove/force
+
+# Add or remove space inside function '(' and ')'
+sp_inside_fparen = remove # ignore/add/remove/force
+
+# Add or remove space between ']' and '(' when part of a function call.
+sp_square_fparen = ignore # ignore/add/remove/force
+
+# Add or remove space between ')' and '{' of function
+sp_fparen_brace = add # ignore/add/remove/force
+
+# Add or remove space between function name and '(' on function calls
+sp_func_call_paren = remove # ignore/add/remove/force
+
+# Add or remove space between a constructor/destructor and the open paren
+sp_func_class_paren = remove # ignore/add/remove/force
+
+# Add or remove space between 'return' and '('
+sp_return_paren = add # ignore/add/remove/force
+
+# Add or remove space between macro and value
+sp_macro = ignore # ignore/add/remove/force
+
+# Add or remove space between macro function ')' and value
+sp_macro_func = ignore # ignore/add/remove/force
+
+# Add or remove space between 'else' and '{' if on the same line
+sp_else_brace = ignore # ignore/add/remove/force
+
+# Add or remove space between '}' and 'else' if on the same line
+sp_brace_else = ignore # ignore/add/remove/force
+
+#
+# Code alignment (not left column spaces/tabs)
+#
+
+# Whether to keep non-indenting tabs
+align_keep_tabs = false # false/true
+
+# Whether to use tabs for alinging
+align_with_tabs = false # false/true
+
+# Whether to bump out to the next tab when aligning
+align_on_tabstop = false # false/true
+
+# Whether to left-align numbers
+align_number_left = true # false/true
+
+# The span for aligning variable definitions (0=don't align)
+align_var_def_span = 1 # number
+
+# Whether the pointer star is part of the variable name or not
+align_var_def_star = true # false/true
+
+# The threshold for aligning variable definitions (0=no limit)
+align_var_def_thresh = 12 # number
+
+# Whether to align the colon in struct bit fields
+align_var_def_colon = true # false/true
+
+# Whether to align inline struct/enum/union variable definitions
+align_var_def_inline = true # false/true
+
+# The span for aligning on '=' in assignments (0=don't align)
+align_assign_span = 1 # number
+
+# The threshold for aligning on '=' in assignments (0=no limit)
+align_assign_thresh = 12 # number
+
+# The span for aligning on '=' in enums (0=don't align)
+align_enum_equ_span = 4 # number
+
+# The threshold for aligning on '=' in enums (0=no limit)
+align_enum_equ_thresh = 0 # number
+
+# The span for aligning struct/union (0=don't align)
+align_var_struct_span = 99 # number
+
+# The span for aligning struct initializer values (0=don't align)
+align_struct_init_span = 3 # number
+
+# The minimum space between the type and the synonym of a typedef
+align_typedef_gap = 1 # number
+
+# The span for aligning single-line typedefs (0=don't align)
+align_typedef_span = 5 # number
+
+# Controls the positioning of the '*' in typedefs. Just try it.
+# 0: Align on typdef type, ignore '*'
+# 1: The '*' is part of type name: typedef int *pint;
+# 2: The '*' is part of the type: typedef int * pint;
+align_typedef_star_style = 1 # number
+
+# The span for aligning comments that end lines (0=don't align)
+align_right_cmt_span = 3 # number
+
+# The span for aligning function prototypes (0=don't align)
+align_func_proto_span = 0 # number
+
+# Whether to align macros wrapped with a backslash and a newline
+align_nl_cont = true # false/true
+
+# The minimum space between label and value of a preprocessor define
+align_pp_define_gap = 4 # number
+
+# The span for aligning on '#define' bodies (0=don't align)
+align_pp_define_span = 3 # number
+
+#
+# Newline adding and removing options
+#
+
+# Try to limit code width to N number of columns
+code_width = 79 # number
+
+# Whether to collapse empty blocks between '{' and '}'
+nl_collapse_empty_body = true # false/true
+
+# Don't touch one-line function bodies inside a class xx { } body
+nl_class_leave_one_liners = true # false/true
+
+# Add or remove newlines at the start of the file
+nl_start_of_file = remove # ignore/add/remove/force
+
+# The number of newlines at the start of the file (only used if nl_start_of_file is 'add' or 'force'
+nl_start_of_file_min = 0 # number
+
+# Add or remove newline at the end of the file
+nl_end_of_file = force # ignore/add/remove/force
+
+# The number of newlines at the end of the file (only used if nl_end_of_file is 'add' or 'force')
+nl_end_of_file_min = 1 # number
+
+# Add or remove newline between '=' and '{'
+nl_assign_brace = remove # ignore/add/remove/force
+
+# The number of newlines after a block of variable definitions
+nl_func_var_def_blk = 1 # number
+
+# Add or remove newline between function call and '('
+nl_fcall_brace = add # ignore/add/remove/force
+
+# Add or remove newline between 'enum' and '{'
+nl_enum_brace = remove # ignore/add/remove/force
+
+# Add or remove newline between 'struct and '{'
+nl_struct_brace = remove # ignore/add/remove/force
+
+# Add or remove newline between 'union' and '{'
+nl_union_brace = remove # ignore/add/remove/force
+
+# Add or remove newline between 'if' and '{'
+nl_if_brace = remove # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'else'
+nl_brace_else = add # ignore/add/remove/force
+
+# Add or remove newline between 'else if' and '{'
+# If set to ignore, nl_if_brace is used instead
+nl_elseif_brace = remove # ignore/add/remove/force
+
+# Add or remove newline between 'else' and '{'
+nl_else_brace = remove # ignore/add/remove/force
+
+# Add or remove newline between 'for' and '{'
+nl_for_brace = remove # ignore/add/remove/force
+
+# Add or remove newline between 'while' and '{'
+nl_while_brace = remove # ignore/add/remove/force
+
+# Add or remove newline between 'do' and '{'
+nl_do_brace = remove # ignore/add/remove/force
+
+# Add or remove newline between '}' and 'while' of 'do' statement
+nl_brace_while = remove # ignore/add/remove/force
+
+# Add or remove newline between 'switch' and '{'
+nl_switch_brace = remove # ignore/add/remove/force
+
+# Whether to put a newline before 'case' statement
+nl_before_case = true # false/true
+
+# Whether to put a newline after 'case' statement
+nl_after_case = false # false/true
+
+# Newline between namespace and {
+nl_namespace_brace = ignore # ignore/add/remove/force
+
+# Add or remove newline between 'template<>' and 'class'
+nl_template_class = add # ignore/add/remove/force
+
+# Add or remove newline between 'class' and '{'
+nl_class_brace = remove # ignore/add/remove/force
+
+# Add or remove newline after each ',' in the constructor member initialization
+nl_class_init_args = add # ignore/add/remove/force
+
+# Add or remove newline between return type and function name in definition
+nl_func_type_name = ignore # ignore/add/remove/force
+
+# Add or remove newline after '(' in a function declaration
+nl_func_decl_start = ignore # ignore/add/remove/force
+
+# Add or remove newline after each ',' in a function declaration
+nl_func_decl_args = ignore # ignore/add/remove/force
+
+# Add or remove newline before the ')' in a function declaration
+nl_func_decl_end = ignore # ignore/add/remove/force
+
+# Add or remove newline between function signature and '{'
+nl_fdef_brace = add # ignore/add/remove/force
+
+# Whether to put a newline after 'return' statement
+nl_after_return = true # false/true
+
+# Whether to put a newline after semicolons, except in 'for' statements
+nl_after_semicolon = false # false/true
+
+# Whether to put a newline after brace open
+nl_after_brace_open = false # false/true
+
+# Whether to alter newlines in '#define' macros
+nl_define_macro = false # false/true
+
+# Whether to not put blanks after '#ifxx', '#elxx', or before '#endif'
+nl_squeeze_ifdef = true # false/true
+
+# Add or remove newline before 'if'
+nl_before_if = ignore # ignore/add/remove/force
+
+# Add or remove newline after 'if'
+nl_after_if = ignore # ignore/add/remove/force
+
+# Add or remove newline before 'for'
+nl_before_for = ignore # ignore/add/remove/force
+
+# Add or remove newline after 'for'
+nl_after_for = ignore # ignore/add/remove/force
+
+# Add or remove newline before 'while'
+nl_before_while = ignore # ignore/add/remove/force
+
+# Add or remove newline after 'while'
+nl_after_while = ignore # ignore/add/remove/force
+
+# Add or remove newline before 'switch'
+nl_before_switch = ignore # ignore/add/remove/force
+
+# Add or remove newline after 'switch'
+nl_after_switch = ignore # ignore/add/remove/force
+
+# Add or remove newline before 'do'
+nl_before_do = ignore # ignore/add/remove/force
+
+# Add or remove newline after 'do'
+nl_after_do = ignore # ignore/add/remove/force
+
+#
+# Positioning options
+#
+
+# The position of boolean operators in wrapped expressions
+pos_bool = trail # ignore/lead/trail
+
+# The position of colons between constructor and member initialization
+pos_class_colon = lead # ignore/lead/trail
+
+#
+# Blank line options
+#
+
+# The maximum consecutive newlines
+nl_max = 4 # number
+
+# The number of newlines after a function prototype, if followed by another function prototype
+nl_after_func_proto = 0 # number
+
+# The number of newlines after a function prototype, if not followed by another function prototype
+nl_after_func_proto_group = 2 # number
+
+# The number of newlines after '}' of the function body
+nl_after_func_body = 2 # number
+
+# The minimum number of newlines before a multi-line comment (doesn't apply if after a brace open)
+nl_before_block_comment = 2 # number
+
+# Whether to remove blank lines after '{'
+eat_blanks_after_open_brace = true # false/true
+
+# Whether to remove blank lines before '}'
+eat_blanks_before_close_brace = true # false/true
+
+#
+# Code modifying options (non-whitespace)
+#
+
+# Add or remove braces on single-line 'do' statement
+mod_full_brace_do = add # ignore/add/remove/force
+
+# Add or remove braces on single-line 'for' statement
+mod_full_brace_for = add # ignore/add/remove/force
+
+# Add or remove braces on single-line function defintions. (Pawn)
+mod_full_brace_function = ignore # ignore/add/remove/force
+
+# Add or remove braces on single-line 'if' statement
+mod_full_brace_if = add # ignore/add/remove/force
+
+# Don't remove braces around statements that span N newlines
+mod_full_brace_nl = 0 # number
+
+# Add or remove braces on single-line 'while' statement
+mod_full_brace_while = add # ignore/add/remove/force
+
+# Add or remove unnecessary paren on 'return' statement
+mod_paren_on_return = remove # ignore/add/remove/force
+
+# Whether to change optional semicolons to real semicolons
+mod_pawn_semicolon = false # false/true
+
+#
+# Comment modifications
+#
+
+# Whether to group cpp-comments that look like they are in a block
+cmt_cpp_group = false # false/true
+
+# Whether to put an empty '/*' on the first line of the combined cpp-comment
+cmt_cpp_nl_start = false # false/true
+
+# Whether to put a newline before the closing '*/' of the combined cpp-comment
+cmt_cpp_nl_end = false # false/true
+
+# Whether to change cpp-comments into c-comments
+cmt_cpp_to_c = true # false/true
+
+# Whether to put a star on subsequent comment lines
+cmt_star_cont = true # false/true
+
+#
+# Preprocessor options
+#
+
+# Add or remove indent of preprocessor directives
+pp_indent = ignore # ignore/add/remove/force
+
+# Add or remove space between # and, say, define
+pp_space = ignore # ignore/add/remove/force
diff --git a/m4/ax_ac_append_to_file.m4 b/m4/ax_ac_append_to_file.m4
new file mode 100644
index 0000000..242b3d5
--- /dev/null
+++ b/m4/ax_ac_append_to_file.m4
@@ -0,0 +1,32 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_ac_append_to_file.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_AC_APPEND_TO_FILE([FILE],[DATA])
+#
+# DESCRIPTION
+#
+# Appends the specified data to the specified Autoconf is run. If you want
+# to append to a file when configure is run use AX_APPEND_TO_FILE instead.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 10
+
+AC_DEFUN([AX_AC_APPEND_TO_FILE],[
+AC_REQUIRE([AX_FILE_ESCAPES])
+m4_esyscmd(
+AX_FILE_ESCAPES
+[
+printf "%s" "$2" >> "$1"
+])
+])
diff --git a/m4/ax_ac_print_to_file.m4 b/m4/ax_ac_print_to_file.m4
new file mode 100644
index 0000000..642dfc1
--- /dev/null
+++ b/m4/ax_ac_print_to_file.m4
@@ -0,0 +1,32 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_ac_print_to_file.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_AC_PRINT_TO_FILE([FILE],[DATA])
+#
+# DESCRIPTION
+#
+# Writes the specified data to the specified file when Autoconf is run. If
+# you want to print to a file when configure is run use AX_PRINT_TO_FILE
+# instead.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 10
+
+AC_DEFUN([AX_AC_PRINT_TO_FILE],[
+m4_esyscmd(
+AC_REQUIRE([AX_FILE_ESCAPES])
+[
+printf "%s" "$2" > "$1"
+])
+])
diff --git a/m4/ax_add_am_macro_static.m4 b/m4/ax_add_am_macro_static.m4
new file mode 100644
index 0000000..6442d24
--- /dev/null
+++ b/m4/ax_add_am_macro_static.m4
@@ -0,0 +1,28 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_add_am_macro_static.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_ADD_AM_MACRO_STATIC([RULE])
+#
+# DESCRIPTION
+#
+# Adds the specified rule to $AMINCLUDE.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Tom Howard <tomhoward@users.sf.net>
+# Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 8
+
+AC_DEFUN([AX_ADD_AM_MACRO_STATIC],[
+ AC_REQUIRE([AX_AM_MACROS_STATIC])
+ AX_AC_APPEND_TO_FILE(AMINCLUDE_STATIC,[$1])
+])
diff --git a/m4/ax_am_macros_static.m4 b/m4/ax_am_macros_static.m4
new file mode 100644
index 0000000..f4cee8c
--- /dev/null
+++ b/m4/ax_am_macros_static.m4
@@ -0,0 +1,38 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_am_macros_static.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_AM_MACROS_STATIC
+#
+# DESCRIPTION
+#
+# Adds support for macros that create Automake rules. You must manually
+# add the following line
+#
+# include $(top_srcdir)/aminclude_static.am
+#
+# to your Makefile.am files.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Tom Howard <tomhoward@users.sf.net>
+# Copyright (c) 2009 Allan Caffee <allan.caffee@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 11
+
+AC_DEFUN([AMINCLUDE_STATIC],[aminclude_static.am])
+
+AC_DEFUN([AX_AM_MACROS_STATIC],
+[
+AX_AC_PRINT_TO_FILE(AMINCLUDE_STATIC,[
+# ]AMINCLUDE_STATIC[ generated automatically by Autoconf
+# from AX_AM_MACROS_STATIC on ]m4_esyscmd([LC_ALL=C date])[
+])
+])
diff --git a/m4/ax_check_gnu_make.m4 b/m4/ax_check_gnu_make.m4
new file mode 100644
index 0000000..785dc96
--- /dev/null
+++ b/m4/ax_check_gnu_make.m4
@@ -0,0 +1,95 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_check_gnu_make.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CHECK_GNU_MAKE([run-if-true],[run-if-false])
+#
+# DESCRIPTION
+#
+# This macro searches for a GNU version of make. If a match is found:
+#
+# * The makefile variable `ifGNUmake' is set to the empty string, otherwise
+# it is set to "#". This is useful for including a special features in a
+# Makefile, which cannot be handled by other versions of make.
+# * The makefile variable `ifnGNUmake' is set to #, otherwise
+# it is set to the empty string. This is useful for including a special
+# features in a Makefile, which can be handled
+# by other versions of make or to specify else like clause.
+# * The variable `_cv_gnu_make_command` is set to the command to invoke
+# GNU make if it exists, the empty string otherwise.
+# * The variable `ax_cv_gnu_make_command` is set to the command to invoke
+# GNU make by copying `_cv_gnu_make_command`, otherwise it is unset.
+# * If GNU Make is found, its version is extracted from the output of
+# `make --version` as the last field of a record of space-separated
+# columns and saved into the variable `ax_check_gnu_make_version`.
+# * Additionally if GNU Make is found, run shell code run-if-true
+# else run shell code run-if-false.
+#
+# Here is an example of its use:
+#
+# Makefile.in might contain:
+#
+# # A failsafe way of putting a dependency rule into a makefile
+# $(DEPEND):
+# $(CC) -MM $(srcdir)/*.c > $(DEPEND)
+#
+# @ifGNUmake@ ifeq ($(DEPEND),$(wildcard $(DEPEND)))
+# @ifGNUmake@ include $(DEPEND)
+# @ifGNUmake@ else
+# fallback code
+# @ifGNUmake@ endif
+#
+# Then configure.in would normally contain:
+#
+# AX_CHECK_GNU_MAKE()
+# AC_OUTPUT(Makefile)
+#
+# Then perhaps to cause gnu make to override any other make, we could do
+# something like this (note that GNU make always looks for GNUmakefile
+# first):
+#
+# if ! test x$_cv_gnu_make_command = x ; then
+# mv Makefile GNUmakefile
+# echo .DEFAULT: > Makefile ;
+# echo \ $_cv_gnu_make_command \$@ >> Makefile;
+# fi
+#
+# Then, if any (well almost any) other make is called, and GNU make also
+# exists, then the other make wraps the GNU make.
+#
+# LICENSE
+#
+# Copyright (c) 2008 John Darrington <j.darrington@elvis.murdoch.edu.au>
+# Copyright (c) 2015 Enrico M. Crisostomo <enrico.m.crisostomo@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 12
+
+AC_DEFUN([AX_CHECK_GNU_MAKE],dnl
+ [AC_PROG_AWK
+ AC_CACHE_CHECK([for GNU make],[_cv_gnu_make_command],[dnl
+ _cv_gnu_make_command="" ;
+dnl Search all the common names for GNU make
+ for a in "$MAKE" make gmake gnumake ; do
+ if test -z "$a" ; then continue ; fi ;
+ if "$a" --version 2> /dev/null | grep GNU 2>&1 > /dev/null ; then
+ _cv_gnu_make_command=$a ;
+ AX_CHECK_GNU_MAKE_HEADLINE=$("$a" --version 2> /dev/null | grep "GNU Make")
+ ax_check_gnu_make_version=$(echo ${AX_CHECK_GNU_MAKE_HEADLINE} | ${AWK} -F " " '{ print $(NF); }')
+ break ;
+ fi
+ done ;])
+dnl If there was a GNU version, then set @ifGNUmake@ to the empty string, '#' otherwise
+ AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifGNUmake], ["#"])], [AS_VAR_SET([ifGNUmake], [""])])
+ AS_VAR_IF([_cv_gnu_make_command], [""], [AS_VAR_SET([ifnGNUmake], [""])], [AS_VAR_SET([ifnGNUmake], ["#"])])
+ AS_VAR_IF([_cv_gnu_make_command], [""], [AS_UNSET(ax_cv_gnu_make_command)], [AS_VAR_SET([ax_cv_gnu_make_command], [${_cv_gnu_make_command}])])
+ AS_VAR_IF([_cv_gnu_make_command], [""],[$2],[$1])
+ AC_SUBST([ifGNUmake])
+ AC_SUBST([ifnGNUmake])
+])
diff --git a/m4/ax_check_link_flag.m4 b/m4/ax_check_link_flag.m4
new file mode 100644
index 0000000..819409a
--- /dev/null
+++ b/m4/ax_check_link_flag.m4
@@ -0,0 +1,74 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_check_link_flag.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CHECK_LINK_FLAG(FLAG, [ACTION-SUCCESS], [ACTION-FAILURE], [EXTRA-FLAGS], [INPUT])
+#
+# DESCRIPTION
+#
+# Check whether the given FLAG works with the linker or gives an error.
+# (Warnings, however, are ignored)
+#
+# ACTION-SUCCESS/ACTION-FAILURE are shell commands to execute on
+# success/failure.
+#
+# If EXTRA-FLAGS is defined, it is added to the linker's default flags
+# when the check is done. The check is thus made with the flags: "LDFLAGS
+# EXTRA-FLAGS FLAG". This can for example be used to force the linker to
+# issue an error when a bad flag is given.
+#
+# INPUT gives an alternative input source to AC_LINK_IFELSE.
+#
+# NOTE: Implementation based on AX_CFLAGS_GCC_OPTION. Please keep this
+# macro in sync with AX_CHECK_{PREPROC,COMPILE}_FLAG.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Guido U. Draheim <guidod@gmx.de>
+# Copyright (c) 2011 Maarten Bosmans <mkbosmans@gmail.com>
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# As a special exception, the respective Autoconf Macro's copyright owner
+# gives unlimited permission to copy, distribute and modify the configure
+# scripts that are the output of Autoconf when processing the Macro. You
+# need not follow the terms of the GNU General Public License when using
+# or distributing such scripts, even though portions of the text of the
+# Macro appear in them. The GNU General Public License (GPL) does govern
+# all other use of the material that constitutes the Autoconf Macro.
+#
+# This special exception to the GPL applies to versions of the Autoconf
+# Macro released by the Autoconf Archive. When you make and distribute a
+# modified version of the Autoconf Macro, you may extend this special
+# exception to the GPL to apply to your modified version as well.
+
+#serial 5
+
+AC_DEFUN([AX_CHECK_LINK_FLAG],
+[AC_PREREQ(2.64)dnl for _AC_LANG_PREFIX and AS_VAR_IF
+AS_VAR_PUSHDEF([CACHEVAR],[ax_cv_check_ldflags_$4_$1])dnl
+AC_CACHE_CHECK([whether the linker accepts $1], CACHEVAR, [
+ ax_check_save_flags=$LDFLAGS
+ LDFLAGS="$LDFLAGS $4 $1"
+ AC_LINK_IFELSE([m4_default([$5],[AC_LANG_PROGRAM()])],
+ [AS_VAR_SET(CACHEVAR,[yes])],
+ [AS_VAR_SET(CACHEVAR,[no])])
+ LDFLAGS=$ax_check_save_flags])
+AS_VAR_IF(CACHEVAR,yes,
+ [m4_default([$2], :)],
+ [m4_default([$3], :)])
+AS_VAR_POPDEF([CACHEVAR])dnl
+])dnl AX_CHECK_LINK_FLAGS
diff --git a/m4/ax_check_pcre2.m4 b/m4/ax_check_pcre2.m4
new file mode 100644
index 0000000..857a8c8
--- /dev/null
+++ b/m4/ax_check_pcre2.m4
@@ -0,0 +1,165 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_check_pcre2.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CHECK_PCRE2([bits], [action-if-found], [action-if-not-found])
+#
+# DESCRIPTION
+#
+# Search for an installed libpcre2-8 library. If nothing was specified
+# when calling configure, it searches first in /usr/local and then in
+# /usr, /opt/local and /sw. If the --with-pcre2=DIR is specified, it will
+# try to find it in DIR/include/pcre2.h and DIR/lib/libpcre2-8. If
+# --without-pcre2 is specified, the library is not searched at all.
+#
+# If 'bits' is empty or '8', PCRE2 8-bit character support is checked
+# only. If 'bits' contains '16', PCRE2 8-bit and 16-bit character support
+# are checked. If 'bits' contains '32', PCRE2 8-bit and 32-bit character
+# support are checked. When 'bits' contains both '16' and '32', PCRE2
+# 8-bit, 16-bit, and 32-bit character support is checked.
+#
+# If either the header file (pcre2.h), or the library (libpcre2-8) is not
+# found, or the specified PCRE2 character bit width is not supported,
+# shell commands 'action-if-not-found' is run. If 'action-if-not-found' is
+# not specified, the configuration exits on error, asking for a valid
+# PCRE2 installation directory or --without-pcre2.
+#
+# If both header file and library are found, and the specified PCRE2 bit
+# widths are supported, shell commands 'action-if-found' is run. If
+# 'action-if-found' is not specified, the default action appends
+# '-I${PCRE2_HOME}/include' to CPFLAGS, appends '-L$PCRE2_HOME}/lib' to
+# LDFLAGS, prepends '-lpcre2-8' to LIBS, and calls AC_DEFINE(HAVE_PCRE2).
+# You should use autoheader to include a definition for this symbol in a
+# config.h file. Sample usage in a C/C++ source is as follows:
+#
+# #ifdef HAVE_PCRE2
+# #define PCRE2_CODE_UNIT_WIDTH 8
+# #include <pcre2.h>
+# #endif /* HAVE_PCRE2 */
+#
+# LICENSE
+#
+# Copyright (c) 2020 Robert van Engelen <engelen@acm.org>
+#
+# This program is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# As a special exception, the respective Autoconf Macro's copyright owner
+# gives unlimited permission to copy, distribute and modify the configure
+# scripts that are the output of Autoconf when processing the Macro. You
+# need not follow the terms of the GNU General Public License when using
+# or distributing such scripts, even though portions of the text of the
+# Macro appear in them. The GNU General Public License (GPL) does govern
+# all other use of the material that constitutes the Autoconf Macro.
+#
+# This special exception to the GPL applies to versions of the Autoconf
+# Macro released by the Autoconf Archive. When you make and distribute a
+# modified version of the Autoconf Macro, you may extend this special
+# exception to the GPL to apply to your modified version as well.
+
+#serial 2
+
+AC_DEFUN([AX_CHECK_PCRE2],
+#
+# Handle user hints
+#
+[AC_MSG_CHECKING(if PCRE2 is wanted)
+pcre2_places="/usr/local /usr /opt/local /sw"
+AC_ARG_WITH([pcre2],
+[ --with-pcre2=DIR root directory path of PCRE2 installation @<:@defaults to
+ /usr/local or /usr if not found in /usr/local@:>@
+ --without-pcre2 to disable PCRE2 usage completely],
+[if test "$withval" != "no" ; then
+ AC_MSG_RESULT(yes)
+ if test -d "$withval"
+ then
+ pcre2_places="$withval $pcre2_places"
+ else
+ AC_MSG_WARN([Sorry, $withval does not exist, checking usual places])
+ fi
+else
+ pcre2_places=""
+ AC_MSG_RESULT(no)
+fi],
+[AC_MSG_RESULT(yes)])
+#
+# Locate PCRE2, if wanted
+#
+if test -n "${pcre2_places}"
+then
+ # check the user supplied or any other more or less 'standard' place:
+ # Most UNIX systems : /usr/local and /usr
+ # MacPorts / Fink on OSX : /opt/local respectively /sw
+ for PCRE2_HOME in ${pcre2_places} ; do
+ if test -f "${PCRE2_HOME}/include/pcre2.h"; then break; fi
+ PCRE2_HOME=""
+ done
+
+ PCRE2_OLD_LDFLAGS=$LDFLAGS
+ PCRE2_OLD_CPPFLAGS=$CPPFLAGS
+ if test -n "${PCRE2_HOME}"; then
+ LDFLAGS="$LDFLAGS -L${PCRE2_HOME}/lib"
+ CPPFLAGS="$CPPFLAGS -I${PCRE2_HOME}/include"
+ fi
+ AC_LANG_PUSH([C])
+ AC_CHECK_LIB([pcre2-8], [pcre2_compile_8], [pcre2_cv_libpcre2=yes], [pcre2_cv_libpcre2=no])
+ AC_CHECK_HEADER([pcre2.h], [pcre2_cv_pcre2_h=yes], [pcre2_cv_pcre2_h=no], [#define PCRE2_CODE_UNIT_WIDTH 8])
+ case "$1" in
+ *16*)
+ AC_CHECK_LIB([pcre2-16], [pcre2_compile_16], [pcre2_cv_libpcre2_16=yes], [pcre2_cv_libpcre2_16=no])
+ AC_CHECK_HEADER([pcre2.h], [pcre2_cv_pcre2_16_h=yes], [pcre2_cv_pcre2_16_h=no], [#define PCRE2_CODE_UNIT_WIDTH 16])
+ if test "$pcre2_cv_libpcre2_16" = "no" || test "$pcre2_cv_pcre2_16_h" = "no"; then
+ pcre2_cv_libpcre2=no
+ fi
+ ;;
+ esac
+ case "$1" in
+ *32*)
+ AC_CHECK_LIB([pcre2-32], [pcre2_compile_32], [pcre2_cv_libpcre2_32=yes], [pcre2_cv_libpcre2_32=no])
+ AC_CHECK_HEADER([pcre2.h], [pcre2_cv_pcre2_32_h=yes], [pcre2_cv_pcre2_32_h=no], [#define PCRE2_CODE_UNIT_WIDTH 32])
+ if test "$pcre2_cv_libpcre2_32" = "no" || test "$pcre2_cv_pcre2_32_h" = "no"; then
+ pcre2_cv_libpcre2=no
+ fi
+ esac
+ AC_LANG_POP([C])
+ if test "$pcre2_cv_libpcre2" = "yes" && test "$pcre2_cv_pcre2_h" = "yes"
+ then
+ #
+ # If both library and header were found, action-if-found
+ #
+ m4_ifblank([$2],[
+ if test -n "${PCRE2_HOME}"; then
+ CPPFLAGS="$CPPFLAGS -I${PCRE2_HOME}/include"
+ LDFLAGS="$LDFLAGS -L${PCRE2_HOME}/lib"
+ fi
+ LIBS="-lpcre2-8 $LIBS"
+ AC_DEFINE([HAVE_PCRE2], [1],
+ [Define to 1 if you have `PCRE2' library (-lpcre2-$1)])
+ ],[
+ # Restore variables
+ LDFLAGS="$PCRE2_OLD_LDFLAGS"
+ CPPFLAGS="$PCRE2_OLD_CPPFLAGS"
+ $2
+ ])
+ else
+ #
+ # If either header or library was not found, action-if-not-found
+ #
+ m4_default([$3],[
+ AC_MSG_ERROR([either specify a valid PCRE2 installation with --with-pcre2=DIR or disable PCRE2 usage with --without-pcre2])
+ ])
+ fi
+fi
+])
diff --git a/m4/ax_code_coverage.m4 b/m4/ax_code_coverage.m4
new file mode 100644
index 0000000..53dcf7b
--- /dev/null
+++ b/m4/ax_code_coverage.m4
@@ -0,0 +1,272 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_code_coverage.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CODE_COVERAGE()
+#
+# DESCRIPTION
+#
+# Defines CODE_COVERAGE_CPPFLAGS, CODE_COVERAGE_CFLAGS,
+# CODE_COVERAGE_CXXFLAGS and CODE_COVERAGE_LIBS which should be included
+# in the CPPFLAGS, CFLAGS CXXFLAGS and LIBS/LIBADD variables of every
+# build target (program or library) which should be built with code
+# coverage support. Also add rules using AX_ADD_AM_MACRO_STATIC; and
+# $enable_code_coverage which can be used in subsequent configure output.
+# CODE_COVERAGE_ENABLED is defined and substituted, and corresponds to the
+# value of the --enable-code-coverage option, which defaults to being
+# disabled.
+#
+# Test also for gcov program and create GCOV variable that could be
+# substituted.
+#
+# Note that all optimization flags in CFLAGS must be disabled when code
+# coverage is enabled.
+#
+# Usage example:
+#
+# configure.ac:
+#
+# AX_CODE_COVERAGE
+#
+# Makefile.am:
+#
+# include $(top_srcdir)/aminclude_static.am
+#
+# my_program_LIBS = ... $(CODE_COVERAGE_LIBS) ...
+# my_program_CPPFLAGS = ... $(CODE_COVERAGE_CPPFLAGS) ...
+# my_program_CFLAGS = ... $(CODE_COVERAGE_CFLAGS) ...
+# my_program_CXXFLAGS = ... $(CODE_COVERAGE_CXXFLAGS) ...
+#
+# clean-local: code-coverage-clean
+# distclean-local: code-coverage-dist-clean
+#
+# This results in a "check-code-coverage" rule being added to any
+# Makefile.am which do "include $(top_srcdir)/aminclude_static.am"
+# (assuming the module has been configured with --enable-code-coverage).
+# Running `make check-code-coverage` in that directory will run the
+# module's test suite (`make check`) and build a code coverage report
+# detailing the code which was touched, then print the URI for the report.
+#
+# This code was derived from Makefile.decl in GLib, originally licensed
+# under LGPLv2.1+.
+#
+# LICENSE
+#
+# Copyright (c) 2012, 2016 Philip Withnall
+# Copyright (c) 2012 Xan Lopez
+# Copyright (c) 2012 Christian Persch
+# Copyright (c) 2012 Paolo Borelli
+# Copyright (c) 2012 Dan Winship
+# Copyright (c) 2015,2018 Bastien ROUCARIES
+#
+# This library is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or (at
+# your option) any later version.
+#
+# This library is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser
+# General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+#serial 34
+
+m4_define(_AX_CODE_COVERAGE_RULES,[
+AX_ADD_AM_MACRO_STATIC([
+# Code coverage
+#
+# Optional:
+# - CODE_COVERAGE_DIRECTORY: Top-level directory for code coverage reporting.
+# Multiple directories may be specified, separated by whitespace.
+# (Default: \$(top_builddir))
+# - CODE_COVERAGE_OUTPUT_FILE: Filename and path for the .info file generated
+# by lcov for code coverage. (Default:
+# \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info)
+# - CODE_COVERAGE_OUTPUT_DIRECTORY: Directory for generated code coverage
+# reports to be created. (Default:
+# \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage)
+# - CODE_COVERAGE_BRANCH_COVERAGE: Set to 1 to enforce branch coverage,
+# set to 0 to disable it and leave empty to stay with the default.
+# (Default: empty)
+# - CODE_COVERAGE_LCOV_SHOPTS_DEFAULT: Extra options shared between both lcov
+# instances. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE)
+# - CODE_COVERAGE_LCOV_SHOPTS: Extra options to shared between both lcov
+# instances. (Default: $CODE_COVERAGE_LCOV_SHOPTS_DEFAULT)
+# - CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH: --gcov-tool pathtogcov
+# - CODE_COVERAGE_LCOV_OPTIONS_DEFAULT: Extra options to pass to the
+# collecting lcov instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH)
+# - CODE_COVERAGE_LCOV_OPTIONS: Extra options to pass to the collecting lcov
+# instance. (Default: $CODE_COVERAGE_LCOV_OPTIONS_DEFAULT)
+# - CODE_COVERAGE_LCOV_RMOPTS_DEFAULT: Extra options to pass to the filtering
+# lcov instance. (Default: empty)
+# - CODE_COVERAGE_LCOV_RMOPTS: Extra options to pass to the filtering lcov
+# instance. (Default: $CODE_COVERAGE_LCOV_RMOPTS_DEFAULT)
+# - CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT: Extra options to pass to the
+# genhtml instance. (Default: based on $CODE_COVERAGE_BRANCH_COVERAGE)
+# - CODE_COVERAGE_GENHTML_OPTIONS: Extra options to pass to the genhtml
+# instance. (Default: $CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT)
+# - CODE_COVERAGE_IGNORE_PATTERN: Extra glob pattern of files to ignore
+#
+# The generated report will be titled using the \$(PACKAGE_NAME) and
+# \$(PACKAGE_VERSION). In order to add the current git hash to the title,
+# use the git-version-gen script, available online.
+# Optional variables
+# run only on top dir
+if CODE_COVERAGE_ENABLED
+ ifeq (\$(abs_builddir), \$(abs_top_builddir))
+CODE_COVERAGE_DIRECTORY ?= \$(top_builddir)
+CODE_COVERAGE_OUTPUT_FILE ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage.info
+CODE_COVERAGE_OUTPUT_DIRECTORY ?= \$(PACKAGE_NAME)-\$(PACKAGE_VERSION)-coverage
+
+CODE_COVERAGE_BRANCH_COVERAGE ?=
+CODE_COVERAGE_LCOV_SHOPTS_DEFAULT ?= \$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\
+--rc lcov_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE))
+CODE_COVERAGE_LCOV_SHOPTS ?= \$(CODE_COVERAGE_LCOV_SHOPTS_DEFAULT)
+CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH ?= --gcov-tool \"\$(GCOV)\"
+CODE_COVERAGE_LCOV_OPTIONS_DEFAULT ?= \$(CODE_COVERAGE_LCOV_OPTIONS_GCOVPATH)
+CODE_COVERAGE_LCOV_OPTIONS ?= \$(CODE_COVERAGE_LCOV_OPTIONS_DEFAULT)
+CODE_COVERAGE_LCOV_RMOPTS_DEFAULT ?=
+CODE_COVERAGE_LCOV_RMOPTS ?= \$(CODE_COVERAGE_LCOV_RMOPTS_DEFAULT)
+CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT ?=\
+\$(if \$(CODE_COVERAGE_BRANCH_COVERAGE),\
+--rc genhtml_branch_coverage=\$(CODE_COVERAGE_BRANCH_COVERAGE))
+CODE_COVERAGE_GENHTML_OPTIONS ?= \$(CODE_COVERAGE_GENHTML_OPTIONS_DEFAULT)
+CODE_COVERAGE_IGNORE_PATTERN ?=
+
+GITIGNOREFILES := \$(GITIGNOREFILES) \$(CODE_COVERAGE_OUTPUT_FILE) \$(CODE_COVERAGE_OUTPUT_DIRECTORY)
+code_coverage_v_lcov_cap = \$(code_coverage_v_lcov_cap_\$(V))
+code_coverage_v_lcov_cap_ = \$(code_coverage_v_lcov_cap_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_v_lcov_cap_0 = @echo \" LCOV --capture\" \$(CODE_COVERAGE_OUTPUT_FILE);
+code_coverage_v_lcov_ign = \$(code_coverage_v_lcov_ign_\$(V))
+code_coverage_v_lcov_ign_ = \$(code_coverage_v_lcov_ign_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_v_lcov_ign_0 = @echo \" LCOV --remove /tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN);
+code_coverage_v_genhtml = \$(code_coverage_v_genhtml_\$(V))
+code_coverage_v_genhtml_ = \$(code_coverage_v_genhtml_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_v_genhtml_0 = @echo \" GEN \" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\";
+code_coverage_quiet = \$(code_coverage_quiet_\$(V))
+code_coverage_quiet_ = \$(code_coverage_quiet_\$(AM_DEFAULT_VERBOSITY))
+code_coverage_quiet_0 = --quiet
+
+# sanitizes the test-name: replaces with underscores: dashes and dots
+code_coverage_sanitize = \$(subst -,_,\$(subst .,_,\$(1)))
+
+# Use recursive makes in order to ignore errors during check
+check-code-coverage:
+ -\$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) -k check
+ \$(AM_V_at)\$(MAKE) \$(AM_MAKEFLAGS) code-coverage-capture
+
+# Capture code coverage data
+code-coverage-capture: code-coverage-capture-hook
+ \$(code_coverage_v_lcov_cap)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --capture --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" --test-name \"\$(call code_coverage_sanitize,\$(PACKAGE_NAME)-\$(PACKAGE_VERSION))\" --no-checksum --compat-libtool \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_OPTIONS)
+ \$(code_coverage_v_lcov_ign)\$(LCOV) \$(code_coverage_quiet) \$(addprefix --directory ,\$(CODE_COVERAGE_DIRECTORY)) --remove \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"/tmp/*\" \$(CODE_COVERAGE_IGNORE_PATTERN) --output-file \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_LCOV_SHOPTS) \$(CODE_COVERAGE_LCOV_RMOPTS)
+ -@rm -f \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\"
+ \$(code_coverage_v_genhtml)LANG=C \$(GENHTML) \$(code_coverage_quiet) \$(addprefix --prefix ,\$(CODE_COVERAGE_DIRECTORY)) --output-directory \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\" --title \"\$(PACKAGE_NAME)-\$(PACKAGE_VERSION) Code Coverage\" --legend --show-details \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \$(CODE_COVERAGE_GENHTML_OPTIONS)
+ @echo \"file://\$(abs_builddir)/\$(CODE_COVERAGE_OUTPUT_DIRECTORY)/index.html\"
+
+code-coverage-clean:
+ -\$(LCOV) --directory \$(top_builddir) -z
+ -rm -rf \"\$(CODE_COVERAGE_OUTPUT_FILE)\" \"\$(CODE_COVERAGE_OUTPUT_FILE).tmp\" \"\$(CODE_COVERAGE_OUTPUT_DIRECTORY)\"
+ -find . \\( -name \"*.gcda\" -o -name \"*.gcno\" -o -name \"*.gcov\" \\) -delete
+
+code-coverage-dist-clean:
+
+A][M_DISTCHECK_CONFIGURE_FLAGS := \$(A][M_DISTCHECK_CONFIGURE_FLAGS) --disable-code-coverage
+ else # ifneq (\$(abs_builddir), \$(abs_top_builddir))
+check-code-coverage:
+
+code-coverage-capture: code-coverage-capture-hook
+
+code-coverage-clean:
+
+code-coverage-dist-clean:
+ endif # ifeq (\$(abs_builddir), \$(abs_top_builddir))
+else #! CODE_COVERAGE_ENABLED
+# Use recursive makes in order to ignore errors during check
+check-code-coverage:
+ @echo \"Need to reconfigure with --enable-code-coverage\"
+# Capture code coverage data
+code-coverage-capture: code-coverage-capture-hook
+ @echo \"Need to reconfigure with --enable-code-coverage\"
+
+code-coverage-clean:
+
+code-coverage-dist-clean:
+
+endif #CODE_COVERAGE_ENABLED
+# Hook rule executed before code-coverage-capture, overridable by the user
+code-coverage-capture-hook:
+
+.PHONY: check-code-coverage code-coverage-capture code-coverage-dist-clean code-coverage-clean code-coverage-capture-hook
+])
+])
+
+AC_DEFUN([_AX_CODE_COVERAGE_ENABLED],[
+ AX_CHECK_GNU_MAKE([],[AC_MSG_ERROR([not using GNU make that is needed for coverage])])
+ AC_REQUIRE([AX_ADD_AM_MACRO_STATIC])
+ # check for gcov
+ AC_CHECK_TOOL([GCOV],
+ [$_AX_CODE_COVERAGE_GCOV_PROG_WITH],
+ [:])
+ AS_IF([test "X$GCOV" = "X:"],
+ [AC_MSG_ERROR([gcov is needed to do coverage])])
+ AC_SUBST([GCOV])
+
+ dnl Check if gcc is being used
+ AS_IF([ test "$GCC" = "no" ], [
+ AC_MSG_ERROR([not compiling with gcc, which is required for gcov code coverage])
+ ])
+
+ AC_CHECK_PROG([LCOV], [lcov], [lcov])
+ AC_CHECK_PROG([GENHTML], [genhtml], [genhtml])
+
+ AS_IF([ test x"$LCOV" = x ], [
+ AC_MSG_ERROR([To enable code coverage reporting you must have lcov installed])
+ ])
+
+ AS_IF([ test x"$GENHTML" = x ], [
+ AC_MSG_ERROR([Could not find genhtml from the lcov package])
+ ])
+
+ dnl Build the code coverage flags
+ dnl Define CODE_COVERAGE_LDFLAGS for backwards compatibility
+ CODE_COVERAGE_CPPFLAGS="-DNDEBUG"
+ CODE_COVERAGE_CFLAGS="-O0 -g -fprofile-arcs -ftest-coverage -fprofile-abs-path"
+ CODE_COVERAGE_CXXFLAGS="-O0 -g -fprofile-arcs -ftest-coverage -fprofile-abs-path"
+ CODE_COVERAGE_LIBS="-lgcov"
+
+ AC_SUBST([CODE_COVERAGE_CPPFLAGS])
+ AC_SUBST([CODE_COVERAGE_CFLAGS])
+ AC_SUBST([CODE_COVERAGE_CXXFLAGS])
+ AC_SUBST([CODE_COVERAGE_LIBS])
+])
+
+AC_DEFUN([AX_CODE_COVERAGE],[
+ dnl Check for --enable-code-coverage
+
+ # allow to override gcov location
+ AC_ARG_WITH([gcov],
+ [AS_HELP_STRING([--with-gcov[=GCOV]], [use given GCOV for coverage (GCOV=gcov).])],
+ [_AX_CODE_COVERAGE_GCOV_PROG_WITH=$with_gcov],
+ [_AX_CODE_COVERAGE_GCOV_PROG_WITH=gcov])
+
+ AC_MSG_CHECKING([whether to build with code coverage support])
+ AC_ARG_ENABLE([code-coverage],
+ AS_HELP_STRING([--enable-code-coverage],
+ [Whether to enable code coverage support]),,
+ enable_code_coverage=no)
+
+ AM_CONDITIONAL([CODE_COVERAGE_ENABLED], [test "x$enable_code_coverage" = xyes])
+ AC_SUBST([CODE_COVERAGE_ENABLED], [$enable_code_coverage])
+ AC_MSG_RESULT($enable_code_coverage)
+
+ AS_IF([ test "x$enable_code_coverage" = xyes ], [
+ _AX_CODE_COVERAGE_ENABLED
+ ])
+
+ _AX_CODE_COVERAGE_RULES
+])
diff --git a/m4/ax_cxx_compile_stdcxx.m4 b/m4/ax_cxx_compile_stdcxx.m4
new file mode 100644
index 0000000..9413da6
--- /dev/null
+++ b/m4/ax_cxx_compile_stdcxx.m4
@@ -0,0 +1,962 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_CXX_COMPILE_STDCXX(VERSION, [ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+# Check for baseline language coverage in the compiler for the specified
+# version of the C++ standard. If necessary, add switches to CXX and
+# CXXCPP to enable support. VERSION may be '11' (for the C++11 standard)
+# or '14' (for the C++14 standard).
+#
+# The second argument, if specified, indicates whether you insist on an
+# extended mode (e.g. -std=gnu++11) or a strict conformance mode (e.g.
+# -std=c++11). If neither is specified, you get whatever works, with
+# preference for no added switch, and then for an extended mode.
+#
+# The third argument, if specified 'mandatory' or if left unspecified,
+# indicates that baseline support for the specified C++ standard is
+# required and that the macro should error out if no mode with that
+# support is found. If specified 'optional', then configuration proceeds
+# regardless, after defining HAVE_CXX${VERSION} if and only if a
+# supporting mode is found.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Benjamin Kosnik <bkoz@redhat.com>
+# Copyright (c) 2012 Zack Weinberg <zackw@panix.com>
+# Copyright (c) 2013 Roy Stogner <roystgnr@ices.utexas.edu>
+# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov <sokolov@google.com>
+# Copyright (c) 2015 Paul Norman <penorman@mac.com>
+# Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+# Copyright (c) 2016, 2018 Krzesimir Nowak <qdlacz@gmail.com>
+# Copyright (c) 2019 Enji Cooper <yaneurabeya@gmail.com>
+# Copyright (c) 2020 Jason Merrill <jason@redhat.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 12
+
+dnl This macro is based on the code from the AX_CXX_COMPILE_STDCXX_11 macro
+dnl (serial version number 13).
+
+AC_DEFUN([AX_CXX_COMPILE_STDCXX], [dnl
+ m4_if([$1], [11], [ax_cxx_compile_alternatives="11 0x"],
+ [$1], [14], [ax_cxx_compile_alternatives="14 1y"],
+ [$1], [17], [ax_cxx_compile_alternatives="17 1z"],
+ [m4_fatal([invalid first argument `$1' to AX_CXX_COMPILE_STDCXX])])dnl
+ m4_if([$2], [], [],
+ [$2], [ext], [],
+ [$2], [noext], [],
+ [m4_fatal([invalid second argument `$2' to AX_CXX_COMPILE_STDCXX])])dnl
+ m4_if([$3], [], [ax_cxx_compile_cxx$1_required=true],
+ [$3], [mandatory], [ax_cxx_compile_cxx$1_required=true],
+ [$3], [optional], [ax_cxx_compile_cxx$1_required=false],
+ [m4_fatal([invalid third argument `$3' to AX_CXX_COMPILE_STDCXX])])
+ AC_LANG_PUSH([C++])dnl
+ ac_success=no
+
+ m4_if([$2], [], [dnl
+ AC_CACHE_CHECK(whether $CXX supports C++$1 features by default,
+ ax_cv_cxx_compile_cxx$1,
+ [AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+ [ax_cv_cxx_compile_cxx$1=yes],
+ [ax_cv_cxx_compile_cxx$1=no])])
+ if test x$ax_cv_cxx_compile_cxx$1 = xyes; then
+ ac_success=yes
+ fi])
+
+ m4_if([$2], [noext], [], [dnl
+ if test x$ac_success = xno; then
+ for alternative in ${ax_cxx_compile_alternatives}; do
+ switch="-std=gnu++${alternative}"
+ cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+ AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+ $cachevar,
+ [ac_save_CXX="$CXX"
+ CXX="$CXX $switch"
+ AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+ [eval $cachevar=yes],
+ [eval $cachevar=no])
+ CXX="$ac_save_CXX"])
+ if eval test x\$$cachevar = xyes; then
+ CXX="$CXX $switch"
+ if test -n "$CXXCPP" ; then
+ CXXCPP="$CXXCPP $switch"
+ fi
+ ac_success=yes
+ break
+ fi
+ done
+ fi])
+
+ m4_if([$2], [ext], [], [dnl
+ if test x$ac_success = xno; then
+ dnl HP's aCC needs +std=c++11 according to:
+ dnl http://h21007.www2.hp.com/portal/download/files/unprot/aCxx/PDF_Release_Notes/769149-001.pdf
+ dnl Cray's crayCC needs "-h std=c++11"
+ for alternative in ${ax_cxx_compile_alternatives}; do
+ for switch in -std=c++${alternative} +std=c++${alternative} "-h std=c++${alternative}"; do
+ cachevar=AS_TR_SH([ax_cv_cxx_compile_cxx$1_$switch])
+ AC_CACHE_CHECK(whether $CXX supports C++$1 features with $switch,
+ $cachevar,
+ [ac_save_CXX="$CXX"
+ CXX="$CXX $switch"
+ AC_COMPILE_IFELSE([AC_LANG_SOURCE([_AX_CXX_COMPILE_STDCXX_testbody_$1])],
+ [eval $cachevar=yes],
+ [eval $cachevar=no])
+ CXX="$ac_save_CXX"])
+ if eval test x\$$cachevar = xyes; then
+ CXX="$CXX $switch"
+ if test -n "$CXXCPP" ; then
+ CXXCPP="$CXXCPP $switch"
+ fi
+ ac_success=yes
+ break
+ fi
+ done
+ if test x$ac_success = xyes; then
+ break
+ fi
+ done
+ fi])
+ AC_LANG_POP([C++])
+ if test x$ax_cxx_compile_cxx$1_required = xtrue; then
+ if test x$ac_success = xno; then
+ AC_MSG_ERROR([*** A compiler with support for C++$1 language features is required.])
+ fi
+ fi
+ if test x$ac_success = xno; then
+ HAVE_CXX$1=0
+ AC_MSG_NOTICE([No compiler with C++$1 support was found])
+ else
+ HAVE_CXX$1=1
+ AC_DEFINE(HAVE_CXX$1,1,
+ [define if the compiler supports basic C++$1 syntax])
+ fi
+ AC_SUBST(HAVE_CXX$1)
+])
+
+
+dnl Test body for checking C++11 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_11],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+)
+
+
+dnl Test body for checking C++14 support
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_14],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+)
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_17],
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_11
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_14
+ _AX_CXX_COMPILE_STDCXX_testbody_new_in_17
+)
+
+dnl Tests for new features in C++11
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_11], [[
+
+// If the compiler admits that it is not ready for C++11, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201103L
+
+#error "This is not a C++11 compiler"
+
+#else
+
+namespace cxx11
+{
+
+ namespace test_static_assert
+ {
+
+ template <typename T>
+ struct check
+ {
+ static_assert(sizeof(int) <= sizeof(T), "not big enough");
+ };
+
+ }
+
+ namespace test_final_override
+ {
+
+ struct Base
+ {
+ virtual ~Base() {}
+ virtual void f() {}
+ };
+
+ struct Derived : public Base
+ {
+ virtual ~Derived() override {}
+ virtual void f() override {}
+ };
+
+ }
+
+ namespace test_double_right_angle_brackets
+ {
+
+ template < typename T >
+ struct check {};
+
+ typedef check<void> single_type;
+ typedef check<check<void>> double_type;
+ typedef check<check<check<void>>> triple_type;
+ typedef check<check<check<check<void>>>> quadruple_type;
+
+ }
+
+ namespace test_decltype
+ {
+
+ int
+ f()
+ {
+ int a = 1;
+ decltype(a) b = 2;
+ return a + b;
+ }
+
+ }
+
+ namespace test_type_deduction
+ {
+
+ template < typename T1, typename T2 >
+ struct is_same
+ {
+ static const bool value = false;
+ };
+
+ template < typename T >
+ struct is_same<T, T>
+ {
+ static const bool value = true;
+ };
+
+ template < typename T1, typename T2 >
+ auto
+ add(T1 a1, T2 a2) -> decltype(a1 + a2)
+ {
+ return a1 + a2;
+ }
+
+ int
+ test(const int c, volatile int v)
+ {
+ static_assert(is_same<int, decltype(0)>::value == true, "");
+ static_assert(is_same<int, decltype(c)>::value == false, "");
+ static_assert(is_same<int, decltype(v)>::value == false, "");
+ auto ac = c;
+ auto av = v;
+ auto sumi = ac + av + 'x';
+ auto sumf = ac + av + 1.0;
+ static_assert(is_same<int, decltype(ac)>::value == true, "");
+ static_assert(is_same<int, decltype(av)>::value == true, "");
+ static_assert(is_same<int, decltype(sumi)>::value == true, "");
+ static_assert(is_same<int, decltype(sumf)>::value == false, "");
+ static_assert(is_same<int, decltype(add(c, v))>::value == true, "");
+ return (sumf > 0.0) ? sumi : add(c, v);
+ }
+
+ }
+
+ namespace test_noexcept
+ {
+
+ int f() { return 0; }
+ int g() noexcept { return 0; }
+
+ static_assert(noexcept(f()) == false, "");
+ static_assert(noexcept(g()) == true, "");
+
+ }
+
+ namespace test_constexpr
+ {
+
+ template < typename CharT >
+ unsigned long constexpr
+ strlen_c_r(const CharT *const s, const unsigned long acc) noexcept
+ {
+ return *s ? strlen_c_r(s + 1, acc + 1) : acc;
+ }
+
+ template < typename CharT >
+ unsigned long constexpr
+ strlen_c(const CharT *const s) noexcept
+ {
+ return strlen_c_r(s, 0UL);
+ }
+
+ static_assert(strlen_c("") == 0UL, "");
+ static_assert(strlen_c("1") == 1UL, "");
+ static_assert(strlen_c("example") == 7UL, "");
+ static_assert(strlen_c("another\0example") == 7UL, "");
+
+ }
+
+ namespace test_rvalue_references
+ {
+
+ template < int N >
+ struct answer
+ {
+ static constexpr int value = N;
+ };
+
+ answer<1> f(int&) { return answer<1>(); }
+ answer<2> f(const int&) { return answer<2>(); }
+ answer<3> f(int&&) { return answer<3>(); }
+
+ void
+ test()
+ {
+ int i = 0;
+ const int c = 0;
+ static_assert(decltype(f(i))::value == 1, "");
+ static_assert(decltype(f(c))::value == 2, "");
+ static_assert(decltype(f(0))::value == 3, "");
+ }
+
+ }
+
+ namespace test_uniform_initialization
+ {
+
+ struct test
+ {
+ static const int zero {};
+ static const int one {1};
+ };
+
+ static_assert(test::zero == 0, "");
+ static_assert(test::one == 1, "");
+
+ }
+
+ namespace test_lambdas
+ {
+
+ void
+ test1()
+ {
+ auto lambda1 = [](){};
+ auto lambda2 = lambda1;
+ lambda1();
+ lambda2();
+ }
+
+ int
+ test2()
+ {
+ auto a = [](int i, int j){ return i + j; }(1, 2);
+ auto b = []() -> int { return '0'; }();
+ auto c = [=](){ return a + b; }();
+ auto d = [&](){ return c; }();
+ auto e = [a, &b](int x) mutable {
+ const auto identity = [](int y){ return y; };
+ for (auto i = 0; i < a; ++i)
+ a += b--;
+ return x + identity(a + b);
+ }(0);
+ return a + b + c + d + e;
+ }
+
+ int
+ test3()
+ {
+ const auto nullary = [](){ return 0; };
+ const auto unary = [](int x){ return x; };
+ using nullary_t = decltype(nullary);
+ using unary_t = decltype(unary);
+ const auto higher1st = [](nullary_t f){ return f(); };
+ const auto higher2nd = [unary](nullary_t f1){
+ return [unary, f1](unary_t f2){ return f2(unary(f1())); };
+ };
+ return higher1st(nullary) + higher2nd(nullary)(unary);
+ }
+
+ }
+
+ namespace test_variadic_templates
+ {
+
+ template <int...>
+ struct sum;
+
+ template <int N0, int... N1toN>
+ struct sum<N0, N1toN...>
+ {
+ static constexpr auto value = N0 + sum<N1toN...>::value;
+ };
+
+ template <>
+ struct sum<>
+ {
+ static constexpr auto value = 0;
+ };
+
+ static_assert(sum<>::value == 0, "");
+ static_assert(sum<1>::value == 1, "");
+ static_assert(sum<23>::value == 23, "");
+ static_assert(sum<1, 2>::value == 3, "");
+ static_assert(sum<5, 5, 11>::value == 21, "");
+ static_assert(sum<2, 3, 5, 7, 11, 13>::value == 41, "");
+
+ }
+
+ // http://stackoverflow.com/questions/13728184/template-aliases-and-sfinae
+ // Clang 3.1 fails with headers of libstd++ 4.8.3 when using std::function
+ // because of this.
+ namespace test_template_alias_sfinae
+ {
+
+ struct foo {};
+
+ template<typename T>
+ using member = typename T::member_type;
+
+ template<typename T>
+ void func(...) {}
+
+ template<typename T>
+ void func(member<T>*) {}
+
+ void test();
+
+ void test() { func<foo>(0); }
+
+ }
+
+} // namespace cxx11
+
+#endif // __cplusplus >= 201103L
+
+]])
+
+
+dnl Tests for new features in C++14
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_14], [[
+
+// If the compiler admits that it is not ready for C++14, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201402L
+
+#error "This is not a C++14 compiler"
+
+#else
+
+namespace cxx14
+{
+
+ namespace test_polymorphic_lambdas
+ {
+
+ int
+ test()
+ {
+ const auto lambda = [](auto&&... args){
+ const auto istiny = [](auto x){
+ return (sizeof(x) == 1UL) ? 1 : 0;
+ };
+ const int aretiny[] = { istiny(args)... };
+ return aretiny[0];
+ };
+ return lambda(1, 1L, 1.0f, '1');
+ }
+
+ }
+
+ namespace test_binary_literals
+ {
+
+ constexpr auto ivii = 0b0000000000101010;
+ static_assert(ivii == 42, "wrong value");
+
+ }
+
+ namespace test_generalized_constexpr
+ {
+
+ template < typename CharT >
+ constexpr unsigned long
+ strlen_c(const CharT *const s) noexcept
+ {
+ auto length = 0UL;
+ for (auto p = s; *p; ++p)
+ ++length;
+ return length;
+ }
+
+ static_assert(strlen_c("") == 0UL, "");
+ static_assert(strlen_c("x") == 1UL, "");
+ static_assert(strlen_c("test") == 4UL, "");
+ static_assert(strlen_c("another\0test") == 7UL, "");
+
+ }
+
+ namespace test_lambda_init_capture
+ {
+
+ int
+ test()
+ {
+ auto x = 0;
+ const auto lambda1 = [a = x](int b){ return a + b; };
+ const auto lambda2 = [a = lambda1(x)](){ return a; };
+ return lambda2();
+ }
+
+ }
+
+ namespace test_digit_separators
+ {
+
+ constexpr auto ten_million = 100'000'000;
+ static_assert(ten_million == 100000000, "");
+
+ }
+
+ namespace test_return_type_deduction
+ {
+
+ auto f(int& x) { return x; }
+ decltype(auto) g(int& x) { return x; }
+
+ template < typename T1, typename T2 >
+ struct is_same
+ {
+ static constexpr auto value = false;
+ };
+
+ template < typename T >
+ struct is_same<T, T>
+ {
+ static constexpr auto value = true;
+ };
+
+ int
+ test()
+ {
+ auto x = 0;
+ static_assert(is_same<int, decltype(f(x))>::value, "");
+ static_assert(is_same<int&, decltype(g(x))>::value, "");
+ return x;
+ }
+
+ }
+
+} // namespace cxx14
+
+#endif // __cplusplus >= 201402L
+
+]])
+
+
+dnl Tests for new features in C++17
+
+m4_define([_AX_CXX_COMPILE_STDCXX_testbody_new_in_17], [[
+
+// If the compiler admits that it is not ready for C++17, why torture it?
+// Hopefully, this will speed up the test.
+
+#ifndef __cplusplus
+
+#error "This is not a C++ compiler"
+
+#elif __cplusplus < 201703L
+
+#error "This is not a C++17 compiler"
+
+#else
+
+#include <initializer_list>
+#include <utility>
+#include <type_traits>
+
+namespace cxx17
+{
+
+ namespace test_constexpr_lambdas
+ {
+
+ constexpr int foo = [](){return 42;}();
+
+ }
+
+ namespace test::nested_namespace::definitions
+ {
+
+ }
+
+ namespace test_fold_expression
+ {
+
+ template<typename... Args>
+ int multiply(Args... args)
+ {
+ return (args * ... * 1);
+ }
+
+ template<typename... Args>
+ bool all(Args... args)
+ {
+ return (args && ...);
+ }
+
+ }
+
+ namespace test_extended_static_assert
+ {
+
+ static_assert (true);
+
+ }
+
+ namespace test_auto_brace_init_list
+ {
+
+ auto foo = {5};
+ auto bar {5};
+
+ static_assert(std::is_same<std::initializer_list<int>, decltype(foo)>::value);
+ static_assert(std::is_same<int, decltype(bar)>::value);
+ }
+
+ namespace test_typename_in_template_template_parameter
+ {
+
+ template<template<typename> typename X> struct D;
+
+ }
+
+ namespace test_fallthrough_nodiscard_maybe_unused_attributes
+ {
+
+ int f1()
+ {
+ return 42;
+ }
+
+ [[nodiscard]] int f2()
+ {
+ [[maybe_unused]] auto unused = f1();
+
+ switch (f1())
+ {
+ case 17:
+ f1();
+ [[fallthrough]];
+ case 42:
+ f1();
+ }
+ return f1();
+ }
+
+ }
+
+ namespace test_extended_aggregate_initialization
+ {
+
+ struct base1
+ {
+ int b1, b2 = 42;
+ };
+
+ struct base2
+ {
+ base2() {
+ b3 = 42;
+ }
+ int b3;
+ };
+
+ struct derived : base1, base2
+ {
+ int d;
+ };
+
+ derived d1 {{1, 2}, {}, 4}; // full initialization
+ derived d2 {{}, {}, 4}; // value-initialized bases
+
+ }
+
+ namespace test_general_range_based_for_loop
+ {
+
+ struct iter
+ {
+ int i;
+
+ int& operator* ()
+ {
+ return i;
+ }
+
+ const int& operator* () const
+ {
+ return i;
+ }
+
+ iter& operator++()
+ {
+ ++i;
+ return *this;
+ }
+ };
+
+ struct sentinel
+ {
+ int i;
+ };
+
+ bool operator== (const iter& i, const sentinel& s)
+ {
+ return i.i == s.i;
+ }
+
+ bool operator!= (const iter& i, const sentinel& s)
+ {
+ return !(i == s);
+ }
+
+ struct range
+ {
+ iter begin() const
+ {
+ return {0};
+ }
+
+ sentinel end() const
+ {
+ return {5};
+ }
+ };
+
+ void f()
+ {
+ range r {};
+
+ for (auto i : r)
+ {
+ [[maybe_unused]] auto v = i;
+ }
+ }
+
+ }
+
+ namespace test_lambda_capture_asterisk_this_by_value
+ {
+
+ struct t
+ {
+ int i;
+ int foo()
+ {
+ return [*this]()
+ {
+ return i;
+ }();
+ }
+ };
+
+ }
+
+ namespace test_enum_class_construction
+ {
+
+ enum class byte : unsigned char
+ {};
+
+ byte foo {42};
+
+ }
+
+ namespace test_constexpr_if
+ {
+
+ template <bool cond>
+ int f ()
+ {
+ if constexpr(cond)
+ {
+ return 13;
+ }
+ else
+ {
+ return 42;
+ }
+ }
+
+ }
+
+ namespace test_selection_statement_with_initializer
+ {
+
+ int f()
+ {
+ return 13;
+ }
+
+ int f2()
+ {
+ if (auto i = f(); i > 0)
+ {
+ return 3;
+ }
+
+ switch (auto i = f(); i + 4)
+ {
+ case 17:
+ return 2;
+
+ default:
+ return 1;
+ }
+ }
+
+ }
+
+ namespace test_template_argument_deduction_for_class_templates
+ {
+
+ template <typename T1, typename T2>
+ struct pair
+ {
+ pair (T1 p1, T2 p2)
+ : m1 {p1},
+ m2 {p2}
+ {}
+
+ T1 m1;
+ T2 m2;
+ };
+
+ void f()
+ {
+ [[maybe_unused]] auto p = pair{13, 42u};
+ }
+
+ }
+
+ namespace test_non_type_auto_template_parameters
+ {
+
+ template <auto n>
+ struct B
+ {};
+
+ B<5> b1;
+ B<'a'> b2;
+
+ }
+
+ namespace test_structured_bindings
+ {
+
+ int arr[2] = { 1, 2 };
+ std::pair<int, int> pr = { 1, 2 };
+
+ auto f1() -> int(&)[2]
+ {
+ return arr;
+ }
+
+ auto f2() -> std::pair<int, int>&
+ {
+ return pr;
+ }
+
+ struct S
+ {
+ int x1 : 2;
+ volatile double y1;
+ };
+
+ S f3()
+ {
+ return {};
+ }
+
+ auto [ x1, y1 ] = f1();
+ auto& [ xr1, yr1 ] = f1();
+ auto [ x2, y2 ] = f2();
+ auto& [ xr2, yr2 ] = f2();
+ const auto [ x3, y3 ] = f3();
+
+ }
+
+ namespace test_exception_spec_type_system
+ {
+
+ struct Good {};
+ struct Bad {};
+
+ void g1() noexcept;
+ void g2();
+
+ template<typename T>
+ Bad
+ f(T*, T*);
+
+ template<typename T1, typename T2>
+ Good
+ f(T1*, T2*);
+
+ static_assert (std::is_same_v<Good, decltype(f(g1, g2))>);
+
+ }
+
+ namespace test_inline_variables
+ {
+
+ template<class T> void f(T)
+ {}
+
+ template<class T> inline T g(T)
+ {
+ return T{};
+ }
+
+ template<> inline void f<>(int)
+ {}
+
+ template<> int g<>(int)
+ {
+ return 5;
+ }
+
+ }
+
+} // namespace cxx17
+
+#endif // __cplusplus < 201703L
+
+]])
diff --git a/m4/ax_cxx_compile_stdcxx_11.m4 b/m4/ax_cxx_compile_stdcxx_11.m4
new file mode 100644
index 0000000..0aadeaf
--- /dev/null
+++ b/m4/ax_cxx_compile_stdcxx_11.m4
@@ -0,0 +1,39 @@
+# ============================================================================
+# http://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx_11.html
+# ============================================================================
+#
+# SYNOPSIS
+#
+# AX_CXX_COMPILE_STDCXX_11([ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+# Check for baseline language coverage in the compiler for the C++11
+# standard; if necessary, add switches to CXX and CXXCPP to enable
+# support.
+#
+# This macro is a convenience alias for calling the AX_CXX_COMPILE_STDCXX
+# macro with the version set to C++11. The two optional arguments are
+# forwarded literally as the second and third argument respectively.
+# Please see the documentation for the AX_CXX_COMPILE_STDCXX macro for
+# more information. If you want to use this macro, you also need to
+# download the ax_cxx_compile_stdcxx.m4 file.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Benjamin Kosnik <bkoz@redhat.com>
+# Copyright (c) 2012 Zack Weinberg <zackw@panix.com>
+# Copyright (c) 2013 Roy Stogner <roystgnr@ices.utexas.edu>
+# Copyright (c) 2014, 2015 Google Inc.; contributed by Alexey Sokolov <sokolov@google.com>
+# Copyright (c) 2015 Paul Norman <penorman@mac.com>
+# Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 17
+
+AX_REQUIRE_DEFINED([AX_CXX_COMPILE_STDCXX])
+AC_DEFUN([AX_CXX_COMPILE_STDCXX_11], [AX_CXX_COMPILE_STDCXX([11], [$1], [$2])])
diff --git a/m4/ax_cxx_compile_stdcxx_14.m4 b/m4/ax_cxx_compile_stdcxx_14.m4
new file mode 100644
index 0000000..094db0d
--- /dev/null
+++ b/m4/ax_cxx_compile_stdcxx_14.m4
@@ -0,0 +1,34 @@
+# =============================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx_14.html
+# =============================================================================
+#
+# SYNOPSIS
+#
+# AX_CXX_COMPILE_STDCXX_14([ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+# Check for baseline language coverage in the compiler for the C++14
+# standard; if necessary, add switches to CXX and CXXCPP to enable
+# support.
+#
+# This macro is a convenience alias for calling the AX_CXX_COMPILE_STDCXX
+# macro with the version set to C++14. The two optional arguments are
+# forwarded literally as the second and third argument respectively.
+# Please see the documentation for the AX_CXX_COMPILE_STDCXX macro for
+# more information. If you want to use this macro, you also need to
+# download the ax_cxx_compile_stdcxx.m4 file.
+#
+# LICENSE
+#
+# Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 5
+
+AX_REQUIRE_DEFINED([AX_CXX_COMPILE_STDCXX])
+AC_DEFUN([AX_CXX_COMPILE_STDCXX_14], [AX_CXX_COMPILE_STDCXX([14], [$1], [$2])])
diff --git a/m4/ax_cxx_compile_stdcxx_17.m4 b/m4/ax_cxx_compile_stdcxx_17.m4
new file mode 100644
index 0000000..a683417
--- /dev/null
+++ b/m4/ax_cxx_compile_stdcxx_17.m4
@@ -0,0 +1,35 @@
+# =============================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_cxx_compile_stdcxx_17.html
+# =============================================================================
+#
+# SYNOPSIS
+#
+# AX_CXX_COMPILE_STDCXX_17([ext|noext], [mandatory|optional])
+#
+# DESCRIPTION
+#
+# Check for baseline language coverage in the compiler for the C++17
+# standard; if necessary, add switches to CXX and CXXCPP to enable
+# support.
+#
+# This macro is a convenience alias for calling the AX_CXX_COMPILE_STDCXX
+# macro with the version set to C++17. The two optional arguments are
+# forwarded literally as the second and third argument respectively.
+# Please see the documentation for the AX_CXX_COMPILE_STDCXX macro for
+# more information. If you want to use this macro, you also need to
+# download the ax_cxx_compile_stdcxx.m4 file.
+#
+# LICENSE
+#
+# Copyright (c) 2015 Moritz Klammler <moritz@klammler.eu>
+# Copyright (c) 2016 Krzesimir Nowak <qdlacz@gmail.com>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 2
+
+AX_REQUIRE_DEFINED([AX_CXX_COMPILE_STDCXX])
+AC_DEFUN([AX_CXX_COMPILE_STDCXX_17], [AX_CXX_COMPILE_STDCXX([17], [$1], [$2])])
diff --git a/m4/ax_file_escapes.m4 b/m4/ax_file_escapes.m4
new file mode 100644
index 0000000..a86fdc3
--- /dev/null
+++ b/m4/ax_file_escapes.m4
@@ -0,0 +1,30 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_file_escapes.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_FILE_ESCAPES
+#
+# DESCRIPTION
+#
+# Writes the specified data to the specified file.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Tom Howard <tomhoward@users.sf.net>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 8
+
+AC_DEFUN([AX_FILE_ESCAPES],[
+AX_DOLLAR="\$"
+AX_SRB="\\135"
+AX_SLB="\\133"
+AX_BS="\\\\"
+AX_DQ="\""
+])
diff --git a/m4/ax_prog_cc_for_build.m4 b/m4/ax_prog_cc_for_build.m4
new file mode 100644
index 0000000..18a4287
--- /dev/null
+++ b/m4/ax_prog_cc_for_build.m4
@@ -0,0 +1,141 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_prog_cc_for_build.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_PROG_CC_FOR_BUILD
+#
+# DESCRIPTION
+#
+# This macro searches for a C compiler that generates native executables,
+# that is a C compiler that surely is not a cross-compiler. This can be
+# useful if you have to generate source code at compile-time like for
+# example GCC does.
+#
+# The macro sets the CC_FOR_BUILD and CPP_FOR_BUILD macros to anything
+# needed to compile or link (CC_FOR_BUILD) and preprocess (CPP_FOR_BUILD).
+# The value of these variables can be overridden by the user by specifying
+# a compiler with an environment variable (like you do for standard CC).
+#
+# It also sets BUILD_EXEEXT and BUILD_OBJEXT to the executable and object
+# file extensions for the build platform, and GCC_FOR_BUILD to `yes' if
+# the compiler we found is GCC. All these variables but GCC_FOR_BUILD are
+# substituted in the Makefile.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Paolo Bonzini <bonzini@gnu.org>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice
+# and this notice are preserved. This file is offered as-is, without any
+# warranty.
+
+#serial 18
+
+AU_ALIAS([AC_PROG_CC_FOR_BUILD], [AX_PROG_CC_FOR_BUILD])
+AC_DEFUN([AX_PROG_CC_FOR_BUILD], [dnl
+AC_REQUIRE([AC_PROG_CC])dnl
+AC_REQUIRE([AC_PROG_CPP])dnl
+AC_REQUIRE([AC_CANONICAL_BUILD])dnl
+
+dnl Use the standard macros, but make them use other variable names
+dnl
+pushdef([ac_cv_prog_CPP], ac_cv_build_prog_CPP)dnl
+pushdef([ac_cv_prog_cc_c89], ac_cv_build_prog_cc_c89)dnl
+pushdef([ac_cv_prog_gcc], ac_cv_build_prog_gcc)dnl
+pushdef([ac_cv_prog_cc_works], ac_cv_build_prog_cc_works)dnl
+pushdef([ac_cv_prog_cc_cross], ac_cv_build_prog_cc_cross)dnl
+pushdef([ac_cv_prog_cc_g], ac_cv_build_prog_cc_g)dnl
+dnl pushdef([ac_cv_c_compiler_gnu], ac_cv_build_c_compiler_gnu)dnl
+pushdef([ac_cv_exeext], ac_cv_build_exeext)dnl
+pushdef([ac_cv_objext], ac_cv_build_objext)dnl
+pushdef([ac_exeext], ac_build_exeext)dnl
+pushdef([ac_objext], ac_build_objext)dnl
+pushdef([CC], CC_FOR_BUILD)dnl
+pushdef([CPP], CPP_FOR_BUILD)dnl
+pushdef([GCC], GCC_FOR_BUILD)dnl
+pushdef([CFLAGS], CFLAGS_FOR_BUILD)dnl
+pushdef([CPPFLAGS], CPPFLAGS_FOR_BUILD)dnl
+pushdef([EXEEXT], BUILD_EXEEXT)dnl
+pushdef([LDFLAGS], LDFLAGS_FOR_BUILD)dnl
+pushdef([OBJEXT], BUILD_OBJEXT)dnl
+pushdef([host], build)dnl
+pushdef([host_alias], build_alias)dnl
+pushdef([host_cpu], build_cpu)dnl
+pushdef([host_vendor], build_vendor)dnl
+pushdef([host_os], build_os)dnl
+pushdef([ac_cv_host], ac_cv_build)dnl
+pushdef([ac_cv_host_alias], ac_cv_build_alias)dnl
+pushdef([ac_cv_host_cpu], ac_cv_build_cpu)dnl
+pushdef([ac_cv_host_vendor], ac_cv_build_vendor)dnl
+pushdef([ac_cv_host_os], ac_cv_build_os)dnl
+pushdef([ac_tool_prefix], ac_build_tool_prefix)dnl
+pushdef([am_cv_CC_dependencies_compiler_type], am_cv_build_CC_dependencies_compiler_type)dnl
+pushdef([am_cv_prog_cc_c_o], am_cv_build_prog_cc_c_o)dnl
+pushdef([cross_compiling], cross_compiling_build)dnl
+
+_save_ax_prog_cc_for_build__ac_ext="$ac_cv_c_compiler_gnu"
+cross_compiling_build=no
+
+ac_build_tool_prefix=
+AS_IF([test -n "$build"], [ac_build_tool_prefix="$build-"],
+ [test -n "$build_alias"],[ac_build_tool_prefix="$build_alias-"])
+
+AC_LANG_PUSH([C])
+AC_PROG_CC
+_AC_COMPILER_EXEEXT
+_AC_COMPILER_OBJEXT
+AC_PROG_CPP
+
+dnl Restore the old definitions
+dnl
+ac_cv_c_compiler_gnu="$_save_ax_prog_cc_for_build__ac_ext"
+popdef([cross_compiling])dnl
+popdef([am_cv_prog_cc_c_o])dnl
+popdef([am_cv_CC_dependencies_compiler_type])dnl
+popdef([ac_tool_prefix])dnl
+popdef([ac_cv_host_os])dnl
+popdef([ac_cv_host_vendor])dnl
+popdef([ac_cv_host_cpu])dnl
+popdef([ac_cv_host_alias])dnl
+popdef([ac_cv_host])dnl
+popdef([host_os])dnl
+popdef([host_vendor])dnl
+popdef([host_cpu])dnl
+popdef([host_alias])dnl
+popdef([host])dnl
+popdef([OBJEXT])dnl
+popdef([LDFLAGS])dnl
+popdef([EXEEXT])dnl
+popdef([CPPFLAGS])dnl
+popdef([CFLAGS])dnl
+popdef([GCC])dnl
+popdef([CPP])dnl
+popdef([CC])dnl
+popdef([ac_objext])dnl
+popdef([ac_exeext])dnl
+popdef([ac_cv_objext])dnl
+popdef([ac_cv_exeext])dnl
+dnl popdef([ac_cv_c_compiler_gnu])dnl
+popdef([ac_cv_prog_cc_g])dnl
+popdef([ac_cv_prog_cc_cross])dnl
+popdef([ac_cv_prog_cc_works])dnl
+popdef([ac_cv_prog_cc_c89])dnl
+popdef([ac_cv_prog_gcc])dnl
+popdef([ac_cv_prog_CPP])dnl
+
+dnl restore global variables ac_ext, ac_cpp, ac_compile,
+dnl ac_link, ac_compiler_gnu (dependant on the current
+dnl language after popping):
+AC_LANG_POP([C])
+
+dnl Finally, set Makefile variables
+dnl
+AC_SUBST(BUILD_EXEEXT)dnl
+AC_SUBST(BUILD_OBJEXT)dnl
+AC_SUBST([CFLAGS_FOR_BUILD])dnl
+AC_SUBST([CPPFLAGS_FOR_BUILD])dnl
+AC_SUBST([LDFLAGS_FOR_BUILD])dnl
+])
diff --git a/m4/ax_pthread.m4 b/m4/ax_pthread.m4
new file mode 100644
index 0000000..1598d07
--- /dev/null
+++ b/m4/ax_pthread.m4
@@ -0,0 +1,507 @@
+# ===========================================================================
+# https://www.gnu.org/software/autoconf-archive/ax_pthread.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_PTHREAD([ACTION-IF-FOUND[, ACTION-IF-NOT-FOUND]])
+#
+# DESCRIPTION
+#
+# This macro figures out how to build C programs using POSIX threads. It
+# sets the PTHREAD_LIBS output variable to the threads library and linker
+# flags, and the PTHREAD_CFLAGS output variable to any special C compiler
+# flags that are needed. (The user can also force certain compiler
+# flags/libs to be tested by setting these environment variables.)
+#
+# Also sets PTHREAD_CC to any special C compiler that is needed for
+# multi-threaded programs (defaults to the value of CC otherwise). (This
+# is necessary on AIX to use the special cc_r compiler alias.)
+#
+# NOTE: You are assumed to not only compile your program with these flags,
+# but also to link with them as well. For example, you might link with
+# $PTHREAD_CC $CFLAGS $PTHREAD_CFLAGS $LDFLAGS ... $PTHREAD_LIBS $LIBS
+#
+# If you are only building threaded programs, you may wish to use these
+# variables in your default LIBS, CFLAGS, and CC:
+#
+# LIBS="$PTHREAD_LIBS $LIBS"
+# CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+# CC="$PTHREAD_CC"
+#
+# In addition, if the PTHREAD_CREATE_JOINABLE thread-attribute constant
+# has a nonstandard name, this macro defines PTHREAD_CREATE_JOINABLE to
+# that name (e.g. PTHREAD_CREATE_UNDETACHED on AIX).
+#
+# Also HAVE_PTHREAD_PRIO_INHERIT is defined if pthread is found and the
+# PTHREAD_PRIO_INHERIT symbol is defined when compiling with
+# PTHREAD_CFLAGS.
+#
+# ACTION-IF-FOUND is a list of shell commands to run if a threads library
+# is found, and ACTION-IF-NOT-FOUND is a list of commands to run it if it
+# is not found. If ACTION-IF-FOUND is not specified, the default action
+# will define HAVE_PTHREAD.
+#
+# Please let the authors know if this macro fails on any platform, or if
+# you have any other suggestions or comments. This macro was based on work
+# by SGJ on autoconf scripts for FFTW (http://www.fftw.org/) (with help
+# from M. Frigo), as well as ac_pthread and hb_pthread macros posted by
+# Alejandro Forero Cuervo to the autoconf macro repository. We are also
+# grateful for the helpful feedback of numerous users.
+#
+# Updated for Autoconf 2.68 by Daniel Richard G.
+#
+# LICENSE
+#
+# Copyright (c) 2008 Steven G. Johnson <stevenj@alum.mit.edu>
+# Copyright (c) 2011 Daniel Richard G. <skunk@iSKUNK.ORG>
+# Copyright (c) 2019 Marc Stevens <marc.stevens@cwi.nl>
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# As a special exception, the respective Autoconf Macro's copyright owner
+# gives unlimited permission to copy, distribute and modify the configure
+# scripts that are the output of Autoconf when processing the Macro. You
+# need not follow the terms of the GNU General Public License when using
+# or distributing such scripts, even though portions of the text of the
+# Macro appear in them. The GNU General Public License (GPL) does govern
+# all other use of the material that constitutes the Autoconf Macro.
+#
+# This special exception to the GPL applies to versions of the Autoconf
+# Macro released by the Autoconf Archive. When you make and distribute a
+# modified version of the Autoconf Macro, you may extend this special
+# exception to the GPL to apply to your modified version as well.
+
+#serial 27
+
+AU_ALIAS([ACX_PTHREAD], [AX_PTHREAD])
+AC_DEFUN([AX_PTHREAD], [
+AC_REQUIRE([AC_CANONICAL_HOST])
+AC_REQUIRE([AC_PROG_CC])
+AC_REQUIRE([AC_PROG_SED])
+AC_LANG_PUSH([C])
+ax_pthread_ok=no
+
+# We used to check for pthread.h first, but this fails if pthread.h
+# requires special compiler flags (e.g. on Tru64 or Sequent).
+# It gets checked for in the link test anyway.
+
+# First of all, check if the user has set any of the PTHREAD_LIBS,
+# etcetera environment variables, and if threads linking works using
+# them:
+if test "x$PTHREAD_CFLAGS$PTHREAD_LIBS" != "x"; then
+ ax_pthread_save_CC="$CC"
+ ax_pthread_save_CFLAGS="$CFLAGS"
+ ax_pthread_save_LIBS="$LIBS"
+ AS_IF([test "x$PTHREAD_CC" != "x"], [CC="$PTHREAD_CC"])
+ CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+ LIBS="$PTHREAD_LIBS $LIBS"
+ AC_MSG_CHECKING([for pthread_join using $CC $PTHREAD_CFLAGS $PTHREAD_LIBS])
+ AC_LINK_IFELSE([AC_LANG_CALL([], [pthread_join])], [ax_pthread_ok=yes])
+ AC_MSG_RESULT([$ax_pthread_ok])
+ if test "x$ax_pthread_ok" = "xno"; then
+ PTHREAD_LIBS=""
+ PTHREAD_CFLAGS=""
+ fi
+ CC="$ax_pthread_save_CC"
+ CFLAGS="$ax_pthread_save_CFLAGS"
+ LIBS="$ax_pthread_save_LIBS"
+fi
+
+# We must check for the threads library under a number of different
+# names; the ordering is very important because some systems
+# (e.g. DEC) have both -lpthread and -lpthreads, where one of the
+# libraries is broken (non-POSIX).
+
+# Create a list of thread flags to try. Items with a "," contain both
+# C compiler flags (before ",") and linker flags (after ","). Other items
+# starting with a "-" are C compiler flags, and remaining items are
+# library names, except for "none" which indicates that we try without
+# any flags at all, and "pthread-config" which is a program returning
+# the flags for the Pth emulation library.
+
+ax_pthread_flags="pthreads none -Kthread -pthread -pthreads -mthreads pthread --thread-safe -mt pthread-config"
+
+# The ordering *is* (sometimes) important. Some notes on the
+# individual items follow:
+
+# pthreads: AIX (must check this before -lpthread)
+# none: in case threads are in libc; should be tried before -Kthread and
+# other compiler flags to prevent continual compiler warnings
+# -Kthread: Sequent (threads in libc, but -Kthread needed for pthread.h)
+# -pthread: Linux/gcc (kernel threads), BSD/gcc (userland threads), Tru64
+# (Note: HP C rejects this with "bad form for `-t' option")
+# -pthreads: Solaris/gcc (Note: HP C also rejects)
+# -mt: Sun Workshop C (may only link SunOS threads [-lthread], but it
+# doesn't hurt to check since this sometimes defines pthreads and
+# -D_REENTRANT too), HP C (must be checked before -lpthread, which
+# is present but should not be used directly; and before -mthreads,
+# because the compiler interprets this as "-mt" + "-hreads")
+# -mthreads: Mingw32/gcc, Lynx/gcc
+# pthread: Linux, etcetera
+# --thread-safe: KAI C++
+# pthread-config: use pthread-config program (for GNU Pth library)
+
+case $host_os in
+
+ freebsd*)
+
+ # -kthread: FreeBSD kernel threads (preferred to -pthread since SMP-able)
+ # lthread: LinuxThreads port on FreeBSD (also preferred to -pthread)
+
+ ax_pthread_flags="-kthread lthread $ax_pthread_flags"
+ ;;
+
+ hpux*)
+
+ # From the cc(1) man page: "[-mt] Sets various -D flags to enable
+ # multi-threading and also sets -lpthread."
+
+ ax_pthread_flags="-mt -pthread pthread $ax_pthread_flags"
+ ;;
+
+ openedition*)
+
+ # IBM z/OS requires a feature-test macro to be defined in order to
+ # enable POSIX threads at all, so give the user a hint if this is
+ # not set. (We don't define these ourselves, as they can affect
+ # other portions of the system API in unpredictable ways.)
+
+ AC_EGREP_CPP([AX_PTHREAD_ZOS_MISSING],
+ [
+# if !defined(_OPEN_THREADS) && !defined(_UNIX03_THREADS)
+ AX_PTHREAD_ZOS_MISSING
+# endif
+ ],
+ [AC_MSG_WARN([IBM z/OS requires -D_OPEN_THREADS or -D_UNIX03_THREADS to enable pthreads support.])])
+ ;;
+
+ solaris*)
+
+ # On Solaris (at least, for some versions), libc contains stubbed
+ # (non-functional) versions of the pthreads routines, so link-based
+ # tests will erroneously succeed. (N.B.: The stubs are missing
+ # pthread_cleanup_push, or rather a function called by this macro,
+ # so we could check for that, but who knows whether they'll stub
+ # that too in a future libc.) So we'll check first for the
+ # standard Solaris way of linking pthreads (-mt -lpthread).
+
+ ax_pthread_flags="-mt,-lpthread pthread $ax_pthread_flags"
+ ;;
+esac
+
+# Are we compiling with Clang?
+
+AC_CACHE_CHECK([whether $CC is Clang],
+ [ax_cv_PTHREAD_CLANG],
+ [ax_cv_PTHREAD_CLANG=no
+ # Note that Autoconf sets GCC=yes for Clang as well as GCC
+ if test "x$GCC" = "xyes"; then
+ AC_EGREP_CPP([AX_PTHREAD_CC_IS_CLANG],
+ [/* Note: Clang 2.7 lacks __clang_[a-z]+__ */
+# if defined(__clang__) && defined(__llvm__)
+ AX_PTHREAD_CC_IS_CLANG
+# endif
+ ],
+ [ax_cv_PTHREAD_CLANG=yes])
+ fi
+ ])
+ax_pthread_clang="$ax_cv_PTHREAD_CLANG"
+
+
+# GCC generally uses -pthread, or -pthreads on some platforms (e.g. SPARC)
+
+# Note that for GCC and Clang -pthread generally implies -lpthread,
+# except when -nostdlib is passed.
+# This is problematic using libtool to build C++ shared libraries with pthread:
+# [1] https://gcc.gnu.org/bugzilla/show_bug.cgi?id=25460
+# [2] https://bugzilla.redhat.com/show_bug.cgi?id=661333
+# [3] https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=468555
+# To solve this, first try -pthread together with -lpthread for GCC
+
+AS_IF([test "x$GCC" = "xyes"],
+ [ax_pthread_flags="-pthread,-lpthread -pthread -pthreads $ax_pthread_flags"])
+
+# Clang takes -pthread (never supported any other flag), but we'll try with -lpthread first
+
+AS_IF([test "x$ax_pthread_clang" = "xyes"],
+ [ax_pthread_flags="-pthread,-lpthread -pthread"])
+
+
+# The presence of a feature test macro requesting re-entrant function
+# definitions is, on some systems, a strong hint that pthreads support is
+# correctly enabled
+
+case $host_os in
+ darwin* | hpux* | linux* | osf* | solaris*)
+ ax_pthread_check_macro="_REENTRANT"
+ ;;
+
+ aix*)
+ ax_pthread_check_macro="_THREAD_SAFE"
+ ;;
+
+ *)
+ ax_pthread_check_macro="--"
+ ;;
+esac
+AS_IF([test "x$ax_pthread_check_macro" = "x--"],
+ [ax_pthread_check_cond=0],
+ [ax_pthread_check_cond="!defined($ax_pthread_check_macro)"])
+
+
+if test "x$ax_pthread_ok" = "xno"; then
+for ax_pthread_try_flag in $ax_pthread_flags; do
+
+ case $ax_pthread_try_flag in
+ none)
+ AC_MSG_CHECKING([whether pthreads work without any flags])
+ ;;
+
+ *,*)
+ PTHREAD_CFLAGS=`echo $ax_pthread_try_flag | sed "s/^\(.*\),\(.*\)$/\1/"`
+ PTHREAD_LIBS=`echo $ax_pthread_try_flag | sed "s/^\(.*\),\(.*\)$/\2/"`
+ AC_MSG_CHECKING([whether pthreads work with "$PTHREAD_CFLAGS" and "$PTHREAD_LIBS"])
+ ;;
+
+ -*)
+ AC_MSG_CHECKING([whether pthreads work with $ax_pthread_try_flag])
+ PTHREAD_CFLAGS="$ax_pthread_try_flag"
+ ;;
+
+ pthread-config)
+ AC_CHECK_PROG([ax_pthread_config], [pthread-config], [yes], [no])
+ AS_IF([test "x$ax_pthread_config" = "xno"], [continue])
+ PTHREAD_CFLAGS="`pthread-config --cflags`"
+ PTHREAD_LIBS="`pthread-config --ldflags` `pthread-config --libs`"
+ ;;
+
+ *)
+ AC_MSG_CHECKING([for the pthreads library -l$ax_pthread_try_flag])
+ PTHREAD_LIBS="-l$ax_pthread_try_flag"
+ ;;
+ esac
+
+ ax_pthread_save_CFLAGS="$CFLAGS"
+ ax_pthread_save_LIBS="$LIBS"
+ CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+ LIBS="$PTHREAD_LIBS $LIBS"
+
+ # Check for various functions. We must include pthread.h,
+ # since some functions may be macros. (On the Sequent, we
+ # need a special flag -Kthread to make this header compile.)
+ # We check for pthread_join because it is in -lpthread on IRIX
+ # while pthread_create is in libc. We check for pthread_attr_init
+ # due to DEC craziness with -lpthreads. We check for
+ # pthread_cleanup_push because it is one of the few pthread
+ # functions on Solaris that doesn't have a non-functional libc stub.
+ # We try pthread_create on general principles.
+
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([#include <pthread.h>
+# if $ax_pthread_check_cond
+# error "$ax_pthread_check_macro must be defined"
+# endif
+ static void *some_global = NULL;
+ static void routine(void *a)
+ {
+ /* To avoid any unused-parameter or
+ unused-but-set-parameter warning. */
+ some_global = a;
+ }
+ static void *start_routine(void *a) { return a; }],
+ [pthread_t th; pthread_attr_t attr;
+ pthread_create(&th, 0, start_routine, 0);
+ pthread_join(th, 0);
+ pthread_attr_init(&attr);
+ pthread_cleanup_push(routine, 0);
+ pthread_cleanup_pop(0) /* ; */])],
+ [ax_pthread_ok=yes],
+ [])
+
+ CFLAGS="$ax_pthread_save_CFLAGS"
+ LIBS="$ax_pthread_save_LIBS"
+
+ AC_MSG_RESULT([$ax_pthread_ok])
+ AS_IF([test "x$ax_pthread_ok" = "xyes"], [break])
+
+ PTHREAD_LIBS=""
+ PTHREAD_CFLAGS=""
+done
+fi
+
+
+# Clang needs special handling, because older versions handle the -pthread
+# option in a rather... idiosyncratic way
+
+if test "x$ax_pthread_clang" = "xyes"; then
+
+ # Clang takes -pthread; it has never supported any other flag
+
+ # (Note 1: This will need to be revisited if a system that Clang
+ # supports has POSIX threads in a separate library. This tends not
+ # to be the way of modern systems, but it's conceivable.)
+
+ # (Note 2: On some systems, notably Darwin, -pthread is not needed
+ # to get POSIX threads support; the API is always present and
+ # active. We could reasonably leave PTHREAD_CFLAGS empty. But
+ # -pthread does define _REENTRANT, and while the Darwin headers
+ # ignore this macro, third-party headers might not.)
+
+ # However, older versions of Clang make a point of warning the user
+ # that, in an invocation where only linking and no compilation is
+ # taking place, the -pthread option has no effect ("argument unused
+ # during compilation"). They expect -pthread to be passed in only
+ # when source code is being compiled.
+ #
+ # Problem is, this is at odds with the way Automake and most other
+ # C build frameworks function, which is that the same flags used in
+ # compilation (CFLAGS) are also used in linking. Many systems
+ # supported by AX_PTHREAD require exactly this for POSIX threads
+ # support, and in fact it is often not straightforward to specify a
+ # flag that is used only in the compilation phase and not in
+ # linking. Such a scenario is extremely rare in practice.
+ #
+ # Even though use of the -pthread flag in linking would only print
+ # a warning, this can be a nuisance for well-run software projects
+ # that build with -Werror. So if the active version of Clang has
+ # this misfeature, we search for an option to squash it.
+
+ AC_CACHE_CHECK([whether Clang needs flag to prevent "argument unused" warning when linking with -pthread],
+ [ax_cv_PTHREAD_CLANG_NO_WARN_FLAG],
+ [ax_cv_PTHREAD_CLANG_NO_WARN_FLAG=unknown
+ # Create an alternate version of $ac_link that compiles and
+ # links in two steps (.c -> .o, .o -> exe) instead of one
+ # (.c -> exe), because the warning occurs only in the second
+ # step
+ ax_pthread_save_ac_link="$ac_link"
+ ax_pthread_sed='s/conftest\.\$ac_ext/conftest.$ac_objext/g'
+ ax_pthread_link_step=`$as_echo "$ac_link" | sed "$ax_pthread_sed"`
+ ax_pthread_2step_ac_link="($ac_compile) && (echo ==== >&5) && ($ax_pthread_link_step)"
+ ax_pthread_save_CFLAGS="$CFLAGS"
+ for ax_pthread_try in '' -Qunused-arguments -Wno-unused-command-line-argument unknown; do
+ AS_IF([test "x$ax_pthread_try" = "xunknown"], [break])
+ CFLAGS="-Werror -Wunknown-warning-option $ax_pthread_try -pthread $ax_pthread_save_CFLAGS"
+ ac_link="$ax_pthread_save_ac_link"
+ AC_LINK_IFELSE([AC_LANG_SOURCE([[int main(void){return 0;}]])],
+ [ac_link="$ax_pthread_2step_ac_link"
+ AC_LINK_IFELSE([AC_LANG_SOURCE([[int main(void){return 0;}]])],
+ [break])
+ ])
+ done
+ ac_link="$ax_pthread_save_ac_link"
+ CFLAGS="$ax_pthread_save_CFLAGS"
+ AS_IF([test "x$ax_pthread_try" = "x"], [ax_pthread_try=no])
+ ax_cv_PTHREAD_CLANG_NO_WARN_FLAG="$ax_pthread_try"
+ ])
+
+ case "$ax_cv_PTHREAD_CLANG_NO_WARN_FLAG" in
+ no | unknown) ;;
+ *) PTHREAD_CFLAGS="$ax_cv_PTHREAD_CLANG_NO_WARN_FLAG $PTHREAD_CFLAGS" ;;
+ esac
+
+fi # $ax_pthread_clang = yes
+
+
+
+# Various other checks:
+if test "x$ax_pthread_ok" = "xyes"; then
+ ax_pthread_save_CFLAGS="$CFLAGS"
+ ax_pthread_save_LIBS="$LIBS"
+ CFLAGS="$CFLAGS $PTHREAD_CFLAGS"
+ LIBS="$PTHREAD_LIBS $LIBS"
+
+ # Detect AIX lossage: JOINABLE attribute is called UNDETACHED.
+ AC_CACHE_CHECK([for joinable pthread attribute],
+ [ax_cv_PTHREAD_JOINABLE_ATTR],
+ [ax_cv_PTHREAD_JOINABLE_ATTR=unknown
+ for ax_pthread_attr in PTHREAD_CREATE_JOINABLE PTHREAD_CREATE_UNDETACHED; do
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([#include <pthread.h>],
+ [int attr = $ax_pthread_attr; return attr /* ; */])],
+ [ax_cv_PTHREAD_JOINABLE_ATTR=$ax_pthread_attr; break],
+ [])
+ done
+ ])
+ AS_IF([test "x$ax_cv_PTHREAD_JOINABLE_ATTR" != "xunknown" && \
+ test "x$ax_cv_PTHREAD_JOINABLE_ATTR" != "xPTHREAD_CREATE_JOINABLE" && \
+ test "x$ax_pthread_joinable_attr_defined" != "xyes"],
+ [AC_DEFINE_UNQUOTED([PTHREAD_CREATE_JOINABLE],
+ [$ax_cv_PTHREAD_JOINABLE_ATTR],
+ [Define to necessary symbol if this constant
+ uses a non-standard name on your system.])
+ ax_pthread_joinable_attr_defined=yes
+ ])
+
+ AC_CACHE_CHECK([whether more special flags are required for pthreads],
+ [ax_cv_PTHREAD_SPECIAL_FLAGS],
+ [ax_cv_PTHREAD_SPECIAL_FLAGS=no
+ case $host_os in
+ solaris*)
+ ax_cv_PTHREAD_SPECIAL_FLAGS="-D_POSIX_PTHREAD_SEMANTICS"
+ ;;
+ esac
+ ])
+ AS_IF([test "x$ax_cv_PTHREAD_SPECIAL_FLAGS" != "xno" && \
+ test "x$ax_pthread_special_flags_added" != "xyes"],
+ [PTHREAD_CFLAGS="$ax_cv_PTHREAD_SPECIAL_FLAGS $PTHREAD_CFLAGS"
+ ax_pthread_special_flags_added=yes])
+
+ AC_CACHE_CHECK([for PTHREAD_PRIO_INHERIT],
+ [ax_cv_PTHREAD_PRIO_INHERIT],
+ [AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include <pthread.h>]],
+ [[int i = PTHREAD_PRIO_INHERIT;
+ return i;]])],
+ [ax_cv_PTHREAD_PRIO_INHERIT=yes],
+ [ax_cv_PTHREAD_PRIO_INHERIT=no])
+ ])
+ AS_IF([test "x$ax_cv_PTHREAD_PRIO_INHERIT" = "xyes" && \
+ test "x$ax_pthread_prio_inherit_defined" != "xyes"],
+ [AC_DEFINE([HAVE_PTHREAD_PRIO_INHERIT], [1], [Have PTHREAD_PRIO_INHERIT.])
+ ax_pthread_prio_inherit_defined=yes
+ ])
+
+ CFLAGS="$ax_pthread_save_CFLAGS"
+ LIBS="$ax_pthread_save_LIBS"
+
+ # More AIX lossage: compile with *_r variant
+ if test "x$GCC" != "xyes"; then
+ case $host_os in
+ aix*)
+ AS_CASE(["x/$CC"],
+ [x*/c89|x*/c89_128|x*/c99|x*/c99_128|x*/cc|x*/cc128|x*/xlc|x*/xlc_v6|x*/xlc128|x*/xlc128_v6],
+ [#handle absolute path differently from PATH based program lookup
+ AS_CASE(["x$CC"],
+ [x/*],
+ [AS_IF([AS_EXECUTABLE_P([${CC}_r])],[PTHREAD_CC="${CC}_r"])],
+ [AC_CHECK_PROGS([PTHREAD_CC],[${CC}_r],[$CC])])])
+ ;;
+ esac
+ fi
+fi
+
+test -n "$PTHREAD_CC" || PTHREAD_CC="$CC"
+
+AC_SUBST([PTHREAD_LIBS])
+AC_SUBST([PTHREAD_CFLAGS])
+AC_SUBST([PTHREAD_CC])
+
+# Finally, execute ACTION-IF-FOUND/ACTION-IF-NOT-FOUND:
+if test "x$ax_pthread_ok" = "xyes"; then
+ ifelse([$1],,[AC_DEFINE([HAVE_PTHREAD],[1],[Define if you have POSIX threads libraries and header files.])],[$1])
+ :
+else
+ ax_pthread_ok=no
+ $2
+fi
+AC_LANG_POP
+])dnl AX_PTHREAD
diff --git a/m4/ax_with_curses.m4 b/m4/ax_with_curses.m4
new file mode 100644
index 0000000..cf1ee01
--- /dev/null
+++ b/m4/ax_with_curses.m4
@@ -0,0 +1,517 @@
+# ===========================================================================
+# http://www.gnu.org/software/autoconf-archive/ax_with_curses.html
+# ===========================================================================
+#
+# SYNOPSIS
+#
+# AX_WITH_CURSES
+#
+# DESCRIPTION
+#
+# This macro checks whether a SysV or X/Open-compatible Curses library is
+# present, along with the associated header file. The NcursesW
+# (wide-character) library is searched for first, followed by Ncurses,
+# then the system-default plain Curses. The first library found is the
+# one returned.
+#
+# The following options are understood: --with-ncursesw, --with-ncurses,
+# --without-ncursesw, --without-ncurses. The "--with" options force the
+# macro to use that particular library, terminating with an error if not
+# found. The "--without" options simply skip the check for that library.
+# The effect on the search pattern is:
+#
+# (no options) - NcursesW, Ncurses, Curses
+# --with-ncurses --with-ncursesw - NcursesW only [*]
+# --without-ncurses --with-ncursesw - NcursesW only [*]
+# --with-ncursesw - NcursesW only [*]
+# --with-ncurses --without-ncursesw - Ncurses only [*]
+# --with-ncurses - NcursesW, Ncurses [**]
+# --without-ncurses --without-ncursesw - Curses only
+# --without-ncursesw - Ncurses, Curses
+# --without-ncurses - NcursesW, Curses
+#
+# [*] If the library is not found, abort the configure script.
+#
+# [**] If the second library (Ncurses) is not found, abort configure.
+#
+# The following preprocessor symbols may be defined by this macro if the
+# appropriate conditions are met:
+#
+# HAVE_CURSES - if any SysV or X/Open Curses library found
+# HAVE_CURSES_ENHANCED - if library supports X/Open Enhanced functions
+# HAVE_CURSES_COLOR - if library supports color (enhanced functions)
+# HAVE_CURSES_OBSOLETE - if library supports certain obsolete features
+# HAVE_NCURSESW - if NcursesW (wide char) library is to be used
+# HAVE_NCURSES - if the Ncurses library is to be used
+#
+# HAVE_CURSES_H - if <curses.h> is present and should be used
+# HAVE_NCURSESW_H - if <ncursesw.h> should be used
+# HAVE_NCURSES_H - if <ncurses.h> should be used
+# HAVE_NCURSESW_CURSES_H - if <ncursesw/curses.h> should be used
+# HAVE_NCURSES_CURSES_H - if <ncurses/curses.h> should be used
+#
+# (These preprocessor symbols are discussed later in this document.)
+#
+# The following output variable is defined by this macro; it is precious
+# and may be overridden on the ./configure command line:
+#
+# CURSES_LIB - library to add to xxx_LDADD
+#
+# The library listed in CURSES_LIB is NOT added to LIBS by default. You
+# need to add CURSES_LIB to the appropriate xxx_LDADD line in your
+# Makefile.am. For example:
+#
+# prog_LDADD = @CURSES_LIB@
+#
+# If CURSES_LIB is set on the configure command line (such as by running
+# "./configure CURSES_LIB=-lmycurses"), then the only header searched for
+# is <curses.h>. The user may use the CPPFLAGS precious variable to
+# override the standard #include search path. If the user needs to
+# specify an alternative path for a library (such as for a non-standard
+# NcurseW), the user should use the LDFLAGS variable.
+#
+# The following shell variables may be defined by this macro:
+#
+# ax_cv_curses - set to "yes" if any Curses library found
+# ax_cv_curses_enhanced - set to "yes" if Enhanced functions present
+# ax_cv_curses_color - set to "yes" if color functions present
+# ax_cv_curses_obsolete - set to "yes" if obsolete features present
+#
+# ax_cv_ncursesw - set to "yes" if NcursesW library found
+# ax_cv_ncurses - set to "yes" if Ncurses library found
+# ax_cv_plaincurses - set to "yes" if plain Curses library found
+# ax_cv_curses_which - set to "ncursesw", "ncurses", "plaincurses" or "no"
+#
+# These variables can be used in your configure.ac to determine the level
+# of support you need from the Curses library. For example, if you must
+# have either Ncurses or NcursesW, you could include:
+#
+# AX_WITH_CURSES
+# if test "x$ax_cv_ncursesw" != xyes && test "x$ax_cv_ncurses" != xyes; then
+# AX_MSG_ERROR([requires either NcursesW or Ncurses library])
+# fi
+#
+# If any Curses library will do (but one must be present and must support
+# color), you could use:
+#
+# AX_WITH_CURSES
+# if test "x$ax_cv_curses" != xyes || test "x$ax_cv_curses_color" != xyes; then
+# AC_MSG_ERROR([requires an X/Open-compatible Curses library with color])
+# fi
+#
+# Certain preprocessor symbols and shell variables defined by this macro
+# can be used to determine various features of the Curses library. In
+# particular, HAVE_CURSES and ax_cv_curses are defined if the Curses
+# library found conforms to the traditional SysV and/or X/Open Base Curses
+# definition. Any working Curses library conforms to this level.
+#
+# HAVE_CURSES_ENHANCED and ax_cv_curses_enhanced are defined if the
+# library supports the X/Open Enhanced Curses definition. In particular,
+# the wide-character types attr_t, cchar_t and wint_t, the functions
+# wattr_set() and wget_wch() and the macros WA_NORMAL and _XOPEN_CURSES
+# are checked. The Ncurses library does NOT conform to this definition,
+# although NcursesW does.
+#
+# HAVE_CURSES_COLOR and ax_cv_curses_color are defined if the library
+# supports color functions and macros such as COLOR_PAIR, A_COLOR,
+# COLOR_WHITE, COLOR_RED and init_pair(). These are NOT part of the
+# X/Open Base Curses definition, but are part of the Enhanced set of
+# functions. The Ncurses library DOES support these functions, as does
+# NcursesW.
+#
+# HAVE_CURSES_OBSOLETE and ax_cv_curses_obsolete are defined if the
+# library supports certain features present in SysV and BSD Curses but not
+# defined in the X/Open definition. In particular, the functions
+# getattrs(), getcurx() and getmaxx() are checked.
+#
+# To use the HAVE_xxx_H preprocessor symbols, insert the following into
+# your system.h (or equivalent) header file:
+#
+# #if defined HAVE_NCURSESW_CURSES_H
+# # include <ncursesw/curses.h>
+# #elif defined HAVE_NCURSESW_H
+# # include <ncursesw.h>
+# #elif defined HAVE_NCURSES_CURSES_H
+# # include <ncurses/curses.h>
+# #elif defined HAVE_NCURSES_H
+# # include <ncurses.h>
+# #elif defined HAVE_CURSES_H
+# # include <curses.h>
+# #else
+# # error "SysV or X/Open-compatible Curses header file required"
+# #endif
+#
+# For previous users of this macro: you should not need to change anything
+# in your configure.ac or Makefile.am, as the previous (serial 10)
+# semantics are still valid. However, you should update your system.h (or
+# equivalent) header file to the fragment shown above. You are encouraged
+# also to make use of the extended functionality provided by this version
+# of AX_WITH_CURSES, as well as in the additional macros
+# AX_WITH_CURSES_PANEL, AX_WITH_CURSES_MENU and AX_WITH_CURSES_FORM.
+#
+# LICENSE
+#
+# Copyright (c) 2009 Mark Pulford <mark@kyne.com.au>
+# Copyright (c) 2009 Damian Pietras <daper@daper.net>
+# Copyright (c) 2012 Reuben Thomas <rrt@sc3d.org>
+# Copyright (c) 2011 John Zaitseff <J.Zaitseff@zap.org.au>
+#
+# This program is free software: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation, either version 3 of the License, or (at your
+# option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+# Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# As a special exception, the respective Autoconf Macro's copyright owner
+# gives unlimited permission to copy, distribute and modify the configure
+# scripts that are the output of Autoconf when processing the Macro. You
+# need not follow the terms of the GNU General Public License when using
+# or distributing such scripts, even though portions of the text of the
+# Macro appear in them. The GNU General Public License (GPL) does govern
+# all other use of the material that constitutes the Autoconf Macro.
+#
+# This special exception to the GPL applies to versions of the Autoconf
+# Macro released by the Autoconf Archive. When you make and distribute a
+# modified version of the Autoconf Macro, you may extend this special
+# exception to the GPL to apply to your modified version as well.
+
+#serial 13
+
+AC_DEFUN([AX_WITH_CURSES], [
+ AC_ARG_VAR([CURSES_LIB], [linker library for Curses, e.g. -lcurses])
+ AC_ARG_WITH([ncurses], [AS_HELP_STRING([--with-ncurses],
+ [force the use of Ncurses or NcursesW])],
+ [], [with_ncurses=check])
+ AC_ARG_WITH([ncursesw], [AS_HELP_STRING([--without-ncursesw],
+ [do not use NcursesW (wide character support)])],
+ [], [with_ncursesw=check])
+
+ ax_saved_LIBS=$LIBS
+ AS_IF([test "x$with_ncurses" = xyes || test "x$with_ncursesw" = xyes],
+ [ax_with_plaincurses=no], [ax_with_plaincurses=check])
+
+ ax_cv_curses_which=no
+
+ # Test for NcursesW
+
+ AS_IF([test "x$CURSES_LIB" = x && test "x$with_ncursesw" != xno], [
+ LIBS="$ax_saved_LIBS -lncursesw"
+
+ AC_CACHE_CHECK([for NcursesW wide-character library], [ax_cv_ncursesw], [
+ AC_LINK_IFELSE([AC_LANG_CALL([], [initscr])],
+ [ax_cv_ncursesw=yes], [ax_cv_ncursesw=no])
+ ])
+ AS_IF([test "x$ax_cv_ncursesw" = xno && test "x$with_ncursesw" = xyes], [
+ AC_MSG_ERROR([--with-ncursesw specified but could not find NcursesW library])
+ ])
+
+ AS_IF([test "x$ax_cv_ncursesw" = xyes], [
+ ax_cv_curses=yes
+ ax_cv_curses_which=ncursesw
+ CURSES_LIB="-lncursesw"
+ AC_DEFINE([HAVE_NCURSESW], [1], [Define to 1 if the NcursesW library is present])
+ AC_DEFINE([HAVE_CURSES], [1], [Define to 1 if a SysV or X/Open compatible Curses library is present])
+
+ AC_CACHE_CHECK([for working ncursesw/curses.h], [ax_cv_header_ncursesw_curses_h], [
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+ @%:@define _XOPEN_SOURCE_EXTENDED 1
+ @%:@include <ncursesw/curses.h>
+ ]], [[
+ chtype a = A_BOLD;
+ int b = KEY_LEFT;
+ chtype c = COLOR_PAIR(1) & A_COLOR;
+ attr_t d = WA_NORMAL;
+ cchar_t e;
+ wint_t f;
+ int g = getattrs(stdscr);
+ int h = getcurx(stdscr) + getmaxx(stdscr);
+ initscr();
+ init_pair(1, COLOR_WHITE, COLOR_RED);
+ wattr_set(stdscr, d, 0, NULL);
+ wget_wch(stdscr, &f);
+ ]])],
+ [ax_cv_header_ncursesw_curses_h=yes],
+ [ax_cv_header_ncursesw_curses_h=no])
+ ])
+ AS_IF([test "x$ax_cv_header_ncursesw_curses_h" = xyes], [
+ ax_cv_curses_enhanced=yes
+ ax_cv_curses_color=yes
+ ax_cv_curses_obsolete=yes
+ AC_DEFINE([HAVE_CURSES_ENHANCED], [1], [Define to 1 if library supports X/Open Enhanced functions])
+ AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)])
+ AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features])
+ AC_DEFINE([HAVE_NCURSESW_CURSES_H], [1], [Define to 1 if <ncursesw/curses.h> is present])
+ ])
+
+ AC_CACHE_CHECK([for working ncursesw.h], [ax_cv_header_ncursesw_h], [
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+ @%:@define _XOPEN_SOURCE_EXTENDED 1
+ @%:@include <ncursesw.h>
+ ]], [[
+ chtype a = A_BOLD;
+ int b = KEY_LEFT;
+ chtype c = COLOR_PAIR(1) & A_COLOR;
+ attr_t d = WA_NORMAL;
+ cchar_t e;
+ wint_t f;
+ int g = getattrs(stdscr);
+ int h = getcurx(stdscr) + getmaxx(stdscr);
+ initscr();
+ init_pair(1, COLOR_WHITE, COLOR_RED);
+ wattr_set(stdscr, d, 0, NULL);
+ wget_wch(stdscr, &f);
+ ]])],
+ [ax_cv_header_ncursesw_h=yes],
+ [ax_cv_header_ncursesw_h=no])
+ ])
+ AS_IF([test "x$ax_cv_header_ncursesw_h" = xyes], [
+ ax_cv_curses_enhanced=yes
+ ax_cv_curses_color=yes
+ ax_cv_curses_obsolete=yes
+ AC_DEFINE([HAVE_CURSES_ENHANCED], [1], [Define to 1 if library supports X/Open Enhanced functions])
+ AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)])
+ AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features])
+ AC_DEFINE([HAVE_NCURSESW_H], [1], [Define to 1 if <ncursesw.h> is present])
+ ])
+
+ AC_CACHE_CHECK([for working ncurses.h], [ax_cv_header_ncurses_h_with_ncursesw], [
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+ @%:@define _XOPEN_SOURCE_EXTENDED 1
+ @%:@include <ncurses.h>
+ ]], [[
+ chtype a = A_BOLD;
+ int b = KEY_LEFT;
+ chtype c = COLOR_PAIR(1) & A_COLOR;
+ attr_t d = WA_NORMAL;
+ cchar_t e;
+ wint_t f;
+ int g = getattrs(stdscr);
+ int h = getcurx(stdscr) + getmaxx(stdscr);
+ initscr();
+ init_pair(1, COLOR_WHITE, COLOR_RED);
+ wattr_set(stdscr, d, 0, NULL);
+ wget_wch(stdscr, &f);
+ ]])],
+ [ax_cv_header_ncurses_h_with_ncursesw=yes],
+ [ax_cv_header_ncurses_h_with_ncursesw=no])
+ ])
+ AS_IF([test "x$ax_cv_header_ncurses_h_with_ncursesw" = xyes], [
+ ax_cv_curses_enhanced=yes
+ ax_cv_curses_color=yes
+ ax_cv_curses_obsolete=yes
+ AC_DEFINE([HAVE_CURSES_ENHANCED], [1], [Define to 1 if library supports X/Open Enhanced functions])
+ AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)])
+ AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features])
+ AC_DEFINE([HAVE_NCURSES_H], [1], [Define to 1 if <ncurses.h> is present])
+ ])
+
+ AS_IF([test "x$ax_cv_header_ncursesw_curses_h" = xno && test "x$ax_cv_header_ncursesw_h" = xno && test "x$ax_cv_header_ncurses_h_with_ncursesw" = xno], [
+ AC_MSG_WARN([could not find a working ncursesw/curses.h, ncursesw.h or ncurses.h])
+ ])
+ ])
+ ])
+
+ # Test for Ncurses
+
+ AS_IF([test "x$CURSES_LIB" = x && test "x$with_ncurses" != xno && test "x$ax_cv_curses_which" = xno], [
+ LIBS="$ax_saved_LIBS -lncurses"
+
+ AC_CACHE_CHECK([for Ncurses library], [ax_cv_ncurses], [
+ AC_LINK_IFELSE([AC_LANG_CALL([], [initscr])],
+ [ax_cv_ncurses=yes], [ax_cv_ncurses=no])
+ ])
+ AS_IF([test "x$ax_cv_ncurses" = xno && test "x$with_ncurses" = xyes], [
+ AC_MSG_ERROR([--with-ncurses specified but could not find Ncurses library])
+ ])
+
+ AS_IF([test "x$ax_cv_ncurses" = xyes], [
+ ax_cv_curses=yes
+ ax_cv_curses_which=ncurses
+ CURSES_LIB="-lncurses"
+ AC_DEFINE([HAVE_NCURSES], [1], [Define to 1 if the Ncurses library is present])
+ AC_DEFINE([HAVE_CURSES], [1], [Define to 1 if a SysV or X/Open compatible Curses library is present])
+
+ AC_CACHE_CHECK([for working ncurses/curses.h], [ax_cv_header_ncurses_curses_h], [
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+ @%:@include <ncurses/curses.h>
+ ]], [[
+ chtype a = A_BOLD;
+ int b = KEY_LEFT;
+ chtype c = COLOR_PAIR(1) & A_COLOR;
+ int g = getattrs(stdscr);
+ int h = getcurx(stdscr) + getmaxx(stdscr);
+ initscr();
+ init_pair(1, COLOR_WHITE, COLOR_RED);
+ ]])],
+ [ax_cv_header_ncurses_curses_h=yes],
+ [ax_cv_header_ncurses_curses_h=no])
+ ])
+ AS_IF([test "x$ax_cv_header_ncurses_curses_h" = xyes], [
+ ax_cv_curses_color=yes
+ ax_cv_curses_obsolete=yes
+ AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)])
+ AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features])
+ AC_DEFINE([HAVE_NCURSES_CURSES_H], [1], [Define to 1 if <ncurses/curses.h> is present])
+ ])
+
+ AC_CACHE_CHECK([for working ncurses.h], [ax_cv_header_ncurses_h], [
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+ @%:@include <ncurses.h>
+ ]], [[
+ chtype a = A_BOLD;
+ int b = KEY_LEFT;
+ chtype c = COLOR_PAIR(1) & A_COLOR;
+ int g = getattrs(stdscr);
+ int h = getcurx(stdscr) + getmaxx(stdscr);
+ initscr();
+ init_pair(1, COLOR_WHITE, COLOR_RED);
+ ]])],
+ [ax_cv_header_ncurses_h=yes],
+ [ax_cv_header_ncurses_h=no])
+ ])
+ AS_IF([test "x$ax_cv_header_ncurses_h" = xyes], [
+ ax_cv_curses_color=yes
+ ax_cv_curses_obsolete=yes
+ AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)])
+ AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features])
+ AC_DEFINE([HAVE_NCURSES_H], [1], [Define to 1 if <ncurses.h> is present])
+ ])
+
+ AS_IF([test "x$ax_cv_header_ncurses_curses_h" = xno && test "x$ax_cv_header_ncurses_h" = xno], [
+ AC_MSG_WARN([could not find a working ncurses/curses.h or ncurses.h])
+ ])
+ ])
+ ])
+
+ # Test for plain Curses (or if CURSES_LIB was set by user)
+
+ AS_IF([test "x$with_plaincurses" != xno && test "x$ax_cv_curses_which" = xno], [
+ AS_IF([test "x$CURSES_LIB" != x], [
+ LIBS="$ax_saved_LIBS $CURSES_LIB"
+ ], [
+ LIBS="$ax_saved_LIBS -lcurses"
+ ])
+
+ AC_CACHE_CHECK([for Curses library], [ax_cv_plaincurses], [
+ AC_LINK_IFELSE([AC_LANG_CALL([], [initscr])],
+ [ax_cv_plaincurses=yes], [ax_cv_plaincurses=no])
+ ])
+
+ AS_IF([test "x$ax_cv_plaincurses" = xyes], [
+ ax_cv_curses=yes
+ ax_cv_curses_which=plaincurses
+ AS_IF([test "x$CURSES_LIB" = x], [
+ CURSES_LIB="-lcurses"
+ ])
+ AC_DEFINE([HAVE_CURSES], [1], [Define to 1 if a SysV or X/Open compatible Curses library is present])
+
+ # Check for base conformance (and header file)
+
+ AC_CACHE_CHECK([for working curses.h], [ax_cv_header_curses_h], [
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+ @%:@include <curses.h>
+ ]], [[
+ chtype a = A_BOLD;
+ int b = KEY_LEFT;
+ initscr();
+ ]])],
+ [ax_cv_header_curses_h=yes],
+ [ax_cv_header_curses_h=no])
+ ])
+ AS_IF([test "x$ax_cv_header_curses_h" = xyes], [
+ AC_DEFINE([HAVE_CURSES_H], [1], [Define to 1 if <curses.h> is present])
+
+ # Check for X/Open Enhanced conformance
+
+ AC_CACHE_CHECK([for X/Open Enhanced Curses conformance], [ax_cv_plaincurses_enhanced], [
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+ @%:@define _XOPEN_SOURCE_EXTENDED 1
+ @%:@include <curses.h>
+ @%:@ifndef _XOPEN_CURSES
+ @%:@error "this Curses library is not enhanced"
+ "this Curses library is not enhanced"
+ @%:@endif
+ ]], [[
+ chtype a = A_BOLD;
+ int b = KEY_LEFT;
+ chtype c = COLOR_PAIR(1) & A_COLOR;
+ attr_t d = WA_NORMAL;
+ cchar_t e;
+ wint_t f;
+ initscr();
+ init_pair(1, COLOR_WHITE, COLOR_RED);
+ wattr_set(stdscr, d, 0, NULL);
+ wget_wch(stdscr, &f);
+ ]])],
+ [ax_cv_plaincurses_enhanced=yes],
+ [ax_cv_plaincurses_enhanced=no])
+ ])
+ AS_IF([test "x$ax_cv_plaincurses_enhanced" = xyes], [
+ ax_cv_curses_enhanced=yes
+ ax_cv_curses_color=yes
+ AC_DEFINE([HAVE_CURSES_ENHANCED], [1], [Define to 1 if library supports X/Open Enhanced functions])
+ AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)])
+ ])
+
+ # Check for color functions
+
+ AC_CACHE_CHECK([for Curses color functions], [ax_cv_plaincurses_color], [
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+ @%:@define _XOPEN_SOURCE_EXTENDED 1
+ @%:@include <curses.h>
+ ]], [[
+ chtype a = A_BOLD;
+ int b = KEY_LEFT;
+ chtype c = COLOR_PAIR(1) & A_COLOR;
+ initscr();
+ init_pair(1, COLOR_WHITE, COLOR_RED);
+ ]])],
+ [ax_cv_plaincurses_color=yes],
+ [ax_cv_plaincurses_color=no])
+ ])
+ AS_IF([test "x$ax_cv_plaincurses_color" = xyes], [
+ ax_cv_curses_color=yes
+ AC_DEFINE([HAVE_CURSES_COLOR], [1], [Define to 1 if library supports color (enhanced functions)])
+ ])
+
+ # Check for obsolete functions
+
+ AC_CACHE_CHECK([for obsolete Curses functions], [ax_cv_plaincurses_obsolete], [
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[
+ @%:@include <curses.h>
+ ]], [[
+ chtype a = A_BOLD;
+ int b = KEY_LEFT;
+ int g = getattrs(stdscr);
+ int h = getcurx(stdscr) + getmaxx(stdscr);
+ initscr();
+ ]])],
+ [ax_cv_plaincurses_obsolete=yes],
+ [ax_cv_plaincurses_obsolete=no])
+ ])
+ AS_IF([test "x$ax_cv_plaincurses_obsolete" = xyes], [
+ ax_cv_curses_obsolete=yes
+ AC_DEFINE([HAVE_CURSES_OBSOLETE], [1], [Define to 1 if library supports certain obsolete features])
+ ])
+ ])
+
+ AS_IF([test "x$ax_cv_header_curses_h" = xno], [
+ AC_MSG_WARN([could not find a working curses.h])
+ ])
+ ])
+ ])
+
+ AS_IF([test "x$ax_cv_curses" != xyes], [ax_cv_curses=no])
+ AS_IF([test "x$ax_cv_curses_enhanced" != xyes], [ax_cv_curses_enhanced=no])
+ AS_IF([test "x$ax_cv_curses_color" != xyes], [ax_cv_curses_color=no])
+ AS_IF([test "x$ax_cv_curses_obsolete" != xyes], [ax_cv_curses_obsolete=no])
+
+ LIBS=$ax_saved_LIBS
+])dnl
diff --git a/m4/libcurl.m4 b/m4/libcurl.m4
new file mode 100644
index 0000000..02d00a7
--- /dev/null
+++ b/m4/libcurl.m4
@@ -0,0 +1,301 @@
+#***************************************************************************
+# _ _ ____ _
+# Project ___| | | | _ \| |
+# / __| | | | |_) | |
+# | (__| |_| | _ <| |___
+# \___|\___/|_| \_\_____|
+#
+# Copyright (C) 2006 - 2020, David Shaw <dshaw@jabberwocky.com>
+#
+# This software is licensed as described in the file COPYING, which
+# you should have received as part of this distribution. The terms
+# are also available at https://curl.se/docs/copyright.html.
+#
+# You may opt to use, copy, modify, merge, publish, distribute and/or sell
+# copies of the Software, and permit persons to whom the Software is
+# furnished to do so, under the terms of the COPYING file.
+#
+# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
+# KIND, either express or implied.
+#
+###########################################################################
+# LIBCURL_CHECK_CONFIG ([DEFAULT-ACTION], [MINIMUM-VERSION],
+# [ACTION-IF-YES], [ACTION-IF-NO])
+# ----------------------------------------------------------
+# David Shaw <dshaw@jabberwocky.com> May-09-2006
+#
+# Checks for libcurl. DEFAULT-ACTION is the string yes or no to
+# specify whether to default to --with-libcurl or --without-libcurl.
+# If not supplied, DEFAULT-ACTION is yes. MINIMUM-VERSION is the
+# minimum version of libcurl to accept. Pass the version as a regular
+# version number like 7.10.1. If not supplied, any version is
+# accepted. ACTION-IF-YES is a list of shell commands to run if
+# libcurl was successfully found and passed the various tests.
+# ACTION-IF-NO is a list of shell commands that are run otherwise.
+# Note that using --without-libcurl does run ACTION-IF-NO.
+#
+# This macro #defines HAVE_LIBCURL if a working libcurl setup is
+# found, and sets @LIBCURL@ and @LIBCURL_CPPFLAGS@ to the necessary
+# values. Other useful defines are LIBCURL_FEATURE_xxx where xxx are
+# the various features supported by libcurl, and LIBCURL_PROTOCOL_yyy
+# where yyy are the various protocols supported by libcurl. Both xxx
+# and yyy are capitalized. See the list of AH_TEMPLATEs at the top of
+# the macro for the complete list of possible defines. Shell
+# variables $libcurl_feature_xxx and $libcurl_protocol_yyy are also
+# defined to 'yes' for those features and protocols that were found.
+# Note that xxx and yyy keep the same capitalization as in the
+# curl-config list (e.g. it's "HTTP" and not "http").
+#
+# Users may override the detected values by doing something like:
+# LIBCURL="-lcurl" LIBCURL_CPPFLAGS="-I/usr/myinclude" ./configure
+#
+# For the sake of sanity, this macro assumes that any libcurl that is
+# found is after version 7.7.2, the first version that included the
+# curl-config script. Note that it is very important for people
+# packaging binary versions of libcurl to include this script!
+# Without curl-config, we can only guess what protocols are available,
+# or use curl_version_info to figure it out at runtime.
+
+AC_DEFUN([LIBCURL_CHECK_CONFIG],
+[
+ AH_TEMPLATE([LIBCURL_FEATURE_SSL],[Defined if libcurl supports SSL])
+ AH_TEMPLATE([LIBCURL_FEATURE_KRB4],[Defined if libcurl supports KRB4])
+ AH_TEMPLATE([LIBCURL_FEATURE_IPV6],[Defined if libcurl supports IPv6])
+ AH_TEMPLATE([LIBCURL_FEATURE_LIBZ],[Defined if libcurl supports libz])
+ AH_TEMPLATE([LIBCURL_FEATURE_ASYNCHDNS],[Defined if libcurl supports AsynchDNS])
+ AH_TEMPLATE([LIBCURL_FEATURE_IDN],[Defined if libcurl supports IDN])
+ AH_TEMPLATE([LIBCURL_FEATURE_SSPI],[Defined if libcurl supports SSPI])
+ AH_TEMPLATE([LIBCURL_FEATURE_NTLM],[Defined if libcurl supports NTLM])
+
+ AH_TEMPLATE([LIBCURL_PROTOCOL_HTTP],[Defined if libcurl supports HTTP])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_HTTPS],[Defined if libcurl supports HTTPS])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_FTP],[Defined if libcurl supports FTP])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_FTPS],[Defined if libcurl supports FTPS])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_FILE],[Defined if libcurl supports FILE])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_TELNET],[Defined if libcurl supports TELNET])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_LDAP],[Defined if libcurl supports LDAP])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_DICT],[Defined if libcurl supports DICT])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_TFTP],[Defined if libcurl supports TFTP])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_RTSP],[Defined if libcurl supports RTSP])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_POP3],[Defined if libcurl supports POP3])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_IMAP],[Defined if libcurl supports IMAP])
+ AH_TEMPLATE([LIBCURL_PROTOCOL_SMTP],[Defined if libcurl supports SMTP])
+
+ AC_ARG_WITH(libcurl,
+ AS_HELP_STRING([--with-libcurl=PREFIX],[look for the curl library in PREFIX/lib and headers in PREFIX/include]),
+ [_libcurl_with=$withval],[_libcurl_with=ifelse([$1],,[yes],[$1])])
+
+ if test "$_libcurl_with" != "no" ; then
+
+ AC_PROG_AWK
+
+ _libcurl_version_parse="eval $AWK '{split(\$NF,A,\".\"); X=256*256*A[[1]]+256*A[[2]]+A[[3]]; print X;}'"
+
+ _libcurl_try_link=yes
+
+ if test -d "$_libcurl_with" ; then
+ LIBCURL_CPPFLAGS="-I$withval/include"
+ _libcurl_ldflags="-L$withval/lib"
+ AC_PATH_PROG([_libcurl_config],[curl-config],[],
+ ["$withval/bin"])
+ else
+ AC_PATH_PROG([_libcurl_config],[curl-config],[],[$PATH])
+ fi
+
+ if test x$_libcurl_config != "x" ; then
+ AC_CACHE_CHECK([for the version of libcurl],
+ [libcurl_cv_lib_curl_version],
+ [libcurl_cv_lib_curl_version=`$_libcurl_config --version | $AWK '{print $[]2}'`])
+
+ _libcurl_version=`echo $libcurl_cv_lib_curl_version | $_libcurl_version_parse`
+ _libcurl_wanted=`echo ifelse([$2],,[0],[$2]) | $_libcurl_version_parse`
+
+ if test $_libcurl_wanted -gt 0 ; then
+ AC_CACHE_CHECK([for libcurl >= version $2],
+ [libcurl_cv_lib_version_ok],
+ [
+ if test $_libcurl_version -ge $_libcurl_wanted ; then
+ libcurl_cv_lib_version_ok=yes
+ else
+ libcurl_cv_lib_version_ok=no
+ fi
+ ])
+ fi
+
+ if test $_libcurl_wanted -eq 0 || test x$libcurl_cv_lib_version_ok = xyes ; then
+ if test x"$LIBCURL_CPPFLAGS" = "x" ; then
+ LIBCURL_CPPFLAGS=`$_libcurl_config --cflags`
+ fi
+ if test x"$LIBCURL" = "x" ; then
+ if $5; then
+ case "$host_os" in
+ darwin*)
+ LIBCURL=`$_libcurl_config --libs`
+ ;;
+ *)
+ LIBCURL=`$_libcurl_config --static-libs`
+ ;;
+ esac
+ else
+ LIBCURL=`$_libcurl_config --libs`
+ fi
+
+ # This is so silly, but Apple actually has a bug in their
+ # curl-config script. Fixed in Tiger, but there are still
+ # lots of Panther installs around.
+ case "${host}" in
+ powerpc-apple-darwin7*)
+ LIBCURL=`echo $LIBCURL | sed -e 's|-arch i386||g'`
+ ;;
+ esac
+ fi
+
+ dnl If we are on OS X and we haven't picked up libcurl static or
+ dnl otherwise, then let's just go ahead and use the one present on
+ dnl the system. Since this compiled binary will only run on OS X
+ dnl which almost always has cURL installed, it's OK to add the
+ dnl static dependency.
+ AS_IF([test "x${LIBCURL}" = "x"],
+ [AS_CASE(["$host_os"],
+ [darwin*],
+ [AS_IF([test "x$_libcurl_config" != "x"],
+ AS_VAR_SET(LIBCURL, $($_libcurl_config --libs)),
+ []
+ )],
+ []
+ )],
+ []
+ )
+
+ # All curl-config scripts support --feature
+ _libcurl_features=`$_libcurl_config --feature`
+
+ # Is it modern enough to have --protocols? (7.12.4)
+ if test $_libcurl_version -ge 461828 ; then
+ _libcurl_protocols=`$_libcurl_config --protocols`
+ fi
+ else
+ _libcurl_try_link=no
+ fi
+
+ unset _libcurl_wanted
+ fi
+
+ if test $_libcurl_try_link = yes ; then
+
+ # we didn't find curl-config, so let's see if the user-supplied
+ # link line (or failing that, "-lcurl") is enough.
+ # LIBCURL=${LIBCURL-"$_libcurl_ldflags -lcurl"}
+
+ AC_CACHE_CHECK([whether libcurl is usable],
+ [libcurl_cv_lib_curl_usable],
+ [
+ _libcurl_save_cppflags=$CPPFLAGS
+ CPPFLAGS="$LIBCURL_CPPFLAGS $CPPFLAGS"
+ _libcurl_save_libs=$LIBS
+ LIBS="$_libcurl_ldflags $LIBCURL $LIBS"
+
+ AC_LINK_IFELSE([AC_LANG_PROGRAM([[#include <curl/curl.h>]],[[
+/* Try and use a few common options to force a failure if we are
+ missing symbols or can't link. */
+int x;
+curl_easy_setopt(NULL,CURLOPT_URL,NULL);
+x=CURL_ERROR_SIZE;
+x=CURLOPT_WRITEFUNCTION;
+x=CURLOPT_WRITEDATA;
+x=CURLOPT_ERRORBUFFER;
+x=CURLOPT_STDERR;
+x=CURLOPT_VERBOSE;
+if (x) {;}
+]])],libcurl_cv_lib_curl_usable=yes,libcurl_cv_lib_curl_usable=no)
+
+ CPPFLAGS=$_libcurl_save_cppflags
+ LIBS=$_libcurl_save_libs
+ unset _libcurl_save_cppflags
+ unset _libcurl_save_libs
+ ])
+
+ if test $libcurl_cv_lib_curl_usable = yes ; then
+
+ # Does curl_free() exist in this version of libcurl?
+ # If not, fake it with free()
+
+ _libcurl_save_cppflags=$CPPFLAGS
+ CPPFLAGS="$CPPFLAGS $LIBCURL_CPPFLAGS"
+ _libcurl_save_libs=$LIBS
+ LIBS="$LIBS $LIBCURL"
+ LDFLAGS="$_libcurl_ldflags $LDFLAGS"
+
+ AC_CHECK_FUNC(curl_free,,
+ AC_DEFINE(curl_free,free,
+ [Define curl_free() as free() if our version of curl lacks curl_free.]))
+
+ CPPFLAGS=$_libcurl_save_cppflags
+ LIBS=$_libcurl_save_libs
+ unset _libcurl_save_cppflags
+ unset _libcurl_save_libs
+
+ AC_DEFINE(HAVE_LIBCURL,1,
+ [Define to 1 if you have a functional curl library.])
+ AC_SUBST(LIBCURL_CPPFLAGS)
+ AC_SUBST(LIBCURL)
+
+ for _libcurl_feature in $_libcurl_features ; do
+ AC_DEFINE_UNQUOTED(AS_TR_CPP(libcurl_feature_$_libcurl_feature),[1])
+ eval AS_TR_SH(libcurl_feature_$_libcurl_feature)=yes
+ done
+
+ if test "x$_libcurl_protocols" = "x" ; then
+
+ # We don't have --protocols, so just assume that all
+ # protocols are available
+ _libcurl_protocols="HTTP FTP FILE TELNET LDAP DICT TFTP"
+
+ if test x$libcurl_feature_SSL = xyes ; then
+ _libcurl_protocols="$_libcurl_protocols HTTPS"
+
+ # FTPS wasn't standards-compliant until version
+ # 7.11.0 (0x070b00 == 461568)
+ if test $_libcurl_version -ge 461568; then
+ _libcurl_protocols="$_libcurl_protocols FTPS"
+ fi
+ fi
+
+ # RTSP, IMAP, POP3 and SMTP were added in
+ # 7.20.0 (0x071400 == 463872)
+ if test $_libcurl_version -ge 463872; then
+ _libcurl_protocols="$_libcurl_protocols RTSP IMAP POP3 SMTP"
+ fi
+ fi
+
+ for _libcurl_protocol in $_libcurl_protocols ; do
+ AC_DEFINE_UNQUOTED(AS_TR_CPP(libcurl_protocol_$_libcurl_protocol),[1])
+ eval AS_TR_SH(libcurl_protocol_$_libcurl_protocol)=yes
+ done
+ else
+ unset LIBCURL
+ unset LIBCURL_CPPFLAGS
+ fi
+ fi
+
+ unset _libcurl_try_link
+ unset _libcurl_version_parse
+ unset _libcurl_config
+ unset _libcurl_feature
+ unset _libcurl_features
+ unset _libcurl_protocol
+ unset _libcurl_protocols
+ unset _libcurl_version
+ unset _libcurl_ldflags
+ fi
+
+ if test x$_libcurl_with = xno || test x$libcurl_cv_lib_curl_usable != xyes ; then
+ # This is the IF-NO path
+ ifelse([$4],,:,[$4])
+ else
+ # This is the IF-YES path
+ ifelse([$3],,:,[$3])
+ fi
+
+ unset _libcurl_with
+])dnl
diff --git a/m4/lnav_common.m4 b/m4/lnav_common.m4
new file mode 100644
index 0000000..1619afe
--- /dev/null
+++ b/m4/lnav_common.m4
@@ -0,0 +1,22 @@
+AC_DEFUN([LNAV_ADDTO], [
+ if test "x$$1" = "x"; then
+ test "x$verbose" = "xyes" && echo " setting $1 to \"$2\""
+ $1="$2"
+ else
+ ats_addto_bugger="$2"
+ for i in $ats_addto_bugger; do
+ ats_addto_duplicate="0"
+ for j in $$1; do
+ if test "x$i" = "x$j"; then
+ ats_addto_duplicate="1"
+ break
+ fi
+ done
+ if test $ats_addto_duplicate = "0"; then
+ test "x$verbose" = "xyes" && echo " adding \"$i\" to $1"
+ $1="$$1 $i"
+ fi
+ done
+ fi
+])dnl
+
diff --git a/m4/lnav_with_jemalloc.m4 b/m4/lnav_with_jemalloc.m4
new file mode 100644
index 0000000..99103eb
--- /dev/null
+++ b/m4/lnav_with_jemalloc.m4
@@ -0,0 +1,81 @@
+dnl -------------------------------------------------------- -*- autoconf -*-
+dnl Licensed to the Apache Software Foundation (ASF) under one or more
+dnl contributor license agreements. See the NOTICE file distributed with
+dnl this work for additional information regarding copyright ownership.
+dnl The ASF licenses this file to You under the Apache License, Version 2.0
+dnl (the "License"); you may not use this file except in compliance with
+dnl the License. You may obtain a copy of the License at
+dnl
+dnl http://www.apache.org/licenses/LICENSE-2.0
+dnl
+dnl Unless required by applicable law or agreed to in writing, software
+dnl distributed under the License is distributed on an "AS IS" BASIS,
+dnl WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+dnl See the License for the specific language governing permissions and
+dnl limitations under the License.
+
+dnl
+dnl lnav_with_jemalloc.m4: lnav's jemalloc autoconf macros
+dnl
+dnl
+AC_DEFUN([LNAV_WITH_JEMALLOC], [
+enable_jemalloc=no
+AC_ARG_WITH([jemalloc],
+ [AS_HELP_STRING([--with-jemalloc=DIR],[use a specific jemalloc library])],
+[
+ if test "$withval" != "no"; then
+ enable_jemalloc=yes
+ modify_env_variables=no
+ case "$withval" in
+ yes)
+ AC_MSG_NOTICE([Checking for jemalloc libs])
+ ;;
+ *)
+ jemalloc_include="$withval/include"
+ jemalloc_ldflags="$withval/lib"
+ modify_env_variables="yes"
+ AC_MSG_NOTICE([Checking for jemalloc libs in $withval])
+ ;;
+ esac
+ fi
+])
+
+jemalloch=0
+if test "$enable_jemalloc" != "no"; then
+ saved_ldflags=$LDFLAGS
+ saved_cppflags=$CPPFLAGS
+ saved_libtool_link_flags=$LIBTOOL_LINK_FLAGS
+ jemalloc_have_headers=0
+ jemalloc_have_libs=0
+
+ if test "$modify_env_variables" != "no"; then
+ LNAV_ADDTO(CPPFLAGS, [-I${jemalloc_include}])
+ LNAV_ADDTO(LDFLAGS, [-L${jemalloc_ldflags}])
+ LNAV_ADDTO(LIBTOOL_LINK_FLAGS, [-R${jemalloc_ldflags}])
+ fi
+ # On Darwin, jemalloc symbols are prefixed with je_. Search for that first,
+ # then fall back to unadorned symbols.
+ AC_SEARCH_LIBS([je_malloc_stats_print], [jemalloc], [jemalloc_have_libs=1],
+ [AC_SEARCH_LIBS([malloc_stats_print], [jemalloc], [jemalloc_have_libs=1])]
+ )
+ if test "$jemalloc_have_libs" != "0"; then
+ AC_MSG_NOTICE([Checking for jemalloc includes])
+ AC_CHECK_HEADERS(jemalloc/jemalloc.h, [jemalloc_have_headers=1])
+ if test "$jemalloc_have_headers" != "0"; then
+ jemalloch=1
+ LNAV_ADDTO(LIBS, [-ljemalloc])
+ else
+ AC_MSG_WARN([jemalloc headers not found])
+ fi
+ else
+ AC_MSG_WARN([jemalloc libs not found])
+ fi
+ if test "$jemalloc_have_libs" = "0"; then
+ CPPFLAGS=$saved_cppflags
+ LDFLAGS=$saved_ldflags
+ LIBTOOL_LINK_FLAGS=$saved_libtool_link_flags
+ AC_MSG_WARN([jemalloc not found])
+ fi
+fi
+AC_SUBST(jemalloch)
+])dnl
diff --git a/m4/lnav_with_libarchive.m4 b/m4/lnav_with_libarchive.m4
new file mode 100644
index 0000000..a6fa4fd
--- /dev/null
+++ b/m4/lnav_with_libarchive.m4
@@ -0,0 +1,77 @@
+dnl
+dnl Copyright (c) 2020, Timothy Stack
+dnl
+dnl All rights reserved.
+dnl
+dnl Redistribution and use in source and binary forms, with or without
+dnl modification, are permitted provided that the following conditions are met:
+dnl
+dnl dnl Redistributions of source code must retain the above copyright notice, this
+dnl list of conditions and the following disclaimer.
+dnl dnl Redistributions in binary form must reproduce the above copyright notice,
+dnl this list of conditions and the following disclaimer in the documentation
+dnl and/or other materials provided with the distribution.
+dnl dnl Neither the name of Timothy Stack nor the names of its contributors
+dnl may be used to endorse or promote products derived from this software
+dnl without specific prior written permission.
+dnl
+dnl THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+dnl EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+dnl WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+dnl DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+dnl DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+dnl (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+dnl LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+dnl ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+dnl SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+dnl
+dnl @file lnav_with_libarchive.m4
+dnl
+AC_DEFUN([AX_PATH_LIB_ARCHIVE],[dnl
+AC_MSG_CHECKING([lib archive])
+AC_ARG_WITH(libarchive,
+[ --with-libarchive[[=prefix]]],,
+ with_libarchive="yes")
+if test ".$with_libarchive" = ".no" ; then
+ AC_MSG_RESULT([disabled])
+ m4_ifval($2,$2)
+else
+ AC_MSG_RESULT([(testing)])
+ AC_CHECK_LIB(archive, archive_read_new)
+ AC_CHECK_HEADERS(archive.h)
+ if test "$ac_cv_lib_archive_archive_read_new" = "yes" && \
+ test "x$ac_cv_header_archive_h" = xyes; then
+ LIBARCHIVE_LIBS="-larchive"
+ AC_MSG_CHECKING([lib archive])
+ AC_MSG_RESULT([$LIBARCHIVE_LIBS])
+ m4_ifval($1,$1)
+ else
+ unset ac_cv_header_archive_h
+ OLDLDFLAGS="$LDFLAGS" ; LDFLAGS="$LDFLAGS -L$with_libarchive/lib"
+ OLDCPPFLAGS="$CPPFLAGS" ; CPPFLAGS="$CPPFLAGS -I$with_libarchive/include"
+ AC_CHECK_LIB(archive, archive_read_new)
+ AC_CHECK_HEADERS(archive.h)
+ CPPFLAGS="$OLDCPPFLAGS"
+ LDFLAGS="$OLDLDFLAGS"
+ if test "$ac_cv_lib_archive_archive_read_new" = "yes" && \
+ test "x$ac_cv_header_archive_h" = xyes; then
+ AC_MSG_RESULT(.setting LIBARCHIVE_LIBS -L$with_libarchive/lib -larchive)
+ LIBARCHIVE_LDFLAGS="-L$with_libarchive/lib"
+ LIBARCHIVE_LIBS="-larchive"
+ test -d "$with_libarchive/include" && LIBARCHIVE_CFLAGS="-I$with_libarchive/include"
+ AC_MSG_CHECKING([lib archive])
+ AC_MSG_RESULT([$LIBARCHIVE_LIBS])
+ m4_ifval($1,$1)
+ else
+ AC_MSG_CHECKING([lib archive])
+ AC_MSG_RESULT([[no]])
+ m4_ifval($2,$2)
+ fi
+ fi
+fi
+AC_SUBST([LIBARCHIVE_LIBS])
+AC_SUBST([LIBARCHIVE_LDFLAGS])
+AC_SUBST([LIBARCHIVE_CFLAGS])
+])
+
diff --git a/m4/lnav_with_readline.m4 b/m4/lnav_with_readline.m4
new file mode 100644
index 0000000..217391c
--- /dev/null
+++ b/m4/lnav_with_readline.m4
@@ -0,0 +1,95 @@
+dnl
+dnl Copyright (c) 2007-2015, Timothy Stack
+dnl Copyright (c) 2015, Suresh Sundriyal
+dnl
+dnl All rights reserved.
+dnl
+dnl Redistribution and use in source and binary forms, with or without
+dnl modification, are permitted provided that the following conditions are met:
+dnl
+dnl dnl Redistributions of source code must retain the above copyright notice, this
+dnl list of conditions and the following disclaimer.
+dnl dnl Redistributions in binary form must reproduce the above copyright notice,
+dnl this list of conditions and the following disclaimer in the documentation
+dnl and/or other materials provided with the distribution.
+dnl dnl Neither the name of Timothy Stack nor the names of its contributors
+dnl may be used to endorse or promote products derived from this software
+dnl without specific prior written permission.
+dnl
+dnl THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+dnl EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+dnl WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+dnl DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+dnl DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+dnl (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+dnl LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+dnl ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+dnl SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+dnl
+dnl @file lnav_with_readline.m4
+dnl
+AC_DEFUN([AX_PATH_LIB_READLINE],
+ [dnl
+ AC_REQUIRE([AX_WITH_CURSES])
+ AC_MSG_CHECKING([lib readline])
+ AC_ARG_WITH([readline],
+ AS_HELP_STRING([--with-readline@<:@=prefix@:>@],dnl
+ [compile xmlreadline part (via libreadline check)]),
+ [],
+ [with_readline="yes"]
+ )dnl
+
+ AS_VAR_SET(saved_CFLAGS, $CFLAGS)
+ AS_VAR_SET(saved_CPPFLAGS, $CPPFLAGS)
+ AS_VAR_SET(saved_LDFLAGS, $LDFLAGS)
+ AS_VAR_SET(saved_LIBS, $LIBS)
+ AS_CASE(["$with_readline"],
+ [no],
+ AC_MSG_ERROR([readline required to build]),
+ [yes],
+ [],
+ [dnl
+ AS_VAR_SET([READLINE_CFLAGS], ["-I$with_readline/include"])
+ AS_VAR_SET([READLINE_LDFLAGS], ["-L$with_readline/lib"])
+ LNAV_ADDTO(CFLAGS, [-I$with_readline/include])
+ LNAV_ADDTO(CPPFLAGS, [-I$with_readline/include])
+ dnl We want the provided path to be the first in the search order.
+ LDFLAGS="-L$with_readline/lib $LDFLAGS"
+ ]dnl
+ )
+
+ AC_SEARCH_LIBS([readline], [readline],
+ [AS_VAR_SET([READLINE_LIBS], ["-lreadline"])],
+ [AC_MSG_ERROR([libreadline library not found])],
+ [$CURSES_LIB]dnl
+ )dnl
+
+ dnl Ensure that the readline library has the required symbols.
+ dnl i.e. We haven't picked up editline.
+ AC_SEARCH_LIBS([history_set_history_state], [readline],
+ [],
+ AC_MSG_ERROR([libreadline does not have the required symbols. editline possibly masquerading as readline.]),
+ [$CURSES_LIB]dnl
+ )
+
+ AC_CHECK_HEADERS([readline.h readline/readline.h],
+ [dnl
+ AS_VAR_SET([HAVE_READLINE_HEADERS], [1])
+ break
+ ]dnl
+ )
+
+ AS_VAR_SET_IF([HAVE_READLINE_HEADERS], [],
+ [AC_MSG_ERROR([readline headers not found])]
+ )
+ AS_VAR_SET(CFLAGS, $saved_CFLAGS)
+ AS_VAR_SET(CPPFLAGS, $saved_CPPFLAGS)
+ AS_VAR_SET(LDFLAGS, $saved_LDFLAGS)
+ AS_VAR_SET(LIBS, $saved_LIBS)
+
+ AC_SUBST([READLINE_LIBS])
+ AC_SUBST([READLINE_CFLAGS])
+ AC_SUBST([READLINE_LDFLAGS])
+ ]dnl
+)dnl
diff --git a/m4/lnav_with_sqlite3.m4 b/m4/lnav_with_sqlite3.m4
new file mode 100644
index 0000000..6eaa9fa
--- /dev/null
+++ b/m4/lnav_with_sqlite3.m4
@@ -0,0 +1,127 @@
+dnl
+dnl @Modified from: ax_sqlite3.m4
+dnl @author Mateusz Loskot <mateusz@loskot.net>
+dnl @version $Date: 2006/08/30 14:28:55 $
+dnl @license AllPermissive
+dnl
+dnl Copyright (c) 2015, Suresh Sundriyal
+dnl
+dnl @file lnav_with_sqlite3.m4
+dnl
+AC_DEFUN([LNAV_WITH_SQLITE3],
+[dnl
+ AC_ARG_WITH([sqlite3],
+ AS_HELP_STRING([--with-sqlite3=@<:@prefix@:>@],[compile with sqlite3]),
+ [],
+ [with_sqlite3="yes"]
+ )
+
+ AS_VAR_SET(saved_CFLAGS, $CFLAGS)
+ AS_VAR_SET(saved_CPPFLAGS, $CPPFLAGS)
+ AS_VAR_SET(saved_LDFLAGS, $LDFLAGS)
+ AS_VAR_SET(saved_LIBS, $LIBS)
+
+ AS_CASE(["$with_sqlite3"],
+ [no],
+ AC_MSG_ERROR([sqlite3 required to build]),
+ [yes],
+ [],
+ [dnl
+ LNAV_ADDTO(CFLAGS, [-I$with_sqlite3/include])
+ LNAV_ADDTO(CPPFLAGS, [-I$with_sqlite3/include])
+ AS_VAR_SET([SQLITE3_LDFLAGS], ["-L$with_sqlite3/lib"])
+ AS_VAR_SET([SQLITE3_CFLAGS], ["-I$with_sqlite3/include"])
+ LDFLAGS="-L$with_sqlite3/lib $LDFLAGS"
+ ]
+ )
+
+ AC_SEARCH_LIBS([sqlite3_open], [sqlite3],
+ AS_VAR_SET([SQLITE3_LIBS], ["-lsqlite3"]),
+ AC_MSG_ERROR([sqlite3 library not found]),
+ [-pthread]dnl
+ )
+
+ AC_CHECK_HEADERS([sqlite3.h], [],
+ AC_MSG_ERROR([sqlite3 headers not found])
+ )
+
+ sqlite3_version_req=ifelse([$1], [], [3.0.0], [$1])
+ sqlite3_version_req_shorten=`expr $sqlite3_version_req : '\([[0-9]]*\.[[0-9]]*\)'`
+ sqlite3_version_req_major=`expr $sqlite3_version_req : '\([[0-9]]*\)'`
+ sqlite3_version_req_minor=`expr $sqlite3_version_req : '[[0-9]]*\.\([[0-9]]*\)'`
+ sqlite3_version_req_micro=`expr $sqlite3_version_req : '[[0-9]]*\.[[0-9]]*\.\([[0-9]]*\)'`
+ if test "x$sqlite3_version_req_micro" = "x" ; then
+ sqlite3_version_req_micro="0"
+ fi
+
+ sqlite3_version_req_number=`expr $sqlite3_version_req_major \* 1000000 \
+ \+ $sqlite3_version_req_minor \* 1000 \
+ \+ $sqlite3_version_req_micro`
+
+ AC_MSG_CHECKING([for SQLite3 library >= $sqlite3_version_req])
+ AC_LANG_PUSH(C++)
+ AC_COMPILE_IFELSE(
+ [
+ AC_LANG_PROGRAM([[@%:@include <sqlite3.h>]],
+ [[
+ #if (SQLITE_VERSION_NUMBER >= $sqlite3_version_req_number)
+ // Everything is okay
+ #else
+ # error SQLite version is too old
+ #endif
+ ]]
+ )
+ ],
+ [
+ AC_MSG_RESULT([yes])
+ ],
+ [
+ AC_MSG_RESULT([not found])
+ AC_MSG_ERROR([SQLite3 version >= $sqlite3_version_req is required])
+ ]
+ )
+ AC_LANG_POP([C++])
+
+ AC_CHECK_FUNC(sqlite3_stmt_readonly,
+ AC_DEFINE([HAVE_SQLITE3_STMT_READONLY], [1],
+ [Have the sqlite3_stmt_readonly function]
+ )
+ )
+
+ AC_CHECK_FUNC(sqlite3_value_subtype,
+ HAVE_SQLITE3_VALUE_SUBTYPE=1
+ AC_DEFINE([HAVE_SQLITE3_VALUE_SUBTYPE], [1],
+ [Have the sqlite3_value_subtype function]
+ )
+ )
+
+ AC_SUBST(HAVE_SQLITE3_VALUE_SUBTYPE)
+
+ AC_CHECK_FUNC(sqlite3_error_offset,
+ HAVE_SQLITE3_ERROR_OFFSET=1
+ AC_DEFINE([HAVE_SQLITE3_ERROR_OFFSET], [1],
+ [Have the sqlite3_error_offset function]
+ )
+ )
+
+ AC_SUBST(HAVE_SQLITE3_ERROR_OFFSET)
+
+ AC_CHECK_FUNC(sqlite3_drop_modules,
+ HAVE_SQLITE3_DROP_MODULES=1
+ AC_DEFINE([HAVE_SQLITE3_DROP_MODULES], [1],
+ [Have the sqlite3_drop_modules function]
+ )
+ )
+
+ AC_SUBST(HAVE_SQLITE3_DROP_MODULES)
+
+ AS_VAR_SET(CFLAGS, $saved_CFLAGS)
+ AS_VAR_SET(CPPFLAGS, $saved_CPPFLAGS)
+ AS_VAR_SET(LDFLAGS, $saved_LDFLAGS)
+ AS_VAR_SET(LIBS, $saved_LIBS)
+
+ AC_SUBST(SQLITE3_CFLAGS)
+ AC_SUBST(SQLITE3_LDFLAGS)
+ AC_SUBST(SQLITE3_LIBS)
+]
+)
diff --git a/m4/lnav_with_yajl.m4 b/m4/lnav_with_yajl.m4
new file mode 100644
index 0000000..0f9d942
--- /dev/null
+++ b/m4/lnav_with_yajl.m4
@@ -0,0 +1,87 @@
+dnl
+dnl Copyright (c) 2015, Suresh Sundriyal
+dnl
+dnl All rights reserved.
+dnl
+dnl Redistribution and use in source and binary forms, with or without
+dnl modification, are permitted provided that the following conditions are met:
+dnl
+dnl dnl Redistributions of source code must retain the above copyright notice, this
+dnl list of conditions and the following disclaimer.
+dnl dnl Redistributions in binary form must reproduce the above copyright notice,
+dnl this list of conditions and the following disclaimer in the documentation
+dnl and/or other materials provided with the distribution.
+dnl dnl Neither the name of Timothy Stack nor the names of its contributors
+dnl may be used to endorse or promote products derived from this software
+dnl without specific prior written permission.
+dnl
+dnl THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+dnl EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+dnl WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+dnl DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+dnl DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+dnl (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+dnl LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+dnl ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+dnl (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+dnl SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+dnl
+dnl @file lnav_with_yajl.m4
+AC_DEFUN([LNAV_WITH_LOCAL_YAJL],
+ [
+ AC_ARG_WITH([yajl],
+ AS_HELP_STRING([--with-yajl=DIR],[use a local installed version of yajl]),
+ [
+ AS_IF([test "$withval" != "no"],
+ [
+ AS_CASE(["$withval"],
+ [yes],
+ [AC_MSG_NOTICE([Checking for yajl libs])],
+ [
+ AS_VAR_SET([yajl_saved_ldflags], ["$LDFLAGS"])
+ AS_VAR_SET([yajl_saved_cppflags], ["$CPPFLAGS"])
+ AS_VAR_SET([yajl_saved_libtool_link_flags],
+ ["$LIBTOOL_LIBK_FLAGS"]
+ )
+
+ LNAV_ADDTO(CPPFLAGS, [-I$withval/include])
+ LNAV_ADDTO(LDFLAGS, [-I$withval/lib])
+ LNAV_ADDTO(LIBTOOL_LINK_FLAGS, [-R$withval/lib])
+
+ AC_MSG_NOTICE([Checking for yajl libs in $withval])
+ ]
+ )
+
+ AC_SEARCH_LIBS([yajl_gen_alloc],
+ [yajl],
+ [
+ AC_MSG_NOTICE([Checking for yajl headers])
+ AC_CHECK_HEADERS([yajl/yajl_parse.h],
+ AS_VAR_SET([HAVE_LOCAL_YAJL], [1]),
+ AC_MSG_WARN([yajl not found on the local system])
+ )
+ ]
+ )
+ ]
+ )
+ ]
+ )
+
+ AS_VAR_SET_IF([HAVE_LOCAL_YAJL],
+ [],
+ [
+ AC_MSG_NOTICE([compiling with the included version of yajl])
+ AS_VAR_SET([HAVE_LOCAL_YAJL], [0])
+ AS_VAR_SET_IF([yajl_saved_ldflags],
+ [
+ AS_VAR_SET([CPPFLAGS], ["$yajl_saved_cppflags"])
+ AS_VAR_SET([LDFLAGS], ["$yajl_saved_ldflags"])
+ AS_VAR_SET([LIBTOOL_LIBK_FLAGS], ["$yajl_saved_libtool_link_flags"])
+ ]
+ )
+ ]
+ )
+
+ AC_SUBST(HAVE_LOCAL_YAJL)
+ ]
+)dnl
diff --git a/release/Makefile b/release/Makefile
new file mode 100644
index 0000000..f141740
--- /dev/null
+++ b/release/Makefile
@@ -0,0 +1,105 @@
+
+VERSION=0.11.2
+
+VERSION_TAG=v$(VERSION)
+
+SRC_VERSION=master
+
+outbox:
+ mkdir -p $@
+
+clean-outbox: outbox
+ rm -f outbox/*
+
+PACKAGE_URLS = \
+ https://www.libarchive.org/downloads/libarchive-3.6.2.tar.gz \
+ https://ftp.gnu.org/gnu/make/make-4.2.1.tar.gz \
+ ftp://ftp.gnu.org/gnu/ncurses/ncurses-6.4.tar.gz \
+ https://github.com/PCRE2Project/pcre2/releases/download/pcre2-10.42/pcre2-10.42.tar.gz \
+ https://ftp.gnu.org/gnu/readline/readline-6.3.tar.gz \
+ https://zlib.net/zlib-1.2.13.tar.gz \
+ https://sourceware.org/pub/bzip2/bzip2-1.0.8.tar.gz \
+ https://www.sqlite.org/2023/sqlite-autoconf-3420000.tar.gz \
+ https://www.openssl.org/source/openssl-1.0.2n.tar.gz \
+ https://www.libssh2.org/download/libssh2-1.11.0.tar.gz \
+ https://curl.se/download/curl-8.1.2.tar.gz \
+ https://tukaani.org/xz/xz-5.4.3.tar.gz
+
+.PHONY: linux freebsd pkger download-pkgs musl
+
+%-vm: %
+ cd vagrant-static && vagrant up $<
+
+download-pkgs:
+ mkdir -p vagrant-static/pkgs && cd vagrant-static/pkgs && \
+ for pkg in $(PACKAGE_URLS); do \
+ if ! wget -N $${pkg}; then \
+ exit 1; \
+ fi \
+ done
+
+%-build: % %-vm
+ cd vagrant-static && vagrant ssh $< -c "/vagrant/build.sh ${SRC_VERSION}"
+
+%-package: % clean-outbox %-build
+ mkdir -p $<-pkg/lnav-${VERSION}
+ cp ../README ../NEWS.md $<-pkg/lnav-${VERSION}
+ mv vagrant-static/lnav $<-pkg/lnav-${VERSION}
+ cd $<-pkg && zip -r ../outbox/lnav-${VERSION}-x86_64-linux-$<.zip lnav-${VERSION}
+ rm -rf $<-pkg
+
+linux-ospkg: pkger-vm linux-package
+ rm -f vagrant-static/lnav\*.deb vagrant-static/lnav\*.rpm
+ cd vagrant-static && vagrant ssh pkger -c "/vagrant/build-pkg.sh ${VERSION}"
+ mv vagrant-static/lnav*.deb vagrant-static/lnav*.rpm outbox/
+
+osx-build:
+ rm -rf osx-build-dir
+ mkdir -p osx-build-dir
+ cd ../ && ./autogen.sh
+ cd osx-build-dir && \
+ ../../configure --enable-static \
+ --with-readline=/usr/local/opt/readline \
+ --with-sqlite3=/usr/local/opt/sqlite \
+ --with-libarchive=/usr/local/opt/libarchive \
+ "CXXFLAGS=-I/usr/local/opt/ncurses/include -g1 -O2" \
+ "CFLAGS=-I/usr/local/opt/ncurses/include -g1 -O2" \
+ "LDFLAGS=-L/usr/local/opt/ncurses/lib -L/usr/local/opt/xz/lib" \
+ "LIBS=-llzma -lzstd -lbrotlidec-static -liconv -llz4" \
+ && make -j8 && make dist -j8 && make dist-bzip2
+
+osx-package: clean-outbox osx-build
+ mkdir -p osx-pkg/lnav-${VERSION}
+ git pull --rebase
+ cp ../README ../NEWS.md osx-pkg/lnav-${VERSION}
+ cp osx-build-dir/src/lnav osx-pkg/lnav-${VERSION}
+ cp osx-build-dir/lnav-${VERSION}.tar.gz outbox/
+ cp osx-build-dir/lnav-${VERSION}.tar.bz2 outbox/
+ cd osx-pkg && zip -r ../outbox/lnav-${VERSION}-x86_64-macos.zip lnav-${VERSION}
+ rm -rf osx-pkg
+
+release-NEWS.md: ../NEWS.md
+ sed -n "/^## lnav v${VERSION}/,/^## /p" $< | sed '$$d' > $@
+
+release-tag: release-NEWS.md
+ gh release create ${VERSION_TAG} \
+ -d \
+ -t "lnav v${VERSION}" \
+ --notes-file release-NEWS.md
+
+release: osx-package musl-package release-NEWS.md
+ gh release edit ${VERSION_TAG} \
+ -t "lnav v${VERSION}" \
+ --notes-file release-NEWS.md
+ gh release upload ${VERSION_TAG} outbox/*
+
+push:
+ env LANG=UTF-8 package_cloud push tstack/lnav/ubuntu/lucid outbox/lnav*.deb
+ env LANG=UTF-8 package_cloud push tstack/lnav/el/5 outbox/lnav-0.11.2-1.x86_64.rpm
+
+clean:
+ cd vagrant-static && vagrant destroy -f
+ rm -rf vagrant-static/pkgs
+
+lnav.spec: lnav.spec.in makespec.sh
+ ./makespec.sh < lnav.spec.in > $@
diff --git a/release/README.md b/release/README.md
new file mode 100644
index 0000000..1832b2a
--- /dev/null
+++ b/release/README.md
@@ -0,0 +1,4 @@
+# Release
+
+This directory contains the [Makefile](Makefile) and scripts used to build the
+binaries for a release.
diff --git a/release/lnav-screenshot.terminal b/release/lnav-screenshot.terminal
new file mode 100644
index 0000000..c22cbf4
--- /dev/null
+++ b/release/lnav-screenshot.terminal
@@ -0,0 +1,453 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>WindowList</key>
+ <array>
+ <dict>
+ <key>Origin</key>
+ <string>{2361, 836}</string>
+ <key>ShowsTabBar</key>
+ <false/>
+ <key>ShowsTabBarInFullScreen</key>
+ <false/>
+ <key>Window Settings</key>
+ <array>
+ <dict>
+ <key>Tab Column Count</key>
+ <integer>114</integer>
+ <key>Tab Row Count</key>
+ <integer>35</integer>
+ <key>Tab Scrollback Restorable</key>
+ <true/>
+ <key>Tab Session Class ID</key>
+ <string>E5A9F716-7408-4C2C-8F6D-F626F389C346</string>
+ <key>Tab Settings</key>
+ <dict>
+ <key>ANSIBlueColor</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGFRZYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKMHCA9VJG51
+ bGzTCQoLDA0OVU5TUkdCXE5TQ29s
+ b3JTcGFjZVYkY2xhc3NPECowLjAw
+ MDUwNTEyMTkyOTggMC40NTMwNDQz
+ ODc3IDAuODI4MjczNjA3MwAQAYAC
+ 0hAREhNaJGNsYXNzbmFtZVgkY2xh
+ c3Nlc1dOU0NvbG9yohIUWE5TT2Jq
+ ZWN0XxAPTlNLZXllZEFyY2hpdmVy
+ 0RcYVHJvb3SAAQgRGiMtMjc7QUhO
+ W2KPkZOYo6y0t8DS1doAAAAAAAAB
+ AQAAAAAAAAAZAAAAAAAAAAAAAAAA
+ AAAA3A==
+ </data>
+ <key>ANSIBrightBlueColor</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGFRZYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKMHCA9VJG51
+ bGzTCQoLDA0OVU5TUkdCXE5TQ29s
+ b3JTcGFjZVYkY2xhc3NPEBwwIDAu
+ NTk4MzQyOTQ5MSAwLjk5ODE4ODk3
+ MjUAEAGAAtIQERITWiRjbGFzc25h
+ bWVYJGNsYXNzZXNXTlNDb2xvcqIS
+ FFhOU09iamVjdF8QD05TS2V5ZWRB
+ cmNoaXZlctEXGFRyb290gAEIERoj
+ LTI3O0FITltigYOFipWepqmyxMfM
+ AAAAAAAAAQEAAAAAAAAAGQAAAAAA
+ AAAAAAAAAAAAAM4=
+ </data>
+ <key>ANSIRedColor</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGFRZYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKMHCA9VJG51
+ bGzTCQoLDA0OVU5TUkdCXE5TQ29s
+ b3JTcGFjZVYkY2xhc3NPECkwLjc0
+ MzAzNjY4NDggMC4wMzYwNjI5Mjkw
+ OSAwLjA5ODYwMDQ3MDg0ABABgALS
+ EBESE1okY2xhc3NuYW1lWCRjbGFz
+ c2VzV05TQ29sb3KiEhRYTlNPYmpl
+ Y3RfEA9OU0tleWVkQXJjaGl2ZXLR
+ FxhUcm9vdIABCBEaIy0yNztBSE5b
+ Yo6Qkpeiq7O2v9HU2QAAAAAAAAEB
+ AAAAAAAAABkAAAAAAAAAAAAAAAAA
+ AADb
+ </data>
+ <key>ANSIYellowColor</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGFRZYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKMHCA9VJG51
+ bGzTCQoLDA0OVU5TUkdCXE5TQ29s
+ b3JTcGFjZVYkY2xhc3NPECcwLjU5
+ MzYzNjA0OSAwLjYwMTM3MTQzMzQg
+ MC4wMjMwMzQ5ODc5OAAQAYAC0hAR
+ EhNaJGNsYXNzbmFtZVgkY2xhc3Nl
+ c1dOU0NvbG9yohIUWE5TT2JqZWN0
+ XxAPTlNLZXllZEFyY2hpdmVy0RcY
+ VHJvb3SAAQgRGiMtMjc7QUhOW2KM
+ jpCVoKmxtL3P0tcAAAAAAAABAQAA
+ AAAAAAAZAAAAAAAAAAAAAAAAAAAA
+ 2Q==
+ </data>
+ <key>BackgroundBlur</key>
+ <real>0.10492684248554913</real>
+ <key>BackgroundColor</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGFRZYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKMHCA9VJG51
+ bGzTCQoLDA0OV05TV2hpdGVcTlND
+ b2xvclNwYWNlViRjbGFzc00wIDAu
+ ODUwMDAwMDIAEAOAAtIQERITWiRj
+ bGFzc25hbWVYJGNsYXNzZXNXTlND
+ b2xvcqISFFhOU09iamVjdF8QD05T
+ S2V5ZWRBcmNoaXZlctEXGFRyb290
+ gAEIERojLTI3O0FIUF1kcnR2e4aP
+ l5qjtbi9AAAAAAAAAQEAAAAAAAAA
+ GQAAAAAAAAAAAAAAAAAAAL8=
+ </data>
+ <key>Bell</key>
+ <false/>
+ <key>CursorColor</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGFRZYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKMHCA9VJG51
+ bGzTCQoLDA0OVU5TUkdCXE5TQ29s
+ b3JTcGFjZVYkY2xhc3NPECcwLjgy
+ MjYwNTI5ODkgMC4yMTIxNjI2MDU5
+ IDAuNTc2NDI4NDQzMgAQAYAC0hAR
+ EhNaJGNsYXNzbmFtZVgkY2xhc3Nl
+ c1dOU0NvbG9yohIUWE5TT2JqZWN0
+ XxAPTlNLZXllZEFyY2hpdmVy0RcY
+ VHJvb3SAAQgRGiMtMjc7QUhOW2KM
+ jpCVoKmxtL3P0tcAAAAAAAABAQAA
+ AAAAAAAZAAAAAAAAAAAAAAAAAAAA
+ 2Q==
+ </data>
+ <key>DisableANSIColor</key>
+ <false/>
+ <key>Font</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGGBlYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKQHCBESVSRu
+ dWxs1AkKCwwNDg8QVk5TU2l6ZVhO
+ U2ZGbGFnc1ZOU05hbWVWJGNsYXNz
+ I0AkAAAAAAAAEBCAAoADVk1vbmFj
+ b9ITFBUWWiRjbGFzc25hbWVYJGNs
+ YXNzZXNWTlNGb250ohUXWE5TT2Jq
+ ZWN0XxAPTlNLZXllZEFyY2hpdmVy
+ 0RobVHJvb3SAAQgRGiMtMjc8QktS
+ W2JpcnR2eH+Ej5ifoqu9wMUAAAAA
+ AAABAQAAAAAAAAAcAAAAAAAAAAAA
+ AAAAAAAAxw==
+ </data>
+ <key>FontAntialias</key>
+ <true/>
+ <key>FontHeightSpacing</key>
+ <integer>1</integer>
+ <key>FontWidthSpacing</key>
+ <integer>1</integer>
+ <key>Linewrap</key>
+ <true/>
+ <key>ProfileCurrentVersion</key>
+ <real>2.02</real>
+ <key>SelectionColor</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGFRZYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKMHCA9VJG51
+ bGzTCQoLDA0OV05TV2hpdGVcTlND
+ b2xvclNwYWNlViRjbGFzc0swLjI1
+ NDAzMjI1ABADgALSEBESE1okY2xh
+ c3NuYW1lWCRjbGFzc2VzV05TQ29s
+ b3KiEhRYTlNPYmplY3RfEA9OU0tl
+ eWVkQXJjaGl2ZXLRFxhUcm9vdIAB
+ CBEaIy0yNztBSFBdZHBydHmEjZWY
+ obO2uwAAAAAAAAEBAAAAAAAAABkA
+ AAAAAAAAAAAAAAAAAAC9
+ </data>
+ <key>ShowWindowSettingsNameInTitle</key>
+ <false/>
+ <key>TerminalType</key>
+ <string>xterm-256color</string>
+ <key>TextBoldColor</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGFRZYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKMHCA9VJG51
+ bGzTCQoLDA0OV05TV2hpdGVcTlND
+ b2xvclNwYWNlViRjbGFzc0IxABAD
+ gALSEBESE1okY2xhc3NuYW1lWCRj
+ bGFzc2VzV05TQ29sb3KiEhRYTlNP
+ YmplY3RfEA9OU0tleWVkQXJjaGl2
+ ZXLRFxhUcm9vdIABCBEaIy0yNztB
+ SFBdZGdpa3B7hIyPmKqtsgAAAAAA
+ AAEBAAAAAAAAABkAAAAAAAAAAAAA
+ AAAAAAC0
+ </data>
+ <key>TextColor</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGFRZYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKMHCA9VJG51
+ bGzTCQoLDA0OV05TV2hpdGVcTlND
+ b2xvclNwYWNlViRjbGFzc0swLjk0
+ NzU4MDY0ABADgALSEBESE1okY2xh
+ c3NuYW1lWCRjbGFzc2VzV05TQ29s
+ b3KiEhRYTlNPYmplY3RfEA9OU0tl
+ eWVkQXJjaGl2ZXLRFxhUcm9vdIAB
+ CBEaIy0yNztBSFBdZHBydHmEjZWY
+ obO2uwAAAAAAAAEBAAAAAAAAABkA
+ AAAAAAAAAAAAAAAAAAC9
+ </data>
+ <key>UseBoldFonts</key>
+ <true/>
+ <key>UseBrightBold</key>
+ <false/>
+ <key>WindowTitle</key>
+ <string>Terminal</string>
+ <key>columnCount</key>
+ <integer>156</integer>
+ <key>deleteSendsBackspace</key>
+ <false/>
+ <key>keyMapBoundKeys</key>
+ <dict>
+ <key>$F700</key>
+ <string>Oa</string>
+ <key>$F701</key>
+ <string>Ob</string>
+ <key>$F708</key>
+ <string>[25~</string>
+ <key>$F709</key>
+ <string>[26~</string>
+ <key>$F70A</key>
+ <string>[28~</string>
+ <key>$F70B</key>
+ <string>[29~</string>
+ <key>$F70C</key>
+ <string>[31~</string>
+ <key>$F70D</key>
+ <string>[22~</string>
+ <key>$F70E</key>
+ <string>[33~</string>
+ <key>$F70F</key>
+ <string>[34~</string>
+ <key>$F729</key>
+ <string>scrollToBeginningOfDocument:</string>
+ <key>$F72B</key>
+ <string></string>
+ <key>$F72C</key>
+ <string>scrollPageUp:</string>
+ <key>$F72D</key>
+ <string>scrollPageDown:</string>
+ <key>F704</key>
+ <string>OP</string>
+ <key>F705</key>
+ <string>OQ</string>
+ <key>F706</key>
+ <string>OR</string>
+ <key>F707</key>
+ <string>OS</string>
+ <key>F708</key>
+ <string>[15~</string>
+ <key>F709</key>
+ <string>[17~</string>
+ <key>F70A</key>
+ <string>[18~</string>
+ <key>F70B</key>
+ <string>[19~</string>
+ <key>F70C</key>
+ <string>[20~</string>
+ <key>F70D</key>
+ <string>[21~</string>
+ <key>F70E</key>
+ <string>[23~</string>
+ <key>F70F</key>
+ <string>[24~</string>
+ <key>F710</key>
+ <string>[25~</string>
+ <key>F711</key>
+ <string>[26~</string>
+ <key>F712</key>
+ <string>[28~</string>
+ <key>F713</key>
+ <string>[29~</string>
+ <key>F714</key>
+ <string>[31~</string>
+ <key>F715</key>
+ <string>[32~</string>
+ <key>F716</key>
+ <string>[33~</string>
+ <key>F717</key>
+ <string>[34~</string>
+ <key>F728</key>
+ <string>[3~</string>
+ <key>F729</key>
+ <string>OH</string>
+ <key>F72B</key>
+ <string>OF</string>
+ <key>F72C</key>
+ <string>[5~</string>
+ <key>F72D</key>
+ <string>[6~</string>
+ <key>^F702</key>
+ <string>Od</string>
+ <key>^F703</key>
+ <string>Oc</string>
+ <key>~F702</key>
+ <string>b</string>
+ <key>~F703</key>
+ <string>f</string>
+ <key>~F704</key>
+ <string>[17~</string>
+ <key>~F705</key>
+ <string>[18~</string>
+ <key>~F706</key>
+ <string>[19~</string>
+ <key>~F707</key>
+ <string>[20~</string>
+ <key>~F708</key>
+ <string>[21~</string>
+ <key>~F709</key>
+ <string>[23~</string>
+ <key>~F70A</key>
+ <string>[24~</string>
+ <key>~F70B</key>
+ <string>[25~</string>
+ <key>~F70C</key>
+ <string>[26~</string>
+ <key>~F70D</key>
+ <string>[28~</string>
+ <key>~F70E</key>
+ <string>[29~</string>
+ <key>~F70F</key>
+ <string>[31~</string>
+ <key>~F710</key>
+ <string>[32~</string>
+ <key>~F711</key>
+ <string>[33~</string>
+ <key>~F712</key>
+ <string>[34~</string>
+ </dict>
+ <key>mouseLeftClick</key>
+ <data>
+ BAtzdHJlYW10eXBlZIHoA4QBQISE
+ hAhOU051bWJlcgCEhAdOU1ZhbHVl
+ AISECE5TT2JqZWN0AIWEASqEhAFj
+ lwGG
+ </data>
+ <key>mouseMiddleClick</key>
+ <data>
+ BAtzdHJlYW10eXBlZIHoA4QBQISE
+ hAhOU051bWJlcgCEhAdOU1ZhbHVl
+ AISECE5TT2JqZWN0AIWEASqEhAFj
+ lwGG
+ </data>
+ <key>mouseRightClick</key>
+ <data>
+ BAtzdHJlYW10eXBlZIHoA4QBQISE
+ hAhOU051bWJlcgCEhAdOU1ZhbHVl
+ AISECE5TT2JqZWN0AIWEASqEhAFj
+ lwGG
+ </data>
+ <key>mouseWheel</key>
+ <data>
+ BAtzdHJlYW10eXBlZIHoA4QBQISE
+ hAhOU051bWJlcgCEhAdOU1ZhbHVl
+ AISECE5TT2JqZWN0AIWEASqEhAFj
+ lwGG
+ </data>
+ <key>mouseWheelEmulation</key>
+ <data>
+ BAtzdHJlYW10eXBlZIHoA4QBQISE
+ hAhOU051bWJlcgCEhAdOU1ZhbHVl
+ AISECE5TT2JqZWN0AIWEASqEhAFj
+ lwGG
+ </data>
+ <key>name</key>
+ <string>ProTerminalLatest</string>
+ <key>rowCount</key>
+ <integer>49</integer>
+ <key>shellExitAction</key>
+ <integer>0</integer>
+ <key>type</key>
+ <string>Window Settings</string>
+ <key>useOptionAsMetaKey</key>
+ <true/>
+ </dict>
+ <key>Tab Settings Name</key>
+ <string>ProTerminalLatest</string>
+ <key>TabSelected</key>
+ <true/>
+ </dict>
+ </array>
+ <key>WindowNumber</key>
+ <integer>5</integer>
+ </dict>
+ <dict>
+ <key>Origin</key>
+ <string>{2106, 1046}</string>
+ <key>ShowsTabBar</key>
+ <false/>
+ <key>ShowsTabBarInFullScreen</key>
+ <false/>
+ <key>Window Settings</key>
+ <array>
+ <dict>
+ <key>Tab Column Count</key>
+ <integer>171</integer>
+ <key>Tab Row Count</key>
+ <integer>63</integer>
+ <key>Tab Scrollback Restorable</key>
+ <true/>
+ <key>Tab Session Class ID</key>
+ <string>6DC31F6C-EFF7-4364-93B8-EBD258566CB7</string>
+ <key>Tab Settings</key>
+ <dict>
+ <key>Font</key>
+ <data>
+ YnBsaXN0MDDUAQIDBAUGGBlYJHZl
+ cnNpb25YJG9iamVjdHNZJGFyY2hp
+ dmVyVCR0b3ASAAGGoKQHCBESVSRu
+ dWxs1AkKCwwNDg8QVk5TU2l6ZVhO
+ U2ZGbGFnc1ZOU05hbWVWJGNsYXNz
+ I0AmAAAAAAAAEBCAAoADXU1lbmxv
+ LVJlZ3VsYXLSExQVFlokY2xhc3Nu
+ YW1lWCRjbGFzc2VzVk5TRm9udKIV
+ F1hOU09iamVjdF8QD05TS2V5ZWRB
+ cmNoaXZlctEaG1Ryb290gAEIERoj
+ LTI3PEJLUltiaXJ0dniGi5afpqmy
+ xMfMAAAAAAAAAQEAAAAAAAAAHAAA
+ AAAAAAAAAAAAAAAAAM4=
+ </data>
+ <key>FontAntialias</key>
+ <true/>
+ <key>FontWidthSpacing</key>
+ <real>1.004032258064516</real>
+ <key>ProfileCurrentVersion</key>
+ <real>2.02</real>
+ <key>name</key>
+ <string>Basic</string>
+ <key>type</key>
+ <string>Window Settings</string>
+ </dict>
+ <key>Tab Settings Name</key>
+ <string>Basic</string>
+ <key>TabSelected</key>
+ <true/>
+ </dict>
+ </array>
+ <key>WindowNumber</key>
+ <integer>1</integer>
+ </dict>
+ </array>
+ <key>name</key>
+ <string>lnav-screenshot</string>
+ <key>type</key>
+ <string>Window Group</string>
+</dict>
+</plist>
diff --git a/release/lnav.spec.in b/release/lnav.spec.in
new file mode 100644
index 0000000..3e4e299
--- /dev/null
+++ b/release/lnav.spec.in
@@ -0,0 +1,93 @@
+# vim: set ts=4 sw=4 et:
+#
+# spec file for package lnav
+#
+# Copyright (c) 2023 SUSE LLC
+# Copyright (c) 2010-2013 Pascal Bleser <pascal.bleser@opensuse.org>
+#
+# All modifications and additions to the file contributed by third parties
+# remain the property of their copyright owners, unless otherwise agreed
+# upon. The license for this file, and modifications and additions to the
+# file, is the same license as for the pristine package itself (unless the
+# license for the pristine package is not an Open Source License, in which
+# case the license is the MIT License). An "Open Source License" is a
+# license that conforms to the Open Source Definition (Version 1.9)
+# published by the Open Source Initiative.
+
+Name: lnav
+Version: @@LNAV_VERSION_NUMBER@@
+Release: 0
+Summary: Logfile Navigator
+License: BSD-2-Clause
+Group: System/Monitoring
+URL: https://lnav.org
+# Git-Clone: https://github.com/tstack/lnav.git
+# Source: https://github.com/tstack/%{name}/archive/v%{version}.tar.gz#/%{name}-%{version}.tar.gz
+Source: /github/home/rpmbuild/SOURCES/%{name}-%{version}.tar.gz
+# Source1: lnav.desktop
+BuildRequires: gcc-toolset-12
+BuildRequires: gcc-toolset-12-annobin-plugin-gcc
+BuildRequires: gcc-toolset-12-annobin-annocheck
+BuildRequires: autoconf
+BuildRequires: automake
+BuildRequires: libarchive-devel
+BuildRequires: libcurl-devel
+BuildRequires: ncurses-devel
+# Only needed for the tests to run
+BuildRequires: openssh
+BuildRequires: bzip2-devel
+BuildRequires: pcre2-devel
+BuildRequires: readline-devel
+BuildRequires: zlib-devel
+%if 0%{?suse_version}
+BuildRequires: sqlite3-devel >= 3.9.0
+%else
+BuildRequires: sqlite-devel >= 3.9.0
+%endif
+%if 0%{?suse_version} > 0
+BuildRequires: update-desktop-files
+%endif
+
+%description
+The Logfile Navigator, lnav for short, is a curses-based tool for viewing and
+analyzing log files. The value added by lnav over text viewers or editors is
+that it takes advantage of any semantic information that can be gleaned from
+the log file, such as timestamps and log levels. Using this extra semantic
+information, lnav can do things like interleaving messages from different
+files, generate histograms of messages over time, and provide hotkeys for
+navigating through the file. These features are meant to allow the user to
+quickly and efficiently focus on problems.
+
+%prep
+%autosetup -p1
+
+%build
+source /opt/rh/gcc-toolset-12/enable
+(cd /opt/rh/gcc-toolset-12/root/usr/lib/gcc/x86_64-redhat-linux/12/plugin/ && ln -s annobin.so gcc-annobin.so)
+autoreconf -fiv
+%configure \
+ --disable-silent-rules \
+ --disable-static \
+ --with-ncurses \
+ --with-readline || cat config.log
+
+%make_build
+
+%install
+%make_install
+
+%if %{defined suse_version}
+install -D -m0644 "%{SOURCE1}" "%{buildroot}%{_datadir}/applications/%{name}.desktop"
+%suse_update_desktop_file -r "%{name}" System Monitor
+%endif
+
+%files
+%license LICENSE
+%doc AUTHORS NEWS.md README
+%{_bindir}/lnav
+%{_mandir}/man1/lnav.1.gz
+%if %{defined suse_version}
+%{_datadir}/applications/%{name}.desktop
+%endif
+
+%changelog
diff --git a/release/loggen.py b/release/loggen.py
new file mode 100755
index 0000000..5e0bbbb
--- /dev/null
+++ b/release/loggen.py
@@ -0,0 +1,225 @@
+#! /usr/bin/env python3
+
+import os
+import sys
+import time
+import uuid
+import shutil
+import random
+import datetime
+
+SYSLOG_DATE_FMT = "%b %d %H:%M:%S"
+ACCESS_LOG_DATE_FMT = "%d/%b/%Y:%H:%M:%S"
+GENERIC_DATE_FMT = "%Y-%m-%dT%H:%M:%S.%%s"
+
+TEST_ADDRESSES = (
+ ["192.0.2.55"] * 20 +
+ ["192.0.2.44"] * 20 +
+ ["192.0.2.3"] * 40 +
+ ["192.0.2.100"] * 10 +
+ ["192.0.2.122"] * 30 +
+ ["192.0.2.42"] * 100
+)
+
+TEST_USERNAMES = [
+ "bob@example.com",
+ "bob@example.com",
+ "bob@example.com",
+ "combatcarl@example.com",
+ "combatcarl@example.com",
+ "combatcarl@example.com",
+ "combatcarl@example.com",
+ "combatcarl@example.com",
+ "combatcarl@example.com",
+ "-",
+ "-",
+ "-",
+ "-",
+ "-",
+ "-",
+]
+
+TEST_METHODS = [
+ "GET",
+ "GET",
+ "GET",
+ "GET",
+ "GET",
+ "GET",
+ "PUT",
+]
+
+PATH_COMPS = "Lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua".split()
+
+TEST_URLS = [
+ "/index.html",
+ "/index.html",
+ "/index.html",
+ "/features.html",
+ "/images/compass.jpg",
+ "/obj/1234",
+ "/obj/1235?foo=bar",
+ "/obj/1236?search=demo&start=1",
+]
+
+for index in range(0, 50):
+ path_list = []
+ for count in range(random.randint(2, 8)):
+ path_list.append(random.choice(PATH_COMPS))
+ TEST_URLS.append("/".join(path_list))
+
+TEST_VERSIONS = [
+ "HTTP/1.0",
+ "HTTP/1.0",
+ "HTTP/1.1",
+]
+
+TEST_STATUS = [
+ 200,
+ 200,
+ 200,
+ 200,
+ 200,
+ 200,
+ 200,
+ 200,
+ 200,
+ 200,
+ 404,
+ 404,
+ 404,
+ 404,
+ 403,
+ 403,
+ 403,
+ 500
+]
+
+TEST_REFERRERS = [
+ "-",
+ "-",
+ "-",
+ "-",
+ "-",
+ "-",
+ "http://lnav.org/download.html",
+]
+
+TEST_AGENTS = [
+ "-",
+ "-",
+ "-",
+ "-",
+ "Apache-HttpClient/4.2.3 (java 1.5)",
+ "Apache-HttpClient/4.2.3 (java 1.5)",
+ "Apache-HttpClient/4.2.3 (java 1.5)",
+ "Apache-HttpClient/4.2.3 (java 1.5)",
+ "Apache-HttpClient/4.2.3 (java 1.5)",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.103 Safari/537.36",
+ "Roku4640X/DVP-7.70 (297.70E04154A)",
+ "Roku4640X/DVP-7.70 (297.70E04154A)",
+ "Roku4640X/DVP-7.70 (297.70E04154A)",
+]
+
+START_TIME = datetime.datetime.fromtimestamp(1641898727)
+ACCESS_LOG_CURR_TIME = START_TIME
+SYSLOG_LOG_CURR_TIME = START_TIME
+
+
+def access_log_msgs():
+ global ACCESS_LOG_CURR_TIME
+ while True:
+ ACCESS_LOG_CURR_TIME += datetime.timedelta(seconds=random.randrange(1, 3))
+ yield '%s - %s [%s +0000] "%s %s %s" %s %s "%s" "%s"\n' % (
+ random.choice(TEST_ADDRESSES),
+ random.choice(TEST_USERNAMES),
+ ACCESS_LOG_CURR_TIME.strftime(ACCESS_LOG_DATE_FMT),
+ random.choice(TEST_METHODS),
+ random.choice(TEST_URLS),
+ random.choice(TEST_VERSIONS),
+ random.choice(TEST_STATUS),
+ int(random.lognormvariate(1, 1) * 1000),
+ random.choice(TEST_REFERRERS),
+ random.choice(TEST_AGENTS)
+ )
+
+
+TEST_PROCS = [
+ "server[123]",
+ "server[123]",
+ "server[123]",
+ "server[121]",
+ "server[124]",
+ "server[123]",
+ "worker[61456]",
+ "worker[61456]",
+ "worker[61457]",
+]
+
+TEST_MSGS = [
+ "Handling request %s" % uuid.uuid4(),
+ "Handling request %s" % uuid.uuid4(),
+ "Handling request %s" % uuid.uuid4(),
+ "Successfully started helper",
+ "Reading from device: /dev/hda",
+ "Received packet from %s" % random.choice(TEST_ADDRESSES),
+ "Received packet from %s" % random.choice(TEST_ADDRESSES),
+ "Received packet from %s" % random.choice(TEST_ADDRESSES),
+]
+
+
+def syslog_msgs():
+ global SYSLOG_LOG_CURR_TIME
+ while True:
+ SYSLOG_LOG_CURR_TIME += datetime.timedelta(seconds=random.randrange(1, 3))
+ yield '%s frontend3 %s: %s\n' % (
+ SYSLOG_LOG_CURR_TIME.strftime(SYSLOG_DATE_FMT),
+ random.choice(TEST_PROCS),
+ random.choice(TEST_MSGS),
+ )
+
+
+try:
+ shutil.rmtree("/tmp/demo")
+ os.makedirs("/tmp/demo")
+except OSError:
+ pass
+
+FILES = [
+ ("/tmp/demo/access_log", access_log_msgs()),
+ ("/tmp/demo/messages", syslog_msgs()),
+]
+
+COUNTER = 0
+while COUNTER < 5000:
+ loop_inc = datetime.timedelta(seconds=random.weibullvariate(1, 1.5) * 500)
+ ACCESS_LOG_CURR_TIME += loop_inc
+ SYSLOG_LOG_CURR_TIME += loop_inc
+ for fname, gen in FILES:
+ for i in range(random.randrange(4, 8)):
+ COUNTER += 1
+ with open(fname, "a+") as fp:
+ if random.uniform(0.0, 1.0) < 0.01:
+ line = next(gen)
+ prefix = line[:50]
+ suffix = line[50:]
+ fp.write(prefix)
+ # time.sleep(random.uniform(0.5, 0.6))
+ fp.write(suffix)
+ else:
+ fp.write(next(gen))
+ # if random.uniform(0.0, 1.0) < 0.010:
+ # fp.truncate(0)
+ time.sleep(random.uniform(0.01, 0.02))
+ # if random.uniform(0.0, 1.0) < 0.001:
+ # os.remove(fname)
diff --git a/release/makespec.sh b/release/makespec.sh
new file mode 100755
index 0000000..98d06da
--- /dev/null
+++ b/release/makespec.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+DEFAULT_VERSION=$(grep AC_INIT ../configure.ac | cut -d, -f2)
+DEFAULT_VERSION=${DEFAULT_VERSION#[}
+DEFAULT_VERSION=${DEFAULT_VERSION%]}
+
+if [ "$GITHUB_REF_TYPE" == 'tag' ]; then
+ export LNAV_VERSION_NUMBER=$(echo ${GITHUB_REF#refs/tags/v} | sed -e 's/-/~/g')
+else
+ export LNAV_VERSION_NUMBER="${DEFAULT_VERSION}^"$(date +%Y%m%d).git$(git rev-parse --short HEAD)
+fi
+
+sed -e "s/@@LNAV_VERSION_NUMBER@@/${LNAV_VERSION_NUMBER}/g"
diff --git a/release/spectrolog.py b/release/spectrolog.py
new file mode 100755
index 0000000..df08a12
--- /dev/null
+++ b/release/spectrolog.py
@@ -0,0 +1,114 @@
+#! /usr/bin/env python
+
+import sys
+import time
+import datetime
+import random
+
+DATE_FMT = "%a %b %d %H:%M:%S %Y"
+
+duration = [] + [80] * 10 + [100] * 10 + [40] * 10
+
+diter = iter(duration)
+
+DURATIONS = (
+ 40,
+ 40,
+ 40,
+ 40,
+ 40,
+ 40,
+ 40,
+ 40,
+ 40,
+ 40,
+ 40,
+ 40,
+ 40,
+ 50,
+ 50,
+ 50,
+ 50,
+ 75,
+ 75,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+ 100,
+)
+
+DURATION_FUZZ = (
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -1,
+ -2,
+ -2,
+ -2
+)
+
+while True:
+ print ("[pid: 88186|app: 0|req: 5/19] 127.0.0.1 () {38 vars in 696 bytes} "
+ "[%s] POST /update_metrics => generated 47 bytes "
+ "in %s msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 60)" %
+ (datetime.datetime.utcnow().strftime(DATE_FMT),
+ random.choice(DURATIONS) + random.choice(DURATION_FUZZ)))
+ # diter.next()))
+ sys.stdout.flush()
+
+ time.sleep(0.01)
diff --git a/release/tail-demo.sh b/release/tail-demo.sh
new file mode 100755
index 0000000..a9a5998
--- /dev/null
+++ b/release/tail-demo.sh
@@ -0,0 +1,19 @@
+#!/usr/bin/env bash
+
+PAUSE_TIME=0.5
+
+while true; do
+ logger "lnav is always looking for new log messages"
+ sleep ${PAUSE_TIME}
+ logger " and files in directories"
+ sleep ${PAUSE_TIME}
+ logger "Filters are always applied as new data is loaded in"
+ logger " which is indicated by the 'Not Shown' count"
+ logger "bad-message-to-filter"
+ sleep ${PAUSE_TIME}
+ logger ""
+ sleep ${PAUSE_TIME}
+ logger "Scroll up to lock the view in place"
+ sleep 3
+ logger "Data is still being read in the meantime"
+done
diff --git a/release/vagrant-static/Vagrantfile b/release/vagrant-static/Vagrantfile
new file mode 100644
index 0000000..98df560
--- /dev/null
+++ b/release/vagrant-static/Vagrantfile
@@ -0,0 +1,110 @@
+# -*- mode: ruby -*-
+# vi: set ft=ruby :
+
+# All Vagrant configuration is done below. The "2" in Vagrant.configure
+# configures the configuration version (we support older styles for
+# backwards compatibility). Please don't change it unless you know what
+# you're doing.
+Vagrant.configure(2) do |config|
+ # The most common configuration options are documented and commented below.
+ # For a complete reference, please see the online documentation at
+ # https://docs.vagrantup.com.
+
+ # Every Vagrant development environment requires a box. You can search for
+ # boxes at https://atlas.hashicorp.com/search.
+ config.vm.provider "virtualbox" do |v|
+ v.memory = 2048
+ v.cpus = 4
+ end
+
+ config.vm.synced_folder ".", "/vagrant", type: "nfs"
+ config.vm.define :freebsd do |freebsd|
+ freebsd.vm.network "private_network", :type => 'dhcp'
+ freebsd.vm.provision "shell", path:"pkg.sh"
+ freebsd.vm.provision "shell", path:"provision.sh"
+ freebsd.vm.box = "freebsd/FreeBSD-12.1-RELEASE"
+ freebsd.vm.box_version = "2019.11.01"
+ freebsd.vm.synced_folder ".", "/vagrant", type: "nfs"
+ freebsd.ssh.shell = "sh"
+ freebsd.vm.base_mac = "080027D14C66"
+ freebsd.vm.boot_timeout = 1200
+ end
+
+ config.vm.define :musl do |musl|
+ musl.vbguest.auto_update = false
+ musl.vm.synced_folder ".", "/vagrant", type: "virtualbox"
+ musl.vm.network "private_network", ip: "192.168.56.16"
+ musl.vm.provision "shell", path:"musl-pkg.sh"
+ musl.vm.provision "shell", path:"provision.sh"
+ musl.vm.box = "generic/alpine315"
+ end
+
+ config.vm.define :linux do |linux|
+ linux.vm.network :private_network, ip: "192.168.56.14"
+ linux.vm.provision "shell", path:"pkg.sh"
+ linux.vm.provision "shell", path:"provision.sh"
+ linux.vm.box = "centos/7"
+ end
+
+ config.vm.define :pkger do |pkger|
+ # config.vm.synced_folder ".", "/vagrant", type: "nfs"
+ pkger.vm.network :private_network, ip: "192.168.56.15"
+ pkger.vm.provision "shell", path:"provision-pkg.sh"
+ pkger.vm.box = "ubuntu/xenial64"
+ end
+
+ # Disable automatic box update checking. If you disable this, then
+ # boxes will only be checked for updates when the user runs
+ # `vagrant box outdated`. This is not recommended.
+ # config.vm.box_check_update = false
+
+ # Create a forwarded port mapping which allows access to a specific port
+ # within the machine from a port on the host machine. In the example below,
+ # accessing "localhost:8080" will access port 80 on the guest machine.
+ # config.vm.network "forwarded_port", guest: 80, host: 8080
+
+ # Create a private network, which allows host-only access to the machine
+ # using a specific IP.
+ # config.vm.network "private_network", ip: "192.168.33.10"
+
+ # Create a public network, which generally matched to bridged network.
+ # Bridged networks make the machine appear as another physical device on
+ # your network.
+ # config.vm.network "public_network"
+
+ # Share an additional folder to the guest VM. The first argument is
+ # the path on the host to the actual folder. The second argument is
+ # the path on the guest to mount the folder. And the optional third
+ # argument is a set of non-required options.
+ # config.vm.synced_folder "../data", "/vagrant_data"
+
+ # Provider-specific configuration so you can fine-tune various
+ # backing providers for Vagrant. These expose provider-specific options.
+ # Example for VirtualBox:
+ #
+ # config.vm.provider "virtualbox" do |vb|
+ # # Display the VirtualBox GUI when booting the machine
+ # vb.gui = true
+ #
+ # # Customize the amount of memory on the VM:
+ # vb.memory = "1024"
+ # end
+ #
+ # View the documentation for the provider you are using for more
+ # information on available options.
+
+ # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies
+ # such as FTP and Heroku are also available. See the documentation at
+ # https://docs.vagrantup.com/v2/push/atlas.html for more information.
+ # config.push.define "atlas" do |push|
+ # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME"
+ # end
+
+ # Enable provisioning with a shell script. Additional provisioners such as
+ # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the
+ # documentation for more information about their specific syntax and use.
+ # config.vm.provision "shell", inline: <<-SHELL
+ # sudo apt-get update
+ # sudo apt-get install -y apache2
+ # SHELL
+end
diff --git a/release/vagrant-static/build-pkg.sh b/release/vagrant-static/build-pkg.sh
new file mode 100755
index 0000000..232abd5
--- /dev/null
+++ b/release/vagrant-static/build-pkg.sh
@@ -0,0 +1,30 @@
+#!/usr/bin/env bash
+
+VERSION=$1
+
+fpm --verbose \
+ -s zip \
+ -t rpm \
+ -n "lnav" \
+ -v "$VERSION" \
+ -p /vagrant/ \
+ -a native \
+ --url https://lnav.org \
+ -m 'timothyshanestack@gmail.com' \
+ --description 'A log file viewer and analyzer for the terminal' \
+ --license BSD-2-Clause \
+ --category 'System/Monitoring' \
+ /vagrant/lnav-linux.zip
+
+fpm --verbose \
+ -s zip \
+ -t deb \
+ -n "lnav" \
+ -v "$VERSION" \
+ -p /vagrant/ \
+ -a native \
+ --url https://lnav.org \
+ -m 'timothyshanestack@gmail.com' \
+ --description 'A log file viewer and analyzer for the terminal' \
+ --license BSD-2-Clause \
+ /vagrant/lnav-linux.zip
diff --git a/release/vagrant-static/build.sh b/release/vagrant-static/build.sh
new file mode 100755
index 0000000..aa0491b
--- /dev/null
+++ b/release/vagrant-static/build.sh
@@ -0,0 +1,84 @@
+#!/usr/bin/env bash
+
+OS=$(uname -s)
+if test x"${OS}" != x"FreeBSD"; then
+ source scl_source enable devtoolset-9
+fi
+
+if test x"${OS}" != x"FreeBSD"; then
+ MAKE=make
+else
+ MAKE=gmake
+fi
+
+FAKE_ROOT=/home/vagrant/fake.root
+
+SRC_VERSION=$1
+
+mkdir -p ~/github
+
+cd ~/github
+if ! test -d lnav; then
+ git clone https://github.com/tstack/lnav.git
+fi
+
+cd ~/github/lnav
+git restore .
+git pull --rebase
+
+if test -n "$SRC_VERSION"; then
+ git checkout "$SRC_VERSION"
+fi
+
+saved_PATH=${PATH}
+export PATH=${FAKE_ROOT}/bin:${PATH}
+saved_LD_LIBRARY_PATH=${LD_LIBRARY_PATH}
+export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:${FAKE_ROOT}/lib
+if test ! -f "configure"; then
+ ./autogen.sh
+ rm -rf ~/github/lbuild
+ mkdir -p ~/github/lbuild
+fi
+cd ~/github/lbuild
+
+TARGET_FILE='/vagrant/lnav-linux.zip'
+if test x"${OS}" != x"FreeBSD"; then
+ if test x"$(lsb_release | awk '{print $3}')" == x"Alpine"; then
+ TARGET_FILE='/vagrant/lnav-musl.zip'
+ ../lnav/configure \
+ --with-libarchive=${FAKE_ROOT} \
+ CFLAGS='-static -g1 -gz=zlib -no-pie -O2' \
+ CXXFLAGS='-static -g1 -gz=zlib -U__unused -no-pie -O2' \
+ LDFLAGS="-L${FAKE_ROOT}/lib" \
+ CPPFLAGS="-I${FAKE_ROOT}/include" \
+ LIBS="-L${FAKE_ROOT}/lib -lexecinfo -lssh2 -llzma -lssl -lcrypto -lz" \
+ --enable-static
+ PATH="${FAKE_ROOT}/bin:${PATH}"
+ else
+ ../lnav/configure \
+ --enable-static \
+ --with-libarchive=${FAKE_ROOT} \
+ LDFLAGS="-L${FAKE_ROOT}/lib" \
+ CPPFLAGS="-I${FAKE_ROOT}/include -O2" \
+ LIBS="-L${FAKE_ROOT}/lib -lssh2 -llzma -lssl -lcrypto -lz" \
+ PATH="${FAKE_ROOT}/bin:${PATH}"
+ fi
+else
+ ../lnav/configure \
+ --enable-static \
+ LDFLAGS="-L${FAKE_ROOT}/lib -static" \
+ LIBS="-lm -lelf" \
+ CPPFLAGS="-I${FAKE_ROOT}/include -O2" \
+ PATH="${FAKE_ROOT}/bin:${PATH}"
+fi
+
+${MAKE} -j2 && cp src/lnav /vagrant/lnav
+
+if test x"${OS}" != x"FreeBSD"; then
+ mkdir instdir
+ make install DESTDIR=$PWD/instdir
+ (cd instdir/ && zip -r "${TARGET_FILE}" .)
+fi
+
+export PATH=${saved_PATH}
+export LD_LIBRARY_PATH=${saved_LD_LIBRARY_PATH}
diff --git a/release/vagrant-static/musl-pkg.sh b/release/vagrant-static/musl-pkg.sh
new file mode 100644
index 0000000..fea8308
--- /dev/null
+++ b/release/vagrant-static/musl-pkg.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+
+sudo apk update && sudo apk upgrade
+sudo apk add \
+ build-base \
+ binutils \
+ m4 \
+ git \
+ zip \
+ perl \
+ ncurses \
+ ncurses-dev \
+ autoconf \
+ automake \
+ elfutils \
+ elfutils-dev \
+ libelf-static \
+ libexecinfo-dev \
+ libexecinfo-static \
+ libtool \
+ libunwind \
+ libunwind-dev \
+ libunwind-static \
+ linux-headers
diff --git a/release/vagrant-static/pkg.sh b/release/vagrant-static/pkg.sh
new file mode 100755
index 0000000..931dc33
--- /dev/null
+++ b/release/vagrant-static/pkg.sh
@@ -0,0 +1,22 @@
+#!/bin/sh
+
+OS=$(uname -s)
+if test x"${OS}" != x"FreeBSD"; then
+ sudo yum install -y \
+ zip \
+ unzip \
+ m4 \
+ autoconf \
+ automake \
+ ncurses \
+ ncurses-devel \
+ ncurses-static \
+ git \
+ centos-release-scl \
+ perl-Data-Dumper \
+ patch \
+ wget
+ sudo yum install -y "devtoolset-9-gcc*"
+else
+ pkg install -y wget git m4 bash autoconf automake sqlite3 gmake
+fi
diff --git a/release/vagrant-static/provision-pkg.sh b/release/vagrant-static/provision-pkg.sh
new file mode 100644
index 0000000..bb1991d
--- /dev/null
+++ b/release/vagrant-static/provision-pkg.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+# Installs dependencies for running fpm
+
+sudo apt-get update
+sudo apt-get install -y ruby2.3 ruby-dev gcc make zip unzip rpm
+sudo gem install fpm
diff --git a/release/vagrant-static/provision.sh b/release/vagrant-static/provision.sh
new file mode 100755
index 0000000..3b98103
--- /dev/null
+++ b/release/vagrant-static/provision.sh
@@ -0,0 +1,152 @@
+#!/usr/bin/env bash
+
+OS=$(uname -s)
+if test x"${OS}" != x"FreeBSD"; then
+ source scl_source enable devtoolset-9
+fi
+
+FAKE_ROOT=/home/vagrant/fake.root
+
+rm -rf ~/extract
+mkdir -p ${FAKE_ROOT} ~/packages ~/extract ~/github
+
+export PATH=${FAKE_ROOT}/bin:${PATH}
+
+cd ~/github
+
+SQLITE_CFLAGS="\
+ -DSQLITE_ENABLE_COLUMN_METADATA \
+ -DSQLITE_SOUNDEX \
+ -DSQLITE_ENABLE_DBSTAT_VTAB \
+ -DSQLITE_ENABLE_API_ARMOR \
+ -DSQLITE_ENABLE_JSON1 \
+ -DSQLITE_ENABLE_UPDATE_DELETE_LIMIT \
+ "
+
+NCURSES_FALLBACKS="\
+ansi,\
+cygwin,\
+Eterm,\
+Eterm-256color,\
+gnome,\
+gnome-256color,\
+konsole,\
+konsole-256color,\
+linux,\
+putty,\
+rxvt,\
+rxvt-256color,\
+screen,\
+screen-16color,\
+screen-256color,\
+tmux,\
+tmux-256color,\
+vt100,\
+vt220,\
+xterm,\
+xterm-256color\
+"
+
+cd ~/extract
+
+for pkg in /vagrant/pkgs/*.tar.gz; do
+ tar xfz "$pkg"
+done
+
+(cd make-4.2.1 && ./configure --prefix=${FAKE_ROOT} && make && make install)
+
+OS=$(uname -s)
+
+
+(cd readline-6.3 && ./configure --prefix=${FAKE_ROOT} && make && make install)
+
+(cd bzip2-1.0.8 && make install PREFIX=${FAKE_ROOT})
+
+(cd sqlite-* &&
+ ./configure --disable-editline --prefix=${FAKE_ROOT} \
+ CFLAGS="${SQLITE_CFLAGS}" \
+ && \
+ make && make install)
+
+(cd openssl-* &&
+ ./config --prefix=${FAKE_ROOT} -fPIC &&
+ make &&
+ make install)
+
+(cd ncurses-6.3 && \
+ ./configure --prefix=${FAKE_ROOT} \
+ --enable-ext-mouse \
+ --enable-sigwinch \
+ --with-default-terminfo-dir=/usr/share/terminfo \
+ --enable-ext-colors \
+ --enable-widec \
+ --enable-termcap \
+ --with-fallbacks=$NCURSES_FALLBACKS \
+ && \
+ make && make install)
+
+(cd pcre2-* && \
+ ./configure --prefix=${FAKE_ROOT} \
+ --enable-jit \
+ && \
+ make && make install)
+
+if test x"${OS}" != x"FreeBSD"; then
+ (cd zlib-1.2.12 && ./configure --prefix=${FAKE_ROOT} && make && make install)
+
+ (cd libssh2-* &&
+ ./configure --prefix=${FAKE_ROOT} \
+ --with-libssl-prefix=${FAKE_ROOT} \
+ --with-libz-prefix=${FAKE_ROOT} \
+ "CPPFLAGS=-I${FAKE_ROOT}/include" \
+ "LDFLAGS=-ldl -L${FAKE_ROOT}/lib" &&
+ make &&
+ make install)
+
+ (cd curl-* &&
+ ./configure --prefix=${FAKE_ROOT} \
+ --with-libssh2=${FAKE_ROOT} \
+ --with-ssl=${FAKE_ROOT} \
+ --with-zlib=${FAKE_ROOT} \
+ "LDFLAGS=-ldl" &&
+ make &&
+ make install)
+else
+ (cd zlib-1.2.12 && ./configure --prefix=${FAKE_ROOT} "CFLAGS=-fPIC" \
+ && make && make install)
+
+ (cd libssh2-* &&
+ ./configure --prefix=${FAKE_ROOT} \
+ --with-libssl-prefix=${FAKE_ROOT} \
+ --with-libz-prefix=${FAKE_ROOT} \
+ &&
+ make &&
+ make install)
+
+ (cd curl-* &&
+ ./configure --prefix=${FAKE_ROOT} \
+ --with-libssh2=${FAKE_ROOT} \
+ --with-ssl=${FAKE_ROOT} \
+ --with-zlib=${FAKE_ROOT} \
+ "CFLAGS=-fPIC" &&
+ make &&
+ make install)
+fi
+
+(cd xz-* &&
+ ./configure --prefix=${FAKE_ROOT} \
+ --disable-shared \
+ "LDFLAGS=-L${FAKE_ROOT}/lib" \
+ "CPPFLAGS=-I${FAKE_ROOT}/include" \
+ &&
+ make &&
+ make install)
+
+(cd libarchive-* &&
+ ./configure --prefix=${FAKE_ROOT} \
+ --disable-shared \
+ "LDFLAGS=-L${FAKE_ROOT}/lib" \
+ "CPPFLAGS=-I${FAKE_ROOT}/include" \
+ &&
+ make &&
+ make install)
diff --git a/snapcraft.yaml b/snapcraft.yaml
new file mode 100644
index 0000000..109b541
--- /dev/null
+++ b/snapcraft.yaml
@@ -0,0 +1,91 @@
+name: lnav
+adopt-info: lnav
+summary: Log File Navigator
+description: |
+ The Log File Navigator, **lnav** for short, is an advanced log file viewer
+ for the small-scale.
+icon: docs/assets/images/favicon.png
+
+base: core20
+grade: stable
+confinement: strict
+
+environment:
+ LOCPATH: $SNAP/usr/lib/locale
+ GIT_TEMPLATE_DIR: $SNAP/usr/share/git-core/templates
+ GIT_EXEC_PATH: $SNAP/usr/lib/git-core
+
+plugs:
+ etc-lnav:
+ interface: system-files
+ read:
+ - /etc/lnav
+
+apps:
+ lnav:
+ command: usr/bin/lnav
+ plugs:
+ - etc-lnav
+ - home # optional, allows to read log files from home directory
+ - log-observe # required, provide access to system logs in /var/log
+ - network # required, lnav uses sendto() with UNIX domain socket
+ - removable-media
+ - ssh-keys
+ - x11
+
+parts:
+ selective-checkout:
+ source: https://github.com/Lin-Buo-Ren/selective-checkout.git
+ source-tag: v2.0.2
+ plugin: dump
+ build-packages:
+ # Uncomment the VCS your main part is using
+ - git
+ - curl
+ - jq
+ - sed
+
+ #- mercurial
+ #- subversion
+ stage:
+ - scriptlets/selective-checkout
+ prime:
+ - -*
+ lnav:
+ after:
+ - selective-checkout
+ plugin: autotools
+ autotools-configure-parameters:
+ - CFLAGS="-O2"
+ - CXXFLAGS="-O2"
+ source: https://github.com/tstack/lnav.git
+ source-depth: 500
+ override-pull: |
+ snapcraftctl pull
+
+ "$SNAPCRAFT_STAGE"/scriptlets/selective-checkout --debug --force-snapshot
+ build-packages:
+ - build-essential
+ - libarchive-dev
+ - libcurl4-gnutls-dev
+ - libpcre2-dev
+ - libsqlite3-dev
+ - libncursesw6
+ - libreadline-dev
+ - zlib1g-dev
+ - libbz2-dev
+ - libgpm-dev
+ stage-packages:
+ - zlib1g
+ - git-core
+ - libcurl4
+ - libncursesw6
+ - libpcre2-8-0
+ - libgpm2
+ - libarchive13
+ - libicu66
+ - libxml2
+ - locales-all
+ - ssh
+ - tshark
+ - xclip
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
new file mode 100644
index 0000000..58cfd7b
--- /dev/null
+++ b/src/CMakeLists.txt
@@ -0,0 +1,633 @@
+include(CheckTypeSize)
+include(CheckIncludeFile)
+include(CheckLibraryExists)
+include(CheckFunctionExists)
+
+check_type_size(off_t SIZEOF_OFF_T)
+
+check_include_file("pty.h" HAVE_PTY_H)
+check_include_file("util.h" HAVE_UTIL_H)
+check_include_file("execinfo.h" HAVE_EXECINFO_H)
+
+set(VCS_PACKAGE_STRING "lnav ${CMAKE_PROJECT_VERSION}")
+set(PACKAGE_VERSION "${CMAKE_PROJECT_VERSION}")
+
+configure_file(config.cmake.h.in config.h)
+
+add_subdirectory(base)
+add_subdirectory(pcrepp)
+add_subdirectory(remote)
+add_subdirectory(tailer)
+add_subdirectory(formats/logfmt)
+add_subdirectory(yajl)
+add_subdirectory(yajlpp)
+
+add_executable(bin2c bin2c.hh ../tools/bin2c.c)
+target_link_libraries(bin2c ZLIB::ZLIB)
+
+add_executable(ptimec ptimec.hh ptimec.c)
+
+set(TIME_FORMATS
+ "@%@"
+ "%Y-%m-%d %H:%M:%S"
+ "%Y-%m-%d %H:%M:%S%z"
+ "%Y-%m-%d %H:%M:%S %z"
+ "%Y-%m-%d %H:%M"
+ "%Y-%m-%dT%H:%M:%S.%f%z"
+ "%y-%m-%dT%H:%M:%S.%f%z"
+ "%Y-%m-%dT%H:%M:%SZ"
+ "%Y-%m-%dT%H:%M:%S%z"
+ "%Y-%m-%dT%H:%M:%S"
+ "%Y-%m-%dT%H:%M:%S%z"
+ "%Y/%m/%d %H:%M:%S"
+ "%Y/%m/%d %H:%M:%S %z"
+ "%Y/%m/%d %H:%M:%S%z"
+ "%Y/%m/%d %H:%M"
+ "%Y %b %d %a %H:%M:%S.%L"
+ "%Y %b %d %H:%M:%S.%L"
+ "%Y %b %d %H:%M:%S"
+ "%a %b %d %H:%M:%S %Y"
+ "%a %b %d %H:%M:%S.%f %Y"
+ "%a %b %d %H:%M:%S %Z %Y"
+ "%a %b %d %H:%M:%S "
+ "%a %b %d %H:%M:%S.%L "
+ "%a %b %d %H:%M "
+ "%a %b %e %H:%M:%S %Z %Y"
+ "%d/%b/%Y:%H:%M:%S +0000"
+ "%d/%b/%Y:%H:%M:%S %z"
+ "%d-%b-%Y %H:%M:%S %z"
+ "%d-%b-%Y %H:%M:%S %Z"
+ "%d %b %Y %H:%M:%S"
+ "%d %b %Y %H:%M:%S.%L"
+ "%d %b %Y %H:%M:%S,%L"
+ "%b %d %H:%M:%S"
+ "%b %d %k:%M:%S"
+ "%b %d %l:%M:%S"
+ "%b %e, %Y %l:%M:%S %p"
+ "%m/%d/%y %H:%M:%S"
+ "%m/%d/%Y %I:%M:%S:%L %p %Z"
+ "%m/%d/%Y %I:%M:%S %p %Z"
+ "%m/%d/%Y %l:%M:%S %p %Z"
+ "%m/%e/%Y %I:%M:%S %p"
+ "%m/%e/%Y %l:%M:%S %p"
+ "%m/%d/%Y %H:%M:%S"
+ "%d/%b/%y %H:%M:%S"
+ "%m%d %H:%M:%S"
+ "%Y%m%d %H:%M:%S"
+ "%Y%m%d.%H%M%S"
+ "%H:%M:%S"
+ "%H:%M:%S.%f"
+ "%M:%S"
+ "%m/%d %H:%M:%S"
+ "%Y-%m-%d"
+ "%Y-%m"
+ "%Y/%m/%d"
+ "%Y/%m"
+ "%s.%f")
+
+set(GEN_SRCS "")
+
+add_custom_command(OUTPUT time_fmts.cc COMMAND ptimec ${TIME_FORMATS} >
+ time_fmts.cc)
+
+add_library(lnavdt STATIC config.h.in ptimec.hh ptimec_rt.cc time_fmts.cc)
+target_include_directories(lnavdt PUBLIC . ${CMAKE_CURRENT_BINARY_DIR})
+
+function(bin2c)
+ cmake_parse_arguments(BIN2C_ "" "VARNAME" "" ${ARGN})
+
+ list(TRANSFORM BIN2C_UNPARSED_ARGUMENTS "\\." "-")
+ add_custom_command(
+ OUTPUT "${DST_FILE}.h" "${DST_FILE}.cc"
+ COMMAND bin2c "${DST_FILE}" "${CMAKE_CURRENT_SOURCE_DIR}/${FILE_TO_LINK}"
+ DEPENDS bin2c "${FILE_TO_LINK}")
+endfunction(bin2c)
+
+foreach (FILE_TO_LINK animals.json ansi-palette.json diseases.json emojis.json xml-entities.json xterm-palette.json help.txt help.md init.sql words.json)
+ string(REPLACE "." "-" DST_FILE "${FILE_TO_LINK}")
+ add_custom_command(
+ OUTPUT "${DST_FILE}.h" "${DST_FILE}.cc"
+ COMMAND bin2c "${DST_FILE}" "${CMAKE_CURRENT_SOURCE_DIR}/${FILE_TO_LINK}"
+ DEPENDS bin2c "${FILE_TO_LINK}")
+ list(APPEND GEN_SRCS "${CMAKE_CURRENT_BINARY_DIR}/${DST_FILE}.h"
+ "${CMAKE_CURRENT_BINARY_DIR}/${DST_FILE}.cc")
+endforeach (FILE_TO_LINK)
+
+set(FORMAT_FILES
+ formats/access_log.json
+ formats/alb_log.json
+ formats/block_log.json
+ formats/candlepin_log.json
+ formats/choose_repo_log.json
+ formats/cups_log.json
+ formats/dpkg_log.json
+ formats/elb_log.json
+ formats/engine_log.json
+ formats/error_log.json
+ formats/esx_syslog_log.json
+ formats/fsck_hfs_log.json
+ formats/glog_log.json
+ formats/haproxy_log.json
+ formats/java_log.json
+ formats/journald_json_log.json
+ formats/katello_log.json
+ formats/openam_log.json
+ formats/openamdb_log.json
+ formats/openstack_log.json
+ formats/page_log.json
+ formats/papertrail_log.json
+ formats/pcap_log.json
+ formats/procstate_log.json
+ formats/snaplogic_log.json
+ formats/sssd_log.json
+ formats/strace_log.json
+ formats/sudo_log.json
+ formats/syslog_log.json
+ formats/s3_log.json
+ formats/tcf_log.json
+ formats/tcsh_history.json
+ formats/uwsgi_log.json
+ formats/vdsm_log.json
+ formats/vmk_log.json
+ formats/vmw_log.json
+ formats/vmw_vc_svc_log.json
+ formats/vmw_py_log.json
+ formats/xmlrpc_log.json)
+
+set(FORMAT_FILE_PATHS ${FORMAT_FILES})
+
+list(TRANSFORM FORMAT_FILE_PATHS PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/")
+
+add_custom_command(
+ OUTPUT default-formats.h default-formats.cc
+ COMMAND bin2c -n lnav_format_json default-formats ${FORMAT_FILE_PATHS}
+ DEPENDS bin2c ${FORMAT_FILES})
+list(APPEND GEN_SRCS default-formats.h default-formats.cc)
+
+set(CONFIG_FILES
+ root-config.json
+ keymaps/de-keymap.json
+ keymaps/default-keymap.json
+ keymaps/fr-keymap.json
+ keymaps/uk-keymap.json
+ keymaps/us-keymap.json
+ themes/default-theme.json
+ themes/grayscale.json
+ themes/eldar.json
+ themes/monocai.json
+ themes/night-owl.json
+ themes/solarized-dark.json
+ themes/solarized-light.json)
+
+set(CONFIG_FILE_PATHS ${CONFIG_FILES})
+
+list(TRANSFORM CONFIG_FILE_PATHS PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/")
+
+add_custom_command(
+ OUTPUT default-config.h default-config.cc
+ COMMAND bin2c -n lnav_config_json default-config ${CONFIG_FILE_PATHS}
+ DEPENDS bin2c ${CONFIG_FILES})
+list(APPEND GEN_SRCS default-config.h default-config.cc)
+
+set(BUILTIN_LNAV_SCRIPTS
+ scripts/dhclient-summary.lnav scripts/lnav-pop-view.lnav
+ scripts/partition-by-boot.lnav scripts/rename-stdin.lnav
+ scripts/search-for.lnav)
+
+set(BUILTIN_LNAV_SCRIPT_PATHS ${BUILTIN_LNAV_SCRIPTS})
+
+list(TRANSFORM BUILTIN_LNAV_SCRIPT_PATHS PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/")
+
+add_custom_command(
+ OUTPUT builtin-scripts.h builtin-scripts.cc
+ COMMAND bin2c -n lnav_scripts builtin-scripts ${BUILTIN_LNAV_SCRIPT_PATHS}
+ DEPENDS bin2c ${BUILTIN_LNAV_SCRIPTS})
+list(APPEND GEN_SRCS builtin-scripts.h builtin-scripts.cc)
+
+set(BUILTIN_SH_SCRIPTS scripts/dhclient-summary.lnav scripts/lnav-pop-view.lnav
+ scripts/partition-by-boot.lnav scripts/search-for.lnav)
+
+set(BUILTIN_SH_SCRIPT_PATHS ${BUILTIN_SH_SCRIPTS})
+
+list(TRANSFORM BUILTIN_SH_SCRIPT_PATHS PREPEND "${CMAKE_CURRENT_SOURCE_DIR}/")
+
+add_custom_command(
+ OUTPUT builtin-sh-scripts.h builtin-sh-scripts.cc
+ COMMAND bin2c -n lnav_sh_scripts builtin-sh-scripts ${BUILTIN_SH_SCRIPT_PATHS}
+ DEPENDS bin2c ${BUILTIN_SH_SCRIPTS})
+list(APPEND GEN_SRCS builtin-sh-scripts.h builtin-sh-scripts.cc)
+
+add_library(
+ cppfmt STATIC
+ fmtlib/format.cc
+ fmtlib/os.cc
+ fmtlib/fmt/args.h
+ fmtlib/fmt/chrono.h
+ fmtlib/fmt/color.h
+ fmtlib/fmt/compile.h
+ fmtlib/fmt/core.h
+ fmtlib/fmt/format-inl.h
+ fmtlib/fmt/format.h
+ fmtlib/fmt/locale.h
+ fmtlib/fmt/os.h
+ fmtlib/fmt/ostream.h
+ fmtlib/fmt/printf.h
+ fmtlib/fmt/ranges.h
+ fmtlib/fmt/std.h
+ fmtlib/fmt/xchar.h
+)
+target_include_directories(cppfmt PUBLIC fmtlib)
+
+add_library(
+ cppscnlib STATIC
+ third-party/scnlib/src/reader_float.cpp
+ third-party/scnlib/src/reader_int.cpp
+ third-party/scnlib/src/locale.cpp
+ third-party/scnlib/src/file.cpp
+ third-party/scnlib/src/vscan.cpp
+
+ third-party/scnlib/include/scn/reader/reader.h
+ third-party/scnlib/include/scn/reader/float.h
+ third-party/scnlib/include/scn/reader/types.h
+ third-party/scnlib/include/scn/reader/int.h
+ third-party/scnlib/include/scn/reader/common.h
+ third-party/scnlib/include/scn/reader/string.h
+ third-party/scnlib/include/scn/ranges/custom_impl.h
+ third-party/scnlib/include/scn/ranges/std_impl.h
+ third-party/scnlib/include/scn/ranges/ranges.h
+ third-party/scnlib/include/scn/ranges/util.h
+ third-party/scnlib/include/scn/fwd.h
+ third-party/scnlib/include/scn/util/algorithm.h
+ third-party/scnlib/include/scn/util/small_vector.h
+ third-party/scnlib/include/scn/util/optional.h
+ third-party/scnlib/include/scn/util/expected.h
+ third-party/scnlib/include/scn/util/array.h
+ third-party/scnlib/include/scn/util/unique_ptr.h
+ third-party/scnlib/include/scn/util/math.h
+ third-party/scnlib/include/scn/util/memory.h
+ third-party/scnlib/include/scn/util/span.h
+ third-party/scnlib/include/scn/util/meta.h
+ third-party/scnlib/include/scn/util/string_view.h
+ third-party/scnlib/include/scn/unicode/unicode.h
+ third-party/scnlib/include/scn/unicode/common.h
+ third-party/scnlib/include/scn/unicode/utf16.h
+ third-party/scnlib/include/scn/unicode/utf8.h
+ third-party/scnlib/include/scn/all.h
+ third-party/scnlib/include/scn/tuple_return/tuple_return.h
+ third-party/scnlib/include/scn/tuple_return/util.h
+ third-party/scnlib/include/scn/scan/ignore.h
+ third-party/scnlib/include/scn/scan/getline.h
+ third-party/scnlib/include/scn/scan/list.h
+ third-party/scnlib/include/scn/scan/common.h
+ third-party/scnlib/include/scn/scan/istream.h
+ third-party/scnlib/include/scn/scan/vscan.h
+ third-party/scnlib/include/scn/scan/scan.h
+ third-party/scnlib/include/scn/tuple_return.h
+ third-party/scnlib/include/scn/detail/error.h
+ third-party/scnlib/include/scn/detail/fwd.h
+ third-party/scnlib/include/scn/detail/range.h
+ third-party/scnlib/include/scn/detail/locale.h
+ third-party/scnlib/include/scn/detail/config.h
+ third-party/scnlib/include/scn/detail/file.h
+ third-party/scnlib/include/scn/detail/context.h
+ third-party/scnlib/include/scn/detail/result.h
+ third-party/scnlib/include/scn/detail/visitor.h
+ third-party/scnlib/include/scn/detail/args.h
+ third-party/scnlib/include/scn/detail/parse_context.h
+ third-party/scnlib/include/scn/detail/vectored.h
+ third-party/scnlib/include/scn/scn.h
+ third-party/scnlib/include/scn/istream.h
+)
+target_include_directories(cppscnlib PRIVATE third-party/scnlib/src/deps/fast_float/single_include)
+target_include_directories(cppscnlib PUBLIC third-party/scnlib/include)
+
+add_library(
+ base64 STATIC
+ third-party/base64/lib/lib.c
+ third-party/base64/lib/arch/generic/codec.c
+ third-party/base64/lib/tables/tables.c
+)
+target_include_directories(base64 PRIVATE third-party/base64/lib)
+target_include_directories(base64 PUBLIC third-party/base64/include)
+
+add_library(
+ spookyhash STATIC
+ spookyhash/SpookyV2.cpp
+)
+
+add_library(lnavfileio STATIC
+ grep_proc.hh
+ line_buffer.hh
+ pollable.hh
+ shared_buffer.hh
+
+ grep_proc.cc
+ line_buffer.cc
+ pollable.cc
+ shared_buffer.cc
+ )
+target_include_directories(lnavfileio PRIVATE . ${CMAKE_CURRENT_BINARY_DIR})
+target_link_libraries(lnavfileio cppfmt spookyhash pcrepp base BZip2::BZip2 ZLIB::ZLIB)
+
+add_library(
+ diag STATIC
+ ${GEN_SRCS}
+ config.h.in
+ all_logs_vtab.cc
+ archive_manager.cc
+ document.sections.cc
+ bin2c.hh
+ bookmarks.cc
+ bottom_status_source.cc
+ breadcrumb_curses.cc
+ collation-functions.cc
+ column_namer.cc
+ command_executor.cc
+ curl_looper.cc
+ db_sub_source.cc
+ dump_internals.cc
+ elem_to_json.cc
+ environ_vtab.cc
+ extension-functions.cc
+ field_overlay_source.cc
+ file_collection.cc
+ file_format.cc
+ file_vtab.cc
+ files_sub_source.cc
+ filter_observer.cc
+ filter_status_source.cc
+ filter_sub_source.cc
+ fs-extension-functions.cc
+ fstat_vtab.cc
+ fts_fuzzy_match.cc
+ help_text.cc
+ help_text_formatter.cc
+ highlighter.cc
+ hist_source.cc
+ hotkeys.cc
+ input_dispatcher.cc
+ json-extension-functions.cc
+ listview_curses.cc
+ lnav.events.cc
+ lnav.indexing.cc
+ lnav.management_cli.cc
+ lnav_commands.cc
+ lnav_config.cc
+ lnav_util.cc
+ log.watch.cc
+ log_accel.cc
+ log_actions.cc
+ log_data_helper.cc
+ log_data_table.cc
+ log_format.cc
+ log_format_loader.cc
+ log_level.cc
+ log_search_table.cc
+ logfile.cc
+ logfile_sub_source.cc
+ md2attr_line.cc
+ md4cpp.cc
+ network-extension-functions.cc
+ data_scanner.cc
+ data_scanner_re.cc
+ data_parser.cc
+ pcap_manager.cc
+ plain_text_source.cc
+ pretty_printer.cc
+ pugixml/pugixml.cpp
+ readline_callbacks.cc
+ readline_curses.cc
+ readline_highlighters.cc
+ readline_possibilities.cc
+ regexp_vtab.cc
+ regex101.client.cc
+ regex101.import.cc
+ relative_time.cc
+ session.export.cc
+ session_data.cc
+ sequence_matcher.cc
+ shlex.cc
+ sqlite-extension-func.cc
+ static_file_vtab.cc
+ statusview_curses.cc
+ string-extension-functions.cc
+ sysclip.cc
+ piper_proc.cc
+ spectro_impls.cc
+ spectro_source.cc
+ sql_commands.cc
+ sql_util.cc
+ sqlitepp.cc
+ state-extension-functions.cc
+ styling.cc
+ text_anonymizer.cc
+ text_format.cc
+ textfile_highlighters.cc
+ textfile_sub_source.cc
+ textview_curses.cc
+ top_status_source.cc
+ time-extension-functions.cc
+ timer.cc
+ unique_path.cc
+ unique_path.hh
+ view_curses.cc
+ view_helpers.cc
+ views_vtab.cc
+ vt52_curses.cc
+ vtab_module.cc
+ log_vtab_impl.cc
+ xml_util.cc
+ xpath_vtab.cc
+ xterm_mouse.cc
+ yaml-extension-functions.cc
+ third-party/md4c/md4c.c
+ third-party/sqlite/ext/series.c
+ third-party/sqlite/ext/dbdump.c
+
+ all_logs_vtab.hh
+ archive_manager.hh
+ archive_manager.cfg.hh
+ document.sections.hh
+ big_array.hh
+ bottom_status_source.hh
+ bound_tags.hh
+ breadcrumb.hh
+ breadcrumb_curses.hh
+ byte_array.hh
+ command_executor.hh
+ column_namer.hh
+ curl_looper.hh
+ doc_status_source.hh
+ dump_internals.hh
+ elem_to_json.hh
+ field_overlay_source.hh
+ file_collection.hh
+ file_format.hh
+ files_sub_source.hh
+ filter_observer.hh
+ filter_status_source.hh
+ filter_sub_source.hh
+ fstat_vtab.hh
+ fts_fuzzy_match.hh
+ grep_highlighter.hh
+ help_text.hh
+ help_text_formatter.hh
+ highlighter.hh
+ hotkeys.hh
+ input_dispatcher.hh
+ itertools.similar.hh
+ k_merge_tree.h
+ lnav.events.hh
+ lnav.indexing.hh
+ lnav.management_cli.hh
+ lnav_config.hh
+ lnav_config_fwd.hh
+ lnav_util.hh
+ log.watch.hh
+ log_actions.hh
+ log_data_helper.hh
+ log_data_table.hh
+ log_format.hh
+ log_format_ext.hh
+ log_format_fwd.hh
+ log_format_impls.cc
+ log_gutter_source.hh
+ log_level.hh
+ log_search_table.hh
+ log_search_table_fwd.hh
+ logfile_sub_source.cfg.hh
+ logfile.hh
+ logfile_fwd.hh
+ logfile_stats.hh
+ md2attr_line.hh
+ md4cpp.hh
+ optional.hpp
+ pcap_manager.hh
+ plain_text_source.hh
+ pretty_printer.hh
+ preview_status_source.hh
+ pugixml/pugiconfig.hpp
+ pugixml/pugixml.hpp
+ readline_callbacks.hh
+ readline_context.hh
+ readline_possibilities.hh
+ regex101.client.hh
+ regex101.import.hh
+ regexp_vtab.hh
+ relative_time.hh
+ styling.hh
+ ring_span.hh
+ safe/accessmode.h
+ safe/defaulttypes.h
+ safe/mutableref.h
+ safe/safe.h
+ session.export.hh
+ sequence_sink.hh
+ shlex.hh
+ shlex.resolver.hh
+ simdutf8check.h
+ spectro_impls.hh
+ spectro_source.hh
+ sqlitepp.hh
+ sql_help.hh
+ sql_util.hh
+ static_file_vtab.hh
+ strong_int.hh
+ sysclip.hh
+ sysclip.cfg.hh
+ term_extra.hh
+ termios_guard.hh
+ text_anonymizer.hh
+ text_format.hh
+ textfile_highlighters.hh
+ textfile_sub_source.hh
+ textview_curses.hh
+ textview_curses_fwd.hh
+ time_T.hh
+ timer.hh
+ top_status_source.hh
+ url_loader.hh
+ view_helpers.hh
+ view_helpers.crumbs.hh
+ view_helpers.examples.hh
+ view_helpers.hist.hh
+ views_vtab.hh
+ vis_line.hh
+ vtab_module.hh
+ vtab_module_json.hh
+ xml_util.hh
+ xpath_vtab.hh
+ mapbox/recursive_wrapper.hpp
+ mapbox/variant.hpp
+ mapbox/variant_io.hpp
+ mapbox/variant_visitor.hpp
+ ghc/filesystem.hpp
+ ghc/fs_fwd.hpp
+ ghc/fs_impl.hpp
+ ghc/fs_std.hpp
+ ghc/fs_std_fwd.hpp
+ ghc/fs_std_impl.hpp
+ ww898/cp_utf8.hpp
+ log_level_re.cc
+
+ third-party/ArenaAlloc/arenaalloc.h
+ third-party/ArenaAlloc/arenaallocimpl.h
+
+ third-party/CLI/StringTools.hpp
+ third-party/CLI/App.hpp
+ third-party/CLI/Macros.hpp
+ third-party/CLI/Option.hpp
+ third-party/CLI/Config.hpp
+ third-party/CLI/CLI.hpp
+ third-party/CLI/Formatter.hpp
+ third-party/CLI/Error.hpp
+ third-party/CLI/Version.hpp
+ third-party/CLI/Timer.hpp
+ third-party/CLI/FormatterFwd.hpp
+ third-party/CLI/Validators.hpp
+ third-party/CLI/Split.hpp
+ third-party/CLI/TypeTools.hpp
+ third-party/CLI/ConfigFwd.hpp
+
+ third-party/intervaltree/IntervalTree.h
+
+ third-party/md4c/md4c.h
+
+ third-party/robin_hood/robin_hood.h
+)
+
+set(lnav_SRCS lnav.cc)
+
+target_include_directories(diag PUBLIC . fmtlib ${CMAKE_CURRENT_BINARY_DIR}
+ third-party
+ third-party/base64/include
+ third-party/rapidyaml
+ )
+
+target_link_libraries(
+ diag
+ base
+ lnavdt
+ lnavfileio
+ pcrepp
+ tailerservice
+ tailerpp
+ tailercommon
+ logfmt
+ yajlpp
+ cppfmt
+ base64
+ spookyhash
+ ${lnav_LIBS})
+target_compile_definitions(diag PRIVATE SQLITE_OMIT_LOAD_EXTENSION)
+
+check_library_exists(util openpty "" HAVE_LIBUTIL)
+
+if (HAVE_LIBUTIL)
+ target_link_libraries(diag util)
+endif ()
+
+add_executable(lnav ${lnav_SRCS})
+target_link_libraries(lnav diag)
+
+install(TARGETS lnav DESTINATION bin)
diff --git a/src/Makefile.am b/src/Makefile.am
new file mode 100644
index 0000000..24eaaf1
--- /dev/null
+++ b/src/Makefile.am
@@ -0,0 +1,548 @@
+
+include $(top_srcdir)/aminclude_static.am
+
+CXXFLAGS =
+
+SUBDIRS = \
+ fmtlib \
+ third-party/base64/lib \
+ third-party/scnlib/src \
+ pcrepp \
+ base \
+ tailer \
+ pugixml \
+ yajl \
+ yajlpp \
+ formats/logfmt \
+ .
+
+bin_PROGRAMS = lnav
+
+noinst_PROGRAMS = lnav-test
+
+noinst_LIBRARIES = libdiag.a libdatascanner.a
+
+PTIME_V = $(PTIME_V_@AM_V@)
+PTIME_V_ = $(PTIME_V_@AM_DEFAULT_V@)
+PTIME_V_0 = @echo " TIMEFMT " $@;
+
+BIN2C_V = $(BIN2C_V_@AM_V@)
+BIN2C_V_ = $(BIN2C_V_@AM_DEFAULT_V@)
+BIN2C_V_0 = @echo " BIN2C " $@;
+
+RE2C_V = $(RE2C_V_@AM_V@)
+RE2C_V_ = $(RE2C_V_@AM_DEFAULT_V@)
+RE2C_V_0 = @echo " RE2C " $@;
+
+BIN2C_PATH = ../tools/bin2c$(BUILD_EXEEXT)
+
+include formats/formats.am
+
+default-formats.cc: $(BIN2C_PATH) $(FORMAT_FILES)
+ $(BIN2C_V)$(BIN2C_PATH) -n lnav_format_json default-formats $(FORMAT_FILES)
+
+include keymaps/keymaps.am
+include themes/themes.am
+
+CONFIG_FILES = \
+ $(srcdir)/root-config.json \
+ $(KEYMAP_FILES) \
+ $(THEME_FILES) \
+ $()
+
+default-config.cc: $(BIN2C_PATH) $(CONFIG_FILES)
+ $(BIN2C_V)$(BIN2C_PATH) -n lnav_config_json default-config $(CONFIG_FILES)
+
+include scripts/scripts.am
+
+builtin-scripts.cc: $(BIN2C_PATH) $(BUILTIN_LNAVSCRIPTS)
+ $(BIN2C_V)$(BIN2C_PATH) -n lnav_scripts builtin-scripts $(BUILTIN_LNAVSCRIPTS)
+
+builtin-sh-scripts.cc: $(BIN2C_PATH) $(BUILTIN_SHSCRIPTS)
+ $(BIN2C_V)$(BIN2C_PATH) -n lnav_sh_scripts builtin-sh-scripts $(BUILTIN_SHSCRIPTS)
+
+%-sh.cc: $(srcdir)/%.sh $(BIN2C_PATH)
+ $(BIN2C_V)$(BIN2C_PATH) $(*)-sh $<
+
+%-txt.cc: $(srcdir)/%.txt $(BIN2C_PATH)
+ $(BIN2C_V)$(BIN2C_PATH) $(*)-txt $<
+
+%-md.cc: $(srcdir)/%.md $(BIN2C_PATH)
+ $(BIN2C_V)$(BIN2C_PATH) $(*)-md $<
+
+%-sql.cc: $(srcdir)/%.sql $(BIN2C_PATH)
+ $(BIN2C_V)$(BIN2C_PATH) $(*)-sql $<
+
+%-lnav.cc: $(srcdir)/%.lnav $(BIN2C_PATH)
+ $(BIN2C_V)$(BIN2C_PATH) $(*)-lnav $<
+
+%-json.cc: $(srcdir)/%.json $(BIN2C_PATH)
+ $(BIN2C_V)$(BIN2C_PATH) $(*)-json $<
+
+include time_formats.am
+
+time_fmts.cc: ptimec$(BUILD_EXEEXT)
+ $(PTIME_V)./ptimec$(BUILD_EXEEXT) $(TIME_FORMATS) > $@
+
+if HAVE_RE2C
+%.cc: %.re
+ $(RE2C_V)$(RE2C_CMD) --bit-vectors -W --tags -8 -o $@ $<
+ $(REC2_V)test $@ -ef $(srcdir)/$*.cc || cp $@ $(srcdir)/$*.cc
+endif
+
+LNAV_BUILT_FILES = \
+ animals-json.cc \
+ ansi-palette-json.cc \
+ builtin-scripts.cc \
+ builtin-sh-scripts.cc \
+ default-config.cc \
+ default-formats.cc \
+ diseases-json.cc \
+ emojis-json.cc \
+ words-json.cc \
+ help-md.cc \
+ init-sql.cc \
+ time_fmts.cc \
+ xml-entities-json.cc \
+ xterm-palette-json.cc
+
+BUILT_SOURCES = $(LNAV_BUILT_FILES)
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS) $(USER_CXXFLAGS)
+
+AM_LDFLAGS = \
+ $(STATIC_LDFLAGS) \
+ $(LIBARCHIVE_LDFLAGS) \
+ $(READLINE_LDFLAGS) \
+ $(SQLITE3_LDFLAGS) \
+ $(PCRE_LDFLAGS)
+
+AM_CPPFLAGS = \
+ -DSYSCONFDIR='"$(sysconfdir)"' \
+ -DSQLITE_OMIT_LOAD_EXTENSION \
+ -I$(srcdir)/fmtlib \
+ -I$(srcdir)/third-party \
+ -I$(srcdir)/third-party/base64/include \
+ -I$(srcdir)/third-party/rapidyaml \
+ -I$(top_srcdir)/src/third-party/scnlib/include \
+ -Wall \
+ $(CODE_COVERAGE_CPPFLAGS) \
+ $(LIBARCHIVE_CFLAGS) \
+ $(READLINE_CFLAGS) \
+ $(SQLITE3_CFLAGS) \
+ $(PCRE_CFLAGS) \
+ $(LIBCURL_CPPFLAGS)
+
+LDADD = \
+ libdiag.a \
+ libdatascanner.a \
+ base/libbase.a \
+ formats/logfmt/liblogfmt.a \
+ fmtlib/libcppfmt.a \
+ third-party/scnlib/src/libscnlib.a \
+ pcrepp/libpcrepp.a \
+ pugixml/libpugixml.a \
+ tailer/libtailerservice.a \
+ tailer/libtailercommon.a \
+ tailer/libtailerpp.a \
+ yajl/libyajl.a \
+ yajlpp/libyajlpp.a \
+ third-party/base64/lib/libbase64.a \
+ $(READLINE_LIBS) \
+ $(CURSES_LIB) \
+ $(SQLITE3_LIBS) \
+ $(LIBARCHIVE_LIBS) \
+ $(LIBCURL)
+
+# emojis.json is from https://gist.github.com/oliveratgithub/0bf11a9aff0d6da7b46f1490f86a71eb/
+# xml-entities.json is from https://html.spec.whatwg.org/entities.json
+
+dist_noinst_DATA = \
+ alpha-release.sh \
+ animals.json \
+ ansi-palette.json \
+ diseases.json \
+ emojis.json \
+ $(BUILTIN_LNAVSCRIPTS) \
+ $(BUILTIN_SHSCRIPTS) \
+ $(CONFIG_FILES) \
+ $(FORMAT_FILES) \
+ words.json \
+ xml-entities.json \
+ xterm-palette.json
+
+noinst_HEADERS = \
+ third-party/md4c/md4c.h \
+ third-party/rapidyaml/ryml_all.hpp \
+ all_logs_vtab.hh \
+ archive_manager.hh \
+ archive_manager.cfg.hh \
+ big_array.hh \
+ bin2c.hh \
+ bookmarks.hh \
+ bottom_status_source.hh \
+ bound_tags.hh \
+ breadcrumb.hh \
+ breadcrumb_curses.hh \
+ byte_array.hh \
+ column_namer.hh \
+ command_executor.hh \
+ curl_looper.hh \
+ data_scanner.hh \
+ data_scanner_re.re \
+ data_parser.hh \
+ db_sub_source.hh \
+ doc_status_source.hh \
+ document.sections.hh \
+ dump_internals.hh \
+ elem_to_json.hh \
+ environ_vtab.hh \
+ field_overlay_source.hh \
+ file_collection.hh \
+ file_format.hh \
+ file_vtab.cfg.hh \
+ files_sub_source.hh \
+ filter_observer.hh \
+ filter_status_source.hh \
+ filter_sub_source.hh \
+ fstat_vtab.hh \
+ fts_fuzzy_match.hh \
+ grep_highlighter.hh \
+ grep_proc.hh \
+ help.md \
+ help.txt \
+ help_text.hh \
+ help_text_formatter.hh \
+ highlighter.hh \
+ hist_source.hh \
+ hotkeys.hh \
+ init.sql \
+ input_dispatcher.hh \
+ itertools.similar.hh \
+ k_merge_tree.h \
+ line_buffer.hh \
+ listview_curses.hh \
+ lnav.hh \
+ lnav.events.hh \
+ lnav.indexing.hh \
+ lnav.management_cli.hh \
+ lnav_commands.hh \
+ lnav_config.hh \
+ lnav_config_fwd.hh \
+ lnav_util.hh \
+ log.watch.hh \
+ log_accel.hh \
+ log_actions.hh \
+ log_data_helper.hh \
+ log_data_table.hh \
+ log_format.hh \
+ log_format_ext.hh \
+ log_format_fwd.hh \
+ log_format_loader.hh \
+ log_gutter_source.hh \
+ log_level.hh \
+ log_level_re.re \
+ log_search_table.hh \
+ log_search_table_fwd.hh \
+ logfile.hh \
+ logfile.cfg.hh \
+ logfile_fwd.hh \
+ logfile_sub_source.hh \
+ logfile_sub_source.cfg.hh \
+ mapbox/recursive_wrapper.hpp \
+ mapbox/variant.hpp \
+ mapbox/variant_io.hpp \
+ mapbox/variant_visitor.hpp \
+ md2attr_line.hh \
+ md4cpp.hh \
+ optional.hpp \
+ pcap_manager.hh \
+ piper_proc.hh \
+ plain_text_source.hh \
+ pollable.hh \
+ pretty_printer.hh \
+ preview_status_source.hh \
+ ptimec.hh \
+ readline_callbacks.hh \
+ readline_context.hh \
+ readline_curses.hh \
+ readline_highlighters.hh \
+ readline_possibilities.hh \
+ regex101.client.hh \
+ regex101.import.hh \
+ regexp_vtab.hh \
+ relative_time.hh \
+ ring_span.hh \
+ safe/accessmode.h \
+ safe/defaulttypes.h \
+ safe/mutableref.h \
+ safe/safe.h \
+ service_tags.hh \
+ session.export.hh \
+ session_data.hh \
+ shared_buffer.hh \
+ shlex.hh \
+ shlex.resolver.hh \
+ simdutf8check.h \
+ spectro_impls.hh \
+ spectro_source.hh \
+ sqlitepp.hh \
+ sqlitepp.client.hh \
+ sql_help.hh \
+ sql_util.hh \
+ sqlite-extension-func.hh \
+ static_file_vtab.hh \
+ styling.hh \
+ statusview_curses.hh \
+ strong_int.hh \
+ sysclip.hh \
+ sysclip.cfg.hh \
+ termios_guard.hh \
+ term_extra.hh \
+ text_anonymizer.hh \
+ text_format.hh \
+ textfile_highlighters.hh \
+ textfile_sub_source.hh \
+ textview_curses.hh \
+ textview_curses_fwd.hh \
+ time_T.hh \
+ timer.hh \
+ top_status_source.hh \
+ top_status_source.cfg.hh \
+ unique_path.hh \
+ url_loader.hh \
+ view_curses.hh \
+ view_helpers.hh \
+ view_helpers.crumbs.hh \
+ view_helpers.examples.hh \
+ view_helpers.hist.hh \
+ views_vtab.hh \
+ vis_line.hh \
+ vt52_curses.hh \
+ vtab_module.hh \
+ vtab_module_json.hh \
+ log_vtab_impl.hh \
+ log_format_impls.cc \
+ xml_util.hh \
+ xpath_vtab.hh \
+ xterm_mouse.hh \
+ spookyhash/SpookyV2.h \
+ ghc/filesystem.hpp \
+ ghc/fs_fwd.hpp \
+ ghc/fs_impl.hpp \
+ ghc/fs_std.hpp \
+ ghc/fs_std_fwd.hpp \
+ ghc/fs_std_impl.hpp \
+ ww898/cp_utf8.hpp
+
+nodist_libdiag_a_SOURCES = \
+ $(LNAV_BUILT_FILES)
+
+THIRD_PARTY_SRCS = \
+ third-party/ArenaAlloc/arenaalloc.h \
+ third-party/ArenaAlloc/arenaallocimpl.h \
+ third-party/ArenaAlloc/recyclealloc.h \
+ third-party/backward-cpp/backward.hpp \
+ third-party/CLI/StringTools.hpp \
+ third-party/CLI/App.hpp \
+ third-party/CLI/Macros.hpp \
+ third-party/CLI/Option.hpp \
+ third-party/CLI/Config.hpp \
+ third-party/CLI/CLI.hpp \
+ third-party/CLI/Formatter.hpp \
+ third-party/CLI/Error.hpp \
+ third-party/CLI/Version.hpp \
+ third-party/CLI/Timer.hpp \
+ third-party/CLI/FormatterFwd.hpp \
+ third-party/CLI/Validators.hpp \
+ third-party/CLI/Split.hpp \
+ third-party/CLI/TypeTools.hpp \
+ third-party/CLI/ConfigFwd.hpp \
+ third-party/doctest-root/doctest/doctest.h \
+ third-party/intervaltree/IntervalTree.h \
+ third-party/md4c/md4c.c \
+ third-party/robin_hood/robin_hood.h \
+ third-party/sqlite/ext/dbdump.c \
+ third-party/sqlite/ext/series.c
+
+libdatascanner_a_SOURCES = \
+ data_scanner.cc \
+ data_scanner_re.cc
+
+libdiag_a_SOURCES = \
+ $(THIRD_PARTY_SRCS) \
+ all_logs_vtab.cc \
+ archive_manager.cc \
+ bookmarks.cc \
+ bottom_status_source.cc \
+ breadcrumb_curses.cc \
+ collation-functions.cc \
+ column_namer.cc \
+ command_executor.cc \
+ curl_looper.cc \
+ db_sub_source.cc \
+ document.sections.cc \
+ dump_internals.cc \
+ elem_to_json.cc \
+ environ_vtab.cc \
+ extension-functions.cc \
+ field_overlay_source.cc \
+ file_collection.cc \
+ file_format.cc \
+ files_sub_source.cc \
+ filter_observer.cc \
+ filter_status_source.cc \
+ filter_sub_source.cc \
+ fstat_vtab.cc \
+ fs-extension-functions.cc \
+ fts_fuzzy_match.cc \
+ grep_proc.cc \
+ help_text.cc \
+ help_text_formatter.cc \
+ highlighter.cc \
+ hist_source.cc \
+ hotkeys.cc \
+ input_dispatcher.cc \
+ json-extension-functions.cc \
+ line_buffer.cc \
+ listview_curses.cc \
+ lnav_commands.cc \
+ lnav_config.cc \
+ lnav_util.cc \
+ log.watch.cc \
+ log_accel.cc \
+ log_actions.cc \
+ log_data_helper.cc \
+ log_data_table.cc \
+ log_format.cc \
+ log_format_loader.cc \
+ log_level.cc \
+ log_level_re.cc \
+ log_search_table.cc \
+ logfile.cc \
+ logfile_sub_source.cc \
+ md2attr_line.cc \
+ md4cpp.cc \
+ network-extension-functions.cc \
+ data_parser.cc \
+ pcap_manager.cc \
+ plain_text_source.cc \
+ pollable.cc \
+ pretty_printer.cc \
+ ptimec_rt.cc \
+ readline_callbacks.cc \
+ readline_curses.cc \
+ readline_highlighters.cc \
+ readline_possibilities.cc \
+ regex101.client.cc \
+ regex101.import.cc \
+ regexp_vtab.cc \
+ relative_time.cc \
+ session.export.cc \
+ session_data.cc \
+ shared_buffer.cc \
+ shlex.cc \
+ spectro_impls.cc \
+ spectro_source.cc \
+ sqlitepp.cc \
+ sqlite-extension-func.cc \
+ static_file_vtab.cc \
+ statusview_curses.cc \
+ string-extension-functions.cc \
+ styling.cc \
+ text_anonymizer.cc \
+ text_format.cc \
+ textfile_sub_source.cc \
+ timer.cc \
+ piper_proc.cc \
+ sql_commands.cc \
+ sql_util.cc \
+ state-extension-functions.cc \
+ sysclip.cc \
+ textfile_highlighters.cc \
+ textview_curses.cc \
+ time-extension-functions.cc \
+ top_status_source.cc \
+ unique_path.cc \
+ view_curses.cc \
+ view_helpers.cc \
+ views_vtab.cc \
+ vt52_curses.cc \
+ vtab_module.cc \
+ log_vtab_impl.cc \
+ xml_util.cc \
+ xpath_vtab.cc \
+ xterm_mouse.cc \
+ yaml-extension-functions.cc \
+ spookyhash/SpookyV2.cpp
+
+PLUGIN_SRCS = \
+ file_vtab.cc
+
+lnav_SOURCES = \
+ lnav.cc \
+ lnav.events.cc \
+ lnav.indexing.cc \
+ lnav.management_cli.cc \
+ $(PLUGIN_SRCS)
+
+lnav_test_SOURCES = \
+ lnav.cc \
+ lnav.events.cc \
+ lnav.indexing.cc \
+ lnav.management_cli.cc \
+ test_override.c \
+ $(PLUGIN_SRCS)
+
+ptimec$(BUILD_EXEEXT): ptimec.c
+ $(AM_V_CC) $(CC_FOR_BUILD) $(CPPFLAGS_FOR_BUILD) $(LDFLAGS_FOR_BUILD) -g3 -o $@ $?
+
+if HAVE_RE2C
+RE2C_FILES = data_scanner_re.cc log_level_re.cc
+endif
+
+EXTRA_DIST = \
+ ptimec.c
+
+CLEANFILES = \
+ ptimec$(BUILD_EXEEXT)
+
+DISTCLEANFILES = \
+ $(LNAV_BUILT_FILES) \
+ animals-json.h \
+ ansi-palette-json.h \
+ builtin-scripts.h \
+ builtin-sh-scripts.h \
+ default-config.h \
+ default-formats.h \
+ diseases-json.h \
+ emojis-json.h \
+ words-json.h \
+ help-md.h \
+ init-sql.h \
+ time_fmts.h \
+ xml-entities-json.h \
+ xterm-palette-json.h \
+ $(RE2C_FILES)
+
+distclean-local:
+ $(RM_V)rm -rf *.dSYM
+
+uncrusty:
+ (cd $(srcdir) && uncrustify -c ../lnav.cfg --replace $(SOURCES) \
+ $(HEADERS))
+
+if !DISABLE_DOCUMENTATION
+all-local: $(LNAV_BUILT_FILES) lnav
+ if test -w $(srcdir)/internals; then \
+ env DUMP_INTERNALS_DIR=$(srcdir)/internals DUMP_CRASH=1 ./lnav Makefile; \
+ mv $(srcdir)/internals/*.schema.json $(top_srcdir)/docs/schemas; \
+ fi
+else
+all-local: $(LNAV_BUILT_FILES)
+endif
+
+install-exec-hook:
+ bash $(srcdir)/alpha-release.sh
diff --git a/src/all_logs_vtab.cc b/src/all_logs_vtab.cc
new file mode 100644
index 0000000..f4468a6
--- /dev/null
+++ b/src/all_logs_vtab.cc
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "all_logs_vtab.hh"
+
+#include "base/attr_line.hh"
+#include "config.h"
+
+static auto intern_lifetime = intern_string::get_table_lifetime();
+
+all_logs_vtab::all_logs_vtab()
+ : log_vtab_impl(intern_string::lookup("all_logs")),
+ alv_msg_meta(
+ intern_string::lookup("log_msg_format"), value_kind_t::VALUE_TEXT, 0),
+ alv_schema_meta(
+ intern_string::lookup("log_msg_schema"), value_kind_t::VALUE_TEXT, 1)
+{
+ this->alv_msg_meta.lvm_identifier = true;
+ this->alv_schema_meta.lvm_identifier = true;
+}
+
+void
+all_logs_vtab::get_columns(std::vector<vtab_column>& cols) const
+{
+ cols.emplace_back(
+ vtab_column(this->alv_msg_meta.lvm_name.get())
+ .with_comment(
+ "The message format with variables replaced by hash marks"));
+ cols.emplace_back(this->alv_schema_meta.lvm_name.get(),
+ SQLITE3_TEXT,
+ "",
+ true,
+ "The ID for the message schema");
+}
+
+void
+all_logs_vtab::extract(logfile* lf,
+ uint64_t line_number,
+ logline_value_vector& values)
+{
+ auto& line = values.lvv_sbr;
+ auto* format = lf->get_format_ptr();
+
+ logline_value_vector sub_values;
+
+ this->vi_attrs.clear();
+ sub_values.lvv_sbr = line;
+ format->annotate(line_number, this->vi_attrs, sub_values, false);
+
+ auto body = find_string_attr_range(this->vi_attrs, &SA_BODY);
+ if (body.lr_start == -1) {
+ body.lr_start = 0;
+ body.lr_end = line.length();
+ }
+
+ data_scanner ds(
+ line.to_string_fragment().sub_range(body.lr_start, body.lr_end));
+ data_parser dp(&ds);
+ std::string str;
+
+ dp.dp_msg_format = &str;
+ dp.parse();
+
+ values.lvv_values.emplace_back(this->alv_msg_meta, std::move(str));
+ values.lvv_values.emplace_back(this->alv_schema_meta,
+ dp.dp_schema_id.to_string());
+}
+
+bool
+all_logs_vtab::next(log_cursor& lc, logfile_sub_source& lss)
+{
+ return true;
+}
diff --git a/src/all_logs_vtab.hh b/src/all_logs_vtab.hh
new file mode 100644
index 0000000..cab0a4d
--- /dev/null
+++ b/src/all_logs_vtab.hh
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_all_logs_vtab_hh
+#define lnav_all_logs_vtab_hh
+
+#include <array>
+
+#include "data_parser.hh"
+#include "log_vtab_impl.hh"
+
+/**
+ * A virtual table that provides access to all log messages from all formats.
+ *
+ * @feature f0:sql.tables.all_logs
+ */
+class all_logs_vtab : public log_vtab_impl {
+public:
+ all_logs_vtab();
+
+ void get_columns(std::vector<vtab_column>& cols) const override;
+
+ void extract(logfile* lf,
+ uint64_t line_number,
+ logline_value_vector& values) override;
+
+ bool next(log_cursor& lc, logfile_sub_source& lss) override;
+
+private:
+ logline_value_meta alv_msg_meta;
+ logline_value_meta alv_schema_meta;
+};
+
+#endif // LNAV_ALL_LOGS_VTAB_HH
diff --git a/src/alpha-release.sh b/src/alpha-release.sh
new file mode 100755
index 0000000..d0104ab
--- /dev/null
+++ b/src/alpha-release.sh
@@ -0,0 +1,13 @@
+#!/bin/bash
+
+
+if test x"${TRAVIS_BUILD_DIR}" == x""; then
+ exit 0
+fi
+
+cp lnav ${TRAVIS_BUILD_DIR}/
+cd ${TRAVIS_BUILD_DIR}
+
+ldd lnav
+VERSION=`git describe --tags`
+zip lnav-${VERSION}-linux-64bit.zip lnav
diff --git a/src/animals.json b/src/animals.json
new file mode 100644
index 0000000..46d2e3d
--- /dev/null
+++ b/src/animals.json
@@ -0,0 +1 @@
+{"data":["meerkat","aardvark","addax","alligator","alpaca","anteater","antelope","aoudad","ape","argali","armadillo","baboon","badger","basilisk","bat","bear","beaver","bighorn","bison","boar","budgerigar","buffalo","bull","bunny","burro","camel","canary","capybara","cat","chameleon","chamois","cheetah","chimpanzee","chinchilla","chipmunk","civet","coati","colt","cougar","cow","coyote","crocodile","crow","deer","dingo","doe","dung-beetle","dog","donkey","dormouse","dromedary","duckbill-platypus","dugong","eland","elephant","elk","ermine","ewe","fawn","ferret","finch","fish","fox","frog","gazelle","gemsbok","gila-monster","giraffe","gnu","goat","gopher","gorilla","grizzly-bear","ground-hog","guanaco","guinea-pig","hamster","hare","hartebeest","hedgehog","highland-cow","hippopotamus","hog","horse","hyena","ibex","iguana","impala","jackal","jaguar","jerboa","kangaroo","kitten","koala","lamb","lemur","leopard","lion","lizard","llama","lovebird","lynx","mandrill","mare","marmoset","marten","mink","mole","mongoose","monkey","moose","mountain-goat","mouse","mule","musk-deer","musk-ox","muskrat","mustang","mynah-bird","newt","ocelot","okapi","opossum","orangutan","oryx","otter","ox","panda","panther","parakeet","parrot","peccary","pig","octopus","thorny-devil","starfish","blue-crab","snowy-owl","chicken","rooster","bumble-bee","eagle-owl","polar-bear","pony","porcupine","porpoise","prairie-dog","pronghorn","puma","puppy","quagga","rabbit","raccoon","ram","rat","reindeer","rhinoceros","salamander","seal","sheep","shrew","silver-fox","skunk","sloth","snake","springbok","squirrel","stallion","steer","tapir","tiger","toad","turtle","vicuna","walrus","warthog","waterbuck","weasel","whale","wildcat","bald-eagle","wolf","wolverine","wombat","woodchuck","yak","zebra","zebu"]} \ No newline at end of file
diff --git a/src/ansi-palette.json b/src/ansi-palette.json
new file mode 100644
index 0000000..f419ba6
--- /dev/null
+++ b/src/ansi-palette.json
@@ -0,0 +1,122 @@
+[
+ {
+ "colorId": 0,
+ "hexString": "#000000",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 0
+ },
+ "name": "Black"
+ },
+ {
+ "colorId": 1,
+ "hexString": "#800000",
+ "rgb": {
+ "r": 128,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Maroon"
+ },
+ {
+ "colorId": 2,
+ "hexString": "#008000",
+ "rgb": {
+ "r": 0,
+ "g": 128,
+ "b": 0
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Green"
+ },
+ {
+ "colorId": 3,
+ "hexString": "#808000",
+ "rgb": {
+ "r": 128,
+ "g": 128,
+ "b": 0
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Olive"
+ },
+ {
+ "colorId": 4,
+ "hexString": "#000080",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 128
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Navy"
+ },
+ {
+ "colorId": 5,
+ "hexString": "#800080",
+ "rgb": {
+ "r": 128,
+ "g": 0,
+ "b": 128
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Purple"
+ },
+ {
+ "colorId": 6,
+ "hexString": "#008080",
+ "rgb": {
+ "r": 0,
+ "g": 128,
+ "b": 128
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Teal"
+ },
+ {
+ "colorId": 7,
+ "hexString": "#c0c0c0",
+ "rgb": {
+ "r": 192,
+ "g": 192,
+ "b": 192
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 75
+ },
+ "name": "Silver"
+ }
+] \ No newline at end of file
diff --git a/src/archive_manager.cc b/src/archive_manager.cc
new file mode 100644
index 0000000..705842e
--- /dev/null
+++ b/src/archive_manager.cc
@@ -0,0 +1,406 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file archive_manager.cc
+ */
+
+#include <unistd.h>
+
+#include "config.h"
+
+#if HAVE_ARCHIVE_H
+# include "archive.h"
+# include "archive_entry.h"
+#endif
+
+#include "archive_manager.cfg.hh"
+#include "archive_manager.hh"
+#include "base/auto_fd.hh"
+#include "base/auto_mem.hh"
+#include "base/fs_util.hh"
+#include "base/humanize.hh"
+#include "base/injector.hh"
+#include "base/lnav_log.hh"
+#include "base/paths.hh"
+#include "fmt/format.h"
+#include "lnav_util.hh"
+
+namespace fs = ghc::filesystem;
+
+namespace archive_manager {
+
+#if HAVE_ARCHIVE_H
+/**
+ * Enables a subset of the supported archive formats to speed up detection,
+ * since some formats, like xar are unlikely to be used.
+ */
+static void
+enable_desired_archive_formats(archive* arc)
+{
+ /** @feature f0:archive.formats */
+ archive_read_support_format_7zip(arc);
+ archive_read_support_format_cpio(arc);
+ archive_read_support_format_lha(arc);
+ archive_read_support_format_rar(arc);
+ archive_read_support_format_tar(arc);
+ archive_read_support_format_zip(arc);
+}
+#endif
+
+bool
+is_archive(const fs::path& filename)
+{
+#if HAVE_ARCHIVE_H
+ auto_mem<archive> arc(archive_read_free);
+
+ arc = archive_read_new();
+
+ archive_read_support_filter_all(arc);
+ enable_desired_archive_formats(arc);
+ archive_read_support_format_raw(arc);
+ log_debug("read open %s", filename.c_str());
+ auto r = archive_read_open_filename(arc, filename.c_str(), 128 * 1024);
+ if (r == ARCHIVE_OK) {
+ struct archive_entry* entry = nullptr;
+
+ const auto* format_name = archive_format_name(arc);
+
+ log_debug("read next header %s %s", format_name, filename.c_str());
+ if (archive_read_next_header(arc, &entry) == ARCHIVE_OK) {
+ log_debug("read next done %s", filename.c_str());
+
+ static const auto RAW_FORMAT_NAME = string_fragment("raw");
+ static const auto GZ_FILTER_NAME = string_fragment("gzip");
+
+ format_name = archive_format_name(arc);
+
+ if (RAW_FORMAT_NAME == format_name) {
+ auto filter_count = archive_filter_count(arc);
+
+ if (filter_count == 1) {
+ return false;
+ }
+
+ const auto* first_filter_name = archive_filter_name(arc, 0);
+ if (filter_count == 2 && GZ_FILTER_NAME == first_filter_name) {
+ return false;
+ }
+ }
+ log_info(
+ "detected archive: %s -- %s", filename.c_str(), format_name);
+ return true;
+ }
+
+ log_info("archive read header failed: %s -- %s",
+ filename.c_str(),
+ archive_error_string(arc));
+ } else {
+ log_info("archive open failed: %s -- %s",
+ filename.c_str(),
+ archive_error_string(arc));
+ }
+#endif
+
+ return false;
+}
+
+static fs::path
+archive_cache_path()
+{
+ return lnav::paths::workdir() / "archives";
+}
+
+fs::path
+filename_to_tmp_path(const std::string& filename)
+{
+ auto fn_path = fs::path(filename);
+ auto basename = fn_path.filename().string();
+ hasher h;
+
+ h.update(basename);
+ auto fd = auto_fd(lnav::filesystem::openp(filename, O_RDONLY));
+ if (fd != -1) {
+ char buffer[1024];
+ int rc;
+
+ rc = read(fd, buffer, sizeof(buffer));
+ if (rc >= 0) {
+ h.update(buffer, rc);
+ }
+ }
+ basename = fmt::format(FMT_STRING("arc-{}-{}"), h.to_string(), basename);
+
+ return archive_cache_path() / basename;
+}
+
+#if HAVE_ARCHIVE_H
+static walk_result_t
+copy_data(const std::string& filename,
+ struct archive* ar,
+ struct archive_entry* entry,
+ struct archive* aw,
+ const fs::path& entry_path,
+ struct extract_progress* ep)
+{
+ int r;
+ const void* buff;
+ size_t size, total = 0, next_space_check = 0;
+ la_int64_t offset;
+
+ for (;;) {
+ if (total >= next_space_check) {
+ const auto& cfg = injector::get<const config&>();
+ auto tmp_space = fs::space(entry_path);
+
+ if (tmp_space.available < cfg.amc_min_free_space) {
+ return Err(fmt::format(
+ FMT_STRING("available space on disk ({}) is below the "
+ "minimum-free threshold ({}). Unable to unpack "
+ "'{}' to '{}'"),
+ humanize::file_size(tmp_space.available,
+ humanize::alignment::none),
+ humanize::file_size(cfg.amc_min_free_space,
+ humanize::alignment::none),
+ entry_path.filename().string(),
+ entry_path.parent_path().string()));
+ }
+ next_space_check += 1024 * 1024;
+ }
+
+ r = archive_read_data_block(ar, &buff, &size, &offset);
+ if (r == ARCHIVE_EOF) {
+ return Ok();
+ }
+ if (r != ARCHIVE_OK) {
+ return Err(fmt::format(
+ FMT_STRING("failed to extract '{}' from archive '{}' -- {}"),
+ archive_entry_pathname_utf8(entry),
+ filename,
+ archive_error_string(ar)));
+ }
+ r = archive_write_data_block(aw, buff, size, offset);
+ if (r != ARCHIVE_OK) {
+ return Err(fmt::format(FMT_STRING("failed to write file: {} -- {}"),
+ entry_path.string(),
+ archive_error_string(aw)));
+ }
+
+ total += size;
+ ep->ep_out_size.fetch_add(size);
+ }
+}
+
+static walk_result_t
+extract(const std::string& filename, const extract_cb& cb)
+{
+ static const int FLAGS = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM
+ | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS;
+
+ std::error_code ec;
+ auto tmp_path = filename_to_tmp_path(filename);
+
+ fs::create_directories(tmp_path.parent_path(), ec);
+ if (ec) {
+ return Err(fmt::format("unable to create directory: {} -- {}",
+ tmp_path.parent_path().string(),
+ ec.message()));
+ }
+
+ auto arc_lock = lnav::filesystem::file_lock(tmp_path);
+ auto lock_guard = lnav::filesystem::file_lock::guard(&arc_lock);
+ auto done_path = tmp_path;
+
+ done_path += ".done";
+
+ if (fs::exists(done_path)) {
+ size_t file_count = 0;
+ if (fs::is_directory(tmp_path)) {
+ for (const auto& entry : fs::directory_iterator(tmp_path)) {
+ (void) entry;
+ file_count += 1;
+ }
+ }
+ if (file_count > 0) {
+ fs::last_write_time(done_path, std::chrono::system_clock::now());
+ log_info("%s: archive has already been extracted!",
+ done_path.c_str());
+ return Ok();
+ }
+ log_warning("%s: archive cache has been damaged, re-extracting",
+ done_path.c_str());
+
+ fs::remove(done_path);
+ }
+
+ auto_mem<archive> arc(archive_free);
+ auto_mem<archive> ext(archive_free);
+
+ arc = archive_read_new();
+ enable_desired_archive_formats(arc);
+ archive_read_support_format_raw(arc);
+ archive_read_support_filter_all(arc);
+ ext = archive_write_disk_new();
+ archive_write_disk_set_options(ext, FLAGS);
+ archive_write_disk_set_standard_lookup(ext);
+ if (archive_read_open_filename(arc, filename.c_str(), 10240) != ARCHIVE_OK)
+ {
+ return Err(fmt::format(FMT_STRING("unable to open archive: {} -- {}"),
+ filename,
+ archive_error_string(arc)));
+ }
+
+ log_info("extracting %s to %s", filename.c_str(), tmp_path.c_str());
+ while (true) {
+ struct archive_entry* entry = nullptr;
+ auto r = archive_read_next_header(arc, &entry);
+ if (r == ARCHIVE_EOF) {
+ log_info("all done");
+ break;
+ }
+ if (r != ARCHIVE_OK) {
+ return Err(
+ fmt::format(FMT_STRING("unable to read entry header: {} -- {}"),
+ filename,
+ archive_error_string(arc)));
+ }
+
+ const auto* format_name = archive_format_name(arc);
+ auto filter_count = archive_filter_count(arc);
+
+ auto_mem<archive_entry> wentry(archive_entry_free);
+ wentry = archive_entry_clone(entry);
+ auto desired_pathname = fs::path(archive_entry_pathname(entry));
+ if (strcmp(format_name, "raw") == 0 && filter_count >= 2) {
+ desired_pathname = fs::path(filename).filename();
+ }
+ auto entry_path = tmp_path / desired_pathname;
+ auto* prog = cb(
+ entry_path,
+ archive_entry_size_is_set(entry) ? archive_entry_size(entry) : -1);
+ archive_entry_copy_pathname(wentry, entry_path.c_str());
+ auto entry_mode = archive_entry_mode(wentry);
+
+ archive_entry_set_perm(
+ wentry, S_IRUSR | (S_ISDIR(entry_mode) ? S_IXUSR | S_IWUSR : 0));
+ r = archive_write_header(ext, wentry);
+ if (r < ARCHIVE_OK) {
+ return Err(
+ fmt::format(FMT_STRING("unable to write entry: {} -- {}"),
+ entry_path.string(),
+ archive_error_string(ext)));
+ }
+
+ if (!archive_entry_size_is_set(entry) || archive_entry_size(entry) > 0)
+ {
+ TRY(copy_data(filename, arc, entry, ext, entry_path, prog));
+ }
+ r = archive_write_finish_entry(ext);
+ if (r != ARCHIVE_OK) {
+ return Err(
+ fmt::format(FMT_STRING("unable to finish entry: {} -- {}"),
+ entry_path.string(),
+ archive_error_string(ext)));
+ }
+ }
+ archive_read_close(arc);
+ archive_write_close(ext);
+
+ lnav::filesystem::create_file(done_path, O_WRONLY, 0600);
+
+ return Ok();
+}
+#endif
+
+walk_result_t
+walk_archive_files(
+ const std::string& filename,
+ const extract_cb& cb,
+ const std::function<void(const fs::path&, const fs::directory_entry&)>&
+ callback)
+{
+#if HAVE_ARCHIVE_H
+ auto tmp_path = filename_to_tmp_path(filename);
+
+ auto result = extract(filename, cb);
+ if (result.isErr()) {
+ fs::remove_all(tmp_path);
+ return result;
+ }
+
+ for (const auto& entry : fs::recursive_directory_iterator(tmp_path)) {
+ if (!entry.is_regular_file()) {
+ continue;
+ }
+
+ callback(tmp_path, entry);
+ }
+
+ return Ok();
+#else
+ return Err(std::string("not compiled with libarchive"));
+#endif
+}
+
+void
+cleanup_cache()
+{
+ (void) std::async(std::launch::async, []() {
+ auto now = std::chrono::system_clock::now();
+ auto cache_path = archive_cache_path();
+ const auto& cfg = injector::get<const config&>();
+ std::vector<fs::path> to_remove;
+
+ log_debug("cache-ttl %d", cfg.amc_cache_ttl.count());
+ for (const auto& entry : fs::directory_iterator(cache_path)) {
+ if (entry.path().extension() != ".done") {
+ continue;
+ }
+
+ auto mtime = fs::last_write_time(entry.path());
+ auto exp_time = mtime + cfg.amc_cache_ttl;
+ if (now < exp_time) {
+ continue;
+ }
+
+ to_remove.emplace_back(entry.path());
+ }
+
+ for (auto& entry : to_remove) {
+ log_debug("removing cached archive: %s", entry.c_str());
+ fs::remove(entry);
+
+ entry.replace_extension(".lck");
+ fs::remove(entry);
+
+ entry.replace_extension();
+ fs::remove_all(entry);
+ }
+ });
+}
+
+} // namespace archive_manager
diff --git a/src/archive_manager.cfg.hh b/src/archive_manager.cfg.hh
new file mode 100644
index 0000000..1c0b821
--- /dev/null
+++ b/src/archive_manager.cfg.hh
@@ -0,0 +1,46 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file archive_manager.cfg.hh
+ */
+
+#ifndef lnav_archive_manager_cfg_hh
+#define lnav_archive_manager_cfg_hh
+
+#include <chrono>
+
+namespace archive_manager {
+
+struct config {
+ uint64_t amc_min_free_space{32 * 1024 * 1024};
+ std::chrono::seconds amc_cache_ttl{std::chrono::hours(48)};
+};
+
+} // namespace archive_manager
+
+#endif
diff --git a/src/archive_manager.hh b/src/archive_manager.hh
new file mode 100644
index 0000000..777ae87
--- /dev/null
+++ b/src/archive_manager.hh
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file archive_manager.hh
+ */
+
+#ifndef lnav_archive_manager_hh
+#define lnav_archive_manager_hh
+
+#include <atomic>
+#include <functional>
+#include <string>
+#include <utility>
+
+#include "base/result.h"
+#include "ghc/filesystem.hpp"
+
+namespace archive_manager {
+
+struct extract_progress {
+ extract_progress(ghc::filesystem::path path, ssize_t total)
+ : ep_path(std::move(path)), ep_total_size(total)
+ {
+ }
+
+ const ghc::filesystem::path ep_path;
+ const ssize_t ep_total_size;
+ std::atomic<size_t> ep_out_size{0};
+};
+
+using extract_cb
+ = std::function<extract_progress*(const ghc::filesystem::path&, ssize_t)>;
+
+bool is_archive(const ghc::filesystem::path& filename);
+
+ghc::filesystem::path filename_to_tmp_path(const std::string& filename);
+
+using walk_result_t = Result<void, std::string>;
+
+/**
+ *
+ * @feature f0:archive
+ *
+ * @param filename
+ * @param cb
+ * @return
+ */
+walk_result_t walk_archive_files(
+ const std::string& filename,
+ const extract_cb& cb,
+ const std::function<void(const ghc::filesystem::path&,
+ const ghc::filesystem::directory_entry&)>&);
+
+void cleanup_cache();
+
+} // namespace archive_manager
+
+#endif
diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt
new file mode 100644
index 0000000..aa4143f
--- /dev/null
+++ b/src/base/CMakeLists.txt
@@ -0,0 +1,83 @@
+add_library(
+ base STATIC
+ ../config.h.in
+ ansi_scrubber.cc
+ attr_line.cc
+ attr_line.builder.cc
+ auto_pid.cc
+ date_time_scanner.cc
+ fs_util.cc
+ humanize.cc
+ humanize.network.cc
+ humanize.time.cc
+ intern_string.cc
+ is_utf8.cc
+ isc.cc
+ lnav.console.cc
+ lnav.gzip.cc
+ lnav_log.cc
+ network.tcp.cc
+ paths.cc
+ snippet_highlighters.cc
+ string_attr_type.cc
+ string_util.cc
+ strnatcmp.c
+ time_util.cc
+
+ ansi_scrubber.hh
+ attr_line.hh
+ attr_line.builder.hh
+ auto_fd.hh
+ auto_mem.hh
+ auto_pid.hh
+ bus.hh
+ date_time_scanner.hh
+ enum_util.hh
+ fs_util.hh
+ func_util.hh
+ future_util.hh
+ humanize.hh
+ humanize.network.hh
+ humanize.time.hh
+ injector.hh
+ injector.bind.hh
+ intern_string.hh
+ is_utf8.hh
+ isc.hh
+ itertools.hh
+ lnav.console.hh
+ lnav.console.into.hh
+ log_level_enum.hh
+ lrucache.hpp
+ math_util.hh
+ network.tcp.hh
+ paths.hh
+ result.h
+ snippet_highlighters.hh
+ string_attr_type.hh
+ strnatcmp.h
+ time_util.hh
+
+ ../third-party/xxHash/xxhash.h
+ ../third-party/xxHash/xxhash.c
+)
+
+target_include_directories(base PUBLIC . .. ../third-party
+ ${CMAKE_CURRENT_BINARY_DIR}/..)
+target_link_libraries(base cppfmt cppscnlib pcrepp ncurses::libcurses pthread)
+
+add_executable(
+ test_base
+ attr_line.tests.cc
+ fs_util.tests.cc
+ humanize.file_size.tests.cc
+ humanize.network.tests.cc
+ humanize.time.tests.cc
+ intern_string.tests.cc
+ lnav.gzip.tests.cc
+ string_util.tests.cc
+ network.tcp.tests.cc
+ test_base.cc)
+target_include_directories(test_base PUBLIC ../third-party/doctest-root)
+target_link_libraries(test_base base pcrepp ZLIB::ZLIB)
+add_test(NAME test_base COMMAND test_base)
diff --git a/src/base/Makefile.am b/src/base/Makefile.am
new file mode 100644
index 0000000..4a459a6
--- /dev/null
+++ b/src/base/Makefile.am
@@ -0,0 +1,110 @@
+
+include $(top_srcdir)/aminclude_static.am
+
+AM_CPPFLAGS = \
+ $(CODE_COVERAGE_CPPFLAGS) \
+ -Wall \
+ -I$(top_srcdir)/src/ \
+ -I$(top_srcdir)/src/third-party \
+ -I$(top_srcdir)/src/fmtlib \
+ -I$(top_srcdir)/src/third-party/scnlib/include \
+ $(LIBARCHIVE_CFLAGS) \
+ $(READLINE_CFLAGS) \
+ $(SQLITE3_CFLAGS) \
+ $(PCRE_CFLAGS) \
+ $(LIBCURL_CPPFLAGS)
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS)
+
+noinst_LIBRARIES = libbase.a
+
+noinst_HEADERS = \
+ ansi_scrubber.hh \
+ attr_line.hh \
+ attr_line.builder.hh \
+ auto_fd.hh \
+ auto_mem.hh \
+ auto_pid.hh \
+ bus.hh \
+ date_time_scanner.hh \
+ enum_util.hh \
+ file_range.hh \
+ fs_util.hh \
+ func_util.hh \
+ future_util.hh \
+ humanize.hh \
+ humanize.network.hh \
+ humanize.time.hh \
+ injector.hh \
+ injector.bind.hh \
+ intern_string.hh \
+ is_utf8.hh \
+ isc.hh \
+ itertools.hh \
+ lnav_log.hh \
+ lnav.console.hh \
+ lnav.console.into.hh \
+ lnav.gzip.hh \
+ log_level_enum.hh \
+ lrucache.hpp \
+ math_util.hh \
+ network.tcp.hh \
+ opt_util.hh \
+ paths.hh \
+ result.h \
+ snippet_highlighters.hh \
+ string_attr_type.hh \
+ string_util.hh \
+ strnatcmp.h \
+ time_util.hh
+
+libbase_a_SOURCES = \
+ ansi_scrubber.cc \
+ attr_line.cc \
+ attr_line.builder.cc \
+ auto_pid.cc \
+ date_time_scanner.cc \
+ fs_util.cc \
+ humanize.cc \
+ humanize.network.cc \
+ humanize.time.cc \
+ intern_string.cc \
+ is_utf8.cc \
+ isc.cc \
+ lnav.console.cc \
+ lnav.gzip.cc \
+ lnav_log.cc \
+ network.tcp.cc \
+ paths.cc \
+ snippet_highlighters.cc \
+ string_attr_type.cc \
+ string_util.cc \
+ strnatcmp.c \
+ time_util.cc \
+ ../third-party/xxHash/xxhash.h \
+ ../third-party/xxHash/xxhash.c
+
+check_PROGRAMS = \
+ test_base
+
+test_base_SOURCES = \
+ attr_line.tests.cc \
+ fs_util.tests.cc \
+ humanize.file_size.tests.cc \
+ humanize.network.tests.cc \
+ humanize.time.tests.cc \
+ intern_string.tests.cc \
+ lnav.gzip.tests.cc \
+ string_util.tests.cc \
+ test_base.cc
+
+test_base_LDADD = \
+ libbase.a \
+ ../fmtlib/libcppfmt.a \
+ ../third-party/scnlib/src/libscnlib.a \
+ ../pcrepp/libpcrepp.a
+
+TESTS = \
+ test_base
diff --git a/src/base/README.md b/src/base/README.md
new file mode 100644
index 0000000..944dde8
--- /dev/null
+++ b/src/base/README.md
@@ -0,0 +1 @@
+# libbase -- Library of utility functions
diff --git a/src/base/ansi_scrubber.cc b/src/base/ansi_scrubber.cc
new file mode 100644
index 0000000..98f6c96
--- /dev/null
+++ b/src/base/ansi_scrubber.cc
@@ -0,0 +1,388 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file ansi_scrubber.cc
+ */
+
+#include <algorithm>
+
+#include "ansi_scrubber.hh"
+
+#include "base/opt_util.hh"
+#include "config.h"
+#include "pcrepp/pcre2pp.hh"
+#include "scn/scn.h"
+#include "view_curses.hh"
+
+static const lnav::pcre2pp::code&
+ansi_regex()
+{
+ static const auto retval = lnav::pcre2pp::code::from_const(
+ "\x1b\\[([\\d=;\\?]*)([a-zA-Z])|(?:\\X\x08\\X)+");
+
+ return retval;
+}
+
+size_t
+erase_ansi_escapes(string_fragment input)
+{
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+
+ const auto& regex = ansi_regex();
+ nonstd::optional<int> move_start;
+ size_t fill_index = 0;
+
+ auto matcher = regex.capture_from(input).into(md);
+ while (true) {
+ auto match_res = matcher.matches(PCRE2_NO_UTF_CHECK);
+
+ if (match_res.is<lnav::pcre2pp::matcher::not_found>()) {
+ break;
+ }
+ if (match_res.is<lnav::pcre2pp::matcher::error>()) {
+ log_error("ansi scrub regex failure");
+ break;
+ }
+
+ auto sf = md[0].value();
+ auto bs_index_res = sf.codepoint_to_byte_index(1);
+
+ if (move_start) {
+ auto move_len = sf.sf_begin - move_start.value();
+ memmove(input.writable_data(fill_index),
+ input.data() + move_start.value(),
+ move_len);
+ fill_index += move_len;
+ } else {
+ fill_index = sf.sf_begin;
+ }
+
+ if (sf.length() >= 3 && bs_index_res.isOk()
+ && sf[bs_index_res.unwrap()] == '\b')
+ {
+ static const auto OVERSTRIKE_RE
+ = lnav::pcre2pp::code::from_const(R"((\X)\x08(\X))");
+
+ auto loop_res = OVERSTRIKE_RE.capture_from(sf).for_each(
+ [&fill_index, &input](lnav::pcre2pp::match_data& over_md) {
+ auto lhs = over_md[1].value();
+ if (lhs == "_") {
+ auto rhs = over_md[2].value();
+ memmove(input.writable_data(fill_index),
+ rhs.data(),
+ rhs.length());
+ fill_index += rhs.length();
+ } else {
+ memmove(input.writable_data(fill_index),
+ lhs.data(),
+ lhs.length());
+ fill_index += lhs.length();
+ }
+ });
+ }
+ move_start = md.remaining().sf_begin;
+ }
+
+ memmove(input.writable_data(fill_index),
+ md.remaining().data(),
+ md.remaining().length());
+ fill_index += md.remaining().length();
+
+ return fill_index;
+}
+
+void
+scrub_ansi_string(std::string& str, string_attrs_t* sa)
+{
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+ const auto& regex = ansi_regex();
+ int64_t origin_offset = 0;
+ int last_origin_offset_end = 0;
+
+ replace(str.begin(), str.end(), '\0', ' ');
+ auto matcher = regex.capture_from(str).into(md);
+ while (true) {
+ auto match_res = matcher.matches(PCRE2_NO_UTF_CHECK);
+
+ if (match_res.is<lnav::pcre2pp::matcher::not_found>()) {
+ break;
+ }
+ if (match_res.is<lnav::pcre2pp::matcher::error>()) {
+ log_error("ansi scrub regex failure");
+ break;
+ }
+
+ const auto sf = md[0].value();
+ auto bs_index_res = sf.codepoint_to_byte_index(1);
+
+ if (sf.length() >= 3 && bs_index_res.isOk()
+ && sf[bs_index_res.unwrap()] == '\b')
+ {
+ ssize_t fill_index = sf.sf_begin;
+ line_range bold_range;
+ line_range ul_range;
+ auto sub_sf = sf;
+
+ while (!sub_sf.empty()) {
+ auto lhs_opt = sub_sf.consume_codepoint();
+ if (!lhs_opt) {
+ return;
+ }
+ auto lhs_pair = lhs_opt.value();
+ auto mid_opt = lhs_pair.second.consume_codepoint();
+ if (!mid_opt) {
+ return;
+ }
+ auto mid_pair = mid_opt.value();
+ auto rhs_opt = mid_pair.second.consume_codepoint();
+ if (!rhs_opt) {
+ return;
+ }
+ auto rhs_pair = rhs_opt.value();
+ sub_sf = rhs_pair.second;
+
+ if (lhs_pair.first == '_' || rhs_pair.first == '_') {
+ if (sa != nullptr && bold_range.is_valid()) {
+ sa->emplace_back(bold_range,
+ VC_STYLE.value(text_attrs{A_BOLD}));
+ bold_range.clear();
+ }
+ if (ul_range.is_valid()) {
+ ul_range.lr_end += 1;
+ } else {
+ ul_range.lr_start = fill_index;
+ ul_range.lr_end = fill_index + 1;
+ }
+ auto cp = lhs_pair.first == '_' ? rhs_pair.first
+ : lhs_pair.first;
+ ww898::utf::utf8::write(cp, [&str, &fill_index](auto ch) {
+ str[fill_index++] = ch;
+ });
+ } else {
+ if (sa != nullptr && ul_range.is_valid()) {
+ sa->emplace_back(
+ ul_range, VC_STYLE.value(text_attrs{A_UNDERLINE}));
+ ul_range.clear();
+ }
+ if (bold_range.is_valid()) {
+ bold_range.lr_end += 1;
+ } else {
+ bold_range.lr_start = fill_index;
+ bold_range.lr_end = fill_index + 1;
+ }
+ try {
+ ww898::utf::utf8::write(lhs_pair.first,
+ [&str, &fill_index](auto ch) {
+ str[fill_index++] = ch;
+ });
+ } catch (const std::runtime_error& e) {
+ log_error("invalid UTF-8 at %d", sf.sf_begin);
+ return;
+ }
+ }
+ }
+
+ auto output_size = fill_index - sf.sf_begin;
+ auto erased_size = sf.length() - output_size;
+
+ if (sa != nullptr) {
+#if 0
+ shift_string_attrs(
+ *sa, caps->c_begin + sf.length() / 3, -erased_size);
+#endif
+ sa->emplace_back(line_range{last_origin_offset_end,
+ sf.sf_begin + (int) output_size},
+ SA_ORIGIN_OFFSET.value(origin_offset));
+ }
+
+ if (sa != nullptr && ul_range.is_valid()) {
+ sa->emplace_back(ul_range,
+ VC_STYLE.value(text_attrs{A_UNDERLINE}));
+ ul_range.clear();
+ }
+ if (sa != nullptr && bold_range.is_valid()) {
+ sa->emplace_back(bold_range,
+ VC_STYLE.value(text_attrs{A_BOLD}));
+ bold_range.clear();
+ }
+
+ str.erase(str.begin() + fill_index, str.begin() + sf.sf_end);
+ last_origin_offset_end = sf.sf_begin + output_size;
+ origin_offset += erased_size;
+ matcher.reload_input(str, last_origin_offset_end);
+ continue;
+ }
+
+ auto seq = md[1].value();
+ auto terminator = md[2].value();
+ struct line_range lr;
+ bool has_attrs = false;
+ text_attrs attrs;
+ nonstd::optional<role_t> role;
+ size_t lpc;
+
+ switch (terminator[0]) {
+ case 'm':
+ for (lpc = seq.sf_begin;
+ lpc != std::string::npos && lpc < (size_t) seq.sf_end;)
+ {
+ auto ansi_code_res = scn::scan_value<int>(
+ scn::string_view{&str[lpc], &str[seq.sf_end]});
+
+ if (ansi_code_res) {
+ auto ansi_code = ansi_code_res.value();
+ if (90 <= ansi_code && ansi_code <= 97) {
+ ansi_code -= 60;
+ attrs.ta_attrs |= A_STANDOUT;
+ }
+ if (30 <= ansi_code && ansi_code <= 37) {
+ attrs.ta_fg_color = ansi_code - 30;
+ }
+ if (40 <= ansi_code && ansi_code <= 47) {
+ attrs.ta_bg_color = ansi_code - 40;
+ }
+ switch (ansi_code) {
+ case 1:
+ attrs.ta_attrs |= A_BOLD;
+ break;
+
+ case 2:
+ attrs.ta_attrs |= A_DIM;
+ break;
+
+ case 4:
+ attrs.ta_attrs |= A_UNDERLINE;
+ break;
+
+ case 7:
+ attrs.ta_attrs |= A_REVERSE;
+ break;
+ }
+ }
+ lpc = str.find(';', lpc);
+ if (lpc != std::string::npos) {
+ lpc += 1;
+ }
+ }
+ has_attrs = true;
+ break;
+
+ case 'C': {
+ auto spaces_res
+ = scn::scan_value<unsigned int>(seq.to_string_view());
+
+ if (spaces_res && spaces_res.value() > 0) {
+ str.insert((std::string::size_type) sf.sf_end,
+ spaces_res.value(),
+ ' ');
+ }
+ break;
+ }
+
+ case 'H': {
+ unsigned int row = 0, spaces = 0;
+
+ if (scn::scan(seq.to_string_view(), "{};{}", row, spaces)
+ && spaces > 1)
+ {
+ int ispaces = spaces - 1;
+ if (ispaces > sf.sf_begin) {
+ str.insert((unsigned long) sf.sf_end,
+ ispaces - sf.sf_begin,
+ ' ');
+ }
+ }
+ break;
+ }
+
+ case 'O': {
+ auto role_res = scn::scan_value<int>(seq.to_string_view());
+
+ if (role_res) {
+ role_t role_tmp = (role_t) role_res.value();
+ if (role_tmp > role_t::VCR_NONE
+ && role_tmp < role_t::VCR__MAX)
+ {
+ role = role_tmp;
+ has_attrs = true;
+ }
+ }
+ break;
+ }
+ }
+ str.erase(str.begin() + sf.sf_begin, str.begin() + sf.sf_end);
+ if (sa != nullptr) {
+ shift_string_attrs(*sa, sf.sf_begin, -sf.length());
+
+ if (has_attrs) {
+ for (auto rit = sa->rbegin(); rit != sa->rend(); rit++) {
+ if (rit->sa_range.lr_end != -1) {
+ continue;
+ }
+ rit->sa_range.lr_end = sf.sf_begin;
+ }
+ lr.lr_start = sf.sf_begin;
+ lr.lr_end = -1;
+ if (attrs.ta_attrs || attrs.ta_fg_color || attrs.ta_bg_color) {
+ sa->emplace_back(lr, VC_STYLE.value(attrs));
+ }
+ role | [&lr, &sa](role_t r) {
+ sa->emplace_back(lr, VC_ROLE.value(r));
+ };
+ }
+ sa->emplace_back(line_range{last_origin_offset_end, sf.sf_begin},
+ SA_ORIGIN_OFFSET.value(origin_offset));
+ last_origin_offset_end = sf.sf_begin;
+ origin_offset += sf.length();
+ }
+
+ matcher.reload_input(str, sf.sf_begin);
+ }
+
+ if (sa != nullptr && last_origin_offset_end > 0) {
+ sa->emplace_back(line_range{last_origin_offset_end, (int) str.size()},
+ SA_ORIGIN_OFFSET.value(origin_offset));
+ }
+}
+
+void
+add_ansi_vars(std::map<std::string, scoped_value_t>& vars)
+{
+ vars["ansi_csi"] = ANSI_CSI;
+ vars["ansi_norm"] = ANSI_NORM;
+ vars["ansi_bold"] = ANSI_BOLD_START;
+ vars["ansi_underline"] = ANSI_UNDERLINE_START;
+ vars["ansi_black"] = ANSI_COLOR(COLOR_BLACK);
+ vars["ansi_red"] = ANSI_COLOR(COLOR_RED);
+ vars["ansi_green"] = ANSI_COLOR(COLOR_GREEN);
+ vars["ansi_yellow"] = ANSI_COLOR(COLOR_YELLOW);
+ vars["ansi_blue"] = ANSI_COLOR(COLOR_BLUE);
+ vars["ansi_magenta"] = ANSI_COLOR(COLOR_MAGENTA);
+ vars["ansi_cyan"] = ANSI_COLOR(COLOR_CYAN);
+ vars["ansi_white"] = ANSI_COLOR(COLOR_WHITE);
+}
diff --git a/src/base/ansi_scrubber.hh b/src/base/ansi_scrubber.hh
new file mode 100644
index 0000000..b832e17
--- /dev/null
+++ b/src/base/ansi_scrubber.hh
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file ansi_scrubber.hh
+ */
+
+#ifndef lnav_ansi_scrubber_hh
+#define lnav_ansi_scrubber_hh
+
+#include <map>
+#include <string>
+
+#include "attr_line.hh"
+#include "shlex.resolver.hh"
+
+#define ANSI_CSI "\x1b["
+#define ANSI_CHAR_ATTR "m"
+#define ANSI_BOLD_PARAM "1"
+#define ANSI_BOLD_START ANSI_CSI ANSI_BOLD_PARAM ANSI_CHAR_ATTR
+#define ANSI_UNDERLINE_START ANSI_CSI "4m"
+#define ANSI_NORM ANSI_CSI "0m"
+#define ANSI_STRIKE_PARAM "9"
+#define ANSI_STRIKE_START ANSI_CSI ANSI_STRIKE_PARAM ANSI_CHAR_ATTR
+
+#define ANSI_BOLD(msg) ANSI_BOLD_START msg ANSI_NORM
+#define ANSI_UNDERLINE(msg) ANSI_UNDERLINE_START msg ANSI_NORM
+
+#define ANSI_ROLE(msg) ANSI_CSI "%dO" msg ANSI_NORM
+#define XANSI_COLOR(col) "3" #col
+#define ANSI_COLOR_PARAM(col) XANSI_COLOR(col)
+#define ANSI_COLOR(col) ANSI_CSI XANSI_COLOR(col) "m"
+
+/**
+ * Check a string for ANSI escape sequences, process them, remove them, and add
+ * any style attributes to the given attribute container.
+ *
+ * @param str The string to check for ANSI escape sequences.
+ * @param sa The container for any style attributes.
+ */
+void scrub_ansi_string(std::string& str, string_attrs_t* sa);
+
+size_t erase_ansi_escapes(string_fragment input);
+
+/**
+ * Populate a variable map with strings that contain escape sequences that
+ * might be useful to script writers.
+ */
+void add_ansi_vars(std::map<std::string, scoped_value_t>& vars);
+
+#endif
diff --git a/src/base/attr_line.builder.cc b/src/base/attr_line.builder.cc
new file mode 100644
index 0000000..95416dc
--- /dev/null
+++ b/src/base/attr_line.builder.cc
@@ -0,0 +1,30 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "attr_line.builder.hh"
diff --git a/src/base/attr_line.builder.hh b/src/base/attr_line.builder.hh
new file mode 100644
index 0000000..1e62532
--- /dev/null
+++ b/src/base/attr_line.builder.hh
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_attr_line_builder_hh
+#define lnav_attr_line_builder_hh
+
+#include <utility>
+
+#include "attr_line.hh"
+
+class attr_line_builder {
+public:
+ explicit attr_line_builder(attr_line_t& al) : alb_line(al) {}
+
+ class attr_guard {
+ public:
+ attr_guard(attr_line_t& al, string_attr_pair sap)
+ : ag_line(al), ag_start(al.get_string().length()),
+ ag_attr(std::move(sap))
+ {
+ }
+
+ attr_guard(const attr_guard&) = delete;
+
+ attr_guard& operator=(const attr_guard&) = delete;
+
+ attr_guard(attr_guard&& other) noexcept
+ : ag_line(other.ag_line), ag_start(other.ag_start),
+ ag_attr(std::move(other.ag_attr))
+ {
+ other.ag_start = nonstd::nullopt;
+ }
+
+ ~attr_guard()
+ {
+ if (this->ag_start) {
+ this->ag_line.al_attrs.emplace_back(
+ line_range{
+ this->ag_start.value(),
+ (int) this->ag_line.get_string().length(),
+ },
+ this->ag_attr);
+ }
+ }
+
+ private:
+ attr_line_t& ag_line;
+ nonstd::optional<int> ag_start;
+ string_attr_pair ag_attr;
+ };
+
+ attr_guard with_attr(string_attr_pair sap)
+ {
+ return {this->alb_line, std::move(sap)};
+ }
+
+ template<typename... Args>
+ attr_line_builder& overlay_attr(Args... args)
+ {
+ this->alb_line.al_attrs.template emplace_back(args...);
+ return *this;
+ }
+
+ template<typename... Args>
+ attr_line_builder& overlay_attr_for_char(int index, Args... args)
+ {
+ this->alb_line.al_attrs.template emplace_back(
+ line_range{index, index + 1}, args...);
+ return *this;
+ }
+
+ template<typename... Args>
+ attr_line_builder& append(Args... args)
+ {
+ this->alb_line.append(args...);
+
+ return *this;
+ }
+
+ attr_line_builder& indent(size_t amount)
+ {
+ auto pre = this->with_attr(SA_PREFORMATTED.value());
+
+ this->append(amount, ' ');
+
+ return *this;
+ }
+
+private:
+ attr_line_t& alb_line;
+};
+
+#endif
diff --git a/src/base/attr_line.cc b/src/base/attr_line.cc
new file mode 100644
index 0000000..b65f4ba
--- /dev/null
+++ b/src/base/attr_line.cc
@@ -0,0 +1,537 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+
+#include "attr_line.hh"
+
+#include <stdarg.h>
+
+#include "ansi_scrubber.hh"
+#include "auto_mem.hh"
+#include "config.h"
+#include "lnav_log.hh"
+#include "pcrepp/pcre2pp.hh"
+
+attr_line_t&
+attr_line_t::with_ansi_string(const char* str, ...)
+{
+ auto_mem<char> formatted_str;
+ va_list args;
+
+ va_start(args, str);
+ auto ret = vasprintf(formatted_str.out(), str, args);
+ va_end(args);
+
+ if (ret >= 0 && formatted_str != nullptr) {
+ this->al_string = formatted_str;
+ scrub_ansi_string(this->al_string, &this->al_attrs);
+ }
+
+ return *this;
+}
+
+attr_line_t&
+attr_line_t::with_ansi_string(const std::string& str)
+{
+ this->al_string = str;
+ scrub_ansi_string(this->al_string, &this->al_attrs);
+
+ return *this;
+}
+
+namespace text_stream {
+struct word {
+ string_fragment w_word;
+ string_fragment w_remaining;
+};
+
+struct space {
+ string_fragment s_value;
+ string_fragment s_remaining;
+};
+
+struct corrupt {
+ string_fragment c_value;
+ string_fragment c_remaining;
+};
+
+struct eof {
+ string_fragment e_remaining;
+};
+
+using chunk = mapbox::util::variant<word, space, corrupt, eof>;
+
+chunk
+consume(const string_fragment text)
+{
+ static const auto WORD_RE
+ = lnav::pcre2pp::code::from_const(R"((*UTF)^[^\p{Z}\p{So}\p{C}]+)");
+ static const auto SPACE_RE
+ = lnav::pcre2pp::code::from_const(R"((*UTF)^\s)");
+
+ if (text.empty()) {
+ return eof{text};
+ }
+
+ auto word_find_res
+ = WORD_RE.find_in(text, PCRE2_NO_UTF_CHECK).ignore_error();
+ if (word_find_res) {
+ auto split_res = text.split_n(word_find_res->f_all.length()).value();
+
+ return word{split_res.first, split_res.second};
+ }
+
+ if (isspace(text.front())) {
+ auto split_res = text.split_n(1).value();
+
+ return space{split_res.first, split_res.second};
+ }
+
+ auto space_find_res
+ = SPACE_RE.find_in(text, PCRE2_NO_UTF_CHECK).ignore_error();
+ if (space_find_res) {
+ auto split_res = text.split_n(space_find_res->f_all.length()).value();
+
+ return space{split_res.first, split_res.second};
+ }
+
+ auto csize_res = ww898::utf::utf8::char_size(
+ [&text]() { return std::make_pair(text.front(), text.length()); });
+
+ if (csize_res.isErr()) {
+ auto split_res = text.split_n(1);
+
+ return corrupt{split_res->first, split_res->second};
+ }
+
+ auto split_res = text.split_n(csize_res.unwrap());
+
+ return word{split_res->first, split_res->second};
+}
+
+} // namespace text_stream
+
+static void
+split_attrs(attr_line_t& al, const line_range& lr)
+{
+ if (lr.empty()) {
+ return;
+ }
+
+ string_attrs_t new_attrs;
+
+ for (auto& attr : al.al_attrs) {
+ if (!lr.intersects(attr.sa_range)) {
+ continue;
+ }
+
+ new_attrs.emplace_back(line_range{lr.lr_end, attr.sa_range.lr_end},
+ std::make_pair(attr.sa_type, attr.sa_value));
+ attr.sa_range.lr_end = lr.lr_start;
+ }
+ for (auto& new_attr : new_attrs) {
+ al.al_attrs.emplace_back(std::move(new_attr));
+ }
+}
+
+attr_line_t&
+attr_line_t::insert(size_t index,
+ const attr_line_t& al,
+ text_wrap_settings* tws)
+{
+ if (index < this->al_string.length()) {
+ shift_string_attrs(this->al_attrs, index, al.al_string.length());
+ }
+
+ this->al_string.insert(index, al.al_string);
+
+ for (const auto& sa : al.al_attrs) {
+ this->al_attrs.emplace_back(sa);
+
+ line_range& lr = this->al_attrs.back().sa_range;
+
+ lr.shift(0, index);
+ if (lr.lr_end == -1) {
+ lr.lr_end = index + al.al_string.length();
+ }
+ }
+
+ if (tws == nullptr) {
+ return *this;
+ }
+
+ auto starting_line_index = this->al_string.rfind('\n', index);
+ if (starting_line_index == std::string::npos) {
+ starting_line_index = 0;
+ } else {
+ starting_line_index += 1;
+ }
+
+ const ssize_t usable_width = tws->tws_width - tws->tws_indent;
+
+ auto text_to_wrap = string_fragment::from_str_range(
+ this->al_string, starting_line_index, this->al_string.length());
+ string_fragment last_word;
+ ssize_t line_ch_count = 0;
+ auto needs_indent = false;
+
+ while (!text_to_wrap.empty()) {
+ if (needs_indent) {
+ this->insert(text_to_wrap.sf_begin,
+ tws->tws_indent + tws->tws_padding_indent,
+ ' ');
+ auto indent_lr = line_range{
+ text_to_wrap.sf_begin,
+ text_to_wrap.sf_begin + tws->tws_indent,
+ };
+ split_attrs(*this, indent_lr);
+ indent_lr.lr_end += tws->tws_padding_indent;
+ line_ch_count += tws->tws_padding_indent;
+ if (!indent_lr.empty()) {
+ this->al_attrs.emplace_back(indent_lr, SA_PREFORMATTED.value());
+ }
+ text_to_wrap = text_to_wrap.prepend(
+ this->al_string.data(),
+ tws->tws_indent + tws->tws_padding_indent);
+ needs_indent = false;
+ }
+ auto chunk = text_stream::consume(text_to_wrap);
+
+ text_to_wrap = chunk.match(
+ [&](text_stream::word word) {
+ auto ch_count
+ = word.w_word.utf8_length().unwrapOr(word.w_word.length());
+
+ if ((line_ch_count + ch_count) > usable_width
+ && find_string_attr_containing(this->al_attrs,
+ &SA_PREFORMATTED,
+ text_to_wrap.sf_begin)
+ == this->al_attrs.end())
+ {
+ this->insert(word.w_word.sf_begin, 1, '\n');
+ this->insert(word.w_word.sf_begin + 1,
+ tws->tws_indent + tws->tws_padding_indent,
+ ' ');
+ auto indent_lr = line_range{
+ word.w_word.sf_begin + 1,
+ word.w_word.sf_begin + 1 + tws->tws_indent,
+ };
+ split_attrs(*this, indent_lr);
+ indent_lr.lr_end += tws->tws_padding_indent;
+ if (!indent_lr.empty()) {
+ this->al_attrs.emplace_back(indent_lr,
+ SA_PREFORMATTED.value());
+ }
+ line_ch_count = tws->tws_padding_indent + ch_count;
+ auto trailing_space_count = 0;
+ if (!last_word.empty()) {
+ trailing_space_count
+ = word.w_word.sf_begin - last_word.sf_begin;
+ this->erase(last_word.sf_begin, trailing_space_count);
+ }
+ return word.w_remaining
+ .erase_before(this->al_string.data(),
+ trailing_space_count)
+ .prepend(this->al_string.data(),
+ 1 + tws->tws_indent + tws->tws_padding_indent);
+ }
+ line_ch_count += ch_count;
+
+ return word.w_remaining;
+ },
+ [&](text_stream::space space) {
+ if (space.s_value == "\n") {
+ line_ch_count = 0;
+ needs_indent = true;
+ return space.s_remaining;
+ }
+
+ if (line_ch_count > 0) {
+ auto ch_count = space.s_value.utf8_length().unwrapOr(
+ space.s_value.length());
+
+ if ((line_ch_count + ch_count) > usable_width
+ && find_string_attr_containing(this->al_attrs,
+ &SA_PREFORMATTED,
+ text_to_wrap.sf_begin)
+ == this->al_attrs.end())
+ {
+ this->erase(space.s_value.sf_begin,
+ space.s_value.length());
+ this->insert(space.s_value.sf_begin, "\n");
+ line_ch_count = 0;
+ needs_indent = true;
+
+ auto trailing_space_count = 0;
+ if (!last_word.empty()) {
+ trailing_space_count
+ = space.s_value.sf_begin - last_word.sf_begin;
+ this->erase(last_word.sf_end, trailing_space_count);
+ }
+
+ return space.s_remaining
+ .erase_before(
+ this->al_string.data(),
+ space.s_value.length() + trailing_space_count)
+ .prepend(this->al_string.data(), 1);
+ }
+ line_ch_count += ch_count;
+ } else if (find_string_attr_containing(this->al_attrs,
+ &SA_PREFORMATTED,
+ text_to_wrap.sf_begin)
+ == this->al_attrs.end())
+ {
+ this->erase(space.s_value.sf_begin, space.s_value.length());
+ return space.s_remaining.erase_before(
+ this->al_string.data(), space.s_value.length());
+ }
+
+ return space.s_remaining;
+ },
+ [](text_stream::corrupt corrupt) { return corrupt.c_remaining; },
+ [](text_stream::eof eof) { return eof.e_remaining; });
+
+ if (chunk.is<text_stream::word>()) {
+ last_word = text_to_wrap;
+ }
+
+ ensure(this->al_string.data() == text_to_wrap.sf_string);
+ ensure(text_to_wrap.sf_begin <= text_to_wrap.sf_end);
+ }
+ return *this;
+}
+
+attr_line_t
+attr_line_t::subline(size_t start, size_t len) const
+{
+ if (len == std::string::npos) {
+ len = this->al_string.length() - start;
+ }
+
+ line_range lr{(int) start, (int) (start + len)};
+ attr_line_t retval;
+
+ retval.al_string = this->al_string.substr(start, len);
+ for (const auto& sa : this->al_attrs) {
+ if (!lr.intersects(sa.sa_range)) {
+ continue;
+ }
+
+ auto ilr = lr.intersection(sa.sa_range).shift(0, -lr.lr_start);
+ retval.al_attrs.emplace_back(ilr,
+ std::make_pair(sa.sa_type, sa.sa_value));
+ const auto& last_lr = retval.al_attrs.back().sa_range;
+
+ ensure(last_lr.lr_end <= (int) retval.al_string.length());
+ }
+
+ return retval;
+}
+
+void
+attr_line_t::split_lines(std::vector<attr_line_t>& lines) const
+{
+ size_t pos = 0, next_line;
+
+ while ((next_line = this->al_string.find('\n', pos)) != std::string::npos) {
+ lines.emplace_back(this->subline(pos, next_line - pos));
+ pos = next_line + 1;
+ }
+ lines.emplace_back(this->subline(pos));
+}
+
+attr_line_t&
+attr_line_t::right_justify(unsigned long width)
+{
+ long padding = width - this->length();
+ if (padding > 0) {
+ this->al_string.insert(0, padding, ' ');
+ for (auto& al_attr : this->al_attrs) {
+ if (al_attr.sa_range.lr_start > 0) {
+ al_attr.sa_range.lr_start += padding;
+ }
+ if (al_attr.sa_range.lr_end != -1) {
+ al_attr.sa_range.lr_end += padding;
+ }
+ }
+ }
+
+ return *this;
+}
+
+size_t
+attr_line_t::nearest_text(size_t x) const
+{
+ if (x > 0 && x >= (size_t) this->length()) {
+ if (this->empty()) {
+ x = 0;
+ } else {
+ x = this->length() - 1;
+ }
+ }
+
+ while (x > 0 && isspace(this->al_string[x])) {
+ x -= 1;
+ }
+
+ return x;
+}
+
+void
+attr_line_t::apply_hide()
+{
+ auto& sa = this->al_attrs;
+
+ for (auto& sattr : sa) {
+ if (sattr.sa_type == &SA_HIDDEN && sattr.sa_range.length() > 3) {
+ auto& lr = sattr.sa_range;
+
+ std::for_each(sa.begin(), sa.end(), [&](string_attr& attr) {
+ if (attr.sa_type == &VC_STYLE && lr.contains(attr.sa_range)) {
+ attr.sa_type = &SA_REMOVED;
+ }
+ });
+
+ this->al_string.replace(lr.lr_start, lr.length(), "\xE2\x8B\xAE");
+ shift_string_attrs(sa, lr.lr_start + 1, -(lr.length() - 3));
+ sattr.sa_type = &VC_ROLE;
+ sattr.sa_value = role_t::VCR_HIDDEN;
+ lr.lr_end = lr.lr_start + 3;
+ }
+ }
+}
+
+attr_line_t&
+attr_line_t::rtrim()
+{
+ auto index = this->al_string.length();
+
+ for (; index > 0; index--) {
+ if (find_string_attr_containing(
+ this->al_attrs, &SA_PREFORMATTED, index - 1)
+ != this->al_attrs.end())
+ {
+ break;
+ }
+ if (!isspace(this->al_string[index - 1])) {
+ break;
+ }
+ }
+ if (index > 0 && index < this->al_string.length()) {
+ this->erase(index);
+ }
+ return *this;
+}
+
+attr_line_t&
+attr_line_t::erase(size_t pos, size_t len)
+{
+ if (len == std::string::npos) {
+ len = this->al_string.size() - pos;
+ }
+ if (len == 0) {
+ return *this;
+ }
+
+ this->al_string.erase(pos, len);
+
+ shift_string_attrs(this->al_attrs, pos, -((int32_t) len));
+ auto new_end = std::remove_if(
+ this->al_attrs.begin(), this->al_attrs.end(), [](const auto& attr) {
+ return attr.sa_range.empty();
+ });
+ this->al_attrs.erase(new_end, this->al_attrs.end());
+
+ return *this;
+}
+
+attr_line_t&
+attr_line_t::pad_to(ssize_t size)
+{
+ const auto curr_len = this->utf8_length_or_length();
+
+ if (curr_len < size) {
+ this->append((size - curr_len), ' ');
+ for (auto& attr : this->al_attrs) {
+ if (attr.sa_range.lr_start == 0 && attr.sa_range.lr_end == curr_len)
+ {
+ attr.sa_range.lr_end = this->al_string.length();
+ }
+ }
+ }
+
+ return *this;
+}
+
+line_range
+line_range::intersection(const line_range& other) const
+{
+ int actual_end;
+
+ if (this->lr_end == -1) {
+ actual_end = other.lr_end;
+ } else if (other.lr_end == -1) {
+ actual_end = this->lr_end;
+ } else {
+ actual_end = std::min(this->lr_end, other.lr_end);
+ }
+ return line_range{std::max(this->lr_start, other.lr_start), actual_end};
+}
+
+line_range&
+line_range::shift(int32_t start, int32_t amount)
+{
+ if (start == this->lr_start) {
+ if (amount > 0) {
+ this->lr_start += amount;
+ }
+ if (this->lr_end != -1) {
+ this->lr_end += amount;
+ if (this->lr_end < this->lr_start) {
+ this->lr_end = this->lr_start;
+ }
+ }
+ } else if (start < this->lr_start) {
+ this->lr_start = std::max(0, this->lr_start + amount);
+ if (this->lr_end != -1) {
+ this->lr_end = std::max(0, this->lr_end + amount);
+ }
+ } else if (this->lr_end != -1) {
+ if (start < this->lr_end) {
+ if (amount < 0 && amount < (start - this->lr_end)) {
+ this->lr_end = start;
+ } else {
+ this->lr_end = std::max(this->lr_start, this->lr_end + amount);
+ }
+ }
+ }
+
+ return *this;
+}
diff --git a/src/base/attr_line.hh b/src/base/attr_line.hh
new file mode 100644
index 0000000..c9cb6a8
--- /dev/null
+++ b/src/base/attr_line.hh
@@ -0,0 +1,758 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file attr_line.hh
+ */
+
+#ifndef attr_line_hh
+#define attr_line_hh
+
+#include <new>
+#include <string>
+#include <vector>
+
+#include <limits.h>
+
+#include "fmt/format.h"
+#include "intern_string.hh"
+#include "string_attr_type.hh"
+#include "string_util.hh"
+
+/**
+ * Encapsulates a range in a string.
+ */
+struct line_range {
+ enum class unit {
+ bytes,
+ codepoint,
+ };
+
+ int lr_start;
+ int lr_end;
+ unit lr_unit;
+
+ explicit line_range(int start = -1, int end = -1, unit u = unit::bytes)
+ : lr_start(start), lr_end(end), lr_unit(u)
+ {
+ }
+
+ bool is_valid() const { return this->lr_start != -1; }
+
+ int length() const
+ {
+ return this->lr_end == -1 ? INT_MAX : this->lr_end - this->lr_start;
+ }
+
+ bool empty() const { return this->length() == 0; }
+
+ void clear()
+ {
+ this->lr_start = -1;
+ this->lr_end = -1;
+ }
+
+ int end_for_string(const std::string& str) const
+ {
+ return this->lr_end == -1 ? str.length() : this->lr_end;
+ }
+
+ bool contains(int pos) const
+ {
+ return this->lr_start <= pos
+ && (this->lr_end == -1 || pos < this->lr_end);
+ }
+
+ bool contains(const struct line_range& other) const
+ {
+ return this->contains(other.lr_start)
+ && (this->lr_end == -1 || other.lr_end <= this->lr_end);
+ }
+
+ bool intersects(const struct line_range& other) const
+ {
+ if (this->contains(other.lr_start)) {
+ return true;
+ }
+ if (other.lr_end > 0 && this->contains(other.lr_end - 1)) {
+ return true;
+ }
+ if (other.contains(this->lr_start)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ line_range intersection(const struct line_range& other) const;
+
+ line_range& shift(int32_t start, int32_t amount);
+
+ void ltrim(const char* str)
+ {
+ while (this->lr_start < this->lr_end && isspace(str[this->lr_start])) {
+ this->lr_start += 1;
+ }
+ }
+
+ bool operator<(const struct line_range& rhs) const
+ {
+ if (this->lr_start < rhs.lr_start) {
+ return true;
+ }
+ if (this->lr_start > rhs.lr_start) {
+ return false;
+ }
+
+ // this->lr_start == rhs.lr_start
+ if (this->lr_end == rhs.lr_end) {
+ return false;
+ }
+
+ if (this->lr_end < rhs.lr_end) {
+ return false;
+ }
+ return true;
+ }
+
+ bool operator==(const struct line_range& rhs) const
+ {
+ return (this->lr_start == rhs.lr_start && this->lr_end == rhs.lr_end);
+ }
+
+ const char* substr(const std::string& str) const
+ {
+ if (this->lr_start == -1) {
+ return str.c_str();
+ }
+ return &(str.c_str()[this->lr_start]);
+ }
+
+ size_t sublen(const std::string& str) const
+ {
+ if (this->lr_start == -1) {
+ return str.length();
+ }
+ if (this->lr_end == -1) {
+ return str.length() - this->lr_start;
+ }
+ return this->length();
+ }
+};
+
+inline line_range
+to_line_range(const string_fragment& frag)
+{
+ return line_range{frag.sf_begin, frag.sf_end};
+}
+
+struct string_attr {
+ string_attr(const struct line_range& lr, const string_attr_pair& value)
+ : sa_range(lr), sa_type(value.first), sa_value(value.second)
+ {
+ }
+
+ string_attr() = default;
+
+ bool operator<(const struct string_attr& rhs) const
+ {
+ if (this->sa_range < rhs.sa_range) {
+ return true;
+ }
+ if (this->sa_range == rhs.sa_range && this->sa_type == rhs.sa_type
+ && this->sa_type == &VC_ROLE
+ && this->sa_value.get<role_t>() < rhs.sa_value.get<role_t>())
+ {
+ return true;
+ }
+
+ return false;
+ }
+
+ struct line_range sa_range;
+ const string_attr_type_base* sa_type{nullptr};
+ string_attr_value sa_value;
+};
+
+template<typename T>
+struct string_attr_wrapper {
+ explicit string_attr_wrapper(const string_attr* sa) : saw_string_attr(sa) {}
+
+ template<typename U = T>
+ std::enable_if_t<!std::is_void<U>::value, const U&> get() const
+ {
+ return this->saw_string_attr->sa_value.template get<T>();
+ }
+
+ const string_attr* saw_string_attr;
+};
+
+/** A map of line ranges to attributes for that range. */
+using string_attrs_t = std::vector<string_attr>;
+
+inline string_attrs_t::const_iterator
+find_string_attr(const string_attrs_t& sa,
+ const string_attr_type_base* type,
+ int start = 0)
+{
+ string_attrs_t::const_iterator iter;
+
+ for (iter = sa.begin(); iter != sa.end(); ++iter) {
+ if (iter->sa_type == type && iter->sa_range.lr_start >= start) {
+ break;
+ }
+ }
+
+ return iter;
+}
+
+inline nonstd::optional<const string_attr*>
+get_string_attr(const string_attrs_t& sa,
+ const string_attr_type_base* type,
+ int start = 0)
+{
+ auto iter = find_string_attr(sa, type, start);
+
+ if (iter == sa.end()) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional(&(*iter));
+}
+
+template<typename T>
+inline nonstd::optional<string_attr_wrapper<T>>
+get_string_attr(const string_attrs_t& sa,
+ const string_attr_type<T>& type,
+ int start = 0)
+{
+ auto iter = find_string_attr(sa, &type, start);
+
+ if (iter == sa.end()) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional(string_attr_wrapper<T>(&(*iter)));
+}
+
+template<typename T>
+inline string_attrs_t::const_iterator
+find_string_attr_containing(const string_attrs_t& sa,
+ const string_attr_type_base* type,
+ T x)
+{
+ string_attrs_t::const_iterator iter;
+
+ for (iter = sa.begin(); iter != sa.end(); ++iter) {
+ if (iter->sa_type == type && iter->sa_range.contains(x)) {
+ break;
+ }
+ }
+
+ return iter;
+}
+
+inline string_attrs_t::iterator
+find_string_attr(string_attrs_t& sa, const struct line_range& lr)
+{
+ string_attrs_t::iterator iter;
+
+ for (iter = sa.begin(); iter != sa.end(); ++iter) {
+ if (lr.contains(iter->sa_range)) {
+ break;
+ }
+ }
+
+ return iter;
+}
+
+inline string_attrs_t::const_iterator
+find_string_attr(const string_attrs_t& sa, size_t near)
+{
+ auto nearest = sa.end();
+ ssize_t last_diff = INT_MAX;
+
+ for (auto iter = sa.begin(); iter != sa.end(); ++iter) {
+ const auto& lr = iter->sa_range;
+
+ if (!lr.is_valid() || !lr.contains(near)) {
+ continue;
+ }
+
+ ssize_t diff = near - lr.lr_start;
+ if (diff < last_diff) {
+ last_diff = diff;
+ nearest = iter;
+ }
+ }
+
+ return nearest;
+}
+
+template<typename T>
+inline string_attrs_t::const_iterator
+rfind_string_attr_if(const string_attrs_t& sa, ssize_t near, T predicate)
+{
+ auto nearest = sa.end();
+ ssize_t last_diff = INT_MAX;
+
+ for (auto iter = sa.begin(); iter != sa.end(); ++iter) {
+ const auto& lr = iter->sa_range;
+
+ if (lr.lr_start > near) {
+ continue;
+ }
+
+ if (!predicate(*iter)) {
+ continue;
+ }
+
+ ssize_t diff = near - lr.lr_start;
+ if (diff < last_diff) {
+ last_diff = diff;
+ nearest = iter;
+ }
+ }
+
+ return nearest;
+}
+
+inline struct line_range
+find_string_attr_range(const string_attrs_t& sa, string_attr_type_base* type)
+{
+ auto iter = find_string_attr(sa, type);
+
+ if (iter != sa.end()) {
+ return iter->sa_range;
+ }
+
+ return line_range();
+}
+
+inline void
+remove_string_attr(string_attrs_t& sa, const struct line_range& lr)
+{
+ string_attrs_t::iterator iter;
+
+ while ((iter = find_string_attr(sa, lr)) != sa.end()) {
+ sa.erase(iter);
+ }
+}
+
+inline void
+remove_string_attr(string_attrs_t& sa, string_attr_type_base* type)
+{
+ for (auto iter = sa.begin(); iter != sa.end();) {
+ if (iter->sa_type == type) {
+ iter = sa.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+}
+
+inline void
+shift_string_attrs(string_attrs_t& sa, int32_t start, int32_t amount)
+{
+ for (auto& iter : sa) {
+ iter.sa_range.shift(start, amount);
+ }
+}
+
+struct text_wrap_settings {
+ text_wrap_settings& with_indent(int indent)
+ {
+ this->tws_indent = indent;
+ return *this;
+ }
+
+ text_wrap_settings& with_padding_indent(int indent)
+ {
+ this->tws_padding_indent = indent;
+ return *this;
+ }
+
+ text_wrap_settings& with_width(int width)
+ {
+ this->tws_width = width;
+ return *this;
+ }
+
+ int tws_indent{2};
+ int tws_width{80};
+ int tws_padding_indent{0};
+};
+
+/**
+ * A line that has attributes.
+ */
+class attr_line_t {
+public:
+ attr_line_t() = default;
+
+ attr_line_t(std::string str) : al_string(std::move(str)) {}
+
+ attr_line_t(const char* str) : al_string(str) {}
+
+ static inline attr_line_t from_ansi_str(const char* str)
+ {
+ attr_line_t retval;
+
+ return retval.with_ansi_string("%s", str);
+ }
+
+ /** @return The string itself. */
+ std::string& get_string() { return this->al_string; }
+
+ const std::string& get_string() const { return this->al_string; }
+
+ /** @return The attributes for the string. */
+ string_attrs_t& get_attrs() { return this->al_attrs; }
+
+ const string_attrs_t& get_attrs() const { return this->al_attrs; }
+
+ attr_line_t& with_string(const std::string& str)
+ {
+ this->al_string = str;
+ return *this;
+ }
+
+ attr_line_t& with_ansi_string(const char* str, ...);
+
+ attr_line_t& with_ansi_string(const std::string& str);
+
+ attr_line_t& with_attr(const string_attr& sa)
+ {
+ this->al_attrs.push_back(sa);
+ return *this;
+ }
+
+ attr_line_t& ensure_space()
+ {
+ if (!this->al_string.empty() && this->al_string.back() != ' '
+ && this->al_string.back() != '[')
+ {
+ this->append(1, ' ');
+ }
+
+ return *this;
+ }
+
+ template<typename S>
+ attr_line_t& append(S str, const string_attr_pair& value)
+ {
+ size_t start_len = this->al_string.length();
+
+ this->al_string.append(str);
+
+ line_range lr{(int) start_len, (int) this->al_string.length()};
+
+ this->al_attrs.emplace_back(lr, value);
+
+ return *this;
+ }
+
+ template<typename S>
+ attr_line_t& append(const std::pair<S, string_attr_pair>& value)
+ {
+ size_t start_len = this->al_string.length();
+
+ this->al_string.append(std::move(value.first));
+
+ line_range lr{(int) start_len, (int) this->al_string.length()};
+
+ this->al_attrs.emplace_back(lr, value.second);
+
+ return *this;
+ }
+
+ template<typename S>
+ attr_line_t& append_quoted(const std::pair<S, string_attr_pair>& value)
+ {
+ this->al_string.append("\u201c");
+
+ size_t start_len = this->al_string.length();
+
+ this->al_string.append(std::move(value.first));
+
+ line_range lr{(int) start_len, (int) this->al_string.length()};
+
+ this->al_attrs.emplace_back(lr, value.second);
+
+ this->al_string.append("\u201d");
+
+ return *this;
+ }
+
+ attr_line_t& append_quoted(const intern_string_t str)
+ {
+ this->al_string.append("\u201c");
+ this->al_string.append(str.get(), str.size());
+ this->al_string.append("\u201d");
+
+ return *this;
+ }
+
+ attr_line_t& append_quoted(const attr_line_t& al)
+ {
+ this->al_string.append("\u201c");
+ this->append(al);
+ this->al_string.append("\u201d");
+
+ return *this;
+ }
+
+ template<typename S>
+ attr_line_t& append_quoted(S s)
+ {
+ this->al_string.append("\u201c");
+ this->append(std::move(s));
+ this->al_string.append("\u201d");
+
+ return *this;
+ }
+
+ attr_line_t& append(const intern_string_t str)
+ {
+ this->al_string.append(str.get(), str.size());
+ return *this;
+ }
+
+ attr_line_t& append(const string_fragment& str)
+ {
+ this->al_string.append(str.data(), str.length());
+ return *this;
+ }
+
+ template<typename S>
+ attr_line_t& append(S str)
+ {
+ this->al_string.append(str);
+ return *this;
+ }
+
+ template<typename... Args>
+ attr_line_t& appendf(fmt::format_string<Args...> fstr, Args&&... args)
+ {
+ this->template append(
+ fmt::vformat(fstr, fmt::make_format_args(args...)));
+ return *this;
+ }
+
+ attr_line_t& with_attr_for_all(const string_attr_pair& sap)
+ {
+ this->al_attrs.emplace_back(line_range{0, -1}, sap);
+ return *this;
+ }
+
+ template<typename C, typename F>
+ attr_line_t& join(const C& container,
+ const string_attr_pair& sap,
+ const F& fill)
+ {
+ bool init = true;
+ for (const auto& elem : container) {
+ if (!init) {
+ this->append(fill);
+ }
+ this->append(std::make_pair(elem, sap));
+ init = false;
+ }
+
+ return *this;
+ }
+
+ template<typename C, typename F>
+ attr_line_t& join(const C& container, const F& fill)
+ {
+ bool init = true;
+ for (const auto& elem : container) {
+ if (!init) {
+ this->append(fill);
+ }
+ this->append(elem);
+ init = false;
+ }
+
+ return *this;
+ }
+
+ attr_line_t& insert(size_t index,
+ const attr_line_t& al,
+ text_wrap_settings* tws = nullptr);
+
+ attr_line_t& append(const attr_line_t& al,
+ text_wrap_settings* tws = nullptr)
+ {
+ return this->insert(this->al_string.length(), al, tws);
+ }
+
+ attr_line_t& append(size_t len, char c)
+ {
+ this->al_string.append(len, c);
+ return *this;
+ }
+
+ attr_line_t& insert(size_t index, size_t len, char c)
+ {
+ this->al_string.insert(index, len, c);
+
+ shift_string_attrs(this->al_attrs, index, len);
+
+ return *this;
+ }
+
+ attr_line_t& insert(size_t index, const char* str)
+ {
+ this->al_string.insert(index, str);
+
+ shift_string_attrs(this->al_attrs, index, strlen(str));
+
+ return *this;
+ }
+
+ template<typename... Args>
+ attr_line_t& add_header(Args... args)
+ {
+ if (!this->blank()) {
+ this->insert(0, args...);
+ }
+ return *this;
+ }
+
+ template<typename... Args>
+ attr_line_t& with_default(Args... args)
+ {
+ if (this->blank()) {
+ this->clear();
+ this->append(args...);
+ }
+
+ return *this;
+ }
+
+ attr_line_t& erase(size_t pos, size_t len = std::string::npos);
+
+ attr_line_t& rtrim();
+
+ attr_line_t& erase_utf8_chars(size_t start)
+ {
+ auto byte_index = utf8_char_to_byte_index(this->al_string, start);
+ this->erase(byte_index);
+
+ return *this;
+ }
+
+ attr_line_t& right_justify(unsigned long width);
+
+ attr_line_t& pad_to(ssize_t size);
+
+ ssize_t length() const
+ {
+ size_t retval = this->al_string.length();
+
+ for (const auto& al_attr : this->al_attrs) {
+ retval = std::max(retval, (size_t) al_attr.sa_range.lr_start);
+ if (al_attr.sa_range.lr_end != -1) {
+ retval = std::max(retval, (size_t) al_attr.sa_range.lr_end);
+ }
+ }
+
+ return retval;
+ }
+
+ Result<size_t, const char*> utf8_length() const
+ {
+ return utf8_string_length(this->al_string);
+ }
+
+ ssize_t utf8_length_or_length() const
+ {
+ return utf8_string_length(this->al_string).unwrapOr(this->length());
+ }
+
+ std::string get_substring(const line_range& lr) const
+ {
+ if (!lr.is_valid()) {
+ return "";
+ }
+ return this->al_string.substr(lr.lr_start, lr.sublen(this->al_string));
+ }
+
+ string_fragment to_string_fragment(
+ string_attrs_t::const_iterator iter) const
+ {
+ return string_fragment(this->al_string.c_str(),
+ iter->sa_range.lr_start,
+ iter->sa_range.end_for_string(this->al_string));
+ }
+
+ string_attrs_t::const_iterator find_attr(size_t near) const
+ {
+ near = std::min(near, this->al_string.length() - 1);
+
+ while (near > 0 && isspace(this->al_string[near])) {
+ near -= 1;
+ }
+
+ return find_string_attr(this->al_attrs, near);
+ }
+
+ bool empty() const { return this->length() == 0; }
+
+ bool blank() const { return is_blank(this->al_string); }
+
+ /** Clear the string and the attributes for the string. */
+ attr_line_t& clear()
+ {
+ this->al_string.clear();
+ this->al_attrs.clear();
+
+ return *this;
+ }
+
+ attr_line_t subline(size_t start, size_t len = std::string::npos) const;
+
+ void split_lines(std::vector<attr_line_t>& lines) const;
+
+ std::vector<attr_line_t> split_lines() const
+ {
+ std::vector<attr_line_t> retval;
+
+ this->split_lines(retval);
+ return retval;
+ }
+
+ size_t nearest_text(size_t x) const;
+
+ void apply_hide();
+
+ std::string al_string;
+ string_attrs_t al_attrs;
+};
+
+#endif
diff --git a/src/base/attr_line.tests.cc b/src/base/attr_line.tests.cc
new file mode 100644
index 0000000..53b338e
--- /dev/null
+++ b/src/base/attr_line.tests.cc
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <iostream>
+
+#include "attr_line.hh"
+
+#include "config.h"
+#include "doctest/doctest.h"
+
+using namespace lnav::roles::literals;
+
+TEST_CASE("attr_line_t::basic-wrapping")
+{
+ text_wrap_settings tws = {3, 21};
+ attr_line_t to_be_wrapped{"This line, right here, needs to be wrapped."};
+ attr_line_t dst;
+
+ to_be_wrapped.al_attrs.emplace_back(
+ line_range{0, (int) to_be_wrapped.al_string.length()},
+ VC_ROLE.value(role_t::VCR_ERROR));
+ dst.append(to_be_wrapped, &tws);
+
+ CHECK(dst.get_string() ==
+ "This line, right\n"
+ " here, needs to be\n"
+ " wrapped.");
+
+ for (const auto& attr : dst.al_attrs) {
+ printf("attr %d:%d %s\n",
+ attr.sa_range.lr_start,
+ attr.sa_range.lr_end,
+ attr.sa_type->sat_name);
+ }
+}
+
+TEST_CASE("attr_line_t::unicode-wrap")
+{
+ text_wrap_settings tws = {3, 21};
+ attr_line_t prefix;
+
+ prefix.append(" ")
+ .append("\u2022"_list_glyph)
+ .append(" ")
+ .with_attr_for_all(SA_PREFORMATTED.value());
+
+ attr_line_t body;
+ body.append("This is a long line that needs to be wrapped and indented");
+
+ attr_line_t li;
+
+ li.append(prefix)
+ .append(body, &tws)
+ .with_attr_for_all(SA_PREFORMATTED.value());
+
+ attr_line_t dst;
+
+ dst.append(li);
+
+ CHECK(dst.get_string()
+ == " \u2022 This is a long\n"
+ " line that needs to\n"
+ " be wrapped and\n"
+ " indented");
+}
diff --git a/src/base/auto_fd.hh b/src/base/auto_fd.hh
new file mode 100644
index 0000000..da4a582
--- /dev/null
+++ b/src/base/auto_fd.hh
@@ -0,0 +1,318 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file auto_fd.hh
+ */
+
+#ifndef auto_fd_hh
+#define auto_fd_hh
+
+#include <exception>
+#include <new>
+#include <string>
+
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/select.h>
+#include <unistd.h>
+
+#include "base/lnav_log.hh"
+#include "base/result.h"
+
+/**
+ * Resource management class for file descriptors.
+ *
+ * @see auto_ptr
+ */
+class auto_fd {
+public:
+ /**
+ * Wrapper for the posix pipe(2) function that stores the file descriptor
+ * results in an auto_fd array.
+ *
+ * @param af An array of at least two auto_fd elements, where the first
+ * contains the reader end of the pipe and the second contains the writer.
+ * @return The result of the pipe(2) function.
+ */
+ static int pipe(auto_fd* af)
+ {
+ int retval, fd[2];
+
+ require(af != nullptr);
+
+ if ((retval = ::pipe(fd)) == 0) {
+ af[0] = fd[0];
+ af[1] = fd[1];
+ }
+
+ return retval;
+ }
+
+ /**
+ * dup(2) the given file descriptor and wrap it in an auto_fd.
+ *
+ * @param fd The file descriptor to duplicate.
+ * @return A new auto_fd that contains the duplicated file descriptor.
+ */
+ static auto_fd dup_of(int fd)
+ {
+ if (fd == -1) {
+ return auto_fd{};
+ }
+
+ auto new_fd = ::dup(fd);
+
+ if (new_fd == -1) {
+ throw std::bad_alloc();
+ }
+
+ return auto_fd(new_fd);
+ }
+
+ /**
+ * Construct an auto_fd to manage the given file descriptor.
+ *
+ * @param fd The file descriptor to be managed.
+ */
+ explicit auto_fd(int fd = -1) : af_fd(fd) { require(fd >= -1); }
+
+ /**
+ * Non-const copy constructor. Management of the file descriptor will be
+ * transferred from the source to this object and the source will be
+ * cleared.
+ *
+ * @param af The source of the file descriptor.
+ */
+ auto_fd(auto_fd&& af) noexcept : af_fd(af.release()) {}
+
+ /**
+ * Const copy constructor. The file descriptor from the source will be
+ * dup(2)'d and the new descriptor stored in this object.
+ *
+ * @param af The source of the file descriptor.
+ */
+ auto_fd(const auto_fd& af) = delete;
+
+ auto_fd dup() const
+ {
+ int new_fd;
+
+ if (this->af_fd == -1 || (new_fd = ::dup(this->af_fd)) == -1) {
+ throw std::bad_alloc();
+ }
+
+ return auto_fd{new_fd};
+ }
+
+ /**
+ * Destructor that will close the file descriptor managed by this object.
+ */
+ ~auto_fd() { this->reset(); }
+
+ /** @return The file descriptor as a plain integer. */
+ operator int() const { return this->af_fd; }
+
+ /**
+ * Replace the current descriptor with the given one. The current
+ * descriptor will be closed.
+ *
+ * @param fd The file descriptor to store in this object.
+ * @return *this
+ */
+ auto_fd& operator=(int fd)
+ {
+ require(fd >= -1);
+
+ this->reset(fd);
+ return *this;
+ }
+
+ /**
+ * Transfer management of the given file descriptor to this object.
+ *
+ * @param af The old manager of the file descriptor.
+ * @return *this
+ */
+ auto_fd& operator=(auto_fd&& af) noexcept
+ {
+ this->reset(af.release());
+ return *this;
+ }
+
+ /**
+ * Return a pointer that can be passed to functions that require an out
+ * parameter for file descriptors (e.g. openpty).
+ *
+ * @return A pointer to the internal integer.
+ */
+ int* out()
+ {
+ this->reset();
+ return &this->af_fd;
+ }
+
+ /**
+ * Stop managing the file descriptor in this object and return its value.
+ *
+ * @return The file descriptor.
+ */
+ int release()
+ {
+ int retval = this->af_fd;
+
+ this->af_fd = -1;
+ return retval;
+ }
+
+ /**
+ * @return The file descriptor.
+ */
+ int get() const { return this->af_fd; }
+
+ /**
+ * Closes the current file descriptor and replaces its value with the given
+ * one.
+ *
+ * @param fd The new file descriptor to be managed.
+ */
+ void reset(int fd = -1)
+ {
+ require(fd >= -1);
+
+ if (this->af_fd != fd) {
+ if (this->af_fd != -1) {
+ switch (this->af_fd) {
+ case STDIN_FILENO:
+ case STDOUT_FILENO:
+ case STDERR_FILENO:
+ break;
+ default:
+ close(this->af_fd);
+ break;
+ }
+ }
+ this->af_fd = fd;
+ }
+ }
+
+ void close_on_exec() const
+ {
+ if (this->af_fd == -1) {
+ return;
+ }
+ log_perror(fcntl(this->af_fd, F_SETFD, FD_CLOEXEC));
+ }
+
+private:
+ int af_fd; /*< The managed file descriptor. */
+};
+
+class auto_pipe {
+public:
+ static Result<auto_pipe, std::string> for_child_fd(int child_fd)
+ {
+ auto_pipe retval(child_fd);
+
+ if (retval.open() == -1) {
+ return Err(std::string(strerror(errno)));
+ }
+
+ return Ok(std::move(retval));
+ }
+
+ explicit auto_pipe(int child_fd = -1, int child_flags = O_RDONLY)
+ : ap_child_flags(child_flags), ap_child_fd(child_fd)
+ {
+ switch (child_fd) {
+ case STDIN_FILENO:
+ this->ap_child_flags = O_RDONLY;
+ break;
+ case STDOUT_FILENO:
+ case STDERR_FILENO:
+ this->ap_child_flags = O_WRONLY;
+ break;
+ }
+ }
+
+ int open() { return auto_fd::pipe(this->ap_fd); }
+
+ void close()
+ {
+ this->ap_fd[0].reset();
+ this->ap_fd[1].reset();
+ }
+
+ auto_fd& read_end() { return this->ap_fd[0]; }
+
+ auto_fd& write_end() { return this->ap_fd[1]; }
+
+ void after_fork(pid_t child_pid)
+ {
+ int new_fd;
+
+ switch (child_pid) {
+ case -1:
+ this->close();
+ break;
+ case 0:
+ if (this->ap_child_flags == O_RDONLY) {
+ this->write_end().reset();
+ if (this->read_end().get() == -1) {
+ this->read_end() = ::open("/dev/null", O_RDONLY);
+ }
+ new_fd = this->read_end().get();
+ } else {
+ this->read_end().reset();
+ if (this->write_end().get() == -1) {
+ this->write_end() = ::open("/dev/null", O_WRONLY);
+ }
+ new_fd = this->write_end().get();
+ }
+ if (this->ap_child_fd != -1) {
+ if (new_fd != this->ap_child_fd) {
+ dup2(new_fd, this->ap_child_fd);
+ this->close();
+ }
+ }
+ break;
+ default:
+ if (this->ap_child_flags == O_RDONLY) {
+ this->read_end().reset();
+ } else {
+ this->write_end().reset();
+ }
+ break;
+ }
+ }
+
+ int ap_child_flags;
+ int ap_child_fd;
+ auto_fd ap_fd[2];
+};
+
+#endif
diff --git a/src/base/auto_mem.hh b/src/base/auto_mem.hh
new file mode 100644
index 0000000..e6b456c
--- /dev/null
+++ b/src/base/auto_mem.hh
@@ -0,0 +1,402 @@
+/**
+ * Copyright (c) 2007-2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file auto_mem.hh
+ */
+
+#ifndef lnav_auto_mem_hh
+#define lnav_auto_mem_hh
+
+#include <exception>
+#include <iterator>
+#include <string>
+#include <utility>
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "base/result.h"
+
+using free_func_t = void (*)(void*);
+
+/**
+ * Resource management class for memory allocated by a custom allocator.
+ *
+ * @param T The object type.
+ * @param auto_free The function to call to free the managed object.
+ */
+template<class T, free_func_t default_free = free>
+class auto_mem {
+public:
+ static void noop_free(void*) {}
+
+ static auto_mem<T> leak(T* ptr)
+ {
+ auto_mem<T> retval(noop_free);
+
+ retval = ptr;
+
+ return retval;
+ }
+
+ explicit auto_mem(T* ptr = nullptr)
+ : am_ptr(ptr), am_free_func(default_free)
+ {
+ }
+
+ auto_mem(const auto_mem& am) = delete;
+
+ template<typename F>
+ explicit auto_mem(F free_func) noexcept
+ : am_ptr(nullptr), am_free_func((free_func_t) free_func)
+ {
+ }
+
+ auto_mem(auto_mem&& other) noexcept
+ : am_ptr(other.release()), am_free_func(other.am_free_func)
+ {
+ }
+
+ ~auto_mem() { this->reset(); }
+
+ bool empty() const { return this->am_ptr == nullptr; }
+
+ operator T*() const { return this->am_ptr; }
+
+ T* operator->() { return this->am_ptr; }
+
+ auto_mem& operator=(T* ptr)
+ {
+ this->reset(ptr);
+ return *this;
+ }
+
+ auto_mem& operator=(auto_mem&) = delete;
+
+ auto_mem& operator=(auto_mem&& am) noexcept
+ {
+ this->reset(am.release());
+ this->am_free_func = am.am_free_func;
+ return *this;
+ }
+
+ T* release()
+ {
+ T* retval = this->am_ptr;
+
+ this->am_ptr = nullptr;
+ return retval;
+ }
+
+ T* in() const { return this->am_ptr; }
+
+ T** out()
+ {
+ this->reset();
+ return &this->am_ptr;
+ }
+
+ template<typename F>
+ F get_free_func() const
+ {
+ return (F) this->am_free_func;
+ }
+
+ void reset(T* ptr = nullptr)
+ {
+ if (this->am_ptr != ptr) {
+ if (this->am_ptr != nullptr) {
+ this->am_free_func((void*) this->am_ptr);
+ }
+ this->am_ptr = ptr;
+ }
+ }
+
+private:
+ T* am_ptr;
+ void (*am_free_func)(void*);
+};
+
+template<typename T, void (*free_func)(T*)>
+class static_root_mem {
+public:
+ static_root_mem() { memset(&this->srm_value, 0, sizeof(T)); }
+
+ ~static_root_mem() { free_func(&this->srm_value); }
+
+ const T* operator->() const { return &this->srm_value; }
+
+ const T& in() const { return this->srm_value; }
+
+ T* inout()
+ {
+ free_func(&this->srm_value);
+ memset(&this->srm_value, 0, sizeof(T));
+ return &this->srm_value;
+ }
+
+private:
+ static_root_mem& operator=(T&) { return *this; }
+
+ static_root_mem& operator=(static_root_mem&) { return *this; }
+
+ T srm_value;
+};
+
+class auto_buffer {
+public:
+ using value_type = char;
+
+ static auto_buffer alloc(size_t capacity)
+ {
+ return auto_buffer{capacity == 0 ? nullptr : (char*) malloc(capacity),
+ capacity};
+ }
+
+ static auto_buffer alloc_bitmap(size_t capacity_in_bits)
+ {
+ return alloc((capacity_in_bits + 7) / 8);
+ }
+
+ static auto_buffer from(const char* mem, size_t size)
+ {
+ auto retval = alloc(size);
+
+ retval.resize(size);
+ memcpy(retval.in(), mem, size);
+ return retval;
+ }
+
+ auto_buffer(const auto_buffer&) = delete;
+
+ auto_buffer(auto_buffer&& other) noexcept
+ : ab_buffer(other.ab_buffer), ab_size(other.ab_size),
+ ab_capacity(other.ab_capacity)
+ {
+ other.ab_buffer = nullptr;
+ other.ab_size = 0;
+ other.ab_capacity = 0;
+ }
+
+ ~auto_buffer()
+ {
+ free(this->ab_buffer);
+ this->ab_buffer = nullptr;
+ this->ab_size = 0;
+ this->ab_capacity = 0;
+ }
+
+ auto_buffer& operator=(auto_buffer&) = delete;
+
+ auto_buffer& operator=(auto_buffer&& other) noexcept
+ {
+ free(this->ab_buffer);
+ this->ab_buffer = std::exchange(other.ab_buffer, nullptr);
+ this->ab_size = std::exchange(other.ab_size, 0);
+ this->ab_capacity = std::exchange(other.ab_capacity, 0);
+ return *this;
+ }
+
+ void swap(auto_buffer& other)
+ {
+ std::swap(this->ab_buffer, other.ab_buffer);
+ std::swap(this->ab_size, other.ab_size);
+ std::swap(this->ab_capacity, other.ab_capacity);
+ }
+
+ char* in() { return this->ab_buffer; }
+
+ char* at(size_t offset) { return &this->ab_buffer[offset]; }
+
+ const char* at(size_t offset) const { return &this->ab_buffer[offset]; }
+
+ char* begin() { return this->ab_buffer; }
+
+ const char* begin() const { return this->ab_buffer; }
+
+ auto_buffer& push_back(char ch)
+ {
+ if (this->ab_size == this->ab_capacity) {
+ this->expand_by(256);
+ }
+ this->ab_buffer[this->ab_size] = ch;
+ this->ab_size += 1;
+
+ return *this;
+ }
+
+ void pop_back() { this->ab_size -= 1; }
+
+ bool is_bit_set(size_t bit_offset) const
+ {
+ size_t byte_offset = bit_offset / 8;
+ auto bitmask = 1UL << (bit_offset % 8);
+
+ return this->ab_buffer[byte_offset] & bitmask;
+ }
+
+ void set_bit(size_t bit_offset)
+ {
+ size_t byte_offset = bit_offset / 8;
+ auto bitmask = 1UL << (bit_offset % 8);
+
+ this->ab_buffer[byte_offset] |= bitmask;
+ }
+
+ void clear_bit(size_t bit_offset)
+ {
+ size_t byte_offset = bit_offset / 8;
+ auto bitmask = 1UL << (bit_offset % 8);
+
+ this->ab_buffer[byte_offset] &= ~bitmask;
+ }
+
+ std::reverse_iterator<char*> rbegin()
+ {
+ return std::reverse_iterator<char*>(this->end());
+ }
+
+ std::reverse_iterator<const char*> rbegin() const
+ {
+ return std::reverse_iterator<const char*>(this->end());
+ }
+
+ char* end() { return &this->ab_buffer[this->ab_size]; }
+
+ const char* end() const { return &this->ab_buffer[this->ab_size]; }
+
+ std::reverse_iterator<char*> rend()
+ {
+ return std::reverse_iterator<char*>(this->begin());
+ }
+
+ std::reverse_iterator<const char*> rend() const
+ {
+ return std::reverse_iterator<const char*>(this->begin());
+ }
+
+ std::pair<char*, size_t> release()
+ {
+ auto retval = std::make_pair(this->ab_buffer, this->ab_size);
+
+ this->ab_buffer = nullptr;
+ this->ab_size = 0;
+ this->ab_capacity = 0;
+ return retval;
+ }
+
+ size_t size() const { return this->ab_size; }
+
+ size_t bitmap_size() const { return this->ab_size * 8; }
+
+ bool empty() const { return this->ab_size == 0; }
+
+ bool full() const { return this->ab_size == this->ab_capacity; }
+
+ size_t capacity() const { return this->ab_capacity; }
+
+ size_t available() const { return this->ab_capacity - this->ab_size; }
+
+ void clear() { this->resize(0); }
+
+ auto_buffer& resize(size_t new_size)
+ {
+ assert(new_size <= this->ab_capacity);
+
+ this->ab_size = new_size;
+ return *this;
+ }
+
+ auto_buffer& resize_bitmap(size_t new_size_in_bits, int fill = 0)
+ {
+ auto new_size = (new_size_in_bits + 7) / 8;
+ assert(new_size <= this->ab_capacity);
+
+ auto old_size = std::exchange(this->ab_size, new_size);
+ memset(this->at(old_size), 0, this->ab_size - old_size);
+ return *this;
+ }
+
+ auto_buffer& resize_by(ssize_t amount)
+ {
+ return this->resize(this->ab_size + amount);
+ }
+
+ void expand_to(size_t new_capacity)
+ {
+ if (new_capacity <= this->ab_capacity) {
+ return;
+ }
+ auto* new_buffer = (char*) realloc(this->ab_buffer, new_capacity);
+
+ if (new_buffer == nullptr) {
+ throw std::bad_alloc();
+ }
+
+ this->ab_buffer = new_buffer;
+ this->ab_capacity = new_capacity;
+ }
+
+ void expand_bitmap_to(size_t new_capacity_in_bits)
+ {
+ this->expand_to((new_capacity_in_bits + 7) / 8);
+ }
+
+ void expand_by(size_t amount)
+ {
+ if (amount == 0) {
+ return;
+ }
+
+ this->expand_to(this->ab_capacity + amount);
+ }
+
+ std::string to_string() const { return {this->ab_buffer, this->ab_size}; }
+
+private:
+ auto_buffer(char* buffer, size_t capacity)
+ : ab_buffer(buffer), ab_capacity(capacity)
+ {
+ }
+
+ char* ab_buffer;
+ size_t ab_size{0};
+ size_t ab_capacity;
+};
+
+struct text_auto_buffer {
+ auto_buffer inner;
+};
+
+struct blob_auto_buffer {
+ auto_buffer inner;
+};
+
+#endif
diff --git a/src/base/auto_pid.cc b/src/base/auto_pid.cc
new file mode 100644
index 0000000..a662988
--- /dev/null
+++ b/src/base/auto_pid.cc
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "auto_pid.hh"
+
+#include <unistd.h>
+
+#include "config.h"
+#include "fmt/format.h"
+#include "lnav_log.hh"
+
+namespace lnav {
+namespace pid {
+
+bool in_child = false;
+
+Result<auto_pid<process_state::running>, std::string>
+from_fork()
+{
+ auto pid = ::fork();
+
+ if (pid == -1) {
+ return Err(
+ fmt::format(FMT_STRING("fork() failed: {}"), strerror(errno)));
+ }
+
+ if (pid != 0) {
+ log_debug("started child: %d", pid);
+ } else {
+ in_child = true;
+ }
+
+ return Ok(auto_pid<process_state::running>(pid));
+}
+
+} // namespace pid
+} // namespace lnav
diff --git a/src/base/auto_pid.hh b/src/base/auto_pid.hh
new file mode 100644
index 0000000..702af1e
--- /dev/null
+++ b/src/base/auto_pid.hh
@@ -0,0 +1,175 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file auto_pid.hh
+ */
+
+#ifndef auto_pid_hh
+#define auto_pid_hh
+
+#include <cerrno>
+#include <csignal>
+
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "base/lnav_log.hh"
+#include "base/result.h"
+#include "mapbox/variant.hpp"
+
+enum class process_state {
+ running,
+ finished,
+};
+
+template<process_state ProcState>
+class auto_pid {
+public:
+ explicit auto_pid(pid_t child, int status = 0)
+ : ap_status(status), ap_child(child)
+ {
+ }
+
+ auto_pid(const auto_pid& other) = delete;
+
+ auto_pid(auto_pid&& other) noexcept
+ : ap_status(other.ap_status), ap_child(std::move(other).release())
+ {
+ }
+
+ ~auto_pid() noexcept
+ {
+ this->reset();
+ }
+
+ auto_pid& operator=(auto_pid&& other) noexcept
+ {
+ auto other_status = other.ap_status;
+ this->reset(std::move(other).release());
+ this->ap_status = other_status;
+ return *this;
+ }
+
+ auto_pid& operator=(const auto_pid& other) = delete;
+
+ pid_t in() const
+ {
+ return this->ap_child;
+ }
+
+ bool in_child() const
+ {
+ static_assert(ProcState == process_state::running,
+ "this method is only available in the RUNNING state");
+ return this->ap_child == 0;
+ }
+
+ pid_t release() &&
+ {
+ return std::exchange(this->ap_child, -1); }
+
+ int status() const
+ {
+ static_assert(ProcState == process_state::finished,
+ "wait_for_child() must be called first");
+ return this->ap_status;
+ }
+
+ bool was_normal_exit() const
+ {
+ static_assert(ProcState == process_state::finished,
+ "wait_for_child() must be called first");
+ return WIFEXITED(this->ap_status);
+ }
+
+ int exit_status() const
+ {
+ static_assert(ProcState == process_state::finished,
+ "wait_for_child() must be called first");
+ return WEXITSTATUS(this->ap_status);
+ }
+
+ using poll_result
+ = mapbox::util::variant<auto_pid<process_state::running>,
+ auto_pid<process_state::finished>>;
+
+ poll_result poll() &&
+ {
+ if (this->ap_child != -1) {
+ auto rc = waitpid(this->ap_child, &this->ap_status, WNOHANG);
+
+ if (rc <= 0) {
+ return std::move(*this);
+ }
+ }
+
+ return auto_pid<process_state::finished>(
+ std::exchange(this->ap_child, -1), this->ap_status);
+ }
+
+ auto_pid<process_state::finished> wait_for_child(int options = 0) &&
+ {
+ if (this->ap_child != -1) {
+ while ((waitpid(this->ap_child, &this->ap_status, options)) < 0
+ && (errno == EINTR))
+ {
+ ;
+ }
+ }
+
+ return auto_pid<process_state::finished>(
+ std::exchange(this->ap_child, -1), this->ap_status);
+ }
+
+ void reset(pid_t child = -1) noexcept
+ {
+ if (this->ap_child != child) {
+ this->ap_status = 0;
+ if (ProcState == process_state::running && this->ap_child != -1) {
+ log_debug("sending SIGTERM to child: %d", this->ap_child);
+ kill(this->ap_child, SIGTERM);
+ }
+ this->ap_child = child;
+ }
+ }
+
+private:
+ int ap_status{0};
+ pid_t ap_child;
+};
+
+namespace lnav {
+namespace pid {
+
+extern bool in_child;
+
+Result<auto_pid<process_state::running>, std::string> from_fork();
+} // namespace pid
+} // namespace lnav
+
+#endif
diff --git a/src/base/bus.hh b/src/base/bus.hh
new file mode 100644
index 0000000..dea23ac
--- /dev/null
+++ b/src/base/bus.hh
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_bus_hh
+#define lnav_bus_hh
+
+#include <vector>
+
+#include "lnav_log.hh"
+
+template<typename T>
+class bus {
+public:
+ bus() = default;
+
+ virtual ~bus() { require(this->b_components.empty()); }
+
+ bus(const bus<T>&) = delete;
+
+ void attach(T* component)
+ {
+ this->b_components.template emplace_back(component);
+ }
+
+ void detach(T* component)
+ {
+ auto iter = this->b_components.begin();
+ for (; iter != this->b_components.end(); ++iter) {
+ if (*iter == component) {
+ break;
+ }
+ }
+ require(iter != this->b_components.end());
+
+ this->b_components.erase(iter);
+ }
+
+protected:
+ std::vector<T*> b_components;
+};
+
+#endif
diff --git a/src/base/date_time_scanner.cc b/src/base/date_time_scanner.cc
new file mode 100644
index 0000000..72b7e5d
--- /dev/null
+++ b/src/base/date_time_scanner.cc
@@ -0,0 +1,308 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file date_time_scanner.cc
+ */
+
+#include <chrono>
+
+#include "date_time_scanner.hh"
+
+#include "config.h"
+#include "ptimec.hh"
+#include "scn/scn.h"
+
+size_t
+date_time_scanner::ftime(char* dst,
+ size_t len,
+ const char* const time_fmt[],
+ const exttm& tm) const
+{
+ off_t off = 0;
+
+ if (time_fmt == nullptr) {
+ PTIMEC_FORMATS[this->dts_fmt_lock].pf_ffunc(dst, off, len, tm);
+ if (tm.et_flags & ETF_MILLIS_SET) {
+ dst[off++] = '.';
+ ftime_L(dst, off, len, tm);
+ } else if (tm.et_flags & ETF_MICROS_SET) {
+ dst[off++] = '.';
+ ftime_f(dst, off, len, tm);
+ } else if (tm.et_flags & ETF_NANOS_SET) {
+ dst[off++] = '.';
+ ftime_N(dst, off, len, tm);
+ }
+ dst[off] = '\0';
+ } else {
+ off = ftime_fmt(dst, len, time_fmt[this->dts_fmt_lock], tm);
+ }
+
+ return (size_t) off;
+}
+
+bool
+next_format(const char* const fmt[], int& index, int& locked_index)
+{
+ bool retval = true;
+
+ if (locked_index == -1) {
+ index += 1;
+ if (fmt[index] == nullptr) {
+ retval = false;
+ }
+ } else if (index == locked_index) {
+ retval = false;
+ } else {
+ index = locked_index;
+ }
+
+ return retval;
+}
+
+const char*
+date_time_scanner::scan(const char* time_dest,
+ size_t time_len,
+ const char* const time_fmt[],
+ struct exttm* tm_out,
+ struct timeval& tv_out,
+ bool convert_local)
+{
+ int curr_time_fmt = -1;
+ bool found = false;
+ const char* retval = nullptr;
+
+ if (!time_fmt) {
+ time_fmt = PTIMEC_FORMAT_STR;
+ }
+
+ while (next_format(time_fmt, curr_time_fmt, this->dts_fmt_lock)) {
+ *tm_out = this->dts_base_tm;
+ tm_out->et_flags = 0;
+ if (time_len > 1 && time_dest[0] == '+' && isdigit(time_dest[1])) {
+ retval = nullptr;
+ auto epoch_scan_res = scn::scan_value<int64_t>(
+ scn::string_view{time_dest, time_len});
+ if (epoch_scan_res) {
+ time_t gmt = epoch_scan_res.value();
+
+ if (convert_local && this->dts_local_time) {
+ localtime_r(&gmt, &tm_out->et_tm);
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ tm_out->et_tm.tm_zone = nullptr;
+#endif
+ tm_out->et_tm.tm_isdst = 0;
+ gmt = tm2sec(&tm_out->et_tm);
+ }
+ tv_out.tv_sec = gmt;
+ tv_out.tv_usec = 0;
+ tm_out->et_flags = ETF_DAY_SET | ETF_MONTH_SET | ETF_YEAR_SET
+ | ETF_MACHINE_ORIENTED | ETF_EPOCH_TIME;
+
+ this->dts_fmt_lock = curr_time_fmt;
+ this->dts_fmt_len = std::distance(epoch_scan_res.begin(),
+ epoch_scan_res.end());
+ retval = time_dest + this->dts_fmt_len;
+ found = true;
+ break;
+ }
+ } else if (time_fmt == PTIMEC_FORMAT_STR) {
+ ptime_func func = PTIMEC_FORMATS[curr_time_fmt].pf_func;
+ off_t off = 0;
+
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ if (!this->dts_keep_base_tz) {
+ tm_out->et_tm.tm_zone = nullptr;
+ }
+#endif
+ if (func(tm_out, time_dest, off, time_len)) {
+ retval = &time_dest[off];
+
+ if (tm_out->et_tm.tm_year < 70) {
+ tm_out->et_tm.tm_year = 80;
+ }
+ if (convert_local
+ && (this->dts_local_time
+ || tm_out->et_flags & ETF_EPOCH_TIME))
+ {
+ time_t gmt = tm2sec(&tm_out->et_tm);
+
+ this->to_localtime(gmt, *tm_out);
+ }
+ const auto& last_tm = this->dts_last_tm.et_tm;
+ if (last_tm.tm_year == tm_out->et_tm.tm_year
+ && last_tm.tm_mon == tm_out->et_tm.tm_mon
+ && last_tm.tm_mday == tm_out->et_tm.tm_mday
+ && last_tm.tm_hour == tm_out->et_tm.tm_hour
+ && last_tm.tm_min == tm_out->et_tm.tm_min)
+ {
+ const auto sec_diff = tm_out->et_tm.tm_sec - last_tm.tm_sec;
+
+ // log_debug("diff %d", sec_diff);
+ tv_out = this->dts_last_tv;
+ tv_out.tv_sec += sec_diff;
+ tm_out->et_tm.tm_wday = last_tm.tm_wday;
+ } else {
+ // log_debug("doing tm2sec");
+ tv_out.tv_sec = tm2sec(&tm_out->et_tm);
+ secs2wday(tv_out, &tm_out->et_tm);
+ }
+ tv_out.tv_usec = tm_out->et_nsec / 1000;
+
+ this->dts_fmt_lock = curr_time_fmt;
+ this->dts_fmt_len = retval - time_dest;
+
+ found = true;
+ break;
+ }
+ } else {
+ off_t off = 0;
+
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ if (!this->dts_keep_base_tz) {
+ tm_out->et_tm.tm_zone = nullptr;
+ }
+#endif
+ if (ptime_fmt(
+ time_fmt[curr_time_fmt], tm_out, time_dest, off, time_len)
+ && (time_dest[off] == '.' || time_dest[off] == ','
+ || off == (off_t) time_len))
+ {
+ retval = &time_dest[off];
+ if (tm_out->et_tm.tm_year < 70) {
+ tm_out->et_tm.tm_year = 80;
+ }
+ if (convert_local
+ && (this->dts_local_time
+ || tm_out->et_flags & ETF_EPOCH_TIME))
+ {
+ time_t gmt = tm2sec(&tm_out->et_tm);
+
+ this->to_localtime(gmt, *tm_out);
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ tm_out->et_tm.tm_zone = nullptr;
+#endif
+ tm_out->et_tm.tm_isdst = 0;
+ }
+
+ tv_out.tv_sec = tm2sec(&tm_out->et_tm);
+ tv_out.tv_usec = tm_out->et_nsec / 1000;
+ secs2wday(tv_out, &tm_out->et_tm);
+
+ this->dts_fmt_lock = curr_time_fmt;
+ this->dts_fmt_len = retval - time_dest;
+
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (!found) {
+ retval = nullptr;
+ }
+
+ if (retval != nullptr) {
+ this->dts_last_tm = *tm_out;
+ this->dts_last_tv = tv_out;
+ }
+
+ if (retval != nullptr && static_cast<size_t>(retval - time_dest) < time_len)
+ {
+ /* Try to pull out the milli/micro-second value. */
+ if (retval[0] == '.' || retval[0] == ',') {
+ off_t off = (retval - time_dest) + 1;
+
+ if (ptime_N(tm_out, time_dest, off, time_len)) {
+ tv_out.tv_usec
+ = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::nanoseconds{tm_out->et_nsec})
+ .count();
+ this->dts_fmt_len += 10;
+ tm_out->et_flags |= ETF_NANOS_SET;
+ retval += 10;
+ } else if (ptime_f(tm_out, time_dest, off, time_len)) {
+ tv_out.tv_usec
+ = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::nanoseconds{tm_out->et_nsec})
+ .count();
+ this->dts_fmt_len += 7;
+ tm_out->et_flags |= ETF_MICROS_SET;
+ retval += 7;
+ } else if (ptime_L(tm_out, time_dest, off, time_len)) {
+ tv_out.tv_usec
+ = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::nanoseconds{tm_out->et_nsec})
+ .count();
+ this->dts_fmt_len += 4;
+ tm_out->et_flags |= ETF_MILLIS_SET;
+ retval += 4;
+ }
+ }
+ }
+
+ return retval;
+}
+
+void
+date_time_scanner::set_base_time(time_t base_time, const tm& local_tm)
+{
+ this->dts_base_time = base_time;
+ this->dts_base_tm.et_tm = local_tm;
+ this->dts_last_tm = exttm{};
+ this->dts_last_tv = timeval{};
+}
+
+void
+date_time_scanner::to_localtime(time_t t, exttm& tm_out)
+{
+ if (t < (24 * 60 * 60)) {
+ // Don't convert and risk going past the epoch.
+ return;
+ }
+
+ if (t < this->dts_local_offset_valid || t >= this->dts_local_offset_expiry)
+ {
+ time_t new_gmt;
+
+ localtime_r(&t, &tm_out.et_tm);
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ tm_out.et_tm.tm_zone = nullptr;
+#endif
+ tm_out.et_tm.tm_isdst = 0;
+
+ new_gmt = tm2sec(&tm_out.et_tm);
+ this->dts_local_offset_cache = t - new_gmt;
+ this->dts_local_offset_valid = t;
+ this->dts_local_offset_expiry = t + (EXPIRE_TIME - 1);
+ this->dts_local_offset_expiry
+ -= this->dts_local_offset_expiry % EXPIRE_TIME;
+ } else {
+ time_t adjust_gmt = t - this->dts_local_offset_cache;
+ gmtime_r(&adjust_gmt, &tm_out.et_tm);
+ }
+}
diff --git a/src/base/date_time_scanner.hh b/src/base/date_time_scanner.hh
new file mode 100644
index 0000000..90eaffa
--- /dev/null
+++ b/src/base/date_time_scanner.hh
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file date_time_scanner.hh
+ */
+
+#ifndef lnav_date_time_scanner_hh
+#define lnav_date_time_scanner_hh
+
+#include <ctime>
+#include <string>
+
+#include <sys/types.h>
+
+#include "time_util.hh"
+
+/**
+ * Scans a timestamp string to discover the date-time format using the custom
+ * ptimec parser. Once a format is found, it is locked in so that the next
+ * time a timestamp needs to be scanned, the format does not have to be
+ * rediscovered. The discovered date-time format can also be used to convert
+ * an exttm struct to a string using the ftime() method.
+ */
+struct date_time_scanner {
+ date_time_scanner() { this->clear(); }
+
+ void clear()
+ {
+ this->dts_base_time = 0;
+ this->dts_base_tm = exttm{};
+ this->dts_fmt_lock = -1;
+ this->dts_fmt_len = -1;
+ this->dts_last_tv = timeval{};
+ this->dts_last_tm = exttm{};
+ }
+
+ /**
+ * Unlock this scanner so that the format is rediscovered.
+ */
+ void unlock()
+ {
+ this->dts_fmt_lock = -1;
+ this->dts_fmt_len = -1;
+ }
+
+ void set_base_time(time_t base_time, const tm& local_tm);
+
+ /**
+ * Convert a timestamp to local time.
+ *
+ * Calling localtime_r is slow since it wants to lookup the timezone on
+ * every call, so we cache the result and only call it again if the
+ * requested time falls outside of a fifteen minute range.
+ */
+ void to_localtime(time_t t, struct exttm& tm_out);
+
+ bool dts_keep_base_tz{false};
+ bool dts_local_time{false};
+ time_t dts_base_time{0};
+ struct exttm dts_base_tm;
+ int dts_fmt_lock{-1};
+ int dts_fmt_len{-1};
+ struct exttm dts_last_tm {};
+ struct timeval dts_last_tv {};
+ time_t dts_local_offset_cache{0};
+ time_t dts_local_offset_valid{0};
+ time_t dts_local_offset_expiry{0};
+
+ static const int EXPIRE_TIME = 15 * 60;
+
+ const char* scan(const char* time_src,
+ size_t time_len,
+ const char* const time_fmt[],
+ struct exttm* tm_out,
+ struct timeval& tv_out,
+ bool convert_local = true);
+
+ size_t ftime(char* dst,
+ size_t len,
+ const char* const time_fmt[],
+ const struct exttm& tm) const;
+
+ bool convert_to_timeval(const char* time_src,
+ ssize_t time_len,
+ const char* const time_fmt[],
+ struct timeval& tv_out)
+ {
+ struct exttm tm;
+
+ if (time_len == -1) {
+ time_len = strlen(time_src);
+ }
+ if (this->scan(time_src, time_len, time_fmt, &tm, tv_out) != nullptr) {
+ return true;
+ }
+ return false;
+ }
+
+ bool convert_to_timeval(const std::string& time_src, struct timeval& tv_out)
+ {
+ struct exttm tm;
+
+ if (this->scan(time_src.c_str(), time_src.size(), nullptr, &tm, tv_out)
+ != nullptr)
+ {
+ return true;
+ }
+ return false;
+ }
+};
+
+#endif
diff --git a/src/base/enum_util.hh b/src/base/enum_util.hh
new file mode 100644
index 0000000..437292d
--- /dev/null
+++ b/src/base/enum_util.hh
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_enum_util_hh
+#define lnav_enum_util_hh
+
+#include <type_traits>
+
+namespace lnav {
+namespace enums {
+
+template<typename E>
+constexpr auto
+to_underlying(E e) noexcept
+{
+ return static_cast<std::underlying_type_t<E>>(e);
+}
+
+} // namespace enums
+} // namespace lnav
+
+#endif
diff --git a/src/base/file_range.hh b/src/base/file_range.hh
new file mode 100644
index 0000000..d9a3de4
--- /dev/null
+++ b/src/base/file_range.hh
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_file_range_hh
+#define lnav_file_range_hh
+
+#include <sys/types.h>
+
+#include "intern_string.hh"
+
+using file_off_t = int64_t;
+using file_size_t = uint64_t;
+using file_ssize_t = int64_t;
+
+class file_range {
+public:
+ struct metadata {
+ bool m_valid_utf{true};
+ bool m_has_ansi{false};
+ };
+
+ file_off_t fr_offset{0};
+ file_ssize_t fr_size{0};
+ metadata fr_metadata;
+
+ void clear()
+ {
+ this->fr_offset = 0;
+ this->fr_size = 0;
+ }
+
+ ssize_t next_offset() const { return this->fr_offset + this->fr_size; }
+
+ bool empty() const { return this->fr_size == 0; }
+};
+
+struct source_location {
+ source_location()
+ : sl_source(intern_string::lookup("unknown")), sl_line_number(0)
+ {
+ }
+
+ explicit source_location(intern_string_t source, int32_t line = 0)
+ : sl_source(source), sl_line_number(line)
+ {
+ }
+
+ intern_string_t sl_source;
+ int32_t sl_line_number;
+};
+
+#endif
diff --git a/src/base/fs_util.cc b/src/base/fs_util.cc
new file mode 100644
index 0000000..f72aaa6
--- /dev/null
+++ b/src/base/fs_util.cc
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "fs_util.hh"
+
+#include "config.h"
+#include "fmt/format.h"
+#include "itertools.hh"
+#include "opt_util.hh"
+
+namespace lnav {
+namespace filesystem {
+
+Result<auto_fd, std::string>
+create_file(const ghc::filesystem::path& path, int flags, mode_t mode)
+{
+ auto fd = openp(path, flags | O_CREAT, mode);
+
+ if (fd == -1) {
+ return Err(fmt::format(FMT_STRING("Failed to open: {} -- {}"),
+ path.string(),
+ strerror(errno)));
+ }
+
+ return Ok(auto_fd(fd));
+}
+
+Result<auto_fd, std::string>
+open_file(const ghc::filesystem::path& path, int flags)
+{
+ auto fd = openp(path, flags);
+
+ if (fd == -1) {
+ return Err(fmt::format(FMT_STRING("Failed to open: {} -- {}"),
+ path.string(),
+ strerror(errno)));
+ }
+
+ return Ok(auto_fd(fd));
+}
+
+Result<std::pair<ghc::filesystem::path, auto_fd>, std::string>
+open_temp_file(const ghc::filesystem::path& pattern)
+{
+ auto pattern_str = pattern.string();
+ char pattern_copy[pattern_str.size() + 1];
+ int fd;
+
+ strcpy(pattern_copy, pattern_str.c_str());
+ if ((fd = mkstemp(pattern_copy)) == -1) {
+ return Err(
+ fmt::format(FMT_STRING("unable to create temporary file: {} -- {}"),
+ pattern.string(),
+ strerror(errno)));
+ }
+
+ return Ok(std::make_pair(ghc::filesystem::path(pattern_copy), auto_fd(fd)));
+}
+
+Result<std::string, std::string>
+read_file(const ghc::filesystem::path& path)
+{
+ try {
+ ghc::filesystem::ifstream file_stream(path);
+
+ if (!file_stream) {
+ return Err(std::string(strerror(errno)));
+ }
+
+ std::string retval;
+ retval.assign((std::istreambuf_iterator<char>(file_stream)),
+ std::istreambuf_iterator<char>());
+ return Ok(retval);
+ } catch (const std::exception& e) {
+ return Err(std::string(e.what()));
+ }
+}
+
+Result<void, std::string>
+write_file(const ghc::filesystem::path& path, const string_fragment& content)
+{
+ auto tmp_pattern = path;
+ tmp_pattern += ".XXXXXX";
+
+ auto tmp_pair = TRY(open_temp_file(tmp_pattern));
+ auto bytes_written
+ = write(tmp_pair.second.get(), content.data(), content.length());
+ if (bytes_written < 0) {
+ return Err(
+ fmt::format(FMT_STRING("unable to write to temporary file {}: {}"),
+ tmp_pair.first.string(),
+ strerror(errno)));
+ }
+ if (bytes_written != content.length()) {
+ return Err(fmt::format(FMT_STRING("short write to file {}: {} < {}"),
+ tmp_pair.first.string(),
+ bytes_written,
+ content.length()));
+ }
+ std::error_code ec;
+ ghc::filesystem::rename(tmp_pair.first, path, ec);
+ if (ec) {
+ return Err(
+ fmt::format(FMT_STRING("unable to move temporary file {}: {}"),
+ tmp_pair.first.string(),
+ ec.message()));
+ }
+
+ return Ok();
+}
+
+std::string
+build_path(const std::vector<ghc::filesystem::path>& paths)
+{
+ return paths
+ | lnav::itertools::map([](const auto& path) { return path.string(); })
+ | lnav::itertools::append(getenv_opt("PATH").value_or(""))
+ | lnav::itertools::filter_out(&std::string::empty)
+ | lnav::itertools::fold(
+ [](const auto& elem, auto& accum) {
+ if (!accum.empty()) {
+ accum.push_back(':');
+ }
+ return accum.append(elem);
+ },
+ std::string());
+}
+
+Result<struct stat, std::string>
+stat_file(const ghc::filesystem::path& path)
+{
+ struct stat retval;
+
+ if (statp(path, &retval) == 0) {
+ return Ok(retval);
+ }
+
+ return Err(fmt::format(FMT_STRING("failed to find file: {} -- {}"),
+ path.string(),
+ strerror(errno)));
+}
+
+file_lock::file_lock(const ghc::filesystem::path& archive_path)
+{
+ auto lock_path = archive_path;
+
+ lock_path += ".lck";
+ auto open_res
+ = lnav::filesystem::create_file(lock_path, O_RDWR | O_CLOEXEC, 0600);
+ if (open_res.isErr()) {
+ throw std::runtime_error(open_res.unwrapErr());
+ }
+ this->lh_fd = open_res.unwrap();
+}
+
+} // namespace filesystem
+} // namespace lnav
diff --git a/src/base/fs_util.hh b/src/base/fs_util.hh
new file mode 100644
index 0000000..b9253ff
--- /dev/null
+++ b/src/base/fs_util.hh
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_fs_util_hh
+#define lnav_fs_util_hh
+
+#include <string>
+#include <vector>
+
+#include "auto_fd.hh"
+#include "ghc/filesystem.hpp"
+#include "intern_string.hh"
+#include "result.h"
+
+namespace lnav {
+namespace filesystem {
+
+inline int
+statp(const ghc::filesystem::path& path, struct stat* buf)
+{
+ return stat(path.c_str(), buf);
+}
+
+inline int
+openp(const ghc::filesystem::path& path, int flags)
+{
+ return open(path.c_str(), flags);
+}
+
+inline int
+openp(const ghc::filesystem::path& path, int flags, mode_t mode)
+{
+ return open(path.c_str(), flags, mode);
+}
+
+Result<auto_fd, std::string> create_file(const ghc::filesystem::path& path,
+ int flags,
+ mode_t mode);
+
+Result<auto_fd, std::string> open_file(const ghc::filesystem::path& path,
+ int flags);
+
+Result<struct stat, std::string> stat_file(const ghc::filesystem::path& path);
+
+Result<std::pair<ghc::filesystem::path, auto_fd>, std::string> open_temp_file(
+ const ghc::filesystem::path& pattern);
+
+Result<std::string, std::string> read_file(const ghc::filesystem::path& path);
+
+Result<void, std::string> write_file(const ghc::filesystem::path& path,
+ const string_fragment& content);
+
+std::string build_path(const std::vector<ghc::filesystem::path>& paths);
+
+class file_lock {
+public:
+ class guard {
+ public:
+ explicit guard(file_lock* arc_lock) : g_lock(arc_lock)
+ {
+ this->g_lock->lock();
+ }
+
+ guard(guard&& other) noexcept
+ : g_lock(std::exchange(other.g_lock, nullptr))
+ {
+ }
+
+ ~guard()
+ {
+ if (this->g_lock != nullptr) {
+ this->g_lock->unlock();
+ }
+ }
+
+ guard(const guard&) = delete;
+ guard& operator=(const guard&) = delete;
+ guard& operator=(guard&&) = delete;
+
+ private:
+ file_lock* g_lock;
+ };
+
+ void lock() const { lockf(this->lh_fd, F_LOCK, 0); }
+
+ void unlock() const { lockf(this->lh_fd, F_ULOCK, 0); }
+
+ explicit file_lock(const ghc::filesystem::path& archive_path);
+
+ auto_fd lh_fd;
+};
+
+} // namespace filesystem
+} // namespace lnav
+
+#endif
diff --git a/src/base/fs_util.tests.cc b/src/base/fs_util.tests.cc
new file mode 100644
index 0000000..9ed3377
--- /dev/null
+++ b/src/base/fs_util.tests.cc
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <iostream>
+
+#include "base/fs_util.hh"
+
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("fs_util::build_path")
+{
+ auto* old_path = getenv("PATH");
+ unsetenv("PATH");
+
+ CHECK("" == lnav::filesystem::build_path({}));
+
+ CHECK("/bin:/usr/bin"
+ == lnav::filesystem::build_path({"", "/bin", "/usr/bin", ""}));
+ setenv("PATH", "/usr/local/bin", 1);
+ CHECK("/bin:/usr/bin:/usr/local/bin"
+ == lnav::filesystem::build_path({"", "/bin", "/usr/bin", ""}));
+ setenv("PATH", "/usr/local/bin:/opt/bin", 1);
+ CHECK("/usr/local/bin:/opt/bin" == lnav::filesystem::build_path({}));
+ CHECK("/bin:/usr/bin:/usr/local/bin:/opt/bin"
+ == lnav::filesystem::build_path({"", "/bin", "/usr/bin", ""}));
+ if (old_path != nullptr) {
+ setenv("PATH", old_path, 1);
+ }
+}
diff --git a/src/base/func_util.hh b/src/base/func_util.hh
new file mode 100644
index 0000000..01a2328
--- /dev/null
+++ b/src/base/func_util.hh
@@ -0,0 +1,159 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_func_util_hh
+#define lnav_func_util_hh
+
+#include <functional>
+#include <utility>
+
+template<typename F, typename FrontArg>
+decltype(auto)
+bind_mem(F&& f, FrontArg&& frontArg)
+{
+ return [f = std::forward<F>(f),
+ frontArg = std::forward<FrontArg>(frontArg)](auto&&... backArgs) {
+ return (frontArg->*f)(std::forward<decltype(backArgs)>(backArgs)...);
+ };
+}
+
+struct noop_func {
+ struct anything {
+ template<class T>
+ operator T()
+ {
+ return {};
+ }
+ // optional reference support. Somewhat evil.
+ template<class T>
+ operator T&() const
+ {
+ static T t{};
+ return t;
+ }
+ };
+ template<class... Args>
+ anything operator()(Args&&...) const
+ {
+ return {};
+ }
+};
+
+namespace lnav {
+namespace func {
+
+class scoped_cb {
+public:
+ class guard {
+ public:
+ explicit guard(scoped_cb* owner) : g_owner(owner) {}
+
+ guard(const guard&) = delete;
+ guard& operator=(const guard&) = delete;
+
+ guard(guard&& gu) noexcept : g_owner(std::exchange(gu.g_owner, nullptr))
+ {
+ }
+
+ guard& operator=(guard&& gu) noexcept
+ {
+ this->g_owner = std::exchange(gu.g_owner, nullptr);
+ return *this;
+ }
+
+ ~guard()
+ {
+ if (this->g_owner != nullptr) {
+ this->g_owner->s_callback = {};
+ }
+ }
+
+ private:
+ scoped_cb* g_owner;
+ };
+
+ guard install(std::function<void()> cb)
+ {
+ this->s_callback = std::move(cb);
+
+ return guard{this};
+ }
+
+ void operator()()
+ {
+ if (s_callback) {
+ s_callback();
+ }
+ }
+
+private:
+ std::function<void()> s_callback;
+};
+
+template<typename Fn,
+ typename... Args,
+ std::enable_if_t<std::is_member_pointer<std::decay_t<Fn>>{}, int> = 0>
+constexpr decltype(auto)
+invoke(Fn&& f, Args&&... args) noexcept(
+ noexcept(std::mem_fn(f)(std::forward<Args>(args)...)))
+{
+ return std::mem_fn(f)(std::forward<Args>(args)...);
+}
+
+template<typename Fn,
+ typename... Args,
+ std::enable_if_t<!std::is_member_pointer<std::decay_t<Fn>>{}, int> = 0>
+constexpr decltype(auto)
+invoke(Fn&& f, Args&&... args) noexcept(
+ noexcept(std::forward<Fn>(f)(std::forward<Args>(args)...)))
+{
+ return std::forward<Fn>(f)(std::forward<Args>(args)...);
+}
+
+template<class F, class... Args>
+struct is_invocable {
+ template<typename U, typename Obj, typename... FuncArgs>
+ static auto test(U&& p)
+ -> decltype((std::declval<Obj>().*p)(std::declval<FuncArgs>()...),
+ void(),
+ std::true_type());
+ template<typename U, typename... FuncArgs>
+ static auto test(U* p) -> decltype((*p)(std::declval<FuncArgs>()...),
+ void(),
+ std::true_type());
+ template<typename U, typename... FuncArgs>
+ static auto test(...) -> decltype(std::false_type());
+
+ static constexpr bool value = decltype(test<F, Args...>(0))::value;
+};
+
+} // namespace func
+} // namespace lnav
+
+#endif
diff --git a/src/base/future_util.hh b/src/base/future_util.hh
new file mode 100644
index 0000000..8797faa
--- /dev/null
+++ b/src/base/future_util.hh
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_future_util_hh
+#define lnav_future_util_hh
+
+#include <deque>
+#include <future>
+
+namespace lnav {
+namespace futures {
+
+/**
+ * Create a future that is ready to immediately return a result.
+ *
+ * @tparam T The result type of the future.
+ * @param t The value the future should return.
+ * @return The new future.
+ */
+template<class T>
+std::future<std::decay_t<T>>
+make_ready_future(T&& t)
+{
+ std::promise<std::decay_t<T>> pr;
+ auto r = pr.get_future();
+ pr.set_value(std::forward<T>(t));
+ return r;
+}
+
+/**
+ * A queue used to limit the number of futures that are running concurrently.
+ *
+ * @tparam T The result of the futures.
+ * @tparam MAX_QUEUE_SIZE The maximum number of futures that can be in flight.
+ */
+template<typename T, int MAX_QUEUE_SIZE = 8>
+class future_queue {
+public:
+ /**
+ * @param processor The function to execute with the result of a future.
+ */
+ explicit future_queue(std::function<void(T&)> processor)
+ : fq_processor(processor){};
+
+ ~future_queue()
+ {
+ this->pop_to();
+ }
+
+ /**
+ * Add a future to the queue. If the size of the queue is greater than the
+ * MAX_QUEUE_SIZE, this call will block waiting for the first queued
+ * future to return a result.
+ *
+ * @param f The future to add to the queue.
+ */
+ void push_back(std::future<T>&& f)
+ {
+ this->fq_deque.emplace_back(std::move(f));
+ this->pop_to(MAX_QUEUE_SIZE);
+ }
+
+ /**
+ * Removes the next future from the queue, waits for the result, and then
+ * repeats until the queue reaches the given size.
+ *
+ * @param size The new desired size of the queue.
+ */
+ void pop_to(size_t size = 0)
+ {
+ while (this->fq_deque.size() > size) {
+ auto v = this->fq_deque.front().get();
+ this->fq_processor(v);
+ this->fq_deque.pop_front();
+ }
+ }
+
+ std::function<void(T&)> fq_processor;
+ std::deque<std::future<T>> fq_deque;
+};
+
+} // namespace futures
+} // namespace lnav
+
+#endif
diff --git a/src/base/humanize.cc b/src/base/humanize.cc
new file mode 100644
index 0000000..5e96bf3
--- /dev/null
+++ b/src/base/humanize.cc
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <cmath>
+#include <vector>
+
+#include "humanize.hh"
+
+#include "config.h"
+#include "fmt/format.h"
+
+namespace humanize {
+
+std::string
+file_size(file_ssize_t value, alignment align)
+{
+ static const double LN1024 = log(1024.0);
+ static const std::vector<const char*> UNITS = {
+ " ",
+ "K",
+ "M",
+ "G",
+ "T",
+ "P",
+ "E",
+ };
+
+ if (value < 0) {
+ return "Unknown";
+ }
+
+ if (value == 0) {
+ switch (align) {
+ case alignment::none:
+ return "0B";
+ case alignment::columnar:
+ return "0.0 B";
+ }
+ }
+
+ auto exp
+ = floor(std::min(log(value) / LN1024, (double) (UNITS.size() - 1)));
+ auto divisor = pow(1024, exp);
+
+ if (align == alignment::none && divisor <= 1) {
+ return fmt::format(FMT_STRING("{}B"), value, UNITS[exp]);
+ }
+ return fmt::format(FMT_STRING("{:.1f}{}B"),
+ divisor == 0 ? value : value / divisor,
+ UNITS[exp]);
+}
+
+const std::string&
+sparkline(double value, nonstd::optional<double> upper_opt)
+{
+ static const std::string ZERO = " ";
+ static const std::string BARS[] = {
+ "\u2581",
+ "\u2582",
+ "\u2583",
+ "\u2584",
+ "\u2585",
+ "\u2586",
+ "\u2587",
+ "\u2588",
+ };
+ static const double BARS_COUNT = std::distance(begin(BARS), end(BARS));
+
+ if (value <= 0.0) {
+ return ZERO;
+ }
+
+ auto upper = upper_opt.value_or(100.0);
+
+ if (value >= upper) {
+ return BARS[(size_t) BARS_COUNT - 1];
+ }
+
+ size_t index = ceil((value / upper) * BARS_COUNT) - 1;
+
+ return BARS[index];
+}
+
+} // namespace humanize
diff --git a/src/base/humanize.file_size.tests.cc b/src/base/humanize.file_size.tests.cc
new file mode 100644
index 0000000..ff70c7e
--- /dev/null
+++ b/src/base/humanize.file_size.tests.cc
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <iostream>
+
+#include "base/humanize.hh"
+
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("humanize::file_size")
+{
+ CHECK(humanize::file_size(0, humanize::alignment::columnar) == "0.0 B");
+ CHECK(humanize::file_size(1, humanize::alignment::columnar) == "1.0 B");
+ CHECK(humanize::file_size(1024, humanize::alignment::columnar) == "1.0KB");
+ CHECK(humanize::file_size(1500, humanize::alignment::columnar) == "1.5KB");
+ CHECK(humanize::file_size(55LL * 784LL * 1024LL * 1024LL,
+ humanize::alignment::columnar)
+ == "42.1GB");
+ CHECK(humanize::file_size(-1LL, humanize::alignment::columnar)
+ == "Unknown");
+ CHECK(humanize::file_size(std::numeric_limits<int64_t>::max(),
+ humanize::alignment::columnar)
+ == "8.0EB");
+}
diff --git a/src/base/humanize.hh b/src/base/humanize.hh
new file mode 100644
index 0000000..3f9f66c
--- /dev/null
+++ b/src/base/humanize.hh
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_humanize_hh
+#define lnav_humanize_hh
+
+#include <string>
+
+#include <sys/types.h>
+
+#include "file_range.hh"
+
+namespace humanize {
+
+enum class alignment {
+ none,
+ columnar,
+};
+
+/**
+ * Format the given size as a human-friendly string.
+ *
+ * @param value The value to format.
+ * @return The formatted string.
+ */
+std::string file_size(file_ssize_t value, alignment align);
+
+const std::string& sparkline(double value, nonstd::optional<double> upper);
+
+} // namespace humanize
+
+#endif
diff --git a/src/base/humanize.network.cc b/src/base/humanize.network.cc
new file mode 100644
index 0000000..2bf390d
--- /dev/null
+++ b/src/base/humanize.network.cc
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "humanize.network.hh"
+
+#include "config.h"
+#include "pcrepp/pcre2pp.hh"
+
+namespace humanize {
+namespace network {
+namespace path {
+
+nonstd::optional<::network::path>
+from_str(string_fragment sf)
+{
+ static const auto REMOTE_PATTERN = lnav::pcre2pp::code::from_const(
+ "^(?:(?<username>[\\w\\._\\-]+)@)?"
+ "(?:\\[(?<ipv6>[^\\]]+)\\]|(?<hostname>[^\\[/:]+)):"
+ "(?<path>.*)$");
+ static thread_local auto REMOTE_MATCH_DATA
+ = REMOTE_PATTERN.create_match_data();
+
+ auto match_res = REMOTE_PATTERN.capture_from(sf)
+ .into(REMOTE_MATCH_DATA)
+ .matches()
+ .ignore_error();
+
+ if (!match_res) {
+ return nonstd::nullopt;
+ }
+
+ const auto username = REMOTE_MATCH_DATA["username"].map(
+ [](auto sf) { return sf.to_string(); });
+ const auto ipv6 = REMOTE_MATCH_DATA["ipv6"];
+ const auto hostname = REMOTE_MATCH_DATA["hostname"];
+ const auto locality_hostname = ipv6 ? ipv6.value() : hostname.value();
+ auto path = *REMOTE_MATCH_DATA["path"];
+
+ if (path.empty()) {
+ path = string_fragment::from_const(".");
+ }
+ return ::network::path{
+ {username, locality_hostname.to_string(), nonstd::nullopt},
+ path.to_string(),
+ };
+}
+
+} // namespace path
+} // namespace network
+} // namespace humanize
diff --git a/src/base/humanize.network.hh b/src/base/humanize.network.hh
new file mode 100644
index 0000000..609f57d
--- /dev/null
+++ b/src/base/humanize.network.hh
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_humanize_network_hh
+#define lnav_humanize_network_hh
+
+#include <string>
+
+#include "fmt/format.h"
+#include "intern_string.hh"
+#include "network.tcp.hh"
+#include "optional.hpp"
+
+namespace fmt {
+
+template<>
+struct formatter<network::locality> {
+ constexpr auto parse(format_parse_context& ctx)
+ {
+ const auto it = ctx.begin();
+ const auto end = ctx.end();
+
+ // Check if reached the end of the range:
+ if (it != end && *it != '}') {
+ throw format_error("invalid format");
+ }
+
+ // Return an iterator past the end of the parsed range:
+ return it;
+ }
+
+ template<typename FormatContext>
+ auto format(const network::locality& l, FormatContext& ctx)
+ {
+ bool is_ipv6 = l.l_hostname.find(':') != std::string::npos;
+
+ return format_to(ctx.out(),
+ "{}{}{}{}{}",
+ l.l_username.value_or(std::string()),
+ l.l_username ? "@" : "",
+ is_ipv6 ? "[" : "",
+ l.l_hostname,
+ is_ipv6 ? "]" : "");
+ }
+};
+
+template<>
+struct formatter<network::path> {
+ constexpr auto parse(format_parse_context& ctx)
+ {
+ const auto it = ctx.begin();
+ const auto end = ctx.end();
+
+ // Check if reached the end of the range:
+ if (it != end && *it != '}') {
+ throw format_error("invalid format");
+ }
+
+ // Return an iterator past the end of the parsed range:
+ return it;
+ }
+
+ template<typename FormatContext>
+ auto format(const network::path& p, FormatContext& ctx)
+ {
+ return format_to(
+ ctx.out(), "{}:{}", p.p_locality, p.p_path == "." ? "" : p.p_path);
+ }
+};
+
+} // namespace fmt
+
+namespace humanize {
+namespace network {
+namespace path {
+
+nonstd::optional<::network::path> from_str(string_fragment sf);
+
+} // namespace path
+} // namespace network
+} // namespace humanize
+
+#endif
diff --git a/src/base/humanize.network.tests.cc b/src/base/humanize.network.tests.cc
new file mode 100644
index 0000000..fe2e9d2
--- /dev/null
+++ b/src/base/humanize.network.tests.cc
@@ -0,0 +1,118 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <iostream>
+
+#include "base/humanize.network.hh"
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("humanize::network::path")
+{
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("foobar"));
+ CHECK(!rp_opt);
+ }
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("dean@foobar/bar"));
+ CHECK(!rp_opt);
+ }
+
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("dean@host1.example.com:/var/log"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_username.value() == "dean");
+ CHECK(rp.p_locality.l_hostname == "host1.example.com");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == "/var/log");
+ }
+
+ {
+ auto rp_opt
+ = humanize::network::path::from_str(string_fragment::from_const(
+ "dean@[fe80::184f:c67:baf1:fe02%en0]:/var/log"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_username.value() == "dean");
+ CHECK(rp.p_locality.l_hostname == "fe80::184f:c67:baf1:fe02%en0");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == "/var/log");
+
+ CHECK(fmt::format("{}", rp.p_locality)
+ == "dean@[fe80::184f:c67:baf1:fe02%en0]");
+ }
+
+ {
+ auto rp_opt
+ = humanize::network::path::from_str(string_fragment::from_const(
+ "[fe80::184f:c67:baf1:fe02%en0]:/var/log"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(!rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_hostname == "fe80::184f:c67:baf1:fe02%en0");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == "/var/log");
+
+ CHECK(fmt::format("{}", rp.p_locality)
+ == "[fe80::184f:c67:baf1:fe02%en0]");
+ }
+
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("host1.example.com:/var/log"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(!rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_hostname == "host1.example.com");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == "/var/log");
+ }
+
+ {
+ auto rp_opt = humanize::network::path::from_str(
+ string_fragment::from_const("host1.example.com:"));
+ CHECK(rp_opt.has_value());
+
+ auto rp = *rp_opt;
+ CHECK(!rp.p_locality.l_username.has_value());
+ CHECK(rp.p_locality.l_hostname == "host1.example.com");
+ CHECK(!rp.p_locality.l_service.has_value());
+ CHECK(rp.p_path == ".");
+ }
+}
diff --git a/src/base/humanize.time.cc b/src/base/humanize.time.cc
new file mode 100644
index 0000000..68cfe0f
--- /dev/null
+++ b/src/base/humanize.time.cc
@@ -0,0 +1,208 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <chrono>
+
+#include "humanize.time.hh"
+
+#include "config.h"
+#include "fmt/format.h"
+#include "time_util.hh"
+
+namespace humanize {
+namespace time {
+
+using namespace std::chrono_literals;
+
+point
+point::from_tv(const timeval& tv)
+{
+ return point(tv);
+}
+
+std::string
+point::as_time_ago() const
+{
+ struct timeval current_time
+ = this->p_recent_point.value_or(current_timeval());
+
+ if (this->p_convert_to_local) {
+ current_time.tv_sec = convert_log_time_to_local(current_time.tv_sec);
+ }
+
+ auto delta
+ = std::chrono::seconds(current_time.tv_sec - this->p_past_point.tv_sec);
+ if (delta < 0s) {
+ return "in the future";
+ }
+ if (delta < 1min) {
+ return "just now";
+ }
+ if (delta < 2min) {
+ return "one minute ago";
+ }
+ if (delta < 1h) {
+ return fmt::format(
+ FMT_STRING("{} minutes ago"),
+ std::chrono::duration_cast<std::chrono::minutes>(delta).count());
+ }
+ if (delta < 2h) {
+ return "one hour ago";
+ }
+ if (delta < 24h) {
+ return fmt::format(
+ FMT_STRING("{} hours ago"),
+ std::chrono::duration_cast<std::chrono::hours>(delta).count());
+ }
+ if (delta < 48h) {
+ return "one day ago";
+ }
+ if (delta < 365 * 24h) {
+ return fmt::format(FMT_STRING("{} days ago"), delta / 24h);
+ }
+ if (delta < (2 * 365 * 24h)) {
+ return "over a year ago";
+ }
+ return fmt::format(FMT_STRING("over {} years ago"), delta / (365 * 24h));
+}
+
+std::string
+point::as_precise_time_ago() const
+{
+ struct timeval now, diff;
+
+ now = this->p_recent_point.value_or(current_timeval());
+ if (this->p_convert_to_local) {
+ now.tv_sec = convert_log_time_to_local(now.tv_sec);
+ }
+
+ timersub(&now, &this->p_past_point, &diff);
+ if (diff.tv_sec < 0) {
+ return this->as_time_ago();
+ } else if (diff.tv_sec <= 1) {
+ return "a second ago";
+ } else if (diff.tv_sec < (10 * 60)) {
+ if (diff.tv_sec < 60) {
+ return fmt::format(FMT_STRING("{:2} seconds ago"), diff.tv_sec);
+ }
+
+ time_t seconds = diff.tv_sec % 60;
+ time_t minutes = diff.tv_sec / 60;
+
+ return fmt::format(FMT_STRING("{:2} minute{} and {:2} second{} ago"),
+ minutes,
+ minutes > 1 ? "s" : "",
+ seconds,
+ seconds == 1 ? "" : "s");
+ } else {
+ return this->as_time_ago();
+ }
+}
+
+duration
+duration::from_tv(const struct timeval& tv)
+{
+ return duration{tv};
+}
+
+std::string
+duration::to_string() const
+{
+ /* 24h22m33s111 */
+
+ static const struct rel_interval {
+ uint64_t length;
+ const char* format;
+ const char* symbol;
+ } intervals[] = {
+ {1000, "%03lld%s", ""},
+ {60, "%lld%s", "s"},
+ {60, "%lld%s", "m"},
+ {24, "%lld%s", "h"},
+ {0, "%lld%s", "d"},
+ };
+
+ const auto* curr_interval = intervals;
+ auto usecs = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::seconds(this->d_timeval.tv_sec))
+ + std::chrono::microseconds(this->d_timeval.tv_usec);
+ auto millis = std::chrono::duration_cast<std::chrono::milliseconds>(usecs);
+ std::string retval;
+ bool neg = false;
+
+ if (millis < 0s) {
+ neg = true;
+ millis = -millis;
+ }
+
+ uint64_t remaining;
+ if (millis >= 10min) {
+ remaining
+ = std::chrono::duration_cast<std::chrono::seconds>(millis).count();
+ curr_interval += 1;
+ } else {
+ remaining = millis.count();
+ }
+
+ for (; curr_interval != std::end(intervals); curr_interval++) {
+ uint64_t amount;
+ char segment[32];
+
+ if (curr_interval->length) {
+ amount = remaining % curr_interval->length;
+ remaining = remaining / curr_interval->length;
+ } else {
+ amount = remaining;
+ remaining = 0;
+ }
+
+ if (!amount && !remaining) {
+ break;
+ }
+
+ snprintf(segment,
+ sizeof(segment),
+ curr_interval->format,
+ amount,
+ curr_interval->symbol);
+ retval.insert(0, segment);
+ if (remaining > 0 && amount < 10 && curr_interval->symbol[0]) {
+ retval.insert(0, "0");
+ }
+ }
+
+ if (neg) {
+ retval.insert(0, "-");
+ }
+
+ return retval;
+}
+
+} // namespace time
+} // namespace humanize
diff --git a/src/base/humanize.time.hh b/src/base/humanize.time.hh
new file mode 100644
index 0000000..96edebd
--- /dev/null
+++ b/src/base/humanize.time.hh
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_humanize_time_hh
+#define lnav_humanize_time_hh
+
+#include <string>
+
+#include <sys/time.h>
+
+#include "optional.hpp"
+
+namespace humanize {
+namespace time {
+
+class point {
+public:
+ static point from_tv(const struct timeval& tv);
+
+ point& with_recent_point(const struct timeval& tv)
+ {
+ this->p_recent_point = tv;
+ return *this;
+ }
+
+ point& with_convert_to_local(bool convert_to_local)
+ {
+ this->p_convert_to_local = convert_to_local;
+ return *this;
+ }
+
+ std::string as_time_ago() const;
+
+ std::string as_precise_time_ago() const;
+
+private:
+ explicit point(const struct timeval& tv)
+ : p_past_point{tv.tv_sec, tv.tv_usec}
+ {
+ }
+
+ struct timeval p_past_point;
+ nonstd::optional<struct timeval> p_recent_point;
+ bool p_convert_to_local{false};
+};
+
+class duration {
+public:
+ static duration from_tv(const struct timeval& tv);
+
+ std::string to_string() const;
+
+private:
+ explicit duration(const struct timeval& tv) : d_timeval(tv) {}
+
+ struct timeval d_timeval;
+};
+
+} // namespace time
+} // namespace humanize
+
+#endif
diff --git a/src/base/humanize.time.tests.cc b/src/base/humanize.time.tests.cc
new file mode 100644
index 0000000..853dd04
--- /dev/null
+++ b/src/base/humanize.time.tests.cc
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <chrono>
+#include <iostream>
+
+#include "config.h"
+#include "doctest/doctest.h"
+#include "humanize.time.hh"
+
+TEST_CASE("time ago")
+{
+ using namespace std::chrono_literals;
+
+ time_t t1 = 1610000000;
+ auto t1_chrono = std::chrono::seconds(t1);
+
+ auto p1 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) t1 + 5, 0});
+
+ CHECK(p1.as_time_ago() == "just now");
+ CHECK(p1.as_precise_time_ago() == " 5 seconds ago");
+
+ auto p2 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) t1 + 65, 0});
+
+ CHECK(p2.as_time_ago() == "one minute ago");
+ CHECK(p2.as_precise_time_ago() == " 1 minute and 5 seconds ago");
+
+ auto p3 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) t1 + (3 * 60 + 5), 0});
+
+ CHECK(p3.as_time_ago() == "3 minutes ago");
+ CHECK(p3.as_precise_time_ago() == " 3 minutes and 5 seconds ago");
+
+ auto p4 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 65min).count(), 0});
+
+ CHECK(p4.as_time_ago() == "one hour ago");
+ CHECK(p4.as_precise_time_ago() == "one hour ago");
+
+ auto p5 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 3h).count(), 0});
+
+ CHECK(p5.as_time_ago() == "3 hours ago");
+ CHECK(p5.as_precise_time_ago() == "3 hours ago");
+
+ auto p6 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 25h).count(), 0});
+
+ CHECK(p6.as_time_ago() == "one day ago");
+ CHECK(p6.as_precise_time_ago() == "one day ago");
+
+ auto p7 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 50h).count(), 0});
+
+ CHECK(p7.as_time_ago() == "2 days ago");
+ CHECK(p7.as_precise_time_ago() == "2 days ago");
+
+ auto p8 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 370 * 24h).count(), 0});
+
+ CHECK(p8.as_time_ago() == "over a year ago");
+ CHECK(p8.as_precise_time_ago() == "over a year ago");
+
+ auto p9 = humanize::time::point::from_tv({t1, 0}).with_recent_point(
+ {(time_t) (t1_chrono + 800 * 24h).count(), 0});
+
+ CHECK(p9.as_time_ago() == "over 2 years ago");
+ CHECK(p9.as_precise_time_ago() == "over 2 years ago");
+
+ CHECK(humanize::time::point::from_tv({1610000000, 0})
+ .with_recent_point({(time_t) 1612000000, 0})
+ .as_time_ago()
+ == "23 days ago");
+}
+
+TEST_CASE("duration to_string")
+{
+ std::string val;
+
+ val = humanize::time::duration::from_tv({25 * 60 * 60, 123000}).to_string();
+ CHECK(val == "1d01h00m00s");
+ val = humanize::time::duration::from_tv({25 * 60 * 60 + 25 * 60, 123000})
+ .to_string();
+ CHECK(val == "1d01h25m00s");
+ val = humanize::time::duration::from_tv({10, 123000}).to_string();
+ CHECK(val == "10s123");
+ val = humanize::time::duration::from_tv({10, 0}).to_string();
+ CHECK(val == "10s000");
+ val = humanize::time::duration::from_tv({0, 100000}).to_string();
+ CHECK(val == "100");
+ val = humanize::time::duration::from_tv({0, 0}).to_string();
+ CHECK(val == "");
+ val = humanize::time::duration::from_tv({0, -10000}).to_string();
+ CHECK(val == "-010");
+ val = humanize::time::duration::from_tv({-10, 0}).to_string();
+ CHECK(val == "-10s000");
+}
diff --git a/src/base/injector.bind.hh b/src/base/injector.bind.hh
new file mode 100644
index 0000000..f240cd1
--- /dev/null
+++ b/src/base/injector.bind.hh
@@ -0,0 +1,173 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file injector.bind.hh
+ */
+
+#ifndef lnav_injector_bind_hh
+#define lnav_injector_bind_hh
+
+#include "injector.hh"
+
+namespace injector {
+
+namespace details {
+
+template<typename I, typename R, typename... Args>
+std::function<std::shared_ptr<I>()>
+create_factory(R (*)(Args...))
+{
+ return []() { return std::make_shared<I>(::injector::get<Args>()...); };
+}
+
+template<typename I, std::enable_if_t<has_injectable<I>::value, bool> = true>
+std::function<std::shared_ptr<I>()>
+create_factory()
+{
+ typename I::injectable* i = nullptr;
+
+ return create_factory<I>(i);
+}
+
+template<typename I, std::enable_if_t<!has_injectable<I>::value, bool> = true>
+std::function<std::shared_ptr<I>()>
+create_factory() noexcept
+{
+ return []() { return std::make_shared<I>(); };
+}
+
+} // namespace details
+
+template<typename T, typename... Annotations>
+struct bind : singleton_storage<T, Annotations...> {
+ template<typename I = T,
+ std::enable_if_t<has_injectable<I>::value, bool> = true>
+ static bool to_singleton() noexcept
+ {
+ typename I::injectable* i = nullptr;
+ singleton_storage<T, Annotations...>::ss_owner
+ = create_from_injectable<I>(i)();
+ singleton_storage<T, Annotations...>::ss_data
+ = singleton_storage<T, Annotations...>::ss_owner.get();
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+
+ return true;
+ }
+
+ template<typename I = T,
+ std::enable_if_t<!has_injectable<I>::value, bool> = true>
+ static bool to_singleton() noexcept
+ {
+ singleton_storage<T, Annotations...>::ss_owner = std::make_shared<T>();
+ singleton_storage<T, Annotations...>::ss_data
+ = singleton_storage<T, Annotations...>::ss_owner.get();
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+ return true;
+ }
+
+ struct lifetime {
+ ~lifetime()
+ {
+ singleton_storage<T, Annotations...>::ss_owner = nullptr;
+ singleton_storage<T, Annotations...>::ss_data = nullptr;
+ }
+ };
+
+ template<typename I = T,
+ std::enable_if_t<has_injectable<I>::value, bool> = true>
+ static lifetime to_scoped_singleton() noexcept
+ {
+ typename I::injectable* i = nullptr;
+ singleton_storage<T, Annotations...>::ss_owner
+ = create_from_injectable<I>(i)();
+ singleton_storage<T, Annotations...>::ss_data
+ = singleton_storage<T, Annotations...>::ss_owner.get();
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+
+ return {};
+ }
+
+ template<typename... Args>
+ static bool to_instance(T* (*f)(Args...)) noexcept
+ {
+ singleton_storage<T, Annotations...>::ss_data
+ = f(::injector::get<Args>()...);
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+ return true;
+ }
+
+ static bool to_instance(T* data) noexcept
+ {
+ singleton_storage<T, Annotations...>::ss_data = data;
+ singleton_storage<T, Annotations...>::ss_scope = scope::singleton;
+ return true;
+ }
+
+ template<typename I>
+ static bool to() noexcept
+ {
+ singleton_storage<T, Annotations...>::ss_factory
+ = details::create_factory<I>();
+ singleton_storage<T, Annotations...>::ss_scope = scope::none;
+ return true;
+ }
+};
+
+template<typename T>
+struct bind_multiple : multiple_storage<T> {
+ bind_multiple() noexcept = default;
+
+ template<typename I>
+ bind_multiple& add() noexcept
+ {
+ multiple_storage<T>::get_factories()[typeid(I).name()]
+ = details::create_factory<I>();
+
+ return *this;
+ }
+
+ template<typename I, typename... Annotations>
+ bind_multiple& add_singleton() noexcept
+ {
+ auto factory = details::create_factory<I>();
+ auto single = factory();
+
+ if (sizeof...(Annotations) > 0) {
+ bind<T, Annotations...>::to_instance(single.get());
+ }
+ bind<I, Annotations...>::to_instance(single.get());
+ multiple_storage<T>::get_factories()[typeid(I).name()]
+ = [single]() { return single; };
+
+ return *this;
+ }
+};
+
+} // namespace injector
+
+#endif
diff --git a/src/base/injector.hh b/src/base/injector.hh
new file mode 100644
index 0000000..c6848a6
--- /dev/null
+++ b/src/base/injector.hh
@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file injector.hh
+ */
+
+#ifndef lnav_injector_hh
+#define lnav_injector_hh
+
+#include <map>
+#include <memory>
+#include <type_traits>
+#include <vector>
+
+#include <assert.h>
+
+#include "base/lnav_log.hh"
+
+namespace injector {
+
+enum class scope {
+ undefined,
+ none,
+ singleton,
+};
+
+template<typename Annotation>
+void force_linking(Annotation anno);
+
+template<class...>
+using void_t = void;
+
+template<typename T, typename... Annotations>
+struct with_annotations {
+ T value;
+};
+
+template<class, class = void>
+struct has_injectable : std::false_type {};
+
+template<class T>
+struct has_injectable<T, void_t<typename T::injectable>> : std::true_type {};
+
+template<typename T, typename... Annotations>
+struct singleton_storage {
+ static scope get_scope() { return ss_scope; }
+
+ static T* get()
+ {
+ static int _[] = {0, (force_linking(Annotations{}), 0)...};
+ (void) _;
+ return ss_data;
+ }
+
+ static std::shared_ptr<T> get_owner()
+ {
+ static int _[] = {0, (force_linking(Annotations{}), 0)...};
+ (void) _;
+ return ss_owner;
+ }
+
+ static std::shared_ptr<T> create()
+ {
+ static int _[] = {0, (force_linking(Annotations{}), 0)...};
+ (void) _;
+ return ss_factory();
+ }
+
+protected:
+ static scope ss_scope;
+ static T* ss_data;
+ static std::shared_ptr<T> ss_owner;
+ static std::function<std::shared_ptr<T>()> ss_factory;
+};
+
+template<typename T, typename... Annotations>
+T* singleton_storage<T, Annotations...>::ss_data = nullptr;
+
+template<typename T, typename... Annotations>
+scope singleton_storage<T, Annotations...>::ss_scope = scope::undefined;
+
+template<typename T, typename... Annotations>
+std::shared_ptr<T> singleton_storage<T, Annotations...>::ss_owner;
+
+template<typename T, typename... Annotations>
+std::function<std::shared_ptr<T>()>
+ singleton_storage<T, Annotations...>::ss_factory;
+
+template<typename T>
+struct Impl {
+ using type = T;
+};
+
+template<typename T>
+struct multiple_storage {
+ static std::vector<std::shared_ptr<T>> create()
+ {
+ std::vector<std::shared_ptr<T>> retval;
+
+ for (const auto& pair : get_factories()) {
+ retval.template emplace_back(pair.second());
+ }
+ return retval;
+ }
+
+protected:
+ using factory_map_t
+ = std::map<std::string, std::function<std::shared_ptr<T>()>>;
+
+ static factory_map_t& get_factories()
+ {
+ static factory_map_t retval;
+
+ return retval;
+ }
+};
+
+template<typename T,
+ typename... Annotations,
+ std::enable_if_t<std::is_reference<T>::value, bool> = true>
+T
+get()
+{
+ using plain_t = std::remove_const_t<std::remove_reference_t<T>>;
+
+ return *singleton_storage<plain_t, Annotations...>::get();
+}
+
+template<typename T,
+ typename... Annotations,
+ std::enable_if_t<std::is_pointer<T>::value, bool> = true>
+T
+get()
+{
+ using plain_t = std::remove_const_t<std::remove_pointer_t<T>>;
+
+ return singleton_storage<plain_t, Annotations...>::get();
+}
+
+template<class T>
+struct is_shared_ptr : std::false_type {};
+
+template<class T>
+struct is_shared_ptr<std::shared_ptr<T>> : std::true_type {};
+
+template<class T>
+struct is_vector : std::false_type {};
+
+template<class T>
+struct is_vector<std::vector<T>> : std::true_type {};
+
+template<typename I, typename R, typename... IArgs, typename... Args>
+std::function<std::shared_ptr<I>()> create_from_injectable(R (*)(IArgs...),
+ Args&... args);
+
+template<typename T,
+ typename... Args,
+ std::enable_if_t<has_injectable<typename T::element_type>::value, bool>
+ = true,
+ std::enable_if_t<is_shared_ptr<T>::value, bool> = true>
+T
+get(Args&... args)
+{
+ typename T::element_type::injectable* i = nullptr;
+
+ if (singleton_storage<typename T::element_type>::get_scope()
+ == scope::singleton)
+ {
+ return singleton_storage<typename T::element_type>::get_owner();
+ }
+ return create_from_injectable<typename T::element_type>(i, args...)();
+}
+
+template<
+ typename T,
+ typename... Annotations,
+ std::enable_if_t<!has_injectable<typename T::element_type>::value, bool>
+ = true,
+ std::enable_if_t<is_shared_ptr<T>::value, bool> = true>
+T
+get()
+{
+ return singleton_storage<typename T::element_type,
+ Annotations...>::get_owner();
+}
+
+template<typename T, std::enable_if_t<is_vector<T>::value, bool> = true>
+T
+get()
+{
+ return multiple_storage<typename T::value_type::element_type>::create();
+}
+
+template<typename I, typename R, typename... IArgs, typename... Args>
+std::function<std::shared_ptr<I>()>
+create_from_injectable(R (*)(IArgs...), Args&... args)
+{
+ return [&]() {
+ return std::make_shared<I>(args..., ::injector::get<IArgs>()...);
+ };
+}
+
+} // namespace injector
+
+#endif
diff --git a/src/base/intern_string.cc b/src/base/intern_string.cc
new file mode 100644
index 0000000..676d2bc
--- /dev/null
+++ b/src/base/intern_string.cc
@@ -0,0 +1,303 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file intern_string.cc
+ */
+
+#include <mutex>
+
+#include "intern_string.hh"
+
+#include <string.h>
+
+#include "config.h"
+#include "pcrepp/pcre2pp.hh"
+#include "xxHash/xxhash.h"
+
+const static int TABLE_SIZE = 4095;
+
+struct intern_string::intern_table {
+ ~intern_table()
+ {
+ for (auto is : this->it_table) {
+ auto curr = is;
+
+ while (curr != nullptr) {
+ auto next = curr->is_next;
+
+ delete curr;
+ curr = next;
+ }
+ }
+ }
+
+ intern_string* it_table[TABLE_SIZE];
+};
+
+intern_table_lifetime
+intern_string::get_table_lifetime()
+{
+ static intern_table_lifetime retval = std::make_shared<intern_table>();
+
+ return retval;
+}
+
+unsigned long
+hash_str(const char* str, size_t len)
+{
+ return XXH3_64bits(str, len);
+}
+
+const intern_string*
+intern_string::lookup(const char* str, ssize_t len) noexcept
+{
+ unsigned long h;
+ intern_string* curr;
+
+ if (len == -1) {
+ len = strlen(str);
+ }
+ h = hash_str(str, len) % TABLE_SIZE;
+
+ {
+ static std::mutex table_mutex;
+
+ std::lock_guard<std::mutex> lk(table_mutex);
+ auto tab = get_table_lifetime();
+
+ curr = tab->it_table[h];
+ while (curr != nullptr) {
+ if (static_cast<ssize_t>(curr->is_str.size()) == len
+ && strncmp(curr->is_str.c_str(), str, len) == 0)
+ {
+ return curr;
+ }
+ curr = curr->is_next;
+ }
+
+ curr = new intern_string(str, len);
+ curr->is_next = tab->it_table[h];
+ tab->it_table[h] = curr;
+
+ return curr;
+ }
+}
+
+const intern_string*
+intern_string::lookup(const string_fragment& sf) noexcept
+{
+ return lookup(sf.data(), sf.length());
+}
+
+const intern_string*
+intern_string::lookup(const std::string& str) noexcept
+{
+ return lookup(str.c_str(), str.size());
+}
+
+bool
+intern_string::startswith(const char* prefix) const
+{
+ const char* curr = this->is_str.data();
+
+ while (*prefix != '\0' && *prefix == *curr) {
+ prefix += 1;
+ curr += 1;
+ }
+
+ return *prefix == '\0';
+}
+
+string_fragment
+string_fragment::trim(const char* tokens) const
+{
+ string_fragment retval = *this;
+
+ while (retval.sf_begin < retval.sf_end) {
+ bool found = false;
+
+ for (int lpc = 0; tokens[lpc] != '\0'; lpc++) {
+ if (retval.sf_string[retval.sf_begin] == tokens[lpc]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ break;
+ }
+
+ retval.sf_begin += 1;
+ }
+ while (retval.sf_begin < retval.sf_end) {
+ bool found = false;
+
+ for (int lpc = 0; tokens[lpc] != '\0'; lpc++) {
+ if (retval.sf_string[retval.sf_end - 1] == tokens[lpc]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ break;
+ }
+
+ retval.sf_end -= 1;
+ }
+
+ return retval;
+}
+
+string_fragment
+string_fragment::trim() const
+{
+ return this->trim(" \t\r\n");
+}
+
+nonstd::optional<string_fragment>
+string_fragment::consume_n(int amount) const
+{
+ if (amount > this->length()) {
+ return nonstd::nullopt;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ this->sf_begin + amount,
+ this->sf_end,
+ };
+}
+
+string_fragment::split_result
+string_fragment::split_n(int amount) const
+{
+ if (amount > this->length()) {
+ return nonstd::nullopt;
+ }
+
+ return std::make_pair(
+ string_fragment{
+ this->sf_string,
+ this->sf_begin,
+ this->sf_begin + amount,
+ },
+ string_fragment{
+ this->sf_string,
+ this->sf_begin + amount,
+ this->sf_end,
+ });
+}
+
+std::vector<string_fragment>
+string_fragment::split_lines() const
+{
+ std::vector<string_fragment> retval;
+ int start = this->sf_begin;
+
+ for (auto index = start; index < this->sf_end; index++) {
+ if (this->sf_string[index] == '\n') {
+ retval.emplace_back(this->sf_string, start, index + 1);
+ start = index + 1;
+ }
+ }
+ retval.emplace_back(this->sf_string, start, this->sf_end);
+
+ return retval;
+}
+
+Result<ssize_t, const char*>
+string_fragment::utf8_length() const
+{
+ ssize_t retval = 0;
+
+ for (ssize_t byte_index = this->sf_begin; byte_index < this->sf_end;) {
+ auto ch_size = TRY(ww898::utf::utf8::char_size([this, byte_index]() {
+ return std::make_pair(this->sf_string[byte_index],
+ this->sf_end - byte_index);
+ }));
+ byte_index += ch_size;
+ retval += 1;
+ }
+
+ return Ok(retval);
+}
+
+string_fragment::case_style
+string_fragment::detect_text_case_style() const
+{
+ static const auto LOWER_RE
+ = lnav::pcre2pp::code::from_const(R"(^[^A-Z]+$)");
+ static const auto UPPER_RE
+ = lnav::pcre2pp::code::from_const(R"(^[^a-z]+$)");
+ static const auto CAMEL_RE
+ = lnav::pcre2pp::code::from_const(R"(^(?:[A-Z][a-z0-9]+)+$)");
+
+ if (LOWER_RE.find_in(*this).ignore_error().has_value()) {
+ return case_style::lower;
+ }
+ if (UPPER_RE.find_in(*this).ignore_error().has_value()) {
+ return case_style::upper;
+ }
+ if (CAMEL_RE.find_in(*this).ignore_error().has_value()) {
+ return case_style::camel;
+ }
+
+ return case_style::mixed;
+}
+
+std::string
+string_fragment::to_string_with_case_style(case_style style) const
+{
+ std::string retval;
+
+ switch (style) {
+ case case_style::lower: {
+ for (auto ch : *this) {
+ retval.append(1, std::tolower(ch));
+ }
+ break;
+ }
+ case case_style::upper: {
+ for (auto ch : *this) {
+ retval.append(1, std::toupper(ch));
+ }
+ break;
+ }
+ case case_style::camel: {
+ retval = this->to_string();
+ if (!this->empty()) {
+ retval[0] = toupper(retval[0]);
+ }
+ break;
+ }
+ case case_style::mixed: {
+ return this->to_string();
+ }
+ }
+
+ return retval;
+}
diff --git a/src/base/intern_string.hh b/src/base/intern_string.hh
new file mode 100644
index 0000000..4ae40da
--- /dev/null
+++ b/src/base/intern_string.hh
@@ -0,0 +1,865 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file intern_string.hh
+ */
+
+#ifndef intern_string_hh
+#define intern_string_hh
+
+#include <ostream>
+#include <string>
+#include <vector>
+
+#include <assert.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include "fmt/format.h"
+#include "optional.hpp"
+#include "scn/util/string_view.h"
+#include "strnatcmp.h"
+#include "ww898/cp_utf8.hpp"
+
+struct string_fragment {
+ using iterator = const char*;
+
+ static string_fragment invalid()
+ {
+ string_fragment retval;
+
+ retval.invalidate();
+ return retval;
+ }
+
+ static string_fragment from_c_str(const char* str)
+ {
+ return string_fragment{str, 0, str != nullptr ? (int) strlen(str) : 0};
+ }
+
+ static string_fragment from_c_str(const unsigned char* str)
+ {
+ return string_fragment{
+ str, 0, str != nullptr ? (int) strlen((char*) str) : 0};
+ }
+
+ template<typename T, std::size_t N>
+ static string_fragment from_const(const T (&str)[N])
+ {
+ return string_fragment{str, 0, (int) N - 1};
+ }
+
+ static string_fragment from_str(const std::string& str)
+ {
+ return string_fragment{str.c_str(), 0, (int) str.size()};
+ }
+
+ static string_fragment from_substr(const std::string& str,
+ size_t offset,
+ size_t length)
+ {
+ return string_fragment{
+ str.c_str(), (int) offset, (int) (offset + length)};
+ }
+
+ static string_fragment from_str_range(const std::string& str,
+ size_t begin,
+ size_t end)
+ {
+ return string_fragment{str.c_str(), (int) begin, (int) end};
+ }
+
+ static string_fragment from_bytes(const char* bytes, size_t len)
+ {
+ return string_fragment{bytes, 0, (int) len};
+ }
+
+ static string_fragment from_bytes(const unsigned char* bytes, size_t len)
+ {
+ return string_fragment{(const char*) bytes, 0, (int) len};
+ }
+
+ static string_fragment from_memory_buffer(const fmt::memory_buffer& buf)
+ {
+ return string_fragment{buf.data(), 0, (int) buf.size()};
+ }
+
+ static string_fragment from_byte_range(const char* bytes,
+ size_t begin,
+ size_t end)
+ {
+ return string_fragment{bytes, (int) begin, (int) end};
+ }
+
+ explicit string_fragment(const char* str = "", int begin = 0, int end = -1)
+ : sf_string(str), sf_begin(begin), sf_end(end == -1 ? strlen(str) : end)
+ {
+ }
+
+ explicit string_fragment(const unsigned char* str,
+ int begin = 0,
+ int end = -1)
+ : sf_string((const char*) str), sf_begin(begin),
+ sf_end(end == -1 ? strlen((const char*) str) : end)
+ {
+ }
+
+ string_fragment(const std::string& str)
+ : sf_string(str.c_str()), sf_begin(0), sf_end(str.length())
+ {
+ }
+
+ bool is_valid() const
+ {
+ return this->sf_begin != -1 && this->sf_begin <= this->sf_end;
+ }
+
+ int length() const { return this->sf_end - this->sf_begin; }
+
+ Result<ssize_t, const char*> utf8_length() const;
+
+ const char* data() const { return &this->sf_string[this->sf_begin]; }
+
+ const unsigned char* udata() const
+ {
+ return (const unsigned char*) &this->sf_string[this->sf_begin];
+ }
+
+ char* writable_data(int offset = 0)
+ {
+ return (char*) &this->sf_string[this->sf_begin + offset];
+ }
+
+ char front() const { return this->sf_string[this->sf_begin]; }
+
+ uint32_t front_codepoint() const
+ {
+ size_t index = 0;
+ try {
+ return ww898::utf::utf8::read(
+ [this, &index]() { return this->data()[index++]; });
+ } catch (const std::runtime_error& e) {
+ return this->data()[0];
+ }
+ }
+
+ char back() const { return this->sf_string[this->sf_end - 1]; }
+
+ void pop_back()
+ {
+ if (!this->empty()) {
+ this->sf_end -= 1;
+ }
+ }
+
+ iterator begin() const { return &this->sf_string[this->sf_begin]; }
+
+ iterator end() const { return &this->sf_string[this->sf_end]; }
+
+ bool empty() const { return !this->is_valid() || length() == 0; }
+
+ Result<ssize_t, const char*> codepoint_to_byte_index(ssize_t cp_index) const
+ {
+ ssize_t retval = 0;
+
+ while (cp_index > 0) {
+ if (retval >= this->length()) {
+ return Err("index is beyond the end of the string");
+ }
+ auto ch_len = TRY(ww898::utf::utf8::char_size([this, retval]() {
+ return std::make_pair(this->data()[retval],
+ this->length() - retval - 1);
+ }));
+
+ retval += ch_len;
+ cp_index -= 1;
+ }
+
+ return Ok(retval);
+ }
+
+ const char& operator[](int index) const
+ {
+ return this->sf_string[sf_begin + index];
+ }
+
+ bool operator==(const std::string& str) const
+ {
+ if (this->length() != (int) str.length()) {
+ return false;
+ }
+
+ return memcmp(
+ &this->sf_string[this->sf_begin], str.c_str(), str.length())
+ == 0;
+ }
+
+ bool operator==(const string_fragment& sf) const
+ {
+ if (this->length() != sf.length()) {
+ return false;
+ }
+
+ return memcmp(this->data(), sf.data(), sf.length()) == 0;
+ }
+
+ bool iequal(const string_fragment& sf) const
+ {
+ if (this->length() != sf.length()) {
+ return false;
+ }
+
+ return strnatcasecmp(
+ this->length(), this->data(), sf.length(), sf.data())
+ == 0;
+ }
+
+ bool operator==(const char* str) const
+ {
+ size_t len = strlen(str);
+
+ return len == (size_t) this->length()
+ && strncmp(this->data(), str, this->length()) == 0;
+ }
+
+ bool operator!=(const char* str) const { return !(*this == str); }
+
+ bool startswith(const char* prefix) const
+ {
+ const auto* iter = this->begin();
+
+ while (*prefix != '\0' && iter < this->end() && *prefix == *iter) {
+ prefix += 1;
+ iter += 1;
+ }
+
+ return *prefix == '\0';
+ }
+
+ bool endswith(const char* suffix) const
+ {
+ int suffix_len = strlen(suffix);
+
+ if (suffix_len > this->length()) {
+ return false;
+ }
+
+ const auto* curr = this->end() - suffix_len;
+ while (*suffix != '\0' && *curr == *suffix) {
+ suffix += 1;
+ curr += 1;
+ }
+
+ return *suffix == '\0';
+ }
+
+ string_fragment substr(int begin) const
+ {
+ return string_fragment{
+ this->sf_string, this->sf_begin + begin, this->sf_end};
+ }
+
+ string_fragment sub_range(int begin, int end) const
+ {
+ return string_fragment{
+ this->sf_string, this->sf_begin + begin, this->sf_begin + end};
+ }
+
+ size_t count(char ch) const
+ {
+ size_t retval = 0;
+
+ for (int lpc = this->sf_begin; lpc < this->sf_end; lpc++) {
+ if (this->sf_string[lpc] == ch) {
+ retval += 1;
+ }
+ }
+
+ return retval;
+ }
+
+ nonstd::optional<size_t> find(char ch) const
+ {
+ for (int lpc = this->sf_begin; lpc < this->sf_end; lpc++) {
+ if (this->sf_string[lpc] == ch) {
+ return lpc - this->sf_begin;
+ }
+ }
+
+ return nonstd::nullopt;
+ }
+
+ template<typename P>
+ string_fragment find_left_boundary(size_t start, P&& predicate) const
+ {
+ assert((int) start <= this->length());
+
+ if (start > 0 && start == this->length()) {
+ start -= 1;
+ }
+ while (start > 0) {
+ if (predicate(this->data()[start])) {
+ start += 1;
+ break;
+ }
+ start -= 1;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ (int) start,
+ this->sf_end,
+ };
+ }
+
+ template<typename P>
+ string_fragment find_right_boundary(size_t start, P&& predicate) const
+ {
+ while ((int) start < this->length()) {
+ if (predicate(this->data()[start])) {
+ break;
+ }
+ start += 1;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ this->sf_begin,
+ this->sf_begin + (int) start,
+ };
+ }
+
+ template<typename P>
+ string_fragment find_boundaries_around(size_t start, P&& predicate) const
+ {
+ return this->template find_left_boundary(start, predicate)
+ .find_right_boundary(0, predicate);
+ }
+
+ nonstd::optional<std::pair<uint32_t, string_fragment>> consume_codepoint()
+ const
+ {
+ auto cp = this->front_codepoint();
+ auto index_res = this->codepoint_to_byte_index(1);
+
+ if (index_res.isErr()) {
+ return nonstd::nullopt;
+ }
+
+ return std::make_pair(cp, this->substr(index_res.unwrap()));
+ }
+
+ template<typename P>
+ nonstd::optional<string_fragment> consume(P predicate) const
+ {
+ int consumed = 0;
+ while (consumed < this->length()) {
+ if (!predicate(this->data()[consumed])) {
+ break;
+ }
+
+ consumed += 1;
+ }
+
+ if (consumed == 0) {
+ return nonstd::nullopt;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ this->sf_begin + consumed,
+ this->sf_end,
+ };
+ }
+
+ nonstd::optional<string_fragment> consume_n(int amount) const;
+
+ template<typename P>
+ string_fragment skip(P predicate) const
+ {
+ int offset = 0;
+ while (offset < this->length() && predicate(this->data()[offset])) {
+ offset += 1;
+ }
+
+ return string_fragment{
+ this->sf_string,
+ this->sf_begin + offset,
+ this->sf_end,
+ };
+ }
+
+ using split_result
+ = nonstd::optional<std::pair<string_fragment, string_fragment>>;
+
+ template<typename P>
+ split_result split_while(P&& predicate) const
+ {
+ int consumed = 0;
+ while (consumed < this->length()) {
+ if (!predicate(this->data()[consumed])) {
+ break;
+ }
+
+ consumed += 1;
+ }
+
+ if (consumed == 0) {
+ return nonstd::nullopt;
+ }
+
+ return std::make_pair(
+ string_fragment{
+ this->sf_string,
+ this->sf_begin,
+ this->sf_begin + consumed,
+ },
+ string_fragment{
+ this->sf_string,
+ this->sf_begin + consumed,
+ this->sf_end,
+ });
+ }
+
+ template<typename P>
+ split_result split_when(P&& predicate) const
+ {
+ int consumed = 0;
+ while (consumed < this->length()) {
+ if (predicate(this->data()[consumed])) {
+ break;
+ }
+
+ consumed += 1;
+ }
+
+ if (consumed == 0) {
+ return nonstd::nullopt;
+ }
+
+ return std::make_pair(
+ string_fragment{
+ this->sf_string,
+ this->sf_begin,
+ this->sf_begin + consumed,
+ },
+ string_fragment{
+ this->sf_string,
+ this->sf_begin + consumed + 1,
+ this->sf_end,
+ });
+ }
+
+ split_result split_n(int amount) const;
+
+ std::vector<string_fragment> split_lines() const;
+
+ struct tag1 {
+ const char t_value;
+
+ bool operator()(char ch) const { return this->t_value == ch; }
+ };
+
+ struct quoted_string_body {
+ bool qs_in_escape{false};
+
+ bool operator()(char ch)
+ {
+ if (this->qs_in_escape) {
+ this->qs_in_escape = false;
+ return true;
+ } else if (ch == '\\') {
+ this->qs_in_escape = true;
+ return true;
+ } else if (ch == '"') {
+ return false;
+ } else {
+ return true;
+ }
+ }
+ };
+
+ const char* to_string(char* buf) const
+ {
+ memcpy(buf, this->data(), this->length());
+ buf[this->length()] = '\0';
+
+ return buf;
+ }
+
+ std::string to_string() const
+ {
+ return {this->data(), (size_t) this->length()};
+ }
+
+ void clear()
+ {
+ this->sf_begin = 0;
+ this->sf_end = 0;
+ }
+
+ void invalidate()
+ {
+ this->sf_begin = -1;
+ this->sf_end = -1;
+ }
+
+ string_fragment trim(const char* tokens) const;
+ string_fragment trim() const;
+
+ string_fragment prepend(const char* str, int amount) const
+ {
+ return string_fragment{
+ str,
+ this->sf_begin + amount,
+ this->sf_end + amount,
+ };
+ }
+
+ string_fragment erase_before(const char* str, int amount) const
+ {
+ return string_fragment{
+ str,
+ this->sf_begin - amount,
+ this->sf_end - amount,
+ };
+ }
+
+ string_fragment erase(const char* str, int amount) const
+ {
+ return string_fragment{
+ str,
+ this->sf_begin,
+ this->sf_end - amount,
+ };
+ }
+
+ template<typename A>
+ const char* to_c_str(A allocator) const
+ {
+ auto* retval = allocator.allocate(this->length() + 1);
+ memcpy(retval, this->data(), this->length());
+ retval[this->length()] = '\0';
+ return retval;
+ }
+
+ template<typename A>
+ string_fragment to_owned(A allocator) const
+ {
+ return string_fragment{
+ this->template to_c_str(allocator),
+ 0,
+ this->length(),
+ };
+ }
+
+ scn::string_view to_string_view() const
+ {
+ return scn::string_view{this->begin(), this->end()};
+ }
+
+ enum class case_style {
+ lower,
+ upper,
+ camel,
+ mixed,
+ };
+
+ case_style detect_text_case_style() const;
+
+ std::string to_string_with_case_style(case_style style) const;
+
+ const char* sf_string;
+ int sf_begin;
+ int sf_end;
+};
+
+inline bool
+operator==(const std::string& left, const string_fragment& right)
+{
+ return right == left;
+}
+
+inline bool
+operator<(const char* left, const string_fragment& right)
+{
+ int rc = strncmp(left, right.data(), right.length());
+ return rc < 0;
+}
+
+inline void
+operator+=(std::string& left, const string_fragment& right)
+{
+ left.append(right.data(), right.length());
+}
+
+inline bool
+operator<(const string_fragment& left, const char* right)
+{
+ return strncmp(left.data(), right, left.length()) < 0;
+}
+
+inline std::ostream&
+operator<<(std::ostream& os, const string_fragment& sf)
+{
+ os.write(sf.data(), sf.length());
+ return os;
+}
+
+class intern_string {
+public:
+ static const intern_string* lookup(const char* str, ssize_t len) noexcept;
+
+ static const intern_string* lookup(const string_fragment& sf) noexcept;
+
+ static const intern_string* lookup(const std::string& str) noexcept;
+
+ const char* get() const { return this->is_str.c_str(); };
+
+ size_t size() const { return this->is_str.size(); }
+
+ std::string to_string() const { return this->is_str; }
+
+ string_fragment to_string_fragment() const
+ {
+ return string_fragment{this->is_str};
+ }
+
+ bool startswith(const char* prefix) const;
+
+ struct intern_table;
+ static std::shared_ptr<intern_table> get_table_lifetime();
+
+private:
+ friend intern_table;
+
+ intern_string(const char* str, ssize_t len)
+ : is_next(nullptr), is_str(str, (size_t) len)
+ {
+ }
+
+ intern_string* is_next;
+ std::string is_str;
+};
+
+using intern_table_lifetime = std::shared_ptr<intern_string::intern_table>;
+
+class intern_string_t {
+public:
+ using iterator = const char*;
+
+ intern_string_t(const intern_string* is = nullptr) : ist_interned_string(is)
+ {
+ }
+
+ const intern_string* unwrap() const { return this->ist_interned_string; }
+
+ void clear() { this->ist_interned_string = nullptr; };
+
+ bool empty() const { return this->ist_interned_string == nullptr; }
+
+ const char* get() const
+ {
+ if (this->empty()) {
+ return "";
+ }
+ return this->ist_interned_string->get();
+ }
+
+ const char* c_str() const { return this->get(); }
+
+ iterator begin() const { return this->get(); }
+
+ iterator end() const { return this->get() + this->size(); }
+
+ size_t size() const
+ {
+ if (this->ist_interned_string == nullptr) {
+ return 0;
+ }
+ return this->ist_interned_string->size();
+ }
+
+ size_t hash() const
+ {
+ auto ptr = (uintptr_t) this->ist_interned_string;
+
+ return ptr;
+ }
+
+ std::string to_string() const
+ {
+ if (this->ist_interned_string == nullptr) {
+ return "";
+ }
+ return this->ist_interned_string->to_string();
+ }
+
+ string_fragment to_string_fragment() const
+ {
+ if (this->ist_interned_string == nullptr) {
+ return string_fragment{"", 0, 0};
+ }
+ return this->ist_interned_string->to_string_fragment();
+ }
+
+ bool operator<(const intern_string_t& rhs) const
+ {
+ return strcmp(this->get(), rhs.get()) < 0;
+ }
+
+ bool operator==(const intern_string_t& rhs) const
+ {
+ return this->ist_interned_string == rhs.ist_interned_string;
+ }
+
+ bool operator!=(const intern_string_t& rhs) const
+ {
+ return !(*this == rhs);
+ }
+
+ bool operator==(const char* rhs) const
+ {
+ return strcmp(this->get(), rhs) == 0;
+ }
+
+ bool operator!=(const char* rhs) const
+ {
+ return strcmp(this->get(), rhs) != 0;
+ }
+
+ static bool case_lt(const intern_string_t& lhs, const intern_string_t& rhs)
+ {
+ return strnatcasecmp(lhs.size(), lhs.get(), rhs.size(), rhs.get()) < 0;
+ }
+
+private:
+ const intern_string* ist_interned_string;
+};
+
+unsigned long hash_str(const char* str, size_t len);
+
+namespace fmt {
+template<>
+struct formatter<string_fragment> : formatter<string_view> {
+ template<typename FormatContext>
+ auto format(const string_fragment& sf, FormatContext& ctx)
+ {
+ return formatter<string_view>::format(
+ string_view{sf.data(), (size_t) sf.length()}, ctx);
+ }
+};
+
+template<>
+struct formatter<intern_string_t> : formatter<string_view> {
+ template<typename FormatContext>
+ auto format(const intern_string_t& is, FormatContext& ctx)
+ {
+ return formatter<string_view>::format(
+ string_view{is.get(), (size_t) is.size()}, ctx);
+ }
+};
+} // namespace fmt
+
+namespace std {
+template<>
+struct hash<const intern_string_t> {
+ std::size_t operator()(const intern_string_t& ist) const
+ {
+ return ist.hash();
+ }
+};
+} // namespace std
+
+inline bool
+operator<(const char* left, const intern_string_t& right)
+{
+ int rc = strncmp(left, right.get(), right.size());
+ return rc < 0;
+}
+
+inline bool
+operator<(const intern_string_t& left, const char* right)
+{
+ return strncmp(left.get(), right, left.size()) < 0;
+}
+
+inline bool
+operator==(const intern_string_t& left, const string_fragment& sf)
+{
+ return ((int) left.size() == sf.length())
+ && (memcmp(left.get(), sf.data(), left.size()) == 0);
+}
+
+inline bool
+operator==(const string_fragment& left, const intern_string_t& right)
+{
+ return (left.length() == (int) right.size())
+ && (memcmp(left.data(), right.get(), left.length()) == 0);
+}
+
+namespace std {
+inline string
+to_string(const string_fragment& s)
+{
+ return {s.data(), (size_t) s.length()};
+}
+
+inline string
+to_string(const intern_string_t& s)
+{
+ return s.to_string();
+}
+} // namespace std
+
+inline string_fragment
+to_string_fragment(const string_fragment& s)
+{
+ return s;
+}
+
+inline string_fragment
+to_string_fragment(const intern_string_t& s)
+{
+ return string_fragment(s.get(), 0, s.size());
+}
+
+inline string_fragment
+to_string_fragment(const std::string& s)
+{
+ return string_fragment(s.c_str(), 0, s.length());
+}
+
+struct frag_hasher {
+ size_t operator()(const string_fragment& sf) const
+ {
+ return hash_str(sf.data(), sf.length());
+ }
+};
+
+#endif
diff --git a/src/base/intern_string.tests.cc b/src/base/intern_string.tests.cc
new file mode 100644
index 0000000..8816803
--- /dev/null
+++ b/src/base/intern_string.tests.cc
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <cctype>
+#include <iostream>
+
+#include "intern_string.hh"
+
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("string_fragment::startswith")
+{
+ std::string empty;
+ auto sf = string_fragment{empty};
+
+ CHECK_FALSE(sf.startswith("abc"));
+}
+
+TEST_CASE("split_lines")
+{
+ std::string in1 = "Hello, World!";
+ std::string in2 = "Hello, World!\nGoodbye, World!";
+
+ {
+ auto sf = string_fragment(in1);
+ auto split = sf.split_lines();
+
+ CHECK(1 == split.size());
+ CHECK(in1 == split[0].to_string());
+ }
+
+ {
+ auto sf = string_fragment::from_str_range(in1, 7, -1);
+ auto split = sf.split_lines();
+
+ CHECK(1 == split.size());
+ CHECK("World!" == split[0].to_string());
+ }
+
+ {
+ auto sf = string_fragment(in2);
+ auto split = sf.split_lines();
+
+ CHECK(2 == split.size());
+ CHECK("Hello, World!\n" == split[0].to_string());
+ CHECK("Goodbye, World!" == split[1].to_string());
+ }
+}
+
+TEST_CASE("consume")
+{
+ auto is_eq = string_fragment::tag1{'='};
+ auto is_dq = string_fragment::tag1{'"'};
+ auto is_colon = string_fragment::tag1{':'};
+
+ const char* pair = "foo = bar";
+ auto sf = string_fragment(pair);
+
+ auto split_sf = sf.split_while(isalnum);
+
+ CHECK(split_sf.has_value());
+ CHECK(split_sf->first.to_string() == "foo");
+ CHECK(split_sf->second.to_string() == " = bar");
+
+ auto value_frag = split_sf->second.skip(isspace).consume(is_eq);
+
+ CHECK(value_frag.has_value());
+ CHECK(value_frag->to_string() == " bar");
+
+ auto stripped_value_frag = value_frag->consume(isspace);
+
+ CHECK(stripped_value_frag.has_value());
+ CHECK(stripped_value_frag->to_string() == "bar");
+
+ auto no_value = sf.consume(is_colon);
+ CHECK(!no_value.has_value());
+
+ const char* qs = R"("foo \" bar")";
+ auto qs_sf = string_fragment{qs};
+
+ auto qs_body = qs_sf.consume(is_dq);
+ string_fragment::quoted_string_body qsb;
+ auto split_body = qs_body->split_while(qsb);
+
+ CHECK(split_body.has_value());
+ CHECK(split_body->first.to_string() == "foo \\\" bar");
+ CHECK(split_body->second.to_string() == "\"");
+
+ auto empty = split_body->second.consume(is_dq);
+
+ CHECK(empty.has_value());
+ CHECK(empty->empty());
+}
+
+TEST_CASE("find_left_boundary")
+{
+ std::string in1 = "Hello,\nWorld!\n";
+
+ {
+ auto sf = string_fragment{in1};
+
+ auto world_sf = sf.find_left_boundary(
+ in1.length() - 3, [](auto ch) { return ch == '\n'; });
+ CHECK(world_sf.to_string() == "World!\n");
+ auto full_sf
+ = sf.find_left_boundary(3, [](auto ch) { return ch == '\n'; });
+ CHECK(full_sf.to_string() == in1);
+ }
+}
+
+TEST_CASE("find_right_boundary")
+{
+ std::string in1 = "Hello,\nWorld!\n";
+
+ {
+ auto sf = string_fragment{in1};
+
+ auto world_sf = sf.find_right_boundary(
+ in1.length() - 3, [](auto ch) { return ch == '\n'; });
+ CHECK(world_sf.to_string() == "Hello,\nWorld!");
+ auto hello_sf
+ = sf.find_right_boundary(3, [](auto ch) { return ch == '\n'; });
+ CHECK(hello_sf.to_string() == "Hello,");
+ }
+}
diff --git a/src/base/is_utf8.cc b/src/base/is_utf8.cc
new file mode 100644
index 0000000..f55dfe0
--- /dev/null
+++ b/src/base/is_utf8.cc
@@ -0,0 +1,313 @@
+/*
+ * is_utf8 is distributed under the following terms:
+ *
+ * Copyright (c) 2013 Palard Julien. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "is_utf8.hh"
+
+#include "config.h"
+
+/*
+ Check if the given unsigned char * is a valid utf-8 sequence.
+
+ Return value :
+ If the string is valid utf-8, 0 is returned.
+ Else the position, starting from 1, is returned.
+
+ Source:
+ http://www.unicode.org/versions/Unicode7.0.0/UnicodeStandard-7.0.pdf
+ page 124, 3.9 "Unicode Encoding Forms", "UTF-8"
+
+
+ Table 3-7. Well-Formed UTF-8 Byte Sequences
+ -----------------------------------------------------------------------------
+ | Code Points | First Byte | Second Byte | Third Byte | Fourth Byte |
+ | U+0000..U+007F | 00..7F | | | |
+ | U+0080..U+07FF | C2..DF | 80..BF | | |
+ | U+0800..U+0FFF | E0 | A0..BF | 80..BF | |
+ | U+1000..U+CFFF | E1..EC | 80..BF | 80..BF | |
+ | U+D000..U+D7FF | ED | 80..9F | 80..BF | |
+ | U+E000..U+FFFF | EE..EF | 80..BF | 80..BF | |
+ | U+10000..U+3FFFF | F0 | 90..BF | 80..BF | 80..BF |
+ | U+40000..U+FFFFF | F1..F3 | 80..BF | 80..BF | 80..BF |
+ | U+100000..U+10FFFF | F4 | 80..8F | 80..BF | 80..BF |
+ -----------------------------------------------------------------------------
+
+ Returns the first erroneous byte position, and give in
+ `faulty_bytes` the number of actually existing bytes taking part in this
+ error.
+*/
+utf8_scan_result
+is_utf8(string_fragment str, nonstd::optional<unsigned char> terminator)
+{
+ const auto* ustr = str.udata();
+ utf8_scan_result retval;
+ ssize_t i = 0;
+
+ while (i < str.length()) {
+ if (ustr[i] == '\x1b') {
+ retval.usr_has_ansi = true;
+ }
+
+ if (terminator && ustr[i] == terminator.value()) {
+ if (retval.usr_message == nullptr) {
+ retval.usr_valid_frag = str.sub_range(0, i);
+ }
+ retval.usr_remaining = str.substr(i + 1);
+ break;
+ }
+
+ if (retval.usr_message != nullptr) {
+ i += 1;
+ continue;
+ }
+
+ retval.usr_valid_frag = str.sub_range(0, i);
+ if (ustr[i] <= 0x7F) /* 00..7F */ {
+ i += 1;
+ } else if (ustr[i] >= 0xC2 && ustr[i] <= 0xDF) /* C2..DF 80..BF */ {
+ if (i + 1 < str.length()) /* Expect a 2nd byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between C2 and DF, expecting a "
+ "2nd byte between 80 and BF";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte between C2 and DF, expecting a 2nd "
+ "byte.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 2;
+ } else if (ustr[i] == 0xE0) /* E0 A0..BF 80..BF */ {
+ if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ {
+ if (ustr[i + 1] < 0xA0 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of E0, expecting a 2nd byte "
+ "between A0 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of E0, expecting a 3nd byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of E0, expecting two following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 3;
+ } else if (ustr[i] >= 0xE1
+ && ustr[i] <= 0xEC) /* E1..EC 80..BF 80..BF */
+ {
+ if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between E1 and EC, expecting the "
+ "2nd byte between 80 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between E1 and EC, expecting the "
+ "3rd byte between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte between E1 and EC, expecting two "
+ "following bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 3;
+ } else if (ustr[i] == 0xED) /* ED 80..9F 80..BF */ {
+ if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0x9F) {
+ retval.usr_message
+ = "After a first byte of ED, expecting 2nd byte "
+ "between 80 and 9F.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of ED, expecting 3rd byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of ED, expecting two following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 3;
+ } else if (ustr[i] >= 0xEE
+ && ustr[i] <= 0xEF) /* EE..EF 80..BF 80..BF */
+ {
+ if (i + 2 < str.length()) /* Expect a 2nd and 3rd byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between EE and EF, expecting 2nd "
+ "byte between 80 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte between EE and EF, expecting 3rd "
+ "byte between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte between EE and EF, two following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 3;
+ } else if (ustr[i] == 0xF0) /* F0 90..BF 80..BF 80..BF */ {
+ if (i + 3 < str.length()) /* Expect a 2nd, 3rd 3th byte */ {
+ if (ustr[i + 1] < 0x90 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F0, expecting 2nd byte "
+ "between 90 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F0, expecting 3rd byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ if (ustr[i + 3] < 0x80 || ustr[i + 3] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F0, expecting 4th byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 4;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of F0, expecting three following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 4;
+ } else if (ustr[i] >= 0xF1
+ && ustr[i] <= 0xF3) /* F1..F3 80..BF 80..BF 80..BF */
+ {
+ if (i + 3 < str.length()) /* Expect a 2nd, 3rd 3th byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F1, F2, or F3, expecting a "
+ "2nd byte between 80 and BF.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F1, F2, or F3, expecting a "
+ "3rd byte between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ if (ustr[i + 3] < 0x80 || ustr[i + 3] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F1, F2, or F3, expecting a "
+ "4th byte between 80 and BF.";
+ retval.usr_faulty_bytes = 4;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of F1, F2, or F3, expecting three "
+ "following bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 4;
+ } else if (ustr[i] == 0xF4) /* F4 80..8F 80..BF 80..BF */ {
+ if (i + 3 < str.length()) /* Expect a 2nd, 3rd 3th byte */ {
+ if (ustr[i + 1] < 0x80 || ustr[i + 1] > 0x8F) {
+ retval.usr_message
+ = "After a first byte of F4, expecting 2nd byte "
+ "between 80 and 8F.";
+ retval.usr_faulty_bytes = 2;
+ continue;
+ }
+ if (ustr[i + 2] < 0x80 || ustr[i + 2] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F4, expecting 3rd byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 3;
+ continue;
+ }
+ if (ustr[i + 3] < 0x80 || ustr[i + 3] > 0xBF) {
+ retval.usr_message
+ = "After a first byte of F4, expecting 4th byte "
+ "between 80 and BF.";
+ retval.usr_faulty_bytes = 4;
+ continue;
+ }
+ } else {
+ retval.usr_message
+ = "After a first byte of F4, expecting three following "
+ "bytes.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ i += 4;
+ } else {
+ retval.usr_message
+ = "Expecting bytes in the following ranges: 00..7F C2..F4.";
+ retval.usr_faulty_bytes = 1;
+ continue;
+ }
+ }
+ if (retval.usr_message == nullptr) {
+ retval.usr_valid_frag = str.sub_range(0, i);
+ }
+ return retval;
+}
diff --git a/src/base/is_utf8.hh b/src/base/is_utf8.hh
new file mode 100644
index 0000000..56a959f
--- /dev/null
+++ b/src/base/is_utf8.hh
@@ -0,0 +1,59 @@
+/*
+ * is_utf8 is distributed under the following terms:
+ *
+ * Copyright (c) 2013 Palard Julien. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#ifndef _IS_UTF8_H
+#define _IS_UTF8_H
+
+#include <stdlib.h>
+#include <sys/types.h>
+
+#include "intern_string.hh"
+#include "optional.hpp"
+
+struct utf8_scan_result {
+ const char* usr_message{nullptr};
+ size_t usr_faulty_bytes{0};
+ string_fragment usr_valid_frag{string_fragment::invalid()};
+ nonstd::optional<string_fragment> usr_remaining;
+ bool usr_has_ansi{false};
+
+ const char* remaining_ptr(const string_fragment& frag) const
+ {
+ if (this->usr_remaining) {
+ return this->usr_remaining->begin();
+ } else {
+ return nullptr;
+ }
+ }
+ bool is_valid() const { return this->usr_message == nullptr; }
+};
+
+utf8_scan_result is_utf8(string_fragment frag,
+ nonstd::optional<unsigned char> terminator
+ = nonstd::nullopt);
+
+#endif /* _IS_UTF8_H */
diff --git a/src/base/isc.cc b/src/base/isc.cc
new file mode 100644
index 0000000..7dbce11
--- /dev/null
+++ b/src/base/isc.cc
@@ -0,0 +1,147 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file isc.cc
+ */
+
+#include <algorithm>
+
+#include "isc.hh"
+
+#include "config.h"
+
+namespace isc {
+
+void
+service_base::start()
+{
+ log_debug("starting service thread for: %s", this->s_name.c_str());
+ this->s_thread = std::thread(&service_base::run, this);
+ this->s_started = true;
+}
+
+void*
+service_base::run()
+{
+ log_info("BEGIN isc thread: %s", this->s_name.c_str());
+ while (this->s_looping) {
+ mstime_t current_time = getmstime();
+ auto timeout = this->compute_timeout(current_time);
+
+ this->s_port.process_for(timeout);
+ this->s_children.cleanup_children();
+
+ try {
+ this->loop_body();
+ } catch (const std::exception& e) {
+ log_error("%s: loop_body() failed with -- %s",
+ this->s_name.c_str(),
+ e.what());
+ this->s_looping = false;
+ } catch (...) {
+ log_error("%s: loop_body() failed with non-standard exception",
+ this->s_name.c_str());
+ this->s_looping = false;
+ }
+ }
+ if (!this->s_children.empty()) {
+ log_debug("stopping children of service: %s", this->s_name.c_str());
+ this->s_children.stop_children();
+ }
+ this->stopped();
+ log_info("END isc thread: %s", this->s_name.c_str());
+
+ return nullptr;
+}
+
+void
+service_base::stop()
+{
+ if (this->s_started) {
+ log_debug("stopping service thread: %s", this->s_name.c_str());
+ if (this->s_looping) {
+ this->s_looping = false;
+ this->s_port.send(empty_msg());
+ }
+ log_debug("waiting for service thread: %s", this->s_name.c_str());
+ this->s_thread.join();
+ log_debug("joined service thread: %s", this->s_name.c_str());
+ this->s_started = false;
+ }
+}
+
+supervisor::supervisor(service_list servs, service_base* parent)
+ : s_service_list(std::move(servs)), s_parent(parent)
+{
+ for (auto& serv : this->s_service_list) {
+ serv->start();
+ }
+}
+
+supervisor::~supervisor()
+{
+ this->stop_children();
+}
+
+void
+supervisor::stop_children()
+{
+ for (auto& serv : this->s_service_list) {
+ serv->stop();
+ }
+ this->cleanup_children();
+}
+
+void
+supervisor::cleanup_children()
+{
+ this->s_service_list.erase(
+ std::remove_if(this->s_service_list.begin(),
+ this->s_service_list.end(),
+ [this](auto& child) {
+ if (child->is_looping()) {
+ return false;
+ }
+
+ child->stop();
+ if (this->s_parent != nullptr) {
+ this->s_parent->child_finished(child);
+ }
+ return true;
+ }),
+ this->s_service_list.end());
+}
+
+void
+supervisor::add_child_service(std::shared_ptr<service_base> new_service)
+{
+ this->s_service_list.emplace_back(new_service);
+ new_service->start();
+}
+
+} // namespace isc
diff --git a/src/base/isc.hh b/src/base/isc.hh
new file mode 100644
index 0000000..339dcaf
--- /dev/null
+++ b/src/base/isc.hh
@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file isc.hh
+ */
+
+#include <atomic>
+#include <chrono>
+#include <condition_variable>
+#include <deque>
+#include <mutex>
+#include <thread>
+#include <utility>
+
+#include "injector.hh"
+#include "safe/safe.h"
+#include "time_util.hh"
+
+#ifndef lnav_isc_hh
+# define lnav_isc_hh
+
+namespace isc {
+
+struct msg {
+ std::function<void()> m_callback;
+};
+
+inline msg
+empty_msg()
+{
+ return {[]() {}};
+}
+
+class msg_port {
+public:
+ msg_port() = default;
+
+ void send(msg&& m)
+ {
+ safe::WriteAccess<safe_message_list, std::unique_lock> writable_msgs(
+ this->mp_messages);
+
+ writable_msgs->emplace_back(m);
+ this->sp_cond.notify_all();
+ }
+
+ template<class Rep, class Period>
+ void process_for(const std::chrono::duration<Rep, Period>& rel_time)
+ {
+ std::deque<msg> tmp_msgs;
+
+ {
+ safe::WriteAccess<safe_message_list, std::unique_lock>
+ writable_msgs(this->mp_messages);
+
+ if (writable_msgs->empty() && rel_time.count() > 0) {
+ this->sp_cond.template wait_for(writable_msgs.lock, rel_time);
+ }
+
+ tmp_msgs.swap(*writable_msgs);
+ }
+ while (!tmp_msgs.empty()) {
+ auto& m = tmp_msgs.front();
+
+ m.m_callback();
+ tmp_msgs.pop_front();
+ }
+ }
+
+private:
+ using message_list = std::deque<msg>;
+ using safe_message_list = safe::Safe<message_list>;
+
+ std::condition_variable sp_cond;
+ safe_message_list mp_messages;
+};
+
+class service_base;
+using service_list = std::vector<std::shared_ptr<service_base>>;
+
+struct supervisor {
+ explicit supervisor(service_list servs = {},
+ service_base* parent = nullptr);
+
+ ~supervisor();
+
+ bool empty() const { return this->s_service_list.empty(); }
+
+ void add_child_service(std::shared_ptr<service_base> new_service);
+
+ void stop_children();
+
+ void cleanup_children();
+
+protected:
+ service_list s_service_list;
+ service_base* s_parent;
+};
+
+class service_base : public std::enable_shared_from_this<service_base> {
+public:
+ explicit service_base(std::string name)
+ : s_name(std::move(name)), s_children({}, this)
+ {
+ }
+
+ virtual ~service_base() = default;
+
+ bool is_looping() const { return this->s_looping; }
+
+ msg_port& get_port() { return this->s_port; }
+
+ friend supervisor;
+
+private:
+ void start();
+
+ void stop();
+
+protected:
+ virtual void* run();
+ virtual void loop_body() {}
+ virtual void child_finished(std::shared_ptr<service_base> child) {}
+ virtual void stopped() {}
+ virtual std::chrono::milliseconds compute_timeout(
+ mstime_t current_time) const
+ {
+ using namespace std::literals::chrono_literals;
+
+ return 1s;
+ }
+
+ const std::string s_name;
+ bool s_started{false};
+ std::thread s_thread;
+ std::atomic<bool> s_looping{true};
+ msg_port s_port;
+ supervisor s_children;
+};
+
+template<typename T>
+class service : public service_base {
+public:
+ explicit service(std::string sub_name = "")
+ : service_base(std::string(__PRETTY_FUNCTION__) + " " + sub_name)
+ {
+ }
+
+ template<typename F>
+ void send(F msg)
+ {
+ this->s_port.send({[lifetime = this->shared_from_this(), this, msg]() {
+ msg(*(static_cast<T*>(this)));
+ }});
+ }
+
+ template<typename F, class Rep, class Period>
+ void send_and_wait(F msg,
+ const std::chrono::duration<Rep, Period>& rel_time)
+ {
+ msg_port reply_port;
+
+ this->s_port.send(
+ {[lifetime = this->shared_from_this(), this, &reply_port, msg]() {
+ msg(*(static_cast<T*>(this)));
+ reply_port.send(empty_msg());
+ }});
+ reply_port.template process_for(rel_time);
+ }
+};
+
+template<typename T, typename Service, typename... Annotations>
+struct to {
+ void send(std::function<void(T&)> cb)
+ {
+ auto& service = injector::get<T&, Service>();
+
+ service.send(cb);
+ }
+
+ template<class Rep, class Period>
+ void send_and_wait(std::function<void(T)> cb,
+ const std::chrono::duration<Rep, Period>& rel_time)
+ {
+ auto& service = injector::get<T&, Service>();
+
+ service.send_and_wait(cb, rel_time);
+ }
+
+ void send_and_wait(std::function<void(T)> cb)
+ {
+ using namespace std::literals::chrono_literals;
+
+ this->send_and_wait(cb, 48h);
+ }
+};
+
+} // namespace isc
+
+#endif
diff --git a/src/base/itertools.hh b/src/base/itertools.hh
new file mode 100644
index 0000000..058ceb8
--- /dev/null
+++ b/src/base/itertools.hh
@@ -0,0 +1,785 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_itertools_hh
+#define lnav_itertools_hh
+
+#include <algorithm>
+#include <memory>
+#include <set>
+#include <type_traits>
+#include <vector>
+
+#include "func_util.hh"
+#include "optional.hpp"
+
+namespace lnav {
+namespace itertools {
+
+struct empty {};
+
+struct not_empty {};
+
+struct full {
+ size_t f_max_size;
+};
+
+namespace details {
+
+template<typename T>
+struct unwrap_or {
+ T uo_value;
+};
+
+template<typename P>
+struct find_if {
+ P fi_predicate;
+};
+
+template<typename T>
+struct find {
+ T f_value;
+};
+
+struct first {};
+
+struct second {};
+
+template<typename F>
+struct filter_in {
+ F f_func;
+};
+
+template<typename F>
+struct filter_out {
+ F f_func;
+};
+
+template<typename C>
+struct sort_by {
+ C sb_cmp;
+};
+
+struct sorted {};
+
+template<typename F>
+struct mapper {
+ F m_func;
+};
+
+template<typename F>
+struct flat_mapper {
+ F fm_func;
+};
+
+template<typename F>
+struct for_eacher {
+ F fe_func;
+};
+
+template<typename R, typename T>
+struct folder {
+ R f_func;
+ T f_init;
+};
+
+template<typename T>
+struct prepend {
+ T p_value;
+};
+
+template<typename T>
+struct append {
+ T p_value;
+};
+
+struct nth {
+ nonstd::optional<size_t> a_index;
+};
+
+struct skip {
+ size_t a_count;
+};
+
+struct unique {};
+
+struct max_value {};
+
+template<typename T>
+struct max_with_init {
+ T m_init;
+};
+
+struct sum {};
+
+} // namespace details
+
+template<typename T>
+inline details::unwrap_or<T>
+unwrap_or(T value)
+{
+ return details::unwrap_or<T>{
+ value,
+ };
+}
+
+template<typename P>
+inline details::find_if<P>
+find_if(P predicate)
+{
+ return details::find_if<P>{
+ predicate,
+ };
+}
+
+template<typename T>
+inline details::find<T>
+find(T value)
+{
+ return details::find<T>{
+ value,
+ };
+}
+
+inline details::first
+first()
+{
+ return details::first{};
+}
+
+inline details::second
+second()
+{
+ return details::second{};
+}
+
+inline details::nth
+nth(nonstd::optional<size_t> index)
+{
+ return details::nth{
+ index,
+ };
+}
+
+inline details::skip
+skip(size_t count)
+{
+ return details::skip{
+ count,
+ };
+}
+
+template<typename F>
+inline details::filter_in<F>
+filter_in(F func)
+{
+ return details::filter_in<F>{
+ func,
+ };
+}
+
+template<typename F>
+inline details::filter_out<F>
+filter_out(F func)
+{
+ return details::filter_out<F>{
+ func,
+ };
+}
+
+template<typename T>
+inline details::prepend<T>
+prepend(T value)
+{
+ return details::prepend<T>{
+ std::move(value),
+ };
+}
+
+template<typename T>
+inline details::append<T>
+append(T value)
+{
+ return details::append<T>{
+ std::move(value),
+ };
+}
+
+template<typename C>
+inline details::sort_by<C>
+sort_with(C cmp)
+{
+ return details::sort_by<C>{cmp};
+}
+
+template<typename C, typename T>
+inline auto
+sort_by(T C::*m)
+{
+ return sort_with(
+ [m](const C& lhs, const C& rhs) { return lhs.*m < rhs.*m; });
+}
+
+template<typename F>
+inline details::mapper<F>
+map(F func)
+{
+ return details::mapper<F>{func};
+}
+
+template<typename F>
+inline details::flat_mapper<F>
+flat_map(F func)
+{
+ return details::flat_mapper<F>{func};
+}
+
+template<typename F>
+inline details::for_eacher<F>
+for_each(F func)
+{
+ return details::for_eacher<F>{func};
+}
+
+inline auto
+deref()
+{
+ return map([](auto iter) { return *iter; });
+}
+
+template<typename R, typename T>
+inline details::folder<R, T>
+fold(R func, T init)
+{
+ return details::folder<R, T>{func, init};
+}
+
+inline details::unique
+unique()
+{
+ return details::unique{};
+}
+
+inline details::sorted
+sorted()
+{
+ return details::sorted{};
+}
+
+template<typename T, typename... Args>
+T
+chain(const T& value1, const Args&... args)
+{
+ T retval;
+
+ for (const auto& arg : {value1, args...}) {
+ for (const auto& elem : arg) {
+ retval.emplace_back(elem);
+ }
+ }
+
+ return retval;
+}
+
+inline details::max_value
+max()
+{
+ return details::max_value{};
+}
+
+template<typename T>
+inline details::max_with_init<T>
+max(T init)
+{
+ return details::max_with_init<T>{init};
+}
+
+inline details::sum
+sum()
+{
+ return details::sum{};
+}
+
+} // namespace itertools
+} // namespace lnav
+
+template<typename C, typename P>
+nonstd::optional<std::conditional_t<
+ std::is_const<typename std::remove_reference_t<C>>::value,
+ typename std::remove_reference_t<C>::const_iterator,
+ typename std::remove_reference_t<C>::iterator>>
+operator|(C&& in, const lnav::itertools::details::find_if<P>& finder)
+{
+ for (auto iter = in.begin(); iter != in.end(); ++iter) {
+ if (lnav::func::invoke(finder.fi_predicate, *iter)) {
+ return nonstd::make_optional(iter);
+ }
+ }
+
+ return nonstd::nullopt;
+}
+
+template<typename C, typename T>
+nonstd::optional<size_t>
+operator|(const C& in, const lnav::itertools::details::find<T>& finder)
+{
+ size_t retval = 0;
+ for (const auto& elem : in) {
+ if (elem == finder.f_value) {
+ return nonstd::make_optional(retval);
+ }
+ retval += 1;
+ }
+
+ return nonstd::nullopt;
+}
+
+template<typename C>
+nonstd::optional<typename C::const_iterator>
+operator|(const C& in, const lnav::itertools::details::nth indexer)
+{
+ if (!indexer.a_index.has_value()) {
+ return nonstd::nullopt;
+ }
+
+ if (indexer.a_index.value() < in.size()) {
+ auto iter = in.begin();
+
+ std::advance(iter, indexer.a_index.value());
+ return nonstd::make_optional(iter);
+ }
+
+ return nonstd::nullopt;
+}
+
+template<typename C>
+std::vector<typename C::key_type>
+operator|(const C& in, const lnav::itertools::details::first indexer)
+{
+ std::vector<typename C::key_type> retval;
+
+ for (const auto& pair : in) {
+ retval.emplace_back(pair.first);
+ }
+
+ return retval;
+}
+
+template<typename C>
+nonstd::optional<typename C::value_type>
+operator|(const C& in, const lnav::itertools::details::max_value maxer)
+{
+ nonstd::optional<typename C::value_type> retval;
+
+ for (const auto& elem : in) {
+ if (!retval) {
+ retval = elem;
+ continue;
+ }
+
+ if (elem > retval.value()) {
+ retval = elem;
+ }
+ }
+
+ return retval;
+}
+
+template<typename C, typename T>
+typename C::value_type
+operator|(const C& in, const lnav::itertools::details::max_with_init<T> maxer)
+{
+ typename C::value_type retval = (typename C::value_type) maxer.m_init;
+
+ for (const auto& elem : in) {
+ if (elem > retval) {
+ retval = elem;
+ }
+ }
+
+ return retval;
+}
+
+template<typename C>
+typename C::value_type
+operator|(const C& in, const lnav::itertools::details::sum summer)
+{
+ typename C::value_type retval{0};
+
+ for (const auto& elem : in) {
+ retval += elem;
+ }
+
+ return retval;
+}
+
+template<typename C>
+C
+operator|(const C& in, const lnav::itertools::details::skip& skipper)
+{
+ C retval;
+
+ if (skipper.a_count < in.size()) {
+ auto iter = in.begin();
+ std::advance(iter, skipper.a_count);
+ for (; iter != in.end(); ++iter) {
+ retval.emplace_back(*iter);
+ }
+ }
+
+ return retval;
+}
+
+template<typename T, typename F>
+std::vector<T*>
+operator|(const std::vector<std::unique_ptr<T>>& in,
+ const lnav::itertools::details::filter_in<F>& filterer)
+{
+ std::vector<T*> retval;
+
+ for (const auto& elem : in) {
+ if (lnav::func::invoke(filterer.f_func, elem)) {
+ retval.emplace_back(elem.get());
+ }
+ }
+
+ return retval;
+}
+
+template<typename C, typename F>
+C
+operator|(const C& in, const lnav::itertools::details::filter_in<F>& filterer)
+{
+ C retval;
+
+ for (const auto& elem : in) {
+ if (lnav::func::invoke(filterer.f_func, elem)) {
+ retval.emplace_back(elem);
+ }
+ }
+
+ return retval;
+}
+
+template<typename C, typename F>
+C
+operator|(const C& in, const lnav::itertools::details::filter_out<F>& filterer)
+{
+ C retval;
+
+ for (const auto& elem : in) {
+ if (!lnav::func::invoke(filterer.f_func, elem)) {
+ retval.emplace_back(elem);
+ }
+ }
+
+ return retval;
+}
+
+template<typename C, typename T>
+C
+operator|(C in, const lnav::itertools::details::prepend<T>& prepender)
+{
+ in.emplace(in.begin(), prepender.p_value);
+
+ return in;
+}
+
+template<typename C, typename T>
+C
+operator|(C in, const lnav::itertools::details::append<T>& appender)
+{
+ in.emplace_back(appender.p_value);
+
+ return in;
+}
+
+template<typename C, typename R, typename T>
+T
+operator|(const C& in, const lnav::itertools::details::folder<R, T>& folder)
+{
+ auto accum = folder.f_init;
+
+ for (const auto& elem : in) {
+ accum = folder.f_func(elem, accum);
+ }
+
+ return accum;
+}
+
+template<typename C>
+std::set<typename C::value_type>
+operator|(C&& in, const lnav::itertools::details::unique& sorter)
+{
+ return {in.begin(), in.end()};
+}
+
+template<typename T, typename C>
+T
+operator|(T in, const lnav::itertools::details::sort_by<C>& sorter)
+{
+ std::sort(in.begin(), in.end(), sorter.sb_cmp);
+
+ return in;
+}
+
+template<typename T>
+T
+operator|(T in, const lnav::itertools::details::sorted& sorter)
+{
+ std::sort(in.begin(), in.end());
+
+ return in;
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::flat_mapper<F>& mapper) ->
+ typename std::remove_const_t<typename std::remove_reference_t<
+ decltype(lnav::func::invoke(mapper.fm_func, in.value()))>>
+{
+ if (!in) {
+ return nonstd::nullopt;
+ }
+
+ return lnav::func::invoke(mapper.fm_func, in.value());
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<lnav::func::is_invocable<F, T>::value, int> = 0>
+void
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::for_eacher<F>& eacher)
+{
+ if (!in) {
+ return;
+ }
+
+ lnav::func::invoke(eacher.fe_func, in.value());
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<lnav::func::is_invocable<F, T>::value, int> = 0>
+void
+operator|(std::vector<std::shared_ptr<T>>& in,
+ const lnav::itertools::details::for_eacher<F>& eacher)
+{
+ for (auto& elem : in) {
+ lnav::func::invoke(eacher.fe_func, *elem);
+ }
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> nonstd::optional<
+ typename std::remove_const_t<typename std::remove_reference_t<
+ decltype(lnav::func::invoke(mapper.m_func, in.value()))>>>
+{
+ if (!in) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional(lnav::func::invoke(mapper.m_func, in.value()));
+}
+
+template<typename T, typename F>
+auto
+operator|(const T& in, const lnav::itertools::details::mapper<F>& mapper)
+ -> std::vector<std::remove_const_t<std::remove_reference_t<
+ decltype(mapper.m_func(std::declval<typename T::value_type>()))>>>
+{
+ using return_type = std::vector<std::remove_const_t<std::remove_reference_t<
+ decltype(mapper.m_func(std::declval<typename T::value_type>()))>>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ std::transform(
+ in.begin(), in.end(), std::back_inserter(retval), mapper.m_func);
+
+ return retval;
+}
+
+template<typename T, typename F>
+auto
+operator|(const T& in, const lnav::itertools::details::mapper<F>& mapper)
+ -> std::vector<
+ std::remove_const_t<decltype(((*in.begin()).*mapper.m_func)())>>
+{
+ using return_type = std::vector<
+ std::remove_const_t<decltype(((*in.begin()).*mapper.m_func)())>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ for (const auto& elem : in) {
+ retval.template emplace_back((elem.*mapper.m_func)());
+ }
+
+ return retval;
+}
+
+template<typename T, typename F>
+auto
+operator|(const std::vector<T>& in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> std::vector<typename std::remove_const_t<std::remove_reference_t<
+ decltype((*(std::declval<T>()).*mapper.m_func)())>>>
+{
+ using return_type
+ = std::vector<typename std::remove_const_t<std::remove_reference_t<
+ decltype((*(std::declval<T>()).*mapper.m_func)())>>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ std::transform(
+ in.begin(),
+ in.end(),
+ std::back_inserter(retval),
+ [&mapper](const auto& elem) { return ((*elem).*mapper.m_func)(); });
+
+ return retval;
+}
+
+template<typename T, typename F>
+auto
+operator|(const std::set<T>& in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> std::vector<typename std::remove_const_t<std::remove_reference_t<
+ decltype((*(std::declval<T>()).*mapper.m_func)())>>>
+{
+ using return_type
+ = std::vector<typename std::remove_const_t<std::remove_reference_t<
+ decltype((*(std::declval<T>()).*mapper.m_func)())>>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ std::transform(
+ in.begin(),
+ in.end(),
+ std::back_inserter(retval),
+ [&mapper](const auto& elem) { return ((*elem).*mapper.m_func)(); });
+
+ return retval;
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<!lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(const std::vector<std::shared_ptr<T>>& in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> std::vector<typename std::remove_reference_t<
+ typename std::remove_const_t<decltype(((*in.front()).*mapper.m_func))>>>
+{
+ using return_type = std::vector<
+ typename std::remove_const_t<typename std::remove_reference_t<decltype((
+ (*in.front()).*mapper.m_func))>>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ for (const auto& elem : in) {
+ retval.template emplace_back(((*elem).*mapper.m_func));
+ }
+
+ return retval;
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<!lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(const std::vector<T>& in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> std::vector<
+ typename std::remove_const_t<typename std::remove_reference_t<
+ decltype(((in.front()).*mapper.m_func))>>>
+{
+ using return_type = std::vector<
+ typename std::remove_const_t<typename std::remove_reference_t<decltype((
+ (in.front()).*mapper.m_func))>>>;
+ return_type retval;
+
+ retval.reserve(in.size());
+ for (const auto& elem : in) {
+ retval.template emplace_back(elem.*mapper.m_func);
+ }
+
+ return retval;
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<!lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> nonstd::optional<typename std::remove_reference_t<
+ typename std::remove_const_t<decltype(((in.value()).*mapper.m_func))>>>
+{
+ if (!in) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional((in.value()).*mapper.m_func);
+}
+
+template<typename T,
+ typename F,
+ std::enable_if_t<!lnav::func::is_invocable<F, T>::value, int> = 0>
+auto
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::mapper<F>& mapper)
+ -> nonstd::optional<
+ typename std::remove_const_t<typename std::remove_reference_t<
+ decltype(((*in.value()).*mapper.m_func))>>>
+{
+ if (!in) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional((*in.value()).*mapper.m_func);
+}
+
+template<typename T>
+T
+operator|(nonstd::optional<T> in,
+ const lnav::itertools::details::unwrap_or<T>& unwrapper)
+{
+ return in.value_or(unwrapper.uo_value);
+}
+
+#endif
diff --git a/src/base/lnav.console.cc b/src/base/lnav.console.cc
new file mode 100644
index 0000000..a34ebac
--- /dev/null
+++ b/src/base/lnav.console.cc
@@ -0,0 +1,494 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+
+#include "lnav.console.hh"
+
+#include "config.h"
+#include "fmt/color.h"
+#include "itertools.hh"
+#include "lnav.console.into.hh"
+#include "log_level_enum.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "snippet_highlighters.hh"
+#include "view_curses.hh"
+
+using namespace lnav::roles::literals;
+
+namespace lnav {
+namespace console {
+
+user_message
+user_message::raw(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::raw;
+ retval.um_message.append(al);
+ return retval;
+}
+
+user_message
+user_message::error(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::error;
+ retval.um_message.append(al);
+ return retval;
+}
+
+user_message
+user_message::info(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::info;
+ retval.um_message.append(al);
+ return retval;
+}
+
+user_message
+user_message::ok(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::ok;
+ retval.um_message.append(al);
+ return retval;
+}
+
+user_message
+user_message::warning(const attr_line_t& al)
+{
+ user_message retval;
+
+ retval.um_level = level::warning;
+ retval.um_message.append(al);
+ return retval;
+}
+
+attr_line_t
+user_message::to_attr_line(std::set<render_flags> flags) const
+{
+ auto indent = 1;
+ attr_line_t retval;
+
+ if (this->um_level == level::warning) {
+ indent = 3;
+ }
+
+ if (flags.count(render_flags::prefix)) {
+ switch (this->um_level) {
+ case level::raw:
+ break;
+ case level::ok:
+ retval.append(lnav::roles::ok("\u2714 "));
+ break;
+ case level::info:
+ retval.append("\u24d8 info"_info).append(": ");
+ break;
+ case level::warning:
+ retval.append(lnav::roles::warning("\u26a0 warning"))
+ .append(": ");
+ break;
+ case level::error:
+ retval.append(lnav::roles::error("\u2718 error")).append(": ");
+ break;
+ }
+ }
+
+ retval.append(this->um_message).append("\n");
+ if (!this->um_reason.empty()) {
+ bool first_line = true;
+ for (const auto& line : this->um_reason.split_lines()) {
+ auto role = this->um_level == level::error ? role_t::VCR_ERROR
+ : role_t::VCR_WARNING;
+ attr_line_t prefix;
+
+ if (first_line) {
+ prefix.append(indent, ' ')
+ .append("reason", VC_ROLE.value(role))
+ .append(": ");
+ first_line = false;
+ } else {
+ prefix.append(" | ", VC_ROLE.value(role))
+ .append(indent, ' ');
+ }
+ retval.append(prefix).append(line).append("\n");
+ }
+ }
+ if (!this->um_snippets.empty()) {
+ for (const auto& snip : this->um_snippets) {
+ attr_line_t header;
+
+ header.append(" --> "_snippet_border)
+ .append(lnav::roles::file(snip.s_location.sl_source.get()));
+ if (snip.s_location.sl_line_number > 0) {
+ header.append(":").appendf(FMT_STRING("{}"),
+ snip.s_location.sl_line_number);
+ }
+ retval.append(header).append("\n");
+ if (!snip.s_content.blank()) {
+ auto snippet_lines = snip.s_content.split_lines();
+ auto longest_line_length = snippet_lines
+ | lnav::itertools::map(&attr_line_t::utf8_length_or_length)
+ | lnav::itertools::max(40);
+
+ for (auto& line : snippet_lines) {
+ line.pad_to(longest_line_length);
+ retval.append(" | "_snippet_border)
+ .append(line)
+ .append("\n");
+ }
+ }
+ }
+ }
+ if (!this->um_notes.empty()) {
+ for (const auto& note : this->um_notes) {
+ bool first_line = true;
+ for (const auto& line : note.split_lines()) {
+ attr_line_t prefix;
+
+ if (first_line) {
+ prefix.append(" ="_snippet_border)
+ .append(indent, ' ')
+ .append("note"_snippet_border)
+ .append(": ");
+ first_line = false;
+ } else {
+ prefix.append(" ").append(indent, ' ');
+ }
+
+ retval.append(prefix).append(line).append("\n");
+ }
+ }
+ }
+ if (!this->um_help.empty()) {
+ bool first_line = true;
+ for (const auto& line : this->um_help.split_lines()) {
+ attr_line_t prefix;
+
+ if (first_line) {
+ prefix.append(" ="_snippet_border)
+ .append(indent, ' ')
+ .append("help"_snippet_border)
+ .append(": ");
+ first_line = false;
+ } else {
+ prefix.append(" ");
+ }
+
+ retval.append(prefix).append(line).append("\n");
+ }
+ }
+
+ return retval;
+}
+
+static nonstd::optional<fmt::terminal_color>
+curses_color_to_terminal_color(int curses_color)
+{
+ switch (curses_color) {
+ case COLOR_BLACK:
+ return fmt::terminal_color::black;
+ case COLOR_CYAN:
+ return fmt::terminal_color::cyan;
+ case COLOR_WHITE:
+ return fmt::terminal_color::white;
+ case COLOR_MAGENTA:
+ return fmt::terminal_color::magenta;
+ case COLOR_BLUE:
+ return fmt::terminal_color::blue;
+ case COLOR_YELLOW:
+ return fmt::terminal_color::yellow;
+ case COLOR_GREEN:
+ return fmt::terminal_color::green;
+ case COLOR_RED:
+ return fmt::terminal_color::red;
+ default:
+ return nonstd::nullopt;
+ }
+}
+
+void
+println(FILE* file, const attr_line_t& al)
+{
+ const auto& str = al.get_string();
+
+ if (getenv("NO_COLOR") != nullptr
+ || (!isatty(fileno(file)) && getenv("YES_COLOR") == nullptr))
+ {
+ fmt::print(file, "{}\n", str);
+ return;
+ }
+
+ std::set<size_t> points = {0, static_cast<size_t>(al.length())};
+
+ for (const auto& attr : al.get_attrs()) {
+ if (!attr.sa_range.is_valid()) {
+ continue;
+ }
+ points.insert(attr.sa_range.lr_start);
+ if (attr.sa_range.lr_end > 0) {
+ points.insert(attr.sa_range.lr_end);
+ }
+ }
+
+ nonstd::optional<size_t> last_point;
+ for (const auto& point : points) {
+ if (!last_point) {
+ last_point = point;
+ continue;
+ }
+ auto default_fg_style = fmt::text_style{};
+ auto default_bg_style = fmt::text_style{};
+ auto line_style = fmt::text_style{};
+ auto fg_style = fmt::text_style{};
+ auto start = last_point.value();
+
+ for (const auto& attr : al.get_attrs()) {
+ if (!attr.sa_range.contains(start)
+ && !attr.sa_range.contains(point - 1))
+ {
+ continue;
+ }
+
+ try {
+ if (attr.sa_type == &VC_BACKGROUND) {
+ auto saw = string_attr_wrapper<int64_t>(&attr);
+ auto color_opt = curses_color_to_terminal_color(saw.get());
+
+ if (color_opt) {
+ line_style |= fmt::bg(color_opt.value());
+ }
+ } else if (attr.sa_type == &VC_FOREGROUND) {
+ auto saw = string_attr_wrapper<int64_t>(&attr);
+ auto color_opt = curses_color_to_terminal_color(saw.get());
+
+ if (color_opt) {
+ fg_style = fmt::fg(color_opt.value());
+ }
+ } else if (attr.sa_type == &VC_STYLE) {
+ auto saw = string_attr_wrapper<text_attrs>(&attr);
+ auto style = saw.get();
+
+ if (style.ta_attrs & A_REVERSE) {
+ line_style |= fmt::emphasis::reverse;
+ }
+ if (style.ta_attrs & A_BOLD) {
+ line_style |= fmt::emphasis::bold;
+ }
+ if (style.ta_attrs & A_UNDERLINE) {
+ line_style |= fmt::emphasis::underline;
+ }
+ if (style.ta_fg_color) {
+ auto color_opt = curses_color_to_terminal_color(
+ style.ta_fg_color.value());
+
+ if (color_opt) {
+ fg_style = fmt::fg(color_opt.value());
+ }
+ }
+ if (style.ta_bg_color) {
+ auto color_opt = curses_color_to_terminal_color(
+ style.ta_bg_color.value());
+
+ if (color_opt) {
+ line_style |= fmt::bg(color_opt.value());
+ }
+ }
+ } else if (attr.sa_type == &SA_LEVEL) {
+ auto level = static_cast<log_level_t>(
+ attr.sa_value.get<int64_t>());
+
+ switch (level) {
+ case LEVEL_FATAL:
+ case LEVEL_CRITICAL:
+ case LEVEL_ERROR:
+ line_style |= fmt::fg(fmt::terminal_color::red);
+ break;
+ case LEVEL_WARNING:
+ line_style |= fmt::fg(fmt::terminal_color::yellow);
+ break;
+ default:
+ break;
+ }
+ } else if (attr.sa_type == &VC_ROLE) {
+ auto saw = string_attr_wrapper<role_t>(&attr);
+ auto role = saw.get();
+
+ switch (role) {
+ case role_t::VCR_TEXT:
+ case role_t::VCR_IDENTIFIER:
+ break;
+ case role_t::VCR_SEARCH:
+ line_style |= fmt::emphasis::reverse;
+ break;
+ case role_t::VCR_ERROR:
+ line_style |= fmt::fg(fmt::terminal_color::red)
+ | fmt::emphasis::bold;
+ break;
+ case role_t::VCR_WARNING:
+ case role_t::VCR_RE_REPEAT:
+ line_style |= fmt::fg(fmt::terminal_color::yellow);
+ break;
+ case role_t::VCR_COMMENT:
+ line_style |= fmt::fg(fmt::terminal_color::green);
+ break;
+ case role_t::VCR_SNIPPET_BORDER:
+ line_style |= fmt::fg(fmt::terminal_color::cyan);
+ break;
+ case role_t::VCR_OK:
+ line_style |= fmt::emphasis::bold
+ | fmt::fg(fmt::terminal_color::green);
+ break;
+ case role_t::VCR_INFO:
+ case role_t::VCR_STATUS:
+ line_style |= fmt::emphasis::bold
+ | fmt::fg(fmt::terminal_color::magenta);
+ break;
+ case role_t::VCR_KEYWORD:
+ case role_t::VCR_RE_SPECIAL:
+ line_style |= fmt::emphasis::bold
+ | fmt::fg(fmt::terminal_color::cyan);
+ break;
+ case role_t::VCR_STRING:
+ line_style |= fmt::fg(fmt::terminal_color::magenta);
+ break;
+ case role_t::VCR_VARIABLE:
+ line_style |= fmt::emphasis::underline;
+ break;
+ case role_t::VCR_SYMBOL:
+ case role_t::VCR_NUMBER:
+ case role_t::VCR_FILE:
+ line_style |= fmt::emphasis::bold;
+ break;
+ case role_t::VCR_H1:
+ line_style |= fmt::emphasis::bold
+ | fmt::fg(fmt::terminal_color::magenta);
+ break;
+ case role_t::VCR_H2:
+ line_style |= fmt::emphasis::bold;
+ break;
+ case role_t::VCR_H3:
+ case role_t::VCR_H4:
+ case role_t::VCR_H5:
+ case role_t::VCR_H6:
+ line_style |= fmt::emphasis::underline;
+ break;
+ case role_t::VCR_LIST_GLYPH:
+ line_style |= fmt::fg(fmt::terminal_color::yellow);
+ break;
+ case role_t::VCR_QUOTED_CODE:
+ default_fg_style
+ = fmt::fg(fmt::terminal_color::white);
+ default_bg_style
+ = fmt::bg(fmt::terminal_color::black);
+ break;
+ case role_t::VCR_LOW_THRESHOLD:
+ line_style |= fmt::bg(fmt::terminal_color::green);
+ break;
+ case role_t::VCR_MED_THRESHOLD:
+ line_style |= fmt::bg(fmt::terminal_color::yellow);
+ break;
+ case role_t::VCR_HIGH_THRESHOLD:
+ line_style |= fmt::bg(fmt::terminal_color::red);
+ break;
+ default:
+ // log_debug("missing role handler %d", (int) role);
+ break;
+ }
+ }
+ } catch (const fmt::format_error& e) {
+ log_error("style error: %s", e.what());
+ }
+ }
+
+ if (!line_style.has_foreground() && fg_style.has_foreground()) {
+ line_style |= fg_style;
+ }
+ if (!line_style.has_foreground() && default_fg_style.has_foreground()) {
+ line_style |= default_fg_style;
+ }
+ if (!line_style.has_background() && default_bg_style.has_background()) {
+ line_style |= default_bg_style;
+ }
+
+ if (start < str.size()) {
+ auto actual_end = std::min(str.size(), static_cast<size_t>(point));
+ fmt::print(file,
+ line_style,
+ FMT_STRING("{}"),
+ str.substr(start, actual_end - start));
+ }
+ last_point = point;
+ }
+ fmt::print(file, "\n");
+}
+
+void
+print(FILE* file, const user_message& um)
+{
+ auto al = um.to_attr_line();
+
+ if (endswith(al.get_string(), "\n")) {
+ al.erase(al.length() - 1);
+ }
+ println(file, al);
+}
+
+user_message
+to_user_message(intern_string_t src, const lnav::pcre2pp::compile_error& ce)
+{
+ attr_line_t pcre_error_content{ce.ce_pattern};
+
+ lnav::snippets::regex_highlighter(pcre_error_content,
+ pcre_error_content.length(),
+ line_range{
+ 0,
+ (int) pcre_error_content.length(),
+ });
+ pcre_error_content.append("\n")
+ .append(ce.ce_offset, ' ')
+ .append(lnav::roles::error("^ "))
+ .append(lnav::roles::error(ce.get_message()))
+ .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+
+ return user_message::error(
+ attr_line_t()
+ .append_quoted(ce.ce_pattern)
+ .append(" is not a valid regular expression"))
+ .with_reason(ce.get_message())
+ .with_snippet(lnav::console::snippet::from(src, pcre_error_content));
+}
+
+} // namespace console
+} // namespace lnav
diff --git a/src/base/lnav.console.hh b/src/base/lnav.console.hh
new file mode 100644
index 0000000..ac4c2b0
--- /dev/null
+++ b/src/base/lnav.console.hh
@@ -0,0 +1,176 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_console_hh
+#define lnav_console_hh
+
+#include <set>
+#include <vector>
+
+#include "base/attr_line.hh"
+#include "base/file_range.hh"
+
+namespace lnav {
+namespace console {
+
+void println(FILE* file, const attr_line_t& al);
+
+struct snippet {
+ static snippet from(intern_string_t src, const attr_line_t& content)
+ {
+ snippet retval;
+
+ retval.s_location.sl_source = src;
+ retval.s_content = content;
+ return retval;
+ }
+
+ static snippet from(source_location loc, const attr_line_t& content)
+ {
+ snippet retval;
+
+ retval.s_location = loc;
+ retval.s_content = content;
+ return retval;
+ }
+
+ snippet& with_line(int32_t line)
+ {
+ this->s_location.sl_line_number = line;
+ return *this;
+ }
+
+ source_location s_location;
+ attr_line_t s_content;
+};
+
+struct user_message {
+ enum class level {
+ raw,
+ ok,
+ info,
+ warning,
+ error,
+ };
+
+ static user_message raw(const attr_line_t& al);
+
+ static user_message error(const attr_line_t& al);
+
+ static user_message warning(const attr_line_t& al);
+
+ static user_message info(const attr_line_t& al);
+
+ static user_message ok(const attr_line_t& al);
+
+ user_message& with_reason(const attr_line_t& al)
+ {
+ this->um_reason = al;
+ this->um_reason.rtrim();
+ return *this;
+ }
+
+ user_message& with_reason(const user_message& um)
+ {
+ return this->with_reason(um.to_attr_line({}));
+ }
+
+ user_message& with_errno_reason()
+ {
+ this->um_reason = strerror(errno);
+ return *this;
+ }
+
+ user_message& with_snippet(const snippet& sn)
+ {
+ this->um_snippets.emplace_back(sn);
+ return *this;
+ }
+
+ template<typename C>
+ user_message& with_snippets(C snippets)
+ {
+ this->um_snippets.insert(this->um_snippets.end(),
+ std::make_move_iterator(std::begin(snippets)),
+ std::make_move_iterator(std::end(snippets)));
+ if (this->um_snippets.size() > 1) {
+ for (auto iter = this->um_snippets.begin();
+ iter != this->um_snippets.end();) {
+ if (iter->s_content.empty()) {
+ iter = this->um_snippets.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+ }
+ return *this;
+ }
+
+ user_message& with_note(const attr_line_t& al)
+ {
+ if (!al.blank()) {
+ this->um_notes.emplace_back(al);
+ }
+
+ return *this;
+ }
+
+ user_message& with_help(const attr_line_t& al)
+ {
+ if (al.blank()) {
+ this->um_help.clear();
+ } else {
+ this->um_help = al;
+ this->um_help.rtrim();
+ }
+
+ return *this;
+ }
+
+ enum class render_flags {
+ prefix,
+ };
+
+ attr_line_t to_attr_line(std::set<render_flags> flags
+ = {render_flags::prefix}) const;
+
+ level um_level{level::ok};
+ attr_line_t um_message;
+ std::vector<snippet> um_snippets;
+ attr_line_t um_reason;
+ std::vector<attr_line_t> um_notes;
+ attr_line_t um_help;
+};
+
+void print(FILE* file, const user_message& um);
+
+} // namespace console
+} // namespace lnav
+
+#endif
diff --git a/src/base/lnav.console.into.hh b/src/base/lnav.console.into.hh
new file mode 100644
index 0000000..206d563
--- /dev/null
+++ b/src/base/lnav.console.into.hh
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_console_into_hh
+#define lnav_console_into_hh
+
+#include "intern_string.hh"
+#include "lnav.console.hh"
+
+namespace lnav {
+namespace pcre2pp {
+
+struct compile_error;
+
+}
+
+namespace console {
+
+user_message to_user_message(intern_string_t src,
+ const pcre2pp::compile_error& ce);
+
+}
+} // namespace lnav
+
+#endif
diff --git a/src/base/lnav.gzip.cc b/src/base/lnav.gzip.cc
new file mode 100644
index 0000000..6b31dad
--- /dev/null
+++ b/src/base/lnav.gzip.cc
@@ -0,0 +1,135 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav.gzip.cc
+ */
+
+#include "lnav.gzip.hh"
+
+#include <zlib.h>
+
+#include "config.h"
+#include "fmt/format.h"
+
+namespace lnav {
+namespace gzip {
+
+bool
+is_gzipped(const char* buffer, size_t len)
+{
+ return len > 2 && buffer[0] == '\037' && buffer[1] == '\213';
+}
+
+Result<auto_buffer, std::string>
+compress(const void* input, size_t len)
+{
+ auto retval = auto_buffer::alloc(len + 4096);
+
+ z_stream zs;
+ zs.zalloc = Z_NULL;
+ zs.zfree = Z_NULL;
+ zs.opaque = Z_NULL;
+ zs.avail_in = (uInt) len;
+ zs.next_in = (Bytef*) input;
+ zs.avail_out = (uInt) retval.capacity();
+ zs.next_out = (Bytef*) retval.in();
+ zs.total_out = 0;
+
+ auto rc = deflateInit2(
+ &zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY);
+ if (rc != Z_OK) {
+ return Err(fmt::format(
+ FMT_STRING("unable to initialize compressor -- {}"), zError(rc)));
+ }
+ rc = deflate(&zs, Z_FINISH);
+ if (rc != Z_STREAM_END) {
+ return Err(fmt::format(FMT_STRING("unable to compress data -- {}"),
+ zError(rc)));
+ }
+ rc = deflateEnd(&zs);
+ if (rc != Z_OK) {
+ return Err(fmt::format(
+ FMT_STRING("unable to finalize compression -- {}"), zError(rc)));
+ }
+ return Ok(std::move(retval.resize(zs.total_out)));
+}
+
+Result<auto_buffer, std::string>
+uncompress(const std::string& src, const void* buffer, size_t size)
+{
+ auto uncomp = auto_buffer::alloc(size * 2);
+ z_stream strm;
+ int err;
+
+ strm.next_in = (Bytef*) buffer;
+ strm.msg = Z_NULL;
+ strm.avail_in = size;
+ strm.total_in = 0;
+ strm.total_out = 0;
+ strm.zalloc = Z_NULL;
+ strm.zfree = Z_NULL;
+
+ if ((err = inflateInit2(&strm, (16 + MAX_WBITS))) != Z_OK) {
+ return Err(fmt::format(FMT_STRING("invalid gzip data: {} -- {}"),
+ src,
+ strm.msg ? strm.msg : zError(err)));
+ }
+
+ bool done = false;
+
+ while (!done) {
+ if (strm.total_out >= uncomp.size()) {
+ uncomp.expand_by(size / 2);
+ }
+
+ strm.next_out = (Bytef*) (uncomp.in() + strm.total_out);
+ strm.avail_out = uncomp.capacity() - strm.total_out;
+
+ // Inflate another chunk.
+ err = inflate(&strm, Z_SYNC_FLUSH);
+ if (err == Z_STREAM_END) {
+ done = true;
+ } else if (err != Z_OK) {
+ inflateEnd(&strm);
+ return Err(fmt::format(FMT_STRING("unable to uncompress: {} -- {}"),
+ src,
+ strm.msg ? strm.msg : zError(err)));
+ }
+ }
+
+ if (inflateEnd(&strm) != Z_OK) {
+ return Err(fmt::format(FMT_STRING("unable to uncompress: {} -- {}"),
+ src,
+ strm.msg ? strm.msg : zError(err)));
+ }
+
+ return Ok(std::move(uncomp.resize(strm.total_out)));
+}
+
+} // namespace gzip
+} // namespace lnav
diff --git a/src/base/lnav.gzip.hh b/src/base/lnav.gzip.hh
new file mode 100644
index 0000000..bd73965
--- /dev/null
+++ b/src/base/lnav.gzip.hh
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav.gzip.hh
+ */
+
+#ifndef lnav_gzip_hh
+#define lnav_gzip_hh
+
+#include <string>
+
+#include "auto_mem.hh"
+#include "result.h"
+
+namespace lnav {
+namespace gzip {
+
+bool is_gzipped(const char* buffer, size_t len);
+
+Result<auto_buffer, std::string> compress(const void* input, size_t len);
+
+Result<auto_buffer, std::string> uncompress(const std::string& src,
+ const void* buffer,
+ size_t size);
+
+} // namespace gzip
+} // namespace lnav
+
+#endif
diff --git a/src/base/lnav.gzip.tests.cc b/src/base/lnav.gzip.tests.cc
new file mode 100644
index 0000000..2f6939e
--- /dev/null
+++ b/src/base/lnav.gzip.tests.cc
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <iostream>
+
+#include <zlib.h>
+
+#include "base/lnav.gzip.hh"
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("lnav::gzip::uncompress")
+{
+ {
+ auto u_res = lnav::gzip::uncompress("empty", nullptr, 0);
+
+ CHECK(u_res.isErr());
+ CHECK(u_res.unwrapErr()
+ == "unable to uncompress: empty -- stream error");
+ }
+
+ {
+ auto u_res = lnav::gzip::uncompress("garbage", "abc", 3);
+
+ CHECK(u_res.isErr());
+ CHECK(u_res.unwrapErr()
+ == "unable to uncompress: garbage -- incorrect header check");
+ }
+}
+
+TEST_CASE("lnav::gzip::roundtrip")
+{
+ const char msg[] = "Hello, World!";
+
+ auto c_res = lnav::gzip::compress(msg, sizeof(msg));
+ auto buf = c_res.unwrap();
+ auto u_res = lnav::gzip::uncompress("test", buf.in(), buf.size());
+ auto buf2 = u_res.unwrap();
+
+ CHECK(std::string(msg) == std::string(buf2.in()));
+}
diff --git a/src/base/lnav_log.cc b/src/base/lnav_log.cc
new file mode 100644
index 0000000..2e5dbba
--- /dev/null
+++ b/src/base/lnav_log.cc
@@ -0,0 +1,686 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav_log.cc
+ */
+
+#include <assert.h>
+#include <ctype.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/resource.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#ifdef HAVE_EXECINFO_H
+# include <execinfo.h>
+#endif
+#if BACKWARD_HAS_DW == 1
+# include "backward-cpp/backward.hpp"
+#endif
+
+#include <algorithm>
+#include <mutex>
+#include <thread>
+#include <vector>
+
+#define PCRE2_CODE_UNIT_WIDTH 8
+#include <pcre2.h>
+#include <sys/param.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/utsname.h>
+#include <sys/wait.h>
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/curses.h>
+# include <ncursesw/termcap.h>
+#elif defined HAVE_NCURSESW_H
+# include <ncursesw.h>
+# include <termcap.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/curses.h>
+# include <ncurses/termcap.h>
+#elif defined HAVE_NCURSES_H
+# include <ncurses.h>
+# include <termcap.h>
+#elif defined HAVE_CURSES_H
+# include <curses.h>
+# include <termcap.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+
+#include "auto_mem.hh"
+#include "enum_util.hh"
+#include "lnav_log.hh"
+#include "opt_util.hh"
+
+static const size_t BUFFER_SIZE = 256 * 1024;
+static const size_t MAX_LOG_LINE_SIZE = 2 * 1024;
+
+static const char* CRASH_MSG
+ = "\n"
+ "\n"
+ "==== GURU MEDITATION ====\n"
+ "Unfortunately, lnav has crashed, sorry for the inconvenience.\n"
+ "\n"
+ "You can help improve lnav by sending the following file "
+ "to " PACKAGE_BUGREPORT
+ " :\n"
+ " %s\n"
+ "=========================\n";
+
+nonstd::optional<FILE*> lnav_log_file;
+lnav_log_level_t lnav_log_level = lnav_log_level_t::DEBUG;
+const char* lnav_log_crash_dir;
+nonstd::optional<const struct termios*> lnav_log_orig_termios;
+// NOTE: This mutex is leaked so that it is not destroyed during exit.
+// Otherwise, any attempts to log will fail.
+static std::mutex*
+lnav_log_mutex()
+{
+ static auto* retval = new std::mutex();
+
+ return retval;
+}
+
+static std::vector<log_state_dumper*>&
+DUMPER_LIST()
+{
+ static auto* retval = new std::vector<log_state_dumper*>();
+
+ return *retval;
+}
+static std::vector<log_crash_recoverer*> CRASH_LIST;
+
+struct thid {
+ static uint32_t COUNTER;
+
+ thid() noexcept : t_id(COUNTER++) {}
+
+ uint32_t t_id;
+};
+
+uint32_t thid::COUNTER = 0;
+
+thread_local thid current_thid;
+thread_local std::string thread_log_prefix;
+
+static struct {
+ size_t lr_length;
+ off_t lr_frag_start;
+ off_t lr_frag_end;
+ char lr_data[BUFFER_SIZE];
+} log_ring = {0, BUFFER_SIZE, 0, {}};
+
+static const char* LEVEL_NAMES[] = {
+ "T",
+ "D",
+ "I",
+ "W",
+ "E",
+};
+
+static char*
+log_alloc()
+{
+ off_t data_end = log_ring.lr_length + MAX_LOG_LINE_SIZE;
+
+ if (data_end >= (off_t) BUFFER_SIZE) {
+ const char* new_start = &log_ring.lr_data[MAX_LOG_LINE_SIZE];
+
+ new_start = (const char*) memchr(
+ new_start, '\n', log_ring.lr_length - MAX_LOG_LINE_SIZE);
+ log_ring.lr_frag_start = new_start - log_ring.lr_data;
+ log_ring.lr_frag_end = log_ring.lr_length;
+ log_ring.lr_length = 0;
+
+ assert(log_ring.lr_frag_start >= 0);
+ assert(log_ring.lr_frag_start <= (off_t) BUFFER_SIZE);
+ } else if (data_end >= log_ring.lr_frag_start) {
+ const char* new_start = &log_ring.lr_data[log_ring.lr_frag_start];
+
+ new_start = (const char*) memchr(
+ new_start, '\n', log_ring.lr_frag_end - log_ring.lr_frag_start);
+ assert(new_start != nullptr);
+ log_ring.lr_frag_start = new_start - log_ring.lr_data;
+ assert(log_ring.lr_frag_start >= 0);
+ assert(log_ring.lr_frag_start <= (off_t) BUFFER_SIZE);
+ }
+
+ return &log_ring.lr_data[log_ring.lr_length];
+}
+
+void
+log_argv(int argc, char* argv[])
+{
+ const char* log_path = getenv("LNAV_LOG_PATH");
+
+ if (log_path != nullptr) {
+ lnav_log_file = make_optional_from_nullable(fopen(log_path, "a"));
+ }
+
+ log_info("argv[%d] =", argc);
+ for (int lpc = 0; lpc < argc; lpc++) {
+ log_info(" [%d] = %s", lpc, argv[lpc]);
+ }
+}
+
+void
+log_set_thread_prefix(std::string prefix)
+{
+ // thread_log_prefix = std::move(prefix);
+}
+
+void
+log_host_info()
+{
+ char cwd[MAXPATHLEN];
+ char jittarget[128];
+ struct utsname un;
+ struct rusage ru;
+ uint32_t pcre_jit;
+
+ uname(&un);
+ pcre2_config(PCRE2_CONFIG_JIT, &pcre_jit);
+ pcre2_config(PCRE2_CONFIG_JITTARGET, jittarget);
+
+ log_info("uname:");
+ log_info(" sysname=%s", un.sysname);
+ log_info(" nodename=%s", un.nodename);
+ log_info(" machine=%s", un.machine);
+ log_info(" release=%s", un.release);
+ log_info(" version=%s", un.version);
+ log_info("PCRE:");
+ log_info(" jit=%d", pcre_jit);
+ log_info(" jittarget=%s", jittarget);
+ log_info("Environment:");
+ log_info(" HOME=%s", getenv("HOME"));
+ log_info(" XDG_CONFIG_HOME=%s", getenv("XDG_CONFIG_HOME"));
+ log_info(" LANG=%s", getenv("LANG"));
+ log_info(" PATH=%s", getenv("PATH"));
+ log_info(" TERM=%s", getenv("TERM"));
+ log_info(" TZ=%s", getenv("TZ"));
+ log_info("Process:");
+ log_info(" pid=%d", getpid());
+ log_info(" ppid=%d", getppid());
+ log_info(" pgrp=%d", getpgrp());
+ log_info(" uid=%d", getuid());
+ log_info(" gid=%d", getgid());
+ log_info(" euid=%d", geteuid());
+ log_info(" egid=%d", getegid());
+ if (getcwd(cwd, sizeof(cwd)) == nullptr) {
+ log_info(" ERROR: getcwd failed");
+ } else {
+ log_info(" cwd=%s", cwd);
+ }
+ log_info("Executable:");
+ log_info(" version=%s", VCS_PACKAGE_STRING);
+
+ getrusage(RUSAGE_SELF, &ru);
+ log_rusage(lnav_log_level_t::INFO, ru);
+}
+
+void
+log_rusage_raw(enum lnav_log_level_t level,
+ const char* src_file,
+ int line_number,
+ const struct rusage& ru)
+{
+ log_msg(level, src_file, line_number, "rusage:");
+ log_msg(level,
+ src_file,
+ line_number,
+ " utime=%d.%06d",
+ ru.ru_utime.tv_sec,
+ ru.ru_utime.tv_usec);
+ log_msg(level,
+ src_file,
+ line_number,
+ " stime=%d.%06d",
+ ru.ru_stime.tv_sec,
+ ru.ru_stime.tv_usec);
+ log_msg(level, src_file, line_number, " maxrss=%ld", ru.ru_maxrss);
+ log_msg(level, src_file, line_number, " ixrss=%ld", ru.ru_ixrss);
+ log_msg(level, src_file, line_number, " idrss=%ld", ru.ru_idrss);
+ log_msg(level, src_file, line_number, " isrss=%ld", ru.ru_isrss);
+ log_msg(level, src_file, line_number, " minflt=%ld", ru.ru_minflt);
+ log_msg(level, src_file, line_number, " majflt=%ld", ru.ru_majflt);
+ log_msg(level, src_file, line_number, " nswap=%ld", ru.ru_nswap);
+ log_msg(level, src_file, line_number, " inblock=%ld", ru.ru_inblock);
+ log_msg(level, src_file, line_number, " oublock=%ld", ru.ru_oublock);
+ log_msg(level, src_file, line_number, " msgsnd=%ld", ru.ru_msgsnd);
+ log_msg(level, src_file, line_number, " msgrcv=%ld", ru.ru_msgrcv);
+ log_msg(level, src_file, line_number, " nsignals=%ld", ru.ru_nsignals);
+ log_msg(level, src_file, line_number, " nvcsw=%ld", ru.ru_nvcsw);
+ log_msg(level, src_file, line_number, " nivcsw=%ld", ru.ru_nivcsw);
+}
+
+void
+log_msg(lnav_log_level_t level,
+ const char* src_file,
+ int line_number,
+ const char* fmt,
+ ...)
+{
+ struct timeval curr_time;
+ struct tm localtm;
+ ssize_t prefix_size;
+ va_list args;
+ ssize_t rc;
+
+ if (level < lnav_log_level) {
+ return;
+ }
+
+ std::lock_guard<std::mutex> log_lock(*lnav_log_mutex());
+
+ {
+ // get the base name of the file. NB: can't use basename() since it
+ // can modify its argument
+ const char* last_slash = src_file;
+
+ for (int lpc = 0; src_file[lpc]; lpc++) {
+ if (src_file[lpc] == '/' || src_file[lpc] == '\\') {
+ last_slash = &src_file[lpc + 1];
+ }
+ }
+
+ src_file = last_slash;
+ }
+
+ va_start(args, fmt);
+ gettimeofday(&curr_time, nullptr);
+ localtime_r(&curr_time.tv_sec, &localtm);
+ auto line = log_alloc();
+ prefix_size = snprintf(line,
+ MAX_LOG_LINE_SIZE,
+ "%4d-%02d-%02dT%02d:%02d:%02d.%03d %s t%u %s:%d ",
+ localtm.tm_year + 1900,
+ localtm.tm_mon + 1,
+ localtm.tm_mday,
+ localtm.tm_hour,
+ localtm.tm_min,
+ localtm.tm_sec,
+ (int) (curr_time.tv_usec / 1000),
+ LEVEL_NAMES[lnav::enums::to_underlying(level)],
+ current_thid.t_id,
+ src_file,
+ line_number);
+#if 0
+ if (!thread_log_prefix.empty()) {
+ prefix_size += snprintf(
+ &line[prefix_size], MAX_LOG_LINE_SIZE - prefix_size,
+ "%s ",
+ thread_log_prefix.c_str());
+ }
+#endif
+ rc = vsnprintf(
+ &line[prefix_size], MAX_LOG_LINE_SIZE - prefix_size, fmt, args);
+ if (rc >= (ssize_t) (MAX_LOG_LINE_SIZE - prefix_size)) {
+ rc = MAX_LOG_LINE_SIZE - prefix_size - 1;
+ }
+ line[prefix_size + rc] = '\n';
+ log_ring.lr_length += prefix_size + rc + 1;
+ lnav_log_file | [&](auto file) {
+ fwrite(line, 1, prefix_size + rc + 1, file);
+ fflush(file);
+ };
+ va_end(args);
+}
+
+void
+log_msg_extra(const char* fmt, ...)
+{
+ std::lock_guard<std::mutex> mg(*lnav_log_mutex());
+ va_list args;
+
+ va_start(args, fmt);
+ auto line = log_alloc();
+ auto rc = vsnprintf(line, MAX_LOG_LINE_SIZE - 1, fmt, args);
+ log_ring.lr_length += rc;
+ lnav_log_file | [&](auto file) {
+ fwrite(line, 1, rc, file);
+ fflush(file);
+ };
+ va_end(args);
+}
+
+void
+log_msg_extra_complete()
+{
+ std::lock_guard<std::mutex> mg(*lnav_log_mutex());
+ auto line = log_alloc();
+ line[0] = '\n';
+ log_ring.lr_length += 1;
+ lnav_log_file | [&](auto file) {
+ fwrite(line, 1, 1, file);
+ fflush(file);
+ };
+}
+
+void
+log_backtrace(lnav_log_level_t level)
+{
+#ifdef HAVE_EXECINFO_H
+ int frame_count;
+ void* frames[128];
+
+ frame_count = backtrace(frames, 128);
+ auto bt = backtrace_symbols(frames, frame_count);
+ for (int lpc = 0; lpc < frame_count; lpc++) {
+ log_msg(level, __FILE__, __LINE__, "%s", bt[lpc]);
+ }
+#endif
+}
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunused-result"
+static void
+sigabrt(int sig, siginfo_t* info, void* ctx)
+{
+ char crash_path[1024], latest_crash_path[1024];
+ int fd;
+#ifdef HAVE_EXECINFO_H
+ int frame_count;
+ void* frames[128];
+#endif
+ struct tm localtm;
+ time_t curr_time;
+
+ if (lnav_log_crash_dir == nullptr) {
+ printf("%*s", (int) log_ring.lr_length, log_ring.lr_data);
+ return;
+ }
+
+ log_error("Received signal: %d", sig);
+
+#ifdef HAVE_EXECINFO_H
+ frame_count = backtrace(frames, 128);
+#endif
+ curr_time = time(nullptr);
+ localtime_r(&curr_time, &localtm);
+ snprintf(crash_path,
+ sizeof(crash_path),
+ "%s/crash-%4d-%02d-%02d-%02d-%02d-%02d.%d.log",
+ lnav_log_crash_dir,
+ localtm.tm_year + 1900,
+ localtm.tm_mon + 1,
+ localtm.tm_mday,
+ localtm.tm_hour,
+ localtm.tm_min,
+ localtm.tm_sec,
+ getpid());
+ snprintf(latest_crash_path,
+ sizeof(latest_crash_path),
+ "%s/latest-crash.log",
+ lnav_log_crash_dir);
+ if ((fd = open(crash_path, O_CREAT | O_TRUNC | O_RDWR, 0600)) != -1) {
+ if (log_ring.lr_frag_start < (off_t) BUFFER_SIZE) {
+ (void) write(fd,
+ &log_ring.lr_data[log_ring.lr_frag_start],
+ log_ring.lr_frag_end - log_ring.lr_frag_start);
+ }
+ (void) write(fd, log_ring.lr_data, log_ring.lr_length);
+#ifdef HAVE_EXECINFO_H
+ backtrace_symbols_fd(frames, frame_count, fd);
+#endif
+#if BACKWARD_HAS_DW == 1
+ {
+ ucontext_t* uctx = static_cast<ucontext_t*>(ctx);
+ void* error_addr = nullptr;
+
+# ifdef REG_RIP // x86_64
+ error_addr
+ = reinterpret_cast<void*>(uctx->uc_mcontext.gregs[REG_RIP]);
+# elif defined(REG_EIP) // x86_32
+ error_addr
+ = reinterpret_cast<void*>(uctx->uc_mcontext.gregs[REG_EIP]);
+# endif
+
+ backward::StackTrace st;
+
+ if (error_addr) {
+ st.load_from(error_addr,
+ 32,
+ reinterpret_cast<void*>(uctx),
+ info->si_addr);
+ } else {
+ st.load_here(32, reinterpret_cast<void*>(uctx), info->si_addr);
+ }
+ backward::TraceResolver tr;
+
+ tr.load_stacktrace(st);
+ for (size_t lpc = 0; lpc < st.size(); lpc++) {
+ auto trace = tr.resolve(st[lpc]);
+ char buf[1024];
+
+ snprintf(buf,
+ sizeof(buf),
+ "Frame %lu:%s:%s (%s:%d)\n",
+ lpc,
+ trace.object_filename.c_str(),
+ trace.object_function.c_str(),
+ trace.source.filename.c_str(),
+ trace.source.line);
+ write(fd, buf, strlen(buf));
+ }
+ }
+#endif
+ log_ring.lr_length = 0;
+ log_ring.lr_frag_start = BUFFER_SIZE;
+ log_ring.lr_frag_end = 0;
+
+ log_host_info();
+
+ for (auto lsd : DUMPER_LIST()) {
+ lsd->log_state();
+ }
+
+ if (log_ring.lr_frag_start < (off_t) BUFFER_SIZE) {
+ write(fd,
+ &log_ring.lr_data[log_ring.lr_frag_start],
+ log_ring.lr_frag_end - log_ring.lr_frag_start);
+ }
+ write(fd, log_ring.lr_data, log_ring.lr_length);
+ if (getenv("DUMP_CRASH") != nullptr) {
+ char buffer[1024];
+ int rc;
+
+ lseek(fd, 0, SEEK_SET);
+ while ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
+ write(STDERR_FILENO, buffer, rc);
+ }
+ }
+ close(fd);
+
+ remove(latest_crash_path);
+ symlink(crash_path, latest_crash_path);
+ }
+
+ lnav_log_orig_termios | [](auto termios) {
+ for (auto lcr : CRASH_LIST) {
+ lcr->log_crash_recover();
+ }
+
+ tcsetattr(STDOUT_FILENO, TCSAFLUSH, termios);
+ dup2(STDOUT_FILENO, STDERR_FILENO);
+ };
+ fprintf(stderr, CRASH_MSG, crash_path);
+
+#ifndef ATTACH_ON_SIGNAL
+ if (isatty(STDIN_FILENO)) {
+ char response;
+
+ fprintf(stderr, "\nWould you like to attach a debugger? (y/N) ");
+ fflush(stderr);
+
+ if (scanf("%c", &response) > 0 && tolower(response) == 'y') {
+ pid_t lnav_pid = getpid();
+ pid_t child_pid;
+
+ switch ((child_pid = fork())) {
+ case 0: {
+ char pid_str[32];
+
+ snprintf(pid_str, sizeof(pid_str), "--pid=%d", lnav_pid);
+ execlp("gdb", "gdb", pid_str, nullptr);
+
+ snprintf(pid_str, sizeof(pid_str), "%d", lnav_pid);
+ execlp("lldb", "lldb", "--attach-pid", pid_str, nullptr);
+
+ fprintf(stderr, "Could not attach gdb or lldb, exiting.\n");
+ _exit(1);
+ break;
+ }
+
+ case -1: {
+ break;
+ }
+
+ default: {
+ int status;
+
+ while (wait(&status) < 0) {
+ }
+ break;
+ }
+ }
+ }
+ }
+#endif
+
+ _exit(1);
+}
+#pragma GCC diagnostic pop
+
+void
+log_install_handlers()
+{
+ const size_t stack_size = 8 * 1024 * 1024;
+ const int sigs[] = {
+ SIGABRT,
+ SIGSEGV,
+ SIGBUS,
+ SIGILL,
+ SIGFPE,
+ };
+ static auto_mem<void> stack_content;
+
+ stack_t ss;
+
+ stack_content = malloc(stack_size);
+ ss.ss_sp = stack_content;
+ ss.ss_size = stack_size;
+ ss.ss_flags = 0;
+ sigaltstack(&ss, nullptr);
+ for (const auto sig : sigs) {
+ struct sigaction sa;
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_flags = SA_SIGINFO | SA_ONSTACK | SA_NODEFER | SA_RESETHAND;
+ sigfillset(&sa.sa_mask);
+ sigdelset(&sa.sa_mask, sig);
+ sa.sa_sigaction = sigabrt;
+
+ sigaction(sig, &sa, nullptr);
+ }
+}
+
+void
+log_abort()
+{
+ raise(SIGABRT);
+ _exit(1);
+}
+
+void
+log_pipe_err(int fd)
+{
+ std::thread reader([fd]() {
+ char buffer[1024];
+ bool done = false;
+
+ while (!done) {
+ int rc = read(fd, buffer, sizeof(buffer));
+
+ switch (rc) {
+ case -1:
+ case 0:
+ done = true;
+ break;
+ default:
+ while (buffer[rc - 1] == '\n' || buffer[rc - 1] == '\r') {
+ rc -= 1;
+ }
+
+ log_error("%.*s", rc, buffer);
+ break;
+ }
+ }
+
+ close(fd);
+ });
+
+ reader.detach();
+}
+
+log_state_dumper::log_state_dumper()
+{
+ DUMPER_LIST().push_back(this);
+}
+
+log_state_dumper::~log_state_dumper()
+{
+ auto iter = std::find(DUMPER_LIST().begin(), DUMPER_LIST().end(), this);
+ if (iter != DUMPER_LIST().end()) {
+ DUMPER_LIST().erase(iter);
+ }
+}
+
+log_crash_recoverer::log_crash_recoverer()
+{
+ CRASH_LIST.push_back(this);
+}
+
+log_crash_recoverer::~log_crash_recoverer()
+{
+ auto iter = std::find(CRASH_LIST.begin(), CRASH_LIST.end(), this);
+
+ if (iter != CRASH_LIST.end()) {
+ CRASH_LIST.erase(iter);
+ }
+}
diff --git a/src/base/lnav_log.hh b/src/base/lnav_log.hh
new file mode 100644
index 0000000..551b4f8
--- /dev/null
+++ b/src/base/lnav_log.hh
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav_log.hh
+ */
+
+#ifndef lnav_log_hh
+#define lnav_log_hh
+
+#include <cstdint>
+#include <string>
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+
+#ifndef lnav_dead2
+# define lnav_dead2 __attribute__((noreturn))
+#endif
+
+#include "optional.hpp"
+
+struct termios;
+
+enum class lnav_log_level_t : uint32_t {
+ TRACE,
+ DEBUG,
+ INFO,
+ WARNING,
+ ERROR,
+};
+
+void log_argv(int argc, char* argv[]);
+void log_host_info();
+void log_rusage_raw(enum lnav_log_level_t level,
+ const char* src_file,
+ int line_number,
+ const struct rusage& ru);
+void log_msg(enum lnav_log_level_t level,
+ const char* src_file,
+ int line_number,
+ const char* fmt,
+ ...);
+void log_msg_extra(const char* fmt, ...);
+void log_msg_extra_complete();
+void log_install_handlers();
+void log_abort() lnav_dead2;
+void log_pipe_err(int fd);
+void log_set_thread_prefix(std::string prefix);
+void log_backtrace(lnav_log_level_t level);
+
+struct log_state_dumper {
+public:
+ log_state_dumper();
+
+ virtual ~log_state_dumper();
+
+ virtual void log_state(){
+
+ };
+
+ log_state_dumper(const log_state_dumper&) = delete;
+ log_state_dumper& operator=(const log_state_dumper&) = delete;
+};
+
+struct log_crash_recoverer {
+public:
+ log_crash_recoverer();
+
+ virtual ~log_crash_recoverer();
+
+ virtual void log_crash_recover() = 0;
+};
+
+extern nonstd::optional<FILE*> lnav_log_file;
+extern const char* lnav_log_crash_dir;
+extern nonstd::optional<const struct termios*> lnav_log_orig_termios;
+extern enum lnav_log_level_t lnav_log_level;
+
+#define log_msg_wrapper(level, fmt...) \
+ do { \
+ if (lnav_log_level <= level) { \
+ log_msg(level, __FILE__, __LINE__, fmt); \
+ } \
+ } while (false)
+
+#define log_rusage(level, ru) log_rusage_raw(level, __FILE__, __LINE__, ru);
+
+#define log_error(fmt...) log_msg_wrapper(lnav_log_level_t::ERROR, fmt);
+
+#define log_warning(fmt...) log_msg_wrapper(lnav_log_level_t::WARNING, fmt);
+
+#define log_info(fmt...) log_msg_wrapper(lnav_log_level_t::INFO, fmt);
+
+#define log_debug(fmt...) log_msg_wrapper(lnav_log_level_t::DEBUG, fmt);
+
+#define log_trace(fmt...) log_msg_wrapper(lnav_log_level_t::TRACE, fmt);
+
+#define require(e) ((void) ((e) ? 0 : lnav_require(#e, __FILE__, __LINE__)))
+#define lnav_require(e, file, line) \
+ (log_msg( \
+ lnav_log_level_t::ERROR, file, line, "failed precondition `%s'", e), \
+ log_abort(), \
+ 1)
+
+#define require_true(lhs) \
+ ((void) ((lhs) ? 0 : lnav_require_unary(#lhs, lhs, __FILE__, __LINE__)))
+#define require_false(lhs) \
+ ((void) ((!lhs) ? 0 : lnav_require_unary(#lhs, lhs, __FILE__, __LINE__)))
+#define lnav_require_unary(e, lhs, file, line) \
+ (log_msg(lnav_log_level_t::ERROR, \
+ file, \
+ line, \
+ "failed precondition `%s' (lhs=%s)", \
+ e, \
+ std::to_string(lhs).c_str()), \
+ log_abort(), \
+ 1)
+
+#define require_ge(lhs, rhs) \
+ ((void) ((lhs >= rhs) \
+ ? 0 \
+ : lnav_require_binary( \
+ #lhs " >= " #rhs, lhs, rhs, __FILE__, __LINE__)))
+#define require_gt(lhs, rhs) \
+ ((void) ((lhs > rhs) ? 0 \
+ : lnav_require_binary( \
+ #lhs " > " #rhs, lhs, rhs, __FILE__, __LINE__)))
+#define require_lt(lhs, rhs) \
+ ((void) ((lhs < rhs) ? 0 \
+ : lnav_require_binary( \
+ #lhs " < " #rhs, lhs, rhs, __FILE__, __LINE__)))
+
+#define lnav_require_binary(e, lhs, rhs, file, line) \
+ (log_msg(lnav_log_level_t::ERROR, \
+ file, \
+ line, \
+ "failed precondition `%s' (lhs=%s; rhs=%s)", \
+ e, \
+ std::to_string(lhs).c_str(), \
+ std::to_string(rhs).c_str()), \
+ log_abort(), \
+ 1)
+
+#define ensure(e) ((void) ((e) ? 0 : lnav_ensure(#e, __FILE__, __LINE__)))
+#define lnav_ensure(e, file, line) \
+ (log_msg( \
+ lnav_log_level_t::ERROR, file, line, "failed postcondition `%s'", e), \
+ log_abort(), \
+ 1)
+
+#define log_perror(e) \
+ ((void) ((e != -1) ? 0 : lnav_log_perror(#e, __FILE__, __LINE__)))
+#define lnav_log_perror(e, file, line) \
+ (log_msg(lnav_log_level_t::ERROR, \
+ file, \
+ line, \
+ "syscall failed `%s' -- %s", \
+ e, \
+ strerror(errno)), \
+ 1)
+
+#endif
diff --git a/src/base/log_level_enum.hh b/src/base/log_level_enum.hh
new file mode 100644
index 0000000..983fd5c
--- /dev/null
+++ b/src/base/log_level_enum.hh
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_log_level_enum_hh
+#define lnav_log_level_enum_hh
+
+/**
+ * The logging level identifiers for a line(s).
+ */
+enum log_level_t : int {
+ LEVEL_UNKNOWN,
+ LEVEL_TRACE,
+ LEVEL_DEBUG5,
+ LEVEL_DEBUG4,
+ LEVEL_DEBUG3,
+ LEVEL_DEBUG2,
+ LEVEL_DEBUG,
+ LEVEL_INFO,
+ LEVEL_STATS,
+ LEVEL_NOTICE,
+ LEVEL_WARNING,
+ LEVEL_ERROR,
+ LEVEL_CRITICAL,
+ LEVEL_FATAL,
+ LEVEL_INVALID,
+
+ LEVEL__MAX,
+
+ LEVEL_IGNORE = 0x10, /*< Ignore */
+ LEVEL_TIME_SKEW = 0x20, /*< Received after timestamp. */
+ LEVEL_MARK = 0x40, /*< Bookmarked line. */
+ LEVEL_CONTINUED = 0x80, /*< Continuation of multiline entry. */
+
+ /** Mask of flags for the level field. */
+ LEVEL__FLAGS
+ = (LEVEL_IGNORE | LEVEL_TIME_SKEW | LEVEL_MARK | LEVEL_CONTINUED)
+};
+
+#endif
diff --git a/src/base/lrucache.hpp b/src/base/lrucache.hpp
new file mode 100644
index 0000000..8bcbad6
--- /dev/null
+++ b/src/base/lrucache.hpp
@@ -0,0 +1,83 @@
+/*
+ * File: lrucache.hpp
+ * Author: Alexander Ponomarev
+ *
+ * Created on June 20, 2013, 5:09 PM
+ */
+
+#ifndef _LRUCACHE_HPP_INCLUDED_
+#define _LRUCACHE_HPP_INCLUDED_
+
+#include <map>
+#include <list>
+#include <cstddef>
+#include <stdexcept>
+
+#include "optional.hpp"
+
+namespace cache {
+
+template<typename key_t, typename value_t>
+class lru_cache {
+public:
+ typedef typename std::pair<key_t, value_t> key_value_pair_t;
+ typedef typename std::list<key_value_pair_t>::iterator list_iterator_t;
+
+ lru_cache(size_t max_size) :
+ _max_size(max_size) {
+ }
+
+ void put(const key_t& key, const value_t& value) {
+ auto it = _cache_items_map.find(key);
+ _cache_items_list.push_front(key_value_pair_t(key, value));
+ if (it != _cache_items_map.end()) {
+ _cache_items_list.erase(it->second);
+ _cache_items_map.erase(it);
+ }
+ _cache_items_map[key] = _cache_items_list.begin();
+
+ if (_cache_items_map.size() > _max_size) {
+ auto last = _cache_items_list.end();
+ last--;
+ _cache_items_map.erase(last->first);
+ _cache_items_list.pop_back();
+ }
+ }
+
+ nonstd::optional<value_t> get(const key_t& key) {
+ auto it = _cache_items_map.find(key);
+ if (it == _cache_items_map.end()) {
+ return nonstd::nullopt;
+ }
+
+ _cache_items_list.splice(_cache_items_list.begin(), _cache_items_list, it->second);
+ return it->second->second;
+ }
+
+ bool exists(const key_t& key) const {
+ return _cache_items_map.find(key) != _cache_items_map.end();
+ }
+
+ size_t size() const {
+ return _cache_items_map.size();
+ }
+
+ void set_max_size(size_t max_size) {
+ this->_max_size = max_size;
+ }
+
+ void clear() {
+ this->_cache_items_map.clear();
+ this->_cache_items_list.clear();
+ }
+
+private:
+ std::list<key_value_pair_t> _cache_items_list;
+ std::map<key_t, list_iterator_t> _cache_items_map;
+ size_t _max_size;
+};
+
+} // namespace cache
+
+#endif /* _LRUCACHE_HPP_INCLUDED_ */
+
diff --git a/src/base/math_util.hh b/src/base/math_util.hh
new file mode 100644
index 0000000..842b319
--- /dev/null
+++ b/src/base/math_util.hh
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_math_util_hh
+#define lnav_math_util_hh
+
+#include <sys/types.h>
+
+#undef rounddown
+
+/**
+ * Round down a number based on a given granularity.
+ *
+ * @param
+ * @param step The granularity.
+ */
+template<typename Size, typename Step>
+inline int
+rounddown(Size size, Step step)
+{
+ return size - (size % step);
+}
+
+inline int
+rounddown_offset(size_t size, int step, int offset)
+{
+ return size - ((size - offset) % step);
+}
+
+inline size_t
+roundup_size(size_t size, int step)
+{
+ size_t retval = size + step;
+
+ retval -= (retval % step);
+
+ return retval;
+}
+
+template<typename T>
+T
+abs_diff(T a, T b)
+{
+ return a > b ? a - b : b - a;
+}
+
+#endif
diff --git a/src/base/network.tcp.cc b/src/base/network.tcp.cc
new file mode 100644
index 0000000..0194b6f
--- /dev/null
+++ b/src/base/network.tcp.cc
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "network.tcp.hh"
+
+#include <netdb.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+
+#include "auto_mem.hh"
+#include "config.h"
+#include "fmt/format.h"
+
+namespace network {
+namespace tcp {
+
+Result<auto_fd, std::string>
+connect(const char* hostname, const char* servname)
+{
+ struct addrinfo hints;
+ auto_mem<addrinfo> ai(freeaddrinfo);
+
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = AF_UNSPEC;
+ hints.ai_socktype = SOCK_STREAM;
+ auto rc = getaddrinfo(hostname, servname, &hints, ai.out());
+
+ if (rc != 0) {
+ return Err(fmt::format(FMT_STRING("unable to resolve {}:{} -- {}"),
+ hostname,
+ servname,
+ gai_strerror(rc)));
+ }
+
+ auto retval = auto_fd(socket(ai->ai_family, ai->ai_socktype, 0));
+
+ rc = ::connect(retval, ai->ai_addr, ai->ai_addrlen);
+ if (rc != 0) {
+ return Err(fmt::format(FMT_STRING("unable to connect to {}:{} -- {}"),
+ hostname,
+ servname,
+ strerror(rc)));
+ }
+
+ return Ok(std::move(retval));
+}
+
+} // namespace tcp
+} // namespace network
diff --git a/src/base/network.tcp.hh b/src/base/network.tcp.hh
new file mode 100644
index 0000000..49fc392
--- /dev/null
+++ b/src/base/network.tcp.hh
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_network_tcp_hh
+#define lnav_network_tcp_hh
+
+#include <string>
+
+#include "auto_fd.hh"
+#include "result.h"
+
+namespace network {
+
+struct locality {
+ locality(nonstd::optional<std::string> username,
+ std::string hostname,
+ nonstd::optional<std::string> service)
+ : l_username(std::move(username)), l_hostname(std::move(hostname)),
+ l_service(std::move(service))
+ {
+ }
+
+ nonstd::optional<std::string> l_username;
+ std::string l_hostname;
+ nonstd::optional<std::string> l_service;
+};
+
+struct path {
+ locality p_locality;
+ std::string p_path;
+
+ path(locality loc, std::string path)
+ : p_locality(std::move(loc)), p_path(std::move(path))
+ {
+ }
+
+ path home() const
+ {
+ return {
+ this->p_locality,
+ ".",
+ };
+ }
+};
+
+namespace tcp {
+
+Result<auto_fd, std::string> connect(const char* hostname,
+ const char* servname);
+
+}
+} // namespace network
+
+#endif
diff --git a/src/base/network.tcp.tests.cc b/src/base/network.tcp.tests.cc
new file mode 100644
index 0000000..76846ca
--- /dev/null
+++ b/src/base/network.tcp.tests.cc
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <iostream>
+
+#include "config.h"
+#include "doctest/doctest.h"
+#include "network.tcp.hh"
+
+TEST_CASE("bad hostname")
+{
+ auto connect_res = network::tcp::connect("foobar.bazzer", "http");
+ CHECK(connect_res.unwrapErr() ==
+ "unable to resolve foobar.bazzer:http -- nodename nor servname "
+ "provided, or not known");
+}
+
+TEST_CASE("bad servname")
+{
+ auto connect_res = network::tcp::connect("www.cnn.com", "non-existent");
+ CHECK(connect_res.unwrapErr() ==
+ "unable to resolve www.cnn.com:non-existent -- nodename nor "
+ "servname provided, or not known");
+}
diff --git a/src/base/opt_util.hh b/src/base/opt_util.hh
new file mode 100644
index 0000000..aea038e
--- /dev/null
+++ b/src/base/opt_util.hh
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_opt_util_hh
+#define lnav_opt_util_hh
+
+#include <stdlib.h>
+
+#include "optional.hpp"
+
+namespace detail {
+
+template<class T>
+typename std::enable_if<std::is_void<T>::value, T>::type
+void_or_nullopt()
+{
+ return;
+}
+
+template<class T>
+typename std::enable_if<not std::is_void<T>::value, T>::type
+void_or_nullopt()
+{
+ return nonstd::nullopt;
+}
+
+template<class T>
+struct is_optional : std::false_type {
+};
+
+template<class T>
+struct is_optional<nonstd::optional<T>> : std::true_type {
+};
+} // namespace detail
+
+template<class T,
+ class F,
+ std::enable_if_t<detail::is_optional<std::decay_t<T>>::value, int> = 0>
+auto
+operator|(T&& t, F f)
+ -> decltype(detail::void_or_nullopt<decltype(f(std::forward<T>(t).
+ operator*()))>())
+{
+ using return_type = decltype(f(std::forward<T>(t).operator*()));
+ if (t)
+ return f(std::forward<T>(t).operator*());
+ else
+ return detail::void_or_nullopt<return_type>();
+}
+
+template<class T>
+optional_constexpr nonstd::optional<typename std::decay<T>::type>
+make_optional_from_nullable(T&& v)
+{
+ if (v != nullptr) {
+ return nonstd::optional<typename std::decay<T>::type>(
+ std::forward<T>(v));
+ }
+ return nonstd::nullopt;
+}
+
+template<template<typename, typename...> class C, typename T>
+nonstd::optional<T>
+cget(const C<T>& container, size_t index)
+{
+ if (index < container.size()) {
+ return container[index];
+ }
+
+ return nonstd::nullopt;
+}
+
+inline nonstd::optional<const char*>
+getenv_opt(const char* name)
+{
+ return make_optional_from_nullable(getenv(name));
+}
+
+#endif
diff --git a/src/base/paths.cc b/src/base/paths.cc
new file mode 100644
index 0000000..ca53a07
--- /dev/null
+++ b/src/base/paths.cc
@@ -0,0 +1,130 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#ifdef __CYGWIN__
+# include <iostream>
+# include <sstream>
+#endif
+
+#include "fmt/format.h"
+#include "paths.hh"
+
+namespace lnav {
+namespace paths {
+
+#ifdef __CYGWIN__
+char*
+windows_to_unix_file_path(char* input)
+{
+ if (input == nullptr) {
+ return nullptr;
+ }
+ std::string file_path;
+ file_path.assign(input);
+
+ // Replace the slashes
+ std::replace(file_path.begin(),
+ file_path.end(),
+ WINDOWS_FILE_PATH_SEPARATOR,
+ UNIX_FILE_PATH_SEPARATOR);
+
+ // Convert the drive letter to lowercase
+ std::transform(
+ file_path.begin(),
+ file_path.begin() + 1,
+ file_path.begin(),
+ [](unsigned char character) { return std::tolower(character); });
+
+ // Remove the colon
+ const auto drive_letter = file_path.substr(0, 1);
+ const auto remaining_path = file_path.substr(2, file_path.size() - 2);
+ file_path = drive_letter + remaining_path;
+
+ std::stringstream stringstream;
+ stringstream << "/cygdrive/";
+ stringstream << file_path;
+
+ return const_cast<char*>(stringstream.str().c_str());
+}
+#endif
+
+ghc::filesystem::path
+dotlnav()
+{
+#ifdef __CYGWIN__
+ auto home_env = windows_to_unix_file_path(getenv("APPDATA"));
+#else
+ auto home_env = getenv("HOME");
+#endif
+ auto xdg_config_home = getenv("XDG_CONFIG_HOME");
+
+ if (home_env != nullptr) {
+ auto home_path = ghc::filesystem::path(home_env);
+
+ if (ghc::filesystem::is_directory(home_path)) {
+ auto home_lnav = home_path / ".lnav";
+
+ if (ghc::filesystem::is_directory(home_lnav)) {
+ return home_lnav;
+ }
+
+ if (xdg_config_home != nullptr) {
+ auto xdg_path = ghc::filesystem::path(xdg_config_home);
+
+ if (ghc::filesystem::is_directory(xdg_path)) {
+ return xdg_path / "lnav";
+ }
+ }
+
+ auto home_config = home_path / ".config";
+
+ if (ghc::filesystem::is_directory(home_config)) {
+ return home_config / "lnav";
+ }
+
+ return home_lnav;
+ }
+ }
+
+ return ghc::filesystem::current_path();
+}
+
+ghc::filesystem::path
+workdir()
+{
+ auto subdir_name = fmt::format(FMT_STRING("lnav-user-{}-work"), getuid());
+ auto tmp_path = ghc::filesystem::temp_directory_path();
+
+ return tmp_path / ghc::filesystem::path(subdir_name);
+}
+
+} // namespace paths
+} // namespace lnav
diff --git a/src/base/paths.hh b/src/base/paths.hh
new file mode 100644
index 0000000..6c43a2a
--- /dev/null
+++ b/src/base/paths.hh
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_paths_hh
+#define lnav_paths_hh
+
+#include "ghc/filesystem.hpp"
+
+namespace lnav {
+namespace paths {
+
+#ifdef __CYGWIN__
+static const char WINDOWS_FILE_PATH_SEPARATOR = '\\';
+static const char UNIX_FILE_PATH_SEPARATOR = '/';
+
+char* windows_to_unix_file_path(char* input);
+#endif
+
+/**
+ * Compute the path to a file in the user's '.lnav' directory.
+ *
+ * @param sub The path to the file in the '.lnav' directory.
+ * @return The full path
+ */
+ghc::filesystem::path dotlnav();
+
+ghc::filesystem::path workdir();
+
+} // namespace paths
+} // namespace lnav
+
+#endif
diff --git a/src/base/result.h b/src/base/result.h
new file mode 100644
index 0000000..e3e8ac5
--- /dev/null
+++ b/src/base/result.h
@@ -0,0 +1,1032 @@
+/*
+ Mathieu Stefani, 03 mai 2016
+
+ This header provides a Result type that can be used to replace exceptions in
+ code that has to handle error.
+
+ Result<T, E> can be used to return and propagate an error to the caller.
+ Result<T, E> is an algebraic data type that can either Ok(T) to represent
+ success or Err(E) to represent an error.
+*/
+
+#pragma once
+
+#include <exception>
+#include <functional>
+#include <type_traits>
+
+#include <stdio.h>
+
+namespace types {
+template<typename T>
+struct Ok {
+ Ok(const T& val) : val(val) {}
+ Ok(T&& val) : val(std::move(val)) {}
+
+ T val;
+};
+
+template<>
+struct Ok<void> {};
+
+template<typename E>
+struct Err {
+ Err(const E& val) : val(val) {}
+ Err(E&& val) : val(std::move(val)) {}
+
+ E val;
+};
+
+template<>
+struct Err<void> {};
+}; // namespace types
+
+template<typename T, typename CleanT = typename std::decay<T>::type>
+types::Ok<CleanT>
+Ok(T&& val)
+{
+ return types::Ok<CleanT>(std::forward<T>(val));
+}
+
+inline types::Ok<void>
+Ok()
+{
+ return {};
+}
+
+template<typename E, typename CleanE = typename std::decay<E>::type>
+types::Err<CleanE>
+Err(E&& val)
+{
+ return types::Err<CleanE>(std::forward<E>(val));
+}
+
+inline types::Err<void>
+Err()
+{
+ return {};
+}
+
+template<typename T, typename E>
+struct Result;
+
+namespace details {
+
+template<typename...>
+struct void_t {
+ typedef void type;
+};
+
+namespace impl {
+template<typename Func>
+struct result_of;
+
+template<typename Ret, typename Cls, typename... Args>
+struct result_of<Ret (Cls::*)(Args...)> : public result_of<Ret(Args...)> {};
+
+template<typename Ret, typename... Args>
+struct result_of<std::function<Ret(Args...)>> {
+ typedef Ret type;
+};
+} // namespace impl
+
+template<typename Func>
+struct result_of : public impl::result_of<decltype(&Func::operator())> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct result_of<Ret (Cls::*)(Args...) const> {
+ typedef Ret type;
+};
+
+template<typename Ret, typename... Args>
+struct result_of<Ret (*)(Args...)> {
+ typedef Ret type;
+};
+
+template<typename R>
+struct ResultOkType {
+ typedef typename std::decay<R>::type type;
+};
+
+template<typename T, typename E>
+struct ResultOkType<Result<T, E>> {
+ typedef T type;
+};
+
+template<typename R>
+struct ResultErrType {
+ typedef R type;
+};
+
+template<typename T, typename E>
+struct ResultErrType<Result<T, E>> {
+ typedef typename std::remove_reference<E>::type type;
+};
+
+template<typename R>
+struct IsResult : public std::false_type {};
+template<typename T, typename E>
+struct IsResult<Result<T, E>> : public std::true_type {};
+
+namespace ok {
+
+namespace impl {
+
+template<typename T>
+struct Map;
+
+template<typename Ret, typename Cls, typename Arg>
+struct Map<Ret (Cls::*)(Arg) const> : public Map<Ret(Arg)> {};
+
+template<typename Ret, typename Cls, typename Arg>
+struct Map<Ret (Cls::*)(Arg)> : public Map<Ret(Arg)> {};
+
+// General implementation
+template<typename Ret, typename Arg>
+struct Map<Ret(Arg)> {
+ static_assert(
+ !IsResult<Ret>::value,
+ "Can not map a callback returning a Result, use then instead");
+
+ template<typename T, typename E, typename Func>
+ static Result<Ret, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(
+ std::is_same<T, Arg>::value || std::is_convertible<T, Arg>::value,
+ "Incompatible types detected");
+
+ if (result.isOk()) {
+ auto res = func(result.storage().template get<T>());
+ return types::Ok<Ret>(std::move(res));
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// Specialization for callback returning void
+template<typename Arg>
+struct Map<void(Arg)> {
+ template<typename T, typename E, typename Func>
+ static Result<void, E> map(const Result<T, E>& result, Func func)
+ {
+ if (result.isOk()) {
+ func(result.storage().template get<T>());
+ return types::Ok<void>();
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// Specialization for a void Result
+template<typename Ret>
+struct Map<Ret(void)> {
+ template<typename T, typename E, typename Func>
+ static Result<Ret, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not map a void callback on a non-void Result");
+
+ if (result.isOk()) {
+ auto ret = func();
+ return types::Ok<Ret>(std::move(ret));
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// Specialization for callback returning void on a void Result
+template<>
+struct Map<void(void)> {
+ template<typename T, typename E, typename Func>
+ static Result<void, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not map a void callback on a non-void Result");
+
+ if (result.isOk()) {
+ func();
+ return types::Ok<void>();
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// General specialization for a callback returning a Result
+template<typename U, typename E, typename Arg>
+struct Map<Result<U, E>(Arg)> {
+ template<typename T, typename Func>
+ static Result<U, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(
+ std::is_same<T, Arg>::value || std::is_convertible<T, Arg>::value,
+ "Incompatible types detected");
+
+ if (result.isOk()) {
+ auto res = func(result.storage().template get<T>());
+ return res;
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+// Specialization for a void callback returning a Result
+template<typename U, typename E>
+struct Map<Result<U, E>(void)> {
+ template<typename T, typename Func>
+ static Result<U, E> map(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not call a void-callback on a non-void Result");
+
+ if (result.isOk()) {
+ auto res = func();
+ return res;
+ }
+
+ return types::Err<E>(result.storage().template get<E>());
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Map;
+
+template<typename Ret, typename... Args>
+struct Map<Ret (*)(Args...)> : public impl::Map<Ret(Args...)> {};
+
+template<typename Ret, typename... Args>
+struct Map<Ret(Args...)> : public impl::Map<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Map<Ret (Cls::*)(Args...)> : public impl::Map<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Map<Ret (Cls::*)(Args...) const> : public impl::Map<Ret(Args...)> {};
+
+template<typename Ret, typename... Args>
+struct Map<std::function<Ret(Args...)>> : public impl::Map<Ret(Args...)> {};
+
+} // namespace ok
+
+namespace err {
+
+namespace impl {
+
+template<typename T>
+struct Map;
+
+template<typename Ret, typename Cls, typename Arg>
+struct Map<Ret (Cls::*)(Arg) const> {
+ static_assert(
+ !IsResult<Ret>::value,
+ "Can not map a callback returning a Result, use orElse instead");
+
+ template<typename T, typename E, typename Func>
+ static Result<T, Ret> map(const Result<T, E>& result, Func func)
+ {
+ if (result.isErr()) {
+ auto res = func(result.storage().template get<E>());
+ return types::Err<Ret>(res);
+ }
+
+ return types::Ok<T>(result.storage().template get<T>());
+ }
+
+ template<typename E, typename Func>
+ static Result<void, Ret> map(const Result<void, E>& result, Func func)
+ {
+ if (result.isErr()) {
+ auto res = func(result.storage().template get<E>());
+ return types::Err<Ret>(res);
+ }
+
+ return types::Ok<void>();
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Map : public impl::Map<decltype(&Func::operator())> {};
+
+} // namespace err
+
+namespace And {
+
+namespace impl {
+
+template<typename Func>
+struct Then;
+
+template<typename Ret, typename... Args>
+struct Then<Ret (*)(Args...)> : public Then<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Then<Ret (Cls::*)(Args...)> : public Then<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Then<Ret (Cls::*)(Args...) const> : public Then<Ret(Args...)> {};
+
+template<typename Ret, typename Arg>
+struct Then<Ret(Arg)> {
+ static_assert(std::is_same<Ret, void>::value,
+ "then() should not return anything, use map() instead");
+
+ template<typename T, typename E, typename Func>
+ static Result<T, E> then(const Result<T, E>& result, Func func)
+ {
+ if (result.isOk()) {
+ func(result.storage().template get<T>());
+ }
+ return result;
+ }
+};
+
+template<typename Ret>
+struct Then<Ret(void)> {
+ static_assert(std::is_same<Ret, void>::value,
+ "then() should not return anything, use map() instead");
+
+ template<typename T, typename E, typename Func>
+ static Result<T, E> then(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not call a void-callback on a non-void Result");
+
+ if (result.isOk()) {
+ func();
+ }
+
+ return result;
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Then : public impl::Then<decltype(&Func::operator())> {};
+
+template<typename Ret, typename... Args>
+struct Then<Ret (*)(Args...)> : public impl::Then<Ret(Args...)> {};
+
+template<typename Ret, typename Arg>
+struct Then<Ret(Arg)> : public impl::Then<Ret(Arg)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Then<Ret (Cls::*)(Args...)> : public impl::Then<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Then<Ret (Cls::*)(Args...) const> : public impl::Then<Ret(Args...)> {};
+
+template<typename Ret, typename... Args>
+struct Then<std::function<Ret(Args...)>> : public impl::Then<Ret(Args...)> {};
+
+} // namespace And
+
+namespace Or {
+
+namespace impl {
+
+template<typename Func>
+struct Else;
+
+template<typename Ret, typename... Args>
+struct Else<Ret (*)(Args...)> : public Else<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Else<Ret (Cls::*)(Args...)> : public Else<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Else<Ret (Cls::*)(Args...) const> : public Else<Ret(Args...)> {};
+
+template<typename T, typename F, typename Arg>
+struct Else<Result<T, F>(Arg)> {
+ template<typename E, typename Func>
+ static Result<T, F> orElse(const Result<T, E>& result, Func func)
+ {
+ static_assert(
+ std::is_same<E, Arg>::value || std::is_convertible<E, Arg>::value,
+ "Incompatible types detected");
+
+ if (result.isErr()) {
+ auto res = func(result.storage().template get<E>());
+ return res;
+ }
+
+ return types::Ok<T>(result.storage().template get<T>());
+ }
+
+ template<typename E, typename Func>
+ static Result<void, F> orElse(const Result<void, E>& result, Func func)
+ {
+ if (result.isErr()) {
+ auto res = func(result.storage().template get<E>());
+ return res;
+ }
+
+ return types::Ok<void>();
+ }
+};
+
+template<typename T, typename F>
+struct Else<Result<T, F>(void)> {
+ template<typename E, typename Func>
+ static Result<T, F> orElse(const Result<T, E>& result, Func func)
+ {
+ static_assert(std::is_same<T, void>::value,
+ "Can not call a void-callback on a non-void Result");
+
+ if (result.isErr()) {
+ auto res = func();
+ return res;
+ }
+
+ return types::Ok<T>(result.storage().template get<T>());
+ }
+
+ template<typename E, typename Func>
+ static Result<void, F> orElse(const Result<void, E>& result, Func func)
+ {
+ if (result.isErr()) {
+ auto res = func();
+ return res;
+ }
+
+ return types::Ok<void>();
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Else : public impl::Else<decltype(&Func::operator())> {};
+
+template<typename Ret, typename... Args>
+struct Else<Ret (*)(Args...)> : public impl::Else<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Else<Ret (Cls::*)(Args...)> : public impl::Else<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Else<Ret (Cls::*)(Args...) const> : public impl::Else<Ret(Args...)> {};
+
+} // namespace Or
+
+namespace Other {
+
+namespace impl {
+
+template<typename Func>
+struct Wise;
+
+template<typename Ret, typename... Args>
+struct Wise<Ret (*)(Args...)> : public Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Wise<Ret (Cls::*)(Args...)> : public Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Wise<Ret (Cls::*)(Args...) const> : public Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Arg>
+struct Wise<Ret(Arg)> {
+ template<typename T, typename E, typename Func>
+ static Result<T, E> otherwise(const Result<T, E>& result, Func func)
+ {
+ static_assert(
+ std::is_same<E, Arg>::value || std::is_convertible<E, Arg>::value,
+ "Incompatible types detected");
+
+ static_assert(
+ std::is_same<Ret, void>::value,
+ "callback should not return anything, use mapError() for that");
+
+ if (result.isErr()) {
+ func(result.storage().template get<E>());
+ }
+ return result;
+ }
+};
+
+} // namespace impl
+
+template<typename Func>
+struct Wise : public impl::Wise<decltype(&Func::operator())> {};
+
+template<typename Ret, typename... Args>
+struct Wise<Ret (*)(Args...)> : public impl::Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Wise<Ret (Cls::*)(Args...)> : public impl::Wise<Ret(Args...)> {};
+
+template<typename Ret, typename Cls, typename... Args>
+struct Wise<Ret (Cls::*)(Args...) const> : public impl::Wise<Ret(Args...)> {};
+
+} // namespace Other
+
+template<typename T, typename E, typename Func>
+decltype(auto)
+map(const Result<T, E>& result, Func func)
+{
+ return ok::Map<Func>::map(result, func);
+}
+
+template<typename T,
+ typename E,
+ typename Func,
+ typename Ret
+ = Result<T,
+ typename details::ResultErrType<
+ typename details::result_of<Func>::type>::type>>
+Ret
+mapError(const Result<T, E>& result, Func func)
+{
+ return err::Map<Func>::map(result, func);
+}
+
+template<typename T, typename E, typename Func>
+Result<T, E>
+then(const Result<T, E>& result, Func func)
+{
+ return And::Then<Func>::then(result, func);
+}
+
+template<typename T, typename E, typename Func>
+Result<T, E>
+otherwise(const Result<T, E>& result, Func func)
+{
+ return Other::Wise<Func>::otherwise(result, func);
+}
+
+template<typename T,
+ typename E,
+ typename Func,
+ typename Ret
+ = Result<T,
+ typename details::ResultErrType<
+ typename details::result_of<Func>::type>::type>>
+Ret
+orElse(const Result<T, E>& result, Func func)
+{
+ return Or::Else<Func>::orElse(result, func);
+}
+
+struct ok_tag {};
+struct err_tag {};
+
+template<typename T, typename E>
+struct Storage {
+ static constexpr size_t Size = sizeof(T) > sizeof(E) ? sizeof(T)
+ : sizeof(E);
+ static constexpr size_t Align = sizeof(T) > sizeof(E) ? alignof(T)
+ : alignof(E);
+
+ typedef typename std::aligned_storage<Size, Align>::type type;
+
+ Storage() : initialized_(false) {}
+
+ void construct(types::Ok<T> ok)
+ {
+ new (&storage_) T(std::move(ok.val));
+ initialized_ = true;
+ }
+ void construct(types::Err<E> err)
+ {
+ new (&storage_) E(err.val);
+ initialized_ = true;
+ }
+
+ template<typename U>
+ void rawConstruct(U&& val)
+ {
+ typedef typename std::decay<U>::type CleanU;
+
+ new (&storage_) CleanU(std::forward<U>(val));
+ initialized_ = true;
+ }
+
+ template<typename U>
+ const U& get() const
+ {
+ return *reinterpret_cast<const U*>(&storage_);
+ }
+
+ template<typename U>
+ U& get()
+ {
+ return *reinterpret_cast<U*>(&storage_);
+ }
+
+ void destroy(ok_tag)
+ {
+ if (initialized_) {
+ get<T>().~T();
+ initialized_ = false;
+ }
+ }
+
+ void destroy(err_tag)
+ {
+ if (initialized_) {
+ get<E>().~E();
+ initialized_ = false;
+ }
+ }
+
+ type storage_;
+ bool initialized_;
+};
+
+template<typename E>
+struct Storage<void, E> {
+ typedef typename std::aligned_storage<sizeof(E), alignof(E)>::type type;
+
+ void construct(types::Ok<void>) { initialized_ = true; }
+
+ void construct(types::Err<E> err)
+ {
+ new (&storage_) E(err.val);
+ initialized_ = true;
+ }
+
+ template<typename U>
+ void rawConstruct(U&& val)
+ {
+ typedef typename std::decay<U>::type CleanU;
+
+ new (&storage_) CleanU(std::forward<U>(val));
+ initialized_ = true;
+ }
+
+ void destroy(ok_tag) { initialized_ = false; }
+ void destroy(err_tag)
+ {
+ if (initialized_) {
+ get<E>().~E();
+ initialized_ = false;
+ }
+ }
+
+ template<typename U,
+ typename = std::enable_if_t<!std::is_same<U, void>::value>>
+ const U& get() const
+ {
+ return *reinterpret_cast<const U*>(&storage_);
+ }
+
+ template<typename U,
+ typename = std::enable_if_t<!std::is_same<U, void>::value>>
+ typename std::add_lvalue_reference<U>::type get()
+ {
+ return *reinterpret_cast<U*>(&storage_);
+ }
+
+ template<typename U,
+ typename = std::enable_if_t<std::is_same<U, void>::value>>
+ void get()
+ {
+ }
+
+ type storage_;
+ bool initialized_;
+};
+
+template<typename T, typename E>
+struct Constructor {
+ static void move(Storage<T, E>&& src, Storage<T, E>& dst, ok_tag)
+ {
+ dst.rawConstruct(std::move(src.template get<T>()));
+ src.destroy(ok_tag());
+ }
+
+ static void copy(const Storage<T, E>& src, Storage<T, E>& dst, ok_tag)
+ {
+ dst.rawConstruct(src.template get<T>());
+ }
+
+ static void move(Storage<T, E>&& src, Storage<T, E>& dst, err_tag)
+ {
+ dst.rawConstruct(std::move(src.template get<E>()));
+ src.destroy(err_tag());
+ }
+
+ static void copy(const Storage<T, E>& src, Storage<T, E>& dst, err_tag)
+ {
+ dst.rawConstruct(src.template get<E>());
+ }
+};
+
+template<typename E>
+struct Constructor<void, E> {
+ static void move(Storage<void, E>&& src, Storage<void, E>& dst, ok_tag) {}
+
+ static void copy(const Storage<void, E>& src, Storage<void, E>& dst, ok_tag)
+ {
+ }
+
+ static void move(Storage<void, E>&& src, Storage<void, E>& dst, err_tag)
+ {
+ dst.rawConstruct(std::move(src.template get<E>()));
+ src.destroy(err_tag());
+ }
+
+ static void copy(const Storage<void, E>& src,
+ Storage<void, E>& dst,
+ err_tag)
+ {
+ dst.rawConstruct(src.template get<E>());
+ }
+};
+
+} // namespace details
+
+namespace local_concept {
+
+template<typename T, typename = void>
+struct EqualityComparable : std::false_type {};
+
+template<typename T>
+struct EqualityComparable<
+ T,
+ typename std::enable_if<
+ true,
+ typename details::void_t<decltype(std::declval<T>()
+ == std::declval<T>())>::type>::type>
+ : std::true_type {};
+
+} // namespace local_concept
+
+template<typename T, typename E>
+struct Result {
+ static_assert(!std::is_same<E, void>::value,
+ "void error type is not allowed");
+
+ typedef details::Storage<T, E> storage_type;
+
+ Result(types::Ok<T> ok) : ok_(true) { storage_.construct(std::move(ok)); }
+
+ Result(types::Err<E> err) : ok_(false)
+ {
+ storage_.construct(std::move(err));
+ }
+
+ Result(Result&& other)
+ {
+ if (other.isOk()) {
+ details::Constructor<T, E>::move(
+ std::move(other.storage_), storage_, details::ok_tag());
+ ok_ = true;
+ } else {
+ details::Constructor<T, E>::move(
+ std::move(other.storage_), storage_, details::err_tag());
+ ok_ = false;
+ }
+ }
+
+ Result(const Result& other)
+ {
+ if (other.isOk()) {
+ details::Constructor<T, E>::copy(
+ other.storage_, storage_, details::ok_tag());
+ ok_ = true;
+ } else {
+ details::Constructor<T, E>::copy(
+ other.storage_, storage_, details::err_tag());
+ ok_ = false;
+ }
+ }
+
+ ~Result()
+ {
+ if (ok_)
+ storage_.destroy(details::ok_tag());
+ else
+ storage_.destroy(details::err_tag());
+ }
+
+ bool isOk() const { return ok_; }
+
+ bool isErr() const { return !ok_; }
+
+ T expect(const char* str)
+ {
+ if (!isOk()) {
+ ::fprintf(stderr, "%s\n", str);
+ abort();
+ }
+ return expect_impl(std::is_same<T, void>());
+ }
+
+ template<typename Func>
+ auto map(Func func)
+ {
+ using return_type = decltype(func(T{}));
+
+ if (this->isOk()) {
+ auto value = std::move(this->storage().template get<T>());
+ auto res = func(std::move(value));
+ return Result<return_type, E>(
+ types::Ok<return_type>(std::move(res)));
+ }
+
+ return Result<return_type, E>(
+ types::Err<E>(this->storage().template get<E>()));
+ }
+
+ template<typename Func,
+ typename Ret
+ = Result<T,
+ typename details::ResultErrType<
+ typename details::result_of<Func>::type>::type>>
+ Ret mapError(Func func) const
+ {
+ return details::mapError(*this, func);
+ }
+
+ template<typename Func>
+ Result<void, E> then(Func func)
+ {
+ if (this->isOk()) {
+ func(std::move(this->storage().template get<T>()));
+
+ return Ok();
+ }
+
+ return Err(std::move(this->storage().template get<E>()));
+ }
+
+ template<typename Func>
+ Result<typename std::result_of<Func>::type, E> then(Func func)
+ {
+ if (this->isOk()) {
+ return Ok(func(std::move(this->storage().template get<T>())));
+ }
+
+ return Err(std::move(this->storage().template get<E>()));
+ }
+
+ template<typename Func>
+ void otherwise(Func func)
+ {
+ if (this->isOk()) {
+ return;
+ }
+
+ func(std::move(this->storage().template get<E>()));
+ }
+
+ template<typename Func,
+ typename Ret
+ = Result<T,
+ typename details::ResultErrType<
+ typename details::result_of<Func>::type>::type>>
+ Ret orElse(Func func) const
+ {
+ return details::orElse(*this, func);
+ }
+
+ storage_type& storage() { return storage_; }
+
+ const storage_type& storage() const { return storage_; }
+
+ template<typename U = T>
+ typename std::enable_if<!std::is_same<U, void>::value, T>::type unwrapOr(
+ const U& defaultValue) const
+ {
+ if (isOk()) {
+ return storage().template get<T>();
+ }
+ return defaultValue;
+ }
+
+ template<typename Func>
+ auto unwrapOrElse(Func func) const
+ {
+ if (isOk()) {
+ return storage().template get<T>();
+ }
+ return func(this->storage().template get<E>());
+ }
+
+ template<typename U = T>
+ typename std::enable_if<!std::is_same<U, void>::value, U>::type unwrap()
+ const
+ {
+ if (isOk()) {
+ return std::move(storage().template get<U>());
+ }
+
+ ::fprintf(stderr, "Attempting to unwrap an error Result\n");
+ abort();
+ }
+
+ template<typename U = T>
+ typename std::enable_if<!std::is_same<U, void>::value, U>::type unwrap()
+ {
+ if (isOk()) {
+ return std::move(storage().template get<U>());
+ }
+
+ ::fprintf(stderr, "Attempting to unwrap an error Result\n");
+ abort();
+ }
+
+ template<typename U = T>
+ typename std::enable_if<std::is_same<U, void>::value, U>::type unwrap()
+ const
+ {
+ if (isOk()) {
+ return;
+ }
+
+ ::fprintf(stderr, "Attempting to unwrap an error Result\n");
+ abort();
+ }
+
+ E unwrapErr() const
+ {
+ if (isErr()) {
+ return storage().template get<E>();
+ }
+
+ ::fprintf(stderr, "Attempting to unwrapErr an ok Result\n");
+ abort();
+ }
+
+private:
+ T expect_impl(std::true_type) const {}
+ T expect_impl(std::false_type)
+ {
+ return std::move(storage_.template get<T>());
+ }
+
+ bool ok_;
+ storage_type storage_;
+};
+
+template<typename T, typename E>
+bool
+operator==(const Result<T, E>& lhs, const Result<T, E>& rhs)
+{
+ static_assert(local_concept::EqualityComparable<T>::value,
+ "T must be EqualityComparable for Result to be comparable");
+ static_assert(local_concept::EqualityComparable<E>::value,
+ "E must be EqualityComparable for Result to be comparable");
+
+ if (lhs.isOk() && rhs.isOk()) {
+ return lhs.storage().template get<T>()
+ == rhs.storage().template get<T>();
+ }
+ if (lhs.isErr() && rhs.isErr()) {
+ return lhs.storage().template get<E>()
+ == rhs.storage().template get<E>();
+ }
+}
+
+template<typename T, typename E>
+bool
+operator==(const Result<T, E>& lhs, types::Ok<T> ok)
+{
+ static_assert(local_concept::EqualityComparable<T>::value,
+ "T must be EqualityComparable for Result to be comparable");
+
+ if (!lhs.isOk())
+ return false;
+
+ return lhs.storage().template get<T>() == ok.val;
+}
+
+template<typename E>
+bool
+operator==(const Result<void, E>& lhs, types::Ok<void>)
+{
+ return lhs.isOk();
+}
+
+template<typename T, typename E>
+bool
+operator==(const Result<T, E>& lhs, types::Err<E> err)
+{
+ static_assert(local_concept::EqualityComparable<E>::value,
+ "E must be EqualityComparable for Result to be comparable");
+ if (!lhs.isErr())
+ return false;
+
+ return lhs.storage().template get<E>() == err.val;
+}
+
+#define TRY(...) \
+ ({ \
+ auto res = __VA_ARGS__; \
+ if (!res.isOk()) { \
+ typedef typename ::details::ResultErrType<decltype(res)>::type E; \
+ return ::types::Err<E>(res.storage().template get<E>()); \
+ } \
+ res.unwrap(); \
+ })
diff --git a/src/base/snippet_highlighters.cc b/src/base/snippet_highlighters.cc
new file mode 100644
index 0000000..058fa41
--- /dev/null
+++ b/src/base/snippet_highlighters.cc
@@ -0,0 +1,344 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "snippet_highlighters.hh"
+
+#include "attr_line.builder.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "view_curses.hh"
+
+namespace lnav {
+namespace snippets {
+
+static bool
+is_bracket(const std::string& str, int index, bool is_lit)
+{
+ if (index == 0) {
+ return true;
+ }
+
+ if (is_lit && str[index - 1] == '\\') {
+ return true;
+ }
+ if (!is_lit && str[index - 1] != '\\') {
+ return true;
+ }
+ return false;
+}
+
+static void
+find_matching_bracket(
+ attr_line_t& al, int x, line_range sub, char left, char right)
+{
+ bool is_lit = (left == 'Q');
+ attr_line_builder alb(al);
+ const auto& line = al.get_string();
+ int depth = 0;
+
+ if (x < sub.lr_start || x > sub.lr_end) {
+ return;
+ }
+
+ if (line[x] == right && is_bracket(line, x, is_lit)) {
+ for (int lpc = x - 1; lpc >= sub.lr_start; lpc--) {
+ if (line[lpc] == right && is_bracket(line, lpc, is_lit)) {
+ depth += 1;
+ } else if (line[lpc] == left && is_bracket(line, lpc, is_lit)) {
+ if (depth == 0) {
+ alb.overlay_attr_for_char(
+ lpc, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr_for_char(lpc,
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ }
+ depth -= 1;
+ }
+ }
+ }
+
+ if (line[x] == left && is_bracket(line, x, is_lit)) {
+ for (int lpc = x + 1; lpc < sub.lr_end; lpc++) {
+ if (line[lpc] == left && is_bracket(line, lpc, is_lit)) {
+ depth += 1;
+ } else if (line[lpc] == right && is_bracket(line, lpc, is_lit)) {
+ if (depth == 0) {
+ alb.overlay_attr_for_char(
+ lpc, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr_for_char(lpc,
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ }
+ depth -= 1;
+ }
+ }
+ }
+
+ nonstd::optional<int> first_left;
+
+ depth = 0;
+
+ for (auto lpc = sub.lr_start; lpc < sub.lr_end; lpc++) {
+ if (line[lpc] == left && is_bracket(line, lpc, is_lit)) {
+ depth += 1;
+ if (!first_left) {
+ first_left = lpc;
+ }
+ } else if (line[lpc] == right && is_bracket(line, lpc, is_lit)) {
+ if (depth > 0) {
+ depth -= 1;
+ } else {
+ auto lr = line_range(is_lit ? lpc - 1 : lpc, lpc + 1);
+ alb.overlay_attr(
+ lr, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_ERROR));
+ }
+ }
+ }
+
+ if (depth > 0) {
+ auto lr
+ = line_range(is_lit ? first_left.value() - 1 : first_left.value(),
+ first_left.value() + 1);
+ alb.overlay_attr(lr, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_ERROR));
+ }
+}
+
+static bool
+check_re_prev(const std::string& line, int x)
+{
+ bool retval = false;
+
+ if ((x > 0 && line[x - 1] != ')' && line[x - 1] != ']' && line[x - 1] != '*'
+ && line[x - 1] != '?' && line[x - 1] != '+')
+ && (x < 2 || line[x - 2] != '\\'))
+ {
+ retval = true;
+ }
+
+ return retval;
+}
+
+static char
+safe_read(const std::string& str, std::string::size_type index)
+{
+ if (index < str.length()) {
+ return str.at(index);
+ }
+
+ return 0;
+}
+
+void
+regex_highlighter(attr_line_t& al, int x, line_range sub)
+{
+ static const char* brackets[] = {
+ "[]",
+ "{}",
+ "()",
+ "QE",
+
+ nullptr,
+ };
+
+ const auto& line = al.get_string();
+ attr_line_builder alb(al);
+ bool backslash_is_quoted = false;
+
+ for (auto lpc = sub.lr_start; lpc < sub.lr_end; lpc++) {
+ if (lpc == 0 || line[lpc - 1] != '\\') {
+ switch (line[lpc]) {
+ case '^':
+ case '$':
+ case '*':
+ case '+':
+ case '|':
+ case '.':
+ alb.overlay_attr_for_char(
+ lpc, VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+
+ if ((line[lpc] == '*' || line[lpc] == '+')
+ && check_re_prev(line, lpc))
+ {
+ alb.overlay_attr_for_char(
+ lpc - 1, VC_ROLE.value(role_t::VCR_RE_REPEAT));
+ }
+ break;
+ case '?': {
+ struct line_range lr(lpc, lpc + 1);
+
+ if (lpc == sub.lr_start || (lpc - sub.lr_start) == 0) {
+ alb.overlay_attr_for_char(
+ lpc,
+ VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr_for_char(
+ lpc, VC_ROLE.value(role_t::VCR_ERROR));
+ } else if (line[lpc - 1] == '(') {
+ switch (line[lpc + 1]) {
+ case ':':
+ case '!':
+ case '#':
+ lr.lr_end += 1;
+ break;
+ }
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_OK));
+ if (line[lpc + 1] == '<') {
+ alb.overlay_attr(
+ line_range(lpc + 1, lpc + 2),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ }
+ } else {
+ alb.overlay_attr(lr,
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+
+ if (check_re_prev(line, lpc)) {
+ alb.overlay_attr_for_char(
+ lpc - 1, VC_ROLE.value(role_t::VCR_RE_REPEAT));
+ }
+ }
+ break;
+ }
+ case '>': {
+ static const auto CAP_RE
+ = lnav::pcre2pp::code::from_const(R"(\(\?\<\w+$)");
+
+ auto capture_start
+ = string_fragment::from_str_range(
+ line, sub.lr_start, lpc)
+ .find_left_boundary(lpc - sub.lr_start - 1,
+ string_fragment::tag1{'('});
+
+ auto cap_find_res
+ = CAP_RE.find_in(capture_start).ignore_error();
+
+ if (cap_find_res) {
+ alb.overlay_attr(
+ line_range(capture_start.sf_begin
+ + cap_find_res->f_all.sf_begin + 3,
+ capture_start.sf_begin
+ + cap_find_res->f_all.sf_end),
+ VC_ROLE.value(role_t::VCR_IDENTIFIER));
+ alb.overlay_attr(line_range(lpc, lpc + 1),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ }
+ break;
+ }
+
+ case '(':
+ case ')':
+ case '{':
+ case '}':
+ case '[':
+ case ']':
+ alb.overlay_attr_for_char(lpc,
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ }
+ }
+ if (lpc > 0 && line[lpc - 1] == '\\') {
+ if (backslash_is_quoted) {
+ backslash_is_quoted = false;
+ continue;
+ }
+ switch (line[lpc]) {
+ case '\\':
+ backslash_is_quoted = true;
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ break;
+ case 'd':
+ case 'D':
+ case 'h':
+ case 'H':
+ case 'N':
+ case 'R':
+ case 's':
+ case 'S':
+ case 'v':
+ case 'V':
+ case 'w':
+ case 'W':
+ case 'X':
+
+ case 'A':
+ case 'b':
+ case 'B':
+ case 'G':
+ case 'Z':
+ case 'z':
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_SYMBOL));
+ break;
+ case ' ':
+ alb.overlay_attr(
+ line_range(lpc - 1, lpc + 1),
+ VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_ERROR));
+ break;
+ case '0':
+ case 'x':
+ if (safe_read(line, lpc + 1) == '{') {
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ } else if (isdigit(safe_read(line, lpc + 1))
+ && isdigit(safe_read(line, lpc + 2)))
+ {
+ alb.overlay_attr(line_range(lpc - 1, lpc + 3),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ } else {
+ alb.overlay_attr(
+ line_range(lpc - 1, lpc + 1),
+ VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_ERROR));
+ }
+ break;
+ case 'Q':
+ case 'E':
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ default:
+ if (isdigit(line[lpc])) {
+ alb.overlay_attr(line_range(lpc - 1, lpc + 1),
+ VC_ROLE.value(role_t::VCR_RE_SPECIAL));
+ }
+ break;
+ }
+ }
+ }
+
+ for (int lpc = 0; brackets[lpc]; lpc++) {
+ find_matching_bracket(al, x, sub, brackets[lpc][0], brackets[lpc][1]);
+ }
+}
+
+} // namespace snippets
+} // namespace lnav
diff --git a/src/base/snippet_highlighters.hh b/src/base/snippet_highlighters.hh
new file mode 100644
index 0000000..0da6291
--- /dev/null
+++ b/src/base/snippet_highlighters.hh
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_snippet_highlighters_hh
+#define lnav_snippet_highlighters_hh
+
+#include "attr_line.hh"
+
+namespace lnav {
+namespace snippets {
+
+void regex_highlighter(attr_line_t& al, int x, line_range sub);
+
+} // namespace snippets
+} // namespace lnav
+
+#endif
diff --git a/src/base/string_attr_type.cc b/src/base/string_attr_type.cc
new file mode 100644
index 0000000..9a3950b
--- /dev/null
+++ b/src/base/string_attr_type.cc
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "string_attr_type.hh"
+
+#include "config.h"
+
+string_attr_type<void> SA_ORIGINAL_LINE("original_line");
+string_attr_type<void> SA_BODY("body");
+string_attr_type<void> SA_HIDDEN("hidden");
+string_attr_type<const intern_string_t> SA_FORMAT("format");
+string_attr_type<void> SA_REMOVED("removed");
+string_attr_type<void> SA_PREFORMATTED("preformatted");
+string_attr_type<std::string> SA_INVALID("invalid");
+string_attr_type<std::string> SA_ERROR("error");
+string_attr_type<int64_t> SA_LEVEL("level");
+string_attr_type<string_fragment> SA_ORIGIN("origin");
+string_attr_type<int64_t> SA_ORIGIN_OFFSET("origin-offset");
+
+string_attr_type<role_t> VC_ROLE("role");
+string_attr_type<role_t> VC_ROLE_FG("role-fg");
+string_attr_type<text_attrs> VC_STYLE("style");
+string_attr_type<int64_t> VC_GRAPHIC("graphic");
+string_attr_type<int64_t> VC_FOREGROUND("foreground");
+string_attr_type<int64_t> VC_BACKGROUND("background");
diff --git a/src/base/string_attr_type.hh b/src/base/string_attr_type.hh
new file mode 100644
index 0000000..5bff345
--- /dev/null
+++ b/src/base/string_attr_type.hh
@@ -0,0 +1,679 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_string_attr_type_hh
+#define lnav_string_attr_type_hh
+
+#include <string>
+#include <utility>
+
+#include <stdint.h>
+
+#include "base/intern_string.hh"
+#include "mapbox/variant.hpp"
+
+class logfile;
+struct bookmark_metadata;
+
+/** Roles that can be mapped to curses attributes using attrs_for_role() */
+enum class role_t : int32_t {
+ VCR_NONE = -1,
+
+ VCR_TEXT, /*< Raw text. */
+ VCR_IDENTIFIER,
+ VCR_SEARCH, /*< A search hit. */
+ VCR_OK,
+ VCR_INFO,
+ VCR_ERROR, /*< An error message. */
+ VCR_WARNING, /*< A warning message. */
+ VCR_ALT_ROW, /*< Highlight for alternating rows in a list */
+ VCR_HIDDEN,
+ VCR_CURSOR_LINE,
+ VCR_ADJUSTED_TIME,
+ VCR_SKEWED_TIME,
+ VCR_OFFSET_TIME,
+ VCR_INVALID_MSG,
+ VCR_STATUS, /*< Normal status line text. */
+ VCR_WARN_STATUS,
+ VCR_ALERT_STATUS, /*< Alert status line text. */
+ VCR_ACTIVE_STATUS, /*< */
+ VCR_ACTIVE_STATUS2, /*< */
+ VCR_STATUS_TITLE,
+ VCR_STATUS_SUBTITLE,
+ VCR_STATUS_INFO,
+ VCR_STATUS_STITCH_TITLE_TO_SUB,
+ VCR_STATUS_STITCH_SUB_TO_TITLE,
+ VCR_STATUS_STITCH_SUB_TO_NORMAL,
+ VCR_STATUS_STITCH_NORMAL_TO_SUB,
+ VCR_STATUS_STITCH_TITLE_TO_NORMAL,
+ VCR_STATUS_STITCH_NORMAL_TO_TITLE,
+ VCR_STATUS_TITLE_HOTKEY,
+ VCR_STATUS_DISABLED_TITLE,
+ VCR_STATUS_HOTKEY,
+ VCR_INACTIVE_STATUS,
+ VCR_INACTIVE_ALERT_STATUS,
+ VCR_SCROLLBAR,
+ VCR_SCROLLBAR_ERROR,
+ VCR_SCROLLBAR_WARNING,
+ VCR_FOCUSED,
+ VCR_DISABLED_FOCUSED,
+ VCR_POPUP,
+ VCR_COLOR_HINT,
+
+ VCR_QUOTED_CODE,
+ VCR_CODE_BORDER,
+ VCR_KEYWORD,
+ VCR_STRING,
+ VCR_COMMENT,
+ VCR_DOC_DIRECTIVE,
+ VCR_VARIABLE,
+ VCR_SYMBOL,
+ VCR_NUMBER,
+ VCR_RE_SPECIAL,
+ VCR_RE_REPEAT,
+ VCR_FILE,
+
+ VCR_DIFF_DELETE, /*< Deleted line in a diff. */
+ VCR_DIFF_ADD, /*< Added line in a diff. */
+ VCR_DIFF_SECTION, /*< Section marker in a diff. */
+
+ VCR_LOW_THRESHOLD,
+ VCR_MED_THRESHOLD,
+ VCR_HIGH_THRESHOLD,
+
+ VCR_H1,
+ VCR_H2,
+ VCR_H3,
+ VCR_H4,
+ VCR_H5,
+ VCR_H6,
+
+ VCR_HR,
+ VCR_HYPERLINK,
+ VCR_LIST_GLYPH,
+ VCR_BREADCRUMB,
+ VCR_TABLE_BORDER,
+ VCR_TABLE_HEADER,
+ VCR_QUOTE_BORDER,
+ VCR_QUOTED_TEXT,
+ VCR_FOOTNOTE_BORDER,
+ VCR_FOOTNOTE_TEXT,
+ VCR_SNIPPET_BORDER,
+
+ VCR__MAX
+};
+
+struct text_attrs {
+ bool empty() const
+ {
+ return this->ta_attrs == 0 && !this->ta_fg_color && !this->ta_bg_color;
+ }
+
+ text_attrs operator|(const text_attrs& other) const
+ {
+ return text_attrs{
+ this->ta_attrs | other.ta_attrs,
+ this->ta_fg_color ? this->ta_fg_color : other.ta_fg_color,
+ this->ta_bg_color ? this->ta_bg_color : other.ta_bg_color,
+ };
+ }
+
+ bool operator==(const text_attrs& other) const
+ {
+ return this->ta_attrs == other.ta_attrs
+ && this->ta_fg_color == other.ta_fg_color
+ && this->ta_bg_color == other.ta_bg_color;
+ }
+
+ int32_t ta_attrs{0};
+ nonstd::optional<short> ta_fg_color;
+ nonstd::optional<short> ta_bg_color;
+};
+
+using string_attr_value = mapbox::util::variant<int64_t,
+ role_t,
+ text_attrs,
+ const intern_string_t,
+ std::string,
+ std::shared_ptr<logfile>,
+ bookmark_metadata*,
+ timespec,
+ string_fragment>;
+
+class string_attr_type_base {
+public:
+ explicit string_attr_type_base(const char* name) noexcept : sat_name(name)
+ {
+ }
+
+ const char* const sat_name;
+};
+
+using string_attr_pair
+ = std::pair<const string_attr_type_base*, string_attr_value>;
+
+template<typename T>
+class string_attr_type : public string_attr_type_base {
+public:
+ using value_type = T;
+
+ explicit string_attr_type(const char* name) noexcept
+ : string_attr_type_base(name)
+ {
+ }
+
+ template<typename U = T>
+ std::enable_if_t<!std::is_void<U>::value, string_attr_pair> value(
+ const U& val) const
+ {
+ return std::make_pair(this, val);
+ }
+
+ template<typename U = T>
+ std::enable_if_t<std::is_void<U>::value, string_attr_pair> value() const
+ {
+ return std::make_pair(this, string_attr_value{});
+ }
+};
+
+extern string_attr_type<void> SA_ORIGINAL_LINE;
+extern string_attr_type<void> SA_BODY;
+extern string_attr_type<void> SA_HIDDEN;
+extern string_attr_type<const intern_string_t> SA_FORMAT;
+extern string_attr_type<void> SA_REMOVED;
+extern string_attr_type<void> SA_PREFORMATTED;
+extern string_attr_type<std::string> SA_INVALID;
+extern string_attr_type<std::string> SA_ERROR;
+extern string_attr_type<int64_t> SA_LEVEL;
+extern string_attr_type<string_fragment> SA_ORIGIN;
+extern string_attr_type<int64_t> SA_ORIGIN_OFFSET;
+
+extern string_attr_type<role_t> VC_ROLE;
+extern string_attr_type<role_t> VC_ROLE_FG;
+extern string_attr_type<text_attrs> VC_STYLE;
+extern string_attr_type<int64_t> VC_GRAPHIC;
+extern string_attr_type<int64_t> VC_FOREGROUND;
+extern string_attr_type<int64_t> VC_BACKGROUND;
+
+namespace lnav {
+
+namespace string {
+namespace attrs {
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+preformatted(S str)
+{
+ return std::make_pair(std::move(str), SA_PREFORMATTED.template value());
+}
+
+} // namespace attrs
+} // namespace string
+
+namespace roles {
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+error(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_ERROR));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+warning(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_WARNING));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+status(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_STATUS));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+inactive_status(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_INACTIVE_STATUS));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+status_title(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_STATUS_TITLE));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+ok(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_OK));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+file(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_FILE));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+symbol(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_SYMBOL));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+keyword(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_KEYWORD));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+variable(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_VARIABLE));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+number(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_NUMBER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+comment(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_COMMENT));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+identifier(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_IDENTIFIER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+hr(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_HR));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+hyperlink(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_HYPERLINK));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+list_glyph(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_LIST_GLYPH));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+breadcrumb(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_BREADCRUMB));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+quoted_code(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_QUOTED_CODE));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+code_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_CODE_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+snippet_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_SNIPPET_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+table_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_TABLE_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+table_header(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_TABLE_HEADER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+quote_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_QUOTE_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+quoted_text(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_QUOTED_TEXT));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+footnote_border(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_FOOTNOTE_BORDER));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+footnote_text(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_FOOTNOTE_TEXT));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h1(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H1));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h2(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H2));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h3(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H3));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h4(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H4));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h5(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H5));
+}
+
+template<typename S>
+inline std::pair<S, string_attr_pair>
+h6(S str)
+{
+ return std::make_pair(std::move(str),
+ VC_ROLE.template value(role_t::VCR_H6));
+}
+
+namespace literals {
+
+inline std::pair<std::string, string_attr_pair> operator"" _ok(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_OK));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _error(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_ERROR));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _info(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_INFO));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _symbol(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_SYMBOL));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _keyword(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_KEYWORD));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _variable(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_VARIABLE));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _comment(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_COMMENT));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _hotkey(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_STATUS_HOTKEY));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h1(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H1));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h2(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H2));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h3(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H3));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h4(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H4));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _h5(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_H5));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _hr(const char* str,
+ std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_HR));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _hyperlink(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_HYPERLINK));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _list_glyph(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_LIST_GLYPH));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _breadcrumb(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_BREADCRUMB));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _quoted_code(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_QUOTED_CODE));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _code_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_CODE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _table_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_TABLE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _quote_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_QUOTE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _quoted_text(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_QUOTED_TEXT));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _footnote_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_FOOTNOTE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _footnote_text(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_FOOTNOTE_BORDER));
+}
+
+inline std::pair<std::string, string_attr_pair> operator"" _snippet_border(
+ const char* str, std::size_t len)
+{
+ return std::make_pair(std::string(str, len),
+ VC_ROLE.template value(role_t::VCR_SNIPPET_BORDER));
+}
+
+} // namespace literals
+
+} // namespace roles
+} // namespace lnav
+
+#endif
diff --git a/src/base/string_util.cc b/src/base/string_util.cc
new file mode 100644
index 0000000..8af686c
--- /dev/null
+++ b/src/base/string_util.cc
@@ -0,0 +1,304 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+#include <iterator>
+#include <regex>
+#include <sstream>
+
+#include "string_util.hh"
+
+#include "config.h"
+#include "is_utf8.hh"
+#include "lnav_log.hh"
+
+void
+scrub_to_utf8(char* buffer, size_t length)
+{
+ while (true) {
+ auto frag = string_fragment::from_bytes(buffer, length);
+ auto scan_res = is_utf8(frag);
+
+ if (scan_res.is_valid()) {
+ break;
+ }
+ for (size_t lpc = 0; lpc < scan_res.usr_faulty_bytes; lpc++) {
+ buffer[scan_res.usr_valid_frag.sf_end + lpc] = '?';
+ }
+ }
+}
+
+void
+quote_content(auto_buffer& buf, const string_fragment& sf, char quote_char)
+{
+ for (char ch : sf) {
+ if (ch == quote_char) {
+ buf.push_back('\\').push_back(ch);
+ continue;
+ }
+ switch (ch) {
+ case '\\':
+ buf.push_back('\\').push_back('\\');
+ break;
+ case '\n':
+ buf.push_back('\\').push_back('n');
+ break;
+ case '\t':
+ buf.push_back('\\').push_back('t');
+ break;
+ case '\r':
+ buf.push_back('\\').push_back('r');
+ break;
+ case '\a':
+ buf.push_back('\\').push_back('a');
+ break;
+ case '\b':
+ buf.push_back('\\').push_back('b');
+ break;
+ default:
+ buf.push_back(ch);
+ break;
+ }
+ }
+}
+
+size_t
+unquote_content(char* dst, const char* str, size_t len, char quote_char)
+{
+ size_t index = 0;
+
+ for (size_t lpc = 0; lpc < len; lpc++, index++) {
+ dst[index] = str[lpc];
+ if (str[lpc] == quote_char) {
+ lpc += 1;
+ } else if (str[lpc] == '\\' && (lpc + 1) < len) {
+ switch (str[lpc + 1]) {
+ case 'n':
+ dst[index] = '\n';
+ break;
+ case 'r':
+ dst[index] = '\r';
+ break;
+ case 't':
+ dst[index] = '\t';
+ break;
+ default:
+ dst[index] = str[lpc + 1];
+ break;
+ }
+ lpc += 1;
+ }
+ }
+ dst[index] = '\0';
+
+ return index;
+}
+
+size_t
+unquote(char* dst, const char* str, size_t len)
+{
+ if (str[0] == 'r' || str[0] == 'u') {
+ str += 1;
+ len -= 1;
+ }
+ char quote_char = str[0];
+
+ require(str[0] == '\'' || str[0] == '"');
+
+ return unquote_content(dst, &str[1], len - 2, quote_char);
+}
+
+size_t
+unquote_w3c(char* dst, const char* str, size_t len)
+{
+ size_t index = 0;
+
+ require(str[0] == '\'' || str[0] == '"');
+
+ for (size_t lpc = 1; lpc < (len - 1); lpc++, index++) {
+ dst[index] = str[lpc];
+ if (str[lpc] == '"') {
+ lpc += 1;
+ }
+ }
+ dst[index] = '\0';
+
+ return index;
+}
+
+void
+truncate_to(std::string& str, size_t max_char_len)
+{
+ static const std::string ELLIPSIS = "\u22ef";
+
+ if (str.length() < max_char_len) {
+ return;
+ }
+
+ auto str_char_len_res = utf8_string_length(str);
+
+ if (str_char_len_res.isErr()) {
+ // XXX
+ return;
+ }
+
+ auto str_char_len = str_char_len_res.unwrap();
+ if (str_char_len <= max_char_len) {
+ return;
+ }
+
+ if (max_char_len < 3) {
+ str = ELLIPSIS;
+ return;
+ }
+
+ auto chars_to_remove = (str_char_len - max_char_len) + 1;
+ auto midpoint = str_char_len / 2;
+ auto chars_to_keep_at_front = midpoint - (chars_to_remove / 2);
+ auto bytes_to_keep_at_front
+ = utf8_char_to_byte_index(str, chars_to_keep_at_front);
+ auto remove_up_to_bytes = utf8_char_to_byte_index(
+ str, chars_to_keep_at_front + chars_to_remove);
+ auto bytes_to_remove = remove_up_to_bytes - bytes_to_keep_at_front;
+ str.erase(bytes_to_keep_at_front, bytes_to_remove);
+ str.insert(bytes_to_keep_at_front, ELLIPSIS);
+}
+
+bool
+is_url(const std::string& fn)
+{
+ static const auto url_re = std::regex("^(file|https?|ftps?|scp|sftp):.*");
+
+ return std::regex_match(fn, url_re);
+}
+
+size_t
+abbreviate_str(char* str, size_t len, size_t max_len)
+{
+ size_t last_start = 1;
+
+ if (len < max_len) {
+ return len;
+ }
+
+ for (size_t index = 0; index < len; index++) {
+ switch (str[index]) {
+ case '.':
+ case '-':
+ case '/':
+ case ':':
+ memmove(&str[last_start], &str[index], len - index);
+ len -= (index - last_start);
+ index = last_start + 1;
+ last_start = index + 1;
+
+ if (len < max_len) {
+ return len;
+ }
+ break;
+ }
+ }
+
+ return len;
+}
+
+void
+split_ws(const std::string& str, std::vector<std::string>& toks_out)
+{
+ std::stringstream ss(str);
+ std::string buf;
+
+ while (ss >> buf) {
+ toks_out.push_back(buf);
+ }
+}
+
+std::string
+repeat(const std::string& input, size_t num)
+{
+ std::ostringstream os;
+ std::fill_n(std::ostream_iterator<std::string>(os), num, input);
+ return os.str();
+}
+
+std::string
+center_str(const std::string& subject, size_t width)
+{
+ std::string retval = subject;
+
+ truncate_to(retval, width);
+
+ auto retval_length = utf8_string_length(retval).unwrapOr(retval.length());
+ auto total_fill = width - retval_length;
+ auto before = total_fill / 2;
+ auto after = total_fill - before;
+
+ retval.insert(0, before, ' ');
+ retval.append(after, ' ');
+
+ return retval;
+}
+
+bool
+is_blank(const std::string& str)
+{
+ return std::all_of(
+ str.begin(), str.end(), [](const auto ch) { return isspace(ch); });
+}
+
+std::string
+scrub_ws(const char* in)
+{
+ static const std::string TAB_SYMBOL = "\u21e5";
+ static const std::string LF_SYMBOL = "\u240a";
+ static const std::string CR_SYMBOL = "\u240d";
+
+ std::string retval;
+
+ for (size_t lpc = 0; in[lpc]; lpc++) {
+ auto ch = in[lpc];
+
+ switch (ch) {
+ case '\t':
+ retval.append(TAB_SYMBOL);
+ break;
+ case '\n':
+ retval.append(LF_SYMBOL);
+ break;
+ case '\r':
+ retval.append(CR_SYMBOL);
+ break;
+ default:
+ retval.append(1, ch);
+ break;
+ }
+ }
+
+ return retval;
+}
diff --git a/src/base/string_util.hh b/src/base/string_util.hh
new file mode 100644
index 0000000..73a8b87
--- /dev/null
+++ b/src/base/string_util.hh
@@ -0,0 +1,232 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_string_util_hh
+#define lnav_string_util_hh
+
+#include <string>
+#include <vector>
+
+#include <string.h>
+
+#include "auto_mem.hh"
+#include "intern_string.hh"
+#include "ww898/cp_utf8.hpp"
+
+void scrub_to_utf8(char* buffer, size_t length);
+
+inline bool
+is_line_ending(char ch)
+{
+ return ch == '\r' || ch == '\n';
+}
+
+void quote_content(auto_buffer& buf,
+ const string_fragment& sf,
+ char quote_char);
+
+size_t unquote_content(char* dst, const char* str, size_t len, char quote_char);
+
+size_t unquote(char* dst, const char* str, size_t len);
+
+size_t unquote_w3c(char* dst, const char* str, size_t len);
+
+inline bool
+startswith(const char* str, const char* prefix)
+{
+ return strncmp(str, prefix, strlen(prefix)) == 0;
+}
+
+inline bool
+startswith(const std::string& str, const char* prefix)
+{
+ return startswith(str.c_str(), prefix);
+}
+
+inline bool
+startswith(const std::string& str, const std::string& prefix)
+{
+ return startswith(str.c_str(), prefix.c_str());
+}
+
+inline bool
+endswith(const char* str, const char* suffix)
+{
+ size_t len = strlen(str), suffix_len = strlen(suffix);
+
+ if (suffix_len > len) {
+ return false;
+ }
+
+ return strcmp(&str[len - suffix_len], suffix) == 0;
+}
+
+template<int N>
+inline bool
+endswith(const std::string& str, const char (&suffix)[N])
+{
+ if (N - 1 > str.length()) {
+ return false;
+ }
+
+ return strcmp(&str[str.size() - (N - 1)], suffix) == 0;
+}
+
+void truncate_to(std::string& str, size_t max_char_len);
+
+std::string scrub_ws(const char* in);
+
+inline std::string
+trim(const std::string& str)
+{
+ std::string::size_type start, end;
+
+ for (start = 0; start < str.size() && isspace(str[start]); start++)
+ ;
+ for (end = str.size(); end > 0 && isspace(str[end - 1]); end--)
+ ;
+
+ return str.substr(start, end - start);
+}
+
+inline std::string
+rtrim(const std::string& str)
+{
+ std::string::size_type end;
+
+ for (end = str.size(); end > 0 && isspace(str[end - 1]); end--)
+ ;
+
+ return str.substr(0, end);
+}
+
+inline std::string
+tolower(const char* str)
+{
+ std::string retval;
+
+ for (int lpc = 0; str[lpc]; lpc++) {
+ retval.push_back(::tolower(str[lpc]));
+ }
+
+ return retval;
+}
+
+inline std::string
+tolower(const std::string& str)
+{
+ return tolower(str.c_str());
+}
+
+inline std::string
+toupper(const char* str)
+{
+ std::string retval;
+
+ for (int lpc = 0; str[lpc]; lpc++) {
+ retval.push_back(::toupper(str[lpc]));
+ }
+
+ return retval;
+}
+
+inline std::string
+toupper(const std::string& str)
+{
+ return toupper(str.c_str());
+}
+
+inline ssize_t
+utf8_char_to_byte_index(const std::string& str, ssize_t ch_index)
+{
+ ssize_t retval = 0;
+
+ while (ch_index > 0) {
+ auto ch_len
+ = ww898::utf::utf8::char_size([&str, retval]() {
+ return std::make_pair(str[retval], str.length() - retval - 1);
+ }).unwrapOr(1);
+
+ retval += ch_len;
+ ch_index -= 1;
+ }
+
+ return retval;
+}
+
+inline Result<size_t, const char*>
+utf8_string_length(const char* str, ssize_t len = -1)
+{
+ size_t retval = 0;
+
+ if (len == -1) {
+ len = strlen(str);
+ }
+
+ for (ssize_t byte_index = 0; byte_index < len;) {
+ auto ch_size
+ = TRY(ww898::utf::utf8::char_size([str, len, byte_index]() {
+ return std::make_pair(str[byte_index], len - byte_index);
+ }));
+ byte_index += ch_size;
+ retval += 1;
+ }
+
+ return Ok(retval);
+}
+
+inline Result<size_t, const char*>
+utf8_string_length(const std::string& str)
+{
+ return utf8_string_length(str.c_str(), str.length());
+}
+
+bool is_url(const std::string& fn);
+
+bool is_blank(const std::string& str);
+
+size_t abbreviate_str(char* str, size_t len, size_t max_len);
+
+void split_ws(const std::string& str, std::vector<std::string>& toks_out);
+
+std::string repeat(const std::string& input, size_t num);
+
+std::string center_str(const std::string& subject, size_t width);
+
+inline std::string
+on_blank(const std::string& str, const std::string& def)
+{
+ if (is_blank(str)) {
+ return def;
+ }
+
+ return str;
+}
+
+#endif
diff --git a/src/base/string_util.tests.cc b/src/base/string_util.tests.cc
new file mode 100644
index 0000000..98cf5c8
--- /dev/null
+++ b/src/base/string_util.tests.cc
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <iostream>
+
+#include "base/string_util.hh"
+
+#include "base/strnatcmp.h"
+#include "config.h"
+#include "doctest/doctest.h"
+
+TEST_CASE("endswith")
+{
+ std::string hw("hello");
+
+ CHECK(endswith(hw, "f") == false);
+ CHECK(endswith(hw, "lo") == true);
+}
+
+TEST_CASE("truncate_to")
+{
+ const std::string orig = "0123456789abcdefghijklmnopqrstuvwxyz";
+ std::string str;
+
+ truncate_to(str, 10);
+ CHECK(str == "");
+ str = "abc";
+ truncate_to(str, 10);
+ CHECK(str == "abc");
+ str = orig;
+ truncate_to(str, 10);
+ CHECK(str == "01234\u22efwxyz");
+ str = orig;
+ truncate_to(str, 1);
+ CHECK(str == "\u22ef");
+ str = orig;
+ truncate_to(str, 2);
+ CHECK(str == "\u22ef");
+ str = orig;
+ truncate_to(str, 3);
+ CHECK(str == "0\u22efz");
+ str = orig;
+ truncate_to(str, 4);
+ CHECK(str == "01\u22efz");
+ str = orig;
+ truncate_to(str, 5);
+ CHECK(str == "01\u22efyz");
+}
+
+TEST_CASE("strnatcmp")
+{
+ {
+ constexpr const char* n1 = "010";
+ constexpr const char* n2 = "020";
+
+ CHECK(strnatcmp(strlen(n1), n1, strlen(n2), n2) < 0);
+ }
+ {
+ constexpr const char* n1 = "2";
+ constexpr const char* n2 = "10";
+
+ CHECK(strnatcmp(strlen(n1), n1, strlen(n2), n2) < 0);
+ }
+}
diff --git a/src/base/strnatcmp.c b/src/base/strnatcmp.c
new file mode 100644
index 0000000..6773164
--- /dev/null
+++ b/src/base/strnatcmp.c
@@ -0,0 +1,302 @@
+/* -*- mode: c; c-file-style: "k&r" -*-
+
+ strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
+ Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+
+/* partial change history:
+ *
+ * 2004-10-10 mbp: Lift out character type dependencies into macros.
+ *
+ * Eric Sosman pointed out that ctype functions take a parameter whose
+ * value must be that of an unsigned int, even on platforms that have
+ * negative chars in their default char type.
+ */
+
+#include <assert.h>
+#include <ctype.h>
+
+#include "strnatcmp.h"
+
+
+/* These are defined as macros to make it easier to adapt this code to
+ * different characters types or comparison functions. */
+static inline int
+nat_isdigit(nat_char a)
+{
+ return isdigit((unsigned char) a);
+}
+
+
+static inline int
+nat_isspace(nat_char a)
+{
+ return isspace((unsigned char) a);
+}
+
+
+static inline nat_char
+nat_toupper(nat_char a)
+{
+ return toupper((unsigned char) a);
+}
+
+
+
+static int
+compare_right(int a_len, nat_char const *a, int b_len, nat_char const *b, int *len_out)
+{
+ int bias = 0;
+
+ /* The longest run of digits wins. That aside, the greatest
+ value wins, but we can't know that it will until we've scanned
+ both numbers to know that they have the same magnitude, so we
+ remember it in BIAS. */
+ for (;; a++, b++, a_len--, b_len--, (*len_out)++) {
+ if (a_len == 0 && b_len == 0)
+ return bias;
+ if (a_len == 0)
+ return -1;
+ if (b_len == 0)
+ return 1;
+ if (!nat_isdigit(*a) && !nat_isdigit(*b))
+ return bias;
+ else if (!nat_isdigit(*a))
+ return -1;
+ else if (!nat_isdigit(*b))
+ return +1;
+ else if (*a < *b) {
+ if (!bias)
+ bias = -1;
+ } else if (*a > *b) {
+ if (!bias)
+ bias = +1;
+ } else if (!*a && !*b)
+ return bias;
+ }
+
+ return 0;
+}
+
+static int
+compare_left(int a_len, nat_char const *a, int b_len, nat_char const *b, int *len_out)
+{
+ /* Compare two left-aligned numbers: the first to have a
+ different value wins. */
+ for (;; a++, b++, a_len--, b_len--, (*len_out)++) {
+ if (a_len == 0 && b_len == 0)
+ return 0;
+ if (a_len == 0)
+ return -1;
+ if (b_len == 0)
+ return 1;
+ if (!nat_isdigit(*a) && !nat_isdigit(*b))
+ return 0;
+ else if (!nat_isdigit(*a))
+ return -1;
+ else if (!nat_isdigit(*b))
+ return +1;
+ else if (*a < *b)
+ return -1;
+ else if (*a > *b)
+ return +1;
+ }
+
+ return 0;
+}
+
+static int strnatcmp0(int a_len, nat_char const *a,
+ int b_len, nat_char const *b,
+ int fold_case)
+{
+ int ai, bi;
+ nat_char ca, cb;
+ int fractional, result;
+
+ assert(a && b);
+ ai = bi = 0;
+ while (1) {
+ if (ai >= a_len)
+ ca = 0;
+ else
+ ca = a[ai];
+ if (bi >= b_len)
+ cb = 0;
+ else
+ cb = b[bi];
+
+ /* skip over leading spaces or zeros */
+ while (nat_isspace(ca)) {
+ ai += 1;
+ if (ai >= a_len)
+ ca = 0;
+ else
+ ca = a[ai];
+ }
+
+ while (nat_isspace(cb)) {
+ bi += 1;
+ if (bi >= b_len)
+ cb = 0;
+ else
+ cb = b[bi];
+ }
+
+ /* process run of digits */
+ if (nat_isdigit(ca) && nat_isdigit(cb)) {
+ int num_len = 0;
+
+ fractional = (ca == '0' || cb == '0');
+
+ if (fractional) {
+ if ((result = compare_left(a_len - ai, a + ai, b_len - bi,
+ b + bi, &num_len)) != 0) {
+ return result;
+ }
+ } else {
+ if ((result = compare_right(a_len - ai, a + ai, b_len - bi,
+ b + bi, &num_len)) != 0) {
+ return result;
+ }
+ }
+
+ ai += num_len;
+ bi += num_len;
+ continue;
+ }
+
+ if (!ca && !cb) {
+ /* The strings compare the same. Perhaps the caller
+ will want to call strcmp to break the tie. */
+ return 0;
+ }
+
+ if (fold_case) {
+ ca = nat_toupper(ca);
+ cb = nat_toupper(cb);
+ }
+
+ if (ca < cb)
+ return -1;
+ else if (ca > cb)
+ return +1;
+
+ ++ai;
+ ++bi;
+ }
+}
+
+int ipv4cmp(int a_len, nat_char const *a,
+ int b_len, nat_char const *b,
+ int *res_out)
+{
+ int ai, bi;
+ nat_char ca, cb;
+ int fractional, result = 0;
+
+ assert(a && b);
+ ai = bi = 0;
+ while (result == 0) {
+ if (ai >= a_len)
+ ca = 0;
+ else
+ ca = a[ai];
+ if (bi >= b_len)
+ cb = 0;
+ else
+ cb = b[bi];
+
+ /* skip over leading spaces or zeros */
+ while (nat_isspace(ca)) {
+ ai += 1;
+ if (ai >= a_len)
+ ca = 0;
+ else
+ ca = a[ai];
+ }
+
+ while (nat_isspace(cb)) {
+ bi += 1;
+ if (bi >= b_len)
+ cb = 0;
+ else
+ cb = b[bi];
+ }
+
+ /* process run of digits */
+ if (nat_isdigit(ca) && nat_isdigit(cb)) {
+ int num_len = 0;
+
+ fractional = (ca == '0' || cb == '0');
+
+ if (fractional) {
+ result = compare_left(a_len - ai, a + ai, b_len - bi,
+ b + bi, &num_len);
+ } else {
+ result = compare_right(a_len - ai, a + ai, b_len - bi,
+ b + bi, &num_len);
+ }
+
+ ai += num_len;
+ bi += num_len;
+ continue;
+ }
+
+ if (!ca && !cb) {
+ /* The strings compare the same. Perhaps the caller
+ will want to call strcmp to break the tie. */
+ *res_out = result;
+ return 1;
+ }
+
+ if (ca != '.' || cb != '.') {
+ return 0;
+ }
+
+ ++ai;
+ ++bi;
+ }
+
+ for (; ai < a_len; ai++) {
+ if (!isdigit((unsigned char)a[ai]) || a[ai] != '.') {
+ return 0;
+ }
+ }
+
+ for (; bi < b_len; bi++) {
+ if (!isdigit((unsigned char)b[bi]) || b[bi] != '.') {
+ return 0;
+ }
+ }
+
+ *res_out = result;
+ return 1;
+}
+
+int strnatcmp(int a_len, nat_char const *a, int b_len, nat_char const *b)
+{
+ return strnatcmp0(a_len, a, b_len, b, 0);
+}
+
+/* Compare, recognizing numeric string and ignoring case. */
+int strnatcasecmp(int a_len, nat_char const *a, int b_len, nat_char const *b)
+{
+ return strnatcmp0(a_len, a, b_len, b, 1);
+}
diff --git a/src/base/strnatcmp.h b/src/base/strnatcmp.h
new file mode 100644
index 0000000..24b0c53
--- /dev/null
+++ b/src/base/strnatcmp.h
@@ -0,0 +1,41 @@
+/* -*- mode: c; c-file-style: "k&r" -*-
+
+ strnatcmp.c -- Perform 'natural order' comparisons of strings in C.
+ Copyright (C) 2000, 2004 by Martin Pool <mbp sourcefrog net>
+
+ This software is provided 'as-is', without any express or implied
+ warranty. In no event will the authors be held liable for any damages
+ arising from the use of this software.
+
+ Permission is granted to anyone to use this software for any purpose,
+ including commercial applications, and to alter it and redistribute it
+ freely, subject to the following restrictions:
+
+ 1. The origin of this software must not be misrepresented; you must not
+ claim that you wrote the original software. If you use this software
+ in a product, an acknowledgment in the product documentation would be
+ appreciated but is not required.
+ 2. Altered source versions must be plainly marked as such, and must not be
+ misrepresented as being the original software.
+ 3. This notice may not be removed or altered from any source distribution.
+*/
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* CUSTOMIZATION SECTION
+ *
+ * You can change this typedef, but must then also change the inline
+ * functions in strnatcmp.c */
+typedef char nat_char;
+
+int strnatcmp(int a_len, nat_char const *a, int b_len, nat_char const *b);
+
+int strnatcasecmp(int a_len, nat_char const *a, int b_len, nat_char const *b);
+
+int ipv4cmp(int a_len, nat_char const *a, int b_len, nat_char const *b, int *res_out);
+
+#ifdef __cplusplus
+}
+#endif
diff --git a/src/base/test_base.cc b/src/base/test_base.cc
new file mode 100644
index 0000000..654cd57
--- /dev/null
+++ b/src/base/test_base.cc
@@ -0,0 +1,33 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "doctest/doctest.h"
diff --git a/src/base/time_util.cc b/src/base/time_util.cc
new file mode 100644
index 0000000..0d46107
--- /dev/null
+++ b/src/base/time_util.cc
@@ -0,0 +1,239 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file time_util.cc
+ */
+
+#include <chrono>
+
+#include "time_util.hh"
+
+#include "config.h"
+
+namespace lnav {
+
+ssize_t
+strftime_rfc3339(
+ char* buffer, size_t buffer_size, lnav::time64_t tim, int millis, char sep)
+{
+ struct tm gmtm;
+ int year, month, index = 0;
+
+ secs2tm(tim, &gmtm);
+ year = gmtm.tm_year + 1900;
+ month = gmtm.tm_mon + 1;
+ buffer[index++] = '0' + ((year / 1000) % 10);
+ buffer[index++] = '0' + ((year / 100) % 10);
+ buffer[index++] = '0' + ((year / 10) % 10);
+ buffer[index++] = '0' + ((year / 1) % 10);
+ buffer[index++] = '-';
+ buffer[index++] = '0' + ((month / 10) % 10);
+ buffer[index++] = '0' + ((month / 1) % 10);
+ buffer[index++] = '-';
+ buffer[index++] = '0' + ((gmtm.tm_mday / 10) % 10);
+ buffer[index++] = '0' + ((gmtm.tm_mday / 1) % 10);
+ buffer[index++] = sep;
+ buffer[index++] = '0' + ((gmtm.tm_hour / 10) % 10);
+ buffer[index++] = '0' + ((gmtm.tm_hour / 1) % 10);
+ buffer[index++] = ':';
+ buffer[index++] = '0' + ((gmtm.tm_min / 10) % 10);
+ buffer[index++] = '0' + ((gmtm.tm_min / 1) % 10);
+ buffer[index++] = ':';
+ buffer[index++] = '0' + ((gmtm.tm_sec / 10) % 10);
+ buffer[index++] = '0' + ((gmtm.tm_sec / 1) % 10);
+ buffer[index++] = '.';
+ buffer[index++] = '0' + ((millis / 100) % 10);
+ buffer[index++] = '0' + ((millis / 10) % 10);
+ buffer[index++] = '0' + ((millis / 1) % 10);
+ buffer[index] = '\0';
+
+ return index;
+}
+
+}
+
+static time_t BAD_DATE = -1;
+
+time_t
+tm2sec(const struct tm* t)
+{
+ int year;
+ time_t days, secs;
+ const int dayoffset[12]
+ = {306, 337, 0, 31, 61, 92, 122, 153, 184, 214, 245, 275};
+
+ year = t->tm_year;
+
+ if (year < 70) {
+ return BAD_DATE;
+ }
+ if ((sizeof(time_t) <= 4) && (year >= 138)) {
+ year = 137;
+ }
+
+ /* shift new year to 1st March in order to make leap year calc easy */
+
+ if (t->tm_mon < 2) {
+ year--;
+ }
+
+ /* Find number of days since 1st March 1900 (in the Gregorian calendar). */
+
+ days = year * 365 + year / 4 - year / 100 + (year / 100 + 3) / 4;
+ days += dayoffset[t->tm_mon] + t->tm_mday - 1;
+ days -= 25508; /* 1 jan 1970 is 25508 days since 1 mar 1900 */
+
+ secs = ((days * 24 + t->tm_hour) * 60 + t->tm_min) * 60 + t->tm_sec;
+
+ if (secs < 0) {
+ return BAD_DATE;
+ } /* must have overflowed */
+ else
+ {
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ if (t->tm_zone) {
+ secs -= t->tm_gmtoff;
+ }
+#endif
+ return secs;
+ } /* must be a valid time */
+}
+
+static const int SECSPERMIN = 60;
+static const int SECSPERHOUR = 60 * SECSPERMIN;
+static const int SECSPERDAY = 24 * SECSPERHOUR;
+static const int YEAR_BASE = 1900;
+static const int EPOCH_WDAY = 4;
+static const int DAYSPERWEEK = 7;
+static const int EPOCH_YEAR = 1970;
+
+#define isleap(y) ((((y) % 4) == 0 && ((y) % 100) != 0) || ((y) % 400) == 0)
+
+static const int year_lengths[2] = {365, 366};
+
+const unsigned short int mon_yday[2][13] = {
+ /* Normal years. */
+ {0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365},
+ /* Leap years. */
+ {0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366}};
+
+void
+secs2wday(const struct timeval& tv, struct tm* res)
+{
+ long days, rem;
+ time_t lcltime;
+
+ /* base decision about std/dst time on current time */
+ lcltime = tv.tv_sec;
+
+ days = ((long) lcltime) / SECSPERDAY;
+ rem = ((long) lcltime) % SECSPERDAY;
+ while (rem < 0) {
+ rem += SECSPERDAY;
+ --days;
+ }
+
+ /* compute day of week */
+ if ((res->tm_wday = ((EPOCH_WDAY + days) % DAYSPERWEEK)) < 0) {
+ res->tm_wday += DAYSPERWEEK;
+ }
+}
+
+struct tm*
+secs2tm(lnav::time64_t tim, struct tm* res)
+{
+ long days, rem;
+ lnav::time64_t lcltime;
+ int y;
+ int yleap;
+ const unsigned short int* ip;
+
+ /* base decision about std/dst time on current time */
+ lcltime = tim;
+
+ days = ((long) lcltime) / SECSPERDAY;
+ rem = ((long) lcltime) % SECSPERDAY;
+ while (rem < 0) {
+ rem += SECSPERDAY;
+ --days;
+ }
+
+ /* compute hour, min, and sec */
+ res->tm_hour = (int) (rem / SECSPERHOUR);
+ rem %= SECSPERHOUR;
+ res->tm_min = (int) (rem / SECSPERMIN);
+ res->tm_sec = (int) (rem % SECSPERMIN);
+
+ /* compute day of week */
+ if ((res->tm_wday = ((EPOCH_WDAY + days) % DAYSPERWEEK)) < 0)
+ res->tm_wday += DAYSPERWEEK;
+
+ /* compute year & day of year */
+ y = EPOCH_YEAR;
+ if (days >= 0) {
+ for (;;) {
+ yleap = isleap(y);
+ if (days < year_lengths[yleap])
+ break;
+ y++;
+ days -= year_lengths[yleap];
+ }
+ } else {
+ do {
+ --y;
+ yleap = isleap(y);
+ days += year_lengths[yleap];
+ } while (days < 0);
+ }
+
+ res->tm_year = y - YEAR_BASE;
+ res->tm_yday = days;
+ ip = mon_yday[isleap(y)];
+ for (y = 11; days < (long int) ip[y]; --y)
+ continue;
+ days -= ip[y];
+ res->tm_mon = y;
+ res->tm_mday = days + 1;
+
+ res->tm_isdst = 0;
+
+ return (res);
+}
+
+struct timeval
+exttm::to_timeval() const
+{
+ struct timeval retval;
+
+ retval.tv_sec = tm2sec(&this->et_tm);
+ retval.tv_usec = std::chrono::duration_cast<std::chrono::microseconds>(
+ std::chrono::nanoseconds(this->et_nsec))
+ .count();
+
+ return retval;
+}
diff --git a/src/base/time_util.hh b/src/base/time_util.hh
new file mode 100644
index 0000000..ef9687f
--- /dev/null
+++ b/src/base/time_util.hh
@@ -0,0 +1,206 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_time_util_hh
+#define lnav_time_util_hh
+
+#include <inttypes.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+
+#include "config.h"
+
+namespace lnav {
+
+using time64_t = uint64_t;
+
+ssize_t strftime_rfc3339(char* buffer,
+ size_t buffer_size,
+ lnav::time64_t tim,
+ int millis,
+ char sep = ' ');
+
+} // namespace lnav
+
+struct tm* secs2tm(lnav::time64_t tim, struct tm* res);
+/**
+ * Convert the time stored in a 'tm' struct into epoch time.
+ *
+ * @param t The 'tm' structure to convert to epoch time.
+ * @return The given time in seconds since the epoch.
+ */
+time_t tm2sec(const struct tm* t);
+void secs2wday(const struct timeval& tv, struct tm* res);
+
+inline time_t
+convert_log_time_to_local(time_t value)
+{
+ struct tm tm;
+
+ localtime_r(&value, &tm);
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ tm.tm_zone = NULL;
+#endif
+ tm.tm_isdst = 0;
+ return tm2sec(&tm);
+}
+
+constexpr lnav::time64_t MAX_TIME_T = 4000000000LL;
+
+enum exttm_bits_t {
+ ETB_YEAR_SET,
+ ETB_MONTH_SET,
+ ETB_DAY_SET,
+ ETB_HOUR_SET,
+ ETB_MINUTE_SET,
+ ETB_SECOND_SET,
+ ETB_MACHINE_ORIENTED,
+ ETB_EPOCH_TIME,
+ ETB_MILLIS_SET,
+ ETB_MICROS_SET,
+ ETB_NANOS_SET,
+};
+
+enum exttm_flags_t {
+ ETF_YEAR_SET = (1UL << ETB_YEAR_SET),
+ ETF_MONTH_SET = (1UL << ETB_MONTH_SET),
+ ETF_DAY_SET = (1UL << ETB_DAY_SET),
+ ETF_HOUR_SET = (1UL << ETB_HOUR_SET),
+ ETF_MINUTE_SET = (1UL << ETB_MINUTE_SET),
+ ETF_SECOND_SET = (1UL << ETB_SECOND_SET),
+ ETF_MACHINE_ORIENTED = (1UL << ETB_MACHINE_ORIENTED),
+ ETF_EPOCH_TIME = (1UL << ETB_EPOCH_TIME),
+ ETF_MILLIS_SET = (1UL << ETB_MILLIS_SET),
+ ETF_MICROS_SET = (1UL << ETB_MICROS_SET),
+ ETF_NANOS_SET = (1UL << ETB_NANOS_SET),
+};
+
+struct exttm {
+ struct tm et_tm {};
+ int32_t et_nsec{0};
+ unsigned int et_flags{0};
+ long et_gmtoff{0};
+
+ exttm() { memset(&this->et_tm, 0, sizeof(this->et_tm)); }
+
+ bool operator==(const exttm& other) const
+ {
+ return memcmp(this, &other, sizeof(exttm)) == 0;
+ }
+
+ struct timeval to_timeval() const;
+};
+
+inline bool
+operator<(const struct timeval& left, time_t right)
+{
+ return left.tv_sec < right;
+}
+
+inline bool
+operator<(time_t left, const struct timeval& right)
+{
+ return left < right.tv_sec;
+}
+
+inline bool
+operator<(const struct timeval& left, const struct timeval& right)
+{
+ return left.tv_sec < right.tv_sec
+ || ((left.tv_sec == right.tv_sec) && (left.tv_usec < right.tv_usec));
+}
+
+inline bool
+operator!=(const struct timeval& left, const struct timeval& right)
+{
+ return left.tv_sec != right.tv_sec || left.tv_usec != right.tv_usec;
+}
+
+inline bool
+operator==(const struct timeval& left, const struct timeval& right)
+{
+ return left.tv_sec == right.tv_sec || left.tv_usec == right.tv_usec;
+}
+
+inline struct timeval
+operator-(const struct timeval& lhs, const struct timeval& rhs)
+{
+ struct timeval diff;
+
+ timersub(&lhs, &rhs, &diff);
+ return diff;
+}
+
+typedef int64_t mstime_t;
+
+inline mstime_t
+getmstime()
+{
+ struct timeval tv;
+
+ gettimeofday(&tv, nullptr);
+
+ return tv.tv_sec * 1000ULL + tv.tv_usec / 1000ULL;
+}
+
+inline struct timeval
+current_timeval()
+{
+ struct timeval retval;
+
+ gettimeofday(&retval, nullptr);
+
+ return retval;
+}
+
+inline struct timespec
+current_timespec()
+{
+ struct timespec retval;
+
+ clock_gettime(CLOCK_REALTIME, &retval);
+
+ return retval;
+}
+
+inline time_t
+day_num(time_t ti)
+{
+ return ti / (24 * 60 * 60);
+}
+
+inline time_t
+hour_num(time_t ti)
+{
+ return ti / (60 * 60);
+}
+
+#endif
diff --git a/src/big_array.hh b/src/big_array.hh
new file mode 100644
index 0000000..8401ead
--- /dev/null
+++ b/src/big_array.hh
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file big_array.hh
+ */
+
+#ifndef lnav_big_array_hh
+#define lnav_big_array_hh
+
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include "base/math_util.hh"
+
+template<typename T>
+struct big_array {
+ static const size_t DEFAULT_INCREMENT = 100 * 1000;
+
+ big_array()
+ : ba_ptr(nullptr), ba_size(0), ba_capacity(0){
+
+ };
+
+ bool reserve(size_t size)
+ {
+ if (size < this->ba_capacity) {
+ return false;
+ }
+
+ if (this->ba_ptr) {
+ munmap(this->ba_ptr,
+ roundup_size(this->ba_capacity * sizeof(T), getpagesize()));
+ }
+
+ this->ba_capacity = size + DEFAULT_INCREMENT;
+ void* result
+ = mmap(nullptr,
+ roundup_size(this->ba_capacity * sizeof(T), getpagesize()),
+ PROT_READ | PROT_WRITE,
+ MAP_ANONYMOUS | MAP_PRIVATE,
+ -1,
+ 0);
+
+ ensure(result != MAP_FAILED);
+
+ this->ba_ptr = (T*) result;
+
+ return true;
+ };
+
+ void clear()
+ {
+ this->ba_size = 0;
+ };
+
+ size_t size() const
+ {
+ return this->ba_size;
+ };
+
+ void shrink_to(size_t new_size)
+ {
+ require(new_size <= this->ba_size);
+
+ this->ba_size = new_size;
+ }
+
+ bool empty() const
+ {
+ return this->ba_size == 0;
+ };
+
+ void push_back(const T& val)
+ {
+ this->ba_ptr[this->ba_size] = val;
+ this->ba_size += 1;
+ };
+
+ T& operator[](size_t index)
+ {
+ return this->ba_ptr[index];
+ };
+
+ const T& operator[](size_t index) const
+ {
+ return this->ba_ptr[index];
+ };
+
+ T& back()
+ {
+ return this->ba_ptr[this->ba_size - 1];
+ }
+
+ typedef T* iterator;
+
+ iterator begin()
+ {
+ return this->ba_ptr;
+ };
+
+ iterator end()
+ {
+ return this->ba_ptr + this->ba_size;
+ };
+
+ T* ba_ptr;
+ size_t ba_size;
+ size_t ba_capacity;
+};
+
+#endif
diff --git a/src/bin2c.hh b/src/bin2c.hh
new file mode 100644
index 0000000..fc6c4db
--- /dev/null
+++ b/src/bin2c.hh
@@ -0,0 +1,74 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file bin2c.hh
+ */
+
+#ifndef lnav_bin2c_hh
+#define lnav_bin2c_hh
+
+#include <memory>
+
+#include <assert.h>
+#include <sys/types.h>
+#include <zlib.h>
+
+#include "base/intern_string.hh"
+
+struct bin_src_file {
+ bin_src_file(const char* name,
+ const unsigned char* data,
+ size_t compressed_size,
+ size_t size)
+ : bsf_name(name), bsf_data(new unsigned char[size + 1]), bsf_size(size)
+ {
+ uLongf zsize = size;
+ auto rc
+ = uncompress(this->bsf_data.get(), &zsize, data, compressed_size);
+ assert(rc == Z_OK);
+ assert(zsize == size);
+ this->bsf_data[size] = '\0';
+ };
+
+ string_fragment to_string_fragment() const
+ {
+ return string_fragment{this->bsf_data.get(), 0, (int) this->bsf_size};
+ }
+
+ const char* get_name() const
+ {
+ return this->bsf_name;
+ }
+
+private:
+ const char* bsf_name;
+ std::unique_ptr<unsigned char[]> bsf_data;
+ ssize_t bsf_size;
+};
+
+#endif
diff --git a/src/bookmarks.cc b/src/bookmarks.cc
new file mode 100644
index 0000000..89637ad
--- /dev/null
+++ b/src/bookmarks.cc
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file bookmarks.cc
+ */
+
+#include "bookmarks.hh"
+
+#include "base/itertools.hh"
+#include "config.h"
+
+std::unordered_set<std::string> bookmark_metadata::KNOWN_TAGS;
+
+void
+bookmark_metadata::add_tag(const std::string& tag)
+{
+ if (!(this->bm_tags | lnav::itertools::find(tag))) {
+ this->bm_tags.emplace_back(tag);
+ }
+}
+
+bool
+bookmark_metadata::remove_tag(const std::string& tag)
+{
+ auto iter = std::find(this->bm_tags.begin(), this->bm_tags.end(), tag);
+ bool retval = false;
+
+ if (iter != this->bm_tags.end()) {
+ this->bm_tags.erase(iter);
+ retval = true;
+ }
+ return retval;
+}
+
+bool
+bookmark_metadata::empty() const
+{
+ return this->bm_name.empty() && this->bm_comment.empty()
+ && this->bm_tags.empty();
+}
+
+void
+bookmark_metadata::clear()
+{
+ this->bm_comment.clear();
+ this->bm_tags.clear();
+}
+
+nonstd::optional<bookmark_type_t*>
+bookmark_type_t::find_type(const std::string& name)
+{
+ return get_all_types()
+ | lnav::itertools::find_if(
+ [&name](const auto& elem) { return elem->bt_name == name; })
+ | lnav::itertools::deref();
+}
+
+std::vector<bookmark_type_t*>&
+bookmark_type_t::get_all_types()
+{
+ static std::vector<bookmark_type_t*> all_types;
+
+ return all_types;
+}
diff --git a/src/bookmarks.hh b/src/bookmarks.hh
new file mode 100644
index 0000000..189e830
--- /dev/null
+++ b/src/bookmarks.hh
@@ -0,0 +1,209 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file bookmarks.hh
+ */
+
+#ifndef bookmarks_hh
+#define bookmarks_hh
+
+#include <algorithm>
+#include <map>
+#include <unordered_set>
+#include <string>
+#include <vector>
+
+#include "base/lnav_log.hh"
+
+struct bookmark_metadata {
+ static std::unordered_set<std::string> KNOWN_TAGS;
+
+ std::string bm_name;
+ std::string bm_comment;
+ std::vector<std::string> bm_tags;
+
+ void add_tag(const std::string& tag);
+
+ bool remove_tag(const std::string& tag);
+
+ bool empty() const;
+
+ void clear();
+};
+
+/**
+ * Extension of the STL vector that is used to store bookmarks for
+ * files being viewed, where a bookmark is just a particular line in
+ * the file(s). The value-added over the standard vector are some
+ * methods for doing content-wise iteration. In other words, given a
+ * value that may or may not be in the vector, find the next or
+ * previous value that is in the vector.
+ *
+ * @param LineType The type used to store line numbers. (e.g.
+ * vis_line_t or content_line_t)
+ *
+ * @note The vector is expected to be sorted.
+ */
+template<typename LineType>
+class bookmark_vector : public std::vector<LineType> {
+ using base_vector = std::vector<LineType>;
+
+public:
+ using size_type = typename base_vector::size_type;
+ using iterator = typename base_vector::iterator;
+ using const_iterator = typename base_vector::const_iterator;
+
+ /**
+ * Insert a bookmark into this vector, but only if it is not already in the
+ * vector.
+ *
+ * @param vl The line to bookmark.
+ */
+ iterator insert_once(LineType vl)
+ {
+ iterator retval;
+
+ require(vl >= 0);
+
+ auto lb = std::lower_bound(this->begin(), this->end(), vl);
+ if (lb == this->end() || *lb != vl) {
+ this->insert(lb, vl);
+ retval = this->end();
+ } else {
+ retval = lb;
+ }
+
+ return retval;
+ }
+
+ std::pair<iterator, iterator> equal_range(LineType start, LineType stop)
+ {
+ auto lb = std::lower_bound(this->begin(), this->end(), start);
+
+ if (stop == LineType(-1)) {
+ return std::make_pair(lb, this->end());
+ }
+
+ auto up = std::upper_bound(this->begin(), this->end(), stop);
+
+ return std::make_pair(lb, up);
+ }
+
+ /**
+ * @param start The value to start the search for the next bookmark.
+ * @return The next bookmark value in the vector or -1 if there are
+ * no more remaining bookmarks. If the 'start' value is a bookmark,
+ * the next bookmark is returned. If the 'start' value is not a
+ * bookmark, the next highest value in the vector is returned.
+ */
+ nonstd::optional<LineType> next(LineType start) const;
+
+ /**
+ * @param start The value to start the search for the previous
+ * bookmark.
+ * @return The previous bookmark value in the vector or -1 if there
+ * are no more prior bookmarks.
+ * @see next
+ */
+ nonstd::optional<LineType> prev(LineType start) const;
+};
+
+/**
+ * Dummy type whose instances are used to distinguish between
+ * bookmarks maintained by different source modules.
+ */
+class bookmark_type_t {
+public:
+ using type_iterator = std::vector<bookmark_type_t*>::iterator;
+
+ static type_iterator type_begin() { return get_all_types().begin(); }
+
+ static type_iterator type_end() { return get_all_types().end(); }
+
+ static nonstd::optional<bookmark_type_t*> find_type(
+ const std::string& name);
+
+ static std::vector<bookmark_type_t*>& get_all_types();
+
+ explicit bookmark_type_t(std::string name) : bt_name(std::move(name))
+ {
+ get_all_types().push_back(this);
+ }
+
+ const std::string& get_name() const { return this->bt_name; }
+
+private:
+ const std::string bt_name;
+};
+
+template<typename LineType>
+nonstd::optional<LineType>
+bookmark_vector<LineType>::next(LineType start) const
+{
+ nonstd::optional<LineType> retval;
+
+ require(start >= -1);
+
+ auto ub = std::upper_bound(this->cbegin(), this->cend(), start);
+ if (ub != this->cend()) {
+ retval = *ub;
+ }
+
+ ensure(!retval || start < retval.value());
+
+ return retval;
+}
+
+template<typename LineType>
+nonstd::optional<LineType>
+bookmark_vector<LineType>::prev(LineType start) const
+{
+ nonstd::optional<LineType> retval;
+
+ require(start >= 0);
+
+ auto lb = std::lower_bound(this->cbegin(), this->cend(), start);
+ if (lb != this->cbegin()) {
+ lb -= 1;
+ retval = *lb;
+ }
+
+ ensure(!retval || retval.value() < start);
+
+ return retval;
+}
+
+/**
+ * Map of bookmark types to bookmark vectors.
+ */
+template<typename LineType>
+struct bookmarks {
+ using type = std::map<const bookmark_type_t*, bookmark_vector<LineType>>;
+};
+
+#endif
diff --git a/src/bottom_status_source.cc b/src/bottom_status_source.cc
new file mode 100644
index 0000000..080be39
--- /dev/null
+++ b/src/bottom_status_source.cc
@@ -0,0 +1,249 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "bottom_status_source.hh"
+
+#include "base/snippet_highlighters.hh"
+#include "config.h"
+
+bottom_status_source::bottom_status_source()
+{
+ this->bss_fields[BSF_LINE_NUMBER].set_min_width(10);
+ this->bss_fields[BSF_LINE_NUMBER].set_share(1000);
+ this->bss_fields[BSF_PERCENT].set_width(6);
+ this->bss_fields[BSF_PERCENT].set_left_pad(1);
+ this->bss_fields[BSF_HITS].set_min_width(10);
+ this->bss_fields[BSF_HITS].set_share(5);
+ this->bss_fields[BSF_SEARCH_TERM].set_min_width(10);
+ this->bss_fields[BSF_SEARCH_TERM].set_share(1);
+ this->bss_fields[BSF_LOADING].set_width(13);
+ this->bss_fields[BSF_LOADING].right_justify(true);
+ this->bss_fields[BSF_HELP].set_width(14);
+ this->bss_fields[BSF_HELP].set_value("?:View Help");
+ this->bss_fields[BSF_HELP].right_justify(true);
+ this->bss_prompt.set_left_pad(1);
+ this->bss_prompt.set_min_width(35);
+ this->bss_prompt.set_share(1);
+ this->bss_error.set_left_pad(1);
+ this->bss_error.set_min_width(35);
+ this->bss_error.set_share(1);
+ this->bss_line_error.set_left_pad(1);
+ this->bss_line_error.set_min_width(35);
+ this->bss_line_error.set_share(1);
+}
+
+void
+bottom_status_source::update_line_number(listview_curses* lc)
+{
+ status_field& sf = this->bss_fields[BSF_LINE_NUMBER];
+
+ if (lc->get_inner_height() == 0) {
+ sf.set_value(" L0");
+ } else {
+ sf.set_value(" L%'d", (int) lc->get_selection());
+ }
+
+ this->bss_line_error.set_value(
+ lc->map_top_row([](const attr_line_t& top_row)
+ -> nonstd::optional<std::string> {
+ const auto& sa = top_row.get_attrs();
+ auto error_wrapper = get_string_attr(sa, SA_ERROR);
+ if (error_wrapper) {
+ return error_wrapper.value().get();
+ }
+ return nonstd::nullopt;
+ }).value_or(""));
+}
+
+void
+bottom_status_source::update_search_term(textview_curses& tc)
+{
+ auto& sf = this->bss_fields[BSF_SEARCH_TERM];
+ auto search_term = tc.get_current_search();
+
+ sf.clear();
+ if (!search_term.empty()) {
+ auto search_term_al = attr_line_t(search_term);
+
+ lnav::snippets::regex_highlighter(
+ search_term_al, -1, line_range{0, (int) search_term_al.length()});
+ sf.get_value().append_quoted(search_term_al);
+ }
+
+ this->bss_paused = tc.is_paused();
+ this->update_loading(0, 0);
+}
+
+void
+bottom_status_source::update_percent(listview_curses* lc)
+{
+ status_field& sf = this->bss_fields[BSF_PERCENT];
+ vis_line_t top = lc->get_top();
+ vis_line_t bottom, height;
+ unsigned long width;
+ double percent;
+
+ lc->get_dimensions(height, width);
+
+ if (lc->get_inner_height() > 0) {
+ bottom = std::min(top + height - 1_vl, lc->get_inner_height() - 1_vl);
+ percent = (double) (bottom + 1);
+ percent /= (double) lc->get_inner_height();
+ percent *= 100.0;
+ } else {
+ percent = 0.0;
+ }
+ sf.set_value("%3d%% ", (int) percent);
+}
+
+void
+bottom_status_source::update_marks(listview_curses* lc)
+{
+ auto* tc = static_cast<textview_curses*>(lc);
+ vis_bookmarks& bm = tc->get_bookmarks();
+ status_field& sf = this->bss_fields[BSF_HITS];
+
+ if (bm.find(&textview_curses::BM_SEARCH) != bm.end()) {
+ bookmark_vector<vis_line_t>& bv = bm[&textview_curses::BM_SEARCH];
+
+ if (!bv.empty() || !tc->get_current_search().empty()) {
+ auto vl = tc->get_selection();
+ auto lb = std::lower_bound(bv.begin(), bv.end(), vl);
+ if (lb != bv.end() && *lb == vl) {
+ sf.set_value(" Hit %'d of %'d for ",
+ std::distance(bv.begin(), lb) + 1,
+ tc->get_match_count());
+ } else {
+ sf.set_value(" %'d hits for ", tc->get_match_count());
+ }
+ } else {
+ sf.clear();
+ }
+ } else {
+ sf.clear();
+ }
+}
+
+void
+bottom_status_source::update_hits(textview_curses* tc)
+{
+ status_field& sf = this->bss_fields[BSF_HITS];
+ role_t new_role;
+
+ if (tc->is_searching()) {
+ this->bss_hit_spinner += 1;
+ if (this->bss_hit_spinner % 2) {
+ new_role = role_t::VCR_ACTIVE_STATUS;
+ } else {
+ new_role = role_t::VCR_ACTIVE_STATUS2;
+ }
+ sf.set_cylon(true);
+ } else {
+ new_role = role_t::VCR_STATUS;
+ sf.set_cylon(false);
+ }
+ // this->bss_error.clear();
+ sf.set_role(new_role);
+ this->update_marks(tc);
+}
+
+void
+bottom_status_source::update_loading(file_off_t off, file_ssize_t total)
+{
+ auto& sf = this->bss_fields[BSF_LOADING];
+
+ require(off >= 0);
+ require(off <= total);
+
+ if (total == 0) {
+ sf.set_cylon(false);
+ sf.set_role(role_t::VCR_STATUS);
+ if (this->bss_paused) {
+ sf.set_value("\xE2\x80\x96 Paused");
+ } else {
+ sf.clear();
+ }
+ } else if (off == total) {
+ static const std::vector<std::string> DOTS = {
+ " ",
+ ". ",
+ ".. ",
+ "...",
+ ".. ",
+ ". ",
+ };
+
+ this->bss_load_percent += 1;
+ sf.set_cylon(true);
+ sf.set_role(role_t::VCR_ACTIVE_STATUS2);
+ sf.set_value(" Working%s ",
+ DOTS[this->bss_load_percent % DOTS.size()].c_str());
+ } else {
+ int pct = (int) (((double) off / (double) total) * 100.0);
+
+ if (this->bss_load_percent != pct) {
+ this->bss_load_percent = pct;
+
+ sf.set_cylon(true);
+ sf.set_role(role_t::VCR_ACTIVE_STATUS2);
+ sf.set_value(" Loading %2d%% ", pct);
+ }
+ }
+}
+
+size_t
+bottom_status_source::statusview_fields()
+{
+ size_t retval;
+
+ if (this->bss_prompt.empty() && this->bss_error.empty()
+ && this->bss_line_error.empty())
+ {
+ retval = BSF__MAX;
+ } else {
+ retval = 1;
+ }
+
+ return retval;
+}
+
+status_field&
+bottom_status_source::statusview_value_for_field(int field)
+{
+ if (!this->bss_error.empty()) {
+ return this->bss_error;
+ }
+ if (!this->bss_prompt.empty()) {
+ return this->bss_prompt;
+ }
+ if (!this->bss_line_error.empty()) {
+ return this->bss_line_error;
+ }
+ return this->get_field((field_t) field);
+}
diff --git a/src/bottom_status_source.hh b/src/bottom_status_source.hh
new file mode 100644
index 0000000..dcf6c35
--- /dev/null
+++ b/src/bottom_status_source.hh
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_bottom_status_source_hh
+#define lnav_bottom_status_source_hh
+
+#include <string>
+
+#include "grep_proc.hh"
+#include "statusview_curses.hh"
+#include "textview_curses.hh"
+
+class bottom_status_source
+ : public status_data_source
+ , public grep_proc_control {
+public:
+ typedef enum {
+ BSF_LINE_NUMBER,
+ BSF_PERCENT,
+ BSF_HITS,
+ BSF_SEARCH_TERM,
+ BSF_LOADING,
+ BSF_HELP,
+
+ BSF__MAX
+ } field_t;
+
+ bottom_status_source();
+
+ status_field& get_field(field_t id) { return this->bss_fields[id]; }
+
+ void set_prompt(const std::string& prompt)
+ {
+ this->bss_prompt.set_value(prompt);
+ }
+
+ void grep_error(const std::string& msg) override
+ {
+ this->bss_error.set_value(msg);
+ }
+
+ size_t statusview_fields() override;
+
+ status_field& statusview_value_for_field(int field) override;
+
+ void update_line_number(listview_curses* lc);
+
+ void update_search_term(textview_curses& tc);
+
+ void update_percent(listview_curses* lc);
+
+ void update_marks(listview_curses* lc);
+
+ void update_hits(textview_curses* tc);
+
+ void update_loading(file_off_t off, file_ssize_t total);
+
+private:
+ status_field bss_prompt{1024, role_t::VCR_STATUS};
+ status_field bss_error{1024, role_t::VCR_ALERT_STATUS};
+ status_field bss_line_error{1024, role_t::VCR_ALERT_STATUS};
+ status_field bss_fields[BSF__MAX];
+ int bss_hit_spinner{0};
+ int bss_load_percent{0};
+ bool bss_paused{false};
+};
+
+#endif
diff --git a/src/bound_tags.hh b/src/bound_tags.hh
new file mode 100644
index 0000000..a24f38c
--- /dev/null
+++ b/src/bound_tags.hh
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file bound_tags.hh
+ */
+
+#ifndef lnav_bound_tags_hh
+#define lnav_bound_tags_hh
+
+struct last_relative_time_tag {};
+
+struct sql_cmd_map_tag {};
+
+enum {
+ LNB_HEADLESS,
+ LNB_MANAGEMENT,
+ LNB_SECURE_MODE,
+};
+
+/** Flags set on the lnav command-line. */
+typedef enum {
+ LNF_HEADLESS = (1L << LNB_HEADLESS),
+ LNF_MANAGEMENT = (1L << LNB_MANAGEMENT),
+ LNF_SECURE_MODE = (1L << LNB_SECURE_MODE),
+} lnav_flags_t;
+
+struct lnav_flags_tag {};
+
+#endif
diff --git a/src/breadcrumb.hh b/src/breadcrumb.hh
new file mode 100644
index 0000000..051ba2b
--- /dev/null
+++ b/src/breadcrumb.hh
@@ -0,0 +1,134 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_breadcrumb_hh
+#define lnav_breadcrumb_hh
+
+#include <string>
+
+#include "base/attr_line.hh"
+#include "fmt/format.h"
+#include "mapbox/variant.hpp"
+
+namespace breadcrumb {
+
+struct possibility {
+ possibility(std::string key, attr_line_t value)
+ : p_key(std::move(key)), p_display_value(std::move(value))
+ {
+ }
+
+ explicit possibility(std::string key) : p_key(key), p_display_value(key) {}
+
+ possibility() = default;
+
+ bool operator<(const possibility& rhs) const { return p_key < rhs.p_key; }
+ bool operator>(const possibility& rhs) const { return rhs < *this; }
+ bool operator<=(const possibility& rhs) const { return !(rhs < *this); }
+ bool operator>=(const possibility& rhs) const { return !(*this < rhs); }
+
+ std::string p_key;
+ attr_line_t p_display_value;
+};
+
+using crumb_possibilities = std::function<std::vector<possibility>()>;
+
+struct crumb {
+ using key_t = mapbox::util::variant<std::string, size_t>;
+
+ using perform = std::function<void(const key_t& key)>;
+
+ crumb(std::string key,
+ attr_line_t al,
+ crumb_possibilities cp,
+ perform performer)
+ : c_key(std::move(key)), c_display_value(std::move(al)),
+ c_possibility_provider(std::move(cp)),
+ c_performer(std::move(performer))
+ {
+ }
+
+ crumb(std::string key, crumb_possibilities cp, perform performer)
+ : c_key(key), c_display_value(key),
+ c_possibility_provider(std::move(cp)),
+ c_performer(std::move(performer))
+ {
+ }
+
+ explicit crumb(size_t index, crumb_possibilities cp, perform performer)
+ : c_key(index), c_display_value(fmt::format(FMT_STRING("[{}]"), index)),
+ c_possibility_provider(std::move(cp)),
+ c_performer(std::move(performer))
+ {
+ }
+
+ explicit crumb(key_t key, crumb_possibilities cp, perform performer)
+ : c_key(key), c_display_value(key.match(
+ [](std::string str) { return str; },
+ [](size_t index) {
+ return fmt::format(FMT_STRING("[{}]"), index);
+ })),
+ c_possibility_provider(std::move(cp)),
+ c_performer(std::move(performer))
+ {
+ }
+
+ crumb& with_possible_range(size_t count)
+ {
+ this->c_possible_range = count;
+ if (count == 0) {
+ this->c_search_placeholder = "(Array is empty)";
+ } else if (count == 1) {
+ this->c_search_placeholder = "(Array contains a single element)";
+ } else {
+ this->c_search_placeholder = fmt::format(
+ FMT_STRING("(Enter a number from 0 to {})"), count - 1);
+ }
+ return *this;
+ }
+
+ enum class expected_input_t {
+ exact,
+ index,
+ index_or_exact,
+ anything,
+ };
+
+ key_t c_key;
+ attr_line_t c_display_value;
+ crumb_possibilities c_possibility_provider;
+ perform c_performer;
+ nonstd::optional<size_t> c_possible_range;
+ expected_input_t c_expected_input{expected_input_t::exact};
+ std::string c_search_placeholder;
+};
+
+} // namespace breadcrumb
+
+#endif
diff --git a/src/breadcrumb_curses.cc b/src/breadcrumb_curses.cc
new file mode 100644
index 0000000..6481e6b
--- /dev/null
+++ b/src/breadcrumb_curses.cc
@@ -0,0 +1,461 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "breadcrumb_curses.hh"
+
+#include "base/itertools.hh"
+#include "itertools.similar.hh"
+#include "log_format_fwd.hh"
+#include "logfile.hh"
+
+using namespace lnav::roles::literals;
+
+breadcrumb_curses::breadcrumb_curses()
+{
+ this->bc_match_search_overlay.sos_parent = this;
+ this->bc_match_source.set_reverse_selection(true);
+ this->bc_match_view.set_selectable(true);
+ this->bc_match_view.set_overlay_source(&this->bc_match_search_overlay);
+ this->bc_match_view.set_sub_source(&this->bc_match_source);
+ this->bc_match_view.set_height(0_vl);
+ this->bc_match_view.set_show_scrollbar(true);
+ this->bc_match_view.set_default_role(role_t::VCR_POPUP);
+ this->add_child_view(&this->bc_match_view);
+}
+
+void
+breadcrumb_curses::do_update()
+{
+ if (!this->bc_line_source) {
+ return;
+ }
+
+ size_t crumb_index = 0;
+ size_t sel_crumb_offset = 0;
+ auto width = static_cast<size_t>(getmaxx(this->bc_window));
+ auto crumbs = this->bc_focused_crumbs.empty() ? this->bc_line_source()
+ : this->bc_focused_crumbs;
+ if (this->bc_last_selected_crumb
+ && this->bc_last_selected_crumb.value() >= crumbs.size())
+ {
+ this->bc_last_selected_crumb = crumbs.size() - 1;
+ }
+ attr_line_t crumbs_line;
+ for (const auto& crumb : crumbs) {
+ auto accum_width
+ = utf8_string_length(crumbs_line.get_string()).template unwrap();
+ auto elem_width = utf8_string_length(crumb.c_display_value.get_string())
+ .template unwrap();
+ auto is_selected = this->bc_selected_crumb
+ && (crumb_index == this->bc_selected_crumb.value());
+
+ if (is_selected && ((accum_width + elem_width) > width)) {
+ crumbs_line.clear();
+ crumbs_line.append("\u22ef\u276d"_breadcrumb);
+ accum_width = 2;
+ }
+
+ crumbs_line.append(crumb.c_display_value);
+ if (is_selected) {
+ sel_crumb_offset = accum_width;
+ crumbs_line.get_attrs().emplace_back(
+ line_range{
+ (int) (crumbs_line.length()
+ - crumb.c_display_value.length()),
+ (int) crumbs_line.length(),
+ },
+ VC_STYLE.template value(text_attrs{A_REVERSE}));
+ }
+ crumb_index += 1;
+ crumbs_line.append("\u276d"_breadcrumb);
+ }
+
+ line_range lr{0, static_cast<int>(width)};
+ view_curses::mvwattrline(
+ this->bc_window, this->bc_y, 0, crumbs_line, lr, role_t::VCR_STATUS);
+
+ if (this->bc_selected_crumb) {
+ this->bc_match_view.set_x(sel_crumb_offset);
+ }
+ view_curses::do_update();
+}
+
+void
+breadcrumb_curses::reload_data()
+{
+ if (!this->bc_selected_crumb) {
+ return;
+ }
+
+ auto& selected_crumb_ref
+ = this->bc_focused_crumbs[this->bc_selected_crumb.value()];
+ this->bc_possible_values = selected_crumb_ref.c_possibility_provider();
+
+ nonstd::optional<size_t> selected_value;
+ this->bc_similar_values = this->bc_possible_values
+ | lnav::itertools::similar_to(
+ [](const auto& elem) { return elem.p_key; },
+ this->bc_current_search,
+ 128)
+ | lnav::itertools::sort_with([](const auto& lhs, const auto& rhs) {
+ return strnatcasecmp(lhs.p_key.size(),
+ lhs.p_key.data(),
+ rhs.p_key.size(),
+ rhs.p_key.data())
+ < 0;
+ });
+ if (selected_crumb_ref.c_key.is<std::string>()
+ && selected_crumb_ref.c_expected_input
+ != breadcrumb::crumb::expected_input_t::anything)
+ {
+ auto& selected_crumb_key = selected_crumb_ref.c_key.get<std::string>();
+ auto found_poss_opt = this->bc_similar_values
+ | lnav::itertools::find_if([&selected_crumb_key](const auto& elem) {
+ return elem.p_key == selected_crumb_key;
+ });
+
+ if (found_poss_opt) {
+ selected_value = std::distance(this->bc_similar_values.begin(),
+ found_poss_opt.value());
+ } else {
+ selected_value = 0;
+ }
+ }
+
+ auto matches = attr_line_t().join(
+ this->bc_similar_values
+ | lnav::itertools::map(&breadcrumb::possibility::p_display_value),
+ "\n");
+ this->bc_match_source.replace_with(matches);
+ auto width = this->bc_possible_values
+ | lnav::itertools::fold(
+ [](const auto& match, auto& accum) {
+ auto mlen = match.p_display_value.length();
+ if (mlen > accum) {
+ return mlen;
+ }
+ return accum;
+ },
+ selected_crumb_ref.c_display_value.length());
+
+ if (static_cast<ssize_t>(selected_crumb_ref.c_search_placeholder.size())
+ > width)
+ {
+ width = selected_crumb_ref.c_search_placeholder.size();
+ }
+ this->bc_match_view.set_height(vis_line_t(
+ std::min(this->bc_match_source.get_lines().size() + 1, size_t{4})));
+ this->bc_match_view.set_width(width + 3);
+ this->bc_match_view.set_needs_update();
+ this->bc_match_view.set_selection(
+ vis_line_t(selected_value.value_or(-1_vl)));
+ this->bc_match_view.reload_data();
+ this->set_needs_update();
+}
+
+void
+breadcrumb_curses::focus()
+{
+ this->bc_focused_crumbs = this->bc_line_source();
+ if (this->bc_focused_crumbs.empty()) {
+ return;
+ }
+
+ this->bc_current_search.clear();
+ if (this->bc_last_selected_crumb
+ && this->bc_last_selected_crumb.value()
+ >= this->bc_focused_crumbs.size())
+ {
+ this->bc_last_selected_crumb = this->bc_focused_crumbs.size() - 1;
+ }
+ this->bc_selected_crumb = this->bc_last_selected_crumb.value_or(0);
+ this->reload_data();
+}
+
+void
+breadcrumb_curses::blur()
+{
+ this->bc_last_selected_crumb = this->bc_selected_crumb;
+ this->bc_focused_crumbs.clear();
+ this->bc_selected_crumb = nonstd::nullopt;
+ this->bc_current_search.clear();
+ this->bc_match_view.set_height(0_vl);
+ this->bc_match_view.set_selection(-1_vl);
+ this->bc_match_source.clear();
+ this->set_needs_update();
+}
+
+bool
+breadcrumb_curses::handle_key(int ch)
+{
+ bool retval = false;
+
+ switch (ch) {
+ case KEY_CTRL_A:
+ if (this->bc_selected_crumb) {
+ this->bc_selected_crumb = 0;
+ this->bc_current_search.clear();
+ this->reload_data();
+ }
+ retval = true;
+ break;
+ case KEY_CTRL_E:
+ if (this->bc_selected_crumb) {
+ this->bc_selected_crumb = this->bc_focused_crumbs.size() - 1;
+ this->bc_current_search.clear();
+ this->reload_data();
+ }
+ retval = true;
+ break;
+ case KEY_BTAB:
+ case KEY_LEFT:
+ if (this->bc_selected_crumb) {
+ if (this->bc_selected_crumb.value() > 0) {
+ this->bc_selected_crumb
+ = this->bc_selected_crumb.value() - 1;
+ } else {
+ this->bc_selected_crumb
+ = this->bc_focused_crumbs.size() - 1;
+ }
+ this->bc_current_search.clear();
+ this->reload_data();
+ }
+ retval = true;
+ break;
+ case '\t':
+ case KEY_RIGHT:
+ if (this->bc_selected_crumb) {
+ this->perform_selection(perform_behavior_t::if_different);
+ this->blur();
+ this->focus();
+ this->reload_data();
+ if (this->bc_selected_crumb.value()
+ < this->bc_focused_crumbs.size() - 1)
+ {
+ this->bc_selected_crumb
+ = this->bc_selected_crumb.value() + 1;
+ retval = true;
+ }
+ this->bc_current_search.clear();
+ this->reload_data();
+ } else {
+ retval = true;
+ }
+ break;
+ case KEY_HOME:
+ this->bc_match_view.set_selection(0_vl);
+ retval = true;
+ break;
+ case KEY_END:
+ this->bc_match_view.set_selection(
+ this->bc_match_view.get_inner_height() - 1_vl);
+ retval = true;
+ break;
+ case KEY_NPAGE:
+ this->bc_match_view.shift_selection(3);
+ retval = true;
+ break;
+ case KEY_PPAGE:
+ this->bc_match_view.shift_selection(-3);
+ retval = true;
+ break;
+ case KEY_UP:
+ this->bc_match_view.shift_selection(-1);
+ retval = true;
+ break;
+ case KEY_DOWN:
+ this->bc_match_view.shift_selection(1);
+ retval = true;
+ break;
+ case 0x7f:
+ case KEY_BACKSPACE:
+ if (!this->bc_current_search.empty()) {
+ this->bc_current_search.pop_back();
+ this->reload_data();
+ }
+ retval = true;
+ break;
+ case KEY_ENTER:
+ case '\r':
+ this->perform_selection(perform_behavior_t::always);
+ break;
+ default:
+ if (isprint(ch)) {
+ this->bc_current_search.push_back(ch);
+ this->reload_data();
+ retval = true;
+ }
+ break;
+ }
+
+ if (!retval) {
+ this->blur();
+ }
+ this->set_needs_update();
+ return retval;
+}
+
+void
+breadcrumb_curses::perform_selection(
+ breadcrumb_curses::perform_behavior_t behavior)
+{
+ if (!this->bc_selected_crumb) {
+ return;
+ }
+
+ auto& selected_crumb_ref
+ = this->bc_focused_crumbs[this->bc_selected_crumb.value()];
+ auto match_sel = this->bc_match_view.get_selection();
+ if (match_sel >= 0
+ && match_sel < vis_line_t(this->bc_similar_values.size()))
+ {
+ const auto& new_value = this->bc_similar_values[match_sel].p_key;
+
+ switch (behavior) {
+ case perform_behavior_t::if_different:
+ if (breadcrumb::crumb::key_t{new_value}
+ == selected_crumb_ref.c_key)
+ {
+ return;
+ }
+ break;
+ case perform_behavior_t::always:
+ break;
+ }
+ selected_crumb_ref.c_performer(new_value);
+ } else if (!this->bc_current_search.empty()) {
+ switch (selected_crumb_ref.c_expected_input) {
+ case breadcrumb::crumb::expected_input_t::exact:
+ break;
+ case breadcrumb::crumb::expected_input_t::index:
+ case breadcrumb::crumb::expected_input_t::index_or_exact: {
+ size_t index;
+
+ if (sscanf(this->bc_current_search.c_str(), "%zu", &index) == 1)
+ {
+ selected_crumb_ref.c_performer(index);
+ }
+ break;
+ }
+ case breadcrumb::crumb::expected_input_t::anything:
+ selected_crumb_ref.c_performer(this->bc_current_search);
+ break;
+ }
+ }
+}
+
+bool
+breadcrumb_curses::search_overlay_source::list_value_for_overlay(
+ const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t line,
+ attr_line_t& value_out)
+{
+ if (y == 0) {
+ auto* parent = this->sos_parent;
+ auto sel_opt = parent->bc_focused_crumbs
+ | lnav::itertools::nth(parent->bc_selected_crumb);
+ auto exp_input = sel_opt
+ | lnav::itertools::map(&breadcrumb::crumb::c_expected_input)
+ | lnav::itertools::unwrap_or(
+ breadcrumb::crumb::expected_input_t::exact);
+
+ value_out.with_attr_for_all(VC_STYLE.value(text_attrs{A_UNDERLINE}));
+
+ if (!parent->bc_current_search.empty()) {
+ value_out = parent->bc_current_search;
+
+ role_t combobox_role = role_t::VCR_STATUS;
+ switch (exp_input) {
+ case breadcrumb::crumb::expected_input_t::exact:
+ if (parent->bc_similar_values.empty()) {
+ combobox_role = role_t::VCR_ALERT_STATUS;
+ }
+ break;
+ case breadcrumb::crumb::expected_input_t::index: {
+ size_t index;
+
+ if (sscanf(parent->bc_current_search.c_str(), "%zu", &index)
+ != 1
+ || index < 0
+ || (index
+ >= (sel_opt
+ | lnav::itertools::map([](const auto& cr) {
+ return cr->c_possible_range.value_or(0);
+ })
+ | lnav::itertools::unwrap_or(size_t{0}))))
+ {
+ combobox_role = role_t::VCR_ALERT_STATUS;
+ }
+ break;
+ }
+ case breadcrumb::crumb::expected_input_t::index_or_exact: {
+ size_t index;
+
+ if (sscanf(parent->bc_current_search.c_str(), "%zu", &index)
+ == 1)
+ {
+ if (index < 0
+ || (index
+ >= (sel_opt
+ | lnav::itertools::map([](const auto& cr) {
+ return cr->c_possible_range.value_or(
+ 0);
+ })
+ | lnav::itertools::unwrap_or(size_t{0}))))
+ {
+ combobox_role = role_t::VCR_ALERT_STATUS;
+ }
+ } else if (parent->bc_similar_values.empty()) {
+ combobox_role = role_t::VCR_ALERT_STATUS;
+ }
+ break;
+ }
+ case breadcrumb::crumb::expected_input_t::anything:
+ break;
+ }
+ value_out.with_attr_for_all(VC_ROLE.value(combobox_role));
+ return true;
+ }
+ if (parent->bc_selected_crumb) {
+ auto& selected_crumb_ref
+ = parent->bc_focused_crumbs[parent->bc_selected_crumb.value()];
+
+ if (!selected_crumb_ref.c_search_placeholder.empty()) {
+ value_out = selected_crumb_ref.c_search_placeholder;
+ value_out.with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_INACTIVE_STATUS));
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
diff --git a/src/breadcrumb_curses.hh b/src/breadcrumb_curses.hh
new file mode 100644
index 0000000..06218c3
--- /dev/null
+++ b/src/breadcrumb_curses.hh
@@ -0,0 +1,107 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_breadcrumb_curses_hh
+#define lnav_breadcrumb_curses_hh
+
+#include <functional>
+#include <utility>
+#include <vector>
+
+#include "plain_text_source.hh"
+#include "textview_curses.hh"
+#include "view_curses.hh"
+
+class breadcrumb_curses : public view_curses {
+public:
+ breadcrumb_curses();
+
+ void set_y(int y)
+ {
+ this->bc_y = y;
+ this->bc_match_view.set_y(y + 1);
+ }
+
+ int get_y() const { return this->bc_y; }
+
+ void set_window(WINDOW* win)
+ {
+ this->bc_window = win;
+ this->bc_match_view.set_window(win);
+ }
+
+ void set_line_source(std::function<std::vector<breadcrumb::crumb>()> ls)
+ {
+ this->bc_line_source = std::move(ls);
+ }
+
+ void focus();
+ void blur();
+
+ bool handle_key(int ch);
+
+ void do_update() override;
+
+ void reload_data();
+
+private:
+ class search_overlay_source : public list_overlay_source {
+ public:
+ bool list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t line,
+ attr_line_t& value_out) override;
+
+ breadcrumb_curses* sos_parent{nullptr};
+ };
+
+ enum class perform_behavior_t {
+ always,
+ if_different,
+ };
+
+ void perform_selection(perform_behavior_t behavior);
+
+ WINDOW* bc_window{nullptr};
+ std::function<std::vector<breadcrumb::crumb>()> bc_line_source;
+ int bc_y{0};
+ std::vector<breadcrumb::crumb> bc_focused_crumbs;
+ nonstd::optional<size_t> bc_selected_crumb;
+ nonstd::optional<size_t> bc_last_selected_crumb;
+ std::vector<breadcrumb::possibility> bc_possible_values;
+ std::vector<breadcrumb::possibility> bc_similar_values;
+ std::string bc_current_search;
+
+ plain_text_source bc_match_source;
+ search_overlay_source bc_match_search_overlay;
+ textview_curses bc_match_view;
+};
+
+#endif
diff --git a/src/byte_array.hh b/src/byte_array.hh
new file mode 100644
index 0000000..9057f96
--- /dev/null
+++ b/src/byte_array.hh
@@ -0,0 +1,148 @@
+/**
+ * Copyright (c) 2007-2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef byte_array_hh
+#define byte_array_hh
+
+#include <ostream>
+#include <string>
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include "base/lnav_log.hh"
+#include "fmt/format.h"
+#include "optional.hpp"
+
+template<size_t COUNT, typename T = unsigned char>
+struct byte_array {
+ static constexpr size_t BYTE_COUNT = COUNT * sizeof(T);
+ static constexpr size_t STRING_SIZE = BYTE_COUNT * 2 + 1;
+
+ byte_array() = default;
+
+ static byte_array from(std::initializer_list<T> bytes)
+ {
+ byte_array retval;
+ size_t index = 0;
+
+ for (const auto by : bytes) {
+ retval.ba_data[index++] = by;
+ }
+ return retval;
+ }
+
+ byte_array(const byte_array& other)
+ {
+ memcpy(this->ba_data, other.ba_data, BYTE_COUNT);
+ }
+
+ bool operator<(const byte_array& other) const
+ {
+ return memcmp(this->ba_data, other.ba_data, BYTE_COUNT) < 0;
+ }
+
+ bool operator!=(const byte_array& other) const
+ {
+ return memcmp(this->ba_data, other.ba_data, BYTE_COUNT) != 0;
+ }
+
+ bool operator==(const byte_array& other) const
+ {
+ return memcmp(this->ba_data, other.ba_data, BYTE_COUNT) == 0;
+ }
+
+ void clear() { memset(this->ba_data, 0, BYTE_COUNT); }
+
+ template<typename OutputIt>
+ void to_string(OutputIt out,
+ nonstd::optional<char> separator = nonstd::nullopt) const
+ {
+ for (size_t lpc = 0; lpc < BYTE_COUNT; lpc++) {
+ if (lpc > 0 && separator) {
+ *out = separator.value();
+ }
+ fmt::format_to(out, FMT_STRING("{:02x}"), this->ba_data[lpc]);
+ }
+ }
+
+ std::string to_uuid_string() const
+ {
+ return fmt::format(
+ FMT_STRING("{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-"
+ "{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}"),
+ this->ba_data[0 % BYTE_COUNT],
+ this->ba_data[1 % BYTE_COUNT],
+ this->ba_data[2 % BYTE_COUNT],
+ this->ba_data[3 % BYTE_COUNT],
+ this->ba_data[4 % BYTE_COUNT],
+ this->ba_data[5 % BYTE_COUNT],
+ this->ba_data[6 % BYTE_COUNT],
+ this->ba_data[7 % BYTE_COUNT],
+ this->ba_data[8 % BYTE_COUNT],
+ this->ba_data[9 % BYTE_COUNT],
+ this->ba_data[10 % BYTE_COUNT],
+ this->ba_data[11 % BYTE_COUNT],
+ this->ba_data[12 % BYTE_COUNT],
+ this->ba_data[13 % BYTE_COUNT],
+ this->ba_data[14 % BYTE_COUNT],
+ this->ba_data[15 % BYTE_COUNT]);
+ }
+
+ std::string to_string(nonstd::optional<char> separator
+ = nonstd::nullopt) const
+ {
+ std::string retval;
+
+ retval.reserve(STRING_SIZE);
+ this->to_string(std::back_inserter(retval), separator);
+ return retval;
+ }
+
+ const unsigned char* in() const { return this->ba_data; }
+
+ T* out(int offset = 0)
+ {
+ T* ptr = (T*) this->ba_data;
+
+ return &ptr[offset];
+ }
+
+ unsigned char ba_data[BYTE_COUNT]{};
+};
+
+template<size_t COUNT, typename T = unsigned char>
+std::ostream&
+operator<<(std::ostream& os, const byte_array<COUNT, T>& ba)
+{
+ os << ba.to_string();
+ return os;
+}
+
+#endif
diff --git a/src/collation-functions.cc b/src/collation-functions.cc
new file mode 100644
index 0000000..8ad474b
--- /dev/null
+++ b/src/collation-functions.cc
@@ -0,0 +1,155 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file logfile_sub_source.hh
+ */
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sqlite3.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include "base/strnatcmp.h"
+#include "config.h"
+#include "log_level.hh"
+
+constexpr int MAX_ADDR_LEN = 128;
+
+static int
+try_inet_pton(int p_len, const char* p, char* n)
+{
+ static int ADDR_FAMILIES[] = {AF_INET, AF_INET6};
+
+ char buf[MAX_ADDR_LEN + 1];
+ int retval = AF_MAX;
+
+ strncpy(buf, p, p_len);
+ buf[p_len] = '\0';
+ for (int family : ADDR_FAMILIES) {
+ if (inet_pton(family, buf, n) == 1) {
+ retval = family;
+ break;
+ }
+ }
+
+ return retval;
+}
+
+static int
+convert_v6_to_v4(int family, char* n)
+{
+ struct in6_addr* ia = (struct in6_addr*) n;
+
+ if (family == AF_INET6
+ && (IN6_IS_ADDR_V4COMPAT(ia) || IN6_IS_ADDR_V4MAPPED(ia)))
+ {
+ family = AF_INET;
+ memmove(n, n + 12, sizeof(struct in_addr));
+ }
+
+ return family;
+}
+
+static int
+ipaddress(void* ptr, int a_len, const void* a_in, int b_len, const void* b_in)
+{
+ char a_addr[sizeof(struct in6_addr)], b_addr[sizeof(struct in6_addr)];
+ const char *a_str = (const char*) a_in, *b_str = (const char*) b_in;
+ int a_family, b_family, retval;
+
+ if ((a_len > MAX_ADDR_LEN) || (b_len > MAX_ADDR_LEN)) {
+ return strnatcasecmp(a_len, a_str, b_len, b_str);
+ }
+
+ int v4res = 0;
+ if (ipv4cmp(a_len, a_str, b_len, b_str, &v4res)) {
+ return v4res;
+ }
+
+ a_family = try_inet_pton(a_len, a_str, a_addr);
+ b_family = try_inet_pton(b_len, b_str, b_addr);
+
+ if (a_family == AF_MAX && b_family == AF_MAX) {
+ return strnatcasecmp(a_len, a_str, b_len, b_str);
+ } else if (a_family == AF_MAX && b_family != AF_MAX) {
+ retval = -1;
+ } else if (a_family != AF_MAX && b_family == AF_MAX) {
+ retval = 1;
+ } else {
+ a_family = convert_v6_to_v4(a_family, a_addr);
+ b_family = convert_v6_to_v4(b_family, b_addr);
+ if (a_family == b_family) {
+ retval = memcmp(a_addr,
+ b_addr,
+ a_family == AF_INET ? sizeof(struct in_addr)
+ : sizeof(struct in6_addr));
+ } else if (a_family == AF_INET) {
+ retval = -1;
+ } else {
+ retval = 1;
+ }
+ }
+
+ return retval;
+}
+
+static int
+sql_strnatcmp(
+ void* ptr, int a_len, const void* a_in, int b_len, const void* b_in)
+{
+ return strnatcmp(a_len, (char*) a_in, b_len, (char*) b_in);
+}
+
+static int
+sql_strnatcasecmp(
+ void* ptr, int a_len, const void* a_in, int b_len, const void* b_in)
+{
+ return strnatcasecmp(a_len, (char*) a_in, b_len, (char*) b_in);
+}
+
+static int
+sql_loglevelcmp(
+ void* ptr, int a_len, const void* a_in, int b_len, const void* b_in)
+{
+ return levelcmp((const char*) a_in, a_len, (const char*) b_in, b_len);
+}
+
+int
+register_collation_functions(sqlite3* db)
+{
+ sqlite3_create_collation(db, "ipaddress", SQLITE_UTF8, nullptr, ipaddress);
+ sqlite3_create_collation(
+ db, "naturalcase", SQLITE_UTF8, nullptr, sql_strnatcmp);
+ sqlite3_create_collation(
+ db, "naturalnocase", SQLITE_UTF8, nullptr, sql_strnatcasecmp);
+ sqlite3_create_collation(
+ db, "loglevel", SQLITE_UTF8, nullptr, sql_loglevelcmp);
+
+ return 0;
+}
diff --git a/src/column_namer.cc b/src/column_namer.cc
new file mode 100644
index 0000000..ba2d872
--- /dev/null
+++ b/src/column_namer.cc
@@ -0,0 +1,114 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file column_namer.cc
+ */
+
+#include <algorithm>
+
+#include "column_namer.hh"
+
+#include "base/itertools.hh"
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "sql_util.hh"
+
+const char column_namer::BUILTIN_COL[] = "col";
+
+column_namer::column_namer(language lang) : cn_language(lang) {}
+
+bool
+column_namer::existing_name(const string_fragment& in_name) const
+{
+ switch (this->cn_language) {
+ case language::SQL: {
+ auto upped = toupper(in_name.to_string());
+
+ if (std::binary_search(
+ std::begin(sql_keywords), std::end(sql_keywords), upped)) {
+ return true;
+ }
+ break;
+ }
+ case language::JSON:
+ break;
+ }
+
+ if (this->cn_builtin_names | lnav::itertools::find(in_name)) {
+ return true;
+ }
+
+ if (this->cn_names | lnav::itertools::find(in_name)) {
+ return true;
+ }
+
+ return false;
+}
+
+string_fragment
+column_namer::add_column(const string_fragment& in_name)
+{
+ string_fragment base_name;
+ string_fragment retval;
+ fmt::memory_buffer buf;
+ int num = 0;
+
+ if (in_name.empty()) {
+ base_name = string_fragment::from_const(BUILTIN_COL);
+ } else {
+ base_name = in_name;
+ }
+
+ retval = base_name;
+ auto counter_iter = this->cn_name_counters.find(retval);
+ if (counter_iter != this->cn_name_counters.end()) {
+ num = ++counter_iter->second;
+ fmt::format_to(
+ std::back_inserter(buf), FMT_STRING("{}_{}"), base_name, num);
+ retval = string_fragment::from_memory_buffer(buf);
+ }
+
+ while (this->existing_name(retval)) {
+ if (num == 0) {
+ auto counter_name = retval.to_owned(this->cn_alloc);
+ this->cn_name_counters[counter_name] = num;
+ }
+
+ log_debug(
+ "column name already exists: %.*s", retval.length(), retval.data());
+ fmt::format_to(
+ std::back_inserter(buf), FMT_STRING("{}_{}"), base_name, num);
+ retval = string_fragment::from_memory_buffer(buf);
+ num += 1;
+ }
+
+ retval = retval.to_owned(this->cn_alloc);
+ this->cn_names.emplace_back(retval);
+
+ return retval;
+}
diff --git a/src/column_namer.hh b/src/column_namer.hh
new file mode 100644
index 0000000..a98d9ef
--- /dev/null
+++ b/src/column_namer.hh
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file column_namer.hh
+ */
+
+#ifndef lnav_column_namer_hh
+#define lnav_column_namer_hh
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "ArenaAlloc/arenaalloc.h"
+#include "base/intern_string.hh"
+
+class column_namer {
+public:
+ enum class language {
+ SQL,
+ JSON,
+ };
+
+ column_namer(language lang);
+
+ bool existing_name(const string_fragment& in_name) const;
+
+ string_fragment add_column(const string_fragment& in_name);
+
+ static const char BUILTIN_COL[];
+
+ ArenaAlloc::Alloc<char> cn_alloc;
+ language cn_language;
+ std::vector<string_fragment> cn_builtin_names{string_fragment(BUILTIN_COL)};
+ std::vector<string_fragment> cn_names;
+ std::unordered_map<
+ string_fragment,
+ size_t,
+ frag_hasher,
+ std::equal_to<string_fragment>,
+ ArenaAlloc::Alloc<std::pair<const string_fragment, size_t>>>
+ cn_name_counters;
+};
+
+#endif
diff --git a/src/command_executor.cc b/src/command_executor.cc
new file mode 100644
index 0000000..574fefb
--- /dev/null
+++ b/src/command_executor.cc
@@ -0,0 +1,1135 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <vector>
+
+#include "command_executor.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/fs_util.hh"
+#include "base/injector.hh"
+#include "base/itertools.hh"
+#include "base/string_util.hh"
+#include "bound_tags.hh"
+#include "config.h"
+#include "db_sub_source.hh"
+#include "help_text_formatter.hh"
+#include "lnav.hh"
+#include "lnav.indexing.hh"
+#include "lnav_config.hh"
+#include "lnav_util.hh"
+#include "log_format_loader.hh"
+#include "readline_highlighters.hh"
+#include "service_tags.hh"
+#include "shlex.hh"
+#include "sql_util.hh"
+#include "vtab_module.hh"
+#include "yajlpp/json_ptr.hh"
+
+using namespace lnav::roles::literals;
+
+exec_context INIT_EXEC_CONTEXT;
+
+static const std::string MSG_FORMAT_STMT = R"(
+SELECT count(*) AS total, min(log_line) AS log_line, log_msg_format
+ FROM all_logs
+ GROUP BY log_msg_format
+ ORDER BY total DESC
+)";
+
+int
+sql_progress(const struct log_cursor& lc)
+{
+ ssize_t total = lnav_data.ld_log_source.text_line_count();
+ off_t off = lc.lc_curr_line;
+
+ if (off < 0 || off >= total) {
+ return 0;
+ }
+
+ if (lnav_data.ld_window == nullptr) {
+ return 0;
+ }
+
+ if (!lnav_data.ld_looping) {
+ return 1;
+ }
+
+ static sig_atomic_t sql_counter = 0;
+
+ if (ui_periodic_timer::singleton().time_to_update(sql_counter)) {
+ lnav_data.ld_bottom_source.update_loading(off, total);
+ lnav_data.ld_status_refresher();
+ }
+
+ return 0;
+}
+
+void
+sql_progress_finished()
+{
+ if (lnav_data.ld_window == nullptr) {
+ return;
+ }
+
+ lnav_data.ld_bottom_source.update_loading(0, 0);
+ lnav_data.ld_status_refresher();
+ lnav_data.ld_views[LNV_DB].redo_search();
+}
+
+static Result<std::string, lnav::console::user_message> execute_from_file(
+ exec_context& ec,
+ const ghc::filesystem::path& path,
+ int line_number,
+ const std::string& cmdline);
+
+Result<std::string, lnav::console::user_message>
+execute_command(exec_context& ec, const std::string& cmdline)
+{
+ std::vector<std::string> args;
+
+ log_info("Executing: %s", cmdline.c_str());
+
+ split_ws(cmdline, args);
+
+ if (!args.empty()) {
+ readline_context::command_map_t::iterator iter;
+
+ if ((iter = lnav_commands.find(args[0])) == lnav_commands.end()) {
+ return ec.make_error("unknown command - {}", args[0]);
+ }
+
+ ec.ec_current_help = &iter->second->c_help;
+ auto retval = iter->second->c_func(ec, cmdline, args);
+ if (retval.isErr()) {
+ auto um = retval.unwrapErr();
+
+ ec.add_error_context(um);
+ ec.ec_current_help = nullptr;
+ return Err(um);
+ }
+ ec.ec_current_help = nullptr;
+
+ return retval;
+ }
+
+ return ec.make_error("no command to execute");
+}
+
+static Result<std::map<std::string, scoped_value_t>,
+ lnav::console::user_message>
+bind_sql_parameters(exec_context& ec, sqlite3_stmt* stmt)
+{
+ std::map<std::string, scoped_value_t> retval;
+ auto param_count = sqlite3_bind_parameter_count(stmt);
+ for (int lpc = 0; lpc < param_count; lpc++) {
+ std::map<std::string, std::string>::iterator ov_iter;
+ const auto* name = sqlite3_bind_parameter_name(stmt, lpc + 1);
+ if (name == nullptr) {
+ auto um
+ = lnav::console::user_message::error("invalid SQL statement")
+ .with_reason(
+ "using a question-mark (?) for bound variables "
+ "is not supported, only named bound parameters "
+ "are supported")
+ .with_help(
+ "named parameters start with a dollar-sign "
+ "($) or colon (:) followed by the variable name");
+ ec.add_error_context(um);
+
+ return Err(um);
+ }
+
+ ov_iter = ec.ec_override.find(name);
+ if (ov_iter != ec.ec_override.end()) {
+ sqlite3_bind_text(stmt,
+ lpc,
+ ov_iter->second.c_str(),
+ ov_iter->second.length(),
+ SQLITE_TRANSIENT);
+ } else if (name[0] == '$') {
+ const auto& lvars = ec.ec_local_vars.top();
+ const auto& gvars = ec.ec_global_vars;
+ std::map<std::string, scoped_value_t>::const_iterator local_var,
+ global_var;
+ const char* env_value;
+
+ if (lnav_data.ld_window) {
+ char buf[32];
+ int lines, cols;
+
+ getmaxyx(lnav_data.ld_window, lines, cols);
+ if (strcmp(name, "$LINES") == 0) {
+ snprintf(buf, sizeof(buf), "%d", lines);
+ sqlite3_bind_text(stmt, lpc + 1, buf, -1, SQLITE_TRANSIENT);
+ } else if (strcmp(name, "$COLS") == 0) {
+ snprintf(buf, sizeof(buf), "%d", cols);
+ sqlite3_bind_text(stmt, lpc + 1, buf, -1, SQLITE_TRANSIENT);
+ }
+ }
+
+ if ((local_var = lvars.find(&name[1])) != lvars.end()) {
+ mapbox::util::apply_visitor(
+ sqlitepp::bind_visitor(stmt, lpc + 1), local_var->second);
+ retval[name] = local_var->second;
+ } else if ((global_var = gvars.find(&name[1])) != gvars.end()) {
+ mapbox::util::apply_visitor(
+ sqlitepp::bind_visitor(stmt, lpc + 1), global_var->second);
+ retval[name] = global_var->second;
+ } else if ((env_value = getenv(&name[1])) != nullptr) {
+ sqlite3_bind_text(stmt, lpc + 1, env_value, -1, SQLITE_STATIC);
+ retval[name] = env_value;
+ }
+ } else if (name[0] == ':' && ec.ec_line_values != nullptr) {
+ for (auto& lv : ec.ec_line_values->lvv_values) {
+ if (lv.lv_meta.lvm_name != &name[1]) {
+ continue;
+ }
+ switch (lv.lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_BOOLEAN:
+ sqlite3_bind_int64(stmt, lpc + 1, lv.lv_value.i);
+ retval[name] = fmt::to_string(lv.lv_value.i);
+ break;
+ case value_kind_t::VALUE_FLOAT:
+ sqlite3_bind_double(stmt, lpc + 1, lv.lv_value.d);
+ retval[name] = fmt::to_string(lv.lv_value.d);
+ break;
+ case value_kind_t::VALUE_INTEGER:
+ sqlite3_bind_int64(stmt, lpc + 1, lv.lv_value.i);
+ retval[name] = fmt::to_string(lv.lv_value.i);
+ break;
+ case value_kind_t::VALUE_NULL:
+ sqlite3_bind_null(stmt, lpc + 1);
+ retval[name] = db_label_source::NULL_STR;
+ break;
+ default:
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ lv.text_value(),
+ lv.text_length(),
+ SQLITE_TRANSIENT);
+ retval[name] = lv.to_string();
+ break;
+ }
+ }
+ } else {
+ sqlite3_bind_null(stmt, lpc + 1);
+ log_warning("Could not bind variable: %s", name);
+ retval[name] = db_label_source::NULL_STR;
+ }
+ }
+
+ return Ok(retval);
+}
+
+static void
+execute_search(const std::string& search_cmd)
+{
+ lnav_data.ld_view_stack.top() | [&search_cmd](auto tc) {
+ auto search_term
+ = string_fragment(search_cmd)
+ .find_right_boundary(0, string_fragment::tag1{'\n'})
+ .to_string();
+ tc->execute_search(search_term);
+ };
+}
+
+Result<std::string, lnav::console::user_message>
+execute_sql(exec_context& ec, const std::string& sql, std::string& alt_msg)
+{
+ db_label_source& dls = lnav_data.ld_db_row_source;
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+ struct timeval start_tv, end_tv;
+ std::string stmt_str = trim(sql);
+ std::string retval;
+ int retcode = SQLITE_OK;
+
+ log_info("Executing SQL: %s", sql.c_str());
+
+ auto old_mode = lnav_data.ld_mode;
+ lnav_data.ld_mode = ln_mode_t::BUSY;
+ auto mode_fin = finally([old_mode]() { lnav_data.ld_mode = old_mode; });
+ lnav_data.ld_bottom_source.grep_error("");
+
+ if (startswith(stmt_str, ".")) {
+ std::vector<std::string> args;
+ split_ws(stmt_str, args);
+
+ auto* sql_cmd_map = injector::get<readline_context::command_map_t*,
+ sql_cmd_map_tag>();
+ auto cmd_iter = sql_cmd_map->find(args[0]);
+
+ if (cmd_iter != sql_cmd_map->end()) {
+ ec.ec_current_help = &cmd_iter->second->c_help;
+ auto retval = cmd_iter->second->c_func(ec, stmt_str, args);
+ ec.ec_current_help = nullptr;
+
+ return retval;
+ }
+ }
+
+ if (stmt_str == ".msgformats") {
+ stmt_str = MSG_FORMAT_STMT;
+ }
+
+ ec.ec_accumulator->clear();
+
+ const auto& source = ec.ec_source.back();
+ sql_progress_guard progress_guard(sql_progress,
+ sql_progress_finished,
+ source.s_location,
+ source.s_content);
+ gettimeofday(&start_tv, nullptr);
+
+ const auto* curr_stmt = stmt_str.c_str();
+ auto last_is_readonly = false;
+ while (curr_stmt != nullptr) {
+ const char* tail = nullptr;
+ while (isspace(*curr_stmt)) {
+ curr_stmt += 1;
+ }
+ retcode = sqlite3_prepare_v2(
+ lnav_data.ld_db.in(), curr_stmt, -1, stmt.out(), &tail);
+ if (retcode != SQLITE_OK) {
+ const char* errmsg = sqlite3_errmsg(lnav_data.ld_db);
+
+ alt_msg = "";
+
+ auto um = lnav::console::user_message::error(
+ "failed to compile SQL statement")
+ .with_reason(errmsg)
+ .with_snippets(ec.ec_source);
+
+ auto annotated_sql = annotate_sql_with_error(
+ lnav_data.ld_db.in(), curr_stmt, tail);
+ auto loc = um.um_snippets.back().s_location;
+ if (curr_stmt == stmt_str.c_str()) {
+ um.um_snippets.pop_back();
+ } else {
+ auto tail_iter = stmt_str.begin();
+
+ std::advance(tail_iter, (curr_stmt - stmt_str.c_str()));
+ loc.sl_line_number
+ += std::count(stmt_str.begin(), tail_iter, '\n');
+ }
+
+ um.with_snippet(lnav::console::snippet::from(loc, annotated_sql));
+
+ return Err(um);
+ }
+ if (stmt == nullptr) {
+ retcode = SQLITE_DONE;
+ break;
+ }
+#ifdef HAVE_SQLITE3_STMT_READONLY
+ last_is_readonly = sqlite3_stmt_readonly(stmt.in());
+ if (ec.is_read_only() && !last_is_readonly) {
+ return ec.make_error(
+ "modifying statements are not allowed in this context: {}",
+ sql);
+ }
+#endif
+ bool done = false;
+
+ auto bound_values = TRY(bind_sql_parameters(ec, stmt.in()));
+ if (lnav_data.ld_rl_view != nullptr) {
+ if (lnav_data.ld_rl_view) {
+ lnav_data.ld_rl_view->set_attr_value(
+ lnav::console::user_message::info(
+ attr_line_t("executing SQL statement, press ")
+ .append("CTRL+]"_hotkey)
+ .append(" to cancel"))
+ .to_attr_line());
+ lnav_data.ld_rl_view->do_update();
+ }
+ }
+
+ ec.ec_sql_callback(ec, stmt.in());
+ while (!done) {
+ retcode = sqlite3_step(stmt.in());
+
+ switch (retcode) {
+ case SQLITE_OK:
+ case SQLITE_DONE:
+ done = true;
+ break;
+
+ case SQLITE_ROW:
+ ec.ec_sql_callback(ec, stmt.in());
+ break;
+
+ default: {
+ attr_line_t bound_note;
+
+ if (!bound_values.empty()) {
+ bound_note.append(
+ "the bound parameters are set as follows:\n");
+ for (const auto& bval : bound_values) {
+ auto val_as_str = fmt::to_string(bval.second);
+ auto sql_type = bval.second.match(
+ [](const std::string&) { return SQLITE_TEXT; },
+ [](const string_fragment&) {
+ return SQLITE_TEXT;
+ },
+ [](int64_t) { return SQLITE_INTEGER; },
+ [](null_value_t) { return SQLITE_NULL; },
+ [](double) { return SQLITE_FLOAT; });
+ auto scrubbed_val = scrub_ws(val_as_str.c_str());
+ truncate_to(scrubbed_val, 40);
+ bound_note.append(" ")
+ .append(lnav::roles::variable(bval.first))
+ .append(":")
+ .append(sqlite3_type_to_string(sql_type))
+ .append(" = ")
+ .append_quoted(scrubbed_val)
+ .append("\n");
+ }
+ }
+
+ log_error("sqlite3_step error code: %d", retcode);
+ auto um = sqlite3_error_to_user_message(lnav_data.ld_db)
+ .with_snippets(ec.ec_source)
+ .with_note(bound_note);
+
+ return Err(um);
+ }
+ }
+ }
+
+ curr_stmt = tail;
+ }
+
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->clear_value();
+ }
+
+ gettimeofday(&end_tv, nullptr);
+ if (retcode == SQLITE_DONE) {
+ if (lnav_data.ld_log_source.is_line_meta_changed()) {
+ lnav_data.ld_log_source.text_filters_changed();
+ lnav_data.ld_views[LNV_LOG].reload_data();
+ }
+ lnav_data.ld_filter_view.reload_data();
+ lnav_data.ld_files_view.reload_data();
+ lnav_data.ld_views[LNV_DB].reload_data();
+ lnav_data.ld_views[LNV_DB].set_left(0);
+
+ lnav_data.ld_active_files.fc_files
+ | lnav::itertools::for_each(&logfile::dump_stats);
+ if (ec.ec_sql_callback != sql_callback) {
+ retval = ec.ec_accumulator->get_string();
+ } else if (!dls.dls_rows.empty()) {
+ if (lnav_data.ld_flags & LNF_HEADLESS) {
+ if (ec.ec_local_vars.size() == 1) {
+ ensure_view(&lnav_data.ld_views[LNV_DB]);
+ }
+
+ retval = "";
+ alt_msg = "";
+ } else if (dls.dls_rows.size() == 1) {
+ auto& row = dls.dls_rows[0];
+
+ if (dls.dls_headers.size() == 1) {
+ retval = row[0];
+ } else {
+ for (unsigned int lpc = 0; lpc < dls.dls_headers.size();
+ lpc++)
+ {
+ if (lpc > 0) {
+ retval.append("; ");
+ }
+ retval.append(dls.dls_headers[lpc].hm_name);
+ retval.push_back('=');
+ retval.append(row[lpc]);
+ }
+ }
+ } else {
+ int row_count = dls.dls_rows.size();
+ char row_count_buf[128];
+ struct timeval diff_tv;
+
+ timersub(&end_tv, &start_tv, &diff_tv);
+ snprintf(row_count_buf,
+ sizeof(row_count_buf),
+ ANSI_BOLD("%'d") " row%s matched in " ANSI_BOLD(
+ "%ld.%03ld") " seconds",
+ row_count,
+ row_count == 1 ? "" : "s",
+ diff_tv.tv_sec,
+ std::max((long) diff_tv.tv_usec / 1000, 1L));
+ retval = row_count_buf;
+ if (dls.has_log_time_column()) {
+ alt_msg = HELP_MSG_1(Q,
+ "to switch back to the previous view "
+ "at the matching 'log_time' value");
+ } else {
+ alt_msg = "";
+ }
+ }
+ }
+#ifdef HAVE_SQLITE3_STMT_READONLY
+ else if (last_is_readonly)
+ {
+ retval = "info: No rows matched";
+ alt_msg = "";
+
+ if (lnav_data.ld_flags & LNF_HEADLESS) {
+ if (ec.ec_local_vars.size() == 1) {
+ ensure_view(&lnav_data.ld_views[LNV_DB]);
+ }
+ }
+ }
+#endif
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+execute_file_contents(exec_context& ec,
+ const ghc::filesystem::path& path,
+ bool multiline)
+{
+ static ghc::filesystem::path stdin_path("-");
+ static ghc::filesystem::path dev_stdin_path("/dev/stdin");
+
+ std::string retval;
+ FILE* file;
+
+ if (path == stdin_path || path == dev_stdin_path) {
+ if (isatty(STDIN_FILENO)) {
+ return ec.make_error("stdin has already been consumed");
+ }
+ file = stdin;
+ } else if ((file = fopen(path.c_str(), "r")) == nullptr) {
+ return ec.make_error("unable to open file");
+ }
+
+ int line_number = 0, starting_line_number = 0;
+ auto_mem<char> line;
+ size_t line_max_size;
+ ssize_t line_size;
+ nonstd::optional<std::string> cmdline;
+
+ ec.ec_path_stack.emplace_back(path.parent_path());
+ exec_context::output_guard og(ec);
+ while ((line_size = getline(line.out(), &line_max_size, file)) != -1) {
+ line_number += 1;
+
+ if (trim(line.in()).empty()) {
+ if (multiline && cmdline) {
+ cmdline = cmdline.value() + "\n";
+ }
+ continue;
+ }
+ if (line[0] == '#') {
+ continue;
+ }
+
+ switch (line[0]) {
+ case ':':
+ case '/':
+ case ';':
+ case '|':
+ if (cmdline) {
+ retval = TRY(execute_from_file(
+ ec, path, starting_line_number, trim(cmdline.value())));
+ }
+
+ starting_line_number = line_number;
+ cmdline = std::string(line);
+ break;
+ default:
+ if (multiline) {
+ cmdline = fmt::format("{}{}", cmdline.value(), line.in());
+ } else {
+ retval = TRY(execute_from_file(
+ ec, path, line_number, fmt::format(":{}", line.in())));
+ }
+ break;
+ }
+ }
+
+ if (cmdline) {
+ retval = TRY(execute_from_file(
+ ec, path, starting_line_number, trim(cmdline.value())));
+ }
+
+ if (file == stdin) {
+ if (isatty(STDOUT_FILENO)) {
+ log_perror(dup2(STDOUT_FILENO, STDIN_FILENO));
+ }
+ } else {
+ fclose(file);
+ }
+ ec.ec_path_stack.pop_back();
+
+ return Ok(retval);
+}
+
+Result<std::string, lnav::console::user_message>
+execute_file(exec_context& ec, const std::string& path_and_args, bool multiline)
+{
+ available_scripts scripts;
+ std::vector<std::string> split_args;
+ std::string retval, msg;
+ shlex lexer(path_and_args);
+
+ log_info("Executing file: %s", path_and_args.c_str());
+
+ if (!lexer.split(split_args, ec.ec_local_vars.top())) {
+ return ec.make_error("unable to parse path");
+ }
+ if (split_args.empty()) {
+ return ec.make_error("no script specified");
+ }
+
+ ec.ec_local_vars.push({});
+
+ auto script_name = split_args[0];
+ auto& vars = ec.ec_local_vars.top();
+ char env_arg_name[32];
+ std::string star, open_error = "file not found";
+
+ add_ansi_vars(vars);
+
+ snprintf(
+ env_arg_name, sizeof(env_arg_name), "%d", (int) split_args.size() - 1);
+
+ vars["#"] = env_arg_name;
+ for (size_t lpc = 0; lpc < split_args.size(); lpc++) {
+ snprintf(env_arg_name, sizeof(env_arg_name), "%lu", lpc);
+ vars[env_arg_name] = split_args[lpc];
+ }
+ for (size_t lpc = 1; lpc < split_args.size(); lpc++) {
+ if (lpc > 1) {
+ star.append(" ");
+ }
+ star.append(split_args[lpc]);
+ }
+ vars["__all__"] = star;
+
+ std::vector<script_metadata> paths_to_exec;
+
+ find_format_scripts(lnav_data.ld_config_paths, scripts);
+ auto iter = scripts.as_scripts.find(script_name);
+ if (iter != scripts.as_scripts.end()) {
+ paths_to_exec = iter->second;
+ }
+ if (script_name == "-" || script_name == "/dev/stdin") {
+ paths_to_exec.push_back({script_name, "", "", ""});
+ } else if (access(script_name.c_str(), R_OK) == 0) {
+ struct script_metadata meta;
+
+ meta.sm_path = script_name;
+ extract_metadata_from_file(meta);
+ paths_to_exec.push_back(meta);
+ } else if (errno != ENOENT) {
+ open_error = strerror(errno);
+ } else {
+ auto script_path = ghc::filesystem::path(script_name);
+
+ if (!script_path.is_absolute()) {
+ script_path = ec.ec_path_stack.back() / script_path;
+ }
+
+ if (ghc::filesystem::is_regular_file(script_path)) {
+ struct script_metadata meta;
+
+ meta.sm_path = script_path;
+ extract_metadata_from_file(meta);
+ paths_to_exec.push_back(meta);
+ } else if (errno != ENOENT) {
+ open_error = strerror(errno);
+ }
+ }
+
+ if (!paths_to_exec.empty()) {
+ for (auto& path_iter : paths_to_exec) {
+ retval
+ = TRY(execute_file_contents(ec, path_iter.sm_path, multiline));
+ }
+ }
+ ec.ec_local_vars.pop();
+
+ if (paths_to_exec.empty()) {
+ return ec.make_error(
+ "unknown script -- {} -- {}", script_name, open_error);
+ }
+
+ return Ok(retval);
+}
+
+Result<std::string, lnav::console::user_message>
+execute_from_file(exec_context& ec,
+ const ghc::filesystem::path& path,
+ int line_number,
+ const std::string& cmdline)
+{
+ std::string retval, alt_msg;
+ auto _sg = ec.enter_source(
+ intern_string::lookup(path.string()), line_number, cmdline);
+
+ switch (cmdline[0]) {
+ case ':':
+ retval = TRY(execute_command(ec, cmdline.substr(1)));
+ break;
+ case '/':
+ execute_search(cmdline.substr(1));
+ break;
+ case ';':
+ setup_logline_table(ec);
+ retval = TRY(execute_sql(ec, cmdline.substr(1), alt_msg));
+ break;
+ case '|':
+ retval = TRY(execute_file(ec, cmdline.substr(1)));
+ break;
+ default:
+ retval = TRY(execute_command(ec, cmdline));
+ break;
+ }
+
+ log_info("%s:%d:execute result -- %s",
+ path.c_str(),
+ line_number,
+ retval.c_str());
+
+ return Ok(retval);
+}
+
+Result<std::string, lnav::console::user_message>
+execute_any(exec_context& ec, const std::string& cmdline_with_mode)
+{
+ std::string retval, alt_msg, cmdline = cmdline_with_mode.substr(1);
+ auto _cleanup = finally([&ec] {
+ if (ec.is_read_write() &&
+ // only rebuild in a script or non-interactive mode so we don't
+ // block the UI.
+ (lnav_data.ld_flags & LNF_HEADLESS || ec.ec_path_stack.size() > 1))
+ {
+ rescan_files();
+ rebuild_indexes_repeatedly();
+ }
+ });
+
+ switch (cmdline_with_mode[0]) {
+ case ':':
+ retval = TRY(execute_command(ec, cmdline));
+ break;
+ case '/':
+ execute_search(cmdline);
+ break;
+ case ';':
+ setup_logline_table(ec);
+ retval = TRY(execute_sql(ec, cmdline, alt_msg));
+ break;
+ case '|': {
+ retval = TRY(execute_file(ec, cmdline));
+ break;
+ }
+ default:
+ retval = TRY(execute_command(ec, cmdline));
+ break;
+ }
+
+ return Ok(retval);
+}
+
+void
+execute_init_commands(
+ exec_context& ec,
+ std::vector<std::pair<Result<std::string, lnav::console::user_message>,
+ std::string>>& msgs)
+{
+ if (lnav_data.ld_cmd_init_done) {
+ return;
+ }
+
+ nonstd::optional<exec_context::output_t> ec_out;
+ auto_fd fd_copy;
+
+ if (!(lnav_data.ld_flags & LNF_HEADLESS)) {
+ auto_mem<FILE> tmpout(fclose);
+
+ tmpout = std::tmpfile();
+ if (!tmpout) {
+ msgs.emplace_back(Err(lnav::console::user_message::error(
+ "Unable to open temporary output file")
+ .with_errno_reason()),
+ "");
+ return;
+ }
+ fd_copy = auto_fd::dup_of(fileno(tmpout));
+ ec_out = std::make_pair(tmpout.release(), fclose);
+ }
+
+ auto& dls = lnav_data.ld_db_row_source;
+ int option_index = 1;
+
+ {
+ log_info("Executing initial commands");
+ exec_context::output_guard og(ec, "tmp", ec_out);
+
+ for (auto& cmd : lnav_data.ld_commands) {
+ static const auto COMMAND_OPTION_SRC
+ = intern_string::lookup("command-option");
+
+ std::string alt_msg;
+
+ wait_for_children();
+
+ log_debug("init cmd: %s", cmd.c_str());
+ {
+ auto _sg
+ = ec.enter_source(COMMAND_OPTION_SRC, option_index++, cmd);
+ switch (cmd.at(0)) {
+ case ':':
+ msgs.emplace_back(execute_command(ec, cmd.substr(1)),
+ alt_msg);
+ break;
+ case '/':
+ execute_search(cmd.substr(1));
+ break;
+ case ';':
+ setup_logline_table(ec);
+ msgs.emplace_back(
+ execute_sql(ec, cmd.substr(1), alt_msg), alt_msg);
+ break;
+ case '|':
+ msgs.emplace_back(execute_file(ec, cmd.substr(1)),
+ alt_msg);
+ break;
+ }
+
+ rescan_files();
+ rebuild_indexes_repeatedly();
+ }
+ if (dls.dls_rows.size() > 1) {
+ ensure_view(LNV_DB);
+ }
+ }
+ }
+ lnav_data.ld_commands.clear();
+
+ struct stat st;
+
+ if (ec_out && fstat(fd_copy, &st) != -1 && st.st_size > 0) {
+ static const auto OUTPUT_NAME = std::string("Initial command output");
+
+ lnav_data.ld_active_files.fc_file_names[OUTPUT_NAME]
+ .with_fd(std::move(fd_copy))
+ .with_include_in_session(false)
+ .with_detect_format(false);
+ lnav_data.ld_files_to_front.emplace_back(OUTPUT_NAME, 0_vl);
+
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(X, "to close the file"));
+ }
+ }
+
+ lnav_data.ld_cmd_init_done = true;
+}
+
+int
+sql_callback(exec_context& ec, sqlite3_stmt* stmt)
+{
+ auto& dls = lnav_data.ld_db_row_source;
+
+ if (!sqlite3_stmt_busy(stmt)) {
+ dls.clear();
+
+ return 0;
+ }
+
+ auto& chart = dls.dls_chart;
+ auto& vc = view_colors::singleton();
+ int ncols = sqlite3_column_count(stmt);
+ int row_number;
+ int lpc, retval = 0;
+ auto set_vars = false;
+
+ row_number = dls.dls_rows.size();
+ dls.dls_rows.resize(row_number + 1);
+ if (dls.dls_headers.empty()) {
+ for (lpc = 0; lpc < ncols; lpc++) {
+ int type = sqlite3_column_type(stmt, lpc);
+ std::string colname = sqlite3_column_name(stmt, lpc);
+ bool graphable;
+
+ graphable = ((type == SQLITE_INTEGER || type == SQLITE_FLOAT)
+ && !binary_search(lnav_data.ld_db_key_names.begin(),
+ lnav_data.ld_db_key_names.end(),
+ colname));
+
+ dls.push_header(colname, type, graphable);
+ if (graphable) {
+ auto name_for_ident_attrs = colname;
+ auto attrs = vc.attrs_for_ident(name_for_ident_attrs);
+ for (size_t attempt = 0;
+ chart.attrs_in_use(attrs) && attempt < 3;
+ attempt++)
+ {
+ name_for_ident_attrs += " ";
+ attrs = vc.attrs_for_ident(name_for_ident_attrs);
+ }
+ chart.with_attrs_for_ident(colname, attrs);
+ dls.dls_headers.back().hm_title_attrs = attrs;
+ }
+ }
+ set_vars = true;
+ }
+ for (lpc = 0; lpc < ncols; lpc++) {
+ auto* raw_value = sqlite3_column_value(stmt, lpc);
+ auto value_type = sqlite3_value_type(raw_value);
+ scoped_value_t value;
+ auto& hm = dls.dls_headers[lpc];
+
+ switch (value_type) {
+ case SQLITE_INTEGER:
+ value = (int64_t) sqlite3_value_int64(raw_value);
+ break;
+ case SQLITE_FLOAT:
+ value = sqlite3_value_double(raw_value);
+ break;
+ case SQLITE_NULL:
+ value = null_value_t{};
+ break;
+ default:
+ value = string_fragment::from_bytes(
+ sqlite3_value_text(raw_value),
+ sqlite3_value_bytes(raw_value));
+ break;
+ }
+ dls.push_column(value);
+ if ((hm.hm_column_type == SQLITE_TEXT
+ || hm.hm_column_type == SQLITE_NULL)
+ && hm.hm_sub_type == 0)
+ {
+ switch (value_type) {
+ case SQLITE_TEXT:
+ hm.hm_column_type = SQLITE_TEXT;
+ hm.hm_sub_type = sqlite3_value_subtype(raw_value);
+ break;
+ }
+ }
+ if (set_vars && !ec.ec_local_vars.empty() && !ec.ec_dry_run) {
+ if (sql_ident_needs_quote(hm.hm_name.c_str())) {
+ continue;
+ }
+ auto& vars = ec.ec_local_vars.top();
+
+ if (value.is<string_fragment>()) {
+ value = value.get<string_fragment>().to_string();
+ }
+ vars[hm.hm_name] = value;
+ }
+ }
+
+ return retval;
+}
+
+std::future<std::string>
+pipe_callback(exec_context& ec, const std::string& cmdline, auto_fd& fd)
+{
+ auto out = ec.get_output();
+
+ if (out) {
+ FILE* file = *out;
+
+ return std::async(std::launch::async, [&fd, file]() {
+ char buffer[1024];
+ ssize_t rc;
+
+ if (file == stdout) {
+ lnav_data.ld_stdout_used = true;
+ }
+
+ while ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
+ fwrite(buffer, rc, 1, file);
+ }
+
+ return std::string();
+ });
+ }
+ auto tmp_fd
+ = lnav::filesystem::open_temp_file(
+ ghc::filesystem::temp_directory_path() / "lnav.out.XXXXXX")
+ .map([](auto pair) {
+ ghc::filesystem::remove(pair.first);
+
+ return std::move(pair.second);
+ })
+ .expect("Cannot create temporary file for callback");
+ auto pp
+ = std::make_shared<piper_proc>(std::move(fd), false, std::move(tmp_fd));
+ static int exec_count = 0;
+
+ lnav_data.ld_pipers.push_back(pp);
+ auto desc
+ = fmt::format(FMT_STRING("[{}] Output of {}"), exec_count++, cmdline);
+ lnav_data.ld_active_files.fc_file_names[desc]
+ .with_fd(pp->get_fd())
+ .with_include_in_session(false)
+ .with_detect_format(false);
+ lnav_data.ld_files_to_front.emplace_back(desc, 0_vl);
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1(X, "to close the file"));
+ }
+
+ return lnav::futures::make_ready_future(std::string());
+}
+
+void
+add_global_vars(exec_context& ec)
+{
+ for (const auto& iter : lnav_config.lc_global_vars) {
+ shlex subber(iter.second);
+ std::string str;
+
+ if (!subber.eval(str, ec.ec_global_vars)) {
+ log_error("Unable to evaluate global variable value: %s",
+ iter.second.c_str());
+ continue;
+ }
+
+ ec.ec_global_vars[iter.first] = str;
+ }
+}
+
+void
+exec_context::set_output(const std::string& name,
+ FILE* file,
+ int (*closer)(FILE*))
+{
+ log_info("redirecting command output to: %s", name.c_str());
+ this->ec_output_stack.back().second | [](auto out) {
+ if (out.second != nullptr) {
+ out.second(out.first);
+ }
+ };
+ this->ec_output_stack.back()
+ = std::make_pair(name, std::make_pair(file, closer));
+}
+
+void
+exec_context::clear_output()
+{
+ log_info("redirecting command output to screen");
+ this->ec_output_stack.back().second | [](auto out) {
+ if (out.second != nullptr) {
+ out.second(out.first);
+ }
+ };
+ this->ec_output_stack.back() = std::make_pair("default", nonstd::nullopt);
+}
+
+exec_context::exec_context(logline_value_vector* line_values,
+ sql_callback_t sql_callback,
+ pipe_callback_t pipe_callback)
+ : ec_line_values(line_values),
+ ec_accumulator(std::make_unique<attr_line_t>()),
+ ec_sql_callback(sql_callback), ec_pipe_callback(pipe_callback)
+{
+ static const auto COMMAND_SRC = intern_string::lookup("command");
+
+ this->ec_local_vars.push(std::map<std::string, scoped_value_t>());
+ this->ec_path_stack.emplace_back(".");
+ this->ec_source.emplace_back(
+ lnav::console::snippet::from(COMMAND_SRC, "").with_line(1));
+ this->ec_output_stack.emplace_back("screen", nonstd::nullopt);
+ this->ec_error_callback_stack.emplace_back(
+ [](const auto& um) { lnav::console::print(stderr, um); });
+}
+
+void
+exec_context::execute(const std::string& cmdline)
+{
+ auto exec_res = execute_any(*this, cmdline);
+ if (exec_res.isErr()) {
+ this->ec_error_callback_stack.back()(exec_res.unwrapErr());
+ }
+}
+
+void
+exec_context::add_error_context(lnav::console::user_message& um)
+{
+ switch (um.um_level) {
+ case lnav::console::user_message::level::raw:
+ case lnav::console::user_message::level::info:
+ case lnav::console::user_message::level::ok:
+ return;
+ default:
+ break;
+ }
+
+ if (um.um_snippets.empty()) {
+ um.with_snippets(this->ec_source);
+ }
+
+ if (this->ec_current_help != nullptr && um.um_help.empty()) {
+ attr_line_t help;
+
+ format_help_text_for_term(*this->ec_current_help,
+ 70,
+ help,
+ help_text_content::synopsis_and_summary);
+ um.with_help(help);
+ }
+}
+
+exec_context::source_guard
+exec_context::enter_source(intern_string_t path,
+ int line_number,
+ const std::string& content)
+{
+ attr_line_t content_al{content};
+ content_al.with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ readline_lnav_highlighter(content_al, -1);
+ this->ec_source.emplace_back(
+ lnav::console::snippet::from(path, content_al).with_line(line_number));
+ return {this};
+}
+
+exec_context::output_guard::output_guard(exec_context& context,
+ std::string name,
+ const nonstd::optional<output_t>& file)
+ : sg_context(context)
+{
+ if (file) {
+ log_info("redirecting command output to: %s", name.c_str());
+ }
+ context.ec_output_stack.emplace_back(std::move(name), file);
+}
+
+exec_context::output_guard::~output_guard()
+{
+ this->sg_context.clear_output();
+ this->sg_context.ec_output_stack.pop_back();
+}
diff --git a/src/command_executor.hh b/src/command_executor.hh
new file mode 100644
index 0000000..4461d55
--- /dev/null
+++ b/src/command_executor.hh
@@ -0,0 +1,262 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LNAV_COMMAND_EXECUTOR_H
+#define LNAV_COMMAND_EXECUTOR_H
+
+#include <future>
+#include <stack>
+#include <string>
+
+#include <sqlite3.h>
+
+#include "base/auto_fd.hh"
+#include "base/lnav.console.hh"
+#include "bookmarks.hh"
+#include "fmt/format.h"
+#include "ghc/filesystem.hpp"
+#include "help_text.hh"
+#include "optional.hpp"
+#include "shlex.resolver.hh"
+#include "vis_line.hh"
+
+struct exec_context;
+class attr_line_t;
+class logline_value;
+struct logline_value_vector;
+
+using sql_callback_t = int (*)(exec_context&, sqlite3_stmt*);
+int sql_callback(exec_context& ec, sqlite3_stmt* stmt);
+
+using pipe_callback_t
+ = std::future<std::string> (*)(exec_context&, const std::string&, auto_fd&);
+
+using error_callback_t
+ = std::function<void(const lnav::console::user_message&)>;
+
+struct exec_context {
+ enum class perm_t {
+ READ_WRITE,
+ READ_ONLY,
+ };
+
+ using output_t = std::pair<FILE*, int (*)(FILE*)>;
+
+ exec_context(logline_value_vector* line_values = nullptr,
+ sql_callback_t sql_callback = ::sql_callback,
+ pipe_callback_t pipe_callback = nullptr);
+
+ bool is_read_write() const
+ {
+ return this->ec_perms == perm_t::READ_WRITE;
+ }
+
+ bool is_read_only() const
+ {
+ return this->ec_perms == perm_t::READ_ONLY;
+ }
+
+ exec_context& with_perms(perm_t perms)
+ {
+ this->ec_perms = perms;
+ return *this;
+ }
+
+ void add_error_context(lnav::console::user_message& um);
+
+ template<typename... Args>
+ Result<std::string, lnav::console::user_message> make_error(
+ fmt::string_view format_str, const Args&... args)
+ {
+ auto retval = lnav::console::user_message::error(
+ fmt::vformat(format_str, fmt::make_format_args(args...)));
+
+ this->add_error_context(retval);
+
+ return Err(retval);
+ }
+
+ nonstd::optional<FILE*> get_output()
+ {
+ for (auto iter = this->ec_output_stack.rbegin();
+ iter != this->ec_output_stack.rend();
+ ++iter)
+ {
+ if (iter->second && (*iter->second).first) {
+ return (*iter->second).first;
+ }
+ }
+
+ return nonstd::nullopt;
+ }
+
+ void set_output(const std::string& name, FILE* file, int (*closer)(FILE*));
+
+ void clear_output();
+
+ struct source_guard {
+ source_guard(exec_context* context) : sg_context(context) {}
+
+ source_guard(const source_guard&) = delete;
+
+ source_guard(source_guard&& other) : sg_context(other.sg_context)
+ {
+ other.sg_context = nullptr;
+ }
+
+ ~source_guard()
+ {
+ if (this->sg_context != nullptr) {
+ this->sg_context->ec_source.pop_back();
+ }
+ }
+
+ exec_context* sg_context;
+ };
+
+ struct output_guard {
+ explicit output_guard(exec_context& context,
+ std::string name = "default",
+ const nonstd::optional<output_t>& file
+ = nonstd::nullopt);
+
+ ~output_guard();
+
+ exec_context& sg_context;
+ };
+
+ source_guard enter_source(intern_string_t path,
+ int line_number,
+ const std::string& content);
+
+ struct error_cb_guard {
+ error_cb_guard(exec_context* context) : sg_context(context) {}
+
+ error_cb_guard(const error_cb_guard&) = delete;
+ error_cb_guard(error_cb_guard&& other) : sg_context(other.sg_context)
+ {
+ other.sg_context = nullptr;
+ }
+
+ ~error_cb_guard()
+ {
+ if (this->sg_context != nullptr) {
+ this->sg_context->ec_error_callback_stack.pop_back();
+ }
+ }
+
+ exec_context* sg_context;
+ };
+
+ error_cb_guard add_error_callback(error_callback_t cb)
+ {
+ this->ec_error_callback_stack.emplace_back(std::move(cb));
+ return {this};
+ }
+
+ scoped_resolver create_resolver()
+ {
+ return {
+ &this->ec_local_vars.top(),
+ &this->ec_global_vars,
+ };
+ }
+
+ void execute(const std::string& cmdline);
+
+ using kv_pair_t = std::pair<std::string, std::string>;
+
+ void execute_with_int(const std::string& cmdline)
+ {
+ this->execute(cmdline);
+ }
+
+ template<typename... Args>
+ void execute_with_int(const std::string& cmdline,
+ kv_pair_t pair,
+ Args... args)
+ {
+ this->ec_local_vars.top().template emplace(pair);
+ this->execute(cmdline, args...);
+ }
+
+ template<typename... Args>
+ void execute_with(const std::string& cmdline, Args... args)
+ {
+ this->ec_local_vars.push({});
+ this->execute_with_int(cmdline, args...);
+ this->ec_local_vars.pop();
+ }
+
+ vis_line_t ec_top_line{0_vl};
+ bool ec_dry_run{false};
+ perm_t ec_perms{perm_t::READ_WRITE};
+
+ std::map<std::string, std::string> ec_override;
+ logline_value_vector* ec_line_values;
+ std::stack<std::map<std::string, scoped_value_t>> ec_local_vars;
+ std::map<std::string, scoped_value_t> ec_global_vars;
+ std::vector<ghc::filesystem::path> ec_path_stack;
+ std::vector<lnav::console::snippet> ec_source;
+ help_text* ec_current_help{nullptr};
+
+ std::vector<std::pair<std::string, nonstd::optional<output_t>>>
+ ec_output_stack;
+
+ std::unique_ptr<attr_line_t> ec_accumulator;
+
+ sql_callback_t ec_sql_callback;
+ pipe_callback_t ec_pipe_callback;
+ std::vector<error_callback_t> ec_error_callback_stack;
+};
+
+Result<std::string, lnav::console::user_message> execute_command(
+ exec_context& ec, const std::string& cmdline);
+
+Result<std::string, lnav::console::user_message> execute_sql(
+ exec_context& ec, const std::string& sql, std::string& alt_msg);
+Result<std::string, lnav::console::user_message> execute_file(
+ exec_context& ec, const std::string& path_and_args, bool multiline = true);
+Result<std::string, lnav::console::user_message> execute_any(
+ exec_context& ec, const std::string& cmdline);
+void execute_init_commands(
+ exec_context& ec,
+ std::vector<std::pair<Result<std::string, lnav::console::user_message>,
+ std::string>>& msgs);
+
+std::future<std::string> pipe_callback(exec_context& ec,
+ const std::string& cmdline,
+ auto_fd& fd);
+
+int sql_progress(const struct log_cursor& lc);
+void sql_progress_finished();
+
+void add_global_vars(exec_context& ec);
+
+#endif // LNAV_COMMAND_EXECUTOR_H
diff --git a/src/config.cmake.h.in b/src/config.cmake.h.in
new file mode 100644
index 0000000..56803fb
--- /dev/null
+++ b/src/config.cmake.h.in
@@ -0,0 +1,31 @@
+
+#define HAVE_PCRE_H
+#define HAVE_NCURSESW_CURSES_H
+#define HAVE_ARCHIVE_H 1
+#define HAVE_BZLIB_H 1
+
+#define HAVE_LIBCURL
+
+#cmakedefine SIZEOF_OFF_T @SIZEOF_OFF_T@
+
+#cmakedefine VCS_PACKAGE_STRING "@VCS_PACKAGE_STRING@"
+
+#cmakedefine PACKAGE_VERSION "@PACKAGE_VERSION@"
+
+#cmakedefine HAVE_PTY_H
+
+#cmakedefine HAVE_UTIL_H
+
+#cmakedefine HAVE_EXECINFO_H
+
+#define HAVE_SQLITE3_STMT_READONLY
+
+#define HAVE_SQLITE3_VALUE_SUBTYPE
+
+#define HAVE_SQLITE3_ERROR_OFFSET
+
+#define HAVE_SQLITE3_DROP_MODULES
+
+#define _XOPEN_SOURCE_EXTENDED 1
+
+#define PACKAGE_BUGREPORT "lnav@googlegroups.com"
diff --git a/src/curl_looper.cc b/src/curl_looper.cc
new file mode 100644
index 0000000..27e41fe
--- /dev/null
+++ b/src/curl_looper.cc
@@ -0,0 +1,358 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file curl_looper.cc
+ */
+
+#include <algorithm>
+
+#include "config.h"
+
+#if defined(HAVE_LIBCURL)
+# include <curl/multi.h>
+
+# include "curl_looper.hh"
+
+using namespace std::chrono_literals;
+
+# if !CURL_AT_LEAST_VERSION(7, 80, 0)
+extern "C"
+{
+const char*
+curl_url_strerror(CURLUcode error)
+{
+ switch (error) {
+ case CURLUE_OK:
+ return "No error";
+
+ case CURLUE_BAD_HANDLE:
+ return "An invalid CURLU pointer was passed as argument";
+
+ case CURLUE_BAD_PARTPOINTER:
+ return "An invalid 'part' argument was passed as argument";
+
+ case CURLUE_MALFORMED_INPUT:
+ return "Malformed input to a URL function";
+
+ case CURLUE_BAD_PORT_NUMBER:
+ return "Port number was not a decimal number between 0 and 65535";
+
+ case CURLUE_UNSUPPORTED_SCHEME:
+ return "Unsupported URL scheme";
+
+ case CURLUE_URLDECODE:
+ return "URL decode error, most likely because of rubbish in the "
+ "input";
+
+ case CURLUE_OUT_OF_MEMORY:
+ return "A memory function failed";
+
+ case CURLUE_USER_NOT_ALLOWED:
+ return "Credentials was passed in the URL when prohibited";
+
+ case CURLUE_UNKNOWN_PART:
+ return "An unknown part ID was passed to a URL API function";
+
+ case CURLUE_NO_SCHEME:
+ return "No scheme part in the URL";
+
+ case CURLUE_NO_USER:
+ return "No user part in the URL";
+
+ case CURLUE_NO_PASSWORD:
+ return "No password part in the URL";
+
+ case CURLUE_NO_OPTIONS:
+ return "No options part in the URL";
+
+ case CURLUE_NO_HOST:
+ return "No host part in the URL";
+
+ case CURLUE_NO_PORT:
+ return "No port part in the URL";
+
+ case CURLUE_NO_QUERY:
+ return "No query part in the URL";
+
+ case CURLUE_NO_FRAGMENT:
+ return "No fragment part in the URL";
+ }
+
+ return "CURLUcode unknown";
+}
+}
+# endif
+
+struct curl_request_eq {
+ explicit curl_request_eq(const std::string& name) : cre_name(name){};
+
+ bool operator()(const std::shared_ptr<curl_request>& cr) const
+ {
+ return this->cre_name == cr->get_name();
+ }
+
+ bool operator()(
+ const std::pair<mstime_t, std::shared_ptr<curl_request>>& pair) const
+ {
+ return this->cre_name == pair.second->get_name();
+ }
+
+ const std::string& cre_name;
+};
+
+int
+curl_request::debug_cb(
+ CURL* handle, curl_infotype type, char* data, size_t size, void* userp)
+{
+ curl_request* cr = (curl_request*) userp;
+ bool write_to_log;
+
+ switch (type) {
+ case CURLINFO_TEXT:
+ write_to_log = true;
+ break;
+ case CURLINFO_HEADER_IN:
+ case CURLINFO_HEADER_OUT:
+ if (lnav_log_level == lnav_log_level_t::TRACE) {
+ write_to_log = true;
+ } else {
+ write_to_log = false;
+ }
+ break;
+ default:
+ write_to_log = false;
+ break;
+ }
+
+ if (write_to_log) {
+ while (size > 0 && isspace(data[size - 1])) {
+ size -= 1;
+ }
+ log_debug("%s:%.*s", cr->get_name().c_str(), size, data);
+ }
+
+ return 0;
+}
+
+size_t
+curl_request::string_cb(void* data, size_t size, size_t nmemb, void* userp)
+{
+ auto realsize = size * nmemb;
+ auto& vec = *static_cast<std::string*>(userp);
+
+ vec.append((char*) data, ((char*) data) + realsize);
+
+ return realsize;
+}
+
+long
+curl_request::complete(CURLcode result)
+{
+ double total_time = 0, download_size = 0, download_speed = 0;
+
+ this->cr_completions += 1;
+ curl_easy_getinfo(this->cr_handle, CURLINFO_TOTAL_TIME, &total_time);
+ log_debug("%s: total_time=%f", this->cr_name.c_str(), total_time);
+ curl_easy_getinfo(this->cr_handle, CURLINFO_SIZE_DOWNLOAD, &download_size);
+ log_debug("%s: download_size=%f", this->cr_name.c_str(), download_size);
+ curl_easy_getinfo(
+ this->cr_handle, CURLINFO_SPEED_DOWNLOAD, &download_speed);
+ log_debug("%s: download_speed=%f", this->cr_name.c_str(), download_speed);
+
+ return -1;
+}
+
+void
+curl_looper::loop_body()
+{
+ mstime_t current_time = getmstime();
+
+ this->perform_io();
+
+ this->check_for_finished_requests();
+
+ this->check_for_new_requests();
+
+ this->requeue_requests(current_time + 5);
+}
+
+void
+curl_looper::perform_io()
+{
+ if (this->cl_handle_to_request.empty()) {
+ return;
+ }
+
+ mstime_t current_time = getmstime();
+ auto timeout = this->compute_timeout(current_time);
+ int running_handles;
+
+ if (timeout < 1ms) {
+ timeout = 5ms;
+ }
+ curl_multi_wait(this->cl_curl_multi, nullptr, 0, timeout.count(), nullptr);
+ curl_multi_perform(this->cl_curl_multi, &running_handles);
+}
+
+void
+curl_looper::requeue_requests(mstime_t up_to_time)
+{
+ while (!this->cl_poll_queue.empty()
+ && this->cl_poll_queue.front().first <= up_to_time)
+ {
+ auto cr = this->cl_poll_queue.front().second;
+
+ log_debug("%s:polling request is ready again -- %p",
+ cr->get_name().c_str(),
+ cr.get());
+ this->cl_handle_to_request[cr->get_handle()] = cr;
+ curl_multi_add_handle(this->cl_curl_multi, cr->get_handle());
+ this->cl_poll_queue.erase(this->cl_poll_queue.begin());
+ }
+}
+
+void
+curl_looper::check_for_new_requests()
+{
+ while (!this->cl_new_requests.empty()) {
+ auto cr = this->cl_new_requests.back();
+
+ log_info("%s:new curl request %p", cr->get_name().c_str(), cr.get());
+ this->cl_handle_to_request[cr->get_handle()] = cr;
+ curl_multi_add_handle(this->cl_curl_multi, cr->get_handle());
+ this->cl_new_requests.pop_back();
+ }
+ while (!this->cl_close_requests.empty()) {
+ const std::string& name = this->cl_close_requests.back();
+ auto all_iter = find_if(this->cl_all_requests.begin(),
+ this->cl_all_requests.end(),
+ curl_request_eq(name));
+
+ log_info("attempting to close request -- %s", name.c_str());
+ if (all_iter != this->cl_all_requests.end()) {
+ auto cr = *all_iter;
+
+ log_info(
+ "%s:closing request -- %p", cr->get_name().c_str(), cr.get());
+ (*all_iter)->close();
+ auto act_iter = this->cl_handle_to_request.find(cr->get_handle());
+ if (act_iter != this->cl_handle_to_request.end()) {
+ curl_multi_remove_handle(this->cl_curl_multi, cr->get_handle());
+ this->cl_handle_to_request.erase(act_iter);
+ }
+ auto poll_iter = find_if(this->cl_poll_queue.begin(),
+ this->cl_poll_queue.end(),
+ curl_request_eq(name));
+ if (poll_iter != this->cl_poll_queue.end()) {
+ this->cl_poll_queue.erase(poll_iter);
+ }
+ this->cl_all_requests.erase(all_iter);
+ } else {
+ log_error("Unable to find request with the name -- %s",
+ name.c_str());
+ }
+
+ this->cl_close_requests.pop_back();
+ }
+}
+
+void
+curl_looper::check_for_finished_requests()
+{
+ CURLMsg* msg;
+ int msgs_left;
+
+ while ((msg = curl_multi_info_read(this->cl_curl_multi, &msgs_left))
+ != nullptr)
+ {
+ if (msg->msg != CURLMSG_DONE) {
+ continue;
+ }
+
+ CURL* easy = msg->easy_handle;
+ auto iter = this->cl_handle_to_request.find(easy);
+
+ curl_multi_remove_handle(this->cl_curl_multi, easy);
+ if (iter != this->cl_handle_to_request.end()) {
+ auto cr = iter->second;
+ this->cl_handle_to_request.erase(iter);
+ auto delay_ms = cr->complete(msg->data.result);
+ if (delay_ms < 0) {
+ log_info("%s:curl_request %p finished, deleting...",
+ cr->get_name().c_str(),
+ cr.get());
+ auto all_iter = find(this->cl_all_requests.begin(),
+ this->cl_all_requests.end(),
+ cr);
+ if (all_iter != this->cl_all_requests.end()) {
+ this->cl_all_requests.erase(all_iter);
+ }
+ } else {
+ log_debug("%s:curl_request %p is polling, requeueing in %d",
+ cr->get_name().c_str(),
+ cr.get(),
+ delay_ms);
+ this->cl_poll_queue.emplace_back(getmstime() + delay_ms, cr);
+ sort(this->cl_poll_queue.begin(), this->cl_poll_queue.end());
+ }
+ }
+ }
+}
+
+std::chrono::milliseconds
+curl_looper::compute_timeout(mstime_t current_time) const
+{
+ std::chrono::milliseconds retval = 1s;
+
+ if (!this->cl_handle_to_request.empty()) {
+ retval = 0ms;
+ } else if (!this->cl_poll_queue.empty()) {
+ retval
+ = std::max(1ms,
+ std::chrono::milliseconds(
+ this->cl_poll_queue.front().first - current_time));
+ }
+
+ return retval;
+}
+
+void
+curl_looper::process_all()
+{
+ this->check_for_new_requests();
+
+ this->requeue_requests(LONG_MAX);
+
+ while (!this->cl_handle_to_request.empty()) {
+ this->perform_io();
+
+ this->check_for_finished_requests();
+ }
+}
+
+#endif
diff --git a/src/curl_looper.hh b/src/curl_looper.hh
new file mode 100644
index 0000000..fbfbf2e
--- /dev/null
+++ b/src/curl_looper.hh
@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file curl_looper.hh
+ */
+
+#ifndef curl_looper_hh
+#define curl_looper_hh
+
+#include <atomic>
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/isc.hh"
+#include "base/lnav.console.hh"
+#include "base/result.h"
+#include "config.h"
+
+#if !defined(HAVE_LIBCURL)
+
+typedef int CURLcode;
+
+class curl_request {
+public:
+ curl_request(const std::string& name){};
+};
+
+class curl_looper : public isc::service<curl_looper> {
+public:
+ void start(){};
+ void stop(){};
+ void add_request(std::shared_ptr<curl_request> cr){};
+ void close_request(const std::string& name){};
+ void process_all(){};
+};
+
+#else
+# include <condition_variable>
+# include <mutex>
+# include <thread>
+
+# include <curl/curl.h>
+
+# include "base/auto_mem.hh"
+# include "base/lnav_log.hh"
+# include "base/time_util.hh"
+
+class curl_request {
+public:
+ curl_request(std::string name)
+ : cr_name(std::move(name)), cr_handle(curl_easy_cleanup)
+ {
+ this->cr_handle.reset(curl_easy_init());
+ curl_easy_setopt(this->cr_handle, CURLOPT_NOSIGNAL, 1);
+ curl_easy_setopt(
+ this->cr_handle, CURLOPT_ERRORBUFFER, this->cr_error_buffer);
+ curl_easy_setopt(this->cr_handle, CURLOPT_DEBUGFUNCTION, debug_cb);
+ curl_easy_setopt(this->cr_handle, CURLOPT_DEBUGDATA, this);
+ curl_easy_setopt(this->cr_handle, CURLOPT_VERBOSE, 1);
+ if (getenv("SSH_AUTH_SOCK") != nullptr) {
+ curl_easy_setopt(this->cr_handle,
+ CURLOPT_SSH_AUTH_TYPES,
+# ifdef CURLSSH_AUTH_AGENT
+ CURLSSH_AUTH_AGENT |
+# endif
+ CURLSSH_AUTH_PASSWORD);
+ }
+ }
+
+ virtual ~curl_request() = default;
+
+ const std::string& get_name() const { return this->cr_name; }
+
+ virtual void close() { this->cr_open = false; }
+
+ bool is_open() const { return this->cr_open; }
+
+ CURL* get_handle() const { return this->cr_handle; }
+
+ operator CURL*() const { return this->cr_handle; }
+
+ int get_completions() const { return this->cr_completions; }
+
+ virtual long complete(CURLcode result);
+
+ Result<std::string, CURLcode> perform()
+ {
+ std::string response;
+
+ curl_easy_setopt(this->get_handle(), CURLOPT_WRITEFUNCTION, string_cb);
+ curl_easy_setopt(this->get_handle(), CURLOPT_WRITEDATA, &response);
+
+ auto rc = curl_easy_perform(this->get_handle());
+ if (rc == CURLE_OK) {
+ return Ok(response);
+ }
+
+ return Err(rc);
+ }
+
+ long get_response_code() const
+ {
+ long retval;
+
+ curl_easy_getinfo(this->get_handle(), CURLINFO_RESPONSE_CODE, &retval);
+ return retval;
+ }
+
+protected:
+ static int debug_cb(
+ CURL* handle, curl_infotype type, char* data, size_t size, void* userp);
+
+ static size_t string_cb(void* data, size_t size, size_t nmemb, void* userp);
+
+ const std::string cr_name;
+ bool cr_open{true};
+ auto_mem<CURL> cr_handle;
+ char cr_error_buffer[CURL_ERROR_SIZE];
+ int cr_completions{0};
+};
+
+class curl_looper : public isc::service<curl_looper> {
+public:
+ curl_looper() : cl_curl_multi(curl_multi_cleanup)
+ {
+ this->cl_curl_multi.reset(curl_multi_init());
+ }
+
+ void process_all();
+
+ void add_request(const std::shared_ptr<curl_request>& cr)
+ {
+ require(cr != nullptr);
+
+ this->cl_all_requests.emplace_back(cr);
+ this->cl_new_requests.emplace_back(cr);
+ }
+
+ void close_request(const std::string& name)
+ {
+ this->cl_close_requests.emplace_back(name);
+ }
+
+protected:
+ void loop_body() override;
+
+private:
+ void perform_io();
+ void check_for_new_requests();
+ void check_for_finished_requests();
+ void requeue_requests(mstime_t up_to_time);
+ std::chrono::milliseconds compute_timeout(
+ mstime_t current_time) const override;
+
+ auto_mem<CURLM> cl_curl_multi;
+ std::vector<std::shared_ptr<curl_request> > cl_all_requests;
+ std::vector<std::shared_ptr<curl_request> > cl_new_requests;
+ std::vector<std::string> cl_close_requests;
+ std::map<CURL*, std::shared_ptr<curl_request> > cl_handle_to_request;
+ std::vector<std::pair<mstime_t, std::shared_ptr<curl_request> > >
+ cl_poll_queue;
+};
+#endif
+
+#endif
diff --git a/src/data_parser.cc b/src/data_parser.cc
new file mode 100644
index 0000000..a751b30
--- /dev/null
+++ b/src/data_parser.cc
@@ -0,0 +1,1071 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+
+#include "data_parser.hh"
+
+#include "config.h"
+#include "spookyhash/SpookyV2.h"
+
+data_format data_parser::FORMAT_SEMI("semi", DT_COMMA, DT_SEMI);
+data_format data_parser::FORMAT_COMMA("comma", DT_INVALID, DT_COMMA);
+data_format data_parser::FORMAT_PLAIN("plain", DT_INVALID, DT_INVALID);
+
+data_parser::data_parser(data_scanner* ds)
+ : dp_errors("dp_errors", __FILE__, __LINE__),
+ dp_pairs("dp_pairs", __FILE__, __LINE__), dp_msg_format(nullptr),
+ dp_msg_format_begin(ds->get_init_offset()), dp_scanner(ds)
+{
+ if (TRACE_FILE != nullptr) {
+ fprintf(TRACE_FILE, "input %s\n", ds->get_input().to_string().c_str());
+ }
+}
+
+void
+data_parser::pairup(data_parser::schema_id_t* schema,
+ data_parser::element_list_t& pairs_out,
+ data_parser::element_list_t& in_list,
+ int group_depth)
+{
+ element_list_t ELEMENT_LIST_T(el_stack), ELEMENT_LIST_T(free_row),
+ ELEMENT_LIST_T(key_comps), ELEMENT_LIST_T(value),
+ ELEMENT_LIST_T(prefix);
+ SpookyHash context;
+
+ require(in_list.el_format.df_name != nullptr);
+
+ POINT_TRACE("pairup_start");
+
+ FORMAT_TRACE(in_list);
+
+ for (auto iter = in_list.begin(); iter != in_list.end(); ++iter) {
+ if (iter->e_token == DNT_GROUP) {
+ element_list_t ELEMENT_LIST_T(group_pairs);
+
+ this->pairup(
+ nullptr, group_pairs, *iter->e_sub_elements, group_depth + 1);
+ if (!group_pairs.empty()) {
+ iter->assign_elements(group_pairs);
+ }
+ }
+
+ if (in_list.el_format.df_prefix_terminator != DT_INVALID) {
+ if (iter->e_token == in_list.el_format.df_prefix_terminator) {
+ in_list.el_format.df_prefix_terminator = DT_INVALID;
+ } else {
+ el_stack.PUSH_BACK(*iter);
+ }
+ } else if (iter->e_token == in_list.el_format.df_terminator) {
+ this->end_of_value(
+ el_stack, key_comps, value, in_list, group_depth);
+
+ key_comps.PUSH_BACK(*iter);
+ } else if (iter->e_token == in_list.el_format.df_qualifier) {
+ value.SPLICE(
+ value.end(), key_comps, key_comps.begin(), key_comps.end());
+ strip(value, element_is_space{});
+ if (!value.empty()) {
+ el_stack.PUSH_BACK(element(value, DNT_VALUE));
+ }
+ } else if (iter->e_token == in_list.el_format.df_separator) {
+ auto key_iter = key_comps.end();
+ bool found = false, key_is_values = true;
+
+ if (!key_comps.empty()) {
+ do {
+ --key_iter;
+ if (key_iter->e_token == in_list.el_format.df_appender) {
+ ++key_iter;
+ value.SPLICE(value.end(),
+ key_comps,
+ key_comps.begin(),
+ key_iter);
+ key_comps.POP_FRONT();
+ found = true;
+ } else if (key_iter->e_token
+ == in_list.el_format.df_terminator)
+ {
+ std::vector<element> key_copy;
+
+ value.SPLICE(value.end(),
+ key_comps,
+ key_comps.begin(),
+ key_iter);
+ key_comps.POP_FRONT();
+ strip(key_comps, element_is_space{});
+ if (key_comps.empty()) {
+ key_iter = key_comps.end();
+ } else {
+ key_iter = key_comps.begin();
+ }
+ found = true;
+ }
+ if (key_iter != key_comps.end()) {
+ switch (key_iter->e_token) {
+ case DT_WORD:
+ case DT_SYMBOL:
+ key_is_values = false;
+ break;
+ default:
+ break;
+ }
+ }
+ } while (key_iter != key_comps.begin() && !found);
+ }
+ if (!found && !el_stack.empty() && !key_comps.empty()) {
+ element_list_t::iterator value_iter;
+
+ if (el_stack.size() > 1
+ && in_list.el_format.df_appender != DT_INVALID
+ && in_list.el_format.df_terminator != DT_INVALID)
+ {
+ /* If we're expecting a terminator and haven't found it */
+ /* then this is part of the value. */
+ continue;
+ }
+
+ value.SPLICE(
+ value.end(), key_comps, key_comps.begin(), key_comps.end());
+ value_iter = value.end();
+ std::advance(value_iter, -1);
+ key_comps.SPLICE(
+ key_comps.begin(), value, value_iter, value.end());
+ key_comps.resize(1);
+ }
+
+ strip(value, element_is_space{});
+ value.remove_if(element_if(DT_COMMA));
+ if (!value.empty()) {
+ el_stack.PUSH_BACK(element(value, DNT_VALUE));
+ }
+ strip(key_comps, element_is_space{});
+ if (!key_comps.empty()) {
+ if (key_is_values) {
+ el_stack.PUSH_BACK(element(key_comps, DNT_VALUE));
+ } else {
+ el_stack.PUSH_BACK(element(key_comps, DNT_KEY, false));
+ }
+ }
+ key_comps.CLEAR();
+ value.CLEAR();
+ } else {
+ key_comps.PUSH_BACK(*iter);
+ }
+
+ POINT_TRACE("pairup_loop");
+ }
+
+ POINT_TRACE("pairup_eol");
+
+ CONSUMED_TRACE(in_list);
+
+ // Only perform the free-row logic at the top level, if we're in a group
+ // assume it is a list.
+ if (group_depth < 1 && el_stack.empty()) {
+ free_row.SPLICE(
+ free_row.begin(), key_comps, key_comps.begin(), key_comps.end());
+ } else {
+ this->end_of_value(el_stack, key_comps, value, in_list, group_depth);
+ }
+
+ POINT_TRACE("pairup_stack");
+
+ context.Init(0, 0);
+ while (!el_stack.empty()) {
+ auto kv_iter = el_stack.begin();
+ if (kv_iter->e_token == DNT_VALUE) {
+ if (pairs_out.empty()) {
+ free_row.PUSH_BACK(el_stack.front());
+ } else {
+ element_list_t ELEMENT_LIST_T(free_pair_subs);
+ struct element blank;
+
+ blank.e_capture.c_begin = blank.e_capture.c_end
+ = el_stack.front().e_capture.c_begin;
+ blank.e_token = DNT_KEY;
+ free_pair_subs.PUSH_BACK(blank);
+ free_pair_subs.PUSH_BACK(el_stack.front());
+ pairs_out.PUSH_BACK(element(free_pair_subs, DNT_PAIR));
+ }
+ }
+ if (kv_iter->e_token != DNT_KEY) {
+ el_stack.POP_FRONT();
+ continue;
+ }
+
+ ++kv_iter;
+ if (kv_iter == el_stack.end()) {
+ el_stack.POP_FRONT();
+ continue;
+ }
+
+ element_list_t ELEMENT_LIST_T(pair_subs);
+
+ if (schema != nullptr) {
+ size_t key_len;
+ const char* key_val
+ = this->get_element_string(el_stack.front(), key_len);
+ context.Update(key_val, key_len);
+ }
+
+ while (!free_row.empty()) {
+ element_list_t ELEMENT_LIST_T(free_pair_subs);
+ struct element blank;
+
+ blank.e_capture.c_begin = blank.e_capture.c_end
+ = free_row.front().e_capture.c_begin;
+ blank.e_token = DNT_KEY;
+ free_pair_subs.PUSH_BACK(blank);
+ free_pair_subs.PUSH_BACK(free_row.front());
+ pairs_out.PUSH_BACK(element(free_pair_subs, DNT_PAIR));
+ free_row.POP_FRONT();
+ }
+
+ bool has_value = false;
+
+ if (kv_iter->e_token == DNT_VALUE) {
+ ++kv_iter;
+ has_value = true;
+ }
+
+ pair_subs.SPLICE(
+ pair_subs.begin(), el_stack, el_stack.begin(), kv_iter);
+
+ if (!has_value) {
+ element_list_t ELEMENT_LIST_T(blank_value);
+ struct element blank;
+
+ blank.e_token = DT_QUOTED_STRING;
+ blank.e_capture.c_begin = blank.e_capture.c_end
+ = pair_subs.front().e_capture.c_end;
+ if (blank.e_capture.c_begin >= 0
+ && blank.e_capture.c_begin
+ < this->dp_scanner->get_input().sf_end)
+ {
+ switch (this->dp_scanner->to_string_fragment(blank.e_capture)
+ .front())
+ {
+ case '=':
+ case ':':
+ blank.e_capture.c_begin += 1;
+ blank.e_capture.c_end += 1;
+ break;
+ }
+ }
+ blank_value.PUSH_BACK(blank);
+ pair_subs.PUSH_BACK(element(blank_value, DNT_VALUE));
+ }
+
+ pairs_out.PUSH_BACK(element(pair_subs, DNT_PAIR));
+ }
+
+ if (pairs_out.size() == 1) {
+ element& pair = pairs_out.front();
+ element& evalue = pair.e_sub_elements->back();
+
+ if (evalue.e_token == DNT_VALUE && evalue.e_sub_elements != nullptr
+ && evalue.e_sub_elements->size() > 1)
+ {
+ element_list_t::iterator next_sub;
+
+ next_sub = pair.e_sub_elements->begin();
+ ++next_sub;
+ prefix.SPLICE(prefix.begin(),
+ *pair.e_sub_elements,
+ pair.e_sub_elements->begin(),
+ next_sub);
+ free_row.CLEAR();
+ free_row.SPLICE(free_row.begin(),
+ *evalue.e_sub_elements,
+ evalue.e_sub_elements->begin(),
+ evalue.e_sub_elements->end());
+ pairs_out.CLEAR();
+ context.Init(0, 0);
+ }
+ }
+
+ if (group_depth >= 1 && pairs_out.empty() && !free_row.empty()) {
+ pairs_out.SWAP(free_row);
+ }
+
+ if (pairs_out.empty() && !free_row.empty()) {
+ while (!free_row.empty()) {
+ switch (free_row.front().e_token) {
+ case DNT_GROUP:
+ case DNT_VALUE:
+ case DT_EMAIL:
+ case DT_CONSTANT:
+ case DT_NUMBER:
+ case DT_SYMBOL:
+ case DT_HEX_NUMBER:
+ case DT_OCTAL_NUMBER:
+ case DT_VERSION_NUMBER:
+ case DT_QUOTED_STRING:
+ case DT_IPV4_ADDRESS:
+ case DT_IPV6_ADDRESS:
+ case DT_MAC_ADDRESS:
+ case DT_HEX_DUMP:
+ case DT_XML_DECL_TAG:
+ case DT_XML_OPEN_TAG:
+ case DT_XML_CLOSE_TAG:
+ case DT_XML_EMPTY_TAG:
+ case DT_UUID:
+ case DT_URL:
+ case DT_PATH:
+ case DT_DATE:
+ case DT_TIME:
+ case DT_PERCENTAGE: {
+ element_list_t ELEMENT_LIST_T(pair_subs);
+ struct element blank;
+
+ blank.e_capture.c_begin = blank.e_capture.c_end
+ = free_row.front().e_capture.c_begin;
+ blank.e_token = DNT_KEY;
+ pair_subs.PUSH_BACK(blank);
+ pair_subs.PUSH_BACK(free_row.front());
+ pairs_out.PUSH_BACK(element(pair_subs, DNT_PAIR));
+
+ // Throw something into the hash so that the number of
+ // columns is significant. I don't think we want to
+ // use the token ID since some columns values might vary
+ // between rows.
+ context.Update(" ", 1);
+ } break;
+
+ default: {
+ size_t key_len;
+ const char* key_val
+ = this->get_element_string(free_row.front(), key_len);
+
+ context.Update(key_val, key_len);
+ } break;
+ }
+
+ free_row.POP_FRONT();
+ }
+ }
+
+ if (!prefix.empty()) {
+ element_list_t ELEMENT_LIST_T(pair_subs);
+ struct element blank;
+
+ blank.e_capture.c_begin = blank.e_capture.c_end
+ = prefix.front().e_capture.c_begin;
+ blank.e_token = DNT_KEY;
+ pair_subs.PUSH_BACK(blank);
+ pair_subs.PUSH_BACK(prefix.front());
+ pairs_out.PUSH_FRONT(element(pair_subs, DNT_PAIR));
+ }
+
+ if (schema != nullptr) {
+ context.Final(schema->out(0), schema->out(1));
+ }
+
+ if (schema != nullptr && this->dp_msg_format != nullptr) {
+ for (auto& fiter : pairs_out) {
+ *(this->dp_msg_format) += this->get_string_up_to_value(fiter);
+ this->dp_msg_format->append("#");
+ }
+ if ((size_t) this->dp_msg_format_begin
+ < this->dp_scanner->get_input().length())
+ {
+ auto last = this->dp_scanner->get_input().substr(
+ this->dp_msg_format_begin);
+
+ switch (last.front()) {
+ case '\'':
+ case '"':
+ last.sf_begin += 1;
+ break;
+ }
+ *(this->dp_msg_format) += last.to_string();
+ }
+ }
+
+ if (pairs_out.size() > 1000) {
+ pairs_out.resize(1000);
+ }
+}
+
+void
+data_parser::discover_format()
+{
+ std::stack<discover_format_state> state_stack;
+ this->dp_group_token.push_back(DT_INVALID);
+ this->dp_group_stack.resize(1);
+
+ state_stack.push(discover_format_state());
+ while (true) {
+ auto tok_res = this->dp_scanner->tokenize2();
+ if (!tok_res) {
+ break;
+ }
+
+ element elem;
+ elem.e_token = tok_res->tr_token;
+ elem.e_capture = tok_res->tr_inner_capture;
+
+ require(elem.e_capture.c_begin >= 0);
+ require(elem.e_capture.c_end >= 0);
+
+ state_stack.top().update_for_element(elem);
+ switch (elem.e_token) {
+ case DT_LPAREN:
+ case DT_LANGLE:
+ case DT_LCURLY:
+ case DT_LSQUARE:
+ this->dp_group_token.push_back(elem.e_token);
+ this->dp_group_stack.emplace_back("_anon_", __FILE__, __LINE__);
+ state_stack.push(discover_format_state());
+ break;
+
+ case DT_EMPTY_CONTAINER: {
+ auto& curr_group = this->dp_group_stack.back();
+ auto empty_list = element_list_t("_anon_", __FILE__, __LINE__);
+ discover_format_state dfs;
+
+ dfs.finalize();
+
+ empty_list.el_format = dfs.dfs_format;
+ curr_group.PUSH_BACK(element());
+
+ auto& empty = curr_group.back();
+ empty.e_capture.c_begin = elem.e_capture.c_begin + 1;
+ empty.e_capture.c_end = elem.e_capture.c_begin + 1;
+ empty.e_token = DNT_GROUP;
+ empty.assign_elements(empty_list);
+ break;
+ }
+
+ case DT_RPAREN:
+ case DT_RANGLE:
+ case DT_RCURLY:
+ case DT_RSQUARE:
+ if (this->dp_group_token.back() == (elem.e_token - 1)) {
+ this->dp_group_token.pop_back();
+
+ auto riter = this->dp_group_stack.rbegin();
+ ++riter;
+ state_stack.top().finalize();
+ this->dp_group_stack.back().el_format
+ = state_stack.top().dfs_format;
+ state_stack.pop();
+ if (!this->dp_group_stack.back().empty()) {
+ (*riter).PUSH_BACK(
+ element(this->dp_group_stack.back(), DNT_GROUP));
+ } else {
+ (*riter).PUSH_BACK(element());
+ riter->back().e_capture.c_begin
+ = elem.e_capture.c_begin;
+ riter->back().e_capture.c_end = elem.e_capture.c_begin;
+ riter->back().e_token = DNT_GROUP;
+ riter->back().assign_elements(
+ this->dp_group_stack.back());
+ }
+ this->dp_group_stack.pop_back();
+ } else {
+ this->dp_group_stack.back().PUSH_BACK(elem);
+ }
+ break;
+
+ default:
+ this->dp_group_stack.back().PUSH_BACK(elem);
+ break;
+ }
+ }
+
+ while (this->dp_group_stack.size() > 1) {
+ this->dp_group_token.pop_back();
+
+ auto riter = this->dp_group_stack.rbegin();
+ ++riter;
+ if (!this->dp_group_stack.back().empty()) {
+ state_stack.top().finalize();
+ this->dp_group_stack.back().el_format
+ = state_stack.top().dfs_format;
+ state_stack.pop();
+ (*riter).PUSH_BACK(element(this->dp_group_stack.back(), DNT_GROUP));
+ }
+ this->dp_group_stack.pop_back();
+ }
+
+ state_stack.top().finalize();
+ this->dp_group_stack.back().el_format = state_stack.top().dfs_format;
+}
+
+void
+data_parser::end_of_value(data_parser::element_list_t& el_stack,
+ data_parser::element_list_t& key_comps,
+ data_parser::element_list_t& value,
+ const data_parser::element_list_t& in_list,
+ int group_depth)
+{
+ key_comps.remove_if(element_if(in_list.el_format.df_terminator));
+ key_comps.remove_if(element_if(DT_COMMA));
+ value.remove_if(element_if(in_list.el_format.df_terminator));
+ value.remove_if(element_if(DT_COMMA));
+ strip(key_comps, element_is_space{});
+ strip(value, element_is_space{});
+ if ((el_stack.empty() || el_stack.back().e_token != DNT_KEY)
+ && value.empty() && key_comps.size() > 1
+ && (key_comps.front().e_token == DT_WORD
+ || key_comps.front().e_token == DT_SYMBOL))
+ {
+ element_list_t::iterator key_iter, key_end;
+ bool found_value = false;
+ int word_count = 0;
+ key_iter = key_comps.begin();
+ key_end = key_comps.begin();
+ for (; key_iter != key_comps.end(); ++key_iter) {
+ if (key_iter->e_token == DT_WORD || key_iter->e_token == DT_SYMBOL)
+ {
+ word_count += 1;
+ if (found_value) {
+ key_end = key_comps.begin();
+ }
+ } else if (key_iter->e_token == DT_WHITE
+ || key_iter->e_token == DT_CSI)
+ {
+ } else {
+ if (!found_value) {
+ key_end = key_iter;
+ }
+ found_value = true;
+ }
+ }
+ if (word_count != 1) {
+ key_end = key_comps.begin();
+ }
+ value.SPLICE(value.end(), key_comps, key_end, key_comps.end());
+ strip(key_comps, element_is_space{});
+ if (!key_comps.empty()) {
+ el_stack.PUSH_BACK(element(key_comps, DNT_KEY, false));
+ }
+ key_comps.CLEAR();
+ } else {
+ value.SPLICE(
+ value.end(), key_comps, key_comps.begin(), key_comps.end());
+ }
+ strip(value, element_is_space{});
+ strip(value, element_if(DT_COLON));
+ strip(value, element_is_space{});
+ if (!value.empty()) {
+ if (value.size() == 2 && value.back().e_token == DNT_GROUP) {
+ element_list_t ELEMENT_LIST_T(group_pair);
+
+ group_pair.PUSH_BACK(element(value, DNT_PAIR));
+ el_stack.PUSH_BACK(element(group_pair, DNT_VALUE));
+ } else {
+ el_stack.PUSH_BACK(element(value, DNT_VALUE));
+ }
+ }
+ value.CLEAR();
+}
+
+void
+data_parser::parse()
+{
+ this->discover_format();
+
+ this->pairup(
+ &this->dp_schema_id, this->dp_pairs, this->dp_group_stack.front());
+}
+
+std::string
+data_parser::get_element_string(const data_parser::element& elem) const
+{
+ return this->dp_scanner->to_string_fragment(elem.e_capture).to_string();
+}
+
+std::string
+data_parser::get_string_up_to_value(const data_parser::element& elem)
+{
+ const element& val_elem
+ = elem.e_token == DNT_PAIR ? elem.e_sub_elements->back() : elem;
+
+ if (this->dp_msg_format_begin <= val_elem.e_capture.c_begin) {
+ auto leading_and_key = data_scanner::capture_t(
+ this->dp_msg_format_begin, val_elem.e_capture.c_begin);
+ auto str = this->dp_scanner->get_input().data();
+ if (leading_and_key.length() >= 2) {
+ switch (str[leading_and_key.c_end - 1]) {
+ case '\'':
+ case '"':
+ leading_and_key.c_end -= 1;
+ switch (str[leading_and_key.c_end - 1]) {
+ case 'r':
+ case 'u':
+ leading_and_key.c_end -= 1;
+ break;
+ }
+ break;
+ }
+ switch (str[leading_and_key.c_begin]) {
+ case '\'':
+ case '"':
+ leading_and_key.c_begin += 1;
+ break;
+ }
+ }
+ this->dp_msg_format_begin = val_elem.e_capture.c_end;
+ return this->dp_scanner->to_string_fragment(leading_and_key)
+ .to_string();
+ } else {
+ this->dp_msg_format_begin = val_elem.e_capture.c_end;
+ }
+ return "";
+}
+
+const char*
+data_parser::get_element_string(const data_parser::element& elem,
+ size_t& len_out)
+{
+ len_out = elem.e_capture.length();
+ return this->dp_scanner->to_string_fragment(elem.e_capture).data();
+}
+
+void
+data_parser::print(FILE* out, data_parser::element_list_t& el)
+{
+ fprintf(out,
+ " %s\n",
+ this->dp_scanner->get_input().to_string().c_str());
+ for (auto& iter : el) {
+ iter.print(out, *this->dp_scanner);
+ }
+}
+
+FILE* data_parser::TRACE_FILE;
+
+data_format_state_t
+dfs_prefix_next(data_format_state_t state, data_token_t next_token)
+{
+ data_format_state_t retval = state;
+
+ switch (state) {
+ case DFS_INIT:
+ switch (next_token) {
+ case DT_PATH:
+ case DT_COLON:
+ case DT_EQUALS:
+ case DT_CONSTANT:
+ case DT_EMAIL:
+ case DT_WORD:
+ case DT_SYMBOL:
+ case DT_OCTAL_NUMBER:
+ case DT_HEX_NUMBER:
+ case DT_NUMBER:
+ case DT_WHITE:
+ case DT_CSI:
+ case DT_LSQUARE:
+ case DT_RSQUARE:
+ case DT_LANGLE:
+ case DT_RANGLE:
+ case DT_EMPTY_CONTAINER:
+ break;
+
+ default:
+ retval = DFS_ERROR;
+ break;
+ }
+ break;
+
+ case DFS_EXPECTING_SEP:
+ case DFS_ERROR:
+ retval = DFS_ERROR;
+ break;
+
+ default:
+ break;
+ }
+
+ return retval;
+}
+
+data_format_state_t
+dfs_semi_next(data_format_state_t state, data_token_t next_token)
+{
+ data_format_state_t retval = state;
+
+ switch (state) {
+ case DFS_INIT:
+ switch (next_token) {
+ case DT_COMMA:
+ case DT_SEMI:
+ retval = DFS_ERROR;
+ break;
+
+ default:
+ retval = DFS_KEY;
+ break;
+ }
+ break;
+
+ case DFS_KEY:
+ switch (next_token) {
+ case DT_COLON:
+ case DT_EQUALS:
+ retval = DFS_VALUE;
+ break;
+
+ case DT_SEMI:
+ retval = DFS_ERROR;
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case DFS_VALUE:
+ switch (next_token) {
+ case DT_SEMI:
+ retval = DFS_INIT;
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case DFS_EXPECTING_SEP:
+ case DFS_ERROR:
+ retval = DFS_ERROR;
+ break;
+ }
+
+ return retval;
+}
+
+data_format_state_t
+dfs_comma_next(data_format_state_t state, data_token_t next_token)
+{
+ data_format_state_t retval = state;
+
+ switch (state) {
+ case DFS_INIT:
+ switch (next_token) {
+ case DT_COMMA:
+ break;
+
+ case DT_SEMI:
+ retval = DFS_ERROR;
+ break;
+
+ default:
+ retval = DFS_KEY;
+ break;
+ }
+ break;
+
+ case DFS_KEY:
+ switch (next_token) {
+ case DT_COLON:
+ case DT_EQUALS:
+ retval = DFS_VALUE;
+ break;
+
+ case DT_COMMA:
+ retval = DFS_INIT;
+ break;
+
+ case DT_WORD:
+ retval = DFS_EXPECTING_SEP;
+ break;
+
+ case DT_SEMI:
+ retval = DFS_ERROR;
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case DFS_EXPECTING_SEP:
+ switch (next_token) {
+ case DT_COLON:
+ case DT_EQUALS:
+ case DT_LPAREN:
+ case DT_LCURLY:
+ case DT_LSQUARE:
+ case DT_LANGLE:
+ retval = DFS_VALUE;
+ break;
+
+ case DT_EMPTY_CONTAINER:
+ retval = DFS_INIT;
+ break;
+
+ case DT_COMMA:
+ case DT_SEMI:
+ retval = DFS_ERROR;
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case DFS_VALUE:
+ switch (next_token) {
+ case DT_COMMA:
+ retval = DFS_INIT;
+ break;
+
+ case DT_COLON:
+ case DT_EQUALS:
+ retval = DFS_ERROR;
+ break;
+
+ default:
+ break;
+ }
+ break;
+
+ case DFS_ERROR:
+ retval = DFS_ERROR;
+ break;
+ }
+
+ return retval;
+}
+
+data_parser::element::element()
+ : e_capture(-1, -1), e_token(DT_INVALID), e_sub_elements(nullptr)
+{
+}
+
+data_parser::element::element(data_parser::element_list_t& subs,
+ data_token_t token,
+ bool assign_subs_elements)
+ : e_capture(subs.front().e_capture.c_begin, subs.back().e_capture.c_end),
+ e_token(token), e_sub_elements(nullptr)
+{
+ if (assign_subs_elements) {
+ this->assign_elements(subs);
+ }
+}
+
+data_parser::element::element(const data_parser::element& other)
+{
+ /* require(other.e_sub_elements == nullptr); */
+
+ this->e_capture = other.e_capture;
+ this->e_token = other.e_token;
+ this->e_sub_elements = nullptr;
+ if (other.e_sub_elements != nullptr) {
+ this->assign_elements(*other.e_sub_elements);
+ }
+}
+
+data_parser::element::~element()
+{
+ delete this->e_sub_elements;
+ this->e_sub_elements = nullptr;
+}
+
+data_parser::element&
+data_parser::element::operator=(const data_parser::element& other)
+{
+ this->e_capture = other.e_capture;
+ this->e_token = other.e_token;
+ this->e_sub_elements = nullptr;
+ if (other.e_sub_elements != nullptr) {
+ this->assign_elements(*other.e_sub_elements);
+ }
+ return *this;
+}
+
+void
+data_parser::element::assign_elements(data_parser::element_list_t& subs)
+{
+ if (this->e_sub_elements == nullptr) {
+ this->e_sub_elements = new element_list_t("_sub_", __FILE__, __LINE__);
+ this->e_sub_elements->el_format = subs.el_format;
+ }
+ this->e_sub_elements->SWAP(subs);
+ this->update_capture();
+}
+
+void
+data_parser::element::update_capture()
+{
+ if (this->e_sub_elements != nullptr && !this->e_sub_elements->empty()) {
+ this->e_capture.c_begin
+ = this->e_sub_elements->front().e_capture.c_begin;
+ this->e_capture.c_end = this->e_sub_elements->back().e_capture.c_end;
+ }
+}
+
+const data_parser::element&
+data_parser::element::get_pair_value() const
+{
+ require(this->e_token == DNT_PAIR);
+
+ return this->e_sub_elements->back();
+}
+
+data_token_t
+data_parser::element::value_token() const
+{
+ data_token_t retval = DT_INVALID;
+
+ if (this->e_token == DNT_VALUE) {
+ if (this->e_sub_elements != nullptr
+ && this->e_sub_elements->size() == 1)
+ {
+ retval = this->e_sub_elements->front().e_token;
+ } else {
+ retval = DT_SYMBOL;
+ }
+ } else {
+ retval = this->e_token;
+ }
+ return retval;
+}
+
+const data_parser::element&
+data_parser::element::get_value_elem() const
+{
+ if (this->e_token == DNT_VALUE) {
+ if (this->e_sub_elements != nullptr
+ && this->e_sub_elements->size() == 1)
+ {
+ return this->e_sub_elements->front();
+ }
+ }
+ return *this;
+}
+
+const data_parser::element&
+data_parser::element::get_pair_elem() const
+{
+ if (this->e_token == DNT_VALUE) {
+ return this->e_sub_elements->front();
+ }
+ return *this;
+}
+
+void
+data_parser::element::print(FILE* out, data_scanner& ds, int offset) const
+{
+ int lpc;
+
+ if (this->e_sub_elements != nullptr) {
+ for (auto& e_sub_element : *this->e_sub_elements) {
+ e_sub_element.print(out, ds, offset + 1);
+ }
+ }
+
+ fprintf(out,
+ "%4s %3d:%-3d ",
+ data_scanner::token2name(this->e_token),
+ this->e_capture.c_begin,
+ this->e_capture.c_end);
+ for (lpc = 0; lpc < this->e_capture.c_end; lpc++) {
+ if (lpc == this->e_capture.c_begin) {
+ fputc('^', out);
+ } else if (lpc == (this->e_capture.c_end - 1)) {
+ fputc('^', out);
+ } else if (lpc > this->e_capture.c_begin) {
+ fputc('-', out);
+ } else {
+ fputc(' ', out);
+ }
+ }
+ for (; lpc < (int) ds.get_input().length(); lpc++) {
+ fputc(' ', out);
+ }
+
+ std::string sub = ds.to_string_fragment(this->e_capture).to_string();
+ fprintf(out, " %s\n", sub.c_str());
+}
+
+data_parser::discover_format_state::discover_format_state()
+ : dfs_prefix_state(DFS_INIT), dfs_semi_state(DFS_INIT),
+ dfs_comma_state(DFS_INIT)
+{
+ memset(this->dfs_hist, 0, sizeof(this->dfs_hist));
+}
+
+void
+data_parser::discover_format_state::update_for_element(
+ const data_parser::element& elem)
+{
+ this->dfs_prefix_state
+ = dfs_prefix_next(this->dfs_prefix_state, elem.e_token);
+ this->dfs_semi_state = dfs_semi_next(this->dfs_semi_state, elem.e_token);
+ this->dfs_comma_state = dfs_comma_next(this->dfs_comma_state, elem.e_token);
+ if (this->dfs_prefix_state != DFS_ERROR) {
+ if (this->dfs_semi_state == DFS_ERROR) {
+ this->dfs_semi_state = DFS_INIT;
+ }
+ if (this->dfs_comma_state == DFS_ERROR) {
+ this->dfs_comma_state = DFS_INIT;
+ }
+ }
+ this->dfs_hist[elem.e_token] += 1;
+}
+
+void
+data_parser::discover_format_state::finalize()
+{
+ data_token_t qualifier = this->dfs_format.df_qualifier;
+ data_token_t separator = this->dfs_format.df_separator;
+ data_token_t prefix_term = this->dfs_format.df_prefix_terminator;
+
+ this->dfs_format = FORMAT_PLAIN;
+ if (this->dfs_hist[DT_EQUALS]) {
+ qualifier = DT_COLON;
+ separator = DT_EQUALS;
+ }
+
+ if (this->dfs_semi_state != DFS_ERROR && this->dfs_hist[DT_SEMI]) {
+ this->dfs_format = FORMAT_SEMI;
+ } else if (this->dfs_comma_state != DFS_ERROR) {
+ this->dfs_format = FORMAT_COMMA;
+ if (separator == DT_COLON && this->dfs_hist[DT_COMMA] > 0) {
+ if (!((this->dfs_hist[DT_COLON] == this->dfs_hist[DT_COMMA])
+ || ((this->dfs_hist[DT_COLON] - 1)
+ == this->dfs_hist[DT_COMMA])))
+ {
+ separator = DT_INVALID;
+ if (this->dfs_hist[DT_COLON] == 1) {
+ prefix_term = DT_COLON;
+ }
+ }
+ }
+ }
+
+ this->dfs_format.df_qualifier = qualifier;
+ this->dfs_format.df_separator = separator;
+ this->dfs_format.df_prefix_terminator = prefix_term;
+}
diff --git a/src/data_parser.hh b/src/data_parser.hh
new file mode 100644
index 0000000..ca54a58
--- /dev/null
+++ b/src/data_parser.hh
@@ -0,0 +1,431 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef data_parser_hh
+#define data_parser_hh
+
+#include <iterator>
+#include <list>
+#include <stack>
+#include <vector>
+
+#include <stdio.h>
+
+#include "base/lnav_log.hh"
+#include "byte_array.hh"
+#include "data_scanner.hh"
+
+#define ELEMENT_LIST_T(var) var("" #var, __FILE__, __LINE__, group_depth)
+#define PUSH_FRONT(elem) push_front(elem, __FILE__, __LINE__)
+#define PUSH_BACK(elem) push_back(elem, __FILE__, __LINE__)
+#define POP_FRONT(elem) pop_front(__FILE__, __LINE__)
+#define POP_BACK(elem) pop_back(__FILE__, __LINE__)
+#define CLEAR(elem) clear2(__FILE__, __LINE__)
+#define SWAP(other) swap(other, __FILE__, __LINE__)
+#define SPLICE(pos, other, first, last) \
+ splice(pos, other, first, last, __FILE__, __LINE__)
+
+template<class Container, class UnaryPredicate>
+void
+strip(Container& container, UnaryPredicate p)
+{
+ while (!container.empty() && p(container.front())) {
+ container.POP_FRONT();
+ }
+ while (!container.empty() && p(container.back())) {
+ container.POP_BACK();
+ }
+}
+
+enum data_format_state_t {
+ DFS_ERROR = -1,
+ DFS_INIT,
+ DFS_KEY,
+ DFS_EXPECTING_SEP,
+ DFS_VALUE,
+};
+
+struct data_format {
+ data_format(const char* name = nullptr,
+ data_token_t appender = DT_INVALID,
+ data_token_t terminator = DT_INVALID) noexcept
+ : df_name(name), df_appender(appender), df_terminator(terminator),
+ df_qualifier(DT_INVALID), df_separator(DT_COLON),
+ df_prefix_terminator(DT_INVALID)
+ {
+ }
+
+ const char* df_name;
+ data_token_t df_appender;
+ data_token_t df_terminator;
+ data_token_t df_qualifier;
+ data_token_t df_separator;
+ data_token_t df_prefix_terminator;
+};
+
+data_format_state_t dfs_prefix_next(data_format_state_t state,
+ data_token_t next_token);
+data_format_state_t dfs_semi_next(data_format_state_t state,
+ data_token_t next_token);
+data_format_state_t dfs_comma_next(data_format_state_t state,
+ data_token_t next_token);
+
+#define LIST_INIT_TRACE \
+ do { \
+ if (TRACE_FILE != NULL) { \
+ fprintf(TRACE_FILE, \
+ "%p %s:%d %s %s %d\n", \
+ this, \
+ fn, \
+ line, \
+ __func__, \
+ varname, \
+ group_depth); \
+ } \
+ } while (false)
+
+#define LIST_DEINIT_TRACE \
+ do { \
+ if (TRACE_FILE != NULL) { \
+ fprintf(TRACE_FILE, "%p %s:%d %s\n", this, fn, line, __func__); \
+ } \
+ } while (false)
+
+#define ELEMENT_TRACE \
+ do { \
+ if (TRACE_FILE != NULL) { \
+ fprintf(TRACE_FILE, \
+ "%p %s:%d %s %s %d:%d\n", \
+ this, \
+ fn, \
+ line, \
+ __func__, \
+ data_scanner::token2name(elem.e_token), \
+ elem.e_capture.c_begin, \
+ elem.e_capture.c_end); \
+ } \
+ } while (false)
+
+#define LIST_TRACE \
+ do { \
+ if (TRACE_FILE != NULL) { \
+ fprintf(TRACE_FILE, "%p %s:%d %s\n", this, fn, line, __func__); \
+ } \
+ } while (false)
+
+#define SPLICE_TRACE \
+ do { \
+ if (TRACE_FILE != NULL) { \
+ fprintf(TRACE_FILE, \
+ "%p %s:%d %s %d %p %d:%d\n", \
+ this, \
+ fn, \
+ line, \
+ __func__, \
+ (int) std::distance(this->begin(), pos), \
+ &other, \
+ (int) std::distance(other.begin(), first), \
+ (int) std::distance(last, other.end())); \
+ } \
+ } while (false);
+
+#define SWAP_TRACE(other) \
+ do { \
+ if (TRACE_FILE != NULL) { \
+ fprintf(TRACE_FILE, \
+ "%p %s:%d %s %p\n", \
+ this, \
+ fn, \
+ line, \
+ __func__, \
+ &other); \
+ } \
+ } while (false);
+
+#define POINT_TRACE(name) \
+ do { \
+ if (TRACE_FILE) { \
+ fprintf( \
+ TRACE_FILE, "0x0 %s:%d point %s\n", __FILE__, __LINE__, name); \
+ } \
+ } while (false);
+
+#define FORMAT_TRACE(elist) \
+ do { \
+ if (TRACE_FILE) { \
+ const data_format& df = elist.el_format; \
+ fprintf(TRACE_FILE, \
+ "%p %s:%d format %d %s %s %s %s %s\n", \
+ &elist, \
+ __FILE__, \
+ __LINE__, \
+ group_depth, \
+ data_scanner::token2name(df.df_appender), \
+ data_scanner::token2name(df.df_terminator), \
+ data_scanner::token2name(df.df_qualifier), \
+ data_scanner::token2name(df.df_separator), \
+ data_scanner::token2name(df.df_prefix_terminator)); \
+ } \
+ } while (false);
+
+#define CONSUMED_TRACE(elist) \
+ do { \
+ if (TRACE_FILE) { \
+ fprintf(TRACE_FILE, \
+ "%p %s:%d consumed\n", \
+ &elist, \
+ __FILE__, \
+ __LINE__); \
+ } \
+ } while (false);
+
+class data_parser {
+public:
+ static data_format FORMAT_SEMI;
+ static data_format FORMAT_COMMA;
+ static data_format FORMAT_PLAIN;
+
+ static FILE* TRACE_FILE;
+
+ typedef byte_array<2, uint64_t> schema_id_t;
+
+ struct element;
+ /* typedef std::list<element> element_list_t; */
+
+ class element_list_t : public std::list<element> {
+ public:
+ element_list_t(const char* varname,
+ const char* fn,
+ int line,
+ int group_depth = -1)
+ {
+ LIST_INIT_TRACE;
+ }
+
+ element_list_t()
+ {
+ const char* varname = "_anon2_";
+ const char* fn = __FILE__;
+ int line = __LINE__;
+ int group_depth = -1;
+
+ LIST_INIT_TRACE;
+ }
+
+ element_list_t(const element_list_t& other) : std::list<element>(other)
+ {
+ this->el_format = other.el_format;
+ }
+
+ ~element_list_t()
+ {
+ const char* fn = __FILE__;
+ int line = __LINE__;
+
+ LIST_DEINIT_TRACE;
+ }
+
+ void push_front(const element& elem, const char* fn, int line)
+ {
+ ELEMENT_TRACE;
+
+ require(elem.e_capture.c_end >= -1);
+ this->std::list<element>::push_front(elem);
+ }
+
+ void push_back(const element& elem, const char* fn, int line)
+ {
+ ELEMENT_TRACE;
+
+ require(elem.e_capture.c_end >= -1);
+ this->std::list<element>::push_back(elem);
+ }
+
+ void pop_front(const char* fn, int line)
+ {
+ LIST_TRACE;
+
+ this->std::list<element>::pop_front();
+ }
+
+ void pop_back(const char* fn, int line)
+ {
+ LIST_TRACE;
+
+ this->std::list<element>::pop_back();
+ }
+
+ void clear2(const char* fn, int line)
+ {
+ LIST_TRACE;
+
+ this->std::list<element>::clear();
+ }
+
+ void swap(element_list_t& other, const char* fn, int line)
+ {
+ SWAP_TRACE(other);
+
+ this->std::list<element>::swap(other);
+ }
+
+ void splice(iterator pos,
+ element_list_t& other,
+ iterator first,
+ iterator last,
+ const char* fn,
+ int line)
+ {
+ SPLICE_TRACE;
+
+ this->std::list<element>::splice(pos, other, first, last);
+ }
+
+ data_format el_format;
+ };
+
+ struct element {
+ element();
+
+ element(element_list_t& subs,
+ data_token_t token,
+ bool assign_subs_elements = true);
+
+ element(const element& other);
+
+ ~element();
+
+ element& operator=(const element& other);
+
+ void assign_elements(element_list_t& subs);
+
+ void update_capture();
+
+ const element& get_pair_value() const;
+
+ data_token_t value_token() const;
+
+ const element& get_value_elem() const;
+
+ const element& get_pair_elem() const;
+
+ void print(FILE* out, data_scanner&, int offset = 0) const;
+
+ data_scanner::capture_t e_capture;
+ data_token_t e_token;
+
+ element_list_t* e_sub_elements;
+ };
+
+ struct element_cmp {
+ bool operator()(data_token_t token, const element& elem) const
+ {
+ return token == elem.e_token || token == DT_ANY;
+ }
+
+ bool operator()(const element& elem, data_token_t token) const
+ {
+ return (*this)(token, elem);
+ }
+ };
+
+ struct element_if {
+ element_if(data_token_t token) : ei_token(token) {}
+
+ bool operator()(const element& a) const
+ {
+ return a.e_token == this->ei_token;
+ }
+
+ private:
+ data_token_t ei_token;
+ };
+
+ struct element_is_space {
+ bool operator()(const element& el) const
+ {
+ return el.e_token == DT_WHITE || el.e_token == DT_CSI;
+ }
+ };
+
+ struct discover_format_state {
+ discover_format_state();
+
+ void update_for_element(const element& elem);
+
+ void finalize();
+
+ data_format_state_t dfs_prefix_state;
+ data_format_state_t dfs_semi_state;
+ data_format_state_t dfs_comma_state;
+ int dfs_hist[DT_TERMINAL_MAX];
+
+ data_format dfs_format;
+ };
+
+ data_parser(data_scanner* ds);
+
+ void pairup(schema_id_t* schema,
+ element_list_t& pairs_out,
+ element_list_t& in_list,
+ int group_depth = 0);
+
+ void discover_format();
+
+ void end_of_value(element_list_t& el_stack,
+ element_list_t& key_comps,
+ element_list_t& value,
+ const element_list_t& in_list,
+ int group_depth);
+
+ void parse();
+
+ std::string get_element_string(const element& elem) const;
+
+ std::string get_string_up_to_value(const element& elem);
+
+ const char* get_element_string(const element& elem, size_t& len_out);
+
+ void print(FILE* out, element_list_t& el);
+
+ std::vector<data_token_t> dp_group_token;
+ std::list<element_list_t> dp_group_stack;
+
+ element_list_t dp_errors;
+
+ element_list_t dp_pairs;
+ schema_id_t dp_schema_id;
+ std::string* dp_msg_format;
+ int dp_msg_format_begin;
+
+private:
+ data_scanner* dp_scanner;
+};
+
+#endif
diff --git a/src/data_scanner.cc b/src/data_scanner.cc
new file mode 100644
index 0000000..f270a13
--- /dev/null
+++ b/src/data_scanner.cc
@@ -0,0 +1,265 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+
+#include "data_scanner.hh"
+
+#include "config.h"
+
+void
+data_scanner::capture_t::ltrim(const char* str)
+{
+ while (this->c_begin < this->c_end && isspace(str[this->c_begin])) {
+ this->c_begin += 1;
+ }
+}
+
+static struct {
+ const char* name;
+} MATCHERS[DT_TERMINAL_MAX] = {
+ {
+ "quot",
+ },
+ {
+ "url",
+ },
+ {
+ "path",
+ },
+ {
+ "mac",
+ },
+ {
+ "date",
+ },
+ {
+ "time",
+ },
+ {
+ "dt",
+ },
+ /* { "qual", pcrepp("\\A([^\\s:=]+:[^\\s:=,]+(?!,)(?::[^\\s:=,]+)*)"), }, */
+ {
+ "ipv6",
+ },
+ {
+ "hexd",
+ },
+
+ {
+ "xmld",
+ },
+ {
+ "xmlt",
+ },
+ {
+ "xmlo",
+ },
+
+ {
+ "xmlc",
+ },
+
+ {
+ "h1",
+ },
+ {
+ "h2",
+ },
+ {
+ "h3",
+ },
+
+ {
+ "coln",
+ },
+ {
+ "eq",
+ },
+ {
+ "comm",
+ },
+ {
+ "semi",
+ },
+
+ {
+ "empt",
+ },
+
+ {
+ "lcur",
+ },
+ {
+ "rcur",
+ },
+
+ {
+ "lsqu",
+ },
+ {
+ "rsqu",
+ },
+
+ {
+ "lpar",
+ },
+ {
+ "rpar",
+ },
+
+ {
+ "lang",
+ },
+ {
+ "rang",
+ },
+
+ {
+ "ipv4",
+ },
+
+ {
+ "uuid",
+ },
+
+ {
+ "cc",
+ },
+ {
+ "vers",
+ },
+ {
+ "oct",
+ },
+ {
+ "pcnt",
+ },
+ {
+ "num",
+ },
+ {
+ "hex",
+ },
+
+ {
+ "mail",
+ },
+ {
+ "cnst",
+ },
+ {
+ "word",
+ },
+ {
+ "sym",
+ },
+ {
+ "line",
+ },
+ {
+ "wspc",
+ },
+ {
+ "dot",
+ },
+ {
+ "escc",
+ },
+ {
+ "csi",
+ },
+
+ {
+ "gbg",
+ },
+};
+
+const char* DNT_NAMES[DNT_MAX - DNT_KEY] = {
+ "key",
+ "pair",
+ "val",
+ "row",
+ "unit",
+ "meas",
+ "var",
+ "rang",
+ "grp",
+};
+
+const char*
+data_scanner::token2name(data_token_t token)
+{
+ if (token < 0) {
+ return "inv";
+ }
+ if (token < DT_TERMINAL_MAX) {
+ return MATCHERS[token].name;
+ }
+ if (token == DT_ANY) {
+ return "any";
+ }
+ return DNT_NAMES[token - DNT_KEY];
+}
+
+bool
+data_scanner::is_credit_card(string_fragment cc) const
+{
+ auto cc_no_spaces = cc.to_string();
+ auto new_end = std::remove_if(cc_no_spaces.begin(),
+ cc_no_spaces.end(),
+ [](auto ch) { return ch == ' '; });
+ cc_no_spaces.erase(new_end, cc_no_spaces.end());
+ int len = cc_no_spaces.size();
+ int double_even_sum = 0;
+
+ // Step 1: double every second digit, starting from right.
+ // if results in 2 digit number, add the digits to obtain single digit
+ // number. sum all answers to obtain 'double_even_sum'
+
+ for (int lpc = len - 2; lpc >= 0; lpc = lpc - 2) {
+ int dbl = ((cc_no_spaces[lpc] - '0') * 2);
+ if (dbl > 9) {
+ dbl = (dbl / 10) + (dbl % 10);
+ }
+ double_even_sum += dbl;
+ }
+
+ // Step 2: add every odd placed digit from right to double_even_sum's value
+
+ for (int lpc = len - 1; lpc >= 0; lpc = lpc - 2) {
+ double_even_sum += (cc_no_spaces[lpc] - 48);
+ }
+
+ // Step 3: check if final 'double_even_sum' is multiple of 10
+ // if yes, it is valid.
+
+ return double_even_sum % 10 == 0;
+}
diff --git a/src/data_scanner.hh b/src/data_scanner.hh
new file mode 100644
index 0000000..3859ebb
--- /dev/null
+++ b/src/data_scanner.hh
@@ -0,0 +1,211 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef data_scanner_hh
+#define data_scanner_hh
+
+#include <string>
+
+#include "pcrepp/pcre2pp.hh"
+#include "shared_buffer.hh"
+
+enum data_token_t {
+ DT_INVALID = -1,
+
+ DT_QUOTED_STRING = 0,
+ DT_URL,
+ DT_PATH,
+ DT_MAC_ADDRESS,
+ DT_DATE,
+ DT_TIME,
+ DT_DATE_TIME,
+ DT_IPV6_ADDRESS,
+ DT_HEX_DUMP,
+ DT_XML_DECL_TAG,
+ DT_XML_EMPTY_TAG,
+ DT_XML_OPEN_TAG,
+ DT_XML_CLOSE_TAG,
+
+ DT_H1,
+ DT_H2,
+ DT_H3,
+
+ /* DT_QUALIFIED_NAME, */
+
+ DT_COLON,
+ DT_EQUALS,
+ DT_COMMA,
+ DT_SEMI,
+
+ DT_EMPTY_CONTAINER,
+
+ DT_LCURLY,
+ DT_RCURLY,
+
+ DT_LSQUARE,
+ DT_RSQUARE,
+
+ DT_LPAREN,
+ DT_RPAREN,
+
+ DT_LANGLE,
+ DT_RANGLE,
+
+ DT_IPV4_ADDRESS,
+ DT_UUID,
+
+ DT_CREDIT_CARD_NUMBER,
+ DT_VERSION_NUMBER,
+ DT_OCTAL_NUMBER,
+ DT_PERCENTAGE,
+ DT_NUMBER,
+ DT_HEX_NUMBER,
+
+ DT_EMAIL,
+ DT_CONSTANT,
+ DT_WORD,
+ DT_SYMBOL,
+ DT_LINE,
+ DT_WHITE,
+ DT_DOT,
+ DT_ESCAPED_CHAR,
+ DT_CSI,
+
+ DT_GARBAGE,
+
+ DT_TERMINAL_MAX = DT_GARBAGE + 1,
+
+ DNT_KEY = 50,
+ DNT_PAIR,
+ DNT_VALUE,
+ DNT_ROW,
+ DNT_UNITS,
+ DNT_MEASUREMENT,
+ DNT_VARIABLE_KEY,
+ DNT_ROWRANGE,
+ DNT_GROUP,
+
+ DNT_MAX,
+
+ DT_ANY = 100,
+};
+
+class data_scanner {
+public:
+ static const char* token2name(data_token_t token);
+
+ struct capture_t {
+ capture_t()
+ { /* We don't initialize anything since it's a perf hit. */
+ }
+
+ capture_t(int begin, int end) : c_begin(begin), c_end(end)
+ {
+ assert(begin <= end);
+ }
+
+ int c_begin;
+ int c_end;
+
+ void ltrim(const char* str);
+
+ bool contains(int pos) const
+ {
+ return this->c_begin <= pos && pos < this->c_end;
+ }
+
+ bool is_valid() const { return this->c_begin != -1; }
+
+ int length() const { return this->c_end - this->c_begin; }
+
+ bool empty() const { return this->c_begin == this->c_end; }
+ };
+
+ data_scanner(const std::string& line, size_t off = 0)
+ : ds_line(line), ds_input(this->ds_line), ds_init_offset(off),
+ ds_next_offset(off)
+ {
+ if (!line.empty() && line.back() == '.') {
+ this->ds_input.sf_end -= 1;
+ }
+ }
+
+ explicit data_scanner(string_fragment sf) : ds_input(sf)
+ {
+ if (!sf.empty() && sf.back() == '.') {
+ this->ds_input.sf_end -= 1;
+ }
+ }
+
+ explicit data_scanner(shared_buffer_ref& line, size_t off, size_t end)
+ : ds_sbr(line), ds_input(line.to_string_fragment().sub_range(0, end)),
+ ds_init_offset(off), ds_next_offset(off)
+ {
+ if (!this->ds_input.empty() && this->ds_input.back() == '.') {
+ this->ds_input.sf_end -= 1;
+ }
+ }
+
+ struct tokenize_result {
+ data_token_t tr_token{DT_INVALID};
+ capture_t tr_capture;
+ capture_t tr_inner_capture;
+ const char* tr_data{nullptr};
+
+ std::string to_string() const
+ {
+ return {&this->tr_data[this->tr_capture.c_begin],
+ (size_t) this->tr_capture.length()};
+ }
+ };
+
+ nonstd::optional<tokenize_result> tokenize2();
+
+ void reset() { this->ds_next_offset = this->ds_init_offset; }
+
+ int get_init_offset() const { return this->ds_init_offset; }
+
+ string_fragment get_input() const { return this->ds_input; }
+
+ string_fragment to_string_fragment(capture_t cap) const
+ {
+ return this->ds_input.sub_range(cap.c_begin, cap.c_end);
+ }
+
+private:
+ bool is_credit_card(string_fragment frag) const;
+
+ std::string ds_line;
+ shared_buffer_ref ds_sbr;
+ string_fragment ds_input;
+ int ds_init_offset{0};
+ int ds_next_offset{0};
+};
+
+#endif
diff --git a/src/data_scanner_re.cc b/src/data_scanner_re.cc
new file mode 100644
index 0000000..21bb7ce
--- /dev/null
+++ b/src/data_scanner_re.cc
@@ -0,0 +1,36096 @@
+/* Generated by re2c 3.0 on Mon Sep 19 01:36:56 2022 */
+#line 1 "../../lnav/src/data_scanner_re.re"
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#include "base/date_time_scanner.hh"
+#include "config.h"
+#include "data_scanner.hh"
+
+nonstd::optional<data_scanner::tokenize_result> data_scanner::tokenize2()
+{
+ data_token_t token_out = DT_INVALID;
+ capture_t cap_all;
+ capture_t cap_inner;
+# define YYCTYPE unsigned char
+# define CAPTURE(tok) { \
+ if (YYCURSOR.val == EMPTY) { \
+ this->ds_next_offset = this->ds_input.length(); \
+ } else { \
+ this->ds_next_offset = YYCURSOR.val - this->ds_input.udata(); \
+ } \
+ cap_all.c_end = this->ds_next_offset; \
+ cap_inner.c_end = this->ds_next_offset; \
+ token_out = tok; \
+ }
+
+# define RET(tok) { \
+ CAPTURE(tok); \
+ return tokenize_result{token_out, cap_all, cap_inner, this->ds_input.data()}; \
+ }
+ static const unsigned char *EMPTY = (const unsigned char *) "";
+
+ struct _YYCURSOR {
+ YYCTYPE operator*() const {
+ if (this->val < this->lim) {
+ return *val;
+ }
+ return '\0';
+ }
+
+ operator const YYCTYPE *() const {
+ if (this->val < this->lim) {
+ return this->val;
+ }
+ return EMPTY;
+ }
+
+ const YYCTYPE *operator=(const YYCTYPE *rhs) {
+ this->val = rhs;
+ return rhs;
+ }
+
+ const YYCTYPE *operator+(int rhs) {
+ return this->val + rhs;
+ }
+
+ const _YYCURSOR *operator-=(int rhs) {
+ this->val -= rhs;
+ return this;
+ }
+
+ _YYCURSOR& operator++() {
+ this->val += 1;
+ return *this;
+ }
+
+ const YYCTYPE *val{nullptr};
+ const YYCTYPE *lim{nullptr};
+ } YYCURSOR;
+ YYCURSOR = (const unsigned char *) this->ds_input.udata() + this->ds_next_offset;
+ _YYCURSOR yyt1;
+ _YYCURSOR yyt2;
+ _YYCURSOR yyt3;
+ _YYCURSOR yyt4;
+ const YYCTYPE *YYLIMIT = (const unsigned char *) this->ds_input.end();
+ const YYCTYPE *YYMARKER = YYCURSOR;
+
+ YYCURSOR.lim = YYLIMIT;
+
+ cap_all.c_begin = this->ds_next_offset;
+ cap_all.c_end = this->ds_next_offset;
+ cap_inner.c_begin = this->ds_next_offset;
+ cap_inner.c_end = this->ds_next_offset;
+
+
+#line 117 "../../lnav/src/data_scanner_re.cc"
+{
+ YYCTYPE yych;
+ unsigned int yyaccept = 0;
+ static const unsigned char yybm[] = {
+ /* table 1 .. 8: 0 */
+ 0, 231, 231, 231, 231, 231, 231, 231,
+ 231, 239, 231, 231, 231, 239, 231, 231,
+ 231, 231, 231, 231, 231, 231, 231, 231,
+ 231, 231, 231, 231, 231, 231, 231, 231,
+ 239, 231, 34, 231, 231, 231, 231, 36,
+ 231, 231, 231, 231, 231, 175, 231, 183,
+ 175, 175, 175, 175, 175, 175, 175, 175,
+ 175, 175, 175, 231, 231, 231, 128, 183,
+ 231, 175, 175, 175, 175, 175, 175, 175,
+ 175, 175, 175, 175, 175, 175, 175, 175,
+ 175, 175, 175, 175, 175, 175, 175, 175,
+ 175, 175, 175, 231, 1, 231, 231, 175,
+ 231, 175, 175, 175, 175, 175, 175, 175,
+ 175, 175, 175, 175, 175, 175, 175, 175,
+ 175, 175, 175, 175, 175, 175, 175, 175,
+ 175, 175, 175, 231, 231, 231, 231, 231,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ /* table 9 .. 16: 256 */
+ 0, 249, 249, 249, 249, 249, 249, 249,
+ 249, 248, 248, 249, 249, 248, 249, 249,
+ 249, 249, 249, 249, 249, 249, 249, 249,
+ 249, 249, 249, 248, 249, 249, 249, 249,
+ 248, 249, 176, 249, 249, 252, 252, 104,
+ 248, 248, 249, 249, 249, 28, 249, 28,
+ 28, 28, 28, 28, 28, 28, 28, 28,
+ 28, 28, 25, 249, 249, 252, 25, 28,
+ 249, 30, 30, 30, 30, 30, 30, 30,
+ 30, 30, 30, 30, 30, 30, 30, 30,
+ 30, 30, 30, 30, 30, 30, 30, 30,
+ 30, 30, 30, 248, 33, 248, 249, 25,
+ 249, 30, 30, 30, 30, 30, 30, 30,
+ 30, 30, 30, 30, 30, 30, 30, 30,
+ 30, 30, 30, 30, 30, 30, 30, 30,
+ 30, 30, 30, 248, 249, 248, 249, 249,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ /* table 17 .. 24: 512 */
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 1, 0, 0, 0, 0, 0, 0, 32,
+ 0, 0, 0, 0, 0, 19, 64, 0,
+ 159, 159, 159, 159, 159, 159, 159, 159,
+ 155, 155, 16, 0, 0, 0, 0, 0,
+ 0, 155, 155, 155, 155, 155, 155, 147,
+ 147, 147, 147, 147, 147, 147, 147, 147,
+ 147, 147, 147, 147, 147, 147, 147, 147,
+ 147, 147, 147, 0, 0, 0, 0, 145,
+ 0, 154, 154, 154, 154, 154, 154, 146,
+ 146, 146, 146, 146, 146, 146, 146, 146,
+ 146, 146, 146, 146, 146, 146, 146, 146,
+ 146, 146, 146, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ /* table 25 .. 32: 768 */
+ 0, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 0, 2, 2, 2, 2,
+ 34, 10, 2, 10, 26, 11, 10, 0,
+ 10, 10, 10, 3, 2, 27, 27, 26,
+ 95, 95, 95, 95, 95, 95, 95, 95,
+ 95, 95, 2, 2, 2, 2, 2, 2,
+ 26, 91, 91, 91, 91, 91, 91, 91,
+ 91, 91, 91, 91, 91, 91, 91, 91,
+ 91, 91, 91, 91, 91, 91, 91, 91,
+ 91, 91, 91, 2, 8, 2, 10, 27,
+ 2, 219, 219, 219, 219, 219, 219, 219,
+ 219, 219, 219, 219, 219, 219, 219, 219,
+ 219, 219, 219, 219, 219, 219, 219, 219,
+ 219, 219, 219, 2, 2, 2, 10, 2,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ /* table 33 .. 37: 1024 */
+ 0, 136, 136, 136, 136, 136, 136, 136,
+ 136, 160, 128, 136, 136, 160, 136, 136,
+ 136, 136, 136, 136, 136, 136, 136, 136,
+ 136, 136, 136, 0, 136, 136, 136, 136,
+ 160, 128, 0, 128, 136, 128, 128, 128,
+ 128, 128, 128, 128, 128, 152, 152, 136,
+ 216, 216, 216, 216, 216, 216, 216, 216,
+ 216, 216, 128, 192, 128, 192, 128, 192,
+ 136, 152, 152, 152, 152, 152, 152, 152,
+ 152, 152, 152, 152, 152, 152, 152, 152,
+ 152, 152, 152, 152, 152, 152, 152, 152,
+ 152, 152, 152, 128, 0, 128, 128, 152,
+ 128, 152, 152, 152, 152, 152, 152, 152,
+ 152, 152, 152, 152, 152, 152, 152, 152,
+ 152, 152, 152, 152, 152, 152, 152, 152,
+ 152, 152, 152, 128, 128, 128, 128, 136,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ };
+ yych = *YYCURSOR;
+ if (yych <= 'E') {
+ if (yych <= '(') {
+ if (yych <= 0x1F) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy2;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\t') goto yy6;
+ goto yy8;
+ } else {
+ if (yych <= '\r') {
+ if (yych <= '\f') goto yy3;
+ goto yy10;
+ } else {
+ if (yych == 0x1B) goto yy11;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= '#') {
+ if (yych <= ' ') goto yy6;
+ if (yych == '"') goto yy14;
+ goto yy13;
+ } else {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy3;
+ goto yy15;
+ } else {
+ if (yych <= '&') goto yy13;
+ if (yych <= '\'') goto yy16;
+ goto yy17;
+ }
+ }
+ }
+ } else {
+ if (yych <= '1') {
+ if (yych <= ',') {
+ if (yych <= ')') goto yy18;
+ if (yych <= '*') goto yy13;
+ if (yych <= '+') goto yy15;
+ goto yy19;
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy20;
+ goto yy21;
+ } else {
+ if (yych <= '/') goto yy23;
+ if (yych <= '0') goto yy25;
+ goto yy27;
+ }
+ }
+ } else {
+ if (yych <= '<') {
+ if (yych <= '9') {
+ if (yych <= '2') goto yy28;
+ goto yy29;
+ } else {
+ if (yych <= ':') goto yy30;
+ if (yych <= ';') goto yy32;
+ goto yy33;
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= '=') goto yy35;
+ goto yy36;
+ } else {
+ if (yych <= '?') goto yy13;
+ if (yych <= '@') goto yy3;
+ goto yy37;
+ }
+ }
+ }
+ }
+ } else {
+ if (yych <= 'n') {
+ if (yych <= 'Z') {
+ if (yych <= 'Q') {
+ if (yych <= 'F') goto yy38;
+ if (yych == 'N') goto yy40;
+ goto yy39;
+ } else {
+ if (yych <= 'S') {
+ if (yych <= 'R') goto yy41;
+ goto yy39;
+ } else {
+ if (yych <= 'T') goto yy42;
+ if (yych <= 'U') goto yy41;
+ goto yy39;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '\\') {
+ if (yych <= '[') goto yy43;
+ goto yy44;
+ } else {
+ if (yych <= ']') goto yy45;
+ if (yych <= '^') goto yy13;
+ goto yy46;
+ }
+ } else {
+ if (yych <= 'e') {
+ if (yych <= '`') goto yy13;
+ goto yy47;
+ } else {
+ if (yych <= 'f') goto yy48;
+ if (yych <= 'm') goto yy49;
+ goto yy50;
+ }
+ }
+ }
+ } else {
+ if (yych <= '}') {
+ if (yych <= 't') {
+ if (yych == 'r') goto yy51;
+ if (yych <= 's') goto yy49;
+ goto yy52;
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'u') goto yy53;
+ goto yy49;
+ } else {
+ if (yych <= '{') goto yy54;
+ if (yych <= '|') goto yy13;
+ goto yy55;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy13;
+ goto yy3;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ }
+ }
+ }
+ }
+ }
+yy1:
+ YYCURSOR = YYMARKER;
+ if (yyaccept <= 17) {
+ if (yyaccept <= 8) {
+ if (yyaccept <= 4) {
+ if (yyaccept <= 2) {
+ if (yyaccept <= 1) {
+ if (yyaccept == 0) {
+ goto yy5;
+ } else {
+ goto yy7;
+ }
+ } else {
+ goto yy9;
+ }
+ } else {
+ if (yyaccept == 3) {
+ goto yy12;
+ } else {
+ goto yy22;
+ }
+ }
+ } else {
+ if (yyaccept <= 6) {
+ if (yyaccept == 5) {
+ goto yy24;
+ } else {
+ goto yy26;
+ }
+ } else {
+ if (yyaccept == 7) {
+ goto yy31;
+ } else {
+ goto yy34;
+ }
+ }
+ }
+ } else {
+ if (yyaccept <= 13) {
+ if (yyaccept <= 11) {
+ if (yyaccept <= 10) {
+ if (yyaccept == 9) {
+ goto yy72;
+ } else {
+ goto yy106;
+ }
+ } else {
+ goto yy109;
+ }
+ } else {
+ if (yyaccept == 12) {
+ goto yy113;
+ } else {
+ goto yy125;
+ }
+ }
+ } else {
+ if (yyaccept <= 15) {
+ if (yyaccept == 14) {
+ goto yy161;
+ } else {
+ goto yy222;
+ }
+ } else {
+ if (yyaccept == 16) {
+ yyt3 = yyt4;
+ goto yy222;
+ } else {
+ goto yy199;
+ }
+ }
+ }
+ }
+ } else {
+ if (yyaccept <= 26) {
+ if (yyaccept <= 22) {
+ if (yyaccept <= 20) {
+ if (yyaccept <= 19) {
+ if (yyaccept == 18) {
+ yyt2 = yyt1;
+ goto yy199;
+ } else {
+ goto yy338;
+ }
+ } else {
+ goto yy345;
+ }
+ } else {
+ if (yyaccept == 21) {
+ goto yy354;
+ } else {
+ goto yy371;
+ }
+ }
+ } else {
+ if (yyaccept <= 24) {
+ if (yyaccept == 23) {
+ goto yy428;
+ } else {
+ goto yy450;
+ }
+ } else {
+ if (yyaccept == 25) {
+ goto yy435;
+ } else {
+ goto yy217;
+ }
+ }
+ }
+ } else {
+ if (yyaccept <= 31) {
+ if (yyaccept <= 29) {
+ if (yyaccept <= 28) {
+ if (yyaccept == 27) {
+ goto yy310;
+ } else {
+ goto yy315;
+ }
+ } else {
+ goto yy643;
+ }
+ } else {
+ if (yyaccept == 30) {
+ goto yy654;
+ } else {
+ goto yy674;
+ }
+ }
+ } else {
+ if (yyaccept <= 33) {
+ if (yyaccept == 32) {
+ goto yy943;
+ } else {
+ goto yy971;
+ }
+ } else {
+ if (yyaccept == 34) {
+ goto yy1017;
+ } else {
+ goto yy1091;
+ }
+ }
+ }
+ }
+ }
+yy2:
+ ++YYCURSOR;
+#line 141 "../../lnav/src/data_scanner_re.re"
+ { return nonstd::nullopt; }
+#line 586 "../../lnav/src/data_scanner_re.cc"
+yy3:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+yy4:
+ if (yybm[1024+yych] & 8) {
+ goto yy3;
+ }
+ if (yych <= 0xE0) {
+ if (yych <= ':') {
+ if (yych >= '-') goto yy62;
+ } else {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ }
+ }
+yy5:
+#line 266 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_SYMBOL);
+ }
+#line 616 "../../lnav/src/data_scanner_re.cc"
+yy6:
+ yyaccept = 1;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') goto yy64;
+ if (yych <= '9') goto yy65;
+ if (yych <= ':') goto yy66;
+ goto yy64;
+yy7:
+#line 271 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_WHITE); }
+#line 627 "../../lnav/src/data_scanner_re.cc"
+yy8:
+ yyaccept = 2;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') goto yy9;
+ if (yych <= 'Z') goto yy67;
+yy9:
+#line 270 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_LINE); }
+#line 636 "../../lnav/src/data_scanner_re.cc"
+yy10:
+ yyaccept = 1;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych == '\n') goto yy68;
+ goto yy64;
+ } else {
+ if (yych <= '9') goto yy65;
+ if (yych <= ':') goto yy66;
+ goto yy64;
+ }
+yy11:
+ yyaccept = 3;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '[') goto yy69;
+yy12:
+#line 274 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_GARBAGE); }
+#line 655 "../../lnav/src/data_scanner_re.cc"
+yy13:
+ ++YYCURSOR;
+ goto yy12;
+yy14:
+ yyaccept = 3;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0x1B) {
+ if (yych <= 0x00) goto yy12;
+ if (yych <= 0x1A) goto yy71;
+ goto yy12;
+ } else {
+ if (yych <= 0x7F) goto yy71;
+ if (yych <= 0xC1) goto yy12;
+ if (yych <= 0xF4) goto yy71;
+ goto yy12;
+ }
+yy15:
+ yyaccept = 3;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '*') {
+ if (yych == '%') goto yy81;
+ goto yy12;
+ } else {
+ if (yych == ',') goto yy12;
+ if (yych <= '.') goto yy81;
+ goto yy12;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '9') goto yy81;
+ if (yych <= '?') goto yy12;
+ if (yych <= 'Z') goto yy81;
+ goto yy12;
+ } else {
+ if (yych == '`') goto yy12;
+ if (yych <= 'z') goto yy81;
+ goto yy12;
+ }
+ }
+yy16:
+ yyaccept = 3;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0x1B) {
+ if (yych <= 0x00) goto yy12;
+ if (yych <= 0x1A) goto yy84;
+ goto yy12;
+ } else {
+ if (yych <= 0x7F) goto yy84;
+ if (yych <= 0xC1) goto yy12;
+ if (yych <= 0xF4) goto yy84;
+ goto yy12;
+ }
+yy17:
+ yych = *++YYCURSOR;
+ if (yych == ')') goto yy93;
+#line 225 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_LPAREN); }
+#line 714 "../../lnav/src/data_scanner_re.cc"
+yy18:
+ ++YYCURSOR;
+#line 226 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_RPAREN); }
+#line 719 "../../lnav/src/data_scanner_re.cc"
+yy19:
+ ++YYCURSOR;
+#line 218 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_COMMA); }
+#line 724 "../../lnav/src/data_scanner_re.cc"
+yy20:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '0') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy94;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') goto yy95;
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy21:
+ yyaccept = 4;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy81;
+ if (yych >= '+') goto yy81;
+ } else {
+ if (yych <= ',') goto yy22;
+ if (yych <= '-') goto yy81;
+ if (yych <= '.') goto yy97;
+ goto yy98;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '9') goto yy81;
+ if (yych <= '?') goto yy22;
+ if (yych <= 'Z') goto yy81;
+ } else {
+ if (yych == '`') goto yy22;
+ if (yych <= 'z') goto yy81;
+ }
+ }
+yy22:
+#line 272 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_DOT); }
+#line 782 "../../lnav/src/data_scanner_re.cc"
+yy23:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy24;
+ if (yych <= 0x08) goto yy4;
+ if (yych >= '\v') goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych >= 0x0E) goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy24;
+ if (yych <= 0x1F) goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '$') {
+ if (yych == '"') goto yy24;
+ if (yych <= '#') goto yy99;
+ goto yy101;
+ } else {
+ if (yych <= '\'') {
+ if (yych <= '&') goto yy99;
+ } else {
+ if (yych <= '*') goto yy99;
+ if (yych >= '-') goto yy101;
+ }
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= 'P') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy24;
+ if (yych <= 'O') goto yy101;
+ goto yy102;
+ } else {
+ if (yych <= '[') {
+ if (yych <= 'Z') goto yy101;
+ } else {
+ if (yych != ']') goto yy99;
+ }
+ }
+ } else {
+ if (yych <= '}') {
+ if (yych == '`') goto yy24;
+ if (yych <= 'z') goto yy101;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy99;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy24;
+ if (yych <= 0xF4) goto yy4;
+ }
+ }
+ }
+ }
+yy24:
+#line 171 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_PATH); }
+#line 846 "../../lnav/src/data_scanner_re.cc"
+yy25:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= ':') {
+ if (yych <= '$') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych >= 0x01) goto yy4;
+ } else {
+ if (yych <= '\n') goto yy26;
+ if (yych <= '\f') goto yy4;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy4;
+ } else {
+ if (yych <= 0x1F) goto yy4;
+ if (yych >= '$') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '*') {
+ if (yych <= '%') goto yy105;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych >= '-') goto yy46;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') goto yy107;
+ goto yy4;
+ } else {
+ if (yych <= '7') goto yy108;
+ if (yych <= '9') goto yy110;
+ goto yy111;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'd') {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ if (yych >= '@') goto yy96;
+ } else {
+ if (yych == 'E') goto yy114;
+ goto yy112;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= 'Z') goto yy115;
+ } else {
+ if (yych <= '_') goto yy46;
+ if (yych >= 'a') goto yy112;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'f') {
+ if (yych <= 'e') goto yy114;
+ goto yy112;
+ } else {
+ if (yych == 'x') goto yy116;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych >= 0x7F) goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ }
+ }
+ }
+ }
+yy26:
+#line 257 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_NUMBER); }
+#line 928 "../../lnav/src/data_scanner_re.cc"
+yy27:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy107;
+ if (yych <= '/') goto yy4;
+ goto yy117;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy111;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy112;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy114;
+ goto yy112;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy112;
+ } else {
+ if (yych <= 'e') goto yy114;
+ if (yych <= 'f') goto yy112;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy28:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '5') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy107;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '4') goto yy117;
+ goto yy118;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') {
+ if (yych <= '9') goto yy110;
+ goto yy111;
+ } else {
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy112;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy114;
+ goto yy112;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy112;
+ } else {
+ if (yych <= 'e') goto yy114;
+ if (yych <= 'f') goto yy112;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy29:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy107;
+ if (yych <= '/') goto yy4;
+ goto yy110;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy111;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy112;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy114;
+ goto yy112;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy112;
+ } else {
+ if (yych <= 'e') goto yy114;
+ if (yych <= 'f') goto yy112;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy30:
+ yyaccept = 7;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == ':') goto yy119;
+yy31:
+#line 216 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_COLON); }
+#line 1180 "../../lnav/src/data_scanner_re.cc"
+yy32:
+ ++YYCURSOR;
+#line 219 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_SEMI); }
+#line 1185 "../../lnav/src/data_scanner_re.cc"
+yy33:
+ yyaccept = 8;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '>') {
+ if (yych <= '-') {
+ if (yych == '!') goto yy120;
+ if (yych >= '-') goto yy121;
+ } else {
+ if (yych <= '.') goto yy34;
+ if (yych <= '/') goto yy122;
+ if (yych <= ':') goto yy121;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy123;
+ if (yych <= '@') goto yy34;
+ if (yych <= 'Z') goto yy121;
+ } else {
+ if (yych == '`') goto yy34;
+ if (yych <= 'z') goto yy121;
+ }
+ }
+yy34:
+#line 227 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_LANGLE); }
+#line 1211 "../../lnav/src/data_scanner_re.cc"
+yy35:
+ ++YYCURSOR;
+#line 217 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_EQUALS); }
+#line 1216 "../../lnav/src/data_scanner_re.cc"
+yy36:
+ ++YYCURSOR;
+#line 228 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_RANGLE); }
+#line 1221 "../../lnav/src/data_scanner_re.cc"
+yy37:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '*') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '\'') goto yy124;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy126;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ if (yych <= ':') goto yy127;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy128;
+ goto yy129;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych == '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy130;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy38:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '*') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '\'') goto yy124;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy126;
+ goto yy127;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= 'A') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy132;
+ } else {
+ if (yych <= 'F') goto yy128;
+ if (yych <= 'Z') goto yy129;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'a') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ goto yy133;
+ } else {
+ if (yych <= 'f') goto yy130;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy39:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '\'') {
+ if (yych == '%') goto yy80;
+ if (yych <= '&') goto yy4;
+ goto yy124;
+ } else {
+ if (yych <= '+') {
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= ':') {
+ if (yych <= '9') goto yy115;
+ goto yy134;
+ } else {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy129;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy40:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '*') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '\'') goto yy124;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy115;
+ goto yy134;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= 'T') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy129;
+ } else {
+ if (yych <= 'U') goto yy135;
+ if (yych <= 'Z') goto yy129;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'n') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ goto yy131;
+ } else {
+ if (yych <= 'o') goto yy136;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy41:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '\'') {
+ if (yych == '%') goto yy80;
+ if (yych <= '&') goto yy4;
+ goto yy137;
+ } else {
+ if (yych <= '+') {
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= ':') {
+ if (yych <= '9') goto yy115;
+ goto yy134;
+ } else {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy129;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy42:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '*') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '\'') goto yy124;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy115;
+ goto yy134;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= 'Q') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy129;
+ } else {
+ if (yych <= 'R') goto yy138;
+ if (yych <= 'Z') goto yy129;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'q') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ goto yy131;
+ } else {
+ if (yych <= 'r') goto yy139;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy43:
+ yych = *++YYCURSOR;
+ if (yych == ']') goto yy93;
+#line 223 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_LSQUARE); }
+#line 1486 "../../lnav/src/data_scanner_re.cc"
+yy44:
+ yyaccept = 3;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0x7F) {
+ if (yych <= '[') {
+ if (yych == '\n') goto yy12;
+ goto yy140;
+ } else {
+ if (yych <= '\\') goto yy98;
+ if (yych == 'n') goto yy68;
+ goto yy140;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy12;
+ if (yych <= 0xDF) goto yy141;
+ if (yych <= 0xE0) goto yy142;
+ goto yy143;
+ } else {
+ if (yych <= 0xF0) goto yy144;
+ if (yych <= 0xF3) goto yy145;
+ if (yych <= 0xF4) goto yy146;
+ goto yy12;
+ }
+ }
+yy45:
+ ++YYCURSOR;
+#line 224 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_RSQUARE); }
+#line 1516 "../../lnav/src/data_scanner_re.cc"
+yy46:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[1024+yych] & 16) {
+ goto yy46;
+ }
+ if (yych <= ',') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy5;
+ if (yych <= 0x08) goto yy3;
+ goto yy5;
+ } else {
+ if (yych == '\r') goto yy5;
+ if (yych <= 0x1A) goto yy3;
+ goto yy5;
+ }
+ } else {
+ if (yych <= '$') {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= '#') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= '%') goto yy80;
+ if (yych == '+') goto yy80;
+ goto yy5;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy3;
+ if (yych <= ':') goto yy62;
+ goto yy5;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych == 0x7F) goto yy3;
+ goto yy5;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy56;
+ if (yych <= 0xE0) goto yy57;
+ goto yy58;
+ } else {
+ if (yych <= 0xF0) goto yy59;
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+yy47:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '*') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '\'') goto yy124;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy126;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ if (yych <= ':') goto yy147;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy128;
+ goto yy129;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych == '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy130;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy48:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '*') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '\'') goto yy124;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy126;
+ goto yy147;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy128;
+ if (yych <= 'Z') goto yy129;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'a') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ goto yy133;
+ } else {
+ if (yych <= 'f') goto yy130;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy49:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '\'') {
+ if (yych == '%') goto yy80;
+ if (yych <= '&') goto yy4;
+ goto yy124;
+ } else {
+ if (yych <= '+') {
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= ':') {
+ if (yych <= '9') goto yy115;
+ goto yy148;
+ } else {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy129;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy50:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '*') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '\'') goto yy124;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= ':') goto yy148;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy129;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 't') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ goto yy131;
+ } else {
+ if (yych <= 'u') goto yy149;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy51:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ if (yych <= '!') goto yy4;
+ goto yy70;
+ } else {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '\'') goto yy150;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '9') goto yy115;
+ if (yych <= ':') goto yy148;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy129;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'd') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ goto yy131;
+ } else {
+ if (yych <= 'e') goto yy151;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy52:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '*') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '\'') goto yy124;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= ':') goto yy148;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy129;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'q') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ goto yy131;
+ } else {
+ if (yych <= 'r') goto yy139;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy53:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ if (yych <= '!') goto yy4;
+ goto yy70;
+ } else {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '\'') goto yy150;
+ goto yy4;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '9') {
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ } else {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy129;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy131;
+ goto yy4;
+ }
+ }
+ }
+yy54:
+ yych = *++YYCURSOR;
+ if (yych == '}') goto yy93;
+#line 221 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_LCURLY); }
+#line 1879 "../../lnav/src/data_scanner_re.cc"
+yy55:
+ ++YYCURSOR;
+#line 222 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_RCURLY); }
+#line 1884 "../../lnav/src/data_scanner_re.cc"
+yy56:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy3;
+ goto yy1;
+yy57:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy56;
+ goto yy1;
+yy58:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy56;
+ goto yy1;
+yy59:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy58;
+ goto yy1;
+yy60:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy58;
+ goto yy1;
+yy61:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy58;
+ goto yy1;
+yy62:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy152;
+ goto yy1;
+yy63:
+ yych = *++YYCURSOR;
+yy64:
+ if (yybm[1024+yych] & 32) {
+ goto yy63;
+ }
+ goto yy7;
+yy65:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy153;
+ goto yy1;
+yy66:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy154;
+ goto yy1;
+yy67:
+ yych = *++YYCURSOR;
+ if (yych == '\n') goto yy1;
+ goto yy156;
+yy68:
+ ++YYCURSOR;
+ goto yy9;
+yy69:
+ yych = *++YYCURSOR;
+ if (yybm[1024+yych] & 64) {
+ goto yy69;
+ }
+ if (yych <= '@') goto yy1;
+ if (yych <= 'Z') goto yy157;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy157;
+ goto yy1;
+yy70:
+ yych = *++YYCURSOR;
+yy71:
+ if (yybm[1024+yych] & 128) {
+ goto yy70;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '"') {
+ if (yych <= 0x1B) goto yy1;
+ } else {
+ if (yych <= '\\') goto yy73;
+ if (yych <= 0xC1) goto yy1;
+ goto yy74;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy75;
+ if (yych <= 0xEF) goto yy76;
+ goto yy77;
+ } else {
+ if (yych <= 0xF3) goto yy78;
+ if (yych <= 0xF4) goto yy79;
+ goto yy1;
+ }
+ }
+ yyaccept = 9;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '"') goto yy70;
+yy72:
+#line 143 "../../lnav/src/data_scanner_re.re"
+ {
+ CAPTURE(DT_QUOTED_STRING);
+ switch (this->ds_input[cap_inner.c_begin]) {
+ case 'u':
+ case 'r':
+ cap_inner.c_begin += 1;
+ break;
+ }
+ cap_inner.c_begin += 1;
+ cap_inner.c_end -= 1;
+ return tokenize_result{token_out, cap_all, cap_inner, this->ds_input.data()};
+ }
+#line 1994 "../../lnav/src/data_scanner_re.cc"
+yy73:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy70;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy70;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy75;
+ if (yych <= 0xEF) goto yy76;
+ goto yy77;
+ } else {
+ if (yych <= 0xF3) goto yy78;
+ if (yych <= 0xF4) goto yy79;
+ goto yy1;
+ }
+ }
+yy74:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy70;
+ goto yy1;
+yy75:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy74;
+ goto yy1;
+yy76:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy74;
+ goto yy1;
+yy77:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy76;
+ goto yy1;
+yy78:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy76;
+ goto yy1;
+yy79:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy76;
+ goto yy1;
+yy80:
+ yych = *++YYCURSOR;
+yy81:
+ if (yybm[768+yych] & 1) {
+ goto yy80;
+ }
+ if (yych != '@') goto yy1;
+yy82:
+ yych = *++YYCURSOR;
+ if (yych == '.') goto yy158;
+ goto yy159;
+yy83:
+ yych = *++YYCURSOR;
+yy84:
+ if (yybm[768+yych] & 2) {
+ goto yy83;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '\'') {
+ if (yych <= 0x1B) goto yy1;
+ } else {
+ if (yych <= '\\') goto yy86;
+ if (yych <= 0xC1) goto yy1;
+ goto yy87;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy88;
+ if (yych <= 0xEF) goto yy89;
+ goto yy90;
+ } else {
+ if (yych <= 0xF3) goto yy91;
+ if (yych <= 0xF4) goto yy92;
+ goto yy1;
+ }
+ }
+yy85:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= 'R') {
+ if (yych == '\'') {
+ yyt2 = YYCURSOR;
+ goto yy162;
+ }
+ yyt2 = YYCURSOR;
+ goto yy160;
+ } else {
+ if (yych <= 'S') goto yy1;
+ if (yych == 's') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy160;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy163;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy164;
+ }
+ yyt2 = YYCURSOR;
+ goto yy165;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy166;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy167;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy168;
+ }
+ goto yy1;
+ }
+ }
+yy86:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy83;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy83;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy88;
+ if (yych <= 0xEF) goto yy89;
+ goto yy90;
+ } else {
+ if (yych <= 0xF3) goto yy91;
+ if (yych <= 0xF4) goto yy92;
+ goto yy1;
+ }
+ }
+yy87:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy83;
+ goto yy1;
+yy88:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy87;
+ goto yy1;
+yy89:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy87;
+ goto yy1;
+yy90:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy89;
+ goto yy1;
+yy91:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy89;
+ goto yy1;
+yy92:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy89;
+ goto yy1;
+yy93:
+ ++YYCURSOR;
+#line 220 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_EMPTY_CONTAINER); }
+#line 2182 "../../lnav/src/data_scanner_re.cc"
+yy94:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= ':') {
+ if (yych <= '$') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\n') goto yy26;
+ if (yych <= '\f') goto yy4;
+ goto yy26;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy4;
+ goto yy26;
+ } else {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy26;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '*') {
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') goto yy169;
+ goto yy4;
+ } else {
+ if (yych <= '7') goto yy170;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'd') {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych == 'E') goto yy172;
+ goto yy171;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= 'Z') goto yy46;
+ goto yy26;
+ } else {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy26;
+ goto yy171;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'f') {
+ if (yych <= 'e') goto yy172;
+ goto yy171;
+ } else {
+ if (yych == 'x') goto yy173;
+ goto yy46;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy95:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '@') {
+ if (yych <= '$') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= '\n') goto yy26;
+ if (yych <= '\f') goto yy3;
+ goto yy26;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy3;
+ goto yy26;
+ } else {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= '#') goto yy26;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '*') {
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '9') {
+ if (yych <= '.') goto yy169;
+ if (yych <= '/') goto yy3;
+ goto yy95;
+ } else {
+ if (yych <= ':') goto yy62;
+ if (yych <= '?') goto yy26;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'f') {
+ if (yych <= '^') {
+ if (yych <= 'E') {
+ if (yych <= 'D') goto yy171;
+ goto yy172;
+ } else {
+ if (yych <= 'F') goto yy171;
+ if (yych <= 'Z') goto yy46;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy26;
+ } else {
+ if (yych == 'e') goto yy172;
+ goto yy171;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '~') {
+ if (yych <= 'z') goto yy46;
+ goto yy26;
+ } else {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy26;
+ goto yy56;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy57;
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy96:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= ',') goto yy4;
+ if (yych == '/') goto yy4;
+ goto yy174;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '@') goto yy4;
+ goto yy174;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy174;
+ goto yy4;
+ }
+ }
+yy97:
+ yych = *++YYCURSOR;
+ if (yych != '/') goto yy81;
+yy98:
+ yych = *++YYCURSOR;
+ if (yych == 'P') goto yy175;
+ goto yy100;
+yy99:
+ yych = *++YYCURSOR;
+yy100:
+ if (yybm[768+yych] & 8) {
+ goto yy99;
+ }
+ goto yy24;
+yy101:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 16) {
+ goto yy101;
+ }
+ if (yych <= ',') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy24;
+ if (yych <= 0x08) goto yy3;
+ goto yy24;
+ } else {
+ if (yych == '\r') goto yy24;
+ if (yych <= 0x1A) goto yy3;
+ goto yy24;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= 0x1F) goto yy3;
+ if (yych == '!') goto yy99;
+ goto yy24;
+ } else {
+ if (yych == '\'') goto yy24;
+ if (yych <= '*') goto yy99;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= ']') {
+ if (yych <= ':') goto yy62;
+ if (yych == '\\') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '^') goto yy99;
+ if (yych <= '}') goto yy24;
+ if (yych <= '~') goto yy99;
+ goto yy3;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy24;
+ if (yych <= 0xDF) goto yy56;
+ if (yych <= 0xE0) goto yy57;
+ goto yy58;
+ } else {
+ if (yych <= 0xF0) goto yy59;
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy24;
+ }
+ }
+ }
+yy102:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy24;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy24;
+ if (yych <= 0x1F) goto yy4;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '$') {
+ if (yych == '"') goto yy24;
+ if (yych <= '#') goto yy99;
+ goto yy101;
+ } else {
+ if (yych <= '\'') {
+ if (yych <= '&') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '*') goto yy99;
+ if (yych <= ',') goto yy24;
+ goto yy101;
+ }
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '[') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy24;
+ if (yych <= 'Z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= ']') {
+ if (yych <= '\\') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '^') goto yy99;
+ if (yych <= '_') goto yy101;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '}') {
+ if (yych == 'r') goto yy176;
+ if (yych <= 'z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy99;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy24;
+ if (yych <= 0xF4) goto yy4;
+ goto yy24;
+ }
+ }
+ }
+ }
+yy103:
+ yych = *++YYCURSOR;
+yy104:
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych == '%') goto yy177;
+ goto yy1;
+yy105:
+ yyaccept = 10;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '*') {
+ if (yych == '%') goto yy81;
+ } else {
+ if (yych == ',') goto yy106;
+ if (yych <= '.') goto yy81;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '9') goto yy81;
+ if (yych <= '?') goto yy106;
+ if (yych <= 'Z') goto yy81;
+ } else {
+ if (yych == '`') goto yy106;
+ if (yych <= 'z') goto yy81;
+ }
+ }
+yy106:
+#line 256 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_PERCENTAGE); }
+#line 2548 "../../lnav/src/data_scanner_re.cc"
+yy107:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '1') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '0') goto yy178;
+ goto yy179;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') {
+ if (yych <= '2') goto yy180;
+ goto yy178;
+ } else {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy108:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych >= '\v') goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych >= 0x0E) goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy181;
+ goto yy182;
+ } else {
+ if (yych <= '/') goto yy183;
+ if (yych <= '7') goto yy184;
+ goto yy185;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy186;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy187;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy188;
+ goto yy187;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych >= '_') goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych >= 'a') goto yy187;
+ } else {
+ if (yych <= 'e') goto yy188;
+ if (yych <= 'f') goto yy187;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych >= 0x7F) goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ }
+ }
+ }
+ }
+yy109:
+#line 255 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_OCTAL_NUMBER); }
+#line 2665 "../../lnav/src/data_scanner_re.cc"
+yy110:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy181;
+ } else {
+ if (yych <= '.') goto yy182;
+ if (yych <= '/') goto yy183;
+ goto yy185;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy186;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy187;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy188;
+ goto yy187;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy187;
+ } else {
+ if (yych <= 'e') goto yy188;
+ if (yych <= 'f') goto yy187;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy111:
+ yych = *++YYCURSOR;
+ if (yych <= ':') {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy189;
+ if (yych <= '9') goto yy190;
+ goto yy191;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy1;
+ goto yy192;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy192;
+ goto yy1;
+ }
+ }
+yy112:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych >= '\v') goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych != 0x1B) goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy181;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy187;
+ if (yych <= ':') goto yy193;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy187;
+ if (yych <= 'Z') goto yy115;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy187;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych >= 0x7F) goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ }
+ }
+ }
+ }
+yy113:
+#line 258 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_HEX_NUMBER); }
+#line 2817 "../../lnav/src/data_scanner_re.cc"
+yy114:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy194;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy195;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy187;
+ if (yych <= ':') goto yy193;
+ goto yy113;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy187;
+ if (yych >= '[') goto yy113;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy187;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy115:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 64) {
+ goto yy115;
+ }
+ if (yych <= '.') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy5;
+ if (yych <= 0x08) goto yy3;
+ goto yy5;
+ } else {
+ if (yych == '\r') goto yy5;
+ if (yych <= 0x1A) goto yy3;
+ goto yy5;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= '#') goto yy5;
+ if (yych <= '$') goto yy3;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy5;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') goto yy3;
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy5;
+ goto yy96;
+ } else {
+ if (yych == '_') goto yy46;
+ if (yych <= '~') goto yy5;
+ goto yy3;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) goto yy56;
+ if (yych <= 0xE0) goto yy57;
+ goto yy58;
+ } else {
+ if (yych <= 0xF0) goto yy59;
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+yy116:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy196;
+ goto yy148;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy196;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy196;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy117:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy181;
+ } else {
+ if (yych <= '.') goto yy182;
+ if (yych <= '/') goto yy183;
+ goto yy197;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy186;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy187;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy188;
+ goto yy187;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy187;
+ } else {
+ if (yych <= 'e') goto yy188;
+ if (yych <= 'f') goto yy187;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy118:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy181;
+ goto yy182;
+ } else {
+ if (yych <= '/') goto yy183;
+ if (yych <= '5') goto yy197;
+ goto yy185;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy186;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy187;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy188;
+ goto yy187;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy187;
+ } else {
+ if (yych <= 'e') goto yy188;
+ if (yych <= 'f') goto yy187;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy119:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '0') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy200;
+ } else {
+ if (yych <= '1') goto yy201;
+ if (yych <= '2') goto yy202;
+ goto yy200;
+ }
+ } else {
+ if (yych <= 'E') {
+ if (yych <= ':') goto yy1;
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy203;
+ } else {
+ if (yych <= 'F') goto yy204;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'e') goto yy203;
+ if (yych <= 'f') goto yy204;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy120:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy1;
+ goto yy211;
+ } else {
+ if (yych <= '/') goto yy1;
+ if (yych <= ':') goto yy211;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy211;
+ if (yych <= '^') goto yy1;
+ goto yy211;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy211;
+ goto yy1;
+ }
+ }
+yy121:
+ yych = *++YYCURSOR;
+ if (yybm[512+yych] & 16) {
+ goto yy214;
+ }
+ goto yy213;
+yy122:
+ yych = *++YYCURSOR;
+ if (yych <= '\r') {
+ if (yych == '\t') goto yy1;
+ if (yych <= '\f') goto yy219;
+ goto yy1;
+ } else {
+ if (yych <= ' ') {
+ if (yych <= 0x1F) goto yy219;
+ goto yy1;
+ } else {
+ if (yych == '>') goto yy1;
+ goto yy219;
+ }
+ }
+yy123:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy1;
+ goto yy220;
+ } else {
+ if (yych <= '/') goto yy1;
+ if (yych <= ':') goto yy220;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy220;
+ if (yych <= '^') goto yy1;
+ goto yy220;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy220;
+ goto yy1;
+ }
+ }
+yy124:
+ yyaccept = 13;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 128) {
+ goto yy137;
+ }
+ if (yych <= '*') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy225;
+ if (yych <= 0x08) goto yy125;
+ if (yych <= '\n') goto yy225;
+ } else {
+ if (yych <= 0x1F) {
+ if (yych <= '\r') goto yy225;
+ } else {
+ if (yych <= '"') goto yy225;
+ if (yych >= '\'') goto yy225;
+ }
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych == ',') goto yy225;
+ if (yych >= '.') {
+ yyt4 = YYCURSOR;
+ goto yy226;
+ }
+ } else {
+ if (yych <= ';') {
+ if (yych >= ':') goto yy225;
+ } else {
+ if (yych == '?') goto yy225;
+ }
+ }
+ }
+yy125:
+#line 155 "../../lnav/src/data_scanner_re.re"
+ {
+ CAPTURE(DT_WORD);
+ }
+#line 3304 "../../lnav/src/data_scanner_re.cc"
+yy126:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy181;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy227;
+ goto yy193;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy227;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy227;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy127:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') {
+ if (yych <= '.') goto yy1;
+ goto yy189;
+ } else {
+ if (yych <= '9') goto yy192;
+ if (yych <= ':') goto yy191;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '\\') {
+ if (yych <= 'F') goto yy192;
+ if (yych <= '[') goto yy1;
+ goto yy98;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy192;
+ goto yy1;
+ }
+ }
+yy128:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy181;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy227;
+ goto yy193;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy228;
+ if (yych <= 'Z') goto yy229;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy228;
+ if (yych <= 'z') goto yy229;
+ goto yy4;
+ }
+ }
+ }
+yy129:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy229;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy229;
+ goto yy4;
+ }
+ }
+ }
+yy130:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy181;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy227;
+ } else {
+ if (yych <= ':') {
+ yyt4 = YYCURSOR;
+ goto yy231;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy228;
+ if (yych <= 'Z') goto yy229;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy232;
+ if (yych <= 'z') goto yy233;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy131:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '+') {
+ if (yych <= ' ') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ } else {
+ if (yych == '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= '!') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '&') goto yy4;
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy80;
+ }
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= '/') {
+ if (yych <= ',') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '9') goto yy115;
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy229;
+ goto yy4;
+ } else {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy233;
+ goto yy4;
+ }
+ }
+ }
+yy132:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych <= '-') goto yy181;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '9') goto yy227;
+ if (yych <= ':') goto yy193;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy96;
+ goto yy228;
+ } else {
+ if (yych == 'L') goto yy235;
+ goto yy229;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych == '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy228;
+ if (yych <= 'z') goto yy229;
+ goto yy4;
+ }
+ }
+ }
+yy133:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '-') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '*') {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ yyt4 = YYCURSOR;
+ goto yy221;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ goto yy181;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy227;
+ yyt4 = YYCURSOR;
+ goto yy231;
+ } else {
+ if (yych <= '>') {
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '?') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '@') goto yy96;
+ goto yy228;
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= 'Z') goto yy229;
+ if (yych == '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'k') {
+ if (yych <= 'f') goto yy232;
+ goto yy233;
+ } else {
+ if (yych <= 'l') goto yy236;
+ if (yych <= 'z') goto yy233;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy134:
+ yych = *++YYCURSOR;
+ if (yych <= '9') {
+ if (yych == '/') goto yy189;
+ goto yy1;
+ } else {
+ if (yych <= ':') goto yy152;
+ if (yych == '\\') goto yy98;
+ goto yy1;
+ }
+yy135:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy115;
+ goto yy148;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych == 'L') goto yy237;
+ goto yy229;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy229;
+ goto yy4;
+ }
+ }
+ }
+yy136:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy229;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= 'm') {
+ if (yych <= '`') goto yy4;
+ goto yy233;
+ } else {
+ if (yych <= 'n') goto yy238;
+ if (yych <= 'z') goto yy233;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy137:
+ yych = *++YYCURSOR;
+ if (yybm[768+yych] & 128) {
+ goto yy137;
+ }
+ if (yych <= '&') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy1;
+ } else {
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '\f') goto yy1;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ } else {
+ if (yych <= ' ') {
+ if (yych <= 0x1F) goto yy1;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ } else {
+ if (yych <= '!') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '*') {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ yyt4 = YYCURSOR;
+ goto yy221;
+ } else {
+ if (yych == ',') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ goto yy1;
+ }
+ } else {
+ if (yych <= ';') {
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy226;
+ }
+ if (yych <= '9') goto yy1;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ } else {
+ if (yych == '?') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy138:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy115;
+ goto yy148;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych == 'U') goto yy239;
+ goto yy229;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy229;
+ goto yy4;
+ }
+ }
+ }
+yy139:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy229;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= 't') {
+ if (yych <= '`') goto yy4;
+ goto yy233;
+ } else {
+ if (yych <= 'u') goto yy238;
+ if (yych <= 'z') goto yy233;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy140:
+ ++YYCURSOR;
+#line 273 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_ESCAPED_CHAR); }
+#line 4114 "../../lnav/src/data_scanner_re.cc"
+yy141:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy140;
+ goto yy1;
+yy142:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy141;
+ goto yy1;
+yy143:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy141;
+ goto yy1;
+yy144:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy143;
+ goto yy1;
+yy145:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy143;
+ goto yy1;
+yy146:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy143;
+ goto yy1;
+yy147:
+ yych = *++YYCURSOR;
+ if (yych <= ':') {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy189;
+ if (yych <= '9') goto yy192;
+ goto yy191;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy1;
+ goto yy192;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy192;
+ goto yy1;
+ }
+ }
+yy148:
+ yych = *++YYCURSOR;
+ if (yych == '/') goto yy189;
+ if (yych == ':') goto yy152;
+ goto yy1;
+yy149:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy229;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= 'k') {
+ if (yych <= '`') goto yy4;
+ goto yy233;
+ } else {
+ if (yych <= 'l') goto yy240;
+ if (yych <= 'z') goto yy233;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy150:
+ yych = *++YYCURSOR;
+ if (yych <= '-') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy83;
+ if (yych <= '\n') {
+ yyt3 = YYCURSOR;
+ goto yy241;
+ }
+ goto yy83;
+ } else {
+ if (yych <= '\r') {
+ yyt3 = YYCURSOR;
+ goto yy241;
+ }
+ if (yych == 0x1B) goto yy1;
+ goto yy83;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych == '!') {
+ yyt3 = YYCURSOR;
+ goto yy242;
+ }
+ if (yych <= '"') {
+ yyt3 = YYCURSOR;
+ goto yy241;
+ }
+ goto yy83;
+ } else {
+ if (yych <= '*') {
+ if (yych <= '\'') {
+ yyt3 = YYCURSOR;
+ goto yy243;
+ }
+ yyt3 = YYCURSOR;
+ goto yy241;
+ } else {
+ if (yych == ',') {
+ yyt3 = YYCURSOR;
+ goto yy242;
+ }
+ goto yy83;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '>') {
+ if (yych <= '.') {
+ yyt3 = YYCURSOR;
+ goto yy244;
+ }
+ if (yych <= '9') goto yy83;
+ if (yych <= ';') {
+ yyt3 = YYCURSOR;
+ goto yy241;
+ }
+ goto yy83;
+ } else {
+ if (yych <= '[') {
+ if (yych <= '?') {
+ yyt3 = YYCURSOR;
+ goto yy242;
+ }
+ goto yy83;
+ } else {
+ if (yych <= '\\') goto yy86;
+ if (yych <= '`') goto yy83;
+ goto yy150;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy83;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy87;
+ goto yy88;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy89;
+ goto yy90;
+ } else {
+ if (yych <= 0xF3) goto yy91;
+ if (yych <= 0xF4) goto yy92;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy151:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '+') {
+ if (yych <= ' ') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ } else {
+ if (yych == '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= '!') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '&') goto yy4;
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy80;
+ }
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= '/') {
+ if (yych <= ',') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '-') goto yy245;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '9') goto yy115;
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy229;
+ goto yy4;
+ } else {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy233;
+ goto yy4;
+ }
+ }
+ }
+yy152:
+ yych = *++YYCURSOR;
+ if (yybm[1024+yych] & 8) {
+ goto yy3;
+ }
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy56;
+ if (yych <= 0xE0) goto yy57;
+ goto yy58;
+ } else {
+ if (yych <= 0xF0) goto yy59;
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy1;
+ }
+yy153:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy246;
+ goto yy1;
+yy154:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy247;
+ goto yy1;
+yy155:
+ yych = *++YYCURSOR;
+yy156:
+ if (yybm[512+yych] & 1) {
+ goto yy155;
+ }
+ if (yych == '\n') goto yy248;
+ goto yy1;
+yy157:
+ ++YYCURSOR;
+#line 212 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_CSI);
+ }
+#line 4483 "../../lnav/src/data_scanner_re.cc"
+yy158:
+ yych = *++YYCURSOR;
+yy159:
+ if (yybm[512+yych] & 2) {
+ goto yy158;
+ }
+ if (yych <= ',') goto yy1;
+ if (yych <= '.') goto yy249;
+ goto yy1;
+yy160:
+ ++YYCURSOR;
+ yyt1 = yyt2;
+yy161:
+ YYCURSOR = yyt1;
+#line 158 "../../lnav/src/data_scanner_re.re"
+ {
+ CAPTURE(DT_QUOTED_STRING);
+ switch (this->ds_input[cap_inner.c_begin]) {
+ case 'u':
+ case 'r':
+ cap_inner.c_begin += 1;
+ break;
+ }
+ cap_inner.c_begin += 1;
+ cap_inner.c_end -= 1;
+ return tokenize_result{token_out, cap_all, cap_inner, this->ds_input.data()};
+ }
+#line 4511 "../../lnav/src/data_scanner_re.cc"
+yy162:
+ yyaccept = 14;
+ yych = *(YYMARKER = ++YYCURSOR);
+ yyt1 = yyt2;
+ if (yybm[768+yych] & 2) {
+ goto yy83;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '\'') {
+ if (yych <= 0x1B) goto yy161;
+ goto yy85;
+ } else {
+ if (yych <= '\\') goto yy86;
+ if (yych <= 0xC1) goto yy161;
+ goto yy87;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy88;
+ if (yych <= 0xEF) goto yy89;
+ goto yy90;
+ } else {
+ if (yych <= 0xF3) goto yy91;
+ if (yych <= 0xF4) goto yy92;
+ goto yy161;
+ }
+ }
+yy163:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy160;
+ goto yy1;
+yy164:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy163;
+ goto yy1;
+yy165:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy163;
+ goto yy1;
+yy166:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy165;
+ goto yy1;
+yy167:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy165;
+ goto yy1;
+yy168:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy165;
+ goto yy1;
+yy169:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy250;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+yy170:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '?') {
+ if (yych <= '$') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy109;
+ goto yy3;
+ } else {
+ if (yych <= '\n') goto yy109;
+ if (yych <= '\f') goto yy3;
+ goto yy109;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy3;
+ goto yy109;
+ } else {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= '#') goto yy109;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '*') {
+ if (yych <= '%') goto yy105;
+ goto yy109;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy109;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '7') {
+ if (yych <= '.') goto yy169;
+ if (yych <= '/') goto yy3;
+ goto yy170;
+ } else {
+ if (yych <= '9') goto yy95;
+ if (yych <= ':') goto yy62;
+ goto yy109;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'f') {
+ if (yych <= 'Z') {
+ if (yych <= 'D') {
+ if (yych <= '@') goto yy96;
+ } else {
+ if (yych <= 'E') goto yy172;
+ if (yych >= 'G') goto yy46;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych == '_') goto yy46;
+ goto yy109;
+ } else {
+ if (yych == 'e') goto yy172;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '~') {
+ if (yych <= 'z') goto yy46;
+ goto yy109;
+ } else {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy109;
+ goto yy56;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy57;
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy171:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[512+yych] & 8) {
+ goto yy171;
+ }
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy113;
+ goto yy3;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy3;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy3;
+ goto yy62;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy113;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy113;
+ } else {
+ if (yych == '`') goto yy113;
+ if (yych <= 'z') goto yy46;
+ goto yy113;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy172:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[512+yych] & 8) {
+ goto yy171;
+ }
+ if (yych <= ',') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ goto yy113;
+ } else {
+ if (yych == '\r') goto yy113;
+ if (yych <= 0x1A) goto yy4;
+ goto yy113;
+ }
+ } else {
+ if (yych <= '$') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '%') goto yy80;
+ if (yych == '+') goto yy194;
+ goto yy113;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '-') goto yy251;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy113;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy113;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy113;
+ if (yych <= 'z') goto yy46;
+ goto yy113;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+yy173:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[512+yych] & 8) {
+ goto yy171;
+ }
+ if (yych <= '?') {
+ if (yych <= '*') {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+yy174:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy5;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= '\r') goto yy5;
+ if (yych == 0x1B) goto yy5;
+ goto yy3;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '$') goto yy3;
+ if (yych <= ',') goto yy5;
+ goto yy174;
+ } else {
+ if (yych <= '.') goto yy252;
+ if (yych <= '/') goto yy3;
+ if (yych <= '9') goto yy174;
+ goto yy62;
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy5;
+ if (yych <= '@') goto yy3;
+ if (yych <= 'Z') goto yy174;
+ goto yy5;
+ } else {
+ if (yych <= '_') goto yy3;
+ if (yych <= '`') goto yy5;
+ if (yych <= 'z') goto yy174;
+ goto yy5;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+ }
+yy175:
+ yych = *++YYCURSOR;
+ if (yych == 'r') goto yy253;
+ goto yy100;
+yy176:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy24;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy24;
+ if (yych <= 0x1F) goto yy4;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '$') {
+ if (yych == '"') goto yy24;
+ if (yych <= '#') goto yy99;
+ goto yy101;
+ } else {
+ if (yych <= '\'') {
+ if (yych <= '&') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '*') goto yy99;
+ if (yych <= ',') goto yy24;
+ goto yy101;
+ }
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '[') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy24;
+ if (yych <= 'Z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= ']') {
+ if (yych <= '\\') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '^') goto yy99;
+ if (yych <= '_') goto yy101;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '}') {
+ if (yych == 'o') goto yy254;
+ if (yych <= 'z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy99;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy24;
+ if (yych <= 0xF4) goto yy4;
+ goto yy24;
+ }
+ }
+ }
+ }
+yy177:
+ ++YYCURSOR;
+ goto yy106;
+yy178:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '-') goto yy255;
+ if (yych <= '.') goto yy256;
+ goto yy4;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy257;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'E') {
+ if (yych <= 'D') goto yy258;
+ goto yy259;
+ } else {
+ if (yych <= 'Z') goto yy258;
+ if (yych <= '^') goto yy26;
+ goto yy258;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') goto yy26;
+ if (yych == 'e') goto yy259;
+ goto yy258;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy179:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '-') goto yy255;
+ if (yych <= '.') goto yy256;
+ goto yy4;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy178;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'E') {
+ if (yych <= 'D') goto yy258;
+ goto yy259;
+ } else {
+ if (yych <= 'Z') goto yy258;
+ if (yych <= '^') goto yy26;
+ goto yy258;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') goto yy26;
+ if (yych == 'e') goto yy259;
+ goto yy258;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy180:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '4') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy255;
+ } else {
+ if (yych <= '.') goto yy256;
+ if (yych <= '/') goto yy4;
+ goto yy178;
+ }
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '5') goto yy260;
+ if (yych <= '9') goto yy257;
+ if (yych <= ':') goto yy4;
+ goto yy26;
+ } else {
+ if (yych <= 'D') {
+ if (yych <= '@') goto yy96;
+ goto yy258;
+ } else {
+ if (yych <= 'E') goto yy259;
+ if (yych <= 'Z') goto yy258;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy258;
+ goto yy26;
+ } else {
+ if (yych == 'e') goto yy259;
+ goto yy258;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy181:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy5;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= 0x1B) goto yy5;
+ if (yych <= 0x1F) goto yy3;
+ goto yy5;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ if (yych <= '*') goto yy5;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy5;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy3;
+ if (yych <= '9') goto yy261;
+ goto yy62;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'Z') {
+ if (yych <= '?') goto yy5;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy261;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy5;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy5;
+ if (yych <= 'f') goto yy261;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+ }
+yy182:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '1') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '0') goto yy262;
+ goto yy263;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') {
+ if (yych <= '2') goto yy264;
+ goto yy262;
+ } else {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy183:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') goto yy4;
+ if (yych <= 'Z') goto yy265;
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy265;
+ goto yy4;
+yy184:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy267;
+ goto yy268;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy147;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy269;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy270;
+ goto yy269;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy269;
+ } else {
+ if (yych <= 'e') goto yy270;
+ if (yych <= 'f') goto yy269;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy185:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy268;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy147;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy269;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy270;
+ goto yy269;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy269;
+ } else {
+ if (yych <= 'e') goto yy270;
+ if (yych <= 'f') goto yy269;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy186:
+ yych = *++YYCURSOR;
+ if (yych <= ':') {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy189;
+ if (yych <= '9') goto yy271;
+ goto yy191;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy1;
+ goto yy272;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy272;
+ goto yy1;
+ }
+ }
+yy187:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy113;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy269;
+ if (yych <= ':') goto yy147;
+ if (yych <= '?') goto yy113;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy269;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy113;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy269;
+ if (yych <= 'z') goto yy115;
+ goto yy113;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+yy188:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy194;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy251;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy269;
+ if (yych <= ':') goto yy147;
+ goto yy113;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy269;
+ if (yych <= 'Z') goto yy115;
+ goto yy113;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy269;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy189:
+ yych = *++YYCURSOR;
+ if (yych <= '<') {
+ if (yych <= ',') {
+ if (yych <= '$') goto yy274;
+ if (yych <= '&') goto yy273;
+ goto yy274;
+ } else {
+ if (yych == '.') goto yy274;
+ if (yych <= '9') goto yy273;
+ goto yy274;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych == '>') goto yy274;
+ if (yych <= '?') goto yy273;
+ goto yy274;
+ } else {
+ if (yych <= 'Z') goto yy273;
+ if (yych <= '`') goto yy274;
+ if (yych <= 'z') goto yy273;
+ goto yy274;
+ }
+ }
+yy190:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy281;
+ if (yych <= ':') goto yy282;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy283;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy283;
+ goto yy1;
+ }
+yy191:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '0') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ goto yy285;
+ } else {
+ if (yych <= '2') {
+ if (yych <= '1') goto yy286;
+ goto yy287;
+ } else {
+ if (yych <= '9') goto yy285;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy288;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy288;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy1;
+ }
+ }
+ }
+ }
+yy192:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy283;
+ if (yych <= ':') goto yy282;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy283;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy283;
+ goto yy1;
+ }
+yy193:
+ yych = *++YYCURSOR;
+ if (yych <= ':') {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy189;
+ if (yych <= '9') goto yy272;
+ goto yy191;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy1;
+ goto yy272;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy272;
+ goto yy1;
+ }
+ }
+yy194:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy81;
+ if (yych <= '9') goto yy295;
+ goto yy81;
+yy195:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy296;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy261;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy261;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy196:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy113;
+ goto yy3;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy113;
+ goto yy3;
+ } else {
+ if (yych <= 0x1B) goto yy113;
+ if (yych <= 0x1F) goto yy3;
+ goto yy113;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ if (yych <= '*') goto yy113;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy113;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy3;
+ if (yych <= '9') goto yy196;
+ goto yy148;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'Z') {
+ if (yych <= '?') goto yy113;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy196;
+ goto yy115;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy113;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy3;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy197:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy182;
+ if (yych <= '/') goto yy4;
+ goto yy268;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy147;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy269;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy270;
+ goto yy269;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy269;
+ } else {
+ if (yych <= 'e') goto yy270;
+ if (yych <= 'f') goto yy269;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy198:
+ ++YYCURSOR;
+yy199:
+ YYCURSOR = yyt2;
+#line 190 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_IPV6_ADDRESS); }
+#line 5991 "../../lnav/src/data_scanner_re.cc"
+yy200:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy298;
+ goto yy299;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy300;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy300;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy201:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy301;
+ goto yy299;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy300;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy300;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy202:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '5') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '4') goto yy301;
+ goto yy302;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy298;
+ if (yych <= ':') goto yy299;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy300;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy300;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy203:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy300;
+ if (yych <= ':') goto yy299;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy300;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy300;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy204:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= 'E') {
+ if (yych <= '9') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy300;
+ } else {
+ if (yych <= ':') goto yy299;
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy300;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy303;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'e') goto yy300;
+ goto yy303;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych >= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy205:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy198;
+ goto yy1;
+yy206:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy205;
+ goto yy1;
+yy207:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy205;
+ goto yy1;
+yy208:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy207;
+ goto yy1;
+yy209:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy207;
+ goto yy1;
+yy210:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy207;
+ goto yy1;
+yy211:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy305;
+ goto yy308;
+ } else {
+ if (yych <= '/') goto yy305;
+ if (yych <= ':') goto yy308;
+ goto yy305;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy308;
+ if (yych <= '^') goto yy305;
+ goto yy308;
+ } else {
+ if (yych <= '`') goto yy305;
+ if (yych <= 'z') goto yy308;
+ goto yy305;
+ }
+ }
+yy212:
+ yych = *++YYCURSOR;
+yy213:
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy212;
+ } else {
+ if (yych == '\r') goto yy212;
+ goto yy1;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= ' ') goto yy212;
+ goto yy1;
+ } else {
+ if (yych <= '-') goto yy311;
+ if (yych <= '.') goto yy1;
+ goto yy215;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '=') {
+ if (yych <= ':') goto yy311;
+ goto yy1;
+ } else {
+ if (yych <= '>') goto yy216;
+ if (yych <= '?') goto yy215;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy311;
+ if (yych <= '^') goto yy1;
+ goto yy311;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy311;
+ goto yy1;
+ }
+ }
+ }
+yy214:
+ yych = *++YYCURSOR;
+ if (yybm[512+yych] & 16) {
+ goto yy214;
+ }
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy312;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy312;
+ if (yych <= 0x1F) goto yy1;
+ goto yy312;
+ }
+ } else {
+ if (yych <= '<') {
+ if (yych != '/') goto yy1;
+ } else {
+ if (yych <= '=') goto yy313;
+ if (yych <= '>') goto yy216;
+ if (yych >= '@') goto yy1;
+ }
+ }
+yy215:
+ yych = *++YYCURSOR;
+ if (yych == '>') goto yy314;
+ goto yy1;
+yy216:
+ ++YYCURSOR;
+yy217:
+#line 200 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_XML_OPEN_TAG);
+ }
+#line 6460 "../../lnav/src/data_scanner_re.cc"
+yy218:
+ yych = *++YYCURSOR;
+yy219:
+ if (yych <= '/') {
+ if (yych <= '\r') {
+ if (yych == '\t') goto yy316;
+ if (yych <= '\f') goto yy1;
+ goto yy316;
+ } else {
+ if (yych <= ' ') {
+ if (yych <= 0x1F) goto yy1;
+ goto yy316;
+ } else {
+ if (yych == '-') goto yy218;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '=') {
+ if (yych <= ':') goto yy218;
+ goto yy1;
+ } else {
+ if (yych <= '>') goto yy317;
+ if (yych <= '@') goto yy1;
+ goto yy218;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy1;
+ goto yy218;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy218;
+ goto yy1;
+ }
+ }
+ }
+yy220:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy319;
+ goto yy320;
+ } else {
+ if (yych <= '/') goto yy319;
+ if (yych <= ':') goto yy320;
+ goto yy319;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy320;
+ if (yych <= '^') goto yy319;
+ goto yy320;
+ } else {
+ if (yych <= '`') goto yy319;
+ if (yych <= 'z') goto yy320;
+ goto yy319;
+ }
+ }
+yy221:
+ ++YYCURSOR;
+ yyt3 = yyt4;
+yy222:
+ YYCURSOR = yyt3;
+#line 264 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_WORD); }
+#line 6528 "../../lnav/src/data_scanner_re.cc"
+yy223:
+ yych = *++YYCURSOR;
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy221;
+ yyt3 = yyt4;
+ goto yy222;
+ } else {
+ if (yych <= '\r') goto yy221;
+ if (yych == ' ') goto yy221;
+ yyt3 = yyt4;
+ goto yy222;
+ }
+yy224:
+ yyaccept = 15;
+ yych = *(YYMARKER = ++YYCURSOR);
+yy225:
+ if (yych <= '\'') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt3 = yyt4;
+ goto yy222;
+ } else {
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '\f') {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ } else {
+ if (yych <= '!') {
+ if (yych <= 0x1F) {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ } else {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '&') {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ }
+ } else {
+ if (yych <= '9') {
+ if (yych <= ',') {
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ } else {
+ if (yych != '.') {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ yyt3 = yyt4;
+ yyt4 = YYCURSOR;
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ } else {
+ if (yych <= '`') {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ if (yych <= 'z') {
+ yyt3 = yyt4;
+ goto yy137;
+ }
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ }
+ }
+yy226:
+ yych = *++YYCURSOR;
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy221;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy221;
+ if (yych == ' ') goto yy221;
+ goto yy1;
+ }
+yy227:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy321;
+ goto yy147;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy321;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy321;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy228:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '*') {
+ if (yych <= ' ') {
+ if (yych <= 0x1F) goto yy4;
+ goto yy322;
+ } else {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy321;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ if (yych <= ':') goto yy147;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy321;
+ goto yy115;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych == '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy321;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy229:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 64) {
+ goto yy115;
+ }
+ if (yych <= ',') {
+ if (yych <= '$') {
+ if (yych == ' ') goto yy322;
+ goto yy4;
+ } else {
+ if (yych <= '%') goto yy80;
+ if (yych == '+') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ if (yych <= ':') goto yy148;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych == '_') goto yy46;
+ goto yy4;
+ }
+ }
+yy230:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[1024+yych] & 16) {
+ goto yy46;
+ }
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy221;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy221;
+ if (yych <= 0x1F) goto yy4;
+ goto yy221;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych == '@') goto yy96;
+ goto yy4;
+ }
+ }
+yy231:
+ yyaccept = 16;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '.') {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ if (yych <= '/') goto yy189;
+ if (yych <= '9') goto yy272;
+ goto yy191;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ goto yy272;
+ } else {
+ if (yych <= '`') {
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ if (yych <= 'f') goto yy272;
+ yyt3 = yyt4;
+ goto yy222;
+ }
+ }
+yy232:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt3 = YYCURSOR;
+ goto yy323;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy321;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy324;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy321;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy325;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy233:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '+') {
+ if (yych <= ' ') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ } else {
+ if (yych == '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ yyt3 = YYCURSOR;
+ goto yy323;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= '!') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '&') goto yy4;
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy80;
+ }
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= '/') {
+ if (yych <= ',') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '9') goto yy115;
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ } else {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+yy234:
+ yyaccept = 15;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '/') goto yy189;
+ if (yych == ':') goto yy152;
+ goto yy222;
+yy235:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '*') {
+ if (yych <= ' ') {
+ if (yych <= 0x1F) goto yy4;
+ goto yy322;
+ } else {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ }
+ }
+ } else {
+ if (yych <= 'S') {
+ if (yych <= '?') {
+ if (yych <= ':') goto yy148;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'R') goto yy115;
+ goto yy327;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy236:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt3 = YYCURSOR;
+ goto yy323;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= 'r') {
+ if (yych <= '`') goto yy4;
+ goto yy326;
+ } else {
+ if (yych <= 's') goto yy328;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy237:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '*') {
+ if (yych <= ' ') {
+ if (yych <= 0x1F) goto yy4;
+ goto yy322;
+ } else {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ }
+ }
+ } else {
+ if (yych <= 'L') {
+ if (yych <= '?') {
+ if (yych <= ':') goto yy148;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'K') goto yy115;
+ goto yy329;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy238:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt3 = YYCURSOR;
+ goto yy323;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy4;
+ goto yy326;
+ } else {
+ if (yych <= 'e') goto yy330;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy239:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '*') {
+ if (yych <= ' ') {
+ if (yych <= 0x1F) goto yy4;
+ goto yy322;
+ } else {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ }
+ }
+ } else {
+ if (yych <= 'E') {
+ if (yych <= '?') {
+ if (yych <= ':') goto yy148;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'D') goto yy115;
+ goto yy329;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy240:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt3 = YYCURSOR;
+ goto yy323;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= 'k') {
+ if (yych <= '`') goto yy4;
+ goto yy326;
+ } else {
+ if (yych <= 'l') goto yy330;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy241:
+ yyaccept = 15;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0x1B) {
+ if (yych <= 0x00) goto yy222;
+ if (yych <= 0x1A) goto yy84;
+ goto yy222;
+ } else {
+ if (yych <= 0x7F) goto yy84;
+ if (yych <= 0xC1) goto yy222;
+ if (yych <= 0xF4) goto yy84;
+ goto yy222;
+ }
+yy242:
+ yyaccept = 15;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0x1A) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy222;
+ if (yych <= 0x08) goto yy84;
+ goto yy241;
+ } else {
+ if (yych == '\r') goto yy241;
+ goto yy84;
+ }
+ } else {
+ if (yych <= ' ') {
+ if (yych <= 0x1B) goto yy222;
+ if (yych <= 0x1F) goto yy84;
+ goto yy241;
+ } else {
+ if (yych <= 0x7F) goto yy84;
+ if (yych <= 0xC1) goto yy222;
+ if (yych <= 0xF4) goto yy84;
+ goto yy222;
+ }
+ }
+yy243:
+ yyaccept = 15;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 'S') {
+ if (yych <= ',') {
+ if (yych <= '&') {
+ if (yych == '!') {
+ yyt1 = yyt4 = YYCURSOR;
+ goto yy331;
+ }
+ yyt2 = YYCURSOR;
+ goto yy160;
+ } else {
+ if (yych <= '\'') {
+ yyt1 = YYCURSOR;
+ goto yy332;
+ }
+ if (yych <= '+') {
+ yyt2 = YYCURSOR;
+ goto yy160;
+ }
+ yyt1 = yyt4 = YYCURSOR;
+ goto yy331;
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych == '.') {
+ yyt1 = yyt4 = YYCURSOR;
+ goto yy331;
+ }
+ yyt2 = YYCURSOR;
+ goto yy160;
+ } else {
+ if (yych <= '?') {
+ yyt1 = yyt4 = YYCURSOR;
+ goto yy331;
+ }
+ if (yych <= 'R') {
+ yyt2 = YYCURSOR;
+ goto yy160;
+ }
+ goto yy222;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= 's') {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy160;
+ }
+ if (yych <= 'r') {
+ yyt1 = YYCURSOR;
+ goto yy333;
+ }
+ goto yy137;
+ } else {
+ if (yych <= 'z') {
+ yyt1 = YYCURSOR;
+ goto yy333;
+ }
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy160;
+ }
+ goto yy222;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy163;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy164;
+ }
+ yyt2 = YYCURSOR;
+ goto yy165;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy166;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy167;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy168;
+ }
+ goto yy222;
+ }
+ }
+ }
+yy244:
+ yych = *++YYCURSOR;
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy241;
+ goto yy84;
+ } else {
+ if (yych <= '\r') goto yy241;
+ if (yych == ' ') goto yy241;
+ goto yy84;
+ }
+yy245:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych == '/') goto yy4;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy334;
+ goto yy4;
+ } else {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy334;
+ goto yy4;
+ }
+ }
+yy246:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy335;
+ goto yy1;
+yy247:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy336;
+ goto yy1;
+yy248:
+ ++YYCURSOR;
+#line 208 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_H1);
+ }
+#line 7589 "../../lnav/src/data_scanner_re.cc"
+yy249:
+ yych = *++YYCURSOR;
+ if (yybm[512+yych] & 64) {
+ goto yy249;
+ }
+ if (yych <= '9') {
+ if (yych == '-') goto yy158;
+ if (yych <= '/') goto yy1;
+ goto yy158;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '@') goto yy1;
+ goto yy337;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy337;
+ goto yy1;
+ }
+ }
+yy250:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy3;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy3;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') goto yy46;
+ goto yy3;
+ } else {
+ if (yych <= '9') goto yy250;
+ if (yych <= ':') goto yy62;
+ goto yy26;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '^') {
+ if (yych <= 'D') {
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= 'E') goto yy339;
+ if (yych <= 'Z') goto yy46;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy26;
+ } else {
+ if (yych == 'e') goto yy339;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy251:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy340;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+yy252:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy5;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= '\r') goto yy5;
+ if (yych == 0x1B) goto yy5;
+ goto yy3;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '$') goto yy3;
+ if (yych <= ',') goto yy5;
+ goto yy174;
+ } else {
+ if (yych <= '.') goto yy252;
+ if (yych <= '/') goto yy3;
+ if (yych <= '9') goto yy174;
+ goto yy62;
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy5;
+ if (yych <= '@') goto yy3;
+ if (yych <= 'Z') goto yy341;
+ goto yy5;
+ } else {
+ if (yych <= '_') goto yy3;
+ if (yych <= '`') goto yy5;
+ if (yych <= 'z') goto yy341;
+ goto yy5;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+ }
+yy253:
+ yych = *++YYCURSOR;
+ if (yych == 'o') goto yy342;
+ goto yy100;
+yy254:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy24;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy24;
+ if (yych <= 0x1F) goto yy4;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '$') {
+ if (yych == '"') goto yy24;
+ if (yych <= '#') goto yy99;
+ goto yy101;
+ } else {
+ if (yych <= '\'') {
+ if (yych <= '&') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '*') goto yy99;
+ if (yych <= ',') goto yy24;
+ goto yy101;
+ }
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '[') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy24;
+ if (yych <= 'Z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= ']') {
+ if (yych <= '\\') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '^') goto yy99;
+ if (yych <= '_') goto yy101;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '}') {
+ if (yych == 'g') goto yy343;
+ if (yych <= 'z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy99;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy24;
+ if (yych <= 0xF4) goto yy4;
+ goto yy24;
+ }
+ }
+ }
+ }
+yy255:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy344;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy344;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy344;
+ goto yy4;
+ }
+ }
+yy256:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '1') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '0') goto yy346;
+ goto yy347;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') {
+ if (yych <= '2') goto yy348;
+ goto yy346;
+ } else {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy257:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '-') goto yy255;
+ if (yych <= '.') goto yy256;
+ goto yy4;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy349;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'E') {
+ if (yych >= 'E') goto yy259;
+ } else {
+ if (yych <= 'Z') goto yy258;
+ if (yych <= '^') goto yy26;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') goto yy26;
+ if (yych == 'e') goto yy259;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy258:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[512+yych] & 128) {
+ goto yy258;
+ }
+ if (yych <= '-') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy5;
+ if (yych <= 0x08) goto yy3;
+ goto yy5;
+ } else {
+ if (yych == '\r') goto yy5;
+ if (yych <= 0x1A) goto yy3;
+ goto yy5;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= '#') goto yy5;
+ if (yych <= '$') goto yy3;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy5;
+ goto yy255;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= ':') {
+ if (yych <= '.') goto yy350;
+ if (yych <= '/') goto yy3;
+ goto yy62;
+ } else {
+ if (yych == '@') goto yy96;
+ if (yych <= '~') goto yy5;
+ goto yy3;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) goto yy56;
+ if (yych <= 0xE0) goto yy57;
+ goto yy58;
+ } else {
+ if (yych <= 0xF0) goto yy59;
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+yy259:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[512+yych] & 128) {
+ goto yy258;
+ }
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy351;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy352;
+ goto yy350;
+ } else {
+ if (yych == '@') goto yy96;
+ goto yy4;
+ }
+ }
+yy260:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '5') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy255;
+ } else {
+ if (yych <= '.') goto yy256;
+ if (yych <= '/') goto yy4;
+ goto yy257;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy349;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'E') {
+ if (yych <= 'D') goto yy258;
+ goto yy259;
+ } else {
+ if (yych <= 'Z') goto yy258;
+ if (yych <= '^') goto yy26;
+ goto yy258;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') goto yy26;
+ if (yych == 'e') goto yy259;
+ goto yy258;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy261:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy5;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= 0x1B) goto yy5;
+ if (yych <= 0x1F) goto yy3;
+ goto yy5;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ if (yych <= '*') goto yy5;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy5;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy3;
+ if (yych <= '9') goto yy353;
+ goto yy62;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'Z') {
+ if (yych <= '?') goto yy5;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy353;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy5;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy5;
+ if (yych <= 'f') goto yy353;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+ }
+yy262:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '-') goto yy255;
+ if (yych <= '.') goto yy256;
+ goto yy4;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy355;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'E') {
+ if (yych <= 'D') goto yy258;
+ goto yy356;
+ } else {
+ if (yych <= 'Z') goto yy258;
+ if (yych <= '^') goto yy26;
+ goto yy258;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') goto yy26;
+ if (yych == 'e') goto yy356;
+ goto yy258;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy263:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '-') goto yy255;
+ if (yych <= '.') goto yy256;
+ goto yy4;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy262;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'E') {
+ if (yych <= 'D') goto yy258;
+ goto yy356;
+ } else {
+ if (yych <= 'Z') goto yy258;
+ if (yych <= '^') goto yy26;
+ goto yy258;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') goto yy26;
+ if (yych == 'e') goto yy356;
+ goto yy258;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy264:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '4') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy255;
+ } else {
+ if (yych <= '.') goto yy256;
+ if (yych <= '/') goto yy4;
+ goto yy262;
+ }
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '5') goto yy357;
+ if (yych <= '9') goto yy355;
+ if (yych <= ':') goto yy4;
+ goto yy26;
+ } else {
+ if (yych <= 'D') {
+ if (yych <= '@') goto yy96;
+ goto yy258;
+ } else {
+ if (yych <= 'E') goto yy356;
+ if (yych <= 'Z') goto yy258;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy258;
+ goto yy26;
+ } else {
+ if (yych == 'e') goto yy356;
+ goto yy258;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy265:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') goto yy4;
+ if (yych <= 'Z') goto yy358;
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy358;
+ goto yy4;
+yy266:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy359;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+yy267:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '7') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy360;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') {
+ if (yych <= '#') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy109;
+ goto yy80;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy109;
+ goto yy361;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy362;
+ goto yy363;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') {
+ if (yych <= '9') goto yy364;
+ goto yy365;
+ } else {
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy366;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy367;
+ goto yy366;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy366;
+ } else {
+ if (yych <= 'e') goto yy367;
+ if (yych <= 'f') goto yy366;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy268:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy360;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') {
+ if (yych <= '#') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy361;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy362;
+ goto yy364;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy365;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy366;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy367;
+ goto yy366;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy366;
+ } else {
+ if (yych <= 'e') goto yy367;
+ if (yych <= 'f') goto yy366;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy269:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy113;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy366;
+ if (yych <= ':') goto yy365;
+ if (yych <= '?') goto yy113;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy366;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy113;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy366;
+ if (yych <= 'z') goto yy115;
+ goto yy113;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+yy270:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy194;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy251;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy366;
+ if (yych <= ':') goto yy365;
+ goto yy113;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy366;
+ if (yych <= 'Z') goto yy115;
+ goto yy113;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy366;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy271:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy368;
+ if (yych <= ':') goto yy282;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy369;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy369;
+ goto yy1;
+ }
+yy272:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy369;
+ if (yych <= ':') goto yy282;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy369;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy369;
+ goto yy1;
+ }
+yy273:
+ yych = *++YYCURSOR;
+yy274:
+ if (yybm[256+yych] & 1) {
+ goto yy273;
+ }
+ if (yych <= 0xC1) {
+ if (yych <= ')') {
+ if (yych <= '"') goto yy1;
+ if (yych <= '&') goto yy370;
+ goto yy1;
+ } else {
+ if (yych <= 'Z') goto yy370;
+ if (yych <= ']') goto yy1;
+ if (yych <= 'z') goto yy370;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy275;
+ if (yych <= 0xE0) goto yy276;
+ goto yy277;
+ } else {
+ if (yych <= 0xF0) goto yy278;
+ if (yych <= 0xF3) goto yy279;
+ if (yych <= 0xF4) goto yy280;
+ goto yy1;
+ }
+ }
+yy275:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy273;
+ goto yy1;
+yy276:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy275;
+ goto yy1;
+yy277:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy275;
+ goto yy1;
+yy278:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy277;
+ goto yy1;
+yy279:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy277;
+ goto yy1;
+yy280:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy277;
+ goto yy1;
+yy281:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy372;
+ if (yych <= ':') goto yy373;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy372;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy372;
+ goto yy1;
+ }
+yy282:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy374;
+ if (yych <= ':') goto yy375;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy374;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy374;
+ goto yy1;
+ }
+yy283:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy372;
+ if (yych <= ':') goto yy282;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy372;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy372;
+ goto yy1;
+ }
+yy284:
+ yyaccept = 17;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy199;
+ goto yy4;
+ } else {
+ if (yych <= '\n') goto yy199;
+ if (yych <= '\f') goto yy4;
+ goto yy199;
+ }
+ } else {
+ if (yych <= 0x1F) {
+ if (yych == 0x1B) goto yy199;
+ goto yy4;
+ } else {
+ if (yych == '$') goto yy4;
+ goto yy199;
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy199;
+ goto yy4;
+ } else {
+ if (yych == '_') goto yy4;
+ goto yy199;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy4;
+ if (yych <= '~') goto yy199;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy199;
+ if (yych <= 0xF4) goto yy4;
+ goto yy199;
+ }
+ }
+ }
+yy285:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy377;
+ if (yych <= ':') goto yy378;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy379;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy379;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy286:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy380;
+ if (yych <= ':') goto yy378;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy379;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy379;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy287:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy376;
+ } else {
+ if (yych <= '4') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ goto yy380;
+ } else {
+ if (yych <= '5') goto yy381;
+ if (yych <= '9') goto yy377;
+ goto yy378;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy379;
+ goto yy3;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy379;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy288:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy379;
+ if (yych <= ':') goto yy378;
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '_') {
+ if (yych <= 'F') goto yy379;
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy379;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych >= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ yyt2 = YYCURSOR;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy289:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy284;
+ goto yy1;
+yy290:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy289;
+ goto yy1;
+yy291:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy289;
+ goto yy1;
+yy292:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy291;
+ goto yy1;
+yy293:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy291;
+ goto yy1;
+yy294:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy291;
+ goto yy1;
+yy295:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy26;
+ if (yych <= '.') goto yy80;
+ if (yych <= '/') goto yy26;
+ goto yy295;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy82;
+ if (yych <= 'Z') goto yy80;
+ goto yy26;
+ } else {
+ if (yych == '`') goto yy26;
+ if (yych <= 'z') goto yy80;
+ goto yy26;
+ }
+ }
+yy296:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy382;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy353;
+ if (yych <= 'Z') goto yy46;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy26;
+ if (yych <= 'f') goto yy353;
+ if (yych <= 'z') goto yy46;
+ goto yy26;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+yy297:
+ yyaccept = 18;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '1') {
+ if (yych <= '/') {
+ yyt2 = yyt1;
+ goto yy199;
+ }
+ if (yych <= '0') goto yy383;
+ goto yy384;
+ } else {
+ if (yych <= '2') goto yy385;
+ if (yych <= '9') goto yy383;
+ yyt2 = yyt1;
+ goto yy199;
+ }
+yy298:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy386;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy386;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy386;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy299:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy387;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy387;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy387;
+ goto yy1;
+ }
+yy300:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy386;
+ if (yych <= ':') goto yy299;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy386;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy386;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy301:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy388;
+ goto yy299;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy386;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy386;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy302:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '5') goto yy388;
+ goto yy386;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy299;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy386;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy386;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy303:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= 'E') {
+ if (yych <= '9') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy386;
+ } else {
+ if (yych <= ':') goto yy299;
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy386;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy389;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'e') goto yy386;
+ goto yy389;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy304:
+ yych = *++YYCURSOR;
+yy305:
+ if (yych <= ',') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy304;
+ } else {
+ if (yych == '\r') goto yy304;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy304;
+ if (yych <= '!') goto yy1;
+ } else {
+ if (yych == '\'') goto yy307;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') {
+ if (yych <= '-') goto yy390;
+ if (yych <= '/') goto yy1;
+ goto yy390;
+ } else {
+ if (yych == '>') goto yy309;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy390;
+ if (yych <= '^') goto yy1;
+ goto yy390;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy390;
+ goto yy1;
+ }
+ }
+ }
+yy306:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 8) {
+ goto yy391;
+ }
+ if (yych <= 0xE0) {
+ if (yych <= '\\') {
+ if (yych <= '"') goto yy1;
+ goto yy392;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy393;
+ goto yy394;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy395;
+ goto yy396;
+ } else {
+ if (yych <= 0xF3) goto yy397;
+ if (yych <= 0xF4) goto yy398;
+ goto yy1;
+ }
+ }
+yy307:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 16) {
+ goto yy399;
+ }
+ if (yych <= 0xE0) {
+ if (yych <= '\\') {
+ if (yych <= '\'') goto yy1;
+ goto yy400;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy401;
+ goto yy402;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy403;
+ goto yy404;
+ } else {
+ if (yych <= 0xF3) goto yy405;
+ if (yych <= 0xF4) goto yy406;
+ goto yy1;
+ }
+ }
+yy308:
+ yych = *++YYCURSOR;
+ if (yych <= '-') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy407;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy407;
+ if (yych <= 0x1F) goto yy1;
+ goto yy407;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych == '"') goto yy306;
+ goto yy1;
+ } else {
+ if (yych <= '\'') goto yy307;
+ if (yych <= ',') goto yy1;
+ goto yy308;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '<') {
+ if (yych <= '/') goto yy1;
+ if (yych <= ':') goto yy308;
+ goto yy1;
+ } else {
+ if (yych <= '=') goto yy408;
+ if (yych >= '?') goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy308;
+ if (yych <= '^') goto yy1;
+ goto yy308;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy308;
+ goto yy1;
+ }
+ }
+ }
+yy309:
+ ++YYCURSOR;
+yy310:
+#line 192 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_XML_DECL_TAG);
+ }
+#line 10017 "../../lnav/src/data_scanner_re.cc"
+yy311:
+ yych = *++YYCURSOR;
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy409;
+ } else {
+ if (yych == '\r') goto yy409;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= ' ') goto yy409;
+ if (yych <= ',') goto yy1;
+ goto yy311;
+ } else {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy215;
+ goto yy311;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '=') {
+ if (yych <= '<') goto yy1;
+ goto yy313;
+ } else {
+ if (yych <= '>') goto yy216;
+ if (yych <= '?') goto yy215;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy311;
+ if (yych <= '^') goto yy1;
+ goto yy311;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy311;
+ goto yy1;
+ }
+ }
+ }
+yy312:
+ yych = *++YYCURSOR;
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy312;
+ } else {
+ if (yych == '\r') goto yy312;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= ' ') goto yy312;
+ if (yych <= ',') goto yy1;
+ goto yy311;
+ } else {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy215;
+ goto yy311;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '=') {
+ if (yych <= '<') goto yy1;
+ } else {
+ if (yych <= '>') goto yy216;
+ if (yych <= '?') goto yy215;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy311;
+ if (yych <= '^') goto yy1;
+ goto yy311;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy311;
+ goto yy1;
+ }
+ }
+ }
+yy313:
+ yych = *++YYCURSOR;
+ if (yych <= '\'') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy1;
+ goto yy410;
+ } else {
+ if (yych <= '\t') goto yy411;
+ if (yych <= '\f') goto yy410;
+ goto yy411;
+ }
+ } else {
+ if (yych <= '!') {
+ if (yych == ' ') goto yy411;
+ goto yy410;
+ } else {
+ if (yych <= '"') goto yy412;
+ if (yych <= '&') goto yy410;
+ goto yy413;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '>') {
+ if (yych <= '=') goto yy410;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy410;
+ if (yych <= 0xC1) goto yy1;
+ goto yy414;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy415;
+ if (yych <= 0xEF) goto yy416;
+ goto yy417;
+ } else {
+ if (yych <= 0xF3) goto yy418;
+ if (yych <= 0xF4) goto yy419;
+ goto yy1;
+ }
+ }
+ }
+yy314:
+ ++YYCURSOR;
+yy315:
+#line 196 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_XML_EMPTY_TAG);
+ }
+#line 10156 "../../lnav/src/data_scanner_re.cc"
+yy316:
+ yych = *++YYCURSOR;
+ if (yych <= '\r') {
+ if (yych == '\t') goto yy316;
+ if (yych <= '\f') goto yy1;
+ goto yy316;
+ } else {
+ if (yych <= ' ') {
+ if (yych <= 0x1F) goto yy1;
+ goto yy316;
+ } else {
+ if (yych != '>') goto yy1;
+ }
+ }
+yy317:
+ ++YYCURSOR;
+#line 204 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_XML_CLOSE_TAG);
+ }
+#line 10177 "../../lnav/src/data_scanner_re.cc"
+yy318:
+ yych = *++YYCURSOR;
+yy319:
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy318;
+ } else {
+ if (yych == '\r') goto yy318;
+ goto yy1;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= ' ') goto yy318;
+ goto yy1;
+ } else {
+ if (yych <= '-') goto yy420;
+ if (yych <= '.') goto yy1;
+ goto yy215;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy420;
+ goto yy1;
+ } else {
+ if (yych <= '?') goto yy215;
+ if (yych <= '@') goto yy1;
+ goto yy420;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy1;
+ goto yy420;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy420;
+ goto yy1;
+ }
+ }
+ }
+yy320:
+ yych = *++YYCURSOR;
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy421;
+ } else {
+ if (yych == '\r') goto yy421;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= ' ') goto yy421;
+ if (yych <= ',') goto yy1;
+ goto yy320;
+ } else {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy215;
+ goto yy320;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '=') {
+ if (yych <= '<') goto yy1;
+ goto yy422;
+ } else {
+ if (yych == '?') goto yy215;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy320;
+ if (yych <= '^') goto yy1;
+ goto yy320;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy320;
+ goto yy1;
+ }
+ }
+ }
+yy321:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy423;
+ goto yy365;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy423;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy423;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy322:
+ yych = *++YYCURSOR;
+ if (yych == ' ') goto yy424;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy424;
+ goto yy1;
+yy323:
+ yyaccept = 15;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == ' ') goto yy424;
+ if (yych <= '/') goto yy222;
+ if (yych <= '9') goto yy424;
+ goto yy222;
+yy324:
+ yyaccept = 15;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '.') goto yy222;
+ if (yych <= '/') goto yy189;
+ if (yych <= '9') goto yy192;
+ goto yy191;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy222;
+ goto yy192;
+ } else {
+ if (yych <= '`') goto yy222;
+ if (yych <= 'f') goto yy192;
+ goto yy222;
+ }
+ }
+yy325:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy423;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy425;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy423;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy426;
+ if (yych >= '{') goto yy4;
+ }
+ }
+ }
+ }
+yy326:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= '!') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy3;
+ } else {
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '\f') goto yy3;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy3;
+ goto yy5;
+ } else {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '#') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy5;
+ } else {
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ goto yy5;
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy80;
+ } else {
+ if (yych <= ',') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '-') goto yy46;
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '>') {
+ if (yych <= '9') {
+ if (yych <= '/') goto yy3;
+ goto yy115;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy5;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '@') goto yy96;
+ goto yy115;
+ } else {
+ if (yych == '_') goto yy46;
+ goto yy5;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '~') {
+ if (yych <= 'z') goto yy326;
+ goto yy5;
+ } else {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy5;
+ goto yy56;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy57;
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+ }
+yy327:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy115;
+ goto yy148;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych == 'E') goto yy329;
+ goto yy115;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy328:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy115;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy4;
+ goto yy326;
+ } else {
+ if (yych <= 'e') goto yy330;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy329:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 64) {
+ goto yy115;
+ }
+ if (yych <= '*') {
+ if (yych <= 0x1F) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ if (yych <= 0x08) goto yy4;
+ yyt1 = YYCURSOR;
+ goto yy427;
+ } else {
+ if (yych == '\r') {
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ goto yy4;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych == '!') {
+ yyt1 = YYCURSOR;
+ goto yy429;
+ }
+ yyt1 = YYCURSOR;
+ goto yy427;
+ } else {
+ if (yych == '%') goto yy80;
+ if (yych <= '&') goto yy4;
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ }
+ } else {
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') {
+ yyt1 = YYCURSOR;
+ goto yy429;
+ }
+ goto yy46;
+ } else {
+ if (yych <= '.') {
+ yyt1 = YYCURSOR;
+ goto yy430;
+ }
+ if (yych <= '/') goto yy4;
+ yyt1 = YYCURSOR;
+ goto yy431;
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= ';') {
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ if (yych <= '>') goto yy4;
+ yyt1 = YYCURSOR;
+ goto yy429;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych == '_') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy330:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '+') {
+ if (yych <= ' ') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ if (yych <= 0x08) goto yy4;
+ yyt1 = YYCURSOR;
+ goto yy427;
+ } else {
+ if (yych == '\r') {
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ if (yych <= 0x1F) goto yy4;
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= '!') {
+ yyt1 = YYCURSOR;
+ goto yy429;
+ }
+ if (yych <= '"') {
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '&') goto yy4;
+ if (yych <= '\'') {
+ yyt1 = YYCURSOR;
+ goto yy432;
+ }
+ if (yych <= '*') {
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ goto yy80;
+ }
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= '/') {
+ if (yych <= ',') {
+ yyt1 = YYCURSOR;
+ goto yy429;
+ }
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt1 = YYCURSOR;
+ goto yy430;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '9') goto yy115;
+ if (yych <= ':') {
+ yyt1 = YYCURSOR;
+ goto yy431;
+ }
+ if (yych <= ';') {
+ yyt1 = YYCURSOR;
+ goto yy427;
+ }
+ goto yy4;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ yyt1 = YYCURSOR;
+ goto yy429;
+ }
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ } else {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+yy331:
+ yych = *++YYCURSOR;
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy221;
+ goto yy161;
+ } else {
+ if (yych <= '\r') goto yy221;
+ if (yych == ' ') goto yy221;
+ goto yy161;
+ }
+yy332:
+ yyaccept = 14;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '-') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy83;
+ if (yych <= '\n') {
+ yyt3 = YYCURSOR;
+ goto yy241;
+ }
+ goto yy83;
+ } else {
+ if (yych <= '\r') {
+ yyt3 = YYCURSOR;
+ goto yy241;
+ }
+ if (yych == 0x1B) goto yy161;
+ goto yy83;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych == '!') {
+ yyt3 = YYCURSOR;
+ goto yy242;
+ }
+ if (yych <= '"') {
+ yyt3 = YYCURSOR;
+ goto yy241;
+ }
+ goto yy83;
+ } else {
+ if (yych <= '*') {
+ if (yych <= '\'') {
+ yyt3 = YYCURSOR;
+ goto yy243;
+ }
+ yyt3 = YYCURSOR;
+ goto yy241;
+ } else {
+ if (yych == ',') {
+ yyt3 = YYCURSOR;
+ goto yy242;
+ }
+ goto yy83;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '>') {
+ if (yych <= '.') {
+ yyt3 = YYCURSOR;
+ goto yy244;
+ }
+ if (yych <= '9') goto yy83;
+ if (yych <= ';') {
+ yyt3 = YYCURSOR;
+ goto yy241;
+ }
+ goto yy83;
+ } else {
+ if (yych <= '[') {
+ if (yych <= '?') {
+ yyt3 = YYCURSOR;
+ goto yy242;
+ }
+ goto yy83;
+ } else {
+ if (yych <= '\\') goto yy86;
+ if (yych <= '`') goto yy83;
+ goto yy150;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy83;
+ if (yych <= 0xC1) goto yy161;
+ if (yych <= 0xDF) goto yy87;
+ goto yy88;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy89;
+ goto yy90;
+ } else {
+ if (yych <= 0xF3) goto yy91;
+ if (yych <= 0xF4) goto yy92;
+ goto yy161;
+ }
+ }
+ }
+ }
+yy333:
+ yyaccept = 14;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 128) {
+ goto yy137;
+ }
+ if (yych <= '*') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy225;
+ if (yych <= 0x08) goto yy161;
+ if (yych <= '\n') goto yy225;
+ goto yy161;
+ } else {
+ if (yych <= 0x1F) {
+ if (yych <= '\r') goto yy225;
+ goto yy161;
+ } else {
+ if (yych <= '"') goto yy225;
+ if (yych <= '&') goto yy161;
+ goto yy225;
+ }
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych == ',') goto yy225;
+ if (yych <= '-') goto yy161;
+ yyt4 = YYCURSOR;
+ goto yy226;
+ } else {
+ if (yych <= ';') {
+ if (yych <= '9') goto yy161;
+ goto yy225;
+ } else {
+ if (yych == '?') goto yy225;
+ goto yy161;
+ }
+ }
+ }
+yy334:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '\'') {
+ if (yych == '%') goto yy80;
+ if (yych <= '&') goto yy4;
+ goto yy137;
+ } else {
+ if (yych <= '+') {
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') goto yy46;
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy433;
+ goto yy4;
+ }
+ }
+ }
+yy335:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= ':') {
+ if (yych <= '9') {
+ yyt1 = YYCURSOR;
+ goto yy434;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ yyt1 = YYCURSOR;
+ goto yy434;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt1 = YYCURSOR;
+ goto yy436;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt1 = YYCURSOR;
+ goto yy437;
+ }
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy438;
+ }
+ yyt1 = YYCURSOR;
+ goto yy439;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy440;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy441;
+ }
+ goto yy1;
+ }
+ }
+yy336:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy442;
+ goto yy1;
+yy337:
+ yyaccept = 19;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[512+yych] & 64) {
+ goto yy249;
+ }
+ if (yych <= '9') {
+ if (yych == '-') goto yy158;
+ if (yych >= '0') goto yy158;
+ } else {
+ if (yych <= 'Z') {
+ if (yych >= 'A') goto yy337;
+ } else {
+ if (yych <= '`') goto yy338;
+ if (yych <= 'z') goto yy337;
+ }
+ }
+yy338:
+#line 260 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_EMAIL); }
+#line 11084 "../../lnav/src/data_scanner_re.cc"
+yy339:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy194;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '-') goto yy251;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') goto yy46;
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy340:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy3;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy3;
+ goto yy340;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy62;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy26;
+ if (yych <= 'z') goto yy46;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy341:
+ yyaccept = 19;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy338;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy338;
+ goto yy3;
+ } else {
+ if (yych <= '\r') goto yy338;
+ if (yych == 0x1B) goto yy338;
+ goto yy3;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '$') goto yy3;
+ if (yych <= ',') goto yy338;
+ goto yy174;
+ } else {
+ if (yych <= '.') goto yy252;
+ if (yych <= '/') goto yy3;
+ if (yych <= '9') goto yy174;
+ goto yy62;
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy338;
+ if (yych <= '@') goto yy3;
+ if (yych <= 'Z') goto yy341;
+ goto yy338;
+ } else {
+ if (yych <= '_') goto yy3;
+ if (yych <= '`') goto yy338;
+ if (yych <= 'z') goto yy341;
+ goto yy338;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy338;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy338;
+ }
+ }
+ }
+ }
+yy342:
+ yych = *++YYCURSOR;
+ if (yych == 'g') goto yy443;
+ goto yy100;
+yy343:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy24;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy24;
+ if (yych <= 0x1F) goto yy4;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '$') {
+ if (yych == '"') goto yy24;
+ if (yych <= '#') goto yy99;
+ goto yy101;
+ } else {
+ if (yych <= '\'') {
+ if (yych <= '&') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '*') goto yy99;
+ if (yych <= ',') goto yy24;
+ goto yy101;
+ }
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '[') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy24;
+ if (yych <= 'Z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= ']') {
+ if (yych <= '\\') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '^') goto yy99;
+ if (yych <= '_') goto yy101;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '}') {
+ if (yych == 'r') goto yy444;
+ if (yych <= 'z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy99;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy24;
+ if (yych <= 0xF4) goto yy4;
+ goto yy24;
+ }
+ }
+ }
+ }
+yy344:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy345;
+ if (yych <= 0x08) goto yy3;
+ if (yych >= '\v') goto yy3;
+ } else {
+ if (yych <= '\r') goto yy345;
+ if (yych != 0x1B) goto yy3;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy345;
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy3;
+ goto yy344;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy62;
+ if (yych <= '?') goto yy345;
+ if (yych <= '@') goto yy96;
+ goto yy344;
+ } else {
+ if (yych <= '_') {
+ if (yych >= '_') goto yy344;
+ } else {
+ if (yych <= '`') goto yy345;
+ if (yych <= 'z') goto yy344;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ }
+ }
+ }
+ }
+yy345:
+#line 251 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_VERSION_NUMBER);
+ }
+#line 11380 "../../lnav/src/data_scanner_re.cc"
+yy346:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '-') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy345;
+ if (yych <= 0x08) goto yy4;
+ goto yy345;
+ } else {
+ if (yych == '\r') goto yy345;
+ if (yych <= 0x1A) goto yy4;
+ goto yy345;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy345;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy345;
+ goto yy255;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '.') goto yy445;
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy446;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy345;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy447;
+ goto yy345;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy345;
+ if (yych <= 'z') goto yy447;
+ goto yy345;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xF4) goto yy4;
+ goto yy345;
+ }
+ }
+ }
+yy347:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '-') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy345;
+ if (yych <= 0x08) goto yy4;
+ goto yy345;
+ } else {
+ if (yych == '\r') goto yy345;
+ if (yych <= 0x1A) goto yy4;
+ goto yy345;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy345;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy345;
+ goto yy255;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '.') goto yy445;
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy346;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy345;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy447;
+ goto yy345;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy345;
+ if (yych <= 'z') goto yy447;
+ goto yy345;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xF4) goto yy4;
+ goto yy345;
+ }
+ }
+ }
+yy348:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy345;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy345;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy345;
+ if (yych == 0x1B) goto yy345;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy345;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy345;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy345;
+ if (yych <= '-') goto yy255;
+ goto yy445;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '4') goto yy346;
+ if (yych <= '5') goto yy448;
+ goto yy446;
+ } else {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy345;
+ if (yych <= '@') goto yy96;
+ goto yy447;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych == '_') goto yy447;
+ if (yych <= '`') goto yy345;
+ goto yy447;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy345;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xF4) goto yy4;
+ goto yy345;
+ }
+ }
+ }
+ }
+yy349:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '?') {
+ if (yych <= '$') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= '\n') goto yy26;
+ if (yych <= '\f') goto yy3;
+ goto yy26;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy3;
+ goto yy26;
+ } else {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= '#') goto yy26;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '*') {
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ goto yy255;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych >= '/') goto yy3;
+ } else {
+ if (yych <= '9') goto yy349;
+ if (yych <= ':') goto yy62;
+ goto yy26;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '^') {
+ if (yych <= 'D') {
+ if (yych <= '@') goto yy96;
+ goto yy258;
+ } else {
+ if (yych <= 'E') goto yy259;
+ if (yych <= 'Z') goto yy258;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy258;
+ goto yy26;
+ } else {
+ if (yych == 'e') goto yy259;
+ goto yy258;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy350:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy5;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy5;
+ goto yy3;
+ } else {
+ if (yych <= '\r') goto yy5;
+ if (yych == 0x1B) goto yy5;
+ goto yy3;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy5;
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ goto yy5;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy5;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy3;
+ goto yy447;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy62;
+ if (yych <= '?') goto yy5;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy5;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy5;
+ if (yych <= 'z') goto yy46;
+ goto yy5;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+ }
+yy351:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy81;
+ if (yych <= '9') goto yy449;
+ goto yy81;
+yy352:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy451;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy344;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy344;
+ goto yy4;
+ }
+ }
+yy353:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy354;
+ if (yych <= 0x08) goto yy3;
+ if (yych >= '\v') goto yy3;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych >= 0x0E) goto yy3;
+ } else {
+ if (yych <= 0x1B) goto yy354;
+ if (yych <= 0x1F) goto yy3;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ if (yych >= '+') goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych >= '-') goto yy181;
+ } else {
+ if (yych == '/') goto yy3;
+ goto yy46;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy452;
+ if (yych <= '?') goto yy354;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych >= '_') goto yy46;
+ } else {
+ if (yych <= '`') goto yy354;
+ if (yych <= 'z') goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy354;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ }
+ }
+ }
+ }
+yy354:
+#line 174 "../../lnav/src/data_scanner_re.re"
+ {
+ if ((YYCURSOR.val - (this->ds_input.udata() + this->ds_next_offset)) == 17) {
+ RET(DT_MAC_ADDRESS);
+ } else {
+ RET(DT_HEX_DUMP);
+ }
+ }
+#line 11802 "../../lnav/src/data_scanner_re.cc"
+yy355:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '-') goto yy255;
+ if (yych <= '.') goto yy256;
+ goto yy4;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy359;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'E') {
+ if (yych <= 'D') goto yy258;
+ } else {
+ if (yych <= 'Z') goto yy258;
+ if (yych <= '^') goto yy26;
+ goto yy258;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') goto yy26;
+ if (yych != 'e') goto yy258;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy356:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[512+yych] & 128) {
+ goto yy258;
+ }
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy194;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy255;
+ goto yy350;
+ } else {
+ if (yych == '@') goto yy96;
+ goto yy4;
+ }
+ }
+yy357:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '5') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy255;
+ } else {
+ if (yych <= '.') goto yy256;
+ if (yych <= '/') goto yy4;
+ goto yy355;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy359;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'E') {
+ if (yych <= 'D') goto yy258;
+ goto yy356;
+ } else {
+ if (yych <= 'Z') goto yy258;
+ if (yych <= '^') goto yy26;
+ goto yy258;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '`') goto yy26;
+ if (yych == 'e') goto yy356;
+ goto yy258;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy358:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') goto yy4;
+ if (yych <= 'Z') goto yy453;
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy453;
+ goto yy4;
+yy359:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '?') {
+ if (yych <= '$') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= '\n') goto yy26;
+ if (yych <= '\f') goto yy3;
+ goto yy26;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy3;
+ goto yy26;
+ } else {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= '#') goto yy26;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '*') {
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ goto yy255;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') goto yy350;
+ goto yy3;
+ } else {
+ if (yych <= '9') goto yy359;
+ if (yych <= ':') goto yy62;
+ goto yy26;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '^') {
+ if (yych <= 'D') {
+ if (yych <= '@') goto yy96;
+ goto yy258;
+ } else {
+ if (yych <= 'E') goto yy356;
+ if (yych <= 'Z') goto yy258;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy258;
+ goto yy26;
+ } else {
+ if (yych == 'e') goto yy356;
+ goto yy258;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy360:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy104;
+ if (yych <= '9') goto yy454;
+ goto yy104;
+yy361:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy455;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+yy362:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy456;
+ goto yy4;
+yy363:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy457;
+ goto yy458;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy459;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy460;
+ goto yy459;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy459;
+ } else {
+ if (yych <= 'e') goto yy460;
+ if (yych <= 'f') goto yy459;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy364:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy458;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy459;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy460;
+ goto yy459;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy459;
+ } else {
+ if (yych <= 'e') goto yy460;
+ if (yych <= 'f') goto yy459;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy365:
+ yych = *++YYCURSOR;
+ if (yych <= '9') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy1;
+ goto yy461;
+ } else {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy189;
+ goto yy192;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= ':') goto yy462;
+ if (yych <= '@') goto yy1;
+ goto yy192;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy192;
+ goto yy1;
+ }
+ }
+yy366:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy113;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy459;
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy113;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy459;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy113;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy459;
+ if (yych <= 'z') goto yy115;
+ goto yy113;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+yy367:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy194;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy251;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy459;
+ if (yych <= ':') goto yy148;
+ goto yy113;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy459;
+ if (yych <= 'Z') goto yy115;
+ goto yy113;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy459;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy368:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 'f') {
+ if (yych <= '9') {
+ if (yych == '-') {
+ yyt1 = YYCURSOR;
+ goto yy463;
+ }
+ if (yych <= '/') {
+ yyt1 = YYCURSOR;
+ goto yy434;
+ }
+ yyt1 = YYCURSOR;
+ goto yy464;
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy465;
+ yyt1 = YYCURSOR;
+ goto yy434;
+ } else {
+ if (yych <= 'F') {
+ yyt1 = YYCURSOR;
+ goto yy464;
+ }
+ if (yych <= '`') {
+ yyt1 = YYCURSOR;
+ goto yy434;
+ }
+ yyt1 = YYCURSOR;
+ goto yy464;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt1 = YYCURSOR;
+ goto yy434;
+ }
+ if (yych <= 0xC1) goto yy354;
+ if (yych <= 0xDF) {
+ yyt1 = YYCURSOR;
+ goto yy436;
+ }
+ yyt1 = YYCURSOR;
+ goto yy437;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy438;
+ }
+ yyt1 = YYCURSOR;
+ goto yy439;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy440;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy441;
+ }
+ goto yy354;
+ }
+ }
+ }
+yy369:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy354;
+ goto yy466;
+ } else {
+ if (yych <= '/') goto yy354;
+ if (yych <= '9') goto yy372;
+ goto yy467;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy354;
+ goto yy372;
+ } else {
+ if (yych <= '`') goto yy354;
+ if (yych <= 'f') goto yy372;
+ goto yy354;
+ }
+ }
+yy370:
+ yyaccept = 22;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[256+yych] & 1) {
+ goto yy273;
+ }
+ if (yych <= 0xC1) {
+ if (yych <= ')') {
+ if (yych <= '"') goto yy371;
+ if (yych <= '&') goto yy370;
+ } else {
+ if (yych <= 'Z') goto yy370;
+ if (yych <= ']') goto yy371;
+ if (yych <= 'z') goto yy370;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy275;
+ if (yych <= 0xE0) goto yy276;
+ goto yy277;
+ } else {
+ if (yych <= 0xF0) goto yy278;
+ if (yych <= 0xF3) goto yy279;
+ if (yych <= 0xF4) goto yy280;
+ }
+ }
+yy371:
+#line 170 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_URL); }
+#line 12517 "../../lnav/src/data_scanner_re.cc"
+yy372:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy468;
+ if (yych <= ':') goto yy282;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy468;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy468;
+ goto yy1;
+ }
+yy373:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy469;
+ if (yych <= ':') goto yy375;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy374;
+ if (yych <= '`') goto yy1;
+ if (yych >= 'g') goto yy1;
+ }
+yy374:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy470;
+ if (yych <= ':') goto yy471;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy470;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy470;
+ goto yy1;
+ }
+yy375:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '0') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy472;
+ } else {
+ if (yych <= '1') goto yy473;
+ if (yych <= '2') goto yy474;
+ goto yy472;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy475;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy475;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy376:
+ yyaccept = 17;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '1') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy199;
+ if (yych <= 0x08) goto yy4;
+ goto yy199;
+ } else {
+ if (yych == '\r') goto yy199;
+ if (yych <= 0x1A) goto yy4;
+ goto yy199;
+ }
+ } else {
+ if (yych <= '$') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy199;
+ goto yy4;
+ } else {
+ if (yych <= ',') goto yy199;
+ if (yych <= '/') goto yy4;
+ if (yych <= '0') goto yy476;
+ goto yy477;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= ':') {
+ if (yych <= '2') goto yy478;
+ if (yych <= '9') goto yy476;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy199;
+ if (yych <= 'Z') goto yy4;
+ if (yych <= '^') goto yy199;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy199;
+ if (yych <= 'z') goto yy4;
+ goto yy199;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy199;
+ if (yych <= 0xF4) goto yy4;
+ goto yy199;
+ }
+ }
+ }
+yy377:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy479;
+ if (yych >= ';') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy479;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy479;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy378:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy475;
+ if (yych <= ':') goto yy152;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy475;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy475;
+ goto yy1;
+ }
+yy379:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy479;
+ if (yych <= ':') goto yy378;
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '_') {
+ if (yych <= 'F') goto yy479;
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy479;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy380:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy480;
+ if (yych <= ':') goto yy378;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy479;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy479;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy381:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy376;
+ } else {
+ if (yych <= '5') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ goto yy480;
+ } else {
+ if (yych <= '9') goto yy479;
+ if (yych <= ':') goto yy378;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy479;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy479;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy382:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '-') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy354;
+ if (yych <= 0x08) goto yy4;
+ goto yy354;
+ } else {
+ if (yych == '\r') goto yy354;
+ if (yych <= 0x1A) goto yy4;
+ goto yy354;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy354;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy354;
+ goto yy181;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy340;
+ goto yy452;
+ } else {
+ if (yych <= '?') goto yy354;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy354;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy354;
+ if (yych <= 'z') goto yy46;
+ goto yy354;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy354;
+ if (yych <= 0xF4) goto yy4;
+ goto yy354;
+ }
+ }
+ }
+yy383:
+ yych = *++YYCURSOR;
+ if (yych == '.') goto yy481;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy482;
+ goto yy1;
+yy384:
+ yych = *++YYCURSOR;
+ if (yych == '.') goto yy481;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy383;
+ goto yy1;
+yy385:
+ yych = *++YYCURSOR;
+ if (yych <= '/') {
+ if (yych == '.') goto yy481;
+ goto yy1;
+ } else {
+ if (yych <= '4') goto yy383;
+ if (yych <= '5') goto yy483;
+ if (yych <= '9') goto yy482;
+ goto yy1;
+ }
+yy386:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy484;
+ if (yych <= ':') goto yy299;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy484;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy484;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy387:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy485;
+ if (yych <= ':') goto yy486;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy485;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy485;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy388:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy484;
+ goto yy299;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy484;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy484;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy389:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= 'E') {
+ if (yych <= '9') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy484;
+ } else {
+ if (yych <= ':') goto yy299;
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy484;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy487;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'e') goto yy484;
+ goto yy487;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy390:
+ yych = *++YYCURSOR;
+ if (yych <= '-') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy488;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy488;
+ if (yych <= 0x1F) goto yy1;
+ goto yy488;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych == '"') goto yy306;
+ goto yy1;
+ } else {
+ if (yych <= '\'') goto yy307;
+ if (yych <= ',') goto yy1;
+ goto yy390;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '<') {
+ if (yych <= '/') goto yy1;
+ if (yych <= ':') goto yy390;
+ goto yy1;
+ } else {
+ if (yych <= '=') goto yy408;
+ if (yych <= '>') goto yy309;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy390;
+ if (yych <= '^') goto yy1;
+ goto yy390;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy390;
+ goto yy1;
+ }
+ }
+ }
+yy391:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 8) {
+ goto yy391;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy1;
+ goto yy489;
+ } else {
+ if (yych <= '\\') goto yy392;
+ if (yych <= 0xC1) goto yy1;
+ goto yy393;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy394;
+ if (yych <= 0xEF) goto yy395;
+ goto yy396;
+ } else {
+ if (yych <= 0xF3) goto yy397;
+ if (yych <= 0xF4) goto yy398;
+ goto yy1;
+ }
+ }
+yy392:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy391;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy391;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy394;
+ if (yych <= 0xEF) goto yy395;
+ goto yy396;
+ } else {
+ if (yych <= 0xF3) goto yy397;
+ if (yych <= 0xF4) goto yy398;
+ goto yy1;
+ }
+ }
+yy393:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy391;
+ goto yy1;
+yy394:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy393;
+ goto yy1;
+yy395:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy393;
+ goto yy1;
+yy396:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy395;
+ goto yy1;
+yy397:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy395;
+ goto yy1;
+yy398:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy395;
+ goto yy1;
+yy399:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 16) {
+ goto yy399;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy1;
+ goto yy489;
+ } else {
+ if (yych <= '\\') goto yy400;
+ if (yych <= 0xC1) goto yy1;
+ goto yy401;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy402;
+ if (yych <= 0xEF) goto yy403;
+ goto yy404;
+ } else {
+ if (yych <= 0xF3) goto yy405;
+ if (yych <= 0xF4) goto yy406;
+ goto yy1;
+ }
+ }
+yy400:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy399;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy399;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy402;
+ if (yych <= 0xEF) goto yy403;
+ goto yy404;
+ } else {
+ if (yych <= 0xF3) goto yy405;
+ if (yych <= 0xF4) goto yy406;
+ goto yy1;
+ }
+ }
+yy401:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy399;
+ goto yy1;
+yy402:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy401;
+ goto yy1;
+yy403:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy401;
+ goto yy1;
+yy404:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy403;
+ goto yy1;
+yy405:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy403;
+ goto yy1;
+yy406:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy403;
+ goto yy1;
+yy407:
+ yych = *++YYCURSOR;
+ if (yych <= '-') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy407;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy407;
+ if (yych <= 0x1F) goto yy1;
+ goto yy407;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych == '"') goto yy306;
+ goto yy1;
+ } else {
+ if (yych <= '\'') goto yy307;
+ if (yych <= ',') goto yy1;
+ goto yy390;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '<') {
+ if (yych <= '/') goto yy1;
+ if (yych <= ':') goto yy390;
+ goto yy1;
+ } else {
+ if (yych <= '=') goto yy408;
+ if (yych <= '>') goto yy309;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy390;
+ if (yych <= '^') goto yy1;
+ goto yy390;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy390;
+ goto yy1;
+ }
+ }
+ }
+yy408:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 1) {
+ goto yy490;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy1;
+ goto yy491;
+ } else {
+ if (yych <= '\'') goto yy492;
+ if (yych <= 0xC1) goto yy1;
+ goto yy493;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy494;
+ if (yych <= 0xEF) goto yy495;
+ goto yy496;
+ } else {
+ if (yych <= 0xF3) goto yy497;
+ if (yych <= 0xF4) goto yy498;
+ goto yy1;
+ }
+ }
+yy409:
+ yych = *++YYCURSOR;
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy409;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy409;
+ if (yych <= 0x1F) goto yy1;
+ goto yy409;
+ }
+ } else {
+ if (yych <= '<') {
+ if (yych == '/') goto yy215;
+ goto yy1;
+ } else {
+ if (yych <= '=') goto yy313;
+ if (yych <= '>') goto yy216;
+ if (yych <= '?') goto yy215;
+ goto yy1;
+ }
+ }
+yy410:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 32) {
+ goto yy410;
+ }
+ if (yych <= 'z') {
+ if (yych <= '/') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '-') goto yy499;
+ goto yy500;
+ } else {
+ if (yych <= ':') goto yy499;
+ if (yych <= '>') goto yy216;
+ if (yych <= '?') goto yy500;
+ goto yy499;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy414;
+ if (yych <= 0xE0) goto yy415;
+ goto yy416;
+ } else {
+ if (yych <= 0xF0) goto yy417;
+ if (yych <= 0xF3) goto yy418;
+ if (yych <= 0xF4) goto yy419;
+ goto yy1;
+ }
+ }
+yy411:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 16) {
+ goto yy500;
+ }
+ if (yych <= '=') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy411;
+ goto yy410;
+ } else {
+ if (yych <= '\r') goto yy411;
+ if (yych == ' ') goto yy411;
+ goto yy410;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '"') goto yy412;
+ if (yych == '\'') goto yy413;
+ goto yy410;
+ } else {
+ if (yych == '.') goto yy410;
+ if (yych <= ':') goto yy499;
+ goto yy410;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '^') {
+ if (yych <= '>') goto yy216;
+ if (yych <= '@') goto yy410;
+ if (yych <= 'Z') goto yy499;
+ goto yy410;
+ } else {
+ if (yych == '`') goto yy410;
+ if (yych <= 'z') goto yy499;
+ goto yy410;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy414;
+ if (yych <= 0xE0) goto yy415;
+ goto yy416;
+ } else {
+ if (yych <= 0xF0) goto yy417;
+ if (yych <= 0xF3) goto yy418;
+ if (yych <= 0xF4) goto yy419;
+ goto yy1;
+ }
+ }
+ }
+yy412:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 64) {
+ goto yy412;
+ }
+ if (yych <= '\\') {
+ if (yych <= '/') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '"') goto yy410;
+ if (yych <= '-') goto yy501;
+ goto yy502;
+ } else {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy501;
+ goto yy503;
+ } else {
+ if (yych <= '?') goto yy502;
+ if (yych <= 'Z') goto yy501;
+ goto yy504;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 'z') goto yy501;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy505;
+ goto yy506;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy507;
+ goto yy508;
+ } else {
+ if (yych <= 0xF3) goto yy509;
+ if (yych <= 0xF4) goto yy510;
+ goto yy1;
+ }
+ }
+ }
+yy413:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 128) {
+ goto yy413;
+ }
+ if (yych <= '\\') {
+ if (yych <= '/') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '\'') goto yy410;
+ if (yych <= '-') goto yy511;
+ goto yy512;
+ } else {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy511;
+ goto yy513;
+ } else {
+ if (yych <= '?') goto yy512;
+ if (yych <= 'Z') goto yy511;
+ goto yy514;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 'z') goto yy511;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy515;
+ goto yy516;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy517;
+ goto yy518;
+ } else {
+ if (yych <= 0xF3) goto yy519;
+ if (yych <= 0xF4) goto yy520;
+ goto yy1;
+ }
+ }
+ }
+yy414:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy410;
+ goto yy1;
+yy415:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy414;
+ goto yy1;
+yy416:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy414;
+ goto yy1;
+yy417:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy416;
+ goto yy1;
+yy418:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy416;
+ goto yy1;
+yy419:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy416;
+ goto yy1;
+yy420:
+ yych = *++YYCURSOR;
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy521;
+ } else {
+ if (yych == '\r') goto yy521;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= ' ') goto yy521;
+ if (yych <= ',') goto yy1;
+ goto yy420;
+ } else {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy215;
+ goto yy420;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '=') {
+ if (yych <= '<') goto yy1;
+ goto yy422;
+ } else {
+ if (yych == '?') goto yy215;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy420;
+ if (yych <= '^') goto yy1;
+ goto yy420;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy420;
+ goto yy1;
+ }
+ }
+ }
+yy421:
+ yych = *++YYCURSOR;
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy421;
+ } else {
+ if (yych == '\r') goto yy421;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= ' ') goto yy421;
+ if (yych <= ',') goto yy1;
+ goto yy420;
+ } else {
+ if (yych <= '.') goto yy1;
+ if (yych <= '/') goto yy215;
+ goto yy420;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '=') {
+ if (yych <= '<') goto yy1;
+ } else {
+ if (yych == '?') goto yy215;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy420;
+ if (yych <= '^') goto yy1;
+ goto yy420;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy420;
+ goto yy1;
+ }
+ }
+ }
+yy422:
+ yych = *++YYCURSOR;
+ if (yych <= '\'') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy1;
+ goto yy522;
+ } else {
+ if (yych <= '\t') goto yy523;
+ if (yych <= '\f') goto yy522;
+ goto yy523;
+ }
+ } else {
+ if (yych <= '!') {
+ if (yych == ' ') goto yy523;
+ goto yy522;
+ } else {
+ if (yych <= '"') goto yy524;
+ if (yych <= '&') goto yy522;
+ goto yy525;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '>') {
+ if (yych <= '=') goto yy522;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy522;
+ if (yych <= 0xC1) goto yy1;
+ goto yy526;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy527;
+ if (yych <= 0xEF) goto yy528;
+ goto yy529;
+ } else {
+ if (yych <= 0xF3) goto yy530;
+ if (yych <= 0xF4) goto yy531;
+ goto yy1;
+ }
+ }
+ }
+yy423:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy532;
+ goto yy148;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy532;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy532;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy424:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy533;
+ goto yy1;
+yy425:
+ yyaccept = 15;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy222;
+ goto yy461;
+ } else {
+ if (yych <= '.') goto yy222;
+ if (yych <= '/') goto yy189;
+ goto yy192;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= ':') goto yy462;
+ if (yych <= '@') goto yy222;
+ goto yy192;
+ } else {
+ if (yych <= '`') goto yy222;
+ if (yych <= 'f') goto yy192;
+ goto yy222;
+ }
+ }
+yy426:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy532;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy532;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy534;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy427:
+ ++YYCURSOR;
+yy428:
+ YYCURSOR = yyt1;
+#line 262 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_CONSTANT); }
+#line 14275 "../../lnav/src/data_scanner_re.cc"
+yy429:
+ yych = *++YYCURSOR;
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy427;
+ goto yy428;
+ } else {
+ if (yych <= '\r') goto yy427;
+ if (yych == ' ') goto yy427;
+ goto yy428;
+ }
+yy430:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[1024+yych] & 16) {
+ goto yy46;
+ }
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy427;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy427;
+ if (yych <= 0x1F) goto yy4;
+ goto yy427;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych == '@') goto yy96;
+ goto yy4;
+ }
+ }
+yy431:
+ yyaccept = 23;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '/') goto yy189;
+ if (yych == ':') goto yy152;
+ goto yy428;
+yy432:
+ yyaccept = 23;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 128) {
+ goto yy137;
+ }
+ if (yych <= '*') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy225;
+ if (yych <= 0x08) goto yy428;
+ if (yych <= '\n') goto yy225;
+ goto yy428;
+ } else {
+ if (yych <= 0x1F) {
+ if (yych <= '\r') goto yy225;
+ goto yy428;
+ } else {
+ if (yych <= '"') goto yy225;
+ if (yych <= '&') goto yy428;
+ goto yy225;
+ }
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych == ',') goto yy225;
+ if (yych <= '-') goto yy428;
+ yyt4 = YYCURSOR;
+ goto yy226;
+ } else {
+ if (yych <= ';') {
+ if (yych <= '9') goto yy428;
+ goto yy225;
+ } else {
+ if (yych == '?') goto yy225;
+ goto yy428;
+ }
+ }
+ }
+yy433:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= '!') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy3;
+ } else {
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '\f') goto yy3;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy3;
+ goto yy5;
+ } else {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '#') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy5;
+ } else {
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ goto yy5;
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy80;
+ } else {
+ if (yych <= ',') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '-') goto yy46;
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '>') {
+ if (yych <= '9') {
+ if (yych <= '/') goto yy3;
+ goto yy46;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy535;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy5;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych == '_') goto yy46;
+ goto yy5;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '~') {
+ if (yych <= 'z') goto yy433;
+ goto yy5;
+ } else {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy5;
+ goto yy56;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy57;
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy5;
+ }
+ }
+ }
+ }
+yy434:
+ ++YYCURSOR;
+yy435:
+ YYCURSOR = yyt1;
+#line 172 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_TIME); }
+#line 14484 "../../lnav/src/data_scanner_re.cc"
+yy436:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy434;
+ goto yy1;
+yy437:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy436;
+ goto yy1;
+yy438:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy436;
+ goto yy1;
+yy439:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy438;
+ goto yy1;
+yy440:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy438;
+ goto yy1;
+yy441:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy438;
+ goto yy1;
+yy442:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy536;
+ goto yy1;
+yy443:
+ yych = *++YYCURSOR;
+ if (yych == 'r') goto yy537;
+ goto yy100;
+yy444:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy24;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy24;
+ if (yych == 0x1B) goto yy24;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '#') {
+ if (yych == '!') goto yy99;
+ if (yych <= '"') goto yy24;
+ goto yy99;
+ } else {
+ if (yych <= '&') {
+ if (yych <= '$') goto yy101;
+ goto yy99;
+ } else {
+ if (yych <= '\'') goto yy24;
+ if (yych <= '*') goto yy99;
+ goto yy24;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') {
+ if (yych <= '9') goto yy101;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy24;
+ goto yy101;
+ } else {
+ if (yych <= '\\') {
+ if (yych <= '[') goto yy24;
+ goto yy99;
+ } else {
+ if (yych <= ']') goto yy24;
+ if (yych <= '^') goto yy99;
+ goto yy101;
+ }
+ }
+ } else {
+ if (yych <= '}') {
+ if (yych <= '`') goto yy24;
+ if (yych <= 'a') goto yy538;
+ if (yych <= 'z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy99;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy24;
+ if (yych <= 0xF4) goto yy4;
+ goto yy24;
+ }
+ }
+ }
+ }
+yy445:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '1') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '0') goto yy539;
+ goto yy540;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') {
+ if (yych <= '2') goto yy541;
+ goto yy539;
+ } else {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy446:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '-') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy345;
+ if (yych <= 0x08) goto yy4;
+ goto yy345;
+ } else {
+ if (yych == '\r') goto yy345;
+ if (yych <= 0x1A) goto yy4;
+ goto yy345;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy345;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy345;
+ goto yy255;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '.') goto yy445;
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ } else {
+ if (yych <= '?') goto yy345;
+ if (yych <= '@') goto yy96;
+ if (yych >= '[') goto yy345;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy345;
+ if (yych >= '{') goto yy345;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xF4) goto yy4;
+ goto yy345;
+ }
+ }
+ }
+yy447:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy345;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy345;
+ goto yy3;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy345;
+ goto yy3;
+ } else {
+ if (yych <= 0x1B) goto yy345;
+ if (yych <= 0x1F) goto yy3;
+ goto yy345;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ if (yych <= '*') goto yy345;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy345;
+ goto yy255;
+ } else {
+ if (yych <= '.') goto yy350;
+ if (yych <= '/') goto yy3;
+ goto yy447;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy62;
+ if (yych <= '?') goto yy345;
+ if (yych <= '@') goto yy96;
+ goto yy447;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy345;
+ goto yy447;
+ } else {
+ if (yych <= '`') goto yy345;
+ if (yych <= 'z') goto yy447;
+ goto yy345;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy345;
+ }
+ }
+ }
+ }
+yy448:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy345;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy345;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy345;
+ if (yych == 0x1B) goto yy345;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy345;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy345;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy345;
+ if (yych <= '-') goto yy255;
+ goto yy445;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '5') goto yy446;
+ if (yych <= '9') goto yy447;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy345;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy447;
+ goto yy345;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy345;
+ if (yych <= 'z') goto yy447;
+ goto yy345;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xF4) goto yy4;
+ goto yy345;
+ }
+ }
+ }
+yy449:
+ yyaccept = 24;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych >= '+') goto yy80;
+ } else {
+ if (yych <= ',') goto yy450;
+ if (yych <= '.') goto yy80;
+ if (yych >= '0') goto yy449;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy450;
+ if (yych <= '@') goto yy82;
+ if (yych <= 'Z') goto yy80;
+ } else {
+ if (yych == '`') goto yy450;
+ if (yych <= 'z') goto yy80;
+ }
+ }
+yy450:
+#line 249 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_NUMBER); }
+#line 14829 "../../lnav/src/data_scanner_re.cc"
+yy451:
+ yyaccept = 24;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy450;
+ if (yych <= 0x08) goto yy3;
+ if (yych <= '\n') goto yy450;
+ goto yy3;
+ } else {
+ if (yych <= '\r') goto yy450;
+ if (yych == 0x1B) goto yy450;
+ goto yy3;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy450;
+ if (yych <= '$') goto yy3;
+ if (yych <= '%') goto yy80;
+ goto yy450;
+ } else {
+ if (yych <= ',') {
+ if (yych <= '+') goto yy80;
+ goto yy450;
+ } else {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy3;
+ goto yy451;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy62;
+ if (yych <= '?') goto yy450;
+ if (yych <= '@') goto yy96;
+ goto yy344;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy450;
+ goto yy344;
+ } else {
+ if (yych <= '`') goto yy450;
+ if (yych <= 'z') goto yy344;
+ goto yy450;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy450;
+ if (yych <= 0xDF) goto yy56;
+ goto yy57;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy450;
+ }
+ }
+ }
+ }
+yy452:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy542;
+ if (yych <= ':') goto yy152;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy542;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy542;
+ goto yy1;
+ }
+yy453:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '/') goto yy543;
+ goto yy4;
+yy454:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy544;
+ goto yy1;
+yy455:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '-') goto yy545;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') goto yy546;
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy456:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') goto yy4;
+ if (yych <= '/') goto yy547;
+ if (yych <= '9') goto yy548;
+ goto yy4;
+yy457:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy549;
+ goto yy550;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy551;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy552;
+ goto yy551;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy551;
+ } else {
+ if (yych <= 'e') goto yy552;
+ if (yych <= 'f') goto yy551;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy458:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy550;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy551;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy552;
+ goto yy551;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy551;
+ } else {
+ if (yych <= 'e') goto yy552;
+ if (yych <= 'f') goto yy551;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy459:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy113;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy551;
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy113;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy551;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy113;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy551;
+ if (yych <= 'z') goto yy115;
+ goto yy113;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+yy460:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy194;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy251;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy551;
+ if (yych <= ':') goto yy148;
+ goto yy113;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy551;
+ if (yych <= 'Z') goto yy115;
+ goto yy113;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy551;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy461:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy553;
+ goto yy1;
+ } else {
+ if (yych <= 'Z') goto yy553;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy553;
+ goto yy1;
+ }
+yy462:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '0') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ goto yy554;
+ } else {
+ if (yych <= '2') {
+ if (yych <= '1') goto yy555;
+ goto yy556;
+ } else {
+ if (yych <= '9') goto yy554;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy557;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy557;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy1;
+ }
+ }
+ }
+ }
+yy463:
+ yyaccept = 25;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= '/') goto yy435;
+ if (yych <= '9') goto yy542;
+ goto yy435;
+ } else {
+ if (yych <= 'F') goto yy542;
+ if (yych <= '`') goto yy435;
+ if (yych <= 'f') goto yy542;
+ goto yy435;
+ }
+yy464:
+ yyaccept = 25;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= '/') goto yy435;
+ if (yych <= '9') goto yy468;
+ if (yych <= ':') goto yy282;
+ goto yy435;
+ } else {
+ if (yych <= 'F') goto yy468;
+ if (yych <= '`') goto yy435;
+ if (yych <= 'f') goto yy468;
+ goto yy435;
+ }
+yy465:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy558;
+ if (yych <= ':') goto yy375;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy559;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy559;
+ goto yy1;
+ }
+yy466:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy542;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy542;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy542;
+ goto yy1;
+ }
+yy467:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy559;
+ if (yych <= ':') goto yy375;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy559;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy559;
+ goto yy1;
+ }
+yy468:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy282;
+ goto yy1;
+yy469:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy560;
+ if (yych <= ':') goto yy471;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy470;
+ if (yych <= '`') goto yy1;
+ if (yych >= 'g') goto yy1;
+ }
+yy470:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy561;
+ if (yych >= ';') goto yy1;
+ } else {
+ if (yych <= 'F') goto yy561;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy561;
+ goto yy1;
+ }
+yy471:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy562;
+ if (yych <= ':') goto yy563;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy562;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy562;
+ goto yy1;
+ }
+yy472:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy564;
+ goto yy565;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy566;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy566;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy473:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy567;
+ goto yy565;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy566;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy566;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy474:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '5') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '4') goto yy567;
+ goto yy568;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy564;
+ if (yych <= ':') goto yy565;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy566;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy566;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy475:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy566;
+ if (yych <= ':') goto yy565;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy566;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy566;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy476:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy569;
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy570;
+ goto yy4;
+yy477:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy569;
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy476;
+ goto yy4;
+yy478:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych == '.') goto yy569;
+ goto yy4;
+ } else {
+ if (yych <= '4') goto yy476;
+ if (yych <= '5') goto yy571;
+ if (yych <= '9') goto yy570;
+ goto yy4;
+ }
+yy479:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy572;
+ if (yych <= ':') goto yy378;
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '_') {
+ if (yych <= 'F') goto yy572;
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy572;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy480:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy572;
+ if (yych <= ':') goto yy378;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy572;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy572;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy481:
+ yych = *++YYCURSOR;
+ if (yych <= '1') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '0') goto yy573;
+ goto yy574;
+ } else {
+ if (yych <= '2') goto yy575;
+ if (yych <= '9') goto yy573;
+ goto yy1;
+ }
+yy482:
+ yych = *++YYCURSOR;
+ if (yych == '.') goto yy481;
+ goto yy1;
+yy483:
+ yych = *++YYCURSOR;
+ if (yych == '.') goto yy481;
+ if (yych <= '/') goto yy1;
+ if (yych <= '5') goto yy482;
+ goto yy1;
+yy484:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1;
+ if (yych <= ':') goto yy299;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy485:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy576;
+ if (yych >= ';') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy576;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy576;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy486:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy475;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy475;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy475;
+ goto yy1;
+ }
+yy487:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1;
+ if (yych <= ':') goto yy577;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy488:
+ yych = *++YYCURSOR;
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy488;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy488;
+ if (yych == ' ') goto yy488;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '\'') {
+ if (yych <= '"') goto yy306;
+ if (yych <= '&') goto yy1;
+ goto yy307;
+ } else {
+ if (yych <= '<') goto yy1;
+ if (yych <= '=') goto yy408;
+ if (yych <= '>') goto yy309;
+ goto yy1;
+ }
+ }
+yy489:
+ yych = *++YYCURSOR;
+ if (yych <= ',') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy578;
+ } else {
+ if (yych == '\r') goto yy578;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy578;
+ if (yych <= '!') goto yy1;
+ goto yy306;
+ } else {
+ if (yych == '\'') goto yy307;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') {
+ if (yych <= '-') goto yy390;
+ if (yych <= '/') goto yy1;
+ goto yy390;
+ } else {
+ if (yych == '>') goto yy309;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy390;
+ if (yych <= '^') goto yy1;
+ goto yy390;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy390;
+ goto yy1;
+ }
+ }
+ }
+yy490:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 1) {
+ goto yy490;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy1;
+ if (yych >= '#') goto yy492;
+ } else {
+ if (yych <= '>') goto yy309;
+ if (yych <= 0xC1) goto yy1;
+ goto yy493;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy494;
+ if (yych <= 0xEF) goto yy495;
+ goto yy496;
+ } else {
+ if (yych <= 0xF3) goto yy497;
+ if (yych <= 0xF4) goto yy498;
+ goto yy1;
+ }
+ }
+yy491:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 2) {
+ goto yy491;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '>') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '\'') goto yy579;
+ goto yy580;
+ } else {
+ if (yych <= '\\') goto yy581;
+ if (yych <= 0xC1) goto yy1;
+ goto yy582;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy583;
+ if (yych <= 0xEF) goto yy584;
+ goto yy585;
+ } else {
+ if (yych <= 0xF3) goto yy586;
+ if (yych <= 0xF4) goto yy587;
+ goto yy1;
+ }
+ }
+yy492:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 4) {
+ goto yy492;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '>') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '"') goto yy579;
+ goto yy588;
+ } else {
+ if (yych <= '\\') goto yy589;
+ if (yych <= 0xC1) goto yy1;
+ goto yy590;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy591;
+ if (yych <= 0xEF) goto yy592;
+ goto yy593;
+ } else {
+ if (yych <= 0xF3) goto yy594;
+ if (yych <= 0xF4) goto yy595;
+ goto yy1;
+ }
+ }
+yy493:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy490;
+ goto yy1;
+yy494:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy493;
+ goto yy1;
+yy495:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy493;
+ goto yy1;
+yy496:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy495;
+ goto yy1;
+yy497:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy495;
+ goto yy1;
+yy498:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy495;
+ goto yy1;
+yy499:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 8) {
+ goto yy499;
+ }
+ if (yych <= 0x7F) {
+ if (yych <= '<') {
+ if (yych <= 0x00) goto yy1;
+ if (yych != '/') goto yy410;
+ } else {
+ if (yych <= '=') goto yy411;
+ if (yych <= '>') goto yy216;
+ if (yych >= '@') goto yy410;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy414;
+ if (yych <= 0xE0) goto yy415;
+ goto yy416;
+ } else {
+ if (yych <= 0xF0) goto yy417;
+ if (yych <= 0xF3) goto yy418;
+ if (yych <= 0xF4) goto yy419;
+ goto yy1;
+ }
+ }
+yy500:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 32) {
+ goto yy410;
+ }
+ if (yych <= 'z') {
+ if (yych <= '/') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '-') goto yy499;
+ goto yy500;
+ } else {
+ if (yych <= ':') goto yy499;
+ if (yych <= '>') goto yy314;
+ if (yych <= '?') goto yy500;
+ goto yy499;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy414;
+ if (yych <= 0xE0) goto yy415;
+ goto yy416;
+ } else {
+ if (yych <= 0xF0) goto yy417;
+ if (yych <= 0xF3) goto yy418;
+ if (yych <= 0xF4) goto yy419;
+ goto yy1;
+ }
+ }
+yy501:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy501;
+ goto yy412;
+ } else {
+ if (yych <= '\r') goto yy501;
+ if (yych == ' ') goto yy501;
+ goto yy412;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '"') goto yy410;
+ if (yych == '-') goto yy501;
+ goto yy412;
+ } else {
+ if (yych <= ':') {
+ if (yych >= '0') goto yy501;
+ } else {
+ if (yych <= '<') goto yy412;
+ if (yych <= '=') goto yy596;
+ goto yy503;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '?') goto yy502;
+ if (yych <= '@') goto yy412;
+ if (yych <= 'Z') goto yy501;
+ goto yy412;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy504;
+ goto yy412;
+ } else {
+ if (yych == '`') goto yy412;
+ goto yy501;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy412;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy505;
+ goto yy506;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy507;
+ goto yy508;
+ } else {
+ if (yych <= 0xF3) goto yy509;
+ if (yych <= 0xF4) goto yy510;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy502:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 64) {
+ goto yy412;
+ }
+ if (yych <= '\\') {
+ if (yych <= '/') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '"') goto yy410;
+ if (yych <= '-') goto yy501;
+ goto yy502;
+ } else {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy501;
+ goto yy597;
+ } else {
+ if (yych <= '?') goto yy502;
+ if (yych <= 'Z') goto yy501;
+ goto yy504;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 'z') goto yy501;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy505;
+ goto yy506;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy507;
+ goto yy508;
+ } else {
+ if (yych <= 0xF3) goto yy509;
+ if (yych <= 0xF4) goto yy510;
+ goto yy1;
+ }
+ }
+ }
+yy503:
+ yyaccept = 26;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0xC1) {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy217;
+ if (yych <= '!') goto yy598;
+ goto yy599;
+ } else {
+ if (yych == '\\') goto yy600;
+ if (yych <= 0x7F) goto yy598;
+ goto yy217;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy601;
+ if (yych <= 0xE0) goto yy602;
+ goto yy603;
+ } else {
+ if (yych <= 0xF0) goto yy604;
+ if (yych <= 0xF3) goto yy605;
+ if (yych <= 0xF4) goto yy606;
+ goto yy217;
+ }
+ }
+yy504:
+ yych = *++YYCURSOR;
+ if (yych <= 'Z') {
+ if (yych <= '.') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy598;
+ if (yych <= '\t') goto yy412;
+ goto yy410;
+ } else {
+ if (yych == '-') goto yy501;
+ goto yy412;
+ }
+ } else {
+ if (yych <= '=') {
+ if (yych <= '/') goto yy502;
+ if (yych <= ':') goto yy501;
+ goto yy412;
+ } else {
+ if (yych <= '>') goto yy503;
+ if (yych <= '?') goto yy502;
+ if (yych <= '@') goto yy412;
+ goto yy501;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '`') {
+ if (yych == '_') goto yy501;
+ goto yy412;
+ } else {
+ if (yych <= 'z') goto yy501;
+ if (yych <= 0x7F) goto yy412;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy505;
+ if (yych <= 0xE0) goto yy506;
+ goto yy507;
+ } else {
+ if (yych <= 0xF0) goto yy508;
+ if (yych <= 0xF3) goto yy509;
+ if (yych <= 0xF4) goto yy510;
+ goto yy1;
+ }
+ }
+ }
+yy505:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy412;
+ goto yy1;
+yy506:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy505;
+ goto yy1;
+yy507:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy505;
+ goto yy1;
+yy508:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy507;
+ goto yy1;
+yy509:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy507;
+ goto yy1;
+yy510:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy507;
+ goto yy1;
+yy511:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '&') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy511;
+ goto yy413;
+ } else {
+ if (yych <= '\r') goto yy511;
+ if (yych == ' ') goto yy511;
+ goto yy413;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '\'') goto yy410;
+ if (yych == '-') goto yy511;
+ goto yy413;
+ } else {
+ if (yych <= ':') {
+ if (yych >= '0') goto yy511;
+ } else {
+ if (yych <= '<') goto yy413;
+ if (yych <= '=') goto yy607;
+ goto yy513;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '?') goto yy512;
+ if (yych <= '@') goto yy413;
+ if (yych <= 'Z') goto yy511;
+ goto yy413;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy514;
+ goto yy413;
+ } else {
+ if (yych == '`') goto yy413;
+ goto yy511;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy413;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy515;
+ goto yy516;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy517;
+ goto yy518;
+ } else {
+ if (yych <= 0xF3) goto yy519;
+ if (yych <= 0xF4) goto yy520;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy512:
+ yych = *++YYCURSOR;
+ if (yybm[256+yych] & 128) {
+ goto yy413;
+ }
+ if (yych <= '\\') {
+ if (yych <= '/') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '\'') goto yy410;
+ if (yych <= '-') goto yy511;
+ goto yy512;
+ } else {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy511;
+ goto yy608;
+ } else {
+ if (yych <= '?') goto yy512;
+ if (yych <= 'Z') goto yy511;
+ goto yy514;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 'z') goto yy511;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy515;
+ goto yy516;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy517;
+ goto yy518;
+ } else {
+ if (yych <= 0xF3) goto yy519;
+ if (yych <= 0xF4) goto yy520;
+ goto yy1;
+ }
+ }
+ }
+yy513:
+ yyaccept = 26;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0xC1) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy217;
+ if (yych <= '&') goto yy609;
+ goto yy599;
+ } else {
+ if (yych == '\\') goto yy610;
+ if (yych <= 0x7F) goto yy609;
+ goto yy217;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy611;
+ if (yych <= 0xE0) goto yy612;
+ goto yy613;
+ } else {
+ if (yych <= 0xF0) goto yy614;
+ if (yych <= 0xF3) goto yy615;
+ if (yych <= 0xF4) goto yy616;
+ goto yy217;
+ }
+ }
+yy514:
+ yych = *++YYCURSOR;
+ if (yych <= 'Z') {
+ if (yych <= '.') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy609;
+ if (yych <= '\t') goto yy413;
+ goto yy410;
+ } else {
+ if (yych == '-') goto yy511;
+ goto yy413;
+ }
+ } else {
+ if (yych <= '=') {
+ if (yych <= '/') goto yy512;
+ if (yych <= ':') goto yy511;
+ goto yy413;
+ } else {
+ if (yych <= '>') goto yy513;
+ if (yych <= '?') goto yy512;
+ if (yych <= '@') goto yy413;
+ goto yy511;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '`') {
+ if (yych == '_') goto yy511;
+ goto yy413;
+ } else {
+ if (yych <= 'z') goto yy511;
+ if (yych <= 0x7F) goto yy413;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy515;
+ if (yych <= 0xE0) goto yy516;
+ goto yy517;
+ } else {
+ if (yych <= 0xF0) goto yy518;
+ if (yych <= 0xF3) goto yy519;
+ if (yych <= 0xF4) goto yy520;
+ goto yy1;
+ }
+ }
+ }
+yy515:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy413;
+ goto yy1;
+yy516:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy515;
+ goto yy1;
+yy517:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy515;
+ goto yy1;
+yy518:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy517;
+ goto yy1;
+yy519:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy517;
+ goto yy1;
+yy520:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy517;
+ goto yy1;
+yy521:
+ yych = *++YYCURSOR;
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy521;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy521;
+ if (yych <= 0x1F) goto yy1;
+ goto yy521;
+ }
+ } else {
+ if (yych <= '<') {
+ if (yych == '/') goto yy215;
+ goto yy1;
+ } else {
+ if (yych <= '=') goto yy422;
+ if (yych == '?') goto yy215;
+ goto yy1;
+ }
+ }
+yy522:
+ yych = *++YYCURSOR;
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= ',') goto yy522;
+ goto yy617;
+ } else {
+ if (yych <= '.') goto yy522;
+ if (yych <= '/') goto yy618;
+ goto yy617;
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '=') goto yy522;
+ if (yych <= '>') goto yy1;
+ goto yy618;
+ } else {
+ if (yych <= '@') goto yy522;
+ if (yych <= 'Z') goto yy617;
+ goto yy522;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych == '`') goto yy522;
+ goto yy617;
+ } else {
+ if (yych <= 0x7F) goto yy522;
+ if (yych <= 0xC1) goto yy1;
+ goto yy526;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy527;
+ if (yych <= 0xEF) goto yy528;
+ goto yy529;
+ } else {
+ if (yych <= 0xF3) goto yy530;
+ if (yych <= 0xF4) goto yy531;
+ goto yy1;
+ }
+ }
+ }
+yy523:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy523;
+ goto yy522;
+ } else {
+ if (yych <= '\r') goto yy523;
+ if (yych == ' ') goto yy523;
+ goto yy522;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '"') goto yy524;
+ if (yych == '\'') goto yy525;
+ goto yy522;
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy617;
+ goto yy522;
+ } else {
+ if (yych <= '/') goto yy618;
+ if (yych <= ':') goto yy617;
+ goto yy522;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'Z') {
+ if (yych <= '>') goto yy1;
+ if (yych <= '?') goto yy618;
+ if (yych <= '@') goto yy522;
+ goto yy617;
+ } else {
+ if (yych == '_') goto yy617;
+ if (yych <= '`') goto yy522;
+ goto yy617;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy522;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy526;
+ goto yy527;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy528;
+ goto yy529;
+ } else {
+ if (yych <= 0xF3) goto yy530;
+ if (yych <= 0xF4) goto yy531;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy524:
+ yych = *++YYCURSOR;
+ if (yych <= '[') {
+ if (yych <= '/') {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '!') goto yy524;
+ goto yy522;
+ } else {
+ if (yych == '-') goto yy619;
+ if (yych <= '.') goto yy524;
+ goto yy620;
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy619;
+ if (yych <= '=') goto yy524;
+ goto yy621;
+ } else {
+ if (yych <= '?') goto yy620;
+ if (yych <= '@') goto yy524;
+ if (yych <= 'Z') goto yy619;
+ goto yy524;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '_') {
+ if (yych <= '\\') goto yy622;
+ if (yych <= '^') goto yy524;
+ goto yy619;
+ } else {
+ if (yych <= '`') goto yy524;
+ if (yych <= 'z') goto yy619;
+ if (yych <= 0x7F) goto yy524;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy623;
+ if (yych <= 0xE0) goto yy624;
+ goto yy625;
+ } else {
+ if (yych <= 0xF0) goto yy626;
+ if (yych <= 0xF3) goto yy627;
+ if (yych <= 0xF4) goto yy628;
+ goto yy1;
+ }
+ }
+ }
+yy525:
+ yych = *++YYCURSOR;
+ if (yych <= '[') {
+ if (yych <= '/') {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '&') goto yy525;
+ goto yy522;
+ } else {
+ if (yych == '-') goto yy629;
+ if (yych <= '.') goto yy525;
+ goto yy630;
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy629;
+ if (yych <= '=') goto yy525;
+ goto yy631;
+ } else {
+ if (yych <= '?') goto yy630;
+ if (yych <= '@') goto yy525;
+ if (yych <= 'Z') goto yy629;
+ goto yy525;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '_') {
+ if (yych <= '\\') goto yy632;
+ if (yych <= '^') goto yy525;
+ goto yy629;
+ } else {
+ if (yych <= '`') goto yy525;
+ if (yych <= 'z') goto yy629;
+ if (yych <= 0x7F) goto yy525;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy633;
+ if (yych <= 0xE0) goto yy634;
+ goto yy635;
+ } else {
+ if (yych <= 0xF0) goto yy636;
+ if (yych <= 0xF3) goto yy637;
+ if (yych <= 0xF4) goto yy638;
+ goto yy1;
+ }
+ }
+ }
+yy526:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy522;
+ goto yy1;
+yy527:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy526;
+ goto yy1;
+yy528:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy526;
+ goto yy1;
+yy529:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy528;
+ goto yy1;
+yy530:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy528;
+ goto yy1;
+yy531:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy528;
+ goto yy1;
+yy532:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy639;
+ goto yy148;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy639;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy639;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy533:
+ yych = *++YYCURSOR;
+ if (yych == ' ') goto yy640;
+ goto yy1;
+yy534:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy639;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy639;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy641;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy535:
+ yyaccept = 15;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == ':') goto yy152;
+ goto yy222;
+yy536:
+ yych = *++YYCURSOR;
+ if (yych <= 0xC1) {
+ if (yych <= '9') {
+ if (yych == '.') {
+ yyt2 = YYCURSOR;
+ goto yy644;
+ }
+ yyt2 = YYCURSOR;
+ goto yy642;
+ } else {
+ if (yych <= ':') goto yy1;
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy645;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy646;
+ }
+ yyt2 = YYCURSOR;
+ goto yy647;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy648;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy649;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy650;
+ }
+ goto yy1;
+ }
+ }
+yy537:
+ yych = *++YYCURSOR;
+ if (yych == 'a') goto yy651;
+ goto yy100;
+yy538:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy24;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy24;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy24;
+ if (yych <= 0x1F) goto yy4;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '$') {
+ if (yych == '"') goto yy24;
+ if (yych <= '#') goto yy99;
+ goto yy101;
+ } else {
+ if (yych <= '\'') {
+ if (yych <= '&') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '*') goto yy99;
+ if (yych <= ',') goto yy24;
+ goto yy101;
+ }
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '[') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy24;
+ if (yych <= 'Z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= ']') {
+ if (yych <= '\\') goto yy99;
+ goto yy24;
+ } else {
+ if (yych <= '^') goto yy99;
+ if (yych <= '_') goto yy101;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= '}') {
+ if (yych == 'm') goto yy652;
+ if (yych <= 'z') goto yy101;
+ goto yy24;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy99;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy24;
+ if (yych <= 0xF4) goto yy4;
+ goto yy24;
+ }
+ }
+ }
+ }
+yy539:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x08) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '\n') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1B) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x1F) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy656;
+ }
+ if (yych <= '*') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy656;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy657;
+ } else {
+ if (yych <= '.') {
+ yyt1 = YYCURSOR;
+ goto yy658;
+ }
+ if (yych <= '/') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ goto yy659;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= 'Z') {
+ if (yych <= ':') {
+ yyt1 = YYCURSOR;
+ goto yy660;
+ }
+ if (yych <= '?') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= '@') {
+ yyt1 = YYCURSOR;
+ goto yy661;
+ }
+ yyt1 = YYCURSOR;
+ goto yy662;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy662;
+ } else {
+ if (yych <= '`') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 'z') {
+ yyt1 = YYCURSOR;
+ goto yy662;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xDF) {
+ yyt1 = YYCURSOR;
+ goto yy663;
+ }
+ yyt1 = YYCURSOR;
+ goto yy664;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy665;
+ }
+ yyt1 = YYCURSOR;
+ goto yy666;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy667;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy668;
+ }
+ goto yy345;
+ }
+ }
+ }
+ }
+yy540:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x08) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '\n') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1B) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x1F) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy656;
+ }
+ if (yych <= '*') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy656;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy657;
+ } else {
+ if (yych <= '.') {
+ yyt1 = YYCURSOR;
+ goto yy658;
+ }
+ if (yych <= '/') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ goto yy539;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= 'Z') {
+ if (yych <= ':') {
+ yyt1 = YYCURSOR;
+ goto yy660;
+ }
+ if (yych <= '?') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= '@') {
+ yyt1 = YYCURSOR;
+ goto yy661;
+ }
+ yyt1 = YYCURSOR;
+ goto yy662;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy662;
+ } else {
+ if (yych <= '`') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 'z') {
+ yyt1 = YYCURSOR;
+ goto yy662;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xDF) {
+ yyt1 = YYCURSOR;
+ goto yy663;
+ }
+ yyt1 = YYCURSOR;
+ goto yy664;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy665;
+ }
+ yyt1 = YYCURSOR;
+ goto yy666;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy667;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy668;
+ }
+ goto yy345;
+ }
+ }
+ }
+ }
+yy541:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '5') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x08) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '\n') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1B) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x1F) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ yyt1 = YYCURSOR;
+ goto yy656;
+ } else {
+ if (yych == '+') {
+ yyt1 = YYCURSOR;
+ goto yy656;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt1 = YYCURSOR;
+ goto yy657;
+ }
+ yyt1 = YYCURSOR;
+ goto yy658;
+ } else {
+ if (yych <= '/') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '4') goto yy539;
+ goto yy669;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy659;
+ if (yych <= ':') {
+ yyt1 = YYCURSOR;
+ goto yy660;
+ }
+ if (yych <= '?') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy661;
+ } else {
+ if (yych <= '^') {
+ if (yych <= 'Z') {
+ yyt1 = YYCURSOR;
+ goto yy662;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ } else {
+ if (yych == '`') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy662;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xDF) {
+ yyt1 = YYCURSOR;
+ goto yy663;
+ }
+ yyt1 = YYCURSOR;
+ goto yy664;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy665;
+ }
+ yyt1 = YYCURSOR;
+ goto yy666;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy667;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy668;
+ }
+ goto yy345;
+ }
+ }
+ }
+ }
+yy542:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy670;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy670;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy670;
+ goto yy1;
+ }
+yy543:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy671;
+ goto yy4;
+yy544:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy672;
+ goto yy1;
+yy545:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy673;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+yy546:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '-') goto yy545;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') goto yy46;
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy547:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy675;
+ goto yy4;
+yy548:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '/') goto yy547;
+ goto yy4;
+yy549:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy676;
+ goto yy677;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy678;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy679;
+ goto yy678;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy678;
+ } else {
+ if (yych <= 'e') goto yy679;
+ if (yych <= 'f') goto yy678;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy550:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy677;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy678;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy679;
+ goto yy678;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy678;
+ } else {
+ if (yych <= 'e') goto yy679;
+ if (yych <= 'f') goto yy678;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy551:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy113;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy678;
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy113;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy678;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy113;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy678;
+ if (yych <= 'z') goto yy115;
+ goto yy113;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+yy552:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy194;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy251;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy678;
+ if (yych <= ':') goto yy148;
+ goto yy113;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy678;
+ if (yych <= 'Z') goto yy115;
+ goto yy113;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy678;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy553:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy553;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy553;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy553;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy554:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy681;
+ if (yych <= ':') goto yy682;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy683;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy683;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy555:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy684;
+ if (yych <= ':') goto yy682;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy683;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy683;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy556:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ } else {
+ if (yych <= '4') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ goto yy684;
+ } else {
+ if (yych <= '5') goto yy685;
+ if (yych <= '9') goto yy681;
+ goto yy682;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy683;
+ goto yy3;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy683;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy557:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '#') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '9') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ goto yy683;
+ } else {
+ if (yych <= ':') goto yy682;
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '_') {
+ if (yych <= 'F') goto yy683;
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy683;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy558:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy686;
+ if (yych <= ':') goto yy471;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy687;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy687;
+ goto yy1;
+ }
+yy559:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy687;
+ if (yych <= ':') goto yy471;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy687;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy687;
+ goto yy1;
+ }
+yy560:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= '9') {
+ if (yych == '.') {
+ yyt2 = YYCURSOR;
+ goto yy644;
+ }
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ yyt1 = YYCURSOR;
+ goto yy688;
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy471;
+ yyt2 = YYCURSOR;
+ goto yy642;
+ } else {
+ if (yych <= 'F') {
+ yyt1 = YYCURSOR;
+ goto yy688;
+ }
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ yyt1 = YYCURSOR;
+ goto yy688;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy645;
+ }
+ yyt2 = YYCURSOR;
+ goto yy646;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy647;
+ }
+ yyt2 = YYCURSOR;
+ goto yy648;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy649;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy650;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy561:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy689;
+ if (yych <= ':') goto yy471;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy689;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy689;
+ goto yy1;
+ }
+yy562:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy690;
+ if (yych <= ':') goto yy691;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy690;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy690;
+ goto yy1;
+ }
+yy563:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '0') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy692;
+ } else {
+ if (yych <= '1') goto yy693;
+ if (yych <= '2') goto yy694;
+ goto yy692;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy695;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy695;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy564:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy696;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy696;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy696;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy565:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy695;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy695;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy695;
+ goto yy1;
+ }
+yy566:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy696;
+ if (yych <= ':') goto yy565;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy696;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy696;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy567:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy697;
+ goto yy565;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy696;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy696;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy568:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '5') goto yy697;
+ goto yy696;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy565;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy696;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy696;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy569:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '1') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '0') goto yy698;
+ goto yy699;
+ } else {
+ if (yych <= '2') goto yy700;
+ if (yych <= '9') goto yy698;
+ goto yy4;
+ }
+yy570:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy569;
+ goto yy4;
+yy571:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy569;
+ if (yych <= '/') goto yy4;
+ if (yych <= '5') goto yy570;
+ goto yy4;
+yy572:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych == '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1A) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '9') goto yy3;
+ if (yych <= ':') goto yy378;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy3;
+ if (yych <= '~') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ yyt2 = YYCURSOR;
+ goto yy291;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy292;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+yy573:
+ yych = *++YYCURSOR;
+ if (yych == '.') goto yy701;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy702;
+ goto yy1;
+yy574:
+ yych = *++YYCURSOR;
+ if (yych == '.') goto yy701;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy573;
+ goto yy1;
+yy575:
+ yych = *++YYCURSOR;
+ if (yych <= '/') {
+ if (yych == '.') goto yy701;
+ goto yy1;
+ } else {
+ if (yych <= '4') goto yy573;
+ if (yych <= '5') goto yy703;
+ if (yych <= '9') goto yy702;
+ goto yy1;
+ }
+yy576:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy704;
+ if (yych <= ':') goto yy486;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy704;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy704;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy577:
+ yych = *++YYCURSOR;
+ if (yych <= '9') {
+ if (yych <= '0') {
+ if (yych <= '/') goto yy1;
+ goto yy705;
+ } else {
+ if (yych <= '1') goto yy706;
+ if (yych <= '2') goto yy707;
+ goto yy708;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy1;
+ goto yy387;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy387;
+ goto yy1;
+ }
+ }
+yy578:
+ yych = *++YYCURSOR;
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy578;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy578;
+ if (yych <= 0x1F) goto yy1;
+ goto yy578;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych == '"') goto yy306;
+ goto yy1;
+ } else {
+ if (yych <= '\'') goto yy307;
+ if (yych == '>') goto yy309;
+ goto yy1;
+ }
+ }
+yy579:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 32) {
+ goto yy579;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '>') {
+ if (yych <= 0x00) goto yy1;
+ goto yy709;
+ } else {
+ if (yych <= '\\') goto yy710;
+ if (yych <= 0xC1) goto yy1;
+ goto yy711;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy712;
+ if (yych <= 0xEF) goto yy713;
+ goto yy714;
+ } else {
+ if (yych <= 0xF3) goto yy715;
+ if (yych <= 0xF4) goto yy716;
+ goto yy1;
+ }
+ }
+yy580:
+ yyaccept = 27;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[256+yych] & 8) {
+ goto yy391;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy310;
+ goto yy489;
+ } else {
+ if (yych <= '\\') goto yy392;
+ if (yych <= 0xC1) goto yy310;
+ goto yy393;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy394;
+ if (yych <= 0xEF) goto yy395;
+ goto yy396;
+ } else {
+ if (yych <= 0xF3) goto yy397;
+ if (yych <= 0xF4) goto yy398;
+ goto yy310;
+ }
+ }
+yy581:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '&') {
+ if (yych <= 0x00) goto yy391;
+ if (yych == '\n') goto yy490;
+ goto yy491;
+ } else {
+ if (yych <= '\'') goto yy579;
+ if (yych == '>') goto yy580;
+ goto yy491;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy582;
+ if (yych <= 0xE0) goto yy583;
+ goto yy584;
+ } else {
+ if (yych <= 0xF0) goto yy585;
+ if (yych <= 0xF3) goto yy586;
+ if (yych <= 0xF4) goto yy587;
+ goto yy1;
+ }
+ }
+yy582:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy491;
+ goto yy1;
+yy583:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy582;
+ goto yy1;
+yy584:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy582;
+ goto yy1;
+yy585:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy584;
+ goto yy1;
+yy586:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy584;
+ goto yy1;
+yy587:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy584;
+ goto yy1;
+yy588:
+ yyaccept = 27;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[256+yych] & 16) {
+ goto yy399;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy310;
+ goto yy489;
+ } else {
+ if (yych <= '\\') goto yy400;
+ if (yych <= 0xC1) goto yy310;
+ goto yy401;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy402;
+ if (yych <= 0xEF) goto yy403;
+ goto yy404;
+ } else {
+ if (yych <= 0xF3) goto yy405;
+ if (yych <= 0xF4) goto yy406;
+ goto yy310;
+ }
+ }
+yy589:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '!') {
+ if (yych <= 0x00) goto yy399;
+ if (yych == '\n') goto yy490;
+ goto yy492;
+ } else {
+ if (yych <= '"') goto yy579;
+ if (yych == '>') goto yy588;
+ goto yy492;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy590;
+ if (yych <= 0xE0) goto yy591;
+ goto yy592;
+ } else {
+ if (yych <= 0xF0) goto yy593;
+ if (yych <= 0xF3) goto yy594;
+ if (yych <= 0xF4) goto yy595;
+ goto yy1;
+ }
+ }
+yy590:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy492;
+ goto yy1;
+yy591:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy590;
+ goto yy1;
+yy592:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy590;
+ goto yy1;
+yy593:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy592;
+ goto yy1;
+yy594:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy592;
+ goto yy1;
+yy595:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy592;
+ goto yy1;
+yy596:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '&') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy596;
+ goto yy412;
+ } else {
+ if (yych <= '\r') goto yy596;
+ if (yych == ' ') goto yy596;
+ goto yy412;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '\'') goto yy717;
+ if (yych == '-') goto yy501;
+ goto yy412;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy502;
+ goto yy501;
+ } else {
+ if (yych <= '=') goto yy412;
+ if (yych <= '>') goto yy503;
+ goto yy502;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy412;
+ if (yych <= 'Z') goto yy501;
+ if (yych <= '[') goto yy412;
+ goto yy504;
+ } else {
+ if (yych == '_') goto yy501;
+ if (yych <= '`') goto yy412;
+ goto yy501;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy412;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy505;
+ goto yy506;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy507;
+ goto yy508;
+ } else {
+ if (yych <= 0xF3) goto yy509;
+ if (yych <= 0xF4) goto yy510;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy597:
+ yyaccept = 28;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0xC1) {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy315;
+ if (yych >= '"') goto yy599;
+ } else {
+ if (yych == '\\') goto yy600;
+ if (yych >= 0x80) goto yy315;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy601;
+ if (yych <= 0xE0) goto yy602;
+ goto yy603;
+ } else {
+ if (yych <= 0xF0) goto yy604;
+ if (yych <= 0xF3) goto yy605;
+ if (yych <= 0xF4) goto yy606;
+ goto yy315;
+ }
+ }
+yy598:
+ yych = *++YYCURSOR;
+ if (yych <= 0xC1) {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '!') goto yy598;
+ } else {
+ if (yych == '\\') goto yy600;
+ if (yych <= 0x7F) goto yy598;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy601;
+ if (yych <= 0xE0) goto yy602;
+ goto yy603;
+ } else {
+ if (yych <= 0xF0) goto yy604;
+ if (yych <= 0xF3) goto yy605;
+ if (yych <= 0xF4) goto yy606;
+ goto yy1;
+ }
+ }
+yy599:
+ yych = *++YYCURSOR;
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy718;
+ } else {
+ if (yych == '\r') goto yy718;
+ goto yy1;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= ' ') goto yy718;
+ goto yy1;
+ } else {
+ if (yych <= '-') goto yy311;
+ if (yych <= '.') goto yy1;
+ goto yy215;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '=') {
+ if (yych <= ':') goto yy311;
+ goto yy1;
+ } else {
+ if (yych <= '>') goto yy216;
+ if (yych <= '?') goto yy215;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy311;
+ if (yych <= '^') goto yy1;
+ goto yy311;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy311;
+ goto yy1;
+ }
+ }
+ }
+yy600:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy598;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy598;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy602;
+ if (yych <= 0xEF) goto yy603;
+ goto yy604;
+ } else {
+ if (yych <= 0xF3) goto yy605;
+ if (yych <= 0xF4) goto yy606;
+ goto yy1;
+ }
+ }
+yy601:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy598;
+ goto yy1;
+yy602:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy601;
+ goto yy1;
+yy603:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy601;
+ goto yy1;
+yy604:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy603;
+ goto yy1;
+yy605:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy603;
+ goto yy1;
+yy606:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy603;
+ goto yy1;
+yy607:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy607;
+ goto yy413;
+ } else {
+ if (yych <= '\r') goto yy607;
+ if (yych == ' ') goto yy607;
+ goto yy413;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '"') goto yy717;
+ if (yych == '-') goto yy511;
+ goto yy413;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy512;
+ goto yy511;
+ } else {
+ if (yych <= '=') goto yy413;
+ if (yych <= '>') goto yy513;
+ goto yy512;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy413;
+ if (yych <= 'Z') goto yy511;
+ if (yych <= '[') goto yy413;
+ goto yy514;
+ } else {
+ if (yych == '_') goto yy511;
+ if (yych <= '`') goto yy413;
+ goto yy511;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy413;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy515;
+ goto yy516;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy517;
+ goto yy518;
+ } else {
+ if (yych <= 0xF3) goto yy519;
+ if (yych <= 0xF4) goto yy520;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy608:
+ yyaccept = 28;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0xC1) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy315;
+ if (yych >= '\'') goto yy599;
+ } else {
+ if (yych == '\\') goto yy610;
+ if (yych >= 0x80) goto yy315;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy611;
+ if (yych <= 0xE0) goto yy612;
+ goto yy613;
+ } else {
+ if (yych <= 0xF0) goto yy614;
+ if (yych <= 0xF3) goto yy615;
+ if (yych <= 0xF4) goto yy616;
+ goto yy315;
+ }
+ }
+yy609:
+ yych = *++YYCURSOR;
+ if (yych <= 0xC1) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '&') goto yy609;
+ goto yy599;
+ } else {
+ if (yych == '\\') goto yy610;
+ if (yych <= 0x7F) goto yy609;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy611;
+ if (yych <= 0xE0) goto yy612;
+ goto yy613;
+ } else {
+ if (yych <= 0xF0) goto yy614;
+ if (yych <= 0xF3) goto yy615;
+ if (yych <= 0xF4) goto yy616;
+ goto yy1;
+ }
+ }
+yy610:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy609;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy609;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy612;
+ if (yych <= 0xEF) goto yy613;
+ goto yy614;
+ } else {
+ if (yych <= 0xF3) goto yy615;
+ if (yych <= 0xF4) goto yy616;
+ goto yy1;
+ }
+ }
+yy611:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy609;
+ goto yy1;
+yy612:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy611;
+ goto yy1;
+yy613:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy611;
+ goto yy1;
+yy614:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy613;
+ goto yy1;
+yy615:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy613;
+ goto yy1;
+yy616:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy613;
+ goto yy1;
+yy617:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= ' ') {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy522;
+ goto yy617;
+ } else {
+ if (yych == '\r') goto yy617;
+ if (yych <= 0x1F) goto yy522;
+ goto yy617;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych == '-') goto yy617;
+ if (yych <= '.') goto yy522;
+ } else {
+ if (yych <= ':') goto yy617;
+ if (yych <= '<') goto yy522;
+ if (yych <= '=') goto yy523;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy618;
+ if (yych <= '@') goto yy522;
+ if (yych <= 'Z') goto yy617;
+ goto yy522;
+ } else {
+ if (yych == '`') goto yy522;
+ if (yych <= 'z') goto yy617;
+ goto yy522;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy526;
+ if (yych <= 0xE0) goto yy527;
+ goto yy528;
+ } else {
+ if (yych <= 0xF0) goto yy529;
+ if (yych <= 0xF3) goto yy530;
+ if (yych <= 0xF4) goto yy531;
+ goto yy1;
+ }
+ }
+ }
+yy618:
+ yych = *++YYCURSOR;
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= ',') goto yy522;
+ goto yy617;
+ } else {
+ if (yych <= '.') goto yy522;
+ if (yych <= '/') goto yy618;
+ goto yy617;
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '=') goto yy522;
+ if (yych <= '>') goto yy314;
+ goto yy618;
+ } else {
+ if (yych <= '@') goto yy522;
+ if (yych <= 'Z') goto yy617;
+ goto yy522;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych == '`') goto yy522;
+ goto yy617;
+ } else {
+ if (yych <= 0x7F) goto yy522;
+ if (yych <= 0xC1) goto yy1;
+ goto yy526;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy527;
+ if (yych <= 0xEF) goto yy528;
+ goto yy529;
+ } else {
+ if (yych <= 0xF3) goto yy530;
+ if (yych <= 0xF4) goto yy531;
+ goto yy1;
+ }
+ }
+ }
+yy619:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy619;
+ goto yy524;
+ } else {
+ if (yych <= '\r') goto yy619;
+ if (yych == ' ') goto yy619;
+ goto yy524;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '"') goto yy522;
+ if (yych == '-') goto yy619;
+ goto yy524;
+ } else {
+ if (yych <= ':') {
+ if (yych >= '0') goto yy619;
+ } else {
+ if (yych <= '<') goto yy524;
+ if (yych <= '=') goto yy719;
+ goto yy621;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '?') goto yy620;
+ if (yych <= '@') goto yy524;
+ if (yych <= 'Z') goto yy619;
+ goto yy524;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy622;
+ goto yy524;
+ } else {
+ if (yych == '`') goto yy524;
+ goto yy619;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy524;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy623;
+ goto yy624;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy625;
+ goto yy626;
+ } else {
+ if (yych <= 0xF3) goto yy627;
+ if (yych <= 0xF4) goto yy628;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy620:
+ yych = *++YYCURSOR;
+ if (yych <= '[') {
+ if (yych <= '/') {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '!') goto yy524;
+ goto yy522;
+ } else {
+ if (yych == '-') goto yy619;
+ if (yych <= '.') goto yy524;
+ goto yy620;
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy619;
+ if (yych <= '=') goto yy524;
+ goto yy720;
+ } else {
+ if (yych <= '?') goto yy620;
+ if (yych <= '@') goto yy524;
+ if (yych <= 'Z') goto yy619;
+ goto yy524;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '_') {
+ if (yych <= '\\') goto yy622;
+ if (yych <= '^') goto yy524;
+ goto yy619;
+ } else {
+ if (yych <= '`') goto yy524;
+ if (yych <= 'z') goto yy619;
+ if (yych <= 0x7F) goto yy524;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy623;
+ if (yych <= 0xE0) goto yy624;
+ goto yy625;
+ } else {
+ if (yych <= 0xF0) goto yy626;
+ if (yych <= 0xF3) goto yy627;
+ if (yych <= 0xF4) goto yy628;
+ goto yy1;
+ }
+ }
+ }
+yy621:
+ yych = *++YYCURSOR;
+ if (yych <= 0xC1) {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '!') goto yy621;
+ goto yy721;
+ } else {
+ if (yych == '\\') goto yy722;
+ if (yych <= 0x7F) goto yy621;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy723;
+ if (yych <= 0xE0) goto yy724;
+ goto yy725;
+ } else {
+ if (yych <= 0xF0) goto yy726;
+ if (yych <= 0xF3) goto yy727;
+ if (yych <= 0xF4) goto yy728;
+ goto yy1;
+ }
+ }
+yy622:
+ yych = *++YYCURSOR;
+ if (yych <= 'Z') {
+ if (yych <= '.') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy621;
+ if (yych <= '\t') goto yy524;
+ goto yy522;
+ } else {
+ if (yych == '-') goto yy619;
+ goto yy524;
+ }
+ } else {
+ if (yych <= '=') {
+ if (yych <= '/') goto yy620;
+ if (yych <= ':') goto yy619;
+ goto yy524;
+ } else {
+ if (yych <= '>') goto yy621;
+ if (yych <= '?') goto yy620;
+ if (yych <= '@') goto yy524;
+ goto yy619;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '`') {
+ if (yych == '_') goto yy619;
+ goto yy524;
+ } else {
+ if (yych <= 'z') goto yy619;
+ if (yych <= 0x7F) goto yy524;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy623;
+ if (yych <= 0xE0) goto yy624;
+ goto yy625;
+ } else {
+ if (yych <= 0xF0) goto yy626;
+ if (yych <= 0xF3) goto yy627;
+ if (yych <= 0xF4) goto yy628;
+ goto yy1;
+ }
+ }
+ }
+yy623:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy524;
+ goto yy1;
+yy624:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy623;
+ goto yy1;
+yy625:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy623;
+ goto yy1;
+yy626:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy625;
+ goto yy1;
+yy627:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy625;
+ goto yy1;
+yy628:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy625;
+ goto yy1;
+yy629:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '&') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy629;
+ goto yy525;
+ } else {
+ if (yych <= '\r') goto yy629;
+ if (yych == ' ') goto yy629;
+ goto yy525;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '\'') goto yy522;
+ if (yych == '-') goto yy629;
+ goto yy525;
+ } else {
+ if (yych <= ':') {
+ if (yych >= '0') goto yy629;
+ } else {
+ if (yych <= '<') goto yy525;
+ if (yych <= '=') goto yy729;
+ goto yy631;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '?') goto yy630;
+ if (yych <= '@') goto yy525;
+ if (yych <= 'Z') goto yy629;
+ goto yy525;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy632;
+ goto yy525;
+ } else {
+ if (yych == '`') goto yy525;
+ goto yy629;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy525;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy633;
+ goto yy634;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy635;
+ goto yy636;
+ } else {
+ if (yych <= 0xF3) goto yy637;
+ if (yych <= 0xF4) goto yy638;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy630:
+ yych = *++YYCURSOR;
+ if (yych <= '[') {
+ if (yych <= '/') {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '&') goto yy525;
+ goto yy522;
+ } else {
+ if (yych == '-') goto yy629;
+ if (yych <= '.') goto yy525;
+ goto yy630;
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy629;
+ if (yych <= '=') goto yy525;
+ goto yy730;
+ } else {
+ if (yych <= '?') goto yy630;
+ if (yych <= '@') goto yy525;
+ if (yych <= 'Z') goto yy629;
+ goto yy525;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '_') {
+ if (yych <= '\\') goto yy632;
+ if (yych <= '^') goto yy525;
+ goto yy629;
+ } else {
+ if (yych <= '`') goto yy525;
+ if (yych <= 'z') goto yy629;
+ if (yych <= 0x7F) goto yy525;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy633;
+ if (yych <= 0xE0) goto yy634;
+ goto yy635;
+ } else {
+ if (yych <= 0xF0) goto yy636;
+ if (yych <= 0xF3) goto yy637;
+ if (yych <= 0xF4) goto yy638;
+ goto yy1;
+ }
+ }
+ }
+yy631:
+ yych = *++YYCURSOR;
+ if (yych <= 0xC1) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '&') goto yy631;
+ goto yy721;
+ } else {
+ if (yych == '\\') goto yy731;
+ if (yych <= 0x7F) goto yy631;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy732;
+ if (yych <= 0xE0) goto yy733;
+ goto yy734;
+ } else {
+ if (yych <= 0xF0) goto yy735;
+ if (yych <= 0xF3) goto yy736;
+ if (yych <= 0xF4) goto yy737;
+ goto yy1;
+ }
+ }
+yy632:
+ yych = *++YYCURSOR;
+ if (yych <= 'Z') {
+ if (yych <= '.') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy631;
+ if (yych <= '\t') goto yy525;
+ goto yy522;
+ } else {
+ if (yych == '-') goto yy629;
+ goto yy525;
+ }
+ } else {
+ if (yych <= '=') {
+ if (yych <= '/') goto yy630;
+ if (yych <= ':') goto yy629;
+ goto yy525;
+ } else {
+ if (yych <= '>') goto yy631;
+ if (yych <= '?') goto yy630;
+ if (yych <= '@') goto yy525;
+ goto yy629;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '`') {
+ if (yych == '_') goto yy629;
+ goto yy525;
+ } else {
+ if (yych <= 'z') goto yy629;
+ if (yych <= 0x7F) goto yy525;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy633;
+ if (yych <= 0xE0) goto yy634;
+ goto yy635;
+ } else {
+ if (yych <= 0xF0) goto yy636;
+ if (yych <= 0xF3) goto yy637;
+ if (yych <= 0xF4) goto yy638;
+ goto yy1;
+ }
+ }
+ }
+yy633:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy525;
+ goto yy1;
+yy634:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy633;
+ goto yy1;
+yy635:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy633;
+ goto yy1;
+yy636:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy635;
+ goto yy1;
+yy637:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy635;
+ goto yy1;
+yy638:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy635;
+ goto yy1;
+yy639:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy738;
+ goto yy148;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ if (yych <= '?') goto yy4;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy738;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy738;
+ if (yych <= 'z') goto yy115;
+ goto yy4;
+ }
+ }
+ }
+yy640:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy739;
+ goto yy1;
+yy641:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '+') goto yy80;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '9') {
+ if (yych <= '-') goto yy46;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ if (yych <= '/') goto yy4;
+ goto yy738;
+ } else {
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '>') goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy738;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy740;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+ }
+yy642:
+ ++YYCURSOR;
+ yyt1 = yyt2;
+yy643:
+ YYCURSOR = yyt1;
+#line 173 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_TIME); }
+#line 20685 "../../lnav/src/data_scanner_re.cc"
+yy644:
+ yyaccept = 29;
+ yych = *(YYMARKER = ++YYCURSOR);
+ yyt1 = yyt2;
+ if (yych <= '/') goto yy643;
+ if (yych <= '9') goto yy741;
+ goto yy643;
+yy645:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy642;
+ goto yy1;
+yy646:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy645;
+ goto yy1;
+yy647:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy645;
+ goto yy1;
+yy648:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy647;
+ goto yy1;
+yy649:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy647;
+ goto yy1;
+yy650:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy647;
+ goto yy1;
+yy651:
+ yych = *++YYCURSOR;
+ if (yych == 'm') goto yy742;
+ goto yy100;
+yy652:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 16) {
+ goto yy101;
+ }
+ if (yych <= '\'') {
+ if (yych <= 0x1A) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy24;
+ if (yych <= 0x08) goto yy4;
+ goto yy24;
+ } else {
+ if (yych == '\r') goto yy24;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ' ') {
+ if (yych <= 0x1B) goto yy24;
+ if (yych <= 0x1F) goto yy4;
+ goto yy743;
+ } else {
+ if (yych == '"') goto yy24;
+ if (yych <= '&') goto yy99;
+ goto yy24;
+ }
+ }
+ } else {
+ if (yych <= ']') {
+ if (yych <= ':') {
+ if (yych <= '*') goto yy99;
+ if (yych <= ',') goto yy24;
+ goto yy4;
+ } else {
+ if (yych == '\\') goto yy99;
+ goto yy24;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') goto yy99;
+ if (yych <= '}') goto yy24;
+ goto yy99;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy24;
+ if (yych <= 0xF4) goto yy4;
+ goto yy24;
+ }
+ }
+ }
+yy653:
+ ++YYCURSOR;
+yy654:
+ YYCURSOR = yyt1;
+#line 230 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_IPV4_ADDRESS);
+ }
+#line 20785 "../../lnav/src/data_scanner_re.cc"
+yy655:
+ yyaccept = 30;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy654;
+ goto yy4;
+ } else {
+ if (yych <= '\n') goto yy654;
+ if (yych <= '\f') goto yy4;
+ goto yy654;
+ }
+ } else {
+ if (yych <= 0x1F) {
+ if (yych == 0x1B) goto yy654;
+ goto yy4;
+ } else {
+ if (yych == '$') goto yy4;
+ goto yy654;
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy654;
+ goto yy4;
+ } else {
+ if (yych == '_') goto yy4;
+ goto yy654;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy4;
+ if (yych <= '~') goto yy654;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy654;
+ if (yych <= 0xF4) goto yy4;
+ goto yy654;
+ }
+ }
+ }
+yy656:
+ yyaccept = 30;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '*') {
+ if (yych == '%') goto yy81;
+ goto yy654;
+ } else {
+ if (yych == ',') goto yy654;
+ if (yych <= '.') goto yy81;
+ goto yy654;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '9') goto yy81;
+ if (yych <= '?') goto yy654;
+ if (yych <= 'Z') goto yy81;
+ goto yy654;
+ } else {
+ if (yych == '`') goto yy654;
+ if (yych <= 'z') goto yy81;
+ goto yy654;
+ }
+ }
+yy657:
+ yyaccept = 30;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy654;
+ if (yych <= 0x08) goto yy4;
+ goto yy654;
+ } else {
+ if (yych == '\r') goto yy654;
+ if (yych <= 0x1A) goto yy4;
+ goto yy654;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy654;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy654;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy344;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy654;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy344;
+ goto yy654;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy654;
+ if (yych <= 'z') goto yy344;
+ goto yy654;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy654;
+ if (yych <= 0xF4) goto yy4;
+ goto yy654;
+ }
+ }
+ }
+yy658:
+ yyaccept = 30;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy654;
+ if (yych <= 0x08) goto yy4;
+ goto yy654;
+ } else {
+ if (yych == '\r') goto yy654;
+ if (yych <= 0x1A) goto yy4;
+ goto yy654;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy654;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy654;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy447;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy654;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy654;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy654;
+ if (yych <= 'z') goto yy46;
+ goto yy654;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy654;
+ if (yych <= 0xF4) goto yy4;
+ goto yy654;
+ }
+ }
+ }
+yy659:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x08) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '\n') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1B) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x1F) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy656;
+ }
+ if (yych <= '*') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy656;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy657;
+ } else {
+ if (yych <= '.') {
+ yyt1 = YYCURSOR;
+ goto yy658;
+ }
+ if (yych <= '/') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ goto yy447;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= 'Z') {
+ if (yych <= ':') {
+ yyt1 = YYCURSOR;
+ goto yy660;
+ }
+ if (yych <= '?') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= '@') {
+ yyt1 = YYCURSOR;
+ goto yy661;
+ }
+ yyt1 = YYCURSOR;
+ goto yy662;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy662;
+ } else {
+ if (yych <= '`') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 'z') {
+ yyt1 = YYCURSOR;
+ goto yy662;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xDF) {
+ yyt1 = YYCURSOR;
+ goto yy663;
+ }
+ yyt1 = YYCURSOR;
+ goto yy664;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy665;
+ }
+ yyt1 = YYCURSOR;
+ goto yy666;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy667;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy668;
+ }
+ goto yy345;
+ }
+ }
+ }
+ }
+yy660:
+ yyaccept = 30;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == ':') goto yy152;
+ goto yy654;
+yy661:
+ yyaccept = 30;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1A) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy654;
+ if (yych <= 0x08) goto yy4;
+ goto yy654;
+ } else {
+ if (yych == '\r') goto yy654;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '#') {
+ if (yych <= 0x1B) goto yy654;
+ if (yych <= 0x1F) goto yy4;
+ goto yy654;
+ } else {
+ if (yych <= '$') goto yy4;
+ if (yych <= ',') goto yy654;
+ if (yych <= '.') goto yy174;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '?') {
+ if (yych <= '9') goto yy174;
+ if (yych <= ':') goto yy4;
+ goto yy654;
+ } else {
+ if (yych <= '@') goto yy4;
+ if (yych <= 'Z') goto yy174;
+ if (yych <= '^') goto yy654;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy654;
+ if (yych <= 'z') goto yy174;
+ goto yy654;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy654;
+ if (yych <= 0xF4) goto yy4;
+ goto yy654;
+ }
+ }
+ }
+yy662:
+ yyaccept = 30;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '-') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy654;
+ if (yych <= 0x08) goto yy4;
+ goto yy654;
+ } else {
+ if (yych == '\r') goto yy654;
+ if (yych <= 0x1A) goto yy4;
+ goto yy654;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy654;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy654;
+ goto yy255;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '.') goto yy350;
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy447;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy654;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy447;
+ goto yy654;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy654;
+ if (yych <= 'z') goto yy447;
+ goto yy654;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy654;
+ if (yych <= 0xF4) goto yy4;
+ goto yy654;
+ }
+ }
+ }
+yy663:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy655;
+ goto yy1;
+yy664:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy663;
+ goto yy1;
+yy665:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy663;
+ goto yy1;
+yy666:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy665;
+ goto yy1;
+yy667:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy665;
+ goto yy1;
+yy668:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy665;
+ goto yy1;
+yy669:
+ yyaccept = 20;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '5') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x08) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '\n') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0x1B) {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ if (yych <= 0x1F) {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy656;
+ }
+ if (yych <= '*') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy656;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy657;
+ } else {
+ if (yych <= '.') {
+ yyt1 = YYCURSOR;
+ goto yy658;
+ }
+ if (yych <= '/') {
+ yyt1 = YYCURSOR;
+ goto yy655;
+ }
+ goto yy659;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy447;
+ if (yych <= ':') {
+ yyt1 = YYCURSOR;
+ goto yy660;
+ }
+ if (yych <= '?') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy661;
+ } else {
+ if (yych <= '^') {
+ if (yych <= 'Z') {
+ yyt1 = YYCURSOR;
+ goto yy662;
+ }
+ yyt1 = YYCURSOR;
+ goto yy653;
+ } else {
+ if (yych == '`') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy662;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') {
+ yyt1 = YYCURSOR;
+ goto yy653;
+ }
+ yyt1 = YYCURSOR;
+ goto yy655;
+ } else {
+ if (yych <= 0xC1) goto yy345;
+ if (yych <= 0xDF) {
+ yyt1 = YYCURSOR;
+ goto yy663;
+ }
+ yyt1 = YYCURSOR;
+ goto yy664;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy665;
+ }
+ yyt1 = YYCURSOR;
+ goto yy666;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy667;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy668;
+ }
+ goto yy345;
+ }
+ }
+ }
+ }
+yy670:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '-') goto yy466;
+ if (yych == ':') goto yy466;
+ goto yy354;
+yy671:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy744;
+ goto yy4;
+yy672:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy745;
+ goto yy1;
+yy673:
+ yyaccept = 31;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy674;
+ if (yych <= 0x08) goto yy4;
+ if (yych >= '\v') goto yy4;
+ } else {
+ if (yych <= '\r') goto yy674;
+ if (yych != 0x1B) goto yy4;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= ' ') goto yy746;
+ if (yych <= '#') goto yy674;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych >= '-') goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy747;
+ if (yych <= ':') goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych == 'T') goto yy748;
+ goto yy46;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych == '_') goto yy46;
+ if (yych >= 'a') goto yy46;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych >= 0x7F) goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy674;
+ if (yych <= 0xF4) goto yy4;
+ }
+ }
+ }
+ }
+yy674:
+#line 187 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_DATE);
+ }
+#line 21464 "../../lnav/src/data_scanner_re.cc"
+yy675:
+ yyaccept = 31;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy674;
+ if (yych <= 0x08) goto yy4;
+ goto yy674;
+ } else {
+ if (yych == '\r') goto yy674;
+ if (yych <= 0x1A) goto yy4;
+ goto yy674;
+ }
+ } else {
+ if (yych <= '#') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= ' ') goto yy746;
+ goto yy674;
+ } else {
+ if (yych <= '$') goto yy4;
+ if (yych <= ',') goto yy674;
+ if (yych <= '/') goto yy4;
+ goto yy749;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'S') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy674;
+ goto yy4;
+ } else {
+ if (yych <= 'T') goto yy750;
+ if (yych <= 'Z') goto yy4;
+ if (yych <= '^') goto yy674;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy674;
+ if (yych <= 'z') goto yy4;
+ goto yy674;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy674;
+ if (yych <= 0xF4) goto yy4;
+ goto yy674;
+ }
+ }
+ }
+yy676:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy751;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy752;
+ goto yy753;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy677:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy751;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy753;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy678:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy751;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy196;
+ if (yych <= ':') goto yy148;
+ goto yy113;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy196;
+ if (yych <= 'Z') goto yy115;
+ goto yy113;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy679:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy194;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy755;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy196;
+ if (yych <= ':') goto yy148;
+ goto yy113;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy196;
+ if (yych <= 'Z') goto yy115;
+ goto yy113;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy680:
+ yyaccept = 18;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = yyt1;
+ goto yy199;
+ }
+ if (yych <= '9') goto yy553;
+ yyt2 = yyt1;
+ goto yy199;
+ } else {
+ if (yych <= 'Z') goto yy553;
+ if (yych <= '`') {
+ yyt2 = yyt1;
+ goto yy199;
+ }
+ if (yych <= 'z') goto yy553;
+ yyt2 = yyt1;
+ goto yy199;
+ }
+yy681:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy756;
+ if (yych >= ';') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy756;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy756;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy682:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy757;
+ if (yych <= ':') goto yy152;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy757;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy757;
+ goto yy1;
+ }
+yy683:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '#') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '9') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ goto yy756;
+ } else {
+ if (yych <= ':') goto yy682;
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '_') {
+ if (yych <= 'F') goto yy756;
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy756;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy684:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy758;
+ if (yych <= ':') goto yy682;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy756;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy756;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy685:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '5') goto yy758;
+ if (yych <= '9') goto yy756;
+ goto yy682;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy756;
+ goto yy3;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy756;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= '~') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy686:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '`') {
+ if (yych <= '/') {
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= '-') {
+ yyt1 = YYCURSOR;
+ goto yy759;
+ }
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy644;
+ }
+ yyt2 = YYCURSOR;
+ goto yy642;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '9') {
+ yyt1 = YYCURSOR;
+ goto yy688;
+ }
+ goto yy760;
+ } else {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= 'F') {
+ yyt1 = YYCURSOR;
+ goto yy688;
+ }
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'f') {
+ yyt1 = YYCURSOR;
+ goto yy688;
+ }
+ yyt2 = YYCURSOR;
+ goto yy642;
+ } else {
+ if (yych <= 0xC1) goto yy354;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy645;
+ }
+ yyt2 = YYCURSOR;
+ goto yy646;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy647;
+ }
+ yyt2 = YYCURSOR;
+ goto yy648;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy649;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy650;
+ }
+ goto yy354;
+ }
+ }
+ }
+yy687:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy354;
+ goto yy466;
+ } else {
+ if (yych <= '/') goto yy354;
+ if (yych <= '9') goto yy561;
+ goto yy760;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy354;
+ goto yy561;
+ } else {
+ if (yych <= '`') goto yy354;
+ if (yych <= 'f') goto yy561;
+ goto yy354;
+ }
+ }
+yy688:
+ yyaccept = 29;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= '/') goto yy643;
+ if (yych <= '9') goto yy689;
+ if (yych <= ':') goto yy471;
+ goto yy643;
+ } else {
+ if (yych <= 'F') goto yy689;
+ if (yych <= '`') goto yy643;
+ if (yych >= 'g') goto yy643;
+ }
+yy689:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy471;
+ goto yy1;
+yy690:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy761;
+ if (yych >= ';') goto yy1;
+ } else {
+ if (yych <= 'F') goto yy761;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy761;
+ goto yy1;
+ }
+yy691:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy762;
+ if (yych <= ':') goto yy763;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy762;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy762;
+ goto yy1;
+ }
+yy692:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy764;
+ goto yy765;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy766;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy766;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy693:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy767;
+ goto yy765;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy766;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy766;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy694:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '5') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '4') goto yy767;
+ goto yy768;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy764;
+ if (yych <= ':') goto yy765;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy766;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy766;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy695:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy766;
+ if (yych <= ':') goto yy765;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy766;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy766;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy696:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy769;
+ if (yych <= ':') goto yy565;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy769;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy769;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy697:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy769;
+ goto yy565;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy769;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy769;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy698:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy770;
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy771;
+ goto yy4;
+yy699:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy770;
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy698;
+ goto yy4;
+yy700:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych == '.') goto yy770;
+ goto yy4;
+ } else {
+ if (yych <= '4') goto yy698;
+ if (yych <= '5') goto yy772;
+ if (yych <= '9') goto yy771;
+ goto yy4;
+ }
+yy701:
+ yych = *++YYCURSOR;
+ if (yych <= '1') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '0') goto yy773;
+ goto yy774;
+ } else {
+ if (yych <= '2') goto yy775;
+ if (yych <= '9') goto yy773;
+ goto yy1;
+ }
+yy702:
+ yych = *++YYCURSOR;
+ if (yych == '.') goto yy701;
+ goto yy1;
+yy703:
+ yych = *++YYCURSOR;
+ if (yych == '.') goto yy701;
+ if (yych <= '/') goto yy1;
+ if (yych <= '5') goto yy702;
+ goto yy1;
+yy704:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1;
+ if (yych <= ':') goto yy486;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy705:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy776;
+ goto yy777;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy485;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy485;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy706:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy778;
+ goto yy486;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy485;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy485;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy707:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '5') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '4') goto yy778;
+ goto yy779;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy776;
+ if (yych <= ':') goto yy486;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy485;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy485;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy708:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy776;
+ goto yy486;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy485;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy485;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy709:
+ yyaccept = 27;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[0+yych] & 128) {
+ goto yy780;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy310;
+ if (yych <= '"') goto yy781;
+ goto yy782;
+ } else {
+ if (yych <= '\\') goto yy783;
+ if (yych <= 0xC1) goto yy310;
+ goto yy784;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy785;
+ if (yych <= 0xEF) goto yy786;
+ goto yy787;
+ } else {
+ if (yych <= 0xF3) goto yy788;
+ if (yych <= 0xF4) goto yy789;
+ goto yy310;
+ }
+ }
+yy710:
+ yych = *++YYCURSOR;
+ if (yych <= 0xC1) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy780;
+ if (yych <= '\t') goto yy579;
+ goto yy490;
+ } else {
+ if (yych == '>') goto yy709;
+ if (yych <= 0x7F) goto yy579;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy711;
+ if (yych <= 0xE0) goto yy712;
+ goto yy713;
+ } else {
+ if (yych <= 0xF0) goto yy714;
+ if (yych <= 0xF3) goto yy715;
+ if (yych <= 0xF4) goto yy716;
+ goto yy1;
+ }
+ }
+yy711:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy579;
+ goto yy1;
+yy712:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy711;
+ goto yy1;
+yy713:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy711;
+ goto yy1;
+yy714:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy713;
+ goto yy1;
+yy715:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy713;
+ goto yy1;
+yy716:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy713;
+ goto yy1;
+yy717:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 64) {
+ goto yy717;
+ }
+ if (yych <= 'Z') {
+ if (yych <= '-') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '"') goto yy413;
+ if (yych <= '\'') goto yy412;
+ goto yy790;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy791;
+ goto yy790;
+ } else {
+ if (yych <= '>') goto yy792;
+ if (yych <= '?') goto yy791;
+ goto yy790;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 'z') {
+ if (yych <= '\\') goto yy793;
+ goto yy790;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy794;
+ goto yy795;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy796;
+ goto yy797;
+ } else {
+ if (yych <= 0xF3) goto yy798;
+ if (yych <= 0xF4) goto yy799;
+ goto yy1;
+ }
+ }
+ }
+yy718:
+ yych = *++YYCURSOR;
+ if (yych <= ' ') {
+ if (yych <= '\f') {
+ if (yych == '\t') goto yy718;
+ goto yy1;
+ } else {
+ if (yych <= '\r') goto yy718;
+ if (yych <= 0x1F) goto yy1;
+ goto yy718;
+ }
+ } else {
+ if (yych <= '=') {
+ if (yych == '/') goto yy215;
+ goto yy1;
+ } else {
+ if (yych <= '>') goto yy216;
+ if (yych <= '?') goto yy215;
+ goto yy1;
+ }
+ }
+yy719:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '&') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy719;
+ goto yy524;
+ } else {
+ if (yych <= '\r') goto yy719;
+ if (yych == ' ') goto yy719;
+ goto yy524;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '\'') goto yy800;
+ if (yych == '-') goto yy619;
+ goto yy524;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy620;
+ goto yy619;
+ } else {
+ if (yych <= '=') goto yy524;
+ if (yych <= '>') goto yy621;
+ goto yy620;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy524;
+ if (yych <= 'Z') goto yy619;
+ if (yych <= '[') goto yy524;
+ goto yy622;
+ } else {
+ if (yych == '_') goto yy619;
+ if (yych <= '`') goto yy524;
+ goto yy619;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy524;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy623;
+ goto yy624;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy625;
+ goto yy626;
+ } else {
+ if (yych <= 0xF3) goto yy627;
+ if (yych <= 0xF4) goto yy628;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy720:
+ yyaccept = 28;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0xC1) {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy315;
+ if (yych <= '!') goto yy621;
+ } else {
+ if (yych == '\\') goto yy722;
+ if (yych <= 0x7F) goto yy621;
+ goto yy315;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy723;
+ if (yych <= 0xE0) goto yy724;
+ goto yy725;
+ } else {
+ if (yych <= 0xF0) goto yy726;
+ if (yych <= 0xF3) goto yy727;
+ if (yych <= 0xF4) goto yy728;
+ goto yy315;
+ }
+ }
+yy721:
+ yych = *++YYCURSOR;
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy801;
+ } else {
+ if (yych == '\r') goto yy801;
+ goto yy1;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= ' ') goto yy801;
+ goto yy1;
+ } else {
+ if (yych <= '-') goto yy420;
+ if (yych <= '.') goto yy1;
+ goto yy215;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '>') {
+ if (yych <= ':') goto yy420;
+ goto yy1;
+ } else {
+ if (yych <= '?') goto yy215;
+ if (yych <= '@') goto yy1;
+ goto yy420;
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy1;
+ goto yy420;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'z') goto yy420;
+ goto yy1;
+ }
+ }
+ }
+yy722:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy621;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy621;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy724;
+ if (yych <= 0xEF) goto yy725;
+ goto yy726;
+ } else {
+ if (yych <= 0xF3) goto yy727;
+ if (yych <= 0xF4) goto yy728;
+ goto yy1;
+ }
+ }
+yy723:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy621;
+ goto yy1;
+yy724:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy723;
+ goto yy1;
+yy725:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy723;
+ goto yy1;
+yy726:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy725;
+ goto yy1;
+yy727:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy725;
+ goto yy1;
+yy728:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy725;
+ goto yy1;
+yy729:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy729;
+ goto yy525;
+ } else {
+ if (yych <= '\r') goto yy729;
+ if (yych == ' ') goto yy729;
+ goto yy525;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '"') goto yy800;
+ if (yych == '-') goto yy629;
+ goto yy525;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy630;
+ goto yy629;
+ } else {
+ if (yych <= '=') goto yy525;
+ if (yych <= '>') goto yy631;
+ goto yy630;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy525;
+ if (yych <= 'Z') goto yy629;
+ if (yych <= '[') goto yy525;
+ goto yy632;
+ } else {
+ if (yych == '_') goto yy629;
+ if (yych <= '`') goto yy525;
+ goto yy629;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy525;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy633;
+ goto yy634;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy635;
+ goto yy636;
+ } else {
+ if (yych <= 0xF3) goto yy637;
+ if (yych <= 0xF4) goto yy638;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy730:
+ yyaccept = 28;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0xC1) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy315;
+ if (yych <= '&') goto yy631;
+ goto yy721;
+ } else {
+ if (yych == '\\') goto yy731;
+ if (yych <= 0x7F) goto yy631;
+ goto yy315;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy732;
+ if (yych <= 0xE0) goto yy733;
+ goto yy734;
+ } else {
+ if (yych <= 0xF0) goto yy735;
+ if (yych <= 0xF3) goto yy736;
+ if (yych <= 0xF4) goto yy737;
+ goto yy315;
+ }
+ }
+yy731:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy631;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy631;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy733;
+ if (yych <= 0xEF) goto yy734;
+ goto yy735;
+ } else {
+ if (yych <= 0xF3) goto yy736;
+ if (yych <= 0xF4) goto yy737;
+ goto yy1;
+ }
+ }
+yy732:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy631;
+ goto yy1;
+yy733:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy732;
+ goto yy1;
+yy734:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy732;
+ goto yy1;
+yy735:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy734;
+ goto yy1;
+yy736:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy734;
+ goto yy1;
+yy737:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy734;
+ goto yy1;
+yy738:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 64) {
+ goto yy115;
+ }
+ if (yych <= '.') {
+ if (yych <= '*') {
+ if (yych == '%') goto yy80;
+ goto yy4;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy4;
+ if (yych <= '-') goto yy751;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= ':') goto yy148;
+ goto yy4;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych == '_') goto yy46;
+ goto yy4;
+ }
+ }
+yy739:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy802;
+ goto yy1;
+yy740:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '+') {
+ if (yych <= ' ') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x08) goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ } else {
+ if (yych == '\r') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= 0x1F) goto yy4;
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= '!') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '"') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '&') goto yy4;
+ if (yych <= '\'') {
+ yyt4 = YYCURSOR;
+ goto yy224;
+ }
+ if (yych <= '*') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy80;
+ }
+ }
+ } else {
+ if (yych <= '>') {
+ if (yych <= '/') {
+ if (yych <= ',') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '-') goto yy751;
+ if (yych <= '.') {
+ yyt4 = YYCURSOR;
+ goto yy230;
+ }
+ goto yy4;
+ } else {
+ if (yych <= '9') goto yy115;
+ if (yych <= ':') {
+ yyt3 = YYCURSOR;
+ goto yy234;
+ }
+ if (yych <= ';') {
+ yyt4 = YYCURSOR;
+ goto yy221;
+ }
+ goto yy4;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ yyt4 = YYCURSOR;
+ goto yy223;
+ }
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy115;
+ goto yy4;
+ } else {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy326;
+ goto yy4;
+ }
+ }
+ }
+yy741:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy803;
+ goto yy1;
+yy742:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych != ' ') goto yy100;
+yy743:
+ yych = *++YYCURSOR;
+ if (yych == 'F') goto yy804;
+ goto yy1;
+yy744:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy805;
+ goto yy4;
+yy745:
+ yych = *++YYCURSOR;
+ if (yych == ' ') goto yy806;
+ goto yy1;
+yy746:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy807;
+ goto yy1;
+yy747:
+ yyaccept = 31;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy674;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy674;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy674;
+ if (yych == 0x1B) goto yy674;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= ' ') goto yy746;
+ if (yych <= '#') goto yy674;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy674;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy46;
+ if (yych <= ':') goto yy4;
+ goto yy674;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych != 'T') goto yy46;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych == '_') goto yy46;
+ if (yych <= '`') goto yy674;
+ goto yy46;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy674;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy674;
+ if (yych <= 0xF4) goto yy4;
+ goto yy674;
+ }
+ }
+ }
+ }
+yy748:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy808;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+yy749:
+ yyaccept = 31;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= 0x1A) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy674;
+ if (yych <= 0x08) goto yy4;
+ goto yy674;
+ } else {
+ if (yych == '\r') goto yy674;
+ goto yy4;
+ }
+ } else {
+ if (yych <= ' ') {
+ if (yych <= 0x1B) goto yy674;
+ if (yych <= 0x1F) goto yy4;
+ goto yy746;
+ } else {
+ if (yych == '$') goto yy4;
+ if (yych <= ',') goto yy674;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'T') {
+ if (yych <= '?') goto yy674;
+ if (yych <= 'S') goto yy4;
+ } else {
+ if (yych <= 'Z') goto yy4;
+ if (yych <= '^') goto yy674;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy674;
+ if (yych <= 'z') goto yy4;
+ goto yy674;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy674;
+ if (yych <= 0xF4) goto yy4;
+ goto yy674;
+ }
+ }
+ }
+yy750:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy809;
+ goto yy4;
+yy751:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy810;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy810;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy810;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy752:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy811;
+ goto yy812;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy753:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy812;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych >= 'F') goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy754:
+ yyaccept = 12;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy113;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy113;
+ if (yych == 0x1B) goto yy113;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy113;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy113;
+ } else {
+ if (yych <= '+') goto yy194;
+ if (yych <= ',') goto yy113;
+ if (yych <= '-') goto yy251;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy196;
+ if (yych <= ':') goto yy148;
+ goto yy113;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy196;
+ if (yych <= 'Z') goto yy115;
+ goto yy113;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy113;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy113;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy113;
+ if (yych <= 0xF4) goto yy4;
+ goto yy113;
+ }
+ }
+ }
+ }
+yy755:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy813;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy810;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy810;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy756:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '#') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '9') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ goto yy814;
+ } else {
+ if (yych <= ':') goto yy682;
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '_') {
+ if (yych <= 'F') goto yy814;
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy814;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy757:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy815;
+ goto yy816;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy815;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy815;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy758:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') {
+ yyt2 = YYCURSOR;
+ goto yy376;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '9') goto yy814;
+ if (yych <= ':') goto yy682;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'F') goto yy814;
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'f') goto yy814;
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy759:
+ yyaccept = 29;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= '/') goto yy643;
+ if (yych <= '9') goto yy542;
+ goto yy643;
+ } else {
+ if (yych <= 'F') goto yy542;
+ if (yych <= '`') goto yy643;
+ if (yych <= 'f') goto yy542;
+ goto yy643;
+ }
+yy760:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy817;
+ if (yych <= ':') goto yy563;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy817;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy817;
+ goto yy1;
+ }
+yy761:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy818;
+ if (yych <= ':') goto yy691;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy818;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy818;
+ goto yy1;
+ }
+yy762:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy819;
+ if (yych <= ':') goto yy820;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy819;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy819;
+ goto yy1;
+ }
+yy763:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '0') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy821;
+ } else {
+ if (yych <= '1') goto yy822;
+ if (yych <= '2') goto yy823;
+ goto yy821;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy824;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy824;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy764:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy825;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy825;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy825;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy765:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy824;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy824;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy824;
+ goto yy1;
+ }
+yy766:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy825;
+ if (yych <= ':') goto yy765;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy825;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy825;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy767:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy826;
+ goto yy765;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy825;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy825;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy768:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '5') goto yy826;
+ goto yy825;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy765;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy825;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy825;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy769:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1;
+ if (yych <= ':') goto yy565;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy770:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '1') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '0') goto yy827;
+ goto yy828;
+ } else {
+ if (yych <= '2') goto yy829;
+ if (yych <= '9') goto yy827;
+ goto yy4;
+ }
+yy771:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy770;
+ goto yy4;
+yy772:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy770;
+ if (yych <= '/') goto yy4;
+ if (yych <= '5') goto yy771;
+ goto yy4;
+yy773:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy830;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy774:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy773;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy775:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '9') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '4') goto yy773;
+ if (yych <= '5') goto yy831;
+ goto yy830;
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy776:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy576;
+ goto yy486;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy576;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy576;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy777:
+ yych = *++YYCURSOR;
+ if (yych <= '9') {
+ if (yych <= '0') {
+ if (yych <= '/') goto yy1;
+ goto yy832;
+ } else {
+ if (yych <= '1') goto yy473;
+ if (yych <= '2') goto yy474;
+ goto yy472;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy1;
+ goto yy475;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy475;
+ goto yy1;
+ }
+ }
+yy778:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy833;
+ goto yy486;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy576;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy576;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy779:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '5') goto yy833;
+ goto yy576;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy486;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy576;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy576;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy780:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 128) {
+ goto yy780;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy1;
+ if (yych >= '#') goto yy782;
+ } else {
+ if (yych <= '\\') goto yy783;
+ if (yych <= 0xC1) goto yy1;
+ goto yy784;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy785;
+ if (yych <= 0xEF) goto yy786;
+ goto yy787;
+ } else {
+ if (yych <= 0xF3) goto yy788;
+ if (yych <= 0xF4) goto yy789;
+ goto yy1;
+ }
+ }
+yy781:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy834;
+ goto yy399;
+ } else {
+ if (yych <= '\r') goto yy834;
+ if (yych == ' ') goto yy834;
+ goto yy399;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '"') goto yy835;
+ if (yych == '\'') goto yy836;
+ goto yy399;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '-') goto yy837;
+ goto yy399;
+ } else {
+ if (yych <= ':') goto yy837;
+ if (yych <= '=') goto yy399;
+ goto yy588;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy399;
+ if (yych <= 'Z') goto yy837;
+ if (yych <= '[') goto yy399;
+ goto yy400;
+ } else {
+ if (yych == '_') goto yy837;
+ if (yych <= '`') goto yy399;
+ goto yy837;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy399;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy401;
+ goto yy402;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy403;
+ goto yy404;
+ } else {
+ if (yych <= 0xF3) goto yy405;
+ if (yych <= 0xF4) goto yy406;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy782:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy838;
+ goto yy391;
+ } else {
+ if (yych <= '\r') goto yy838;
+ if (yych == ' ') goto yy838;
+ goto yy391;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '"') goto yy839;
+ if (yych == '\'') goto yy840;
+ goto yy391;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '-') goto yy841;
+ goto yy391;
+ } else {
+ if (yych <= ':') goto yy841;
+ if (yych <= '=') goto yy391;
+ goto yy580;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy391;
+ if (yych <= 'Z') goto yy841;
+ if (yych <= '[') goto yy391;
+ goto yy392;
+ } else {
+ if (yych == '_') goto yy841;
+ if (yych <= '`') goto yy391;
+ goto yy841;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy391;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy393;
+ goto yy394;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy395;
+ goto yy396;
+ } else {
+ if (yych <= 0xF3) goto yy397;
+ if (yych <= 0xF4) goto yy398;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy783:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy780;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy780;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy785;
+ if (yych <= 0xEF) goto yy786;
+ goto yy787;
+ } else {
+ if (yych <= 0xF3) goto yy788;
+ if (yych <= 0xF4) goto yy789;
+ goto yy1;
+ }
+ }
+yy784:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy780;
+ goto yy1;
+yy785:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy784;
+ goto yy1;
+yy786:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy784;
+ goto yy1;
+yy787:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy786;
+ goto yy1;
+yy788:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy786;
+ goto yy1;
+yy789:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy786;
+ goto yy1;
+yy790:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= '"') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy790;
+ goto yy717;
+ } else {
+ if (yych <= 0x1F) {
+ if (yych <= '\r') goto yy790;
+ goto yy717;
+ } else {
+ if (yych <= ' ') goto yy790;
+ if (yych <= '!') goto yy717;
+ goto yy413;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '\'') goto yy412;
+ if (yych <= ',') goto yy717;
+ goto yy790;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') goto yy717;
+ } else {
+ if (yych <= ':') goto yy790;
+ if (yych <= '<') goto yy717;
+ goto yy842;
+ }
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= 'Z') {
+ if (yych <= '>') goto yy792;
+ if (yych <= '?') goto yy791;
+ if (yych <= '@') goto yy717;
+ goto yy790;
+ } else {
+ if (yych <= '\\') {
+ if (yych <= '[') goto yy717;
+ goto yy793;
+ } else {
+ if (yych == '_') goto yy790;
+ goto yy717;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy790;
+ goto yy717;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy794;
+ goto yy795;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy796;
+ goto yy797;
+ } else {
+ if (yych <= 0xF3) goto yy798;
+ if (yych <= 0xF4) goto yy799;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy791:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 64) {
+ goto yy717;
+ }
+ if (yych <= 'Z') {
+ if (yych <= '-') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '"') goto yy413;
+ if (yych <= '\'') goto yy412;
+ goto yy790;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy791;
+ goto yy790;
+ } else {
+ if (yych <= '>') goto yy843;
+ if (yych <= '?') goto yy791;
+ goto yy790;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 'z') {
+ if (yych <= '\\') goto yy793;
+ goto yy790;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy794;
+ goto yy795;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy796;
+ goto yy797;
+ } else {
+ if (yych <= 0xF3) goto yy798;
+ if (yych <= 0xF4) goto yy799;
+ goto yy1;
+ }
+ }
+ }
+yy792:
+ yyaccept = 26;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0x7F) {
+ if (yych <= '&') {
+ if (yych <= 0x00) goto yy217;
+ if (yych == '"') goto yy845;
+ goto yy844;
+ } else {
+ if (yych <= '\'') goto yy846;
+ if (yych == '\\') goto yy847;
+ goto yy844;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy217;
+ if (yych <= 0xDF) goto yy848;
+ if (yych <= 0xE0) goto yy849;
+ goto yy850;
+ } else {
+ if (yych <= 0xF0) goto yy851;
+ if (yych <= 0xF3) goto yy852;
+ if (yych <= 0xF4) goto yy853;
+ goto yy217;
+ }
+ }
+yy793:
+ yych = *++YYCURSOR;
+ if (yych <= 'Z') {
+ if (yych <= '.') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy844;
+ if (yych <= '\t') goto yy717;
+ goto yy410;
+ } else {
+ if (yych == '-') goto yy790;
+ goto yy717;
+ }
+ } else {
+ if (yych <= '=') {
+ if (yych <= '/') goto yy791;
+ if (yych <= ':') goto yy790;
+ goto yy717;
+ } else {
+ if (yych <= '>') goto yy792;
+ if (yych <= '?') goto yy791;
+ if (yych <= '@') goto yy717;
+ goto yy790;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '`') {
+ if (yych == '_') goto yy790;
+ goto yy717;
+ } else {
+ if (yych <= 'z') goto yy790;
+ if (yych <= 0x7F) goto yy717;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy794;
+ if (yych <= 0xE0) goto yy795;
+ goto yy796;
+ } else {
+ if (yych <= 0xF0) goto yy797;
+ if (yych <= 0xF3) goto yy798;
+ if (yych <= 0xF4) goto yy799;
+ goto yy1;
+ }
+ }
+ }
+yy794:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy717;
+ goto yy1;
+yy795:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy794;
+ goto yy1;
+yy796:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy794;
+ goto yy1;
+yy797:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy796;
+ goto yy1;
+yy798:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy796;
+ goto yy1;
+yy799:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy796;
+ goto yy1;
+yy800:
+ yych = *++YYCURSOR;
+ if (yych <= 'Z') {
+ if (yych <= '-') {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '!') goto yy800;
+ goto yy525;
+ } else {
+ if (yych == '\'') goto yy524;
+ if (yych <= ',') goto yy800;
+ goto yy854;
+ }
+ } else {
+ if (yych <= '=') {
+ if (yych <= '.') goto yy800;
+ if (yych <= '/') goto yy855;
+ if (yych <= ':') goto yy854;
+ goto yy800;
+ } else {
+ if (yych <= '>') goto yy856;
+ if (yych <= '?') goto yy855;
+ if (yych <= '@') goto yy800;
+ goto yy854;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '^') {
+ if (yych == '\\') goto yy857;
+ goto yy800;
+ } else {
+ if (yych == '`') goto yy800;
+ if (yych <= 'z') goto yy854;
+ goto yy800;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy858;
+ if (yych <= 0xE0) goto yy859;
+ goto yy860;
+ } else {
+ if (yych <= 0xF0) goto yy861;
+ if (yych <= 0xF3) goto yy862;
+ if (yych <= 0xF4) goto yy863;
+ goto yy1;
+ }
+ }
+ }
+yy801:
+ yych = *++YYCURSOR;
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x08) goto yy1;
+ goto yy801;
+ } else {
+ if (yych == '\r') goto yy801;
+ goto yy1;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych <= ' ') goto yy801;
+ if (yych <= '.') goto yy1;
+ goto yy215;
+ } else {
+ if (yych == '?') goto yy215;
+ goto yy1;
+ }
+ }
+yy802:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy864;
+ goto yy1;
+yy803:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy865;
+ goto yy1;
+yy804:
+ yych = *++YYCURSOR;
+ if (yych == 'i') goto yy866;
+ goto yy1;
+yy805:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy749;
+ goto yy4;
+yy806:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy867;
+ goto yy1;
+yy807:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy868;
+ goto yy1;
+yy808:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '9') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ goto yy869;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych == '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+yy809:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy870;
+ goto yy4;
+yy810:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy871;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy871;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy871;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy811:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy872;
+ goto yy873;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy812:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy873;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy813:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy874;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy871;
+ if (yych <= 'Z') goto yy46;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy26;
+ if (yych <= 'f') goto yy871;
+ if (yych <= 'z') goto yy46;
+ goto yy26;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+yy814:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '#') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '9') goto yy3;
+ if (yych <= ':') goto yy682;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy3;
+ if (yych <= '~') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ yyt2 = YYCURSOR;
+ goto yy291;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy292;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+yy815:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy875;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy875;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy875;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy816:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy876;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy876;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy876;
+ goto yy1;
+ }
+yy817:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy877;
+ if (yych <= ':') goto yy691;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy877;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy877;
+ goto yy1;
+ }
+yy818:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy691;
+ goto yy1;
+yy819:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy878;
+ if (yych >= ';') goto yy1;
+ } else {
+ if (yych <= 'F') goto yy878;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy878;
+ goto yy1;
+ }
+yy820:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy879;
+ if (yych <= ':') goto yy880;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy879;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy879;
+ goto yy1;
+ }
+yy821:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy881;
+ goto yy882;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy883;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy883;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy822:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy884;
+ goto yy882;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy883;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy883;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy823:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '5') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '4') goto yy884;
+ goto yy885;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy881;
+ if (yych <= ':') goto yy882;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy883;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy883;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy824:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy883;
+ if (yych <= ':') goto yy882;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy883;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy883;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy825:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy886;
+ if (yych <= ':') goto yy765;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy886;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy886;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy826:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy886;
+ goto yy765;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy886;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy886;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy827:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych == '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1A) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '9') goto yy887;
+ if (yych <= ':') goto yy62;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy3;
+ if (yych <= '~') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ yyt2 = YYCURSOR;
+ goto yy291;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy292;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+yy828:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych == '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1A) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '9') goto yy827;
+ if (yych <= ':') goto yy62;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'Z') goto yy3;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy3;
+ if (yych <= '~') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ yyt2 = YYCURSOR;
+ goto yy291;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy292;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+yy829:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '\n') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych == 0x1B) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '4') goto yy827;
+ if (yych <= '5') goto yy888;
+ if (yych <= '9') goto yy887;
+ goto yy62;
+ }
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'Z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '_') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy3;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 0xC1) goto yy5;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ yyt2 = YYCURSOR;
+ goto yy290;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy291;
+ }
+ yyt2 = YYCURSOR;
+ goto yy292;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy5;
+ }
+ }
+ }
+ }
+yy830:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy831:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '5') goto yy830;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy832:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy564;
+ goto yy889;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy566;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy566;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy833:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy704;
+ goto yy486;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy704;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy704;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy834:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy399;
+ goto yy834;
+ } else {
+ if (yych == '\r') goto yy834;
+ goto yy399;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy834;
+ if (yych <= '!') goto yy399;
+ } else {
+ if (yych == '\'') goto yy836;
+ goto yy399;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '\\') {
+ if (yych <= '>') goto yy588;
+ if (yych <= '[') goto yy399;
+ goto yy400;
+ } else {
+ if (yych <= 0x7F) goto yy399;
+ if (yych <= 0xC1) goto yy1;
+ goto yy401;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy402;
+ if (yych <= 0xEF) goto yy403;
+ goto yy404;
+ } else {
+ if (yych <= 0xF3) goto yy405;
+ if (yych <= 0xF4) goto yy406;
+ goto yy1;
+ }
+ }
+ }
+yy835:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 128) {
+ goto yy780;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '"') goto yy399;
+ goto yy782;
+ } else {
+ if (yych <= '\\') goto yy783;
+ if (yych <= 0xC1) goto yy1;
+ goto yy784;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy785;
+ if (yych <= 0xEF) goto yy786;
+ goto yy787;
+ } else {
+ if (yych <= 0xF3) goto yy788;
+ if (yych <= 0xF4) goto yy789;
+ goto yy1;
+ }
+ }
+yy836:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy834;
+ goto yy399;
+ } else {
+ if (yych <= '\r') goto yy834;
+ if (yych == ' ') goto yy834;
+ goto yy399;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '"') goto yy835;
+ if (yych == '\'') goto yy307;
+ goto yy399;
+ } else {
+ if (yych <= '/') {
+ if (yych >= '.') goto yy399;
+ } else {
+ if (yych <= ':') goto yy837;
+ if (yych <= '=') goto yy399;
+ goto yy588;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy399;
+ if (yych <= 'Z') goto yy837;
+ if (yych <= '[') goto yy399;
+ goto yy400;
+ } else {
+ if (yych == '_') goto yy837;
+ if (yych <= '`') goto yy399;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy399;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy401;
+ goto yy402;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy403;
+ goto yy404;
+ } else {
+ if (yych <= 0xF3) goto yy405;
+ if (yych <= 0xF4) goto yy406;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy837:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy890;
+ goto yy399;
+ } else {
+ if (yych <= '\r') goto yy890;
+ if (yych == ' ') goto yy890;
+ goto yy399;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '"') goto yy835;
+ if (yych == '\'') goto yy836;
+ goto yy399;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '-') goto yy837;
+ goto yy399;
+ } else {
+ if (yych <= ':') goto yy837;
+ if (yych <= '<') goto yy399;
+ goto yy891;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '>') goto yy588;
+ if (yych <= '@') goto yy399;
+ if (yych <= 'Z') goto yy837;
+ goto yy399;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy400;
+ goto yy399;
+ } else {
+ if (yych == '`') goto yy399;
+ goto yy837;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy399;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy401;
+ goto yy402;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy403;
+ goto yy404;
+ } else {
+ if (yych <= 0xF3) goto yy405;
+ if (yych <= 0xF4) goto yy406;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy838:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy391;
+ goto yy838;
+ } else {
+ if (yych == '\r') goto yy838;
+ goto yy391;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy838;
+ if (yych <= '!') goto yy391;
+ } else {
+ if (yych == '\'') goto yy840;
+ goto yy391;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '\\') {
+ if (yych <= '>') goto yy580;
+ if (yych <= '[') goto yy391;
+ goto yy392;
+ } else {
+ if (yych <= 0x7F) goto yy391;
+ if (yych <= 0xC1) goto yy1;
+ goto yy393;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy394;
+ if (yych <= 0xEF) goto yy395;
+ goto yy396;
+ } else {
+ if (yych <= 0xF3) goto yy397;
+ if (yych <= 0xF4) goto yy398;
+ goto yy1;
+ }
+ }
+ }
+yy839:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy838;
+ goto yy391;
+ } else {
+ if (yych <= '\r') goto yy838;
+ if (yych == ' ') goto yy838;
+ goto yy391;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '"') goto yy306;
+ if (yych != '\'') goto yy391;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '-') goto yy841;
+ goto yy391;
+ } else {
+ if (yych <= ':') goto yy841;
+ if (yych <= '=') goto yy391;
+ goto yy580;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy391;
+ if (yych <= 'Z') goto yy841;
+ if (yych <= '[') goto yy391;
+ goto yy392;
+ } else {
+ if (yych == '_') goto yy841;
+ if (yych <= '`') goto yy391;
+ goto yy841;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy391;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy393;
+ goto yy394;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy395;
+ goto yy396;
+ } else {
+ if (yych <= 0xF3) goto yy397;
+ if (yych <= 0xF4) goto yy398;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy840:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 128) {
+ goto yy780;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '\'') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '"') goto yy781;
+ goto yy391;
+ } else {
+ if (yych <= '\\') goto yy783;
+ if (yych <= 0xC1) goto yy1;
+ goto yy784;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy785;
+ if (yych <= 0xEF) goto yy786;
+ goto yy787;
+ } else {
+ if (yych <= 0xF3) goto yy788;
+ if (yych <= 0xF4) goto yy789;
+ goto yy1;
+ }
+ }
+yy841:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy892;
+ goto yy391;
+ } else {
+ if (yych <= '\r') goto yy892;
+ if (yych == ' ') goto yy892;
+ goto yy391;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '"') goto yy839;
+ if (yych == '\'') goto yy840;
+ goto yy391;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '-') goto yy841;
+ goto yy391;
+ } else {
+ if (yych <= ':') goto yy841;
+ if (yych <= '<') goto yy391;
+ goto yy893;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '>') goto yy580;
+ if (yych <= '@') goto yy391;
+ if (yych <= 'Z') goto yy841;
+ goto yy391;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy392;
+ goto yy391;
+ } else {
+ if (yych == '`') goto yy391;
+ goto yy841;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy391;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy393;
+ goto yy394;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy395;
+ goto yy396;
+ } else {
+ if (yych <= 0xF3) goto yy397;
+ if (yych <= 0xF4) goto yy398;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy842:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= ',') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy842;
+ goto yy717;
+ } else {
+ if (yych <= '\r') goto yy842;
+ if (yych == ' ') goto yy842;
+ goto yy717;
+ }
+ } else {
+ if (yych <= ':') {
+ if (yych <= '-') goto yy790;
+ if (yych <= '.') goto yy717;
+ if (yych <= '/') goto yy791;
+ goto yy790;
+ } else {
+ if (yych <= '=') goto yy717;
+ if (yych <= '>') goto yy792;
+ if (yych <= '?') goto yy791;
+ goto yy717;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '^') {
+ if (yych <= 'Z') goto yy790;
+ if (yych == '\\') goto yy793;
+ goto yy717;
+ } else {
+ if (yych == '`') goto yy717;
+ if (yych <= 'z') goto yy790;
+ goto yy717;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy794;
+ if (yych <= 0xE0) goto yy795;
+ goto yy796;
+ } else {
+ if (yych <= 0xF0) goto yy797;
+ if (yych <= 0xF3) goto yy798;
+ if (yych <= 0xF4) goto yy799;
+ goto yy1;
+ }
+ }
+ }
+yy843:
+ yyaccept = 28;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0x7F) {
+ if (yych <= '&') {
+ if (yych <= 0x00) goto yy315;
+ if (yych == '"') goto yy845;
+ } else {
+ if (yych <= '\'') goto yy846;
+ if (yych == '\\') goto yy847;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy315;
+ if (yych <= 0xDF) goto yy848;
+ if (yych <= 0xE0) goto yy849;
+ goto yy850;
+ } else {
+ if (yych <= 0xF0) goto yy851;
+ if (yych <= 0xF3) goto yy852;
+ if (yych <= 0xF4) goto yy853;
+ goto yy315;
+ }
+ }
+yy844:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '&') {
+ if (yych <= 0x00) goto yy1;
+ if (yych != '"') goto yy844;
+ } else {
+ if (yych <= '\'') goto yy846;
+ if (yych == '\\') goto yy847;
+ goto yy844;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy848;
+ if (yych <= 0xE0) goto yy849;
+ goto yy850;
+ } else {
+ if (yych <= 0xF0) goto yy851;
+ if (yych <= 0xF3) goto yy852;
+ if (yych <= 0xF4) goto yy853;
+ goto yy1;
+ }
+ }
+yy845:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '&') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy894;
+ goto yy609;
+ } else {
+ if (yych <= '\r') goto yy894;
+ if (yych == ' ') goto yy894;
+ goto yy609;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '\'') goto yy599;
+ if (yych == '-') goto yy895;
+ goto yy609;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy896;
+ goto yy895;
+ } else {
+ if (yych <= '=') goto yy609;
+ if (yych <= '>') goto yy513;
+ goto yy896;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy609;
+ if (yych <= 'Z') goto yy895;
+ if (yych <= '[') goto yy609;
+ goto yy610;
+ } else {
+ if (yych == '_') goto yy895;
+ if (yych <= '`') goto yy609;
+ goto yy895;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy609;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy611;
+ goto yy612;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy613;
+ goto yy614;
+ } else {
+ if (yych <= 0xF3) goto yy615;
+ if (yych <= 0xF4) goto yy616;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy846:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy897;
+ goto yy598;
+ } else {
+ if (yych <= '\r') goto yy897;
+ if (yych == ' ') goto yy897;
+ goto yy598;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '"') goto yy599;
+ if (yych == '-') goto yy898;
+ goto yy598;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy899;
+ goto yy898;
+ } else {
+ if (yych <= '=') goto yy598;
+ if (yych <= '>') goto yy503;
+ goto yy899;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy598;
+ if (yych <= 'Z') goto yy898;
+ if (yych <= '[') goto yy598;
+ goto yy600;
+ } else {
+ if (yych == '_') goto yy898;
+ if (yych <= '`') goto yy598;
+ goto yy898;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy598;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy601;
+ goto yy602;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy603;
+ goto yy604;
+ } else {
+ if (yych <= 0xF3) goto yy605;
+ if (yych <= 0xF4) goto yy606;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy847:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy844;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy844;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy849;
+ if (yych <= 0xEF) goto yy850;
+ goto yy851;
+ } else {
+ if (yych <= 0xF3) goto yy852;
+ if (yych <= 0xF4) goto yy853;
+ goto yy1;
+ }
+ }
+yy848:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy844;
+ goto yy1;
+yy849:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy848;
+ goto yy1;
+yy850:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy848;
+ goto yy1;
+yy851:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy850;
+ goto yy1;
+yy852:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy850;
+ goto yy1;
+yy853:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy850;
+ goto yy1;
+yy854:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= '"') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy854;
+ goto yy800;
+ } else {
+ if (yych <= 0x1F) {
+ if (yych <= '\r') goto yy854;
+ goto yy800;
+ } else {
+ if (yych <= ' ') goto yy854;
+ if (yych <= '!') goto yy800;
+ goto yy525;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych == '\'') goto yy524;
+ if (yych <= ',') goto yy800;
+ goto yy854;
+ } else {
+ if (yych <= '/') {
+ if (yych <= '.') goto yy800;
+ } else {
+ if (yych <= ':') goto yy854;
+ if (yych <= '<') goto yy800;
+ goto yy900;
+ }
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= 'Z') {
+ if (yych <= '>') goto yy856;
+ if (yych <= '?') goto yy855;
+ if (yych <= '@') goto yy800;
+ goto yy854;
+ } else {
+ if (yych <= '\\') {
+ if (yych <= '[') goto yy800;
+ goto yy857;
+ } else {
+ if (yych == '_') goto yy854;
+ goto yy800;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy854;
+ goto yy800;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy858;
+ goto yy859;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy860;
+ goto yy861;
+ } else {
+ if (yych <= 0xF3) goto yy862;
+ if (yych <= 0xF4) goto yy863;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy855:
+ yych = *++YYCURSOR;
+ if (yych <= 'Z') {
+ if (yych <= '-') {
+ if (yych <= '"') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '!') goto yy800;
+ goto yy525;
+ } else {
+ if (yych == '\'') goto yy524;
+ if (yych <= ',') goto yy800;
+ goto yy854;
+ }
+ } else {
+ if (yych <= '=') {
+ if (yych <= '.') goto yy800;
+ if (yych <= '/') goto yy855;
+ if (yych <= ':') goto yy854;
+ goto yy800;
+ } else {
+ if (yych <= '>') goto yy901;
+ if (yych <= '?') goto yy855;
+ if (yych <= '@') goto yy800;
+ goto yy854;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '^') {
+ if (yych == '\\') goto yy857;
+ goto yy800;
+ } else {
+ if (yych == '`') goto yy800;
+ if (yych <= 'z') goto yy854;
+ goto yy800;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy858;
+ if (yych <= 0xE0) goto yy859;
+ goto yy860;
+ } else {
+ if (yych <= 0xF0) goto yy861;
+ if (yych <= 0xF3) goto yy862;
+ if (yych <= 0xF4) goto yy863;
+ goto yy1;
+ }
+ }
+ }
+yy856:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '&') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '"') goto yy902;
+ goto yy856;
+ } else {
+ if (yych <= '\'') goto yy903;
+ if (yych == '\\') goto yy904;
+ goto yy856;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy905;
+ if (yych <= 0xE0) goto yy906;
+ goto yy907;
+ } else {
+ if (yych <= 0xF0) goto yy908;
+ if (yych <= 0xF3) goto yy909;
+ if (yych <= 0xF4) goto yy910;
+ goto yy1;
+ }
+ }
+yy857:
+ yych = *++YYCURSOR;
+ if (yych <= 'Z') {
+ if (yych <= '.') {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy856;
+ if (yych <= '\t') goto yy800;
+ goto yy522;
+ } else {
+ if (yych == '-') goto yy854;
+ goto yy800;
+ }
+ } else {
+ if (yych <= '=') {
+ if (yych <= '/') goto yy855;
+ if (yych <= ':') goto yy854;
+ goto yy800;
+ } else {
+ if (yych <= '>') goto yy856;
+ if (yych <= '?') goto yy855;
+ if (yych <= '@') goto yy800;
+ goto yy854;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '`') {
+ if (yych == '_') goto yy854;
+ goto yy800;
+ } else {
+ if (yych <= 'z') goto yy854;
+ if (yych <= 0x7F) goto yy800;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy858;
+ if (yych <= 0xE0) goto yy859;
+ goto yy860;
+ } else {
+ if (yych <= 0xF0) goto yy861;
+ if (yych <= 0xF3) goto yy862;
+ if (yych <= 0xF4) goto yy863;
+ goto yy1;
+ }
+ }
+ }
+yy858:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy800;
+ goto yy1;
+yy859:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy858;
+ goto yy1;
+yy860:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy858;
+ goto yy1;
+yy861:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy860;
+ goto yy1;
+yy862:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy860;
+ goto yy1;
+yy863:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy860;
+ goto yy1;
+yy864:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy911;
+ goto yy1;
+yy865:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= ':') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= '9') {
+ yyt1 = YYCURSOR;
+ goto yy912;
+ }
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy645;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy646;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy647;
+ }
+ yyt2 = YYCURSOR;
+ goto yy648;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy649;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy650;
+ }
+ goto yy1;
+ }
+ }
+yy866:
+ yych = *++YYCURSOR;
+ if (yych == 'l') goto yy913;
+ goto yy1;
+yy867:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy914;
+ goto yy1;
+yy868:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy915;
+ goto yy1;
+yy869:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[1024+yych] & 16) {
+ goto yy46;
+ }
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy4;
+ goto yy916;
+ } else {
+ if (yych == '@') goto yy96;
+ goto yy4;
+ }
+ }
+yy870:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == ':') goto yy916;
+ goto yy4;
+yy871:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy917;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy917;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy917;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy872:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy918;
+ goto yy919;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy873:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy919;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy874:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy920;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy917;
+ if (yych <= 'Z') goto yy46;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy26;
+ if (yych <= 'f') goto yy917;
+ if (yych <= 'z') goto yy46;
+ goto yy26;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+yy875:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy921;
+ goto yy816;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy921;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy921;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy876:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy922;
+ goto yy923;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy922;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy922;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy877:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy354;
+ goto yy466;
+ } else {
+ if (yych <= '/') goto yy354;
+ if (yych <= '9') goto yy761;
+ goto yy924;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy354;
+ goto yy761;
+ } else {
+ if (yych <= '`') goto yy354;
+ if (yych <= 'f') goto yy761;
+ goto yy354;
+ }
+ }
+yy878:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy925;
+ if (yych <= ':') goto yy820;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy925;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy925;
+ goto yy1;
+ }
+yy879:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy926;
+ if (yych <= ':') goto yy927;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy926;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy926;
+ goto yy1;
+ }
+yy880:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy928;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy928;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy928;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy881:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy929;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy929;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy929;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy882:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy928;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy928;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy928;
+ goto yy1;
+ }
+yy883:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy929;
+ if (yych <= ':') goto yy882;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy929;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy929;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy884:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy930;
+ goto yy882;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy929;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy929;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy885:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '5') goto yy930;
+ goto yy929;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy882;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy929;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy929;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy886:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1;
+ if (yych <= ':') goto yy765;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy887:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych == '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1A) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '_') {
+ if (yych <= 'Z') goto yy4;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy4;
+ if (yych <= '~') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy4;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ yyt2 = YYCURSOR;
+ goto yy291;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy292;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy4;
+ }
+ }
+ }
+yy888:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x08) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych == '\r') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0x1A) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= 0x1F) {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych == '$') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= '5') goto yy887;
+ if (yych <= ':') goto yy4;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ if (yych <= 'Z') goto yy4;
+ if (yych <= '^') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy4;
+ if (yych <= '~') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy284;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy4;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy289;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy290;
+ }
+ yyt2 = YYCURSOR;
+ goto yy291;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy292;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy293;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy294;
+ }
+ goto yy4;
+ }
+ }
+ }
+yy889:
+ yych = *++YYCURSOR;
+ if (yych <= '9') {
+ if (yych <= '0') {
+ if (yych <= '/') goto yy1;
+ goto yy931;
+ } else {
+ if (yych <= '1') goto yy693;
+ if (yych <= '2') goto yy694;
+ goto yy692;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy1;
+ goto yy695;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy695;
+ goto yy1;
+ }
+ }
+yy890:
+ yych = *++YYCURSOR;
+ if (yych <= '<') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy399;
+ goto yy890;
+ } else {
+ if (yych == '\r') goto yy890;
+ goto yy399;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy890;
+ if (yych <= '!') goto yy399;
+ goto yy835;
+ } else {
+ if (yych == '\'') goto yy836;
+ goto yy399;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '[') {
+ if (yych <= '=') goto yy891;
+ if (yych <= '>') goto yy588;
+ goto yy399;
+ } else {
+ if (yych <= '\\') goto yy400;
+ if (yych <= 0x7F) goto yy399;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy401;
+ if (yych <= 0xE0) goto yy402;
+ goto yy403;
+ } else {
+ if (yych <= 0xF0) goto yy404;
+ if (yych <= 0xF3) goto yy405;
+ if (yych <= 0xF4) goto yy406;
+ goto yy1;
+ }
+ }
+ }
+yy891:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 4) {
+ goto yy492;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '>') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '"') goto yy579;
+ goto yy399;
+ } else {
+ if (yych <= '\\') goto yy589;
+ if (yych <= 0xC1) goto yy1;
+ goto yy590;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy591;
+ if (yych <= 0xEF) goto yy592;
+ goto yy593;
+ } else {
+ if (yych <= 0xF3) goto yy594;
+ if (yych <= 0xF4) goto yy595;
+ goto yy1;
+ }
+ }
+yy892:
+ yych = *++YYCURSOR;
+ if (yych <= '<') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy391;
+ goto yy892;
+ } else {
+ if (yych == '\r') goto yy892;
+ goto yy391;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy892;
+ if (yych <= '!') goto yy391;
+ goto yy839;
+ } else {
+ if (yych == '\'') goto yy840;
+ goto yy391;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '[') {
+ if (yych <= '=') goto yy893;
+ if (yych <= '>') goto yy580;
+ goto yy391;
+ } else {
+ if (yych <= '\\') goto yy392;
+ if (yych <= 0x7F) goto yy391;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy393;
+ if (yych <= 0xE0) goto yy394;
+ goto yy395;
+ } else {
+ if (yych <= 0xF0) goto yy396;
+ if (yych <= 0xF3) goto yy397;
+ if (yych <= 0xF4) goto yy398;
+ goto yy1;
+ }
+ }
+ }
+yy893:
+ yych = *++YYCURSOR;
+ if (yybm[0+yych] & 2) {
+ goto yy491;
+ }
+ if (yych <= 0xDF) {
+ if (yych <= '>') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= '\'') goto yy579;
+ goto yy391;
+ } else {
+ if (yych <= '\\') goto yy581;
+ if (yych <= 0xC1) goto yy1;
+ goto yy582;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy583;
+ if (yych <= 0xEF) goto yy584;
+ goto yy585;
+ } else {
+ if (yych <= 0xF3) goto yy586;
+ if (yych <= 0xF4) goto yy587;
+ goto yy1;
+ }
+ }
+yy894:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy609;
+ goto yy894;
+ } else {
+ if (yych == '\r') goto yy894;
+ goto yy609;
+ }
+ } else {
+ if (yych <= '\'') {
+ if (yych <= ' ') goto yy894;
+ if (yych <= '&') goto yy609;
+ goto yy599;
+ } else {
+ if (yych == '/') goto yy896;
+ goto yy609;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '[') {
+ if (yych <= '>') goto yy513;
+ if (yych <= '?') goto yy896;
+ goto yy609;
+ } else {
+ if (yych <= '\\') goto yy610;
+ if (yych <= 0x7F) goto yy609;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy611;
+ if (yych <= 0xE0) goto yy612;
+ goto yy613;
+ } else {
+ if (yych <= 0xF0) goto yy614;
+ if (yych <= 0xF3) goto yy615;
+ if (yych <= 0xF4) goto yy616;
+ goto yy1;
+ }
+ }
+ }
+yy895:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '&') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy932;
+ goto yy609;
+ } else {
+ if (yych <= '\r') goto yy932;
+ if (yych == ' ') goto yy932;
+ goto yy609;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '\'') goto yy599;
+ if (yych == '-') goto yy895;
+ goto yy609;
+ } else {
+ if (yych <= ':') {
+ if (yych >= '0') goto yy895;
+ } else {
+ if (yych <= '<') goto yy609;
+ if (yych <= '=') goto yy933;
+ goto yy513;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '?') goto yy896;
+ if (yych <= '@') goto yy609;
+ if (yych <= 'Z') goto yy895;
+ goto yy609;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy610;
+ goto yy609;
+ } else {
+ if (yych == '`') goto yy609;
+ goto yy895;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy609;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy611;
+ goto yy612;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy613;
+ goto yy614;
+ } else {
+ if (yych <= 0xF3) goto yy615;
+ if (yych <= 0xF4) goto yy616;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy896:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '=') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\'') goto yy599;
+ goto yy609;
+ } else {
+ if (yych <= '>') goto yy608;
+ if (yych == '\\') goto yy610;
+ goto yy609;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy611;
+ if (yych <= 0xE0) goto yy612;
+ goto yy613;
+ } else {
+ if (yych <= 0xF0) goto yy614;
+ if (yych <= 0xF3) goto yy615;
+ if (yych <= 0xF4) goto yy616;
+ goto yy1;
+ }
+ }
+yy897:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy598;
+ goto yy897;
+ } else {
+ if (yych == '\r') goto yy897;
+ goto yy598;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy897;
+ if (yych <= '!') goto yy598;
+ goto yy599;
+ } else {
+ if (yych == '/') goto yy899;
+ goto yy598;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '[') {
+ if (yych <= '>') goto yy503;
+ if (yych <= '?') goto yy899;
+ goto yy598;
+ } else {
+ if (yych <= '\\') goto yy600;
+ if (yych <= 0x7F) goto yy598;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy601;
+ if (yych <= 0xE0) goto yy602;
+ goto yy603;
+ } else {
+ if (yych <= 0xF0) goto yy604;
+ if (yych <= 0xF3) goto yy605;
+ if (yych <= 0xF4) goto yy606;
+ goto yy1;
+ }
+ }
+ }
+yy898:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy934;
+ goto yy598;
+ } else {
+ if (yych <= '\r') goto yy934;
+ if (yych == ' ') goto yy934;
+ goto yy598;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '"') goto yy599;
+ if (yych == '-') goto yy898;
+ goto yy598;
+ } else {
+ if (yych <= ':') {
+ if (yych >= '0') goto yy898;
+ } else {
+ if (yych <= '<') goto yy598;
+ if (yych <= '=') goto yy935;
+ goto yy503;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '?') goto yy899;
+ if (yych <= '@') goto yy598;
+ if (yych <= 'Z') goto yy898;
+ goto yy598;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy600;
+ goto yy598;
+ } else {
+ if (yych == '`') goto yy598;
+ goto yy898;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy598;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy601;
+ goto yy602;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy603;
+ goto yy604;
+ } else {
+ if (yych <= 0xF3) goto yy605;
+ if (yych <= 0xF4) goto yy606;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy899:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '=') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '"') goto yy599;
+ goto yy598;
+ } else {
+ if (yych <= '>') goto yy597;
+ if (yych == '\\') goto yy600;
+ goto yy598;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy601;
+ if (yych <= 0xE0) goto yy602;
+ goto yy603;
+ } else {
+ if (yych <= 0xF0) goto yy604;
+ if (yych <= 0xF3) goto yy605;
+ if (yych <= 0xF4) goto yy606;
+ goto yy1;
+ }
+ }
+yy900:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= ',') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy900;
+ goto yy800;
+ } else {
+ if (yych <= '\r') goto yy900;
+ if (yych == ' ') goto yy900;
+ goto yy800;
+ }
+ } else {
+ if (yych <= ':') {
+ if (yych <= '-') goto yy854;
+ if (yych <= '.') goto yy800;
+ if (yych <= '/') goto yy855;
+ goto yy854;
+ } else {
+ if (yych <= '=') goto yy800;
+ if (yych <= '>') goto yy856;
+ if (yych <= '?') goto yy855;
+ goto yy800;
+ }
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '^') {
+ if (yych <= 'Z') goto yy854;
+ if (yych == '\\') goto yy857;
+ goto yy800;
+ } else {
+ if (yych == '`') goto yy800;
+ if (yych <= 'z') goto yy854;
+ goto yy800;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy858;
+ if (yych <= 0xE0) goto yy859;
+ goto yy860;
+ } else {
+ if (yych <= 0xF0) goto yy861;
+ if (yych <= 0xF3) goto yy862;
+ if (yych <= 0xF4) goto yy863;
+ goto yy1;
+ }
+ }
+ }
+yy901:
+ yyaccept = 28;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0x7F) {
+ if (yych <= '&') {
+ if (yych <= 0x00) goto yy315;
+ if (yych != '"') goto yy856;
+ } else {
+ if (yych <= '\'') goto yy903;
+ if (yych == '\\') goto yy904;
+ goto yy856;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy315;
+ if (yych <= 0xDF) goto yy905;
+ if (yych <= 0xE0) goto yy906;
+ goto yy907;
+ } else {
+ if (yych <= 0xF0) goto yy908;
+ if (yych <= 0xF3) goto yy909;
+ if (yych <= 0xF4) goto yy910;
+ goto yy315;
+ }
+ }
+yy902:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '&') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy936;
+ goto yy631;
+ } else {
+ if (yych <= '\r') goto yy936;
+ if (yych == ' ') goto yy936;
+ goto yy631;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '\'') goto yy721;
+ if (yych == '-') goto yy937;
+ goto yy631;
+ } else {
+ if (yych <= '/') goto yy938;
+ if (yych <= ':') goto yy937;
+ if (yych <= '>') goto yy631;
+ goto yy938;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy631;
+ if (yych <= 'Z') goto yy937;
+ if (yych <= '[') goto yy631;
+ goto yy731;
+ } else {
+ if (yych == '_') goto yy937;
+ if (yych <= '`') goto yy631;
+ goto yy937;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy631;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy732;
+ goto yy733;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy734;
+ goto yy735;
+ } else {
+ if (yych <= 0xF3) goto yy736;
+ if (yych <= 0xF4) goto yy737;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy903:
+ yych = *++YYCURSOR;
+ if (yych <= '?') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy939;
+ goto yy621;
+ } else {
+ if (yych <= '\r') goto yy939;
+ if (yych == ' ') goto yy939;
+ goto yy621;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '"') goto yy721;
+ if (yych == '-') goto yy940;
+ goto yy621;
+ } else {
+ if (yych <= '/') goto yy941;
+ if (yych <= ':') goto yy940;
+ if (yych <= '>') goto yy621;
+ goto yy941;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '\\') {
+ if (yych <= '@') goto yy621;
+ if (yych <= 'Z') goto yy940;
+ if (yych <= '[') goto yy621;
+ goto yy722;
+ } else {
+ if (yych == '_') goto yy940;
+ if (yych <= '`') goto yy621;
+ goto yy940;
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy621;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy723;
+ goto yy724;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy725;
+ goto yy726;
+ } else {
+ if (yych <= 0xF3) goto yy727;
+ if (yych <= 0xF4) goto yy728;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy904:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '\n') {
+ if (yych <= '\t') goto yy856;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) goto yy856;
+ if (yych <= 0xC1) goto yy1;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy906;
+ if (yych <= 0xEF) goto yy907;
+ goto yy908;
+ } else {
+ if (yych <= 0xF3) goto yy909;
+ if (yych <= 0xF4) goto yy910;
+ goto yy1;
+ }
+ }
+yy905:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy856;
+ goto yy1;
+yy906:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy905;
+ goto yy1;
+yy907:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy905;
+ goto yy1;
+yy908:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy907;
+ goto yy1;
+yy909:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy907;
+ goto yy1;
+yy910:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy907;
+ goto yy1;
+yy911:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy942;
+ goto yy1;
+yy912:
+ yyaccept = 29;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0xDF) {
+ if (yych <= ':') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= '9') {
+ yyt1 = YYCURSOR;
+ goto yy944;
+ }
+ goto yy643;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= 0xC1) goto yy643;
+ yyt2 = YYCURSOR;
+ goto yy645;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy646;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy647;
+ }
+ yyt2 = YYCURSOR;
+ goto yy648;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy649;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy650;
+ }
+ goto yy643;
+ }
+ }
+yy913:
+ yych = *++YYCURSOR;
+ if (yych == 'e') goto yy945;
+ goto yy1;
+yy914:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy946;
+ goto yy1;
+yy915:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy947;
+ goto yy1;
+yy916:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy947;
+ if (yych <= ':') goto yy152;
+ goto yy1;
+yy917:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy948;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy948;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy948;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy918:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy949;
+ goto yy950;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy919:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy950;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy920:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy26;
+ if (yych == 0x1B) goto yy26;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy26;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy951;
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy948;
+ if (yych <= 'Z') goto yy46;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy26;
+ if (yych <= 'f') goto yy948;
+ if (yych <= 'z') goto yy46;
+ goto yy26;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+yy921:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '9') {
+ if (yych == '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1;
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy816;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy922:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy952;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy952;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy952;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy923:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy953;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy953;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy953;
+ goto yy1;
+ }
+yy924:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy954;
+ if (yych <= ':') goto yy763;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy954;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy954;
+ goto yy1;
+ }
+yy925:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy820;
+ goto yy1;
+yy926:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy955;
+ if (yych >= ';') goto yy1;
+ } else {
+ if (yych <= 'F') goto yy955;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy955;
+ goto yy1;
+ }
+yy927:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy956;
+ if (yych <= ':') goto yy957;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy956;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy956;
+ goto yy1;
+ }
+yy928:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy958;
+ if (yych <= ':') goto yy959;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy958;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy958;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy929:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy960;
+ if (yych <= ':') goto yy882;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy960;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy960;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy930:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy960;
+ goto yy882;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy960;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy960;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy931:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy764;
+ goto yy961;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy766;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy766;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy932:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy609;
+ goto yy932;
+ } else {
+ if (yych == '\r') goto yy932;
+ goto yy609;
+ }
+ } else {
+ if (yych <= '\'') {
+ if (yych <= ' ') goto yy932;
+ if (yych <= '&') goto yy609;
+ goto yy599;
+ } else {
+ if (yych == '/') goto yy896;
+ if (yych <= '<') goto yy609;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '[') {
+ if (yych <= '>') goto yy513;
+ if (yych <= '?') goto yy896;
+ goto yy609;
+ } else {
+ if (yych <= '\\') goto yy610;
+ if (yych <= 0x7F) goto yy609;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy611;
+ if (yych <= 0xE0) goto yy612;
+ goto yy613;
+ } else {
+ if (yych <= 0xF0) goto yy614;
+ if (yych <= 0xF3) goto yy615;
+ if (yych <= 0xF4) goto yy616;
+ goto yy1;
+ }
+ }
+ }
+yy933:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy1;
+ goto yy413;
+ } else {
+ if (yych <= '\t') goto yy607;
+ if (yych <= '\f') goto yy413;
+ goto yy607;
+ }
+ } else {
+ if (yych <= '!') {
+ if (yych == ' ') goto yy607;
+ goto yy413;
+ } else {
+ if (yych <= '"') goto yy717;
+ if (yych <= '=') goto yy413;
+ goto yy609;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '\\') {
+ if (yych <= '[') goto yy413;
+ goto yy514;
+ } else {
+ if (yych <= 0x7F) goto yy413;
+ if (yych <= 0xC1) goto yy1;
+ goto yy515;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy516;
+ if (yych <= 0xEF) goto yy517;
+ goto yy518;
+ } else {
+ if (yych <= 0xF3) goto yy519;
+ if (yych <= 0xF4) goto yy520;
+ goto yy1;
+ }
+ }
+ }
+yy934:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy598;
+ goto yy934;
+ } else {
+ if (yych == '\r') goto yy934;
+ goto yy598;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy934;
+ if (yych <= '!') goto yy598;
+ goto yy599;
+ } else {
+ if (yych == '/') goto yy899;
+ if (yych <= '<') goto yy598;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '[') {
+ if (yych <= '>') goto yy503;
+ if (yych <= '?') goto yy899;
+ goto yy598;
+ } else {
+ if (yych <= '\\') goto yy600;
+ if (yych <= 0x7F) goto yy598;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy601;
+ if (yych <= 0xE0) goto yy602;
+ goto yy603;
+ } else {
+ if (yych <= 0xF0) goto yy604;
+ if (yych <= 0xF3) goto yy605;
+ if (yych <= 0xF4) goto yy606;
+ goto yy1;
+ }
+ }
+ }
+yy935:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy1;
+ goto yy412;
+ } else {
+ if (yych <= '\t') goto yy596;
+ if (yych <= '\f') goto yy412;
+ goto yy596;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych == ' ') goto yy596;
+ goto yy412;
+ } else {
+ if (yych <= '\'') goto yy717;
+ if (yych <= '=') goto yy412;
+ goto yy598;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '\\') {
+ if (yych <= '[') goto yy412;
+ goto yy504;
+ } else {
+ if (yych <= 0x7F) goto yy412;
+ if (yych <= 0xC1) goto yy1;
+ goto yy505;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy506;
+ if (yych <= 0xEF) goto yy507;
+ goto yy508;
+ } else {
+ if (yych <= 0xF3) goto yy509;
+ if (yych <= 0xF4) goto yy510;
+ goto yy1;
+ }
+ }
+ }
+yy936:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy631;
+ goto yy936;
+ } else {
+ if (yych == '\r') goto yy936;
+ goto yy631;
+ }
+ } else {
+ if (yych <= '\'') {
+ if (yych <= ' ') goto yy936;
+ if (yych <= '&') goto yy631;
+ goto yy721;
+ } else {
+ if (yych == '/') goto yy938;
+ goto yy631;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '\\') {
+ if (yych <= '?') goto yy938;
+ if (yych <= '[') goto yy631;
+ goto yy731;
+ } else {
+ if (yych <= 0x7F) goto yy631;
+ if (yych <= 0xC1) goto yy1;
+ goto yy732;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy733;
+ if (yych <= 0xEF) goto yy734;
+ goto yy735;
+ } else {
+ if (yych <= 0xF3) goto yy736;
+ if (yych <= 0xF4) goto yy737;
+ goto yy1;
+ }
+ }
+ }
+yy937:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '&') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy962;
+ goto yy631;
+ } else {
+ if (yych <= '\r') goto yy962;
+ if (yych == ' ') goto yy962;
+ goto yy631;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '\'') goto yy721;
+ if (yych == '-') goto yy937;
+ goto yy631;
+ } else {
+ if (yych <= ':') {
+ if (yych >= '0') goto yy937;
+ } else {
+ if (yych == '=') goto yy963;
+ goto yy631;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '?') goto yy938;
+ if (yych <= '@') goto yy631;
+ if (yych <= 'Z') goto yy937;
+ goto yy631;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy731;
+ goto yy631;
+ } else {
+ if (yych == '`') goto yy631;
+ goto yy937;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy631;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy732;
+ goto yy733;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy734;
+ goto yy735;
+ } else {
+ if (yych <= 0xF3) goto yy736;
+ if (yych <= 0xF4) goto yy737;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy938:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '=') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\'') goto yy721;
+ goto yy631;
+ } else {
+ if (yych <= '>') goto yy730;
+ if (yych == '\\') goto yy731;
+ goto yy631;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy732;
+ if (yych <= 0xE0) goto yy733;
+ goto yy734;
+ } else {
+ if (yych <= 0xF0) goto yy735;
+ if (yych <= 0xF3) goto yy736;
+ if (yych <= 0xF4) goto yy737;
+ goto yy1;
+ }
+ }
+yy939:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy621;
+ goto yy939;
+ } else {
+ if (yych == '\r') goto yy939;
+ goto yy621;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy939;
+ if (yych <= '!') goto yy621;
+ goto yy721;
+ } else {
+ if (yych == '/') goto yy941;
+ goto yy621;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '\\') {
+ if (yych <= '?') goto yy941;
+ if (yych <= '[') goto yy621;
+ goto yy722;
+ } else {
+ if (yych <= 0x7F) goto yy621;
+ if (yych <= 0xC1) goto yy1;
+ goto yy723;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy724;
+ if (yych <= 0xEF) goto yy725;
+ goto yy726;
+ } else {
+ if (yych <= 0xF3) goto yy727;
+ if (yych <= 0xF4) goto yy728;
+ goto yy1;
+ }
+ }
+ }
+yy940:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '!') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '\t') goto yy964;
+ goto yy621;
+ } else {
+ if (yych <= '\r') goto yy964;
+ if (yych == ' ') goto yy964;
+ goto yy621;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '"') goto yy721;
+ if (yych == '-') goto yy940;
+ goto yy621;
+ } else {
+ if (yych <= ':') {
+ if (yych >= '0') goto yy940;
+ } else {
+ if (yych == '=') goto yy965;
+ goto yy621;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '[') {
+ if (yych <= '?') goto yy941;
+ if (yych <= '@') goto yy621;
+ if (yych <= 'Z') goto yy940;
+ goto yy621;
+ } else {
+ if (yych <= '^') {
+ if (yych <= '\\') goto yy722;
+ goto yy621;
+ } else {
+ if (yych == '`') goto yy621;
+ goto yy940;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) goto yy621;
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy723;
+ goto yy724;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) goto yy725;
+ goto yy726;
+ } else {
+ if (yych <= 0xF3) goto yy727;
+ if (yych <= 0xF4) goto yy728;
+ goto yy1;
+ }
+ }
+ }
+ }
+yy941:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '=') {
+ if (yych <= 0x00) goto yy1;
+ if (yych == '"') goto yy721;
+ goto yy621;
+ } else {
+ if (yych <= '>') goto yy720;
+ if (yych == '\\') goto yy722;
+ goto yy621;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) goto yy723;
+ if (yych <= 0xE0) goto yy724;
+ goto yy725;
+ } else {
+ if (yych <= 0xF0) goto yy726;
+ if (yych <= 0xF3) goto yy727;
+ if (yych <= 0xF4) goto yy728;
+ goto yy1;
+ }
+ }
+yy942:
+ yyaccept = 32;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == ':') goto yy966;
+yy943:
+#line 184 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_DATE_TIME);
+ }
+#line 31412 "../../lnav/src/data_scanner_re.cc"
+yy944:
+ yyaccept = 29;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0xDF) {
+ if (yych <= ':') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= '9') {
+ yyt1 = YYCURSOR;
+ goto yy967;
+ }
+ goto yy643;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= 0xC1) goto yy643;
+ yyt2 = YYCURSOR;
+ goto yy645;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy646;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy647;
+ }
+ yyt2 = YYCURSOR;
+ goto yy648;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy649;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy650;
+ }
+ goto yy643;
+ }
+ }
+yy945:
+ yych = *++YYCURSOR;
+ if (yych == 's') goto yy968;
+ goto yy1;
+yy946:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy969;
+ goto yy1;
+yy947:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy970;
+ goto yy1;
+yy948:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '-') goto yy972;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') goto yy46;
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy949:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy973;
+ goto yy974;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy950:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy974;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy951:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '-') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ goto yy26;
+ } else {
+ if (yych == '\r') goto yy26;
+ if (yych <= 0x1A) goto yy4;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy26;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ goto yy972;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '.') goto yy46;
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy340;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy26;
+ if (yych <= 'z') goto yy46;
+ goto yy26;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+yy952:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy975;
+ goto yy923;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy975;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy975;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy953:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy976;
+ goto yy882;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy976;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy976;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy954:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy977;
+ if (yych <= ':') goto yy820;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy977;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy977;
+ goto yy1;
+ }
+yy955:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy978;
+ if (yych <= ':') goto yy927;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy978;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy978;
+ goto yy1;
+ }
+yy956:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy979;
+ if (yych <= ':') goto yy980;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy979;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy979;
+ goto yy1;
+ }
+yy957:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy981;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy981;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy981;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy958:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy982;
+ if (yych >= ';') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy982;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy982;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy959:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy981;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy981;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy981;
+ goto yy1;
+ }
+yy960:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1;
+ if (yych <= ':') goto yy882;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy961:
+ yych = *++YYCURSOR;
+ if (yych <= '9') {
+ if (yych <= '0') {
+ if (yych <= '/') goto yy1;
+ goto yy983;
+ } else {
+ if (yych <= '1') goto yy822;
+ if (yych <= '2') goto yy823;
+ goto yy821;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy1;
+ goto yy824;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy824;
+ goto yy1;
+ }
+ }
+yy962:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy631;
+ goto yy962;
+ } else {
+ if (yych == '\r') goto yy962;
+ goto yy631;
+ }
+ } else {
+ if (yych <= '\'') {
+ if (yych <= ' ') goto yy962;
+ if (yych <= '&') goto yy631;
+ goto yy721;
+ } else {
+ if (yych == '/') goto yy938;
+ if (yych <= '<') goto yy631;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '[') {
+ if (yych == '?') goto yy938;
+ goto yy631;
+ } else {
+ if (yych <= '\\') goto yy731;
+ if (yych <= 0x7F) goto yy631;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy732;
+ if (yych <= 0xE0) goto yy733;
+ goto yy734;
+ } else {
+ if (yych <= 0xF0) goto yy735;
+ if (yych <= 0xF3) goto yy736;
+ if (yych <= 0xF4) goto yy737;
+ goto yy1;
+ }
+ }
+ }
+yy963:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy1;
+ goto yy525;
+ } else {
+ if (yych <= '\t') goto yy729;
+ if (yych <= '\f') goto yy525;
+ goto yy729;
+ }
+ } else {
+ if (yych <= '!') {
+ if (yych == ' ') goto yy729;
+ goto yy525;
+ } else {
+ if (yych <= '"') goto yy800;
+ if (yych <= '=') goto yy525;
+ goto yy631;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '\\') {
+ if (yych <= '[') goto yy525;
+ goto yy632;
+ } else {
+ if (yych <= 0x7F) goto yy525;
+ if (yych <= 0xC1) goto yy1;
+ goto yy633;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy634;
+ if (yych <= 0xEF) goto yy635;
+ goto yy636;
+ } else {
+ if (yych <= 0xF3) goto yy637;
+ if (yych <= 0xF4) goto yy638;
+ goto yy1;
+ }
+ }
+ }
+yy964:
+ yych = *++YYCURSOR;
+ if (yych <= '=') {
+ if (yych <= 0x1F) {
+ if (yych <= '\t') {
+ if (yych <= 0x00) goto yy1;
+ if (yych <= 0x08) goto yy621;
+ goto yy964;
+ } else {
+ if (yych == '\r') goto yy964;
+ goto yy621;
+ }
+ } else {
+ if (yych <= '"') {
+ if (yych <= ' ') goto yy964;
+ if (yych <= '!') goto yy621;
+ goto yy721;
+ } else {
+ if (yych == '/') goto yy941;
+ if (yych <= '<') goto yy621;
+ }
+ }
+ } else {
+ if (yych <= 0xC1) {
+ if (yych <= '[') {
+ if (yych == '?') goto yy941;
+ goto yy621;
+ } else {
+ if (yych <= '\\') goto yy722;
+ if (yych <= 0x7F) goto yy621;
+ goto yy1;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xDF) goto yy723;
+ if (yych <= 0xE0) goto yy724;
+ goto yy725;
+ } else {
+ if (yych <= 0xF0) goto yy726;
+ if (yych <= 0xF3) goto yy727;
+ if (yych <= 0xF4) goto yy728;
+ goto yy1;
+ }
+ }
+ }
+yy965:
+ yych = *++YYCURSOR;
+ if (yych <= '>') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy1;
+ goto yy524;
+ } else {
+ if (yych <= '\t') goto yy719;
+ if (yych <= '\f') goto yy524;
+ goto yy719;
+ }
+ } else {
+ if (yych <= '&') {
+ if (yych == ' ') goto yy719;
+ goto yy524;
+ } else {
+ if (yych <= '\'') goto yy800;
+ if (yych <= '=') goto yy524;
+ goto yy621;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '\\') {
+ if (yych <= '[') goto yy524;
+ goto yy622;
+ } else {
+ if (yych <= 0x7F) goto yy524;
+ if (yych <= 0xC1) goto yy1;
+ goto yy623;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy624;
+ if (yych <= 0xEF) goto yy625;
+ goto yy626;
+ } else {
+ if (yych <= 0xF3) goto yy627;
+ if (yych <= 0xF4) goto yy628;
+ goto yy1;
+ }
+ }
+ }
+yy966:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy984;
+ goto yy1;
+yy967:
+ yyaccept = 29;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 0xDF) {
+ if (yych <= ':') {
+ if (yych <= '9') {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ goto yy643;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy642;
+ }
+ if (yych <= 0xC1) goto yy643;
+ yyt2 = YYCURSOR;
+ goto yy645;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy646;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy647;
+ }
+ yyt2 = YYCURSOR;
+ goto yy648;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy649;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy650;
+ }
+ goto yy643;
+ }
+ }
+yy968:
+ yyaccept = 5;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == ' ') goto yy985;
+ goto yy100;
+yy969:
+ yych = *++YYCURSOR;
+ if (yych == ' ') goto yy986;
+ goto yy1;
+yy970:
+ yyaccept = 33;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == ':') goto yy987;
+yy971:
+#line 181 "../../lnav/src/data_scanner_re.re"
+ {
+ RET(DT_DATE_TIME);
+ }
+#line 32346 "../../lnav/src/data_scanner_re.cc"
+yy972:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy988;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy988;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy988;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy973:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy989;
+ goto yy990;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy974:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy990;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy975:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '9') {
+ if (yych == '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1;
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy923;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy976:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy991;
+ goto yy882;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy991;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy991;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy977:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy354;
+ goto yy466;
+ } else {
+ if (yych <= '/') goto yy354;
+ if (yych <= '9') goto yy878;
+ goto yy992;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy354;
+ goto yy878;
+ } else {
+ if (yych <= '`') goto yy354;
+ if (yych <= 'f') goto yy878;
+ goto yy354;
+ }
+ }
+yy978:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy927;
+ goto yy1;
+yy979:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy993;
+ if (yych >= ';') goto yy1;
+ } else {
+ if (yych <= 'F') goto yy993;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy993;
+ goto yy1;
+ }
+yy980:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy981;
+ if (yych <= ':') goto yy830;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy981;
+ if (yych <= '`') goto yy1;
+ if (yych >= 'g') goto yy1;
+ }
+yy981:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy994;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy994;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy994;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy982:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy995;
+ if (yych <= ':') goto yy959;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy995;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy995;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy983:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy881;
+ goto yy996;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy883;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy883;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy984:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy997;
+ goto yy1;
+yy985:
+ yych = *++YYCURSOR;
+ if (yych == '(') goto yy998;
+ goto yy1;
+yy986:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy999;
+ goto yy1;
+yy987:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1000;
+ goto yy1;
+yy988:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy1001;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1001;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1001;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy989:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy109;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy109;
+ if (yych <= 0x1F) goto yy4;
+ goto yy109;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') goto yy4;
+ goto yy105;
+ } else {
+ if (yych == '+') goto yy80;
+ goto yy109;
+ }
+ } else {
+ if (yych <= '.') {
+ if (yych <= '-') goto yy46;
+ goto yy266;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '7') goto yy1002;
+ goto yy1003;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy109;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy109;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy109;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy109;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy109;
+ if (yych <= 0xF4) goto yy4;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy990:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '9') {
+ if (yych <= '#') {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy26;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1A) {
+ if (yych <= '\r') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0x1B) goto yy26;
+ if (yych <= 0x1F) goto yy4;
+ goto yy26;
+ }
+ }
+ } else {
+ if (yych <= '+') {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy105;
+ if (yych <= '*') goto yy26;
+ goto yy80;
+ } else {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ } else {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy4;
+ goto yy1003;
+ }
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= 'D') {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'F') {
+ if (yych <= 'E') goto yy754;
+ goto yy196;
+ } else {
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy26;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= 'd') {
+ if (yych <= '`') goto yy26;
+ goto yy196;
+ } else {
+ if (yych <= 'e') goto yy754;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy26;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy26;
+ if (yych <= 0xF4) goto yy4;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy991:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy680;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1004;
+ goto yy882;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1004;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1004;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy992:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1005;
+ if (yych <= ':') goto yy880;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy1005;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy1005;
+ goto yy1;
+ }
+yy993:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1006;
+ if (yych <= ':') goto yy980;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy1006;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy1006;
+ goto yy1;
+ }
+yy994:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1007;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy1007;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy1007;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy995:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1;
+ if (yych <= ':') goto yy959;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ } else {
+ if (yych <= 0xEF) {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ yyt2 = YYCURSOR;
+ goto yy207;
+ } else {
+ if (yych <= 0xF0) {
+ yyt2 = YYCURSOR;
+ goto yy208;
+ }
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+yy996:
+ yych = *++YYCURSOR;
+ if (yych <= '9') {
+ if (yych <= '0') {
+ if (yych <= '/') goto yy1;
+ goto yy1008;
+ } else {
+ if (yych <= '1') goto yy1009;
+ if (yych <= '2') goto yy1010;
+ goto yy1008;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy1;
+ goto yy928;
+ } else {
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy928;
+ goto yy1;
+ }
+ }
+yy997:
+ yyaccept = 32;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy1011;
+ goto yy943;
+yy998:
+ yych = *++YYCURSOR;
+ if (yych == 'x') goto yy1012;
+ goto yy1;
+yy999:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1013;
+ goto yy1;
+yy1000:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1014;
+ goto yy1;
+yy1001:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy1015;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1015;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1015;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1002:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) {
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ } else {
+ if (yych <= '\n') {
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ if (yych <= '\f') {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ } else {
+ if (yych <= 0x1F) {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ if (yych <= ' ') {
+ yyt1 = YYCURSOR;
+ goto yy1019;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1020;
+ } else {
+ if (yych == '+') {
+ yyt1 = YYCURSOR;
+ goto yy1020;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych <= '-') {
+ yyt1 = YYCURSOR;
+ goto yy1021;
+ }
+ if (yych <= '.') {
+ yyt1 = YYCURSOR;
+ goto yy1022;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ } else {
+ if (yych <= '9') goto yy1024;
+ if (yych <= ':') {
+ yyt1 = YYCURSOR;
+ goto yy1026;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'f') {
+ if (yych <= 'Z') {
+ if (yych <= 'D') {
+ if (yych <= '@') {
+ yyt1 = YYCURSOR;
+ goto yy1027;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1028;
+ } else {
+ if (yych <= 'E') {
+ yyt1 = YYCURSOR;
+ goto yy1029;
+ }
+ if (yych <= 'F') {
+ yyt1 = YYCURSOR;
+ goto yy1028;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1030;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych == '_') {
+ yyt1 = YYCURSOR;
+ goto yy1021;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ } else {
+ if (yych == 'e') {
+ yyt1 = YYCURSOR;
+ goto yy1029;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1028;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '~') {
+ if (yych <= 'z') {
+ yyt1 = YYCURSOR;
+ goto yy1030;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ } else {
+ if (yych <= 0x7F) {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ if (yych <= 0xC1) goto yy1024;
+ yyt1 = YYCURSOR;
+ goto yy1031;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt1 = YYCURSOR;
+ goto yy1032;
+ }
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy1033;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1034;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy1035;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy1036;
+ }
+ goto yy1024;
+ }
+ }
+ }
+ }
+yy1003:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '#') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) {
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ } else {
+ if (yych <= '\n') {
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ if (yych <= '\f') {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ } else {
+ if (yych <= 0x1F) {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ if (yych <= ' ') {
+ yyt1 = YYCURSOR;
+ goto yy1019;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ }
+ } else {
+ if (yych <= ',') {
+ if (yych <= '%') {
+ if (yych <= '$') {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1020;
+ } else {
+ if (yych == '+') {
+ yyt1 = YYCURSOR;
+ goto yy1020;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ } else {
+ if (yych <= '/') {
+ if (yych <= '-') {
+ yyt1 = YYCURSOR;
+ goto yy1021;
+ }
+ if (yych <= '.') {
+ yyt1 = YYCURSOR;
+ goto yy1022;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ } else {
+ if (yych <= '9') goto yy1025;
+ if (yych <= ':') {
+ yyt1 = YYCURSOR;
+ goto yy1026;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'f') {
+ if (yych <= 'Z') {
+ if (yych <= 'D') {
+ if (yych <= '@') {
+ yyt1 = YYCURSOR;
+ goto yy1027;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1028;
+ } else {
+ if (yych <= 'E') {
+ yyt1 = YYCURSOR;
+ goto yy1029;
+ }
+ if (yych <= 'F') {
+ yyt1 = YYCURSOR;
+ goto yy1028;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1030;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych == '_') {
+ yyt1 = YYCURSOR;
+ goto yy1021;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ } else {
+ if (yych == 'e') {
+ yyt1 = YYCURSOR;
+ goto yy1029;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1028;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '~') {
+ if (yych <= 'z') {
+ yyt1 = YYCURSOR;
+ goto yy1030;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ } else {
+ if (yych <= 0x7F) {
+ yyt1 = YYCURSOR;
+ goto yy1018;
+ }
+ if (yych <= 0xC1) goto yy26;
+ yyt1 = YYCURSOR;
+ goto yy1031;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt1 = YYCURSOR;
+ goto yy1032;
+ }
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy1033;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1034;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy1035;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy1036;
+ }
+ goto yy26;
+ }
+ }
+ }
+ }
+yy1004:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '9') {
+ if (yych == '%') {
+ yyt1 = YYCURSOR;
+ goto yy680;
+ }
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1;
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy882;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1005:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1037;
+ if (yych <= ':') goto yy927;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy1037;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy1037;
+ goto yy1;
+ }
+yy1006:
+ yych = *++YYCURSOR;
+ if (yych == ':') goto yy980;
+ goto yy1;
+yy1007:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy830;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy830;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy830;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1008:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1038;
+ goto yy959;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy958;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy958;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1009:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1039;
+ goto yy959;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy958;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy958;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1010:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '5') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '4') goto yy1039;
+ goto yy1040;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy1038;
+ if (yych <= ':') goto yy959;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy958;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy958;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1011:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1041;
+ goto yy1;
+yy1012:
+ yych = *++YYCURSOR;
+ if (yych == '8') goto yy1042;
+ goto yy1;
+yy1013:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1043;
+ goto yy1;
+yy1014:
+ yyaccept = 33;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '.') goto yy1044;
+ goto yy971;
+yy1015:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy1045;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1045;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1045;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1016:
+ ++YYCURSOR;
+yy1017:
+ YYCURSOR = yyt1;
+#line 236 "../../lnav/src/data_scanner_re.re"
+ {
+ CAPTURE(DT_CREDIT_CARD_NUMBER);
+ if (!this->is_credit_card(this->to_string_fragment(cap_all))) {
+ if (cap_all.length() > 16) {
+ cap_all.c_end = cap_all.c_begin + 4;
+ cap_inner.c_end = cap_inner.c_begin + 4;
+ }
+ this->ds_next_offset = cap_all.c_end;
+ token_out = DT_NUMBER;
+ }
+ return tokenize_result{token_out, cap_all, cap_inner, this->ds_input.data()};
+ }
+#line 34170 "../../lnav/src/data_scanner_re.cc"
+yy1018:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ',') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy1017;
+ goto yy4;
+ } else {
+ if (yych <= '\n') goto yy1017;
+ if (yych <= '\f') goto yy4;
+ goto yy1017;
+ }
+ } else {
+ if (yych <= 0x1F) {
+ if (yych == 0x1B) goto yy1017;
+ goto yy4;
+ } else {
+ if (yych == '$') goto yy4;
+ goto yy1017;
+ }
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= 'Z') {
+ if (yych <= ':') goto yy4;
+ if (yych <= '?') goto yy1017;
+ goto yy4;
+ } else {
+ if (yych == '_') goto yy4;
+ goto yy1017;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy4;
+ if (yych <= '~') goto yy1017;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy1017;
+ if (yych <= 0xF4) goto yy4;
+ goto yy1017;
+ }
+ }
+ }
+yy1019:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych == '%') goto yy177;
+ goto yy1017;
+yy1020:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '*') {
+ if (yych == '%') goto yy81;
+ goto yy1017;
+ } else {
+ if (yych == ',') goto yy1017;
+ if (yych <= '.') goto yy81;
+ goto yy1017;
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '9') goto yy81;
+ if (yych <= '?') goto yy1017;
+ if (yych <= 'Z') goto yy81;
+ goto yy1017;
+ } else {
+ if (yych == '`') goto yy1017;
+ if (yych <= 'z') goto yy81;
+ goto yy1017;
+ }
+ }
+yy1021:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[1024+yych] & 16) {
+ goto yy46;
+ }
+ if (yych <= '%') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy1017;
+ goto yy4;
+ } else {
+ if (yych <= '\n') goto yy1017;
+ if (yych <= '\f') goto yy4;
+ goto yy1017;
+ }
+ } else {
+ if (yych <= 0x1F) {
+ if (yych == 0x1B) goto yy1017;
+ goto yy4;
+ } else {
+ if (yych <= '#') goto yy1017;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych <= '*') goto yy1017;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy1017;
+ if (yych <= ':') goto yy4;
+ goto yy1017;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '@') goto yy96;
+ if (yych <= '~') goto yy1017;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy1017;
+ if (yych <= 0xF4) goto yy4;
+ goto yy1017;
+ }
+ }
+ }
+yy1022:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1B) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy1017;
+ if (yych <= 0x08) goto yy4;
+ goto yy1017;
+ } else {
+ if (yych == '\r') goto yy1017;
+ if (yych <= 0x1A) goto yy4;
+ goto yy1017;
+ }
+ } else {
+ if (yych <= '%') {
+ if (yych <= 0x1F) goto yy4;
+ if (yych <= '#') goto yy1017;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ } else {
+ if (yych == '+') goto yy80;
+ if (yych <= ',') goto yy1017;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= ':') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy359;
+ goto yy4;
+ } else {
+ if (yych <= '?') goto yy1017;
+ if (yych <= '@') goto yy96;
+ if (yych <= 'Z') goto yy46;
+ goto yy1017;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '`') goto yy1017;
+ if (yych <= 'z') goto yy46;
+ goto yy1017;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy1017;
+ if (yych <= 0xF4) goto yy4;
+ goto yy1017;
+ }
+ }
+ }
+yy1023:
+ yyaccept = 11;
+ yych = *(YYMARKER = ++YYCURSOR);
+yy1024:
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '?') {
+ if (yych <= '$') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy109;
+ goto yy3;
+ } else {
+ if (yych <= '\n') goto yy109;
+ if (yych <= '\f') goto yy3;
+ goto yy109;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy3;
+ goto yy109;
+ } else {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= '#') goto yy109;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '*') {
+ if (yych <= '%') goto yy105;
+ goto yy109;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy109;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '7') {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy3;
+ goto yy1023;
+ } else {
+ if (yych <= '9') goto yy1025;
+ if (yych <= ':') goto yy148;
+ goto yy109;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'f') {
+ if (yych <= 'Z') {
+ if (yych <= 'D') {
+ if (yych <= '@') goto yy96;
+ goto yy196;
+ } else {
+ if (yych <= 'E') goto yy754;
+ if (yych <= 'F') goto yy196;
+ goto yy115;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych == '_') goto yy46;
+ goto yy109;
+ } else {
+ if (yych == 'e') goto yy754;
+ goto yy196;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '~') {
+ if (yych <= 'z') goto yy115;
+ goto yy109;
+ } else {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy109;
+ goto yy56;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy57;
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy109;
+ }
+ }
+ }
+ }
+yy1025:
+ yyaccept = 6;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 32) {
+ goto yy103;
+ }
+ if (yych <= '@') {
+ if (yych <= '$') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych <= 0x00) goto yy26;
+ goto yy3;
+ } else {
+ if (yych <= '\n') goto yy26;
+ if (yych <= '\f') goto yy3;
+ goto yy26;
+ }
+ } else {
+ if (yych <= 0x1B) {
+ if (yych <= 0x1A) goto yy3;
+ goto yy26;
+ } else {
+ if (yych <= 0x1F) goto yy3;
+ if (yych <= '#') goto yy26;
+ goto yy3;
+ }
+ }
+ } else {
+ if (yych <= '-') {
+ if (yych <= '*') {
+ if (yych <= '%') goto yy105;
+ goto yy26;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy26;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '9') {
+ if (yych <= '.') goto yy266;
+ if (yych <= '/') goto yy3;
+ goto yy1025;
+ } else {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy26;
+ goto yy96;
+ }
+ }
+ }
+ } else {
+ if (yych <= 'f') {
+ if (yych <= '^') {
+ if (yych <= 'E') {
+ if (yych <= 'D') goto yy196;
+ goto yy754;
+ } else {
+ if (yych <= 'F') goto yy196;
+ if (yych <= 'Z') goto yy115;
+ goto yy26;
+ }
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy26;
+ } else {
+ if (yych == 'e') goto yy754;
+ goto yy196;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= '~') {
+ if (yych <= 'z') goto yy115;
+ goto yy26;
+ } else {
+ if (yych <= 0x7F) goto yy3;
+ if (yych <= 0xC1) goto yy26;
+ goto yy56;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) goto yy57;
+ if (yych <= 0xEF) goto yy58;
+ goto yy59;
+ } else {
+ if (yych <= 0xF3) goto yy60;
+ if (yych <= 0xF4) goto yy61;
+ goto yy26;
+ }
+ }
+ }
+ }
+yy1026:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych == '/') goto yy189;
+ if (yych == ':') goto yy152;
+ goto yy1017;
+yy1027:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1A) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy1017;
+ if (yych <= 0x08) goto yy4;
+ goto yy1017;
+ } else {
+ if (yych == '\r') goto yy1017;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '#') {
+ if (yych <= 0x1B) goto yy1017;
+ if (yych <= 0x1F) goto yy4;
+ goto yy1017;
+ } else {
+ if (yych <= '$') goto yy4;
+ if (yych <= ',') goto yy1017;
+ if (yych <= '.') goto yy174;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '?') {
+ if (yych <= '9') goto yy174;
+ if (yych <= ':') goto yy4;
+ goto yy1017;
+ } else {
+ if (yych <= '@') goto yy4;
+ if (yych <= 'Z') goto yy174;
+ if (yych <= '^') goto yy1017;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy1017;
+ if (yych <= 'z') goto yy174;
+ goto yy1017;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy1017;
+ if (yych <= 0xF4) goto yy4;
+ goto yy1017;
+ }
+ }
+ }
+yy1028:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1017;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy1017;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy1017;
+ if (yych == 0x1B) goto yy1017;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy1017;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy1017;
+ } else {
+ if (yych <= '+') goto yy80;
+ if (yych <= ',') goto yy1017;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '_') {
+ if (yych <= '@') {
+ if (yych <= '9') goto yy196;
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy1017;
+ goto yy96;
+ } else {
+ if (yych <= 'F') goto yy196;
+ if (yych <= 'Z') goto yy115;
+ if (yych <= '^') goto yy1017;
+ goto yy46;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych <= '`') goto yy1017;
+ if (yych <= 'f') goto yy196;
+ if (yych <= 'z') goto yy115;
+ goto yy1017;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy1017;
+ if (yych <= 0xF4) goto yy4;
+ goto yy1017;
+ }
+ }
+ }
+yy1029:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '.') {
+ if (yych <= 0x1F) {
+ if (yych <= '\f') {
+ if (yych <= 0x00) goto yy1017;
+ if (yych <= 0x08) goto yy4;
+ if (yych <= '\n') goto yy1017;
+ goto yy4;
+ } else {
+ if (yych <= '\r') goto yy1017;
+ if (yych == 0x1B) goto yy1017;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '*') {
+ if (yych <= '#') goto yy1017;
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ goto yy1017;
+ } else {
+ if (yych <= '+') goto yy194;
+ if (yych <= ',') goto yy1017;
+ if (yych <= '-') goto yy251;
+ goto yy46;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '?') {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy196;
+ if (yych <= ':') goto yy148;
+ goto yy1017;
+ } else {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy196;
+ if (yych <= 'Z') goto yy115;
+ goto yy1017;
+ }
+ } else {
+ if (yych <= 'z') {
+ if (yych <= '_') goto yy46;
+ if (yych <= '`') goto yy1017;
+ if (yych <= 'f') goto yy196;
+ goto yy115;
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '~') goto yy1017;
+ goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy1017;
+ if (yych <= 0xF4) goto yy4;
+ goto yy1017;
+ }
+ }
+ }
+ }
+yy1030:
+ yyaccept = 34;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[768+yych] & 64) {
+ goto yy115;
+ }
+ if (yych <= '+') {
+ if (yych <= 0x1A) {
+ if (yych <= '\n') {
+ if (yych <= 0x00) goto yy1017;
+ if (yych <= 0x08) goto yy4;
+ goto yy1017;
+ } else {
+ if (yych == '\r') goto yy1017;
+ goto yy4;
+ }
+ } else {
+ if (yych <= '#') {
+ if (yych <= 0x1B) goto yy1017;
+ if (yych <= 0x1F) goto yy4;
+ goto yy1017;
+ } else {
+ if (yych <= '$') goto yy4;
+ if (yych <= '%') goto yy80;
+ if (yych <= '*') goto yy1017;
+ goto yy80;
+ }
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ if (yych <= ',') goto yy1017;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= ':') goto yy148;
+ if (yych <= '?') goto yy1017;
+ goto yy96;
+ }
+ } else {
+ if (yych <= '~') {
+ if (yych == '_') goto yy46;
+ goto yy1017;
+ } else {
+ if (yych <= 0x7F) goto yy4;
+ if (yych <= 0xC1) goto yy1017;
+ if (yych <= 0xF4) goto yy4;
+ goto yy1017;
+ }
+ }
+ }
+yy1031:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy1018;
+ goto yy1;
+yy1032:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy1031;
+ goto yy1;
+yy1033:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy1031;
+ goto yy1;
+yy1034:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy1033;
+ goto yy1;
+yy1035:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy1033;
+ goto yy1;
+yy1036:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy1033;
+ goto yy1;
+yy1037:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy354;
+ goto yy466;
+ } else {
+ if (yych <= '/') goto yy354;
+ if (yych <= '9') goto yy955;
+ goto yy1046;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy354;
+ goto yy955;
+ } else {
+ if (yych <= '`') goto yy354;
+ if (yych <= 'f') goto yy955;
+ goto yy354;
+ }
+ }
+yy1038:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy982;
+ goto yy959;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy982;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy982;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1039:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1047;
+ goto yy959;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy982;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy982;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1040:
+ yych = *++YYCURSOR;
+ if (yych <= '`') {
+ if (yych <= '9') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '5') goto yy1047;
+ goto yy982;
+ }
+ } else {
+ if (yych <= '@') {
+ if (yych <= ':') goto yy959;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'F') goto yy982;
+ if (yych <= 'Z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ }
+ } else {
+ if (yych <= 0xDF) {
+ if (yych <= 'z') {
+ if (yych <= 'f') goto yy982;
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1041:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1048;
+ goto yy1;
+yy1042:
+ yych = *++YYCURSOR;
+ if (yych == '6') goto yy1049;
+ goto yy1;
+yy1043:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1050;
+ goto yy1;
+yy1044:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1051;
+ goto yy1;
+yy1045:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '-') goto yy1052;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') goto yy46;
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1046:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1053;
+ if (yych <= ':') goto yy957;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy1053;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy1053;
+ goto yy1;
+ }
+yy1047:
+ yych = *++YYCURSOR;
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '.') {
+ if (yych <= '-') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt1 = YYCURSOR;
+ goto yy297;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy995;
+ goto yy959;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy995;
+ } else {
+ if (yych <= 'Z') goto yy1;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy995;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1048:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1054;
+ goto yy1;
+yy1049:
+ yych = *++YYCURSOR;
+ if (yych == ')') goto yy99;
+ goto yy1;
+yy1050:
+ yych = *++YYCURSOR;
+ if (yych <= 0xDF) {
+ if (yych <= '9') {
+ if (yych <= '/') {
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ goto yy1;
+ } else {
+ if (yych <= 0x7F) {
+ yyt1 = YYCURSOR;
+ goto yy1016;
+ }
+ if (yych <= 0xC1) goto yy1;
+ yyt1 = YYCURSOR;
+ goto yy1055;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xE0) {
+ yyt1 = YYCURSOR;
+ goto yy1056;
+ }
+ if (yych <= 0xEF) {
+ yyt1 = YYCURSOR;
+ goto yy1057;
+ }
+ yyt1 = YYCURSOR;
+ goto yy1058;
+ } else {
+ if (yych <= 0xF3) {
+ yyt1 = YYCURSOR;
+ goto yy1059;
+ }
+ if (yych <= 0xF4) {
+ yyt1 = YYCURSOR;
+ goto yy1060;
+ }
+ goto yy1;
+ }
+ }
+yy1051:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1061;
+ goto yy1;
+yy1052:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy1062;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1062;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1062;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1053:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1063;
+ if (yych <= ':') goto yy980;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy1063;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy1063;
+ goto yy1;
+ }
+yy1054:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy943;
+ if (yych <= '9') goto yy1064;
+ goto yy943;
+yy1055:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy1016;
+ goto yy1;
+yy1056:
+ yych = *++YYCURSOR;
+ if (yych <= 0x9F) goto yy1;
+ if (yych <= 0xBF) goto yy1055;
+ goto yy1;
+yy1057:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy1055;
+ goto yy1;
+yy1058:
+ yych = *++YYCURSOR;
+ if (yych <= 0x8F) goto yy1;
+ if (yych <= 0xBF) goto yy1057;
+ goto yy1;
+yy1059:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0xBF) goto yy1057;
+ goto yy1;
+yy1060:
+ yych = *++YYCURSOR;
+ if (yych <= 0x7F) goto yy1;
+ if (yych <= 0x8F) goto yy1057;
+ goto yy1;
+yy1061:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1065;
+ goto yy1;
+yy1062:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy1066;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1066;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1066;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1063:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= ',') goto yy354;
+ goto yy466;
+ } else {
+ if (yych <= '/') goto yy354;
+ if (yych <= '9') goto yy993;
+ goto yy1067;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') goto yy354;
+ goto yy993;
+ } else {
+ if (yych <= '`') goto yy354;
+ if (yych <= 'f') goto yy993;
+ goto yy354;
+ }
+ }
+yy1064:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy943;
+ if (yych <= '9') goto yy1068;
+ goto yy943;
+yy1065:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy971;
+ if (yych <= '9') goto yy1069;
+ goto yy971;
+yy1066:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy1070;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1070;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1070;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1067:
+ yych = *++YYCURSOR;
+ if (yych <= '@') {
+ if (yych <= '/') goto yy1;
+ if (yych <= '9') goto yy1071;
+ if (yych <= ':') goto yy830;
+ goto yy1;
+ } else {
+ if (yych <= 'F') goto yy1071;
+ if (yych <= '`') goto yy1;
+ if (yych <= 'f') goto yy1071;
+ goto yy1;
+ }
+yy1068:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy943;
+ if (yych <= '9') goto yy1072;
+ goto yy943;
+yy1069:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy971;
+ if (yych <= '9') goto yy1073;
+ goto yy971;
+yy1070:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy1074;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1074;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1074;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1071:
+ yych = *++YYCURSOR;
+ if (yych <= 'z') {
+ if (yych <= '@') {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1075;
+ if (yych <= ':') goto yy1;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= 'F') goto yy1075;
+ goto yy1;
+ } else {
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 'f') goto yy1075;
+ goto yy1;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= 0xC1) goto yy1;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy1;
+ }
+ }
+ }
+yy1072:
+ ++YYCURSOR;
+ goto yy943;
+yy1073:
+ yych = *++YYCURSOR;
+ if (yych <= '/') goto yy971;
+ if (yych <= '9') goto yy1076;
+ goto yy971;
+yy1074:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '/') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= ',') goto yy4;
+ if (yych <= '-') goto yy1077;
+ if (yych <= '.') goto yy46;
+ goto yy4;
+ }
+ } else {
+ if (yych <= 'Z') {
+ if (yych <= '9') goto yy46;
+ if (yych <= '?') goto yy4;
+ if (yych <= '@') goto yy96;
+ goto yy46;
+ } else {
+ if (yych <= '_') {
+ if (yych <= '^') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '`') goto yy4;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1075:
+ yyaccept = 21;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= 'f') {
+ if (yych <= ':') {
+ if (yych <= '-') {
+ if (yych <= ',') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ yyt2 = YYCURSOR;
+ goto yy1078;
+ } else {
+ if (yych <= '/') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ if (yych <= '9') goto yy1007;
+ goto yy466;
+ }
+ } else {
+ if (yych <= 'F') {
+ if (yych <= '@') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1007;
+ } else {
+ if (yych <= 'Z') goto yy354;
+ if (yych <= '`') {
+ yyt2 = YYCURSOR;
+ goto yy198;
+ }
+ goto yy1007;
+ }
+ }
+ } else {
+ if (yych <= 0xE0) {
+ if (yych <= 0x7F) {
+ if (yych <= 'z') goto yy354;
+ yyt2 = YYCURSOR;
+ goto yy198;
+ } else {
+ if (yych <= 0xC1) goto yy354;
+ if (yych <= 0xDF) {
+ yyt2 = YYCURSOR;
+ goto yy205;
+ }
+ yyt2 = YYCURSOR;
+ goto yy206;
+ }
+ } else {
+ if (yych <= 0xF0) {
+ if (yych <= 0xEF) {
+ yyt2 = YYCURSOR;
+ goto yy207;
+ }
+ yyt2 = YYCURSOR;
+ goto yy208;
+ } else {
+ if (yych <= 0xF3) {
+ yyt2 = YYCURSOR;
+ goto yy209;
+ }
+ if (yych <= 0xF4) {
+ yyt2 = YYCURSOR;
+ goto yy210;
+ }
+ goto yy354;
+ }
+ }
+ }
+yy1076:
+ ++YYCURSOR;
+ goto yy971;
+yy1077:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych <= '9') goto yy1079;
+ goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1079;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1079;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1078:
+ yyaccept = 17;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '@') {
+ if (yych <= '/') goto yy199;
+ if (yych <= '9') goto yy542;
+ goto yy199;
+ } else {
+ if (yych <= 'F') goto yy542;
+ if (yych <= '`') goto yy199;
+ if (yych <= 'f') goto yy542;
+ goto yy199;
+ }
+yy1079:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1080;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1080;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1080:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1081;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1081;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1081:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1082;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1082;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1082:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1083;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1083;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1083:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1084;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1084;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1084:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1085;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1085;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1085:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1086;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1086;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1086:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1087;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1087;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1087:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1088;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1088;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1088:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1089;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1089;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1089:
+ yyaccept = 0;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych == '%') goto yy80;
+ if (yych <= '*') goto yy4;
+ goto yy80;
+ } else {
+ if (yych <= '.') {
+ if (yych <= ',') goto yy4;
+ goto yy46;
+ } else {
+ if (yych <= '/') goto yy4;
+ if (yych >= ':') goto yy4;
+ }
+ }
+ } else {
+ if (yych <= '^') {
+ if (yych <= '@') goto yy96;
+ if (yych <= 'F') goto yy1090;
+ if (yych <= 'Z') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= '`') {
+ if (yych <= '_') goto yy46;
+ goto yy4;
+ } else {
+ if (yych <= 'f') goto yy1090;
+ if (yych <= 'z') goto yy46;
+ goto yy4;
+ }
+ }
+ }
+yy1090:
+ yyaccept = 35;
+ yych = *(YYMARKER = ++YYCURSOR);
+ if (yybm[1024+yych] & 16) {
+ goto yy46;
+ }
+ if (yych <= '%') {
+ if (yych <= '\r') {
+ if (yych <= 0x08) {
+ if (yych >= 0x01) goto yy4;
+ } else {
+ if (yych <= '\n') goto yy1091;
+ if (yych <= '\f') goto yy4;
+ }
+ } else {
+ if (yych <= 0x1F) {
+ if (yych != 0x1B) goto yy4;
+ } else {
+ if (yych <= '#') goto yy1091;
+ if (yych <= '$') goto yy4;
+ goto yy80;
+ }
+ }
+ } else {
+ if (yych <= '?') {
+ if (yych <= '+') {
+ if (yych >= '+') goto yy80;
+ } else {
+ if (yych <= ',') goto yy1091;
+ if (yych <= ':') goto yy4;
+ }
+ } else {
+ if (yych <= 0x7F) {
+ if (yych <= '@') goto yy96;
+ if (yych >= 0x7F) goto yy4;
+ } else {
+ if (yych <= 0xC1) goto yy1091;
+ if (yych <= 0xF4) goto yy4;
+ }
+ }
+ }
+yy1091:
+#line 234 "../../lnav/src/data_scanner_re.re"
+ { RET(DT_UUID); }
+#line 36093 "../../lnav/src/data_scanner_re.cc"
+}
+#line 276 "../../lnav/src/data_scanner_re.re"
+
+}
diff --git a/src/data_scanner_re.re b/src/data_scanner_re.re
new file mode 100644
index 0000000..904aa9f
--- /dev/null
+++ b/src/data_scanner_re.re
@@ -0,0 +1,277 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <arpa/inet.h>
+#include <netinet/in.h>
+#include <sys/socket.h>
+
+#include "base/date_time_scanner.hh"
+#include "config.h"
+#include "data_scanner.hh"
+
+nonstd::optional<data_scanner::tokenize_result> data_scanner::tokenize2()
+{
+ data_token_t token_out = DT_INVALID;
+ capture_t cap_all;
+ capture_t cap_inner;
+# define YYCTYPE unsigned char
+# define CAPTURE(tok) { \
+ if (YYCURSOR.val == EMPTY) { \
+ this->ds_next_offset = this->ds_input.length(); \
+ } else { \
+ this->ds_next_offset = YYCURSOR.val - this->ds_input.udata(); \
+ } \
+ cap_all.c_end = this->ds_next_offset; \
+ cap_inner.c_end = this->ds_next_offset; \
+ token_out = tok; \
+ }
+
+# define RET(tok) { \
+ CAPTURE(tok); \
+ return tokenize_result{token_out, cap_all, cap_inner, this->ds_input.data()}; \
+ }
+ static const unsigned char *EMPTY = (const unsigned char *) "";
+
+ struct _YYCURSOR {
+ YYCTYPE operator*() const {
+ if (this->val < this->lim) {
+ return *val;
+ }
+ return '\0';
+ }
+
+ operator const YYCTYPE *() const {
+ if (this->val < this->lim) {
+ return this->val;
+ }
+ return EMPTY;
+ }
+
+ const YYCTYPE *operator=(const YYCTYPE *rhs) {
+ this->val = rhs;
+ return rhs;
+ }
+
+ const YYCTYPE *operator+(int rhs) {
+ return this->val + rhs;
+ }
+
+ const _YYCURSOR *operator-=(int rhs) {
+ this->val -= rhs;
+ return this;
+ }
+
+ _YYCURSOR& operator++() {
+ this->val += 1;
+ return *this;
+ }
+
+ const YYCTYPE *val{nullptr};
+ const YYCTYPE *lim{nullptr};
+ } YYCURSOR;
+ YYCURSOR = (const unsigned char *) this->ds_input.udata() + this->ds_next_offset;
+ _YYCURSOR yyt1;
+ _YYCURSOR yyt2;
+ _YYCURSOR yyt3;
+ _YYCURSOR yyt4;
+ const YYCTYPE *YYLIMIT = (const unsigned char *) this->ds_input.end();
+ const YYCTYPE *YYMARKER = YYCURSOR;
+
+ YYCURSOR.lim = YYLIMIT;
+
+ cap_all.c_begin = this->ds_next_offset;
+ cap_all.c_end = this->ds_next_offset;
+ cap_inner.c_begin = this->ds_next_offset;
+ cap_inner.c_end = this->ds_next_offset;
+
+ /*!re2c
+ re2c:yyfill:enable = 0;
+ re2c:flags:tags = 1;
+
+ SPACE = [ \t\r];
+ ALPHA = [a-zA-Z];
+ ESC = "\x1b";
+ NUM = [0-9];
+ ALPHANUM = [a-zA-Z0-9_];
+ EOF = "\x00";
+ IPV4SEG = ("25"[0-5]|("2"[0-4]|"1"{0,1}[0-9]){0,1}[0-9]);
+ IPV4ADDR = (IPV4SEG"."){3,3}IPV4SEG;
+ IPV6SEG = [0-9a-fA-F]{1,4};
+ IPV6ADDR = (
+ (IPV6SEG":"){7,7}IPV6SEG|
+ (IPV6SEG":"){1,7}":"|
+ (IPV6SEG":"){1,6}":"IPV6SEG|
+ (IPV6SEG":"){1,5}(":"IPV6SEG){1,2}|
+ (IPV6SEG":"){1,4}(":"IPV6SEG){1,3}|
+ (IPV6SEG":"){1,3}(":"IPV6SEG){1,4}|
+ (IPV6SEG":"){1,2}(":"IPV6SEG){1,5}|
+ IPV6SEG":"((":"IPV6SEG){1,6})|
+ ":"((":"IPV6SEG){1,7}|":")|
+ [a-fA-F0-9]{4}":"(":"IPV6SEG){0,4}"%"[0-9a-zA-Z]{1,}|
+ "::"('ffff'(":0"{1,4}){0,1}":"){0,1}IPV4ADDR|
+ (IPV6SEG":"){1,4}":"IPV4ADDR
+ );
+
+ EOF { return nonstd::nullopt; }
+
+ ("u"|"r")?'"'('\\'.|[^\x00\x1b"\\]|'""')*'"' {
+ CAPTURE(DT_QUOTED_STRING);
+ switch (this->ds_input[cap_inner.c_begin]) {
+ case 'u':
+ case 'r':
+ cap_inner.c_begin += 1;
+ break;
+ }
+ cap_inner.c_begin += 1;
+ cap_inner.c_end -= 1;
+ return tokenize_result{token_out, cap_all, cap_inner, this->ds_input.data()};
+ }
+ [a-qstv-zA-QSTV-Z]"'" {
+ CAPTURE(DT_WORD);
+ }
+ ("u"|"r")?"'"('\\'.|"''"|[^\x00\x1b'\\])*"'"/[^sS] {
+ CAPTURE(DT_QUOTED_STRING);
+ switch (this->ds_input[cap_inner.c_begin]) {
+ case 'u':
+ case 'r':
+ cap_inner.c_begin += 1;
+ break;
+ }
+ cap_inner.c_begin += 1;
+ cap_inner.c_end -= 1;
+ return tokenize_result{token_out, cap_all, cap_inner, this->ds_input.data()};
+ }
+ [a-zA-Z0-9]+":/""/"?[^\x00\x1b\r\n\t '"[\](){}]+[/a-zA-Z0-9\-=&?%] { RET(DT_URL); }
+ ("/"|"./"|"../"|[A-Z]":\\"|"\\\\")("Program Files"(" (x86)")?)?[a-zA-Z0-9_\.\-\~/\\!@#$%^&*()]* { RET(DT_PATH); }
+ (SPACE|NUM)NUM":"NUM{2}/[^:] { RET(DT_TIME); }
+ (SPACE|NUM)NUM?":"NUM{2}":"NUM{2}("."NUM{3,6})?/[^:] { RET(DT_TIME); }
+ [0-9a-fA-F][0-9a-fA-F]((":"|"-")[0-9a-fA-F][0-9a-fA-F])+ {
+ if ((YYCURSOR.val - (this->ds_input.udata() + this->ds_next_offset)) == 17) {
+ RET(DT_MAC_ADDRESS);
+ } else {
+ RET(DT_HEX_DUMP);
+ }
+ }
+ (NUM{4}"/"NUM{1,2}"/"NUM{1,2}|NUM{4}"-"NUM{1,2}"-"NUM{1,2}|NUM{2}"/"ALPHA{3}"/"NUM{4})("T"|" ")NUM{2}":"NUM{2}(":"NUM{2}("."NUM{3,6})?)? {
+ RET(DT_DATE_TIME);
+ }
+ ALPHA{3}(" "NUM|" "NUM{2})" "NUM{2}":"NUM{2}(":"NUM{2}("."NUM{3,6})?)? {
+ RET(DT_DATE_TIME);
+ }
+ (NUM{4}"/"NUM{1,2}"/"NUM{1,2}|NUM{4}"-"NUM{1,2}"-"NUM{1,2}|NUM{2}"/"ALPHA{3}"/"NUM{4}) {
+ RET(DT_DATE);
+ }
+ IPV6ADDR/[^:a-zA-Z0-9] { RET(DT_IPV6_ADDRESS); }
+
+ "<!"[a-zA-Z0-9_:\-]+SPACE*([a-zA-Z0-9_:\-]+(SPACE*'='SPACE*('"'(('\\'.|[^\x00"\\])+)'"'|"'"(('\\'.|[^\x00'\\])+)"'"|[^\x00>]+))?|SPACE*('"'(('\\'.|[^\x00"\\])+)'"'|"'"(('\\'.|[^\x00'\\])+)"'"))*SPACE*">" {
+ RET(DT_XML_DECL_TAG);
+ }
+
+ "<""?"?[a-zA-Z0-9_:\-]+SPACE*([a-zA-Z0-9_:\-]+(SPACE*'='SPACE*('"'(('\\'.|[^\x00"\\])+)'"'|"'"(('\\'.|[^\x00'\\])+)"'"|[^\x00>]+))?)*SPACE*("/"|"?")">" {
+ RET(DT_XML_EMPTY_TAG);
+ }
+
+ "<"[a-zA-Z0-9_:\-]+SPACE*([a-zA-Z0-9_:\-]+(SPACE*"="SPACE*('"'(('\\'.|[^\x00"\\])+)'"'|"'"(('\\'.|[^\x00'\\])+)"'"|[^\x00>]+))?)*SPACE*">" {
+ RET(DT_XML_OPEN_TAG);
+ }
+
+ "</"[a-zA-Z0-9_:\-]+SPACE*">" {
+ RET(DT_XML_CLOSE_TAG);
+ }
+
+ "\n"[A-Z][A-Z _\-0-9]+"\n" {
+ RET(DT_H1);
+ }
+
+ ESC"["[0-9=;?]*[a-zA-Z] {
+ RET(DT_CSI);
+ }
+
+ ":" { RET(DT_COLON); }
+ "=" { RET(DT_EQUALS); }
+ "," { RET(DT_COMMA); }
+ ";" { RET(DT_SEMI); }
+ "()" | "{}" | "[]" { RET(DT_EMPTY_CONTAINER); }
+ "{" { RET(DT_LCURLY); }
+ "}" { RET(DT_RCURLY); }
+ "[" { RET(DT_LSQUARE); }
+ "]" { RET(DT_RSQUARE); }
+ "(" { RET(DT_LPAREN); }
+ ")" { RET(DT_RPAREN); }
+ "<" { RET(DT_LANGLE); }
+ ">" { RET(DT_RANGLE); }
+
+ IPV4ADDR/[^0-9] {
+ RET(DT_IPV4_ADDRESS);
+ }
+
+ [0-9a-fA-F]{8}("-"[0-9a-fA-F]{4}){3}"-"[0-9a-fA-F]{12} { RET(DT_UUID); }
+
+ (NUM{4}" "NUM{4}" "NUM{4}" "NUM{4}|NUM{16})/[^0-9] {
+ CAPTURE(DT_CREDIT_CARD_NUMBER);
+ if (!this->is_credit_card(this->to_string_fragment(cap_all))) {
+ if (cap_all.length() > 16) {
+ cap_all.c_end = cap_all.c_begin + 4;
+ cap_inner.c_end = cap_inner.c_begin + 4;
+ }
+ this->ds_next_offset = cap_all.c_end;
+ token_out = DT_NUMBER;
+ }
+ return tokenize_result{token_out, cap_all, cap_inner, this->ds_input.data()};
+ }
+
+ [0-9]"."[0-9]+'e'[\-\+][0-9]+ { RET(DT_NUMBER); }
+
+ [0-9]+("."[0-9]+[a-zA-Z0-9_]*){2,}("-"[a-zA-Z0-9_]+)?|[0-9]+("."[0-9]+[a-zA-Z0-9_]*)+"-"[a-zA-Z0-9_]+ {
+ RET(DT_VERSION_NUMBER);
+ }
+
+ "-"?"0"[0-7]+ { RET(DT_OCTAL_NUMBER); }
+ "-"?[0-9]+("."[0-9]+)?[ ]*"%" { RET(DT_PERCENTAGE); }
+ "-"?[0-9]+("."[0-9]+)?([eE][\-+][0-9]+)? { RET(DT_NUMBER); }
+ "-"?("0x"|[0-9])[0-9a-fA-F]+ { RET(DT_HEX_NUMBER); }
+
+ [a-zA-Z0-9\._%+-]+"@"[a-zA-Z0-9\.-]+"."[a-zA-Z]+ { RET(DT_EMAIL); }
+
+ "true"|"True"|"TRUE"|"false"|"False"|"FALSE"|"None"|"null"|"NULL"/([\r\n\t \(\)!\*:;'\"\?,]|[\.\!,\?]SPACE|EOF) { RET(DT_CONSTANT); }
+
+ ("re-")?[a-zA-Z][a-z']+/([\r\n\t \(\)!\*:;'\"\?,]|[\.\!,\?]SPACE|EOF) { RET(DT_WORD); }
+
+ [^\x00\x1b"; \t\r\n:=,\(\)\{\}\[\]\+#!%\^&\*'\?<>\~`\|\.\\][^\x00\x1b"; \t\r\n:=,\(\)\{\}\[\]\+#!%\^&\*'\?<>\~`\|\\]*("::"[^\x00\x1b"; \r\n\t:=,\(\)\{\}\[\]\+#!%\^&\*'\?<>\~`\|\\]+)* {
+ RET(DT_SYMBOL);
+ }
+
+ ("\r"?"\n"|"\\n") { RET(DT_LINE); }
+ SPACE+ { RET(DT_WHITE); }
+ "." { RET(DT_DOT); }
+ "\\". { RET(DT_ESCAPED_CHAR); }
+ . { RET(DT_GARBAGE); }
+
+ */
+}
diff --git a/src/db_sub_source.cc b/src/db_sub_source.cc
new file mode 100644
index 0000000..795bda8
--- /dev/null
+++ b/src/db_sub_source.cc
@@ -0,0 +1,457 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <regex>
+
+#include "db_sub_source.hh"
+
+#include "base/date_time_scanner.hh"
+#include "base/itertools.hh"
+#include "base/time_util.hh"
+#include "config.h"
+#include "scn/scn.h"
+#include "yajlpp/json_ptr.hh"
+
+const char db_label_source::NULL_STR[] = "<NULL>";
+
+constexpr size_t MAX_COLUMN_WIDTH = 120;
+constexpr size_t MAX_JSON_WIDTH = 16 * 1024;
+
+void
+db_label_source::text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& label_out,
+ text_sub_source::line_flags_t flags)
+{
+ /*
+ * start_value is the result rowid, each bucket type is a column value
+ * label_out should be the raw text output.
+ */
+
+ label_out.clear();
+ if (row >= (int) this->dls_rows.size()) {
+ return;
+ }
+ for (int lpc = 0; lpc < (int) this->dls_rows[row].size(); lpc++) {
+ auto actual_col_size
+ = std::min(MAX_COLUMN_WIDTH, this->dls_headers[lpc].hm_column_size);
+ auto cell_str = scrub_ws(this->dls_rows[row][lpc]);
+
+ truncate_to(cell_str, MAX_COLUMN_WIDTH);
+
+ auto cell_length
+ = utf8_string_length(cell_str).unwrapOr(actual_col_size);
+ auto padding = actual_col_size - cell_length;
+ this->dls_cell_width[lpc] = cell_str.length() + padding;
+ if (this->dls_headers[lpc].hm_column_type != SQLITE3_TEXT) {
+ label_out.append(padding, ' ');
+ }
+ label_out.append(cell_str);
+ if (this->dls_headers[lpc].hm_column_type == SQLITE3_TEXT) {
+ label_out.append(padding, ' ');
+ }
+ label_out.append(1, ' ');
+ }
+}
+
+void
+db_label_source::text_attrs_for_line(textview_curses& tc,
+ int row,
+ string_attrs_t& sa)
+{
+ struct line_range lr(0, 0);
+ const struct line_range lr2(0, -1);
+
+ if (row >= (int) this->dls_rows.size()) {
+ return;
+ }
+ for (size_t lpc = 0; lpc < this->dls_headers.size() - 1; lpc++) {
+ if (row % 2 == 0) {
+ sa.emplace_back(lr2, VC_STYLE.value(text_attrs{A_BOLD}));
+ }
+ lr.lr_start += this->dls_cell_width[lpc];
+ lr.lr_end = lr.lr_start + 1;
+ sa.emplace_back(lr, VC_GRAPHIC.value(ACS_VLINE));
+ lr.lr_start += 1;
+ }
+
+ int left = 0;
+ for (size_t lpc = 0; lpc < this->dls_headers.size(); lpc++) {
+ auto row_view = scn::string_view{this->dls_rows[row][lpc]};
+ const auto& hm = this->dls_headers[lpc];
+
+ if (hm.hm_graphable) {
+ auto num_scan_res = scn::scan_value<double>(row_view);
+
+ if (num_scan_res) {
+ this->dls_chart.chart_attrs_for_value(
+ tc, left, hm.hm_name, num_scan_res.value(), sa);
+ }
+ }
+ if (row_view.length() > 2 && row_view.length() < MAX_JSON_WIDTH
+ && ((row_view.front() == '{' && row_view.back() == '}')
+ || (row_view.front() == '[' && row_view.back() == ']')))
+ {
+ json_ptr_walk jpw;
+
+ if (jpw.parse(row_view.data(), row_view.length()) == yajl_status_ok
+ && jpw.complete_parse() == yajl_status_ok)
+ {
+ for (const auto& jpw_value : jpw.jpw_values) {
+ if (jpw_value.wt_type != yajl_t_number) {
+ continue;
+ }
+
+ auto num_scan_res
+ = scn::scan_value<double>(jpw_value.wt_value);
+
+ if (num_scan_res) {
+ this->dls_chart.chart_attrs_for_value(
+ tc,
+ left,
+ jpw_value.wt_ptr,
+ num_scan_res.value(),
+ sa);
+ }
+ }
+ }
+ }
+ }
+}
+
+void
+db_label_source::push_header(const std::string& colstr,
+ int type,
+ bool graphable)
+{
+ this->dls_headers.emplace_back(colstr);
+ this->dls_cell_width.push_back(0);
+
+ header_meta& hm = this->dls_headers.back();
+
+ hm.hm_column_size = utf8_string_length(colstr).unwrapOr(colstr.length());
+ hm.hm_column_type = type;
+ hm.hm_graphable = graphable;
+ if (colstr == "log_time") {
+ this->dls_time_column_index = this->dls_headers.size() - 1;
+ }
+}
+
+void
+db_label_source::push_column(const scoped_value_t& sv)
+{
+ auto& vc = view_colors::singleton();
+ int index = this->dls_rows.back().size();
+ auto& hm = this->dls_headers[index];
+
+ auto col_sf = sv.match(
+ [](const std::string& str) { return string_fragment::from_str(str); },
+ [this](const string_fragment& sf) {
+ return sf.to_owned(*this->dls_allocator);
+ },
+ [this](int64_t i) {
+ fmt::memory_buffer buf;
+
+ fmt::format_to(std::back_inserter(buf), FMT_STRING("{}"), i);
+ return string_fragment::from_memory_buffer(buf).to_owned(
+ *this->dls_allocator);
+ },
+ [this](double d) {
+ fmt::memory_buffer buf;
+
+ fmt::format_to(std::back_inserter(buf), FMT_STRING("{}"), d);
+ return string_fragment::from_memory_buffer(buf).to_owned(
+ *this->dls_allocator);
+ },
+ [](null_value_t) { return string_fragment::from_const(NULL_STR); });
+
+ if (index == this->dls_time_column_index) {
+ date_time_scanner dts;
+ struct timeval tv;
+
+ if (!dts.convert_to_timeval(
+ col_sf.data(), col_sf.length(), nullptr, tv))
+ {
+ tv.tv_sec = -1;
+ tv.tv_usec = -1;
+ }
+ if (!this->dls_time_column.empty() && tv < this->dls_time_column.back())
+ {
+ this->dls_time_column_invalidated_at = this->dls_time_column.size();
+ this->dls_time_column_index = -1;
+ this->dls_time_column.clear();
+ } else {
+ this->dls_time_column.push_back(tv);
+ }
+ }
+
+ this->dls_rows.back().push_back(col_sf.data());
+ hm.hm_column_size
+ = std::max(this->dls_headers[index].hm_column_size,
+ (size_t) utf8_string_length(col_sf.data(), col_sf.length())
+ .unwrapOr(col_sf.length()));
+
+ if ((sv.is<int64_t>() || sv.is<double>())
+ && this->dls_headers[index].hm_graphable)
+ {
+ if (sv.is<int64_t>()) {
+ this->dls_chart.add_value(hm.hm_name, sv.get<int64_t>());
+ } else {
+ this->dls_chart.add_value(hm.hm_name, sv.get<double>());
+ }
+ } else if (col_sf.length() > 2
+ && ((col_sf.startswith("{") && col_sf.endswith("}"))
+ || (col_sf.startswith("[") && col_sf.endswith("]"))))
+ {
+ json_ptr_walk jpw;
+
+ if (jpw.parse(col_sf.data(), col_sf.length()) == yajl_status_ok
+ && jpw.complete_parse() == yajl_status_ok)
+ {
+ for (const auto& jpw_value : jpw.jpw_values) {
+ if (jpw_value.wt_type != yajl_t_number) {
+ continue;
+ }
+
+ auto num_scan_res = scn::scan_value<double>(jpw_value.wt_value);
+ if (num_scan_res) {
+ this->dls_chart.add_value(jpw_value.wt_ptr,
+ num_scan_res.value());
+ this->dls_chart.with_attrs_for_ident(
+ jpw_value.wt_ptr, vc.attrs_for_ident(jpw_value.wt_ptr));
+ }
+ }
+ }
+ }
+}
+
+void
+db_label_source::clear()
+{
+ this->dls_chart.clear();
+ this->dls_headers.clear();
+ this->dls_rows.clear();
+ this->dls_time_column.clear();
+ this->dls_cell_width.clear();
+ this->dls_allocator = std::make_unique<ArenaAlloc::Alloc<char>>(64 * 1024);
+}
+
+nonstd::optional<size_t>
+db_label_source::column_name_to_index(const std::string& name) const
+{
+ return this->dls_headers | lnav::itertools::find(name);
+}
+
+nonstd::optional<vis_line_t>
+db_label_source::row_for_time(struct timeval time_bucket)
+{
+ std::vector<struct timeval>::iterator iter;
+
+ iter = std::lower_bound(this->dls_time_column.begin(),
+ this->dls_time_column.end(),
+ time_bucket);
+ if (iter != this->dls_time_column.end()) {
+ return vis_line_t(std::distance(this->dls_time_column.begin(), iter));
+ }
+ return nonstd::nullopt;
+}
+
+size_t
+db_overlay_source::list_overlay_count(const listview_curses& lv)
+{
+ size_t retval = 1;
+
+ if (!this->dos_active || lv.get_inner_height() == 0) {
+ this->dos_lines.clear();
+
+ return retval;
+ }
+
+ auto& vc = view_colors::singleton();
+ auto top = lv.get_top();
+ const auto& cols = this->dos_labels->dls_rows[top];
+ unsigned long width;
+ vis_line_t height;
+
+ lv.get_dimensions(height, width);
+
+ this->dos_lines.clear();
+ for (size_t col = 0; col < cols.size(); col++) {
+ const char* col_value = cols[col];
+ size_t col_len = strlen(col_value);
+
+ if (!(col_len >= 2
+ && ((col_value[0] == '{' && col_value[col_len - 1] == '}')
+ || (col_value[0] == '[' && col_value[col_len - 1] == ']'))))
+ {
+ continue;
+ }
+
+ json_ptr_walk jpw;
+
+ if (jpw.parse(col_value, col_len) == yajl_status_ok
+ && jpw.complete_parse() == yajl_status_ok)
+ {
+ {
+ const std::string& header
+ = this->dos_labels->dls_headers[col].hm_name;
+ this->dos_lines.emplace_back(" JSON Column: " + header);
+
+ retval += 1;
+ }
+
+ stacked_bar_chart<std::string> chart;
+ int start_line = this->dos_lines.size();
+
+ chart.with_stacking_enabled(false).with_margins(3, 0);
+
+ for (auto& jpw_value : jpw.jpw_values) {
+ this->dos_lines.emplace_back(" " + jpw_value.wt_ptr + " = "
+ + jpw_value.wt_value);
+
+ string_attrs_t& sa = this->dos_lines.back().get_attrs();
+ struct line_range lr(1, 2);
+
+ sa.emplace_back(lr, VC_GRAPHIC.value(ACS_LTEE));
+ lr.lr_start = 3 + jpw_value.wt_ptr.size() + 3;
+ lr.lr_end = -1;
+ sa.emplace_back(lr, VC_STYLE.value(text_attrs{A_BOLD}));
+
+ if (jpw_value.wt_type == yajl_t_number) {
+ auto num_scan_res
+ = scn::scan_value<double>(jpw_value.wt_value);
+
+ if (num_scan_res) {
+ auto attrs = vc.attrs_for_ident(jpw_value.wt_ptr);
+
+ chart.add_value(jpw_value.wt_ptr, num_scan_res.value());
+ chart.with_attrs_for_ident(jpw_value.wt_ptr, attrs);
+ }
+ }
+
+ retval += 1;
+ }
+
+ int curr_line = start_line;
+ for (auto iter = jpw.jpw_values.begin();
+ iter != jpw.jpw_values.end();
+ ++iter, curr_line++)
+ {
+ if (iter->wt_type != yajl_t_number) {
+ continue;
+ }
+
+ auto num_scan_res = scn::scan_value<double>(iter->wt_value);
+
+ if (num_scan_res) {
+ auto& sa = this->dos_lines[curr_line].get_attrs();
+ int left = 3;
+
+ chart.chart_attrs_for_value(
+ lv, left, iter->wt_ptr, num_scan_res.value(), sa);
+ }
+ }
+ }
+ }
+
+ if (retval > 1) {
+ this->dos_lines.emplace_back("");
+
+ string_attrs_t& sa = this->dos_lines.back().get_attrs();
+ struct line_range lr(1, 2);
+
+ sa.emplace_back(lr, VC_GRAPHIC.value(ACS_LLCORNER));
+ lr.lr_start = 2;
+ lr.lr_end = -1;
+ sa.emplace_back(lr, VC_GRAPHIC.value(ACS_HLINE));
+
+ retval += 1;
+ }
+
+ return retval;
+}
+
+bool
+db_overlay_source::list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t row,
+ attr_line_t& value_out)
+{
+ if (y == 0) {
+ this->list_overlay_count(lv);
+ std::string& line = value_out.get_string();
+ db_label_source* dls = this->dos_labels;
+ string_attrs_t& sa = value_out.get_attrs();
+
+ for (size_t lpc = 0; lpc < this->dos_labels->dls_headers.size(); lpc++)
+ {
+ auto actual_col_size = std::min(
+ MAX_COLUMN_WIDTH, dls->dls_headers[lpc].hm_column_size);
+ std::string cell_title = dls->dls_headers[lpc].hm_name;
+
+ truncate_to(cell_title, MAX_COLUMN_WIDTH);
+
+ auto cell_length
+ = utf8_string_length(cell_title).unwrapOr(actual_col_size);
+ int before, total_fill = actual_col_size - cell_length;
+ auto line_len_before = line.length();
+
+ before = total_fill / 2;
+ total_fill -= before;
+ line.append(before, ' ');
+ line.append(cell_title);
+ line.append(total_fill, ' ');
+ line.append(1, ' ');
+
+ struct line_range header_range(line_len_before, line.length());
+
+ text_attrs attrs;
+ if (this->dos_labels->dls_headers[lpc].hm_graphable) {
+ attrs = dls->dls_headers[lpc].hm_title_attrs
+ | text_attrs{A_REVERSE};
+ } else {
+ attrs.ta_attrs = A_UNDERLINE;
+ }
+ sa.emplace_back(header_range, VC_STYLE.value(text_attrs{attrs}));
+ }
+
+ struct line_range lr(0);
+
+ sa.emplace_back(lr, VC_STYLE.value(text_attrs{A_BOLD | A_UNDERLINE}));
+ return true;
+ } else if (this->dos_active && y >= 2
+ && ((size_t) y) < (this->dos_lines.size() + 2))
+ {
+ value_out = this->dos_lines[y - 2];
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/db_sub_source.hh b/src/db_sub_source.hh
new file mode 100644
index 0000000..d727989
--- /dev/null
+++ b/src/db_sub_source.hh
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef db_sub_source_hh
+#define db_sub_source_hh
+
+#include <iterator>
+#include <string>
+#include <vector>
+
+#include <sqlite3.h>
+
+#include "ArenaAlloc/arenaalloc.h"
+#include "hist_source.hh"
+#include "shlex.resolver.hh"
+#include "textview_curses.hh"
+
+class db_label_source
+ : public text_sub_source
+ , public text_time_translator {
+public:
+ ~db_label_source() override { this->clear(); }
+
+ bool has_log_time_column() const { return !this->dls_time_column.empty(); }
+
+ size_t text_line_count() override { return this->dls_rows.size(); }
+
+ size_t text_size_for_line(textview_curses& tc,
+ int line,
+ line_flags_t flags) override
+ {
+ return this->text_line_width(tc);
+ }
+
+ size_t text_line_width(textview_curses& curses) override
+ {
+ size_t retval = 0;
+
+ for (auto& dls_header : this->dls_headers) {
+ retval += dls_header.hm_column_size + 1;
+ }
+ return retval;
+ }
+
+ void text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& label_out,
+ line_flags_t flags) override;
+
+ void text_attrs_for_line(textview_curses& tc,
+ int row,
+ string_attrs_t& sa) override;
+
+ void push_header(const std::string& colstr, int type, bool graphable);
+
+ void push_column(const scoped_value_t& sv);
+
+ void clear();
+
+ nonstd::optional<size_t> column_name_to_index(
+ const std::string& name) const;
+
+ nonstd::optional<vis_line_t> row_for_time(
+ struct timeval time_bucket) override;
+
+ nonstd::optional<struct timeval> time_for_row(vis_line_t row) override
+ {
+ if ((row < 0_vl) || (((size_t) row) >= this->dls_time_column.size())) {
+ return nonstd::nullopt;
+ }
+
+ return this->dls_time_column[row];
+ }
+
+ struct header_meta {
+ explicit header_meta(std::string name) : hm_name(std::move(name)) {}
+
+ bool operator==(const std::string& name) const
+ {
+ return this->hm_name == name;
+ }
+
+ std::string hm_name;
+ int hm_column_type{SQLITE3_TEXT};
+ unsigned int hm_sub_type{0};
+ bool hm_graphable{false};
+ size_t hm_column_size{0};
+ text_attrs hm_title_attrs;
+ };
+
+ stacked_bar_chart<std::string> dls_chart;
+ std::vector<header_meta> dls_headers;
+ std::vector<std::vector<const char*>> dls_rows;
+ std::vector<struct timeval> dls_time_column;
+ std::vector<size_t> dls_cell_width;
+ int dls_time_column_index{-1};
+ nonstd::optional<size_t> dls_time_column_invalidated_at;
+ std::unique_ptr<ArenaAlloc::Alloc<char>> dls_allocator{
+ std::make_unique<ArenaAlloc::Alloc<char>>(64 * 1024)};
+
+ static const char NULL_STR[];
+};
+
+class db_overlay_source : public list_overlay_source {
+public:
+ size_t list_overlay_count(const listview_curses& lv);
+
+ bool list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t row,
+ attr_line_t& value_out) override;
+
+ bool dos_active{false};
+ db_label_source* dos_labels{nullptr};
+ std::vector<attr_line_t> dos_lines;
+};
+#endif
diff --git a/src/diseases.json b/src/diseases.json
new file mode 100644
index 0000000..8ec4914
--- /dev/null
+++ b/src/diseases.json
@@ -0,0 +1,549 @@
+{
+ "data": [
+ "achondroplasia",
+ "acinetobacter-infections",
+ "acne",
+ "actinomycosis",
+ "addisons-disease",
+ "african-sleeping-sickness",
+ "agammaglobulinemia",
+ "albinism",
+ "alcoholic-hepatitis",
+ "allergy",
+ "alopecia",
+ "alopecia-areata",
+ "alzheimers-disease",
+ "amblyopia",
+ "amebiasis",
+ "ampylobacter-infection",
+ "amyloidosis",
+ "anaplasmosis",
+ "anemia",
+ "aneurdu",
+ "ankylosing-spondylitis",
+ "anorexia",
+ "anosmia",
+ "anotia",
+ "anthrax",
+ "anti-gbm",
+ "anti-tbm-nephritis",
+ "antiphospholipid-syndrome",
+ "appendicitis",
+ "apraxia",
+ "arcanobacterium-haemolyticum-infection",
+ "argentine-hemorrhagic-fever",
+ "argyria",
+ "arthritis",
+ "ascariasis",
+ "aseptic-meningitis",
+ "aspergillosis",
+ "asthenia",
+ "asthma",
+ "astigmatism",
+ "astrovirus-infection",
+ "atherosclerosis",
+ "athetosis",
+ "atrophy",
+ "autoimmune-angioedema",
+ "autoimmune-aplastic-anemia",
+ "autoimmune-dysautonomia",
+ "autoimmune-hepatitis",
+ "autoimmune-hyperlipidemia",
+ "autoimmune-immunodeficiency",
+ "autoimmune-inner-ear-disease",
+ "autoimmune-myocarditis",
+ "autoimmune-oophoritis",
+ "autoimmune-pancreatitis",
+ "autoimmune-retinopathy",
+ "autoimmune-thrombocytopenic-purpura",
+ "autoimmune-thyroid-disease",
+ "autoimmune-urticaria",
+ "axonal-&-neuronal-neuropathies",
+ "babesiosis",
+ "bacillary-dysentery",
+ "bacillus-cereus-infection",
+ "bacterial-meningitis",
+ "bacterial-pneumonia",
+ "bacterial-vaginosis",
+ "bacteroides-infection",
+ "balantidiasis",
+ "balo-disease",
+ "barbers-itch",
+ "baylisascaris-infection",
+ "behcets-disease",
+ "beriberi",
+ "bk-virus-infection",
+ "black-death",
+ "black-piedra",
+ "blastocystis-hominis-infection",
+ "blastomycosis",
+ "bolivian-hemorrhagic-fever",
+ "borrelia-infection",
+ "botulism",
+ "brazilian-hemorrhagic-fever",
+ "breast-cancer",
+ "bronchitis",
+ "brucellosis",
+ "bubonic-plague",
+ "bullous-pemphigoid",
+ "bunion",
+ "burkholderia-infection",
+ "buruli-ulcer",
+ "calculi",
+ "calicivirus-infection",
+ "campylobacteriosis",
+ "cancer",
+ "candidiasis",
+ "carbon-monoxide-poisoning",
+ "cardiomyopathy",
+ "castleman-disease",
+ "cat-scratch-disease",
+ "celiac-disease",
+ "celiacs-disease",
+ "cellulitis",
+ "cerebral-palsy",
+ "chagas-disease",
+ "chalazion",
+ "chancroid",
+ "chavia",
+ "cherubism",
+ "chickenpox",
+ "chikungunya",
+ "chlamydia",
+ "chlamydia-trachomatis",
+ "chlamydophila-pneumoniae-infection",
+ "cholera",
+ "chordoma",
+ "chorea",
+ "chromoblastomycosis",
+ "chronic-fatigue-syndrome",
+ "chronic-inflammatory-demyelinating-polyneuropathy",
+ "chronic-recurrent-multifocal-ostomyelitis",
+ "churg-strauss-syndrome",
+ "circadian-rhythm-sleep-disorder",
+ "clonorchiasis",
+ "clostridial-myonecrosis",
+ "clostridium-difficile-infection",
+ "coccidioidomycosis",
+ "cogans-syndrome",
+ "cold-agglutinin-disease",
+ "colitis",
+ "colorado-tick-fever",
+ "common-cold",
+ "condyloma",
+ "congenital-heart-block",
+ "congestive-heart-disease",
+ "coronary-heart-disease",
+ "cowpox",
+ "coxsackie-myocarditis",
+ "crest-disease",
+ "cretinism",
+ "creutzfeldt-jakob-disease",
+ "crimean-congo-hemorrhagic-fever",
+ "crohns-disease",
+ "cryptococcosis",
+ "cryptosporidiosis",
+ "cutaneous-larva-migrans",
+ "cyclosporiasis",
+ "cysticercosis",
+ "cytomegalovirus-infection",
+ "demyelinating-neuropathies",
+ "dengue-fever",
+ "dermatitis-herpetiformis",
+ "dermatomyositis",
+ "devics-disease",
+ "diabetes-mellitus",
+ "dientamoebiasis",
+ "diphtheria",
+ "diphyllobothriasis",
+ "discoid-lupus",
+ "donovanosis",
+ "dracunculiasis",
+ "dresslers-syndrome",
+ "ear-infection",
+ "ebola",
+ "ebola-hemorrhagic-fever",
+ "echinococcosis",
+ "ehrlichiosis",
+ "elephantiasis",
+ "emphysema",
+ "encephalitis",
+ "endometriosis",
+ "enterovirus-infection",
+ "eosinophilic-esophagitis",
+ "eosinophilic-fasciitis",
+ "epidemic-typhus",
+ "epilepsy",
+ "erectile-dysfunctions",
+ "erythema-infectiosum",
+ "erythema-nodosum",
+ "essential-mixed-cryoglobulinemia",
+ "evans-syndrome",
+ "exanthem-subitum",
+ "experimental-allergic-encephalomyelitis",
+ "fasciolopsiasis",
+ "fasciolosis",
+ "fatal-familial-insomnia",
+ "fibromyalgia",
+ "fibrosing-alveolitis",
+ "fifth-disease",
+ "filariasis",
+ "foodborne-illness",
+ "free-living-amebic-infection",
+ "fusobacterium-infection",
+ "gangrene",
+ "gas-gangrene",
+ "gastroenteritis",
+ "genital-herpes",
+ "geotrichosis",
+ "gerd",
+ "gerstmann-sträussler-scheinker-syndrome",
+ "giant-cell-arteritis",
+ "giant-cell-myocarditis",
+ "giardiasis",
+ "glanders",
+ "glomerulonephritis",
+ "gnathostomiasis",
+ "goitre",
+ "gonorrhea",
+ "goodpastures-syndrome",
+ "granuloma-inguinale",
+ "graves-disease",
+ "group-a-streptococcal-infection",
+ "group-b-streptococcal-infection",
+ "guillain-barre-syndrome",
+ "haemophilus-influenzae-infection",
+ "hand-foot-and-mouth-disease",
+ "hantavirus-pulmonary-syndrome",
+ "hashimotos-encephalitis",
+ "hashimotos-thyroiditis",
+ "heart-disease",
+ "heartland-virus-disease",
+ "helicobacter-pylori-infection",
+ "hemolytic-anemia",
+ "hemolytic-uremic-syndrome",
+ "henoch-schonlein-purpura",
+ "hepatitis-a",
+ "hepatitis-b",
+ "hepatitis-c",
+ "hepatitis-d",
+ "hepatitis-e",
+ "herpes-gestationis",
+ "herpes-simplex",
+ "histoplasmosis",
+ "hiv",
+ "hookworm-infection",
+ "human-bocavirus-infection",
+ "human-ewingii-ehrlichiosis",
+ "human-granulocytic-anaplasmosis",
+ "human-metapneumovirus-infection",
+ "human-monocytic-ehrlichiosis",
+ "human-papillomavirus-(hpv)",
+ "human-parainfluenza-virus-infection",
+ "huntingtons-disease",
+ "hymenolepiasis",
+ "hypermetropia",
+ "hyperopia",
+ "hyperthyroidism",
+ "hypogammaglobulinemia",
+ "hypothyroid",
+ "hypotonia",
+ "idiopathic-pulmonary-fibrosis",
+ "idiopathic-thrombocytopenic-purpura",
+ "iga-nephropathy",
+ "igg4-related-sclerosing-disease",
+ "ignious-syndrome",
+ "immunoregulatory-lipoproteins",
+ "impetigo",
+ "inclusion-body-myositis",
+ "infertility",
+ "influenza",
+ "interstitial-cystitis",
+ "iritis",
+ "iron-deficiency-anemia",
+ "irritable-bowel-syndrome",
+ "isosporiasis",
+ "jaundice",
+ "juvenile-arthritis",
+ "juvenile-diabetes",
+ "juvenile-myositis",
+ "kawasaki-disease",
+ "kawasaki-syndrome",
+ "keloids",
+ "keratitis",
+ "kingella-kingae-infection",
+ "kuru",
+ "kwashiorkor",
+ "lambert-eaton-syndrome",
+ "laryngitis",
+ "lassa-fever",
+ "lead-poisoning",
+ "legionellosis",
+ "legionnaires-disease",
+ "leishmaniasis",
+ "leprosy",
+ "leptospirosis",
+ "leukemia",
+ "leukocytoclastic-vasculitis",
+ "lice",
+ "lichen-planus",
+ "lichen-sclerosus",
+ "ligneous-conjunctivitis",
+ "listeriosis",
+ "lockjaw",
+ "loiasis",
+ "lung-cancer",
+ "lupus",
+ "lupus-erythematosus",
+ "lyme-disease",
+ "lymphocytic-choriomeningitis",
+ "lymphogranuloma-venereum",
+ "lymphoma",
+ "mad-cow-disease",
+ "malaria",
+ "marburg-fever",
+ "marburg-hemorrhagic-fever",
+ "mattticular-syndrome",
+ "measles",
+ "melanoma",
+ "melioidosis",
+ "menieres-disease",
+ "meningitis",
+ "meningococcal-disease",
+ "metagonimiasis",
+ "metastatic-cancer",
+ "microscopic-polyangiitis",
+ "microsporidiosis",
+ "middle-east-respiratory-syndrome",
+ "migraine",
+ "mixed-connective-tissue-disease",
+ "molluscum-contagiosum",
+ "monkeypox",
+ "mononucleosis",
+ "moorens-ulcer",
+ "morquio-syndrome",
+ "mucha-habermann-disease",
+ "multiple-myeloma",
+ "multiple-sclerosis",
+ "mumps",
+ "murine-typhus",
+ "muscular-dystrophy",
+ "myasthenia-gravis",
+ "mycetoma",
+ "mycoplasma-pneumonia",
+ "myelitis",
+ "myiasis",
+ "myoclonus",
+ "myopia",
+ "myositis",
+ "myxedema",
+ "ménières-disease",
+ "narcolepsy",
+ "necrotizing-fasciitis",
+ "neonatal-conjunctivitis",
+ "neoplasm",
+ "neuromyelitis-optica",
+ "neutropenia",
+ "night-blindness",
+ "nocardiosis",
+ "non-gonococcal-urethritis",
+ "obesity",
+ "ocular-cicatricial-pemphigoid",
+ "onchocerciasis",
+ "optic-neuritis",
+ "osteoarthritis",
+ "osteoporosis",
+ "otitis",
+ "palindromic-rheumatism",
+ "paracoccidioidomycosis",
+ "paragonimiasis",
+ "paraneoplastic-cerebellar-degeneration",
+ "paratyphoid-fever",
+ "parkinsons-disease",
+ "paroxysmal-nocturnal-hemoglobinuria",
+ "parry-romberg-syndrome",
+ "pars-planitis",
+ "parsonnage-turner-syndrome",
+ "pasteurellosis",
+ "pediculosis-capitis-(head-lice)",
+ "pediculosis-corporis-(body-lice)",
+ "pediculosis-pubis-(pubic-lice)",
+ "pelvic-inflammatory-disease",
+ "pemphigus",
+ "periodontal-disease",
+ "peripheral-neuropathy",
+ "peritonitis",
+ "perivenous-encephalomyelitis",
+ "pernicious-anemia",
+ "pertussis",
+ "phenylketonuria",
+ "pilia",
+ "pinworm-infection",
+ "plague",
+ "pneumococcal-infection",
+ "pneumocystis-pneumonia",
+ "pneumonia",
+ "pneumonia",
+ "poems-syndrome",
+ "poliomyelitis",
+ "polyarteritis-nodosa",
+ "polymyalgia-rheumatica",
+ "polymyositis",
+ "pontiac-fever",
+ "porphyria",
+ "postmyocardial-infarction-syndrome",
+ "postpericardiotomy-syndrome",
+ "prevotella-infection",
+ "primary-amoebic-meningoencephalitis",
+ "primary-biliary-cirrhosis",
+ "primary-sclerosing-cholangitis",
+ "progeria",
+ "progesterone-dermatitis",
+ "progressive-multifocal-leukoencephalopathy",
+ "prostatitis",
+ "psittacosis",
+ "psoriasis",
+ "psoriatic-arthritis",
+ "pubic-lice",
+ "pulmonary-embolism",
+ "pure-red-cell-aplasia",
+ "pyoderma-gangrenosum",
+ "q-fever",
+ "ques-fever",
+ "rabies",
+ "rat-bite-fever",
+ "raynauds-phenomenon",
+ "reactive-arthritis",
+ "reflex-sympathetic-dystrophy",
+ "reiters-syndrome",
+ "relapsing-polychondritis",
+ "repetitive-strain-injury",
+ "respiratory-syncytial-virus-infection",
+ "restless-legs-syndrome",
+ "retroperitoneal-fibrosis",
+ "rheumatic-fever",
+ "rheumatic-heart",
+ "rheumatism",
+ "rheumatoid-arthritis",
+ "rhinosporidiosis",
+ "rhinovirus-infection",
+ "rickets",
+ "rickettsial-infection",
+ "rickettsialpox",
+ "rift-valley-fever",
+ "ringworm",
+ "river-blindness",
+ "rocky-mountain-spotted-fever",
+ "rotavirus-infection",
+ "rubella",
+ "salmonellosis",
+ "sarcoidosis",
+ "sars",
+ "scabies",
+ "scarlet-fever",
+ "schistosomiasis",
+ "schizophrenia",
+ "schmidt-syndrome",
+ "sciatica",
+ "scleritis",
+ "scleroderma",
+ "scrapie",
+ "scurvy",
+ "sepsis",
+ "septicemia",
+ "shigellosis",
+ "shin-splints",
+ "shingles",
+ "sickle-cell-anemia",
+ "siderosis",
+ "sids",
+ "silicosis",
+ "sixth-disease",
+ "sjogrens-syndrome",
+ "smallpox",
+ "south-american-blastomycosis",
+ "sperm-&-testicular-autoimmunity",
+ "sporotrichosis",
+ "staphylococcal-food-poisoning",
+ "staphylococcal-infection",
+ "stevens-johnson-syndrome",
+ "stiff-person-syndrome",
+ "stomach-flu",
+ "stomach-ulcers",
+ "strabismus",
+ "strep-throat",
+ "streptococcal-infection",
+ "strongyloidiasis",
+ "subacute-bacterial-endocarditis",
+ "subacute-sclerosing-panencephalitis",
+ "susacs-syndrome",
+ "swine-influenza",
+ "sympathetic-ophthalmia",
+ "synovitis",
+ "syphilis",
+ "taeniasis",
+ "takayasus-arteritis",
+ "tay-sachs-disease",
+ "temporal-arteritis/giant-cell-arteritis",
+ "tennis-elbow",
+ "teratoma",
+ "tetanus",
+ "thalassaemia",
+ "thrombocytopenic-purpura",
+ "thrush",
+ "thymoma",
+ "tinea-barbae",
+ "tinnitus",
+ "tolosa-hunt-syndrome",
+ "tonsillitis",
+ "tooth-decay",
+ "toxic-shock-syndrome",
+ "toxocariasis",
+ "transverse-myelitis",
+ "trichinosis",
+ "trichomoniasis",
+ "trichuriasis",
+ "trinochccliasis",
+ "trisomy",
+ "tuberculosis",
+ "tularemia",
+ "tumor",
+ "tungiasis",
+ "type-1-diabetes",
+ "typhoid-fever",
+ "typhus",
+ "ulcerative-colitis",
+ "ulcers",
+ "undifferentiated-connective-tissue-disease",
+ "ureaplasma-urealyticum-infection",
+ "uremia",
+ "urticaria",
+ "uveitis",
+ "valley-fever",
+ "varicella",
+ "varicose-veins",
+ "vasculitis",
+ "vasovagal-syncope",
+ "venezuelan-equine-encephalitis",
+ "venezuelan-hemorrhagic-fever",
+ "vesiculobullous-dermatosis",
+ "viral-fever",
+ "viral-meningitis",
+ "viral-pneumonia",
+ "vitiligo",
+ "von-hippel-lindau-disease",
+ "warkany-syndrome",
+ "warts",
+ "watkins",
+ "wegeners-granulomatosis",
+ "west-nile-fever",
+ "whipworm-infection",
+ "white-piedra",
+ "whitmores-disease",
+ "whooping-cough",
+ "yellow-fever",
+ "yersinia-pseudotuberculosis-infection",
+ "yersiniosis",
+ "zygomycosis"
+ ]
+} \ No newline at end of file
diff --git a/src/doc_status_source.hh b/src/doc_status_source.hh
new file mode 100644
index 0000000..0624fa4
--- /dev/null
+++ b/src/doc_status_source.hh
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_doc_status_source_hh
+#define lnav_doc_status_source_hh
+
+#include <string>
+
+#include "statusview_curses.hh"
+
+class doc_status_source : public status_data_source {
+public:
+ typedef enum {
+ TSF_TITLE,
+ TSF_STITCH_TITLE,
+ TSF_DESCRIPTION,
+
+ TSF__MAX
+ } field_t;
+
+ doc_status_source()
+ {
+ this->tss_fields[TSF_TITLE].set_width(14);
+ this->tss_fields[TSF_TITLE].set_left_pad(1);
+ this->tss_fields[TSF_TITLE].set_role(role_t::VCR_STATUS_TITLE);
+ this->tss_fields[TSF_STITCH_TITLE].set_width(2);
+ this->tss_fields[TSF_STITCH_TITLE].set_stitch_value(
+ role_t::VCR_STATUS_STITCH_TITLE_TO_NORMAL,
+ role_t::VCR_STATUS_STITCH_NORMAL_TO_TITLE);
+ this->tss_fields[TSF_DESCRIPTION].set_share(1);
+ this->tss_fields[TSF_DESCRIPTION].set_role(role_t::VCR_STATUS);
+ }
+
+ size_t statusview_fields() override { return TSF__MAX; }
+
+ status_field& statusview_value_for_field(int field) override
+ {
+ return this->tss_fields[field];
+ }
+
+ void set_title(const std::string& title)
+ {
+ this->tss_fields[TSF_TITLE].set_value(title);
+ }
+
+ void set_description(const std::string& description)
+ {
+ this->tss_fields[TSF_DESCRIPTION].set_value(description);
+ }
+
+private:
+ status_field tss_fields[TSF__MAX];
+};
+
+#endif
diff --git a/src/document.sections.cc b/src/document.sections.cc
new file mode 100644
index 0000000..04eb516
--- /dev/null
+++ b/src/document.sections.cc
@@ -0,0 +1,544 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <utility>
+#include <vector>
+
+#include "document.sections.hh"
+
+#include "base/enum_util.hh"
+#include "base/itertools.hh"
+#include "base/lnav_log.hh"
+#include "base/opt_util.hh"
+#include "data_scanner.hh"
+
+namespace lnav {
+namespace document {
+
+nonstd::optional<hier_node*>
+hier_node::lookup_child(section_key_t key) const
+{
+ return make_optional_from_nullable(key.match(
+ [this](const std::string& str) -> hier_node* {
+ auto iter = this->hn_named_children.find(str);
+ if (iter != this->hn_named_children.end()) {
+ return iter->second;
+ }
+ return nullptr;
+ },
+ [this](size_t index) -> hier_node* {
+ if (index < this->hn_children.size()) {
+ return this->hn_children[index].get();
+ }
+ return nullptr;
+ }));
+}
+
+nonstd::optional<const hier_node*>
+hier_node::lookup_path(const hier_node* root,
+ const std::vector<section_key_t>& path)
+{
+ auto retval = make_optional_from_nullable(root);
+
+ for (const auto& comp : path) {
+ if (!retval) {
+ break;
+ }
+
+ retval = retval.value()->lookup_child(comp);
+ }
+
+ if (!retval) {
+ return nonstd::nullopt;
+ }
+
+ return retval;
+}
+
+struct metadata_builder {
+ std::vector<section_interval_t> mb_intervals;
+ std::unique_ptr<hier_node> mb_root_node;
+
+ metadata to_metadata() &&
+ {
+ return {
+ std::move(this->mb_intervals),
+ std::move(this->mb_root_node),
+ };
+ }
+};
+
+static void
+discover_metadata_int(const attr_line_t& al, metadata_builder& mb)
+{
+ const auto& orig_attrs = al.get_attrs();
+ auto headers = orig_attrs
+ | lnav::itertools::filter_in([](const string_attr& attr) {
+ if (attr.sa_type != &VC_ROLE) {
+ return false;
+ }
+
+ auto role = attr.sa_value.get<role_t>();
+ switch (role) {
+ case role_t::VCR_H1:
+ case role_t::VCR_H2:
+ case role_t::VCR_H3:
+ case role_t::VCR_H4:
+ case role_t::VCR_H5:
+ case role_t::VCR_H6:
+ return true;
+ default:
+ return false;
+ }
+ })
+ | lnav::itertools::sort_by(&string_attr::sa_range);
+
+ // Remove headers from quoted text
+ for (const auto& orig_attr : orig_attrs) {
+ if (orig_attr.sa_type == &VC_ROLE
+ && orig_attr.sa_value.get<role_t>() == role_t::VCR_QUOTED_TEXT)
+ {
+ remove_string_attr(headers, orig_attr.sa_range);
+ }
+ }
+
+ auto& intervals = mb.mb_intervals;
+
+ struct open_interval_t {
+ open_interval_t(uint32_t level, file_off_t start, section_key_t id)
+ : oi_level(level), oi_start(start), oi_id(std::move(id))
+ {
+ }
+
+ int32_t oi_level;
+ file_off_t oi_start;
+ section_key_t oi_id;
+ std::unique_ptr<hier_node> oi_node{std::make_unique<hier_node>()};
+ };
+ std::vector<open_interval_t> open_intervals;
+ auto root_node = std::make_unique<hier_node>();
+
+ for (const auto& hdr_attr : headers) {
+ auto role = hdr_attr.sa_value.get<role_t>();
+ auto role_num = lnav::enums::to_underlying(role)
+ - lnav::enums::to_underlying(role_t::VCR_H1);
+ std::vector<open_interval_t> new_open_intervals;
+
+ for (auto& oi : open_intervals) {
+ if (oi.oi_level >= role_num) {
+ // close out this section
+ intervals.emplace_back(
+ oi.oi_start, hdr_attr.sa_range.lr_start - 1, oi.oi_id);
+ auto* node_ptr = oi.oi_node.get();
+ auto* parent_node = oi.oi_node->hn_parent;
+ if (parent_node != nullptr) {
+ parent_node->hn_children.emplace_back(
+ std::move(oi.oi_node));
+ parent_node->hn_named_children.insert({
+ oi.oi_id.get<std::string>(),
+ node_ptr,
+ });
+ }
+ } else {
+ new_open_intervals.emplace_back(std::move(oi));
+ }
+ }
+ auto* parent_node = new_open_intervals.empty()
+ ? root_node.get()
+ : new_open_intervals.back().oi_node.get();
+ new_open_intervals.emplace_back(role_num,
+ hdr_attr.sa_range.lr_start,
+ al.get_substring(hdr_attr.sa_range));
+ new_open_intervals.back().oi_node->hn_parent = parent_node;
+ new_open_intervals.back().oi_node->hn_start
+ = hdr_attr.sa_range.lr_start;
+
+ open_intervals = std::move(new_open_intervals);
+ }
+
+ for (auto& oi : open_intervals) {
+ // close out this section
+ intervals.emplace_back(oi.oi_start, al.length(), oi.oi_id);
+ auto* node_ptr = oi.oi_node.get();
+ auto* parent_node = oi.oi_node->hn_parent;
+ if (parent_node == nullptr) {
+ root_node = std::move(oi.oi_node);
+ } else {
+ parent_node->hn_children.emplace_back(std::move(oi.oi_node));
+ parent_node->hn_named_children.insert({
+ oi.oi_id.get<std::string>(),
+ node_ptr,
+ });
+ }
+ }
+
+ for (auto& interval : intervals) {
+ auto start_off_iter = find_string_attr_containing(
+ orig_attrs, &SA_ORIGIN_OFFSET, interval.start);
+ if (start_off_iter != orig_attrs.end()) {
+ interval.start += start_off_iter->sa_value.get<int64_t>();
+ }
+ auto stop_off_iter = find_string_attr_containing(
+ orig_attrs, &SA_ORIGIN_OFFSET, interval.stop - 1);
+ if (stop_off_iter != orig_attrs.end()) {
+ interval.stop += stop_off_iter->sa_value.get<int64_t>();
+ }
+ }
+
+ hier_node::depth_first(root_node.get(), [&orig_attrs](hier_node* node) {
+ auto off_opt
+ = get_string_attr(orig_attrs, &SA_ORIGIN_OFFSET, node->hn_start);
+
+ if (off_opt) {
+ node->hn_start += off_opt.value()->sa_value.get<int64_t>();
+ }
+ });
+
+ if (!root_node->hn_children.empty()
+ || !root_node->hn_named_children.empty())
+ {
+ mb.mb_root_node = std::move(root_node);
+ }
+}
+
+metadata
+discover_metadata(const attr_line_t& al)
+{
+ metadata_builder mb;
+
+ discover_metadata_int(al, mb);
+
+ return std::move(mb).to_metadata();
+}
+
+class structure_walker {
+public:
+ explicit structure_walker(attr_line_t& al, line_range lr)
+ : sw_line(al), sw_range(lr),
+ sw_scanner(string_fragment::from_str_range(
+ al.get_string(), lr.lr_start, lr.lr_end))
+ {
+ this->sw_interval_state.resize(1);
+ this->sw_hier_nodes.push_back(std::make_unique<hier_node>());
+ }
+
+ metadata walk()
+ {
+ metadata_builder mb;
+ size_t garbage_count = 0;
+
+ while (garbage_count < 1000) {
+ auto tokenize_res = this->sw_scanner.tokenize2();
+ if (!tokenize_res) {
+ break;
+ }
+
+ auto dt = tokenize_res->tr_token;
+ element el(tokenize_res->tr_token, tokenize_res->tr_capture);
+
+ switch (dt) {
+ case DT_XML_DECL_TAG:
+ case DT_XML_EMPTY_TAG:
+ this->sw_values.emplace_back(el);
+ break;
+ case DT_XML_OPEN_TAG:
+ this->flush_values();
+ this->sw_interval_state.back().is_start
+ = el.e_capture.c_begin;
+ this->sw_interval_state.back().is_line_number
+ = this->sw_line_number;
+ this->sw_interval_state.back().is_name
+ = tokenize_res->to_string();
+ this->sw_depth += 1;
+ this->sw_interval_state.resize(this->sw_depth + 1);
+ this->sw_hier_nodes.push_back(
+ std::make_unique<hier_node>());
+ break;
+ case DT_XML_CLOSE_TAG: {
+ auto term = this->flush_values();
+ if (this->sw_depth > 0) {
+ if (term) {
+ this->append_child_node(term);
+ }
+ this->sw_interval_state.pop_back();
+ this->sw_hier_stage
+ = std::move(this->sw_hier_nodes.back());
+ this->sw_hier_nodes.pop_back();
+ }
+ this->append_child_node(el.e_capture);
+ if (this->sw_depth > 0) {
+ this->sw_depth -= 1;
+ }
+ this->flush_values();
+ break;
+ }
+ case DT_H1: {
+ this->sw_line.get_attrs().emplace_back(
+ line_range{
+ this->sw_range.lr_start + el.e_capture.c_begin + 1,
+ this->sw_range.lr_start + el.e_capture.c_end - 1,
+ },
+ VC_ROLE.value(role_t::VCR_H1));
+ this->sw_line_number += 2;
+ break;
+ }
+ case DT_LCURLY:
+ case DT_LSQUARE:
+ case DT_LPAREN: {
+ this->flush_values();
+ // this->append_child_node(term);
+ this->sw_depth += 1;
+ this->sw_interval_state.back().is_start
+ = el.e_capture.c_begin;
+ this->sw_interval_state.back().is_line_number
+ = this->sw_line_number;
+ this->sw_interval_state.resize(this->sw_depth + 1);
+ this->sw_hier_nodes.push_back(
+ std::make_unique<hier_node>());
+ break;
+ }
+ case DT_RCURLY:
+ case DT_RSQUARE:
+ case DT_RPAREN: {
+ auto term = this->flush_values();
+ if (this->sw_depth > 0) {
+ this->append_child_node(term);
+ this->sw_depth -= 1;
+ this->sw_interval_state.pop_back();
+ this->sw_hier_stage
+ = std::move(this->sw_hier_nodes.back());
+ this->sw_hier_nodes.pop_back();
+ if (this->sw_interval_state.back().is_start) {
+ data_scanner::capture_t obj_cap = {
+ static_cast<int>(this->sw_interval_state.back()
+ .is_start.value()),
+ el.e_capture.c_end,
+ };
+
+ auto sf
+ = this->sw_scanner.to_string_fragment(obj_cap);
+ if (!sf.find('\n')) {
+ this->sw_hier_stage->hn_named_children.clear();
+ this->sw_hier_stage->hn_children.clear();
+ while (!this->sw_intervals.empty()
+ && this->sw_intervals.back().start
+ > obj_cap.c_begin)
+ {
+ this->sw_intervals.pop_back();
+ }
+ }
+ }
+ }
+ this->sw_values.emplace_back(el);
+ break;
+ }
+ case DT_COMMA:
+ if (this->sw_depth > 0) {
+ auto term = this->flush_values();
+ this->append_child_node(term);
+ }
+ break;
+ case DT_LINE:
+ this->sw_line_number += 1;
+ break;
+ case DT_WHITE:
+ break;
+ default:
+ if (dt == DT_GARBAGE) {
+ garbage_count += 1;
+ }
+ this->sw_values.emplace_back(el);
+ break;
+ }
+ }
+ this->flush_values();
+
+ if (this->sw_hier_stage != nullptr) {
+ this->sw_hier_stage->hn_parent = this->sw_hier_nodes.back().get();
+ this->sw_hier_nodes.back()->hn_children.push_back(
+ std::move(this->sw_hier_stage));
+ }
+ this->sw_hier_stage = std::move(this->sw_hier_nodes.back());
+ this->sw_hier_nodes.pop_back();
+ if (this->sw_hier_stage->hn_children.size() == 1
+ && this->sw_hier_stage->hn_named_children.empty())
+ {
+ this->sw_hier_stage
+ = std::move(this->sw_hier_stage->hn_children.front());
+ this->sw_hier_stage->hn_parent = nullptr;
+ }
+
+ mb.mb_root_node = std::move(this->sw_hier_stage);
+ mb.mb_intervals = std::move(this->sw_intervals);
+
+ discover_metadata_int(this->sw_line, mb);
+
+ return std::move(mb).to_metadata();
+ }
+
+private:
+ struct element {
+ element(data_token_t token, data_scanner::capture_t& cap)
+ : e_token(token), e_capture(cap)
+ {
+ }
+
+ data_token_t e_token;
+ data_scanner::capture_t e_capture;
+ };
+
+ struct interval_state {
+ nonstd::optional<file_off_t> is_start;
+ size_t is_line_number{0};
+ std::string is_name;
+ };
+
+ nonstd::optional<data_scanner::capture_t> flush_values()
+ {
+ nonstd::optional<data_scanner::capture_t> last_key;
+ nonstd::optional<data_scanner::capture_t> retval;
+
+ if (!this->sw_values.empty()) {
+ if (!this->sw_interval_state.back().is_start) {
+ this->sw_interval_state.back().is_start
+ = this->sw_values.front().e_capture.c_begin;
+ this->sw_interval_state.back().is_line_number
+ = this->sw_line_number;
+ }
+ retval = this->sw_values.back().e_capture;
+ }
+ for (const auto& el : this->sw_values) {
+ switch (el.e_token) {
+ case DT_SYMBOL:
+ case DT_CONSTANT:
+ case DT_WORD:
+ case DT_QUOTED_STRING:
+ last_key = el.e_capture;
+ break;
+ case DT_COLON:
+ case DT_EQUALS:
+ if (last_key) {
+ this->sw_interval_state.back().is_name
+ = this->sw_scanner
+ .to_string_fragment(last_key.value())
+ .to_string();
+ if (!this->sw_interval_state.back().is_name.empty()) {
+ this->sw_interval_state.back().is_start
+ = static_cast<ssize_t>(
+ last_key.value().c_begin);
+ this->sw_interval_state.back().is_line_number
+ = this->sw_line_number;
+ }
+ last_key = nonstd::nullopt;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ this->sw_values.clear();
+
+ return retval;
+ }
+
+ void append_child_node(nonstd::optional<data_scanner::capture_t> terminator)
+ {
+ auto& ivstate = this->sw_interval_state.back();
+ if (!ivstate.is_start || !terminator || this->sw_depth == 0) {
+ ivstate.is_start = nonstd::nullopt;
+ ivstate.is_line_number = 0;
+ ivstate.is_name.clear();
+ return;
+ }
+
+ auto new_node = this->sw_hier_stage != nullptr
+ ? std::move(this->sw_hier_stage)
+ : std::make_unique<lnav::document::hier_node>();
+ auto iv_start = ivstate.is_start.value();
+ auto iv_stop = static_cast<ssize_t>(terminator.value().c_end);
+ auto* top_node = this->sw_hier_nodes.back().get();
+ auto new_key = ivstate.is_name.empty()
+ ? lnav::document::section_key_t{top_node->hn_children.size()}
+ : lnav::document::section_key_t{ivstate.is_name};
+ this->sw_intervals.emplace_back(iv_start, iv_stop, new_key);
+ auto* retval = new_node.get();
+ new_node->hn_parent = top_node;
+ new_node->hn_start = this->sw_intervals.back().start;
+ new_node->hn_line_number = ivstate.is_line_number;
+ if (!ivstate.is_name.empty()) {
+ top_node->hn_named_children.insert({
+ ivstate.is_name,
+ retval,
+ });
+ }
+ top_node->hn_children.emplace_back(std::move(new_node));
+ ivstate.is_start = nonstd::nullopt;
+ ivstate.is_line_number = 0;
+ ivstate.is_name.clear();
+ }
+
+ attr_line_t& sw_line;
+ line_range sw_range;
+ data_scanner sw_scanner;
+ int sw_depth{0};
+ size_t sw_line_number{0};
+ std::vector<element> sw_values{};
+ std::vector<interval_state> sw_interval_state;
+ std::vector<lnav::document::section_interval_t> sw_intervals;
+ std::vector<std::unique_ptr<lnav::document::hier_node>> sw_hier_nodes;
+ std::unique_ptr<lnav::document::hier_node> sw_hier_stage;
+};
+
+metadata
+discover_structure(attr_line_t& al, struct line_range lr)
+{
+ return structure_walker(al, lr).walk();
+}
+
+std::vector<breadcrumb::possibility>
+metadata::possibility_provider(const std::vector<section_key_t>& path)
+{
+ std::vector<breadcrumb::possibility> retval;
+ auto curr_node = lnav::document::hier_node::lookup_path(
+ this->m_sections_root.get(), path);
+ if (curr_node) {
+ auto* parent_node = curr_node.value()->hn_parent;
+
+ if (parent_node != nullptr) {
+ for (const auto& sibling : parent_node->hn_named_children) {
+ retval.template emplace_back(sibling.first);
+ }
+ }
+ }
+ return retval;
+}
+
+} // namespace document
+} // namespace lnav
diff --git a/src/document.sections.hh b/src/document.sections.hh
new file mode 100644
index 0000000..94cd01a
--- /dev/null
+++ b/src/document.sections.hh
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_attr_line_breadcrumbs_hh
+#define lnav_attr_line_breadcrumbs_hh
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/attr_line.hh"
+#include "base/file_range.hh"
+#include "breadcrumb.hh"
+#include "intervaltree/IntervalTree.h"
+#include "mapbox/variant.hpp"
+#include "optional.hpp"
+
+namespace lnav {
+namespace document {
+
+using section_key_t = mapbox::util::variant<std::string, size_t>;
+using section_interval_t = interval_tree::Interval<file_off_t, section_key_t>;
+using sections_tree_t = interval_tree::IntervalTree<file_off_t, section_key_t>;
+
+struct hier_node {
+ hier_node* hn_parent{nullptr};
+ file_off_t hn_start{0};
+ size_t hn_line_number{0};
+ std::multimap<std::string, hier_node*> hn_named_children;
+ std::vector<std::unique_ptr<hier_node>> hn_children;
+
+ nonstd::optional<hier_node*> lookup_child(section_key_t key) const;
+
+ nonstd::optional<size_t> find_line_number(const std::string& str) const
+ {
+ auto iter = this->hn_named_children.find(str);
+ if (iter != this->hn_named_children.end()) {
+ return nonstd::make_optional(iter->second->hn_line_number);
+ }
+
+ return nonstd::nullopt;
+ }
+
+ nonstd::optional<size_t> find_line_number(size_t index) const
+ {
+ if (index < this->hn_children.size()) {
+ return nonstd::make_optional(
+ this->hn_children[index]->hn_line_number);
+ }
+ return nonstd::nullopt;
+ }
+
+ bool is_named_only() const
+ {
+ return this->hn_children.size() == this->hn_named_children.size();
+ }
+
+ static nonstd::optional<const hier_node*> lookup_path(
+ const hier_node* root, const std::vector<section_key_t>& path);
+
+ template<typename F>
+ static void depth_first(hier_node* root, F func)
+ {
+ if (root == nullptr) {
+ return;
+ }
+
+ for (auto& child : root->hn_children) {
+ depth_first(child.get(), func);
+ }
+ func(root);
+ }
+};
+
+struct metadata {
+ sections_tree_t m_sections_tree;
+ std::unique_ptr<hier_node> m_sections_root;
+
+ std::vector<breadcrumb::possibility> possibility_provider(
+ const std::vector<section_key_t>& path);
+};
+
+metadata discover_metadata(const attr_line_t& al);
+
+metadata discover_structure(attr_line_t& al, struct line_range lr);
+
+} // namespace document
+} // namespace lnav
+
+#endif
diff --git a/src/dump_internals.cc b/src/dump_internals.cc
new file mode 100644
index 0000000..8317769
--- /dev/null
+++ b/src/dump_internals.cc
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "dump_internals.hh"
+
+#include "lnav.events.hh"
+#include "lnav.hh"
+#include "lnav_config.hh"
+#include "log_format_loader.hh"
+#include "sql_help.hh"
+#include "view_helpers.examples.hh"
+#include "yajlpp/yajlpp.hh"
+
+namespace lnav {
+
+void
+dump_internals(const char* internals_dir)
+{
+ for (const auto* handlers :
+ std::initializer_list<const json_path_container*>{
+ &lnav_config_handlers,
+ &root_format_handler,
+ &lnav::events::file::open::handlers,
+ &lnav::events::file::format_detected::handlers,
+ &lnav::events::log::msg_detected::handlers,
+ &lnav::events::session::loaded::handlers,
+ })
+ {
+ dump_schema_to(*handlers, internals_dir);
+ }
+
+ execute_examples();
+
+ auto cmd_ref_path = ghc::filesystem::path(internals_dir) / "cmd-ref.rst";
+ auto cmd_file = std::unique_ptr<FILE, decltype(&fclose)>(
+ fopen(cmd_ref_path.c_str(), "w+"), fclose);
+
+ if (cmd_file != nullptr) {
+ std::set<readline_context::command_t*> unique_cmds;
+
+ for (auto& cmd : lnav_commands) {
+ if (unique_cmds.find(cmd.second) != unique_cmds.end()) {
+ continue;
+ }
+ unique_cmds.insert(cmd.second);
+ format_help_text_for_rst(
+ cmd.second->c_help, eval_example, cmd_file.get());
+ }
+ }
+
+ auto sql_ref_path = ghc::filesystem::path(internals_dir) / "sql-ref.rst";
+ auto sql_file = std::unique_ptr<FILE, decltype(&fclose)>(
+ fopen(sql_ref_path.c_str(), "w+"), fclose);
+ std::set<help_text*> unique_sql_help;
+
+ if (sql_file != nullptr) {
+ for (auto& sql : sqlite_function_help) {
+ if (unique_sql_help.find(sql.second) != unique_sql_help.end()) {
+ continue;
+ }
+ unique_sql_help.insert(sql.second);
+ format_help_text_for_rst(*sql.second, eval_example, sql_file.get());
+ }
+ }
+}
+
+} // namespace lnav
diff --git a/src/dump_internals.hh b/src/dump_internals.hh
new file mode 100644
index 0000000..52a4815
--- /dev/null
+++ b/src/dump_internals.hh
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef dump_internals_hh
+#define dump_internals_hh
+
+#include <string>
+
+namespace lnav {
+
+void dump_internals(const char* dir);
+
+} // namespace lnav
+
+#endif
diff --git a/src/elem_to_json.cc b/src/elem_to_json.cc
new file mode 100644
index 0000000..4b04dfb
--- /dev/null
+++ b/src/elem_to_json.cc
@@ -0,0 +1,222 @@
+/**
+ * Copyright (c) 2016, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+
+#include "elem_to_json.hh"
+
+#include "base/itertools.hh"
+#include "config.h"
+#include "yajlpp/yajlpp.hh"
+
+static void
+element_to_json(yajl_gen gen, data_parser& dp, const data_parser::element& elem)
+{
+ size_t value_len;
+ const char* value_str = dp.get_element_string(elem, value_len);
+
+ switch (elem.value_token()) {
+ case DT_NUMBER: {
+ yajl_gen_number(gen, value_str, value_len);
+ break;
+ }
+ case DNT_GROUP: {
+ elements_to_json(
+ gen, dp, elem.get_value_elem().e_sub_elements, false);
+ break;
+ }
+ case DNT_PAIR: {
+ const data_parser::element& pair_elem = elem.get_pair_elem();
+ const auto key_str
+ = dp.get_element_string(pair_elem.e_sub_elements->front());
+
+ if (!key_str.empty()) {
+ yajlpp_map singleton_map(gen);
+
+ singleton_map.gen(key_str);
+ element_to_json(gen, dp, pair_elem.get_pair_value());
+ } else {
+ element_to_json(gen, dp, pair_elem.get_pair_value());
+ }
+ break;
+ }
+ case DT_CONSTANT: {
+ if (strncasecmp("true", value_str, value_len) == 0) {
+ yajl_gen_bool(gen, true);
+ } else if (strncasecmp("false", value_str, value_len) == 0) {
+ yajl_gen_bool(gen, false);
+ } else {
+ yajl_gen_null(gen);
+ }
+ break;
+ }
+ default:
+ yajl_gen_pstring(gen, value_str, value_len);
+ break;
+ }
+}
+
+static void
+map_elements_to_json2(yajl_gen gen,
+ data_parser& dp,
+ data_parser::element_list_t* el)
+{
+ yajlpp_map root_map(gen);
+ int col = 0;
+
+ for (auto& iter : *el) {
+ if (iter.e_token != DNT_PAIR) {
+ log_warning("dropping non-pair element: %s",
+ dp.get_element_string(iter).c_str());
+ continue;
+ }
+
+ const data_parser::element& pvalue = iter.get_pair_value();
+
+ if (pvalue.value_token() == DT_INVALID) {
+ log_debug("invalid!!");
+ // continue;
+ }
+
+ std::string key_str
+ = dp.get_element_string(iter.e_sub_elements->front());
+
+ if (key_str.empty()) {
+ key_str = fmt::format(FMT_STRING("col_{}"), col);
+ col += 1;
+ }
+ root_map.gen(key_str);
+ element_to_json(gen, dp, pvalue);
+ }
+}
+
+static void
+list_body_elements_to_json(yajl_gen gen,
+ data_parser& dp,
+ data_parser::element_list_t* el)
+{
+ for (auto& iter : *el) {
+ element_to_json(gen, dp, iter);
+ }
+}
+
+static void
+list_elements_to_json(yajl_gen gen,
+ data_parser& dp,
+ data_parser::element_list_t* el)
+{
+ yajlpp_array root_array(gen);
+
+ list_body_elements_to_json(gen, dp, el);
+}
+
+static void
+map_elements_to_json(yajl_gen gen,
+ data_parser& dp,
+ data_parser::element_list_t* el)
+{
+ bool unique_names = el->size() > 1;
+ std::vector<std::string> names;
+
+ for (auto& iter : *el) {
+ if (iter.e_token != DNT_PAIR) {
+ unique_names = false;
+ continue;
+ }
+
+ const auto& pvalue = iter.get_pair_value();
+
+ if (pvalue.value_token() == DT_INVALID) {
+ log_debug("invalid!!");
+ // continue;
+ }
+
+ std::string key_str
+ = dp.get_element_string(iter.e_sub_elements->front());
+ if (key_str.empty()) {
+ continue;
+ }
+ if (names | lnav::itertools::find(key_str)) {
+ unique_names = false;
+ break;
+ }
+ names.push_back(key_str);
+ }
+
+ names.clear();
+
+ if (unique_names) {
+ map_elements_to_json2(gen, dp, el);
+ } else {
+ list_elements_to_json(gen, dp, el);
+ }
+}
+
+void
+elements_to_json(yajl_gen gen,
+ data_parser& dp,
+ data_parser::element_list_t* el,
+ bool root)
+{
+ if (el->empty()) {
+ yajl_gen_null(gen);
+ } else {
+ switch (el->front().e_token) {
+ case DNT_PAIR: {
+ if (root && el->size() == 1) {
+ const data_parser::element& pair_elem
+ = el->front().get_pair_elem();
+ std::string key_str = dp.get_element_string(
+ pair_elem.e_sub_elements->front());
+
+ if (key_str.empty()
+ && el->front().get_pair_value().value_token()
+ == DNT_GROUP)
+ {
+ element_to_json(gen, dp, el->front().get_pair_value());
+ } else {
+ yajlpp_map singleton_map(gen);
+
+ if (key_str.empty()) {
+ key_str = "col_0";
+ }
+ singleton_map.gen(key_str);
+ element_to_json(gen, dp, pair_elem.get_pair_value());
+ }
+ } else {
+ map_elements_to_json(gen, dp, el);
+ }
+ break;
+ }
+ default:
+ list_elements_to_json(gen, dp, el);
+ break;
+ }
+ }
+}
diff --git a/src/elem_to_json.hh b/src/elem_to_json.hh
new file mode 100644
index 0000000..f4c14b2
--- /dev/null
+++ b/src/elem_to_json.hh
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2016, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef elem_to_json_hh
+#define elem_to_json_hh
+
+#include "data_parser.hh"
+#include "yajl/api/yajl_gen.h"
+
+void elements_to_json(yajl_gen gen,
+ data_parser& dp,
+ data_parser::element_list_t* el,
+ bool root = true);
+
+#endif
diff --git a/src/emojis.json b/src/emojis.json
new file mode 100644
index 0000000..62f2507
--- /dev/null
+++ b/src/emojis.json
@@ -0,0 +1,4036 @@
+{
+ "emojis": [
+ {"emoji": "👩‍👩‍👧‍👧", "name": "family: woman, woman, girl, girl", "shortname": ":woman_woman_girl_girl:", "unicode": "1F469 200D 1F469 200D 1F467 200D 1F467", "html": "&#128105;&zwj;&#128105;&zwj;&#128103;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍👩‍👧‍👦", "name": "family: woman, woman, girl, boy", "shortname": ":woman_woman_girl_boy:", "unicode": "1F469 200D 1F469 200D 1F467 200D 1F466", "html": "&#128105;&zwj;&#128105;&zwj;&#128103;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍👩‍👦‍👦", "name": "family: woman, woman, boy, boy", "shortname": ":woman_woman_boy_boy:", "unicode": "1F469 200D 1F469 200D 1F466 200D 1F466", "html": "&#128105;&zwj;&#128105;&zwj;&#128102;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👩‍👧‍👧", "name": "family: man, woman, girl, girl", "shortname": ":man_woman_girl_girl:", "unicode": "1F468 200D 1F469 200D 1F467 200D 1F467", "html": "&#128104;&zwj;&#128105;&zwj;&#128103;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👩‍👧‍👦", "name": "family: man, woman, girl, boy", "shortname": ":man_woman_girl_boy:", "unicode": "1F468 200D 1F469 200D 1F467 200D 1F466", "html": "&#128104;&zwj;&#128105;&zwj;&#128103;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👩‍👦‍👦", "name": "family: man, woman, boy, boy", "shortname": ":man_woman_boy_boy:", "unicode": "1F468 200D 1F469 200D 1F466 200D 1F466", "html": "&#128104;&zwj;&#128105;&zwj;&#128102;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👨‍👧‍👧", "name": "family: man, man, girl, girl", "shortname": ":man_man_girl_girl:", "unicode": "1F468 200D 1F468 200D 1F467 200D 1F467", "html": "&#128104;&zwj;&#128104;&zwj;&#128103;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👨‍👧‍👦", "name": "family: man, man, girl, boy", "shortname": ":man_man_girl_boy:", "unicode": "1F468 200D 1F468 200D 1F467 200D 1F466", "html": "&#128104;&zwj;&#128104;&zwj;&#128103;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👨‍👦‍👦", "name": "family: man, man, boy, boy", "shortname": ":man_man_boy_boy:", "unicode": "1F468 200D 1F468 200D 1F466 200D 1F466", "html": "&#128104;&zwj;&#128104;&zwj;&#128102;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍👩‍👧", "name": "family: woman, woman, girl", "shortname": ":woman_woman_girl:", "unicode": "1F469 200D 1F469 200D 1F467", "html": "&#128105;&zwj;&#128105;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍👩‍👦", "name": "family: woman, woman, boy", "shortname": ":woman_woman_boy:", "unicode": "1F469 200D 1F469 200D 1F466", "html": "&#128105;&zwj;&#128105;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍👧‍👧", "name": "family: woman, girl, girl", "shortname": ":woman_girl_girl:", "unicode": "1F469 200D 1F467 200D 1F467", "html": "&#128105;&zwj;&#128103;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍👧‍👦", "name": "family: woman, girl, boy", "shortname": ":woman_girl_boy:", "unicode": "1F469 200D 1F467 200D 1F466", "html": "&#128105;&zwj;&#128103;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍👦‍👦", "name": "family: woman, boy, boy", "shortname": ":woman_boy_boy:", "unicode": "1F469 200D 1F466 200D 1F466", "html": "&#128105;&zwj;&#128102;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👩‍👧", "name": "family: man, woman, girl", "shortname": ":man_woman_girl:", "unicode": "1F468 200D 1F469 200D 1F467", "html": "&#128104;&zwj;&#128105;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👨‍👧", "name": "family: man, man, girl", "shortname": ":man_man_girl:", "unicode": "1F468 200D 1F468 200D 1F467", "html": "&#128104;&zwj;&#128104;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👨‍👦", "name": "family: man, man, boy", "shortname": ":man_man_boy:", "unicode": "1F468 200D 1F468 200D 1F466", "html": "&#128104;&zwj;&#128104;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👧‍👧", "name": "family: man, girl, girl", "shortname": ":man_girl_girl:", "unicode": "1F468 200D 1F467 200D 1F467", "html": "&#128104;&zwj;&#128103;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👧‍👦", "name": "family: man, girl, boy", "shortname": ":man_girl_boy:", "unicode": "1F468 200D 1F467 200D 1F466", "html": "&#128104;&zwj;&#128103;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👦‍👦", "name": "family: man, boy, boy", "shortname": ":man_boy_boy:", "unicode": "1F468 200D 1F466 200D 1F466", "html": "&#128104;&zwj;&#128102;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍👧", "name": "family: woman, girl", "shortname": ":woman_girl:", "unicode": "1F469 200D 1F467", "html": "&#128105;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍👦", "name": "family: woman, boy", "shortname": ":woman_boy:", "unicode": "1F469 200D 1F466", "html": "&#128105;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👧", "name": "family: man, girl", "shortname": ":man_girl:", "unicode": "1F468 200D 1F467", "html": "&#128104;&zwj;&#128103;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👦", "name": "family: man, boy", "shortname": ":man_boy:", "unicode": "1F468 200D 1F466", "html": "&#128104;&zwj;&#128102;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "😂", "name": "face with tears of joy", "shortname": ":joy:", "unicode": "1f602", "html": "&#128514;", "category": "Smileys & Emotion (face-smiling)", "order": "3"},
+ {"emoji": "❤️", "name": "red heart", "shortname": ":heart:", "unicode": "2764", "html": "&#10084;", "category": "Smileys & Emotion (emotion)", "order": "1286"},
+ {"emoji": "♥️", "name": "heart suit", "shortname": ":heart_suit:", "unicode": "2665 FE0F", "html": "&hearts;️", "category": "Activities (game)", "order": ""},
+ {"emoji": "😍", "name": "smiling face with heart-eyes", "shortname": ":heart_eyes:", "unicode": "1f60d", "html": "&#128525;", "category": "Smileys & Emotion (face-affection)", "order": "13"},
+ {"emoji": "😭", "name": "loudly crying face", "shortname": ":sob:", "unicode": "1f62d", "html": "&#128557;", "category": "Smileys & Emotion (face-concerned)", "order": "55"},
+ {"emoji": "😊", "name": "smiling face with smiling eyes", "shortname": ":blush:", "unicode": "1f60a", "html": "&#128522;", "category": "Smileys & Emotion (face-smiling)", "order": "10"},
+ {"emoji": "😒", "name": "unamused face", "shortname": ":unamused:", "unicode": "1f612", "html": "&#128530;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": "41"},
+ {"emoji": "😘", "name": "face blowing a kiss", "shortname": ":kissing_heart:", "unicode": "1f618", "html": "&#128536;", "category": "Smileys & Emotion (face-affection)", "order": "14"},
+ {"emoji": "💕", "name": "two hearts", "shortname": ":two_hearts:", "unicode": "1f495", "html": "&#128149;", "category": "Smileys & Emotion (emotion)", "order": "1289"},
+ {"emoji": "☺️", "name": "smiling face", "shortname": ":smiling:", "unicode": "263A FE0F", "html": "&#9786;", "category": "Smileys & Emotion (face-affection)", "order": ""},
+ {"emoji": "😩", "name": "weary face", "shortname": ":weary:", "unicode": "1f629", "html": "&#128553;", "category": "Smileys & Emotion (face-concerned)", "order": "59"},
+ {"emoji": "👌🏿", "name": "OK hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F44C 1F3FF", "html": "&#128076;&#127999;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "👌🏾", "name": "OK hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F44C 1F3FE", "html": "&#128076;&#127998;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "👌🏽", "name": "OK hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F44C 1F3FD", "html": "&#128076;&#127997;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "👌🏼", "name": "OK hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F44C 1F3FC", "html": "&#128076;&#127996;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "👌🏻", "name": "OK hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F44C 1F3FB", "html": "&#128076;&#127995;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "👌", "name": "OK hand", "shortname": ":ok_hand:", "unicode": "1f44c", "html": "&#128076;", "category": "People & Body (hand-fingers-partial)", "order": "1170"},
+ {"emoji": "😔", "name": "pensive face", "shortname": ":pensive:", "unicode": "1f614", "html": "&#128532;", "category": "Smileys & Emotion (face-sleepy)", "order": "43"},
+ {"emoji": "😏", "name": "smirking face", "shortname": ":smirk:", "unicode": "1f60f", "html": "&#128527;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": "26"},
+ {"emoji": "😁", "name": "beaming face with smiling eyes", "shortname": ":grin:", "unicode": "1f601", "html": "&#128513;", "category": "Smileys & Emotion (face-smiling)", "order": "2"},
+ {"emoji": "♻", "name": "recycling symbol", "shortname": ":recycle:", "unicode": "267b", "html": "&#9851;", "category": "Symbols (other-symbol)", "order": "2072"},
+ {"emoji": "😉", "name": "winking face", "shortname": ":wink:", "unicode": "1f609", "html": "&#128521;", "category": "Smileys & Emotion (face-smiling)", "order": "9"},
+ {"emoji": "👍🏿", "name": "thumbs up: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F44D 1F3FF", "html": "&#128077;&#127999;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👍🏾", "name": "thumbs up: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F44D 1F3FE", "html": "&#128077;&#127998;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👍🏽", "name": "thumbs up: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F44D 1F3FD", "html": "&#128077;&#127997;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👍🏼", "name": "thumbs up: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F44D 1F3FC", "html": "&#128077;&#127996;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👍🏻", "name": "thumbs up: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F44D 1F3FB", "html": "&#128077;&#127995;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👍", "name": "thumbs up", "shortname": ":thumbsup:", "unicode": "1f44d", "html": "&#128077;", "category": "People & Body (hand-fingers-closed)", "order": "1176"},
+ {"emoji": "🙏🏿", "name": "folded hands: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64F 1F3FF", "html": "&#128591;&#127999;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙏🏾", "name": "folded hands: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64F 1F3FE", "html": "&#128591;&#127998;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙏🏽", "name": "folded hands: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64F 1F3FD", "html": "&#128591;&#127997;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙏🏼", "name": "folded hands: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64F 1F3FC", "html": "&#128591;&#127996;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙏🏻", "name": "folded hands: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64F 1F3FB", "html": "&#128591;&#127995;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙏", "name": "folded hands", "shortname": ":pray:", "unicode": "1f64f", "html": "&#128591;", "category": "People & Body (hands)", "order": "1248"},
+ {"emoji": "😌", "name": "relieved face", "shortname": ":relieved:", "unicode": "1f60c", "html": "&#128524;", "category": "Smileys & Emotion (face-sleepy)", "order": "35"},
+ {"emoji": "🎶", "name": "musical notes", "shortname": ":notes:", "unicode": "1f3b6", "html": "&#127926;", "category": "Objects (music)", "order": "1825"},
+ {"emoji": "😳", "name": "flushed face", "shortname": ":flushed:", "unicode": "1f633", "html": "&#128563;", "category": "Smileys & Emotion (face-concerned)", "order": "63"},
+ {"emoji": "🙌🏿", "name": "raising hands: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64C 1F3FF", "html": "&#128588;&#127999;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙌🏾", "name": "raising hands: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64C 1F3FE", "html": "&#128588;&#127998;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙌🏽", "name": "raising hands: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64C 1F3FD", "html": "&#128588;&#127997;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙌🏼", "name": "raising hands: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64C 1F3FC", "html": "&#128588;&#127996;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙌🏻", "name": "raising hands: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64C 1F3FB", "html": "&#128588;&#127995;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🙌", "name": "raising hands", "shortname": ":raised_hands:", "unicode": "1f64c", "html": "&#128588;", "category": "People & Body (hands)", "order": "1242"},
+ {"emoji": "🙈", "name": "see-no-evil monkey", "shortname": ":see_no_evil:", "unicode": "1f648", "html": "&#128584;", "category": "Smileys & Emotion (monkey-face)", "order": "96"},
+ {"emoji": "😢", "name": "crying face", "shortname": ":cry:", "unicode": "1f622", "html": "&#128546;", "category": "Smileys & Emotion (face-concerned)", "order": "54"},
+ {"emoji": "😎", "name": "smiling face with sunglasses", "shortname": ":sunglasses:", "unicode": "1f60e", "html": "&#128526;", "category": "Smileys & Emotion (face-glasses)", "order": "12"},
+ {"emoji": "✌🏿", "name": "victory hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "270C 1F3FF", "html": "&#9996;&#127999;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "✌🏾", "name": "victory hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "270C 1F3FE", "html": "&#9996;&#127998;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "✌🏽", "name": "victory hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "270C 1F3FD", "html": "&#9996;&#127997;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "✌🏼", "name": "victory hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "270C 1F3FC", "html": "&#9996;&#127996;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "✌🏻", "name": "victory hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "270C 1F3FB", "html": "&#9996;&#127995;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "✌️", "name": "victory hand", "shortname": ":v:", "unicode": "270c", "html": "&#9996;", "category": "People & Body (hand-fingers-partial)", "order": "1128"},
+ {"emoji": "👀", "name": "eyes", "shortname": ":eyes:", "unicode": "1f440", "html": "&#128064;", "category": "People & Body (body-parts)", "order": "1279"},
+ {"emoji": "😅", "name": "grinning face with sweat", "shortname": ":sweat_smile:", "unicode": "1f605", "html": "&#128517;", "category": "Smileys & Emotion (face-smiling)", "order": "7"},
+ {"emoji": "✨", "name": "sparkles", "shortname": ":sparkles:", "unicode": "2728", "html": "&#10024;", "category": "Activities (event)", "order": "1760"},
+ {"emoji": "😴", "name": "sleeping face", "shortname": ":sleeping:", "unicode": "1f634", "html": "&#128564;", "category": "Smileys & Emotion (face-sleepy)", "order": "34"},
+ {"emoji": "😄", "name": "grinning face with smiling eyes", "shortname": ":smile:", "unicode": "1f604", "html": "&#128516;", "category": "Smileys & Emotion (face-smiling)", "order": "6"},
+ {"emoji": "💜", "name": "purple heart", "shortname": ":purple_heart:", "unicode": "1f49c", "html": "&#128156;", "category": "Smileys & Emotion (emotion)", "order": "1295"},
+ {"emoji": "💔", "name": "broken heart", "shortname": ":broken_heart:", "unicode": "1f494", "html": "&#128148;", "category": "Smileys & Emotion (emotion)", "order": "1288"},
+ {"emoji": "💯", "name": "hundred points", "shortname": ":hundred_points:", "unicode": "1F4AF", "html": "&#128175;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "😑", "name": "expressionless face", "shortname": ":expressionless:", "unicode": "1f611", "html": "&#128529;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": "23"},
+ {"emoji": "💖", "name": "sparkling heart", "shortname": ":sparkling_heart:", "unicode": "1f496", "html": "&#128150;", "category": "Smileys & Emotion (emotion)", "order": "1290"},
+ {"emoji": "💙", "name": "blue heart", "shortname": ":blue_heart:", "unicode": "1f499", "html": "&#128153;", "category": "Smileys & Emotion (emotion)", "order": "1292"},
+ {"emoji": "😕", "name": "confused face", "shortname": ":confused:", "unicode": "1f615", "html": "&#128533;", "category": "Smileys & Emotion (face-concerned)", "order": "44"},
+ {"emoji": "💁🏿‍♂", "name": "man tipping hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F481 1F3FF 200D 2642", "html": "&#128129;&#127999;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏾‍♂", "name": "man tipping hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F481 1F3FE 200D 2642", "html": "&#128129;&#127998;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏽‍♂", "name": "man tipping hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F481 1F3FD 200D 2642", "html": "&#128129;&#127997;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏼‍♂", "name": "man tipping hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F481 1F3FC 200D 2642", "html": "&#128129;&#127996;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏻‍♂", "name": "man tipping hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F481 1F3FB 200D 2642", "html": "&#128129;&#127995;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁‍♂", "name": "man tipping hand", "shortname": ":man_tipping_hand:", "unicode": "1F481 200D 2642", "html": "&#128129;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏿", "name": "person tipping hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F481 1F3FF", "html": "&#128129;&#127999;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏾", "name": "person tipping hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F481 1F3FE", "html": "&#128129;&#127998;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏽", "name": "person tipping hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F481 1F3FD", "html": "&#128129;&#127997;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏼", "name": "person tipping hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F481 1F3FC", "html": "&#128129;&#127996;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏻", "name": "person tipping hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F481 1F3FB", "html": "&#128129;&#127995;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁", "name": "person tipping hand", "shortname": ":information_desk_person:", "unicode": "1f481", "html": "&#128129;", "category": "People & Body (person-gesture)", "order": "567"},
+ {"emoji": "😜", "name": "winking face with tongue", "shortname": ":stuck_out_tongue_winking_eye:", "unicode": "1f61c", "html": "&#128540;", "category": "Smileys & Emotion (face-tongue)", "order": "38"},
+ {"emoji": "😞", "name": "disappointed face", "shortname": ":disappointed:", "unicode": "1f61e", "html": "&#128542;", "category": "Smileys & Emotion (face-concerned)", "order": "51"},
+ {"emoji": "😋", "name": "face savoring food", "shortname": ":yum:", "unicode": "1f60b", "html": "&#128523;", "category": "Smileys & Emotion (face-tongue)", "order": "11"},
+ {"emoji": "😐", "name": "neutral face", "shortname": ":neutral_face:", "unicode": "1f610", "html": "&#128528;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": "22"},
+ {"emoji": "😪", "name": "sleepy face", "shortname": ":sleepy:", "unicode": "1f62a", "html": "&#128554;", "category": "Smileys & Emotion (face-sleepy)", "order": "32"},
+ {"emoji": "👏🏿", "name": "clapping hands: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F44F 1F3FF", "html": "&#128079;&#127999;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👏🏾", "name": "clapping hands: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F44F 1F3FE", "html": "&#128079;&#127998;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👏🏽", "name": "clapping hands: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F44F 1F3FD", "html": "&#128079;&#127997;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👏🏼", "name": "clapping hands: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F44F 1F3FC", "html": "&#128079;&#127996;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👏🏻", "name": "clapping hands: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F44F 1F3FB", "html": "&#128079;&#127995;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👏", "name": "clapping hands", "shortname": ":clap:", "unicode": "1f44f", "html": "&#128079;", "category": "People & Body (hands)", "order": "1224"},
+ {"emoji": "💘", "name": "heart with arrow", "shortname": ":cupid:", "unicode": "1f498", "html": "&#128152;", "category": "Smileys & Emotion (emotion)", "order": "1285"},
+ {"emoji": "💗", "name": "growing heart", "shortname": ":heartpulse:", "unicode": "1f497", "html": "&#128151;", "category": "Smileys & Emotion (emotion)", "order": "1291"},
+ {"emoji": "💞", "name": "revolving hearts", "shortname": ":revolving_hearts:", "unicode": "1f49e", "html": "&#128158;", "category": "Smileys & Emotion (emotion)", "order": "1298"},
+ {"emoji": "⬅️", "name": "left arrow", "shortname": ":arrow_left:", "unicode": "2b05", "html": "&#11013;", "category": "Symbols (arrow)", "order": "2008"},
+ {"emoji": "🙊", "name": "speak-no-evil monkey", "shortname": ":speak_no_evil:", "unicode": "1f64a", "html": "&#128586;", "category": "Smileys & Emotion (monkey-face)", "order": "98"},
+ {"emoji": "✋🏿", "name": "raised hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "270B 1F3FF", "html": "&#9995;&#127999;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "✋🏾", "name": "raised hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "270B 1F3FE", "html": "&#9995;&#127998;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "✋🏽", "name": "raised hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "270B 1F3FD", "html": "&#9995;&#127997;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "✋🏼", "name": "raised hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "270B 1F3FC", "html": "&#9995;&#127996;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "✋🏻", "name": "raised hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "270B 1F3FB", "html": "&#9995;&#127995;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "✋", "name": "raised hand", "shortname": ":raised_hand:", "unicode": "270B", "html": "&#9995;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "💋", "name": "kiss mark", "shortname": ":kiss:", "unicode": "1f48b", "html": "&#128139;", "category": "Smileys & Emotion (emotion)", "order": "1284"},
+ {"emoji": "👉🏿", "name": "backhand index pointing right: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F449 1F3FF", "html": "&#128073;&#127999;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👉🏾", "name": "backhand index pointing right: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F449 1F3FE", "html": "&#128073;&#127998;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👉🏽", "name": "backhand index pointing right: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F449 1F3FD", "html": "&#128073;&#127997;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👉🏼", "name": "backhand index pointing right: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F449 1F3FC", "html": "&#128073;&#127996;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👉🏻", "name": "backhand index pointing right: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F449 1F3FB", "html": "&#128073;&#127995;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👉", "name": "backhand index pointing right", "shortname": ":point_right:", "unicode": "1f449", "html": "&#128073;", "category": "People & Body (hand-single-finger)", "order": "1098"},
+ {"emoji": "🌸", "name": "cherry blossom", "shortname": ":cherry_blossom:", "unicode": "1f338", "html": "&#127800;", "category": "Animals & Nature (plant-flower)", "order": "1428"},
+ {"emoji": "😱", "name": "face screaming in fear", "shortname": ":scream:", "unicode": "1f631", "html": "&#128561;", "category": "Smileys & Emotion (face-concerned)", "order": "62"},
+ {"emoji": "🔥", "name": "fire", "shortname": ":fire:", "unicode": "1f525", "html": "&#128293;", "category": "Travel & Places (sky & weather)", "order": "1753"},
+ {"emoji": "😡", "name": "pouting face", "shortname": ":rage:", "unicode": "1f621", "html": "&#128545;", "category": "Smileys & Emotion (face-negative)", "order": "65"},
+ {"emoji": "😃", "name": "grinning face with big eyes", "shortname": ":smiley:", "unicode": "1f603", "html": "&#128515;", "category": "Smileys & Emotion (face-smiling)", "order": "5"},
+ {"emoji": "🎉", "name": "party popper", "shortname": ":tada:", "unicode": "1f389", "html": "&#127881;", "category": "Activities (event)", "order": "1762"},
+ {"emoji": "👊🏿", "name": "oncoming fist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F44A 1F3FF", "html": "&#128074;&#127999;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👊🏾", "name": "oncoming fist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F44A 1F3FE", "html": "&#128074;&#127998;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👊🏽", "name": "oncoming fist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F44A 1F3FD", "html": "&#128074;&#127997;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👊🏼", "name": "oncoming fist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F44A 1F3FC", "html": "&#128074;&#127996;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👊🏻", "name": "oncoming fist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F44A 1F3FB", "html": "&#128074;&#127995;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👊", "name": "oncoming fist", "shortname": ":oncoming_fist:", "unicode": "1F44A", "html": "&#128074;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "😫", "name": "tired face", "shortname": ":tired_face:", "unicode": "1f62b", "html": "&#128555;", "category": "Smileys & Emotion (face-concerned)", "order": "33"},
+ {"emoji": "📷", "name": "camera", "shortname": ":camera:", "unicode": "1f4f7", "html": "&#128247;", "category": "Objects (light & video)", "order": "1861"},
+ {"emoji": "🌹", "name": "rose", "shortname": ":rose:", "unicode": "1f339", "html": "&#127801;", "category": "Animals & Nature (plant-flower)", "order": "1431"},
+ {"emoji": "😝", "name": "squinting face with tongue", "shortname": ":stuck_out_tongue_closed_eyes:", "unicode": "1f61d", "html": "&#128541;", "category": "Smileys & Emotion (face-tongue)", "order": "39"},
+ {"emoji": "💪🏿", "name": "flexed biceps: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F4AA 1F3FF", "html": "&#128170;&#127999;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "💪🏾", "name": "flexed biceps: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F4AA 1F3FE", "html": "&#128170;&#127998;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "💪🏽", "name": "flexed biceps: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F4AA 1F3FD", "html": "&#128170;&#127997;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "💪🏼", "name": "flexed biceps: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F4AA 1F3FC", "html": "&#128170;&#127996;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "💪🏻", "name": "flexed biceps: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F4AA 1F3FB", "html": "&#128170;&#127995;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "💪", "name": "flexed biceps", "shortname": ":muscle:", "unicode": "1f4aa", "html": "&#128170;", "category": "People & Body (body-parts)", "order": "1080"},
+ {"emoji": "💀", "name": "skull", "shortname": ":skull:", "unicode": "1f480", "html": "&#128128;", "category": "Smileys & Emotion (face-negative)", "order": "80"},
+ {"emoji": "☀️", "name": "sun", "shortname": ":sunny:", "unicode": "2600", "html": "&#9728;", "category": "Travel & Places (sky & weather)", "order": "1724"},
+ {"emoji": "💛", "name": "yellow heart", "shortname": ":yellow_heart:", "unicode": "1f49b", "html": "&#128155;", "category": "Smileys & Emotion (emotion)", "order": "1294"},
+ {"emoji": "😤", "name": "face with steam from nose", "shortname": ":triumph:", "unicode": "1f624", "html": "&#128548;", "category": "Smileys & Emotion (face-negative)", "order": "53"},
+ {"emoji": "🌚", "name": "new moon face", "shortname": ":new_moon_with_face:", "unicode": "1f31a", "html": "&#127770;", "category": "Travel & Places (sky & weather)", "order": "1720"},
+ {"emoji": "😆", "name": "grinning squinting face", "shortname": ":laughing:", "unicode": "1f606", "html": "&#128518;", "category": "Smileys & Emotion (face-smiling)", "order": "8"},
+ {"emoji": "😓", "name": "downcast face with sweat", "shortname": ":sweat:", "unicode": "1f613", "html": "&#128531;", "category": "Smileys & Emotion (face-concerned)", "order": "42"},
+ {"emoji": "👈🏿", "name": "backhand index pointing left: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F448 1F3FF", "html": "&#128072;&#127999;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👈🏾", "name": "backhand index pointing left: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F448 1F3FE", "html": "&#128072;&#127998;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👈🏽", "name": "backhand index pointing left: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F448 1F3FD", "html": "&#128072;&#127997;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👈🏼", "name": "backhand index pointing left: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F448 1F3FC", "html": "&#128072;&#127996;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👈🏻", "name": "backhand index pointing left: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F448 1F3FB", "html": "&#128072;&#127995;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👈", "name": "backhand index pointing left", "shortname": ":point_left:", "unicode": "1f448", "html": "&#128072;", "category": "People & Body (hand-single-finger)", "order": "1092"},
+ {"emoji": "✔️", "name": "check mark", "shortname": ":heavy_check_mark:", "unicode": "2714", "html": "&#10004;", "category": "Symbols (other-symbol)", "order": "2080"},
+ {"emoji": "😻", "name": "smiling cat with heart-eyes", "shortname": ":heart_eyes_cat:", "unicode": "1f63b", "html": "&#128571;", "category": "Smileys & Emotion (cat-face)", "order": "90"},
+ {"emoji": "😀", "name": "grinning face", "shortname": ":grinning:", "unicode": "1f600", "html": "&#128512;", "category": "Smileys & Emotion (face-smiling)", "order": "1"},
+ {"emoji": "😷", "name": "face with medical mask", "shortname": ":mask:", "unicode": "1f637", "html": "&#128567;", "category": "Smileys & Emotion (face-unwell)", "order": "71"},
+ {"emoji": "💚", "name": "green heart", "shortname": ":green_heart:", "unicode": "1f49a", "html": "&#128154;", "category": "Smileys & Emotion (emotion)", "order": "1293"},
+ {"emoji": "👋🏿", "name": "waving hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F44B 1F3FF", "html": "&#128075;&#127999;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "👋🏾", "name": "waving hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F44B 1F3FE", "html": "&#128075;&#127998;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "👋🏽", "name": "waving hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F44B 1F3FD", "html": "&#128075;&#127997;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "👋🏼", "name": "waving hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F44B 1F3FC", "html": "&#128075;&#127996;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "👋🏻", "name": "waving hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F44B 1F3FB", "html": "&#128075;&#127995;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "👋", "name": "waving hand", "shortname": ":wave:", "unicode": "1f44b", "html": "&#128075;", "category": "People & Body (hand-fingers-open)", "order": "1218"},
+ {"emoji": "😣", "name": "persevering face", "shortname": ":persevere:", "unicode": "1f623", "html": "&#128547;", "category": "Smileys & Emotion (face-concerned)", "order": "27"},
+ {"emoji": "💓", "name": "beating heart", "shortname": ":heartbeat:", "unicode": "1f493", "html": "&#128147;", "category": "Smileys & Emotion (emotion)", "order": "1287"},
+ {"emoji": "▶️", "name": "play button", "shortname": ":arrow_forward:", "unicode": "25b6", "html": "&#9654;", "category": "Symbols (av-symbol)", "order": "2051"},
+ {"emoji": "◀️", "name": "reverse button", "shortname": ":arrow_backward:", "unicode": "25c0", "html": "&#9664;", "category": "Symbols (av-symbol)", "order": "2055"},
+ {"emoji": "↪️", "name": "left arrow curving right", "shortname": ":arrow_right_hook:", "unicode": "21aa", "html": "&#8618;", "category": "Symbols (arrow)", "order": "2013"},
+ {"emoji": "↩️", "name": "right arrow curving left", "shortname": ":leftwards_arrow_with_hook:", "unicode": "21a9", "html": "&#8617;", "category": "Symbols (arrow)", "order": "2012"},
+ {"emoji": "👑", "name": "crown", "shortname": ":crown:", "unicode": "1f451", "html": "&#128081;", "category": "Objects (clothing)", "order": "1333"},
+ {"emoji": "😚", "name": "kissing face with closed eyes", "shortname": ":kissing_closed_eyes:", "unicode": "1f61a", "html": "&#128538;", "category": "Smileys & Emotion (face-affection)", "order": "17"},
+ {"emoji": "😛", "name": "face with tongue", "shortname": ":stuck_out_tongue:", "unicode": "1f61b", "html": "&#128539;", "category": "Smileys & Emotion (face-tongue)", "order": "37"},
+ {"emoji": "😥", "name": "sad but relieved face", "shortname": ":disappointed_relieved:", "unicode": "1f625", "html": "&#128549;", "category": "Smileys & Emotion (face-concerned)", "order": "28"},
+ {"emoji": "😇", "name": "smiling face with halo", "shortname": ":innocent:", "unicode": "1f607", "html": "&#128519;", "category": "Smileys & Emotion (face-smiling)", "order": "67"},
+ {"emoji": "🎧", "name": "headphone", "shortname": ":headphones:", "unicode": "1f3a7", "html": "&#127911;", "category": "Objects (music)", "order": "1830"},
+ {"emoji": "✅", "name": "check mark button", "shortname": ":white_check_mark:", "unicode": "2705", "html": "&#9989;", "category": "Symbols (other-symbol)", "order": "2078"},
+ {"emoji": "😖", "name": "confounded face", "shortname": ":confounded:", "unicode": "1f616", "html": "&#128534;", "category": "Smileys & Emotion (face-concerned)", "order": "50"},
+ {"emoji": "➡", "name": "right arrow", "shortname": ":arrow_right:", "unicode": "27a1", "html": "&#10145;", "category": "Symbols (arrow)", "order": "2004"},
+ {"emoji": "😠", "name": "angry face", "shortname": ":angry:", "unicode": "1f620", "html": "&#128544;", "category": "Smileys & Emotion (face-negative)", "order": "66"},
+ {"emoji": "😬", "name": "grimacing face", "shortname": ":grimacing:", "unicode": "1f62c", "html": "&#128556;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": "60"},
+ {"emoji": "🌟", "name": "glowing star", "shortname": ":star2:", "unicode": "1f31f", "html": "&#127775;", "category": "Travel & Places (sky & weather)", "order": "1728"},
+ {"emoji": "🔫", "name": "pistol", "shortname": ":gun:", "unicode": "1f52b", "html": "&#128299;", "category": "Objects (tool)", "order": "1956"},
+ {"emoji": "🙋🏿‍♂", "name": "man raising hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64B 1F3FF 200D 2642", "html": "&#128587;&#127999;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏾‍♂", "name": "man raising hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64B 1F3FE 200D 2642", "html": "&#128587;&#127998;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏽‍♂", "name": "man raising hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64B 1F3FD 200D 2642", "html": "&#128587;&#127997;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏼‍♂", "name": "man raising hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64B 1F3FC 200D 2642", "html": "&#128587;&#127996;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏻‍♂", "name": "man raising hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64B 1F3FB 200D 2642", "html": "&#128587;&#127995;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋‍♂", "name": "man raising hand", "shortname": ":man_raising_hand:", "unicode": "1F64B 200D 2642", "html": "&#128587;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏿", "name": "person raising hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64B 1F3FF", "html": "&#128587;&#127999;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏾", "name": "person raising hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64B 1F3FE", "html": "&#128587;&#127998;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏽", "name": "person raising hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64B 1F3FD", "html": "&#128587;&#127997;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏼", "name": "person raising hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64B 1F3FC", "html": "&#128587;&#127996;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏻", "name": "person raising hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64B 1F3FB", "html": "&#128587;&#127995;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋", "name": "person raising hand", "shortname": ":raising_hand:", "unicode": "1f64b", "html": "&#128587;", "category": "People & Body (person-gesture)", "order": "585"},
+ {"emoji": "👎🏿", "name": "thumbs down: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F44E 1F3FF", "html": "&#128078;&#127999;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👎🏾", "name": "thumbs down: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F44E 1F3FE", "html": "&#128078;&#127998;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👎🏽", "name": "thumbs down: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F44E 1F3FD", "html": "&#128078;&#127997;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👎🏼", "name": "thumbs down: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F44E 1F3FC", "html": "&#128078;&#127996;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👎🏻", "name": "thumbs down: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F44E 1F3FB", "html": "&#128078;&#127995;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "👎", "name": "thumbs down", "shortname": ":thumbsdown:", "unicode": "1f44e", "html": "&#128078;", "category": "People & Body (hand-fingers-closed)", "order": "1182"},
+ {"emoji": "💃🏿", "name": "woman dancing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F483 1F3FF", "html": "&#128131;&#127999;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💃🏾", "name": "woman dancing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F483 1F3FE", "html": "&#128131;&#127998;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💃🏽", "name": "woman dancing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F483 1F3FD", "html": "&#128131;&#127997;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💃🏼", "name": "woman dancing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F483 1F3FC", "html": "&#128131;&#127996;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💃🏻", "name": "woman dancing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F483 1F3FB", "html": "&#128131;&#127995;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💃", "name": "woman dancing", "shortname": ":dancer:", "unicode": "1f483", "html": "&#128131;", "category": "People & Body (person-activity)", "order": "729"},
+ {"emoji": "🎵", "name": "musical note", "shortname": ":musical_note:", "unicode": "1f3b5", "html": "&#127925;", "category": "Objects (music)", "order": "1824"},
+ {"emoji": "😶", "name": "face without mouth", "shortname": ":no_mouth:", "unicode": "1f636", "html": "&#128566;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": "24"},
+ {"emoji": "💫", "name": "dizzy", "shortname": ":dizzy:", "unicode": "1f4ab", "html": "&#128171;", "category": "Smileys & Emotion (emotion)", "order": "1308"},
+ {"emoji": "✊🏿", "name": "raised fist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "270A 1F3FF", "html": "&#9994;&#127999;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "✊🏾", "name": "raised fist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "270A 1F3FE", "html": "&#9994;&#127998;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "✊🏽", "name": "raised fist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "270A 1F3FD", "html": "&#9994;&#127997;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "✊🏼", "name": "raised fist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "270A 1F3FC", "html": "&#9994;&#127996;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "✊🏻", "name": "raised fist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "270A 1F3FB", "html": "&#9994;&#127995;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "✊", "name": "raised fist", "shortname": ":fist:", "unicode": "270a", "html": "&#9994;", "category": "People & Body (hand-fingers-closed)", "order": "1188"},
+ {"emoji": "👇🏿", "name": "backhand index pointing down: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F447 1F3FF", "html": "&#128071;&#127999;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👇🏾", "name": "backhand index pointing down: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F447 1F3FE", "html": "&#128071;&#127998;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👇🏽", "name": "backhand index pointing down: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F447 1F3FD", "html": "&#128071;&#127997;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👇🏼", "name": "backhand index pointing down: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F447 1F3FC", "html": "&#128071;&#127996;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👇🏻", "name": "backhand index pointing down: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F447 1F3FB", "html": "&#128071;&#127995;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👇", "name": "backhand index pointing down", "shortname": ":point_down:", "unicode": "1f447", "html": "&#128071;", "category": "People & Body (hand-single-finger)", "order": "1122"},
+ {"emoji": "🔴", "name": "red circle", "shortname": ":red_circle:", "unicode": "1f534", "html": "&#128308;", "category": "Symbols (geometric)", "order": "2179"},
+ {"emoji": "🙅🏿‍♂", "name": "man gesturing NO: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F645 1F3FF 200D 2642", "html": "&#128581;&#127999;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏾‍♂", "name": "man gesturing NO: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F645 1F3FE 200D 2642", "html": "&#128581;&#127998;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏽‍♂", "name": "man gesturing NO: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F645 1F3FD 200D 2642", "html": "&#128581;&#127997;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏼‍♂", "name": "man gesturing NO: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F645 1F3FC 200D 2642", "html": "&#128581;&#127996;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏻‍♂", "name": "man gesturing NO: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F645 1F3FB 200D 2642", "html": "&#128581;&#127995;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅‍♂", "name": "man gesturing NO", "shortname": ":man_gesturing_NO:", "unicode": "1F645 200D 2642", "html": "&#128581;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏿", "name": "person gesturing NO: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F645 1F3FF", "html": "&#128581;&#127999;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏾", "name": "person gesturing NO: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F645 1F3FE", "html": "&#128581;&#127998;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏽", "name": "person gesturing NO: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F645 1F3FD", "html": "&#128581;&#127997;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏼", "name": "person gesturing NO: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F645 1F3FC", "html": "&#128581;&#127996;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏻", "name": "person gesturing NO: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F645 1F3FB", "html": "&#128581;&#127995;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅", "name": "person gesturing NO", "shortname": ":no_good:", "unicode": "1f645", "html": "&#128581;", "category": "People & Body (person-gesture)", "order": "531"},
+ {"emoji": "💥", "name": "collision", "shortname": ":boom:", "unicode": "1f4a5", "html": "&#128165;", "category": "Smileys & Emotion (emotion)", "order": "1305"},
+ {"emoji": "©️", "name": "copyright", "shortname": ":copyright:", "unicode": "00A9 FE0F", "html": "&copy;️", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "💭", "name": "thought balloon", "shortname": ":thought_balloon:", "unicode": "1f4ad", "html": "&#128173;", "category": "Smileys & Emotion (emotion)", "order": "1312"},
+ {"emoji": "👅", "name": "tongue", "shortname": ":tongue:", "unicode": "1f445", "html": "&#128069;", "category": "People & Body (body-parts)", "order": "1282"},
+ {"emoji": "💩", "name": "pile of poo", "shortname": ":poop:", "unicode": "1f4a9", "html": "&#128169;", "category": "Smileys & Emotion (face-costume)", "order": "85"},
+ {"emoji": "😰", "name": "anxious face with sweat", "shortname": ":cold_sweat:", "unicode": "1f630", "html": "&#128560;", "category": "Smileys & Emotion (face-concerned)", "order": "61"},
+ {"emoji": "💎", "name": "gem stone", "shortname": ":gem:", "unicode": "1f48e", "html": "&#128142;", "category": "Objects (clothing)", "order": "1341"},
+ {"emoji": "🙆🏿‍♂", "name": "man gesturing OK: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F646 1F3FF 200D 2642", "html": "&#128582;&#127999;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏾‍♂", "name": "man gesturing OK: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F646 1F3FE 200D 2642", "html": "&#128582;&#127998;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏽‍♂", "name": "man gesturing OK: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F646 1F3FD 200D 2642", "html": "&#128582;&#127997;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏼‍♂", "name": "man gesturing OK: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F646 1F3FC 200D 2642", "html": "&#128582;&#127996;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏻‍♂", "name": "man gesturing OK: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F646 1F3FB 200D 2642", "html": "&#128582;&#127995;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆‍♂", "name": "man gesturing OK", "shortname": ":man_gesturing_OK:", "unicode": "1F646 200D 2642", "html": "&#128582;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏿", "name": "person gesturing OK: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F646 1F3FF", "html": "&#128582;&#127999;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏾", "name": "person gesturing OK: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F646 1F3FE", "html": "&#128582;&#127998;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏽", "name": "person gesturing OK: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F646 1F3FD", "html": "&#128582;&#127997;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏼", "name": "person gesturing OK: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F646 1F3FC", "html": "&#128582;&#127996;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏻", "name": "person gesturing OK: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F646 1F3FB", "html": "&#128582;&#127995;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆", "name": "person gesturing OK", "shortname": ":ok_woman:", "unicode": "1f646", "html": "&#128582;", "category": "People & Body (person-gesture)", "order": "549"},
+ {"emoji": "🍕", "name": "pizza", "shortname": ":pizza:", "unicode": "1f355", "html": "&#127829;", "category": "Food & Drink (food-prepared)", "order": "1484"},
+ {"emoji": "😹", "name": "cat with tears of joy", "shortname": ":joy_cat:", "unicode": "1f639", "html": "&#128569;", "category": "Smileys & Emotion (cat-face)", "order": "89"},
+ {"emoji": "🌞", "name": "sun with face", "shortname": ":sun_with_face:", "unicode": "1f31e", "html": "&#127774;", "category": "Travel & Places (sky & weather)", "order": "1726"},
+ {"emoji": "🍃", "name": "leaf fluttering in wind", "shortname": ":leaves:", "unicode": "1f343", "html": "&#127811;", "category": "Animals & Nature (plant-other)", "order": "1448"},
+ {"emoji": "💦", "name": "sweat droplets", "shortname": ":sweat_drops:", "unicode": "1f4a6", "html": "&#128166;", "category": "Smileys & Emotion (emotion)", "order": "1306"},
+ {"emoji": "🐧", "name": "penguin", "shortname": ":penguin:", "unicode": "1f427", "html": "&#128039;", "category": "Animals & Nature (animal-bird)", "order": "1394"},
+ {"emoji": "💤", "name": "zzz", "shortname": ":zzz:", "unicode": "1f4a4", "html": "&#128164;", "category": "Smileys & Emotion (emotion)", "order": "1302"},
+ {"emoji": "🚶🏿‍♀", "name": "woman walking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B6 1F3FF 200D 2640", "html": "&#128694;&#127999;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏾‍♀", "name": "woman walking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B6 1F3FE 200D 2640", "html": "&#128694;&#127998;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏽‍♀", "name": "woman walking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B6 1F3FD 200D 2640", "html": "&#128694;&#127997;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏼‍♀", "name": "woman walking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B6 1F3FC 200D 2640", "html": "&#128694;&#127996;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏻‍♀", "name": "woman walking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B6 1F3FB 200D 2640", "html": "&#128694;&#127995;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶‍♀", "name": "woman walking", "shortname": ":woman_walking:", "unicode": "1F6B6 200D 2640", "html": "&#128694;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏿", "name": "person walking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B6 1F3FF", "html": "&#128694;&#127999;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏾", "name": "person walking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B6 1F3FE", "html": "&#128694;&#127998;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏽", "name": "person walking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B6 1F3FD", "html": "&#128694;&#127997;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏼", "name": "person walking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B6 1F3FC", "html": "&#128694;&#127996;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏻", "name": "person walking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B6 1F3FB", "html": "&#128694;&#127995;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶", "name": "person walking", "shortname": ":walking:", "unicode": "1f6b6", "html": "&#128694;", "category": "People & Body (person-activity)", "order": "693"},
+ {"emoji": "✈️", "name": "airplane", "shortname": ":airplane:", "unicode": "2708", "html": "&#9992;", "category": "Travel & Places (transport-air)", "order": "1650"},
+ {"emoji": "🎈", "name": "balloon", "shortname": ":balloon:", "unicode": "1f388", "html": "&#127880;", "category": "Activities (event)", "order": "1761"},
+ {"emoji": "⭐", "name": "star", "shortname": ":star:", "unicode": "2b50", "html": "&#11088;", "category": "Travel & Places (sky & weather)", "order": "1727"},
+ {"emoji": "🎀", "name": "ribbon", "shortname": ":ribbon:", "unicode": "1f380", "html": "&#127872;", "category": "Activities (event)", "order": "1770"},
+ {"emoji": "☑️", "name": "check box with check", "shortname": ":ballot_box_with_check:", "unicode": "2611", "html": "&#9745;", "category": "Symbols (other-symbol)", "order": "2079"},
+ {"emoji": "😟", "name": "worried face", "shortname": ":worried:", "unicode": "1f61f", "html": "&#128543;", "category": "Smileys & Emotion (face-concerned)", "order": "52"},
+ {"emoji": "🔞", "name": "no one under eighteen", "shortname": ":underage:", "unicode": "1f51e", "html": "&#128286;", "category": "Symbols (warning)", "order": "1999"},
+ {"emoji": "😨", "name": "fearful face", "shortname": ":fearful:", "unicode": "1f628", "html": "&#128552;", "category": "Smileys & Emotion (face-concerned)", "order": "58"},
+ {"emoji": "🍀", "name": "four leaf clover", "shortname": ":four_leaf_clover:", "unicode": "1f340", "html": "&#127808;", "category": "Animals & Nature (plant-other)", "order": "1445"},
+ {"emoji": "🌺", "name": "hibiscus", "shortname": ":hibiscus:", "unicode": "1f33a", "html": "&#127802;", "category": "Animals & Nature (plant-flower)", "order": "1433"},
+ {"emoji": "🎤", "name": "microphone", "shortname": ":microphone:", "unicode": "1f3a4", "html": "&#127908;", "category": "Objects (music)", "order": "1829"},
+ {"emoji": "👐🏿", "name": "open hands: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F450 1F3FF", "html": "&#128080;&#127999;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👐🏾", "name": "open hands: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F450 1F3FE", "html": "&#128080;&#127998;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👐🏽", "name": "open hands: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F450 1F3FD", "html": "&#128080;&#127997;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👐🏼", "name": "open hands: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F450 1F3FC", "html": "&#128080;&#127996;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👐🏻", "name": "open hands: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F450 1F3FB", "html": "&#128080;&#127995;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "👐", "name": "open hands", "shortname": ":open_hands:", "unicode": "1f450", "html": "&#128080;", "category": "People & Body (hands)", "order": "1236"},
+ {"emoji": "👻", "name": "ghost", "shortname": ":ghost:", "unicode": "1f47b", "html": "&#128123;", "category": "Smileys & Emotion (face-costume)", "order": "82"},
+ {"emoji": "🌴", "name": "palm tree", "shortname": ":palm_tree:", "unicode": "1f334", "html": "&#127796;", "category": "Animals & Nature (plant-other)", "order": "1440"},
+ {"emoji": "‼️", "name": "double exclamation mark", "shortname": ":bangbang:", "unicode": "203c", "html": "&#8252;", "category": "Symbols (other-symbol)", "order": "2096"},
+ {"emoji": "💅🏿", "name": "nail polish: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F485 1F3FF", "html": "&#128133;&#127999;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "💅🏾", "name": "nail polish: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F485 1F3FE", "html": "&#128133;&#127998;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "💅🏽", "name": "nail polish: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F485 1F3FD", "html": "&#128133;&#127997;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "💅🏼", "name": "nail polish: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F485 1F3FC", "html": "&#128133;&#127996;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "💅🏻", "name": "nail polish: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F485 1F3FB", "html": "&#128133;&#127995;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "💅", "name": "nail polish", "shortname": ":nail_care:", "unicode": "1f485", "html": "&#128133;", "category": "People & Body (hand-prop)", "order": "1260"},
+ {"emoji": "❌", "name": "cross mark", "shortname": ":x:", "unicode": "274c", "html": "&#10060;", "category": "Symbols (other-symbol)", "order": "2082"},
+ {"emoji": "👽", "name": "alien", "shortname": ":alien:", "unicode": "1f47d", "html": "&#128125;", "category": "Smileys & Emotion (face-costume)", "order": "83"},
+ {"emoji": "🙇🏿‍♀", "name": "woman bowing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F647 1F3FF 200D 2640", "html": "&#128583;&#127999;&zwj;&#9792;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏾‍♀", "name": "woman bowing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F647 1F3FE 200D 2640", "html": "&#128583;&#127998;&zwj;&#9792;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏽‍♀", "name": "woman bowing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F647 1F3FD 200D 2640", "html": "&#128583;&#127997;&zwj;&#9792;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏼‍♀", "name": "woman bowing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F647 1F3FC 200D 2640", "html": "&#128583;&#127996;&zwj;&#9792;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏻‍♀", "name": "woman bowing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F647 1F3FB 200D 2640", "html": "&#128583;&#127995;&zwj;&#9792;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇‍♀", "name": "woman bowing", "shortname": ":woman_bowing:", "unicode": "1F647 200D 2640", "html": "&#128583;&zwj;&#9792;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏿", "name": "person bowing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F647 1F3FF", "html": "&#128583;&#127999;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏾", "name": "person bowing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F647 1F3FE", "html": "&#128583;&#127998;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏽", "name": "person bowing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F647 1F3FD", "html": "&#128583;&#127997;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏼", "name": "person bowing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F647 1F3FC", "html": "&#128583;&#127996;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏻", "name": "person bowing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F647 1F3FB", "html": "&#128583;&#127995;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇", "name": "person bowing", "shortname": ":bow:", "unicode": "1f647", "html": "&#128583;", "category": "People & Body (person-gesture)", "order": "603"},
+ {"emoji": "☁", "name": "cloud", "shortname": ":cloud:", "unicode": "2601", "html": "&#9729;", "category": "Travel & Places (sky & weather)", "order": "1730"},
+ {"emoji": "⚽", "name": "soccer ball", "shortname": ":soccer:", "unicode": "26bd", "html": "&#9917;", "category": "Activities (sport)", "order": "1781"},
+ {"emoji": "👼🏿", "name": "baby angel: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F47C 1F3FF", "html": "&#128124;&#127999;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "👼🏾", "name": "baby angel: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F47C 1F3FE", "html": "&#128124;&#127998;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "👼🏽", "name": "baby angel: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F47C 1F3FD", "html": "&#128124;&#127997;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "👼🏼", "name": "baby angel: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F47C 1F3FC", "html": "&#128124;&#127996;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "👼🏻", "name": "baby angel: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F47C 1F3FB", "html": "&#128124;&#127995;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "👼", "name": "baby angel", "shortname": ":angel:", "unicode": "1f47c", "html": "&#128124;", "category": "People & Body (person-fantasy)", "order": "141"},
+ {"emoji": "👯‍♂", "name": "men with bunny ears", "shortname": ":men_with_bunny_ears:", "unicode": "1F46F 200D 2642", "html": "&#128111;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👯", "name": "people with bunny ears", "shortname": ":dancers:", "unicode": "1f46f", "html": "&#128111;", "category": "People & Body (person-activity)", "order": "741"},
+ {"emoji": "❗", "name": "exclamation mark", "shortname": ":exclamation:", "unicode": "2757", "html": "&#10071;", "category": "Symbols (other-symbol)", "order": "2101"},
+ {"emoji": "❄️", "name": "snowflake", "shortname": ":snowflake:", "unicode": "2744", "html": "&#10052;", "category": "Travel & Places (sky & weather)", "order": "1749"},
+ {"emoji": "☝🏿", "name": "index pointing up: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "261D 1F3FF", "html": "&#9757;&#127999;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "☝🏾", "name": "index pointing up: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "261D 1F3FE", "html": "&#9757;&#127998;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "☝🏽", "name": "index pointing up: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "261D 1F3FD", "html": "&#9757;&#127997;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "☝🏼", "name": "index pointing up: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "261D 1F3FC", "html": "&#9757;&#127996;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "☝🏻", "name": "index pointing up: light skin tone", "shortname": ":light_skin_tone:", "unicode": "261D 1F3FB", "html": "&#9757;&#127995;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "☝️", "name": "index pointing up", "shortname": ":point_up:", "unicode": "261d", "html": "&#9757;", "category": "People & Body (hand-single-finger)", "order": "1104"},
+ {"emoji": "😙", "name": "kissing face with smiling eyes", "shortname": ":kissing_smiling_eyes:", "unicode": "1f619", "html": "&#128537;", "category": "Smileys & Emotion (face-affection)", "order": "16"},
+ {"emoji": "🌈", "name": "rainbow", "shortname": ":rainbow:", "unicode": "1f308", "html": "&#127752;", "category": "Travel & Places (sky & weather)", "order": "1743"},
+ {"emoji": "🌙", "name": "crescent moon", "shortname": ":crescent_moon:", "unicode": "1f319", "html": "&#127769;", "category": "Travel & Places (sky & weather)", "order": "1719"},
+ {"emoji": "💟", "name": "heart decoration", "shortname": ":heart_decoration:", "unicode": "1f49f", "html": "&#128159;", "category": "Smileys & Emotion (emotion)", "order": "1299"},
+ {"emoji": "💝", "name": "heart with ribbon", "shortname": ":gift_heart:", "unicode": "1f49d", "html": "&#128157;", "category": "Smileys & Emotion (emotion)", "order": "1297"},
+ {"emoji": "🎁", "name": "wrapped gift", "shortname": ":gift:", "unicode": "1f381", "html": "&#127873;", "category": "Activities (event)", "order": "1771"},
+ {"emoji": "🍻", "name": "clinking beer mugs", "shortname": ":beers:", "unicode": "1f37b", "html": "&#127867;", "category": "Food & Drink (drink)", "order": "1530"},
+ {"emoji": "😧", "name": "anguished face", "shortname": ":anguished:", "unicode": "1f627", "html": "&#128551;", "category": "Smileys & Emotion (face-concerned)", "order": "57"},
+ {"emoji": "🌍", "name": "globe showing Europe-Africa", "shortname": ":earth_africa:", "unicode": "1f30d", "html": "&#127757;", "category": "Travel & Places (place-map)", "order": "1538"},
+ {"emoji": "🎥", "name": "movie camera", "shortname": ":movie_camera:", "unicode": "1f3a5", "html": "&#127909;", "category": "Objects (light & video)", "order": "1856"},
+ {"emoji": "⚓", "name": "anchor", "shortname": ":anchor:", "unicode": "2693", "html": "&#9875;", "category": "Travel & Places (transport-water)", "order": "1642"},
+ {"emoji": "⚡", "name": "high voltage", "shortname": ":zap:", "unicode": "26a1", "html": "&#9889;", "category": "Travel & Places (sky & weather)", "order": "1748"},
+ {"emoji": "♣️", "name": "club suit", "shortname": ":club_suit:", "unicode": "2663 FE0F", "html": "&clubs;️", "category": "Activities (game)", "order": ""},
+ {"emoji": "✖️", "name": "multiplication sign", "shortname": ":heavy_multiplication_x:", "unicode": "2716", "html": "&#10006;", "category": "Symbols (other-symbol)", "order": "2081"},
+ {"emoji": "🏃🏿‍♀", "name": "woman running: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C3 1F3FF 200D 2640", "html": "&#127939;&#127999;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏾‍♀", "name": "woman running: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C3 1F3FE 200D 2640", "html": "&#127939;&#127998;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏽‍♀", "name": "woman running: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C3 1F3FD 200D 2640", "html": "&#127939;&#127997;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏼‍♀", "name": "woman running: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C3 1F3FC 200D 2640", "html": "&#127939;&#127996;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏻‍♀", "name": "woman running: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C3 1F3FB 200D 2640", "html": "&#127939;&#127995;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃‍♀", "name": "woman running", "shortname": ":woman_running:", "unicode": "1F3C3 200D 2640", "html": "&#127939;&zwj;&#9792;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏿", "name": "person running: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C3 1F3FF", "html": "&#127939;&#127999;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏾", "name": "person running: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C3 1F3FE", "html": "&#127939;&#127998;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏽", "name": "person running: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C3 1F3FD", "html": "&#127939;&#127997;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏼", "name": "person running: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C3 1F3FC", "html": "&#127939;&#127996;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏻", "name": "person running: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C3 1F3FB", "html": "&#127939;&#127995;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃", "name": "person running", "shortname": ":runner:", "unicode": "1f3c3", "html": "&#127939;", "category": "People & Body (person-activity)", "order": "711"},
+ {"emoji": "🌻", "name": "sunflower", "shortname": ":sunflower:", "unicode": "1f33b", "html": "&#127803;", "category": "Animals & Nature (plant-flower)", "order": "1434"},
+ {"emoji": "🌎", "name": "globe showing Americas", "shortname": ":earth_americas:", "unicode": "1f30e", "html": "&#127758;", "category": "Travel & Places (place-map)", "order": "1539"},
+ {"emoji": "💐", "name": "bouquet", "shortname": ":bouquet:", "unicode": "1f490", "html": "&#128144;", "category": "Animals & Nature (plant-flower)", "order": "1427"},
+ {"emoji": "🐶", "name": "dog face", "shortname": ":dog:", "unicode": "1f436", "html": "&#128054;", "category": "Animals & Nature (animal-mammal)", "order": "1345"},
+ {"emoji": "💰", "name": "money bag", "shortname": ":moneybag:", "unicode": "1f4b0", "html": "&#128176;", "category": "Objects (money)", "order": "1891"},
+ {"emoji": "🌿", "name": "herb", "shortname": ":herb:", "unicode": "1f33f", "html": "&#127807;", "category": "Animals & Nature (plant-other)", "order": "1443"},
+ {"emoji": "👫", "name": "woman and man holding hands", "shortname": ":couple:", "unicode": "1f46b", "html": "&#128107;", "category": "People & Body (family)", "order": "1018"},
+ {"emoji": "🍂", "name": "fallen leaf", "shortname": ":fallen_leaf:", "unicode": "1f342", "html": "&#127810;", "category": "Animals & Nature (plant-other)", "order": "1447"},
+ {"emoji": "🌷", "name": "tulip", "shortname": ":tulip:", "unicode": "1f337", "html": "&#127799;", "category": "Animals & Nature (plant-flower)", "order": "1436"},
+ {"emoji": "🎂", "name": "birthday cake", "shortname": ":birthday:", "unicode": "1f382", "html": "&#127874;", "category": "Food & Drink (food-sweet)", "order": "1513"},
+ {"emoji": "🐱", "name": "cat face", "shortname": ":cat:", "unicode": "1f431", "html": "&#128049;", "category": "Animals & Nature (animal-mammal)", "order": "1350"},
+ {"emoji": "☕", "name": "hot beverage", "shortname": ":coffee:", "unicode": "2615", "html": "&#9749;", "category": "Food & Drink (drink)", "order": "1522"},
+ {"emoji": "😵", "name": "dizzy face", "shortname": ":dizzy_face:", "unicode": "1f635", "html": "&#128565;", "category": "Smileys & Emotion (face-unwell)", "order": "64"},
+ {"emoji": "👆🏿", "name": "backhand index pointing up: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F446 1F3FF", "html": "&#128070;&#127999;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👆🏾", "name": "backhand index pointing up: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F446 1F3FE", "html": "&#128070;&#127998;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👆🏽", "name": "backhand index pointing up: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F446 1F3FD", "html": "&#128070;&#127997;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👆🏼", "name": "backhand index pointing up: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F446 1F3FC", "html": "&#128070;&#127996;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👆🏻", "name": "backhand index pointing up: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F446 1F3FB", "html": "&#128070;&#127995;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "👆", "name": "backhand index pointing up", "shortname": ":point_up_2:", "unicode": "1f446", "html": "&#128070;", "category": "People & Body (hand-single-finger)", "order": "1110"},
+ {"emoji": "😮", "name": "face with open mouth", "shortname": ":open_mouth:", "unicode": "1f62e", "html": "&#128558;", "category": "Smileys & Emotion (face-concerned)", "order": "29"},
+ {"emoji": "😯", "name": "hushed face", "shortname": ":hushed:", "unicode": "1f62f", "html": "&#128559;", "category": "Smileys & Emotion (face-concerned)", "order": "31"},
+ {"emoji": "🏀", "name": "basketball", "shortname": ":basketball:", "unicode": "1f3c0", "html": "&#127936;", "category": "Activities (sport)", "order": "1783"},
+ {"emoji": "🎄", "name": "Christmas tree", "shortname": ":christmas_tree:", "unicode": "1f384", "html": "&#127876;", "category": "Activities (event)", "order": "1757"},
+ {"emoji": "💍", "name": "ring", "shortname": ":ring:", "unicode": "1f48d", "html": "&#128141;", "category": "Objects (clothing)", "order": "1340"},
+ {"emoji": "🌝", "name": "full moon face", "shortname": ":full_moon_with_face:", "unicode": "1f31d", "html": "&#127773;", "category": "Travel & Places (sky & weather)", "order": "1725"},
+ {"emoji": "😲", "name": "astonished face", "shortname": ":astonished:", "unicode": "1f632", "html": "&#128562;", "category": "Smileys & Emotion (face-concerned)", "order": "47"},
+ {"emoji": "👭", "name": "women holding hands", "shortname": ":two_women_holding_hands:", "unicode": "1f46d", "html": "&#128109;", "category": "People & Body (family)", "order": "1030"},
+ {"emoji": "💸", "name": "money with wings", "shortname": ":money_with_wings:", "unicode": "1f4b8", "html": "&#128184;", "category": "Objects (money)", "order": "1896"},
+ {"emoji": "😿", "name": "crying cat", "shortname": ":crying_cat_face:", "unicode": "1f63f", "html": "&#128575;", "category": "Smileys & Emotion (cat-face)", "order": "94"},
+ {"emoji": "🙉", "name": "hear-no-evil monkey", "shortname": ":hear_no_evil:", "unicode": "1f649", "html": "&#128585;", "category": "Smileys & Emotion (monkey-face)", "order": "97"},
+ {"emoji": "💨", "name": "dashing away", "shortname": ":dash:", "unicode": "1f4a8", "html": "&#128168;", "category": "Smileys & Emotion (emotion)", "order": "1307"},
+ {"emoji": "🌵", "name": "cactus", "shortname": ":cactus:", "unicode": "1f335", "html": "&#127797;", "category": "Animals & Nature (plant-other)", "order": "1441"},
+ {"emoji": "♨️", "name": "hot springs", "shortname": ":hotsprings:", "unicode": "2668", "html": "&#9832;", "category": "Travel & Places (place-other)", "order": "1591"},
+ {"emoji": "☎️", "name": "telephone", "shortname": ":telephone:", "unicode": "260e", "html": "&#9742;", "category": "Objects (phone)", "order": "1840"},
+ {"emoji": "🍁", "name": "maple leaf", "shortname": ":maple_leaf:", "unicode": "1f341", "html": "&#127809;", "category": "Animals & Nature (plant-other)", "order": "1446"},
+ {"emoji": "👸🏿", "name": "princess: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F478 1F3FF", "html": "&#128120;&#127999;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👸🏾", "name": "princess: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F478 1F3FE", "html": "&#128120;&#127998;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👸🏽", "name": "princess: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F478 1F3FD", "html": "&#128120;&#127997;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👸🏼", "name": "princess: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F478 1F3FC", "html": "&#128120;&#127996;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👸🏻", "name": "princess: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F478 1F3FB", "html": "&#128120;&#127995;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👸", "name": "princess", "shortname": ":princess:", "unicode": "1f478", "html": "&#128120;", "category": "People & Body (person-role)", "order": "459"},
+ {"emoji": "💆🏿‍♂", "name": "man getting massage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F486 1F3FF 200D 2642", "html": "&#128134;&#127999;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏾‍♂", "name": "man getting massage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F486 1F3FE 200D 2642", "html": "&#128134;&#127998;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏽‍♂", "name": "man getting massage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F486 1F3FD 200D 2642", "html": "&#128134;&#127997;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏼‍♂", "name": "man getting massage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F486 1F3FC 200D 2642", "html": "&#128134;&#127996;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏻‍♂", "name": "man getting massage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F486 1F3FB 200D 2642", "html": "&#128134;&#127995;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆‍♂", "name": "man getting massage", "shortname": ":man_getting_massage:", "unicode": "1F486 200D 2642", "html": "&#128134;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏿", "name": "person getting massage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F486 1F3FF", "html": "&#128134;&#127999;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏾", "name": "person getting massage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F486 1F3FE", "html": "&#128134;&#127998;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏽", "name": "person getting massage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F486 1F3FD", "html": "&#128134;&#127997;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏼", "name": "person getting massage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F486 1F3FC", "html": "&#128134;&#127996;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏻", "name": "person getting massage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F486 1F3FB", "html": "&#128134;&#127995;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆", "name": "person getting massage", "shortname": ":massage:", "unicode": "1f486", "html": "&#128134;", "category": "People & Body (person-activity)", "order": "657"},
+ {"emoji": "💌", "name": "love letter", "shortname": ":love_letter:", "unicode": "1f48c", "html": "&#128140;", "category": "Smileys & Emotion (emotion)", "order": "1301"},
+ {"emoji": "🏆", "name": "trophy", "shortname": ":trophy:", "unicode": "1f3c6", "html": "&#127942;", "category": "Activities (award-medal)", "order": "1776"},
+ {"emoji": "🙍🏿‍♂", "name": "man frowning: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64D 1F3FF 200D 2642", "html": "&#128589;&#127999;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏾‍♂", "name": "man frowning: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64D 1F3FE 200D 2642", "html": "&#128589;&#127998;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏽‍♂", "name": "man frowning: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64D 1F3FD 200D 2642", "html": "&#128589;&#127997;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏼‍♂", "name": "man frowning: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64D 1F3FC 200D 2642", "html": "&#128589;&#127996;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏻‍♂", "name": "man frowning: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64D 1F3FB 200D 2642", "html": "&#128589;&#127995;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍‍♂", "name": "man frowning", "shortname": ":man_frowning:", "unicode": "1F64D 200D 2642", "html": "&#128589;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏿", "name": "person frowning: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64D 1F3FF", "html": "&#128589;&#127999;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏾", "name": "person frowning: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64D 1F3FE", "html": "&#128589;&#127998;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏽", "name": "person frowning: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64D 1F3FD", "html": "&#128589;&#127997;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏼", "name": "person frowning: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64D 1F3FC", "html": "&#128589;&#127996;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏻", "name": "person frowning: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64D 1F3FB", "html": "&#128589;&#127995;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍", "name": "person frowning", "shortname": ":person_frowning:", "unicode": "1f64d", "html": "&#128589;", "category": "People & Body (person-gesture)", "order": "495"},
+ {"emoji": "🇺🇸", "name": "flag: United States", "shortname": ":us:", "unicode": "1f1fa", "html": "&#127482;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "🎊", "name": "confetti ball", "shortname": ":confetti_ball:", "unicode": "1f38a", "html": "&#127882;", "category": "Activities (event)", "order": "1763"},
+ {"emoji": "🌼", "name": "blossom", "shortname": ":blossom:", "unicode": "1f33c", "html": "&#127804;", "category": "Animals & Nature (plant-flower)", "order": "1435"},
+ {"emoji": "🔪", "name": "kitchen knife", "shortname": ":kitchen_knife:", "unicode": "1F52A", "html": "&#128298;", "category": "Food & Drink (dishware)", "order": ""},
+ {"emoji": "👄", "name": "mouth", "shortname": ":lips:", "unicode": "1f444", "html": "&#128068;", "category": "People & Body (body-parts)", "order": "1283"},
+ {"emoji": "🍟", "name": "french fries", "shortname": ":fries:", "unicode": "1f35f", "html": "&#127839;", "category": "Food & Drink (food-prepared)", "order": "1483"},
+ {"emoji": "🍩", "name": "doughnut", "shortname": ":doughnut:", "unicode": "1f369", "html": "&#127849;", "category": "Food & Drink (food-sweet)", "order": "1511"},
+ {"emoji": "😦", "name": "frowning face with open mouth", "shortname": ":frowning:", "unicode": "1f626", "html": "&#128550;", "category": "Smileys & Emotion (face-concerned)", "order": "56"},
+ {"emoji": "🌊", "name": "water wave", "shortname": ":ocean:", "unicode": "1f30a", "html": "&#127754;", "category": "Travel & Places (sky & weather)", "order": "1755"},
+ {"emoji": "💣", "name": "bomb", "shortname": ":bomb:", "unicode": "1f4a3", "html": "&#128163;", "category": "Smileys & Emotion (emotion)", "order": "1304"},
+ {"emoji": "🆗", "name": "OK button", "shortname": ":ok:", "unicode": "1f197", "html": "&#127383;", "category": "Symbols (alphanum)", "order": "2137"},
+ {"emoji": "🌀", "name": "cyclone", "shortname": ":cyclone:", "unicode": "1f300", "html": "&#127744;", "category": "Travel & Places (sky & weather)", "order": "1742"},
+ {"emoji": "🚀", "name": "rocket", "shortname": ":rocket:", "unicode": "1f680", "html": "&#128640;", "category": "Travel & Places (transport-air)", "order": "1659"},
+ {"emoji": "☔", "name": "umbrella with rain drops", "shortname": ":umbrella:", "unicode": "2614", "html": "&#9748;", "category": "Travel & Places (sky & weather)", "order": "1746"},
+ {"emoji": "💏", "name": "kiss", "shortname": ":couplekiss:", "unicode": "1f48f", "html": "&#128143;", "category": "People & Body (family)", "order": "1036"},
+ {"emoji": "👩‍❤️‍💋‍👩", "name": "kiss: woman, woman", "shortname": ":couple_woman_kiss:", "unicode": "1F469 200D 2764 FE0F 200D 1F48B 200D 1F469", "html": "&#128105;&zwj;&#10084;&zwj;&#128139;&zwj;&#128105;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍❤️‍💋‍👨", "name": "kiss: man, man", "shortname": ":couple_man_kiss:", "unicode": "1F468 200D 2764 FE0F 200D 1F48B 200D 1F468", "html": "&#128104;&zwj;&#10084;&zwj;&#128139;&zwj;&#128104;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "💑", "name": "couple with heart", "shortname": ":couple_with_heart:", "unicode": "1f491", "html": "&#128145;", "category": "People & Body (family)", "order": "1040"},
+ {"emoji": "👩‍❤️‍👩", "name": "couple with heart: woman, woman", "shortname": ":woman_woman:", "unicode": "1F469 200D 2764 FE0F 200D 1F469", "html": "&#128105;&zwj;&#10084;&zwj;&#128105;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍❤️‍👨", "name": "couple with heart: man, man", "shortname": ":man_man_love:", "unicode": "1F468 200D 2764 FE0F 200D 1F468", "html": "&#128104;&zwj;&#10084;&zwj;&#128104;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🍭", "name": "lollipop", "shortname": ":lollipop:", "unicode": "1f36d", "html": "&#127853;", "category": "Food & Drink (food-sweet)", "order": "1517"},
+ {"emoji": "🎬", "name": "clapper board", "shortname": ":clapper:", "unicode": "1f3ac", "html": "&#127916;", "category": "Objects (light & video)", "order": "1859"},
+ {"emoji": "🐷", "name": "pig face", "shortname": ":pig:", "unicode": "1f437", "html": "&#128055;", "category": "Animals & Nature (animal-mammal)", "order": "1364"},
+ {"emoji": "😈", "name": "smiling face with horns", "shortname": ":smiling_imp:", "unicode": "1f608", "html": "&#128520;", "category": "Smileys & Emotion (face-negative)", "order": "76"},
+ {"emoji": "👿", "name": "angry face with horns", "shortname": ":imp:", "unicode": "1f47f", "html": "&#128127;", "category": "Smileys & Emotion (face-negative)", "order": "77"},
+ {"emoji": "🐝", "name": "honeybee", "shortname": ":bee:", "unicode": "1f41d", "html": "&#128029;", "category": "Animals & Nature (animal-bug)", "order": "1422"},
+ {"emoji": "😽", "name": "kissing cat", "shortname": ":kissing_cat:", "unicode": "1f63d", "html": "&#128573;", "category": "Smileys & Emotion (cat-face)", "order": "92"},
+ {"emoji": "💢", "name": "anger symbol", "shortname": ":anger:", "unicode": "1f4a2", "html": "&#128162;", "category": "Smileys & Emotion (emotion)", "order": "1303"},
+ {"emoji": "🎼", "name": "musical score", "shortname": ":musical_score:", "unicode": "1f3bc", "html": "&#127932;", "category": "Objects (music)", "order": "1823"},
+ {"emoji": "🎅🏿", "name": "Santa Claus: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F385 1F3FF", "html": "&#127877;&#127999;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🎅🏾", "name": "Santa Claus: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F385 1F3FE", "html": "&#127877;&#127998;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🎅🏽", "name": "Santa Claus: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F385 1F3FD", "html": "&#127877;&#127997;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🎅🏼", "name": "Santa Claus: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F385 1F3FC", "html": "&#127877;&#127996;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🎅🏻", "name": "Santa Claus: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F385 1F3FB", "html": "&#127877;&#127995;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🎅", "name": "Santa Claus", "shortname": ":santa:", "unicode": "1f385", "html": "&#127877;", "category": "People & Body (person-fantasy)", "order": "447"},
+ {"emoji": "🌏", "name": "globe showing Asia-Australia", "shortname": ":earth_asia:", "unicode": "1f30f", "html": "&#127759;", "category": "Travel & Places (place-map)", "order": "1540"},
+ {"emoji": "🏈", "name": "american football", "shortname": ":football:", "unicode": "1f3c8", "html": "&#127944;", "category": "Activities (sport)", "order": "1785"},
+ {"emoji": "🎸", "name": "guitar", "shortname": ":guitar:", "unicode": "1f3b8", "html": "&#127928;", "category": "Objects (musical-instrument)", "order": "1833"},
+ {"emoji": "♦️", "name": "diamond suit", "shortname": ":diamond_suit:", "unicode": "2666 FE0F", "html": "&diams;️", "category": "Activities (game)", "order": ""},
+ {"emoji": "🐼", "name": "panda", "shortname": ":panda_face:", "unicode": "1f43c", "html": "&#128060;", "category": "Animals & Nature (animal-mammal)", "order": "1385"},
+ {"emoji": "💬", "name": "speech balloon", "shortname": ":speech_balloon:", "unicode": "1f4ac", "html": "&#128172;", "category": "Smileys & Emotion (emotion)", "order": "1309"},
+ {"emoji": "🍓", "name": "strawberry", "shortname": ":strawberry:", "unicode": "1f353", "html": "&#127827;", "category": "Food & Drink (food-fruit)", "order": "1461"},
+ {"emoji": "😼", "name": "cat with wry smile", "shortname": ":smirk_cat:", "unicode": "1f63c", "html": "&#128572;", "category": "Smileys & Emotion (cat-face)", "order": "91"},
+ {"emoji": "🍌", "name": "banana", "shortname": ":banana:", "unicode": "1f34c", "html": "&#127820;", "category": "Food & Drink (food-fruit)", "order": "1454"},
+ {"emoji": "🍉", "name": "watermelon", "shortname": ":watermelon:", "unicode": "1f349", "html": "&#127817;", "category": "Food & Drink (food-fruit)", "order": "1451"},
+ {"emoji": "⛄", "name": "snowman without snow", "shortname": ":snowman:", "unicode": "26c4", "html": "&#9924;", "category": "Travel & Places (sky & weather)", "order": "1751"},
+ {"emoji": "😸", "name": "grinning cat with smiling eyes", "shortname": ":smile_cat:", "unicode": "1f638", "html": "&#128568;", "category": "Smileys & Emotion (cat-face)", "order": "88"},
+ {"emoji": "♠️", "name": "spade suit", "shortname": ":spade_suit:", "unicode": "2660 FE0F", "html": "&spades;️", "category": "Activities (game)", "order": ""},
+ {"emoji": "🔝", "name": "TOP arrow", "shortname": ":top:", "unicode": "1f51d", "html": "&#128285;", "category": "Symbols (arrow)", "order": "2022"},
+ {"emoji": "🍆", "name": "eggplant", "shortname": ":eggplant:", "unicode": "1f346", "html": "&#127814;", "category": "Food & Drink (food-vegetable)", "order": "1465"},
+ {"emoji": "🔮", "name": "crystal ball", "shortname": ":crystal_ball:", "unicode": "1f52e", "html": "&#128302;", "category": "Activities (game)", "order": "1974"},
+ {"emoji": "🍴", "name": "fork and knife", "shortname": ":fork_and_knife:", "unicode": "1f374", "html": "&#127860;", "category": "Food & Drink (dishware)", "order": "1534"},
+ {"emoji": "📲", "name": "mobile phone with arrow", "shortname": ":calling:", "unicode": "1f4f2", "html": "&#128242;", "category": "Objects (phone)", "order": "1839"},
+ {"emoji": "📱", "name": "mobile phone", "shortname": ":iphone:", "unicode": "1f4f1", "html": "&#128241;", "category": "Objects (phone)", "order": "1838"},
+ {"emoji": "⛅", "name": "sun behind cloud", "shortname": ":partly_sunny:", "unicode": "26c5", "html": "&#9925;", "category": "Travel & Places (sky & weather)", "order": "1731"},
+ {"emoji": "⚠️", "name": "warning", "shortname": ":warning:", "unicode": "26a0", "html": "&#9888;", "category": "Symbols (warning)", "order": "1989"},
+ {"emoji": "🙀", "name": "weary cat", "shortname": ":scream_cat:", "unicode": "1f640", "html": "&#128576;", "category": "Smileys & Emotion (cat-face)", "order": "93"},
+ {"emoji": "🔸", "name": "small orange diamond", "shortname": ":small_orange_diamond:", "unicode": "1f538", "html": "&#128312;", "category": "Symbols (geometric)", "order": "2169"},
+ {"emoji": "👶🏿", "name": "baby: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F476 1F3FF", "html": "&#128118;&#127999;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👶🏾", "name": "baby: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F476 1F3FE", "html": "&#128118;&#127998;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👶🏽", "name": "baby: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F476 1F3FD", "html": "&#128118;&#127997;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👶🏼", "name": "baby: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F476 1F3FC", "html": "&#128118;&#127996;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👶🏻", "name": "baby: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F476 1F3FB", "html": "&#128118;&#127995;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👶", "name": "baby", "shortname": ":baby:", "unicode": "1f476", "html": "&#128118;", "category": "People & Body (person)", "order": "135"},
+ {"emoji": "🐾", "name": "paw prints", "shortname": ":feet:", "unicode": "1f43e", "html": "&#128062;", "category": "Animals & Nature (animal-mammal)", "order": "1386"},
+ {"emoji": "👣", "name": "footprints", "shortname": ":footprints:", "unicode": "1f463", "html": "&#128099;", "category": "People & Body (person-symbol)", "order": "1278"},
+ {"emoji": "🍺", "name": "beer mug", "shortname": ":beer:", "unicode": "1f37a", "html": "&#127866;", "category": "Food & Drink (drink)", "order": "1529"},
+ {"emoji": "🍷", "name": "wine glass", "shortname": ":wine_glass:", "unicode": "1f377", "html": "&#127863;", "category": "Food & Drink (drink)", "order": "1526"},
+ {"emoji": "⭕", "name": "hollow red circle", "shortname": ":o:", "unicode": "2b55", "html": "&#11093;", "category": "Symbols (other-symbol)", "order": "2077"},
+ {"emoji": "📹", "name": "video camera", "shortname": ":video_camera:", "unicode": "1f4f9", "html": "&#128249;", "category": "Objects (light & video)", "order": "1863"},
+ {"emoji": "🐰", "name": "rabbit face", "shortname": ":rabbit:", "unicode": "1f430", "html": "&#128048;", "category": "Animals & Nature (animal-mammal)", "order": "1379"},
+ {"emoji": "🍹", "name": "tropical drink", "shortname": ":tropical_drink:", "unicode": "1f379", "html": "&#127865;", "category": "Food & Drink (drink)", "order": "1528"},
+ {"emoji": "🚬", "name": "cigarette", "shortname": ":smoking:", "unicode": "1f6ac", "html": "&#128684;", "category": "Objects (other-object)", "order": "1969"},
+ {"emoji": "👾", "name": "alien monster", "shortname": ":space_invader:", "unicode": "1f47e", "html": "&#128126;", "category": "Smileys & Emotion (face-costume)", "order": "84"},
+ {"emoji": "🍑", "name": "peach", "shortname": ":peach:", "unicode": "1f351", "html": "&#127825;", "category": "Food & Drink (food-fruit)", "order": "1459"},
+ {"emoji": "🐍", "name": "snake", "shortname": ":snake:", "unicode": "1f40d", "html": "&#128013;", "category": "Animals & Nature (animal-reptile)", "order": "1403"},
+ {"emoji": "🐢", "name": "turtle", "shortname": ":turtle:", "unicode": "1f422", "html": "&#128034;", "category": "Animals & Nature (animal-reptile)", "order": "1401"},
+ {"emoji": "🍒", "name": "cherries", "shortname": ":cherries:", "unicode": "1f352", "html": "&#127826;", "category": "Food & Drink (food-fruit)", "order": "1460"},
+ {"emoji": "😗", "name": "kissing face", "shortname": ":kissing:", "unicode": "1f617", "html": "&#128535;", "category": "Smileys & Emotion (face-affection)", "order": "15"},
+ {"emoji": "🐸", "name": "frog", "shortname": ":frog:", "unicode": "1f438", "html": "&#128056;", "category": "Animals & Nature (animal-amphibian)", "order": "1399"},
+ {"emoji": "🌌", "name": "milky way", "shortname": ":milky_way:", "unicode": "1f30c", "html": "&#127756;", "category": "Travel & Places (sky & weather)", "order": "1592"},
+ {"emoji": "🚨", "name": "police car light", "shortname": ":rotating_light:", "unicode": "1f6a8", "html": "&#128680;", "category": "Travel & Places (transport-ground)", "order": "1637"},
+ {"emoji": "🐣", "name": "hatching chick", "shortname": ":hatching_chick:", "unicode": "1f423", "html": "&#128035;", "category": "Animals & Nature (animal-bird)", "order": "1390"},
+ {"emoji": "📕", "name": "closed book", "shortname": ":closed_book:", "unicode": "1f4d5", "html": "&#128213;", "category": "Objects (book-paper)", "order": "1875"},
+ {"emoji": "🍬", "name": "candy", "shortname": ":candy:", "unicode": "1f36c", "html": "&#127852;", "category": "Food & Drink (food-sweet)", "order": "1516"},
+ {"emoji": "🍔", "name": "hamburger", "shortname": ":hamburger:", "unicode": "1f354", "html": "&#127828;", "category": "Food & Drink (food-prepared)", "order": "1482"},
+ {"emoji": "🐻", "name": "bear", "shortname": ":bear:", "unicode": "1f43b", "html": "&#128059;", "category": "Animals & Nature (animal-mammal)", "order": "1383"},
+ {"emoji": "🐯", "name": "tiger face", "shortname": ":tiger:", "unicode": "1f42f", "html": "&#128047;", "category": "Animals & Nature (animal-mammal)", "order": "1353"},
+ {"emoji": "🚗", "name": "automobile", "shortname": ":automobile:", "unicode": "1F697", "html": "&#128663;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "⏩", "name": "fast-forward button", "shortname": ":fast_forward:", "unicode": "2.3E+10", "html": "&#9193;", "category": "Symbols (av-symbol)", "order": "2052"},
+ {"emoji": "🍦", "name": "soft ice cream", "shortname": ":icecream:", "unicode": "1f366", "html": "&#127846;", "category": "Food & Drink (food-sweet)", "order": "1508"},
+ {"emoji": "🍍", "name": "pineapple", "shortname": ":pineapple:", "unicode": "1f34d", "html": "&#127821;", "category": "Food & Drink (food-fruit)", "order": "1455"},
+ {"emoji": "🌾", "name": "sheaf of rice", "shortname": ":ear_of_rice:", "unicode": "1f33e", "html": "&#127806;", "category": "Animals & Nature (plant-other)", "order": "1442"},
+ {"emoji": "💉", "name": "syringe", "shortname": ":syringe:", "unicode": "1f489", "html": "&#128137;", "category": "Objects (medical)", "order": "1967"},
+ {"emoji": "🚮", "name": "litter in bin sign", "shortname": ":put_litter_in_its_place:", "unicode": "1f6ae", "html": "&#128686;", "category": "Symbols (transport-sign)", "order": "1977"},
+ {"emoji": "🍫", "name": "chocolate bar", "shortname": ":chocolate_bar:", "unicode": "1f36b", "html": "&#127851;", "category": "Food & Drink (food-sweet)", "order": "1515"},
+ {"emoji": "▪️", "name": "black small square", "shortname": ":black_small_square:", "unicode": "25aa", "html": "&#9642;", "category": "Symbols (geometric)", "order": "2159"},
+ {"emoji": "📺", "name": "television", "shortname": ":tv:", "unicode": "1f4fa", "html": "&#128250;", "category": "Objects (light & video)", "order": "1860"},
+ {"emoji": "💊", "name": "pill", "shortname": ":pill:", "unicode": "1f48a", "html": "&#128138;", "category": "Objects (medical)", "order": "1968"},
+ {"emoji": "🐙", "name": "octopus", "shortname": ":octopus:", "unicode": "1f419", "html": "&#128025;", "category": "Animals & Nature (animal-marine)", "order": "1413"},
+ {"emoji": "🎃", "name": "jack-o-lantern", "shortname": ":jack_o_lantern:", "unicode": "1f383", "html": "&#127875;", "category": "Activities (event)", "order": "1756"},
+ {"emoji": "🍇", "name": "grapes", "shortname": ":grapes:", "unicode": "1f347", "html": "&#127815;", "category": "Food & Drink (food-fruit)", "order": "1449"},
+ {"emoji": "😺", "name": "grinning cat", "shortname": ":smiley_cat:", "unicode": "1f63a", "html": "&#128570;", "category": "Smileys & Emotion (cat-face)", "order": "87"},
+ {"emoji": "💿", "name": "optical disk", "shortname": ":cd:", "unicode": "1f4bf", "html": "&#128191;", "category": "Objects (computer)", "order": "1854"},
+ {"emoji": "🍸", "name": "cocktail glass", "shortname": ":cocktail:", "unicode": "1f378", "html": "&#127864;", "category": "Food & Drink (drink)", "order": "1527"},
+ {"emoji": "🍰", "name": "shortcake", "shortname": ":cake:", "unicode": "1f370", "html": "&#127856;", "category": "Food & Drink (food-sweet)", "order": "1514"},
+ {"emoji": "🎮", "name": "video game", "shortname": ":video_game:", "unicode": "1f3ae", "html": "&#127918;", "category": "Activities (game)", "order": "1804"},
+ {"emoji": "™️", "name": "trade mark", "shortname": ":trade_mark:", "unicode": "2122 FE0F", "html": "&trade;️", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "⬇️", "name": "down arrow", "shortname": ":arrow_down:", "unicode": "2b07", "html": "&#11015;", "category": "Symbols (arrow)", "order": "2006"},
+ {"emoji": "🚫", "name": "prohibited", "shortname": ":no_entry_sign:", "unicode": "1f6ab", "html": "&#128683;", "category": "Symbols (warning)", "order": "1992"},
+ {"emoji": "💄", "name": "lipstick", "shortname": ":lipstick:", "unicode": "1f484", "html": "&#128132;", "category": "Objects (clothing)", "order": "1339"},
+ {"emoji": "🐳", "name": "spouting whale", "shortname": ":whale:", "unicode": "1f433", "html": "&#128051;", "category": "Animals & Nature (animal-marine)", "order": "1406"},
+ {"emoji": "📝", "name": "memo", "shortname": ":memo:", "unicode": "1F4DD", "html": "&#128221;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "®️", "name": "registered", "shortname": ":registered:", "unicode": "00AE FE0F", "html": "&reg;️", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "🍪", "name": "cookie", "shortname": ":cookie:", "unicode": "1f36a", "html": "&#127850;", "category": "Food & Drink (food-sweet)", "order": "1512"},
+ {"emoji": "🐬", "name": "dolphin", "shortname": ":dolphin:", "unicode": "1f42c", "html": "&#128044;", "category": "Animals & Nature (animal-marine)", "order": "1408"},
+ {"emoji": "🔊", "name": "speaker high volume", "shortname": ":loud_sound:", "unicode": "1f50a", "html": "&#128266;", "category": "Objects (sound)", "order": "1817"},
+ {"emoji": "👨🏿", "name": "man: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF", "html": "&#128104;&#127999;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏾", "name": "man: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE", "html": "&#128104;&#127998;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏽", "name": "man: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD", "html": "&#128104;&#127997;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏼", "name": "man: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC", "html": "&#128104;&#127996;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏻", "name": "man: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB", "html": "&#128104;&#127995;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨", "name": "man", "shortname": ":man:", "unicode": "1f468", "html": "&#128104;", "category": "People & Body (person)", "order": "111"},
+ {"emoji": "🐥", "name": "front-facing baby chick", "shortname": ":hatched_chick:", "unicode": "1f425", "html": "&#128037;", "category": "Animals & Nature (animal-bird)", "order": "1392"},
+ {"emoji": "🐒", "name": "monkey", "shortname": ":monkey:", "unicode": "1f412", "html": "&#128018;", "category": "Animals & Nature (animal-mammal)", "order": "1343"},
+ {"emoji": "📚", "name": "books", "shortname": ":books:", "unicode": "1f4da", "html": "&#128218;", "category": "Objects (book-paper)", "order": "1880"},
+ {"emoji": "👹", "name": "ogre", "shortname": ":japanese_ogre:", "unicode": "1f479", "html": "&#128121;", "category": "Smileys & Emotion (face-costume)", "order": "78"},
+ {"emoji": "💂🏿‍♀", "name": "woman guard: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F482 1F3FF 200D 2640", "html": "&#128130;&#127999;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏾‍♀", "name": "woman guard: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F482 1F3FE 200D 2640", "html": "&#128130;&#127998;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏽‍♀", "name": "woman guard: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F482 1F3FD 200D 2640", "html": "&#128130;&#127997;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏼‍♀", "name": "woman guard: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F482 1F3FC 200D 2640", "html": "&#128130;&#127996;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏻‍♀", "name": "woman guard: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F482 1F3FB 200D 2640", "html": "&#128130;&#127995;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂‍♀", "name": "woman guard", "shortname": ":woman_guard:", "unicode": "1F482 200D 2640", "html": "&#128130;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏿", "name": "guard: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F482 1F3FF", "html": "&#128130;&#127999;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏾", "name": "guard: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F482 1F3FE", "html": "&#128130;&#127998;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏽", "name": "guard: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F482 1F3FD", "html": "&#128130;&#127997;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏼", "name": "guard: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F482 1F3FC", "html": "&#128130;&#127996;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏻", "name": "guard: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F482 1F3FB", "html": "&#128130;&#127995;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂", "name": "guard", "shortname": ":guardsman:", "unicode": "1f482", "html": "&#128130;", "category": "People & Body (person-role)", "order": "375"},
+ {"emoji": "📢", "name": "loudspeaker", "shortname": ":loudspeaker:", "unicode": "1f4e2", "html": "&#128226;", "category": "Objects (sound)", "order": "1818"},
+ {"emoji": "✂️", "name": "scissors", "shortname": ":scissors:", "unicode": "2702", "html": "&#9986;", "category": "Objects (office)", "order": "1940"},
+ {"emoji": "👧🏿", "name": "girl: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F467 1F3FF", "html": "&#128103;&#127999;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👧🏾", "name": "girl: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F467 1F3FE", "html": "&#128103;&#127998;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👧🏽", "name": "girl: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F467 1F3FD", "html": "&#128103;&#127997;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👧🏼", "name": "girl: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F467 1F3FC", "html": "&#128103;&#127996;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👧🏻", "name": "girl: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F467 1F3FB", "html": "&#128103;&#127995;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👧", "name": "girl", "shortname": ":girl:", "unicode": "1f467", "html": "&#128103;", "category": "People & Body (person)", "order": "105"},
+ {"emoji": "🎓", "name": "graduation cap", "shortname": ":mortar_board:", "unicode": "1f393", "html": "&#127891;", "category": "Objects (clothing)", "order": "1336"},
+ {"emoji": "🇫🇷", "name": "flag: France", "shortname": ":fr:", "unicode": "1f1eb", "html": "&#127467;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "⚾️", "name": "", "shortname": ":baseball:", "unicode": "26be", "html": "&#9918;", "category": "", "order": "1782"},
+ {"emoji": "🚦", "name": "vertical traffic light", "shortname": ":vertical_traffic_light:", "unicode": "1f6a6", "html": "&#128678;", "category": "Travel & Places (transport-ground)", "order": "1639"},
+ {"emoji": "👩🏿", "name": "woman: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF", "html": "&#128105;&#127999;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏾", "name": "woman: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE", "html": "&#128105;&#127998;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏽", "name": "woman: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD", "html": "&#128105;&#127997;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏼", "name": "woman: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC", "html": "&#128105;&#127996;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏻", "name": "woman: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB", "html": "&#128105;&#127995;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩", "name": "woman", "shortname": ":woman:", "unicode": "1f469", "html": "&#128105;", "category": "People & Body (person)", "order": "117"},
+ {"emoji": "🎆", "name": "fireworks", "shortname": ":fireworks:", "unicode": "1f386", "html": "&#127878;", "category": "Activities (event)", "order": "1758"},
+ {"emoji": "🌠", "name": "shooting star", "shortname": ":stars:", "unicode": "1f320", "html": "&#127776;", "category": "Travel & Places (sky & weather)", "order": "1729"},
+ {"emoji": "🆘", "name": "SOS button", "shortname": ":sos:", "unicode": "1f198", "html": "&#127384;", "category": "Symbols (alphanum)", "order": "2139"},
+ {"emoji": "🍄", "name": "mushroom", "shortname": ":mushroom:", "unicode": "1f344", "html": "&#127812;", "category": "Food & Drink (food-vegetable)", "order": "1471"},
+ {"emoji": "😾", "name": "pouting cat", "shortname": ":pouting_cat:", "unicode": "1f63e", "html": "&#128574;", "category": "Smileys & Emotion (cat-face)", "order": "95"},
+ {"emoji": "🛅", "name": "left luggage", "shortname": ":left_luggage:", "unicode": "1f6c5", "html": "&#128709;", "category": "Symbols (transport-sign)", "order": "1988"},
+ {"emoji": "👠", "name": "high-heeled shoe", "shortname": ":high_heel:", "unicode": "1f460", "html": "&#128096;", "category": "Objects (clothing)", "order": "1330"},
+ {"emoji": "🎯", "name": "direct hit", "shortname": ":dart:", "unicode": "1f3af", "html": "&#127919;", "category": "Activities (game)", "order": "1798"},
+ {"emoji": "🏊🏿‍♀", "name": "woman swimming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CA 1F3FF 200D 2640", "html": "&#127946;&#127999;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏾‍♀", "name": "woman swimming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CA 1F3FE 200D 2640", "html": "&#127946;&#127998;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏽‍♀", "name": "woman swimming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CA 1F3FD 200D 2640", "html": "&#127946;&#127997;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏼‍♀", "name": "woman swimming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CA 1F3FC 200D 2640", "html": "&#127946;&#127996;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏻‍♀", "name": "woman swimming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CA 1F3FB 200D 2640", "html": "&#127946;&#127995;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊‍♀", "name": "woman swimming", "shortname": ":woman_swimming:", "unicode": "1F3CA 200D 2640", "html": "&#127946;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏿", "name": "person swimming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CA 1F3FF", "html": "&#127946;&#127999;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏾", "name": "person swimming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CA 1F3FE", "html": "&#127946;&#127998;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏽", "name": "person swimming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CA 1F3FD", "html": "&#127946;&#127997;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏼", "name": "person swimming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CA 1F3FC", "html": "&#127946;&#127996;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏻", "name": "person swimming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CA 1F3FB", "html": "&#127946;&#127995;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊", "name": "person swimming", "shortname": ":swimmer:", "unicode": "1f3ca", "html": "&#127946;", "category": "People & Body (person-sport)", "order": "836"},
+ {"emoji": "🔑", "name": "key", "shortname": ":key:", "unicode": "1f511", "html": "&#128273;", "category": "Objects (lock)", "order": "1948"},
+ {"emoji": "👙", "name": "bikini", "shortname": ":bikini:", "unicode": "1f459", "html": "&#128089;", "category": "Objects (clothing)", "order": "1321"},
+ {"emoji": "👪", "name": "family", "shortname": ":family:", "unicode": "1f46a", "html": "&#128106;", "category": "People & Body (family)", "order": "1044"},
+ {"emoji": "✏", "name": "pencil", "shortname": ":pencil2:", "unicode": "270f", "html": "&#9999;", "category": "Objects (writing)", "order": "1914"},
+ {"emoji": "🐘", "name": "elephant", "shortname": ":elephant:", "unicode": "1f418", "html": "&#128024;", "category": "Animals & Nature (animal-mammal)", "order": "1373"},
+ {"emoji": "💧", "name": "droplet", "shortname": ":droplet:", "unicode": "1f4a7", "html": "&#128167;", "category": "Travel & Places (sky & weather)", "order": "1754"},
+ {"emoji": "🌱", "name": "seedling", "shortname": ":seedling:", "unicode": "1f331", "html": "&#127793;", "category": "Animals & Nature (plant-other)", "order": "1437"},
+ {"emoji": "🍎", "name": "red apple", "shortname": ":apple:", "unicode": "1f34e", "html": "&#127822;", "category": "Food & Drink (food-fruit)", "order": "1456"},
+ {"emoji": "🆒", "name": "COOL button", "shortname": ":cool:", "unicode": "1f192", "html": "&#127378;", "category": "Symbols (alphanum)", "order": "2129"},
+ {"emoji": "📞", "name": "telephone receiver", "shortname": ":telephone_receiver:", "unicode": "1f4de", "html": "&#128222;", "category": "Objects (phone)", "order": "1841"},
+ {"emoji": "💵", "name": "dollar banknote", "shortname": ":dollar:", "unicode": "1f4b5", "html": "&#128181;", "category": "Objects (money)", "order": "1893"},
+ {"emoji": "🏡", "name": "house with garden", "shortname": ":house_with_garden:", "unicode": "1f3e1", "html": "&#127969;", "category": "Travel & Places (place-building)", "order": "1560"},
+ {"emoji": "📖", "name": "open book", "shortname": ":book:", "unicode": "1f4d6", "html": "&#128214;", "category": "Objects (book-paper)", "order": "1876"},
+ {"emoji": "💇🏿‍♂", "name": "man getting haircut: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F487 1F3FF 200D 2642", "html": "&#128135;&#127999;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏾‍♂", "name": "man getting haircut: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F487 1F3FE 200D 2642", "html": "&#128135;&#127998;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏽‍♂", "name": "man getting haircut: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F487 1F3FD 200D 2642", "html": "&#128135;&#127997;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏼‍♂", "name": "man getting haircut: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F487 1F3FC 200D 2642", "html": "&#128135;&#127996;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏻‍♂", "name": "man getting haircut: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F487 1F3FB 200D 2642", "html": "&#128135;&#127995;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇‍♂", "name": "man getting haircut", "shortname": ":man_getting_haircut:", "unicode": "1F487 200D 2642", "html": "&#128135;&zwj;&#9794;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏿", "name": "person getting haircut: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F487 1F3FF", "html": "&#128135;&#127999;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏾", "name": "person getting haircut: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F487 1F3FE", "html": "&#128135;&#127998;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏽", "name": "person getting haircut: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F487 1F3FD", "html": "&#128135;&#127997;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏼", "name": "person getting haircut: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F487 1F3FC", "html": "&#128135;&#127996;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏻", "name": "person getting haircut: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F487 1F3FB", "html": "&#128135;&#127995;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇", "name": "person getting haircut", "shortname": ":haircut:", "unicode": "1f487", "html": "&#128135;", "category": "People & Body (person-activity)", "order": "675"},
+ {"emoji": "💻", "name": "laptop", "shortname": ":computer:", "unicode": "1f4bb", "html": "&#128187;", "category": "Objects (computer)", "order": "1846"},
+ {"emoji": "💡", "name": "light bulb", "shortname": ":bulb:", "unicode": "1f4a1", "html": "&#128161;", "category": "Objects (light & video)", "order": "1871"},
+ {"emoji": "❓", "name": "question mark", "shortname": ":question:", "unicode": "2753", "html": "&#10067;", "category": "Symbols (other-symbol)", "order": "2098"},
+ {"emoji": "🔙", "name": "BACK arrow", "shortname": ":back:", "unicode": "1f519", "html": "&#128281;", "category": "Symbols (arrow)", "order": "2018"},
+ {"emoji": "👦🏿", "name": "boy: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F466 1F3FF", "html": "&#128102;&#127999;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👦🏾", "name": "boy: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F466 1F3FE", "html": "&#128102;&#127998;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👦🏽", "name": "boy: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F466 1F3FD", "html": "&#128102;&#127997;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👦🏼", "name": "boy: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F466 1F3FC", "html": "&#128102;&#127996;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👦🏻", "name": "boy: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F466 1F3FB", "html": "&#128102;&#127995;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👦", "name": "boy", "shortname": ":boy:", "unicode": "1f466", "html": "&#128102;", "category": "People & Body (person)", "order": "99"},
+ {"emoji": "🔐", "name": "locked with key", "shortname": ":closed_lock_with_key:", "unicode": "1f510", "html": "&#128272;", "category": "Objects (lock)", "order": "1947"},
+ {"emoji": "🙎🏿‍♂", "name": "man pouting: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64E 1F3FF 200D 2642", "html": "&#128590;&#127999;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏾‍♂", "name": "man pouting: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64E 1F3FE 200D 2642", "html": "&#128590;&#127998;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏽‍♂", "name": "man pouting: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64E 1F3FD 200D 2642", "html": "&#128590;&#127997;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏼‍♂", "name": "man pouting: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64E 1F3FC 200D 2642", "html": "&#128590;&#127996;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏻‍♂", "name": "man pouting: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64E 1F3FB 200D 2642", "html": "&#128590;&#127995;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎‍♂", "name": "man pouting", "shortname": ":man_pouting:", "unicode": "1F64E 200D 2642", "html": "&#128590;&zwj;&#9794;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏿", "name": "person pouting: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64E 1F3FF", "html": "&#128590;&#127999;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏾", "name": "person pouting: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64E 1F3FE", "html": "&#128590;&#127998;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏽", "name": "person pouting: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64E 1F3FD", "html": "&#128590;&#127997;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏼", "name": "person pouting: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64E 1F3FC", "html": "&#128590;&#127996;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏻", "name": "person pouting: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64E 1F3FB", "html": "&#128590;&#127995;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎", "name": "person pouting", "shortname": ":person_with_pouting_face:", "unicode": "1f64e", "html": "&#128590;", "category": "People & Body (person-gesture)", "order": "513"},
+ {"emoji": "🍊", "name": "tangerine", "shortname": ":tangerine:", "unicode": "1f34a", "html": "&#127818;", "category": "Food & Drink (food-fruit)", "order": "1452"},
+ {"emoji": "↔️", "name": "left-right arrow", "shortname": ":leftright_arrow:", "unicode": "2194 FE0F", "html": "&harr;️", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "🌅", "name": "sunrise", "shortname": ":sunrise:", "unicode": "1f305", "html": "&#127749;", "category": "Travel & Places (place-other)", "order": "1587"},
+ {"emoji": "🍗", "name": "poultry leg", "shortname": ":poultry_leg:", "unicode": "1f357", "html": "&#127831;", "category": "Food & Drink (food-prepared)", "order": "1480"},
+ {"emoji": "🔵", "name": "blue circle", "shortname": ":blue_circle:", "unicode": "1f535", "html": "&#128309;", "category": "Symbols (geometric)", "order": "2180"},
+ {"emoji": "🚘", "name": "oncoming automobile", "shortname": ":oncoming_automobile:", "unicode": "1f698", "html": "&#128664;", "category": "Travel & Places (transport-ground)", "order": "1625"},
+ {"emoji": "🍧", "name": "shaved ice", "shortname": ":shaved_ice:", "unicode": "1f367", "html": "&#127847;", "category": "Food & Drink (food-sweet)", "order": "1509"},
+ {"emoji": "🇮🇹", "name": "flag: Italy", "shortname": ":it:", "unicode": "1F1EE 1F1F9", "html": "&#127470;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "🐦", "name": "bird", "shortname": ":bird:", "unicode": "1f426", "html": "&#128038;", "category": "Animals & Nature (animal-bird)", "order": "1393"},
+ {"emoji": "🇬🇧", "name": "flag: United Kingdom", "shortname": ":gb:", "unicode": "1F1EC 1F1E7", "html": "&#127468;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "🌛", "name": "first quarter moon face", "shortname": ":first_quarter_moon_with_face:", "unicode": "1f31b", "html": "&#127771;", "category": "Travel & Places (sky & weather)", "order": "1721"},
+ {"emoji": "👓", "name": "glasses", "shortname": ":eyeglasses:", "unicode": "1f453", "html": "&#128083;", "category": "Objects (clothing)", "order": "1314"},
+ {"emoji": "🐐", "name": "goat", "shortname": ":goat:", "unicode": "1f410", "html": "&#128016;", "category": "Animals & Nature (animal-mammal)", "order": "1370"},
+ {"emoji": "🌃", "name": "night with stars", "shortname": ":night_with_stars:", "unicode": "1f303", "html": "&#127747;", "category": "Travel & Places (place-other)", "order": "1585"},
+ {"emoji": "👵🏿", "name": "old woman: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F475 1F3FF", "html": "&#128117;&#127999;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👵🏾", "name": "old woman: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F475 1F3FE", "html": "&#128117;&#127998;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👵🏽", "name": "old woman: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F475 1F3FD", "html": "&#128117;&#127997;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👵🏼", "name": "old woman: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F475 1F3FC", "html": "&#128117;&#127996;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👵🏻", "name": "old woman: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F475 1F3FB", "html": "&#128117;&#127995;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👵", "name": "old woman", "shortname": ":older_woman:", "unicode": "1f475", "html": "&#128117;", "category": "People & Body (person)", "order": "129"},
+ {"emoji": "⚫", "name": "black circle", "shortname": ":black_circle:", "unicode": "26ab", "html": "&#9899;", "category": "Symbols (geometric)", "order": "2178"},
+ {"emoji": "🌑", "name": "new moon", "shortname": ":new_moon:", "unicode": "1f311", "html": "&#127761;", "category": "Travel & Places (sky & weather)", "order": "1711"},
+ {"emoji": "👬", "name": "men holding hands", "shortname": ":two_men_holding_hands:", "unicode": "1f46c", "html": "&#128108;", "category": "People & Body (family)", "order": "1024"},
+ {"emoji": "⚪", "name": "white circle", "shortname": ":white_circle:", "unicode": "26aa", "html": "&#9898;", "category": "Symbols (geometric)", "order": "2177"},
+ {"emoji": "🛃", "name": "customs", "shortname": ":customs:", "unicode": "1f6c3", "html": "&#128707;", "category": "Symbols (transport-sign)", "order": "1986"},
+ {"emoji": "🐠", "name": "tropical fish", "shortname": ":tropical_fish:", "unicode": "1f420", "html": "&#128032;", "category": "Animals & Nature (animal-marine)", "order": "1410"},
+ {"emoji": "🏠", "name": "house", "shortname": ":house:", "unicode": "1f3e0", "html": "&#127968;", "category": "Travel & Places (place-building)", "order": "1559"},
+ {"emoji": "🔃", "name": "clockwise vertical arrows", "shortname": ":arrows_clockwise:", "unicode": "1f503", "html": "&#128259;", "category": "Symbols (arrow)", "order": "2016"},
+ {"emoji": "🌜", "name": "last quarter moon face", "shortname": ":last_quarter_moon_with_face:", "unicode": "1f31c", "html": "&#127772;", "category": "Travel & Places (sky & weather)", "order": "1722"},
+ {"emoji": "📍", "name": "round pushpin", "shortname": ":round_pushpin:", "unicode": "1f4cd", "html": "&#128205;", "category": "Objects (office)", "order": "1935"},
+ {"emoji": "🌕", "name": "full moon", "shortname": ":full_moon:", "unicode": "1f315", "html": "&#127765;", "category": "Travel & Places (sky & weather)", "order": "1715"},
+ {"emoji": "👟", "name": "running shoe", "shortname": ":athletic_shoe:", "unicode": "1f45f", "html": "&#128095;", "category": "Objects (clothing)", "order": "1329"},
+ {"emoji": "🍋", "name": "lemon", "shortname": ":lemon:", "unicode": "1f34b", "html": "&#127819;", "category": "Food & Drink (food-fruit)", "order": "1453"},
+ {"emoji": "🍼", "name": "baby bottle", "shortname": ":baby_bottle:", "unicode": "1f37c", "html": "&#127868;", "category": "Food & Drink (drink)", "order": "1520"},
+ {"emoji": "🎨", "name": "artist palette", "shortname": ":artist_palette:", "unicode": "1F3A8", "html": "&#127912;", "category": "Activities (arts & crafts)", "order": ""},
+ {"emoji": "✉️", "name": "envelope", "shortname": ":envelope:", "unicode": "2709 FE0F", "html": "&#9993;", "category": "Objects (mail)", "order": ""},
+ {"emoji": "🍝", "name": "spaghetti", "shortname": ":spaghetti:", "unicode": "1f35d", "html": "&#127837;", "category": "Food & Drink (food-asian)", "order": "1501"},
+ {"emoji": "🎐", "name": "wind chime", "shortname": ":wind_chime:", "unicode": "1f390", "html": "&#127888;", "category": "Activities (event)", "order": "1768"},
+ {"emoji": "🍥", "name": "fish cake with swirl", "shortname": ":fish_cake:", "unicode": "1f365", "html": "&#127845;", "category": "Food & Drink (food-asian)", "order": "1506"},
+ {"emoji": "🌲", "name": "evergreen tree", "shortname": ":evergreen_tree:", "unicode": "1f332", "html": "&#127794;", "category": "Animals & Nature (plant-other)", "order": "1438"},
+ {"emoji": "🆙", "name": "UP! button", "shortname": ":up:", "unicode": "1f199", "html": "&#127385;", "category": "Symbols (alphanum)", "order": "2140"},
+ {"emoji": "⬆️", "name": "up arrow", "shortname": ":arrow_up:", "unicode": "2b06", "html": "&#11014;", "category": "Symbols (arrow)", "order": "2002"},
+ {"emoji": "↗️", "name": "up-right arrow", "shortname": ":arrow_upper_right:", "unicode": "2197", "html": "&#8599;", "category": "Symbols (arrow)", "order": "2003"},
+ {"emoji": "↘️", "name": "down-right arrow", "shortname": ":arrow_lower_right:", "unicode": "2198", "html": "&#8600;", "category": "Symbols (arrow)", "order": "2005"},
+ {"emoji": "↙️", "name": "down-left arrow", "shortname": ":arrow_lower_left:", "unicode": "2199", "html": "&#8601;", "category": "Symbols (arrow)", "order": "2007"},
+ {"emoji": "🎭", "name": "performing arts", "shortname": ":performing_arts:", "unicode": "1f3ad", "html": "&#127917;", "category": "Activities (arts & crafts)", "order": "1598"},
+ {"emoji": "👃🏿", "name": "nose: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F443 1F3FF", "html": "&#128067;&#127999;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👃🏾", "name": "nose: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F443 1F3FE", "html": "&#128067;&#127998;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👃🏽", "name": "nose: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F443 1F3FD", "html": "&#128067;&#127997;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👃🏼", "name": "nose: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F443 1F3FC", "html": "&#128067;&#127996;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👃🏻", "name": "nose: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F443 1F3FB", "html": "&#128067;&#127995;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👃", "name": "nose", "shortname": ":nose:", "unicode": "1f443", "html": "&#128067;", "category": "People & Body (body-parts)", "order": "1272"},
+ {"emoji": "🐽", "name": "pig nose", "shortname": ":pig_nose:", "unicode": "1f43d", "html": "&#128061;", "category": "Animals & Nature (animal-mammal)", "order": "1367"},
+ {"emoji": "🐟", "name": "fish", "shortname": ":fish:", "unicode": "1f41f", "html": "&#128031;", "category": "Animals & Nature (animal-marine)", "order": "1409"},
+ {"emoji": "👳🏿‍♀", "name": "woman wearing turban: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F473 1F3FF 200D 2640", "html": "&#128115;&#127999;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏾‍♀", "name": "woman wearing turban: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F473 1F3FE 200D 2640", "html": "&#128115;&#127998;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏽‍♀", "name": "woman wearing turban: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F473 1F3FD 200D 2640", "html": "&#128115;&#127997;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏼‍♀", "name": "woman wearing turban: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F473 1F3FC 200D 2640", "html": "&#128115;&#127996;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏻‍♀", "name": "woman wearing turban: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F473 1F3FB 200D 2640", "html": "&#128115;&#127995;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳‍♀", "name": "woman wearing turban", "shortname": ":woman_wearing_turban:", "unicode": "1F473 200D 2640", "html": "&#128115;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏿", "name": "person wearing turban: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F473 1F3FF", "html": "&#128115;&#127999;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏾", "name": "person wearing turban: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F473 1F3FE", "html": "&#128115;&#127998;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏽", "name": "person wearing turban: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F473 1F3FD", "html": "&#128115;&#127997;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏼", "name": "person wearing turban: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F473 1F3FC", "html": "&#128115;&#127996;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏻", "name": "person wearing turban: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F473 1F3FB", "html": "&#128115;&#127995;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳", "name": "person wearing turban", "shortname": ":man_with_turban:", "unicode": "1f473", "html": "&#128115;", "category": "People & Body (person-role)", "order": "411"},
+ {"emoji": "🐨", "name": "koala", "shortname": ":koala:", "unicode": "1f428", "html": "&#128040;", "category": "Animals & Nature (animal-mammal)", "order": "1384"},
+ {"emoji": "👂🏿", "name": "ear: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F442 1F3FF", "html": "&#128066;&#127999;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👂🏾", "name": "ear: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F442 1F3FE", "html": "&#128066;&#127998;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👂🏽", "name": "ear: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F442 1F3FD", "html": "&#128066;&#127997;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👂🏼", "name": "ear: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F442 1F3FC", "html": "&#128066;&#127996;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👂🏻", "name": "ear: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F442 1F3FB", "html": "&#128066;&#127995;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👂", "name": "ear", "shortname": ":ear:", "unicode": "1f442", "html": "&#128066;", "category": "People & Body (body-parts)", "order": "1266"},
+ {"emoji": "✳️", "name": "eight-spoked asterisk", "shortname": ":eight_spoked_asterisk:", "unicode": "2733", "html": "&#10035;", "category": "Symbols (other-symbol)", "order": "2093"},
+ {"emoji": "🔹", "name": "small blue diamond", "shortname": ":small_blue_diamond:", "unicode": "1f539", "html": "&#128313;", "category": "Symbols (geometric)", "order": "2170"},
+ {"emoji": "🚿", "name": "shower", "shortname": ":shower:", "unicode": "1f6bf", "html": "&#128703;", "category": "Objects (household)", "order": "1672"},
+ {"emoji": "🐛", "name": "bug", "shortname": ":bug:", "unicode": "1f41b", "html": "&#128027;", "category": "Animals & Nature (animal-bug)", "order": "1420"},
+ {"emoji": "🍜", "name": "steaming bowl", "shortname": ":ramen:", "unicode": "1f35c", "html": "&#127836;", "category": "Food & Drink (food-asian)", "order": "1500"},
+ {"emoji": "🎩", "name": "top hat", "shortname": ":tophat:", "unicode": "1f3a9", "html": "&#127913;", "category": "Objects (clothing)", "order": "1335"},
+ {"emoji": "👰🏿", "name": "bride with veil: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F470 1F3FF", "html": "&#128112;&#127999;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👰🏾", "name": "bride with veil: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F470 1F3FE", "html": "&#128112;&#127998;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👰🏽", "name": "bride with veil: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F470 1F3FD", "html": "&#128112;&#127997;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👰🏼", "name": "bride with veil: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F470 1F3FC", "html": "&#128112;&#127996;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👰🏻", "name": "bride with veil: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F470 1F3FB", "html": "&#128112;&#127995;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👰", "name": "bride with veil", "shortname": ":bride_with_veil:", "unicode": "1f470", "html": "&#128112;", "category": "People & Body (person-role)", "order": "471"},
+ {"emoji": "⛽", "name": "fuel pump", "shortname": ":fuelpump:", "unicode": "26fd", "html": "&#9981;", "category": "Travel & Places (transport-ground)", "order": "1636"},
+ {"emoji": "🏁", "name": "chequered flag", "shortname": ":checkered_flag:", "unicode": "1f3c1", "html": "&#127937;", "category": "Flags (flag)", "order": "2181"},
+ {"emoji": "🐴", "name": "horse face", "shortname": ":horse:", "unicode": "1f434", "html": "&#128052;", "category": "Animals & Nature (animal-mammal)", "order": "1356"},
+ {"emoji": "⌚", "name": "watch", "shortname": ":watch:", "unicode": "231a", "html": "&#8986;", "category": "Travel & Places (time)", "order": "1682"},
+ {"emoji": "🐵", "name": "monkey face", "shortname": ":monkey_face:", "unicode": "1f435", "html": "&#128053;", "category": "Animals & Nature (animal-mammal)", "order": "1342"},
+ {"emoji": "🚼", "name": "baby symbol", "shortname": ":baby_symbol:", "unicode": "1f6bc", "html": "&#128700;", "category": "Symbols (transport-sign)", "order": "1983"},
+ {"emoji": "🆕", "name": "NEW button", "shortname": ":new:", "unicode": "1f195", "html": "&#127381;", "category": "Symbols (alphanum)", "order": "2134"},
+ {"emoji": "🆓", "name": "FREE button", "shortname": ":free:", "unicode": "1f193", "html": "&#127379;", "category": "Symbols (alphanum)", "order": "2130"},
+ {"emoji": "🎇", "name": "sparkler", "shortname": ":sparkler:", "unicode": "1f387", "html": "&#127879;", "category": "Activities (event)", "order": "1759"},
+ {"emoji": "🌽", "name": "ear of corn", "shortname": ":corn:", "unicode": "1f33d", "html": "&#127805;", "category": "Food & Drink (food-vegetable)", "order": "1468"},
+ {"emoji": "🎾", "name": "tennis", "shortname": ":tennis:", "unicode": "1f3be", "html": "&#127934;", "category": "Activities (sport)", "order": "1787"},
+ {"emoji": "⏰", "name": "alarm clock", "shortname": ":alarm_clock:", "unicode": "23f0", "html": "&#9200;", "category": "Travel & Places (time)", "order": "1683"},
+ {"emoji": "🔋", "name": "battery", "shortname": ":battery:", "unicode": "1f50b", "html": "&#128267;", "category": "Objects (computer)", "order": "1844"},
+ {"emoji": "❕", "name": "white exclamation mark", "shortname": ":grey_exclamation:", "unicode": "2755", "html": "&#10069;", "category": "Symbols (other-symbol)", "order": "2100"},
+ {"emoji": "🐺", "name": "wolf", "shortname": ":wolf:", "unicode": "1f43a", "html": "&#128058;", "category": "Animals & Nature (animal-mammal)", "order": "1348"},
+ {"emoji": "🗿", "name": "moai", "shortname": ":moyai:", "unicode": "1f5ff", "html": "&#128511;", "category": "Objects (other-object)", "order": "1972"},
+ {"emoji": "🐮", "name": "cow face", "shortname": ":cow:", "unicode": "1f42e", "html": "&#128046;", "category": "Animals & Nature (animal-mammal)", "order": "1360"},
+ {"emoji": "📣", "name": "megaphone", "shortname": ":mega:", "unicode": "1f4e3", "html": "&#128227;", "category": "Objects (sound)", "order": "1819"},
+ {"emoji": "👴🏿", "name": "old man: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F474 1F3FF", "html": "&#128116;&#127999;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👴🏾", "name": "old man: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F474 1F3FE", "html": "&#128116;&#127998;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👴🏽", "name": "old man: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F474 1F3FD", "html": "&#128116;&#127997;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👴🏼", "name": "old man: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F474 1F3FC", "html": "&#128116;&#127996;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👴🏻", "name": "old man: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F474 1F3FB", "html": "&#128116;&#127995;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👴", "name": "old man", "shortname": ":older_man:", "unicode": "1f474", "html": "&#128116;", "category": "People & Body (person)", "order": "123"},
+ {"emoji": "👗", "name": "dress", "shortname": ":dress:", "unicode": "1f457", "html": "&#128087;", "category": "Objects (clothing)", "order": "1319"},
+ {"emoji": "🔗", "name": "link", "shortname": ":link:", "unicode": "1f517", "html": "&#128279;", "category": "Objects (tool)", "order": "1965"},
+ {"emoji": "🐔", "name": "chicken", "shortname": ":chicken:", "unicode": "1f414", "html": "&#128020;", "category": "Animals & Nature (animal-bird)", "order": "1388"},
+ {"emoji": "🍳", "name": "cooking", "shortname": ":cooking:", "unicode": "1F373", "html": "&#127859;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🐋", "name": "whale", "shortname": ":whale2:", "unicode": "1f40b", "html": "&#128011;", "category": "Animals & Nature (animal-marine)", "order": "1407"},
+ {"emoji": "↖", "name": "up-left arrow", "shortname": ":arrow_upper_left:", "unicode": "2196", "html": "&#8598;", "category": "Symbols (arrow)", "order": "2009"},
+ {"emoji": "🌳", "name": "deciduous tree", "shortname": ":deciduous_tree:", "unicode": "1f333", "html": "&#127795;", "category": "Animals & Nature (plant-other)", "order": "1439"},
+ {"emoji": "🍱", "name": "bento box", "shortname": ":bento:", "unicode": "1f371", "html": "&#127857;", "category": "Food & Drink (food-asian)", "order": "1495"},
+ {"emoji": "📌", "name": "pushpin", "shortname": ":pushpin:", "unicode": "1f4cc", "html": "&#128204;", "category": "Objects (office)", "order": "1934"},
+ {"emoji": "🔜", "name": "SOON arrow", "shortname": ":soon:", "unicode": "1f51c", "html": "&#128284;", "category": "Symbols (arrow)", "order": "2021"},
+ {"emoji": "🔁", "name": "repeat button", "shortname": ":repeat:", "unicode": "1f501", "html": "&#128257;", "category": "Symbols (av-symbol)", "order": "2049"},
+ {"emoji": "🐉", "name": "dragon", "shortname": ":dragon:", "unicode": "1f409", "html": "&#128009;", "category": "Animals & Nature (animal-reptile)", "order": "1405"},
+ {"emoji": "🐹", "name": "hamster", "shortname": ":hamster:", "unicode": "1f439", "html": "&#128057;", "category": "Animals & Nature (animal-mammal)", "order": "1378"},
+ {"emoji": "⛳", "name": "flag in hole", "shortname": ":golf:", "unicode": "26f3", "html": "&#9971;", "category": "Activities (sport)", "order": "1799"},
+ {"emoji": "🏄🏿‍♀", "name": "woman surfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C4 1F3FF 200D 2640", "html": "&#127940;&#127999;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏾‍♀", "name": "woman surfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C4 1F3FE 200D 2640", "html": "&#127940;&#127998;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏽‍♀", "name": "woman surfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C4 1F3FD 200D 2640", "html": "&#127940;&#127997;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏼‍♀", "name": "woman surfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C4 1F3FC 200D 2640", "html": "&#127940;&#127996;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏻‍♀", "name": "woman surfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C4 1F3FB 200D 2640", "html": "&#127940;&#127995;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄‍♀", "name": "woman surfing", "shortname": ":woman_surfing:", "unicode": "1F3C4 200D 2640", "html": "&#127940;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏿", "name": "person surfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C4 1F3FF", "html": "&#127940;&#127999;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏾", "name": "person surfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C4 1F3FE", "html": "&#127940;&#127998;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏽", "name": "person surfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C4 1F3FD", "html": "&#127940;&#127997;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏼", "name": "person surfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C4 1F3FC", "html": "&#127940;&#127996;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏻", "name": "person surfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C4 1F3FB", "html": "&#127940;&#127995;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄", "name": "person surfing", "shortname": ":surfer:", "unicode": "1f3c4", "html": "&#127940;", "category": "People & Body (person-sport)", "order": "800"},
+ {"emoji": "🐭", "name": "mouse face", "shortname": ":mouse:", "unicode": "1f42d", "html": "&#128045;", "category": "Animals & Nature (animal-mammal)", "order": "1375"},
+ {"emoji": "🌒", "name": "waxing crescent moon", "shortname": ":waxing_crescent_moon:", "unicode": "1f312", "html": "&#127762;", "category": "Travel & Places (sky & weather)", "order": "1712"},
+ {"emoji": "🚙", "name": "sport utility vehicle", "shortname": ":blue_car:", "unicode": "1f699", "html": "&#128665;", "category": "Travel & Places (transport-ground)", "order": "1626"},
+ {"emoji": "🅰️", "name": "A button (blood type)", "shortname": ":a:", "unicode": "1f170", "html": "&#127344;", "category": "Symbols (alphanum)", "order": "2125"},
+ {"emoji": "⁉️", "name": "exclamation question mark", "shortname": ":interrobang:", "unicode": "2049", "html": "&#8265;", "category": "Symbols (other-symbol)", "order": "2097"},
+ {"emoji": "🈹", "name": "Japanese discount button", "shortname": ":u5272:", "unicode": "1f239", "html": "&#127545;", "category": "Symbols (alphanum)", "order": "2148"},
+ {"emoji": "🔌", "name": "electric plug", "shortname": ":electric_plug:", "unicode": "1f50c", "html": "&#128268;", "category": "Objects (computer)", "order": "1845"},
+ {"emoji": "🌓", "name": "first quarter moon", "shortname": ":first_quarter_moon:", "unicode": "1f313", "html": "&#127763;", "category": "Travel & Places (sky & weather)", "order": "1713"},
+ {"emoji": "♋", "name": "Cancer", "shortname": ":cancer:", "unicode": "264b", "html": "&#9803;", "category": "Symbols (zodiac)", "order": "2038"},
+ {"emoji": "🔱", "name": "trident emblem", "shortname": ":trident:", "unicode": "1f531", "html": "&#128305;", "category": "Symbols (other-symbol)", "order": "2076"},
+ {"emoji": "🍞", "name": "bread", "shortname": ":bread:", "unicode": "1f35e", "html": "&#127838;", "category": "Food & Drink (food-prepared)", "order": "1474"},
+ {"emoji": "👮🏿‍♀", "name": "woman police officer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F46E 1F3FF 200D 2640", "html": "&#128110;&#127999;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏾‍♀", "name": "woman police officer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F46E 1F3FE 200D 2640", "html": "&#128110;&#127998;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏽‍♀", "name": "woman police officer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F46E 1F3FD 200D 2640", "html": "&#128110;&#127997;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏼‍♀", "name": "woman police officer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F46E 1F3FC 200D 2640", "html": "&#128110;&#127996;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏻‍♀", "name": "woman police officer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F46E 1F3FB 200D 2640", "html": "&#128110;&#127995;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮‍♀", "name": "woman police officer", "shortname": ":woman_police_officer:", "unicode": "1F46E 200D 2640", "html": "&#128110;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏿", "name": "police officer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F46E 1F3FF", "html": "&#128110;&#127999;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏾", "name": "police officer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F46E 1F3FE", "html": "&#128110;&#127998;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏽", "name": "police officer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F46E 1F3FD", "html": "&#128110;&#127997;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏼", "name": "police officer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F46E 1F3FC", "html": "&#128110;&#127996;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏻", "name": "police officer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F46E 1F3FB", "html": "&#128110;&#127995;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮", "name": "police officer", "shortname": ":cop:", "unicode": "1f46e", "html": "&#128110;", "category": "People & Body (person-role)", "order": "339"},
+ {"emoji": "🍵", "name": "teacup without handle", "shortname": ":tea:", "unicode": "1f375", "html": "&#127861;", "category": "Food & Drink (drink)", "order": "1523"},
+ {"emoji": "🎣", "name": "fishing pole", "shortname": ":fishing_pole_and_fish:", "unicode": "1f3a3", "html": "&#127907;", "category": "Activities (sport)", "order": "1801"},
+ {"emoji": "🌔", "name": "waxing gibbous moon", "shortname": ":waxing_gibbous_moon:", "unicode": "1F314", "html": "&#127764;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🚲", "name": "bicycle", "shortname": ":bike:", "unicode": "1f6b2", "html": "&#128690;", "category": "Travel & Places (transport-ground)", "order": "1630"},
+ {"emoji": "👤", "name": "bust in silhouette", "shortname": ":bust_in_silhouette:", "unicode": "1F464", "html": "&#128100;", "category": "People & Body (person-symbol)", "order": ""},
+ {"emoji": "🍚", "name": "cooked rice", "shortname": ":rice:", "unicode": "1f35a", "html": "&#127834;", "category": "Food & Drink (food-asian)", "order": "1498"},
+ {"emoji": "📻", "name": "radio", "shortname": ":radio:", "unicode": "1f4fb", "html": "&#128251;", "category": "Objects (music)", "order": "1831"},
+ {"emoji": "🐤", "name": "baby chick", "shortname": ":baby_chick:", "unicode": "1f424", "html": "&#128036;", "category": "Animals & Nature (animal-bird)", "order": "1391"},
+ {"emoji": "⤵️", "name": "right arrow curving down", "shortname": ":arrow_heading_down:", "unicode": "2935", "html": "&#10549;", "category": "Symbols (arrow)", "order": "2015"},
+ {"emoji": "🌘", "name": "waning crescent moon", "shortname": ":waning_crescent_moon:", "unicode": "1f318", "html": "&#127768;", "category": "Travel & Places (sky & weather)", "order": "1718"},
+ {"emoji": "↕", "name": "up-down arrow", "shortname": ":arrow_up_down:", "unicode": "2195", "html": "&#8597;", "category": "Symbols (arrow)", "order": "2010"},
+ {"emoji": "🇪", "name": "", "shortname": "", "unicode": "", "html": "&#127466;", "category": "", "order": ""},
+ {"emoji": "🌗", "name": "last quarter moon", "shortname": ":last_quarter_moon:", "unicode": "1f317", "html": "&#127767;", "category": "Travel & Places (sky & weather)", "order": "1717"},
+ {"emoji": "🔘", "name": "radio button", "shortname": ":radio_button:", "unicode": "1f518", "html": "&#128280;", "category": "Symbols (geometric)", "order": "2174"},
+ {"emoji": "🐑", "name": "ewe", "shortname": ":sheep:", "unicode": "1f411", "html": "&#128017;", "category": "Animals & Nature (animal-mammal)", "order": "1369"},
+ {"emoji": "👱🏿‍♀", "name": "woman: dark skin tone, blond hair", "shortname": ":dark_skin_tone_blond_hair:", "unicode": "1F471 1F3FF 200D 2640", "html": "&#128113;&#127999;&zwj;&#9792;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏾‍♀", "name": "woman: medium-dark skin tone, blond hair", "shortname": ":mediumdark_skin_tone_blond_hair:", "unicode": "1F471 1F3FE 200D 2640", "html": "&#128113;&#127998;&zwj;&#9792;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏽‍♀", "name": "woman: medium skin tone, blond hair", "shortname": ":medium_skin_tone_blond_hair:", "unicode": "1F471 1F3FD 200D 2640", "html": "&#128113;&#127997;&zwj;&#9792;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏼‍♀", "name": "woman: medium-light skin tone, blond hair", "shortname": ":mediumlight_skin_tone_blond_hair:", "unicode": "1F471 1F3FC 200D 2640", "html": "&#128113;&#127996;&zwj;&#9792;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏻‍♀", "name": "woman: light skin tone, blond hair", "shortname": ":light_skin_tone_blond_hair:", "unicode": "1F471 1F3FB 200D 2640", "html": "&#128113;&#127995;&zwj;&#9792;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱‍♀", "name": "woman: blond hair", "shortname": ":blond_hair:", "unicode": "1F471 200D 2640", "html": "&#128113;&zwj;&#9792;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏿", "name": "person: dark skin tone, blond hair", "shortname": ":dark_skin_tone_blond_hair:", "unicode": "1F471 1F3FF", "html": "&#128113;&#127999;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏾", "name": "person: medium-dark skin tone, blond hair", "shortname": ":mediumdark_skin_tone_blond_hair:", "unicode": "1F471 1F3FE", "html": "&#128113;&#127998;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏽", "name": "person: medium skin tone, blond hair", "shortname": ":medium_skin_tone_blond_hair:", "unicode": "1F471 1F3FD", "html": "&#128113;&#127997;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏼", "name": "person: medium-light skin tone, blond hair", "shortname": ":mediumlight_skin_tone_blond_hair:", "unicode": "1F471 1F3FC", "html": "&#128113;&#127996;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏻", "name": "person: light skin tone, blond hair", "shortname": ":light_skin_tone_blond_hair:", "unicode": "1F471 1F3FB", "html": "&#128113;&#127995;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱", "name": "person: blond hair", "shortname": ":person_with_blond_hair:", "unicode": "1f471", "html": "&#128113;", "category": "People & Body (person)", "order": "429"},
+ {"emoji": "🌖", "name": "waning gibbous moon", "shortname": ":waning_gibbous_moon:", "unicode": "1f316", "html": "&#127766;", "category": "Travel & Places (sky & weather)", "order": "1716"},
+ {"emoji": "🔒", "name": "locked", "shortname": ":lock:", "unicode": "1f512", "html": "&#128274;", "category": "Objects (lock)", "order": "1944"},
+ {"emoji": "🍏", "name": "green apple", "shortname": ":green_apple:", "unicode": "1f34f", "html": "&#127823;", "category": "Food & Drink (food-fruit)", "order": "1457"},
+ {"emoji": "👺", "name": "goblin", "shortname": ":japanese_goblin:", "unicode": "1f47a", "html": "&#128122;", "category": "Smileys & Emotion (face-costume)", "order": "79"},
+ {"emoji": "➰", "name": "curly loop", "shortname": ":curly_loop:", "unicode": "27b0", "html": "&#10160;", "category": "Symbols (other-symbol)", "order": "2090"},
+ {"emoji": "🚩", "name": "triangular flag", "shortname": ":triangular_flag_on_post:", "unicode": "1f6a9", "html": "&#128681;", "category": "Flags (flag)", "order": "2182"},
+ {"emoji": "🔄", "name": "counterclockwise arrows button", "shortname": ":arrows_counterclockwise:", "unicode": "1f504", "html": "&#128260;", "category": "Symbols (arrow)", "order": "2017"},
+ {"emoji": "🐎", "name": "horse", "shortname": ":racehorse:", "unicode": "1f40e", "html": "&#128014;", "category": "Animals & Nature (animal-mammal)", "order": "1357"},
+ {"emoji": "🍤", "name": "fried shrimp", "shortname": ":fried_shrimp:", "unicode": "1f364", "html": "&#127844;", "category": "Food & Drink (food-asian)", "order": "1505"},
+ {"emoji": "🌄", "name": "sunrise over mountains", "shortname": ":sunrise_over_mountains:", "unicode": "1f304", "html": "&#127748;", "category": "Travel & Places (place-other)", "order": "1586"},
+ {"emoji": "🌋", "name": "volcano", "shortname": ":volcano:", "unicode": "1f30b", "html": "&#127755;", "category": "Travel & Places (place-geographic)", "order": "1546"},
+ {"emoji": "🐓", "name": "rooster", "shortname": ":rooster:", "unicode": "1f413", "html": "&#128019;", "category": "Animals & Nature (animal-bird)", "order": "1389"},
+ {"emoji": "📥", "name": "inbox tray", "shortname": ":inbox_tray:", "unicode": "1f4e5", "html": "&#128229;", "category": "Objects (mail)", "order": "1906"},
+ {"emoji": "💒", "name": "wedding", "shortname": ":wedding:", "unicode": "1f492", "html": "&#128146;", "category": "Travel & Places (place-building)", "order": "1574"},
+ {"emoji": "🍣", "name": "sushi", "shortname": ":sushi:", "unicode": "1f363", "html": "&#127843;", "category": "Food & Drink (food-asian)", "order": "1504"},
+ {"emoji": "〰", "name": "wavy dash", "shortname": ":wavy_dash:", "unicode": "3030", "html": "&#12336;", "category": "Symbols (other-symbol)", "order": "2102"},
+ {"emoji": "🍨", "name": "ice cream", "shortname": ":ice_cream:", "unicode": "1f368", "html": "&#127848;", "category": "Food & Drink (food-sweet)", "order": "1510"},
+ {"emoji": "⏪", "name": "fast reverse button", "shortname": ":rewind:", "unicode": "23ea", "html": "&#9194;", "category": "Symbols (av-symbol)", "order": "2056"},
+ {"emoji": "🍅", "name": "tomato", "shortname": ":tomato:", "unicode": "1f345", "html": "&#127813;", "category": "Food & Drink (food-fruit)", "order": "1463"},
+ {"emoji": "🐇", "name": "rabbit", "shortname": ":rabbit2:", "unicode": "1f407", "html": "&#128007;", "category": "Animals & Nature (animal-mammal)", "order": "1380"},
+ {"emoji": "✴️", "name": "eight-pointed star", "shortname": ":eight_pointed_black_star:", "unicode": "2734", "html": "&#10036;", "category": "Symbols (other-symbol)", "order": "2094"},
+ {"emoji": "🔺", "name": "red triangle pointed up", "shortname": ":small_red_triangle:", "unicode": "1f53a", "html": "&#128314;", "category": "Symbols (geometric)", "order": "2171"},
+ {"emoji": "🔆", "name": "bright button", "shortname": ":high_brightness:", "unicode": "1f506", "html": "&#128262;", "category": "Symbols (av-symbol)", "order": "2068"},
+ {"emoji": "➕", "name": "plus sign", "shortname": ":heavy_plus_sign:", "unicode": "2795", "html": "&#10133;", "category": "Symbols (other-symbol)", "order": "2084"},
+ {"emoji": "👲🏿", "name": "man with skullcap: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F472 1F3FF", "html": "&#128114;&#127999;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👲🏾", "name": "man with skullcap: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F472 1F3FE", "html": "&#128114;&#127998;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👲🏽", "name": "man with skullcap: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F472 1F3FD", "html": "&#128114;&#127997;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👲🏼", "name": "man with skullcap: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F472 1F3FC", "html": "&#128114;&#127996;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👲🏻", "name": "man with skullcap: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F472 1F3FB", "html": "&#128114;&#127995;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👲", "name": "man with skullcap", "shortname": ":man_with_gua_pi_mao:", "unicode": "1f472", "html": "&#128114;", "category": "People & Body (person-role)", "order": "489"},
+ {"emoji": "🏪", "name": "convenience store", "shortname": ":convenience_store:", "unicode": "1f3ea", "html": "&#127978;", "category": "Travel & Places (place-building)", "order": "1568"},
+ {"emoji": "👥", "name": "busts in silhouette", "shortname": ":busts_in_silhouette:", "unicode": "1f465", "html": "&#128101;", "category": "People & Body (person-symbol)", "order": "767"},
+ {"emoji": "🐞", "name": "lady beetle", "shortname": ":beetle:", "unicode": "1f41e", "html": "&#128030;", "category": "Animals & Nature (animal-bug)", "order": "1423"},
+ {"emoji": "🔻", "name": "red triangle pointed down", "shortname": ":small_red_triangle_down:", "unicode": "1f53b", "html": "&#128315;", "category": "Symbols (geometric)", "order": "2172"},
+ {"emoji": "🇩🇪", "name": "flag: Germany", "shortname": ":ger:", "unicode": "1F1E9 1F1EA", "html": "&#127465;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "⤴️", "name": "right arrow curving up", "shortname": ":arrow_heading_up:", "unicode": "2934", "html": "&#10548;", "category": "Symbols (arrow)", "order": "2014"},
+ {"emoji": "📛", "name": "name badge", "shortname": ":name_badge:", "unicode": "1f4db", "html": "&#128219;", "category": "Symbols (other-symbol)", "order": "2073"},
+ {"emoji": "🛀🏿", "name": "person taking bath: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6C0 1F3FF", "html": "&#128704;&#127999;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛀🏾", "name": "person taking bath: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6C0 1F3FE", "html": "&#128704;&#127998;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛀🏽", "name": "person taking bath: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6C0 1F3FD", "html": "&#128704;&#127997;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛀🏼", "name": "person taking bath: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6C0 1F3FC", "html": "&#128704;&#127996;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛀🏻", "name": "person taking bath: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6C0 1F3FB", "html": "&#128704;&#127995;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛀", "name": "person taking bath", "shortname": ":bath:", "unicode": "1f6c0", "html": "&#128704;", "category": "People & Body (person-resting)", "order": "1673"},
+ {"emoji": "⛔", "name": "no entry", "shortname": ":no_entry:", "unicode": "26d4", "html": "&#9940;", "category": "Symbols (warning)", "order": "1991"},
+ {"emoji": "🐊", "name": "crocodile", "shortname": ":crocodile:", "unicode": "1f40a", "html": "&#128010;", "category": "Animals & Nature (animal-reptile)", "order": "1400"},
+ {"emoji": "🌰", "name": "chestnut", "shortname": ":chestnut:", "unicode": "1F330", "html": "&#127792;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🐕", "name": "dog", "shortname": ":dog2:", "unicode": "1f415", "html": "&#128021;", "category": "Animals & Nature (animal-mammal)", "order": "1346"},
+ {"emoji": "🐈", "name": "cat", "shortname": ":cat2:", "unicode": "1f408", "html": "&#128008;", "category": "Animals & Nature (animal-mammal)", "order": "1351"},
+ {"emoji": "🔨", "name": "hammer", "shortname": ":hammer:", "unicode": "1f528", "html": "&#128296;", "category": "Objects (tool)", "order": "1950"},
+ {"emoji": "🍖", "name": "meat on bone", "shortname": ":meat_on_bone:", "unicode": "1f356", "html": "&#127830;", "category": "Food & Drink (food-prepared)", "order": "1479"},
+ {"emoji": "🐚", "name": "spiral shell", "shortname": ":shell:", "unicode": "1f41a", "html": "&#128026;", "category": "Animals & Nature (animal-marine)", "order": "1414"},
+ {"emoji": "❇️", "name": "sparkle", "shortname": ":sparkle:", "unicode": "2747", "html": "&#10055;", "category": "Symbols (other-symbol)", "order": "2095"},
+ {"emoji": "⛵", "name": "sailboat", "shortname": ":sailboat:", "unicode": "26F5", "html": "&#9973;", "category": "Travel & Places (transport-water)", "order": ""},
+ {"emoji": "🅱️", "name": "B button (blood type)", "shortname": ":b:", "unicode": "1f171", "html": "&#127345;", "category": "Symbols (alphanum)", "order": "2127"},
+ {"emoji": "Ⓜ️", "name": "circled M", "shortname": ":m:", "unicode": "24c2", "html": "&#9410;", "category": "Symbols (alphanum)", "order": "2133"},
+ {"emoji": "🐩", "name": "poodle", "shortname": ":poodle:", "unicode": "1f429", "html": "&#128041;", "category": "Animals & Nature (animal-mammal)", "order": "1347"},
+ {"emoji": "♒", "name": "Aquarius", "shortname": ":aquarius:", "unicode": "2652", "html": "&#9810;", "category": "Symbols (zodiac)", "order": "2045"},
+ {"emoji": "🍲", "name": "pot of food", "shortname": ":stew:", "unicode": "1f372", "html": "&#127858;", "category": "Food & Drink (food-prepared)", "order": "1492"},
+ {"emoji": "👖", "name": "jeans", "shortname": ":jeans:", "unicode": "1f456", "html": "&#128086;", "category": "Objects (clothing)", "order": "1318"},
+ {"emoji": "🍯", "name": "honey pot", "shortname": ":honey_pot:", "unicode": "1f36f", "html": "&#127855;", "category": "Food & Drink (food-sweet)", "order": "1519"},
+ {"emoji": "🎹", "name": "musical keyboard", "shortname": ":musical_keyboard:", "unicode": "1f3b9", "html": "&#127929;", "category": "Objects (musical-instrument)", "order": "1834"},
+ {"emoji": "🔓", "name": "unlocked", "shortname": ":unlock:", "unicode": "1f513", "html": "&#128275;", "category": "Objects (lock)", "order": "1945"},
+ {"emoji": "✒", "name": "black nib", "shortname": ":black_nib:", "unicode": "2712", "html": "&#10002;", "category": "Objects (writing)", "order": "1915"},
+ {"emoji": "🗽", "name": "Statue of Liberty", "shortname": ":statue_of_liberty:", "unicode": "1f5fd", "html": "&#128509;", "category": "Travel & Places (place-building)", "order": "1576"},
+ {"emoji": "💲", "name": "heavy dollar sign", "shortname": ":heavy_dollar_sign:", "unicode": "1f4b2", "html": "&#128178;", "category": "Objects (money)", "order": "1900"},
+ {"emoji": "🏂", "name": "snowboarder", "shortname": ":snowboarder:", "unicode": "1f3c2", "html": "&#127938;", "category": "People & Body (person-sport)", "order": "776"},
+ {"emoji": "💮", "name": "white flower", "shortname": ":white_flower:", "unicode": "1f4ae", "html": "&#128174;", "category": "Animals & Nature (plant-flower)", "order": "1429"},
+ {"emoji": "👔", "name": "necktie", "shortname": ":necktie:", "unicode": "1f454", "html": "&#128084;", "category": "Objects (clothing)", "order": "1316"},
+ {"emoji": "💠", "name": "diamond with a dot", "shortname": ":diamond_shape_with_a_dot_inside:", "unicode": "1f4a0", "html": "&#128160;", "category": "Symbols (geometric)", "order": "2173"},
+ {"emoji": "♈", "name": "Aries", "shortname": ":aries:", "unicode": "2648", "html": "&#9800;", "category": "Symbols (zodiac)", "order": "2035"},
+ {"emoji": "🚺", "name": "women’s room", "shortname": ":womens:", "unicode": "1f6ba", "html": "&#128698;", "category": "Symbols (transport-sign)", "order": "1981"},
+ {"emoji": "🐜", "name": "ant", "shortname": ":ant:", "unicode": "1f41c", "html": "&#128028;", "category": "Animals & Nature (animal-bug)", "order": "1421"},
+ {"emoji": "♏", "name": "Scorpio", "shortname": ":scorpius:", "unicode": "264f", "html": "&#9807;", "category": "Symbols (zodiac)", "order": "2042"},
+ {"emoji": "🌇", "name": "sunset", "shortname": ":city_sunset:", "unicode": "1f307", "html": "&#127751;", "category": "Travel & Places (place-other)", "order": "1589"},
+ {"emoji": "⏳", "name": "hourglass not done", "shortname": ":hourglass_flowing_sand:", "unicode": "23f3", "html": "&#9203;", "category": "Travel & Places (time)", "order": "1681"},
+ {"emoji": "🅾️", "name": "O button (blood type)", "shortname": ":o2:", "unicode": "1f17e", "html": "&#127358;", "category": "Symbols (alphanum)", "order": "2136"},
+ {"emoji": "🐲", "name": "dragon face", "shortname": ":dragon_face:", "unicode": "1f432", "html": "&#128050;", "category": "Animals & Nature (animal-reptile)", "order": "1404"},
+ {"emoji": "🐌", "name": "snail", "shortname": ":snail:", "unicode": "1f40c", "html": "&#128012;", "category": "Animals & Nature (animal-bug)", "order": "1419"},
+ {"emoji": "📀", "name": "dvd", "shortname": ":dvd:", "unicode": "1f4c0", "html": "&#128192;", "category": "Objects (computer)", "order": "1855"},
+ {"emoji": "👕", "name": "t-shirt", "shortname": ":shirt:", "unicode": "1f455", "html": "&#128085;", "category": "Objects (clothing)", "order": "1317"},
+ {"emoji": "🎲", "name": "game die", "shortname": ":game_die:", "unicode": "1f3b2", "html": "&#127922;", "category": "Activities (game)", "order": "1806"},
+ {"emoji": "➖", "name": "minus sign", "shortname": ":heavy_minus_sign:", "unicode": "2796", "html": "&#10134;", "category": "Symbols (other-symbol)", "order": "2088"},
+ {"emoji": "🎎", "name": "Japanese dolls", "shortname": ":dolls:", "unicode": "1f38e", "html": "&#127886;", "category": "Activities (event)", "order": "1766"},
+ {"emoji": "♐", "name": "Sagittarius", "shortname": ":sagittarius:", "unicode": "2650", "html": "&#9808;", "category": "Symbols (zodiac)", "order": "2043"},
+ {"emoji": "🎱", "name": "pool 8 ball", "shortname": ":8ball:", "unicode": "1f3b1", "html": "&#127921;", "category": "Activities (game)", "order": "1788"},
+ {"emoji": "🚌", "name": "bus", "shortname": ":bus:", "unicode": "1f68c", "html": "&#128652;", "category": "Travel & Places (transport-ground)", "order": "1614"},
+ {"emoji": "🍮", "name": "custard", "shortname": ":custard:", "unicode": "1f36e", "html": "&#127854;", "category": "Food & Drink (food-sweet)", "order": "1518"},
+ {"emoji": "🎌", "name": "crossed flags", "shortname": ":crossed_flags:", "unicode": "1f38c", "html": "&#127884;", "category": "Flags (flag)", "order": "2183"},
+ {"emoji": "〽️", "name": "part alternation mark", "shortname": ":part_alternation_mark:", "unicode": "303d", "html": "&#12349;", "category": "Symbols (other-symbol)", "order": "2092"},
+ {"emoji": "🐫", "name": "two-hump camel", "shortname": ":camel:", "unicode": "1f42b", "html": "&#128043;", "category": "Animals & Nature (animal-mammal)", "order": "1372"},
+ {"emoji": "🍛", "name": "curry rice", "shortname": ":curry:", "unicode": "1f35b", "html": "&#127835;", "category": "Food & Drink (food-asian)", "order": "1499"},
+ {"emoji": "🚂", "name": "locomotive", "shortname": ":steam_locomotive:", "unicode": "1f682", "html": "&#128642;", "category": "Travel & Places (transport-ground)", "order": "1602"},
+ {"emoji": "🏥", "name": "hospital", "shortname": ":hospital:", "unicode": "1f3e5", "html": "&#127973;", "category": "Travel & Places (place-building)", "order": "1564"},
+ {"emoji": "🇯🇵", "name": "flag: Japan", "shortname": ":jp:", "unicode": "1F1EF 1F1F5", "html": "&#127471;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "🔷", "name": "large blue diamond", "shortname": ":large_blue_diamond:", "unicode": "1f537", "html": "&#128311;", "category": "Symbols (geometric)", "order": "2168"},
+ {"emoji": "🎋", "name": "tanabata tree", "shortname": ":tanabata_tree:", "unicode": "1f38b", "html": "&#127883;", "category": "Activities (event)", "order": "1764"},
+ {"emoji": "🔔", "name": "bell", "shortname": ":bell:", "unicode": "1f514", "html": "&#128276;", "category": "Objects (sound)", "order": "1821"},
+ {"emoji": "♌", "name": "Leo", "shortname": ":leo:", "unicode": "264c", "html": "&#9804;", "category": "Symbols (zodiac)", "order": "2039"},
+ {"emoji": "♊", "name": "Gemini", "shortname": ":gemini:", "unicode": "264a", "html": "&#9802;", "category": "Symbols (zodiac)", "order": "2037"},
+ {"emoji": "🍐", "name": "pear", "shortname": ":pear:", "unicode": "1f350", "html": "&#127824;", "category": "Food & Drink (food-fruit)", "order": "1458"},
+ {"emoji": "🔶", "name": "large orange diamond", "shortname": ":large_orange_diamond:", "unicode": "1f536", "html": "&#128310;", "category": "Symbols (geometric)", "order": "2167"},
+ {"emoji": "♉", "name": "Taurus", "shortname": ":taurus:", "unicode": "2649", "html": "&#9801;", "category": "Symbols (zodiac)", "order": "2036"},
+ {"emoji": "🌐", "name": "globe with meridians", "shortname": ":globe_with_meridians:", "unicode": "1f310", "html": "&#127760;", "category": "Travel & Places (place-map)", "order": "1541"},
+ {"emoji": "🚪", "name": "door", "shortname": ":door:", "unicode": "1f6aa", "html": "&#128682;", "category": "Objects (household)", "order": "1662"},
+ {"emoji": "🕕", "name": "six o’clock", "shortname": ":clock6:", "unicode": "1f555", "html": "&#128341;", "category": "Travel & Places (time)", "order": "1699"},
+ {"emoji": "🚔", "name": "oncoming police car", "shortname": ":oncoming_police_car:", "unicode": "1f694", "html": "&#128660;", "category": "Travel & Places (transport-ground)", "order": "1621"},
+ {"emoji": "📩", "name": "envelope with arrow", "shortname": ":envelope_with_arrow:", "unicode": "1f4e9", "html": "&#128233;", "category": "Objects (mail)", "order": "1904"},
+ {"emoji": "🌂", "name": "closed umbrella", "shortname": ":closed_umbrella:", "unicode": "1f302", "html": "&#127746;", "category": "Travel & Places (sky & weather)", "order": "1744"},
+ {"emoji": "🎷", "name": "saxophone", "shortname": ":saxophone:", "unicode": "1f3b7", "html": "&#127927;", "category": "Objects (musical-instrument)", "order": "1832"},
+ {"emoji": "⛪", "name": "church", "shortname": ":church:", "unicode": "26ea", "html": "&#9962;", "category": "Travel & Places (place-religious)", "order": "1577"},
+ {"emoji": "🚴🏿‍♀", "name": "woman biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B4 1F3FF 200D 2640", "html": "&#128692;&#127999;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏾‍♀", "name": "woman biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B4 1F3FE 200D 2640", "html": "&#128692;&#127998;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏽‍♀", "name": "woman biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B4 1F3FD 200D 2640", "html": "&#128692;&#127997;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏼‍♀", "name": "woman biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B4 1F3FC 200D 2640", "html": "&#128692;&#127996;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏻‍♀", "name": "woman biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B4 1F3FB 200D 2640", "html": "&#128692;&#127995;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴‍♀", "name": "woman biking", "shortname": ":woman_biking:", "unicode": "1F6B4 200D 2640", "html": "&#128692;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏿", "name": "person biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B4 1F3FF", "html": "&#128692;&#127999;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏾", "name": "person biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B4 1F3FE", "html": "&#128692;&#127998;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏽", "name": "person biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B4 1F3FD", "html": "&#128692;&#127997;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏼", "name": "person biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B4 1F3FC", "html": "&#128692;&#127996;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏻", "name": "person biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B4 1F3FB", "html": "&#128692;&#127995;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴", "name": "person biking", "shortname": ":bicyclist:", "unicode": "1f6b4", "html": "&#128692;", "category": "People & Body (person-sport)", "order": "890"},
+ {"emoji": "♓", "name": "Pisces", "shortname": ":pisces:", "unicode": "2653", "html": "&#9811;", "category": "Symbols (zodiac)", "order": "2046"},
+ {"emoji": "🍡", "name": "dango", "shortname": ":dango:", "unicode": "1f361", "html": "&#127841;", "category": "Food & Drink (food-asian)", "order": "1507"},
+ {"emoji": "♑", "name": "Capricorn", "shortname": ":capricorn:", "unicode": "2651", "html": "&#9809;", "category": "Symbols (zodiac)", "order": "2044"},
+ {"emoji": "🏢", "name": "office building", "shortname": ":office:", "unicode": "1f3e2", "html": "&#127970;", "category": "Travel & Places (place-building)", "order": "1561"},
+ {"emoji": "🚣🏿‍♀", "name": "woman rowing boat: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6A3 1F3FF 200D 2640", "html": "&#128675;&#127999;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏾‍♀", "name": "woman rowing boat: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6A3 1F3FE 200D 2640", "html": "&#128675;&#127998;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏽‍♀", "name": "woman rowing boat: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6A3 1F3FD 200D 2640", "html": "&#128675;&#127997;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏼‍♀", "name": "woman rowing boat: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6A3 1F3FC 200D 2640", "html": "&#128675;&#127996;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏻‍♀", "name": "woman rowing boat: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6A3 1F3FB 200D 2640", "html": "&#128675;&#127995;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣‍♀", "name": "woman rowing boat", "shortname": ":woman_rowing_boat:", "unicode": "1F6A3 200D 2640", "html": "&#128675;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏿", "name": "person rowing boat: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6A3 1F3FF", "html": "&#128675;&#127999;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏾", "name": "person rowing boat: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6A3 1F3FE", "html": "&#128675;&#127998;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏽", "name": "person rowing boat: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6A3 1F3FD", "html": "&#128675;&#127997;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏼", "name": "person rowing boat: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6A3 1F3FC", "html": "&#128675;&#127996;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏻", "name": "person rowing boat: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6A3 1F3FB", "html": "&#128675;&#127995;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣", "name": "person rowing boat", "shortname": ":rowboat:", "unicode": "1f6a3", "html": "&#128675;", "category": "People & Body (person-sport)", "order": "818"},
+ {"emoji": "👒", "name": "woman’s hat", "shortname": ":womans_hat:", "unicode": "1f452", "html": "&#128082;", "category": "Objects (clothing)", "order": "1334"},
+ {"emoji": "👞", "name": "man’s shoe", "shortname": ":mans_shoe:", "unicode": "1f45e", "html": "&#128094;", "category": "Objects (clothing)", "order": "1328"},
+ {"emoji": "🏩", "name": "love hotel", "shortname": ":love_hotel:", "unicode": "1f3e9", "html": "&#127977;", "category": "Travel & Places (place-building)", "order": "1567"},
+ {"emoji": "🗻", "name": "mount fuji", "shortname": ":mount_fuji:", "unicode": "1f5fb", "html": "&#128507;", "category": "Travel & Places (place-geographic)", "order": "1547"},
+ {"emoji": "🐪", "name": "camel", "shortname": ":dromedary_camel:", "unicode": "1f42a", "html": "&#128042;", "category": "Animals & Nature (animal-mammal)", "order": "1371"},
+ {"emoji": "👜", "name": "handbag", "shortname": ":handbag:", "unicode": "1f45c", "html": "&#128092;", "category": "Objects (clothing)", "order": "1324"},
+ {"emoji": "⌛", "name": "hourglass done", "shortname": ":hourglass:", "unicode": "231b", "html": "&#8987;", "category": "Travel & Places (time)", "order": "1680"},
+ {"emoji": "❎", "name": "cross mark button", "shortname": ":negative_squared_cross_mark:", "unicode": "274e", "html": "&#10062;", "category": "Symbols (other-symbol)", "order": "2083"},
+ {"emoji": "🎺", "name": "trumpet", "shortname": ":trumpet:", "unicode": "1f3ba", "html": "&#127930;", "category": "Objects (musical-instrument)", "order": "1835"},
+ {"emoji": "🏫", "name": "school", "shortname": ":school:", "unicode": "1f3eb", "html": "&#127979;", "category": "Travel & Places (place-building)", "order": "1569"},
+ {"emoji": "🐄", "name": "cow", "shortname": ":cow2:", "unicode": "1f404", "html": "&#128004;", "category": "Animals & Nature (animal-mammal)", "order": "1363"},
+ {"emoji": "🌆", "name": "cityscape at dusk", "shortname": ":cityscape_at_dusk:", "unicode": "1F306", "html": "&#127750;", "category": "Travel & Places (place-other)", "order": ""},
+ {"emoji": "👷🏿‍♀", "name": "woman construction worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F477 1F3FF 200D 2640", "html": "&#128119;&#127999;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏾‍♀", "name": "woman construction worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F477 1F3FE 200D 2640", "html": "&#128119;&#127998;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏽‍♀", "name": "woman construction worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F477 1F3FD 200D 2640", "html": "&#128119;&#127997;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏼‍♀", "name": "woman construction worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F477 1F3FC 200D 2640", "html": "&#128119;&#127996;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏻‍♀", "name": "woman construction worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F477 1F3FB 200D 2640", "html": "&#128119;&#127995;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷‍♀", "name": "woman construction worker", "shortname": ":woman_construction_worker:", "unicode": "1F477 200D 2640", "html": "&#128119;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏿", "name": "construction worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F477 1F3FF", "html": "&#128119;&#127999;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏾", "name": "construction worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F477 1F3FE", "html": "&#128119;&#127998;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏽", "name": "construction worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F477 1F3FD", "html": "&#128119;&#127997;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏼", "name": "construction worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F477 1F3FC", "html": "&#128119;&#127996;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏻", "name": "construction worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F477 1F3FB", "html": "&#128119;&#127995;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷", "name": "construction worker", "shortname": ":construction_worker:", "unicode": "1f477", "html": "&#128119;", "category": "People & Body (person-role)", "order": "393"},
+ {"emoji": "🚽", "name": "toilet", "shortname": ":toilet:", "unicode": "1f6bd", "html": "&#128701;", "category": "Objects (household)", "order": "1671"},
+ {"emoji": "🐖", "name": "pig", "shortname": ":pig2:", "unicode": "1f416", "html": "&#128022;", "category": "Animals & Nature (animal-mammal)", "order": "1365"},
+ {"emoji": "❔", "name": "white question mark", "shortname": ":grey_question:", "unicode": "2754", "html": "&#10068;", "category": "Symbols (other-symbol)", "order": "2099"},
+ {"emoji": "🔰", "name": "Japanese symbol for beginner", "shortname": ":beginner:", "unicode": "1f530", "html": "&#128304;", "category": "Symbols (other-symbol)", "order": "2075"},
+ {"emoji": "🎻", "name": "violin", "shortname": ":violin:", "unicode": "1f3bb", "html": "&#127931;", "category": "Objects (musical-instrument)", "order": "1836"},
+ {"emoji": "🔛", "name": "ON! arrow", "shortname": ":on:", "unicode": "1f51b", "html": "&#128283;", "category": "Symbols (arrow)", "order": "2020"},
+ {"emoji": "💳", "name": "credit card", "shortname": ":credit_card:", "unicode": "1f4b3", "html": "&#128179;", "category": "Objects (money)", "order": "1897"},
+ {"emoji": "🆔", "name": "ID button", "shortname": ":id:", "unicode": "1f194", "html": "&#127380;", "category": "Symbols (alphanum)", "order": "2132"},
+ {"emoji": "㊙", "name": "Japanese secret button", "shortname": ":secret:", "unicode": "3299", "html": "&#12953;", "category": "Symbols (alphanum)", "order": "2156"},
+ {"emoji": "🎡", "name": "ferris wheel", "shortname": ":ferris_wheel:", "unicode": "1f3a1", "html": "&#127905;", "category": "Travel & Places (place-other)", "order": "1594"},
+ {"emoji": "🎳", "name": "bowling", "shortname": ":bowling:", "unicode": "1f3b3", "html": "&#127923;", "category": "Activities (sport)", "order": "1789"},
+ {"emoji": "♎", "name": "Libra", "shortname": ":libra:", "unicode": "264e", "html": "&#9806;", "category": "Symbols (zodiac)", "order": "2041"},
+ {"emoji": "♍", "name": "Virgo", "shortname": ":virgo:", "unicode": "264d", "html": "&#9805;", "category": "Symbols (zodiac)", "order": "2040"},
+ {"emoji": "💈", "name": "barber pole", "shortname": ":barber:", "unicode": "1f488", "html": "&#128136;", "category": "Travel & Places (place-other)", "order": "1596"},
+ {"emoji": "👛", "name": "purse", "shortname": ":purse:", "unicode": "1f45b", "html": "&#128091;", "category": "Objects (clothing)", "order": "1323"},
+ {"emoji": "🎢", "name": "roller coaster", "shortname": ":roller_coaster:", "unicode": "1f3a2", "html": "&#127906;", "category": "Travel & Places (place-other)", "order": "1595"},
+ {"emoji": "🐀", "name": "rat", "shortname": ":rat:", "unicode": "1f400", "html": "&#128000;", "category": "Animals & Nature (animal-mammal)", "order": "1377"},
+ {"emoji": "📅", "name": "calendar", "shortname": ":date:", "unicode": "1f4c5", "html": "&#128197;", "category": "Objects (office)", "order": "1925"},
+ {"emoji": "🏉", "name": "rugby football", "shortname": ":rugby_football:", "unicode": "1f3c9", "html": "&#127945;", "category": "Activities (sport)", "order": "1786"},
+ {"emoji": "🐏", "name": "ram", "shortname": ":ram:", "unicode": "1f40f", "html": "&#128015;", "category": "Animals & Nature (animal-mammal)", "order": "1368"},
+ {"emoji": "🔼", "name": "upwards button", "shortname": ":arrow_up_small:", "unicode": "1f53c", "html": "&#128316;", "category": "Symbols (av-symbol)", "order": "2058"},
+ {"emoji": "🔲", "name": "black square button", "shortname": ":black_square_button:", "unicode": "1f532", "html": "&#128306;", "category": "Symbols (geometric)", "order": "2175"},
+ {"emoji": "📴", "name": "mobile phone off", "shortname": ":mobile_phone_off:", "unicode": "1f4f4", "html": "&#128244;", "category": "Symbols (av-symbol)", "order": "2071"},
+ {"emoji": "🗼", "name": "Tokyo tower", "shortname": ":tokyo_tower:", "unicode": "1f5fc", "html": "&#128508;", "category": "Travel & Places (place-building)", "order": "1575"},
+ {"emoji": "㊗", "name": "Japanese congratulations button", "shortname": ":congratulations:", "unicode": "3297", "html": "&#12951;", "category": "Symbols (alphanum)", "order": "2155"},
+ {"emoji": "👘", "name": "kimono", "shortname": ":kimono:", "unicode": "1f458", "html": "&#128088;", "category": "Objects (clothing)", "order": "1320"},
+ {"emoji": "🇷🇺", "name": "flag: Russia", "shortname": ":ru:", "unicode": "1F1F7 1F1FA", "html": "&#127479;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "🚢", "name": "ship", "shortname": ":ship:", "unicode": "1f6a2", "html": "&#128674;", "category": "Travel & Places (transport-water)", "order": "1649"},
+ {"emoji": "🔎", "name": "magnifying glass tilted right", "shortname": ":mag_right:", "unicode": "1f50e", "html": "&#128270;", "category": "Objects (light & video)", "order": "1866"},
+ {"emoji": "🔍", "name": "magnifying glass tilted left", "shortname": ":mag:", "unicode": "1f50d", "html": "&#128269;", "category": "Objects (light & video)", "order": "1865"},
+ {"emoji": "🚒", "name": "fire engine", "shortname": ":fire_engine:", "unicode": "1f692", "html": "&#128658;", "category": "Travel & Places (transport-ground)", "order": "1619"},
+ {"emoji": "🕦", "name": "eleven-thirty", "shortname": ":clock1130:", "unicode": "1f566", "html": "&#128358;", "category": "Travel & Places (time)", "order": "1710"},
+ {"emoji": "🚓", "name": "police car", "shortname": ":police_car:", "unicode": "1f693", "html": "&#128659;", "category": "Travel & Places (transport-ground)", "order": "1620"},
+ {"emoji": "🃏", "name": "joker", "shortname": ":black_joker:", "unicode": "1f0cf", "html": "&#127183;", "category": "Activities (game)", "order": "1811"},
+ {"emoji": "🌉", "name": "bridge at night", "shortname": ":bridge_at_night:", "unicode": "1f309", "html": "&#127753;", "category": "Travel & Places (place-other)", "order": "1590"},
+ {"emoji": "📦", "name": "package", "shortname": ":package:", "unicode": "1f4e6", "html": "&#128230;", "category": "Objects (mail)", "order": "1907"},
+ {"emoji": "🚖", "name": "oncoming taxi", "shortname": ":oncoming_taxi:", "unicode": "1f696", "html": "&#128662;", "category": "Travel & Places (transport-ground)", "order": "1623"},
+ {"emoji": "📆", "name": "tear-off calendar", "shortname": ":calendar:", "unicode": "1f4c6", "html": "&#128198;", "category": "Objects (office)", "order": "1926"},
+ {"emoji": "🏇", "name": "horse racing", "shortname": ":horse_racing:", "unicode": "1f3c7", "html": "&#127943;", "category": "People & Body (person-sport)", "order": "769"},
+ {"emoji": "🐅", "name": "tiger", "shortname": ":tiger2:", "unicode": "1f405", "html": "&#128005;", "category": "Animals & Nature (animal-mammal)", "order": "1354"},
+ {"emoji": "👢", "name": "woman’s boot", "shortname": ":boot:", "unicode": "1f462", "html": "&#128098;", "category": "Objects (clothing)", "order": "1332"},
+ {"emoji": "🚑", "name": "ambulance", "shortname": ":ambulance:", "unicode": "1f691", "html": "&#128657;", "category": "Travel & Places (transport-ground)", "order": "1618"},
+ {"emoji": "🔳", "name": "white square button", "shortname": ":white_square_button:", "unicode": "1f533", "html": "&#128307;", "category": "Symbols (geometric)", "order": "2176"},
+ {"emoji": "🐗", "name": "boar", "shortname": ":boar:", "unicode": "1f417", "html": "&#128023;", "category": "Animals & Nature (animal-mammal)", "order": "1366"},
+ {"emoji": "🎒", "name": "backpack", "shortname": ":school_satchel:", "unicode": "1f392", "html": "&#127890;", "category": "Objects (clothing)", "order": "1327"},
+ {"emoji": "➿", "name": "double curly loop", "shortname": ":loop:", "unicode": "27bf", "html": "&#10175;", "category": "Symbols (other-symbol)", "order": "2091"},
+ {"emoji": "💷", "name": "pound banknote", "shortname": ":pound:", "unicode": "1f4b7", "html": "&#128183;", "category": "Objects (money)", "order": "1895"},
+ {"emoji": "ℹ", "name": "information", "shortname": ":information_source:", "unicode": "2139", "html": "&#8505;", "category": "Symbols (alphanum)", "order": "2131"},
+ {"emoji": "🐂", "name": "ox", "shortname": ":ox:", "unicode": "1f402", "html": "&#128002;", "category": "Animals & Nature (animal-mammal)", "order": "1361"},
+ {"emoji": "🍙", "name": "rice ball", "shortname": ":rice_ball:", "unicode": "1f359", "html": "&#127833;", "category": "Food & Drink (food-asian)", "order": "1497"},
+ {"emoji": "🆚", "name": "VS button", "shortname": ":vs:", "unicode": "1f19a", "html": "&#127386;", "category": "Symbols (alphanum)", "order": "2141"},
+ {"emoji": "🔚", "name": "END arrow", "shortname": ":end:", "unicode": "1f51a", "html": "&#128282;", "category": "Symbols (arrow)", "order": "2019"},
+ {"emoji": "🅿️", "name": "P button", "shortname": ":parking:", "unicode": "1f17f", "html": "&#127359;", "category": "Symbols (alphanum)", "order": "2138"},
+ {"emoji": "👡", "name": "woman’s sandal", "shortname": ":sandal:", "unicode": "1f461", "html": "&#128097;", "category": "Objects (clothing)", "order": "1331"},
+ {"emoji": "⛺", "name": "tent", "shortname": ":tent:", "unicode": "26fa", "html": "&#9978;", "category": "Travel & Places (place-other)", "order": "1583"},
+ {"emoji": "💺", "name": "seat", "shortname": ":seat:", "unicode": "1f4ba", "html": "&#128186;", "category": "Travel & Places (transport-air)", "order": "1654"},
+ {"emoji": "🚕", "name": "taxi", "shortname": ":taxi:", "unicode": "1f695", "html": "&#128661;", "category": "Travel & Places (transport-ground)", "order": "1622"},
+ {"emoji": "◾", "name": "black medium-small square", "shortname": ":black_medium_small_square:", "unicode": "25fe", "html": "&#9726;", "category": "Symbols (geometric)", "order": "2164"},
+ {"emoji": "💼", "name": "briefcase", "shortname": ":briefcase:", "unicode": "1f4bc", "html": "&#128188;", "category": "Objects (office)", "order": "1921"},
+ {"emoji": "📰", "name": "newspaper", "shortname": ":newspaper:", "unicode": "1f4f0", "html": "&#128240;", "category": "Objects (book-paper)", "order": "1886"},
+ {"emoji": "🎪", "name": "circus tent", "shortname": ":circus_tent:", "unicode": "1f3aa", "html": "&#127914;", "category": "Travel & Places (place-other)", "order": "1597"},
+ {"emoji": "🔯", "name": "dotted six-pointed star", "shortname": ":six_pointed_star:", "unicode": "1f52f", "html": "&#128303;", "category": "Symbols (religion)", "order": "2034"},
+ {"emoji": "🚹", "name": "men’s room", "shortname": ":mens:", "unicode": "1f6b9", "html": "&#128697;", "category": "Symbols (transport-sign)", "order": "1980"},
+ {"emoji": "🏰", "name": "castle", "shortname": ":european_castle:", "unicode": "1f3f0", "html": "&#127984;", "category": "Travel & Places (place-building)", "order": "1573"},
+ {"emoji": "🔦", "name": "flashlight", "shortname": ":flashlight:", "unicode": "1f526", "html": "&#128294;", "category": "Objects (light & video)", "order": "1872"},
+ {"emoji": "🌁", "name": "foggy", "shortname": ":foggy:", "unicode": "1f301", "html": "&#127745;", "category": "Travel & Places (place-other)", "order": "1584"},
+ {"emoji": "⏫", "name": "fast up button", "shortname": ":arrow_double_up:", "unicode": "23eb", "html": "&#9195;", "category": "Symbols (av-symbol)", "order": "2059"},
+ {"emoji": "🎍", "name": "pine decoration", "shortname": ":bamboo:", "unicode": "1f38d", "html": "&#127885;", "category": "Activities (event)", "order": "1765"},
+ {"emoji": "🎫", "name": "ticket", "shortname": ":ticket:", "unicode": "1f3ab", "html": "&#127915;", "category": "Activities (event)", "order": "1774"},
+ {"emoji": "🚁", "name": "helicopter", "shortname": ":helicopter:", "unicode": "1f681", "html": "&#128641;", "category": "Travel & Places (transport-air)", "order": "1655"},
+ {"emoji": "💽", "name": "computer disk", "shortname": ":minidisc:", "unicode": "1f4bd", "html": "&#128189;", "category": "Objects (computer)", "order": "1852"},
+ {"emoji": "🚍", "name": "oncoming bus", "shortname": ":oncoming_bus:", "unicode": "1f68d", "html": "&#128653;", "category": "Travel & Places (transport-ground)", "order": "1615"},
+ {"emoji": "🍈", "name": "melon", "shortname": ":melon:", "unicode": "1f348", "html": "&#127816;", "category": "Food & Drink (food-fruit)", "order": "1450"},
+ {"emoji": "▫", "name": "white small square", "shortname": ":white_small_square:", "unicode": "25ab", "html": "&#9643;", "category": "Symbols (geometric)", "order": "2160"},
+ {"emoji": "🏤", "name": "post office", "shortname": ":european_post_office:", "unicode": "1f3e4", "html": "&#127972;", "category": "Travel & Places (place-building)", "order": "1563"},
+ {"emoji": "🔟", "name": "keycap: 10", "shortname": ":keycap_ten:", "unicode": "1f51f", "html": "&#128287;", "category": "Symbols (keycap)", "order": "2118"},
+ {"emoji": "📓", "name": "notebook", "shortname": ":notebook:", "unicode": "1f4d3", "html": "&#128211;", "category": "Objects (book-paper)", "order": "1881"},
+ {"emoji": "🔕", "name": "bell with slash", "shortname": ":no_bell:", "unicode": "1f515", "html": "&#128277;", "category": "Objects (sound)", "order": "1822"},
+ {"emoji": "🍢", "name": "oden", "shortname": ":oden:", "unicode": "1f362", "html": "&#127842;", "category": "Food & Drink (food-asian)", "order": "1503"},
+ {"emoji": "🎏", "name": "carp streamer", "shortname": ":flags:", "unicode": "1f38f", "html": "&#127887;", "category": "Activities (event)", "order": "1767"},
+ {"emoji": "🎠", "name": "carousel horse", "shortname": ":carousel_horse:", "unicode": "1f3a0", "html": "&#127904;", "category": "Travel & Places (place-other)", "order": "1593"},
+ {"emoji": "🐡", "name": "blowfish", "shortname": ":blowfish:", "unicode": "1f421", "html": "&#128033;", "category": "Animals & Nature (animal-marine)", "order": "1411"},
+ {"emoji": "📈", "name": "chart increasing", "shortname": ":chart_with_upwards_trend:", "unicode": "1f4c8", "html": "&#128200;", "category": "Objects (office)", "order": "1930"},
+ {"emoji": "🍠", "name": "roasted sweet potato", "shortname": ":sweet_potato:", "unicode": "1f360", "html": "&#127840;", "category": "Food & Drink (food-asian)", "order": "1502"},
+ {"emoji": "🎿", "name": "skis", "shortname": ":ski:", "unicode": "1f3bf", "html": "&#127935;", "category": "Activities (sport)", "order": "1803"},
+ {"emoji": "🕛", "name": "twelve o’clock", "shortname": ":clock12:", "unicode": "1f55b", "html": "&#128347;", "category": "Travel & Places (time)", "order": "1687"},
+ {"emoji": "📶", "name": "antenna bars", "shortname": ":signal_strength:", "unicode": "1f4f6", "html": "&#128246;", "category": "Symbols (av-symbol)", "order": "2069"},
+ {"emoji": "🚧", "name": "construction", "shortname": ":construction:", "unicode": "1f6a7", "html": "&#128679;", "category": "Travel & Places (transport-ground)", "order": "1640"},
+ {"emoji": "#", "name": "", "shortname": "", "unicode": "", "html": "&#35;", "category": "", "order": ""},
+ {"emoji": "◼", "name": "black medium square", "shortname": ":black_medium_square:", "unicode": "25fc", "html": "&#9724;", "category": "Symbols (geometric)", "order": "2162"},
+ {"emoji": "📡", "name": "satellite antenna", "shortname": ":satellite:", "unicode": "1f4e1", "html": "&#128225;", "category": "Objects (science)", "order": "1869"},
+ {"emoji": "💶", "name": "euro banknote", "shortname": ":euro:", "unicode": "1f4b6", "html": "&#128182;", "category": "Objects (money)", "order": "1894"},
+ {"emoji": "👚", "name": "woman’s clothes", "shortname": ":womans_clothes:", "unicode": "1f45a", "html": "&#128090;", "category": "Objects (clothing)", "order": "1322"},
+ {"emoji": "📒", "name": "ledger", "shortname": ":ledger:", "unicode": "1f4d2", "html": "&#128210;", "category": "Objects (book-paper)", "order": "1882"},
+ {"emoji": "🐆", "name": "leopard", "shortname": ":leopard:", "unicode": "1f406", "html": "&#128006;", "category": "Animals & Nature (animal-mammal)", "order": "1355"},
+ {"emoji": "🔅", "name": "dim button", "shortname": ":low_brightness:", "unicode": "1f505", "html": "&#128261;", "category": "Symbols (av-symbol)", "order": "2067"},
+ {"emoji": "🕒", "name": "three o’clock", "shortname": ":clock3:", "unicode": "1f552", "html": "&#128338;", "category": "Travel & Places (time)", "order": "1693"},
+ {"emoji": "🏬", "name": "department store", "shortname": ":department_store:", "unicode": "1f3ec", "html": "&#127980;", "category": "Travel & Places (place-building)", "order": "1570"},
+ {"emoji": "🚚", "name": "delivery truck", "shortname": ":truck:", "unicode": "1f69a", "html": "&#128666;", "category": "Travel & Places (transport-ground)", "order": "1627"},
+ {"emoji": "🍶", "name": "sake", "shortname": ":sake:", "unicode": "1f376", "html": "&#127862;", "category": "Food & Drink (drink)", "order": "1524"},
+ {"emoji": "🚃", "name": "railway car", "shortname": ":railway_car:", "unicode": "1f683", "html": "&#128643;", "category": "Travel & Places (transport-ground)", "order": "1603"},
+ {"emoji": "🚤", "name": "speedboat", "shortname": ":speedboat:", "unicode": "1f6a4", "html": "&#128676;", "category": "Travel & Places (transport-water)", "order": "1645"},
+ {"emoji": "🇰🇷", "name": "flag: South Korea", "shortname": ":ko:", "unicode": "1F1F0 1F1F7", "html": "&#127472;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "📼", "name": "videocassette", "shortname": ":vhs:", "unicode": "1f4fc", "html": "&#128252;", "category": "Objects (light & video)", "order": "1864"},
+ {"emoji": "🕐", "name": "one o’clock", "shortname": ":clock1:", "unicode": "1f550", "html": "&#128336;", "category": "Travel & Places (time)", "order": "1689"},
+ {"emoji": "⏬", "name": "fast down button", "shortname": ":arrow_double_down:", "unicode": "23ec", "html": "&#9196;", "category": "Symbols (av-symbol)", "order": "2061"},
+ {"emoji": "🐃", "name": "water buffalo", "shortname": ":water_buffalo:", "unicode": "1f403", "html": "&#128003;", "category": "Animals & Nature (animal-mammal)", "order": "1362"},
+ {"emoji": "🔽", "name": "downwards button", "shortname": ":arrow_down_small:", "unicode": "1f53d", "html": "&#128317;", "category": "Symbols (av-symbol)", "order": "2060"},
+ {"emoji": "💴", "name": "yen banknote", "shortname": ":yen:", "unicode": "1f4b4", "html": "&#128180;", "category": "Objects (money)", "order": "1892"},
+ {"emoji": "🔇", "name": "muted speaker", "shortname": ":mute:", "unicode": "1f507", "html": "&#128263;", "category": "Objects (sound)", "order": "1814"},
+ {"emoji": "🎽", "name": "running shirt", "shortname": ":running_shirt_with_sash:", "unicode": "1f3bd", "html": "&#127933;", "category": "Activities (sport)", "order": "1802"},
+ {"emoji": "⬜", "name": "white large square", "shortname": ":white_large_square:", "unicode": "2b1c", "html": "&#11036;", "category": "Symbols (geometric)", "order": "2166"},
+ {"emoji": "♿", "name": "wheelchair symbol", "shortname": ":wheelchair:", "unicode": "267f", "html": "&#9855;", "category": "Symbols (transport-sign)", "order": "1979"},
+ {"emoji": "🕑", "name": "two o’clock", "shortname": ":clock2:", "unicode": "1f551", "html": "&#128337;", "category": "Travel & Places (time)", "order": "1691"},
+ {"emoji": "📎", "name": "paperclip", "shortname": ":paperclip:", "unicode": "1f4ce", "html": "&#128206;", "category": "Objects (office)", "order": "1936"},
+ {"emoji": "🏧", "name": "ATM sign", "shortname": ":atm:", "unicode": "1f3e7", "html": "&#127975;", "category": "Symbols (transport-sign)", "order": "1976"},
+ {"emoji": "🎦", "name": "cinema", "shortname": ":cinema:", "unicode": "1f3a6", "html": "&#127910;", "category": "Symbols (av-symbol)", "order": "2066"},
+ {"emoji": "🔭", "name": "telescope", "shortname": ":telescope:", "unicode": "1f52d", "html": "&#128301;", "category": "Objects (science)", "order": "1868"},
+ {"emoji": "🎑", "name": "moon viewing ceremony", "shortname": ":rice_scene:", "unicode": "1f391", "html": "&#127889;", "category": "Activities (event)", "order": "1769"},
+ {"emoji": "📘", "name": "blue book", "shortname": ":blue_book:", "unicode": "1f4d8", "html": "&#128216;", "category": "Objects (book-paper)", "order": "1878"},
+ {"emoji": "◻️", "name": "white medium square", "shortname": ":white_medium_square:", "unicode": "25fb", "html": "&#9723;", "category": "Symbols (geometric)", "order": "2161"},
+ {"emoji": "📮", "name": "postbox", "shortname": ":postbox:", "unicode": "1f4ee", "html": "&#128238;", "category": "Objects (mail)", "order": "1912"},
+ {"emoji": "📧", "name": "e-mail", "shortname": ":e-mail:", "unicode": "1f4e7", "html": "&#128231;", "category": "Objects (mail)", "order": "1902"},
+ {"emoji": "🐁", "name": "mouse", "shortname": ":mouse2:", "unicode": "1f401", "html": "&#128001;", "category": "Animals & Nature (animal-mammal)", "order": "1376"},
+ {"emoji": "🚄", "name": "high-speed train", "shortname": ":bullettrain_side:", "unicode": "1f684", "html": "&#128644;", "category": "Travel & Places (transport-ground)", "order": "1604"},
+ {"emoji": "🉐", "name": "Japanese bargain button", "shortname": ":ideograph_advantage:", "unicode": "1f250", "html": "&#127568;", "category": "Symbols (alphanum)", "order": "2147"},
+ {"emoji": "🔩", "name": "nut and bolt", "shortname": ":nut_and_bolt:", "unicode": "1f529", "html": "&#128297;", "category": "Objects (tool)", "order": "1960"},
+ {"emoji": "🆖", "name": "NG button", "shortname": ":ng:", "unicode": "1f196", "html": "&#127382;", "category": "Symbols (alphanum)", "order": "2135"},
+ {"emoji": "🏨", "name": "hotel", "shortname": ":hotel:", "unicode": "1f3e8", "html": "&#127976;", "category": "Travel & Places (place-building)", "order": "1566"},
+ {"emoji": "🚾", "name": "water closet", "shortname": ":wc:", "unicode": "1f6be", "html": "&#128702;", "category": "Symbols (transport-sign)", "order": "1984"},
+ {"emoji": "🏮", "name": "red paper lantern", "shortname": ":izakaya_lantern:", "unicode": "1f3ee", "html": "&#127982;", "category": "Objects (light & video)", "order": "1873"},
+ {"emoji": "🔂", "name": "repeat single button", "shortname": ":repeat_one:", "unicode": "1f502", "html": "&#128258;", "category": "Symbols (av-symbol)", "order": "2050"},
+ {"emoji": "📬", "name": "open mailbox with raised flag", "shortname": ":mailbox_with_mail:", "unicode": "1f4ec", "html": "&#128236;", "category": "Objects (mail)", "order": "1910"},
+ {"emoji": "📉", "name": "chart decreasing", "shortname": ":chart_with_downwards_trend:", "unicode": "1f4c9", "html": "&#128201;", "category": "Objects (office)", "order": "1931"},
+ {"emoji": "📗", "name": "green book", "shortname": ":green_book:", "unicode": "1f4d7", "html": "&#128215;", "category": "Objects (book-paper)", "order": "1877"},
+ {"emoji": "🚜", "name": "tractor", "shortname": ":tractor:", "unicode": "1f69c", "html": "&#128668;", "category": "Travel & Places (transport-ground)", "order": "1629"},
+ {"emoji": "⛲", "name": "fountain", "shortname": ":fountain:", "unicode": "26f2", "html": "&#9970;", "category": "Travel & Places (place-other)", "order": "1582"},
+ {"emoji": "🚇", "name": "metro", "shortname": ":metro:", "unicode": "1f687", "html": "&#128647;", "category": "Travel & Places (transport-ground)", "order": "1607"},
+ {"emoji": "📋", "name": "clipboard", "shortname": ":clipboard:", "unicode": "1f4cb", "html": "&#128203;", "category": "Objects (office)", "order": "1933"},
+ {"emoji": "📵", "name": "no mobile phones", "shortname": ":no_mobile_phones:", "unicode": "1f4f5", "html": "&#128245;", "category": "Symbols (warning)", "order": "1998"},
+ {"emoji": "🕓", "name": "four o’clock", "shortname": ":clock4:", "unicode": "1f553", "html": "&#128339;", "category": "Travel & Places (time)", "order": "1695"},
+ {"emoji": "🚭", "name": "no smoking", "shortname": ":no_smoking:", "unicode": "1f6ad", "html": "&#128685;", "category": "Symbols (warning)", "order": "1994"},
+ {"emoji": "⬛", "name": "black large square", "shortname": ":black_large_square:", "unicode": "2b1b", "html": "&#11035;", "category": "Symbols (geometric)", "order": "2165"},
+ {"emoji": "🎰", "name": "slot machine", "shortname": ":slot_machine:", "unicode": "1f3b0", "html": "&#127920;", "category": "Activities (game)", "order": "1601"},
+ {"emoji": "🕔", "name": "five o’clock", "shortname": ":clock5:", "unicode": "1f554", "html": "&#128340;", "category": "Travel & Places (time)", "order": "1697"},
+ {"emoji": "🛁", "name": "bathtub", "shortname": ":bathtub:", "unicode": "1f6c1", "html": "&#128705;", "category": "Objects (household)", "order": "1679"},
+ {"emoji": "📜", "name": "scroll", "shortname": ":scroll:", "unicode": "1f4dc", "html": "&#128220;", "category": "Objects (book-paper)", "order": "1884"},
+ {"emoji": "🚉", "name": "station", "shortname": ":station:", "unicode": "1f689", "html": "&#128649;", "category": "Travel & Places (transport-ground)", "order": "1609"},
+ {"emoji": "🍘", "name": "rice cracker", "shortname": ":rice_cracker:", "unicode": "1f358", "html": "&#127832;", "category": "Food & Drink (food-asian)", "order": "1496"},
+ {"emoji": "🏦", "name": "bank", "shortname": ":bank:", "unicode": "1f3e6", "html": "&#127974;", "category": "Travel & Places (place-building)", "order": "1565"},
+ {"emoji": "🔧", "name": "wrench", "shortname": ":wrench:", "unicode": "1f527", "html": "&#128295;", "category": "Objects (tool)", "order": "1959"},
+ {"emoji": "🈯️", "name": "", "shortname": ":u6307:", "unicode": "1f22f", "html": "&#127535;", "category": "", "order": "2146"},
+ {"emoji": "🚛", "name": "articulated lorry", "shortname": ":articulated_lorry:", "unicode": "1f69b", "html": "&#128667;", "category": "Travel & Places (transport-ground)", "order": "1628"},
+ {"emoji": "📄", "name": "page facing up", "shortname": ":page_facing_up:", "unicode": "1f4c4", "html": "&#128196;", "category": "Objects (book-paper)", "order": "1885"},
+ {"emoji": "⛎", "name": "Ophiuchus", "shortname": ":ophiuchus:", "unicode": "26ce", "html": "&#9934;", "category": "Symbols (zodiac)", "order": "2047"},
+ {"emoji": "📊", "name": "bar chart", "shortname": ":bar_chart:", "unicode": "1f4ca", "html": "&#128202;", "category": "Objects (office)", "order": "1932"},
+ {"emoji": "🚷", "name": "no pedestrians", "shortname": ":no_pedestrians:", "unicode": "1f6b7", "html": "&#128695;", "category": "Symbols (warning)", "order": "1997"},
+ {"emoji": "🇨🇳", "name": "flag: China", "shortname": ":cn:", "unicode": "1F1E8 1F1F3", "html": "&#127464;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "📳", "name": "vibration mode", "shortname": ":vibration_mode:", "unicode": "1f4f3", "html": "&#128243;", "category": "Symbols (av-symbol)", "order": "2070"},
+ {"emoji": "🕙", "name": "ten o’clock", "shortname": ":clock10:", "unicode": "1f559", "html": "&#128345;", "category": "Travel & Places (time)", "order": "1707"},
+ {"emoji": "🕘", "name": "nine o’clock", "shortname": ":clock9:", "unicode": "1f558", "html": "&#128344;", "category": "Travel & Places (time)", "order": "1705"},
+ {"emoji": "🚅", "name": "bullet train", "shortname": ":bullettrain_front:", "unicode": "1f685", "html": "&#128645;", "category": "Travel & Places (transport-ground)", "order": "1605"},
+ {"emoji": "🚐", "name": "minibus", "shortname": ":minibus:", "unicode": "1f690", "html": "&#128656;", "category": "Travel & Places (transport-ground)", "order": "1617"},
+ {"emoji": "🚊", "name": "tram", "shortname": ":tram:", "unicode": "1f68a", "html": "&#128650;", "category": "Travel & Places (transport-ground)", "order": "1610"},
+ {"emoji": "🕗", "name": "eight o’clock", "shortname": ":clock8:", "unicode": "1f557", "html": "&#128343;", "category": "Travel & Places (time)", "order": "1703"},
+ {"emoji": "🈳", "name": "Japanese vacancy button", "shortname": ":u7a7a:", "unicode": "1f233", "html": "&#127539;", "category": "Symbols (alphanum)", "order": "2154"},
+ {"emoji": "🚥", "name": "horizontal traffic light", "shortname": ":traffic_light:", "unicode": "1f6a5", "html": "&#128677;", "category": "Travel & Places (transport-ground)", "order": "1638"},
+ {"emoji": "🚵🏿‍♀", "name": "woman mountain biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B5 1F3FF 200D 2640", "html": "&#128693;&#127999;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏾‍♀", "name": "woman mountain biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B5 1F3FE 200D 2640", "html": "&#128693;&#127998;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏽‍♀", "name": "woman mountain biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B5 1F3FD 200D 2640", "html": "&#128693;&#127997;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏼‍♀", "name": "woman mountain biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B5 1F3FC 200D 2640", "html": "&#128693;&#127996;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏻‍♀", "name": "woman mountain biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B5 1F3FB 200D 2640", "html": "&#128693;&#127995;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵‍♀", "name": "woman mountain biking", "shortname": ":woman_mountain_biking:", "unicode": "1F6B5 200D 2640", "html": "&#128693;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏿", "name": "person mountain biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B5 1F3FF", "html": "&#128693;&#127999;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏾", "name": "person mountain biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B5 1F3FE", "html": "&#128693;&#127998;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏽", "name": "person mountain biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B5 1F3FD", "html": "&#128693;&#127997;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏼", "name": "person mountain biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B5 1F3FC", "html": "&#128693;&#127996;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏻", "name": "person mountain biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B5 1F3FB", "html": "&#128693;&#127995;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵", "name": "person mountain biking", "shortname": ":mountain_bicyclist:", "unicode": "1f6b5", "html": "&#128693;", "category": "People & Body (person-sport)", "order": "908"},
+ {"emoji": "🔬", "name": "microscope", "shortname": ":microscope:", "unicode": "1f52c", "html": "&#128300;", "category": "Objects (science)", "order": "1867"},
+ {"emoji": "🏯", "name": "Japanese castle", "shortname": ":japanese_castle:", "unicode": "1f3ef", "html": "&#127983;", "category": "Travel & Places (place-building)", "order": "1572"},
+ {"emoji": "🔖", "name": "bookmark", "shortname": ":bookmark:", "unicode": "1f516", "html": "&#128278;", "category": "Objects (book-paper)", "order": "1889"},
+ {"emoji": "📑", "name": "bookmark tabs", "shortname": ":bookmark_tabs:", "unicode": "1f4d1", "html": "&#128209;", "category": "Objects (book-paper)", "order": "1888"},
+ {"emoji": "👝", "name": "clutch bag", "shortname": ":pouch:", "unicode": "1f45d", "html": "&#128093;", "category": "Objects (clothing)", "order": "1325"},
+ {"emoji": "🆎", "name": "AB button (blood type)", "shortname": ":ab:", "unicode": "1f18e", "html": "&#127374;", "category": "Symbols (alphanum)", "order": "2126"},
+ {"emoji": "📃", "name": "page with curl", "shortname": ":page_with_curl:", "unicode": "1f4c3", "html": "&#128195;", "category": "Objects (book-paper)", "order": "1883"},
+ {"emoji": "🎴", "name": "flower playing cards", "shortname": ":flower_playing_cards:", "unicode": "1f3b4", "html": "&#127924;", "category": "Activities (game)", "order": "1813"},
+ {"emoji": "🕚", "name": "eleven o’clock", "shortname": ":clock11:", "unicode": "1f55a", "html": "&#128346;", "category": "Travel & Places (time)", "order": "1709"},
+ {"emoji": "📠", "name": "fax machine", "shortname": ":fax:", "unicode": "1f4e0", "html": "&#128224;", "category": "Objects (phone)", "order": "1843"},
+ {"emoji": "🕖", "name": "seven o’clock", "shortname": ":clock7:", "unicode": "1f556", "html": "&#128342;", "category": "Travel & Places (time)", "order": "1701"},
+ {"emoji": "◽", "name": "white medium-small square", "shortname": ":white_medium_small_square:", "unicode": "25fd", "html": "&#9725;", "category": "Symbols (geometric)", "order": "2163"},
+ {"emoji": "💱", "name": "currency exchange", "shortname": ":currency_exchange:", "unicode": "1f4b1", "html": "&#128177;", "category": "Objects (money)", "order": "1899"},
+ {"emoji": "🔉", "name": "speaker medium volume", "shortname": ":sound:", "unicode": "1f509", "html": "&#128265;", "category": "Objects (sound)", "order": "1816"},
+ {"emoji": "💹", "name": "chart increasing with yen", "shortname": ":chart:", "unicode": "1f4b9", "html": "&#128185;", "category": "Objects (money)", "order": "1898"},
+ {"emoji": "🆑", "name": "CL button", "shortname": ":cl:", "unicode": "1f191", "html": "&#127377;", "category": "Symbols (alphanum)", "order": "2128"},
+ {"emoji": "💾", "name": "floppy disk", "shortname": ":floppy_disk:", "unicode": "1f4be", "html": "&#128190;", "category": "Objects (computer)", "order": "1853"},
+ {"emoji": "🏣", "name": "Japanese post office", "shortname": ":post_office:", "unicode": "1f3e3", "html": "&#127971;", "category": "Travel & Places (place-building)", "order": "1562"},
+ {"emoji": "🔈", "name": "speaker low volume", "shortname": ":speaker:", "unicode": "1f508", "html": "&#128264;", "category": "Objects (sound)", "order": "1815"},
+ {"emoji": "🗾", "name": "map of Japan", "shortname": ":japan:", "unicode": "1f5fe", "html": "&#128510;", "category": "Travel & Places (place-map)", "order": "1543"},
+ {"emoji": "🈺", "name": "Japanese open for business button", "shortname": ":u55b6:", "unicode": "1f23a", "html": "&#127546;", "category": "Symbols (alphanum)", "order": "2157"},
+ {"emoji": "🀄", "name": "mahjong red dragon", "shortname": ":mahjong:", "unicode": "1f004", "html": "&#126980;", "category": "Activities (game)", "order": "1812"},
+ {"emoji": "📨", "name": "incoming envelope", "shortname": ":incoming_envelope:", "unicode": "1f4e8", "html": "&#128232;", "category": "Objects (mail)", "order": "1903"},
+ {"emoji": "📙", "name": "orange book", "shortname": ":orange_book:", "unicode": "1f4d9", "html": "&#128217;", "category": "Objects (book-paper)", "order": "1879"},
+ {"emoji": "🚻", "name": "restroom", "shortname": ":restroom:", "unicode": "1f6bb", "html": "&#128699;", "category": "Symbols (transport-sign)", "order": "1982"},
+ {"emoji": "🈚️", "name": "", "shortname": ":u7121:", "unicode": "1f21a", "html": "&#127514;", "category": "", "order": "2149"},
+ {"emoji": "🈶", "name": "Japanese not free of charge button", "shortname": ":u6709:", "unicode": "1f236", "html": "&#127542;", "category": "Symbols (alphanum)", "order": "2145"},
+ {"emoji": "📐", "name": "triangular ruler", "shortname": ":triangular_ruler:", "unicode": "1f4d0", "html": "&#128208;", "category": "Objects (office)", "order": "1939"},
+ {"emoji": "🚋", "name": "tram car", "shortname": ":train:", "unicode": "1f68b", "html": "&#128651;", "category": "Travel & Places (transport-ground)", "order": "1613"},
+ {"emoji": "🈸", "name": "Japanese application button", "shortname": ":u7533:", "unicode": "1f238", "html": "&#127544;", "category": "Symbols (alphanum)", "order": "2152"},
+ {"emoji": "🚎", "name": "trolleybus", "shortname": ":trolleybus:", "unicode": "1f68e", "html": "&#128654;", "category": "Travel & Places (transport-ground)", "order": "1616"},
+ {"emoji": "🈷", "name": "Japanese monthly amount button", "shortname": ":u6708:", "unicode": "1f237", "html": "&#127543;", "category": "Symbols (alphanum)", "order": "2144"},
+ {"emoji": "🔢", "name": "input numbers", "shortname": ":input_numbers:", "unicode": "1F522", "html": "&#128290;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "📔", "name": "notebook with decorative cover", "shortname": ":notebook_with_decorative_cover:", "unicode": "1f4d4", "html": "&#128212;", "category": "Objects (book-paper)", "order": "1874"},
+ {"emoji": "🈲", "name": "Japanese prohibited button", "shortname": ":u7981:", "unicode": "1f232", "html": "&#127538;", "category": "Symbols (alphanum)", "order": "2150"},
+ {"emoji": "🈵", "name": "Japanese no vacancy button", "shortname": ":u6e80:", "unicode": "1f235", "html": "&#127541;", "category": "Symbols (alphanum)", "order": "2158"},
+ {"emoji": "📯", "name": "postal horn", "shortname": ":postal_horn:", "unicode": "1f4ef", "html": "&#128239;", "category": "Objects (sound)", "order": "1820"},
+ {"emoji": "🏭", "name": "factory", "shortname": ":factory:", "unicode": "1f3ed", "html": "&#127981;", "category": "Travel & Places (place-building)", "order": "1571"},
+ {"emoji": "🚸", "name": "children crossing", "shortname": ":children_crossing:", "unicode": "1f6b8", "html": "&#128696;", "category": "Symbols (warning)", "order": "1990"},
+ {"emoji": "🚆", "name": "train", "shortname": ":train2:", "unicode": "1f686", "html": "&#128646;", "category": "Travel & Places (transport-ground)", "order": "1606"},
+ {"emoji": "📏", "name": "straight ruler", "shortname": ":straight_ruler:", "unicode": "1f4cf", "html": "&#128207;", "category": "Objects (office)", "order": "1938"},
+ {"emoji": "📟", "name": "pager", "shortname": ":pager:", "unicode": "1f4df", "html": "&#128223;", "category": "Objects (phone)", "order": "1842"},
+ {"emoji": "🉑", "name": "Japanese acceptable button", "shortname": ":accept:", "unicode": "1f251", "html": "&#127569;", "category": "Symbols (alphanum)", "order": "2151"},
+ {"emoji": "🈴", "name": "Japanese passing grade button", "shortname": ":u5408:", "unicode": "1f234", "html": "&#127540;", "category": "Symbols (alphanum)", "order": "2153"},
+ {"emoji": "🔏", "name": "locked with pen", "shortname": ":lock_with_ink_pen:", "unicode": "1f50f", "html": "&#128271;", "category": "Objects (lock)", "order": "1946"},
+ {"emoji": "🕜", "name": "one-thirty", "shortname": ":clock130:", "unicode": "1f55c", "html": "&#128348;", "category": "Travel & Places (time)", "order": "1690"},
+ {"emoji": "🈂️", "name": "Japanese service charge button", "shortname": ":sa:", "unicode": "1f202", "html": "&#127490;", "category": "Symbols (alphanum)", "order": "2143"},
+ {"emoji": "📤", "name": "outbox tray", "shortname": ":outbox_tray:", "unicode": "1f4e4", "html": "&#128228;", "category": "Objects (mail)", "order": "1905"},
+ {"emoji": "🔀", "name": "shuffle tracks button", "shortname": ":twisted_rightwards_arrows:", "unicode": "1f500", "html": "&#128256;", "category": "Symbols (av-symbol)", "order": "2048"},
+ {"emoji": "📫", "name": "closed mailbox with raised flag", "shortname": ":mailbox:", "unicode": "1f4eb", "html": "&#128235;", "category": "Objects (mail)", "order": "1908"},
+ {"emoji": "🚈", "name": "light rail", "shortname": ":light_rail:", "unicode": "1f688", "html": "&#128648;", "category": "Travel & Places (transport-ground)", "order": "1608"},
+ {"emoji": "🕤", "name": "nine-thirty", "shortname": ":clock930:", "unicode": "1f564", "html": "&#128356;", "category": "Travel & Places (time)", "order": "1706"},
+ {"emoji": "🚏", "name": "bus stop", "shortname": ":busstop:", "unicode": "1f68f", "html": "&#128655;", "category": "Travel & Places (transport-ground)", "order": "1633"},
+ {"emoji": "📂", "name": "open file folder", "shortname": ":open_file_folder:", "unicode": "1f4c2", "html": "&#128194;", "category": "Objects (office)", "order": "1923"},
+ {"emoji": "📁", "name": "file folder", "shortname": ":file_folder:", "unicode": "1f4c1", "html": "&#128193;", "category": "Objects (office)", "order": "1922"},
+ {"emoji": "🚰", "name": "potable water", "shortname": ":potable_water:", "unicode": "1f6b0", "html": "&#128688;", "category": "Symbols (transport-sign)", "order": "1978"},
+ {"emoji": "📇", "name": "card index", "shortname": ":card_index:", "unicode": "1f4c7", "html": "&#128199;", "category": "Objects (office)", "order": "1929"},
+ {"emoji": "🕝", "name": "two-thirty", "shortname": ":clock230:", "unicode": "1f55d", "html": "&#128349;", "category": "Travel & Places (time)", "order": "1692"},
+ {"emoji": "🚝", "name": "monorail", "shortname": ":monorail:", "unicode": "1f69d", "html": "&#128669;", "category": "Travel & Places (transport-ground)", "order": "1611"},
+ {"emoji": "🕧", "name": "twelve-thirty", "shortname": ":clock1230:", "unicode": "1f567", "html": "&#128359;", "category": "Travel & Places (time)", "order": "1688"},
+ {"emoji": "🕥", "name": "ten-thirty", "shortname": ":clock1030:", "unicode": "1f565", "html": "&#128357;", "category": "Travel & Places (time)", "order": "1708"},
+ {"emoji": "🔤", "name": "input latin letters", "shortname": ":abc:", "unicode": "1f524", "html": "&#128292;", "category": "Symbols (alphanum)", "order": "2124"},
+ {"emoji": "📪", "name": "closed mailbox with lowered flag", "shortname": ":mailbox_closed:", "unicode": "1f4ea", "html": "&#128234;", "category": "Objects (mail)", "order": "1909"},
+ {"emoji": "🕟", "name": "four-thirty", "shortname": ":clock430:", "unicode": "1f55f", "html": "&#128351;", "category": "Travel & Places (time)", "order": "1696"},
+ {"emoji": "🚞", "name": "mountain railway", "shortname": ":mountain_railway:", "unicode": "1f69e", "html": "&#128670;", "category": "Travel & Places (transport-ground)", "order": "1612"},
+ {"emoji": "🚯", "name": "no littering", "shortname": ":do_not_litter:", "unicode": "1f6af", "html": "&#128687;", "category": "Symbols (warning)", "order": "1995"},
+ {"emoji": "🕞", "name": "three-thirty", "shortname": ":clock330:", "unicode": "1f55e", "html": "&#128350;", "category": "Travel & Places (time)", "order": "1694"},
+ {"emoji": "➗", "name": "division sign", "shortname": ":heavy_division_sign:", "unicode": "2797", "html": "&#10135;", "category": "Symbols (other-symbol)", "order": "2089"},
+ {"emoji": "🕢", "name": "seven-thirty", "shortname": ":clock730:", "unicode": "1f562", "html": "&#128354;", "category": "Travel & Places (time)", "order": "1702"},
+ {"emoji": "🕠", "name": "five-thirty", "shortname": ":clock530:", "unicode": "1f560", "html": "&#128352;", "category": "Travel & Places (time)", "order": "1698"},
+ {"emoji": "🔠", "name": "input latin uppercase", "shortname": ":capital_abcd:", "unicode": "1f520", "html": "&#128288;", "category": "Symbols (alphanum)", "order": "2120"},
+ {"emoji": "📭", "name": "open mailbox with lowered flag", "shortname": ":mailbox_with_no_mail:", "unicode": "1f4ed", "html": "&#128237;", "category": "Objects (mail)", "order": "1911"},
+ {"emoji": "🔣", "name": "input symbols", "shortname": ":symbols:", "unicode": "1f523", "html": "&#128291;", "category": "Symbols (alphanum)", "order": "2123"},
+ {"emoji": "🚡", "name": "aerial tramway", "shortname": ":aerial_tramway:", "unicode": "1f6a1", "html": "&#128673;", "category": "Travel & Places (transport-air)", "order": "1658"},
+ {"emoji": "🕣", "name": "eight-thirty", "shortname": ":clock830:", "unicode": "1f563", "html": "&#128355;", "category": "Travel & Places (time)", "order": "1704"},
+ {"emoji": "🕡", "name": "six-thirty", "shortname": ":clock630:", "unicode": "1f561", "html": "&#128353;", "category": "Travel & Places (time)", "order": "1700"},
+ {"emoji": "🔡", "name": "input latin lowercase", "shortname": ":abcd:", "unicode": "1f521", "html": "&#128289;", "category": "Symbols (alphanum)", "order": "2121"},
+ {"emoji": "🚠", "name": "mountain cableway", "shortname": ":mountain_cableway:", "unicode": "1f6a0", "html": "&#128672;", "category": "Travel & Places (transport-air)", "order": "1657"},
+ {"emoji": "🈁", "name": "Japanese here button", "shortname": ":koko:", "unicode": "1f201", "html": "&#127489;", "category": "Symbols (alphanum)", "order": "2142"},
+ {"emoji": "🛂", "name": "passport control", "shortname": ":passport_control:", "unicode": "1f6c2", "html": "&#128706;", "category": "Symbols (transport-sign)", "order": "1985"},
+ {"emoji": "🚱", "name": "non-potable water", "shortname": ":non-potable_water:", "unicode": "1f6b1", "html": "&#128689;", "category": "Symbols (warning)", "order": "1996"},
+ {"emoji": "🚟", "name": "suspension railway", "shortname": ":suspension_railway:", "unicode": "1f69f", "html": "&#128671;", "category": "Travel & Places (transport-air)", "order": "1656"},
+ {"emoji": "🛄", "name": "baggage claim", "shortname": ":baggage_claim:", "unicode": "1f6c4", "html": "&#128708;", "category": "Symbols (transport-sign)", "order": "1987"},
+ {"emoji": "🚳", "name": "no bicycles", "shortname": ":no_bicycles:", "unicode": "1f6b3", "html": "&#128691;", "category": "Symbols (warning)", "order": "1993"},
+ {"emoji": "🏳‍🌈", "name": "rainbow flag", "shortname": ":rainbow_flag:", "unicode": "1F3F3 200D 1F308", "html": "&#127987;&zwj;&#127752;", "category": "Flags (flag)", "order": ""},
+ {"emoji": "🕵🏿‍♀", "name": "woman detective: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F575 1F3FF 200D 2640", "html": "&#128373;&#127999;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏾‍♀", "name": "woman detective: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F575 1F3FE 200D 2640", "html": "&#128373;&#127998;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏽‍♀", "name": "woman detective: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F575 1F3FD 200D 2640", "html": "&#128373;&#127997;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏼‍♀", "name": "woman detective: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F575 1F3FC 200D 2640", "html": "&#128373;&#127996;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏻‍♀", "name": "woman detective: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F575 1F3FB 200D 2640", "html": "&#128373;&#127995;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵‍♀", "name": "woman detective", "shortname": ":woman_detective:", "unicode": "1F575 200D 2640", "html": "&#128373;&zwj;&#9792;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏿", "name": "detective: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F575 1F3FF", "html": "&#128373;&#127999;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏾", "name": "detective: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F575 1F3FE", "html": "&#128373;&#127998;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏽", "name": "detective: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F575 1F3FD", "html": "&#128373;&#127997;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏼", "name": "detective: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F575 1F3FC", "html": "&#128373;&#127996;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏻", "name": "detective: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F575 1F3FB", "html": "&#128373;&#127995;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵", "name": "detective", "shortname": ":detective:", "unicode": "1F575", "html": "&#128373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "☹", "name": "frowning face", "shortname": ":frowning_face:", "unicode": "2639", "html": "&#9785;", "category": "Smileys & Emotion (face-concerned)", "order": ""},
+ {"emoji": "☠", "name": "skull and crossbones", "shortname": ":skull_crossbones:", "unicode": "2620", "html": "&#9760;", "category": "Smileys & Emotion (face-negative)", "order": "81"},
+ {"emoji": "🤗", "name": "hugging face", "shortname": ":hugging:", "unicode": "1f917", "html": "&#129303;", "category": "Smileys & Emotion (face-hand)", "order": "20"},
+ {"emoji": "🤖", "name": "robot", "shortname": ":robot:", "unicode": "1F916", "html": "&#129302;", "category": "Smileys & Emotion (face-costume)", "order": ""},
+ {"emoji": "🤕", "name": "face with head-bandage", "shortname": ":face_with_headbandage:", "unicode": "1F915", "html": "&#129301;", "category": "Smileys & Emotion (face-unwell)", "order": ""},
+ {"emoji": "🤔", "name": "thinking face", "shortname": ":thinking:", "unicode": "1f914", "html": "&#129300;", "category": "Smileys & Emotion (face-hand)", "order": "21"},
+ {"emoji": "🤓", "name": "nerd face", "shortname": ":nerd:", "unicode": "1f913", "html": "&#129299;", "category": "Smileys & Emotion (face-glasses)", "order": "36"},
+ {"emoji": "🤒", "name": "face with thermometer", "shortname": ":face_with_thermometer:", "unicode": "1F912", "html": "&#129298;", "category": "Smileys & Emotion (face-unwell)", "order": ""},
+ {"emoji": "🤑", "name": "money-mouth face", "shortname": ":moneymouth_face:", "unicode": "1F911", "html": "&#129297;", "category": "Smileys & Emotion (face-tongue)", "order": ""},
+ {"emoji": "🤐", "name": "zipper-mouth face", "shortname": ":zipper_mouth:", "unicode": "1f910", "html": "&#129296;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": "30"},
+ {"emoji": "🙄", "name": "face with rolling eyes", "shortname": ":rolling_eyes:", "unicode": "1f644", "html": "&#128580;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": "25"},
+ {"emoji": "🙃", "name": "upside-down face", "shortname": ":upside_down:", "unicode": "1f643", "html": "&#128579;", "category": "Smileys & Emotion (face-smiling)", "order": "45"},
+ {"emoji": "🙂", "name": "slightly smiling face", "shortname": ":slight_smile:", "unicode": "1f642", "html": "&#128578;", "category": "Smileys & Emotion (face-smiling)", "order": "19"},
+ {"emoji": "🙁", "name": "slightly frowning face", "shortname": ":slightly_frowning_face:", "unicode": "1F641", "html": "&#128577;", "category": "Smileys & Emotion (face-concerned)", "order": ""},
+ {"emoji": "🤘🏿", "name": "sign of the horns: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F918 1F3FF", "html": "&#129304;&#127999;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤘🏾", "name": "sign of the horns: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F918 1F3FE", "html": "&#129304;&#127998;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤘🏽", "name": "sign of the horns: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F918 1F3FD", "html": "&#129304;&#127997;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤘🏼", "name": "sign of the horns: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F918 1F3FC", "html": "&#129304;&#127996;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤘🏻", "name": "sign of the horns: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F918 1F3FB", "html": "&#129304;&#127995;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤘", "name": "sign of the horns", "shortname": ":sign_of_the_horns:", "unicode": "1F918", "html": "&#129304;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🖖🏿", "name": "vulcan salute: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F596 1F3FF", "html": "&#128406;&#127999;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖖🏾", "name": "vulcan salute: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F596 1F3FE", "html": "&#128406;&#127998;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖖🏽", "name": "vulcan salute: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F596 1F3FD", "html": "&#128406;&#127997;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖖🏼", "name": "vulcan salute: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F596 1F3FC", "html": "&#128406;&#127996;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖖🏻", "name": "vulcan salute: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F596 1F3FB", "html": "&#128406;&#127995;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖖", "name": "vulcan salute", "shortname": ":vulcan_salute:", "unicode": "1F596", "html": "&#128406;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖕🏿", "name": "middle finger: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F595 1F3FF", "html": "&#128405;&#127999;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "🖕🏾", "name": "middle finger: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F595 1F3FE", "html": "&#128405;&#127998;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "🖕🏽", "name": "middle finger: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F595 1F3FD", "html": "&#128405;&#127997;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "🖕🏼", "name": "middle finger: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F595 1F3FC", "html": "&#128405;&#127996;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "🖕🏻", "name": "middle finger: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F595 1F3FB", "html": "&#128405;&#127995;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "🖕", "name": "middle finger", "shortname": ":middle_finger:", "unicode": "1f595", "html": "&#128405;", "category": "People & Body (hand-single-finger)", "order": "1116"},
+ {"emoji": "🖐🏿", "name": "hand with fingers splayed: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F590 1F3FF", "html": "&#128400;&#127999;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖐🏾", "name": "hand with fingers splayed: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F590 1F3FE", "html": "&#128400;&#127998;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖐🏽", "name": "hand with fingers splayed: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F590 1F3FD", "html": "&#128400;&#127997;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖐🏼", "name": "hand with fingers splayed: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F590 1F3FC", "html": "&#128400;&#127996;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖐🏻", "name": "hand with fingers splayed: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F590 1F3FB", "html": "&#128400;&#127995;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖐", "name": "hand with fingers splayed", "shortname": ":hand_with_fingers_splayed:", "unicode": "1F590", "html": "&#128400;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "✍🏿", "name": "writing hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "270D 1F3FF", "html": "&#9997;&#127999;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "✍🏾", "name": "writing hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "270D 1F3FE", "html": "&#9997;&#127998;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "✍🏽", "name": "writing hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "270D 1F3FD", "html": "&#9997;&#127997;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "✍🏼", "name": "writing hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "270D 1F3FC", "html": "&#9997;&#127996;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "✍🏻", "name": "writing hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "270D 1F3FB", "html": "&#9997;&#127995;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "✍", "name": "writing hand", "shortname": ":writing_hand:", "unicode": "270d", "html": "&#9997;", "category": "People & Body (hand-prop)", "order": "1230"},
+ {"emoji": "🕶", "name": "sunglasses", "shortname": ":dark_sunglasses:", "unicode": "1f576", "html": "&#128374;", "category": "Objects (clothing)", "order": "1315"},
+ {"emoji": "👁‍🗨", "name": "eye in speech bubble", "shortname": ":eye_speachbubble:", "unicode": "1F441 200D 1F5E8", "html": "&#128065;&zwj;&#128488;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "👁", "name": "eye", "shortname": ":eye:", "unicode": "1f441", "html": "&#128065;", "category": "People & Body (body-parts)", "order": "1280"},
+ {"emoji": "🏋🏿‍♀", "name": "woman lifting weights: dark skin tone", "shortname": ":weightlifter_woman_dt:", "unicode": "1F3CB 1F3FF 200D 2640", "html": "&#127947;&#127999;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏾‍♀", "name": "woman lifting weights: medium-dark skin tone", "shortname": ":weightlifter_woman_mdt:", "unicode": "1F3CB 1F3FE 200D 2640", "html": "&#127947;&#127998;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏽‍♀", "name": "woman lifting weights: medium skin tone", "shortname": ":weightlifter_woman_mt:", "unicode": "1F3CB 1F3FD 200D 2640", "html": "&#127947;&#127997;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏼‍♀", "name": "woman lifting weights: medium-light skin tone", "shortname": ":weightlifter_woman_mlt:", "unicode": "1F3CB 1F3FC 200D 2640", "html": "&#127947;&#127996;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏻‍♀", "name": "woman lifting weights: light skin tone", "shortname": ":weightlifter_woman_lt:", "unicode": "1F3CB 1F3FB 200D 2640", "html": "&#127947;&#127995;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋‍♀", "name": "woman lifting weights", "shortname": ":weightlifter_woman:", "unicode": "1F3CB 200D 2640", "html": "&#127947;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏿", "name": "person lifting weights: dark skin tone", "shortname": ":weightlifter_dt:", "unicode": "1F3CB 1F3FF", "html": "&#127947;&#127999;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏾", "name": "person lifting weights: medium-dark skin tone", "shortname": ":weightlifter_mdt:", "unicode": "1F3CB 1F3FE", "html": "&#127947;&#127998;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏽", "name": "person lifting weights: medium skin tone", "shortname": ":weightlifter_mt:", "unicode": "1F3CB 1F3FD", "html": "&#127947;&#127997;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏼", "name": "person lifting weights: medium-light skin tone", "shortname": ":weightlifter_mlt:", "unicode": "1F3CB 1F3FC", "html": "&#127947;&#127996;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏻", "name": "person lifting weights: light skin tone", "shortname": ":weightlifter_lt:", "unicode": "1F3CB 1F3FB", "html": "&#127947;&#127995;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋", "name": "person lifting weights", "shortname": ":weightlifter:", "unicode": "1F3CB", "html": "&#127947;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏿‍♀", "name": "woman bouncing ball: dark skin tone", "shortname": ":basketballer_woman_dt:", "unicode": ":basketballer:", "html": "&#9977;&#127999;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏾‍♀", "name": "woman bouncing ball: medium-dark skin tone", "shortname": ":basketballer_woman_mdt:", "unicode": ":basketballer:", "html": "&#9977;&#127998;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏽‍♀", "name": "woman bouncing ball: medium skin tone", "shortname": ":basketballer_woman_mt:", "unicode": ":basketballer:", "html": "&#9977;&#127997;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏼‍♀", "name": "woman bouncing ball: medium-light skin tone", "shortname": ":basketballer_woman_mlt:", "unicode": ":basketballer:", "html": "&#9977;&#127996;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏻‍♀", "name": "woman bouncing ball: light skin tone", "shortname": ":basketballer_woman_lt:", "unicode": ":basketballer:", "html": "&#9977;&#127995;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹‍♀", "name": "woman bouncing ball", "shortname": ":woman_bouncing_ball:", "unicode": ":basketballer_woman:", "html": "&#9977;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏿", "name": "person bouncing ball: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": ":basketballer_dt:", "html": "&#9977;&#127999;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏾", "name": "person bouncing ball: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": ":basketballer_mdt:", "html": "&#9977;&#127998;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏽", "name": "person bouncing ball: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": ":basketballer_mt:", "html": "&#9977;&#127997;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏼", "name": "person bouncing ball: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": ":basketballer_mlt:", "html": "&#9977;&#127996;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏻", "name": "person bouncing ball: light skin tone", "shortname": ":basketballer_lt:", "unicode": "26F9 1F3FB", "html": "&#9977;&#127995;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹", "name": "person bouncing ball", "shortname": ":basketballer:", "unicode": "26F9", "html": "&#9977;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🕴", "name": "man in suit levitating", "shortname": ":man_in_suit:", "unicode": "1f574", "html": "&#128372;", "category": "People & Body (person-activity)", "order": "784"},
+ {"emoji": "🏌", "name": "person golfing", "shortname": ":golfer:", "unicode": "1f3cc", "html": "&#127948;", "category": "People & Body (person-sport)", "order": "782"},
+ {"emoji": "🏌‍♀", "name": "woman golfing", "shortname": ":golfer_woman:", "unicode": "1F3CC 200D 2640", "html": "&#127948;&zwj;&#9792;", "category": "People & Body (person-sport)", "order": "783"},
+ {"emoji": "*️⃣", "name": "keycap: #", "shortname": "*", "unicode": "0023 FE0F 20E3", "html": "&#42;", "category": "Symbols (keycap)", "order": "2118"},
+ {"emoji": "❣️", "name": "heart exclamation", "shortname": ":heart_exclamation:", "unicode": "2763", "html": "&#10083;", "category": "Smileys & Emotion (emotion)", "order": "1300"},
+ {"emoji": "✡️", "name": "star of David", "shortname": ":star_of_david:", "unicode": "2721", "html": "&#10017;", "category": "Symbols (religion)", "order": "2026"},
+ {"emoji": "✝️", "name": "latin cross", "shortname": ":cross:", "unicode": "271d", "html": "&#10013;", "category": "Symbols (religion)", "order": "2029"},
+ {"emoji": "⚜", "name": "fleur-de-lis", "shortname": ":fleur-de-lis:", "unicode": "269c", "html": "&#9884;", "category": "Symbols (other-symbol)", "order": "2074"},
+ {"emoji": "⚛", "name": "atom symbol", "shortname": ":atom:", "unicode": "269b", "html": "&#9883;", "category": "Symbols (religion)", "order": "2024"},
+ {"emoji": "☸", "name": "wheel of dharma", "shortname": ":wheel_of_dharma:", "unicode": "2638", "html": "&#9784;", "category": "Symbols (religion)", "order": "2027"},
+ {"emoji": "☯", "name": "yin yang", "shortname": ":yin_yang:", "unicode": "262f", "html": "&#9775;", "category": "Symbols (religion)", "order": "2028"},
+ {"emoji": "☮", "name": "peace symbol", "shortname": ":peace:", "unicode": "262e", "html": "&#9774;", "category": "Symbols (religion)", "order": "2032"},
+ {"emoji": "☪", "name": "star and crescent", "shortname": ":star_and_crescent:", "unicode": "262a", "html": "&#9770;", "category": "Symbols (religion)", "order": "2031"},
+ {"emoji": "☦", "name": "orthodox cross", "shortname": ":orthodox_cross:", "unicode": "2626", "html": "&#9766;", "category": "Symbols (religion)", "order": "2030"},
+ {"emoji": "☣", "name": "biohazard", "shortname": ":biohazard:", "unicode": "2623", "html": "&#9763;", "category": "Symbols (warning)", "order": "2001"},
+ {"emoji": "☢", "name": "radioactive", "shortname": ":radioactive:", "unicode": "2622", "html": "&#9762;", "category": "Symbols (warning)", "order": "2000"},
+ {"emoji": "🛐", "name": "place of worship", "shortname": ":place_of_worship:", "unicode": "1f6d0", "html": "&#128720;", "category": "Symbols (religion)", "order": "2023"},
+ {"emoji": "🗯", "name": "right anger bubble", "shortname": ":anger_right:", "unicode": "1f5ef", "html": "&#128495;", "category": "Smileys & Emotion (emotion)", "order": "1311"},
+ {"emoji": "🕎", "name": "menorah", "shortname": ":menorah:", "unicode": "1f54e", "html": "&#128334;", "category": "Symbols (religion)", "order": "2033"},
+ {"emoji": "🕉", "name": "om", "shortname": ":om_symbol:", "unicode": "1f549", "html": "&#128329;", "category": "Symbols (religion)", "order": "2025"},
+ {"emoji": "⚱", "name": "funeral urn", "shortname": ":funeral_urn:", "unicode": "26B1", "html": "&#9905;", "category": "Objects (other-object)", "order": ""},
+ {"emoji": "⚰", "name": "coffin", "shortname": ":coffin:", "unicode": "26b0", "html": "&#9904;", "category": "Objects (other-object)", "order": "1970"},
+ {"emoji": "⚙", "name": "gear", "shortname": ":gear:", "unicode": "2699", "html": "&#9881;", "category": "Objects (tool)", "order": "1961"},
+ {"emoji": "⚗", "name": "alembic", "shortname": ":alembic:", "unicode": "2697", "html": "&#9879;", "category": "Objects (science)", "order": "1963"},
+ {"emoji": "⚖", "name": "balance scale", "shortname": ":scales:", "unicode": "2696", "html": "&#9878;", "category": "Objects (tool)", "order": "1964"},
+ {"emoji": "⚔", "name": "crossed swords", "shortname": ":crossed_swords:", "unicode": "2694", "html": "&#9876;", "category": "Objects (tool)", "order": "1955"},
+ {"emoji": "⌨", "name": "keyboard", "shortname": ":keyboard:", "unicode": "2328", "html": "&#9000;", "category": "Objects (computer)", "order": "1849"},
+ {"emoji": "🛢", "name": "oil drum", "shortname": ":oil_drum:", "unicode": "1F6E2", "html": "&#128738;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🛡", "name": "shield", "shortname": ":shield:", "unicode": "1f6e1", "html": "&#128737;", "category": "Objects (tool)", "order": "1958"},
+ {"emoji": "🛠", "name": "hammer and wrench", "shortname": ":hammer_and_wrench:", "unicode": "1F6E0", "html": "&#128736;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "🛏", "name": "bed", "shortname": ":bed:", "unicode": "1f6cf", "html": "&#128719;", "category": "Objects (household)", "order": "1669"},
+ {"emoji": "🛎", "name": "bellhop bell", "shortname": ":bellhop_bell:", "unicode": "1F6CE", "html": "&#128718;", "category": "Travel & Places (hotel)", "order": ""},
+ {"emoji": "🛍", "name": "shopping bags", "shortname": ":shopping_bags:", "unicode": "1f6cd", "html": "&#128717;", "category": "Objects (clothing)", "order": "1326"},
+ {"emoji": "🛌", "name": "person in bed", "shortname": ":sleeping_accommodation:", "unicode": "1f6cc", "html": "&#128716;", "category": "People & Body (person-resting)", "order": "1663"},
+ {"emoji": "🛋", "name": "couch and lamp", "shortname": ":couch_and_lamp:", "unicode": "1F6CB", "html": "&#128715;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🗳", "name": "ballot box with ballot", "shortname": ":ballot_box:", "unicode": "1f5f3", "html": "&#128499;", "category": "Objects (mail)", "order": "1913"},
+ {"emoji": "🗡", "name": "dagger", "shortname": ":dagger:", "unicode": "1F5E1", "html": "&#128481;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "🗞", "name": "rolled-up newspaper", "shortname": ":rolledup_newspaper:", "unicode": "1F5DE", "html": "&#128478;", "category": "Objects (book-paper)", "order": ""},
+ {"emoji": "🗝", "name": "old key", "shortname": ":old_key:", "unicode": "1F5DD", "html": "&#128477;", "category": "Objects (lock)", "order": ""},
+ {"emoji": "🗜", "name": "clamp", "shortname": ":compression:", "unicode": "1f5dc", "html": "&#128476;", "category": "Objects (tool)", "order": "1962"},
+ {"emoji": "🗓", "name": "spiral calendar", "shortname": ":spiral_calendar:", "unicode": "1F5D3", "html": "&#128467;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🗒", "name": "spiral notepad", "shortname": ":spiral_notepad:", "unicode": "1F5D2", "html": "&#128466;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🗑", "name": "wastebasket", "shortname": ":wastebasket:", "unicode": "1f5d1", "html": "&#128465;", "category": "Objects (office)", "order": "1943"},
+ {"emoji": "🗄", "name": "file cabinet", "shortname": ":file_cabinet:", "unicode": "1f5c4", "html": "&#128452;", "category": "Objects (office)", "order": "1942"},
+ {"emoji": "🗃", "name": "card file box", "shortname": ":card_file_box:", "unicode": "1F5C3", "html": "&#128451;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🗂", "name": "card index dividers", "shortname": ":card_index_dividers:", "unicode": "1F5C2", "html": "&#128450;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🖼", "name": "framed picture", "shortname": ":framed_picture:", "unicode": "1F5BC", "html": "&#128444;", "category": "Activities (arts & crafts)", "order": ""},
+ {"emoji": "🖲", "name": "trackball", "shortname": ":trackball:", "unicode": "1f5b2", "html": "&#128434;", "category": "Objects (computer)", "order": "1851"},
+ {"emoji": "🖱", "name": "computer mouse", "shortname": ":computer_mouse:", "unicode": "1F5B1", "html": "&#128433;", "category": "Objects (computer)", "order": ""},
+ {"emoji": "🖨", "name": "printer", "shortname": ":printer:", "unicode": "1f5a8", "html": "&#128424;", "category": "Objects (computer)", "order": "1848"},
+ {"emoji": "🖥", "name": "desktop computer", "shortname": ":desktop_computer:", "unicode": "1F5A5", "html": "&#128421;", "category": "Objects (computer)", "order": ""},
+ {"emoji": "🖍", "name": "crayon", "shortname": ":crayon:", "unicode": "1F58D", "html": "&#128397;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "🖌", "name": "paintbrush", "shortname": ":paintbrush:", "unicode": "1F58C", "html": "&#128396;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "🖋", "name": "fountain pen", "shortname": ":fountain_pen:", "unicode": "1F58B", "html": "&#128395;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "🖊", "name": "pen", "shortname": ":pen:", "unicode": "1F58A", "html": "&#128394;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "🖇", "name": "linked paperclips", "shortname": ":linked_paperclips:", "unicode": "1F587", "html": "&#128391;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🕹", "name": "joystick", "shortname": ":joystick:", "unicode": "1f579", "html": "&#128377;", "category": "Activities (game)", "order": "1805"},
+ {"emoji": "🕳", "name": "hole", "shortname": ":hole:", "unicode": "1f573", "html": "&#128371;", "category": "Smileys & Emotion (emotion)", "order": "1313"},
+ {"emoji": "🕰", "name": "mantelpiece clock", "shortname": ":mantelpiece_clock:", "unicode": "1F570", "html": "&#128368;", "category": "Travel & Places (time)", "order": ""},
+ {"emoji": "🕯", "name": "candle", "shortname": ":candle:", "unicode": "1f56f", "html": "&#128367;", "category": "Objects (light & video)", "order": "1870"},
+ {"emoji": "📿", "name": "prayer beads", "shortname": ":prayer_beads:", "unicode": "1f4ff", "html": "&#128255;", "category": "Objects (clothing)", "order": "1338"},
+ {"emoji": "📽", "name": "film projector", "shortname": ":film_projector:", "unicode": "1F4FD", "html": "&#128253;", "category": "Objects (light & video)", "order": ""},
+ {"emoji": "📸", "name": "camera with flash", "shortname": ":camera_with_flash:", "unicode": "1f4f8", "html": "&#128248;", "category": "Objects (light & video)", "order": "1862"},
+ {"emoji": "🏺", "name": "amphora", "shortname": ":amphora:", "unicode": "1f3fa", "html": "&#127994;", "category": "Food & Drink (dishware)", "order": "1537"},
+ {"emoji": "🏷", "name": "label", "shortname": ":label:", "unicode": "1f3f7", "html": "&#127991;", "category": "Objects (book-paper)", "order": "1890"},
+ {"emoji": "🏴", "name": "black flag", "shortname": ":flag_black:", "unicode": "1f3f4", "html": "&#127988;", "category": "Flags (flag)", "order": "2184"},
+ {"emoji": "🏳", "name": "white flag", "shortname": ":flag_white:", "unicode": "1f3f3", "html": "&#127987;", "category": "Flags (flag)", "order": "2185"},
+ {"emoji": "🎞", "name": "film frames", "shortname": ":film_frames:", "unicode": "1f39e", "html": "&#127902;", "category": "Objects (light & video)", "order": "1857"},
+ {"emoji": "🎛", "name": "control knobs", "shortname": ":control_knobs:", "unicode": "1f39b", "html": "&#127899;", "category": "Objects (music)", "order": "1828"},
+ {"emoji": "🎚", "name": "level slider", "shortname": ":level_slider:", "unicode": "1f39a", "html": "&#127898;", "category": "Objects (music)", "order": "1827"},
+ {"emoji": "🎙", "name": "studio microphone", "shortname": ":studio_microphone:", "unicode": "1F399", "html": "&#127897;", "category": "Objects (music)", "order": ""},
+ {"emoji": "🌡", "name": "thermometer", "shortname": ":thermometer:", "unicode": "1f321", "html": "&#127777;", "category": "Travel & Places (sky & weather)", "order": "1723"},
+ {"emoji": "🛳", "name": "passenger ship", "shortname": ":passenger_ship:", "unicode": "1F6F3", "html": "&#128755;", "category": "Travel & Places (transport-water)", "order": ""},
+ {"emoji": "🛰", "name": "satellite", "shortname": ":satellite:", "unicode": "1F6F0", "html": "&#128752;", "category": "Travel & Places (transport-air)", "order": ""},
+ {"emoji": "🛬", "name": "airplane arrival", "shortname": ":airplane_arriving:", "unicode": "1f6ec", "html": "&#128748;", "category": "Travel & Places (transport-air)", "order": "1653"},
+ {"emoji": "🛫", "name": "airplane departure", "shortname": ":airplane_departure:", "unicode": "1f6eb", "html": "&#128747;", "category": "Travel & Places (transport-air)", "order": "1652"},
+ {"emoji": "🛩", "name": "small airplane", "shortname": ":small_airplane:", "unicode": "1F6E9", "html": "&#128745;", "category": "Travel & Places (transport-air)", "order": ""},
+ {"emoji": "🛥", "name": "motor boat", "shortname": ":motor_boat:", "unicode": "1F6E5", "html": "&#128741;", "category": "Travel & Places (transport-water)", "order": ""},
+ {"emoji": "🛤", "name": "railway track", "shortname": ":railway_track:", "unicode": "1f6e4", "html": "&#128740;", "category": "Travel & Places (transport-ground)", "order": "1635"},
+ {"emoji": "🛣", "name": "motorway", "shortname": ":motorway:", "unicode": "1f6e3", "html": "&#128739;", "category": "Travel & Places (transport-ground)", "order": "1634"},
+ {"emoji": "🗺", "name": "world map", "shortname": ":world_map:", "unicode": "1F5FA", "html": "&#128506;", "category": "Travel & Places (place-map)", "order": ""},
+ {"emoji": "🕍", "name": "synagogue", "shortname": ":synagogue:", "unicode": "1f54d", "html": "&#128333;", "category": "Travel & Places (place-religious)", "order": "1579"},
+ {"emoji": "🕌", "name": "mosque", "shortname": ":mosque:", "unicode": "1f54c", "html": "&#128332;", "category": "Travel & Places (place-religious)", "order": "1578"},
+ {"emoji": "🕋", "name": "kaaba", "shortname": ":kaaba:", "unicode": "1f54b", "html": "&#128331;", "category": "Travel & Places (place-religious)", "order": "1581"},
+ {"emoji": "🏟", "name": "stadium", "shortname": ":stadium:", "unicode": "1f3df", "html": "&#127967;", "category": "Travel & Places (place-building)", "order": "1553"},
+ {"emoji": "🏞", "name": "national park", "shortname": ":national_park:", "unicode": "1F3DE", "html": "&#127966;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏝", "name": "desert island", "shortname": ":desert_island:", "unicode": "1F3DD", "html": "&#127965;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏜", "name": "desert", "shortname": ":desert:", "unicode": "1f3dc", "html": "&#127964;", "category": "Travel & Places (place-geographic)", "order": "1550"},
+ {"emoji": "🏛", "name": "classical building", "shortname": ":classical_building:", "unicode": "1f3db", "html": "&#127963;", "category": "Travel & Places (place-building)", "order": "1554"},
+ {"emoji": "🏚", "name": "derelict house", "shortname": ":derelict_house:", "unicode": "1F3DA", "html": "&#127962;", "category": "Travel & Places (place-building)", "order": ""},
+ {"emoji": "🏙", "name": "cityscape", "shortname": ":cityscape:", "unicode": "1f3d9", "html": "&#127961;", "category": "Travel & Places (place-other)", "order": "1557"},
+ {"emoji": "🏘", "name": "houses", "shortname": ":houses:", "unicode": "1F3D8", "html": "&#127960;", "category": "Travel & Places (place-building)", "order": ""},
+ {"emoji": "🏗", "name": "building construction", "shortname": ":building_construction:", "unicode": "1F3D7", "html": "&#127959;", "category": "Travel & Places (place-building)", "order": ""},
+ {"emoji": "🏖", "name": "beach with umbrella", "shortname": ":beach_with_umbrella:", "unicode": "1F3D6", "html": "&#127958;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏕", "name": "camping", "shortname": ":camping:", "unicode": "1f3d5", "html": "&#127957;", "category": "Travel & Places (place-geographic)", "order": "1548"},
+ {"emoji": "🏔", "name": "snow-capped mountain", "shortname": ":snowcapped_mountain:", "unicode": "1F3D4", "html": "&#127956;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏎", "name": "racing car", "shortname": ":racing_car:", "unicode": "1F3CE", "html": "&#127950;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🏍", "name": "motorcycle", "shortname": ":motorcycle:", "unicode": "1F3CD", "html": "&#127949;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🏹", "name": "bow and arrow", "shortname": ":bow_and_arrow:", "unicode": "1f3f9", "html": "&#127993;", "category": "Objects (tool)", "order": "1957"},
+ {"emoji": "🏸", "name": "badminton", "shortname": ":badminton:", "unicode": "1F3F8", "html": "&#127992;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🏵", "name": "rosette", "shortname": ":rosette:", "unicode": "1f3f5", "html": "&#127989;", "category": "Animals & Nature (plant-flower)", "order": "1430"},
+ {"emoji": "🏓", "name": "ping pong", "shortname": ":ping_pong:", "unicode": "1F3D3", "html": "&#127955;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🏒", "name": "ice hockey", "shortname": ":ice_hockey:", "unicode": "1F3D2", "html": "&#127954;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🏑", "name": "field hockey", "shortname": ":field_hockey:", "unicode": "1F3D1", "html": "&#127953;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🏐", "name": "volleyball", "shortname": ":volleyball:", "unicode": "1f3d0", "html": "&#127952;", "category": "Activities (sport)", "order": "1784"},
+ {"emoji": "🏏", "name": "cricket game", "shortname": ":cricket_game:", "unicode": "1F3CF", "html": "&#127951;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🏅", "name": "sports medal", "shortname": ":medal:", "unicode": "1f3c5", "html": "&#127941;", "category": "Activities (award-medal)", "order": "1777"},
+ {"emoji": "🎟", "name": "admission tickets", "shortname": ":admission_tickets:", "unicode": "1F39F", "html": "&#127903;", "category": "Activities (event)", "order": ""},
+ {"emoji": "🎗", "name": "reminder ribbon", "shortname": ":reminder_ribbon:", "unicode": "1f397", "html": "&#127895;", "category": "Activities (event)", "order": "1772"},
+ {"emoji": "🎖", "name": "military medal", "shortname": ":military_medal:", "unicode": "1F396", "html": "&#127894;", "category": "Activities (award-medal)", "order": ""},
+ {"emoji": "🧀", "name": "cheese wedge", "shortname": ":cheese_wedge:", "unicode": "1F9C0", "html": "&#129472;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🍿", "name": "popcorn", "shortname": ":popcorn:", "unicode": "1f37f", "html": "&#127871;", "category": "Food & Drink (food-prepared)", "order": "1494"},
+ {"emoji": "🍾", "name": "bottle with popping cork", "shortname": ":champagne:", "unicode": "1f37e", "html": "&#127870;", "category": "Food & Drink (drink)", "order": "1525"},
+ {"emoji": "🍽", "name": "fork and knife with plate", "shortname": ":fork_and_knife_with_plate:", "unicode": "1F37D", "html": "&#127869;", "category": "Food & Drink (dishware)", "order": ""},
+ {"emoji": "🌶", "name": "hot pepper", "shortname": ":hot_pepper:", "unicode": "1f336", "html": "&#127798;", "category": "Food & Drink (food-vegetable)", "order": "1469"},
+ {"emoji": "🌯", "name": "burrito", "shortname": ":burrito:", "unicode": "1f32f", "html": "&#127791;", "category": "Food & Drink (food-prepared)", "order": "1487"},
+ {"emoji": "🌮", "name": "taco", "shortname": ":taco:", "unicode": "1f32e", "html": "&#127790;", "category": "Food & Drink (food-prepared)", "order": "1486"},
+ {"emoji": "🌭", "name": "hot dog", "shortname": ":hotdog:", "unicode": "1f32d", "html": "&#127789;", "category": "Food & Drink (food-prepared)", "order": "1485"},
+ {"emoji": "☘", "name": "shamrock", "shortname": ":shamrock:", "unicode": "2618", "html": "&#9752;", "category": "Animals & Nature (plant-other)", "order": "1444"},
+ {"emoji": "☄", "name": "comet", "shortname": ":comet:", "unicode": "2604", "html": "&#9732;", "category": "Travel & Places (sky & weather)", "order": "1752"},
+ {"emoji": "☃️", "name": "snowman", "shortname": ":snowman:", "unicode": "2603 FE0F", "html": "&#9731;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "☂️", "name": "umbrella", "shortname": ":umbrella:", "unicode": "2602 FE0F", "html": "&#9730;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🦄", "name": "unicorn", "shortname": ":unicorn:", "unicode": "1F984", "html": "&#129412;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦃", "name": "turkey", "shortname": ":turkey:", "unicode": "1f983", "html": "&#129411;", "category": "Animals & Nature (animal-bird)", "order": "1387"},
+ {"emoji": "🦂", "name": "scorpion", "shortname": ":scorpion:", "unicode": "1f982", "html": "&#129410;", "category": "Animals & Nature (animal-bug)", "order": "1426"},
+ {"emoji": "🦁", "name": "lion", "shortname": ":lion_face:", "unicode": "1f981", "html": "&#129409;", "category": "Animals & Nature (animal-mammal)", "order": "1352"},
+ {"emoji": "🦀", "name": "crab", "shortname": ":crab:", "unicode": "1f980", "html": "&#129408;", "category": "Food & Drink (food-marine)", "order": "1415"},
+ {"emoji": "🕸", "name": "spider web", "shortname": ":spider_web:", "unicode": "1f578", "html": "&#128376;", "category": "Animals & Nature (animal-bug)", "order": "1425"},
+ {"emoji": "🕷", "name": "spider", "shortname": ":spider:", "unicode": "1f577", "html": "&#128375;", "category": "Animals & Nature (animal-bug)", "order": "1424"},
+ {"emoji": "🕊", "name": "dove", "shortname": ":dove:", "unicode": "1F54A", "html": "&#128330;", "category": "Animals & Nature (animal-bird)", "order": ""},
+ {"emoji": "🐿", "name": "chipmunk", "shortname": ":chipmunk:", "unicode": "1f43f", "html": "&#128063;", "category": "Animals & Nature (animal-mammal)", "order": "1381"},
+ {"emoji": "🌬", "name": "wind face", "shortname": ":wind_blowing_face:", "unicode": "1f32c", "html": "&#127788;", "category": "Travel & Places (sky & weather)", "order": "1741"},
+ {"emoji": "🌫", "name": "fog", "shortname": ":fog:", "unicode": "1f32b", "html": "&#127787;", "category": "Travel & Places (sky & weather)", "order": "1740"},
+ {"emoji": "🌪", "name": "tornado", "shortname": ":tornado:", "unicode": "1F32A", "html": "&#127786;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌩", "name": "cloud with lightning", "shortname": ":cloud_with_lightning:", "unicode": "1F329", "html": "&#127785;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌨", "name": "cloud with snow", "shortname": ":cloud_with_snow:", "unicode": "1F328", "html": "&#127784;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌧", "name": "cloud with rain", "shortname": ":cloud_with_rain:", "unicode": "1F327", "html": "&#127783;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌦", "name": "sun behind rain cloud", "shortname": ":sun_behind_rain_cloud:", "unicode": "1F326", "html": "&#127782;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌥", "name": "sun behind large cloud", "shortname": ":sun_behind_large_cloud:", "unicode": "1F325", "html": "&#127781;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌤", "name": "sun behind small cloud", "shortname": ":sun_behind_small_cloud:", "unicode": "1F324", "html": "&#127780;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🗣", "name": "speaking head", "shortname": ":speaking_head:", "unicode": "1F5E3", "html": "&#128483;", "category": "People & Body (person-symbol)", "order": ""},
+ {"emoji": "⏺", "name": "record button", "shortname": ":record_button:", "unicode": "23FA", "html": "&#9210;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏹", "name": "stop button", "shortname": ":stop_button:", "unicode": "23F9", "html": "&#9209;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏸", "name": "pause button", "shortname": ":pause_button:", "unicode": "23F8", "html": "&#9208;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏯", "name": "play or pause button", "shortname": ":play_pause:", "unicode": "23ef", "html": "&#9199;", "category": "Symbols (av-symbol)", "order": "2054"},
+ {"emoji": "⏮", "name": "last track button", "shortname": ":track_previous:", "unicode": "23ee", "html": "&#9198;", "category": "Symbols (av-symbol)", "order": "2057"},
+ {"emoji": "⏭", "name": "next track button", "shortname": ":track_next:", "unicode": "23ed", "html": "&#9197;", "category": "Symbols (av-symbol)", "order": "2053"},
+ {"emoji": "⛱", "name": "umbrella on ground", "shortname": ":beach_umbrella:", "unicode": "26f1", "html": "&#9969;", "category": "Travel & Places (sky & weather)", "order": "1747"},
+ {"emoji": "⛓", "name": "chains", "shortname": ":chains:", "unicode": "26d3", "html": "&#9939;", "category": "Objects (tool)", "order": "1966"},
+ {"emoji": "⛏", "name": "pick", "shortname": ":pick:", "unicode": "26cf", "html": "&#9935;", "category": "Objects (tool)", "order": "1951"},
+ {"emoji": "⚒", "name": "hammer and pick", "shortname": ":hammer_and_pick:", "unicode": "2692", "html": "&#9874;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "⏲", "name": "timer clock", "shortname": ":timer_clock:", "unicode": "23F2", "html": "&#9202;", "category": "Travel & Places (time)", "order": ""},
+ {"emoji": "⏱", "name": "stopwatch", "shortname": ":stopwatch:", "unicode": "23f1", "html": "&#9201;", "category": "Travel & Places (time)", "order": "1684"},
+ {"emoji": "⛴", "name": "ferry", "shortname": ":ferry:", "unicode": "26f4", "html": "&#9972;", "category": "Travel & Places (transport-water)", "order": "1647"},
+ {"emoji": "⛰", "name": "mountain", "shortname": ":mountain:", "unicode": "26f0", "html": "&#9968;", "category": "Travel & Places (place-geographic)", "order": "1545"},
+ {"emoji": "⛩", "name": "shinto shrine", "shortname": ":shinto_shrine:", "unicode": "2.6E+10", "html": "&#9961;", "category": "Travel & Places (place-religious)", "order": "1580"},
+ {"emoji": "⛸", "name": "ice skate", "shortname": ":ice_skate:", "unicode": "26f8", "html": "&#9976;", "category": "Activities (sport)", "order": "1800"},
+ {"emoji": "⛷", "name": "skier", "shortname": ":skier:", "unicode": "26f7", "html": "&#9975;", "category": "People & Body (person-sport)", "order": "775"},
+ {"emoji": "⛈", "name": "cloud with lightning and rain", "shortname": ":cloud_with_lightning_and_rain:", "unicode": "26C8", "html": "&#9928;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "⛑", "name": "rescue worker’s helmet", "shortname": ":rescue_worker’s_helmet:", "unicode": "26D1", "html": "&#9937;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🇦🇨", "name": "flag: Ascension Island", "shortname": ":flag_ac:", "unicode": "1f1e6-1f1e8", "html": "&#127462;&#127464;", "category": "Flags (country-flag)", "order": "2187"},
+ {"emoji": "🇦🇩", "name": "flag: Andorra", "shortname": ":flag_ad:", "unicode": "1f1e6-1f1e9", "html": "&#127462;&#127465;", "category": "Flags (country-flag)", "order": "2188"},
+ {"emoji": "🇦🇪", "name": "flag: United Arab Emirates", "shortname": ":flag_ae:", "unicode": "1f1e6-1f1ea", "html": "&#127462;&#127466;", "category": "Flags (country-flag)", "order": "2189"},
+ {"emoji": "🇦🇫", "name": "flag: Afghanistan", "shortname": ":flag_af:", "unicode": "1f1e6-1f1eb", "html": "&#127462;&#127467;", "category": "Flags (country-flag)", "order": "2190"},
+ {"emoji": "🇦🇬", "name": "flag: Antigua & Barbuda", "shortname": ":flag_ag:", "unicode": "1f1e6-1f1ec", "html": "&#127462;&#127468;", "category": "Flags (country-flag)", "order": "2191"},
+ {"emoji": "🇦🇮", "name": "flag: Anguilla", "shortname": ":flag_ai:", "unicode": "1f1e6-1f1ee", "html": "&#127462;&#127470;", "category": "Flags (country-flag)", "order": "2192"},
+ {"emoji": "🇦🇱", "name": "flag: Albania", "shortname": ":flag_al:", "unicode": "1f1e6-1f1f1", "html": "&#127462;&#127473;", "category": "Flags (country-flag)", "order": "2193"},
+ {"emoji": "🇦🇲", "name": "flag: Armenia", "shortname": ":flag_am:", "unicode": "1f1e6-1f1f2", "html": "&#127462;&#127474;", "category": "Flags (country-flag)", "order": "2194"},
+ {"emoji": "🇦🇴", "name": "flag: Angola", "shortname": ":flag-ao:", "unicode": "1f1e6-1f1f4", "html": "&#127462;&#127476;", "category": "Flags (country-flag)", "order": "2195"},
+ {"emoji": "🇦🇶", "name": "flag: Antarctica", "shortname": ":flag-aq:", "unicode": "1f1e6-1f1f6", "html": "&#127462;&#127478;", "category": "Flags (country-flag)", "order": "2196"},
+ {"emoji": "🇦🇷", "name": "flag: Argentina", "shortname": ":flag-ar:", "unicode": "1f1e6-1f1f7", "html": "&#127462;&#127479;", "category": "Flags (country-flag)", "order": "2197"},
+ {"emoji": "🇦🇸", "name": "flag: American Samoa", "shortname": ":flag-as:", "unicode": "1f1e6-1f1f8", "html": "&#127462;&#127480;", "category": "Flags (country-flag)", "order": "2198"},
+ {"emoji": "🇦🇹", "name": "flag: Austria", "shortname": ":flag-at:", "unicode": "1f1e6-1f1f9", "html": "&#127462;&#127481;", "category": "Flags (country-flag)", "order": "2199"},
+ {"emoji": "🇦🇺", "name": "flag: Australia", "shortname": ":flag-au:", "unicode": "1f1e6-1f1fa", "html": "&#127462;&#127482;", "category": "Flags (country-flag)", "order": "2200"},
+ {"emoji": "🇦🇼", "name": "flag: Aruba", "shortname": ":flag-aw:", "unicode": "1f1e6-1f1fc", "html": "&#127462;&#127484;", "category": "Flags (country-flag)", "order": "2201"},
+ {"emoji": "🇦🇽", "name": "flag: Åland Islands", "shortname": ":flag-ax:", "unicode": "1f1e6-1f1fd", "html": "&#127462;&#127485;", "category": "Flags (country-flag)", "order": "2202"},
+ {"emoji": "🇦🇿", "name": "flag: Azerbaijan", "shortname": ":flag-az:", "unicode": "1f1e6-1f1ff", "html": "&#127462;&#127487;", "category": "Flags (country-flag)", "order": "2203"},
+ {"emoji": "🇧🇦", "name": "flag: Bosnia & Herzegovina", "shortname": ":flag-ba:", "unicode": "1f1e7-1f1e6", "html": "&#127463;&#127462;", "category": "Flags (country-flag)", "order": "2204"},
+ {"emoji": "🇧🇧", "name": "flag: Barbados", "shortname": ":flag-bb:", "unicode": "1f1e7-1f1e7", "html": "&#127463;&#127463;", "category": "Flags (country-flag)", "order": "2205"},
+ {"emoji": "🇧🇩", "name": "flag: Bangladesh", "shortname": ":flag-bd:", "unicode": "1f1e7-1f1e9", "html": "&#127463;&#127465;", "category": "Flags (country-flag)", "order": "2206"},
+ {"emoji": "🇧🇪", "name": "flag: Belgium", "shortname": ":flag-be:", "unicode": "1f1e7-1f1ea", "html": "&#127463;&#127466;", "category": "Flags (country-flag)", "order": "2207"},
+ {"emoji": "🇧🇫", "name": "flag: Burkina Faso", "shortname": ":flag-bf:", "unicode": "1f1e7-1f1eb", "html": "&#127463;&#127467;", "category": "Flags (country-flag)", "order": "2208"},
+ {"emoji": "🇧🇬", "name": "flag: Bulgaria", "shortname": ":flag-bg:", "unicode": "1f1e7-1f1ec", "html": "&#127463;&#127468;", "category": "Flags (country-flag)", "order": "2209"},
+ {"emoji": "🇧🇭", "name": "flag: Bahrain", "shortname": ":flag-bh:", "unicode": "1f1e7-1f1ed", "html": "&#127463;&#127469;", "category": "Flags (country-flag)", "order": "2210"},
+ {"emoji": "🇧🇮", "name": "flag: Burundi", "shortname": ":flag-bi:", "unicode": "1f1e7-1f1ee", "html": "&#127463;&#127470;", "category": "Flags (country-flag)", "order": "2211"},
+ {"emoji": "🇧🇯", "name": "flag: Benin", "shortname": ":flag-bj:", "unicode": "1f1e7-1f1ef", "html": "&#127463;&#127471;", "category": "Flags (country-flag)", "order": "2212"},
+ {"emoji": "🇧🇱", "name": "flag: St. Barthélemy", "shortname": ":flag-bl:", "unicode": "1f1e7-1f1f1", "html": "&#127463;&#127473;", "category": "Flags (country-flag)", "order": "2213"},
+ {"emoji": "🇧🇲", "name": "flag: Bermuda", "shortname": ":flag-bm:", "unicode": "1f1e7-1f1f2", "html": "&#127463;&#127474;", "category": "Flags (country-flag)", "order": "2214"},
+ {"emoji": "🇧🇳", "name": "flag: Brunei", "shortname": ":flag-bn:", "unicode": "1f1e7-1f1f3", "html": "&#127463;&#127475;", "category": "Flags (country-flag)", "order": "2215"},
+ {"emoji": "🇧🇴", "name": "flag: Bolivia", "shortname": ":flag-bo:", "unicode": "1f1e7-1f1f4", "html": "&#127463;&#127476;", "category": "Flags (country-flag)", "order": "2216"},
+ {"emoji": "🇧🇶", "name": "flag: Caribbean Netherlands", "shortname": ":flag-bq:", "unicode": "1f1e7-1f1f6", "html": "&#127463;&#127478;", "category": "Flags (country-flag)", "order": "2217"},
+ {"emoji": "🇧🇷", "name": "flag: Brazil", "shortname": ":flag-br:", "unicode": "1f1e7-1f1f7", "html": "&#127463;&#127479;", "category": "Flags (country-flag)", "order": "2218"},
+ {"emoji": "🇧🇸", "name": "flag: Bahamas", "shortname": ":flag-bs:", "unicode": "1f1e7-1f1f8", "html": "&#127463;&#127480;", "category": "Flags (country-flag)", "order": "2219"},
+ {"emoji": "🇧🇹", "name": "flag: Bhutan", "shortname": ":flag-bt:", "unicode": "1f1e7-1f1f9", "html": "&#127463;&#127481;", "category": "Flags (country-flag)", "order": "2220"},
+ {"emoji": "🇧🇻", "name": "flag: Bouvet Island", "shortname": ":flag-bv:", "unicode": "1f1e7-1f1fb", "html": "&#127463;&#127483;", "category": "Flags (country-flag)", "order": "2221"},
+ {"emoji": "🇧🇼", "name": "flag: Botswana", "shortname": ":flag-bw:", "unicode": "1f1e7-1f1fc", "html": "&#127463;&#127484;", "category": "Flags (country-flag)", "order": "2222"},
+ {"emoji": "🇧🇾", "name": "flag: Belarus", "shortname": ":flag-by:", "unicode": "1f1e7-1f1fe", "html": "&#127463;&#127486;", "category": "Flags (country-flag)", "order": "2223"},
+ {"emoji": "🇧🇿", "name": "flag: Belize", "shortname": ":flag-bz:", "unicode": "1f1e7-1f1ff", "html": "&#127463;&#127487;", "category": "Flags (country-flag)", "order": "2224"},
+ {"emoji": "🇨🇦", "name": "flag: Canada", "shortname": ":flag-ca:", "unicode": "1f1e8-1f1e6", "html": "&#127464;&#127462;", "category": "Flags (country-flag)", "order": "2225"},
+ {"emoji": "🇨🇨", "name": "flag: Cocos (Keeling) Islands", "shortname": ":flag-cc:", "unicode": "1f1e8-1f1e8", "html": "&#127464;&#127464;", "category": "Flags (country-flag)", "order": "2226"},
+ {"emoji": "🇨🇩", "name": "flag: Congo - Kinshasa", "shortname": ":flag-cd:", "unicode": "1f1e8-1f1e9", "html": "&#127464;&#127465;", "category": "Flags (country-flag)", "order": "2227"},
+ {"emoji": "🇨🇫", "name": "flag: Central African Republic", "shortname": ":flag-cf:", "unicode": "1f1e8-1f1eb", "html": "&#127464;&#127467;", "category": "Flags (country-flag)", "order": "2228"},
+ {"emoji": "🇨🇬", "name": "flag: Congo - Brazzaville", "shortname": ":flag-cg:", "unicode": "1f1e8-1f1ec", "html": "&#127464;&#127468;", "category": "Flags (country-flag)", "order": "2229"},
+ {"emoji": "🇨🇭", "name": "flag: Switzerland", "shortname": ":flag-ch:", "unicode": "1f1e8-1f1ed", "html": "&#127464;&#127469;", "category": "Flags (country-flag)", "order": "2230"},
+ {"emoji": "🇨🇮", "name": "flag: Côte d’Ivoire", "shortname": ":flag-ci:", "unicode": "1f1e8-1f1ee", "html": "&#127464;&#127470;", "category": "Flags (country-flag)", "order": "2231"},
+ {"emoji": "🇨🇰", "name": "flag: Cook Islands", "shortname": ":flag-ck:", "unicode": "1f1e8-1f1f0", "html": "&#127464;&#127472;", "category": "Flags (country-flag)", "order": "2232"},
+ {"emoji": "🇨🇱", "name": "flag: Chile", "shortname": ":flag-cl:", "unicode": "1f1e8-1f1f1", "html": "&#127464;&#127473;", "category": "Flags (country-flag)", "order": "2233"},
+ {"emoji": "🇨🇲", "name": "flag: Cameroon", "shortname": ":flag-cm:", "unicode": "1f1e8-1f1f2", "html": "&#127464;&#127474;", "category": "Flags (country-flag)", "order": "2234"},
+ {"emoji": "🇨🇳", "name": "flag: China", "shortname": ":flag-cn:", "unicode": "1f1e8-1f1f3", "html": "&#127464;&#127475;", "category": "Flags (country-flag)", "order": "2235"},
+ {"emoji": "🇨🇴", "name": "flag: Colombia", "shortname": ":flag-co:", "unicode": "1f1e8-1f1f4", "html": "&#127464;&#127476;", "category": "Flags (country-flag)", "order": "2236"},
+ {"emoji": "🇨🇵", "name": "flag: Clipperton Island", "shortname": ":flag-cp:", "unicode": "1f1e8-1f1f5", "html": "&#127464;&#127477;", "category": "Flags (country-flag)", "order": "2237"},
+ {"emoji": "🇨🇷", "name": "flag: Costa Rica", "shortname": ":flag-cr:", "unicode": "1f1e8-1f1f7", "html": "&#127464;&#127479;", "category": "Flags (country-flag)", "order": "2238"},
+ {"emoji": "🇨🇺", "name": "flag: Cuba", "shortname": ":flag-cu:", "unicode": "1f1e8-1f1fa", "html": "&#127464;&#127482;", "category": "Flags (country-flag)", "order": "2239"},
+ {"emoji": "🇨🇻", "name": "flag: Cape Verde", "shortname": ":flag-cv:", "unicode": "1f1e8-1f1fb", "html": "&#127464;&#127483;", "category": "Flags (country-flag)", "order": "2240"},
+ {"emoji": "🇨🇼", "name": "flag: Curaçao", "shortname": ":flag-cw:", "unicode": "1f1e8-1f1fc", "html": "&#127464;&#127484;", "category": "Flags (country-flag)", "order": "2241"},
+ {"emoji": "🇨🇽", "name": "flag: Christmas Island", "shortname": ":flag-cx:", "unicode": "1f1e8-1f1fd", "html": "&#127464;&#127485;", "category": "Flags (country-flag)", "order": "2242"},
+ {"emoji": "🇨🇾", "name": "flag: Cyprus", "shortname": ":flag-cy:", "unicode": "1f1e8-1f1fe", "html": "&#127464;&#127486;", "category": "Flags (country-flag)", "order": "2243"},
+ {"emoji": "🇨🇿", "name": "flag: Czechia", "shortname": ":flag-cz:", "unicode": "1f1e8-1f1ff", "html": "&#127464;&#127487;", "category": "Flags (country-flag)", "order": "2244"},
+ {"emoji": "🇩🇪", "name": "flag: Germany", "shortname": ":flag-de:", "unicode": "1f1e9-1f1ea", "html": "&#127465;&#127466;", "category": "Flags (country-flag)", "order": "2245"},
+ {"emoji": "🇩🇬", "name": "flag: Diego Garcia", "shortname": ":flag-dg:", "unicode": "1f1e9-1f1ec", "html": "&#127465;&#127468;", "category": "Flags (country-flag)", "order": "2246"},
+ {"emoji": "🇩🇯", "name": "flag: Djibouti", "shortname": ":flag-dj:", "unicode": "1f1e9-1f1ef", "html": "&#127465;&#127471;", "category": "Flags (country-flag)", "order": "2247"},
+ {"emoji": "🇩🇰", "name": "flag: Denmark", "shortname": ":flag-dk:", "unicode": "1f1e9-1f1f0", "html": "&#127465;&#127472;", "category": "Flags (country-flag)", "order": "2248"},
+ {"emoji": "🇩🇲", "name": "flag: Dominica", "shortname": ":flag-dm:", "unicode": "1f1e9-1f1f2", "html": "&#127465;&#127474;", "category": "Flags (country-flag)", "order": "2249"},
+ {"emoji": "🇩🇴", "name": "flag: Dominican Republic", "shortname": ":flag-do:", "unicode": "1f1e9-1f1f4", "html": "&#127465;&#127476;", "category": "Flags (country-flag)", "order": "2250"},
+ {"emoji": "🇩🇿", "name": "flag: Algeria", "shortname": ":flag-dz:", "unicode": "1f1e9-1f1ff", "html": "&#127465;&#127487;", "category": "Flags (country-flag)", "order": "2251"},
+ {"emoji": "🇪🇦", "name": "flag: Ceuta & Melilla", "shortname": ":flag-ea:", "unicode": "1f1ea-1f1e6", "html": "&#127466;&#127462;", "category": "Flags (country-flag)", "order": "2252"},
+ {"emoji": "🇪🇨", "name": "flag: Ecuador", "shortname": ":flag-ec:", "unicode": "1f1ea-1f1e8", "html": "&#127466;&#127464;", "category": "Flags (country-flag)", "order": "2253"},
+ {"emoji": "🇪🇪", "name": "flag: Estonia", "shortname": ":flag-ee:", "unicode": "1f1ea-1f1ea", "html": "&#127466;&#127466;", "category": "Flags (country-flag)", "order": "2254"},
+ {"emoji": "🇪🇬", "name": "flag: Egypt", "shortname": ":flag-eg:", "unicode": "1f1ea-1f1ec", "html": "&#127466;&#127468;", "category": "Flags (country-flag)", "order": "2255"},
+ {"emoji": "🇪🇭", "name": "flag: Western Sahara", "shortname": ":flag-eh:", "unicode": "1f1ea-1f1ed", "html": "&#127466;&#127469;", "category": "Flags (country-flag)", "order": "2256"},
+ {"emoji": "🇪🇷", "name": "flag: Eritrea", "shortname": ":flag-er:", "unicode": "1f1ea-1f1f7", "html": "&#127466;&#127479;", "category": "Flags (country-flag)", "order": "2257"},
+ {"emoji": "🇪🇸", "name": "flag: Spain", "shortname": ":flag-es:", "unicode": "1f1ea-1f1f8", "html": "&#127466;&#127480;", "category": "Flags (country-flag)", "order": "2258"},
+ {"emoji": "🇪🇹", "name": "flag: Ethiopia", "shortname": ":flag-et:", "unicode": "1f1ea-1f1f9", "html": "&#127466;&#127481;", "category": "Flags (country-flag)", "order": "2259"},
+ {"emoji": "🇪🇺", "name": "flag: European Union", "shortname": ":flag-eu:", "unicode": "1f1ea-1f1fa", "html": "&#127466;&#127482;", "category": "Flags (country-flag)", "order": "2260"},
+ {"emoji": "🇫🇮", "name": "flag: Finland", "shortname": ":flag-fi:", "unicode": "1f1eb-1f1ee", "html": "&#127467;&#127470;", "category": "Flags (country-flag)", "order": "2261"},
+ {"emoji": "🇫🇯", "name": "flag: Fiji", "shortname": ":flag-fj:", "unicode": "1f1eb-1f1ef", "html": "&#127467;&#127471;", "category": "Flags (country-flag)", "order": "2262"},
+ {"emoji": "🇫🇰", "name": "flag: Falkland Islands", "shortname": ":flag-fk:", "unicode": "1f1eb-1f1f0", "html": "&#127467;&#127472;", "category": "Flags (country-flag)", "order": "2263"},
+ {"emoji": "🇫🇲", "name": "flag: Micronesia", "shortname": ":flag-fm:", "unicode": "1f1eb-1f1f2", "html": "&#127467;&#127474;", "category": "Flags (country-flag)", "order": "2264"},
+ {"emoji": "🇫🇴", "name": "flag: Faroe Islands", "shortname": ":flag-fo:", "unicode": "1f1eb-1f1f4", "html": "&#127467;&#127476;", "category": "Flags (country-flag)", "order": "2265"},
+ {"emoji": "🇫🇷", "name": "flag: France", "shortname": ":flag-fr:", "unicode": "1f1eb-1f1f7", "html": "&#127467;&#127479;", "category": "Flags (country-flag)", "order": "2266"},
+ {"emoji": "🇬🇦", "name": "flag: Gabon", "shortname": ":flag-ga:", "unicode": "1f1ec-1f1e6", "html": "&#127468;&#127462;", "category": "Flags (country-flag)", "order": "2267"},
+ {"emoji": "🇬🇧", "name": "flag: United Kingdom", "shortname": ":flag-gb:", "unicode": "1f1ec-1f1e7", "html": "&#127468;&#127463;", "category": "Flags (country-flag)", "order": "2268"},
+ {"emoji": "🇬🇩", "name": "flag: Grenada", "shortname": ":flag-gd:", "unicode": "1f1ec-1f1e9", "html": "&#127468;&#127465;", "category": "Flags (country-flag)", "order": "2269"},
+ {"emoji": "🇬🇪", "name": "flag: Georgia", "shortname": ":flag-ge:", "unicode": "1f1ec-1f1ea", "html": "&#127468;&#127466;", "category": "Flags (country-flag)", "order": "2270"},
+ {"emoji": "🇬🇫", "name": "flag: French Guiana", "shortname": ":flag-gf:", "unicode": "1f1ec-1f1eb", "html": "&#127468;&#127467;", "category": "Flags (country-flag)", "order": "2271"},
+ {"emoji": "🇬🇬", "name": "flag: Guernsey", "shortname": ":flag-gg:", "unicode": "1f1ec-1f1ec", "html": "&#127468;&#127468;", "category": "Flags (country-flag)", "order": "2272"},
+ {"emoji": "🇬🇭", "name": "flag: Ghana", "shortname": ":flag-gh:", "unicode": "1f1ec-1f1ed", "html": "&#127468;&#127469;", "category": "Flags (country-flag)", "order": "2273"},
+ {"emoji": "🇬🇮", "name": "flag: Gibraltar", "shortname": ":flag-gi:", "unicode": "1f1ec-1f1ee", "html": "&#127468;&#127470;", "category": "Flags (country-flag)", "order": "2274"},
+ {"emoji": "🇬🇱", "name": "flag: Greenland", "shortname": ":flag-gl:", "unicode": "1f1ec-1f1f1", "html": "&#127468;&#127473;", "category": "Flags (country-flag)", "order": "2275"},
+ {"emoji": "🇬🇲", "name": "flag: Gambia", "shortname": ":flag-gm:", "unicode": "1f1ec-1f1f2", "html": "&#127468;&#127474;", "category": "Flags (country-flag)", "order": "2276"},
+ {"emoji": "🇬🇳", "name": "flag: Guinea", "shortname": ":flag-gn:", "unicode": "1f1ec-1f1f3", "html": "&#127468;&#127475;", "category": "Flags (country-flag)", "order": "2277"},
+ {"emoji": "🇬🇵", "name": "flag: Guadeloupe", "shortname": ":flag-gp:", "unicode": "1f1ec-1f1f5", "html": "&#127468;&#127477;", "category": "Flags (country-flag)", "order": "2278"},
+ {"emoji": "🇬🇶", "name": "flag: Equatorial Guinea", "shortname": ":flag-gq:", "unicode": "1f1ec-1f1f6", "html": "&#127468;&#127478;", "category": "Flags (country-flag)", "order": "2279"},
+ {"emoji": "🇬🇷", "name": "flag: Greece", "shortname": ":flag-gr:", "unicode": "1f1ec-1f1f7", "html": "&#127468;&#127479;", "category": "Flags (country-flag)", "order": "2280"},
+ {"emoji": "🇬🇸", "name": "flag: South Georgia & South Sandwich Islands", "shortname": ":flag-gs:", "unicode": "1f1ec-1f1f8", "html": "&#127468;&#127480;", "category": "Flags (country-flag)", "order": "2281"},
+ {"emoji": "🇬🇹", "name": "flag: Guatemala", "shortname": ":flag-gt:", "unicode": "1f1ec-1f1f9", "html": "&#127468;&#127481;", "category": "Flags (country-flag)", "order": "2282"},
+ {"emoji": "🇬🇺", "name": "flag: Guam", "shortname": ":flag-gu:", "unicode": "1f1ec-1f1fa", "html": "&#127468;&#127482;", "category": "Flags (country-flag)", "order": "2283"},
+ {"emoji": "🇬🇼", "name": "flag: Guinea-Bissau", "shortname": ":flag-gw:", "unicode": "1f1ec-1f1fc", "html": "&#127468;&#127484;", "category": "Flags (country-flag)", "order": "2284"},
+ {"emoji": "🇬🇾", "name": "flag: Guyana", "shortname": ":flag-gy:", "unicode": "1f1ec-1f1fe", "html": "&#127468;&#127486;", "category": "Flags (country-flag)", "order": "2285"},
+ {"emoji": "🇭🇰", "name": "flag: Hong Kong SAR China", "shortname": ":flag-hk:", "unicode": "1f1ed-1f1f0", "html": "&#127469;&#127472;", "category": "Flags (country-flag)", "order": "2286"},
+ {"emoji": "🇭🇲", "name": "flag: Heard & McDonald Islands", "shortname": ":flag-hm:", "unicode": "1f1ed-1f1f2", "html": "&#127469;&#127474;", "category": "Flags (country-flag)", "order": "2287"},
+ {"emoji": "🇭🇳", "name": "flag: Honduras", "shortname": ":flag-hn:", "unicode": "1f1ed-1f1f3", "html": "&#127469;&#127475;", "category": "Flags (country-flag)", "order": "2288"},
+ {"emoji": "🇭🇷", "name": "flag: Croatia", "shortname": ":flag-hr:", "unicode": "1f1ed-1f1f7", "html": "&#127469;&#127479;", "category": "Flags (country-flag)", "order": "2289"},
+ {"emoji": "🇭🇹", "name": "flag: Haiti", "shortname": ":flag-ht:", "unicode": "1f1ed-1f1f9", "html": "&#127469;&#127481;", "category": "Flags (country-flag)", "order": "2290"},
+ {"emoji": "🇭🇺", "name": "flag: Hungary", "shortname": ":flag-hu:", "unicode": "1f1ed-1f1fa", "html": "&#127469;&#127482;", "category": "Flags (country-flag)", "order": "2291"},
+ {"emoji": "🇮🇨", "name": "flag: Canary Islands", "shortname": ":flag-ic:", "unicode": "1f1ee-1f1e8", "html": "&#127470;&#127464;", "category": "Flags (country-flag)", "order": "2292"},
+ {"emoji": "🇮🇩", "name": "flag: Indonesia", "shortname": ":flag-id:", "unicode": "1f1ee-1f1e9", "html": "&#127470;&#127465;", "category": "Flags (country-flag)", "order": "2293"},
+ {"emoji": "🇮🇪", "name": "flag: Ireland", "shortname": ":flag-ie:", "unicode": "1f1ee-1f1ea", "html": "&#127470;&#127466;", "category": "Flags (country-flag)", "order": "2294"},
+ {"emoji": "🇮🇱", "name": "flag: Israel", "shortname": ":flag-il:", "unicode": "1f1ee-1f1f1", "html": "&#127470;&#127473;", "category": "Flags (country-flag)", "order": "2295"},
+ {"emoji": "🇮🇲", "name": "flag: Isle of Man", "shortname": ":flag-im:", "unicode": "1f1ee-1f1f2", "html": "&#127470;&#127474;", "category": "Flags (country-flag)", "order": "2296"},
+ {"emoji": "🇮🇳", "name": "flag: India", "shortname": ":flag-in:", "unicode": "1f1ee-1f1f3", "html": "&#127470;&#127475;", "category": "Flags (country-flag)", "order": "2297"},
+ {"emoji": "🇮🇴", "name": "flag: British Indian Ocean Territory", "shortname": ":flag-io:", "unicode": "1f1ee-1f1f4", "html": "&#127470;&#127476;", "category": "Flags (country-flag)", "order": "2298"},
+ {"emoji": "🇮🇶", "name": "flag: Iraq", "shortname": ":flag-iq:", "unicode": "1f1ee-1f1f6", "html": "&#127470;&#127478;", "category": "Flags (country-flag)", "order": "2299"},
+ {"emoji": "🇮🇷", "name": "flag: Iran", "shortname": ":flag-ir:", "unicode": "1f1ee-1f1f7", "html": "&#127470;&#127479;", "category": "Flags (country-flag)", "order": "2300"},
+ {"emoji": "🇮🇸", "name": "flag: Iceland", "shortname": ":flag-is:", "unicode": "1f1ee-1f1f8", "html": "&#127470;&#127480;", "category": "Flags (country-flag)", "order": "2301"},
+ {"emoji": "🇮🇹", "name": "flag: Italy", "shortname": ":flag-it:", "unicode": "1f1ee-1f1f9", "html": "&#127470;&#127481;", "category": "Flags (country-flag)", "order": "2302"},
+ {"emoji": "🇯🇪", "name": "flag: Jersey", "shortname": ":flag-je:", "unicode": "1f1ef-1f1ea", "html": "&#127471;&#127466;", "category": "Flags (country-flag)", "order": "2303"},
+ {"emoji": "🇯🇲", "name": "flag: Jamaica", "shortname": ":flag-jm:", "unicode": "1f1ef-1f1f2", "html": "&#127471;&#127474;", "category": "Flags (country-flag)", "order": "2304"},
+ {"emoji": "🇯🇴", "name": "flag: Jordan", "shortname": ":flag-jo:", "unicode": "1f1ef-1f1f4", "html": "&#127471;&#127476;", "category": "Flags (country-flag)", "order": "2305"},
+ {"emoji": "🇯🇵", "name": "flag: Japan", "shortname": ":flag-jp:", "unicode": "1f1ef-1f1f5", "html": "&#127471;&#127477;", "category": "Flags (country-flag)", "order": "2306"},
+ {"emoji": "🇰🇪", "name": "flag: Kenya", "shortname": ":flag-ke:", "unicode": "1f1f0-1f1ea", "html": "&#127472;&#127466;", "category": "Flags (country-flag)", "order": "2307"},
+ {"emoji": "🇰🇬", "name": "flag: Kyrgyzstan", "shortname": ":flag-kg:", "unicode": "1f1f0-1f1ec", "html": "&#127472;&#127468;", "category": "Flags (country-flag)", "order": "2308"},
+ {"emoji": "🇰🇭", "name": "flag: Cambodia", "shortname": ":flag-kh:", "unicode": "1f1f0-1f1ed", "html": "&#127472;&#127469;", "category": "Flags (country-flag)", "order": "2309"},
+ {"emoji": "🇰🇮", "name": "flag: Kiribati", "shortname": ":flag-ki:", "unicode": "1f1f0-1f1ee", "html": "&#127472;&#127470;", "category": "Flags (country-flag)", "order": "2310"},
+ {"emoji": "🇰🇲", "name": "flag: Comoros", "shortname": ":flag-km:", "unicode": "1f1f0-1f1f2", "html": "&#127472;&#127474;", "category": "Flags (country-flag)", "order": "2311"},
+ {"emoji": "🇰🇳", "name": "flag: St. Kitts & Nevis", "shortname": ":flag-kn:", "unicode": "1f1f0-1f1f3", "html": "&#127472;&#127475;", "category": "Flags (country-flag)", "order": "2312"},
+ {"emoji": "🇰🇵", "name": "flag: North Korea", "shortname": ":flag-kp:", "unicode": "1f1f0-1f1f5", "html": "&#127472;&#127477;", "category": "Flags (country-flag)", "order": "2313"},
+ {"emoji": "🇰🇷", "name": "flag: South Korea", "shortname": ":flag-kr:", "unicode": "1f1f0-1f1f7", "html": "&#127472;&#127479;", "category": "Flags (country-flag)", "order": "2314"},
+ {"emoji": "🇰🇼", "name": "flag: Kuwait", "shortname": ":flag-kw:", "unicode": "1f1f0-1f1fc", "html": "&#127472;&#127484;", "category": "Flags (country-flag)", "order": "2315"},
+ {"emoji": "🇰🇾", "name": "flag: Cayman Islands", "shortname": ":flag-ky:", "unicode": "1f1f0-1f1fe", "html": "&#127472;&#127486;", "category": "Flags (country-flag)", "order": "2316"},
+ {"emoji": "🇰🇿", "name": "flag: Kazakhstan", "shortname": ":flag-kz:", "unicode": "1f1f0-1f1ff", "html": "&#127472;&#127487;", "category": "Flags (country-flag)", "order": "2317"},
+ {"emoji": "🇱🇦", "name": "flag: Laos", "shortname": ":flag-la:", "unicode": "1f1f1-1f1e6", "html": "&#127473;&#127462;", "category": "Flags (country-flag)", "order": "2318"},
+ {"emoji": "🇱🇧", "name": "flag: Lebanon", "shortname": ":flag-lb:", "unicode": "1f1f1-1f1e7", "html": "&#127473;&#127463;", "category": "Flags (country-flag)", "order": "2319"},
+ {"emoji": "🇱🇨", "name": "flag: St. Lucia", "shortname": ":flag-lc:", "unicode": "1f1f1-1f1e8", "html": "&#127473;&#127464;", "category": "Flags (country-flag)", "order": "2320"},
+ {"emoji": "🇱🇮", "name": "flag: Liechtenstein", "shortname": ":flag-li:", "unicode": "1f1f1-1f1ee", "html": "&#127473;&#127470;", "category": "Flags (country-flag)", "order": "2321"},
+ {"emoji": "🇱🇰", "name": "flag: Sri Lanka", "shortname": ":flag-lk:", "unicode": "1f1f1-1f1f0", "html": "&#127473;&#127472;", "category": "Flags (country-flag)", "order": "2322"},
+ {"emoji": "🇱🇷", "name": "flag: Liberia", "shortname": ":flag-lr:", "unicode": "1f1f1-1f1f7", "html": "&#127473;&#127479;", "category": "Flags (country-flag)", "order": "2323"},
+ {"emoji": "🇱🇸", "name": "flag: Lesotho", "shortname": ":flag-ls:", "unicode": "1f1f1-1f1f8", "html": "&#127473;&#127480;", "category": "Flags (country-flag)", "order": "2324"},
+ {"emoji": "🇱🇹", "name": "flag: Lithuania", "shortname": ":flag-lt:", "unicode": "1f1f1-1f1f9", "html": "&#127473;&#127481;", "category": "Flags (country-flag)", "order": "2325"},
+ {"emoji": "🇱🇺", "name": "flag: Luxembourg", "shortname": ":flag-lu:", "unicode": "1f1f1-1f1fa", "html": "&#127473;&#127482;", "category": "Flags (country-flag)", "order": "2326"},
+ {"emoji": "🇱🇻", "name": "flag: Latvia", "shortname": ":flag-lv:", "unicode": "1f1f1-1f1fb", "html": "&#127473;&#127483;", "category": "Flags (country-flag)", "order": "2327"},
+ {"emoji": "🇱🇾", "name": "flag: Libya", "shortname": ":flag-ly:", "unicode": "1f1f1-1f1fe", "html": "&#127473;&#127486;", "category": "Flags (country-flag)", "order": "2328"},
+ {"emoji": "🇲🇦", "name": "flag: Morocco", "shortname": ":flag-ma:", "unicode": "1f1f2-1f1e6", "html": "&#127474;&#127462;", "category": "Flags (country-flag)", "order": "2329"},
+ {"emoji": "🇲🇨", "name": "flag: Monaco", "shortname": ":flag-mc:", "unicode": "1f1f2-1f1e8", "html": "&#127474;&#127464;", "category": "Flags (country-flag)", "order": "2330"},
+ {"emoji": "🇲🇩", "name": "flag: Moldova", "shortname": ":flag-md:", "unicode": "1f1f2-1f1e9", "html": "&#127474;&#127465;", "category": "Flags (country-flag)", "order": "2331"},
+ {"emoji": "🇲🇪", "name": "flag: Montenegro", "shortname": ":flag-me:", "unicode": "1f1f2-1f1ea", "html": "&#127474;&#127466;", "category": "Flags (country-flag)", "order": "2332"},
+ {"emoji": "🇲🇫", "name": "flag: St. Martin", "shortname": ":flag-mf:", "unicode": "1f1f2-1f1eb", "html": "&#127474;&#127467;", "category": "Flags (country-flag)", "order": "2333"},
+ {"emoji": "🇲🇬", "name": "flag: Madagascar", "shortname": ":flag-mg:", "unicode": "1f1f2-1f1ec", "html": "&#127474;&#127468;", "category": "Flags (country-flag)", "order": "2334"},
+ {"emoji": "🇲🇭", "name": "flag: Marshall Islands", "shortname": ":flag-mh:", "unicode": "1f1f2-1f1ed", "html": "&#127474;&#127469;", "category": "Flags (country-flag)", "order": "2335"},
+ {"emoji": "🇲🇰", "name": "flag: North Macedonia", "shortname": ":flag-mk:", "unicode": "1f1f2-1f1f0", "html": "&#127474;&#127472;", "category": "Flags (country-flag)", "order": "2336"},
+ {"emoji": "🇲🇱", "name": "flag: Mali", "shortname": ":flag-ml:", "unicode": "1f1f2-1f1f1", "html": "&#127474;&#127473;", "category": "Flags (country-flag)", "order": "2337"},
+ {"emoji": "🇲🇲", "name": "flag: Myanmar (Burma)", "shortname": ":flag-mm:", "unicode": "1f1f2-1f1f2", "html": "&#127474;&#127474;", "category": "Flags (country-flag)", "order": "2338"},
+ {"emoji": "🇲🇳", "name": "flag: Mongolia", "shortname": ":flag-mn:", "unicode": "1f1f2-1f1f3", "html": "&#127474;&#127475;", "category": "Flags (country-flag)", "order": "2339"},
+ {"emoji": "🇲🇴", "name": "flag: Macao SAR China", "shortname": ":flag-mo:", "unicode": "1f1f2-1f1f4", "html": "&#127474;&#127476;", "category": "Flags (country-flag)", "order": "2340"},
+ {"emoji": "🇲🇵", "name": "flag: Northern Mariana Islands", "shortname": ":flag-mp:", "unicode": "1f1f2-1f1f5", "html": "&#127474;&#127477;", "category": "Flags (country-flag)", "order": "2341"},
+ {"emoji": "🇲🇶", "name": "flag: Martinique", "shortname": ":flag-mq:", "unicode": "1f1f2-1f1f6", "html": "&#127474;&#127478;", "category": "Flags (country-flag)", "order": "2342"},
+ {"emoji": "🇲🇷", "name": "flag: Mauritania", "shortname": ":flag-mr:", "unicode": "1f1f2-1f1f7", "html": "&#127474;&#127479;", "category": "Flags (country-flag)", "order": "2343"},
+ {"emoji": "🇲🇸", "name": "flag: Montserrat", "shortname": ":flag-ms:", "unicode": "1f1f2-1f1f8", "html": "&#127474;&#127480;", "category": "Flags (country-flag)", "order": "2344"},
+ {"emoji": "🇲🇹", "name": "flag: Malta", "shortname": ":flag-mt:", "unicode": "1f1f2-1f1f9", "html": "&#127474;&#127481;", "category": "Flags (country-flag)", "order": "2345"},
+ {"emoji": "🇲🇺", "name": "flag: Mauritius", "shortname": ":flag-mu:", "unicode": "1f1f2-1f1fa", "html": "&#127474;&#127482;", "category": "Flags (country-flag)", "order": "2346"},
+ {"emoji": "🇲🇻", "name": "flag: Maldives", "shortname": ":flag-mv:", "unicode": "1f1f2-1f1fb", "html": "&#127474;&#127483;", "category": "Flags (country-flag)", "order": "2347"},
+ {"emoji": "🇲🇼", "name": "flag: Malawi", "shortname": ":flag-mw:", "unicode": "1f1f2-1f1fc", "html": "&#127474;&#127484;", "category": "Flags (country-flag)", "order": "2348"},
+ {"emoji": "🇲🇽", "name": "flag: Mexico", "shortname": ":flag-mx:", "unicode": "1f1f2-1f1fd", "html": "&#127474;&#127485;", "category": "Flags (country-flag)", "order": "2349"},
+ {"emoji": "🇲🇾", "name": "flag: Malaysia", "shortname": ":flag-my:", "unicode": "1f1f2-1f1fe", "html": "&#127474;&#127486;", "category": "Flags (country-flag)", "order": "2350"},
+ {"emoji": "🇲🇿", "name": "flag: Mozambique", "shortname": ":flag-mz:", "unicode": "1f1f2-1f1ff", "html": "&#127474;&#127487;", "category": "Flags (country-flag)", "order": "2351"},
+ {"emoji": "🇳🇦", "name": "flag: Namibia", "shortname": ":flag-na:", "unicode": "1f1f3-1f1e6", "html": "&#127475;&#127462;", "category": "Flags (country-flag)", "order": "2352"},
+ {"emoji": "🇳🇨", "name": "flag: New Caledonia", "shortname": ":flag-nc:", "unicode": "1f1f3-1f1e8", "html": "&#127475;&#127464;", "category": "Flags (country-flag)", "order": "2353"},
+ {"emoji": "🇳🇪", "name": "flag: Niger", "shortname": ":flag-ne:", "unicode": "1f1f3-1f1ea", "html": "&#127475;&#127466;", "category": "Flags (country-flag)", "order": "2354"},
+ {"emoji": "🇳🇫", "name": "flag: Norfolk Island", "shortname": ":flag-nf:", "unicode": "1f1f3-1f1eb", "html": "&#127475;&#127467;", "category": "Flags (country-flag)", "order": "2355"},
+ {"emoji": "🇳🇬", "name": "flag: Nigeria", "shortname": ":flag-ng:", "unicode": "1f1f3-1f1ec", "html": "&#127475;&#127468;", "category": "Flags (country-flag)", "order": "2356"},
+ {"emoji": "🇳🇮", "name": "flag: Nicaragua", "shortname": ":flag-ni:", "unicode": "1f1f3-1f1ee", "html": "&#127475;&#127470;", "category": "Flags (country-flag)", "order": "2357"},
+ {"emoji": "🇳🇱", "name": "flag: Netherlands", "shortname": ":flag-nl:", "unicode": "1f1f3-1f1f1", "html": "&#127475;&#127473;", "category": "Flags (country-flag)", "order": "2358"},
+ {"emoji": "🇳🇴", "name": "flag: Norway", "shortname": ":flag-no:", "unicode": "1f1f3-1f1f4", "html": "&#127475;&#127476;", "category": "Flags (country-flag)", "order": "2359"},
+ {"emoji": "🇳🇵", "name": "flag: Nepal", "shortname": ":flag-np:", "unicode": "1f1f3-1f1f5", "html": "&#127475;&#127477;", "category": "Flags (country-flag)", "order": "2360"},
+ {"emoji": "🇳🇷", "name": "flag: Nauru", "shortname": ":flag-nr:", "unicode": "1f1f3-1f1f7", "html": "&#127475;&#127479;", "category": "Flags (country-flag)", "order": "2361"},
+ {"emoji": "🇳🇺", "name": "flag: Niue", "shortname": ":flag-nu:", "unicode": "1f1f3-1f1fa", "html": "&#127475;&#127482;", "category": "Flags (country-flag)", "order": "2362"},
+ {"emoji": "🇳🇿", "name": "flag: New Zealand", "shortname": ":flag-nz:", "unicode": "1f1f3-1f1ff", "html": "&#127475;&#127487;", "category": "Flags (country-flag)", "order": "2363"},
+ {"emoji": "🇴🇲", "name": "flag: Oman", "shortname": ":flag-om:", "unicode": "1f1f4-1f1f2", "html": "&#127476;&#127474;", "category": "Flags (country-flag)", "order": "2364"},
+ {"emoji": "🇵🇦", "name": "flag: Panama", "shortname": ":flag-pa:", "unicode": "1f1f5-1f1e6", "html": "&#127477;&#127462;", "category": "Flags (country-flag)", "order": "2365"},
+ {"emoji": "🇵🇪", "name": "flag: Peru", "shortname": ":flag-pe:", "unicode": "1f1f5-1f1ea", "html": "&#127477;&#127466;", "category": "Flags (country-flag)", "order": "2366"},
+ {"emoji": "🇵🇫", "name": "flag: French Polynesia", "shortname": ":flag-pf:", "unicode": "1f1f5-1f1eb", "html": "&#127477;&#127467;", "category": "Flags (country-flag)", "order": "2367"},
+ {"emoji": "🇵🇬", "name": "flag: Papua New Guinea", "shortname": ":flag-pg:", "unicode": "1f1f5-1f1ec", "html": "&#127477;&#127468;", "category": "Flags (country-flag)", "order": "2368"},
+ {"emoji": "🇵🇭", "name": "flag: Philippines", "shortname": ":flag-ph:", "unicode": "1f1f5-1f1ed", "html": "&#127477;&#127469;", "category": "Flags (country-flag)", "order": "2369"},
+ {"emoji": "🇵🇰", "name": "flag: Pakistan", "shortname": ":flag-pk:", "unicode": "1f1f5-1f1f0", "html": "&#127477;&#127472;", "category": "Flags (country-flag)", "order": "2370"},
+ {"emoji": "🇵🇱", "name": "flag: Poland", "shortname": ":flag-pl:", "unicode": "1f1f5-1f1f1", "html": "&#127477;&#127473;", "category": "Flags (country-flag)", "order": "2371"},
+ {"emoji": "🇵🇲", "name": "flag: St. Pierre & Miquelon", "shortname": ":flag-pm:", "unicode": "1f1f5-1f1f2", "html": "&#127477;&#127474;", "category": "Flags (country-flag)", "order": "2372"},
+ {"emoji": "🇵🇳", "name": "flag: Pitcairn Islands", "shortname": ":flag-pn:", "unicode": "1f1f5-1f1f3", "html": "&#127477;&#127475;", "category": "Flags (country-flag)", "order": "2373"},
+ {"emoji": "🇵🇷", "name": "flag: Puerto Rico", "shortname": ":flag-pr:", "unicode": "1f1f5-1f1f7", "html": "&#127477;&#127479;", "category": "Flags (country-flag)", "order": "2374"},
+ {"emoji": "🇵🇸", "name": "flag: Palestinian Territories", "shortname": ":flag-ps:", "unicode": "1f1f5-1f1f8", "html": "&#127477;&#127480;", "category": "Flags (country-flag)", "order": "2375"},
+ {"emoji": "🇵🇹", "name": "flag: Portugal", "shortname": ":flag-pt:", "unicode": "1f1f5-1f1f9", "html": "&#127477;&#127481;", "category": "Flags (country-flag)", "order": "2376"},
+ {"emoji": "🇵🇼", "name": "flag: Palau", "shortname": ":flag-pw:", "unicode": "1f1f5-1f1fc", "html": "&#127477;&#127484;", "category": "Flags (country-flag)", "order": "2377"},
+ {"emoji": "🇵🇾", "name": "flag: Paraguay", "shortname": ":flag-py:", "unicode": "1f1f5-1f1fe", "html": "&#127477;&#127486;", "category": "Flags (country-flag)", "order": "2378"},
+ {"emoji": "🇶🇦", "name": "flag: Qatar", "shortname": ":flag-qa:", "unicode": "1f1f6-1f1e6", "html": "&#127478;&#127462;", "category": "Flags (country-flag)", "order": "2379"},
+ {"emoji": "🇷🇪", "name": "flag: Réunion", "shortname": ":flag-re:", "unicode": "1f1f7-1f1ea", "html": "&#127479;&#127466;", "category": "Flags (country-flag)", "order": "2380"},
+ {"emoji": "🇷🇴", "name": "flag: Romania", "shortname": ":flag-ro:", "unicode": "1f1f7-1f1f4", "html": "&#127479;&#127476;", "category": "Flags (country-flag)", "order": "2381"},
+ {"emoji": "🇷🇸", "name": "flag: Serbia", "shortname": ":flag-rs:", "unicode": "1f1f7-1f1f8", "html": "&#127479;&#127480;", "category": "Flags (country-flag)", "order": "2382"},
+ {"emoji": "🇷🇺", "name": "flag: Russia", "shortname": ":flag-ru:", "unicode": "1f1f7-1f1fa", "html": "&#127479;&#127482;", "category": "Flags (country-flag)", "order": "2383"},
+ {"emoji": "🇷🇼", "name": "flag: Rwanda", "shortname": ":flag-rw:", "unicode": "1f1f7-1f1fc", "html": "&#127479;&#127484;", "category": "Flags (country-flag)", "order": "2384"},
+ {"emoji": "🇸🇦", "name": "flag: Saudi Arabia", "shortname": ":flag-sa:", "unicode": "1f1f8-1f1e6", "html": "&#127480;&#127462;", "category": "Flags (country-flag)", "order": "2385"},
+ {"emoji": "🇸🇧", "name": "flag: Solomon Islands", "shortname": ":flag-sb:", "unicode": "1f1f8-1f1e7", "html": "&#127480;&#127463;", "category": "Flags (country-flag)", "order": "2386"},
+ {"emoji": "🇸🇨", "name": "flag: Seychelles", "shortname": ":flag-sc:", "unicode": "1f1f8-1f1e8", "html": "&#127480;&#127464;", "category": "Flags (country-flag)", "order": "2387"},
+ {"emoji": "🇸🇩", "name": "flag: Sudan", "shortname": ":flag-sd:", "unicode": "1f1f8-1f1e9", "html": "&#127480;&#127465;", "category": "Flags (country-flag)", "order": "2388"},
+ {"emoji": "🇸🇪", "name": "flag: Sweden", "shortname": ":flag-se:", "unicode": "1f1f8-1f1ea", "html": "&#127480;&#127466;", "category": "Flags (country-flag)", "order": "2389"},
+ {"emoji": "🇸🇬", "name": "flag: Singapore", "shortname": ":flag-sg:", "unicode": "1f1f8-1f1ec", "html": "&#127480;&#127468;", "category": "Flags (country-flag)", "order": "2390"},
+ {"emoji": "🇸🇭", "name": "flag: St. Helena", "shortname": ":flag-sh:", "unicode": "1f1f8-1f1ed", "html": "&#127480;&#127469;", "category": "Flags (country-flag)", "order": "2391"},
+ {"emoji": "🇸🇮", "name": "flag: Slovenia", "shortname": ":flag-si:", "unicode": "1f1f8-1f1ee", "html": "&#127480;&#127470;", "category": "Flags (country-flag)", "order": "2392"},
+ {"emoji": "🇸🇯", "name": "flag: Svalbard & Jan Mayen", "shortname": ":flag-sj:", "unicode": "1f1f8-1f1ef", "html": "&#127480;&#127471;", "category": "Flags (country-flag)", "order": "2393"},
+ {"emoji": "🇸🇰", "name": "flag: Slovakia", "shortname": ":flag-sk:", "unicode": "1f1f8-1f1f0", "html": "&#127480;&#127472;", "category": "Flags (country-flag)", "order": "2394"},
+ {"emoji": "🇸🇱", "name": "flag: Sierra Leone", "shortname": ":flag-sl:", "unicode": "1f1f8-1f1f1", "html": "&#127480;&#127473;", "category": "Flags (country-flag)", "order": "2395"},
+ {"emoji": "🇸🇲", "name": "flag: San Marino", "shortname": ":flag-sm:", "unicode": "1f1f8-1f1f2", "html": "&#127480;&#127474;", "category": "Flags (country-flag)", "order": "2396"},
+ {"emoji": "🇸🇳", "name": "flag: Senegal", "shortname": ":flag-sn:", "unicode": "1f1f8-1f1f3", "html": "&#127480;&#127475;", "category": "Flags (country-flag)", "order": "2397"},
+ {"emoji": "🇸🇴", "name": "flag: Somalia", "shortname": ":flag-so:", "unicode": "1f1f8-1f1f4", "html": "&#127480;&#127476;", "category": "Flags (country-flag)", "order": "2398"},
+ {"emoji": "🇸🇷", "name": "flag: Suriname", "shortname": ":flag-sr:", "unicode": "1f1f8-1f1f7", "html": "&#127480;&#127479;", "category": "Flags (country-flag)", "order": "2399"},
+ {"emoji": "🇸🇸", "name": "flag: South Sudan", "shortname": ":flag-ss:", "unicode": "1f1f8-1f1f8", "html": "&#127480;&#127480;", "category": "Flags (country-flag)", "order": "2400"},
+ {"emoji": "🇸🇹", "name": "flag: São Tomé & Príncipe", "shortname": ":flag-st:", "unicode": "1f1f8-1f1f9", "html": "&#127480;&#127481;", "category": "Flags (country-flag)", "order": "2401"},
+ {"emoji": "🇸🇻", "name": "flag: El Salvador", "shortname": ":flag-sv:", "unicode": "1f1f8-1f1fb", "html": "&#127480;&#127483;", "category": "Flags (country-flag)", "order": "2402"},
+ {"emoji": "🇸🇽", "name": "flag: Sint Maarten", "shortname": ":flag-sx:", "unicode": "1f1f8-1f1fd", "html": "&#127480;&#127485;", "category": "Flags (country-flag)", "order": "2403"},
+ {"emoji": "🇸🇾", "name": "flag: Syria", "shortname": ":flag-sy:", "unicode": "1f1f8-1f1fe", "html": "&#127480;&#127486;", "category": "Flags (country-flag)", "order": "2404"},
+ {"emoji": "🇸🇿", "name": "flag: Eswatini", "shortname": ":flag-sz:", "unicode": "1f1f8-1f1ff", "html": "&#127480;&#127487;", "category": "Flags (country-flag)", "order": "2405"},
+ {"emoji": "🇹🇦", "name": "flag: Tristan da Cunha", "shortname": ":flag-ta:", "unicode": "1f1f9-1f1e6", "html": "&#127481;&#127462;", "category": "Flags (country-flag)", "order": "2406"},
+ {"emoji": "🇹🇨", "name": "flag: Turks & Caicos Islands", "shortname": ":flag-tc:", "unicode": "1f1f9-1f1e8", "html": "&#127481;&#127464;", "category": "Flags (country-flag)", "order": "2407"},
+ {"emoji": "🇹🇩", "name": "flag: Chad", "shortname": ":flag-td:", "unicode": "1f1f9-1f1e9", "html": "&#127481;&#127465;", "category": "Flags (country-flag)", "order": "2408"},
+ {"emoji": "🇹🇫", "name": "flag: French Southern Territories", "shortname": ":flag-tf:", "unicode": "1f1f9-1f1eb", "html": "&#127481;&#127467;", "category": "Flags (country-flag)", "order": "2409"},
+ {"emoji": "🇹🇬", "name": "flag: Togo", "shortname": ":flag-tg:", "unicode": "1f1f9-1f1ec", "html": "&#127481;&#127468;", "category": "Flags (country-flag)", "order": "2410"},
+ {"emoji": "🇹🇭", "name": "flag: Thailand", "shortname": ":flag-th:", "unicode": "1f1f9-1f1ed", "html": "&#127481;&#127469;", "category": "Flags (country-flag)", "order": "2411"},
+ {"emoji": "🇹🇯", "name": "flag: Tajikistan", "shortname": ":flag-tj:", "unicode": "1f1f9-1f1ef", "html": "&#127481;&#127471;", "category": "Flags (country-flag)", "order": "2412"},
+ {"emoji": "🇹🇰", "name": "flag: Tokelau", "shortname": ":flag-tk:", "unicode": "1f1f9-1f1f0", "html": "&#127481;&#127472;", "category": "Flags (country-flag)", "order": "2413"},
+ {"emoji": "🇹🇱", "name": "flag: Timor-Leste", "shortname": ":flag-tl:", "unicode": "1f1f9-1f1f1", "html": "&#127481;&#127473;", "category": "Flags (country-flag)", "order": "2414"},
+ {"emoji": "🇹🇲", "name": "flag: Turkmenistan", "shortname": ":flag-tm:", "unicode": "1f1f9-1f1f2", "html": "&#127481;&#127474;", "category": "Flags (country-flag)", "order": "2415"},
+ {"emoji": "🇹🇳", "name": "flag: Tunisia", "shortname": ":flag-tn:", "unicode": "1f1f9-1f1f3", "html": "&#127481;&#127475;", "category": "Flags (country-flag)", "order": "2416"},
+ {"emoji": "🇹🇴", "name": "flag: Tonga", "shortname": ":flag-to:", "unicode": "1f1f9-1f1f4", "html": "&#127481;&#127476;", "category": "Flags (country-flag)", "order": "2417"},
+ {"emoji": "🇹🇷", "name": "flag: Turkey", "shortname": ":flag-tr:", "unicode": "1f1f9-1f1f7", "html": "&#127481;&#127479;", "category": "Flags (country-flag)", "order": "2418"},
+ {"emoji": "🇹🇹", "name": "flag: Trinidad & Tobago", "shortname": ":flag-tt:", "unicode": "1f1f9-1f1f9", "html": "&#127481;&#127481;", "category": "Flags (country-flag)", "order": "2419"},
+ {"emoji": "🇹🇻", "name": "flag: Tuvalu", "shortname": ":flag-tv:", "unicode": "1f1f9-1f1fb", "html": "&#127481;&#127483;", "category": "Flags (country-flag)", "order": "2420"},
+ {"emoji": "🇹🇼", "name": "flag: Taiwan", "shortname": ":flag-tw:", "unicode": "1f1f9-1f1fc", "html": "&#127481;&#127484;", "category": "Flags (country-flag)", "order": "2421"},
+ {"emoji": "🇹🇿", "name": "flag: Tanzania", "shortname": ":flag-tz:", "unicode": "1f1f9-1f1ff", "html": "&#127481;&#127487;", "category": "Flags (country-flag)", "order": "2422"},
+ {"emoji": "🇺🇦", "name": "flag: Ukraine", "shortname": ":flag-ua:", "unicode": "1f1fa-1f1e6", "html": "&#127482;&#127462;", "category": "Flags (country-flag)", "order": "2423"},
+ {"emoji": "🇺🇬", "name": "flag: Uganda", "shortname": ":flag-ug:", "unicode": "1f1fa-1f1ec", "html": "&#127482;&#127468;", "category": "Flags (country-flag)", "order": "2424"},
+ {"emoji": "🇺🇲", "name": "flag: U.S. Outlying Islands", "shortname": ":flag-um:", "unicode": "1f1fa-1f1f2", "html": "&#127482;&#127474;", "category": "Flags (country-flag)", "order": "2425"},
+ {"emoji": "🇺🇸", "name": "flag: United States", "shortname": ":flag-us:", "unicode": "1f1fa-1f1f8", "html": "&#127482;&#127480;", "category": "Flags (country-flag)", "order": "2427"},
+ {"emoji": "🇺🇾", "name": "flag: Uruguay", "shortname": ":flag-uy:", "unicode": "1f1fa-1f1fe", "html": "&#127482;&#127486;", "category": "Flags (country-flag)", "order": "2428"},
+ {"emoji": "🇺🇿", "name": "flag: Uzbekistan", "shortname": ":flag-uz:", "unicode": "1f1fa-1f1ff", "html": "&#127482;&#127487;", "category": "Flags (country-flag)", "order": "2429"},
+ {"emoji": "🇻🇦", "name": "flag: Vatican City", "shortname": ":flag-va:", "unicode": "1f1fb-1f1e6", "html": "&#127483;&#127462;", "category": "Flags (country-flag)", "order": "2430"},
+ {"emoji": "🇻🇨", "name": "flag: St. Vincent & Grenadines", "shortname": ":flag-vc:", "unicode": "1f1fb-1f1e8", "html": "&#127483;&#127464;", "category": "Flags (country-flag)", "order": "2431"},
+ {"emoji": "🇻🇪", "name": "flag: Venezuela", "shortname": ":flag-ve:", "unicode": "1f1fb-1f1ea", "html": "&#127483;&#127466;", "category": "Flags (country-flag)", "order": "2432"},
+ {"emoji": "🇻🇬", "name": "flag: British Virgin Islands", "shortname": ":flag-vg:", "unicode": "1f1fb-1f1ec", "html": "&#127483;&#127468;", "category": "Flags (country-flag)", "order": "2433"},
+ {"emoji": "🇻🇮", "name": "flag: U.S. Virgin Islands", "shortname": ":flag-vi:", "unicode": "1f1fb-1f1ee", "html": "&#127483;&#127470;", "category": "Flags (country-flag)", "order": "2434"},
+ {"emoji": "🇻🇳", "name": "flag: Vietnam", "shortname": ":flag-vn:", "unicode": "1f1fb-1f1f3", "html": "&#127483;&#127475;", "category": "Flags (country-flag)", "order": "2435"},
+ {"emoji": "🇻🇺", "name": "flag: Vanuatu", "shortname": ":flag_vu:", "unicode": "1f1fb-1f1fa", "html": "&#127483;&#127482;", "category": "Flags (country-flag)", "order": "2436"},
+ {"emoji": "🇼🇫", "name": "flag: Wallis & Futuna", "shortname": ":flag_wf:", "unicode": "1f1fc-1f1eb", "html": "&#127484;&#127467;", "category": "Flags (country-flag)", "order": "2437"},
+ {"emoji": "🇼🇸", "name": "flag: Samoa", "shortname": ":flag_ws:", "unicode": "1f1fc-1f1f8", "html": "&#127484;&#127480;", "category": "Flags (country-flag)", "order": "2438"},
+ {"emoji": "🇽🇰", "name": "flag: Kosovo", "shortname": ":flag_xk:", "unicode": "1f1fd-1f1f0", "html": "&#127485;&#127472;", "category": "Flags (country-flag)", "order": "2439"},
+ {"emoji": "🇾🇪", "name": "flag: Yemen", "shortname": ":flag_ye:", "unicode": "1f1fe-1f1ea", "html": "&#127486;&#127466;", "category": "Flags (country-flag)", "order": "2440"},
+ {"emoji": "🇾🇹", "name": "flag: Mayotte", "shortname": ":flag_yt:", "unicode": "1f1fe-1f1f9", "html": "&#127486;&#127481;", "category": "Flags (country-flag)", "order": "2441"},
+ {"emoji": "🇿🇦", "name": "flag: South Africa", "shortname": ":flag_za:", "unicode": "1f1ff-1f1e6", "html": "&#127487;&#127462;", "category": "Flags (country-flag)", "order": "2442"},
+ {"emoji": "🇿🇲", "name": "flag: Zambia", "shortname": ":flag_zm:", "unicode": "1f1ff-1f1f2", "html": "&#127487;&#127474;", "category": "Flags (country-flag)", "order": "2443"},
+ {"emoji": "🇿🇼", "name": "flag: Zimbabwe", "shortname": ":flag_zw:", "unicode": "1f1ff-1f1fc", "html": "&#127487;&#127484;", "category": "Flags (country-flag)", "order": "2444"},
+ {"emoji": "🖤", "name": "black heart", "shortname": ":black_heart:", "unicode": "1f5a4", "html": "&#128420;", "category": "Smileys & Emotion (emotion)", "order": "1296"},
+ {"emoji": "🗨", "name": "left speech bubble", "shortname": ":speech_left:", "unicode": "1f5e8", "html": "&#128488;", "category": "Smileys & Emotion (emotion)", "order": "1310"},
+ {"emoji": "🥚", "name": "egg", "shortname": ":egg:", "unicode": "1f95a", "html": "&#129370;", "category": "Food & Drink (food-prepared)", "order": "1489"},
+ {"emoji": "🛑", "name": "stop sign", "shortname": ":octagonal_sign:", "unicode": "1f6d1", "html": "&#128721;", "category": "Travel & Places (transport-ground)", "order": "1641"},
+ {"emoji": "♠", "name": "spade suit", "shortname": ":spades:", "unicode": "2660", "html": "&spades;", "category": "Activities (game)", "order": "1807"},
+ {"emoji": "♥", "name": "heart suit", "shortname": ":hearts:", "unicode": "2665", "html": "&hearts;", "category": "Activities (game)", "order": "1808"},
+ {"emoji": "♦", "name": "diamond suit", "shortname": ":diamonds:", "unicode": "2666", "html": "&diams;", "category": "Activities (game)", "order": "1809"},
+ {"emoji": "♣", "name": "club suit", "shortname": ":clubs:", "unicode": "2663", "html": "&clubs;", "category": "Activities (game)", "order": "1810"},
+ {"emoji": "🥁", "name": "drum", "shortname": ":drum:", "unicode": "1f941", "html": "&#129345;", "category": "Objects (musical-instrument)", "order": "1837"},
+ {"emoji": "↔", "name": "left-right arrow", "shortname": ":left_right_arrow:", "unicode": "2194", "html": "&harr;", "category": "Symbols (arrow)", "order": "2011"},
+ {"emoji": "©", "name": "copyright", "shortname": ":copyright:", "unicode": "00a9", "html": "&copy;", "category": "Symbols (other-symbol)", "order": "2103"},
+ {"emoji": "®", "name": "registered", "shortname": ":registered:", "unicode": "00ae", "html": "&reg;", "category": "Symbols (other-symbol)", "order": "2104"},
+ {"emoji": "™", "name": "trade mark", "shortname": ":tm:", "unicode": "2122", "html": "&trade;", "category": "Symbols (other-symbol)", "order": "2105"},
+ {"emoji": "0️⃣", "name": "keycap: 0", "shortname": ":0:", "unicode": "0030 FE0F 20E3", "html": "&#48;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "1️⃣", "name": "keycap: 1", "shortname": ":1:", "unicode": "0031 FE0F 20E3", "html": "&#49;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "2️⃣", "name": "keycap: 2", "shortname": ":2:", "unicode": "0032 FE0F 20E3", "html": "&#50;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "3️⃣", "name": "keycap: 3", "shortname": ":3:", "unicode": "0033 FE0F 20E3", "html": "&#51;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "4️⃣", "name": "keycap: 4", "shortname": ":4:", "unicode": "0034 FE0F 20E3", "html": "&#52;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "5️⃣", "name": "keycap: 5", "shortname": ":5:", "unicode": "0035 FE0F 20E3", "html": "&#53;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "6️⃣", "name": "keycap: 6", "shortname": ":6:", "unicode": "0036 FE0F 20E3", "html": "&#54;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "7️⃣", "name": "keycap: 7", "shortname": ":7:", "unicode": "0037 FE0F 20E3", "html": "&#55;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "8️⃣", "name": "keycap: 8", "shortname": ":8:", "unicode": "0038 FE0F 20E3", "html": "&#56;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "9️⃣", "name": "keycap: 9", "shortname": ":9:", "unicode": "0039 FE0F 20E3", "html": "&#57;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "#⃣", "name": "keycap: #", "shortname": ":hash:", "unicode": "0023-20e3", "html": "#&#8419;", "category": "Symbols (keycap)", "order": "2106"},
+ {"emoji": "*⃣", "name": "keycap: #", "shortname": ":asterisk:", "unicode": "002a-20e3", "html": "*&#8419;", "category": "Symbols (keycap)", "order": "2107"},
+ {"emoji": "0⃣", "name": "keycap: 0", "shortname": ":zero:", "unicode": "0030-20e3", "html": "0&#8419;", "category": "Symbols (keycap)", "order": "2108"},
+ {"emoji": "1⃣", "name": "keycap: 1", "shortname": ":one:", "unicode": "0031-20e3", "html": "1&#8419;", "category": "Symbols (keycap)", "order": "2109"},
+ {"emoji": "2⃣", "name": "keycap: 2", "shortname": ":two:", "unicode": "0032-20e3", "html": "2&#8419;", "category": "Symbols (keycap)", "order": "2110"},
+ {"emoji": "3⃣", "name": "keycap: 3", "shortname": ":three:", "unicode": "0033-20e3", "html": "3&#8419;", "category": "Symbols (keycap)", "order": "2111"},
+ {"emoji": "4⃣", "name": "keycap: 4", "shortname": ":four:", "unicode": "0034-20e3", "html": "4&#8419;", "category": "Symbols (keycap)", "order": "2112"},
+ {"emoji": "5⃣", "name": "keycap: 5", "shortname": ":five:", "unicode": "0035-20e3", "html": "5&#8419;", "category": "Symbols (keycap)", "order": "2113"},
+ {"emoji": "6⃣", "name": "keycap: 6", "shortname": ":six:", "unicode": "0036-20e3", "html": "6&#8419;", "category": "Symbols (keycap)", "order": "2114"},
+ {"emoji": "7⃣", "name": "keycap: 7", "shortname": ":seven:", "unicode": "0037-20e3", "html": "7&#8419;", "category": "Symbols (keycap)", "order": "2115"},
+ {"emoji": "8⃣", "name": "keycap: 8", "shortname": ":eight:", "unicode": "0038-20e3", "html": "8&#8419;", "category": "Symbols (keycap)", "order": "2116"},
+ {"emoji": "9⃣", "name": "keycap: 9", "shortname": ":nine:", "unicode": "0039-20e3", "html": "9&#8419;", "category": "Symbols (keycap)", "order": "2117"},
+ {"emoji": "🤣", "name": "rolling on the floor laughing", "shortname": ":rolling_on_the_floor_laughing:", "unicode": "1F923", "html": "&#x1F923;", "category": "Smileys & Emotion (face-smiling)", "order": ""},
+ {"emoji": "🥰", "name": "smiling face with hearts", "shortname": ":smiling_face_with_hearts:", "unicode": "1F970", "html": "&#x1F970;", "category": "Smileys & Emotion (face-affection)", "order": ""},
+ {"emoji": "🤩", "name": "star-struck", "shortname": ":starstruck:", "unicode": "1F929", "html": "&#x1F929;", "category": "Smileys & Emotion (face-affection)", "order": ""},
+ {"emoji": "☺", "name": "smiling face", "shortname": ":smiling_face:", "unicode": "263A", "html": "&#x263A;", "category": "Smileys & Emotion (face-affection)", "order": ""},
+ {"emoji": "🤪", "name": "zany face", "shortname": ":zany_face:", "unicode": "1F92A", "html": "&#x1F92A;", "category": "Smileys & Emotion (face-tongue)", "order": ""},
+ {"emoji": "🤭", "name": "face with hand over mouth", "shortname": ":face_with_hand_over_mouth:", "unicode": "1F92D", "html": "&#x1F92D;", "category": "Smileys & Emotion (face-hand)", "order": ""},
+ {"emoji": "🤫", "name": "shushing face", "shortname": ":shushing_face:", "unicode": "1F92B", "html": "&#x1F92B;", "category": "Smileys & Emotion (face-hand)", "order": ""},
+ {"emoji": "🤨", "name": "face with raised eyebrow", "shortname": ":face_with_raised_eyebrow:", "unicode": "1F928", "html": "&#x1F928;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": ""},
+ {"emoji": "🤥", "name": "lying face", "shortname": ":lying_face:", "unicode": "1F925", "html": "&#x1F925;", "category": "Smileys & Emotion (face-neutral-skeptical)", "order": ""},
+ {"emoji": "🤤", "name": "drooling face", "shortname": ":drooling_face:", "unicode": "1F924", "html": "&#x1F924;", "category": "Smileys & Emotion (face-sleepy)", "order": ""},
+ {"emoji": "🤢", "name": "nauseated face", "shortname": ":nauseated_face:", "unicode": "1F922", "html": "&#x1F922;", "category": "Smileys & Emotion (face-unwell)", "order": ""},
+ {"emoji": "🤮", "name": "face vomiting", "shortname": ":face_vomiting:", "unicode": "1F92E", "html": "&#x1F92E;", "category": "Smileys & Emotion (face-unwell)", "order": ""},
+ {"emoji": "🤧", "name": "sneezing face", "shortname": ":sneezing_face:", "unicode": "1F927", "html": "&#x1F927;", "category": "Smileys & Emotion (face-unwell)", "order": ""},
+ {"emoji": "🥵", "name": "hot face", "shortname": ":hot_face:", "unicode": "1F975", "html": "&#x1F975;", "category": "Smileys & Emotion (face-unwell)", "order": ""},
+ {"emoji": "🥶", "name": "cold face", "shortname": ":cold_face:", "unicode": "1F976", "html": "&#x1F976;", "category": "Smileys & Emotion (face-unwell)", "order": ""},
+ {"emoji": "🥴", "name": "woozy face", "shortname": ":woozy_face:", "unicode": "1F974", "html": "&#x1F974;", "category": "Smileys & Emotion (face-unwell)", "order": ""},
+ {"emoji": "🤯", "name": "exploding head", "shortname": ":exploding_head:", "unicode": "1F92F", "html": "&#x1F92F;", "category": "Smileys & Emotion (face-unwell)", "order": ""},
+ {"emoji": "🤠", "name": "cowboy hat face", "shortname": ":cowboy_hat_face:", "unicode": "1F920", "html": "&#x1F920;", "category": "Smileys & Emotion (face-hat)", "order": ""},
+ {"emoji": "🥳", "name": "partying face", "shortname": ":partying_face:", "unicode": "1F973", "html": "&#x1F973;", "category": "Smileys & Emotion (face-hat)", "order": ""},
+ {"emoji": "🧐", "name": "face with monocle", "shortname": ":face_with_monocle:", "unicode": "1F9D0", "html": "&#x1F9D0;", "category": "Smileys & Emotion (face-glasses)", "order": ""},
+ {"emoji": "☹️", "name": "frowning face", "shortname": ":frowning_face:", "unicode": "2639 FE0F", "html": "&#x2639;&#xFE0F;", "category": "Smileys & Emotion (face-concerned)", "order": ""},
+ {"emoji": "🥺", "name": "pleading face", "shortname": ":pleading_face:", "unicode": "1F97A", "html": "&#x1F97A;", "category": "Smileys & Emotion (face-concerned)", "order": ""},
+ {"emoji": "🥱", "name": "yawning face", "shortname": ":yawning_face:", "unicode": "1F971", "html": "&#x1F971;", "category": "Smileys & Emotion (face-concerned)", "order": ""},
+ {"emoji": "🤬", "name": "face with symbols on mouth", "shortname": ":face_with_symbols_on_mouth:", "unicode": "1F92C", "html": "&#x1F92C;", "category": "Smileys & Emotion (face-negative)", "order": ""},
+ {"emoji": "☠️", "name": "skull and crossbones", "shortname": ":skull_and_crossbones:", "unicode": "2620 FE0F", "html": "&#x2620;&#xFE0F;", "category": "Smileys & Emotion (face-negative)", "order": ""},
+ {"emoji": "🤡", "name": "clown face", "shortname": ":clown_face:", "unicode": "1F921", "html": "&#x1F921;", "category": "Smileys & Emotion (face-costume)", "order": ""},
+ {"emoji": "❣", "name": "heart exclamation", "shortname": ":heart_exclamation:", "unicode": "2763", "html": "&#x2763;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "❤", "name": "red heart", "shortname": ":red_heart:", "unicode": "2764", "html": "&#x2764;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "🧡", "name": "orange heart", "shortname": ":orange_heart:", "unicode": "1F9E1", "html": "&#x1F9E1;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "🤎", "name": "brown heart", "shortname": ":brown_heart:", "unicode": "1F90E", "html": "&#x1F90E;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "🤍", "name": "white heart", "shortname": ":white_heart:", "unicode": "1F90D", "html": "&#x1F90D;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "🕳️", "name": "hole", "shortname": ":hole:", "unicode": "1F573 FE0F", "html": "&#x1F573;&#xFE0F;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "👁️‍🗨️", "name": "eye in speech bubble", "shortname": ":eye_in_speech_bubble:", "unicode": "1F441 FE0F 200D 1F5E8 FE0F", "html": "&#x1F441;&#xFE0F;&#x200D;&#x1F5E8;&#xFE0F;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "👁‍🗨️", "name": "eye in speech bubble", "shortname": ":eye_in_speech_bubble:", "unicode": "1F441 200D 1F5E8 FE0F", "html": "&#x1F441;&#x200D;&#x1F5E8;&#xFE0F;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "👁️‍🗨", "name": "eye in speech bubble", "shortname": ":eye_in_speech_bubble:", "unicode": "1F441 FE0F 200D 1F5E8", "html": "&#x1F441;&#xFE0F;&#x200D;&#x1F5E8;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "🗨️", "name": "left speech bubble", "shortname": ":left_speech_bubble:", "unicode": "1F5E8 FE0F", "html": "&#x1F5E8;&#xFE0F;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "🗯️", "name": "right anger bubble", "shortname": ":right_anger_bubble:", "unicode": "1F5EF FE0F", "html": "&#x1F5EF;&#xFE0F;", "category": "Smileys & Emotion (emotion)", "order": ""},
+ {"emoji": "🤚", "name": "raised back of hand", "shortname": ":raised_back_of_hand:", "unicode": "1F91A", "html": "&#x1F91A;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🤚🏻", "name": "raised back of hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F91A 1F3FB", "html": "&#x1F91A;&#x1F3FB;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🤚🏼", "name": "raised back of hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F91A 1F3FC", "html": "&#x1F91A;&#x1F3FC;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🤚🏽", "name": "raised back of hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F91A 1F3FD", "html": "&#x1F91A;&#x1F3FD;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🤚🏾", "name": "raised back of hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F91A 1F3FE", "html": "&#x1F91A;&#x1F3FE;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🤚🏿", "name": "raised back of hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F91A 1F3FF", "html": "&#x1F91A;&#x1F3FF;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🖐️", "name": "hand with fingers splayed", "shortname": ":hand_with_fingers_splayed:", "unicode": "1F590 FE0F", "html": "&#x1F590;&#xFE0F;", "category": "People & Body (hand-fingers-open)", "order": ""},
+ {"emoji": "🤏", "name": "pinching hand", "shortname": ":pinching_hand:", "unicode": "1F90F", "html": "&#x1F90F;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤏🏻", "name": "pinching hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F90F 1F3FB", "html": "&#x1F90F;&#x1F3FB;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤏🏼", "name": "pinching hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F90F 1F3FC", "html": "&#x1F90F;&#x1F3FC;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤏🏽", "name": "pinching hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F90F 1F3FD", "html": "&#x1F90F;&#x1F3FD;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤏🏾", "name": "pinching hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F90F 1F3FE", "html": "&#x1F90F;&#x1F3FE;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤏🏿", "name": "pinching hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F90F 1F3FF", "html": "&#x1F90F;&#x1F3FF;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "✌", "name": "victory hand", "shortname": ":victory_hand:", "unicode": "270C", "html": "&#x270C;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤞", "name": "crossed fingers", "shortname": ":crossed_fingers:", "unicode": "1F91E", "html": "&#x1F91E;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤞🏻", "name": "crossed fingers: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F91E 1F3FB", "html": "&#x1F91E;&#x1F3FB;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤞🏼", "name": "crossed fingers: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F91E 1F3FC", "html": "&#x1F91E;&#x1F3FC;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤞🏽", "name": "crossed fingers: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F91E 1F3FD", "html": "&#x1F91E;&#x1F3FD;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤞🏾", "name": "crossed fingers: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F91E 1F3FE", "html": "&#x1F91E;&#x1F3FE;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤞🏿", "name": "crossed fingers: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F91E 1F3FF", "html": "&#x1F91E;&#x1F3FF;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤟", "name": "love-you gesture", "shortname": ":loveyou_gesture:", "unicode": "1F91F", "html": "&#x1F91F;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤟🏻", "name": "love-you gesture: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F91F 1F3FB", "html": "&#x1F91F;&#x1F3FB;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤟🏼", "name": "love-you gesture: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F91F 1F3FC", "html": "&#x1F91F;&#x1F3FC;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤟🏽", "name": "love-you gesture: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F91F 1F3FD", "html": "&#x1F91F;&#x1F3FD;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤟🏾", "name": "love-you gesture: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F91F 1F3FE", "html": "&#x1F91F;&#x1F3FE;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤟🏿", "name": "love-you gesture: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F91F 1F3FF", "html": "&#x1F91F;&#x1F3FF;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤙", "name": "call me hand", "shortname": ":call_me_hand:", "unicode": "1F919", "html": "&#x1F919;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤙🏻", "name": "call me hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F919 1F3FB", "html": "&#x1F919;&#x1F3FB;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤙🏼", "name": "call me hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F919 1F3FC", "html": "&#x1F919;&#x1F3FC;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤙🏽", "name": "call me hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F919 1F3FD", "html": "&#x1F919;&#x1F3FD;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤙🏾", "name": "call me hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F919 1F3FE", "html": "&#x1F919;&#x1F3FE;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "🤙🏿", "name": "call me hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F919 1F3FF", "html": "&#x1F919;&#x1F3FF;", "category": "People & Body (hand-fingers-partial)", "order": ""},
+ {"emoji": "☝", "name": "index pointing up", "shortname": ":index_pointing_up:", "unicode": "261D", "html": "&#x261D;", "category": "People & Body (hand-single-finger)", "order": ""},
+ {"emoji": "🤛", "name": "left-facing fist", "shortname": ":leftfacing_fist:", "unicode": "1F91B", "html": "&#x1F91B;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤛🏻", "name": "left-facing fist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F91B 1F3FB", "html": "&#x1F91B;&#x1F3FB;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤛🏼", "name": "left-facing fist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F91B 1F3FC", "html": "&#x1F91B;&#x1F3FC;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤛🏽", "name": "left-facing fist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F91B 1F3FD", "html": "&#x1F91B;&#x1F3FD;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤛🏾", "name": "left-facing fist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F91B 1F3FE", "html": "&#x1F91B;&#x1F3FE;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤛🏿", "name": "left-facing fist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F91B 1F3FF", "html": "&#x1F91B;&#x1F3FF;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤜", "name": "right-facing fist", "shortname": ":rightfacing_fist:", "unicode": "1F91C", "html": "&#x1F91C;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤜🏻", "name": "right-facing fist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F91C 1F3FB", "html": "&#x1F91C;&#x1F3FB;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤜🏼", "name": "right-facing fist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F91C 1F3FC", "html": "&#x1F91C;&#x1F3FC;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤜🏽", "name": "right-facing fist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F91C 1F3FD", "html": "&#x1F91C;&#x1F3FD;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤜🏾", "name": "right-facing fist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F91C 1F3FE", "html": "&#x1F91C;&#x1F3FE;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤜🏿", "name": "right-facing fist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F91C 1F3FF", "html": "&#x1F91C;&#x1F3FF;", "category": "People & Body (hand-fingers-closed)", "order": ""},
+ {"emoji": "🤲", "name": "palms up together", "shortname": ":palms_up_together:", "unicode": "1F932", "html": "&#x1F932;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🤲🏻", "name": "palms up together: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F932 1F3FB", "html": "&#x1F932;&#x1F3FB;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🤲🏼", "name": "palms up together: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F932 1F3FC", "html": "&#x1F932;&#x1F3FC;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🤲🏽", "name": "palms up together: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F932 1F3FD", "html": "&#x1F932;&#x1F3FD;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🤲🏾", "name": "palms up together: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F932 1F3FE", "html": "&#x1F932;&#x1F3FE;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🤲🏿", "name": "palms up together: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F932 1F3FF", "html": "&#x1F932;&#x1F3FF;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "🤝", "name": "handshake", "shortname": ":handshake:", "unicode": "1F91D", "html": "&#x1F91D;", "category": "People & Body (hands)", "order": ""},
+ {"emoji": "✍️", "name": "writing hand", "shortname": ":writing_hand:", "unicode": "270D FE0F", "html": "&#x270D;&#xFE0F;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "🤳", "name": "selfie", "shortname": ":selfie:", "unicode": "1F933", "html": "&#x1F933;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "🤳🏻", "name": "selfie: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F933 1F3FB", "html": "&#x1F933;&#x1F3FB;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "🤳🏼", "name": "selfie: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F933 1F3FC", "html": "&#x1F933;&#x1F3FC;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "🤳🏽", "name": "selfie: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F933 1F3FD", "html": "&#x1F933;&#x1F3FD;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "🤳🏾", "name": "selfie: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F933 1F3FE", "html": "&#x1F933;&#x1F3FE;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "🤳🏿", "name": "selfie: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F933 1F3FF", "html": "&#x1F933;&#x1F3FF;", "category": "People & Body (hand-prop)", "order": ""},
+ {"emoji": "🦾", "name": "mechanical arm", "shortname": ":mechanical_arm:", "unicode": "1F9BE", "html": "&#x1F9BE;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦿", "name": "mechanical leg", "shortname": ":mechanical_leg:", "unicode": "1F9BF", "html": "&#x1F9BF;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦵", "name": "leg", "shortname": ":leg:", "unicode": "1F9B5", "html": "&#x1F9B5;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦵🏻", "name": "leg: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B5 1F3FB", "html": "&#x1F9B5;&#x1F3FB;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦵🏼", "name": "leg: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B5 1F3FC", "html": "&#x1F9B5;&#x1F3FC;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦵🏽", "name": "leg: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B5 1F3FD", "html": "&#x1F9B5;&#x1F3FD;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦵🏾", "name": "leg: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B5 1F3FE", "html": "&#x1F9B5;&#x1F3FE;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦵🏿", "name": "leg: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B5 1F3FF", "html": "&#x1F9B5;&#x1F3FF;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦶", "name": "foot", "shortname": ":foot:", "unicode": "1F9B6", "html": "&#x1F9B6;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦶🏻", "name": "foot: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B6 1F3FB", "html": "&#x1F9B6;&#x1F3FB;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦶🏼", "name": "foot: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B6 1F3FC", "html": "&#x1F9B6;&#x1F3FC;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦶🏽", "name": "foot: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B6 1F3FD", "html": "&#x1F9B6;&#x1F3FD;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦶🏾", "name": "foot: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B6 1F3FE", "html": "&#x1F9B6;&#x1F3FE;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦶🏿", "name": "foot: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B6 1F3FF", "html": "&#x1F9B6;&#x1F3FF;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦻", "name": "ear with hearing aid", "shortname": ":ear_with_hearing_aid:", "unicode": "1F9BB", "html": "&#x1F9BB;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦻🏻", "name": "ear with hearing aid: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9BB 1F3FB", "html": "&#x1F9BB;&#x1F3FB;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦻🏼", "name": "ear with hearing aid: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9BB 1F3FC", "html": "&#x1F9BB;&#x1F3FC;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦻🏽", "name": "ear with hearing aid: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9BB 1F3FD", "html": "&#x1F9BB;&#x1F3FD;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦻🏾", "name": "ear with hearing aid: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9BB 1F3FE", "html": "&#x1F9BB;&#x1F3FE;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦻🏿", "name": "ear with hearing aid: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9BB 1F3FF", "html": "&#x1F9BB;&#x1F3FF;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🧠", "name": "brain", "shortname": ":brain:", "unicode": "1F9E0", "html": "&#x1F9E0;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦷", "name": "tooth", "shortname": ":tooth:", "unicode": "1F9B7", "html": "&#x1F9B7;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🦴", "name": "bone", "shortname": ":bone:", "unicode": "1F9B4", "html": "&#x1F9B4;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "👁️", "name": "eye", "shortname": ":eye:", "unicode": "1F441 FE0F", "html": "&#x1F441;&#xFE0F;", "category": "People & Body (body-parts)", "order": ""},
+ {"emoji": "🧒", "name": "child", "shortname": ":child:", "unicode": "1F9D2", "html": "&#x1F9D2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧒🏻", "name": "child: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D2 1F3FB", "html": "&#x1F9D2;&#x1F3FB;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧒🏼", "name": "child: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D2 1F3FC", "html": "&#x1F9D2;&#x1F3FC;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧒🏽", "name": "child: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D2 1F3FD", "html": "&#x1F9D2;&#x1F3FD;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧒🏾", "name": "child: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D2 1F3FE", "html": "&#x1F9D2;&#x1F3FE;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧒🏿", "name": "child: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D2 1F3FF", "html": "&#x1F9D2;&#x1F3FF;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑", "name": "person", "shortname": ":person:", "unicode": "1F9D1", "html": "&#x1F9D1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏻", "name": "person: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB", "html": "&#x1F9D1;&#x1F3FB;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏼", "name": "person: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC", "html": "&#x1F9D1;&#x1F3FC;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏽", "name": "person: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD", "html": "&#x1F9D1;&#x1F3FD;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏾", "name": "person: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE", "html": "&#x1F9D1;&#x1F3FE;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏿", "name": "person: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF", "html": "&#x1F9D1;&#x1F3FF;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧔", "name": "man: beard", "shortname": ":beard:", "unicode": "1F9D4", "html": "&#x1F9D4;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧔🏻", "name": "man: light skin tone, beard", "shortname": ":light_skin_tone_beard:", "unicode": "1F9D4 1F3FB", "html": "&#x1F9D4;&#x1F3FB;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧔🏼", "name": "man: medium-light skin tone, beard", "shortname": ":mediumlight_skin_tone_beard:", "unicode": "1F9D4 1F3FC", "html": "&#x1F9D4;&#x1F3FC;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧔🏽", "name": "man: medium skin tone, beard", "shortname": ":medium_skin_tone_beard:", "unicode": "1F9D4 1F3FD", "html": "&#x1F9D4;&#x1F3FD;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧔🏾", "name": "man: medium-dark skin tone, beard", "shortname": ":mediumdark_skin_tone_beard:", "unicode": "1F9D4 1F3FE", "html": "&#x1F9D4;&#x1F3FE;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧔🏿", "name": "man: dark skin tone, beard", "shortname": ":dark_skin_tone_beard:", "unicode": "1F9D4 1F3FF", "html": "&#x1F9D4;&#x1F3FF;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨‍🦰", "name": "man: red hair", "shortname": ":red_hair:", "unicode": "1F468 200D 1F9B0", "html": "&#x1F468;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏻‍🦰", "name": "man: light skin tone, red hair", "shortname": ":light_skin_tone_red_hair:", "unicode": "1F468 1F3FB 200D 1F9B0", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏼‍🦰", "name": "man: medium-light skin tone, red hair", "shortname": ":mediumlight_skin_tone_red_hair:", "unicode": "1F468 1F3FC 200D 1F9B0", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏽‍🦰", "name": "man: medium skin tone, red hair", "shortname": ":medium_skin_tone_red_hair:", "unicode": "1F468 1F3FD 200D 1F9B0", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏾‍🦰", "name": "man: medium-dark skin tone, red hair", "shortname": ":mediumdark_skin_tone_red_hair:", "unicode": "1F468 1F3FE 200D 1F9B0", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏿‍🦰", "name": "man: dark skin tone, red hair", "shortname": ":dark_skin_tone_red_hair:", "unicode": "1F468 1F3FF 200D 1F9B0", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨‍🦱", "name": "man: curly hair", "shortname": ":curly_hair:", "unicode": "1F468 200D 1F9B1", "html": "&#x1F468;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏻‍🦱", "name": "man: light skin tone, curly hair", "shortname": ":light_skin_tone_curly_hair:", "unicode": "1F468 1F3FB 200D 1F9B1", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏼‍🦱", "name": "man: medium-light skin tone, curly hair", "shortname": ":mediumlight_skin_tone_curly_hair:", "unicode": "1F468 1F3FC 200D 1F9B1", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏽‍🦱", "name": "man: medium skin tone, curly hair", "shortname": ":medium_skin_tone_curly_hair:", "unicode": "1F468 1F3FD 200D 1F9B1", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏾‍🦱", "name": "man: medium-dark skin tone, curly hair", "shortname": ":mediumdark_skin_tone_curly_hair:", "unicode": "1F468 1F3FE 200D 1F9B1", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏿‍🦱", "name": "man: dark skin tone, curly hair", "shortname": ":dark_skin_tone_curly_hair:", "unicode": "1F468 1F3FF 200D 1F9B1", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨‍🦳", "name": "man: white hair", "shortname": ":white_hair:", "unicode": "1F468 200D 1F9B3", "html": "&#x1F468;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏻‍🦳", "name": "man: light skin tone, white hair", "shortname": ":light_skin_tone_white_hair:", "unicode": "1F468 1F3FB 200D 1F9B3", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏼‍🦳", "name": "man: medium-light skin tone, white hair", "shortname": ":mediumlight_skin_tone_white_hair:", "unicode": "1F468 1F3FC 200D 1F9B3", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏽‍🦳", "name": "man: medium skin tone, white hair", "shortname": ":medium_skin_tone_white_hair:", "unicode": "1F468 1F3FD 200D 1F9B3", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏾‍🦳", "name": "man: medium-dark skin tone, white hair", "shortname": ":mediumdark_skin_tone_white_hair:", "unicode": "1F468 1F3FE 200D 1F9B3", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏿‍🦳", "name": "man: dark skin tone, white hair", "shortname": ":dark_skin_tone_white_hair:", "unicode": "1F468 1F3FF 200D 1F9B3", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨‍🦲", "name": "man: bald", "shortname": ":bald:", "unicode": "1F468 200D 1F9B2", "html": "&#x1F468;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏻‍🦲", "name": "man: light skin tone, bald", "shortname": ":light_skin_tone_bald:", "unicode": "1F468 1F3FB 200D 1F9B2", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏼‍🦲", "name": "man: medium-light skin tone, bald", "shortname": ":mediumlight_skin_tone_bald:", "unicode": "1F468 1F3FC 200D 1F9B2", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏽‍🦲", "name": "man: medium skin tone, bald", "shortname": ":medium_skin_tone_bald:", "unicode": "1F468 1F3FD 200D 1F9B2", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏾‍🦲", "name": "man: medium-dark skin tone, bald", "shortname": ":mediumdark_skin_tone_bald:", "unicode": "1F468 1F3FE 200D 1F9B2", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👨🏿‍🦲", "name": "man: dark skin tone, bald", "shortname": ":dark_skin_tone_bald:", "unicode": "1F468 1F3FF 200D 1F9B2", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩‍🦰", "name": "woman: red hair", "shortname": ":red_hair:", "unicode": "1F469 200D 1F9B0", "html": "&#x1F469;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏻‍🦰", "name": "woman: light skin tone, red hair", "shortname": ":light_skin_tone_red_hair:", "unicode": "1F469 1F3FB 200D 1F9B0", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏼‍🦰", "name": "woman: medium-light skin tone, red hair", "shortname": ":mediumlight_skin_tone_red_hair:", "unicode": "1F469 1F3FC 200D 1F9B0", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏽‍🦰", "name": "woman: medium skin tone, red hair", "shortname": ":medium_skin_tone_red_hair:", "unicode": "1F469 1F3FD 200D 1F9B0", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏾‍🦰", "name": "woman: medium-dark skin tone, red hair", "shortname": ":mediumdark_skin_tone_red_hair:", "unicode": "1F469 1F3FE 200D 1F9B0", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏿‍🦰", "name": "woman: dark skin tone, red hair", "shortname": ":dark_skin_tone_red_hair:", "unicode": "1F469 1F3FF 200D 1F9B0", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑‍🦰", "name": "person: red hair", "shortname": ":red_hair:", "unicode": "1F9D1 200D 1F9B0", "html": "&#x1F9D1;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏻‍🦰", "name": "person: light skin tone, red hair", "shortname": ":light_skin_tone_red_hair:", "unicode": "1F9D1 1F3FB 200D 1F9B0", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏼‍🦰", "name": "person: medium-light skin tone, red hair", "shortname": ":mediumlight_skin_tone_red_hair:", "unicode": "1F9D1 1F3FC 200D 1F9B0", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏽‍🦰", "name": "person: medium skin tone, red hair", "shortname": ":medium_skin_tone_red_hair:", "unicode": "1F9D1 1F3FD 200D 1F9B0", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏾‍🦰", "name": "person: medium-dark skin tone, red hair", "shortname": ":mediumdark_skin_tone_red_hair:", "unicode": "1F9D1 1F3FE 200D 1F9B0", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏿‍🦰", "name": "person: dark skin tone, red hair", "shortname": ":dark_skin_tone_red_hair:", "unicode": "1F9D1 1F3FF 200D 1F9B0", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F9B0;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩‍🦱", "name": "woman: curly hair", "shortname": ":curly_hair:", "unicode": "1F469 200D 1F9B1", "html": "&#x1F469;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏻‍🦱", "name": "woman: light skin tone, curly hair", "shortname": ":light_skin_tone_curly_hair:", "unicode": "1F469 1F3FB 200D 1F9B1", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏼‍🦱", "name": "woman: medium-light skin tone, curly hair", "shortname": ":mediumlight_skin_tone_curly_hair:", "unicode": "1F469 1F3FC 200D 1F9B1", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏽‍🦱", "name": "woman: medium skin tone, curly hair", "shortname": ":medium_skin_tone_curly_hair:", "unicode": "1F469 1F3FD 200D 1F9B1", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏾‍🦱", "name": "woman: medium-dark skin tone, curly hair", "shortname": ":mediumdark_skin_tone_curly_hair:", "unicode": "1F469 1F3FE 200D 1F9B1", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏿‍🦱", "name": "woman: dark skin tone, curly hair", "shortname": ":dark_skin_tone_curly_hair:", "unicode": "1F469 1F3FF 200D 1F9B1", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑‍🦱", "name": "person: curly hair", "shortname": ":curly_hair:", "unicode": "1F9D1 200D 1F9B1", "html": "&#x1F9D1;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏻‍🦱", "name": "person: light skin tone, curly hair", "shortname": ":light_skin_tone_curly_hair:", "unicode": "1F9D1 1F3FB 200D 1F9B1", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏼‍🦱", "name": "person: medium-light skin tone, curly hair", "shortname": ":mediumlight_skin_tone_curly_hair:", "unicode": "1F9D1 1F3FC 200D 1F9B1", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏽‍🦱", "name": "person: medium skin tone, curly hair", "shortname": ":medium_skin_tone_curly_hair:", "unicode": "1F9D1 1F3FD 200D 1F9B1", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏾‍🦱", "name": "person: medium-dark skin tone, curly hair", "shortname": ":mediumdark_skin_tone_curly_hair:", "unicode": "1F9D1 1F3FE 200D 1F9B1", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏿‍🦱", "name": "person: dark skin tone, curly hair", "shortname": ":dark_skin_tone_curly_hair:", "unicode": "1F9D1 1F3FF 200D 1F9B1", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F9B1;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩‍🦳", "name": "woman: white hair", "shortname": ":white_hair:", "unicode": "1F469 200D 1F9B3", "html": "&#x1F469;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏻‍🦳", "name": "woman: light skin tone, white hair", "shortname": ":light_skin_tone_white_hair:", "unicode": "1F469 1F3FB 200D 1F9B3", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏼‍🦳", "name": "woman: medium-light skin tone, white hair", "shortname": ":mediumlight_skin_tone_white_hair:", "unicode": "1F469 1F3FC 200D 1F9B3", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏽‍🦳", "name": "woman: medium skin tone, white hair", "shortname": ":medium_skin_tone_white_hair:", "unicode": "1F469 1F3FD 200D 1F9B3", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏾‍🦳", "name": "woman: medium-dark skin tone, white hair", "shortname": ":mediumdark_skin_tone_white_hair:", "unicode": "1F469 1F3FE 200D 1F9B3", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏿‍🦳", "name": "woman: dark skin tone, white hair", "shortname": ":dark_skin_tone_white_hair:", "unicode": "1F469 1F3FF 200D 1F9B3", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑‍🦳", "name": "person: white hair", "shortname": ":white_hair:", "unicode": "1F9D1 200D 1F9B3", "html": "&#x1F9D1;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏻‍🦳", "name": "person: light skin tone, white hair", "shortname": ":light_skin_tone_white_hair:", "unicode": "1F9D1 1F3FB 200D 1F9B3", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏼‍🦳", "name": "person: medium-light skin tone, white hair", "shortname": ":mediumlight_skin_tone_white_hair:", "unicode": "1F9D1 1F3FC 200D 1F9B3", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏽‍🦳", "name": "person: medium skin tone, white hair", "shortname": ":medium_skin_tone_white_hair:", "unicode": "1F9D1 1F3FD 200D 1F9B3", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏾‍🦳", "name": "person: medium-dark skin tone, white hair", "shortname": ":mediumdark_skin_tone_white_hair:", "unicode": "1F9D1 1F3FE 200D 1F9B3", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏿‍🦳", "name": "person: dark skin tone, white hair", "shortname": ":dark_skin_tone_white_hair:", "unicode": "1F9D1 1F3FF 200D 1F9B3", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F9B3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩‍🦲", "name": "woman: bald", "shortname": ":bald:", "unicode": "1F469 200D 1F9B2", "html": "&#x1F469;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏻‍🦲", "name": "woman: light skin tone, bald", "shortname": ":light_skin_tone_bald:", "unicode": "1F469 1F3FB 200D 1F9B2", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏼‍🦲", "name": "woman: medium-light skin tone, bald", "shortname": ":mediumlight_skin_tone_bald:", "unicode": "1F469 1F3FC 200D 1F9B2", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏽‍🦲", "name": "woman: medium skin tone, bald", "shortname": ":medium_skin_tone_bald:", "unicode": "1F469 1F3FD 200D 1F9B2", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏾‍🦲", "name": "woman: medium-dark skin tone, bald", "shortname": ":mediumdark_skin_tone_bald:", "unicode": "1F469 1F3FE 200D 1F9B2", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👩🏿‍🦲", "name": "woman: dark skin tone, bald", "shortname": ":dark_skin_tone_bald:", "unicode": "1F469 1F3FF 200D 1F9B2", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑‍🦲", "name": "person: bald", "shortname": ":bald:", "unicode": "1F9D1 200D 1F9B2", "html": "&#x1F9D1;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏻‍🦲", "name": "person: light skin tone, bald", "shortname": ":light_skin_tone_bald:", "unicode": "1F9D1 1F3FB 200D 1F9B2", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏼‍🦲", "name": "person: medium-light skin tone, bald", "shortname": ":mediumlight_skin_tone_bald:", "unicode": "1F9D1 1F3FC 200D 1F9B2", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏽‍🦲", "name": "person: medium skin tone, bald", "shortname": ":medium_skin_tone_bald:", "unicode": "1F9D1 1F3FD 200D 1F9B2", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏾‍🦲", "name": "person: medium-dark skin tone, bald", "shortname": ":mediumdark_skin_tone_bald:", "unicode": "1F9D1 1F3FE 200D 1F9B2", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧑🏿‍🦲", "name": "person: dark skin tone, bald", "shortname": ":dark_skin_tone_bald:", "unicode": "1F9D1 1F3FF 200D 1F9B2", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F9B2;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱‍♀️", "name": "woman: blond hair", "shortname": ":blond_hair:", "unicode": "1F471 200D 2640 FE0F", "html": "&#x1F471;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏻‍♀️", "name": "woman: light skin tone, blond hair", "shortname": ":light_skin_tone_blond_hair:", "unicode": "1F471 1F3FB 200D 2640 FE0F", "html": "&#x1F471;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏼‍♀️", "name": "woman: medium-light skin tone, blond hair", "shortname": ":mediumlight_skin_tone_blond_hair:", "unicode": "1F471 1F3FC 200D 2640 FE0F", "html": "&#x1F471;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏽‍♀️", "name": "woman: medium skin tone, blond hair", "shortname": ":medium_skin_tone_blond_hair:", "unicode": "1F471 1F3FD 200D 2640 FE0F", "html": "&#x1F471;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏾‍♀️", "name": "woman: medium-dark skin tone, blond hair", "shortname": ":mediumdark_skin_tone_blond_hair:", "unicode": "1F471 1F3FE 200D 2640 FE0F", "html": "&#x1F471;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏿‍♀️", "name": "woman: dark skin tone, blond hair", "shortname": ":dark_skin_tone_blond_hair:", "unicode": "1F471 1F3FF 200D 2640 FE0F", "html": "&#x1F471;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱‍♂️", "name": "man: blond hair", "shortname": ":blond_hair:", "unicode": "1F471 200D 2642 FE0F", "html": "&#x1F471;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱‍♂", "name": "man: blond hair", "shortname": ":blond_hair:", "unicode": "1F471 200D 2642", "html": "&#x1F471;&#x200D;&#x2642;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏻‍♂️", "name": "man: light skin tone, blond hair", "shortname": ":light_skin_tone_blond_hair:", "unicode": "1F471 1F3FB 200D 2642 FE0F", "html": "&#x1F471;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏻‍♂", "name": "man: light skin tone, blond hair", "shortname": ":light_skin_tone_blond_hair:", "unicode": "1F471 1F3FB 200D 2642", "html": "&#x1F471;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏼‍♂️", "name": "man: medium-light skin tone, blond hair", "shortname": ":mediumlight_skin_tone_blond_hair:", "unicode": "1F471 1F3FC 200D 2642 FE0F", "html": "&#x1F471;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏼‍♂", "name": "man: medium-light skin tone, blond hair", "shortname": ":mediumlight_skin_tone_blond_hair:", "unicode": "1F471 1F3FC 200D 2642", "html": "&#x1F471;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏽‍♂️", "name": "man: medium skin tone, blond hair", "shortname": ":medium_skin_tone_blond_hair:", "unicode": "1F471 1F3FD 200D 2642 FE0F", "html": "&#x1F471;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏽‍♂", "name": "man: medium skin tone, blond hair", "shortname": ":medium_skin_tone_blond_hair:", "unicode": "1F471 1F3FD 200D 2642", "html": "&#x1F471;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏾‍♂️", "name": "man: medium-dark skin tone, blond hair", "shortname": ":mediumdark_skin_tone_blond_hair:", "unicode": "1F471 1F3FE 200D 2642 FE0F", "html": "&#x1F471;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏾‍♂", "name": "man: medium-dark skin tone, blond hair", "shortname": ":mediumdark_skin_tone_blond_hair:", "unicode": "1F471 1F3FE 200D 2642", "html": "&#x1F471;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏿‍♂️", "name": "man: dark skin tone, blond hair", "shortname": ":dark_skin_tone_blond_hair:", "unicode": "1F471 1F3FF 200D 2642 FE0F", "html": "&#x1F471;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "👱🏿‍♂", "name": "man: dark skin tone, blond hair", "shortname": ":dark_skin_tone_blond_hair:", "unicode": "1F471 1F3FF 200D 2642", "html": "&#x1F471;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧓", "name": "older person", "shortname": ":older_person:", "unicode": "1F9D3", "html": "&#x1F9D3;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧓🏻", "name": "older person: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D3 1F3FB", "html": "&#x1F9D3;&#x1F3FB;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧓🏼", "name": "older person: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D3 1F3FC", "html": "&#x1F9D3;&#x1F3FC;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧓🏽", "name": "older person: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D3 1F3FD", "html": "&#x1F9D3;&#x1F3FD;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧓🏾", "name": "older person: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D3 1F3FE", "html": "&#x1F9D3;&#x1F3FE;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🧓🏿", "name": "older person: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D3 1F3FF", "html": "&#x1F9D3;&#x1F3FF;", "category": "People & Body (person)", "order": ""},
+ {"emoji": "🙍‍♂️", "name": "man frowning", "shortname": ":man_frowning:", "unicode": "1F64D 200D 2642 FE0F", "html": "&#x1F64D;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏻‍♂️", "name": "man frowning: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64D 1F3FB 200D 2642 FE0F", "html": "&#x1F64D;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏼‍♂️", "name": "man frowning: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64D 1F3FC 200D 2642 FE0F", "html": "&#x1F64D;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏽‍♂️", "name": "man frowning: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64D 1F3FD 200D 2642 FE0F", "html": "&#x1F64D;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏾‍♂️", "name": "man frowning: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64D 1F3FE 200D 2642 FE0F", "html": "&#x1F64D;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏿‍♂️", "name": "man frowning: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64D 1F3FF 200D 2642 FE0F", "html": "&#x1F64D;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍‍♀️", "name": "woman frowning", "shortname": ":woman_frowning:", "unicode": "1F64D 200D 2640 FE0F", "html": "&#x1F64D;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍‍♀", "name": "woman frowning", "shortname": ":woman_frowning:", "unicode": "1F64D 200D 2640", "html": "&#x1F64D;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏻‍♀️", "name": "woman frowning: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64D 1F3FB 200D 2640 FE0F", "html": "&#x1F64D;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏻‍♀", "name": "woman frowning: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64D 1F3FB 200D 2640", "html": "&#x1F64D;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏼‍♀️", "name": "woman frowning: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64D 1F3FC 200D 2640 FE0F", "html": "&#x1F64D;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏼‍♀", "name": "woman frowning: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64D 1F3FC 200D 2640", "html": "&#x1F64D;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏽‍♀️", "name": "woman frowning: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64D 1F3FD 200D 2640 FE0F", "html": "&#x1F64D;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏽‍♀", "name": "woman frowning: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64D 1F3FD 200D 2640", "html": "&#x1F64D;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏾‍♀️", "name": "woman frowning: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64D 1F3FE 200D 2640 FE0F", "html": "&#x1F64D;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏾‍♀", "name": "woman frowning: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64D 1F3FE 200D 2640", "html": "&#x1F64D;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏿‍♀️", "name": "woman frowning: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64D 1F3FF 200D 2640 FE0F", "html": "&#x1F64D;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙍🏿‍♀", "name": "woman frowning: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64D 1F3FF 200D 2640", "html": "&#x1F64D;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎‍♂️", "name": "man pouting", "shortname": ":man_pouting:", "unicode": "1F64E 200D 2642 FE0F", "html": "&#x1F64E;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏻‍♂️", "name": "man pouting: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64E 1F3FB 200D 2642 FE0F", "html": "&#x1F64E;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏼‍♂️", "name": "man pouting: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64E 1F3FC 200D 2642 FE0F", "html": "&#x1F64E;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏽‍♂️", "name": "man pouting: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64E 1F3FD 200D 2642 FE0F", "html": "&#x1F64E;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏾‍♂️", "name": "man pouting: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64E 1F3FE 200D 2642 FE0F", "html": "&#x1F64E;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏿‍♂️", "name": "man pouting: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64E 1F3FF 200D 2642 FE0F", "html": "&#x1F64E;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎‍♀️", "name": "woman pouting", "shortname": ":woman_pouting:", "unicode": "1F64E 200D 2640 FE0F", "html": "&#x1F64E;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎‍♀", "name": "woman pouting", "shortname": ":woman_pouting:", "unicode": "1F64E 200D 2640", "html": "&#x1F64E;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏻‍♀️", "name": "woman pouting: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64E 1F3FB 200D 2640 FE0F", "html": "&#x1F64E;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏻‍♀", "name": "woman pouting: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64E 1F3FB 200D 2640", "html": "&#x1F64E;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏼‍♀️", "name": "woman pouting: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64E 1F3FC 200D 2640 FE0F", "html": "&#x1F64E;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏼‍♀", "name": "woman pouting: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64E 1F3FC 200D 2640", "html": "&#x1F64E;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏽‍♀️", "name": "woman pouting: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64E 1F3FD 200D 2640 FE0F", "html": "&#x1F64E;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏽‍♀", "name": "woman pouting: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64E 1F3FD 200D 2640", "html": "&#x1F64E;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏾‍♀️", "name": "woman pouting: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64E 1F3FE 200D 2640 FE0F", "html": "&#x1F64E;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏾‍♀", "name": "woman pouting: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64E 1F3FE 200D 2640", "html": "&#x1F64E;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏿‍♀️", "name": "woman pouting: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64E 1F3FF 200D 2640 FE0F", "html": "&#x1F64E;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙎🏿‍♀", "name": "woman pouting: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64E 1F3FF 200D 2640", "html": "&#x1F64E;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅‍♂️", "name": "man gesturing NO", "shortname": ":man_gesturing_NO:", "unicode": "1F645 200D 2642 FE0F", "html": "&#x1F645;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏻‍♂️", "name": "man gesturing NO: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F645 1F3FB 200D 2642 FE0F", "html": "&#x1F645;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏼‍♂️", "name": "man gesturing NO: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F645 1F3FC 200D 2642 FE0F", "html": "&#x1F645;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏽‍♂️", "name": "man gesturing NO: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F645 1F3FD 200D 2642 FE0F", "html": "&#x1F645;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏾‍♂️", "name": "man gesturing NO: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F645 1F3FE 200D 2642 FE0F", "html": "&#x1F645;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏿‍♂️", "name": "man gesturing NO: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F645 1F3FF 200D 2642 FE0F", "html": "&#x1F645;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅‍♀️", "name": "woman gesturing NO", "shortname": ":woman_gesturing_NO:", "unicode": "1F645 200D 2640 FE0F", "html": "&#x1F645;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅‍♀", "name": "woman gesturing NO", "shortname": ":woman_gesturing_NO:", "unicode": "1F645 200D 2640", "html": "&#x1F645;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏻‍♀️", "name": "woman gesturing NO: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F645 1F3FB 200D 2640 FE0F", "html": "&#x1F645;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏻‍♀", "name": "woman gesturing NO: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F645 1F3FB 200D 2640", "html": "&#x1F645;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏼‍♀️", "name": "woman gesturing NO: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F645 1F3FC 200D 2640 FE0F", "html": "&#x1F645;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏼‍♀", "name": "woman gesturing NO: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F645 1F3FC 200D 2640", "html": "&#x1F645;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏽‍♀️", "name": "woman gesturing NO: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F645 1F3FD 200D 2640 FE0F", "html": "&#x1F645;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏽‍♀", "name": "woman gesturing NO: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F645 1F3FD 200D 2640", "html": "&#x1F645;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏾‍♀️", "name": "woman gesturing NO: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F645 1F3FE 200D 2640 FE0F", "html": "&#x1F645;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏾‍♀", "name": "woman gesturing NO: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F645 1F3FE 200D 2640", "html": "&#x1F645;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏿‍♀️", "name": "woman gesturing NO: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F645 1F3FF 200D 2640 FE0F", "html": "&#x1F645;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙅🏿‍♀", "name": "woman gesturing NO: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F645 1F3FF 200D 2640", "html": "&#x1F645;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆‍♂️", "name": "man gesturing OK", "shortname": ":man_gesturing_OK:", "unicode": "1F646 200D 2642 FE0F", "html": "&#x1F646;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏻‍♂️", "name": "man gesturing OK: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F646 1F3FB 200D 2642 FE0F", "html": "&#x1F646;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏼‍♂️", "name": "man gesturing OK: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F646 1F3FC 200D 2642 FE0F", "html": "&#x1F646;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏽‍♂️", "name": "man gesturing OK: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F646 1F3FD 200D 2642 FE0F", "html": "&#x1F646;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏾‍♂️", "name": "man gesturing OK: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F646 1F3FE 200D 2642 FE0F", "html": "&#x1F646;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏿‍♂️", "name": "man gesturing OK: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F646 1F3FF 200D 2642 FE0F", "html": "&#x1F646;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆‍♀️", "name": "woman gesturing OK", "shortname": ":woman_gesturing_OK:", "unicode": "1F646 200D 2640 FE0F", "html": "&#x1F646;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆‍♀", "name": "woman gesturing OK", "shortname": ":woman_gesturing_OK:", "unicode": "1F646 200D 2640", "html": "&#x1F646;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏻‍♀️", "name": "woman gesturing OK: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F646 1F3FB 200D 2640 FE0F", "html": "&#x1F646;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏻‍♀", "name": "woman gesturing OK: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F646 1F3FB 200D 2640", "html": "&#x1F646;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏼‍♀️", "name": "woman gesturing OK: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F646 1F3FC 200D 2640 FE0F", "html": "&#x1F646;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏼‍♀", "name": "woman gesturing OK: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F646 1F3FC 200D 2640", "html": "&#x1F646;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏽‍♀️", "name": "woman gesturing OK: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F646 1F3FD 200D 2640 FE0F", "html": "&#x1F646;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏽‍♀", "name": "woman gesturing OK: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F646 1F3FD 200D 2640", "html": "&#x1F646;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏾‍♀️", "name": "woman gesturing OK: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F646 1F3FE 200D 2640 FE0F", "html": "&#x1F646;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏾‍♀", "name": "woman gesturing OK: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F646 1F3FE 200D 2640", "html": "&#x1F646;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏿‍♀️", "name": "woman gesturing OK: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F646 1F3FF 200D 2640 FE0F", "html": "&#x1F646;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙆🏿‍♀", "name": "woman gesturing OK: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F646 1F3FF 200D 2640", "html": "&#x1F646;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁‍♂️", "name": "man tipping hand", "shortname": ":man_tipping_hand:", "unicode": "1F481 200D 2642 FE0F", "html": "&#x1F481;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏻‍♂️", "name": "man tipping hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F481 1F3FB 200D 2642 FE0F", "html": "&#x1F481;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏼‍♂️", "name": "man tipping hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F481 1F3FC 200D 2642 FE0F", "html": "&#x1F481;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏽‍♂️", "name": "man tipping hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F481 1F3FD 200D 2642 FE0F", "html": "&#x1F481;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏾‍♂️", "name": "man tipping hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F481 1F3FE 200D 2642 FE0F", "html": "&#x1F481;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏿‍♂️", "name": "man tipping hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F481 1F3FF 200D 2642 FE0F", "html": "&#x1F481;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁‍♀️", "name": "woman tipping hand", "shortname": ":woman_tipping_hand:", "unicode": "1F481 200D 2640 FE0F", "html": "&#x1F481;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁‍♀", "name": "woman tipping hand", "shortname": ":woman_tipping_hand:", "unicode": "1F481 200D 2640", "html": "&#x1F481;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏻‍♀️", "name": "woman tipping hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F481 1F3FB 200D 2640 FE0F", "html": "&#x1F481;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏻‍♀", "name": "woman tipping hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F481 1F3FB 200D 2640", "html": "&#x1F481;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏼‍♀️", "name": "woman tipping hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F481 1F3FC 200D 2640 FE0F", "html": "&#x1F481;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏼‍♀", "name": "woman tipping hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F481 1F3FC 200D 2640", "html": "&#x1F481;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏽‍♀️", "name": "woman tipping hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F481 1F3FD 200D 2640 FE0F", "html": "&#x1F481;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏽‍♀", "name": "woman tipping hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F481 1F3FD 200D 2640", "html": "&#x1F481;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏾‍♀️", "name": "woman tipping hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F481 1F3FE 200D 2640 FE0F", "html": "&#x1F481;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏾‍♀", "name": "woman tipping hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F481 1F3FE 200D 2640", "html": "&#x1F481;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏿‍♀️", "name": "woman tipping hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F481 1F3FF 200D 2640 FE0F", "html": "&#x1F481;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "💁🏿‍♀", "name": "woman tipping hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F481 1F3FF 200D 2640", "html": "&#x1F481;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋‍♂️", "name": "man raising hand", "shortname": ":man_raising_hand:", "unicode": "1F64B 200D 2642 FE0F", "html": "&#x1F64B;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏻‍♂️", "name": "man raising hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64B 1F3FB 200D 2642 FE0F", "html": "&#x1F64B;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏼‍♂️", "name": "man raising hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64B 1F3FC 200D 2642 FE0F", "html": "&#x1F64B;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏽‍♂️", "name": "man raising hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64B 1F3FD 200D 2642 FE0F", "html": "&#x1F64B;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏾‍♂️", "name": "man raising hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64B 1F3FE 200D 2642 FE0F", "html": "&#x1F64B;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏿‍♂️", "name": "man raising hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64B 1F3FF 200D 2642 FE0F", "html": "&#x1F64B;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋‍♀️", "name": "woman raising hand", "shortname": ":woman_raising_hand:", "unicode": "1F64B 200D 2640 FE0F", "html": "&#x1F64B;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋‍♀", "name": "woman raising hand", "shortname": ":woman_raising_hand:", "unicode": "1F64B 200D 2640", "html": "&#x1F64B;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏻‍♀️", "name": "woman raising hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64B 1F3FB 200D 2640 FE0F", "html": "&#x1F64B;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏻‍♀", "name": "woman raising hand: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F64B 1F3FB 200D 2640", "html": "&#x1F64B;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏼‍♀️", "name": "woman raising hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64B 1F3FC 200D 2640 FE0F", "html": "&#x1F64B;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏼‍♀", "name": "woman raising hand: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F64B 1F3FC 200D 2640", "html": "&#x1F64B;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏽‍♀️", "name": "woman raising hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64B 1F3FD 200D 2640 FE0F", "html": "&#x1F64B;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏽‍♀", "name": "woman raising hand: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F64B 1F3FD 200D 2640", "html": "&#x1F64B;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏾‍♀️", "name": "woman raising hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64B 1F3FE 200D 2640 FE0F", "html": "&#x1F64B;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏾‍♀", "name": "woman raising hand: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F64B 1F3FE 200D 2640", "html": "&#x1F64B;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏿‍♀️", "name": "woman raising hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64B 1F3FF 200D 2640 FE0F", "html": "&#x1F64B;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙋🏿‍♀", "name": "woman raising hand: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F64B 1F3FF 200D 2640", "html": "&#x1F64B;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏", "name": "deaf person", "shortname": ":deaf_person:", "unicode": "1F9CF", "html": "&#x1F9CF;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏻", "name": "deaf person: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CF 1F3FB", "html": "&#x1F9CF;&#x1F3FB;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏼", "name": "deaf person: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CF 1F3FC", "html": "&#x1F9CF;&#x1F3FC;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏽", "name": "deaf person: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CF 1F3FD", "html": "&#x1F9CF;&#x1F3FD;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏾", "name": "deaf person: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CF 1F3FE", "html": "&#x1F9CF;&#x1F3FE;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏿", "name": "deaf person: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CF 1F3FF", "html": "&#x1F9CF;&#x1F3FF;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏‍♂️", "name": "deaf man", "shortname": ":deaf_man:", "unicode": "1F9CF 200D 2642 FE0F", "html": "&#x1F9CF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏‍♂", "name": "deaf man", "shortname": ":deaf_man:", "unicode": "1F9CF 200D 2642", "html": "&#x1F9CF;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏻‍♂️", "name": "deaf man: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CF 1F3FB 200D 2642 FE0F", "html": "&#x1F9CF;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏻‍♂", "name": "deaf man: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CF 1F3FB 200D 2642", "html": "&#x1F9CF;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏼‍♂️", "name": "deaf man: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CF 1F3FC 200D 2642 FE0F", "html": "&#x1F9CF;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏼‍♂", "name": "deaf man: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CF 1F3FC 200D 2642", "html": "&#x1F9CF;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏽‍♂️", "name": "deaf man: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CF 1F3FD 200D 2642 FE0F", "html": "&#x1F9CF;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏽‍♂", "name": "deaf man: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CF 1F3FD 200D 2642", "html": "&#x1F9CF;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏾‍♂️", "name": "deaf man: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CF 1F3FE 200D 2642 FE0F", "html": "&#x1F9CF;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏾‍♂", "name": "deaf man: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CF 1F3FE 200D 2642", "html": "&#x1F9CF;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏿‍♂️", "name": "deaf man: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CF 1F3FF 200D 2642 FE0F", "html": "&#x1F9CF;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏿‍♂", "name": "deaf man: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CF 1F3FF 200D 2642", "html": "&#x1F9CF;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏‍♀️", "name": "deaf woman", "shortname": ":deaf_woman:", "unicode": "1F9CF 200D 2640 FE0F", "html": "&#x1F9CF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏‍♀", "name": "deaf woman", "shortname": ":deaf_woman:", "unicode": "1F9CF 200D 2640", "html": "&#x1F9CF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏻‍♀️", "name": "deaf woman: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CF 1F3FB 200D 2640 FE0F", "html": "&#x1F9CF;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏻‍♀", "name": "deaf woman: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CF 1F3FB 200D 2640", "html": "&#x1F9CF;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏼‍♀️", "name": "deaf woman: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CF 1F3FC 200D 2640 FE0F", "html": "&#x1F9CF;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏼‍♀", "name": "deaf woman: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CF 1F3FC 200D 2640", "html": "&#x1F9CF;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏽‍♀️", "name": "deaf woman: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CF 1F3FD 200D 2640 FE0F", "html": "&#x1F9CF;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏽‍♀", "name": "deaf woman: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CF 1F3FD 200D 2640", "html": "&#x1F9CF;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏾‍♀️", "name": "deaf woman: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CF 1F3FE 200D 2640 FE0F", "html": "&#x1F9CF;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏾‍♀", "name": "deaf woman: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CF 1F3FE 200D 2640", "html": "&#x1F9CF;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏿‍♀️", "name": "deaf woman: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CF 1F3FF 200D 2640 FE0F", "html": "&#x1F9CF;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧏🏿‍♀", "name": "deaf woman: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CF 1F3FF 200D 2640", "html": "&#x1F9CF;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇‍♂️", "name": "man bowing", "shortname": ":man_bowing:", "unicode": "1F647 200D 2642 FE0F", "html": "&#x1F647;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇‍♂", "name": "man bowing", "shortname": ":man_bowing:", "unicode": "1F647 200D 2642", "html": "&#x1F647;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏻‍♂️", "name": "man bowing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F647 1F3FB 200D 2642 FE0F", "html": "&#x1F647;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏻‍♂", "name": "man bowing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F647 1F3FB 200D 2642", "html": "&#x1F647;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏼‍♂️", "name": "man bowing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F647 1F3FC 200D 2642 FE0F", "html": "&#x1F647;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏼‍♂", "name": "man bowing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F647 1F3FC 200D 2642", "html": "&#x1F647;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏽‍♂️", "name": "man bowing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F647 1F3FD 200D 2642 FE0F", "html": "&#x1F647;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏽‍♂", "name": "man bowing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F647 1F3FD 200D 2642", "html": "&#x1F647;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏾‍♂️", "name": "man bowing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F647 1F3FE 200D 2642 FE0F", "html": "&#x1F647;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏾‍♂", "name": "man bowing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F647 1F3FE 200D 2642", "html": "&#x1F647;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏿‍♂️", "name": "man bowing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F647 1F3FF 200D 2642 FE0F", "html": "&#x1F647;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏿‍♂", "name": "man bowing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F647 1F3FF 200D 2642", "html": "&#x1F647;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇‍♀️", "name": "woman bowing", "shortname": ":woman_bowing:", "unicode": "1F647 200D 2640 FE0F", "html": "&#x1F647;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏻‍♀️", "name": "woman bowing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F647 1F3FB 200D 2640 FE0F", "html": "&#x1F647;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏼‍♀️", "name": "woman bowing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F647 1F3FC 200D 2640 FE0F", "html": "&#x1F647;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏽‍♀️", "name": "woman bowing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F647 1F3FD 200D 2640 FE0F", "html": "&#x1F647;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏾‍♀️", "name": "woman bowing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F647 1F3FE 200D 2640 FE0F", "html": "&#x1F647;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🙇🏿‍♀️", "name": "woman bowing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F647 1F3FF 200D 2640 FE0F", "html": "&#x1F647;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦", "name": "person facepalming", "shortname": ":person_facepalming:", "unicode": "1F926", "html": "&#x1F926;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏻", "name": "person facepalming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F926 1F3FB", "html": "&#x1F926;&#x1F3FB;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏼", "name": "person facepalming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F926 1F3FC", "html": "&#x1F926;&#x1F3FC;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏽", "name": "person facepalming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F926 1F3FD", "html": "&#x1F926;&#x1F3FD;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏾", "name": "person facepalming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F926 1F3FE", "html": "&#x1F926;&#x1F3FE;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏿", "name": "person facepalming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F926 1F3FF", "html": "&#x1F926;&#x1F3FF;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦‍♂️", "name": "man facepalming", "shortname": ":man_facepalming:", "unicode": "1F926 200D 2642 FE0F", "html": "&#x1F926;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦‍♂", "name": "man facepalming", "shortname": ":man_facepalming:", "unicode": "1F926 200D 2642", "html": "&#x1F926;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏻‍♂️", "name": "man facepalming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F926 1F3FB 200D 2642 FE0F", "html": "&#x1F926;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏻‍♂", "name": "man facepalming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F926 1F3FB 200D 2642", "html": "&#x1F926;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏼‍♂️", "name": "man facepalming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F926 1F3FC 200D 2642 FE0F", "html": "&#x1F926;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏼‍♂", "name": "man facepalming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F926 1F3FC 200D 2642", "html": "&#x1F926;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏽‍♂️", "name": "man facepalming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F926 1F3FD 200D 2642 FE0F", "html": "&#x1F926;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏽‍♂", "name": "man facepalming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F926 1F3FD 200D 2642", "html": "&#x1F926;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏾‍♂️", "name": "man facepalming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F926 1F3FE 200D 2642 FE0F", "html": "&#x1F926;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏾‍♂", "name": "man facepalming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F926 1F3FE 200D 2642", "html": "&#x1F926;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏿‍♂️", "name": "man facepalming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F926 1F3FF 200D 2642 FE0F", "html": "&#x1F926;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏿‍♂", "name": "man facepalming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F926 1F3FF 200D 2642", "html": "&#x1F926;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦‍♀️", "name": "woman facepalming", "shortname": ":woman_facepalming:", "unicode": "1F926 200D 2640 FE0F", "html": "&#x1F926;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦‍♀", "name": "woman facepalming", "shortname": ":woman_facepalming:", "unicode": "1F926 200D 2640", "html": "&#x1F926;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏻‍♀️", "name": "woman facepalming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F926 1F3FB 200D 2640 FE0F", "html": "&#x1F926;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏻‍♀", "name": "woman facepalming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F926 1F3FB 200D 2640", "html": "&#x1F926;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏼‍♀️", "name": "woman facepalming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F926 1F3FC 200D 2640 FE0F", "html": "&#x1F926;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏼‍♀", "name": "woman facepalming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F926 1F3FC 200D 2640", "html": "&#x1F926;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏽‍♀️", "name": "woman facepalming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F926 1F3FD 200D 2640 FE0F", "html": "&#x1F926;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏽‍♀", "name": "woman facepalming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F926 1F3FD 200D 2640", "html": "&#x1F926;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏾‍♀️", "name": "woman facepalming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F926 1F3FE 200D 2640 FE0F", "html": "&#x1F926;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏾‍♀", "name": "woman facepalming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F926 1F3FE 200D 2640", "html": "&#x1F926;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏿‍♀️", "name": "woman facepalming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F926 1F3FF 200D 2640 FE0F", "html": "&#x1F926;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤦🏿‍♀", "name": "woman facepalming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F926 1F3FF 200D 2640", "html": "&#x1F926;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷", "name": "person shrugging", "shortname": ":person_shrugging:", "unicode": "1F937", "html": "&#x1F937;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏻", "name": "person shrugging: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F937 1F3FB", "html": "&#x1F937;&#x1F3FB;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏼", "name": "person shrugging: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F937 1F3FC", "html": "&#x1F937;&#x1F3FC;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏽", "name": "person shrugging: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F937 1F3FD", "html": "&#x1F937;&#x1F3FD;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏾", "name": "person shrugging: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F937 1F3FE", "html": "&#x1F937;&#x1F3FE;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏿", "name": "person shrugging: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F937 1F3FF", "html": "&#x1F937;&#x1F3FF;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷‍♂️", "name": "man shrugging", "shortname": ":man_shrugging:", "unicode": "1F937 200D 2642 FE0F", "html": "&#x1F937;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷‍♂", "name": "man shrugging", "shortname": ":man_shrugging:", "unicode": "1F937 200D 2642", "html": "&#x1F937;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏻‍♂️", "name": "man shrugging: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F937 1F3FB 200D 2642 FE0F", "html": "&#x1F937;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏻‍♂", "name": "man shrugging: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F937 1F3FB 200D 2642", "html": "&#x1F937;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏼‍♂️", "name": "man shrugging: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F937 1F3FC 200D 2642 FE0F", "html": "&#x1F937;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏼‍♂", "name": "man shrugging: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F937 1F3FC 200D 2642", "html": "&#x1F937;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏽‍♂️", "name": "man shrugging: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F937 1F3FD 200D 2642 FE0F", "html": "&#x1F937;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏽‍♂", "name": "man shrugging: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F937 1F3FD 200D 2642", "html": "&#x1F937;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏾‍♂️", "name": "man shrugging: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F937 1F3FE 200D 2642 FE0F", "html": "&#x1F937;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏾‍♂", "name": "man shrugging: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F937 1F3FE 200D 2642", "html": "&#x1F937;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏿‍♂️", "name": "man shrugging: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F937 1F3FF 200D 2642 FE0F", "html": "&#x1F937;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏿‍♂", "name": "man shrugging: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F937 1F3FF 200D 2642", "html": "&#x1F937;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷‍♀️", "name": "woman shrugging", "shortname": ":woman_shrugging:", "unicode": "1F937 200D 2640 FE0F", "html": "&#x1F937;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷‍♀", "name": "woman shrugging", "shortname": ":woman_shrugging:", "unicode": "1F937 200D 2640", "html": "&#x1F937;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏻‍♀️", "name": "woman shrugging: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F937 1F3FB 200D 2640 FE0F", "html": "&#x1F937;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏻‍♀", "name": "woman shrugging: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F937 1F3FB 200D 2640", "html": "&#x1F937;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏼‍♀️", "name": "woman shrugging: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F937 1F3FC 200D 2640 FE0F", "html": "&#x1F937;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏼‍♀", "name": "woman shrugging: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F937 1F3FC 200D 2640", "html": "&#x1F937;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏽‍♀️", "name": "woman shrugging: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F937 1F3FD 200D 2640 FE0F", "html": "&#x1F937;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏽‍♀", "name": "woman shrugging: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F937 1F3FD 200D 2640", "html": "&#x1F937;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏾‍♀️", "name": "woman shrugging: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F937 1F3FE 200D 2640 FE0F", "html": "&#x1F937;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏾‍♀", "name": "woman shrugging: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F937 1F3FE 200D 2640", "html": "&#x1F937;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏿‍♀️", "name": "woman shrugging: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F937 1F3FF 200D 2640 FE0F", "html": "&#x1F937;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🤷🏿‍♀", "name": "woman shrugging: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F937 1F3FF 200D 2640", "html": "&#x1F937;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-gesture)", "order": ""},
+ {"emoji": "🧑‍⚕️", "name": "health worker", "shortname": ":health_worker:", "unicode": "1F9D1 200D 2695 FE0F", "html": "&#x1F9D1;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍⚕", "name": "health worker", "shortname": ":health_worker:", "unicode": "1F9D1 200D 2695", "html": "&#x1F9D1;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍⚕️", "name": "health worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 2695 FE0F", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍⚕", "name": "health worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 2695", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍⚕️", "name": "health worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 2695 FE0F", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍⚕", "name": "health worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 2695", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍⚕️", "name": "health worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 2695 FE0F", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍⚕", "name": "health worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 2695", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍⚕️", "name": "health worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 2695 FE0F", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍⚕", "name": "health worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 2695", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍⚕️", "name": "health worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 2695 FE0F", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍⚕", "name": "health worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 2695", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍⚕️", "name": "man health worker", "shortname": ":man_health_worker:", "unicode": "1F468 200D 2695 FE0F", "html": "&#x1F468;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍⚕", "name": "man health worker", "shortname": ":man_health_worker:", "unicode": "1F468 200D 2695", "html": "&#x1F468;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍⚕️", "name": "man health worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 2695 FE0F", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍⚕", "name": "man health worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 2695", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍⚕️", "name": "man health worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 2695 FE0F", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍⚕", "name": "man health worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 2695", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍⚕️", "name": "man health worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 2695 FE0F", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍⚕", "name": "man health worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 2695", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍⚕️", "name": "man health worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 2695 FE0F", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍⚕", "name": "man health worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 2695", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍⚕️", "name": "man health worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 2695 FE0F", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍⚕", "name": "man health worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 2695", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍⚕️", "name": "woman health worker", "shortname": ":woman_health_worker:", "unicode": "1F469 200D 2695 FE0F", "html": "&#x1F469;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍⚕", "name": "woman health worker", "shortname": ":woman_health_worker:", "unicode": "1F469 200D 2695", "html": "&#x1F469;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍⚕️", "name": "woman health worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 2695 FE0F", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍⚕", "name": "woman health worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 2695", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍⚕️", "name": "woman health worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 2695 FE0F", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍⚕", "name": "woman health worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 2695", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍⚕️", "name": "woman health worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 2695 FE0F", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍⚕", "name": "woman health worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 2695", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍⚕️", "name": "woman health worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 2695 FE0F", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍⚕", "name": "woman health worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 2695", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍⚕️", "name": "woman health worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 2695 FE0F", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x2695;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍⚕", "name": "woman health worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 2695", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x2695;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🎓", "name": "student", "shortname": ":student:", "unicode": "1F9D1 200D 1F393", "html": "&#x1F9D1;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🎓", "name": "student: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F393", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🎓", "name": "student: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F393", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🎓", "name": "student: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F393", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🎓", "name": "student: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F393", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🎓", "name": "student: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F393", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🎓", "name": "man student", "shortname": ":man_student:", "unicode": "1F468 200D 1F393", "html": "&#x1F468;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🎓", "name": "man student: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F393", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🎓", "name": "man student: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F393", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🎓", "name": "man student: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F393", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🎓", "name": "man student: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F393", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🎓", "name": "man student: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F393", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🎓", "name": "woman student", "shortname": ":woman_student:", "unicode": "1F469 200D 1F393", "html": "&#x1F469;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🎓", "name": "woman student: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F393", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🎓", "name": "woman student: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F393", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🎓", "name": "woman student: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F393", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🎓", "name": "woman student: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F393", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🎓", "name": "woman student: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F393", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F393;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🏫", "name": "teacher", "shortname": ":teacher:", "unicode": "1F9D1 200D 1F3EB", "html": "&#x1F9D1;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🏫", "name": "teacher: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F3EB", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🏫", "name": "teacher: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F3EB", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🏫", "name": "teacher: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F3EB", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🏫", "name": "teacher: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F3EB", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🏫", "name": "teacher: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F3EB", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🏫", "name": "man teacher", "shortname": ":man_teacher:", "unicode": "1F468 200D 1F3EB", "html": "&#x1F468;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🏫", "name": "man teacher: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F3EB", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🏫", "name": "man teacher: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F3EB", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🏫", "name": "man teacher: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F3EB", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🏫", "name": "man teacher: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F3EB", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🏫", "name": "man teacher: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F3EB", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🏫", "name": "woman teacher", "shortname": ":woman_teacher:", "unicode": "1F469 200D 1F3EB", "html": "&#x1F469;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🏫", "name": "woman teacher: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F3EB", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🏫", "name": "woman teacher: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F3EB", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🏫", "name": "woman teacher: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F3EB", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🏫", "name": "woman teacher: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F3EB", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🏫", "name": "woman teacher: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F3EB", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F3EB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍⚖️", "name": "judge", "shortname": ":judge:", "unicode": "1F9D1 200D 2696 FE0F", "html": "&#x1F9D1;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍⚖", "name": "judge", "shortname": ":judge:", "unicode": "1F9D1 200D 2696", "html": "&#x1F9D1;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍⚖️", "name": "judge: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 2696 FE0F", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍⚖", "name": "judge: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 2696", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍⚖️", "name": "judge: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 2696 FE0F", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍⚖", "name": "judge: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 2696", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍⚖️", "name": "judge: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 2696 FE0F", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍⚖", "name": "judge: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 2696", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍⚖️", "name": "judge: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 2696 FE0F", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍⚖", "name": "judge: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 2696", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍⚖️", "name": "judge: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 2696 FE0F", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍⚖", "name": "judge: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 2696", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍⚖️", "name": "man judge", "shortname": ":man_judge:", "unicode": "1F468 200D 2696 FE0F", "html": "&#x1F468;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍⚖", "name": "man judge", "shortname": ":man_judge:", "unicode": "1F468 200D 2696", "html": "&#x1F468;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍⚖️", "name": "man judge: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 2696 FE0F", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍⚖", "name": "man judge: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 2696", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍⚖️", "name": "man judge: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 2696 FE0F", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍⚖", "name": "man judge: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 2696", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍⚖️", "name": "man judge: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 2696 FE0F", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍⚖", "name": "man judge: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 2696", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍⚖️", "name": "man judge: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 2696 FE0F", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍⚖", "name": "man judge: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 2696", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍⚖️", "name": "man judge: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 2696 FE0F", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍⚖", "name": "man judge: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 2696", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍⚖️", "name": "woman judge", "shortname": ":woman_judge:", "unicode": "1F469 200D 2696 FE0F", "html": "&#x1F469;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍⚖", "name": "woman judge", "shortname": ":woman_judge:", "unicode": "1F469 200D 2696", "html": "&#x1F469;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍⚖️", "name": "woman judge: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 2696 FE0F", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍⚖", "name": "woman judge: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 2696", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍⚖️", "name": "woman judge: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 2696 FE0F", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍⚖", "name": "woman judge: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 2696", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍⚖️", "name": "woman judge: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 2696 FE0F", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍⚖", "name": "woman judge: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 2696", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍⚖️", "name": "woman judge: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 2696 FE0F", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍⚖", "name": "woman judge: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 2696", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍⚖️", "name": "woman judge: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 2696 FE0F", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x2696;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍⚖", "name": "woman judge: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 2696", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x2696;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🌾", "name": "farmer", "shortname": ":farmer:", "unicode": "1F9D1 200D 1F33E", "html": "&#x1F9D1;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🌾", "name": "farmer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F33E", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🌾", "name": "farmer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F33E", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🌾", "name": "farmer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F33E", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🌾", "name": "farmer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F33E", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🌾", "name": "farmer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F33E", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🌾", "name": "man farmer", "shortname": ":man_farmer:", "unicode": "1F468 200D 1F33E", "html": "&#x1F468;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🌾", "name": "man farmer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F33E", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🌾", "name": "man farmer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F33E", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🌾", "name": "man farmer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F33E", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🌾", "name": "man farmer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F33E", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🌾", "name": "man farmer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F33E", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🌾", "name": "woman farmer", "shortname": ":woman_farmer:", "unicode": "1F469 200D 1F33E", "html": "&#x1F469;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🌾", "name": "woman farmer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F33E", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🌾", "name": "woman farmer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F33E", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🌾", "name": "woman farmer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F33E", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🌾", "name": "woman farmer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F33E", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🌾", "name": "woman farmer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F33E", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F33E;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🍳", "name": "cook", "shortname": ":cook:", "unicode": "1F9D1 200D 1F373", "html": "&#x1F9D1;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🍳", "name": "cook: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F373", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🍳", "name": "cook: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F373", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🍳", "name": "cook: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F373", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🍳", "name": "cook: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F373", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🍳", "name": "cook: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F373", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🍳", "name": "man cook", "shortname": ":man_cook:", "unicode": "1F468 200D 1F373", "html": "&#x1F468;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🍳", "name": "man cook: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F373", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🍳", "name": "man cook: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F373", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🍳", "name": "man cook: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F373", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🍳", "name": "man cook: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F373", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🍳", "name": "man cook: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F373", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🍳", "name": "woman cook", "shortname": ":woman_cook:", "unicode": "1F469 200D 1F373", "html": "&#x1F469;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🍳", "name": "woman cook: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F373", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🍳", "name": "woman cook: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F373", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🍳", "name": "woman cook: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F373", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🍳", "name": "woman cook: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F373", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🍳", "name": "woman cook: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F373", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F373;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🔧", "name": "mechanic", "shortname": ":mechanic:", "unicode": "1F9D1 200D 1F527", "html": "&#x1F9D1;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🔧", "name": "mechanic: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F527", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🔧", "name": "mechanic: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F527", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🔧", "name": "mechanic: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F527", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🔧", "name": "mechanic: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F527", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🔧", "name": "mechanic: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F527", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🔧", "name": "man mechanic", "shortname": ":man_mechanic:", "unicode": "1F468 200D 1F527", "html": "&#x1F468;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🔧", "name": "man mechanic: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F527", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🔧", "name": "man mechanic: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F527", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🔧", "name": "man mechanic: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F527", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🔧", "name": "man mechanic: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F527", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🔧", "name": "man mechanic: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F527", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🔧", "name": "woman mechanic", "shortname": ":woman_mechanic:", "unicode": "1F469 200D 1F527", "html": "&#x1F469;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🔧", "name": "woman mechanic: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F527", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🔧", "name": "woman mechanic: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F527", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🔧", "name": "woman mechanic: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F527", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🔧", "name": "woman mechanic: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F527", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🔧", "name": "woman mechanic: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F527", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F527;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🏭", "name": "factory worker", "shortname": ":factory_worker:", "unicode": "1F9D1 200D 1F3ED", "html": "&#x1F9D1;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🏭", "name": "factory worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F3ED", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🏭", "name": "factory worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F3ED", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🏭", "name": "factory worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F3ED", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🏭", "name": "factory worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F3ED", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🏭", "name": "factory worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F3ED", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🏭", "name": "man factory worker", "shortname": ":man_factory_worker:", "unicode": "1F468 200D 1F3ED", "html": "&#x1F468;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🏭", "name": "man factory worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F3ED", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🏭", "name": "man factory worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F3ED", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🏭", "name": "man factory worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F3ED", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🏭", "name": "man factory worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F3ED", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🏭", "name": "man factory worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F3ED", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🏭", "name": "woman factory worker", "shortname": ":woman_factory_worker:", "unicode": "1F469 200D 1F3ED", "html": "&#x1F469;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🏭", "name": "woman factory worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F3ED", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🏭", "name": "woman factory worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F3ED", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🏭", "name": "woman factory worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F3ED", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🏭", "name": "woman factory worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F3ED", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🏭", "name": "woman factory worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F3ED", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F3ED;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍💼", "name": "office worker", "shortname": ":office_worker:", "unicode": "1F9D1 200D 1F4BC", "html": "&#x1F9D1;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍💼", "name": "office worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F4BC", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍💼", "name": "office worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F4BC", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍💼", "name": "office worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F4BC", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍💼", "name": "office worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F4BC", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍💼", "name": "office worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F4BC", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍💼", "name": "man office worker", "shortname": ":man_office_worker:", "unicode": "1F468 200D 1F4BC", "html": "&#x1F468;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍💼", "name": "man office worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F4BC", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍💼", "name": "man office worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F4BC", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍💼", "name": "man office worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F4BC", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍💼", "name": "man office worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F4BC", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍💼", "name": "man office worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F4BC", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍💼", "name": "woman office worker", "shortname": ":woman_office_worker:", "unicode": "1F469 200D 1F4BC", "html": "&#x1F469;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍💼", "name": "woman office worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F4BC", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍💼", "name": "woman office worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F4BC", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍💼", "name": "woman office worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F4BC", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍💼", "name": "woman office worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F4BC", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍💼", "name": "woman office worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F4BC", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F4BC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🔬", "name": "scientist", "shortname": ":scientist:", "unicode": "1F9D1 200D 1F52C", "html": "&#x1F9D1;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🔬", "name": "scientist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F52C", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🔬", "name": "scientist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F52C", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🔬", "name": "scientist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F52C", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🔬", "name": "scientist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F52C", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🔬", "name": "scientist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F52C", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🔬", "name": "man scientist", "shortname": ":man_scientist:", "unicode": "1F468 200D 1F52C", "html": "&#x1F468;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🔬", "name": "man scientist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F52C", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🔬", "name": "man scientist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F52C", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🔬", "name": "man scientist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F52C", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🔬", "name": "man scientist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F52C", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🔬", "name": "man scientist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F52C", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🔬", "name": "woman scientist", "shortname": ":woman_scientist:", "unicode": "1F469 200D 1F52C", "html": "&#x1F469;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🔬", "name": "woman scientist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F52C", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🔬", "name": "woman scientist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F52C", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🔬", "name": "woman scientist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F52C", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🔬", "name": "woman scientist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F52C", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🔬", "name": "woman scientist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F52C", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F52C;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍💻", "name": "technologist", "shortname": ":technologist:", "unicode": "1F9D1 200D 1F4BB", "html": "&#x1F9D1;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍💻", "name": "technologist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F4BB", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍💻", "name": "technologist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F4BB", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍💻", "name": "technologist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F4BB", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍💻", "name": "technologist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F4BB", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍💻", "name": "technologist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F4BB", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍💻", "name": "man technologist", "shortname": ":man_technologist:", "unicode": "1F468 200D 1F4BB", "html": "&#x1F468;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍💻", "name": "man technologist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F4BB", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍💻", "name": "man technologist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F4BB", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍💻", "name": "man technologist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F4BB", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍💻", "name": "man technologist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F4BB", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍💻", "name": "man technologist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F4BB", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍💻", "name": "woman technologist", "shortname": ":woman_technologist:", "unicode": "1F469 200D 1F4BB", "html": "&#x1F469;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍💻", "name": "woman technologist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F4BB", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍💻", "name": "woman technologist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F4BB", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍💻", "name": "woman technologist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F4BB", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍💻", "name": "woman technologist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F4BB", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍💻", "name": "woman technologist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F4BB", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F4BB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🎤", "name": "singer", "shortname": ":singer:", "unicode": "1F9D1 200D 1F3A4", "html": "&#x1F9D1;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🎤", "name": "singer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F3A4", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🎤", "name": "singer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F3A4", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🎤", "name": "singer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F3A4", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🎤", "name": "singer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F3A4", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🎤", "name": "singer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F3A4", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🎤", "name": "man singer", "shortname": ":man_singer:", "unicode": "1F468 200D 1F3A4", "html": "&#x1F468;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🎤", "name": "man singer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F3A4", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🎤", "name": "man singer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F3A4", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🎤", "name": "man singer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F3A4", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🎤", "name": "man singer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F3A4", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🎤", "name": "man singer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F3A4", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🎤", "name": "woman singer", "shortname": ":woman_singer:", "unicode": "1F469 200D 1F3A4", "html": "&#x1F469;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🎤", "name": "woman singer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F3A4", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🎤", "name": "woman singer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F3A4", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🎤", "name": "woman singer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F3A4", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🎤", "name": "woman singer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F3A4", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🎤", "name": "woman singer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F3A4", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F3A4;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🎨", "name": "artist", "shortname": ":artist:", "unicode": "1F9D1 200D 1F3A8", "html": "&#x1F9D1;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🎨", "name": "artist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F3A8", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🎨", "name": "artist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F3A8", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🎨", "name": "artist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F3A8", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🎨", "name": "artist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F3A8", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🎨", "name": "artist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F3A8", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🎨", "name": "man artist", "shortname": ":man_artist:", "unicode": "1F468 200D 1F3A8", "html": "&#x1F468;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🎨", "name": "man artist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F3A8", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🎨", "name": "man artist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F3A8", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🎨", "name": "man artist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F3A8", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🎨", "name": "man artist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F3A8", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🎨", "name": "man artist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F3A8", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🎨", "name": "woman artist", "shortname": ":woman_artist:", "unicode": "1F469 200D 1F3A8", "html": "&#x1F469;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🎨", "name": "woman artist: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F3A8", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🎨", "name": "woman artist: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F3A8", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🎨", "name": "woman artist: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F3A8", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🎨", "name": "woman artist: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F3A8", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🎨", "name": "woman artist: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F3A8", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F3A8;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍✈️", "name": "pilot", "shortname": ":pilot:", "unicode": "1F9D1 200D 2708 FE0F", "html": "&#x1F9D1;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍✈", "name": "pilot", "shortname": ":pilot:", "unicode": "1F9D1 200D 2708", "html": "&#x1F9D1;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍✈️", "name": "pilot: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 2708 FE0F", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍✈", "name": "pilot: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 2708", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍✈️", "name": "pilot: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 2708 FE0F", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍✈", "name": "pilot: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 2708", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍✈️", "name": "pilot: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 2708 FE0F", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍✈", "name": "pilot: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 2708", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍✈️", "name": "pilot: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 2708 FE0F", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍✈", "name": "pilot: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 2708", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍✈️", "name": "pilot: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 2708 FE0F", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍✈", "name": "pilot: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 2708", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍✈️", "name": "man pilot", "shortname": ":man_pilot:", "unicode": "1F468 200D 2708 FE0F", "html": "&#x1F468;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍✈", "name": "man pilot", "shortname": ":man_pilot:", "unicode": "1F468 200D 2708", "html": "&#x1F468;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍✈️", "name": "man pilot: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 2708 FE0F", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍✈", "name": "man pilot: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 2708", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍✈️", "name": "man pilot: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 2708 FE0F", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍✈", "name": "man pilot: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 2708", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍✈️", "name": "man pilot: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 2708 FE0F", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍✈", "name": "man pilot: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 2708", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍✈️", "name": "man pilot: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 2708 FE0F", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍✈", "name": "man pilot: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 2708", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍✈️", "name": "man pilot: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 2708 FE0F", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍✈", "name": "man pilot: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 2708", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍✈️", "name": "woman pilot", "shortname": ":woman_pilot:", "unicode": "1F469 200D 2708 FE0F", "html": "&#x1F469;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍✈", "name": "woman pilot", "shortname": ":woman_pilot:", "unicode": "1F469 200D 2708", "html": "&#x1F469;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍✈️", "name": "woman pilot: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 2708 FE0F", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍✈", "name": "woman pilot: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 2708", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍✈️", "name": "woman pilot: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 2708 FE0F", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍✈", "name": "woman pilot: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 2708", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍✈️", "name": "woman pilot: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 2708 FE0F", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍✈", "name": "woman pilot: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 2708", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍✈️", "name": "woman pilot: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 2708 FE0F", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍✈", "name": "woman pilot: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 2708", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍✈️", "name": "woman pilot: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 2708 FE0F", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x2708;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍✈", "name": "woman pilot: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 2708", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x2708;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🚀", "name": "astronaut", "shortname": ":astronaut:", "unicode": "1F9D1 200D 1F680", "html": "&#x1F9D1;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🚀", "name": "astronaut: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F680", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🚀", "name": "astronaut: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F680", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🚀", "name": "astronaut: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F680", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🚀", "name": "astronaut: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F680", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🚀", "name": "astronaut: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F680", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🚀", "name": "man astronaut", "shortname": ":man_astronaut:", "unicode": "1F468 200D 1F680", "html": "&#x1F468;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🚀", "name": "man astronaut: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F680", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🚀", "name": "man astronaut: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F680", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🚀", "name": "man astronaut: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F680", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🚀", "name": "man astronaut: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F680", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🚀", "name": "man astronaut: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F680", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🚀", "name": "woman astronaut", "shortname": ":woman_astronaut:", "unicode": "1F469 200D 1F680", "html": "&#x1F469;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🚀", "name": "woman astronaut: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F680", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🚀", "name": "woman astronaut: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F680", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🚀", "name": "woman astronaut: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F680", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🚀", "name": "woman astronaut: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F680", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🚀", "name": "woman astronaut: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F680", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F680;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑‍🚒", "name": "firefighter", "shortname": ":firefighter:", "unicode": "1F9D1 200D 1F692", "html": "&#x1F9D1;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏻‍🚒", "name": "firefighter: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F692", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏼‍🚒", "name": "firefighter: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F692", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏽‍🚒", "name": "firefighter: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F692", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏾‍🚒", "name": "firefighter: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F692", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧑🏿‍🚒", "name": "firefighter: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F692", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨‍🚒", "name": "man firefighter", "shortname": ":man_firefighter:", "unicode": "1F468 200D 1F692", "html": "&#x1F468;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏻‍🚒", "name": "man firefighter: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F692", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏼‍🚒", "name": "man firefighter: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F692", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏽‍🚒", "name": "man firefighter: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F692", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏾‍🚒", "name": "man firefighter: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F692", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👨🏿‍🚒", "name": "man firefighter: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F692", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩‍🚒", "name": "woman firefighter", "shortname": ":woman_firefighter:", "unicode": "1F469 200D 1F692", "html": "&#x1F469;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏻‍🚒", "name": "woman firefighter: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F692", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏼‍🚒", "name": "woman firefighter: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F692", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏽‍🚒", "name": "woman firefighter: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F692", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏾‍🚒", "name": "woman firefighter: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F692", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👩🏿‍🚒", "name": "woman firefighter: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F692", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F692;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮‍♂️", "name": "man police officer", "shortname": ":man_police_officer:", "unicode": "1F46E 200D 2642 FE0F", "html": "&#x1F46E;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮‍♂", "name": "man police officer", "shortname": ":man_police_officer:", "unicode": "1F46E 200D 2642", "html": "&#x1F46E;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏻‍♂️", "name": "man police officer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F46E 1F3FB 200D 2642 FE0F", "html": "&#x1F46E;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏻‍♂", "name": "man police officer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F46E 1F3FB 200D 2642", "html": "&#x1F46E;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏼‍♂️", "name": "man police officer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F46E 1F3FC 200D 2642 FE0F", "html": "&#x1F46E;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏼‍♂", "name": "man police officer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F46E 1F3FC 200D 2642", "html": "&#x1F46E;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏽‍♂️", "name": "man police officer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F46E 1F3FD 200D 2642 FE0F", "html": "&#x1F46E;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏽‍♂", "name": "man police officer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F46E 1F3FD 200D 2642", "html": "&#x1F46E;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏾‍♂️", "name": "man police officer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F46E 1F3FE 200D 2642 FE0F", "html": "&#x1F46E;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏾‍♂", "name": "man police officer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F46E 1F3FE 200D 2642", "html": "&#x1F46E;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏿‍♂️", "name": "man police officer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F46E 1F3FF 200D 2642 FE0F", "html": "&#x1F46E;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏿‍♂", "name": "man police officer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F46E 1F3FF 200D 2642", "html": "&#x1F46E;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮‍♀️", "name": "woman police officer", "shortname": ":woman_police_officer:", "unicode": "1F46E 200D 2640 FE0F", "html": "&#x1F46E;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏻‍♀️", "name": "woman police officer: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F46E 1F3FB 200D 2640 FE0F", "html": "&#x1F46E;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏼‍♀️", "name": "woman police officer: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F46E 1F3FC 200D 2640 FE0F", "html": "&#x1F46E;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏽‍♀️", "name": "woman police officer: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F46E 1F3FD 200D 2640 FE0F", "html": "&#x1F46E;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏾‍♀️", "name": "woman police officer: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F46E 1F3FE 200D 2640 FE0F", "html": "&#x1F46E;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👮🏿‍♀️", "name": "woman police officer: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F46E 1F3FF 200D 2640 FE0F", "html": "&#x1F46E;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵️", "name": "detective", "shortname": ":detective:", "unicode": "1F575 FE0F", "html": "&#x1F575;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵️‍♂️", "name": "man detective", "shortname": ":man_detective:", "unicode": "1F575 FE0F 200D 2642 FE0F", "html": "&#x1F575;&#xFE0F;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵‍♂️", "name": "man detective", "shortname": ":man_detective:", "unicode": "1F575 200D 2642 FE0F", "html": "&#x1F575;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵️‍♂", "name": "man detective", "shortname": ":man_detective:", "unicode": "1F575 FE0F 200D 2642", "html": "&#x1F575;&#xFE0F;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵‍♂", "name": "man detective", "shortname": ":man_detective:", "unicode": "1F575 200D 2642", "html": "&#x1F575;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏻‍♂️", "name": "man detective: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F575 1F3FB 200D 2642 FE0F", "html": "&#x1F575;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏻‍♂", "name": "man detective: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F575 1F3FB 200D 2642", "html": "&#x1F575;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏼‍♂️", "name": "man detective: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F575 1F3FC 200D 2642 FE0F", "html": "&#x1F575;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏼‍♂", "name": "man detective: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F575 1F3FC 200D 2642", "html": "&#x1F575;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏽‍♂️", "name": "man detective: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F575 1F3FD 200D 2642 FE0F", "html": "&#x1F575;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏽‍♂", "name": "man detective: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F575 1F3FD 200D 2642", "html": "&#x1F575;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏾‍♂️", "name": "man detective: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F575 1F3FE 200D 2642 FE0F", "html": "&#x1F575;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏾‍♂", "name": "man detective: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F575 1F3FE 200D 2642", "html": "&#x1F575;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏿‍♂️", "name": "man detective: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F575 1F3FF 200D 2642 FE0F", "html": "&#x1F575;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏿‍♂", "name": "man detective: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F575 1F3FF 200D 2642", "html": "&#x1F575;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵️‍♀️", "name": "woman detective", "shortname": ":woman_detective:", "unicode": "1F575 FE0F 200D 2640 FE0F", "html": "&#x1F575;&#xFE0F;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵‍♀️", "name": "woman detective", "shortname": ":woman_detective:", "unicode": "1F575 200D 2640 FE0F", "html": "&#x1F575;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵️‍♀", "name": "woman detective", "shortname": ":woman_detective:", "unicode": "1F575 FE0F 200D 2640", "html": "&#x1F575;&#xFE0F;&#x200D;&#x2640;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏻‍♀️", "name": "woman detective: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F575 1F3FB 200D 2640 FE0F", "html": "&#x1F575;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏼‍♀️", "name": "woman detective: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F575 1F3FC 200D 2640 FE0F", "html": "&#x1F575;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏽‍♀️", "name": "woman detective: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F575 1F3FD 200D 2640 FE0F", "html": "&#x1F575;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏾‍♀️", "name": "woman detective: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F575 1F3FE 200D 2640 FE0F", "html": "&#x1F575;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🕵🏿‍♀️", "name": "woman detective: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F575 1F3FF 200D 2640 FE0F", "html": "&#x1F575;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂‍♂️", "name": "man guard", "shortname": ":man_guard:", "unicode": "1F482 200D 2642 FE0F", "html": "&#x1F482;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂‍♂", "name": "man guard", "shortname": ":man_guard:", "unicode": "1F482 200D 2642", "html": "&#x1F482;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏻‍♂️", "name": "man guard: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F482 1F3FB 200D 2642 FE0F", "html": "&#x1F482;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏻‍♂", "name": "man guard: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F482 1F3FB 200D 2642", "html": "&#x1F482;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏼‍♂️", "name": "man guard: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F482 1F3FC 200D 2642 FE0F", "html": "&#x1F482;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏼‍♂", "name": "man guard: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F482 1F3FC 200D 2642", "html": "&#x1F482;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏽‍♂️", "name": "man guard: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F482 1F3FD 200D 2642 FE0F", "html": "&#x1F482;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏽‍♂", "name": "man guard: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F482 1F3FD 200D 2642", "html": "&#x1F482;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏾‍♂️", "name": "man guard: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F482 1F3FE 200D 2642 FE0F", "html": "&#x1F482;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏾‍♂", "name": "man guard: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F482 1F3FE 200D 2642", "html": "&#x1F482;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏿‍♂️", "name": "man guard: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F482 1F3FF 200D 2642 FE0F", "html": "&#x1F482;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏿‍♂", "name": "man guard: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F482 1F3FF 200D 2642", "html": "&#x1F482;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂‍♀️", "name": "woman guard", "shortname": ":woman_guard:", "unicode": "1F482 200D 2640 FE0F", "html": "&#x1F482;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏻‍♀️", "name": "woman guard: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F482 1F3FB 200D 2640 FE0F", "html": "&#x1F482;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏼‍♀️", "name": "woman guard: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F482 1F3FC 200D 2640 FE0F", "html": "&#x1F482;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏽‍♀️", "name": "woman guard: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F482 1F3FD 200D 2640 FE0F", "html": "&#x1F482;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏾‍♀️", "name": "woman guard: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F482 1F3FE 200D 2640 FE0F", "html": "&#x1F482;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "💂🏿‍♀️", "name": "woman guard: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F482 1F3FF 200D 2640 FE0F", "html": "&#x1F482;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷‍♂️", "name": "man construction worker", "shortname": ":man_construction_worker:", "unicode": "1F477 200D 2642 FE0F", "html": "&#x1F477;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷‍♂", "name": "man construction worker", "shortname": ":man_construction_worker:", "unicode": "1F477 200D 2642", "html": "&#x1F477;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏻‍♂️", "name": "man construction worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F477 1F3FB 200D 2642 FE0F", "html": "&#x1F477;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏻‍♂", "name": "man construction worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F477 1F3FB 200D 2642", "html": "&#x1F477;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏼‍♂️", "name": "man construction worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F477 1F3FC 200D 2642 FE0F", "html": "&#x1F477;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏼‍♂", "name": "man construction worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F477 1F3FC 200D 2642", "html": "&#x1F477;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏽‍♂️", "name": "man construction worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F477 1F3FD 200D 2642 FE0F", "html": "&#x1F477;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏽‍♂", "name": "man construction worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F477 1F3FD 200D 2642", "html": "&#x1F477;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏾‍♂️", "name": "man construction worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F477 1F3FE 200D 2642 FE0F", "html": "&#x1F477;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏾‍♂", "name": "man construction worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F477 1F3FE 200D 2642", "html": "&#x1F477;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏿‍♂️", "name": "man construction worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F477 1F3FF 200D 2642 FE0F", "html": "&#x1F477;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏿‍♂", "name": "man construction worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F477 1F3FF 200D 2642", "html": "&#x1F477;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷‍♀️", "name": "woman construction worker", "shortname": ":woman_construction_worker:", "unicode": "1F477 200D 2640 FE0F", "html": "&#x1F477;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏻‍♀️", "name": "woman construction worker: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F477 1F3FB 200D 2640 FE0F", "html": "&#x1F477;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏼‍♀️", "name": "woman construction worker: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F477 1F3FC 200D 2640 FE0F", "html": "&#x1F477;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏽‍♀️", "name": "woman construction worker: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F477 1F3FD 200D 2640 FE0F", "html": "&#x1F477;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏾‍♀️", "name": "woman construction worker: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F477 1F3FE 200D 2640 FE0F", "html": "&#x1F477;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👷🏿‍♀️", "name": "woman construction worker: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F477 1F3FF 200D 2640 FE0F", "html": "&#x1F477;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤴", "name": "prince", "shortname": ":prince:", "unicode": "1F934", "html": "&#x1F934;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤴🏻", "name": "prince: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F934 1F3FB", "html": "&#x1F934;&#x1F3FB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤴🏼", "name": "prince: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F934 1F3FC", "html": "&#x1F934;&#x1F3FC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤴🏽", "name": "prince: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F934 1F3FD", "html": "&#x1F934;&#x1F3FD;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤴🏾", "name": "prince: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F934 1F3FE", "html": "&#x1F934;&#x1F3FE;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤴🏿", "name": "prince: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F934 1F3FF", "html": "&#x1F934;&#x1F3FF;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳‍♂️", "name": "man wearing turban", "shortname": ":man_wearing_turban:", "unicode": "1F473 200D 2642 FE0F", "html": "&#x1F473;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳‍♂", "name": "man wearing turban", "shortname": ":man_wearing_turban:", "unicode": "1F473 200D 2642", "html": "&#x1F473;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏻‍♂️", "name": "man wearing turban: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F473 1F3FB 200D 2642 FE0F", "html": "&#x1F473;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏻‍♂", "name": "man wearing turban: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F473 1F3FB 200D 2642", "html": "&#x1F473;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏼‍♂️", "name": "man wearing turban: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F473 1F3FC 200D 2642 FE0F", "html": "&#x1F473;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏼‍♂", "name": "man wearing turban: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F473 1F3FC 200D 2642", "html": "&#x1F473;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏽‍♂️", "name": "man wearing turban: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F473 1F3FD 200D 2642 FE0F", "html": "&#x1F473;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏽‍♂", "name": "man wearing turban: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F473 1F3FD 200D 2642", "html": "&#x1F473;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏾‍♂️", "name": "man wearing turban: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F473 1F3FE 200D 2642 FE0F", "html": "&#x1F473;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏾‍♂", "name": "man wearing turban: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F473 1F3FE 200D 2642", "html": "&#x1F473;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏿‍♂️", "name": "man wearing turban: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F473 1F3FF 200D 2642 FE0F", "html": "&#x1F473;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏿‍♂", "name": "man wearing turban: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F473 1F3FF 200D 2642", "html": "&#x1F473;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳‍♀️", "name": "woman wearing turban", "shortname": ":woman_wearing_turban:", "unicode": "1F473 200D 2640 FE0F", "html": "&#x1F473;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏻‍♀️", "name": "woman wearing turban: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F473 1F3FB 200D 2640 FE0F", "html": "&#x1F473;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏼‍♀️", "name": "woman wearing turban: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F473 1F3FC 200D 2640 FE0F", "html": "&#x1F473;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏽‍♀️", "name": "woman wearing turban: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F473 1F3FD 200D 2640 FE0F", "html": "&#x1F473;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏾‍♀️", "name": "woman wearing turban: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F473 1F3FE 200D 2640 FE0F", "html": "&#x1F473;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "👳🏿‍♀️", "name": "woman wearing turban: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F473 1F3FF 200D 2640 FE0F", "html": "&#x1F473;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧕", "name": "woman with headscarf", "shortname": ":woman_with_headscarf:", "unicode": "1F9D5", "html": "&#x1F9D5;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧕🏻", "name": "woman with headscarf: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D5 1F3FB", "html": "&#x1F9D5;&#x1F3FB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧕🏼", "name": "woman with headscarf: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D5 1F3FC", "html": "&#x1F9D5;&#x1F3FC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧕🏽", "name": "woman with headscarf: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D5 1F3FD", "html": "&#x1F9D5;&#x1F3FD;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧕🏾", "name": "woman with headscarf: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D5 1F3FE", "html": "&#x1F9D5;&#x1F3FE;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🧕🏿", "name": "woman with headscarf: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D5 1F3FF", "html": "&#x1F9D5;&#x1F3FF;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤵", "name": "man in tuxedo", "shortname": ":man_in_tuxedo:", "unicode": "1F935", "html": "&#x1F935;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤵🏻", "name": "man in tuxedo: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F935 1F3FB", "html": "&#x1F935;&#x1F3FB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤵🏼", "name": "man in tuxedo: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F935 1F3FC", "html": "&#x1F935;&#x1F3FC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤵🏽", "name": "man in tuxedo: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F935 1F3FD", "html": "&#x1F935;&#x1F3FD;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤵🏾", "name": "man in tuxedo: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F935 1F3FE", "html": "&#x1F935;&#x1F3FE;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤵🏿", "name": "man in tuxedo: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F935 1F3FF", "html": "&#x1F935;&#x1F3FF;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤰", "name": "pregnant woman", "shortname": ":pregnant_woman:", "unicode": "1F930", "html": "&#x1F930;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤰🏻", "name": "pregnant woman: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F930 1F3FB", "html": "&#x1F930;&#x1F3FB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤰🏼", "name": "pregnant woman: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F930 1F3FC", "html": "&#x1F930;&#x1F3FC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤰🏽", "name": "pregnant woman: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F930 1F3FD", "html": "&#x1F930;&#x1F3FD;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤰🏾", "name": "pregnant woman: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F930 1F3FE", "html": "&#x1F930;&#x1F3FE;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤰🏿", "name": "pregnant woman: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F930 1F3FF", "html": "&#x1F930;&#x1F3FF;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤱", "name": "breast-feeding", "shortname": ":breastfeeding:", "unicode": "1F931", "html": "&#x1F931;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤱🏻", "name": "breast-feeding: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F931 1F3FB", "html": "&#x1F931;&#x1F3FB;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤱🏼", "name": "breast-feeding: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F931 1F3FC", "html": "&#x1F931;&#x1F3FC;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤱🏽", "name": "breast-feeding: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F931 1F3FD", "html": "&#x1F931;&#x1F3FD;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤱🏾", "name": "breast-feeding: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F931 1F3FE", "html": "&#x1F931;&#x1F3FE;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤱🏿", "name": "breast-feeding: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F931 1F3FF", "html": "&#x1F931;&#x1F3FF;", "category": "People & Body (person-role)", "order": ""},
+ {"emoji": "🤶", "name": "Mrs. Claus", "shortname": ":Mrs_Claus:", "unicode": "1F936", "html": "&#x1F936;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🤶🏻", "name": "Mrs. Claus: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F936 1F3FB", "html": "&#x1F936;&#x1F3FB;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🤶🏼", "name": "Mrs. Claus: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F936 1F3FC", "html": "&#x1F936;&#x1F3FC;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🤶🏽", "name": "Mrs. Claus: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F936 1F3FD", "html": "&#x1F936;&#x1F3FD;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🤶🏾", "name": "Mrs. Claus: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F936 1F3FE", "html": "&#x1F936;&#x1F3FE;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🤶🏿", "name": "Mrs. Claus: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F936 1F3FF", "html": "&#x1F936;&#x1F3FF;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸", "name": "superhero", "shortname": ":superhero:", "unicode": "1F9B8", "html": "&#x1F9B8;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏻", "name": "superhero: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B8 1F3FB", "html": "&#x1F9B8;&#x1F3FB;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏼", "name": "superhero: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B8 1F3FC", "html": "&#x1F9B8;&#x1F3FC;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏽", "name": "superhero: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B8 1F3FD", "html": "&#x1F9B8;&#x1F3FD;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏾", "name": "superhero: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B8 1F3FE", "html": "&#x1F9B8;&#x1F3FE;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏿", "name": "superhero: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B8 1F3FF", "html": "&#x1F9B8;&#x1F3FF;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸‍♂️", "name": "man superhero", "shortname": ":man_superhero:", "unicode": "1F9B8 200D 2642 FE0F", "html": "&#x1F9B8;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸‍♂", "name": "man superhero", "shortname": ":man_superhero:", "unicode": "1F9B8 200D 2642", "html": "&#x1F9B8;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏻‍♂️", "name": "man superhero: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B8 1F3FB 200D 2642 FE0F", "html": "&#x1F9B8;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏻‍♂", "name": "man superhero: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B8 1F3FB 200D 2642", "html": "&#x1F9B8;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏼‍♂️", "name": "man superhero: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B8 1F3FC 200D 2642 FE0F", "html": "&#x1F9B8;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏼‍♂", "name": "man superhero: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B8 1F3FC 200D 2642", "html": "&#x1F9B8;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏽‍♂️", "name": "man superhero: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B8 1F3FD 200D 2642 FE0F", "html": "&#x1F9B8;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏽‍♂", "name": "man superhero: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B8 1F3FD 200D 2642", "html": "&#x1F9B8;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏾‍♂️", "name": "man superhero: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B8 1F3FE 200D 2642 FE0F", "html": "&#x1F9B8;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏾‍♂", "name": "man superhero: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B8 1F3FE 200D 2642", "html": "&#x1F9B8;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏿‍♂️", "name": "man superhero: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B8 1F3FF 200D 2642 FE0F", "html": "&#x1F9B8;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏿‍♂", "name": "man superhero: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B8 1F3FF 200D 2642", "html": "&#x1F9B8;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸‍♀️", "name": "woman superhero", "shortname": ":woman_superhero:", "unicode": "1F9B8 200D 2640 FE0F", "html": "&#x1F9B8;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸‍♀", "name": "woman superhero", "shortname": ":woman_superhero:", "unicode": "1F9B8 200D 2640", "html": "&#x1F9B8;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏻‍♀️", "name": "woman superhero: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B8 1F3FB 200D 2640 FE0F", "html": "&#x1F9B8;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏻‍♀", "name": "woman superhero: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B8 1F3FB 200D 2640", "html": "&#x1F9B8;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏼‍♀️", "name": "woman superhero: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B8 1F3FC 200D 2640 FE0F", "html": "&#x1F9B8;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏼‍♀", "name": "woman superhero: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B8 1F3FC 200D 2640", "html": "&#x1F9B8;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏽‍♀️", "name": "woman superhero: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B8 1F3FD 200D 2640 FE0F", "html": "&#x1F9B8;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏽‍♀", "name": "woman superhero: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B8 1F3FD 200D 2640", "html": "&#x1F9B8;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏾‍♀️", "name": "woman superhero: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B8 1F3FE 200D 2640 FE0F", "html": "&#x1F9B8;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏾‍♀", "name": "woman superhero: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B8 1F3FE 200D 2640", "html": "&#x1F9B8;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏿‍♀️", "name": "woman superhero: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B8 1F3FF 200D 2640 FE0F", "html": "&#x1F9B8;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦸🏿‍♀", "name": "woman superhero: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B8 1F3FF 200D 2640", "html": "&#x1F9B8;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹", "name": "supervillain", "shortname": ":supervillain:", "unicode": "1F9B9", "html": "&#x1F9B9;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏻", "name": "supervillain: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B9 1F3FB", "html": "&#x1F9B9;&#x1F3FB;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏼", "name": "supervillain: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B9 1F3FC", "html": "&#x1F9B9;&#x1F3FC;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏽", "name": "supervillain: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B9 1F3FD", "html": "&#x1F9B9;&#x1F3FD;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏾", "name": "supervillain: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B9 1F3FE", "html": "&#x1F9B9;&#x1F3FE;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏿", "name": "supervillain: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B9 1F3FF", "html": "&#x1F9B9;&#x1F3FF;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹‍♂️", "name": "man supervillain", "shortname": ":man_supervillain:", "unicode": "1F9B9 200D 2642 FE0F", "html": "&#x1F9B9;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹‍♂", "name": "man supervillain", "shortname": ":man_supervillain:", "unicode": "1F9B9 200D 2642", "html": "&#x1F9B9;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏻‍♂️", "name": "man supervillain: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B9 1F3FB 200D 2642 FE0F", "html": "&#x1F9B9;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏻‍♂", "name": "man supervillain: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B9 1F3FB 200D 2642", "html": "&#x1F9B9;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏼‍♂️", "name": "man supervillain: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B9 1F3FC 200D 2642 FE0F", "html": "&#x1F9B9;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏼‍♂", "name": "man supervillain: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B9 1F3FC 200D 2642", "html": "&#x1F9B9;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏽‍♂️", "name": "man supervillain: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B9 1F3FD 200D 2642 FE0F", "html": "&#x1F9B9;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏽‍♂", "name": "man supervillain: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B9 1F3FD 200D 2642", "html": "&#x1F9B9;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏾‍♂️", "name": "man supervillain: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B9 1F3FE 200D 2642 FE0F", "html": "&#x1F9B9;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏾‍♂", "name": "man supervillain: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B9 1F3FE 200D 2642", "html": "&#x1F9B9;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏿‍♂️", "name": "man supervillain: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B9 1F3FF 200D 2642 FE0F", "html": "&#x1F9B9;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏿‍♂", "name": "man supervillain: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B9 1F3FF 200D 2642", "html": "&#x1F9B9;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹‍♀️", "name": "woman supervillain", "shortname": ":woman_supervillain:", "unicode": "1F9B9 200D 2640 FE0F", "html": "&#x1F9B9;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹‍♀", "name": "woman supervillain", "shortname": ":woman_supervillain:", "unicode": "1F9B9 200D 2640", "html": "&#x1F9B9;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏻‍♀️", "name": "woman supervillain: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B9 1F3FB 200D 2640 FE0F", "html": "&#x1F9B9;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏻‍♀", "name": "woman supervillain: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9B9 1F3FB 200D 2640", "html": "&#x1F9B9;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏼‍♀️", "name": "woman supervillain: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B9 1F3FC 200D 2640 FE0F", "html": "&#x1F9B9;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏼‍♀", "name": "woman supervillain: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9B9 1F3FC 200D 2640", "html": "&#x1F9B9;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏽‍♀️", "name": "woman supervillain: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B9 1F3FD 200D 2640 FE0F", "html": "&#x1F9B9;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏽‍♀", "name": "woman supervillain: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9B9 1F3FD 200D 2640", "html": "&#x1F9B9;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏾‍♀️", "name": "woman supervillain: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B9 1F3FE 200D 2640 FE0F", "html": "&#x1F9B9;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏾‍♀", "name": "woman supervillain: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9B9 1F3FE 200D 2640", "html": "&#x1F9B9;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏿‍♀️", "name": "woman supervillain: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B9 1F3FF 200D 2640 FE0F", "html": "&#x1F9B9;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🦹🏿‍♀", "name": "woman supervillain: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9B9 1F3FF 200D 2640", "html": "&#x1F9B9;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙", "name": "mage", "shortname": ":mage:", "unicode": "1F9D9", "html": "&#x1F9D9;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏻", "name": "mage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D9 1F3FB", "html": "&#x1F9D9;&#x1F3FB;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏼", "name": "mage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D9 1F3FC", "html": "&#x1F9D9;&#x1F3FC;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏽", "name": "mage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D9 1F3FD", "html": "&#x1F9D9;&#x1F3FD;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏾", "name": "mage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D9 1F3FE", "html": "&#x1F9D9;&#x1F3FE;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏿", "name": "mage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D9 1F3FF", "html": "&#x1F9D9;&#x1F3FF;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙‍♂️", "name": "man mage", "shortname": ":man_mage:", "unicode": "1F9D9 200D 2642 FE0F", "html": "&#x1F9D9;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙‍♂", "name": "man mage", "shortname": ":man_mage:", "unicode": "1F9D9 200D 2642", "html": "&#x1F9D9;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏻‍♂️", "name": "man mage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D9 1F3FB 200D 2642 FE0F", "html": "&#x1F9D9;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏻‍♂", "name": "man mage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D9 1F3FB 200D 2642", "html": "&#x1F9D9;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏼‍♂️", "name": "man mage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D9 1F3FC 200D 2642 FE0F", "html": "&#x1F9D9;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏼‍♂", "name": "man mage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D9 1F3FC 200D 2642", "html": "&#x1F9D9;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏽‍♂️", "name": "man mage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D9 1F3FD 200D 2642 FE0F", "html": "&#x1F9D9;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏽‍♂", "name": "man mage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D9 1F3FD 200D 2642", "html": "&#x1F9D9;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏾‍♂️", "name": "man mage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D9 1F3FE 200D 2642 FE0F", "html": "&#x1F9D9;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏾‍♂", "name": "man mage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D9 1F3FE 200D 2642", "html": "&#x1F9D9;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏿‍♂️", "name": "man mage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D9 1F3FF 200D 2642 FE0F", "html": "&#x1F9D9;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏿‍♂", "name": "man mage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D9 1F3FF 200D 2642", "html": "&#x1F9D9;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙‍♀️", "name": "woman mage", "shortname": ":woman_mage:", "unicode": "1F9D9 200D 2640 FE0F", "html": "&#x1F9D9;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙‍♀", "name": "woman mage", "shortname": ":woman_mage:", "unicode": "1F9D9 200D 2640", "html": "&#x1F9D9;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏻‍♀️", "name": "woman mage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D9 1F3FB 200D 2640 FE0F", "html": "&#x1F9D9;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏻‍♀", "name": "woman mage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D9 1F3FB 200D 2640", "html": "&#x1F9D9;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏼‍♀️", "name": "woman mage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D9 1F3FC 200D 2640 FE0F", "html": "&#x1F9D9;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏼‍♀", "name": "woman mage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D9 1F3FC 200D 2640", "html": "&#x1F9D9;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏽‍♀️", "name": "woman mage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D9 1F3FD 200D 2640 FE0F", "html": "&#x1F9D9;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏽‍♀", "name": "woman mage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D9 1F3FD 200D 2640", "html": "&#x1F9D9;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏾‍♀️", "name": "woman mage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D9 1F3FE 200D 2640 FE0F", "html": "&#x1F9D9;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏾‍♀", "name": "woman mage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D9 1F3FE 200D 2640", "html": "&#x1F9D9;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏿‍♀️", "name": "woman mage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D9 1F3FF 200D 2640 FE0F", "html": "&#x1F9D9;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧙🏿‍♀", "name": "woman mage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D9 1F3FF 200D 2640", "html": "&#x1F9D9;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚", "name": "fairy", "shortname": ":fairy:", "unicode": "1F9DA", "html": "&#x1F9DA;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏻", "name": "fairy: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DA 1F3FB", "html": "&#x1F9DA;&#x1F3FB;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏼", "name": "fairy: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DA 1F3FC", "html": "&#x1F9DA;&#x1F3FC;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏽", "name": "fairy: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DA 1F3FD", "html": "&#x1F9DA;&#x1F3FD;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏾", "name": "fairy: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DA 1F3FE", "html": "&#x1F9DA;&#x1F3FE;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏿", "name": "fairy: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DA 1F3FF", "html": "&#x1F9DA;&#x1F3FF;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚‍♂️", "name": "man fairy", "shortname": ":man_fairy:", "unicode": "1F9DA 200D 2642 FE0F", "html": "&#x1F9DA;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚‍♂", "name": "man fairy", "shortname": ":man_fairy:", "unicode": "1F9DA 200D 2642", "html": "&#x1F9DA;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏻‍♂️", "name": "man fairy: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DA 1F3FB 200D 2642 FE0F", "html": "&#x1F9DA;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏻‍♂", "name": "man fairy: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DA 1F3FB 200D 2642", "html": "&#x1F9DA;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏼‍♂️", "name": "man fairy: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DA 1F3FC 200D 2642 FE0F", "html": "&#x1F9DA;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏼‍♂", "name": "man fairy: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DA 1F3FC 200D 2642", "html": "&#x1F9DA;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏽‍♂️", "name": "man fairy: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DA 1F3FD 200D 2642 FE0F", "html": "&#x1F9DA;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏽‍♂", "name": "man fairy: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DA 1F3FD 200D 2642", "html": "&#x1F9DA;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏾‍♂️", "name": "man fairy: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DA 1F3FE 200D 2642 FE0F", "html": "&#x1F9DA;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏾‍♂", "name": "man fairy: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DA 1F3FE 200D 2642", "html": "&#x1F9DA;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏿‍♂️", "name": "man fairy: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DA 1F3FF 200D 2642 FE0F", "html": "&#x1F9DA;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏿‍♂", "name": "man fairy: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DA 1F3FF 200D 2642", "html": "&#x1F9DA;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚‍♀️", "name": "woman fairy", "shortname": ":woman_fairy:", "unicode": "1F9DA 200D 2640 FE0F", "html": "&#x1F9DA;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚‍♀", "name": "woman fairy", "shortname": ":woman_fairy:", "unicode": "1F9DA 200D 2640", "html": "&#x1F9DA;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏻‍♀️", "name": "woman fairy: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DA 1F3FB 200D 2640 FE0F", "html": "&#x1F9DA;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏻‍♀", "name": "woman fairy: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DA 1F3FB 200D 2640", "html": "&#x1F9DA;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏼‍♀️", "name": "woman fairy: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DA 1F3FC 200D 2640 FE0F", "html": "&#x1F9DA;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏼‍♀", "name": "woman fairy: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DA 1F3FC 200D 2640", "html": "&#x1F9DA;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏽‍♀️", "name": "woman fairy: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DA 1F3FD 200D 2640 FE0F", "html": "&#x1F9DA;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏽‍♀", "name": "woman fairy: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DA 1F3FD 200D 2640", "html": "&#x1F9DA;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏾‍♀️", "name": "woman fairy: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DA 1F3FE 200D 2640 FE0F", "html": "&#x1F9DA;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏾‍♀", "name": "woman fairy: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DA 1F3FE 200D 2640", "html": "&#x1F9DA;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏿‍♀️", "name": "woman fairy: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DA 1F3FF 200D 2640 FE0F", "html": "&#x1F9DA;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧚🏿‍♀", "name": "woman fairy: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DA 1F3FF 200D 2640", "html": "&#x1F9DA;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛", "name": "vampire", "shortname": ":vampire:", "unicode": "1F9DB", "html": "&#x1F9DB;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏻", "name": "vampire: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DB 1F3FB", "html": "&#x1F9DB;&#x1F3FB;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏼", "name": "vampire: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DB 1F3FC", "html": "&#x1F9DB;&#x1F3FC;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏽", "name": "vampire: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DB 1F3FD", "html": "&#x1F9DB;&#x1F3FD;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏾", "name": "vampire: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DB 1F3FE", "html": "&#x1F9DB;&#x1F3FE;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏿", "name": "vampire: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DB 1F3FF", "html": "&#x1F9DB;&#x1F3FF;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛‍♂️", "name": "man vampire", "shortname": ":man_vampire:", "unicode": "1F9DB 200D 2642 FE0F", "html": "&#x1F9DB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛‍♂", "name": "man vampire", "shortname": ":man_vampire:", "unicode": "1F9DB 200D 2642", "html": "&#x1F9DB;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏻‍♂️", "name": "man vampire: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DB 1F3FB 200D 2642 FE0F", "html": "&#x1F9DB;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏻‍♂", "name": "man vampire: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DB 1F3FB 200D 2642", "html": "&#x1F9DB;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏼‍♂️", "name": "man vampire: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DB 1F3FC 200D 2642 FE0F", "html": "&#x1F9DB;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏼‍♂", "name": "man vampire: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DB 1F3FC 200D 2642", "html": "&#x1F9DB;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏽‍♂️", "name": "man vampire: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DB 1F3FD 200D 2642 FE0F", "html": "&#x1F9DB;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏽‍♂", "name": "man vampire: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DB 1F3FD 200D 2642", "html": "&#x1F9DB;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏾‍♂️", "name": "man vampire: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DB 1F3FE 200D 2642 FE0F", "html": "&#x1F9DB;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏾‍♂", "name": "man vampire: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DB 1F3FE 200D 2642", "html": "&#x1F9DB;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏿‍♂️", "name": "man vampire: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DB 1F3FF 200D 2642 FE0F", "html": "&#x1F9DB;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏿‍♂", "name": "man vampire: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DB 1F3FF 200D 2642", "html": "&#x1F9DB;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛‍♀️", "name": "woman vampire", "shortname": ":woman_vampire:", "unicode": "1F9DB 200D 2640 FE0F", "html": "&#x1F9DB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛‍♀", "name": "woman vampire", "shortname": ":woman_vampire:", "unicode": "1F9DB 200D 2640", "html": "&#x1F9DB;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏻‍♀️", "name": "woman vampire: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DB 1F3FB 200D 2640 FE0F", "html": "&#x1F9DB;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏻‍♀", "name": "woman vampire: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DB 1F3FB 200D 2640", "html": "&#x1F9DB;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏼‍♀️", "name": "woman vampire: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DB 1F3FC 200D 2640 FE0F", "html": "&#x1F9DB;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏼‍♀", "name": "woman vampire: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DB 1F3FC 200D 2640", "html": "&#x1F9DB;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏽‍♀️", "name": "woman vampire: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DB 1F3FD 200D 2640 FE0F", "html": "&#x1F9DB;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏽‍♀", "name": "woman vampire: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DB 1F3FD 200D 2640", "html": "&#x1F9DB;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏾‍♀️", "name": "woman vampire: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DB 1F3FE 200D 2640 FE0F", "html": "&#x1F9DB;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏾‍♀", "name": "woman vampire: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DB 1F3FE 200D 2640", "html": "&#x1F9DB;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏿‍♀️", "name": "woman vampire: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DB 1F3FF 200D 2640 FE0F", "html": "&#x1F9DB;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧛🏿‍♀", "name": "woman vampire: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DB 1F3FF 200D 2640", "html": "&#x1F9DB;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜", "name": "merperson", "shortname": ":merperson:", "unicode": "1F9DC", "html": "&#x1F9DC;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏻", "name": "merperson: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DC 1F3FB", "html": "&#x1F9DC;&#x1F3FB;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏼", "name": "merperson: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DC 1F3FC", "html": "&#x1F9DC;&#x1F3FC;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏽", "name": "merperson: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DC 1F3FD", "html": "&#x1F9DC;&#x1F3FD;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏾", "name": "merperson: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DC 1F3FE", "html": "&#x1F9DC;&#x1F3FE;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏿", "name": "merperson: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DC 1F3FF", "html": "&#x1F9DC;&#x1F3FF;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜‍♂️", "name": "merman", "shortname": ":merman:", "unicode": "1F9DC 200D 2642 FE0F", "html": "&#x1F9DC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜‍♂", "name": "merman", "shortname": ":merman:", "unicode": "1F9DC 200D 2642", "html": "&#x1F9DC;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏻‍♂️", "name": "merman: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DC 1F3FB 200D 2642 FE0F", "html": "&#x1F9DC;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏻‍♂", "name": "merman: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DC 1F3FB 200D 2642", "html": "&#x1F9DC;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏼‍♂️", "name": "merman: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DC 1F3FC 200D 2642 FE0F", "html": "&#x1F9DC;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏼‍♂", "name": "merman: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DC 1F3FC 200D 2642", "html": "&#x1F9DC;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏽‍♂️", "name": "merman: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DC 1F3FD 200D 2642 FE0F", "html": "&#x1F9DC;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏽‍♂", "name": "merman: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DC 1F3FD 200D 2642", "html": "&#x1F9DC;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏾‍♂️", "name": "merman: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DC 1F3FE 200D 2642 FE0F", "html": "&#x1F9DC;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏾‍♂", "name": "merman: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DC 1F3FE 200D 2642", "html": "&#x1F9DC;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏿‍♂️", "name": "merman: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DC 1F3FF 200D 2642 FE0F", "html": "&#x1F9DC;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏿‍♂", "name": "merman: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DC 1F3FF 200D 2642", "html": "&#x1F9DC;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜‍♀️", "name": "mermaid", "shortname": ":mermaid:", "unicode": "1F9DC 200D 2640 FE0F", "html": "&#x1F9DC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜‍♀", "name": "mermaid", "shortname": ":mermaid:", "unicode": "1F9DC 200D 2640", "html": "&#x1F9DC;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏻‍♀️", "name": "mermaid: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DC 1F3FB 200D 2640 FE0F", "html": "&#x1F9DC;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏻‍♀", "name": "mermaid: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DC 1F3FB 200D 2640", "html": "&#x1F9DC;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏼‍♀️", "name": "mermaid: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DC 1F3FC 200D 2640 FE0F", "html": "&#x1F9DC;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏼‍♀", "name": "mermaid: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DC 1F3FC 200D 2640", "html": "&#x1F9DC;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏽‍♀️", "name": "mermaid: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DC 1F3FD 200D 2640 FE0F", "html": "&#x1F9DC;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏽‍♀", "name": "mermaid: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DC 1F3FD 200D 2640", "html": "&#x1F9DC;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏾‍♀️", "name": "mermaid: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DC 1F3FE 200D 2640 FE0F", "html": "&#x1F9DC;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏾‍♀", "name": "mermaid: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DC 1F3FE 200D 2640", "html": "&#x1F9DC;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏿‍♀️", "name": "mermaid: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DC 1F3FF 200D 2640 FE0F", "html": "&#x1F9DC;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧜🏿‍♀", "name": "mermaid: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DC 1F3FF 200D 2640", "html": "&#x1F9DC;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝", "name": "elf", "shortname": ":elf:", "unicode": "1F9DD", "html": "&#x1F9DD;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏻", "name": "elf: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DD 1F3FB", "html": "&#x1F9DD;&#x1F3FB;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏼", "name": "elf: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DD 1F3FC", "html": "&#x1F9DD;&#x1F3FC;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏽", "name": "elf: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DD 1F3FD", "html": "&#x1F9DD;&#x1F3FD;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏾", "name": "elf: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DD 1F3FE", "html": "&#x1F9DD;&#x1F3FE;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏿", "name": "elf: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DD 1F3FF", "html": "&#x1F9DD;&#x1F3FF;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝‍♂️", "name": "man elf", "shortname": ":man_elf:", "unicode": "1F9DD 200D 2642 FE0F", "html": "&#x1F9DD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝‍♂", "name": "man elf", "shortname": ":man_elf:", "unicode": "1F9DD 200D 2642", "html": "&#x1F9DD;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏻‍♂️", "name": "man elf: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DD 1F3FB 200D 2642 FE0F", "html": "&#x1F9DD;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏻‍♂", "name": "man elf: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DD 1F3FB 200D 2642", "html": "&#x1F9DD;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏼‍♂️", "name": "man elf: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DD 1F3FC 200D 2642 FE0F", "html": "&#x1F9DD;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏼‍♂", "name": "man elf: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DD 1F3FC 200D 2642", "html": "&#x1F9DD;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏽‍♂️", "name": "man elf: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DD 1F3FD 200D 2642 FE0F", "html": "&#x1F9DD;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏽‍♂", "name": "man elf: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DD 1F3FD 200D 2642", "html": "&#x1F9DD;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏾‍♂️", "name": "man elf: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DD 1F3FE 200D 2642 FE0F", "html": "&#x1F9DD;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏾‍♂", "name": "man elf: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DD 1F3FE 200D 2642", "html": "&#x1F9DD;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏿‍♂️", "name": "man elf: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DD 1F3FF 200D 2642 FE0F", "html": "&#x1F9DD;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏿‍♂", "name": "man elf: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DD 1F3FF 200D 2642", "html": "&#x1F9DD;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝‍♀️", "name": "woman elf", "shortname": ":woman_elf:", "unicode": "1F9DD 200D 2640 FE0F", "html": "&#x1F9DD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝‍♀", "name": "woman elf", "shortname": ":woman_elf:", "unicode": "1F9DD 200D 2640", "html": "&#x1F9DD;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏻‍♀️", "name": "woman elf: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DD 1F3FB 200D 2640 FE0F", "html": "&#x1F9DD;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏻‍♀", "name": "woman elf: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9DD 1F3FB 200D 2640", "html": "&#x1F9DD;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏼‍♀️", "name": "woman elf: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DD 1F3FC 200D 2640 FE0F", "html": "&#x1F9DD;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏼‍♀", "name": "woman elf: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9DD 1F3FC 200D 2640", "html": "&#x1F9DD;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏽‍♀️", "name": "woman elf: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DD 1F3FD 200D 2640 FE0F", "html": "&#x1F9DD;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏽‍♀", "name": "woman elf: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9DD 1F3FD 200D 2640", "html": "&#x1F9DD;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏾‍♀️", "name": "woman elf: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DD 1F3FE 200D 2640 FE0F", "html": "&#x1F9DD;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏾‍♀", "name": "woman elf: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9DD 1F3FE 200D 2640", "html": "&#x1F9DD;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏿‍♀️", "name": "woman elf: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DD 1F3FF 200D 2640 FE0F", "html": "&#x1F9DD;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧝🏿‍♀", "name": "woman elf: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9DD 1F3FF 200D 2640", "html": "&#x1F9DD;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧞", "name": "genie", "shortname": ":genie:", "unicode": "1F9DE", "html": "&#x1F9DE;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧞‍♂️", "name": "man genie", "shortname": ":man_genie:", "unicode": "1F9DE 200D 2642 FE0F", "html": "&#x1F9DE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧞‍♂", "name": "man genie", "shortname": ":man_genie:", "unicode": "1F9DE 200D 2642", "html": "&#x1F9DE;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧞‍♀️", "name": "woman genie", "shortname": ":woman_genie:", "unicode": "1F9DE 200D 2640 FE0F", "html": "&#x1F9DE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧞‍♀", "name": "woman genie", "shortname": ":woman_genie:", "unicode": "1F9DE 200D 2640", "html": "&#x1F9DE;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧟", "name": "zombie", "shortname": ":zombie:", "unicode": "1F9DF", "html": "&#x1F9DF;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧟‍♂️", "name": "man zombie", "shortname": ":man_zombie:", "unicode": "1F9DF 200D 2642 FE0F", "html": "&#x1F9DF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧟‍♂", "name": "man zombie", "shortname": ":man_zombie:", "unicode": "1F9DF 200D 2642", "html": "&#x1F9DF;&#x200D;&#x2642;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧟‍♀️", "name": "woman zombie", "shortname": ":woman_zombie:", "unicode": "1F9DF 200D 2640 FE0F", "html": "&#x1F9DF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "🧟‍♀", "name": "woman zombie", "shortname": ":woman_zombie:", "unicode": "1F9DF 200D 2640", "html": "&#x1F9DF;&#x200D;&#x2640;", "category": "People & Body (person-fantasy)", "order": ""},
+ {"emoji": "💆‍♂️", "name": "man getting massage", "shortname": ":man_getting_massage:", "unicode": "1F486 200D 2642 FE0F", "html": "&#x1F486;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏻‍♂️", "name": "man getting massage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F486 1F3FB 200D 2642 FE0F", "html": "&#x1F486;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏼‍♂️", "name": "man getting massage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F486 1F3FC 200D 2642 FE0F", "html": "&#x1F486;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏽‍♂️", "name": "man getting massage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F486 1F3FD 200D 2642 FE0F", "html": "&#x1F486;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏾‍♂️", "name": "man getting massage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F486 1F3FE 200D 2642 FE0F", "html": "&#x1F486;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏿‍♂️", "name": "man getting massage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F486 1F3FF 200D 2642 FE0F", "html": "&#x1F486;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆‍♀️", "name": "woman getting massage", "shortname": ":woman_getting_massage:", "unicode": "1F486 200D 2640 FE0F", "html": "&#x1F486;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆‍♀", "name": "woman getting massage", "shortname": ":woman_getting_massage:", "unicode": "1F486 200D 2640", "html": "&#x1F486;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏻‍♀️", "name": "woman getting massage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F486 1F3FB 200D 2640 FE0F", "html": "&#x1F486;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏻‍♀", "name": "woman getting massage: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F486 1F3FB 200D 2640", "html": "&#x1F486;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏼‍♀️", "name": "woman getting massage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F486 1F3FC 200D 2640 FE0F", "html": "&#x1F486;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏼‍♀", "name": "woman getting massage: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F486 1F3FC 200D 2640", "html": "&#x1F486;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏽‍♀️", "name": "woman getting massage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F486 1F3FD 200D 2640 FE0F", "html": "&#x1F486;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏽‍♀", "name": "woman getting massage: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F486 1F3FD 200D 2640", "html": "&#x1F486;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏾‍♀️", "name": "woman getting massage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F486 1F3FE 200D 2640 FE0F", "html": "&#x1F486;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏾‍♀", "name": "woman getting massage: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F486 1F3FE 200D 2640", "html": "&#x1F486;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏿‍♀️", "name": "woman getting massage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F486 1F3FF 200D 2640 FE0F", "html": "&#x1F486;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💆🏿‍♀", "name": "woman getting massage: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F486 1F3FF 200D 2640", "html": "&#x1F486;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇‍♂️", "name": "man getting haircut", "shortname": ":man_getting_haircut:", "unicode": "1F487 200D 2642 FE0F", "html": "&#x1F487;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏻‍♂️", "name": "man getting haircut: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F487 1F3FB 200D 2642 FE0F", "html": "&#x1F487;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏼‍♂️", "name": "man getting haircut: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F487 1F3FC 200D 2642 FE0F", "html": "&#x1F487;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏽‍♂️", "name": "man getting haircut: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F487 1F3FD 200D 2642 FE0F", "html": "&#x1F487;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏾‍♂️", "name": "man getting haircut: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F487 1F3FE 200D 2642 FE0F", "html": "&#x1F487;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏿‍♂️", "name": "man getting haircut: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F487 1F3FF 200D 2642 FE0F", "html": "&#x1F487;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇‍♀️", "name": "woman getting haircut", "shortname": ":woman_getting_haircut:", "unicode": "1F487 200D 2640 FE0F", "html": "&#x1F487;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇‍♀", "name": "woman getting haircut", "shortname": ":woman_getting_haircut:", "unicode": "1F487 200D 2640", "html": "&#x1F487;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏻‍♀️", "name": "woman getting haircut: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F487 1F3FB 200D 2640 FE0F", "html": "&#x1F487;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏻‍♀", "name": "woman getting haircut: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F487 1F3FB 200D 2640", "html": "&#x1F487;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏼‍♀️", "name": "woman getting haircut: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F487 1F3FC 200D 2640 FE0F", "html": "&#x1F487;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏼‍♀", "name": "woman getting haircut: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F487 1F3FC 200D 2640", "html": "&#x1F487;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏽‍♀️", "name": "woman getting haircut: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F487 1F3FD 200D 2640 FE0F", "html": "&#x1F487;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏽‍♀", "name": "woman getting haircut: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F487 1F3FD 200D 2640", "html": "&#x1F487;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏾‍♀️", "name": "woman getting haircut: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F487 1F3FE 200D 2640 FE0F", "html": "&#x1F487;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏾‍♀", "name": "woman getting haircut: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F487 1F3FE 200D 2640", "html": "&#x1F487;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏿‍♀️", "name": "woman getting haircut: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F487 1F3FF 200D 2640 FE0F", "html": "&#x1F487;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "💇🏿‍♀", "name": "woman getting haircut: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F487 1F3FF 200D 2640", "html": "&#x1F487;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶‍♂️", "name": "man walking", "shortname": ":man_walking:", "unicode": "1F6B6 200D 2642 FE0F", "html": "&#x1F6B6;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶‍♂", "name": "man walking", "shortname": ":man_walking:", "unicode": "1F6B6 200D 2642", "html": "&#x1F6B6;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏻‍♂️", "name": "man walking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B6 1F3FB 200D 2642 FE0F", "html": "&#x1F6B6;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏻‍♂", "name": "man walking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B6 1F3FB 200D 2642", "html": "&#x1F6B6;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏼‍♂️", "name": "man walking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B6 1F3FC 200D 2642 FE0F", "html": "&#x1F6B6;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏼‍♂", "name": "man walking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B6 1F3FC 200D 2642", "html": "&#x1F6B6;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏽‍♂️", "name": "man walking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B6 1F3FD 200D 2642 FE0F", "html": "&#x1F6B6;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏽‍♂", "name": "man walking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B6 1F3FD 200D 2642", "html": "&#x1F6B6;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏾‍♂️", "name": "man walking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B6 1F3FE 200D 2642 FE0F", "html": "&#x1F6B6;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏾‍♂", "name": "man walking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B6 1F3FE 200D 2642", "html": "&#x1F6B6;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏿‍♂️", "name": "man walking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B6 1F3FF 200D 2642 FE0F", "html": "&#x1F6B6;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏿‍♂", "name": "man walking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B6 1F3FF 200D 2642", "html": "&#x1F6B6;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶‍♀️", "name": "woman walking", "shortname": ":woman_walking:", "unicode": "1F6B6 200D 2640 FE0F", "html": "&#x1F6B6;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏻‍♀️", "name": "woman walking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B6 1F3FB 200D 2640 FE0F", "html": "&#x1F6B6;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏼‍♀️", "name": "woman walking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B6 1F3FC 200D 2640 FE0F", "html": "&#x1F6B6;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏽‍♀️", "name": "woman walking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B6 1F3FD 200D 2640 FE0F", "html": "&#x1F6B6;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏾‍♀️", "name": "woman walking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B6 1F3FE 200D 2640 FE0F", "html": "&#x1F6B6;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🚶🏿‍♀️", "name": "woman walking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B6 1F3FF 200D 2640 FE0F", "html": "&#x1F6B6;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍", "name": "person standing", "shortname": ":person_standing:", "unicode": "1F9CD", "html": "&#x1F9CD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏻", "name": "person standing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CD 1F3FB", "html": "&#x1F9CD;&#x1F3FB;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏼", "name": "person standing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CD 1F3FC", "html": "&#x1F9CD;&#x1F3FC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏽", "name": "person standing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CD 1F3FD", "html": "&#x1F9CD;&#x1F3FD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏾", "name": "person standing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CD 1F3FE", "html": "&#x1F9CD;&#x1F3FE;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏿", "name": "person standing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CD 1F3FF", "html": "&#x1F9CD;&#x1F3FF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍‍♂️", "name": "man standing", "shortname": ":man_standing:", "unicode": "1F9CD 200D 2642 FE0F", "html": "&#x1F9CD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍‍♂", "name": "man standing", "shortname": ":man_standing:", "unicode": "1F9CD 200D 2642", "html": "&#x1F9CD;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏻‍♂️", "name": "man standing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CD 1F3FB 200D 2642 FE0F", "html": "&#x1F9CD;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏻‍♂", "name": "man standing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CD 1F3FB 200D 2642", "html": "&#x1F9CD;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏼‍♂️", "name": "man standing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CD 1F3FC 200D 2642 FE0F", "html": "&#x1F9CD;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏼‍♂", "name": "man standing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CD 1F3FC 200D 2642", "html": "&#x1F9CD;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏽‍♂️", "name": "man standing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CD 1F3FD 200D 2642 FE0F", "html": "&#x1F9CD;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏽‍♂", "name": "man standing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CD 1F3FD 200D 2642", "html": "&#x1F9CD;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏾‍♂️", "name": "man standing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CD 1F3FE 200D 2642 FE0F", "html": "&#x1F9CD;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏾‍♂", "name": "man standing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CD 1F3FE 200D 2642", "html": "&#x1F9CD;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏿‍♂️", "name": "man standing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CD 1F3FF 200D 2642 FE0F", "html": "&#x1F9CD;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏿‍♂", "name": "man standing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CD 1F3FF 200D 2642", "html": "&#x1F9CD;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍‍♀️", "name": "woman standing", "shortname": ":woman_standing:", "unicode": "1F9CD 200D 2640 FE0F", "html": "&#x1F9CD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍‍♀", "name": "woman standing", "shortname": ":woman_standing:", "unicode": "1F9CD 200D 2640", "html": "&#x1F9CD;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏻‍♀️", "name": "woman standing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CD 1F3FB 200D 2640 FE0F", "html": "&#x1F9CD;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏻‍♀", "name": "woman standing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CD 1F3FB 200D 2640", "html": "&#x1F9CD;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏼‍♀️", "name": "woman standing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CD 1F3FC 200D 2640 FE0F", "html": "&#x1F9CD;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏼‍♀", "name": "woman standing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CD 1F3FC 200D 2640", "html": "&#x1F9CD;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏽‍♀️", "name": "woman standing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CD 1F3FD 200D 2640 FE0F", "html": "&#x1F9CD;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏽‍♀", "name": "woman standing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CD 1F3FD 200D 2640", "html": "&#x1F9CD;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏾‍♀️", "name": "woman standing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CD 1F3FE 200D 2640 FE0F", "html": "&#x1F9CD;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏾‍♀", "name": "woman standing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CD 1F3FE 200D 2640", "html": "&#x1F9CD;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏿‍♀️", "name": "woman standing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CD 1F3FF 200D 2640 FE0F", "html": "&#x1F9CD;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧍🏿‍♀", "name": "woman standing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CD 1F3FF 200D 2640", "html": "&#x1F9CD;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎", "name": "person kneeling", "shortname": ":person_kneeling:", "unicode": "1F9CE", "html": "&#x1F9CE;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏻", "name": "person kneeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CE 1F3FB", "html": "&#x1F9CE;&#x1F3FB;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏼", "name": "person kneeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CE 1F3FC", "html": "&#x1F9CE;&#x1F3FC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏽", "name": "person kneeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CE 1F3FD", "html": "&#x1F9CE;&#x1F3FD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏾", "name": "person kneeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CE 1F3FE", "html": "&#x1F9CE;&#x1F3FE;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏿", "name": "person kneeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CE 1F3FF", "html": "&#x1F9CE;&#x1F3FF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎‍♂️", "name": "man kneeling", "shortname": ":man_kneeling:", "unicode": "1F9CE 200D 2642 FE0F", "html": "&#x1F9CE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎‍♂", "name": "man kneeling", "shortname": ":man_kneeling:", "unicode": "1F9CE 200D 2642", "html": "&#x1F9CE;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏻‍♂️", "name": "man kneeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CE 1F3FB 200D 2642 FE0F", "html": "&#x1F9CE;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏻‍♂", "name": "man kneeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CE 1F3FB 200D 2642", "html": "&#x1F9CE;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏼‍♂️", "name": "man kneeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CE 1F3FC 200D 2642 FE0F", "html": "&#x1F9CE;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏼‍♂", "name": "man kneeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CE 1F3FC 200D 2642", "html": "&#x1F9CE;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏽‍♂️", "name": "man kneeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CE 1F3FD 200D 2642 FE0F", "html": "&#x1F9CE;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏽‍♂", "name": "man kneeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CE 1F3FD 200D 2642", "html": "&#x1F9CE;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏾‍♂️", "name": "man kneeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CE 1F3FE 200D 2642 FE0F", "html": "&#x1F9CE;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏾‍♂", "name": "man kneeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CE 1F3FE 200D 2642", "html": "&#x1F9CE;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏿‍♂️", "name": "man kneeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CE 1F3FF 200D 2642 FE0F", "html": "&#x1F9CE;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏿‍♂", "name": "man kneeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CE 1F3FF 200D 2642", "html": "&#x1F9CE;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎‍♀️", "name": "woman kneeling", "shortname": ":woman_kneeling:", "unicode": "1F9CE 200D 2640 FE0F", "html": "&#x1F9CE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎‍♀", "name": "woman kneeling", "shortname": ":woman_kneeling:", "unicode": "1F9CE 200D 2640", "html": "&#x1F9CE;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏻‍♀️", "name": "woman kneeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CE 1F3FB 200D 2640 FE0F", "html": "&#x1F9CE;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏻‍♀", "name": "woman kneeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9CE 1F3FB 200D 2640", "html": "&#x1F9CE;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏼‍♀️", "name": "woman kneeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CE 1F3FC 200D 2640 FE0F", "html": "&#x1F9CE;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏼‍♀", "name": "woman kneeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9CE 1F3FC 200D 2640", "html": "&#x1F9CE;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏽‍♀️", "name": "woman kneeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CE 1F3FD 200D 2640 FE0F", "html": "&#x1F9CE;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏽‍♀", "name": "woman kneeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9CE 1F3FD 200D 2640", "html": "&#x1F9CE;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏾‍♀️", "name": "woman kneeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CE 1F3FE 200D 2640 FE0F", "html": "&#x1F9CE;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏾‍♀", "name": "woman kneeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9CE 1F3FE 200D 2640", "html": "&#x1F9CE;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏿‍♀️", "name": "woman kneeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CE 1F3FF 200D 2640 FE0F", "html": "&#x1F9CE;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧎🏿‍♀", "name": "woman kneeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9CE 1F3FF 200D 2640", "html": "&#x1F9CE;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑‍🦯", "name": "person with probing cane", "shortname": ":person_with_probing_cane:", "unicode": "1F9D1 200D 1F9AF", "html": "&#x1F9D1;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏻‍🦯", "name": "person with probing cane: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F9AF", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏼‍🦯", "name": "person with probing cane: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F9AF", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏽‍🦯", "name": "person with probing cane: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F9AF", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏾‍🦯", "name": "person with probing cane: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F9AF", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏿‍🦯", "name": "person with probing cane: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F9AF", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨‍🦯", "name": "man with probing cane", "shortname": ":man_with_probing_cane:", "unicode": "1F468 200D 1F9AF", "html": "&#x1F468;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏻‍🦯", "name": "man with probing cane: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F9AF", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏼‍🦯", "name": "man with probing cane: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F9AF", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏽‍🦯", "name": "man with probing cane: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F9AF", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏾‍🦯", "name": "man with probing cane: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F9AF", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏿‍🦯", "name": "man with probing cane: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F9AF", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩‍🦯", "name": "woman with probing cane", "shortname": ":woman_with_probing_cane:", "unicode": "1F469 200D 1F9AF", "html": "&#x1F469;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏻‍🦯", "name": "woman with probing cane: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F9AF", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏼‍🦯", "name": "woman with probing cane: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F9AF", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏽‍🦯", "name": "woman with probing cane: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F9AF", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏾‍🦯", "name": "woman with probing cane: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F9AF", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏿‍🦯", "name": "woman with probing cane: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F9AF", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F9AF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑‍🦼", "name": "person in motorized wheelchair", "shortname": ":person_in_motorized_wheelchair:", "unicode": "1F9D1 200D 1F9BC", "html": "&#x1F9D1;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏻‍🦼", "name": "person in motorized wheelchair: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F9BC", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏼‍🦼", "name": "person in motorized wheelchair: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F9BC", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏽‍🦼", "name": "person in motorized wheelchair: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F9BC", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏾‍🦼", "name": "person in motorized wheelchair: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F9BC", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏿‍🦼", "name": "person in motorized wheelchair: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F9BC", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨‍🦼", "name": "man in motorized wheelchair", "shortname": ":man_in_motorized_wheelchair:", "unicode": "1F468 200D 1F9BC", "html": "&#x1F468;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏻‍🦼", "name": "man in motorized wheelchair: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F9BC", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏼‍🦼", "name": "man in motorized wheelchair: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F9BC", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏽‍🦼", "name": "man in motorized wheelchair: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F9BC", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏾‍🦼", "name": "man in motorized wheelchair: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F9BC", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏿‍🦼", "name": "man in motorized wheelchair: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F9BC", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩‍🦼", "name": "woman in motorized wheelchair", "shortname": ":woman_in_motorized_wheelchair:", "unicode": "1F469 200D 1F9BC", "html": "&#x1F469;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏻‍🦼", "name": "woman in motorized wheelchair: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F9BC", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏼‍🦼", "name": "woman in motorized wheelchair: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F9BC", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏽‍🦼", "name": "woman in motorized wheelchair: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F9BC", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏾‍🦼", "name": "woman in motorized wheelchair: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F9BC", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏿‍🦼", "name": "woman in motorized wheelchair: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F9BC", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F9BC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑‍🦽", "name": "person in manual wheelchair", "shortname": ":person_in_manual_wheelchair:", "unicode": "1F9D1 200D 1F9BD", "html": "&#x1F9D1;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏻‍🦽", "name": "person in manual wheelchair: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F9BD", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏼‍🦽", "name": "person in manual wheelchair: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F9BD", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏽‍🦽", "name": "person in manual wheelchair: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F9BD", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏾‍🦽", "name": "person in manual wheelchair: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F9BD", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧑🏿‍🦽", "name": "person in manual wheelchair: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F9BD", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨‍🦽", "name": "man in manual wheelchair", "shortname": ":man_in_manual_wheelchair:", "unicode": "1F468 200D 1F9BD", "html": "&#x1F468;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏻‍🦽", "name": "man in manual wheelchair: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F468 1F3FB 200D 1F9BD", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏼‍🦽", "name": "man in manual wheelchair: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F468 1F3FC 200D 1F9BD", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏽‍🦽", "name": "man in manual wheelchair: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F468 1F3FD 200D 1F9BD", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏾‍🦽", "name": "man in manual wheelchair: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F9BD", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👨🏿‍🦽", "name": "man in manual wheelchair: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F9BD", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩‍🦽", "name": "woman in manual wheelchair", "shortname": ":woman_in_manual_wheelchair:", "unicode": "1F469 200D 1F9BD", "html": "&#x1F469;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏻‍🦽", "name": "woman in manual wheelchair: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F469 1F3FB 200D 1F9BD", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏼‍🦽", "name": "woman in manual wheelchair: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F469 1F3FC 200D 1F9BD", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏽‍🦽", "name": "woman in manual wheelchair: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F469 1F3FD 200D 1F9BD", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏾‍🦽", "name": "woman in manual wheelchair: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F9BD", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👩🏿‍🦽", "name": "woman in manual wheelchair: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F9BD", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F9BD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃‍♂️", "name": "man running", "shortname": ":man_running:", "unicode": "1F3C3 200D 2642 FE0F", "html": "&#x1F3C3;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃‍♂", "name": "man running", "shortname": ":man_running:", "unicode": "1F3C3 200D 2642", "html": "&#x1F3C3;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏻‍♂️", "name": "man running: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C3 1F3FB 200D 2642 FE0F", "html": "&#x1F3C3;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏻‍♂", "name": "man running: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C3 1F3FB 200D 2642", "html": "&#x1F3C3;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏼‍♂️", "name": "man running: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C3 1F3FC 200D 2642 FE0F", "html": "&#x1F3C3;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏼‍♂", "name": "man running: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C3 1F3FC 200D 2642", "html": "&#x1F3C3;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏽‍♂️", "name": "man running: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C3 1F3FD 200D 2642 FE0F", "html": "&#x1F3C3;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏽‍♂", "name": "man running: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C3 1F3FD 200D 2642", "html": "&#x1F3C3;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏾‍♂️", "name": "man running: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C3 1F3FE 200D 2642 FE0F", "html": "&#x1F3C3;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏾‍♂", "name": "man running: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C3 1F3FE 200D 2642", "html": "&#x1F3C3;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏿‍♂️", "name": "man running: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C3 1F3FF 200D 2642 FE0F", "html": "&#x1F3C3;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏿‍♂", "name": "man running: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C3 1F3FF 200D 2642", "html": "&#x1F3C3;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃‍♀️", "name": "woman running", "shortname": ":woman_running:", "unicode": "1F3C3 200D 2640 FE0F", "html": "&#x1F3C3;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏻‍♀️", "name": "woman running: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C3 1F3FB 200D 2640 FE0F", "html": "&#x1F3C3;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏼‍♀️", "name": "woman running: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C3 1F3FC 200D 2640 FE0F", "html": "&#x1F3C3;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏽‍♀️", "name": "woman running: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C3 1F3FD 200D 2640 FE0F", "html": "&#x1F3C3;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏾‍♀️", "name": "woman running: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C3 1F3FE 200D 2640 FE0F", "html": "&#x1F3C3;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🏃🏿‍♀️", "name": "woman running: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C3 1F3FF 200D 2640 FE0F", "html": "&#x1F3C3;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕺", "name": "man dancing", "shortname": ":man_dancing:", "unicode": "1F57A", "html": "&#x1F57A;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕺🏻", "name": "man dancing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F57A 1F3FB", "html": "&#x1F57A;&#x1F3FB;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕺🏼", "name": "man dancing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F57A 1F3FC", "html": "&#x1F57A;&#x1F3FC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕺🏽", "name": "man dancing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F57A 1F3FD", "html": "&#x1F57A;&#x1F3FD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕺🏾", "name": "man dancing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F57A 1F3FE", "html": "&#x1F57A;&#x1F3FE;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕺🏿", "name": "man dancing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F57A 1F3FF", "html": "&#x1F57A;&#x1F3FF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕴️", "name": "man in suit levitating", "shortname": ":man_in_suit_levitating:", "unicode": "1F574 FE0F", "html": "&#x1F574;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕴🏻", "name": "man in suit levitating: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F574 1F3FB", "html": "&#x1F574;&#x1F3FB;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕴🏼", "name": "man in suit levitating: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F574 1F3FC", "html": "&#x1F574;&#x1F3FC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕴🏽", "name": "man in suit levitating: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F574 1F3FD", "html": "&#x1F574;&#x1F3FD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕴🏾", "name": "man in suit levitating: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F574 1F3FE", "html": "&#x1F574;&#x1F3FE;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🕴🏿", "name": "man in suit levitating: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F574 1F3FF", "html": "&#x1F574;&#x1F3FF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👯‍♂️", "name": "men with bunny ears", "shortname": ":men_with_bunny_ears:", "unicode": "1F46F 200D 2642 FE0F", "html": "&#x1F46F;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👯‍♀️", "name": "women with bunny ears", "shortname": ":women_with_bunny_ears:", "unicode": "1F46F 200D 2640 FE0F", "html": "&#x1F46F;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "👯‍♀", "name": "women with bunny ears", "shortname": ":women_with_bunny_ears:", "unicode": "1F46F 200D 2640", "html": "&#x1F46F;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖", "name": "person in steamy room", "shortname": ":person_in_steamy_room:", "unicode": "1F9D6", "html": "&#x1F9D6;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏻", "name": "person in steamy room: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D6 1F3FB", "html": "&#x1F9D6;&#x1F3FB;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏼", "name": "person in steamy room: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D6 1F3FC", "html": "&#x1F9D6;&#x1F3FC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏽", "name": "person in steamy room: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D6 1F3FD", "html": "&#x1F9D6;&#x1F3FD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏾", "name": "person in steamy room: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D6 1F3FE", "html": "&#x1F9D6;&#x1F3FE;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏿", "name": "person in steamy room: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D6 1F3FF", "html": "&#x1F9D6;&#x1F3FF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖‍♂️", "name": "man in steamy room", "shortname": ":man_in_steamy_room:", "unicode": "1F9D6 200D 2642 FE0F", "html": "&#x1F9D6;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖‍♂", "name": "man in steamy room", "shortname": ":man_in_steamy_room:", "unicode": "1F9D6 200D 2642", "html": "&#x1F9D6;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏻‍♂️", "name": "man in steamy room: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D6 1F3FB 200D 2642 FE0F", "html": "&#x1F9D6;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏻‍♂", "name": "man in steamy room: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D6 1F3FB 200D 2642", "html": "&#x1F9D6;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏼‍♂️", "name": "man in steamy room: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D6 1F3FC 200D 2642 FE0F", "html": "&#x1F9D6;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏼‍♂", "name": "man in steamy room: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D6 1F3FC 200D 2642", "html": "&#x1F9D6;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏽‍♂️", "name": "man in steamy room: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D6 1F3FD 200D 2642 FE0F", "html": "&#x1F9D6;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏽‍♂", "name": "man in steamy room: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D6 1F3FD 200D 2642", "html": "&#x1F9D6;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏾‍♂️", "name": "man in steamy room: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D6 1F3FE 200D 2642 FE0F", "html": "&#x1F9D6;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏾‍♂", "name": "man in steamy room: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D6 1F3FE 200D 2642", "html": "&#x1F9D6;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏿‍♂️", "name": "man in steamy room: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D6 1F3FF 200D 2642 FE0F", "html": "&#x1F9D6;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏿‍♂", "name": "man in steamy room: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D6 1F3FF 200D 2642", "html": "&#x1F9D6;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖‍♀️", "name": "woman in steamy room", "shortname": ":woman_in_steamy_room:", "unicode": "1F9D6 200D 2640 FE0F", "html": "&#x1F9D6;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖‍♀", "name": "woman in steamy room", "shortname": ":woman_in_steamy_room:", "unicode": "1F9D6 200D 2640", "html": "&#x1F9D6;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏻‍♀️", "name": "woman in steamy room: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D6 1F3FB 200D 2640 FE0F", "html": "&#x1F9D6;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏻‍♀", "name": "woman in steamy room: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D6 1F3FB 200D 2640", "html": "&#x1F9D6;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏼‍♀️", "name": "woman in steamy room: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D6 1F3FC 200D 2640 FE0F", "html": "&#x1F9D6;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏼‍♀", "name": "woman in steamy room: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D6 1F3FC 200D 2640", "html": "&#x1F9D6;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏽‍♀️", "name": "woman in steamy room: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D6 1F3FD 200D 2640 FE0F", "html": "&#x1F9D6;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏽‍♀", "name": "woman in steamy room: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D6 1F3FD 200D 2640", "html": "&#x1F9D6;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏾‍♀️", "name": "woman in steamy room: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D6 1F3FE 200D 2640 FE0F", "html": "&#x1F9D6;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏾‍♀", "name": "woman in steamy room: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D6 1F3FE 200D 2640", "html": "&#x1F9D6;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏿‍♀️", "name": "woman in steamy room: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D6 1F3FF 200D 2640 FE0F", "html": "&#x1F9D6;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧖🏿‍♀", "name": "woman in steamy room: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D6 1F3FF 200D 2640", "html": "&#x1F9D6;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗", "name": "person climbing", "shortname": ":person_climbing:", "unicode": "1F9D7", "html": "&#x1F9D7;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏻", "name": "person climbing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D7 1F3FB", "html": "&#x1F9D7;&#x1F3FB;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏼", "name": "person climbing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D7 1F3FC", "html": "&#x1F9D7;&#x1F3FC;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏽", "name": "person climbing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D7 1F3FD", "html": "&#x1F9D7;&#x1F3FD;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏾", "name": "person climbing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D7 1F3FE", "html": "&#x1F9D7;&#x1F3FE;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏿", "name": "person climbing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D7 1F3FF", "html": "&#x1F9D7;&#x1F3FF;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗‍♂️", "name": "man climbing", "shortname": ":man_climbing:", "unicode": "1F9D7 200D 2642 FE0F", "html": "&#x1F9D7;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗‍♂", "name": "man climbing", "shortname": ":man_climbing:", "unicode": "1F9D7 200D 2642", "html": "&#x1F9D7;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏻‍♂️", "name": "man climbing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D7 1F3FB 200D 2642 FE0F", "html": "&#x1F9D7;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏻‍♂", "name": "man climbing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D7 1F3FB 200D 2642", "html": "&#x1F9D7;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏼‍♂️", "name": "man climbing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D7 1F3FC 200D 2642 FE0F", "html": "&#x1F9D7;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏼‍♂", "name": "man climbing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D7 1F3FC 200D 2642", "html": "&#x1F9D7;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏽‍♂️", "name": "man climbing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D7 1F3FD 200D 2642 FE0F", "html": "&#x1F9D7;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏽‍♂", "name": "man climbing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D7 1F3FD 200D 2642", "html": "&#x1F9D7;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏾‍♂️", "name": "man climbing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D7 1F3FE 200D 2642 FE0F", "html": "&#x1F9D7;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏾‍♂", "name": "man climbing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D7 1F3FE 200D 2642", "html": "&#x1F9D7;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏿‍♂️", "name": "man climbing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D7 1F3FF 200D 2642 FE0F", "html": "&#x1F9D7;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏿‍♂", "name": "man climbing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D7 1F3FF 200D 2642", "html": "&#x1F9D7;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗‍♀️", "name": "woman climbing", "shortname": ":woman_climbing:", "unicode": "1F9D7 200D 2640 FE0F", "html": "&#x1F9D7;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗‍♀", "name": "woman climbing", "shortname": ":woman_climbing:", "unicode": "1F9D7 200D 2640", "html": "&#x1F9D7;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏻‍♀️", "name": "woman climbing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D7 1F3FB 200D 2640 FE0F", "html": "&#x1F9D7;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏻‍♀", "name": "woman climbing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D7 1F3FB 200D 2640", "html": "&#x1F9D7;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏼‍♀️", "name": "woman climbing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D7 1F3FC 200D 2640 FE0F", "html": "&#x1F9D7;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏼‍♀", "name": "woman climbing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D7 1F3FC 200D 2640", "html": "&#x1F9D7;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏽‍♀️", "name": "woman climbing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D7 1F3FD 200D 2640 FE0F", "html": "&#x1F9D7;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏽‍♀", "name": "woman climbing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D7 1F3FD 200D 2640", "html": "&#x1F9D7;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏾‍♀️", "name": "woman climbing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D7 1F3FE 200D 2640 FE0F", "html": "&#x1F9D7;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏾‍♀", "name": "woman climbing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D7 1F3FE 200D 2640", "html": "&#x1F9D7;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏿‍♀️", "name": "woman climbing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D7 1F3FF 200D 2640 FE0F", "html": "&#x1F9D7;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🧗🏿‍♀", "name": "woman climbing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D7 1F3FF 200D 2640", "html": "&#x1F9D7;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-activity)", "order": ""},
+ {"emoji": "🤺", "name": "person fencing", "shortname": ":person_fencing:", "unicode": "1F93A", "html": "&#x1F93A;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏇🏻", "name": "horse racing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C7 1F3FB", "html": "&#x1F3C7;&#x1F3FB;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏇🏼", "name": "horse racing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C7 1F3FC", "html": "&#x1F3C7;&#x1F3FC;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏇🏽", "name": "horse racing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C7 1F3FD", "html": "&#x1F3C7;&#x1F3FD;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏇🏾", "name": "horse racing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C7 1F3FE", "html": "&#x1F3C7;&#x1F3FE;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏇🏿", "name": "horse racing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C7 1F3FF", "html": "&#x1F3C7;&#x1F3FF;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛷️", "name": "skier", "shortname": ":skier:", "unicode": "26F7 FE0F", "html": "&#x26F7;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏂🏻", "name": "snowboarder: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C2 1F3FB", "html": "&#x1F3C2;&#x1F3FB;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏂🏼", "name": "snowboarder: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C2 1F3FC", "html": "&#x1F3C2;&#x1F3FC;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏂🏽", "name": "snowboarder: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C2 1F3FD", "html": "&#x1F3C2;&#x1F3FD;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏂🏾", "name": "snowboarder: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C2 1F3FE", "html": "&#x1F3C2;&#x1F3FE;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏂🏿", "name": "snowboarder: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C2 1F3FF", "html": "&#x1F3C2;&#x1F3FF;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌️", "name": "person golfing", "shortname": ":person_golfing:", "unicode": "1F3CC FE0F", "html": "&#x1F3CC;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏻", "name": "person golfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CC 1F3FB", "html": "&#x1F3CC;&#x1F3FB;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏼", "name": "person golfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CC 1F3FC", "html": "&#x1F3CC;&#x1F3FC;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏽", "name": "person golfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CC 1F3FD", "html": "&#x1F3CC;&#x1F3FD;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏾", "name": "person golfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CC 1F3FE", "html": "&#x1F3CC;&#x1F3FE;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏿", "name": "person golfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CC 1F3FF", "html": "&#x1F3CC;&#x1F3FF;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌️‍♂️", "name": "man golfing", "shortname": ":man_golfing:", "unicode": "1F3CC FE0F 200D 2642 FE0F", "html": "&#x1F3CC;&#xFE0F;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌‍♂️", "name": "man golfing", "shortname": ":man_golfing:", "unicode": "1F3CC 200D 2642 FE0F", "html": "&#x1F3CC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌️‍♂", "name": "man golfing", "shortname": ":man_golfing:", "unicode": "1F3CC FE0F 200D 2642", "html": "&#x1F3CC;&#xFE0F;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌‍♂", "name": "man golfing", "shortname": ":man_golfing:", "unicode": "1F3CC 200D 2642", "html": "&#x1F3CC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏻‍♂️", "name": "man golfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CC 1F3FB 200D 2642 FE0F", "html": "&#x1F3CC;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏻‍♂", "name": "man golfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CC 1F3FB 200D 2642", "html": "&#x1F3CC;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏼‍♂️", "name": "man golfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CC 1F3FC 200D 2642 FE0F", "html": "&#x1F3CC;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏼‍♂", "name": "man golfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CC 1F3FC 200D 2642", "html": "&#x1F3CC;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏽‍♂️", "name": "man golfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CC 1F3FD 200D 2642 FE0F", "html": "&#x1F3CC;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏽‍♂", "name": "man golfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CC 1F3FD 200D 2642", "html": "&#x1F3CC;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏾‍♂️", "name": "man golfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CC 1F3FE 200D 2642 FE0F", "html": "&#x1F3CC;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏾‍♂", "name": "man golfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CC 1F3FE 200D 2642", "html": "&#x1F3CC;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏿‍♂️", "name": "man golfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CC 1F3FF 200D 2642 FE0F", "html": "&#x1F3CC;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏿‍♂", "name": "man golfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CC 1F3FF 200D 2642", "html": "&#x1F3CC;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌️‍♀️", "name": "woman golfing", "shortname": ":woman_golfing:", "unicode": "1F3CC FE0F 200D 2640 FE0F", "html": "&#x1F3CC;&#xFE0F;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌‍♀️", "name": "woman golfing", "shortname": ":woman_golfing:", "unicode": "1F3CC 200D 2640 FE0F", "html": "&#x1F3CC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌️‍♀", "name": "woman golfing", "shortname": ":woman_golfing:", "unicode": "1F3CC FE0F 200D 2640", "html": "&#x1F3CC;&#xFE0F;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏻‍♀️", "name": "woman golfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CC 1F3FB 200D 2640 FE0F", "html": "&#x1F3CC;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏻‍♀", "name": "woman golfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CC 1F3FB 200D 2640", "html": "&#x1F3CC;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏼‍♀️", "name": "woman golfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CC 1F3FC 200D 2640 FE0F", "html": "&#x1F3CC;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏼‍♀", "name": "woman golfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CC 1F3FC 200D 2640", "html": "&#x1F3CC;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏽‍♀️", "name": "woman golfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CC 1F3FD 200D 2640 FE0F", "html": "&#x1F3CC;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏽‍♀", "name": "woman golfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CC 1F3FD 200D 2640", "html": "&#x1F3CC;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏾‍♀️", "name": "woman golfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CC 1F3FE 200D 2640 FE0F", "html": "&#x1F3CC;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏾‍♀", "name": "woman golfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CC 1F3FE 200D 2640", "html": "&#x1F3CC;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏿‍♀️", "name": "woman golfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CC 1F3FF 200D 2640 FE0F", "html": "&#x1F3CC;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏌🏿‍♀", "name": "woman golfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CC 1F3FF 200D 2640", "html": "&#x1F3CC;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄‍♂️", "name": "man surfing", "shortname": ":man_surfing:", "unicode": "1F3C4 200D 2642 FE0F", "html": "&#x1F3C4;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄‍♂", "name": "man surfing", "shortname": ":man_surfing:", "unicode": "1F3C4 200D 2642", "html": "&#x1F3C4;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏻‍♂️", "name": "man surfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C4 1F3FB 200D 2642 FE0F", "html": "&#x1F3C4;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏻‍♂", "name": "man surfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C4 1F3FB 200D 2642", "html": "&#x1F3C4;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏼‍♂️", "name": "man surfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C4 1F3FC 200D 2642 FE0F", "html": "&#x1F3C4;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏼‍♂", "name": "man surfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C4 1F3FC 200D 2642", "html": "&#x1F3C4;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏽‍♂️", "name": "man surfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C4 1F3FD 200D 2642 FE0F", "html": "&#x1F3C4;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏽‍♂", "name": "man surfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C4 1F3FD 200D 2642", "html": "&#x1F3C4;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏾‍♂️", "name": "man surfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C4 1F3FE 200D 2642 FE0F", "html": "&#x1F3C4;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏾‍♂", "name": "man surfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C4 1F3FE 200D 2642", "html": "&#x1F3C4;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏿‍♂️", "name": "man surfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C4 1F3FF 200D 2642 FE0F", "html": "&#x1F3C4;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏿‍♂", "name": "man surfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C4 1F3FF 200D 2642", "html": "&#x1F3C4;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄‍♀️", "name": "woman surfing", "shortname": ":woman_surfing:", "unicode": "1F3C4 200D 2640 FE0F", "html": "&#x1F3C4;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏻‍♀️", "name": "woman surfing: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3C4 1F3FB 200D 2640 FE0F", "html": "&#x1F3C4;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏼‍♀️", "name": "woman surfing: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3C4 1F3FC 200D 2640 FE0F", "html": "&#x1F3C4;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏽‍♀️", "name": "woman surfing: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3C4 1F3FD 200D 2640 FE0F", "html": "&#x1F3C4;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏾‍♀️", "name": "woman surfing: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3C4 1F3FE 200D 2640 FE0F", "html": "&#x1F3C4;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏄🏿‍♀️", "name": "woman surfing: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3C4 1F3FF 200D 2640 FE0F", "html": "&#x1F3C4;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣‍♂️", "name": "man rowing boat", "shortname": ":man_rowing_boat:", "unicode": "1F6A3 200D 2642 FE0F", "html": "&#x1F6A3;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣‍♂", "name": "man rowing boat", "shortname": ":man_rowing_boat:", "unicode": "1F6A3 200D 2642", "html": "&#x1F6A3;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏻‍♂️", "name": "man rowing boat: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6A3 1F3FB 200D 2642 FE0F", "html": "&#x1F6A3;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏻‍♂", "name": "man rowing boat: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6A3 1F3FB 200D 2642", "html": "&#x1F6A3;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏼‍♂️", "name": "man rowing boat: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6A3 1F3FC 200D 2642 FE0F", "html": "&#x1F6A3;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏼‍♂", "name": "man rowing boat: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6A3 1F3FC 200D 2642", "html": "&#x1F6A3;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏽‍♂️", "name": "man rowing boat: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6A3 1F3FD 200D 2642 FE0F", "html": "&#x1F6A3;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏽‍♂", "name": "man rowing boat: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6A3 1F3FD 200D 2642", "html": "&#x1F6A3;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏾‍♂️", "name": "man rowing boat: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6A3 1F3FE 200D 2642 FE0F", "html": "&#x1F6A3;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏾‍♂", "name": "man rowing boat: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6A3 1F3FE 200D 2642", "html": "&#x1F6A3;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏿‍♂️", "name": "man rowing boat: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6A3 1F3FF 200D 2642 FE0F", "html": "&#x1F6A3;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏿‍♂", "name": "man rowing boat: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6A3 1F3FF 200D 2642", "html": "&#x1F6A3;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣‍♀️", "name": "woman rowing boat", "shortname": ":woman_rowing_boat:", "unicode": "1F6A3 200D 2640 FE0F", "html": "&#x1F6A3;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏻‍♀️", "name": "woman rowing boat: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6A3 1F3FB 200D 2640 FE0F", "html": "&#x1F6A3;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏼‍♀️", "name": "woman rowing boat: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6A3 1F3FC 200D 2640 FE0F", "html": "&#x1F6A3;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏽‍♀️", "name": "woman rowing boat: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6A3 1F3FD 200D 2640 FE0F", "html": "&#x1F6A3;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏾‍♀️", "name": "woman rowing boat: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6A3 1F3FE 200D 2640 FE0F", "html": "&#x1F6A3;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚣🏿‍♀️", "name": "woman rowing boat: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6A3 1F3FF 200D 2640 FE0F", "html": "&#x1F6A3;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊‍♂️", "name": "man swimming", "shortname": ":man_swimming:", "unicode": "1F3CA 200D 2642 FE0F", "html": "&#x1F3CA;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊‍♂", "name": "man swimming", "shortname": ":man_swimming:", "unicode": "1F3CA 200D 2642", "html": "&#x1F3CA;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏻‍♂️", "name": "man swimming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CA 1F3FB 200D 2642 FE0F", "html": "&#x1F3CA;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏻‍♂", "name": "man swimming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CA 1F3FB 200D 2642", "html": "&#x1F3CA;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏼‍♂️", "name": "man swimming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CA 1F3FC 200D 2642 FE0F", "html": "&#x1F3CA;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏼‍♂", "name": "man swimming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CA 1F3FC 200D 2642", "html": "&#x1F3CA;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏽‍♂️", "name": "man swimming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CA 1F3FD 200D 2642 FE0F", "html": "&#x1F3CA;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏽‍♂", "name": "man swimming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CA 1F3FD 200D 2642", "html": "&#x1F3CA;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏾‍♂️", "name": "man swimming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CA 1F3FE 200D 2642 FE0F", "html": "&#x1F3CA;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏾‍♂", "name": "man swimming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CA 1F3FE 200D 2642", "html": "&#x1F3CA;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏿‍♂️", "name": "man swimming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CA 1F3FF 200D 2642 FE0F", "html": "&#x1F3CA;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏿‍♂", "name": "man swimming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CA 1F3FF 200D 2642", "html": "&#x1F3CA;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊‍♀️", "name": "woman swimming", "shortname": ":woman_swimming:", "unicode": "1F3CA 200D 2640 FE0F", "html": "&#x1F3CA;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏻‍♀️", "name": "woman swimming: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CA 1F3FB 200D 2640 FE0F", "html": "&#x1F3CA;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏼‍♀️", "name": "woman swimming: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CA 1F3FC 200D 2640 FE0F", "html": "&#x1F3CA;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏽‍♀️", "name": "woman swimming: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CA 1F3FD 200D 2640 FE0F", "html": "&#x1F3CA;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏾‍♀️", "name": "woman swimming: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CA 1F3FE 200D 2640 FE0F", "html": "&#x1F3CA;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏊🏿‍♀️", "name": "woman swimming: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CA 1F3FF 200D 2640 FE0F", "html": "&#x1F3CA;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹️", "name": "person bouncing ball", "shortname": ":person_bouncing_ball:", "unicode": "26F9 FE0F", "html": "&#x26F9;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹️‍♂️", "name": "man bouncing ball", "shortname": ":man_bouncing_ball:", "unicode": "26F9 FE0F 200D 2642 FE0F", "html": "&#x26F9;&#xFE0F;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹‍♂️", "name": "man bouncing ball", "shortname": ":man_bouncing_ball:", "unicode": "26F9 200D 2642 FE0F", "html": "&#x26F9;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹️‍♂", "name": "man bouncing ball", "shortname": ":man_bouncing_ball:", "unicode": "26F9 FE0F 200D 2642", "html": "&#x26F9;&#xFE0F;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹‍♂", "name": "man bouncing ball", "shortname": ":man_bouncing_ball:", "unicode": "26F9 200D 2642", "html": "&#x26F9;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏻‍♂️", "name": "man bouncing ball: light skin tone", "shortname": ":light_skin_tone:", "unicode": "26F9 1F3FB 200D 2642 FE0F", "html": "&#x26F9;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏻‍♂", "name": "man bouncing ball: light skin tone", "shortname": ":light_skin_tone:", "unicode": "26F9 1F3FB 200D 2642", "html": "&#x26F9;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏼‍♂️", "name": "man bouncing ball: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "26F9 1F3FC 200D 2642 FE0F", "html": "&#x26F9;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏼‍♂", "name": "man bouncing ball: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "26F9 1F3FC 200D 2642", "html": "&#x26F9;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏽‍♂️", "name": "man bouncing ball: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "26F9 1F3FD 200D 2642 FE0F", "html": "&#x26F9;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏽‍♂", "name": "man bouncing ball: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "26F9 1F3FD 200D 2642", "html": "&#x26F9;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏾‍♂️", "name": "man bouncing ball: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "26F9 1F3FE 200D 2642 FE0F", "html": "&#x26F9;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏾‍♂", "name": "man bouncing ball: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "26F9 1F3FE 200D 2642", "html": "&#x26F9;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏿‍♂️", "name": "man bouncing ball: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "26F9 1F3FF 200D 2642 FE0F", "html": "&#x26F9;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏿‍♂", "name": "man bouncing ball: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "26F9 1F3FF 200D 2642", "html": "&#x26F9;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹️‍♀️", "name": "woman bouncing ball", "shortname": ":woman_bouncing_ball:", "unicode": "26F9 FE0F 200D 2640 FE0F", "html": "&#x26F9;&#xFE0F;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹‍♀️", "name": "woman bouncing ball", "shortname": ":woman_bouncing_ball:", "unicode": "26F9 200D 2640 FE0F", "html": "&#x26F9;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹️‍♀", "name": "woman bouncing ball", "shortname": ":woman_bouncing_ball:", "unicode": "26F9 FE0F 200D 2640", "html": "&#x26F9;&#xFE0F;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏻‍♀️", "name": "woman bouncing ball: light skin tone", "shortname": ":light_skin_tone:", "unicode": "26F9 1F3FB 200D 2640 FE0F", "html": "&#x26F9;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏼‍♀️", "name": "woman bouncing ball: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "26F9 1F3FC 200D 2640 FE0F", "html": "&#x26F9;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏽‍♀️", "name": "woman bouncing ball: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "26F9 1F3FD 200D 2640 FE0F", "html": "&#x26F9;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏾‍♀️", "name": "woman bouncing ball: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "26F9 1F3FE 200D 2640 FE0F", "html": "&#x26F9;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "⛹🏿‍♀️", "name": "woman bouncing ball: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "26F9 1F3FF 200D 2640 FE0F", "html": "&#x26F9;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋️", "name": "person lifting weights", "shortname": ":person_lifting_weights:", "unicode": "1F3CB FE0F", "html": "&#x1F3CB;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋️‍♂️", "name": "man lifting weights", "shortname": ":man_lifting_weights:", "unicode": "1F3CB FE0F 200D 2642 FE0F", "html": "&#x1F3CB;&#xFE0F;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋‍♂️", "name": "man lifting weights", "shortname": ":man_lifting_weights:", "unicode": "1F3CB 200D 2642 FE0F", "html": "&#x1F3CB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋️‍♂", "name": "man lifting weights", "shortname": ":man_lifting_weights:", "unicode": "1F3CB FE0F 200D 2642", "html": "&#x1F3CB;&#xFE0F;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋‍♂", "name": "man lifting weights", "shortname": ":man_lifting_weights:", "unicode": "1F3CB 200D 2642", "html": "&#x1F3CB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏻‍♂️", "name": "man lifting weights: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CB 1F3FB 200D 2642 FE0F", "html": "&#x1F3CB;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏻‍♂", "name": "man lifting weights: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CB 1F3FB 200D 2642", "html": "&#x1F3CB;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏼‍♂️", "name": "man lifting weights: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CB 1F3FC 200D 2642 FE0F", "html": "&#x1F3CB;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏼‍♂", "name": "man lifting weights: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CB 1F3FC 200D 2642", "html": "&#x1F3CB;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏽‍♂️", "name": "man lifting weights: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CB 1F3FD 200D 2642 FE0F", "html": "&#x1F3CB;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏽‍♂", "name": "man lifting weights: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CB 1F3FD 200D 2642", "html": "&#x1F3CB;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏾‍♂️", "name": "man lifting weights: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CB 1F3FE 200D 2642 FE0F", "html": "&#x1F3CB;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏾‍♂", "name": "man lifting weights: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CB 1F3FE 200D 2642", "html": "&#x1F3CB;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏿‍♂️", "name": "man lifting weights: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CB 1F3FF 200D 2642 FE0F", "html": "&#x1F3CB;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏿‍♂", "name": "man lifting weights: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CB 1F3FF 200D 2642", "html": "&#x1F3CB;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋️‍♀️", "name": "woman lifting weights", "shortname": ":woman_lifting_weights:", "unicode": "1F3CB FE0F 200D 2640 FE0F", "html": "&#x1F3CB;&#xFE0F;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋‍♀️", "name": "woman lifting weights", "shortname": ":woman_lifting_weights:", "unicode": "1F3CB 200D 2640 FE0F", "html": "&#x1F3CB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋️‍♀", "name": "woman lifting weights", "shortname": ":woman_lifting_weights:", "unicode": "1F3CB FE0F 200D 2640", "html": "&#x1F3CB;&#xFE0F;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏻‍♀️", "name": "woman lifting weights: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3CB 1F3FB 200D 2640 FE0F", "html": "&#x1F3CB;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏼‍♀️", "name": "woman lifting weights: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3CB 1F3FC 200D 2640 FE0F", "html": "&#x1F3CB;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏽‍♀️", "name": "woman lifting weights: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3CB 1F3FD 200D 2640 FE0F", "html": "&#x1F3CB;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏾‍♀️", "name": "woman lifting weights: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3CB 1F3FE 200D 2640 FE0F", "html": "&#x1F3CB;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🏋🏿‍♀️", "name": "woman lifting weights: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3CB 1F3FF 200D 2640 FE0F", "html": "&#x1F3CB;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴‍♂️", "name": "man biking", "shortname": ":man_biking:", "unicode": "1F6B4 200D 2642 FE0F", "html": "&#x1F6B4;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴‍♂", "name": "man biking", "shortname": ":man_biking:", "unicode": "1F6B4 200D 2642", "html": "&#x1F6B4;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏻‍♂️", "name": "man biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B4 1F3FB 200D 2642 FE0F", "html": "&#x1F6B4;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏻‍♂", "name": "man biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B4 1F3FB 200D 2642", "html": "&#x1F6B4;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏼‍♂️", "name": "man biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B4 1F3FC 200D 2642 FE0F", "html": "&#x1F6B4;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏼‍♂", "name": "man biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B4 1F3FC 200D 2642", "html": "&#x1F6B4;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏽‍♂️", "name": "man biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B4 1F3FD 200D 2642 FE0F", "html": "&#x1F6B4;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏽‍♂", "name": "man biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B4 1F3FD 200D 2642", "html": "&#x1F6B4;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏾‍♂️", "name": "man biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B4 1F3FE 200D 2642 FE0F", "html": "&#x1F6B4;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏾‍♂", "name": "man biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B4 1F3FE 200D 2642", "html": "&#x1F6B4;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏿‍♂️", "name": "man biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B4 1F3FF 200D 2642 FE0F", "html": "&#x1F6B4;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏿‍♂", "name": "man biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B4 1F3FF 200D 2642", "html": "&#x1F6B4;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴‍♀️", "name": "woman biking", "shortname": ":woman_biking:", "unicode": "1F6B4 200D 2640 FE0F", "html": "&#x1F6B4;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏻‍♀️", "name": "woman biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B4 1F3FB 200D 2640 FE0F", "html": "&#x1F6B4;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏼‍♀️", "name": "woman biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B4 1F3FC 200D 2640 FE0F", "html": "&#x1F6B4;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏽‍♀️", "name": "woman biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B4 1F3FD 200D 2640 FE0F", "html": "&#x1F6B4;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏾‍♀️", "name": "woman biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B4 1F3FE 200D 2640 FE0F", "html": "&#x1F6B4;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚴🏿‍♀️", "name": "woman biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B4 1F3FF 200D 2640 FE0F", "html": "&#x1F6B4;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵‍♂️", "name": "man mountain biking", "shortname": ":man_mountain_biking:", "unicode": "1F6B5 200D 2642 FE0F", "html": "&#x1F6B5;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵‍♂", "name": "man mountain biking", "shortname": ":man_mountain_biking:", "unicode": "1F6B5 200D 2642", "html": "&#x1F6B5;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏻‍♂️", "name": "man mountain biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B5 1F3FB 200D 2642 FE0F", "html": "&#x1F6B5;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏻‍♂", "name": "man mountain biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B5 1F3FB 200D 2642", "html": "&#x1F6B5;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏼‍♂️", "name": "man mountain biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B5 1F3FC 200D 2642 FE0F", "html": "&#x1F6B5;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏼‍♂", "name": "man mountain biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B5 1F3FC 200D 2642", "html": "&#x1F6B5;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏽‍♂️", "name": "man mountain biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B5 1F3FD 200D 2642 FE0F", "html": "&#x1F6B5;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏽‍♂", "name": "man mountain biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B5 1F3FD 200D 2642", "html": "&#x1F6B5;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏾‍♂️", "name": "man mountain biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B5 1F3FE 200D 2642 FE0F", "html": "&#x1F6B5;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏾‍♂", "name": "man mountain biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B5 1F3FE 200D 2642", "html": "&#x1F6B5;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏿‍♂️", "name": "man mountain biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B5 1F3FF 200D 2642 FE0F", "html": "&#x1F6B5;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏿‍♂", "name": "man mountain biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B5 1F3FF 200D 2642", "html": "&#x1F6B5;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵‍♀️", "name": "woman mountain biking", "shortname": ":woman_mountain_biking:", "unicode": "1F6B5 200D 2640 FE0F", "html": "&#x1F6B5;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏻‍♀️", "name": "woman mountain biking: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6B5 1F3FB 200D 2640 FE0F", "html": "&#x1F6B5;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏼‍♀️", "name": "woman mountain biking: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6B5 1F3FC 200D 2640 FE0F", "html": "&#x1F6B5;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏽‍♀️", "name": "woman mountain biking: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6B5 1F3FD 200D 2640 FE0F", "html": "&#x1F6B5;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏾‍♀️", "name": "woman mountain biking: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6B5 1F3FE 200D 2640 FE0F", "html": "&#x1F6B5;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🚵🏿‍♀️", "name": "woman mountain biking: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6B5 1F3FF 200D 2640 FE0F", "html": "&#x1F6B5;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸", "name": "person cartwheeling", "shortname": ":person_cartwheeling:", "unicode": "1F938", "html": "&#x1F938;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏻", "name": "person cartwheeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F938 1F3FB", "html": "&#x1F938;&#x1F3FB;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏼", "name": "person cartwheeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F938 1F3FC", "html": "&#x1F938;&#x1F3FC;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏽", "name": "person cartwheeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F938 1F3FD", "html": "&#x1F938;&#x1F3FD;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏾", "name": "person cartwheeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F938 1F3FE", "html": "&#x1F938;&#x1F3FE;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏿", "name": "person cartwheeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F938 1F3FF", "html": "&#x1F938;&#x1F3FF;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸‍♂️", "name": "man cartwheeling", "shortname": ":man_cartwheeling:", "unicode": "1F938 200D 2642 FE0F", "html": "&#x1F938;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸‍♂", "name": "man cartwheeling", "shortname": ":man_cartwheeling:", "unicode": "1F938 200D 2642", "html": "&#x1F938;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏻‍♂️", "name": "man cartwheeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F938 1F3FB 200D 2642 FE0F", "html": "&#x1F938;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏻‍♂", "name": "man cartwheeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F938 1F3FB 200D 2642", "html": "&#x1F938;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏼‍♂️", "name": "man cartwheeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F938 1F3FC 200D 2642 FE0F", "html": "&#x1F938;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏼‍♂", "name": "man cartwheeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F938 1F3FC 200D 2642", "html": "&#x1F938;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏽‍♂️", "name": "man cartwheeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F938 1F3FD 200D 2642 FE0F", "html": "&#x1F938;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏽‍♂", "name": "man cartwheeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F938 1F3FD 200D 2642", "html": "&#x1F938;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏾‍♂️", "name": "man cartwheeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F938 1F3FE 200D 2642 FE0F", "html": "&#x1F938;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏾‍♂", "name": "man cartwheeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F938 1F3FE 200D 2642", "html": "&#x1F938;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏿‍♂️", "name": "man cartwheeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F938 1F3FF 200D 2642 FE0F", "html": "&#x1F938;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏿‍♂", "name": "man cartwheeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F938 1F3FF 200D 2642", "html": "&#x1F938;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸‍♀️", "name": "woman cartwheeling", "shortname": ":woman_cartwheeling:", "unicode": "1F938 200D 2640 FE0F", "html": "&#x1F938;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸‍♀", "name": "woman cartwheeling", "shortname": ":woman_cartwheeling:", "unicode": "1F938 200D 2640", "html": "&#x1F938;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏻‍♀️", "name": "woman cartwheeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F938 1F3FB 200D 2640 FE0F", "html": "&#x1F938;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏻‍♀", "name": "woman cartwheeling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F938 1F3FB 200D 2640", "html": "&#x1F938;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏼‍♀️", "name": "woman cartwheeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F938 1F3FC 200D 2640 FE0F", "html": "&#x1F938;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏼‍♀", "name": "woman cartwheeling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F938 1F3FC 200D 2640", "html": "&#x1F938;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏽‍♀️", "name": "woman cartwheeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F938 1F3FD 200D 2640 FE0F", "html": "&#x1F938;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏽‍♀", "name": "woman cartwheeling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F938 1F3FD 200D 2640", "html": "&#x1F938;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏾‍♀️", "name": "woman cartwheeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F938 1F3FE 200D 2640 FE0F", "html": "&#x1F938;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏾‍♀", "name": "woman cartwheeling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F938 1F3FE 200D 2640", "html": "&#x1F938;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏿‍♀️", "name": "woman cartwheeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F938 1F3FF 200D 2640 FE0F", "html": "&#x1F938;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤸🏿‍♀", "name": "woman cartwheeling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F938 1F3FF 200D 2640", "html": "&#x1F938;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤼", "name": "people wrestling", "shortname": ":people_wrestling:", "unicode": "1F93C", "html": "&#x1F93C;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤼‍♂️", "name": "men wrestling", "shortname": ":men_wrestling:", "unicode": "1F93C 200D 2642 FE0F", "html": "&#x1F93C;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤼‍♂", "name": "men wrestling", "shortname": ":men_wrestling:", "unicode": "1F93C 200D 2642", "html": "&#x1F93C;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤼‍♀️", "name": "women wrestling", "shortname": ":women_wrestling:", "unicode": "1F93C 200D 2640 FE0F", "html": "&#x1F93C;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤼‍♀", "name": "women wrestling", "shortname": ":women_wrestling:", "unicode": "1F93C 200D 2640", "html": "&#x1F93C;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽", "name": "person playing water polo", "shortname": ":person_playing_water_polo:", "unicode": "1F93D", "html": "&#x1F93D;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏻", "name": "person playing water polo: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93D 1F3FB", "html": "&#x1F93D;&#x1F3FB;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏼", "name": "person playing water polo: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93D 1F3FC", "html": "&#x1F93D;&#x1F3FC;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏽", "name": "person playing water polo: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93D 1F3FD", "html": "&#x1F93D;&#x1F3FD;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏾", "name": "person playing water polo: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93D 1F3FE", "html": "&#x1F93D;&#x1F3FE;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏿", "name": "person playing water polo: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93D 1F3FF", "html": "&#x1F93D;&#x1F3FF;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽‍♂️", "name": "man playing water polo", "shortname": ":man_playing_water_polo:", "unicode": "1F93D 200D 2642 FE0F", "html": "&#x1F93D;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽‍♂", "name": "man playing water polo", "shortname": ":man_playing_water_polo:", "unicode": "1F93D 200D 2642", "html": "&#x1F93D;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏻‍♂️", "name": "man playing water polo: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93D 1F3FB 200D 2642 FE0F", "html": "&#x1F93D;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏻‍♂", "name": "man playing water polo: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93D 1F3FB 200D 2642", "html": "&#x1F93D;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏼‍♂️", "name": "man playing water polo: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93D 1F3FC 200D 2642 FE0F", "html": "&#x1F93D;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏼‍♂", "name": "man playing water polo: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93D 1F3FC 200D 2642", "html": "&#x1F93D;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏽‍♂️", "name": "man playing water polo: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93D 1F3FD 200D 2642 FE0F", "html": "&#x1F93D;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏽‍♂", "name": "man playing water polo: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93D 1F3FD 200D 2642", "html": "&#x1F93D;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏾‍♂️", "name": "man playing water polo: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93D 1F3FE 200D 2642 FE0F", "html": "&#x1F93D;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏾‍♂", "name": "man playing water polo: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93D 1F3FE 200D 2642", "html": "&#x1F93D;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏿‍♂️", "name": "man playing water polo: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93D 1F3FF 200D 2642 FE0F", "html": "&#x1F93D;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏿‍♂", "name": "man playing water polo: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93D 1F3FF 200D 2642", "html": "&#x1F93D;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽‍♀️", "name": "woman playing water polo", "shortname": ":woman_playing_water_polo:", "unicode": "1F93D 200D 2640 FE0F", "html": "&#x1F93D;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽‍♀", "name": "woman playing water polo", "shortname": ":woman_playing_water_polo:", "unicode": "1F93D 200D 2640", "html": "&#x1F93D;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏻‍♀️", "name": "woman playing water polo: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93D 1F3FB 200D 2640 FE0F", "html": "&#x1F93D;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏻‍♀", "name": "woman playing water polo: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93D 1F3FB 200D 2640", "html": "&#x1F93D;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏼‍♀️", "name": "woman playing water polo: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93D 1F3FC 200D 2640 FE0F", "html": "&#x1F93D;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏼‍♀", "name": "woman playing water polo: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93D 1F3FC 200D 2640", "html": "&#x1F93D;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏽‍♀️", "name": "woman playing water polo: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93D 1F3FD 200D 2640 FE0F", "html": "&#x1F93D;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏽‍♀", "name": "woman playing water polo: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93D 1F3FD 200D 2640", "html": "&#x1F93D;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏾‍♀️", "name": "woman playing water polo: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93D 1F3FE 200D 2640 FE0F", "html": "&#x1F93D;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏾‍♀", "name": "woman playing water polo: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93D 1F3FE 200D 2640", "html": "&#x1F93D;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏿‍♀️", "name": "woman playing water polo: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93D 1F3FF 200D 2640 FE0F", "html": "&#x1F93D;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤽🏿‍♀", "name": "woman playing water polo: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93D 1F3FF 200D 2640", "html": "&#x1F93D;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾", "name": "person playing handball", "shortname": ":person_playing_handball:", "unicode": "1F93E", "html": "&#x1F93E;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏻", "name": "person playing handball: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93E 1F3FB", "html": "&#x1F93E;&#x1F3FB;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏼", "name": "person playing handball: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93E 1F3FC", "html": "&#x1F93E;&#x1F3FC;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏽", "name": "person playing handball: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93E 1F3FD", "html": "&#x1F93E;&#x1F3FD;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏾", "name": "person playing handball: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93E 1F3FE", "html": "&#x1F93E;&#x1F3FE;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏿", "name": "person playing handball: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93E 1F3FF", "html": "&#x1F93E;&#x1F3FF;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾‍♂️", "name": "man playing handball", "shortname": ":man_playing_handball:", "unicode": "1F93E 200D 2642 FE0F", "html": "&#x1F93E;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾‍♂", "name": "man playing handball", "shortname": ":man_playing_handball:", "unicode": "1F93E 200D 2642", "html": "&#x1F93E;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏻‍♂️", "name": "man playing handball: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93E 1F3FB 200D 2642 FE0F", "html": "&#x1F93E;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏻‍♂", "name": "man playing handball: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93E 1F3FB 200D 2642", "html": "&#x1F93E;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏼‍♂️", "name": "man playing handball: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93E 1F3FC 200D 2642 FE0F", "html": "&#x1F93E;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏼‍♂", "name": "man playing handball: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93E 1F3FC 200D 2642", "html": "&#x1F93E;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏽‍♂️", "name": "man playing handball: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93E 1F3FD 200D 2642 FE0F", "html": "&#x1F93E;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏽‍♂", "name": "man playing handball: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93E 1F3FD 200D 2642", "html": "&#x1F93E;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏾‍♂️", "name": "man playing handball: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93E 1F3FE 200D 2642 FE0F", "html": "&#x1F93E;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏾‍♂", "name": "man playing handball: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93E 1F3FE 200D 2642", "html": "&#x1F93E;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏿‍♂️", "name": "man playing handball: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93E 1F3FF 200D 2642 FE0F", "html": "&#x1F93E;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏿‍♂", "name": "man playing handball: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93E 1F3FF 200D 2642", "html": "&#x1F93E;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾‍♀️", "name": "woman playing handball", "shortname": ":woman_playing_handball:", "unicode": "1F93E 200D 2640 FE0F", "html": "&#x1F93E;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾‍♀", "name": "woman playing handball", "shortname": ":woman_playing_handball:", "unicode": "1F93E 200D 2640", "html": "&#x1F93E;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏻‍♀️", "name": "woman playing handball: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93E 1F3FB 200D 2640 FE0F", "html": "&#x1F93E;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏻‍♀", "name": "woman playing handball: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F93E 1F3FB 200D 2640", "html": "&#x1F93E;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏼‍♀️", "name": "woman playing handball: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93E 1F3FC 200D 2640 FE0F", "html": "&#x1F93E;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏼‍♀", "name": "woman playing handball: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F93E 1F3FC 200D 2640", "html": "&#x1F93E;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏽‍♀️", "name": "woman playing handball: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93E 1F3FD 200D 2640 FE0F", "html": "&#x1F93E;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏽‍♀", "name": "woman playing handball: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F93E 1F3FD 200D 2640", "html": "&#x1F93E;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏾‍♀️", "name": "woman playing handball: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93E 1F3FE 200D 2640 FE0F", "html": "&#x1F93E;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏾‍♀", "name": "woman playing handball: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F93E 1F3FE 200D 2640", "html": "&#x1F93E;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏿‍♀️", "name": "woman playing handball: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93E 1F3FF 200D 2640 FE0F", "html": "&#x1F93E;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤾🏿‍♀", "name": "woman playing handball: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F93E 1F3FF 200D 2640", "html": "&#x1F93E;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹", "name": "person juggling", "shortname": ":person_juggling:", "unicode": "1F939", "html": "&#x1F939;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏻", "name": "person juggling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F939 1F3FB", "html": "&#x1F939;&#x1F3FB;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏼", "name": "person juggling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F939 1F3FC", "html": "&#x1F939;&#x1F3FC;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏽", "name": "person juggling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F939 1F3FD", "html": "&#x1F939;&#x1F3FD;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏾", "name": "person juggling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F939 1F3FE", "html": "&#x1F939;&#x1F3FE;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏿", "name": "person juggling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F939 1F3FF", "html": "&#x1F939;&#x1F3FF;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹‍♂️", "name": "man juggling", "shortname": ":man_juggling:", "unicode": "1F939 200D 2642 FE0F", "html": "&#x1F939;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹‍♂", "name": "man juggling", "shortname": ":man_juggling:", "unicode": "1F939 200D 2642", "html": "&#x1F939;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏻‍♂️", "name": "man juggling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F939 1F3FB 200D 2642 FE0F", "html": "&#x1F939;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏻‍♂", "name": "man juggling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F939 1F3FB 200D 2642", "html": "&#x1F939;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏼‍♂️", "name": "man juggling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F939 1F3FC 200D 2642 FE0F", "html": "&#x1F939;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏼‍♂", "name": "man juggling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F939 1F3FC 200D 2642", "html": "&#x1F939;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏽‍♂️", "name": "man juggling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F939 1F3FD 200D 2642 FE0F", "html": "&#x1F939;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏽‍♂", "name": "man juggling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F939 1F3FD 200D 2642", "html": "&#x1F939;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏾‍♂️", "name": "man juggling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F939 1F3FE 200D 2642 FE0F", "html": "&#x1F939;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏾‍♂", "name": "man juggling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F939 1F3FE 200D 2642", "html": "&#x1F939;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏿‍♂️", "name": "man juggling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F939 1F3FF 200D 2642 FE0F", "html": "&#x1F939;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏿‍♂", "name": "man juggling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F939 1F3FF 200D 2642", "html": "&#x1F939;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹‍♀️", "name": "woman juggling", "shortname": ":woman_juggling:", "unicode": "1F939 200D 2640 FE0F", "html": "&#x1F939;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹‍♀", "name": "woman juggling", "shortname": ":woman_juggling:", "unicode": "1F939 200D 2640", "html": "&#x1F939;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏻‍♀️", "name": "woman juggling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F939 1F3FB 200D 2640 FE0F", "html": "&#x1F939;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏻‍♀", "name": "woman juggling: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F939 1F3FB 200D 2640", "html": "&#x1F939;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏼‍♀️", "name": "woman juggling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F939 1F3FC 200D 2640 FE0F", "html": "&#x1F939;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏼‍♀", "name": "woman juggling: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F939 1F3FC 200D 2640", "html": "&#x1F939;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏽‍♀️", "name": "woman juggling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F939 1F3FD 200D 2640 FE0F", "html": "&#x1F939;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏽‍♀", "name": "woman juggling: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F939 1F3FD 200D 2640", "html": "&#x1F939;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏾‍♀️", "name": "woman juggling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F939 1F3FE 200D 2640 FE0F", "html": "&#x1F939;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏾‍♀", "name": "woman juggling: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F939 1F3FE 200D 2640", "html": "&#x1F939;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏿‍♀️", "name": "woman juggling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F939 1F3FF 200D 2640 FE0F", "html": "&#x1F939;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🤹🏿‍♀", "name": "woman juggling: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F939 1F3FF 200D 2640", "html": "&#x1F939;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-sport)", "order": ""},
+ {"emoji": "🧘", "name": "person in lotus position", "shortname": ":person_in_lotus_position:", "unicode": "1F9D8", "html": "&#x1F9D8;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏻", "name": "person in lotus position: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D8 1F3FB", "html": "&#x1F9D8;&#x1F3FB;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏼", "name": "person in lotus position: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D8 1F3FC", "html": "&#x1F9D8;&#x1F3FC;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏽", "name": "person in lotus position: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D8 1F3FD", "html": "&#x1F9D8;&#x1F3FD;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏾", "name": "person in lotus position: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D8 1F3FE", "html": "&#x1F9D8;&#x1F3FE;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏿", "name": "person in lotus position: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D8 1F3FF", "html": "&#x1F9D8;&#x1F3FF;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘‍♂️", "name": "man in lotus position", "shortname": ":man_in_lotus_position:", "unicode": "1F9D8 200D 2642 FE0F", "html": "&#x1F9D8;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘‍♂", "name": "man in lotus position", "shortname": ":man_in_lotus_position:", "unicode": "1F9D8 200D 2642", "html": "&#x1F9D8;&#x200D;&#x2642;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏻‍♂️", "name": "man in lotus position: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D8 1F3FB 200D 2642 FE0F", "html": "&#x1F9D8;&#x1F3FB;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏻‍♂", "name": "man in lotus position: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D8 1F3FB 200D 2642", "html": "&#x1F9D8;&#x1F3FB;&#x200D;&#x2642;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏼‍♂️", "name": "man in lotus position: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D8 1F3FC 200D 2642 FE0F", "html": "&#x1F9D8;&#x1F3FC;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏼‍♂", "name": "man in lotus position: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D8 1F3FC 200D 2642", "html": "&#x1F9D8;&#x1F3FC;&#x200D;&#x2642;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏽‍♂️", "name": "man in lotus position: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D8 1F3FD 200D 2642 FE0F", "html": "&#x1F9D8;&#x1F3FD;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏽‍♂", "name": "man in lotus position: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D8 1F3FD 200D 2642", "html": "&#x1F9D8;&#x1F3FD;&#x200D;&#x2642;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏾‍♂️", "name": "man in lotus position: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D8 1F3FE 200D 2642 FE0F", "html": "&#x1F9D8;&#x1F3FE;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏾‍♂", "name": "man in lotus position: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D8 1F3FE 200D 2642", "html": "&#x1F9D8;&#x1F3FE;&#x200D;&#x2642;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏿‍♂️", "name": "man in lotus position: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D8 1F3FF 200D 2642 FE0F", "html": "&#x1F9D8;&#x1F3FF;&#x200D;&#x2642;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏿‍♂", "name": "man in lotus position: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D8 1F3FF 200D 2642", "html": "&#x1F9D8;&#x1F3FF;&#x200D;&#x2642;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘‍♀️", "name": "woman in lotus position", "shortname": ":woman_in_lotus_position:", "unicode": "1F9D8 200D 2640 FE0F", "html": "&#x1F9D8;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘‍♀", "name": "woman in lotus position", "shortname": ":woman_in_lotus_position:", "unicode": "1F9D8 200D 2640", "html": "&#x1F9D8;&#x200D;&#x2640;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏻‍♀️", "name": "woman in lotus position: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D8 1F3FB 200D 2640 FE0F", "html": "&#x1F9D8;&#x1F3FB;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏻‍♀", "name": "woman in lotus position: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D8 1F3FB 200D 2640", "html": "&#x1F9D8;&#x1F3FB;&#x200D;&#x2640;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏼‍♀️", "name": "woman in lotus position: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D8 1F3FC 200D 2640 FE0F", "html": "&#x1F9D8;&#x1F3FC;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏼‍♀", "name": "woman in lotus position: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D8 1F3FC 200D 2640", "html": "&#x1F9D8;&#x1F3FC;&#x200D;&#x2640;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏽‍♀️", "name": "woman in lotus position: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D8 1F3FD 200D 2640 FE0F", "html": "&#x1F9D8;&#x1F3FD;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏽‍♀", "name": "woman in lotus position: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D8 1F3FD 200D 2640", "html": "&#x1F9D8;&#x1F3FD;&#x200D;&#x2640;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏾‍♀️", "name": "woman in lotus position: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D8 1F3FE 200D 2640 FE0F", "html": "&#x1F9D8;&#x1F3FE;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏾‍♀", "name": "woman in lotus position: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D8 1F3FE 200D 2640", "html": "&#x1F9D8;&#x1F3FE;&#x200D;&#x2640;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏿‍♀️", "name": "woman in lotus position: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D8 1F3FF 200D 2640 FE0F", "html": "&#x1F9D8;&#x1F3FF;&#x200D;&#x2640;&#xFE0F;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧘🏿‍♀", "name": "woman in lotus position: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D8 1F3FF 200D 2640", "html": "&#x1F9D8;&#x1F3FF;&#x200D;&#x2640;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛌🏻", "name": "person in bed: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F6CC 1F3FB", "html": "&#x1F6CC;&#x1F3FB;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛌🏼", "name": "person in bed: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F6CC 1F3FC", "html": "&#x1F6CC;&#x1F3FC;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛌🏽", "name": "person in bed: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F6CC 1F3FD", "html": "&#x1F6CC;&#x1F3FD;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛌🏾", "name": "person in bed: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F6CC 1F3FE", "html": "&#x1F6CC;&#x1F3FE;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🛌🏿", "name": "person in bed: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F6CC 1F3FF", "html": "&#x1F6CC;&#x1F3FF;", "category": "People & Body (person-resting)", "order": ""},
+ {"emoji": "🧑‍🤝‍🧑", "name": "people holding hands", "shortname": ":people_holding_hands:", "unicode": "1F9D1 200D 1F91D 200D 1F9D1", "html": "&#x1F9D1;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏻‍🤝‍🧑🏻", "name": "people holding hands: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FB", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏻‍🤝‍🧑🏼", "name": "people holding hands: light skin tone, medium-light skin tone", "shortname": ":light_skin_tone_mediumlight_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FC", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏻‍🤝‍🧑🏽", "name": "people holding hands: light skin tone, medium skin tone", "shortname": ":light_skin_tone_medium_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FD", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏻‍🤝‍🧑🏾", "name": "people holding hands: light skin tone, medium-dark skin tone", "shortname": ":light_skin_tone_mediumdark_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FE", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏻‍🤝‍🧑🏿", "name": "people holding hands: light skin tone, dark skin tone", "shortname": ":light_skin_tone_dark_skin_tone:", "unicode": "1F9D1 1F3FB 200D 1F91D 200D 1F9D1 1F3FF", "html": "&#x1F9D1;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏼‍🤝‍🧑🏻", "name": "people holding hands: medium-light skin tone, light skin tone", "shortname": ":mediumlight_skin_tone_light_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FB", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏼‍🤝‍🧑🏼", "name": "people holding hands: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FC", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏼‍🤝‍🧑🏽", "name": "people holding hands: medium-light skin tone, medium skin tone", "shortname": ":mediumlight_skin_tone_medium_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FD", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏼‍🤝‍🧑🏾", "name": "people holding hands: medium-light skin tone, medium-dark skin tone", "shortname": ":mediumlight_skin_tone_medium-dark_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FE", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏼‍🤝‍🧑🏿", "name": "people holding hands: medium-light skin tone, dark skin tone", "shortname": ":mediumlight_skin_tone_dark_skin_tone:", "unicode": "1F9D1 1F3FC 200D 1F91D 200D 1F9D1 1F3FF", "html": "&#x1F9D1;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏽‍🤝‍🧑🏻", "name": "people holding hands: medium skin tone, light skin tone", "shortname": ":medium_skin_tone_light_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FB", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏽‍🤝‍🧑🏼", "name": "people holding hands: medium skin tone, medium-light skin tone", "shortname": ":medium_skin_tone_mediumlight_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FC", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏽‍🤝‍🧑🏽", "name": "people holding hands: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FD", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏽‍🤝‍🧑🏾", "name": "people holding hands: medium skin tone, medium-dark skin tone", "shortname": ":medium_skin_tone_mediumdark_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FE", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏽‍🤝‍🧑🏿", "name": "people holding hands: medium skin tone, dark skin tone", "shortname": ":medium_skin_tone_dark_skin_tone:", "unicode": "1F9D1 1F3FD 200D 1F91D 200D 1F9D1 1F3FF", "html": "&#x1F9D1;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏾‍🤝‍🧑🏻", "name": "people holding hands: medium-dark skin tone, light skin tone", "shortname": ":mediumdark_skin_tone_light_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FB", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏾‍🤝‍🧑🏼", "name": "people holding hands: medium-dark skin tone, medium-light skin tone", "shortname": ":mediumdark_skin_tone_medium-light_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FC", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏾‍🤝‍🧑🏽", "name": "people holding hands: medium-dark skin tone, medium skin tone", "shortname": ":mediumdark_skin_tone_medium_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FD", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏾‍🤝‍🧑🏾", "name": "people holding hands: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FE", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏾‍🤝‍🧑🏿", "name": "people holding hands: medium-dark skin tone, dark skin tone", "shortname": ":mediumdark_skin_tone_dark_skin_tone:", "unicode": "1F9D1 1F3FE 200D 1F91D 200D 1F9D1 1F3FF", "html": "&#x1F9D1;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏿‍🤝‍🧑🏻", "name": "people holding hands: dark skin tone, light skin tone", "shortname": ":dark_skin_tone_light_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FB", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏿‍🤝‍🧑🏼", "name": "people holding hands: dark skin tone, medium-light skin tone", "shortname": ":dark_skin_tone_mediumlight_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FC", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏿‍🤝‍🧑🏽", "name": "people holding hands: dark skin tone, medium skin tone", "shortname": ":dark_skin_tone_medium_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FD", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏿‍🤝‍🧑🏾", "name": "people holding hands: dark skin tone, medium-dark skin tone", "shortname": ":dark_skin_tone_mediumdark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FE", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🧑🏿‍🤝‍🧑🏿", "name": "people holding hands: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F9D1 1F3FF 200D 1F91D 200D 1F9D1 1F3FF", "html": "&#x1F9D1;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F9D1;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👭🏻", "name": "women holding hands: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F46D 1F3FB", "html": "&#x1F46D;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏻‍🤝‍👩🏼", "name": "women holding hands: light skin tone, medium-light skin tone", "shortname": ":light_skin_tone_mediumlight_skin_tone:", "unicode": "1F469 1F3FB 200D 1F91D 200D 1F469 1F3FC", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏻‍🤝‍👩🏽", "name": "women holding hands: light skin tone, medium skin tone", "shortname": ":light_skin_tone_medium_skin_tone:", "unicode": "1F469 1F3FB 200D 1F91D 200D 1F469 1F3FD", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏻‍🤝‍👩🏾", "name": "women holding hands: light skin tone, medium-dark skin tone", "shortname": ":light_skin_tone_mediumdark_skin_tone:", "unicode": "1F469 1F3FB 200D 1F91D 200D 1F469 1F3FE", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏻‍🤝‍👩🏿", "name": "women holding hands: light skin tone, dark skin tone", "shortname": ":light_skin_tone_dark_skin_tone:", "unicode": "1F469 1F3FB 200D 1F91D 200D 1F469 1F3FF", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏼‍🤝‍👩🏻", "name": "women holding hands: medium-light skin tone, light skin tone", "shortname": ":mediumlight_skin_tone_light_skin_tone:", "unicode": "1F469 1F3FC 200D 1F91D 200D 1F469 1F3FB", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👭🏼", "name": "women holding hands: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F46D 1F3FC", "html": "&#x1F46D;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏼‍🤝‍👩🏽", "name": "women holding hands: medium-light skin tone, medium skin tone", "shortname": ":mediumlight_skin_tone_medium_skin_tone:", "unicode": "1F469 1F3FC 200D 1F91D 200D 1F469 1F3FD", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏼‍🤝‍👩🏾", "name": "women holding hands: medium-light skin tone, medium-dark skin tone", "shortname": ":mediumlight_skin_tone_medium-dark_skin_tone:", "unicode": "1F469 1F3FC 200D 1F91D 200D 1F469 1F3FE", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏼‍🤝‍👩🏿", "name": "women holding hands: medium-light skin tone, dark skin tone", "shortname": ":mediumlight_skin_tone_dark_skin_tone:", "unicode": "1F469 1F3FC 200D 1F91D 200D 1F469 1F3FF", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏽‍🤝‍👩🏻", "name": "women holding hands: medium skin tone, light skin tone", "shortname": ":medium_skin_tone_light_skin_tone:", "unicode": "1F469 1F3FD 200D 1F91D 200D 1F469 1F3FB", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏽‍🤝‍👩🏼", "name": "women holding hands: medium skin tone, medium-light skin tone", "shortname": ":medium_skin_tone_mediumlight_skin_tone:", "unicode": "1F469 1F3FD 200D 1F91D 200D 1F469 1F3FC", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👭🏽", "name": "women holding hands: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F46D 1F3FD", "html": "&#x1F46D;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏽‍🤝‍👩🏾", "name": "women holding hands: medium skin tone, medium-dark skin tone", "shortname": ":medium_skin_tone_mediumdark_skin_tone:", "unicode": "1F469 1F3FD 200D 1F91D 200D 1F469 1F3FE", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏽‍🤝‍👩🏿", "name": "women holding hands: medium skin tone, dark skin tone", "shortname": ":medium_skin_tone_dark_skin_tone:", "unicode": "1F469 1F3FD 200D 1F91D 200D 1F469 1F3FF", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏾‍🤝‍👩🏻", "name": "women holding hands: medium-dark skin tone, light skin tone", "shortname": ":mediumdark_skin_tone_light_skin_tone:", "unicode": "1F469 1F3FE 200D 1F91D 200D 1F469 1F3FB", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏾‍🤝‍👩🏼", "name": "women holding hands: medium-dark skin tone, medium-light skin tone", "shortname": ":mediumdark_skin_tone_medium-light_skin_tone:", "unicode": "1F469 1F3FE 200D 1F91D 200D 1F469 1F3FC", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏾‍🤝‍👩🏽", "name": "women holding hands: medium-dark skin tone, medium skin tone", "shortname": ":mediumdark_skin_tone_medium_skin_tone:", "unicode": "1F469 1F3FE 200D 1F91D 200D 1F469 1F3FD", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👭🏾", "name": "women holding hands: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F46D 1F3FE", "html": "&#x1F46D;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏾‍🤝‍👩🏿", "name": "women holding hands: medium-dark skin tone, dark skin tone", "shortname": ":mediumdark_skin_tone_dark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F91D 200D 1F469 1F3FF", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏿‍🤝‍👩🏻", "name": "women holding hands: dark skin tone, light skin tone", "shortname": ":dark_skin_tone_light_skin_tone:", "unicode": "1F469 1F3FF 200D 1F91D 200D 1F469 1F3FB", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏿‍🤝‍👩🏼", "name": "women holding hands: dark skin tone, medium-light skin tone", "shortname": ":dark_skin_tone_mediumlight_skin_tone:", "unicode": "1F469 1F3FF 200D 1F91D 200D 1F469 1F3FC", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏿‍🤝‍👩🏽", "name": "women holding hands: dark skin tone, medium skin tone", "shortname": ":dark_skin_tone_medium_skin_tone:", "unicode": "1F469 1F3FF 200D 1F91D 200D 1F469 1F3FD", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏿‍🤝‍👩🏾", "name": "women holding hands: dark skin tone, medium-dark skin tone", "shortname": ":dark_skin_tone_mediumdark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F91D 200D 1F469 1F3FE", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F469;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👭🏿", "name": "women holding hands: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F46D 1F3FF", "html": "&#x1F46D;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👫🏻", "name": "woman and man holding hands: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F46B 1F3FB", "html": "&#x1F46B;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏻‍🤝‍👨🏼", "name": "woman and man holding hands: light skin tone, medium-light skin tone", "shortname": ":light_skin_tone_mediumlight_skin_tone:", "unicode": "1F469 1F3FB 200D 1F91D 200D 1F468 1F3FC", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏻‍🤝‍👨🏽", "name": "woman and man holding hands: light skin tone, medium skin tone", "shortname": ":light_skin_tone_medium_skin_tone:", "unicode": "1F469 1F3FB 200D 1F91D 200D 1F468 1F3FD", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏻‍🤝‍👨🏾", "name": "woman and man holding hands: light skin tone, medium-dark skin tone", "shortname": ":light_skin_tone_mediumdark_skin_tone:", "unicode": "1F469 1F3FB 200D 1F91D 200D 1F468 1F3FE", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏻‍🤝‍👨🏿", "name": "woman and man holding hands: light skin tone, dark skin tone", "shortname": ":light_skin_tone_dark_skin_tone:", "unicode": "1F469 1F3FB 200D 1F91D 200D 1F468 1F3FF", "html": "&#x1F469;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏼‍🤝‍👨🏻", "name": "woman and man holding hands: medium-light skin tone, light skin tone", "shortname": ":mediumlight_skin_tone_light_skin_tone:", "unicode": "1F469 1F3FC 200D 1F91D 200D 1F468 1F3FB", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👫🏼", "name": "woman and man holding hands: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F46B 1F3FC", "html": "&#x1F46B;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏼‍🤝‍👨🏽", "name": "woman and man holding hands: medium-light skin tone, medium skin tone", "shortname": ":mediumlight_skin_tone_medium_skin_tone:", "unicode": "1F469 1F3FC 200D 1F91D 200D 1F468 1F3FD", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏼‍🤝‍👨🏾", "name": "woman and man holding hands: medium-light skin tone, medium-dark skin tone", "shortname": ":mediumlight_skin_tone_medium-dark_skin_tone:", "unicode": "1F469 1F3FC 200D 1F91D 200D 1F468 1F3FE", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏼‍🤝‍👨🏿", "name": "woman and man holding hands: medium-light skin tone, dark skin tone", "shortname": ":mediumlight_skin_tone_dark_skin_tone:", "unicode": "1F469 1F3FC 200D 1F91D 200D 1F468 1F3FF", "html": "&#x1F469;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏽‍🤝‍👨🏻", "name": "woman and man holding hands: medium skin tone, light skin tone", "shortname": ":medium_skin_tone_light_skin_tone:", "unicode": "1F469 1F3FD 200D 1F91D 200D 1F468 1F3FB", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏽‍🤝‍👨🏼", "name": "woman and man holding hands: medium skin tone, medium-light skin tone", "shortname": ":medium_skin_tone_mediumlight_skin_tone:", "unicode": "1F469 1F3FD 200D 1F91D 200D 1F468 1F3FC", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👫🏽", "name": "woman and man holding hands: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F46B 1F3FD", "html": "&#x1F46B;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏽‍🤝‍👨🏾", "name": "woman and man holding hands: medium skin tone, medium-dark skin tone", "shortname": ":medium_skin_tone_mediumdark_skin_tone:", "unicode": "1F469 1F3FD 200D 1F91D 200D 1F468 1F3FE", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏽‍🤝‍👨🏿", "name": "woman and man holding hands: medium skin tone, dark skin tone", "shortname": ":medium_skin_tone_dark_skin_tone:", "unicode": "1F469 1F3FD 200D 1F91D 200D 1F468 1F3FF", "html": "&#x1F469;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏾‍🤝‍👨🏻", "name": "woman and man holding hands: medium-dark skin tone, light skin tone", "shortname": ":mediumdark_skin_tone_light_skin_tone:", "unicode": "1F469 1F3FE 200D 1F91D 200D 1F468 1F3FB", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏾‍🤝‍👨🏼", "name": "woman and man holding hands: medium-dark skin tone, medium-light skin tone", "shortname": ":mediumdark_skin_tone_medium-light_skin_tone:", "unicode": "1F469 1F3FE 200D 1F91D 200D 1F468 1F3FC", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏾‍🤝‍👨🏽", "name": "woman and man holding hands: medium-dark skin tone, medium skin tone", "shortname": ":mediumdark_skin_tone_medium_skin_tone:", "unicode": "1F469 1F3FE 200D 1F91D 200D 1F468 1F3FD", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👫🏾", "name": "woman and man holding hands: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F46B 1F3FE", "html": "&#x1F46B;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏾‍🤝‍👨🏿", "name": "woman and man holding hands: medium-dark skin tone, dark skin tone", "shortname": ":mediumdark_skin_tone_dark_skin_tone:", "unicode": "1F469 1F3FE 200D 1F91D 200D 1F468 1F3FF", "html": "&#x1F469;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏿‍🤝‍👨🏻", "name": "woman and man holding hands: dark skin tone, light skin tone", "shortname": ":dark_skin_tone_light_skin_tone:", "unicode": "1F469 1F3FF 200D 1F91D 200D 1F468 1F3FB", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏿‍🤝‍👨🏼", "name": "woman and man holding hands: dark skin tone, medium-light skin tone", "shortname": ":dark_skin_tone_mediumlight_skin_tone:", "unicode": "1F469 1F3FF 200D 1F91D 200D 1F468 1F3FC", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏿‍🤝‍👨🏽", "name": "woman and man holding hands: dark skin tone, medium skin tone", "shortname": ":dark_skin_tone_medium_skin_tone:", "unicode": "1F469 1F3FF 200D 1F91D 200D 1F468 1F3FD", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩🏿‍🤝‍👨🏾", "name": "woman and man holding hands: dark skin tone, medium-dark skin tone", "shortname": ":dark_skin_tone_mediumdark_skin_tone:", "unicode": "1F469 1F3FF 200D 1F91D 200D 1F468 1F3FE", "html": "&#x1F469;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👫🏿", "name": "woman and man holding hands: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F46B 1F3FF", "html": "&#x1F46B;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👬🏻", "name": "men holding hands: light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F46C 1F3FB", "html": "&#x1F46C;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏻‍🤝‍👨🏼", "name": "men holding hands: light skin tone, medium-light skin tone", "shortname": ":light_skin_tone_mediumlight_skin_tone:", "unicode": "1F468 1F3FB 200D 1F91D 200D 1F468 1F3FC", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏻‍🤝‍👨🏽", "name": "men holding hands: light skin tone, medium skin tone", "shortname": ":light_skin_tone_medium_skin_tone:", "unicode": "1F468 1F3FB 200D 1F91D 200D 1F468 1F3FD", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏻‍🤝‍👨🏾", "name": "men holding hands: light skin tone, medium-dark skin tone", "shortname": ":light_skin_tone_mediumdark_skin_tone:", "unicode": "1F468 1F3FB 200D 1F91D 200D 1F468 1F3FE", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏻‍🤝‍👨🏿", "name": "men holding hands: light skin tone, dark skin tone", "shortname": ":light_skin_tone_dark_skin_tone:", "unicode": "1F468 1F3FB 200D 1F91D 200D 1F468 1F3FF", "html": "&#x1F468;&#x1F3FB;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏼‍🤝‍👨🏻", "name": "men holding hands: medium-light skin tone, light skin tone", "shortname": ":mediumlight_skin_tone_light_skin_tone:", "unicode": "1F468 1F3FC 200D 1F91D 200D 1F468 1F3FB", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👬🏼", "name": "men holding hands: medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F46C 1F3FC", "html": "&#x1F46C;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏼‍🤝‍👨🏽", "name": "men holding hands: medium-light skin tone, medium skin tone", "shortname": ":mediumlight_skin_tone_medium_skin_tone:", "unicode": "1F468 1F3FC 200D 1F91D 200D 1F468 1F3FD", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏼‍🤝‍👨🏾", "name": "men holding hands: medium-light skin tone, medium-dark skin tone", "shortname": ":mediumlight_skin_tone_medium-dark_skin_tone:", "unicode": "1F468 1F3FC 200D 1F91D 200D 1F468 1F3FE", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏼‍🤝‍👨🏿", "name": "men holding hands: medium-light skin tone, dark skin tone", "shortname": ":mediumlight_skin_tone_dark_skin_tone:", "unicode": "1F468 1F3FC 200D 1F91D 200D 1F468 1F3FF", "html": "&#x1F468;&#x1F3FC;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏽‍🤝‍👨🏻", "name": "men holding hands: medium skin tone, light skin tone", "shortname": ":medium_skin_tone_light_skin_tone:", "unicode": "1F468 1F3FD 200D 1F91D 200D 1F468 1F3FB", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏽‍🤝‍👨🏼", "name": "men holding hands: medium skin tone, medium-light skin tone", "shortname": ":medium_skin_tone_mediumlight_skin_tone:", "unicode": "1F468 1F3FD 200D 1F91D 200D 1F468 1F3FC", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👬🏽", "name": "men holding hands: medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F46C 1F3FD", "html": "&#x1F46C;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏽‍🤝‍👨🏾", "name": "men holding hands: medium skin tone, medium-dark skin tone", "shortname": ":medium_skin_tone_mediumdark_skin_tone:", "unicode": "1F468 1F3FD 200D 1F91D 200D 1F468 1F3FE", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏽‍🤝‍👨🏿", "name": "men holding hands: medium skin tone, dark skin tone", "shortname": ":medium_skin_tone_dark_skin_tone:", "unicode": "1F468 1F3FD 200D 1F91D 200D 1F468 1F3FF", "html": "&#x1F468;&#x1F3FD;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏾‍🤝‍👨🏻", "name": "men holding hands: medium-dark skin tone, light skin tone", "shortname": ":mediumdark_skin_tone_light_skin_tone:", "unicode": "1F468 1F3FE 200D 1F91D 200D 1F468 1F3FB", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏾‍🤝‍👨🏼", "name": "men holding hands: medium-dark skin tone, medium-light skin tone", "shortname": ":mediumdark_skin_tone_medium-light_skin_tone:", "unicode": "1F468 1F3FE 200D 1F91D 200D 1F468 1F3FC", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏾‍🤝‍👨🏽", "name": "men holding hands: medium-dark skin tone, medium skin tone", "shortname": ":mediumdark_skin_tone_medium_skin_tone:", "unicode": "1F468 1F3FE 200D 1F91D 200D 1F468 1F3FD", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👬🏾", "name": "men holding hands: medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F46C 1F3FE", "html": "&#x1F46C;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏾‍🤝‍👨🏿", "name": "men holding hands: medium-dark skin tone, dark skin tone", "shortname": ":mediumdark_skin_tone_dark_skin_tone:", "unicode": "1F468 1F3FE 200D 1F91D 200D 1F468 1F3FF", "html": "&#x1F468;&#x1F3FE;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏿‍🤝‍👨🏻", "name": "men holding hands: dark skin tone, light skin tone", "shortname": ":dark_skin_tone_light_skin_tone:", "unicode": "1F468 1F3FF 200D 1F91D 200D 1F468 1F3FB", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FB;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏿‍🤝‍👨🏼", "name": "men holding hands: dark skin tone, medium-light skin tone", "shortname": ":dark_skin_tone_mediumlight_skin_tone:", "unicode": "1F468 1F3FF 200D 1F91D 200D 1F468 1F3FC", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FC;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏿‍🤝‍👨🏽", "name": "men holding hands: dark skin tone, medium skin tone", "shortname": ":dark_skin_tone_medium_skin_tone:", "unicode": "1F468 1F3FF 200D 1F91D 200D 1F468 1F3FD", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FD;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨🏿‍🤝‍👨🏾", "name": "men holding hands: dark skin tone, medium-dark skin tone", "shortname": ":dark_skin_tone_mediumdark_skin_tone:", "unicode": "1F468 1F3FF 200D 1F91D 200D 1F468 1F3FE", "html": "&#x1F468;&#x1F3FF;&#x200D;&#x1F91D;&#x200D;&#x1F468;&#x1F3FE;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👬🏿", "name": "men holding hands: dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F46C 1F3FF", "html": "&#x1F46C;&#x1F3FF;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍❤️‍💋‍👨", "name": "kiss: woman, man", "shortname": ":woman_man:", "unicode": "1F469 200D 2764 FE0F 200D 1F48B 200D 1F468", "html": "&#x1F469;&#x200D;&#x2764;&#xFE0F;&#x200D;&#x1F48B;&#x200D;&#x1F468;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍❤‍💋‍👨", "name": "kiss: woman, man", "shortname": ":woman_man:", "unicode": "1F469 200D 2764 200D 1F48B 200D 1F468", "html": "&#x1F469;&#x200D;&#x2764;&#x200D;&#x1F48B;&#x200D;&#x1F468;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍❤‍💋‍👨", "name": "kiss: man, man", "shortname": ":man_man:", "unicode": "1F468 200D 2764 200D 1F48B 200D 1F468", "html": "&#x1F468;&#x200D;&#x2764;&#x200D;&#x1F48B;&#x200D;&#x1F468;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍❤‍💋‍👩", "name": "kiss: woman, woman", "shortname": ":woman_woman:", "unicode": "1F469 200D 2764 200D 1F48B 200D 1F469", "html": "&#x1F469;&#x200D;&#x2764;&#x200D;&#x1F48B;&#x200D;&#x1F469;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍❤️‍👨", "name": "couple with heart: woman, man", "shortname": ":woman_man:", "unicode": "1F469 200D 2764 FE0F 200D 1F468", "html": "&#x1F469;&#x200D;&#x2764;&#xFE0F;&#x200D;&#x1F468;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍❤‍👨", "name": "couple with heart: woman, man", "shortname": ":woman_man:", "unicode": "1F469 200D 2764 200D 1F468", "html": "&#x1F469;&#x200D;&#x2764;&#x200D;&#x1F468;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍❤‍👨", "name": "couple with heart: man, man", "shortname": ":man_man:", "unicode": "1F468 200D 2764 200D 1F468", "html": "&#x1F468;&#x200D;&#x2764;&#x200D;&#x1F468;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👩‍❤‍👩", "name": "couple with heart: woman, woman", "shortname": ":woman_woman:", "unicode": "1F469 200D 2764 200D 1F469", "html": "&#x1F469;&#x200D;&#x2764;&#x200D;&#x1F469;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "👨‍👩‍👦", "name": "family: man, woman, boy", "shortname": ":man_woman_boy:", "unicode": "1F468 200D 1F469 200D 1F466", "html": "&#x1F468;&#x200D;&#x1F469;&#x200D;&#x1F466;", "category": "People & Body (family)", "order": ""},
+ {"emoji": "🗣️", "name": "speaking head", "shortname": ":speaking_head:", "unicode": "1F5E3 FE0F", "html": "&#x1F5E3;&#xFE0F;", "category": "People & Body (person-symbol)", "order": ""},
+ {"emoji": "🏻", "name": "light skin tone", "shortname": ":light_skin_tone:", "unicode": "1F3FB", "html": "&#x1F3FB;", "category": "Component (skin-tone)", "order": ""},
+ {"emoji": "🏼", "name": "medium-light skin tone", "shortname": ":mediumlight_skin_tone:", "unicode": "1F3FC", "html": "&#x1F3FC;", "category": "Component (skin-tone)", "order": ""},
+ {"emoji": "🏽", "name": "medium skin tone", "shortname": ":medium_skin_tone:", "unicode": "1F3FD", "html": "&#x1F3FD;", "category": "Component (skin-tone)", "order": ""},
+ {"emoji": "🏾", "name": "medium-dark skin tone", "shortname": ":mediumdark_skin_tone:", "unicode": "1F3FE", "html": "&#x1F3FE;", "category": "Component (skin-tone)", "order": ""},
+ {"emoji": "🏿", "name": "dark skin tone", "shortname": ":dark_skin_tone:", "unicode": "1F3FF", "html": "&#x1F3FF;", "category": "Component (skin-tone)", "order": ""},
+ {"emoji": "🦰", "name": "red hair", "shortname": ":red_hair:", "unicode": "1F9B0", "html": "&#x1F9B0;", "category": "Component (hair-style)", "order": ""},
+ {"emoji": "🦱", "name": "curly hair", "shortname": ":curly_hair:", "unicode": "1F9B1", "html": "&#x1F9B1;", "category": "Component (hair-style)", "order": ""},
+ {"emoji": "🦳", "name": "white hair", "shortname": ":white_hair:", "unicode": "1F9B3", "html": "&#x1F9B3;", "category": "Component (hair-style)", "order": ""},
+ {"emoji": "🦲", "name": "bald", "shortname": ":bald:", "unicode": "1F9B2", "html": "&#x1F9B2;", "category": "Component (hair-style)", "order": ""},
+ {"emoji": "🦍", "name": "gorilla", "shortname": ":gorilla:", "unicode": "1F98D", "html": "&#x1F98D;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦧", "name": "orangutan", "shortname": ":orangutan:", "unicode": "1F9A7", "html": "&#x1F9A7;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦮", "name": "guide dog", "shortname": ":guide_dog:", "unicode": "1F9AE", "html": "&#x1F9AE;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🐕‍🦺", "name": "service dog", "shortname": ":service_dog:", "unicode": "1F415 200D 1F9BA", "html": "&#x1F415;&#x200D;&#x1F9BA;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦊", "name": "fox", "shortname": ":fox:", "unicode": "1F98A", "html": "&#x1F98A;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦝", "name": "raccoon", "shortname": ":raccoon:", "unicode": "1F99D", "html": "&#x1F99D;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦓", "name": "zebra", "shortname": ":zebra:", "unicode": "1F993", "html": "&#x1F993;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦌", "name": "deer", "shortname": ":deer:", "unicode": "1F98C", "html": "&#x1F98C;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦙", "name": "llama", "shortname": ":llama:", "unicode": "1F999", "html": "&#x1F999;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦒", "name": "giraffe", "shortname": ":giraffe:", "unicode": "1F992", "html": "&#x1F992;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦏", "name": "rhinoceros", "shortname": ":rhinoceros:", "unicode": "1F98F", "html": "&#x1F98F;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦛", "name": "hippopotamus", "shortname": ":hippopotamus:", "unicode": "1F99B", "html": "&#x1F99B;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🐿️", "name": "chipmunk", "shortname": ":chipmunk:", "unicode": "1F43F FE0F", "html": "&#x1F43F;&#xFE0F;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦔", "name": "hedgehog", "shortname": ":hedgehog:", "unicode": "1F994", "html": "&#x1F994;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦇", "name": "bat", "shortname": ":bat:", "unicode": "1F987", "html": "&#x1F987;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦥", "name": "sloth", "shortname": ":sloth:", "unicode": "1F9A5", "html": "&#x1F9A5;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦦", "name": "otter", "shortname": ":otter:", "unicode": "1F9A6", "html": "&#x1F9A6;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦨", "name": "skunk", "shortname": ":skunk:", "unicode": "1F9A8", "html": "&#x1F9A8;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦘", "name": "kangaroo", "shortname": ":kangaroo:", "unicode": "1F998", "html": "&#x1F998;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🦡", "name": "badger", "shortname": ":badger:", "unicode": "1F9A1", "html": "&#x1F9A1;", "category": "Animals & Nature (animal-mammal)", "order": ""},
+ {"emoji": "🕊️", "name": "dove", "shortname": ":dove:", "unicode": "1F54A FE0F", "html": "&#x1F54A;&#xFE0F;", "category": "Animals & Nature (animal-bird)", "order": ""},
+ {"emoji": "🦅", "name": "eagle", "shortname": ":eagle:", "unicode": "1F985", "html": "&#x1F985;", "category": "Animals & Nature (animal-bird)", "order": ""},
+ {"emoji": "🦆", "name": "duck", "shortname": ":duck:", "unicode": "1F986", "html": "&#x1F986;", "category": "Animals & Nature (animal-bird)", "order": ""},
+ {"emoji": "🦢", "name": "swan", "shortname": ":swan:", "unicode": "1F9A2", "html": "&#x1F9A2;", "category": "Animals & Nature (animal-bird)", "order": ""},
+ {"emoji": "🦉", "name": "owl", "shortname": ":owl:", "unicode": "1F989", "html": "&#x1F989;", "category": "Animals & Nature (animal-bird)", "order": ""},
+ {"emoji": "🦩", "name": "flamingo", "shortname": ":flamingo:", "unicode": "1F9A9", "html": "&#x1F9A9;", "category": "Animals & Nature (animal-bird)", "order": ""},
+ {"emoji": "🦚", "name": "peacock", "shortname": ":peacock:", "unicode": "1F99A", "html": "&#x1F99A;", "category": "Animals & Nature (animal-bird)", "order": ""},
+ {"emoji": "🦜", "name": "parrot", "shortname": ":parrot:", "unicode": "1F99C", "html": "&#x1F99C;", "category": "Animals & Nature (animal-bird)", "order": ""},
+ {"emoji": "🦎", "name": "lizard", "shortname": ":lizard:", "unicode": "1F98E", "html": "&#x1F98E;", "category": "Animals & Nature (animal-reptile)", "order": ""},
+ {"emoji": "🦕", "name": "sauropod", "shortname": ":sauropod:", "unicode": "1F995", "html": "&#x1F995;", "category": "Animals & Nature (animal-reptile)", "order": ""},
+ {"emoji": "🦖", "name": "T-Rex", "shortname": ":TRex:", "unicode": "1F996", "html": "&#x1F996;", "category": "Animals & Nature (animal-reptile)", "order": ""},
+ {"emoji": "🦈", "name": "shark", "shortname": ":shark:", "unicode": "1F988", "html": "&#x1F988;", "category": "Animals & Nature (animal-marine)", "order": ""},
+ {"emoji": "🦋", "name": "butterfly", "shortname": ":butterfly:", "unicode": "1F98B", "html": "&#x1F98B;", "category": "Animals & Nature (animal-bug)", "order": ""},
+ {"emoji": "🦗", "name": "cricket", "shortname": ":cricket:", "unicode": "1F997", "html": "&#x1F997;", "category": "Animals & Nature (animal-bug)", "order": ""},
+ {"emoji": "🕷️", "name": "spider", "shortname": ":spider:", "unicode": "1F577 FE0F", "html": "&#x1F577;&#xFE0F;", "category": "Animals & Nature (animal-bug)", "order": ""},
+ {"emoji": "🕸️", "name": "spider web", "shortname": ":spider_web:", "unicode": "1F578 FE0F", "html": "&#x1F578;&#xFE0F;", "category": "Animals & Nature (animal-bug)", "order": ""},
+ {"emoji": "🦟", "name": "mosquito", "shortname": ":mosquito:", "unicode": "1F99F", "html": "&#x1F99F;", "category": "Animals & Nature (animal-bug)", "order": ""},
+ {"emoji": "🦠", "name": "microbe", "shortname": ":microbe:", "unicode": "1F9A0", "html": "&#x1F9A0;", "category": "Animals & Nature (animal-bug)", "order": ""},
+ {"emoji": "🏵️", "name": "rosette", "shortname": ":rosette:", "unicode": "1F3F5 FE0F", "html": "&#x1F3F5;&#xFE0F;", "category": "Animals & Nature (plant-flower)", "order": ""},
+ {"emoji": "🥀", "name": "wilted flower", "shortname": ":wilted_flower:", "unicode": "1F940", "html": "&#x1F940;", "category": "Animals & Nature (plant-flower)", "order": ""},
+ {"emoji": "☘️", "name": "shamrock", "shortname": ":shamrock:", "unicode": "2618 FE0F", "html": "&#x2618;&#xFE0F;", "category": "Animals & Nature (plant-other)", "order": ""},
+ {"emoji": "🥭", "name": "mango", "shortname": ":mango:", "unicode": "1F96D", "html": "&#x1F96D;", "category": "Food & Drink (food-fruit)", "order": ""},
+ {"emoji": "🥝", "name": "kiwi fruit", "shortname": ":kiwi_fruit:", "unicode": "1F95D", "html": "&#x1F95D;", "category": "Food & Drink (food-fruit)", "order": ""},
+ {"emoji": "🥥", "name": "coconut", "shortname": ":coconut:", "unicode": "1F965", "html": "&#x1F965;", "category": "Food & Drink (food-fruit)", "order": ""},
+ {"emoji": "🥑", "name": "avocado", "shortname": ":avocado:", "unicode": "1F951", "html": "&#x1F951;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🥔", "name": "potato", "shortname": ":potato:", "unicode": "1F954", "html": "&#x1F954;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🥕", "name": "carrot", "shortname": ":carrot:", "unicode": "1F955", "html": "&#x1F955;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🌶️", "name": "hot pepper", "shortname": ":hot_pepper:", "unicode": "1F336 FE0F", "html": "&#x1F336;&#xFE0F;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🥒", "name": "cucumber", "shortname": ":cucumber:", "unicode": "1F952", "html": "&#x1F952;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🥬", "name": "leafy green", "shortname": ":leafy_green:", "unicode": "1F96C", "html": "&#x1F96C;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🥦", "name": "broccoli", "shortname": ":broccoli:", "unicode": "1F966", "html": "&#x1F966;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🧄", "name": "garlic", "shortname": ":garlic:", "unicode": "1F9C4", "html": "&#x1F9C4;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🧅", "name": "onion", "shortname": ":onion:", "unicode": "1F9C5", "html": "&#x1F9C5;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🥜", "name": "peanuts", "shortname": ":peanuts:", "unicode": "1F95C", "html": "&#x1F95C;", "category": "Food & Drink (food-vegetable)", "order": ""},
+ {"emoji": "🥐", "name": "croissant", "shortname": ":croissant:", "unicode": "1F950", "html": "&#x1F950;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥖", "name": "baguette bread", "shortname": ":baguette_bread:", "unicode": "1F956", "html": "&#x1F956;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥨", "name": "pretzel", "shortname": ":pretzel:", "unicode": "1F968", "html": "&#x1F968;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥯", "name": "bagel", "shortname": ":bagel:", "unicode": "1F96F", "html": "&#x1F96F;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥞", "name": "pancakes", "shortname": ":pancakes:", "unicode": "1F95E", "html": "&#x1F95E;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🧇", "name": "waffle", "shortname": ":waffle:", "unicode": "1F9C7", "html": "&#x1F9C7;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥩", "name": "cut of meat", "shortname": ":cut_of_meat:", "unicode": "1F969", "html": "&#x1F969;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥓", "name": "bacon", "shortname": ":bacon:", "unicode": "1F953", "html": "&#x1F953;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥪", "name": "sandwich", "shortname": ":sandwich:", "unicode": "1F96A", "html": "&#x1F96A;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥙", "name": "stuffed flatbread", "shortname": ":stuffed_flatbread:", "unicode": "1F959", "html": "&#x1F959;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🧆", "name": "falafel", "shortname": ":falafel:", "unicode": "1F9C6", "html": "&#x1F9C6;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥘", "name": "shallow pan of food", "shortname": ":shallow_pan_of_food:", "unicode": "1F958", "html": "&#x1F958;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥣", "name": "bowl with spoon", "shortname": ":bowl_with_spoon:", "unicode": "1F963", "html": "&#x1F963;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥗", "name": "green salad", "shortname": ":green_salad:", "unicode": "1F957", "html": "&#x1F957;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🧈", "name": "butter", "shortname": ":butter:", "unicode": "1F9C8", "html": "&#x1F9C8;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🧂", "name": "salt", "shortname": ":salt:", "unicode": "1F9C2", "html": "&#x1F9C2;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥫", "name": "canned food", "shortname": ":canned_food:", "unicode": "1F96B", "html": "&#x1F96B;", "category": "Food & Drink (food-prepared)", "order": ""},
+ {"emoji": "🥮", "name": "moon cake", "shortname": ":moon_cake:", "unicode": "1F96E", "html": "&#x1F96E;", "category": "Food & Drink (food-asian)", "order": ""},
+ {"emoji": "🥟", "name": "dumpling", "shortname": ":dumpling:", "unicode": "1F95F", "html": "&#x1F95F;", "category": "Food & Drink (food-asian)", "order": ""},
+ {"emoji": "🥠", "name": "fortune cookie", "shortname": ":fortune_cookie:", "unicode": "1F960", "html": "&#x1F960;", "category": "Food & Drink (food-asian)", "order": ""},
+ {"emoji": "🥡", "name": "takeout box", "shortname": ":takeout_box:", "unicode": "1F961", "html": "&#x1F961;", "category": "Food & Drink (food-asian)", "order": ""},
+ {"emoji": "🦞", "name": "lobster", "shortname": ":lobster:", "unicode": "1F99E", "html": "&#x1F99E;", "category": "Food & Drink (food-marine)", "order": ""},
+ {"emoji": "🦐", "name": "shrimp", "shortname": ":shrimp:", "unicode": "1F990", "html": "&#x1F990;", "category": "Food & Drink (food-marine)", "order": ""},
+ {"emoji": "🦑", "name": "squid", "shortname": ":squid:", "unicode": "1F991", "html": "&#x1F991;", "category": "Food & Drink (food-marine)", "order": ""},
+ {"emoji": "🦪", "name": "oyster", "shortname": ":oyster:", "unicode": "1F9AA", "html": "&#x1F9AA;", "category": "Food & Drink (food-marine)", "order": ""},
+ {"emoji": "🧁", "name": "cupcake", "shortname": ":cupcake:", "unicode": "1F9C1", "html": "&#x1F9C1;", "category": "Food & Drink (food-sweet)", "order": ""},
+ {"emoji": "🥧", "name": "pie", "shortname": ":pie:", "unicode": "1F967", "html": "&#x1F967;", "category": "Food & Drink (food-sweet)", "order": ""},
+ {"emoji": "🥛", "name": "glass of milk", "shortname": ":glass_of_milk:", "unicode": "1F95B", "html": "&#x1F95B;", "category": "Food & Drink (drink)", "order": ""},
+ {"emoji": "🥂", "name": "clinking glasses", "shortname": ":clinking_glasses:", "unicode": "1F942", "html": "&#x1F942;", "category": "Food & Drink (drink)", "order": ""},
+ {"emoji": "🥃", "name": "tumbler glass", "shortname": ":tumbler_glass:", "unicode": "1F943", "html": "&#x1F943;", "category": "Food & Drink (drink)", "order": ""},
+ {"emoji": "🥤", "name": "cup with straw", "shortname": ":cup_with_straw:", "unicode": "1F964", "html": "&#x1F964;", "category": "Food & Drink (drink)", "order": ""},
+ {"emoji": "🧃", "name": "beverage box", "shortname": ":beverage_box:", "unicode": "1F9C3", "html": "&#x1F9C3;", "category": "Food & Drink (drink)", "order": ""},
+ {"emoji": "🧉", "name": "mate", "shortname": ":mate:", "unicode": "1F9C9", "html": "&#x1F9C9;", "category": "Food & Drink (drink)", "order": ""},
+ {"emoji": "🧊", "name": "ice", "shortname": ":ice:", "unicode": "1F9CA", "html": "&#x1F9CA;", "category": "Food & Drink (drink)", "order": ""},
+ {"emoji": "🥢", "name": "chopsticks", "shortname": ":chopsticks:", "unicode": "1F962", "html": "&#x1F962;", "category": "Food & Drink (dishware)", "order": ""},
+ {"emoji": "🍽️", "name": "fork and knife with plate", "shortname": ":fork_and_knife_with_plate:", "unicode": "1F37D FE0F", "html": "&#x1F37D;&#xFE0F;", "category": "Food & Drink (dishware)", "order": ""},
+ {"emoji": "🥄", "name": "spoon", "shortname": ":spoon:", "unicode": "1F944", "html": "&#x1F944;", "category": "Food & Drink (dishware)", "order": ""},
+ {"emoji": "🗺️", "name": "world map", "shortname": ":world_map:", "unicode": "1F5FA FE0F", "html": "&#x1F5FA;&#xFE0F;", "category": "Travel & Places (place-map)", "order": ""},
+ {"emoji": "🧭", "name": "compass", "shortname": ":compass:", "unicode": "1F9ED", "html": "&#x1F9ED;", "category": "Travel & Places (place-map)", "order": ""},
+ {"emoji": "🏔️", "name": "snow-capped mountain", "shortname": ":snowcapped_mountain:", "unicode": "1F3D4 FE0F", "html": "&#x1F3D4;&#xFE0F;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "⛰️", "name": "mountain", "shortname": ":mountain:", "unicode": "26F0 FE0F", "html": "&#x26F0;&#xFE0F;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏕️", "name": "camping", "shortname": ":camping:", "unicode": "1F3D5 FE0F", "html": "&#x1F3D5;&#xFE0F;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏖️", "name": "beach with umbrella", "shortname": ":beach_with_umbrella:", "unicode": "1F3D6 FE0F", "html": "&#x1F3D6;&#xFE0F;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏜️", "name": "desert", "shortname": ":desert:", "unicode": "1F3DC FE0F", "html": "&#x1F3DC;&#xFE0F;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏝️", "name": "desert island", "shortname": ":desert_island:", "unicode": "1F3DD FE0F", "html": "&#x1F3DD;&#xFE0F;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏞️", "name": "national park", "shortname": ":national_park:", "unicode": "1F3DE FE0F", "html": "&#x1F3DE;&#xFE0F;", "category": "Travel & Places (place-geographic)", "order": ""},
+ {"emoji": "🏟️", "name": "stadium", "shortname": ":stadium:", "unicode": "1F3DF FE0F", "html": "&#x1F3DF;&#xFE0F;", "category": "Travel & Places (place-building)", "order": ""},
+ {"emoji": "🏛️", "name": "classical building", "shortname": ":classical_building:", "unicode": "1F3DB FE0F", "html": "&#x1F3DB;&#xFE0F;", "category": "Travel & Places (place-building)", "order": ""},
+ {"emoji": "🏗️", "name": "building construction", "shortname": ":building_construction:", "unicode": "1F3D7 FE0F", "html": "&#x1F3D7;&#xFE0F;", "category": "Travel & Places (place-building)", "order": ""},
+ {"emoji": "🧱", "name": "brick", "shortname": ":brick:", "unicode": "1F9F1", "html": "&#x1F9F1;", "category": "Travel & Places (place-building)", "order": ""},
+ {"emoji": "🏘️", "name": "houses", "shortname": ":houses:", "unicode": "1F3D8 FE0F", "html": "&#x1F3D8;&#xFE0F;", "category": "Travel & Places (place-building)", "order": ""},
+ {"emoji": "🏚️", "name": "derelict house", "shortname": ":derelict_house:", "unicode": "1F3DA FE0F", "html": "&#x1F3DA;&#xFE0F;", "category": "Travel & Places (place-building)", "order": ""},
+ {"emoji": "🛕", "name": "hindu temple", "shortname": ":hindu_temple:", "unicode": "1F6D5", "html": "&#x1F6D5;", "category": "Travel & Places (place-religious)", "order": ""},
+ {"emoji": "⛩️", "name": "shinto shrine", "shortname": ":shinto_shrine:", "unicode": "26E9 FE0F", "html": "&#x26E9;&#xFE0F;", "category": "Travel & Places (place-religious)", "order": ""},
+ {"emoji": "🏙️", "name": "cityscape", "shortname": ":cityscape:", "unicode": "1F3D9 FE0F", "html": "&#x1F3D9;&#xFE0F;", "category": "Travel & Places (place-other)", "order": ""},
+ {"emoji": "♨", "name": "hot springs", "shortname": ":hot_springs:", "unicode": "2668", "html": "&#x2668;", "category": "Travel & Places (place-other)", "order": ""},
+ {"emoji": "🏎️", "name": "racing car", "shortname": ":racing_car:", "unicode": "1F3CE FE0F", "html": "&#x1F3CE;&#xFE0F;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🏍️", "name": "motorcycle", "shortname": ":motorcycle:", "unicode": "1F3CD FE0F", "html": "&#x1F3CD;&#xFE0F;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🛵", "name": "motor scooter", "shortname": ":motor_scooter:", "unicode": "1F6F5", "html": "&#x1F6F5;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🦽", "name": "manual wheelchair", "shortname": ":manual_wheelchair:", "unicode": "1F9BD", "html": "&#x1F9BD;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🦼", "name": "motorized wheelchair", "shortname": ":motorized_wheelchair:", "unicode": "1F9BC", "html": "&#x1F9BC;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🛺", "name": "auto rickshaw", "shortname": ":auto_rickshaw:", "unicode": "1F6FA", "html": "&#x1F6FA;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🛴", "name": "kick scooter", "shortname": ":kick_scooter:", "unicode": "1F6F4", "html": "&#x1F6F4;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🛹", "name": "skateboard", "shortname": ":skateboard:", "unicode": "1F6F9", "html": "&#x1F6F9;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🛣️", "name": "motorway", "shortname": ":motorway:", "unicode": "1F6E3 FE0F", "html": "&#x1F6E3;&#xFE0F;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🛤️", "name": "railway track", "shortname": ":railway_track:", "unicode": "1F6E4 FE0F", "html": "&#x1F6E4;&#xFE0F;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🛢️", "name": "oil drum", "shortname": ":oil_drum:", "unicode": "1F6E2 FE0F", "html": "&#x1F6E2;&#xFE0F;", "category": "Travel & Places (transport-ground)", "order": ""},
+ {"emoji": "🛶", "name": "canoe", "shortname": ":canoe:", "unicode": "1F6F6", "html": "&#x1F6F6;", "category": "Travel & Places (transport-water)", "order": ""},
+ {"emoji": "🛳️", "name": "passenger ship", "shortname": ":passenger_ship:", "unicode": "1F6F3 FE0F", "html": "&#x1F6F3;&#xFE0F;", "category": "Travel & Places (transport-water)", "order": ""},
+ {"emoji": "⛴️", "name": "ferry", "shortname": ":ferry:", "unicode": "26F4 FE0F", "html": "&#x26F4;&#xFE0F;", "category": "Travel & Places (transport-water)", "order": ""},
+ {"emoji": "🛥️", "name": "motor boat", "shortname": ":motor_boat:", "unicode": "1F6E5 FE0F", "html": "&#x1F6E5;&#xFE0F;", "category": "Travel & Places (transport-water)", "order": ""},
+ {"emoji": "✈", "name": "airplane", "shortname": ":airplane:", "unicode": "2708", "html": "&#x2708;", "category": "Travel & Places (transport-air)", "order": ""},
+ {"emoji": "🛩️", "name": "small airplane", "shortname": ":small_airplane:", "unicode": "1F6E9 FE0F", "html": "&#x1F6E9;&#xFE0F;", "category": "Travel & Places (transport-air)", "order": ""},
+ {"emoji": "🪂", "name": "parachute", "shortname": ":parachute:", "unicode": "1FA82", "html": "&#x1FA82;", "category": "Travel & Places (transport-air)", "order": ""},
+ {"emoji": "🛰️", "name": "satellite", "shortname": ":satellite:", "unicode": "1F6F0 FE0F", "html": "&#x1F6F0;&#xFE0F;", "category": "Travel & Places (transport-air)", "order": ""},
+ {"emoji": "🛸", "name": "flying saucer", "shortname": ":flying_saucer:", "unicode": "1F6F8", "html": "&#x1F6F8;", "category": "Travel & Places (transport-air)", "order": ""},
+ {"emoji": "🛎️", "name": "bellhop bell", "shortname": ":bellhop_bell:", "unicode": "1F6CE FE0F", "html": "&#x1F6CE;&#xFE0F;", "category": "Travel & Places (hotel)", "order": ""},
+ {"emoji": "🧳", "name": "luggage", "shortname": ":luggage:", "unicode": "1F9F3", "html": "&#x1F9F3;", "category": "Travel & Places (hotel)", "order": ""},
+ {"emoji": "⏱️", "name": "stopwatch", "shortname": ":stopwatch:", "unicode": "23F1 FE0F", "html": "&#x23F1;&#xFE0F;", "category": "Travel & Places (time)", "order": ""},
+ {"emoji": "⏲️", "name": "timer clock", "shortname": ":timer_clock:", "unicode": "23F2 FE0F", "html": "&#x23F2;&#xFE0F;", "category": "Travel & Places (time)", "order": ""},
+ {"emoji": "🕰️", "name": "mantelpiece clock", "shortname": ":mantelpiece_clock:", "unicode": "1F570 FE0F", "html": "&#x1F570;&#xFE0F;", "category": "Travel & Places (time)", "order": ""},
+ {"emoji": "🌡️", "name": "thermometer", "shortname": ":thermometer:", "unicode": "1F321 FE0F", "html": "&#x1F321;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "☀", "name": "sun", "shortname": ":sun:", "unicode": "2600", "html": "&#x2600;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🪐", "name": "ringed planet", "shortname": ":ringed_planet:", "unicode": "1FA90", "html": "&#x1FA90;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "☁️", "name": "cloud", "shortname": ":cloud:", "unicode": "2601 FE0F", "html": "&#x2601;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "⛈️", "name": "cloud with lightning and rain", "shortname": ":cloud_with_lightning_and_rain:", "unicode": "26C8 FE0F", "html": "&#x26C8;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌤️", "name": "sun behind small cloud", "shortname": ":sun_behind_small_cloud:", "unicode": "1F324 FE0F", "html": "&#x1F324;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌥️", "name": "sun behind large cloud", "shortname": ":sun_behind_large_cloud:", "unicode": "1F325 FE0F", "html": "&#x1F325;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌦️", "name": "sun behind rain cloud", "shortname": ":sun_behind_rain_cloud:", "unicode": "1F326 FE0F", "html": "&#x1F326;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌧️", "name": "cloud with rain", "shortname": ":cloud_with_rain:", "unicode": "1F327 FE0F", "html": "&#x1F327;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌨️", "name": "cloud with snow", "shortname": ":cloud_with_snow:", "unicode": "1F328 FE0F", "html": "&#x1F328;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌩️", "name": "cloud with lightning", "shortname": ":cloud_with_lightning:", "unicode": "1F329 FE0F", "html": "&#x1F329;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌪️", "name": "tornado", "shortname": ":tornado:", "unicode": "1F32A FE0F", "html": "&#x1F32A;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌫️", "name": "fog", "shortname": ":fog:", "unicode": "1F32B FE0F", "html": "&#x1F32B;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🌬️", "name": "wind face", "shortname": ":wind_face:", "unicode": "1F32C FE0F", "html": "&#x1F32C;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "☂", "name": "umbrella", "shortname": ":umbrella:", "unicode": "2602", "html": "&#x2602;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "⛱️", "name": "umbrella on ground", "shortname": ":umbrella_on_ground:", "unicode": "26F1 FE0F", "html": "&#x26F1;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "❄", "name": "snowflake", "shortname": ":snowflake:", "unicode": "2744", "html": "&#x2744;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "☃", "name": "snowman", "shortname": ":snowman:", "unicode": "2603", "html": "&#x2603;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "☄️", "name": "comet", "shortname": ":comet:", "unicode": "2604 FE0F", "html": "&#x2604;&#xFE0F;", "category": "Travel & Places (sky & weather)", "order": ""},
+ {"emoji": "🧨", "name": "firecracker", "shortname": ":firecracker:", "unicode": "1F9E8", "html": "&#x1F9E8;", "category": "Activities (event)", "order": ""},
+ {"emoji": "🧧", "name": "red envelope", "shortname": ":red_envelope:", "unicode": "1F9E7", "html": "&#x1F9E7;", "category": "Activities (event)", "order": ""},
+ {"emoji": "🎗️", "name": "reminder ribbon", "shortname": ":reminder_ribbon:", "unicode": "1F397 FE0F", "html": "&#x1F397;&#xFE0F;", "category": "Activities (event)", "order": ""},
+ {"emoji": "🎟️", "name": "admission tickets", "shortname": ":admission_tickets:", "unicode": "1F39F FE0F", "html": "&#x1F39F;&#xFE0F;", "category": "Activities (event)", "order": ""},
+ {"emoji": "🎖️", "name": "military medal", "shortname": ":military_medal:", "unicode": "1F396 FE0F", "html": "&#x1F396;&#xFE0F;", "category": "Activities (award-medal)", "order": ""},
+ {"emoji": "🥇", "name": "1st place medal", "shortname": ":1st_place_medal:", "unicode": "1F947", "html": "&#x1F947;", "category": "Activities (award-medal)", "order": ""},
+ {"emoji": "🥈", "name": "2nd place medal", "shortname": ":2nd_place_medal:", "unicode": "1F948", "html": "&#x1F948;", "category": "Activities (award-medal)", "order": ""},
+ {"emoji": "🥉", "name": "3rd place medal", "shortname": ":3rd_place_medal:", "unicode": "1F949", "html": "&#x1F949;", "category": "Activities (award-medal)", "order": ""},
+ {"emoji": "⚾", "name": "baseball", "shortname": ":baseball:", "unicode": "26BE", "html": "&#x26BE;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🥎", "name": "softball", "shortname": ":softball:", "unicode": "1F94E", "html": "&#x1F94E;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🥏", "name": "flying disc", "shortname": ":flying_disc:", "unicode": "1F94F", "html": "&#x1F94F;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🥍", "name": "lacrosse", "shortname": ":lacrosse:", "unicode": "1F94D", "html": "&#x1F94D;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🥊", "name": "boxing glove", "shortname": ":boxing_glove:", "unicode": "1F94A", "html": "&#x1F94A;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🥋", "name": "martial arts uniform", "shortname": ":martial_arts_uniform:", "unicode": "1F94B", "html": "&#x1F94B;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🥅", "name": "goal net", "shortname": ":goal_net:", "unicode": "1F945", "html": "&#x1F945;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "⛸️", "name": "ice skate", "shortname": ":ice_skate:", "unicode": "26F8 FE0F", "html": "&#x26F8;&#xFE0F;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🤿", "name": "diving mask", "shortname": ":diving_mask:", "unicode": "1F93F", "html": "&#x1F93F;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🛷", "name": "sled", "shortname": ":sled:", "unicode": "1F6F7", "html": "&#x1F6F7;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🥌", "name": "curling stone", "shortname": ":curling_stone:", "unicode": "1F94C", "html": "&#x1F94C;", "category": "Activities (sport)", "order": ""},
+ {"emoji": "🪀", "name": "yo-yo", "shortname": ":yoyo:", "unicode": "1FA80", "html": "&#x1FA80;", "category": "Activities (game)", "order": ""},
+ {"emoji": "🪁", "name": "kite", "shortname": ":kite:", "unicode": "1FA81", "html": "&#x1FA81;", "category": "Activities (game)", "order": ""},
+ {"emoji": "🧿", "name": "nazar amulet", "shortname": ":nazar_amulet:", "unicode": "1F9FF", "html": "&#x1F9FF;", "category": "Activities (game)", "order": ""},
+ {"emoji": "🕹️", "name": "joystick", "shortname": ":joystick:", "unicode": "1F579 FE0F", "html": "&#x1F579;&#xFE0F;", "category": "Activities (game)", "order": ""},
+ {"emoji": "🧩", "name": "puzzle piece", "shortname": ":puzzle_piece:", "unicode": "1F9E9", "html": "&#x1F9E9;", "category": "Activities (game)", "order": ""},
+ {"emoji": "🧸", "name": "teddy bear", "shortname": ":teddy_bear:", "unicode": "1F9F8", "html": "&#x1F9F8;", "category": "Activities (game)", "order": ""},
+ {"emoji": "♟️", "name": "chess pawn", "shortname": ":chess_pawn:", "unicode": "265F FE0F", "html": "&#x265F;&#xFE0F;", "category": "Activities (game)", "order": ""},
+ {"emoji": "♟", "name": "chess pawn", "shortname": ":chess_pawn:", "unicode": "265F", "html": "&#x265F;", "category": "Activities (game)", "order": ""},
+ {"emoji": "🖼️", "name": "framed picture", "shortname": ":framed_picture:", "unicode": "1F5BC FE0F", "html": "&#x1F5BC;&#xFE0F;", "category": "Activities (arts & crafts)", "order": ""},
+ {"emoji": "🧵", "name": "thread", "shortname": ":thread:", "unicode": "1F9F5", "html": "&#x1F9F5;", "category": "Activities (arts & crafts)", "order": ""},
+ {"emoji": "🧶", "name": "yarn", "shortname": ":yarn:", "unicode": "1F9F6", "html": "&#x1F9F6;", "category": "Activities (arts & crafts)", "order": ""},
+ {"emoji": "🕶️", "name": "sunglasses", "shortname": ":sunglasses:", "unicode": "1F576 FE0F", "html": "&#x1F576;&#xFE0F;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🥽", "name": "goggles", "shortname": ":goggles:", "unicode": "1F97D", "html": "&#x1F97D;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🥼", "name": "lab coat", "shortname": ":lab_coat:", "unicode": "1F97C", "html": "&#x1F97C;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🦺", "name": "safety vest", "shortname": ":safety_vest:", "unicode": "1F9BA", "html": "&#x1F9BA;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🧣", "name": "scarf", "shortname": ":scarf:", "unicode": "1F9E3", "html": "&#x1F9E3;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🧤", "name": "gloves", "shortname": ":gloves:", "unicode": "1F9E4", "html": "&#x1F9E4;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🧥", "name": "coat", "shortname": ":coat:", "unicode": "1F9E5", "html": "&#x1F9E5;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🧦", "name": "socks", "shortname": ":socks:", "unicode": "1F9E6", "html": "&#x1F9E6;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🥻", "name": "sari", "shortname": ":sari:", "unicode": "1F97B", "html": "&#x1F97B;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🩱", "name": "one-piece swimsuit", "shortname": ":onepiece_swimsuit:", "unicode": "1FA71", "html": "&#x1FA71;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🩲", "name": "briefs", "shortname": ":briefs:", "unicode": "1FA72", "html": "&#x1FA72;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🩳", "name": "shorts", "shortname": ":shorts:", "unicode": "1FA73", "html": "&#x1FA73;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🛍️", "name": "shopping bags", "shortname": ":shopping_bags:", "unicode": "1F6CD FE0F", "html": "&#x1F6CD;&#xFE0F;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🥾", "name": "hiking boot", "shortname": ":hiking_boot:", "unicode": "1F97E", "html": "&#x1F97E;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🥿", "name": "flat shoe", "shortname": ":flat_shoe:", "unicode": "1F97F", "html": "&#x1F97F;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🩰", "name": "ballet shoes", "shortname": ":ballet_shoes:", "unicode": "1FA70", "html": "&#x1FA70;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🧢", "name": "billed cap", "shortname": ":billed_cap:", "unicode": "1F9E2", "html": "&#x1F9E2;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "⛑️", "name": "rescue worker’s helmet", "shortname": ":rescue_worker’s_helmet:", "unicode": "26D1 FE0F", "html": "&#x26D1;&#xFE0F;", "category": "Objects (clothing)", "order": ""},
+ {"emoji": "🎙️", "name": "studio microphone", "shortname": ":studio_microphone:", "unicode": "1F399 FE0F", "html": "&#x1F399;&#xFE0F;", "category": "Objects (music)", "order": ""},
+ {"emoji": "🎚️", "name": "level slider", "shortname": ":level_slider:", "unicode": "1F39A FE0F", "html": "&#x1F39A;&#xFE0F;", "category": "Objects (music)", "order": ""},
+ {"emoji": "🎛️", "name": "control knobs", "shortname": ":control_knobs:", "unicode": "1F39B FE0F", "html": "&#x1F39B;&#xFE0F;", "category": "Objects (music)", "order": ""},
+ {"emoji": "🪕", "name": "banjo", "shortname": ":banjo:", "unicode": "1FA95", "html": "&#x1FA95;", "category": "Objects (musical-instrument)", "order": ""},
+ {"emoji": "☎", "name": "telephone", "shortname": ":telephone:", "unicode": "260E", "html": "&#x260E;", "category": "Objects (phone)", "order": ""},
+ {"emoji": "🖥️", "name": "desktop computer", "shortname": ":desktop_computer:", "unicode": "1F5A5 FE0F", "html": "&#x1F5A5;&#xFE0F;", "category": "Objects (computer)", "order": ""},
+ {"emoji": "🖨️", "name": "printer", "shortname": ":printer:", "unicode": "1F5A8 FE0F", "html": "&#x1F5A8;&#xFE0F;", "category": "Objects (computer)", "order": ""},
+ {"emoji": "⌨️", "name": "keyboard", "shortname": ":keyboard:", "unicode": "2328 FE0F", "html": "&#x2328;&#xFE0F;", "category": "Objects (computer)", "order": ""},
+ {"emoji": "🖱️", "name": "computer mouse", "shortname": ":computer_mouse:", "unicode": "1F5B1 FE0F", "html": "&#x1F5B1;&#xFE0F;", "category": "Objects (computer)", "order": ""},
+ {"emoji": "🖲️", "name": "trackball", "shortname": ":trackball:", "unicode": "1F5B2 FE0F", "html": "&#x1F5B2;&#xFE0F;", "category": "Objects (computer)", "order": ""},
+ {"emoji": "🧮", "name": "abacus", "shortname": ":abacus:", "unicode": "1F9EE", "html": "&#x1F9EE;", "category": "Objects (computer)", "order": ""},
+ {"emoji": "🎞️", "name": "film frames", "shortname": ":film_frames:", "unicode": "1F39E FE0F", "html": "&#x1F39E;&#xFE0F;", "category": "Objects (light & video)", "order": ""},
+ {"emoji": "📽️", "name": "film projector", "shortname": ":film_projector:", "unicode": "1F4FD FE0F", "html": "&#x1F4FD;&#xFE0F;", "category": "Objects (light & video)", "order": ""},
+ {"emoji": "🕯️", "name": "candle", "shortname": ":candle:", "unicode": "1F56F FE0F", "html": "&#x1F56F;&#xFE0F;", "category": "Objects (light & video)", "order": ""},
+ {"emoji": "🪔", "name": "diya lamp", "shortname": ":diya_lamp:", "unicode": "1FA94", "html": "&#x1FA94;", "category": "Objects (light & video)", "order": ""},
+ {"emoji": "🗞️", "name": "rolled-up newspaper", "shortname": ":rolledup_newspaper:", "unicode": "1F5DE FE0F", "html": "&#x1F5DE;&#xFE0F;", "category": "Objects (book-paper)", "order": ""},
+ {"emoji": "🏷️", "name": "label", "shortname": ":label:", "unicode": "1F3F7 FE0F", "html": "&#x1F3F7;&#xFE0F;", "category": "Objects (book-paper)", "order": ""},
+ {"emoji": "🧾", "name": "receipt", "shortname": ":receipt:", "unicode": "1F9FE", "html": "&#x1F9FE;", "category": "Objects (money)", "order": ""},
+ {"emoji": "✉", "name": "envelope", "shortname": ":envelope:", "unicode": "2709", "html": "&#x2709;", "category": "Objects (mail)", "order": ""},
+ {"emoji": "🗳️", "name": "ballot box with ballot", "shortname": ":ballot_box_with_ballot:", "unicode": "1F5F3 FE0F", "html": "&#x1F5F3;&#xFE0F;", "category": "Objects (mail)", "order": ""},
+ {"emoji": "✏️", "name": "pencil", "shortname": ":pencil:", "unicode": "270F FE0F", "html": "&#x270F;&#xFE0F;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "✒️", "name": "black nib", "shortname": ":black_nib:", "unicode": "2712 FE0F", "html": "&#x2712;&#xFE0F;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "🖋️", "name": "fountain pen", "shortname": ":fountain_pen:", "unicode": "1F58B FE0F", "html": "&#x1F58B;&#xFE0F;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "🖊️", "name": "pen", "shortname": ":pen:", "unicode": "1F58A FE0F", "html": "&#x1F58A;&#xFE0F;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "🖌️", "name": "paintbrush", "shortname": ":paintbrush:", "unicode": "1F58C FE0F", "html": "&#x1F58C;&#xFE0F;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "🖍️", "name": "crayon", "shortname": ":crayon:", "unicode": "1F58D FE0F", "html": "&#x1F58D;&#xFE0F;", "category": "Objects (writing)", "order": ""},
+ {"emoji": "🗂️", "name": "card index dividers", "shortname": ":card_index_dividers:", "unicode": "1F5C2 FE0F", "html": "&#x1F5C2;&#xFE0F;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🗒️", "name": "spiral notepad", "shortname": ":spiral_notepad:", "unicode": "1F5D2 FE0F", "html": "&#x1F5D2;&#xFE0F;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🗓️", "name": "spiral calendar", "shortname": ":spiral_calendar:", "unicode": "1F5D3 FE0F", "html": "&#x1F5D3;&#xFE0F;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🖇️", "name": "linked paperclips", "shortname": ":linked_paperclips:", "unicode": "1F587 FE0F", "html": "&#x1F587;&#xFE0F;", "category": "Objects (office)", "order": ""},
+ {"emoji": "✂", "name": "scissors", "shortname": ":scissors:", "unicode": "2702", "html": "&#x2702;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🗃️", "name": "card file box", "shortname": ":card_file_box:", "unicode": "1F5C3 FE0F", "html": "&#x1F5C3;&#xFE0F;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🗄️", "name": "file cabinet", "shortname": ":file_cabinet:", "unicode": "1F5C4 FE0F", "html": "&#x1F5C4;&#xFE0F;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🗑️", "name": "wastebasket", "shortname": ":wastebasket:", "unicode": "1F5D1 FE0F", "html": "&#x1F5D1;&#xFE0F;", "category": "Objects (office)", "order": ""},
+ {"emoji": "🗝️", "name": "old key", "shortname": ":old_key:", "unicode": "1F5DD FE0F", "html": "&#x1F5DD;&#xFE0F;", "category": "Objects (lock)", "order": ""},
+ {"emoji": "🪓", "name": "axe", "shortname": ":axe:", "unicode": "1FA93", "html": "&#x1FA93;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "⛏️", "name": "pick", "shortname": ":pick:", "unicode": "26CF FE0F", "html": "&#x26CF;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "⚒️", "name": "hammer and pick", "shortname": ":hammer_and_pick:", "unicode": "2692 FE0F", "html": "&#x2692;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "🛠️", "name": "hammer and wrench", "shortname": ":hammer_and_wrench:", "unicode": "1F6E0 FE0F", "html": "&#x1F6E0;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "🗡️", "name": "dagger", "shortname": ":dagger:", "unicode": "1F5E1 FE0F", "html": "&#x1F5E1;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "⚔️", "name": "crossed swords", "shortname": ":crossed_swords:", "unicode": "2694 FE0F", "html": "&#x2694;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "🛡️", "name": "shield", "shortname": ":shield:", "unicode": "1F6E1 FE0F", "html": "&#x1F6E1;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "⚙️", "name": "gear", "shortname": ":gear:", "unicode": "2699 FE0F", "html": "&#x2699;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "🗜️", "name": "clamp", "shortname": ":clamp:", "unicode": "1F5DC FE0F", "html": "&#x1F5DC;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "⚖️", "name": "balance scale", "shortname": ":balance_scale:", "unicode": "2696 FE0F", "html": "&#x2696;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "🦯", "name": "probing cane", "shortname": ":probing_cane:", "unicode": "1F9AF", "html": "&#x1F9AF;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "⛓️", "name": "chains", "shortname": ":chains:", "unicode": "26D3 FE0F", "html": "&#x26D3;&#xFE0F;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "🧰", "name": "toolbox", "shortname": ":toolbox:", "unicode": "1F9F0", "html": "&#x1F9F0;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "🧲", "name": "magnet", "shortname": ":magnet:", "unicode": "1F9F2", "html": "&#x1F9F2;", "category": "Objects (tool)", "order": ""},
+ {"emoji": "⚗️", "name": "alembic", "shortname": ":alembic:", "unicode": "2697 FE0F", "html": "&#x2697;&#xFE0F;", "category": "Objects (science)", "order": ""},
+ {"emoji": "🧪", "name": "test tube", "shortname": ":test_tube:", "unicode": "1F9EA", "html": "&#x1F9EA;", "category": "Objects (science)", "order": ""},
+ {"emoji": "🧫", "name": "petri dish", "shortname": ":petri_dish:", "unicode": "1F9EB", "html": "&#x1F9EB;", "category": "Objects (science)", "order": ""},
+ {"emoji": "🧬", "name": "dna", "shortname": ":dna:", "unicode": "1F9EC", "html": "&#x1F9EC;", "category": "Objects (science)", "order": ""},
+ {"emoji": "🩸", "name": "drop of blood", "shortname": ":drop_of_blood:", "unicode": "1FA78", "html": "&#x1FA78;", "category": "Objects (medical)", "order": ""},
+ {"emoji": "🩹", "name": "adhesive bandage", "shortname": ":adhesive_bandage:", "unicode": "1FA79", "html": "&#x1FA79;", "category": "Objects (medical)", "order": ""},
+ {"emoji": "🩺", "name": "stethoscope", "shortname": ":stethoscope:", "unicode": "1FA7A", "html": "&#x1FA7A;", "category": "Objects (medical)", "order": ""},
+ {"emoji": "🛏️", "name": "bed", "shortname": ":bed:", "unicode": "1F6CF FE0F", "html": "&#x1F6CF;&#xFE0F;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🛋️", "name": "couch and lamp", "shortname": ":couch_and_lamp:", "unicode": "1F6CB FE0F", "html": "&#x1F6CB;&#xFE0F;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🪑", "name": "chair", "shortname": ":chair:", "unicode": "1FA91", "html": "&#x1FA91;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🪒", "name": "razor", "shortname": ":razor:", "unicode": "1FA92", "html": "&#x1FA92;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🧴", "name": "lotion bottle", "shortname": ":lotion_bottle:", "unicode": "1F9F4", "html": "&#x1F9F4;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🧷", "name": "safety pin", "shortname": ":safety_pin:", "unicode": "1F9F7", "html": "&#x1F9F7;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🧹", "name": "broom", "shortname": ":broom:", "unicode": "1F9F9", "html": "&#x1F9F9;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🧺", "name": "basket", "shortname": ":basket:", "unicode": "1F9FA", "html": "&#x1F9FA;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🧻", "name": "roll of paper", "shortname": ":roll_of_paper:", "unicode": "1F9FB", "html": "&#x1F9FB;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🧼", "name": "soap", "shortname": ":soap:", "unicode": "1F9FC", "html": "&#x1F9FC;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🧽", "name": "sponge", "shortname": ":sponge:", "unicode": "1F9FD", "html": "&#x1F9FD;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🧯", "name": "fire extinguisher", "shortname": ":fire_extinguisher:", "unicode": "1F9EF", "html": "&#x1F9EF;", "category": "Objects (household)", "order": ""},
+ {"emoji": "🛒", "name": "shopping cart", "shortname": ":shopping_cart:", "unicode": "1F6D2", "html": "&#x1F6D2;", "category": "Objects (household)", "order": ""},
+ {"emoji": "⚰️", "name": "coffin", "shortname": ":coffin:", "unicode": "26B0 FE0F", "html": "&#x26B0;&#xFE0F;", "category": "Objects (other-object)", "order": ""},
+ {"emoji": "⚱️", "name": "funeral urn", "shortname": ":funeral_urn:", "unicode": "26B1 FE0F", "html": "&#x26B1;&#xFE0F;", "category": "Objects (other-object)", "order": ""},
+ {"emoji": "⚠", "name": "warning", "shortname": ":warning:", "unicode": "26A0", "html": "&#x26A0;", "category": "Symbols (warning)", "order": ""},
+ {"emoji": "☢️", "name": "radioactive", "shortname": ":radioactive:", "unicode": "2622 FE0F", "html": "&#x2622;&#xFE0F;", "category": "Symbols (warning)", "order": ""},
+ {"emoji": "☣️", "name": "biohazard", "shortname": ":biohazard:", "unicode": "2623 FE0F", "html": "&#x2623;&#xFE0F;", "category": "Symbols (warning)", "order": ""},
+ {"emoji": "⬆", "name": "up arrow", "shortname": ":up_arrow:", "unicode": "2B06", "html": "&#x2B06;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "↗", "name": "up-right arrow", "shortname": ":upright_arrow:", "unicode": "2197", "html": "&#x2197;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "➡️", "name": "right arrow", "shortname": ":right_arrow:", "unicode": "27A1 FE0F", "html": "&#x27A1;&#xFE0F;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "↘", "name": "down-right arrow", "shortname": ":downright_arrow:", "unicode": "2198", "html": "&#x2198;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "⬇", "name": "down arrow", "shortname": ":down_arrow:", "unicode": "2B07", "html": "&#x2B07;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "↙", "name": "down-left arrow", "shortname": ":downleft_arrow:", "unicode": "2199", "html": "&#x2199;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "⬅", "name": "left arrow", "shortname": ":left_arrow:", "unicode": "2B05", "html": "&#x2B05;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "↖️", "name": "up-left arrow", "shortname": ":upleft_arrow:", "unicode": "2196 FE0F", "html": "&#x2196;&#xFE0F;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "↕️", "name": "up-down arrow", "shortname": ":updown_arrow:", "unicode": "2195 FE0F", "html": "&#x2195;&#xFE0F;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "↩", "name": "right arrow curving left", "shortname": ":right_arrow_curving_left:", "unicode": "21A9", "html": "&#x21A9;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "↪", "name": "left arrow curving right", "shortname": ":left_arrow_curving_right:", "unicode": "21AA", "html": "&#x21AA;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "⤴", "name": "right arrow curving up", "shortname": ":right_arrow_curving_up:", "unicode": "2934", "html": "&#x2934;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "⤵", "name": "right arrow curving down", "shortname": ":right_arrow_curving_down:", "unicode": "2935", "html": "&#x2935;", "category": "Symbols (arrow)", "order": ""},
+ {"emoji": "⚛️", "name": "atom symbol", "shortname": ":atom_symbol:", "unicode": "269B FE0F", "html": "&#x269B;&#xFE0F;", "category": "Symbols (religion)", "order": ""},
+ {"emoji": "🕉️", "name": "om", "shortname": ":om:", "unicode": "1F549 FE0F", "html": "&#x1F549;&#xFE0F;", "category": "Symbols (religion)", "order": ""},
+ {"emoji": "✡", "name": "star of David", "shortname": ":star_of_David:", "unicode": "2721", "html": "&#x2721;", "category": "Symbols (religion)", "order": ""},
+ {"emoji": "☸️", "name": "wheel of dharma", "shortname": ":wheel_of_dharma:", "unicode": "2638 FE0F", "html": "&#x2638;&#xFE0F;", "category": "Symbols (religion)", "order": ""},
+ {"emoji": "☯️", "name": "yin yang", "shortname": ":yin_yang:", "unicode": "262F FE0F", "html": "&#x262F;&#xFE0F;", "category": "Symbols (religion)", "order": ""},
+ {"emoji": "✝", "name": "latin cross", "shortname": ":latin_cross:", "unicode": "271D", "html": "&#x271D;", "category": "Symbols (religion)", "order": ""},
+ {"emoji": "☦️", "name": "orthodox cross", "shortname": ":orthodox_cross:", "unicode": "2626 FE0F", "html": "&#x2626;&#xFE0F;", "category": "Symbols (religion)", "order": ""},
+ {"emoji": "☪️", "name": "star and crescent", "shortname": ":star_and_crescent:", "unicode": "262A FE0F", "html": "&#x262A;&#xFE0F;", "category": "Symbols (religion)", "order": ""},
+ {"emoji": "☮️", "name": "peace symbol", "shortname": ":peace_symbol:", "unicode": "262E FE0F", "html": "&#x262E;&#xFE0F;", "category": "Symbols (religion)", "order": ""},
+ {"emoji": "▶", "name": "play button", "shortname": ":play_button:", "unicode": "25B6", "html": "&#x25B6;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏭️", "name": "next track button", "shortname": ":next_track_button:", "unicode": "23ED FE0F", "html": "&#x23ED;&#xFE0F;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏯️", "name": "play or pause button", "shortname": ":play_or_pause_button:", "unicode": "23EF FE0F", "html": "&#x23EF;&#xFE0F;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "◀", "name": "reverse button", "shortname": ":reverse_button:", "unicode": "25C0", "html": "&#x25C0;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏮️", "name": "last track button", "shortname": ":last_track_button:", "unicode": "23EE FE0F", "html": "&#x23EE;&#xFE0F;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏸️", "name": "pause button", "shortname": ":pause_button:", "unicode": "23F8 FE0F", "html": "&#x23F8;&#xFE0F;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏹️", "name": "stop button", "shortname": ":stop_button:", "unicode": "23F9 FE0F", "html": "&#x23F9;&#xFE0F;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏺️", "name": "record button", "shortname": ":record_button:", "unicode": "23FA FE0F", "html": "&#x23FA;&#xFE0F;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏏️", "name": "eject button", "shortname": ":eject_button:", "unicode": "23CF FE0F", "html": "&#x23CF;&#xFE0F;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "⏏", "name": "eject button", "shortname": ":eject_button:", "unicode": "23CF", "html": "&#x23CF;", "category": "Symbols (av-symbol)", "order": ""},
+ {"emoji": "♀️", "name": "female sign", "shortname": ":female_sign:", "unicode": "2640 FE0F", "html": "&#x2640;&#xFE0F;", "category": "Symbols (gender)", "order": ""},
+ {"emoji": "♀", "name": "female sign", "shortname": ":female_sign:", "unicode": "2640", "html": "&#x2640;", "category": "Symbols (gender)", "order": ""},
+ {"emoji": "♂️", "name": "male sign", "shortname": ":male_sign:", "unicode": "2642 FE0F", "html": "&#x2642;&#xFE0F;", "category": "Symbols (gender)", "order": ""},
+ {"emoji": "♂", "name": "male sign", "shortname": ":male_sign:", "unicode": "2642", "html": "&#x2642;", "category": "Symbols (gender)", "order": ""},
+ {"emoji": "⚕️", "name": "medical symbol", "shortname": ":medical_symbol:", "unicode": "2695 FE0F", "html": "&#x2695;&#xFE0F;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "⚕", "name": "medical symbol", "shortname": ":medical_symbol:", "unicode": "2695", "html": "&#x2695;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "♾️", "name": "infinity", "shortname": ":infinity:", "unicode": "267E FE0F", "html": "&#x267E;&#xFE0F;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "♾", "name": "infinity", "shortname": ":infinity:", "unicode": "267E", "html": "&#x267E;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "♻️", "name": "recycling symbol", "shortname": ":recycling_symbol:", "unicode": "267B FE0F", "html": "&#x267B;&#xFE0F;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "⚜️", "name": "fleur-de-lis", "shortname": ":fleurde-lis:", "unicode": "269C FE0F", "html": "&#x269C;&#xFE0F;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "☑", "name": "check box with check", "shortname": ":check_box_with_check:", "unicode": "2611", "html": "&#x2611;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "✔", "name": "check mark", "shortname": ":check_mark:", "unicode": "2714", "html": "&#x2714;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "✖", "name": "multiplication sign", "shortname": ":multiplication_sign:", "unicode": "2716", "html": "&#x2716;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "〽", "name": "part alternation mark", "shortname": ":part_alternation_mark:", "unicode": "303D", "html": "&#x303D;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "✳", "name": "eight-spoked asterisk", "shortname": ":eightspoked_asterisk:", "unicode": "2733", "html": "&#x2733;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "✴", "name": "eight-pointed star", "shortname": ":eightpointed_star:", "unicode": "2734", "html": "&#x2734;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "❇", "name": "sparkle", "shortname": ":sparkle:", "unicode": "2747", "html": "&#x2747;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "‼", "name": "double exclamation mark", "shortname": ":double_exclamation_mark:", "unicode": "203C", "html": "&#x203C;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "⁉", "name": "exclamation question mark", "shortname": ":exclamation_question_mark:", "unicode": "2049", "html": "&#x2049;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "〰️", "name": "wavy dash", "shortname": ":wavy_dash:", "unicode": "3030 FE0F", "html": "&#x3030;&#xFE0F;", "category": "Symbols (other-symbol)", "order": ""},
+ {"emoji": "#️⃣", "name": "keycap: #", "shortname": ":#:", "unicode": "0023 FE0F 20E3", "html": "&#x0023;&#xFE0F;&#x20E3;", "category": "Symbols (keycap)", "order": ""},
+ {"emoji": "🅰", "name": "A button (blood type)", "shortname": ":A_button_blood_type:", "unicode": "1F170", "html": "&#x1F170;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "🅱", "name": "B button (blood type)", "shortname": ":B_button_blood_type:", "unicode": "1F171", "html": "&#x1F171;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "ℹ️", "name": "information", "shortname": ":information:", "unicode": "2139 FE0F", "html": "&#x2139;&#xFE0F;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "Ⓜ", "name": "circled M", "shortname": ":circled_M:", "unicode": "24C2", "html": "&#x24C2;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "🅾", "name": "O button (blood type)", "shortname": ":O_button_blood_type:", "unicode": "1F17E", "html": "&#x1F17E;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "🅿", "name": "P button", "shortname": ":P_button:", "unicode": "1F17F", "html": "&#x1F17F;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "🈂", "name": "Japanese service charge button", "shortname": ":Japanese_service_charge_button:", "unicode": "1F202", "html": "&#x1F202;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "🈷️", "name": "Japanese monthly amount button", "shortname": ":Japanese_monthly_amount_button:", "unicode": "1F237 FE0F", "html": "&#x1F237;&#xFE0F;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "🈯", "name": "Japanese reserved button", "shortname": ":Japanese_reserved_button:", "unicode": "1F22F", "html": "&#x1F22F;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "🈚", "name": "Japanese free of charge button", "shortname": ":Japanese_free_of_charge_button:", "unicode": "1F21A", "html": "&#x1F21A;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "㊗️", "name": "Japanese congratulations button", "shortname": ":Japanese_congratulations_button:", "unicode": "3297 FE0F", "html": "&#x3297;&#xFE0F;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "㊙️", "name": "Japanese secret button", "shortname": ":Japanese_secret_button:", "unicode": "3299 FE0F", "html": "&#x3299;&#xFE0F;", "category": "Symbols (alphanum)", "order": ""},
+ {"emoji": "🟠", "name": "orange circle", "shortname": ":orange_circle:", "unicode": "1F7E0", "html": "&#x1F7E0;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🟡", "name": "yellow circle", "shortname": ":yellow_circle:", "unicode": "1F7E1", "html": "&#x1F7E1;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🟢", "name": "green circle", "shortname": ":green_circle:", "unicode": "1F7E2", "html": "&#x1F7E2;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🟣", "name": "purple circle", "shortname": ":purple_circle:", "unicode": "1F7E3", "html": "&#x1F7E3;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🟤", "name": "brown circle", "shortname": ":brown_circle:", "unicode": "1F7E4", "html": "&#x1F7E4;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🟥", "name": "red square", "shortname": ":red_square:", "unicode": "1F7E5", "html": "&#x1F7E5;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🟧", "name": "orange square", "shortname": ":orange_square:", "unicode": "1F7E7", "html": "&#x1F7E7;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🟨", "name": "yellow square", "shortname": ":yellow_square:", "unicode": "1F7E8", "html": "&#x1F7E8;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🟩", "name": "green square", "shortname": ":green_square:", "unicode": "1F7E9", "html": "&#x1F7E9;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🟦", "name": "blue square", "shortname": ":blue_square:", "unicode": "1F7E6", "html": "&#x1F7E6;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "▪", "name": "black small square", "shortname": ":black_small_square:", "unicode": "25AA", "html": "&#x25AA;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "▫️", "name": "white small square", "shortname": ":white_small_square:", "unicode": "25AB FE0F", "html": "&#x25AB;&#xFE0F;", "category": "Symbols (geometric)", "order": ""},
+ {"emoji": "🏳️", "name": "white flag", "shortname": ":white_flag:", "unicode": "1F3F3 FE0F", "html": "&#x1F3F3;&#xFE0F;", "category": "Flags (flag)", "order": ""},
+ {"emoji": "🏳️‍🌈", "name": "rainbow flag", "shortname": ":rainbow_flag:", "unicode": "1F3F3 FE0F 200D 1F308", "html": "&#x1F3F3;&#xFE0F;&#x200D;&#x1F308;", "category": "Flags (flag)", "order": ""},
+ {"emoji": "🏴‍☠️", "name": "pirate flag", "shortname": ":pirate_flag:", "unicode": "1F3F4 200D 2620 FE0F", "html": "&#x1F3F4;&#x200D;&#x2620;&#xFE0F;", "category": "Flags (flag)", "order": ""},
+ {"emoji": "🏴‍☠", "name": "pirate flag", "shortname": ":pirate_flag:", "unicode": "1F3F4 200D 2620", "html": "&#x1F3F4;&#x200D;&#x2620;", "category": "Flags (flag)", "order": ""},
+ {"emoji": "🇺🇳", "name": "flag: United Nations", "shortname": ":United_Nations:", "unicode": "1F1FA 1F1F3", "html": "&#x1F1FA;&#x1F1F3;", "category": "Flags (country-flag)", "order": ""},
+ {"emoji": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", "name": "flag: England", "shortname": ":England:", "unicode": "1F3F4 E0067 E0062 E0065 E006E E0067 E007F", "html": "&#x1F3F4;&#xE0067;&#xE0062;&#xE0065;&#xE006E;&#xE0067;&#xE007F;", "category": "Flags (subdivision-flag)", "order": ""},
+ {"emoji": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "name": "flag: Scotland", "shortname": ":Scotland:", "unicode": "1F3F4 E0067 E0062 E0073 E0063 E0074 E007F", "html": "&#x1F3F4;&#xE0067;&#xE0062;&#xE0073;&#xE0063;&#xE0074;&#xE007F;", "category": "Flags (subdivision-flag)", "order": ""},
+ {"emoji": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", "name": "Wales", "shortname": ":wales:", "unicode": "1F3F4 E0067 E0062 E0077 E006C E0073 E007F", "html": "&#x1F3F4;&#xE0067;&#xE0062;&#xE0077;&#xE006C;&#xE0073;&#xE007F;", "category": "(subdivision-flag)", "order": ""}
+ ]
+} \ No newline at end of file
diff --git a/src/environ_vtab.cc b/src/environ_vtab.cc
new file mode 100644
index 0000000..1265f4c
--- /dev/null
+++ b/src/environ_vtab.cc
@@ -0,0 +1,338 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "environ_vtab.hh"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/auto_mem.hh"
+#include "base/lnav_log.hh"
+#include "config.h"
+
+extern char** environ;
+
+const char* const ENVIRON_CREATE_STMT = R"(
+-- Access lnav's environment variables through this table.
+CREATE TABLE environ (
+ name TEXT PRIMARY KEY,
+ value TEXT
+);
+)";
+
+struct env_vtab {
+ sqlite3_vtab base;
+ sqlite3* db;
+};
+
+struct env_vtab_cursor {
+ sqlite3_vtab_cursor base;
+ char** env_cursor;
+};
+
+static int vt_destructor(sqlite3_vtab* p_svt);
+
+static int
+vt_create(sqlite3* db,
+ void* pAux,
+ int argc,
+ const char* const* argv,
+ sqlite3_vtab** pp_vt,
+ char** pzErr)
+{
+ env_vtab* p_vt;
+
+ /* Allocate the sqlite3_vtab/vtab structure itself */
+ p_vt = (env_vtab*) sqlite3_malloc(sizeof(*p_vt));
+
+ if (p_vt == NULL) {
+ return SQLITE_NOMEM;
+ }
+
+ memset(&p_vt->base, 0, sizeof(sqlite3_vtab));
+ p_vt->db = db;
+
+ *pp_vt = &p_vt->base;
+
+ int rc = sqlite3_declare_vtab(db, ENVIRON_CREATE_STMT);
+
+ return rc;
+}
+
+static int
+vt_destructor(sqlite3_vtab* p_svt)
+{
+ env_vtab* p_vt = (env_vtab*) p_svt;
+
+ /* Free the SQLite structure */
+ sqlite3_free(p_vt);
+
+ return SQLITE_OK;
+}
+
+static int
+vt_connect(sqlite3* db,
+ void* p_aux,
+ int argc,
+ const char* const* argv,
+ sqlite3_vtab** pp_vt,
+ char** pzErr)
+{
+ return vt_create(db, p_aux, argc, argv, pp_vt, pzErr);
+}
+
+static int
+vt_disconnect(sqlite3_vtab* pVtab)
+{
+ return vt_destructor(pVtab);
+}
+
+static int
+vt_destroy(sqlite3_vtab* p_vt)
+{
+ return vt_destructor(p_vt);
+}
+
+static int vt_next(sqlite3_vtab_cursor* cur);
+
+static int
+vt_open(sqlite3_vtab* p_svt, sqlite3_vtab_cursor** pp_cursor)
+{
+ env_vtab* p_vt = (env_vtab*) p_svt;
+
+ p_vt->base.zErrMsg = NULL;
+
+ env_vtab_cursor* p_cur = (env_vtab_cursor*) new env_vtab_cursor();
+
+ if (p_cur == NULL) {
+ return SQLITE_NOMEM;
+ } else {
+ *pp_cursor = (sqlite3_vtab_cursor*) p_cur;
+
+ p_cur->base.pVtab = p_svt;
+ p_cur->env_cursor = environ;
+ }
+
+ return SQLITE_OK;
+}
+
+static int
+vt_close(sqlite3_vtab_cursor* cur)
+{
+ env_vtab_cursor* p_cur = (env_vtab_cursor*) cur;
+
+ /* Free cursor struct. */
+ delete p_cur;
+
+ return SQLITE_OK;
+}
+
+static int
+vt_eof(sqlite3_vtab_cursor* cur)
+{
+ env_vtab_cursor* vc = (env_vtab_cursor*) cur;
+
+ return vc->env_cursor[0] == NULL;
+}
+
+static int
+vt_next(sqlite3_vtab_cursor* cur)
+{
+ env_vtab_cursor* vc = (env_vtab_cursor*) cur;
+
+ if (vc->env_cursor[0] != NULL) {
+ vc->env_cursor += 1;
+ }
+
+ return SQLITE_OK;
+}
+
+static int
+vt_column(sqlite3_vtab_cursor* cur, sqlite3_context* ctx, int col)
+{
+ env_vtab_cursor* vc = (env_vtab_cursor*) cur;
+ const char* eq = strchr(vc->env_cursor[0], '=');
+
+ switch (col) {
+ case 0:
+ sqlite3_result_text(ctx,
+ vc->env_cursor[0],
+ eq - vc->env_cursor[0],
+ SQLITE_TRANSIENT);
+ break;
+ case 1:
+ sqlite3_result_text(ctx, eq + 1, -1, SQLITE_TRANSIENT);
+ break;
+ }
+
+ return SQLITE_OK;
+}
+
+static int
+vt_rowid(sqlite3_vtab_cursor* cur, sqlite_int64* p_rowid)
+{
+ env_vtab_cursor* p_cur = (env_vtab_cursor*) cur;
+
+ *p_rowid = (int64_t) p_cur->env_cursor[0];
+
+ return SQLITE_OK;
+}
+
+static int
+vt_best_index(sqlite3_vtab* tab, sqlite3_index_info* p_info)
+{
+ return SQLITE_OK;
+}
+
+static int
+vt_filter(sqlite3_vtab_cursor* p_vtc,
+ int idxNum,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv)
+{
+ return SQLITE_OK;
+}
+
+static int
+vt_update(sqlite3_vtab* tab,
+ int argc,
+ sqlite3_value** argv,
+ sqlite_int64* rowid)
+{
+ const char* name
+ = (argc > 2 ? (const char*) sqlite3_value_text(argv[2]) : nullptr);
+ env_vtab* p_vt = (env_vtab*) tab;
+ int retval = SQLITE_ERROR;
+
+ if (argc != 1
+ && (argc < 3 || sqlite3_value_type(argv[2]) == SQLITE_NULL
+ || sqlite3_value_type(argv[3]) == SQLITE_NULL
+ || sqlite3_value_text(argv[2])[0] == '\0'))
+ {
+ tab->zErrMsg = sqlite3_mprintf(
+ "A non-empty name and value must be provided when inserting an "
+ "environment variable");
+
+ return SQLITE_ERROR;
+ }
+ if (name != nullptr && strchr(name, '=') != nullptr) {
+ tab->zErrMsg = sqlite3_mprintf(
+ "Environment variable names cannot contain an equals sign (=)");
+
+ return SQLITE_ERROR;
+ }
+
+ if (sqlite3_value_type(argv[0]) != SQLITE_NULL) {
+ int64_t index = sqlite3_value_int64(argv[0]);
+ const char* var = (const char*) index;
+ const char* eq = strchr(var, '=');
+ size_t namelen = eq - var;
+ char name[namelen + 1];
+
+ memcpy(name, var, namelen);
+ name[namelen] = '\0';
+ unsetenv(name);
+
+ retval = SQLITE_OK;
+ } else if (name != nullptr && getenv(name) != nullptr) {
+#ifdef SQLITE_FAIL
+ int rc;
+
+ rc = sqlite3_vtab_on_conflict(p_vt->db);
+ switch (rc) {
+ case SQLITE_FAIL:
+ case SQLITE_ABORT:
+ tab->zErrMsg = sqlite3_mprintf(
+ "An environment variable with the name '%s' already exists",
+ name);
+ return rc;
+ case SQLITE_IGNORE:
+ return SQLITE_OK;
+ case SQLITE_REPLACE:
+ break;
+ default:
+ return rc;
+ }
+#endif
+ }
+
+ if (name != nullptr && argc == 4) {
+ const unsigned char* value = sqlite3_value_text(argv[3]);
+
+ setenv((const char*) name, (const char*) value, 1);
+
+ return SQLITE_OK;
+ }
+
+ return retval;
+}
+
+static sqlite3_module vtab_module = {
+ 0, /* iVersion */
+ vt_create, /* xCreate - create a vtable */
+ vt_connect, /* xConnect - associate a vtable with a connection */
+ vt_best_index, /* xBestIndex - best index */
+ vt_disconnect, /* xDisconnect - disassociate a vtable with a connection */
+ vt_destroy, /* xDestroy - destroy a vtable */
+ vt_open, /* xOpen - open a cursor */
+ vt_close, /* xClose - close a cursor */
+ vt_filter, /* xFilter - configure scan constraints */
+ vt_next, /* xNext - advance a cursor */
+ vt_eof, /* xEof - inidicate end of result set*/
+ vt_column, /* xColumn - read data */
+ vt_rowid, /* xRowid - read data */
+ vt_update, /* xUpdate - write data */
+ NULL, /* xBegin - begin transaction */
+ NULL, /* xSync - sync transaction */
+ NULL, /* xCommit - commit transaction */
+ NULL, /* xRollback - rollback transaction */
+ NULL, /* xFindFunction - function overloading */
+};
+
+int
+register_environ_vtab(sqlite3* db)
+{
+ auto_mem<char, sqlite3_free> errmsg;
+ int rc;
+
+ rc = sqlite3_create_module(db, "environ_vtab_impl", &vtab_module, NULL);
+ ensure(rc == SQLITE_OK);
+ if ((rc = sqlite3_exec(
+ db,
+ "CREATE VIRTUAL TABLE environ USING environ_vtab_impl()",
+ NULL,
+ NULL,
+ errmsg.out()))
+ != SQLITE_OK)
+ {
+ fprintf(stderr, "unable to create environ table %s\n", errmsg.in());
+ }
+ return rc;
+}
diff --git a/src/environ_vtab.hh b/src/environ_vtab.hh
new file mode 100644
index 0000000..0b307d3
--- /dev/null
+++ b/src/environ_vtab.hh
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef environ_vtab_hh
+#define environ_vtab_hh
+
+#include <sqlite3.h>
+
+int register_environ_vtab(sqlite3* db);
+
+extern const char* const ENVIRON_CREATE_STMT;
+
+#endif
diff --git a/src/extension-functions.cc b/src/extension-functions.cc
new file mode 100644
index 0000000..461a382
--- /dev/null
+++ b/src/extension-functions.cc
@@ -0,0 +1,2828 @@
+/*
+This library will provide common mathematical and string functions in
+SQL queries using the operating system libraries or provided
+definitions. It includes the following functions:
+
+Math: acos, asin, atan, atn2, atan2, acosh, asinh, atanh, difference,
+degrees, radians, cos, sin, tan, cot, cosh, sinh, tanh, coth, exp,
+log, log10, power, sign, sqrt, square, ceil, floor, pi.
+
+String: replicate, charindex, leftstr, rightstr, ltrim, rtrim, trim,
+replace, reverse, proper, padl, padr, padc, strfilter.
+
+Aggregate: stdev, variance, mode, median, lower_quartile,
+upper_quartile.
+
+The string functions ltrim, rtrim, trim, replace are included in
+recent versions of SQLite and so by default do not build.
+
+Compilation instructions:
+ Compile this C source file into a dynamic library as follows:
+ * Linux:
+ gcc -fPIC -lm -shared extension-functions.c -o libsqlitefunctions.so
+ * Mac OS X:
+ gcc -fno-common -dynamiclib extension-functions.c -o libsqlitefunctions.dylib
+ (You may need to add flags
+ -I /opt/local/include/ -L/opt/local/lib -lsqlite3
+ if your sqlite3 is installed from Mac ports, or
+ -I /sw/include/ -L/sw/lib -lsqlite3
+ if installed with Fink.)
+ * Windows:
+ 1. Install MinGW (http://www.mingw.org/) and you will get the gcc
+ (gnu compiler collection)
+ 2. add the path to your path variable (isn't done during the
+ installation!)
+ 3. compile:
+ gcc -shared -I "path" -o libsqlitefunctions.so extension-functions.c
+ (path = path of sqlite3ext.h; i.e. C:\programs\sqlite)
+
+Usage instructions for applications calling the sqlite3 API functions:
+ In your application, call sqlite3_enable_load_extension(db,1) to
+ allow loading external libraries. Then load the library libsqlitefunctions
+ using sqlite3_load_extension; the third argument should be 0.
+ See http://www.sqlite.org/cvstrac/wiki?p=LoadableExtensions.
+ Select statements may now use these functions, as in
+ SELECT cos(radians(inclination)) FROM satsum WHERE satnum = 25544;
+
+Usage instructions for the sqlite3 program:
+ If the program is built so that loading extensions is permitted,
+ the following will work:
+ sqlite> SELECT load_extension('./libsqlitefunctions.so');
+ sqlite> select cos(radians(45));
+ 0.707106781186548
+ Note: Loading extensions is by default prohibited as a
+ security measure; see "Security Considerations" in
+ http://www.sqlite.org/cvstrac/wiki?p=LoadableExtensions.
+ If the sqlite3 program and library are built this
+ way, you cannot use these functions from the program, you
+ must write your own program using the sqlite3 API, and call
+ sqlite3_enable_load_extension as described above, or else
+ rebuilt the sqlite3 program to allow loadable extensions.
+
+Alterations:
+The instructions are for Linux, Mac OS X, and Windows; users of other
+OSes may need to modify this procedure. In particular, if your math
+library lacks one or more of the needed trig or log functions, comment
+out the appropriate HAVE_ #define at the top of file. If you do not
+wish to make a loadable module, comment out the define for
+COMPILE_SQLITE_EXTENSIONS_AS_LOADABLE_MODULE. If you are using a
+version of SQLite without the trim functions and replace, comment out
+the HAVE_TRIM #define.
+
+Liam Healy
+
+History:
+2010-01-06 Correct check for argc in squareFunc, and add Windows
+compilation instructions.
+2009-06-24 Correct check for argc in properFunc.
+2008-09-14 Add check that memory was actually allocated after
+sqlite3_malloc or sqlite3StrDup, call sqlite3_result_error_nomem if
+not. Thanks to Robert Simpson.
+2008-06-13 Change to instructions to indicate use of the math library
+and that program might work.
+2007-10-01 Minor clarification to instructions.
+2007-09-29 Compilation as loadable module is optional with
+COMPILE_SQLITE_EXTENSIONS_AS_LOADABLE_MODULE.
+2007-09-28 Use sqlite3_extension_init and macros
+SQLITE_EXTENSION_INIT1, SQLITE_EXTENSION_INIT2, so that it works with
+sqlite3_load_extension. Thanks to Eric Higashino and Joe Wilson.
+New instructions for Mac compilation.
+2007-09-17 With help from Joe Wilson and Nuno Luca, made use of
+external interfaces so that compilation is no longer dependent on
+SQLite source code. Merged source, header, and README into a single
+file. Added casts so that Mac will compile without warnings (unsigned
+and signed char).
+2007-09-05 Included some definitions from sqlite 3.3.13 so that this
+will continue to work in newer versions of sqlite. Completed
+description of functions available.
+2007-03-27 Revised description.
+2007-03-23 Small cleanup and a bug fix on the code. This was mainly
+letting errno flag errors encountered in the math library and checking
+the result, rather than pre-checking. This fixes a bug in power that
+would cause an error if any non-positive number was raised to any
+power.
+2007-02-07 posted by Mikey C to sqlite mailing list.
+Original code 2006 June 05 by relicoder.
+
+*/
+
+//#include "config.h"
+
+// #define COMPILE_SQLITE_EXTENSIONS_AS_LOADABLE_MODULE 1
+#define HAVE_ACOSH 1
+#define HAVE_ASINH 1
+#define HAVE_ATANH 1
+#define HAVE_SINH 1
+#define HAVE_COSH 1
+#define HAVE_TANH 1
+#define HAVE_LOG10 1
+#define HAVE_ISBLANK 1
+#define SQLITE_SOUNDEX 1
+#define HAVE_TRIM 1 /* LMH 2007-03-25 if sqlite has trim functions */
+
+#define __STDC_FORMAT_MACROS
+
+#ifdef COMPILE_SQLITE_EXTENSIONS_AS_LOADABLE_MODULE
+# include "sqlite3ext.h"
+SQLITE_EXTENSION_INIT1
+#else
+# include "sqlite3.h"
+#endif
+
+#include <ctype.h>
+/* relicoder */
+#include <assert.h>
+#include <errno.h> /* LMH 2007-03-25 */
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#ifndef _MAP_H_
+# define _MAP_H_
+
+# include <inttypes.h>
+# include <stdint.h>
+
+# include "sqlite-extension-func.hh"
+
+/*
+** Simple binary tree implementation to use in median, mode and quartile
+*calculations
+** Tree is not necessarily balanced. That would require something like red&black
+*trees of AVL
+*/
+
+typedef int (*cmp_func)(const void*, const void*);
+typedef void (*map_iterator)(void*, int64_t, void*);
+
+typedef struct node {
+ struct node* l;
+ struct node* r;
+ void* data;
+ int64_t count;
+} node;
+
+typedef struct map {
+ node* base;
+ cmp_func cmp;
+ short free;
+} map;
+
+/*
+** creates a map given a comparison function
+*/
+map map_make(cmp_func cmp);
+
+/*
+** inserts the element e into map m
+*/
+void map_insert(map* m, void* e);
+
+/*
+** executes function iter over all elements in the map, in key increasing order
+*/
+void map_iterate(map* m, map_iterator iter, void* p);
+
+/*
+** frees all memory used by a map
+*/
+void map_destroy(map* m);
+
+/*
+** compares 2 integers
+** to use with map_make
+*/
+int int_cmp(const void* a, const void* b);
+
+/*
+** compares 2 doubles
+** to use with map_make
+*/
+int double_cmp(const void* a, const void* b);
+
+#endif /* _MAP_H_ */
+
+typedef uint8_t u8;
+typedef uint16_t u16;
+typedef int64_t i64;
+
+static char*
+sqlite3StrDup(const char* z)
+{
+ char* res = (char*) sqlite3_malloc(strlen(z) + 1);
+ return strcpy(res, z);
+}
+
+/*
+** These are copied verbatim from fun.c so as to not have the names exported
+*/
+
+/* LMH from sqlite3 3.3.13 */
+/*
+** This table maps from the first byte of a UTF-8 character to the number
+** of trailing bytes expected. A value '4' indicates that the table key
+** is not a legal first byte for a UTF-8 character.
+*/
+static const u8 xtra_utf8_bytes[256] = {
+ /* 0xxxxxxx */
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+
+ /* 10wwwwww */
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+
+ /* 110yyyyy */
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+ 1,
+
+ /* 1110zzzz */
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+ 2,
+
+ /* 11110yyy */
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 3,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+ 4,
+};
+
+/*
+** This table maps from the number of trailing bytes in a UTF-8 character
+** to an integer constant that is effectively calculated for each character
+** read by a naive implementation of a UTF-8 character reader. The code
+** in the READ_UTF8 macro explains things best.
+*/
+static const int xtra_utf8_bits[] = {
+ 0,
+ 12416, /* (0xC0 << 6) + (0x80) */
+ 925824, /* (0xE0 << 12) + (0x80 << 6) + (0x80) */
+ 63447168 /* (0xF0 << 18) + (0x80 << 12) + (0x80 << 6) + 0x80 */
+};
+
+/*
+** If a UTF-8 character contains N bytes extra bytes (N bytes follow
+** the initial byte so that the total character length is N+1) then
+** masking the character with utf8_mask[N] must produce a non-zero
+** result. Otherwise, we have an (illegal) overlong encoding.
+*/
+static const unsigned long utf_mask[] = {
+ 0x00000000,
+ 0xffffff80,
+ 0xfffff800,
+ 0xffff0000,
+};
+
+/* LMH salvaged from sqlite3 3.3.13 source code src/utf.c */
+#define READ_UTF8(zIn, c) \
+ { \
+ int xtra; \
+ c = *(zIn)++; \
+ xtra = xtra_utf8_bytes[c]; \
+ switch (xtra) { \
+ case 4: \
+ c = (int) 0xFFFD; \
+ break; \
+ case 3: \
+ c = (c << 6) + *(zIn)++; \
+ case 2: \
+ c = (c << 6) + *(zIn)++; \
+ case 1: \
+ c = (c << 6) + *(zIn)++; \
+ c -= xtra_utf8_bits[xtra]; \
+ if ((utf_mask[xtra] & c) == 0 || (c & 0xFFFFF800) == 0xD800 \
+ || (c & 0xFFFFFFFE) == 0xFFFE) \
+ { \
+ c = 0xFFFD; \
+ } \
+ } \
+ }
+
+static int
+sqlite3ReadUtf8(const unsigned char* z)
+{
+ int c;
+ READ_UTF8(z, c);
+ return c;
+}
+
+#define SKIP_UTF8(zIn) \
+ { \
+ zIn += (xtra_utf8_bytes[*(u8*) zIn] + 1); \
+ }
+
+/*
+** pZ is a UTF-8 encoded unicode string. If nByte is less than zero,
+** return the number of unicode characters in pZ up to (but not including)
+** the first 0x00 byte. If nByte is not less than zero, return the
+** number of unicode characters in the first nByte of pZ (or up to
+** the first 0x00, whichever comes first).
+*/
+static int
+sqlite3Utf8CharLen(const char* z, int nByte)
+{
+ int r = 0;
+ const char* zTerm;
+ if (nByte >= 0) {
+ zTerm = &z[nByte];
+ } else {
+ zTerm = (const char*) (-1);
+ }
+ assert(z <= zTerm);
+ while (*z != 0 && z < zTerm) {
+ SKIP_UTF8(z);
+ r++;
+ }
+ return r;
+}
+
+/*
+** X is a pointer to the first byte of a UTF-8 character. Increment
+** X so that it points to the next character. This only works right
+** if X points to a well-formed UTF-8 string.
+*/
+#define sqliteNextChar(X) \
+ while ((0xc0 & *++(X)) == 0x80) { \
+ }
+#define sqliteCharVal(X) sqlite3ReadUtf8(X)
+
+/*
+** This is a macro that facilitates writting wrappers for math.h functions
+** it creates code for a function to use in SQlite that gets one numeric input
+** and returns a floating point value.
+**
+** Could have been implemented using pointers to functions but this way it's
+*inline
+** and thus more efficient. Lower * ranking though...
+**
+** Parameters:
+** name: function name to de defined (eg: sinFunc)
+** function: function defined in math.h to wrap (eg: sin)
+** domain: boolean condition that CAN'T happen in terms of the input
+*parameter rVal
+** (eg: rval<0 for sqrt)
+*/
+/* LMH 2007-03-25 Changed to use errno and remove domain; no pre-checking for
+ * errors. */
+#define GEN_MATH_WRAP_DOUBLE_1(name, function) \
+ static void name(sqlite3_context* context, int argc, sqlite3_value** argv) \
+ { \
+ double rVal = 0.0, val; \
+ assert(argc == 1); \
+ switch (sqlite3_value_type(argv[0])) { \
+ case SQLITE_NULL: { \
+ sqlite3_result_null(context); \
+ break; \
+ } \
+ default: { \
+ rVal = sqlite3_value_double(argv[0]); \
+ errno = 0; \
+ val = function(rVal); \
+ if (errno == 0) { \
+ sqlite3_result_double(context, val); \
+ } else { \
+ sqlite3_result_error(context, strerror(errno), errno); \
+ } \
+ break; \
+ } \
+ } \
+ }
+
+/*
+** Example of GEN_MATH_WRAP_DOUBLE_1 usage
+** this creates function sqrtFunc to wrap the math.h standard function
+*sqrt(x)=x^0.5
+*/
+GEN_MATH_WRAP_DOUBLE_1(sqrtFunc, sqrt)
+
+/* trignometric functions */
+GEN_MATH_WRAP_DOUBLE_1(acosFunc, acos)
+GEN_MATH_WRAP_DOUBLE_1(asinFunc, asin)
+GEN_MATH_WRAP_DOUBLE_1(atanFunc, atan)
+
+/*
+** Many of systems don't have inverse hyperbolic trig functions so this will
+*emulate
+** them on those systems in terms of log and sqrt (formulas are too trivial to
+*demand
+** written proof here)
+*/
+
+#ifndef HAVE_ACOSH
+static double
+acosh(double x)
+{
+ return log(x + sqrt(x * x - 1.0));
+}
+#endif
+
+GEN_MATH_WRAP_DOUBLE_1(acoshFunc, acosh)
+
+#ifndef HAVE_ASINH
+static double
+asinh(double x)
+{
+ return log(x + sqrt(x * x + 1.0));
+}
+#endif
+
+GEN_MATH_WRAP_DOUBLE_1(asinhFunc, asinh)
+
+#ifndef HAVE_ATANH
+static double
+atanh(double x)
+{
+ return (1.0 / 2.0) * log((1 + x) / (1 - x));
+}
+#endif
+
+GEN_MATH_WRAP_DOUBLE_1(atanhFunc, atanh)
+
+/*
+** math.h doesn't require cot (cotangent) so it's defined here
+*/
+static double
+cot(double x)
+{
+ return 1.0 / tan(x);
+}
+
+GEN_MATH_WRAP_DOUBLE_1(sinFunc, sin)
+GEN_MATH_WRAP_DOUBLE_1(cosFunc, cos)
+GEN_MATH_WRAP_DOUBLE_1(tanFunc, tan)
+GEN_MATH_WRAP_DOUBLE_1(cotFunc, cot)
+
+static double
+coth(double x)
+{
+ return 1.0 / tanh(x);
+}
+
+/*
+** Many systems don't have hyperbolic trigonometric functions so this will
+*emulate
+** them on those systems directly from the definition in terms of exp
+*/
+#ifndef HAVE_SINH
+static double
+sinh(double x)
+{
+ return (exp(x) - exp(-x)) / 2.0;
+}
+#endif
+
+GEN_MATH_WRAP_DOUBLE_1(sinhFunc, sinh)
+
+#ifndef HAVE_COSH
+static double
+cosh(double x)
+{
+ return (exp(x) + exp(-x)) / 2.0;
+}
+#endif
+
+GEN_MATH_WRAP_DOUBLE_1(coshFunc, cosh)
+
+#ifndef HAVE_TANH
+static double
+tanh(double x)
+{
+ return sinh(x) / cosh(x);
+}
+#endif
+
+GEN_MATH_WRAP_DOUBLE_1(tanhFunc, tanh)
+
+GEN_MATH_WRAP_DOUBLE_1(cothFunc, coth)
+
+/*
+** Some systems lack log in base 10. This will emulate it
+*/
+
+#ifndef HAVE_LOG10
+static double
+log10(double x)
+{
+ static double l10 = -1.0;
+ if (l10 < 0.0) {
+ l10 = log(10.0);
+ }
+ return log(x) / l10;
+}
+#endif
+
+GEN_MATH_WRAP_DOUBLE_1(logFunc, log)
+GEN_MATH_WRAP_DOUBLE_1(log10Func, log10)
+GEN_MATH_WRAP_DOUBLE_1(expFunc, exp)
+
+/*
+** Fallback for systems where math.h doesn't define M_PI
+*/
+#undef M_PI
+#ifndef M_PI
+/*
+** static double PI = acos(-1.0);
+** #define M_PI (PI)
+*/
+# define M_PI 3.14159265358979323846
+#endif
+
+/* Convert Degrees into Radians */
+static double
+deg2rad(double x)
+{
+ return x * M_PI / 180.0;
+}
+
+/* Convert Radians into Degrees */
+static double
+rad2deg(double x)
+{
+ return 180.0 * x / M_PI;
+}
+
+GEN_MATH_WRAP_DOUBLE_1(rad2degFunc, rad2deg)
+GEN_MATH_WRAP_DOUBLE_1(deg2radFunc, deg2rad)
+
+/* constant function that returns the value of PI=3.1415... */
+static void
+piFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ sqlite3_result_double(context, M_PI);
+}
+
+/*
+** Implements the sqrt function, it has the peculiarity of returning an integer
+*when the
+** the argument is an integer.
+** Since SQLite isn't strongly typed (almost untyped actually) this is a bit
+*pedantic
+*/
+static void
+squareFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ i64 iVal = 0;
+ double rVal = 0.0;
+ assert(argc == 1);
+ switch (sqlite3_value_type(argv[0])) {
+ case SQLITE_INTEGER: {
+ iVal = sqlite3_value_int64(argv[0]);
+ sqlite3_result_int64(context, iVal * iVal);
+ break;
+ }
+ case SQLITE_NULL: {
+ sqlite3_result_null(context);
+ break;
+ }
+ default: {
+ rVal = sqlite3_value_double(argv[0]);
+ sqlite3_result_double(context, rVal * rVal);
+ break;
+ }
+ }
+}
+
+/*
+** Wraps the pow math.h function
+** When both the base and the exponent are integers the result should be integer
+** (see sqrt just before this). Here the result is always double
+*/
+/* LMH 2007-03-25 Changed to use errno; no pre-checking for errors. Also
+ removes but that was present in the pre-checking that called
+ sqlite3_result_error on
+ a non-positive first argument, which is not always an error. */
+static void
+powerFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ double r1 = 0.0;
+ double r2 = 0.0;
+ double val;
+
+ assert(argc == 2);
+
+ if (sqlite3_value_type(argv[0]) == SQLITE_NULL
+ || sqlite3_value_type(argv[1]) == SQLITE_NULL)
+ {
+ sqlite3_result_null(context);
+ } else {
+ r1 = sqlite3_value_double(argv[0]);
+ r2 = sqlite3_value_double(argv[1]);
+ errno = 0;
+ val = pow(r1, r2);
+ if (errno == 0) {
+ sqlite3_result_double(context, val);
+ } else {
+ sqlite3_result_error(context, strerror(errno), errno);
+ }
+ }
+}
+
+/*
+** atan2 wrapper
+*/
+static void
+atn2Func(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ double r1 = 0.0;
+ double r2 = 0.0;
+
+ assert(argc == 2);
+
+ if (sqlite3_value_type(argv[0]) == SQLITE_NULL
+ || sqlite3_value_type(argv[1]) == SQLITE_NULL)
+ {
+ sqlite3_result_null(context);
+ } else {
+ r1 = sqlite3_value_double(argv[0]);
+ r2 = sqlite3_value_double(argv[1]);
+ sqlite3_result_double(context, atan2(r1, r2));
+ }
+}
+
+/*
+** Implementation of the sign() function
+** return one of 3 possibilities +1,0 or -1 when the argument is respectively
+** positive, 0 or negative.
+** When the argument is NULL the result is also NULL (completly conventional)
+*/
+static void
+signFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ double rVal = 0.0;
+ i64 iVal = 0;
+ assert(argc == 1);
+ switch (sqlite3_value_type(argv[0])) {
+ case SQLITE_INTEGER: {
+ iVal = sqlite3_value_int64(argv[0]);
+ iVal = (iVal > 0) ? 1 : (iVal < 0) ? -1 : 0;
+ sqlite3_result_int64(context, iVal);
+ break;
+ }
+ case SQLITE_NULL: {
+ sqlite3_result_null(context);
+ break;
+ }
+ default: {
+ /* 2nd change below. Line for abs was: if( rVal<0 ) rVal = rVal *
+ * -1.0; */
+
+ rVal = sqlite3_value_double(argv[0]);
+ rVal = (rVal > 0) ? 1 : (rVal < 0) ? -1 : 0;
+ sqlite3_result_double(context, rVal);
+ break;
+ }
+ }
+}
+
+/*
+** smallest integer value not less than argument
+*/
+static void
+ceilFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ double rVal = 0.0;
+ assert(argc == 1);
+ switch (sqlite3_value_type(argv[0])) {
+ case SQLITE_INTEGER: {
+ i64 iVal = sqlite3_value_int64(argv[0]);
+ sqlite3_result_int64(context, iVal);
+ break;
+ }
+ case SQLITE_NULL: {
+ sqlite3_result_null(context);
+ break;
+ }
+ default: {
+ rVal = sqlite3_value_double(argv[0]);
+ sqlite3_result_int64(context, (i64) ceil(rVal));
+ break;
+ }
+ }
+}
+
+/*
+** largest integer value not greater than argument
+*/
+static void
+floorFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ double rVal = 0.0;
+ assert(argc == 1);
+ switch (sqlite3_value_type(argv[0])) {
+ case SQLITE_INTEGER: {
+ i64 iVal = sqlite3_value_int64(argv[0]);
+ sqlite3_result_int64(context, iVal);
+ break;
+ }
+ case SQLITE_NULL: {
+ sqlite3_result_null(context);
+ break;
+ }
+ default: {
+ rVal = sqlite3_value_double(argv[0]);
+ sqlite3_result_int64(context, (i64) floor(rVal));
+ break;
+ }
+ }
+}
+
+/*
+** Given a string (s) in the first argument and an integer (n) in the second
+*returns the
+** string that constains s contatenated n times
+*/
+static void
+replicateFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ static const char* EMPTY = "";
+ unsigned char* z; /* result string */
+ i64 iCount; /* times to repeat */
+ i64 nLen; /* length of the input string (no multibyte considerations) */
+ i64 nTLen; /* length of the result string (no multibyte considerations) */
+ i64 i = 0;
+
+ if (argc != 2 || SQLITE_NULL == sqlite3_value_type(argv[0]))
+ return;
+
+ iCount = sqlite3_value_int64(argv[1]);
+
+ if (iCount < 0) {
+ sqlite3_result_error(context, "domain error", -1);
+ return;
+ }
+
+ if (iCount == 0) {
+ sqlite3_result_text(context, EMPTY, 0, SQLITE_STATIC);
+ return;
+ }
+
+ nLen = sqlite3_value_bytes(argv[0]);
+ nTLen = nLen * iCount;
+ z = (unsigned char*) sqlite3_malloc(nTLen + 1);
+ if (!z) {
+ sqlite3_result_error_nomem(context);
+ if (z)
+ sqlite3_free(z);
+ return;
+ }
+ auto zo = sqlite3_value_text(argv[0]);
+
+ for (i = 0; i < iCount; ++i) {
+ strcpy((char*) (z + i * nLen), (char*) zo);
+ }
+
+ sqlite3_result_text(context, (char*) z, -1, sqlite3_free);
+}
+
+/*
+** Some systems (win32 among others) don't have an isblank function, this will
+*emulate it.
+** This function is not UFT-8 safe since it only analyses a byte character.
+*/
+#ifndef HAVE_ISBLANK
+int
+isblank(char c)
+{
+ return (' ' == c || '\t' == c);
+}
+#endif
+
+static void
+properFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ const unsigned char* z; /* input string */
+ unsigned char* zo; /* output string */
+ unsigned char* zt; /* iterator */
+ char r;
+ int c = 1;
+
+ assert(argc == 1);
+ if (SQLITE_NULL == sqlite3_value_type(argv[0])) {
+ sqlite3_result_null(context);
+ return;
+ }
+
+ z = sqlite3_value_text(argv[0]);
+ zo = (unsigned char*) sqlite3StrDup((char*) z);
+ if (!zo) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ zt = zo;
+
+ while ((r = *(z++)) != 0) {
+ if (isblank(r)) {
+ c = 1;
+ } else {
+ if (c == 1) {
+ r = toupper(r);
+ } else {
+ r = tolower(r);
+ }
+ c = 0;
+ }
+ *(zt++) = r;
+ }
+ *zt = '\0';
+
+ sqlite3_result_text(context, (char*) zo, -1, SQLITE_TRANSIENT);
+ sqlite3_free(zo);
+}
+
+/*
+** given an input string (s) and an integer (n) adds spaces at the begining of s
+** until it has a length of n characters.
+** When s has a length >=n it's a NOP
+** padl(NULL) = NULL
+*/
+static void
+padlFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ i64 ilen; /* length to pad to */
+ i64 zl; /* length of the input string (UTF-8 chars) */
+ int i = 0;
+ const char* zi; /* input string */
+ char* zo; /* output string */
+ char* zt;
+
+ assert(argc == 2);
+
+ if (sqlite3_value_type(argv[0]) == SQLITE_NULL) {
+ sqlite3_result_null(context);
+ } else {
+ zi = (char*) sqlite3_value_text(argv[0]);
+ ilen = sqlite3_value_int64(argv[1]);
+ /* check domain */
+ if (ilen < 0) {
+ sqlite3_result_error(context, "domain error", -1);
+ return;
+ }
+ zl = sqlite3Utf8CharLen(zi, -1);
+ if (zl >= ilen) {
+ /* string is longer than the requested pad length, return the same
+ * string (dup it) */
+ zo = sqlite3StrDup(zi);
+ if (!zo) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ sqlite3_result_text(context, zo, -1, SQLITE_TRANSIENT);
+ } else {
+ zo = (char*) sqlite3_malloc(strlen(zi) + ilen - zl + 1);
+ if (!zo) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ zt = zo;
+ for (i = 1; i + zl <= ilen; ++i) {
+ *(zt++) = ' ';
+ }
+ /* no need to take UTF-8 into consideration here */
+ strcpy(zt, zi);
+ }
+ sqlite3_result_text(context, zo, -1, SQLITE_TRANSIENT);
+ sqlite3_free(zo);
+ }
+}
+
+/*
+** given an input string (s) and an integer (n) appends spaces at the end of s
+** until it has a length of n characters.
+** When s has a length >=n it's a NOP
+** padl(NULL) = NULL
+*/
+static void
+padrFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ i64 ilen; /* length to pad to */
+ i64 zl; /* length of the input string (UTF-8 chars) */
+ i64 zll; /* length of the input string (bytes) */
+ int i = 0;
+ const char* zi; /* input string */
+ char* zo; /* output string */
+ char* zt;
+
+ assert(argc == 2);
+
+ if (sqlite3_value_type(argv[0]) == SQLITE_NULL) {
+ sqlite3_result_null(context);
+ } else {
+ zi = (char*) sqlite3_value_text(argv[0]);
+ ilen = sqlite3_value_int64(argv[1]);
+ /* check domain */
+ if (ilen < 0) {
+ sqlite3_result_error(context, "domain error", -1);
+ return;
+ }
+ zl = sqlite3Utf8CharLen(zi, -1);
+ if (zl >= ilen) {
+ /* string is longer than the requested pad length, return the same
+ * string (dup it) */
+ zo = sqlite3StrDup(zi);
+ if (!zo) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ sqlite3_result_text(context, zo, -1, SQLITE_TRANSIENT);
+ } else {
+ zll = strlen(zi);
+ zo = (char*) sqlite3_malloc(zll + ilen - zl + 1);
+ if (!zo) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ zt = strcpy(zo, zi) + zll;
+ for (i = 1; i + zl <= ilen; ++i) {
+ *(zt++) = ' ';
+ }
+ *zt = '\0';
+ }
+ sqlite3_result_text(context, zo, -1, SQLITE_TRANSIENT);
+ sqlite3_free(zo);
+ }
+}
+
+/*
+** given an input string (s) and an integer (n) appends spaces at the end of s
+** and adds spaces at the begining of s until it has a length of n characters.
+** Tries to add has many characters at the left as at the right.
+** When s has a length >=n it's a NOP
+** padl(NULL) = NULL
+*/
+static void
+padcFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ i64 ilen; /* length to pad to */
+ i64 zl; /* length of the input string (UTF-8 chars) */
+ i64 zll; /* length of the input string (bytes) */
+ int i = 0;
+ const char* zi; /* input string */
+ char* zo; /* output string */
+ char* zt;
+
+ assert(argc == 2);
+
+ if (sqlite3_value_type(argv[0]) == SQLITE_NULL) {
+ sqlite3_result_null(context);
+ } else {
+ zi = (char*) sqlite3_value_text(argv[0]);
+ ilen = sqlite3_value_int64(argv[1]);
+ /* check domain */
+ if (ilen < 0) {
+ sqlite3_result_error(context, "domain error", -1);
+ return;
+ }
+ zl = sqlite3Utf8CharLen(zi, -1);
+ if (zl >= ilen) {
+ /* string is longer than the requested pad length, return the same
+ * string (dup it) */
+ zo = sqlite3StrDup(zi);
+ if (!zo) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ sqlite3_result_text(context, zo, -1, SQLITE_TRANSIENT);
+ } else {
+ zll = strlen(zi);
+ zo = (char*) sqlite3_malloc(zll + ilen - zl + 1);
+ if (!zo) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ zt = zo;
+ for (i = 1; 2 * i + zl <= ilen; ++i) {
+ *(zt++) = ' ';
+ }
+ strcpy(zt, zi);
+ zt += zll;
+ for (; i + zl <= ilen; ++i) {
+ *(zt++) = ' ';
+ }
+ *zt = '\0';
+ }
+ sqlite3_result_text(context, zo, -1, SQLITE_TRANSIENT);
+ sqlite3_free(zo);
+ }
+}
+
+/*
+** given 2 string (s1,s2) returns the string s1 with the characters NOT in s2
+*removed
+** assumes strings are UTF-8 encoded
+*/
+static void
+strfilterFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ const char* zi1; /* first parameter string (searched string) */
+ const char* zi2; /* second parameter string (vcontains valid characters) */
+ const char* z1;
+ const char* z21;
+ const char* z22;
+ char* zo; /* output string */
+ char* zot;
+ int c1 = 0;
+ int c2 = 0;
+
+ assert(argc == 2);
+
+ if (sqlite3_value_type(argv[0]) == SQLITE_NULL
+ || sqlite3_value_type(argv[1]) == SQLITE_NULL)
+ {
+ sqlite3_result_null(context);
+ } else {
+ zi1 = (char*) sqlite3_value_text(argv[0]);
+ zi2 = (char*) sqlite3_value_text(argv[1]);
+ /*
+ ** maybe I could allocate less, but that would imply 2 passes, rather
+ *waste
+ ** (possibly) some memory
+ */
+ zo = (char*) sqlite3_malloc(strlen(zi1) + 1);
+ if (!zo) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ zot = zo;
+ z1 = zi1;
+ while ((c1 = sqliteCharVal((unsigned char*) z1)) != 0) {
+ z21 = zi2;
+ while ((c2 = sqliteCharVal((unsigned char*) z21)) != 0 && c2 != c1)
+ {
+ sqliteNextChar(z21);
+ }
+ if (c2 != 0) {
+ z22 = z21;
+ sqliteNextChar(z22);
+ strncpy(zot, z21, z22 - z21);
+ zot += z22 - z21;
+ }
+ sqliteNextChar(z1);
+ }
+ *zot = '\0';
+
+ sqlite3_result_text(context, zo, -1, SQLITE_TRANSIENT);
+ sqlite3_free(zo);
+ }
+}
+
+/*
+** Given a string z1, retutns the (0 based) index of it's first occurence
+** in z2 after the first s characters.
+** Returns -1 when there isn't a match.
+** updates p to point to the character where the match occured.
+** This is an auxiliary function.
+*/
+static int
+_substr(const char* z1, const char* z2, int s, const char** p)
+{
+ int c = 0;
+ int rVal = -1;
+ const char* zt1;
+ const char* zt2;
+ int c1, c2;
+
+ if ('\0' == *z1) {
+ return -1;
+ }
+
+ while ((sqliteCharVal((unsigned char*) z2) != 0) && (c++) < s) {
+ sqliteNextChar(z2);
+ }
+
+ c = 0;
+ while ((sqliteCharVal((unsigned char*) z2)) != 0) {
+ zt1 = z1;
+ zt2 = z2;
+
+ do {
+ c1 = sqliteCharVal((unsigned char*) zt1);
+ c2 = sqliteCharVal((unsigned char*) zt2);
+ if (c1 == 0) {
+ break;
+ }
+ if (c2 == 0) {
+ break;
+ }
+ sqliteNextChar(zt1);
+ sqliteNextChar(zt2);
+ } while (c1 == c2 && c1 != 0 && c2 != 0);
+
+ if (c1 == 0) {
+ rVal = c;
+ break;
+ }
+
+ sqliteNextChar(z2);
+ ++c;
+ }
+ if (p) {
+ *p = z2;
+ }
+ return rVal >= 0 ? rVal + s : rVal;
+}
+
+/*
+** given 2 input strings (s1,s2) and an integer (n) searches from the nth
+*character
+** for the string s1. Returns the position where the match occured.
+** Characters are counted from 1.
+** 0 is returned when no match occurs.
+*/
+
+static void
+charindexFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ const u8* z1; /* s1 string */
+ u8* z2; /* s2 string */
+ int s = 0;
+ int rVal = 0;
+
+ assert(argc == 3 || argc == 2);
+
+ if (SQLITE_NULL == sqlite3_value_type(argv[0])
+ || SQLITE_NULL == sqlite3_value_type(argv[1]))
+ {
+ sqlite3_result_null(context);
+ return;
+ }
+
+ z1 = sqlite3_value_text(argv[0]);
+ if (z1 == 0)
+ return;
+ z2 = (u8*) sqlite3_value_text(argv[1]);
+ if (argc == 3) {
+ s = sqlite3_value_int(argv[2]) - 1;
+ if (s < 0) {
+ s = 0;
+ }
+ } else {
+ s = 0;
+ }
+
+ rVal = _substr((char*) z1, (char*) z2, s, NULL);
+ sqlite3_result_int(context, rVal + 1);
+}
+
+/*
+** given a string (s) and an integer (n) returns the n leftmost (UTF-8)
+*characters
+** if the string has a length<=n or is NULL this function is NOP
+*/
+static void
+leftFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ int c = 0;
+ int cc = 0;
+ int l = 0;
+ const unsigned char* z; /* input string */
+ const unsigned char* zt;
+ unsigned char* rz; /* output string */
+
+ assert(argc == 2);
+
+ if (SQLITE_NULL == sqlite3_value_type(argv[0])
+ || SQLITE_NULL == sqlite3_value_type(argv[1]))
+ {
+ sqlite3_result_null(context);
+ return;
+ }
+
+ z = sqlite3_value_text(argv[0]);
+ l = sqlite3_value_int(argv[1]);
+ zt = z;
+
+ while (sqliteCharVal(zt) && c++ < l)
+ sqliteNextChar(zt);
+
+ cc = zt - z;
+
+ rz = (unsigned char*) sqlite3_malloc(zt - z + 1);
+ if (!rz) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ strncpy((char*) rz, (char*) z, zt - z);
+ *(rz + cc) = '\0';
+ sqlite3_result_text(context, (char*) rz, -1, SQLITE_TRANSIENT);
+ sqlite3_free(rz);
+}
+
+/*
+** given a string (s) and an integer (n) returns the n rightmost (UTF-8)
+*characters
+** if the string has a length<=n or is NULL this function is NOP
+*/
+static void
+rightFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ int l = 0;
+ int c = 0;
+ int cc = 0;
+ const char* z;
+ const char* zt;
+ const char* ze;
+ char* rz;
+
+ assert(argc == 2);
+
+ if (SQLITE_NULL == sqlite3_value_type(argv[0])
+ || SQLITE_NULL == sqlite3_value_type(argv[1]))
+ {
+ sqlite3_result_null(context);
+ return;
+ }
+
+ z = (char*) sqlite3_value_text(argv[0]);
+ l = sqlite3_value_int(argv[1]);
+ zt = z;
+
+ while (sqliteCharVal((unsigned char*) zt) != 0) {
+ sqliteNextChar(zt);
+ ++c;
+ }
+
+ ze = zt;
+ zt = z;
+
+ cc = c - l;
+ if (cc < 0)
+ cc = 0;
+
+ while (cc-- > 0) {
+ sqliteNextChar(zt);
+ }
+
+ rz = (char*) sqlite3_malloc(ze - zt + 1);
+ if (!rz) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ strcpy((char*) rz, (char*) (zt));
+ sqlite3_result_text(context, (char*) rz, -1, SQLITE_TRANSIENT);
+ sqlite3_free(rz);
+}
+
+#ifndef HAVE_TRIM
+/*
+** removes the whitespaces at the begining of a string.
+*/
+const char*
+ltrim(const char* s)
+{
+ while (*s == ' ')
+ ++s;
+ return s;
+}
+
+/*
+** removes the whitespaces at the end of a string.
+** !mutates the input string!
+*/
+void
+rtrim(char* s)
+{
+ char* ss = s + strlen(s) - 1;
+ while (ss >= s && *ss == ' ')
+ --ss;
+ *(ss + 1) = '\0';
+}
+
+/*
+** Removes the whitespace at the begining of a string
+*/
+static void
+ltrimFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ const char* z;
+
+ assert(argc == 1);
+
+ if (SQLITE_NULL == sqlite3_value_type(argv[0])) {
+ sqlite3_result_null(context);
+ return;
+ }
+ z = sqlite3_value_text(argv[0]);
+ sqlite3_result_text(context, ltrim(z), -1, SQLITE_TRANSIENT);
+}
+
+/*
+** Removes the whitespace at the end of a string
+*/
+static void
+rtrimFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ const char* z;
+ char* rz;
+ /* try not to change data in argv */
+
+ assert(argc == 1);
+
+ if (SQLITE_NULL == sqlite3_value_type(argv[0])) {
+ sqlite3_result_null(context);
+ return;
+ }
+ z = sqlite3_value_text(argv[0]);
+ rz = sqlite3StrDup(z);
+ rtrim(rz);
+ sqlite3_result_text(context, rz, -1, SQLITE_TRANSIENT);
+ sqlite3_free(rz);
+}
+
+/*
+** Removes the whitespace at the begining and end of a string
+*/
+static void
+trimFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ const char* z;
+ char* rz;
+ /* try not to change data in argv */
+
+ assert(argc == 1);
+
+ if (SQLITE_NULL == sqlite3_value_type(argv[0])) {
+ sqlite3_result_null(context);
+ return;
+ }
+ z = sqlite3_value_text(argv[0]);
+ rz = sqlite3StrDup(z);
+ rtrim(rz);
+ sqlite3_result_text(context, ltrim(rz), -1, SQLITE_TRANSIENT);
+ sqlite3_free(rz);
+}
+#endif
+
+/*
+** given a pointer to a string s1, the length of that string (l1), a new string
+*(s2)
+** and it's length (l2) appends s2 to s1.
+** All lengths in bytes.
+** This is just an auxiliary function
+*/
+// static void _append(char **s1, int l1, const char *s2, int l2){
+// *s1 = realloc(*s1, (l1+l2+1)*sizeof(char));
+// strncpy((*s1)+l1, s2, l2);
+// *(*(s1)+l1+l2) = '\0';
+// }
+
+#ifndef HAVE_TRIM
+
+/*
+** given strings s, s1 and s2 replaces occurrences of s1 in s by s2
+*/
+static void
+replaceFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ const char* z1; /* string s (first parameter) */
+ const char* z2; /* string s1 (second parameter) string to look for */
+ const char* z3; /* string s2 (third parameter) string to replace occurrences
+ of s1 with */
+ int lz1;
+ int lz2;
+ int lz3;
+ int lzo = 0;
+ char* zo = 0;
+ int ret = 0;
+ const char* zt1;
+ const char* zt2;
+
+ assert(3 == argc);
+
+ if (SQLITE_NULL == sqlite3_value_type(argv[0])) {
+ sqlite3_result_null(context);
+ return;
+ }
+
+ z1 = sqlite3_value_text(argv[0]);
+ z2 = sqlite3_value_text(argv[1]);
+ z3 = sqlite3_value_text(argv[2]);
+ /* handle possible null values */
+ if (0 == z2) {
+ z2 = "";
+ }
+ if (0 == z3) {
+ z3 = "";
+ }
+
+ lz1 = strlen(z1);
+ lz2 = strlen(z2);
+ lz3 = strlen(z3);
+
+# if 0
+ /* special case when z2 is empty (or null) nothing will be changed */
+ if( 0==lz2 ){
+ sqlite3_result_text(context, z1, -1, SQLITE_TRANSIENT);
+ return;
+ }
+# endif
+
+ zt1 = z1;
+ zt2 = z1;
+
+ while (1) {
+ ret = _substr(z2, zt1, 0, &zt2);
+
+ if (ret < 0)
+ break;
+
+ _append(&zo, lzo, zt1, zt2 - zt1);
+ lzo += zt2 - zt1;
+ _append(&zo, lzo, z3, lz3);
+ lzo += lz3;
+
+ zt1 = zt2 + lz2;
+ }
+ _append(&zo, lzo, zt1, lz1 - (zt1 - z1));
+ sqlite3_result_text(context, zo, -1, SQLITE_TRANSIENT);
+ sqlite3_free(zo);
+}
+#endif
+
+/*
+** given a string returns the same string but with the characters in reverse
+*order
+*/
+static void
+reverseFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ const char* z;
+ const char* zt;
+ char* rz;
+ char* rzt;
+ int l = 0;
+ int i = 0;
+
+ assert(1 == argc);
+
+ if (SQLITE_NULL == sqlite3_value_type(argv[0])) {
+ sqlite3_result_null(context);
+ return;
+ }
+ z = (char*) sqlite3_value_text(argv[0]);
+ l = strlen(z);
+ rz = (char*) sqlite3_malloc(l + 1);
+ if (!rz) {
+ sqlite3_result_error_nomem(context);
+ return;
+ }
+ rzt = rz + l;
+ *(rzt--) = '\0';
+
+ zt = z;
+ while (sqliteCharVal((unsigned char*) zt) != 0) {
+ z = zt;
+ sqliteNextChar(zt);
+ for (i = 1; zt - i >= z; ++i) {
+ *(rzt--) = *(zt - i);
+ }
+ }
+
+ sqlite3_result_text(context, rz, -1, SQLITE_TRANSIENT);
+ sqlite3_free(rz);
+}
+
+/*
+** An instance of the following structure holds the context of a
+** stdev() or variance() aggregate computation.
+** implementaion of
+*http://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Algorithm_II
+** less prone to rounding errors
+*/
+typedef struct StdevCtx StdevCtx;
+struct StdevCtx {
+ double rM;
+ double rS;
+ i64 cnt; /* number of elements */
+};
+
+/*
+** An instance of the following structure holds the context of a
+** mode() or median() aggregate computation.
+** Depends on structures defined in map.c (see map & map)
+** These aggregate functions only work for integers and floats although
+** they could be made to work for strings. This is usually considered
+*meaningless.
+** Only usuall order (for median), no use of collation functions (would this
+*even make sense?)
+*/
+typedef struct ModeCtx ModeCtx;
+struct ModeCtx {
+ i64 riM; /* integer value found so far */
+ double rdM; /* double value found so far */
+ i64 cnt; /* number of elements so far */
+ double pcnt; /* number of elements smaller than a percentile */
+ i64 mcnt; /* maximum number of occurrences (for mode) */
+ i64 mn; /* number of occurrences (for mode and percentiles) */
+ i64 is_double; /* whether the computation is being done for doubles (>0) or
+ integers (=0) */
+ map* m; /* map structure used for the computation */
+ int done; /* whether the answer has been found */
+};
+
+/*
+** called for each value received during a calculation of stdev or variance
+*/
+static void
+varianceStep(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ StdevCtx* p;
+
+ double delta;
+ double x;
+
+ assert(argc == 1);
+ p = (StdevCtx*) sqlite3_aggregate_context(context, sizeof(*p));
+ /* only consider non-null values */
+ if (SQLITE_NULL != sqlite3_value_numeric_type(argv[0])) {
+ p->cnt++;
+ x = sqlite3_value_double(argv[0]);
+ delta = (x - p->rM);
+ p->rM += delta / p->cnt;
+ p->rS += delta * (x - p->rM);
+ }
+}
+
+/*
+** called for each value received during a calculation of mode of median
+*/
+static void
+modeStep(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ ModeCtx* p;
+ i64 xi = 0;
+ double xd = 0.0;
+ i64* iptr;
+ double* dptr;
+ int type;
+
+ assert(argc == 1);
+ type = sqlite3_value_numeric_type(argv[0]);
+
+ if (type == SQLITE_NULL)
+ return;
+
+ p = (ModeCtx*) sqlite3_aggregate_context(context, sizeof(*p));
+
+ if (0 == (p->m)) {
+ p->m = (map*) calloc(1, sizeof(map));
+ if (type == SQLITE_INTEGER) {
+ /* map will be used for integers */
+ *(p->m) = map_make(int_cmp);
+ p->is_double = 0;
+ } else {
+ p->is_double = 1;
+ /* map will be used for doubles */
+ *(p->m) = map_make(double_cmp);
+ }
+ }
+
+ ++(p->cnt);
+
+ if (0 == p->is_double) {
+ xi = sqlite3_value_int64(argv[0]);
+ iptr = (i64*) calloc(1, sizeof(i64));
+ *iptr = xi;
+ map_insert(p->m, iptr);
+ } else {
+ xd = sqlite3_value_double(argv[0]);
+ dptr = (double*) calloc(1, sizeof(double));
+ *dptr = xd;
+ map_insert(p->m, dptr);
+ }
+}
+
+/*
+** Auxiliary function that iterates all elements in a map and finds the mode
+** (most frequent value)
+*/
+static void
+modeIterate(void* e, i64 c, void* pp)
+{
+ i64 ei;
+ double ed;
+ ModeCtx* p = (ModeCtx*) pp;
+
+ if (0 == p->is_double) {
+ ei = *(int*) (e);
+
+ if (p->mcnt == c) {
+ ++p->mn;
+ } else if (p->mcnt < c) {
+ p->riM = ei;
+ p->mcnt = c;
+ p->mn = 1;
+ }
+ } else {
+ ed = *(double*) (e);
+
+ if (p->mcnt == c) {
+ ++p->mn;
+ } else if (p->mcnt < c) {
+ p->rdM = ed;
+ p->mcnt = c;
+ p->mn = 1;
+ }
+ }
+}
+
+/*
+** Auxiliary function that iterates all elements in a map and finds the median
+** (the value such that the number of elements smaller is equal the number of
+** elements larger)
+*/
+static void
+medianIterate(void* e, i64 c, void* pp)
+{
+ i64 ei;
+ double ed;
+ double iL;
+ double iR;
+ int il;
+ int ir;
+ ModeCtx* p = (ModeCtx*) pp;
+
+ if (p->done > 0)
+ return;
+
+ iL = p->pcnt;
+ iR = p->cnt - p->pcnt;
+ il = p->mcnt + c;
+ ir = p->cnt - p->mcnt;
+
+ if (il >= iL) {
+ if (ir >= iR) {
+ ++p->mn;
+ if (0 == p->is_double) {
+ ei = *(int*) (e);
+ p->riM += ei;
+ } else {
+ ed = *(double*) (e);
+ p->rdM += ed;
+ }
+ } else {
+ p->done = 1;
+ }
+ }
+ p->mcnt += c;
+}
+
+/*
+** Returns the mode value
+*/
+static void
+modeFinalize(sqlite3_context* context)
+{
+ ModeCtx* p;
+ p = (ModeCtx*) sqlite3_aggregate_context(context, 0);
+ if (p && p->m) {
+ map_iterate(p->m, modeIterate, p);
+ map_destroy(p->m);
+ free(p->m);
+
+ if (1 == p->mn) {
+ if (0 == p->is_double)
+ sqlite3_result_int64(context, p->riM);
+ else
+ sqlite3_result_double(context, p->rdM);
+ }
+ }
+}
+
+/*
+** auxiliary function for percentiles
+*/
+static void
+_medianFinalize(sqlite3_context* context)
+{
+ ModeCtx* p;
+ p = (ModeCtx*) sqlite3_aggregate_context(context, 0);
+ if (p && p->m) {
+ p->done = 0;
+ map_iterate(p->m, medianIterate, p);
+ map_destroy(p->m);
+ free(p->m);
+
+ if (0 == p->is_double)
+ if (1 == p->mn)
+ sqlite3_result_int64(context, p->riM);
+ else
+ sqlite3_result_double(context, p->riM * 1.0 / p->mn);
+ else
+ sqlite3_result_double(context, p->rdM / p->mn);
+ }
+}
+
+/*
+** Returns the median value
+*/
+static void
+medianFinalize(sqlite3_context* context)
+{
+ ModeCtx* p;
+ p = (ModeCtx*) sqlite3_aggregate_context(context, 0);
+ if (p != 0) {
+ p->pcnt = (p->cnt) / 2.0;
+ _medianFinalize(context);
+ }
+}
+
+/*
+** Returns the lower_quartile value
+*/
+static void
+lower_quartileFinalize(sqlite3_context* context)
+{
+ ModeCtx* p;
+ p = (ModeCtx*) sqlite3_aggregate_context(context, 0);
+ if (p != 0) {
+ p->pcnt = (p->cnt) / 4.0;
+ _medianFinalize(context);
+ }
+}
+
+/*
+** Returns the upper_quartile value
+*/
+static void
+upper_quartileFinalize(sqlite3_context* context)
+{
+ ModeCtx* p;
+ p = (ModeCtx*) sqlite3_aggregate_context(context, 0);
+ if (p != 0) {
+ p->pcnt = (p->cnt) * 3 / 4.0;
+ _medianFinalize(context);
+ }
+}
+
+/*
+** Returns the stdev value
+*/
+static void
+stdevFinalize(sqlite3_context* context)
+{
+ StdevCtx* p;
+ p = (StdevCtx*) sqlite3_aggregate_context(context, 0);
+ if (p && p->cnt > 1) {
+ sqlite3_result_double(context, sqrt(p->rS / (p->cnt - 1)));
+ } else {
+ sqlite3_result_double(context, 0.0);
+ }
+}
+
+/*
+** Returns the variance value
+*/
+static void
+varianceFinalize(sqlite3_context* context)
+{
+ StdevCtx* p;
+ p = (StdevCtx*) sqlite3_aggregate_context(context, 0);
+ if (p && p->cnt > 1) {
+ sqlite3_result_double(context, p->rS / (p->cnt - 1));
+ } else {
+ sqlite3_result_double(context, 0.0);
+ }
+}
+
+#ifdef SQLITE_SOUNDEX
+
+/* relicoder factored code */
+/*
+** Calculates the soundex value of a string
+*/
+
+static void
+soundex(const u8* zIn, char* zResult)
+{
+ int i, j;
+ static const unsigned char iCode[] = {
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 1, 2, 3, 0, 1, 2, 0, 0, 2, 2, 4, 5, 5, 0, 1, 2, 6, 2, 3, 0, 1, 0,
+ 2, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 0, 1, 2, 0, 0, 2, 2, 4, 5,
+ 5, 0, 1, 2, 6, 2, 3, 0, 1, 0, 2, 0, 2, 0, 0, 0, 0, 0,
+ };
+
+ for (i = 0; zIn[i] && !isalpha(zIn[i]); i++) {
+ }
+ if (zIn[i]) {
+ zResult[0] = toupper(zIn[i]);
+ for (j = 1; j < 4 && zIn[i]; i++) {
+ int code = iCode[zIn[i] & 0x7f];
+ if (code > 0) {
+ zResult[j++] = code + '0';
+ }
+ }
+ while (j < 4) {
+ zResult[j++] = '0';
+ }
+ zResult[j] = 0;
+ } else {
+ strcpy(zResult, "?000");
+ }
+}
+
+/*
+** computes the number of different characters between the soundex value fo 2
+*strings
+*/
+static void
+differenceFunc(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ char zResult1[8];
+ char zResult2[8];
+ char* zR1 = zResult1;
+ char* zR2 = zResult2;
+ int rVal = 0;
+ int i = 0;
+ const u8* zIn1;
+ const u8* zIn2;
+
+ assert(argc == 2);
+
+ if (sqlite3_value_type(argv[0]) == SQLITE_NULL
+ || sqlite3_value_type(argv[1]) == SQLITE_NULL)
+ {
+ sqlite3_result_null(context);
+ return;
+ }
+
+ zIn1 = (u8*) sqlite3_value_text(argv[0]);
+ zIn2 = (u8*) sqlite3_value_text(argv[1]);
+
+ soundex(zIn1, zR1);
+ soundex(zIn2, zR2);
+
+ for (i = 0; i < 4; ++i) {
+ if (sqliteCharVal((unsigned char*) zR1)
+ == sqliteCharVal((unsigned char*) zR2))
+ ++rVal;
+ sqliteNextChar(zR1);
+ sqliteNextChar(zR2);
+ }
+ sqlite3_result_int(context, rVal);
+}
+#endif
+
+/*
+** This function registered all of the above C functions as SQL
+** functions. This should be the only routine in this file with
+** external linkage.
+*/
+int
+common_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs)
+{
+ static struct FuncDef aFuncs[] = {
+ /* math.h */
+ {
+ "acos",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ acosFunc,
+ help_text("acos")
+ .sql_function()
+ .with_summary("Returns the arccosine of a number, in radians")
+ .with_parameter(
+ {"num", "A cosine value that is between -1 and 1"})
+ .with_tags({"math"})
+ .with_example(
+ {"To get the arccosine of 0.2", "SELECT acos(0.2)"}),
+ },
+ {
+ "asin",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ asinFunc,
+ help_text("asin")
+ .sql_function()
+ .with_summary("Returns the arcsine of a number, in radians")
+ .with_parameter(
+ {"num", "A sine value that is between -1 and 1"})
+ .with_tags({"math"})
+ .with_example(
+ {"To get the arcsine of 0.2", "SELECT asin(0.2)"}),
+ },
+ {
+ "atan",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ atanFunc,
+ help_text("atan")
+ .sql_function()
+ .with_summary("Returns the arctangent of a number, in radians")
+ .with_parameter({"num", "The number"})
+ .with_tags({"math"})
+ .with_example(
+ {"To get the arctangent of 0.2", "SELECT atan(0.2)"}),
+ },
+ {
+ "atn2",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ atn2Func,
+ help_text("atn2")
+ .sql_function()
+ .with_summary("Returns the angle in the plane between the "
+ "positive X axis "
+ "and the ray from (0, 0) to the point (x, y)")
+ .with_parameter({"y", "The y coordinate of the point"})
+ .with_parameter({"x", "The x coordinate of the point"})
+ .with_tags({"math"})
+ .with_example({
+ "To get the angle, in degrees, for the point at (5, 5)",
+ "SELECT degrees(atn2(5, 5))",
+ }),
+ },
+ /* XXX alias */
+ {
+ "atan2",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ atn2Func,
+ help_text("atan2")
+ .sql_function()
+ .with_summary("Returns the angle in the plane between the "
+ "positive X axis "
+ "and the ray from (0, 0) to the point (x, y)")
+ .with_parameter({"y", "The y coordinate of the point"})
+ .with_parameter({"x", "The x coordinate of the point"})
+ .with_tags({"math"})
+ .with_example({
+ "To get the angle, in degrees, for the point at (5, 5)",
+ "SELECT degrees(atan2(5, 5))",
+ }),
+ },
+ {
+ "acosh",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ acoshFunc,
+ help_text("acosh")
+ .sql_function()
+ .with_summary("Returns the hyperbolic arccosine of a number")
+ .with_parameter({"num", "A number that is one or more"})
+ .with_tags({"math"})
+ .with_example({
+ "To get the hyperbolic arccosine of 1.2",
+ "SELECT acosh(1.2)",
+ }),
+ },
+ {
+ "asinh",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ asinhFunc,
+ help_text("asinh")
+ .sql_function()
+ .with_summary("Returns the hyperbolic arcsine of a number")
+ .with_parameter({"num", "The number"})
+ .with_tags({"math"})
+ .with_example({
+ "To get the hyperbolic arcsine of 0.2",
+ "SELECT asinh(0.2)",
+ }),
+ },
+ {
+ "atanh",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ atanhFunc,
+ help_text("atanh")
+ .sql_function()
+ .with_summary("Returns the hyperbolic arctangent of a number")
+ .with_parameter({"num", "The number"})
+ .with_tags({"math"})
+ .with_example({
+ "To get the hyperbolic arctangent of 0.2",
+ "SELECT atanh(0.2)",
+ }),
+ },
+
+ {"difference", 2, SQLITE_UTF8, 0, differenceFunc},
+ {
+ "degrees",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ rad2degFunc,
+ help_text("degrees")
+ .sql_function()
+ .with_summary("Converts radians to degrees")
+ .with_parameter(
+ {"radians", "The radians value to convert to degrees"})
+ .with_tags({"math"})
+ .with_example(
+ {"To convert PI to degrees", "SELECT degrees(pi())"}),
+ },
+ {
+ "radians",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ deg2radFunc,
+ help_text("radians")
+ .sql_function()
+ .with_summary("Converts degrees to radians")
+ .with_parameter(
+ {"degrees", "The degrees value to convert to radians"})
+ .with_tags({"math"})
+ .with_example({
+ "To convert 180 degrees to radians",
+ "SELECT radians(180)",
+ }),
+ },
+
+ {"cos", 1, SQLITE_UTF8, 0, cosFunc},
+ {"sin", 1, SQLITE_UTF8, 0, sinFunc},
+ {"tan", 1, SQLITE_UTF8, 0, tanFunc},
+ {"cot", 1, SQLITE_UTF8, 0, cotFunc},
+ {"cosh", 1, SQLITE_UTF8, 0, coshFunc},
+ {"sinh", 1, SQLITE_UTF8, 0, sinhFunc},
+ {"tanh", 1, SQLITE_UTF8, 0, tanhFunc},
+ {"coth", 1, SQLITE_UTF8, 0, cothFunc},
+
+ {
+ "exp",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ expFunc,
+ help_text("exp")
+ .sql_function()
+ .with_summary("Returns the value of e raised to the power of x")
+ .with_parameter({"x", "The exponent"})
+ .with_tags({"math"})
+ .with_example({"To raise e to 2", "SELECT exp(2)"}),
+ },
+ {
+ "log",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ logFunc,
+ help_text("log")
+ .sql_function()
+ .with_summary("Returns the natural logarithm of x")
+ .with_parameter({"x", "The number"})
+ .with_tags({"math"})
+ .with_example(
+ {"To get the natual logarithm of 8", "SELECT log(8)"}),
+ },
+ {
+ "log10",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ log10Func,
+ help_text("log10")
+ .sql_function()
+ .with_summary("Returns the base-10 logarithm of X")
+ .with_parameter({"x", "The number"})
+ .with_tags({"math"})
+ .with_example(
+ {"To get the logarithm of 100", "SELECT log10(100)"}),
+ },
+ {
+ "power",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ powerFunc,
+ help_text("power")
+ .sql_function()
+ .with_summary("Returns the base to the given exponent")
+ .with_parameter({"base", "The base number"})
+ .with_parameter({"exp", "The exponent"})
+ .with_tags({"math"})
+ .with_example({
+ "To raise two to the power of three",
+ "SELECT power(2, 3)",
+ }),
+ },
+ {
+ "sign",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ signFunc,
+ help_text("sign")
+ .sql_function()
+ .with_summary(
+ "Returns the sign of the given number as -1, 0, or 1")
+ .with_parameter({"num", "The number"})
+ .with_tags({"math"})
+ .with_example({"To get the sign of 10", "SELECT sign(10)"})
+ .with_example({"To get the sign of 0", "SELECT sign(0)"})
+ .with_example({"To get the sign of -10", "SELECT sign(-10)"}),
+ },
+ {"sqrt", 1, SQLITE_UTF8, 0, sqrtFunc},
+ {
+ "square",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ squareFunc,
+ help_text("square")
+ .sql_function()
+ .with_summary("Returns the square of the argument")
+ .with_parameter({"num", "The number to square"})
+ .with_tags({"math"})
+ .with_example({"To get the square of two", "SELECT square(2)"}),
+ },
+
+ {
+ "ceil",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ ceilFunc,
+ help_text("ceil")
+ .sql_function()
+ .with_summary(
+ "Returns the smallest integer that is not less than "
+ "the argument")
+ .with_parameter({"num", "The number to raise to the ceiling"})
+ .with_tags({"math"})
+ .with_example(
+ {"To get the ceiling of 1.23", "SELECT ceil(1.23)"}),
+ },
+ {
+ "floor",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ floorFunc,
+ help_text("floor")
+ .sql_function()
+ .with_summary("Returns the largest integer that is not greater "
+ "than the argument")
+ .with_parameter({"num", "The number to lower to the floor"})
+ .with_tags({"math"})
+ .with_example(
+ {"To get the floor of 1.23", "SELECT floor(1.23)"}),
+ },
+
+ {
+ "pi",
+ 0,
+ SQLITE_UTF8,
+ 1,
+ piFunc,
+ help_text("pi")
+ .sql_function()
+ .with_summary("Returns the value of PI")
+ .with_tags({"math"})
+ .with_example({"To get the value of PI", "SELECT pi()"}),
+ },
+
+ /* string */
+ {
+ "replicate",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ replicateFunc,
+ help_text("replicate")
+ .sql_function()
+ .with_summary("Returns the given string concatenated N times.")
+ .with_parameter({"str", "The string to replicate."})
+ .with_parameter(
+ {"N", "The number of times to replicate the string."})
+ .with_tags({"string"})
+ .with_example({
+ "To repeat the string 'abc' three times",
+ "SELECT replicate('abc', 3)",
+ }),
+ },
+ {"charindex", 2, SQLITE_UTF8, 0, charindexFunc},
+ {
+ "charindex",
+ 3,
+ SQLITE_UTF8,
+ 0,
+ charindexFunc,
+ help_text("charindex")
+ .sql_function()
+ .with_summary("Finds the first occurrence of the needle within "
+ "the haystack "
+ "and returns the number of prior characters plus "
+ "1, or 0 if Y "
+ "is nowhere found within X")
+ .with_parameter(
+ {"needle", "The string to look for in the haystack"})
+ .with_parameter({"haystack", "The string to search within"})
+ .with_parameter(help_text("start",
+ "The one-based index within the "
+ "haystack to start the search")
+ .optional())
+ .with_tags({"string"})
+ .with_example({
+ "To search for the string 'abc' within 'abcabc' "
+ "and starting at position 2",
+ "SELECT charindex('abc', 'abcabc', 2)",
+ })
+ .with_example({
+ "To search for the string 'abc' within 'abcdef' "
+ "and starting at position 2",
+ "SELECT charindex('abc', 'abcdef', 2)",
+ }),
+ },
+ {
+ "leftstr",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ leftFunc,
+ help_text("leftstr")
+ .sql_function()
+ .with_summary(
+ "Returns the N leftmost (UTF-8) characters in the "
+ "given string.")
+ .with_parameter({"str", "The string to return subset."})
+ .with_parameter(
+ {"N",
+ "The number of characters from the left side of "
+ "the string to return."})
+ .with_tags({"string"})
+ .with_example({
+ "To get the first character of the string 'abc'",
+ "SELECT leftstr('abc', 1)",
+ })
+ .with_example({
+ "To get the first ten characters of a string, "
+ "regardless of size",
+ "SELECT leftstr('abc', 10)",
+ }),
+ },
+ {
+ "rightstr",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ rightFunc,
+ help_text("rightstr")
+ .sql_function()
+ .with_summary(
+ "Returns the N rightmost (UTF-8) characters in the "
+ "given string.")
+ .with_parameter({"str", "The string to return subset."})
+ .with_parameter(
+ {"N",
+ "The number of characters from the right side of "
+ "the string to return."})
+ .with_tags({"string"})
+ .with_example({
+ "To get the last character of the string 'abc'",
+ "SELECT rightstr('abc', 1)",
+ })
+ .with_example({
+ "To get the last ten characters of a string, "
+ "regardless of size",
+ "SELECT rightstr('abc', 10)",
+ }),
+ },
+#ifndef HAVE_TRIM
+ {"ltrim", 1, SQLITE_UTF8, 0, ltrimFunc},
+ {"rtrim", 1, SQLITE_UTF8, 0, rtrimFunc},
+ {"trim", 1, SQLITE_UTF8, 0, trimFunc},
+ {"replace", 3, SQLITE_UTF8, 0, replaceFunc},
+#endif
+ {
+ "reverse",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ reverseFunc,
+ help_text("reverse")
+ .sql_function()
+ .with_summary("Returns the reverse of the given string.")
+ .with_parameter({"str", "The string to reverse."})
+ .with_tags({"string"})
+ .with_example(
+ {"To reverse the string 'abc'", "SELECT reverse('abc')"}),
+ },
+ {
+ "proper",
+ 1,
+ SQLITE_UTF8,
+ 0,
+ properFunc,
+ help_text("proper")
+ .sql_function()
+ .with_summary("Capitalize the first character of words in the "
+ "given string")
+ .with_parameter({"str", "The string to capitalize."})
+ .with_tags({"string"})
+ .with_example({
+ "To capitalize the words in the string 'hello, world!'",
+ "SELECT proper('hello, world!')",
+ }),
+ },
+ {
+ "padl",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ padlFunc,
+ help_text("padl")
+ .sql_function()
+ .with_summary(
+ "Pad the given string with leading spaces until it "
+ "reaches the desired length")
+ .with_parameter({"str", "The string to pad"})
+ .with_parameter(
+ {"len", "The minimum desired length of the output string"})
+ .with_tags({"string"})
+ .with_example(
+ {"To pad the string 'abc' to a length of six characters",
+ "SELECT padl('abc', 6)"})
+ .with_example({
+ "To pad the string 'abcdef' to a length of four "
+ "characters",
+ "SELECT padl('abcdef', 4)",
+ }),
+ },
+ {
+ "padr",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ padrFunc,
+ help_text("padr")
+ .sql_function()
+ .with_summary(
+ "Pad the given string with trailing spaces until it "
+ "reaches the desired length")
+ .with_parameter({"str", "The string to pad"})
+ .with_parameter(
+ {"len", "The minimum desired length of the output string"})
+ .with_tags({"string"})
+ .with_example(
+ {"To pad the string 'abc' to a length of six characters",
+ "SELECT padr('abc', 6) || 'def'"})
+ .with_example({
+ "To pad the string 'abcdef' to a length of four characters",
+ "SELECT padr('abcdef', 4) || 'ghi'",
+ }),
+ },
+ {
+ "padc",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ padcFunc,
+ help_text("padc")
+ .sql_function()
+ .with_summary(
+ "Pad the given string with enough spaces to make it "
+ "centered within the given length")
+ .with_parameter({"str", "The string to pad"})
+ .with_parameter(
+ {"len", "The minimum desired length of the output string"})
+ .with_tags({"string"})
+ .with_example(
+ {"To pad the string 'abc' to a length of six characters",
+ "SELECT padc('abc', 6) || 'def'"})
+ .with_example({
+ "To pad the string 'abcdef' to a length of "
+ "eight characters",
+ "SELECT padc('abcdef', 8) || 'ghi'",
+ }),
+ },
+ {
+ "strfilter",
+ 2,
+ SQLITE_UTF8,
+ 0,
+ strfilterFunc,
+ help_text("strfilter")
+ .sql_function()
+ .with_summary(
+ "Returns the source string with only the characters "
+ "given in the second parameter")
+ .with_parameter({"source", "The string to filter"})
+ .with_parameter(
+ {"include", "The characters to include in the result"})
+ .with_tags({"string"})
+ .with_example({
+ "To get the 'b', 'c', and 'd' characters from the "
+ "string 'abcabc'",
+ "SELECT strfilter('abcabc', 'bcd')",
+ }),
+ },
+
+ {nullptr},
+ };
+
+ /* Aggregate functions */
+ static struct FuncDefAgg aAggs[] = {
+ {"stdev", 1, 0, varianceStep, stdevFinalize},
+ {"stddev", 1, 0, varianceStep, stdevFinalize},
+ {"variance", 1, 0, varianceStep, varianceFinalize},
+ {"mode", 1, 0, modeStep, modeFinalize},
+ {"median", 1, 0, modeStep, medianFinalize},
+ {"lower_quartile", 1, 0, modeStep, lower_quartileFinalize},
+ {"upper_quartile", 1, 0, modeStep, upper_quartileFinalize},
+
+ {nullptr},
+ };
+
+ *basic_funcs = aFuncs;
+ *agg_funcs = aAggs;
+
+ return SQLITE_OK;
+}
+
+#ifdef COMPILE_SQLITE_EXTENSIONS_AS_LOADABLE_MODULE
+int
+sqlite3_extension_init(sqlite3* db,
+ char** pzErrMsg,
+ const sqlite3_api_routines* pApi)
+{
+ SQLITE_EXTENSION_INIT2(pApi);
+ RegisterExtensionFunctions(db);
+ return 0;
+}
+#endif /* COMPILE_SQLITE_EXTENSIONS_AS_LOADABLE_MODULE */
+
+map
+map_make(cmp_func cmp)
+{
+ map r;
+ r.cmp = cmp;
+ r.base = 0;
+ r.free = 0;
+
+ return r;
+}
+
+void*
+xcalloc(size_t nmemb, size_t size, const char* s)
+{
+ void* ret = calloc(nmemb, size);
+ return ret;
+}
+
+static void
+xfree(void* p)
+{
+ free(p);
+}
+
+void
+node_insert(node** n, cmp_func cmp, void* e)
+{
+ int c;
+ node* nn;
+ if (*n == 0) {
+ nn = (node*) xcalloc(1, sizeof(node), "for node");
+ nn->data = e;
+ nn->count = 1;
+ *n = nn;
+ } else {
+ c = cmp((*n)->data, e);
+ if (0 == c) {
+ ++((*n)->count);
+ xfree(e);
+ } else if (c > 0) {
+ /* put it right here */
+ node_insert(&((*n)->l), cmp, e);
+ } else {
+ node_insert(&((*n)->r), cmp, e);
+ }
+ }
+}
+
+void
+map_insert(map* m, void* e)
+{
+ node_insert(&(m->base), m->cmp, e);
+}
+
+void
+node_iterate(node* n, map_iterator iter, void* p)
+{
+ if (n) {
+ if (n->l)
+ node_iterate(n->l, iter, p);
+ iter(n->data, n->count, p);
+ if (n->r)
+ node_iterate(n->r, iter, p);
+ }
+}
+
+void
+map_iterate(map* m, map_iterator iter, void* p)
+{
+ node_iterate(m->base, iter, p);
+}
+
+void
+node_destroy(node* n)
+{
+ if (0 != n) {
+ xfree(n->data);
+ if (n->l)
+ node_destroy(n->l);
+ if (n->r)
+ node_destroy(n->r);
+
+ xfree(n);
+ }
+}
+
+void
+map_destroy(map* m)
+{
+ node_destroy(m->base);
+}
+
+int
+int_cmp(const void* a, const void* b)
+{
+ int64_t aa = *(int64_t*) (a);
+ int64_t bb = *(int64_t*) (b);
+ /* printf("cmp %d <=> %d\n",aa,bb); */
+ if (aa == bb)
+ return 0;
+ else if (aa < bb)
+ return -1;
+ else
+ return 1;
+}
+
+int
+double_cmp(const void* a, const void* b)
+{
+ double aa = *(double*) (a);
+ double bb = *(double*) (b);
+ /* printf("cmp %d <=> %d\n",aa,bb); */
+ if (aa == bb)
+ return 0;
+ else if (aa < bb)
+ return -1;
+ else
+ return 1;
+}
+
+void
+print_elem(void* e, int64_t c, void* p)
+{
+ int ee = *(int*) (e);
+ printf("%d => %" PRId64 "\n", ee, c);
+}
diff --git a/src/field_overlay_source.cc b/src/field_overlay_source.cc
new file mode 100644
index 0000000..ad4c312
--- /dev/null
+++ b/src/field_overlay_source.cc
@@ -0,0 +1,574 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "field_overlay_source.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/humanize.time.hh"
+#include "base/snippet_highlighters.hh"
+#include "config.h"
+#include "log_format_ext.hh"
+#include "log_vtab_impl.hh"
+#include "md2attr_line.hh"
+#include "readline_highlighters.hh"
+#include "relative_time.hh"
+#include "vtab_module.hh"
+#include "vtab_module_json.hh"
+
+json_string extract(const char* str);
+
+void
+field_overlay_source::build_field_lines(const listview_curses& lv)
+{
+ auto& lss = this->fos_lss;
+ auto& vc = view_colors::singleton();
+
+ this->fos_lines.clear();
+
+ if (lss.text_line_count() == 0) {
+ this->fos_log_helper.clear();
+
+ return;
+ }
+
+ content_line_t cl = lss.at(lv.get_selection());
+ std::shared_ptr<logfile> file = lss.find(cl);
+ auto ll = file->begin() + cl;
+ auto format = file->get_format();
+ bool display = false;
+
+ if (ll->is_time_skewed()
+ || ll->get_msg_level() == log_level_t::LEVEL_INVALID)
+ {
+ display = true;
+ }
+ if (!this->fos_contexts.empty()) {
+ display = display || this->fos_contexts.top().c_show;
+ }
+
+ this->build_meta_line(lv, this->fos_lines, lv.get_top());
+
+ if (!display) {
+ return;
+ }
+
+ if (!this->fos_log_helper.parse_line(lv.get_selection())) {
+ return;
+ }
+
+ if (ll->get_msg_level() == LEVEL_INVALID) {
+ for (const auto& sattr : this->fos_log_helper.ldh_line_attrs) {
+ if (sattr.sa_type != &SA_INVALID) {
+ continue;
+ }
+
+ auto emsg = fmt::format(
+ FMT_STRING(" Invalid log message: {}"),
+ sattr.sa_value.get<decltype(SA_INVALID)::value_type>());
+ auto al = attr_line_t(emsg)
+ .with_attr(string_attr(
+ line_range{1, 2}, VC_GRAPHIC.value(ACS_LLCORNER)))
+ .with_attr(string_attr(
+ line_range{0, 22},
+ VC_ROLE.value(role_t::VCR_INVALID_MSG)));
+ this->fos_lines.emplace_back(al);
+ }
+ }
+
+ char old_timestamp[64], curr_timestamp[64], orig_timestamp[64];
+ struct timeval curr_tv, offset_tv, orig_tv, diff_tv = {0, 0};
+ attr_line_t time_line;
+ auto& time_str = time_line.get_string();
+ struct line_range time_lr;
+
+ sql_strftime(curr_timestamp,
+ sizeof(curr_timestamp),
+ ll->get_time(),
+ ll->get_millis(),
+ 'T');
+
+ if (ll->is_time_skewed()) {
+ time_lr.lr_start = 1;
+ time_lr.lr_end = 2;
+ time_line.with_attr(
+ string_attr(time_lr, VC_GRAPHIC.value(ACS_LLCORNER)));
+ time_str.append(" Out-Of-Time-Order Message");
+ time_lr.lr_start = 3;
+ time_lr.lr_end = time_str.length();
+ time_line.with_attr(
+ string_attr(time_lr, VC_ROLE.value(role_t::VCR_SKEWED_TIME)));
+ time_str.append(" --");
+ }
+
+ time_str.append(" Received Time: ");
+ time_lr.lr_start = time_str.length();
+ time_str.append(curr_timestamp);
+ time_lr.lr_end = time_str.length();
+ time_line.with_attr(
+ string_attr(time_lr, VC_STYLE.value(text_attrs{A_BOLD})));
+ time_str.append(" -- ");
+ time_lr.lr_start = time_str.length();
+ time_str.append(humanize::time::point::from_tv(ll->get_timeval())
+ .with_convert_to_local(true)
+ .as_precise_time_ago());
+ time_lr.lr_end = time_str.length();
+ time_line.with_attr(
+ string_attr(time_lr, VC_STYLE.value(text_attrs{A_BOLD})));
+
+ struct line_range time_range = find_string_attr_range(
+ this->fos_log_helper.ldh_line_attrs, &logline::L_TIMESTAMP);
+
+ curr_tv = this->fos_log_helper.ldh_line->get_timeval();
+ if (ll->is_time_skewed() && time_range.lr_end != -1) {
+ const char* time_src
+ = this->fos_log_helper.ldh_line_values.lvv_sbr.get_data()
+ + time_range.lr_start;
+ struct timeval actual_tv;
+ date_time_scanner dts;
+ struct exttm tm;
+
+ dts.set_base_time(format->lf_date_time.dts_base_time,
+ format->lf_date_time.dts_base_tm.et_tm);
+ if (format->lf_date_time.scan(time_src,
+ time_range.length(),
+ format->get_timestamp_formats(),
+ &tm,
+ actual_tv,
+ false)
+ || dts.scan(
+ time_src, time_range.length(), nullptr, &tm, actual_tv, false))
+ {
+ sql_strftime(
+ orig_timestamp, sizeof(orig_timestamp), actual_tv, 'T');
+ time_str.append("; Actual Time: ");
+ time_lr.lr_start = time_str.length();
+ time_str.append(orig_timestamp);
+ time_lr.lr_end = time_str.length();
+ time_line.with_attr(
+ string_attr(time_lr, VC_ROLE.value(role_t::VCR_SKEWED_TIME)));
+
+ timersub(&curr_tv, &actual_tv, &diff_tv);
+ time_str.append("; Diff: ");
+ time_lr.lr_start = time_str.length();
+ time_str.append(
+ humanize::time::duration::from_tv(diff_tv).to_string());
+ time_lr.lr_end = time_str.length();
+ time_line.with_attr(
+ string_attr(time_lr, VC_STYLE.value(text_attrs{A_BOLD})));
+ }
+ }
+
+ offset_tv = this->fos_log_helper.ldh_file->get_time_offset();
+ timersub(&curr_tv, &offset_tv, &orig_tv);
+ sql_strftime(old_timestamp,
+ sizeof(old_timestamp),
+ orig_tv.tv_sec,
+ orig_tv.tv_usec / 1000,
+ 'T');
+ if (offset_tv.tv_sec || offset_tv.tv_usec) {
+ time_str.append(" Pre-adjust Time: ");
+ time_str.append(old_timestamp);
+ fmt::format_to(std::back_inserter(time_str),
+ FMT_STRING(" Offset: {:+}.{:03}"),
+ offset_tv.tv_sec,
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::microseconds(offset_tv.tv_usec))
+ .count());
+ }
+
+ if (format->lf_date_time.dts_fmt_lock != -1) {
+ const auto* ts_formats = format->get_timestamp_formats();
+ if (ts_formats == nullptr) {
+ ts_formats = PTIMEC_FORMAT_STR;
+ }
+ time_line.append(" Format: ")
+ .append(lnav::roles::symbol(
+ ts_formats[format->lf_date_time.dts_fmt_lock]));
+ }
+
+ if ((!this->fos_contexts.empty() && this->fos_contexts.top().c_show)
+ || diff_tv.tv_sec > 0)
+ {
+ this->fos_lines.emplace_back(time_line);
+ }
+
+ if (this->fos_contexts.empty() || !this->fos_contexts.top().c_show) {
+ return;
+ }
+
+ this->fos_known_key_size = LOG_BODY.length();
+ if (!this->fos_contexts.empty()) {
+ this->fos_known_key_size += this->fos_contexts.top().c_prefix.length();
+ }
+ this->fos_unknown_key_size = 0;
+
+ for (auto& ldh_line_value : this->fos_log_helper.ldh_line_values.lvv_values)
+ {
+ auto& meta = ldh_line_value.lv_meta;
+ int this_key_size = meta.lvm_name.size();
+
+ if (!this->fos_contexts.empty()) {
+ this_key_size += this->fos_contexts.top().c_prefix.length();
+ }
+ if (meta.lvm_kind == value_kind_t::VALUE_STRUCT) {
+ this_key_size += 9;
+ }
+ if (!meta.lvm_struct_name.empty()) {
+ this_key_size += meta.lvm_struct_name.size() + 11;
+ }
+ this->fos_known_key_size
+ = std::max(this->fos_known_key_size, this_key_size);
+ }
+
+ for (auto iter = this->fos_log_helper.ldh_parser->dp_pairs.begin();
+ iter != this->fos_log_helper.ldh_parser->dp_pairs.end();
+ ++iter)
+ {
+ std::string colname
+ = this->fos_log_helper.ldh_parser->get_element_string(
+ iter->e_sub_elements->front());
+
+ colname
+ = this->fos_log_helper.ldh_namer->add_column(colname).to_string();
+ this->fos_unknown_key_size
+ = std::max(this->fos_unknown_key_size, (int) colname.length());
+ }
+
+ auto lf = this->fos_log_helper.ldh_file->get_format();
+ if (!lf->get_pattern_regex(cl).empty()) {
+ attr_line_t pattern_al;
+ std::string& pattern_str = pattern_al.get_string();
+ pattern_str = " Pattern: " + lf->get_pattern_path(cl) + " = ";
+ int skip = pattern_str.length();
+ pattern_str += lf->get_pattern_regex(cl);
+ lnav::snippets::regex_highlighter(
+ pattern_al,
+ pattern_al.length(),
+ line_range{skip, (int) pattern_al.length()});
+ this->fos_lines.emplace_back(pattern_al);
+ }
+
+ if (this->fos_log_helper.ldh_line_values.lvv_values.empty()) {
+ this->fos_lines.emplace_back(" No known message fields");
+ }
+
+ const log_format* last_format = nullptr;
+
+ for (auto& lv : this->fos_log_helper.ldh_line_values.lvv_values) {
+ if (!lv.lv_meta.lvm_format) {
+ continue;
+ }
+
+ auto* curr_format = lv.lv_meta.lvm_format.value();
+ auto* curr_elf = dynamic_cast<external_log_format*>(curr_format);
+ const auto format_name = curr_format->get_name().to_string();
+ attr_line_t al;
+ std::string str, value_str = lv.to_string();
+
+ if (curr_format != last_format) {
+ this->fos_lines.emplace_back(" Known message fields for table "
+ + format_name + ":");
+ this->fos_lines.back().with_attr(
+ string_attr(line_range(32, 32 + format_name.length()),
+ VC_STYLE.value(vc.attrs_for_ident(format_name)
+ | text_attrs{A_BOLD})));
+ last_format = curr_format;
+ }
+
+ std::string field_name, orig_field_name;
+ if (lv.lv_meta.lvm_struct_name.empty()) {
+ if (curr_elf && curr_elf->elf_body_field == lv.lv_meta.lvm_name) {
+ field_name = LOG_BODY;
+ } else if (curr_elf
+ && curr_elf->lf_timestamp_field == lv.lv_meta.lvm_name)
+ {
+ field_name = LOG_TIME;
+ } else {
+ field_name = lv.lv_meta.lvm_name.to_string();
+ }
+ orig_field_name = field_name;
+ if (!this->fos_contexts.empty()) {
+ field_name = this->fos_contexts.top().c_prefix + field_name;
+ }
+ str = " " + field_name;
+ } else {
+ auto_mem<char, sqlite3_free> jgetter;
+
+ jgetter = sqlite3_mprintf(" jget(%s, '/%q')",
+ lv.lv_meta.lvm_struct_name.get(),
+ lv.lv_meta.lvm_name.get());
+ str = jgetter;
+ }
+ str.append(this->fos_known_key_size - (str.length() - 3), ' ');
+ str += " = " + value_str;
+
+ al.with_string(str);
+ if (lv.lv_meta.lvm_struct_name.empty()) {
+ auto prefix_len = field_name.length() - orig_field_name.length();
+ al.with_attr(string_attr(
+ line_range(3 + prefix_len, 3 + prefix_len + field_name.size()),
+ VC_STYLE.value(vc.attrs_for_ident(orig_field_name))));
+ } else {
+ al.with_attr(string_attr(
+ line_range(8, 8 + lv.lv_meta.lvm_struct_name.size()),
+ VC_STYLE.value(
+ vc.attrs_for_ident(lv.lv_meta.lvm_struct_name))));
+ }
+
+ this->fos_lines.emplace_back(al);
+ this->add_key_line_attrs(this->fos_known_key_size);
+
+ if (lv.lv_meta.lvm_kind == value_kind_t::VALUE_STRUCT) {
+ json_string js = extract(value_str.c_str());
+
+ al.clear()
+ .append(" extract(")
+ .append(lv.lv_meta.lvm_name.get(),
+ VC_STYLE.value(vc.attrs_for_ident(lv.lv_meta.lvm_name)))
+ .append(")")
+ .append(this->fos_known_key_size - lv.lv_meta.lvm_name.size()
+ - 9 + 3,
+ ' ')
+ .append(" = ")
+ .append(
+ string_fragment::from_bytes(js.js_content.in(), js.js_len));
+ this->fos_lines.emplace_back(al);
+ this->add_key_line_attrs(this->fos_known_key_size);
+ }
+ }
+
+ std::map<const intern_string_t, json_ptr_walk::walk_list_t>::iterator
+ json_iter;
+
+ if (!this->fos_log_helper.ldh_json_pairs.empty()) {
+ this->fos_lines.emplace_back(" JSON fields:");
+ }
+
+ for (json_iter = this->fos_log_helper.ldh_json_pairs.begin();
+ json_iter != this->fos_log_helper.ldh_json_pairs.end();
+ ++json_iter)
+ {
+ json_ptr_walk::walk_list_t& jpairs = json_iter->second;
+
+ for (size_t lpc = 0; lpc < jpairs.size(); lpc++) {
+ this->fos_lines.emplace_back(
+ " "
+ + this->fos_log_helper.format_json_getter(json_iter->first, lpc)
+ + " = " + jpairs[lpc].wt_value);
+ this->add_key_line_attrs(0);
+ }
+ }
+
+ if (!this->fos_log_helper.ldh_xml_pairs.empty()) {
+ this->fos_lines.emplace_back(" XML fields:");
+ }
+
+ for (const auto& xml_pair : this->fos_log_helper.ldh_xml_pairs) {
+ auto_mem<char, sqlite3_free> qname;
+ auto_mem<char, sqlite3_free> xp_call;
+
+ qname = sql_quote_ident(xml_pair.first.first.get());
+ xp_call = sqlite3_mprintf(
+ "xpath(%Q, %s)", xml_pair.first.second.c_str(), qname.in());
+ this->fos_lines.emplace_back(fmt::format(
+ FMT_STRING(" {} = {}"), xp_call.in(), xml_pair.second));
+ this->add_key_line_attrs(0);
+ }
+
+ if (!this->fos_contexts.empty()
+ && !this->fos_contexts.top().c_show_discovered)
+ {
+ return;
+ }
+
+ if (this->fos_log_helper.ldh_parser->dp_pairs.empty()) {
+ this->fos_lines.emplace_back(" No discovered message fields");
+ } else {
+ this->fos_lines.emplace_back(
+ " Discovered fields for logline table from message format: ");
+ this->fos_lines.back().with_attr(
+ string_attr(line_range(23, 23 + 7),
+ VC_STYLE.value(vc.attrs_for_ident("logline"))));
+ auto& al = this->fos_lines.back();
+ auto& disc_str = al.get_string();
+
+ al.with_attr(string_attr(line_range(disc_str.length(), -1),
+ VC_STYLE.value(text_attrs{A_BOLD})));
+ disc_str.append(this->fos_log_helper.ldh_msg_format);
+ }
+
+ auto iter = this->fos_log_helper.ldh_parser->dp_pairs.begin();
+ for (size_t lpc = 0; lpc < this->fos_log_helper.ldh_parser->dp_pairs.size();
+ lpc++, ++iter)
+ {
+ auto name = this->fos_log_helper.ldh_namer->cn_names[lpc];
+ auto val = this->fos_log_helper.ldh_parser->get_element_string(
+ iter->e_sub_elements->back());
+ attr_line_t al(fmt::format(FMT_STRING(" {} = {}"), name, val));
+
+ al.with_attr(
+ string_attr(line_range(3, 3 + name.length()),
+ VC_STYLE.value(vc.attrs_for_ident(name.to_string()))));
+
+ this->fos_lines.emplace_back(al);
+ this->add_key_line_attrs(
+ this->fos_unknown_key_size,
+ lpc == (this->fos_log_helper.ldh_parser->dp_pairs.size() - 1));
+ }
+}
+
+void
+field_overlay_source::build_meta_line(const listview_curses& lv,
+ std::vector<attr_line_t>& dst,
+ vis_line_t row)
+{
+ auto line_meta_opt = this->fos_lss.find_bookmark_metadata(row);
+
+ if (!line_meta_opt) {
+ return;
+ }
+ auto& vc = view_colors::singleton();
+ const auto& line_meta = *(line_meta_opt.value());
+ size_t filename_width = this->fos_lss.get_filename_offset();
+ const auto* tc = dynamic_cast<const textview_curses*>(&lv);
+
+ if (!line_meta.bm_comment.empty()) {
+ const auto* lead = line_meta.bm_tags.empty() ? " \u2514 " : " \u251c ";
+ md2attr_line mdal;
+ attr_line_t al;
+
+ auto parse_res = md4cpp::parse(line_meta.bm_comment, mdal);
+ if (parse_res.isOk()) {
+ al = parse_res.unwrap();
+ } else {
+ log_error("%d: cannot convert comment to markdown: %s",
+ (int) row,
+ parse_res.unwrapErr().c_str());
+ al = line_meta.bm_comment;
+ }
+
+ auto comment_lines = al.rtrim().split_lines();
+ for (size_t lpc = 0; lpc < comment_lines.size(); lpc++) {
+ auto& comment_line = comment_lines[lpc];
+
+ if (lpc == 0 && comment_line.empty()) {
+ continue;
+ }
+ comment_line.with_attr_for_all(VC_ROLE.value(role_t::VCR_COMMENT));
+ comment_line.insert(
+ 0, lpc == comment_lines.size() - 1 ? lead : " \u2502 ");
+ comment_line.insert(0, filename_width, ' ');
+ if (tc != nullptr) {
+ auto hl = tc->get_highlights();
+ auto hl_iter = hl.find({highlight_source_t::PREVIEW, "search"});
+
+ if (hl_iter != hl.end()) {
+ hl_iter->second.annotate(comment_line, filename_width);
+ }
+ }
+
+ dst.emplace_back(comment_line);
+ }
+ }
+ if (!line_meta.bm_tags.empty()) {
+ attr_line_t al;
+
+ al.with_string(" \u2514");
+ for (const auto& str : line_meta.bm_tags) {
+ al.append(1, ' ').append(str,
+ VC_STYLE.value(vc.attrs_for_ident(str)));
+ }
+
+ if (tc != nullptr) {
+ const auto& hm = tc->get_highlights();
+ auto hl_iter = hm.find({highlight_source_t::PREVIEW, "search"});
+
+ if (hl_iter != hm.end()) {
+ hl_iter->second.annotate(al, 2);
+ }
+ }
+ al.insert(0, filename_width, ' ');
+ if (tc != nullptr) {
+ auto hl = tc->get_highlights();
+ auto hl_iter = hl.find({highlight_source_t::PREVIEW, "search"});
+
+ if (hl_iter != hl.end()) {
+ hl_iter->second.annotate(al, filename_width);
+ }
+ }
+ dst.emplace_back(al);
+ }
+}
+
+void
+field_overlay_source::add_key_line_attrs(int key_size, bool last_line)
+{
+ string_attrs_t& sa = this->fos_lines.back().get_attrs();
+ struct line_range lr(1, 2);
+ int64_t graphic = (int64_t) (last_line ? ACS_LLCORNER : ACS_LTEE);
+ sa.emplace_back(lr, VC_GRAPHIC.value(graphic));
+
+ lr.lr_start = 3 + key_size + 3;
+ lr.lr_end = -1;
+ sa.emplace_back(lr, VC_STYLE.value(text_attrs{A_BOLD}));
+}
+
+bool
+field_overlay_source::list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t row,
+ attr_line_t& value_out)
+{
+ if (y == 0) {
+ this->build_field_lines(lv);
+ return false;
+ }
+
+ if (1 <= y && y <= (int) this->fos_lines.size()) {
+ value_out = this->fos_lines[y - 1];
+ return true;
+ }
+
+ if (!this->fos_meta_lines.empty() && this->fos_meta_lines_row == row - 1_vl)
+ {
+ value_out = this->fos_meta_lines.front();
+ this->fos_meta_lines.erase(this->fos_meta_lines.begin());
+
+ return true;
+ }
+
+ if (row < lv.get_inner_height()) {
+ this->fos_meta_lines.clear();
+ this->build_meta_line(lv, this->fos_meta_lines, row);
+ this->fos_meta_lines_row = row;
+ }
+
+ return false;
+}
diff --git a/src/field_overlay_source.hh b/src/field_overlay_source.hh
new file mode 100644
index 0000000..ab1d17f
--- /dev/null
+++ b/src/field_overlay_source.hh
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LNAV_FIELD_OVERLAY_SOURCE_H
+#define LNAV_FIELD_OVERLAY_SOURCE_H
+
+#include <utility>
+#include <vector>
+
+#include "listview_curses.hh"
+#include "log_data_helper.hh"
+#include "logfile_sub_source.hh"
+#include "textfile_sub_source.hh"
+
+class field_overlay_source : public list_overlay_source {
+public:
+ explicit field_overlay_source(logfile_sub_source& lss,
+ textfile_sub_source& tss)
+ : fos_lss(lss), fos_tss(tss), fos_log_helper(lss)
+ {
+ }
+
+ void add_key_line_attrs(int key_size, bool last_line = false);
+
+ bool list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t row,
+ attr_line_t& value_out) override;
+
+ void build_field_lines(const listview_curses& lv);
+ void build_meta_line(const listview_curses& lv,
+ std::vector<attr_line_t>& dst,
+ vis_line_t row);
+
+ struct context {
+ context(std::string prefix, bool show, bool show_discovered)
+ : c_prefix(std::move(prefix)), c_show(show),
+ c_show_discovered(show_discovered)
+ {
+ }
+
+ std::string c_prefix;
+ bool c_show{false};
+ bool c_show_discovered{true};
+ };
+
+ bool fos_show_status{true};
+ std::stack<context> fos_contexts;
+ logfile_sub_source& fos_lss;
+ textfile_sub_source& fos_tss;
+ log_data_helper fos_log_helper;
+ int fos_known_key_size{0};
+ int fos_unknown_key_size{0};
+ std::vector<attr_line_t> fos_lines;
+ vis_line_t fos_meta_lines_row{0_vl};
+ std::vector<attr_line_t> fos_meta_lines;
+};
+
+#endif // LNAV_FIELD_OVERLAY_SOURCE_H
diff --git a/src/file_collection.cc b/src/file_collection.cc
new file mode 100644
index 0000000..19e71c2
--- /dev/null
+++ b/src/file_collection.cc
@@ -0,0 +1,649 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file file_collection.cc
+ */
+
+#include <unordered_map>
+
+#include "file_collection.hh"
+
+#include <glob.h>
+
+#include "base/humanize.network.hh"
+#include "base/isc.hh"
+#include "base/itertools.hh"
+#include "base/opt_util.hh"
+#include "base/string_util.hh"
+#include "config.h"
+#include "lnav_util.hh"
+#include "logfile.hh"
+#include "pcap_manager.hh"
+#include "service_tags.hh"
+#include "tailer/tailer.looper.hh"
+
+static std::mutex REALPATH_CACHE_MUTEX;
+static std::unordered_map<std::string, std::string> REALPATH_CACHE;
+
+child_poll_result_t
+child_poller::poll(file_collection& fc)
+{
+ if (!this->cp_child) {
+ return child_poll_result_t::FINISHED;
+ }
+
+ auto poll_res = std::move(this->cp_child.value()).poll();
+ this->cp_child = nonstd::nullopt;
+ return poll_res.match(
+ [this](auto_pid<process_state::running>& alive) {
+ this->cp_child = std::move(alive);
+ return child_poll_result_t::ALIVE;
+ },
+ [this, &fc](auto_pid<process_state::finished>& finished) {
+ require(this->cp_finalizer);
+
+ this->cp_finalizer(fc, finished);
+ return child_poll_result_t::FINISHED;
+ });
+}
+
+void
+file_collection::close_files(const std::vector<std::shared_ptr<logfile>>& files)
+{
+ for (const auto& lf : files) {
+ auto actual_path_opt = lf->get_actual_path();
+
+ if (actual_path_opt) {
+ std::lock_guard<std::mutex> lg(REALPATH_CACHE_MUTEX);
+ auto path_str = actual_path_opt.value().string();
+
+ for (auto iter = REALPATH_CACHE.begin();
+ iter != REALPATH_CACHE.end();)
+ {
+ if (iter->first == path_str || iter->second == path_str) {
+ iter = REALPATH_CACHE.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+ } else {
+ this->fc_file_names.erase(lf->get_filename());
+ }
+ auto file_iter = find(this->fc_files.begin(), this->fc_files.end(), lf);
+ if (file_iter != this->fc_files.end()) {
+ this->fc_files.erase(file_iter);
+ }
+ }
+ this->fc_files_generation += 1;
+
+ this->regenerate_unique_file_names();
+}
+
+void
+file_collection::regenerate_unique_file_names()
+{
+ unique_path_generator upg;
+
+ for (const auto& lf : this->fc_files) {
+ upg.add_source(lf);
+ }
+
+ upg.generate();
+
+ this->fc_largest_path_length = 0;
+ for (const auto& pair : this->fc_name_to_errors) {
+ auto path = ghc::filesystem::path(pair.first).filename().string();
+
+ if (path.length() > this->fc_largest_path_length) {
+ this->fc_largest_path_length = path.length();
+ }
+ }
+ for (const auto& lf : this->fc_files) {
+ const auto& path = lf->get_unique_path();
+
+ if (path.length() > this->fc_largest_path_length) {
+ this->fc_largest_path_length = path.length();
+ }
+ }
+ for (const auto& pair : this->fc_other_files) {
+ switch (pair.second.ofd_format) {
+ case file_format_t::UNKNOWN:
+ case file_format_t::ARCHIVE:
+ case file_format_t::PCAP:
+ case file_format_t::SQLITE_DB: {
+ auto bn = ghc::filesystem::path(pair.first).filename().string();
+ if (bn.length() > this->fc_largest_path_length) {
+ this->fc_largest_path_length = bn.length();
+ }
+ break;
+ }
+ case file_format_t::REMOTE: {
+ if (pair.first.length() > this->fc_largest_path_length) {
+ this->fc_largest_path_length = pair.first.length();
+ }
+ break;
+ }
+ }
+ }
+}
+
+void
+file_collection::merge(file_collection& other)
+{
+ this->fc_recursive = this->fc_recursive || other.fc_recursive;
+ this->fc_rotated = this->fc_rotated || other.fc_rotated;
+
+ this->fc_synced_files.insert(other.fc_synced_files.begin(),
+ other.fc_synced_files.end());
+ this->fc_name_to_errors.insert(other.fc_name_to_errors.begin(),
+ other.fc_name_to_errors.end());
+ this->fc_file_names.insert(
+ std::make_move_iterator(other.fc_file_names.begin()),
+ std::make_move_iterator(other.fc_file_names.end()));
+ if (!other.fc_files.empty()) {
+ for (const auto& lf : other.fc_files) {
+ this->fc_name_to_errors.erase(lf->get_filename());
+ }
+ this->fc_files.insert(
+ this->fc_files.end(), other.fc_files.begin(), other.fc_files.end());
+ this->fc_files_generation += 1;
+ }
+ for (auto& pair : other.fc_renamed_files) {
+ pair.first->set_filename(pair.second);
+ }
+ this->fc_closed_files.insert(other.fc_closed_files.begin(),
+ other.fc_closed_files.end());
+ this->fc_other_files.insert(other.fc_other_files.begin(),
+ other.fc_other_files.end());
+ if (!other.fc_child_pollers.empty()) {
+ this->fc_child_pollers.insert(
+ this->fc_child_pollers.begin(),
+ std::make_move_iterator(other.fc_child_pollers.begin()),
+ std::make_move_iterator(other.fc_child_pollers.end()));
+ other.fc_child_pollers.clear();
+ }
+}
+
+/**
+ * Functor used to compare files based on their device and inode number.
+ */
+struct same_file {
+ explicit same_file(const struct stat& stat) : sf_stat(stat){};
+
+ /**
+ * Compare the given log file against the 'stat' given in the constructor.
+ * @param lf The log file to compare.
+ * @return True if the dev/inode values in the stat given in the
+ * constructor matches the stat in the logfile object.
+ */
+ bool operator()(const std::shared_ptr<logfile>& lf) const
+ {
+ return !lf->is_closed() && this->sf_stat.st_dev == lf->get_stat().st_dev
+ && this->sf_stat.st_ino == lf->get_stat().st_ino;
+ }
+
+ const struct stat& sf_stat;
+};
+
+/**
+ * Try to load the given file as a log file. If the file has not already been
+ * loaded, it will be loaded. If the file has already been loaded, the file
+ * name will be updated.
+ *
+ * @param filename The file name to check.
+ * @param fd An already-opened descriptor for 'filename'.
+ * @param required Specifies whether or not the file must exist and be valid.
+ */
+std::future<file_collection>
+file_collection::watch_logfile(const std::string& filename,
+ logfile_open_options& loo,
+ bool required)
+{
+ file_collection retval;
+ struct stat st;
+ int rc;
+
+ if (this->fc_closed_files.count(filename)) {
+ return lnav::futures::make_ready_future(std::move(retval));
+ }
+
+ if (loo.loo_fd != -1) {
+ rc = fstat(loo.loo_fd, &st);
+ if (rc == 0) {
+ loo.with_stat_for_temp(st);
+ }
+ } else if (loo.loo_temp_file) {
+ memset(&st, 0, sizeof(st));
+ st.st_dev = loo.loo_temp_dev;
+ st.st_ino = loo.loo_temp_ino;
+ st.st_mode = S_IFREG;
+ rc = 0;
+ } else {
+ rc = stat(filename.c_str(), &st);
+ }
+
+ if (rc == 0) {
+ if (S_ISDIR(st.st_mode) && this->fc_recursive) {
+ std::string wilddir = filename + "/*";
+
+ if (this->fc_file_names.find(wilddir) == this->fc_file_names.end())
+ {
+ retval.fc_file_names.emplace(wilddir, logfile_open_options());
+ }
+ return lnav::futures::make_ready_future(std::move(retval));
+ }
+ if (!S_ISREG(st.st_mode)) {
+ if (required) {
+ rc = -1;
+ errno = EINVAL;
+ } else {
+ return lnav::futures::make_ready_future(std::move(retval));
+ }
+ }
+ auto err_iter = this->fc_name_to_errors.find(filename);
+ if (err_iter != this->fc_name_to_errors.end()) {
+ if (err_iter->second.fei_mtime != st.st_mtime) {
+ this->fc_name_to_errors.erase(err_iter);
+ }
+ }
+ }
+ if (rc == -1) {
+ if (required) {
+ retval.fc_name_to_errors.emplace(filename,
+ file_error_info{
+ time(nullptr),
+ std::string(strerror(errno)),
+ });
+ }
+ return lnav::futures::make_ready_future(std::move(retval));
+ }
+
+ if (this->fc_new_stats | lnav::itertools::find_if([&st](const auto& elem) {
+ return st.st_ino == elem.st_ino && st.st_dev == elem.st_dev;
+ }))
+ {
+ // this file is probably a link that we have already scanned in this
+ // pass.
+ return lnav::futures::make_ready_future(std::move(retval));
+ }
+
+ this->fc_new_stats.emplace_back(st);
+
+ auto file_iter = std::find_if(
+ this->fc_files.begin(), this->fc_files.end(), same_file(st));
+
+ if (file_iter == this->fc_files.end()) {
+ if (this->fc_other_files.find(filename) != this->fc_other_files.end()) {
+ return lnav::futures::make_ready_future(std::move(retval));
+ }
+
+ require(this->fc_progress.get() != nullptr);
+
+ auto func = [filename,
+ st,
+ loo2 = std::move(loo),
+ prog = this->fc_progress,
+ errs = this->fc_name_to_errors]() mutable {
+ file_collection retval;
+
+ if (errs.find(filename) != errs.end()) {
+ // The file is broken, no reason to try and reopen
+ return retval;
+ }
+
+ auto ff = loo2.loo_temp_file ? file_format_t::UNKNOWN
+ : detect_file_format(filename);
+
+ loo2.loo_file_format = ff;
+ switch (ff) {
+ case file_format_t::SQLITE_DB:
+ retval.fc_other_files[filename].ofd_format = ff;
+ break;
+
+ case file_format_t::PCAP: {
+ auto res = pcap_manager::convert(filename);
+
+ if (res.isOk()) {
+ auto convert_res = res.unwrap();
+
+ loo2.with_fd(std::move(convert_res.cr_destination));
+ retval.fc_child_pollers.emplace_back(child_poller{
+ std::move(convert_res.cr_child),
+ [filename,
+ st,
+ error_queue = convert_res.cr_error_queue](
+ auto& fc, auto& child) {
+ if (child.was_normal_exit()
+ && child.exit_status() == EXIT_SUCCESS)
+ {
+ log_info("pcap[%d] exited normally",
+ child.in());
+ return;
+ }
+ log_error("pcap[%d] exited with %d",
+ child.in(),
+ child.status());
+ fc.fc_name_to_errors.emplace(
+ filename,
+ file_error_info{
+ st.st_mtime,
+ fmt::format(
+ FMT_STRING("{}"),
+ fmt::join(*error_queue, "\n")),
+ });
+ },
+ });
+ auto open_res = logfile::open(filename, loo2);
+ if (open_res.isOk()) {
+ retval.fc_files.push_back(open_res.unwrap());
+ } else {
+ retval.fc_name_to_errors.emplace(
+ filename,
+ file_error_info{
+ st.st_mtime,
+ open_res.unwrapErr(),
+ });
+ }
+ } else {
+ retval.fc_name_to_errors.emplace(filename,
+ file_error_info{
+ st.st_mtime,
+ res.unwrapErr(),
+ });
+ }
+ break;
+ }
+
+ case file_format_t::ARCHIVE: {
+ nonstd::optional<
+ std::list<archive_manager::extract_progress>::iterator>
+ prog_iter_opt;
+
+ if (loo2.loo_source == logfile_name_source::ARCHIVE) {
+ // Don't try to open nested archives
+ return retval;
+ }
+
+ auto res = archive_manager::walk_archive_files(
+ filename,
+ [prog, &prog_iter_opt](const auto& path,
+ const auto total) {
+ safe::WriteAccess<safe_scan_progress> sp(*prog);
+
+ prog_iter_opt | [&sp](auto prog_iter) {
+ sp->sp_extractions.erase(prog_iter);
+ };
+ auto prog_iter = sp->sp_extractions.emplace(
+ sp->sp_extractions.begin(), path, total);
+ prog_iter_opt = prog_iter;
+
+ return &(*prog_iter);
+ },
+ [&filename, &retval](const auto& tmp_path,
+ const auto& entry) {
+ auto arc_path = ghc::filesystem::relative(
+ entry.path(), tmp_path);
+ auto custom_name = filename / arc_path;
+ bool is_visible = true;
+
+ if (entry.file_size() == 0) {
+ log_info("hiding empty archive file: %s",
+ entry.path().c_str());
+ is_visible = false;
+ }
+
+ log_info("adding file from archive: %s/%s",
+ filename.c_str(),
+ entry.path().c_str());
+ retval.fc_file_names[entry.path().string()]
+ .with_filename(custom_name.string())
+ .with_source(logfile_name_source::ARCHIVE)
+ .with_visibility(is_visible)
+ .with_non_utf_visibility(false)
+ .with_visible_size_limit(256 * 1024);
+ });
+ if (res.isErr()) {
+ log_error("archive extraction failed: %s",
+ res.unwrapErr().c_str());
+ retval.clear();
+ retval.fc_name_to_errors.emplace(filename,
+ file_error_info{
+ st.st_mtime,
+ res.unwrapErr(),
+ });
+ } else {
+ retval.fc_other_files[filename] = ff;
+ }
+ {
+ prog_iter_opt | [&prog](auto prog_iter) {
+ prog->writeAccess()->sp_extractions.erase(
+ prog_iter);
+ };
+ }
+ break;
+ }
+
+ default:
+ log_info("loading new file: filename=%s", filename.c_str());
+
+ auto open_res = logfile::open(filename, loo2);
+ if (open_res.isOk()) {
+ retval.fc_files.push_back(open_res.unwrap());
+ } else {
+ retval.fc_name_to_errors.emplace(
+ filename,
+ file_error_info{
+ st.st_mtime,
+ open_res.unwrapErr(),
+ });
+ }
+ break;
+ }
+
+ return retval;
+ };
+
+ return std::async(std::launch::async, std::move(func));
+ }
+
+ auto lf = *file_iter;
+
+ if (lf->is_valid_filename() && lf->get_filename() != filename) {
+ /* The file is already loaded, but has been found under a different
+ * name. We just need to update the stored file name.
+ */
+ retval.fc_renamed_files.emplace_back(lf, filename);
+ }
+
+ return lnav::futures::make_ready_future(std::move(retval));
+}
+
+/**
+ * Expand a glob pattern and call watch_logfile with the file names that match
+ * the pattern.
+ * @param path The glob pattern to expand.
+ * @param required Passed to watch_logfile.
+ */
+void
+file_collection::expand_filename(
+ lnav::futures::future_queue<file_collection>& fq,
+ const std::string& path,
+ logfile_open_options& loo,
+ bool required)
+{
+ static_root_mem<glob_t, globfree> gl;
+
+ {
+ std::lock_guard<std::mutex> lg(REALPATH_CACHE_MUTEX);
+
+ if (REALPATH_CACHE.find(path) != REALPATH_CACHE.end()) {
+ return;
+ }
+ }
+
+ if (is_url(path.c_str())) {
+ return;
+ }
+
+ if (glob(path.c_str(), GLOB_NOCHECK, nullptr, gl.inout()) == 0) {
+ int lpc;
+
+ if (gl->gl_pathc == 1 /*&& gl.gl_matchc == 0*/) {
+ /* It's a pattern that doesn't match any files
+ * yet, allow it through since we'll load it in
+ * dynamically.
+ */
+ if (access(gl->gl_pathv[0], F_OK) == -1) {
+ auto rp_opt = humanize::network::path::from_str(path);
+ if (rp_opt) {
+ auto iter = this->fc_other_files.find(path);
+ auto rp = *rp_opt;
+
+ if (iter != this->fc_other_files.end()) {
+ return;
+ }
+
+ file_collection retval;
+ logfile_open_options_base loo_base{loo};
+
+ isc::to<tailer::looper&, services::remote_tailer_t>().send(
+ [rp, loo_base](auto& tlooper) {
+ tlooper.add_remote(rp, loo_base);
+ });
+ retval.fc_other_files[path] = file_format_t::REMOTE;
+ {
+ this->fc_progress->writeAccess()
+ ->sp_tailers[fmt::to_string(rp.home())]
+ .tp_message
+ = "Initializing...";
+ }
+
+ fq.push_back(
+ lnav::futures::make_ready_future(std::move(retval)));
+ return;
+ }
+
+ required = false;
+ }
+ }
+ if (gl->gl_pathc > 1 || strcmp(path.c_str(), gl->gl_pathv[0]) != 0) {
+ required = false;
+ }
+
+ std::lock_guard<std::mutex> lg(REALPATH_CACHE_MUTEX);
+ for (lpc = 0; lpc < (int) gl->gl_pathc; lpc++) {
+ auto path_str = std::string(gl->gl_pathv[lpc]);
+ auto iter = REALPATH_CACHE.find(path_str);
+
+ if (iter == REALPATH_CACHE.end()) {
+ auto_mem<char> abspath;
+
+ if ((abspath = realpath(gl->gl_pathv[lpc], nullptr)) == nullptr)
+ {
+ auto* errmsg = strerror(errno);
+
+ if (required) {
+ fprintf(stderr,
+ "Cannot find file: %s -- %s",
+ gl->gl_pathv[lpc],
+ errmsg);
+ } else if (loo.loo_source != logfile_name_source::REMOTE) {
+ // XXX The remote code path adds the file name before
+ // the file exists... not sure checking for that here
+ // is a good idea (prolly not)
+ file_collection retval;
+
+ if (gl->gl_pathc == 1) {
+ retval.fc_name_to_errors.emplace(path,
+ file_error_info{
+ time(nullptr),
+ errmsg,
+ });
+ } else {
+ retval.fc_name_to_errors.emplace(path_str,
+ file_error_info{
+ time(nullptr),
+ errmsg,
+ });
+ }
+ fq.push_back(lnav::futures::make_ready_future(
+ std::move(retval)));
+ }
+ continue;
+ }
+
+ auto p = REALPATH_CACHE.emplace(path_str, abspath.in());
+
+ iter = p.first;
+ }
+
+ if (required || access(iter->second.c_str(), R_OK) == 0) {
+ fq.push_back(watch_logfile(iter->second, loo, required));
+ }
+ }
+ }
+}
+
+file_collection
+file_collection::rescan_files(bool required)
+{
+ file_collection retval;
+ lnav::futures::future_queue<file_collection> fq(
+ [&retval](auto& fc) { retval.merge(fc); });
+
+ for (auto& pair : this->fc_file_names) {
+ if (!pair.second.loo_temp_file) {
+ this->expand_filename(fq, pair.first, pair.second, required);
+ if (this->fc_rotated) {
+ std::string path = pair.first + ".*";
+
+ this->expand_filename(fq, path, pair.second, false);
+ }
+ } else if (pair.second.loo_fd.get() != -1) {
+ fq.push_back(watch_logfile(pair.first, pair.second, required));
+ }
+
+ if (retval.fc_files.size() >= 100) {
+ log_debug("too many new files, breaking...");
+ break;
+ }
+ }
+
+ fq.pop_to();
+
+ this->fc_new_stats.clear();
+
+ return retval;
+}
+
+void
+file_collection::request_close(const std::shared_ptr<logfile>& lf)
+{
+ lf->close();
+ this->fc_files_generation += 1;
+}
diff --git a/src/file_collection.hh b/src/file_collection.hh
new file mode 100644
index 0000000..926f8f1
--- /dev/null
+++ b/src/file_collection.hh
@@ -0,0 +1,180 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file file_collection.hh
+ */
+
+#ifndef lnav_file_collection_hh
+#define lnav_file_collection_hh
+
+#include <forward_list>
+#include <list>
+#include <map>
+#include <set>
+#include <string>
+#include <utility>
+
+#include "archive_manager.hh"
+#include "base/future_util.hh"
+#include "file_format.hh"
+#include "logfile_fwd.hh"
+#include "safe/safe.h"
+#include "tailer/tailer.looper.hh"
+
+struct tailer_progress {
+ std::string tp_message;
+};
+
+struct scan_progress {
+ std::list<archive_manager::extract_progress> sp_extractions;
+ std::map<std::string, tailer_progress> sp_tailers;
+};
+
+using safe_scan_progress = safe::Safe<scan_progress>;
+
+struct other_file_descriptor {
+ file_format_t ofd_format;
+ std::string ofd_description;
+
+ other_file_descriptor(file_format_t format = file_format_t::UNKNOWN,
+ std::string description = "")
+ : ofd_format(format), ofd_description(std::move(description))
+ {
+ }
+};
+
+struct file_error_info {
+ const time_t fei_mtime;
+ const std::string fei_description;
+};
+
+struct file_collection;
+
+enum class child_poll_result_t {
+ ALIVE,
+ FINISHED,
+};
+
+class child_poller {
+public:
+ explicit child_poller(
+ auto_pid<process_state::running> child,
+ std::function<void(file_collection&,
+ auto_pid<process_state::finished>&)> finalizer)
+ : cp_child(std::move(child)), cp_finalizer(std::move(finalizer))
+ {
+ ensure(this->cp_finalizer);
+ }
+
+ child_poller(child_poller&& other) noexcept
+ : cp_child(std::move(other.cp_child)),
+ cp_finalizer(std::move(other.cp_finalizer))
+ {
+ ensure(this->cp_finalizer);
+ }
+
+ child_poller& operator=(child_poller&& other) noexcept
+ {
+ require(other.cp_finalizer);
+
+ this->cp_child = std::move(other.cp_child);
+ this->cp_finalizer = std::move(other.cp_finalizer);
+
+ return *this;
+ }
+
+ ~child_poller() noexcept = default;
+
+ child_poller(const child_poller&) = delete;
+
+ child_poller& operator=(const child_poller&) = delete;
+
+ child_poll_result_t poll(file_collection& fc);
+
+private:
+ nonstd::optional<auto_pid<process_state::running>> cp_child;
+ std::function<void(file_collection&, auto_pid<process_state::finished>&)>
+ cp_finalizer;
+};
+
+struct file_collection {
+ bool fc_invalidate_merge{false};
+
+ bool fc_recursive{false};
+ bool fc_rotated{false};
+
+ std::map<std::string, file_error_info> fc_name_to_errors;
+ std::map<std::string, logfile_open_options> fc_file_names;
+ std::vector<std::shared_ptr<logfile>> fc_files;
+ int fc_files_generation{0};
+ std::vector<std::pair<std::shared_ptr<logfile>, std::string>>
+ fc_renamed_files;
+ std::set<std::string> fc_closed_files;
+ std::map<std::string, other_file_descriptor> fc_other_files;
+ std::set<std::string> fc_synced_files;
+ std::shared_ptr<safe_scan_progress> fc_progress;
+ std::vector<struct stat> fc_new_stats;
+ std::list<child_poller> fc_child_pollers;
+ size_t fc_largest_path_length{0};
+
+ file_collection()
+ : fc_progress(std::make_shared<safe::Safe<scan_progress>>())
+ {
+ }
+
+ void clear()
+ {
+ this->fc_name_to_errors.clear();
+ this->fc_file_names.clear();
+ this->fc_files.clear();
+ this->fc_closed_files.clear();
+ this->fc_other_files.clear();
+ this->fc_new_stats.clear();
+ }
+
+ file_collection rescan_files(bool required = false);
+
+ void expand_filename(lnav::futures::future_queue<file_collection>& fq,
+ const std::string& path,
+ logfile_open_options& loo,
+ bool required);
+
+ std::future<file_collection> watch_logfile(const std::string& filename,
+ logfile_open_options& loo,
+ bool required);
+
+ void merge(file_collection& other);
+
+ void request_close(const std::shared_ptr<logfile>& lf);
+
+ void close_files(const std::vector<std::shared_ptr<logfile>>& files);
+
+ void regenerate_unique_file_names();
+};
+
+#endif
diff --git a/src/file_format.cc b/src/file_format.cc
new file mode 100644
index 0000000..d82b78c
--- /dev/null
+++ b/src/file_format.cc
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file file_format.hh
+ */
+
+#include <unordered_map>
+
+#include "file_format.hh"
+
+#include "archive_manager.hh"
+#include "base/auto_fd.hh"
+#include "base/fs_util.hh"
+#include "base/intern_string.hh"
+#include "base/lnav_log.hh"
+#include "config.h"
+
+static bool
+is_pcap_header(uint8_t* buffer)
+{
+ size_t offset = 0;
+ if (buffer[0] == 0x0a && buffer[1] == 0x0d && buffer[2] == 0x0d
+ && buffer[3] == 0x0a)
+ {
+ offset += sizeof(uint32_t) * 2;
+ if (buffer[offset + 0] == 0x1a && buffer[offset + 1] == 0x2b
+ && buffer[offset + 2] == 0x3c && buffer[offset + 3] == 0x4d)
+ {
+ return true;
+ }
+
+ if (buffer[offset + 0] == 0x4d && buffer[offset + 1] == 0x3c
+ && buffer[offset + 2] == 0x2b && buffer[offset + 3] == 0x1a)
+ {
+ return true;
+ }
+ return false;
+ }
+
+ if (buffer[0] == 0xa1 && buffer[1] == 0xb2 && buffer[2] == 0xc3
+ && buffer[3] == 0xd4)
+ {
+ return true;
+ }
+
+ if (buffer[0] == 0xd4 && buffer[1] == 0xc3 && buffer[2] == 0xb2
+ && buffer[3] == 0xa1)
+ {
+ return true;
+ }
+
+ if (buffer[0] == 0xa1 && buffer[1] == 0xb2 && buffer[2] == 0x3c
+ && buffer[3] == 0x4d)
+ {
+ return true;
+ }
+
+ if (buffer[0] == 0x4d && buffer[1] == 0x3c && buffer[2] == 0xb2
+ && buffer[3] == 0xa1)
+ {
+ return true;
+ }
+
+ return false;
+}
+
+file_format_t
+detect_file_format(const ghc::filesystem::path& filename)
+{
+ if (archive_manager::is_archive(filename)) {
+ return file_format_t::ARCHIVE;
+ }
+
+ file_format_t retval = file_format_t::UNKNOWN;
+ auto_fd fd;
+
+ if ((fd = lnav::filesystem::openp(filename, O_RDONLY)) != -1) {
+ uint8_t buffer[32];
+ ssize_t rc;
+
+ if ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
+ static auto SQLITE3_HEADER = "SQLite format 3";
+ auto header_frag = string_fragment(buffer, 0, rc);
+
+ if (header_frag.startswith(SQLITE3_HEADER)) {
+ retval = file_format_t::SQLITE_DB;
+ } else if (rc > 24 && is_pcap_header(buffer)) {
+ retval = file_format_t::PCAP;
+ }
+ }
+ }
+
+ return retval;
+}
diff --git a/src/file_format.hh b/src/file_format.hh
new file mode 100644
index 0000000..a8eb3e4
--- /dev/null
+++ b/src/file_format.hh
@@ -0,0 +1,76 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file file_format.hh
+ */
+
+#ifndef lnav_file_format_hh
+#define lnav_file_format_hh
+
+#include "fmt/format.h"
+#include "ghc/filesystem.hpp"
+
+enum class file_format_t : int {
+ UNKNOWN,
+ SQLITE_DB,
+ ARCHIVE,
+ PCAP,
+ REMOTE,
+};
+
+file_format_t detect_file_format(const ghc::filesystem::path& filename);
+
+namespace fmt {
+template<>
+struct formatter<file_format_t> : formatter<string_view> {
+ template<typename FormatContext>
+ auto format(file_format_t ff, FormatContext& ctx)
+ {
+ string_view name = "unknown";
+ switch (ff) {
+ case file_format_t::SQLITE_DB:
+ name = "\U0001F5C2 SQLite DB";
+ break;
+ case file_format_t::ARCHIVE:
+ name = "\U0001F5C4 Archive";
+ break;
+ case file_format_t::PCAP:
+ name = "\U0001F5A5 Pcap";
+ break;
+ case file_format_t::REMOTE:
+ name = "\U0001F5A5 Remote";
+ break;
+ default:
+ break;
+ }
+ return formatter<string_view>::format(name, ctx);
+ }
+};
+} // namespace fmt
+
+#endif
diff --git a/src/file_vtab.cc b/src/file_vtab.cc
new file mode 100644
index 0000000..aab5d5b
--- /dev/null
+++ b/src/file_vtab.cc
@@ -0,0 +1,345 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <string.h>
+#include <unistd.h>
+
+#include "base/injector.bind.hh"
+#include "base/lnav.gzip.hh"
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "file_collection.hh"
+#include "file_vtab.cfg.hh"
+#include "log_format.hh"
+#include "logfile.hh"
+#include "session_data.hh"
+#include "vtab_module.hh"
+
+struct lnav_file : public tvt_iterator_cursor<lnav_file> {
+ using iterator = std::vector<std::shared_ptr<logfile>>::iterator;
+
+ static constexpr const char* NAME = "lnav_file";
+ static constexpr const char* CREATE_STMT = R"(
+-- Access lnav's open file list through this table.
+CREATE TABLE lnav_file (
+ device integer, -- The device the file is stored on.
+ inode integer, -- The inode for the file on the device.
+ filepath text, -- The path to the file.
+ mimetype text, -- The MIME type for the file.
+ content_id text, -- The hash of some unique content in the file.
+ format text, -- The log file format for the file.
+ lines integer, -- The number of lines in the file.
+ time_offset integer, -- The millisecond offset for timestamps.
+
+ content BLOB HIDDEN -- The contents of the file.
+);
+)";
+
+ explicit lnav_file(file_collection& fc) : lf_collection(fc) {}
+
+ iterator begin() { return this->lf_collection.fc_files.begin(); }
+
+ iterator end() { return this->lf_collection.fc_files.end(); }
+
+ int get_column(const cursor& vc, sqlite3_context* ctx, int col)
+ {
+ auto lf = *vc.iter;
+ const struct stat& st = lf->get_stat();
+ const auto& name = lf->get_filename();
+ auto format = lf->get_format();
+ const char* format_name = format != nullptr ? format->get_name().get()
+ : nullptr;
+
+ switch (col) {
+ case 0:
+ to_sqlite(ctx, (int64_t) st.st_dev);
+ break;
+ case 1:
+ to_sqlite(ctx, (int64_t) st.st_ino);
+ break;
+ case 2:
+ to_sqlite(ctx, name);
+ break;
+ case 3:
+ to_sqlite(ctx, fmt::to_string(lf->get_text_format()));
+ break;
+ case 4:
+ to_sqlite(
+ ctx,
+ fmt::format(FMT_STRING("v1:{}"), lf->get_content_id()));
+ break;
+ case 5:
+ to_sqlite(ctx, format_name);
+ break;
+ case 6:
+ to_sqlite(ctx, (int64_t) lf->size());
+ break;
+ case 7: {
+ auto tv = lf->get_time_offset();
+ int64_t ms = (tv.tv_sec * 1000LL) + tv.tv_usec / 1000LL;
+
+ to_sqlite(ctx, ms);
+ break;
+ }
+ case 8: {
+ auto& cfg = injector::get<const file_vtab::config&>();
+ auto lf_stat = lf->get_stat();
+
+ if (lf_stat.st_size > cfg.fvc_max_content_size) {
+ sqlite3_result_error(ctx, "file is too large", -1);
+ } else {
+ auto fd = lf->get_fd();
+ auto_mem<char> buf;
+ buf = (char*) malloc(lf_stat.st_size);
+ auto rc = pread(fd, buf, lf_stat.st_size, 0);
+
+ if (rc == -1) {
+ auto errmsg
+ = fmt::format(FMT_STRING("unable to read file: {}"),
+ strerror(errno));
+
+ sqlite3_result_error(
+ ctx, errmsg.c_str(), errmsg.length());
+ } else if (rc != lf_stat.st_size) {
+ auto errmsg = fmt::format(
+ FMT_STRING("short read of file: {} < {}"),
+ rc,
+ lf_stat.st_size);
+
+ sqlite3_result_error(
+ ctx, errmsg.c_str(), errmsg.length());
+ } else if (lnav::gzip::is_gzipped(buf, rc)) {
+ lnav::gzip::uncompress(lf->get_unique_path(), buf, rc)
+ .then([ctx](auto uncomp) {
+ auto pair = uncomp.release();
+
+ sqlite3_result_blob64(
+ ctx, pair.first, pair.second, free);
+ })
+ .otherwise([ctx](auto msg) {
+ sqlite3_result_error(
+ ctx, msg.c_str(), msg.size());
+ });
+ } else {
+ sqlite3_result_blob64(ctx, buf.release(), rc, free);
+ }
+ }
+ break;
+ }
+ default:
+ ensure(0);
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+
+ int delete_row(sqlite3_vtab* vt, sqlite3_int64 rowid)
+ {
+ vt->zErrMsg = sqlite3_mprintf("Rows cannot be deleted from this table");
+ return SQLITE_ERROR;
+ }
+
+ int insert_row(sqlite3_vtab* tab, sqlite3_int64& rowid_out)
+ {
+ tab->zErrMsg
+ = sqlite3_mprintf("Rows cannot be inserted into this table");
+ return SQLITE_ERROR;
+ }
+
+ int update_row(sqlite3_vtab* tab,
+ sqlite3_int64& rowid,
+ int64_t device,
+ int64_t inode,
+ std::string path,
+ const char* text_format,
+ const char* content_id,
+ const char* format,
+ int64_t lines,
+ int64_t time_offset,
+ const char* content)
+ {
+ auto lf = this->lf_collection.fc_files[rowid];
+ struct timeval tv = {
+ (int) (time_offset / 1000LL),
+ (int) (time_offset / (1000LL * 1000LL)),
+ };
+
+ lf->adjust_content_time(0, tv, true);
+
+ if (path != lf->get_filename()) {
+ if (lf->is_valid_filename()) {
+ throw sqlite_func_error(
+ "real file paths cannot be updated, only symbolic ones");
+ }
+
+ auto iter
+ = this->lf_collection.fc_file_names.find(lf->get_filename());
+
+ if (iter != this->lf_collection.fc_file_names.end()) {
+ auto loo = std::move(iter->second);
+
+ this->lf_collection.fc_file_names.erase(iter);
+
+ loo.loo_include_in_session = true;
+ this->lf_collection.fc_file_names[path] = std::move(loo);
+ lf->set_filename(path);
+ this->lf_collection.regenerate_unique_file_names();
+
+ init_session();
+ load_session();
+ }
+ }
+
+ return SQLITE_OK;
+ }
+
+ file_collection& lf_collection;
+};
+
+struct lnav_file_metadata {
+ static constexpr const char* NAME = "lnav_file_metadata";
+ static constexpr const char* CREATE_STMT = R"(
+-- Access the metadata embedded in open files
+CREATE TABLE lnav_file_metadata (
+ filepath text, -- The path to the file.
+ descriptor text, -- The descriptor that identifies the source of the metadata.
+ mimetype text, -- The MIME type of the metadata.
+ content text -- The metadata itself.
+);
+)";
+
+ struct cursor {
+ struct metadata_row {
+ metadata_row(std::shared_ptr<logfile> lf, std::string desc)
+ : mr_logfile(lf), mr_descriptor(std::move(desc))
+ {
+ }
+ std::shared_ptr<logfile> mr_logfile;
+ std::string mr_descriptor;
+ };
+
+ sqlite3_vtab_cursor base;
+ lnav_file_metadata& c_meta;
+ std::vector<metadata_row>::iterator c_iter;
+ std::vector<metadata_row> c_rows;
+
+ cursor(sqlite3_vtab* vt)
+ : base({vt}),
+ c_meta(((vtab_module<lnav_file_metadata>::vtab*) vt)->v_impl)
+ {
+ for (auto& lf : this->c_meta.lfm_collection.fc_files) {
+ auto& lf_meta = lf->get_embedded_metadata();
+
+ for (const auto& meta_pair : lf_meta) {
+ this->c_rows.emplace_back(lf, meta_pair.first);
+ }
+ }
+ }
+
+ ~cursor() { this->c_iter = this->c_rows.end(); }
+
+ int next()
+ {
+ if (this->c_iter != this->c_rows.end()) {
+ ++this->c_iter;
+ }
+ return SQLITE_OK;
+ }
+
+ int eof() { return this->c_iter == this->c_rows.end(); }
+
+ int reset()
+ {
+ this->c_iter = this->c_rows.begin();
+ return SQLITE_OK;
+ }
+
+ int get_rowid(sqlite3_int64& rowid_out)
+ {
+ rowid_out = this->c_iter - this->c_rows.begin();
+
+ return SQLITE_OK;
+ }
+ };
+
+ explicit lnav_file_metadata(file_collection& fc) : lfm_collection(fc) {}
+
+ int get_column(const cursor& vc, sqlite3_context* ctx, int col)
+ {
+ auto& mr = *vc.c_iter;
+
+ switch (col) {
+ case 0:
+ to_sqlite(ctx, mr.mr_logfile->get_filename());
+ break;
+ case 1:
+ to_sqlite(ctx, mr.mr_descriptor);
+ break;
+ case 2:
+ to_sqlite(
+ ctx,
+ fmt::to_string(
+ mr.mr_logfile->get_embedded_metadata()[mr.mr_descriptor]
+ .m_format));
+ break;
+ case 3:
+ to_sqlite(
+ ctx,
+ fmt::to_string(
+ mr.mr_logfile->get_embedded_metadata()[mr.mr_descriptor]
+ .m_value));
+ break;
+ default:
+ ensure(0);
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+
+ file_collection& lfm_collection;
+};
+
+struct injectable_lnav_file : vtab_module<lnav_file> {
+ using vtab_module<lnav_file>::vtab_module;
+ using injectable = injectable_lnav_file(file_collection&);
+};
+
+struct injectable_lnav_file_metadata
+ : vtab_module<tvt_no_update<lnav_file_metadata>> {
+ using vtab_module<tvt_no_update<lnav_file_metadata>>::vtab_module;
+ using injectable = injectable_lnav_file_metadata(file_collection&);
+};
+
+static auto file_binder
+ = injector::bind_multiple<vtab_module_base>().add<injectable_lnav_file>();
+
+static auto file_meta_binder = injector::bind_multiple<vtab_module_base>()
+ .add<injectable_lnav_file_metadata>();
diff --git a/src/file_vtab.cfg.hh b/src/file_vtab.cfg.hh
new file mode 100644
index 0000000..8e99b20
--- /dev/null
+++ b/src/file_vtab.cfg.hh
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file file_vtab.cfg.hh
+ */
+
+#ifndef lnav_file_vtab_cfg_hh
+#define lnav_file_vtab_cfg_hh
+
+namespace file_vtab {
+
+struct config {
+ int64_t fvc_max_content_size{32 * 1024 * 1024};
+};
+
+} // namespace file_vtab
+
+#endif
diff --git a/src/files_sub_source.cc b/src/files_sub_source.cc
new file mode 100644
index 0000000..03d0303
--- /dev/null
+++ b/src/files_sub_source.cc
@@ -0,0 +1,432 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "files_sub_source.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/humanize.hh"
+#include "base/humanize.network.hh"
+#include "base/opt_util.hh"
+#include "base/string_util.hh"
+#include "config.h"
+#include "lnav.hh"
+#include "mapbox/variant.hpp"
+
+namespace files_model {
+files_list_selection
+from_selection(vis_line_t sel_vis)
+{
+ auto& fc = lnav_data.ld_active_files;
+ int sel = (int) sel_vis;
+
+ if (sel < fc.fc_name_to_errors.size()) {
+ auto iter = fc.fc_name_to_errors.begin();
+
+ std::advance(iter, sel);
+ return error_selection::build(sel, iter);
+ }
+
+ sel -= fc.fc_name_to_errors.size();
+
+ if (sel < fc.fc_other_files.size()) {
+ auto iter = fc.fc_other_files.begin();
+
+ std::advance(iter, sel);
+ return other_selection::build(sel, iter);
+ }
+
+ sel -= fc.fc_other_files.size();
+
+ if (sel < fc.fc_files.size()) {
+ auto iter = fc.fc_files.begin();
+
+ std::advance(iter, sel);
+ return file_selection::build(sel, iter);
+ }
+
+ return no_selection{};
+}
+} // namespace files_model
+
+files_sub_source::files_sub_source() {}
+
+bool
+files_sub_source::list_input_handle_key(listview_curses& lv, int ch)
+{
+ switch (ch) {
+ case KEY_ENTER:
+ case '\r': {
+ auto sel = files_model::from_selection(lv.get_selection());
+
+ sel.match(
+ [](files_model::no_selection) {},
+ [](files_model::error_selection) {},
+ [](files_model::other_selection) {},
+ [&](files_model::file_selection& fs) {
+ auto& lss = lnav_data.ld_log_source;
+ auto lf = *fs.sb_iter;
+
+ lss.find_data(lf) | [](auto ld) {
+ ld->set_visibility(true);
+ lnav_data.ld_log_source.text_filters_changed();
+ };
+
+ if (lf->get_format() != nullptr) {
+ auto& log_view = lnav_data.ld_views[LNV_LOG];
+ lss.row_for_time(lf->front().get_timeval()) |
+ [](auto row) {
+ lnav_data.ld_views[LNV_LOG].set_selection(row);
+ };
+ ensure_view(&log_view);
+ } else {
+ auto& tv = lnav_data.ld_views[LNV_TEXT];
+ auto& tss = lnav_data.ld_text_source;
+
+ tss.to_front(lf);
+ tv.reload_data();
+ ensure_view(&tv);
+ }
+
+ lv.reload_data();
+ lnav_data.ld_mode = ln_mode_t::PAGING;
+ });
+
+ return true;
+ }
+
+ case ' ': {
+ auto sel = files_model::from_selection(lv.get_selection());
+
+ sel.match([](files_model::no_selection) {},
+ [](files_model::error_selection) {},
+ [](files_model::other_selection) {},
+ [&](files_model::file_selection& fs) {
+ auto& lss = lnav_data.ld_log_source;
+ auto lf = *fs.sb_iter;
+
+ lss.find_data(lf) | [](auto ld) {
+ ld->set_visibility(!ld->ld_visible);
+ };
+
+ auto top_view = *lnav_data.ld_view_stack.top();
+ auto tss = top_view->get_sub_source();
+
+ if (tss != nullptr) {
+ tss->text_filters_changed();
+ top_view->reload_data();
+ }
+
+ lv.reload_data();
+ });
+ return true;
+ }
+ case 'n': {
+ execute_command(lnav_data.ld_exec_context, "next-mark search");
+ return true;
+ }
+ case 'N': {
+ execute_command(lnav_data.ld_exec_context, "prev-mark search");
+ return true;
+ }
+ case '/': {
+ execute_command(lnav_data.ld_exec_context, "prompt search-files");
+ return true;
+ }
+ case 'X': {
+ auto sel = files_model::from_selection(lv.get_selection());
+
+ sel.match(
+ [](files_model::no_selection) {},
+ [&](files_model::error_selection& es) {
+ auto& fc = lnav_data.ld_active_files;
+
+ fc.fc_file_names.erase(es.sb_iter->first);
+
+ auto name_iter = fc.fc_file_names.begin();
+ while (name_iter != fc.fc_file_names.end()) {
+ if (name_iter->first == es.sb_iter->first) {
+ name_iter = fc.fc_file_names.erase(name_iter);
+ continue;
+ }
+
+ auto rp_opt = humanize::network::path::from_str(
+ name_iter->first);
+
+ if (rp_opt) {
+ auto rp = *rp_opt;
+
+ if (fmt::to_string(rp.home()) == es.sb_iter->first)
+ {
+ fc.fc_other_files.erase(name_iter->first);
+ name_iter = fc.fc_file_names.erase(name_iter);
+ continue;
+ }
+ }
+ ++name_iter;
+ }
+
+ fc.fc_name_to_errors.erase(es.sb_iter);
+ fc.fc_invalidate_merge = true;
+ lv.reload_data();
+ },
+ [](files_model::other_selection) {},
+ [](files_model::file_selection) {});
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+files_sub_source::list_input_handle_scroll_out(listview_curses& lv)
+{
+ lnav_data.ld_mode = ln_mode_t::PAGING;
+ lnav_data.ld_filter_view.reload_data();
+}
+
+size_t
+files_sub_source::text_line_count()
+{
+ const auto& fc = lnav_data.ld_active_files;
+
+ return fc.fc_name_to_errors.size() + fc.fc_other_files.size()
+ + fc.fc_files.size();
+}
+
+size_t
+files_sub_source::text_line_width(textview_curses& curses)
+{
+ return 512;
+}
+
+void
+files_sub_source::text_value_for_line(textview_curses& tc,
+ int line,
+ std::string& value_out,
+ text_sub_source::line_flags_t flags)
+{
+ const auto dim = tc.get_dimensions();
+ const auto& fc = lnav_data.ld_active_files;
+ auto filename_width
+ = std::min(fc.fc_largest_path_length,
+ std::max((size_t) 40, (size_t) dim.second - 30));
+
+ if (line < fc.fc_name_to_errors.size()) {
+ auto iter = fc.fc_name_to_errors.begin();
+ std::advance(iter, line);
+ auto path = ghc::filesystem::path(iter->first);
+ auto fn = path.filename().string();
+
+ truncate_to(fn, filename_width);
+ value_out = fmt::format(FMT_STRING(" {:<{}} {}"),
+ fn,
+ filename_width,
+ iter->second.fei_description);
+ return;
+ }
+
+ line -= fc.fc_name_to_errors.size();
+
+ if (line < fc.fc_other_files.size()) {
+ auto iter = fc.fc_other_files.begin();
+ std::advance(iter, line);
+ auto path = ghc::filesystem::path(iter->first);
+ auto fn = path.string();
+
+ truncate_to(fn, filename_width);
+ value_out = fmt::format(FMT_STRING(" {:<{}} {:14} {}"),
+ fn,
+ filename_width,
+ iter->second.ofd_format,
+ iter->second.ofd_description);
+ return;
+ }
+
+ line -= fc.fc_other_files.size();
+
+ const auto& lf = fc.fc_files[line];
+ auto fn = lf->get_unique_path();
+ char start_time[64] = "", end_time[64] = "";
+ std::vector<std::string> file_notes;
+
+ if (lf->get_format() != nullptr) {
+ sql_strftime(start_time, sizeof(start_time), lf->front().get_timeval());
+ sql_strftime(end_time, sizeof(end_time), lf->back().get_timeval());
+ }
+ truncate_to(fn, filename_width);
+ for (const auto& pair : lf->get_notes()) {
+ file_notes.push_back(pair.second);
+ }
+ value_out = fmt::format(FMT_STRING(" {:<{}} {:>8} {} \u2014 {} {}"),
+ fn,
+ filename_width,
+ humanize::file_size(lf->get_index_size(),
+ humanize::alignment::columnar),
+ start_time,
+ end_time,
+ fmt::join(file_notes, "; "));
+ this->fss_last_line_len
+ = filename_width + 23 + strlen(start_time) + strlen(end_time);
+}
+
+void
+files_sub_source::text_attrs_for_line(textview_curses& tc,
+ int line,
+ string_attrs_t& value_out)
+{
+ bool selected
+ = lnav_data.ld_mode == ln_mode_t::FILES && line == tc.get_selection();
+ const auto& fc = lnav_data.ld_active_files;
+ const auto dim = tc.get_dimensions();
+ auto filename_width
+ = std::min(fc.fc_largest_path_length,
+ std::max((size_t) 40, (size_t) dim.second - 30));
+
+ if (selected) {
+ value_out.emplace_back(line_range{0, 1}, VC_GRAPHIC.value(ACS_RARROW));
+ }
+
+ if (line < fc.fc_name_to_errors.size()) {
+ if (selected) {
+ value_out.emplace_back(line_range{0, -1},
+ VC_ROLE.value(role_t::VCR_DISABLED_FOCUSED));
+ }
+
+ value_out.emplace_back(line_range{4 + (int) filename_width, -1},
+ VC_ROLE_FG.value(role_t::VCR_ERROR));
+ return;
+ }
+ line -= fc.fc_name_to_errors.size();
+
+ if (line < fc.fc_other_files.size()) {
+ if (selected) {
+ value_out.emplace_back(line_range{0, -1},
+ VC_ROLE.value(role_t::VCR_DISABLED_FOCUSED));
+ }
+ if (line == fc.fc_other_files.size() - 1) {
+ value_out.emplace_back(line_range{0, -1},
+ VC_STYLE.value(text_attrs{A_UNDERLINE}));
+ }
+ return;
+ }
+
+ line -= fc.fc_other_files.size();
+
+ if (selected) {
+ value_out.emplace_back(line_range{0, -1},
+ VC_ROLE.value(role_t::VCR_FOCUSED));
+ }
+
+ auto& lss = lnav_data.ld_log_source;
+ auto& lf = fc.fc_files[line];
+ auto ld_opt = lss.find_data(lf);
+
+ chtype visible = ACS_DIAMOND;
+ if (ld_opt && !ld_opt.value()->ld_visible) {
+ visible = ' ';
+ }
+ value_out.emplace_back(line_range{2, 3}, VC_GRAPHIC.value(visible));
+ if (visible == ACS_DIAMOND) {
+ value_out.emplace_back(line_range{2, 3},
+ VC_FOREGROUND.value(COLOR_GREEN));
+ }
+
+ auto lr = line_range{
+ (int) filename_width + 3 + 4,
+ (int) filename_width + 3 + 10,
+ };
+ value_out.emplace_back(lr, VC_STYLE.value(text_attrs{A_BOLD}));
+
+ lr.lr_start = this->fss_last_line_len;
+ lr.lr_end = -1;
+ value_out.emplace_back(lr, VC_FOREGROUND.value(COLOR_YELLOW));
+}
+
+size_t
+files_sub_source::text_size_for_line(textview_curses& tc,
+ int line,
+ text_sub_source::line_flags_t raw)
+{
+ return 0;
+}
+
+static auto
+spinner_index()
+{
+ auto now = ui_clock::now();
+
+ return std::chrono::duration_cast<std::chrono::milliseconds>(
+ now.time_since_epoch())
+ .count()
+ / 100;
+}
+
+bool
+files_overlay_source::list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t line,
+ attr_line_t& value_out)
+{
+ if (y == 0) {
+ static const char PROG[] = "-\\|/";
+ constexpr size_t PROG_SIZE = sizeof(PROG) - 1;
+
+ auto& fc = lnav_data.ld_active_files;
+ auto fc_prog = fc.fc_progress;
+ safe::WriteAccess<safe_scan_progress> sp(*fc_prog);
+
+ if (!sp->sp_extractions.empty()) {
+ const auto& prog = sp->sp_extractions.front();
+
+ value_out.with_ansi_string(fmt::format(
+ "{} Extracting " ANSI_COLOR(COLOR_CYAN) "{}" ANSI_NORM
+ "... {:>8}/{}",
+ PROG[spinner_index() % PROG_SIZE],
+ prog.ep_path.filename().string(),
+ humanize::file_size(prog.ep_out_size,
+ humanize::alignment::none),
+ humanize::file_size(prog.ep_total_size,
+ humanize::alignment::none)));
+ return true;
+ }
+ if (!sp->sp_tailers.empty()) {
+ auto first_iter = sp->sp_tailers.begin();
+
+ value_out.with_ansi_string(fmt::format(
+ "{} Connecting to " ANSI_COLOR(COLOR_CYAN) "{}" ANSI_NORM
+ ": {}",
+ PROG[spinner_index() % PROG_SIZE],
+ first_iter->first,
+ first_iter->second.tp_message));
+ return true;
+ }
+ }
+ return false;
+}
diff --git a/src/files_sub_source.hh b/src/files_sub_source.hh
new file mode 100644
index 0000000..7c3b325
--- /dev/null
+++ b/src/files_sub_source.hh
@@ -0,0 +1,117 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef files_sub_source_hh
+#define files_sub_source_hh
+
+#include "file_collection.hh"
+#include "textview_curses.hh"
+
+class files_sub_source
+ : public text_sub_source
+ , public list_input_delegate {
+public:
+ files_sub_source();
+
+ bool list_input_handle_key(listview_curses& lv, int ch) override;
+
+ void list_input_handle_scroll_out(listview_curses& lv) override;
+
+ size_t text_line_count() override;
+
+ size_t text_line_width(textview_curses& curses) override;
+
+ void text_value_for_line(textview_curses& tc,
+ int line,
+ std::string& value_out,
+ line_flags_t flags) override;
+
+ void text_attrs_for_line(textview_curses& tc,
+ int line,
+ string_attrs_t& value_out) override;
+
+ size_t text_size_for_line(textview_curses& tc,
+ int line,
+ line_flags_t raw) override;
+
+ size_t fss_last_line_len{0};
+};
+
+struct files_overlay_source : public list_overlay_source {
+ bool list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t line,
+ attr_line_t& value_out) override;
+};
+
+namespace files_model {
+
+struct no_selection {
+};
+
+template<typename C, typename T>
+struct selection_base {
+ int sb_index{0};
+ T sb_iter;
+
+ static C build(int index, T iter)
+ {
+ C retval;
+
+ retval.sb_index = index;
+ retval.sb_iter = iter;
+ return retval;
+ }
+};
+
+struct error_selection
+ : public selection_base<error_selection,
+ std::map<std::string, file_error_info>::iterator> {
+};
+
+struct other_selection
+ : public selection_base<
+ other_selection,
+ std::map<std::string, other_file_descriptor>::iterator> {
+};
+
+struct file_selection
+ : public selection_base<file_selection,
+ std::vector<std::shared_ptr<logfile>>::iterator> {
+};
+
+using files_list_selection = mapbox::util::
+ variant<no_selection, error_selection, other_selection, file_selection>;
+
+files_list_selection from_selection(vis_line_t sel_vis);
+
+} // namespace files_model
+
+#endif
diff --git a/src/filter_observer.cc b/src/filter_observer.cc
new file mode 100644
index 0000000..cbe1c8f
--- /dev/null
+++ b/src/filter_observer.cc
@@ -0,0 +1,113 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "filter_observer.hh"
+
+#include "config.h"
+#include "log_format.hh"
+
+void
+line_filter_observer::logline_new_lines(const logfile& lf,
+ logfile::const_iterator ll_begin,
+ logfile::const_iterator ll_end,
+ const shared_buffer_ref& sbr)
+{
+ size_t offset = std::distance(lf.begin(), ll_begin);
+
+ require(&lf == this->lfo_filter_state.tfs_logfile.get());
+
+ this->lfo_filter_state.resize(lf.size());
+ if (this->lfo_filter_stack.empty()) {
+ return;
+ }
+
+ for (; ll_begin != ll_end; ++ll_begin) {
+ auto sbr_copy = sbr;
+ if (lf.get_format() != nullptr) {
+ lf.get_format()->get_subline(*ll_begin, sbr_copy);
+ }
+ sbr_copy.erase_ansi();
+ for (auto& filter : this->lfo_filter_stack) {
+ if (filter->lf_deleted) {
+ continue;
+ }
+ if (offset
+ >= this->lfo_filter_state.tfs_filter_count[filter->get_index()])
+ {
+ filter->add_line(this->lfo_filter_state, ll_begin, sbr_copy);
+ }
+ }
+ }
+}
+
+void
+line_filter_observer::logline_eof(const logfile& lf)
+{
+ for (auto& iter : this->lfo_filter_stack) {
+ if (iter->lf_deleted) {
+ continue;
+ }
+ iter->end_of_message(this->lfo_filter_state);
+ }
+}
+
+size_t
+line_filter_observer::get_min_count(size_t max) const
+{
+ size_t retval = max;
+
+ for (auto& filter : this->lfo_filter_stack) {
+ if (filter->lf_deleted) {
+ continue;
+ }
+ retval = std::min(
+ retval,
+ this->lfo_filter_state.tfs_filter_count[filter->get_index()]);
+ }
+
+ return retval;
+}
+
+void
+line_filter_observer::clear_deleted_filter_state()
+{
+ uint32_t used_mask = 0;
+
+ for (auto& filter : this->lfo_filter_stack) {
+ if (filter->lf_deleted) {
+ log_debug("skipping deleted %p %d %d",
+ filter.get(),
+ filter->get_index(),
+ filter->get_lang());
+ continue;
+ }
+ used_mask |= (1UL << filter->get_index());
+ }
+ this->lfo_filter_state.clear_deleted_filter_state(used_mask);
+}
diff --git a/src/filter_observer.hh b/src/filter_observer.hh
new file mode 100644
index 0000000..1e1c302
--- /dev/null
+++ b/src/filter_observer.hh
@@ -0,0 +1,78 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef filter_observer_hh
+#define filter_observer_hh
+
+#include <sys/types.h>
+
+#include "logfile.hh"
+#include "textview_curses.hh"
+
+class line_filter_observer : public logline_observer {
+public:
+ line_filter_observer(filter_stack& fs, std::shared_ptr<logfile> lf)
+ : lfo_filter_stack(fs), lfo_filter_state(lf)
+ {
+ }
+
+ void logline_restart(const logfile& lf, file_size_t rollback_size) override
+ {
+ for (auto& filter : this->lfo_filter_stack) {
+ filter->revert_to_last(this->lfo_filter_state, rollback_size);
+ }
+ }
+
+ void logline_new_lines(const logfile& lf,
+ logfile::const_iterator ll_begin,
+ logfile::const_iterator ll_end,
+ const shared_buffer_ref& sbr) override;
+
+ void logline_eof(const logfile& lf) override;
+
+ bool excluded(uint32_t filter_in_mask,
+ uint32_t filter_out_mask,
+ size_t offset) const
+ {
+ bool filtered_in = (filter_in_mask == 0)
+ || (this->lfo_filter_state.tfs_mask[offset] & filter_in_mask) != 0;
+ bool filtered_out
+ = (this->lfo_filter_state.tfs_mask[offset] & filter_out_mask) != 0;
+ return !filtered_in || filtered_out;
+ }
+
+ size_t get_min_count(size_t max) const;
+
+ void clear_deleted_filter_state();
+
+ filter_stack& lfo_filter_stack;
+ logfile_filter_state lfo_filter_state;
+};
+
+#endif
diff --git a/src/filter_status_source.cc b/src/filter_status_source.cc
new file mode 100644
index 0000000..cbc56c0
--- /dev/null
+++ b/src/filter_status_source.cc
@@ -0,0 +1,332 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "filter_status_source.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/opt_util.hh"
+#include "config.h"
+#include "files_sub_source.hh"
+#include "filter_sub_source.hh"
+#include "lnav.hh"
+
+static auto TOGGLE_MSG = "Press " ANSI_BOLD("TAB") " to edit ";
+static auto EXIT_MSG = "Press " ANSI_BOLD("q") " to exit ";
+
+static auto CREATE_HELP = ANSI_BOLD("i") "/" ANSI_BOLD("o") ": Create in/out";
+static auto ENABLE_HELP = ANSI_BOLD("SPC") ": ";
+static auto EDIT_HELP = ANSI_BOLD("ENTER") ": Edit";
+static auto TOGGLE_HELP = ANSI_BOLD("t") ": To ";
+static auto DELETE_HELP = ANSI_BOLD("D") ": Delete";
+static auto FILTERING_HELP = ANSI_BOLD("f") ": ";
+static auto JUMP_HELP = ANSI_BOLD("ENTER") ": Jump To";
+static auto CLOSE_HELP = ANSI_BOLD("X") ": Close";
+
+filter_status_source::filter_status_source()
+{
+ this->tss_fields[TSF_TITLE].set_width(14);
+ this->tss_fields[TSF_TITLE].set_role(role_t::VCR_STATUS_TITLE);
+ this->tss_fields[TSF_TITLE].set_value(" " ANSI_ROLE("T") "ext Filters ",
+ role_t::VCR_STATUS_TITLE_HOTKEY);
+
+ this->tss_fields[TSF_STITCH_TITLE].set_width(2);
+ this->tss_fields[TSF_STITCH_TITLE].set_stitch_value(
+ role_t::VCR_STATUS_STITCH_TITLE_TO_NORMAL,
+ role_t::VCR_STATUS_STITCH_NORMAL_TO_TITLE);
+
+ this->tss_fields[TSF_COUNT].set_min_width(16);
+ this->tss_fields[TSF_COUNT].set_share(1);
+ this->tss_fields[TSF_COUNT].set_role(role_t::VCR_STATUS);
+
+ this->tss_fields[TSF_FILTERED].set_min_width(20);
+ this->tss_fields[TSF_FILTERED].set_share(1);
+ this->tss_fields[TSF_FILTERED].set_role(role_t::VCR_STATUS);
+
+ this->tss_fields[TSF_FILES_TITLE].set_width(7);
+ this->tss_fields[TSF_FILES_TITLE].set_role(
+ role_t::VCR_STATUS_DISABLED_TITLE);
+ this->tss_fields[TSF_FILES_TITLE].set_value(" " ANSI_ROLE("F") "iles ",
+ role_t::VCR_STATUS_HOTKEY);
+
+ this->tss_fields[TSF_FILES_RIGHT_STITCH].set_width(2);
+ this->tss_fields[TSF_FILES_RIGHT_STITCH].set_stitch_value(
+ role_t::VCR_STATUS, role_t::VCR_STATUS);
+
+ this->tss_fields[TSF_HELP].right_justify(true);
+ this->tss_fields[TSF_HELP].set_width(20);
+ this->tss_fields[TSF_HELP].set_value(TOGGLE_MSG);
+ this->tss_fields[TSF_HELP].set_left_pad(1);
+
+ this->tss_error.set_min_width(20);
+ this->tss_error.set_share(1);
+ this->tss_error.set_role(role_t::VCR_ALERT_STATUS);
+}
+
+size_t
+filter_status_source::statusview_fields()
+{
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::SEARCH_FILTERS:
+ case ln_mode_t::SEARCH_FILES:
+ this->tss_fields[TSF_HELP].set_value("");
+ break;
+ case ln_mode_t::FILTER:
+ case ln_mode_t::FILES:
+ this->tss_fields[TSF_HELP].set_value(EXIT_MSG);
+ break;
+ default:
+ this->tss_fields[TSF_HELP].set_value(TOGGLE_MSG);
+ break;
+ }
+
+ if (lnav_data.ld_mode == ln_mode_t::FILES
+ || lnav_data.ld_mode == ln_mode_t::SEARCH_FILES)
+ {
+ this->tss_fields[TSF_FILES_TITLE].set_value(
+ " " ANSI_ROLE("F") "iles ", role_t::VCR_STATUS_TITLE_HOTKEY);
+ this->tss_fields[TSF_FILES_TITLE].set_role(role_t::VCR_STATUS_TITLE);
+ this->tss_fields[TSF_FILES_RIGHT_STITCH].set_stitch_value(
+ role_t::VCR_STATUS_STITCH_TITLE_TO_NORMAL,
+ role_t::VCR_STATUS_STITCH_NORMAL_TO_TITLE);
+ this->tss_fields[TSF_TITLE].set_value(" " ANSI_ROLE("T") "ext Filters ",
+ role_t::VCR_STATUS_HOTKEY);
+ this->tss_fields[TSF_TITLE].set_role(role_t::VCR_STATUS_DISABLED_TITLE);
+ this->tss_fields[TSF_STITCH_TITLE].set_stitch_value(role_t::VCR_STATUS,
+ role_t::VCR_STATUS);
+ } else {
+ this->tss_fields[TSF_FILES_TITLE].set_value(" " ANSI_ROLE("F") "iles ",
+ role_t::VCR_STATUS_HOTKEY);
+ if (lnav_data.ld_active_files.fc_name_to_errors.empty()) {
+ this->tss_fields[TSF_FILES_TITLE].set_role(
+ role_t::VCR_STATUS_DISABLED_TITLE);
+ } else {
+ this->tss_fields[TSF_FILES_TITLE].set_role(
+ role_t::VCR_ALERT_STATUS);
+
+ auto& fc = lnav_data.ld_active_files;
+ if (fc.fc_name_to_errors.size() == 1) {
+ this->tss_error.set_value(" error: a file cannot be opened ");
+ } else {
+ this->tss_error.set_value(
+ " error: %u files cannot be opened ",
+ lnav_data.ld_active_files.fc_name_to_errors.size());
+ }
+ }
+ this->tss_fields[TSF_FILES_RIGHT_STITCH].set_stitch_value(
+ role_t::VCR_STATUS_STITCH_NORMAL_TO_TITLE,
+ role_t::VCR_STATUS_STITCH_TITLE_TO_NORMAL);
+ this->tss_fields[TSF_TITLE].set_value(" " ANSI_ROLE("T") "ext Filters ",
+ role_t::VCR_STATUS_TITLE_HOTKEY);
+ this->tss_fields[TSF_TITLE].set_role(role_t::VCR_STATUS_TITLE);
+ this->tss_fields[TSF_STITCH_TITLE].set_stitch_value(
+ role_t::VCR_STATUS_STITCH_TITLE_TO_NORMAL,
+ role_t::VCR_STATUS_STITCH_NORMAL_TO_TITLE);
+ }
+
+ lnav_data.ld_view_stack.top() | [this](auto tc) {
+ text_sub_source* tss = tc->get_sub_source();
+ if (tss == nullptr) {
+ return;
+ }
+
+ filter_stack& fs = tss->get_filters();
+ auto enabled_count = 0, filter_count = 0;
+
+ for (const auto& tf : fs) {
+ if (tf->is_enabled()) {
+ enabled_count += 1;
+ }
+ filter_count += 1;
+ }
+ if (filter_count == 0) {
+ this->tss_fields[TSF_COUNT].set_value("");
+ } else {
+ this->tss_fields[TSF_COUNT].set_value(
+ " " ANSI_BOLD("%d") " of " ANSI_BOLD("%d") " enabled ",
+ enabled_count,
+ filter_count);
+ }
+ };
+
+ return TSF__MAX;
+}
+
+status_field&
+filter_status_source::statusview_value_for_field(int field)
+{
+ if (field == TSF_FILTERED
+ && !lnav_data.ld_active_files.fc_name_to_errors.empty())
+ {
+ return this->tss_error;
+ }
+
+ return this->tss_fields[field];
+}
+
+void
+filter_status_source::update_filtered(text_sub_source* tss)
+{
+ if (tss == nullptr) {
+ return;
+ }
+
+ auto& sf = this->tss_fields[TSF_FILTERED];
+
+ if (tss->get_filtered_count() == 0) {
+ if (tss->tss_apply_filters) {
+ sf.clear();
+ } else {
+ sf.set_value(
+ " \u2718 Filtering disabled, re-enable with " ANSI_BOLD_START
+ ":toggle-filtering" ANSI_NORM);
+ }
+ } else {
+ auto& timer = ui_periodic_timer::singleton();
+ auto& al = sf.get_value();
+
+ if (tss->get_filtered_count() == this->bss_last_filtered_count) {
+ if (timer.fade_diff(this->bss_filter_counter) == 0) {
+ this->tss_fields[TSF_FILTERED].set_role(role_t::VCR_STATUS);
+ al.with_attr(string_attr(line_range{0, -1},
+ VC_STYLE.value(text_attrs{A_BOLD})));
+ }
+ } else {
+ this->tss_fields[TSF_FILTERED].set_role(role_t::VCR_ALERT_STATUS);
+ this->bss_last_filtered_count = tss->get_filtered_count();
+ timer.start_fade(this->bss_filter_counter, 3);
+ }
+ sf.set_value("%'9d Lines not shown ", tss->get_filtered_count());
+ }
+}
+
+filter_help_status_source::filter_help_status_source()
+{
+ this->fss_help.set_min_width(10);
+ this->fss_help.set_share(1);
+ this->fss_prompt.set_left_pad(1);
+ this->fss_prompt.set_min_width(35);
+ this->fss_prompt.set_share(1);
+ this->fss_error.set_left_pad(25);
+ this->fss_error.set_min_width(35);
+ this->fss_error.set_share(1);
+}
+
+size_t
+filter_help_status_source::statusview_fields()
+{
+ lnav_data.ld_view_stack.top() | [this](auto tc) {
+ text_sub_source* tss = tc->get_sub_source();
+ if (tss == nullptr) {
+ return;
+ }
+
+ if (lnav_data.ld_mode == ln_mode_t::FILTER) {
+ static auto* editor = injector::get<filter_sub_source*>();
+ auto& lv = lnav_data.ld_filter_view;
+ auto& fs = tss->get_filters();
+
+ if (editor->fss_editing) {
+ auto tf = *(fs.begin() + lv.get_selection());
+ auto lang = tf->get_lang() == filter_lang_t::SQL ? "an SQL"
+ : "a regular";
+
+ if (tf->get_type() == text_filter::type_t::INCLUDE) {
+ this->fss_help.set_value(
+ " "
+ "Enter %s expression to match lines to filter in:",
+ lang);
+ } else {
+ this->fss_help.set_value(
+ " "
+ "Enter %s expression to match lines to filter out:",
+ lang);
+ }
+ } else if (fs.empty()) {
+ this->fss_help.set_value(" %s", CREATE_HELP);
+ } else {
+ auto tf = *(fs.begin() + lv.get_selection());
+
+ this->fss_help.set_value(
+ " %s %s%s %s %s%s %s %s%s",
+ CREATE_HELP,
+ ENABLE_HELP,
+ tf->is_enabled() ? "Disable" : "Enable ",
+ EDIT_HELP,
+ TOGGLE_HELP,
+ tf->get_type() == text_filter::type_t::INCLUDE ? "OUT"
+ : "IN ",
+ DELETE_HELP,
+ FILTERING_HELP,
+ tss->tss_apply_filters ? "Disable Filtering"
+ : "Enable Filtering");
+ }
+ } else if (lnav_data.ld_mode == ln_mode_t::FILES
+ && lnav_data.ld_session_loaded)
+ {
+ auto& lv = lnav_data.ld_files_view;
+ auto sel = files_model::from_selection(lv.get_selection());
+
+ sel.match(
+ [this](files_model::no_selection) { this->fss_help.clear(); },
+ [this](files_model::error_selection) {
+ this->fss_help.set_value(" %s", CLOSE_HELP);
+ },
+ [this](files_model::other_selection) {
+ this->fss_help.clear();
+ },
+ [this](files_model::file_selection& fs) {
+ auto& lss = lnav_data.ld_log_source;
+ auto vis_help = "Hide";
+ auto ld_opt = lss.find_data(*fs.sb_iter);
+ if (ld_opt && !ld_opt.value()->ld_visible) {
+ vis_help = "Show";
+ }
+
+ this->fss_help.set_value(
+ " %s%s %s", ENABLE_HELP, vis_help, JUMP_HELP);
+ });
+ }
+ };
+
+ return 1;
+}
+
+status_field&
+filter_help_status_source::statusview_value_for_field(int field)
+{
+ if (!this->fss_error.empty()) {
+ return this->fss_error;
+ }
+
+ if (!this->fss_prompt.empty()) {
+ return this->fss_prompt;
+ }
+
+ return this->fss_help;
+}
diff --git a/src/filter_status_source.hh b/src/filter_status_source.hh
new file mode 100644
index 0000000..7d62966
--- /dev/null
+++ b/src/filter_status_source.hh
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_filter_status_source_hh
+#define lnav_filter_status_source_hh
+
+#include <string>
+
+#include "statusview_curses.hh"
+#include "textview_curses.hh"
+
+class filter_status_source : public status_data_source {
+public:
+ typedef enum {
+ TSF_FILES_TITLE,
+ TSF_FILES_RIGHT_STITCH,
+ TSF_TITLE,
+ TSF_STITCH_TITLE,
+ TSF_COUNT,
+ TSF_FILTERED,
+ TSF_HELP,
+
+ TSF__MAX
+ } field_t;
+
+ filter_status_source();
+
+ size_t statusview_fields() override;
+
+ status_field& statusview_value_for_field(int field) override;
+
+ void update_filtered(text_sub_source* tss);
+
+private:
+ status_field tss_fields[TSF__MAX];
+ status_field tss_error;
+ int bss_last_filtered_count{0};
+ sig_atomic_t bss_filter_counter{0};
+};
+
+class filter_help_status_source : public status_data_source {
+public:
+ filter_help_status_source();
+
+ size_t statusview_fields() override;
+
+ status_field& statusview_value_for_field(int field) override;
+
+ status_field fss_prompt{1024, role_t::VCR_STATUS};
+ status_field fss_error{1024, role_t::VCR_ALERT_STATUS};
+
+private:
+ status_field fss_help;
+};
+
+#endif
diff --git a/src/filter_sub_source.cc b/src/filter_sub_source.cc
new file mode 100644
index 0000000..10e53a1
--- /dev/null
+++ b/src/filter_sub_source.cc
@@ -0,0 +1,669 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "filter_sub_source.hh"
+
+#include "base/enum_util.hh"
+#include "base/func_util.hh"
+#include "base/opt_util.hh"
+#include "config.h"
+#include "lnav.hh"
+#include "readline_highlighters.hh"
+#include "readline_possibilities.hh"
+
+using namespace lnav::roles::literals;
+
+filter_sub_source::filter_sub_source(std::shared_ptr<readline_curses> editor)
+ : fss_editor(editor)
+{
+ this->fss_editor->set_left(25);
+ this->fss_editor->set_width(-1);
+ this->fss_editor->set_save_history(!(lnav_data.ld_flags & LNF_SECURE_MODE));
+ this->fss_regex_context.set_highlighter(readline_regex_highlighter)
+ .set_append_character(0);
+ this->fss_editor->add_context(filter_lang_t::REGEX,
+ this->fss_regex_context);
+ this->fss_sql_context.set_highlighter(readline_sqlite_highlighter)
+ .set_append_character(0);
+ this->fss_editor->add_context(filter_lang_t::SQL, this->fss_sql_context);
+ this->fss_editor->set_change_action(
+ bind_mem(&filter_sub_source::rl_change, this));
+ this->fss_editor->set_perform_action(
+ bind_mem(&filter_sub_source::rl_perform, this));
+ this->fss_editor->set_abort_action(
+ bind_mem(&filter_sub_source::rl_abort, this));
+ this->fss_editor->set_display_match_action(
+ bind_mem(&filter_sub_source::rl_display_matches, this));
+ this->fss_editor->set_display_next_action(
+ bind_mem(&filter_sub_source::rl_display_next, this));
+ this->fss_match_view.set_sub_source(&this->fss_match_source);
+ this->fss_match_view.set_height(0_vl);
+ this->fss_match_view.set_show_scrollbar(true);
+ this->fss_match_view.set_default_role(role_t::VCR_POPUP);
+}
+
+bool
+filter_sub_source::list_input_handle_key(listview_curses& lv, int ch)
+{
+ if (this->fss_editing) {
+ switch (ch) {
+ case KEY_CTRL_RBRACKET:
+ this->fss_editor->abort();
+ return true;
+ default:
+ this->fss_editor->handle_key(ch);
+ return true;
+ }
+ }
+
+ switch (ch) {
+ case 'f': {
+ auto* top_view = *lnav_data.ld_view_stack.top();
+ auto* tss = top_view->get_sub_source();
+
+ tss->toggle_apply_filters();
+ top_view->reload_data();
+ break;
+ }
+ case ' ': {
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+
+ if (fs.empty()) {
+ return true;
+ }
+
+ auto tf = *(fs.begin() + lv.get_selection());
+
+ fs.set_filter_enabled(tf, !tf->is_enabled());
+ tss->text_filters_changed();
+ lv.reload_data();
+ top_view->reload_data();
+ return true;
+ }
+ case 't': {
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+
+ if (fs.empty()) {
+ return true;
+ }
+
+ auto tf = *(fs.begin() + lv.get_selection());
+
+ if (tf->get_type() == text_filter::INCLUDE) {
+ tf->set_type(text_filter::EXCLUDE);
+ } else {
+ tf->set_type(text_filter::INCLUDE);
+ }
+
+ tss->text_filters_changed();
+ lv.reload_data();
+ top_view->reload_data();
+ return true;
+ }
+ case 'D': {
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+
+ if (fs.empty()) {
+ return true;
+ }
+
+ auto tf = *(fs.begin() + lv.get_selection());
+
+ fs.delete_filter(tf->get_id());
+ lv.reload_data();
+ tss->text_filters_changed();
+ top_view->reload_data();
+ return true;
+ }
+ case 'i': {
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ auto filter_index = fs.next_index();
+
+ if (!filter_index) {
+ lnav_data.ld_filter_help_status_source.fss_error.set_value(
+ "error: too many filters");
+ return true;
+ }
+
+ auto ef = std::make_shared<empty_filter>(
+ text_filter::type_t::INCLUDE, *filter_index);
+ fs.add_filter(ef);
+ lv.set_selection(vis_line_t(fs.size() - 1));
+ lv.reload_data();
+
+ this->fss_editing = true;
+
+ add_view_text_possibilities(this->fss_editor.get(),
+ filter_lang_t::REGEX,
+ "*",
+ top_view,
+ text_quoting::regex);
+ this->fss_editor->set_window(lv.get_window());
+ this->fss_editor->set_visible(true);
+ this->fss_editor->set_y(
+ lv.get_y() + (int) (lv.get_selection() - lv.get_top()));
+ this->fss_editor->window_change();
+ this->fss_editor->focus(filter_lang_t::REGEX, "", "");
+ this->fss_filter_state = true;
+ ef->disable();
+ return true;
+ }
+ case 'o': {
+ auto* top_view = *lnav_data.ld_view_stack.top();
+ auto* tss = top_view->get_sub_source();
+ auto& fs = tss->get_filters();
+ auto filter_index = fs.next_index();
+
+ if (!filter_index) {
+ lnav_data.ld_filter_help_status_source.fss_error.set_value(
+ "error: too many filters");
+ return true;
+ }
+
+ auto ef = std::make_shared<empty_filter>(
+ text_filter::type_t::EXCLUDE, *filter_index);
+ fs.add_filter(ef);
+ lv.set_selection(vis_line_t(fs.size() - 1));
+ lv.reload_data();
+
+ this->fss_editing = true;
+
+ add_view_text_possibilities(this->fss_editor.get(),
+ filter_lang_t::REGEX,
+ "*",
+ top_view,
+ text_quoting::regex);
+ this->fss_editor->set_window(lv.get_window());
+ this->fss_editor->set_visible(true);
+ this->fss_editor->set_y(
+ lv.get_y() + (int) (lv.get_selection() - lv.get_top()));
+ this->fss_editor->window_change();
+ this->fss_editor->focus(filter_lang_t::REGEX, "", "");
+ this->fss_filter_state = true;
+ ef->disable();
+ return true;
+ }
+ case '\r':
+ case KEY_ENTER: {
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+
+ if (fs.empty()) {
+ return true;
+ }
+
+ auto tf = *(fs.begin() + lv.get_selection());
+
+ this->fss_editing = true;
+
+ auto tq = tf->get_lang() == filter_lang_t::SQL
+ ? text_quoting::sql
+ : text_quoting::regex;
+ add_view_text_possibilities(
+ this->fss_editor.get(), tf->get_lang(), "*", top_view, tq);
+ if (top_view == &lnav_data.ld_views[LNV_LOG]) {
+ add_filter_expr_possibilities(
+ this->fss_editor.get(), filter_lang_t::SQL, "*");
+ }
+ this->fss_editor->set_window(lv.get_window());
+ this->fss_editor->set_visible(true);
+ this->fss_editor->set_y(
+ lv.get_y() + (int) (lv.get_selection() - lv.get_top()));
+ this->fss_editor->focus(tf->get_lang(), "");
+ this->fss_editor->rewrite_line(0, tf->get_id().c_str());
+ this->fss_filter_state = tf->is_enabled();
+ tf->disable();
+ tss->text_filters_changed();
+ return true;
+ }
+ case 'n': {
+ execute_command(lnav_data.ld_exec_context, "next-mark search");
+ return true;
+ }
+ case 'N': {
+ execute_command(lnav_data.ld_exec_context, "prev-mark search");
+ return true;
+ }
+ case '/': {
+ execute_command(lnav_data.ld_exec_context, "prompt search-filters");
+ return true;
+ }
+ default:
+ log_debug("unhandled %x", ch);
+ break;
+ }
+
+ return false;
+}
+
+size_t
+filter_sub_source::text_line_count()
+{
+ return (lnav_data.ld_view_stack.top() |
+ [](auto tc) {
+ text_sub_source* tss = tc->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+
+ return nonstd::make_optional(fs.size());
+ })
+ .value_or(0);
+}
+
+size_t
+filter_sub_source::text_line_width(textview_curses& curses)
+{
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ size_t retval = 0;
+
+ for (auto& filter : fs) {
+ retval = std::max(filter->get_id().size() + 8, retval);
+ }
+
+ return retval;
+}
+
+void
+filter_sub_source::text_value_for_line(textview_curses& tc,
+ int line,
+ std::string& value_out,
+ text_sub_source::line_flags_t flags)
+{
+ auto* top_view = *lnav_data.ld_view_stack.top();
+ auto* tss = top_view->get_sub_source();
+ auto& fs = tss->get_filters();
+ auto tf = *(fs.begin() + line);
+
+ value_out = " ";
+ switch (tf->get_type()) {
+ case text_filter::INCLUDE:
+ value_out.append(" IN ");
+ break;
+ case text_filter::EXCLUDE:
+ if (tf->get_lang() == filter_lang_t::REGEX) {
+ value_out.append("OUT ");
+ } else {
+ value_out.append(" ");
+ }
+ break;
+ default:
+ ensure(0);
+ break;
+ }
+
+ if (this->fss_editing && line == tc.get_selection()) {
+ fmt::format_to(
+ std::back_inserter(value_out), FMT_STRING("{:>9} hits | "), "-");
+ } else {
+ fmt::format_to(std::back_inserter(value_out),
+ FMT_STRING("{:>9L} hits | "),
+ tss->get_filtered_count_for(tf->get_index()));
+ }
+
+ value_out.append(tf->get_id());
+}
+
+void
+filter_sub_source::text_attrs_for_line(textview_curses& tc,
+ int line,
+ string_attrs_t& value_out)
+{
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ auto tf = *(fs.begin() + line);
+ bool selected
+ = lnav_data.ld_mode == ln_mode_t::FILTER && line == tc.get_selection();
+
+ if (selected) {
+ value_out.emplace_back(line_range{0, 1}, VC_GRAPHIC.value(ACS_RARROW));
+ }
+
+ chtype enabled = tf->is_enabled() ? ACS_DIAMOND : ' ';
+
+ line_range lr{2, 3};
+ value_out.emplace_back(lr, VC_GRAPHIC.value(enabled));
+ if (tf->is_enabled()) {
+ value_out.emplace_back(lr, VC_FOREGROUND.value(COLOR_GREEN));
+ }
+
+ if (selected) {
+ value_out.emplace_back(line_range{0, -1},
+ VC_ROLE.value(role_t::VCR_FOCUSED));
+ }
+
+ role_t fg_role = tf->get_type() == text_filter::INCLUDE ? role_t::VCR_OK
+ : role_t::VCR_ERROR;
+ value_out.emplace_back(line_range{4, 7}, VC_ROLE.value(fg_role));
+ value_out.emplace_back(line_range{4, 7},
+ VC_STYLE.value(text_attrs{A_BOLD}));
+
+ value_out.emplace_back(line_range{8, 17},
+ VC_STYLE.value(text_attrs{A_BOLD}));
+ value_out.emplace_back(line_range{23, 24}, VC_GRAPHIC.value(ACS_VLINE));
+
+ attr_line_t content{tf->get_id()};
+ auto& content_attrs = content.get_attrs();
+
+ switch (tf->get_lang()) {
+ case filter_lang_t::REGEX:
+ readline_regex_highlighter(content, content.length());
+ break;
+ case filter_lang_t::SQL:
+ readline_sqlite_highlighter(content, content.length());
+ break;
+ case filter_lang_t::NONE:
+ break;
+ }
+
+ shift_string_attrs(content_attrs, 0, 25);
+ value_out.insert(
+ value_out.end(), content_attrs.begin(), content_attrs.end());
+}
+
+size_t
+filter_sub_source::text_size_for_line(textview_curses& tc,
+ int line,
+ text_sub_source::line_flags_t raw)
+{
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ auto tf = *(fs.begin() + line);
+
+ return 8 + tf->get_id().size();
+}
+
+void
+filter_sub_source::rl_change(readline_curses* rc)
+{
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ if (fs.empty()) {
+ return;
+ }
+
+ auto iter = fs.begin() + this->tss_view->get_selection();
+ auto tf = *iter;
+ auto new_value = rc->get_line_buffer();
+
+ switch (tf->get_lang()) {
+ case filter_lang_t::NONE:
+ break;
+ case filter_lang_t::REGEX: {
+ auto regex_res
+ = lnav::pcre2pp::code::from(new_value, PCRE2_CASELESS);
+
+ if (regex_res.isErr()) {
+ auto pe = regex_res.unwrapErr();
+ lnav_data.ld_filter_help_status_source.fss_error.set_value(
+ "error: %s", pe.get_message().c_str());
+ } else {
+ auto& hm = top_view->get_highlights();
+ highlighter hl(regex_res.unwrap().to_shared());
+ auto role = tf->get_type() == text_filter::EXCLUDE
+ ? role_t::VCR_DIFF_DELETE
+ : role_t::VCR_DIFF_ADD;
+ hl.with_role(role);
+ hl.with_attrs(text_attrs{A_BLINK | A_REVERSE});
+
+ hm[{highlight_source_t::PREVIEW, "preview"}] = hl;
+ top_view->set_needs_update();
+ lnav_data.ld_filter_help_status_source.fss_error.clear();
+ }
+ break;
+ }
+ case filter_lang_t::SQL: {
+ auto full_sql
+ = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), new_value);
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+#ifdef SQLITE_PREPARE_PERSISTENT
+ auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
+ full_sql.c_str(),
+ full_sql.size(),
+ SQLITE_PREPARE_PERSISTENT,
+ stmt.out(),
+ nullptr);
+#else
+ auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
+ full_sql.c_str(),
+ full_sql.size(),
+ stmt.out(),
+ nullptr);
+#endif
+ if (retcode != SQLITE_OK) {
+ lnav_data.ld_filter_help_status_source.fss_error.set_value(
+ "error: %s", sqlite3_errmsg(lnav_data.ld_db));
+ } else {
+ auto set_res = lnav_data.ld_log_source.set_preview_sql_filter(
+ stmt.release());
+
+ if (set_res.isErr()) {
+ lnav_data.ld_filter_help_status_source.fss_error.set_value(
+ "error: %s",
+ set_res.unwrapErr()
+ .to_attr_line()
+ .get_string()
+ .c_str());
+ } else {
+ top_view->set_needs_update();
+ lnav_data.ld_filter_help_status_source.fss_error.clear();
+ }
+ }
+ break;
+ }
+ }
+}
+
+void
+filter_sub_source::rl_perform(readline_curses* rc)
+{
+ static const intern_string_t INPUT_SRC = intern_string::lookup("input");
+
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ auto iter = fs.begin() + this->tss_view->get_selection();
+ auto tf = *iter;
+ auto new_value = rc->get_value().get_string();
+
+ if (new_value.empty()) {
+ this->rl_abort(rc);
+ } else {
+ top_view->get_highlights().erase(
+ {highlight_source_t::PREVIEW, "preview"});
+ switch (tf->get_lang()) {
+ case filter_lang_t::NONE:
+ case filter_lang_t::REGEX: {
+ auto compile_res
+ = lnav::pcre2pp::code::from(new_value, PCRE2_CASELESS);
+
+ if (compile_res.isErr()) {
+ auto ce = compile_res.unwrapErr();
+ auto um = lnav::console::to_user_message(INPUT_SRC, ce);
+ lnav_data.ld_exec_context.ec_error_callback_stack.back()(
+ um);
+ this->rl_abort(rc);
+ } else {
+ tf->lf_deleted = true;
+ tss->text_filters_changed();
+
+ auto pf = std::make_shared<pcre_filter>(
+ tf->get_type(),
+ new_value,
+ tf->get_index(),
+ compile_res.unwrap().to_shared());
+
+ *iter = pf;
+ tss->text_filters_changed();
+ }
+ break;
+ }
+ case filter_lang_t::SQL: {
+ auto full_sql
+ = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), new_value);
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+#ifdef SQLITE_PREPARE_PERSISTENT
+ auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
+ full_sql.c_str(),
+ full_sql.size(),
+ SQLITE_PREPARE_PERSISTENT,
+ stmt.out(),
+ nullptr);
+#else
+ auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
+ full_sql.c_str(),
+ full_sql.size(),
+ stmt.out(),
+ nullptr);
+#endif
+ if (retcode != SQLITE_OK) {
+ auto sqlerr = annotate_sql_with_error(
+ lnav_data.ld_db.in(), full_sql.c_str(), nullptr);
+ auto um
+ = lnav::console::user_message::error(
+ "invalid SQL expression")
+ .with_reason(sqlite3_errmsg(lnav_data.ld_db.in()))
+ .with_snippet(lnav::console::snippet::from(
+ INPUT_SRC, sqlerr));
+ lnav_data.ld_exec_context.ec_error_callback_stack.back()(
+ um);
+ this->rl_abort(rc);
+ } else {
+ lnav_data.ld_log_source.set_sql_filter(new_value,
+ stmt.release());
+ tss->text_filters_changed();
+ }
+ break;
+ }
+ }
+ }
+
+ lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
+ lnav_data.ld_filter_help_status_source.fss_prompt.clear();
+ this->fss_editing = false;
+ this->fss_editor->set_visible(false);
+ top_view->reload_data();
+ this->tss_view->reload_data();
+}
+
+void
+filter_sub_source::rl_abort(readline_curses* rc)
+{
+ textview_curses* top_view = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = top_view->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ auto iter = fs.begin() + this->tss_view->get_selection();
+ auto tf = *iter;
+
+ lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
+ lnav_data.ld_filter_help_status_source.fss_prompt.clear();
+ lnav_data.ld_filter_help_status_source.fss_error.clear();
+ top_view->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
+ top_view->reload_data();
+ fs.delete_filter("");
+ this->tss_view->reload_data();
+ this->fss_editor->set_visible(false);
+ this->fss_editing = false;
+ this->tss_view->set_needs_update();
+ tf->set_enabled(this->fss_filter_state);
+ tss->text_filters_changed();
+ this->tss_view->reload_data();
+}
+
+void
+filter_sub_source::rl_display_matches(readline_curses* rc)
+{
+ const std::vector<std::string>& matches = rc->get_matches();
+ unsigned long width = 0;
+
+ if (matches.empty()) {
+ this->fss_match_source.clear();
+ this->fss_match_view.set_height(0_vl);
+ this->tss_view->set_needs_update();
+ } else {
+ auto current_match = rc->get_match_string();
+ attr_line_t al;
+ vis_line_t line, selected_line;
+
+ for (const auto& match : matches) {
+ if (match == current_match) {
+ al.append(match, VC_STYLE.value(text_attrs{A_REVERSE}));
+ selected_line = line;
+ } else {
+ al.append(match);
+ }
+ al.append(1, '\n');
+ width = std::max(width, (unsigned long) match.size());
+ line += 1_vl;
+ }
+
+ this->fss_match_view.set_selection(selected_line);
+ this->fss_match_source.replace_with(al);
+ this->fss_match_view.set_height(
+ std::min(vis_line_t(matches.size()), 3_vl));
+ }
+
+ this->fss_match_view.set_window(this->tss_view->get_window());
+ this->fss_match_view.set_y(rc->get_y() + 1);
+ this->fss_match_view.set_x(rc->get_left() + rc->get_match_start());
+ this->fss_match_view.set_width(width + 3);
+ this->fss_match_view.set_needs_update();
+ this->fss_match_view.reload_data();
+}
+
+void
+filter_sub_source::rl_display_next(readline_curses* rc)
+{
+ textview_curses& tc = this->fss_match_view;
+
+ if (tc.get_top() >= (tc.get_top_for_last_row() - 1)) {
+ tc.set_top(0_vl);
+ } else {
+ tc.shift_top(tc.get_height());
+ }
+}
+
+void
+filter_sub_source::list_input_handle_scroll_out(listview_curses& lv)
+{
+ lnav_data.ld_mode = ln_mode_t::PAGING;
+ lnav_data.ld_filter_view.reload_data();
+}
diff --git a/src/filter_sub_source.hh b/src/filter_sub_source.hh
new file mode 100644
index 0000000..11587da
--- /dev/null
+++ b/src/filter_sub_source.hh
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef filter_sub_source_hh
+#define filter_sub_source_hh
+
+#include "base/injector.hh"
+#include "plain_text_source.hh"
+#include "readline_curses.hh"
+#include "textview_curses.hh"
+
+class filter_sub_source
+ : public text_sub_source
+ , public list_input_delegate {
+public:
+ filter_sub_source(std::shared_ptr<readline_curses> editor);
+
+ using injectable
+ = filter_sub_source(std::shared_ptr<readline_curses> editor);
+
+ filter_sub_source(const filter_sub_source*) = delete;
+
+ ~filter_sub_source() override = default;
+
+ bool list_input_handle_key(listview_curses& lv, int ch) override;
+
+ void list_input_handle_scroll_out(listview_curses& lv) override;
+
+ size_t text_line_count() override;
+
+ size_t text_line_width(textview_curses& curses) override;
+
+ void text_value_for_line(textview_curses& tc,
+ int line,
+ std::string& value_out,
+ line_flags_t flags) override;
+
+ void text_attrs_for_line(textview_curses& tc,
+ int line,
+ string_attrs_t& value_out) override;
+
+ size_t text_size_for_line(textview_curses& tc,
+ int line,
+ line_flags_t raw) override;
+
+ void rl_change(readline_curses* rc);
+
+ void rl_perform(readline_curses* rc);
+
+ void rl_abort(readline_curses* rc);
+
+ void rl_display_matches(readline_curses* rc);
+
+ void rl_display_next(readline_curses* rc);
+
+ readline_context fss_regex_context{"filter-regex", nullptr, false};
+ readline_context fss_sql_context{"filter-sql", nullptr, false};
+ std::shared_ptr<readline_curses> fss_editor;
+ plain_text_source fss_match_source;
+ textview_curses fss_match_view;
+
+ bool fss_editing{false};
+ bool fss_filter_state{false};
+};
+
+#endif
diff --git a/src/fmtlib/Makefile.am b/src/fmtlib/Makefile.am
new file mode 100644
index 0000000..6c6fa12
--- /dev/null
+++ b/src/fmtlib/Makefile.am
@@ -0,0 +1,22 @@
+
+noinst_HEADERS = \
+ fmt/args.h \
+ fmt/chrono.h \
+ fmt/color.h \
+ fmt/compile.h \
+ fmt/core.h \
+ fmt/format-inl.h \
+ fmt/format.h \
+ fmt/locale.h \
+ fmt/os.h \
+ fmt/ostream.h \
+ fmt/printf.h \
+ fmt/ranges.h \
+ fmt/std.h \
+ fmt/xchar.h
+
+noinst_LIBRARIES = libcppfmt.a
+
+libcppfmt_a_SOURCES = \
+ format.cc \
+ os.cc
diff --git a/src/fmtlib/fmt/args.h b/src/fmtlib/fmt/args.h
new file mode 100644
index 0000000..a3966d1
--- /dev/null
+++ b/src/fmtlib/fmt/args.h
@@ -0,0 +1,234 @@
+// Formatting library for C++ - dynamic format arguments
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_ARGS_H_
+#define FMT_ARGS_H_
+
+#include <functional> // std::reference_wrapper
+#include <memory> // std::unique_ptr
+#include <vector>
+
+#include "core.h"
+
+FMT_BEGIN_NAMESPACE
+
+namespace detail {
+
+template <typename T> struct is_reference_wrapper : std::false_type {};
+template <typename T>
+struct is_reference_wrapper<std::reference_wrapper<T>> : std::true_type {};
+
+template <typename T> const T& unwrap(const T& v) { return v; }
+template <typename T> const T& unwrap(const std::reference_wrapper<T>& v) {
+ return static_cast<const T&>(v);
+}
+
+class dynamic_arg_list {
+ // Workaround for clang's -Wweak-vtables. Unlike for regular classes, for
+ // templates it doesn't complain about inability to deduce single translation
+ // unit for placing vtable. So storage_node_base is made a fake template.
+ template <typename = void> struct node {
+ virtual ~node() = default;
+ std::unique_ptr<node<>> next;
+ };
+
+ template <typename T> struct typed_node : node<> {
+ T value;
+
+ template <typename Arg>
+ FMT_CONSTEXPR typed_node(const Arg& arg) : value(arg) {}
+
+ template <typename Char>
+ FMT_CONSTEXPR typed_node(const basic_string_view<Char>& arg)
+ : value(arg.data(), arg.size()) {}
+ };
+
+ std::unique_ptr<node<>> head_;
+
+ public:
+ template <typename T, typename Arg> const T& push(const Arg& arg) {
+ auto new_node = std::unique_ptr<typed_node<T>>(new typed_node<T>(arg));
+ auto& value = new_node->value;
+ new_node->next = std::move(head_);
+ head_ = std::move(new_node);
+ return value;
+ }
+};
+} // namespace detail
+
+/**
+ \rst
+ A dynamic version of `fmt::format_arg_store`.
+ It's equipped with a storage to potentially temporary objects which lifetimes
+ could be shorter than the format arguments object.
+
+ It can be implicitly converted into `~fmt::basic_format_args` for passing
+ into type-erased formatting functions such as `~fmt::vformat`.
+ \endrst
+ */
+template <typename Context>
+class dynamic_format_arg_store
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+ // Workaround a GCC template argument substitution bug.
+ : public basic_format_args<Context>
+#endif
+{
+ private:
+ using char_type = typename Context::char_type;
+
+ template <typename T> struct need_copy {
+ static constexpr detail::type mapped_type =
+ detail::mapped_type_constant<T, Context>::value;
+
+ enum {
+ value = !(detail::is_reference_wrapper<T>::value ||
+ std::is_same<T, basic_string_view<char_type>>::value ||
+ std::is_same<T, detail::std_string_view<char_type>>::value ||
+ (mapped_type != detail::type::cstring_type &&
+ mapped_type != detail::type::string_type &&
+ mapped_type != detail::type::custom_type))
+ };
+ };
+
+ template <typename T>
+ using stored_type = conditional_t<
+ std::is_convertible<T, std::basic_string<char_type>>::value &&
+ !detail::is_reference_wrapper<T>::value,
+ std::basic_string<char_type>, T>;
+
+ // Storage of basic_format_arg must be contiguous.
+ std::vector<basic_format_arg<Context>> data_;
+ std::vector<detail::named_arg_info<char_type>> named_info_;
+
+ // Storage of arguments not fitting into basic_format_arg must grow
+ // without relocation because items in data_ refer to it.
+ detail::dynamic_arg_list dynamic_args_;
+
+ friend class basic_format_args<Context>;
+
+ unsigned long long get_types() const {
+ return detail::is_unpacked_bit | data_.size() |
+ (named_info_.empty()
+ ? 0ULL
+ : static_cast<unsigned long long>(detail::has_named_args_bit));
+ }
+
+ const basic_format_arg<Context>* data() const {
+ return named_info_.empty() ? data_.data() : data_.data() + 1;
+ }
+
+ template <typename T> void emplace_arg(const T& arg) {
+ data_.emplace_back(detail::make_arg<Context>(arg));
+ }
+
+ template <typename T>
+ void emplace_arg(const detail::named_arg<char_type, T>& arg) {
+ if (named_info_.empty()) {
+ constexpr const detail::named_arg_info<char_type>* zero_ptr{nullptr};
+ data_.insert(data_.begin(), {zero_ptr, 0});
+ }
+ data_.emplace_back(detail::make_arg<Context>(detail::unwrap(arg.value)));
+ auto pop_one = [](std::vector<basic_format_arg<Context>>* data) {
+ data->pop_back();
+ };
+ std::unique_ptr<std::vector<basic_format_arg<Context>>, decltype(pop_one)>
+ guard{&data_, pop_one};
+ named_info_.push_back({arg.name, static_cast<int>(data_.size() - 2u)});
+ data_[0].value_.named_args = {named_info_.data(), named_info_.size()};
+ guard.release();
+ }
+
+ public:
+ constexpr dynamic_format_arg_store() = default;
+
+ /**
+ \rst
+ Adds an argument into the dynamic store for later passing to a formatting
+ function.
+
+ Note that custom types and string types (but not string views) are copied
+ into the store dynamically allocating memory if necessary.
+
+ **Example**::
+
+ fmt::dynamic_format_arg_store<fmt::format_context> store;
+ store.push_back(42);
+ store.push_back("abc");
+ store.push_back(1.5f);
+ std::string result = fmt::vformat("{} and {} and {}", store);
+ \endrst
+ */
+ template <typename T> void push_back(const T& arg) {
+ if (detail::const_check(need_copy<T>::value))
+ emplace_arg(dynamic_args_.push<stored_type<T>>(arg));
+ else
+ emplace_arg(detail::unwrap(arg));
+ }
+
+ /**
+ \rst
+ Adds a reference to the argument into the dynamic store for later passing to
+ a formatting function.
+
+ **Example**::
+
+ fmt::dynamic_format_arg_store<fmt::format_context> store;
+ char band[] = "Rolling Stones";
+ store.push_back(std::cref(band));
+ band[9] = 'c'; // Changing str affects the output.
+ std::string result = fmt::vformat("{}", store);
+ // result == "Rolling Scones"
+ \endrst
+ */
+ template <typename T> void push_back(std::reference_wrapper<T> arg) {
+ static_assert(
+ need_copy<T>::value,
+ "objects of built-in types and string views are always copied");
+ emplace_arg(arg.get());
+ }
+
+ /**
+ Adds named argument into the dynamic store for later passing to a formatting
+ function. ``std::reference_wrapper`` is supported to avoid copying of the
+ argument. The name is always copied into the store.
+ */
+ template <typename T>
+ void push_back(const detail::named_arg<char_type, T>& arg) {
+ const char_type* arg_name =
+ dynamic_args_.push<std::basic_string<char_type>>(arg.name).c_str();
+ if (detail::const_check(need_copy<T>::value)) {
+ emplace_arg(
+ fmt::arg(arg_name, dynamic_args_.push<stored_type<T>>(arg.value)));
+ } else {
+ emplace_arg(fmt::arg(arg_name, arg.value));
+ }
+ }
+
+ /** Erase all elements from the store */
+ void clear() {
+ data_.clear();
+ named_info_.clear();
+ dynamic_args_ = detail::dynamic_arg_list();
+ }
+
+ /**
+ \rst
+ Reserves space to store at least *new_cap* arguments including
+ *new_cap_named* named arguments.
+ \endrst
+ */
+ void reserve(size_t new_cap, size_t new_cap_named) {
+ FMT_ASSERT(new_cap >= new_cap_named,
+ "Set of arguments includes set of named arguments");
+ data_.reserve(new_cap);
+ named_info_.reserve(new_cap_named);
+ }
+};
+
+FMT_END_NAMESPACE
+
+#endif // FMT_ARGS_H_
diff --git a/src/fmtlib/fmt/chrono.h b/src/fmtlib/fmt/chrono.h
new file mode 100644
index 0000000..55e8a50
--- /dev/null
+++ b/src/fmtlib/fmt/chrono.h
@@ -0,0 +1,2267 @@
+// Formatting library for C++ - chrono support
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_CHRONO_H_
+#define FMT_CHRONO_H_
+
+#include <algorithm>
+#include <chrono>
+#include <cmath> // std::isfinite
+#include <cstring> // std::memcpy
+#include <ctime>
+#include <iterator>
+#include <locale>
+#include <ostream>
+#include <type_traits>
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+
+// Check if std::chrono::local_t is available.
+#ifndef FMT_USE_LOCAL_TIME
+# ifdef __cpp_lib_chrono
+# define FMT_USE_LOCAL_TIME (__cpp_lib_chrono >= 201907L)
+# else
+# define FMT_USE_LOCAL_TIME 0
+# endif
+#endif
+
+// Check if std::chrono::utc_timestamp is available.
+#ifndef FMT_USE_UTC_TIME
+# ifdef __cpp_lib_chrono
+# define FMT_USE_UTC_TIME (__cpp_lib_chrono >= 201907L)
+# else
+# define FMT_USE_UTC_TIME 0
+# endif
+#endif
+
+// Enable tzset.
+#ifndef FMT_USE_TZSET
+// UWP doesn't provide _tzset.
+# if FMT_HAS_INCLUDE("winapifamily.h")
+# include <winapifamily.h>
+# endif
+# if defined(_WIN32) && (!defined(WINAPI_FAMILY) || \
+ (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP))
+# define FMT_USE_TZSET 1
+# else
+# define FMT_USE_TZSET 0
+# endif
+#endif
+
+// Enable safe chrono durations, unless explicitly disabled.
+#ifndef FMT_SAFE_DURATION_CAST
+# define FMT_SAFE_DURATION_CAST 1
+#endif
+#if FMT_SAFE_DURATION_CAST
+
+// For conversion between std::chrono::durations without undefined
+// behaviour or erroneous results.
+// This is a stripped down version of duration_cast, for inclusion in fmt.
+// See https://github.com/pauldreik/safe_duration_cast
+//
+// Copyright Paul Dreik 2019
+namespace safe_duration_cast {
+
+template <typename To, typename From,
+ FMT_ENABLE_IF(!std::is_same<From, To>::value &&
+ std::numeric_limits<From>::is_signed ==
+ std::numeric_limits<To>::is_signed)>
+FMT_CONSTEXPR To lossless_integral_conversion(const From from, int& ec) {
+ ec = 0;
+ using F = std::numeric_limits<From>;
+ using T = std::numeric_limits<To>;
+ static_assert(F::is_integer, "From must be integral");
+ static_assert(T::is_integer, "To must be integral");
+
+ // A and B are both signed, or both unsigned.
+ if (detail::const_check(F::digits <= T::digits)) {
+ // From fits in To without any problem.
+ } else {
+ // From does not always fit in To, resort to a dynamic check.
+ if (from < (T::min)() || from > (T::max)()) {
+ // outside range.
+ ec = 1;
+ return {};
+ }
+ }
+ return static_cast<To>(from);
+}
+
+/**
+ * converts From to To, without loss. If the dynamic value of from
+ * can't be converted to To without loss, ec is set.
+ */
+template <typename To, typename From,
+ FMT_ENABLE_IF(!std::is_same<From, To>::value &&
+ std::numeric_limits<From>::is_signed !=
+ std::numeric_limits<To>::is_signed)>
+FMT_CONSTEXPR To lossless_integral_conversion(const From from, int& ec) {
+ ec = 0;
+ using F = std::numeric_limits<From>;
+ using T = std::numeric_limits<To>;
+ static_assert(F::is_integer, "From must be integral");
+ static_assert(T::is_integer, "To must be integral");
+
+ if (detail::const_check(F::is_signed && !T::is_signed)) {
+ // From may be negative, not allowed!
+ if (fmt::detail::is_negative(from)) {
+ ec = 1;
+ return {};
+ }
+ // From is positive. Can it always fit in To?
+ if (detail::const_check(F::digits > T::digits) &&
+ from > static_cast<From>(detail::max_value<To>())) {
+ ec = 1;
+ return {};
+ }
+ }
+
+ if (detail::const_check(!F::is_signed && T::is_signed &&
+ F::digits >= T::digits) &&
+ from > static_cast<From>(detail::max_value<To>())) {
+ ec = 1;
+ return {};
+ }
+ return static_cast<To>(from); // Lossless conversion.
+}
+
+template <typename To, typename From,
+ FMT_ENABLE_IF(std::is_same<From, To>::value)>
+FMT_CONSTEXPR To lossless_integral_conversion(const From from, int& ec) {
+ ec = 0;
+ return from;
+} // function
+
+// clang-format off
+/**
+ * converts From to To if possible, otherwise ec is set.
+ *
+ * input | output
+ * ---------------------------------|---------------
+ * NaN | NaN
+ * Inf | Inf
+ * normal, fits in output | converted (possibly lossy)
+ * normal, does not fit in output | ec is set
+ * subnormal | best effort
+ * -Inf | -Inf
+ */
+// clang-format on
+template <typename To, typename From,
+ FMT_ENABLE_IF(!std::is_same<From, To>::value)>
+FMT_CONSTEXPR To safe_float_conversion(const From from, int& ec) {
+ ec = 0;
+ using T = std::numeric_limits<To>;
+ static_assert(std::is_floating_point<From>::value, "From must be floating");
+ static_assert(std::is_floating_point<To>::value, "To must be floating");
+
+ // catch the only happy case
+ if (std::isfinite(from)) {
+ if (from >= T::lowest() && from <= (T::max)()) {
+ return static_cast<To>(from);
+ }
+ // not within range.
+ ec = 1;
+ return {};
+ }
+
+ // nan and inf will be preserved
+ return static_cast<To>(from);
+} // function
+
+template <typename To, typename From,
+ FMT_ENABLE_IF(std::is_same<From, To>::value)>
+FMT_CONSTEXPR To safe_float_conversion(const From from, int& ec) {
+ ec = 0;
+ static_assert(std::is_floating_point<From>::value, "From must be floating");
+ return from;
+}
+
+/**
+ * safe duration cast between integral durations
+ */
+template <typename To, typename FromRep, typename FromPeriod,
+ FMT_ENABLE_IF(std::is_integral<FromRep>::value),
+ FMT_ENABLE_IF(std::is_integral<typename To::rep>::value)>
+To safe_duration_cast(std::chrono::duration<FromRep, FromPeriod> from,
+ int& ec) {
+ using From = std::chrono::duration<FromRep, FromPeriod>;
+ ec = 0;
+ // the basic idea is that we need to convert from count() in the from type
+ // to count() in the To type, by multiplying it with this:
+ struct Factor
+ : std::ratio_divide<typename From::period, typename To::period> {};
+
+ static_assert(Factor::num > 0, "num must be positive");
+ static_assert(Factor::den > 0, "den must be positive");
+
+ // the conversion is like this: multiply from.count() with Factor::num
+ // /Factor::den and convert it to To::rep, all this without
+ // overflow/underflow. let's start by finding a suitable type that can hold
+ // both To, From and Factor::num
+ using IntermediateRep =
+ typename std::common_type<typename From::rep, typename To::rep,
+ decltype(Factor::num)>::type;
+
+ // safe conversion to IntermediateRep
+ IntermediateRep count =
+ lossless_integral_conversion<IntermediateRep>(from.count(), ec);
+ if (ec) return {};
+ // multiply with Factor::num without overflow or underflow
+ if (detail::const_check(Factor::num != 1)) {
+ const auto max1 = detail::max_value<IntermediateRep>() / Factor::num;
+ if (count > max1) {
+ ec = 1;
+ return {};
+ }
+ const auto min1 =
+ (std::numeric_limits<IntermediateRep>::min)() / Factor::num;
+ if (detail::const_check(!std::is_unsigned<IntermediateRep>::value) &&
+ count < min1) {
+ ec = 1;
+ return {};
+ }
+ count *= Factor::num;
+ }
+
+ if (detail::const_check(Factor::den != 1)) count /= Factor::den;
+ auto tocount = lossless_integral_conversion<typename To::rep>(count, ec);
+ return ec ? To() : To(tocount);
+}
+
+/**
+ * safe duration_cast between floating point durations
+ */
+template <typename To, typename FromRep, typename FromPeriod,
+ FMT_ENABLE_IF(std::is_floating_point<FromRep>::value),
+ FMT_ENABLE_IF(std::is_floating_point<typename To::rep>::value)>
+To safe_duration_cast(std::chrono::duration<FromRep, FromPeriod> from,
+ int& ec) {
+ using From = std::chrono::duration<FromRep, FromPeriod>;
+ ec = 0;
+ if (std::isnan(from.count())) {
+ // nan in, gives nan out. easy.
+ return To{std::numeric_limits<typename To::rep>::quiet_NaN()};
+ }
+ // maybe we should also check if from is denormal, and decide what to do about
+ // it.
+
+ // +-inf should be preserved.
+ if (std::isinf(from.count())) {
+ return To{from.count()};
+ }
+
+ // the basic idea is that we need to convert from count() in the from type
+ // to count() in the To type, by multiplying it with this:
+ struct Factor
+ : std::ratio_divide<typename From::period, typename To::period> {};
+
+ static_assert(Factor::num > 0, "num must be positive");
+ static_assert(Factor::den > 0, "den must be positive");
+
+ // the conversion is like this: multiply from.count() with Factor::num
+ // /Factor::den and convert it to To::rep, all this without
+ // overflow/underflow. let's start by finding a suitable type that can hold
+ // both To, From and Factor::num
+ using IntermediateRep =
+ typename std::common_type<typename From::rep, typename To::rep,
+ decltype(Factor::num)>::type;
+
+ // force conversion of From::rep -> IntermediateRep to be safe,
+ // even if it will never happen be narrowing in this context.
+ IntermediateRep count =
+ safe_float_conversion<IntermediateRep>(from.count(), ec);
+ if (ec) {
+ return {};
+ }
+
+ // multiply with Factor::num without overflow or underflow
+ if (detail::const_check(Factor::num != 1)) {
+ constexpr auto max1 = detail::max_value<IntermediateRep>() /
+ static_cast<IntermediateRep>(Factor::num);
+ if (count > max1) {
+ ec = 1;
+ return {};
+ }
+ constexpr auto min1 = std::numeric_limits<IntermediateRep>::lowest() /
+ static_cast<IntermediateRep>(Factor::num);
+ if (count < min1) {
+ ec = 1;
+ return {};
+ }
+ count *= static_cast<IntermediateRep>(Factor::num);
+ }
+
+ // this can't go wrong, right? den>0 is checked earlier.
+ if (detail::const_check(Factor::den != 1)) {
+ using common_t = typename std::common_type<IntermediateRep, intmax_t>::type;
+ count /= static_cast<common_t>(Factor::den);
+ }
+
+ // convert to the to type, safely
+ using ToRep = typename To::rep;
+
+ const ToRep tocount = safe_float_conversion<ToRep>(count, ec);
+ if (ec) {
+ return {};
+ }
+ return To{tocount};
+}
+} // namespace safe_duration_cast
+#endif
+
+// Prevents expansion of a preceding token as a function-style macro.
+// Usage: f FMT_NOMACRO()
+#define FMT_NOMACRO
+
+namespace detail {
+template <typename T = void> struct null {};
+inline null<> localtime_r FMT_NOMACRO(...) { return null<>(); }
+inline null<> localtime_s(...) { return null<>(); }
+inline null<> gmtime_r(...) { return null<>(); }
+inline null<> gmtime_s(...) { return null<>(); }
+
+inline const std::locale& get_classic_locale() {
+ static const auto& locale = std::locale::classic();
+ return locale;
+}
+
+template <typename CodeUnit> struct codecvt_result {
+ static constexpr const size_t max_size = 32;
+ CodeUnit buf[max_size];
+ CodeUnit* end;
+};
+template <typename CodeUnit>
+constexpr const size_t codecvt_result<CodeUnit>::max_size;
+
+template <typename CodeUnit>
+void write_codecvt(codecvt_result<CodeUnit>& out, string_view in_buf,
+ const std::locale& loc) {
+#if FMT_CLANG_VERSION
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wdeprecated"
+ auto& f = std::use_facet<std::codecvt<CodeUnit, char, std::mbstate_t>>(loc);
+# pragma clang diagnostic pop
+#else
+ auto& f = std::use_facet<std::codecvt<CodeUnit, char, std::mbstate_t>>(loc);
+#endif
+ auto mb = std::mbstate_t();
+ const char* from_next = nullptr;
+ auto result = f.in(mb, in_buf.begin(), in_buf.end(), from_next,
+ std::begin(out.buf), std::end(out.buf), out.end);
+ if (result != std::codecvt_base::ok)
+ FMT_THROW(format_error("failed to format time"));
+}
+
+template <typename OutputIt>
+auto write_encoded_tm_str(OutputIt out, string_view in, const std::locale& loc)
+ -> OutputIt {
+ if (detail::is_utf8() && loc != get_classic_locale()) {
+ // char16_t and char32_t codecvts are broken in MSVC (linkage errors) and
+ // gcc-4.
+#if FMT_MSC_VERSION != 0 || \
+ (defined(__GLIBCXX__) && !defined(_GLIBCXX_USE_DUAL_ABI))
+ // The _GLIBCXX_USE_DUAL_ABI macro is always defined in libstdc++ from gcc-5
+ // and newer.
+ using code_unit = wchar_t;
+#else
+ using code_unit = char32_t;
+#endif
+
+ using unit_t = codecvt_result<code_unit>;
+ unit_t unit;
+ write_codecvt(unit, in, loc);
+ // In UTF-8 is used one to four one-byte code units.
+ unicode_to_utf8<code_unit, basic_memory_buffer<char, unit_t::max_size * 4>>
+ u;
+ if (!u.convert({unit.buf, to_unsigned(unit.end - unit.buf)}))
+ FMT_THROW(format_error("failed to format time"));
+ return copy_str<char>(u.c_str(), u.c_str() + u.size(), out);
+ }
+ return copy_str<char>(in.data(), in.data() + in.size(), out);
+}
+
+template <typename Char, typename OutputIt,
+ FMT_ENABLE_IF(!std::is_same<Char, char>::value)>
+auto write_tm_str(OutputIt out, string_view sv, const std::locale& loc)
+ -> OutputIt {
+ codecvt_result<Char> unit;
+ write_codecvt(unit, sv, loc);
+ return copy_str<Char>(unit.buf, unit.end, out);
+}
+
+template <typename Char, typename OutputIt,
+ FMT_ENABLE_IF(std::is_same<Char, char>::value)>
+auto write_tm_str(OutputIt out, string_view sv, const std::locale& loc)
+ -> OutputIt {
+ return write_encoded_tm_str(out, sv, loc);
+}
+
+template <typename Char>
+inline void do_write(buffer<Char>& buf, const std::tm& time,
+ const std::locale& loc, char format, char modifier) {
+ auto&& format_buf = formatbuf<std::basic_streambuf<Char>>(buf);
+ auto&& os = std::basic_ostream<Char>(&format_buf);
+ os.imbue(loc);
+ using iterator = std::ostreambuf_iterator<Char>;
+ const auto& facet = std::use_facet<std::time_put<Char, iterator>>(loc);
+ auto end = facet.put(os, os, Char(' '), &time, format, modifier);
+ if (end.failed()) FMT_THROW(format_error("failed to format time"));
+}
+
+template <typename Char, typename OutputIt,
+ FMT_ENABLE_IF(!std::is_same<Char, char>::value)>
+auto write(OutputIt out, const std::tm& time, const std::locale& loc,
+ char format, char modifier = 0) -> OutputIt {
+ auto&& buf = get_buffer<Char>(out);
+ do_write<Char>(buf, time, loc, format, modifier);
+ return get_iterator(buf, out);
+}
+
+template <typename Char, typename OutputIt,
+ FMT_ENABLE_IF(std::is_same<Char, char>::value)>
+auto write(OutputIt out, const std::tm& time, const std::locale& loc,
+ char format, char modifier = 0) -> OutputIt {
+ auto&& buf = basic_memory_buffer<Char>();
+ do_write<char>(buf, time, loc, format, modifier);
+ return write_encoded_tm_str(out, string_view(buf.data(), buf.size()), loc);
+}
+
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+/**
+ Converts given time since epoch as ``std::time_t`` value into calendar time,
+ expressed in local time. Unlike ``std::localtime``, this function is
+ thread-safe on most platforms.
+ */
+inline std::tm localtime(std::time_t time) {
+ struct dispatcher {
+ std::time_t time_;
+ std::tm tm_;
+
+ dispatcher(std::time_t t) : time_(t) {}
+
+ bool run() {
+ using namespace fmt::detail;
+ return handle(localtime_r(&time_, &tm_));
+ }
+
+ bool handle(std::tm* tm) { return tm != nullptr; }
+
+ bool handle(detail::null<>) {
+ using namespace fmt::detail;
+ return fallback(localtime_s(&tm_, &time_));
+ }
+
+ bool fallback(int res) { return res == 0; }
+
+#if !FMT_MSC_VERSION
+ bool fallback(detail::null<>) {
+ using namespace fmt::detail;
+ std::tm* tm = std::localtime(&time_);
+ if (tm) tm_ = *tm;
+ return tm != nullptr;
+ }
+#endif
+ };
+ dispatcher lt(time);
+ // Too big time values may be unsupported.
+ if (!lt.run()) FMT_THROW(format_error("time_t value out of range"));
+ return lt.tm_;
+}
+
+#if FMT_USE_LOCAL_TIME
+template <typename Duration>
+inline auto localtime(std::chrono::local_time<Duration> time) -> std::tm {
+ return localtime(std::chrono::system_clock::to_time_t(
+ std::chrono::current_zone()->to_sys(time)));
+}
+#endif
+
+/**
+ Converts given time since epoch as ``std::time_t`` value into calendar time,
+ expressed in Coordinated Universal Time (UTC). Unlike ``std::gmtime``, this
+ function is thread-safe on most platforms.
+ */
+inline std::tm gmtime(std::time_t time) {
+ struct dispatcher {
+ std::time_t time_;
+ std::tm tm_;
+
+ dispatcher(std::time_t t) : time_(t) {}
+
+ bool run() {
+ using namespace fmt::detail;
+ return handle(gmtime_r(&time_, &tm_));
+ }
+
+ bool handle(std::tm* tm) { return tm != nullptr; }
+
+ bool handle(detail::null<>) {
+ using namespace fmt::detail;
+ return fallback(gmtime_s(&tm_, &time_));
+ }
+
+ bool fallback(int res) { return res == 0; }
+
+#if !FMT_MSC_VERSION
+ bool fallback(detail::null<>) {
+ std::tm* tm = std::gmtime(&time_);
+ if (tm) tm_ = *tm;
+ return tm != nullptr;
+ }
+#endif
+ };
+ dispatcher gt(time);
+ // Too big time values may be unsupported.
+ if (!gt.run()) FMT_THROW(format_error("time_t value out of range"));
+ return gt.tm_;
+}
+
+inline std::tm gmtime(
+ std::chrono::time_point<std::chrono::system_clock> time_point) {
+ return gmtime(std::chrono::system_clock::to_time_t(time_point));
+}
+
+FMT_BEGIN_DETAIL_NAMESPACE
+
+// DEPRECATED!
+template <typename Char>
+FMT_CONSTEXPR auto parse_align(const Char* begin, const Char* end,
+ format_specs<Char>& specs) -> const Char* {
+ FMT_ASSERT(begin != end, "");
+ auto align = align::none;
+ auto p = begin + code_point_length(begin);
+ if (end - p <= 0) p = begin;
+ for (;;) {
+ switch (to_ascii(*p)) {
+ case '<':
+ align = align::left;
+ break;
+ case '>':
+ align = align::right;
+ break;
+ case '^':
+ align = align::center;
+ break;
+ }
+ if (align != align::none) {
+ if (p != begin) {
+ auto c = *begin;
+ if (c == '}') return begin;
+ if (c == '{') {
+ throw_format_error("invalid fill character '{'");
+ return begin;
+ }
+ specs.fill = {begin, to_unsigned(p - begin)};
+ begin = p + 1;
+ } else {
+ ++begin;
+ }
+ break;
+ } else if (p == begin) {
+ break;
+ }
+ p = begin;
+ }
+ specs.align = align;
+ return begin;
+}
+
+// Writes two-digit numbers a, b and c separated by sep to buf.
+// The method by Pavel Novikov based on
+// https://johnnylee-sde.github.io/Fast-unsigned-integer-to-time-string/.
+inline void write_digit2_separated(char* buf, unsigned a, unsigned b,
+ unsigned c, char sep) {
+ unsigned long long digits =
+ a | (b << 24) | (static_cast<unsigned long long>(c) << 48);
+ // Convert each value to BCD.
+ // We have x = a * 10 + b and we want to convert it to BCD y = a * 16 + b.
+ // The difference is
+ // y - x = a * 6
+ // a can be found from x:
+ // a = floor(x / 10)
+ // then
+ // y = x + a * 6 = x + floor(x / 10) * 6
+ // floor(x / 10) is (x * 205) >> 11 (needs 16 bits).
+ digits += (((digits * 205) >> 11) & 0x000f00000f00000f) * 6;
+ // Put low nibbles to high bytes and high nibbles to low bytes.
+ digits = ((digits & 0x00f00000f00000f0) >> 4) |
+ ((digits & 0x000f00000f00000f) << 8);
+ auto usep = static_cast<unsigned long long>(sep);
+ // Add ASCII '0' to each digit byte and insert separators.
+ digits |= 0x3030003030003030 | (usep << 16) | (usep << 40);
+
+ constexpr const size_t len = 8;
+ if (const_check(is_big_endian())) {
+ char tmp[len];
+ std::memcpy(tmp, &digits, len);
+ std::reverse_copy(tmp, tmp + len, buf);
+ } else {
+ std::memcpy(buf, &digits, len);
+ }
+}
+
+template <typename Period> FMT_CONSTEXPR inline const char* get_units() {
+ if (std::is_same<Period, std::atto>::value) return "as";
+ if (std::is_same<Period, std::femto>::value) return "fs";
+ if (std::is_same<Period, std::pico>::value) return "ps";
+ if (std::is_same<Period, std::nano>::value) return "ns";
+ if (std::is_same<Period, std::micro>::value) return "µs";
+ if (std::is_same<Period, std::milli>::value) return "ms";
+ if (std::is_same<Period, std::centi>::value) return "cs";
+ if (std::is_same<Period, std::deci>::value) return "ds";
+ if (std::is_same<Period, std::ratio<1>>::value) return "s";
+ if (std::is_same<Period, std::deca>::value) return "das";
+ if (std::is_same<Period, std::hecto>::value) return "hs";
+ if (std::is_same<Period, std::kilo>::value) return "ks";
+ if (std::is_same<Period, std::mega>::value) return "Ms";
+ if (std::is_same<Period, std::giga>::value) return "Gs";
+ if (std::is_same<Period, std::tera>::value) return "Ts";
+ if (std::is_same<Period, std::peta>::value) return "Ps";
+ if (std::is_same<Period, std::exa>::value) return "Es";
+ if (std::is_same<Period, std::ratio<60>>::value) return "m";
+ if (std::is_same<Period, std::ratio<3600>>::value) return "h";
+ return nullptr;
+}
+
+enum class numeric_system {
+ standard,
+ // Alternative numeric system, e.g. 十二 instead of 12 in ja_JP locale.
+ alternative
+};
+
+// Glibc extensions for formatting numeric values.
+enum class pad_type {
+ unspecified,
+ // Do not pad a numeric result string.
+ none,
+ // Pad a numeric result string with zeros even if the conversion specifier
+ // character uses space-padding by default.
+ zero,
+ // Pad a numeric result string with spaces.
+ space,
+};
+
+template <typename OutputIt>
+auto write_padding(OutputIt out, pad_type pad, int width) -> OutputIt {
+ if (pad == pad_type::none) return out;
+ return std::fill_n(out, width, pad == pad_type::space ? ' ' : '0');
+}
+
+template <typename OutputIt>
+auto write_padding(OutputIt out, pad_type pad) -> OutputIt {
+ if (pad != pad_type::none) *out++ = pad == pad_type::space ? ' ' : '0';
+ return out;
+}
+
+// Parses a put_time-like format string and invokes handler actions.
+template <typename Char, typename Handler>
+FMT_CONSTEXPR const Char* parse_chrono_format(const Char* begin,
+ const Char* end,
+ Handler&& handler) {
+ if (begin == end || *begin == '}') return begin;
+ if (*begin != '%') FMT_THROW(format_error("invalid format"));
+ auto ptr = begin;
+ pad_type pad = pad_type::unspecified;
+ while (ptr != end) {
+ auto c = *ptr;
+ if (c == '}') break;
+ if (c != '%') {
+ ++ptr;
+ continue;
+ }
+ if (begin != ptr) handler.on_text(begin, ptr);
+ ++ptr; // consume '%'
+ if (ptr == end) FMT_THROW(format_error("invalid format"));
+ c = *ptr;
+ switch (c) {
+ case '_':
+ pad = pad_type::space;
+ ++ptr;
+ break;
+ case '-':
+ pad = pad_type::none;
+ ++ptr;
+ break;
+ case '0':
+ pad = pad_type::zero;
+ ++ptr;
+ break;
+ }
+ if (ptr == end) FMT_THROW(format_error("invalid format"));
+ c = *ptr++;
+ switch (c) {
+ case '%':
+ handler.on_text(ptr - 1, ptr);
+ break;
+ case 'n': {
+ const Char newline[] = {'\n'};
+ handler.on_text(newline, newline + 1);
+ break;
+ }
+ case 't': {
+ const Char tab[] = {'\t'};
+ handler.on_text(tab, tab + 1);
+ break;
+ }
+ // Year:
+ case 'Y':
+ handler.on_year(numeric_system::standard);
+ break;
+ case 'y':
+ handler.on_short_year(numeric_system::standard);
+ break;
+ case 'C':
+ handler.on_century(numeric_system::standard);
+ break;
+ case 'G':
+ handler.on_iso_week_based_year();
+ break;
+ case 'g':
+ handler.on_iso_week_based_short_year();
+ break;
+ // Day of the week:
+ case 'a':
+ handler.on_abbr_weekday();
+ break;
+ case 'A':
+ handler.on_full_weekday();
+ break;
+ case 'w':
+ handler.on_dec0_weekday(numeric_system::standard);
+ break;
+ case 'u':
+ handler.on_dec1_weekday(numeric_system::standard);
+ break;
+ // Month:
+ case 'b':
+ case 'h':
+ handler.on_abbr_month();
+ break;
+ case 'B':
+ handler.on_full_month();
+ break;
+ case 'm':
+ handler.on_dec_month(numeric_system::standard);
+ break;
+ // Day of the year/month:
+ case 'U':
+ handler.on_dec0_week_of_year(numeric_system::standard);
+ break;
+ case 'W':
+ handler.on_dec1_week_of_year(numeric_system::standard);
+ break;
+ case 'V':
+ handler.on_iso_week_of_year(numeric_system::standard);
+ break;
+ case 'j':
+ handler.on_day_of_year();
+ break;
+ case 'd':
+ handler.on_day_of_month(numeric_system::standard);
+ break;
+ case 'e':
+ handler.on_day_of_month_space(numeric_system::standard);
+ break;
+ // Hour, minute, second:
+ case 'H':
+ handler.on_24_hour(numeric_system::standard, pad);
+ break;
+ case 'I':
+ handler.on_12_hour(numeric_system::standard, pad);
+ break;
+ case 'M':
+ handler.on_minute(numeric_system::standard, pad);
+ break;
+ case 'S':
+ handler.on_second(numeric_system::standard, pad);
+ break;
+ // Other:
+ case 'c':
+ handler.on_datetime(numeric_system::standard);
+ break;
+ case 'x':
+ handler.on_loc_date(numeric_system::standard);
+ break;
+ case 'X':
+ handler.on_loc_time(numeric_system::standard);
+ break;
+ case 'D':
+ handler.on_us_date();
+ break;
+ case 'F':
+ handler.on_iso_date();
+ break;
+ case 'r':
+ handler.on_12_hour_time();
+ break;
+ case 'R':
+ handler.on_24_hour_time();
+ break;
+ case 'T':
+ handler.on_iso_time();
+ break;
+ case 'p':
+ handler.on_am_pm();
+ break;
+ case 'Q':
+ handler.on_duration_value();
+ break;
+ case 'q':
+ handler.on_duration_unit();
+ break;
+ case 'z':
+ handler.on_utc_offset(numeric_system::standard);
+ break;
+ case 'Z':
+ handler.on_tz_name();
+ break;
+ // Alternative representation:
+ case 'E': {
+ if (ptr == end) FMT_THROW(format_error("invalid format"));
+ c = *ptr++;
+ switch (c) {
+ case 'Y':
+ handler.on_year(numeric_system::alternative);
+ break;
+ case 'y':
+ handler.on_offset_year();
+ break;
+ case 'C':
+ handler.on_century(numeric_system::alternative);
+ break;
+ case 'c':
+ handler.on_datetime(numeric_system::alternative);
+ break;
+ case 'x':
+ handler.on_loc_date(numeric_system::alternative);
+ break;
+ case 'X':
+ handler.on_loc_time(numeric_system::alternative);
+ break;
+ case 'z':
+ handler.on_utc_offset(numeric_system::alternative);
+ break;
+ default:
+ FMT_THROW(format_error("invalid format"));
+ }
+ break;
+ }
+ case 'O':
+ if (ptr == end) FMT_THROW(format_error("invalid format"));
+ c = *ptr++;
+ switch (c) {
+ case 'y':
+ handler.on_short_year(numeric_system::alternative);
+ break;
+ case 'm':
+ handler.on_dec_month(numeric_system::alternative);
+ break;
+ case 'U':
+ handler.on_dec0_week_of_year(numeric_system::alternative);
+ break;
+ case 'W':
+ handler.on_dec1_week_of_year(numeric_system::alternative);
+ break;
+ case 'V':
+ handler.on_iso_week_of_year(numeric_system::alternative);
+ break;
+ case 'd':
+ handler.on_day_of_month(numeric_system::alternative);
+ break;
+ case 'e':
+ handler.on_day_of_month_space(numeric_system::alternative);
+ break;
+ case 'w':
+ handler.on_dec0_weekday(numeric_system::alternative);
+ break;
+ case 'u':
+ handler.on_dec1_weekday(numeric_system::alternative);
+ break;
+ case 'H':
+ handler.on_24_hour(numeric_system::alternative, pad);
+ break;
+ case 'I':
+ handler.on_12_hour(numeric_system::alternative, pad);
+ break;
+ case 'M':
+ handler.on_minute(numeric_system::alternative, pad);
+ break;
+ case 'S':
+ handler.on_second(numeric_system::alternative, pad);
+ break;
+ case 'z':
+ handler.on_utc_offset(numeric_system::alternative);
+ break;
+ default:
+ FMT_THROW(format_error("invalid format"));
+ }
+ break;
+ default:
+ FMT_THROW(format_error("invalid format"));
+ }
+ begin = ptr;
+ }
+ if (begin != ptr) handler.on_text(begin, ptr);
+ return ptr;
+}
+
+template <typename Derived> struct null_chrono_spec_handler {
+ FMT_CONSTEXPR void unsupported() {
+ static_cast<Derived*>(this)->unsupported();
+ }
+ FMT_CONSTEXPR void on_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_short_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_offset_year() { unsupported(); }
+ FMT_CONSTEXPR void on_century(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_iso_week_based_year() { unsupported(); }
+ FMT_CONSTEXPR void on_iso_week_based_short_year() { unsupported(); }
+ FMT_CONSTEXPR void on_abbr_weekday() { unsupported(); }
+ FMT_CONSTEXPR void on_full_weekday() { unsupported(); }
+ FMT_CONSTEXPR void on_dec0_weekday(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_dec1_weekday(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_abbr_month() { unsupported(); }
+ FMT_CONSTEXPR void on_full_month() { unsupported(); }
+ FMT_CONSTEXPR void on_dec_month(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_dec0_week_of_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_dec1_week_of_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_iso_week_of_year(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_day_of_year() { unsupported(); }
+ FMT_CONSTEXPR void on_day_of_month(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_day_of_month_space(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_24_hour(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_12_hour(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_minute(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_second(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_datetime(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_loc_date(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_loc_time(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_us_date() { unsupported(); }
+ FMT_CONSTEXPR void on_iso_date() { unsupported(); }
+ FMT_CONSTEXPR void on_12_hour_time() { unsupported(); }
+ FMT_CONSTEXPR void on_24_hour_time() { unsupported(); }
+ FMT_CONSTEXPR void on_iso_time() { unsupported(); }
+ FMT_CONSTEXPR void on_am_pm() { unsupported(); }
+ FMT_CONSTEXPR void on_duration_value() { unsupported(); }
+ FMT_CONSTEXPR void on_duration_unit() { unsupported(); }
+ FMT_CONSTEXPR void on_utc_offset(numeric_system) { unsupported(); }
+ FMT_CONSTEXPR void on_tz_name() { unsupported(); }
+};
+
+struct tm_format_checker : null_chrono_spec_handler<tm_format_checker> {
+ FMT_NORETURN void unsupported() { FMT_THROW(format_error("no format")); }
+
+ template <typename Char>
+ FMT_CONSTEXPR void on_text(const Char*, const Char*) {}
+ FMT_CONSTEXPR void on_year(numeric_system) {}
+ FMT_CONSTEXPR void on_short_year(numeric_system) {}
+ FMT_CONSTEXPR void on_offset_year() {}
+ FMT_CONSTEXPR void on_century(numeric_system) {}
+ FMT_CONSTEXPR void on_iso_week_based_year() {}
+ FMT_CONSTEXPR void on_iso_week_based_short_year() {}
+ FMT_CONSTEXPR void on_abbr_weekday() {}
+ FMT_CONSTEXPR void on_full_weekday() {}
+ FMT_CONSTEXPR void on_dec0_weekday(numeric_system) {}
+ FMT_CONSTEXPR void on_dec1_weekday(numeric_system) {}
+ FMT_CONSTEXPR void on_abbr_month() {}
+ FMT_CONSTEXPR void on_full_month() {}
+ FMT_CONSTEXPR void on_dec_month(numeric_system) {}
+ FMT_CONSTEXPR void on_dec0_week_of_year(numeric_system) {}
+ FMT_CONSTEXPR void on_dec1_week_of_year(numeric_system) {}
+ FMT_CONSTEXPR void on_iso_week_of_year(numeric_system) {}
+ FMT_CONSTEXPR void on_day_of_year() {}
+ FMT_CONSTEXPR void on_day_of_month(numeric_system) {}
+ FMT_CONSTEXPR void on_day_of_month_space(numeric_system) {}
+ FMT_CONSTEXPR void on_24_hour(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_12_hour(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_minute(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_second(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_datetime(numeric_system) {}
+ FMT_CONSTEXPR void on_loc_date(numeric_system) {}
+ FMT_CONSTEXPR void on_loc_time(numeric_system) {}
+ FMT_CONSTEXPR void on_us_date() {}
+ FMT_CONSTEXPR void on_iso_date() {}
+ FMT_CONSTEXPR void on_12_hour_time() {}
+ FMT_CONSTEXPR void on_24_hour_time() {}
+ FMT_CONSTEXPR void on_iso_time() {}
+ FMT_CONSTEXPR void on_am_pm() {}
+ FMT_CONSTEXPR void on_utc_offset(numeric_system) {}
+ FMT_CONSTEXPR void on_tz_name() {}
+};
+
+inline const char* tm_wday_full_name(int wday) {
+ static constexpr const char* full_name_list[] = {
+ "Sunday", "Monday", "Tuesday", "Wednesday",
+ "Thursday", "Friday", "Saturday"};
+ return wday >= 0 && wday <= 6 ? full_name_list[wday] : "?";
+}
+inline const char* tm_wday_short_name(int wday) {
+ static constexpr const char* short_name_list[] = {"Sun", "Mon", "Tue", "Wed",
+ "Thu", "Fri", "Sat"};
+ return wday >= 0 && wday <= 6 ? short_name_list[wday] : "???";
+}
+
+inline const char* tm_mon_full_name(int mon) {
+ static constexpr const char* full_name_list[] = {
+ "January", "February", "March", "April", "May", "June",
+ "July", "August", "September", "October", "November", "December"};
+ return mon >= 0 && mon <= 11 ? full_name_list[mon] : "?";
+}
+inline const char* tm_mon_short_name(int mon) {
+ static constexpr const char* short_name_list[] = {
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+ };
+ return mon >= 0 && mon <= 11 ? short_name_list[mon] : "???";
+}
+
+template <typename T, typename = void>
+struct has_member_data_tm_gmtoff : std::false_type {};
+template <typename T>
+struct has_member_data_tm_gmtoff<T, void_t<decltype(T::tm_gmtoff)>>
+ : std::true_type {};
+
+template <typename T, typename = void>
+struct has_member_data_tm_zone : std::false_type {};
+template <typename T>
+struct has_member_data_tm_zone<T, void_t<decltype(T::tm_zone)>>
+ : std::true_type {};
+
+#if FMT_USE_TZSET
+inline void tzset_once() {
+ static bool init = []() -> bool {
+ _tzset();
+ return true;
+ }();
+ ignore_unused(init);
+}
+#endif
+
+// Converts value to Int and checks that it's in the range [0, upper).
+template <typename T, typename Int, FMT_ENABLE_IF(std::is_integral<T>::value)>
+inline Int to_nonnegative_int(T value, Int upper) {
+ FMT_ASSERT(std::is_unsigned<Int>::value ||
+ (value >= 0 && to_unsigned(value) <= to_unsigned(upper)),
+ "invalid value");
+ (void)upper;
+ return static_cast<Int>(value);
+}
+template <typename T, typename Int, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+inline Int to_nonnegative_int(T value, Int upper) {
+ if (value < 0 || value > static_cast<T>(upper))
+ FMT_THROW(format_error("invalid value"));
+ return static_cast<Int>(value);
+}
+
+constexpr long long pow10(std::uint32_t n) {
+ return n == 0 ? 1 : 10 * pow10(n - 1);
+}
+
+// Counts the number of fractional digits in the range [0, 18] according to the
+// C++20 spec. If more than 18 fractional digits are required then returns 6 for
+// microseconds precision.
+template <long long Num, long long Den, int N = 0,
+ bool Enabled = (N < 19) && (Num <= max_value<long long>() / 10)>
+struct count_fractional_digits {
+ static constexpr int value =
+ Num % Den == 0 ? N : count_fractional_digits<Num * 10, Den, N + 1>::value;
+};
+
+// Base case that doesn't instantiate any more templates
+// in order to avoid overflow.
+template <long long Num, long long Den, int N>
+struct count_fractional_digits<Num, Den, N, false> {
+ static constexpr int value = (Num % Den == 0) ? N : 6;
+};
+
+// Format subseconds which are given as an integer type with an appropriate
+// number of digits.
+template <typename Char, typename OutputIt, typename Duration>
+void write_fractional_seconds(OutputIt& out, Duration d, int precision = -1) {
+ constexpr auto num_fractional_digits =
+ count_fractional_digits<Duration::period::num,
+ Duration::period::den>::value;
+
+ using subsecond_precision = std::chrono::duration<
+ typename std::common_type<typename Duration::rep,
+ std::chrono::seconds::rep>::type,
+ std::ratio<1, detail::pow10(num_fractional_digits)>>;
+
+ const auto fractional =
+ d - std::chrono::duration_cast<std::chrono::seconds>(d);
+ const auto subseconds =
+ std::chrono::treat_as_floating_point<
+ typename subsecond_precision::rep>::value
+ ? fractional.count()
+ : std::chrono::duration_cast<subsecond_precision>(fractional).count();
+ auto n = static_cast<uint32_or_64_or_128_t<long long>>(subseconds);
+ const int num_digits = detail::count_digits(n);
+
+ int leading_zeroes = (std::max)(0, num_fractional_digits - num_digits);
+ if (precision < 0) {
+ FMT_ASSERT(!std::is_floating_point<typename Duration::rep>::value, "");
+ if (std::ratio_less<typename subsecond_precision::period,
+ std::chrono::seconds::period>::value) {
+ *out++ = '.';
+ out = std::fill_n(out, leading_zeroes, '0');
+ out = format_decimal<Char>(out, n, num_digits).end;
+ }
+ } else {
+ *out++ = '.';
+ leading_zeroes = (std::min)(leading_zeroes, precision);
+ out = std::fill_n(out, leading_zeroes, '0');
+ int remaining = precision - leading_zeroes;
+ if (remaining != 0 && remaining < num_digits) {
+ n /= to_unsigned(detail::pow10(to_unsigned(num_digits - remaining)));
+ out = format_decimal<Char>(out, n, remaining).end;
+ return;
+ }
+ out = format_decimal<Char>(out, n, num_digits).end;
+ remaining -= num_digits;
+ out = std::fill_n(out, remaining, '0');
+ }
+}
+
+// Format subseconds which are given as a floating point type with an
+// appropriate number of digits. We cannot pass the Duration here, as we
+// explicitly need to pass the Rep value in the chrono_formatter.
+template <typename Duration>
+void write_floating_seconds(memory_buffer& buf, Duration duration,
+ int num_fractional_digits = -1) {
+ using rep = typename Duration::rep;
+ FMT_ASSERT(std::is_floating_point<rep>::value, "");
+
+ auto val = duration.count();
+
+ if (num_fractional_digits < 0) {
+ // For `std::round` with fallback to `round`:
+ // On some toolchains `std::round` is not available (e.g. GCC 6).
+ using namespace std;
+ num_fractional_digits =
+ count_fractional_digits<Duration::period::num,
+ Duration::period::den>::value;
+ if (num_fractional_digits < 6 && static_cast<rep>(round(val)) != val)
+ num_fractional_digits = 6;
+ }
+
+ format_to(std::back_inserter(buf), FMT_STRING("{:.{}f}"),
+ std::fmod(val * static_cast<rep>(Duration::period::num) /
+ static_cast<rep>(Duration::period::den),
+ static_cast<rep>(60)),
+ num_fractional_digits);
+}
+
+template <typename OutputIt, typename Char,
+ typename Duration = std::chrono::seconds>
+class tm_writer {
+ private:
+ static constexpr int days_per_week = 7;
+
+ const std::locale& loc_;
+ const bool is_classic_;
+ OutputIt out_;
+ const Duration* subsecs_;
+ const std::tm& tm_;
+
+ auto tm_sec() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_sec >= 0 && tm_.tm_sec <= 61, "");
+ return tm_.tm_sec;
+ }
+ auto tm_min() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_min >= 0 && tm_.tm_min <= 59, "");
+ return tm_.tm_min;
+ }
+ auto tm_hour() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_hour >= 0 && tm_.tm_hour <= 23, "");
+ return tm_.tm_hour;
+ }
+ auto tm_mday() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_mday >= 1 && tm_.tm_mday <= 31, "");
+ return tm_.tm_mday;
+ }
+ auto tm_mon() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_mon >= 0 && tm_.tm_mon <= 11, "");
+ return tm_.tm_mon;
+ }
+ auto tm_year() const noexcept -> long long { return 1900ll + tm_.tm_year; }
+ auto tm_wday() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_wday >= 0 && tm_.tm_wday <= 6, "");
+ return tm_.tm_wday;
+ }
+ auto tm_yday() const noexcept -> int {
+ FMT_ASSERT(tm_.tm_yday >= 0 && tm_.tm_yday <= 365, "");
+ return tm_.tm_yday;
+ }
+
+ auto tm_hour12() const noexcept -> int {
+ const auto h = tm_hour();
+ const auto z = h < 12 ? h : h - 12;
+ return z == 0 ? 12 : z;
+ }
+
+ // POSIX and the C Standard are unclear or inconsistent about what %C and %y
+ // do if the year is negative or exceeds 9999. Use the convention that %C
+ // concatenated with %y yields the same output as %Y, and that %Y contains at
+ // least 4 characters, with more only if necessary.
+ auto split_year_lower(long long year) const noexcept -> int {
+ auto l = year % 100;
+ if (l < 0) l = -l; // l in [0, 99]
+ return static_cast<int>(l);
+ }
+
+ // Algorithm:
+ // https://en.wikipedia.org/wiki/ISO_week_date#Calculating_the_week_number_from_a_month_and_day_of_the_month_or_ordinal_date
+ auto iso_year_weeks(long long curr_year) const noexcept -> int {
+ const auto prev_year = curr_year - 1;
+ const auto curr_p =
+ (curr_year + curr_year / 4 - curr_year / 100 + curr_year / 400) %
+ days_per_week;
+ const auto prev_p =
+ (prev_year + prev_year / 4 - prev_year / 100 + prev_year / 400) %
+ days_per_week;
+ return 52 + ((curr_p == 4 || prev_p == 3) ? 1 : 0);
+ }
+ auto iso_week_num(int tm_yday, int tm_wday) const noexcept -> int {
+ return (tm_yday + 11 - (tm_wday == 0 ? days_per_week : tm_wday)) /
+ days_per_week;
+ }
+ auto tm_iso_week_year() const noexcept -> long long {
+ const auto year = tm_year();
+ const auto w = iso_week_num(tm_yday(), tm_wday());
+ if (w < 1) return year - 1;
+ if (w > iso_year_weeks(year)) return year + 1;
+ return year;
+ }
+ auto tm_iso_week_of_year() const noexcept -> int {
+ const auto year = tm_year();
+ const auto w = iso_week_num(tm_yday(), tm_wday());
+ if (w < 1) return iso_year_weeks(year - 1);
+ if (w > iso_year_weeks(year)) return 1;
+ return w;
+ }
+
+ void write1(int value) {
+ *out_++ = static_cast<char>('0' + to_unsigned(value) % 10);
+ }
+ void write2(int value) {
+ const char* d = digits2(to_unsigned(value) % 100);
+ *out_++ = *d++;
+ *out_++ = *d;
+ }
+ void write2(int value, pad_type pad) {
+ unsigned int v = to_unsigned(value) % 100;
+ if (v >= 10) {
+ const char* d = digits2(v);
+ *out_++ = *d++;
+ *out_++ = *d;
+ } else {
+ out_ = detail::write_padding(out_, pad);
+ *out_++ = static_cast<char>('0' + v);
+ }
+ }
+
+ void write_year_extended(long long year) {
+ // At least 4 characters.
+ int width = 4;
+ if (year < 0) {
+ *out_++ = '-';
+ year = 0 - year;
+ --width;
+ }
+ uint32_or_64_or_128_t<long long> n = to_unsigned(year);
+ const int num_digits = count_digits(n);
+ if (width > num_digits) out_ = std::fill_n(out_, width - num_digits, '0');
+ out_ = format_decimal<Char>(out_, n, num_digits).end;
+ }
+ void write_year(long long year) {
+ if (year >= 0 && year < 10000) {
+ write2(static_cast<int>(year / 100));
+ write2(static_cast<int>(year % 100));
+ } else {
+ write_year_extended(year);
+ }
+ }
+
+ void write_utc_offset(long offset, numeric_system ns) {
+ if (offset < 0) {
+ *out_++ = '-';
+ offset = -offset;
+ } else {
+ *out_++ = '+';
+ }
+ offset /= 60;
+ write2(static_cast<int>(offset / 60));
+ if (ns != numeric_system::standard) *out_++ = ':';
+ write2(static_cast<int>(offset % 60));
+ }
+ template <typename T, FMT_ENABLE_IF(has_member_data_tm_gmtoff<T>::value)>
+ void format_utc_offset_impl(const T& tm, numeric_system ns) {
+ write_utc_offset(tm.tm_gmtoff, ns);
+ }
+ template <typename T, FMT_ENABLE_IF(!has_member_data_tm_gmtoff<T>::value)>
+ void format_utc_offset_impl(const T& tm, numeric_system ns) {
+#if defined(_WIN32) && defined(_UCRT)
+# if FMT_USE_TZSET
+ tzset_once();
+# endif
+ long offset = 0;
+ _get_timezone(&offset);
+ if (tm.tm_isdst) {
+ long dstbias = 0;
+ _get_dstbias(&dstbias);
+ offset += dstbias;
+ }
+ write_utc_offset(-offset, ns);
+#else
+ if (ns == numeric_system::standard) return format_localized('z');
+
+ // Extract timezone offset from timezone conversion functions.
+ std::tm gtm = tm;
+ std::time_t gt = std::mktime(&gtm);
+ std::tm ltm = gmtime(gt);
+ std::time_t lt = std::mktime(&ltm);
+ long offset = gt - lt;
+ write_utc_offset(offset, ns);
+#endif
+ }
+
+ template <typename T, FMT_ENABLE_IF(has_member_data_tm_zone<T>::value)>
+ void format_tz_name_impl(const T& tm) {
+ if (is_classic_)
+ out_ = write_tm_str<Char>(out_, tm.tm_zone, loc_);
+ else
+ format_localized('Z');
+ }
+ template <typename T, FMT_ENABLE_IF(!has_member_data_tm_zone<T>::value)>
+ void format_tz_name_impl(const T&) {
+ format_localized('Z');
+ }
+
+ void format_localized(char format, char modifier = 0) {
+ out_ = write<Char>(out_, tm_, loc_, format, modifier);
+ }
+
+ public:
+ tm_writer(const std::locale& loc, OutputIt out, const std::tm& tm,
+ const Duration* subsecs = nullptr)
+ : loc_(loc),
+ is_classic_(loc_ == get_classic_locale()),
+ out_(out),
+ subsecs_(subsecs),
+ tm_(tm) {}
+
+ OutputIt out() const { return out_; }
+
+ FMT_CONSTEXPR void on_text(const Char* begin, const Char* end) {
+ out_ = copy_str<Char>(begin, end, out_);
+ }
+
+ void on_abbr_weekday() {
+ if (is_classic_)
+ out_ = write(out_, tm_wday_short_name(tm_wday()));
+ else
+ format_localized('a');
+ }
+ void on_full_weekday() {
+ if (is_classic_)
+ out_ = write(out_, tm_wday_full_name(tm_wday()));
+ else
+ format_localized('A');
+ }
+ void on_dec0_weekday(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) return write1(tm_wday());
+ format_localized('w', 'O');
+ }
+ void on_dec1_weekday(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ auto wday = tm_wday();
+ write1(wday == 0 ? days_per_week : wday);
+ } else {
+ format_localized('u', 'O');
+ }
+ }
+
+ void on_abbr_month() {
+ if (is_classic_)
+ out_ = write(out_, tm_mon_short_name(tm_mon()));
+ else
+ format_localized('b');
+ }
+ void on_full_month() {
+ if (is_classic_)
+ out_ = write(out_, tm_mon_full_name(tm_mon()));
+ else
+ format_localized('B');
+ }
+
+ void on_datetime(numeric_system ns) {
+ if (is_classic_) {
+ on_abbr_weekday();
+ *out_++ = ' ';
+ on_abbr_month();
+ *out_++ = ' ';
+ on_day_of_month_space(numeric_system::standard);
+ *out_++ = ' ';
+ on_iso_time();
+ *out_++ = ' ';
+ on_year(numeric_system::standard);
+ } else {
+ format_localized('c', ns == numeric_system::standard ? '\0' : 'E');
+ }
+ }
+ void on_loc_date(numeric_system ns) {
+ if (is_classic_)
+ on_us_date();
+ else
+ format_localized('x', ns == numeric_system::standard ? '\0' : 'E');
+ }
+ void on_loc_time(numeric_system ns) {
+ if (is_classic_)
+ on_iso_time();
+ else
+ format_localized('X', ns == numeric_system::standard ? '\0' : 'E');
+ }
+ void on_us_date() {
+ char buf[8];
+ write_digit2_separated(buf, to_unsigned(tm_mon() + 1),
+ to_unsigned(tm_mday()),
+ to_unsigned(split_year_lower(tm_year())), '/');
+ out_ = copy_str<Char>(std::begin(buf), std::end(buf), out_);
+ }
+ void on_iso_date() {
+ auto year = tm_year();
+ char buf[10];
+ size_t offset = 0;
+ if (year >= 0 && year < 10000) {
+ copy2(buf, digits2(static_cast<size_t>(year / 100)));
+ } else {
+ offset = 4;
+ write_year_extended(year);
+ year = 0;
+ }
+ write_digit2_separated(buf + 2, static_cast<unsigned>(year % 100),
+ to_unsigned(tm_mon() + 1), to_unsigned(tm_mday()),
+ '-');
+ out_ = copy_str<Char>(std::begin(buf) + offset, std::end(buf), out_);
+ }
+
+ void on_utc_offset(numeric_system ns) { format_utc_offset_impl(tm_, ns); }
+ void on_tz_name() { format_tz_name_impl(tm_); }
+
+ void on_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write_year(tm_year());
+ format_localized('Y', 'E');
+ }
+ void on_short_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(split_year_lower(tm_year()));
+ format_localized('y', 'O');
+ }
+ void on_offset_year() {
+ if (is_classic_) return write2(split_year_lower(tm_year()));
+ format_localized('y', 'E');
+ }
+
+ void on_century(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ auto year = tm_year();
+ auto upper = year / 100;
+ if (year >= -99 && year < 0) {
+ // Zero upper on negative year.
+ *out_++ = '-';
+ *out_++ = '0';
+ } else if (upper >= 0 && upper < 100) {
+ write2(static_cast<int>(upper));
+ } else {
+ out_ = write<Char>(out_, upper);
+ }
+ } else {
+ format_localized('C', 'E');
+ }
+ }
+
+ void on_dec_month(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_mon() + 1);
+ format_localized('m', 'O');
+ }
+
+ void on_dec0_week_of_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2((tm_yday() + days_per_week - tm_wday()) / days_per_week);
+ format_localized('U', 'O');
+ }
+ void on_dec1_week_of_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ auto wday = tm_wday();
+ write2((tm_yday() + days_per_week -
+ (wday == 0 ? (days_per_week - 1) : (wday - 1))) /
+ days_per_week);
+ } else {
+ format_localized('W', 'O');
+ }
+ }
+ void on_iso_week_of_year(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_iso_week_of_year());
+ format_localized('V', 'O');
+ }
+
+ void on_iso_week_based_year() { write_year(tm_iso_week_year()); }
+ void on_iso_week_based_short_year() {
+ write2(split_year_lower(tm_iso_week_year()));
+ }
+
+ void on_day_of_year() {
+ auto yday = tm_yday() + 1;
+ write1(yday / 100);
+ write2(yday % 100);
+ }
+ void on_day_of_month(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) return write2(tm_mday());
+ format_localized('d', 'O');
+ }
+ void on_day_of_month_space(numeric_system ns) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ auto mday = to_unsigned(tm_mday()) % 100;
+ const char* d2 = digits2(mday);
+ *out_++ = mday < 10 ? ' ' : d2[0];
+ *out_++ = d2[1];
+ } else {
+ format_localized('e', 'O');
+ }
+ }
+
+ void on_24_hour(numeric_system ns, pad_type pad) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_hour(), pad);
+ format_localized('H', 'O');
+ }
+ void on_12_hour(numeric_system ns, pad_type pad) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_hour12(), pad);
+ format_localized('I', 'O');
+ }
+ void on_minute(numeric_system ns, pad_type pad) {
+ if (is_classic_ || ns == numeric_system::standard)
+ return write2(tm_min(), pad);
+ format_localized('M', 'O');
+ }
+
+ void on_second(numeric_system ns, pad_type pad) {
+ if (is_classic_ || ns == numeric_system::standard) {
+ write2(tm_sec(), pad);
+ if (subsecs_) {
+ if (std::is_floating_point<typename Duration::rep>::value) {
+ auto buf = memory_buffer();
+ write_floating_seconds(buf, *subsecs_);
+ if (buf.size() > 1) {
+ // Remove the leading "0", write something like ".123".
+ out_ = std::copy(buf.begin() + 1, buf.end(), out_);
+ }
+ } else {
+ write_fractional_seconds<Char>(out_, *subsecs_);
+ }
+ }
+ } else {
+ // Currently no formatting of subseconds when a locale is set.
+ format_localized('S', 'O');
+ }
+ }
+
+ void on_12_hour_time() {
+ if (is_classic_) {
+ char buf[8];
+ write_digit2_separated(buf, to_unsigned(tm_hour12()),
+ to_unsigned(tm_min()), to_unsigned(tm_sec()), ':');
+ out_ = copy_str<Char>(std::begin(buf), std::end(buf), out_);
+ *out_++ = ' ';
+ on_am_pm();
+ } else {
+ format_localized('r');
+ }
+ }
+ void on_24_hour_time() {
+ write2(tm_hour());
+ *out_++ = ':';
+ write2(tm_min());
+ }
+ void on_iso_time() {
+ on_24_hour_time();
+ *out_++ = ':';
+ on_second(numeric_system::standard, pad_type::unspecified);
+ }
+
+ void on_am_pm() {
+ if (is_classic_) {
+ *out_++ = tm_hour() < 12 ? 'A' : 'P';
+ *out_++ = 'M';
+ } else {
+ format_localized('p');
+ }
+ }
+
+ // These apply to chrono durations but not tm.
+ void on_duration_value() {}
+ void on_duration_unit() {}
+};
+
+struct chrono_format_checker : null_chrono_spec_handler<chrono_format_checker> {
+ bool has_precision_integral = false;
+
+ FMT_NORETURN void unsupported() { FMT_THROW(format_error("no date")); }
+
+ template <typename Char>
+ FMT_CONSTEXPR void on_text(const Char*, const Char*) {}
+ FMT_CONSTEXPR void on_24_hour(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_12_hour(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_minute(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_second(numeric_system, pad_type) {}
+ FMT_CONSTEXPR void on_12_hour_time() {}
+ FMT_CONSTEXPR void on_24_hour_time() {}
+ FMT_CONSTEXPR void on_iso_time() {}
+ FMT_CONSTEXPR void on_am_pm() {}
+ FMT_CONSTEXPR void on_duration_value() const {
+ if (has_precision_integral) {
+ FMT_THROW(format_error("precision not allowed for this argument type"));
+ }
+ }
+ FMT_CONSTEXPR void on_duration_unit() {}
+};
+
+template <typename T,
+ FMT_ENABLE_IF(std::is_integral<T>::value&& has_isfinite<T>::value)>
+inline bool isfinite(T) {
+ return true;
+}
+
+template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+inline T mod(T x, int y) {
+ return x % static_cast<T>(y);
+}
+template <typename T, FMT_ENABLE_IF(std::is_floating_point<T>::value)>
+inline T mod(T x, int y) {
+ return std::fmod(x, static_cast<T>(y));
+}
+
+// If T is an integral type, maps T to its unsigned counterpart, otherwise
+// leaves it unchanged (unlike std::make_unsigned).
+template <typename T, bool INTEGRAL = std::is_integral<T>::value>
+struct make_unsigned_or_unchanged {
+ using type = T;
+};
+
+template <typename T> struct make_unsigned_or_unchanged<T, true> {
+ using type = typename std::make_unsigned<T>::type;
+};
+
+#if FMT_SAFE_DURATION_CAST
+// throwing version of safe_duration_cast
+template <typename To, typename FromRep, typename FromPeriod>
+To fmt_safe_duration_cast(std::chrono::duration<FromRep, FromPeriod> from) {
+ int ec;
+ To to = safe_duration_cast::safe_duration_cast<To>(from, ec);
+ if (ec) FMT_THROW(format_error("cannot format duration"));
+ return to;
+}
+#endif
+
+template <typename Rep, typename Period,
+ FMT_ENABLE_IF(std::is_integral<Rep>::value)>
+inline std::chrono::duration<Rep, std::milli> get_milliseconds(
+ std::chrono::duration<Rep, Period> d) {
+ // this may overflow and/or the result may not fit in the
+ // target type.
+#if FMT_SAFE_DURATION_CAST
+ using CommonSecondsType =
+ typename std::common_type<decltype(d), std::chrono::seconds>::type;
+ const auto d_as_common = fmt_safe_duration_cast<CommonSecondsType>(d);
+ const auto d_as_whole_seconds =
+ fmt_safe_duration_cast<std::chrono::seconds>(d_as_common);
+ // this conversion should be nonproblematic
+ const auto diff = d_as_common - d_as_whole_seconds;
+ const auto ms =
+ fmt_safe_duration_cast<std::chrono::duration<Rep, std::milli>>(diff);
+ return ms;
+#else
+ auto s = std::chrono::duration_cast<std::chrono::seconds>(d);
+ return std::chrono::duration_cast<std::chrono::milliseconds>(d - s);
+#endif
+}
+
+template <typename Char, typename Rep, typename OutputIt,
+ FMT_ENABLE_IF(std::is_integral<Rep>::value)>
+OutputIt format_duration_value(OutputIt out, Rep val, int) {
+ return write<Char>(out, val);
+}
+
+template <typename Char, typename Rep, typename OutputIt,
+ FMT_ENABLE_IF(std::is_floating_point<Rep>::value)>
+OutputIt format_duration_value(OutputIt out, Rep val, int precision) {
+ auto specs = format_specs<Char>();
+ specs.precision = precision;
+ specs.type = precision >= 0 ? presentation_type::fixed_lower
+ : presentation_type::general_lower;
+ return write<Char>(out, val, specs);
+}
+
+template <typename Char, typename OutputIt>
+OutputIt copy_unit(string_view unit, OutputIt out, Char) {
+ return std::copy(unit.begin(), unit.end(), out);
+}
+
+template <typename OutputIt>
+OutputIt copy_unit(string_view unit, OutputIt out, wchar_t) {
+ // This works when wchar_t is UTF-32 because units only contain characters
+ // that have the same representation in UTF-16 and UTF-32.
+ utf8_to_utf16 u(unit);
+ return std::copy(u.c_str(), u.c_str() + u.size(), out);
+}
+
+template <typename Char, typename Period, typename OutputIt>
+OutputIt format_duration_unit(OutputIt out) {
+ if (const char* unit = get_units<Period>())
+ return copy_unit(string_view(unit), out, Char());
+ *out++ = '[';
+ out = write<Char>(out, Period::num);
+ if (const_check(Period::den != 1)) {
+ *out++ = '/';
+ out = write<Char>(out, Period::den);
+ }
+ *out++ = ']';
+ *out++ = 's';
+ return out;
+}
+
+class get_locale {
+ private:
+ union {
+ std::locale locale_;
+ };
+ bool has_locale_ = false;
+
+ public:
+ get_locale(bool localized, locale_ref loc) : has_locale_(localized) {
+ if (localized)
+ ::new (&locale_) std::locale(loc.template get<std::locale>());
+ }
+ ~get_locale() {
+ if (has_locale_) locale_.~locale();
+ }
+ operator const std::locale&() const {
+ return has_locale_ ? locale_ : get_classic_locale();
+ }
+};
+
+template <typename FormatContext, typename OutputIt, typename Rep,
+ typename Period>
+struct chrono_formatter {
+ FormatContext& context;
+ OutputIt out;
+ int precision;
+ bool localized = false;
+ // rep is unsigned to avoid overflow.
+ using rep =
+ conditional_t<std::is_integral<Rep>::value && sizeof(Rep) < sizeof(int),
+ unsigned, typename make_unsigned_or_unchanged<Rep>::type>;
+ rep val;
+ using seconds = std::chrono::duration<rep>;
+ seconds s;
+ using milliseconds = std::chrono::duration<rep, std::milli>;
+ bool negative;
+
+ using char_type = typename FormatContext::char_type;
+ using tm_writer_type = tm_writer<OutputIt, char_type>;
+
+ chrono_formatter(FormatContext& ctx, OutputIt o,
+ std::chrono::duration<Rep, Period> d)
+ : context(ctx),
+ out(o),
+ val(static_cast<rep>(d.count())),
+ negative(false) {
+ if (d.count() < 0) {
+ val = 0 - val;
+ negative = true;
+ }
+
+ // this may overflow and/or the result may not fit in the
+ // target type.
+#if FMT_SAFE_DURATION_CAST
+ // might need checked conversion (rep!=Rep)
+ auto tmpval = std::chrono::duration<rep, Period>(val);
+ s = fmt_safe_duration_cast<seconds>(tmpval);
+#else
+ s = std::chrono::duration_cast<seconds>(
+ std::chrono::duration<rep, Period>(val));
+#endif
+ }
+
+ // returns true if nan or inf, writes to out.
+ bool handle_nan_inf() {
+ if (isfinite(val)) {
+ return false;
+ }
+ if (isnan(val)) {
+ write_nan();
+ return true;
+ }
+ // must be +-inf
+ if (val > 0) {
+ write_pinf();
+ } else {
+ write_ninf();
+ }
+ return true;
+ }
+
+ Rep hour() const { return static_cast<Rep>(mod((s.count() / 3600), 24)); }
+
+ Rep hour12() const {
+ Rep hour = static_cast<Rep>(mod((s.count() / 3600), 12));
+ return hour <= 0 ? 12 : hour;
+ }
+
+ Rep minute() const { return static_cast<Rep>(mod((s.count() / 60), 60)); }
+ Rep second() const { return static_cast<Rep>(mod(s.count(), 60)); }
+
+ std::tm time() const {
+ auto time = std::tm();
+ time.tm_hour = to_nonnegative_int(hour(), 24);
+ time.tm_min = to_nonnegative_int(minute(), 60);
+ time.tm_sec = to_nonnegative_int(second(), 60);
+ return time;
+ }
+
+ void write_sign() {
+ if (negative) {
+ *out++ = '-';
+ negative = false;
+ }
+ }
+
+ void write(Rep value, int width, pad_type pad = pad_type::unspecified) {
+ write_sign();
+ if (isnan(value)) return write_nan();
+ uint32_or_64_or_128_t<int> n =
+ to_unsigned(to_nonnegative_int(value, max_value<int>()));
+ int num_digits = detail::count_digits(n);
+ if (width > num_digits) {
+ out = detail::write_padding(out, pad, width - num_digits);
+ }
+ out = format_decimal<char_type>(out, n, num_digits).end;
+ }
+
+ void write_nan() { std::copy_n("nan", 3, out); }
+ void write_pinf() { std::copy_n("inf", 3, out); }
+ void write_ninf() { std::copy_n("-inf", 4, out); }
+
+ template <typename Callback, typename... Args>
+ void format_tm(const tm& time, Callback cb, Args... args) {
+ if (isnan(val)) return write_nan();
+ get_locale loc(localized, context.locale());
+ auto w = tm_writer_type(loc, out, time);
+ (w.*cb)(args...);
+ out = w.out();
+ }
+
+ void on_text(const char_type* begin, const char_type* end) {
+ std::copy(begin, end, out);
+ }
+
+ // These are not implemented because durations don't have date information.
+ void on_abbr_weekday() {}
+ void on_full_weekday() {}
+ void on_dec0_weekday(numeric_system) {}
+ void on_dec1_weekday(numeric_system) {}
+ void on_abbr_month() {}
+ void on_full_month() {}
+ void on_datetime(numeric_system) {}
+ void on_loc_date(numeric_system) {}
+ void on_loc_time(numeric_system) {}
+ void on_us_date() {}
+ void on_iso_date() {}
+ void on_utc_offset(numeric_system) {}
+ void on_tz_name() {}
+ void on_year(numeric_system) {}
+ void on_short_year(numeric_system) {}
+ void on_offset_year() {}
+ void on_century(numeric_system) {}
+ void on_iso_week_based_year() {}
+ void on_iso_week_based_short_year() {}
+ void on_dec_month(numeric_system) {}
+ void on_dec0_week_of_year(numeric_system) {}
+ void on_dec1_week_of_year(numeric_system) {}
+ void on_iso_week_of_year(numeric_system) {}
+ void on_day_of_year() {}
+ void on_day_of_month(numeric_system) {}
+ void on_day_of_month_space(numeric_system) {}
+
+ void on_24_hour(numeric_system ns, pad_type pad) {
+ if (handle_nan_inf()) return;
+
+ if (ns == numeric_system::standard) return write(hour(), 2, pad);
+ auto time = tm();
+ time.tm_hour = to_nonnegative_int(hour(), 24);
+ format_tm(time, &tm_writer_type::on_24_hour, ns, pad);
+ }
+
+ void on_12_hour(numeric_system ns, pad_type pad) {
+ if (handle_nan_inf()) return;
+
+ if (ns == numeric_system::standard) return write(hour12(), 2, pad);
+ auto time = tm();
+ time.tm_hour = to_nonnegative_int(hour12(), 12);
+ format_tm(time, &tm_writer_type::on_12_hour, ns, pad);
+ }
+
+ void on_minute(numeric_system ns, pad_type pad) {
+ if (handle_nan_inf()) return;
+
+ if (ns == numeric_system::standard) return write(minute(), 2, pad);
+ auto time = tm();
+ time.tm_min = to_nonnegative_int(minute(), 60);
+ format_tm(time, &tm_writer_type::on_minute, ns, pad);
+ }
+
+ void on_second(numeric_system ns, pad_type pad) {
+ if (handle_nan_inf()) return;
+
+ if (ns == numeric_system::standard) {
+ if (std::is_floating_point<rep>::value) {
+ auto buf = memory_buffer();
+ write_floating_seconds(buf, std::chrono::duration<rep, Period>(val),
+ precision);
+ if (negative) *out++ = '-';
+ if (buf.size() < 2 || buf[1] == '.') {
+ out = detail::write_padding(out, pad);
+ }
+ out = std::copy(buf.begin(), buf.end(), out);
+ } else {
+ write(second(), 2, pad);
+ write_fractional_seconds<char_type>(
+ out, std::chrono::duration<rep, Period>(val), precision);
+ }
+ return;
+ }
+ auto time = tm();
+ time.tm_sec = to_nonnegative_int(second(), 60);
+ format_tm(time, &tm_writer_type::on_second, ns, pad);
+ }
+
+ void on_12_hour_time() {
+ if (handle_nan_inf()) return;
+ format_tm(time(), &tm_writer_type::on_12_hour_time);
+ }
+
+ void on_24_hour_time() {
+ if (handle_nan_inf()) {
+ *out++ = ':';
+ handle_nan_inf();
+ return;
+ }
+
+ write(hour(), 2);
+ *out++ = ':';
+ write(minute(), 2);
+ }
+
+ void on_iso_time() {
+ on_24_hour_time();
+ *out++ = ':';
+ if (handle_nan_inf()) return;
+ on_second(numeric_system::standard, pad_type::unspecified);
+ }
+
+ void on_am_pm() {
+ if (handle_nan_inf()) return;
+ format_tm(time(), &tm_writer_type::on_am_pm);
+ }
+
+ void on_duration_value() {
+ if (handle_nan_inf()) return;
+ write_sign();
+ out = format_duration_value<char_type>(out, val, precision);
+ }
+
+ void on_duration_unit() {
+ out = format_duration_unit<char_type, Period>(out);
+ }
+};
+
+FMT_END_DETAIL_NAMESPACE
+
+#if defined(__cpp_lib_chrono) && __cpp_lib_chrono >= 201907
+using weekday = std::chrono::weekday;
+#else
+// A fallback version of weekday.
+class weekday {
+ private:
+ unsigned char value;
+
+ public:
+ weekday() = default;
+ explicit constexpr weekday(unsigned wd) noexcept
+ : value(static_cast<unsigned char>(wd != 7 ? wd : 0)) {}
+ constexpr unsigned c_encoding() const noexcept { return value; }
+};
+
+class year_month_day {};
+#endif
+
+// A rudimentary weekday formatter.
+template <typename Char> struct formatter<weekday, Char> {
+ private:
+ bool localized = false;
+
+ public:
+ FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
+ -> decltype(ctx.begin()) {
+ auto begin = ctx.begin(), end = ctx.end();
+ if (begin != end && *begin == 'L') {
+ ++begin;
+ localized = true;
+ }
+ return begin;
+ }
+
+ template <typename FormatContext>
+ auto format(weekday wd, FormatContext& ctx) const -> decltype(ctx.out()) {
+ auto time = std::tm();
+ time.tm_wday = static_cast<int>(wd.c_encoding());
+ detail::get_locale loc(localized, ctx.locale());
+ auto w = detail::tm_writer<decltype(ctx.out()), Char>(loc, ctx.out(), time);
+ w.on_abbr_weekday();
+ return w.out();
+ }
+};
+
+template <typename Rep, typename Period, typename Char>
+struct formatter<std::chrono::duration<Rep, Period>, Char> {
+ private:
+ format_specs<Char> specs;
+ int precision = -1;
+ using arg_ref_type = detail::arg_ref<Char>;
+ arg_ref_type width_ref;
+ arg_ref_type precision_ref;
+ bool localized = false;
+ basic_string_view<Char> format_str;
+ using duration = std::chrono::duration<Rep, Period>;
+
+ using iterator = typename basic_format_parse_context<Char>::iterator;
+ struct parse_range {
+ iterator begin;
+ iterator end;
+ };
+
+ FMT_CONSTEXPR parse_range do_parse(basic_format_parse_context<Char>& ctx) {
+ auto begin = ctx.begin(), end = ctx.end();
+ if (begin == end || *begin == '}') return {begin, begin};
+
+ begin = detail::parse_align(begin, end, specs);
+ if (begin == end) return {begin, begin};
+
+ begin = detail::parse_dynamic_spec(begin, end, specs.width, width_ref, ctx);
+ if (begin == end) return {begin, begin};
+
+ auto checker = detail::chrono_format_checker();
+ if (*begin == '.') {
+ checker.has_precision_integral = !std::is_floating_point<Rep>::value;
+ begin =
+ detail::parse_precision(begin, end, precision, precision_ref, ctx);
+ }
+ if (begin != end && *begin == 'L') {
+ ++begin;
+ localized = true;
+ }
+ end = detail::parse_chrono_format(begin, end, checker);
+ return {begin, end};
+ }
+
+ public:
+ FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
+ -> decltype(ctx.begin()) {
+ auto range = do_parse(ctx);
+ format_str = basic_string_view<Char>(
+ &*range.begin, detail::to_unsigned(range.end - range.begin));
+ return range.end;
+ }
+
+ template <typename FormatContext>
+ auto format(const duration& d, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ auto specs_copy = specs;
+ auto precision_copy = precision;
+ auto begin = format_str.begin(), end = format_str.end();
+ // As a possible future optimization, we could avoid extra copying if width
+ // is not specified.
+ basic_memory_buffer<Char> buf;
+ auto out = std::back_inserter(buf);
+ detail::handle_dynamic_spec<detail::width_checker>(specs_copy.width,
+ width_ref, ctx);
+ detail::handle_dynamic_spec<detail::precision_checker>(precision_copy,
+ precision_ref, ctx);
+ if (begin == end || *begin == '}') {
+ out = detail::format_duration_value<Char>(out, d.count(), precision_copy);
+ detail::format_duration_unit<Char, Period>(out);
+ } else {
+ detail::chrono_formatter<FormatContext, decltype(out), Rep, Period> f(
+ ctx, out, d);
+ f.precision = precision_copy;
+ f.localized = localized;
+ detail::parse_chrono_format(begin, end, f);
+ }
+ return detail::write(
+ ctx.out(), basic_string_view<Char>(buf.data(), buf.size()), specs_copy);
+ }
+};
+
+template <typename Char, typename Duration>
+struct formatter<std::chrono::time_point<std::chrono::system_clock, Duration>,
+ Char> : formatter<std::tm, Char> {
+ FMT_CONSTEXPR formatter() {
+ this->format_str = detail::string_literal<Char, '%', 'F', ' ', '%', 'T'>{};
+ }
+
+ template <typename FormatContext>
+ auto format(std::chrono::time_point<std::chrono::system_clock, Duration> val,
+ FormatContext& ctx) const -> decltype(ctx.out()) {
+ using period = typename Duration::period;
+ if (period::num != 1 || period::den != 1 ||
+ std::is_floating_point<typename Duration::rep>::value) {
+ const auto epoch = val.time_since_epoch();
+ auto subsecs = std::chrono::duration_cast<Duration>(
+ epoch - std::chrono::duration_cast<std::chrono::seconds>(epoch));
+
+ if (subsecs.count() < 0) {
+ auto second = std::chrono::seconds(1);
+ if (epoch.count() < ((Duration::min)() + second).count())
+ FMT_THROW(format_error("duration is too small"));
+ subsecs += second;
+ val -= second;
+ }
+
+ return formatter<std::tm, Char>::do_format(
+ gmtime(std::chrono::time_point_cast<std::chrono::seconds>(val)), ctx,
+ &subsecs);
+ }
+
+ return formatter<std::tm, Char>::format(
+ gmtime(std::chrono::time_point_cast<std::chrono::seconds>(val)), ctx);
+ }
+};
+
+#if FMT_USE_LOCAL_TIME
+template <typename Char, typename Duration>
+struct formatter<std::chrono::local_time<Duration>, Char>
+ : formatter<std::tm, Char> {
+ FMT_CONSTEXPR formatter() {
+ this->format_str = detail::string_literal<Char, '%', 'F', ' ', '%', 'T'>{};
+ }
+
+ template <typename FormatContext>
+ auto format(std::chrono::local_time<Duration> val, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ using period = typename Duration::period;
+ if (period::num != 1 || period::den != 1 ||
+ std::is_floating_point<typename Duration::rep>::value) {
+ const auto epoch = val.time_since_epoch();
+ const auto subsecs = std::chrono::duration_cast<Duration>(
+ epoch - std::chrono::duration_cast<std::chrono::seconds>(epoch));
+
+ return formatter<std::tm, Char>::do_format(
+ localtime(std::chrono::time_point_cast<std::chrono::seconds>(val)),
+ ctx, &subsecs);
+ }
+
+ return formatter<std::tm, Char>::format(
+ localtime(std::chrono::time_point_cast<std::chrono::seconds>(val)),
+ ctx);
+ }
+};
+#endif
+
+#if FMT_USE_UTC_TIME
+template <typename Char, typename Duration>
+struct formatter<std::chrono::time_point<std::chrono::utc_clock, Duration>,
+ Char>
+ : formatter<std::chrono::time_point<std::chrono::system_clock, Duration>,
+ Char> {
+ template <typename FormatContext>
+ auto format(std::chrono::time_point<std::chrono::utc_clock, Duration> val,
+ FormatContext& ctx) const -> decltype(ctx.out()) {
+ return formatter<
+ std::chrono::time_point<std::chrono::system_clock, Duration>,
+ Char>::format(std::chrono::utc_clock::to_sys(val), ctx);
+ }
+};
+#endif
+
+template <typename Char> struct formatter<std::tm, Char> {
+ private:
+ format_specs<Char> specs;
+ detail::arg_ref<Char> width_ref;
+
+ protected:
+ basic_string_view<Char> format_str;
+
+ FMT_CONSTEXPR auto do_parse(basic_format_parse_context<Char>& ctx)
+ -> decltype(ctx.begin()) {
+ auto begin = ctx.begin(), end = ctx.end();
+ if (begin == end || *begin == '}') return begin;
+
+ begin = detail::parse_align(begin, end, specs);
+ if (begin == end) return end;
+
+ begin = detail::parse_dynamic_spec(begin, end, specs.width, width_ref, ctx);
+ if (begin == end) return end;
+
+ end = detail::parse_chrono_format(begin, end, detail::tm_format_checker());
+ // Replace default format_str only if the new spec is not empty.
+ if (end != begin) format_str = {begin, detail::to_unsigned(end - begin)};
+ return end;
+ }
+
+ template <typename FormatContext, typename Duration>
+ auto do_format(const std::tm& tm, FormatContext& ctx,
+ const Duration* subsecs) const -> decltype(ctx.out()) {
+ auto specs_copy = specs;
+ basic_memory_buffer<Char> buf;
+ auto out = std::back_inserter(buf);
+ detail::handle_dynamic_spec<detail::width_checker>(specs_copy.width,
+ width_ref, ctx);
+
+ const auto loc_ref = ctx.locale();
+ detail::get_locale loc(static_cast<bool>(loc_ref), loc_ref);
+ auto w =
+ detail::tm_writer<decltype(out), Char, Duration>(loc, out, tm, subsecs);
+ detail::parse_chrono_format(format_str.begin(), format_str.end(), w);
+ return detail::write(
+ ctx.out(), basic_string_view<Char>(buf.data(), buf.size()), specs_copy);
+ }
+
+ public:
+ FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
+ -> decltype(ctx.begin()) {
+ return this->do_parse(ctx);
+ }
+
+ template <typename FormatContext>
+ auto format(const std::tm& tm, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return do_format<FormatContext, std::chrono::seconds>(tm, ctx, nullptr);
+ }
+};
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_CHRONO_H_
diff --git a/src/fmtlib/fmt/color.h b/src/fmtlib/fmt/color.h
new file mode 100644
index 0000000..d175448
--- /dev/null
+++ b/src/fmtlib/fmt/color.h
@@ -0,0 +1,633 @@
+// Formatting library for C++ - color support
+//
+// Copyright (c) 2018 - present, Victor Zverovich and fmt contributors
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_COLOR_H_
+#define FMT_COLOR_H_
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+FMT_BEGIN_EXPORT
+
+enum class color : uint32_t {
+ alice_blue = 0xF0F8FF, // rgb(240,248,255)
+ antique_white = 0xFAEBD7, // rgb(250,235,215)
+ aqua = 0x00FFFF, // rgb(0,255,255)
+ aquamarine = 0x7FFFD4, // rgb(127,255,212)
+ azure = 0xF0FFFF, // rgb(240,255,255)
+ beige = 0xF5F5DC, // rgb(245,245,220)
+ bisque = 0xFFE4C4, // rgb(255,228,196)
+ black = 0x000000, // rgb(0,0,0)
+ blanched_almond = 0xFFEBCD, // rgb(255,235,205)
+ blue = 0x0000FF, // rgb(0,0,255)
+ blue_violet = 0x8A2BE2, // rgb(138,43,226)
+ brown = 0xA52A2A, // rgb(165,42,42)
+ burly_wood = 0xDEB887, // rgb(222,184,135)
+ cadet_blue = 0x5F9EA0, // rgb(95,158,160)
+ chartreuse = 0x7FFF00, // rgb(127,255,0)
+ chocolate = 0xD2691E, // rgb(210,105,30)
+ coral = 0xFF7F50, // rgb(255,127,80)
+ cornflower_blue = 0x6495ED, // rgb(100,149,237)
+ cornsilk = 0xFFF8DC, // rgb(255,248,220)
+ crimson = 0xDC143C, // rgb(220,20,60)
+ cyan = 0x00FFFF, // rgb(0,255,255)
+ dark_blue = 0x00008B, // rgb(0,0,139)
+ dark_cyan = 0x008B8B, // rgb(0,139,139)
+ dark_golden_rod = 0xB8860B, // rgb(184,134,11)
+ dark_gray = 0xA9A9A9, // rgb(169,169,169)
+ dark_green = 0x006400, // rgb(0,100,0)
+ dark_khaki = 0xBDB76B, // rgb(189,183,107)
+ dark_magenta = 0x8B008B, // rgb(139,0,139)
+ dark_olive_green = 0x556B2F, // rgb(85,107,47)
+ dark_orange = 0xFF8C00, // rgb(255,140,0)
+ dark_orchid = 0x9932CC, // rgb(153,50,204)
+ dark_red = 0x8B0000, // rgb(139,0,0)
+ dark_salmon = 0xE9967A, // rgb(233,150,122)
+ dark_sea_green = 0x8FBC8F, // rgb(143,188,143)
+ dark_slate_blue = 0x483D8B, // rgb(72,61,139)
+ dark_slate_gray = 0x2F4F4F, // rgb(47,79,79)
+ dark_turquoise = 0x00CED1, // rgb(0,206,209)
+ dark_violet = 0x9400D3, // rgb(148,0,211)
+ deep_pink = 0xFF1493, // rgb(255,20,147)
+ deep_sky_blue = 0x00BFFF, // rgb(0,191,255)
+ dim_gray = 0x696969, // rgb(105,105,105)
+ dodger_blue = 0x1E90FF, // rgb(30,144,255)
+ fire_brick = 0xB22222, // rgb(178,34,34)
+ floral_white = 0xFFFAF0, // rgb(255,250,240)
+ forest_green = 0x228B22, // rgb(34,139,34)
+ fuchsia = 0xFF00FF, // rgb(255,0,255)
+ gainsboro = 0xDCDCDC, // rgb(220,220,220)
+ ghost_white = 0xF8F8FF, // rgb(248,248,255)
+ gold = 0xFFD700, // rgb(255,215,0)
+ golden_rod = 0xDAA520, // rgb(218,165,32)
+ gray = 0x808080, // rgb(128,128,128)
+ green = 0x008000, // rgb(0,128,0)
+ green_yellow = 0xADFF2F, // rgb(173,255,47)
+ honey_dew = 0xF0FFF0, // rgb(240,255,240)
+ hot_pink = 0xFF69B4, // rgb(255,105,180)
+ indian_red = 0xCD5C5C, // rgb(205,92,92)
+ indigo = 0x4B0082, // rgb(75,0,130)
+ ivory = 0xFFFFF0, // rgb(255,255,240)
+ khaki = 0xF0E68C, // rgb(240,230,140)
+ lavender = 0xE6E6FA, // rgb(230,230,250)
+ lavender_blush = 0xFFF0F5, // rgb(255,240,245)
+ lawn_green = 0x7CFC00, // rgb(124,252,0)
+ lemon_chiffon = 0xFFFACD, // rgb(255,250,205)
+ light_blue = 0xADD8E6, // rgb(173,216,230)
+ light_coral = 0xF08080, // rgb(240,128,128)
+ light_cyan = 0xE0FFFF, // rgb(224,255,255)
+ light_golden_rod_yellow = 0xFAFAD2, // rgb(250,250,210)
+ light_gray = 0xD3D3D3, // rgb(211,211,211)
+ light_green = 0x90EE90, // rgb(144,238,144)
+ light_pink = 0xFFB6C1, // rgb(255,182,193)
+ light_salmon = 0xFFA07A, // rgb(255,160,122)
+ light_sea_green = 0x20B2AA, // rgb(32,178,170)
+ light_sky_blue = 0x87CEFA, // rgb(135,206,250)
+ light_slate_gray = 0x778899, // rgb(119,136,153)
+ light_steel_blue = 0xB0C4DE, // rgb(176,196,222)
+ light_yellow = 0xFFFFE0, // rgb(255,255,224)
+ lime = 0x00FF00, // rgb(0,255,0)
+ lime_green = 0x32CD32, // rgb(50,205,50)
+ linen = 0xFAF0E6, // rgb(250,240,230)
+ magenta = 0xFF00FF, // rgb(255,0,255)
+ maroon = 0x800000, // rgb(128,0,0)
+ medium_aquamarine = 0x66CDAA, // rgb(102,205,170)
+ medium_blue = 0x0000CD, // rgb(0,0,205)
+ medium_orchid = 0xBA55D3, // rgb(186,85,211)
+ medium_purple = 0x9370DB, // rgb(147,112,219)
+ medium_sea_green = 0x3CB371, // rgb(60,179,113)
+ medium_slate_blue = 0x7B68EE, // rgb(123,104,238)
+ medium_spring_green = 0x00FA9A, // rgb(0,250,154)
+ medium_turquoise = 0x48D1CC, // rgb(72,209,204)
+ medium_violet_red = 0xC71585, // rgb(199,21,133)
+ midnight_blue = 0x191970, // rgb(25,25,112)
+ mint_cream = 0xF5FFFA, // rgb(245,255,250)
+ misty_rose = 0xFFE4E1, // rgb(255,228,225)
+ moccasin = 0xFFE4B5, // rgb(255,228,181)
+ navajo_white = 0xFFDEAD, // rgb(255,222,173)
+ navy = 0x000080, // rgb(0,0,128)
+ old_lace = 0xFDF5E6, // rgb(253,245,230)
+ olive = 0x808000, // rgb(128,128,0)
+ olive_drab = 0x6B8E23, // rgb(107,142,35)
+ orange = 0xFFA500, // rgb(255,165,0)
+ orange_red = 0xFF4500, // rgb(255,69,0)
+ orchid = 0xDA70D6, // rgb(218,112,214)
+ pale_golden_rod = 0xEEE8AA, // rgb(238,232,170)
+ pale_green = 0x98FB98, // rgb(152,251,152)
+ pale_turquoise = 0xAFEEEE, // rgb(175,238,238)
+ pale_violet_red = 0xDB7093, // rgb(219,112,147)
+ papaya_whip = 0xFFEFD5, // rgb(255,239,213)
+ peach_puff = 0xFFDAB9, // rgb(255,218,185)
+ peru = 0xCD853F, // rgb(205,133,63)
+ pink = 0xFFC0CB, // rgb(255,192,203)
+ plum = 0xDDA0DD, // rgb(221,160,221)
+ powder_blue = 0xB0E0E6, // rgb(176,224,230)
+ purple = 0x800080, // rgb(128,0,128)
+ rebecca_purple = 0x663399, // rgb(102,51,153)
+ red = 0xFF0000, // rgb(255,0,0)
+ rosy_brown = 0xBC8F8F, // rgb(188,143,143)
+ royal_blue = 0x4169E1, // rgb(65,105,225)
+ saddle_brown = 0x8B4513, // rgb(139,69,19)
+ salmon = 0xFA8072, // rgb(250,128,114)
+ sandy_brown = 0xF4A460, // rgb(244,164,96)
+ sea_green = 0x2E8B57, // rgb(46,139,87)
+ sea_shell = 0xFFF5EE, // rgb(255,245,238)
+ sienna = 0xA0522D, // rgb(160,82,45)
+ silver = 0xC0C0C0, // rgb(192,192,192)
+ sky_blue = 0x87CEEB, // rgb(135,206,235)
+ slate_blue = 0x6A5ACD, // rgb(106,90,205)
+ slate_gray = 0x708090, // rgb(112,128,144)
+ snow = 0xFFFAFA, // rgb(255,250,250)
+ spring_green = 0x00FF7F, // rgb(0,255,127)
+ steel_blue = 0x4682B4, // rgb(70,130,180)
+ tan = 0xD2B48C, // rgb(210,180,140)
+ teal = 0x008080, // rgb(0,128,128)
+ thistle = 0xD8BFD8, // rgb(216,191,216)
+ tomato = 0xFF6347, // rgb(255,99,71)
+ turquoise = 0x40E0D0, // rgb(64,224,208)
+ violet = 0xEE82EE, // rgb(238,130,238)
+ wheat = 0xF5DEB3, // rgb(245,222,179)
+ white = 0xFFFFFF, // rgb(255,255,255)
+ white_smoke = 0xF5F5F5, // rgb(245,245,245)
+ yellow = 0xFFFF00, // rgb(255,255,0)
+ yellow_green = 0x9ACD32 // rgb(154,205,50)
+}; // enum class color
+
+enum class terminal_color : uint8_t {
+ black = 30,
+ red,
+ green,
+ yellow,
+ blue,
+ magenta,
+ cyan,
+ white,
+ bright_black = 90,
+ bright_red,
+ bright_green,
+ bright_yellow,
+ bright_blue,
+ bright_magenta,
+ bright_cyan,
+ bright_white
+};
+
+enum class emphasis : uint8_t {
+ bold = 1,
+ faint = 1 << 1,
+ italic = 1 << 2,
+ underline = 1 << 3,
+ blink = 1 << 4,
+ reverse = 1 << 5,
+ conceal = 1 << 6,
+ strikethrough = 1 << 7,
+};
+
+// rgb is a struct for red, green and blue colors.
+// Using the name "rgb" makes some editors show the color in a tooltip.
+struct rgb {
+ FMT_CONSTEXPR rgb() : r(0), g(0), b(0) {}
+ FMT_CONSTEXPR rgb(uint8_t r_, uint8_t g_, uint8_t b_) : r(r_), g(g_), b(b_) {}
+ FMT_CONSTEXPR rgb(uint32_t hex)
+ : r((hex >> 16) & 0xFF), g((hex >> 8) & 0xFF), b(hex & 0xFF) {}
+ FMT_CONSTEXPR rgb(color hex)
+ : r((uint32_t(hex) >> 16) & 0xFF),
+ g((uint32_t(hex) >> 8) & 0xFF),
+ b(uint32_t(hex) & 0xFF) {}
+ uint8_t r;
+ uint8_t g;
+ uint8_t b;
+};
+
+FMT_BEGIN_DETAIL_NAMESPACE
+
+// color is a struct of either a rgb color or a terminal color.
+struct color_type {
+ FMT_CONSTEXPR color_type() noexcept : is_rgb(), value{} {}
+ FMT_CONSTEXPR color_type(color rgb_color) noexcept : is_rgb(true), value{} {
+ value.rgb_color = static_cast<uint32_t>(rgb_color);
+ }
+ FMT_CONSTEXPR color_type(rgb rgb_color) noexcept : is_rgb(true), value{} {
+ value.rgb_color = (static_cast<uint32_t>(rgb_color.r) << 16) |
+ (static_cast<uint32_t>(rgb_color.g) << 8) | rgb_color.b;
+ }
+ FMT_CONSTEXPR color_type(terminal_color term_color) noexcept
+ : is_rgb(), value{} {
+ value.term_color = static_cast<uint8_t>(term_color);
+ }
+ bool is_rgb;
+ union color_union {
+ uint8_t term_color;
+ uint32_t rgb_color;
+ } value;
+};
+
+FMT_END_DETAIL_NAMESPACE
+
+/** A text style consisting of foreground and background colors and emphasis. */
+class text_style {
+ public:
+ FMT_CONSTEXPR text_style(emphasis em = emphasis()) noexcept
+ : set_foreground_color(), set_background_color(), ems(em) {}
+
+ FMT_CONSTEXPR text_style& operator|=(const text_style& rhs) {
+ if (!set_foreground_color) {
+ set_foreground_color = rhs.set_foreground_color;
+ foreground_color = rhs.foreground_color;
+ } else if (rhs.set_foreground_color) {
+ if (!foreground_color.is_rgb || !rhs.foreground_color.is_rgb)
+ FMT_THROW(format_error("can't OR a terminal color"));
+ foreground_color.value.rgb_color |= rhs.foreground_color.value.rgb_color;
+ }
+
+ if (!set_background_color) {
+ set_background_color = rhs.set_background_color;
+ background_color = rhs.background_color;
+ } else if (rhs.set_background_color) {
+ if (!background_color.is_rgb || !rhs.background_color.is_rgb)
+ FMT_THROW(format_error("can't OR a terminal color"));
+ background_color.value.rgb_color |= rhs.background_color.value.rgb_color;
+ }
+
+ ems = static_cast<emphasis>(static_cast<uint8_t>(ems) |
+ static_cast<uint8_t>(rhs.ems));
+ return *this;
+ }
+
+ friend FMT_CONSTEXPR text_style operator|(text_style lhs,
+ const text_style& rhs) {
+ return lhs |= rhs;
+ }
+
+ FMT_CONSTEXPR bool has_foreground() const noexcept {
+ return set_foreground_color;
+ }
+ FMT_CONSTEXPR bool has_background() const noexcept {
+ return set_background_color;
+ }
+ FMT_CONSTEXPR bool has_emphasis() const noexcept {
+ return static_cast<uint8_t>(ems) != 0;
+ }
+ FMT_CONSTEXPR detail::color_type get_foreground() const noexcept {
+ FMT_ASSERT(has_foreground(), "no foreground specified for this style");
+ return foreground_color;
+ }
+ FMT_CONSTEXPR detail::color_type get_background() const noexcept {
+ FMT_ASSERT(has_background(), "no background specified for this style");
+ return background_color;
+ }
+ FMT_CONSTEXPR emphasis get_emphasis() const noexcept {
+ FMT_ASSERT(has_emphasis(), "no emphasis specified for this style");
+ return ems;
+ }
+
+ private:
+ FMT_CONSTEXPR text_style(bool is_foreground,
+ detail::color_type text_color) noexcept
+ : set_foreground_color(), set_background_color(), ems() {
+ if (is_foreground) {
+ foreground_color = text_color;
+ set_foreground_color = true;
+ } else {
+ background_color = text_color;
+ set_background_color = true;
+ }
+ }
+
+ friend FMT_CONSTEXPR text_style fg(detail::color_type foreground) noexcept;
+
+ friend FMT_CONSTEXPR text_style bg(detail::color_type background) noexcept;
+
+ detail::color_type foreground_color;
+ detail::color_type background_color;
+ bool set_foreground_color;
+ bool set_background_color;
+ emphasis ems;
+};
+
+/** Creates a text style from the foreground (text) color. */
+FMT_CONSTEXPR inline text_style fg(detail::color_type foreground) noexcept {
+ return text_style(true, foreground);
+}
+
+/** Creates a text style from the background color. */
+FMT_CONSTEXPR inline text_style bg(detail::color_type background) noexcept {
+ return text_style(false, background);
+}
+
+FMT_CONSTEXPR inline text_style operator|(emphasis lhs, emphasis rhs) noexcept {
+ return text_style(lhs) | rhs;
+}
+
+FMT_BEGIN_DETAIL_NAMESPACE
+
+template <typename Char> struct ansi_color_escape {
+ FMT_CONSTEXPR ansi_color_escape(detail::color_type text_color,
+ const char* esc) noexcept {
+ // If we have a terminal color, we need to output another escape code
+ // sequence.
+ if (!text_color.is_rgb) {
+ bool is_background = esc == string_view("\x1b[48;2;");
+ uint32_t value = text_color.value.term_color;
+ // Background ASCII codes are the same as the foreground ones but with
+ // 10 more.
+ if (is_background) value += 10u;
+
+ size_t index = 0;
+ buffer[index++] = static_cast<Char>('\x1b');
+ buffer[index++] = static_cast<Char>('[');
+
+ if (value >= 100u) {
+ buffer[index++] = static_cast<Char>('1');
+ value %= 100u;
+ }
+ buffer[index++] = static_cast<Char>('0' + value / 10u);
+ buffer[index++] = static_cast<Char>('0' + value % 10u);
+
+ buffer[index++] = static_cast<Char>('m');
+ buffer[index++] = static_cast<Char>('\0');
+ return;
+ }
+
+ for (int i = 0; i < 7; i++) {
+ buffer[i] = static_cast<Char>(esc[i]);
+ }
+ rgb color(text_color.value.rgb_color);
+ to_esc(color.r, buffer + 7, ';');
+ to_esc(color.g, buffer + 11, ';');
+ to_esc(color.b, buffer + 15, 'm');
+ buffer[19] = static_cast<Char>(0);
+ }
+ FMT_CONSTEXPR ansi_color_escape(emphasis em) noexcept {
+ uint8_t em_codes[num_emphases] = {};
+ if (has_emphasis(em, emphasis::bold)) em_codes[0] = 1;
+ if (has_emphasis(em, emphasis::faint)) em_codes[1] = 2;
+ if (has_emphasis(em, emphasis::italic)) em_codes[2] = 3;
+ if (has_emphasis(em, emphasis::underline)) em_codes[3] = 4;
+ if (has_emphasis(em, emphasis::blink)) em_codes[4] = 5;
+ if (has_emphasis(em, emphasis::reverse)) em_codes[5] = 7;
+ if (has_emphasis(em, emphasis::conceal)) em_codes[6] = 8;
+ if (has_emphasis(em, emphasis::strikethrough)) em_codes[7] = 9;
+
+ size_t index = 0;
+ for (size_t i = 0; i < num_emphases; ++i) {
+ if (!em_codes[i]) continue;
+ buffer[index++] = static_cast<Char>('\x1b');
+ buffer[index++] = static_cast<Char>('[');
+ buffer[index++] = static_cast<Char>('0' + em_codes[i]);
+ buffer[index++] = static_cast<Char>('m');
+ }
+ buffer[index++] = static_cast<Char>(0);
+ }
+ FMT_CONSTEXPR operator const Char*() const noexcept { return buffer; }
+
+ FMT_CONSTEXPR const Char* begin() const noexcept { return buffer; }
+ FMT_CONSTEXPR_CHAR_TRAITS const Char* end() const noexcept {
+ return buffer + std::char_traits<Char>::length(buffer);
+ }
+
+ private:
+ static constexpr size_t num_emphases = 8;
+ Char buffer[7u + 3u * num_emphases + 1u];
+
+ static FMT_CONSTEXPR void to_esc(uint8_t c, Char* out,
+ char delimiter) noexcept {
+ out[0] = static_cast<Char>('0' + c / 100);
+ out[1] = static_cast<Char>('0' + c / 10 % 10);
+ out[2] = static_cast<Char>('0' + c % 10);
+ out[3] = static_cast<Char>(delimiter);
+ }
+ static FMT_CONSTEXPR bool has_emphasis(emphasis em, emphasis mask) noexcept {
+ return static_cast<uint8_t>(em) & static_cast<uint8_t>(mask);
+ }
+};
+
+template <typename Char>
+FMT_CONSTEXPR ansi_color_escape<Char> make_foreground_color(
+ detail::color_type foreground) noexcept {
+ return ansi_color_escape<Char>(foreground, "\x1b[38;2;");
+}
+
+template <typename Char>
+FMT_CONSTEXPR ansi_color_escape<Char> make_background_color(
+ detail::color_type background) noexcept {
+ return ansi_color_escape<Char>(background, "\x1b[48;2;");
+}
+
+template <typename Char>
+FMT_CONSTEXPR ansi_color_escape<Char> make_emphasis(emphasis em) noexcept {
+ return ansi_color_escape<Char>(em);
+}
+
+template <typename Char> inline void reset_color(buffer<Char>& buffer) {
+ auto reset_color = string_view("\x1b[0m");
+ buffer.append(reset_color.begin(), reset_color.end());
+}
+
+template <typename T> struct styled_arg {
+ const T& value;
+ text_style style;
+};
+
+template <typename Char>
+void vformat_to(buffer<Char>& buf, const text_style& ts,
+ basic_string_view<Char> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args) {
+ bool has_style = false;
+ if (ts.has_emphasis()) {
+ has_style = true;
+ auto emphasis = detail::make_emphasis<Char>(ts.get_emphasis());
+ buf.append(emphasis.begin(), emphasis.end());
+ }
+ if (ts.has_foreground()) {
+ has_style = true;
+ auto foreground = detail::make_foreground_color<Char>(ts.get_foreground());
+ buf.append(foreground.begin(), foreground.end());
+ }
+ if (ts.has_background()) {
+ has_style = true;
+ auto background = detail::make_background_color<Char>(ts.get_background());
+ buf.append(background.begin(), background.end());
+ }
+ detail::vformat_to(buf, format_str, args, {});
+ if (has_style) detail::reset_color<Char>(buf);
+}
+
+FMT_END_DETAIL_NAMESPACE
+
+inline void vprint(std::FILE* f, const text_style& ts, string_view fmt,
+ format_args args) {
+ // Legacy wide streams are not supported.
+ auto buf = memory_buffer();
+ detail::vformat_to(buf, ts, fmt, args);
+ if (detail::is_utf8()) {
+ detail::print(f, string_view(buf.begin(), buf.size()));
+ return;
+ }
+ buf.push_back('\0');
+ int result = std::fputs(buf.data(), f);
+ if (result < 0)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot write to file")));
+}
+
+/**
+ \rst
+ Formats a string and prints it to the specified file stream using ANSI
+ escape sequences to specify text formatting.
+
+ **Example**::
+
+ fmt::print(fmt::emphasis::bold | fg(fmt::color::red),
+ "Elapsed time: {0:.2f} seconds", 1.23);
+ \endrst
+ */
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_string<S>::value)>
+void print(std::FILE* f, const text_style& ts, const S& format_str,
+ const Args&... args) {
+ vprint(f, ts, format_str,
+ fmt::make_format_args<buffer_context<char_t<S>>>(args...));
+}
+
+/**
+ \rst
+ Formats a string and prints it to stdout using ANSI escape sequences to
+ specify text formatting.
+
+ **Example**::
+
+ fmt::print(fmt::emphasis::bold | fg(fmt::color::red),
+ "Elapsed time: {0:.2f} seconds", 1.23);
+ \endrst
+ */
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_string<S>::value)>
+void print(const text_style& ts, const S& format_str, const Args&... args) {
+ return print(stdout, ts, format_str, args...);
+}
+
+template <typename S, typename Char = char_t<S>>
+inline std::basic_string<Char> vformat(
+ const text_style& ts, const S& format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args) {
+ basic_memory_buffer<Char> buf;
+ detail::vformat_to(buf, ts, detail::to_string_view(format_str), args);
+ return fmt::to_string(buf);
+}
+
+/**
+ \rst
+ Formats arguments and returns the result as a string using ANSI
+ escape sequences to specify text formatting.
+
+ **Example**::
+
+ #include <fmt/color.h>
+ std::string message = fmt::format(fmt::emphasis::bold | fg(fmt::color::red),
+ "The answer is {}", 42);
+ \endrst
+*/
+template <typename S, typename... Args, typename Char = char_t<S>>
+inline std::basic_string<Char> format(const text_style& ts, const S& format_str,
+ const Args&... args) {
+ return fmt::vformat(ts, detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+/**
+ Formats a string with the given text_style and writes the output to ``out``.
+ */
+template <typename OutputIt, typename Char,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value)>
+OutputIt vformat_to(
+ OutputIt out, const text_style& ts, basic_string_view<Char> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args) {
+ auto&& buf = detail::get_buffer<Char>(out);
+ detail::vformat_to(buf, ts, format_str, args);
+ return detail::get_iterator(buf, out);
+}
+
+/**
+ \rst
+ Formats arguments with the given text_style, writes the result to the output
+ iterator ``out`` and returns the iterator past the end of the output range.
+
+ **Example**::
+
+ std::vector<char> out;
+ fmt::format_to(std::back_inserter(out),
+ fmt::emphasis::bold | fg(fmt::color::red), "{}", 42);
+ \endrst
+*/
+template <typename OutputIt, typename S, typename... Args,
+ bool enable = detail::is_output_iterator<OutputIt, char_t<S>>::value&&
+ detail::is_string<S>::value>
+inline auto format_to(OutputIt out, const text_style& ts, const S& format_str,
+ Args&&... args) ->
+ typename std::enable_if<enable, OutputIt>::type {
+ return vformat_to(out, ts, detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<char_t<S>>>(args...));
+}
+
+template <typename T, typename Char>
+struct formatter<detail::styled_arg<T>, Char> : formatter<T, Char> {
+ template <typename FormatContext>
+ auto format(const detail::styled_arg<T>& arg, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ const auto& ts = arg.style;
+ const auto& value = arg.value;
+ auto out = ctx.out();
+
+ bool has_style = false;
+ if (ts.has_emphasis()) {
+ has_style = true;
+ auto emphasis = detail::make_emphasis<Char>(ts.get_emphasis());
+ out = std::copy(emphasis.begin(), emphasis.end(), out);
+ }
+ if (ts.has_foreground()) {
+ has_style = true;
+ auto foreground =
+ detail::make_foreground_color<Char>(ts.get_foreground());
+ out = std::copy(foreground.begin(), foreground.end(), out);
+ }
+ if (ts.has_background()) {
+ has_style = true;
+ auto background =
+ detail::make_background_color<Char>(ts.get_background());
+ out = std::copy(background.begin(), background.end(), out);
+ }
+ out = formatter<T, Char>::format(value, ctx);
+ if (has_style) {
+ auto reset_color = string_view("\x1b[0m");
+ out = std::copy(reset_color.begin(), reset_color.end(), out);
+ }
+ return out;
+ }
+};
+
+/**
+ \rst
+ Returns an argument that will be formatted using ANSI escape sequences,
+ to be used in a formatting function.
+
+ **Example**::
+
+ fmt::print("Elapsed time: {0:.2f} seconds",
+ fmt::styled(1.23, fmt::fg(fmt::color::green) |
+ fmt::bg(fmt::color::blue)));
+ \endrst
+ */
+template <typename T>
+FMT_CONSTEXPR auto styled(const T& value, text_style ts)
+ -> detail::styled_arg<remove_cvref_t<T>> {
+ return detail::styled_arg<remove_cvref_t<T>>{value, ts};
+}
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_COLOR_H_
diff --git a/src/fmtlib/fmt/compile.h b/src/fmtlib/fmt/compile.h
new file mode 100644
index 0000000..94e13c0
--- /dev/null
+++ b/src/fmtlib/fmt/compile.h
@@ -0,0 +1,607 @@
+// Formatting library for C++ - experimental format string compilation
+//
+// Copyright (c) 2012 - present, Victor Zverovich and fmt contributors
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_COMPILE_H_
+#define FMT_COMPILE_H_
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+template <typename Char, typename InputIt>
+FMT_CONSTEXPR inline counting_iterator copy_str(InputIt begin, InputIt end,
+ counting_iterator it) {
+ return it + (end - begin);
+}
+
+template <typename OutputIt> class truncating_iterator_base {
+ protected:
+ OutputIt out_;
+ size_t limit_;
+ size_t count_ = 0;
+
+ truncating_iterator_base() : out_(), limit_(0) {}
+
+ truncating_iterator_base(OutputIt out, size_t limit)
+ : out_(out), limit_(limit) {}
+
+ public:
+ using iterator_category = std::output_iterator_tag;
+ using value_type = typename std::iterator_traits<OutputIt>::value_type;
+ using difference_type = std::ptrdiff_t;
+ using pointer = void;
+ using reference = void;
+ FMT_UNCHECKED_ITERATOR(truncating_iterator_base);
+
+ OutputIt base() const { return out_; }
+ size_t count() const { return count_; }
+};
+
+// An output iterator that truncates the output and counts the number of objects
+// written to it.
+template <typename OutputIt,
+ typename Enable = typename std::is_void<
+ typename std::iterator_traits<OutputIt>::value_type>::type>
+class truncating_iterator;
+
+template <typename OutputIt>
+class truncating_iterator<OutputIt, std::false_type>
+ : public truncating_iterator_base<OutputIt> {
+ mutable typename truncating_iterator_base<OutputIt>::value_type blackhole_;
+
+ public:
+ using value_type = typename truncating_iterator_base<OutputIt>::value_type;
+
+ truncating_iterator() = default;
+
+ truncating_iterator(OutputIt out, size_t limit)
+ : truncating_iterator_base<OutputIt>(out, limit) {}
+
+ truncating_iterator& operator++() {
+ if (this->count_++ < this->limit_) ++this->out_;
+ return *this;
+ }
+
+ truncating_iterator operator++(int) {
+ auto it = *this;
+ ++*this;
+ return it;
+ }
+
+ value_type& operator*() const {
+ return this->count_ < this->limit_ ? *this->out_ : blackhole_;
+ }
+};
+
+template <typename OutputIt>
+class truncating_iterator<OutputIt, std::true_type>
+ : public truncating_iterator_base<OutputIt> {
+ public:
+ truncating_iterator() = default;
+
+ truncating_iterator(OutputIt out, size_t limit)
+ : truncating_iterator_base<OutputIt>(out, limit) {}
+
+ template <typename T> truncating_iterator& operator=(T val) {
+ if (this->count_++ < this->limit_) *this->out_++ = val;
+ return *this;
+ }
+
+ truncating_iterator& operator++() { return *this; }
+ truncating_iterator& operator++(int) { return *this; }
+ truncating_iterator& operator*() { return *this; }
+};
+
+// A compile-time string which is compiled into fast formatting code.
+class compiled_string {};
+
+template <typename S>
+struct is_compiled_string : std::is_base_of<compiled_string, S> {};
+
+/**
+ \rst
+ Converts a string literal *s* into a format string that will be parsed at
+ compile time and converted into efficient formatting code. Requires C++17
+ ``constexpr if`` compiler support.
+
+ **Example**::
+
+ // Converts 42 into std::string using the most efficient method and no
+ // runtime format string processing.
+ std::string s = fmt::format(FMT_COMPILE("{}"), 42);
+ \endrst
+ */
+#if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
+# define FMT_COMPILE(s) \
+ FMT_STRING_IMPL(s, fmt::detail::compiled_string, explicit)
+#else
+# define FMT_COMPILE(s) FMT_STRING(s)
+#endif
+
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct udl_compiled_string : compiled_string {
+ using char_type = Char;
+ explicit constexpr operator basic_string_view<char_type>() const {
+ return {Str.data, N - 1};
+ }
+};
+#endif
+
+template <typename T, typename... Tail>
+const T& first(const T& value, const Tail&...) {
+ return value;
+}
+
+#if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
+template <typename... Args> struct type_list {};
+
+// Returns a reference to the argument at index N from [first, rest...].
+template <int N, typename T, typename... Args>
+constexpr const auto& get([[maybe_unused]] const T& first,
+ [[maybe_unused]] const Args&... rest) {
+ static_assert(N < 1 + sizeof...(Args), "index is out of bounds");
+ if constexpr (N == 0)
+ return first;
+ else
+ return detail::get<N - 1>(rest...);
+}
+
+template <typename Char, typename... Args>
+constexpr int get_arg_index_by_name(basic_string_view<Char> name,
+ type_list<Args...>) {
+ return get_arg_index_by_name<Args...>(name);
+}
+
+template <int N, typename> struct get_type_impl;
+
+template <int N, typename... Args> struct get_type_impl<N, type_list<Args...>> {
+ using type =
+ remove_cvref_t<decltype(detail::get<N>(std::declval<Args>()...))>;
+};
+
+template <int N, typename T>
+using get_type = typename get_type_impl<N, T>::type;
+
+template <typename T> struct is_compiled_format : std::false_type {};
+
+template <typename Char> struct text {
+ basic_string_view<Char> data;
+ using char_type = Char;
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&...) const {
+ return write<Char>(out, data);
+ }
+};
+
+template <typename Char>
+struct is_compiled_format<text<Char>> : std::true_type {};
+
+template <typename Char>
+constexpr text<Char> make_text(basic_string_view<Char> s, size_t pos,
+ size_t size) {
+ return {{&s[pos], size}};
+}
+
+template <typename Char> struct code_unit {
+ Char value;
+ using char_type = Char;
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&...) const {
+ return write<Char>(out, value);
+ }
+};
+
+// This ensures that the argument type is convertible to `const T&`.
+template <typename T, int N, typename... Args>
+constexpr const T& get_arg_checked(const Args&... args) {
+ const auto& arg = detail::get<N>(args...);
+ if constexpr (detail::is_named_arg<remove_cvref_t<decltype(arg)>>()) {
+ return arg.value;
+ } else {
+ return arg;
+ }
+}
+
+template <typename Char>
+struct is_compiled_format<code_unit<Char>> : std::true_type {};
+
+// A replacement field that refers to argument N.
+template <typename Char, typename T, int N> struct field {
+ using char_type = Char;
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&... args) const {
+ return write<Char>(out, get_arg_checked<T, N>(args...));
+ }
+};
+
+template <typename Char, typename T, int N>
+struct is_compiled_format<field<Char, T, N>> : std::true_type {};
+
+// A replacement field that refers to argument with name.
+template <typename Char> struct runtime_named_field {
+ using char_type = Char;
+ basic_string_view<Char> name;
+
+ template <typename OutputIt, typename T>
+ constexpr static bool try_format_argument(
+ OutputIt& out,
+ // [[maybe_unused]] due to unused-but-set-parameter warning in GCC 7,8,9
+ [[maybe_unused]] basic_string_view<Char> arg_name, const T& arg) {
+ if constexpr (is_named_arg<typename std::remove_cv<T>::type>::value) {
+ if (arg_name == arg.name) {
+ out = write<Char>(out, arg.value);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&... args) const {
+ bool found = (try_format_argument(out, name, args) || ...);
+ if (!found) {
+ FMT_THROW(format_error("argument with specified name is not found"));
+ }
+ return out;
+ }
+};
+
+template <typename Char>
+struct is_compiled_format<runtime_named_field<Char>> : std::true_type {};
+
+// A replacement field that refers to argument N and has format specifiers.
+template <typename Char, typename T, int N> struct spec_field {
+ using char_type = Char;
+ formatter<T, Char> fmt;
+
+ template <typename OutputIt, typename... Args>
+ constexpr FMT_INLINE OutputIt format(OutputIt out,
+ const Args&... args) const {
+ const auto& vargs =
+ fmt::make_format_args<basic_format_context<OutputIt, Char>>(args...);
+ basic_format_context<OutputIt, Char> ctx(out, vargs);
+ return fmt.format(get_arg_checked<T, N>(args...), ctx);
+ }
+};
+
+template <typename Char, typename T, int N>
+struct is_compiled_format<spec_field<Char, T, N>> : std::true_type {};
+
+template <typename L, typename R> struct concat {
+ L lhs;
+ R rhs;
+ using char_type = typename L::char_type;
+
+ template <typename OutputIt, typename... Args>
+ constexpr OutputIt format(OutputIt out, const Args&... args) const {
+ out = lhs.format(out, args...);
+ return rhs.format(out, args...);
+ }
+};
+
+template <typename L, typename R>
+struct is_compiled_format<concat<L, R>> : std::true_type {};
+
+template <typename L, typename R>
+constexpr concat<L, R> make_concat(L lhs, R rhs) {
+ return {lhs, rhs};
+}
+
+struct unknown_format {};
+
+template <typename Char>
+constexpr size_t parse_text(basic_string_view<Char> str, size_t pos) {
+ for (size_t size = str.size(); pos != size; ++pos) {
+ if (str[pos] == '{' || str[pos] == '}') break;
+ }
+ return pos;
+}
+
+template <typename Args, size_t POS, int ID, typename S>
+constexpr auto compile_format_string(S format_str);
+
+template <typename Args, size_t POS, int ID, typename T, typename S>
+constexpr auto parse_tail(T head, S format_str) {
+ if constexpr (POS !=
+ basic_string_view<typename S::char_type>(format_str).size()) {
+ constexpr auto tail = compile_format_string<Args, POS, ID>(format_str);
+ if constexpr (std::is_same<remove_cvref_t<decltype(tail)>,
+ unknown_format>())
+ return tail;
+ else
+ return make_concat(head, tail);
+ } else {
+ return head;
+ }
+}
+
+template <typename T, typename Char> struct parse_specs_result {
+ formatter<T, Char> fmt;
+ size_t end;
+ int next_arg_id;
+};
+
+enum { manual_indexing_id = -1 };
+
+template <typename T, typename Char>
+constexpr parse_specs_result<T, Char> parse_specs(basic_string_view<Char> str,
+ size_t pos, int next_arg_id) {
+ str.remove_prefix(pos);
+ auto ctx =
+ compile_parse_context<Char>(str, max_value<int>(), nullptr, next_arg_id);
+ auto f = formatter<T, Char>();
+ auto end = f.parse(ctx);
+ return {f, pos + fmt::detail::to_unsigned(end - str.data()),
+ next_arg_id == 0 ? manual_indexing_id : ctx.next_arg_id()};
+}
+
+template <typename Char> struct arg_id_handler {
+ arg_ref<Char> arg_id;
+
+ constexpr int on_auto() {
+ FMT_ASSERT(false, "handler cannot be used with automatic indexing");
+ return 0;
+ }
+ constexpr int on_index(int id) {
+ arg_id = arg_ref<Char>(id);
+ return 0;
+ }
+ constexpr int on_name(basic_string_view<Char> id) {
+ arg_id = arg_ref<Char>(id);
+ return 0;
+ }
+};
+
+template <typename Char> struct parse_arg_id_result {
+ arg_ref<Char> arg_id;
+ const Char* arg_id_end;
+};
+
+template <int ID, typename Char>
+constexpr auto parse_arg_id(const Char* begin, const Char* end) {
+ auto handler = arg_id_handler<Char>{arg_ref<Char>{}};
+ auto arg_id_end = parse_arg_id(begin, end, handler);
+ return parse_arg_id_result<Char>{handler.arg_id, arg_id_end};
+}
+
+template <typename T, typename Enable = void> struct field_type {
+ using type = remove_cvref_t<T>;
+};
+
+template <typename T>
+struct field_type<T, enable_if_t<detail::is_named_arg<T>::value>> {
+ using type = remove_cvref_t<decltype(T::value)>;
+};
+
+template <typename T, typename Args, size_t END_POS, int ARG_INDEX, int NEXT_ID,
+ typename S>
+constexpr auto parse_replacement_field_then_tail(S format_str) {
+ using char_type = typename S::char_type;
+ constexpr auto str = basic_string_view<char_type>(format_str);
+ constexpr char_type c = END_POS != str.size() ? str[END_POS] : char_type();
+ if constexpr (c == '}') {
+ return parse_tail<Args, END_POS + 1, NEXT_ID>(
+ field<char_type, typename field_type<T>::type, ARG_INDEX>(),
+ format_str);
+ } else if constexpr (c != ':') {
+ FMT_THROW(format_error("expected ':'"));
+ } else {
+ constexpr auto result = parse_specs<typename field_type<T>::type>(
+ str, END_POS + 1, NEXT_ID == manual_indexing_id ? 0 : NEXT_ID);
+ if constexpr (result.end >= str.size() || str[result.end] != '}') {
+ FMT_THROW(format_error("expected '}'"));
+ return 0;
+ } else {
+ return parse_tail<Args, result.end + 1, result.next_arg_id>(
+ spec_field<char_type, typename field_type<T>::type, ARG_INDEX>{
+ result.fmt},
+ format_str);
+ }
+ }
+}
+
+// Compiles a non-empty format string and returns the compiled representation
+// or unknown_format() on unrecognized input.
+template <typename Args, size_t POS, int ID, typename S>
+constexpr auto compile_format_string(S format_str) {
+ using char_type = typename S::char_type;
+ constexpr auto str = basic_string_view<char_type>(format_str);
+ if constexpr (str[POS] == '{') {
+ if constexpr (POS + 1 == str.size())
+ FMT_THROW(format_error("unmatched '{' in format string"));
+ if constexpr (str[POS + 1] == '{') {
+ return parse_tail<Args, POS + 2, ID>(make_text(str, POS, 1), format_str);
+ } else if constexpr (str[POS + 1] == '}' || str[POS + 1] == ':') {
+ static_assert(ID != manual_indexing_id,
+ "cannot switch from manual to automatic argument indexing");
+ constexpr auto next_id =
+ ID != manual_indexing_id ? ID + 1 : manual_indexing_id;
+ return parse_replacement_field_then_tail<get_type<ID, Args>, Args,
+ POS + 1, ID, next_id>(
+ format_str);
+ } else {
+ constexpr auto arg_id_result =
+ parse_arg_id<ID>(str.data() + POS + 1, str.data() + str.size());
+ constexpr auto arg_id_end_pos = arg_id_result.arg_id_end - str.data();
+ constexpr char_type c =
+ arg_id_end_pos != str.size() ? str[arg_id_end_pos] : char_type();
+ static_assert(c == '}' || c == ':', "missing '}' in format string");
+ if constexpr (arg_id_result.arg_id.kind == arg_id_kind::index) {
+ static_assert(
+ ID == manual_indexing_id || ID == 0,
+ "cannot switch from automatic to manual argument indexing");
+ constexpr auto arg_index = arg_id_result.arg_id.val.index;
+ return parse_replacement_field_then_tail<get_type<arg_index, Args>,
+ Args, arg_id_end_pos,
+ arg_index, manual_indexing_id>(
+ format_str);
+ } else if constexpr (arg_id_result.arg_id.kind == arg_id_kind::name) {
+ constexpr auto arg_index =
+ get_arg_index_by_name(arg_id_result.arg_id.val.name, Args{});
+ if constexpr (arg_index != invalid_arg_index) {
+ constexpr auto next_id =
+ ID != manual_indexing_id ? ID + 1 : manual_indexing_id;
+ return parse_replacement_field_then_tail<
+ decltype(get_type<arg_index, Args>::value), Args, arg_id_end_pos,
+ arg_index, next_id>(format_str);
+ } else {
+ if constexpr (c == '}') {
+ return parse_tail<Args, arg_id_end_pos + 1, ID>(
+ runtime_named_field<char_type>{arg_id_result.arg_id.val.name},
+ format_str);
+ } else if constexpr (c == ':') {
+ return unknown_format(); // no type info for specs parsing
+ }
+ }
+ }
+ }
+ } else if constexpr (str[POS] == '}') {
+ if constexpr (POS + 1 == str.size())
+ FMT_THROW(format_error("unmatched '}' in format string"));
+ return parse_tail<Args, POS + 2, ID>(make_text(str, POS, 1), format_str);
+ } else {
+ constexpr auto end = parse_text(str, POS + 1);
+ if constexpr (end - POS > 1) {
+ return parse_tail<Args, end, ID>(make_text(str, POS, end - POS),
+ format_str);
+ } else {
+ return parse_tail<Args, end, ID>(code_unit<char_type>{str[POS]},
+ format_str);
+ }
+ }
+}
+
+template <typename... Args, typename S,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+constexpr auto compile(S format_str) {
+ constexpr auto str = basic_string_view<typename S::char_type>(format_str);
+ if constexpr (str.size() == 0) {
+ return detail::make_text(str, 0, 0);
+ } else {
+ constexpr auto result =
+ detail::compile_format_string<detail::type_list<Args...>, 0, 0>(
+ format_str);
+ return result;
+ }
+}
+#endif // defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+#if defined(__cpp_if_constexpr) && defined(__cpp_return_type_deduction)
+
+template <typename CompiledFormat, typename... Args,
+ typename Char = typename CompiledFormat::char_type,
+ FMT_ENABLE_IF(detail::is_compiled_format<CompiledFormat>::value)>
+FMT_INLINE std::basic_string<Char> format(const CompiledFormat& cf,
+ const Args&... args) {
+ auto s = std::basic_string<Char>();
+ cf.format(std::back_inserter(s), args...);
+ return s;
+}
+
+template <typename OutputIt, typename CompiledFormat, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_format<CompiledFormat>::value)>
+constexpr FMT_INLINE OutputIt format_to(OutputIt out, const CompiledFormat& cf,
+ const Args&... args) {
+ return cf.format(out, args...);
+}
+
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+FMT_INLINE std::basic_string<typename S::char_type> format(const S&,
+ Args&&... args) {
+ if constexpr (std::is_same<typename S::char_type, char>::value) {
+ constexpr auto str = basic_string_view<typename S::char_type>(S());
+ if constexpr (str.size() == 2 && str[0] == '{' && str[1] == '}') {
+ const auto& first = detail::first(args...);
+ if constexpr (detail::is_named_arg<
+ remove_cvref_t<decltype(first)>>::value) {
+ return fmt::to_string(first.value);
+ } else {
+ return fmt::to_string(first);
+ }
+ }
+ }
+ constexpr auto compiled = detail::compile<Args...>(S());
+ if constexpr (std::is_same<remove_cvref_t<decltype(compiled)>,
+ detail::unknown_format>()) {
+ return fmt::format(
+ static_cast<basic_string_view<typename S::char_type>>(S()),
+ std::forward<Args>(args)...);
+ } else {
+ return fmt::format(compiled, std::forward<Args>(args)...);
+ }
+}
+
+template <typename OutputIt, typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+FMT_CONSTEXPR OutputIt format_to(OutputIt out, const S&, Args&&... args) {
+ constexpr auto compiled = detail::compile<Args...>(S());
+ if constexpr (std::is_same<remove_cvref_t<decltype(compiled)>,
+ detail::unknown_format>()) {
+ return fmt::format_to(
+ out, static_cast<basic_string_view<typename S::char_type>>(S()),
+ std::forward<Args>(args)...);
+ } else {
+ return fmt::format_to(out, compiled, std::forward<Args>(args)...);
+ }
+}
+#endif
+
+template <typename OutputIt, typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+format_to_n_result<OutputIt> format_to_n(OutputIt out, size_t n,
+ const S& format_str, Args&&... args) {
+ auto it = fmt::format_to(detail::truncating_iterator<OutputIt>(out, n),
+ format_str, std::forward<Args>(args)...);
+ return {it.base(), it.count()};
+}
+
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+FMT_CONSTEXPR20 size_t formatted_size(const S& format_str,
+ const Args&... args) {
+ return fmt::format_to(detail::counting_iterator(), format_str, args...)
+ .count();
+}
+
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+void print(std::FILE* f, const S& format_str, const Args&... args) {
+ memory_buffer buffer;
+ fmt::format_to(std::back_inserter(buffer), format_str, args...);
+ detail::print(f, {buffer.data(), buffer.size()});
+}
+
+template <typename S, typename... Args,
+ FMT_ENABLE_IF(detail::is_compiled_string<S>::value)>
+void print(const S& format_str, const Args&... args) {
+ print(stdout, format_str, args...);
+}
+
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+inline namespace literals {
+template <detail_exported::fixed_string Str> constexpr auto operator""_cf() {
+ using char_t = remove_cvref_t<decltype(Str.data[0])>;
+ return detail::udl_compiled_string<char_t, sizeof(Str.data) / sizeof(char_t),
+ Str>();
+}
+} // namespace literals
+#endif
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_COMPILE_H_
diff --git a/src/fmtlib/fmt/core.h b/src/fmtlib/fmt/core.h
new file mode 100644
index 0000000..46723d5
--- /dev/null
+++ b/src/fmtlib/fmt/core.h
@@ -0,0 +1,2951 @@
+// Formatting library for C++ - the core API for char/UTF-8
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_CORE_H_
+#define FMT_CORE_H_
+
+#include <cstddef> // std::byte
+#include <cstdio> // std::FILE
+#include <cstring> // std::strlen
+#include <iterator>
+#include <limits>
+#include <string>
+#include <type_traits>
+
+// The fmt library version in the form major * 10000 + minor * 100 + patch.
+#define FMT_VERSION 100000
+
+#if defined(__clang__) && !defined(__ibmxl__)
+# define FMT_CLANG_VERSION (__clang_major__ * 100 + __clang_minor__)
+#else
+# define FMT_CLANG_VERSION 0
+#endif
+
+#if defined(__GNUC__) && !defined(__clang__) && !defined(__INTEL_COMPILER) && \
+ !defined(__NVCOMPILER)
+# define FMT_GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__)
+#else
+# define FMT_GCC_VERSION 0
+#endif
+
+#ifndef FMT_GCC_PRAGMA
+// Workaround _Pragma bug https://gcc.gnu.org/bugzilla/show_bug.cgi?id=59884.
+# if FMT_GCC_VERSION >= 504
+# define FMT_GCC_PRAGMA(arg) _Pragma(arg)
+# else
+# define FMT_GCC_PRAGMA(arg)
+# endif
+#endif
+
+#ifdef __ICL
+# define FMT_ICC_VERSION __ICL
+#elif defined(__INTEL_COMPILER)
+# define FMT_ICC_VERSION __INTEL_COMPILER
+#else
+# define FMT_ICC_VERSION 0
+#endif
+
+#ifdef _MSC_VER
+# define FMT_MSC_VERSION _MSC_VER
+# define FMT_MSC_WARNING(...) __pragma(warning(__VA_ARGS__))
+#else
+# define FMT_MSC_VERSION 0
+# define FMT_MSC_WARNING(...)
+#endif
+
+#ifdef _MSVC_LANG
+# define FMT_CPLUSPLUS _MSVC_LANG
+#else
+# define FMT_CPLUSPLUS __cplusplus
+#endif
+
+#ifdef __has_feature
+# define FMT_HAS_FEATURE(x) __has_feature(x)
+#else
+# define FMT_HAS_FEATURE(x) 0
+#endif
+
+#if defined(__has_include) || FMT_ICC_VERSION >= 1600 || FMT_MSC_VERSION > 1900
+# define FMT_HAS_INCLUDE(x) __has_include(x)
+#else
+# define FMT_HAS_INCLUDE(x) 0
+#endif
+
+#ifdef __has_cpp_attribute
+# define FMT_HAS_CPP_ATTRIBUTE(x) __has_cpp_attribute(x)
+#else
+# define FMT_HAS_CPP_ATTRIBUTE(x) 0
+#endif
+
+#define FMT_HAS_CPP14_ATTRIBUTE(attribute) \
+ (FMT_CPLUSPLUS >= 201402L && FMT_HAS_CPP_ATTRIBUTE(attribute))
+
+#define FMT_HAS_CPP17_ATTRIBUTE(attribute) \
+ (FMT_CPLUSPLUS >= 201703L && FMT_HAS_CPP_ATTRIBUTE(attribute))
+
+// Check if relaxed C++14 constexpr is supported.
+// GCC doesn't allow throw in constexpr until version 6 (bug 67371).
+#ifndef FMT_USE_CONSTEXPR
+# if (FMT_HAS_FEATURE(cxx_relaxed_constexpr) || FMT_MSC_VERSION >= 1912 || \
+ (FMT_GCC_VERSION >= 600 && FMT_CPLUSPLUS >= 201402L)) && \
+ !FMT_ICC_VERSION && !defined(__NVCC__)
+# define FMT_USE_CONSTEXPR 1
+# else
+# define FMT_USE_CONSTEXPR 0
+# endif
+#endif
+#if FMT_USE_CONSTEXPR
+# define FMT_CONSTEXPR constexpr
+#else
+# define FMT_CONSTEXPR
+#endif
+
+#if ((FMT_CPLUSPLUS >= 202002L) && \
+ (!defined(_GLIBCXX_RELEASE) || _GLIBCXX_RELEASE > 9)) || \
+ (FMT_CPLUSPLUS >= 201709L && FMT_GCC_VERSION >= 1002)
+# define FMT_CONSTEXPR20 constexpr
+#else
+# define FMT_CONSTEXPR20
+#endif
+
+// Check if constexpr std::char_traits<>::{compare,length} are supported.
+#if defined(__GLIBCXX__)
+# if FMT_CPLUSPLUS >= 201703L && defined(_GLIBCXX_RELEASE) && \
+ _GLIBCXX_RELEASE >= 7 // GCC 7+ libstdc++ has _GLIBCXX_RELEASE.
+# define FMT_CONSTEXPR_CHAR_TRAITS constexpr
+# endif
+#elif defined(_LIBCPP_VERSION) && FMT_CPLUSPLUS >= 201703L && \
+ _LIBCPP_VERSION >= 4000
+# define FMT_CONSTEXPR_CHAR_TRAITS constexpr
+#elif FMT_MSC_VERSION >= 1914 && FMT_CPLUSPLUS >= 201703L
+# define FMT_CONSTEXPR_CHAR_TRAITS constexpr
+#endif
+#ifndef FMT_CONSTEXPR_CHAR_TRAITS
+# define FMT_CONSTEXPR_CHAR_TRAITS
+#endif
+
+// Check if exceptions are disabled.
+#ifndef FMT_EXCEPTIONS
+# if (defined(__GNUC__) && !defined(__EXCEPTIONS)) || \
+ (FMT_MSC_VERSION && !_HAS_EXCEPTIONS)
+# define FMT_EXCEPTIONS 0
+# else
+# define FMT_EXCEPTIONS 1
+# endif
+#endif
+
+// Disable [[noreturn]] on MSVC/NVCC because of bogus unreachable code warnings.
+#if FMT_EXCEPTIONS && FMT_HAS_CPP_ATTRIBUTE(noreturn) && !FMT_MSC_VERSION && \
+ !defined(__NVCC__)
+# define FMT_NORETURN [[noreturn]]
+#else
+# define FMT_NORETURN
+#endif
+
+#ifndef FMT_NODISCARD
+# if FMT_HAS_CPP17_ATTRIBUTE(nodiscard)
+# define FMT_NODISCARD [[nodiscard]]
+# else
+# define FMT_NODISCARD
+# endif
+#endif
+
+#ifndef FMT_INLINE
+# if FMT_GCC_VERSION || FMT_CLANG_VERSION
+# define FMT_INLINE inline __attribute__((always_inline))
+# else
+# define FMT_INLINE inline
+# endif
+#endif
+
+// An inline std::forward replacement.
+#define FMT_FORWARD(...) static_cast<decltype(__VA_ARGS__)&&>(__VA_ARGS__)
+
+#ifdef _MSC_VER
+# define FMT_UNCHECKED_ITERATOR(It) \
+ using _Unchecked_type = It // Mark iterator as checked.
+#else
+# define FMT_UNCHECKED_ITERATOR(It) using unchecked_type = It
+#endif
+
+#ifndef FMT_BEGIN_NAMESPACE
+# define FMT_BEGIN_NAMESPACE \
+ namespace fmt { \
+ inline namespace v10 {
+# define FMT_END_NAMESPACE \
+ } \
+ }
+#endif
+
+#ifndef FMT_MODULE_EXPORT
+# define FMT_MODULE_EXPORT
+# define FMT_BEGIN_EXPORT
+# define FMT_END_EXPORT
+#endif
+
+#if !defined(FMT_HEADER_ONLY) && defined(_WIN32)
+# ifdef FMT_LIB_EXPORT
+# define FMT_API __declspec(dllexport)
+# elif defined(FMT_SHARED)
+# define FMT_API __declspec(dllimport)
+# endif
+#else
+# if defined(FMT_LIB_EXPORT) || defined(FMT_SHARED)
+# if defined(__GNUC__) || defined(__clang__)
+# define FMT_API __attribute__((visibility("default")))
+# endif
+# endif
+#endif
+#ifndef FMT_API
+# define FMT_API
+#endif
+
+// libc++ supports string_view in pre-c++17.
+#if FMT_HAS_INCLUDE(<string_view>) && \
+ (FMT_CPLUSPLUS >= 201703L || defined(_LIBCPP_VERSION))
+# include <string_view>
+# define FMT_USE_STRING_VIEW
+#elif FMT_HAS_INCLUDE("experimental/string_view") && FMT_CPLUSPLUS >= 201402L
+# include <experimental/string_view>
+# define FMT_USE_EXPERIMENTAL_STRING_VIEW
+#endif
+
+#ifndef FMT_UNICODE
+# define FMT_UNICODE !FMT_MSC_VERSION
+#endif
+
+#ifndef FMT_CONSTEVAL
+# if ((FMT_GCC_VERSION >= 1000 || FMT_CLANG_VERSION >= 1101) && \
+ (!defined(__apple_build_version__) || \
+ __apple_build_version__ >= 14000029L) && \
+ FMT_CPLUSPLUS >= 202002L) || \
+ (defined(__cpp_consteval) && \
+ (!FMT_MSC_VERSION || _MSC_FULL_VER >= 193030704))
+// consteval is broken in MSVC before VS2022 and Apple clang before 14.
+# define FMT_CONSTEVAL consteval
+# define FMT_HAS_CONSTEVAL
+# else
+# define FMT_CONSTEVAL
+# endif
+#endif
+
+#ifndef FMT_USE_NONTYPE_TEMPLATE_ARGS
+# if defined(__cpp_nontype_template_args) && \
+ ((FMT_GCC_VERSION >= 903 && FMT_CPLUSPLUS >= 201709L) || \
+ __cpp_nontype_template_args >= 201911L) && \
+ !defined(__NVCOMPILER) && !defined(__LCC__)
+# define FMT_USE_NONTYPE_TEMPLATE_ARGS 1
+# else
+# define FMT_USE_NONTYPE_TEMPLATE_ARGS 0
+# endif
+#endif
+
+#if defined __cpp_inline_variables && __cpp_inline_variables >= 201606L
+# define FMT_INLINE_VARIABLE inline
+#else
+# define FMT_INLINE_VARIABLE
+#endif
+
+// Enable minimal optimizations for more compact code in debug mode.
+FMT_GCC_PRAGMA("GCC push_options")
+#if !defined(__OPTIMIZE__) && !defined(__NVCOMPILER) && !defined(__LCC__) && \
+ !defined(__CUDACC__)
+FMT_GCC_PRAGMA("GCC optimize(\"Og\")")
+#endif
+
+FMT_BEGIN_NAMESPACE
+
+// Implementations of enable_if_t and other metafunctions for older systems.
+template <bool B, typename T = void>
+using enable_if_t = typename std::enable_if<B, T>::type;
+template <bool B, typename T, typename F>
+using conditional_t = typename std::conditional<B, T, F>::type;
+template <bool B> using bool_constant = std::integral_constant<bool, B>;
+template <typename T>
+using remove_reference_t = typename std::remove_reference<T>::type;
+template <typename T>
+using remove_const_t = typename std::remove_const<T>::type;
+template <typename T>
+using remove_cvref_t = typename std::remove_cv<remove_reference_t<T>>::type;
+template <typename T> struct type_identity { using type = T; };
+template <typename T> using type_identity_t = typename type_identity<T>::type;
+template <typename T>
+using underlying_t = typename std::underlying_type<T>::type;
+
+struct monostate {
+ constexpr monostate() {}
+};
+
+// An enable_if helper to be used in template parameters which results in much
+// shorter symbols: https://godbolt.org/z/sWw4vP. Extra parentheses are needed
+// to workaround a bug in MSVC 2019 (see #1140 and #1186).
+#ifdef FMT_DOC
+# define FMT_ENABLE_IF(...)
+#else
+# define FMT_ENABLE_IF(...) fmt::enable_if_t<(__VA_ARGS__), int> = 0
+#endif
+
+#ifdef __cpp_lib_byte
+inline auto format_as(std::byte b) -> unsigned char {
+ return static_cast<unsigned char>(b);
+}
+#endif
+
+namespace detail {
+// Suppresses "unused variable" warnings with the method described in
+// https://herbsutter.com/2009/10/18/mailbag-shutting-up-compiler-warnings/.
+// (void)var does not work on many Intel compilers.
+template <typename... T> FMT_CONSTEXPR void ignore_unused(const T&...) {}
+
+constexpr FMT_INLINE auto is_constant_evaluated(
+ bool default_value = false) noexcept -> bool {
+// Workaround for incompatibility between libstdc++ consteval-based
+// std::is_constant_evaluated() implementation and clang-14.
+// https://github.com/fmtlib/fmt/issues/3247
+#if FMT_CPLUSPLUS >= 202002L && defined(_GLIBCXX_RELEASE) && \
+ _GLIBCXX_RELEASE >= 12 && \
+ (FMT_CLANG_VERSION >= 1400 && FMT_CLANG_VERSION < 1500)
+ ignore_unused(default_value);
+ return __builtin_is_constant_evaluated();
+#elif defined(__cpp_lib_is_constant_evaluated)
+ ignore_unused(default_value);
+ return std::is_constant_evaluated();
+#else
+ return default_value;
+#endif
+}
+
+// Suppresses "conditional expression is constant" warnings.
+template <typename T> constexpr FMT_INLINE auto const_check(T value) -> T {
+ return value;
+}
+
+FMT_NORETURN FMT_API void assert_fail(const char* file, int line,
+ const char* message);
+
+#ifndef FMT_ASSERT
+# ifdef NDEBUG
+// FMT_ASSERT is not empty to avoid -Wempty-body.
+# define FMT_ASSERT(condition, message) \
+ fmt::detail::ignore_unused((condition), (message))
+# else
+# define FMT_ASSERT(condition, message) \
+ ((condition) /* void() fails with -Winvalid-constexpr on clang 4.0.1 */ \
+ ? (void)0 \
+ : fmt::detail::assert_fail(__FILE__, __LINE__, (message)))
+# endif
+#endif
+
+#if defined(FMT_USE_STRING_VIEW)
+template <typename Char> using std_string_view = std::basic_string_view<Char>;
+#elif defined(FMT_USE_EXPERIMENTAL_STRING_VIEW)
+template <typename Char>
+using std_string_view = std::experimental::basic_string_view<Char>;
+#else
+template <typename T> struct std_string_view {};
+#endif
+
+#ifdef FMT_USE_INT128
+// Do nothing.
+#elif defined(__SIZEOF_INT128__) && !defined(__NVCC__) && \
+ !(FMT_CLANG_VERSION && FMT_MSC_VERSION)
+# define FMT_USE_INT128 1
+using int128_opt = __int128_t; // An optional native 128-bit integer.
+using uint128_opt = __uint128_t;
+template <typename T> inline auto convert_for_visit(T value) -> T {
+ return value;
+}
+#else
+# define FMT_USE_INT128 0
+#endif
+#if !FMT_USE_INT128
+enum class int128_opt {};
+enum class uint128_opt {};
+// Reduce template instantiations.
+template <typename T> auto convert_for_visit(T) -> monostate { return {}; }
+#endif
+
+// Casts a nonnegative integer to unsigned.
+template <typename Int>
+FMT_CONSTEXPR auto to_unsigned(Int value) ->
+ typename std::make_unsigned<Int>::type {
+ FMT_ASSERT(std::is_unsigned<Int>::value || value >= 0, "negative value");
+ return static_cast<typename std::make_unsigned<Int>::type>(value);
+}
+
+FMT_CONSTEXPR inline auto is_utf8() -> bool {
+ FMT_MSC_WARNING(suppress : 4566) constexpr unsigned char section[] = "\u00A7";
+
+ // Avoid buggy sign extensions in MSVC's constant evaluation mode (#2297).
+ using uchar = unsigned char;
+ return FMT_UNICODE || (sizeof(section) == 3 && uchar(section[0]) == 0xC2 &&
+ uchar(section[1]) == 0xA7);
+}
+} // namespace detail
+
+/**
+ An implementation of ``std::basic_string_view`` for pre-C++17. It provides a
+ subset of the API. ``fmt::basic_string_view`` is used for format strings even
+ if ``std::string_view`` is available to prevent issues when a library is
+ compiled with a different ``-std`` option than the client code (which is not
+ recommended).
+ */
+FMT_MODULE_EXPORT
+template <typename Char> class basic_string_view {
+ private:
+ const Char* data_;
+ size_t size_;
+
+ public:
+ using value_type = Char;
+ using iterator = const Char*;
+
+ constexpr basic_string_view() noexcept : data_(nullptr), size_(0) {}
+
+ /** Constructs a string reference object from a C string and a size. */
+ constexpr basic_string_view(const Char* s, size_t count) noexcept
+ : data_(s), size_(count) {}
+
+ /**
+ \rst
+ Constructs a string reference object from a C string computing
+ the size with ``std::char_traits<Char>::length``.
+ \endrst
+ */
+ FMT_CONSTEXPR_CHAR_TRAITS
+ FMT_INLINE
+ basic_string_view(const Char* s)
+ : data_(s),
+ size_(detail::const_check(std::is_same<Char, char>::value &&
+ !detail::is_constant_evaluated(true))
+ ? std::strlen(reinterpret_cast<const char*>(s))
+ : std::char_traits<Char>::length(s)) {}
+
+ /** Constructs a string reference from a ``std::basic_string`` object. */
+ template <typename Traits, typename Alloc>
+ FMT_CONSTEXPR basic_string_view(
+ const std::basic_string<Char, Traits, Alloc>& s) noexcept
+ : data_(s.data()), size_(s.size()) {}
+
+ template <typename S, FMT_ENABLE_IF(std::is_same<
+ S, detail::std_string_view<Char>>::value)>
+ FMT_CONSTEXPR basic_string_view(S s) noexcept
+ : data_(s.data()), size_(s.size()) {}
+
+ /** Returns a pointer to the string data. */
+ constexpr auto data() const noexcept -> const Char* { return data_; }
+
+ /** Returns the string size. */
+ constexpr auto size() const noexcept -> size_t { return size_; }
+
+ constexpr auto begin() const noexcept -> iterator { return data_; }
+ constexpr auto end() const noexcept -> iterator { return data_ + size_; }
+
+ constexpr auto operator[](size_t pos) const noexcept -> const Char& {
+ return data_[pos];
+ }
+
+ FMT_CONSTEXPR void remove_prefix(size_t n) noexcept {
+ data_ += n;
+ size_ -= n;
+ }
+
+ FMT_CONSTEXPR_CHAR_TRAITS bool starts_with(
+ basic_string_view<Char> sv) const noexcept {
+ return size_ >= sv.size_ &&
+ std::char_traits<Char>::compare(data_, sv.data_, sv.size_) == 0;
+ }
+ FMT_CONSTEXPR_CHAR_TRAITS bool starts_with(Char c) const noexcept {
+ return size_ >= 1 && std::char_traits<Char>::eq(*data_, c);
+ }
+ FMT_CONSTEXPR_CHAR_TRAITS bool starts_with(const Char* s) const {
+ return starts_with(basic_string_view<Char>(s));
+ }
+
+ // Lexicographically compare this string reference to other.
+ FMT_CONSTEXPR_CHAR_TRAITS auto compare(basic_string_view other) const -> int {
+ size_t str_size = size_ < other.size_ ? size_ : other.size_;
+ int result = std::char_traits<Char>::compare(data_, other.data_, str_size);
+ if (result == 0)
+ result = size_ == other.size_ ? 0 : (size_ < other.size_ ? -1 : 1);
+ return result;
+ }
+
+ FMT_CONSTEXPR_CHAR_TRAITS friend auto operator==(basic_string_view lhs,
+ basic_string_view rhs)
+ -> bool {
+ return lhs.compare(rhs) == 0;
+ }
+ friend auto operator!=(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) != 0;
+ }
+ friend auto operator<(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) < 0;
+ }
+ friend auto operator<=(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) <= 0;
+ }
+ friend auto operator>(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) > 0;
+ }
+ friend auto operator>=(basic_string_view lhs, basic_string_view rhs) -> bool {
+ return lhs.compare(rhs) >= 0;
+ }
+};
+
+FMT_MODULE_EXPORT
+using string_view = basic_string_view<char>;
+
+/** Specifies if ``T`` is a character type. Can be specialized by users. */
+FMT_MODULE_EXPORT
+template <typename T> struct is_char : std::false_type {};
+template <> struct is_char<char> : std::true_type {};
+
+namespace detail {
+
+// A base class for compile-time strings.
+struct compile_string {};
+
+template <typename S>
+struct is_compile_string : std::is_base_of<compile_string, S> {};
+
+template <typename Char, FMT_ENABLE_IF(is_char<Char>::value)>
+FMT_INLINE auto to_string_view(const Char* s) -> basic_string_view<Char> {
+ return s;
+}
+template <typename Char, typename Traits, typename Alloc>
+inline auto to_string_view(const std::basic_string<Char, Traits, Alloc>& s)
+ -> basic_string_view<Char> {
+ return s;
+}
+template <typename Char>
+constexpr auto to_string_view(basic_string_view<Char> s)
+ -> basic_string_view<Char> {
+ return s;
+}
+template <typename Char,
+ FMT_ENABLE_IF(!std::is_empty<std_string_view<Char>>::value)>
+inline auto to_string_view(std_string_view<Char> s) -> basic_string_view<Char> {
+ return s;
+}
+template <typename S, FMT_ENABLE_IF(is_compile_string<S>::value)>
+constexpr auto to_string_view(const S& s)
+ -> basic_string_view<typename S::char_type> {
+ return basic_string_view<typename S::char_type>(s);
+}
+void to_string_view(...);
+
+// Specifies whether S is a string type convertible to fmt::basic_string_view.
+// It should be a constexpr function but MSVC 2017 fails to compile it in
+// enable_if and MSVC 2015 fails to compile it as an alias template.
+// ADL is intentionally disabled as to_string_view is not an extension point.
+template <typename S>
+struct is_string
+ : std::is_class<decltype(detail::to_string_view(std::declval<S>()))> {};
+
+template <typename S, typename = void> struct char_t_impl {};
+template <typename S> struct char_t_impl<S, enable_if_t<is_string<S>::value>> {
+ using result = decltype(to_string_view(std::declval<S>()));
+ using type = typename result::value_type;
+};
+
+enum class type {
+ none_type,
+ // Integer types should go first,
+ int_type,
+ uint_type,
+ long_long_type,
+ ulong_long_type,
+ int128_type,
+ uint128_type,
+ bool_type,
+ char_type,
+ last_integer_type = char_type,
+ // followed by floating-point types.
+ float_type,
+ double_type,
+ long_double_type,
+ last_numeric_type = long_double_type,
+ cstring_type,
+ string_type,
+ pointer_type,
+ custom_type
+};
+
+// Maps core type T to the corresponding type enum constant.
+template <typename T, typename Char>
+struct type_constant : std::integral_constant<type, type::custom_type> {};
+
+#define FMT_TYPE_CONSTANT(Type, constant) \
+ template <typename Char> \
+ struct type_constant<Type, Char> \
+ : std::integral_constant<type, type::constant> {}
+
+FMT_TYPE_CONSTANT(int, int_type);
+FMT_TYPE_CONSTANT(unsigned, uint_type);
+FMT_TYPE_CONSTANT(long long, long_long_type);
+FMT_TYPE_CONSTANT(unsigned long long, ulong_long_type);
+FMT_TYPE_CONSTANT(int128_opt, int128_type);
+FMT_TYPE_CONSTANT(uint128_opt, uint128_type);
+FMT_TYPE_CONSTANT(bool, bool_type);
+FMT_TYPE_CONSTANT(Char, char_type);
+FMT_TYPE_CONSTANT(float, float_type);
+FMT_TYPE_CONSTANT(double, double_type);
+FMT_TYPE_CONSTANT(long double, long_double_type);
+FMT_TYPE_CONSTANT(const Char*, cstring_type);
+FMT_TYPE_CONSTANT(basic_string_view<Char>, string_type);
+FMT_TYPE_CONSTANT(const void*, pointer_type);
+
+constexpr bool is_integral_type(type t) {
+ return t > type::none_type && t <= type::last_integer_type;
+}
+constexpr bool is_arithmetic_type(type t) {
+ return t > type::none_type && t <= type::last_numeric_type;
+}
+
+constexpr auto set(type rhs) -> int { return 1 << static_cast<int>(rhs); }
+constexpr auto in(type t, int set) -> bool {
+ return ((set >> static_cast<int>(t)) & 1) != 0;
+}
+
+// Bitsets of types.
+enum {
+ sint_set =
+ set(type::int_type) | set(type::long_long_type) | set(type::int128_type),
+ uint_set = set(type::uint_type) | set(type::ulong_long_type) |
+ set(type::uint128_type),
+ bool_set = set(type::bool_type),
+ char_set = set(type::char_type),
+ float_set = set(type::float_type) | set(type::double_type) |
+ set(type::long_double_type),
+ string_set = set(type::string_type),
+ cstring_set = set(type::cstring_type),
+ pointer_set = set(type::pointer_type)
+};
+
+FMT_NORETURN FMT_API void throw_format_error(const char* message);
+
+struct error_handler {
+ constexpr error_handler() = default;
+
+ // This function is intentionally not constexpr to give a compile-time error.
+ FMT_NORETURN void on_error(const char* message) {
+ throw_format_error(message);
+ }
+};
+} // namespace detail
+
+/** String's character type. */
+template <typename S> using char_t = typename detail::char_t_impl<S>::type;
+
+/**
+ \rst
+ Parsing context consisting of a format string range being parsed and an
+ argument counter for automatic indexing.
+ You can use the ``format_parse_context`` type alias for ``char`` instead.
+ \endrst
+ */
+FMT_MODULE_EXPORT
+template <typename Char> class basic_format_parse_context {
+ private:
+ basic_string_view<Char> format_str_;
+ int next_arg_id_;
+
+ FMT_CONSTEXPR void do_check_arg_id(int id);
+
+ public:
+ using char_type = Char;
+ using iterator = const Char*;
+
+ explicit constexpr basic_format_parse_context(
+ basic_string_view<Char> format_str, int next_arg_id = 0)
+ : format_str_(format_str), next_arg_id_(next_arg_id) {}
+
+ /**
+ Returns an iterator to the beginning of the format string range being
+ parsed.
+ */
+ constexpr auto begin() const noexcept -> iterator {
+ return format_str_.begin();
+ }
+
+ /**
+ Returns an iterator past the end of the format string range being parsed.
+ */
+ constexpr auto end() const noexcept -> iterator { return format_str_.end(); }
+
+ /** Advances the begin iterator to ``it``. */
+ FMT_CONSTEXPR void advance_to(iterator it) {
+ format_str_.remove_prefix(detail::to_unsigned(it - begin()));
+ }
+
+ /**
+ Reports an error if using the manual argument indexing; otherwise returns
+ the next argument index and switches to the automatic indexing.
+ */
+ FMT_CONSTEXPR auto next_arg_id() -> int {
+ if (next_arg_id_ < 0) {
+ detail::throw_format_error(
+ "cannot switch from manual to automatic argument indexing");
+ return 0;
+ }
+ int id = next_arg_id_++;
+ do_check_arg_id(id);
+ return id;
+ }
+
+ /**
+ Reports an error if using the automatic argument indexing; otherwise
+ switches to the manual indexing.
+ */
+ FMT_CONSTEXPR void check_arg_id(int id) {
+ if (next_arg_id_ > 0) {
+ detail::throw_format_error(
+ "cannot switch from automatic to manual argument indexing");
+ return;
+ }
+ next_arg_id_ = -1;
+ do_check_arg_id(id);
+ }
+ FMT_CONSTEXPR void check_arg_id(basic_string_view<Char>) {}
+ FMT_CONSTEXPR void check_dynamic_spec(int arg_id);
+};
+
+FMT_MODULE_EXPORT
+using format_parse_context = basic_format_parse_context<char>;
+
+namespace detail {
+// A parse context with extra data used only in compile-time checks.
+template <typename Char>
+class compile_parse_context : public basic_format_parse_context<Char> {
+ private:
+ int num_args_;
+ const type* types_;
+ using base = basic_format_parse_context<Char>;
+
+ public:
+ explicit FMT_CONSTEXPR compile_parse_context(
+ basic_string_view<Char> format_str, int num_args, const type* types,
+ int next_arg_id = 0)
+ : base(format_str, next_arg_id), num_args_(num_args), types_(types) {}
+
+ constexpr auto num_args() const -> int { return num_args_; }
+ constexpr auto arg_type(int id) const -> type { return types_[id]; }
+
+ FMT_CONSTEXPR auto next_arg_id() -> int {
+ int id = base::next_arg_id();
+ if (id >= num_args_) throw_format_error("argument not found");
+ return id;
+ }
+
+ FMT_CONSTEXPR void check_arg_id(int id) {
+ base::check_arg_id(id);
+ if (id >= num_args_) throw_format_error("argument not found");
+ }
+ using base::check_arg_id;
+
+ FMT_CONSTEXPR void check_dynamic_spec(int arg_id) {
+ detail::ignore_unused(arg_id);
+#if !defined(__LCC__)
+ if (arg_id < num_args_ && types_ && !is_integral_type(types_[arg_id]))
+ throw_format_error("width/precision is not integer");
+#endif
+ }
+};
+} // namespace detail
+
+template <typename Char>
+FMT_CONSTEXPR void basic_format_parse_context<Char>::do_check_arg_id(int id) {
+ // Argument id is only checked at compile-time during parsing because
+ // formatting has its own validation.
+ if (detail::is_constant_evaluated() &&
+ (!FMT_GCC_VERSION || FMT_GCC_VERSION >= 1200)) {
+ using context = detail::compile_parse_context<Char>;
+ if (id >= static_cast<context*>(this)->num_args())
+ detail::throw_format_error("argument not found");
+ }
+}
+
+template <typename Char>
+FMT_CONSTEXPR void basic_format_parse_context<Char>::check_dynamic_spec(
+ int arg_id) {
+ if (detail::is_constant_evaluated() &&
+ (!FMT_GCC_VERSION || FMT_GCC_VERSION >= 1200)) {
+ using context = detail::compile_parse_context<Char>;
+ static_cast<context*>(this)->check_dynamic_spec(arg_id);
+ }
+}
+
+FMT_MODULE_EXPORT template <typename Context> class basic_format_arg;
+FMT_MODULE_EXPORT template <typename Context> class basic_format_args;
+FMT_MODULE_EXPORT template <typename Context> class dynamic_format_arg_store;
+
+// A formatter for objects of type T.
+FMT_MODULE_EXPORT
+template <typename T, typename Char = char, typename Enable = void>
+struct formatter {
+ // A deleted default constructor indicates a disabled formatter.
+ formatter() = delete;
+};
+
+// Specifies if T has an enabled formatter specialization. A type can be
+// formattable even if it doesn't have a formatter e.g. via a conversion.
+template <typename T, typename Context>
+using has_formatter =
+ std::is_constructible<typename Context::template formatter_type<T>>;
+
+// Checks whether T is a container with contiguous storage.
+template <typename T> struct is_contiguous : std::false_type {};
+template <typename Char>
+struct is_contiguous<std::basic_string<Char>> : std::true_type {};
+
+class appender;
+
+namespace detail {
+
+template <typename Context, typename T>
+constexpr auto has_const_formatter_impl(T*)
+ -> decltype(typename Context::template formatter_type<T>().format(
+ std::declval<const T&>(), std::declval<Context&>()),
+ true) {
+ return true;
+}
+template <typename Context>
+constexpr auto has_const_formatter_impl(...) -> bool {
+ return false;
+}
+template <typename T, typename Context>
+constexpr auto has_const_formatter() -> bool {
+ return has_const_formatter_impl<Context>(static_cast<T*>(nullptr));
+}
+
+// Extracts a reference to the container from back_insert_iterator.
+template <typename Container>
+inline auto get_container(std::back_insert_iterator<Container> it)
+ -> Container& {
+ using base = std::back_insert_iterator<Container>;
+ struct accessor : base {
+ accessor(base b) : base(b) {}
+ using base::container;
+ };
+ return *accessor(it).container;
+}
+
+template <typename Char, typename InputIt, typename OutputIt>
+FMT_CONSTEXPR auto copy_str(InputIt begin, InputIt end, OutputIt out)
+ -> OutputIt {
+ while (begin != end) *out++ = static_cast<Char>(*begin++);
+ return out;
+}
+
+template <typename Char, typename T, typename U,
+ FMT_ENABLE_IF(
+ std::is_same<remove_const_t<T>, U>::value&& is_char<U>::value)>
+FMT_CONSTEXPR auto copy_str(T* begin, T* end, U* out) -> U* {
+ if (is_constant_evaluated()) return copy_str<Char, T*, U*>(begin, end, out);
+ auto size = to_unsigned(end - begin);
+ if (size > 0) memcpy(out, begin, size * sizeof(U));
+ return out + size;
+}
+
+/**
+ \rst
+ A contiguous memory buffer with an optional growing ability. It is an internal
+ class and shouldn't be used directly, only via `~fmt::basic_memory_buffer`.
+ \endrst
+ */
+template <typename T> class buffer {
+ private:
+ T* ptr_;
+ size_t size_;
+ size_t capacity_;
+
+ protected:
+ // Don't initialize ptr_ since it is not accessed to save a few cycles.
+ FMT_MSC_WARNING(suppress : 26495)
+ buffer(size_t sz) noexcept : size_(sz), capacity_(sz) {}
+
+ FMT_CONSTEXPR20 buffer(T* p = nullptr, size_t sz = 0, size_t cap = 0) noexcept
+ : ptr_(p), size_(sz), capacity_(cap) {}
+
+ FMT_CONSTEXPR20 ~buffer() = default;
+ buffer(buffer&&) = default;
+
+ /** Sets the buffer data and capacity. */
+ FMT_CONSTEXPR void set(T* buf_data, size_t buf_capacity) noexcept {
+ ptr_ = buf_data;
+ capacity_ = buf_capacity;
+ }
+
+ /** Increases the buffer capacity to hold at least *capacity* elements. */
+ virtual FMT_CONSTEXPR20 void grow(size_t capacity) = 0;
+
+ public:
+ using value_type = T;
+ using const_reference = const T&;
+
+ buffer(const buffer&) = delete;
+ void operator=(const buffer&) = delete;
+
+ FMT_INLINE auto begin() noexcept -> T* { return ptr_; }
+ FMT_INLINE auto end() noexcept -> T* { return ptr_ + size_; }
+
+ FMT_INLINE auto begin() const noexcept -> const T* { return ptr_; }
+ FMT_INLINE auto end() const noexcept -> const T* { return ptr_ + size_; }
+
+ /** Returns the size of this buffer. */
+ constexpr auto size() const noexcept -> size_t { return size_; }
+
+ /** Returns the capacity of this buffer. */
+ constexpr auto capacity() const noexcept -> size_t { return capacity_; }
+
+ /** Returns a pointer to the buffer data. */
+ FMT_CONSTEXPR auto data() noexcept -> T* { return ptr_; }
+
+ /** Returns a pointer to the buffer data. */
+ FMT_CONSTEXPR auto data() const noexcept -> const T* { return ptr_; }
+
+ /** Clears this buffer. */
+ void clear() { size_ = 0; }
+
+ // Tries resizing the buffer to contain *count* elements. If T is a POD type
+ // the new elements may not be initialized.
+ FMT_CONSTEXPR20 void try_resize(size_t count) {
+ try_reserve(count);
+ size_ = count <= capacity_ ? count : capacity_;
+ }
+
+ // Tries increasing the buffer capacity to *new_capacity*. It can increase the
+ // capacity by a smaller amount than requested but guarantees there is space
+ // for at least one additional element either by increasing the capacity or by
+ // flushing the buffer if it is full.
+ FMT_CONSTEXPR20 void try_reserve(size_t new_capacity) {
+ if (new_capacity > capacity_) grow(new_capacity);
+ }
+
+ FMT_CONSTEXPR20 void push_back(const T& value) {
+ try_reserve(size_ + 1);
+ ptr_[size_++] = value;
+ }
+
+ /** Appends data to the end of the buffer. */
+ template <typename U> void append(const U* begin, const U* end);
+
+ template <typename Idx> FMT_CONSTEXPR auto operator[](Idx index) -> T& {
+ return ptr_[index];
+ }
+ template <typename Idx>
+ FMT_CONSTEXPR auto operator[](Idx index) const -> const T& {
+ return ptr_[index];
+ }
+};
+
+struct buffer_traits {
+ explicit buffer_traits(size_t) {}
+ auto count() const -> size_t { return 0; }
+ auto limit(size_t size) -> size_t { return size; }
+};
+
+class fixed_buffer_traits {
+ private:
+ size_t count_ = 0;
+ size_t limit_;
+
+ public:
+ explicit fixed_buffer_traits(size_t limit) : limit_(limit) {}
+ auto count() const -> size_t { return count_; }
+ auto limit(size_t size) -> size_t {
+ size_t n = limit_ > count_ ? limit_ - count_ : 0;
+ count_ += size;
+ return size < n ? size : n;
+ }
+};
+
+// A buffer that writes to an output iterator when flushed.
+template <typename OutputIt, typename T, typename Traits = buffer_traits>
+class iterator_buffer final : public Traits, public buffer<T> {
+ private:
+ OutputIt out_;
+ enum { buffer_size = 256 };
+ T data_[buffer_size];
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t) override {
+ if (this->size() == buffer_size) flush();
+ }
+
+ void flush() {
+ auto size = this->size();
+ this->clear();
+ out_ = copy_str<T>(data_, data_ + this->limit(size), out_);
+ }
+
+ public:
+ explicit iterator_buffer(OutputIt out, size_t n = buffer_size)
+ : Traits(n), buffer<T>(data_, 0, buffer_size), out_(out) {}
+ iterator_buffer(iterator_buffer&& other)
+ : Traits(other), buffer<T>(data_, 0, buffer_size), out_(other.out_) {}
+ ~iterator_buffer() { flush(); }
+
+ auto out() -> OutputIt {
+ flush();
+ return out_;
+ }
+ auto count() const -> size_t { return Traits::count() + this->size(); }
+};
+
+template <typename T>
+class iterator_buffer<T*, T, fixed_buffer_traits> final
+ : public fixed_buffer_traits,
+ public buffer<T> {
+ private:
+ T* out_;
+ enum { buffer_size = 256 };
+ T data_[buffer_size];
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t) override {
+ if (this->size() == this->capacity()) flush();
+ }
+
+ void flush() {
+ size_t n = this->limit(this->size());
+ if (this->data() == out_) {
+ out_ += n;
+ this->set(data_, buffer_size);
+ }
+ this->clear();
+ }
+
+ public:
+ explicit iterator_buffer(T* out, size_t n = buffer_size)
+ : fixed_buffer_traits(n), buffer<T>(out, 0, n), out_(out) {}
+ iterator_buffer(iterator_buffer&& other)
+ : fixed_buffer_traits(other),
+ buffer<T>(std::move(other)),
+ out_(other.out_) {
+ if (this->data() != out_) {
+ this->set(data_, buffer_size);
+ this->clear();
+ }
+ }
+ ~iterator_buffer() { flush(); }
+
+ auto out() -> T* {
+ flush();
+ return out_;
+ }
+ auto count() const -> size_t {
+ return fixed_buffer_traits::count() + this->size();
+ }
+};
+
+template <typename T> class iterator_buffer<T*, T> final : public buffer<T> {
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t) override {}
+
+ public:
+ explicit iterator_buffer(T* out, size_t = 0) : buffer<T>(out, 0, ~size_t()) {}
+
+ auto out() -> T* { return &*this->end(); }
+};
+
+// A buffer that writes to a container with the contiguous storage.
+template <typename Container>
+class iterator_buffer<std::back_insert_iterator<Container>,
+ enable_if_t<is_contiguous<Container>::value,
+ typename Container::value_type>>
+ final : public buffer<typename Container::value_type> {
+ private:
+ Container& container_;
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t capacity) override {
+ container_.resize(capacity);
+ this->set(&container_[0], capacity);
+ }
+
+ public:
+ explicit iterator_buffer(Container& c)
+ : buffer<typename Container::value_type>(c.size()), container_(c) {}
+ explicit iterator_buffer(std::back_insert_iterator<Container> out, size_t = 0)
+ : iterator_buffer(get_container(out)) {}
+
+ auto out() -> std::back_insert_iterator<Container> {
+ return std::back_inserter(container_);
+ }
+};
+
+// A buffer that counts the number of code units written discarding the output.
+template <typename T = char> class counting_buffer final : public buffer<T> {
+ private:
+ enum { buffer_size = 256 };
+ T data_[buffer_size];
+ size_t count_ = 0;
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t) override {
+ if (this->size() != buffer_size) return;
+ count_ += this->size();
+ this->clear();
+ }
+
+ public:
+ counting_buffer() : buffer<T>(data_, 0, buffer_size) {}
+
+ auto count() -> size_t { return count_ + this->size(); }
+};
+
+template <typename T>
+using buffer_appender = conditional_t<std::is_same<T, char>::value, appender,
+ std::back_insert_iterator<buffer<T>>>;
+
+// Maps an output iterator to a buffer.
+template <typename T, typename OutputIt>
+auto get_buffer(OutputIt out) -> iterator_buffer<OutputIt, T> {
+ return iterator_buffer<OutputIt, T>(out);
+}
+template <typename T, typename Buf,
+ FMT_ENABLE_IF(std::is_base_of<buffer<char>, Buf>::value)>
+auto get_buffer(std::back_insert_iterator<Buf> out) -> buffer<char>& {
+ return get_container(out);
+}
+
+template <typename Buf, typename OutputIt>
+FMT_INLINE auto get_iterator(Buf& buf, OutputIt) -> decltype(buf.out()) {
+ return buf.out();
+}
+template <typename T, typename OutputIt>
+auto get_iterator(buffer<T>&, OutputIt out) -> OutputIt {
+ return out;
+}
+
+struct view {};
+
+template <typename Char, typename T> struct named_arg : view {
+ const Char* name;
+ const T& value;
+ named_arg(const Char* n, const T& v) : name(n), value(v) {}
+};
+
+template <typename Char> struct named_arg_info {
+ const Char* name;
+ int id;
+};
+
+template <typename T, typename Char, size_t NUM_ARGS, size_t NUM_NAMED_ARGS>
+struct arg_data {
+ // args_[0].named_args points to named_args_ to avoid bloating format_args.
+ // +1 to workaround a bug in gcc 7.5 that causes duplicated-branches warning.
+ T args_[1 + (NUM_ARGS != 0 ? NUM_ARGS : +1)];
+ named_arg_info<Char> named_args_[NUM_NAMED_ARGS];
+
+ template <typename... U>
+ arg_data(const U&... init) : args_{T(named_args_, NUM_NAMED_ARGS), init...} {}
+ arg_data(const arg_data& other) = delete;
+ auto args() const -> const T* { return args_ + 1; }
+ auto named_args() -> named_arg_info<Char>* { return named_args_; }
+};
+
+template <typename T, typename Char, size_t NUM_ARGS>
+struct arg_data<T, Char, NUM_ARGS, 0> {
+ // +1 to workaround a bug in gcc 7.5 that causes duplicated-branches warning.
+ T args_[NUM_ARGS != 0 ? NUM_ARGS : +1];
+
+ template <typename... U>
+ FMT_CONSTEXPR FMT_INLINE arg_data(const U&... init) : args_{init...} {}
+ FMT_CONSTEXPR FMT_INLINE auto args() const -> const T* { return args_; }
+ FMT_CONSTEXPR FMT_INLINE auto named_args() -> std::nullptr_t {
+ return nullptr;
+ }
+};
+
+template <typename Char>
+inline void init_named_args(named_arg_info<Char>*, int, int) {}
+
+template <typename T> struct is_named_arg : std::false_type {};
+template <typename T> struct is_statically_named_arg : std::false_type {};
+
+template <typename T, typename Char>
+struct is_named_arg<named_arg<Char, T>> : std::true_type {};
+
+template <typename Char, typename T, typename... Tail,
+ FMT_ENABLE_IF(!is_named_arg<T>::value)>
+void init_named_args(named_arg_info<Char>* named_args, int arg_count,
+ int named_arg_count, const T&, const Tail&... args) {
+ init_named_args(named_args, arg_count + 1, named_arg_count, args...);
+}
+
+template <typename Char, typename T, typename... Tail,
+ FMT_ENABLE_IF(is_named_arg<T>::value)>
+void init_named_args(named_arg_info<Char>* named_args, int arg_count,
+ int named_arg_count, const T& arg, const Tail&... args) {
+ named_args[named_arg_count++] = {arg.name, arg_count};
+ init_named_args(named_args, arg_count + 1, named_arg_count, args...);
+}
+
+template <typename... Args>
+FMT_CONSTEXPR FMT_INLINE void init_named_args(std::nullptr_t, int, int,
+ const Args&...) {}
+
+template <bool B = false> constexpr auto count() -> size_t { return B ? 1 : 0; }
+template <bool B1, bool B2, bool... Tail> constexpr auto count() -> size_t {
+ return (B1 ? 1 : 0) + count<B2, Tail...>();
+}
+
+template <typename... Args> constexpr auto count_named_args() -> size_t {
+ return count<is_named_arg<Args>::value...>();
+}
+
+template <typename... Args>
+constexpr auto count_statically_named_args() -> size_t {
+ return count<is_statically_named_arg<Args>::value...>();
+}
+
+struct unformattable {};
+struct unformattable_char : unformattable {};
+struct unformattable_pointer : unformattable {};
+
+template <typename Char> struct string_value {
+ const Char* data;
+ size_t size;
+};
+
+template <typename Char> struct named_arg_value {
+ const named_arg_info<Char>* data;
+ size_t size;
+};
+
+template <typename Context> struct custom_value {
+ using parse_context = typename Context::parse_context_type;
+ void* value;
+ void (*format)(void* arg, parse_context& parse_ctx, Context& ctx);
+};
+
+// A formatting argument value.
+template <typename Context> class value {
+ public:
+ using char_type = typename Context::char_type;
+
+ union {
+ monostate no_value;
+ int int_value;
+ unsigned uint_value;
+ long long long_long_value;
+ unsigned long long ulong_long_value;
+ int128_opt int128_value;
+ uint128_opt uint128_value;
+ bool bool_value;
+ char_type char_value;
+ float float_value;
+ double double_value;
+ long double long_double_value;
+ const void* pointer;
+ string_value<char_type> string;
+ custom_value<Context> custom;
+ named_arg_value<char_type> named_args;
+ };
+
+ constexpr FMT_INLINE value() : no_value() {}
+ constexpr FMT_INLINE value(int val) : int_value(val) {}
+ constexpr FMT_INLINE value(unsigned val) : uint_value(val) {}
+ constexpr FMT_INLINE value(long long val) : long_long_value(val) {}
+ constexpr FMT_INLINE value(unsigned long long val) : ulong_long_value(val) {}
+ FMT_INLINE value(int128_opt val) : int128_value(val) {}
+ FMT_INLINE value(uint128_opt val) : uint128_value(val) {}
+ constexpr FMT_INLINE value(float val) : float_value(val) {}
+ constexpr FMT_INLINE value(double val) : double_value(val) {}
+ FMT_INLINE value(long double val) : long_double_value(val) {}
+ constexpr FMT_INLINE value(bool val) : bool_value(val) {}
+ constexpr FMT_INLINE value(char_type val) : char_value(val) {}
+ FMT_CONSTEXPR FMT_INLINE value(const char_type* val) {
+ string.data = val;
+ if (is_constant_evaluated()) string.size = {};
+ }
+ FMT_CONSTEXPR FMT_INLINE value(basic_string_view<char_type> val) {
+ string.data = val.data();
+ string.size = val.size();
+ }
+ FMT_INLINE value(const void* val) : pointer(val) {}
+ FMT_INLINE value(const named_arg_info<char_type>* args, size_t size)
+ : named_args{args, size} {}
+
+ template <typename T> FMT_CONSTEXPR FMT_INLINE value(T& val) {
+ using value_type = remove_cvref_t<T>;
+ custom.value = const_cast<value_type*>(&val);
+ // Get the formatter type through the context to allow different contexts
+ // have different extension points, e.g. `formatter<T>` for `format` and
+ // `printf_formatter<T>` for `printf`.
+ custom.format = format_custom_arg<
+ value_type, typename Context::template formatter_type<value_type>>;
+ }
+ value(unformattable);
+ value(unformattable_char);
+ value(unformattable_pointer);
+
+ private:
+ // Formats an argument of a custom type, such as a user-defined class.
+ template <typename T, typename Formatter>
+ static void format_custom_arg(void* arg,
+ typename Context::parse_context_type& parse_ctx,
+ Context& ctx) {
+ auto f = Formatter();
+ parse_ctx.advance_to(f.parse(parse_ctx));
+ using qualified_type =
+ conditional_t<has_const_formatter<T, Context>(), const T, T>;
+ ctx.advance_to(f.format(*static_cast<qualified_type*>(arg), ctx));
+ }
+};
+
+template <typename Context, typename T>
+FMT_CONSTEXPR auto make_arg(T&& value) -> basic_format_arg<Context>;
+
+// To minimize the number of types we need to deal with, long is translated
+// either to int or to long long depending on its size.
+enum { long_short = sizeof(long) == sizeof(int) };
+using long_type = conditional_t<long_short, int, long long>;
+using ulong_type = conditional_t<long_short, unsigned, unsigned long long>;
+
+template <typename T> struct format_as_result {
+ template <typename U,
+ FMT_ENABLE_IF(std::is_enum<U>::value || std::is_class<U>::value)>
+ static auto map(U*) -> decltype(format_as(std::declval<U>()));
+ static auto map(...) -> void;
+
+ using type = decltype(map(static_cast<T*>(nullptr)));
+};
+template <typename T> using format_as_t = typename format_as_result<T>::type;
+
+template <typename T>
+struct has_format_as
+ : bool_constant<!std::is_same<format_as_t<T>, void>::value> {};
+
+// Maps formatting arguments to core types.
+// arg_mapper reports errors by returning unformattable instead of using
+// static_assert because it's used in the is_formattable trait.
+template <typename Context> struct arg_mapper {
+ using char_type = typename Context::char_type;
+
+ FMT_CONSTEXPR FMT_INLINE auto map(signed char val) -> int { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned char val) -> unsigned {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(short val) -> int { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned short val) -> unsigned {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(int val) -> int { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned val) -> unsigned { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(long val) -> long_type { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned long val) -> ulong_type {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(long long val) -> long long { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(unsigned long long val)
+ -> unsigned long long {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(int128_opt val) -> int128_opt {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(uint128_opt val) -> uint128_opt {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(bool val) -> bool { return val; }
+
+ template <typename T, FMT_ENABLE_IF(std::is_same<T, char>::value ||
+ std::is_same<T, char_type>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(T val) -> char_type {
+ return val;
+ }
+ template <typename T, enable_if_t<(std::is_same<T, wchar_t>::value ||
+#ifdef __cpp_char8_t
+ std::is_same<T, char8_t>::value ||
+#endif
+ std::is_same<T, char16_t>::value ||
+ std::is_same<T, char32_t>::value) &&
+ !std::is_same<T, char_type>::value,
+ int> = 0>
+ FMT_CONSTEXPR FMT_INLINE auto map(T) -> unformattable_char {
+ return {};
+ }
+
+ FMT_CONSTEXPR FMT_INLINE auto map(float val) -> float { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(double val) -> double { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(long double val) -> long double {
+ return val;
+ }
+
+ FMT_CONSTEXPR FMT_INLINE auto map(char_type* val) -> const char_type* {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(const char_type* val) -> const char_type* {
+ return val;
+ }
+ template <typename T,
+ FMT_ENABLE_IF(is_string<T>::value && !std::is_pointer<T>::value &&
+ std::is_same<char_type, char_t<T>>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T& val)
+ -> basic_string_view<char_type> {
+ return to_string_view(val);
+ }
+ template <typename T,
+ FMT_ENABLE_IF(is_string<T>::value && !std::is_pointer<T>::value &&
+ !std::is_same<char_type, char_t<T>>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T&) -> unformattable_char {
+ return {};
+ }
+
+ FMT_CONSTEXPR FMT_INLINE auto map(void* val) -> const void* { return val; }
+ FMT_CONSTEXPR FMT_INLINE auto map(const void* val) -> const void* {
+ return val;
+ }
+ FMT_CONSTEXPR FMT_INLINE auto map(std::nullptr_t val) -> const void* {
+ return val;
+ }
+
+ // Use SFINAE instead of a const T* parameter to avoid a conflict with the
+ // array overload.
+ template <
+ typename T,
+ FMT_ENABLE_IF(
+ std::is_pointer<T>::value || std::is_member_pointer<T>::value ||
+ std::is_function<typename std::remove_pointer<T>::type>::value ||
+ (std::is_convertible<const T&, const void*>::value &&
+ !std::is_convertible<const T&, const char_type*>::value &&
+ !has_formatter<T, Context>::value))>
+ FMT_CONSTEXPR auto map(const T&) -> unformattable_pointer {
+ return {};
+ }
+
+ template <typename T, std::size_t N,
+ FMT_ENABLE_IF(!std::is_same<T, wchar_t>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T (&values)[N]) -> const T (&)[N] {
+ return values;
+ }
+
+ // Only map owning types because mapping views can be unsafe.
+ template <typename T, typename U = format_as_t<T>,
+ FMT_ENABLE_IF(std::is_arithmetic<U>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T& val) -> decltype(this->map(U())) {
+ return map(format_as(val));
+ }
+
+ template <typename T, typename U = remove_cvref_t<T>>
+ struct formattable
+ : bool_constant<has_const_formatter<U, Context>() ||
+ (has_formatter<U, Context>::value &&
+ !std::is_const<remove_reference_t<T>>::value)> {};
+
+ template <typename T, FMT_ENABLE_IF(formattable<T>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto do_map(T&& val) -> T& {
+ return val;
+ }
+ template <typename T, FMT_ENABLE_IF(!formattable<T>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto do_map(T&&) -> unformattable {
+ return {};
+ }
+
+ template <typename T, typename U = remove_cvref_t<T>,
+ FMT_ENABLE_IF((std::is_class<U>::value || std::is_enum<U>::value ||
+ std::is_union<U>::value) &&
+ !is_string<U>::value && !is_char<U>::value &&
+ !is_named_arg<U>::value &&
+ !std::is_arithmetic<format_as_t<U>>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(T&& val)
+ -> decltype(this->do_map(std::forward<T>(val))) {
+ return do_map(std::forward<T>(val));
+ }
+
+ template <typename T, FMT_ENABLE_IF(is_named_arg<T>::value)>
+ FMT_CONSTEXPR FMT_INLINE auto map(const T& named_arg)
+ -> decltype(this->map(named_arg.value)) {
+ return map(named_arg.value);
+ }
+
+ auto map(...) -> unformattable { return {}; }
+};
+
+// A type constant after applying arg_mapper<Context>.
+template <typename T, typename Context>
+using mapped_type_constant =
+ type_constant<decltype(arg_mapper<Context>().map(std::declval<const T&>())),
+ typename Context::char_type>;
+
+enum { packed_arg_bits = 4 };
+// Maximum number of arguments with packed types.
+enum { max_packed_args = 62 / packed_arg_bits };
+enum : unsigned long long { is_unpacked_bit = 1ULL << 63 };
+enum : unsigned long long { has_named_args_bit = 1ULL << 62 };
+} // namespace detail
+
+// An output iterator that appends to a buffer.
+// It is used to reduce symbol sizes for the common case.
+class appender : public std::back_insert_iterator<detail::buffer<char>> {
+ using base = std::back_insert_iterator<detail::buffer<char>>;
+
+ public:
+ using std::back_insert_iterator<detail::buffer<char>>::back_insert_iterator;
+ appender(base it) noexcept : base(it) {}
+ FMT_UNCHECKED_ITERATOR(appender);
+
+ auto operator++() noexcept -> appender& { return *this; }
+ auto operator++(int) noexcept -> appender { return *this; }
+};
+
+// A formatting argument. It is a trivially copyable/constructible type to
+// allow storage in basic_memory_buffer.
+template <typename Context> class basic_format_arg {
+ private:
+ detail::value<Context> value_;
+ detail::type type_;
+
+ template <typename ContextType, typename T>
+ friend FMT_CONSTEXPR auto detail::make_arg(T&& value)
+ -> basic_format_arg<ContextType>;
+
+ template <typename Visitor, typename Ctx>
+ friend FMT_CONSTEXPR auto visit_format_arg(Visitor&& vis,
+ const basic_format_arg<Ctx>& arg)
+ -> decltype(vis(0));
+
+ friend class basic_format_args<Context>;
+ friend class dynamic_format_arg_store<Context>;
+
+ using char_type = typename Context::char_type;
+
+ template <typename T, typename Char, size_t NUM_ARGS, size_t NUM_NAMED_ARGS>
+ friend struct detail::arg_data;
+
+ basic_format_arg(const detail::named_arg_info<char_type>* args, size_t size)
+ : value_(args, size) {}
+
+ public:
+ class handle {
+ public:
+ explicit handle(detail::custom_value<Context> custom) : custom_(custom) {}
+
+ void format(typename Context::parse_context_type& parse_ctx,
+ Context& ctx) const {
+ custom_.format(custom_.value, parse_ctx, ctx);
+ }
+
+ private:
+ detail::custom_value<Context> custom_;
+ };
+
+ constexpr basic_format_arg() : type_(detail::type::none_type) {}
+
+ constexpr explicit operator bool() const noexcept {
+ return type_ != detail::type::none_type;
+ }
+
+ auto type() const -> detail::type { return type_; }
+
+ auto is_integral() const -> bool { return detail::is_integral_type(type_); }
+ auto is_arithmetic() const -> bool {
+ return detail::is_arithmetic_type(type_);
+ }
+};
+
+/**
+ \rst
+ Visits an argument dispatching to the appropriate visit method based on
+ the argument type. For example, if the argument type is ``double`` then
+ ``vis(value)`` will be called with the value of type ``double``.
+ \endrst
+ */
+FMT_MODULE_EXPORT
+template <typename Visitor, typename Context>
+FMT_CONSTEXPR FMT_INLINE auto visit_format_arg(
+ Visitor&& vis, const basic_format_arg<Context>& arg) -> decltype(vis(0)) {
+ switch (arg.type_) {
+ case detail::type::none_type:
+ break;
+ case detail::type::int_type:
+ return vis(arg.value_.int_value);
+ case detail::type::uint_type:
+ return vis(arg.value_.uint_value);
+ case detail::type::long_long_type:
+ return vis(arg.value_.long_long_value);
+ case detail::type::ulong_long_type:
+ return vis(arg.value_.ulong_long_value);
+ case detail::type::int128_type:
+ return vis(detail::convert_for_visit(arg.value_.int128_value));
+ case detail::type::uint128_type:
+ return vis(detail::convert_for_visit(arg.value_.uint128_value));
+ case detail::type::bool_type:
+ return vis(arg.value_.bool_value);
+ case detail::type::char_type:
+ return vis(arg.value_.char_value);
+ case detail::type::float_type:
+ return vis(arg.value_.float_value);
+ case detail::type::double_type:
+ return vis(arg.value_.double_value);
+ case detail::type::long_double_type:
+ return vis(arg.value_.long_double_value);
+ case detail::type::cstring_type:
+ return vis(arg.value_.string.data);
+ case detail::type::string_type:
+ using sv = basic_string_view<typename Context::char_type>;
+ return vis(sv(arg.value_.string.data, arg.value_.string.size));
+ case detail::type::pointer_type:
+ return vis(arg.value_.pointer);
+ case detail::type::custom_type:
+ return vis(typename basic_format_arg<Context>::handle(arg.value_.custom));
+ }
+ return vis(monostate());
+}
+
+namespace detail {
+
+template <typename Char, typename InputIt>
+auto copy_str(InputIt begin, InputIt end, appender out) -> appender {
+ get_container(out).append(begin, end);
+ return out;
+}
+
+template <typename Char, typename R, typename OutputIt>
+FMT_CONSTEXPR auto copy_str(R&& rng, OutputIt out) -> OutputIt {
+ return detail::copy_str<Char>(rng.begin(), rng.end(), out);
+}
+
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 500
+// A workaround for gcc 4.8 to make void_t work in a SFINAE context.
+template <typename...> struct void_t_impl { using type = void; };
+template <typename... T> using void_t = typename void_t_impl<T...>::type;
+#else
+template <typename...> using void_t = void;
+#endif
+
+template <typename It, typename T, typename Enable = void>
+struct is_output_iterator : std::false_type {};
+
+template <typename It, typename T>
+struct is_output_iterator<
+ It, T,
+ void_t<typename std::iterator_traits<It>::iterator_category,
+ decltype(*std::declval<It>() = std::declval<T>())>>
+ : std::true_type {};
+
+template <typename It> struct is_back_insert_iterator : std::false_type {};
+template <typename Container>
+struct is_back_insert_iterator<std::back_insert_iterator<Container>>
+ : std::true_type {};
+
+template <typename It>
+struct is_contiguous_back_insert_iterator : std::false_type {};
+template <typename Container>
+struct is_contiguous_back_insert_iterator<std::back_insert_iterator<Container>>
+ : is_contiguous<Container> {};
+template <>
+struct is_contiguous_back_insert_iterator<appender> : std::true_type {};
+
+// A type-erased reference to an std::locale to avoid a heavy <locale> include.
+class locale_ref {
+ private:
+ const void* locale_; // A type-erased pointer to std::locale.
+
+ public:
+ constexpr FMT_INLINE locale_ref() : locale_(nullptr) {}
+ template <typename Locale> explicit locale_ref(const Locale& loc);
+
+ explicit operator bool() const noexcept { return locale_ != nullptr; }
+
+ template <typename Locale> auto get() const -> Locale;
+};
+
+template <typename> constexpr auto encode_types() -> unsigned long long {
+ return 0;
+}
+
+template <typename Context, typename Arg, typename... Args>
+constexpr auto encode_types() -> unsigned long long {
+ return static_cast<unsigned>(mapped_type_constant<Arg, Context>::value) |
+ (encode_types<Context, Args...>() << packed_arg_bits);
+}
+
+template <typename Context, typename T>
+FMT_CONSTEXPR FMT_INLINE auto make_value(T&& val) -> value<Context> {
+ auto&& arg = arg_mapper<Context>().map(FMT_FORWARD(val));
+ using arg_type = remove_cvref_t<decltype(arg)>;
+
+ constexpr bool formattable_char =
+ !std::is_same<arg_type, unformattable_char>::value;
+ static_assert(formattable_char, "Mixing character types is disallowed.");
+
+ // Formatting of arbitrary pointers is disallowed. If you want to format a
+ // pointer cast it to `void*` or `const void*`. In particular, this forbids
+ // formatting of `[const] volatile char*` printed as bool by iostreams.
+ constexpr bool formattable_pointer =
+ !std::is_same<arg_type, unformattable_pointer>::value;
+ static_assert(formattable_pointer,
+ "Formatting of non-void pointers is disallowed.");
+
+ constexpr bool formattable = !std::is_same<arg_type, unformattable>::value;
+ static_assert(
+ formattable,
+ "Cannot format an argument. To make type T formattable provide a "
+ "formatter<T> specialization: https://fmt.dev/latest/api.html#udt");
+ return {arg};
+}
+
+template <typename Context, typename T>
+FMT_CONSTEXPR auto make_arg(T&& value) -> basic_format_arg<Context> {
+ auto arg = basic_format_arg<Context>();
+ arg.type_ = mapped_type_constant<T, Context>::value;
+ arg.value_ = make_value<Context>(value);
+ return arg;
+}
+
+// The DEPRECATED type template parameter is there to avoid an ODR violation
+// when using a fallback formatter in one translation unit and an implicit
+// conversion in another (not recommended).
+template <bool IS_PACKED, typename Context, type, typename T,
+ FMT_ENABLE_IF(IS_PACKED)>
+FMT_CONSTEXPR FMT_INLINE auto make_arg(T&& val) -> value<Context> {
+ return make_value<Context>(val);
+}
+
+template <bool IS_PACKED, typename Context, type, typename T,
+ FMT_ENABLE_IF(!IS_PACKED)>
+FMT_CONSTEXPR inline auto make_arg(T&& value) -> basic_format_arg<Context> {
+ return make_arg<Context>(value);
+}
+} // namespace detail
+FMT_BEGIN_EXPORT
+
+// Formatting context.
+template <typename OutputIt, typename Char> class basic_format_context {
+ private:
+ OutputIt out_;
+ basic_format_args<basic_format_context> args_;
+ detail::locale_ref loc_;
+
+ public:
+ using iterator = OutputIt;
+ using format_arg = basic_format_arg<basic_format_context>;
+ using format_args = basic_format_args<basic_format_context>;
+ using parse_context_type = basic_format_parse_context<Char>;
+ template <typename T> using formatter_type = formatter<T, Char>;
+
+ /** The character type for the output. */
+ using char_type = Char;
+
+ basic_format_context(basic_format_context&&) = default;
+ basic_format_context(const basic_format_context&) = delete;
+ void operator=(const basic_format_context&) = delete;
+ /**
+ Constructs a ``basic_format_context`` object. References to the arguments
+ are stored in the object so make sure they have appropriate lifetimes.
+ */
+ constexpr basic_format_context(OutputIt out, format_args ctx_args,
+ detail::locale_ref loc = {})
+ : out_(out), args_(ctx_args), loc_(loc) {}
+
+ constexpr auto arg(int id) const -> format_arg { return args_.get(id); }
+ FMT_CONSTEXPR auto arg(basic_string_view<Char> name) -> format_arg {
+ return args_.get(name);
+ }
+ FMT_CONSTEXPR auto arg_id(basic_string_view<Char> name) -> int {
+ return args_.get_id(name);
+ }
+ auto args() const -> const format_args& { return args_; }
+
+ FMT_CONSTEXPR auto error_handler() -> detail::error_handler { return {}; }
+ void on_error(const char* message) { error_handler().on_error(message); }
+
+ // Returns an iterator to the beginning of the output range.
+ FMT_CONSTEXPR auto out() -> iterator { return out_; }
+
+ // Advances the begin iterator to ``it``.
+ void advance_to(iterator it) {
+ if (!detail::is_back_insert_iterator<iterator>()) out_ = it;
+ }
+
+ FMT_CONSTEXPR auto locale() -> detail::locale_ref { return loc_; }
+};
+
+template <typename Char>
+using buffer_context =
+ basic_format_context<detail::buffer_appender<Char>, Char>;
+using format_context = buffer_context<char>;
+
+template <typename T, typename Char = char>
+using is_formattable = bool_constant<!std::is_base_of<
+ detail::unformattable, decltype(detail::arg_mapper<buffer_context<Char>>()
+ .map(std::declval<T>()))>::value>;
+
+/**
+ \rst
+ An array of references to arguments. It can be implicitly converted into
+ `~fmt::basic_format_args` for passing into type-erased formatting functions
+ such as `~fmt::vformat`.
+ \endrst
+ */
+template <typename Context, typename... Args>
+class format_arg_store
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+ // Workaround a GCC template argument substitution bug.
+ : public basic_format_args<Context>
+#endif
+{
+ private:
+ static const size_t num_args = sizeof...(Args);
+ static const size_t num_named_args = detail::count_named_args<Args...>();
+ static const bool is_packed = num_args <= detail::max_packed_args;
+
+ using value_type = conditional_t<is_packed, detail::value<Context>,
+ basic_format_arg<Context>>;
+
+ detail::arg_data<value_type, typename Context::char_type, num_args,
+ num_named_args>
+ data_;
+
+ friend class basic_format_args<Context>;
+
+ static constexpr unsigned long long desc =
+ (is_packed ? detail::encode_types<Context, Args...>()
+ : detail::is_unpacked_bit | num_args) |
+ (num_named_args != 0
+ ? static_cast<unsigned long long>(detail::has_named_args_bit)
+ : 0);
+
+ public:
+ template <typename... T>
+ FMT_CONSTEXPR FMT_INLINE format_arg_store(T&&... args)
+ :
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+ basic_format_args<Context>(*this),
+#endif
+ data_{detail::make_arg<
+ is_packed, Context,
+ detail::mapped_type_constant<remove_cvref_t<T>, Context>::value>(
+ FMT_FORWARD(args))...} {
+ detail::init_named_args(data_.named_args(), 0, 0, args...);
+ }
+};
+
+/**
+ \rst
+ Constructs a `~fmt::format_arg_store` object that contains references to
+ arguments and can be implicitly converted to `~fmt::format_args`. `Context`
+ can be omitted in which case it defaults to `~fmt::context`.
+ See `~fmt::arg` for lifetime considerations.
+ \endrst
+ */
+template <typename Context = format_context, typename... T>
+constexpr auto make_format_args(T&&... args)
+ -> format_arg_store<Context, remove_cvref_t<T>...> {
+ return {FMT_FORWARD(args)...};
+}
+
+/**
+ \rst
+ Returns a named argument to be used in a formatting function.
+ It should only be used in a call to a formatting function or
+ `dynamic_format_arg_store::push_back`.
+
+ **Example**::
+
+ fmt::print("Elapsed time: {s:.2f} seconds", fmt::arg("s", 1.23));
+ \endrst
+ */
+template <typename Char, typename T>
+inline auto arg(const Char* name, const T& arg) -> detail::named_arg<Char, T> {
+ static_assert(!detail::is_named_arg<T>(), "nested named arguments");
+ return {name, arg};
+}
+FMT_END_EXPORT
+
+/**
+ \rst
+ A view of a collection of formatting arguments. To avoid lifetime issues it
+ should only be used as a parameter type in type-erased functions such as
+ ``vformat``::
+
+ void vlog(string_view format_str, format_args args); // OK
+ format_args args = make_format_args(42); // Error: dangling reference
+ \endrst
+ */
+template <typename Context> class basic_format_args {
+ public:
+ using size_type = int;
+ using format_arg = basic_format_arg<Context>;
+
+ private:
+ // A descriptor that contains information about formatting arguments.
+ // If the number of arguments is less or equal to max_packed_args then
+ // argument types are passed in the descriptor. This reduces binary code size
+ // per formatting function call.
+ unsigned long long desc_;
+ union {
+ // If is_packed() returns true then argument values are stored in values_;
+ // otherwise they are stored in args_. This is done to improve cache
+ // locality and reduce compiled code size since storing larger objects
+ // may require more code (at least on x86-64) even if the same amount of
+ // data is actually copied to stack. It saves ~10% on the bloat test.
+ const detail::value<Context>* values_;
+ const format_arg* args_;
+ };
+
+ constexpr auto is_packed() const -> bool {
+ return (desc_ & detail::is_unpacked_bit) == 0;
+ }
+ auto has_named_args() const -> bool {
+ return (desc_ & detail::has_named_args_bit) != 0;
+ }
+
+ FMT_CONSTEXPR auto type(int index) const -> detail::type {
+ int shift = index * detail::packed_arg_bits;
+ unsigned int mask = (1 << detail::packed_arg_bits) - 1;
+ return static_cast<detail::type>((desc_ >> shift) & mask);
+ }
+
+ constexpr FMT_INLINE basic_format_args(unsigned long long desc,
+ const detail::value<Context>* values)
+ : desc_(desc), values_(values) {}
+ constexpr basic_format_args(unsigned long long desc, const format_arg* args)
+ : desc_(desc), args_(args) {}
+
+ public:
+ constexpr basic_format_args() : desc_(0), args_(nullptr) {}
+
+ /**
+ \rst
+ Constructs a `basic_format_args` object from `~fmt::format_arg_store`.
+ \endrst
+ */
+ template <typename... Args>
+ constexpr FMT_INLINE basic_format_args(
+ const format_arg_store<Context, Args...>& store)
+ : basic_format_args(format_arg_store<Context, Args...>::desc,
+ store.data_.args()) {}
+
+ /**
+ \rst
+ Constructs a `basic_format_args` object from
+ `~fmt::dynamic_format_arg_store`.
+ \endrst
+ */
+ constexpr FMT_INLINE basic_format_args(
+ const dynamic_format_arg_store<Context>& store)
+ : basic_format_args(store.get_types(), store.data()) {}
+
+ /**
+ \rst
+ Constructs a `basic_format_args` object from a dynamic set of arguments.
+ \endrst
+ */
+ constexpr basic_format_args(const format_arg* args, int count)
+ : basic_format_args(detail::is_unpacked_bit | detail::to_unsigned(count),
+ args) {}
+
+ /** Returns the argument with the specified id. */
+ FMT_CONSTEXPR auto get(int id) const -> format_arg {
+ format_arg arg;
+ if (!is_packed()) {
+ if (id < max_size()) arg = args_[id];
+ return arg;
+ }
+ if (id >= detail::max_packed_args) return arg;
+ arg.type_ = type(id);
+ if (arg.type_ == detail::type::none_type) return arg;
+ arg.value_ = values_[id];
+ return arg;
+ }
+
+ template <typename Char>
+ auto get(basic_string_view<Char> name) const -> format_arg {
+ int id = get_id(name);
+ return id >= 0 ? get(id) : format_arg();
+ }
+
+ template <typename Char>
+ auto get_id(basic_string_view<Char> name) const -> int {
+ if (!has_named_args()) return -1;
+ const auto& named_args =
+ (is_packed() ? values_[-1] : args_[-1].value_).named_args;
+ for (size_t i = 0; i < named_args.size; ++i) {
+ if (named_args.data[i].name == name) return named_args.data[i].id;
+ }
+ return -1;
+ }
+
+ auto max_size() const -> int {
+ unsigned long long max_packed = detail::max_packed_args;
+ return static_cast<int>(is_packed() ? max_packed
+ : desc_ & ~detail::is_unpacked_bit);
+ }
+};
+
+/** An alias to ``basic_format_args<format_context>``. */
+// A separate type would result in shorter symbols but break ABI compatibility
+// between clang and gcc on ARM (#1919).
+FMT_MODULE_EXPORT using format_args = basic_format_args<format_context>;
+
+// We cannot use enum classes as bit fields because of a gcc bug, so we put them
+// in namespaces instead (https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61414).
+// Additionally, if an underlying type is specified, older gcc incorrectly warns
+// that the type is too small. Both bugs are fixed in gcc 9.3.
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 903
+# define FMT_ENUM_UNDERLYING_TYPE(type)
+#else
+# define FMT_ENUM_UNDERLYING_TYPE(type) : type
+#endif
+namespace align {
+enum type FMT_ENUM_UNDERLYING_TYPE(unsigned char){none, left, right, center,
+ numeric};
+}
+using align_t = align::type;
+namespace sign {
+enum type FMT_ENUM_UNDERLYING_TYPE(unsigned char){none, minus, plus, space};
+}
+using sign_t = sign::type;
+
+namespace detail {
+
+// Workaround an array initialization issue in gcc 4.8.
+template <typename Char> struct fill_t {
+ private:
+ enum { max_size = 4 };
+ Char data_[max_size] = {Char(' '), Char(0), Char(0), Char(0)};
+ unsigned char size_ = 1;
+
+ public:
+ FMT_CONSTEXPR void operator=(basic_string_view<Char> s) {
+ auto size = s.size();
+ FMT_ASSERT(size <= max_size, "invalid fill");
+ for (size_t i = 0; i < size; ++i) data_[i] = s[i];
+ size_ = static_cast<unsigned char>(size);
+ }
+
+ constexpr auto size() const -> size_t { return size_; }
+ constexpr auto data() const -> const Char* { return data_; }
+
+ FMT_CONSTEXPR auto operator[](size_t index) -> Char& { return data_[index]; }
+ FMT_CONSTEXPR auto operator[](size_t index) const -> const Char& {
+ return data_[index];
+ }
+};
+} // namespace detail
+
+enum class presentation_type : unsigned char {
+ none,
+ dec, // 'd'
+ oct, // 'o'
+ hex_lower, // 'x'
+ hex_upper, // 'X'
+ bin_lower, // 'b'
+ bin_upper, // 'B'
+ hexfloat_lower, // 'a'
+ hexfloat_upper, // 'A'
+ exp_lower, // 'e'
+ exp_upper, // 'E'
+ fixed_lower, // 'f'
+ fixed_upper, // 'F'
+ general_lower, // 'g'
+ general_upper, // 'G'
+ chr, // 'c'
+ string, // 's'
+ pointer, // 'p'
+ debug // '?'
+};
+
+// Format specifiers for built-in and string types.
+template <typename Char = char> struct format_specs {
+ int width;
+ int precision;
+ presentation_type type;
+ align_t align : 4;
+ sign_t sign : 3;
+ bool alt : 1; // Alternate form ('#').
+ bool localized : 1;
+ detail::fill_t<Char> fill;
+
+ constexpr format_specs()
+ : width(0),
+ precision(-1),
+ type(presentation_type::none),
+ align(align::none),
+ sign(sign::none),
+ alt(false),
+ localized(false) {}
+};
+
+namespace detail {
+
+enum class arg_id_kind { none, index, name };
+
+// An argument reference.
+template <typename Char> struct arg_ref {
+ FMT_CONSTEXPR arg_ref() : kind(arg_id_kind::none), val() {}
+
+ FMT_CONSTEXPR explicit arg_ref(int index)
+ : kind(arg_id_kind::index), val(index) {}
+ FMT_CONSTEXPR explicit arg_ref(basic_string_view<Char> name)
+ : kind(arg_id_kind::name), val(name) {}
+
+ FMT_CONSTEXPR auto operator=(int idx) -> arg_ref& {
+ kind = arg_id_kind::index;
+ val.index = idx;
+ return *this;
+ }
+
+ arg_id_kind kind;
+ union value {
+ FMT_CONSTEXPR value(int idx = 0) : index(idx) {}
+ FMT_CONSTEXPR value(basic_string_view<Char> n) : name(n) {}
+
+ int index;
+ basic_string_view<Char> name;
+ } val;
+};
+
+// Format specifiers with width and precision resolved at formatting rather
+// than parsing time to allow reusing the same parsed specifiers with
+// different sets of arguments (precompilation of format strings).
+template <typename Char = char>
+struct dynamic_format_specs : format_specs<Char> {
+ arg_ref<Char> width_ref;
+ arg_ref<Char> precision_ref;
+};
+
+// Converts a character to ASCII. Returns '\0' on conversion failure.
+template <typename Char, FMT_ENABLE_IF(std::is_integral<Char>::value)>
+constexpr auto to_ascii(Char c) -> char {
+ return c <= 0xff ? static_cast<char>(c) : '\0';
+}
+template <typename Char, FMT_ENABLE_IF(std::is_enum<Char>::value)>
+constexpr auto to_ascii(Char c) -> char {
+ return c <= 0xff ? static_cast<char>(c) : '\0';
+}
+
+// Returns the number of code units in a code point or 1 on error.
+template <typename Char>
+FMT_CONSTEXPR auto code_point_length(const Char* begin) -> int {
+ if (const_check(sizeof(Char) != 1)) return 1;
+ auto c = static_cast<unsigned char>(*begin);
+ return static_cast<int>((0x3a55000000000000ull >> (2 * (c >> 3))) & 0x3) + 1;
+}
+
+// Return the result via the out param to workaround gcc bug 77539.
+template <bool IS_CONSTEXPR, typename T, typename Ptr = const T*>
+FMT_CONSTEXPR auto find(Ptr first, Ptr last, T value, Ptr& out) -> bool {
+ for (out = first; out != last; ++out) {
+ if (*out == value) return true;
+ }
+ return false;
+}
+
+template <>
+inline auto find<false, char>(const char* first, const char* last, char value,
+ const char*& out) -> bool {
+ out = static_cast<const char*>(
+ std::memchr(first, value, to_unsigned(last - first)));
+ return out != nullptr;
+}
+
+// Parses the range [begin, end) as an unsigned integer. This function assumes
+// that the range is non-empty and the first character is a digit.
+template <typename Char>
+FMT_CONSTEXPR auto parse_nonnegative_int(const Char*& begin, const Char* end,
+ int error_value) noexcept -> int {
+ FMT_ASSERT(begin != end && '0' <= *begin && *begin <= '9', "");
+ unsigned value = 0, prev = 0;
+ auto p = begin;
+ do {
+ prev = value;
+ value = value * 10 + unsigned(*p - '0');
+ ++p;
+ } while (p != end && '0' <= *p && *p <= '9');
+ auto num_digits = p - begin;
+ begin = p;
+ if (num_digits <= std::numeric_limits<int>::digits10)
+ return static_cast<int>(value);
+ // Check for overflow.
+ const unsigned max = to_unsigned((std::numeric_limits<int>::max)());
+ return num_digits == std::numeric_limits<int>::digits10 + 1 &&
+ prev * 10ull + unsigned(p[-1] - '0') <= max
+ ? static_cast<int>(value)
+ : error_value;
+}
+
+FMT_CONSTEXPR inline auto parse_align(char c) -> align_t {
+ switch (c) {
+ case '<':
+ return align::left;
+ case '>':
+ return align::right;
+ case '^':
+ return align::center;
+ }
+ return align::none;
+}
+
+template <typename Char> constexpr auto is_name_start(Char c) -> bool {
+ return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '_';
+}
+
+template <typename Char, typename Handler>
+FMT_CONSTEXPR auto do_parse_arg_id(const Char* begin, const Char* end,
+ Handler&& handler) -> const Char* {
+ Char c = *begin;
+ if (c >= '0' && c <= '9') {
+ int index = 0;
+ constexpr int max = (std::numeric_limits<int>::max)();
+ if (c != '0')
+ index = parse_nonnegative_int(begin, end, max);
+ else
+ ++begin;
+ if (begin == end || (*begin != '}' && *begin != ':'))
+ throw_format_error("invalid format string");
+ else
+ handler.on_index(index);
+ return begin;
+ }
+ if (!is_name_start(c)) {
+ throw_format_error("invalid format string");
+ return begin;
+ }
+ auto it = begin;
+ do {
+ ++it;
+ } while (it != end && (is_name_start(*it) || ('0' <= *it && *it <= '9')));
+ handler.on_name({begin, to_unsigned(it - begin)});
+ return it;
+}
+
+template <typename Char, typename Handler>
+FMT_CONSTEXPR FMT_INLINE auto parse_arg_id(const Char* begin, const Char* end,
+ Handler&& handler) -> const Char* {
+ FMT_ASSERT(begin != end, "");
+ Char c = *begin;
+ if (c != '}' && c != ':') return do_parse_arg_id(begin, end, handler);
+ handler.on_auto();
+ return begin;
+}
+
+template <typename Char> struct dynamic_spec_id_handler {
+ basic_format_parse_context<Char>& ctx;
+ arg_ref<Char>& ref;
+
+ FMT_CONSTEXPR void on_auto() {
+ int id = ctx.next_arg_id();
+ ref = arg_ref<Char>(id);
+ ctx.check_dynamic_spec(id);
+ }
+ FMT_CONSTEXPR void on_index(int id) {
+ ref = arg_ref<Char>(id);
+ ctx.check_arg_id(id);
+ ctx.check_dynamic_spec(id);
+ }
+ FMT_CONSTEXPR void on_name(basic_string_view<Char> id) {
+ ref = arg_ref<Char>(id);
+ ctx.check_arg_id(id);
+ }
+};
+
+// Parses [integer | "{" [arg_id] "}"].
+template <typename Char>
+FMT_CONSTEXPR auto parse_dynamic_spec(const Char* begin, const Char* end,
+ int& value, arg_ref<Char>& ref,
+ basic_format_parse_context<Char>& ctx)
+ -> const Char* {
+ FMT_ASSERT(begin != end, "");
+ if ('0' <= *begin && *begin <= '9') {
+ int val = parse_nonnegative_int(begin, end, -1);
+ if (val != -1)
+ value = val;
+ else
+ throw_format_error("number is too big");
+ } else if (*begin == '{') {
+ ++begin;
+ auto handler = dynamic_spec_id_handler<Char>{ctx, ref};
+ if (begin != end) begin = parse_arg_id(begin, end, handler);
+ if (begin != end && *begin == '}') return ++begin;
+ throw_format_error("invalid format string");
+ }
+ return begin;
+}
+
+template <typename Char>
+FMT_CONSTEXPR auto parse_precision(const Char* begin, const Char* end,
+ int& value, arg_ref<Char>& ref,
+ basic_format_parse_context<Char>& ctx)
+ -> const Char* {
+ ++begin;
+ if (begin == end || *begin == '}') {
+ throw_format_error("invalid precision");
+ return begin;
+ }
+ return parse_dynamic_spec(begin, end, value, ref, ctx);
+}
+
+enum class state { start, align, sign, hash, zero, width, precision, locale };
+
+// Parses standard format specifiers.
+template <typename Char>
+FMT_CONSTEXPR FMT_INLINE auto parse_format_specs(
+ const Char* begin, const Char* end, dynamic_format_specs<Char>& specs,
+ basic_format_parse_context<Char>& ctx, type arg_type) -> const Char* {
+ auto c = '\0';
+ if (end - begin > 1) {
+ auto next = to_ascii(begin[1]);
+ c = parse_align(next) == align::none ? to_ascii(*begin) : '\0';
+ } else {
+ if (begin == end) return begin;
+ c = to_ascii(*begin);
+ }
+
+ struct {
+ state current_state = state::start;
+ FMT_CONSTEXPR void operator()(state s, bool valid = true) {
+ if (current_state >= s || !valid)
+ throw_format_error("invalid format specifier");
+ current_state = s;
+ }
+ } enter_state;
+
+ using pres = presentation_type;
+ constexpr auto integral_set = sint_set | uint_set | bool_set | char_set;
+ struct {
+ const Char*& begin;
+ dynamic_format_specs<Char>& specs;
+ type arg_type;
+
+ FMT_CONSTEXPR auto operator()(pres type, int set) -> const Char* {
+ if (!in(arg_type, set)) throw_format_error("invalid format specifier");
+ specs.type = type;
+ return begin + 1;
+ }
+ } parse_presentation_type{begin, specs, arg_type};
+
+ for (;;) {
+ switch (c) {
+ case '<':
+ case '>':
+ case '^':
+ enter_state(state::align);
+ specs.align = parse_align(c);
+ ++begin;
+ break;
+ case '+':
+ case '-':
+ case ' ':
+ enter_state(state::sign, in(arg_type, sint_set | float_set));
+ switch (c) {
+ case '+':
+ specs.sign = sign::plus;
+ break;
+ case '-':
+ specs.sign = sign::minus;
+ break;
+ case ' ':
+ specs.sign = sign::space;
+ break;
+ }
+ ++begin;
+ break;
+ case '#':
+ enter_state(state::hash, is_arithmetic_type(arg_type));
+ specs.alt = true;
+ ++begin;
+ break;
+ case '0':
+ enter_state(state::zero);
+ if (!is_arithmetic_type(arg_type))
+ throw_format_error("format specifier requires numeric argument");
+ if (specs.align == align::none) {
+ // Ignore 0 if align is specified for compatibility with std::format.
+ specs.align = align::numeric;
+ specs.fill[0] = Char('0');
+ }
+ ++begin;
+ break;
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ case '{':
+ enter_state(state::width);
+ begin = parse_dynamic_spec(begin, end, specs.width, specs.width_ref, ctx);
+ break;
+ case '.':
+ enter_state(state::precision,
+ in(arg_type, float_set | string_set | cstring_set));
+ begin = parse_precision(begin, end, specs.precision, specs.precision_ref,
+ ctx);
+ break;
+ case 'L':
+ enter_state(state::locale, is_arithmetic_type(arg_type));
+ specs.localized = true;
+ ++begin;
+ break;
+ case 'd':
+ return parse_presentation_type(pres::dec, integral_set);
+ case 'o':
+ return parse_presentation_type(pres::oct, integral_set);
+ case 'x':
+ return parse_presentation_type(pres::hex_lower, integral_set);
+ case 'X':
+ return parse_presentation_type(pres::hex_upper, integral_set);
+ case 'b':
+ return parse_presentation_type(pres::bin_lower, integral_set);
+ case 'B':
+ return parse_presentation_type(pres::bin_upper, integral_set);
+ case 'a':
+ return parse_presentation_type(pres::hexfloat_lower, float_set);
+ case 'A':
+ return parse_presentation_type(pres::hexfloat_upper, float_set);
+ case 'e':
+ return parse_presentation_type(pres::exp_lower, float_set);
+ case 'E':
+ return parse_presentation_type(pres::exp_upper, float_set);
+ case 'f':
+ return parse_presentation_type(pres::fixed_lower, float_set);
+ case 'F':
+ return parse_presentation_type(pres::fixed_upper, float_set);
+ case 'g':
+ return parse_presentation_type(pres::general_lower, float_set);
+ case 'G':
+ return parse_presentation_type(pres::general_upper, float_set);
+ case 'c':
+ return parse_presentation_type(pres::chr, integral_set);
+ case 's':
+ return parse_presentation_type(pres::string,
+ bool_set | string_set | cstring_set);
+ case 'p':
+ return parse_presentation_type(pres::pointer, pointer_set | cstring_set);
+ case '?':
+ return parse_presentation_type(pres::debug,
+ char_set | string_set | cstring_set);
+ case '}':
+ return begin;
+ default: {
+ if (*begin == '}') return begin;
+ // Parse fill and alignment.
+ auto fill_end = begin + code_point_length(begin);
+ if (end - fill_end <= 0) {
+ throw_format_error("invalid format specifier");
+ return begin;
+ }
+ if (*begin == '{') {
+ throw_format_error("invalid fill character '{'");
+ return begin;
+ }
+ auto align = parse_align(to_ascii(*fill_end));
+ enter_state(state::align, align != align::none);
+ specs.fill = {begin, to_unsigned(fill_end - begin)};
+ specs.align = align;
+ begin = fill_end + 1;
+ }
+ }
+ if (begin == end) return begin;
+ c = to_ascii(*begin);
+ }
+}
+
+template <typename Char, typename Handler>
+FMT_CONSTEXPR auto parse_replacement_field(const Char* begin, const Char* end,
+ Handler&& handler) -> const Char* {
+ struct id_adapter {
+ Handler& handler;
+ int arg_id;
+
+ FMT_CONSTEXPR void on_auto() { arg_id = handler.on_arg_id(); }
+ FMT_CONSTEXPR void on_index(int id) { arg_id = handler.on_arg_id(id); }
+ FMT_CONSTEXPR void on_name(basic_string_view<Char> id) {
+ arg_id = handler.on_arg_id(id);
+ }
+ };
+
+ ++begin;
+ if (begin == end) return handler.on_error("invalid format string"), end;
+ if (*begin == '}') {
+ handler.on_replacement_field(handler.on_arg_id(), begin);
+ } else if (*begin == '{') {
+ handler.on_text(begin, begin + 1);
+ } else {
+ auto adapter = id_adapter{handler, 0};
+ begin = parse_arg_id(begin, end, adapter);
+ Char c = begin != end ? *begin : Char();
+ if (c == '}') {
+ handler.on_replacement_field(adapter.arg_id, begin);
+ } else if (c == ':') {
+ begin = handler.on_format_specs(adapter.arg_id, begin + 1, end);
+ if (begin == end || *begin != '}')
+ return handler.on_error("unknown format specifier"), end;
+ } else {
+ return handler.on_error("missing '}' in format string"), end;
+ }
+ }
+ return begin + 1;
+}
+
+template <bool IS_CONSTEXPR, typename Char, typename Handler>
+FMT_CONSTEXPR FMT_INLINE void parse_format_string(
+ basic_string_view<Char> format_str, Handler&& handler) {
+ auto begin = format_str.data();
+ auto end = begin + format_str.size();
+ if (end - begin < 32) {
+ // Use a simple loop instead of memchr for small strings.
+ const Char* p = begin;
+ while (p != end) {
+ auto c = *p++;
+ if (c == '{') {
+ handler.on_text(begin, p - 1);
+ begin = p = parse_replacement_field(p - 1, end, handler);
+ } else if (c == '}') {
+ if (p == end || *p != '}')
+ return handler.on_error("unmatched '}' in format string");
+ handler.on_text(begin, p);
+ begin = ++p;
+ }
+ }
+ handler.on_text(begin, end);
+ return;
+ }
+ struct writer {
+ FMT_CONSTEXPR void operator()(const Char* from, const Char* to) {
+ if (from == to) return;
+ for (;;) {
+ const Char* p = nullptr;
+ if (!find<IS_CONSTEXPR>(from, to, Char('}'), p))
+ return handler_.on_text(from, to);
+ ++p;
+ if (p == to || *p != '}')
+ return handler_.on_error("unmatched '}' in format string");
+ handler_.on_text(from, p);
+ from = p + 1;
+ }
+ }
+ Handler& handler_;
+ } write = {handler};
+ while (begin != end) {
+ // Doing two passes with memchr (one for '{' and another for '}') is up to
+ // 2.5x faster than the naive one-pass implementation on big format strings.
+ const Char* p = begin;
+ if (*begin != '{' && !find<IS_CONSTEXPR>(begin + 1, end, Char('{'), p))
+ return write(begin, end);
+ write(begin, p);
+ begin = parse_replacement_field(p, end, handler);
+ }
+}
+
+template <typename T, bool = is_named_arg<T>::value> struct strip_named_arg {
+ using type = T;
+};
+template <typename T> struct strip_named_arg<T, true> {
+ using type = remove_cvref_t<decltype(T::value)>;
+};
+
+template <typename T, typename ParseContext>
+FMT_CONSTEXPR auto parse_format_specs(ParseContext& ctx)
+ -> decltype(ctx.begin()) {
+ using char_type = typename ParseContext::char_type;
+ using context = buffer_context<char_type>;
+ using mapped_type = conditional_t<
+ mapped_type_constant<T, context>::value != type::custom_type,
+ decltype(arg_mapper<context>().map(std::declval<const T&>())),
+ typename strip_named_arg<T>::type>;
+ return formatter<mapped_type, char_type>().parse(ctx);
+}
+
+// Checks char specs and returns true iff the presentation type is char-like.
+template <typename Char>
+FMT_CONSTEXPR auto check_char_specs(const format_specs<Char>& specs) -> bool {
+ if (specs.type != presentation_type::none &&
+ specs.type != presentation_type::chr &&
+ specs.type != presentation_type::debug) {
+ return false;
+ }
+ if (specs.align == align::numeric || specs.sign != sign::none || specs.alt)
+ throw_format_error("invalid format specifier for char");
+ return true;
+}
+
+constexpr FMT_INLINE_VARIABLE int invalid_arg_index = -1;
+
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <int N, typename T, typename... Args, typename Char>
+constexpr auto get_arg_index_by_name(basic_string_view<Char> name) -> int {
+ if constexpr (is_statically_named_arg<T>()) {
+ if (name == T::name) return N;
+ }
+ if constexpr (sizeof...(Args) > 0)
+ return get_arg_index_by_name<N + 1, Args...>(name);
+ (void)name; // Workaround an MSVC bug about "unused" parameter.
+ return invalid_arg_index;
+}
+#endif
+
+template <typename... Args, typename Char>
+FMT_CONSTEXPR auto get_arg_index_by_name(basic_string_view<Char> name) -> int {
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+ if constexpr (sizeof...(Args) > 0)
+ return get_arg_index_by_name<0, Args...>(name);
+#endif
+ (void)name;
+ return invalid_arg_index;
+}
+
+template <typename Char, typename... Args> class format_string_checker {
+ private:
+ using parse_context_type = compile_parse_context<Char>;
+ static constexpr int num_args = sizeof...(Args);
+
+ // Format specifier parsing function.
+ // In the future basic_format_parse_context will replace compile_parse_context
+ // here and will use is_constant_evaluated and downcasting to access the data
+ // needed for compile-time checks: https://godbolt.org/z/GvWzcTjh1.
+ using parse_func = const Char* (*)(parse_context_type&);
+
+ parse_context_type context_;
+ parse_func parse_funcs_[num_args > 0 ? static_cast<size_t>(num_args) : 1];
+ type types_[num_args > 0 ? static_cast<size_t>(num_args) : 1];
+
+ public:
+ explicit FMT_CONSTEXPR format_string_checker(basic_string_view<Char> fmt)
+ : context_(fmt, num_args, types_),
+ parse_funcs_{&parse_format_specs<Args, parse_context_type>...},
+ types_{mapped_type_constant<Args, buffer_context<Char>>::value...} {}
+
+ FMT_CONSTEXPR void on_text(const Char*, const Char*) {}
+
+ FMT_CONSTEXPR auto on_arg_id() -> int { return context_.next_arg_id(); }
+ FMT_CONSTEXPR auto on_arg_id(int id) -> int {
+ return context_.check_arg_id(id), id;
+ }
+ FMT_CONSTEXPR auto on_arg_id(basic_string_view<Char> id) -> int {
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+ auto index = get_arg_index_by_name<Args...>(id);
+ if (index == invalid_arg_index) on_error("named argument is not found");
+ return index;
+#else
+ (void)id;
+ on_error("compile-time checks for named arguments require C++20 support");
+ return 0;
+#endif
+ }
+
+ FMT_CONSTEXPR void on_replacement_field(int, const Char*) {}
+
+ FMT_CONSTEXPR auto on_format_specs(int id, const Char* begin, const Char*)
+ -> const Char* {
+ context_.advance_to(begin);
+ // id >= 0 check is a workaround for gcc 10 bug (#2065).
+ return id >= 0 && id < num_args ? parse_funcs_[id](context_) : begin;
+ }
+
+ FMT_CONSTEXPR void on_error(const char* message) {
+ throw_format_error(message);
+ }
+};
+
+// Reports a compile-time error if S is not a valid format string.
+template <typename..., typename S, FMT_ENABLE_IF(!is_compile_string<S>::value)>
+FMT_INLINE void check_format_string(const S&) {
+#ifdef FMT_ENFORCE_COMPILE_STRING
+ static_assert(is_compile_string<S>::value,
+ "FMT_ENFORCE_COMPILE_STRING requires all format strings to use "
+ "FMT_STRING.");
+#endif
+}
+template <typename... Args, typename S,
+ FMT_ENABLE_IF(is_compile_string<S>::value)>
+void check_format_string(S format_str) {
+ using char_t = typename S::char_type;
+ FMT_CONSTEXPR auto s = basic_string_view<char_t>(format_str);
+ using checker = format_string_checker<char_t, remove_cvref_t<Args>...>;
+ FMT_CONSTEXPR bool error = (parse_format_string<true>(s, checker(s)), true);
+ ignore_unused(error);
+}
+
+template <typename Char = char> struct vformat_args {
+ using type = basic_format_args<
+ basic_format_context<std::back_insert_iterator<buffer<Char>>, Char>>;
+};
+template <> struct vformat_args<char> { using type = format_args; };
+
+// Use vformat_args and avoid type_identity to keep symbols short.
+template <typename Char>
+void vformat_to(buffer<Char>& buf, basic_string_view<Char> fmt,
+ typename vformat_args<Char>::type args, locale_ref loc = {});
+
+FMT_API void vprint_mojibake(std::FILE*, string_view, format_args);
+#ifndef _WIN32
+inline void vprint_mojibake(std::FILE*, string_view, format_args) {}
+#endif
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+// A formatter specialization for natively supported types.
+template <typename T, typename Char>
+struct formatter<T, Char,
+ enable_if_t<detail::type_constant<T, Char>::value !=
+ detail::type::custom_type>> {
+ private:
+ detail::dynamic_format_specs<Char> specs_;
+
+ public:
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const Char* {
+ auto type = detail::type_constant<T, Char>::value;
+ auto end =
+ detail::parse_format_specs(ctx.begin(), ctx.end(), specs_, ctx, type);
+ if (type == detail::type::char_type) detail::check_char_specs(specs_);
+ return end;
+ }
+
+ template <detail::type U = detail::type_constant<T, Char>::value,
+ FMT_ENABLE_IF(U == detail::type::string_type ||
+ U == detail::type::cstring_type ||
+ U == detail::type::char_type)>
+ FMT_CONSTEXPR void set_debug_format(bool set = true) {
+ specs_.type = set ? presentation_type::debug : presentation_type::none;
+ }
+
+ template <typename FormatContext>
+ FMT_CONSTEXPR auto format(const T& val, FormatContext& ctx) const
+ -> decltype(ctx.out());
+};
+
+#define FMT_FORMAT_AS(Type, Base) \
+ template <typename Char> \
+ struct formatter<Type, Char> : formatter<Base, Char> { \
+ template <typename FormatContext> \
+ auto format(const Type& val, FormatContext& ctx) const \
+ -> decltype(ctx.out()) { \
+ return formatter<Base, Char>::format(static_cast<Base>(val), ctx); \
+ } \
+ }
+
+FMT_FORMAT_AS(signed char, int);
+FMT_FORMAT_AS(unsigned char, unsigned);
+FMT_FORMAT_AS(short, int);
+FMT_FORMAT_AS(unsigned short, unsigned);
+FMT_FORMAT_AS(long, long long);
+FMT_FORMAT_AS(unsigned long, unsigned long long);
+FMT_FORMAT_AS(Char*, const Char*);
+FMT_FORMAT_AS(std::basic_string<Char>, basic_string_view<Char>);
+FMT_FORMAT_AS(std::nullptr_t, const void*);
+FMT_FORMAT_AS(detail::std_string_view<Char>, basic_string_view<Char>);
+
+template <typename Char = char> struct runtime_format_string {
+ basic_string_view<Char> str;
+};
+
+/** A compile-time format string. */
+template <typename Char, typename... Args> class basic_format_string {
+ private:
+ basic_string_view<Char> str_;
+
+ public:
+ template <typename S,
+ FMT_ENABLE_IF(
+ std::is_convertible<const S&, basic_string_view<Char>>::value)>
+ FMT_CONSTEVAL FMT_INLINE basic_format_string(const S& s) : str_(s) {
+ static_assert(
+ detail::count<
+ (std::is_base_of<detail::view, remove_reference_t<Args>>::value &&
+ std::is_reference<Args>::value)...>() == 0,
+ "passing views as lvalues is disallowed");
+#ifdef FMT_HAS_CONSTEVAL
+ if constexpr (detail::count_named_args<Args...>() ==
+ detail::count_statically_named_args<Args...>()) {
+ using checker =
+ detail::format_string_checker<Char, remove_cvref_t<Args>...>;
+ detail::parse_format_string<true>(str_, checker(s));
+ }
+#else
+ detail::check_format_string<Args...>(s);
+#endif
+ }
+ basic_format_string(runtime_format_string<Char> fmt) : str_(fmt.str) {}
+
+ FMT_INLINE operator basic_string_view<Char>() const { return str_; }
+ FMT_INLINE auto get() const -> basic_string_view<Char> { return str_; }
+};
+
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+// Workaround broken conversion on older gcc.
+template <typename...> using format_string = string_view;
+inline auto runtime(string_view s) -> string_view { return s; }
+#else
+template <typename... Args>
+using format_string = basic_format_string<char, type_identity_t<Args>...>;
+/**
+ \rst
+ Creates a runtime format string.
+
+ **Example**::
+
+ // Check format string at runtime instead of compile-time.
+ fmt::print(fmt::runtime("{:d}"), "I am not a number");
+ \endrst
+ */
+inline auto runtime(string_view s) -> runtime_format_string<> { return {{s}}; }
+#endif
+
+FMT_API auto vformat(string_view fmt, format_args args) -> std::string;
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt`` and returns the result
+ as a string.
+
+ **Example**::
+
+ #include <fmt/core.h>
+ std::string message = fmt::format("The answer is {}.", 42);
+ \endrst
+*/
+template <typename... T>
+FMT_NODISCARD FMT_INLINE auto format(format_string<T...> fmt, T&&... args)
+ -> std::string {
+ return vformat(fmt, fmt::make_format_args(args...));
+}
+
+/** Formats a string and writes the output to ``out``. */
+template <typename OutputIt,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
+auto vformat_to(OutputIt out, string_view fmt, format_args args) -> OutputIt {
+ auto&& buf = detail::get_buffer<char>(out);
+ detail::vformat_to(buf, fmt, args, {});
+ return detail::get_iterator(buf, out);
+}
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt``, writes the result to
+ the output iterator ``out`` and returns the iterator past the end of the output
+ range. `format_to` does not append a terminating null character.
+
+ **Example**::
+
+ auto out = std::vector<char>();
+ fmt::format_to(std::back_inserter(out), "{}", 42);
+ \endrst
+ */
+template <typename OutputIt, typename... T,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
+FMT_INLINE auto format_to(OutputIt out, format_string<T...> fmt, T&&... args)
+ -> OutputIt {
+ return vformat_to(out, fmt, fmt::make_format_args(args...));
+}
+
+template <typename OutputIt> struct format_to_n_result {
+ /** Iterator past the end of the output range. */
+ OutputIt out;
+ /** Total (not truncated) output size. */
+ size_t size;
+};
+
+template <typename OutputIt, typename... T,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
+auto vformat_to_n(OutputIt out, size_t n, string_view fmt, format_args args)
+ -> format_to_n_result<OutputIt> {
+ using traits = detail::fixed_buffer_traits;
+ auto buf = detail::iterator_buffer<OutputIt, char, traits>(out, n);
+ detail::vformat_to(buf, fmt, args, {});
+ return {buf.out(), buf.count()};
+}
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt``, writes up to ``n``
+ characters of the result to the output iterator ``out`` and returns the total
+ (not truncated) output size and the iterator past the end of the output range.
+ `format_to_n` does not append a terminating null character.
+ \endrst
+ */
+template <typename OutputIt, typename... T,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value)>
+FMT_INLINE auto format_to_n(OutputIt out, size_t n, format_string<T...> fmt,
+ T&&... args) -> format_to_n_result<OutputIt> {
+ return vformat_to_n(out, n, fmt, fmt::make_format_args(args...));
+}
+
+/** Returns the number of chars in the output of ``format(fmt, args...)``. */
+template <typename... T>
+FMT_NODISCARD FMT_INLINE auto formatted_size(format_string<T...> fmt,
+ T&&... args) -> size_t {
+ auto buf = detail::counting_buffer<>();
+ detail::vformat_to<char>(buf, fmt, fmt::make_format_args(args...), {});
+ return buf.count();
+}
+
+FMT_API void vprint(string_view fmt, format_args args);
+FMT_API void vprint(std::FILE* f, string_view fmt, format_args args);
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt`` and writes the output
+ to ``stdout``.
+
+ **Example**::
+
+ fmt::print("Elapsed time: {0:.2f} seconds", 1.23);
+ \endrst
+ */
+template <typename... T>
+FMT_INLINE void print(format_string<T...> fmt, T&&... args) {
+ const auto& vargs = fmt::make_format_args(args...);
+ return detail::is_utf8() ? vprint(fmt, vargs)
+ : detail::vprint_mojibake(stdout, fmt, vargs);
+}
+
+/**
+ \rst
+ Formats ``args`` according to specifications in ``fmt`` and writes the
+ output to the file ``f``.
+
+ **Example**::
+
+ fmt::print(stderr, "Don't {}!", "panic");
+ \endrst
+ */
+template <typename... T>
+FMT_INLINE void print(std::FILE* f, format_string<T...> fmt, T&&... args) {
+ const auto& vargs = fmt::make_format_args(args...);
+ return detail::is_utf8() ? vprint(f, fmt, vargs)
+ : detail::vprint_mojibake(f, fmt, vargs);
+}
+
+/**
+ Formats ``args`` according to specifications in ``fmt`` and writes the
+ output to the file ``f`` followed by a newline.
+ */
+template <typename... T>
+FMT_INLINE void println(std::FILE* f, format_string<T...> fmt, T&&... args) {
+ return fmt::print(f, "{}\n", fmt::format(fmt, std::forward<T>(args)...));
+}
+
+/**
+ Formats ``args`` according to specifications in ``fmt`` and writes the output
+ to ``stdout`` followed by a newline.
+ */
+template <typename... T>
+FMT_INLINE void println(format_string<T...> fmt, T&&... args) {
+ return fmt::println(stdout, fmt, std::forward<T>(args)...);
+}
+
+FMT_END_EXPORT
+FMT_GCC_PRAGMA("GCC pop_options")
+FMT_END_NAMESPACE
+
+#ifdef FMT_HEADER_ONLY
+# include "format.h"
+#endif
+#endif // FMT_CORE_H_
diff --git a/src/fmtlib/fmt/format-inl.h b/src/fmtlib/fmt/format-inl.h
new file mode 100644
index 0000000..5bae3c7
--- /dev/null
+++ b/src/fmtlib/fmt/format-inl.h
@@ -0,0 +1,1681 @@
+// Formatting library for C++ - implementation
+//
+// Copyright (c) 2012 - 2016, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_FORMAT_INL_H_
+#define FMT_FORMAT_INL_H_
+
+#include <algorithm>
+#include <cerrno> // errno
+#include <climits>
+#include <cmath>
+#include <exception>
+
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+# include <locale>
+#endif
+
+#ifdef _WIN32
+# include <io.h> // _isatty
+#endif
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+FMT_FUNC void assert_fail(const char* file, int line, const char* message) {
+ // Use unchecked std::fprintf to avoid triggering another assertion when
+ // writing to stderr fails
+ std::fprintf(stderr, "%s:%d: assertion failed: %s", file, line, message);
+ // Chosen instead of std::abort to satisfy Clang in CUDA mode during device
+ // code pass.
+ std::terminate();
+}
+
+FMT_FUNC void throw_format_error(const char* message) {
+ FMT_THROW(format_error(message));
+}
+
+FMT_FUNC void format_error_code(detail::buffer<char>& out, int error_code,
+ string_view message) noexcept {
+ // Report error code making sure that the output fits into
+ // inline_buffer_size to avoid dynamic memory allocation and potential
+ // bad_alloc.
+ out.try_resize(0);
+ static const char SEP[] = ": ";
+ static const char ERROR_STR[] = "error ";
+ // Subtract 2 to account for terminating null characters in SEP and ERROR_STR.
+ size_t error_code_size = sizeof(SEP) + sizeof(ERROR_STR) - 2;
+ auto abs_value = static_cast<uint32_or_64_or_128_t<int>>(error_code);
+ if (detail::is_negative(error_code)) {
+ abs_value = 0 - abs_value;
+ ++error_code_size;
+ }
+ error_code_size += detail::to_unsigned(detail::count_digits(abs_value));
+ auto it = buffer_appender<char>(out);
+ if (message.size() <= inline_buffer_size - error_code_size)
+ format_to(it, FMT_STRING("{}{}"), message, SEP);
+ format_to(it, FMT_STRING("{}{}"), ERROR_STR, error_code);
+ FMT_ASSERT(out.size() <= inline_buffer_size, "");
+}
+
+FMT_FUNC void report_error(format_func func, int error_code,
+ const char* message) noexcept {
+ memory_buffer full_message;
+ func(full_message, error_code, message);
+ // Don't use fwrite_fully because the latter may throw.
+ if (std::fwrite(full_message.data(), full_message.size(), 1, stderr) > 0)
+ std::fputc('\n', stderr);
+}
+
+// A wrapper around fwrite that throws on error.
+inline void fwrite_fully(const void* ptr, size_t size, size_t count,
+ FILE* stream) {
+ size_t written = std::fwrite(ptr, size, count, stream);
+ if (written < count)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot write to file")));
+}
+
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+template <typename Locale>
+locale_ref::locale_ref(const Locale& loc) : locale_(&loc) {
+ static_assert(std::is_same<Locale, std::locale>::value, "");
+}
+
+template <typename Locale> Locale locale_ref::get() const {
+ static_assert(std::is_same<Locale, std::locale>::value, "");
+ return locale_ ? *static_cast<const std::locale*>(locale_) : std::locale();
+}
+
+template <typename Char>
+FMT_FUNC auto thousands_sep_impl(locale_ref loc) -> thousands_sep_result<Char> {
+ auto& facet = std::use_facet<std::numpunct<Char>>(loc.get<std::locale>());
+ auto grouping = facet.grouping();
+ auto thousands_sep = grouping.empty() ? Char() : facet.thousands_sep();
+ return {std::move(grouping), thousands_sep};
+}
+template <typename Char> FMT_FUNC Char decimal_point_impl(locale_ref loc) {
+ return std::use_facet<std::numpunct<Char>>(loc.get<std::locale>())
+ .decimal_point();
+}
+#else
+template <typename Char>
+FMT_FUNC auto thousands_sep_impl(locale_ref) -> thousands_sep_result<Char> {
+ return {"\03", FMT_STATIC_THOUSANDS_SEPARATOR};
+}
+template <typename Char> FMT_FUNC Char decimal_point_impl(locale_ref) {
+ return '.';
+}
+#endif
+
+FMT_FUNC auto write_loc(appender out, loc_value value,
+ const format_specs<>& specs, locale_ref loc) -> bool {
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+ auto locale = loc.get<std::locale>();
+ // We cannot use the num_put<char> facet because it may produce output in
+ // a wrong encoding.
+ using facet = format_facet<std::locale>;
+ if (std::has_facet<facet>(locale))
+ return std::use_facet<facet>(locale).put(out, value, specs);
+ return facet(locale).put(out, value, specs);
+#endif
+ return false;
+}
+} // namespace detail
+
+template <typename Locale> typename Locale::id format_facet<Locale>::id;
+
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+template <typename Locale> format_facet<Locale>::format_facet(Locale& loc) {
+ auto& numpunct = std::use_facet<std::numpunct<char>>(loc);
+ grouping_ = numpunct.grouping();
+ if (!grouping_.empty()) separator_ = std::string(1, numpunct.thousands_sep());
+}
+
+template <>
+FMT_API FMT_FUNC auto format_facet<std::locale>::do_put(
+ appender out, loc_value val, const format_specs<>& specs) const -> bool {
+ return val.visit(
+ detail::loc_writer<>{out, specs, separator_, grouping_, decimal_point_});
+}
+#endif
+
+FMT_FUNC std::system_error vsystem_error(int error_code, string_view fmt,
+ format_args args) {
+ auto ec = std::error_code(error_code, std::generic_category());
+ return std::system_error(ec, vformat(fmt, args));
+}
+
+namespace detail {
+
+template <typename F> inline bool operator==(basic_fp<F> x, basic_fp<F> y) {
+ return x.f == y.f && x.e == y.e;
+}
+
+// Compilers should be able to optimize this into the ror instruction.
+FMT_CONSTEXPR inline uint32_t rotr(uint32_t n, uint32_t r) noexcept {
+ r &= 31;
+ return (n >> r) | (n << (32 - r));
+}
+FMT_CONSTEXPR inline uint64_t rotr(uint64_t n, uint32_t r) noexcept {
+ r &= 63;
+ return (n >> r) | (n << (64 - r));
+}
+
+// Implementation of Dragonbox algorithm: https://github.com/jk-jeon/dragonbox.
+namespace dragonbox {
+// Computes upper 64 bits of multiplication of a 32-bit unsigned integer and a
+// 64-bit unsigned integer.
+inline uint64_t umul96_upper64(uint32_t x, uint64_t y) noexcept {
+ return umul128_upper64(static_cast<uint64_t>(x) << 32, y);
+}
+
+// Computes lower 128 bits of multiplication of a 64-bit unsigned integer and a
+// 128-bit unsigned integer.
+inline uint128_fallback umul192_lower128(uint64_t x,
+ uint128_fallback y) noexcept {
+ uint64_t high = x * y.high();
+ uint128_fallback high_low = umul128(x, y.low());
+ return {high + high_low.high(), high_low.low()};
+}
+
+// Computes lower 64 bits of multiplication of a 32-bit unsigned integer and a
+// 64-bit unsigned integer.
+inline uint64_t umul96_lower64(uint32_t x, uint64_t y) noexcept {
+ return x * y;
+}
+
+// Various fast log computations.
+inline int floor_log10_pow2_minus_log10_4_over_3(int e) noexcept {
+ FMT_ASSERT(e <= 2936 && e >= -2985, "too large exponent");
+ return (e * 631305 - 261663) >> 21;
+}
+
+FMT_INLINE_VARIABLE constexpr struct {
+ uint32_t divisor;
+ int shift_amount;
+} div_small_pow10_infos[] = {{10, 16}, {100, 16}};
+
+// Replaces n by floor(n / pow(10, N)) returning true if and only if n is
+// divisible by pow(10, N).
+// Precondition: n <= pow(10, N + 1).
+template <int N>
+bool check_divisibility_and_divide_by_pow10(uint32_t& n) noexcept {
+ // The numbers below are chosen such that:
+ // 1. floor(n/d) = floor(nm / 2^k) where d=10 or d=100,
+ // 2. nm mod 2^k < m if and only if n is divisible by d,
+ // where m is magic_number, k is shift_amount
+ // and d is divisor.
+ //
+ // Item 1 is a common technique of replacing division by a constant with
+ // multiplication, see e.g. "Division by Invariant Integers Using
+ // Multiplication" by Granlund and Montgomery (1994). magic_number (m) is set
+ // to ceil(2^k/d) for large enough k.
+ // The idea for item 2 originates from Schubfach.
+ constexpr auto info = div_small_pow10_infos[N - 1];
+ FMT_ASSERT(n <= info.divisor * 10, "n is too large");
+ constexpr uint32_t magic_number =
+ (1u << info.shift_amount) / info.divisor + 1;
+ n *= magic_number;
+ const uint32_t comparison_mask = (1u << info.shift_amount) - 1;
+ bool result = (n & comparison_mask) < magic_number;
+ n >>= info.shift_amount;
+ return result;
+}
+
+// Computes floor(n / pow(10, N)) for small n and N.
+// Precondition: n <= pow(10, N + 1).
+template <int N> uint32_t small_division_by_pow10(uint32_t n) noexcept {
+ constexpr auto info = div_small_pow10_infos[N - 1];
+ FMT_ASSERT(n <= info.divisor * 10, "n is too large");
+ constexpr uint32_t magic_number =
+ (1u << info.shift_amount) / info.divisor + 1;
+ return (n * magic_number) >> info.shift_amount;
+}
+
+// Computes floor(n / 10^(kappa + 1)) (float)
+inline uint32_t divide_by_10_to_kappa_plus_1(uint32_t n) noexcept {
+ // 1374389535 = ceil(2^37/100)
+ return static_cast<uint32_t>((static_cast<uint64_t>(n) * 1374389535) >> 37);
+}
+// Computes floor(n / 10^(kappa + 1)) (double)
+inline uint64_t divide_by_10_to_kappa_plus_1(uint64_t n) noexcept {
+ // 2361183241434822607 = ceil(2^(64+7)/1000)
+ return umul128_upper64(n, 2361183241434822607ull) >> 7;
+}
+
+// Various subroutines using pow10 cache
+template <typename T> struct cache_accessor;
+
+template <> struct cache_accessor<float> {
+ using carrier_uint = float_info<float>::carrier_uint;
+ using cache_entry_type = uint64_t;
+
+ static uint64_t get_cached_power(int k) noexcept {
+ FMT_ASSERT(k >= float_info<float>::min_k && k <= float_info<float>::max_k,
+ "k is out of range");
+ static constexpr const uint64_t pow10_significands[] = {
+ 0x81ceb32c4b43fcf5, 0xa2425ff75e14fc32, 0xcad2f7f5359a3b3f,
+ 0xfd87b5f28300ca0e, 0x9e74d1b791e07e49, 0xc612062576589ddb,
+ 0xf79687aed3eec552, 0x9abe14cd44753b53, 0xc16d9a0095928a28,
+ 0xf1c90080baf72cb2, 0x971da05074da7bef, 0xbce5086492111aeb,
+ 0xec1e4a7db69561a6, 0x9392ee8e921d5d08, 0xb877aa3236a4b44a,
+ 0xe69594bec44de15c, 0x901d7cf73ab0acda, 0xb424dc35095cd810,
+ 0xe12e13424bb40e14, 0x8cbccc096f5088cc, 0xafebff0bcb24aaff,
+ 0xdbe6fecebdedd5bf, 0x89705f4136b4a598, 0xabcc77118461cefd,
+ 0xd6bf94d5e57a42bd, 0x8637bd05af6c69b6, 0xa7c5ac471b478424,
+ 0xd1b71758e219652c, 0x83126e978d4fdf3c, 0xa3d70a3d70a3d70b,
+ 0xcccccccccccccccd, 0x8000000000000000, 0xa000000000000000,
+ 0xc800000000000000, 0xfa00000000000000, 0x9c40000000000000,
+ 0xc350000000000000, 0xf424000000000000, 0x9896800000000000,
+ 0xbebc200000000000, 0xee6b280000000000, 0x9502f90000000000,
+ 0xba43b74000000000, 0xe8d4a51000000000, 0x9184e72a00000000,
+ 0xb5e620f480000000, 0xe35fa931a0000000, 0x8e1bc9bf04000000,
+ 0xb1a2bc2ec5000000, 0xde0b6b3a76400000, 0x8ac7230489e80000,
+ 0xad78ebc5ac620000, 0xd8d726b7177a8000, 0x878678326eac9000,
+ 0xa968163f0a57b400, 0xd3c21bcecceda100, 0x84595161401484a0,
+ 0xa56fa5b99019a5c8, 0xcecb8f27f4200f3a, 0x813f3978f8940985,
+ 0xa18f07d736b90be6, 0xc9f2c9cd04674edf, 0xfc6f7c4045812297,
+ 0x9dc5ada82b70b59e, 0xc5371912364ce306, 0xf684df56c3e01bc7,
+ 0x9a130b963a6c115d, 0xc097ce7bc90715b4, 0xf0bdc21abb48db21,
+ 0x96769950b50d88f5, 0xbc143fa4e250eb32, 0xeb194f8e1ae525fe,
+ 0x92efd1b8d0cf37bf, 0xb7abc627050305ae, 0xe596b7b0c643c71a,
+ 0x8f7e32ce7bea5c70, 0xb35dbf821ae4f38c, 0xe0352f62a19e306f};
+ return pow10_significands[k - float_info<float>::min_k];
+ }
+
+ struct compute_mul_result {
+ carrier_uint result;
+ bool is_integer;
+ };
+ struct compute_mul_parity_result {
+ bool parity;
+ bool is_integer;
+ };
+
+ static compute_mul_result compute_mul(
+ carrier_uint u, const cache_entry_type& cache) noexcept {
+ auto r = umul96_upper64(u, cache);
+ return {static_cast<carrier_uint>(r >> 32),
+ static_cast<carrier_uint>(r) == 0};
+ }
+
+ static uint32_t compute_delta(const cache_entry_type& cache,
+ int beta) noexcept {
+ return static_cast<uint32_t>(cache >> (64 - 1 - beta));
+ }
+
+ static compute_mul_parity_result compute_mul_parity(
+ carrier_uint two_f, const cache_entry_type& cache, int beta) noexcept {
+ FMT_ASSERT(beta >= 1, "");
+ FMT_ASSERT(beta < 64, "");
+
+ auto r = umul96_lower64(two_f, cache);
+ return {((r >> (64 - beta)) & 1) != 0,
+ static_cast<uint32_t>(r >> (32 - beta)) == 0};
+ }
+
+ static carrier_uint compute_left_endpoint_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept {
+ return static_cast<carrier_uint>(
+ (cache - (cache >> (num_significand_bits<float>() + 2))) >>
+ (64 - num_significand_bits<float>() - 1 - beta));
+ }
+
+ static carrier_uint compute_right_endpoint_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept {
+ return static_cast<carrier_uint>(
+ (cache + (cache >> (num_significand_bits<float>() + 1))) >>
+ (64 - num_significand_bits<float>() - 1 - beta));
+ }
+
+ static carrier_uint compute_round_up_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept {
+ return (static_cast<carrier_uint>(
+ cache >> (64 - num_significand_bits<float>() - 2 - beta)) +
+ 1) /
+ 2;
+ }
+};
+
+template <> struct cache_accessor<double> {
+ using carrier_uint = float_info<double>::carrier_uint;
+ using cache_entry_type = uint128_fallback;
+
+ static uint128_fallback get_cached_power(int k) noexcept {
+ FMT_ASSERT(k >= float_info<double>::min_k && k <= float_info<double>::max_k,
+ "k is out of range");
+
+ static constexpr const uint128_fallback pow10_significands[] = {
+#if FMT_USE_FULL_CACHE_DRAGONBOX
+ {0xff77b1fcbebcdc4f, 0x25e8e89c13bb0f7b},
+ {0x9faacf3df73609b1, 0x77b191618c54e9ad},
+ {0xc795830d75038c1d, 0xd59df5b9ef6a2418},
+ {0xf97ae3d0d2446f25, 0x4b0573286b44ad1e},
+ {0x9becce62836ac577, 0x4ee367f9430aec33},
+ {0xc2e801fb244576d5, 0x229c41f793cda740},
+ {0xf3a20279ed56d48a, 0x6b43527578c11110},
+ {0x9845418c345644d6, 0x830a13896b78aaaa},
+ {0xbe5691ef416bd60c, 0x23cc986bc656d554},
+ {0xedec366b11c6cb8f, 0x2cbfbe86b7ec8aa9},
+ {0x94b3a202eb1c3f39, 0x7bf7d71432f3d6aa},
+ {0xb9e08a83a5e34f07, 0xdaf5ccd93fb0cc54},
+ {0xe858ad248f5c22c9, 0xd1b3400f8f9cff69},
+ {0x91376c36d99995be, 0x23100809b9c21fa2},
+ {0xb58547448ffffb2d, 0xabd40a0c2832a78b},
+ {0xe2e69915b3fff9f9, 0x16c90c8f323f516d},
+ {0x8dd01fad907ffc3b, 0xae3da7d97f6792e4},
+ {0xb1442798f49ffb4a, 0x99cd11cfdf41779d},
+ {0xdd95317f31c7fa1d, 0x40405643d711d584},
+ {0x8a7d3eef7f1cfc52, 0x482835ea666b2573},
+ {0xad1c8eab5ee43b66, 0xda3243650005eed0},
+ {0xd863b256369d4a40, 0x90bed43e40076a83},
+ {0x873e4f75e2224e68, 0x5a7744a6e804a292},
+ {0xa90de3535aaae202, 0x711515d0a205cb37},
+ {0xd3515c2831559a83, 0x0d5a5b44ca873e04},
+ {0x8412d9991ed58091, 0xe858790afe9486c3},
+ {0xa5178fff668ae0b6, 0x626e974dbe39a873},
+ {0xce5d73ff402d98e3, 0xfb0a3d212dc81290},
+ {0x80fa687f881c7f8e, 0x7ce66634bc9d0b9a},
+ {0xa139029f6a239f72, 0x1c1fffc1ebc44e81},
+ {0xc987434744ac874e, 0xa327ffb266b56221},
+ {0xfbe9141915d7a922, 0x4bf1ff9f0062baa9},
+ {0x9d71ac8fada6c9b5, 0x6f773fc3603db4aa},
+ {0xc4ce17b399107c22, 0xcb550fb4384d21d4},
+ {0xf6019da07f549b2b, 0x7e2a53a146606a49},
+ {0x99c102844f94e0fb, 0x2eda7444cbfc426e},
+ {0xc0314325637a1939, 0xfa911155fefb5309},
+ {0xf03d93eebc589f88, 0x793555ab7eba27cb},
+ {0x96267c7535b763b5, 0x4bc1558b2f3458df},
+ {0xbbb01b9283253ca2, 0x9eb1aaedfb016f17},
+ {0xea9c227723ee8bcb, 0x465e15a979c1cadd},
+ {0x92a1958a7675175f, 0x0bfacd89ec191eca},
+ {0xb749faed14125d36, 0xcef980ec671f667c},
+ {0xe51c79a85916f484, 0x82b7e12780e7401b},
+ {0x8f31cc0937ae58d2, 0xd1b2ecb8b0908811},
+ {0xb2fe3f0b8599ef07, 0x861fa7e6dcb4aa16},
+ {0xdfbdcece67006ac9, 0x67a791e093e1d49b},
+ {0x8bd6a141006042bd, 0xe0c8bb2c5c6d24e1},
+ {0xaecc49914078536d, 0x58fae9f773886e19},
+ {0xda7f5bf590966848, 0xaf39a475506a899f},
+ {0x888f99797a5e012d, 0x6d8406c952429604},
+ {0xaab37fd7d8f58178, 0xc8e5087ba6d33b84},
+ {0xd5605fcdcf32e1d6, 0xfb1e4a9a90880a65},
+ {0x855c3be0a17fcd26, 0x5cf2eea09a550680},
+ {0xa6b34ad8c9dfc06f, 0xf42faa48c0ea481f},
+ {0xd0601d8efc57b08b, 0xf13b94daf124da27},
+ {0x823c12795db6ce57, 0x76c53d08d6b70859},
+ {0xa2cb1717b52481ed, 0x54768c4b0c64ca6f},
+ {0xcb7ddcdda26da268, 0xa9942f5dcf7dfd0a},
+ {0xfe5d54150b090b02, 0xd3f93b35435d7c4d},
+ {0x9efa548d26e5a6e1, 0xc47bc5014a1a6db0},
+ {0xc6b8e9b0709f109a, 0x359ab6419ca1091c},
+ {0xf867241c8cc6d4c0, 0xc30163d203c94b63},
+ {0x9b407691d7fc44f8, 0x79e0de63425dcf1e},
+ {0xc21094364dfb5636, 0x985915fc12f542e5},
+ {0xf294b943e17a2bc4, 0x3e6f5b7b17b2939e},
+ {0x979cf3ca6cec5b5a, 0xa705992ceecf9c43},
+ {0xbd8430bd08277231, 0x50c6ff782a838354},
+ {0xece53cec4a314ebd, 0xa4f8bf5635246429},
+ {0x940f4613ae5ed136, 0x871b7795e136be9a},
+ {0xb913179899f68584, 0x28e2557b59846e40},
+ {0xe757dd7ec07426e5, 0x331aeada2fe589d0},
+ {0x9096ea6f3848984f, 0x3ff0d2c85def7622},
+ {0xb4bca50b065abe63, 0x0fed077a756b53aa},
+ {0xe1ebce4dc7f16dfb, 0xd3e8495912c62895},
+ {0x8d3360f09cf6e4bd, 0x64712dd7abbbd95d},
+ {0xb080392cc4349dec, 0xbd8d794d96aacfb4},
+ {0xdca04777f541c567, 0xecf0d7a0fc5583a1},
+ {0x89e42caaf9491b60, 0xf41686c49db57245},
+ {0xac5d37d5b79b6239, 0x311c2875c522ced6},
+ {0xd77485cb25823ac7, 0x7d633293366b828c},
+ {0x86a8d39ef77164bc, 0xae5dff9c02033198},
+ {0xa8530886b54dbdeb, 0xd9f57f830283fdfd},
+ {0xd267caa862a12d66, 0xd072df63c324fd7c},
+ {0x8380dea93da4bc60, 0x4247cb9e59f71e6e},
+ {0xa46116538d0deb78, 0x52d9be85f074e609},
+ {0xcd795be870516656, 0x67902e276c921f8c},
+ {0x806bd9714632dff6, 0x00ba1cd8a3db53b7},
+ {0xa086cfcd97bf97f3, 0x80e8a40eccd228a5},
+ {0xc8a883c0fdaf7df0, 0x6122cd128006b2ce},
+ {0xfad2a4b13d1b5d6c, 0x796b805720085f82},
+ {0x9cc3a6eec6311a63, 0xcbe3303674053bb1},
+ {0xc3f490aa77bd60fc, 0xbedbfc4411068a9d},
+ {0xf4f1b4d515acb93b, 0xee92fb5515482d45},
+ {0x991711052d8bf3c5, 0x751bdd152d4d1c4b},
+ {0xbf5cd54678eef0b6, 0xd262d45a78a0635e},
+ {0xef340a98172aace4, 0x86fb897116c87c35},
+ {0x9580869f0e7aac0e, 0xd45d35e6ae3d4da1},
+ {0xbae0a846d2195712, 0x8974836059cca10a},
+ {0xe998d258869facd7, 0x2bd1a438703fc94c},
+ {0x91ff83775423cc06, 0x7b6306a34627ddd0},
+ {0xb67f6455292cbf08, 0x1a3bc84c17b1d543},
+ {0xe41f3d6a7377eeca, 0x20caba5f1d9e4a94},
+ {0x8e938662882af53e, 0x547eb47b7282ee9d},
+ {0xb23867fb2a35b28d, 0xe99e619a4f23aa44},
+ {0xdec681f9f4c31f31, 0x6405fa00e2ec94d5},
+ {0x8b3c113c38f9f37e, 0xde83bc408dd3dd05},
+ {0xae0b158b4738705e, 0x9624ab50b148d446},
+ {0xd98ddaee19068c76, 0x3badd624dd9b0958},
+ {0x87f8a8d4cfa417c9, 0xe54ca5d70a80e5d7},
+ {0xa9f6d30a038d1dbc, 0x5e9fcf4ccd211f4d},
+ {0xd47487cc8470652b, 0x7647c32000696720},
+ {0x84c8d4dfd2c63f3b, 0x29ecd9f40041e074},
+ {0xa5fb0a17c777cf09, 0xf468107100525891},
+ {0xcf79cc9db955c2cc, 0x7182148d4066eeb5},
+ {0x81ac1fe293d599bf, 0xc6f14cd848405531},
+ {0xa21727db38cb002f, 0xb8ada00e5a506a7d},
+ {0xca9cf1d206fdc03b, 0xa6d90811f0e4851d},
+ {0xfd442e4688bd304a, 0x908f4a166d1da664},
+ {0x9e4a9cec15763e2e, 0x9a598e4e043287ff},
+ {0xc5dd44271ad3cdba, 0x40eff1e1853f29fe},
+ {0xf7549530e188c128, 0xd12bee59e68ef47d},
+ {0x9a94dd3e8cf578b9, 0x82bb74f8301958cf},
+ {0xc13a148e3032d6e7, 0xe36a52363c1faf02},
+ {0xf18899b1bc3f8ca1, 0xdc44e6c3cb279ac2},
+ {0x96f5600f15a7b7e5, 0x29ab103a5ef8c0ba},
+ {0xbcb2b812db11a5de, 0x7415d448f6b6f0e8},
+ {0xebdf661791d60f56, 0x111b495b3464ad22},
+ {0x936b9fcebb25c995, 0xcab10dd900beec35},
+ {0xb84687c269ef3bfb, 0x3d5d514f40eea743},
+ {0xe65829b3046b0afa, 0x0cb4a5a3112a5113},
+ {0x8ff71a0fe2c2e6dc, 0x47f0e785eaba72ac},
+ {0xb3f4e093db73a093, 0x59ed216765690f57},
+ {0xe0f218b8d25088b8, 0x306869c13ec3532d},
+ {0x8c974f7383725573, 0x1e414218c73a13fc},
+ {0xafbd2350644eeacf, 0xe5d1929ef90898fb},
+ {0xdbac6c247d62a583, 0xdf45f746b74abf3a},
+ {0x894bc396ce5da772, 0x6b8bba8c328eb784},
+ {0xab9eb47c81f5114f, 0x066ea92f3f326565},
+ {0xd686619ba27255a2, 0xc80a537b0efefebe},
+ {0x8613fd0145877585, 0xbd06742ce95f5f37},
+ {0xa798fc4196e952e7, 0x2c48113823b73705},
+ {0xd17f3b51fca3a7a0, 0xf75a15862ca504c6},
+ {0x82ef85133de648c4, 0x9a984d73dbe722fc},
+ {0xa3ab66580d5fdaf5, 0xc13e60d0d2e0ebbb},
+ {0xcc963fee10b7d1b3, 0x318df905079926a9},
+ {0xffbbcfe994e5c61f, 0xfdf17746497f7053},
+ {0x9fd561f1fd0f9bd3, 0xfeb6ea8bedefa634},
+ {0xc7caba6e7c5382c8, 0xfe64a52ee96b8fc1},
+ {0xf9bd690a1b68637b, 0x3dfdce7aa3c673b1},
+ {0x9c1661a651213e2d, 0x06bea10ca65c084f},
+ {0xc31bfa0fe5698db8, 0x486e494fcff30a63},
+ {0xf3e2f893dec3f126, 0x5a89dba3c3efccfb},
+ {0x986ddb5c6b3a76b7, 0xf89629465a75e01d},
+ {0xbe89523386091465, 0xf6bbb397f1135824},
+ {0xee2ba6c0678b597f, 0x746aa07ded582e2d},
+ {0x94db483840b717ef, 0xa8c2a44eb4571cdd},
+ {0xba121a4650e4ddeb, 0x92f34d62616ce414},
+ {0xe896a0d7e51e1566, 0x77b020baf9c81d18},
+ {0x915e2486ef32cd60, 0x0ace1474dc1d122f},
+ {0xb5b5ada8aaff80b8, 0x0d819992132456bb},
+ {0xe3231912d5bf60e6, 0x10e1fff697ed6c6a},
+ {0x8df5efabc5979c8f, 0xca8d3ffa1ef463c2},
+ {0xb1736b96b6fd83b3, 0xbd308ff8a6b17cb3},
+ {0xddd0467c64bce4a0, 0xac7cb3f6d05ddbdf},
+ {0x8aa22c0dbef60ee4, 0x6bcdf07a423aa96c},
+ {0xad4ab7112eb3929d, 0x86c16c98d2c953c7},
+ {0xd89d64d57a607744, 0xe871c7bf077ba8b8},
+ {0x87625f056c7c4a8b, 0x11471cd764ad4973},
+ {0xa93af6c6c79b5d2d, 0xd598e40d3dd89bd0},
+ {0xd389b47879823479, 0x4aff1d108d4ec2c4},
+ {0x843610cb4bf160cb, 0xcedf722a585139bb},
+ {0xa54394fe1eedb8fe, 0xc2974eb4ee658829},
+ {0xce947a3da6a9273e, 0x733d226229feea33},
+ {0x811ccc668829b887, 0x0806357d5a3f5260},
+ {0xa163ff802a3426a8, 0xca07c2dcb0cf26f8},
+ {0xc9bcff6034c13052, 0xfc89b393dd02f0b6},
+ {0xfc2c3f3841f17c67, 0xbbac2078d443ace3},
+ {0x9d9ba7832936edc0, 0xd54b944b84aa4c0e},
+ {0xc5029163f384a931, 0x0a9e795e65d4df12},
+ {0xf64335bcf065d37d, 0x4d4617b5ff4a16d6},
+ {0x99ea0196163fa42e, 0x504bced1bf8e4e46},
+ {0xc06481fb9bcf8d39, 0xe45ec2862f71e1d7},
+ {0xf07da27a82c37088, 0x5d767327bb4e5a4d},
+ {0x964e858c91ba2655, 0x3a6a07f8d510f870},
+ {0xbbe226efb628afea, 0x890489f70a55368c},
+ {0xeadab0aba3b2dbe5, 0x2b45ac74ccea842f},
+ {0x92c8ae6b464fc96f, 0x3b0b8bc90012929e},
+ {0xb77ada0617e3bbcb, 0x09ce6ebb40173745},
+ {0xe55990879ddcaabd, 0xcc420a6a101d0516},
+ {0x8f57fa54c2a9eab6, 0x9fa946824a12232e},
+ {0xb32df8e9f3546564, 0x47939822dc96abfa},
+ {0xdff9772470297ebd, 0x59787e2b93bc56f8},
+ {0x8bfbea76c619ef36, 0x57eb4edb3c55b65b},
+ {0xaefae51477a06b03, 0xede622920b6b23f2},
+ {0xdab99e59958885c4, 0xe95fab368e45ecee},
+ {0x88b402f7fd75539b, 0x11dbcb0218ebb415},
+ {0xaae103b5fcd2a881, 0xd652bdc29f26a11a},
+ {0xd59944a37c0752a2, 0x4be76d3346f04960},
+ {0x857fcae62d8493a5, 0x6f70a4400c562ddc},
+ {0xa6dfbd9fb8e5b88e, 0xcb4ccd500f6bb953},
+ {0xd097ad07a71f26b2, 0x7e2000a41346a7a8},
+ {0x825ecc24c873782f, 0x8ed400668c0c28c9},
+ {0xa2f67f2dfa90563b, 0x728900802f0f32fb},
+ {0xcbb41ef979346bca, 0x4f2b40a03ad2ffba},
+ {0xfea126b7d78186bc, 0xe2f610c84987bfa9},
+ {0x9f24b832e6b0f436, 0x0dd9ca7d2df4d7ca},
+ {0xc6ede63fa05d3143, 0x91503d1c79720dbc},
+ {0xf8a95fcf88747d94, 0x75a44c6397ce912b},
+ {0x9b69dbe1b548ce7c, 0xc986afbe3ee11abb},
+ {0xc24452da229b021b, 0xfbe85badce996169},
+ {0xf2d56790ab41c2a2, 0xfae27299423fb9c4},
+ {0x97c560ba6b0919a5, 0xdccd879fc967d41b},
+ {0xbdb6b8e905cb600f, 0x5400e987bbc1c921},
+ {0xed246723473e3813, 0x290123e9aab23b69},
+ {0x9436c0760c86e30b, 0xf9a0b6720aaf6522},
+ {0xb94470938fa89bce, 0xf808e40e8d5b3e6a},
+ {0xe7958cb87392c2c2, 0xb60b1d1230b20e05},
+ {0x90bd77f3483bb9b9, 0xb1c6f22b5e6f48c3},
+ {0xb4ecd5f01a4aa828, 0x1e38aeb6360b1af4},
+ {0xe2280b6c20dd5232, 0x25c6da63c38de1b1},
+ {0x8d590723948a535f, 0x579c487e5a38ad0f},
+ {0xb0af48ec79ace837, 0x2d835a9df0c6d852},
+ {0xdcdb1b2798182244, 0xf8e431456cf88e66},
+ {0x8a08f0f8bf0f156b, 0x1b8e9ecb641b5900},
+ {0xac8b2d36eed2dac5, 0xe272467e3d222f40},
+ {0xd7adf884aa879177, 0x5b0ed81dcc6abb10},
+ {0x86ccbb52ea94baea, 0x98e947129fc2b4ea},
+ {0xa87fea27a539e9a5, 0x3f2398d747b36225},
+ {0xd29fe4b18e88640e, 0x8eec7f0d19a03aae},
+ {0x83a3eeeef9153e89, 0x1953cf68300424ad},
+ {0xa48ceaaab75a8e2b, 0x5fa8c3423c052dd8},
+ {0xcdb02555653131b6, 0x3792f412cb06794e},
+ {0x808e17555f3ebf11, 0xe2bbd88bbee40bd1},
+ {0xa0b19d2ab70e6ed6, 0x5b6aceaeae9d0ec5},
+ {0xc8de047564d20a8b, 0xf245825a5a445276},
+ {0xfb158592be068d2e, 0xeed6e2f0f0d56713},
+ {0x9ced737bb6c4183d, 0x55464dd69685606c},
+ {0xc428d05aa4751e4c, 0xaa97e14c3c26b887},
+ {0xf53304714d9265df, 0xd53dd99f4b3066a9},
+ {0x993fe2c6d07b7fab, 0xe546a8038efe402a},
+ {0xbf8fdb78849a5f96, 0xde98520472bdd034},
+ {0xef73d256a5c0f77c, 0x963e66858f6d4441},
+ {0x95a8637627989aad, 0xdde7001379a44aa9},
+ {0xbb127c53b17ec159, 0x5560c018580d5d53},
+ {0xe9d71b689dde71af, 0xaab8f01e6e10b4a7},
+ {0x9226712162ab070d, 0xcab3961304ca70e9},
+ {0xb6b00d69bb55c8d1, 0x3d607b97c5fd0d23},
+ {0xe45c10c42a2b3b05, 0x8cb89a7db77c506b},
+ {0x8eb98a7a9a5b04e3, 0x77f3608e92adb243},
+ {0xb267ed1940f1c61c, 0x55f038b237591ed4},
+ {0xdf01e85f912e37a3, 0x6b6c46dec52f6689},
+ {0x8b61313bbabce2c6, 0x2323ac4b3b3da016},
+ {0xae397d8aa96c1b77, 0xabec975e0a0d081b},
+ {0xd9c7dced53c72255, 0x96e7bd358c904a22},
+ {0x881cea14545c7575, 0x7e50d64177da2e55},
+ {0xaa242499697392d2, 0xdde50bd1d5d0b9ea},
+ {0xd4ad2dbfc3d07787, 0x955e4ec64b44e865},
+ {0x84ec3c97da624ab4, 0xbd5af13bef0b113f},
+ {0xa6274bbdd0fadd61, 0xecb1ad8aeacdd58f},
+ {0xcfb11ead453994ba, 0x67de18eda5814af3},
+ {0x81ceb32c4b43fcf4, 0x80eacf948770ced8},
+ {0xa2425ff75e14fc31, 0xa1258379a94d028e},
+ {0xcad2f7f5359a3b3e, 0x096ee45813a04331},
+ {0xfd87b5f28300ca0d, 0x8bca9d6e188853fd},
+ {0x9e74d1b791e07e48, 0x775ea264cf55347e},
+ {0xc612062576589dda, 0x95364afe032a819e},
+ {0xf79687aed3eec551, 0x3a83ddbd83f52205},
+ {0x9abe14cd44753b52, 0xc4926a9672793543},
+ {0xc16d9a0095928a27, 0x75b7053c0f178294},
+ {0xf1c90080baf72cb1, 0x5324c68b12dd6339},
+ {0x971da05074da7bee, 0xd3f6fc16ebca5e04},
+ {0xbce5086492111aea, 0x88f4bb1ca6bcf585},
+ {0xec1e4a7db69561a5, 0x2b31e9e3d06c32e6},
+ {0x9392ee8e921d5d07, 0x3aff322e62439fd0},
+ {0xb877aa3236a4b449, 0x09befeb9fad487c3},
+ {0xe69594bec44de15b, 0x4c2ebe687989a9b4},
+ {0x901d7cf73ab0acd9, 0x0f9d37014bf60a11},
+ {0xb424dc35095cd80f, 0x538484c19ef38c95},
+ {0xe12e13424bb40e13, 0x2865a5f206b06fba},
+ {0x8cbccc096f5088cb, 0xf93f87b7442e45d4},
+ {0xafebff0bcb24aafe, 0xf78f69a51539d749},
+ {0xdbe6fecebdedd5be, 0xb573440e5a884d1c},
+ {0x89705f4136b4a597, 0x31680a88f8953031},
+ {0xabcc77118461cefc, 0xfdc20d2b36ba7c3e},
+ {0xd6bf94d5e57a42bc, 0x3d32907604691b4d},
+ {0x8637bd05af6c69b5, 0xa63f9a49c2c1b110},
+ {0xa7c5ac471b478423, 0x0fcf80dc33721d54},
+ {0xd1b71758e219652b, 0xd3c36113404ea4a9},
+ {0x83126e978d4fdf3b, 0x645a1cac083126ea},
+ {0xa3d70a3d70a3d70a, 0x3d70a3d70a3d70a4},
+ {0xcccccccccccccccc, 0xcccccccccccccccd},
+ {0x8000000000000000, 0x0000000000000000},
+ {0xa000000000000000, 0x0000000000000000},
+ {0xc800000000000000, 0x0000000000000000},
+ {0xfa00000000000000, 0x0000000000000000},
+ {0x9c40000000000000, 0x0000000000000000},
+ {0xc350000000000000, 0x0000000000000000},
+ {0xf424000000000000, 0x0000000000000000},
+ {0x9896800000000000, 0x0000000000000000},
+ {0xbebc200000000000, 0x0000000000000000},
+ {0xee6b280000000000, 0x0000000000000000},
+ {0x9502f90000000000, 0x0000000000000000},
+ {0xba43b74000000000, 0x0000000000000000},
+ {0xe8d4a51000000000, 0x0000000000000000},
+ {0x9184e72a00000000, 0x0000000000000000},
+ {0xb5e620f480000000, 0x0000000000000000},
+ {0xe35fa931a0000000, 0x0000000000000000},
+ {0x8e1bc9bf04000000, 0x0000000000000000},
+ {0xb1a2bc2ec5000000, 0x0000000000000000},
+ {0xde0b6b3a76400000, 0x0000000000000000},
+ {0x8ac7230489e80000, 0x0000000000000000},
+ {0xad78ebc5ac620000, 0x0000000000000000},
+ {0xd8d726b7177a8000, 0x0000000000000000},
+ {0x878678326eac9000, 0x0000000000000000},
+ {0xa968163f0a57b400, 0x0000000000000000},
+ {0xd3c21bcecceda100, 0x0000000000000000},
+ {0x84595161401484a0, 0x0000000000000000},
+ {0xa56fa5b99019a5c8, 0x0000000000000000},
+ {0xcecb8f27f4200f3a, 0x0000000000000000},
+ {0x813f3978f8940984, 0x4000000000000000},
+ {0xa18f07d736b90be5, 0x5000000000000000},
+ {0xc9f2c9cd04674ede, 0xa400000000000000},
+ {0xfc6f7c4045812296, 0x4d00000000000000},
+ {0x9dc5ada82b70b59d, 0xf020000000000000},
+ {0xc5371912364ce305, 0x6c28000000000000},
+ {0xf684df56c3e01bc6, 0xc732000000000000},
+ {0x9a130b963a6c115c, 0x3c7f400000000000},
+ {0xc097ce7bc90715b3, 0x4b9f100000000000},
+ {0xf0bdc21abb48db20, 0x1e86d40000000000},
+ {0x96769950b50d88f4, 0x1314448000000000},
+ {0xbc143fa4e250eb31, 0x17d955a000000000},
+ {0xeb194f8e1ae525fd, 0x5dcfab0800000000},
+ {0x92efd1b8d0cf37be, 0x5aa1cae500000000},
+ {0xb7abc627050305ad, 0xf14a3d9e40000000},
+ {0xe596b7b0c643c719, 0x6d9ccd05d0000000},
+ {0x8f7e32ce7bea5c6f, 0xe4820023a2000000},
+ {0xb35dbf821ae4f38b, 0xdda2802c8a800000},
+ {0xe0352f62a19e306e, 0xd50b2037ad200000},
+ {0x8c213d9da502de45, 0x4526f422cc340000},
+ {0xaf298d050e4395d6, 0x9670b12b7f410000},
+ {0xdaf3f04651d47b4c, 0x3c0cdd765f114000},
+ {0x88d8762bf324cd0f, 0xa5880a69fb6ac800},
+ {0xab0e93b6efee0053, 0x8eea0d047a457a00},
+ {0xd5d238a4abe98068, 0x72a4904598d6d880},
+ {0x85a36366eb71f041, 0x47a6da2b7f864750},
+ {0xa70c3c40a64e6c51, 0x999090b65f67d924},
+ {0xd0cf4b50cfe20765, 0xfff4b4e3f741cf6d},
+ {0x82818f1281ed449f, 0xbff8f10e7a8921a5},
+ {0xa321f2d7226895c7, 0xaff72d52192b6a0e},
+ {0xcbea6f8ceb02bb39, 0x9bf4f8a69f764491},
+ {0xfee50b7025c36a08, 0x02f236d04753d5b5},
+ {0x9f4f2726179a2245, 0x01d762422c946591},
+ {0xc722f0ef9d80aad6, 0x424d3ad2b7b97ef6},
+ {0xf8ebad2b84e0d58b, 0xd2e0898765a7deb3},
+ {0x9b934c3b330c8577, 0x63cc55f49f88eb30},
+ {0xc2781f49ffcfa6d5, 0x3cbf6b71c76b25fc},
+ {0xf316271c7fc3908a, 0x8bef464e3945ef7b},
+ {0x97edd871cfda3a56, 0x97758bf0e3cbb5ad},
+ {0xbde94e8e43d0c8ec, 0x3d52eeed1cbea318},
+ {0xed63a231d4c4fb27, 0x4ca7aaa863ee4bde},
+ {0x945e455f24fb1cf8, 0x8fe8caa93e74ef6b},
+ {0xb975d6b6ee39e436, 0xb3e2fd538e122b45},
+ {0xe7d34c64a9c85d44, 0x60dbbca87196b617},
+ {0x90e40fbeea1d3a4a, 0xbc8955e946fe31ce},
+ {0xb51d13aea4a488dd, 0x6babab6398bdbe42},
+ {0xe264589a4dcdab14, 0xc696963c7eed2dd2},
+ {0x8d7eb76070a08aec, 0xfc1e1de5cf543ca3},
+ {0xb0de65388cc8ada8, 0x3b25a55f43294bcc},
+ {0xdd15fe86affad912, 0x49ef0eb713f39ebf},
+ {0x8a2dbf142dfcc7ab, 0x6e3569326c784338},
+ {0xacb92ed9397bf996, 0x49c2c37f07965405},
+ {0xd7e77a8f87daf7fb, 0xdc33745ec97be907},
+ {0x86f0ac99b4e8dafd, 0x69a028bb3ded71a4},
+ {0xa8acd7c0222311bc, 0xc40832ea0d68ce0d},
+ {0xd2d80db02aabd62b, 0xf50a3fa490c30191},
+ {0x83c7088e1aab65db, 0x792667c6da79e0fb},
+ {0xa4b8cab1a1563f52, 0x577001b891185939},
+ {0xcde6fd5e09abcf26, 0xed4c0226b55e6f87},
+ {0x80b05e5ac60b6178, 0x544f8158315b05b5},
+ {0xa0dc75f1778e39d6, 0x696361ae3db1c722},
+ {0xc913936dd571c84c, 0x03bc3a19cd1e38ea},
+ {0xfb5878494ace3a5f, 0x04ab48a04065c724},
+ {0x9d174b2dcec0e47b, 0x62eb0d64283f9c77},
+ {0xc45d1df942711d9a, 0x3ba5d0bd324f8395},
+ {0xf5746577930d6500, 0xca8f44ec7ee3647a},
+ {0x9968bf6abbe85f20, 0x7e998b13cf4e1ecc},
+ {0xbfc2ef456ae276e8, 0x9e3fedd8c321a67f},
+ {0xefb3ab16c59b14a2, 0xc5cfe94ef3ea101f},
+ {0x95d04aee3b80ece5, 0xbba1f1d158724a13},
+ {0xbb445da9ca61281f, 0x2a8a6e45ae8edc98},
+ {0xea1575143cf97226, 0xf52d09d71a3293be},
+ {0x924d692ca61be758, 0x593c2626705f9c57},
+ {0xb6e0c377cfa2e12e, 0x6f8b2fb00c77836d},
+ {0xe498f455c38b997a, 0x0b6dfb9c0f956448},
+ {0x8edf98b59a373fec, 0x4724bd4189bd5ead},
+ {0xb2977ee300c50fe7, 0x58edec91ec2cb658},
+ {0xdf3d5e9bc0f653e1, 0x2f2967b66737e3ee},
+ {0x8b865b215899f46c, 0xbd79e0d20082ee75},
+ {0xae67f1e9aec07187, 0xecd8590680a3aa12},
+ {0xda01ee641a708de9, 0xe80e6f4820cc9496},
+ {0x884134fe908658b2, 0x3109058d147fdcde},
+ {0xaa51823e34a7eede, 0xbd4b46f0599fd416},
+ {0xd4e5e2cdc1d1ea96, 0x6c9e18ac7007c91b},
+ {0x850fadc09923329e, 0x03e2cf6bc604ddb1},
+ {0xa6539930bf6bff45, 0x84db8346b786151d},
+ {0xcfe87f7cef46ff16, 0xe612641865679a64},
+ {0x81f14fae158c5f6e, 0x4fcb7e8f3f60c07f},
+ {0xa26da3999aef7749, 0xe3be5e330f38f09e},
+ {0xcb090c8001ab551c, 0x5cadf5bfd3072cc6},
+ {0xfdcb4fa002162a63, 0x73d9732fc7c8f7f7},
+ {0x9e9f11c4014dda7e, 0x2867e7fddcdd9afb},
+ {0xc646d63501a1511d, 0xb281e1fd541501b9},
+ {0xf7d88bc24209a565, 0x1f225a7ca91a4227},
+ {0x9ae757596946075f, 0x3375788de9b06959},
+ {0xc1a12d2fc3978937, 0x0052d6b1641c83af},
+ {0xf209787bb47d6b84, 0xc0678c5dbd23a49b},
+ {0x9745eb4d50ce6332, 0xf840b7ba963646e1},
+ {0xbd176620a501fbff, 0xb650e5a93bc3d899},
+ {0xec5d3fa8ce427aff, 0xa3e51f138ab4cebf},
+ {0x93ba47c980e98cdf, 0xc66f336c36b10138},
+ {0xb8a8d9bbe123f017, 0xb80b0047445d4185},
+ {0xe6d3102ad96cec1d, 0xa60dc059157491e6},
+ {0x9043ea1ac7e41392, 0x87c89837ad68db30},
+ {0xb454e4a179dd1877, 0x29babe4598c311fc},
+ {0xe16a1dc9d8545e94, 0xf4296dd6fef3d67b},
+ {0x8ce2529e2734bb1d, 0x1899e4a65f58660d},
+ {0xb01ae745b101e9e4, 0x5ec05dcff72e7f90},
+ {0xdc21a1171d42645d, 0x76707543f4fa1f74},
+ {0x899504ae72497eba, 0x6a06494a791c53a9},
+ {0xabfa45da0edbde69, 0x0487db9d17636893},
+ {0xd6f8d7509292d603, 0x45a9d2845d3c42b7},
+ {0x865b86925b9bc5c2, 0x0b8a2392ba45a9b3},
+ {0xa7f26836f282b732, 0x8e6cac7768d7141f},
+ {0xd1ef0244af2364ff, 0x3207d795430cd927},
+ {0x8335616aed761f1f, 0x7f44e6bd49e807b9},
+ {0xa402b9c5a8d3a6e7, 0x5f16206c9c6209a7},
+ {0xcd036837130890a1, 0x36dba887c37a8c10},
+ {0x802221226be55a64, 0xc2494954da2c978a},
+ {0xa02aa96b06deb0fd, 0xf2db9baa10b7bd6d},
+ {0xc83553c5c8965d3d, 0x6f92829494e5acc8},
+ {0xfa42a8b73abbf48c, 0xcb772339ba1f17fa},
+ {0x9c69a97284b578d7, 0xff2a760414536efc},
+ {0xc38413cf25e2d70d, 0xfef5138519684abb},
+ {0xf46518c2ef5b8cd1, 0x7eb258665fc25d6a},
+ {0x98bf2f79d5993802, 0xef2f773ffbd97a62},
+ {0xbeeefb584aff8603, 0xaafb550ffacfd8fb},
+ {0xeeaaba2e5dbf6784, 0x95ba2a53f983cf39},
+ {0x952ab45cfa97a0b2, 0xdd945a747bf26184},
+ {0xba756174393d88df, 0x94f971119aeef9e5},
+ {0xe912b9d1478ceb17, 0x7a37cd5601aab85e},
+ {0x91abb422ccb812ee, 0xac62e055c10ab33b},
+ {0xb616a12b7fe617aa, 0x577b986b314d600a},
+ {0xe39c49765fdf9d94, 0xed5a7e85fda0b80c},
+ {0x8e41ade9fbebc27d, 0x14588f13be847308},
+ {0xb1d219647ae6b31c, 0x596eb2d8ae258fc9},
+ {0xde469fbd99a05fe3, 0x6fca5f8ed9aef3bc},
+ {0x8aec23d680043bee, 0x25de7bb9480d5855},
+ {0xada72ccc20054ae9, 0xaf561aa79a10ae6b},
+ {0xd910f7ff28069da4, 0x1b2ba1518094da05},
+ {0x87aa9aff79042286, 0x90fb44d2f05d0843},
+ {0xa99541bf57452b28, 0x353a1607ac744a54},
+ {0xd3fa922f2d1675f2, 0x42889b8997915ce9},
+ {0x847c9b5d7c2e09b7, 0x69956135febada12},
+ {0xa59bc234db398c25, 0x43fab9837e699096},
+ {0xcf02b2c21207ef2e, 0x94f967e45e03f4bc},
+ {0x8161afb94b44f57d, 0x1d1be0eebac278f6},
+ {0xa1ba1ba79e1632dc, 0x6462d92a69731733},
+ {0xca28a291859bbf93, 0x7d7b8f7503cfdcff},
+ {0xfcb2cb35e702af78, 0x5cda735244c3d43f},
+ {0x9defbf01b061adab, 0x3a0888136afa64a8},
+ {0xc56baec21c7a1916, 0x088aaa1845b8fdd1},
+ {0xf6c69a72a3989f5b, 0x8aad549e57273d46},
+ {0x9a3c2087a63f6399, 0x36ac54e2f678864c},
+ {0xc0cb28a98fcf3c7f, 0x84576a1bb416a7de},
+ {0xf0fdf2d3f3c30b9f, 0x656d44a2a11c51d6},
+ {0x969eb7c47859e743, 0x9f644ae5a4b1b326},
+ {0xbc4665b596706114, 0x873d5d9f0dde1fef},
+ {0xeb57ff22fc0c7959, 0xa90cb506d155a7eb},
+ {0x9316ff75dd87cbd8, 0x09a7f12442d588f3},
+ {0xb7dcbf5354e9bece, 0x0c11ed6d538aeb30},
+ {0xe5d3ef282a242e81, 0x8f1668c8a86da5fb},
+ {0x8fa475791a569d10, 0xf96e017d694487bd},
+ {0xb38d92d760ec4455, 0x37c981dcc395a9ad},
+ {0xe070f78d3927556a, 0x85bbe253f47b1418},
+ {0x8c469ab843b89562, 0x93956d7478ccec8f},
+ {0xaf58416654a6babb, 0x387ac8d1970027b3},
+ {0xdb2e51bfe9d0696a, 0x06997b05fcc0319f},
+ {0x88fcf317f22241e2, 0x441fece3bdf81f04},
+ {0xab3c2fddeeaad25a, 0xd527e81cad7626c4},
+ {0xd60b3bd56a5586f1, 0x8a71e223d8d3b075},
+ {0x85c7056562757456, 0xf6872d5667844e4a},
+ {0xa738c6bebb12d16c, 0xb428f8ac016561dc},
+ {0xd106f86e69d785c7, 0xe13336d701beba53},
+ {0x82a45b450226b39c, 0xecc0024661173474},
+ {0xa34d721642b06084, 0x27f002d7f95d0191},
+ {0xcc20ce9bd35c78a5, 0x31ec038df7b441f5},
+ {0xff290242c83396ce, 0x7e67047175a15272},
+ {0x9f79a169bd203e41, 0x0f0062c6e984d387},
+ {0xc75809c42c684dd1, 0x52c07b78a3e60869},
+ {0xf92e0c3537826145, 0xa7709a56ccdf8a83},
+ {0x9bbcc7a142b17ccb, 0x88a66076400bb692},
+ {0xc2abf989935ddbfe, 0x6acff893d00ea436},
+ {0xf356f7ebf83552fe, 0x0583f6b8c4124d44},
+ {0x98165af37b2153de, 0xc3727a337a8b704b},
+ {0xbe1bf1b059e9a8d6, 0x744f18c0592e4c5d},
+ {0xeda2ee1c7064130c, 0x1162def06f79df74},
+ {0x9485d4d1c63e8be7, 0x8addcb5645ac2ba9},
+ {0xb9a74a0637ce2ee1, 0x6d953e2bd7173693},
+ {0xe8111c87c5c1ba99, 0xc8fa8db6ccdd0438},
+ {0x910ab1d4db9914a0, 0x1d9c9892400a22a3},
+ {0xb54d5e4a127f59c8, 0x2503beb6d00cab4c},
+ {0xe2a0b5dc971f303a, 0x2e44ae64840fd61e},
+ {0x8da471a9de737e24, 0x5ceaecfed289e5d3},
+ {0xb10d8e1456105dad, 0x7425a83e872c5f48},
+ {0xdd50f1996b947518, 0xd12f124e28f7771a},
+ {0x8a5296ffe33cc92f, 0x82bd6b70d99aaa70},
+ {0xace73cbfdc0bfb7b, 0x636cc64d1001550c},
+ {0xd8210befd30efa5a, 0x3c47f7e05401aa4f},
+ {0x8714a775e3e95c78, 0x65acfaec34810a72},
+ {0xa8d9d1535ce3b396, 0x7f1839a741a14d0e},
+ {0xd31045a8341ca07c, 0x1ede48111209a051},
+ {0x83ea2b892091e44d, 0x934aed0aab460433},
+ {0xa4e4b66b68b65d60, 0xf81da84d56178540},
+ {0xce1de40642e3f4b9, 0x36251260ab9d668f},
+ {0x80d2ae83e9ce78f3, 0xc1d72b7c6b42601a},
+ {0xa1075a24e4421730, 0xb24cf65b8612f820},
+ {0xc94930ae1d529cfc, 0xdee033f26797b628},
+ {0xfb9b7cd9a4a7443c, 0x169840ef017da3b2},
+ {0x9d412e0806e88aa5, 0x8e1f289560ee864f},
+ {0xc491798a08a2ad4e, 0xf1a6f2bab92a27e3},
+ {0xf5b5d7ec8acb58a2, 0xae10af696774b1dc},
+ {0x9991a6f3d6bf1765, 0xacca6da1e0a8ef2a},
+ {0xbff610b0cc6edd3f, 0x17fd090a58d32af4},
+ {0xeff394dcff8a948e, 0xddfc4b4cef07f5b1},
+ {0x95f83d0a1fb69cd9, 0x4abdaf101564f98f},
+ {0xbb764c4ca7a4440f, 0x9d6d1ad41abe37f2},
+ {0xea53df5fd18d5513, 0x84c86189216dc5ee},
+ {0x92746b9be2f8552c, 0x32fd3cf5b4e49bb5},
+ {0xb7118682dbb66a77, 0x3fbc8c33221dc2a2},
+ {0xe4d5e82392a40515, 0x0fabaf3feaa5334b},
+ {0x8f05b1163ba6832d, 0x29cb4d87f2a7400f},
+ {0xb2c71d5bca9023f8, 0x743e20e9ef511013},
+ {0xdf78e4b2bd342cf6, 0x914da9246b255417},
+ {0x8bab8eefb6409c1a, 0x1ad089b6c2f7548f},
+ {0xae9672aba3d0c320, 0xa184ac2473b529b2},
+ {0xda3c0f568cc4f3e8, 0xc9e5d72d90a2741f},
+ {0x8865899617fb1871, 0x7e2fa67c7a658893},
+ {0xaa7eebfb9df9de8d, 0xddbb901b98feeab8},
+ {0xd51ea6fa85785631, 0x552a74227f3ea566},
+ {0x8533285c936b35de, 0xd53a88958f872760},
+ {0xa67ff273b8460356, 0x8a892abaf368f138},
+ {0xd01fef10a657842c, 0x2d2b7569b0432d86},
+ {0x8213f56a67f6b29b, 0x9c3b29620e29fc74},
+ {0xa298f2c501f45f42, 0x8349f3ba91b47b90},
+ {0xcb3f2f7642717713, 0x241c70a936219a74},
+ {0xfe0efb53d30dd4d7, 0xed238cd383aa0111},
+ {0x9ec95d1463e8a506, 0xf4363804324a40ab},
+ {0xc67bb4597ce2ce48, 0xb143c6053edcd0d6},
+ {0xf81aa16fdc1b81da, 0xdd94b7868e94050b},
+ {0x9b10a4e5e9913128, 0xca7cf2b4191c8327},
+ {0xc1d4ce1f63f57d72, 0xfd1c2f611f63a3f1},
+ {0xf24a01a73cf2dccf, 0xbc633b39673c8ced},
+ {0x976e41088617ca01, 0xd5be0503e085d814},
+ {0xbd49d14aa79dbc82, 0x4b2d8644d8a74e19},
+ {0xec9c459d51852ba2, 0xddf8e7d60ed1219f},
+ {0x93e1ab8252f33b45, 0xcabb90e5c942b504},
+ {0xb8da1662e7b00a17, 0x3d6a751f3b936244},
+ {0xe7109bfba19c0c9d, 0x0cc512670a783ad5},
+ {0x906a617d450187e2, 0x27fb2b80668b24c6},
+ {0xb484f9dc9641e9da, 0xb1f9f660802dedf7},
+ {0xe1a63853bbd26451, 0x5e7873f8a0396974},
+ {0x8d07e33455637eb2, 0xdb0b487b6423e1e9},
+ {0xb049dc016abc5e5f, 0x91ce1a9a3d2cda63},
+ {0xdc5c5301c56b75f7, 0x7641a140cc7810fc},
+ {0x89b9b3e11b6329ba, 0xa9e904c87fcb0a9e},
+ {0xac2820d9623bf429, 0x546345fa9fbdcd45},
+ {0xd732290fbacaf133, 0xa97c177947ad4096},
+ {0x867f59a9d4bed6c0, 0x49ed8eabcccc485e},
+ {0xa81f301449ee8c70, 0x5c68f256bfff5a75},
+ {0xd226fc195c6a2f8c, 0x73832eec6fff3112},
+ {0x83585d8fd9c25db7, 0xc831fd53c5ff7eac},
+ {0xa42e74f3d032f525, 0xba3e7ca8b77f5e56},
+ {0xcd3a1230c43fb26f, 0x28ce1bd2e55f35ec},
+ {0x80444b5e7aa7cf85, 0x7980d163cf5b81b4},
+ {0xa0555e361951c366, 0xd7e105bcc3326220},
+ {0xc86ab5c39fa63440, 0x8dd9472bf3fefaa8},
+ {0xfa856334878fc150, 0xb14f98f6f0feb952},
+ {0x9c935e00d4b9d8d2, 0x6ed1bf9a569f33d4},
+ {0xc3b8358109e84f07, 0x0a862f80ec4700c9},
+ {0xf4a642e14c6262c8, 0xcd27bb612758c0fb},
+ {0x98e7e9cccfbd7dbd, 0x8038d51cb897789d},
+ {0xbf21e44003acdd2c, 0xe0470a63e6bd56c4},
+ {0xeeea5d5004981478, 0x1858ccfce06cac75},
+ {0x95527a5202df0ccb, 0x0f37801e0c43ebc9},
+ {0xbaa718e68396cffd, 0xd30560258f54e6bb},
+ {0xe950df20247c83fd, 0x47c6b82ef32a206a},
+ {0x91d28b7416cdd27e, 0x4cdc331d57fa5442},
+ {0xb6472e511c81471d, 0xe0133fe4adf8e953},
+ {0xe3d8f9e563a198e5, 0x58180fddd97723a7},
+ {0x8e679c2f5e44ff8f, 0x570f09eaa7ea7649},
+ {0xb201833b35d63f73, 0x2cd2cc6551e513db},
+ {0xde81e40a034bcf4f, 0xf8077f7ea65e58d2},
+ {0x8b112e86420f6191, 0xfb04afaf27faf783},
+ {0xadd57a27d29339f6, 0x79c5db9af1f9b564},
+ {0xd94ad8b1c7380874, 0x18375281ae7822bd},
+ {0x87cec76f1c830548, 0x8f2293910d0b15b6},
+ {0xa9c2794ae3a3c69a, 0xb2eb3875504ddb23},
+ {0xd433179d9c8cb841, 0x5fa60692a46151ec},
+ {0x849feec281d7f328, 0xdbc7c41ba6bcd334},
+ {0xa5c7ea73224deff3, 0x12b9b522906c0801},
+ {0xcf39e50feae16bef, 0xd768226b34870a01},
+ {0x81842f29f2cce375, 0xe6a1158300d46641},
+ {0xa1e53af46f801c53, 0x60495ae3c1097fd1},
+ {0xca5e89b18b602368, 0x385bb19cb14bdfc5},
+ {0xfcf62c1dee382c42, 0x46729e03dd9ed7b6},
+ {0x9e19db92b4e31ba9, 0x6c07a2c26a8346d2},
+ {0xc5a05277621be293, 0xc7098b7305241886},
+ {0xf70867153aa2db38, 0xb8cbee4fc66d1ea8},
+ {0x9a65406d44a5c903, 0x737f74f1dc043329},
+ {0xc0fe908895cf3b44, 0x505f522e53053ff3},
+ {0xf13e34aabb430a15, 0x647726b9e7c68ff0},
+ {0x96c6e0eab509e64d, 0x5eca783430dc19f6},
+ {0xbc789925624c5fe0, 0xb67d16413d132073},
+ {0xeb96bf6ebadf77d8, 0xe41c5bd18c57e890},
+ {0x933e37a534cbaae7, 0x8e91b962f7b6f15a},
+ {0xb80dc58e81fe95a1, 0x723627bbb5a4adb1},
+ {0xe61136f2227e3b09, 0xcec3b1aaa30dd91d},
+ {0x8fcac257558ee4e6, 0x213a4f0aa5e8a7b2},
+ {0xb3bd72ed2af29e1f, 0xa988e2cd4f62d19e},
+ {0xe0accfa875af45a7, 0x93eb1b80a33b8606},
+ {0x8c6c01c9498d8b88, 0xbc72f130660533c4},
+ {0xaf87023b9bf0ee6a, 0xeb8fad7c7f8680b5},
+ { 0xdb68c2ca82ed2a05,
+ 0xa67398db9f6820e2 }
+#else
+ {0xff77b1fcbebcdc4f, 0x25e8e89c13bb0f7b},
+ {0xce5d73ff402d98e3, 0xfb0a3d212dc81290},
+ {0xa6b34ad8c9dfc06f, 0xf42faa48c0ea481f},
+ {0x86a8d39ef77164bc, 0xae5dff9c02033198},
+ {0xd98ddaee19068c76, 0x3badd624dd9b0958},
+ {0xafbd2350644eeacf, 0xe5d1929ef90898fb},
+ {0x8df5efabc5979c8f, 0xca8d3ffa1ef463c2},
+ {0xe55990879ddcaabd, 0xcc420a6a101d0516},
+ {0xb94470938fa89bce, 0xf808e40e8d5b3e6a},
+ {0x95a8637627989aad, 0xdde7001379a44aa9},
+ {0xf1c90080baf72cb1, 0x5324c68b12dd6339},
+ {0xc350000000000000, 0x0000000000000000},
+ {0x9dc5ada82b70b59d, 0xf020000000000000},
+ {0xfee50b7025c36a08, 0x02f236d04753d5b5},
+ {0xcde6fd5e09abcf26, 0xed4c0226b55e6f87},
+ {0xa6539930bf6bff45, 0x84db8346b786151d},
+ {0x865b86925b9bc5c2, 0x0b8a2392ba45a9b3},
+ {0xd910f7ff28069da4, 0x1b2ba1518094da05},
+ {0xaf58416654a6babb, 0x387ac8d1970027b3},
+ {0x8da471a9de737e24, 0x5ceaecfed289e5d3},
+ {0xe4d5e82392a40515, 0x0fabaf3feaa5334b},
+ {0xb8da1662e7b00a17, 0x3d6a751f3b936244},
+ {0x95527a5202df0ccb, 0x0f37801e0c43ebc9},
+ {0xf13e34aabb430a15, 0x647726b9e7c68ff0}
+#endif
+ };
+
+#if FMT_USE_FULL_CACHE_DRAGONBOX
+ return pow10_significands[k - float_info<double>::min_k];
+#else
+ static constexpr const uint64_t powers_of_5_64[] = {
+ 0x0000000000000001, 0x0000000000000005, 0x0000000000000019,
+ 0x000000000000007d, 0x0000000000000271, 0x0000000000000c35,
+ 0x0000000000003d09, 0x000000000001312d, 0x000000000005f5e1,
+ 0x00000000001dcd65, 0x00000000009502f9, 0x0000000002e90edd,
+ 0x000000000e8d4a51, 0x0000000048c27395, 0x000000016bcc41e9,
+ 0x000000071afd498d, 0x0000002386f26fc1, 0x000000b1a2bc2ec5,
+ 0x000003782dace9d9, 0x00001158e460913d, 0x000056bc75e2d631,
+ 0x0001b1ae4d6e2ef5, 0x000878678326eac9, 0x002a5a058fc295ed,
+ 0x00d3c21bcecceda1, 0x0422ca8b0a00a425, 0x14adf4b7320334b9};
+
+ static const int compression_ratio = 27;
+
+ // Compute base index.
+ int cache_index = (k - float_info<double>::min_k) / compression_ratio;
+ int kb = cache_index * compression_ratio + float_info<double>::min_k;
+ int offset = k - kb;
+
+ // Get base cache.
+ uint128_fallback base_cache = pow10_significands[cache_index];
+ if (offset == 0) return base_cache;
+
+ // Compute the required amount of bit-shift.
+ int alpha = floor_log2_pow10(kb + offset) - floor_log2_pow10(kb) - offset;
+ FMT_ASSERT(alpha > 0 && alpha < 64, "shifting error detected");
+
+ // Try to recover the real cache.
+ uint64_t pow5 = powers_of_5_64[offset];
+ uint128_fallback recovered_cache = umul128(base_cache.high(), pow5);
+ uint128_fallback middle_low = umul128(base_cache.low(), pow5);
+
+ recovered_cache += middle_low.high();
+
+ uint64_t high_to_middle = recovered_cache.high() << (64 - alpha);
+ uint64_t middle_to_low = recovered_cache.low() << (64 - alpha);
+
+ recovered_cache =
+ uint128_fallback{(recovered_cache.low() >> alpha) | high_to_middle,
+ ((middle_low.low() >> alpha) | middle_to_low)};
+ FMT_ASSERT(recovered_cache.low() + 1 != 0, "");
+ return {recovered_cache.high(), recovered_cache.low() + 1};
+#endif
+ }
+
+ struct compute_mul_result {
+ carrier_uint result;
+ bool is_integer;
+ };
+ struct compute_mul_parity_result {
+ bool parity;
+ bool is_integer;
+ };
+
+ static compute_mul_result compute_mul(
+ carrier_uint u, const cache_entry_type& cache) noexcept {
+ auto r = umul192_upper128(u, cache);
+ return {r.high(), r.low() == 0};
+ }
+
+ static uint32_t compute_delta(cache_entry_type const& cache,
+ int beta) noexcept {
+ return static_cast<uint32_t>(cache.high() >> (64 - 1 - beta));
+ }
+
+ static compute_mul_parity_result compute_mul_parity(
+ carrier_uint two_f, const cache_entry_type& cache, int beta) noexcept {
+ FMT_ASSERT(beta >= 1, "");
+ FMT_ASSERT(beta < 64, "");
+
+ auto r = umul192_lower128(two_f, cache);
+ return {((r.high() >> (64 - beta)) & 1) != 0,
+ ((r.high() << beta) | (r.low() >> (64 - beta))) == 0};
+ }
+
+ static carrier_uint compute_left_endpoint_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept {
+ return (cache.high() -
+ (cache.high() >> (num_significand_bits<double>() + 2))) >>
+ (64 - num_significand_bits<double>() - 1 - beta);
+ }
+
+ static carrier_uint compute_right_endpoint_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept {
+ return (cache.high() +
+ (cache.high() >> (num_significand_bits<double>() + 1))) >>
+ (64 - num_significand_bits<double>() - 1 - beta);
+ }
+
+ static carrier_uint compute_round_up_for_shorter_interval_case(
+ const cache_entry_type& cache, int beta) noexcept {
+ return ((cache.high() >> (64 - num_significand_bits<double>() - 2 - beta)) +
+ 1) /
+ 2;
+ }
+};
+
+FMT_FUNC uint128_fallback get_cached_power(int k) noexcept {
+ return cache_accessor<double>::get_cached_power(k);
+}
+
+// Various integer checks
+template <typename T>
+bool is_left_endpoint_integer_shorter_interval(int exponent) noexcept {
+ const int case_shorter_interval_left_endpoint_lower_threshold = 2;
+ const int case_shorter_interval_left_endpoint_upper_threshold = 3;
+ return exponent >= case_shorter_interval_left_endpoint_lower_threshold &&
+ exponent <= case_shorter_interval_left_endpoint_upper_threshold;
+}
+
+// Remove trailing zeros from n and return the number of zeros removed (float)
+FMT_INLINE int remove_trailing_zeros(uint32_t& n) noexcept {
+ FMT_ASSERT(n != 0, "");
+ // Modular inverse of 5 (mod 2^32): (mod_inv_5 * 5) mod 2^32 = 1.
+ // See https://github.com/fmtlib/fmt/issues/3163 for more details.
+ const uint32_t mod_inv_5 = 0xcccccccd;
+ // Casts are needed to workaround a bug in MSVC 19.22 and older.
+ const uint32_t mod_inv_25 =
+ static_cast<uint32_t>(uint64_t(mod_inv_5) * mod_inv_5);
+
+ int s = 0;
+ while (true) {
+ auto q = rotr(n * mod_inv_25, 2);
+ if (q > max_value<uint32_t>() / 100) break;
+ n = q;
+ s += 2;
+ }
+ auto q = rotr(n * mod_inv_5, 1);
+ if (q <= max_value<uint32_t>() / 10) {
+ n = q;
+ s |= 1;
+ }
+ return s;
+}
+
+// Removes trailing zeros and returns the number of zeros removed (double)
+FMT_INLINE int remove_trailing_zeros(uint64_t& n) noexcept {
+ FMT_ASSERT(n != 0, "");
+
+ // This magic number is ceil(2^90 / 10^8).
+ constexpr uint64_t magic_number = 12379400392853802749ull;
+ auto nm = umul128(n, magic_number);
+
+ // Is n is divisible by 10^8?
+ if ((nm.high() & ((1ull << (90 - 64)) - 1)) == 0 && nm.low() < magic_number) {
+ // If yes, work with the quotient.
+ auto n32 = static_cast<uint32_t>(nm.high() >> (90 - 64));
+
+ const uint32_t mod_inv_5 = 0xcccccccd;
+ const uint32_t mod_inv_25 = mod_inv_5 * mod_inv_5;
+
+ int s = 8;
+ while (true) {
+ auto q = rotr(n32 * mod_inv_25, 2);
+ if (q > max_value<uint32_t>() / 100) break;
+ n32 = q;
+ s += 2;
+ }
+ auto q = rotr(n32 * mod_inv_5, 1);
+ if (q <= max_value<uint32_t>() / 10) {
+ n32 = q;
+ s |= 1;
+ }
+
+ n = n32;
+ return s;
+ }
+
+ // If n is not divisible by 10^8, work with n itself.
+ const uint64_t mod_inv_5 = 0xcccccccccccccccd;
+ const uint64_t mod_inv_25 = mod_inv_5 * mod_inv_5;
+
+ int s = 0;
+ while (true) {
+ auto q = rotr(n * mod_inv_25, 2);
+ if (q > max_value<uint64_t>() / 100) break;
+ n = q;
+ s += 2;
+ }
+ auto q = rotr(n * mod_inv_5, 1);
+ if (q <= max_value<uint64_t>() / 10) {
+ n = q;
+ s |= 1;
+ }
+
+ return s;
+}
+
+// The main algorithm for shorter interval case
+template <typename T>
+FMT_INLINE decimal_fp<T> shorter_interval_case(int exponent) noexcept {
+ decimal_fp<T> ret_value;
+ // Compute k and beta
+ const int minus_k = floor_log10_pow2_minus_log10_4_over_3(exponent);
+ const int beta = exponent + floor_log2_pow10(-minus_k);
+
+ // Compute xi and zi
+ using cache_entry_type = typename cache_accessor<T>::cache_entry_type;
+ const cache_entry_type cache = cache_accessor<T>::get_cached_power(-minus_k);
+
+ auto xi = cache_accessor<T>::compute_left_endpoint_for_shorter_interval_case(
+ cache, beta);
+ auto zi = cache_accessor<T>::compute_right_endpoint_for_shorter_interval_case(
+ cache, beta);
+
+ // If the left endpoint is not an integer, increase it
+ if (!is_left_endpoint_integer_shorter_interval<T>(exponent)) ++xi;
+
+ // Try bigger divisor
+ ret_value.significand = zi / 10;
+
+ // If succeed, remove trailing zeros if necessary and return
+ if (ret_value.significand * 10 >= xi) {
+ ret_value.exponent = minus_k + 1;
+ ret_value.exponent += remove_trailing_zeros(ret_value.significand);
+ return ret_value;
+ }
+
+ // Otherwise, compute the round-up of y
+ ret_value.significand =
+ cache_accessor<T>::compute_round_up_for_shorter_interval_case(cache,
+ beta);
+ ret_value.exponent = minus_k;
+
+ // When tie occurs, choose one of them according to the rule
+ if (exponent >= float_info<T>::shorter_interval_tie_lower_threshold &&
+ exponent <= float_info<T>::shorter_interval_tie_upper_threshold) {
+ ret_value.significand = ret_value.significand % 2 == 0
+ ? ret_value.significand
+ : ret_value.significand - 1;
+ } else if (ret_value.significand < xi) {
+ ++ret_value.significand;
+ }
+ return ret_value;
+}
+
+template <typename T> decimal_fp<T> to_decimal(T x) noexcept {
+ // Step 1: integer promotion & Schubfach multiplier calculation.
+
+ using carrier_uint = typename float_info<T>::carrier_uint;
+ using cache_entry_type = typename cache_accessor<T>::cache_entry_type;
+ auto br = bit_cast<carrier_uint>(x);
+
+ // Extract significand bits and exponent bits.
+ const carrier_uint significand_mask =
+ (static_cast<carrier_uint>(1) << num_significand_bits<T>()) - 1;
+ carrier_uint significand = (br & significand_mask);
+ int exponent =
+ static_cast<int>((br & exponent_mask<T>()) >> num_significand_bits<T>());
+
+ if (exponent != 0) { // Check if normal.
+ exponent -= exponent_bias<T>() + num_significand_bits<T>();
+
+ // Shorter interval case; proceed like Schubfach.
+ // In fact, when exponent == 1 and significand == 0, the interval is
+ // regular. However, it can be shown that the end-results are anyway same.
+ if (significand == 0) return shorter_interval_case<T>(exponent);
+
+ significand |= (static_cast<carrier_uint>(1) << num_significand_bits<T>());
+ } else {
+ // Subnormal case; the interval is always regular.
+ if (significand == 0) return {0, 0};
+ exponent =
+ std::numeric_limits<T>::min_exponent - num_significand_bits<T>() - 1;
+ }
+
+ const bool include_left_endpoint = (significand % 2 == 0);
+ const bool include_right_endpoint = include_left_endpoint;
+
+ // Compute k and beta.
+ const int minus_k = floor_log10_pow2(exponent) - float_info<T>::kappa;
+ const cache_entry_type cache = cache_accessor<T>::get_cached_power(-minus_k);
+ const int beta = exponent + floor_log2_pow10(-minus_k);
+
+ // Compute zi and deltai.
+ // 10^kappa <= deltai < 10^(kappa + 1)
+ const uint32_t deltai = cache_accessor<T>::compute_delta(cache, beta);
+ const carrier_uint two_fc = significand << 1;
+
+ // For the case of binary32, the result of integer check is not correct for
+ // 29711844 * 2^-82
+ // = 6.1442653300000000008655037797566933477355632930994033813476... * 10^-18
+ // and 29711844 * 2^-81
+ // = 1.2288530660000000001731007559513386695471126586198806762695... * 10^-17,
+ // and they are the unique counterexamples. However, since 29711844 is even,
+ // this does not cause any problem for the endpoints calculations; it can only
+ // cause a problem when we need to perform integer check for the center.
+ // Fortunately, with these inputs, that branch is never executed, so we are
+ // fine.
+ const typename cache_accessor<T>::compute_mul_result z_mul =
+ cache_accessor<T>::compute_mul((two_fc | 1) << beta, cache);
+
+ // Step 2: Try larger divisor; remove trailing zeros if necessary.
+
+ // Using an upper bound on zi, we might be able to optimize the division
+ // better than the compiler; we are computing zi / big_divisor here.
+ decimal_fp<T> ret_value;
+ ret_value.significand = divide_by_10_to_kappa_plus_1(z_mul.result);
+ uint32_t r = static_cast<uint32_t>(z_mul.result - float_info<T>::big_divisor *
+ ret_value.significand);
+
+ if (r < deltai) {
+ // Exclude the right endpoint if necessary.
+ if (r == 0 && (z_mul.is_integer & !include_right_endpoint)) {
+ --ret_value.significand;
+ r = float_info<T>::big_divisor;
+ goto small_divisor_case_label;
+ }
+ } else if (r > deltai) {
+ goto small_divisor_case_label;
+ } else {
+ // r == deltai; compare fractional parts.
+ const typename cache_accessor<T>::compute_mul_parity_result x_mul =
+ cache_accessor<T>::compute_mul_parity(two_fc - 1, cache, beta);
+
+ if (!(x_mul.parity | (x_mul.is_integer & include_left_endpoint)))
+ goto small_divisor_case_label;
+ }
+ ret_value.exponent = minus_k + float_info<T>::kappa + 1;
+
+ // We may need to remove trailing zeros.
+ ret_value.exponent += remove_trailing_zeros(ret_value.significand);
+ return ret_value;
+
+ // Step 3: Find the significand with the smaller divisor.
+
+small_divisor_case_label:
+ ret_value.significand *= 10;
+ ret_value.exponent = minus_k + float_info<T>::kappa;
+
+ uint32_t dist = r - (deltai / 2) + (float_info<T>::small_divisor / 2);
+ const bool approx_y_parity =
+ ((dist ^ (float_info<T>::small_divisor / 2)) & 1) != 0;
+
+ // Is dist divisible by 10^kappa?
+ const bool divisible_by_small_divisor =
+ check_divisibility_and_divide_by_pow10<float_info<T>::kappa>(dist);
+
+ // Add dist / 10^kappa to the significand.
+ ret_value.significand += dist;
+
+ if (!divisible_by_small_divisor) return ret_value;
+
+ // Check z^(f) >= epsilon^(f).
+ // We have either yi == zi - epsiloni or yi == (zi - epsiloni) - 1,
+ // where yi == zi - epsiloni if and only if z^(f) >= epsilon^(f).
+ // Since there are only 2 possibilities, we only need to care about the
+ // parity. Also, zi and r should have the same parity since the divisor
+ // is an even number.
+ const auto y_mul = cache_accessor<T>::compute_mul_parity(two_fc, cache, beta);
+
+ // If z^(f) >= epsilon^(f), we might have a tie when z^(f) == epsilon^(f),
+ // or equivalently, when y is an integer.
+ if (y_mul.parity != approx_y_parity)
+ --ret_value.significand;
+ else if (y_mul.is_integer & (ret_value.significand % 2 != 0))
+ --ret_value.significand;
+ return ret_value;
+}
+} // namespace dragonbox
+} // namespace detail
+
+template <> struct formatter<detail::bigint> {
+ FMT_CONSTEXPR auto parse(format_parse_context& ctx)
+ -> format_parse_context::iterator {
+ return ctx.begin();
+ }
+
+ auto format(const detail::bigint& n, format_context& ctx) const
+ -> format_context::iterator {
+ auto out = ctx.out();
+ bool first = true;
+ for (auto i = n.bigits_.size(); i > 0; --i) {
+ auto value = n.bigits_[i - 1u];
+ if (first) {
+ out = format_to(out, FMT_STRING("{:x}"), value);
+ first = false;
+ continue;
+ }
+ out = format_to(out, FMT_STRING("{:08x}"), value);
+ }
+ if (n.exp_ > 0)
+ out = format_to(out, FMT_STRING("p{}"),
+ n.exp_ * detail::bigint::bigit_bits);
+ return out;
+ }
+};
+
+FMT_FUNC detail::utf8_to_utf16::utf8_to_utf16(string_view s) {
+ for_each_codepoint(s, [this](uint32_t cp, string_view) {
+ if (cp == invalid_code_point) FMT_THROW(std::runtime_error("invalid utf8"));
+ if (cp <= 0xFFFF) {
+ buffer_.push_back(static_cast<wchar_t>(cp));
+ } else {
+ cp -= 0x10000;
+ buffer_.push_back(static_cast<wchar_t>(0xD800 + (cp >> 10)));
+ buffer_.push_back(static_cast<wchar_t>(0xDC00 + (cp & 0x3FF)));
+ }
+ return true;
+ });
+ buffer_.push_back(0);
+}
+
+FMT_FUNC void format_system_error(detail::buffer<char>& out, int error_code,
+ const char* message) noexcept {
+ FMT_TRY {
+ auto ec = std::error_code(error_code, std::generic_category());
+ write(std::back_inserter(out), std::system_error(ec, message).what());
+ return;
+ }
+ FMT_CATCH(...) {}
+ format_error_code(out, error_code, message);
+}
+
+FMT_FUNC void report_system_error(int error_code,
+ const char* message) noexcept {
+ report_error(format_system_error, error_code, message);
+}
+
+FMT_FUNC std::string vformat(string_view fmt, format_args args) {
+ // Don't optimize the "{}" case to keep the binary size small and because it
+ // can be better optimized in fmt::format anyway.
+ auto buffer = memory_buffer();
+ detail::vformat_to(buffer, fmt, args);
+ return to_string(buffer);
+}
+
+namespace detail {
+#ifndef _WIN32
+FMT_FUNC bool write_console(std::FILE*, string_view) { return false; }
+#else
+using dword = conditional_t<sizeof(long) == 4, unsigned long, unsigned>;
+extern "C" __declspec(dllimport) int __stdcall WriteConsoleW( //
+ void*, const void*, dword, dword*, void*);
+
+FMT_FUNC bool write_console(std::FILE* f, string_view text) {
+ auto fd = _fileno(f);
+ if (!_isatty(fd)) return false;
+ auto u16 = utf8_to_utf16(text);
+ auto written = dword();
+ return WriteConsoleW(reinterpret_cast<void*>(_get_osfhandle(fd)), u16.c_str(),
+ static_cast<uint32_t>(u16.size()), &written, nullptr);
+}
+
+// Print assuming legacy (non-Unicode) encoding.
+FMT_FUNC void vprint_mojibake(std::FILE* f, string_view fmt, format_args args) {
+ auto buffer = memory_buffer();
+ detail::vformat_to(buffer, fmt,
+ basic_format_args<buffer_context<char>>(args));
+ fwrite_fully(buffer.data(), 1, buffer.size(), f);
+}
+#endif
+
+FMT_FUNC void print(std::FILE* f, string_view text) {
+ if (!write_console(f, text)) fwrite_fully(text.data(), 1, text.size(), f);
+}
+} // namespace detail
+
+FMT_FUNC void vprint(std::FILE* f, string_view fmt, format_args args) {
+ auto buffer = memory_buffer();
+ detail::vformat_to(buffer, fmt, args);
+ detail::print(f, {buffer.data(), buffer.size()});
+}
+
+FMT_FUNC void vprint(string_view fmt, format_args args) {
+ vprint(stdout, fmt, args);
+}
+
+namespace detail {
+
+struct singleton {
+ unsigned char upper;
+ unsigned char lower_count;
+};
+
+inline auto is_printable(uint16_t x, const singleton* singletons,
+ size_t singletons_size,
+ const unsigned char* singleton_lowers,
+ const unsigned char* normal, size_t normal_size)
+ -> bool {
+ auto upper = x >> 8;
+ auto lower_start = 0;
+ for (size_t i = 0; i < singletons_size; ++i) {
+ auto s = singletons[i];
+ auto lower_end = lower_start + s.lower_count;
+ if (upper < s.upper) break;
+ if (upper == s.upper) {
+ for (auto j = lower_start; j < lower_end; ++j) {
+ if (singleton_lowers[j] == (x & 0xff)) return false;
+ }
+ }
+ lower_start = lower_end;
+ }
+
+ auto xsigned = static_cast<int>(x);
+ auto current = true;
+ for (size_t i = 0; i < normal_size; ++i) {
+ auto v = static_cast<int>(normal[i]);
+ auto len = (v & 0x80) != 0 ? (v & 0x7f) << 8 | normal[++i] : v;
+ xsigned -= len;
+ if (xsigned < 0) break;
+ current = !current;
+ }
+ return current;
+}
+
+// This code is generated by support/printable.py.
+FMT_FUNC auto is_printable(uint32_t cp) -> bool {
+ static constexpr singleton singletons0[] = {
+ {0x00, 1}, {0x03, 5}, {0x05, 6}, {0x06, 3}, {0x07, 6}, {0x08, 8},
+ {0x09, 17}, {0x0a, 28}, {0x0b, 25}, {0x0c, 20}, {0x0d, 16}, {0x0e, 13},
+ {0x0f, 4}, {0x10, 3}, {0x12, 18}, {0x13, 9}, {0x16, 1}, {0x17, 5},
+ {0x18, 2}, {0x19, 3}, {0x1a, 7}, {0x1c, 2}, {0x1d, 1}, {0x1f, 22},
+ {0x20, 3}, {0x2b, 3}, {0x2c, 2}, {0x2d, 11}, {0x2e, 1}, {0x30, 3},
+ {0x31, 2}, {0x32, 1}, {0xa7, 2}, {0xa9, 2}, {0xaa, 4}, {0xab, 8},
+ {0xfa, 2}, {0xfb, 5}, {0xfd, 4}, {0xfe, 3}, {0xff, 9},
+ };
+ static constexpr unsigned char singletons0_lower[] = {
+ 0xad, 0x78, 0x79, 0x8b, 0x8d, 0xa2, 0x30, 0x57, 0x58, 0x8b, 0x8c, 0x90,
+ 0x1c, 0x1d, 0xdd, 0x0e, 0x0f, 0x4b, 0x4c, 0xfb, 0xfc, 0x2e, 0x2f, 0x3f,
+ 0x5c, 0x5d, 0x5f, 0xb5, 0xe2, 0x84, 0x8d, 0x8e, 0x91, 0x92, 0xa9, 0xb1,
+ 0xba, 0xbb, 0xc5, 0xc6, 0xc9, 0xca, 0xde, 0xe4, 0xe5, 0xff, 0x00, 0x04,
+ 0x11, 0x12, 0x29, 0x31, 0x34, 0x37, 0x3a, 0x3b, 0x3d, 0x49, 0x4a, 0x5d,
+ 0x84, 0x8e, 0x92, 0xa9, 0xb1, 0xb4, 0xba, 0xbb, 0xc6, 0xca, 0xce, 0xcf,
+ 0xe4, 0xe5, 0x00, 0x04, 0x0d, 0x0e, 0x11, 0x12, 0x29, 0x31, 0x34, 0x3a,
+ 0x3b, 0x45, 0x46, 0x49, 0x4a, 0x5e, 0x64, 0x65, 0x84, 0x91, 0x9b, 0x9d,
+ 0xc9, 0xce, 0xcf, 0x0d, 0x11, 0x29, 0x45, 0x49, 0x57, 0x64, 0x65, 0x8d,
+ 0x91, 0xa9, 0xb4, 0xba, 0xbb, 0xc5, 0xc9, 0xdf, 0xe4, 0xe5, 0xf0, 0x0d,
+ 0x11, 0x45, 0x49, 0x64, 0x65, 0x80, 0x84, 0xb2, 0xbc, 0xbe, 0xbf, 0xd5,
+ 0xd7, 0xf0, 0xf1, 0x83, 0x85, 0x8b, 0xa4, 0xa6, 0xbe, 0xbf, 0xc5, 0xc7,
+ 0xce, 0xcf, 0xda, 0xdb, 0x48, 0x98, 0xbd, 0xcd, 0xc6, 0xce, 0xcf, 0x49,
+ 0x4e, 0x4f, 0x57, 0x59, 0x5e, 0x5f, 0x89, 0x8e, 0x8f, 0xb1, 0xb6, 0xb7,
+ 0xbf, 0xc1, 0xc6, 0xc7, 0xd7, 0x11, 0x16, 0x17, 0x5b, 0x5c, 0xf6, 0xf7,
+ 0xfe, 0xff, 0x80, 0x0d, 0x6d, 0x71, 0xde, 0xdf, 0x0e, 0x0f, 0x1f, 0x6e,
+ 0x6f, 0x1c, 0x1d, 0x5f, 0x7d, 0x7e, 0xae, 0xaf, 0xbb, 0xbc, 0xfa, 0x16,
+ 0x17, 0x1e, 0x1f, 0x46, 0x47, 0x4e, 0x4f, 0x58, 0x5a, 0x5c, 0x5e, 0x7e,
+ 0x7f, 0xb5, 0xc5, 0xd4, 0xd5, 0xdc, 0xf0, 0xf1, 0xf5, 0x72, 0x73, 0x8f,
+ 0x74, 0x75, 0x96, 0x2f, 0x5f, 0x26, 0x2e, 0x2f, 0xa7, 0xaf, 0xb7, 0xbf,
+ 0xc7, 0xcf, 0xd7, 0xdf, 0x9a, 0x40, 0x97, 0x98, 0x30, 0x8f, 0x1f, 0xc0,
+ 0xc1, 0xce, 0xff, 0x4e, 0x4f, 0x5a, 0x5b, 0x07, 0x08, 0x0f, 0x10, 0x27,
+ 0x2f, 0xee, 0xef, 0x6e, 0x6f, 0x37, 0x3d, 0x3f, 0x42, 0x45, 0x90, 0x91,
+ 0xfe, 0xff, 0x53, 0x67, 0x75, 0xc8, 0xc9, 0xd0, 0xd1, 0xd8, 0xd9, 0xe7,
+ 0xfe, 0xff,
+ };
+ static constexpr singleton singletons1[] = {
+ {0x00, 6}, {0x01, 1}, {0x03, 1}, {0x04, 2}, {0x08, 8}, {0x09, 2},
+ {0x0a, 5}, {0x0b, 2}, {0x0e, 4}, {0x10, 1}, {0x11, 2}, {0x12, 5},
+ {0x13, 17}, {0x14, 1}, {0x15, 2}, {0x17, 2}, {0x19, 13}, {0x1c, 5},
+ {0x1d, 8}, {0x24, 1}, {0x6a, 3}, {0x6b, 2}, {0xbc, 2}, {0xd1, 2},
+ {0xd4, 12}, {0xd5, 9}, {0xd6, 2}, {0xd7, 2}, {0xda, 1}, {0xe0, 5},
+ {0xe1, 2}, {0xe8, 2}, {0xee, 32}, {0xf0, 4}, {0xf8, 2}, {0xf9, 2},
+ {0xfa, 2}, {0xfb, 1},
+ };
+ static constexpr unsigned char singletons1_lower[] = {
+ 0x0c, 0x27, 0x3b, 0x3e, 0x4e, 0x4f, 0x8f, 0x9e, 0x9e, 0x9f, 0x06, 0x07,
+ 0x09, 0x36, 0x3d, 0x3e, 0x56, 0xf3, 0xd0, 0xd1, 0x04, 0x14, 0x18, 0x36,
+ 0x37, 0x56, 0x57, 0x7f, 0xaa, 0xae, 0xaf, 0xbd, 0x35, 0xe0, 0x12, 0x87,
+ 0x89, 0x8e, 0x9e, 0x04, 0x0d, 0x0e, 0x11, 0x12, 0x29, 0x31, 0x34, 0x3a,
+ 0x45, 0x46, 0x49, 0x4a, 0x4e, 0x4f, 0x64, 0x65, 0x5c, 0xb6, 0xb7, 0x1b,
+ 0x1c, 0x07, 0x08, 0x0a, 0x0b, 0x14, 0x17, 0x36, 0x39, 0x3a, 0xa8, 0xa9,
+ 0xd8, 0xd9, 0x09, 0x37, 0x90, 0x91, 0xa8, 0x07, 0x0a, 0x3b, 0x3e, 0x66,
+ 0x69, 0x8f, 0x92, 0x6f, 0x5f, 0xee, 0xef, 0x5a, 0x62, 0x9a, 0x9b, 0x27,
+ 0x28, 0x55, 0x9d, 0xa0, 0xa1, 0xa3, 0xa4, 0xa7, 0xa8, 0xad, 0xba, 0xbc,
+ 0xc4, 0x06, 0x0b, 0x0c, 0x15, 0x1d, 0x3a, 0x3f, 0x45, 0x51, 0xa6, 0xa7,
+ 0xcc, 0xcd, 0xa0, 0x07, 0x19, 0x1a, 0x22, 0x25, 0x3e, 0x3f, 0xc5, 0xc6,
+ 0x04, 0x20, 0x23, 0x25, 0x26, 0x28, 0x33, 0x38, 0x3a, 0x48, 0x4a, 0x4c,
+ 0x50, 0x53, 0x55, 0x56, 0x58, 0x5a, 0x5c, 0x5e, 0x60, 0x63, 0x65, 0x66,
+ 0x6b, 0x73, 0x78, 0x7d, 0x7f, 0x8a, 0xa4, 0xaa, 0xaf, 0xb0, 0xc0, 0xd0,
+ 0xae, 0xaf, 0x79, 0xcc, 0x6e, 0x6f, 0x93,
+ };
+ static constexpr unsigned char normal0[] = {
+ 0x00, 0x20, 0x5f, 0x22, 0x82, 0xdf, 0x04, 0x82, 0x44, 0x08, 0x1b, 0x04,
+ 0x06, 0x11, 0x81, 0xac, 0x0e, 0x80, 0xab, 0x35, 0x28, 0x0b, 0x80, 0xe0,
+ 0x03, 0x19, 0x08, 0x01, 0x04, 0x2f, 0x04, 0x34, 0x04, 0x07, 0x03, 0x01,
+ 0x07, 0x06, 0x07, 0x11, 0x0a, 0x50, 0x0f, 0x12, 0x07, 0x55, 0x07, 0x03,
+ 0x04, 0x1c, 0x0a, 0x09, 0x03, 0x08, 0x03, 0x07, 0x03, 0x02, 0x03, 0x03,
+ 0x03, 0x0c, 0x04, 0x05, 0x03, 0x0b, 0x06, 0x01, 0x0e, 0x15, 0x05, 0x3a,
+ 0x03, 0x11, 0x07, 0x06, 0x05, 0x10, 0x07, 0x57, 0x07, 0x02, 0x07, 0x15,
+ 0x0d, 0x50, 0x04, 0x43, 0x03, 0x2d, 0x03, 0x01, 0x04, 0x11, 0x06, 0x0f,
+ 0x0c, 0x3a, 0x04, 0x1d, 0x25, 0x5f, 0x20, 0x6d, 0x04, 0x6a, 0x25, 0x80,
+ 0xc8, 0x05, 0x82, 0xb0, 0x03, 0x1a, 0x06, 0x82, 0xfd, 0x03, 0x59, 0x07,
+ 0x15, 0x0b, 0x17, 0x09, 0x14, 0x0c, 0x14, 0x0c, 0x6a, 0x06, 0x0a, 0x06,
+ 0x1a, 0x06, 0x59, 0x07, 0x2b, 0x05, 0x46, 0x0a, 0x2c, 0x04, 0x0c, 0x04,
+ 0x01, 0x03, 0x31, 0x0b, 0x2c, 0x04, 0x1a, 0x06, 0x0b, 0x03, 0x80, 0xac,
+ 0x06, 0x0a, 0x06, 0x21, 0x3f, 0x4c, 0x04, 0x2d, 0x03, 0x74, 0x08, 0x3c,
+ 0x03, 0x0f, 0x03, 0x3c, 0x07, 0x38, 0x08, 0x2b, 0x05, 0x82, 0xff, 0x11,
+ 0x18, 0x08, 0x2f, 0x11, 0x2d, 0x03, 0x20, 0x10, 0x21, 0x0f, 0x80, 0x8c,
+ 0x04, 0x82, 0x97, 0x19, 0x0b, 0x15, 0x88, 0x94, 0x05, 0x2f, 0x05, 0x3b,
+ 0x07, 0x02, 0x0e, 0x18, 0x09, 0x80, 0xb3, 0x2d, 0x74, 0x0c, 0x80, 0xd6,
+ 0x1a, 0x0c, 0x05, 0x80, 0xff, 0x05, 0x80, 0xdf, 0x0c, 0xee, 0x0d, 0x03,
+ 0x84, 0x8d, 0x03, 0x37, 0x09, 0x81, 0x5c, 0x14, 0x80, 0xb8, 0x08, 0x80,
+ 0xcb, 0x2a, 0x38, 0x03, 0x0a, 0x06, 0x38, 0x08, 0x46, 0x08, 0x0c, 0x06,
+ 0x74, 0x0b, 0x1e, 0x03, 0x5a, 0x04, 0x59, 0x09, 0x80, 0x83, 0x18, 0x1c,
+ 0x0a, 0x16, 0x09, 0x4c, 0x04, 0x80, 0x8a, 0x06, 0xab, 0xa4, 0x0c, 0x17,
+ 0x04, 0x31, 0xa1, 0x04, 0x81, 0xda, 0x26, 0x07, 0x0c, 0x05, 0x05, 0x80,
+ 0xa5, 0x11, 0x81, 0x6d, 0x10, 0x78, 0x28, 0x2a, 0x06, 0x4c, 0x04, 0x80,
+ 0x8d, 0x04, 0x80, 0xbe, 0x03, 0x1b, 0x03, 0x0f, 0x0d,
+ };
+ static constexpr unsigned char normal1[] = {
+ 0x5e, 0x22, 0x7b, 0x05, 0x03, 0x04, 0x2d, 0x03, 0x66, 0x03, 0x01, 0x2f,
+ 0x2e, 0x80, 0x82, 0x1d, 0x03, 0x31, 0x0f, 0x1c, 0x04, 0x24, 0x09, 0x1e,
+ 0x05, 0x2b, 0x05, 0x44, 0x04, 0x0e, 0x2a, 0x80, 0xaa, 0x06, 0x24, 0x04,
+ 0x24, 0x04, 0x28, 0x08, 0x34, 0x0b, 0x01, 0x80, 0x90, 0x81, 0x37, 0x09,
+ 0x16, 0x0a, 0x08, 0x80, 0x98, 0x39, 0x03, 0x63, 0x08, 0x09, 0x30, 0x16,
+ 0x05, 0x21, 0x03, 0x1b, 0x05, 0x01, 0x40, 0x38, 0x04, 0x4b, 0x05, 0x2f,
+ 0x04, 0x0a, 0x07, 0x09, 0x07, 0x40, 0x20, 0x27, 0x04, 0x0c, 0x09, 0x36,
+ 0x03, 0x3a, 0x05, 0x1a, 0x07, 0x04, 0x0c, 0x07, 0x50, 0x49, 0x37, 0x33,
+ 0x0d, 0x33, 0x07, 0x2e, 0x08, 0x0a, 0x81, 0x26, 0x52, 0x4e, 0x28, 0x08,
+ 0x2a, 0x56, 0x1c, 0x14, 0x17, 0x09, 0x4e, 0x04, 0x1e, 0x0f, 0x43, 0x0e,
+ 0x19, 0x07, 0x0a, 0x06, 0x48, 0x08, 0x27, 0x09, 0x75, 0x0b, 0x3f, 0x41,
+ 0x2a, 0x06, 0x3b, 0x05, 0x0a, 0x06, 0x51, 0x06, 0x01, 0x05, 0x10, 0x03,
+ 0x05, 0x80, 0x8b, 0x62, 0x1e, 0x48, 0x08, 0x0a, 0x80, 0xa6, 0x5e, 0x22,
+ 0x45, 0x0b, 0x0a, 0x06, 0x0d, 0x13, 0x39, 0x07, 0x0a, 0x36, 0x2c, 0x04,
+ 0x10, 0x80, 0xc0, 0x3c, 0x64, 0x53, 0x0c, 0x48, 0x09, 0x0a, 0x46, 0x45,
+ 0x1b, 0x48, 0x08, 0x53, 0x1d, 0x39, 0x81, 0x07, 0x46, 0x0a, 0x1d, 0x03,
+ 0x47, 0x49, 0x37, 0x03, 0x0e, 0x08, 0x0a, 0x06, 0x39, 0x07, 0x0a, 0x81,
+ 0x36, 0x19, 0x80, 0xb7, 0x01, 0x0f, 0x32, 0x0d, 0x83, 0x9b, 0x66, 0x75,
+ 0x0b, 0x80, 0xc4, 0x8a, 0xbc, 0x84, 0x2f, 0x8f, 0xd1, 0x82, 0x47, 0xa1,
+ 0xb9, 0x82, 0x39, 0x07, 0x2a, 0x04, 0x02, 0x60, 0x26, 0x0a, 0x46, 0x0a,
+ 0x28, 0x05, 0x13, 0x82, 0xb0, 0x5b, 0x65, 0x4b, 0x04, 0x39, 0x07, 0x11,
+ 0x40, 0x05, 0x0b, 0x02, 0x0e, 0x97, 0xf8, 0x08, 0x84, 0xd6, 0x2a, 0x09,
+ 0xa2, 0xf7, 0x81, 0x1f, 0x31, 0x03, 0x11, 0x04, 0x08, 0x81, 0x8c, 0x89,
+ 0x04, 0x6b, 0x05, 0x0d, 0x03, 0x09, 0x07, 0x10, 0x93, 0x60, 0x80, 0xf6,
+ 0x0a, 0x73, 0x08, 0x6e, 0x17, 0x46, 0x80, 0x9a, 0x14, 0x0c, 0x57, 0x09,
+ 0x19, 0x80, 0x87, 0x81, 0x47, 0x03, 0x85, 0x42, 0x0f, 0x15, 0x85, 0x50,
+ 0x2b, 0x80, 0xd5, 0x2d, 0x03, 0x1a, 0x04, 0x02, 0x81, 0x70, 0x3a, 0x05,
+ 0x01, 0x85, 0x00, 0x80, 0xd7, 0x29, 0x4c, 0x04, 0x0a, 0x04, 0x02, 0x83,
+ 0x11, 0x44, 0x4c, 0x3d, 0x80, 0xc2, 0x3c, 0x06, 0x01, 0x04, 0x55, 0x05,
+ 0x1b, 0x34, 0x02, 0x81, 0x0e, 0x2c, 0x04, 0x64, 0x0c, 0x56, 0x0a, 0x80,
+ 0xae, 0x38, 0x1d, 0x0d, 0x2c, 0x04, 0x09, 0x07, 0x02, 0x0e, 0x06, 0x80,
+ 0x9a, 0x83, 0xd8, 0x08, 0x0d, 0x03, 0x0d, 0x03, 0x74, 0x0c, 0x59, 0x07,
+ 0x0c, 0x14, 0x0c, 0x04, 0x38, 0x08, 0x0a, 0x06, 0x28, 0x08, 0x22, 0x4e,
+ 0x81, 0x54, 0x0c, 0x15, 0x03, 0x03, 0x05, 0x07, 0x09, 0x19, 0x07, 0x07,
+ 0x09, 0x03, 0x0d, 0x07, 0x29, 0x80, 0xcb, 0x25, 0x0a, 0x84, 0x06,
+ };
+ auto lower = static_cast<uint16_t>(cp);
+ if (cp < 0x10000) {
+ return is_printable(lower, singletons0,
+ sizeof(singletons0) / sizeof(*singletons0),
+ singletons0_lower, normal0, sizeof(normal0));
+ }
+ if (cp < 0x20000) {
+ return is_printable(lower, singletons1,
+ sizeof(singletons1) / sizeof(*singletons1),
+ singletons1_lower, normal1, sizeof(normal1));
+ }
+ if (0x2a6de <= cp && cp < 0x2a700) return false;
+ if (0x2b735 <= cp && cp < 0x2b740) return false;
+ if (0x2b81e <= cp && cp < 0x2b820) return false;
+ if (0x2cea2 <= cp && cp < 0x2ceb0) return false;
+ if (0x2ebe1 <= cp && cp < 0x2f800) return false;
+ if (0x2fa1e <= cp && cp < 0x30000) return false;
+ if (0x3134b <= cp && cp < 0xe0100) return false;
+ if (0xe01f0 <= cp && cp < 0x110000) return false;
+ return cp < 0x110000;
+}
+
+} // namespace detail
+
+FMT_END_NAMESPACE
+
+#endif // FMT_FORMAT_INL_H_
diff --git a/src/fmtlib/fmt/format.h b/src/fmtlib/fmt/format.h
new file mode 100644
index 0000000..ed8b29e
--- /dev/null
+++ b/src/fmtlib/fmt/format.h
@@ -0,0 +1,4735 @@
+/*
+ Formatting library for C++
+
+ Copyright (c) 2012 - present, Victor Zverovich
+
+ Permission is hereby granted, free of charge, to any person obtaining
+ a copy of this software and associated documentation files (the
+ "Software"), to deal in the Software without restriction, including
+ without limitation the rights to use, copy, modify, merge, publish,
+ distribute, sublicense, and/or sell copies of the Software, and to
+ permit persons to whom the Software is furnished to do so, subject to
+ the following conditions:
+
+ The above copyright notice and this permission notice shall be
+ included in all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+ --- Optional exception to the license ---
+
+ As an exception, if, as a result of your compiling your source code, portions
+ of this Software are embedded into a machine-executable object form of such
+ source code, you may redistribute such embedded portions in such object form
+ without including the above copyright and permission notices.
+ */
+
+#ifndef FMT_FORMAT_H_
+#define FMT_FORMAT_H_
+
+#include <cmath> // std::signbit
+#include <cstdint> // uint32_t
+#include <cstring> // std::memcpy
+#include <initializer_list> // std::initializer_list
+#include <limits> // std::numeric_limits
+#include <memory> // std::uninitialized_copy
+#include <stdexcept> // std::runtime_error
+#include <system_error> // std::system_error
+
+#ifdef __cpp_lib_bit_cast
+# include <bit> // std::bitcast
+#endif
+
+#include "core.h"
+
+#ifndef FMT_BEGIN_DETAIL_NAMESPACE
+# define FMT_BEGIN_DETAIL_NAMESPACE namespace detail {
+# define FMT_END_DETAIL_NAMESPACE }
+#endif
+
+#if FMT_HAS_CPP17_ATTRIBUTE(fallthrough)
+# define FMT_FALLTHROUGH [[fallthrough]]
+#elif defined(__clang__)
+# define FMT_FALLTHROUGH [[clang::fallthrough]]
+#elif FMT_GCC_VERSION >= 700 && \
+ (!defined(__EDG_VERSION__) || __EDG_VERSION__ >= 520)
+# define FMT_FALLTHROUGH [[gnu::fallthrough]]
+#else
+# define FMT_FALLTHROUGH
+#endif
+
+#ifndef FMT_DEPRECATED
+# if FMT_HAS_CPP14_ATTRIBUTE(deprecated) || FMT_MSC_VERSION >= 1900
+# define FMT_DEPRECATED [[deprecated]]
+# else
+# if (defined(__GNUC__) && !defined(__LCC__)) || defined(__clang__)
+# define FMT_DEPRECATED __attribute__((deprecated))
+# elif FMT_MSC_VERSION
+# define FMT_DEPRECATED __declspec(deprecated)
+# else
+# define FMT_DEPRECATED /* deprecated */
+# endif
+# endif
+#endif
+
+#if FMT_GCC_VERSION
+# define FMT_GCC_VISIBILITY_HIDDEN __attribute__((visibility("hidden")))
+#else
+# define FMT_GCC_VISIBILITY_HIDDEN
+#endif
+
+#ifdef __NVCC__
+# define FMT_CUDA_VERSION (__CUDACC_VER_MAJOR__ * 100 + __CUDACC_VER_MINOR__)
+#else
+# define FMT_CUDA_VERSION 0
+#endif
+
+#ifdef __has_builtin
+# define FMT_HAS_BUILTIN(x) __has_builtin(x)
+#else
+# define FMT_HAS_BUILTIN(x) 0
+#endif
+
+#if FMT_GCC_VERSION || FMT_CLANG_VERSION
+# define FMT_NOINLINE __attribute__((noinline))
+#else
+# define FMT_NOINLINE
+#endif
+
+#ifndef FMT_THROW
+# if FMT_EXCEPTIONS
+# if FMT_MSC_VERSION || defined(__NVCC__)
+FMT_BEGIN_NAMESPACE
+namespace detail {
+template <typename Exception> inline void do_throw(const Exception& x) {
+ // Silence unreachable code warnings in MSVC and NVCC because these
+ // are nearly impossible to fix in a generic code.
+ volatile bool b = true;
+ if (b) throw x;
+}
+} // namespace detail
+FMT_END_NAMESPACE
+# define FMT_THROW(x) detail::do_throw(x)
+# else
+# define FMT_THROW(x) throw x
+# endif
+# else
+# define FMT_THROW(x) \
+ do { \
+ FMT_ASSERT(false, (x).what()); \
+ } while (false)
+# endif
+#endif
+
+#if FMT_EXCEPTIONS
+# define FMT_TRY try
+# define FMT_CATCH(x) catch (x)
+#else
+# define FMT_TRY if (true)
+# define FMT_CATCH(x) if (false)
+#endif
+
+#ifndef FMT_MAYBE_UNUSED
+# if FMT_HAS_CPP17_ATTRIBUTE(maybe_unused)
+# define FMT_MAYBE_UNUSED [[maybe_unused]]
+# else
+# define FMT_MAYBE_UNUSED
+# endif
+#endif
+
+#ifndef FMT_USE_USER_DEFINED_LITERALS
+// EDG based compilers (Intel, NVIDIA, Elbrus, etc), GCC and MSVC support UDLs.
+# if (FMT_HAS_FEATURE(cxx_user_literals) || FMT_GCC_VERSION >= 407 || \
+ FMT_MSC_VERSION >= 1900) && \
+ (!defined(__EDG_VERSION__) || __EDG_VERSION__ >= /* UDL feature */ 480)
+# define FMT_USE_USER_DEFINED_LITERALS 1
+# else
+# define FMT_USE_USER_DEFINED_LITERALS 0
+# endif
+#endif
+
+// Defining FMT_REDUCE_INT_INSTANTIATIONS to 1, will reduce the number of
+// integer formatter template instantiations to just one by only using the
+// largest integer type. This results in a reduction in binary size but will
+// cause a decrease in integer formatting performance.
+#if !defined(FMT_REDUCE_INT_INSTANTIATIONS)
+# define FMT_REDUCE_INT_INSTANTIATIONS 0
+#endif
+
+// __builtin_clz is broken in clang with Microsoft CodeGen:
+// https://github.com/fmtlib/fmt/issues/519.
+#if !FMT_MSC_VERSION
+# if FMT_HAS_BUILTIN(__builtin_clz) || FMT_GCC_VERSION || FMT_ICC_VERSION
+# define FMT_BUILTIN_CLZ(n) __builtin_clz(n)
+# endif
+# if FMT_HAS_BUILTIN(__builtin_clzll) || FMT_GCC_VERSION || FMT_ICC_VERSION
+# define FMT_BUILTIN_CLZLL(n) __builtin_clzll(n)
+# endif
+#endif
+
+// __builtin_ctz is broken in Intel Compiler Classic on Windows:
+// https://github.com/fmtlib/fmt/issues/2510.
+#ifndef __ICL
+# if FMT_HAS_BUILTIN(__builtin_ctz) || FMT_GCC_VERSION || FMT_ICC_VERSION || \
+ defined(__NVCOMPILER)
+# define FMT_BUILTIN_CTZ(n) __builtin_ctz(n)
+# endif
+# if FMT_HAS_BUILTIN(__builtin_ctzll) || FMT_GCC_VERSION || \
+ FMT_ICC_VERSION || defined(__NVCOMPILER)
+# define FMT_BUILTIN_CTZLL(n) __builtin_ctzll(n)
+# endif
+#endif
+
+#if FMT_MSC_VERSION
+# include <intrin.h> // _BitScanReverse[64], _BitScanForward[64], _umul128
+#endif
+
+// Some compilers masquerade as both MSVC and GCC-likes or otherwise support
+// __builtin_clz and __builtin_clzll, so only define FMT_BUILTIN_CLZ using the
+// MSVC intrinsics if the clz and clzll builtins are not available.
+#if FMT_MSC_VERSION && !defined(FMT_BUILTIN_CLZLL) && \
+ !defined(FMT_BUILTIN_CTZLL)
+FMT_BEGIN_NAMESPACE
+namespace detail {
+// Avoid Clang with Microsoft CodeGen's -Wunknown-pragmas warning.
+# if !defined(__clang__)
+# pragma intrinsic(_BitScanForward)
+# pragma intrinsic(_BitScanReverse)
+# if defined(_WIN64)
+# pragma intrinsic(_BitScanForward64)
+# pragma intrinsic(_BitScanReverse64)
+# endif
+# endif
+
+inline auto clz(uint32_t x) -> int {
+ unsigned long r = 0;
+ _BitScanReverse(&r, x);
+ FMT_ASSERT(x != 0, "");
+ // Static analysis complains about using uninitialized data
+ // "r", but the only way that can happen is if "x" is 0,
+ // which the callers guarantee to not happen.
+ FMT_MSC_WARNING(suppress : 6102)
+ return 31 ^ static_cast<int>(r);
+}
+# define FMT_BUILTIN_CLZ(n) detail::clz(n)
+
+inline auto clzll(uint64_t x) -> int {
+ unsigned long r = 0;
+# ifdef _WIN64
+ _BitScanReverse64(&r, x);
+# else
+ // Scan the high 32 bits.
+ if (_BitScanReverse(&r, static_cast<uint32_t>(x >> 32)))
+ return 63 ^ static_cast<int>(r + 32);
+ // Scan the low 32 bits.
+ _BitScanReverse(&r, static_cast<uint32_t>(x));
+# endif
+ FMT_ASSERT(x != 0, "");
+ FMT_MSC_WARNING(suppress : 6102) // Suppress a bogus static analysis warning.
+ return 63 ^ static_cast<int>(r);
+}
+# define FMT_BUILTIN_CLZLL(n) detail::clzll(n)
+
+inline auto ctz(uint32_t x) -> int {
+ unsigned long r = 0;
+ _BitScanForward(&r, x);
+ FMT_ASSERT(x != 0, "");
+ FMT_MSC_WARNING(suppress : 6102) // Suppress a bogus static analysis warning.
+ return static_cast<int>(r);
+}
+# define FMT_BUILTIN_CTZ(n) detail::ctz(n)
+
+inline auto ctzll(uint64_t x) -> int {
+ unsigned long r = 0;
+ FMT_ASSERT(x != 0, "");
+ FMT_MSC_WARNING(suppress : 6102) // Suppress a bogus static analysis warning.
+# ifdef _WIN64
+ _BitScanForward64(&r, x);
+# else
+ // Scan the low 32 bits.
+ if (_BitScanForward(&r, static_cast<uint32_t>(x))) return static_cast<int>(r);
+ // Scan the high 32 bits.
+ _BitScanForward(&r, static_cast<uint32_t>(x >> 32));
+ r += 32;
+# endif
+ return static_cast<int>(r);
+}
+# define FMT_BUILTIN_CTZLL(n) detail::ctzll(n)
+} // namespace detail
+FMT_END_NAMESPACE
+#endif
+
+FMT_BEGIN_NAMESPACE
+
+template <typename...> struct disjunction : std::false_type {};
+template <typename P> struct disjunction<P> : P {};
+template <typename P1, typename... Pn>
+struct disjunction<P1, Pn...>
+ : conditional_t<bool(P1::value), P1, disjunction<Pn...>> {};
+
+template <typename...> struct conjunction : std::true_type {};
+template <typename P> struct conjunction<P> : P {};
+template <typename P1, typename... Pn>
+struct conjunction<P1, Pn...>
+ : conditional_t<bool(P1::value), conjunction<Pn...>, P1> {};
+
+namespace detail {
+
+FMT_CONSTEXPR inline void abort_fuzzing_if(bool condition) {
+ ignore_unused(condition);
+#ifdef FMT_FUZZ
+ if (condition) throw std::runtime_error("fuzzing limit reached");
+#endif
+}
+
+template <typename CharT, CharT... C> struct string_literal {
+ static constexpr CharT value[sizeof...(C)] = {C...};
+ constexpr operator basic_string_view<CharT>() const {
+ return {value, sizeof...(C)};
+ }
+};
+
+#if FMT_CPLUSPLUS < 201703L
+template <typename CharT, CharT... C>
+constexpr CharT string_literal<CharT, C...>::value[sizeof...(C)];
+#endif
+
+template <typename Streambuf> class formatbuf : public Streambuf {
+ private:
+ using char_type = typename Streambuf::char_type;
+ using streamsize = decltype(std::declval<Streambuf>().sputn(nullptr, 0));
+ using int_type = typename Streambuf::int_type;
+ using traits_type = typename Streambuf::traits_type;
+
+ buffer<char_type>& buffer_;
+
+ public:
+ explicit formatbuf(buffer<char_type>& buf) : buffer_(buf) {}
+
+ protected:
+ // The put area is always empty. This makes the implementation simpler and has
+ // the advantage that the streambuf and the buffer are always in sync and
+ // sputc never writes into uninitialized memory. A disadvantage is that each
+ // call to sputc always results in a (virtual) call to overflow. There is no
+ // disadvantage here for sputn since this always results in a call to xsputn.
+
+ auto overflow(int_type ch) -> int_type override {
+ if (!traits_type::eq_int_type(ch, traits_type::eof()))
+ buffer_.push_back(static_cast<char_type>(ch));
+ return ch;
+ }
+
+ auto xsputn(const char_type* s, streamsize count) -> streamsize override {
+ buffer_.append(s, s + count);
+ return count;
+ }
+};
+
+// Implementation of std::bit_cast for pre-C++20.
+template <typename To, typename From, FMT_ENABLE_IF(sizeof(To) == sizeof(From))>
+FMT_CONSTEXPR20 auto bit_cast(const From& from) -> To {
+#ifdef __cpp_lib_bit_cast
+ if (is_constant_evaluated()) return std::bit_cast<To>(from);
+#endif
+ auto to = To();
+ // The cast suppresses a bogus -Wclass-memaccess on GCC.
+ std::memcpy(static_cast<void*>(&to), &from, sizeof(to));
+ return to;
+}
+
+inline auto is_big_endian() -> bool {
+#ifdef _WIN32
+ return false;
+#elif defined(__BIG_ENDIAN__)
+ return true;
+#elif defined(__BYTE_ORDER__) && defined(__ORDER_BIG_ENDIAN__)
+ return __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__;
+#else
+ struct bytes {
+ char data[sizeof(int)];
+ };
+ return bit_cast<bytes>(1).data[0] == 0;
+#endif
+}
+
+class uint128_fallback {
+ private:
+ uint64_t lo_, hi_;
+
+ friend uint128_fallback umul128(uint64_t x, uint64_t y) noexcept;
+
+ public:
+ constexpr uint128_fallback(uint64_t hi, uint64_t lo) : lo_(lo), hi_(hi) {}
+ constexpr uint128_fallback(uint64_t value = 0) : lo_(value), hi_(0) {}
+
+ constexpr uint64_t high() const noexcept { return hi_; }
+ constexpr uint64_t low() const noexcept { return lo_; }
+
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ constexpr explicit operator T() const {
+ return static_cast<T>(lo_);
+ }
+
+ friend constexpr auto operator==(const uint128_fallback& lhs,
+ const uint128_fallback& rhs) -> bool {
+ return lhs.hi_ == rhs.hi_ && lhs.lo_ == rhs.lo_;
+ }
+ friend constexpr auto operator!=(const uint128_fallback& lhs,
+ const uint128_fallback& rhs) -> bool {
+ return !(lhs == rhs);
+ }
+ friend constexpr auto operator>(const uint128_fallback& lhs,
+ const uint128_fallback& rhs) -> bool {
+ return lhs.hi_ != rhs.hi_ ? lhs.hi_ > rhs.hi_ : lhs.lo_ > rhs.lo_;
+ }
+ friend constexpr auto operator|(const uint128_fallback& lhs,
+ const uint128_fallback& rhs)
+ -> uint128_fallback {
+ return {lhs.hi_ | rhs.hi_, lhs.lo_ | rhs.lo_};
+ }
+ friend constexpr auto operator&(const uint128_fallback& lhs,
+ const uint128_fallback& rhs)
+ -> uint128_fallback {
+ return {lhs.hi_ & rhs.hi_, lhs.lo_ & rhs.lo_};
+ }
+ friend constexpr auto operator~(const uint128_fallback& n)
+ -> uint128_fallback {
+ return {~n.hi_, ~n.lo_};
+ }
+ friend auto operator+(const uint128_fallback& lhs,
+ const uint128_fallback& rhs) -> uint128_fallback {
+ auto result = uint128_fallback(lhs);
+ result += rhs;
+ return result;
+ }
+ friend auto operator*(const uint128_fallback& lhs, uint32_t rhs)
+ -> uint128_fallback {
+ FMT_ASSERT(lhs.hi_ == 0, "");
+ uint64_t hi = (lhs.lo_ >> 32) * rhs;
+ uint64_t lo = (lhs.lo_ & ~uint32_t()) * rhs;
+ uint64_t new_lo = (hi << 32) + lo;
+ return {(hi >> 32) + (new_lo < lo ? 1 : 0), new_lo};
+ }
+ friend auto operator-(const uint128_fallback& lhs, uint64_t rhs)
+ -> uint128_fallback {
+ return {lhs.hi_ - (lhs.lo_ < rhs ? 1 : 0), lhs.lo_ - rhs};
+ }
+ FMT_CONSTEXPR auto operator>>(int shift) const -> uint128_fallback {
+ if (shift == 64) return {0, hi_};
+ if (shift > 64) return uint128_fallback(0, hi_) >> (shift - 64);
+ return {hi_ >> shift, (hi_ << (64 - shift)) | (lo_ >> shift)};
+ }
+ FMT_CONSTEXPR auto operator<<(int shift) const -> uint128_fallback {
+ if (shift == 64) return {lo_, 0};
+ if (shift > 64) return uint128_fallback(lo_, 0) << (shift - 64);
+ return {hi_ << shift | (lo_ >> (64 - shift)), (lo_ << shift)};
+ }
+ FMT_CONSTEXPR auto operator>>=(int shift) -> uint128_fallback& {
+ return *this = *this >> shift;
+ }
+ FMT_CONSTEXPR void operator+=(uint128_fallback n) {
+ uint64_t new_lo = lo_ + n.lo_;
+ uint64_t new_hi = hi_ + n.hi_ + (new_lo < lo_ ? 1 : 0);
+ FMT_ASSERT(new_hi >= hi_, "");
+ lo_ = new_lo;
+ hi_ = new_hi;
+ }
+ FMT_CONSTEXPR void operator&=(uint128_fallback n) {
+ lo_ &= n.lo_;
+ hi_ &= n.hi_;
+ }
+
+ FMT_CONSTEXPR20 uint128_fallback& operator+=(uint64_t n) noexcept {
+ if (is_constant_evaluated()) {
+ lo_ += n;
+ hi_ += (lo_ < n ? 1 : 0);
+ return *this;
+ }
+#if FMT_HAS_BUILTIN(__builtin_addcll) && !defined(__ibmxl__)
+ unsigned long long carry;
+ lo_ = __builtin_addcll(lo_, n, 0, &carry);
+ hi_ += carry;
+#elif FMT_HAS_BUILTIN(__builtin_ia32_addcarryx_u64) && !defined(__ibmxl__)
+ unsigned long long result;
+ auto carry = __builtin_ia32_addcarryx_u64(0, lo_, n, &result);
+ lo_ = result;
+ hi_ += carry;
+#elif defined(_MSC_VER) && defined(_M_X64)
+ auto carry = _addcarry_u64(0, lo_, n, &lo_);
+ _addcarry_u64(carry, hi_, 0, &hi_);
+#else
+ lo_ += n;
+ hi_ += (lo_ < n ? 1 : 0);
+#endif
+ return *this;
+ }
+};
+
+using uint128_t = conditional_t<FMT_USE_INT128, uint128_opt, uint128_fallback>;
+
+#ifdef UINTPTR_MAX
+using uintptr_t = ::uintptr_t;
+#else
+using uintptr_t = uint128_t;
+#endif
+
+// Returns the largest possible value for type T. Same as
+// std::numeric_limits<T>::max() but shorter and not affected by the max macro.
+template <typename T> constexpr auto max_value() -> T {
+ return (std::numeric_limits<T>::max)();
+}
+template <typename T> constexpr auto num_bits() -> int {
+ return std::numeric_limits<T>::digits;
+}
+// std::numeric_limits<T>::digits may return 0 for 128-bit ints.
+template <> constexpr auto num_bits<int128_opt>() -> int { return 128; }
+template <> constexpr auto num_bits<uint128_t>() -> int { return 128; }
+
+// A heterogeneous bit_cast used for converting 96-bit long double to uint128_t
+// and 128-bit pointers to uint128_fallback.
+template <typename To, typename From, FMT_ENABLE_IF(sizeof(To) > sizeof(From))>
+inline auto bit_cast(const From& from) -> To {
+ constexpr auto size = static_cast<int>(sizeof(From) / sizeof(unsigned));
+ struct data_t {
+ unsigned value[static_cast<unsigned>(size)];
+ } data = bit_cast<data_t>(from);
+ auto result = To();
+ if (const_check(is_big_endian())) {
+ for (int i = 0; i < size; ++i)
+ result = (result << num_bits<unsigned>()) | data.value[i];
+ } else {
+ for (int i = size - 1; i >= 0; --i)
+ result = (result << num_bits<unsigned>()) | data.value[i];
+ }
+ return result;
+}
+
+template <typename UInt>
+FMT_CONSTEXPR20 inline auto countl_zero_fallback(UInt n) -> int {
+ int lz = 0;
+ constexpr UInt msb_mask = static_cast<UInt>(1) << (num_bits<UInt>() - 1);
+ for (; (n & msb_mask) == 0; n <<= 1) lz++;
+ return lz;
+}
+
+FMT_CONSTEXPR20 inline auto countl_zero(uint32_t n) -> int {
+#ifdef FMT_BUILTIN_CLZ
+ if (!is_constant_evaluated()) return FMT_BUILTIN_CLZ(n);
+#endif
+ return countl_zero_fallback(n);
+}
+
+FMT_CONSTEXPR20 inline auto countl_zero(uint64_t n) -> int {
+#ifdef FMT_BUILTIN_CLZLL
+ if (!is_constant_evaluated()) return FMT_BUILTIN_CLZLL(n);
+#endif
+ return countl_zero_fallback(n);
+}
+
+FMT_INLINE void assume(bool condition) {
+ (void)condition;
+#if FMT_HAS_BUILTIN(__builtin_assume) && !FMT_ICC_VERSION
+ __builtin_assume(condition);
+#endif
+}
+
+// An approximation of iterator_t for pre-C++20 systems.
+template <typename T>
+using iterator_t = decltype(std::begin(std::declval<T&>()));
+template <typename T> using sentinel_t = decltype(std::end(std::declval<T&>()));
+
+// A workaround for std::string not having mutable data() until C++17.
+template <typename Char>
+inline auto get_data(std::basic_string<Char>& s) -> Char* {
+ return &s[0];
+}
+template <typename Container>
+inline auto get_data(Container& c) -> typename Container::value_type* {
+ return c.data();
+}
+
+#if defined(_SECURE_SCL) && _SECURE_SCL
+// Make a checked iterator to avoid MSVC warnings.
+template <typename T> using checked_ptr = stdext::checked_array_iterator<T*>;
+template <typename T>
+constexpr auto make_checked(T* p, size_t size) -> checked_ptr<T> {
+ return {p, size};
+}
+#else
+template <typename T> using checked_ptr = T*;
+template <typename T> constexpr auto make_checked(T* p, size_t) -> T* {
+ return p;
+}
+#endif
+
+// Attempts to reserve space for n extra characters in the output range.
+// Returns a pointer to the reserved range or a reference to it.
+template <typename Container, FMT_ENABLE_IF(is_contiguous<Container>::value)>
+#if FMT_CLANG_VERSION >= 307 && !FMT_ICC_VERSION
+__attribute__((no_sanitize("undefined")))
+#endif
+inline auto
+reserve(std::back_insert_iterator<Container> it, size_t n)
+ -> checked_ptr<typename Container::value_type> {
+ Container& c = get_container(it);
+ size_t size = c.size();
+ c.resize(size + n);
+ return make_checked(get_data(c) + size, n);
+}
+
+template <typename T>
+inline auto reserve(buffer_appender<T> it, size_t n) -> buffer_appender<T> {
+ buffer<T>& buf = get_container(it);
+ buf.try_reserve(buf.size() + n);
+ return it;
+}
+
+template <typename Iterator>
+constexpr auto reserve(Iterator& it, size_t) -> Iterator& {
+ return it;
+}
+
+template <typename OutputIt>
+using reserve_iterator =
+ remove_reference_t<decltype(reserve(std::declval<OutputIt&>(), 0))>;
+
+template <typename T, typename OutputIt>
+constexpr auto to_pointer(OutputIt, size_t) -> T* {
+ return nullptr;
+}
+template <typename T> auto to_pointer(buffer_appender<T> it, size_t n) -> T* {
+ buffer<T>& buf = get_container(it);
+ auto size = buf.size();
+ if (buf.capacity() < size + n) return nullptr;
+ buf.try_resize(size + n);
+ return buf.data() + size;
+}
+
+template <typename Container, FMT_ENABLE_IF(is_contiguous<Container>::value)>
+inline auto base_iterator(std::back_insert_iterator<Container>& it,
+ checked_ptr<typename Container::value_type>)
+ -> std::back_insert_iterator<Container> {
+ return it;
+}
+
+template <typename Iterator>
+constexpr auto base_iterator(Iterator, Iterator it) -> Iterator {
+ return it;
+}
+
+// <algorithm> is spectacularly slow to compile in C++20 so use a simple fill_n
+// instead (#1998).
+template <typename OutputIt, typename Size, typename T>
+FMT_CONSTEXPR auto fill_n(OutputIt out, Size count, const T& value)
+ -> OutputIt {
+ for (Size i = 0; i < count; ++i) *out++ = value;
+ return out;
+}
+template <typename T, typename Size>
+FMT_CONSTEXPR20 auto fill_n(T* out, Size count, char value) -> T* {
+ if (is_constant_evaluated()) {
+ return fill_n<T*, Size, T>(out, count, value);
+ }
+ std::memset(out, value, to_unsigned(count));
+ return out + count;
+}
+
+#ifdef __cpp_char8_t
+using char8_type = char8_t;
+#else
+enum char8_type : unsigned char {};
+#endif
+
+template <typename OutChar, typename InputIt, typename OutputIt>
+FMT_CONSTEXPR FMT_NOINLINE auto copy_str_noinline(InputIt begin, InputIt end,
+ OutputIt out) -> OutputIt {
+ return copy_str<OutChar>(begin, end, out);
+}
+
+// A public domain branchless UTF-8 decoder by Christopher Wellons:
+// https://github.com/skeeto/branchless-utf8
+/* Decode the next character, c, from s, reporting errors in e.
+ *
+ * Since this is a branchless decoder, four bytes will be read from the
+ * buffer regardless of the actual length of the next character. This
+ * means the buffer _must_ have at least three bytes of zero padding
+ * following the end of the data stream.
+ *
+ * Errors are reported in e, which will be non-zero if the parsed
+ * character was somehow invalid: invalid byte sequence, non-canonical
+ * encoding, or a surrogate half.
+ *
+ * The function returns a pointer to the next character. When an error
+ * occurs, this pointer will be a guess that depends on the particular
+ * error, but it will always advance at least one byte.
+ */
+FMT_CONSTEXPR inline auto utf8_decode(const char* s, uint32_t* c, int* e)
+ -> const char* {
+ constexpr const int masks[] = {0x00, 0x7f, 0x1f, 0x0f, 0x07};
+ constexpr const uint32_t mins[] = {4194304, 0, 128, 2048, 65536};
+ constexpr const int shiftc[] = {0, 18, 12, 6, 0};
+ constexpr const int shifte[] = {0, 6, 4, 2, 0};
+
+ int len = "\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\1\0\0\0\0\0\0\0\0\2\2\2\2\3\3\4"
+ [static_cast<unsigned char>(*s) >> 3];
+ // Compute the pointer to the next character early so that the next
+ // iteration can start working on the next character. Neither Clang
+ // nor GCC figure out this reordering on their own.
+ const char* next = s + len + !len;
+
+ using uchar = unsigned char;
+
+ // Assume a four-byte character and load four bytes. Unused bits are
+ // shifted out.
+ *c = uint32_t(uchar(s[0]) & masks[len]) << 18;
+ *c |= uint32_t(uchar(s[1]) & 0x3f) << 12;
+ *c |= uint32_t(uchar(s[2]) & 0x3f) << 6;
+ *c |= uint32_t(uchar(s[3]) & 0x3f) << 0;
+ *c >>= shiftc[len];
+
+ // Accumulate the various error conditions.
+ *e = (*c < mins[len]) << 6; // non-canonical encoding
+ *e |= ((*c >> 11) == 0x1b) << 7; // surrogate half?
+ *e |= (*c > 0x10FFFF) << 8; // out of range?
+ *e |= (uchar(s[1]) & 0xc0) >> 2;
+ *e |= (uchar(s[2]) & 0xc0) >> 4;
+ *e |= uchar(s[3]) >> 6;
+ *e ^= 0x2a; // top two bits of each tail byte correct?
+ *e >>= shifte[len];
+
+ return next;
+}
+
+constexpr FMT_INLINE_VARIABLE uint32_t invalid_code_point = ~uint32_t();
+
+// Invokes f(cp, sv) for every code point cp in s with sv being the string view
+// corresponding to the code point. cp is invalid_code_point on error.
+template <typename F>
+FMT_CONSTEXPR void for_each_codepoint(string_view s, F f) {
+ auto decode = [f](const char* buf_ptr, const char* ptr) {
+ auto cp = uint32_t();
+ auto error = 0;
+ auto end = utf8_decode(buf_ptr, &cp, &error);
+ bool result = f(error ? invalid_code_point : cp,
+ string_view(ptr, error ? 1 : to_unsigned(end - buf_ptr)));
+ return result ? (error ? buf_ptr + 1 : end) : nullptr;
+ };
+ auto p = s.data();
+ const size_t block_size = 4; // utf8_decode always reads blocks of 4 chars.
+ if (s.size() >= block_size) {
+ for (auto end = p + s.size() - block_size + 1; p < end;) {
+ p = decode(p, p);
+ if (!p) return;
+ }
+ }
+ if (auto num_chars_left = s.data() + s.size() - p) {
+ char buf[2 * block_size - 1] = {};
+ copy_str<char>(p, p + num_chars_left, buf);
+ const char* buf_ptr = buf;
+ do {
+ auto end = decode(buf_ptr, p);
+ if (!end) return;
+ p += end - buf_ptr;
+ buf_ptr = end;
+ } while (buf_ptr - buf < num_chars_left);
+ }
+}
+
+template <typename Char>
+inline auto compute_width(basic_string_view<Char> s) -> size_t {
+ return s.size();
+}
+
+// Computes approximate display width of a UTF-8 string.
+FMT_CONSTEXPR inline size_t compute_width(string_view s) {
+ size_t num_code_points = 0;
+ // It is not a lambda for compatibility with C++14.
+ struct count_code_points {
+ size_t* count;
+ FMT_CONSTEXPR auto operator()(uint32_t cp, string_view) const -> bool {
+ *count += detail::to_unsigned(
+ 1 +
+ (cp >= 0x1100 &&
+ (cp <= 0x115f || // Hangul Jamo init. consonants
+ cp == 0x2329 || // LEFT-POINTING ANGLE BRACKET
+ cp == 0x232a || // RIGHT-POINTING ANGLE BRACKET
+ // CJK ... Yi except IDEOGRAPHIC HALF FILL SPACE:
+ (cp >= 0x2e80 && cp <= 0xa4cf && cp != 0x303f) ||
+ (cp >= 0xac00 && cp <= 0xd7a3) || // Hangul Syllables
+ (cp >= 0xf900 && cp <= 0xfaff) || // CJK Compatibility Ideographs
+ (cp >= 0xfe10 && cp <= 0xfe19) || // Vertical Forms
+ (cp >= 0xfe30 && cp <= 0xfe6f) || // CJK Compatibility Forms
+ (cp >= 0xff00 && cp <= 0xff60) || // Fullwidth Forms
+ (cp >= 0xffe0 && cp <= 0xffe6) || // Fullwidth Forms
+ (cp >= 0x20000 && cp <= 0x2fffd) || // CJK
+ (cp >= 0x30000 && cp <= 0x3fffd) ||
+ // Miscellaneous Symbols and Pictographs + Emoticons:
+ (cp >= 0x1f300 && cp <= 0x1f64f) ||
+ // Supplemental Symbols and Pictographs:
+ (cp >= 0x1f900 && cp <= 0x1f9ff))));
+ return true;
+ }
+ };
+ // We could avoid branches by using utf8_decode directly.
+ for_each_codepoint(s, count_code_points{&num_code_points});
+ return num_code_points;
+}
+
+inline auto compute_width(basic_string_view<char8_type> s) -> size_t {
+ return compute_width(
+ string_view(reinterpret_cast<const char*>(s.data()), s.size()));
+}
+
+template <typename Char>
+inline auto code_point_index(basic_string_view<Char> s, size_t n) -> size_t {
+ size_t size = s.size();
+ return n < size ? n : size;
+}
+
+// Calculates the index of the nth code point in a UTF-8 string.
+inline auto code_point_index(string_view s, size_t n) -> size_t {
+ const char* data = s.data();
+ size_t num_code_points = 0;
+ for (size_t i = 0, size = s.size(); i != size; ++i) {
+ if ((data[i] & 0xc0) != 0x80 && ++num_code_points > n) return i;
+ }
+ return s.size();
+}
+
+inline auto code_point_index(basic_string_view<char8_type> s, size_t n)
+ -> size_t {
+ return code_point_index(
+ string_view(reinterpret_cast<const char*>(s.data()), s.size()), n);
+}
+
+template <typename T> struct is_integral : std::is_integral<T> {};
+template <> struct is_integral<int128_opt> : std::true_type {};
+template <> struct is_integral<uint128_t> : std::true_type {};
+
+template <typename T>
+using is_signed =
+ std::integral_constant<bool, std::numeric_limits<T>::is_signed ||
+ std::is_same<T, int128_opt>::value>;
+
+template <typename T>
+using is_integer =
+ bool_constant<is_integral<T>::value && !std::is_same<T, bool>::value &&
+ !std::is_same<T, char>::value &&
+ !std::is_same<T, wchar_t>::value>;
+
+#ifndef FMT_USE_FLOAT
+# define FMT_USE_FLOAT 1
+#endif
+#ifndef FMT_USE_DOUBLE
+# define FMT_USE_DOUBLE 1
+#endif
+#ifndef FMT_USE_LONG_DOUBLE
+# define FMT_USE_LONG_DOUBLE 1
+#endif
+
+#ifndef FMT_USE_FLOAT128
+# ifdef __clang__
+// Clang emulates GCC, so it has to appear early.
+# if FMT_HAS_INCLUDE(<quadmath.h>)
+# define FMT_USE_FLOAT128 1
+# endif
+# elif defined(__GNUC__)
+// GNU C++:
+# if defined(_GLIBCXX_USE_FLOAT128) && !defined(__STRICT_ANSI__)
+# define FMT_USE_FLOAT128 1
+# endif
+# endif
+# ifndef FMT_USE_FLOAT128
+# define FMT_USE_FLOAT128 0
+# endif
+#endif
+
+#if FMT_USE_FLOAT128
+using float128 = __float128;
+#else
+using float128 = void;
+#endif
+template <typename T> using is_float128 = std::is_same<T, float128>;
+
+template <typename T>
+using is_floating_point =
+ bool_constant<std::is_floating_point<T>::value || is_float128<T>::value>;
+
+template <typename T, bool = std::is_floating_point<T>::value>
+struct is_fast_float : bool_constant<std::numeric_limits<T>::is_iec559 &&
+ sizeof(T) <= sizeof(double)> {};
+template <typename T> struct is_fast_float<T, false> : std::false_type {};
+
+template <typename T>
+using is_double_double = bool_constant<std::numeric_limits<T>::digits == 106>;
+
+#ifndef FMT_USE_FULL_CACHE_DRAGONBOX
+# define FMT_USE_FULL_CACHE_DRAGONBOX 0
+#endif
+
+template <typename T>
+template <typename U>
+void buffer<T>::append(const U* begin, const U* end) {
+ while (begin != end) {
+ auto count = to_unsigned(end - begin);
+ try_reserve(size_ + count);
+ auto free_cap = capacity_ - size_;
+ if (free_cap < count) count = free_cap;
+ std::uninitialized_copy_n(begin, count, make_checked(ptr_ + size_, count));
+ size_ += count;
+ begin += count;
+ }
+}
+
+template <typename T, typename Enable = void>
+struct is_locale : std::false_type {};
+template <typename T>
+struct is_locale<T, void_t<decltype(T::classic())>> : std::true_type {};
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+// The number of characters to store in the basic_memory_buffer object itself
+// to avoid dynamic memory allocation.
+enum { inline_buffer_size = 500 };
+
+/**
+ \rst
+ A dynamically growing memory buffer for trivially copyable/constructible types
+ with the first ``SIZE`` elements stored in the object itself.
+
+ You can use the ``memory_buffer`` type alias for ``char`` instead.
+
+ **Example**::
+
+ auto out = fmt::memory_buffer();
+ format_to(std::back_inserter(out), "The answer is {}.", 42);
+
+ This will append the following output to the ``out`` object:
+
+ .. code-block:: none
+
+ The answer is 42.
+
+ The output can be converted to an ``std::string`` with ``to_string(out)``.
+ \endrst
+ */
+template <typename T, size_t SIZE = inline_buffer_size,
+ typename Allocator = std::allocator<T>>
+class basic_memory_buffer final : public detail::buffer<T> {
+ private:
+ T store_[SIZE];
+
+ // Don't inherit from Allocator avoid generating type_info for it.
+ Allocator alloc_;
+
+ // Deallocate memory allocated by the buffer.
+ FMT_CONSTEXPR20 void deallocate() {
+ T* data = this->data();
+ if (data != store_) alloc_.deallocate(data, this->capacity());
+ }
+
+ protected:
+ FMT_CONSTEXPR20 void grow(size_t size) override {
+ detail::abort_fuzzing_if(size > 5000);
+ const size_t max_size = std::allocator_traits<Allocator>::max_size(alloc_);
+ size_t old_capacity = this->capacity();
+ size_t new_capacity = old_capacity + old_capacity / 2;
+ if (size > new_capacity)
+ new_capacity = size;
+ else if (new_capacity > max_size)
+ new_capacity = size > max_size ? size : max_size;
+ T* old_data = this->data();
+ T* new_data =
+ std::allocator_traits<Allocator>::allocate(alloc_, new_capacity);
+ // The following code doesn't throw, so the raw pointer above doesn't leak.
+ std::uninitialized_copy(old_data, old_data + this->size(),
+ detail::make_checked(new_data, new_capacity));
+ this->set(new_data, new_capacity);
+ // deallocate must not throw according to the standard, but even if it does,
+ // the buffer already uses the new storage and will deallocate it in
+ // destructor.
+ if (old_data != store_) alloc_.deallocate(old_data, old_capacity);
+ }
+
+ public:
+ using value_type = T;
+ using const_reference = const T&;
+
+ FMT_CONSTEXPR20 explicit basic_memory_buffer(
+ const Allocator& alloc = Allocator())
+ : alloc_(alloc) {
+ this->set(store_, SIZE);
+ if (detail::is_constant_evaluated()) detail::fill_n(store_, SIZE, T());
+ }
+ FMT_CONSTEXPR20 ~basic_memory_buffer() { deallocate(); }
+
+ private:
+ // Move data from other to this buffer.
+ FMT_CONSTEXPR20 void move(basic_memory_buffer& other) {
+ alloc_ = std::move(other.alloc_);
+ T* data = other.data();
+ size_t size = other.size(), capacity = other.capacity();
+ if (data == other.store_) {
+ this->set(store_, capacity);
+ detail::copy_str<T>(other.store_, other.store_ + size,
+ detail::make_checked(store_, capacity));
+ } else {
+ this->set(data, capacity);
+ // Set pointer to the inline array so that delete is not called
+ // when deallocating.
+ other.set(other.store_, 0);
+ other.clear();
+ }
+ this->resize(size);
+ }
+
+ public:
+ /**
+ \rst
+ Constructs a :class:`fmt::basic_memory_buffer` object moving the content
+ of the other object to it.
+ \endrst
+ */
+ FMT_CONSTEXPR20 basic_memory_buffer(basic_memory_buffer&& other) noexcept {
+ move(other);
+ }
+
+ /**
+ \rst
+ Moves the content of the other ``basic_memory_buffer`` object to this one.
+ \endrst
+ */
+ auto operator=(basic_memory_buffer&& other) noexcept -> basic_memory_buffer& {
+ FMT_ASSERT(this != &other, "");
+ deallocate();
+ move(other);
+ return *this;
+ }
+
+ // Returns a copy of the allocator associated with this buffer.
+ auto get_allocator() const -> Allocator { return alloc_; }
+
+ /**
+ Resizes the buffer to contain *count* elements. If T is a POD type new
+ elements may not be initialized.
+ */
+ FMT_CONSTEXPR20 void resize(size_t count) { this->try_resize(count); }
+
+ /** Increases the buffer capacity to *new_capacity*. */
+ void reserve(size_t new_capacity) { this->try_reserve(new_capacity); }
+
+ // Directly append data into the buffer
+ using detail::buffer<T>::append;
+ template <typename ContiguousRange>
+ void append(const ContiguousRange& range) {
+ append(range.data(), range.data() + range.size());
+ }
+};
+
+using memory_buffer = basic_memory_buffer<char>;
+
+template <typename T, size_t SIZE, typename Allocator>
+struct is_contiguous<basic_memory_buffer<T, SIZE, Allocator>> : std::true_type {
+};
+
+FMT_END_EXPORT
+namespace detail {
+FMT_API bool write_console(std::FILE* f, string_view text);
+FMT_API void print(std::FILE*, string_view);
+} // namespace detail
+FMT_BEGIN_EXPORT
+
+// Suppress a misleading warning in older versions of clang.
+#if FMT_CLANG_VERSION
+# pragma clang diagnostic ignored "-Wweak-vtables"
+#endif
+
+/** An error reported from a formatting function. */
+class FMT_API format_error : public std::runtime_error {
+ public:
+ using std::runtime_error::runtime_error;
+};
+
+namespace detail_exported {
+#if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <typename Char, size_t N> struct fixed_string {
+ constexpr fixed_string(const Char (&str)[N]) {
+ detail::copy_str<Char, const Char*, Char*>(static_cast<const Char*>(str),
+ str + N, data);
+ }
+ Char data[N] = {};
+};
+#endif
+
+// Converts a compile-time string to basic_string_view.
+template <typename Char, size_t N>
+constexpr auto compile_string_to_view(const Char (&s)[N])
+ -> basic_string_view<Char> {
+ // Remove trailing NUL character if needed. Won't be present if this is used
+ // with a raw character array (i.e. not defined as a string).
+ return {s, N - (std::char_traits<Char>::to_int_type(s[N - 1]) == 0 ? 1 : 0)};
+}
+template <typename Char>
+constexpr auto compile_string_to_view(detail::std_string_view<Char> s)
+ -> basic_string_view<Char> {
+ return {s.data(), s.size()};
+}
+} // namespace detail_exported
+
+class loc_value {
+ private:
+ basic_format_arg<format_context> value_;
+
+ public:
+ template <typename T, FMT_ENABLE_IF(!detail::is_float128<T>::value)>
+ loc_value(T value) : value_(detail::make_arg<format_context>(value)) {}
+
+ template <typename T, FMT_ENABLE_IF(detail::is_float128<T>::value)>
+ loc_value(T) {}
+
+ template <typename Visitor> auto visit(Visitor&& vis) -> decltype(vis(0)) {
+ return visit_format_arg(vis, value_);
+ }
+};
+
+// A locale facet that formats values in UTF-8.
+// It is parameterized on the locale to avoid the heavy <locale> include.
+template <typename Locale> class format_facet : public Locale::facet {
+ private:
+ std::string separator_;
+ std::string grouping_;
+ std::string decimal_point_;
+
+ protected:
+ virtual auto do_put(appender out, loc_value val,
+ const format_specs<>& specs) const -> bool;
+
+ public:
+ static FMT_API typename Locale::id id;
+
+ explicit format_facet(Locale& loc);
+ explicit format_facet(string_view sep = "",
+ std::initializer_list<unsigned char> g = {3},
+ std::string decimal_point = ".")
+ : separator_(sep.data(), sep.size()),
+ grouping_(g.begin(), g.end()),
+ decimal_point_(decimal_point) {}
+
+ auto put(appender out, loc_value val, const format_specs<>& specs) const
+ -> bool {
+ return do_put(out, val, specs);
+ }
+};
+
+FMT_BEGIN_DETAIL_NAMESPACE
+
+// Returns true if value is negative, false otherwise.
+// Same as `value < 0` but doesn't produce warnings if T is an unsigned type.
+template <typename T, FMT_ENABLE_IF(is_signed<T>::value)>
+constexpr auto is_negative(T value) -> bool {
+ return value < 0;
+}
+template <typename T, FMT_ENABLE_IF(!is_signed<T>::value)>
+constexpr auto is_negative(T) -> bool {
+ return false;
+}
+
+template <typename T>
+FMT_CONSTEXPR auto is_supported_floating_point(T) -> bool {
+ if (std::is_same<T, float>()) return FMT_USE_FLOAT;
+ if (std::is_same<T, double>()) return FMT_USE_DOUBLE;
+ if (std::is_same<T, long double>()) return FMT_USE_LONG_DOUBLE;
+ return true;
+}
+
+// Smallest of uint32_t, uint64_t, uint128_t that is large enough to
+// represent all values of an integral type T.
+template <typename T>
+using uint32_or_64_or_128_t =
+ conditional_t<num_bits<T>() <= 32 && !FMT_REDUCE_INT_INSTANTIATIONS,
+ uint32_t,
+ conditional_t<num_bits<T>() <= 64, uint64_t, uint128_t>>;
+template <typename T>
+using uint64_or_128_t = conditional_t<num_bits<T>() <= 64, uint64_t, uint128_t>;
+
+#define FMT_POWERS_OF_10(factor) \
+ factor * 10, (factor)*100, (factor)*1000, (factor)*10000, (factor)*100000, \
+ (factor)*1000000, (factor)*10000000, (factor)*100000000, \
+ (factor)*1000000000
+
+// Converts value in the range [0, 100) to a string.
+constexpr const char* digits2(size_t value) {
+ // GCC generates slightly better code when value is pointer-size.
+ return &"0001020304050607080910111213141516171819"
+ "2021222324252627282930313233343536373839"
+ "4041424344454647484950515253545556575859"
+ "6061626364656667686970717273747576777879"
+ "8081828384858687888990919293949596979899"[value * 2];
+}
+
+// Sign is a template parameter to workaround a bug in gcc 4.8.
+template <typename Char, typename Sign> constexpr Char sign(Sign s) {
+#if !FMT_GCC_VERSION || FMT_GCC_VERSION >= 604
+ static_assert(std::is_same<Sign, sign_t>::value, "");
+#endif
+ return static_cast<Char>("\0-+ "[s]);
+}
+
+template <typename T> FMT_CONSTEXPR auto count_digits_fallback(T n) -> int {
+ int count = 1;
+ for (;;) {
+ // Integer division is slow so do it for a group of four digits instead
+ // of for every digit. The idea comes from the talk by Alexandrescu
+ // "Three Optimization Tips for C++". See speed-test for a comparison.
+ if (n < 10) return count;
+ if (n < 100) return count + 1;
+ if (n < 1000) return count + 2;
+ if (n < 10000) return count + 3;
+ n /= 10000u;
+ count += 4;
+ }
+}
+#if FMT_USE_INT128
+FMT_CONSTEXPR inline auto count_digits(uint128_opt n) -> int {
+ return count_digits_fallback(n);
+}
+#endif
+
+#ifdef FMT_BUILTIN_CLZLL
+// It is a separate function rather than a part of count_digits to workaround
+// the lack of static constexpr in constexpr functions.
+inline auto do_count_digits(uint64_t n) -> int {
+ // This has comparable performance to the version by Kendall Willets
+ // (https://github.com/fmtlib/format-benchmark/blob/master/digits10)
+ // but uses smaller tables.
+ // Maps bsr(n) to ceil(log10(pow(2, bsr(n) + 1) - 1)).
+ static constexpr uint8_t bsr2log10[] = {
+ 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
+ 6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10,
+ 10, 11, 11, 11, 12, 12, 12, 13, 13, 13, 13, 14, 14, 14, 15, 15,
+ 15, 16, 16, 16, 16, 17, 17, 17, 18, 18, 18, 19, 19, 19, 19, 20};
+ auto t = bsr2log10[FMT_BUILTIN_CLZLL(n | 1) ^ 63];
+ static constexpr const uint64_t zero_or_powers_of_10[] = {
+ 0, 0, FMT_POWERS_OF_10(1U), FMT_POWERS_OF_10(1000000000ULL),
+ 10000000000000000000ULL};
+ return t - (n < zero_or_powers_of_10[t]);
+}
+#endif
+
+// Returns the number of decimal digits in n. Leading zeros are not counted
+// except for n == 0 in which case count_digits returns 1.
+FMT_CONSTEXPR20 inline auto count_digits(uint64_t n) -> int {
+#ifdef FMT_BUILTIN_CLZLL
+ if (!is_constant_evaluated()) {
+ return do_count_digits(n);
+ }
+#endif
+ return count_digits_fallback(n);
+}
+
+// Counts the number of digits in n. BITS = log2(radix).
+template <int BITS, typename UInt>
+FMT_CONSTEXPR auto count_digits(UInt n) -> int {
+#ifdef FMT_BUILTIN_CLZ
+ if (!is_constant_evaluated() && num_bits<UInt>() == 32)
+ return (FMT_BUILTIN_CLZ(static_cast<uint32_t>(n) | 1) ^ 31) / BITS + 1;
+#endif
+ // Lambda avoids unreachable code warnings from NVHPC.
+ return [](UInt m) {
+ int num_digits = 0;
+ do {
+ ++num_digits;
+ } while ((m >>= BITS) != 0);
+ return num_digits;
+ }(n);
+}
+
+#ifdef FMT_BUILTIN_CLZ
+// It is a separate function rather than a part of count_digits to workaround
+// the lack of static constexpr in constexpr functions.
+FMT_INLINE auto do_count_digits(uint32_t n) -> int {
+// An optimization by Kendall Willets from https://bit.ly/3uOIQrB.
+// This increments the upper 32 bits (log10(T) - 1) when >= T is added.
+# define FMT_INC(T) (((sizeof(# T) - 1ull) << 32) - T)
+ static constexpr uint64_t table[] = {
+ FMT_INC(0), FMT_INC(0), FMT_INC(0), // 8
+ FMT_INC(10), FMT_INC(10), FMT_INC(10), // 64
+ FMT_INC(100), FMT_INC(100), FMT_INC(100), // 512
+ FMT_INC(1000), FMT_INC(1000), FMT_INC(1000), // 4096
+ FMT_INC(10000), FMT_INC(10000), FMT_INC(10000), // 32k
+ FMT_INC(100000), FMT_INC(100000), FMT_INC(100000), // 256k
+ FMT_INC(1000000), FMT_INC(1000000), FMT_INC(1000000), // 2048k
+ FMT_INC(10000000), FMT_INC(10000000), FMT_INC(10000000), // 16M
+ FMT_INC(100000000), FMT_INC(100000000), FMT_INC(100000000), // 128M
+ FMT_INC(1000000000), FMT_INC(1000000000), FMT_INC(1000000000), // 1024M
+ FMT_INC(1000000000), FMT_INC(1000000000) // 4B
+ };
+ auto inc = table[FMT_BUILTIN_CLZ(n | 1) ^ 31];
+ return static_cast<int>((n + inc) >> 32);
+}
+#endif
+
+// Optional version of count_digits for better performance on 32-bit platforms.
+FMT_CONSTEXPR20 inline auto count_digits(uint32_t n) -> int {
+#ifdef FMT_BUILTIN_CLZ
+ if (!is_constant_evaluated()) {
+ return do_count_digits(n);
+ }
+#endif
+ return count_digits_fallback(n);
+}
+
+template <typename Int> constexpr auto digits10() noexcept -> int {
+ return std::numeric_limits<Int>::digits10;
+}
+template <> constexpr auto digits10<int128_opt>() noexcept -> int { return 38; }
+template <> constexpr auto digits10<uint128_t>() noexcept -> int { return 38; }
+
+template <typename Char> struct thousands_sep_result {
+ std::string grouping;
+ Char thousands_sep;
+};
+
+template <typename Char>
+FMT_API auto thousands_sep_impl(locale_ref loc) -> thousands_sep_result<Char>;
+template <typename Char>
+inline auto thousands_sep(locale_ref loc) -> thousands_sep_result<Char> {
+ auto result = thousands_sep_impl<char>(loc);
+ return {result.grouping, Char(result.thousands_sep)};
+}
+template <>
+inline auto thousands_sep(locale_ref loc) -> thousands_sep_result<wchar_t> {
+ return thousands_sep_impl<wchar_t>(loc);
+}
+
+template <typename Char>
+FMT_API auto decimal_point_impl(locale_ref loc) -> Char;
+template <typename Char> inline auto decimal_point(locale_ref loc) -> Char {
+ return Char(decimal_point_impl<char>(loc));
+}
+template <> inline auto decimal_point(locale_ref loc) -> wchar_t {
+ return decimal_point_impl<wchar_t>(loc);
+}
+
+// Compares two characters for equality.
+template <typename Char> auto equal2(const Char* lhs, const char* rhs) -> bool {
+ return lhs[0] == Char(rhs[0]) && lhs[1] == Char(rhs[1]);
+}
+inline auto equal2(const char* lhs, const char* rhs) -> bool {
+ return memcmp(lhs, rhs, 2) == 0;
+}
+
+// Copies two characters from src to dst.
+template <typename Char>
+FMT_CONSTEXPR20 FMT_INLINE void copy2(Char* dst, const char* src) {
+ if (!is_constant_evaluated() && sizeof(Char) == sizeof(char)) {
+ memcpy(dst, src, 2);
+ return;
+ }
+ *dst++ = static_cast<Char>(*src++);
+ *dst = static_cast<Char>(*src);
+}
+
+template <typename Iterator> struct format_decimal_result {
+ Iterator begin;
+ Iterator end;
+};
+
+// Formats a decimal unsigned integer value writing into out pointing to a
+// buffer of specified size. The caller must ensure that the buffer is large
+// enough.
+template <typename Char, typename UInt>
+FMT_CONSTEXPR20 auto format_decimal(Char* out, UInt value, int size)
+ -> format_decimal_result<Char*> {
+ FMT_ASSERT(size >= count_digits(value), "invalid digit count");
+ out += size;
+ Char* end = out;
+ while (value >= 100) {
+ // Integer division is slow so do it for a group of two digits instead
+ // of for every digit. The idea comes from the talk by Alexandrescu
+ // "Three Optimization Tips for C++". See speed-test for a comparison.
+ out -= 2;
+ copy2(out, digits2(static_cast<size_t>(value % 100)));
+ value /= 100;
+ }
+ if (value < 10) {
+ *--out = static_cast<Char>('0' + value);
+ return {out, end};
+ }
+ out -= 2;
+ copy2(out, digits2(static_cast<size_t>(value)));
+ return {out, end};
+}
+
+template <typename Char, typename UInt, typename Iterator,
+ FMT_ENABLE_IF(!std::is_pointer<remove_cvref_t<Iterator>>::value)>
+FMT_CONSTEXPR inline auto format_decimal(Iterator out, UInt value, int size)
+ -> format_decimal_result<Iterator> {
+ // Buffer is large enough to hold all digits (digits10 + 1).
+ Char buffer[digits10<UInt>() + 1] = {};
+ auto end = format_decimal(buffer, value, size).end;
+ return {out, detail::copy_str_noinline<Char>(buffer, end, out)};
+}
+
+template <unsigned BASE_BITS, typename Char, typename UInt>
+FMT_CONSTEXPR auto format_uint(Char* buffer, UInt value, int num_digits,
+ bool upper = false) -> Char* {
+ buffer += num_digits;
+ Char* end = buffer;
+ do {
+ const char* digits = upper ? "0123456789ABCDEF" : "0123456789abcdef";
+ unsigned digit = static_cast<unsigned>(value & ((1 << BASE_BITS) - 1));
+ *--buffer = static_cast<Char>(BASE_BITS < 4 ? static_cast<char>('0' + digit)
+ : digits[digit]);
+ } while ((value >>= BASE_BITS) != 0);
+ return end;
+}
+
+template <unsigned BASE_BITS, typename Char, typename It, typename UInt>
+inline auto format_uint(It out, UInt value, int num_digits, bool upper = false)
+ -> It {
+ if (auto ptr = to_pointer<Char>(out, to_unsigned(num_digits))) {
+ format_uint<BASE_BITS>(ptr, value, num_digits, upper);
+ return out;
+ }
+ // Buffer should be large enough to hold all digits (digits / BASE_BITS + 1).
+ char buffer[num_bits<UInt>() / BASE_BITS + 1];
+ format_uint<BASE_BITS>(buffer, value, num_digits, upper);
+ return detail::copy_str_noinline<Char>(buffer, buffer + num_digits, out);
+}
+
+// A converter from UTF-8 to UTF-16.
+class utf8_to_utf16 {
+ private:
+ basic_memory_buffer<wchar_t> buffer_;
+
+ public:
+ FMT_API explicit utf8_to_utf16(string_view s);
+ operator basic_string_view<wchar_t>() const { return {&buffer_[0], size()}; }
+ auto size() const -> size_t { return buffer_.size() - 1; }
+ auto c_str() const -> const wchar_t* { return &buffer_[0]; }
+ auto str() const -> std::wstring { return {&buffer_[0], size()}; }
+};
+
+// A converter from UTF-16/UTF-32 (host endian) to UTF-8.
+template <typename WChar, typename Buffer = memory_buffer>
+class unicode_to_utf8 {
+ private:
+ Buffer buffer_;
+
+ public:
+ unicode_to_utf8() {}
+ explicit unicode_to_utf8(basic_string_view<WChar> s) {
+ static_assert(sizeof(WChar) == 2 || sizeof(WChar) == 4,
+ "Expect utf16 or utf32");
+
+ if (!convert(s))
+ FMT_THROW(std::runtime_error(sizeof(WChar) == 2 ? "invalid utf16"
+ : "invalid utf32"));
+ }
+ operator string_view() const { return string_view(&buffer_[0], size()); }
+ size_t size() const { return buffer_.size() - 1; }
+ const char* c_str() const { return &buffer_[0]; }
+ std::string str() const { return std::string(&buffer_[0], size()); }
+
+ // Performs conversion returning a bool instead of throwing exception on
+ // conversion error. This method may still throw in case of memory allocation
+ // error.
+ bool convert(basic_string_view<WChar> s) {
+ if (!convert(buffer_, s)) return false;
+ buffer_.push_back(0);
+ return true;
+ }
+ static bool convert(Buffer& buf, basic_string_view<WChar> s) {
+ for (auto p = s.begin(); p != s.end(); ++p) {
+ uint32_t c = static_cast<uint32_t>(*p);
+ if (sizeof(WChar) == 2 && c >= 0xd800 && c <= 0xdfff) {
+ // surrogate pair
+ ++p;
+ if (p == s.end() || (c & 0xfc00) != 0xd800 || (*p & 0xfc00) != 0xdc00) {
+ return false;
+ }
+ c = (c << 10) + static_cast<uint32_t>(*p) - 0x35fdc00;
+ }
+ if (c < 0x80) {
+ buf.push_back(static_cast<char>(c));
+ } else if (c < 0x800) {
+ buf.push_back(static_cast<char>(0xc0 | (c >> 6)));
+ buf.push_back(static_cast<char>(0x80 | (c & 0x3f)));
+ } else if ((c >= 0x800 && c <= 0xd7ff) || (c >= 0xe000 && c <= 0xffff)) {
+ buf.push_back(static_cast<char>(0xe0 | (c >> 12)));
+ buf.push_back(static_cast<char>(0x80 | ((c & 0xfff) >> 6)));
+ buf.push_back(static_cast<char>(0x80 | (c & 0x3f)));
+ } else if (c >= 0x10000 && c <= 0x10ffff) {
+ buf.push_back(static_cast<char>(0xf0 | (c >> 18)));
+ buf.push_back(static_cast<char>(0x80 | ((c & 0x3ffff) >> 12)));
+ buf.push_back(static_cast<char>(0x80 | ((c & 0xfff) >> 6)));
+ buf.push_back(static_cast<char>(0x80 | (c & 0x3f)));
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+};
+
+// Computes 128-bit result of multiplication of two 64-bit unsigned integers.
+inline uint128_fallback umul128(uint64_t x, uint64_t y) noexcept {
+#if FMT_USE_INT128
+ auto p = static_cast<uint128_opt>(x) * static_cast<uint128_opt>(y);
+ return {static_cast<uint64_t>(p >> 64), static_cast<uint64_t>(p)};
+#elif defined(_MSC_VER) && defined(_M_X64)
+ auto result = uint128_fallback();
+ result.lo_ = _umul128(x, y, &result.hi_);
+ return result;
+#else
+ const uint64_t mask = static_cast<uint64_t>(max_value<uint32_t>());
+
+ uint64_t a = x >> 32;
+ uint64_t b = x & mask;
+ uint64_t c = y >> 32;
+ uint64_t d = y & mask;
+
+ uint64_t ac = a * c;
+ uint64_t bc = b * c;
+ uint64_t ad = a * d;
+ uint64_t bd = b * d;
+
+ uint64_t intermediate = (bd >> 32) + (ad & mask) + (bc & mask);
+
+ return {ac + (intermediate >> 32) + (ad >> 32) + (bc >> 32),
+ (intermediate << 32) + (bd & mask)};
+#endif
+}
+
+namespace dragonbox {
+// Computes floor(log10(pow(2, e))) for e in [-2620, 2620] using the method from
+// https://fmt.dev/papers/Dragonbox.pdf#page=28, section 6.1.
+inline int floor_log10_pow2(int e) noexcept {
+ FMT_ASSERT(e <= 2620 && e >= -2620, "too large exponent");
+ static_assert((-1 >> 1) == -1, "right shift is not arithmetic");
+ return (e * 315653) >> 20;
+}
+
+inline int floor_log2_pow10(int e) noexcept {
+ FMT_ASSERT(e <= 1233 && e >= -1233, "too large exponent");
+ return (e * 1741647) >> 19;
+}
+
+// Computes upper 64 bits of multiplication of two 64-bit unsigned integers.
+inline uint64_t umul128_upper64(uint64_t x, uint64_t y) noexcept {
+#if FMT_USE_INT128
+ auto p = static_cast<uint128_opt>(x) * static_cast<uint128_opt>(y);
+ return static_cast<uint64_t>(p >> 64);
+#elif defined(_MSC_VER) && defined(_M_X64)
+ return __umulh(x, y);
+#else
+ return umul128(x, y).high();
+#endif
+}
+
+// Computes upper 128 bits of multiplication of a 64-bit unsigned integer and a
+// 128-bit unsigned integer.
+inline uint128_fallback umul192_upper128(uint64_t x,
+ uint128_fallback y) noexcept {
+ uint128_fallback r = umul128(x, y.high());
+ r += umul128_upper64(x, y.low());
+ return r;
+}
+
+FMT_API uint128_fallback get_cached_power(int k) noexcept;
+
+// Type-specific information that Dragonbox uses.
+template <typename T, typename Enable = void> struct float_info;
+
+template <> struct float_info<float> {
+ using carrier_uint = uint32_t;
+ static const int exponent_bits = 8;
+ static const int kappa = 1;
+ static const int big_divisor = 100;
+ static const int small_divisor = 10;
+ static const int min_k = -31;
+ static const int max_k = 46;
+ static const int shorter_interval_tie_lower_threshold = -35;
+ static const int shorter_interval_tie_upper_threshold = -35;
+};
+
+template <> struct float_info<double> {
+ using carrier_uint = uint64_t;
+ static const int exponent_bits = 11;
+ static const int kappa = 2;
+ static const int big_divisor = 1000;
+ static const int small_divisor = 100;
+ static const int min_k = -292;
+ static const int max_k = 341;
+ static const int shorter_interval_tie_lower_threshold = -77;
+ static const int shorter_interval_tie_upper_threshold = -77;
+};
+
+// An 80- or 128-bit floating point number.
+template <typename T>
+struct float_info<T, enable_if_t<std::numeric_limits<T>::digits == 64 ||
+ std::numeric_limits<T>::digits == 113 ||
+ is_float128<T>::value>> {
+ using carrier_uint = detail::uint128_t;
+ static const int exponent_bits = 15;
+};
+
+// A double-double floating point number.
+template <typename T>
+struct float_info<T, enable_if_t<is_double_double<T>::value>> {
+ using carrier_uint = detail::uint128_t;
+};
+
+template <typename T> struct decimal_fp {
+ using significand_type = typename float_info<T>::carrier_uint;
+ significand_type significand;
+ int exponent;
+};
+
+template <typename T> FMT_API auto to_decimal(T x) noexcept -> decimal_fp<T>;
+} // namespace dragonbox
+
+// Returns true iff Float has the implicit bit which is not stored.
+template <typename Float> constexpr bool has_implicit_bit() {
+ // An 80-bit FP number has a 64-bit significand an no implicit bit.
+ return std::numeric_limits<Float>::digits != 64;
+}
+
+// Returns the number of significand bits stored in Float. The implicit bit is
+// not counted since it is not stored.
+template <typename Float> constexpr int num_significand_bits() {
+ // std::numeric_limits may not support __float128.
+ return is_float128<Float>() ? 112
+ : (std::numeric_limits<Float>::digits -
+ (has_implicit_bit<Float>() ? 1 : 0));
+}
+
+template <typename Float>
+constexpr auto exponent_mask() ->
+ typename dragonbox::float_info<Float>::carrier_uint {
+ using float_uint = typename dragonbox::float_info<Float>::carrier_uint;
+ return ((float_uint(1) << dragonbox::float_info<Float>::exponent_bits) - 1)
+ << num_significand_bits<Float>();
+}
+template <typename Float> constexpr auto exponent_bias() -> int {
+ // std::numeric_limits may not support __float128.
+ return is_float128<Float>() ? 16383
+ : std::numeric_limits<Float>::max_exponent - 1;
+}
+
+// Writes the exponent exp in the form "[+-]d{2,3}" to buffer.
+template <typename Char, typename It>
+FMT_CONSTEXPR auto write_exponent(int exp, It it) -> It {
+ FMT_ASSERT(-10000 < exp && exp < 10000, "exponent out of range");
+ if (exp < 0) {
+ *it++ = static_cast<Char>('-');
+ exp = -exp;
+ } else {
+ *it++ = static_cast<Char>('+');
+ }
+ if (exp >= 100) {
+ const char* top = digits2(to_unsigned(exp / 100));
+ if (exp >= 1000) *it++ = static_cast<Char>(top[0]);
+ *it++ = static_cast<Char>(top[1]);
+ exp %= 100;
+ }
+ const char* d = digits2(to_unsigned(exp));
+ *it++ = static_cast<Char>(d[0]);
+ *it++ = static_cast<Char>(d[1]);
+ return it;
+}
+
+// A floating-point number f * pow(2, e) where F is an unsigned type.
+template <typename F> struct basic_fp {
+ F f;
+ int e;
+
+ static constexpr const int num_significand_bits =
+ static_cast<int>(sizeof(F) * num_bits<unsigned char>());
+
+ constexpr basic_fp() : f(0), e(0) {}
+ constexpr basic_fp(uint64_t f_val, int e_val) : f(f_val), e(e_val) {}
+
+ // Constructs fp from an IEEE754 floating-point number.
+ template <typename Float> FMT_CONSTEXPR basic_fp(Float n) { assign(n); }
+
+ // Assigns n to this and return true iff predecessor is closer than successor.
+ template <typename Float, FMT_ENABLE_IF(!is_double_double<Float>::value)>
+ FMT_CONSTEXPR auto assign(Float n) -> bool {
+ static_assert(std::numeric_limits<Float>::digits <= 113, "unsupported FP");
+ // Assume Float is in the format [sign][exponent][significand].
+ using carrier_uint = typename dragonbox::float_info<Float>::carrier_uint;
+ const auto num_float_significand_bits =
+ detail::num_significand_bits<Float>();
+ const auto implicit_bit = carrier_uint(1) << num_float_significand_bits;
+ const auto significand_mask = implicit_bit - 1;
+ auto u = bit_cast<carrier_uint>(n);
+ f = static_cast<F>(u & significand_mask);
+ auto biased_e = static_cast<int>((u & exponent_mask<Float>()) >>
+ num_float_significand_bits);
+ // The predecessor is closer if n is a normalized power of 2 (f == 0)
+ // other than the smallest normalized number (biased_e > 1).
+ auto is_predecessor_closer = f == 0 && biased_e > 1;
+ if (biased_e == 0)
+ biased_e = 1; // Subnormals use biased exponent 1 (min exponent).
+ else if (has_implicit_bit<Float>())
+ f += static_cast<F>(implicit_bit);
+ e = biased_e - exponent_bias<Float>() - num_float_significand_bits;
+ if (!has_implicit_bit<Float>()) ++e;
+ return is_predecessor_closer;
+ }
+
+ template <typename Float, FMT_ENABLE_IF(is_double_double<Float>::value)>
+ FMT_CONSTEXPR auto assign(Float n) -> bool {
+ static_assert(std::numeric_limits<double>::is_iec559, "unsupported FP");
+ return assign(static_cast<double>(n));
+ }
+};
+
+using fp = basic_fp<unsigned long long>;
+
+// Normalizes the value converted from double and multiplied by (1 << SHIFT).
+template <int SHIFT = 0, typename F>
+FMT_CONSTEXPR basic_fp<F> normalize(basic_fp<F> value) {
+ // Handle subnormals.
+ const auto implicit_bit = F(1) << num_significand_bits<double>();
+ const auto shifted_implicit_bit = implicit_bit << SHIFT;
+ while ((value.f & shifted_implicit_bit) == 0) {
+ value.f <<= 1;
+ --value.e;
+ }
+ // Subtract 1 to account for hidden bit.
+ const auto offset = basic_fp<F>::num_significand_bits -
+ num_significand_bits<double>() - SHIFT - 1;
+ value.f <<= offset;
+ value.e -= offset;
+ return value;
+}
+
+// Computes lhs * rhs / pow(2, 64) rounded to nearest with half-up tie breaking.
+FMT_CONSTEXPR inline uint64_t multiply(uint64_t lhs, uint64_t rhs) {
+#if FMT_USE_INT128
+ auto product = static_cast<__uint128_t>(lhs) * rhs;
+ auto f = static_cast<uint64_t>(product >> 64);
+ return (static_cast<uint64_t>(product) & (1ULL << 63)) != 0 ? f + 1 : f;
+#else
+ // Multiply 32-bit parts of significands.
+ uint64_t mask = (1ULL << 32) - 1;
+ uint64_t a = lhs >> 32, b = lhs & mask;
+ uint64_t c = rhs >> 32, d = rhs & mask;
+ uint64_t ac = a * c, bc = b * c, ad = a * d, bd = b * d;
+ // Compute mid 64-bit of result and round.
+ uint64_t mid = (bd >> 32) + (ad & mask) + (bc & mask) + (1U << 31);
+ return ac + (ad >> 32) + (bc >> 32) + (mid >> 32);
+#endif
+}
+
+FMT_CONSTEXPR inline fp operator*(fp x, fp y) {
+ return {multiply(x.f, y.f), x.e + y.e + 64};
+}
+
+template <typename T = void> struct basic_data {
+ // Normalized 64-bit significands of pow(10, k), for k = -348, -340, ..., 340.
+ // These are generated by support/compute-powers.py.
+ static constexpr uint64_t pow10_significands[87] = {
+ 0xfa8fd5a0081c0288, 0xbaaee17fa23ebf76, 0x8b16fb203055ac76,
+ 0xcf42894a5dce35ea, 0x9a6bb0aa55653b2d, 0xe61acf033d1a45df,
+ 0xab70fe17c79ac6ca, 0xff77b1fcbebcdc4f, 0xbe5691ef416bd60c,
+ 0x8dd01fad907ffc3c, 0xd3515c2831559a83, 0x9d71ac8fada6c9b5,
+ 0xea9c227723ee8bcb, 0xaecc49914078536d, 0x823c12795db6ce57,
+ 0xc21094364dfb5637, 0x9096ea6f3848984f, 0xd77485cb25823ac7,
+ 0xa086cfcd97bf97f4, 0xef340a98172aace5, 0xb23867fb2a35b28e,
+ 0x84c8d4dfd2c63f3b, 0xc5dd44271ad3cdba, 0x936b9fcebb25c996,
+ 0xdbac6c247d62a584, 0xa3ab66580d5fdaf6, 0xf3e2f893dec3f126,
+ 0xb5b5ada8aaff80b8, 0x87625f056c7c4a8b, 0xc9bcff6034c13053,
+ 0x964e858c91ba2655, 0xdff9772470297ebd, 0xa6dfbd9fb8e5b88f,
+ 0xf8a95fcf88747d94, 0xb94470938fa89bcf, 0x8a08f0f8bf0f156b,
+ 0xcdb02555653131b6, 0x993fe2c6d07b7fac, 0xe45c10c42a2b3b06,
+ 0xaa242499697392d3, 0xfd87b5f28300ca0e, 0xbce5086492111aeb,
+ 0x8cbccc096f5088cc, 0xd1b71758e219652c, 0x9c40000000000000,
+ 0xe8d4a51000000000, 0xad78ebc5ac620000, 0x813f3978f8940984,
+ 0xc097ce7bc90715b3, 0x8f7e32ce7bea5c70, 0xd5d238a4abe98068,
+ 0x9f4f2726179a2245, 0xed63a231d4c4fb27, 0xb0de65388cc8ada8,
+ 0x83c7088e1aab65db, 0xc45d1df942711d9a, 0x924d692ca61be758,
+ 0xda01ee641a708dea, 0xa26da3999aef774a, 0xf209787bb47d6b85,
+ 0xb454e4a179dd1877, 0x865b86925b9bc5c2, 0xc83553c5c8965d3d,
+ 0x952ab45cfa97a0b3, 0xde469fbd99a05fe3, 0xa59bc234db398c25,
+ 0xf6c69a72a3989f5c, 0xb7dcbf5354e9bece, 0x88fcf317f22241e2,
+ 0xcc20ce9bd35c78a5, 0x98165af37b2153df, 0xe2a0b5dc971f303a,
+ 0xa8d9d1535ce3b396, 0xfb9b7cd9a4a7443c, 0xbb764c4ca7a44410,
+ 0x8bab8eefb6409c1a, 0xd01fef10a657842c, 0x9b10a4e5e9913129,
+ 0xe7109bfba19c0c9d, 0xac2820d9623bf429, 0x80444b5e7aa7cf85,
+ 0xbf21e44003acdd2d, 0x8e679c2f5e44ff8f, 0xd433179d9c8cb841,
+ 0x9e19db92b4e31ba9, 0xeb96bf6ebadf77d9, 0xaf87023b9bf0ee6b,
+ };
+
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wnarrowing"
+#endif
+ // Binary exponents of pow(10, k), for k = -348, -340, ..., 340, corresponding
+ // to significands above.
+ static constexpr int16_t pow10_exponents[87] = {
+ -1220, -1193, -1166, -1140, -1113, -1087, -1060, -1034, -1007, -980, -954,
+ -927, -901, -874, -847, -821, -794, -768, -741, -715, -688, -661,
+ -635, -608, -582, -555, -529, -502, -475, -449, -422, -396, -369,
+ -343, -316, -289, -263, -236, -210, -183, -157, -130, -103, -77,
+ -50, -24, 3, 30, 56, 83, 109, 136, 162, 189, 216,
+ 242, 269, 295, 322, 348, 375, 402, 428, 455, 481, 508,
+ 534, 561, 588, 614, 641, 667, 694, 720, 747, 774, 800,
+ 827, 853, 880, 907, 933, 960, 986, 1013, 1039, 1066};
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+# pragma GCC diagnostic pop
+#endif
+
+ static constexpr uint64_t power_of_10_64[20] = {
+ 1, FMT_POWERS_OF_10(1ULL), FMT_POWERS_OF_10(1000000000ULL),
+ 10000000000000000000ULL};
+
+ // For checking rounding thresholds.
+ // The kth entry is chosen to be the smallest integer such that the
+ // upper 32-bits of 10^(k+1) times it is strictly bigger than 5 * 10^k.
+ static constexpr uint32_t fractional_part_rounding_thresholds[8] = {
+ 2576980378, // ceil(2^31 + 2^32/10^1)
+ 2190433321, // ceil(2^31 + 2^32/10^2)
+ 2151778616, // ceil(2^31 + 2^32/10^3)
+ 2147913145, // ceil(2^31 + 2^32/10^4)
+ 2147526598, // ceil(2^31 + 2^32/10^5)
+ 2147487943, // ceil(2^31 + 2^32/10^6)
+ 2147484078, // ceil(2^31 + 2^32/10^7)
+ 2147483691 // ceil(2^31 + 2^32/10^8)
+ };
+};
+
+#if FMT_CPLUSPLUS < 201703L
+template <typename T> constexpr uint64_t basic_data<T>::pow10_significands[];
+template <typename T> constexpr int16_t basic_data<T>::pow10_exponents[];
+template <typename T> constexpr uint64_t basic_data<T>::power_of_10_64[];
+template <typename T>
+constexpr uint32_t basic_data<T>::fractional_part_rounding_thresholds[];
+#endif
+
+// This is a struct rather than an alias to avoid shadowing warnings in gcc.
+struct data : basic_data<> {};
+
+// Returns a cached power of 10 `c_k = c_k.f * pow(2, c_k.e)` such that its
+// (binary) exponent satisfies `min_exponent <= c_k.e <= min_exponent + 28`.
+FMT_CONSTEXPR inline fp get_cached_power(int min_exponent,
+ int& pow10_exponent) {
+ const int shift = 32;
+ // log10(2) = 0x0.4d104d427de7fbcc...
+ const int64_t significand = 0x4d104d427de7fbcc;
+ int index = static_cast<int>(
+ ((min_exponent + fp::num_significand_bits - 1) * (significand >> shift) +
+ ((int64_t(1) << shift) - 1)) // ceil
+ >> 32 // arithmetic shift
+ );
+ // Decimal exponent of the first (smallest) cached power of 10.
+ const int first_dec_exp = -348;
+ // Difference between 2 consecutive decimal exponents in cached powers of 10.
+ const int dec_exp_step = 8;
+ index = (index - first_dec_exp - 1) / dec_exp_step + 1;
+ pow10_exponent = first_dec_exp + index * dec_exp_step;
+ // Using *(x + index) instead of x[index] avoids an issue with some compilers
+ // using the EDG frontend (e.g. nvhpc/22.3 in C++17 mode).
+ return {*(data::pow10_significands + index),
+ *(data::pow10_exponents + index)};
+}
+
+template <typename T>
+using convert_float_result =
+ conditional_t<std::is_same<T, float>::value ||
+ std::numeric_limits<T>::digits ==
+ std::numeric_limits<double>::digits,
+ double, T>;
+
+template <typename T>
+constexpr auto convert_float(T value) -> convert_float_result<T> {
+ return static_cast<convert_float_result<T>>(value);
+}
+
+template <typename OutputIt, typename Char>
+FMT_NOINLINE FMT_CONSTEXPR auto fill(OutputIt it, size_t n,
+ const fill_t<Char>& fill) -> OutputIt {
+ auto fill_size = fill.size();
+ if (fill_size == 1) return detail::fill_n(it, n, fill[0]);
+ auto data = fill.data();
+ for (size_t i = 0; i < n; ++i)
+ it = copy_str<Char>(data, data + fill_size, it);
+ return it;
+}
+
+// Writes the output of f, padded according to format specifications in specs.
+// size: output size in code units.
+// width: output display width in (terminal) column positions.
+template <align::type align = align::left, typename OutputIt, typename Char,
+ typename F>
+FMT_CONSTEXPR auto write_padded(OutputIt out, const format_specs<Char>& specs,
+ size_t size, size_t width, F&& f) -> OutputIt {
+ static_assert(align == align::left || align == align::right, "");
+ unsigned spec_width = to_unsigned(specs.width);
+ size_t padding = spec_width > width ? spec_width - width : 0;
+ // Shifts are encoded as string literals because static constexpr is not
+ // supported in constexpr functions.
+ auto* shifts = align == align::left ? "\x1f\x1f\x00\x01" : "\x00\x1f\x00\x01";
+ size_t left_padding = padding >> shifts[specs.align];
+ size_t right_padding = padding - left_padding;
+ auto it = reserve(out, size + padding * specs.fill.size());
+ if (left_padding != 0) it = fill(it, left_padding, specs.fill);
+ it = f(it);
+ if (right_padding != 0) it = fill(it, right_padding, specs.fill);
+ return base_iterator(out, it);
+}
+
+template <align::type align = align::left, typename OutputIt, typename Char,
+ typename F>
+constexpr auto write_padded(OutputIt out, const format_specs<Char>& specs,
+ size_t size, F&& f) -> OutputIt {
+ return write_padded<align>(out, specs, size, size, f);
+}
+
+template <align::type align = align::left, typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write_bytes(OutputIt out, string_view bytes,
+ const format_specs<Char>& specs) -> OutputIt {
+ return write_padded<align>(
+ out, specs, bytes.size(), [bytes](reserve_iterator<OutputIt> it) {
+ const char* data = bytes.data();
+ return copy_str<Char>(data, data + bytes.size(), it);
+ });
+}
+
+template <typename Char, typename OutputIt, typename UIntPtr>
+auto write_ptr(OutputIt out, UIntPtr value, const format_specs<Char>* specs)
+ -> OutputIt {
+ int num_digits = count_digits<4>(value);
+ auto size = to_unsigned(num_digits) + size_t(2);
+ auto write = [=](reserve_iterator<OutputIt> it) {
+ *it++ = static_cast<Char>('0');
+ *it++ = static_cast<Char>('x');
+ return format_uint<4, Char>(it, value, num_digits);
+ };
+ return specs ? write_padded<align::right>(out, *specs, size, write)
+ : base_iterator(out, write(reserve(out, size)));
+}
+
+// Returns true iff the code point cp is printable.
+FMT_API auto is_printable(uint32_t cp) -> bool;
+
+inline auto needs_escape(uint32_t cp) -> bool {
+ return cp < 0x20 || cp == 0x7f || cp == '"' || cp == '\\' ||
+ !is_printable(cp);
+}
+
+template <typename Char> struct find_escape_result {
+ const Char* begin;
+ const Char* end;
+ uint32_t cp;
+};
+
+template <typename Char>
+using make_unsigned_char =
+ typename conditional_t<std::is_integral<Char>::value,
+ std::make_unsigned<Char>,
+ type_identity<uint32_t>>::type;
+
+template <typename Char>
+auto find_escape(const Char* begin, const Char* end)
+ -> find_escape_result<Char> {
+ for (; begin != end; ++begin) {
+ uint32_t cp = static_cast<make_unsigned_char<Char>>(*begin);
+ if (const_check(sizeof(Char) == 1) && cp >= 0x80) continue;
+ if (needs_escape(cp)) return {begin, begin + 1, cp};
+ }
+ return {begin, nullptr, 0};
+}
+
+inline auto find_escape(const char* begin, const char* end)
+ -> find_escape_result<char> {
+ if (!is_utf8()) return find_escape<char>(begin, end);
+ auto result = find_escape_result<char>{end, nullptr, 0};
+ for_each_codepoint(string_view(begin, to_unsigned(end - begin)),
+ [&](uint32_t cp, string_view sv) {
+ if (needs_escape(cp)) {
+ result = {sv.begin(), sv.end(), cp};
+ return false;
+ }
+ return true;
+ });
+ return result;
+}
+
+#define FMT_STRING_IMPL(s, base, explicit) \
+ [] { \
+ /* Use the hidden visibility as a workaround for a GCC bug (#1973). */ \
+ /* Use a macro-like name to avoid shadowing warnings. */ \
+ struct FMT_GCC_VISIBILITY_HIDDEN FMT_COMPILE_STRING : base { \
+ using char_type FMT_MAYBE_UNUSED = fmt::remove_cvref_t<decltype(s[0])>; \
+ FMT_MAYBE_UNUSED FMT_CONSTEXPR explicit \
+ operator fmt::basic_string_view<char_type>() const { \
+ return fmt::detail_exported::compile_string_to_view<char_type>(s); \
+ } \
+ }; \
+ return FMT_COMPILE_STRING(); \
+ }()
+
+/**
+ \rst
+ Constructs a compile-time format string from a string literal *s*.
+
+ **Example**::
+
+ // A compile-time error because 'd' is an invalid specifier for strings.
+ std::string s = fmt::format(FMT_STRING("{:d}"), "foo");
+ \endrst
+ */
+#define FMT_STRING(s) FMT_STRING_IMPL(s, fmt::detail::compile_string, )
+
+template <size_t width, typename Char, typename OutputIt>
+auto write_codepoint(OutputIt out, char prefix, uint32_t cp) -> OutputIt {
+ *out++ = static_cast<Char>('\\');
+ *out++ = static_cast<Char>(prefix);
+ Char buf[width];
+ fill_n(buf, width, static_cast<Char>('0'));
+ format_uint<4>(buf, cp, width);
+ return copy_str<Char>(buf, buf + width, out);
+}
+
+template <typename OutputIt, typename Char>
+auto write_escaped_cp(OutputIt out, const find_escape_result<Char>& escape)
+ -> OutputIt {
+ auto c = static_cast<Char>(escape.cp);
+ switch (escape.cp) {
+ case '\n':
+ *out++ = static_cast<Char>('\\');
+ c = static_cast<Char>('n');
+ break;
+ case '\r':
+ *out++ = static_cast<Char>('\\');
+ c = static_cast<Char>('r');
+ break;
+ case '\t':
+ *out++ = static_cast<Char>('\\');
+ c = static_cast<Char>('t');
+ break;
+ case '"':
+ FMT_FALLTHROUGH;
+ case '\'':
+ FMT_FALLTHROUGH;
+ case '\\':
+ *out++ = static_cast<Char>('\\');
+ break;
+ default:
+ if (escape.cp < 0x100) {
+ return write_codepoint<2, Char>(out, 'x', escape.cp);
+ }
+ if (escape.cp < 0x10000) {
+ return write_codepoint<4, Char>(out, 'u', escape.cp);
+ }
+ if (escape.cp < 0x110000) {
+ return write_codepoint<8, Char>(out, 'U', escape.cp);
+ }
+ for (Char escape_char : basic_string_view<Char>(
+ escape.begin, to_unsigned(escape.end - escape.begin))) {
+ out = write_codepoint<2, Char>(out, 'x',
+ static_cast<uint32_t>(escape_char) & 0xFF);
+ }
+ return out;
+ }
+ *out++ = c;
+ return out;
+}
+
+template <typename Char, typename OutputIt>
+auto write_escaped_string(OutputIt out, basic_string_view<Char> str)
+ -> OutputIt {
+ *out++ = static_cast<Char>('"');
+ auto begin = str.begin(), end = str.end();
+ do {
+ auto escape = find_escape(begin, end);
+ out = copy_str<Char>(begin, escape.begin, out);
+ begin = escape.end;
+ if (!begin) break;
+ out = write_escaped_cp<OutputIt, Char>(out, escape);
+ } while (begin != end);
+ *out++ = static_cast<Char>('"');
+ return out;
+}
+
+template <typename Char, typename OutputIt>
+auto write_escaped_char(OutputIt out, Char v) -> OutputIt {
+ *out++ = static_cast<Char>('\'');
+ if ((needs_escape(static_cast<uint32_t>(v)) && v != static_cast<Char>('"')) ||
+ v == static_cast<Char>('\'')) {
+ out = write_escaped_cp(
+ out, find_escape_result<Char>{&v, &v + 1, static_cast<uint32_t>(v)});
+ } else {
+ *out++ = v;
+ }
+ *out++ = static_cast<Char>('\'');
+ return out;
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write_char(OutputIt out, Char value,
+ const format_specs<Char>& specs) -> OutputIt {
+ bool is_debug = specs.type == presentation_type::debug;
+ return write_padded(out, specs, 1, [=](reserve_iterator<OutputIt> it) {
+ if (is_debug) return write_escaped_char(it, value);
+ *it++ = value;
+ return it;
+ });
+}
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, Char value,
+ const format_specs<Char>& specs, locale_ref loc = {})
+ -> OutputIt {
+ // char is formatted as unsigned char for consistency across platforms.
+ using unsigned_type =
+ conditional_t<std::is_same<Char, char>::value, unsigned char, unsigned>;
+ return check_char_specs(specs)
+ ? write_char(out, value, specs)
+ : write(out, static_cast<unsigned_type>(value), specs, loc);
+}
+
+// Data for write_int that doesn't depend on output iterator type. It is used to
+// avoid template code bloat.
+template <typename Char> struct write_int_data {
+ size_t size;
+ size_t padding;
+
+ FMT_CONSTEXPR write_int_data(int num_digits, unsigned prefix,
+ const format_specs<Char>& specs)
+ : size((prefix >> 24) + to_unsigned(num_digits)), padding(0) {
+ if (specs.align == align::numeric) {
+ auto width = to_unsigned(specs.width);
+ if (width > size) {
+ padding = width - size;
+ size = width;
+ }
+ } else if (specs.precision > num_digits) {
+ size = (prefix >> 24) + to_unsigned(specs.precision);
+ padding = to_unsigned(specs.precision - num_digits);
+ }
+ }
+};
+
+// Writes an integer in the format
+// <left-padding><prefix><numeric-padding><digits><right-padding>
+// where <digits> are written by write_digits(it).
+// prefix contains chars in three lower bytes and the size in the fourth byte.
+template <typename OutputIt, typename Char, typename W>
+FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, int num_digits,
+ unsigned prefix,
+ const format_specs<Char>& specs,
+ W write_digits) -> OutputIt {
+ // Slightly faster check for specs.width == 0 && specs.precision == -1.
+ if ((specs.width | (specs.precision + 1)) == 0) {
+ auto it = reserve(out, to_unsigned(num_digits) + (prefix >> 24));
+ if (prefix != 0) {
+ for (unsigned p = prefix & 0xffffff; p != 0; p >>= 8)
+ *it++ = static_cast<Char>(p & 0xff);
+ }
+ return base_iterator(out, write_digits(it));
+ }
+ auto data = write_int_data<Char>(num_digits, prefix, specs);
+ return write_padded<align::right>(
+ out, specs, data.size, [=](reserve_iterator<OutputIt> it) {
+ for (unsigned p = prefix & 0xffffff; p != 0; p >>= 8)
+ *it++ = static_cast<Char>(p & 0xff);
+ it = detail::fill_n(it, data.padding, static_cast<Char>('0'));
+ return write_digits(it);
+ });
+}
+
+template <typename Char> class digit_grouping {
+ private:
+ std::string grouping_;
+ std::basic_string<Char> thousands_sep_;
+
+ struct next_state {
+ std::string::const_iterator group;
+ int pos;
+ };
+ next_state initial_state() const { return {grouping_.begin(), 0}; }
+
+ // Returns the next digit group separator position.
+ int next(next_state& state) const {
+ if (thousands_sep_.empty()) return max_value<int>();
+ if (state.group == grouping_.end()) return state.pos += grouping_.back();
+ if (*state.group <= 0 || *state.group == max_value<char>())
+ return max_value<int>();
+ state.pos += *state.group++;
+ return state.pos;
+ }
+
+ public:
+ explicit digit_grouping(locale_ref loc, bool localized = true) {
+ if (!localized) return;
+ auto sep = thousands_sep<Char>(loc);
+ grouping_ = sep.grouping;
+ if (sep.thousands_sep) thousands_sep_.assign(1, sep.thousands_sep);
+ }
+ digit_grouping(std::string grouping, std::basic_string<Char> sep)
+ : grouping_(std::move(grouping)), thousands_sep_(std::move(sep)) {}
+
+ bool has_separator() const { return !thousands_sep_.empty(); }
+
+ int count_separators(int num_digits) const {
+ int count = 0;
+ auto state = initial_state();
+ while (num_digits > next(state)) ++count;
+ return count;
+ }
+
+ // Applies grouping to digits and write the output to out.
+ template <typename Out, typename C>
+ Out apply(Out out, basic_string_view<C> digits) const {
+ auto num_digits = static_cast<int>(digits.size());
+ auto separators = basic_memory_buffer<int>();
+ separators.push_back(0);
+ auto state = initial_state();
+ while (int i = next(state)) {
+ if (i >= num_digits) break;
+ separators.push_back(i);
+ }
+ for (int i = 0, sep_index = static_cast<int>(separators.size() - 1);
+ i < num_digits; ++i) {
+ if (num_digits - i == separators[sep_index]) {
+ out =
+ copy_str<Char>(thousands_sep_.data(),
+ thousands_sep_.data() + thousands_sep_.size(), out);
+ --sep_index;
+ }
+ *out++ = static_cast<Char>(digits[to_unsigned(i)]);
+ }
+ return out;
+ }
+};
+
+// Writes a decimal integer with digit grouping.
+template <typename OutputIt, typename UInt, typename Char>
+auto write_int(OutputIt out, UInt value, unsigned prefix,
+ const format_specs<Char>& specs,
+ const digit_grouping<Char>& grouping) -> OutputIt {
+ static_assert(std::is_same<uint64_or_128_t<UInt>, UInt>::value, "");
+ int num_digits = count_digits(value);
+ char digits[40];
+ format_decimal(digits, value, num_digits);
+ unsigned size = to_unsigned((prefix != 0 ? 1 : 0) + num_digits +
+ grouping.count_separators(num_digits));
+ return write_padded<align::right>(
+ out, specs, size, size, [&](reserve_iterator<OutputIt> it) {
+ if (prefix != 0) {
+ char sign = static_cast<char>(prefix);
+ *it++ = static_cast<Char>(sign);
+ }
+ return grouping.apply(it, string_view(digits, to_unsigned(num_digits)));
+ });
+}
+
+// Writes a localized value.
+FMT_API auto write_loc(appender out, loc_value value,
+ const format_specs<>& specs, locale_ref loc) -> bool;
+template <typename OutputIt, typename Char>
+inline auto write_loc(OutputIt, loc_value, const format_specs<Char>&,
+ locale_ref) -> bool {
+ return false;
+}
+
+FMT_CONSTEXPR inline void prefix_append(unsigned& prefix, unsigned value) {
+ prefix |= prefix != 0 ? value << 8 : value;
+ prefix += (1u + (value > 0xff ? 1 : 0)) << 24;
+}
+
+template <typename UInt> struct write_int_arg {
+ UInt abs_value;
+ unsigned prefix;
+};
+
+template <typename T>
+FMT_CONSTEXPR auto make_write_int_arg(T value, sign_t sign)
+ -> write_int_arg<uint32_or_64_or_128_t<T>> {
+ auto prefix = 0u;
+ auto abs_value = static_cast<uint32_or_64_or_128_t<T>>(value);
+ if (is_negative(value)) {
+ prefix = 0x01000000 | '-';
+ abs_value = 0 - abs_value;
+ } else {
+ constexpr const unsigned prefixes[4] = {0, 0, 0x1000000u | '+',
+ 0x1000000u | ' '};
+ prefix = prefixes[sign];
+ }
+ return {abs_value, prefix};
+}
+
+template <typename Char = char> struct loc_writer {
+ buffer_appender<Char> out;
+ const format_specs<Char>& specs;
+ std::basic_string<Char> sep;
+ std::string grouping;
+ std::basic_string<Char> decimal_point;
+
+ template <typename T, FMT_ENABLE_IF(is_integer<T>::value)>
+ auto operator()(T value) -> bool {
+ auto arg = make_write_int_arg(value, specs.sign);
+ write_int(out, static_cast<uint64_or_128_t<T>>(arg.abs_value), arg.prefix,
+ specs, digit_grouping<Char>(grouping, sep));
+ return true;
+ }
+
+ template <typename T, FMT_ENABLE_IF(!is_integer<T>::value)>
+ auto operator()(T) -> bool {
+ return false;
+ }
+};
+
+template <typename Char, typename OutputIt, typename T>
+FMT_CONSTEXPR FMT_INLINE auto write_int(OutputIt out, write_int_arg<T> arg,
+ const format_specs<Char>& specs,
+ locale_ref) -> OutputIt {
+ static_assert(std::is_same<T, uint32_or_64_or_128_t<T>>::value, "");
+ auto abs_value = arg.abs_value;
+ auto prefix = arg.prefix;
+ switch (specs.type) {
+ case presentation_type::none:
+ case presentation_type::dec: {
+ auto num_digits = count_digits(abs_value);
+ return write_int(
+ out, num_digits, prefix, specs, [=](reserve_iterator<OutputIt> it) {
+ return format_decimal<Char>(it, abs_value, num_digits).end;
+ });
+ }
+ case presentation_type::hex_lower:
+ case presentation_type::hex_upper: {
+ bool upper = specs.type == presentation_type::hex_upper;
+ if (specs.alt)
+ prefix_append(prefix, unsigned(upper ? 'X' : 'x') << 8 | '0');
+ int num_digits = count_digits<4>(abs_value);
+ return write_int(
+ out, num_digits, prefix, specs, [=](reserve_iterator<OutputIt> it) {
+ return format_uint<4, Char>(it, abs_value, num_digits, upper);
+ });
+ }
+ case presentation_type::bin_lower:
+ case presentation_type::bin_upper: {
+ bool upper = specs.type == presentation_type::bin_upper;
+ if (specs.alt)
+ prefix_append(prefix, unsigned(upper ? 'B' : 'b') << 8 | '0');
+ int num_digits = count_digits<1>(abs_value);
+ return write_int(out, num_digits, prefix, specs,
+ [=](reserve_iterator<OutputIt> it) {
+ return format_uint<1, Char>(it, abs_value, num_digits);
+ });
+ }
+ case presentation_type::oct: {
+ int num_digits = count_digits<3>(abs_value);
+ // Octal prefix '0' is counted as a digit, so only add it if precision
+ // is not greater than the number of digits.
+ if (specs.alt && specs.precision <= num_digits && abs_value != 0)
+ prefix_append(prefix, '0');
+ return write_int(out, num_digits, prefix, specs,
+ [=](reserve_iterator<OutputIt> it) {
+ return format_uint<3, Char>(it, abs_value, num_digits);
+ });
+ }
+ case presentation_type::chr:
+ return write_char(out, static_cast<Char>(abs_value), specs);
+ default:
+ throw_format_error("invalid format specifier");
+ }
+ return out;
+}
+template <typename Char, typename OutputIt, typename T>
+FMT_CONSTEXPR FMT_NOINLINE auto write_int_noinline(
+ OutputIt out, write_int_arg<T> arg, const format_specs<Char>& specs,
+ locale_ref loc) -> OutputIt {
+ return write_int(out, arg, specs, loc);
+}
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_integral<T>::value &&
+ !std::is_same<T, bool>::value &&
+ std::is_same<OutputIt, buffer_appender<Char>>::value)>
+FMT_CONSTEXPR FMT_INLINE auto write(OutputIt out, T value,
+ const format_specs<Char>& specs,
+ locale_ref loc) -> OutputIt {
+ if (specs.localized && write_loc(out, value, specs, loc)) return out;
+ return write_int_noinline(out, make_write_int_arg(value, specs.sign), specs,
+ loc);
+}
+// An inlined version of write used in format string compilation.
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_integral<T>::value &&
+ !std::is_same<T, bool>::value &&
+ !std::is_same<OutputIt, buffer_appender<Char>>::value)>
+FMT_CONSTEXPR FMT_INLINE auto write(OutputIt out, T value,
+ const format_specs<Char>& specs,
+ locale_ref loc) -> OutputIt {
+ if (specs.localized && write_loc(out, value, specs, loc)) return out;
+ return write_int(out, make_write_int_arg(value, specs.sign), specs, loc);
+}
+
+// An output iterator that counts the number of objects written to it and
+// discards them.
+class counting_iterator {
+ private:
+ size_t count_;
+
+ public:
+ using iterator_category = std::output_iterator_tag;
+ using difference_type = std::ptrdiff_t;
+ using pointer = void;
+ using reference = void;
+ FMT_UNCHECKED_ITERATOR(counting_iterator);
+
+ struct value_type {
+ template <typename T> FMT_CONSTEXPR void operator=(const T&) {}
+ };
+
+ FMT_CONSTEXPR counting_iterator() : count_(0) {}
+
+ FMT_CONSTEXPR size_t count() const { return count_; }
+
+ FMT_CONSTEXPR counting_iterator& operator++() {
+ ++count_;
+ return *this;
+ }
+ FMT_CONSTEXPR counting_iterator operator++(int) {
+ auto it = *this;
+ ++*this;
+ return it;
+ }
+
+ FMT_CONSTEXPR friend counting_iterator operator+(counting_iterator it,
+ difference_type n) {
+ it.count_ += static_cast<size_t>(n);
+ return it;
+ }
+
+ FMT_CONSTEXPR value_type operator*() const { return {}; }
+};
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, basic_string_view<Char> s,
+ const format_specs<Char>& specs) -> OutputIt {
+ auto data = s.data();
+ auto size = s.size();
+ if (specs.precision >= 0 && to_unsigned(specs.precision) < size)
+ size = code_point_index(s, to_unsigned(specs.precision));
+ bool is_debug = specs.type == presentation_type::debug;
+ size_t width = 0;
+ if (specs.width != 0) {
+ if (is_debug)
+ width = write_escaped_string(counting_iterator{}, s).count();
+ else
+ width = compute_width(basic_string_view<Char>(data, size));
+ }
+ return write_padded(out, specs, size, width,
+ [=](reserve_iterator<OutputIt> it) {
+ if (is_debug) return write_escaped_string(it, s);
+ return copy_str<Char>(data, data + size, it);
+ });
+}
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out,
+ basic_string_view<type_identity_t<Char>> s,
+ const format_specs<Char>& specs, locale_ref)
+ -> OutputIt {
+ return write(out, s, specs);
+}
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, const Char* s,
+ const format_specs<Char>& specs, locale_ref)
+ -> OutputIt {
+ return specs.type != presentation_type::pointer
+ ? write(out, basic_string_view<Char>(s), specs, {})
+ : write_ptr<Char>(out, bit_cast<uintptr_t>(s), &specs);
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_integral<T>::value &&
+ !std::is_same<T, bool>::value &&
+ !std::is_same<T, Char>::value)>
+FMT_CONSTEXPR auto write(OutputIt out, T value) -> OutputIt {
+ auto abs_value = static_cast<uint32_or_64_or_128_t<T>>(value);
+ bool negative = is_negative(value);
+ // Don't do -abs_value since it trips unsigned-integer-overflow sanitizer.
+ if (negative) abs_value = ~abs_value + 1;
+ int num_digits = count_digits(abs_value);
+ auto size = (negative ? 1 : 0) + static_cast<size_t>(num_digits);
+ auto it = reserve(out, size);
+ if (auto ptr = to_pointer<Char>(it, size)) {
+ if (negative) *ptr++ = static_cast<Char>('-');
+ format_decimal<Char>(ptr, abs_value, num_digits);
+ return out;
+ }
+ if (negative) *it++ = static_cast<Char>('-');
+ it = format_decimal<Char>(it, abs_value, num_digits).end;
+ return base_iterator(out, it);
+}
+
+// A floating-point presentation format.
+enum class float_format : unsigned char {
+ general, // General: exponent notation or fixed point based on magnitude.
+ exp, // Exponent notation with the default precision of 6, e.g. 1.2e-3.
+ fixed, // Fixed point with the default precision of 6, e.g. 0.0012.
+ hex
+};
+
+struct float_specs {
+ int precision;
+ float_format format : 8;
+ sign_t sign : 8;
+ bool upper : 1;
+ bool locale : 1;
+ bool binary32 : 1;
+ bool showpoint : 1;
+};
+
+template <typename ErrorHandler = error_handler, typename Char>
+FMT_CONSTEXPR auto parse_float_type_spec(const format_specs<Char>& specs,
+ ErrorHandler&& eh = {})
+ -> float_specs {
+ auto result = float_specs();
+ result.showpoint = specs.alt;
+ result.locale = specs.localized;
+ switch (specs.type) {
+ case presentation_type::none:
+ result.format = float_format::general;
+ break;
+ case presentation_type::general_upper:
+ result.upper = true;
+ FMT_FALLTHROUGH;
+ case presentation_type::general_lower:
+ result.format = float_format::general;
+ break;
+ case presentation_type::exp_upper:
+ result.upper = true;
+ FMT_FALLTHROUGH;
+ case presentation_type::exp_lower:
+ result.format = float_format::exp;
+ result.showpoint |= specs.precision != 0;
+ break;
+ case presentation_type::fixed_upper:
+ result.upper = true;
+ FMT_FALLTHROUGH;
+ case presentation_type::fixed_lower:
+ result.format = float_format::fixed;
+ result.showpoint |= specs.precision != 0;
+ break;
+ case presentation_type::hexfloat_upper:
+ result.upper = true;
+ FMT_FALLTHROUGH;
+ case presentation_type::hexfloat_lower:
+ result.format = float_format::hex;
+ break;
+ default:
+ eh.on_error("invalid format specifier");
+ break;
+ }
+ return result;
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR20 auto write_nonfinite(OutputIt out, bool isnan,
+ format_specs<Char> specs,
+ const float_specs& fspecs) -> OutputIt {
+ auto str =
+ isnan ? (fspecs.upper ? "NAN" : "nan") : (fspecs.upper ? "INF" : "inf");
+ constexpr size_t str_size = 3;
+ auto sign = fspecs.sign;
+ auto size = str_size + (sign ? 1 : 0);
+ // Replace '0'-padding with space for non-finite values.
+ const bool is_zero_fill =
+ specs.fill.size() == 1 && *specs.fill.data() == static_cast<Char>('0');
+ if (is_zero_fill) specs.fill[0] = static_cast<Char>(' ');
+ return write_padded(out, specs, size, [=](reserve_iterator<OutputIt> it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ return copy_str<Char>(str, str + str_size, it);
+ });
+}
+
+// A decimal floating-point number significand * pow(10, exp).
+struct big_decimal_fp {
+ const char* significand;
+ int significand_size;
+ int exponent;
+};
+
+constexpr auto get_significand_size(const big_decimal_fp& f) -> int {
+ return f.significand_size;
+}
+template <typename T>
+inline auto get_significand_size(const dragonbox::decimal_fp<T>& f) -> int {
+ return count_digits(f.significand);
+}
+
+template <typename Char, typename OutputIt>
+constexpr auto write_significand(OutputIt out, const char* significand,
+ int significand_size) -> OutputIt {
+ return copy_str<Char>(significand, significand + significand_size, out);
+}
+template <typename Char, typename OutputIt, typename UInt>
+inline auto write_significand(OutputIt out, UInt significand,
+ int significand_size) -> OutputIt {
+ return format_decimal<Char>(out, significand, significand_size).end;
+}
+template <typename Char, typename OutputIt, typename T, typename Grouping>
+FMT_CONSTEXPR20 auto write_significand(OutputIt out, T significand,
+ int significand_size, int exponent,
+ const Grouping& grouping) -> OutputIt {
+ if (!grouping.has_separator()) {
+ out = write_significand<Char>(out, significand, significand_size);
+ return detail::fill_n(out, exponent, static_cast<Char>('0'));
+ }
+ auto buffer = memory_buffer();
+ write_significand<char>(appender(buffer), significand, significand_size);
+ detail::fill_n(appender(buffer), exponent, '0');
+ return grouping.apply(out, string_view(buffer.data(), buffer.size()));
+}
+
+template <typename Char, typename UInt,
+ FMT_ENABLE_IF(std::is_integral<UInt>::value)>
+inline auto write_significand(Char* out, UInt significand, int significand_size,
+ int integral_size, Char decimal_point) -> Char* {
+ if (!decimal_point)
+ return format_decimal(out, significand, significand_size).end;
+ out += significand_size + 1;
+ Char* end = out;
+ int floating_size = significand_size - integral_size;
+ for (int i = floating_size / 2; i > 0; --i) {
+ out -= 2;
+ copy2(out, digits2(static_cast<std::size_t>(significand % 100)));
+ significand /= 100;
+ }
+ if (floating_size % 2 != 0) {
+ *--out = static_cast<Char>('0' + significand % 10);
+ significand /= 10;
+ }
+ *--out = decimal_point;
+ format_decimal(out - integral_size, significand, integral_size);
+ return end;
+}
+
+template <typename OutputIt, typename UInt, typename Char,
+ FMT_ENABLE_IF(!std::is_pointer<remove_cvref_t<OutputIt>>::value)>
+inline auto write_significand(OutputIt out, UInt significand,
+ int significand_size, int integral_size,
+ Char decimal_point) -> OutputIt {
+ // Buffer is large enough to hold digits (digits10 + 1) and a decimal point.
+ Char buffer[digits10<UInt>() + 2];
+ auto end = write_significand(buffer, significand, significand_size,
+ integral_size, decimal_point);
+ return detail::copy_str_noinline<Char>(buffer, end, out);
+}
+
+template <typename OutputIt, typename Char>
+FMT_CONSTEXPR auto write_significand(OutputIt out, const char* significand,
+ int significand_size, int integral_size,
+ Char decimal_point) -> OutputIt {
+ out = detail::copy_str_noinline<Char>(significand,
+ significand + integral_size, out);
+ if (!decimal_point) return out;
+ *out++ = decimal_point;
+ return detail::copy_str_noinline<Char>(significand + integral_size,
+ significand + significand_size, out);
+}
+
+template <typename OutputIt, typename Char, typename T, typename Grouping>
+FMT_CONSTEXPR20 auto write_significand(OutputIt out, T significand,
+ int significand_size, int integral_size,
+ Char decimal_point,
+ const Grouping& grouping) -> OutputIt {
+ if (!grouping.has_separator()) {
+ return write_significand(out, significand, significand_size, integral_size,
+ decimal_point);
+ }
+ auto buffer = basic_memory_buffer<Char>();
+ write_significand(buffer_appender<Char>(buffer), significand,
+ significand_size, integral_size, decimal_point);
+ grouping.apply(
+ out, basic_string_view<Char>(buffer.data(), to_unsigned(integral_size)));
+ return detail::copy_str_noinline<Char>(buffer.data() + integral_size,
+ buffer.end(), out);
+}
+
+template <typename OutputIt, typename DecimalFP, typename Char,
+ typename Grouping = digit_grouping<Char>>
+FMT_CONSTEXPR20 auto do_write_float(OutputIt out, const DecimalFP& f,
+ const format_specs<Char>& specs,
+ float_specs fspecs, locale_ref loc)
+ -> OutputIt {
+ auto significand = f.significand;
+ int significand_size = get_significand_size(f);
+ const Char zero = static_cast<Char>('0');
+ auto sign = fspecs.sign;
+ size_t size = to_unsigned(significand_size) + (sign ? 1 : 0);
+ using iterator = reserve_iterator<OutputIt>;
+
+ Char decimal_point =
+ fspecs.locale ? detail::decimal_point<Char>(loc) : static_cast<Char>('.');
+
+ int output_exp = f.exponent + significand_size - 1;
+ auto use_exp_format = [=]() {
+ if (fspecs.format == float_format::exp) return true;
+ if (fspecs.format != float_format::general) return false;
+ // Use the fixed notation if the exponent is in [exp_lower, exp_upper),
+ // e.g. 0.0001 instead of 1e-04. Otherwise use the exponent notation.
+ const int exp_lower = -4, exp_upper = 16;
+ return output_exp < exp_lower ||
+ output_exp >= (fspecs.precision > 0 ? fspecs.precision : exp_upper);
+ };
+ if (use_exp_format()) {
+ int num_zeros = 0;
+ if (fspecs.showpoint) {
+ num_zeros = fspecs.precision - significand_size;
+ if (num_zeros < 0) num_zeros = 0;
+ size += to_unsigned(num_zeros);
+ } else if (significand_size == 1) {
+ decimal_point = Char();
+ }
+ auto abs_output_exp = output_exp >= 0 ? output_exp : -output_exp;
+ int exp_digits = 2;
+ if (abs_output_exp >= 100) exp_digits = abs_output_exp >= 1000 ? 4 : 3;
+
+ size += to_unsigned((decimal_point ? 1 : 0) + 2 + exp_digits);
+ char exp_char = fspecs.upper ? 'E' : 'e';
+ auto write = [=](iterator it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ // Insert a decimal point after the first digit and add an exponent.
+ it = write_significand(it, significand, significand_size, 1,
+ decimal_point);
+ if (num_zeros > 0) it = detail::fill_n(it, num_zeros, zero);
+ *it++ = static_cast<Char>(exp_char);
+ return write_exponent<Char>(output_exp, it);
+ };
+ return specs.width > 0 ? write_padded<align::right>(out, specs, size, write)
+ : base_iterator(out, write(reserve(out, size)));
+ }
+
+ int exp = f.exponent + significand_size;
+ if (f.exponent >= 0) {
+ // 1234e5 -> 123400000[.0+]
+ size += to_unsigned(f.exponent);
+ int num_zeros = fspecs.precision - exp;
+ abort_fuzzing_if(num_zeros > 5000);
+ if (fspecs.showpoint) {
+ ++size;
+ if (num_zeros <= 0 && fspecs.format != float_format::fixed) num_zeros = 0;
+ if (num_zeros > 0) size += to_unsigned(num_zeros);
+ }
+ auto grouping = Grouping(loc, fspecs.locale);
+ size += to_unsigned(grouping.count_separators(exp));
+ return write_padded<align::right>(out, specs, size, [&](iterator it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ it = write_significand<Char>(it, significand, significand_size,
+ f.exponent, grouping);
+ if (!fspecs.showpoint) return it;
+ *it++ = decimal_point;
+ return num_zeros > 0 ? detail::fill_n(it, num_zeros, zero) : it;
+ });
+ } else if (exp > 0) {
+ // 1234e-2 -> 12.34[0+]
+ int num_zeros = fspecs.showpoint ? fspecs.precision - significand_size : 0;
+ size += 1 + to_unsigned(num_zeros > 0 ? num_zeros : 0);
+ auto grouping = Grouping(loc, fspecs.locale);
+ size += to_unsigned(grouping.count_separators(exp));
+ return write_padded<align::right>(out, specs, size, [&](iterator it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ it = write_significand(it, significand, significand_size, exp,
+ decimal_point, grouping);
+ return num_zeros > 0 ? detail::fill_n(it, num_zeros, zero) : it;
+ });
+ }
+ // 1234e-6 -> 0.001234
+ int num_zeros = -exp;
+ if (significand_size == 0 && fspecs.precision >= 0 &&
+ fspecs.precision < num_zeros) {
+ num_zeros = fspecs.precision;
+ }
+ bool pointy = num_zeros != 0 || significand_size != 0 || fspecs.showpoint;
+ size += 1 + (pointy ? 1 : 0) + to_unsigned(num_zeros);
+ return write_padded<align::right>(out, specs, size, [&](iterator it) {
+ if (sign) *it++ = detail::sign<Char>(sign);
+ *it++ = zero;
+ if (!pointy) return it;
+ *it++ = decimal_point;
+ it = detail::fill_n(it, num_zeros, zero);
+ return write_significand<Char>(it, significand, significand_size);
+ });
+}
+
+template <typename Char> class fallback_digit_grouping {
+ public:
+ constexpr fallback_digit_grouping(locale_ref, bool) {}
+
+ constexpr bool has_separator() const { return false; }
+
+ constexpr int count_separators(int) const { return 0; }
+
+ template <typename Out, typename C>
+ constexpr Out apply(Out out, basic_string_view<C>) const {
+ return out;
+ }
+};
+
+template <typename OutputIt, typename DecimalFP, typename Char>
+FMT_CONSTEXPR20 auto write_float(OutputIt out, const DecimalFP& f,
+ const format_specs<Char>& specs,
+ float_specs fspecs, locale_ref loc)
+ -> OutputIt {
+ if (is_constant_evaluated()) {
+ return do_write_float<OutputIt, DecimalFP, Char,
+ fallback_digit_grouping<Char>>(out, f, specs, fspecs,
+ loc);
+ } else {
+ return do_write_float(out, f, specs, fspecs, loc);
+ }
+}
+
+template <typename T> constexpr bool isnan(T value) {
+ return !(value >= value); // std::isnan doesn't support __float128.
+}
+
+template <typename T, typename Enable = void>
+struct has_isfinite : std::false_type {};
+
+template <typename T>
+struct has_isfinite<T, enable_if_t<sizeof(std::isfinite(T())) != 0>>
+ : std::true_type {};
+
+template <typename T, FMT_ENABLE_IF(std::is_floating_point<T>::value&&
+ has_isfinite<T>::value)>
+FMT_CONSTEXPR20 bool isfinite(T value) {
+ constexpr T inf = T(std::numeric_limits<double>::infinity());
+ if (is_constant_evaluated())
+ return !detail::isnan(value) && value < inf && value > -inf;
+ return std::isfinite(value);
+}
+template <typename T, FMT_ENABLE_IF(!has_isfinite<T>::value)>
+FMT_CONSTEXPR bool isfinite(T value) {
+ T inf = T(std::numeric_limits<double>::infinity());
+ // std::isfinite doesn't support __float128.
+ return !detail::isnan(value) && value < inf && value > -inf;
+}
+
+template <typename T, FMT_ENABLE_IF(is_floating_point<T>::value)>
+FMT_INLINE FMT_CONSTEXPR bool signbit(T value) {
+ if (is_constant_evaluated()) {
+#ifdef __cpp_if_constexpr
+ if constexpr (std::numeric_limits<double>::is_iec559) {
+ auto bits = detail::bit_cast<uint64_t>(static_cast<double>(value));
+ return (bits >> (num_bits<uint64_t>() - 1)) != 0;
+ }
+#endif
+ }
+ return std::signbit(static_cast<double>(value));
+}
+
+enum class round_direction { unknown, up, down };
+
+// Given the divisor (normally a power of 10), the remainder = v % divisor for
+// some number v and the error, returns whether v should be rounded up, down, or
+// whether the rounding direction can't be determined due to error.
+// error should be less than divisor / 2.
+FMT_CONSTEXPR inline round_direction get_round_direction(uint64_t divisor,
+ uint64_t remainder,
+ uint64_t error) {
+ FMT_ASSERT(remainder < divisor, ""); // divisor - remainder won't overflow.
+ FMT_ASSERT(error < divisor, ""); // divisor - error won't overflow.
+ FMT_ASSERT(error < divisor - error, ""); // error * 2 won't overflow.
+ // Round down if (remainder + error) * 2 <= divisor.
+ if (remainder <= divisor - remainder && error * 2 <= divisor - remainder * 2)
+ return round_direction::down;
+ // Round up if (remainder - error) * 2 >= divisor.
+ if (remainder >= error &&
+ remainder - error >= divisor - (remainder - error)) {
+ return round_direction::up;
+ }
+ return round_direction::unknown;
+}
+
+namespace digits {
+enum result {
+ more, // Generate more digits.
+ done, // Done generating digits.
+ error // Digit generation cancelled due to an error.
+};
+}
+
+struct gen_digits_handler {
+ char* buf;
+ int size;
+ int precision;
+ int exp10;
+ bool fixed;
+
+ FMT_CONSTEXPR digits::result on_digit(char digit, uint64_t divisor,
+ uint64_t remainder, uint64_t error,
+ bool integral) {
+ FMT_ASSERT(remainder < divisor, "");
+ buf[size++] = digit;
+ if (!integral && error >= remainder) return digits::error;
+ if (size < precision) return digits::more;
+ if (!integral) {
+ // Check if error * 2 < divisor with overflow prevention.
+ // The check is not needed for the integral part because error = 1
+ // and divisor > (1 << 32) there.
+ if (error >= divisor || error >= divisor - error) return digits::error;
+ } else {
+ FMT_ASSERT(error == 1 && divisor > 2, "");
+ }
+ auto dir = get_round_direction(divisor, remainder, error);
+ if (dir != round_direction::up)
+ return dir == round_direction::down ? digits::done : digits::error;
+ ++buf[size - 1];
+ for (int i = size - 1; i > 0 && buf[i] > '9'; --i) {
+ buf[i] = '0';
+ ++buf[i - 1];
+ }
+ if (buf[0] > '9') {
+ buf[0] = '1';
+ if (fixed)
+ buf[size++] = '0';
+ else
+ ++exp10;
+ }
+ return digits::done;
+ }
+};
+
+inline FMT_CONSTEXPR20 void adjust_precision(int& precision, int exp10) {
+ // Adjust fixed precision by exponent because it is relative to decimal
+ // point.
+ if (exp10 > 0 && precision > max_value<int>() - exp10)
+ FMT_THROW(format_error("number is too big"));
+ precision += exp10;
+}
+
+// Generates output using the Grisu digit-gen algorithm.
+// error: the size of the region (lower, upper) outside of which numbers
+// definitely do not round to value (Delta in Grisu3).
+FMT_INLINE FMT_CONSTEXPR20 auto grisu_gen_digits(fp value, uint64_t error,
+ int& exp,
+ gen_digits_handler& handler)
+ -> digits::result {
+ const fp one(1ULL << -value.e, value.e);
+ // The integral part of scaled value (p1 in Grisu) = value / one. It cannot be
+ // zero because it contains a product of two 64-bit numbers with MSB set (due
+ // to normalization) - 1, shifted right by at most 60 bits.
+ auto integral = static_cast<uint32_t>(value.f >> -one.e);
+ FMT_ASSERT(integral != 0, "");
+ FMT_ASSERT(integral == value.f >> -one.e, "");
+ // The fractional part of scaled value (p2 in Grisu) c = value % one.
+ uint64_t fractional = value.f & (one.f - 1);
+ exp = count_digits(integral); // kappa in Grisu.
+ // Non-fixed formats require at least one digit and no precision adjustment.
+ if (handler.fixed) {
+ adjust_precision(handler.precision, exp + handler.exp10);
+ // Check if precision is satisfied just by leading zeros, e.g.
+ // format("{:.2f}", 0.001) gives "0.00" without generating any digits.
+ if (handler.precision <= 0) {
+ if (handler.precision < 0) return digits::done;
+ // Divide by 10 to prevent overflow.
+ uint64_t divisor = data::power_of_10_64[exp - 1] << -one.e;
+ auto dir = get_round_direction(divisor, value.f / 10, error * 10);
+ if (dir == round_direction::unknown) return digits::error;
+ handler.buf[handler.size++] = dir == round_direction::up ? '1' : '0';
+ return digits::done;
+ }
+ }
+ // Generate digits for the integral part. This can produce up to 10 digits.
+ do {
+ uint32_t digit = 0;
+ auto divmod_integral = [&](uint32_t divisor) {
+ digit = integral / divisor;
+ integral %= divisor;
+ };
+ // This optimization by Milo Yip reduces the number of integer divisions by
+ // one per iteration.
+ switch (exp) {
+ case 10:
+ divmod_integral(1000000000);
+ break;
+ case 9:
+ divmod_integral(100000000);
+ break;
+ case 8:
+ divmod_integral(10000000);
+ break;
+ case 7:
+ divmod_integral(1000000);
+ break;
+ case 6:
+ divmod_integral(100000);
+ break;
+ case 5:
+ divmod_integral(10000);
+ break;
+ case 4:
+ divmod_integral(1000);
+ break;
+ case 3:
+ divmod_integral(100);
+ break;
+ case 2:
+ divmod_integral(10);
+ break;
+ case 1:
+ digit = integral;
+ integral = 0;
+ break;
+ default:
+ FMT_ASSERT(false, "invalid number of digits");
+ }
+ --exp;
+ auto remainder = (static_cast<uint64_t>(integral) << -one.e) + fractional;
+ auto result = handler.on_digit(static_cast<char>('0' + digit),
+ data::power_of_10_64[exp] << -one.e,
+ remainder, error, true);
+ if (result != digits::more) return result;
+ } while (exp > 0);
+ // Generate digits for the fractional part.
+ for (;;) {
+ fractional *= 10;
+ error *= 10;
+ char digit = static_cast<char>('0' + (fractional >> -one.e));
+ fractional &= one.f - 1;
+ --exp;
+ auto result = handler.on_digit(digit, one.f, fractional, error, false);
+ if (result != digits::more) return result;
+ }
+}
+
+class bigint {
+ private:
+ // A bigint is stored as an array of bigits (big digits), with bigit at index
+ // 0 being the least significant one.
+ using bigit = uint32_t;
+ using double_bigit = uint64_t;
+ enum { bigits_capacity = 32 };
+ basic_memory_buffer<bigit, bigits_capacity> bigits_;
+ int exp_;
+
+ FMT_CONSTEXPR20 bigit operator[](int index) const {
+ return bigits_[to_unsigned(index)];
+ }
+ FMT_CONSTEXPR20 bigit& operator[](int index) {
+ return bigits_[to_unsigned(index)];
+ }
+
+ static constexpr const int bigit_bits = num_bits<bigit>();
+
+ friend struct formatter<bigint>;
+
+ FMT_CONSTEXPR20 void subtract_bigits(int index, bigit other, bigit& borrow) {
+ auto result = static_cast<double_bigit>((*this)[index]) - other - borrow;
+ (*this)[index] = static_cast<bigit>(result);
+ borrow = static_cast<bigit>(result >> (bigit_bits * 2 - 1));
+ }
+
+ FMT_CONSTEXPR20 void remove_leading_zeros() {
+ int num_bigits = static_cast<int>(bigits_.size()) - 1;
+ while (num_bigits > 0 && (*this)[num_bigits] == 0) --num_bigits;
+ bigits_.resize(to_unsigned(num_bigits + 1));
+ }
+
+ // Computes *this -= other assuming aligned bigints and *this >= other.
+ FMT_CONSTEXPR20 void subtract_aligned(const bigint& other) {
+ FMT_ASSERT(other.exp_ >= exp_, "unaligned bigints");
+ FMT_ASSERT(compare(*this, other) >= 0, "");
+ bigit borrow = 0;
+ int i = other.exp_ - exp_;
+ for (size_t j = 0, n = other.bigits_.size(); j != n; ++i, ++j)
+ subtract_bigits(i, other.bigits_[j], borrow);
+ while (borrow > 0) subtract_bigits(i, 0, borrow);
+ remove_leading_zeros();
+ }
+
+ FMT_CONSTEXPR20 void multiply(uint32_t value) {
+ const double_bigit wide_value = value;
+ bigit carry = 0;
+ for (size_t i = 0, n = bigits_.size(); i < n; ++i) {
+ double_bigit result = bigits_[i] * wide_value + carry;
+ bigits_[i] = static_cast<bigit>(result);
+ carry = static_cast<bigit>(result >> bigit_bits);
+ }
+ if (carry != 0) bigits_.push_back(carry);
+ }
+
+ template <typename UInt, FMT_ENABLE_IF(std::is_same<UInt, uint64_t>::value ||
+ std::is_same<UInt, uint128_t>::value)>
+ FMT_CONSTEXPR20 void multiply(UInt value) {
+ using half_uint =
+ conditional_t<std::is_same<UInt, uint128_t>::value, uint64_t, uint32_t>;
+ const int shift = num_bits<half_uint>() - bigit_bits;
+ const UInt lower = static_cast<half_uint>(value);
+ const UInt upper = value >> num_bits<half_uint>();
+ UInt carry = 0;
+ for (size_t i = 0, n = bigits_.size(); i < n; ++i) {
+ UInt result = lower * bigits_[i] + static_cast<bigit>(carry);
+ carry = (upper * bigits_[i] << shift) + (result >> bigit_bits) +
+ (carry >> bigit_bits);
+ bigits_[i] = static_cast<bigit>(result);
+ }
+ while (carry != 0) {
+ bigits_.push_back(static_cast<bigit>(carry));
+ carry >>= bigit_bits;
+ }
+ }
+
+ template <typename UInt, FMT_ENABLE_IF(std::is_same<UInt, uint64_t>::value ||
+ std::is_same<UInt, uint128_t>::value)>
+ FMT_CONSTEXPR20 void assign(UInt n) {
+ size_t num_bigits = 0;
+ do {
+ bigits_[num_bigits++] = static_cast<bigit>(n);
+ n >>= bigit_bits;
+ } while (n != 0);
+ bigits_.resize(num_bigits);
+ exp_ = 0;
+ }
+
+ public:
+ FMT_CONSTEXPR20 bigint() : exp_(0) {}
+ explicit bigint(uint64_t n) { assign(n); }
+
+ bigint(const bigint&) = delete;
+ void operator=(const bigint&) = delete;
+
+ FMT_CONSTEXPR20 void assign(const bigint& other) {
+ auto size = other.bigits_.size();
+ bigits_.resize(size);
+ auto data = other.bigits_.data();
+ std::copy(data, data + size, make_checked(bigits_.data(), size));
+ exp_ = other.exp_;
+ }
+
+ template <typename Int> FMT_CONSTEXPR20 void operator=(Int n) {
+ FMT_ASSERT(n > 0, "");
+ assign(uint64_or_128_t<Int>(n));
+ }
+
+ FMT_CONSTEXPR20 int num_bigits() const {
+ return static_cast<int>(bigits_.size()) + exp_;
+ }
+
+ FMT_NOINLINE FMT_CONSTEXPR20 bigint& operator<<=(int shift) {
+ FMT_ASSERT(shift >= 0, "");
+ exp_ += shift / bigit_bits;
+ shift %= bigit_bits;
+ if (shift == 0) return *this;
+ bigit carry = 0;
+ for (size_t i = 0, n = bigits_.size(); i < n; ++i) {
+ bigit c = bigits_[i] >> (bigit_bits - shift);
+ bigits_[i] = (bigits_[i] << shift) + carry;
+ carry = c;
+ }
+ if (carry != 0) bigits_.push_back(carry);
+ return *this;
+ }
+
+ template <typename Int> FMT_CONSTEXPR20 bigint& operator*=(Int value) {
+ FMT_ASSERT(value > 0, "");
+ multiply(uint32_or_64_or_128_t<Int>(value));
+ return *this;
+ }
+
+ friend FMT_CONSTEXPR20 int compare(const bigint& lhs, const bigint& rhs) {
+ int num_lhs_bigits = lhs.num_bigits(), num_rhs_bigits = rhs.num_bigits();
+ if (num_lhs_bigits != num_rhs_bigits)
+ return num_lhs_bigits > num_rhs_bigits ? 1 : -1;
+ int i = static_cast<int>(lhs.bigits_.size()) - 1;
+ int j = static_cast<int>(rhs.bigits_.size()) - 1;
+ int end = i - j;
+ if (end < 0) end = 0;
+ for (; i >= end; --i, --j) {
+ bigit lhs_bigit = lhs[i], rhs_bigit = rhs[j];
+ if (lhs_bigit != rhs_bigit) return lhs_bigit > rhs_bigit ? 1 : -1;
+ }
+ if (i != j) return i > j ? 1 : -1;
+ return 0;
+ }
+
+ // Returns compare(lhs1 + lhs2, rhs).
+ friend FMT_CONSTEXPR20 int add_compare(const bigint& lhs1, const bigint& lhs2,
+ const bigint& rhs) {
+ auto minimum = [](int a, int b) { return a < b ? a : b; };
+ auto maximum = [](int a, int b) { return a > b ? a : b; };
+ int max_lhs_bigits = maximum(lhs1.num_bigits(), lhs2.num_bigits());
+ int num_rhs_bigits = rhs.num_bigits();
+ if (max_lhs_bigits + 1 < num_rhs_bigits) return -1;
+ if (max_lhs_bigits > num_rhs_bigits) return 1;
+ auto get_bigit = [](const bigint& n, int i) -> bigit {
+ return i >= n.exp_ && i < n.num_bigits() ? n[i - n.exp_] : 0;
+ };
+ double_bigit borrow = 0;
+ int min_exp = minimum(minimum(lhs1.exp_, lhs2.exp_), rhs.exp_);
+ for (int i = num_rhs_bigits - 1; i >= min_exp; --i) {
+ double_bigit sum =
+ static_cast<double_bigit>(get_bigit(lhs1, i)) + get_bigit(lhs2, i);
+ bigit rhs_bigit = get_bigit(rhs, i);
+ if (sum > rhs_bigit + borrow) return 1;
+ borrow = rhs_bigit + borrow - sum;
+ if (borrow > 1) return -1;
+ borrow <<= bigit_bits;
+ }
+ return borrow != 0 ? -1 : 0;
+ }
+
+ // Assigns pow(10, exp) to this bigint.
+ FMT_CONSTEXPR20 void assign_pow10(int exp) {
+ FMT_ASSERT(exp >= 0, "");
+ if (exp == 0) return *this = 1;
+ // Find the top bit.
+ int bitmask = 1;
+ while (exp >= bitmask) bitmask <<= 1;
+ bitmask >>= 1;
+ // pow(10, exp) = pow(5, exp) * pow(2, exp). First compute pow(5, exp) by
+ // repeated squaring and multiplication.
+ *this = 5;
+ bitmask >>= 1;
+ while (bitmask != 0) {
+ square();
+ if ((exp & bitmask) != 0) *this *= 5;
+ bitmask >>= 1;
+ }
+ *this <<= exp; // Multiply by pow(2, exp) by shifting.
+ }
+
+ FMT_CONSTEXPR20 void square() {
+ int num_bigits = static_cast<int>(bigits_.size());
+ int num_result_bigits = 2 * num_bigits;
+ basic_memory_buffer<bigit, bigits_capacity> n(std::move(bigits_));
+ bigits_.resize(to_unsigned(num_result_bigits));
+ auto sum = uint128_t();
+ for (int bigit_index = 0; bigit_index < num_bigits; ++bigit_index) {
+ // Compute bigit at position bigit_index of the result by adding
+ // cross-product terms n[i] * n[j] such that i + j == bigit_index.
+ for (int i = 0, j = bigit_index; j >= 0; ++i, --j) {
+ // Most terms are multiplied twice which can be optimized in the future.
+ sum += static_cast<double_bigit>(n[i]) * n[j];
+ }
+ (*this)[bigit_index] = static_cast<bigit>(sum);
+ sum >>= num_bits<bigit>(); // Compute the carry.
+ }
+ // Do the same for the top half.
+ for (int bigit_index = num_bigits; bigit_index < num_result_bigits;
+ ++bigit_index) {
+ for (int j = num_bigits - 1, i = bigit_index - j; i < num_bigits;)
+ sum += static_cast<double_bigit>(n[i++]) * n[j--];
+ (*this)[bigit_index] = static_cast<bigit>(sum);
+ sum >>= num_bits<bigit>();
+ }
+ remove_leading_zeros();
+ exp_ *= 2;
+ }
+
+ // If this bigint has a bigger exponent than other, adds trailing zero to make
+ // exponents equal. This simplifies some operations such as subtraction.
+ FMT_CONSTEXPR20 void align(const bigint& other) {
+ int exp_difference = exp_ - other.exp_;
+ if (exp_difference <= 0) return;
+ int num_bigits = static_cast<int>(bigits_.size());
+ bigits_.resize(to_unsigned(num_bigits + exp_difference));
+ for (int i = num_bigits - 1, j = i + exp_difference; i >= 0; --i, --j)
+ bigits_[j] = bigits_[i];
+ std::uninitialized_fill_n(bigits_.data(), exp_difference, 0);
+ exp_ -= exp_difference;
+ }
+
+ // Divides this bignum by divisor, assigning the remainder to this and
+ // returning the quotient.
+ FMT_CONSTEXPR20 int divmod_assign(const bigint& divisor) {
+ FMT_ASSERT(this != &divisor, "");
+ if (compare(*this, divisor) < 0) return 0;
+ FMT_ASSERT(divisor.bigits_[divisor.bigits_.size() - 1u] != 0, "");
+ align(divisor);
+ int quotient = 0;
+ do {
+ subtract_aligned(divisor);
+ ++quotient;
+ } while (compare(*this, divisor) >= 0);
+ return quotient;
+ }
+};
+
+// format_dragon flags.
+enum dragon {
+ predecessor_closer = 1,
+ fixup = 2, // Run fixup to correct exp10 which can be off by one.
+ fixed = 4,
+};
+
+// Formats a floating-point number using a variation of the Fixed-Precision
+// Positive Floating-Point Printout ((FPP)^2) algorithm by Steele & White:
+// https://fmt.dev/papers/p372-steele.pdf.
+FMT_CONSTEXPR20 inline void format_dragon(basic_fp<uint128_t> value,
+ unsigned flags, int num_digits,
+ buffer<char>& buf, int& exp10) {
+ bigint numerator; // 2 * R in (FPP)^2.
+ bigint denominator; // 2 * S in (FPP)^2.
+ // lower and upper are differences between value and corresponding boundaries.
+ bigint lower; // (M^- in (FPP)^2).
+ bigint upper_store; // upper's value if different from lower.
+ bigint* upper = nullptr; // (M^+ in (FPP)^2).
+ // Shift numerator and denominator by an extra bit or two (if lower boundary
+ // is closer) to make lower and upper integers. This eliminates multiplication
+ // by 2 during later computations.
+ bool is_predecessor_closer = (flags & dragon::predecessor_closer) != 0;
+ int shift = is_predecessor_closer ? 2 : 1;
+ if (value.e >= 0) {
+ numerator = value.f;
+ numerator <<= value.e + shift;
+ lower = 1;
+ lower <<= value.e;
+ if (is_predecessor_closer) {
+ upper_store = 1;
+ upper_store <<= value.e + 1;
+ upper = &upper_store;
+ }
+ denominator.assign_pow10(exp10);
+ denominator <<= shift;
+ } else if (exp10 < 0) {
+ numerator.assign_pow10(-exp10);
+ lower.assign(numerator);
+ if (is_predecessor_closer) {
+ upper_store.assign(numerator);
+ upper_store <<= 1;
+ upper = &upper_store;
+ }
+ numerator *= value.f;
+ numerator <<= shift;
+ denominator = 1;
+ denominator <<= shift - value.e;
+ } else {
+ numerator = value.f;
+ numerator <<= shift;
+ denominator.assign_pow10(exp10);
+ denominator <<= shift - value.e;
+ lower = 1;
+ if (is_predecessor_closer) {
+ upper_store = 1ULL << 1;
+ upper = &upper_store;
+ }
+ }
+ int even = static_cast<int>((value.f & 1) == 0);
+ if (!upper) upper = &lower;
+ if ((flags & dragon::fixup) != 0) {
+ if (add_compare(numerator, *upper, denominator) + even <= 0) {
+ --exp10;
+ numerator *= 10;
+ if (num_digits < 0) {
+ lower *= 10;
+ if (upper != &lower) *upper *= 10;
+ }
+ }
+ if ((flags & dragon::fixed) != 0) adjust_precision(num_digits, exp10 + 1);
+ }
+ // Invariant: value == (numerator / denominator) * pow(10, exp10).
+ if (num_digits < 0) {
+ // Generate the shortest representation.
+ num_digits = 0;
+ char* data = buf.data();
+ for (;;) {
+ int digit = numerator.divmod_assign(denominator);
+ bool low = compare(numerator, lower) - even < 0; // numerator <[=] lower.
+ // numerator + upper >[=] pow10:
+ bool high = add_compare(numerator, *upper, denominator) + even > 0;
+ data[num_digits++] = static_cast<char>('0' + digit);
+ if (low || high) {
+ if (!low) {
+ ++data[num_digits - 1];
+ } else if (high) {
+ int result = add_compare(numerator, numerator, denominator);
+ // Round half to even.
+ if (result > 0 || (result == 0 && (digit % 2) != 0))
+ ++data[num_digits - 1];
+ }
+ buf.try_resize(to_unsigned(num_digits));
+ exp10 -= num_digits - 1;
+ return;
+ }
+ numerator *= 10;
+ lower *= 10;
+ if (upper != &lower) *upper *= 10;
+ }
+ }
+ // Generate the given number of digits.
+ exp10 -= num_digits - 1;
+ if (num_digits == 0) {
+ denominator *= 10;
+ auto digit = add_compare(numerator, numerator, denominator) > 0 ? '1' : '0';
+ buf.push_back(digit);
+ return;
+ }
+ buf.try_resize(to_unsigned(num_digits));
+ for (int i = 0; i < num_digits - 1; ++i) {
+ int digit = numerator.divmod_assign(denominator);
+ buf[i] = static_cast<char>('0' + digit);
+ numerator *= 10;
+ }
+ int digit = numerator.divmod_assign(denominator);
+ auto result = add_compare(numerator, numerator, denominator);
+ if (result > 0 || (result == 0 && (digit % 2) != 0)) {
+ if (digit == 9) {
+ const auto overflow = '0' + 10;
+ buf[num_digits - 1] = overflow;
+ // Propagate the carry.
+ for (int i = num_digits - 1; i > 0 && buf[i] == overflow; --i) {
+ buf[i] = '0';
+ ++buf[i - 1];
+ }
+ if (buf[0] == overflow) {
+ buf[0] = '1';
+ ++exp10;
+ }
+ return;
+ }
+ ++digit;
+ }
+ buf[num_digits - 1] = static_cast<char>('0' + digit);
+}
+
+// Formats a floating-point number using the hexfloat format.
+template <typename Float, FMT_ENABLE_IF(!is_double_double<Float>::value)>
+FMT_CONSTEXPR20 void format_hexfloat(Float value, int precision,
+ float_specs specs, buffer<char>& buf) {
+ // float is passed as double to reduce the number of instantiations and to
+ // simplify implementation.
+ static_assert(!std::is_same<Float, float>::value, "");
+
+ using info = dragonbox::float_info<Float>;
+
+ // Assume Float is in the format [sign][exponent][significand].
+ using carrier_uint = typename info::carrier_uint;
+
+ constexpr auto num_float_significand_bits =
+ detail::num_significand_bits<Float>();
+
+ basic_fp<carrier_uint> f(value);
+ f.e += num_float_significand_bits;
+ if (!has_implicit_bit<Float>()) --f.e;
+
+ constexpr auto num_fraction_bits =
+ num_float_significand_bits + (has_implicit_bit<Float>() ? 1 : 0);
+ constexpr auto num_xdigits = (num_fraction_bits + 3) / 4;
+
+ constexpr auto leading_shift = ((num_xdigits - 1) * 4);
+ const auto leading_mask = carrier_uint(0xF) << leading_shift;
+ const auto leading_xdigit =
+ static_cast<uint32_t>((f.f & leading_mask) >> leading_shift);
+ if (leading_xdigit > 1) f.e -= (32 - countl_zero(leading_xdigit) - 1);
+
+ int print_xdigits = num_xdigits - 1;
+ if (precision >= 0 && print_xdigits > precision) {
+ const int shift = ((print_xdigits - precision - 1) * 4);
+ const auto mask = carrier_uint(0xF) << shift;
+ const auto v = static_cast<uint32_t>((f.f & mask) >> shift);
+
+ if (v >= 8) {
+ const auto inc = carrier_uint(1) << (shift + 4);
+ f.f += inc;
+ f.f &= ~(inc - 1);
+ }
+
+ // Check long double overflow
+ if (!has_implicit_bit<Float>()) {
+ const auto implicit_bit = carrier_uint(1) << num_float_significand_bits;
+ if ((f.f & implicit_bit) == implicit_bit) {
+ f.f >>= 4;
+ f.e += 4;
+ }
+ }
+
+ print_xdigits = precision;
+ }
+
+ char xdigits[num_bits<carrier_uint>() / 4];
+ detail::fill_n(xdigits, sizeof(xdigits), '0');
+ format_uint<4>(xdigits, f.f, num_xdigits, specs.upper);
+
+ // Remove zero tail
+ while (print_xdigits > 0 && xdigits[print_xdigits] == '0') --print_xdigits;
+
+ buf.push_back('0');
+ buf.push_back(specs.upper ? 'X' : 'x');
+ buf.push_back(xdigits[0]);
+ if (specs.showpoint || print_xdigits > 0 || print_xdigits < precision)
+ buf.push_back('.');
+ buf.append(xdigits + 1, xdigits + 1 + print_xdigits);
+ for (; print_xdigits < precision; ++print_xdigits) buf.push_back('0');
+
+ buf.push_back(specs.upper ? 'P' : 'p');
+
+ uint32_t abs_e;
+ if (f.e < 0) {
+ buf.push_back('-');
+ abs_e = static_cast<uint32_t>(-f.e);
+ } else {
+ buf.push_back('+');
+ abs_e = static_cast<uint32_t>(f.e);
+ }
+ format_decimal<char>(appender(buf), abs_e, detail::count_digits(abs_e));
+}
+
+template <typename Float, FMT_ENABLE_IF(is_double_double<Float>::value)>
+FMT_CONSTEXPR20 void format_hexfloat(Float value, int precision,
+ float_specs specs, buffer<char>& buf) {
+ format_hexfloat(static_cast<double>(value), precision, specs, buf);
+}
+
+template <typename Float>
+FMT_CONSTEXPR20 auto format_float(Float value, int precision, float_specs specs,
+ buffer<char>& buf) -> int {
+ // float is passed as double to reduce the number of instantiations.
+ static_assert(!std::is_same<Float, float>::value, "");
+ FMT_ASSERT(value >= 0, "value is negative");
+ auto converted_value = convert_float(value);
+
+ const bool fixed = specs.format == float_format::fixed;
+ if (value <= 0) { // <= instead of == to silence a warning.
+ if (precision <= 0 || !fixed) {
+ buf.push_back('0');
+ return 0;
+ }
+ buf.try_resize(to_unsigned(precision));
+ fill_n(buf.data(), precision, '0');
+ return -precision;
+ }
+
+ int exp = 0;
+ bool use_dragon = true;
+ unsigned dragon_flags = 0;
+ if (!is_fast_float<Float>()) {
+ const auto inv_log2_10 = 0.3010299956639812; // 1 / log2(10)
+ using info = dragonbox::float_info<decltype(converted_value)>;
+ const auto f = basic_fp<typename info::carrier_uint>(converted_value);
+ // Compute exp, an approximate power of 10, such that
+ // 10^(exp - 1) <= value < 10^exp or 10^exp <= value < 10^(exp + 1).
+ // This is based on log10(value) == log2(value) / log2(10) and approximation
+ // of log2(value) by e + num_fraction_bits idea from double-conversion.
+ exp = static_cast<int>(
+ std::ceil((f.e + count_digits<1>(f.f) - 1) * inv_log2_10 - 1e-10));
+ dragon_flags = dragon::fixup;
+ } else if (!is_constant_evaluated() && precision < 0) {
+ // Use Dragonbox for the shortest format.
+ if (specs.binary32) {
+ auto dec = dragonbox::to_decimal(static_cast<float>(value));
+ write<char>(buffer_appender<char>(buf), dec.significand);
+ return dec.exponent;
+ }
+ auto dec = dragonbox::to_decimal(static_cast<double>(value));
+ write<char>(buffer_appender<char>(buf), dec.significand);
+ return dec.exponent;
+ } else if (is_constant_evaluated()) {
+ // Use Grisu + Dragon4 for the given precision:
+ // https://www.cs.tufts.edu/~nr/cs257/archive/florian-loitsch/printf.pdf.
+ const int min_exp = -60; // alpha in Grisu.
+ int cached_exp10 = 0; // K in Grisu.
+ fp normalized = normalize(fp(converted_value));
+ const auto cached_pow = get_cached_power(
+ min_exp - (normalized.e + fp::num_significand_bits), cached_exp10);
+ normalized = normalized * cached_pow;
+ gen_digits_handler handler{buf.data(), 0, precision, -cached_exp10, fixed};
+ if (grisu_gen_digits(normalized, 1, exp, handler) != digits::error &&
+ !is_constant_evaluated()) {
+ exp += handler.exp10;
+ buf.try_resize(to_unsigned(handler.size));
+ use_dragon = false;
+ } else {
+ exp += handler.size - cached_exp10 - 1;
+ precision = handler.precision;
+ }
+ } else {
+ // Extract significand bits and exponent bits.
+ using info = dragonbox::float_info<double>;
+ auto br = bit_cast<uint64_t>(static_cast<double>(value));
+
+ const uint64_t significand_mask =
+ (static_cast<uint64_t>(1) << num_significand_bits<double>()) - 1;
+ uint64_t significand = (br & significand_mask);
+ int exponent = static_cast<int>((br & exponent_mask<double>()) >>
+ num_significand_bits<double>());
+
+ if (exponent != 0) { // Check if normal.
+ exponent -= exponent_bias<double>() + num_significand_bits<double>();
+ significand |=
+ (static_cast<uint64_t>(1) << num_significand_bits<double>());
+ significand <<= 1;
+ } else {
+ // Normalize subnormal inputs.
+ FMT_ASSERT(significand != 0, "zeros should not appear hear");
+ int shift = countl_zero(significand);
+ FMT_ASSERT(shift >= num_bits<uint64_t>() - num_significand_bits<double>(),
+ "");
+ shift -= (num_bits<uint64_t>() - num_significand_bits<double>() - 2);
+ exponent = (std::numeric_limits<double>::min_exponent -
+ num_significand_bits<double>()) -
+ shift;
+ significand <<= shift;
+ }
+
+ // Compute the first several nonzero decimal significand digits.
+ // We call the number we get the first segment.
+ const int k = info::kappa - dragonbox::floor_log10_pow2(exponent);
+ exp = -k;
+ const int beta = exponent + dragonbox::floor_log2_pow10(k);
+ uint64_t first_segment;
+ bool has_more_segments;
+ int digits_in_the_first_segment;
+ {
+ const auto r = dragonbox::umul192_upper128(
+ significand << beta, dragonbox::get_cached_power(k));
+ first_segment = r.high();
+ has_more_segments = r.low() != 0;
+
+ // The first segment can have 18 ~ 19 digits.
+ if (first_segment >= 1000000000000000000ULL) {
+ digits_in_the_first_segment = 19;
+ } else {
+ // When it is of 18-digits, we align it to 19-digits by adding a bogus
+ // zero at the end.
+ digits_in_the_first_segment = 18;
+ first_segment *= 10;
+ }
+ }
+
+ // Compute the actual number of decimal digits to print.
+ if (fixed) {
+ adjust_precision(precision, exp + digits_in_the_first_segment);
+ }
+
+ // Use Dragon4 only when there might be not enough digits in the first
+ // segment.
+ if (digits_in_the_first_segment > precision) {
+ use_dragon = false;
+
+ if (precision <= 0) {
+ exp += digits_in_the_first_segment;
+
+ if (precision < 0) {
+ // Nothing to do, since all we have are just leading zeros.
+ buf.try_resize(0);
+ } else {
+ // We may need to round-up.
+ buf.try_resize(1);
+ if ((first_segment | static_cast<uint64_t>(has_more_segments)) >
+ 5000000000000000000ULL) {
+ buf[0] = '1';
+ } else {
+ buf[0] = '0';
+ }
+ }
+ } // precision <= 0
+ else {
+ exp += digits_in_the_first_segment - precision;
+
+ // When precision > 0, we divide the first segment into three
+ // subsegments, each with 9, 9, and 0 ~ 1 digits so that each fits
+ // in 32-bits which usually allows faster calculation than in
+ // 64-bits. Since some compiler (e.g. MSVC) doesn't know how to optimize
+ // division-by-constant for large 64-bit divisors, we do it here
+ // manually. The magic number 7922816251426433760 below is equal to
+ // ceil(2^(64+32) / 10^10).
+ const uint32_t first_subsegment = static_cast<uint32_t>(
+ dragonbox::umul128_upper64(first_segment, 7922816251426433760ULL) >>
+ 32);
+ const uint64_t second_third_subsegments =
+ first_segment - first_subsegment * 10000000000ULL;
+
+ uint64_t prod;
+ uint32_t digits;
+ bool should_round_up;
+ int number_of_digits_to_print = precision > 9 ? 9 : precision;
+
+ // Print a 9-digits subsegment, either the first or the second.
+ auto print_subsegment = [&](uint32_t subsegment, char* buffer) {
+ int number_of_digits_printed = 0;
+
+ // If we want to print an odd number of digits from the subsegment,
+ if ((number_of_digits_to_print & 1) != 0) {
+ // Convert to 64-bit fixed-point fractional form with 1-digit
+ // integer part. The magic number 720575941 is a good enough
+ // approximation of 2^(32 + 24) / 10^8; see
+ // https://jk-jeon.github.io/posts/2022/12/fixed-precision-formatting/#fixed-length-case
+ // for details.
+ prod = ((subsegment * static_cast<uint64_t>(720575941)) >> 24) + 1;
+ digits = static_cast<uint32_t>(prod >> 32);
+ *buffer = static_cast<char>('0' + digits);
+ number_of_digits_printed++;
+ }
+ // If we want to print an even number of digits from the
+ // first_subsegment,
+ else {
+ // Convert to 64-bit fixed-point fractional form with 2-digits
+ // integer part. The magic number 450359963 is a good enough
+ // approximation of 2^(32 + 20) / 10^7; see
+ // https://jk-jeon.github.io/posts/2022/12/fixed-precision-formatting/#fixed-length-case
+ // for details.
+ prod = ((subsegment * static_cast<uint64_t>(450359963)) >> 20) + 1;
+ digits = static_cast<uint32_t>(prod >> 32);
+ copy2(buffer, digits2(digits));
+ number_of_digits_printed += 2;
+ }
+
+ // Print all digit pairs.
+ while (number_of_digits_printed < number_of_digits_to_print) {
+ prod = static_cast<uint32_t>(prod) * static_cast<uint64_t>(100);
+ digits = static_cast<uint32_t>(prod >> 32);
+ copy2(buffer + number_of_digits_printed, digits2(digits));
+ number_of_digits_printed += 2;
+ }
+ };
+
+ // Print first subsegment.
+ print_subsegment(first_subsegment, buf.data());
+
+ // Perform rounding if the first subsegment is the last subsegment to
+ // print.
+ if (precision <= 9) {
+ // Rounding inside the subsegment.
+ // We round-up if:
+ // - either the fractional part is strictly larger than 1/2, or
+ // - the fractional part is exactly 1/2 and the last digit is odd.
+ // We rely on the following observations:
+ // - If fractional_part >= threshold, then the fractional part is
+ // strictly larger than 1/2.
+ // - If the MSB of fractional_part is set, then the fractional part
+ // must be at least 1/2.
+ // - When the MSB of fractional_part is set, either
+ // second_third_subsegments being nonzero or has_more_segments
+ // being true means there are further digits not printed, so the
+ // fractional part is strictly larger than 1/2.
+ if (precision < 9) {
+ uint32_t fractional_part = static_cast<uint32_t>(prod);
+ should_round_up = fractional_part >=
+ data::fractional_part_rounding_thresholds
+ [8 - number_of_digits_to_print] ||
+ ((fractional_part >> 31) &
+ ((digits & 1) | (second_third_subsegments != 0) |
+ has_more_segments)) != 0;
+ }
+ // Rounding at the subsegment boundary.
+ // In this case, the fractional part is at least 1/2 if and only if
+ // second_third_subsegments >= 5000000000ULL, and is strictly larger
+ // than 1/2 if we further have either second_third_subsegments >
+ // 5000000000ULL or has_more_segments == true.
+ else {
+ should_round_up = second_third_subsegments > 5000000000ULL ||
+ (second_third_subsegments == 5000000000ULL &&
+ ((digits & 1) != 0 || has_more_segments));
+ }
+ }
+ // Otherwise, print the second subsegment.
+ else {
+ // Compilers are not aware of how to leverage the maximum value of
+ // second_third_subsegments to find out a better magic number which
+ // allows us to eliminate an additional shift. 1844674407370955162 =
+ // ceil(2^64/10) < ceil(2^64*(10^9/(10^10 - 1))).
+ const uint32_t second_subsegment =
+ static_cast<uint32_t>(dragonbox::umul128_upper64(
+ second_third_subsegments, 1844674407370955162ULL));
+ const uint32_t third_subsegment =
+ static_cast<uint32_t>(second_third_subsegments) -
+ second_subsegment * 10;
+
+ number_of_digits_to_print = precision - 9;
+ print_subsegment(second_subsegment, buf.data() + 9);
+
+ // Rounding inside the subsegment.
+ if (precision < 18) {
+ // The condition third_subsegment != 0 implies that the segment was
+ // of 19 digits, so in this case the third segment should be
+ // consisting of a genuine digit from the input.
+ uint32_t fractional_part = static_cast<uint32_t>(prod);
+ should_round_up = fractional_part >=
+ data::fractional_part_rounding_thresholds
+ [8 - number_of_digits_to_print] ||
+ ((fractional_part >> 31) &
+ ((digits & 1) | (third_subsegment != 0) |
+ has_more_segments)) != 0;
+ }
+ // Rounding at the subsegment boundary.
+ else {
+ // In this case, the segment must be of 19 digits, thus
+ // the third subsegment should be consisting of a genuine digit from
+ // the input.
+ should_round_up = third_subsegment > 5 ||
+ (third_subsegment == 5 &&
+ ((digits & 1) != 0 || has_more_segments));
+ }
+ }
+
+ // Round-up if necessary.
+ if (should_round_up) {
+ ++buf[precision - 1];
+ for (int i = precision - 1; i > 0 && buf[i] > '9'; --i) {
+ buf[i] = '0';
+ ++buf[i - 1];
+ }
+ if (buf[0] > '9') {
+ buf[0] = '1';
+ if (fixed)
+ buf[precision++] = '0';
+ else
+ ++exp;
+ }
+ }
+ buf.try_resize(to_unsigned(precision));
+ }
+ } // if (digits_in_the_first_segment > precision)
+ else {
+ // Adjust the exponent for its use in Dragon4.
+ exp += digits_in_the_first_segment - 1;
+ }
+ }
+ if (use_dragon) {
+ auto f = basic_fp<uint128_t>();
+ bool is_predecessor_closer = specs.binary32
+ ? f.assign(static_cast<float>(value))
+ : f.assign(converted_value);
+ if (is_predecessor_closer) dragon_flags |= dragon::predecessor_closer;
+ if (fixed) dragon_flags |= dragon::fixed;
+ // Limit precision to the maximum possible number of significant digits in
+ // an IEEE754 double because we don't need to generate zeros.
+ const int max_double_digits = 767;
+ if (precision > max_double_digits) precision = max_double_digits;
+ format_dragon(f, dragon_flags, precision, buf, exp);
+ }
+ if (!fixed && !specs.showpoint) {
+ // Remove trailing zeros.
+ auto num_digits = buf.size();
+ while (num_digits > 0 && buf[num_digits - 1] == '0') {
+ --num_digits;
+ ++exp;
+ }
+ buf.try_resize(num_digits);
+ }
+ return exp;
+}
+template <typename Char, typename OutputIt, typename T>
+FMT_CONSTEXPR20 auto write_float(OutputIt out, T value,
+ format_specs<Char> specs, locale_ref loc)
+ -> OutputIt {
+ float_specs fspecs = parse_float_type_spec(specs);
+ fspecs.sign = specs.sign;
+ if (detail::signbit(value)) { // value < 0 is false for NaN so use signbit.
+ fspecs.sign = sign::minus;
+ value = -value;
+ } else if (fspecs.sign == sign::minus) {
+ fspecs.sign = sign::none;
+ }
+
+ if (!detail::isfinite(value))
+ return write_nonfinite(out, detail::isnan(value), specs, fspecs);
+
+ if (specs.align == align::numeric && fspecs.sign) {
+ auto it = reserve(out, 1);
+ *it++ = detail::sign<Char>(fspecs.sign);
+ out = base_iterator(out, it);
+ fspecs.sign = sign::none;
+ if (specs.width != 0) --specs.width;
+ }
+
+ memory_buffer buffer;
+ if (fspecs.format == float_format::hex) {
+ if (fspecs.sign) buffer.push_back(detail::sign<char>(fspecs.sign));
+ format_hexfloat(convert_float(value), specs.precision, fspecs, buffer);
+ return write_bytes<align::right>(out, {buffer.data(), buffer.size()},
+ specs);
+ }
+ int precision = specs.precision >= 0 || specs.type == presentation_type::none
+ ? specs.precision
+ : 6;
+ if (fspecs.format == float_format::exp) {
+ if (precision == max_value<int>())
+ throw_format_error("number is too big");
+ else
+ ++precision;
+ } else if (fspecs.format != float_format::fixed && precision == 0) {
+ precision = 1;
+ }
+ if (const_check(std::is_same<T, float>())) fspecs.binary32 = true;
+ int exp = format_float(convert_float(value), precision, fspecs, buffer);
+ fspecs.precision = precision;
+ auto f = big_decimal_fp{buffer.data(), static_cast<int>(buffer.size()), exp};
+ return write_float(out, f, specs, fspecs, loc);
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_floating_point<T>::value)>
+FMT_CONSTEXPR20 auto write(OutputIt out, T value, format_specs<Char> specs,
+ locale_ref loc = {}) -> OutputIt {
+ if (const_check(!is_supported_floating_point(value))) return out;
+ return specs.localized && write_loc(out, value, specs, loc)
+ ? out
+ : write_float(out, value, specs, loc);
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_fast_float<T>::value)>
+FMT_CONSTEXPR20 auto write(OutputIt out, T value) -> OutputIt {
+ if (is_constant_evaluated()) return write(out, value, format_specs<Char>());
+ if (const_check(!is_supported_floating_point(value))) return out;
+
+ auto fspecs = float_specs();
+ if (detail::signbit(value)) {
+ fspecs.sign = sign::minus;
+ value = -value;
+ }
+
+ constexpr auto specs = format_specs<Char>();
+ using floaty = conditional_t<std::is_same<T, long double>::value, double, T>;
+ using floaty_uint = typename dragonbox::float_info<floaty>::carrier_uint;
+ floaty_uint mask = exponent_mask<floaty>();
+ if ((bit_cast<floaty_uint>(value) & mask) == mask)
+ return write_nonfinite(out, std::isnan(value), specs, fspecs);
+
+ auto dec = dragonbox::to_decimal(static_cast<floaty>(value));
+ return write_float(out, dec, specs, fspecs, {});
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_floating_point<T>::value &&
+ !is_fast_float<T>::value)>
+inline auto write(OutputIt out, T value) -> OutputIt {
+ return write(out, value, format_specs<Char>());
+}
+
+template <typename Char, typename OutputIt>
+auto write(OutputIt out, monostate, format_specs<Char> = {}, locale_ref = {})
+ -> OutputIt {
+ FMT_ASSERT(false, "");
+ return out;
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, basic_string_view<Char> value)
+ -> OutputIt {
+ auto it = reserve(out, value.size());
+ it = copy_str_noinline<Char>(value.begin(), value.end(), it);
+ return base_iterator(out, it);
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(is_string<T>::value)>
+constexpr auto write(OutputIt out, const T& value) -> OutputIt {
+ return write<Char>(out, to_string_view(value));
+}
+
+// FMT_ENABLE_IF() condition separated to workaround an MSVC bug.
+template <
+ typename Char, typename OutputIt, typename T,
+ bool check =
+ std::is_enum<T>::value && !std::is_same<T, Char>::value &&
+ mapped_type_constant<T, basic_format_context<OutputIt, Char>>::value !=
+ type::custom_type,
+ FMT_ENABLE_IF(check)>
+FMT_CONSTEXPR auto write(OutputIt out, T value) -> OutputIt {
+ return write<Char>(out, static_cast<underlying_t<T>>(value));
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(std::is_same<T, bool>::value)>
+FMT_CONSTEXPR auto write(OutputIt out, T value,
+ const format_specs<Char>& specs = {}, locale_ref = {})
+ -> OutputIt {
+ return specs.type != presentation_type::none &&
+ specs.type != presentation_type::string
+ ? write(out, value ? 1 : 0, specs, {})
+ : write_bytes(out, value ? "true" : "false", specs);
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR auto write(OutputIt out, Char value) -> OutputIt {
+ auto it = reserve(out, 1);
+ *it++ = value;
+ return base_iterator(out, it);
+}
+
+template <typename Char, typename OutputIt>
+FMT_CONSTEXPR_CHAR_TRAITS auto write(OutputIt out, const Char* value)
+ -> OutputIt {
+ if (value) return write(out, basic_string_view<Char>(value));
+ throw_format_error("string pointer is null");
+ return out;
+}
+
+template <typename Char, typename OutputIt, typename T,
+ FMT_ENABLE_IF(std::is_same<T, void>::value)>
+auto write(OutputIt out, const T* value, const format_specs<Char>& specs = {},
+ locale_ref = {}) -> OutputIt {
+ return write_ptr<Char>(out, bit_cast<uintptr_t>(value), &specs);
+}
+
+// A write overload that handles implicit conversions.
+template <typename Char, typename OutputIt, typename T,
+ typename Context = basic_format_context<OutputIt, Char>>
+FMT_CONSTEXPR auto write(OutputIt out, const T& value) -> enable_if_t<
+ std::is_class<T>::value && !is_string<T>::value &&
+ !is_floating_point<T>::value && !std::is_same<T, Char>::value &&
+ !std::is_same<T, remove_cvref_t<decltype(arg_mapper<Context>().map(
+ value))>>::value,
+ OutputIt> {
+ return write<Char>(out, arg_mapper<Context>().map(value));
+}
+
+template <typename Char, typename OutputIt, typename T,
+ typename Context = basic_format_context<OutputIt, Char>>
+FMT_CONSTEXPR auto write(OutputIt out, const T& value)
+ -> enable_if_t<mapped_type_constant<T, Context>::value == type::custom_type,
+ OutputIt> {
+ auto ctx = Context(out, {}, {});
+ return typename Context::template formatter_type<T>().format(value, ctx);
+}
+
+// An argument visitor that formats the argument and writes it via the output
+// iterator. It's a class and not a generic lambda for compatibility with C++11.
+template <typename Char> struct default_arg_formatter {
+ using iterator = buffer_appender<Char>;
+ using context = buffer_context<Char>;
+
+ iterator out;
+ basic_format_args<context> args;
+ locale_ref loc;
+
+ template <typename T> auto operator()(T value) -> iterator {
+ return write<Char>(out, value);
+ }
+ auto operator()(typename basic_format_arg<context>::handle h) -> iterator {
+ basic_format_parse_context<Char> parse_ctx({});
+ context format_ctx(out, args, loc);
+ h.format(parse_ctx, format_ctx);
+ return format_ctx.out();
+ }
+};
+
+template <typename Char> struct arg_formatter {
+ using iterator = buffer_appender<Char>;
+ using context = buffer_context<Char>;
+
+ iterator out;
+ const format_specs<Char>& specs;
+ locale_ref locale;
+
+ template <typename T>
+ FMT_CONSTEXPR FMT_INLINE auto operator()(T value) -> iterator {
+ return detail::write(out, value, specs, locale);
+ }
+ auto operator()(typename basic_format_arg<context>::handle) -> iterator {
+ // User-defined types are handled separately because they require access
+ // to the parse context.
+ return out;
+ }
+};
+
+template <typename Char> struct custom_formatter {
+ basic_format_parse_context<Char>& parse_ctx;
+ buffer_context<Char>& ctx;
+
+ void operator()(
+ typename basic_format_arg<buffer_context<Char>>::handle h) const {
+ h.format(parse_ctx, ctx);
+ }
+ template <typename T> void operator()(T) const {}
+};
+
+template <typename ErrorHandler> class width_checker {
+ public:
+ explicit FMT_CONSTEXPR width_checker(ErrorHandler& eh) : handler_(eh) {}
+
+ template <typename T, FMT_ENABLE_IF(is_integer<T>::value)>
+ FMT_CONSTEXPR auto operator()(T value) -> unsigned long long {
+ if (is_negative(value)) handler_.on_error("negative width");
+ return static_cast<unsigned long long>(value);
+ }
+
+ template <typename T, FMT_ENABLE_IF(!is_integer<T>::value)>
+ FMT_CONSTEXPR auto operator()(T) -> unsigned long long {
+ handler_.on_error("width is not integer");
+ return 0;
+ }
+
+ private:
+ ErrorHandler& handler_;
+};
+
+template <typename ErrorHandler> class precision_checker {
+ public:
+ explicit FMT_CONSTEXPR precision_checker(ErrorHandler& eh) : handler_(eh) {}
+
+ template <typename T, FMT_ENABLE_IF(is_integer<T>::value)>
+ FMT_CONSTEXPR auto operator()(T value) -> unsigned long long {
+ if (is_negative(value)) handler_.on_error("negative precision");
+ return static_cast<unsigned long long>(value);
+ }
+
+ template <typename T, FMT_ENABLE_IF(!is_integer<T>::value)>
+ FMT_CONSTEXPR auto operator()(T) -> unsigned long long {
+ handler_.on_error("precision is not integer");
+ return 0;
+ }
+
+ private:
+ ErrorHandler& handler_;
+};
+
+template <template <typename> class Handler, typename FormatArg,
+ typename ErrorHandler>
+FMT_CONSTEXPR auto get_dynamic_spec(FormatArg arg, ErrorHandler eh) -> int {
+ unsigned long long value = visit_format_arg(Handler<ErrorHandler>(eh), arg);
+ if (value > to_unsigned(max_value<int>())) eh.on_error("number is too big");
+ return static_cast<int>(value);
+}
+
+template <typename Context, typename ID>
+FMT_CONSTEXPR auto get_arg(Context& ctx, ID id) ->
+ typename Context::format_arg {
+ auto arg = ctx.arg(id);
+ if (!arg) ctx.on_error("argument not found");
+ return arg;
+}
+
+template <template <typename> class Handler, typename Context>
+FMT_CONSTEXPR void handle_dynamic_spec(int& value,
+ arg_ref<typename Context::char_type> ref,
+ Context& ctx) {
+ switch (ref.kind) {
+ case arg_id_kind::none:
+ break;
+ case arg_id_kind::index:
+ value = detail::get_dynamic_spec<Handler>(get_arg(ctx, ref.val.index),
+ ctx.error_handler());
+ break;
+ case arg_id_kind::name:
+ value = detail::get_dynamic_spec<Handler>(get_arg(ctx, ref.val.name),
+ ctx.error_handler());
+ break;
+ }
+}
+
+#if FMT_USE_USER_DEFINED_LITERALS
+template <typename Char> struct udl_formatter {
+ basic_string_view<Char> str;
+
+ template <typename... T>
+ auto operator()(T&&... args) const -> std::basic_string<Char> {
+ return vformat(str, fmt::make_format_args<buffer_context<Char>>(args...));
+ }
+};
+
+# if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <typename T, typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct statically_named_arg : view {
+ static constexpr auto name = Str.data;
+
+ const T& value;
+ statically_named_arg(const T& v) : value(v) {}
+};
+
+template <typename T, typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct is_named_arg<statically_named_arg<T, Char, N, Str>> : std::true_type {};
+
+template <typename T, typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct is_statically_named_arg<statically_named_arg<T, Char, N, Str>>
+ : std::true_type {};
+
+template <typename Char, size_t N,
+ fmt::detail_exported::fixed_string<Char, N> Str>
+struct udl_arg {
+ template <typename T> auto operator=(T&& value) const {
+ return statically_named_arg<T, Char, N, Str>(std::forward<T>(value));
+ }
+};
+# else
+template <typename Char> struct udl_arg {
+ const Char* str;
+
+ template <typename T> auto operator=(T&& value) const -> named_arg<Char, T> {
+ return {str, std::forward<T>(value)};
+ }
+};
+# endif
+#endif // FMT_USE_USER_DEFINED_LITERALS
+
+template <typename Locale, typename Char>
+auto vformat(const Locale& loc, basic_string_view<Char> fmt,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> std::basic_string<Char> {
+ auto buf = basic_memory_buffer<Char>();
+ detail::vformat_to(buf, fmt, args, detail::locale_ref(loc));
+ return {buf.data(), buf.size()};
+}
+
+using format_func = void (*)(detail::buffer<char>&, int, const char*);
+
+FMT_API void format_error_code(buffer<char>& out, int error_code,
+ string_view message) noexcept;
+
+FMT_API void report_error(format_func func, int error_code,
+ const char* message) noexcept;
+FMT_END_DETAIL_NAMESPACE
+
+FMT_API auto vsystem_error(int error_code, string_view format_str,
+ format_args args) -> std::system_error;
+
+/**
+ \rst
+ Constructs :class:`std::system_error` with a message formatted with
+ ``fmt::format(fmt, args...)``.
+ *error_code* is a system error code as given by ``errno``.
+
+ **Example**::
+
+ // This throws std::system_error with the description
+ // cannot open file 'madeup': No such file or directory
+ // or similar (system message may vary).
+ const char* filename = "madeup";
+ std::FILE* file = std::fopen(filename, "r");
+ if (!file)
+ throw fmt::system_error(errno, "cannot open file '{}'", filename);
+ \endrst
+*/
+template <typename... T>
+auto system_error(int error_code, format_string<T...> fmt, T&&... args)
+ -> std::system_error {
+ return vsystem_error(error_code, fmt, fmt::make_format_args(args...));
+}
+
+/**
+ \rst
+ Formats an error message for an error returned by an operating system or a
+ language runtime, for example a file opening error, and writes it to *out*.
+ The format is the same as the one used by ``std::system_error(ec, message)``
+ where ``ec`` is ``std::error_code(error_code, std::generic_category()})``.
+ It is implementation-defined but normally looks like:
+
+ .. parsed-literal::
+ *<message>*: *<system-message>*
+
+ where *<message>* is the passed message and *<system-message>* is the system
+ message corresponding to the error code.
+ *error_code* is a system error code as given by ``errno``.
+ \endrst
+ */
+FMT_API void format_system_error(detail::buffer<char>& out, int error_code,
+ const char* message) noexcept;
+
+// Reports a system error without throwing an exception.
+// Can be used to report errors from destructors.
+FMT_API void report_system_error(int error_code, const char* message) noexcept;
+
+/** Fast integer formatter. */
+class format_int {
+ private:
+ // Buffer should be large enough to hold all digits (digits10 + 1),
+ // a sign and a null character.
+ enum { buffer_size = std::numeric_limits<unsigned long long>::digits10 + 3 };
+ mutable char buffer_[buffer_size];
+ char* str_;
+
+ template <typename UInt> auto format_unsigned(UInt value) -> char* {
+ auto n = static_cast<detail::uint32_or_64_or_128_t<UInt>>(value);
+ return detail::format_decimal(buffer_, n, buffer_size - 1).begin;
+ }
+
+ template <typename Int> auto format_signed(Int value) -> char* {
+ auto abs_value = static_cast<detail::uint32_or_64_or_128_t<Int>>(value);
+ bool negative = value < 0;
+ if (negative) abs_value = 0 - abs_value;
+ auto begin = format_unsigned(abs_value);
+ if (negative) *--begin = '-';
+ return begin;
+ }
+
+ public:
+ explicit format_int(int value) : str_(format_signed(value)) {}
+ explicit format_int(long value) : str_(format_signed(value)) {}
+ explicit format_int(long long value) : str_(format_signed(value)) {}
+ explicit format_int(unsigned value) : str_(format_unsigned(value)) {}
+ explicit format_int(unsigned long value) : str_(format_unsigned(value)) {}
+ explicit format_int(unsigned long long value)
+ : str_(format_unsigned(value)) {}
+
+ /** Returns the number of characters written to the output buffer. */
+ auto size() const -> size_t {
+ return detail::to_unsigned(buffer_ - str_ + buffer_size - 1);
+ }
+
+ /**
+ Returns a pointer to the output buffer content. No terminating null
+ character is appended.
+ */
+ auto data() const -> const char* { return str_; }
+
+ /**
+ Returns a pointer to the output buffer content with terminating null
+ character appended.
+ */
+ auto c_str() const -> const char* {
+ buffer_[buffer_size - 1] = '\0';
+ return str_;
+ }
+
+ /**
+ \rst
+ Returns the content of the output buffer as an ``std::string``.
+ \endrst
+ */
+ auto str() const -> std::string { return std::string(str_, size()); }
+};
+
+template <typename T, typename Char>
+struct formatter<T, Char, enable_if_t<detail::has_format_as<T>::value>>
+ : private formatter<detail::format_as_t<T>> {
+ using base = formatter<detail::format_as_t<T>>;
+ using base::parse;
+
+ template <typename FormatContext>
+ auto format(const T& value, FormatContext& ctx) const -> decltype(ctx.out()) {
+ return base::format(format_as(value), ctx);
+ }
+};
+
+template <typename Char>
+struct formatter<void*, Char> : formatter<const void*, Char> {
+ template <typename FormatContext>
+ auto format(void* val, FormatContext& ctx) const -> decltype(ctx.out()) {
+ return formatter<const void*, Char>::format(val, ctx);
+ }
+};
+
+template <typename Char, size_t N>
+struct formatter<Char[N], Char> : formatter<basic_string_view<Char>, Char> {
+ template <typename FormatContext>
+ FMT_CONSTEXPR auto format(const Char* val, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return formatter<basic_string_view<Char>, Char>::format(val, ctx);
+ }
+};
+
+/**
+ \rst
+ Converts ``p`` to ``const void*`` for pointer formatting.
+
+ **Example**::
+
+ auto s = fmt::format("{}", fmt::ptr(p));
+ \endrst
+ */
+template <typename T> auto ptr(T p) -> const void* {
+ static_assert(std::is_pointer<T>::value, "");
+ return detail::bit_cast<const void*>(p);
+}
+template <typename T, typename Deleter>
+auto ptr(const std::unique_ptr<T, Deleter>& p) -> const void* {
+ return p.get();
+}
+template <typename T> auto ptr(const std::shared_ptr<T>& p) -> const void* {
+ return p.get();
+}
+
+/**
+ \rst
+ Converts ``e`` to the underlying type.
+
+ **Example**::
+
+ enum class color { red, green, blue };
+ auto s = fmt::format("{}", fmt::underlying(color::red));
+ \endrst
+ */
+template <typename Enum>
+constexpr auto underlying(Enum e) noexcept -> underlying_t<Enum> {
+ return static_cast<underlying_t<Enum>>(e);
+}
+
+namespace enums {
+template <typename Enum, FMT_ENABLE_IF(std::is_enum<Enum>::value)>
+constexpr auto format_as(Enum e) noexcept -> underlying_t<Enum> {
+ return static_cast<underlying_t<Enum>>(e);
+}
+} // namespace enums
+
+class bytes {
+ private:
+ string_view data_;
+ friend struct formatter<bytes>;
+
+ public:
+ explicit bytes(string_view data) : data_(data) {}
+};
+
+template <> struct formatter<bytes> {
+ private:
+ detail::dynamic_format_specs<> specs_;
+
+ public:
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const char* {
+ return parse_format_specs(ctx.begin(), ctx.end(), specs_, ctx,
+ detail::type::string_type);
+ }
+
+ template <typename FormatContext>
+ auto format(bytes b, FormatContext& ctx) -> decltype(ctx.out()) {
+ detail::handle_dynamic_spec<detail::width_checker>(specs_.width,
+ specs_.width_ref, ctx);
+ detail::handle_dynamic_spec<detail::precision_checker>(
+ specs_.precision, specs_.precision_ref, ctx);
+ return detail::write_bytes(ctx.out(), b.data_, specs_);
+ }
+};
+
+// group_digits_view is not derived from view because it copies the argument.
+template <typename T> struct group_digits_view { T value; };
+
+/**
+ \rst
+ Returns a view that formats an integer value using ',' as a locale-independent
+ thousands separator.
+
+ **Example**::
+
+ fmt::print("{}", fmt::group_digits(12345));
+ // Output: "12,345"
+ \endrst
+ */
+template <typename T> auto group_digits(T value) -> group_digits_view<T> {
+ return {value};
+}
+
+template <typename T> struct formatter<group_digits_view<T>> : formatter<T> {
+ private:
+ detail::dynamic_format_specs<> specs_;
+
+ public:
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const char* {
+ return parse_format_specs(ctx.begin(), ctx.end(), specs_, ctx,
+ detail::type::int_type);
+ }
+
+ template <typename FormatContext>
+ auto format(group_digits_view<T> t, FormatContext& ctx)
+ -> decltype(ctx.out()) {
+ detail::handle_dynamic_spec<detail::width_checker>(specs_.width,
+ specs_.width_ref, ctx);
+ detail::handle_dynamic_spec<detail::precision_checker>(
+ specs_.precision, specs_.precision_ref, ctx);
+ return detail::write_int(
+ ctx.out(), static_cast<detail::uint64_or_128_t<T>>(t.value), 0, specs_,
+ detail::digit_grouping<char>("\3", ","));
+ }
+};
+
+// DEPRECATED! join_view will be moved to ranges.h.
+template <typename It, typename Sentinel, typename Char = char>
+struct join_view : detail::view {
+ It begin;
+ Sentinel end;
+ basic_string_view<Char> sep;
+
+ join_view(It b, Sentinel e, basic_string_view<Char> s)
+ : begin(b), end(e), sep(s) {}
+};
+
+template <typename It, typename Sentinel, typename Char>
+struct formatter<join_view<It, Sentinel, Char>, Char> {
+ private:
+ using value_type =
+#ifdef __cpp_lib_ranges
+ std::iter_value_t<It>;
+#else
+ typename std::iterator_traits<It>::value_type;
+#endif
+ formatter<remove_cvref_t<value_type>, Char> value_formatter_;
+
+ public:
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> const Char* {
+ return value_formatter_.parse(ctx);
+ }
+
+ template <typename FormatContext>
+ auto format(const join_view<It, Sentinel, Char>& value,
+ FormatContext& ctx) const -> decltype(ctx.out()) {
+ auto it = value.begin;
+ auto out = ctx.out();
+ if (it != value.end) {
+ out = value_formatter_.format(*it, ctx);
+ ++it;
+ while (it != value.end) {
+ out = detail::copy_str<Char>(value.sep.begin(), value.sep.end(), out);
+ ctx.advance_to(out);
+ out = value_formatter_.format(*it, ctx);
+ ++it;
+ }
+ }
+ return out;
+ }
+};
+
+/**
+ Returns a view that formats the iterator range `[begin, end)` with elements
+ separated by `sep`.
+ */
+template <typename It, typename Sentinel>
+auto join(It begin, Sentinel end, string_view sep) -> join_view<It, Sentinel> {
+ return {begin, end, sep};
+}
+
+/**
+ \rst
+ Returns a view that formats `range` with elements separated by `sep`.
+
+ **Example**::
+
+ std::vector<int> v = {1, 2, 3};
+ fmt::print("{}", fmt::join(v, ", "));
+ // Output: "1, 2, 3"
+
+ ``fmt::join`` applies passed format specifiers to the range elements::
+
+ fmt::print("{:02}", fmt::join(v, ", "));
+ // Output: "01, 02, 03"
+ \endrst
+ */
+template <typename Range>
+auto join(Range&& range, string_view sep)
+ -> join_view<detail::iterator_t<Range>, detail::sentinel_t<Range>> {
+ return join(std::begin(range), std::end(range), sep);
+}
+
+/**
+ \rst
+ Converts *value* to ``std::string`` using the default format for type *T*.
+
+ **Example**::
+
+ #include <fmt/format.h>
+
+ std::string answer = fmt::to_string(42);
+ \endrst
+ */
+template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+inline auto to_string(const T& value) -> std::string {
+ auto buffer = memory_buffer();
+ detail::write<char>(appender(buffer), value);
+ return {buffer.data(), buffer.size()};
+}
+
+template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+FMT_NODISCARD inline auto to_string(T value) -> std::string {
+ // The buffer should be large enough to store the number including the sign
+ // or "false" for bool.
+ constexpr int max_size = detail::digits10<T>() + 2;
+ char buffer[max_size > 5 ? static_cast<unsigned>(max_size) : 5];
+ char* begin = buffer;
+ return std::string(begin, detail::write<char>(begin, value));
+}
+
+template <typename Char, size_t SIZE>
+FMT_NODISCARD auto to_string(const basic_memory_buffer<Char, SIZE>& buf)
+ -> std::basic_string<Char> {
+ auto size = buf.size();
+ detail::assume(size < std::basic_string<Char>().max_size());
+ return std::basic_string<Char>(buf.data(), size);
+}
+
+FMT_BEGIN_DETAIL_NAMESPACE
+
+template <typename Char>
+void vformat_to(buffer<Char>& buf, basic_string_view<Char> fmt,
+ typename vformat_args<Char>::type args, locale_ref loc) {
+ auto out = buffer_appender<Char>(buf);
+ if (fmt.size() == 2 && equal2(fmt.data(), "{}")) {
+ auto arg = args.get(0);
+ if (!arg) error_handler().on_error("argument not found");
+ visit_format_arg(default_arg_formatter<Char>{out, args, loc}, arg);
+ return;
+ }
+
+ struct format_handler : error_handler {
+ basic_format_parse_context<Char> parse_context;
+ buffer_context<Char> context;
+
+ format_handler(buffer_appender<Char> p_out, basic_string_view<Char> str,
+ basic_format_args<buffer_context<Char>> p_args,
+ locale_ref p_loc)
+ : parse_context(str), context(p_out, p_args, p_loc) {}
+
+ void on_text(const Char* begin, const Char* end) {
+ auto text = basic_string_view<Char>(begin, to_unsigned(end - begin));
+ context.advance_to(write<Char>(context.out(), text));
+ }
+
+ FMT_CONSTEXPR auto on_arg_id() -> int {
+ return parse_context.next_arg_id();
+ }
+ FMT_CONSTEXPR auto on_arg_id(int id) -> int {
+ return parse_context.check_arg_id(id), id;
+ }
+ FMT_CONSTEXPR auto on_arg_id(basic_string_view<Char> id) -> int {
+ int arg_id = context.arg_id(id);
+ if (arg_id < 0) on_error("argument not found");
+ return arg_id;
+ }
+
+ FMT_INLINE void on_replacement_field(int id, const Char*) {
+ auto arg = get_arg(context, id);
+ context.advance_to(visit_format_arg(
+ default_arg_formatter<Char>{context.out(), context.args(),
+ context.locale()},
+ arg));
+ }
+
+ auto on_format_specs(int id, const Char* begin, const Char* end)
+ -> const Char* {
+ auto arg = get_arg(context, id);
+ if (arg.type() == type::custom_type) {
+ parse_context.advance_to(begin);
+ visit_format_arg(custom_formatter<Char>{parse_context, context}, arg);
+ return parse_context.begin();
+ }
+ auto specs = detail::dynamic_format_specs<Char>();
+ begin = parse_format_specs(begin, end, specs, parse_context, arg.type());
+ detail::handle_dynamic_spec<detail::width_checker>(
+ specs.width, specs.width_ref, context);
+ detail::handle_dynamic_spec<detail::precision_checker>(
+ specs.precision, specs.precision_ref, context);
+ if (begin == end || *begin != '}')
+ on_error("missing '}' in format string");
+ auto f = arg_formatter<Char>{context.out(), specs, context.locale()};
+ context.advance_to(visit_format_arg(f, arg));
+ return begin;
+ }
+ };
+ detail::parse_format_string<false>(fmt, format_handler(out, fmt, args, loc));
+}
+
+#ifndef FMT_HEADER_ONLY
+extern template FMT_API void vformat_to(buffer<char>&, string_view,
+ typename vformat_args<>::type,
+ locale_ref);
+extern template FMT_API auto thousands_sep_impl<char>(locale_ref)
+ -> thousands_sep_result<char>;
+extern template FMT_API auto thousands_sep_impl<wchar_t>(locale_ref)
+ -> thousands_sep_result<wchar_t>;
+extern template FMT_API auto decimal_point_impl(locale_ref) -> char;
+extern template FMT_API auto decimal_point_impl(locale_ref) -> wchar_t;
+#endif // FMT_HEADER_ONLY
+
+FMT_END_DETAIL_NAMESPACE
+
+#if FMT_USE_USER_DEFINED_LITERALS
+inline namespace literals {
+/**
+ \rst
+ User-defined literal equivalent of :func:`fmt::arg`.
+
+ **Example**::
+
+ using namespace fmt::literals;
+ fmt::print("Elapsed time: {s:.2f} seconds", "s"_a=1.23);
+ \endrst
+ */
+# if FMT_USE_NONTYPE_TEMPLATE_ARGS
+template <detail_exported::fixed_string Str> constexpr auto operator""_a() {
+ using char_t = remove_cvref_t<decltype(Str.data[0])>;
+ return detail::udl_arg<char_t, sizeof(Str.data) / sizeof(char_t), Str>();
+}
+# else
+constexpr auto operator"" _a(const char* s, size_t) -> detail::udl_arg<char> {
+ return {s};
+}
+# endif
+} // namespace literals
+#endif // FMT_USE_USER_DEFINED_LITERALS
+
+template <typename Locale, FMT_ENABLE_IF(detail::is_locale<Locale>::value)>
+inline auto vformat(const Locale& loc, string_view fmt, format_args args)
+ -> std::string {
+ return detail::vformat(loc, fmt, args);
+}
+
+template <typename Locale, typename... T,
+ FMT_ENABLE_IF(detail::is_locale<Locale>::value)>
+inline auto format(const Locale& loc, format_string<T...> fmt, T&&... args)
+ -> std::string {
+ return fmt::vformat(loc, string_view(fmt), fmt::make_format_args(args...));
+}
+
+template <typename OutputIt, typename Locale,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value&&
+ detail::is_locale<Locale>::value)>
+auto vformat_to(OutputIt out, const Locale& loc, string_view fmt,
+ format_args args) -> OutputIt {
+ using detail::get_buffer;
+ auto&& buf = get_buffer<char>(out);
+ detail::vformat_to(buf, fmt, args, detail::locale_ref(loc));
+ return detail::get_iterator(buf, out);
+}
+
+template <typename OutputIt, typename Locale, typename... T,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, char>::value&&
+ detail::is_locale<Locale>::value)>
+FMT_INLINE auto format_to(OutputIt out, const Locale& loc,
+ format_string<T...> fmt, T&&... args) -> OutputIt {
+ return vformat_to(out, loc, fmt, fmt::make_format_args(args...));
+}
+
+template <typename Locale, typename... T,
+ FMT_ENABLE_IF(detail::is_locale<Locale>::value)>
+FMT_NODISCARD FMT_INLINE auto formatted_size(const Locale& loc,
+ format_string<T...> fmt,
+ T&&... args) -> size_t {
+ auto buf = detail::counting_buffer<>();
+ detail::vformat_to<char>(buf, fmt, fmt::make_format_args(args...),
+ detail::locale_ref(loc));
+ return buf.count();
+}
+
+FMT_END_EXPORT
+
+template <typename T, typename Char>
+template <typename FormatContext>
+FMT_CONSTEXPR FMT_INLINE auto
+formatter<T, Char,
+ enable_if_t<detail::type_constant<T, Char>::value !=
+ detail::type::custom_type>>::format(const T& val,
+ FormatContext& ctx)
+ const -> decltype(ctx.out()) {
+ if (specs_.width_ref.kind != detail::arg_id_kind::none ||
+ specs_.precision_ref.kind != detail::arg_id_kind::none) {
+ auto specs = specs_;
+ detail::handle_dynamic_spec<detail::width_checker>(specs.width,
+ specs.width_ref, ctx);
+ detail::handle_dynamic_spec<detail::precision_checker>(
+ specs.precision, specs.precision_ref, ctx);
+ return detail::write<Char>(ctx.out(), val, specs, ctx.locale());
+ }
+ return detail::write<Char>(ctx.out(), val, specs_, ctx.locale());
+}
+
+FMT_END_NAMESPACE
+
+#ifdef FMT_HEADER_ONLY
+# define FMT_FUNC inline
+# include "format-inl.h"
+#else
+# define FMT_FUNC
+#endif
+
+#endif // FMT_FORMAT_H_
diff --git a/src/fmtlib/fmt/locale.h b/src/fmtlib/fmt/locale.h
new file mode 100644
index 0000000..7571b52
--- /dev/null
+++ b/src/fmtlib/fmt/locale.h
@@ -0,0 +1,2 @@
+#include "xchar.h"
+#warning fmt/locale.h is deprecated, include fmt/format.h or fmt/xchar.h instead
diff --git a/src/fmtlib/fmt/os.h b/src/fmtlib/fmt/os.h
new file mode 100644
index 0000000..ec29040
--- /dev/null
+++ b/src/fmtlib/fmt/os.h
@@ -0,0 +1,451 @@
+// Formatting library for C++ - optional OS-specific functionality
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_OS_H_
+#define FMT_OS_H_
+
+#include <cerrno>
+#include <cstddef>
+#include <cstdio>
+#include <system_error> // std::system_error
+
+#if defined __APPLE__ || defined(__FreeBSD__)
+# include <xlocale.h> // for LC_NUMERIC_MASK on OS X
+#endif
+
+#include "format.h"
+
+#ifndef FMT_USE_FCNTL
+// UWP doesn't provide _pipe.
+# if FMT_HAS_INCLUDE("winapifamily.h")
+# include <winapifamily.h>
+# endif
+# if (FMT_HAS_INCLUDE(<fcntl.h>) || defined(__APPLE__) || \
+ defined(__linux__)) && \
+ (!defined(WINAPI_FAMILY) || \
+ (WINAPI_FAMILY == WINAPI_FAMILY_DESKTOP_APP))
+# include <fcntl.h> // for O_RDONLY
+# define FMT_USE_FCNTL 1
+# else
+# define FMT_USE_FCNTL 0
+# endif
+#endif
+
+#ifndef FMT_POSIX
+# if defined(_WIN32) && !defined(__MINGW32__)
+// Fix warnings about deprecated symbols.
+# define FMT_POSIX(call) _##call
+# else
+# define FMT_POSIX(call) call
+# endif
+#endif
+
+// Calls to system functions are wrapped in FMT_SYSTEM for testability.
+#ifdef FMT_SYSTEM
+# define FMT_POSIX_CALL(call) FMT_SYSTEM(call)
+#else
+# define FMT_SYSTEM(call) ::call
+# ifdef _WIN32
+// Fix warnings about deprecated symbols.
+# define FMT_POSIX_CALL(call) ::_##call
+# else
+# define FMT_POSIX_CALL(call) ::call
+# endif
+#endif
+
+// Retries the expression while it evaluates to error_result and errno
+// equals to EINTR.
+#ifndef _WIN32
+# define FMT_RETRY_VAL(result, expression, error_result) \
+ do { \
+ (result) = (expression); \
+ } while ((result) == (error_result) && errno == EINTR)
+#else
+# define FMT_RETRY_VAL(result, expression, error_result) result = (expression)
+#endif
+
+#define FMT_RETRY(result, expression) FMT_RETRY_VAL(result, expression, -1)
+
+FMT_BEGIN_NAMESPACE
+FMT_BEGIN_EXPORT
+
+/**
+ \rst
+ A reference to a null-terminated string. It can be constructed from a C
+ string or ``std::string``.
+
+ You can use one of the following type aliases for common character types:
+
+ +---------------+-----------------------------+
+ | Type | Definition |
+ +===============+=============================+
+ | cstring_view | basic_cstring_view<char> |
+ +---------------+-----------------------------+
+ | wcstring_view | basic_cstring_view<wchar_t> |
+ +---------------+-----------------------------+
+
+ This class is most useful as a parameter type to allow passing
+ different types of strings to a function, for example::
+
+ template <typename... Args>
+ std::string format(cstring_view format_str, const Args & ... args);
+
+ format("{}", 42);
+ format(std::string("{}"), 42);
+ \endrst
+ */
+template <typename Char> class basic_cstring_view {
+ private:
+ const Char* data_;
+
+ public:
+ /** Constructs a string reference object from a C string. */
+ basic_cstring_view(const Char* s) : data_(s) {}
+
+ /**
+ \rst
+ Constructs a string reference from an ``std::string`` object.
+ \endrst
+ */
+ basic_cstring_view(const std::basic_string<Char>& s) : data_(s.c_str()) {}
+
+ /** Returns the pointer to a C string. */
+ const Char* c_str() const { return data_; }
+};
+
+using cstring_view = basic_cstring_view<char>;
+using wcstring_view = basic_cstring_view<wchar_t>;
+
+#ifdef _WIN32
+FMT_API const std::error_category& system_category() noexcept;
+
+FMT_BEGIN_DETAIL_NAMESPACE
+FMT_API void format_windows_error(buffer<char>& out, int error_code,
+ const char* message) noexcept;
+FMT_END_DETAIL_NAMESPACE
+
+FMT_API std::system_error vwindows_error(int error_code, string_view format_str,
+ format_args args);
+
+/**
+ \rst
+ Constructs a :class:`std::system_error` object with the description
+ of the form
+
+ .. parsed-literal::
+ *<message>*: *<system-message>*
+
+ where *<message>* is the formatted message and *<system-message>* is the
+ system message corresponding to the error code.
+ *error_code* is a Windows error code as given by ``GetLastError``.
+ If *error_code* is not a valid error code such as -1, the system message
+ will look like "error -1".
+
+ **Example**::
+
+ // This throws a system_error with the description
+ // cannot open file 'madeup': The system cannot find the file specified.
+ // or similar (system message may vary).
+ const char *filename = "madeup";
+ LPOFSTRUCT of = LPOFSTRUCT();
+ HFILE file = OpenFile(filename, &of, OF_READ);
+ if (file == HFILE_ERROR) {
+ throw fmt::windows_error(GetLastError(),
+ "cannot open file '{}'", filename);
+ }
+ \endrst
+*/
+template <typename... Args>
+std::system_error windows_error(int error_code, string_view message,
+ const Args&... args) {
+ return vwindows_error(error_code, message, fmt::make_format_args(args...));
+}
+
+// Reports a Windows error without throwing an exception.
+// Can be used to report errors from destructors.
+FMT_API void report_windows_error(int error_code, const char* message) noexcept;
+#else
+inline const std::error_category& system_category() noexcept {
+ return std::system_category();
+}
+#endif // _WIN32
+
+// std::system is not available on some platforms such as iOS (#2248).
+#ifdef __OSX__
+template <typename S, typename... Args, typename Char = char_t<S>>
+void say(const S& format_str, Args&&... args) {
+ std::system(format("say \"{}\"", format(format_str, args...)).c_str());
+}
+#endif
+
+// A buffered file.
+class buffered_file {
+ private:
+ FILE* file_;
+
+ friend class file;
+
+ explicit buffered_file(FILE* f) : file_(f) {}
+
+ public:
+ buffered_file(const buffered_file&) = delete;
+ void operator=(const buffered_file&) = delete;
+
+ // Constructs a buffered_file object which doesn't represent any file.
+ buffered_file() noexcept : file_(nullptr) {}
+
+ // Destroys the object closing the file it represents if any.
+ FMT_API ~buffered_file() noexcept;
+
+ public:
+ buffered_file(buffered_file&& other) noexcept : file_(other.file_) {
+ other.file_ = nullptr;
+ }
+
+ buffered_file& operator=(buffered_file&& other) {
+ close();
+ file_ = other.file_;
+ other.file_ = nullptr;
+ return *this;
+ }
+
+ // Opens a file.
+ FMT_API buffered_file(cstring_view filename, cstring_view mode);
+
+ // Closes the file.
+ FMT_API void close();
+
+ // Returns the pointer to a FILE object representing this file.
+ FILE* get() const noexcept { return file_; }
+
+ FMT_API int descriptor() const;
+
+ void vprint(string_view format_str, format_args args) {
+ fmt::vprint(file_, format_str, args);
+ }
+
+ template <typename... Args>
+ inline void print(string_view format_str, const Args&... args) {
+ vprint(format_str, fmt::make_format_args(args...));
+ }
+};
+
+#if FMT_USE_FCNTL
+// A file. Closed file is represented by a file object with descriptor -1.
+// Methods that are not declared with noexcept may throw
+// fmt::system_error in case of failure. Note that some errors such as
+// closing the file multiple times will cause a crash on Windows rather
+// than an exception. You can get standard behavior by overriding the
+// invalid parameter handler with _set_invalid_parameter_handler.
+class FMT_API file {
+ private:
+ int fd_; // File descriptor.
+
+ // Constructs a file object with a given descriptor.
+ explicit file(int fd) : fd_(fd) {}
+
+ public:
+ // Possible values for the oflag argument to the constructor.
+ enum {
+ RDONLY = FMT_POSIX(O_RDONLY), // Open for reading only.
+ WRONLY = FMT_POSIX(O_WRONLY), // Open for writing only.
+ RDWR = FMT_POSIX(O_RDWR), // Open for reading and writing.
+ CREATE = FMT_POSIX(O_CREAT), // Create if the file doesn't exist.
+ APPEND = FMT_POSIX(O_APPEND), // Open in append mode.
+ TRUNC = FMT_POSIX(O_TRUNC) // Truncate the content of the file.
+ };
+
+ // Constructs a file object which doesn't represent any file.
+ file() noexcept : fd_(-1) {}
+
+ // Opens a file and constructs a file object representing this file.
+ file(cstring_view path, int oflag);
+
+ public:
+ file(const file&) = delete;
+ void operator=(const file&) = delete;
+
+ file(file&& other) noexcept : fd_(other.fd_) { other.fd_ = -1; }
+
+ // Move assignment is not noexcept because close may throw.
+ file& operator=(file&& other) {
+ close();
+ fd_ = other.fd_;
+ other.fd_ = -1;
+ return *this;
+ }
+
+ // Destroys the object closing the file it represents if any.
+ ~file() noexcept;
+
+ // Returns the file descriptor.
+ int descriptor() const noexcept { return fd_; }
+
+ // Closes the file.
+ void close();
+
+ // Returns the file size. The size has signed type for consistency with
+ // stat::st_size.
+ long long size() const;
+
+ // Attempts to read count bytes from the file into the specified buffer.
+ size_t read(void* buffer, size_t count);
+
+ // Attempts to write count bytes from the specified buffer to the file.
+ size_t write(const void* buffer, size_t count);
+
+ // Duplicates a file descriptor with the dup function and returns
+ // the duplicate as a file object.
+ static file dup(int fd);
+
+ // Makes fd be the copy of this file descriptor, closing fd first if
+ // necessary.
+ void dup2(int fd);
+
+ // Makes fd be the copy of this file descriptor, closing fd first if
+ // necessary.
+ void dup2(int fd, std::error_code& ec) noexcept;
+
+ // Creates a pipe setting up read_end and write_end file objects for reading
+ // and writing respectively.
+ static void pipe(file& read_end, file& write_end);
+
+ // Creates a buffered_file object associated with this file and detaches
+ // this file object from the file.
+ buffered_file fdopen(const char* mode);
+
+# if defined(_WIN32) && !defined(__MINGW32__)
+ // Opens a file and constructs a file object representing this file by
+ // wcstring_view filename. Windows only.
+ static file open_windows_file(wcstring_view path, int oflag);
+# endif
+};
+
+// Returns the memory page size.
+long getpagesize();
+
+FMT_BEGIN_DETAIL_NAMESPACE
+
+struct buffer_size {
+ buffer_size() = default;
+ size_t value = 0;
+ buffer_size operator=(size_t val) const {
+ auto bs = buffer_size();
+ bs.value = val;
+ return bs;
+ }
+};
+
+struct ostream_params {
+ int oflag = file::WRONLY | file::CREATE | file::TRUNC;
+ size_t buffer_size = BUFSIZ > 32768 ? BUFSIZ : 32768;
+
+ ostream_params() {}
+
+ template <typename... T>
+ ostream_params(T... params, int new_oflag) : ostream_params(params...) {
+ oflag = new_oflag;
+ }
+
+ template <typename... T>
+ ostream_params(T... params, detail::buffer_size bs)
+ : ostream_params(params...) {
+ this->buffer_size = bs.value;
+ }
+
+// Intel has a bug that results in failure to deduce a constructor
+// for empty parameter packs.
+# if defined(__INTEL_COMPILER) && __INTEL_COMPILER < 2000
+ ostream_params(int new_oflag) : oflag(new_oflag) {}
+ ostream_params(detail::buffer_size bs) : buffer_size(bs.value) {}
+# endif
+};
+
+class file_buffer final : public buffer<char> {
+ file file_;
+
+ FMT_API void grow(size_t) override;
+
+ public:
+ FMT_API file_buffer(cstring_view path, const ostream_params& params);
+ FMT_API file_buffer(file_buffer&& other);
+ FMT_API ~file_buffer();
+
+ void flush() {
+ if (size() == 0) return;
+ file_.write(data(), size() * sizeof(data()[0]));
+ clear();
+ }
+
+ void close() {
+ flush();
+ file_.close();
+ }
+};
+
+FMT_END_DETAIL_NAMESPACE
+
+// Added {} below to work around default constructor error known to
+// occur in Xcode versions 7.2.1 and 8.2.1.
+constexpr detail::buffer_size buffer_size{};
+
+/** A fast output stream which is not thread-safe. */
+class FMT_API ostream {
+ private:
+ FMT_MSC_WARNING(suppress : 4251)
+ detail::file_buffer buffer_;
+
+ ostream(cstring_view path, const detail::ostream_params& params)
+ : buffer_(path, params) {}
+
+ public:
+ ostream(ostream&& other) : buffer_(std::move(other.buffer_)) {}
+
+ ~ostream();
+
+ void flush() { buffer_.flush(); }
+
+ template <typename... T>
+ friend ostream output_file(cstring_view path, T... params);
+
+ void close() { buffer_.close(); }
+
+ /**
+ Formats ``args`` according to specifications in ``fmt`` and writes the
+ output to the file.
+ */
+ template <typename... T> void print(format_string<T...> fmt, T&&... args) {
+ vformat_to(detail::buffer_appender<char>(buffer_), fmt,
+ fmt::make_format_args(args...));
+ }
+};
+
+/**
+ \rst
+ Opens a file for writing. Supported parameters passed in *params*:
+
+ * ``<integer>``: Flags passed to `open
+ <https://pubs.opengroup.org/onlinepubs/007904875/functions/open.html>`_
+ (``file::WRONLY | file::CREATE | file::TRUNC`` by default)
+ * ``buffer_size=<integer>``: Output buffer size
+
+ **Example**::
+
+ auto out = fmt::output_file("guide.txt");
+ out.print("Don't {}", "Panic");
+ \endrst
+ */
+template <typename... T>
+inline ostream output_file(cstring_view path, T... params) {
+ return {path, detail::ostream_params(params...)};
+}
+#endif // FMT_USE_FCNTL
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_OS_H_
diff --git a/src/fmtlib/fmt/ostream.h b/src/fmtlib/fmt/ostream.h
new file mode 100644
index 0000000..ce65909
--- /dev/null
+++ b/src/fmtlib/fmt/ostream.h
@@ -0,0 +1,209 @@
+// Formatting library for C++ - std::ostream support
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_OSTREAM_H_
+#define FMT_OSTREAM_H_
+
+#include <fstream> // std::filebuf
+
+#if defined(_WIN32) && defined(__GLIBCXX__)
+# include <ext/stdio_filebuf.h>
+# include <ext/stdio_sync_filebuf.h>
+#elif defined(_WIN32) && defined(_LIBCPP_VERSION)
+# include <__std_stream>
+#endif
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+
+namespace detail {
+
+// Generate a unique explicit instantion in every translation unit using a tag
+// type in an anonymous namespace.
+namespace {
+struct file_access_tag {};
+} // namespace
+template <typename Tag, typename BufType, FILE* BufType::*FileMemberPtr>
+class file_access {
+ friend auto get_file(BufType& obj) -> FILE* { return obj.*FileMemberPtr; }
+};
+
+#if FMT_MSC_VERSION
+template class file_access<file_access_tag, std::filebuf,
+ &std::filebuf::_Myfile>;
+auto get_file(std::filebuf&) -> FILE*;
+#elif defined(_WIN32) && defined(_LIBCPP_VERSION)
+template class file_access<file_access_tag, std::__stdoutbuf<char>,
+ &std::__stdoutbuf<char>::__file_>;
+auto get_file(std::__stdoutbuf<char>&) -> FILE*;
+#endif
+
+inline bool write_ostream_unicode(std::ostream& os, fmt::string_view data) {
+#if FMT_MSC_VERSION
+ if (auto* buf = dynamic_cast<std::filebuf*>(os.rdbuf()))
+ if (FILE* f = get_file(*buf)) return write_console(f, data);
+#elif defined(_WIN32) && defined(__GLIBCXX__)
+ auto* rdbuf = os.rdbuf();
+ FILE* c_file;
+ if (auto* sfbuf = dynamic_cast<__gnu_cxx::stdio_sync_filebuf<char>*>(rdbuf))
+ c_file = sfbuf->file();
+ else if (auto* fbuf = dynamic_cast<__gnu_cxx::stdio_filebuf<char>*>(rdbuf))
+ c_file = fbuf->file();
+ else
+ return false;
+ if (c_file) return write_console(c_file, data);
+#elif defined(_WIN32) && defined(_LIBCPP_VERSION)
+ if (auto* buf = dynamic_cast<std::__stdoutbuf<char>*>(os.rdbuf()))
+ if (FILE* f = get_file(*buf)) return write_console(f, data);
+#else
+ ignore_unused(os, data);
+#endif
+ return false;
+}
+inline bool write_ostream_unicode(std::wostream&,
+ fmt::basic_string_view<wchar_t>) {
+ return false;
+}
+
+// Write the content of buf to os.
+// It is a separate function rather than a part of vprint to simplify testing.
+template <typename Char>
+void write_buffer(std::basic_ostream<Char>& os, buffer<Char>& buf) {
+ const Char* buf_data = buf.data();
+ using unsigned_streamsize = std::make_unsigned<std::streamsize>::type;
+ unsigned_streamsize size = buf.size();
+ unsigned_streamsize max_size = to_unsigned(max_value<std::streamsize>());
+ do {
+ unsigned_streamsize n = size <= max_size ? size : max_size;
+ os.write(buf_data, static_cast<std::streamsize>(n));
+ buf_data += n;
+ size -= n;
+ } while (size != 0);
+}
+
+template <typename Char, typename T>
+void format_value(buffer<Char>& buf, const T& value,
+ locale_ref loc = locale_ref()) {
+ auto&& format_buf = formatbuf<std::basic_streambuf<Char>>(buf);
+ auto&& output = std::basic_ostream<Char>(&format_buf);
+#if !defined(FMT_STATIC_THOUSANDS_SEPARATOR)
+ if (loc) output.imbue(loc.get<std::locale>());
+#endif
+ output << value;
+ output.exceptions(std::ios_base::failbit | std::ios_base::badbit);
+}
+
+template <typename T> struct streamed_view { const T& value; };
+
+} // namespace detail
+
+// Formats an object of type T that has an overloaded ostream operator<<.
+template <typename Char>
+struct basic_ostream_formatter : formatter<basic_string_view<Char>, Char> {
+ void set_debug_format() = delete;
+
+ template <typename T, typename OutputIt>
+ auto format(const T& value, basic_format_context<OutputIt, Char>& ctx) const
+ -> OutputIt {
+ auto buffer = basic_memory_buffer<Char>();
+ detail::format_value(buffer, value, ctx.locale());
+ return formatter<basic_string_view<Char>, Char>::format(
+ {buffer.data(), buffer.size()}, ctx);
+ }
+};
+
+using ostream_formatter = basic_ostream_formatter<char>;
+
+template <typename T, typename Char>
+struct formatter<detail::streamed_view<T>, Char>
+ : basic_ostream_formatter<Char> {
+ template <typename OutputIt>
+ auto format(detail::streamed_view<T> view,
+ basic_format_context<OutputIt, Char>& ctx) const -> OutputIt {
+ return basic_ostream_formatter<Char>::format(view.value, ctx);
+ }
+};
+
+/**
+ \rst
+ Returns a view that formats `value` via an ostream ``operator<<``.
+
+ **Example**::
+
+ fmt::print("Current thread id: {}\n",
+ fmt::streamed(std::this_thread::get_id()));
+ \endrst
+ */
+template <typename T>
+auto streamed(const T& value) -> detail::streamed_view<T> {
+ return {value};
+}
+
+namespace detail {
+
+inline void vprint_directly(std::ostream& os, string_view format_str,
+ format_args args) {
+ auto buffer = memory_buffer();
+ detail::vformat_to(buffer, format_str, args);
+ detail::write_buffer(os, buffer);
+}
+
+} // namespace detail
+
+FMT_MODULE_EXPORT template <typename Char>
+void vprint(std::basic_ostream<Char>& os,
+ basic_string_view<type_identity_t<Char>> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args) {
+ auto buffer = basic_memory_buffer<Char>();
+ detail::vformat_to(buffer, format_str, args);
+ if (detail::write_ostream_unicode(os, {buffer.data(), buffer.size()})) return;
+ detail::write_buffer(os, buffer);
+}
+
+/**
+ \rst
+ Prints formatted data to the stream *os*.
+
+ **Example**::
+
+ fmt::print(cerr, "Don't {}!", "panic");
+ \endrst
+ */
+FMT_MODULE_EXPORT template <typename... T>
+void print(std::ostream& os, format_string<T...> fmt, T&&... args) {
+ const auto& vargs = fmt::make_format_args(args...);
+ if (detail::is_utf8())
+ vprint(os, fmt, vargs);
+ else
+ detail::vprint_directly(os, fmt, vargs);
+}
+
+FMT_MODULE_EXPORT
+template <typename... Args>
+void print(std::wostream& os,
+ basic_format_string<wchar_t, type_identity_t<Args>...> fmt,
+ Args&&... args) {
+ vprint(os, fmt, fmt::make_format_args<buffer_context<wchar_t>>(args...));
+}
+
+FMT_MODULE_EXPORT template <typename... T>
+void println(std::ostream& os, format_string<T...> fmt, T&&... args) {
+ fmt::print(os, "{}\n", fmt::format(fmt, std::forward<T>(args)...));
+}
+
+FMT_MODULE_EXPORT
+template <typename... Args>
+void println(std::wostream& os,
+ basic_format_string<wchar_t, type_identity_t<Args>...> fmt,
+ Args&&... args) {
+ print(os, L"{}\n", fmt::format(fmt, std::forward<Args>(args)...));
+}
+
+FMT_END_NAMESPACE
+
+#endif // FMT_OSTREAM_H_
diff --git a/src/fmtlib/fmt/printf.h b/src/fmtlib/fmt/printf.h
new file mode 100644
index 0000000..554715e
--- /dev/null
+++ b/src/fmtlib/fmt/printf.h
@@ -0,0 +1,679 @@
+// Formatting library for C++ - legacy printf implementation
+//
+// Copyright (c) 2012 - 2016, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_PRINTF_H_
+#define FMT_PRINTF_H_
+
+#include <algorithm> // std::max
+#include <limits> // std::numeric_limits
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+FMT_BEGIN_EXPORT
+
+template <typename T> struct printf_formatter { printf_formatter() = delete; };
+
+template <typename Char>
+class basic_printf_parse_context : public basic_format_parse_context<Char> {
+ using basic_format_parse_context<Char>::basic_format_parse_context;
+};
+
+template <typename OutputIt, typename Char> class basic_printf_context {
+ private:
+ OutputIt out_;
+ basic_format_args<basic_printf_context> args_;
+
+ public:
+ using char_type = Char;
+ using format_arg = basic_format_arg<basic_printf_context>;
+ using parse_context_type = basic_printf_parse_context<Char>;
+ template <typename T> using formatter_type = printf_formatter<T>;
+
+ /**
+ \rst
+ Constructs a ``printf_context`` object. References to the arguments are
+ stored in the context object so make sure they have appropriate lifetimes.
+ \endrst
+ */
+ basic_printf_context(OutputIt out,
+ basic_format_args<basic_printf_context> args)
+ : out_(out), args_(args) {}
+
+ OutputIt out() { return out_; }
+ void advance_to(OutputIt it) { out_ = it; }
+
+ detail::locale_ref locale() { return {}; }
+
+ format_arg arg(int id) const { return args_.get(id); }
+
+ FMT_CONSTEXPR void on_error(const char* message) {
+ detail::error_handler().on_error(message);
+ }
+};
+
+FMT_BEGIN_DETAIL_NAMESPACE
+
+// Checks if a value fits in int - used to avoid warnings about comparing
+// signed and unsigned integers.
+template <bool IsSigned> struct int_checker {
+ template <typename T> static bool fits_in_int(T value) {
+ unsigned max = max_value<int>();
+ return value <= max;
+ }
+ static bool fits_in_int(bool) { return true; }
+};
+
+template <> struct int_checker<true> {
+ template <typename T> static bool fits_in_int(T value) {
+ return value >= (std::numeric_limits<int>::min)() &&
+ value <= max_value<int>();
+ }
+ static bool fits_in_int(int) { return true; }
+};
+
+class printf_precision_handler {
+ public:
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ int operator()(T value) {
+ if (!int_checker<std::numeric_limits<T>::is_signed>::fits_in_int(value))
+ throw_format_error("number is too big");
+ return (std::max)(static_cast<int>(value), 0);
+ }
+
+ template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+ int operator()(T) {
+ throw_format_error("precision is not integer");
+ return 0;
+ }
+};
+
+// An argument visitor that returns true iff arg is a zero integer.
+class is_zero_int {
+ public:
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ bool operator()(T value) {
+ return value == 0;
+ }
+
+ template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+ bool operator()(T) {
+ return false;
+ }
+};
+
+template <typename T> struct make_unsigned_or_bool : std::make_unsigned<T> {};
+
+template <> struct make_unsigned_or_bool<bool> { using type = bool; };
+
+template <typename T, typename Context> class arg_converter {
+ private:
+ using char_type = typename Context::char_type;
+
+ basic_format_arg<Context>& arg_;
+ char_type type_;
+
+ public:
+ arg_converter(basic_format_arg<Context>& arg, char_type type)
+ : arg_(arg), type_(type) {}
+
+ void operator()(bool value) {
+ if (type_ != 's') operator()<bool>(value);
+ }
+
+ template <typename U, FMT_ENABLE_IF(std::is_integral<U>::value)>
+ void operator()(U value) {
+ bool is_signed = type_ == 'd' || type_ == 'i';
+ using target_type = conditional_t<std::is_same<T, void>::value, U, T>;
+ if (const_check(sizeof(target_type) <= sizeof(int))) {
+ // Extra casts are used to silence warnings.
+ if (is_signed) {
+ arg_ = detail::make_arg<Context>(
+ static_cast<int>(static_cast<target_type>(value)));
+ } else {
+ using unsigned_type = typename make_unsigned_or_bool<target_type>::type;
+ arg_ = detail::make_arg<Context>(
+ static_cast<unsigned>(static_cast<unsigned_type>(value)));
+ }
+ } else {
+ if (is_signed) {
+ // glibc's printf doesn't sign extend arguments of smaller types:
+ // std::printf("%lld", -42); // prints "4294967254"
+ // but we don't have to do the same because it's a UB.
+ arg_ = detail::make_arg<Context>(static_cast<long long>(value));
+ } else {
+ arg_ = detail::make_arg<Context>(
+ static_cast<typename make_unsigned_or_bool<U>::type>(value));
+ }
+ }
+ }
+
+ template <typename U, FMT_ENABLE_IF(!std::is_integral<U>::value)>
+ void operator()(U) {} // No conversion needed for non-integral types.
+};
+
+// Converts an integer argument to T for printf, if T is an integral type.
+// If T is void, the argument is converted to corresponding signed or unsigned
+// type depending on the type specifier: 'd' and 'i' - signed, other -
+// unsigned).
+template <typename T, typename Context, typename Char>
+void convert_arg(basic_format_arg<Context>& arg, Char type) {
+ visit_format_arg(arg_converter<T, Context>(arg, type), arg);
+}
+
+// Converts an integer argument to char for printf.
+template <typename Context> class char_converter {
+ private:
+ basic_format_arg<Context>& arg_;
+
+ public:
+ explicit char_converter(basic_format_arg<Context>& arg) : arg_(arg) {}
+
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ void operator()(T value) {
+ arg_ = detail::make_arg<Context>(
+ static_cast<typename Context::char_type>(value));
+ }
+
+ template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+ void operator()(T) {} // No conversion needed for non-integral types.
+};
+
+// An argument visitor that return a pointer to a C string if argument is a
+// string or null otherwise.
+template <typename Char> struct get_cstring {
+ template <typename T> const Char* operator()(T) { return nullptr; }
+ const Char* operator()(const Char* s) { return s; }
+};
+
+// Checks if an argument is a valid printf width specifier and sets
+// left alignment if it is negative.
+template <typename Char> class printf_width_handler {
+ private:
+ format_specs<Char>& specs_;
+
+ public:
+ explicit printf_width_handler(format_specs<Char>& specs) : specs_(specs) {}
+
+ template <typename T, FMT_ENABLE_IF(std::is_integral<T>::value)>
+ unsigned operator()(T value) {
+ auto width = static_cast<uint32_or_64_or_128_t<T>>(value);
+ if (detail::is_negative(value)) {
+ specs_.align = align::left;
+ width = 0 - width;
+ }
+ unsigned int_max = max_value<int>();
+ if (width > int_max) throw_format_error("number is too big");
+ return static_cast<unsigned>(width);
+ }
+
+ template <typename T, FMT_ENABLE_IF(!std::is_integral<T>::value)>
+ unsigned operator()(T) {
+ throw_format_error("width is not integer");
+ return 0;
+ }
+};
+
+// Workaround for a bug with the XL compiler when initializing
+// printf_arg_formatter's base class.
+template <typename Char>
+auto make_arg_formatter(buffer_appender<Char> iter, format_specs<Char>& s)
+ -> arg_formatter<Char> {
+ return {iter, s, locale_ref()};
+}
+
+// The ``printf`` argument formatter.
+template <typename OutputIt, typename Char>
+class printf_arg_formatter : public arg_formatter<Char> {
+ private:
+ using base = arg_formatter<Char>;
+ using context_type = basic_printf_context<OutputIt, Char>;
+
+ context_type& context_;
+
+ OutputIt write_null_pointer(bool is_string = false) {
+ auto s = this->specs;
+ s.type = presentation_type::none;
+ return write_bytes(this->out, is_string ? "(null)" : "(nil)", s);
+ }
+
+ public:
+ printf_arg_formatter(OutputIt iter, format_specs<Char>& s, context_type& ctx)
+ : base(make_arg_formatter(iter, s)), context_(ctx) {}
+
+ OutputIt operator()(monostate value) { return base::operator()(value); }
+
+ template <typename T, FMT_ENABLE_IF(detail::is_integral<T>::value)>
+ OutputIt operator()(T value) {
+ // MSVC2013 fails to compile separate overloads for bool and Char so use
+ // std::is_same instead.
+ if (std::is_same<T, Char>::value) {
+ format_specs<Char> fmt_specs = this->specs;
+ if (fmt_specs.type != presentation_type::none &&
+ fmt_specs.type != presentation_type::chr) {
+ return (*this)(static_cast<int>(value));
+ }
+ fmt_specs.sign = sign::none;
+ fmt_specs.alt = false;
+ fmt_specs.fill[0] = ' '; // Ignore '0' flag for char types.
+ // align::numeric needs to be overwritten here since the '0' flag is
+ // ignored for non-numeric types
+ if (fmt_specs.align == align::none || fmt_specs.align == align::numeric)
+ fmt_specs.align = align::right;
+ return write<Char>(this->out, static_cast<Char>(value), fmt_specs);
+ }
+ return base::operator()(value);
+ }
+
+ template <typename T, FMT_ENABLE_IF(std::is_floating_point<T>::value)>
+ OutputIt operator()(T value) {
+ return base::operator()(value);
+ }
+
+ /** Formats a null-terminated C string. */
+ OutputIt operator()(const char* value) {
+ if (value) return base::operator()(value);
+ return write_null_pointer(this->specs.type != presentation_type::pointer);
+ }
+
+ /** Formats a null-terminated wide C string. */
+ OutputIt operator()(const wchar_t* value) {
+ if (value) return base::operator()(value);
+ return write_null_pointer(this->specs.type != presentation_type::pointer);
+ }
+
+ OutputIt operator()(basic_string_view<Char> value) {
+ return base::operator()(value);
+ }
+
+ /** Formats a pointer. */
+ OutputIt operator()(const void* value) {
+ return value ? base::operator()(value) : write_null_pointer();
+ }
+
+ /** Formats an argument of a custom (user-defined) type. */
+ OutputIt operator()(typename basic_format_arg<context_type>::handle handle) {
+ auto parse_ctx =
+ basic_printf_parse_context<Char>(basic_string_view<Char>());
+ handle.format(parse_ctx, context_);
+ return this->out;
+ }
+};
+
+template <typename Char>
+void parse_flags(format_specs<Char>& specs, const Char*& it, const Char* end) {
+ for (; it != end; ++it) {
+ switch (*it) {
+ case '-':
+ specs.align = align::left;
+ break;
+ case '+':
+ specs.sign = sign::plus;
+ break;
+ case '0':
+ specs.fill[0] = '0';
+ break;
+ case ' ':
+ if (specs.sign != sign::plus) {
+ specs.sign = sign::space;
+ }
+ break;
+ case '#':
+ specs.alt = true;
+ break;
+ default:
+ return;
+ }
+ }
+}
+
+template <typename Char, typename GetArg>
+int parse_header(const Char*& it, const Char* end, format_specs<Char>& specs,
+ GetArg get_arg) {
+ int arg_index = -1;
+ Char c = *it;
+ if (c >= '0' && c <= '9') {
+ // Parse an argument index (if followed by '$') or a width possibly
+ // preceded with '0' flag(s).
+ int value = parse_nonnegative_int(it, end, -1);
+ if (it != end && *it == '$') { // value is an argument index
+ ++it;
+ arg_index = value != -1 ? value : max_value<int>();
+ } else {
+ if (c == '0') specs.fill[0] = '0';
+ if (value != 0) {
+ // Nonzero value means that we parsed width and don't need to
+ // parse it or flags again, so return now.
+ if (value == -1) throw_format_error("number is too big");
+ specs.width = value;
+ return arg_index;
+ }
+ }
+ }
+ parse_flags(specs, it, end);
+ // Parse width.
+ if (it != end) {
+ if (*it >= '0' && *it <= '9') {
+ specs.width = parse_nonnegative_int(it, end, -1);
+ if (specs.width == -1) throw_format_error("number is too big");
+ } else if (*it == '*') {
+ ++it;
+ specs.width = static_cast<int>(visit_format_arg(
+ detail::printf_width_handler<Char>(specs), get_arg(-1)));
+ }
+ }
+ return arg_index;
+}
+
+inline auto parse_printf_presentation_type(char c, type t)
+ -> presentation_type {
+ using pt = presentation_type;
+ constexpr auto integral_set = sint_set | uint_set | bool_set | char_set;
+ switch (c) {
+ case 'd':
+ return in(t, integral_set) ? pt::dec : pt::none;
+ case 'o':
+ return in(t, integral_set) ? pt::oct : pt::none;
+ case 'x':
+ return in(t, integral_set) ? pt::hex_lower : pt::none;
+ case 'X':
+ return in(t, integral_set) ? pt::hex_upper : pt::none;
+ case 'a':
+ return in(t, float_set) ? pt::hexfloat_lower : pt::none;
+ case 'A':
+ return in(t, float_set) ? pt::hexfloat_upper : pt::none;
+ case 'e':
+ return in(t, float_set) ? pt::exp_lower : pt::none;
+ case 'E':
+ return in(t, float_set) ? pt::exp_upper : pt::none;
+ case 'f':
+ return in(t, float_set) ? pt::fixed_lower : pt::none;
+ case 'F':
+ return in(t, float_set) ? pt::fixed_upper : pt::none;
+ case 'g':
+ return in(t, float_set) ? pt::general_lower : pt::none;
+ case 'G':
+ return in(t, float_set) ? pt::general_upper : pt::none;
+ case 'c':
+ return in(t, integral_set) ? pt::chr : pt::none;
+ case 's':
+ return in(t, string_set | cstring_set) ? pt::string : pt::none;
+ case 'p':
+ return in(t, pointer_set | cstring_set) ? pt::pointer : pt::none;
+ default:
+ return pt::none;
+ }
+}
+
+template <typename Char, typename Context>
+void vprintf(buffer<Char>& buf, basic_string_view<Char> format,
+ basic_format_args<Context> args) {
+ using iterator = buffer_appender<Char>;
+ auto out = iterator(buf);
+ auto context = basic_printf_context<iterator, Char>(out, args);
+ auto parse_ctx = basic_printf_parse_context<Char>(format);
+
+ // Returns the argument with specified index or, if arg_index is -1, the next
+ // argument.
+ auto get_arg = [&](int arg_index) {
+ if (arg_index < 0)
+ arg_index = parse_ctx.next_arg_id();
+ else
+ parse_ctx.check_arg_id(--arg_index);
+ return detail::get_arg(context, arg_index);
+ };
+
+ const Char* start = parse_ctx.begin();
+ const Char* end = parse_ctx.end();
+ auto it = start;
+ while (it != end) {
+ if (!find<false, Char>(it, end, '%', it)) {
+ it = end; // find leaves it == nullptr if it doesn't find '%'.
+ break;
+ }
+ Char c = *it++;
+ if (it != end && *it == c) {
+ out = write(out, basic_string_view<Char>(start, to_unsigned(it - start)));
+ start = ++it;
+ continue;
+ }
+ out =
+ write(out, basic_string_view<Char>(start, to_unsigned(it - 1 - start)));
+
+ auto specs = format_specs<Char>();
+ specs.align = align::right;
+
+ // Parse argument index, flags and width.
+ int arg_index = parse_header(it, end, specs, get_arg);
+ if (arg_index == 0) throw_format_error("argument not found");
+
+ // Parse precision.
+ if (it != end && *it == '.') {
+ ++it;
+ c = it != end ? *it : 0;
+ if ('0' <= c && c <= '9') {
+ specs.precision = parse_nonnegative_int(it, end, 0);
+ } else if (c == '*') {
+ ++it;
+ specs.precision = static_cast<int>(
+ visit_format_arg(printf_precision_handler(), get_arg(-1)));
+ } else {
+ specs.precision = 0;
+ }
+ }
+
+ auto arg = get_arg(arg_index);
+ // For d, i, o, u, x, and X conversion specifiers, if a precision is
+ // specified, the '0' flag is ignored
+ if (specs.precision >= 0 && arg.is_integral())
+ specs.fill[0] =
+ ' '; // Ignore '0' flag for non-numeric types or if '-' present.
+ if (specs.precision >= 0 && arg.type() == type::cstring_type) {
+ auto str = visit_format_arg(get_cstring<Char>(), arg);
+ auto str_end = str + specs.precision;
+ auto nul = std::find(str, str_end, Char());
+ arg = make_arg<basic_printf_context<iterator, Char>>(
+ basic_string_view<Char>(
+ str, to_unsigned(nul != str_end ? nul - str : specs.precision)));
+ }
+ if (specs.alt && visit_format_arg(is_zero_int(), arg)) specs.alt = false;
+ if (specs.fill[0] == '0') {
+ if (arg.is_arithmetic() && specs.align != align::left)
+ specs.align = align::numeric;
+ else
+ specs.fill[0] = ' '; // Ignore '0' flag for non-numeric types or if '-'
+ // flag is also present.
+ }
+
+ // Parse length and convert the argument to the required type.
+ c = it != end ? *it++ : 0;
+ Char t = it != end ? *it : 0;
+ switch (c) {
+ case 'h':
+ if (t == 'h') {
+ ++it;
+ t = it != end ? *it : 0;
+ convert_arg<signed char>(arg, t);
+ } else {
+ convert_arg<short>(arg, t);
+ }
+ break;
+ case 'l':
+ if (t == 'l') {
+ ++it;
+ t = it != end ? *it : 0;
+ convert_arg<long long>(arg, t);
+ } else {
+ convert_arg<long>(arg, t);
+ }
+ break;
+ case 'j':
+ convert_arg<intmax_t>(arg, t);
+ break;
+ case 'z':
+ convert_arg<size_t>(arg, t);
+ break;
+ case 't':
+ convert_arg<std::ptrdiff_t>(arg, t);
+ break;
+ case 'L':
+ // printf produces garbage when 'L' is omitted for long double, no
+ // need to do the same.
+ break;
+ default:
+ --it;
+ convert_arg<void>(arg, c);
+ }
+
+ // Parse type.
+ if (it == end) throw_format_error("invalid format string");
+ char type = static_cast<char>(*it++);
+ if (arg.is_integral()) {
+ // Normalize type.
+ switch (type) {
+ case 'i':
+ case 'u':
+ type = 'd';
+ break;
+ case 'c':
+ visit_format_arg(
+ char_converter<basic_printf_context<iterator, Char>>(arg), arg);
+ break;
+ }
+ }
+ specs.type = parse_printf_presentation_type(type, arg.type());
+ if (specs.type == presentation_type::none)
+ throw_format_error("invalid format specifier");
+
+ start = it;
+
+ // Format argument.
+ out = visit_format_arg(
+ printf_arg_formatter<iterator, Char>(out, specs, context), arg);
+ }
+ write(out, basic_string_view<Char>(start, to_unsigned(it - start)));
+}
+FMT_END_DETAIL_NAMESPACE
+
+template <typename Char>
+using basic_printf_context_t =
+ basic_printf_context<detail::buffer_appender<Char>, Char>;
+
+using printf_context = basic_printf_context_t<char>;
+using wprintf_context = basic_printf_context_t<wchar_t>;
+
+using printf_args = basic_format_args<printf_context>;
+using wprintf_args = basic_format_args<wprintf_context>;
+
+/**
+ \rst
+ Constructs an `~fmt::format_arg_store` object that contains references to
+ arguments and can be implicitly converted to `~fmt::printf_args`.
+ \endrst
+ */
+template <typename... T>
+inline auto make_printf_args(const T&... args)
+ -> format_arg_store<printf_context, T...> {
+ return {args...};
+}
+
+/**
+ \rst
+ Constructs an `~fmt::format_arg_store` object that contains references to
+ arguments and can be implicitly converted to `~fmt::wprintf_args`.
+ \endrst
+ */
+template <typename... T>
+inline auto make_wprintf_args(const T&... args)
+ -> format_arg_store<wprintf_context, T...> {
+ return {args...};
+}
+
+template <typename S, typename Char = char_t<S>>
+inline auto vsprintf(
+ const S& fmt,
+ basic_format_args<basic_printf_context_t<type_identity_t<Char>>> args)
+ -> std::basic_string<Char> {
+ auto buf = basic_memory_buffer<Char>();
+ detail::vprintf(buf, detail::to_string_view(fmt), args);
+ return to_string(buf);
+}
+
+/**
+ \rst
+ Formats arguments and returns the result as a string.
+
+ **Example**::
+
+ std::string message = fmt::sprintf("The answer is %d", 42);
+ \endrst
+*/
+template <typename S, typename... T,
+ typename Char = enable_if_t<detail::is_string<S>::value, char_t<S>>>
+inline auto sprintf(const S& fmt, const T&... args) -> std::basic_string<Char> {
+ using context = basic_printf_context_t<Char>;
+ return vsprintf(detail::to_string_view(fmt),
+ fmt::make_format_args<context>(args...));
+}
+
+template <typename S, typename Char = char_t<S>>
+inline auto vfprintf(
+ std::FILE* f, const S& fmt,
+ basic_format_args<basic_printf_context_t<type_identity_t<Char>>> args)
+ -> int {
+ auto buf = basic_memory_buffer<Char>();
+ detail::vprintf(buf, detail::to_string_view(fmt), args);
+ size_t size = buf.size();
+ return std::fwrite(buf.data(), sizeof(Char), size, f) < size
+ ? -1
+ : static_cast<int>(size);
+}
+
+/**
+ \rst
+ Prints formatted data to the file *f*.
+
+ **Example**::
+
+ fmt::fprintf(stderr, "Don't %s!", "panic");
+ \endrst
+ */
+template <typename S, typename... T, typename Char = char_t<S>>
+inline auto fprintf(std::FILE* f, const S& fmt, const T&... args) -> int {
+ using context = basic_printf_context_t<Char>;
+ return vfprintf(f, detail::to_string_view(fmt),
+ fmt::make_format_args<context>(args...));
+}
+
+template <typename S, typename Char = char_t<S>>
+inline auto vprintf(
+ const S& fmt,
+ basic_format_args<basic_printf_context_t<type_identity_t<Char>>> args)
+ -> int {
+ return vfprintf(stdout, detail::to_string_view(fmt), args);
+}
+
+/**
+ \rst
+ Prints formatted data to ``stdout``.
+
+ **Example**::
+
+ fmt::printf("Elapsed time: %.2f seconds", 1.23);
+ \endrst
+ */
+template <typename S, typename... T, FMT_ENABLE_IF(detail::is_string<S>::value)>
+inline auto printf(const S& fmt, const T&... args) -> int {
+ return vprintf(
+ detail::to_string_view(fmt),
+ fmt::make_format_args<basic_printf_context_t<char_t<S>>>(args...));
+}
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_PRINTF_H_
diff --git a/src/fmtlib/fmt/ranges.h b/src/fmtlib/fmt/ranges.h
new file mode 100644
index 0000000..266b9e1
--- /dev/null
+++ b/src/fmtlib/fmt/ranges.h
@@ -0,0 +1,732 @@
+// Formatting library for C++ - experimental range support
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+//
+// Copyright (c) 2018 - present, Remotion (Igor Schulz)
+// All Rights Reserved
+// {fmt} support for ranges, containers and types tuple interface.
+
+#ifndef FMT_RANGES_H_
+#define FMT_RANGES_H_
+
+#include <initializer_list>
+#include <tuple>
+#include <type_traits>
+
+#include "format.h"
+
+FMT_BEGIN_NAMESPACE
+
+namespace detail {
+
+template <typename Range, typename OutputIt>
+auto copy(const Range& range, OutputIt out) -> OutputIt {
+ for (auto it = range.begin(), end = range.end(); it != end; ++it)
+ *out++ = *it;
+ return out;
+}
+
+template <typename OutputIt>
+auto copy(const char* str, OutputIt out) -> OutputIt {
+ while (*str) *out++ = *str++;
+ return out;
+}
+
+template <typename OutputIt> auto copy(char ch, OutputIt out) -> OutputIt {
+ *out++ = ch;
+ return out;
+}
+
+template <typename OutputIt> auto copy(wchar_t ch, OutputIt out) -> OutputIt {
+ *out++ = ch;
+ return out;
+}
+
+// Returns true if T has a std::string-like interface, like std::string_view.
+template <typename T> class is_std_string_like {
+ template <typename U>
+ static auto check(U* p)
+ -> decltype((void)p->find('a'), p->length(), (void)p->data(), int());
+ template <typename> static void check(...);
+
+ public:
+ static constexpr const bool value =
+ is_string<T>::value ||
+ std::is_convertible<T, std_string_view<char>>::value ||
+ !std::is_void<decltype(check<T>(nullptr))>::value;
+};
+
+template <typename Char>
+struct is_std_string_like<fmt::basic_string_view<Char>> : std::true_type {};
+
+template <typename T> class is_map {
+ template <typename U> static auto check(U*) -> typename U::mapped_type;
+ template <typename> static void check(...);
+
+ public:
+#ifdef FMT_FORMAT_MAP_AS_LIST // DEPRECATED!
+ static constexpr const bool value = false;
+#else
+ static constexpr const bool value =
+ !std::is_void<decltype(check<T>(nullptr))>::value;
+#endif
+};
+
+template <typename T> class is_set {
+ template <typename U> static auto check(U*) -> typename U::key_type;
+ template <typename> static void check(...);
+
+ public:
+#ifdef FMT_FORMAT_SET_AS_LIST // DEPRECATED!
+ static constexpr const bool value = false;
+#else
+ static constexpr const bool value =
+ !std::is_void<decltype(check<T>(nullptr))>::value && !is_map<T>::value;
+#endif
+};
+
+template <typename... Ts> struct conditional_helper {};
+
+template <typename T, typename _ = void> struct is_range_ : std::false_type {};
+
+#if !FMT_MSC_VERSION || FMT_MSC_VERSION > 1800
+
+# define FMT_DECLTYPE_RETURN(val) \
+ ->decltype(val) { return val; } \
+ static_assert( \
+ true, "") // This makes it so that a semicolon is required after the
+ // macro, which helps clang-format handle the formatting.
+
+// C array overload
+template <typename T, std::size_t N>
+auto range_begin(const T (&arr)[N]) -> const T* {
+ return arr;
+}
+template <typename T, std::size_t N>
+auto range_end(const T (&arr)[N]) -> const T* {
+ return arr + N;
+}
+
+template <typename T, typename Enable = void>
+struct has_member_fn_begin_end_t : std::false_type {};
+
+template <typename T>
+struct has_member_fn_begin_end_t<T, void_t<decltype(std::declval<T>().begin()),
+ decltype(std::declval<T>().end())>>
+ : std::true_type {};
+
+// Member function overload
+template <typename T>
+auto range_begin(T&& rng) FMT_DECLTYPE_RETURN(static_cast<T&&>(rng).begin());
+template <typename T>
+auto range_end(T&& rng) FMT_DECLTYPE_RETURN(static_cast<T&&>(rng).end());
+
+// ADL overload. Only participates in overload resolution if member functions
+// are not found.
+template <typename T>
+auto range_begin(T&& rng)
+ -> enable_if_t<!has_member_fn_begin_end_t<T&&>::value,
+ decltype(begin(static_cast<T&&>(rng)))> {
+ return begin(static_cast<T&&>(rng));
+}
+template <typename T>
+auto range_end(T&& rng) -> enable_if_t<!has_member_fn_begin_end_t<T&&>::value,
+ decltype(end(static_cast<T&&>(rng)))> {
+ return end(static_cast<T&&>(rng));
+}
+
+template <typename T, typename Enable = void>
+struct has_const_begin_end : std::false_type {};
+template <typename T, typename Enable = void>
+struct has_mutable_begin_end : std::false_type {};
+
+template <typename T>
+struct has_const_begin_end<
+ T,
+ void_t<
+ decltype(detail::range_begin(std::declval<const remove_cvref_t<T>&>())),
+ decltype(detail::range_end(std::declval<const remove_cvref_t<T>&>()))>>
+ : std::true_type {};
+
+template <typename T>
+struct has_mutable_begin_end<
+ T, void_t<decltype(detail::range_begin(std::declval<T>())),
+ decltype(detail::range_end(std::declval<T>())),
+ // the extra int here is because older versions of MSVC don't
+ // SFINAE properly unless there are distinct types
+ int>> : std::true_type {};
+
+template <typename T>
+struct is_range_<T, void>
+ : std::integral_constant<bool, (has_const_begin_end<T>::value ||
+ has_mutable_begin_end<T>::value)> {};
+# undef FMT_DECLTYPE_RETURN
+#endif
+
+// tuple_size and tuple_element check.
+template <typename T> class is_tuple_like_ {
+ template <typename U>
+ static auto check(U* p) -> decltype(std::tuple_size<U>::value, int());
+ template <typename> static void check(...);
+
+ public:
+ static constexpr const bool value =
+ !std::is_void<decltype(check<T>(nullptr))>::value;
+};
+
+// Check for integer_sequence
+#if defined(__cpp_lib_integer_sequence) || FMT_MSC_VERSION >= 1900
+template <typename T, T... N>
+using integer_sequence = std::integer_sequence<T, N...>;
+template <size_t... N> using index_sequence = std::index_sequence<N...>;
+template <size_t N> using make_index_sequence = std::make_index_sequence<N>;
+#else
+template <typename T, T... N> struct integer_sequence {
+ using value_type = T;
+
+ static FMT_CONSTEXPR size_t size() { return sizeof...(N); }
+};
+
+template <size_t... N> using index_sequence = integer_sequence<size_t, N...>;
+
+template <typename T, size_t N, T... Ns>
+struct make_integer_sequence : make_integer_sequence<T, N - 1, N - 1, Ns...> {};
+template <typename T, T... Ns>
+struct make_integer_sequence<T, 0, Ns...> : integer_sequence<T, Ns...> {};
+
+template <size_t N>
+using make_index_sequence = make_integer_sequence<size_t, N>;
+#endif
+
+template <typename T>
+using tuple_index_sequence = make_index_sequence<std::tuple_size<T>::value>;
+
+template <typename T, typename C, bool = is_tuple_like_<T>::value>
+class is_tuple_formattable_ {
+ public:
+ static constexpr const bool value = false;
+};
+template <typename T, typename C> class is_tuple_formattable_<T, C, true> {
+ template <std::size_t... Is>
+ static std::true_type check2(index_sequence<Is...>,
+ integer_sequence<bool, (Is == Is)...>);
+ static std::false_type check2(...);
+ template <std::size_t... Is>
+ static decltype(check2(
+ index_sequence<Is...>{},
+ integer_sequence<
+ bool, (is_formattable<typename std::tuple_element<Is, T>::type,
+ C>::value)...>{})) check(index_sequence<Is...>);
+
+ public:
+ static constexpr const bool value =
+ decltype(check(tuple_index_sequence<T>{}))::value;
+};
+
+template <typename Tuple, typename F, size_t... Is>
+FMT_CONSTEXPR void for_each(index_sequence<Is...>, Tuple&& t, F&& f) {
+ using std::get;
+ // Using a free function get<Is>(Tuple) now.
+ const int unused[] = {0, ((void)f(get<Is>(t)), 0)...};
+ ignore_unused(unused);
+}
+
+template <typename Tuple, typename F>
+FMT_CONSTEXPR void for_each(Tuple&& t, F&& f) {
+ for_each(tuple_index_sequence<remove_cvref_t<Tuple>>(),
+ std::forward<Tuple>(t), std::forward<F>(f));
+}
+
+template <typename Tuple1, typename Tuple2, typename F, size_t... Is>
+void for_each2(index_sequence<Is...>, Tuple1&& t1, Tuple2&& t2, F&& f) {
+ using std::get;
+ const int unused[] = {0, ((void)f(get<Is>(t1), get<Is>(t2)), 0)...};
+ ignore_unused(unused);
+}
+
+template <typename Tuple1, typename Tuple2, typename F>
+void for_each2(Tuple1&& t1, Tuple2&& t2, F&& f) {
+ for_each2(tuple_index_sequence<remove_cvref_t<Tuple1>>(),
+ std::forward<Tuple1>(t1), std::forward<Tuple2>(t2),
+ std::forward<F>(f));
+}
+
+namespace tuple {
+// Workaround a bug in MSVC 2019 (v140).
+template <typename Char, typename... T>
+using result_t = std::tuple<formatter<remove_cvref_t<T>, Char>...>;
+
+using std::get;
+template <typename Tuple, typename Char, std::size_t... Is>
+auto get_formatters(index_sequence<Is...>)
+ -> result_t<Char, decltype(get<Is>(std::declval<Tuple>()))...>;
+} // namespace tuple
+
+#if FMT_MSC_VERSION && FMT_MSC_VERSION < 1920
+// Older MSVC doesn't get the reference type correctly for arrays.
+template <typename R> struct range_reference_type_impl {
+ using type = decltype(*detail::range_begin(std::declval<R&>()));
+};
+
+template <typename T, std::size_t N> struct range_reference_type_impl<T[N]> {
+ using type = T&;
+};
+
+template <typename T>
+using range_reference_type = typename range_reference_type_impl<T>::type;
+#else
+template <typename Range>
+using range_reference_type =
+ decltype(*detail::range_begin(std::declval<Range&>()));
+#endif
+
+// We don't use the Range's value_type for anything, but we do need the Range's
+// reference type, with cv-ref stripped.
+template <typename Range>
+using uncvref_type = remove_cvref_t<range_reference_type<Range>>;
+
+template <typename Formatter>
+FMT_CONSTEXPR auto maybe_set_debug_format(Formatter& f, bool set)
+ -> decltype(f.set_debug_format(set)) {
+ f.set_debug_format(set);
+}
+template <typename Formatter>
+FMT_CONSTEXPR void maybe_set_debug_format(Formatter&, ...) {}
+
+// These are not generic lambdas for compatibility with C++11.
+template <typename ParseContext> struct parse_empty_specs {
+ template <typename Formatter> FMT_CONSTEXPR void operator()(Formatter& f) {
+ f.parse(ctx);
+ detail::maybe_set_debug_format(f, true);
+ }
+ ParseContext& ctx;
+};
+template <typename FormatContext> struct format_tuple_element {
+ using char_type = typename FormatContext::char_type;
+
+ template <typename T>
+ void operator()(const formatter<T, char_type>& f, const T& v) {
+ if (i > 0)
+ ctx.advance_to(detail::copy_str<char_type>(separator, ctx.out()));
+ ctx.advance_to(f.format(v, ctx));
+ ++i;
+ }
+
+ int i;
+ FormatContext& ctx;
+ basic_string_view<char_type> separator;
+};
+
+} // namespace detail
+
+template <typename T> struct is_tuple_like {
+ static constexpr const bool value =
+ detail::is_tuple_like_<T>::value && !detail::is_range_<T>::value;
+};
+
+template <typename T, typename C> struct is_tuple_formattable {
+ static constexpr const bool value =
+ detail::is_tuple_formattable_<T, C>::value;
+};
+
+template <typename Tuple, typename Char>
+struct formatter<Tuple, Char,
+ enable_if_t<fmt::is_tuple_like<Tuple>::value &&
+ fmt::is_tuple_formattable<Tuple, Char>::value>> {
+ private:
+ decltype(detail::tuple::get_formatters<Tuple, Char>(
+ detail::tuple_index_sequence<Tuple>())) formatters_;
+
+ basic_string_view<Char> separator_ = detail::string_literal<Char, ',', ' '>{};
+ basic_string_view<Char> opening_bracket_ =
+ detail::string_literal<Char, '('>{};
+ basic_string_view<Char> closing_bracket_ =
+ detail::string_literal<Char, ')'>{};
+
+ public:
+ FMT_CONSTEXPR formatter() {}
+
+ FMT_CONSTEXPR void set_separator(basic_string_view<Char> sep) {
+ separator_ = sep;
+ }
+
+ FMT_CONSTEXPR void set_brackets(basic_string_view<Char> open,
+ basic_string_view<Char> close) {
+ opening_bracket_ = open;
+ closing_bracket_ = close;
+ }
+
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ auto it = ctx.begin();
+ if (it != ctx.end() && *it != '}')
+ FMT_THROW(format_error("invalid format specifier"));
+ detail::for_each(formatters_, detail::parse_empty_specs<ParseContext>{ctx});
+ return it;
+ }
+
+ template <typename FormatContext>
+ auto format(const Tuple& value, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ ctx.advance_to(detail::copy_str<Char>(opening_bracket_, ctx.out()));
+ detail::for_each2(
+ formatters_, value,
+ detail::format_tuple_element<FormatContext>{0, ctx, separator_});
+ return detail::copy_str<Char>(closing_bracket_, ctx.out());
+ }
+};
+
+template <typename T, typename Char> struct is_range {
+ static constexpr const bool value =
+ detail::is_range_<T>::value && !detail::is_std_string_like<T>::value &&
+ !std::is_convertible<T, std::basic_string<Char>>::value &&
+ !std::is_convertible<T, detail::std_string_view<Char>>::value;
+};
+
+namespace detail {
+template <typename Context> struct range_mapper {
+ using mapper = arg_mapper<Context>;
+
+ template <typename T,
+ FMT_ENABLE_IF(has_formatter<remove_cvref_t<T>, Context>::value)>
+ static auto map(T&& value) -> T&& {
+ return static_cast<T&&>(value);
+ }
+ template <typename T,
+ FMT_ENABLE_IF(!has_formatter<remove_cvref_t<T>, Context>::value)>
+ static auto map(T&& value)
+ -> decltype(mapper().map(static_cast<T&&>(value))) {
+ return mapper().map(static_cast<T&&>(value));
+ }
+};
+
+template <typename Char, typename Element>
+using range_formatter_type =
+ formatter<remove_cvref_t<decltype(range_mapper<buffer_context<Char>>{}.map(
+ std::declval<Element>()))>,
+ Char>;
+
+template <typename R>
+using maybe_const_range =
+ conditional_t<has_const_begin_end<R>::value, const R, R>;
+
+// Workaround a bug in MSVC 2015 and earlier.
+#if !FMT_MSC_VERSION || FMT_MSC_VERSION >= 1910
+template <typename R, typename Char>
+struct is_formattable_delayed
+ : is_formattable<uncvref_type<maybe_const_range<R>>, Char> {};
+#endif
+} // namespace detail
+
+template <typename T, typename Char, typename Enable = void>
+struct range_formatter;
+
+template <typename T, typename Char>
+struct range_formatter<
+ T, Char,
+ enable_if_t<conjunction<std::is_same<T, remove_cvref_t<T>>,
+ is_formattable<T, Char>>::value>> {
+ private:
+ detail::range_formatter_type<Char, T> underlying_;
+ basic_string_view<Char> separator_ = detail::string_literal<Char, ',', ' '>{};
+ basic_string_view<Char> opening_bracket_ =
+ detail::string_literal<Char, '['>{};
+ basic_string_view<Char> closing_bracket_ =
+ detail::string_literal<Char, ']'>{};
+
+ public:
+ FMT_CONSTEXPR range_formatter() {}
+
+ FMT_CONSTEXPR auto underlying() -> detail::range_formatter_type<Char, T>& {
+ return underlying_;
+ }
+
+ FMT_CONSTEXPR void set_separator(basic_string_view<Char> sep) {
+ separator_ = sep;
+ }
+
+ FMT_CONSTEXPR void set_brackets(basic_string_view<Char> open,
+ basic_string_view<Char> close) {
+ opening_bracket_ = open;
+ closing_bracket_ = close;
+ }
+
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ auto it = ctx.begin();
+ auto end = ctx.end();
+
+ if (it != end && *it == 'n') {
+ set_brackets({}, {});
+ ++it;
+ }
+
+ if (it != end && *it != '}') {
+ if (*it != ':') FMT_THROW(format_error("invalid format specifier"));
+ ++it;
+ } else {
+ detail::maybe_set_debug_format(underlying_, true);
+ }
+
+ ctx.advance_to(it);
+ return underlying_.parse(ctx);
+ }
+
+ template <typename R, typename FormatContext>
+ auto format(R&& range, FormatContext& ctx) const -> decltype(ctx.out()) {
+ detail::range_mapper<buffer_context<Char>> mapper;
+ auto out = ctx.out();
+ out = detail::copy_str<Char>(opening_bracket_, out);
+ int i = 0;
+ auto it = detail::range_begin(range);
+ auto end = detail::range_end(range);
+ for (; it != end; ++it) {
+ if (i > 0) out = detail::copy_str<Char>(separator_, out);
+ ctx.advance_to(out);
+ out = underlying_.format(mapper.map(*it), ctx);
+ ++i;
+ }
+ out = detail::copy_str<Char>(closing_bracket_, out);
+ return out;
+ }
+};
+
+enum class range_format { disabled, map, set, sequence, string, debug_string };
+
+namespace detail {
+template <typename T>
+struct range_format_kind_
+ : std::integral_constant<range_format,
+ std::is_same<uncvref_type<T>, T>::value
+ ? range_format::disabled
+ : is_map<T>::value ? range_format::map
+ : is_set<T>::value ? range_format::set
+ : range_format::sequence> {};
+
+template <range_format K, typename R, typename Char, typename Enable = void>
+struct range_default_formatter;
+
+template <range_format K>
+using range_format_constant = std::integral_constant<range_format, K>;
+
+template <range_format K, typename R, typename Char>
+struct range_default_formatter<
+ K, R, Char,
+ enable_if_t<(K == range_format::sequence || K == range_format::map ||
+ K == range_format::set)>> {
+ using range_type = detail::maybe_const_range<R>;
+ range_formatter<detail::uncvref_type<range_type>, Char> underlying_;
+
+ FMT_CONSTEXPR range_default_formatter() { init(range_format_constant<K>()); }
+
+ FMT_CONSTEXPR void init(range_format_constant<range_format::set>) {
+ underlying_.set_brackets(detail::string_literal<Char, '{'>{},
+ detail::string_literal<Char, '}'>{});
+ }
+
+ FMT_CONSTEXPR void init(range_format_constant<range_format::map>) {
+ underlying_.set_brackets(detail::string_literal<Char, '{'>{},
+ detail::string_literal<Char, '}'>{});
+ underlying_.underlying().set_brackets({}, {});
+ underlying_.underlying().set_separator(
+ detail::string_literal<Char, ':', ' '>{});
+ }
+
+ FMT_CONSTEXPR void init(range_format_constant<range_format::sequence>) {}
+
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return underlying_.parse(ctx);
+ }
+
+ template <typename FormatContext>
+ auto format(range_type& range, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ return underlying_.format(range, ctx);
+ }
+};
+} // namespace detail
+
+template <typename T, typename Char, typename Enable = void>
+struct range_format_kind
+ : conditional_t<
+ is_range<T, Char>::value, detail::range_format_kind_<T>,
+ std::integral_constant<range_format, range_format::disabled>> {};
+
+template <typename R, typename Char>
+struct formatter<
+ R, Char,
+ enable_if_t<conjunction<bool_constant<range_format_kind<R, Char>::value !=
+ range_format::disabled>
+// Workaround a bug in MSVC 2015 and earlier.
+#if !FMT_MSC_VERSION || FMT_MSC_VERSION >= 1910
+ ,
+ detail::is_formattable_delayed<R, Char>
+#endif
+ >::value>>
+ : detail::range_default_formatter<range_format_kind<R, Char>::value, R,
+ Char> {
+};
+
+template <typename Char, typename... T> struct tuple_join_view : detail::view {
+ const std::tuple<T...>& tuple;
+ basic_string_view<Char> sep;
+
+ tuple_join_view(const std::tuple<T...>& t, basic_string_view<Char> s)
+ : tuple(t), sep{s} {}
+};
+
+// Define FMT_TUPLE_JOIN_SPECIFIERS to enable experimental format specifiers
+// support in tuple_join. It is disabled by default because of issues with
+// the dynamic width and precision.
+#ifndef FMT_TUPLE_JOIN_SPECIFIERS
+# define FMT_TUPLE_JOIN_SPECIFIERS 0
+#endif
+
+template <typename Char, typename... T>
+struct formatter<tuple_join_view<Char, T...>, Char> {
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return do_parse(ctx, std::integral_constant<size_t, sizeof...(T)>());
+ }
+
+ template <typename FormatContext>
+ auto format(const tuple_join_view<Char, T...>& value,
+ FormatContext& ctx) const -> typename FormatContext::iterator {
+ return do_format(value, ctx,
+ std::integral_constant<size_t, sizeof...(T)>());
+ }
+
+ private:
+ std::tuple<formatter<typename std::decay<T>::type, Char>...> formatters_;
+
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto do_parse(ParseContext& ctx,
+ std::integral_constant<size_t, 0>)
+ -> decltype(ctx.begin()) {
+ return ctx.begin();
+ }
+
+ template <typename ParseContext, size_t N>
+ FMT_CONSTEXPR auto do_parse(ParseContext& ctx,
+ std::integral_constant<size_t, N>)
+ -> decltype(ctx.begin()) {
+ auto end = ctx.begin();
+#if FMT_TUPLE_JOIN_SPECIFIERS
+ end = std::get<sizeof...(T) - N>(formatters_).parse(ctx);
+ if (N > 1) {
+ auto end1 = do_parse(ctx, std::integral_constant<size_t, N - 1>());
+ if (end != end1)
+ FMT_THROW(format_error("incompatible format specs for tuple elements"));
+ }
+#endif
+ return end;
+ }
+
+ template <typename FormatContext>
+ auto do_format(const tuple_join_view<Char, T...>&, FormatContext& ctx,
+ std::integral_constant<size_t, 0>) const ->
+ typename FormatContext::iterator {
+ return ctx.out();
+ }
+
+ template <typename FormatContext, size_t N>
+ auto do_format(const tuple_join_view<Char, T...>& value, FormatContext& ctx,
+ std::integral_constant<size_t, N>) const ->
+ typename FormatContext::iterator {
+ auto out = std::get<sizeof...(T) - N>(formatters_)
+ .format(std::get<sizeof...(T) - N>(value.tuple), ctx);
+ if (N > 1) {
+ out = std::copy(value.sep.begin(), value.sep.end(), out);
+ ctx.advance_to(out);
+ return do_format(value, ctx, std::integral_constant<size_t, N - 1>());
+ }
+ return out;
+ }
+};
+
+namespace detail {
+// Check if T has an interface like a container adaptor (e.g. std::stack,
+// std::queue, std::priority_queue).
+template <typename T> class is_container_adaptor_like {
+ template <typename U> static auto check(U* p) -> typename U::container_type;
+ template <typename> static void check(...);
+
+ public:
+ static constexpr const bool value =
+ !std::is_void<decltype(check<T>(nullptr))>::value;
+};
+
+template <typename Container> struct all {
+ const Container& c;
+ auto begin() const -> typename Container::const_iterator { return c.begin(); }
+ auto end() const -> typename Container::const_iterator { return c.end(); }
+};
+} // namespace detail
+
+template <typename T, typename Char>
+struct formatter<T, Char,
+ enable_if_t<detail::is_container_adaptor_like<T>::value>>
+ : formatter<detail::all<typename T::container_type>, Char> {
+ using all = detail::all<typename T::container_type>;
+ template <typename FormatContext>
+ auto format(const T& t, FormatContext& ctx) const -> decltype(ctx.out()) {
+ struct getter : T {
+ static auto get(const T& t) -> all {
+ return {t.*(&getter::c)}; // Access c through the derived class.
+ }
+ };
+ return formatter<all>::format(getter::get(t), ctx);
+ }
+};
+
+FMT_BEGIN_EXPORT
+
+/**
+ \rst
+ Returns an object that formats `tuple` with elements separated by `sep`.
+
+ **Example**::
+
+ std::tuple<int, char> t = {1, 'a'};
+ fmt::print("{}", fmt::join(t, ", "));
+ // Output: "1, a"
+ \endrst
+ */
+template <typename... T>
+FMT_CONSTEXPR auto join(const std::tuple<T...>& tuple, string_view sep)
+ -> tuple_join_view<char, T...> {
+ return {tuple, sep};
+}
+
+template <typename... T>
+FMT_CONSTEXPR auto join(const std::tuple<T...>& tuple,
+ basic_string_view<wchar_t> sep)
+ -> tuple_join_view<wchar_t, T...> {
+ return {tuple, sep};
+}
+
+/**
+ \rst
+ Returns an object that formats `initializer_list` with elements separated by
+ `sep`.
+
+ **Example**::
+
+ fmt::print("{}", fmt::join({1, 2, 3}, ", "));
+ // Output: "1, 2, 3"
+ \endrst
+ */
+template <typename T>
+auto join(std::initializer_list<T> list, string_view sep)
+ -> join_view<const T*, const T*> {
+ return join(std::begin(list), std::end(list), sep);
+}
+
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_RANGES_H_
diff --git a/src/fmtlib/fmt/std.h b/src/fmtlib/fmt/std.h
new file mode 100644
index 0000000..4c2a28c
--- /dev/null
+++ b/src/fmtlib/fmt/std.h
@@ -0,0 +1,349 @@
+// Formatting library for C++ - formatters for standard library types
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_STD_H_
+#define FMT_STD_H_
+
+#include <cstdlib>
+#include <exception>
+#include <memory>
+#include <thread>
+#include <type_traits>
+#include <typeinfo>
+#include <utility>
+
+#include "ostream.h"
+
+#if FMT_HAS_INCLUDE(<version>)
+# include <version>
+#endif
+// Checking FMT_CPLUSPLUS for warning suppression in MSVC.
+#if FMT_CPLUSPLUS >= 201703L
+# if FMT_HAS_INCLUDE(<filesystem>)
+# include <filesystem>
+# endif
+# if FMT_HAS_INCLUDE(<variant>)
+# include <variant>
+# endif
+# if FMT_HAS_INCLUDE(<optional>)
+# include <optional>
+# endif
+#endif
+
+// GCC 4 does not support FMT_HAS_INCLUDE.
+#if FMT_HAS_INCLUDE(<cxxabi.h>) || defined(__GLIBCXX__)
+# include <cxxabi.h>
+// Android NDK with gabi++ library on some architectures does not implement
+// abi::__cxa_demangle().
+# ifndef __GABIXX_CXXABI_H__
+# define FMT_HAS_ABI_CXA_DEMANGLE
+# endif
+#endif
+
+#ifdef __cpp_lib_filesystem
+FMT_BEGIN_NAMESPACE
+
+namespace detail {
+
+template <typename Char>
+void write_escaped_path(basic_memory_buffer<Char>& quoted,
+ const std::filesystem::path& p) {
+ write_escaped_string<Char>(std::back_inserter(quoted), p.string<Char>());
+}
+# ifdef _WIN32
+template <>
+inline void write_escaped_path<char>(memory_buffer& quoted,
+ const std::filesystem::path& p) {
+ auto buf = basic_memory_buffer<wchar_t>();
+ write_escaped_string<wchar_t>(std::back_inserter(buf), p.native());
+ // Convert UTF-16 to UTF-8.
+ if (!unicode_to_utf8<wchar_t>::convert(quoted, {buf.data(), buf.size()}))
+ FMT_THROW(std::runtime_error("invalid utf16"));
+}
+# endif
+template <>
+inline void write_escaped_path<std::filesystem::path::value_type>(
+ basic_memory_buffer<std::filesystem::path::value_type>& quoted,
+ const std::filesystem::path& p) {
+ write_escaped_string<std::filesystem::path::value_type>(
+ std::back_inserter(quoted), p.native());
+}
+
+} // namespace detail
+
+FMT_MODULE_EXPORT
+template <typename Char>
+struct formatter<std::filesystem::path, Char>
+ : formatter<basic_string_view<Char>> {
+ template <typename ParseContext> FMT_CONSTEXPR auto parse(ParseContext& ctx) {
+ auto out = formatter<basic_string_view<Char>>::parse(ctx);
+ this->set_debug_format(false);
+ return out;
+ }
+ template <typename FormatContext>
+ auto format(const std::filesystem::path& p, FormatContext& ctx) const ->
+ typename FormatContext::iterator {
+ auto quoted = basic_memory_buffer<Char>();
+ detail::write_escaped_path(quoted, p);
+ return formatter<basic_string_view<Char>>::format(
+ basic_string_view<Char>(quoted.data(), quoted.size()), ctx);
+ }
+};
+FMT_END_NAMESPACE
+#endif
+
+FMT_BEGIN_NAMESPACE
+FMT_MODULE_EXPORT
+template <typename Char>
+struct formatter<std::thread::id, Char> : basic_ostream_formatter<Char> {};
+FMT_END_NAMESPACE
+
+#ifdef __cpp_lib_optional
+FMT_BEGIN_NAMESPACE
+FMT_MODULE_EXPORT
+template <typename T, typename Char>
+struct formatter<std::optional<T>, Char,
+ std::enable_if_t<is_formattable<T, Char>::value>> {
+ private:
+ formatter<T, Char> underlying_;
+ static constexpr basic_string_view<Char> optional =
+ detail::string_literal<Char, 'o', 'p', 't', 'i', 'o', 'n', 'a', 'l',
+ '('>{};
+ static constexpr basic_string_view<Char> none =
+ detail::string_literal<Char, 'n', 'o', 'n', 'e'>{};
+
+ template <class U>
+ FMT_CONSTEXPR static auto maybe_set_debug_format(U& u, bool set)
+ -> decltype(u.set_debug_format(set)) {
+ u.set_debug_format(set);
+ }
+
+ template <class U>
+ FMT_CONSTEXPR static void maybe_set_debug_format(U&, ...) {}
+
+ public:
+ template <typename ParseContext> FMT_CONSTEXPR auto parse(ParseContext& ctx) {
+ maybe_set_debug_format(underlying_, true);
+ return underlying_.parse(ctx);
+ }
+
+ template <typename FormatContext>
+ auto format(std::optional<T> const& opt, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ if (!opt) return detail::write<Char>(ctx.out(), none);
+
+ auto out = ctx.out();
+ out = detail::write<Char>(out, optional);
+ ctx.advance_to(out);
+ out = underlying_.format(*opt, ctx);
+ return detail::write(out, ')');
+ }
+};
+FMT_END_NAMESPACE
+#endif // __cpp_lib_optional
+
+#ifdef __cpp_lib_variant
+FMT_BEGIN_NAMESPACE
+FMT_MODULE_EXPORT
+template <typename Char> struct formatter<std::monostate, Char> {
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return ctx.begin();
+ }
+
+ template <typename FormatContext>
+ auto format(const std::monostate&, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ auto out = ctx.out();
+ out = detail::write<Char>(out, "monostate");
+ return out;
+ }
+};
+
+namespace detail {
+
+template <typename T>
+using variant_index_sequence =
+ std::make_index_sequence<std::variant_size<T>::value>;
+
+template <typename> struct is_variant_like_ : std::false_type {};
+template <typename... Types>
+struct is_variant_like_<std::variant<Types...>> : std::true_type {};
+
+// formattable element check.
+template <typename T, typename C> class is_variant_formattable_ {
+ template <std::size_t... Is>
+ static std::conjunction<
+ is_formattable<std::variant_alternative_t<Is, T>, C>...>
+ check(std::index_sequence<Is...>);
+
+ public:
+ static constexpr const bool value =
+ decltype(check(variant_index_sequence<T>{}))::value;
+};
+
+template <typename Char, typename OutputIt, typename T>
+auto write_variant_alternative(OutputIt out, const T& v) -> OutputIt {
+ if constexpr (is_string<T>::value)
+ return write_escaped_string<Char>(out, detail::to_string_view(v));
+ else if constexpr (std::is_same_v<T, Char>)
+ return write_escaped_char(out, v);
+ else
+ return write<Char>(out, v);
+}
+
+} // namespace detail
+template <typename T> struct is_variant_like {
+ static constexpr const bool value = detail::is_variant_like_<T>::value;
+};
+
+template <typename T, typename C> struct is_variant_formattable {
+ static constexpr const bool value =
+ detail::is_variant_formattable_<T, C>::value;
+};
+
+FMT_MODULE_EXPORT
+template <typename Variant, typename Char>
+struct formatter<
+ Variant, Char,
+ std::enable_if_t<std::conjunction_v<
+ is_variant_like<Variant>, is_variant_formattable<Variant, Char>>>> {
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return ctx.begin();
+ }
+
+ template <typename FormatContext>
+ auto format(const Variant& value, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ auto out = ctx.out();
+
+ out = detail::write<Char>(out, "variant(");
+ try {
+ std::visit(
+ [&](const auto& v) {
+ out = detail::write_variant_alternative<Char>(out, v);
+ },
+ value);
+ } catch (const std::bad_variant_access&) {
+ detail::write<Char>(out, "valueless by exception");
+ }
+ *out++ = ')';
+ return out;
+ }
+};
+FMT_END_NAMESPACE
+#endif // __cpp_lib_variant
+
+FMT_BEGIN_NAMESPACE
+FMT_MODULE_EXPORT
+template <typename Char> struct formatter<std::error_code, Char> {
+ template <typename ParseContext>
+ FMT_CONSTEXPR auto parse(ParseContext& ctx) -> decltype(ctx.begin()) {
+ return ctx.begin();
+ }
+
+ template <typename FormatContext>
+ FMT_CONSTEXPR auto format(const std::error_code& ec, FormatContext& ctx) const
+ -> decltype(ctx.out()) {
+ auto out = ctx.out();
+ out = detail::write_bytes(out, ec.category().name(), format_specs<Char>());
+ out = detail::write<Char>(out, Char(':'));
+ out = detail::write<Char>(out, ec.value());
+ return out;
+ }
+};
+
+FMT_MODULE_EXPORT
+template <typename T, typename Char>
+struct formatter<
+ T, Char,
+ typename std::enable_if<std::is_base_of<std::exception, T>::value>::type> {
+ private:
+ bool with_typename_ = false;
+
+ public:
+ FMT_CONSTEXPR auto parse(basic_format_parse_context<Char>& ctx)
+ -> decltype(ctx.begin()) {
+ auto it = ctx.begin();
+ auto end = ctx.end();
+ if (it == end || *it == '}') return it;
+ if (*it == 't') {
+ ++it;
+ with_typename_ = true;
+ }
+ return it;
+ }
+
+ template <typename OutputIt>
+ auto format(const std::exception& ex,
+ basic_format_context<OutputIt, Char>& ctx) const -> OutputIt {
+ format_specs<Char> spec;
+ auto out = ctx.out();
+ if (!with_typename_)
+ return detail::write_bytes(out, string_view(ex.what()), spec);
+
+ const std::type_info& ti = typeid(ex);
+#ifdef FMT_HAS_ABI_CXA_DEMANGLE
+ int status = 0;
+ std::size_t size = 0;
+ std::unique_ptr<char, decltype(&std::free)> demangled_name_ptr(
+ abi::__cxa_demangle(ti.name(), nullptr, &size, &status), &std::free);
+
+ string_view demangled_name_view;
+ if (demangled_name_ptr) {
+ demangled_name_view = demangled_name_ptr.get();
+
+ // Normalization of stdlib inline namespace names.
+ // libc++ inline namespaces.
+ // std::__1::* -> std::*
+ // std::__1::__fs::* -> std::*
+ // libstdc++ inline namespaces.
+ // std::__cxx11::* -> std::*
+ // std::filesystem::__cxx11::* -> std::filesystem::*
+ if (demangled_name_view.starts_with("std::")) {
+ char* begin = demangled_name_ptr.get();
+ char* to = begin + 5; // std::
+ for (char *from = to, *end = begin + demangled_name_view.size();
+ from < end;) {
+ // This is safe, because demangled_name is NUL-terminated.
+ if (from[0] == '_' && from[1] == '_') {
+ char* next = from + 1;
+ while (next < end && *next != ':') next++;
+ if (next[0] == ':' && next[1] == ':') {
+ from = next + 2;
+ continue;
+ }
+ }
+ *to++ = *from++;
+ }
+ demangled_name_view = {begin, detail::to_unsigned(to - begin)};
+ }
+ } else {
+ demangled_name_view = string_view(ti.name());
+ }
+ out = detail::write_bytes(out, demangled_name_view, spec);
+#elif FMT_MSC_VERSION
+ string_view demangled_name_view(ti.name());
+ if (demangled_name_view.starts_with("class "))
+ demangled_name_view.remove_prefix(6);
+ else if (demangled_name_view.starts_with("struct "))
+ demangled_name_view.remove_prefix(7);
+ out = detail::write_bytes(out, demangled_name_view, spec);
+#else
+ out = detail::write_bytes(out, string_view(ti.name()), spec);
+#endif
+ out = detail::write<Char>(out, Char(':'));
+ out = detail::write<Char>(out, Char(' '));
+ out = detail::write_bytes(out, string_view(ex.what()), spec);
+
+ return out;
+ }
+};
+FMT_END_NAMESPACE
+
+#endif // FMT_STD_H_
diff --git a/src/fmtlib/fmt/xchar.h b/src/fmtlib/fmt/xchar.h
new file mode 100644
index 0000000..4b87f8d
--- /dev/null
+++ b/src/fmtlib/fmt/xchar.h
@@ -0,0 +1,259 @@
+// Formatting library for C++ - optional wchar_t and exotic character support
+//
+// Copyright (c) 2012 - present, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#ifndef FMT_XCHAR_H_
+#define FMT_XCHAR_H_
+
+#include <cwchar>
+
+#include "format.h"
+
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+# include <locale>
+#endif
+
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+template <typename T>
+using is_exotic_char = bool_constant<!std::is_same<T, char>::value>;
+
+inline auto write_loc(std::back_insert_iterator<detail::buffer<wchar_t>> out,
+ loc_value value, const format_specs<wchar_t>& specs,
+ locale_ref loc) -> bool {
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+ auto& numpunct =
+ std::use_facet<std::numpunct<wchar_t>>(loc.get<std::locale>());
+ auto separator = std::wstring();
+ auto grouping = numpunct.grouping();
+ if (!grouping.empty()) separator = std::wstring(1, numpunct.thousands_sep());
+ return value.visit(loc_writer<wchar_t>{out, specs, separator, grouping, {}});
+#endif
+ return false;
+}
+} // namespace detail
+
+FMT_BEGIN_EXPORT
+
+using wstring_view = basic_string_view<wchar_t>;
+using wformat_parse_context = basic_format_parse_context<wchar_t>;
+using wformat_context = buffer_context<wchar_t>;
+using wformat_args = basic_format_args<wformat_context>;
+using wmemory_buffer = basic_memory_buffer<wchar_t>;
+
+#if FMT_GCC_VERSION && FMT_GCC_VERSION < 409
+// Workaround broken conversion on older gcc.
+template <typename... Args> using wformat_string = wstring_view;
+inline auto runtime(wstring_view s) -> wstring_view { return s; }
+#else
+template <typename... Args>
+using wformat_string = basic_format_string<wchar_t, type_identity_t<Args>...>;
+inline auto runtime(wstring_view s) -> runtime_format_string<wchar_t> {
+ return {{s}};
+}
+#endif
+
+template <> struct is_char<wchar_t> : std::true_type {};
+template <> struct is_char<detail::char8_type> : std::true_type {};
+template <> struct is_char<char16_t> : std::true_type {};
+template <> struct is_char<char32_t> : std::true_type {};
+
+template <typename... Args>
+constexpr format_arg_store<wformat_context, Args...> make_wformat_args(
+ const Args&... args) {
+ return {args...};
+}
+
+inline namespace literals {
+#if FMT_USE_USER_DEFINED_LITERALS && !FMT_USE_NONTYPE_TEMPLATE_ARGS
+constexpr detail::udl_arg<wchar_t> operator"" _a(const wchar_t* s, size_t) {
+ return {s};
+}
+#endif
+} // namespace literals
+
+template <typename It, typename Sentinel>
+auto join(It begin, Sentinel end, wstring_view sep)
+ -> join_view<It, Sentinel, wchar_t> {
+ return {begin, end, sep};
+}
+
+template <typename Range>
+auto join(Range&& range, wstring_view sep)
+ -> join_view<detail::iterator_t<Range>, detail::sentinel_t<Range>,
+ wchar_t> {
+ return join(std::begin(range), std::end(range), sep);
+}
+
+template <typename T>
+auto join(std::initializer_list<T> list, wstring_view sep)
+ -> join_view<const T*, const T*, wchar_t> {
+ return join(std::begin(list), std::end(list), sep);
+}
+
+template <typename Char, FMT_ENABLE_IF(!std::is_same<Char, char>::value)>
+auto vformat(basic_string_view<Char> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> std::basic_string<Char> {
+ basic_memory_buffer<Char> buffer;
+ detail::vformat_to(buffer, format_str, args);
+ return to_string(buffer);
+}
+
+template <typename... T>
+auto format(wformat_string<T...> fmt, T&&... args) -> std::wstring {
+ return vformat(fmt::wstring_view(fmt), fmt::make_wformat_args(args...));
+}
+
+// Pass char_t as a default template parameter instead of using
+// std::basic_string<char_t<S>> to reduce the symbol size.
+template <typename S, typename... Args, typename Char = char_t<S>,
+ FMT_ENABLE_IF(!std::is_same<Char, char>::value &&
+ !std::is_same<Char, wchar_t>::value)>
+auto format(const S& format_str, Args&&... args) -> std::basic_string<Char> {
+ return vformat(detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename Locale, typename S, typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_locale<Locale>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto vformat(
+ const Locale& loc, const S& format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> std::basic_string<Char> {
+ return detail::vformat(loc, detail::to_string_view(format_str), args);
+}
+
+template <typename Locale, typename S, typename... Args,
+ typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_locale<Locale>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto format(const Locale& loc, const S& format_str, Args&&... args)
+ -> std::basic_string<Char> {
+ return detail::vformat(loc, detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename OutputIt, typename S, typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_exotic_char<Char>::value)>
+auto vformat_to(OutputIt out, const S& format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> OutputIt {
+ auto&& buf = detail::get_buffer<Char>(out);
+ detail::vformat_to(buf, detail::to_string_view(format_str), args);
+ return detail::get_iterator(buf, out);
+}
+
+template <typename OutputIt, typename S, typename... Args,
+ typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto format_to(OutputIt out, const S& fmt, Args&&... args) -> OutputIt {
+ return vformat_to(out, detail::to_string_view(fmt),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename Locale, typename S, typename OutputIt, typename... Args,
+ typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_locale<Locale>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto vformat_to(
+ OutputIt out, const Locale& loc, const S& format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args) -> OutputIt {
+ auto&& buf = detail::get_buffer<Char>(out);
+ vformat_to(buf, detail::to_string_view(format_str), args,
+ detail::locale_ref(loc));
+ return detail::get_iterator(buf, out);
+}
+
+template <
+ typename OutputIt, typename Locale, typename S, typename... Args,
+ typename Char = char_t<S>,
+ bool enable = detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_locale<Locale>::value&& detail::is_exotic_char<Char>::value>
+inline auto format_to(OutputIt out, const Locale& loc, const S& format_str,
+ Args&&... args) ->
+ typename std::enable_if<enable, OutputIt>::type {
+ return vformat_to(out, loc, detail::to_string_view(format_str),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename OutputIt, typename Char, typename... Args,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto vformat_to_n(
+ OutputIt out, size_t n, basic_string_view<Char> format_str,
+ basic_format_args<buffer_context<type_identity_t<Char>>> args)
+ -> format_to_n_result<OutputIt> {
+ detail::iterator_buffer<OutputIt, Char, detail::fixed_buffer_traits> buf(out,
+ n);
+ detail::vformat_to(buf, format_str, args);
+ return {buf.out(), buf.count()};
+}
+
+template <typename OutputIt, typename S, typename... Args,
+ typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_output_iterator<OutputIt, Char>::value&&
+ detail::is_exotic_char<Char>::value)>
+inline auto format_to_n(OutputIt out, size_t n, const S& fmt,
+ const Args&... args) -> format_to_n_result<OutputIt> {
+ return vformat_to_n(out, n, detail::to_string_view(fmt),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+}
+
+template <typename S, typename... Args, typename Char = char_t<S>,
+ FMT_ENABLE_IF(detail::is_exotic_char<Char>::value)>
+inline auto formatted_size(const S& fmt, Args&&... args) -> size_t {
+ detail::counting_buffer<Char> buf;
+ detail::vformat_to(buf, detail::to_string_view(fmt),
+ fmt::make_format_args<buffer_context<Char>>(args...));
+ return buf.count();
+}
+
+inline void vprint(std::FILE* f, wstring_view fmt, wformat_args args) {
+ wmemory_buffer buffer;
+ detail::vformat_to(buffer, fmt, args);
+ buffer.push_back(L'\0');
+ if (std::fputws(buffer.data(), f) == -1)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot write to file")));
+}
+
+inline void vprint(wstring_view fmt, wformat_args args) {
+ vprint(stdout, fmt, args);
+}
+
+template <typename... T>
+void print(std::FILE* f, wformat_string<T...> fmt, T&&... args) {
+ return vprint(f, wstring_view(fmt), fmt::make_wformat_args(args...));
+}
+
+template <typename... T> void print(wformat_string<T...> fmt, T&&... args) {
+ return vprint(wstring_view(fmt), fmt::make_wformat_args(args...));
+}
+
+template <typename... T>
+void println(std::FILE* f, wformat_string<T...> fmt, T&&... args) {
+ return print(f, L"{}\n", fmt::format(fmt, std::forward<T>(args)...));
+}
+
+template <typename... T> void println(wformat_string<T...> fmt, T&&... args) {
+ return print(L"{}\n", fmt::format(fmt, std::forward<T>(args)...));
+}
+
+/**
+ Converts *value* to ``std::wstring`` using the default format for type *T*.
+ */
+template <typename T> inline auto to_wstring(const T& value) -> std::wstring {
+ return format(FMT_STRING(L"{}"), value);
+}
+FMT_END_EXPORT
+FMT_END_NAMESPACE
+
+#endif // FMT_XCHAR_H_
diff --git a/src/fmtlib/format.cc b/src/fmtlib/format.cc
new file mode 100644
index 0000000..391d3a2
--- /dev/null
+++ b/src/fmtlib/format.cc
@@ -0,0 +1,43 @@
+// Formatting library for C++
+//
+// Copyright (c) 2012 - 2016, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+#include "fmt/format-inl.h"
+
+FMT_BEGIN_NAMESPACE
+namespace detail {
+
+template FMT_API auto dragonbox::to_decimal(float x) noexcept
+ -> dragonbox::decimal_fp<float>;
+template FMT_API auto dragonbox::to_decimal(double x) noexcept
+ -> dragonbox::decimal_fp<double>;
+
+#ifndef FMT_STATIC_THOUSANDS_SEPARATOR
+template FMT_API locale_ref::locale_ref(const std::locale& loc);
+template FMT_API auto locale_ref::get<std::locale>() const -> std::locale;
+#endif
+
+// Explicit instantiations for char.
+
+template FMT_API auto thousands_sep_impl(locale_ref)
+ -> thousands_sep_result<char>;
+template FMT_API auto decimal_point_impl(locale_ref) -> char;
+
+template FMT_API void buffer<char>::append(const char*, const char*);
+
+template FMT_API void vformat_to(buffer<char>&, string_view,
+ typename vformat_args<>::type, locale_ref);
+
+// Explicit instantiations for wchar_t.
+
+template FMT_API auto thousands_sep_impl(locale_ref)
+ -> thousands_sep_result<wchar_t>;
+template FMT_API auto decimal_point_impl(locale_ref) -> wchar_t;
+
+template FMT_API void buffer<wchar_t>::append(const wchar_t*, const wchar_t*);
+
+} // namespace detail
+FMT_END_NAMESPACE
diff --git a/src/fmtlib/os.cc b/src/fmtlib/os.cc
new file mode 100644
index 0000000..01732b3
--- /dev/null
+++ b/src/fmtlib/os.cc
@@ -0,0 +1,390 @@
+// Formatting library for C++ - optional OS-specific functionality
+//
+// Copyright (c) 2012 - 2016, Victor Zverovich
+// All rights reserved.
+//
+// For the license information refer to format.h.
+
+// Disable bogus MSVC warnings.
+#if !defined(_CRT_SECURE_NO_WARNINGS) && defined(_MSC_VER)
+# define _CRT_SECURE_NO_WARNINGS
+#endif
+
+#include "fmt/os.h"
+
+#include <climits>
+
+#if FMT_USE_FCNTL
+# include <sys/stat.h>
+# include <sys/types.h>
+
+# ifndef _WIN32
+# include <unistd.h>
+# else
+# ifndef WIN32_LEAN_AND_MEAN
+# define WIN32_LEAN_AND_MEAN
+# endif
+# include <io.h>
+
+# ifndef S_IRUSR
+# define S_IRUSR _S_IREAD
+# endif
+# ifndef S_IWUSR
+# define S_IWUSR _S_IWRITE
+# endif
+# ifndef S_IRGRP
+# define S_IRGRP 0
+# endif
+# ifndef S_IWGRP
+# define S_IWGRP 0
+# endif
+# ifndef S_IROTH
+# define S_IROTH 0
+# endif
+# ifndef S_IWOTH
+# define S_IWOTH 0
+# endif
+# endif // _WIN32
+#endif // FMT_USE_FCNTL
+
+#ifdef _WIN32
+# include <windows.h>
+#endif
+
+namespace {
+#ifdef _WIN32
+// Return type of read and write functions.
+using rwresult = int;
+
+// On Windows the count argument to read and write is unsigned, so convert
+// it from size_t preventing integer overflow.
+inline unsigned convert_rwcount(std::size_t count) {
+ return count <= UINT_MAX ? static_cast<unsigned>(count) : UINT_MAX;
+}
+#elif FMT_USE_FCNTL
+// Return type of read and write functions.
+using rwresult = ssize_t;
+
+inline std::size_t convert_rwcount(std::size_t count) { return count; }
+#endif
+} // namespace
+
+FMT_BEGIN_NAMESPACE
+
+#ifdef _WIN32
+namespace detail {
+
+class system_message {
+ system_message(const system_message&) = delete;
+ void operator=(const system_message&) = delete;
+
+ unsigned long result_;
+ wchar_t* message_;
+
+ static bool is_whitespace(wchar_t c) noexcept {
+ return c == L' ' || c == L'\n' || c == L'\r' || c == L'\t' || c == L'\0';
+ }
+
+ public:
+ explicit system_message(unsigned long error_code)
+ : result_(0), message_(nullptr) {
+ result_ = FormatMessageW(
+ FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM |
+ FORMAT_MESSAGE_IGNORE_INSERTS,
+ nullptr, error_code, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ reinterpret_cast<wchar_t*>(&message_), 0, nullptr);
+ if (result_ != 0) {
+ while (result_ != 0 && is_whitespace(message_[result_ - 1])) {
+ --result_;
+ }
+ }
+ }
+ ~system_message() { LocalFree(message_); }
+ explicit operator bool() const noexcept { return result_ != 0; }
+ operator basic_string_view<wchar_t>() const noexcept {
+ return basic_string_view<wchar_t>(message_, result_);
+ }
+};
+
+class utf8_system_category final : public std::error_category {
+ public:
+ const char* name() const noexcept override { return "system"; }
+ std::string message(int error_code) const override {
+ system_message msg(error_code);
+ if (msg) {
+ unicode_to_utf8<wchar_t> utf8_message;
+ if (utf8_message.convert(msg)) {
+ return utf8_message.str();
+ }
+ }
+ return "unknown error";
+ }
+};
+
+} // namespace detail
+
+FMT_API const std::error_category& system_category() noexcept {
+ static const detail::utf8_system_category category;
+ return category;
+}
+
+std::system_error vwindows_error(int err_code, string_view format_str,
+ format_args args) {
+ auto ec = std::error_code(err_code, system_category());
+ return std::system_error(ec, vformat(format_str, args));
+}
+
+void detail::format_windows_error(detail::buffer<char>& out, int error_code,
+ const char* message) noexcept {
+ FMT_TRY {
+ system_message msg(error_code);
+ if (msg) {
+ unicode_to_utf8<wchar_t> utf8_message;
+ if (utf8_message.convert(msg)) {
+ fmt::format_to(buffer_appender<char>(out), FMT_STRING("{}: {}"),
+ message, string_view(utf8_message));
+ return;
+ }
+ }
+ }
+ FMT_CATCH(...) {}
+ format_error_code(out, error_code, message);
+}
+
+void report_windows_error(int error_code, const char* message) noexcept {
+ report_error(detail::format_windows_error, error_code, message);
+}
+#endif // _WIN32
+
+buffered_file::~buffered_file() noexcept {
+ if (file_ && FMT_SYSTEM(fclose(file_)) != 0)
+ report_system_error(errno, "cannot close file");
+}
+
+buffered_file::buffered_file(cstring_view filename, cstring_view mode) {
+ FMT_RETRY_VAL(file_, FMT_SYSTEM(fopen(filename.c_str(), mode.c_str())),
+ nullptr);
+ if (!file_)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot open file {}"),
+ filename.c_str()));
+}
+
+void buffered_file::close() {
+ if (!file_) return;
+ int result = FMT_SYSTEM(fclose(file_));
+ file_ = nullptr;
+ if (result != 0)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot close file")));
+}
+
+int buffered_file::descriptor() const {
+#ifdef fileno // fileno is a macro on OpenBSD so we cannot use FMT_POSIX_CALL.
+ int fd = fileno(file_);
+#else
+ int fd = FMT_POSIX_CALL(fileno(file_));
+#endif
+ if (fd == -1)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot get file descriptor")));
+ return fd;
+}
+
+#if FMT_USE_FCNTL
+# ifdef _WIN32
+using mode_t = int;
+# endif
+constexpr mode_t default_open_mode =
+ S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
+
+file::file(cstring_view path, int oflag) {
+# if defined(_WIN32) && !defined(__MINGW32__)
+ fd_ = -1;
+ auto converted = detail::utf8_to_utf16(string_view(path.c_str()));
+ *this = file::open_windows_file(converted.c_str(), oflag);
+# else
+ FMT_RETRY(fd_, FMT_POSIX_CALL(open(path.c_str(), oflag, default_open_mode)));
+ if (fd_ == -1)
+ FMT_THROW(
+ system_error(errno, FMT_STRING("cannot open file {}"), path.c_str()));
+# endif
+}
+
+file::~file() noexcept {
+ // Don't retry close in case of EINTR!
+ // See http://linux.derkeiler.com/Mailing-Lists/Kernel/2005-09/3000.html
+ if (fd_ != -1 && FMT_POSIX_CALL(close(fd_)) != 0)
+ report_system_error(errno, "cannot close file");
+}
+
+void file::close() {
+ if (fd_ == -1) return;
+ // Don't retry close in case of EINTR!
+ // See http://linux.derkeiler.com/Mailing-Lists/Kernel/2005-09/3000.html
+ int result = FMT_POSIX_CALL(close(fd_));
+ fd_ = -1;
+ if (result != 0)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot close file")));
+}
+
+long long file::size() const {
+# ifdef _WIN32
+ // Use GetFileSize instead of GetFileSizeEx for the case when _WIN32_WINNT
+ // is less than 0x0500 as is the case with some default MinGW builds.
+ // Both functions support large file sizes.
+ DWORD size_upper = 0;
+ HANDLE handle = reinterpret_cast<HANDLE>(_get_osfhandle(fd_));
+ DWORD size_lower = FMT_SYSTEM(GetFileSize(handle, &size_upper));
+ if (size_lower == INVALID_FILE_SIZE) {
+ DWORD error = GetLastError();
+ if (error != NO_ERROR)
+ FMT_THROW(windows_error(GetLastError(), "cannot get file size"));
+ }
+ unsigned long long long_size = size_upper;
+ return (long_size << sizeof(DWORD) * CHAR_BIT) | size_lower;
+# else
+ using Stat = struct stat;
+ Stat file_stat = Stat();
+ if (FMT_POSIX_CALL(fstat(fd_, &file_stat)) == -1)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot get file attributes")));
+ static_assert(sizeof(long long) >= sizeof(file_stat.st_size),
+ "return type of file::size is not large enough");
+ return file_stat.st_size;
+# endif
+}
+
+std::size_t file::read(void* buffer, std::size_t count) {
+ rwresult result = 0;
+ FMT_RETRY(result, FMT_POSIX_CALL(read(fd_, buffer, convert_rwcount(count))));
+ if (result < 0)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot read from file")));
+ return detail::to_unsigned(result);
+}
+
+std::size_t file::write(const void* buffer, std::size_t count) {
+ rwresult result = 0;
+ FMT_RETRY(result, FMT_POSIX_CALL(write(fd_, buffer, convert_rwcount(count))));
+ if (result < 0)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot write to file")));
+ return detail::to_unsigned(result);
+}
+
+file file::dup(int fd) {
+ // Don't retry as dup doesn't return EINTR.
+ // http://pubs.opengroup.org/onlinepubs/009695399/functions/dup.html
+ int new_fd = FMT_POSIX_CALL(dup(fd));
+ if (new_fd == -1)
+ FMT_THROW(system_error(
+ errno, FMT_STRING("cannot duplicate file descriptor {}"), fd));
+ return file(new_fd);
+}
+
+void file::dup2(int fd) {
+ int result = 0;
+ FMT_RETRY(result, FMT_POSIX_CALL(dup2(fd_, fd)));
+ if (result == -1) {
+ FMT_THROW(system_error(
+ errno, FMT_STRING("cannot duplicate file descriptor {} to {}"), fd_,
+ fd));
+ }
+}
+
+void file::dup2(int fd, std::error_code& ec) noexcept {
+ int result = 0;
+ FMT_RETRY(result, FMT_POSIX_CALL(dup2(fd_, fd)));
+ if (result == -1) ec = std::error_code(errno, std::generic_category());
+}
+
+void file::pipe(file& read_end, file& write_end) {
+ // Close the descriptors first to make sure that assignments don't throw
+ // and there are no leaks.
+ read_end.close();
+ write_end.close();
+ int fds[2] = {};
+# ifdef _WIN32
+ // Make the default pipe capacity same as on Linux 2.6.11+.
+ enum { DEFAULT_CAPACITY = 65536 };
+ int result = FMT_POSIX_CALL(pipe(fds, DEFAULT_CAPACITY, _O_BINARY));
+# else
+ // Don't retry as the pipe function doesn't return EINTR.
+ // http://pubs.opengroup.org/onlinepubs/009696799/functions/pipe.html
+ int result = FMT_POSIX_CALL(pipe(fds));
+# endif
+ if (result != 0)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot create pipe")));
+ // The following assignments don't throw because read_fd and write_fd
+ // are closed.
+ read_end = file(fds[0]);
+ write_end = file(fds[1]);
+}
+
+buffered_file file::fdopen(const char* mode) {
+// Don't retry as fdopen doesn't return EINTR.
+# if defined(__MINGW32__) && defined(_POSIX_)
+ FILE* f = ::fdopen(fd_, mode);
+# else
+ FILE* f = FMT_POSIX_CALL(fdopen(fd_, mode));
+# endif
+ if (!f) {
+ FMT_THROW(system_error(
+ errno, FMT_STRING("cannot associate stream with file descriptor")));
+ }
+ buffered_file bf(f);
+ fd_ = -1;
+ return bf;
+}
+
+# if defined(_WIN32) && !defined(__MINGW32__)
+file file::open_windows_file(wcstring_view path, int oflag) {
+ int fd = -1;
+ auto err = _wsopen_s(&fd, path.c_str(), oflag, _SH_DENYNO, default_open_mode);
+ if (fd == -1) {
+ FMT_THROW(
+ system_error(err, FMT_STRING("cannot open file {}"),
+ detail::unicode_to_utf8<wchar_t>(path.c_str()).c_str()));
+ }
+ return file(fd);
+}
+# endif
+
+# if !defined(__MSDOS__)
+long getpagesize() {
+# ifdef _WIN32
+ SYSTEM_INFO si;
+ GetSystemInfo(&si);
+ return si.dwPageSize;
+# else
+ long size = FMT_POSIX_CALL(sysconf(_SC_PAGESIZE));
+ if (size < 0)
+ FMT_THROW(system_error(errno, FMT_STRING("cannot get memory page size")));
+ return size;
+# endif
+}
+# endif
+
+namespace detail {
+
+void file_buffer::grow(size_t) {
+ if (this->size() == this->capacity()) flush();
+}
+
+file_buffer::file_buffer(cstring_view path,
+ const detail::ostream_params& params)
+ : file_(path, params.oflag) {
+ set(new char[params.buffer_size], params.buffer_size);
+}
+
+file_buffer::file_buffer(file_buffer&& other)
+ : detail::buffer<char>(other.data(), other.size(), other.capacity()),
+ file_(std::move(other.file_)) {
+ other.clear();
+ other.set(nullptr, 0);
+}
+
+file_buffer::~file_buffer() {
+ flush();
+ delete[] data();
+}
+} // namespace detail
+
+ostream::~ostream() = default;
+#endif // FMT_USE_FCNTL
+FMT_END_NAMESPACE
diff --git a/src/format2csv.py b/src/format2csv.py
new file mode 100644
index 0000000..a4d4d6d
--- /dev/null
+++ b/src/format2csv.py
@@ -0,0 +1,23 @@
+
+import glob
+import csv
+import sys
+import json
+
+def main(args):
+ with open(args[1], 'w') as ofp:
+ out = csv.writer(ofp)
+ for format_path in sorted(glob.glob("%s/*.json" % args[2])):
+ with open(format_path) as fp:
+ format_dict = json.load(fp)
+
+ for key in sorted(format_dict):
+ value = format_dict[key]
+ if not isinstance(value, dict):
+ continue
+ if 'title' not in value:
+ raise Exception("format '%s' is missing 'title'" % key)
+ out.writerow((value['title'], key, value['description']))
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/src/formats/README.md b/src/formats/README.md
new file mode 100644
index 0000000..518dff8
--- /dev/null
+++ b/src/formats/README.md
@@ -0,0 +1,5 @@
+# Formats
+
+This directory contains the built-in log file format definitions. These files
+are converted to C by `bin2c` and compiled into the executable. New formats
+need to be added to the [formats.am](formats.am) file.
diff --git a/src/formats/access_log.json b/src/formats/access_log.json
new file mode 100644
index 0000000..6a5b020
--- /dev/null
+++ b/src/formats/access_log.json
@@ -0,0 +1,117 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "access_log": {
+ "title": "Common Access Log",
+ "description": "The default web access log format for servers like Apache.",
+ "url": "http://en.wikipedia.org/wiki/Common_Log_Format",
+ "multiline": false,
+ "regex": {
+ "ts-first-noquotes": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?) (?<c_ip>[^ ]+) (?<cs_username>[^ ]+) (?<cs_method>[A-Z]+) (?!\")(?<cs_uri_stem>[^ \\?]+)(?:\\?(?<cs_uri_query>[^ ]*))? (?:-1|\\d+) (?<sc_status>\\d+) \\d+\\s*(?<body>.*)"
+ },
+ "ts-first": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?) (?<c_ip>[^ ]+) (?<cs_username>[^ ]+) (?<cs_method>[A-Z]+) \"(?<cs_uri_stem>[^ \\?]+)(?:\\?(?<cs_uri_query>[^ ]*))?\" (?:-1|\\d+) (?<sc_status>\\d+) \\d+\\s*(?<body>.*)"
+ },
+ "std": {
+ "pattern": "^(?<c_ip>[\\w\\.:\\-]+)\\s+[\\w\\.\\-]+\\s+(?<cs_username>\\S+)\\s+\\[(?<timestamp>[^\\]]+)\\] \"(?:\\-|(?<cs_method>\\w+) (?<cs_uri_stem>[^ \\?]+)(?:\\?(?<cs_uri_query>[^ ]*))? (?<cs_version>[\\w/\\.]+))\" (?<sc_status>\\d+) (?<sc_bytes>\\d+|-)(?: \"(?<cs_referer>[^\"]*)\" \"(?<cs_user_agent>[^\"]+)\")?\\s*(?<body>.*)"
+ },
+ "std-vhost": {
+ "pattern": "^(?<cs_host>[\\w\\-\\.]*)(?::\\d+)?\\s+(?<c_ip>[\\w\\.:\\-]+)\\s+[\\w\\.\\-]+\\s+(?<cs_username>\\S+)\\s+\\[(?<timestamp>[^\\]]+)\\] \"(?:\\-|(?<cs_method>\\w+) (?<cs_uri_stem>[^ \\?]+)(?:\\?(?<cs_uri_query>[^ ]*))? (?<cs_version>[\\w/\\.]+))\" (?<sc_status>\\d+) (?<sc_bytes>\\d+|-)(?: \"(?<cs_referer>[^\"]+)\" \"(?<cs_user_agent>[^\"]+)\")?\\s*(?<body>.*)"
+ },
+ "mod-std": {
+ "module-format": true,
+ "pattern": "^(?<c_ip>[\\w\\.:\\-]+)\\s+[\\w\\.\\-]+\\s+(?<cs_username>\\S+)\\s+\"(?:\\-|(?<cs_method>\\w+) (?<cs_uri_stem>[^ \\?]+)(?:\\?(?<cs_uri_query>[^ ]*))? (?<cs_version>[\\w/\\.]+))\" (?<sc_status>\\d+) (?<sc_bytes>\\d+|-)(?: \"(?<cs_referer>[^\"]+)\" \"(?<cs_user_agent>[^\"]+)\")?\\s*(?<body>.*)"
+ }
+ },
+ "level-field": "sc_status",
+ "level": {
+ "error": "^[^123].*"
+ },
+ "opid-field": "c_ip",
+ "value": {
+ "cs_host": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The value of the Host header"
+ },
+ "c_ip": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true,
+ "description": "The client IP address"
+ },
+ "cs_username": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The username passed from the client to the server"
+ },
+ "cs_method": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The request method"
+ },
+ "cs_uri_stem": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The path part of the request URI"
+ },
+ "cs_uri_query": {
+ "kind": "string",
+ "description": "The query parameters in the request URI"
+ },
+ "cs_version": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The client's HTTP version"
+ },
+ "sc_status": {
+ "kind": "integer",
+ "foreign-key": true,
+ "rewriter": ";SELECT :sc_status || ' (' || (SELECT message FROM http_status_codes WHERE status = :sc_status) || ') '",
+ "description": "The status code returned by the server"
+ },
+ "sc_bytes": {
+ "kind": "integer",
+ "description": "The number of bytes returned by the server"
+ },
+ "cs_referer": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The client's referrer"
+ },
+ "cs_user_agent": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The client's HTTP agent"
+ }
+ },
+ "sample": [
+ {
+ "line": "10.112.72.172 - - [11/Feb/2013:06:43:36 +0000] \"GET /client/ HTTP/1.1\" 200 5778 \"-\" \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17\"",
+ "level": "info"
+ },
+ {
+ "line": "10.112.72.172 - - [11/Feb/2013:06:43:36 +0000] \"GET /client/ HTTP/1.1\" 404 5778 \"-\" \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17\"",
+ "level": "error"
+ },
+ {
+ "line": "2013-02-11T06:43:36 10.112.72.172 - GET \"/client/\" -1 200 5778 \"-\" \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17\"",
+ "level": "info"
+ },
+ {
+ "line": "2013-02-11T06:43:36 10.112.72.172 - GET /client/ -1 200 5778 \"-\" \"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.17 (KHTML, like Gecko) Chrome/24.0.1312.57 Safari/537.17\"",
+ "level": "info"
+ },
+ {
+ "line": "10.1.10.51 - - [23/Dec/2014:21:20:35 +0000] \"POST /api/1/rest/foo/bar HTTP/1.1\" 200 - \"-\" \"-\" 293"
+ },
+ {
+ "line": "www.example.com 1.2.3.4 - theuser [10/Feb/2012:16:41:07 -0500] \"GET / HTTP/1.0\" 200 368 \"-\" \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11\""
+ },
+ {
+ "line": "10.112.2.3 - - [16/Sep/2022:00:53:14 +0200] \"POST /api/v4/jobs/request HTTP/1.1\" 204 0 \"\" \"gitlab-runner 15.3.0 (15-3-stable; go1.19; linux/amd64)\" -",
+ "level": "info"
+ }
+ ]
+ }
+}
diff --git a/src/formats/alb_log.json b/src/formats/alb_log.json
new file mode 100644
index 0000000..06dca84
--- /dev/null
+++ b/src/formats/alb_log.json
@@ -0,0 +1,133 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "alb_log": {
+ "title": "Amazon ALB log",
+ "description": "Log format for Amazon Application Load Balancers",
+ "url": "https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html",
+ "regex": {
+ "std": {
+ "pattern": "^(?<type>(http)|(https)|(h2)|(ws)|(wss)) (?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{6}Z) (?<elb>[^ ]+) (?<client_ip>[\\w\\.:]+):(?<client_port>\\d+) (?<target_ip>[\\w\\.:]+):(?<target_port>\\d+) (?<request_processing_time>(-1)|(\\d+(\\.\\d+))?) (?<target_processing_time>(-1)|(\\d+(\\.\\d+))?) (?<response_processing_time>(-1)|(\\d+(\\.\\d+))?) (?<elb_status_code>\\d+|-) (?<target_status_code>\\d+|-) (?<received_bytes>\\d+) (?<sent_bytes>\\d+) \"(?:\\-|(?<cs_method>\\w+|-) (?<cs_uri_whole>(?<cs_uri_stem>(?:(?<cs_uri_scheme>https|http)?://)?(?:(?<cs_uri_hostname>[^:]+):(?<cs_uri_port>\\d+)?)?(?<cs_uri_path>[^ \\?]+)?)(?:\\?(?<cs_uri_query>[^ ]*))?) (?<cs_version>[\\w/\\.]+|-)\\s*)\" \"(?<user_agent>[^\"]+)\" (?<ssl_cipher>[\\w-]+) (?<ssl_protocol>[\\w\\.-]+) (?<target_group_arn>[^ ]+) \"(?<trace_id>[^ ]+)\" (?<domain_name>[^ ]+) (?<chosen_cert_arn>[^ ]+) ?(?<matched_rule_priority>(-1)|\\b([0-9]|[1-8][0-9]|9[0-9]|[1-8][0-9]{2}|9[0-8][0-9]|99[0-9]|[1-8][0-9]{3}|9[0-8][0-9]{2}|99[0-8][0-9]|999[0-9]|[1-4][0-9]{4}|50000)\\b)?"
+ }
+ },
+ "level-field": "elb_status_code",
+ "level": {
+ "error": "^[^123].*"
+ },
+ "opid-field": "client_ip",
+ "value": {
+ "type": {
+ "kind": "string",
+ "identifier": true
+ },
+ "elb": {
+ "kind": "string",
+ "identifier": true
+ },
+ "client_ip": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "client_port": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "target_ip": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "target_port": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "request_processing_time": {
+ "kind": "float"
+ },
+ "target_processing_time": {
+ "kind": "float"
+ },
+ "response_processing_time": {
+ "kind": "float"
+ },
+ "elb_status_code": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "target_status_code": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "received_bytes": {
+ "kind": "integer"
+ },
+ "sent_bytes": {
+ "kind": "integer"
+ },
+ "cs_method": {
+ "kind": "string",
+ "identifier": true
+ },
+ "cs_uri_stem": {
+ "kind": "string",
+ "identifier": true
+ },
+ "cs_uri_query": {
+ "kind": "string"
+ },
+ "cs_version": {
+ "kind": "string",
+ "identifier": true
+ },
+ "user_agent": {
+ "kind": "string",
+ "identifier": true
+ },
+ "ssl_cipher": {
+ "kind": "string",
+ "identifier": true
+ },
+ "ssl_protocol": {
+ "kind": "string",
+ "identifier": true
+ },
+ "target_group_arn": {
+ "kind": "string",
+ "identifier": true
+ },
+ "trace_id": {
+ "kind": "string",
+ "identifier": true
+ },
+ "domain_name": {
+ "kind": "string",
+ "identifier": true
+ },
+ "chosen_cert_arn": {
+ "kind": "string",
+ "identifier": true
+ },
+ "matched_rule_priority": {
+ "kind": "integer",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "http 2016-08-10T22:08:42.945958Z app/my-loadbalancer/50dc6c495c0c9188 192.168.131.39:2817 10.0.0.1:80 0.000 0.001 0.000 200 200 34 366 \"GET http://www.example.com:80/ HTTP/1.1\" \"curl/7.46.0\" - - arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/my-targets/73e2d6bc24d8a067 \"Root=1-58337262-36d228ad5d99923122bbe354\" - -"
+ },
+ {
+ "line": "https 2016-08-10T23:39:43.065466Z app/my-loadbalancer/50dc6c495c0c9188 192.168.131.39:2817 10.0.0.1:80 0.086 0.048 0.037 200 200 0 57 \"GET https://www.example.com:443/ HTTP/1.1\" \"curl/7.46.0\" ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/my-targets/73e2d6bc24d8a067 \"Root=1-58337281-1d84f3d73c47ec4e58577259\" www.example.com arn:aws:acm:us-east-2:123456789012:certificate/12345678-1234-1234-1234-123456789012"
+ },
+ {
+ "line": "h2 2016-08-10T00:10:33.145057Z app/my-loadbalancer/50dc6c495c0c9188 10.0.1.252:48160 10.0.0.66:9000 0.000 0.002 0.000 200 200 5 257 \"GET https://10.0.2.105:773/ HTTP/2.0\" \"curl/7.46.0\" ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/my-targets/73e2d6bc24d8a067 \"Root=1-58337327-72bd00b0343d75b906739c42\" - -"
+ },
+ {
+ "line": "ws 2016-08-10T00:32:08.923954Z app/my-loadbalancer/50dc6c495c0c9188 10.0.0.140:40914 10.0.1.192:8010 0.001 0.003 0.000 101 101 218 587 \"GET http://10.0.0.30:80/ HTTP/1.1\" \"-\" - - arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/my-targets/73e2d6bc24d8a067 \"Root=1-58337364-23a8c76965a2ef7629b185e3\" - -"
+ },
+ {
+ "line": "wss 2016-08-10T00:42:46.423695Z app/my-loadbalancer/50dc6c495c0c9188 10.0.0.140:44244 10.0.0.171:8010 0.000 0.001 0.000 101 101 218 786 \"GET https://10.0.0.30:443/ HTTP/1.1\" \"-\" ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2 arn:aws:elasticloadbalancing:us-west-2:123456789012:targetgroup/my-targets/73e2d6bc24d8a067 \"Root=1-58337364-23a8c76965a2ef7629b185e3\" - -"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/block_log.json b/src/formats/block_log.json
new file mode 100644
index 0000000..aaba6d8
--- /dev/null
+++ b/src/formats/block_log.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "block_log": {
+ "title": "Generic Block",
+ "description": "A generic format for logs, like cron, that have a date at the start of a block.",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\S{3,8} \\w{3}\\s+\\d{1,2} \\d{2}:\\d{2}:\\d{2} \\w+ \\d{4})\\s*(?<body>.*)$"
+ },
+ "sq-brackets": {
+ "pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3,6})?)Z?\\]\\s*(?<body>.*)$"
+ }
+ },
+ "sample": [
+ {
+ "line": "Sat Apr 27 03:33:07 PDT 2013\nHello, World"
+ },
+ {
+ "line": "[2021-05-21T21:58:57.022497Z]"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/bunyan_log.json b/src/formats/bunyan_log.json
new file mode 100644
index 0000000..4902d19
--- /dev/null
+++ b/src/formats/bunyan_log.json
@@ -0,0 +1,105 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "bunyan": {
+ "title": "Bunyan log",
+ "url": "https://github.com/trentm/node-bunyan",
+ "description": "Bunyan JSON logging library for node.js",
+ "json": true,
+ "line-format": [
+ {
+ "field": "time"
+ },
+ " ",
+ {
+ "field": "name"
+ },
+ "[",
+ {
+ "field": "pid"
+ },
+ "] ",
+ {
+ "field": "__level__",
+ "text-transform": "uppercase",
+ "auto-width": true
+ },
+ {
+ "field": "src/file",
+ "default-value": "",
+ "prefix": "["
+ },
+ {
+ "field": "src/line",
+ "default-value": "",
+ "prefix": ":"
+ },
+ {
+ "field": "src/func",
+ "default-value": "",
+ "prefix": ":",
+ "suffix": "]"
+ },
+ " ",
+ {
+ "field": "msg"
+ }
+ ],
+ "level-field": "level",
+ "level": {
+ "fatal": 60,
+ "error": 50,
+ "warning": 40,
+ "info": 30,
+ "debug": 20,
+ "trace": 10
+ },
+ "value": {
+ "pid": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "name": {
+ "kind": "string",
+ "identifier": true
+ },
+ "hostname": {
+ "kind": "string",
+ "identifier": true,
+ "hidden": true
+ },
+ "time": {
+ "kind": "string",
+ "identifier": false
+ },
+ "level": {
+ "kind": "integer",
+ "identifier": true,
+ "foreign-key": true
+ },
+ "v": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "msg": {
+ "kind": "string"
+ },
+ "src": {
+ "kind": "json",
+ "hidden": true
+ },
+ "src/file": {
+ "kind": "string",
+ "identifier": true
+ },
+ "src/line": {
+ "kind": "integer"
+ },
+ "src/func": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "timestamp-field": "time",
+ "body-field": "msg"
+ }
+}
diff --git a/src/formats/candlepin_log.json b/src/formats/candlepin_log.json
new file mode 100644
index 0000000..cab0395
--- /dev/null
+++ b/src/formats/candlepin_log.json
@@ -0,0 +1,49 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "candlepin_log": {
+ "title": "Candlepin log format",
+ "description": "Log format used by Candlepin registration system",
+ "regex": {
+ "reqorg": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}) \\[(req=(?<req>[0-9a-f-]+)|=), org=(?<org>\\w*)\\] (?<alert_level>\\w+) (?<module>[\\w.]+) - (?<body>.*)$"
+ },
+ "other": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}[+-]\\d{4}) (?<body>.*)$"
+ }
+ },
+ "value": {
+ "req": {
+ "kind": "string",
+ "identifier": true
+ },
+ "org": {
+ "kind": "string",
+ "identifier": true
+ },
+ "alert_level": {
+ "kind": "string"
+ },
+ "module": {
+ "kind": "string",
+ "identifier": true
+ },
+ "body": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "2015-04-17 09:41:50,544 [=, org=] INFO org.candlepin.guice.CustomizableModules - Found custom module module.config.katello"
+ },
+ {
+ "line": "2015-04-17 09:41:56,320 [req=f91d4a84-020d-4874-9741-3979d0baf58d, org=] INFO org.candlepin.common.filter.LoggingFilter - Request: verb=GET, uri=/candlepin/status"
+ },
+ {
+ "line": "2015-04-17 09:42:39+0200 principalType=trusteduser principal=admin target=OWNER entityId=8ab219c64cc653a7014cc6545a6c0001 type=CREATED owner=8ab219c64cc653a7014cc6545a6c0001"
+ },
+ {
+ "line": "2015-04-17 10:49:21,912 [req=ec7867ea-2501-4036-bb08-e2d830720cb5, org=npr_goep_hm_com] INFO org.candlepin.common.filter.LoggingFilter - Response: status=200, content-type=\"application/json\", time=235ms"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/choose_repo_log.json b/src/formats/choose_repo_log.json
new file mode 100644
index 0000000..6397049
--- /dev/null
+++ b/src/formats/choose_repo_log.json
@@ -0,0 +1,24 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "choose_repo_log": {
+ "title": "Yum choose_repo Log",
+ "description": "The log format for the yum choose_repo tool.",
+ "regex": {
+ "std": {
+ "pattern": "^\\[(?<level>\\w+):[^\\]]+] [^:]+:\\d+ (?<timestamp>\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(?:[\\.,]\\d{3})?):(?<body>.*)"
+ }
+ },
+ "level-field": "level",
+ "level": {
+ "error": "ERROR",
+ "debug": "DEBUG",
+ "info": "INFO",
+ "warning": "WARNING"
+ },
+ "sample": [
+ {
+ "line": "[INFO:choose_repo] choose_repo:47 2013-06-20 17:26:10,691: Setting region in redhat-rhui.repo"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/cloudflare_log.json b/src/formats/cloudflare_log.json
new file mode 100644
index 0000000..01fa956
--- /dev/null
+++ b/src/formats/cloudflare_log.json
@@ -0,0 +1,236 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "cloudflare_json_log": {
+ "title": "Cloudflare Access Log",
+ "description": "Cloudflare Enterprise detailed logs of metadata",
+ "url": "https://developers.cloudflare.com/logs/",
+ "json": true,
+ "hide-extra": true,
+ "ordered-by-time": false,
+ "level-field": "CacheCacheStatus",
+ "level": {
+ "info": "hit",
+ "warning": "miss"
+ },
+ "line-format": [
+ {
+ "field": "ClientIP",
+ "auto-width": true
+ },
+ " ",
+ {
+ "prefix": "[",
+ "field": "__timestamp__",
+ "suffix": "]"
+ },
+ " \"",
+ {
+ "field": "ClientRequestMethod"
+ },
+ " ",
+ {
+ "field": "ClientRequestURI"
+ },
+ " ",
+ {
+ "field": "ClientRequestProtocol"
+ },
+ "\" ",
+ {
+ "field": "EdgeResponseStatus"
+ },
+ " ",
+ {
+ "field": "EdgeResponseBytes"
+ },
+ " ",
+ {
+ "field": "ClientRequestReferer"
+ },
+ " ",
+ {
+ "field": "ClientRequestUserAgent"
+ }
+ ],
+ "timestamp-field": "EdgeStartTimestamp",
+ "opid-field": "ClientIP",
+ "value": {
+ "ClientIP": {
+ "kind": "string",
+ "identifier": true
+ },
+ "ClientRequestMethod": {
+ "kind": "string"
+ },
+ "ClientRequestURI": {
+ "kind": "string",
+ "identifier": true
+ },
+ "EdgeEndTimestamp": {
+ "kind": "string",
+ "hidden": true
+ },
+ "EdgeResponseBytes": {
+ "kind": "integer"
+ },
+ "EdgeResponseStatus": {
+ "kind": "integer"
+ },
+ "EdgeStartTimestamp": {
+ "kind": "string"
+ },
+ "RayID": {
+ "kind": "string",
+ "identifier": true,
+ "foreign-key": true,
+ "hidden": true
+ },
+ "CacheCacheStatus": {
+ "kind": "string",
+ "hidden": true
+ },
+ "CacheTieredFill": {
+ "kind": "json",
+ "hidden": true
+ },
+ "CacheResponseBytes": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "CacheResponseStatus": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "FirewallMatchesActions": {
+ "kind": "json",
+ "hidden": true
+ },
+ "FirewallMatchesRuleIDs": {
+ "kind": "json",
+ "hidden": true
+ },
+ "FirewallMatchesSources": {
+ "kind": "json",
+ "hidden": true
+ },
+ "OriginResponseBytes": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "OriginResponseDurationMs": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "OriginResponseHTTPExpires": {
+ "kind": "string",
+ "hidden": true
+ },
+ "OriginResponseHTTPLastModified": {
+ "kind": "string",
+ "hidden": true
+ },
+ "OriginResponseHeaderReceiveDurationMs": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "OriginResponseStatus": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "OriginResponseTime": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "OriginDNSResponseTimeMs": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "OriginIP": {
+ "kind": "string",
+ "hidden": true
+ },
+ "OriginRequestHeaderSendDurationMs": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "OriginSSLProtocol": {
+ "kind": "string",
+ "hidden": true
+ },
+ "OriginTCPHandshakeDurationMs": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "OriginTLSHandshakeDurationMs": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "WAFAction": {
+ "kind": "string",
+ "hidden": true
+ },
+ "WAFFlags": {
+ "kind": "string",
+ "hidden": true
+ },
+ "WAFMatchedVar": {
+ "kind": "string",
+ "hidden": true
+ },
+ "WAFProfile": {
+ "kind": "string",
+ "hidden": true
+ },
+ "WAFRuleID": {
+ "kind": "string",
+ "hidden": true
+ },
+ "WAFRuleMessage": {
+ "kind": "string",
+ "hidden": true
+ },
+ "ClientASN": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "ClientCountry": {
+ "kind": "string",
+ "hidden": true
+ },
+ "ClientDeviceType": {
+ "kind": "string",
+ "hidden": true
+ },
+ "ClientIPClass": {
+ "kind": "string",
+ "hidden": true
+ },
+ "ClientRequestBytes": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "ClientRequestPath": {
+ "kind": "integer",
+ "identifier": true,
+ "hidden": true
+ },
+ "ClientRequestProtocol": {
+ "kind": "string"
+ },
+ "ClientRequestUserAgent": {
+ "kind": "string"
+ },
+ "ClientRequestReferer": {
+ "kind": "string"
+ },
+ "ClientRequestScheme": {
+ "kind": "string",
+ "hidden": true
+ },
+ "ClientRequestSource": {
+ "kind": "string",
+ "hidden": true
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/formats/cloudvm_ram_log.json b/src/formats/cloudvm_ram_log.json
new file mode 100644
index 0000000..723cdaa
--- /dev/null
+++ b/src/formats/cloudvm_ram_log.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "cloudvm_ram_log": {
+ "title": "CloudVM Ram Log",
+ "description": "Periodic dumps of ram sizes",
+ "regex": {
+ "std": {
+ "pattern": "========== Start of cloudvm ram size dump at (?<timestamp>[^=]+)==========(?<body>.*)"
+ }
+ },
+ "sample": [
+ {
+ "line": "========== Start of cloudvm ram size dump at Thu Jun 2 08:00:01 UTC 2022 =========="
+ }
+ ],
+ "search-table": {
+ "cloudvm_ram_sizes": {
+ "pattern": "^(?!TOTAL)(?<ServiceName>\\S+)\\s+(?<AllocatedMB>-?\\d+)\\s+(?<MaxMB>-?\\d+)\\s+(?<CurrentMB>-?\\d+)\\s+(?<CurrRSS>-?\\d+)\\s+(?<Cache>-?\\d+)\\s+(?<MapFiles>-?\\d+)\\s+(?<MemoryLimit>-?\\d+)"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/formats/cups_log.json b/src/formats/cups_log.json
new file mode 100644
index 0000000..b79f636
--- /dev/null
+++ b/src/formats/cups_log.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "cups_log": {
+ "title": "CUPS log format",
+ "description": "Log format used by the Common Unix Printing System",
+ "regex": {
+ "system": {
+ "pattern": "^(?<level>[IEW]) \\[(?<timestamp>\\d{2}/\\S{3,8}/\\d{4}:\\d{2}:\\d{2}:\\d{2} [+-]\\d{2,4})\\] (?<section>\\w+): (?<body>.*)$"
+ },
+ "default": {
+ "pattern": "^(?<level>[IEW]) \\[(?<timestamp>\\d{2}/\\S{3,8}/\\d{4}:\\d{2}:\\d{2}:\\d{2} [+-]\\d{2,4})\\] (?!\\w+:)(?<body>.*)$"
+ }
+ },
+ "level": {
+ "error": "E",
+ "warning": "W"
+ },
+ "value": {
+ "level": {
+ "kind": "string",
+ "identifier": true
+ },
+ "section": {
+ "kind": "string",
+ "identifier": true
+ },
+ "body": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "I [04/Nov/2010:17:37:40 -0400] Allowing up to 100 client connections per host."
+ },
+ {
+ "line": "I [04/Nov/2010:17:37:40 -0400] LoadPPDs: Wrote \"/etc/cups/ppds.dat\", 14 PPDs..."
+ },
+ {
+ "line": "E [04/Nov/2010:17:37:40 -0400] StartListening: Unable to find IP address for server name \"localhost.localdomain\" - Host name lookup failure"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/dpkg_log.json b/src/formats/dpkg_log.json
new file mode 100644
index 0000000..d485831
--- /dev/null
+++ b/src/formats/dpkg_log.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "dpkg_log": {
+ "title": "Dpkg Log",
+ "description": "The debian dpkg log.",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}[T ]\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?) (?:(?:(?<action>startup|status|configure|install|upgrade|trigproc|remove|purge)(?: (?<status>config-files|failed-config|half-configured|half-installed|installed|not-installed|post-inst-failed|removal-failed|triggers-awaited|triggers-pending|unpacked))? (?<package>[^ ]+) (?<installed_version>[^ ]+)(?: (?<available_version>[^ ]+))?)|update-alternatives: (?<body>.*))$"
+ }
+ },
+ "value": {
+ "action": {
+ "kind": "string",
+ "identifier": true
+ },
+ "status": {
+ "kind": "string",
+ "identifier": true
+ },
+ "package": {
+ "kind": "string",
+ "identifier": true
+ },
+ "installed_version": {
+ "kind": "string"
+ },
+ "available_version": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "2012-02-14 10:44:10 configure base-files 5.0.0ubuntu20 5.0.0ubuntu20"
+ },
+ {
+ "line": "2012-02-14 10:44:30 status unpacked rsyslog 4.2.0-2ubuntu8"
+ },
+ {
+ "line": "2012-02-14 10:44:32 update-alternatives: run with --install /usr/bin/rview rview /usr/bin/vim.tiny 10"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/elb_log.json b/src/formats/elb_log.json
new file mode 100644
index 0000000..e13dc53
--- /dev/null
+++ b/src/formats/elb_log.json
@@ -0,0 +1,109 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "elb_log": {
+ "title": "Amazon ELB log",
+ "description": "Log format for Amazon Elastic Load Balancers",
+ "url": "http://docs.aws.amazon.com/ElasticLoadBalancing/latest/DeveloperGuide/access-log-collection.html",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{6}Z) (?<elb>[^ ]+) (?<client_ip>[\\w\\.:]+):(?<client_port>\\d+) (?<backend_ip>[\\w\\.:]+):(?<backend_port>\\d+) (?<request_processing_time>\\d+(\\.\\d+)?) (?<backend_processing_time>\\d+(\\.\\d+)?) (?<response_processing_time>\\d+(\\.\\d+)?) (?<elb_status_code>\\d+|-) (?<backend_status_code>\\d+|-) (?<received_bytes>\\d+) (?<sent_bytes>\\d+) \"(?:\\-|(?<cs_method>\\w+|-) (?<cs_uri_stem>[^ \\?]+)(?:\\?(?<cs_uri_query>[^ ]*))? (?<cs_version>[\\w/\\.]+|-)\\s*)\" \"(?<user_agent>[^\"]+)\" (?<ssl_cipher>[\\w-]+) (?<ssl_protocol>[\\w\\.-]+)(?<body>.*)"
+ }
+ },
+ "level-field": "elb_status_code",
+ "level": {
+ "error": "^[^123].*"
+ },
+ "opid-field": "client_ip",
+ "value": {
+ "elb": {
+ "kind": "string",
+ "identifier": true
+ },
+ "client_ip": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "client_port": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "backend_ip": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "backend_port": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "request_processing_time": {
+ "kind": "float"
+ },
+ "backend_processing_time": {
+ "kind": "float"
+ },
+ "response_processing_time": {
+ "kind": "float"
+ },
+ "elb_status_code": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "backend_status_code": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "received_bytes": {
+ "kind": "integer"
+ },
+ "sent_bytes": {
+ "kind": "integer"
+ },
+ "cs_method": {
+ "kind": "string",
+ "identifier": true
+ },
+ "cs_uri_stem": {
+ "kind": "string",
+ "identifier": true
+ },
+ "cs_uri_query": {
+ "kind": "string"
+ },
+ "cs_version": {
+ "kind": "string",
+ "identifier": true
+ },
+ "user_agent": {
+ "kind": "string",
+ "identifier": true
+ },
+ "ssl_cipher": {
+ "kind": "string",
+ "identifier": true
+ },
+ "ssl_protocol": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "2015-11-17T05:45:24.077255Z elastic-prod 54.161.222.121:40909 10.231.68.180:443 0.000031 0.009511 0.000029 200 200 0 415 \"GET https://example.com/foo/bar?baz=1234 HTTP/1.1\" \"test agent\" ECDHE-RSA-AES128-GCM-SHA256 TLSv1.2"
+ },
+ {
+ "line": "2015-05-13T23:39:43.945958Z my-loadbalancer 192.168.131.39:2817 10.0.0.1:80 0.000073 0.001048 0.000057 200 200 0 29 \"GET http://www.example.com:80/ HTTP/1.1\" \"curl/7.38.0\" - -"
+ },
+ {
+ "line": "2015-05-13T23:39:43.945958Z my-loadbalancer 192.168.131.39:2817 10.0.0.1:80 0.000086 0.001048 0.001337 200 200 0 57 \"GET https://www.example.com:443/ HTTP/1.1\" \"curl/7.38.0\" DHE-RSA-AES128-SHA TLSv1.2"
+ },
+ {
+ "line": "2015-05-13T23:39:43.945958Z my-loadbalancer 192.168.131.39:2817 10.0.0.1:80 0.001069 0.000028 0.000041 - - 82 305 \"- - - \" \"-\" - -"
+ },
+ {
+ "line": "2015-05-13T23:39:43.945958Z my-loadbalancer 192.168.131.39:2817 10.0.0.1:80 0.001065 0.000015 0.000023 - - 57 502 \"- - - \" \"-\" ECDHE-ECDSA-AES128-GCM-SHA256 TLSv1.2"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/engine_log.json b/src/formats/engine_log.json
new file mode 100644
index 0000000..eeea2e8
--- /dev/null
+++ b/src/formats/engine_log.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "engine_log": {
+ "title": "engine log",
+ "description": "The log format for the engine.log files from RHEV/oVirt",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}+)\\s+(?<level>\\w+)\\s+\\[(?<logger>[^\\]]+)\\]\\s+\\((?<tid>[^\\)]+)\\)\\s+(?<body>.*)"
+ }
+ },
+ "opid-field": "tid",
+ "value": {
+ "tid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "logger": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "level-field": "level",
+ "level": {
+ "error": "ERROR",
+ "info": "INFO",
+ "warning": "WARN"
+ },
+ "sample": [
+ {
+ "line": "2014-09-21 04:01:29,522 INFO [org.ovirt.engine.core.bll.OvfDataUpdater] (DefaultQuartzScheduler_Worker-90) Successfully updated VM OVFs in Data Center Test"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/error_log.json b/src/formats/error_log.json
new file mode 100644
index 0000000..d3a94ad
--- /dev/null
+++ b/src/formats/error_log.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "error_log": {
+ "title": "Common Error Log",
+ "description": "The default web error log format for servers like Apache.",
+ "regex": {
+ "cups": {
+ "pattern": "^(?<level>\\w) \\[(?<timestamp>[^\\]]+)\\] (?<body>.*)"
+ },
+ "apache": {
+ "pattern": "^\\[(?<timestamp>[^\\]]+)\\] \\[(?:(?<module>[^:]+):)?(?<level>\\w+)\\](?: \\[pid (?<pid>\\d+)(:tid (?<tid>\\d+))?\\])?(?: \\[client (?<c_ip>[\\w\\.:\\-]+):(?<c_port>\\d+)\\])? (?<body>.*)"
+ }
+ },
+ "level-field": "level",
+ "value": {
+ "module": {
+ "kind": "string",
+ "identifier": true
+ },
+ "pid": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "tid": {
+ "kind": "integer",
+ "identifier": true,
+ "description": "The thread id"
+ },
+ "c_ip": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true,
+ "description": "The client IP address"
+ },
+ "c_port": {
+ "kind": "integer",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "E [08/Jun/2013:11:28:58 -0700] Unknown directive BrowseOrder on line 22 of /private/etc/cups/cupsd.conf.",
+ "level": "error"
+ },
+ {
+ "line": "[Tue Apr 04 06:18:29.712806 2017] [mpm_prefork:notice] [pid 17725] AH00163: Apache/2.4.23 (Unix) configured -- resuming normal operations",
+ "level": "notice"
+ },
+ {
+ "line": "[Tue Apr 04 06:28:08.605341 2017] [core:error] [pid 17962] [client 127.0.0.1:60444] AH00135: Invalid method in request FOO /",
+ "level": "error"
+ },
+ {
+ "line": "[Thu Jan 17 02:42:49 2013] [notice] Digest: generating secret for digest authentication ...",
+ "level": "notice"
+ },
+ {
+ "line": "[Thu May 12 08:28:57.652118 2011] [core:error] [pid 8777:tid 4326490112] [client ::1:58619] File does not exist: /usr/local/apache2/htdocs/favicon.ico",
+ "level": "error"
+ },
+ {
+ "line": "[Thu Jan 02 22:23:07.368853 2020] [http:info] [pid 4784:tid 139701043291904] [client 66.220.149.10:45948] AH01593: chunked Transfer-Encoding forbidden: /",
+ "level": "info"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/esx_syslog_log.json b/src/formats/esx_syslog_log.json
new file mode 100644
index 0000000..85fa881
--- /dev/null
+++ b/src/formats/esx_syslog_log.json
@@ -0,0 +1,66 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "esx_syslog_log": {
+ "title": "ESXi Syslog",
+ "description": "Format specific to the ESXi syslog",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>(?:\\S{3,8}\\s+\\d{1,2} \\d{2}:\\d{2}:\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z))\\s+(?<level>\\w+)\\((?<syslog_pri>\\d+)\\)(?:\\[\\+\\]|\\+)?(?:(?: (?<log_syslog_tag>(?<log_procname>(?:[^\\[:]+|[^:]+))(?:\\[(?<log_pid>\\d+)\\])?):\\s*(?<body>.*))$|:?(?:(?: ---)? last message repeated \\d+ times?(?: ---)?))"
+ },
+ "host": {
+ "pattern": "^(?<timestamp>(?:\\S{3,8}\\s+\\d{1,2} \\d{2}:\\d{2}:\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?Z))\\s+(?<level>\\w+)\\((?<syslog_pri>\\d+)\\)(?:\\[\\+\\]|\\+)?(?:(?: (?<log_syslog_tag>(?:host-(?<log_pid>\\d+))?)\\s+(?<body>.*))$|:?(?:(?: ---)? last message repeated \\d+ times?(?: ---)?))"
+ },
+ "notime": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2})\\s+(?<level>\\w+)\\((?<syslog_pri>\\d+)\\)\\s+(?<log_procname>[^\\[]+)\\[(?<log_pid>\\d+)\\]:\\s(?<new_time>\\d{2}:\\d{2}:\\d{2}\\.\\d+)\\s+(?<body>.*)"
+ }
+ },
+ "level": {
+ "debug": "^Db$",
+ "info": "^In$",
+ "notice": "^No$",
+ "warning": "^Wa$",
+ "error": "^Er$",
+ "critical": "^Cr$",
+ "fatal": "^(?:Al|Em)$"
+ },
+ "opid-field": "log_syslog_tag",
+ "time-field": "new_time",
+ "multiline": false,
+ "value": {
+ "body": {
+ "kind": "string"
+ },
+ "log_pid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "log_procname": {
+ "kind": "string",
+ "identifier": true
+ },
+ "log_syslog_tag": {
+ "kind": "string"
+ },
+ "syslog_pri": {
+ "kind": "string"
+ },
+ "timestamp": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "2022-06-02T05:34:56.746Z In(14) ConfigStore[1001430703]: Log for ConfigStore version=1.0 build=build-19833347 option=BETA"
+ },
+ {
+ "line": "2022-06-02T05:34:23Z In(14)[+] hostprofile[1001430319]: {'mode': 'Disabled', 'exceptionUsers': []}"
+ },
+ {
+ "line": "2022-06-02 In(14) hostprofile[1001430319]: 05:34:23.666 {'mode': 'Disabled', 'exceptionUsers': []}"
+ },
+ {
+ "line": "2022-06-01T13:42:40.681Z In(05) host-16250 <analytics> Skip service health check. State STOPPED, Curr request 0"
+ }
+ ]
+ }
+}
diff --git a/src/formats/formats.am b/src/formats/formats.am
new file mode 100644
index 0000000..9b3d8a9
--- /dev/null
+++ b/src/formats/formats.am
@@ -0,0 +1,46 @@
+
+FORMAT_FILES = \
+ $(srcdir)/%reldir%/access_log.json \
+ $(srcdir)/%reldir%/alb_log.json \
+ $(srcdir)/%reldir%/block_log.json \
+ $(srcdir)/%reldir%/bunyan_log.json \
+ $(srcdir)/%reldir%/candlepin_log.json \
+ $(srcdir)/%reldir%/choose_repo_log.json \
+ $(srcdir)/%reldir%/cloudflare_log.json \
+ $(srcdir)/%reldir%/cloudvm_ram_log.json \
+ $(srcdir)/%reldir%/cups_log.json \
+ $(srcdir)/%reldir%/dpkg_log.json \
+ $(srcdir)/%reldir%/elb_log.json \
+ $(srcdir)/%reldir%/engine_log.json \
+ $(srcdir)/%reldir%/error_log.json \
+ $(srcdir)/%reldir%/esx_syslog_log.json \
+ $(srcdir)/%reldir%/fsck_hfs_log.json \
+ $(srcdir)/%reldir%/glog_log.json \
+ $(srcdir)/%reldir%/haproxy_log.json \
+ $(srcdir)/%reldir%/java_log.json \
+ $(srcdir)/%reldir%/journald_json_log.json \
+ $(srcdir)/%reldir%/katello_log.json \
+ $(srcdir)/%reldir%/openam_log.json \
+ $(srcdir)/%reldir%/openamdb_log.json \
+ $(srcdir)/%reldir%/openstack_log.json \
+ $(srcdir)/%reldir%/page_log.json \
+ $(srcdir)/%reldir%/papertrail_log.json \
+ $(srcdir)/%reldir%/pcap_log.json \
+ $(srcdir)/%reldir%/procstate_log.json \
+ $(srcdir)/%reldir%/snaplogic_log.json \
+ $(srcdir)/%reldir%/sssd_log.json \
+ $(srcdir)/%reldir%/strace_log.json \
+ $(srcdir)/%reldir%/sudo_log.json \
+ $(srcdir)/%reldir%/syslog_log.json \
+ $(srcdir)/%reldir%/s3_log.json \
+ $(srcdir)/%reldir%/tcf_log.json \
+ $(srcdir)/%reldir%/tcsh_history.json \
+ $(srcdir)/%reldir%/unifi_log.json \
+ $(srcdir)/%reldir%/uwsgi_log.json \
+ $(srcdir)/%reldir%/vdsm_log.json \
+ $(srcdir)/%reldir%/vmk_log.json \
+ $(srcdir)/%reldir%/vmw_log.json \
+ $(srcdir)/%reldir%/vmw_vc_svc_log.json \
+ $(srcdir)/%reldir%/vmw_py_log.json \
+ $(srcdir)/%reldir%/xmlrpc_log.json \
+ $()
diff --git a/src/formats/fsck_hfs_log.json b/src/formats/fsck_hfs_log.json
new file mode 100644
index 0000000..21757ab
--- /dev/null
+++ b/src/formats/fsck_hfs_log.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "fsck_hfs_log": {
+ "title": "Fsck_hfs Log",
+ "description": "Log for the fsck_hfs tool on Mac OS X.",
+ "regex": {
+ "std": {
+ "pattern": "^(?<device>[^:]+): fsck_hfs (?:run|started) at (?<timestamp>\\S{3,8} \\S{3,8}\\s+\\d{1,2} \\d{2}:\\d{2}:\\d{2} \\d{4})(?<body>.*)"
+ }
+ },
+ "value": {
+ "device": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "/dev/rdisk0s2: fsck_hfs run at Wed Jul 25 23:01:18 2012"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/glog_log.json b/src/formats/glog_log.json
new file mode 100644
index 0000000..09fcff4
--- /dev/null
+++ b/src/formats/glog_log.json
@@ -0,0 +1,52 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "glog_log": {
+ "title": "Glog",
+ "description": "The google glog format.",
+ "url": "https://code.google.com/p/google-glog/",
+ "regex": {
+ "std": {
+ "pattern": "^(?<level>[IWECF])(?<timestamp>\\d{4} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}) +(?<thread>\\d+) (?<src_file>[^:]+):(?<src_line>\\d+)\\] (?<body>.*)"
+ },
+ "std-with-year": {
+ "pattern": "^(?<level>[IWECF])(?<timestamp>\\d{8} \\d{2}:\\d{2}:\\d{2}\\.\\d{6}) +(?<thread>\\d+) (?<src_file>[^:]+):(?<src_line>\\d+)\\] (?<body>.*)"
+ }
+ },
+ "level-field": "level",
+ "level": {
+ "error": "E",
+ "warning": "W",
+ "info": "I",
+ "critical": "C",
+ "fatal": "F"
+ },
+ "opid-field": "thread",
+ "value": {
+ "thread": {
+ "kind": "integer",
+ "identifier": true,
+ "foreign-key": true
+ },
+ "src_file": {
+ "kind": "string",
+ "identifier": true
+ },
+ "src_line": {
+ "kind": "integer",
+ "foreign-key": true
+ }
+ },
+ "sample": [
+ {
+ "line": "E0517 15:04:22.619632 1952452992 logging_unittest.cc:253] Log every 3, iteration 19"
+ },
+ {
+ "line": "E0517 15:04:22.619632 52992 logging_unittest.cc:253] Log every 3, iteration 19"
+ },
+ {
+ "line": "I20200308 23:47:32.089828 400441 config.cc:27] Loading user configuration: /home/aesophor/.config/wmderland/config",
+ "level": "info"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/haproxy_log.json b/src/formats/haproxy_log.json
new file mode 100644
index 0000000..9795a19
--- /dev/null
+++ b/src/formats/haproxy_log.json
@@ -0,0 +1,173 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "haproxy_log": {
+ "title": "HAProxy HTTP Log Format",
+ "description": "The HAProxy log format",
+ "url": "http://www.haproxy.org/download/1.4/doc/configuration.txt",
+ "regex": {
+ "event_started": {
+ "pattern": "(?<timestamp>\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2}) (?<logging_host>[^ ]+) (?<process_name>\\w+)\\[(?<pid>\\d+)\\]: Proxy (?<frontend_name>[^ ]+) started."
+ },
+ "event_stopping": {
+ "pattern": "(?<timestamp>\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2}) (?<logging_host>[^ ]+) (?<process_name>\\w+)\\[(?<pid>\\d+)\\]: Stopping frontend (?<frontend_name>[^ ]+) in (?<stopping_timeout>\\d+) ms."
+ },
+ "event_stopped": {
+ "pattern": "(?<timestamp>\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2}) (?<logging_host>[^ ]+) (?<process_name>\\w+)\\[(?<pid>\\d+)\\]: Proxy (?<frontend_name>[^ ]+) stopped \\(FE: (?<frontend_connections>\\d+) conns, BE: (?<backend_connections>\\d+) conns\\)."
+ },
+ "tcp": {
+ "pattern": "(?<timestamp>\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2}) (?<logging_host>[^ ]+) (?<process_name>\\w+)\\[(?<pid>\\d+)\\]: (?<client_ip>[^:]+):(?<client_port>\\d+) \\[(?<accept_date>\\d{2}\\/\\w{3}\\/\\d{4}:\\d{2}:\\d{2}:\\d{2}.\\d{3})\\] (?<frontend_name>[^ ]+) (?<backend_name>[^ ]+)\\/(?<server_name>[^ ]+) (?<tw>\\d+)\\/(?<tc>\\d+)\\/(?<tt>\\d+) (?<bytes_read>\\d+) (?<termination_state>..) (?<actconn>\\d+)\\/(?<feconn>\\d+)\\/(?<beconn>\\d+)\\/(?<srv_conn>\\d+)\\/(?<retries>\\d+) (?<srv_queue>\\d+)\\/(?<backend_queue>\\d+)"
+ },
+ "http": {
+ "pattern": "(?<timestamp>\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2}) (?<logging_host>[^ ]+) (?<process_name>\\w+)\\[(?<pid>\\d+)\\]: (?<client_ip>[^:]+):(?<client_port>\\d+) \\[(?<accept_date>\\d{2}\\/\\w{3}\\/\\d{4}:\\d{2}:\\d{2}:\\d{2}.\\d{3})\\] (?<frontend_name>[^ ]+)(?<ssl>~)? (?<backend_name>[^ ]+)\\/(?<server_name>[^ ]+) (?<tq>-?\\d+)\\/(?<tw>-?\\d+)\\/(?<tc>-?\\d+)\\/(?<tr>-?\\d+)\\/(?<tt>\\d+) (?<status_code>\\d{3}|-1) (?<bytes_read>\\d+) (?<captured_request_cookie>.*) (?<captured_response_cookie>.*) (?<termination_state>....) (?<actconn>\\d+)\\/(?<feconn>\\d+)\\/(?<beconn>\\d+)\\/(?<srv_conn>\\d+)\\/(?<retries>\\d+) (?<srv_queue>\\d+)\\/(?<backend_queue>\\d+) (?:\\{(?<captured_request_headers>.*)\\} \\{(?<captured_response_headers>.*)\\} )?\"(?<http_method>[A-Z<>]+)(?: (?<http_url>.*?))?(?: (?<http_version>HTTP\\/\\d+.\\d+))?\"?$"
+ },
+ "ssl": {
+ "pattern": "(?<timestamp>\\w{3} \\d{2} \\d{2}:\\d{2}:\\d{2}) (?<logging_host>[^ ]+) (?<process_name>\\w+)\\[(?<pid>\\d+)\\]: (?<client_ip>[^:]+):(?<client_port>\\d+) \\[(?<accept_date>\\d{2}\\/\\w{3}\\/\\d{4}:\\d{2}:\\d{2}:\\d{2}.\\d{3})\\] (?<backend_name>[^ ]+)\\/(?<server_name>[^ ]+): (?<ssl_error>.+)$"
+ }
+ },
+ "json": false,
+ "value": {
+ "stopping_timeout": {
+ "kind": "integer"
+ },
+ "frontend_connections": {
+ "kind": "integer"
+ },
+ "backend_connections": {
+ "kind": "integer"
+ },
+ "logging_host": {
+ "kind": "string"
+ },
+ "process_name": {
+ "kind": "string"
+ },
+ "pid": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "client_ip": {
+ "kind": "string",
+ "collate": "ipaddress"
+ },
+ "client_port": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "accept_date": {
+ "kind": "string"
+ },
+ "frontend_name": {
+ "kind": "string",
+ "identifier": true
+ },
+ "ssl": {
+ "kind": "string"
+ },
+ "ssl_error": {
+ "kind": "string"
+ },
+ "backend_name": {
+ "kind": "string",
+ "identifier": true
+ },
+ "server_name": {
+ "kind": "string",
+ "identifier": true
+ },
+ "tq": {
+ "kind": "integer"
+ },
+ "tw": {
+ "kind": "integer"
+ },
+ "tc": {
+ "kind": "integer"
+ },
+ "tr": {
+ "kind": "integer"
+ },
+ "tt": {
+ "kind": "integer"
+ },
+ "status_code": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "bytes_read": {
+ "kind": "integer"
+ },
+ "captured_request_cookie": {
+ "kind": "string"
+ },
+ "captured_response_cookie": {
+ "kind": "string"
+ },
+ "termination_state": {
+ "kind": "string"
+ },
+ "actconn": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "feconn": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "beconn": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "srv_conn": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "retries": {
+ "kind": "integer"
+ },
+ "srv_queue": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "backend_queue": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "captured_request_headers": {
+ "kind": "string"
+ },
+ "captured_response_headers": {
+ "kind": "string"
+ },
+ "http_method": {
+ "kind": "string",
+ "identifier": true
+ },
+ "http_url": {
+ "kind": "string"
+ },
+ "http_version": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "Feb 26 10:07:24 192.168.8.2 haproxy[1]: Proxy prod_http_in started."
+ },
+ {
+ "line": "Feb 26 10:00:47 192.168.8.2 haproxy[7]: Stopping frontend prod_http_in in 0 ms."
+ },
+ {
+ "line": "Feb 26 10:00:47 192.168.8.2 haproxy[7]: Proxy prod_http_in stopped (FE: 847876 conns, BE: 0 conns)."
+ },
+ {
+ "line": "Feb 26 23:08:47 192.168.8.2 haproxy[7]: 178.203.144.192:50210 [26/Feb/2019:23:08:47.266] prod_http_in/slsp: Connection closed during SSL handshake"
+ },
+ {
+ "line": "Feb 26 23:16:16 192.168.8.2 haproxy[7]: 178.203.144.192:50210 [26/Feb/2019:23:16:15.321] prod_ssh_in prod_ssh_out/ssh1 1/1/861 1485 -- 2/1/0/0/0 0/0"
+ },
+ {
+ "line": "Feb 26 00:29:44 192.168.8.2 haproxy[7]: 178.203.144.192:50210 [26/Feb/2019:00:29:44.326] prod_http_in~ prod_http_out/nginx1 0/0/1/48/49 200 3313 - - ---- 3/2/0/0/0 0/0 {Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} \"POST /schulportal/?Script=934&lehrer=126537&anm=3235&onlinetest=admin HTTP/1.1\""
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/java_log.json b/src/formats/java_log.json
new file mode 100644
index 0000000..0d6297a
--- /dev/null
+++ b/src/formats/java_log.json
@@ -0,0 +1,147 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "java_log": {
+ "title": "Java log format",
+ "description": "Log format used by log4j and output by most java programs",
+ "url": "",
+ "regex": {
+ "jvm": {
+ "pattern": "^(?<level>\\w+)\\s+\\|\\s+jvm (?<jvm_no>\\d+)\\s+\\|\\s(?<timestamp>\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2})\\s+\\| (?<timestamp_f>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}) \\[(?<function>\\w+-\\d+)\\]\\s+(?<debug_level>\\w+)\\s+(?<class>[\\w.]+)\\s+-\\s+(?<body>.*)"
+ },
+ "dump": {
+ "pattern": "^(?<level>\\w+)\\s+\\|\\s+jvm (?<jvm_no>\\d+)\\s+\\|\\s(?<timestamp>\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2})\\s+\\| JVMDUMP\\w+\\s(?<body>.*)$"
+ },
+ "tasko": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}(,|\\.)\\d{3}Z?)\\s+\\[(?<thread>[\\w\\-\\.]+)\\]\\s+(?<level>ERROR|WARN|INFO|DEBUG)\\s+(?<class>[\\w.]+)\\s+(-\\s+)?(?<body>.*)$"
+ },
+ "prefix-brackets": {
+ "pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}(,|\\.)\\d{3}Z?)\\s+(?<thread>[\\w\\-\\.]+)\\s+(?<level>ERROR|WARN|INFO|DEBUG)\\s+(?<class>[\\w.]+)(?:\\s+opId=(?<opid>[^\\]]*))?\\]\\s*(-\\s+)?(?<body>.*)$"
+ },
+ "in-brackets": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}(,|\\.)\\d{3}Z?)\\s+\\[(?<thread>[\\w\\-\\.]+)(?:\\s+(?:\\[\\]|null))?\\s+(?<level>ERROR|WARN|INFO|DEBUG)\\s+(?<class>[\\w.]+)(?:\\s+opId=(?<opid>[^\\]]*))?\\]\\s*(-\\s+)?(?<body>.*)$"
+ },
+ "nobrackets": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}(,|\\.)\\d{3}Z?)\\s+(?<thread>[\\w\\-\\.]+)\\s+(?<level>ERROR|WARN|INFO|DEBUG)\\s+(?<class>[\\w.]+)\\s+(-\\s+)?(?<body>.*)$"
+ },
+ "vmw1": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}(,|\\.)\\d{3}Z?)\\s*\\|\\s*(?<level>ERROR|WARN|INFO|DEBUG)\\s*\\|\\s*(?<thread>[^\\|]+)\\s*\\|\\s*(?<srcfile>[^\\|]+)\\s*\\|\\s*(?<srcline>\\d+)\\s*\\|\\s*(?<body>.*)$"
+ },
+ "vmw2": {
+ "pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}(,|\\.)\\d{3}Z?)\\]\\s*(?<level>ERROR|WARN|INFO|DEBUG)\\s*\\d+\\[(?<thread>[^\\]]+)\\]\\s+-\\s+(?<class>[^\\(]+)\\.(?<method>\\w+)\\((?<srcfile>[^:]+):(?<srcline>\\d+)\\)\\s+-\\s+(?<body>.*)$"
+ },
+ "vmw3": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}(,|\\.)\\d{3}Z?)\\s*\\|\\s*(?<level>ERROR|WARN|INFO|DEBUG)\\s*\\|\\s*(?<thread>[^\\|]+)\\s*\\|\\s*(?<class>[^\\|]+)\\s*\\|\\s+(?!\\d+\\s*\\|)(?<body>.*)$"
+ },
+ "vmw-sso": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}(,|\\.)\\d{3}Z?)\\s+(?<level>ERROR|WARN|INFO|DEBUG)\\s+[\\w\\-]+\\[\\d+:(?<thread>[^\\]]+)\\]\\s+\\[CorId=(?<corid>[^\\s\\]]*)(?:\\s+OpId=(?<opid>[^\\]]*))?\\]\\s+\\[(?<class>[^\\]]+)\\]\\s+(?<body>.*)$"
+ },
+ "vmw-sps": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}( |T)\\d{2}:\\d{2}:\\d{2}(,|\\.)\\d{3}Z?)\\s+\\[(?<thread>[^\\]]+)\\]\\s+(?<level>ERROR|WARN|INFO|DEBUG)\\s+opId=(?<opid>\\S*)\\s+(?<class>\\S+)\\s+-\\s+(?<body>.*)$"
+ }
+ },
+ "level-field": "level",
+ "opid-field": "opid",
+ "level": {
+ "error": "ERROR",
+ "warning": "WARN",
+ "debug": "DEBUG",
+ "info": "INFO"
+ },
+ "value": {
+ "function": {
+ "kind": "string",
+ "identifier": true
+ },
+ "thread": {
+ "kind": "string",
+ "identifier": true
+ },
+ "level": {
+ "kind": "string"
+ },
+ "jvm_no": {
+ "kind": "integer"
+ },
+ "debug_level": {
+ "kind": "string"
+ },
+ "opid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "corid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "class": {
+ "kind": "string",
+ "identifier": true
+ },
+ "method": {
+ "kind": "string",
+ "identifier": true
+ },
+ "srcfile": {
+ "kind": "string",
+ "identifier": true
+ },
+ "srcline": {
+ "kind": "string",
+ "identifier": true
+ },
+ "body": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "INFO | jvm 1 | 2015/04/28 18:40:00 | 2015-04-28 18:40:00,077 [DefaultQuartzScheduler_Worker-8] INFO com.redhat.rhn.taskomatic.TaskoJob - errata-queue-default: bunch errata-queue-bunch STARTED"
+ },
+ {
+ "line": "INFO | jvm 1 | 2015/04/28 18:34:18 | 2015-04-28 18:34:18,872 [Thread-46] DEBUG com.redhat.rhn.common.hibernate.ConnectionManager - Adding resource com/redhat/rhn/domain/action/ActionArchType.hbm.xml"
+ },
+ {
+ "line": "2015-05-22 16:10:00,123 [DefaultQuartzScheduler_Worker-5] INFO com.redhat.rhn.taskomatic.task.ErrataCacheTask - In the queue: 24"
+ },
+ {
+ "line": "INFO | jvm 1 | 2015/05/24 07:35:50 | JVMDUMP013I Processed dump event \"user\", detail \"\"."
+ },
+ {
+ "line": "2022-06-02T12:12:38.414Z phProdLogDrainerTaskExecutor-5 INFO org.bouncycastle.jsse.provider.ProvTrustManagerFactorySpi Initializing with trust store at path: /usr/java/jre-vmware/lib/security/cacerts"
+ },
+ {
+ "line": "2022-06-02T12:23:11.514Z | INFO | vim-async-1 | VcEventManager.java | 806 | [EventIndex: 2154] Event posted."
+ },
+ {
+ "line": "2022-06-02T12:23:44.971Z [syncaas-grpc-5 INFO com.vmware.hvc.topology.util.LookupServiceUtil opId=] Local Node id is 9c66ff98-3fee-420c-a2bb-dbe2276c1aab"
+ },
+ {
+ "line": "[2022-06-02T10:45:15.969Z tomcat-http--188 ERROR com.vmware.vim.vmomi.server.http.impl.AsyncServlet30Template] Internal server error during asynchronous request processing"
+ },
+ {
+ "line": "[2022-06-01T13:37:36,371] WARN574240[Thread-35] - com.vmware.observability.observer.Observer.execute(Observer.java:384) - No metric configured for observation from source LimitCollectorPlugin.limit"
+ },
+ {
+ "line": "2022-06-02T12:23:44.070Z INFO tokenservice[83:tomcat-http--36] [CorId=95c59584-4472-4f7c-ad9e-f228b94d9b45 OpId=16205349-254c-4f76-a7f1-aa15aae385c5] [com.vmware.vcenter.tokenservice.ExchangeFacadeImpl] Parsed Caller token; tokenType=SAML2"
+ },
+ {
+ "line": "2022-06-01T13:43:59.791Z [main [] INFO com.vmware.vcenter.trustmanagement.service.TrustManagement opId=] trustmanagement-vlsi.xml"
+ },
+ {
+ "line": "2022-06-02T08:34:01.203Z | INFO | state-manager1 | org.eclipse.jetty.server.session | DefaultSessionIdManager workerName=node0"
+ },
+ {
+ "line": "2022-06-02T11:26:26.803Z [pool-26-thread-1] INFO opId=sps-Main-158837-921 com.vmware.vim.storage.common.util.OperationIdUtil - OperationID present in invoker thread, adding suffix and re-using it - sps-Main-158837-921-169186-507."
+ },
+ {
+ "line": "2022-06-02T08:34:19.574Z [main null INFO com.vmware.cis.server.util.PerfLog opId=] Requesting LDAP connection"
+ },
+ {
+ "line": "2022-06-01T13:42:32.739Z INFO sts-perf[23:localhost-startStop-1] [CorId=] [com.vmware.identity.performanceSupport.PerfDataSink] restarting PerfDataSink."
+ },
+ {
+ "line": "2022-06-01T13:42:32.742Z INFO sts-default[23:localhost-startStop-1] [CorId= OpId=] [com.vmware.identity.idm.server.provider.PooledLdapConnectionFactory] New connection created in pool PooledLdapConnectionIdentity [tenantName=null, username=vc.vlcm.com@vsphere.local, authType=SRP, useGCPort=false, connectionString=ldap://vc.vlcm.com:389]"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/journald_json_log.json b/src/formats/journald_json_log.json
new file mode 100644
index 0000000..b6ec0e9
--- /dev/null
+++ b/src/formats/journald_json_log.json
@@ -0,0 +1,84 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "journald_json_log": {
+ "title": "journalctl JSON log format",
+ "description": "Logger format as created by systemd journalctl -o json",
+ "url": "https://www.freedesktop.org/wiki/Software/systemd/json/",
+ "json": true,
+ "hide-extra": true,
+ "convert-to-local-time": true,
+ "line-format": [
+ {
+ "field": "__REALTIME_TIMESTAMP"
+ },
+ " ",
+ {
+ "field": "__MONOTONIC_TIMESTAMP"
+ },
+ " ",
+ {
+ "field": "_SYSTEMD_UNIT"
+ },
+ " ",
+ {
+ "field": "SYSLOG_IDENTIFIER"
+ },
+ "[",
+ {
+ "field": "_PID"
+ },
+ "] ",
+ {
+ "field": "__level__",
+ "text-transform": "uppercase"
+ },
+ " ",
+ {
+ "field": "MESSAGE"
+ }
+ ],
+ "timestamp-field": "__REALTIME_TIMESTAMP",
+ "timestamp-format": [
+ "%6"
+ ],
+ "level-field": "PRIORITY",
+ "level": {
+ "fatal": "0|1",
+ "critical": "2",
+ "error": "3",
+ "warning": "4",
+ "stats": "5",
+ "info": "6",
+ "debug": "7"
+ },
+ "body-field": "MESSAGE",
+ "value": {
+ "__REALTIME_TIMESTAMP": {
+ "kind": "integer"
+ },
+ "__MONOTONIC_TIMESTAMP": {
+ "kind": "integer"
+ },
+ "_SYSTEMD_UNIT": {
+ "kind": "string",
+ "identifier": true
+ },
+ "SYSLOG_IDENTIFIER": {
+ "kind": "string",
+ "identifier": true
+ },
+ "_PID": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "PRIORITY": {
+ "kind": "string",
+ "identifier": true,
+ "foreign-key": true
+ },
+ "MESSAGE": {
+ "kind": "string"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/formats/katello_log.json b/src/formats/katello_log.json
new file mode 100644
index 0000000..750b14a
--- /dev/null
+++ b/src/formats/katello_log.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "katello_log": {
+ "title": "Katello log format",
+ "description": "Log format used by katello and foreman as used in Satellite 6.",
+ "url": "http://theforeman.org/",
+ "regex": {
+ "log": {
+ "pattern": "^\\[\\s?(?<alert_level>\\w+)\\s(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\\s(?<module>\\w+)\\]\\s+(?<message>.*)$"
+ }
+ },
+ "level-field": "alert_level",
+ "level": {
+ "error": "ERROR",
+ "warning": "WARN",
+ "debug": "DEBUG"
+ },
+ "value": {
+ "alert_level": {
+ "kind": "string"
+ },
+ "module": {
+ "kind": "string"
+ },
+ "message": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "[DEBUG 2015-05-20 12:22:19 main] /Stage[main]/Certs::Candlepin/Exec[create candlepin qpid exchange]/unless: Failed: ConnectError: [Errno 1] _ssl.c:504: error:14094418:SSL routines:SSL3_READ_BYTES:tlsv1 alert unknown ca",
+ "level": "debug"
+ },
+ {
+ "line": "[DEBUG 2015-05-20 12:22:19 main] Exec[create candlepin qpid exchange](provider=posix): Executing 'qpid-config --ssl-certificate /etc/pki/katello/certs/java-client.crt --ssl-key /etc/pki/katello/private/java-client.key -b 'amqps://avl248.bcc.qld.gov.au:5671' add exchange topic event --durable'",
+ "level": "debug"
+ },
+ {
+ "line": "[ERROR 2015-05-20 12:22:19 main] qpid-config --ssl-certificate /etc/pki/katello/certs/java-client.crt --ssl-key /etc/pki/katello/private/java-client.key -b 'amqps://avl248.bcc.qld.gov.au:5671' add exchange topic event --durable returned 1 instead of one of [0]",
+ "level": "error"
+ },
+ {
+ "line": "[ INFO 2015-05-20 12:22:19 main] /usr/share/ruby/vendor_ruby/puppet/util/errors.rb:104:in `fail'",
+ "level": "info"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/logfmt/CMakeLists.txt b/src/formats/logfmt/CMakeLists.txt
new file mode 100644
index 0000000..a24abae
--- /dev/null
+++ b/src/formats/logfmt/CMakeLists.txt
@@ -0,0 +1,40 @@
+
+add_library(
+ logfmt
+ STATIC
+ logfmt.parser.hh
+ logfmt.parser.cc
+)
+
+target_include_directories(
+ logfmt
+ PUBLIC
+ .
+ ${CMAKE_BINARY_DIR}/src
+ ${CMAKE_SOURCE_DIR}/src
+)
+target_link_libraries(
+ logfmt
+ PRIVATE
+ cppfmt
+ cppscnlib
+)
+
+add_executable(
+ logfmt.parser.test
+ logfmt.parser.test.cc
+)
+target_include_directories(
+ logfmt.parser.test
+ PUBLIC
+ .
+ ${CMAKE_BINARY_DIR}/src
+ ${CMAKE_SOURCE_DIR}/src
+ ../../third-party/doctest-root)
+target_link_libraries(
+ logfmt.parser.test
+ logfmt
+ base
+)
+
+add_test(NAME logfmt.parser.test COMMAND logfmt.parser.test)
diff --git a/src/formats/logfmt/Makefile.am b/src/formats/logfmt/Makefile.am
new file mode 100644
index 0000000..8d1f3ff
--- /dev/null
+++ b/src/formats/logfmt/Makefile.am
@@ -0,0 +1,41 @@
+
+include $(top_srcdir)/aminclude_static.am
+
+AM_CPPFLAGS = \
+ $(CODE_COVERAGE_CPPFLAGS) \
+ -Wall \
+ -I$(top_srcdir)/src/ \
+ -I$(top_srcdir)/src/third-party \
+ -I$(top_srcdir)/src/fmtlib \
+ -I$(top_srcdir)/src/third-party/scnlib/include \
+ $(LIBARCHIVE_CFLAGS) \
+ $(READLINE_CFLAGS) \
+ $(SQLITE3_CFLAGS) \
+ $(LIBCURL_CPPFLAGS)
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS)
+
+noinst_LIBRARIES = liblogfmt.a
+
+noinst_HEADERS = \
+ logfmt.parser.hh
+
+liblogfmt_a_SOURCES = \
+ logfmt.parser.cc
+
+check_PROGRAMS = \
+ logfmt.parser.test
+
+logfmt_parser_test_SOURCES = \
+ logfmt.parser.test.cc
+
+logfmt_parser_test_LDADD = \
+ liblogfmt.a \
+ $(top_builddir)/src/base/libbase.a \
+ $(top_builddir)/src/pcrepp/libpcrepp.a \
+ $(top_builddir)/src/third-party/scnlib/src/libscnlib.a
+
+TESTS = \
+ logfmt.parser.test
diff --git a/src/formats/logfmt/logfmt.parser.cc b/src/formats/logfmt/logfmt.parser.cc
new file mode 100644
index 0000000..20c7252
--- /dev/null
+++ b/src/formats/logfmt/logfmt.parser.cc
@@ -0,0 +1,266 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file logfmt.parser.cc
+ */
+
+#include "logfmt.parser.hh"
+
+#include "base/intern_string.hh"
+#include "config.h"
+#include "scn/scn.h"
+
+logfmt::parser::parser(string_fragment sf) : p_next_input(sf) {}
+
+static bool
+is_not_eq(char ch)
+{
+ return ch != '=';
+}
+
+struct bare_value_predicate {
+ enum class int_state_t {
+ INIT,
+ NEED_DIGIT,
+ DIGITS,
+ INVALID,
+ };
+
+ enum class float_state_t {
+ INIT,
+ NEED_DIGIT,
+ DIGITS,
+ FRACTION_DIGIT,
+ EXPONENT_INIT,
+ EXPONENT_NEED_DIGIT,
+ EXPONENT_DIGIT,
+ INVALID,
+ };
+
+ int_state_t bvp_int_state{int_state_t::INIT};
+ float_state_t bvp_float_state{float_state_t::INIT};
+ size_t bvp_index{0};
+
+ bool is_integer() const
+ {
+ return this->bvp_int_state == int_state_t::DIGITS;
+ }
+
+ bool is_float() const
+ {
+ switch (this->bvp_float_state) {
+ case float_state_t::DIGITS:
+ case float_state_t::FRACTION_DIGIT:
+ case float_state_t::EXPONENT_DIGIT:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ bool operator()(char ch)
+ {
+ if (ch == ' ') {
+ return false;
+ }
+
+ bool got_digit = isdigit(ch);
+ switch (this->bvp_int_state) {
+ case int_state_t::INIT:
+ if (got_digit) {
+ this->bvp_int_state = int_state_t::DIGITS;
+ } else if (ch == '-') {
+ this->bvp_int_state = int_state_t::NEED_DIGIT;
+ } else {
+ this->bvp_int_state = int_state_t::INVALID;
+ }
+ break;
+ case int_state_t::DIGITS:
+ case int_state_t::NEED_DIGIT:
+ if (got_digit) {
+ this->bvp_int_state = int_state_t::DIGITS;
+ } else {
+ this->bvp_int_state = int_state_t::INVALID;
+ }
+ break;
+ case int_state_t::INVALID:
+ break;
+ }
+
+ switch (this->bvp_float_state) {
+ case float_state_t::INIT:
+ if (got_digit) {
+ this->bvp_float_state = float_state_t::DIGITS;
+ } else if (ch == '-') {
+ this->bvp_float_state = float_state_t::NEED_DIGIT;
+ } else {
+ this->bvp_float_state = float_state_t::INVALID;
+ }
+ break;
+ case float_state_t::DIGITS:
+ case float_state_t::NEED_DIGIT:
+ if (got_digit) {
+ this->bvp_float_state = float_state_t::DIGITS;
+ } else if (ch == '.') {
+ this->bvp_float_state = float_state_t::FRACTION_DIGIT;
+ } else if (ch == 'e' || ch == 'E') {
+ this->bvp_float_state = float_state_t::EXPONENT_INIT;
+ } else {
+ this->bvp_float_state = float_state_t::INVALID;
+ }
+ break;
+ case float_state_t::FRACTION_DIGIT:
+ if (got_digit) {
+ this->bvp_float_state = float_state_t::FRACTION_DIGIT;
+ } else if (ch == 'e' || ch == 'E') {
+ this->bvp_float_state = float_state_t::EXPONENT_INIT;
+ } else {
+ this->bvp_float_state = float_state_t::INVALID;
+ }
+ break;
+ case float_state_t::EXPONENT_INIT:
+ if (got_digit) {
+ this->bvp_float_state = float_state_t::EXPONENT_DIGIT;
+ } else if (ch == '-' || ch == '+') {
+ this->bvp_float_state = float_state_t::EXPONENT_NEED_DIGIT;
+ } else {
+ this->bvp_float_state = float_state_t::INVALID;
+ }
+ break;
+ case float_state_t::EXPONENT_NEED_DIGIT:
+ case float_state_t::EXPONENT_DIGIT:
+ if (got_digit) {
+ this->bvp_float_state = float_state_t::EXPONENT_DIGIT;
+ } else {
+ this->bvp_float_state = float_state_t::INVALID;
+ }
+ break;
+ case float_state_t::INVALID:
+ break;
+ }
+
+ this->bvp_index += 1;
+
+ return true;
+ }
+};
+
+logfmt::parser::step_result
+logfmt::parser::step()
+{
+ const static auto IS_DQ = string_fragment::tag1{'"'};
+
+ auto remaining = this->p_next_input.skip(isspace);
+
+ if (remaining.empty()) {
+ return end_of_input{};
+ }
+
+ auto pair_opt = remaining.split_while(is_not_eq);
+
+ if (!pair_opt) {
+ return error{remaining.sf_begin, "expecting key followed by '='"};
+ }
+
+ auto key_frag = pair_opt->first;
+ auto after_eq = pair_opt->second.consume(string_fragment::tag1{'='});
+
+ if (!after_eq) {
+ return error{pair_opt->second.sf_begin, "expecting '='"};
+ }
+
+ auto value_start = after_eq.value();
+
+ if (value_start.startswith("\"")) {
+ string_fragment::quoted_string_body qsb;
+ auto quoted_pair = value_start.consume_n(1)->split_while(qsb);
+
+ if (!quoted_pair) {
+ return error{value_start.sf_begin + 1, "string body missing"};
+ }
+
+ auto after_quote = quoted_pair->second.consume(IS_DQ);
+
+ if (!after_quote) {
+ return error{quoted_pair->second.sf_begin, "non-terminated string"};
+ }
+
+ this->p_next_input = after_quote.value();
+ return std::make_pair(
+ key_frag,
+ quoted_value{string_fragment{quoted_pair->first.sf_string,
+ quoted_pair->first.sf_begin - 1,
+ quoted_pair->first.sf_end + 1}});
+ }
+
+ bare_value_predicate bvp;
+ auto value_pair = value_start.split_while(bvp);
+
+ if (value_pair) {
+ static const auto TRUE_FRAG = string_fragment::from_const("true");
+ static const auto FALSE_FRAG = string_fragment::from_const("false");
+
+ this->p_next_input = value_pair->second;
+ if (bvp.is_integer()) {
+ int_value retval;
+
+ auto int_scan_res
+ = scn::scan_value<int64_t>(value_pair->first.to_string_view());
+ if (int_scan_res) {
+ retval.iv_value = int_scan_res.value();
+ }
+ retval.iv_str_value = value_pair->first;
+
+ return std::make_pair(key_frag, retval);
+ }
+ if (bvp.is_float()) {
+ float_value retval;
+
+ auto float_scan_res
+ = scn::scan_value<double>(value_pair->first.to_string_view());
+ if (float_scan_res) {
+ retval.fv_value = float_scan_res.value();
+ }
+ retval.fv_str_value = value_pair->first;
+
+ return std::make_pair(key_frag, retval);
+ }
+ if (value_pair->first.iequal(TRUE_FRAG)) {
+ return std::make_pair(key_frag,
+ bool_value{true, value_pair->first});
+ }
+ if (value_pair->first.iequal(FALSE_FRAG)) {
+ return std::make_pair(key_frag,
+ bool_value{false, value_pair->first});
+ }
+ return std::make_pair(key_frag, unquoted_value{value_pair->first});
+ }
+
+ this->p_next_input = value_start;
+ return std::make_pair(key_frag, unquoted_value{string_fragment{}});
+}
diff --git a/src/formats/logfmt/logfmt.parser.hh b/src/formats/logfmt/logfmt.parser.hh
new file mode 100644
index 0000000..7806001
--- /dev/null
+++ b/src/formats/logfmt/logfmt.parser.hh
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file logfmt.parser.hh
+ */
+
+#ifndef lnav_logfmt_parser_hh
+#define lnav_logfmt_parser_hh
+
+#include "base/intern_string.hh"
+#include "base/result.h"
+#include "mapbox/variant.hpp"
+
+namespace logfmt {
+
+class parser {
+public:
+ explicit parser(string_fragment sf);
+
+ struct end_of_input {};
+ struct error {
+ int e_offset;
+ const std::string e_msg;
+ };
+ struct unquoted_value {
+ string_fragment uv_value;
+ };
+ struct quoted_value {
+ string_fragment qv_value;
+ };
+ struct bool_value {
+ bool bv_value{false};
+ string_fragment bv_str_value;
+ };
+ struct int_value {
+ int64_t iv_value{0};
+ string_fragment iv_str_value;
+ };
+ struct float_value {
+ double fv_value{0};
+ string_fragment fv_str_value;
+ };
+ using value_type = mapbox::util::variant<
+ bool_value,
+ int_value,
+ float_value,
+ unquoted_value,
+ quoted_value
+ >;
+
+ using kvpair = std::pair<string_fragment, value_type>;
+
+ using step_result = mapbox::util::variant<
+ end_of_input,
+ kvpair,
+ error
+ >;
+
+ step_result step();
+private:
+ string_fragment p_next_input;
+};
+
+}
+
+#endif
diff --git a/src/formats/logfmt/logfmt.parser.test.cc b/src/formats/logfmt/logfmt.parser.test.cc
new file mode 100644
index 0000000..2193bfe
--- /dev/null
+++ b/src/formats/logfmt/logfmt.parser.test.cc
@@ -0,0 +1,221 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file logfmt.parser.test.cc
+ */
+
+#include "config.h"
+
+#include <iostream>
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "doctest/doctest.h"
+
+#include "logfmt.parser.hh"
+
+TEST_CASE("basic")
+{
+ static const char *line = "abc=def ghi=\"1 2 3 4\" time=333 empty1= tf=true empty2=";
+
+ auto p = logfmt::parser{string_fragment{line}};
+
+ auto pair1 = p.step();
+
+ CHECK(pair1.is<logfmt::parser::kvpair>());
+ CHECK(pair1.get<logfmt::parser::kvpair>().first == "abc");
+ CHECK(pair1.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::unquoted_value>().uv_value == "def");
+
+ auto pair2 = p.step();
+
+ CHECK(pair2.is<logfmt::parser::kvpair>());
+ CHECK(pair2.get<logfmt::parser::kvpair>().first == "ghi");
+ CHECK(pair2.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::quoted_value>().qv_value == "\"1 2 3 4\"");
+
+ auto pair3 = p.step();
+
+ CHECK(pair3.is<logfmt::parser::kvpair>());
+ CHECK(pair3.get<logfmt::parser::kvpair>().first == "time");
+ CHECK(pair3.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::int_value>().iv_value == 333);
+
+ auto pair4 = p.step();
+
+ CHECK(pair4.is<logfmt::parser::kvpair>());
+ CHECK(pair4.get<logfmt::parser::kvpair>().first == "empty1");
+ CHECK(pair4.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::unquoted_value>().uv_value == "");
+
+ auto pair5 = p.step();
+
+ CHECK(pair5.is<logfmt::parser::kvpair>());
+ CHECK(pair5.get<logfmt::parser::kvpair>().first == "tf");
+ CHECK(pair5.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::bool_value>().bv_value);
+
+ auto pair6 = p.step();
+
+ CHECK(pair6.is<logfmt::parser::kvpair>());
+ CHECK(pair6.get<logfmt::parser::kvpair>().first == "empty2");
+ CHECK(pair6.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::unquoted_value>().uv_value == "");
+
+ auto eoi = p.step();
+ CHECK(eoi.is<logfmt::parser::end_of_input>());
+}
+
+TEST_CASE("floats")
+{
+ static const char *line = "f1=1.0 f2=-2.0 f3=1.2e3 f4=1.2e-2 f5=2e1 f6=2e+1";
+
+ auto p = logfmt::parser{string_fragment{line}};
+
+ auto pair1 = p.step();
+
+ CHECK(pair1.is<logfmt::parser::kvpair>());
+ CHECK(pair1.get<logfmt::parser::kvpair>().first == "f1");
+ CHECK(pair1.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::float_value>().fv_value == 1.0);
+
+ auto pair2 = p.step();
+
+ CHECK(pair2.is<logfmt::parser::kvpair>());
+ CHECK(pair2.get<logfmt::parser::kvpair>().first == "f2");
+ CHECK(pair2.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::float_value>().fv_value == -2.0);
+
+ auto pair3 = p.step();
+
+ CHECK(pair3.is<logfmt::parser::kvpair>());
+ CHECK(pair3.get<logfmt::parser::kvpair>().first == "f3");
+ CHECK(pair3.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::float_value>().fv_value == 1200);
+
+ auto pair4 = p.step();
+
+ CHECK(pair4.is<logfmt::parser::kvpair>());
+ CHECK(pair4.get<logfmt::parser::kvpair>().first == "f4");
+ CHECK(pair4.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::float_value>().fv_value == 0.012);
+
+ auto pair5 = p.step();
+
+ CHECK(pair5.is<logfmt::parser::kvpair>());
+ CHECK(pair5.get<logfmt::parser::kvpair>().first == "f5");
+ CHECK(pair5.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::float_value>().fv_value == 20);
+
+ auto pair6 = p.step();
+
+ CHECK(pair6.is<logfmt::parser::kvpair>());
+ CHECK(pair6.get<logfmt::parser::kvpair>().first == "f6");
+ CHECK(pair6.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::float_value>().fv_value == 20);
+}
+
+TEST_CASE("bad floats")
+{
+ static const char *line = "bf1=- bf2=-1.2e bf3=1.2.3 bf4=1e2e4";
+
+ auto p = logfmt::parser{string_fragment{line}};
+
+ auto pair1 = p.step();
+
+ CHECK(pair1.is<logfmt::parser::kvpair>());
+ CHECK(pair1.get<logfmt::parser::kvpair>().first == "bf1");
+ CHECK(pair1.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::unquoted_value>().uv_value == "-");
+
+ auto pair2 = p.step();
+
+ CHECK(pair2.is<logfmt::parser::kvpair>());
+ CHECK(pair2.get<logfmt::parser::kvpair>().first == "bf2");
+ CHECK(pair2.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::unquoted_value>().uv_value == "-1.2e");
+
+ auto pair3 = p.step();
+
+ CHECK(pair3.is<logfmt::parser::kvpair>());
+ CHECK(pair3.get<logfmt::parser::kvpair>().first == "bf3");
+ CHECK(pair3.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::unquoted_value>().uv_value == "1.2.3");
+
+ auto pair4 = p.step();
+
+ CHECK(pair4.is<logfmt::parser::kvpair>());
+ CHECK(pair4.get<logfmt::parser::kvpair>().first == "bf4");
+ CHECK(pair4.get<logfmt::parser::kvpair>().second
+ .get<logfmt::parser::unquoted_value>().uv_value == "1e2e4");
+}
+
+TEST_CASE("non-terminated string")
+{
+ static const char *line = "abc=\"12 2";
+
+ auto p = logfmt::parser{string_fragment{line}};
+ auto pair1 = p.step();
+
+ CHECK(pair1.is<logfmt::parser::error>());
+ CHECK(pair1.get<logfmt::parser::error>().e_offset == 9);
+ CHECK(pair1.get<logfmt::parser::error>().e_msg == "non-terminated string");
+}
+
+TEST_CASE("missing equals")
+{
+ static const char *line = "abc";
+
+ auto p = logfmt::parser{string_fragment{line}};
+ auto pair1 = p.step();
+
+ CHECK(pair1.is<logfmt::parser::error>());
+ CHECK(pair1.get<logfmt::parser::error>().e_offset == 3);
+ CHECK(pair1.get<logfmt::parser::error>().e_msg == "expecting '='");
+}
+
+TEST_CASE("missing key")
+{
+ static const char *line = "=def";
+
+ auto p = logfmt::parser{string_fragment{line}};
+ auto pair1 = p.step();
+
+ CHECK(pair1.is<logfmt::parser::error>());
+ CHECK(pair1.get<logfmt::parser::error>().e_offset == 0);
+ CHECK(pair1.get<logfmt::parser::error>().e_msg == "expecting key followed by '='");
+}
+
+TEST_CASE("empty")
+{
+ static const char *line = "";
+
+ auto p = logfmt::parser{string_fragment{line}};
+ auto pair1 = p.step();
+
+ CHECK(pair1.is<logfmt::parser::end_of_input>());
+}
diff --git a/src/formats/openam_log.json b/src/formats/openam_log.json
new file mode 100644
index 0000000..c1a8090
--- /dev/null
+++ b/src/formats/openam_log.json
@@ -0,0 +1,73 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "openam_log": {
+ "title": "OpenAM Log",
+ "description": "The OpenAM identity provider.",
+ "url": "http://openam.forgerock.org",
+ "level-field": "level",
+ "level": {
+ "error": "ERROR",
+ "warning": "WARNING",
+ "info": "INFO",
+ "critical": "SEVERE",
+ "trace": "FINE|FINEST"
+ },
+ "multiline": false,
+ "regex": {
+ "std": {
+ "pattern": "^\"(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2})\"\\s+(?<data>[^ \"]+|\"(?:[^\"]*|\"\")*\")\\s+(?<loginid>[^ \"]+|\"(?:[^\"]*|\"\")*\")\\s+(?<contextid>[^ \"]+|\"(?:[^\"]|\"\")*\")\\s+(?<ipaddr>[^ \"]+|\"(?:[^\"]|\"\")*\")\\s+(?<level>[^ \"]+|\"(?:[^\"]|\"\")*\")\\s+(?<domain>[^ \"]+|\"(?:[^\"]|\"\")*\")\\s+(?<loggedby>[^ \"]+|\"(?:[^\"]|\"\")*\")\\s+(?<messageid>[^ \"]+|\"(?:[^\"]|\"\")*\")\\s+(?<modulename>[^ \"]+|\"(?:[^\"]|\"\")*\")\\s+(?<nameid>[^ \"]+|\"(?:[^\"]|\"\")*\")\\s+(?<hostname>[^ \"]+|\"(?:[^\"]|\"\")*\")(?<body>.*)$"
+ }
+ },
+ "value": {
+ "data": {
+ "kind": "quoted"
+ },
+ "loginid": {
+ "kind": "quoted",
+ "identifier": true
+ },
+ "contextid": {
+ "kind": "quoted",
+ "identifier": true
+ },
+ "ipaddr": {
+ "kind": "quoted",
+ "identifier": true,
+ "collate": "ipaddress"
+ },
+ "domain": {
+ "kind": "quoted",
+ "identifier": true
+ },
+ "loggedby": {
+ "kind": "quoted",
+ "identifier": true
+ },
+ "messageid": {
+ "kind": "quoted",
+ "identifier": true
+ },
+ "modulename": {
+ "kind": "quoted",
+ "identifier": true
+ },
+ "nameid": {
+ "kind": "quoted",
+ "identifier": true
+ },
+ "hostname": {
+ "kind": "quoted",
+ "identifier": true,
+ "collate": "ipaddress"
+ }
+ },
+ "sample": [
+ {
+ "line": "\"2014-06-14 17:08:39\" \"http://localhost:8086|/|<samlp:AuthnRequest ID=\"\"139a40bba4d340108d91022750c2a3a8\"\" Version=\"\"2.0\"\" IssueInstant=\"\"2014-06-14T17:09:04Z\"\" ProtocolBinding=\"\"urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST\"\" AssertionConsumerServiceURL=\"\"http://localhost:8086/api/1/rest/admin/org/530e42ccd6f45fd16d0d0717/saml/consume\"\">\\n<saml:Issuer>http://localhost:8086</saml:Issuer>\\n<samlp:NameIDPolicy Format=\"\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\"\" AllowCreate=\"\"true\"\"></samlp:NameIDPolicy>\\n<samlp:RequestedAuthnContext Comparison=\"\"exact\"\"><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></samlp:RequestedAuthnContext>\\n</samlp:AuthnRequest>\" \"cn=dsameuser,ou=DSAME Users,dc=openam\" 8fc43a8f6a8c14101 \"Not Available\" INFO dc=openam \"cn=dsameuser,ou=DSAME Users,dc=openam\" SAML2-36 SAML2.access \"Not Available\" 127.0.1.1"
+ },
+ {
+ "line": "\"2014-06-09 14:49:56\" /etc/openam/openam/log/ \"cn=dsameuser,ou=DSAME Users,dc=openam\" 3d956febb91fed31 \"Not Available\" INFO dc=openam \"cn=dsameuser,ou=DSAME Users,dc=openam\" LOG-1 amPolicy.access \"Not Available\" 127.0.1.1"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/openamdb_log.json b/src/formats/openamdb_log.json
new file mode 100644
index 0000000..1d5ea37
--- /dev/null
+++ b/src/formats/openamdb_log.json
@@ -0,0 +1,21 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "openamdb_log": {
+ "title": "OpenAM Debug Log",
+ "description": "Debug logs for the OpenAM identity provider.",
+ "url": "http://openam.forgerock.org",
+ "regex": {
+ "std": {
+ "pattern": "^(?<module>[\\w]+):(?<timestamp>\\d{2}/\\d{2}/\\d{4} \\d{2}:\\d{2}:\\d{2}:\\d{3} [AP]M \\w+): Thread\\[(?<thread>[^,]+,\\d+,[^,]+)\\]\\n?(?:\\*+|(?<body>.*))$"
+ }
+ },
+ "sample": [
+ {
+ "line": "amMonitoring:06/09/2014 02:49:59:447 PM UTC: Thread[http-80-1,5,main]\n**********************************************"
+ },
+ {
+ "line": "amLog:06/09/2014 04:08:22:515 PM UTC: Thread[http-80-8,5,main]\nERROR: LogMessageProviderBase.createLogRecord: unable to locate message ID object for ATTEMPT_GET_METAALIAS"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/openstack_log.json b/src/formats/openstack_log.json
new file mode 100644
index 0000000..4dc280f
--- /dev/null
+++ b/src/formats/openstack_log.json
@@ -0,0 +1,65 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "openstack_log": {
+ "title": "OpenStack log format",
+ "description": "The log format for the OpenStack log files",
+ "url": "http://docs.openstack.org/openstack-ops/content/logging_monitoring.html",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}.\\d{3}) (?<pid>\\d+) (?<level>\\w+) (?<logger>\\S+) \\[(?<tid>[^\\]]+)\\] (?<body>.*)"
+ },
+ "mod-std": {
+ "module-format": true,
+ "pattern": "^(?<level>\\w+) (?<logger>\\S+) \\[(?<tid>[^\\]]+)\\] (?<body>.*)"
+ },
+ "keystone": {
+ "pattern": "^[(](?<logger>[^)]+)[)]: (?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}) (?<level>\\w+) (?!\\()(?<body>.*)"
+ },
+ "keystone-debug": {
+ "pattern": "^[(](?<logger>[^)]+)[)]: (?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}) (?<level>\\w+) [(](?<user>[^)]+)[)] (?<body>.*)"
+ }
+ },
+ "timestamp-format": [
+ "%Y-%m-%d %H:%M:%S.%L",
+ "%Y-%m-%d %H:%M:%S,%L"
+ ],
+ "level-field": "level",
+ "level": {
+ "critical": "CRITICAL",
+ "error": "ERROR",
+ "info": "INFO",
+ "warning": "WARNING",
+ "trace": "TRACE",
+ "debug": "DEBUG"
+ },
+ "value": {
+ "tid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "pid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "logger": {
+ "kind": "string",
+ "identifier": true
+ },
+ "user": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "2014-10-28 10:42:22.772 23623 INFO neutron.wsgi [req-40743023-00ed-441c-9d0a-19b8167ea0ad None] 10.1.255.252 - - [28/Oct/2014 10:42:22] GET /v2.0/floatingips.json?fixed_ip_address=80.0.0.9&port_id=b4291e0e-a941-4663-9379-7af6471e983f HTTP/1.1 200 208 0.008971"
+ },
+ {
+ "line": "(sqlalchemy.orm.mapper.Mapper): 2021-04-27 06:25:32,122 INFO (User|user) Identified primary key columns: ColumnSet([Column('id', String(length=64), table=<user>, primary_key=True, nullable=False)])"
+ },
+ {
+ "line": "(sqlalchemy.pool.QueuePool): 2021-04-28 16:37:00,355 DEBUG Connection <pymysql.connections.Connection object at 0x7fd88717d0d0> being returned to pool"
+ }
+ ]
+ }
+}
diff --git a/src/formats/page_log.json b/src/formats/page_log.json
new file mode 100644
index 0000000..0758b13
--- /dev/null
+++ b/src/formats/page_log.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "page_log": {
+ "title": "CUPS Page Log",
+ "description": "The CUPS server log of printed pages.",
+ "url": "http://www.cups.org/documentation.php/doc-1.7/ref-page_log.html",
+ "multiline": false,
+ "regex": {
+ "pre-1.7": {
+ "pattern": "^(?<printer>[\\w_\\-\\.]+) (?<username>[\\w\\.\\-]+) (?<job_id>\\d+) \\[(?<timestamp>[^\\]]+)\\] (?<page_number>total|\\d+) (?<num_copies>\\d+) (?<job_billing>[^ ]+) (?<job_originating_hostname>[\\w\\.:\\-]+)$"
+ },
+ "1.7": {
+ "pattern": "^(?<printer>[\\w_\\-\\.]+) (?<username>[\\w\\.\\-]+) (?<job_id>\\d+) \\[(?<timestamp>[^\\]]+)\\] (?<page_number>total|\\d+) (?<num_copies>\\d+) (?<job_billing>[^ ]+) (?<job_originating_hostname>[\\w\\.:\\-]+) (?<job_name>.+) (?<media>[^ ]+) (?<sides>.+)(?<body>.*)$"
+ }
+ },
+ "value": {
+ "printer": {
+ "kind": "string",
+ "identifier": true
+ },
+ "username": {
+ "kind": "string",
+ "identifier": true
+ },
+ "job_id": {
+ "kind": "integer",
+ "identifier": true,
+ "foreign-key": true
+ },
+ "page_number": {
+ "kind": "string"
+ },
+ "num_copies": {
+ "kind": "integer"
+ },
+ "job_billing": {
+ "kind": "string",
+ "identifier": true
+ },
+ "job_originating_hostname": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "job_name": {
+ "kind": "string",
+ "identifier": true
+ },
+ "media": {
+ "kind": "string",
+ "identifier": true
+ },
+ "sides": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "Photosmart_7520_series stack 11 [18/May/2013:13:21:15 -0700] total 0 - localhost 5615311548-159003235-tickets.pdf Letter one-sided"
+ },
+ {
+ "line": "tec_IS2027 kurt 401 [22/Apr/2003:10:28:43 +0100] 1 3 #marketing 10.160.50.13"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/papertrail_log.json b/src/formats/papertrail_log.json
new file mode 100644
index 0000000..b1a9d87
--- /dev/null
+++ b/src/formats/papertrail_log.json
@@ -0,0 +1,52 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "papertrail_log": {
+ "title": "Papertrail Service",
+ "url": "https://papertrailapp.com/",
+ "description": "Log format for the papertrail log management service",
+ "json": true,
+ "hide-extra": true,
+ "file-pattern": "pt:.*",
+ "line-format": [
+ {
+ "field": "display_received_at"
+ },
+ " ",
+ {
+ "field": "hostname"
+ },
+ " ",
+ {
+ "field": "program"
+ },
+ ": ",
+ {
+ "field": "message"
+ }
+ ],
+ "level-field": "severity",
+ "level": {
+ "error": "Error",
+ "debug": "Debug",
+ "warning": "Warning",
+ "info": "Info(?:rmational)?|Notice",
+ "critical": "Crit(?:ical)?",
+ "fatal": "Emerg(?:ency)?|Alert"
+ },
+ "timestamp-field": "generated_at",
+ "body-field": "message",
+ "value": {
+ "display_received_at": {
+ "kind": "string"
+ },
+ "program": {
+ "kind": "string",
+ "identifier": true
+ },
+ "hostname": {
+ "kind": "string",
+ "identifier": true
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/formats/pcap_log.json b/src/formats/pcap_log.json
new file mode 100644
index 0000000..8ae73e2
--- /dev/null
+++ b/src/formats/pcap_log.json
@@ -0,0 +1,82 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "pcap_log": {
+ "json": true,
+ "title": "Packet Capture",
+ "description": "Internal format for pcap files",
+ "mime-types": [
+ "application/vnd.tcpdump.pcap"
+ ],
+ "multiline": false,
+ "convert-to-local-time": true,
+ "line-format": [
+ {
+ "field": "time"
+ },
+ " ",
+ {
+ "field": "source",
+ "auto-width": true,
+ "align": "right"
+ },
+ " → ",
+ {
+ "field": "destination",
+ "auto-width": true,
+ "align": "left"
+ },
+ " ",
+ {
+ "field": "protocol",
+ "auto-width": true,
+ "align": "left"
+ },
+ " ",
+ {
+ "field": "length",
+ "auto-width": true,
+ "align": "right"
+ },
+ " ",
+ {
+ "field": "info"
+ }
+ ],
+ "level": {
+ "warning": "^6291456$",
+ "error": "^8388608$"
+ },
+ "timestamp-field": "time",
+ "level-pointer": "/_ws_expert__ws_expert_severity$",
+ "body-field": "info",
+ "hide-extra": true,
+ "value": {
+ "source": {
+ "kind": "string",
+ "foreign-key": true,
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "destination": {
+ "kind": "string",
+ "foreign-key": true,
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "protocol": {
+ "kind": "string",
+ "identifier": true
+ },
+ "length": {
+ "kind": "integer"
+ },
+ "info": {
+ "kind": "string"
+ },
+ "layers": {
+ "kind": "json",
+ "hidden": true
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/formats/procstate_log.json b/src/formats/procstate_log.json
new file mode 100644
index 0000000..74332dd
--- /dev/null
+++ b/src/formats/procstate_log.json
@@ -0,0 +1,22 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "procstate_log": {
+ "title": "Process State",
+ "description": "Periodic dumps of process state",
+ "regex": {
+ "std": {
+ "pattern": "========== Start of system state dump at (?<timestamp>[^=]+)==========(?<body>.*)"
+ }
+ },
+ "sample": [
+ {
+ "line": "========== Start of system state dump at Thu Jun 2 00:01:01 UTC 2022 =========="
+ }
+ ],
+ "search-table": {
+ "procstate_procs": {
+ "pattern": "^(?<user>\\S+)\\s+(?<pid>\\d+)\\s+(?<cpu_pct>\\d+(?:\\.\\d+)?)\\s+(?<mem_pct>\\d+(?:\\.\\d+)?)\\s+(?<vsz>\\d+)\\s+(?<rss>\\d+)\\s(?<tty>\\S+)\\s+(?<stat>\\S+)\\s+(?<start_time>\\S+)\\s+(?<cpu_time>\\S+)\\s+(?<cmd>(?<cmd_name>[^ \\n]+)(?: (?<cmd_args>[^\\n]+))?)$"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/formats/s3_log.json b/src/formats/s3_log.json
new file mode 100644
index 0000000..1472f87
--- /dev/null
+++ b/src/formats/s3_log.json
@@ -0,0 +1,158 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "s3_log": {
+ "title": "S3 Access Log",
+ "description": "S3 server access log format",
+ "url": "https://docs.aws.amazon.com/AmazonS3/latest/dev/LogFormat.html",
+ "multiline": false,
+ "regex": {
+ "std": {
+ "pattern": "^(?<owner>\\S+)\\s+(?<bucket>\\S+)\\s+\\[(?<timestamp>[^\\]]+)\\]\\s+(?<c_ip>[\\w*.:-]+)\\s+(?<cs_userid>\\S+)\\s+(?<req_id>\\S+)\\s+(?<op>\\S+)\\s+(?<cs_key>\\S+)\\s+\"(?<cs_method>\\S+)\\s+(?<cs_uri_stem>[^ \\?]+)(?:\\?(?<cs_uri_query>[^ ]*))?\\s+(?<cs_version>\\S+)\"\\s+(?<sc_status>\\d+|-)\\s+(?<sc_error_code>\\S+)\\s+(?<sc_bytes>\\d+|-)\\s+(?<obj_size>\\d+|-)\\s+(?<total_time>\\d+|-)\\s+(?<turn_around_time>\\d+|-)\\s+\"(?<cs_referer>.*?)\"\\s+\"(?<cs_user_agent>.*?)\"$"
+ },
+ "std-v2": {
+ "pattern": "^(?<owner>\\S+)\\s+(?<bucket>\\S+)\\s+\\[(?<timestamp>[^\\]]+)\\]\\s+(?<c_ip>[\\w*.:-]+)\\s+(?<cs_userid>\\S+)\\s+(?<req_id>\\S+)\\s+(?<op>\\S+)\\s+(?<cs_key>\\S+)\\s+\"(?<cs_method>\\S+)\\s+(?<cs_uri_stem>[^ \\?]+)(?:\\?(?<cs_uri_query>[^ ]*))?\\s+(?<cs_version>\\S+)\"\\s+(?<sc_status>\\d+|-)\\s+(?<sc_error_code>\\S+)\\s+(?<sc_bytes>\\d+|-)\\s+(?<obj_size>\\d+|-)\\s+(?<total_time>\\d+|-)\\s+(?<turn_around_time>\\d+|-)\\s+\"(?<cs_referer>.*?)\"\\s+\"(?<cs_user_agent>.*?)\"\\s+(?<version_id>\\S+)\\s+(?<host_id>\\S+)\\s+(?<sig_version>\\S+)\\s+(?<cipher_suite>\\S+)\\s+(?<auth_type>\\S+)\\s+(?<cs_host>\\S+)\\s+(?<tls_version>\\S+)$"
+ }
+ },
+ "level-field": "sc_status",
+ "level": {
+ "error": "^[^123].*"
+ },
+ "opid-field": "c_ip",
+ "value": {
+ "owner": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The bucket owner"
+ },
+ "bucket": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The bucket"
+ },
+ "c_ip": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true,
+ "description": "The client IP address"
+ },
+ "cs_userid": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The user ID passed from the client to the server"
+ },
+ "req_id": {
+ "kind": "string",
+ "description": "The request ID"
+ },
+ "op": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The operation"
+ },
+ "cs_key": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The key for the bucket"
+ },
+ "cs_method": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The request method"
+ },
+ "cs_uri_stem": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The path part of the request URI"
+ },
+ "cs_uri_query": {
+ "kind": "string",
+ "description": "The query parameters in the request URI"
+ },
+ "cs_version": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The client's HTTP version"
+ },
+ "sc_status": {
+ "kind": "integer",
+ "foreign-key": true,
+ "rewriter": ";SELECT :sc_status || ' (' || (SELECT message FROM http_status_codes WHERE status = :sc_status) || ') '",
+ "description": "The status code returned by the server"
+ },
+ "sc_error_code": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The Amazon S3 error code"
+ },
+ "sc_bytes": {
+ "kind": "integer",
+ "description": "The number of bytes returned by the server"
+ },
+ "obj_size": {
+ "kind": "integer",
+ "description": "The size of the object"
+ },
+ "total_time": {
+ "kind": "integer",
+ "description": "The total time taken to satisfy the request"
+ },
+ "turn_around_time": {
+ "kind": "integer",
+ "description": "The turn around time"
+ },
+ "cs_referer": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The client's referrer"
+ },
+ "cs_user_agent": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The client's HTTP agent"
+ },
+ "version_id": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The version ID"
+ },
+ "host_id": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The host ID"
+ },
+ "sig_version": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The signature version"
+ },
+ "cipher_suite": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The SSL layer negotiated cipher suite"
+ },
+ "auth_type": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The type of request authentication used"
+ },
+ "cs_host": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The endpoint used to connect to S3"
+ },
+ "tls_version": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The TLS version negotiated by the client"
+ }
+ },
+ "sample": [
+ {
+ "line": "b659b576cff1e15e4c0313ff8930fba9f53e6794567f5c60dab3abf2f8dfb6cc www.example.com [10/Feb/2012:16:42:07 -0500] 1.2.3.4 arn:aws:iam::179580289999:user/phillip.boss EB3502676500C6BE WEBSITE.GET.OBJECT index \"GET /index HTTP/1.1\" 200 - 368 368 10 9 \"-\" \"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/535.11 (KHTML, like Gecko) Chrome/17.0.963.56 Safari/535.11\""
+ },
+ {
+ "line": "79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be awsexamplebucket1 [06/Feb/2019:00:00:38 +0000] 192.0.2.3 79a59df900b949e55d96a1e698fbacedfd6e09d98eacf8f8d5218e7cd47ef2be 3E57427F3EXAMPLE REST.GET.VERSIONING - \"GET /awsexamplebucket1?versioning HTTP/1.1\" 200 - 113 - 7 - \"-\" \"S3Console/0.4\" - s9lzHYrFp76ZVxRcpX9+5cjAnEH2ROuNkd2BHfIa6UkFVdtjf5mKR3/eTPFvsiP/XV/VLi31234= SigV2 ECDHE-RSA-AES128-GCM-SHA256 AuthHeader awsexamplebucket1.s3.us-west-1.amazonaws.com TLSV1.1"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/snaplogic_log.json b/src/formats/snaplogic_log.json
new file mode 100644
index 0000000..db49c69
--- /dev/null
+++ b/src/formats/snaplogic_log.json
@@ -0,0 +1,55 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "snaplogic_log": {
+ "title": "SnapLogic Server Log",
+ "description": "The SnapLogic server log format.",
+ "url": "http://www.snaplogic.com/docs/user-guide/user-guide.htm",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?) (?:(?:(?<level>\\w{4,}) (?<logger>[^ ]+) (?<facility>[^ ]+) (?<msgid>[^ ]+) (?<pipe_rid>-|\\d+)(?:\\.(?<comp_rid>[^ ]+))? (?<resource_name>[^ ]+) (?<invoker>[^ ]+))|(?:(?:stdout|stderr): ))(?<body>.*)"
+ }
+ },
+ "level-field": "level",
+ "level": {
+ "error": "ERROR",
+ "debug": "DEBUG",
+ "info": "INFO",
+ "warning": "WARNING"
+ },
+ "value": {
+ "logger": {
+ "kind": "string",
+ "identifier": true
+ },
+ "facility": {
+ "kind": "string",
+ "identifier": true
+ },
+ "msgid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "pipe_rid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "comp_rid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "resource_name": {
+ "kind": "string",
+ "identifier": true
+ },
+ "invoker": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "2013-07-30T09:40:25 DEBUG main_process.main PM - 1768839331504132353247612213662950165988626018 - - Pipeline manager '' sending to Leads. Invoker 'admin': PREPARE {'parent_rid': '1768839331504132353247612213662950165988626018', 'resource_name': u'Leads', 'input_views': {}, 'parameters': {u'DELIMITER': u',', u'INPUTFILE': u'file://tutorial/data/leads.csv'}, 'output_views': {u'Output1': {'method': 'GET'}}, 'context_name': u'', 'snap_control_version': '1.2'}"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/sssd_log.json b/src/formats/sssd_log.json
new file mode 100644
index 0000000..04f3a77
--- /dev/null
+++ b/src/formats/sssd_log.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "sssd_log": {
+ "title": "SSSD log format",
+ "description": "Log format used by the System Security Services Daemon",
+ "url": "http://fedorahosted.org/sssd",
+ "regex": {
+ "core": {
+ "pattern": "^\\((?<timestamp>\\S{3,8} \\S{3,8} ( \\d|\\d{2}) \\d{2}:\\d{2}:\\d{2} \\d{4})\\) \\[sssd\\] \\[(?<function>\\w+)\\] \\((?<debug_level>0x[0-9a-fA-F]{4})\\): (?<body>.*)$"
+ },
+ "module": {
+ "pattern": "^\\((?<timestamp>\\S{3,8} \\S{3,8} ( \\d|\\d{2}) \\d{2}:\\d{2}:\\d{2} \\d{4})\\) \\[sssd(?<module>\\[.*?\\])\\] \\[(?<function>\\w+)\\] \\((?<debug_level>0x[0-9a-fA-F]{4})\\): (?<body>.*)$"
+ }
+ },
+ "value": {
+ "module": {
+ "kind": "string"
+ },
+ "function": {
+ "kind": "string"
+ },
+ "debug_level": {
+ "kind": "string"
+ },
+ "body": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "(Tue Mar 31 06:03:46 2015) [sssd[be[default]]] [sysdb_search_by_name] (0x0400): No such entry"
+ },
+ {
+ "line": "(Tue Mar 31 05:58:38 2015) [sssd] [start_service] (0x0100): Queueing service LDAP for startup"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/strace_log.json b/src/formats/strace_log.json
new file mode 100644
index 0000000..46c3a44
--- /dev/null
+++ b/src/formats/strace_log.json
@@ -0,0 +1,44 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "strace_log": {
+ "title": "Strace",
+ "description": "The strace output format.",
+ "url": "http://en.wikipedia.org/wiki/Strace",
+ "multiline": false,
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d{2}:\\d{2}:\\d{2}\\.\\d{6}) (?<syscall>\\w+)\\((?<body>.*)\\)\\s+=\\s+(?<rc>[-\\w]+)(?: (?<errno>\\w+) \\([^\\)]+\\))?(?: <(?<duration>\\d+\\.\\d+)>)?$"
+ }
+ },
+ "level-field": "errno",
+ "level": {
+ "error": ".+"
+ },
+ "value": {
+ "syscall": {
+ "kind": "string",
+ "identifier": true,
+ "rewriter": ":pipe-line-to explain-syscall.sh ${syscall}"
+ },
+ "rc": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "duration": {
+ "kind": "float"
+ },
+ "errno": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "08:09:33.814936 execve(\"/bin/ls\", [\"ls\"], [/* 38 vars */]) = 0 <0.000264>"
+ },
+ {
+ "line": "08:09:33.815943 access(\"/etc/ld.so.nohwcap\", F_OK) = -1 ENOENT (No such file or directory) <0.000019>"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/sudo_log.json b/src/formats/sudo_log.json
new file mode 100644
index 0000000..d2ee72c
--- /dev/null
+++ b/src/formats/sudo_log.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "sudo_log": {
+ "title": "sudo",
+ "description": "The sudo privilege management tool.",
+ "url": "",
+ "regex": {
+ "std": {
+ "module-format": true,
+ "pattern": "^(?<login>\\S+)\\s*: (?:(?<error_msg>[^;]+);)?\\s*TTY=(?<tty>[^;]+)\\s+;\\s*PWD=(?<pwd>[^;]+)\\s+;\\s*USER=(?<user>[^;]+)\\s+;\\s*COMMAND=(?<command>.*)$"
+ }
+ },
+ "level-field": "error_msg",
+ "level": {
+ "error": ".+"
+ },
+ "value": {
+ "login": {
+ "kind": "string",
+ "identifier": true
+ },
+ "error_msg": {
+ "kind": "string"
+ },
+ "tty": {
+ "kind": "string"
+ },
+ "pwd": {
+ "kind": "string"
+ },
+ "user": {
+ "kind": "string",
+ "identifier": true
+ },
+ "command": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "stack : 3 incorrect password attempts ; TTY=ttys005 ; PWD=/Users/stack/ClionProjects/lbuild ; USER=root ; COMMAND=/bin/ls"
+ },
+ {
+ "line": "stack : TTY=ttys005 ; PWD=/Users/stack/ClionProjects/lbuild ; USER=root ; COMMAND=/bin/ls"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/syslog_log.json b/src/formats/syslog_log.json
new file mode 100644
index 0000000..9207f74
--- /dev/null
+++ b/src/formats/syslog_log.json
@@ -0,0 +1,99 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "syslog_log": {
+ "title": "Syslog",
+ "description": "The system logger format found on most posix systems.",
+ "url": "http://en.wikipedia.org/wiki/Syslog",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>(?:\\S{3,8}\\s+\\d{1,2} \\d{2}:\\d{2}:\\d{2}|\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3,6})?(?:Z|(?:\\+|-)\\d{2}:\\d{2})))(?: (?<log_hostname>[a-zA-Z0-9:][^ ]+[a-zA-Z0-9]))?(?: \\[CLOUDINIT\\])?(?:(?: syslogd [\\d\\.]+|(?: (?<log_syslog_tag>(?<log_procname>(?:[^\\[: ]+|[^ :]+))(?:\\[(?<log_pid>\\d+)\\](?: \\([^\\)]+\\))?)?))):\\s*(?<body>.*)$|:?(?:(?: ---)? last message repeated \\d+ times?(?: ---)?))"
+ },
+ "rfc5424": {
+ "pattern": "^<(?<log_pri>\\d+)>(?<syslog_version>\\d+) (?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{6})?(?:[^ ]+)?) (?<log_hostname>[^ ]+|-) (?<log_syslog_tag>(?<log_procname>[^ ]+|-) (?<log_pid>[^ ]+|-) (?<log_msgid>[^ ]+|-)) (?<log_struct>\\[(?:[^\\]\"]|\"(?:\\.|[^\"])+\")*\\]|-|)\\s+(?<body>.*)"
+ }
+ },
+ "level-field": "body",
+ "level": {
+ "error": "(?:(?:(?<![a-zA-Z]))(?:(?i)error(?:s)?)(?:(?![a-zA-Z]))|failed|failure)",
+ "warning": "(?:(?:(?i)warn)|not responding|init: cannot execute)"
+ },
+ "opid-field": "log_syslog_tag",
+ "multiline": true,
+ "module-field": "log_procname",
+ "value": {
+ "log_pri": {
+ "kind": "integer",
+ "foreign-key": true,
+ "description": "The priority level of the message"
+ },
+ "syslog_version": {
+ "kind": "integer",
+ "foreign-key": true,
+ "description": "The version of the syslog format used for this message"
+ },
+ "log_hostname": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true,
+ "description": "The name of the host that generated the message"
+ },
+ "log_procname": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The name of the process that generated the message"
+ },
+ "log_pid": {
+ "kind": "string",
+ "identifier": true,
+ "action-list": [
+ "dump_pid"
+ ],
+ "description": "The ID of the process that generated the message"
+ },
+ "log_syslog_tag": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The combination of the procname and pid"
+ },
+ "log_msgid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "log_struct": {
+ "kind": "struct"
+ }
+ },
+ "action": {
+ "dump_pid": {
+ "label": "Show Process Info",
+ "capture-output": true,
+ "cmd": [
+ "dump-pid.sh"
+ ]
+ }
+ },
+ "sample": [
+ {
+ "line": "Apr 28 04:02:03 tstack-centos5 syslogd 1.4.1: restart."
+ },
+ {
+ "line": "Jun 27 01:47:20 Tims-MacBook-Air.local configd[17]: network changed: v4(en0-:192.168.1.8) DNS- Proxy- SMB"
+ },
+ {
+ "line": "Jun 20 17:26:13 ip-10-188-149-5 [CLOUDINIT] util.py[DEBUG]: Restoring selinux mode for /var/lib/cloud (recursive=False)"
+ },
+ {
+ "line": "<46>1 2017-04-27T07:50:47.381967+02:00 logserver rsyslogd - - [origin software=\"rsyslogd\" swVersion=\"8.4.2\" x-pid=\"900\" x-info=\"http://www.rsyslog.com\"] start"
+ },
+ {
+ "line": "<30>1 2017-04-27T07:59:12+02:00 nextcloud dhclient - - - DHCPREQUEST on eth0 to 192.168.1.1 port 67"
+ },
+ {
+ "line": "<78>1 2017-04-27T08:09:01+02:00 nextcloud CRON 1472 - - (root) CMD ( [ -x /usr/lib/php5/sessionclean ] && /usr/lib/php5/sessionclean)"
+ },
+ {
+ "line": "Aug 1 00:00:03 Tim-Stacks-iMac com.apple.xpc.launchd[1] (com.apple.mdworker.shared.0C000000-0700-0000-0000-000000000000[50989]): Service exited due to SIGKILL | sent by mds[198]"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/tcf_log.json b/src/formats/tcf_log.json
new file mode 100644
index 0000000..335d3ff
--- /dev/null
+++ b/src/formats/tcf_log.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "tcf_log": {
+ "title": "TCF Log",
+ "description": "Target Communication Framework log",
+ "url": [
+ "http://wiki.eclipse.org/TCF",
+ "http://git.eclipse.org/c/tcf/org.eclipse.tcf.git/tree/target_explorer/plugins/org.eclipse.tm.te.tcf.log.core/src/org/eclipse/tm/te/tcf/log/core/internal/listener/ChannelTraceListener.java?id=b6e81bb8405f99dda2764b22cff876fa00f734f5#n144"
+ ],
+ "regex": {
+ "std": {
+ "pattern": "^TCF (?<timestamp>\\d{2}:\\d{2}.\\d{3,6}): (?:Server-Properties: (?:.*)|channel server|\\w+: (?<dir>--->|<---) (?<type>\\w)(?: (?<token>\\w+))?(?: (?<service>\\w+))?(?: (?<name>\\w+))?(?: (?<msg>.*))?(?: <eom>))(?<body>.*)$"
+ }
+ },
+ "value": {
+ "dir": {
+ "kind": "string"
+ },
+ "type": {
+ "kind": "string",
+ "identifier": true
+ },
+ "token": {
+ "kind": "string",
+ "identifier": true
+ },
+ "service": {
+ "kind": "string",
+ "identifier": true
+ },
+ "name": {
+ "kind": "string",
+ "identifier": true
+ },
+ "msg": {
+ "kind": "json"
+ }
+ },
+ "sample": [
+ {
+ "line": "TCF 29:47.191: Server-Properties: {\"Name\":\"TCF Protocol Logger\",\"OSName\":\"Linux 3.2.0-60-generic\",\"UserName\":\"xavier\",\"AgentID\":\"1fde3dd1-d4be-4f79-8090-6f8d212f03bf\",\"TransportName\":\"TCP\",\"Proxy\":\"\",\"ValueAdd\":\"1\",\"Port\":\"1534\"}"
+ },
+ {
+ "line": "TCF 30:11.475: 0: <--- R 2 [\"P1\"] <eom>"
+ },
+ {
+ "line": "TCF 30:11.475: 0: ---> C 4 RunControl getChildren \"P1\" <eom>"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/tcsh_history.json b/src/formats/tcsh_history.json
new file mode 100644
index 0000000..86f7cbd
--- /dev/null
+++ b/src/formats/tcsh_history.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "tcsh_history": {
+ "title": "TCSH History",
+ "description": "The tcsh history file format.",
+ "convert-to-local-time": true,
+ "regex": {
+ "std": {
+ "pattern": "^#(?<timestamp>\\+\\d+)\\n?(?<body>.*)?$"
+ }
+ },
+ "sample": [
+ {
+ "line": "#+1375138067\necho HELLO=BAR"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/unifi_iptables_log.json b/src/formats/unifi_iptables_log.json
new file mode 100644
index 0000000..907266a
--- /dev/null
+++ b/src/formats/unifi_iptables_log.json
@@ -0,0 +1,154 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "unifi_iptables_log": {
+ "title": "UniFi iptables log",
+ "description": "The UniFi gateway iptables logger format (for /var/log/iptables).",
+ "url": "https://www.halolinux.us/firewalls/firewall-log-messages-what-do-they-mean.html",
+ "regex": {
+ "kernel-udp": {
+ "pattern": "^(?<timestamp>[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+) (?<host>[^\\s]+)\\s(?:\\[(?<rule_name>[^\\]]+)\\]\\s*)?(?:ALIEN BLOCK: )?DESCR=\"(?<DESCR>.*?)\"?\\sIN=(?<IP_IN>(?:\\d|\\w)*) OUT=(?<IP_OUT>(?:\\d|\\w)*) MAC=(?:(?<MAC>(?:[0-9a-f]{2}:){5}[0-9a-f]{2})(?::(?<MAC_SRC>[^\\s]+)))? SRC=(?<SRC>(?:[\\d\\.])+) DST=(?<DST>(?:[\\d\\.])+) LEN=(?<LEN>(?:\\d+)) TOS=(?<TOS>(?:[0-9A-F])+) PREC=(?<PREC>0x(?:[0-9A-F])+) TTL=(?<TTL>\\d+) ID=(?<ID>\\d+) ((?<DF>(?:DF)) )?PROTO=(?<PROTO>UDP) SPT=(?<SPT>\\d+) DPT=(?<DPT>\\d+) LEN=(?<LEN_UDP>\\d+)\\s*(?<body>.*)$"
+ },
+ "kernel-tcp": {
+ "pattern": "^(?<timestamp>[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+) (?<host>[^\\s]+)\\s(?:\\[(?<rule_name>[^\\]]+)\\]\\s*)?(?:ALIEN BLOCK: )?DESCR=\"(?<DESCR>.*?)\"?\\sIN=(?<IP_IN>(?:\\d|\\w)*) OUT=(?<IP_OUT>(?:\\d|\\w)*) MAC=(?:(?<MAC>(?:[0-9a-f]{2}:){5}[0-9a-f]{2})(?::(?<MAC_SRC>[^\\s]+))) SRC=(?<SRC>(?:[\\d\\.])+) DST=(?<DST>(?:[\\d\\.])+) LEN=(?<LEN>(?:\\d+)) TOS=(?<TOS>(?:[0-9A-F])+) PREC=(?<PREC>0x(?:[0-9A-F])+) TTL=(?<TTL>\\d+) ID=(?<ID>\\d+) ((?<DF>(?:DF)) )?PROTO=(?<PROTO>TCP) SPT=(?<SPT>\\d+) DPT=(?<DPT>\\d+) SEQ=(?<SEQ>\\d+) ACK=(?<ACK>\\d+) WINDOW=(?<WINDOW>\\d+) (?<body>.*)$"
+ },
+ "kernel-other-proto": {
+ "pattern": "^(?<timestamp>[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+) (?<host>[^\\s]+)\\s(?:\\[(?<rule_name>[^\\]]+)\\]\\s*)?(?:ALIEN BLOCK: )?DESCR=\"(?<DESCR>.*?)\"?\\sIN=(?<IP_IN>(?:\\d|\\w)*) OUT=(?<IP_OUT>(?:\\d|\\w)*) MAC=(?:(?<MAC>(?:[0-9a-f]{2}:){5}[0-9a-f]{2})(?::(?<MAC_SRC>[^\\s]+)))? SRC=(?<SRC>(?:[\\d\\.])+) DST=(?<DST>(?:[\\d\\.])+) LEN=(?<LEN>(?:\\d+)) TOS=(?<TOS>(?:[0-9A-F])+) PREC=(?<PREC>0x(?:[0-9A-F])+) TTL=(?<TTL>\\d+) ID=(?<ID>\\d+) ((?<DF>(?:DF)) )?PROTO=(?<PROTO>(?!TCP|UDP)(?:\\w+))(?<body>.*)$"
+ }
+ },
+ "opid-field": "SEQ",
+ "multiline": false,
+ "timestamp-format": [
+ "%b %d %H:%M:%S"
+ ],
+ "value": {
+ "host" : {
+ "kind": "string",
+ "identifier": true,
+ "hidden": true
+ },
+ "rule_name" : {
+ "kind": "string",
+ "identifier": true
+ },
+ "DESCR" : {
+ "kind": "string",
+ "identifier": false
+ },
+ "dhcp_op" : {
+ "kind": "string",
+ "identifier": true
+ },
+ "dhcp_mac" : {
+ "kind": "string",
+ "identifier": true
+ },
+ "dhcp_iface" : {
+ "kind": "string"
+ },
+ "dhcp_ip" : {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "SEQ" : {
+ "kind": "integer",
+ "identifier": true
+ },
+ "IP_IN" : {
+ "kind": "string",
+ "identifier": false
+ },
+ "IP_OUT" : {
+ "kind": "string",
+ "identifier": false
+ },
+ "MAC" : {
+ "kind": "string",
+ "identifier": true,
+ "hidden": true
+ },
+ "MAC_SRC" : {
+ "kind": "string",
+ "identifier": false,
+ "hidden": true
+ },
+ "SRC" : {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "SPT" : {
+ "kind": "integer",
+ "identifier": true
+ },
+ "DST" : {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "DPT" : {
+ "kind": "integer",
+ "identifier": true
+ },
+ "LEN" : {
+ "kind": "integer"
+ },
+ "TOS" : {
+ "kind": "string",
+ "hidden": true
+ },
+ "PREC" : {
+ "kind": "string",
+ "hidden": true
+ },
+ "TTL" : {
+ "kind": "integer",
+ "hidden": true
+ },
+ "PROTO" : {
+ "kind": "string",
+ "identifier": true
+ },
+ "LEN_UDP" : {
+ "kind": "integer"
+ },
+ "WINDOW" : {
+ "kind": "integer",
+ "hidden": true
+ },
+ "RES" : {
+ "kind": "string",
+ "hidden": true
+ },
+ "SYN" : {
+ "kind": "string",
+ "hidden": true
+ },
+ "URGP" : {
+ "kind": "integer",
+ "hidden": true
+ },
+ "body" : {
+ "kind": "string"
+ }
+ },
+ "highlights": {
+ "rule_name": {
+ "pattern": "(\\[.*-[DR]-.*\\])",
+ "color": "Red",
+ "underline": true
+ }
+ },
+ "sample": [
+ {
+ "line": "Apr 7 21:49:03 UDM-Pro [WAN_IN-RET-3006] DESCR=\"[WAN_IN] PortForward Allow [HTTPS (cli IN=eth8 OUT=br96 MAC=24:5a:4c:a2:b1:09:48:2c:d0:ab:93:3f:08:00 SRC=194.230.158.35 DST=192.168.96.10 LEN=60 TOS=00 PREC=0x00 TTL=49 ID=51880 DF PROTO=TCP SPT=38618 DPT=10443 SEQ=2560518888 ACK=0 WINDOW=65535 SYN URGP=0 MARK=0"
+ },
+ {
+ "line": "Apr 7 23:24:35 UDM-Pro [PREROUTING-DNAT-13] DESCR=\"PortForward DNAT [HTTPS (client-cer IN=eth8 OUT= MAC=24:5a:4c:a2:b1:09:48:2c:d0:ab:93:3f:08:00 SRC=103.203.57.14 DST=192.168.10.2 LEN=40 TOS=00 PREC=0x00 TTL=241 ID=54321 PROTO=TCP SPT=43609 DPT=443 SEQ=3913455798 ACK=0 WINDOW=65535 SYN URGP=0 MARK=0"
+ },
+ {
+ "line": "Apr 7 21:40:58 UDM-Pro [LAN_IN-D-4001] DESCR=\"🛑Drop all InterVLAN traffic\" IN=br96 OUT=tlprt0 MAC=24:5a:4c:a2:b1:0b:24:5e:be:46:df:c8:08:00 SRC=192.168.96.10 DST=192.168.2.1 LEN=40 TOS=00 PREC=0x00 TTL=63 ID=50760 DF PROTO=TCP SPT=8084 DPT=55504 SEQ=1967017102 ACK=3747652110 WINDOW=131 ACK FIN URGP=0 MARK=0"
+ }
+ ]
+ }
+}
diff --git a/src/formats/unifi_log.json b/src/formats/unifi_log.json
new file mode 100644
index 0000000..369e753
--- /dev/null
+++ b/src/formats/unifi_log.json
@@ -0,0 +1,204 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "unifi_log": {
+ "title": "UniFi log",
+ "description": "The UniFi gateway messages logger format (for /var/log/messages).",
+ "url": "https://www.halolinux.us/firewalls/firewall-log-messages-what-do-they-mean.html",
+ "regex": {
+ "kernel-udp": {
+ "pattern": "^(?<timestamp>[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+) (?<host>[^\\s]+) (?<facility>\\w+)\\.(?<level>\\w+) (?<module>kernel): \\[(?:\\s*(?<ellapsed>\\d+\\.\\d+))\\]\\s(?:\\[(?<rule_name>[^\\]]+)\\]\\s*)?(?:ALIEN BLOCK: )?IN=(?<IP_IN>(?:\\d|\\w)*) OUT=(?<IP_OUT>(?:\\d|\\w)*) MAC=(?:(?<MAC>(?:[0-9a-f]{2}:){5}[0-9a-f]{2})(?::(?<MAC_SRC>[^\\s]+)))? SRC=(?<SRC>(?:[\\d\\.])+) DST=(?<DST>(?:[\\d\\.])+) LEN=(?<LEN>(?:\\d+)) TOS=(?<TOS>0x(?:[0-9A-F])+) PREC=(?<PREC>0x(?:[0-9A-F])+) TTL=(?<TTL>\\d+) ID=(?<ID>\\d+) (?<DF>(?:DF) )?PROTO=(?<PROTO>UDP) SPT=(?<SPT>\\d+) DPT=(?<DPT>\\d+) LEN=(?<LEN_UDP>\\d+)\\s*(?<body>.*)$"
+ },
+ "kernel-tcp": {
+ "pattern": "^(?<timestamp>[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+) (?<host>[^\\s]+) (?<facility>\\w+)\\.(?<level>\\w+) (?<module>kernel): \\[(?:\\s*(?<ellapsed>\\d+\\.\\d+))\\]\\s(?:\\[(?<rule_name>[^\\]]+)\\]\\s*)?(?:ALIEN BLOCK: )?IN=(?<IP_IN>(?:\\d|\\w)*) OUT=(?<IP_OUT>(?:\\d|\\w)*) MAC=(?:(?<MAC>(?:[0-9a-f]{2}:){5}[0-9a-f]{2})(?::(?<MAC_SRC>[^\\s]+))) SRC=(?<SRC>(?:[\\d\\.])+) DST=(?<DST>(?:[\\d\\.])+) LEN=(?<LEN>(?:\\d+)) TOS=(?<TOS>0x(?:[0-9A-F])+) PREC=(?<PREC>0x(?:[0-9A-F])+) TTL=(?<TTL>\\d+) ID=(?<ID>\\d+) (?<DF>(?:DF) )?PROTO=(?<PROTO>TCP) SPT=(?<SPT>\\d+) DPT=(?<DPT>\\d+) WINDOW=(?<WINDOW>\\d+) RES=(?<RES>0x(?:[0-9A-F])+) (?<SYN>(?:SYN) )?(?:URGP=(?<URGP>\\d+)\\s*)?(?<body>.*)$"
+ },
+ "kernel-other-proto": {
+ "pattern": "^(?<timestamp>[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+) (?<host>[^\\s]+) (?<facility>\\w+)\\.(?<level>\\w+) (?<module>kernel): \\[(?:\\s*(?<ellapsed>\\d+\\.\\d+))\\]\\s(?:\\[(?<rule_name>[^\\]]+)\\]\\s*)?(?:ALIEN BLOCK: )?IN=(?<IP_IN>(?:\\d|\\w)*) OUT=(?<IP_OUT>(?:\\d|\\w)*) MAC=(?:(?<MAC>(?:[0-9a-f]{2}:){5}[0-9a-f]{2})(?::(?<MAC_SRC>[^\\s]+)))? SRC=(?<SRC>(?:[\\d\\.])+) DST=(?<DST>(?:[\\d\\.])+) LEN=(?<LEN>(?:\\d+)) TOS=(?<TOS>0x(?:[0-9A-F])+) PREC=(?<PREC>0x(?:[0-9A-F])+) TTL=(?<TTL>\\d+) ID=(?<ID>\\d+) (?<DF>(?:DF) )?PROTO=(?<PROTO>(?!TCP|UDP)(?:\\w+))(?<body>.*)$"
+ },
+ "kernel-other": {
+ "pattern": "^(?<timestamp>[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+) (?<host>[^\\s]+) (?<facility>\\w+)\\.(?<level>\\w+) (?<module>kernel): (?:\\[(?:\\s*(?<ellapsed>\\d+\\.\\d+))\\]\\s)?(?!IN|ALIEN BLOCK)(?<body>[^\\[].*)$"
+ },
+ "dnsmasq-dhcp": {
+ "pattern": "^(?<timestamp>[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+) (?<host>[^\\s]+) (?<facility>\\w+)\\.(?<level>\\w+) (?<module>dnsmasq-dhcp[A-Za-z0-9\\.\\-]*)(?:\\[(?<ID>\\d+)\\])?: (?<dhcp_op>DHCP[^(]+)(?:\\((?<dhcp_iface>[^)]*)\\)) (?:(?<dhcp_ip>(?:\\d{1,3}\\.){3}\\d{1,3}) )?(?<dhcp_mac>(?:[0-9a-f]{2}:)+[0-9a-f]{2})(?: (?<body>.*))?$"
+ },
+ "other": {
+ "pattern": "^(?<timestamp>[A-Z][a-z]{2}\\s+\\d+\\s+\\d+:\\d+:\\d+) (?<host>[^\\s]+) (?<facility>\\w+)\\.(?<level>\\w+) (?<module>(?!kernel|dnsmasq-dhcp)[A-Za-z0-9\\.\\-]*)(?:\\[(?<ID>\\d+)\\])?: (?:\\[apply-config\\])?(?:\\[(?:\\s*(?<ellapsed>\\d+\\.\\d+))\\]\\s)?(?<body>.*)$"
+ }
+ },
+ "level-field": "level",
+ "level": {
+ "critical": "emerg",
+ "error": "err",
+ "warning": "warn",
+ "info": "notice"
+ },
+ "opid-field": "ID",
+ "multiline": false,
+ "module-field": "module",
+ "timestamp-format": [
+ "%b %d %H:%M:%S"
+ ],
+ "value": {
+ "level": {
+ "kind": "string",
+ "identifier": true
+ },
+ "facility": {
+ "kind": "string",
+ "identifier": false
+ },
+ "module": {
+ "kind": "string",
+ "identifier": false
+ },
+ "ellapsed": {
+ "kind": "float",
+ "identifier": false,
+ "hidden": true
+ },
+ "rule_name": {
+ "kind": "string",
+ "identifier": true
+ },
+ "host": {
+ "kind": "string",
+ "identifier": true,
+ "hidden": true
+ },
+ "dhcp_op": {
+ "kind": "string",
+ "identifier": true
+ },
+ "dhcp_mac": {
+ "kind": "string",
+ "identifier": true
+ },
+ "dhcp_iface": {
+ "kind": "string"
+ },
+ "dhcp_ip": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "ID": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "IP_IN": {
+ "kind": "string",
+ "identifier": false
+ },
+ "IP_OUT": {
+ "kind": "string",
+ "identifier": false
+ },
+ "MAC": {
+ "kind": "string",
+ "identifier": true,
+ "hidden": true
+ },
+ "MAC_SRC": {
+ "kind": "string",
+ "identifier": false,
+ "hidden": true
+ },
+ "SRC": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "SPT": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "DST": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "DPT": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "LEN": {
+ "kind": "integer"
+ },
+ "TOS": {
+ "kind": "string",
+ "hidden": true
+ },
+ "PREC": {
+ "kind": "string",
+ "hidden": true
+ },
+ "TTL": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "PROTO": {
+ "kind": "string",
+ "identifier": true
+ },
+ "LEN_UDP": {
+ "kind": "integer"
+ },
+ "WINDOW": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "RES": {
+ "kind": "string",
+ "hidden": true
+ },
+ "SYN": {
+ "kind": "string",
+ "hidden": true
+ },
+ "URGP": {
+ "kind": "integer",
+ "hidden": true
+ },
+ "body": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "Mar 2 23:24:28 UDM-Pro user.warn kernel: [1293979.679369] IN=br46 OUT= MAC=24:5a:4c:a2:b1:0b:74:7a:90:9f:e4:ff:08:00 SRC=192.168.46.5 DST=8.8.8.8 LEN=68 TOS=0x00 PREC=0x00 TTL=255 ID=34103 DF PROTO=UDP SPT=65450 DPT=53 LEN=48"
+ },
+ {
+ "line": "Mar 2 23:24:28 UDM-Pro user.warn kernel: [ 979.679369] [DNAT-br46-udp]IN=br46 OUT= MAC=24:5a:4c:a2:b1:0b:74:7a:90:9f:e4:ff:08:00 SRC=192.168.46.5 DST=8.8.8.8 LEN=68 TOS=0x00 PREC=0x00 TTL=255 ID=34103 DF PROTO=UDP SPT=65450 DPT=53 LEN=48"
+ },
+ {
+ "line": "Mar 2 23:00:01 UDM-Pro user.warn kernel: [1293512.217894] [FW-A-LAN_LOCAL_U-2013]IN=br96 OUT= MAC=24:5a:4c:a2:b1:0b:24:5e:be:46:df:c8:08:00 SRC=192.168.96.10 DST=192.168.16.1 LEN=40 TOS=0x00 PREC=0x20 TTL=64 ID=44654 DF PROTO=TCP SPT=55144 DPT=22 WINDOW=837 RES=0x00 ACK URGP=0"
+ },
+ {
+ "line": "Mar 4 19:25:13 UDM-Pro user.warn kernel: [ 1170.826385] ALIEN BLOCK: IN=eth8 OUT= MAC=24:5a:4c:a2:b1:09:48:2c:d0:ab:93:3f:08:00 SRC=185.128.41.50 DST=192.168.10.2 LEN=40 TOS=0x00 PREC=0x00 TTL=244 ID=51242 PROTO=TCP SPT=56958 DPT=7001 WINDOW=1024 RES=0x00 SYN URGP=0"
+ },
+ {
+ "line": "Mar 2 23:27:40 UDM-Pro authpriv.notice dropbear[29787]: Pubkey auth succeeded for 'root' with key sha1!! 0e:16:76:2b:89:b3:c0:c7:14:a4:00:be:8f:9b:38:9a:12:fd:20:48 from 192.168.96.27:56718"
+ },
+ {
+ "line": "Mar 4 19:26:07 UDM-Pro user.warn kernel: [ 1225.134675] conntrack: generic helper won't handle protocol 47. Please consider loading the specific helper module."
+ },
+ {
+ "line": "Mar 4 19:39:06 UDM-Pro user.notice syswrapper: [apply-config] using fast apply"
+ },
+ {
+ "line": "Feb 27 23:59:39 UDM-Pro user.notice dpi-flow-stats: ubnt-dpi-util: fingerprint_overrides API failed with HTTP -1"
+ },
+ {
+ "line": "Mar 4 19:09:18 UDM-Pro user.notice getsig.sh: alien enabled, starting update."
+ },
+ {
+ "line": "Mar 4 19:22:19 UDM-Pro daemon.info dnsmasq-dhcp[7431]: DHCPREQUEST(br96) 192.168.96.31 a0:57:e3:00:78:9e"
+ },
+ {
+ "line": "Mar 6 22:38:20 UDM-Pro user.warn kernel: [ 6509.503966] [FW-A-LAN_LOCAL_U-2147483647]IN=br96 OUT= MAC= SRC=192.168.96.1 DST=233.89.188.1 LEN=32 TOS=0x00 PREC=0xC0 TTL=1 ID=0 DF PROTO=2"
+ },
+ {
+ "line": "Feb 28 14:25:54 UDM-Pro daemon.err mcad: mcad[2910]: ace_reporter.reporter_fail(): initial contact failed #6, url=http://localhost:8080/inform, rc=7"
+ }
+ ]
+ }
+}
diff --git a/src/formats/uwsgi_log.json b/src/formats/uwsgi_log.json
new file mode 100644
index 0000000..cabc436
--- /dev/null
+++ b/src/formats/uwsgi_log.json
@@ -0,0 +1,108 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "uwsgi_log": {
+ "title": "Uwsgi Log",
+ "description": "The uwsgi log format.",
+ "multiline": false,
+ "regex": {
+ "std": {
+ "pattern": "^\\[pid: (?<s_pid>\\d+)\\|app: (?<s_app>[\\-\\d]+)\\|req: (?<s_req>[\\-\\d]+)/(?<s_worker_reqs>\\d+)\\] (?<c_ip>[^ ]+) \\((?<cs_username>[^\\)]*)\\) \\{(?<cs_vars>\\d+) vars in (?<cs_bytes>\\d+) bytes\\} \\[(?<timestamp>[^\\]]+)\\] (?<cs_method>[A-Z]+) (?<cs_uri_stem>[^ \\?]+)(?:\\?(?<cs_uri_query>[^ ]*))? => generated (?<sc_bytes>\\d+) bytes in (?<s_runtime>\\d+) (?<rt_unit>\\w+) \\((?<cs_version>[^ ]+) (?<sc_status>\\d+)\\) (?<sc_headers>\\d+) headers in (?<sc_header_bytes>\\d+) bytes \\((?<s_switches>\\d+) switches on core (?<s_core>\\d+)\\)(?<body>.*)"
+ }
+ },
+ "level-field": "sc_status",
+ "level": {
+ "error": "^[^123]"
+ },
+ "opid-field": "s_pid",
+ "value": {
+ "s_pid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "s_app": {
+ "kind": "string",
+ "identifier": true
+ },
+ "s_req": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "s_worker_reqs": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "c_ip": {
+ "kind": "string",
+ "collate": "ipaddress",
+ "identifier": true
+ },
+ "cs_username": {
+ "kind": "string",
+ "identifier": true
+ },
+ "cs_vars": {
+ "kind": "integer"
+ },
+ "cs_bytes": {
+ "kind": "integer"
+ },
+ "cs_method": {
+ "kind": "string",
+ "identifier": true
+ },
+ "cs_uri_stem": {
+ "kind": "string",
+ "identifier": true
+ },
+ "cs_uri_query": {
+ "kind": "string"
+ },
+ "sc_bytes": {
+ "kind": "integer"
+ },
+ "s_runtime": {
+ "kind": "float",
+ "description": "hello, world",
+ "unit": {
+ "field": "rt_unit",
+ "scaling-factor": {
+ "msecs": {
+ "op": "divide",
+ "value": 1000.0
+ },
+ "micros": {
+ "op": "divide",
+ "value": 1000000.0
+ }
+ }
+ }
+ },
+ "cs_version": {
+ "kind": "string",
+ "identifier": true
+ },
+ "sc_status": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "sc_headers": {
+ "kind": "integer"
+ },
+ "sc_header_bytes": {
+ "kind": "integer"
+ },
+ "s_switches": {
+ "kind": "integer"
+ },
+ "s_core": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "[pid: 24386|app: 0|req: 482950/4125645] 86.221.170.65 () {44 vars in 1322 bytes} [Tue Jan 3 05:01:31 2012] GET /contest/log_presence/shhootter/?_=1325592089910 => generated 192 bytes in 21 msecs (HTTP/1.1 200) 4 headers in 188 bytes (1 switches on core 0)"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/vdsm_log.json b/src/formats/vdsm_log.json
new file mode 100644
index 0000000..52529e3
--- /dev/null
+++ b/src/formats/vdsm_log.json
@@ -0,0 +1,67 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "vdsm_log": {
+ "title": "Vdsm Logs",
+ "description": "Vdsm log format",
+ "url": "http://www.ovirt.org/develop/developer-guide/vdsm/log-files/",
+ "regex": {
+ "v4.1": {
+ "pattern": "(?s)^(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}(?:\\+\\d{4})?) (?<level>\\w+)\\s+\\((?<thread>.+?)\\) \\[(?<logger>.+?)\\]\\s+?(?<body>.*?)\\s+?\\((?<src_file>\\w+?):(?<src_line>\\d+)\\)(\\n(?<traceback>Traceback.*?)(?=\\n(?P=timestamp)|$))?"
+ },
+ "v4": {
+ "pattern": "^(?<tid>[^:]+)::(?<level>[^:]+)::(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}(?:,\\d{3})?)::(?<module>[^:]+)::(?<src_line>[^:]+)::(?<logger>[^:]+)::\\((?<func>[^\\)]+)\\)(?<body>.*)"
+ }
+ },
+ "level-field": "level",
+ "level": {
+ "error": "ERROR",
+ "debug": "DEBUG",
+ "info": "INFO",
+ "warning": "WARNING|WARN",
+ "critical": "CRIT",
+ "trace": "TRACE"
+ },
+ "value": {
+ "tid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "module": {
+ "kind": "string",
+ "identifier": true
+ },
+ "src_line": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "logger": {
+ "kind": "string",
+ "identifier": true
+ },
+ "func": {
+ "kind": "string",
+ "identifier": true
+ },
+ "thread": {
+ "kind": "string",
+ "identifier": true
+ },
+ "src_file": {
+ "kind": "string",
+ "identifier": true
+ },
+ "traceback": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "Thread-1950::INFO::2011-12-07 12:14:15,018::dispatcher::94::Storage.Dispatcher.Protect::(run) Run and protect: getDeviceList, args: ( storageType=2)"
+ },
+ {
+ "line": "2017-03-06 14:49:05,167+0200 INFO (vm/9e5dd42e) [virt.vm] (vmId='9e5dd42e-5177-4da3-a6ce-87fa2052d315') (vm:2104)"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/vmk_log.json b/src/formats/vmk_log.json
new file mode 100644
index 0000000..5d93def
--- /dev/null
+++ b/src/formats/vmk_log.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "vmk_log": {
+ "title": "VMKernel Logs",
+ "description": "The VMKernel's log format",
+ "url": "",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z) cpu(?<cpu>\\d+):(?<world_id>\\d+)(?: opID=(?<opid>[^\\)]+))?\\)((?:(?<level>WARNING|ALERT)|(?<subsystem>[^:]+)): )?(?<body>.*)"
+ }
+ },
+ "level-field": "level",
+ "level": {
+ "error": "ALERT",
+ "warning": "WARNING"
+ },
+ "max-unrecognized-lines": 15000,
+ "opid-field": "opid",
+ "value": {
+ "cpu": {
+ "kind": "integer",
+ "identifier": true,
+ "foreign-key": true
+ },
+ "world_id": {
+ "kind": "integer",
+ "identifier": true,
+ "foreign-key": true
+ },
+ "subsystem": {
+ "kind": "string",
+ "identifier": true
+ },
+ "opid": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "2014-11-14T19:19:51.559Z cpu7:35233)VC: 2002: Device rescan time 704 msec (total number of devices 91)"
+ },
+ {
+ "line": "2015-04-01T22:22:35.038Z cpu22:44012977)ALERT: This is what an alert looks like."
+ },
+ {
+ "line": "2022-06-02T02:16:57.414Z cpu31:1001392590 opID=827cfaf)<unk>: UWVMKSyscall: ForkExec:2408: hostd-worker: Found params <group=hostd-tmp,mem=10>"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/vmw_log.json b/src/formats/vmw_log.json
new file mode 100644
index 0000000..c5c5f7b
--- /dev/null
+++ b/src/formats/vmw_log.json
@@ -0,0 +1,241 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "vmw_log": {
+ "title": "VMware Logs",
+ "description": "One of the log formats used in VMware's ESXi and vCenter software.",
+ "url": "https://kb.vmware.com/kb/2004201",
+ "regex": {
+ "6.0+": {
+ "pattern": "^(?:\\[#\\d+\\] )?(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}(?:Z|[-+]\\d{2}:\\d{2})) (?<level>\\w+)(?:\\(\\d+\\)+)? (?<prc>[\\w\\-]+)\\[(?<tid>\\w+)\\]:? (?:\\w+ -\\[\\d+\\] )?\\[(?<src>\\w+@\\d+)(?:\\s+sub=(?<sub>.*?(?!\\w+=)))?(?:\\s+item=(?<item>[\\w\\.\\-@/:]+))?(?: req=(?<req>[^ \\]]+))?(?: opI(?:D|d)=(?<opid>(?:req=)?[\\w@ \\-\\.:]+?(?!\\w+=)))?(?: sid=(?<sid>[^ \\]]+))?(?: user=(?<user>[^ \\]<]+(?:<[^>]+>)?))?(?: update=(?<vpxa_update>\\d+))?(?:\\s+reason=(?<reason>[^\\]]+))?\\]\\s+(?<body>.*)$"
+ },
+ "6.0+-nosrc": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}(?:Z|[-+]\\d{2}:\\d{2})) (?<level>\\w+)(?:\\(\\d+\\)+)? (?<prc>[\\w\\-]+)\\[(?<tid>\\w+)\\]:? \\[(?:opI(?:D|d)=(?<opid>[^\\]]+))\\]\\s*(?<body>.*)$"
+ },
+ "section": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}(?:Z|[-+]\\d{2}:\\d{2})) (?:- last log rotation time, \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}(?:Z|[-+]\\d{2}:\\d{2}))?\\s*(ESX KMX Agent started.|(?:- time the service was last started(?: \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z)?, )?Section for (?:[^,]+), pid=(?<tid>\\w+).*)"
+ },
+ "esx-section": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}(?:Z|[-+]\\d{2}:\\d{2})) (?<level>\\w+)(?:\\(\\d+\\)+) (?<prc>[\\w\\-]+)\\[(?<tid>\\w+)\\]: (?:Logs rotated. \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}(?:Z|[-+]\\d{2}:\\d{2}))?(?:- last log rotation time, \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}(?:Z|[-+]\\d{2}:\\d{2}))?\\s*(ESX KMX Agent started.|(?:- time the service was last started(?: \\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.\\d{3}Z)?, )?Section for (?:[^,]+), pid=(?:\\w+).*)"
+ },
+ "5.0+": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}(?:Z|[-+]\\d{2}:\\d{2})) \\[(?<tid>\\w+) (?<level>\\w+) '(?<comp>[^']+)'(?: opID=(?<opid>[^ \\]]+))?(?: user=(?<user>[^ \\]]+))?\\](?<body>.*)(?:\\n.*)?$"
+ },
+ "pre-5.0": {
+ "pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}) (?<tid>\\w+) (?<level>\\w+) '(?<comp>[^']+)'(?: opID=(?<opid>[^ \\]]+))?(?: user=(?<user>[^ \\]]+))?\\](?<body>.*)(?:\\n.*)?$"
+ },
+ "ls-log": {
+ "pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}) (?<tid>[\\w\\-]+)\\s+(?<level>\\w+)\\s+(?<comp>[^\\]]+)\\]\\s+(?<body>.*)"
+ },
+ "hdr-ftr": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}(T| )\\d{2}:\\d{2}:\\d{2}(?:.|,)\\d{3}(?:Z|[-+]\\d{2}:\\d{2})) \\[(?<prc>[^\\[]+)\\[(?<tid>\\w+)\\]:\\s+(?<body>.*)\\]$"
+ },
+ "pylog": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3})?(?:Z|[-+]\\d{2}:\\d{2})) (?<prc>[^:]+):\\s+(?<tid>\\d+):\\s+(?<comp>[^:]+):(?<line>\\d+)?\\s+(?<level>\\w+):?\\s+(?<body>.*)(?:\\n.*)?$"
+ },
+ "vum-log4cpp": {
+ "pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}:\\d{3}) '(?<category>[^']*)' (?<tid>\\d+) (?<level>[a-zA-Z]+)\\]\\s+(?>\\[(?<file>\\S+), (?<line>\\d+)\\])? (?<body>.*$)"
+ },
+ "pylog2": {
+ "pattern": "^(?<prc>[^:]+):(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3})\\[(?<tid>\\w+)\\](?<file>[^:]+):(?<line>\\d+) \\[(?<level>[a-zA-Z]+)\\]\\s+(?<body>.*)$"
+ },
+ "pylog3": {
+ "pattern": "^(?<prc>[^:]+): (?<tid>\\d+): (?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3}) (?<file>[^:]+):(?<line>\\d+) (?<level>[a-zA-Z]+)\\s+(?<body>.*)$"
+ }
+ },
+ "level-field": "level",
+ "level": {
+ "info": "^(?i)(?:info|In)$",
+ "debug": "^(?i)(?:debug|Db)$",
+ "error": "^(?i)(error|Er)$",
+ "warning": "^(?i)(warning|warn|Wa)$",
+ "trace": "^(?i)verbose$",
+ "critical": "^(?i)(?:crit|Cr)$",
+ "fatal": "^(?i)(?:alert|fatal|panic|Al|Em)$"
+ },
+ "opid-field": "opid",
+ "value": {
+ "prc": {
+ "kind": "string",
+ "identifier": true
+ },
+ "tid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "src": {
+ "kind": "string",
+ "identifier": true
+ },
+ "comp": {
+ "kind": "string",
+ "identifier": true
+ },
+ "line": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "sub": {
+ "kind": "string",
+ "identifier": true
+ },
+ "item": {
+ "kind": "string",
+ "identifier": true
+ },
+ "opid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "req": {
+ "kind": "string",
+ "identifier": true
+ },
+ "sid": {
+ "kind": "string",
+ "identifier": true
+ },
+ "user": {
+ "kind": "string",
+ "identifier": true
+ },
+ "vpxa_update": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "reason": {
+ "kind": "string"
+ },
+ "file": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "search-table": {
+ "vpxd_session_stats": {
+ "pattern": "/SessionStats/SessionPool/Session/Id='(?<SessionId>[^']+)'/Username='(?<Username>[^']+)'/ClientIP='(?<ClientIP>[^']+)'(?<ProfileKey>[^ ]+) (?<ProfileValue>[^\\n]+)",
+ "glob": "*/vpxd-profile*"
+ },
+ "vpxd_session_pool_stats": {
+ "pattern": "/SessionStats/SessionPool/Id='(?<SessionId>[^']+)'/Username='(?<Username>[^']+)'(?<ProfileKey>[^ ]+) (?<ProfileValue>[^\\n]+)",
+ "glob": "*/vpxd-profile*"
+ },
+ "vpx_lro_begin": {
+ "pattern": "\\[VpxLRO\\] -- BEGIN (?<lro_id>\\S+) -- (?<entity>\\S*) -- (?<operation>\\S*) -- (?:(?<SessionId>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:\\((?<SessionSubId>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\))?)?"
+ },
+ "vpx_lro_finish": {
+ "pattern": "\\[VpxLRO\\] -- FINISH (?<lro_id>\\S+)"
+ },
+ "vpx_lro_error": {
+ "pattern": "\\[VpxLRO\\] -- ERROR (?<lro_id>\\S+) -- (?:(?<SessionId>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:\\((?<SessionSubId>[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\\))?)? -- (?<entity>\\S*) -- (?<operation>\\S*):\\s*(?<error>.*)",
+ "level": "error"
+ }
+ },
+ "tags": {
+ "test-failure": {
+ "description": "Tag for gtest test-case failures",
+ "paths": [
+ {
+ "glob": "*/test.log"
+ }
+ ],
+ "pattern": "^Expected equality of these values:"
+ }
+ },
+ "sample": [
+ {
+ "line": "2021-05-24T20:31:05.671Z - last log rotation time, 2021-05-24T09:30:02.683Z - time the service was last started, Section for VMware ESX, pid=1000080910, version=7.0.3, build=0, option=DEBUG"
+ },
+ {
+ "line": "[2011-04-01 15:14:34.203 F5A5AB90 info 'vm:/vmfs/volumes/4d6579ec-23f981cb-465c-00237da0cfee/Vmotion-test/Vmotion-test.vmx' opID=F6FC49D5-000007E6-d] VMotionPrepare: dstMgmtIp=10.21.49.138"
+ },
+ {
+ "line": "2014-11-04T15:53:31.075+05:30 verbose vpxd[05160] [Originator@6876 sub=PropertyProvider opID=ProcessAlarmFiring-427c3c55] RecordOp ASSIGN: declaredAlarmState[\"alarm-1.host-23\"], host-23. Applied change to temp map."
+ },
+ {
+ "line": "2020-05-11T22:32:22.932Z [/etc/init.d/vvold[1000211787]: WaitVvoldToComeUp /var/run/vmware/.vmware-vvol.started created]"
+ },
+ {
+ "line": "2020-05-11T22:32:22.736Z info -[1000212040] [Originator@6876 sub=Default] Successfully registered SIGHUP handler"
+ },
+ {
+ "line": "2014-01-17T04:55:50.347Z [7F03ECE76700 verbose 'Default' opID=2140bc71] [VpxVmomi] Invoke done: vmodl.query.PropertyCollector.waitForUpdatesEx session: c580b3ef-0011-88a5-b2af-7ca7e74114c8"
+ },
+ {
+ "line": "2014-11-04T12:46:42.990+05:30 Section for VMware VirtualCenter, pid=6432, version=6.0.0, build=2255588, option=BETA"
+ },
+ {
+ "line": "[2013-01-16 02:26:25,500 pool-3-thread-1 INFO com.vmware.vim.license.service.impl.ServiceImpl] License Accounting Service initialized"
+ },
+ {
+ "line": "2020-05-11T22:32:38.751Z info vsansystem[1000212707] [Originator@6876 sub=Libs opId=vsan-d810] VsanConfigStore: Get object host from vsan"
+ },
+ {
+ "line": "2020-05-11T22:32:56Z lifecycle: 1000212917: lifecyclectl:58 ERROR Failed to delete pidfile(/var/vmware/lifecycle/lifecycle.apply.pid) at boot Traceback (most recent call last): File \"/usr/lib/vmware/lifecycle/bin/lifecyclectl.py\", line 55, in main os.remove(PID_APPLY_FILE) FileNotFoundError: [Errno 2] No such file or directory: '/var/vmware/lifecycle/lifecycle.apply.pid'"
+ },
+ {
+ "line": "2020-05-11T22:32:09Z esxupdate: 1000211352: root: INFO: Command = profile.setacceptance"
+ },
+ {
+ "line": "2020-05-11T22:36:11.202Z verbose hostd[1000212678] [Originator@6876 sub=Vimsvc.Ticket 52 66 af 87 76 b0 ab 21-dd 2b 28 27 3a 39 fd fd opID=ec9d7fe8] Ticket issued for root"
+ },
+ {
+ "line": "[2020-05-26 19:46:43:259 'ConfigurationMgr' 139700477753600 INFO] [configurationMgr, 155] Configuration manager created...vcidbInit"
+ },
+ {
+ "line": "HsmService:2021-05-19 17:36:35,978[MainThread]hsmService:737 [INFO] HSM Service is initialized"
+ },
+ {
+ "line": "ImageService: 139853338433792: 2021-05-19 17:36:35,675 imageService:769 INFO Image service is initialized"
+ },
+ {
+ "line": "2021-06-03T16:50:32.890Z - time the service was last started 2021-06-03T16:50:32.884Z, Section for VMware Update Manager, pid=5916, version=7.0.3, build=0, option=DEBUG"
+ },
+ {
+ "line": "2021-06-16T14:59:52.362Z info vsand[1000083783] [opID=MainThread VsanMgmtSvcMain::_PatchPyVmomi] Python 3.8.8 (default, Jun 1 2021, 03:16:39) [GCC 4.6.3] on VMkernel, optimize: 0, recursionLimit: 300"
+ },
+ {
+ "line": "2022-06-01T13:24:08.243Z Section for vdtc, pid=4774, version=e.x.p, build=19889761, option=Release"
+ },
+ {
+ "line": "2022-06-02T12:21:26.285Z info vmware-vum-server[00932] [Originator@6876 sub=SessionAuthData reason=Timeout expired] [vciSessionAuthData 94] Session [521601f2-6eb3-0475-7be8-c30eed1040ab] is closed"
+ },
+ {
+ "line": "2022-06-01T20:17:31.352Z info vpxd[27059] [Originator@6876 sub=drmLogger item=FixNIOCViolation opID=SWI-5441881e] FixNIOCViolation worker starts for host [vim.HostSystem:host-588,esx-3-23.vlcm.com]"
+ },
+ {
+ "line": "2022-06-01T14:23:06.536Z info vpxd[27349] [Originator@6876 sub=Http2Session #28] Starting a Http2Session (server): <io_obj p:0x00007eff5867c618, h:477, <TCP '127.0.0.1 : 8093'>, <TCP '127.0.0.1 : 50504'>>"
+ },
+ {
+ "line": "2022-06-02T08:34:54.811Z info vpxd[44568] [Originator@6876 sub=SoapAdapter[0].HTTPService] Max buffered response size is 104857600 bytes"
+ },
+ {
+ "line": "2022-06-02T08:34:55.613Z info vpxd[44568] [Originator@6876 sub=SSL SoapAdapter[1].HTTPService] Max buffered response size is 104857600 bytes"
+ },
+ {
+ "line": "2022-06-02T08:34:55.753Z info vpxd[44568] [Originator@6876 sub=Req@vsan/VMC M5] Created version logger for vsan/VMC M5 (vsan.version.version8)"
+ },
+ {
+ "line": "2022-06-02T08:34:55.421Z info vpxd[44568] [Originator@6876 sub=Req@vsan/vSAN 7.0U2] Created version logger for vsan/vSAN 7.0U2 (vsan.version.version16)"
+ },
+ {
+ "line": "2022-06-02T08:33:42.327Z info StatsMonitor[40881] [Originator@6876 sub=LinuxStatsProvider(00005575a7a395f0)] Created"
+ },
+ {
+ "line": "2022-06-02T08:33:42.692Z info rhttpproxy[40998] [Originator@6876 sub=WorkQueue.rhttpproxy.packetsniffer[0000562e3c06fee0].queue] Created: WorkQueue.rhttpproxy.packetsniffer[0000562e3c06fee0].queue, type = serial, priority = 16, itemWeight = 1"
+ },
+ {
+ "line": "2022-06-02T02:56:51.118Z Db(167) Hostd[1001392583]: [Originator@6876 sub=AdapterServer] Vdt span started, activation is <<527066d1-5c96-4854-ebdb-fec666747e0e, <TCP '127.0.0.1 : 8307'>, <TCP '127.0.0.1 : 28140'>>, ha-root-pool, vim.ManagedEntity.GetName>, method name is GetName",
+ "level": "debug"
+ },
+ {
+ "line": "2022-06-02T02:56:51.640Z In(14) vmsyslogd[1001390391]: Logs rotated. 2022-06-02T02:54:42.721Z - time the service was last started 2022-06-02T02:54:42.708Z, Section for VMware ESX, pid=1001391976, version=8.0.0, build=19833347, option=BETA"
+ },
+ {
+ "line": "2022-06-02T02:15:22.987Z In(166) Hostd[1001392061]: info -[1001392061] [Originator@6876 sub=Default] Supported VMs 640"
+ },
+ {
+ "line": "2022-06-02T03:20:05.107Z Db(167) Hostd[1001392035]: [Originator@6876 sub=AdapterServer opID=531c52d7-9d8a sid=52806149 user=vpxuser:<no user>] New request: target='vim.HostSystem:ha-host', method='retrieveInternalCapability', session='52806149-fe15-f6ff-7685-353ae5d93dcc'"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/vmw_py_log.json b/src/formats/vmw_py_log.json
new file mode 100644
index 0000000..0ce5ed4
--- /dev/null
+++ b/src/formats/vmw_py_log.json
@@ -0,0 +1,45 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "vmw_py_log": {
+ "title": "VMware vSphere log format",
+ "description": "The log format for some VMware vSphere services",
+ "url": "http://kb.vmware.com/kb/2000988",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{1,3})?(?: (?:AM|PM) UTC)?) \\[(?<pid>\\d+)\\](?<level>ERROR|WARNING|INFO|DEBUG):(?<module>[\\w\\-\\.]+):(?<body>.*$)"
+ }
+ },
+ "level-field": "level",
+ "timestamp-field": "timestamp",
+ "level": {
+ "error": "ERROR",
+ "debug": "DEBUG",
+ "info": "INFO",
+ "warning": "WARNING"
+ },
+ "value": {
+ "pid": {
+ "kind": "integer",
+ "identifier": true,
+ "foreign-key": true,
+ "description": "The ID of the process that generated the message"
+ },
+ "module": {
+ "kind": "string",
+ "identifier": true,
+ "description": "The name of the module that generated the message"
+ }
+ },
+ "sample": [
+ {
+ "line": "2015-04-24T21:09:29.296 [25376]INFO:somemodule:Something very INFOrmative."
+ },
+ {
+ "line": "2022-06-01T13:23:25.515 [2376]DEBUG:vmware.vherd.base.detwist:method = com.vmware.appliance.version1.networking.interfaces.list, args = ()"
+ },
+ {
+ "line": "2022-06-01T13:23:25.31 [2376]DEBUG:com.vmware.vherd.base.detwist:method = com.vmware.appliance.version1.system.version.get, args = ()"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/formats/vmw_vc_svc_log.json b/src/formats/vmw_vc_svc_log.json
new file mode 100644
index 0000000..ed507f7
--- /dev/null
+++ b/src/formats/vmw_vc_svc_log.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "vmw_vc_svc_log": {
+ "title": "VMware Go Log",
+ "description": "Log files for go-based logs",
+ "url": "https://docs.vmware.com/en/VMware-vSphere/7.0/vmware-vsphere-with-tanzu/GUID-2A989D79-463C-4EC8-A5F2-CDC3A2C827FB.html",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{3}Z)\\s+(?<level>\\w+)\\s+(?<module>\\w+)\\s\\[(?<srcfile>[^:]+):(?<srcline>\\d+)\\](\\s+\\[opID=(?<opid>[^\\]]+)\\])?\\s+(?<body>.*)"
+ }
+ },
+ "opid-field": "opid",
+ "value": {
+ "body": {
+ "kind": "string"
+ },
+ "opid": {
+ "kind": "string"
+ },
+ "srcfile": {
+ "kind": "string",
+ "identifier": true
+ },
+ "srcline": {
+ "kind": "string",
+ "identifier": true
+ },
+ "module": {
+ "kind": "string",
+ "identifier": true
+ }
+ },
+ "sample": [
+ {
+ "line": "2022-06-02T12:25:11.537Z info wcp [eamagency/util.go:148] [opID=vCLS] Going to read service config",
+ "level": "info"
+ },
+ {
+ "line": "2022-06-02T10:54:45.001Z debug wcp [auth/session.go:156] Removing expired sessions",
+ "level": "debug"
+ },
+ {
+ "line": "2022-06-02T08:35:17.031Z info vlcm [logger/teelogger.go:47] [opID=vapi] Loading Introspection Services",
+ "level": "info"
+ }
+ ]
+ }
+}
diff --git a/src/formats/xmlrpc_log.json b/src/formats/xmlrpc_log.json
new file mode 100644
index 0000000..31638b3
--- /dev/null
+++ b/src/formats/xmlrpc_log.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "xmlrpc_log": {
+ "title": "RHN server XMLRPC log format",
+ "description": "Generated by Satellite's XMLRPC component",
+ "url": "https://access.redhat.com/products/red-hat-satellite",
+ "regex": {
+ "main": {
+ "pattern": "^(?<timestamp>\\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} [+-]?\\d{2}:\\d{2}) (?<pid>\\d+) (?<client_ip>\\S+): (?<module>\\w+)/(?<function>.*)(?<arguments>\\(.*?\\))?(?<body>.*)$"
+ }
+ },
+ "value": {
+ "pid": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "client_ip": {
+ "kind": "string",
+ "identifier": true
+ },
+ "module": {
+ "kind": "string"
+ },
+ "function": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "2015/05/24 07:48:21 -05:00 767 10.206.22.17: xmlrpc/up2date.listChannels(1000011979,)"
+ },
+ {
+ "line": "2015/05/24 07:48:22 -05:00 1377 10.184.37.105: xmlrpc/registration.welcome_message('lang: None',)"
+ },
+ {
+ "line": "2015/05/24 07:48:22 -05:00 759 10.49.10.30: xmlrpc/registration.register_osad"
+ },
+ {
+ "line": "2015/05/24 07:48:22 -05:00 759 10.49.10.30: rhnServer/server_certificate.valid('Server id ID-1000019942 not found in database',)"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/src/fs-extension-functions.cc b/src/fs-extension-functions.cc
new file mode 100644
index 0000000..a9d34fc
--- /dev/null
+++ b/src/fs-extension-functions.cc
@@ -0,0 +1,260 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file fs-extension-functions.cc
+ */
+
+#include <string>
+
+#include <errno.h>
+#include <stddef.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/param.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "sqlite-extension-func.hh"
+#include "sqlite3.h"
+#include "vtab_module.hh"
+
+using namespace mapbox;
+
+static util::variant<const char*, string_fragment>
+sql_basename(const char* path_in)
+{
+ int text_end = -1;
+
+ if (path_in[0] == '\0') {
+ return ".";
+ }
+
+ for (ssize_t lpc = strlen(path_in) - 1; lpc >= 0; lpc--) {
+ if (path_in[lpc] == '/' || path_in[lpc] == '\\') {
+ if (text_end != -1) {
+ return string_fragment(path_in, lpc + 1, text_end);
+ }
+ } else if (text_end == -1) {
+ text_end = (int) (lpc + 1);
+ }
+ }
+
+ if (text_end == -1) {
+ return "/";
+ } else {
+ return string_fragment(path_in, 0, text_end);
+ }
+}
+
+static util::variant<const char*, string_fragment>
+sql_dirname(const char* path_in)
+{
+ ssize_t text_end;
+
+ text_end = strlen(path_in) - 1;
+ while (text_end >= 0
+ && (path_in[text_end] == '/' || path_in[text_end] == '\\'))
+ {
+ text_end -= 1;
+ }
+
+ while (text_end >= 0) {
+ if (path_in[text_end] == '/' || path_in[text_end] == '\\') {
+ return string_fragment(path_in, 0, text_end == 0 ? 1 : text_end);
+ }
+
+ text_end -= 1;
+ }
+
+ return path_in[0] == '/' ? "/" : ".";
+}
+
+static nonstd::optional<std::string>
+sql_joinpath(const std::vector<const char*>& paths)
+{
+ std::string full_path;
+
+ if (paths.empty()) {
+ return nonstd::nullopt;
+ }
+
+ for (auto& path_in : paths) {
+ if (path_in == nullptr) {
+ return nonstd::nullopt;
+ }
+
+ if (path_in[0] == '/' || path_in[0] == '\\') {
+ full_path.clear();
+ }
+ if (!full_path.empty() && full_path[full_path.length() - 1] != '/'
+ && full_path[full_path.length() - 1] != '\\')
+ {
+ full_path += "/";
+ }
+ full_path += path_in;
+ }
+
+ return full_path;
+}
+
+static std::string
+sql_readlink(const char* path)
+{
+ struct stat st;
+
+ if (lstat(path, &st) == -1) {
+ throw sqlite_func_error(
+ "unable to stat path: {} -- {}", path, strerror(errno));
+ }
+
+ char buf[st.st_size];
+ ssize_t rc;
+
+ rc = readlink(path, buf, sizeof(buf));
+ if (rc < 0) {
+ if (errno == EINVAL) {
+ return path;
+ }
+ throw sqlite_func_error(
+ "unable to read link: {} -- {}", path, strerror(errno));
+ }
+
+ return std::string(buf, rc);
+}
+
+static std::string
+sql_realpath(const char* path)
+{
+ char resolved_path[PATH_MAX];
+
+ if (realpath(path, resolved_path) == nullptr) {
+ throw sqlite_func_error(
+ "Could not get real path for {} -- {}", path, strerror(errno));
+ }
+
+ return resolved_path;
+}
+
+int
+fs_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs)
+{
+ static struct FuncDef fs_funcs[] = {
+
+ sqlite_func_adapter<decltype(&sql_basename), sql_basename>::builder(
+ help_text("basename", "Extract the base portion of a pathname.")
+ .sql_function()
+ .with_parameter({"path", "The path"})
+ .with_tags({"filename"})
+ .with_example({"To get the base of a plain file name",
+ "SELECT basename('foobar')"})
+ .with_example(
+ {"To get the base of a path", "SELECT basename('foo/bar')"})
+ .with_example({"To get the base of a directory",
+ "SELECT basename('foo/bar/')"})
+ .with_example({"To get the base of an empty string",
+ "SELECT basename('')"})
+ .with_example({"To get the base of a Windows path",
+ "SELECT basename('foo\\bar')"})
+ .with_example({"To get the base of the root directory",
+ "SELECT basename('/')"})),
+
+ sqlite_func_adapter<decltype(&sql_dirname), sql_dirname>::builder(
+ help_text("dirname", "Extract the directory portion of a pathname.")
+ .sql_function()
+ .with_parameter({"path", "The path"})
+ .with_tags({"filename"})
+ .with_example({"To get the directory of a relative file path",
+ "SELECT dirname('foo/bar')"})
+ .with_example({"To get the directory of an absolute file path",
+ "SELECT dirname('/foo/bar')"})
+ .with_example(
+ {"To get the directory of a file in the root directory",
+ "SELECT dirname('/bar')"})
+ .with_example({"To get the directory of a Windows path",
+ "SELECT dirname('foo\\bar')"})
+ .with_example({"To get the directory of an empty path",
+ "SELECT dirname('')"})),
+
+ sqlite_func_adapter<decltype(&sql_joinpath), sql_joinpath>::builder(
+ help_text("joinpath", "Join components of a path together.")
+ .sql_function()
+ .with_parameter(
+ help_text(
+ "path",
+ "One or more path components to join together. "
+ "If an argument starts with a forward or backward "
+ "slash, it will be considered "
+ "an absolute path and any preceding elements will "
+ "be ignored.")
+ .one_or_more())
+ .with_tags({"filename"})
+ .with_example(
+ {"To join a directory and file name into a relative path",
+ "SELECT joinpath('foo', 'bar')"})
+ .with_example(
+ {"To join an empty component with other names into "
+ "a relative path",
+ "SELECT joinpath('', 'foo', 'bar')"})
+ .with_example(
+ {"To create an absolute path with two path components",
+ "SELECT joinpath('/', 'foo', 'bar')"})
+ .with_example(
+ {"To create an absolute path from a path component "
+ "that starts with a forward slash",
+ "SELECT joinpath('/', 'foo', '/bar')"})),
+
+ sqlite_func_adapter<decltype(&sql_readlink), sql_readlink>::builder(
+ help_text("readlink", "Read the target of a symbolic link.")
+ .sql_function()
+ .with_parameter({"path", "The path to the symbolic link."})
+ .with_tags({"filename"})),
+
+ sqlite_func_adapter<decltype(&sql_realpath), sql_realpath>::builder(
+ help_text(
+ "realpath",
+ "Returns the resolved version of the given path, expanding "
+ "symbolic links and "
+ "resolving '.' and '..' references.")
+ .sql_function()
+ .with_parameter({"path", "The path to resolve."})
+ .with_tags({"filename"})),
+
+ /*
+ * TODO: add other functions like normpath, ...
+ */
+
+ {nullptr},
+ };
+
+ *basic_funcs = fs_funcs;
+ *agg_funcs = nullptr;
+
+ return SQLITE_OK;
+}
diff --git a/src/fstat_vtab.cc b/src/fstat_vtab.cc
new file mode 100644
index 0000000..09567c7
--- /dev/null
+++ b/src/fstat_vtab.cc
@@ -0,0 +1,368 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "fstat_vtab.hh"
+
+#include <glob.h>
+#include <grp.h>
+#include <pwd.h>
+#include <string.h>
+#include <sys/stat.h>
+
+#include "base/auto_mem.hh"
+#include "base/injector.hh"
+#include "base/lnav_log.hh"
+#include "bound_tags.hh"
+#include "config.h"
+#include "sql_util.hh"
+#include "vtab_module.hh"
+
+enum {
+ FSTAT_COL_PARENT,
+ FSTAT_COL_NAME,
+ FSTAT_COL_DEV,
+ FSTAT_COL_INO,
+ FSTAT_COL_TYPE,
+ FSTAT_COL_MODE,
+ FSTAT_COL_NLINK,
+ FSTAT_COL_UID,
+ FSTAT_COL_USER,
+ FSTAT_COL_GID,
+ FSTAT_COL_GROUP,
+ FSTAT_COL_RDEV,
+ FSTAT_COL_SIZE,
+ FSTAT_COL_BLKSIZE,
+ FSTAT_COL_BLOCKS,
+ FSTAT_COL_ATIME,
+ FSTAT_COL_MTIME,
+ FSTAT_COL_CTIME,
+ FSTAT_COL_PATTERN,
+};
+
+/**
+ * @feature f0:sql.tables.fstat
+ */
+struct fstat_table {
+ static constexpr const char* NAME = "fstat";
+ static constexpr const char* CREATE_STMT = R"(
+CREATE TABLE fstat (
+ st_parent TEXT,
+ st_name TEXT,
+ st_dev INTEGER,
+ st_ino INTEGER,
+ st_type TEXT,
+ st_mode INTEGER,
+ st_nlink INTEGER,
+ st_uid TEXT,
+ st_user TEXT,
+ st_gid TEXT,
+ st_group TEXT,
+ st_rdev INTEGER,
+ st_size INTEGER,
+ st_blksize INTEGER,
+ st_blocks INTEGER,
+ st_atime DATETIME,
+ st_mtime DATETIME,
+ st_ctime DATETIME,
+ pattern TEXT HIDDEN
+);
+)";
+
+ struct cursor {
+ sqlite3_vtab_cursor base;
+ std::string c_pattern;
+ static_root_mem<glob_t, globfree> c_glob;
+ size_t c_path_index{0};
+ struct stat c_stat;
+
+ cursor(sqlite3_vtab* vt) : base({vt})
+ {
+ memset(&this->c_stat, 0, sizeof(this->c_stat));
+ }
+
+ void load_stat()
+ {
+ while ((this->c_path_index < this->c_glob->gl_pathc)
+ && lstat(this->c_glob->gl_pathv[this->c_path_index],
+ &this->c_stat)
+ == -1)
+ {
+ this->c_path_index += 1;
+ }
+ }
+
+ int next()
+ {
+ if (this->c_path_index < this->c_glob->gl_pathc) {
+ this->c_path_index += 1;
+ this->load_stat();
+ }
+
+ return SQLITE_OK;
+ }
+
+ int reset() { return SQLITE_OK; }
+
+ int eof() { return this->c_path_index >= this->c_glob->gl_pathc; }
+
+ int get_rowid(sqlite3_int64& rowid_out)
+ {
+ rowid_out = this->c_path_index;
+
+ return SQLITE_OK;
+ }
+ };
+
+ int get_column(const cursor& vc, sqlite3_context* ctx, int col)
+ {
+ const char* path = vc.c_glob->gl_pathv[vc.c_path_index];
+ char time_buf[32];
+
+ switch (col) {
+ case FSTAT_COL_PARENT: {
+ const char* slash = strrchr(path, '/');
+
+ if (slash == nullptr) {
+ sqlite3_result_text(ctx, ".", 1, SQLITE_STATIC);
+ } else if (path[1] == '\0') {
+ sqlite3_result_text(ctx, "", 0, SQLITE_STATIC);
+ } else {
+ sqlite3_result_text(
+ ctx, path, slash - path + 1, SQLITE_TRANSIENT);
+ }
+ break;
+ }
+ case FSTAT_COL_NAME: {
+ const char* slash = strrchr(path, '/');
+
+ if (slash == nullptr) {
+ sqlite3_result_text(ctx, path, -1, SQLITE_TRANSIENT);
+ } else {
+ sqlite3_result_text(ctx, slash + 1, -1, SQLITE_TRANSIENT);
+ }
+ break;
+ }
+ case FSTAT_COL_DEV:
+ sqlite3_result_int(ctx, vc.c_stat.st_dev);
+ break;
+ case FSTAT_COL_INO:
+ sqlite3_result_int64(ctx, vc.c_stat.st_ino);
+ break;
+ case FSTAT_COL_TYPE:
+ if (S_ISREG(vc.c_stat.st_mode)) {
+ sqlite3_result_text(ctx, "reg", 3, SQLITE_STATIC);
+ } else if (S_ISBLK(vc.c_stat.st_mode)) {
+ sqlite3_result_text(ctx, "blk", 3, SQLITE_STATIC);
+ } else if (S_ISCHR(vc.c_stat.st_mode)) {
+ sqlite3_result_text(ctx, "chr", 3, SQLITE_STATIC);
+ } else if (S_ISDIR(vc.c_stat.st_mode)) {
+ sqlite3_result_text(ctx, "dir", 3, SQLITE_STATIC);
+ } else if (S_ISFIFO(vc.c_stat.st_mode)) {
+ sqlite3_result_text(ctx, "fifo", 4, SQLITE_STATIC);
+ } else if (S_ISLNK(vc.c_stat.st_mode)) {
+ sqlite3_result_text(ctx, "lnk", 3, SQLITE_STATIC);
+ } else if (S_ISSOCK(vc.c_stat.st_mode)) {
+ sqlite3_result_text(ctx, "sock", 3, SQLITE_STATIC);
+ }
+ break;
+ case FSTAT_COL_MODE:
+ sqlite3_result_int(ctx, vc.c_stat.st_mode & 0777);
+ break;
+ case FSTAT_COL_NLINK:
+ sqlite3_result_int(ctx, vc.c_stat.st_nlink);
+ break;
+ case FSTAT_COL_UID:
+ sqlite3_result_int(ctx, vc.c_stat.st_uid);
+ break;
+ case FSTAT_COL_USER: {
+ struct passwd* pw = getpwuid(vc.c_stat.st_uid);
+
+ if (pw != nullptr) {
+ sqlite3_result_text(ctx, pw->pw_name, -1, SQLITE_TRANSIENT);
+ } else {
+ sqlite3_result_int(ctx, vc.c_stat.st_uid);
+ }
+ break;
+ }
+ case FSTAT_COL_GID:
+ sqlite3_result_int(ctx, vc.c_stat.st_gid);
+ break;
+ case FSTAT_COL_GROUP: {
+ struct group* gr = getgrgid(vc.c_stat.st_gid);
+
+ if (gr != nullptr) {
+ sqlite3_result_text(ctx, gr->gr_name, -1, SQLITE_TRANSIENT);
+ } else {
+ sqlite3_result_int(ctx, vc.c_stat.st_gid);
+ }
+ break;
+ }
+ case FSTAT_COL_RDEV:
+ sqlite3_result_int(ctx, vc.c_stat.st_rdev);
+ break;
+ case FSTAT_COL_SIZE:
+ sqlite3_result_int64(ctx, vc.c_stat.st_size);
+ break;
+ case FSTAT_COL_BLKSIZE:
+ sqlite3_result_int(ctx, vc.c_stat.st_blksize);
+ break;
+ case FSTAT_COL_BLOCKS:
+ sqlite3_result_int(ctx, vc.c_stat.st_blocks);
+ break;
+ case FSTAT_COL_ATIME:
+ sql_strftime(time_buf, sizeof(time_buf), vc.c_stat.st_atime, 0);
+ sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT);
+ break;
+ case FSTAT_COL_MTIME:
+ sql_strftime(time_buf, sizeof(time_buf), vc.c_stat.st_mtime, 0);
+ sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT);
+ break;
+ case FSTAT_COL_CTIME:
+ sql_strftime(time_buf, sizeof(time_buf), vc.c_stat.st_ctime, 0);
+ sqlite3_result_text(ctx, time_buf, -1, SQLITE_TRANSIENT);
+ break;
+ case FSTAT_COL_PATTERN:
+ sqlite3_result_text(ctx,
+ vc.c_pattern.c_str(),
+ vc.c_pattern.length(),
+ SQLITE_TRANSIENT);
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+
+#if 0
+ int update_row(sqlite3_vtab *tab,
+ sqlite3_int64 &index,
+ const char *st_parent,
+ const char *st_name,
+ int64_t st_dev,
+ int64_t st_ino,
+ const char *st_type,
+ int64_t st_mode,
+ int64_t st_nlink,
+ int64_t st_uid,
+ const char *st_user,
+ int64_t st_gid,
+ const char *st_group,
+ int64_t st_rdev,
+ int64_t st_size,
+ int64_t st_blksize,
+ int64_t st_blocks,
+ const char *atime,
+ const char *mtime,
+ const char *ctime,
+ const char *pattern) {
+
+ };
+#endif
+};
+
+static int
+rcBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo)
+{
+ vtab_index_constraints vic(pIdxInfo);
+ vtab_index_usage viu(pIdxInfo);
+
+ for (auto iter = vic.begin(); iter != vic.end(); ++iter) {
+ if (iter->op != SQLITE_INDEX_CONSTRAINT_EQ) {
+ continue;
+ }
+
+ switch (iter->iColumn) {
+ case FSTAT_COL_PATTERN:
+ viu.column_used(iter);
+ break;
+ }
+ }
+
+ viu.allocate_args(FSTAT_COL_PATTERN, FSTAT_COL_PATTERN, 1);
+ return SQLITE_OK;
+}
+
+static int
+rcFilter(sqlite3_vtab_cursor* pVtabCursor,
+ int idxNum,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv)
+{
+ fstat_table::cursor* pCur = (fstat_table::cursor*) pVtabCursor;
+
+ if (argc != 1) {
+ pCur->c_pattern.clear();
+ return SQLITE_OK;
+ }
+
+ const char* pattern = (const char*) sqlite3_value_text(argv[0]);
+ pCur->c_pattern = pattern;
+ switch (glob(pattern,
+#ifdef GLOB_TILDE
+ GLOB_TILDE |
+#endif
+ GLOB_ERR,
+ nullptr,
+ pCur->c_glob.inout()))
+ {
+ case GLOB_NOSPACE:
+ pVtabCursor->pVtab->zErrMsg
+ = sqlite3_mprintf("No space to perform glob()");
+ return SQLITE_ERROR;
+ case GLOB_NOMATCH:
+ return SQLITE_OK;
+ }
+
+ pCur->load_stat();
+
+ return SQLITE_OK;
+}
+
+int
+register_fstat_vtab(sqlite3* db)
+{
+ static vtab_module<tvt_no_update<fstat_table>> FSTAT_MODULE;
+
+ int rc;
+
+ FSTAT_MODULE.vm_module.xBestIndex = rcBestIndex;
+ FSTAT_MODULE.vm_module.xFilter = rcFilter;
+
+ static auto& lnav_flags = injector::get<unsigned long&, lnav_flags_tag>();
+
+ if (lnav_flags & LNF_SECURE_MODE) {
+ return SQLITE_OK;
+ }
+
+ rc = FSTAT_MODULE.create(db, "fstat");
+
+ ensure(rc == SQLITE_OK);
+
+ return rc;
+}
diff --git a/src/fstat_vtab.hh b/src/fstat_vtab.hh
new file mode 100644
index 0000000..556f164
--- /dev/null
+++ b/src/fstat_vtab.hh
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef fstat_vtab_hh
+#define fstat_vtab_hh
+
+#include <sqlite3.h>
+
+int register_fstat_vtab(sqlite3* db);
+
+#endif
diff --git a/src/fts_fuzzy_match.cc b/src/fts_fuzzy_match.cc
new file mode 100644
index 0000000..8ae7098
--- /dev/null
+++ b/src/fts_fuzzy_match.cc
@@ -0,0 +1,227 @@
+// LICENSE
+//
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+
+#include <cstring> // memcpy
+
+#include "fts_fuzzy_match.hh"
+
+#include <ctype.h> // ::tolower, ::toupper
+
+#include "config.h"
+
+namespace fts {
+
+// Forward declarations for "private" implementation
+namespace fuzzy_internal {
+static bool fuzzy_match_recursive(const char* pattern,
+ const char* str,
+ int& outScore,
+ const char* strBegin,
+ uint8_t const* srcMatches,
+ uint8_t* newMatches,
+ int maxMatches,
+ int nextMatch,
+ int& recursionCount,
+ int recursionLimit);
+}
+
+// Public interface
+bool
+fuzzy_match_simple(char const* pattern, char const* str)
+{
+ while (*pattern != '\0' && *str != '\0') {
+ if (tolower(*pattern) == tolower(*str))
+ ++pattern;
+ ++str;
+ }
+
+ return *pattern == '\0' ? true : false;
+}
+
+bool
+fuzzy_match(char const* pattern, char const* str, int& outScore)
+{
+ uint8_t matches[256];
+ return fuzzy_match(pattern, str, outScore, matches, sizeof(matches));
+}
+
+bool
+fuzzy_match(char const* pattern,
+ char const* str,
+ int& outScore,
+ uint8_t* matches,
+ int maxMatches)
+{
+ int recursionCount = 0;
+ int recursionLimit = 10;
+
+ return fuzzy_internal::fuzzy_match_recursive(pattern,
+ str,
+ outScore,
+ str,
+ nullptr,
+ matches,
+ maxMatches,
+ 0,
+ recursionCount,
+ recursionLimit);
+}
+
+// Private implementation
+static bool
+fuzzy_internal::fuzzy_match_recursive(const char* pattern,
+ const char* str,
+ int& outScore,
+ const char* strBegin,
+ uint8_t const* srcMatches,
+ uint8_t* matches,
+ int maxMatches,
+ int nextMatch,
+ int& recursionCount,
+ int recursionLimit)
+{
+ // Count recursions
+ ++recursionCount;
+ if (recursionCount >= recursionLimit)
+ return false;
+
+ // Detect end of strings
+ if (*pattern == '\0' || *str == '\0')
+ return false;
+
+ // Recursion params
+ bool recursiveMatch = false;
+ uint8_t bestRecursiveMatches[256];
+ int bestRecursiveScore = 0;
+
+ // Loop through pattern and str looking for a match
+ bool first_match = true;
+ while (*pattern != '\0' && *str != '\0') {
+ // Found match
+ if (tolower(*pattern) == tolower(*str)) {
+ // Supplied matches buffer was too short
+ if (nextMatch >= maxMatches)
+ return false;
+
+ // "Copy-on-Write" srcMatches into matches
+ if (first_match && srcMatches) {
+ memcpy(matches, srcMatches, nextMatch);
+ first_match = false;
+ }
+
+ // Recursive call that "skips" this match
+ uint8_t recursiveMatches[256];
+ int recursiveScore;
+ if (fuzzy_match_recursive(pattern,
+ str + 1,
+ recursiveScore,
+ strBegin,
+ matches,
+ recursiveMatches,
+ sizeof(recursiveMatches),
+ nextMatch,
+ recursionCount,
+ recursionLimit))
+ {
+ // Pick best recursive score
+ if (!recursiveMatch || recursiveScore > bestRecursiveScore) {
+ memcpy(bestRecursiveMatches, recursiveMatches, 256);
+ bestRecursiveScore = recursiveScore;
+ }
+ recursiveMatch = true;
+ }
+
+ // Advance
+ matches[nextMatch++] = (uint8_t) (str - strBegin);
+ ++pattern;
+ }
+ ++str;
+ }
+
+ // Determine if full pattern was matched
+ bool matched = *pattern == '\0' ? true : false;
+
+ // Calculate score
+ if (matched) {
+ const int sequential_bonus = 15; // bonus for adjacent matches
+ const int separator_bonus
+ = 30; // bonus if match occurs after a separator
+ const int camel_bonus
+ = 30; // bonus if match is uppercase and prev is lower
+ const int first_letter_bonus
+ = 15; // bonus if the first letter is matched
+
+ const int leading_letter_penalty
+ = -5; // penalty applied for every letter in str before the first
+ // match
+ const int max_leading_letter_penalty
+ = -15; // maximum penalty for leading letters
+ const int unmatched_letter_penalty
+ = -1; // penalty for every letter that doesn't matter
+
+ // Iterate str to end
+ while (*str != '\0')
+ ++str;
+
+ // Initialize score
+ outScore = 100;
+
+ // Apply leading letter penalty
+ int penalty = leading_letter_penalty * matches[0];
+ if (penalty < max_leading_letter_penalty)
+ penalty = max_leading_letter_penalty;
+ outScore += penalty;
+
+ // Apply unmatched penalty
+ int unmatched = (int) (str - strBegin) - nextMatch;
+ outScore += unmatched_letter_penalty * unmatched;
+
+ // Apply ordering bonuses
+ for (int i = 0; i < nextMatch; ++i) {
+ uint8_t currIdx = matches[i];
+
+ if (i > 0) {
+ uint8_t prevIdx = matches[i - 1];
+
+ // Sequential
+ if (currIdx == (prevIdx + 1))
+ outScore += sequential_bonus;
+ }
+
+ // Check for bonuses based on neighbor character value
+ if (currIdx > 0) {
+ // Camel case
+ char neighbor = strBegin[currIdx - 1];
+ char curr = strBegin[currIdx];
+ if (::islower(neighbor) && ::isupper(curr))
+ outScore += camel_bonus;
+
+ // Separator
+ bool neighborSeparator = neighbor == '_' || neighbor == ' ';
+ if (neighborSeparator)
+ outScore += separator_bonus;
+ } else {
+ // First letter
+ outScore += first_letter_bonus;
+ }
+ }
+ }
+
+ // Return best result
+ if (recursiveMatch && (!matched || bestRecursiveScore > outScore)) {
+ // Recursive score is better than "this"
+ memcpy(matches, bestRecursiveMatches, maxMatches);
+ outScore = bestRecursiveScore;
+ return true;
+ } else if (matched) {
+ // "this" score is better than recursive
+ return true;
+ } else {
+ // no match
+ return false;
+ }
+}
+} // namespace fts
diff --git a/src/fts_fuzzy_match.hh b/src/fts_fuzzy_match.hh
new file mode 100644
index 0000000..1d3b013
--- /dev/null
+++ b/src/fts_fuzzy_match.hh
@@ -0,0 +1,50 @@
+// LICENSE
+//
+// This software is dual-licensed to the public domain and under the following
+// license: you are granted a perpetual, irrevocable license to copy, modify,
+// publish, and distribute this file as you see fit.
+//
+// VERSION
+// 0.2.0 (2017-02-18) Scored matches perform exhaustive search for best
+// score 0.1.0 (2016-03-28) Initial release
+//
+// AUTHOR
+// Forrest Smith
+//
+// NOTES
+// Compiling
+// You MUST add '#define FTS_FUZZY_MATCH_IMPLEMENTATION' before including
+// this header in ONE source file to create implementation.
+//
+// fuzzy_match_simple(...)
+// Returns true if each character in pattern is found sequentially within
+// str
+//
+// fuzzy_match(...)
+// Returns true if pattern is found AND calculates a score.
+// Performs exhaustive search via recursion to find all possible matches and
+// match with highest score. Scores values have no intrinsic meaning.
+// Possible score range is not normalized and varies with pattern. Recursion
+// is limited internally (default=10) to prevent degenerate cases
+// (pattern="aaaaaa" str="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") Uses uint8_t for
+// match indices. Therefore patterns are limited to 256 characters. Score
+// system should be tuned for YOUR use case. Words, sentences, file names,
+// or method names all prefer different tuning.
+
+#ifndef FTS_FUZZY_MATCH_H
+#define FTS_FUZZY_MATCH_H
+
+#include <cstdint> // uint8_t
+
+// Public interface
+namespace fts {
+bool fuzzy_match_simple(char const* pattern, char const* str);
+bool fuzzy_match(char const* pattern, char const* str, int& outScore);
+bool fuzzy_match(char const* pattern,
+ char const* str,
+ int& outScore,
+ uint8_t* matches,
+ int maxMatches);
+} // namespace fts
+
+#endif // FTS_FUZZY_MATCH_H
diff --git a/src/ghc/filesystem.hpp b/src/ghc/filesystem.hpp
new file mode 100644
index 0000000..9540b44
--- /dev/null
+++ b/src/ghc/filesystem.hpp
@@ -0,0 +1,6049 @@
+//---------------------------------------------------------------------------------------
+//
+// ghc::filesystem - A C++17-like filesystem implementation for C++11/C++14/C++17/C++20
+//
+//---------------------------------------------------------------------------------------
+//
+// Copyright (c) 2018, Steffen Schümann <s.schuemann@pobox.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+//---------------------------------------------------------------------------------------
+//
+// To dynamically select std::filesystem where available on most platforms,
+// you could use:
+//
+// #if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
+// #if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
+// #define GHC_USE_STD_FS
+// #include <filesystem>
+// namespace fs = std::filesystem;
+// #endif
+// #endif
+// #ifndef GHC_USE_STD_FS
+// #include <ghc/filesystem.hpp>
+// namespace fs = ghc::filesystem;
+// #endif
+//
+//---------------------------------------------------------------------------------------
+#ifndef GHC_FILESYSTEM_H
+#define GHC_FILESYSTEM_H
+
+// #define BSD manifest constant only in
+// sys/param.h
+#ifndef _WIN32
+#include <sys/param.h>
+#endif
+
+#ifndef GHC_OS_DETECTED
+#if defined(__APPLE__) && defined(__MACH__)
+#define GHC_OS_MACOS
+#elif defined(__linux__)
+#define GHC_OS_LINUX
+#if defined(__ANDROID__)
+#define GHC_OS_ANDROID
+#endif
+#elif defined(_WIN64)
+#define GHC_OS_WINDOWS
+#define GHC_OS_WIN64
+#elif defined(_WIN32)
+#define GHC_OS_WINDOWS
+#define GHC_OS_WIN32
+#elif defined(__CYGWIN__)
+#define GHC_OS_CYGWIN
+#elif defined(__sun) && defined(__SVR4)
+#define GHC_OS_SOLARIS
+#elif defined(__svr4__)
+#define GHC_OS_SYS5R4
+#elif defined(BSD)
+#define GHC_OS_BSD
+#elif defined(__EMSCRIPTEN__)
+#define GHC_OS_WEB
+#include <wasi/api.h>
+#elif defined(__QNX__)
+#define GHC_OS_QNX
+#else
+#error "Operating system currently not supported!"
+#endif
+#define GHC_OS_DETECTED
+#if (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
+#if _MSVC_LANG == 201703L
+#define GHC_FILESYSTEM_RUNNING_CPP17
+#else
+#define GHC_FILESYSTEM_RUNNING_CPP20
+#endif
+#elif (defined(__cplusplus) && __cplusplus >= 201703L)
+#if __cplusplus == 201703L
+#define GHC_FILESYSTEM_RUNNING_CPP17
+#else
+#define GHC_FILESYSTEM_RUNNING_CPP20
+#endif
+#endif
+#endif
+
+#if defined(GHC_FILESYSTEM_IMPLEMENTATION)
+#define GHC_EXPAND_IMPL
+#define GHC_INLINE
+#ifdef GHC_OS_WINDOWS
+#ifndef GHC_FS_API
+#define GHC_FS_API
+#endif
+#ifndef GHC_FS_API_CLASS
+#define GHC_FS_API_CLASS
+#endif
+#else
+#ifndef GHC_FS_API
+#define GHC_FS_API __attribute__((visibility("default")))
+#endif
+#ifndef GHC_FS_API_CLASS
+#define GHC_FS_API_CLASS __attribute__((visibility("default")))
+#endif
+#endif
+#elif defined(GHC_FILESYSTEM_FWD)
+#define GHC_INLINE
+#ifdef GHC_OS_WINDOWS
+#ifndef GHC_FS_API
+#define GHC_FS_API extern
+#endif
+#ifndef GHC_FS_API_CLASS
+#define GHC_FS_API_CLASS
+#endif
+#else
+#ifndef GHC_FS_API
+#define GHC_FS_API extern
+#endif
+#ifndef GHC_FS_API_CLASS
+#define GHC_FS_API_CLASS
+#endif
+#endif
+#else
+#define GHC_EXPAND_IMPL
+#define GHC_INLINE inline
+#ifndef GHC_FS_API
+#define GHC_FS_API
+#endif
+#ifndef GHC_FS_API_CLASS
+#define GHC_FS_API_CLASS
+#endif
+#endif
+
+#ifdef GHC_EXPAND_IMPL
+
+#ifdef GHC_OS_WINDOWS
+#include <windows.h>
+// additional includes
+#include <shellapi.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <wchar.h>
+#include <winioctl.h>
+#else
+#include <dirent.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <sys/param.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <unistd.h>
+#ifdef GHC_OS_ANDROID
+#include <android/api-level.h>
+#if __ANDROID_API__ < 12
+#include <sys/syscall.h>
+#endif
+#include <sys/vfs.h>
+#define statvfs statfs
+#else
+#include <sys/statvfs.h>
+#endif
+#ifdef GHC_OS_CYGWIN
+#include <strings.h>
+#endif
+#if !defined(__ANDROID__) || __ANDROID_API__ >= 26
+#include <langinfo.h>
+#endif
+#endif
+#ifdef GHC_OS_MACOS
+#include <Availability.h>
+#endif
+
+#if defined(__cpp_impl_three_way_comparison) && defined(__has_include)
+#if __has_include(<compare>)
+#define GHC_HAS_THREEWAY_COMP
+#include <compare>
+#endif
+#endif
+
+#include <algorithm>
+#include <cctype>
+#include <chrono>
+#include <clocale>
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <functional>
+#include <memory>
+#include <stack>
+#include <stdexcept>
+#include <string>
+#include <system_error>
+#include <type_traits>
+#include <utility>
+#include <vector>
+
+#else // GHC_EXPAND_IMPL
+
+#if defined(__cpp_impl_three_way_comparison) && defined(__has_include)
+#if __has_include(<compare>)
+#define GHC_HAS_THREEWAY_COMP
+#include <compare>
+#endif
+#endif
+#include <chrono>
+#include <fstream>
+#include <memory>
+#include <stack>
+#include <stdexcept>
+#include <string>
+#include <system_error>
+#ifdef GHC_OS_WINDOWS
+#include <vector>
+#endif
+#endif // GHC_EXPAND_IMPL
+
+// After standard library includes.
+// Standard library support for std::string_view.
+#if defined(__cpp_lib_string_view)
+#define GHC_HAS_STD_STRING_VIEW
+#elif defined(_LIBCPP_VERSION) && (_LIBCPP_VERSION >= 4000) && (__cplusplus >= 201402)
+#define GHC_HAS_STD_STRING_VIEW
+#elif defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE >= 7) && (__cplusplus >= 201703)
+#define GHC_HAS_STD_STRING_VIEW
+#elif defined(_MSC_VER) && (_MSC_VER >= 1910 && _MSVC_LANG >= 201703)
+#define GHC_HAS_STD_STRING_VIEW
+#endif
+
+// Standard library support for std::experimental::string_view.
+#if defined(_LIBCPP_VERSION) && (_LIBCPP_VERSION >= 3700 && _LIBCPP_VERSION < 7000) && (__cplusplus >= 201402)
+#define GHC_HAS_STD_EXPERIMENTAL_STRING_VIEW
+#elif defined(__GNUC__) && (((__GNUC__ == 4) && (__GNUC_MINOR__ >= 9)) || (__GNUC__ > 4)) && (__cplusplus >= 201402)
+#define GHC_HAS_STD_EXPERIMENTAL_STRING_VIEW
+#elif defined(__GLIBCXX__) && defined(_GLIBCXX_USE_DUAL_ABI) && (__cplusplus >= 201402)
+// macro _GLIBCXX_USE_DUAL_ABI is always defined in libstdc++ from gcc-5 and newer
+#define GHC_HAS_STD_EXPERIMENTAL_STRING_VIEW
+#endif
+
+#if defined(GHC_HAS_STD_STRING_VIEW)
+#include <string_view>
+#elif defined(GHC_HAS_STD_EXPERIMENTAL_STRING_VIEW)
+#include <experimental/string_view>
+#endif
+
+#if !defined(GHC_OS_WINDOWS) && !defined(PATH_MAX)
+#define PATH_MAX 4096
+#endif
+
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// Behaviour Switches (see README.md, should match the config in test/filesystem_test.cpp):
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// Enforce C++17 API where possible when compiling for C++20, handles the following cases:
+// * fs::path::u8string() returns std::string instead of std::u8string
+// #define GHC_FILESYSTEM_ENFORCE_CPP17_API
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// LWG #2682 disables the since then invalid use of the copy option create_symlinks on directories
+// configure LWG conformance ()
+#define LWG_2682_BEHAVIOUR
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// LWG #2395 makes crate_directory/create_directories not emit an error if there is a regular
+// file with that name, it is superseded by P1164R1, so only activate if really needed
+// #define LWG_2935_BEHAVIOUR
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// LWG #2936 enables new element wise (more expensive) path comparison
+// * if this->root_name().native().compare(p.root_name().native()) != 0 return result
+// * if this->has_root_directory() and !p.has_root_directory() return -1
+// * if !this->has_root_directory() and p.has_root_directory() return -1
+// * else result of element wise comparison of path iteration where first comparison is != 0 or 0
+// if all comparisons are 0 (on Windows this implementation does case-insensitive root_name()
+// comparison)
+#define LWG_2936_BEHAVIOUR
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// LWG #2937 enforces that fs::equivalent emits an error, if !fs::exists(p1)||!exists(p2)
+#define LWG_2937_BEHAVIOUR
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// UTF8-Everywhere is the original behaviour of ghc::filesystem. But since v1.5 the Windows
+// version defaults to std::wstring storage backend. Still all std::string will be interpreted
+// as UTF-8 encoded. With this define you can enforce the old behavior on Windows, using
+// std::string as backend and for fs::path::native() and char for fs::path::c_str(). This
+// needs more conversions, so it is (and was before v1.5) slower, bot might help keeping source
+// homogeneous in a multi-platform project.
+// #define GHC_WIN_DISABLE_WSTRING_STORAGE_TYPE
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// Raise errors/exceptions when invalid unicode codepoints or UTF-8 sequences are found,
+// instead of replacing them with the unicode replacement character (U+FFFD).
+// #define GHC_RAISE_UNICODE_ERRORS
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+// Automatic prefix windows path with "\\?\" if they would break the MAX_PATH length.
+// instead of replacing them with the unicode replacement character (U+FFFD).
+#ifndef GHC_WIN_DISABLE_AUTO_PREFIXES
+#define GHC_WIN_AUTO_PREFIX_LONG_PATH
+#endif // GHC_WIN_DISABLE_AUTO_PREFIXES
+//- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
+
+// ghc::filesystem version in decimal (major * 10000 + minor * 100 + patch)
+#define GHC_FILESYSTEM_VERSION 10512L
+
+#if !defined(GHC_WITH_EXCEPTIONS) && (defined(__EXCEPTIONS) || defined(__cpp_exceptions) || defined(_CPPUNWIND))
+#define GHC_WITH_EXCEPTIONS
+#endif
+#if !defined(GHC_WITH_EXCEPTIONS) && defined(GHC_RAISE_UNICODE_ERRORS)
+#error "Can't raise unicode errors with exception support disabled"
+#endif
+
+namespace ghc {
+namespace filesystem {
+
+#if defined(GHC_HAS_CUSTOM_STRING_VIEW)
+#define GHC_WITH_STRING_VIEW
+#elif defined(GHC_HAS_STD_STRING_VIEW)
+#define GHC_WITH_STRING_VIEW
+using std::basic_string_view;
+#elif defined(GHC_HAS_STD_EXPERIMENTAL_STRING_VIEW)
+#define GHC_WITH_STRING_VIEW
+using std::experimental::basic_string_view;
+#endif
+
+// temporary existing exception type for yet unimplemented parts
+class GHC_FS_API_CLASS not_implemented_exception : public std::logic_error
+{
+public:
+ not_implemented_exception()
+ : std::logic_error("function not implemented yet.")
+ {
+ }
+};
+
+template <typename char_type>
+class path_helper_base
+{
+public:
+ using value_type = char_type;
+#ifdef GHC_OS_WINDOWS
+ static constexpr value_type preferred_separator = '\\';
+#else
+ static constexpr value_type preferred_separator = '/';
+#endif
+};
+
+#if __cplusplus < 201703L
+template <typename char_type>
+constexpr char_type path_helper_base<char_type>::preferred_separator;
+#endif
+
+#ifdef GHC_OS_WINDOWS
+class path;
+namespace detail {
+bool has_executable_extension(const path& p);
+}
+#endif
+
+// [fs.class.path] class path
+class GHC_FS_API_CLASS path
+#if defined(GHC_OS_WINDOWS) && !defined(GHC_WIN_DISABLE_WSTRING_STORAGE_TYPE)
+#define GHC_USE_WCHAR_T
+#define GHC_NATIVEWP(p) p.c_str()
+#define GHC_PLATFORM_LITERAL(str) L##str
+ : private path_helper_base<std::wstring::value_type>
+{
+public:
+ using path_helper_base<std::wstring::value_type>::value_type;
+#else
+#define GHC_NATIVEWP(p) p.wstring().c_str()
+#define GHC_PLATFORM_LITERAL(str) str
+ : private path_helper_base<std::string::value_type>
+{
+public:
+ using path_helper_base<std::string::value_type>::value_type;
+#endif
+ using string_type = std::basic_string<value_type>;
+ using path_helper_base<value_type>::preferred_separator;
+
+ // [fs.enum.path.format] enumeration format
+ /// The path format in which the constructor argument is given.
+ enum format {
+ generic_format, ///< The generic format, internally used by
+ ///< ghc::filesystem::path with slashes
+ native_format, ///< The format native to the current platform this code
+ ///< is build for
+ auto_format, ///< Try to auto-detect the format, fallback to native
+ };
+
+ template <class T>
+ struct _is_basic_string : std::false_type
+ {
+ };
+ template <class CharT, class Traits, class Alloc>
+ struct _is_basic_string<std::basic_string<CharT, Traits, Alloc>> : std::true_type
+ {
+ };
+ template <class CharT>
+ struct _is_basic_string<std::basic_string<CharT, std::char_traits<CharT>, std::allocator<CharT>>> : std::true_type
+ {
+ };
+#ifdef GHC_WITH_STRING_VIEW
+ template <class CharT, class Traits>
+ struct _is_basic_string<basic_string_view<CharT, Traits>> : std::true_type
+ {
+ };
+ template <class CharT>
+ struct _is_basic_string<basic_string_view<CharT, std::char_traits<CharT>>> : std::true_type
+ {
+ };
+#endif
+
+ template <typename T1, typename T2 = void>
+ using path_type = typename std::enable_if<!std::is_same<path, T1>::value, path>::type;
+ template <typename T>
+#if defined(__cpp_lib_char8_t) && !defined(GHC_FILESYSTEM_ENFORCE_CPP17_API)
+ using path_from_string =
+ typename std::enable_if<_is_basic_string<T>::value || std::is_same<char const*, typename std::decay<T>::type>::value || std::is_same<char*, typename std::decay<T>::type>::value || std::is_same<char8_t const*, typename std::decay<T>::type>::value ||
+ std::is_same<char8_t*, typename std::decay<T>::type>::value || std::is_same<char16_t const*, typename std::decay<T>::type>::value || std::is_same<char16_t*, typename std::decay<T>::type>::value ||
+ std::is_same<char32_t const*, typename std::decay<T>::type>::value || std::is_same<char32_t*, typename std::decay<T>::type>::value || std::is_same<wchar_t const*, typename std::decay<T>::type>::value ||
+ std::is_same<wchar_t*, typename std::decay<T>::type>::value,
+ path>::type;
+ template <typename T>
+ using path_type_EcharT = typename std::enable_if<std::is_same<T, char>::value || std::is_same<T, char8_t>::value || std::is_same<T, char16_t>::value || std::is_same<T, char32_t>::value || std::is_same<T, wchar_t>::value, path>::type;
+#else
+ using path_from_string =
+ typename std::enable_if<_is_basic_string<T>::value || std::is_same<char const*, typename std::decay<T>::type>::value || std::is_same<char*, typename std::decay<T>::type>::value ||
+ std::is_same<char16_t const*, typename std::decay<T>::type>::value || std::is_same<char16_t*, typename std::decay<T>::type>::value || std::is_same<char32_t const*, typename std::decay<T>::type>::value ||
+ std::is_same<char32_t*, typename std::decay<T>::type>::value || std::is_same<wchar_t const*, typename std::decay<T>::type>::value || std::is_same<wchar_t*, typename std::decay<T>::type>::value,
+ path>::type;
+ template <typename T>
+ using path_type_EcharT = typename std::enable_if<std::is_same<T, char>::value || std::is_same<T, char16_t>::value || std::is_same<T, char32_t>::value || std::is_same<T, wchar_t>::value, path>::type;
+#endif
+ // [fs.path.construct] constructors and destructor
+ path() noexcept;
+ path(const path& p);
+ path(path&& p) noexcept;
+ path(string_type&& source, format fmt = auto_format);
+ template <class Source, typename = path_from_string<Source>>
+ path(const Source& source, format fmt = auto_format);
+ template <class InputIterator>
+ path(InputIterator first, InputIterator last, format fmt = auto_format);
+#ifdef GHC_WITH_EXCEPTIONS
+ template <class Source, typename = path_from_string<Source>>
+ path(const Source& source, const std::locale& loc, format fmt = auto_format);
+ template <class InputIterator>
+ path(InputIterator first, InputIterator last, const std::locale& loc, format fmt = auto_format);
+#endif
+ ~path();
+
+ // [fs.path.assign] assignments
+ path& operator=(const path& p);
+ path& operator=(path&& p) noexcept;
+ path& operator=(string_type&& source);
+ path& assign(string_type&& source);
+ template <class Source>
+ path& operator=(const Source& source);
+ template <class Source>
+ path& assign(const Source& source);
+ template <class InputIterator>
+ path& assign(InputIterator first, InputIterator last);
+
+ // [fs.path.append] appends
+ path& operator/=(const path& p);
+ template <class Source>
+ path& operator/=(const Source& source);
+ template <class Source>
+ path& append(const Source& source);
+ template <class InputIterator>
+ path& append(InputIterator first, InputIterator last);
+
+ // [fs.path.concat] concatenation
+ path& operator+=(const path& x);
+ path& operator+=(const string_type& x);
+#ifdef GHC_WITH_STRING_VIEW
+ path& operator+=(basic_string_view<value_type> x);
+#endif
+ path& operator+=(const value_type* x);
+ path& operator+=(value_type x);
+ template <class Source>
+ path_from_string<Source>& operator+=(const Source& x);
+ template <class EcharT>
+ path_type_EcharT<EcharT>& operator+=(EcharT x);
+ template <class Source>
+ path& concat(const Source& x);
+ template <class InputIterator>
+ path& concat(InputIterator first, InputIterator last);
+
+ // [fs.path.modifiers] modifiers
+ void clear() noexcept;
+ path& make_preferred();
+ path& remove_filename();
+ path& replace_filename(const path& replacement);
+ path& replace_extension(const path& replacement = path());
+ void swap(path& rhs) noexcept;
+
+ // [fs.path.native.obs] native format observers
+ const string_type& native() const noexcept;
+ const value_type* c_str() const noexcept;
+ operator string_type() const;
+ template <class EcharT, class traits = std::char_traits<EcharT>, class Allocator = std::allocator<EcharT>>
+ std::basic_string<EcharT, traits, Allocator> string(const Allocator& a = Allocator()) const;
+ std::string string() const;
+ std::wstring wstring() const;
+#if defined(__cpp_lib_char8_t) && !defined(GHC_FILESYSTEM_ENFORCE_CPP17_API)
+ std::u8string u8string() const;
+#else
+ std::string u8string() const;
+#endif
+ std::u16string u16string() const;
+ std::u32string u32string() const;
+
+ // [fs.path.generic.obs] generic format observers
+ template <class EcharT, class traits = std::char_traits<EcharT>, class Allocator = std::allocator<EcharT>>
+ std::basic_string<EcharT, traits, Allocator> generic_string(const Allocator& a = Allocator()) const;
+ std::string generic_string() const;
+ std::wstring generic_wstring() const;
+#if defined(__cpp_lib_char8_t) && !defined(GHC_FILESYSTEM_ENFORCE_CPP17_API)
+ std::u8string generic_u8string() const;
+#else
+ std::string generic_u8string() const;
+#endif
+ std::u16string generic_u16string() const;
+ std::u32string generic_u32string() const;
+
+ // [fs.path.compare] compare
+ int compare(const path& p) const noexcept;
+ int compare(const string_type& s) const;
+#ifdef GHC_WITH_STRING_VIEW
+ int compare(basic_string_view<value_type> s) const;
+#endif
+ int compare(const value_type* s) const;
+
+ // [fs.path.decompose] decomposition
+ path root_name() const;
+ path root_directory() const;
+ path root_path() const;
+ path relative_path() const;
+ path parent_path() const;
+ path filename() const;
+ path stem() const;
+ path extension() const;
+
+ // [fs.path.query] query
+ bool empty() const noexcept;
+ bool has_root_name() const;
+ bool has_root_directory() const;
+ bool has_root_path() const;
+ bool has_relative_path() const;
+ bool has_parent_path() const;
+ bool has_filename() const;
+ bool has_stem() const;
+ bool has_extension() const;
+ bool is_absolute() const;
+ bool is_relative() const;
+
+ // [fs.path.gen] generation
+ path lexically_normal() const;
+ path lexically_relative(const path& base) const;
+ path lexically_proximate(const path& base) const;
+
+ // [fs.path.itr] iterators
+ class iterator;
+ using const_iterator = iterator;
+ iterator begin() const;
+ iterator end() const;
+
+private:
+ using impl_value_type = value_type;
+ using impl_string_type = std::basic_string<impl_value_type>;
+ friend class directory_iterator;
+ void append_name(const value_type* name);
+ static constexpr impl_value_type generic_separator = '/';
+ template <typename InputIterator>
+ class input_iterator_range
+ {
+ public:
+ typedef InputIterator iterator;
+ typedef InputIterator const_iterator;
+ typedef typename InputIterator::difference_type difference_type;
+
+ input_iterator_range(const InputIterator& first, const InputIterator& last)
+ : _first(first)
+ , _last(last)
+ {
+ }
+
+ InputIterator begin() const { return _first; }
+ InputIterator end() const { return _last; }
+
+ private:
+ InputIterator _first;
+ InputIterator _last;
+ };
+ friend void swap(path& lhs, path& rhs) noexcept;
+ friend size_t hash_value(const path& p) noexcept;
+ friend path canonical(const path& p, std::error_code& ec);
+ friend bool create_directories(const path& p, std::error_code& ec) noexcept;
+ string_type::size_type root_name_length() const noexcept;
+ void postprocess_path_with_format(format fmt);
+ void check_long_path();
+ impl_string_type _path;
+#ifdef GHC_OS_WINDOWS
+ void handle_prefixes();
+ friend bool detail::has_executable_extension(const path& p);
+#ifdef GHC_WIN_AUTO_PREFIX_LONG_PATH
+ string_type::size_type _prefixLength{0};
+#else // GHC_WIN_AUTO_PREFIX_LONG_PATH
+ static const string_type::size_type _prefixLength{0};
+#endif // GHC_WIN_AUTO_PREFIX_LONG_PATH
+#else
+ static const string_type::size_type _prefixLength{0};
+#endif
+};
+
+// [fs.path.nonmember] path non-member functions
+GHC_FS_API void swap(path& lhs, path& rhs) noexcept;
+GHC_FS_API size_t hash_value(const path& p) noexcept;
+#ifdef GHC_HAS_THREEWAY_COMP
+GHC_FS_API std::strong_ordering operator<=>(const path& lhs, const path& rhs) noexcept;
+#endif
+GHC_FS_API bool operator==(const path& lhs, const path& rhs) noexcept;
+GHC_FS_API bool operator!=(const path& lhs, const path& rhs) noexcept;
+GHC_FS_API bool operator<(const path& lhs, const path& rhs) noexcept;
+GHC_FS_API bool operator<=(const path& lhs, const path& rhs) noexcept;
+GHC_FS_API bool operator>(const path& lhs, const path& rhs) noexcept;
+GHC_FS_API bool operator>=(const path& lhs, const path& rhs) noexcept;
+GHC_FS_API path operator/(const path& lhs, const path& rhs);
+
+// [fs.path.io] path inserter and extractor
+template <class charT, class traits>
+std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& os, const path& p);
+template <class charT, class traits>
+std::basic_istream<charT, traits>& operator>>(std::basic_istream<charT, traits>& is, path& p);
+
+// [pfs.path.factory] path factory functions
+template <class Source, typename = path::path_from_string<Source>>
+#if defined(__cpp_lib_char8_t) && !defined(GHC_FILESYSTEM_ENFORCE_CPP17_API)
+[[deprecated("use ghc::filesystem::path::path() with std::u8string instead")]]
+#endif
+path u8path(const Source& source);
+template <class InputIterator>
+#if defined(__cpp_lib_char8_t) && !defined(GHC_FILESYSTEM_ENFORCE_CPP17_API)
+[[deprecated("use ghc::filesystem::path::path() with std::u8string instead")]]
+#endif
+path u8path(InputIterator first, InputIterator last);
+
+// [fs.class.filesystem_error] class filesystem_error
+class GHC_FS_API_CLASS filesystem_error : public std::system_error
+{
+public:
+ filesystem_error(const std::string& what_arg, std::error_code ec);
+ filesystem_error(const std::string& what_arg, const path& p1, std::error_code ec);
+ filesystem_error(const std::string& what_arg, const path& p1, const path& p2, std::error_code ec);
+ const path& path1() const noexcept;
+ const path& path2() const noexcept;
+ const char* what() const noexcept override;
+
+private:
+ std::string _what_arg;
+ std::error_code _ec;
+ path _p1, _p2;
+};
+
+class GHC_FS_API_CLASS path::iterator
+{
+public:
+ using value_type = const path;
+ using difference_type = std::ptrdiff_t;
+ using pointer = const path*;
+ using reference = const path&;
+ using iterator_category = std::bidirectional_iterator_tag;
+
+ iterator();
+ iterator(const path& p, const impl_string_type::const_iterator& pos);
+ iterator& operator++();
+ iterator operator++(int);
+ iterator& operator--();
+ iterator operator--(int);
+ bool operator==(const iterator& other) const;
+ bool operator!=(const iterator& other) const;
+ reference operator*() const;
+ pointer operator->() const;
+
+private:
+ friend class path;
+ impl_string_type::const_iterator increment(const impl_string_type::const_iterator& pos) const;
+ impl_string_type::const_iterator decrement(const impl_string_type::const_iterator& pos) const;
+ void updateCurrent();
+ impl_string_type::const_iterator _first;
+ impl_string_type::const_iterator _last;
+ impl_string_type::const_iterator _prefix;
+ impl_string_type::const_iterator _root;
+ impl_string_type::const_iterator _iter;
+ path _current;
+};
+
+struct space_info
+{
+ uintmax_t capacity;
+ uintmax_t free;
+ uintmax_t available;
+};
+
+// [fs.enum] enumerations
+// [fs.enum.file_type]
+enum class file_type {
+ none,
+ not_found,
+ regular,
+ directory,
+ symlink,
+ block,
+ character,
+ fifo,
+ socket,
+ unknown,
+};
+
+// [fs.enum.perms]
+enum class perms : uint16_t {
+ none = 0,
+
+ owner_read = 0400,
+ owner_write = 0200,
+ owner_exec = 0100,
+ owner_all = 0700,
+
+ group_read = 040,
+ group_write = 020,
+ group_exec = 010,
+ group_all = 070,
+
+ others_read = 04,
+ others_write = 02,
+ others_exec = 01,
+ others_all = 07,
+
+ all = 0777,
+ set_uid = 04000,
+ set_gid = 02000,
+ sticky_bit = 01000,
+
+ mask = 07777,
+ unknown = 0xffff
+};
+
+// [fs.enum.perm.opts]
+enum class perm_options : uint16_t {
+ replace = 3,
+ add = 1,
+ remove = 2,
+ nofollow = 4,
+};
+
+// [fs.enum.copy.opts]
+enum class copy_options : uint16_t {
+ none = 0,
+
+ skip_existing = 1,
+ overwrite_existing = 2,
+ update_existing = 4,
+
+ recursive = 8,
+
+ copy_symlinks = 0x10,
+ skip_symlinks = 0x20,
+
+ directories_only = 0x40,
+ create_symlinks = 0x80,
+#ifndef GHC_OS_WEB
+ create_hard_links = 0x100
+#endif
+};
+
+// [fs.enum.dir.opts]
+enum class directory_options : uint16_t {
+ none = 0,
+ follow_directory_symlink = 1,
+ skip_permission_denied = 2,
+};
+
+// [fs.class.file_status] class file_status
+class GHC_FS_API_CLASS file_status
+{
+public:
+ // [fs.file_status.cons] constructors and destructor
+ file_status() noexcept;
+ explicit file_status(file_type ft, perms prms = perms::unknown) noexcept;
+ file_status(const file_status&) noexcept;
+ file_status(file_status&&) noexcept;
+ ~file_status();
+ // assignments:
+ file_status& operator=(const file_status&) noexcept;
+ file_status& operator=(file_status&&) noexcept;
+ // [fs.file_status.mods] modifiers
+ void type(file_type ft) noexcept;
+ void permissions(perms prms) noexcept;
+ // [fs.file_status.obs] observers
+ file_type type() const noexcept;
+ perms permissions() const noexcept;
+ friend bool operator==(const file_status& lhs, const file_status& rhs) noexcept { return lhs.type() == rhs.type() && lhs.permissions() == rhs.permissions(); }
+
+private:
+ file_type _type;
+ perms _perms;
+};
+
+using file_time_type = std::chrono::time_point<std::chrono::system_clock>;
+
+// [fs.class.directory_entry] Class directory_entry
+class GHC_FS_API_CLASS directory_entry
+{
+public:
+ // [fs.dir.entry.cons] constructors and destructor
+ directory_entry() noexcept = default;
+ directory_entry(const directory_entry&) = default;
+ directory_entry(directory_entry&&) noexcept = default;
+#ifdef GHC_WITH_EXCEPTIONS
+ explicit directory_entry(const path& p);
+#endif
+ directory_entry(const path& p, std::error_code& ec);
+ ~directory_entry();
+
+ // assignments:
+ directory_entry& operator=(const directory_entry&) = default;
+ directory_entry& operator=(directory_entry&&) noexcept = default;
+
+ // [fs.dir.entry.mods] modifiers
+#ifdef GHC_WITH_EXCEPTIONS
+ void assign(const path& p);
+ void replace_filename(const path& p);
+ void refresh();
+#endif
+ void assign(const path& p, std::error_code& ec);
+ void replace_filename(const path& p, std::error_code& ec);
+ void refresh(std::error_code& ec) noexcept;
+
+ // [fs.dir.entry.obs] observers
+ const filesystem::path& path() const noexcept;
+ operator const filesystem::path&() const noexcept;
+#ifdef GHC_WITH_EXCEPTIONS
+ bool exists() const;
+ bool is_block_file() const;
+ bool is_character_file() const;
+ bool is_directory() const;
+ bool is_fifo() const;
+ bool is_other() const;
+ bool is_regular_file() const;
+ bool is_socket() const;
+ bool is_symlink() const;
+ uintmax_t file_size() const;
+ file_time_type last_write_time() const;
+ file_status status() const;
+ file_status symlink_status() const;
+#endif
+ bool exists(std::error_code& ec) const noexcept;
+ bool is_block_file(std::error_code& ec) const noexcept;
+ bool is_character_file(std::error_code& ec) const noexcept;
+ bool is_directory(std::error_code& ec) const noexcept;
+ bool is_fifo(std::error_code& ec) const noexcept;
+ bool is_other(std::error_code& ec) const noexcept;
+ bool is_regular_file(std::error_code& ec) const noexcept;
+ bool is_socket(std::error_code& ec) const noexcept;
+ bool is_symlink(std::error_code& ec) const noexcept;
+ uintmax_t file_size(std::error_code& ec) const noexcept;
+ file_time_type last_write_time(std::error_code& ec) const noexcept;
+ file_status status(std::error_code& ec) const noexcept;
+ file_status symlink_status(std::error_code& ec) const noexcept;
+
+#ifndef GHC_OS_WEB
+#ifdef GHC_WITH_EXCEPTIONS
+ uintmax_t hard_link_count() const;
+#endif
+ uintmax_t hard_link_count(std::error_code& ec) const noexcept;
+#endif
+
+#ifdef GHC_HAS_THREEWAY_COMP
+ std::strong_ordering operator<=>(const directory_entry& rhs) const noexcept;
+#endif
+ bool operator<(const directory_entry& rhs) const noexcept;
+ bool operator==(const directory_entry& rhs) const noexcept;
+ bool operator!=(const directory_entry& rhs) const noexcept;
+ bool operator<=(const directory_entry& rhs) const noexcept;
+ bool operator>(const directory_entry& rhs) const noexcept;
+ bool operator>=(const directory_entry& rhs) const noexcept;
+
+private:
+ friend class directory_iterator;
+#ifdef GHC_WITH_EXCEPTIONS
+ file_type status_file_type() const;
+#endif
+ file_type status_file_type(std::error_code& ec) const noexcept;
+ filesystem::path _path;
+ file_status _status;
+ file_status _symlink_status;
+ uintmax_t _file_size = static_cast<uintmax_t>(-1);
+#ifndef GHC_OS_WINDOWS
+ uintmax_t _hard_link_count = static_cast<uintmax_t>(-1);
+#endif
+ time_t _last_write_time = 0;
+};
+
+// [fs.class.directory.iterator] Class directory_iterator
+class GHC_FS_API_CLASS directory_iterator
+{
+public:
+ class GHC_FS_API_CLASS proxy
+ {
+ public:
+ const directory_entry& operator*() const& noexcept { return _dir_entry; }
+ directory_entry operator*() && noexcept { return std::move(_dir_entry); }
+
+ private:
+ explicit proxy(const directory_entry& dir_entry)
+ : _dir_entry(dir_entry)
+ {
+ }
+ friend class directory_iterator;
+ friend class recursive_directory_iterator;
+ directory_entry _dir_entry;
+ };
+ using iterator_category = std::input_iterator_tag;
+ using value_type = directory_entry;
+ using difference_type = std::ptrdiff_t;
+ using pointer = const directory_entry*;
+ using reference = const directory_entry&;
+
+ // [fs.dir.itr.members] member functions
+ directory_iterator() noexcept;
+#ifdef GHC_WITH_EXCEPTIONS
+ explicit directory_iterator(const path& p);
+ directory_iterator(const path& p, directory_options options);
+#endif
+ directory_iterator(const path& p, std::error_code& ec) noexcept;
+ directory_iterator(const path& p, directory_options options, std::error_code& ec) noexcept;
+ directory_iterator(const directory_iterator& rhs);
+ directory_iterator(directory_iterator&& rhs) noexcept;
+ ~directory_iterator();
+ directory_iterator& operator=(const directory_iterator& rhs);
+ directory_iterator& operator=(directory_iterator&& rhs) noexcept;
+ const directory_entry& operator*() const;
+ const directory_entry* operator->() const;
+#ifdef GHC_WITH_EXCEPTIONS
+ directory_iterator& operator++();
+#endif
+ directory_iterator& increment(std::error_code& ec) noexcept;
+
+ // other members as required by [input.iterators]
+#ifdef GHC_WITH_EXCEPTIONS
+ proxy operator++(int)
+ {
+ proxy p{**this};
+ ++*this;
+ return p;
+ }
+#endif
+ bool operator==(const directory_iterator& rhs) const;
+ bool operator!=(const directory_iterator& rhs) const;
+
+private:
+ friend class recursive_directory_iterator;
+ class impl;
+ std::shared_ptr<impl> _impl;
+};
+
+// [fs.dir.itr.nonmembers] directory_iterator non-member functions
+GHC_FS_API directory_iterator begin(directory_iterator iter) noexcept;
+GHC_FS_API directory_iterator end(const directory_iterator&) noexcept;
+
+// [fs.class.re.dir.itr] class recursive_directory_iterator
+class GHC_FS_API_CLASS recursive_directory_iterator
+{
+public:
+ using iterator_category = std::input_iterator_tag;
+ using value_type = directory_entry;
+ using difference_type = std::ptrdiff_t;
+ using pointer = const directory_entry*;
+ using reference = const directory_entry&;
+
+ // [fs.rec.dir.itr.members] constructors and destructor
+ recursive_directory_iterator() noexcept;
+#ifdef GHC_WITH_EXCEPTIONS
+ explicit recursive_directory_iterator(const path& p);
+ recursive_directory_iterator(const path& p, directory_options options);
+#endif
+ recursive_directory_iterator(const path& p, directory_options options, std::error_code& ec) noexcept;
+ recursive_directory_iterator(const path& p, std::error_code& ec) noexcept;
+ recursive_directory_iterator(const recursive_directory_iterator& rhs);
+ recursive_directory_iterator(recursive_directory_iterator&& rhs) noexcept;
+ ~recursive_directory_iterator();
+
+ // [fs.rec.dir.itr.members] observers
+ directory_options options() const;
+ int depth() const;
+ bool recursion_pending() const;
+
+ const directory_entry& operator*() const;
+ const directory_entry* operator->() const;
+
+ // [fs.rec.dir.itr.members] modifiers recursive_directory_iterator&
+ recursive_directory_iterator& operator=(const recursive_directory_iterator& rhs);
+ recursive_directory_iterator& operator=(recursive_directory_iterator&& rhs) noexcept;
+#ifdef GHC_WITH_EXCEPTIONS
+ recursive_directory_iterator& operator++();
+#endif
+ recursive_directory_iterator& increment(std::error_code& ec) noexcept;
+
+#ifdef GHC_WITH_EXCEPTIONS
+ void pop();
+#endif
+ void pop(std::error_code& ec);
+ void disable_recursion_pending();
+
+ // other members as required by [input.iterators]
+#ifdef GHC_WITH_EXCEPTIONS
+ directory_iterator::proxy operator++(int)
+ {
+ directory_iterator::proxy proxy{**this};
+ ++*this;
+ return proxy;
+ }
+#endif
+ bool operator==(const recursive_directory_iterator& rhs) const;
+ bool operator!=(const recursive_directory_iterator& rhs) const;
+
+private:
+ struct recursive_directory_iterator_impl
+ {
+ directory_options _options;
+ bool _recursion_pending;
+ std::stack<directory_iterator> _dir_iter_stack;
+ recursive_directory_iterator_impl(directory_options options, bool recursion_pending)
+ : _options(options)
+ , _recursion_pending(recursion_pending)
+ {
+ }
+ };
+ std::shared_ptr<recursive_directory_iterator_impl> _impl;
+};
+
+// [fs.rec.dir.itr.nonmembers] directory_iterator non-member functions
+GHC_FS_API recursive_directory_iterator begin(recursive_directory_iterator iter) noexcept;
+GHC_FS_API recursive_directory_iterator end(const recursive_directory_iterator&) noexcept;
+
+// [fs.op.funcs] filesystem operations
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_FS_API path absolute(const path& p);
+GHC_FS_API path canonical(const path& p);
+GHC_FS_API void copy(const path& from, const path& to);
+GHC_FS_API void copy(const path& from, const path& to, copy_options options);
+GHC_FS_API bool copy_file(const path& from, const path& to);
+GHC_FS_API bool copy_file(const path& from, const path& to, copy_options option);
+GHC_FS_API void copy_symlink(const path& existing_symlink, const path& new_symlink);
+GHC_FS_API bool create_directories(const path& p);
+GHC_FS_API bool create_directory(const path& p);
+GHC_FS_API bool create_directory(const path& p, const path& attributes);
+GHC_FS_API void create_directory_symlink(const path& to, const path& new_symlink);
+GHC_FS_API void create_symlink(const path& to, const path& new_symlink);
+GHC_FS_API path current_path();
+GHC_FS_API void current_path(const path& p);
+GHC_FS_API bool exists(const path& p);
+GHC_FS_API bool equivalent(const path& p1, const path& p2);
+GHC_FS_API uintmax_t file_size(const path& p);
+GHC_FS_API bool is_block_file(const path& p);
+GHC_FS_API bool is_character_file(const path& p);
+GHC_FS_API bool is_directory(const path& p);
+GHC_FS_API bool is_empty(const path& p);
+GHC_FS_API bool is_fifo(const path& p);
+GHC_FS_API bool is_other(const path& p);
+GHC_FS_API bool is_regular_file(const path& p);
+GHC_FS_API bool is_socket(const path& p);
+GHC_FS_API bool is_symlink(const path& p);
+GHC_FS_API file_time_type last_write_time(const path& p);
+GHC_FS_API void last_write_time(const path& p, file_time_type new_time);
+GHC_FS_API void permissions(const path& p, perms prms, perm_options opts = perm_options::replace);
+GHC_FS_API path proximate(const path& p, const path& base = current_path());
+GHC_FS_API path read_symlink(const path& p);
+GHC_FS_API path relative(const path& p, const path& base = current_path());
+GHC_FS_API bool remove(const path& p);
+GHC_FS_API uintmax_t remove_all(const path& p);
+GHC_FS_API void rename(const path& from, const path& to);
+GHC_FS_API void resize_file(const path& p, uintmax_t size);
+GHC_FS_API space_info space(const path& p);
+GHC_FS_API file_status status(const path& p);
+GHC_FS_API file_status symlink_status(const path& p);
+GHC_FS_API path temp_directory_path();
+GHC_FS_API path weakly_canonical(const path& p);
+#endif
+GHC_FS_API path absolute(const path& p, std::error_code& ec);
+GHC_FS_API path canonical(const path& p, std::error_code& ec);
+GHC_FS_API void copy(const path& from, const path& to, std::error_code& ec) noexcept;
+GHC_FS_API void copy(const path& from, const path& to, copy_options options, std::error_code& ec) noexcept;
+GHC_FS_API bool copy_file(const path& from, const path& to, std::error_code& ec) noexcept;
+GHC_FS_API bool copy_file(const path& from, const path& to, copy_options option, std::error_code& ec) noexcept;
+GHC_FS_API void copy_symlink(const path& existing_symlink, const path& new_symlink, std::error_code& ec) noexcept;
+GHC_FS_API bool create_directories(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool create_directory(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool create_directory(const path& p, const path& attributes, std::error_code& ec) noexcept;
+GHC_FS_API void create_directory_symlink(const path& to, const path& new_symlink, std::error_code& ec) noexcept;
+GHC_FS_API void create_symlink(const path& to, const path& new_symlink, std::error_code& ec) noexcept;
+GHC_FS_API path current_path(std::error_code& ec);
+GHC_FS_API void current_path(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool exists(file_status s) noexcept;
+GHC_FS_API bool exists(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool equivalent(const path& p1, const path& p2, std::error_code& ec) noexcept;
+GHC_FS_API uintmax_t file_size(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool is_block_file(file_status s) noexcept;
+GHC_FS_API bool is_block_file(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool is_character_file(file_status s) noexcept;
+GHC_FS_API bool is_character_file(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool is_directory(file_status s) noexcept;
+GHC_FS_API bool is_directory(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool is_empty(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool is_fifo(file_status s) noexcept;
+GHC_FS_API bool is_fifo(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool is_other(file_status s) noexcept;
+GHC_FS_API bool is_other(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool is_regular_file(file_status s) noexcept;
+GHC_FS_API bool is_regular_file(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool is_socket(file_status s) noexcept;
+GHC_FS_API bool is_socket(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool is_symlink(file_status s) noexcept;
+GHC_FS_API bool is_symlink(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API file_time_type last_write_time(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API void last_write_time(const path& p, file_time_type new_time, std::error_code& ec) noexcept;
+GHC_FS_API void permissions(const path& p, perms prms, std::error_code& ec) noexcept;
+GHC_FS_API void permissions(const path& p, perms prms, perm_options opts, std::error_code& ec) noexcept;
+GHC_FS_API path proximate(const path& p, std::error_code& ec);
+GHC_FS_API path proximate(const path& p, const path& base, std::error_code& ec);
+GHC_FS_API path read_symlink(const path& p, std::error_code& ec);
+GHC_FS_API path relative(const path& p, std::error_code& ec);
+GHC_FS_API path relative(const path& p, const path& base, std::error_code& ec);
+GHC_FS_API bool remove(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API uintmax_t remove_all(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API void rename(const path& from, const path& to, std::error_code& ec) noexcept;
+GHC_FS_API void resize_file(const path& p, uintmax_t size, std::error_code& ec) noexcept;
+GHC_FS_API space_info space(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API file_status status(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API bool status_known(file_status s) noexcept;
+GHC_FS_API file_status symlink_status(const path& p, std::error_code& ec) noexcept;
+GHC_FS_API path temp_directory_path(std::error_code& ec) noexcept;
+GHC_FS_API path weakly_canonical(const path& p, std::error_code& ec) noexcept;
+
+#ifndef GHC_OS_WEB
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_FS_API void create_hard_link(const path& to, const path& new_hard_link);
+GHC_FS_API uintmax_t hard_link_count(const path& p);
+#endif
+GHC_FS_API void create_hard_link(const path& to, const path& new_hard_link, std::error_code& ec) noexcept;
+GHC_FS_API uintmax_t hard_link_count(const path& p, std::error_code& ec) noexcept;
+#endif
+
+// Non-C++17 add-on std::fstream wrappers with path
+template <class charT, class traits = std::char_traits<charT>>
+class basic_filebuf : public std::basic_filebuf<charT, traits>
+{
+public:
+ basic_filebuf() {}
+ ~basic_filebuf() override {}
+ basic_filebuf(const basic_filebuf&) = delete;
+ const basic_filebuf& operator=(const basic_filebuf&) = delete;
+ basic_filebuf<charT, traits>* open(const path& p, std::ios_base::openmode mode)
+ {
+#if defined(GHC_OS_WINDOWS) && !defined(__GLIBCXX__)
+ return std::basic_filebuf<charT, traits>::open(p.wstring().c_str(), mode) ? this : 0;
+#else
+ return std::basic_filebuf<charT, traits>::open(p.string().c_str(), mode) ? this : 0;
+#endif
+ }
+};
+
+template <class charT, class traits = std::char_traits<charT>>
+class basic_ifstream : public std::basic_ifstream<charT, traits>
+{
+public:
+ basic_ifstream() {}
+#if defined(GHC_OS_WINDOWS) && !defined(__GLIBCXX__)
+ explicit basic_ifstream(const path& p, std::ios_base::openmode mode = std::ios_base::in)
+ : std::basic_ifstream<charT, traits>(p.wstring().c_str(), mode)
+ {
+ }
+ void open(const path& p, std::ios_base::openmode mode = std::ios_base::in) { std::basic_ifstream<charT, traits>::open(p.wstring().c_str(), mode); }
+#else
+ explicit basic_ifstream(const path& p, std::ios_base::openmode mode = std::ios_base::in)
+ : std::basic_ifstream<charT, traits>(p.string().c_str(), mode)
+ {
+ }
+ void open(const path& p, std::ios_base::openmode mode = std::ios_base::in) { std::basic_ifstream<charT, traits>::open(p.string().c_str(), mode); }
+#endif
+ basic_ifstream(const basic_ifstream&) = delete;
+ const basic_ifstream& operator=(const basic_ifstream&) = delete;
+ ~basic_ifstream() override {}
+};
+
+template <class charT, class traits = std::char_traits<charT>>
+class basic_ofstream : public std::basic_ofstream<charT, traits>
+{
+public:
+ basic_ofstream() {}
+#if defined(GHC_OS_WINDOWS) && !defined(__GLIBCXX__)
+ explicit basic_ofstream(const path& p, std::ios_base::openmode mode = std::ios_base::out)
+ : std::basic_ofstream<charT, traits>(p.wstring().c_str(), mode)
+ {
+ }
+ void open(const path& p, std::ios_base::openmode mode = std::ios_base::out) { std::basic_ofstream<charT, traits>::open(p.wstring().c_str(), mode); }
+#else
+ explicit basic_ofstream(const path& p, std::ios_base::openmode mode = std::ios_base::out)
+ : std::basic_ofstream<charT, traits>(p.string().c_str(), mode)
+ {
+ }
+ void open(const path& p, std::ios_base::openmode mode = std::ios_base::out) { std::basic_ofstream<charT, traits>::open(p.string().c_str(), mode); }
+#endif
+ basic_ofstream(const basic_ofstream&) = delete;
+ const basic_ofstream& operator=(const basic_ofstream&) = delete;
+ ~basic_ofstream() override {}
+};
+
+template <class charT, class traits = std::char_traits<charT>>
+class basic_fstream : public std::basic_fstream<charT, traits>
+{
+public:
+ basic_fstream() {}
+#if defined(GHC_OS_WINDOWS) && !defined(__GLIBCXX__)
+ explicit basic_fstream(const path& p, std::ios_base::openmode mode = std::ios_base::in | std::ios_base::out)
+ : std::basic_fstream<charT, traits>(p.wstring().c_str(), mode)
+ {
+ }
+ void open(const path& p, std::ios_base::openmode mode = std::ios_base::in | std::ios_base::out) { std::basic_fstream<charT, traits>::open(p.wstring().c_str(), mode); }
+#else
+ explicit basic_fstream(const path& p, std::ios_base::openmode mode = std::ios_base::in | std::ios_base::out)
+ : std::basic_fstream<charT, traits>(p.string().c_str(), mode)
+ {
+ }
+ void open(const path& p, std::ios_base::openmode mode = std::ios_base::in | std::ios_base::out) { std::basic_fstream<charT, traits>::open(p.string().c_str(), mode); }
+#endif
+ basic_fstream(const basic_fstream&) = delete;
+ const basic_fstream& operator=(const basic_fstream&) = delete;
+ ~basic_fstream() override {}
+};
+
+typedef basic_filebuf<char> filebuf;
+typedef basic_filebuf<wchar_t> wfilebuf;
+typedef basic_ifstream<char> ifstream;
+typedef basic_ifstream<wchar_t> wifstream;
+typedef basic_ofstream<char> ofstream;
+typedef basic_ofstream<wchar_t> wofstream;
+typedef basic_fstream<char> fstream;
+typedef basic_fstream<wchar_t> wfstream;
+
+class GHC_FS_API_CLASS u8arguments
+{
+public:
+ u8arguments(int& argc, char**& argv);
+ ~u8arguments()
+ {
+ _refargc = _argc;
+ _refargv = _argv;
+ }
+
+ bool valid() const { return _isvalid; }
+
+private:
+ int _argc;
+ char** _argv;
+ int& _refargc;
+ char**& _refargv;
+ bool _isvalid;
+#ifdef GHC_OS_WINDOWS
+ std::vector<std::string> _args;
+ std::vector<char*> _argp;
+#endif
+};
+
+//-------------------------------------------------------------------------------------------------
+// Implementation
+//-------------------------------------------------------------------------------------------------
+
+namespace detail {
+enum utf8_states_t { S_STRT = 0, S_RJCT = 8 };
+GHC_FS_API void appendUTF8(std::string& str, uint32_t unicode);
+GHC_FS_API bool is_surrogate(uint32_t c);
+GHC_FS_API bool is_high_surrogate(uint32_t c);
+GHC_FS_API bool is_low_surrogate(uint32_t c);
+GHC_FS_API unsigned consumeUtf8Fragment(const unsigned state, const uint8_t fragment, uint32_t& codepoint);
+enum class portable_error {
+ none = 0,
+ exists,
+ not_found,
+ not_supported,
+ not_implemented,
+ invalid_argument,
+ is_a_directory,
+};
+GHC_FS_API std::error_code make_error_code(portable_error err);
+#ifdef GHC_OS_WINDOWS
+GHC_FS_API std::error_code make_system_error(uint32_t err = 0);
+#else
+GHC_FS_API std::error_code make_system_error(int err = 0);
+
+template <typename T, typename = int>
+struct has_d_type : std::false_type{};
+
+template <typename T>
+struct has_d_type<T, decltype((void)T::d_type, 0)> : std::true_type {};
+
+template <typename T>
+GHC_INLINE file_type file_type_from_dirent_impl(const T&, std::false_type)
+{
+ return file_type::none;
+}
+
+template <typename T>
+GHC_INLINE file_type file_type_from_dirent_impl(const T& t, std::true_type)
+{
+ switch (t.d_type) {
+#ifdef DT_BLK
+ case DT_BLK:
+ return file_type::block;
+#endif
+#ifdef DT_CHR
+ case DT_CHR:
+ return file_type::character;
+#endif
+#ifdef DT_DIR
+ case DT_DIR:
+ return file_type::directory;
+#endif
+#ifdef DT_FIFO
+ case DT_FIFO:
+ return file_type::fifo;
+#endif
+#ifdef DT_LNK
+ case DT_LNK:
+ return file_type::symlink;
+#endif
+#ifdef DT_REG
+ case DT_REG:
+ return file_type::regular;
+#endif
+#ifdef DT_SOCK
+ case DT_SOCK:
+ return file_type::socket;
+#endif
+#ifdef DT_UNKNOWN
+ case DT_UNKNOWN:
+ return file_type::none;
+#endif
+ default:
+ return file_type::unknown;
+ }
+}
+
+template <class T>
+GHC_INLINE file_type file_type_from_dirent(const T& t)
+{
+ return file_type_from_dirent_impl(t, has_d_type<T>{});
+}
+#endif
+} // namespace detail
+
+namespace detail {
+
+#ifdef GHC_EXPAND_IMPL
+
+GHC_INLINE std::error_code make_error_code(portable_error err)
+{
+#ifdef GHC_OS_WINDOWS
+ switch (err) {
+ case portable_error::none:
+ return std::error_code();
+ case portable_error::exists:
+ return std::error_code(ERROR_ALREADY_EXISTS, std::system_category());
+ case portable_error::not_found:
+ return std::error_code(ERROR_PATH_NOT_FOUND, std::system_category());
+ case portable_error::not_supported:
+ return std::error_code(ERROR_NOT_SUPPORTED, std::system_category());
+ case portable_error::not_implemented:
+ return std::error_code(ERROR_CALL_NOT_IMPLEMENTED, std::system_category());
+ case portable_error::invalid_argument:
+ return std::error_code(ERROR_INVALID_PARAMETER, std::system_category());
+ case portable_error::is_a_directory:
+#ifdef ERROR_DIRECTORY_NOT_SUPPORTED
+ return std::error_code(ERROR_DIRECTORY_NOT_SUPPORTED, std::system_category());
+#else
+ return std::error_code(ERROR_NOT_SUPPORTED, std::system_category());
+#endif
+ }
+#else
+ switch (err) {
+ case portable_error::none:
+ return std::error_code();
+ case portable_error::exists:
+ return std::error_code(EEXIST, std::system_category());
+ case portable_error::not_found:
+ return std::error_code(ENOENT, std::system_category());
+ case portable_error::not_supported:
+ return std::error_code(ENOTSUP, std::system_category());
+ case portable_error::not_implemented:
+ return std::error_code(ENOSYS, std::system_category());
+ case portable_error::invalid_argument:
+ return std::error_code(EINVAL, std::system_category());
+ case portable_error::is_a_directory:
+ return std::error_code(EISDIR, std::system_category());
+ }
+#endif
+ return std::error_code();
+}
+
+#ifdef GHC_OS_WINDOWS
+GHC_INLINE std::error_code make_system_error(uint32_t err)
+{
+ return std::error_code(err ? static_cast<int>(err) : static_cast<int>(::GetLastError()), std::system_category());
+}
+#else
+GHC_INLINE std::error_code make_system_error(int err)
+{
+ return std::error_code(err ? err : errno, std::system_category());
+}
+#endif
+
+#endif // GHC_EXPAND_IMPL
+
+template <typename Enum>
+using EnableBitmask = typename std::enable_if<std::is_same<Enum, perms>::value || std::is_same<Enum, perm_options>::value || std::is_same<Enum, copy_options>::value || std::is_same<Enum, directory_options>::value, Enum>::type;
+} // namespace detail
+
+template <typename Enum>
+constexpr detail::EnableBitmask<Enum> operator&(Enum X, Enum Y)
+{
+ using underlying = typename std::underlying_type<Enum>::type;
+ return static_cast<Enum>(static_cast<underlying>(X) & static_cast<underlying>(Y));
+}
+
+template <typename Enum>
+constexpr detail::EnableBitmask<Enum> operator|(Enum X, Enum Y)
+{
+ using underlying = typename std::underlying_type<Enum>::type;
+ return static_cast<Enum>(static_cast<underlying>(X) | static_cast<underlying>(Y));
+}
+
+template <typename Enum>
+constexpr detail::EnableBitmask<Enum> operator^(Enum X, Enum Y)
+{
+ using underlying = typename std::underlying_type<Enum>::type;
+ return static_cast<Enum>(static_cast<underlying>(X) ^ static_cast<underlying>(Y));
+}
+
+template <typename Enum>
+constexpr detail::EnableBitmask<Enum> operator~(Enum X)
+{
+ using underlying = typename std::underlying_type<Enum>::type;
+ return static_cast<Enum>(~static_cast<underlying>(X));
+}
+
+template <typename Enum>
+detail::EnableBitmask<Enum>& operator&=(Enum& X, Enum Y)
+{
+ X = X & Y;
+ return X;
+}
+
+template <typename Enum>
+detail::EnableBitmask<Enum>& operator|=(Enum& X, Enum Y)
+{
+ X = X | Y;
+ return X;
+}
+
+template <typename Enum>
+detail::EnableBitmask<Enum>& operator^=(Enum& X, Enum Y)
+{
+ X = X ^ Y;
+ return X;
+}
+
+#ifdef GHC_EXPAND_IMPL
+
+namespace detail {
+
+GHC_INLINE bool in_range(uint32_t c, uint32_t lo, uint32_t hi)
+{
+ return (static_cast<uint32_t>(c - lo) < (hi - lo + 1));
+}
+
+GHC_INLINE bool is_surrogate(uint32_t c)
+{
+ return in_range(c, 0xd800, 0xdfff);
+}
+
+GHC_INLINE bool is_high_surrogate(uint32_t c)
+{
+ return (c & 0xfffffc00) == 0xd800;
+}
+
+GHC_INLINE bool is_low_surrogate(uint32_t c)
+{
+ return (c & 0xfffffc00) == 0xdc00;
+}
+
+GHC_INLINE void appendUTF8(std::string& str, uint32_t unicode)
+{
+ if (unicode <= 0x7f) {
+ str.push_back(static_cast<char>(unicode));
+ }
+ else if (unicode >= 0x80 && unicode <= 0x7ff) {
+ str.push_back(static_cast<char>((unicode >> 6) + 192));
+ str.push_back(static_cast<char>((unicode & 0x3f) + 128));
+ }
+ else if ((unicode >= 0x800 && unicode <= 0xd7ff) || (unicode >= 0xe000 && unicode <= 0xffff)) {
+ str.push_back(static_cast<char>((unicode >> 12) + 224));
+ str.push_back(static_cast<char>(((unicode & 0xfff) >> 6) + 128));
+ str.push_back(static_cast<char>((unicode & 0x3f) + 128));
+ }
+ else if (unicode >= 0x10000 && unicode <= 0x10ffff) {
+ str.push_back(static_cast<char>((unicode >> 18) + 240));
+ str.push_back(static_cast<char>(((unicode & 0x3ffff) >> 12) + 128));
+ str.push_back(static_cast<char>(((unicode & 0xfff) >> 6) + 128));
+ str.push_back(static_cast<char>((unicode & 0x3f) + 128));
+ }
+ else {
+#ifdef GHC_RAISE_UNICODE_ERRORS
+ throw filesystem_error("Illegal code point for unicode character.", str, std::make_error_code(std::errc::illegal_byte_sequence));
+#else
+ appendUTF8(str, 0xfffd);
+#endif
+ }
+}
+
+// Thanks to Bjoern Hoehrmann (https://bjoern.hoehrmann.de/utf-8/decoder/dfa/)
+// and Taylor R Campbell for the ideas to this DFA approach of UTF-8 decoding;
+// Generating debugging and shrinking my own DFA from scratch was a day of fun!
+GHC_INLINE unsigned consumeUtf8Fragment(const unsigned state, const uint8_t fragment, uint32_t& codepoint)
+{
+ static const uint32_t utf8_state_info[] = {
+ // encoded states
+ 0x11111111u, 0x11111111u, 0x77777777u, 0x77777777u, 0x88888888u, 0x88888888u, 0x88888888u, 0x88888888u, 0x22222299u, 0x22222222u, 0x22222222u, 0x22222222u, 0x3333333au, 0x33433333u, 0x9995666bu, 0x99999999u,
+ 0x88888880u, 0x22818108u, 0x88888881u, 0x88888882u, 0x88888884u, 0x88888887u, 0x88888886u, 0x82218108u, 0x82281108u, 0x88888888u, 0x88888883u, 0x88888885u, 0u, 0u, 0u, 0u,
+ };
+ uint8_t category = fragment < 128 ? 0 : (utf8_state_info[(fragment >> 3) & 0xf] >> ((fragment & 7) << 2)) & 0xf;
+ codepoint = (state ? (codepoint << 6) | (fragment & 0x3fu) : (0xffu >> category) & fragment);
+ return state == S_RJCT ? static_cast<unsigned>(S_RJCT) : static_cast<unsigned>((utf8_state_info[category + 16] >> (state << 2)) & 0xf);
+}
+
+GHC_INLINE bool validUtf8(const std::string& utf8String)
+{
+ std::string::const_iterator iter = utf8String.begin();
+ unsigned utf8_state = S_STRT;
+ std::uint32_t codepoint = 0;
+ while (iter < utf8String.end()) {
+ if ((utf8_state = consumeUtf8Fragment(utf8_state, static_cast<uint8_t>(*iter++), codepoint)) == S_RJCT) {
+ return false;
+ }
+ }
+ if (utf8_state) {
+ return false;
+ }
+ return true;
+}
+
+} // namespace detail
+
+#endif
+
+namespace detail {
+
+template <class StringType, class Utf8String, typename std::enable_if<path::_is_basic_string<Utf8String>::value && (sizeof(typename Utf8String::value_type) == 1) && (sizeof(typename StringType::value_type) == 1)>::type* = nullptr>
+inline StringType fromUtf8(const Utf8String& utf8String, const typename StringType::allocator_type& alloc = typename StringType::allocator_type())
+{
+ return StringType(utf8String.begin(), utf8String.end(), alloc);
+}
+
+template <class StringType, class Utf8String, typename std::enable_if<path::_is_basic_string<Utf8String>::value && (sizeof(typename Utf8String::value_type) == 1) && (sizeof(typename StringType::value_type) == 2)>::type* = nullptr>
+inline StringType fromUtf8(const Utf8String& utf8String, const typename StringType::allocator_type& alloc = typename StringType::allocator_type())
+{
+ StringType result(alloc);
+ result.reserve(utf8String.length());
+ auto iter = utf8String.cbegin();
+ unsigned utf8_state = S_STRT;
+ std::uint32_t codepoint = 0;
+ while (iter < utf8String.cend()) {
+ if ((utf8_state = consumeUtf8Fragment(utf8_state, static_cast<uint8_t>(*iter++), codepoint)) == S_STRT) {
+ if (codepoint <= 0xffff) {
+ result += static_cast<typename StringType::value_type>(codepoint);
+ }
+ else {
+ codepoint -= 0x10000;
+ result += static_cast<typename StringType::value_type>((codepoint >> 10) + 0xd800);
+ result += static_cast<typename StringType::value_type>((codepoint & 0x3ff) + 0xdc00);
+ }
+ codepoint = 0;
+ }
+ else if (utf8_state == S_RJCT) {
+#ifdef GHC_RAISE_UNICODE_ERRORS
+ throw filesystem_error("Illegal byte sequence for unicode character.", utf8String, std::make_error_code(std::errc::illegal_byte_sequence));
+#else
+ result += static_cast<typename StringType::value_type>(0xfffd);
+ utf8_state = S_STRT;
+ codepoint = 0;
+#endif
+ }
+ }
+ if (utf8_state) {
+#ifdef GHC_RAISE_UNICODE_ERRORS
+ throw filesystem_error("Illegal byte sequence for unicode character.", utf8String, std::make_error_code(std::errc::illegal_byte_sequence));
+#else
+ result += static_cast<typename StringType::value_type>(0xfffd);
+#endif
+ }
+ return result;
+}
+
+template <class StringType, class Utf8String, typename std::enable_if<path::_is_basic_string<Utf8String>::value && (sizeof(typename Utf8String::value_type) == 1) && (sizeof(typename StringType::value_type) == 4)>::type* = nullptr>
+inline StringType fromUtf8(const Utf8String& utf8String, const typename StringType::allocator_type& alloc = typename StringType::allocator_type())
+{
+ StringType result(alloc);
+ result.reserve(utf8String.length());
+ auto iter = utf8String.cbegin();
+ unsigned utf8_state = S_STRT;
+ std::uint32_t codepoint = 0;
+ while (iter < utf8String.cend()) {
+ if ((utf8_state = consumeUtf8Fragment(utf8_state, static_cast<uint8_t>(*iter++), codepoint)) == S_STRT) {
+ result += static_cast<typename StringType::value_type>(codepoint);
+ codepoint = 0;
+ }
+ else if (utf8_state == S_RJCT) {
+#ifdef GHC_RAISE_UNICODE_ERRORS
+ throw filesystem_error("Illegal byte sequence for unicode character.", utf8String, std::make_error_code(std::errc::illegal_byte_sequence));
+#else
+ result += static_cast<typename StringType::value_type>(0xfffd);
+ utf8_state = S_STRT;
+ codepoint = 0;
+#endif
+ }
+ }
+ if (utf8_state) {
+#ifdef GHC_RAISE_UNICODE_ERRORS
+ throw filesystem_error("Illegal byte sequence for unicode character.", utf8String, std::make_error_code(std::errc::illegal_byte_sequence));
+#else
+ result += static_cast<typename StringType::value_type>(0xfffd);
+#endif
+ }
+ return result;
+}
+
+template <class StringType, typename charT, std::size_t N>
+inline StringType fromUtf8(const charT (&utf8String)[N])
+{
+#ifdef GHC_WITH_STRING_VIEW
+ return fromUtf8<StringType>(basic_string_view<charT>(utf8String, N - 1));
+#else
+ return fromUtf8<StringType>(std::basic_string<charT>(utf8String, N - 1));
+#endif
+}
+
+template <typename strT, typename std::enable_if<path::_is_basic_string<strT>::value && (sizeof(typename strT::value_type) == 1), int>::type size = 1>
+inline std::string toUtf8(const strT& unicodeString)
+{
+ return std::string(unicodeString.begin(), unicodeString.end());
+}
+
+template <typename strT, typename std::enable_if<path::_is_basic_string<strT>::value && (sizeof(typename strT::value_type) == 2), int>::type size = 2>
+inline std::string toUtf8(const strT& unicodeString)
+{
+ std::string result;
+ for (auto iter = unicodeString.begin(); iter != unicodeString.end(); ++iter) {
+ char32_t c = *iter;
+ if (is_surrogate(c)) {
+ ++iter;
+ if (iter != unicodeString.end() && is_high_surrogate(c) && is_low_surrogate(*iter)) {
+ appendUTF8(result, (char32_t(c) << 10) + *iter - 0x35fdc00);
+ }
+ else {
+#ifdef GHC_RAISE_UNICODE_ERRORS
+ throw filesystem_error("Illegal code point for unicode character.", result, std::make_error_code(std::errc::illegal_byte_sequence));
+#else
+ appendUTF8(result, 0xfffd);
+ if (iter == unicodeString.end()) {
+ break;
+ }
+#endif
+ }
+ }
+ else {
+ appendUTF8(result, c);
+ }
+ }
+ return result;
+}
+
+template <typename strT, typename std::enable_if<path::_is_basic_string<strT>::value && (sizeof(typename strT::value_type) == 4), int>::type size = 4>
+inline std::string toUtf8(const strT& unicodeString)
+{
+ std::string result;
+ for (auto c : unicodeString) {
+ appendUTF8(result, static_cast<uint32_t>(c));
+ }
+ return result;
+}
+
+template <typename charT>
+inline std::string toUtf8(const charT* unicodeString)
+{
+#ifdef GHC_WITH_STRING_VIEW
+ return toUtf8(basic_string_view<charT, std::char_traits<charT>>(unicodeString));
+#else
+ return toUtf8(std::basic_string<charT, std::char_traits<charT>>(unicodeString));
+#endif
+}
+
+#ifdef GHC_USE_WCHAR_T
+template <class StringType, class WString, typename std::enable_if<path::_is_basic_string<WString>::value && (sizeof(typename WString::value_type) == 2) && (sizeof(typename StringType::value_type) == 1), bool>::type = false>
+inline StringType fromWChar(const WString& wString, const typename StringType::allocator_type& alloc = typename StringType::allocator_type())
+{
+ auto temp = toUtf8(wString);
+ return StringType(temp.begin(), temp.end(), alloc);
+}
+
+template <class StringType, class WString, typename std::enable_if<path::_is_basic_string<WString>::value && (sizeof(typename WString::value_type) == 2) && (sizeof(typename StringType::value_type) == 2), bool>::type = false>
+inline StringType fromWChar(const WString& wString, const typename StringType::allocator_type& alloc = typename StringType::allocator_type())
+{
+ return StringType(wString.begin(), wString.end(), alloc);
+}
+
+template <class StringType, class WString, typename std::enable_if<path::_is_basic_string<WString>::value && (sizeof(typename WString::value_type) == 2) && (sizeof(typename StringType::value_type) == 4), bool>::type = false>
+inline StringType fromWChar(const WString& wString, const typename StringType::allocator_type& alloc = typename StringType::allocator_type())
+{
+ auto temp = toUtf8(wString);
+ return fromUtf8<StringType>(temp, alloc);
+}
+
+template <typename strT, typename std::enable_if<path::_is_basic_string<strT>::value && (sizeof(typename strT::value_type) == 1), bool>::type = false>
+inline std::wstring toWChar(const strT& unicodeString)
+{
+ return fromUtf8<std::wstring>(unicodeString);
+}
+
+template <typename strT, typename std::enable_if<path::_is_basic_string<strT>::value && (sizeof(typename strT::value_type) == 2), bool>::type = false>
+inline std::wstring toWChar(const strT& unicodeString)
+{
+ return std::wstring(unicodeString.begin(), unicodeString.end());
+}
+
+template <typename strT, typename std::enable_if<path::_is_basic_string<strT>::value && (sizeof(typename strT::value_type) == 4), bool>::type = false>
+inline std::wstring toWChar(const strT& unicodeString)
+{
+ auto temp = toUtf8(unicodeString);
+ return fromUtf8<std::wstring>(temp);
+}
+
+template <typename charT>
+inline std::wstring toWChar(const charT* unicodeString)
+{
+#ifdef GHC_WITH_STRING_VIEW
+ return toWChar(basic_string_view<charT, std::char_traits<charT>>(unicodeString));
+#else
+ return toWChar(std::basic_string<charT, std::char_traits<charT>>(unicodeString));
+#endif
+}
+#endif // GHC_USE_WCHAR_T
+
+} // namespace detail
+
+#ifdef GHC_EXPAND_IMPL
+
+namespace detail {
+
+template <typename strT, typename std::enable_if<path::_is_basic_string<strT>::value, bool>::type = true>
+GHC_INLINE bool startsWith(const strT& what, const strT& with)
+{
+ return with.length() <= what.length() && equal(with.begin(), with.end(), what.begin());
+}
+
+template <typename strT, typename std::enable_if<path::_is_basic_string<strT>::value, bool>::type = true>
+GHC_INLINE bool endsWith(const strT& what, const strT& with)
+{
+ return with.length() <= what.length() && what.compare(what.length() - with.length(), with.size(), with) == 0;
+}
+
+} // namespace detail
+
+GHC_INLINE void path::check_long_path()
+{
+#if defined(GHC_OS_WINDOWS) && defined(GHC_WIN_AUTO_PREFIX_LONG_PATH)
+ if (is_absolute() && _path.length() >= MAX_PATH - 12 && !detail::startsWith(_path, impl_string_type(GHC_PLATFORM_LITERAL("\\\\?\\")))) {
+ postprocess_path_with_format(native_format);
+ }
+#endif
+}
+
+GHC_INLINE void path::postprocess_path_with_format(path::format fmt)
+{
+#ifdef GHC_RAISE_UNICODE_ERRORS
+ if (!detail::validUtf8(_path)) {
+ path t;
+ t._path = _path;
+ throw filesystem_error("Illegal byte sequence for unicode character.", t, std::make_error_code(std::errc::illegal_byte_sequence));
+ }
+#endif
+ switch (fmt) {
+#ifdef GHC_OS_WINDOWS
+ case path::native_format:
+ case path::auto_format:
+ case path::generic_format:
+ for (auto& c : _path) {
+ if (c == generic_separator) {
+ c = preferred_separator;
+ }
+ }
+#ifdef GHC_WIN_AUTO_PREFIX_LONG_PATH
+ if (is_absolute() && _path.length() >= MAX_PATH - 12 && !detail::startsWith(_path, impl_string_type(GHC_PLATFORM_LITERAL("\\\\?\\")))) {
+ _path = GHC_PLATFORM_LITERAL("\\\\?\\") + _path;
+ }
+#endif
+ handle_prefixes();
+ break;
+#else
+ case path::auto_format:
+ case path::native_format:
+ case path::generic_format:
+ // nothing to do
+ break;
+#endif
+ }
+ if (_path.length() > _prefixLength + 2 && _path[_prefixLength] == preferred_separator && _path[_prefixLength + 1] == preferred_separator && _path[_prefixLength + 2] != preferred_separator) {
+ impl_string_type::iterator new_end = std::unique(_path.begin() + static_cast<string_type::difference_type>(_prefixLength) + 2, _path.end(), [](path::value_type lhs, path::value_type rhs) { return lhs == rhs && lhs == preferred_separator; });
+ _path.erase(new_end, _path.end());
+ }
+ else {
+ impl_string_type::iterator new_end = std::unique(_path.begin() + static_cast<string_type::difference_type>(_prefixLength), _path.end(), [](path::value_type lhs, path::value_type rhs) { return lhs == rhs && lhs == preferred_separator; });
+ _path.erase(new_end, _path.end());
+ }
+}
+
+#endif // GHC_EXPAND_IMPL
+
+template <class Source, typename>
+inline path::path(const Source& source, format fmt)
+#ifdef GHC_USE_WCHAR_T
+ : _path(detail::toWChar(source))
+#else
+ : _path(detail::toUtf8(source))
+#endif
+{
+ postprocess_path_with_format(fmt);
+}
+
+template <class Source, typename>
+inline path u8path(const Source& source)
+{
+ return path(source);
+}
+template <class InputIterator>
+inline path u8path(InputIterator first, InputIterator last)
+{
+ return path(first, last);
+}
+
+template <class InputIterator>
+inline path::path(InputIterator first, InputIterator last, format fmt)
+ : path(std::basic_string<typename std::iterator_traits<InputIterator>::value_type>(first, last), fmt)
+{
+ // delegated
+}
+
+#ifdef GHC_EXPAND_IMPL
+
+namespace detail {
+
+GHC_INLINE bool equals_simple_insensitive(const path::value_type* str1, const path::value_type* str2)
+{
+#ifdef GHC_OS_WINDOWS
+#ifdef __GNUC__
+ while (::tolower((unsigned char)*str1) == ::tolower((unsigned char)*str2++)) {
+ if (*str1++ == 0)
+ return true;
+ }
+ return false;
+#else // __GNUC__
+#ifdef GHC_USE_WCHAR_T
+ return 0 == ::_wcsicmp(str1, str2);
+#else // GHC_USE_WCHAR_T
+ return 0 == ::_stricmp(str1, str2);
+#endif // GHC_USE_WCHAR_T
+#endif // __GNUC__
+#else // GHC_OS_WINDOWS
+ return 0 == ::strcasecmp(str1, str2);
+#endif // GHC_OS_WINDOWS
+}
+
+GHC_INLINE int compare_simple_insensitive(const path::value_type* str1, size_t len1, const path::value_type* str2, size_t len2)
+{
+ while (len1 > 0 && len2 > 0 && ::tolower(static_cast<unsigned char>(*str1)) == ::tolower(static_cast<unsigned char>(*str2))) {
+ --len1;
+ --len2;
+ ++str1;
+ ++str2;
+ }
+ if (len1 && len2) {
+ return *str1 < *str2 ? -1 : 1;
+ }
+ if (len1 == 0 && len2 == 0) {
+ return 0;
+ }
+ return len1 == 0 ? -1 : 1;
+}
+
+GHC_INLINE const char* strerror_adapter(char* gnu, char*)
+{
+ return gnu;
+}
+
+GHC_INLINE const char* strerror_adapter(int posix, char* buffer)
+{
+ if (posix) {
+ return "Error in strerror_r!";
+ }
+ return buffer;
+}
+
+template <typename ErrorNumber>
+GHC_INLINE std::string systemErrorText(ErrorNumber code = 0)
+{
+#if defined(GHC_OS_WINDOWS)
+ LPVOID msgBuf;
+ DWORD dw = code ? static_cast<DWORD>(code) : ::GetLastError();
+ FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS, NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPWSTR)&msgBuf, 0, NULL);
+ std::string msg = toUtf8(std::wstring((LPWSTR)msgBuf));
+ LocalFree(msgBuf);
+ return msg;
+#else
+ char buffer[512];
+ return strerror_adapter(strerror_r(code ? code : errno, buffer, sizeof(buffer)), buffer);
+#endif
+}
+
+#ifdef GHC_OS_WINDOWS
+using CreateSymbolicLinkW_fp = BOOLEAN(WINAPI*)(LPCWSTR, LPCWSTR, DWORD);
+using CreateHardLinkW_fp = BOOLEAN(WINAPI*)(LPCWSTR, LPCWSTR, LPSECURITY_ATTRIBUTES);
+
+GHC_INLINE void create_symlink(const path& target_name, const path& new_symlink, bool to_directory, std::error_code& ec)
+{
+ std::error_code tec;
+ auto fs = status(target_name, tec);
+ if ((fs.type() == file_type::directory && !to_directory) || (fs.type() == file_type::regular && to_directory)) {
+ ec = detail::make_error_code(detail::portable_error::not_supported);
+ return;
+ }
+#if defined(__GNUC__) && __GNUC__ >= 8
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wcast-function-type"
+#endif
+ static CreateSymbolicLinkW_fp api_call = reinterpret_cast<CreateSymbolicLinkW_fp>(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateSymbolicLinkW"));
+#if defined(__GNUC__) && __GNUC__ >= 8
+#pragma GCC diagnostic pop
+#endif
+ if (api_call) {
+ if (api_call(GHC_NATIVEWP(new_symlink), GHC_NATIVEWP(target_name), to_directory ? 1 : 0) == 0) {
+ auto result = ::GetLastError();
+ if (result == ERROR_PRIVILEGE_NOT_HELD && api_call(GHC_NATIVEWP(new_symlink), GHC_NATIVEWP(target_name), to_directory ? 3 : 2) != 0) {
+ return;
+ }
+ ec = detail::make_system_error(result);
+ }
+ }
+ else {
+ ec = detail::make_system_error(ERROR_NOT_SUPPORTED);
+ }
+}
+
+GHC_INLINE void create_hardlink(const path& target_name, const path& new_hardlink, std::error_code& ec)
+{
+#if defined(__GNUC__) && __GNUC__ >= 8
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wcast-function-type"
+#endif
+ static CreateHardLinkW_fp api_call = reinterpret_cast<CreateHardLinkW_fp>(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "CreateHardLinkW"));
+#if defined(__GNUC__) && __GNUC__ >= 8
+#pragma GCC diagnostic pop
+#endif
+ if (api_call) {
+ if (api_call(GHC_NATIVEWP(new_hardlink), GHC_NATIVEWP(target_name), NULL) == 0) {
+ ec = detail::make_system_error();
+ }
+ }
+ else {
+ ec = detail::make_system_error(ERROR_NOT_SUPPORTED);
+ }
+}
+
+GHC_INLINE path getFullPathName(const wchar_t* p, std::error_code& ec)
+{
+ ULONG size = ::GetFullPathNameW(p, 0, 0, 0);
+ if (size) {
+ std::vector<wchar_t> buf(size, 0);
+ ULONG s2 = GetFullPathNameW(p, size, buf.data(), nullptr);
+ if (s2 && s2 < size) {
+ return path(std::wstring(buf.data(), s2));
+ }
+ }
+ ec = detail::make_system_error();
+ return path();
+}
+
+#else
+GHC_INLINE void create_symlink(const path& target_name, const path& new_symlink, bool, std::error_code& ec)
+{
+ if (::symlink(target_name.c_str(), new_symlink.c_str()) != 0) {
+ ec = detail::make_system_error();
+ }
+}
+
+#ifndef GHC_OS_WEB
+GHC_INLINE void create_hardlink(const path& target_name, const path& new_hardlink, std::error_code& ec)
+{
+ if (::link(target_name.c_str(), new_hardlink.c_str()) != 0) {
+ ec = detail::make_system_error();
+ }
+}
+#endif
+#endif
+
+template <typename T>
+GHC_INLINE file_status file_status_from_st_mode(T mode)
+{
+#ifdef GHC_OS_WINDOWS
+ file_type ft = file_type::unknown;
+ if ((mode & _S_IFDIR) == _S_IFDIR) {
+ ft = file_type::directory;
+ }
+ else if ((mode & _S_IFREG) == _S_IFREG) {
+ ft = file_type::regular;
+ }
+ else if ((mode & _S_IFCHR) == _S_IFCHR) {
+ ft = file_type::character;
+ }
+ perms prms = static_cast<perms>(mode & 0xfff);
+ return file_status(ft, prms);
+#else
+ file_type ft = file_type::unknown;
+ if (S_ISDIR(mode)) {
+ ft = file_type::directory;
+ }
+ else if (S_ISREG(mode)) {
+ ft = file_type::regular;
+ }
+ else if (S_ISCHR(mode)) {
+ ft = file_type::character;
+ }
+ else if (S_ISBLK(mode)) {
+ ft = file_type::block;
+ }
+ else if (S_ISFIFO(mode)) {
+ ft = file_type::fifo;
+ }
+ else if (S_ISLNK(mode)) {
+ ft = file_type::symlink;
+ }
+ else if (S_ISSOCK(mode)) {
+ ft = file_type::socket;
+ }
+ perms prms = static_cast<perms>(mode & 0xfff);
+ return file_status(ft, prms);
+#endif
+}
+
+#ifdef GHC_OS_WINDOWS
+
+class unique_handle
+{
+public:
+ typedef HANDLE element_type;
+
+ unique_handle() noexcept
+ : _handle(INVALID_HANDLE_VALUE)
+ {
+ }
+ explicit unique_handle(element_type h) noexcept
+ : _handle(h)
+ {
+ }
+ unique_handle(unique_handle&& u) noexcept
+ : _handle(u.release())
+ {
+ }
+ ~unique_handle() { reset(); }
+ unique_handle& operator=(unique_handle&& u) noexcept
+ {
+ reset(u.release());
+ return *this;
+ }
+ element_type get() const noexcept { return _handle; }
+ explicit operator bool() const noexcept { return _handle != INVALID_HANDLE_VALUE; }
+ element_type release() noexcept
+ {
+ element_type tmp = _handle;
+ _handle = INVALID_HANDLE_VALUE;
+ return tmp;
+ }
+ void reset(element_type h = INVALID_HANDLE_VALUE) noexcept
+ {
+ element_type tmp = _handle;
+ _handle = h;
+ if (tmp != INVALID_HANDLE_VALUE) {
+ CloseHandle(tmp);
+ }
+ }
+ void swap(unique_handle& u) noexcept { std::swap(_handle, u._handle); }
+
+private:
+ element_type _handle;
+};
+
+#ifndef REPARSE_DATA_BUFFER_HEADER_SIZE
+typedef struct _REPARSE_DATA_BUFFER
+{
+ ULONG ReparseTag;
+ USHORT ReparseDataLength;
+ USHORT Reserved;
+ union
+ {
+ struct
+ {
+ USHORT SubstituteNameOffset;
+ USHORT SubstituteNameLength;
+ USHORT PrintNameOffset;
+ USHORT PrintNameLength;
+ ULONG Flags;
+ WCHAR PathBuffer[1];
+ } SymbolicLinkReparseBuffer;
+ struct
+ {
+ USHORT SubstituteNameOffset;
+ USHORT SubstituteNameLength;
+ USHORT PrintNameOffset;
+ USHORT PrintNameLength;
+ WCHAR PathBuffer[1];
+ } MountPointReparseBuffer;
+ struct
+ {
+ UCHAR DataBuffer[1];
+ } GenericReparseBuffer;
+ } DUMMYUNIONNAME;
+} REPARSE_DATA_BUFFER;
+#ifndef MAXIMUM_REPARSE_DATA_BUFFER_SIZE
+#define MAXIMUM_REPARSE_DATA_BUFFER_SIZE (16 * 1024)
+#endif
+#endif
+
+template <class T>
+struct free_deleter
+{
+ void operator()(T* p) const { std::free(p); }
+};
+
+GHC_INLINE std::unique_ptr<REPARSE_DATA_BUFFER, free_deleter<REPARSE_DATA_BUFFER>> getReparseData(const path& p, std::error_code& ec)
+{
+ unique_handle file(CreateFileW(GHC_NATIVEWP(p), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, 0));
+ if (!file) {
+ ec = detail::make_system_error();
+ return nullptr;
+ }
+
+ std::unique_ptr<REPARSE_DATA_BUFFER, free_deleter<REPARSE_DATA_BUFFER>> reparseData(reinterpret_cast<REPARSE_DATA_BUFFER*>(std::calloc(1, MAXIMUM_REPARSE_DATA_BUFFER_SIZE)));
+ ULONG bufferUsed;
+ if (DeviceIoControl(file.get(), FSCTL_GET_REPARSE_POINT, 0, 0, reparseData.get(), MAXIMUM_REPARSE_DATA_BUFFER_SIZE, &bufferUsed, 0)) {
+ return reparseData;
+ }
+ else {
+ ec = detail::make_system_error();
+ }
+ return nullptr;
+}
+#endif
+
+GHC_INLINE path resolveSymlink(const path& p, std::error_code& ec)
+{
+#ifdef GHC_OS_WINDOWS
+ path result;
+ auto reparseData = detail::getReparseData(p, ec);
+ if (!ec) {
+ if (reparseData && IsReparseTagMicrosoft(reparseData->ReparseTag)) {
+ switch (reparseData->ReparseTag) {
+ case IO_REPARSE_TAG_SYMLINK: {
+ auto printName = std::wstring(&reparseData->SymbolicLinkReparseBuffer.PathBuffer[reparseData->SymbolicLinkReparseBuffer.PrintNameOffset / sizeof(WCHAR)], reparseData->SymbolicLinkReparseBuffer.PrintNameLength / sizeof(WCHAR));
+ auto substituteName =
+ std::wstring(&reparseData->SymbolicLinkReparseBuffer.PathBuffer[reparseData->SymbolicLinkReparseBuffer.SubstituteNameOffset / sizeof(WCHAR)], reparseData->SymbolicLinkReparseBuffer.SubstituteNameLength / sizeof(WCHAR));
+ if (detail::endsWith(substituteName, printName) && detail::startsWith(substituteName, std::wstring(L"\\??\\"))) {
+ result = printName;
+ }
+ else {
+ result = substituteName;
+ }
+ if (reparseData->SymbolicLinkReparseBuffer.Flags & 0x1 /*SYMLINK_FLAG_RELATIVE*/) {
+ result = p.parent_path() / result;
+ }
+ break;
+ }
+ case IO_REPARSE_TAG_MOUNT_POINT:
+ result = detail::getFullPathName(GHC_NATIVEWP(p), ec);
+ // result = std::wstring(&reparseData->MountPointReparseBuffer.PathBuffer[reparseData->MountPointReparseBuffer.SubstituteNameOffset / sizeof(WCHAR)], reparseData->MountPointReparseBuffer.SubstituteNameLength / sizeof(WCHAR));
+ break;
+ default:
+ break;
+ }
+ }
+ }
+ return result;
+#else
+ size_t bufferSize = 256;
+ while (true) {
+ std::vector<char> buffer(bufferSize, static_cast<char>(0));
+ auto rc = ::readlink(p.c_str(), buffer.data(), buffer.size());
+ if (rc < 0) {
+ ec = detail::make_system_error();
+ return path();
+ }
+ else if (rc < static_cast<int>(bufferSize)) {
+ return path(std::string(buffer.data(), static_cast<std::string::size_type>(rc)));
+ }
+ bufferSize *= 2;
+ }
+ return path();
+#endif
+}
+
+#ifdef GHC_OS_WINDOWS
+GHC_INLINE time_t timeFromFILETIME(const FILETIME& ft)
+{
+ ULARGE_INTEGER ull;
+ ull.LowPart = ft.dwLowDateTime;
+ ull.HighPart = ft.dwHighDateTime;
+ return static_cast<time_t>(ull.QuadPart / 10000000ULL - 11644473600ULL);
+}
+
+GHC_INLINE void timeToFILETIME(time_t t, FILETIME& ft)
+{
+ LONGLONG ll;
+ ll = Int32x32To64(t, 10000000) + 116444736000000000;
+ ft.dwLowDateTime = static_cast<DWORD>(ll);
+ ft.dwHighDateTime = static_cast<DWORD>(ll >> 32);
+}
+
+template <typename INFO>
+GHC_INLINE uintmax_t hard_links_from_INFO(const INFO* info)
+{
+ return static_cast<uintmax_t>(-1);
+}
+
+template <>
+GHC_INLINE uintmax_t hard_links_from_INFO<BY_HANDLE_FILE_INFORMATION>(const BY_HANDLE_FILE_INFORMATION* info)
+{
+ return info->nNumberOfLinks;
+}
+
+template <typename INFO>
+GHC_INLINE DWORD reparse_tag_from_INFO(const INFO*)
+{
+ return 0;
+}
+
+template <>
+GHC_INLINE DWORD reparse_tag_from_INFO(const WIN32_FIND_DATAW* info)
+{
+ return info->dwReserved0;
+}
+
+template <typename INFO>
+GHC_INLINE file_status status_from_INFO(const path& p, const INFO* info, std::error_code& ec, uintmax_t* sz = nullptr, time_t* lwt = nullptr)
+{
+ file_type ft = file_type::unknown;
+ if (sizeof(INFO) == sizeof(WIN32_FIND_DATAW)) {
+ if (detail::reparse_tag_from_INFO(info) == IO_REPARSE_TAG_SYMLINK) {
+ ft = file_type::symlink;
+ }
+ }
+ else {
+ if ((info->dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
+ auto reparseData = detail::getReparseData(p, ec);
+ if (!ec && reparseData && IsReparseTagMicrosoft(reparseData->ReparseTag) && reparseData->ReparseTag == IO_REPARSE_TAG_SYMLINK) {
+ ft = file_type::symlink;
+ }
+ }
+ }
+ if (ft == file_type::unknown) {
+ if ((info->dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)) {
+ ft = file_type::directory;
+ }
+ else {
+ ft = file_type::regular;
+ }
+ }
+ perms prms = perms::owner_read | perms::group_read | perms::others_read;
+ if (!(info->dwFileAttributes & FILE_ATTRIBUTE_READONLY)) {
+ prms = prms | perms::owner_write | perms::group_write | perms::others_write;
+ }
+ if (has_executable_extension(p)) {
+ prms = prms | perms::owner_exec | perms::group_exec | perms::others_exec;
+ }
+ if (sz) {
+ *sz = static_cast<uintmax_t>(info->nFileSizeHigh) << (sizeof(info->nFileSizeHigh) * 8) | info->nFileSizeLow;
+ }
+ if (lwt) {
+ *lwt = detail::timeFromFILETIME(info->ftLastWriteTime);
+ }
+ return file_status(ft, prms);
+}
+
+#endif
+
+GHC_INLINE bool is_not_found_error(std::error_code& ec)
+{
+#ifdef GHC_OS_WINDOWS
+ return ec.value() == ERROR_FILE_NOT_FOUND || ec.value() == ERROR_PATH_NOT_FOUND || ec.value() == ERROR_INVALID_NAME;
+#else
+ return ec.value() == ENOENT || ec.value() == ENOTDIR;
+#endif
+}
+
+GHC_INLINE file_status symlink_status_ex(const path& p, std::error_code& ec, uintmax_t* sz = nullptr, uintmax_t* nhl = nullptr, time_t* lwt = nullptr) noexcept
+{
+#ifdef GHC_OS_WINDOWS
+ file_status fs;
+ WIN32_FILE_ATTRIBUTE_DATA attr;
+ if (!GetFileAttributesExW(GHC_NATIVEWP(p), GetFileExInfoStandard, &attr)) {
+ ec = detail::make_system_error();
+ }
+ else {
+ ec.clear();
+ fs = detail::status_from_INFO(p, &attr, ec, sz, lwt);
+ if (nhl) {
+ *nhl = 0;
+ }
+ }
+ if (detail::is_not_found_error(ec)) {
+ return file_status(file_type::not_found);
+ }
+ return ec ? file_status(file_type::none) : fs;
+#else
+ (void)sz;
+ (void)nhl;
+ (void)lwt;
+ struct ::stat fs;
+ auto result = ::lstat(p.c_str(), &fs);
+ if (result == 0) {
+ ec.clear();
+ file_status f_s = detail::file_status_from_st_mode(fs.st_mode);
+ return f_s;
+ }
+ ec = detail::make_system_error();
+ if (detail::is_not_found_error(ec)) {
+ return file_status(file_type::not_found, perms::unknown);
+ }
+ return file_status(file_type::none);
+#endif
+}
+
+GHC_INLINE file_status status_ex(const path& p, std::error_code& ec, file_status* sls = nullptr, uintmax_t* sz = nullptr, uintmax_t* nhl = nullptr, time_t* lwt = nullptr, int recurse_count = 0) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ if (recurse_count > 16) {
+ ec = detail::make_system_error(0x2A9 /*ERROR_STOPPED_ON_SYMLINK*/);
+ return file_status(file_type::unknown);
+ }
+ WIN32_FILE_ATTRIBUTE_DATA attr;
+ if (!::GetFileAttributesExW(GHC_NATIVEWP(p), GetFileExInfoStandard, &attr)) {
+ ec = detail::make_system_error();
+ }
+ else if (attr.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
+ auto reparseData = detail::getReparseData(p, ec);
+ if (!ec && reparseData && IsReparseTagMicrosoft(reparseData->ReparseTag) && reparseData->ReparseTag == IO_REPARSE_TAG_SYMLINK) {
+ path target = resolveSymlink(p, ec);
+ file_status result;
+ if (!ec && !target.empty()) {
+ if (sls) {
+ *sls = status_from_INFO(p, &attr, ec);
+ }
+ return detail::status_ex(target, ec, nullptr, sz, nhl, lwt, recurse_count + 1);
+ }
+ return file_status(file_type::unknown);
+ }
+ }
+ if (ec) {
+ if (detail::is_not_found_error(ec)) {
+ return file_status(file_type::not_found);
+ }
+ return file_status(file_type::none);
+ }
+ if (nhl) {
+ *nhl = 0;
+ }
+ return detail::status_from_INFO(p, &attr, ec, sz, lwt);
+#else
+ (void)recurse_count;
+ struct ::stat st;
+ auto result = ::lstat(p.c_str(), &st);
+ if (result == 0) {
+ ec.clear();
+ file_status fs = detail::file_status_from_st_mode(st.st_mode);
+ if (sls) {
+ *sls = fs;
+ }
+ if (fs.type() == file_type::symlink) {
+ result = ::stat(p.c_str(), &st);
+ if (result == 0) {
+ fs = detail::file_status_from_st_mode(st.st_mode);
+ }
+ else {
+ ec = detail::make_system_error();
+ if (detail::is_not_found_error(ec)) {
+ return file_status(file_type::not_found, perms::unknown);
+ }
+ return file_status(file_type::none);
+ }
+ }
+ if (sz) {
+ *sz = static_cast<uintmax_t>(st.st_size);
+ }
+ if (nhl) {
+ *nhl = st.st_nlink;
+ }
+ if (lwt) {
+ *lwt = st.st_mtime;
+ }
+ return fs;
+ }
+ else {
+ ec = detail::make_system_error();
+ if (detail::is_not_found_error(ec)) {
+ return file_status(file_type::not_found, perms::unknown);
+ }
+ return file_status(file_type::none);
+ }
+#endif
+}
+
+} // namespace detail
+
+GHC_INLINE u8arguments::u8arguments(int& argc, char**& argv)
+ : _argc(argc)
+ , _argv(argv)
+ , _refargc(argc)
+ , _refargv(argv)
+ , _isvalid(false)
+{
+#ifdef GHC_OS_WINDOWS
+ LPWSTR* p;
+ p = ::CommandLineToArgvW(::GetCommandLineW(), &argc);
+ _args.reserve(static_cast<size_t>(argc));
+ _argp.reserve(static_cast<size_t>(argc));
+ for (size_t i = 0; i < static_cast<size_t>(argc); ++i) {
+ _args.push_back(detail::toUtf8(std::wstring(p[i])));
+ _argp.push_back((char*)_args[i].data());
+ }
+ argv = _argp.data();
+ ::LocalFree(p);
+ _isvalid = true;
+#else
+ std::setlocale(LC_ALL, "");
+#if defined(__ANDROID__) && __ANDROID_API__ < 26
+ _isvalid = true;
+#else
+ if (detail::equals_simple_insensitive(::nl_langinfo(CODESET), "UTF-8")) {
+ _isvalid = true;
+ }
+#endif
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// [fs.path.construct] constructors and destructor
+
+GHC_INLINE path::path() noexcept {}
+
+GHC_INLINE path::path(const path& p)
+ : _path(p._path)
+#if defined(GHC_OS_WINDOWS) && defined(GHC_WIN_AUTO_PREFIX_LONG_PATH)
+ , _prefixLength(p._prefixLength)
+#endif
+{
+}
+
+GHC_INLINE path::path(path&& p) noexcept
+ : _path(std::move(p._path))
+#if defined(GHC_OS_WINDOWS) && defined(GHC_WIN_AUTO_PREFIX_LONG_PATH)
+ , _prefixLength(p._prefixLength)
+#endif
+{
+}
+
+GHC_INLINE path::path(string_type&& source, format fmt)
+ : _path(std::move(source))
+{
+ postprocess_path_with_format(fmt);
+}
+
+#endif // GHC_EXPAND_IMPL
+
+#ifdef GHC_WITH_EXCEPTIONS
+template <class Source, typename>
+inline path::path(const Source& source, const std::locale& loc, format fmt)
+ : path(source, fmt)
+{
+ std::string locName = loc.name();
+ if (!(locName.length() >= 5 && (locName.substr(locName.length() - 5) == "UTF-8" || locName.substr(locName.length() - 5) == "utf-8"))) {
+ throw filesystem_error("This implementation only supports UTF-8 locales!", path(_path), detail::make_error_code(detail::portable_error::not_supported));
+ }
+}
+
+template <class InputIterator>
+inline path::path(InputIterator first, InputIterator last, const std::locale& loc, format fmt)
+ : path(std::basic_string<typename std::iterator_traits<InputIterator>::value_type>(first, last), fmt)
+{
+ std::string locName = loc.name();
+ if (!(locName.length() >= 5 && (locName.substr(locName.length() - 5) == "UTF-8" || locName.substr(locName.length() - 5) == "utf-8"))) {
+ throw filesystem_error("This implementation only supports UTF-8 locales!", path(_path), detail::make_error_code(detail::portable_error::not_supported));
+ }
+}
+#endif
+
+#ifdef GHC_EXPAND_IMPL
+
+GHC_INLINE path::~path() {}
+
+//-----------------------------------------------------------------------------
+// [fs.path.assign] assignments
+
+GHC_INLINE path& path::operator=(const path& p)
+{
+ _path = p._path;
+#if defined(GHC_OS_WINDOWS) && defined(GHC_WIN_AUTO_PREFIX_LONG_PATH)
+ _prefixLength = p._prefixLength;
+#endif
+ return *this;
+}
+
+GHC_INLINE path& path::operator=(path&& p) noexcept
+{
+ _path = std::move(p._path);
+#if defined(GHC_OS_WINDOWS) && defined(GHC_WIN_AUTO_PREFIX_LONG_PATH)
+ _prefixLength = p._prefixLength;
+#endif
+ return *this;
+}
+
+GHC_INLINE path& path::operator=(path::string_type&& source)
+{
+ return assign(source);
+}
+
+GHC_INLINE path& path::assign(path::string_type&& source)
+{
+ _path = std::move(source);
+ postprocess_path_with_format(native_format);
+ return *this;
+}
+
+#endif // GHC_EXPAND_IMPL
+
+template <class Source>
+inline path& path::operator=(const Source& source)
+{
+ return assign(source);
+}
+
+template <class Source>
+inline path& path::assign(const Source& source)
+{
+#ifdef GHC_USE_WCHAR_T
+ _path.assign(detail::toWChar(source));
+#else
+ _path.assign(detail::toUtf8(source));
+#endif
+ postprocess_path_with_format(native_format);
+ return *this;
+}
+
+template <>
+inline path& path::assign<path>(const path& source)
+{
+ _path = source._path;
+#if defined(GHC_OS_WINDOWS) && defined(GHC_WIN_AUTO_PREFIX_LONG_PATH)
+ _prefixLength = source._prefixLength;
+#endif
+ return *this;
+}
+
+template <class InputIterator>
+inline path& path::assign(InputIterator first, InputIterator last)
+{
+ _path.assign(first, last);
+ postprocess_path_with_format(native_format);
+ return *this;
+}
+
+#ifdef GHC_EXPAND_IMPL
+
+//-----------------------------------------------------------------------------
+// [fs.path.append] appends
+
+GHC_INLINE path& path::operator/=(const path& p)
+{
+ if (p.empty()) {
+ // was: if ((!has_root_directory() && is_absolute()) || has_filename())
+ if (!_path.empty() && _path[_path.length() - 1] != preferred_separator && _path[_path.length() - 1] != ':') {
+ _path += preferred_separator;
+ }
+ return *this;
+ }
+ if ((p.is_absolute() && (_path != root_name()._path || p._path != "/")) || (p.has_root_name() && p.root_name() != root_name())) {
+ assign(p);
+ return *this;
+ }
+ if (p.has_root_directory()) {
+ assign(root_name());
+ }
+ else if ((!has_root_directory() && is_absolute()) || has_filename()) {
+ _path += preferred_separator;
+ }
+ auto iter = p.begin();
+ bool first = true;
+ if (p.has_root_name()) {
+ ++iter;
+ }
+ while (iter != p.end()) {
+ if (!first && !(!_path.empty() && _path[_path.length() - 1] == preferred_separator)) {
+ _path += preferred_separator;
+ }
+ first = false;
+ _path += (*iter++).native();
+ }
+ check_long_path();
+ return *this;
+}
+
+GHC_INLINE void path::append_name(const value_type* name)
+{
+ if (_path.empty()) {
+ this->operator/=(path(name));
+ }
+ else {
+ if (_path.back() != path::preferred_separator) {
+ _path.push_back(path::preferred_separator);
+ }
+ _path += name;
+ check_long_path();
+ }
+}
+
+#endif // GHC_EXPAND_IMPL
+
+template <class Source>
+inline path& path::operator/=(const Source& source)
+{
+ return append(source);
+}
+
+template <class Source>
+inline path& path::append(const Source& source)
+{
+ return this->operator/=(path(source));
+}
+
+template <>
+inline path& path::append<path>(const path& p)
+{
+ return this->operator/=(p);
+}
+
+template <class InputIterator>
+inline path& path::append(InputIterator first, InputIterator last)
+{
+ std::basic_string<typename std::iterator_traits<InputIterator>::value_type> part(first, last);
+ return append(part);
+}
+
+#ifdef GHC_EXPAND_IMPL
+
+//-----------------------------------------------------------------------------
+// [fs.path.concat] concatenation
+
+GHC_INLINE path& path::operator+=(const path& x)
+{
+ return concat(x._path);
+}
+
+GHC_INLINE path& path::operator+=(const string_type& x)
+{
+ return concat(x);
+}
+
+#ifdef GHC_WITH_STRING_VIEW
+GHC_INLINE path& path::operator+=(basic_string_view<value_type> x)
+{
+ return concat(x);
+}
+#endif
+
+GHC_INLINE path& path::operator+=(const value_type* x)
+{
+#ifdef GHC_WITH_STRING_VIEW
+ basic_string_view<value_type> part(x);
+#else
+ string_type part(x);
+#endif
+ return concat(part);
+}
+
+GHC_INLINE path& path::operator+=(value_type x)
+{
+#ifdef GHC_OS_WINDOWS
+ if (x == generic_separator) {
+ x = preferred_separator;
+ }
+#endif
+ if (_path.empty() || _path.back() != preferred_separator) {
+ _path += x;
+ }
+ check_long_path();
+ return *this;
+}
+
+#endif // GHC_EXPAND_IMPL
+
+template <class Source>
+inline path::path_from_string<Source>& path::operator+=(const Source& x)
+{
+ return concat(x);
+}
+
+template <class EcharT>
+inline path::path_type_EcharT<EcharT>& path::operator+=(EcharT x)
+{
+#ifdef GHC_WITH_STRING_VIEW
+ basic_string_view<EcharT> part(&x, 1);
+#else
+ std::basic_string<EcharT> part(1, x);
+#endif
+ concat(part);
+ return *this;
+}
+
+template <class Source>
+inline path& path::concat(const Source& x)
+{
+ path p(x);
+ _path += p._path;
+ postprocess_path_with_format(native_format);
+ return *this;
+}
+template <class InputIterator>
+inline path& path::concat(InputIterator first, InputIterator last)
+{
+ _path.append(first, last);
+ postprocess_path_with_format(native_format);
+ return *this;
+}
+
+#ifdef GHC_EXPAND_IMPL
+
+//-----------------------------------------------------------------------------
+// [fs.path.modifiers] modifiers
+GHC_INLINE void path::clear() noexcept
+{
+ _path.clear();
+#if defined(GHC_OS_WINDOWS) && defined(GHC_WIN_AUTO_PREFIX_LONG_PATH)
+ _prefixLength = 0;
+#endif
+}
+
+GHC_INLINE path& path::make_preferred()
+{
+ // as this filesystem implementation only uses generic_format
+ // internally, this must be a no-op
+ return *this;
+}
+
+GHC_INLINE path& path::remove_filename()
+{
+ if (has_filename()) {
+ _path.erase(_path.size() - filename()._path.size());
+ }
+ return *this;
+}
+
+GHC_INLINE path& path::replace_filename(const path& replacement)
+{
+ remove_filename();
+ return append(replacement);
+}
+
+GHC_INLINE path& path::replace_extension(const path& replacement)
+{
+ if (has_extension()) {
+ _path.erase(_path.size() - extension()._path.size());
+ }
+ if (!replacement.empty() && replacement._path[0] != '.') {
+ _path += '.';
+ }
+ return concat(replacement);
+}
+
+GHC_INLINE void path::swap(path& rhs) noexcept
+{
+ _path.swap(rhs._path);
+#if defined(GHC_OS_WINDOWS) && defined(GHC_WIN_AUTO_PREFIX_LONG_PATH)
+ std::swap(_prefixLength, rhs._prefixLength);
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// [fs.path.native.obs] native format observers
+GHC_INLINE const path::string_type& path::native() const noexcept
+{
+ return _path;
+}
+
+GHC_INLINE const path::value_type* path::c_str() const noexcept
+{
+ return native().c_str();
+}
+
+GHC_INLINE path::operator path::string_type() const
+{
+ return native();
+}
+
+#endif // GHC_EXPAND_IMPL
+
+template <class EcharT, class traits, class Allocator>
+inline std::basic_string<EcharT, traits, Allocator> path::string(const Allocator& a) const
+{
+#ifdef GHC_USE_WCHAR_T
+ return detail::fromWChar<std::basic_string<EcharT, traits, Allocator>>(_path, a);
+#else
+ return detail::fromUtf8<std::basic_string<EcharT, traits, Allocator>>(_path, a);
+#endif
+}
+
+#ifdef GHC_EXPAND_IMPL
+
+GHC_INLINE std::string path::string() const
+{
+#ifdef GHC_USE_WCHAR_T
+ return detail::toUtf8(native());
+#else
+ return native();
+#endif
+}
+
+GHC_INLINE std::wstring path::wstring() const
+{
+#ifdef GHC_USE_WCHAR_T
+ return native();
+#else
+ return detail::fromUtf8<std::wstring>(native());
+#endif
+}
+
+#if defined(__cpp_lib_char8_t) && !defined(GHC_FILESYSTEM_ENFORCE_CPP17_API)
+GHC_INLINE std::u8string path::u8string() const
+{
+#ifdef GHC_USE_WCHAR_T
+ return std::u8string(reinterpret_cast<const char8_t*>(detail::toUtf8(native()).c_str()));
+#else
+ return std::u8string(reinterpret_cast<const char8_t*>(c_str()));
+#endif
+}
+#else
+GHC_INLINE std::string path::u8string() const
+{
+#ifdef GHC_USE_WCHAR_T
+ return detail::toUtf8(native());
+#else
+ return native();
+#endif
+}
+#endif
+
+GHC_INLINE std::u16string path::u16string() const
+{
+ // TODO: optimize
+ return detail::fromUtf8<std::u16string>(string());
+}
+
+GHC_INLINE std::u32string path::u32string() const
+{
+ // TODO: optimize
+ return detail::fromUtf8<std::u32string>(string());
+}
+
+#endif // GHC_EXPAND_IMPL
+
+//-----------------------------------------------------------------------------
+// [fs.path.generic.obs] generic format observers
+template <class EcharT, class traits, class Allocator>
+inline std::basic_string<EcharT, traits, Allocator> path::generic_string(const Allocator& a) const
+{
+#ifdef GHC_OS_WINDOWS
+#ifdef GHC_USE_WCHAR_T
+ auto result = detail::fromWChar<std::basic_string<EcharT, traits, Allocator>, path::string_type>(_path, a);
+#else
+ auto result = detail::fromUtf8<std::basic_string<EcharT, traits, Allocator>>(_path, a);
+#endif
+ for (auto& c : result) {
+ if (c == preferred_separator) {
+ c = generic_separator;
+ }
+ }
+ return result;
+#else
+ return detail::fromUtf8<std::basic_string<EcharT, traits, Allocator>>(_path, a);
+#endif
+}
+
+#ifdef GHC_EXPAND_IMPL
+
+GHC_INLINE std::string path::generic_string() const
+{
+#ifdef GHC_OS_WINDOWS
+ return generic_string<std::string::value_type, std::string::traits_type, std::string::allocator_type>();
+#else
+ return _path;
+#endif
+}
+
+GHC_INLINE std::wstring path::generic_wstring() const
+{
+#ifdef GHC_OS_WINDOWS
+ return generic_string<std::wstring::value_type, std::wstring::traits_type, std::wstring::allocator_type>();
+#else
+ return detail::fromUtf8<std::wstring>(_path);
+#endif
+} // namespace filesystem
+
+#if defined(__cpp_lib_char8_t) && !defined(GHC_FILESYSTEM_ENFORCE_CPP17_API)
+GHC_INLINE std::u8string path::generic_u8string() const
+{
+#ifdef GHC_OS_WINDOWS
+ return generic_string<std::u8string::value_type, std::u8string::traits_type, std::u8string::allocator_type>();
+#else
+ return std::u8string(reinterpret_cast<const char8_t*>(_path.c_str()));
+#endif
+}
+#else
+GHC_INLINE std::string path::generic_u8string() const
+{
+#ifdef GHC_OS_WINDOWS
+ return generic_string<std::string::value_type, std::string::traits_type, std::string::allocator_type>();
+#else
+ return _path;
+#endif
+}
+#endif
+
+GHC_INLINE std::u16string path::generic_u16string() const
+{
+#ifdef GHC_OS_WINDOWS
+ return generic_string<std::u16string::value_type, std::u16string::traits_type, std::u16string::allocator_type>();
+#else
+ return detail::fromUtf8<std::u16string>(_path);
+#endif
+}
+
+GHC_INLINE std::u32string path::generic_u32string() const
+{
+#ifdef GHC_OS_WINDOWS
+ return generic_string<std::u32string::value_type, std::u32string::traits_type, std::u32string::allocator_type>();
+#else
+ return detail::fromUtf8<std::u32string>(_path);
+#endif
+}
+
+//-----------------------------------------------------------------------------
+// [fs.path.compare] compare
+GHC_INLINE int path::compare(const path& p) const noexcept
+{
+#ifdef LWG_2936_BEHAVIOUR
+ auto rnl1 = root_name_length();
+ auto rnl2 = p.root_name_length();
+#ifdef GHC_OS_WINDOWS
+ auto rnc = detail::compare_simple_insensitive(_path.c_str(), rnl1, p._path.c_str(), rnl2);
+#else
+ auto rnc = _path.compare(0, rnl1, p._path, 0, (std::min(rnl1, rnl2)));
+#endif
+ if (rnc) {
+ return rnc;
+ }
+ bool hrd1 = has_root_directory(), hrd2 = p.has_root_directory();
+ if (hrd1 != hrd2) {
+ return hrd1 ? 1 : -1;
+ }
+ if (hrd1) {
+ ++rnl1;
+ ++rnl2;
+ }
+ auto iter1 = _path.begin() + static_cast<int>(rnl1);
+ auto iter2 = p._path.begin() + static_cast<int>(rnl2);
+ while (iter1 != _path.end() && iter2 != p._path.end() && *iter1 == *iter2) {
+ ++iter1;
+ ++iter2;
+ }
+ if (iter1 == _path.end()) {
+ return iter2 == p._path.end() ? 0 : -1;
+ }
+ if (iter2 == p._path.end()) {
+ return 1;
+ }
+ if (*iter1 == preferred_separator) {
+ return -1;
+ }
+ if (*iter2 == preferred_separator) {
+ return 1;
+ }
+ return *iter1 < *iter2 ? -1 : 1;
+#else // LWG_2936_BEHAVIOUR
+#ifdef GHC_OS_WINDOWS
+ auto rnl1 = root_name_length();
+ auto rnl2 = p.root_name_length();
+ auto rnc = detail::compare_simple_insensitive(_path.c_str(), rnl1, p._path.c_str(), rnl2);
+ if (rnc) {
+ return rnc;
+ }
+ return _path.compare(rnl1, std::string::npos, p._path, rnl2, std::string::npos);
+#else
+ return _path.compare(p._path);
+#endif
+#endif
+}
+
+GHC_INLINE int path::compare(const string_type& s) const
+{
+ return compare(path(s));
+}
+
+#ifdef GHC_WITH_STRING_VIEW
+GHC_INLINE int path::compare(basic_string_view<value_type> s) const
+{
+ return compare(path(s));
+}
+#endif
+
+GHC_INLINE int path::compare(const value_type* s) const
+{
+ return compare(path(s));
+}
+
+//-----------------------------------------------------------------------------
+// [fs.path.decompose] decomposition
+#ifdef GHC_OS_WINDOWS
+GHC_INLINE void path::handle_prefixes()
+{
+#if defined(GHC_WIN_AUTO_PREFIX_LONG_PATH)
+ _prefixLength = 0;
+ if (_path.length() >= 6 && _path[2] == '?' && std::toupper(static_cast<unsigned char>(_path[4])) >= 'A' && std::toupper(static_cast<unsigned char>(_path[4])) <= 'Z' && _path[5] == ':') {
+ if (detail::startsWith(_path, impl_string_type(GHC_PLATFORM_LITERAL("\\\\?\\"))) || detail::startsWith(_path, impl_string_type(GHC_PLATFORM_LITERAL("\\??\\")))) {
+ _prefixLength = 4;
+ }
+ }
+#endif // GHC_WIN_AUTO_PREFIX_LONG_PATH
+}
+#endif
+
+GHC_INLINE path::string_type::size_type path::root_name_length() const noexcept
+{
+#ifdef GHC_OS_WINDOWS
+ if (_path.length() >= _prefixLength + 2 && std::toupper(static_cast<unsigned char>(_path[_prefixLength])) >= 'A' && std::toupper(static_cast<unsigned char>(_path[_prefixLength])) <= 'Z' && _path[_prefixLength + 1] == ':') {
+ return 2;
+ }
+#endif
+ if (_path.length() > _prefixLength + 2 && _path[_prefixLength] == preferred_separator && _path[_prefixLength + 1] == preferred_separator && _path[_prefixLength + 2] != preferred_separator && std::isprint(_path[_prefixLength + 2])) {
+ impl_string_type::size_type pos = _path.find(preferred_separator, _prefixLength + 3);
+ if (pos == impl_string_type::npos) {
+ return _path.length();
+ }
+ else {
+ return pos;
+ }
+ }
+ return 0;
+}
+
+GHC_INLINE path path::root_name() const
+{
+ return path(_path.substr(_prefixLength, root_name_length()), native_format);
+}
+
+GHC_INLINE path path::root_directory() const
+{
+ if (has_root_directory()) {
+ static const path _root_dir(std::string(1, preferred_separator), native_format);
+ return _root_dir;
+ }
+ return path();
+}
+
+GHC_INLINE path path::root_path() const
+{
+ return path(root_name().string() + root_directory().string(), native_format);
+}
+
+GHC_INLINE path path::relative_path() const
+{
+ auto rootPathLen = _prefixLength + root_name_length() + (has_root_directory() ? 1 : 0);
+ return path(_path.substr((std::min)(rootPathLen, _path.length())), generic_format);
+}
+
+GHC_INLINE path path::parent_path() const
+{
+ auto rootPathLen = _prefixLength + root_name_length() + (has_root_directory() ? 1 : 0);
+ if (rootPathLen < _path.length()) {
+ if (empty()) {
+ return path();
+ }
+ else {
+ auto piter = end();
+ auto iter = piter.decrement(_path.end());
+ if (iter > _path.begin() + static_cast<long>(rootPathLen) && *iter != preferred_separator) {
+ --iter;
+ }
+ return path(_path.begin(), iter, native_format);
+ }
+ }
+ else {
+ return *this;
+ }
+}
+
+GHC_INLINE path path::filename() const
+{
+ return !has_relative_path() ? path() : path(*--end());
+}
+
+GHC_INLINE path path::stem() const
+{
+ impl_string_type fn = filename().native();
+ if (fn != "." && fn != "..") {
+ impl_string_type::size_type pos = fn.rfind('.');
+ if (pos != impl_string_type::npos && pos > 0) {
+ return path{fn.substr(0, pos), native_format};
+ }
+ }
+ return path{fn, native_format};
+}
+
+GHC_INLINE path path::extension() const
+{
+ if (has_relative_path()) {
+ auto iter = end();
+ const auto& fn = *--iter;
+ impl_string_type::size_type pos = fn._path.rfind('.');
+ if (pos != std::string::npos && pos > 0) {
+ return path(fn._path.substr(pos), native_format);
+ }
+ }
+ return path();
+}
+
+#ifdef GHC_OS_WINDOWS
+namespace detail {
+GHC_INLINE bool has_executable_extension(const path& p)
+{
+ if (p.has_relative_path()) {
+ auto iter = p.end();
+ const auto& fn = *--iter;
+ auto pos = fn._path.find_last_of('.');
+ if (pos == std::string::npos || pos == 0 || fn._path.length() - pos != 3) {
+ return false;
+ }
+ const path::value_type* ext = fn._path.c_str() + pos + 1;
+ if (detail::equals_simple_insensitive(ext, GHC_PLATFORM_LITERAL("exe")) || detail::equals_simple_insensitive(ext, GHC_PLATFORM_LITERAL("cmd")) || detail::equals_simple_insensitive(ext, GHC_PLATFORM_LITERAL("bat")) ||
+ detail::equals_simple_insensitive(ext, GHC_PLATFORM_LITERAL("com"))) {
+ return true;
+ }
+ }
+ return false;
+}
+} // namespace detail
+#endif
+
+//-----------------------------------------------------------------------------
+// [fs.path.query] query
+GHC_INLINE bool path::empty() const noexcept
+{
+ return _path.empty();
+}
+
+GHC_INLINE bool path::has_root_name() const
+{
+ return root_name_length() > 0;
+}
+
+GHC_INLINE bool path::has_root_directory() const
+{
+ auto rootLen = _prefixLength + root_name_length();
+ return (_path.length() > rootLen && _path[rootLen] == preferred_separator);
+}
+
+GHC_INLINE bool path::has_root_path() const
+{
+ return has_root_name() || has_root_directory();
+}
+
+GHC_INLINE bool path::has_relative_path() const
+{
+ auto rootPathLen = _prefixLength + root_name_length() + (has_root_directory() ? 1 : 0);
+ return rootPathLen < _path.length();
+}
+
+GHC_INLINE bool path::has_parent_path() const
+{
+ return !parent_path().empty();
+}
+
+GHC_INLINE bool path::has_filename() const
+{
+ return has_relative_path() && !filename().empty();
+}
+
+GHC_INLINE bool path::has_stem() const
+{
+ return !stem().empty();
+}
+
+GHC_INLINE bool path::has_extension() const
+{
+ return !extension().empty();
+}
+
+GHC_INLINE bool path::is_absolute() const
+{
+#ifdef GHC_OS_WINDOWS
+ return has_root_name() && has_root_directory();
+#else
+ return has_root_directory();
+#endif
+}
+
+GHC_INLINE bool path::is_relative() const
+{
+ return !is_absolute();
+}
+
+//-----------------------------------------------------------------------------
+// [fs.path.gen] generation
+GHC_INLINE path path::lexically_normal() const
+{
+ path dest;
+ bool lastDotDot = false;
+ for (string_type s : *this) {
+ if (s == ".") {
+ dest /= "";
+ continue;
+ }
+ else if (s == ".." && !dest.empty()) {
+ auto root = root_path();
+ if (dest == root) {
+ continue;
+ }
+ else if (*(--dest.end()) != "..") {
+ if (dest._path.back() == preferred_separator) {
+ dest._path.pop_back();
+ }
+ dest.remove_filename();
+ continue;
+ }
+ }
+ if (!(s.empty() && lastDotDot)) {
+ dest /= s;
+ }
+ lastDotDot = s == "..";
+ }
+ if (dest.empty()) {
+ dest = ".";
+ }
+ return dest;
+}
+
+GHC_INLINE path path::lexically_relative(const path& base) const
+{
+ if (root_name() != base.root_name() || is_absolute() != base.is_absolute() || (!has_root_directory() && base.has_root_directory())) {
+ return path();
+ }
+ const_iterator a = begin(), b = base.begin();
+ while (a != end() && b != base.end() && *a == *b) {
+ ++a;
+ ++b;
+ }
+ if (a == end() && b == base.end()) {
+ return path(".");
+ }
+ int count = 0;
+ for (const auto& element : input_iterator_range<const_iterator>(b, base.end())) {
+ if (element != "." && element != "" && element != "..") {
+ ++count;
+ }
+ else if (element == "..") {
+ --count;
+ }
+ }
+ if (count < 0) {
+ return path();
+ }
+ path result;
+ for (int i = 0; i < count; ++i) {
+ result /= "..";
+ }
+ for (const auto& element : input_iterator_range<const_iterator>(a, end())) {
+ result /= element;
+ }
+ return result;
+}
+
+GHC_INLINE path path::lexically_proximate(const path& base) const
+{
+ path result = lexically_relative(base);
+ return result.empty() ? *this : result;
+}
+
+//-----------------------------------------------------------------------------
+// [fs.path.itr] iterators
+GHC_INLINE path::iterator::iterator() {}
+
+GHC_INLINE path::iterator::iterator(const path& p, const impl_string_type::const_iterator& pos)
+ : _first(p._path.begin())
+ , _last(p._path.end())
+ , _prefix(_first + static_cast<string_type::difference_type>(p._prefixLength))
+ , _root(p.has_root_directory() ? _first + static_cast<string_type::difference_type>(p._prefixLength + p.root_name_length()) : _last)
+ , _iter(pos)
+{
+ if (pos != _last) {
+ updateCurrent();
+ }
+}
+
+GHC_INLINE path::impl_string_type::const_iterator path::iterator::increment(const path::impl_string_type::const_iterator& pos) const
+{
+ path::impl_string_type::const_iterator i = pos;
+ bool fromStart = i == _first || i == _prefix;
+ if (i != _last) {
+ if (fromStart && i == _first && _prefix > _first) {
+ i = _prefix;
+ }
+ else if (*i++ == preferred_separator) {
+ // we can only sit on a slash if it is a network name or a root
+ if (i != _last && *i == preferred_separator) {
+ if (fromStart && !(i + 1 != _last && *(i + 1) == preferred_separator)) {
+ // leadind double slashes detected, treat this and the
+ // following until a slash as one unit
+ i = std::find(++i, _last, preferred_separator);
+ }
+ else {
+ // skip redundant slashes
+ while (i != _last && *i == preferred_separator) {
+ ++i;
+ }
+ }
+ }
+ }
+ else {
+ if (fromStart && i != _last && *i == ':') {
+ ++i;
+ }
+ else {
+ i = std::find(i, _last, preferred_separator);
+ }
+ }
+ }
+ return i;
+}
+
+GHC_INLINE path::impl_string_type::const_iterator path::iterator::decrement(const path::impl_string_type::const_iterator& pos) const
+{
+ path::impl_string_type::const_iterator i = pos;
+ if (i != _first) {
+ --i;
+ // if this is now the root slash or the trailing slash, we are done,
+ // else check for network name
+ if (i != _root && (pos != _last || *i != preferred_separator)) {
+#ifdef GHC_OS_WINDOWS
+ static const impl_string_type seps = GHC_PLATFORM_LITERAL("\\:");
+ i = std::find_first_of(std::reverse_iterator<path::impl_string_type::const_iterator>(i), std::reverse_iterator<path::impl_string_type::const_iterator>(_first), seps.begin(), seps.end()).base();
+ if (i > _first && *i == ':') {
+ i++;
+ }
+#else
+ i = std::find(std::reverse_iterator<path::impl_string_type::const_iterator>(i), std::reverse_iterator<path::impl_string_type::const_iterator>(_first), preferred_separator).base();
+#endif
+ // Now we have to check if this is a network name
+ if (i - _first == 2 && *_first == preferred_separator && *(_first + 1) == preferred_separator) {
+ i -= 2;
+ }
+ }
+ }
+ return i;
+}
+
+GHC_INLINE void path::iterator::updateCurrent()
+{
+ if ((_iter == _last) || (_iter != _first && _iter != _last && (*_iter == preferred_separator && _iter != _root) && (_iter + 1 == _last))) {
+ _current.clear();
+ }
+ else {
+ _current.assign(_iter, increment(_iter));
+ }
+}
+
+GHC_INLINE path::iterator& path::iterator::operator++()
+{
+ _iter = increment(_iter);
+ while (_iter != _last && // we didn't reach the end
+ _iter != _root && // this is not a root position
+ *_iter == preferred_separator && // we are on a separator
+ (_iter + 1) != _last // the slash is not the last char
+ ) {
+ ++_iter;
+ }
+ updateCurrent();
+ return *this;
+}
+
+GHC_INLINE path::iterator path::iterator::operator++(int)
+{
+ path::iterator i{*this};
+ ++(*this);
+ return i;
+}
+
+GHC_INLINE path::iterator& path::iterator::operator--()
+{
+ _iter = decrement(_iter);
+ updateCurrent();
+ return *this;
+}
+
+GHC_INLINE path::iterator path::iterator::operator--(int)
+{
+ auto i = *this;
+ --(*this);
+ return i;
+}
+
+GHC_INLINE bool path::iterator::operator==(const path::iterator& other) const
+{
+ return _iter == other._iter;
+}
+
+GHC_INLINE bool path::iterator::operator!=(const path::iterator& other) const
+{
+ return _iter != other._iter;
+}
+
+GHC_INLINE path::iterator::reference path::iterator::operator*() const
+{
+ return _current;
+}
+
+GHC_INLINE path::iterator::pointer path::iterator::operator->() const
+{
+ return &_current;
+}
+
+GHC_INLINE path::iterator path::begin() const
+{
+ return iterator(*this, _path.begin());
+}
+
+GHC_INLINE path::iterator path::end() const
+{
+ return iterator(*this, _path.end());
+}
+
+//-----------------------------------------------------------------------------
+// [fs.path.nonmember] path non-member functions
+GHC_INLINE void swap(path& lhs, path& rhs) noexcept
+{
+ swap(lhs._path, rhs._path);
+}
+
+GHC_INLINE size_t hash_value(const path& p) noexcept
+{
+ return std::hash<std::string>()(p.generic_string());
+}
+
+#ifdef GHC_HAS_THREEWAY_COMP
+GHC_INLINE std::strong_ordering operator<=>(const path& lhs, const path& rhs) noexcept
+{
+ return lhs.compare(rhs) <=> 0;
+}
+#endif
+
+GHC_INLINE bool operator==(const path& lhs, const path& rhs) noexcept
+{
+ return lhs.compare(rhs) == 0;
+}
+
+GHC_INLINE bool operator!=(const path& lhs, const path& rhs) noexcept
+{
+ return !(lhs == rhs);
+}
+
+GHC_INLINE bool operator<(const path& lhs, const path& rhs) noexcept
+{
+ return lhs.compare(rhs) < 0;
+}
+
+GHC_INLINE bool operator<=(const path& lhs, const path& rhs) noexcept
+{
+ return lhs.compare(rhs) <= 0;
+}
+
+GHC_INLINE bool operator>(const path& lhs, const path& rhs) noexcept
+{
+ return lhs.compare(rhs) > 0;
+}
+
+GHC_INLINE bool operator>=(const path& lhs, const path& rhs) noexcept
+{
+ return lhs.compare(rhs) >= 0;
+}
+
+GHC_INLINE path operator/(const path& lhs, const path& rhs)
+{
+ path result(lhs);
+ result /= rhs;
+ return result;
+}
+
+#endif // GHC_EXPAND_IMPL
+
+//-----------------------------------------------------------------------------
+// [fs.path.io] path inserter and extractor
+template <class charT, class traits>
+inline std::basic_ostream<charT, traits>& operator<<(std::basic_ostream<charT, traits>& os, const path& p)
+{
+ os << "\"";
+ auto ps = p.string<charT, traits>();
+ for (auto c : ps) {
+ if (c == '"' || c == '\\') {
+ os << '\\';
+ }
+ os << c;
+ }
+ os << "\"";
+ return os;
+}
+
+template <class charT, class traits>
+inline std::basic_istream<charT, traits>& operator>>(std::basic_istream<charT, traits>& is, path& p)
+{
+ std::basic_string<charT, traits> tmp;
+ charT c;
+ is >> c;
+ if (c == '"') {
+ auto sf = is.flags();
+ is >> std::noskipws;
+ while (is) {
+ auto c2 = is.get();
+ if (is) {
+ if (c2 == '\\') {
+ c2 = is.get();
+ if (is) {
+ tmp += static_cast<charT>(c2);
+ }
+ }
+ else if (c2 == '"') {
+ break;
+ }
+ else {
+ tmp += static_cast<charT>(c2);
+ }
+ }
+ }
+ if ((sf & std::ios_base::skipws) == std::ios_base::skipws) {
+ is >> std::skipws;
+ }
+ p = path(tmp);
+ }
+ else {
+ is >> tmp;
+ p = path(static_cast<charT>(c) + tmp);
+ }
+ return is;
+}
+
+#ifdef GHC_EXPAND_IMPL
+
+//-----------------------------------------------------------------------------
+// [fs.class.filesystem_error] Class filesystem_error
+GHC_INLINE filesystem_error::filesystem_error(const std::string& what_arg, std::error_code ec)
+ : std::system_error(ec, what_arg)
+ , _what_arg(what_arg)
+ , _ec(ec)
+{
+}
+
+GHC_INLINE filesystem_error::filesystem_error(const std::string& what_arg, const path& p1, std::error_code ec)
+ : std::system_error(ec, what_arg)
+ , _what_arg(what_arg)
+ , _ec(ec)
+ , _p1(p1)
+{
+ if (!_p1.empty()) {
+ _what_arg += ": '" + _p1.string() + "'";
+ }
+}
+
+GHC_INLINE filesystem_error::filesystem_error(const std::string& what_arg, const path& p1, const path& p2, std::error_code ec)
+ : std::system_error(ec, what_arg)
+ , _what_arg(what_arg)
+ , _ec(ec)
+ , _p1(p1)
+ , _p2(p2)
+{
+ if (!_p1.empty()) {
+ _what_arg += ": '" + _p1.string() + "'";
+ }
+ if (!_p2.empty()) {
+ _what_arg += ", '" + _p2.string() + "'";
+ }
+}
+
+GHC_INLINE const path& filesystem_error::path1() const noexcept
+{
+ return _p1;
+}
+
+GHC_INLINE const path& filesystem_error::path2() const noexcept
+{
+ return _p2;
+}
+
+GHC_INLINE const char* filesystem_error::what() const noexcept
+{
+ return _what_arg.c_str();
+}
+
+//-----------------------------------------------------------------------------
+// [fs.op.funcs] filesystem operations
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE path absolute(const path& p)
+{
+ std::error_code ec;
+ path result = absolute(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE path absolute(const path& p, std::error_code& ec)
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ if (p.empty()) {
+ return absolute(current_path(ec), ec) / "";
+ }
+ ULONG size = ::GetFullPathNameW(GHC_NATIVEWP(p), 0, 0, 0);
+ if (size) {
+ std::vector<wchar_t> buf(size, 0);
+ ULONG s2 = GetFullPathNameW(GHC_NATIVEWP(p), size, buf.data(), nullptr);
+ if (s2 && s2 < size) {
+ path result = path(std::wstring(buf.data(), s2));
+ if (p.filename() == ".") {
+ result /= ".";
+ }
+ return result;
+ }
+ }
+ ec = detail::make_system_error();
+ return path();
+#else
+ path base = current_path(ec);
+ if (!ec) {
+ if (p.empty()) {
+ return base / p;
+ }
+ if (p.has_root_name()) {
+ if (p.has_root_directory()) {
+ return p;
+ }
+ else {
+ return p.root_name() / base.root_directory() / base.relative_path() / p.relative_path();
+ }
+ }
+ else {
+ if (p.has_root_directory()) {
+ return base.root_name() / p;
+ }
+ else {
+ return base / p;
+ }
+ }
+ }
+ ec = detail::make_system_error();
+ return path();
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE path canonical(const path& p)
+{
+ std::error_code ec;
+ auto result = canonical(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE path canonical(const path& p, std::error_code& ec)
+{
+ if (p.empty()) {
+ ec = detail::make_error_code(detail::portable_error::not_found);
+ return path();
+ }
+ path work = p.is_absolute() ? p : absolute(p, ec);
+ path result;
+
+ auto fs = status(work, ec);
+ if (ec) {
+ return path();
+ }
+ if (fs.type() == file_type::not_found) {
+ ec = detail::make_error_code(detail::portable_error::not_found);
+ return path();
+ }
+ bool redo;
+ do {
+ auto rootPathLen = work._prefixLength + work.root_name_length() + (work.has_root_directory() ? 1 : 0);
+ redo = false;
+ result.clear();
+ for (auto pe : work) {
+ if (pe.empty() || pe == ".") {
+ continue;
+ }
+ else if (pe == "..") {
+ result = result.parent_path();
+ continue;
+ }
+ else if ((result / pe).string().length() <= rootPathLen) {
+ result /= pe;
+ continue;
+ }
+ auto sls = symlink_status(result / pe, ec);
+ if (ec) {
+ return path();
+ }
+ if (is_symlink(sls)) {
+ redo = true;
+ auto target = read_symlink(result / pe, ec);
+ if (ec) {
+ return path();
+ }
+ if (target.is_absolute()) {
+ result = target;
+ continue;
+ }
+ else {
+ result /= target;
+ continue;
+ }
+ }
+ else {
+ result /= pe;
+ }
+ }
+ work = result;
+ } while (redo);
+ ec.clear();
+ return result;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void copy(const path& from, const path& to)
+{
+ copy(from, to, copy_options::none);
+}
+
+GHC_INLINE void copy(const path& from, const path& to, copy_options options)
+{
+ std::error_code ec;
+ copy(from, to, options, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), from, to, ec);
+ }
+}
+#endif
+
+GHC_INLINE void copy(const path& from, const path& to, std::error_code& ec) noexcept
+{
+ copy(from, to, copy_options::none, ec);
+}
+
+GHC_INLINE void copy(const path& from, const path& to, copy_options options, std::error_code& ec) noexcept
+{
+ std::error_code tec;
+ file_status fs_from, fs_to;
+ ec.clear();
+ if ((options & (copy_options::skip_symlinks | copy_options::copy_symlinks | copy_options::create_symlinks)) != copy_options::none) {
+ fs_from = symlink_status(from, ec);
+ }
+ else {
+ fs_from = status(from, ec);
+ }
+ if (!exists(fs_from)) {
+ if (!ec) {
+ ec = detail::make_error_code(detail::portable_error::not_found);
+ }
+ return;
+ }
+ if ((options & (copy_options::skip_symlinks | copy_options::create_symlinks)) != copy_options::none) {
+ fs_to = symlink_status(to, tec);
+ }
+ else {
+ fs_to = status(to, tec);
+ }
+ if (is_other(fs_from) || is_other(fs_to) || (is_directory(fs_from) && is_regular_file(fs_to)) || (exists(fs_to) && equivalent(from, to, ec))) {
+ ec = detail::make_error_code(detail::portable_error::invalid_argument);
+ }
+ else if (is_symlink(fs_from)) {
+ if ((options & copy_options::skip_symlinks) == copy_options::none) {
+ if (!exists(fs_to) && (options & copy_options::copy_symlinks) != copy_options::none) {
+ copy_symlink(from, to, ec);
+ }
+ else {
+ ec = detail::make_error_code(detail::portable_error::invalid_argument);
+ }
+ }
+ }
+ else if (is_regular_file(fs_from)) {
+ if ((options & copy_options::directories_only) == copy_options::none) {
+ if ((options & copy_options::create_symlinks) != copy_options::none) {
+ create_symlink(from.is_absolute() ? from : canonical(from, ec), to, ec);
+ }
+#ifndef GHC_OS_WEB
+ else if ((options & copy_options::create_hard_links) != copy_options::none) {
+ create_hard_link(from, to, ec);
+ }
+#endif
+ else if (is_directory(fs_to)) {
+ copy_file(from, to / from.filename(), options, ec);
+ }
+ else {
+ copy_file(from, to, options, ec);
+ }
+ }
+ }
+#ifdef LWG_2682_BEHAVIOUR
+ else if (is_directory(fs_from) && (options & copy_options::create_symlinks) != copy_options::none) {
+ ec = detail::make_error_code(detail::portable_error::is_a_directory);
+ }
+#endif
+ else if (is_directory(fs_from) && (options == copy_options::none || (options & copy_options::recursive) != copy_options::none)) {
+ if (!exists(fs_to)) {
+ create_directory(to, from, ec);
+ if (ec) {
+ return;
+ }
+ }
+ for (auto iter = directory_iterator(from, ec); iter != directory_iterator(); iter.increment(ec)) {
+ if (!ec) {
+ copy(iter->path(), to / iter->path().filename(), options | static_cast<copy_options>(0x8000), ec);
+ }
+ if (ec) {
+ return;
+ }
+ }
+ }
+ return;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool copy_file(const path& from, const path& to)
+{
+ return copy_file(from, to, copy_options::none);
+}
+
+GHC_INLINE bool copy_file(const path& from, const path& to, copy_options option)
+{
+ std::error_code ec;
+ auto result = copy_file(from, to, option, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), from, to, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE bool copy_file(const path& from, const path& to, std::error_code& ec) noexcept
+{
+ return copy_file(from, to, copy_options::none, ec);
+}
+
+GHC_INLINE bool copy_file(const path& from, const path& to, copy_options options, std::error_code& ec) noexcept
+{
+ std::error_code tecf, tect;
+ auto sf = status(from, tecf);
+ auto st = status(to, tect);
+ bool overwrite = false;
+ ec.clear();
+ if (!is_regular_file(sf)) {
+ ec = tecf;
+ return false;
+ }
+ if (exists(st) && (!is_regular_file(st) || equivalent(from, to, ec) || (options & (copy_options::skip_existing | copy_options::overwrite_existing | copy_options::update_existing)) == copy_options::none)) {
+ ec = tect ? tect : detail::make_error_code(detail::portable_error::exists);
+ return false;
+ }
+ if (exists(st)) {
+ if ((options & copy_options::update_existing) == copy_options::update_existing) {
+ auto from_time = last_write_time(from, ec);
+ if (ec) {
+ ec = detail::make_system_error();
+ return false;
+ }
+ auto to_time = last_write_time(to, ec);
+ if (ec) {
+ ec = detail::make_system_error();
+ return false;
+ }
+ if (from_time <= to_time) {
+ return false;
+ }
+ }
+ overwrite = true;
+ }
+#ifdef GHC_OS_WINDOWS
+ if (!::CopyFileW(GHC_NATIVEWP(from), GHC_NATIVEWP(to), !overwrite)) {
+ ec = detail::make_system_error();
+ return false;
+ }
+ return true;
+#else
+ std::vector<char> buffer(16384, '\0');
+ int in = -1, out = -1;
+ if ((in = ::open(from.c_str(), O_RDONLY)) < 0) {
+ ec = detail::make_system_error();
+ return false;
+ }
+ int mode = O_CREAT | O_WRONLY | O_TRUNC;
+ if (!overwrite) {
+ mode |= O_EXCL;
+ }
+ if ((out = ::open(to.c_str(), mode, static_cast<int>(sf.permissions() & perms::all))) < 0) {
+ ec = detail::make_system_error();
+ ::close(in);
+ return false;
+ }
+ ssize_t br, bw;
+ while ((br = ::read(in, buffer.data(), buffer.size())) > 0) {
+ ssize_t offset = 0;
+ do {
+ if ((bw = ::write(out, buffer.data() + offset, static_cast<size_t>(br))) > 0) {
+ br -= bw;
+ offset += bw;
+ }
+ else if (bw < 0) {
+ ec = detail::make_system_error();
+ ::close(in);
+ ::close(out);
+ return false;
+ }
+ } while (br);
+ }
+ ::close(in);
+ ::close(out);
+ return true;
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void copy_symlink(const path& existing_symlink, const path& new_symlink)
+{
+ std::error_code ec;
+ copy_symlink(existing_symlink, new_symlink, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), existing_symlink, new_symlink, ec);
+ }
+}
+#endif
+
+GHC_INLINE void copy_symlink(const path& existing_symlink, const path& new_symlink, std::error_code& ec) noexcept
+{
+ ec.clear();
+ auto to = read_symlink(existing_symlink, ec);
+ if (!ec) {
+ if (exists(to, ec) && is_directory(to, ec)) {
+ create_directory_symlink(to, new_symlink, ec);
+ }
+ else {
+ create_symlink(to, new_symlink, ec);
+ }
+ }
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool create_directories(const path& p)
+{
+ std::error_code ec;
+ auto result = create_directories(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE bool create_directories(const path& p, std::error_code& ec) noexcept
+{
+ path current;
+ ec.clear();
+ bool didCreate = false;
+ auto rootPathLen = p._prefixLength + p.root_name_length() + (p.has_root_directory() ? 1 : 0);
+ current = p.native().substr(0, rootPathLen);
+ path folders(p._path.substr(rootPathLen));
+ for (path::string_type part : folders) {
+ current /= part;
+ std::error_code tec;
+ auto fs = status(current, tec);
+ if (tec && fs.type() != file_type::not_found) {
+ ec = tec;
+ return false;
+ }
+ if (!exists(fs)) {
+ create_directory(current, ec);
+ if (ec) {
+ std::error_code tmp_ec;
+ if (is_directory(current, tmp_ec)) {
+ ec.clear();
+ }
+ else {
+ return false;
+ }
+ }
+ didCreate = true;
+ }
+#ifndef LWG_2935_BEHAVIOUR
+ else if (!is_directory(fs)) {
+ ec = detail::make_error_code(detail::portable_error::exists);
+ return false;
+ }
+#endif
+ }
+ return didCreate;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool create_directory(const path& p)
+{
+ std::error_code ec;
+ auto result = create_directory(p, path(), ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE bool create_directory(const path& p, std::error_code& ec) noexcept
+{
+ return create_directory(p, path(), ec);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool create_directory(const path& p, const path& attributes)
+{
+ std::error_code ec;
+ auto result = create_directory(p, attributes, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE bool create_directory(const path& p, const path& attributes, std::error_code& ec) noexcept
+{
+ std::error_code tec;
+ ec.clear();
+ auto fs = status(p, tec);
+#ifdef LWG_2935_BEHAVIOUR
+ if (status_known(fs) && exists(fs)) {
+ return false;
+ }
+#else
+ if (status_known(fs) && exists(fs) && is_directory(fs)) {
+ return false;
+ }
+#endif
+#ifdef GHC_OS_WINDOWS
+ if (!attributes.empty()) {
+ if (!::CreateDirectoryExW(GHC_NATIVEWP(attributes), GHC_NATIVEWP(p), NULL)) {
+ ec = detail::make_system_error();
+ return false;
+ }
+ }
+ else if (!::CreateDirectoryW(GHC_NATIVEWP(p), NULL)) {
+ ec = detail::make_system_error();
+ return false;
+ }
+#else
+ ::mode_t attribs = static_cast<mode_t>(perms::all);
+ if (!attributes.empty()) {
+ struct ::stat fileStat;
+ if (::stat(attributes.c_str(), &fileStat) != 0) {
+ ec = detail::make_system_error();
+ return false;
+ }
+ attribs = fileStat.st_mode;
+ }
+ if (::mkdir(p.c_str(), attribs) != 0) {
+ ec = detail::make_system_error();
+ return false;
+ }
+#endif
+ return true;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void create_directory_symlink(const path& to, const path& new_symlink)
+{
+ std::error_code ec;
+ create_directory_symlink(to, new_symlink, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), to, new_symlink, ec);
+ }
+}
+#endif
+
+GHC_INLINE void create_directory_symlink(const path& to, const path& new_symlink, std::error_code& ec) noexcept
+{
+ detail::create_symlink(to, new_symlink, true, ec);
+}
+
+#ifndef GHC_OS_WEB
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void create_hard_link(const path& to, const path& new_hard_link)
+{
+ std::error_code ec;
+ create_hard_link(to, new_hard_link, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), to, new_hard_link, ec);
+ }
+}
+#endif
+
+GHC_INLINE void create_hard_link(const path& to, const path& new_hard_link, std::error_code& ec) noexcept
+{
+ detail::create_hardlink(to, new_hard_link, ec);
+}
+#endif
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void create_symlink(const path& to, const path& new_symlink)
+{
+ std::error_code ec;
+ create_symlink(to, new_symlink, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), to, new_symlink, ec);
+ }
+}
+#endif
+
+GHC_INLINE void create_symlink(const path& to, const path& new_symlink, std::error_code& ec) noexcept
+{
+ detail::create_symlink(to, new_symlink, false, ec);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE path current_path()
+{
+ std::error_code ec;
+ auto result = current_path(ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE path current_path(std::error_code& ec)
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ DWORD pathlen = ::GetCurrentDirectoryW(0, 0);
+ std::unique_ptr<wchar_t[]> buffer(new wchar_t[size_t(pathlen) + 1]);
+ if (::GetCurrentDirectoryW(pathlen, buffer.get()) == 0) {
+ ec = detail::make_system_error();
+ return path();
+ }
+ return path(std::wstring(buffer.get()), path::native_format);
+#else
+ size_t pathlen = static_cast<size_t>(std::max(int(::pathconf(".", _PC_PATH_MAX)), int(PATH_MAX)));
+ std::unique_ptr<char[]> buffer(new char[pathlen + 1]);
+ if (::getcwd(buffer.get(), pathlen) == nullptr) {
+ ec = detail::make_system_error();
+ return path();
+ }
+ return path(buffer.get());
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void current_path(const path& p)
+{
+ std::error_code ec;
+ current_path(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+}
+#endif
+
+GHC_INLINE void current_path(const path& p, std::error_code& ec) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ if (!::SetCurrentDirectoryW(GHC_NATIVEWP(p))) {
+ ec = detail::make_system_error();
+ }
+#else
+ if (::chdir(p.string().c_str()) == -1) {
+ ec = detail::make_system_error();
+ }
+#endif
+}
+
+GHC_INLINE bool exists(file_status s) noexcept
+{
+ return status_known(s) && s.type() != file_type::not_found;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool exists(const path& p)
+{
+ return exists(status(p));
+}
+#endif
+
+GHC_INLINE bool exists(const path& p, std::error_code& ec) noexcept
+{
+ file_status s = status(p, ec);
+ if (status_known(s)) {
+ ec.clear();
+ }
+ return exists(s);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool equivalent(const path& p1, const path& p2)
+{
+ std::error_code ec;
+ bool result = equivalent(p1, p2, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p1, p2, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE bool equivalent(const path& p1, const path& p2, std::error_code& ec) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ detail::unique_handle file1(::CreateFileW(GHC_NATIVEWP(p1), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0));
+ auto e1 = ::GetLastError();
+ detail::unique_handle file2(::CreateFileW(GHC_NATIVEWP(p2), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0));
+ if (!file1 || !file2) {
+#ifdef LWG_2937_BEHAVIOUR
+ ec = detail::make_system_error(e1 ? e1 : ::GetLastError());
+#else
+ if (file1 == file2) {
+ ec = detail::make_system_error(e1 ? e1 : ::GetLastError());
+ }
+#endif
+ return false;
+ }
+ BY_HANDLE_FILE_INFORMATION inf1, inf2;
+ if (!::GetFileInformationByHandle(file1.get(), &inf1)) {
+ ec = detail::make_system_error();
+ return false;
+ }
+ if (!::GetFileInformationByHandle(file2.get(), &inf2)) {
+ ec = detail::make_system_error();
+ return false;
+ }
+ return inf1.ftLastWriteTime.dwLowDateTime == inf2.ftLastWriteTime.dwLowDateTime && inf1.ftLastWriteTime.dwHighDateTime == inf2.ftLastWriteTime.dwHighDateTime && inf1.nFileIndexHigh == inf2.nFileIndexHigh && inf1.nFileIndexLow == inf2.nFileIndexLow &&
+ inf1.nFileSizeHigh == inf2.nFileSizeHigh && inf1.nFileSizeLow == inf2.nFileSizeLow && inf1.dwVolumeSerialNumber == inf2.dwVolumeSerialNumber;
+#else
+ struct ::stat s1, s2;
+ auto rc1 = ::stat(p1.c_str(), &s1);
+ auto e1 = errno;
+ auto rc2 = ::stat(p2.c_str(), &s2);
+ if (rc1 || rc2) {
+#ifdef LWG_2937_BEHAVIOUR
+ ec = detail::make_system_error(e1 ? e1 : errno);
+#else
+ if (rc1 && rc2) {
+ ec = detail::make_system_error(e1 ? e1 : errno);
+ }
+#endif
+ return false;
+ }
+ return s1.st_dev == s2.st_dev && s1.st_ino == s2.st_ino && s1.st_size == s2.st_size && s1.st_mtime == s2.st_mtime;
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE uintmax_t file_size(const path& p)
+{
+ std::error_code ec;
+ auto result = file_size(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE uintmax_t file_size(const path& p, std::error_code& ec) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ WIN32_FILE_ATTRIBUTE_DATA attr;
+ if (!GetFileAttributesExW(GHC_NATIVEWP(p), GetFileExInfoStandard, &attr)) {
+ ec = detail::make_system_error();
+ return static_cast<uintmax_t>(-1);
+ }
+ return static_cast<uintmax_t>(attr.nFileSizeHigh) << (sizeof(attr.nFileSizeHigh) * 8) | attr.nFileSizeLow;
+#else
+ struct ::stat fileStat;
+ if (::stat(p.c_str(), &fileStat) == -1) {
+ ec = detail::make_system_error();
+ return static_cast<uintmax_t>(-1);
+ }
+ return static_cast<uintmax_t>(fileStat.st_size);
+#endif
+}
+
+#ifndef GHC_OS_WEB
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE uintmax_t hard_link_count(const path& p)
+{
+ std::error_code ec;
+ auto result = hard_link_count(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE uintmax_t hard_link_count(const path& p, std::error_code& ec) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ uintmax_t result = static_cast<uintmax_t>(-1);
+ detail::unique_handle file(::CreateFileW(GHC_NATIVEWP(p), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0));
+ BY_HANDLE_FILE_INFORMATION inf;
+ if (!file) {
+ ec = detail::make_system_error();
+ }
+ else {
+ if (!::GetFileInformationByHandle(file.get(), &inf)) {
+ ec = detail::make_system_error();
+ }
+ else {
+ result = inf.nNumberOfLinks;
+ }
+ }
+ return result;
+#else
+ uintmax_t result = 0;
+ file_status fs = detail::status_ex(p, ec, nullptr, nullptr, &result, nullptr);
+ if (fs.type() == file_type::not_found) {
+ ec = detail::make_error_code(detail::portable_error::not_found);
+ }
+ return ec ? static_cast<uintmax_t>(-1) : result;
+#endif
+}
+#endif
+
+GHC_INLINE bool is_block_file(file_status s) noexcept
+{
+ return s.type() == file_type::block;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool is_block_file(const path& p)
+{
+ return is_block_file(status(p));
+}
+#endif
+
+GHC_INLINE bool is_block_file(const path& p, std::error_code& ec) noexcept
+{
+ return is_block_file(status(p, ec));
+}
+
+GHC_INLINE bool is_character_file(file_status s) noexcept
+{
+ return s.type() == file_type::character;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool is_character_file(const path& p)
+{
+ return is_character_file(status(p));
+}
+#endif
+
+GHC_INLINE bool is_character_file(const path& p, std::error_code& ec) noexcept
+{
+ return is_character_file(status(p, ec));
+}
+
+GHC_INLINE bool is_directory(file_status s) noexcept
+{
+ return s.type() == file_type::directory;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool is_directory(const path& p)
+{
+ return is_directory(status(p));
+}
+#endif
+
+GHC_INLINE bool is_directory(const path& p, std::error_code& ec) noexcept
+{
+ return is_directory(status(p, ec));
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool is_empty(const path& p)
+{
+ if (is_directory(p)) {
+ return directory_iterator(p) == directory_iterator();
+ }
+ else {
+ return file_size(p) == 0;
+ }
+}
+#endif
+
+GHC_INLINE bool is_empty(const path& p, std::error_code& ec) noexcept
+{
+ auto fs = status(p, ec);
+ if (ec) {
+ return false;
+ }
+ if (is_directory(fs)) {
+ directory_iterator iter(p, ec);
+ if (ec) {
+ return false;
+ }
+ return iter == directory_iterator();
+ }
+ else {
+ auto sz = file_size(p, ec);
+ if (ec) {
+ return false;
+ }
+ return sz == 0;
+ }
+}
+
+GHC_INLINE bool is_fifo(file_status s) noexcept
+{
+ return s.type() == file_type::fifo;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool is_fifo(const path& p)
+{
+ return is_fifo(status(p));
+}
+#endif
+
+GHC_INLINE bool is_fifo(const path& p, std::error_code& ec) noexcept
+{
+ return is_fifo(status(p, ec));
+}
+
+GHC_INLINE bool is_other(file_status s) noexcept
+{
+ return exists(s) && !is_regular_file(s) && !is_directory(s) && !is_symlink(s);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool is_other(const path& p)
+{
+ return is_other(status(p));
+}
+#endif
+
+GHC_INLINE bool is_other(const path& p, std::error_code& ec) noexcept
+{
+ return is_other(status(p, ec));
+}
+
+GHC_INLINE bool is_regular_file(file_status s) noexcept
+{
+ return s.type() == file_type::regular;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool is_regular_file(const path& p)
+{
+ return is_regular_file(status(p));
+}
+#endif
+
+GHC_INLINE bool is_regular_file(const path& p, std::error_code& ec) noexcept
+{
+ return is_regular_file(status(p, ec));
+}
+
+GHC_INLINE bool is_socket(file_status s) noexcept
+{
+ return s.type() == file_type::socket;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool is_socket(const path& p)
+{
+ return is_socket(status(p));
+}
+#endif
+
+GHC_INLINE bool is_socket(const path& p, std::error_code& ec) noexcept
+{
+ return is_socket(status(p, ec));
+}
+
+GHC_INLINE bool is_symlink(file_status s) noexcept
+{
+ return s.type() == file_type::symlink;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool is_symlink(const path& p)
+{
+ return is_symlink(symlink_status(p));
+}
+#endif
+
+GHC_INLINE bool is_symlink(const path& p, std::error_code& ec) noexcept
+{
+ return is_symlink(symlink_status(p, ec));
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE file_time_type last_write_time(const path& p)
+{
+ std::error_code ec;
+ auto result = last_write_time(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE file_time_type last_write_time(const path& p, std::error_code& ec) noexcept
+{
+ time_t result = 0;
+ ec.clear();
+ file_status fs = detail::status_ex(p, ec, nullptr, nullptr, nullptr, &result);
+ return ec ? (file_time_type::min)() : std::chrono::system_clock::from_time_t(result);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void last_write_time(const path& p, file_time_type new_time)
+{
+ std::error_code ec;
+ last_write_time(p, new_time, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+}
+#endif
+
+GHC_INLINE void last_write_time(const path& p, file_time_type new_time, std::error_code& ec) noexcept
+{
+ ec.clear();
+ auto d = new_time.time_since_epoch();
+#ifdef GHC_OS_WINDOWS
+ detail::unique_handle file(::CreateFileW(GHC_NATIVEWP(p), FILE_WRITE_ATTRIBUTES, FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL));
+ FILETIME ft;
+ auto tt = std::chrono::duration_cast<std::chrono::microseconds>(d).count() * 10 + 116444736000000000;
+ ft.dwLowDateTime = static_cast<DWORD>(tt);
+ ft.dwHighDateTime = static_cast<DWORD>(tt >> 32);
+ if (!::SetFileTime(file.get(), 0, 0, &ft)) {
+ ec = detail::make_system_error();
+ }
+#elif defined(GHC_OS_MACOS)
+#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED
+#if __MAC_OS_X_VERSION_MIN_REQUIRED < 101300
+ struct ::stat fs;
+ if (::stat(p.c_str(), &fs) == 0) {
+ struct ::timeval tv[2];
+ tv[0].tv_sec = fs.st_atimespec.tv_sec;
+ tv[0].tv_usec = static_cast<int>(fs.st_atimespec.tv_nsec / 1000);
+ tv[1].tv_sec = std::chrono::duration_cast<std::chrono::seconds>(d).count();
+ tv[1].tv_usec = static_cast<int>(std::chrono::duration_cast<std::chrono::microseconds>(d).count() % 1000000);
+ if (::utimes(p.c_str(), tv) == 0) {
+ return;
+ }
+ }
+ ec = detail::make_system_error();
+ return;
+#else
+ struct ::timespec times[2];
+ times[0].tv_sec = 0;
+ times[0].tv_nsec = UTIME_OMIT;
+ times[1].tv_sec = std::chrono::duration_cast<std::chrono::seconds>(d).count();
+ times[1].tv_nsec = 0; // std::chrono::duration_cast<std::chrono::nanoseconds>(d).count() % 1000000000;
+ if (::utimensat(AT_FDCWD, p.c_str(), times, AT_SYMLINK_NOFOLLOW) != 0) {
+ ec = detail::make_system_error();
+ }
+ return;
+#endif
+#endif
+#else
+#ifndef UTIME_OMIT
+#define UTIME_OMIT ((1l << 30) - 2l)
+#endif
+ struct ::timespec times[2];
+ times[0].tv_sec = 0;
+ times[0].tv_nsec = UTIME_OMIT;
+ times[1].tv_sec = static_cast<decltype(times[1].tv_sec)>(std::chrono::duration_cast<std::chrono::seconds>(d).count());
+ times[1].tv_nsec = static_cast<decltype(times[1].tv_nsec)>(std::chrono::duration_cast<std::chrono::nanoseconds>(d).count() % 1000000000);
+#if defined(__ANDROID_API__) && __ANDROID_API__ < 12
+ if (syscall(__NR_utimensat, AT_FDCWD, p.c_str(), times, AT_SYMLINK_NOFOLLOW) != 0) {
+#else
+ if (::utimensat((int)AT_FDCWD, p.c_str(), times, AT_SYMLINK_NOFOLLOW) != 0) {
+#endif
+ ec = detail::make_system_error();
+ }
+ return;
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void permissions(const path& p, perms prms, perm_options opts)
+{
+ std::error_code ec;
+ permissions(p, prms, opts, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+}
+#endif
+
+GHC_INLINE void permissions(const path& p, perms prms, std::error_code& ec) noexcept
+{
+ permissions(p, prms, perm_options::replace, ec);
+}
+
+GHC_INLINE void permissions(const path& p, perms prms, perm_options opts, std::error_code& ec) noexcept
+{
+ if (static_cast<int>(opts & (perm_options::replace | perm_options::add | perm_options::remove)) == 0) {
+ ec = detail::make_error_code(detail::portable_error::invalid_argument);
+ return;
+ }
+ auto fs = symlink_status(p, ec);
+ if ((opts & perm_options::replace) != perm_options::replace) {
+ if ((opts & perm_options::add) == perm_options::add) {
+ prms = fs.permissions() | prms;
+ }
+ else {
+ prms = fs.permissions() & ~prms;
+ }
+ }
+#ifdef GHC_OS_WINDOWS
+#ifdef __GNUC__
+ auto oldAttr = GetFileAttributesW(GHC_NATIVEWP(p));
+ if (oldAttr != INVALID_FILE_ATTRIBUTES) {
+ DWORD newAttr = ((prms & perms::owner_write) == perms::owner_write) ? oldAttr & ~(static_cast<DWORD>(FILE_ATTRIBUTE_READONLY)) : oldAttr | FILE_ATTRIBUTE_READONLY;
+ if (oldAttr == newAttr || SetFileAttributesW(GHC_NATIVEWP(p), newAttr)) {
+ return;
+ }
+ }
+ ec = detail::make_system_error();
+#else
+ int mode = 0;
+ if ((prms & perms::owner_read) == perms::owner_read) {
+ mode |= _S_IREAD;
+ }
+ if ((prms & perms::owner_write) == perms::owner_write) {
+ mode |= _S_IWRITE;
+ }
+ if (::_wchmod(p.wstring().c_str(), mode) != 0) {
+ ec = detail::make_system_error();
+ }
+#endif
+#else
+ if ((opts & perm_options::nofollow) != perm_options::nofollow) {
+ if (::chmod(p.c_str(), static_cast<mode_t>(prms)) != 0) {
+ ec = detail::make_system_error();
+ }
+ }
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE path proximate(const path& p, std::error_code& ec)
+{
+ auto cp = current_path(ec);
+ if (!ec) {
+ return proximate(p, cp, ec);
+ }
+ return path();
+}
+#endif
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE path proximate(const path& p, const path& base)
+{
+ return weakly_canonical(p).lexically_proximate(weakly_canonical(base));
+}
+#endif
+
+GHC_INLINE path proximate(const path& p, const path& base, std::error_code& ec)
+{
+ return weakly_canonical(p, ec).lexically_proximate(weakly_canonical(base, ec));
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE path read_symlink(const path& p)
+{
+ std::error_code ec;
+ auto result = read_symlink(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE path read_symlink(const path& p, std::error_code& ec)
+{
+ file_status fs = symlink_status(p, ec);
+ if (fs.type() != file_type::symlink) {
+ ec = detail::make_error_code(detail::portable_error::invalid_argument);
+ return path();
+ }
+ auto result = detail::resolveSymlink(p, ec);
+ return ec ? path() : result;
+}
+
+GHC_INLINE path relative(const path& p, std::error_code& ec)
+{
+ return relative(p, current_path(ec), ec);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE path relative(const path& p, const path& base)
+{
+ return weakly_canonical(p).lexically_relative(weakly_canonical(base));
+}
+#endif
+
+GHC_INLINE path relative(const path& p, const path& base, std::error_code& ec)
+{
+ return weakly_canonical(p, ec).lexically_relative(weakly_canonical(base, ec));
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool remove(const path& p)
+{
+ std::error_code ec;
+ auto result = remove(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE bool remove(const path& p, std::error_code& ec) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+#ifdef GHC_USE_WCHAR_T
+ auto cstr = p.c_str();
+#else
+ std::wstring np = detail::fromUtf8<std::wstring>(p.u8string());
+ auto cstr = np.c_str();
+#endif
+ DWORD attr = GetFileAttributesW(cstr);
+ if (attr == INVALID_FILE_ATTRIBUTES) {
+ auto error = ::GetLastError();
+ if (error == ERROR_FILE_NOT_FOUND || error == ERROR_PATH_NOT_FOUND) {
+ return false;
+ }
+ ec = detail::make_system_error(error);
+ }
+ else if (attr & FILE_ATTRIBUTE_READONLY) {
+ auto new_attr = attr & ~static_cast<DWORD>(FILE_ATTRIBUTE_READONLY);
+ if (!SetFileAttributesW(cstr, new_attr)) {
+ auto error = ::GetLastError();
+ ec = detail::make_system_error(error);
+ }
+ }
+ if (!ec) {
+ if (attr & FILE_ATTRIBUTE_DIRECTORY) {
+ if (!RemoveDirectoryW(cstr)) {
+ ec = detail::make_system_error();
+ }
+ }
+ else {
+ if (!DeleteFileW(cstr)) {
+ ec = detail::make_system_error();
+ }
+ }
+ }
+#else
+ if (::remove(p.c_str()) == -1) {
+ auto error = errno;
+ if (error == ENOENT) {
+ return false;
+ }
+ ec = detail::make_system_error();
+ }
+#endif
+ return ec ? false : true;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE uintmax_t remove_all(const path& p)
+{
+ std::error_code ec;
+ auto result = remove_all(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE uintmax_t remove_all(const path& p, std::error_code& ec) noexcept
+{
+ ec.clear();
+ uintmax_t count = 0;
+ if (p == "/") {
+ ec = detail::make_error_code(detail::portable_error::not_supported);
+ return static_cast<uintmax_t>(-1);
+ }
+ std::error_code tec;
+ auto fs = symlink_status(p, tec);
+ if (exists(fs) && is_directory(fs)) {
+ for (auto iter = directory_iterator(p, ec); iter != directory_iterator(); iter.increment(ec)) {
+ if (ec && !detail::is_not_found_error(ec)) {
+ break;
+ }
+ bool is_symlink_result = iter->is_symlink(ec);
+ if (ec)
+ return static_cast<uintmax_t>(-1);
+ if (!is_symlink_result && iter->is_directory(ec)) {
+ count += remove_all(iter->path(), ec);
+ if (ec) {
+ return static_cast<uintmax_t>(-1);
+ }
+ }
+ else {
+ if (!ec) {
+ remove(iter->path(), ec);
+ }
+ if (ec) {
+ return static_cast<uintmax_t>(-1);
+ }
+ ++count;
+ }
+ }
+ }
+ if (!ec) {
+ if (remove(p, ec)) {
+ ++count;
+ }
+ }
+ if (ec) {
+ return static_cast<uintmax_t>(-1);
+ }
+ return count;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void rename(const path& from, const path& to)
+{
+ std::error_code ec;
+ rename(from, to, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), from, to, ec);
+ }
+}
+#endif
+
+GHC_INLINE void rename(const path& from, const path& to, std::error_code& ec) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ if (from != to) {
+ if (!MoveFileExW(GHC_NATIVEWP(from), GHC_NATIVEWP(to), (DWORD)MOVEFILE_REPLACE_EXISTING)) {
+ ec = detail::make_system_error();
+ }
+ }
+#else
+ if (from != to) {
+ if (::rename(from.c_str(), to.c_str()) != 0) {
+ ec = detail::make_system_error();
+ }
+ }
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void resize_file(const path& p, uintmax_t size)
+{
+ std::error_code ec;
+ resize_file(p, size, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+}
+#endif
+
+GHC_INLINE void resize_file(const path& p, uintmax_t size, std::error_code& ec) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ LARGE_INTEGER lisize;
+ lisize.QuadPart = static_cast<LONGLONG>(size);
+ if (lisize.QuadPart < 0) {
+#ifdef ERROR_FILE_TOO_LARGE
+ ec = detail::make_system_error(ERROR_FILE_TOO_LARGE);
+#else
+ ec = detail::make_system_error(223);
+#endif
+ return;
+ }
+ detail::unique_handle file(CreateFileW(GHC_NATIVEWP(p), GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL));
+ if (!file) {
+ ec = detail::make_system_error();
+ }
+ else if (SetFilePointerEx(file.get(), lisize, NULL, FILE_BEGIN) == 0 || SetEndOfFile(file.get()) == 0) {
+ ec = detail::make_system_error();
+ }
+#else
+ if (::truncate(p.c_str(), static_cast<off_t>(size)) != 0) {
+ ec = detail::make_system_error();
+ }
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE space_info space(const path& p)
+{
+ std::error_code ec;
+ auto result = space(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE space_info space(const path& p, std::error_code& ec) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ ULARGE_INTEGER freeBytesAvailableToCaller = {{ 0, 0 }};
+ ULARGE_INTEGER totalNumberOfBytes = {{ 0, 0 }};
+ ULARGE_INTEGER totalNumberOfFreeBytes = {{ 0, 0 }};
+ if (!GetDiskFreeSpaceExW(GHC_NATIVEWP(p), &freeBytesAvailableToCaller, &totalNumberOfBytes, &totalNumberOfFreeBytes)) {
+ ec = detail::make_system_error();
+ return {static_cast<uintmax_t>(-1), static_cast<uintmax_t>(-1), static_cast<uintmax_t>(-1)};
+ }
+ return {static_cast<uintmax_t>(totalNumberOfBytes.QuadPart), static_cast<uintmax_t>(totalNumberOfFreeBytes.QuadPart), static_cast<uintmax_t>(freeBytesAvailableToCaller.QuadPart)};
+#else
+ struct ::statvfs sfs;
+ if (::statvfs(p.c_str(), &sfs) != 0) {
+ ec = detail::make_system_error();
+ return {static_cast<uintmax_t>(-1), static_cast<uintmax_t>(-1), static_cast<uintmax_t>(-1)};
+ }
+ return {static_cast<uintmax_t>(sfs.f_blocks) * static_cast<uintmax_t>(sfs.f_frsize), static_cast<uintmax_t>(sfs.f_bfree) * static_cast<uintmax_t>(sfs.f_frsize), static_cast<uintmax_t>(sfs.f_bavail) * static_cast<uintmax_t>(sfs.f_frsize)};
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE file_status status(const path& p)
+{
+ std::error_code ec;
+ auto result = status(p, ec);
+ if (result.type() == file_type::none) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE file_status status(const path& p, std::error_code& ec) noexcept
+{
+ return detail::status_ex(p, ec);
+}
+
+GHC_INLINE bool status_known(file_status s) noexcept
+{
+ return s.type() != file_type::none;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE file_status symlink_status(const path& p)
+{
+ std::error_code ec;
+ auto result = symlink_status(p, ec);
+ if (result.type() == file_type::none) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE file_status symlink_status(const path& p, std::error_code& ec) noexcept
+{
+ return detail::symlink_status_ex(p, ec);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE path temp_directory_path()
+{
+ std::error_code ec;
+ path result = temp_directory_path(ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE path temp_directory_path(std::error_code& ec) noexcept
+{
+ ec.clear();
+#ifdef GHC_OS_WINDOWS
+ wchar_t buffer[512];
+ auto rc = GetTempPathW(511, buffer);
+ if (!rc || rc > 511) {
+ ec = detail::make_system_error();
+ return path();
+ }
+ return path(std::wstring(buffer));
+#else
+ static const char* temp_vars[] = {"TMPDIR", "TMP", "TEMP", "TEMPDIR", nullptr};
+ const char* temp_path = nullptr;
+ for (auto temp_name = temp_vars; *temp_name != nullptr; ++temp_name) {
+ temp_path = std::getenv(*temp_name);
+ if (temp_path) {
+ return path(temp_path);
+ }
+ }
+ return path("/tmp");
+#endif
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE path weakly_canonical(const path& p)
+{
+ std::error_code ec;
+ auto result = weakly_canonical(p, ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), p, ec);
+ }
+ return result;
+}
+#endif
+
+GHC_INLINE path weakly_canonical(const path& p, std::error_code& ec) noexcept
+{
+ path result;
+ ec.clear();
+ bool scan = true;
+ for (auto pe : p) {
+ if (scan) {
+ std::error_code tec;
+ if (exists(result / pe, tec)) {
+ result /= pe;
+ }
+ else {
+ if (ec) {
+ return path();
+ }
+ scan = false;
+ if (!result.empty()) {
+ result = canonical(result, ec) / pe;
+ if (ec) {
+ break;
+ }
+ }
+ else {
+ result /= pe;
+ }
+ }
+ }
+ else {
+ result /= pe;
+ }
+ }
+ if (scan) {
+ if (!result.empty()) {
+ result = canonical(result, ec);
+ }
+ }
+ return ec ? path() : result.lexically_normal();
+}
+
+//-----------------------------------------------------------------------------
+// [fs.class.file_status] class file_status
+// [fs.file_status.cons] constructors and destructor
+GHC_INLINE file_status::file_status() noexcept
+ : file_status(file_type::none)
+{
+}
+
+GHC_INLINE file_status::file_status(file_type ft, perms prms) noexcept
+ : _type(ft)
+ , _perms(prms)
+{
+}
+
+GHC_INLINE file_status::file_status(const file_status& other) noexcept
+ : _type(other._type)
+ , _perms(other._perms)
+{
+}
+
+GHC_INLINE file_status::file_status(file_status&& other) noexcept
+ : _type(other._type)
+ , _perms(other._perms)
+{
+}
+
+GHC_INLINE file_status::~file_status() {}
+
+// assignments:
+GHC_INLINE file_status& file_status::operator=(const file_status& rhs) noexcept
+{
+ _type = rhs._type;
+ _perms = rhs._perms;
+ return *this;
+}
+
+GHC_INLINE file_status& file_status::operator=(file_status&& rhs) noexcept
+{
+ _type = rhs._type;
+ _perms = rhs._perms;
+ return *this;
+}
+
+// [fs.file_status.mods] modifiers
+GHC_INLINE void file_status::type(file_type ft) noexcept
+{
+ _type = ft;
+}
+
+GHC_INLINE void file_status::permissions(perms prms) noexcept
+{
+ _perms = prms;
+}
+
+// [fs.file_status.obs] observers
+GHC_INLINE file_type file_status::type() const noexcept
+{
+ return _type;
+}
+
+GHC_INLINE perms file_status::permissions() const noexcept
+{
+ return _perms;
+}
+
+//-----------------------------------------------------------------------------
+// [fs.class.directory_entry] class directory_entry
+// [fs.dir.entry.cons] constructors and destructor
+// directory_entry::directory_entry() noexcept = default;
+// directory_entry::directory_entry(const directory_entry&) = default;
+// directory_entry::directory_entry(directory_entry&&) noexcept = default;
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE directory_entry::directory_entry(const filesystem::path& p)
+ : _path(p)
+ , _file_size(static_cast<uintmax_t>(-1))
+#ifndef GHC_OS_WINDOWS
+ , _hard_link_count(static_cast<uintmax_t>(-1))
+#endif
+ , _last_write_time(0)
+{
+ refresh();
+}
+#endif
+
+GHC_INLINE directory_entry::directory_entry(const filesystem::path& p, std::error_code& ec)
+ : _path(p)
+ , _file_size(static_cast<uintmax_t>(-1))
+#ifndef GHC_OS_WINDOWS
+ , _hard_link_count(static_cast<uintmax_t>(-1))
+#endif
+ , _last_write_time(0)
+{
+ refresh(ec);
+}
+
+GHC_INLINE directory_entry::~directory_entry() {}
+
+// assignments:
+// directory_entry& directory_entry::operator=(const directory_entry&) = default;
+// directory_entry& directory_entry::operator=(directory_entry&&) noexcept = default;
+
+// [fs.dir.entry.mods] directory_entry modifiers
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void directory_entry::assign(const filesystem::path& p)
+{
+ _path = p;
+ refresh();
+}
+#endif
+
+GHC_INLINE void directory_entry::assign(const filesystem::path& p, std::error_code& ec)
+{
+ _path = p;
+ refresh(ec);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void directory_entry::replace_filename(const filesystem::path& p)
+{
+ _path.replace_filename(p);
+ refresh();
+}
+#endif
+
+GHC_INLINE void directory_entry::replace_filename(const filesystem::path& p, std::error_code& ec)
+{
+ _path.replace_filename(p);
+ refresh(ec);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void directory_entry::refresh()
+{
+ std::error_code ec;
+ refresh(ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), _path, ec);
+ }
+}
+#endif
+
+GHC_INLINE void directory_entry::refresh(std::error_code& ec) noexcept
+{
+#ifdef GHC_OS_WINDOWS
+ _status = detail::status_ex(_path, ec, &_symlink_status, &_file_size, nullptr, &_last_write_time);
+#else
+ _status = detail::status_ex(_path, ec, &_symlink_status, &_file_size, &_hard_link_count, &_last_write_time);
+#endif
+}
+
+// [fs.dir.entry.obs] directory_entry observers
+GHC_INLINE const filesystem::path& directory_entry::path() const noexcept
+{
+ return _path;
+}
+
+GHC_INLINE directory_entry::operator const filesystem::path&() const noexcept
+{
+ return _path;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE file_type directory_entry::status_file_type() const
+{
+ return _status.type() != file_type::none ? _status.type() : filesystem::status(path()).type();
+}
+#endif
+
+GHC_INLINE file_type directory_entry::status_file_type(std::error_code& ec) const noexcept
+{
+ if (_status.type() != file_type::none) {
+ ec.clear();
+ return _status.type();
+ }
+ return filesystem::status(path(), ec).type();
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool directory_entry::exists() const
+{
+ return status_file_type() != file_type::not_found;
+}
+#endif
+
+GHC_INLINE bool directory_entry::exists(std::error_code& ec) const noexcept
+{
+ return status_file_type(ec) != file_type::not_found;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool directory_entry::is_block_file() const
+{
+ return status_file_type() == file_type::block;
+}
+#endif
+GHC_INLINE bool directory_entry::is_block_file(std::error_code& ec) const noexcept
+{
+ return status_file_type(ec) == file_type::block;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool directory_entry::is_character_file() const
+{
+ return status_file_type() == file_type::character;
+}
+#endif
+
+GHC_INLINE bool directory_entry::is_character_file(std::error_code& ec) const noexcept
+{
+ return status_file_type(ec) == file_type::character;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool directory_entry::is_directory() const
+{
+ return status_file_type() == file_type::directory;
+}
+#endif
+
+GHC_INLINE bool directory_entry::is_directory(std::error_code& ec) const noexcept
+{
+ return status_file_type(ec) == file_type::directory;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool directory_entry::is_fifo() const
+{
+ return status_file_type() == file_type::fifo;
+}
+#endif
+
+GHC_INLINE bool directory_entry::is_fifo(std::error_code& ec) const noexcept
+{
+ return status_file_type(ec) == file_type::fifo;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool directory_entry::is_other() const
+{
+ auto ft = status_file_type();
+ return ft != file_type::none && ft != file_type::not_found && ft != file_type::regular && ft != file_type::directory && !is_symlink();
+}
+#endif
+
+GHC_INLINE bool directory_entry::is_other(std::error_code& ec) const noexcept
+{
+ auto ft = status_file_type(ec);
+ bool other = ft != file_type::none && ft != file_type::not_found && ft != file_type::regular && ft != file_type::directory && !is_symlink(ec);
+ return !ec && other;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool directory_entry::is_regular_file() const
+{
+ return status_file_type() == file_type::regular;
+}
+#endif
+
+GHC_INLINE bool directory_entry::is_regular_file(std::error_code& ec) const noexcept
+{
+ return status_file_type(ec) == file_type::regular;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool directory_entry::is_socket() const
+{
+ return status_file_type() == file_type::socket;
+}
+#endif
+
+GHC_INLINE bool directory_entry::is_socket(std::error_code& ec) const noexcept
+{
+ return status_file_type(ec) == file_type::socket;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE bool directory_entry::is_symlink() const
+{
+ return _symlink_status.type() != file_type::none ? _symlink_status.type() == file_type::symlink : filesystem::is_symlink(symlink_status());
+}
+#endif
+
+GHC_INLINE bool directory_entry::is_symlink(std::error_code& ec) const noexcept
+{
+ if (_symlink_status.type() != file_type::none) {
+ ec.clear();
+ return _symlink_status.type() == file_type::symlink;
+ }
+ return filesystem::is_symlink(symlink_status(ec));
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE uintmax_t directory_entry::file_size() const
+{
+ if (_file_size != static_cast<uintmax_t>(-1)) {
+ return _file_size;
+ }
+ return filesystem::file_size(path());
+}
+#endif
+
+GHC_INLINE uintmax_t directory_entry::file_size(std::error_code& ec) const noexcept
+{
+ if (_file_size != static_cast<uintmax_t>(-1)) {
+ ec.clear();
+ return _file_size;
+ }
+ return filesystem::file_size(path(), ec);
+}
+
+#ifndef GHC_OS_WEB
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE uintmax_t directory_entry::hard_link_count() const
+{
+#ifndef GHC_OS_WINDOWS
+ if (_hard_link_count != static_cast<uintmax_t>(-1)) {
+ return _hard_link_count;
+ }
+#endif
+ return filesystem::hard_link_count(path());
+}
+#endif
+
+GHC_INLINE uintmax_t directory_entry::hard_link_count(std::error_code& ec) const noexcept
+{
+#ifndef GHC_OS_WINDOWS
+ if (_hard_link_count != static_cast<uintmax_t>(-1)) {
+ ec.clear();
+ return _hard_link_count;
+ }
+#endif
+ return filesystem::hard_link_count(path(), ec);
+}
+#endif
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE file_time_type directory_entry::last_write_time() const
+{
+ if (_last_write_time != 0) {
+ return std::chrono::system_clock::from_time_t(_last_write_time);
+ }
+ return filesystem::last_write_time(path());
+}
+#endif
+
+GHC_INLINE file_time_type directory_entry::last_write_time(std::error_code& ec) const noexcept
+{
+ if (_last_write_time != 0) {
+ ec.clear();
+ return std::chrono::system_clock::from_time_t(_last_write_time);
+ }
+ return filesystem::last_write_time(path(), ec);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE file_status directory_entry::status() const
+{
+ if (_status.type() != file_type::none && _status.permissions() != perms::unknown) {
+ return _status;
+ }
+ return filesystem::status(path());
+}
+#endif
+
+GHC_INLINE file_status directory_entry::status(std::error_code& ec) const noexcept
+{
+ if (_status.type() != file_type::none && _status.permissions() != perms::unknown) {
+ ec.clear();
+ return _status;
+ }
+ return filesystem::status(path(), ec);
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE file_status directory_entry::symlink_status() const
+{
+ if (_symlink_status.type() != file_type::none && _symlink_status.permissions() != perms::unknown) {
+ return _symlink_status;
+ }
+ return filesystem::symlink_status(path());
+}
+#endif
+
+GHC_INLINE file_status directory_entry::symlink_status(std::error_code& ec) const noexcept
+{
+ if (_symlink_status.type() != file_type::none && _symlink_status.permissions() != perms::unknown) {
+ ec.clear();
+ return _symlink_status;
+ }
+ return filesystem::symlink_status(path(), ec);
+}
+
+#ifdef GHC_HAS_THREEWAY_COMP
+GHC_INLINE std::strong_ordering directory_entry::operator<=>(const directory_entry& rhs) const noexcept
+{
+ return _path <=> rhs._path;
+}
+#endif
+
+GHC_INLINE bool directory_entry::operator<(const directory_entry& rhs) const noexcept
+{
+ return _path < rhs._path;
+}
+
+GHC_INLINE bool directory_entry::operator==(const directory_entry& rhs) const noexcept
+{
+ return _path == rhs._path;
+}
+
+GHC_INLINE bool directory_entry::operator!=(const directory_entry& rhs) const noexcept
+{
+ return _path != rhs._path;
+}
+
+GHC_INLINE bool directory_entry::operator<=(const directory_entry& rhs) const noexcept
+{
+ return _path <= rhs._path;
+}
+
+GHC_INLINE bool directory_entry::operator>(const directory_entry& rhs) const noexcept
+{
+ return _path > rhs._path;
+}
+
+GHC_INLINE bool directory_entry::operator>=(const directory_entry& rhs) const noexcept
+{
+ return _path >= rhs._path;
+}
+
+//-----------------------------------------------------------------------------
+// [fs.class.directory_iterator] class directory_iterator
+
+#ifdef GHC_OS_WINDOWS
+class directory_iterator::impl
+{
+public:
+ impl(const path& p, directory_options options)
+ : _base(p)
+ , _options(options)
+ , _dirHandle(INVALID_HANDLE_VALUE)
+ {
+ if (!_base.empty()) {
+ ZeroMemory(&_findData, sizeof(WIN32_FIND_DATAW));
+ if ((_dirHandle = FindFirstFileW(GHC_NATIVEWP((_base / "*")), &_findData)) != INVALID_HANDLE_VALUE) {
+ if (std::wstring(_findData.cFileName) == L"." || std::wstring(_findData.cFileName) == L"..") {
+ increment(_ec);
+ }
+ else {
+ _dir_entry._path = _base / std::wstring(_findData.cFileName);
+ copyToDirEntry(_ec);
+ }
+ }
+ else {
+ auto error = ::GetLastError();
+ _base = filesystem::path();
+ if (error != ERROR_ACCESS_DENIED || (options & directory_options::skip_permission_denied) == directory_options::none) {
+ _ec = detail::make_system_error();
+ }
+ }
+ }
+ }
+ impl(const impl& other) = delete;
+ ~impl()
+ {
+ if (_dirHandle != INVALID_HANDLE_VALUE) {
+ FindClose(_dirHandle);
+ _dirHandle = INVALID_HANDLE_VALUE;
+ }
+ }
+ void increment(std::error_code& ec)
+ {
+ if (_dirHandle != INVALID_HANDLE_VALUE) {
+ do {
+ if (FindNextFileW(_dirHandle, &_findData)) {
+ _dir_entry._path = _base;
+#ifdef GHC_USE_WCHAR_T
+ _dir_entry._path.append_name(_findData.cFileName);
+#else
+#ifdef GHC_RAISE_UNICODE_ERRORS
+ try {
+ _dir_entry._path.append_name(detail::toUtf8(_findData.cFileName).c_str());
+ }
+ catch (filesystem_error& fe) {
+ ec = fe.code();
+ return;
+ }
+#else
+ _dir_entry._path.append_name(detail::toUtf8(_findData.cFileName).c_str());
+#endif
+#endif
+ copyToDirEntry(ec);
+ }
+ else {
+ auto err = ::GetLastError();
+ if (err != ERROR_NO_MORE_FILES) {
+ _ec = ec = detail::make_system_error(err);
+ }
+ FindClose(_dirHandle);
+ _dirHandle = INVALID_HANDLE_VALUE;
+ _dir_entry._path.clear();
+ break;
+ }
+ } while (std::wstring(_findData.cFileName) == L"." || std::wstring(_findData.cFileName) == L"..");
+ }
+ else {
+ ec = _ec;
+ }
+ }
+ void copyToDirEntry(std::error_code& ec)
+ {
+ if (_findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
+ _dir_entry._status = detail::status_ex(_dir_entry._path, ec, &_dir_entry._symlink_status, &_dir_entry._file_size, nullptr, &_dir_entry._last_write_time);
+ }
+ else {
+ _dir_entry._status = detail::status_from_INFO(_dir_entry._path, &_findData, ec, &_dir_entry._file_size, &_dir_entry._last_write_time);
+ _dir_entry._symlink_status = _dir_entry._status;
+ }
+ if (ec) {
+ if (_dir_entry._status.type() != file_type::none && _dir_entry._symlink_status.type() != file_type::none) {
+ ec.clear();
+ }
+ else {
+ _dir_entry._file_size = static_cast<uintmax_t>(-1);
+ _dir_entry._last_write_time = 0;
+ }
+ }
+ }
+ path _base;
+ directory_options _options;
+ WIN32_FIND_DATAW _findData;
+ HANDLE _dirHandle;
+ directory_entry _dir_entry;
+ std::error_code _ec;
+};
+#else
+// POSIX implementation
+class directory_iterator::impl
+{
+public:
+ impl(const path& path, directory_options options)
+ : _base(path)
+ , _options(options)
+ , _dir(nullptr)
+ , _entry(nullptr)
+ {
+ if (!path.empty()) {
+ _dir = ::opendir(path.native().c_str());
+ if (!_dir) {
+ auto error = errno;
+ _base = filesystem::path();
+ if ((error != EACCES && error != EPERM) || (options & directory_options::skip_permission_denied) == directory_options::none) {
+ _ec = detail::make_system_error();
+ }
+ }
+ else {
+ increment(_ec);
+ }
+ }
+ }
+ impl(const impl& other) = delete;
+ ~impl()
+ {
+ if (_dir) {
+ ::closedir(_dir);
+ }
+ }
+ void increment(std::error_code& ec)
+ {
+ if (_dir) {
+ bool skip;
+ do {
+ skip = false;
+ errno = 0;
+ _entry = ::readdir(_dir);
+ if (_entry) {
+ _dir_entry._path = _base;
+ _dir_entry._path.append_name(_entry->d_name);
+ copyToDirEntry();
+ if (ec && (ec.value() == EACCES || ec.value() == EPERM) && (_options & directory_options::skip_permission_denied) == directory_options::skip_permission_denied) {
+ ec.clear();
+ skip = true;
+ }
+ }
+ else {
+ ::closedir(_dir);
+ _dir = nullptr;
+ _dir_entry._path.clear();
+ if (errno) {
+ ec = detail::make_system_error();
+ }
+ break;
+ }
+ } while (skip || std::strcmp(_entry->d_name, ".") == 0 || std::strcmp(_entry->d_name, "..") == 0);
+ }
+ }
+
+ void copyToDirEntry()
+ {
+ _dir_entry._symlink_status.permissions(perms::unknown);
+ auto ft = detail::file_type_from_dirent(*_entry);
+ _dir_entry._symlink_status.type(ft);
+ if (ft != file_type::symlink) {
+ _dir_entry._status = _dir_entry._symlink_status;
+ }
+ else {
+ _dir_entry._status.type(file_type::none);
+ _dir_entry._status.permissions(perms::unknown);
+ }
+ _dir_entry._file_size = static_cast<uintmax_t>(-1);
+ _dir_entry._hard_link_count = static_cast<uintmax_t>(-1);
+ _dir_entry._last_write_time = 0;
+ }
+ path _base;
+ directory_options _options;
+ DIR* _dir;
+ struct ::dirent* _entry;
+ directory_entry _dir_entry;
+ std::error_code _ec;
+};
+#endif
+
+// [fs.dir.itr.members] member functions
+GHC_INLINE directory_iterator::directory_iterator() noexcept
+ : _impl(new impl(path(), directory_options::none))
+{
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE directory_iterator::directory_iterator(const path& p)
+ : _impl(new impl(p, directory_options::none))
+{
+ if (_impl->_ec) {
+ throw filesystem_error(detail::systemErrorText(_impl->_ec.value()), p, _impl->_ec);
+ }
+ _impl->_ec.clear();
+}
+
+GHC_INLINE directory_iterator::directory_iterator(const path& p, directory_options options)
+ : _impl(new impl(p, options))
+{
+ if (_impl->_ec) {
+ throw filesystem_error(detail::systemErrorText(_impl->_ec.value()), p, _impl->_ec);
+ }
+}
+#endif
+
+GHC_INLINE directory_iterator::directory_iterator(const path& p, std::error_code& ec) noexcept
+ : _impl(new impl(p, directory_options::none))
+{
+ if (_impl->_ec) {
+ ec = _impl->_ec;
+ }
+}
+
+GHC_INLINE directory_iterator::directory_iterator(const path& p, directory_options options, std::error_code& ec) noexcept
+ : _impl(new impl(p, options))
+{
+ if (_impl->_ec) {
+ ec = _impl->_ec;
+ }
+}
+
+GHC_INLINE directory_iterator::directory_iterator(const directory_iterator& rhs)
+ : _impl(rhs._impl)
+{
+}
+
+GHC_INLINE directory_iterator::directory_iterator(directory_iterator&& rhs) noexcept
+ : _impl(std::move(rhs._impl))
+{
+}
+
+GHC_INLINE directory_iterator::~directory_iterator() {}
+
+GHC_INLINE directory_iterator& directory_iterator::operator=(const directory_iterator& rhs)
+{
+ _impl = rhs._impl;
+ return *this;
+}
+
+GHC_INLINE directory_iterator& directory_iterator::operator=(directory_iterator&& rhs) noexcept
+{
+ _impl = std::move(rhs._impl);
+ return *this;
+}
+
+GHC_INLINE const directory_entry& directory_iterator::operator*() const
+{
+ return _impl->_dir_entry;
+}
+
+GHC_INLINE const directory_entry* directory_iterator::operator->() const
+{
+ return &_impl->_dir_entry;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE directory_iterator& directory_iterator::operator++()
+{
+ std::error_code ec;
+ _impl->increment(ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), _impl->_dir_entry._path, ec);
+ }
+ return *this;
+}
+#endif
+
+GHC_INLINE directory_iterator& directory_iterator::increment(std::error_code& ec) noexcept
+{
+ _impl->increment(ec);
+ return *this;
+}
+
+GHC_INLINE bool directory_iterator::operator==(const directory_iterator& rhs) const
+{
+ return _impl->_dir_entry._path == rhs._impl->_dir_entry._path;
+}
+
+GHC_INLINE bool directory_iterator::operator!=(const directory_iterator& rhs) const
+{
+ return _impl->_dir_entry._path != rhs._impl->_dir_entry._path;
+}
+
+// [fs.dir.itr.nonmembers] directory_iterator non-member functions
+
+GHC_INLINE directory_iterator begin(directory_iterator iter) noexcept
+{
+ return iter;
+}
+
+GHC_INLINE directory_iterator end(const directory_iterator&) noexcept
+{
+ return directory_iterator();
+}
+
+//-----------------------------------------------------------------------------
+// [fs.class.rec.dir.itr] class recursive_directory_iterator
+
+GHC_INLINE recursive_directory_iterator::recursive_directory_iterator() noexcept
+ : _impl(new recursive_directory_iterator_impl(directory_options::none, true))
+{
+ _impl->_dir_iter_stack.push(directory_iterator());
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE recursive_directory_iterator::recursive_directory_iterator(const path& p)
+ : _impl(new recursive_directory_iterator_impl(directory_options::none, true))
+{
+ _impl->_dir_iter_stack.push(directory_iterator(p));
+}
+
+GHC_INLINE recursive_directory_iterator::recursive_directory_iterator(const path& p, directory_options options)
+ : _impl(new recursive_directory_iterator_impl(options, true))
+{
+ _impl->_dir_iter_stack.push(directory_iterator(p, options));
+}
+#endif
+
+GHC_INLINE recursive_directory_iterator::recursive_directory_iterator(const path& p, directory_options options, std::error_code& ec) noexcept
+ : _impl(new recursive_directory_iterator_impl(options, true))
+{
+ _impl->_dir_iter_stack.push(directory_iterator(p, options, ec));
+}
+
+GHC_INLINE recursive_directory_iterator::recursive_directory_iterator(const path& p, std::error_code& ec) noexcept
+ : _impl(new recursive_directory_iterator_impl(directory_options::none, true))
+{
+ _impl->_dir_iter_stack.push(directory_iterator(p, ec));
+}
+
+GHC_INLINE recursive_directory_iterator::recursive_directory_iterator(const recursive_directory_iterator& rhs)
+ : _impl(rhs._impl)
+{
+}
+
+GHC_INLINE recursive_directory_iterator::recursive_directory_iterator(recursive_directory_iterator&& rhs) noexcept
+ : _impl(std::move(rhs._impl))
+{
+}
+
+GHC_INLINE recursive_directory_iterator::~recursive_directory_iterator() {}
+
+// [fs.rec.dir.itr.members] observers
+GHC_INLINE directory_options recursive_directory_iterator::options() const
+{
+ return _impl->_options;
+}
+
+GHC_INLINE int recursive_directory_iterator::depth() const
+{
+ return static_cast<int>(_impl->_dir_iter_stack.size() - 1);
+}
+
+GHC_INLINE bool recursive_directory_iterator::recursion_pending() const
+{
+ return _impl->_recursion_pending;
+}
+
+GHC_INLINE const directory_entry& recursive_directory_iterator::operator*() const
+{
+ return *(_impl->_dir_iter_stack.top());
+}
+
+GHC_INLINE const directory_entry* recursive_directory_iterator::operator->() const
+{
+ return &(*(_impl->_dir_iter_stack.top()));
+}
+
+// [fs.rec.dir.itr.members] modifiers recursive_directory_iterator&
+GHC_INLINE recursive_directory_iterator& recursive_directory_iterator::operator=(const recursive_directory_iterator& rhs)
+{
+ _impl = rhs._impl;
+ return *this;
+}
+
+GHC_INLINE recursive_directory_iterator& recursive_directory_iterator::operator=(recursive_directory_iterator&& rhs) noexcept
+{
+ _impl = std::move(rhs._impl);
+ return *this;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE recursive_directory_iterator& recursive_directory_iterator::operator++()
+{
+ std::error_code ec;
+ increment(ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), _impl->_dir_iter_stack.empty() ? path() : _impl->_dir_iter_stack.top()->path(), ec);
+ }
+ return *this;
+}
+#endif
+
+GHC_INLINE recursive_directory_iterator& recursive_directory_iterator::increment(std::error_code& ec) noexcept
+{
+ bool isSymLink = (*this)->is_symlink(ec);
+ bool isDir = !ec && (*this)->is_directory(ec);
+ if (isSymLink && detail::is_not_found_error(ec)) {
+ ec.clear();
+ }
+ if (!ec) {
+ if (recursion_pending() && isDir && (!isSymLink || (options() & directory_options::follow_directory_symlink) != directory_options::none)) {
+ _impl->_dir_iter_stack.push(directory_iterator((*this)->path(), _impl->_options, ec));
+ }
+ else {
+ _impl->_dir_iter_stack.top().increment(ec);
+ }
+ if (!ec) {
+ while (depth() && _impl->_dir_iter_stack.top() == directory_iterator()) {
+ _impl->_dir_iter_stack.pop();
+ _impl->_dir_iter_stack.top().increment(ec);
+ }
+ }
+ else if (!_impl->_dir_iter_stack.empty()) {
+ _impl->_dir_iter_stack.pop();
+ }
+ _impl->_recursion_pending = true;
+ }
+ return *this;
+}
+
+#ifdef GHC_WITH_EXCEPTIONS
+GHC_INLINE void recursive_directory_iterator::pop()
+{
+ std::error_code ec;
+ pop(ec);
+ if (ec) {
+ throw filesystem_error(detail::systemErrorText(ec.value()), _impl->_dir_iter_stack.empty() ? path() : _impl->_dir_iter_stack.top()->path(), ec);
+ }
+}
+#endif
+
+GHC_INLINE void recursive_directory_iterator::pop(std::error_code& ec)
+{
+ if (depth() == 0) {
+ *this = recursive_directory_iterator();
+ }
+ else {
+ do {
+ _impl->_dir_iter_stack.pop();
+ _impl->_dir_iter_stack.top().increment(ec);
+ } while (depth() && _impl->_dir_iter_stack.top() == directory_iterator());
+ }
+}
+
+GHC_INLINE void recursive_directory_iterator::disable_recursion_pending()
+{
+ _impl->_recursion_pending = false;
+}
+
+// other members as required by [input.iterators]
+GHC_INLINE bool recursive_directory_iterator::operator==(const recursive_directory_iterator& rhs) const
+{
+ return _impl->_dir_iter_stack.top() == rhs._impl->_dir_iter_stack.top();
+}
+
+GHC_INLINE bool recursive_directory_iterator::operator!=(const recursive_directory_iterator& rhs) const
+{
+ return _impl->_dir_iter_stack.top() != rhs._impl->_dir_iter_stack.top();
+}
+
+// [fs.rec.dir.itr.nonmembers] directory_iterator non-member functions
+GHC_INLINE recursive_directory_iterator begin(recursive_directory_iterator iter) noexcept
+{
+ return iter;
+}
+
+GHC_INLINE recursive_directory_iterator end(const recursive_directory_iterator&) noexcept
+{
+ return recursive_directory_iterator();
+}
+
+#endif // GHC_EXPAND_IMPL
+
+} // namespace filesystem
+} // namespace ghc
+
+// cleanup some macros
+#undef GHC_INLINE
+#undef GHC_EXPAND_IMPL
+
+#endif // GHC_FILESYSTEM_H
diff --git a/src/ghc/fs_fwd.hpp b/src/ghc/fs_fwd.hpp
new file mode 100644
index 0000000..31188d1
--- /dev/null
+++ b/src/ghc/fs_fwd.hpp
@@ -0,0 +1,38 @@
+//---------------------------------------------------------------------------------------
+//
+// ghc::filesystem - A C++17-like filesystem implementation for C++11/C++14
+//
+//---------------------------------------------------------------------------------------
+//
+// Copyright (c) 2018, Steffen Schümann <s.schuemann@pobox.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+//---------------------------------------------------------------------------------------
+// fs_fwd.hpp - The forwarding header for the header/implementation seperated usage of
+// ghc::filesystem.
+// This file can be include at any place, where ghc::filesystem api is needed while
+// not bleeding implementation details (e.g. system includes) into the global namespace,
+// as long as one cpp includes fs_impl.hpp to deliver the matching implementations.
+//---------------------------------------------------------------------------------------
+#ifndef GHC_FILESYSTEM_FWD_H
+#define GHC_FILESYSTEM_FWD_H
+#define GHC_FILESYSTEM_FWD
+#include <ghc/filesystem.hpp>
+#endif // GHC_FILESYSTEM_FWD_H
diff --git a/src/ghc/fs_impl.hpp b/src/ghc/fs_impl.hpp
new file mode 100644
index 0000000..92e3eae
--- /dev/null
+++ b/src/ghc/fs_impl.hpp
@@ -0,0 +1,35 @@
+//---------------------------------------------------------------------------------------
+//
+// ghc::filesystem - A C++17-like filesystem implementation for C++11/C++14
+//
+//---------------------------------------------------------------------------------------
+//
+// Copyright (c) 2018, Steffen Schümann <s.schuemann@pobox.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+//---------------------------------------------------------------------------------------
+// fs_impl.hpp - The implementation header for the header/implementation seperated usage of
+// ghc::filesystem.
+// This file can be used to hide the implementation of ghc::filesystem into a single cpp.
+// The cpp has to include this before including fs_fwd.hpp directly or via a different
+// header to work.
+//---------------------------------------------------------------------------------------
+#define GHC_FILESYSTEM_IMPLEMENTATION
+#include <ghc/filesystem.hpp>
diff --git a/src/ghc/fs_std.hpp b/src/ghc/fs_std.hpp
new file mode 100644
index 0000000..c9492fd
--- /dev/null
+++ b/src/ghc/fs_std.hpp
@@ -0,0 +1,60 @@
+//---------------------------------------------------------------------------------------
+//
+// ghc::filesystem - A C++17-like filesystem implementation for C++11/C++14
+//
+//---------------------------------------------------------------------------------------
+//
+// Copyright (c) 2018, Steffen Schümann <s.schuemann@pobox.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+//---------------------------------------------------------------------------------------
+// fs_std.hpp - The dynamic switching header that includes std::filesystem if detected
+// or ghc::filesystem if not, and makes the resulting API available in the
+// namespace fs.
+//---------------------------------------------------------------------------------------
+#ifndef GHC_FILESYSTEM_STD_H
+#define GHC_FILESYSTEM_STD_H
+#if defined(__APPLE__)
+#include <Availability.h>
+#endif
+#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
+#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
+#define GHC_USE_STD_FS
+#include <filesystem>
+namespace fs {
+using namespace std::filesystem;
+using ifstream = std::ifstream;
+using ofstream = std::ofstream;
+using fstream = std::fstream;
+}
+#endif
+#endif
+#ifndef GHC_USE_STD_FS
+//#define GHC_WIN_DISABLE_WSTRING_STORAGE_TYPE
+#include <ghc/filesystem.hpp>
+namespace fs {
+using namespace ghc::filesystem;
+using ifstream = ghc::filesystem::ifstream;
+using ofstream = ghc::filesystem::ofstream;
+using fstream = ghc::filesystem::fstream;
+}
+#endif
+#endif // GHC_FILESYSTEM_STD_H
+
diff --git a/src/ghc/fs_std_fwd.hpp b/src/ghc/fs_std_fwd.hpp
new file mode 100644
index 0000000..163c956
--- /dev/null
+++ b/src/ghc/fs_std_fwd.hpp
@@ -0,0 +1,63 @@
+//---------------------------------------------------------------------------------------
+//
+// ghc::filesystem - A C++17-like filesystem implementation for C++11/C++14
+//
+//---------------------------------------------------------------------------------------
+//
+// Copyright (c) 2018, Steffen Schümann <s.schuemann@pobox.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+//---------------------------------------------------------------------------------------
+// fs_std_fwd.hpp - The forwarding header for the header/implementation seperated usage of
+// ghc::filesystem that uses std::filesystem if it detects it.
+// This file can be include at any place, where fs::filesystem api is needed while
+// not bleeding implementation details (e.g. system includes) into the global namespace,
+// as long as one cpp includes fs_std_impl.hpp to deliver the matching implementations.
+//---------------------------------------------------------------------------------------
+#ifndef GHC_FILESYSTEM_STD_FWD_H
+#define GHC_FILESYSTEM_STD_FWD_H
+#if defined(__APPLE__)
+#include <Availability.h>
+#endif
+#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
+#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
+#define GHC_USE_STD_FS
+#include <filesystem>
+namespace fs {
+using namespace std::filesystem;
+using ifstream = std::ifstream;
+using ofstream = std::ofstream;
+using fstream = std::fstream;
+}
+#endif
+#endif
+#ifndef GHC_USE_STD_FS
+//#define GHC_WIN_DISABLE_WSTRING_STORAGE_TYPE
+#define GHC_FILESYSTEM_FWD
+#include <ghc/filesystem.hpp>
+namespace fs {
+using namespace ghc::filesystem;
+using ifstream = ghc::filesystem::ifstream;
+using ofstream = ghc::filesystem::ofstream;
+using fstream = ghc::filesystem::fstream;
+}
+#endif
+#endif // GHC_FILESYSTEM_STD_FWD_H
+
diff --git a/src/ghc/fs_std_impl.hpp b/src/ghc/fs_std_impl.hpp
new file mode 100644
index 0000000..7042edc
--- /dev/null
+++ b/src/ghc/fs_std_impl.hpp
@@ -0,0 +1,46 @@
+//---------------------------------------------------------------------------------------
+//
+// ghc::filesystem - A C++17-like filesystem implementation for C++11/C++14
+//
+//---------------------------------------------------------------------------------------
+//
+// Copyright (c) 2018, Steffen Schümann <s.schuemann@pobox.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+//
+//---------------------------------------------------------------------------------------
+// fs_std_impl.hpp - The implementation header for the header/implementation seperated usage of
+// ghc::filesystem that does nothing if std::filesystem is detected.
+// This file can be used to hide the implementation of ghc::filesystem into a single cpp.
+// The cpp has to include this before including fs_std_fwd.hpp directly or via a different
+// header to work.
+//---------------------------------------------------------------------------------------
+#if defined(__APPLE__)
+#include <Availability.h>
+#endif
+#if ((defined(_MSVC_LANG) && _MSVC_LANG >= 201703L) || (defined(__cplusplus) && __cplusplus >= 201703L)) && defined(__has_include)
+#if __has_include(<filesystem>) && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101500)
+#define GHC_USE_STD_FS
+#endif
+#endif
+#ifndef GHC_USE_STD_FS
+//#define GHC_WIN_DISABLE_WSTRING_STORAGE_TYPE
+#define GHC_FILESYSTEM_IMPLEMENTATION
+#include <ghc/filesystem.hpp>
+#endif
diff --git a/src/grep_highlighter.hh b/src/grep_highlighter.hh
new file mode 100644
index 0000000..1ce59be
--- /dev/null
+++ b/src/grep_highlighter.hh
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef grep_highlighter_hh
+#define grep_highlighter_hh
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "grep_proc.hh"
+#include "textview_curses.hh"
+
+#endif
diff --git a/src/grep_proc.cc b/src/grep_proc.cc
new file mode 100644
index 0000000..e67ae2b
--- /dev/null
+++ b/src/grep_proc.cc
@@ -0,0 +1,426 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file grep_proc.cc
+ */
+
+#include "grep_proc.hh"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "base/auto_pid.hh"
+#include "base/lnav_log.hh"
+#include "base/opt_util.hh"
+#include "base/string_util.hh"
+#include "config.h"
+#include "lnav_util.hh"
+#include "vis_line.hh"
+
+template<typename LineType>
+grep_proc<LineType>::grep_proc(std::shared_ptr<lnav::pcre2pp::code> code,
+ grep_proc_source<LineType>& gps,
+ std::shared_ptr<pollable_supervisor> ps)
+ : pollable(ps, pollable::category::background), gp_pcre(code),
+ gp_source(gps)
+{
+ require(this->invariant());
+
+ gps.register_proc(this);
+}
+
+template<typename LineType>
+grep_proc<LineType>::~grep_proc()
+{
+ this->invalidate();
+}
+
+template<typename LineType>
+void
+grep_proc<LineType>::handle_match(
+ int line, std::string& line_value, int off, int* matches, int count)
+{
+ int lpc;
+
+ if (off == 0) {
+ fprintf(stdout, "%d\n", line);
+ }
+ fprintf(stdout, "[%d:%d]\n", matches[0], matches[1]);
+ for (lpc = 1; lpc < count; lpc++) {
+ fprintf(stdout, "(%d:%d)", matches[lpc * 2], matches[lpc * 2 + 1]);
+ fwrite(&(line_value.c_str()[matches[lpc * 2]]),
+ 1,
+ matches[lpc * 2 + 1] - matches[lpc * 2],
+ stdout);
+ fputc('\n', stdout);
+ }
+}
+
+template<typename LineType>
+void
+grep_proc<LineType>::start()
+{
+ require(this->invariant());
+
+ if (this->gp_sink) {
+ // XXX hack to make sure threads used by line_buffer are not active
+ // before the fork.
+ this->gp_sink->grep_quiesce();
+ }
+
+ log_debug("grep_proc(%p): start", this);
+ if (this->gp_child_started || this->gp_queue.empty()) {
+ log_debug("grep_proc(%p): nothing to do?", this);
+ return;
+ }
+
+ auto_pipe in_pipe(STDIN_FILENO);
+ auto_pipe out_pipe(STDOUT_FILENO);
+ auto_pipe err_pipe(STDERR_FILENO);
+
+ /* Get ahold of some pipes for stdout and stderr. */
+ if (out_pipe.open() < 0) {
+ throw error(errno);
+ }
+
+ if (err_pipe.open() < 0) {
+ throw error(errno);
+ }
+
+ if ((this->gp_child = fork()) < 0) {
+ throw error(errno);
+ }
+
+ in_pipe.after_fork(this->gp_child);
+ out_pipe.after_fork(this->gp_child);
+ err_pipe.after_fork(this->gp_child);
+
+ if (this->gp_child != 0) {
+ log_perror(fcntl(out_pipe.read_end(), F_SETFL, O_NONBLOCK));
+ log_perror(fcntl(out_pipe.read_end(), F_SETFD, 1));
+ this->gp_line_buffer.set_fd(out_pipe.read_end());
+
+ log_perror(fcntl(err_pipe.read_end(), F_SETFL, O_NONBLOCK));
+ log_perror(fcntl(err_pipe.read_end(), F_SETFD, 1));
+ require(this->gp_err_pipe.get() == -1);
+ this->gp_err_pipe = std::move(err_pipe.read_end());
+ this->gp_child_started = true;
+ this->gp_child_queue_size = this->gp_queue.size();
+
+ this->gp_queue.clear();
+
+ log_debug("grep_proc(%p): started child %d", this, this->gp_child);
+ return;
+ }
+
+ /* In the child... */
+ lnav::pid::in_child = true;
+
+ /*
+ * Restore the default signal handlers so we don't hang around
+ * forever if there is a problem.
+ */
+ signal(SIGINT, SIG_DFL);
+ signal(SIGTERM, SIG_DFL);
+
+ this->child_init();
+
+ this->child_loop();
+
+ _exit(0);
+}
+
+template<typename LineType>
+void
+grep_proc<LineType>::child_loop()
+{
+ char outbuf[BUFSIZ * 2];
+ std::string line_value;
+
+ /* Make sure buffering is on, not sure of the state in the parent. */
+ if (setvbuf(stdout, outbuf, _IOFBF, BUFSIZ * 2) < 0) {
+ perror("setvbuf");
+ }
+ lnav_log_file
+ = make_optional_from_nullable(fopen("/tmp/lnav.grep.err", "a"));
+ line_value.reserve(BUFSIZ * 2);
+ while (!this->gp_queue.empty()) {
+ LineType start_line = this->gp_queue.front().first;
+ LineType stop_line = this->gp_queue.front().second;
+ bool done = false;
+ LineType line;
+
+ this->gp_queue.pop_front();
+ for (line = this->gp_source.grep_initial_line(start_line,
+ this->gp_highest_line);
+ line != -1 && (stop_line == -1 || line < stop_line) && !done;
+ this->gp_source.grep_next_line(line))
+ {
+ line_value.clear();
+ done = !this->gp_source.grep_value_for_line(line, line_value);
+ if (!done) {
+ this->gp_pcre->capture_from(line_value)
+ .for_each([&](lnav::pcre2pp::match_data& md) {
+ if (md.leading().sf_begin == 0) {
+ fprintf(stdout, "%d\n", (int) line);
+ }
+ fprintf(stdout,
+ "[%d:%d]\n",
+ md[0]->sf_begin,
+ md[0]->sf_end);
+ for (int lpc = 1; lpc < md.get_count(); lpc++) {
+ if (!md[lpc]) {
+ continue;
+ }
+ fprintf(stdout,
+ "(%d:%d)",
+ md[lpc]->sf_begin,
+ md[lpc]->sf_end);
+
+ fwrite(
+ md[lpc]->data(), 1, md[lpc]->length(), stdout);
+ fputc('\n', stdout);
+ }
+ fprintf(stdout, "/\n");
+ });
+ }
+
+ if (((line + 1) % 10000) == 0) {
+ /* Periodically flush the buffer so the parent sees progress */
+ this->child_batch();
+ }
+ }
+
+ if (stop_line == -1) {
+ // When scanning to the end of the source, we need to return the
+ // highest line that was seen so that the next request that
+ // continues from the end works properly.
+ fprintf(stdout, "h%d\n", line - 1);
+ }
+ this->gp_highest_line = line - 1_vl;
+ this->child_term();
+ }
+}
+
+template<typename LineType>
+void
+grep_proc<LineType>::cleanup()
+{
+ if (this->gp_child != -1 && this->gp_child != 0) {
+ int status = 0;
+
+ kill(this->gp_child, SIGTERM);
+ while (waitpid(this->gp_child, &status, 0) < 0 && (errno == EINTR)) {
+ ;
+ }
+ require(!WIFSIGNALED(status) || WTERMSIG(status) != SIGABRT);
+ this->gp_child = -1;
+ this->gp_child_started = false;
+
+ if (this->gp_sink) {
+ for (size_t lpc = 0; lpc < this->gp_child_queue_size; lpc++) {
+ this->gp_sink->grep_end(*this);
+ }
+ }
+ }
+
+ if (this->gp_err_pipe != -1) {
+ this->gp_err_pipe.reset();
+ }
+
+ this->gp_pipe_range.clear();
+ this->gp_line_buffer.reset();
+
+ ensure(this->invariant());
+
+ if (!this->gp_queue.empty()) {
+ this->start();
+ }
+}
+
+template<typename LineType>
+void
+grep_proc<LineType>::dispatch_line(char* line)
+{
+ int start, end, capture_start;
+
+ require(line != nullptr);
+
+ if (sscanf(line, "h%d", this->gp_highest_line.out()) == 1) {
+ } else if (sscanf(line, "%d", this->gp_last_line.out()) == 1) {
+ /* Starting a new line with matches. */
+ ensure(this->gp_last_line >= 0);
+ } else if (sscanf(line, "[%d:%d]", &start, &end) == 2) {
+ require(start >= 0);
+ require(end >= 0);
+
+ /* Pass the match offsets to the sink delegate. */
+ if (this->gp_sink != nullptr) {
+ this->gp_sink->grep_match(*this, this->gp_last_line, start, end);
+ }
+ } else if (sscanf(line, "(%d:%d)%n", &start, &end, &capture_start) == 2) {
+ require(start == -1 || start >= 0);
+ require(end >= 0);
+
+ /* Pass the captured strings to the sink delegate. */
+ if (this->gp_sink != nullptr) {
+ this->gp_sink->grep_capture(
+ *this,
+ this->gp_last_line,
+ start,
+ end,
+ start < 0 ? nullptr : &line[capture_start]);
+ }
+ } else if (line[0] == '/') {
+ if (this->gp_sink != nullptr) {
+ this->gp_sink->grep_match_end(*this, this->gp_last_line);
+ }
+ } else {
+ log_error("bad line from child -- %s", line);
+ }
+}
+
+template<typename LineType>
+void
+grep_proc<LineType>::check_poll_set(const std::vector<struct pollfd>& pollfds)
+{
+ require(this->invariant());
+
+ if (this->gp_err_pipe != -1 && pollfd_ready(pollfds, this->gp_err_pipe)) {
+ char buffer[1024 + 1];
+ ssize_t rc;
+
+ rc = read(this->gp_err_pipe, buffer, sizeof(buffer) - 1);
+ if (rc > 0) {
+ static const char* PREFIX = ": ";
+
+ buffer[rc] = '\0';
+ if (strncmp(buffer, PREFIX, strlen(PREFIX)) == 0) {
+ char* lf;
+
+ if ((lf = strchr(buffer, '\n')) != nullptr) {
+ *lf = '\0';
+ }
+ if (this->gp_control != nullptr) {
+ this->gp_control->grep_error(&buffer[strlen(PREFIX)]);
+ }
+ }
+ } else if (rc == 0) {
+ this->gp_err_pipe.reset();
+ }
+ }
+
+ if (this->gp_line_buffer.get_fd() != -1
+ && pollfd_ready(pollfds, this->gp_line_buffer.get_fd()))
+ {
+ try {
+ static const int MAX_LOOPS = 100;
+
+ int loop_count = 0;
+ bool drained = false;
+
+ while (loop_count < MAX_LOOPS) {
+ auto load_result
+ = this->gp_line_buffer.load_next_line(this->gp_pipe_range);
+
+ if (load_result.isErr()) {
+ log_error("failed to read from grep_proc child: %s",
+ load_result.unwrapErr().c_str());
+ break;
+ }
+
+ auto li = load_result.unwrap();
+
+ if (li.li_file_range.empty()) {
+ drained = true;
+ break;
+ }
+
+ this->gp_pipe_range = li.li_file_range;
+ this->gp_line_buffer.read_range(li.li_file_range)
+ .then([this](auto sbr) {
+ auto_mem<char> buf;
+
+ buf = (char*) malloc(sbr.length() + 1);
+ sbr.rtrim(is_line_ending);
+ memcpy(buf, sbr.get_data(), sbr.length());
+ buf[sbr.length()] = '\0';
+ this->dispatch_line(buf);
+ });
+
+ loop_count += 1;
+ }
+
+ if (this->gp_sink != nullptr) {
+ this->gp_sink->grep_end_batch(*this);
+ }
+
+ if (drained && this->gp_line_buffer.is_pipe_closed()) {
+ this->cleanup();
+ }
+ } catch (line_buffer::error& e) {
+ this->cleanup();
+ }
+ }
+
+ ensure(this->invariant());
+}
+
+template<typename LineType>
+grep_proc<LineType>&
+grep_proc<LineType>::invalidate()
+{
+ if (this->gp_sink) {
+ for (size_t lpc = 0; lpc < this->gp_queue.size(); lpc++) {
+ this->gp_sink->grep_end(*this);
+ }
+ }
+ this->gp_queue.clear();
+ this->cleanup();
+ return *this;
+}
+
+template<typename LineType>
+void
+grep_proc<LineType>::update_poll_set(std::vector<struct pollfd>& pollfds)
+{
+ if (this->gp_line_buffer.get_fd() != -1) {
+ pollfds.push_back(
+ (struct pollfd){this->gp_line_buffer.get_fd(), POLLIN, 0});
+ }
+ if (this->gp_err_pipe.get() != -1) {
+ pollfds.push_back((struct pollfd){this->gp_err_pipe, POLLIN, 0});
+ }
+}
+
+template class grep_proc<vis_line_t>;
diff --git a/src/grep_proc.hh b/src/grep_proc.hh
new file mode 100644
index 0000000..58010e3
--- /dev/null
+++ b/src/grep_proc.hh
@@ -0,0 +1,307 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file grep_proc.hh
+ */
+
+#ifndef grep_proc_hh
+#define grep_proc_hh
+
+#include <deque>
+#include <exception>
+#include <string>
+#include <vector>
+
+#include <poll.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "base/auto_fd.hh"
+#include "base/auto_mem.hh"
+#include "base/lnav_log.hh"
+#include "line_buffer.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "pollable.hh"
+#include "strong_int.hh"
+
+template<typename LineType>
+class grep_proc;
+
+/**
+ * Data source for lines to be searched using a grep_proc.
+ */
+template<typename LineType>
+class grep_proc_source {
+public:
+ virtual ~grep_proc_source() = default;
+
+ virtual void register_proc(grep_proc<LineType>* proc)
+ {
+ this->gps_proc = proc;
+ }
+
+ /**
+ * Get the value for a particular line in the source.
+ *
+ * @param line The line to retrieve.
+ * @param value_out The destination for the line value.
+ */
+ virtual bool grep_value_for_line(LineType line, std::string& value_out) = 0;
+
+ virtual LineType grep_initial_line(LineType start, LineType highest)
+ {
+ if (start == -1) {
+ return highest;
+ }
+ return start;
+ }
+
+ virtual void grep_next_line(LineType& line) { line = line + LineType(1); }
+
+ grep_proc<LineType>* gps_proc;
+};
+
+/**
+ * Delegate interface for control messages from the grep_proc.
+ */
+class grep_proc_control {
+public:
+ virtual ~grep_proc_control() = default;
+
+ /** @param msg The error encountered while attempting the grep. */
+ virtual void grep_error(const std::string& msg) {}
+};
+
+/**
+ * Sink for matches produced by a grep_proc instance.
+ */
+template<typename LineType>
+class grep_proc_sink {
+public:
+ virtual ~grep_proc_sink() = default;
+
+ virtual void grep_quiesce() {}
+
+ /** Called at the start of a new grep run. */
+ virtual void grep_begin(grep_proc<LineType>& gp,
+ LineType start,
+ LineType stop)
+ {
+ }
+
+ /** Called periodically between grep_begin and grep_end. */
+ virtual void grep_end_batch(grep_proc<LineType>& gp) {}
+
+ /** Called at the end of a grep run. */
+ virtual void grep_end(grep_proc<LineType>& gp) {}
+
+ /**
+ * Called when a match is found on 'line' and between [start, end).
+ *
+ * @param line The line number that matched.
+ * @param start The offset within the line where the match begins.
+ * @param end The offset of the character after the last character in the
+ * match.
+ */
+ virtual void grep_match(grep_proc<LineType>& gp,
+ LineType line,
+ int start,
+ int end)
+ = 0;
+
+ /**
+ * Called for each captured substring in the line.
+ *
+ * @param line The line number that matched.
+ * @param start The offset within the line where the capture begins.
+ * @param end The offset of the character after the last character in the
+ * capture.
+ * @param capture The captured substring itself.
+ */
+ virtual void grep_capture(grep_proc<LineType>& gp,
+ LineType line,
+ int start,
+ int end,
+ char* capture){};
+
+ virtual void grep_match_end(grep_proc<LineType>& gp, LineType line){};
+};
+
+/**
+ * "Grep" that runs in a separate process so it doesn't stall user-interaction.
+ * This class manages the child process and any interactions between the parent
+ * and child. The source data to be matched comes from the grep_proc_source
+ * delegate and the results are sent to the grep_proc_sink delegate in the
+ * parent process.
+ *
+ * Note: The "grep" executable is not actually used, instead we use the pcre(3)
+ * library directly.
+ */
+template<typename LineType>
+class grep_proc : public pollable {
+public:
+ class error : public std::exception {
+ public:
+ error(int err) : e_err(err){};
+
+ int e_err;
+ };
+
+ /**
+ * Construct a grep_proc object. You must call the start() method
+ * to fork off the child process and begin processing.
+ *
+ * @param code The pcre code to run over the lines of input.
+ * @param gps The source of the data to match.
+ */
+ grep_proc(std::shared_ptr<lnav::pcre2pp::code> code,
+ grep_proc_source<LineType>& gps,
+ std::shared_ptr<pollable_supervisor> ps);
+
+ grep_proc(std::shared_ptr<pollable_supervisor>);
+
+ using injectable = grep_proc(std::shared_ptr<pollable_supervisor>);
+
+ virtual ~grep_proc();
+
+ /** @param gpd The sink to send results to. */
+ void set_sink(grep_proc_sink<LineType>* gpd) { this->gp_sink = gpd; }
+
+ grep_proc& invalidate();
+
+ /** @param gpc The control to send results to. */
+ void set_control(grep_proc_control* gpc) { this->gp_control = gpc; }
+
+ /** @return The sink to send results to. */
+ grep_proc_sink<LineType>* get_sink() { return this->gp_sink; }
+
+ /**
+ * Queue a request to search the input between the given line numbers.
+ *
+ * @param start The line number to start the search at.
+ * @param stop The line number to stop the search at (exclusive) or -1 to
+ * read until the end-of-file.
+ */
+ grep_proc& queue_request(LineType start = LineType(0),
+ LineType stop = LineType(-1))
+ {
+ require(start != -1 || stop == -1);
+ require(stop == -1 || start < stop);
+
+ this->gp_queue.emplace_back(start, stop);
+ if (this->gp_sink) {
+ this->gp_sink->grep_begin(*this, start, stop);
+ }
+
+ return *this;
+ }
+
+ /**
+ * Start the search requests that have been queued up with queue_request.
+ */
+ void start();
+
+ void update_poll_set(std::vector<struct pollfd>& pollfds) override;
+
+ /**
+ * Check the fd_set to see if there is any new data to be processed.
+ *
+ * @param ready_rfds The set of ready-to-read file descriptors.
+ */
+ void check_poll_set(const std::vector<struct pollfd>& pollfds) override;
+
+ /** Check the invariants for this object. */
+ bool invariant()
+ {
+ if (this->gp_child_started) {
+ require(this->gp_child > 0);
+ require(this->gp_line_buffer.get_fd() != -1);
+ } else {
+ /* require(this->gp_child == -1); XXX doesnt work with static destr
+ */
+ require(this->gp_line_buffer.get_fd() == -1);
+ }
+
+ return true;
+ }
+
+protected:
+ /**
+ * Dispatch a line received from the child.
+ */
+ void dispatch_line(char* line);
+
+ /**
+ * Free any resources used by the object and make sure the child has been
+ * terminated.
+ */
+ void cleanup();
+
+ void child_loop();
+
+ virtual void child_init(){};
+
+ virtual void child_batch() { fflush(stdout); }
+
+ virtual void child_term() { fflush(stdout); }
+
+ virtual void handle_match(
+ int line, std::string& line_value, int off, int* matches, int count);
+
+ std::shared_ptr<lnav::pcre2pp::code> gp_pcre;
+ grep_proc_source<LineType>& gp_source; /*< The data source delegate. */
+
+ auto_fd gp_err_pipe; /*< Standard error from the child. */
+ line_buffer gp_line_buffer; /*< Standard out from the child. */
+ file_range gp_pipe_range;
+
+ pid_t gp_child{-1}; /*<
+ * The child's pid or zero in the
+ * child.
+ */
+ bool gp_child_started{false}; /*< True if the child was start()'d. */
+ size_t gp_child_queue_size{0};
+
+ /** The queue of search requests. */
+ std::deque<std::pair<LineType, LineType> > gp_queue;
+ LineType gp_last_line{0}; /*<
+ * The last line number received from
+ * the child. For multiple matches,
+ * the line number is only sent once.
+ */
+ LineType gp_highest_line; /*< The highest numbered line processed
+ * by the grep child process. This
+ * value is used when the start line
+ * for a queued request is -1.
+ */
+ grep_proc_sink<LineType>* gp_sink{nullptr}; /*< The sink delegate. */
+ grep_proc_control* gp_control{nullptr}; /*< The control delegate. */
+};
+
+#endif
diff --git a/src/help.md b/src/help.md
new file mode 100644
index 0000000..8a8e8fa
--- /dev/null
+++ b/src/help.md
@@ -0,0 +1,536 @@
+# lnav
+
+A fancy log file viewer for the terminal.
+
+## Overview
+
+The Logfile Navigator, **lnav**, is an enhanced log file viewer that
+takes advantage of any semantic information that can be gleaned from
+the files being viewed, such as timestamps and log levels. Using this
+extra semantic information, lnav can do things like interleaving
+messages from different files, generate histograms of messages over
+time, and providing hotkeys for navigating through the file. It is
+hoped that these features will allow the user to quickly and
+efficiently zero in on problems.
+
+## Opening Paths/URLs
+
+The main arguments to lnav are the local/remote files, directories,
+glob patterns, or URLs to be viewed. If no arguments are given, the
+default syslog file for your system will be opened. These arguments
+will be polled periodically so that any new data or files will be
+automatically loaded. If a previously loaded file is removed or
+replaced, it will be closed and the replacement opened.
+
+Note: When opening SFTP URLs, if the password is not provided for the
+host, the SSH agent can be used to do authentication.
+
+## Options
+
+Lnav takes a list of files to view and/or you can use the flag
+arguments to load well-known log files, such as the syslog log
+files. The flag arguments are:
+
+* `-a` Load all of the most recent log file types.
+* `-r` Recursively load files from the given directory hierarchies.
+* `-R` Load older rotated log files as well.
+
+When using the flag arguments, lnav will look for the files relative
+to the current directory and its parent directories. In other words,
+if you are working within a directory that has the well-known log
+files, those will be preferred over any others.
+
+If you do not want the default syslog file to be loaded when
+no files are specified, you can pass the `-N` flag.
+
+Any files given on the command-line are scanned to determine their log
+file format and to create an index for each line in the file. You do
+not have to manually specify the log file format. The currently
+supported formats are: syslog, apache, strace, tcsh history, and
+generic log files with timestamps.
+
+Lnav will also display data piped in on the standard input. The
+following options are available when doing so:
+
+* `-t` Prepend timestamps to the lines of data being read in
+ on the standard input.
+* `-w file` Write the contents of the standard input to this file.
+
+To automatically execute queries or lnav commands after the files
+have been loaded, you can use the following options:
+
+* `-c cmd` A command, query, or file to execute. The first character
+ determines the type of operation: a colon (`:`) is used for the
+ built-in commands; a semi-colon (`;`) for SQL queries; and a
+ pipe symbol (`|`) for executing a file containing other
+ commands. For example, to open the file "foo.log" and go
+ to the tenth line in the file, you can do:
+
+ ```shell
+ lnav -c ':goto 10' foo.log
+ ```
+
+ This option can be given multiple times to execute multiple
+ operations in sequence.
+* `-f file` A file that contains commands, queries, or files to execute.
+ This option is a shortcut for `-c '|file'`. You can use a dash
+ (`-`) to execute commands from the standard input.
+
+To execute commands/queries without opening the interactive text UI,
+you can pass the `-n` option. This combination of options allows you to
+write scripts for processing logs with lnav. For example, to get a list
+of IP addresses that dhclient has bound to in CSV format:
+
+```lnav
+#! /usr/bin/lnav -nf
+
+# Usage: dhcp_ip.lnav /var/log/messages
+# Only include lines that look like:
+# Apr 29 00:31:56 example-centos5 dhclient: bound to 10.1.10.103 -- renewal in 9938 seconds.
+
+:filter-in dhclient: bound to
+
+# The log message parser will extract the IP address
+# as col_0, so we select that and alias it to "dhcp_ip".
+;SELECT DISTINCT col_0 AS dhcp_ip FROM logline;
+
+# Finally, write the results of the query to stdout.
+:write-csv-to -
+```
+
+## Display
+
+The main part of the display shows the log lines from the files interleaved
+based on time-of-day. New lines are automatically loaded as they are appended
+to the files and, if you are viewing the bottom of the files, lnav will scroll
+down to display the new lines, much like `tail -f`.
+
+On color displays, the lines will be highlighted as follows:
+
+* Errors will be colored in <span class="-lnav_log-level-styles_error">red</span>;
+* warnings will be <span class="-lnav_log-level-styles_warning">yellow</span>;
+* boundaries between days will be ${ansi_underline}underlined${ansi_norm}; and
+* various color highlights will be applied to: IP addresses, SQL keywords,
+ XML tags, file and line numbers in Java backtraces, and quoted strings.
+
+To give you an idea of where you are spatially, the right side of the
+display has a proportionally sized 'scroll bar' that indicates your
+current position in the files. The scroll bar will also show areas of
+the file where warnings or errors are detected by coloring the bar
+yellow or red, respectively. Tick marks will also be added to the
+left and right-hand side of the bar, for search hits and bookmarks.
+
+The bar on the left side indicates the file the log message is from. A
+break in the bar means that the next log message comes from a different
+file. The color of the bar is derived from the file name. Pressing the
+left-arrow or `h` will reveal the source file names for each message and
+pressing again will show the full paths.
+
+Above and below the main body are status lines that display a variety
+of information. The top line displays:
+
+* The current time, configurable by the `/ui/clock-format` property.
+* The highest priority message from the `lnav_user_notifications` table.
+ You can insert rows into this table to display your own status messages.
+ The default message displayed on startup explains how to focus on the
+ next status line at the top, which is an interactive breadcrumb bar.
+
+The second status line at the top display breadcrumbs for the top line
+in the main view. Pressing `ENTER` will focus input on the breadcrumb
+bar, the cursor keys can be used to select a breadcrumb. The common
+breadcrumbs are:
+
+* The name of the current view.
+* In the log view, the timestamp of the top log message.
+* In the log view, the format of the log file the top log message is from.
+* The name of the file the top line was pulled from.
+* If the top line is within a larger chunk of structured data, the path to
+ the value in the top line will be shown.
+
+Notes:
+
+1. Pressing `CTRL-A`/`CTRL-E` will select the first/last breadcrumb.
+1. Typing text while a breadcrumb is selected will perform a fuzzy
+ search on the possibilities.
+
+The bottom status bar displays:
+
+* The line number for the top line in the display.
+* The current search hit, the total number of hits, and the search term.
+
+If the view supports filtering, there will be a status line showing the
+following:
+
+* The number of enabled filters and the total number of filters.
+* The number of lines not displayed because of filtering.
+
+To edit the filters, you can press TAB to change the focus from the main
+view to the filter editor. The editor allows you to create, enable/disable,
+and delete filters easily.
+
+Along with filters, a "Files" panel will also be available for viewing
+and controlling the files that lnav is currently monitoring.
+
+Finally, the last line on the display is where you can enter search
+patterns and execute internal commands, such as converting a
+unix-timestamp into a human-readable date. The command-line is
+implemented using the readline library, so the usual set of keyboard
+shortcuts are available. Most commands and searches also support
+tab-completion.
+
+The body of the display is also used to display other content, such
+as: the help file, histograms of the log messages over time, and
+SQL results. The views are organized into a stack so that any time
+you activate a new view with a key press or command, the new view
+is pushed onto the stack. Pressing the same key again will pop the
+view off of the stack and return you to the previous view. Note
+that you can always use `q` to pop the top view off of the stack.
+
+## Default Key Bindings
+
+### Views
+
+| Key(s) | Action |
+| ------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **?** | View/leave this help message. |
+| **q** | Leave the current view or quit the program when in the log file view. |
+| Q | Similar to `q`, except it will try to sync the top time between the current and former views. For example, when leaving the spectrogram view with `Q`, the top time in that view will be matched to the top time in the log view. |
+| TAB | Toggle focusing on the filter editor or the main view. |
+| ENTER | Focus on the breadcrumb bar. |
+| a/A | Restore the view that was previously popped with `q`/`Q`. The `A` hotkey will try to match the top times between the two views. |
+| X | Close the current text file or log file. |
+
+### Spatial Navigation
+
+| Key(s) | Action |
+|----------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| g/Home | Move to the top of the file. |
+| G/End | Move to the end of the file. If the view is already at the end, it will move to the last line. |
+| SPACE/PgDn | Move down a page. |
+| CTRL+d | Move down by half a page. |
+| b/PgUp | Move up a page. |
+| CTRL+u | Move up by half a page. |
+| j/&DownArrow; | Move down a line. |
+| k/&UpArrow; | Move up a line. |
+| h/&LeftArrow; | Move to the left. In the log view, moving left will reveal the source log file names for each line. Pressing again will reveal the full path. |
+| l/&RightArrow; | Move to the right. |
+| H/Shift &LeftArrow; | Move to the left by a smaller increment. |
+| L/Shift &RightArrow; | Move to the right by a smaller increment. |
+| e/E | Move to the next/previous error. |
+| w/W | Move to the next/previous warning. |
+| n/N | Move to the next/previous search hit. When pressed repeatedly within a short time, the view will move at least a full page at a time instead of moving to the next hit. |
+| f/F | Move to the next/previous file. In the log view, this moves to the next line from a different file. In the text view, this rotates the view to the next file. |
+| &gt;/&lt; | Move horizontally to the next/previous search hit. |
+| o/O | Move forward/backward to the log message with a matching 'operation ID' (opid) field. |
+| u/U | Move forward/backward through any user bookmarks you have added using the 'm' key. This hotkey will also jump to the start of any log partitions that have been created with the 'partition-name' command. |
+| s/S | Move to the next/previous "slow down" in the log message rate. A slow down is detected by measuring how quickly the message rate has changed over the previous several messages. For example, if one message is logged every second for five seconds and then the last message arrives five seconds later, the last message will be highlighted as a slow down. |
+| {/} | Move to the previous/next location in history. Whenever you jump to a new location in the view, the location will be added to the history. The history is not updated when using only the arrow keys. |
+
+### Chronological Navigation
+
+| Key(s) | Action |
+| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| d/D | Move forward/backward 24 hours from the current position in the log file. |
+| 1-6/Shift 1-6 | Move to the next/previous n'th ten minute of the hour. For example, '4' would move to the first log line in the fortieth minute of the current hour in the log. And, '6' would move to the next hour boundary. |
+| 7/8 | Move to the previous/next minute. |
+| 0/Shift 0 | Move to the next/previous day boundary. |
+| r/R | Move forward/backward based on the relative time that was last used with the 'goto' command. For example, executing ':goto a minute later' will move the log view forward a minute and then pressing 'r' will move it forward a minute again. Pressing 'R' will then move the view in the opposite direction, so backwards a minute. |
+
+### Bookmarks
+
+| Key(s) | Action |
+| ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| m | Mark/unmark the line at the top of the display. The line will be highlighted with reverse video to indicate that it is a user bookmark. You can use the `u` hotkey to iterate through marks you have added. |
+| M | Mark/unmark all the lines between the top of the display and the last line marked/unmarked. |
+| J | Mark/unmark the next line after the previously marked line. |
+| K | Like `J` except it toggles the mark on the previous line. |
+| c | Copy the marked text to the X11 selection buffer or OS X clipboard. |
+| C | Clear all marked lines. |
+
+### Display options
+
+| Key(s) | Action |
+| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| P | Switch to/from the pretty-printed view of the log or text files currently displayed. In this view, structured data, such as XML, will be reformatted to make it easier to read. |
+| t | Switch to/from the text file view. The text file view is for any files that are not recognized as log files. |
+| = | Pause/unpause loading of new file data. |
+| Ctrl-L | (Lo-fi mode) Exit screen-mode and write the displayed log lines in plain text to the terminal until a key is pressed. Useful for copying long lines from the terminal without picking up any of the extra decorations. |
+| T | Toggle the display of the "elapsed time" column that shows the time elapsed since the beginning of the logs or the offset from the previous bookmark. Sharp changes in the message rate are highlighted by coloring the separator between the time column and the log message. A red highlight means the message rate has slowed down and green means it has sped up. You can use the "s/S" hotkeys to scan through the slow downs. |
+| i | View/leave a histogram of the log messages over time. The histogram counts the number of displayed log lines for each bucket of time. The bars are layed out horizontally with colored segments representing the different log levels. You can use the `z` hotkey to change the size of the time buckets (e.g. ten minutes, one hour, one day). |
+| I | Switch between the log and histogram views while keeping the time displayed at the top of each view in sync. For example, if the top line in the log view is "11:40", hitting `I` will switch to the histogram view and scrolled to display "11:00" at the top (if the zoom level is hours). |
+| z/Shift Z | Zoom in or out one step in the histogram view. |
+| v | Switch to/from the SQL result view. |
+| V | Switch between the log and SQL result views while keeping the top line number in the log view in sync with the log_line column in the SQL view. For example, doing a query that selects for "log_idle_msecs" and "log_line", you can move the top of the SQL view to a line and hit 'V' to switch to the log view and move to the line number that was selected in the "log_line" column. If there is no "log_line" column, lnav will find the first column with a timestamp and move to corresponding time in the log view. |
+| TAB/Shift TAB | In the SQL result view, cycle through the columns that are graphed. Initially, all number values are displayed in a stacked graph. Pressing TAB will change the display to only graph the first column. Repeatedly pressing TAB will cycle through the columns until they are all graphed again. |
+| p | In the log view: enable or disable the display of the fields that the log message parser knows about or has discovered. This overlay is temporarily enabled when the semicolon key (;) is pressed so that it is easier to write queries. |
+| | In the DB view: enable or disable the display of values in columns containing JSON-encoded values in the top row. The overlay will display the JSON-Pointer reference and value for all fields in the JSON data. |
+| CTRL-W | Toggle word-wrapping. |
+| CTRL-P | Show/hide the data preview panel that may be opened when entering commands or SQL queries. |
+| CTRL-F | Toggle the enabled/disabled state of all filters in the current view. |
+| x | Toggle the hiding of log message fields. The hidden fields will be replaced with three bullets and highlighted in yellow. |
+| CTRL-X | Toggle the cursor mode. Allows moving the selected line instead of keeping it fixed at the top of the current screen. |
+| F2 | Toggle mouse support. |
+
+### Query
+
+| Key(s) | Action |
+| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| **/**_regexp_ | Start a search for the given regular expression. The search is live, so when there is a pause in typing, the currently running search will be canceled and a new one started. The first ten lines that match the search will be displayed in the preview window at the bottom of the view. History is maintained for your searches so you can rerun them easily. Words that are currently displayed are also available for tab-completion, so you can easily search for values without needing to copy-and-paste the string. If there is an error encountered while trying to interpret the expression, the error will be displayed in red on the status line. While the search is active, the 'hits' field in the status line will be green, when finished it will turn back to black. |
+| **:**&lt;command&gt; | Execute an internal command. The commands are listed below. History is also supported in this context as well as tab-completion for commands and some arguments. The result of the command replaces the command you typed. |
+| **;**&lt;sql&gt; | Execute an SQL query. Most supported log file formats provide a sqlite virtual table backend that can be used in queries. See the SQL section below for more information. |
+| **&VerticalLine;**&lt;script&gt; [arg1...] | Execute an lnav script contained in a format directory (e.g. \~/.lnav/formats/default). The script can contain lines starting with `:`, `;`, or `\|` to execute commands, SQL queries or execute other files in lnav. Any values after the script name are treated as arguments can be referenced in the script using `\$1`, `\$2`, and so on, like in a shell script. |
+| CTRL+], ESCAPE | Abort command-line entry started with `/`, `:`, `;`, or `\|`. |
+
+> **Note**: The regular expression format used by lnav is
+> [PCRE](http://perldoc.perl.org/perlre.html)
+> (Perl-Compatible Regular Expressions).
+>
+> If the search string is not valid PCRE, a search
+> is done for the exact string instead of doing a
+> regex search.
+
+## Session
+
+| Key(s) | Action |
+| ------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
+| CTRL-R | Reset the session state. This will save the current session state (filters, highlights) and then reset the state to the factory default. |
+
+## Filter Editor
+
+The following hotkeys are only available when the focus is on the filter
+editor. You can change the focus by pressing TAB.
+
+| Key(s) | Action |
+| ------------- | ------------------------------------------------------------------- |
+| q | Switch the focus back to the main view. |
+| j/&DownArrow; | Select the next filter. |
+| k/&UpArrow; | Select the previous filter. |
+| o | Create a new "out" filter. |
+| i | Create a new "in" filter . |
+| SPACE | Toggle the enabled/disabled state of the currently selected filter. |
+| t | Toggle the type of filter between "in" and "out". |
+| ENTER | Edit the selected filter. |
+| D | Delete the selected filter. |
+
+## Mouse Support (experimental)
+
+If you are using Xterm, or a compatible terminal, you can use the mouse to
+mark lines of text and move the view by grabbing the scrollbar.
+
+NOTE: You need to manually enable this feature by setting the LNAV_EXP
+environment variable to "mouse". F2 toggles mouse support.
+
+## SQL Queries (experimental)
+
+Lnav has support for performing SQL queries on log files using the
+Sqlite3 "virtual" table feature. For all supported log file types,
+lnav will create tables that can be queried using the subset of SQL
+that is supported by Sqlite3. For example, to get the top ten URLs
+being accessed in any loaded Apache log files, you can execute:
+
+```lnav
+;SELECT cs_uri_stem, count(*) AS total FROM access_log
+ GROUP BY cs_uri_stem ORDER BY total DESC LIMIT 10;
+```
+
+The query result view shows the results and graphs any numeric
+values found in the result, much like the histogram view.
+
+The builtin set of log tables are listed below. Note that only the
+log messages that match a particular format can be queried by a
+particular table. You can find the file format and table name for
+the top log message by looking in the upper right hand corner of the
+log file view.
+
+Some commonly used format tables are:
+
+| Name | Description |
+| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
+| access_log | Apache common access log format |
+| syslog_log | Syslog format |
+| strace_log | Strace log format |
+| generic_log | 'Generic' log format. This table contains messages from files that have a very simple format with a leading timestamp followed by the message. |
+
+NOTE: You can get a dump of the schema for the internal tables, and
+any attached databases, by running the `.schema` SQL command.
+
+The columns available for the top log line in the view will
+automatically be displayed after pressing the semicolon (`;`) key.
+All log tables contain at least the following columns:
+
+| Column | Description |
+| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| log_line | The line number in the file, starting at zero. |
+| log_part | The name of the partition. You can change this column using an UPDATE SQL statement or with the 'partition-name' command. After a value is set, the following log messages will have the same partition name up until another name is set. |
+| log_time | The time of the log entry. |
+| log_idle_msecs | The amount of time, in milliseconds, between the current log message and the previous one. |
+| log_level | The log level (e.g. info, error, etc...). |
+| log_mark | The bookmark status for the line. This column can be written to using an UPDATE query. |
+| log_path | The full path to the file. |
+| log_text | The raw line of text. Note that this column is not included in the result of a 'select *', but it does exist. |
+
+The following tables include the basic columns as listed above and
+include a few more columns since the log file format is more
+structured.
+
+* `syslog_log`
+
+ | Column | Description |
+ | ------------ | ---------------------------------------------------- |
+ | log_hostname | The hostname the message was received from. |
+ | log_procname | The name of the process that sent the message. |
+ | log_pid | The process ID of the process that sent the message. |
+
+* `access_log` (The column names are the same as those in the
+ Microsoft LogParser tool.)
+
+ | Column | Description |
+ | ------------- | ----------------------------------------- |
+ | c_ip | The client IP address. |
+ | cs_username | The client user name. |
+ | cs_method | The HTTP method. |
+ | cs_uri_stem | The stem portion of the URI. |
+ | cs_uri_query | The query portion of the URI. |
+ | cs_version | The HTTP version string. |
+ | sc_status | The status number returned to the client. |
+ | sc_bytes | The number of bytes sent to the client. |
+ | cs_referrer | The URL of the referring page. |
+ | cs_user_agent | The user agent string. |
+
+* `strace_log` (Currently, you need to run strace with the `-tt -T`
+ options so there are timestamps for each function call.)
+
+ | Column | Description |
+ | ----------- | ---------------------------------------- |
+ | funcname | The name of the syscall. |
+ | result | The result code. |
+ | duration | The amount of time spent in the syscall. |
+ | arg0 - arg9 | The arguments passed to the syscall. |
+
+These tables are created dynamically and not stored in memory or on
+disk. If you would like to persist some information from the tables,
+you can attach another database and create tables in that database.
+For example, if you wanted to save the results from the earlier
+example of a top ten query into the "/tmp/topten.db" file, you can do:
+
+```lnav
+;ATTACH DATABASE '/tmp/topten.db' AS topten;
+;CREATE TABLE topten.foo AS SELECT cs_uri_stem, count(*) AS total
+ FROM access_log GROUP BY cs_uri_stem ORDER BY total DESC
+ LIMIT 10;
+```
+
+## Dynamic logline Table (experimental)
+
+(NOTE: This feature is still very new and not completely reliable yet,
+use with care.)
+
+For log formats that lack message structure, lnav can parse the log
+message and attempt to extract any data fields that it finds. This
+feature is available through the `logline` log table. This table is
+dynamically created and defined based on the message at the top of
+the log view. For example, given the following log message from "sudo",
+lnav will create the "logline" table with columns for "TTY", "PWD",
+"USER", and "COMMAND":
+
+```
+May 24 06:48:38 Tim-Stacks-iMac.local sudo[76387]: stack : TTY=ttys003 ; PWD=/Users/stack/github/lbuild ; USER=root ; COMMAND=/bin/echo Hello, World!
+```
+
+Queries executed against this table will then only return results for
+other log messages that have the same format. So, if you were to
+execute the following query while viewing the above line, you might
+get the following results:
+
+```lnav
+;SELECT USER,COMMAND FROM logline;
+```
+
+| USER | COMMAND |
+| ---- | ------------------------- |
+| root | /bin/echo Hello, World! |
+| mal | /bin/echo Goodbye, World! |
+
+The log parser works by examining each message for key/value pairs
+separated by an equal sign (=) or a colon (:). For example, in the
+previous example of a "sudo" message, the parser sees the "USER=root"
+string as a pair where the key is "USER" and the value is "root".
+If no pairs can be found, then anything that looks like a value is
+extracted and assigned a numbered column. For example, the following
+line is from "dhcpd":
+
+```
+Sep 16 22:35:57 drill dhcpd: DHCPDISCOVER from 00:16:ce:54:4e:f3 via hme3
+```
+
+In this case, the lnav parser recognizes that "DHCPDISCOVER", the MAC
+address and the "hme3" device name are values and not normal words. So,
+it builds a table with three columns for each of these values. The
+regular words in the message, like "from" and "via", are then used to
+find other messages with a similar format.
+
+If you would like to execute queries against log messages of different
+formats at the same time, you can use the 'create-logline-table' command
+to permanently create a table using the top line of the log view as a
+template.
+
+## Other SQL Features
+
+Environment variables can be used in SQL statements by prefixing the
+variable name with a dollar-sign (\$). For example, to read the value of
+the `HOME` variable, you can do:
+
+```lnav
+;SELECT \$HOME;
+```
+
+To select the syslog messages that have a hostname field that is equal
+to the `HOSTNAME` variable:
+
+```lnav
+;SELECT * FROM syslog_log WHERE log_hostname = \$HOSTNAME;
+```
+
+NOTE: Variable substitution is done for fields in the query and is not
+a plain text substitution. For example, the following statement
+WILL NOT WORK:
+
+```lnav
+;SELECT * FROM \$TABLE_NAME; -- Syntax error
+```
+
+Access to lnav's environment variables is also available via the "environ"
+table. The table has two columns (name, value) and can be read and written
+to using SQL SELECT, INSERT, UPDATE, and DELETE statements. For example,
+to set the "FOO" variable to the value "BAR":
+
+```lnav
+;INSERT INTO environ SELECT 'FOO', 'BAR';
+```
+
+As a more complex example, you can set the variable "LAST" to the last
+syslog line number by doing:
+
+```lnav
+;INSERT INTO environ SELECT 'LAST', (SELECT max(log_line) FROM syslog_log);
+```
+
+A delete will unset the environment variable:
+
+```lnav
+;DELETE FROM environ WHERE name='LAST';
+```
+
+The table allows you to easily use the results of a SQL query in lnav
+commands, which is especially useful when scripting lnav.
+
+## Contact
+
+For more information, visit the lnav website at:
+
+http://lnav.org
+
+For support questions, email:
+
+* lnav@googlegroups.com
+* support@lnav.org
diff --git a/src/help.txt b/src/help.txt
new file mode 100644
index 0000000..8ef16a8
--- /dev/null
+++ b/src/help.txt
@@ -0,0 +1,1005 @@
+
+ lnav - A fancy log file viewer
+
+DESCRIPTION
+===========
+
+The log file navigator, lnav, is an enhanced log file viewer that
+takes advantage of any semantic information that can be gleaned from
+the files being viewed, such as timestamps and log levels. Using this
+extra semantic information, lnav can do things like interleaving
+messages from different files, generate histograms of messages over
+time, and providing hotkeys for navigating through the file. It is
+hoped that these features will allow the user to quickly and
+efficiently zero in on problems.
+
+
+OPENING PATHS/URLs
+==================
+
+The main arguments to lnav are the files, directories, glob patterns,
+or URLs to be viewed. If no arguments are given, the default syslog
+file for your system will be opened. These arguments will be polled
+periodically so that any new data or files will be automatically
+loaded. If a previously loaded file is removed or replaced, it will
+be closed and the replacement opened.
+
+Note: When opening SFTP URLs, if the password is not provided for the
+host, the SSH agent can be used to do authentication.
+
+
+OPTIONS
+=======
+
+Lnav takes a list of files to view and/or you can use the flag
+arguments to load well-known log files, such as the syslog log
+files. The flag arguments are:
+
+ -a Load all of the most recent log file types.
+ -r Recursively load files from the given directory hierarchies.
+ -R Load older rotated log files as well.
+
+When using the flag arguments, lnav will look for the files relative
+to the current directory and its parent directories. In other words,
+if you are working within a directory that has the well-known log
+files, those will be preferred over any others.
+
+If you do not want the default syslog file to be loaded when
+no files are specified, you can pass the '-N' flag.
+
+Any files given on the command-line are scanned to determine their log
+file format and to create an index for each line in the file. You do
+not have to manually specify the log file format. The currently
+supported formats are: syslog, apache, strace, tcsh history, and
+generic log files with timestamps.
+
+Lnav will also display data piped in on the standard input. The
+following options are available when doing so:
+
+ -t Prepend timestamps to the lines of data being read in
+ on the standard input.
+ -w file Write the contents of the standard input to this file.
+
+To automatically execute queries or lnav commands after the files
+have been loaded, you can use the following options:
+
+ -c cmd A command, query, or file to execute. The first character
+ determines the type of operation: a colon is used for the
+ built-in commands; a semi-colon for SQL queries; and a
+ pipe symbol (|) for executing a file containing other
+ commands. For example, to open the file "foo.log" and go
+ to the tenth line in the file, you can do:
+
+ lnav -c ':goto 10' foo.log
+
+ This option can be given multiple times to execute multiple
+ operations in sequence.
+
+ -f file A file that contains commands, queries, or files to execute.
+ This option is a shortcut for "-c '|file'". You can use a
+ dash (-) to execute commands from the standard input.
+
+To execute commands/queries without the opening the interactive text UI,
+you can pass the '-n' option. This combination of options allows you to
+write scripts for processing logs with lnav. For example, to get a list
+of IP addresses that dhclient has bound to in CSV format:
+
+ #! /usr/bin/lnav -nf
+
+ # Usage: dhcp_ip.lnav /var/log/messages
+
+ # Only include lines that look like:
+ # Apr 29 00:31:56 example-centos5 dhclient: bound to 10.1.10.103 -- renewal in 9938 seconds.
+ :filter-in dhclient: bound to
+
+ # The log message parser will extract the IP address as col_0, so we
+ # select that and alias it to "dhcp_ip".
+ ;select distinct col_0 as dhcp_ip from logline;
+
+ # Finally, write the results of the query to stdout.
+ :write-csv-to -
+
+
+DISPLAY
+=======
+
+The main part of the display shows the log lines from the files interleaved
+based on time-of-day. New lines are automatically loaded as they are appended
+to the files and, if you are viewing the bottom of the files, lnav will scroll
+down to display the new lines, much like 'tail -f'.
+
+On color displays, the lines will be highlighted as follows:
+
+ * Errors will be colored in ${ansi_red}red${ansi_norm};
+ * warnings will be ${ansi_yellow}yellow${ansi_norm};
+ * boundaries between days will be ${ansi_underline}underlined${ansi_norm}; and
+ * various color highlights will be applied to: IP addresses, SQL keywords,
+ XML tags, file and line numbers in Java backtraces, and quoted strings.
+
+To give you an idea of where you are spatially, the right side of the
+display has a proportionally sized 'scroll bar' that indicates your
+current position in the files. The scroll bar will also show areas of
+the file where warnings or errors are detected by coloring the bar
+yellow or red, respectively. Tick marks will also be added to the
+left and right hand side of the bar, for search hits and bookmarks.
+
+A bar on the left side is color coded and broken up to indicate which
+messages are from the same file. Pressing the left-arrow or 'h' will
+reveal the source file names for each message and pressing again will
+show the full paths.
+
+When at the bottom of the log view, a summary line will be displayed on the
+right-hand-side to give you some more information about your logs, including:
+how long ago the last message was generated, the number of log files, the
+error rate, and how much time the logs cover. The error rate display shows
+the errors-per-minute over the last five minutes. A bar chart is also
+overlaid on the "Error rate" label to show the error rate over the past ten
+seconds. For example, if there have not been many errors in the past five
+minutes and there is a sudden spike, the bar chart will fill up completely.
+But, if there has been a steady stream of errors, then the chart will only
+partially fill based on the recent error frequency.
+
+Above and below the main body are status lines that display:
+
+ * the current time;
+ * the name of the file the top line was pulled from;
+ * the log format for the top line;
+ * the current view;
+ * the line number for the top line in the display;
+ * the current search hit, the total number of hits, and the search term;
+
+If the view supports filtering, there will be a status line showing the
+following:
+
+ * the number of enabled filters and the total number of filters;
+ * the number of lines not displayed because of filtering.
+
+To edit the filters, you can press TAB to change the focus from the main
+view to the filter editor. The editor allows you to create, enable/disable,
+and delete filters easily.
+
+Finally, the last line on the display is where you can enter search
+patterns and execute internal commands, such as converting a
+unix-timestamp into a human-readable date. The command-line is
+implemented using the readline library, so the usual set of keyboard
+shortcuts are available. Most commands and searches also support
+tab-completion.
+
+
+The body of the display is also used to display other content, such
+as: the help file, histograms of the log messages over time, and
+SQL results. The views are organized into a stack so that any time
+you activate a new view with a key press or command, the new view
+is pushed onto the stack. Pressing the same key again will pop the
+view off of the stack and return you to the previous view. Note
+that you can always use 'q' to pop the top view off of the stack.
+
+
+DEFAULT KEY BINDINGS
+====================
+
+Views
+-----
+
+ ? View/leave this help message.
+ q Leave the current view or quit the program when in
+ the log file view.
+ Q Similar to 'q', except it will try to sync the top time
+ between the current and former views. For example, when
+ leaving the spectrogram view with 'Q', the top time in that
+ view will be matched to the top time in the log view.
+
+ TAB Toggle focusing on the filter editor or the main view.
+
+ a/A Restore the view that was previously popped with 'q/Q'.
+ The 'A' hotkey will try to match the top times between the
+ two views.
+
+ X Close the current text file or log file.
+
+Spatial Navigation
+------------------
+
+ g/home Move to the top of the file.
+ G/end Move to the end of the file. If the view is already
+ at the end, it will move to the last line.
+ space/pgdn Move down a page.
+ b/bs/pgup Move up a page.
+ j/cr/down-arrow Move down a line.
+ k/up-arrow Move up a line.
+ h/left-arrow Move to the left. In the log view, moving left will reveal
+ the source log file names for each line. Pressing again
+ will reveal the full path.
+ l/right-arrow Move to the right.
+ H/Shift+left Move to the left by a smaller increment.
+ L/Shift+right Move to the right by a smaller increment.
+
+ e/E Move to the next/previous error.
+ w/W Move to the next/previous warning.
+ n/N Move to the next/previous search hit. When pressed
+ repeatedly within a short time, the view will move at
+ least a full page at a time instead of moving to the
+ next hit.
+ f/F Move to the next/previous file. In the log view, this
+ moves to the next line from a different file. In the
+ text view, this rotates the view to the next file.
+
+ >/< Move horizontally to the next/previous search hit.
+
+ o/O Move forward/backward to the log message with a matching
+ 'operation ID' (opid) field.
+
+ u/U Move forward/backward through any user bookmarks
+ you have added using the 'm' key. This hotkey will
+ also jump to the start of any log partitions that have
+ been created with the 'partition-name' command.
+
+ s/S Move to the next/previous "slow down" in the log message
+ rate. A slow down is detected by measuring how quickly
+ the message rate has changed over the previous several
+ messages. For example, if one message is logged every
+ second for five seconds and then the last message arrives
+ five seconds later, the last message will be highlighted
+ as a slow down.
+
+ {/} Move to the previous/next location in history. Whenever
+ you jump to a new location in the view, the location will
+ be added to the history. The history is not updated when
+ using only the arrow keys.
+
+
+Chronological Navigation
+------------------------
+
+ d/D Move forward/backward 24 hours from the current
+ position in the log file.
+
+ 1-6/Shift 1-6 Move to the next/previous n'th ten minute of the
+ hour. For example, '4' would move to the first
+ log line in the fortieth minute of the current
+ hour in the log. And, '6' would move to the next
+ hour boundary.
+
+ 7/8 Move to the previous/next minute.
+
+ 0/Shift 0 Move to the next/previous day boundary.
+
+ r/R Move forward/backward based on the relative time that
+ was last used with the 'goto' command. For example,
+ executing ':goto a minute later' will move the log view
+ forward a minute and then pressing 'r' will move it
+ forward a minute again. Pressing 'R' will then move the
+ view in the opposite direction, so backwards a minute.
+
+Bookmarks
+---------
+
+ m Mark/unmark the line at the top of the display.
+ The line will be highlighted with reverse video to
+ indicate that it is a user bookmark. You can use
+ the 'u' hotkey to iterate through marks you have
+ added.
+
+ M Mark/unmark all the lines between the top of the
+ display and the last line marked/unmarked.
+
+ J Mark/unmark the next line after the previously
+ marked line.
+
+ K Like 'J' except it toggles the mark on the
+ previous line.
+
+ c Copy the marked text to the X11 selection buffer or OS X
+ clipboard.
+
+ C Clear all marked lines.
+
+Display options
+---------------
+
+ P Switch to/from the pretty-printed view of the log or text
+ files currently displayed. In this view, structured data,
+ such as XML, will be reformatted to make it easier to read.
+
+ t Switch to/from the text file view. The text file view is
+ for any files that are not recognized as log files.
+
+ = Pause/unpause loading of new file data.
+
+ Ctrl-L (Lo-fi mode) Exit screen-mode and write the
+ displayed log lines in plain text to the terminal
+ until a key is pressed. Useful for copying long lines
+ from the terminal without picking up any of the extra
+ decorations.
+
+ T Toggle the display of the "elapsed time" column that shows
+ the time elapsed since the beginning of the logs or the
+ offset from the previous bookmark. Sharp changes in the
+ message rate are highlighted by coloring the separator
+ between the time column and the log message. A red
+ highlight means the message rate has slowed down and green
+ means it has sped up. You can use the "s/S" hotkeys to
+ scan through the slow downs.
+
+ i View/leave a histogram of the log messages over
+ time. The histogram counts the number of
+ displayed log lines for each bucket of time. The
+ bars are layed out horizontally with colored
+ segments representing the different log levels.
+ You can use the 'z' hotkey to change the size of
+ the time buckets (e.g. ten minutes, one hour, one
+ day).
+
+ I Switch between the log and histogram views while
+ keeping the time displayed at the top of each view
+ in sync. For example, if the top line in the log
+ view is "11:40", hitting 'I' will switch to the
+ histogram view and scrolled to display "11:00" at
+ the top (if the zoom level is hours).
+
+ z/Shift Z Zoom in or out one step in the histogram view.
+
+ v Switch to/from the SQL result view.
+
+ V Switch between the log and SQL result views while
+ keeping the top line number in the log view in
+ sync with the log_line column in the SQL view.
+ For example, doing a query that selects for
+ "log_idle_msecs" and "log_line", you can move the
+ top of the SQL view to a line and hit 'V' to switch
+ to the log view and move to the line number that was
+ selected in the "log_line" column. If there is no
+ "log_line" column, lnav will find the first column with
+ a timestamp and move to corresponding time in the log
+ view.
+
+ TAB/Shift+TAB In the SQL result view, cycle through the columns that
+ are graphed. Initially, all number values are displayed
+ in a stacked graph. Pressing TAB will change the display
+ to only graph the first column. Repeatedly pressing TAB
+ will cycle through the columns until they are all graphed
+ again.
+
+ p In the log view: enable or disable the display of the
+ fields that the log message parser knows about or has
+ discovered. This overlay is temporarily enabled when the
+ semicolon key (;) is pressed so that it is easier to write
+ queries.
+
+ In the DB view: enable or disable the display of values
+ in columns containing JSON-encoded values in the top row.
+ The overlay will display the JSON-Pointer reference and
+ value for all fields in the JSON data.
+
+ CTRL-W Toggle word-wrapping.
+
+ CTRL-P Show/hide the data preview panel that may be opened when
+ entering commands or SQL queries.
+
+ CTRL-F Toggle the enabled/disabled state of all filters in the
+ current view.
+
+ x Toggle the hiding of log message fields. The hidden fields
+ will be replaced with three bullets and highlighted in
+ yellow.
+
+ CTRL-X Toggle the cursor mode. Allows moving the selected line
+ instead of keeping it fixed at the top of the current
+ screen.
+
+ F2 Toggle mouse support.
+
+Query
+-----
+
+ /<regexp> Start a search for the given regular expression.
+ The search is live, so when there is a pause in
+ typing, the currently running search will be
+ canceled and a new one started. The first ten lines
+ that match the search will be displayed in the preview
+ window at the bottom of the view. History is
+ maintained for your searches so you can rerun them
+ easily. Words that are currently displayed are also
+ available for tab-completion, so you can easily
+ search for values without needing to copy-and-paste
+ the string. If there is an error encountered while
+ trying to interpret the expression, the error will
+ be displayed in red on the status line. While the
+ search is active, the 'hits' field in the status
+ line will be green, when finished it will turn
+ back to black.
+
+ Note: The regular expression format used by is PCRE
+ (Perl-Compatible Regular Expressions). For example,
+ if you wanted to search for ethernet device names,
+ regardless of their ID number, you can type:
+
+ eth\\d+
+
+ You can find more information about Perl regular
+ expressions at:
+
+ http://perldoc.perl.org/perlre.html
+
+ If the search string is not valid PCRE, a search
+ is done for the exact string instead of doing a
+ regex search.
+
+ :<command> Execute an internal command. The commands are
+ listed below. History is also supported in this
+ context as well as tab-completion for commands and
+ some arguments. The result of the command
+ replaces the command you typed.
+
+ ;<sql> Execute an SQL query. Most supported log file
+ formats provide a sqlite virtual table backend
+ that can be used in queries. See the SQL section
+ below for more information.
+
+ |<script> [arg1 .. argN]
+ Execute an lnav script contained in a format directory
+ (e.g. \~/.lnav/formats/default). The script can contain
+ lines starting with ':', ';', or '|' to execute commands,
+ SQL queries or execute other files in lnav. Any values
+ after the script name are treated as arguments can be
+ referenced in the script using '\$1', '\$2', and so on, like
+ in a shell script.
+
+ CTRL+], ESCAPE Abort command-line entry started with '/', ':', ';', or '|'.
+
+Session
+-------
+
+ CTRL-R Reset the session state. This will save the current
+ session state (filters, highlights) and then reset the
+ state to the factory default.
+
+Filter Editor
+-------------
+
+The following hotkeys are only available when the focus is on the filter
+editor. You can change the focus by pressing TAB.
+
+ q Switch the focus back to the main view.
+ j/down-arrow Select the next filter.
+ k/up-arrow Select the previous filter.
+ o Create a new "out" filter.
+ i Create a new "in" filter .
+ SPACE Toggle the enabled/disabled state of the currently
+ selected filter.
+ t Toggle the type of filter between "in" and "out".
+ ENTER Edit the selected filter.
+ D Delete the selected filter.
+
+
+MOUSE SUPPORT (experimental)
+============================
+
+If you are using Xterm, or a compatible terminal, you can use the mouse to
+mark lines of text and move the view by grabbing the scrollbar.
+
+NOTE: You need to manually enable this feature by setting the LNAV_EXP
+environment variable to "mouse". F2 toggles mouse support.
+
+
+COMMANDS
+========
+
+ help Switch to this help text view.
+
+ adjust-log-time <date|relative-time>
+ Change the time of the top log line to the given time or
+ adjusted by the relative time. All other log lines in the
+ same file will also be adjusted using the same offset.
+ After the adjustment, the displayed timestamp will be
+ rewritten to the new time and highlighted with a magenta
+ color.
+
+ This command is useful for lining up log files that
+ have timestamps from different machines.
+
+ unix-time <secs-or-date>
+ Convert a unix-timestamp in seconds to a
+ human-readable form or vice-versa.
+ BEWARE OF TIMEZONE DIFFERENCES.
+
+ current-time Print the current time in human-readable form and
+ as a unix-timestamp.
+
+ goto <line#|N%|abs-time|relative-time>
+ Go to the given line number, N percent into the
+ file, or the given timestamp in the log view. If the
+ line number is negative, it is considered an offset
+ from the last line. Relative time values, like
+ 'a minute ago', 'an hour later', and many other formats
+ are supported. When using a relative time, the 'r/R'
+ hotkeys can be used to move the same amount again or in
+ the same amount in the opposite direction.
+
+ relative-goto <line#|N%>
+ Move the current view up or down by the given amount.
+
+ comment <message>
+ Add a comment to the top line in the log view. The
+ comment will be saved in the session and will be available
+ the next time the file is loaded. Searches will also scan
+ the comment for any matches.
+
+ clear-comment Clear the comment that is attached to the top line in the
+ log view.
+
+ tag <tag1> [<tag2> [... <tagN>]]
+ Attach a tag to the top line in the log view. The tags are
+ prefixed with a '#', if they don't have one already. And,
+ like comments, they are saved in the session and
+ searchable.
+
+ untag <tag1> [<tag2> [... <tagN>]]
+ Detach a tag from the top line in the log view.
+
+ delete-tags <tag1> [<tag2> [... <tagN>]]
+ Detach the tags from all log lines.
+
+ next-mark error|warning|search|user|file|meta
+ Move to the next bookmark of the given type in the
+ current view.
+
+ prev-mark error|warning|search|user|file|meta
+ Move to the previous bookmark of the given type in the
+ current view.
+
+ hide-lines-before <abs-time|relative-time>
+ Hide lines that are before the given time. The given
+ time can be absolute (e.g. 2015-10-11)
+ The hidden lines can be shown again with the
+ 'show-lines-before-and-after' command.
+
+ hide-lines-after <abs-time|relative-time>
+ Hide lines that are after the given time. The time can
+ The hidden lines can be shown again with the
+ 'show-lines-before-and-after' command.
+
+ show-lines-before-and-after
+ Show lines that were hidden by the 'hide-lines' commands.
+
+ hide-unmarked-lines
+ Hide lines that have not been bookmarked.
+
+ show-unmarked-lines
+ Show lines that have not been bookmarked.
+
+ hide-fields <field-name> [<field-name2> ... <field-nameN>]
+ Hide large log message fields by replacing them with an
+ ellipsis. You can quickly switching between showing and
+ hiding hidden fields using the 'x' hotkey.
+
+ show-fields <field-name> [<field-name2> ... <field-nameN>]
+ Show log messages fields that were previously hidden with
+ the ':hide-fields' command.
+
+ highlight <regex> Highlight strings that match the given regular
+ expression.
+
+ clear-highlight <regex>
+ Clear an existing highlight created with the 'highlight'
+ command.
+
+ filter-in <regex> Only display lines that match the given regular
+ expression. This command can be used multiple
+ times to add more lines to the display. The number
+ of lines that are filtered out will be shown in the
+ bottom status bar as 'Not Shown'. Note that filtering
+ only works in the log and plain text views. There is also
+ a limit of 32 filters per-view at any one time.
+
+ filter-out <regex>
+ Do not display lines that match the given regular
+ expression. This command can be used multiple
+ times to remove more lines from the display. If a
+ 'filter-in' expression is also active, it takes
+ priority and the filter-out will remove lines that
+ were matched by the 'filter-in'. The number
+ of lines that are filtered out will be shown in the
+ bottom status bar as 'Not Shown'. Note that filtering
+ only works in the log and plain text views. There is also
+ a limit of 32 filters per-view at any one time. While
+ entering the regular expression at the command-prompt, the
+ matches in the current text view will be highlighted in red
+ after a short delay.
+
+ disable-filter <regex>
+ Disable an active 'filter-in' or 'filter-out'
+ expression.
+
+ enable-filter <regex>
+ Enable a inactive 'filter-in' or 'filter-out'
+ expression.
+
+ delete-filter <regex>
+ Permanently delete a filter.
+
+ set-min-log-level <level>
+ Set the minimum level to display in the log view.
+ You can use TAB to view the possible values.
+
+ disable-word-wrap Disable word wrapping in the log and text file views.
+ enable-word-wrap Enable word wrapping in the log and text file views.
+
+ open <filename>[:<line>]
+ Open the given file within lnav and, if it is a
+ text file, switch to the text view and jump to
+ the given line number.
+
+ close Close the current text file or log file. You can also
+ close the current file by pressing 'X'.
+
+ spectrogram <field-name>
+ Generate a spectrogram for a numeric log message field or
+ SQL result column. The spectrogram view displays the range
+ of possible values of the field on the horizontal axis and
+ time on the vertical axis. The horizontal axis is split
+ into buckets where each bucket counts how many log messages
+ contained the field with a value in that range. The buckets
+ are colored based on the count in the bucket: green means
+ low, yellow means medium, and red means high. The exact
+ ranges for the colors is computed automatically and
+ displayed in the middle of the top line of the view. The
+ minimum and maximum values for the field are displayed in
+ the top left and right sides of the view, respectively.
+
+ append-to <file> Append any marked lines to the given file.
+
+ write-to <file> Write any marked lines to the given file. Use '-' to
+ write the lines to the terminal.
+
+ write-csv-to <file>
+ Write the results of a SQL query to a CSV-formatted file.
+ When running in non-interactive mode, a dash can be used
+ to write to standard out. Use '-' to write the data to
+ the terminal.
+
+ write-json-to <file>
+ Write the results of a SQL query to a JSON-formatted file.
+ The contents of the file will be an array of objects with
+ each column in the query being a field in the objects.
+ When running in non-interactive mode, a dash can be used
+ to write to standard out. Use '-' to write the data to
+ the terminal.
+
+ write-jsonlines-to <file>
+ Write the results of a SQL query to a JSON-Lines-formatted
+ file. Each row in the result set will be a single line of
+ JSON in the output and each column will be a property in
+ that object. Use '-' to write the data to the terminal.
+
+ pipe-to <shell-cmd>
+ Send the currently marked lines to the given shell command
+ for processing and open the resulting file for viewing.
+
+ pipe-line-to <shell-cmd>
+ Send the top log message to the given shell command
+ for processing and open the resulting file for viewing.
+ The known and discovered message fields are available as
+ environment variables. For example, log_procname in a
+ syslog message.
+
+ redirect-to <path>
+ If a path is given, all output from commands, like
+ ":echo" and when writing to stdout (e.g. :write-to -), will
+ be sent to the given file. If no path is specified, the
+ current redirect will be cleared and output will be
+ captured as it was before the redirect was done.
+
+ session <cmd> Add the given command to the session file
+ (\~/.lnav/session). Any commands listed in the session file
+ are executed on startup. Only the highlight, word-wrap, and
+ filter-related commands can be added to the session file.
+
+ create-logline-table <table-name>
+ Create an SQL table using the top line of the log view
+ as a template. See the "SQL QUERIES" and "DYNAMIC LOG
+ LINE TABLE" sections below for more information.
+
+ delete-logline-table <table-name>
+ Delete an SQL table created by the 'create-logline-table'
+ command.
+
+ create-search-table <table-name> [<regex>]
+ Create an SQL table that extracts information from logs
+ using the provided regular expression or the last search
+ that was done. Any captures in the expression will be
+ used as columns in the SQL table. If the capture is named,
+ that name will be used as the column name, otherwise the
+ column name will be of the form 'col_N'.
+
+ delete-search-table <table-name>
+ Delete a table that was created with create-search-table.
+
+ switch-to-view <view-name>
+ Switch the display to the given view, which can be one of:
+ help, log, text, histogram, db, and schema.
+
+ zoom-to <zoom-level>
+ Change the histogram zoom level to the given value, which
+ can be one of: day, 4-hour, hour, 10-minute, minute
+
+ redraw Force redraw the window.
+
+ partition-name <name>
+ Mark the top line in the log view as the start of a new
+ partition with the given name. The current partition name
+ will be reflected in the top status bar next to the current
+ time as well as being available in the 'log_part' column
+ of the SQL log tables. Partitions can be used to make it
+ easier to query subsections of log messages.
+
+ clear-partition
+ Clear the partition the top line is a part of.
+
+ echo [-n] <msg> Display the given message. Useful for scripts to message
+ the user. The '-n' option leaves out the new line at the
+ end of the message.
+
+ eval <cmd|query|file>
+ Execute the given command, query, or file after doing
+ environment variable substitution. The argument to this
+ command should start with a ':', ';', or '|' signify the
+ type of action to perform (command, SQL query, execute
+ script).
+
+ pt-min-time [<date>|<relative-time>]
+ Set/get the minimum time range for any papertrail queries.
+ Absolute or relative time values can be specified.
+
+ pt-max-time [<date>|<relative-time>]
+ Set/get the maximum time range for any papertrail queries.
+ Absolute or relative time values can be specified.
+
+ config <option> [value]
+ Set/get the value of a configuration option.
+
+ reset-config <option>
+ Reset a configuration option to the default value. Use
+ '*' to reset all options.
+
+ quit Quit lnav.
+
+SQL QUERIES (experimental)
+===========
+
+Lnav has support for performing SQL queries on log files using the
+Sqlite3 "virtual" table feature. For all supported log file types,
+lnav will create tables that can be queried using the subset of SQL
+that is supported by Sqlite3. For example, to get the top ten URLs
+being accessed in any loaded Apache log files, you can execute:
+
+ ;select cs_uri_stem, count(*) as total from access_log
+ group by cs_uri_stem order by total desc limit 10;
+
+The query result view shows the results and graphs any numeric
+values found in the result, much like the histogram view.
+
+The builtin set of log tables are listed below. Note that only the
+log messages that match a particular format can be queried by a
+particular table. You can find the file format and table name for
+the top log message by looking in the upper right hand corner of the
+log file view.
+
+Some commonly used format tables are:
+
+ access_log Apache common access log format
+ syslog_log Syslog format
+ strace_log Strace log format
+ generic_log 'Generic' log format. This table contains messages
+ from files that have a very simple format with a
+ leading timestamp followed by the message.
+
+NOTE: You can get a dump of the schema for the internal tables, and
+any attached databases, by running the '.schema' SQL command.
+
+The columns available for the top log line in the view will
+automatically be displayed after pressing the semicolon (;) key.
+All log tables contain at least the following columns:
+
+ log_line The line number in the file, starting at zero.
+ log_part The name of the partition. You can change this
+ column using an UPDATE SQL statement or with the
+ 'partition-name' command. After a value is set,
+ the following log messages will have the same
+ partition name up until another name is set.
+ log_time The time of the log entry.
+ log_idle_msecs The amount of time, in milliseconds, between the
+ current log message and the previous one.
+ log_level The log level (e.g. info, error, etc...).
+ log_mark The bookmark status for the line. This column
+ can be written to using an UPDATE query.
+ log_path The full path to the file.
+ log_text The raw line of text. Note that this column is
+ not included in the result of a 'select *', but
+ it does exist.
+
+The following tables include the basic columns as listed above and
+include a few more columns since the log file format is more
+structured.
+
+ syslog_log
+
+ log_hostname The hostname the message was received from.
+ log_procname The name of the process that sent the message.
+ log_pid The process ID of the process that sent the message.
+
+ access_log (The column names are the same as those in the
+ Microsoft LogParser tool.)
+
+ c_ip The client IP address.
+ cs_username The client user name.
+ cs_method The HTTP method.
+ cs_uri_stem The stem portion of the URI.
+ cs_uri_query The query portion of the URI.
+ cs_version The HTTP version string.
+ sc_status The status number returned to the client.
+ sc_bytes The number of bytes sent to the client.
+ cs_referrer The URL of the referring page.
+ cs_user_agent The user agent string.
+
+ strace_log (Currently, you need to run strace with the
+ "-tt -T" options so there are timestamps for
+ each function call.)
+
+ funcname The name of the syscall.
+ result The result code.
+ duration The amount of time spent in the syscall.
+ arg0 - arg9 The arguments passed to the syscall.
+
+These tables are created dynamically and not stored in memory or on
+disk. If you would like to persist some information from the tables,
+you can attach another database and create tables in that database.
+For example, if you wanted to save the results from the earlier
+example of a top ten query into the "/tmp/topten.db" file, you can do:
+
+ ;attach database "/tmp/topten.db" as topten;
+ ;create table topten.foo as select cs_uri_stem, count(*) as total
+ from access_log group by cs_uri_stem order by total desc
+ limit 10;
+
+
+DYNAMIC LOG LINE TABLE (experimental)
+======================
+
+(NOTE: This feature is still very new and not completely reliable yet,
+ use with care.)
+
+For log formats that lack message structure, lnav can parse the log
+message and attempt to extract any data fields that it finds. This
+feature is available through the "logline" log table. This table is
+dynamically created and defined based on the message at the top of
+the log view. For example, given the following log message from "sudo",
+lnav will create the "logline" table with columns for "TTY", "PWD",
+"USER", and "COMMAND":
+
+ May 24 06:48:38 Tim-Stacks-iMac.local sudo[76387]: stack : TTY=ttys003 ;
+ PWD=/Users/stack/github/lbuild ; USER=root ;
+ COMMAND=/bin/echo Hello, World!
+
+Queries executed against this table will then only return results for
+other log messages that have the same format. So, if you were to
+execute the following query while viewing the above line, you might
+get the following results:
+
+ ;select USER,COMMAND from logline;
+
+ USER | COMMAND
+ ---- | -------------------------
+ root | /bin/echo Hello, World!
+ mal | /bin/echo Goodbye, World!
+
+
+The log parser works by examining each message for key/value pairs
+separated by an equal sign (=) or a colon (:). For example, in the
+previous example of a "sudo" message, the parser sees the "USER=root"
+string as a pair where the key is "USER" and the value is "root".
+If no pairs can be found, then anything that looks like a value is
+extracted and assigned a numbered column. For example, the following
+line is from "dhcpd":
+
+ Sep 16 22:35:57 drill dhcpd: DHCPDISCOVER from 00:16:ce:54:4e:f3 via hme3
+
+In this case, the lnav parser recognizes that "DHCPDISCOVER", the MAC
+address and the "hme3" device name are values and not normal words. So,
+it builds a table with three columns for each of these values. The
+regular words in the message, like "from" and "via", are then used to
+find other messages with a similar format.
+
+If you would like to execute queries against log messages of different
+formats at the same time, you can use the 'create-logline-table' command
+to permanently create a table using the top line of the log view as a
+template.
+
+
+OTHER SQL FEATURES
+==================
+
+Environment variables can be used in SQL statements by prefixing the
+variable name with a dollar-sign (\$). For example, to read the value of
+the HOME variable, you can do:
+
+ ;SELECT \$HOME;
+
+To select the syslog messages that have a hostname field that is equal
+to the HOSTNAME variable:
+
+ ;SELECT * FROM syslog_log WHERE log_hostname = \$HOSTNAME;
+
+NOTE: Variable substitution is done for fields in the query and is not
+a plain text substitution. For example, the following statement
+WILL NOT WORK:
+
+ ;SELECT * FROM \$TABLE_NAME; -- Syntax error
+
+
+Access to lnav's environment variables is also available via the "environ"
+table. The table has two columns (name, value) and can be read and written
+to using SQL SELECT, INSERT, UPDATE, and DELETE statements. For example,
+to set the "FOO" variable to the value "BAR":
+
+ ;INSERT INTO environ SELECT 'FOO', 'BAR';
+
+As a more complex example, you can set the variable "LAST" to the last
+syslog line number by doing:
+
+ ;INSERT INTO environ SELECT 'LAST', (SELECT max(log_line) FROM syslog_log);
+
+A delete will unset the environment variable:
+
+ ;DELETE FROM environ WHERE name='LAST';
+
+The table allows you to easily use the results of a SQL query in lnav
+commands, which is especially useful when scripting lnav.
+
+
+PAPERTRAIL INTEGRATION
+======================
+
+Papertrail is a log management service with free and paid plans at:
+
+ http://papertrailapp.com
+
+To configure lnav to communicate with the papertrail service, you will
+need to set the PAPERTRAIL_API_TOKEN environment variable. You can
+get your API token from your user profile, available here:
+
+ https://papertrailapp.com/user/edit
+
+Searching papertrail using lnav can be done by prefixing the search terms
+with "pt:" and passing the value as a file name. For example, to search
+for log messages with the string 'Critical Error' when starting lnav you
+can do the following:
+
+ \$ setenv PAPERTRAIL_API_TOKEN xxxxxxxxx
+ \$ lnav "pt:'Critical Error'"
+
+If lnav is already started, you can use the ':open' command like so:
+
+ :open pt:'Critical Error'
+
+If you just want to tail your logs in papertrail, you can pass an empty
+search string (i.e. "pt:").
+
+Only one papertrail search can be active at a time. So, if an ':open'
+is done with a new query, the previous query will be closed first.
+
+
+CONTACT
+=======
+
+For more information, visit the lnav website at:
+
+ http://lnav.org
+
+For support questions, email:
+
+ lnav@googlegroups.com
+
+
+REFERENCE
+=========
diff --git a/src/help_text.cc b/src/help_text.cc
new file mode 100644
index 0000000..a409979
--- /dev/null
+++ b/src/help_text.cc
@@ -0,0 +1,105 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "help_text.hh"
+
+#include "config.h"
+
+help_text&
+help_text::with_parameters(
+ const std::initializer_list<help_text>& params) noexcept
+{
+ this->ht_parameters = params;
+ for (auto& param : this->ht_parameters) {
+ param.ht_context = help_context_t::HC_PARAMETER;
+ }
+ return *this;
+}
+
+help_text&
+help_text::with_parameter(const help_text& ht) noexcept
+{
+ this->ht_parameters.emplace_back(ht);
+ this->ht_parameters.back().ht_context = help_context_t::HC_PARAMETER;
+ return *this;
+}
+
+help_text&
+help_text::with_result(const help_text& ht) noexcept
+{
+ this->ht_results.emplace_back(ht);
+ this->ht_results.back().ht_context = help_context_t::HC_RESULT;
+ return *this;
+}
+
+help_text&
+help_text::with_examples(
+ const std::initializer_list<help_example>& examples) noexcept
+{
+ this->ht_example = examples;
+ return *this;
+}
+
+help_text&
+help_text::with_example(const help_example& example) noexcept
+{
+ this->ht_example.emplace_back(example);
+ return *this;
+}
+
+help_text&
+help_text::with_enum_values(
+ const std::initializer_list<const char*>& enum_values) noexcept
+{
+ this->ht_enum_values = enum_values;
+ return *this;
+}
+
+help_text&
+help_text::with_tags(const std::initializer_list<const char*>& tags) noexcept
+{
+ this->ht_tags = tags;
+ return *this;
+}
+
+help_text&
+help_text::with_opposites(
+ const std::initializer_list<const char*>& opps) noexcept
+{
+ this->ht_opposites = opps;
+ return *this;
+}
+
+void
+help_text::index_tags()
+{
+ for (const auto& tag : this->ht_tags) {
+ TAGGED.insert(std::make_pair(tag, this));
+ }
+}
diff --git a/src/help_text.hh b/src/help_text.hh
new file mode 100644
index 0000000..81dc8fb
--- /dev/null
+++ b/src/help_text.hh
@@ -0,0 +1,225 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LNAV_HELP_TEXT_HH
+#define LNAV_HELP_TEXT_HH
+
+#include <map>
+#include <string>
+#include <vector>
+
+enum class help_context_t {
+ HC_NONE,
+ HC_PARAMETER,
+ HC_RESULT,
+ HC_COMMAND,
+ HC_SQL_COMMAND,
+ HC_SQL_KEYWORD,
+ HC_SQL_INFIX,
+ HC_SQL_FUNCTION,
+ HC_SQL_TABLE_VALUED_FUNCTION,
+};
+
+enum class help_function_type_t {
+ HFT_REGULAR,
+ HFT_AGGREGATE,
+};
+
+enum class help_nargs_t {
+ HN_REQUIRED,
+ HN_OPTIONAL,
+ HN_ZERO_OR_MORE,
+ HN_ONE_OR_MORE,
+};
+
+enum class help_parameter_format_t {
+ HPF_STRING,
+ HPF_REGEX,
+ HPF_INTEGER,
+ HPF_NUMBER,
+ HPF_DATETIME,
+ HPF_ENUM,
+};
+
+struct help_example {
+ const char* he_description{nullptr};
+ const char* he_cmd{nullptr};
+};
+
+struct help_text {
+ help_context_t ht_context{help_context_t::HC_NONE};
+ const char* ht_name{nullptr};
+ const char* ht_summary{nullptr};
+ const char* ht_flag_name{nullptr};
+ const char* ht_group_start{nullptr};
+ const char* ht_group_end{nullptr};
+ const char* ht_description{nullptr};
+ std::vector<struct help_text> ht_parameters;
+ std::vector<struct help_text> ht_results;
+ std::vector<struct help_example> ht_example;
+ help_nargs_t ht_nargs{help_nargs_t::HN_REQUIRED};
+ help_parameter_format_t ht_format{help_parameter_format_t::HPF_STRING};
+ std::vector<const char*> ht_enum_values;
+ std::vector<const char*> ht_tags;
+ std::vector<const char*> ht_opposites;
+ help_function_type_t ht_function_type{help_function_type_t::HFT_REGULAR};
+ void* ht_impl{nullptr};
+
+ help_text() = default;
+
+ help_text(const char* name, const char* summary = nullptr) noexcept
+ : ht_name(name), ht_summary(summary)
+ {
+ if (name[0] == ':') {
+ this->ht_context = help_context_t::HC_COMMAND;
+ this->ht_name = &name[1];
+ }
+ }
+
+ help_text& command() noexcept
+ {
+ this->ht_context = help_context_t::HC_COMMAND;
+ return *this;
+ }
+
+ help_text& sql_function() noexcept
+ {
+ this->ht_context = help_context_t::HC_SQL_FUNCTION;
+ return *this;
+ }
+
+ help_text& sql_agg_function() noexcept
+ {
+ this->ht_context = help_context_t::HC_SQL_FUNCTION;
+ this->ht_function_type = help_function_type_t::HFT_AGGREGATE;
+ return *this;
+ }
+
+ help_text& sql_table_valued_function() noexcept
+ {
+ this->ht_context = help_context_t::HC_SQL_TABLE_VALUED_FUNCTION;
+ return *this;
+ }
+
+ help_text& sql_command() noexcept
+ {
+ this->ht_context = help_context_t::HC_SQL_COMMAND;
+ return *this;
+ }
+
+ help_text& sql_keyword() noexcept
+ {
+ this->ht_context = help_context_t::HC_SQL_KEYWORD;
+ return *this;
+ }
+
+ help_text& sql_infix() noexcept
+ {
+ this->ht_context = help_context_t::HC_SQL_INFIX;
+ return *this;
+ }
+
+ help_text& with_summary(const char* summary) noexcept
+ {
+ this->ht_summary = summary;
+ return *this;
+ }
+
+ help_text& with_flag_name(const char* flag) noexcept
+ {
+ this->ht_flag_name = flag;
+ return *this;
+ }
+
+ help_text& with_grouping(const char* group_start,
+ const char* group_end) noexcept
+ {
+ this->ht_group_start = group_start;
+ this->ht_group_end = group_end;
+ return *this;
+ }
+
+ help_text& with_parameters(
+ const std::initializer_list<help_text>& params) noexcept;
+
+ help_text& with_parameter(const help_text& ht) noexcept;
+
+ help_text& with_result(const help_text& ht) noexcept;
+
+ help_text& with_examples(
+ const std::initializer_list<help_example>& examples) noexcept;
+
+ help_text& with_example(const help_example& example) noexcept;
+
+ help_text& optional() noexcept
+ {
+ this->ht_nargs = help_nargs_t::HN_OPTIONAL;
+ return *this;
+ }
+
+ help_text& zero_or_more() noexcept
+ {
+ this->ht_nargs = help_nargs_t::HN_ZERO_OR_MORE;
+ return *this;
+ }
+
+ help_text& one_or_more() noexcept
+ {
+ this->ht_nargs = help_nargs_t::HN_ONE_OR_MORE;
+ return *this;
+ }
+
+ help_text& with_format(help_parameter_format_t format) noexcept
+ {
+ this->ht_format = format;
+ return *this;
+ }
+
+ help_text& with_enum_values(
+ const std::initializer_list<const char*>& enum_values) noexcept;
+
+ help_text& with_tags(
+ const std::initializer_list<const char*>& tags) noexcept;
+
+ help_text& with_opposites(
+ const std::initializer_list<const char*>& opps) noexcept;
+
+ template<typename F>
+ help_text& with_impl(F impl)
+ {
+ this->ht_impl = (void*) impl;
+ return *this;
+ }
+
+ void index_tags();
+
+ static std::multimap<std::string, help_text*> TAGGED;
+};
+
+#endif
diff --git a/src/help_text_formatter.cc b/src/help_text_formatter.cc
new file mode 100644
index 0000000..e0b92a0
--- /dev/null
+++ b/src/help_text_formatter.cc
@@ -0,0 +1,691 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+#include <regex>
+
+#include "help_text_formatter.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/attr_line.builder.hh"
+#include "base/string_util.hh"
+#include "config.h"
+#include "fmt/format.h"
+#include "fmt/printf.h"
+#include "readline_highlighters.hh"
+
+using namespace lnav::roles::literals;
+
+std::multimap<std::string, help_text*> help_text::TAGGED;
+
+static std::vector<help_text*>
+get_related(const help_text& ht)
+{
+ std::vector<help_text*> retval;
+
+ for (const auto& tag : ht.ht_tags) {
+ auto tagged = help_text::TAGGED.equal_range(tag);
+
+ for (auto tag_iter = tagged.first; tag_iter != tagged.second;
+ ++tag_iter) {
+ if (tag_iter->second == &ht) {
+ continue;
+ }
+
+ help_text& related = *tag_iter->second;
+
+ if (!related.ht_opposites.empty()
+ && find_if(related.ht_opposites.begin(),
+ related.ht_opposites.end(),
+ [&ht](const char* x) {
+ return strcmp(x, ht.ht_name) == 0;
+ })
+ == related.ht_opposites.end())
+ {
+ continue;
+ }
+
+ retval.push_back(&related);
+ }
+ }
+
+ return retval;
+}
+
+void
+format_help_text_for_term(const help_text& ht,
+ size_t width,
+ attr_line_t& out,
+ help_text_content htc)
+{
+ static const size_t body_indent = 2;
+
+ attr_line_builder alb(out);
+ text_wrap_settings tws;
+ size_t start_index = out.get_string().length();
+
+ tws.with_width(width);
+
+ switch (ht.ht_context) {
+ case help_context_t::HC_COMMAND: {
+ auto line_start = out.al_string.length();
+
+ out.append(":").append(lnav::roles::symbol(ht.ht_name));
+ for (const auto& param : ht.ht_parameters) {
+ out.append(" ");
+ if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) {
+ out.append("[");
+ }
+ out.append(lnav::roles::variable(param.ht_name));
+ if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) {
+ out.append("]");
+ }
+ if (param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) {
+ out.append("1"_variable);
+ out.append(" [");
+ out.append("..."_variable);
+ out.append(" ");
+ out.append(lnav::roles::variable(param.ht_name));
+ out.append("N"_variable);
+ out.append("]");
+ }
+ }
+ out.with_attr(string_attr{
+ line_range{(int) line_start, (int) out.get_string().length()},
+ VC_ROLE.value(role_t::VCR_H3),
+ });
+ if (htc != help_text_content::synopsis) {
+ alb.append("\n")
+ .append(lnav::roles::table_border(
+ repeat("\u2550", tws.tws_width)))
+ .append("\n")
+ .indent(body_indent)
+ .append(attr_line_t::from_ansi_str(ht.ht_summary),
+ &tws.with_indent(body_indent))
+ .append("\n");
+ }
+ break;
+ }
+ case help_context_t::HC_SQL_FUNCTION:
+ case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION: {
+ auto line_start = out.al_string.length();
+ bool break_all = false;
+ bool needs_comma = false;
+
+ out.append(lnav::roles::symbol(ht.ht_name)).append("(");
+ for (const auto& param : ht.ht_parameters) {
+ if (!param.ht_flag_name && needs_comma) {
+ out.append(", ");
+ }
+ if (break_all
+ || (int) (out.get_string().length() - line_start + 10)
+ >= tws.tws_width)
+ {
+ out.append("\n");
+ line_start = out.get_string().length();
+ alb.indent(body_indent + strlen(ht.ht_name) + 1);
+ break_all = true;
+ }
+ if (param.ht_flag_name) {
+ out.append(" ")
+ .append(lnav::roles::symbol(param.ht_flag_name))
+ .append(" ");
+ }
+ if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) {
+ out.append("[");
+ }
+ out.append(lnav::roles::variable(param.ht_name));
+ if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) {
+ out.append("]");
+ }
+ if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE
+ || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE)
+ {
+ out.append(", ...");
+ }
+ needs_comma = true;
+ }
+ out.append(")");
+ out.with_attr(string_attr{
+ line_range{(int) line_start, (int) out.get_string().length()},
+ VC_ROLE.value(role_t::VCR_H3),
+ });
+ if (htc != help_text_content::synopsis) {
+ if (break_all) {
+ alb.append("\n")
+ .append(lnav::roles::table_border(
+ repeat("\u2550", tws.tws_width)))
+ .append("\n")
+ .indent(body_indent + strlen(ht.ht_name) + 1);
+ } else {
+ alb.append("\n")
+ .append(lnav::roles::table_border(
+ repeat("\u2550", tws.tws_width)))
+ .append("\n")
+ .indent(body_indent);
+ }
+ out.append(attr_line_t::from_ansi_str(ht.ht_summary),
+ &tws.with_indent(body_indent))
+ .append("\n");
+ }
+ break;
+ }
+ case help_context_t::HC_SQL_COMMAND: {
+ auto line_start = out.al_string.length();
+
+ out.append(";").append(lnav::roles::symbol(ht.ht_name));
+ for (const auto& param : ht.ht_parameters) {
+ out.append(" ");
+ if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) {
+ out.append("[");
+ }
+ out.append(lnav::roles::variable(param.ht_name));
+ if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) {
+ out.append("]");
+ }
+ if (param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) {
+ out.append("1"_variable);
+ out.append(" [");
+ out.append("..."_variable);
+ out.append(" ");
+ out.append(lnav::roles::variable(param.ht_name));
+ out.append("N"_variable);
+ out.append("]");
+ }
+ }
+ out.with_attr(string_attr{
+ line_range{(int) line_start, (int) out.get_string().length()},
+ VC_ROLE.value(role_t::VCR_H3),
+ });
+ if (htc != help_text_content::synopsis) {
+ alb.append("\n")
+ .append(lnav::roles::table_border(
+ repeat("\u2550", tws.tws_width)))
+ .append("\n")
+ .indent(body_indent)
+ .append(attr_line_t::from_ansi_str(ht.ht_summary),
+ &tws.with_indent(body_indent + 2))
+ .append("\n");
+ }
+ break;
+ }
+ case help_context_t::HC_SQL_INFIX:
+ case help_context_t::HC_SQL_KEYWORD: {
+ size_t line_start = out.get_string().length();
+ bool break_all = false;
+ bool is_infix = ht.ht_context == help_context_t::HC_SQL_INFIX;
+
+ if (is_infix) {
+ out.append(ht.ht_name);
+ } else {
+ out.append(lnav::roles::keyword(ht.ht_name));
+ }
+ for (const auto& param : ht.ht_parameters) {
+ if (break_all
+ || (int) (out.get_string().length() - start_index
+ - line_start + 10)
+ >= tws.tws_width)
+ {
+ out.append("\n");
+ line_start = out.get_string().length();
+ alb.indent(body_indent + strlen(ht.ht_name) + 1);
+ break_all = true;
+ }
+ if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE
+ || param.ht_nargs == help_nargs_t::HN_OPTIONAL)
+ {
+ if (!break_all) {
+ out.append(" ");
+ }
+ out.append("[");
+ }
+ if (param.ht_flag_name) {
+ out.ensure_space().append(
+ lnav::roles::keyword(param.ht_flag_name));
+ }
+ if (param.ht_group_start) {
+ out.ensure_space().append(
+ lnav::roles::keyword(param.ht_group_start));
+ }
+ if (param.ht_name[0]) {
+ out.ensure_space().append(
+ lnav::roles::variable(param.ht_name));
+ if (!param.ht_parameters.empty()) {
+ if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE
+ || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE)
+ {
+ out.append("1"_variable);
+ }
+ if (param.ht_parameters[0].ht_flag_name) {
+ out.append(" ")
+ .append(lnav::roles::keyword(
+ param.ht_parameters[0].ht_flag_name))
+ .append(" ");
+ }
+ out.append(lnav::roles::variable(
+ param.ht_parameters[0].ht_name));
+ }
+ }
+ if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE
+ || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE)
+ {
+ bool needs_comma = param.ht_parameters.empty()
+ || !param.ht_flag_name;
+
+ out.append("1"_variable)
+ .append(" [")
+ .append(needs_comma ? ", " : "")
+ .append("...")
+ .append(needs_comma ? "" : " ")
+ .append(lnav::roles::keyword(
+ (needs_comma || !param.ht_flag_name)
+ ? ""
+ : param.ht_flag_name))
+ .append(" ")
+ .append(lnav::roles::variable(param.ht_name))
+ .append("N"_variable);
+ if (!param.ht_parameters.empty()) {
+ if (param.ht_parameters[0].ht_flag_name) {
+ out.append(" ")
+ .append(lnav::roles::keyword(
+ param.ht_parameters[0].ht_flag_name))
+ .append(" ");
+ }
+
+ out.append(lnav::roles::variable(
+ param.ht_parameters[0].ht_name))
+ .append("N"_variable);
+ }
+ out.append("]");
+ }
+ if (param.ht_group_end) {
+ out.ensure_space().append(
+ lnav::roles::keyword(param.ht_group_end));
+ }
+ if (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE
+ || param.ht_nargs == help_nargs_t::HN_OPTIONAL)
+ {
+ out.append("]");
+ }
+ }
+ out.with_attr(string_attr{
+ line_range{(int) line_start, (int) out.get_string().length()},
+ VC_ROLE.value(role_t::VCR_H3),
+ });
+ if (htc != help_text_content::synopsis) {
+ alb.append("\n")
+ .append(lnav::roles::table_border(
+ repeat("\u2550", tws.tws_width)))
+ .append("\n")
+ .indent(body_indent)
+ .append(ht.ht_summary, &tws)
+ .append("\n");
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (htc == help_text_content::full && !ht.ht_parameters.empty()) {
+ size_t max_param_name_width = 0;
+
+ for (const auto& param : ht.ht_parameters) {
+ max_param_name_width
+ = std::max(strlen(param.ht_name), max_param_name_width);
+ }
+
+ out.append(ht.ht_parameters.size() == 1 ? "Parameter"_h4
+ : "Parameters"_h4)
+ .append("\n");
+
+ for (const auto& param : ht.ht_parameters) {
+ if (!param.ht_summary) {
+ continue;
+ }
+
+ alb.indent(body_indent)
+ .append(lnav::roles::variable(param.ht_name))
+ .append(max_param_name_width - strlen(param.ht_name), ' ')
+ .append(" ")
+ .append(attr_line_t::from_ansi_str(param.ht_summary),
+ &(tws.with_indent(2 + max_param_name_width + 3)))
+ .append("\n");
+ }
+ }
+ if (htc == help_text_content::full && !ht.ht_results.empty()) {
+ size_t max_result_name_width = 0;
+
+ for (const auto& result : ht.ht_results) {
+ max_result_name_width
+ = std::max(strlen(result.ht_name), max_result_name_width);
+ }
+
+ out.append(ht.ht_results.size() == 1 ? "Result"_h4 : "Results"_h4)
+ .append("\n");
+
+ for (const auto& result : ht.ht_results) {
+ if (!result.ht_summary) {
+ continue;
+ }
+
+ alb.indent(body_indent)
+ .append(lnav::roles::variable(result.ht_name))
+ .append(max_result_name_width - strlen(result.ht_name), ' ')
+ .append(" ")
+ .append(attr_line_t::from_ansi_str(result.ht_summary),
+ &(tws.with_indent(2 + max_result_name_width + 3)))
+ .append("\n");
+ }
+ }
+ if (htc == help_text_content::full && !ht.ht_tags.empty()) {
+ auto related_help = get_related(ht);
+ auto related_refs = std::vector<std::string>();
+
+ for (const auto* related : related_help) {
+ std::string name = related->ht_name;
+ switch (related->ht_context) {
+ case help_context_t::HC_COMMAND:
+ name = ":" + name;
+ break;
+ case help_context_t::HC_SQL_FUNCTION:
+ case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION:
+ name = name + "()";
+ break;
+ default:
+ break;
+ }
+ related_refs.push_back(name);
+ }
+ stable_sort(related_refs.begin(), related_refs.end());
+
+ alb.append("See Also"_h4).append("\n").indent(body_indent);
+
+ bool first = true;
+ size_t line_start = out.get_string().length();
+ for (const auto& ref : related_refs) {
+ if (!first) {
+ out.append(", ");
+ }
+ if ((out.get_string().length() - line_start + ref.length()) > width)
+ {
+ alb.append("\n").indent(body_indent);
+ line_start = out.get_string().length();
+ }
+ out.append(lnav::roles::symbol(ref));
+ first = false;
+ }
+ }
+}
+
+void
+format_example_text_for_term(const help_text& ht,
+ const help_example_to_attr_line_fun_t eval,
+ size_t width,
+ attr_line_t& out)
+{
+ if (ht.ht_example.empty()) {
+ return;
+ }
+
+ attr_line_builder alb(out);
+ int count = 1;
+
+ out.append(ht.ht_example.size() == 1 ? "Example"_h4 : "Examples"_h4)
+ .append("\n");
+ for (const auto& ex : ht.ht_example) {
+ attr_line_t ex_line(ex.he_cmd);
+ const char* prompt = "";
+ text_wrap_settings tws;
+
+ tws.with_width(width);
+ if (count > 1) {
+ out.append("\n");
+ }
+ switch (ht.ht_context) {
+ case help_context_t::HC_COMMAND:
+ ex_line.insert(0, 1, ' ');
+ ex_line.insert(0, 1, ':');
+ ex_line.insert(1, ht.ht_name);
+ readline_command_highlighter(ex_line, 0);
+ break;
+ case help_context_t::HC_SQL_INFIX:
+ case help_context_t::HC_SQL_KEYWORD:
+ case help_context_t::HC_SQL_FUNCTION:
+ case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION:
+ readline_sqlite_highlighter(ex_line, 0);
+ prompt = ";";
+ break;
+ default:
+ break;
+ }
+
+ ex_line.pad_to(50).with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ alb.append("#")
+ .append(fmt::to_string(count))
+ .append(" ")
+ .append(ex.he_description, &tws.with_indent(3))
+ .append(":\n")
+ .indent(3)
+ .append(prompt, VC_ROLE.value(role_t::VCR_QUOTED_CODE))
+ .append(ex_line, &tws.with_indent(3).with_padding_indent(3))
+ .append("\n")
+ .indent(3)
+ .append(eval(ht, ex), &tws.with_indent(3))
+ .append("\n");
+
+ count += 1;
+ }
+}
+
+static std::string
+link_name(const help_text& ht)
+{
+ const static std::regex SCRUBBER("[^\\w_]");
+
+ bool is_sql_infix = ht.ht_context == help_context_t::HC_SQL_INFIX;
+ std::string scrubbed_name;
+
+ if (is_sql_infix) {
+ scrubbed_name = "infix";
+ } else {
+ scrubbed_name = ht.ht_name;
+ }
+ if (ht.ht_function_type == help_function_type_t::HFT_AGGREGATE) {
+ scrubbed_name += "_agg";
+ }
+ for (const auto& param : ht.ht_parameters) {
+ if (!is_sql_infix && param.ht_name[0]) {
+ continue;
+ }
+ if (!param.ht_flag_name) {
+ continue;
+ }
+
+ scrubbed_name += "_";
+ scrubbed_name += param.ht_flag_name;
+ }
+ scrubbed_name = std::regex_replace(scrubbed_name, SCRUBBER, "_");
+
+ return tolower(scrubbed_name);
+}
+
+void
+format_help_text_for_rst(const help_text& ht,
+ const help_example_to_attr_line_fun_t eval,
+ FILE* rst_file)
+{
+ const char* prefix;
+ int out_count = 0;
+
+ if (!ht.ht_name || !ht.ht_name[0]) {
+ return;
+ }
+
+ bool is_sql_func = false, is_sql = false;
+ switch (ht.ht_context) {
+ case help_context_t::HC_COMMAND:
+ prefix = ":";
+ break;
+ case help_context_t::HC_SQL_COMMAND:
+ prefix = ";";
+ break;
+ case help_context_t::HC_SQL_FUNCTION:
+ case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION:
+ is_sql = is_sql_func = true;
+ prefix = "";
+ break;
+ case help_context_t::HC_SQL_INFIX:
+ case help_context_t::HC_SQL_KEYWORD:
+ is_sql = true;
+ prefix = "";
+ break;
+ default:
+ prefix = "";
+ break;
+ }
+
+ fmt::print(rst_file, FMT_STRING("\n.. _{}:\n\n"), link_name(ht));
+ out_count += fmt::fprintf(rst_file, "%s%s", prefix, ht.ht_name);
+ if (is_sql_func) {
+ out_count += fmt::fprintf(rst_file, "(");
+ }
+ bool needs_comma = false;
+ for (const auto& param : ht.ht_parameters) {
+ if (needs_comma) {
+ if (param.ht_flag_name) {
+ out_count += fmt::fprintf(rst_file, " ");
+ } else {
+ out_count += fmt::fprintf(rst_file, ", ");
+ }
+ }
+ if (!is_sql_func) {
+ out_count += fmt::fprintf(rst_file, " ");
+ }
+
+ if (param.ht_flag_name) {
+ out_count += fmt::fprintf(rst_file, "%s ", param.ht_flag_name);
+ }
+ if (param.ht_name[0]) {
+ out_count += fmt::fprintf(rst_file, "*");
+ if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) {
+ out_count += fmt::fprintf(rst_file, "\\[");
+ }
+ out_count += fmt::fprintf(rst_file, "%s", param.ht_name);
+ if (param.ht_nargs == help_nargs_t::HN_OPTIONAL) {
+ out_count += fmt::fprintf(rst_file, "\\]");
+ }
+ out_count += fmt::fprintf(rst_file, "*");
+ }
+ if (is_sql_func) {
+ needs_comma = true;
+ }
+ }
+ if (is_sql_func) {
+ out_count += fmt::fprintf(rst_file, ")");
+ }
+ fmt::fprintf(rst_file, "\n");
+ fmt::print(rst_file, FMT_STRING("{0:^^{1}}\n\n"), "", out_count);
+
+ fmt::fprintf(rst_file, " %s\n", ht.ht_summary);
+ fmt::fprintf(rst_file, "\n");
+ if (ht.ht_description != nullptr) {
+ fmt::fprintf(rst_file, " %s\n", ht.ht_description);
+ }
+
+ int param_count = 0;
+ for (const auto& param : ht.ht_parameters) {
+ if (param.ht_summary && param.ht_summary[0]) {
+ param_count += 1;
+ }
+ }
+
+ if (param_count > 0) {
+ fmt::fprintf(rst_file, " **Parameters**\n");
+ for (const auto& param : ht.ht_parameters) {
+ if (param.ht_summary && param.ht_summary[0]) {
+ fmt::fprintf(
+ rst_file,
+ " * **%s%s** --- %s\n",
+ param.ht_name,
+ param.ht_nargs == help_nargs_t::HN_REQUIRED ? "\\*" : "",
+ param.ht_summary);
+ }
+ }
+ fmt::fprintf(rst_file, "\n");
+ }
+ if (is_sql) {
+ prefix = ";";
+ }
+ if (!ht.ht_example.empty()) {
+ fmt::fprintf(rst_file, " **Examples**\n");
+ for (const auto& example : ht.ht_example) {
+ fmt::fprintf(rst_file, " %s:\n\n", example.he_description);
+ fmt::fprintf(rst_file,
+ " .. code-block:: %s\n\n",
+ is_sql ? "custsqlite" : "lnav");
+ if (ht.ht_context == help_context_t::HC_COMMAND) {
+ fmt::fprintf(rst_file,
+ " %s%s %s\n",
+ prefix,
+ ht.ht_name,
+ example.he_cmd);
+ } else {
+ fmt::fprintf(rst_file, " %s%s\n", prefix, example.he_cmd);
+ }
+ auto result = eval(ht, example);
+ if (!result.empty()) {
+ std::vector<attr_line_t> lines;
+
+ result.split_lines(lines);
+ for (const auto& line : lines) {
+ fmt::fprintf(rst_file, " %s\n", line.get_string());
+ }
+ }
+ fmt::fprintf(rst_file, "\n");
+ }
+ }
+
+ if (!ht.ht_tags.empty()) {
+ auto related_refs = std::vector<std::string>();
+
+ for (const auto* related : get_related(ht)) {
+ related_refs.emplace_back(
+ fmt::format(FMT_STRING(":ref:`{}`"), link_name(*related)));
+ }
+ stable_sort(related_refs.begin(), related_refs.end());
+
+ fmt::print(rst_file,
+ FMT_STRING(" **See Also**\n {}\n"),
+ fmt::join(related_refs, ", "));
+ }
+
+ fmt::fprintf(rst_file, "\n----\n\n");
+}
diff --git a/src/help_text_formatter.hh b/src/help_text_formatter.hh
new file mode 100644
index 0000000..2963695
--- /dev/null
+++ b/src/help_text_formatter.hh
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LNAV_HELP_TEXT_FORMATTER_HH
+#define LNAV_HELP_TEXT_FORMATTER_HH
+
+#include <functional>
+
+#include "base/attr_line.hh"
+#include "help_text.hh"
+
+using help_example_to_attr_line_fun_t
+ = std::function<attr_line_t(const help_text&, const help_example&)>;
+
+enum class help_text_content {
+ synopsis,
+ synopsis_and_summary,
+ full,
+};
+
+void format_help_text_for_term(const help_text& ht,
+ size_t width,
+ attr_line_t& out,
+ help_text_content htc = help_text_content::full);
+
+void format_example_text_for_term(const help_text& ht,
+ help_example_to_attr_line_fun_t eval,
+ size_t width,
+ attr_line_t& out);
+
+void format_help_text_for_rst(const help_text& ht,
+ help_example_to_attr_line_fun_t eval,
+ FILE* rst_file);
+
+#endif // LNAV_HELP_TEXT_FORMATTER_HH
diff --git a/src/highlighter.cc b/src/highlighter.cc
new file mode 100644
index 0000000..fc7b455
--- /dev/null
+++ b/src/highlighter.cc
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "highlighter.hh"
+
+#include "config.h"
+
+highlighter&
+highlighter::operator=(const highlighter& other)
+{
+ if (this == &other) {
+ return *this;
+ }
+
+ this->h_name = other.h_name;
+ this->h_fg = other.h_fg;
+ this->h_bg = other.h_bg;
+ this->h_role = other.h_role;
+ this->h_regex = other.h_regex;
+ this->h_format_name = other.h_format_name;
+ this->h_attrs = other.h_attrs;
+ this->h_text_formats = other.h_text_formats;
+ this->h_nestable = other.h_nestable;
+
+ return *this;
+}
+
+void
+highlighter::annotate_capture(attr_line_t& al, const line_range& lr) const
+{
+ auto& vc = view_colors::singleton();
+ auto& sa = al.get_attrs();
+
+ if (lr.lr_end <= lr.lr_start) {
+ return;
+ }
+ if (!this->h_nestable) {
+ for (const auto& attr : sa) {
+ if (attr.sa_range.lr_end == -1) {
+ continue;
+ }
+ if (!attr.sa_range.intersects(lr)) {
+ continue;
+ }
+ if (attr.sa_type == &VC_STYLE || attr.sa_type == &VC_ROLE
+ || attr.sa_type == &VC_FOREGROUND
+ || attr.sa_type == &VC_BACKGROUND)
+ {
+ return;
+ }
+ }
+ }
+
+ if (!this->h_fg.empty()) {
+ sa.emplace_back(lr,
+ VC_FOREGROUND.value(
+ vc.match_color(this->h_fg)
+ .value_or(view_colors::MATCH_COLOR_DEFAULT)));
+ }
+ if (!this->h_bg.empty()) {
+ sa.emplace_back(lr,
+ VC_BACKGROUND.value(
+ vc.match_color(this->h_bg)
+ .value_or(view_colors::MATCH_COLOR_DEFAULT)));
+ }
+ if (this->h_role != role_t::VCR_NONE) {
+ sa.emplace_back(lr, VC_ROLE.value(this->h_role));
+ }
+ if (!this->h_attrs.empty()) {
+ sa.emplace_back(lr, VC_STYLE.value(this->h_attrs));
+ }
+}
+
+void
+highlighter::annotate(attr_line_t& al, int start) const
+{
+ if (!this->h_regex) {
+ return;
+ }
+
+ auto& vc = view_colors::singleton();
+ const auto& str = al.get_string();
+ auto& sa = al.get_attrs();
+ auto sf = string_fragment::from_str_range(
+ str, start, std::min(size_t{8192}, str.size()));
+
+ if (!sf.is_valid()) {
+ return;
+ }
+
+ this->h_regex->capture_from(sf).for_each(
+ [&](lnav::pcre2pp::match_data& md) {
+ if (md.get_count() == 1) {
+ this->annotate_capture(al, to_line_range(md[0].value()));
+ } else {
+ for (size_t lpc = 1; lpc < md.get_count(); lpc++) {
+ if (!md[lpc]) {
+ continue;
+ }
+
+ const auto* name = this->h_regex->get_name_for_capture(lpc);
+ auto lr = to_line_range(md[lpc].value());
+
+ if (name != nullptr && name[0]) {
+ auto ident_attrs = vc.attrs_for_ident(name);
+
+ ident_attrs.ta_attrs |= this->h_attrs.ta_attrs;
+ if (this->h_role != role_t::VCR_NONE) {
+ auto role_attrs = vc.attrs_for_role(this->h_role);
+
+ ident_attrs.ta_attrs |= role_attrs.ta_attrs;
+ }
+ sa.emplace_back(lr, VC_STYLE.value(ident_attrs));
+ } else {
+ this->annotate_capture(al, lr);
+ }
+ }
+ }
+ });
+}
diff --git a/src/highlighter.hh b/src/highlighter.hh
new file mode 100644
index 0000000..25e8087
--- /dev/null
+++ b/src/highlighter.hh
@@ -0,0 +1,123 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file highlighter.hh
+ */
+
+#ifndef highlighter_hh
+#define highlighter_hh
+
+#include <set>
+#include <utility>
+
+#include "optional.hpp"
+#include "pcrepp/pcre2pp.hh"
+#include "text_format.hh"
+#include "view_curses.hh"
+
+struct highlighter {
+ highlighter() = default;
+
+ explicit highlighter(const std::shared_ptr<lnav::pcre2pp::code>& regex)
+ : h_regex(regex)
+ {
+ }
+
+ highlighter(const highlighter& other) = default;
+
+ highlighter& operator=(const highlighter& other);
+
+ virtual ~highlighter() = default;
+
+ highlighter& with_role(role_t role)
+ {
+ this->h_role = role;
+
+ return *this;
+ }
+
+ highlighter& with_attrs(text_attrs attrs)
+ {
+ this->h_attrs = attrs;
+
+ return *this;
+ }
+
+ highlighter& with_text_format(text_format_t tf)
+ {
+ this->h_text_formats.insert(tf);
+
+ return *this;
+ }
+
+ highlighter& with_format_name(intern_string_t name)
+ {
+ this->h_format_name = name;
+
+ return *this;
+ }
+
+ highlighter& with_color(const styling::color_unit& fg,
+ const styling::color_unit& bg)
+ {
+ this->h_fg = fg;
+ this->h_bg = bg;
+
+ return *this;
+ }
+
+ highlighter& with_name(std::string name)
+ {
+ this->h_name = std::move(name);
+ return *this;
+ }
+
+ highlighter& with_nestable(bool val)
+ {
+ this->h_nestable = val;
+ return *this;
+ }
+
+ text_attrs get_attrs() const { return this->h_attrs; }
+
+ void annotate(attr_line_t& al, int start) const;
+
+ void annotate_capture(attr_line_t& al, const line_range& lr) const;
+
+ std::string h_name;
+ role_t h_role{role_t::VCR_NONE};
+ styling::color_unit h_fg{styling::color_unit::make_empty()};
+ styling::color_unit h_bg{styling::color_unit::make_empty()};
+ std::shared_ptr<lnav::pcre2pp::code> h_regex;
+ text_attrs h_attrs;
+ std::set<text_format_t> h_text_formats;
+ intern_string_t h_format_name;
+ bool h_nestable{true};
+};
+
+#endif
diff --git a/src/hist_source.cc b/src/hist_source.cc
new file mode 100644
index 0000000..f48ccc1
--- /dev/null
+++ b/src/hist_source.cc
@@ -0,0 +1,188 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "hist_source.hh"
+
+#include "base/math_util.hh"
+#include "config.h"
+#include "fmt/chrono.h"
+
+nonstd::optional<vis_line_t>
+hist_source2::row_for_time(struct timeval tv_bucket)
+{
+ std::map<int64_t, struct bucket_block>::iterator iter;
+ int retval = 0;
+ time_t time_bucket = rounddown(tv_bucket.tv_sec, this->hs_time_slice);
+
+ for (iter = this->hs_blocks.begin(); iter != this->hs_blocks.end(); ++iter)
+ {
+ struct bucket_block& bb = iter->second;
+
+ if (time_bucket < bb.bb_buckets[0].b_time) {
+ break;
+ }
+ if (time_bucket > bb.bb_buckets[bb.bb_used].b_time) {
+ retval += bb.bb_used + 1;
+ continue;
+ }
+
+ for (unsigned int lpc = 0; lpc <= bb.bb_used; lpc++, retval++) {
+ if (time_bucket <= bb.bb_buckets[lpc].b_time) {
+ return vis_line_t(retval);
+ }
+ }
+ }
+ return vis_line_t(retval);
+}
+
+void
+hist_source2::text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& value_out,
+ text_sub_source::line_flags_t flags)
+{
+ bucket_t& bucket = this->find_bucket(row);
+ struct tm bucket_tm;
+
+ value_out.clear();
+ if (gmtime_r(&bucket.b_time, &bucket_tm) != nullptr) {
+ fmt::format_to(std::back_inserter(value_out),
+ FMT_STRING(" {:%a %b %d %H:%M:%S} "),
+ bucket_tm);
+ } else {
+ log_error("no time?");
+ }
+ fmt::format_to(
+ std::back_inserter(value_out),
+ FMT_STRING(" {:8L} normal {:8L} errors {:8L} warnings {:8L} marks"),
+ rint(bucket.b_values[HT_NORMAL].hv_value),
+ rint(bucket.b_values[HT_ERROR].hv_value),
+ rint(bucket.b_values[HT_WARNING].hv_value),
+ rint(bucket.b_values[HT_MARK].hv_value));
+}
+
+void
+hist_source2::text_attrs_for_line(textview_curses& tc,
+ int row,
+ string_attrs_t& value_out)
+{
+ bucket_t& bucket = this->find_bucket(row);
+ int left = 0;
+
+ for (int lpc = 0; lpc < HT__MAX; lpc++) {
+ this->hs_chart.chart_attrs_for_value(tc,
+ left,
+ (const hist_type_t) lpc,
+ bucket.b_values[lpc].hv_value,
+ value_out);
+ }
+}
+
+void
+hist_source2::add_value(time_t row,
+ hist_source2::hist_type_t htype,
+ double value)
+{
+ if (row < this->hs_last_row) {
+ log_error("time mismatch %ld %ld", row, this->hs_last_row);
+ }
+
+ require(row >= this->hs_last_row);
+
+ row = rounddown(row, this->hs_time_slice);
+ if (row != this->hs_last_row) {
+ this->end_of_row();
+
+ this->hs_last_bucket += 1;
+ this->hs_last_row = row;
+ }
+
+ auto& bucket = this->find_bucket(this->hs_last_bucket);
+ bucket.b_time = row;
+ bucket.b_values[htype].hv_value += value;
+}
+
+void
+hist_source2::init()
+{
+ view_colors& vc = view_colors::singleton();
+
+ this->hs_chart
+ .with_attrs_for_ident(HT_NORMAL, vc.attrs_for_role(role_t::VCR_TEXT))
+ .with_attrs_for_ident(HT_WARNING,
+ vc.attrs_for_role(role_t::VCR_WARNING))
+ .with_attrs_for_ident(HT_ERROR, vc.attrs_for_role(role_t::VCR_ERROR))
+ .with_attrs_for_ident(HT_MARK, vc.attrs_for_role(role_t::VCR_COMMENT));
+}
+
+void
+hist_source2::clear()
+{
+ this->hs_line_count = 0;
+ this->hs_last_bucket = -1;
+ this->hs_last_row = -1;
+ this->hs_blocks.clear();
+ this->hs_chart.clear();
+ this->init();
+}
+
+void
+hist_source2::end_of_row()
+{
+ if (this->hs_last_bucket >= 0) {
+ bucket_t& last_bucket = this->find_bucket(this->hs_last_bucket);
+
+ for (int lpc = 0; lpc < HT__MAX; lpc++) {
+ this->hs_chart.add_value((const hist_type_t) lpc,
+ last_bucket.b_values[lpc].hv_value);
+ }
+ }
+}
+
+nonstd::optional<struct timeval>
+hist_source2::time_for_row(vis_line_t row)
+{
+ if (row < 0 || row > this->hs_line_count) {
+ return nonstd::nullopt;
+ }
+
+ bucket_t& bucket = this->find_bucket(row);
+
+ return timeval{bucket.b_time, 0};
+}
+
+hist_source2::bucket_t&
+hist_source2::find_bucket(int64_t index)
+{
+ struct bucket_block& bb = this->hs_blocks[index / BLOCK_SIZE];
+ unsigned int intra_block_index = index % BLOCK_SIZE;
+ bb.bb_used = std::max(intra_block_index, bb.bb_used);
+ this->hs_line_count = std::max(this->hs_line_count, index + 1);
+ return bb.bb_buckets[intra_block_index];
+}
diff --git a/src/hist_source.hh b/src/hist_source.hh
new file mode 100644
index 0000000..a6399ad
--- /dev/null
+++ b/src/hist_source.hh
@@ -0,0 +1,408 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file hist_source.hh
+ */
+
+#ifndef hist_source_hh
+#define hist_source_hh
+
+#include <cmath>
+#include <limits>
+#include <map>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "base/lnav_log.hh"
+#include "mapbox/variant.hpp"
+#include "strong_int.hh"
+#include "textview_curses.hh"
+
+/** Type for indexes into a group of buckets. */
+STRONG_INT_TYPE(int, bucket_group);
+
+/** Type used to differentiate values added to the same row in the histogram */
+STRONG_INT_TYPE(int, bucket_type);
+
+struct stacked_bar_chart_base {
+ virtual ~stacked_bar_chart_base() = default;
+
+ struct show_none {};
+ struct show_all {};
+ struct show_one {
+ size_t so_index;
+
+ explicit show_one(int so_index) : so_index(so_index) {}
+ };
+
+ using show_state = mapbox::util::variant<show_none, show_all, show_one>;
+
+ enum class direction {
+ forward,
+ backward,
+ };
+};
+
+template<typename T>
+class stacked_bar_chart : public stacked_bar_chart_base {
+public:
+ stacked_bar_chart& with_stacking_enabled(bool enabled)
+ {
+ this->sbc_do_stacking = enabled;
+ return *this;
+ }
+
+ stacked_bar_chart& with_attrs_for_ident(const T& ident, text_attrs attrs)
+ {
+ auto& ci = this->find_ident(ident);
+ ci.ci_attrs = attrs;
+ return *this;
+ }
+
+ stacked_bar_chart& with_margins(unsigned long left, unsigned long right)
+ {
+ this->sbc_left = left;
+ this->sbc_right = right;
+ return *this;
+ }
+
+ bool attrs_in_use(const text_attrs& attrs) const {
+ for (const auto& ident : this->sbc_idents) {
+ if (ident.ci_attrs == attrs) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ show_state show_next_ident(direction dir)
+ {
+ bool single_ident = this->sbc_idents.size() == 1;
+
+ if (this->sbc_idents.empty()) {
+ return this->sbc_show_state;
+ }
+
+ this->sbc_show_state = this->sbc_show_state.match(
+ [&](show_none) -> show_state {
+ switch (dir) {
+ case direction::forward:
+ if (single_ident) {
+ return show_all();
+ }
+ return show_one(0);
+ case direction::backward:
+ return show_all();
+ }
+
+ return show_all();
+ },
+ [&](show_one& one) -> show_state {
+ switch (dir) {
+ case direction::forward:
+ if (one.so_index + 1 == this->sbc_idents.size()) {
+ return show_all();
+ }
+ return show_one(one.so_index + 1);
+ case direction::backward:
+ if (one.so_index == 0) {
+ return show_none();
+ }
+ return show_one(one.so_index - 1);
+ }
+
+ return show_all();
+ },
+ [&](show_all) -> show_state {
+ switch (dir) {
+ case direction::forward:
+ return show_none();
+ case direction::backward:
+ if (single_ident) {
+ return show_none();
+ }
+ return show_one(this->sbc_idents.size() - 1);
+ }
+
+ return show_all();
+ });
+
+ return this->sbc_show_state;
+ }
+
+ void get_ident_to_show(T& ident_out) const
+ {
+ this->sbc_show_state.match(
+ [](const show_none) {},
+ [](const show_all) {},
+ [&](const show_one& one) {
+ ident_out = this->sbc_idents[one.so_index].ci_ident;
+ });
+ }
+
+ void chart_attrs_for_value(const listview_curses& lc,
+ int& left,
+ const T& ident,
+ double value,
+ string_attrs_t& value_out) const
+ {
+ auto ident_iter = this->sbc_ident_lookup.find(ident);
+
+ require(ident_iter != this->sbc_ident_lookup.end());
+
+ size_t ident_index = ident_iter->second;
+ unsigned long width, avail_width;
+ bucket_stats_t overall_stats;
+ struct line_range lr;
+ vis_line_t height;
+
+ lr.lr_unit = line_range::unit::codepoint;
+
+ size_t ident_to_show = this->sbc_show_state.match(
+ [](const show_none) { return -1; },
+ [ident_index](const show_all) { return ident_index; },
+ [](const show_one& one) { return one.so_index; });
+
+ if (ident_to_show != ident_index) {
+ return;
+ }
+
+ lc.get_dimensions(height, width);
+
+ for (size_t lpc = 0; lpc < this->sbc_idents.size(); lpc++) {
+ if (this->sbc_show_state.template is<show_all>()
+ || lpc == (size_t) ident_to_show)
+ {
+ overall_stats.merge(this->sbc_idents[lpc].ci_stats,
+ this->sbc_do_stacking);
+ }
+ }
+
+ if (this->sbc_show_state.template is<show_all>()) {
+ avail_width = width - this->sbc_idents.size();
+ } else {
+ avail_width = width - 1;
+ }
+ avail_width -= this->sbc_left + this->sbc_right;
+
+ lr.lr_start = left;
+
+ const auto& ci = this->sbc_idents[ident_index];
+ int amount;
+
+ if (value == 0.0) {
+ amount = 0;
+ } else if ((overall_stats.bs_max_value - 0.01) <= value
+ && value <= (overall_stats.bs_max_value + 0.01))
+ {
+ amount = avail_width;
+ } else {
+ double percent
+ = (value - overall_stats.bs_min_value) / overall_stats.width();
+ amount = (int) rint(percent * avail_width);
+ amount = std::max(1, amount);
+ }
+ lr.lr_end = left = lr.lr_start + amount;
+
+ if (!ci.ci_attrs.empty() && !lr.empty()) {
+ auto rev_attrs = ci.ci_attrs;
+ rev_attrs.ta_attrs |= A_REVERSE;
+ value_out.emplace_back(lr, VC_STYLE.value(rev_attrs));
+ }
+ }
+
+ void clear()
+ {
+ this->sbc_idents.clear();
+ this->sbc_ident_lookup.clear();
+ this->sbc_show_state = show_all();
+ }
+
+ void add_value(const T& ident, double amount = 1.0)
+ {
+ struct chart_ident& ci = this->find_ident(ident);
+ ci.ci_stats.update(amount);
+ }
+
+ struct bucket_stats_t {
+ bucket_stats_t()
+ : bs_min_value(std::numeric_limits<double>::max()), bs_max_value(0)
+ {
+ }
+
+ void merge(const bucket_stats_t& rhs, bool do_stacking)
+ {
+ this->bs_min_value = std::min(this->bs_min_value, rhs.bs_min_value);
+ if (do_stacking) {
+ this->bs_max_value += rhs.bs_max_value;
+ } else {
+ this->bs_max_value
+ = std::max(this->bs_max_value, rhs.bs_max_value);
+ }
+ }
+
+ double width() const
+ {
+ return std::fabs(this->bs_max_value - this->bs_min_value);
+ }
+
+ void update(double value)
+ {
+ this->bs_max_value = std::max(this->bs_max_value, value);
+ this->bs_min_value = std::min(this->bs_min_value, value);
+ }
+
+ double bs_min_value;
+ double bs_max_value;
+ };
+
+ const bucket_stats_t& get_stats_for(const T& ident)
+ {
+ const chart_ident& ci = this->find_ident(ident);
+
+ return ci.ci_stats;
+ }
+
+protected:
+ struct chart_ident {
+ explicit chart_ident(const T& ident) : ci_ident(ident) {}
+
+ T ci_ident;
+ text_attrs ci_attrs;
+ bucket_stats_t ci_stats;
+ };
+
+ struct chart_ident& find_ident(const T& ident)
+ {
+ auto iter = this->sbc_ident_lookup.find(ident);
+ if (iter == this->sbc_ident_lookup.end()) {
+ this->sbc_ident_lookup[ident] = this->sbc_idents.size();
+ this->sbc_idents.emplace_back(ident);
+ return this->sbc_idents.back();
+ }
+ return this->sbc_idents[iter->second];
+ }
+
+ bool sbc_do_stacking{true};
+ unsigned long sbc_left{0}, sbc_right{0};
+ std::vector<struct chart_ident> sbc_idents;
+ std::unordered_map<T, unsigned int> sbc_ident_lookup;
+ show_state sbc_show_state{show_all()};
+};
+
+class hist_source2
+ : public text_sub_source
+ , public text_time_translator {
+public:
+ typedef enum {
+ HT_NORMAL,
+ HT_WARNING,
+ HT_ERROR,
+ HT_MARK,
+
+ HT__MAX
+ } hist_type_t;
+
+ hist_source2() { this->clear(); }
+
+ ~hist_source2() override = default;
+
+ void init();
+
+ void set_time_slice(int64_t slice) { this->hs_time_slice = slice; }
+
+ int64_t get_time_slice() const { return this->hs_time_slice; }
+
+ size_t text_line_count() override { return this->hs_line_count; }
+
+ size_t text_line_width(textview_curses& curses) override
+ {
+ return 48 + 8 * 4;
+ }
+
+ void clear();
+
+ void add_value(time_t row, hist_type_t htype, double value = 1.0);
+
+ void end_of_row();
+
+ void text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& value_out,
+ line_flags_t flags) override;
+
+ void text_attrs_for_line(textview_curses& tc,
+ int row,
+ string_attrs_t& value_out) override;
+
+ size_t text_size_for_line(textview_curses& tc,
+ int row,
+ line_flags_t flags) override
+ {
+ return 0;
+ }
+
+ nonstd::optional<struct timeval> time_for_row(vis_line_t row) override;
+
+ nonstd::optional<vis_line_t> row_for_time(
+ struct timeval tv_bucket) override;
+
+private:
+ struct hist_value {
+ double hv_value;
+ };
+
+ struct bucket_t {
+ time_t b_time;
+ hist_value b_values[HT__MAX];
+ };
+
+ static const int64_t BLOCK_SIZE = 100;
+
+ struct bucket_block {
+ bucket_block()
+ {
+ memset(this->bb_buckets, 0, sizeof(this->bb_buckets));
+ }
+
+ unsigned int bb_used{0};
+ bucket_t bb_buckets[BLOCK_SIZE];
+ };
+
+ bucket_t& find_bucket(int64_t index);
+
+ int64_t hs_time_slice{10 * 60};
+ int64_t hs_line_count;
+ int64_t hs_last_bucket;
+ time_t hs_last_row;
+ std::map<int64_t, struct bucket_block> hs_blocks;
+ stacked_bar_chart<hist_type_t> hs_chart;
+};
+
+#endif
diff --git a/src/hotkeys.cc b/src/hotkeys.cc
new file mode 100644
index 0000000..dd3af90
--- /dev/null
+++ b/src/hotkeys.cc
@@ -0,0 +1,988 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "hotkeys.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/injector.hh"
+#include "base/itertools.hh"
+#include "base/math_util.hh"
+#include "base/opt_util.hh"
+#include "bookmarks.hh"
+#include "bound_tags.hh"
+#include "command_executor.hh"
+#include "config.h"
+#include "environ_vtab.hh"
+#include "field_overlay_source.hh"
+#include "lnav.hh"
+#include "lnav_config.hh"
+#include "lnav_util.hh"
+#include "log_data_helper.hh"
+#include "plain_text_source.hh"
+#include "readline_highlighters.hh"
+#include "shlex.hh"
+#include "sql_util.hh"
+#include "sysclip.hh"
+#include "termios_guard.hh"
+#include "xterm_mouse.hh"
+
+using namespace lnav::roles::literals;
+
+class logline_helper {
+public:
+ explicit logline_helper(logfile_sub_source& lss) : lh_sub_source(lss) {}
+
+ logline& move_to_msg_start()
+ {
+ content_line_t cl = this->lh_sub_source.at(this->lh_current_line);
+ std::shared_ptr<logfile> lf = this->lh_sub_source.find(cl);
+ auto ll = lf->begin() + cl;
+ while (!ll->is_message()) {
+ --ll;
+ --this->lh_current_line;
+ }
+
+ return (*lf)[cl];
+ }
+
+ logline& current_line() const
+ {
+ content_line_t cl = this->lh_sub_source.at(this->lh_current_line);
+ std::shared_ptr<logfile> lf = this->lh_sub_source.find(cl);
+
+ return (*lf)[cl];
+ }
+
+ void annotate()
+ {
+ this->lh_string_attrs.clear();
+ this->lh_line_values.clear();
+ content_line_t cl = this->lh_sub_source.at(this->lh_current_line);
+ auto lf = this->lh_sub_source.find(cl);
+ auto ll = lf->begin() + cl;
+ auto format = lf->get_format();
+ lf->read_full_message(ll, this->lh_line_values.lvv_sbr);
+ this->lh_line_values.lvv_sbr.erase_ansi();
+ format->annotate(
+ cl, this->lh_string_attrs, this->lh_line_values, false);
+ }
+
+ std::string to_string(const struct line_range& lr) const
+ {
+ const char* start = this->lh_line_values.lvv_sbr.get_data();
+
+ return {&start[lr.lr_start], (size_t) lr.length()};
+ }
+
+ logfile_sub_source& lh_sub_source;
+ vis_line_t lh_current_line;
+ string_attrs_t lh_string_attrs;
+ logline_value_vector lh_line_values;
+};
+
+static int
+key_sql_callback(exec_context& ec, sqlite3_stmt* stmt)
+{
+ if (!sqlite3_stmt_busy(stmt)) {
+ return 0;
+ }
+
+ int ncols = sqlite3_column_count(stmt);
+
+ auto& vars = ec.ec_local_vars.top();
+
+ for (int lpc = 0; lpc < ncols; lpc++) {
+ const char* column_name = sqlite3_column_name(stmt, lpc);
+
+ if (sql_ident_needs_quote(column_name)) {
+ continue;
+ }
+
+ auto* raw_value = sqlite3_column_value(stmt, lpc);
+ auto value_type = sqlite3_column_type(stmt, lpc);
+ scoped_value_t value;
+ switch (value_type) {
+ case SQLITE_INTEGER:
+ value = (int64_t) sqlite3_value_int64(raw_value);
+ break;
+ case SQLITE_FLOAT:
+ value = sqlite3_value_double(raw_value);
+ break;
+ case SQLITE_NULL:
+ value = null_value_t{};
+ break;
+ default:
+ value = std::string((const char*) sqlite3_value_text(raw_value),
+ sqlite3_value_bytes(raw_value));
+ break;
+ }
+ vars[column_name] = value;
+ }
+
+ return 0;
+}
+
+bool
+handle_keyseq(const char* keyseq)
+{
+ key_map& km = lnav_config.lc_active_keymap;
+
+ const auto& iter = km.km_seq_to_cmd.find(keyseq);
+ if (iter == km.km_seq_to_cmd.end()) {
+ return false;
+ }
+
+ logline_value_vector values;
+ exec_context ec(&values, key_sql_callback, pipe_callback);
+ auto& var_stack = ec.ec_local_vars;
+
+ ec.ec_global_vars = lnav_data.ld_exec_context.ec_global_vars;
+ ec.ec_error_callback_stack
+ = lnav_data.ld_exec_context.ec_error_callback_stack;
+ var_stack.push(std::map<std::string, scoped_value_t>());
+ auto& vars = var_stack.top();
+ vars["keyseq"] = keyseq;
+ const auto& kc = iter->second;
+
+ log_debug("executing key sequence %s: %s", keyseq, kc.kc_cmd.c_str());
+ auto result = execute_any(ec, kc.kc_cmd);
+ if (result.isOk()) {
+ lnav_data.ld_rl_view->set_value(result.unwrap());
+ } else {
+ auto um = result.unwrapErr();
+
+ ec.ec_error_callback_stack.back()(um);
+ }
+
+ if (!kc.kc_alt_msg.empty()) {
+ shlex lexer(kc.kc_alt_msg);
+ std::string expanded_msg;
+
+ if (lexer.eval(expanded_msg,
+ {
+ &vars,
+ &ec.ec_global_vars,
+ }))
+ {
+ lnav_data.ld_rl_view->set_alt_value(expanded_msg);
+ }
+ }
+
+ return true;
+}
+
+bool
+handle_paging_key(int ch)
+{
+ if (lnav_data.ld_view_stack.empty()) {
+ return false;
+ }
+
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ exec_context& ec = lnav_data.ld_exec_context;
+ logfile_sub_source* lss = nullptr;
+ text_sub_source* tc_tss = tc->get_sub_source();
+ bookmarks<vis_line_t>::type& bm = tc->get_bookmarks();
+
+ auto keyseq = fmt::format(FMT_STRING("x{:02x}"), ch);
+ if (handle_keyseq(keyseq.c_str())) {
+ return true;
+ }
+
+ if (tc->handle_key(ch)) {
+ return true;
+ }
+
+ lss = dynamic_cast<logfile_sub_source*>(tc->get_sub_source());
+
+ /* process the command keystroke */
+ switch (ch) {
+ case 0x7f:
+ case KEY_BACKSPACE:
+ break;
+
+ case 'a':
+ if (lnav_data.ld_last_view == nullptr) {
+ alerter::singleton().chime("no last view available");
+ } else {
+ auto* tc = lnav_data.ld_last_view;
+
+ lnav_data.ld_last_view = nullptr;
+ ensure_view(tc);
+ }
+ break;
+
+ case 'A':
+ if (lnav_data.ld_last_view == nullptr) {
+ alerter::singleton().chime("no last view available");
+ } else {
+ auto* tc = lnav_data.ld_last_view;
+ auto* top_tc = *lnav_data.ld_view_stack.top();
+ auto* dst_view
+ = dynamic_cast<text_time_translator*>(tc->get_sub_source());
+ auto* src_view = dynamic_cast<text_time_translator*>(
+ top_tc->get_sub_source());
+
+ lnav_data.ld_last_view = nullptr;
+ if (src_view != nullptr && dst_view != nullptr) {
+ src_view->time_for_row(top_tc->get_selection()) |
+ [dst_view, tc](auto top_time) {
+ dst_view->row_for_time(top_time) |
+ [tc](auto row) { tc->set_selection(row); };
+ };
+ }
+ ensure_view(tc);
+ }
+ break;
+
+ case KEY_F(2):
+ if (xterm_mouse::is_available()) {
+ auto& mouse_i = injector::get<xterm_mouse&>();
+ mouse_i.set_enabled(!mouse_i.is_enabled());
+ auto um = lnav::console::user_message::ok(
+ attr_line_t("mouse mode -- ")
+ .append(mouse_i.is_enabled() ? "enabled"_symbol
+ : "disabled"_symbol));
+ lnav_data.ld_rl_view->set_attr_value(um.to_attr_line());
+ } else {
+ lnav_data.ld_rl_view->set_value(
+ "error: mouse support is not available, make sure your "
+ "TERM is set to "
+ "xterm or xterm-256color");
+ }
+ break;
+
+ case 'C':
+ if (lss) {
+ lss->text_clear_marks(&textview_curses::BM_USER);
+ }
+
+ lnav_data.ld_select_start.erase(tc);
+ lnav_data.ld_last_user_mark.erase(tc);
+ tc->get_bookmarks()[&textview_curses::BM_USER].clear();
+ tc->reload_data();
+
+ lnav_data.ld_rl_view->set_attr_value(
+ lnav::console::user_message::ok("Cleared bookmarks")
+ .to_attr_line());
+ break;
+
+ case '>': {
+ auto range_opt = tc->horiz_shift(
+ tc->get_top(), tc->get_bottom(), tc->get_left());
+ if (range_opt && range_opt.value().second != INT_MAX) {
+ tc->set_left(range_opt.value().second);
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(m, "to bookmark a line"));
+ } else {
+ alerter::singleton().chime("no more search hits to the right");
+ }
+ } break;
+
+ case '<':
+ if (tc->get_left() == 0) {
+ alerter::singleton().chime("no more search hits to the left");
+ } else {
+ auto range_opt = tc->horiz_shift(
+ tc->get_top(), tc->get_bottom(), tc->get_left());
+ if (range_opt && range_opt.value().first != -1) {
+ tc->set_left(range_opt.value().first);
+ } else {
+ tc->set_left(0);
+ }
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(m, "to bookmark a line"));
+ }
+ break;
+
+ case 'f':
+ if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ bm[&logfile_sub_source::BM_FILES].next(tc->get_selection()) |
+ [&tc](auto vl) { tc->set_selection(vl); };
+ } else if (tc == &lnav_data.ld_views[LNV_TEXT]) {
+ textfile_sub_source& tss = lnav_data.ld_text_source;
+
+ if (!tss.empty()) {
+ tss.rotate_left();
+ }
+ tc->reload_data();
+ }
+ break;
+
+ case 'F':
+ if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ bm[&logfile_sub_source::BM_FILES].prev(tc->get_selection()) |
+ [&tc](auto vl) {
+ // setting the selection for movement to previous file
+ // marker instead of the top will move the cursor, too,
+ // if needed.
+ tc->set_selection(vl);
+ };
+ } else if (tc == &lnav_data.ld_views[LNV_TEXT]) {
+ textfile_sub_source& tss = lnav_data.ld_text_source;
+
+ if (!tss.empty()) {
+ tss.rotate_right();
+ }
+ tc->reload_data();
+ }
+ break;
+
+ case 'z':
+ if ((lnav_data.ld_zoom_level - 1) < 0) {
+ alerter::singleton().chime("maximum zoom-in level reached");
+ } else {
+ execute_command(
+ ec,
+ "zoom-to "
+ + lnav_zoom_strings[lnav_data.ld_zoom_level - 1]);
+ }
+ break;
+
+ case 'Z':
+ if ((lnav_data.ld_zoom_level + 1) >= ZOOM_COUNT) {
+ alerter::singleton().chime("maximum zoom-out level reached");
+ } else {
+ execute_command(
+ ec,
+ "zoom-to "
+ + lnav_zoom_strings[lnav_data.ld_zoom_level + 1]);
+ }
+ break;
+
+ case 'J':
+ if (lnav_data.ld_last_user_mark.find(tc)
+ == lnav_data.ld_last_user_mark.end()
+ || !tc->is_line_visible(
+ vis_line_t(lnav_data.ld_last_user_mark[tc])))
+ {
+ lnav_data.ld_select_start[tc] = tc->get_selection();
+ lnav_data.ld_last_user_mark[tc] = tc->get_selection();
+ } else {
+ vis_line_t height;
+ unsigned long width;
+
+ tc->get_dimensions(height, width);
+ if (lnav_data.ld_last_user_mark[tc] > (tc->get_bottom() - 2)
+ && tc->get_selection() + height < tc->get_inner_height())
+ {
+ tc->shift_top(1_vl);
+ }
+ if (lnav_data.ld_last_user_mark[tc] + 1
+ >= tc->get_inner_height())
+ {
+ break;
+ }
+ lnav_data.ld_last_user_mark[tc] += 1;
+ }
+ tc->toggle_user_mark(&textview_curses::BM_USER,
+ vis_line_t(lnav_data.ld_last_user_mark[tc]));
+ if (tc->is_selectable()
+ && tc->get_selection() + 1_vl < tc->get_inner_height())
+ {
+ tc->set_selection(tc->get_selection() + 1_vl);
+ }
+ tc->reload_data();
+
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(c, "to copy marked lines to the clipboard"));
+ break;
+
+ case 'K': {
+ int new_mark;
+
+ if (lnav_data.ld_last_user_mark.find(tc)
+ == lnav_data.ld_last_user_mark.end()
+ || !tc->is_line_visible(
+ vis_line_t(lnav_data.ld_last_user_mark[tc])))
+ {
+ new_mark = tc->get_selection();
+ } else {
+ new_mark = lnav_data.ld_last_user_mark[tc];
+ }
+
+ tc->toggle_user_mark(&textview_curses::BM_USER,
+ vis_line_t(new_mark));
+ if (new_mark == tc->get_selection() && tc->get_top() > 0_vl) {
+ tc->shift_top(-1_vl);
+ }
+ if (new_mark > 0) {
+ lnav_data.ld_last_user_mark[tc] = new_mark - 1;
+ } else {
+ lnav_data.ld_last_user_mark[tc] = new_mark;
+ alerter::singleton().chime("no more lines to mark");
+ }
+ lnav_data.ld_select_start[tc] = tc->get_selection();
+ if (tc->is_selectable() && tc->get_selection() > 0_vl) {
+ tc->set_selection(tc->get_selection() - 1_vl);
+ }
+ tc->reload_data();
+
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(c, "to copy marked lines to the clipboard"));
+ break;
+ }
+
+ case 'M':
+ if (lnav_data.ld_last_user_mark.find(tc)
+ == lnav_data.ld_last_user_mark.end())
+ {
+ alerter::singleton().chime("no lines have been marked");
+ } else {
+ int start_line = std::min((int) tc->get_selection(),
+ lnav_data.ld_last_user_mark[tc] + 1);
+ int end_line = std::max((int) tc->get_selection(),
+ lnav_data.ld_last_user_mark[tc] - 1);
+
+ tc->toggle_user_mark(&textview_curses::BM_USER,
+ vis_line_t(start_line),
+ vis_line_t(end_line));
+ tc->reload_data();
+ }
+ break;
+
+#if 0
+ case 'S':
+ {
+ bookmark_vector<vis_line_t>::iterator iter;
+
+ for (iter = bm[&textview_curses::BM_SEARCH].begin();
+ iter != bm[&textview_curses::BM_SEARCH].end();
+ ++iter) {
+ tc->toggle_user_mark(&textview_curses::BM_USER, *iter);
+ }
+
+ lnav_data.ld_last_user_mark[tc] = -1;
+ tc->reload_data();
+ }
+ break;
+#endif
+
+ case 's':
+ if (lss) {
+ auto next_top = tc->get_selection() + 2_vl;
+
+ if (!lss->is_time_offset_enabled()) {
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(T, "to disable elapsed-time mode"));
+ }
+ lss->set_time_offset(true);
+ while (next_top < tc->get_inner_height()) {
+ if (!lss->find_line(lss->at(next_top))->is_message()) {
+ } else if (lss->get_line_accel_direction(next_top)
+ == log_accel::A_DECEL)
+ {
+ --next_top;
+ tc->set_selection(next_top);
+ break;
+ }
+
+ ++next_top;
+ }
+ }
+ break;
+
+ case 'S':
+ if (lss) {
+ auto next_top = tc->get_selection();
+
+ if (!lss->is_time_offset_enabled()) {
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(T, "to disable elapsed-time mode"));
+ }
+ lss->set_time_offset(true);
+ while (0 <= next_top && next_top < tc->get_inner_height()) {
+ if (!lss->find_line(lss->at(next_top))->is_message()) {
+ } else if (lss->get_line_accel_direction(next_top)
+ == log_accel::A_DECEL)
+ {
+ --next_top;
+ tc->set_selection(next_top);
+ break;
+ }
+
+ --next_top;
+ }
+ }
+ break;
+
+ case '9':
+ if (lss) {
+ double tenth = ((double) tc->get_inner_height()) / 10.0;
+
+ tc->shift_top(vis_line_t(tenth));
+ }
+ break;
+
+ case '(':
+ if (lss) {
+ double tenth = ((double) tc->get_inner_height()) / 10.0;
+
+ tc->shift_top(vis_line_t(-tenth));
+ }
+ break;
+
+ case '0':
+ if (lss) {
+ const int step = 24 * 60 * 60;
+ lss->time_for_row(tc->get_selection()) |
+ [lss, tc](auto first_time) {
+ lss->find_from_time(
+ roundup_size(first_time.tv_sec, step))
+ | [tc](auto line) { tc->set_selection(line); };
+ };
+ }
+ break;
+
+ case ')':
+ if (lss) {
+ lss->time_for_row(tc->get_selection()) |
+ [lss, tc](auto first_time) {
+ time_t day = rounddown(first_time.tv_sec, 24 * 60 * 60);
+ lss->find_from_time(day) | [tc](auto line) {
+ if (line != 0_vl) {
+ --line;
+ }
+ tc->set_selection(line);
+ };
+ };
+ }
+ break;
+
+ case 'D':
+ if (tc->get_selection() == 0) {
+ alerter::singleton().chime(
+ "the top of the log has been reached");
+ } else if (lss) {
+ lss->time_for_row(tc->get_selection()) |
+ [lss, ch, tc](auto first_time) {
+ int step = ch == 'D' ? (24 * 60 * 60) : (60 * 60);
+ time_t top_time = first_time.tv_sec;
+ lss->find_from_time(top_time - step) | [tc](auto line) {
+ if (line != 0_vl) {
+ --line;
+ }
+ tc->set_selection(line);
+ };
+ };
+
+ lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1(/, "to search"));
+ }
+ break;
+
+ case 'd':
+ if (lss) {
+ lss->time_for_row(tc->get_selection()) |
+ [ch, lss, tc](auto first_time) {
+ int step = ch == 'd' ? (24 * 60 * 60) : (60 * 60);
+ lss->find_from_time(first_time.tv_sec + step) |
+ [tc](auto line) { tc->set_selection(line); };
+ };
+
+ lnav_data.ld_rl_view->set_alt_value(HELP_MSG_1(/, "to search"));
+ }
+ break;
+
+ case 'o':
+ case 'O':
+ if (lss != nullptr) {
+ logline_helper start_helper(*lss);
+
+ start_helper.lh_current_line = tc->get_selection();
+ auto& start_line = start_helper.move_to_msg_start();
+ start_helper.annotate();
+
+ struct line_range opid_range = find_string_attr_range(
+ start_helper.lh_string_attrs, &logline::L_OPID);
+ if (!opid_range.is_valid()) {
+ alerter::singleton().chime(
+ "Log message does not contain an opid");
+ lnav_data.ld_rl_view->set_attr_value(
+ lnav::console::user_message::error(
+ "Log message does not contain an opid")
+ .to_attr_line());
+ } else {
+ unsigned int opid_hash = start_line.get_opid();
+ logline_helper next_helper(*lss);
+ bool found = false;
+
+ next_helper.lh_current_line = start_helper.lh_current_line;
+
+ while (true) {
+ if (ch == 'o') {
+ if (++next_helper.lh_current_line
+ >= tc->get_inner_height())
+ {
+ break;
+ }
+ } else {
+ if (--next_helper.lh_current_line <= 0) {
+ break;
+ }
+ }
+ logline& next_line = next_helper.current_line();
+ if (!next_line.is_message()) {
+ continue;
+ }
+ if (next_line.get_opid() != opid_hash) {
+ continue;
+ }
+ next_helper.annotate();
+ struct line_range opid_next_range
+ = find_string_attr_range(
+ next_helper.lh_string_attrs, &logline::L_OPID);
+ const char* start_opid
+ = start_helper.lh_line_values.lvv_sbr.get_data_at(
+ opid_range.lr_start);
+ const char* next_opid
+ = next_helper.lh_line_values.lvv_sbr.get_data_at(
+ opid_next_range.lr_start);
+ if (opid_range.length() != opid_next_range.length()
+ || memcmp(
+ start_opid, next_opid, opid_range.length())
+ != 0)
+ {
+ continue;
+ }
+ found = true;
+ break;
+ }
+ if (found) {
+ lnav_data.ld_rl_view->set_value("");
+ tc->set_selection(next_helper.lh_current_line);
+ } else {
+ const auto opid_str
+ = start_helper.to_string(opid_range);
+
+ lnav_data.ld_rl_view->set_attr_value(
+ lnav::console::user_message::error(
+ attr_line_t(
+ "No more messages found with opid: ")
+ .append(lnav::roles::symbol(opid_str)))
+ .to_attr_line());
+ alerter::singleton().chime(
+ "no more messages found with opid");
+ }
+ }
+ }
+ break;
+
+ case 'p':
+ if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ auto* fos = dynamic_cast<field_overlay_source*>(
+ tc->get_overlay_source());
+ auto& top_context = fos->fos_contexts.top();
+ top_context.c_show = !top_context.c_show;
+ tc->set_sync_selection_and_top(top_context.c_show);
+ tc->set_needs_update();
+ } else if (tc == &lnav_data.ld_views[LNV_DB]) {
+ auto* dos = dynamic_cast<db_overlay_source*>(
+ tc->get_overlay_source());
+
+ dos->dos_active = !dos->dos_active;
+ tc->set_sync_selection_and_top(dos->dos_active);
+ tc->set_needs_update();
+ }
+ break;
+
+ case 't':
+ if (lnav_data.ld_text_source.current_file() == nullptr) {
+ alerter::singleton().chime("No text files loaded");
+ lnav_data.ld_rl_view->set_attr_value(
+ lnav::console::user_message::error("No text files loaded")
+ .to_attr_line());
+ } else if (toggle_view(&lnav_data.ld_views[LNV_TEXT])) {
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_2(f, F, "to switch to the next/previous file"));
+ }
+ break;
+
+ case 'T':
+ lnav_data.ld_log_source.toggle_time_offset();
+ if (lss && lss->is_time_offset_enabled()) {
+ lnav_data.ld_rl_view->set_alt_value(HELP_MSG_2(
+ s, S, "to move forward/backward through slow downs"));
+ }
+ tc->reload_data();
+ break;
+
+ case 'I': {
+ auto& hist_tc = lnav_data.ld_views[LNV_HISTOGRAM];
+
+ if (toggle_view(&hist_tc)) {
+ auto* src_view
+ = dynamic_cast<text_time_translator*>(tc->get_sub_source());
+
+ if (src_view != nullptr) {
+ src_view->time_for_row(tc->get_selection()) |
+ [](auto log_top) {
+ lnav_data.ld_hist_source2.row_for_time(log_top) |
+ [](auto row) {
+ lnav_data.ld_views[LNV_HISTOGRAM]
+ .set_selection(row);
+ };
+ };
+ }
+ } else {
+ lnav_data.ld_view_stack.top() | [&](auto top_tc) {
+ auto* dst_view = dynamic_cast<text_time_translator*>(
+ top_tc->get_sub_source());
+
+ if (dst_view != nullptr) {
+ auto& hs = lnav_data.ld_hist_source2;
+ auto hist_top_time_opt
+ = hs.time_for_row(hist_tc.get_selection());
+ auto curr_top_time_opt
+ = dst_view->time_for_row(top_tc->get_selection());
+ if (hist_top_time_opt && curr_top_time_opt
+ && hs.row_for_time(hist_top_time_opt.value())
+ != hs.row_for_time(curr_top_time_opt.value()))
+ {
+ dst_view->row_for_time(hist_top_time_opt.value()) |
+ [top_tc](auto new_top) {
+ top_tc->set_selection(new_top);
+ top_tc->set_needs_update();
+ };
+ }
+ }
+ };
+ }
+ break;
+ }
+
+ case 'V': {
+ textview_curses* db_tc = &lnav_data.ld_views[LNV_DB];
+ db_label_source& dls = lnav_data.ld_db_row_source;
+
+ if (toggle_view(db_tc)) {
+ auto log_line_index = dls.column_name_to_index("log_line");
+
+ if (!log_line_index) {
+ log_line_index = dls.column_name_to_index("min(log_line)");
+ }
+
+ if (log_line_index) {
+ fmt::memory_buffer linestr;
+ int line_number = (int) tc->get_selection();
+ unsigned int row;
+
+ fmt::format_to(std::back_inserter(linestr),
+ FMT_STRING("{}"),
+ line_number);
+ linestr.push_back('\0');
+ for (row = 0; row < dls.dls_rows.size(); row++) {
+ if (strcmp(dls.dls_rows[row][log_line_index.value()],
+ linestr.data())
+ == 0)
+ {
+ vis_line_t db_line(row);
+
+ db_tc->set_selection(db_line);
+ db_tc->set_needs_update();
+ break;
+ }
+ }
+ }
+ } else if (db_tc->get_inner_height() > 0) {
+ int db_row = db_tc->get_selection();
+ tc = &lnav_data.ld_views[LNV_LOG];
+ auto log_line_index = dls.column_name_to_index("log_line");
+
+ if (!log_line_index) {
+ log_line_index = dls.column_name_to_index("min(log_line)");
+ }
+
+ if (log_line_index) {
+ unsigned int line_number;
+
+ if (sscanf(dls.dls_rows[db_row][log_line_index.value()],
+ "%d",
+ &line_number)
+ && line_number < tc->listview_rows(*tc))
+ {
+ tc->set_selection(vis_line_t(line_number));
+ tc->set_needs_update();
+ }
+ } else {
+ for (size_t lpc = 0; lpc < dls.dls_headers.size(); lpc++) {
+ date_time_scanner dts;
+ struct timeval tv;
+ struct exttm tm;
+ const char* col_value = dls.dls_rows[db_row][lpc];
+ size_t col_len = strlen(col_value);
+
+ if (dts.scan(col_value, col_len, nullptr, &tm, tv)
+ != nullptr)
+ {
+ lnav_data.ld_log_source.find_from_time(tv) |
+ [tc](auto vl) {
+ tc->set_selection(vl);
+ tc->set_needs_update();
+ };
+ break;
+ }
+ }
+ }
+ }
+ } break;
+
+ case '\t':
+ case KEY_BTAB:
+ if (tc == &lnav_data.ld_views[LNV_DB]) {
+ auto& chart = lnav_data.ld_db_row_source.dls_chart;
+ const auto& state = chart.show_next_ident(
+ ch == '\t' ? stacked_bar_chart_base::direction::forward
+ : stacked_bar_chart_base::direction::backward);
+
+ state.match(
+ [&](stacked_bar_chart_base::show_none) {
+ lnav_data.ld_rl_view->set_value("Graphing no values");
+ },
+ [&](stacked_bar_chart_base::show_all) {
+ lnav_data.ld_rl_view->set_value("Graphing all values");
+ },
+ [&](stacked_bar_chart_base::show_one) {
+ std::string colname;
+
+ chart.get_ident_to_show(colname);
+ lnav_data.ld_rl_view->set_value(
+ "Graphing column " ANSI_BOLD_START + colname
+ + ANSI_NORM);
+ });
+
+ tc->reload_data();
+ } else if (tc == &lnav_data.ld_views[LNV_SPECTRO]) {
+ lnav_data.ld_mode = ln_mode_t::SPECTRO_DETAILS;
+ } else if (tc_tss != nullptr && tc_tss->tss_supports_filtering) {
+ lnav_data.ld_mode = lnav_data.ld_last_config_mode;
+ lnav_data.ld_filter_view.reload_data();
+ lnav_data.ld_files_view.reload_data();
+ if (tc->get_inner_height() > 0_vl) {
+ std::vector<attr_line_t> rows(1);
+
+ tc->get_data_source()->listview_value_for_rows(
+ *tc, tc->get_top(), rows);
+ string_attrs_t& sa = rows[0].get_attrs();
+ auto line_attr_opt = get_string_attr(sa, logline::L_FILE);
+ if (line_attr_opt) {
+ const auto& fc = lnav_data.ld_active_files;
+ auto lf = line_attr_opt.value().get();
+ auto index_opt
+ = fc.fc_files | lnav::itertools::find(lf);
+ if (index_opt) {
+ auto index_vl = vis_line_t(index_opt.value());
+
+ lnav_data.ld_files_view.set_top(index_vl);
+ lnav_data.ld_files_view.set_selection(index_vl);
+ }
+ }
+ }
+ } else {
+ alerter::singleton().chime(
+ "no configuration panels in this view");
+ }
+ break;
+
+ case 'x':
+ if (tc->toggle_hide_fields()) {
+ lnav_data.ld_rl_view->set_value("Showing hidden fields");
+ } else {
+ lnav_data.ld_rl_view->set_value("Hiding hidden fields");
+ }
+ tc->set_needs_update();
+ break;
+
+ case 'r':
+ case 'R':
+ if (lss != nullptr) {
+ const auto& last_time = injector::get<const relative_time&,
+ last_relative_time_tag>();
+
+ if (last_time.empty()) {
+ lnav_data.ld_rl_view->set_value(
+ "Use the 'goto' command to set the relative time to "
+ "move by");
+ } else {
+ vis_line_t vl = tc->get_selection(), new_vl;
+ relative_time rt = last_time;
+ content_line_t cl;
+ struct exttm tm;
+ bool done = false;
+
+ if (ch == 'r') {
+ if (rt.is_negative()) {
+ rt.negate();
+ }
+ } else if (ch == 'R') {
+ if (!rt.is_negative()) {
+ rt.negate();
+ }
+ }
+
+ cl = lnav_data.ld_log_source.at(vl);
+ logline* ll = lnav_data.ld_log_source.find_line(cl);
+ ll->to_exttm(tm);
+ do {
+ tm = rt.adjust(tm);
+ auto new_vl_opt
+ = lnav_data.ld_log_source.find_from_time(tm);
+ if (!new_vl_opt) {
+ break;
+ }
+ new_vl = new_vl_opt.value();
+
+ if (new_vl == 0_vl || new_vl != vl || !rt.is_relative())
+ {
+ vl = new_vl;
+ done = true;
+ }
+ } while (!done);
+ tc->set_top(vl);
+ lnav_data.ld_rl_view->set_value(" " + rt.to_string());
+ }
+ }
+ break;
+
+ case KEY_CTRL_W:
+ execute_command(ec,
+ lnav_data.ld_views[LNV_LOG].get_word_wrap()
+ ? "disable-word-wrap"
+ : "enable-word-wrap");
+ break;
+
+ case KEY_CTRL_P:
+ lnav_data.ld_preview_hidden = !lnav_data.ld_preview_hidden;
+ break;
+
+ default:
+ log_debug("key sequence %x", ch);
+ return false;
+ }
+ return true;
+}
diff --git a/src/hotkeys.hh b/src/hotkeys.hh
new file mode 100644
index 0000000..3bbe060
--- /dev/null
+++ b/src/hotkeys.hh
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LNAV_HOTKEYS_H
+#define LNAV_HOTKEYS_H
+
+bool handle_keyseq(const char* keyseq);
+bool handle_paging_key(int ch);
+
+#endif // LNAV_HOTKEYS_H
diff --git a/src/init.sql b/src/init.sql
new file mode 100644
index 0000000..ef75775
--- /dev/null
+++ b/src/init.sql
@@ -0,0 +1,138 @@
+CREATE TABLE IF NOT EXISTS http_status_codes
+(
+ status INTEGER PRIMARY KEY,
+ message TEXT,
+
+ FOREIGN KEY (status) REFERENCES access_log (sc_status)
+);
+
+INSERT INTO http_status_codes VALUES (100, 'Continue');
+INSERT INTO http_status_codes VALUES (101, 'Switching Protocols');
+INSERT INTO http_status_codes VALUES (102, 'Processing');
+INSERT INTO http_status_codes VALUES (200, 'OK');
+INSERT INTO http_status_codes VALUES (201, 'Created');
+INSERT INTO http_status_codes VALUES (202, 'Accepted');
+INSERT INTO http_status_codes VALUES (203, 'Non-Authoritative Information');
+INSERT INTO http_status_codes VALUES (204, 'No Content');
+INSERT INTO http_status_codes VALUES (205, 'Reset Content');
+INSERT INTO http_status_codes VALUES (206, 'Partial Content');
+INSERT INTO http_status_codes VALUES (207, 'Multi-Status');
+INSERT INTO http_status_codes VALUES (208, 'Already Reported');
+INSERT INTO http_status_codes VALUES (226, 'IM Used');
+INSERT INTO http_status_codes VALUES (300, 'Multiple Choices');
+INSERT INTO http_status_codes VALUES (301, 'Moved Permanently');
+INSERT INTO http_status_codes VALUES (302, 'Found');
+INSERT INTO http_status_codes VALUES (303, 'See Other');
+INSERT INTO http_status_codes VALUES (304, 'Not Modified');
+INSERT INTO http_status_codes VALUES (305, 'Use Proxy');
+INSERT INTO http_status_codes VALUES (306, '(Unused)');
+INSERT INTO http_status_codes VALUES (307, 'Temporary Redirect');
+INSERT INTO http_status_codes VALUES (308, 'Permanent Redirect');
+INSERT INTO http_status_codes VALUES (400, 'Bad Request');
+INSERT INTO http_status_codes VALUES (401, 'Unauthorized');
+INSERT INTO http_status_codes VALUES (402, 'Payment Required');
+INSERT INTO http_status_codes VALUES (403, 'Forbidden');
+INSERT INTO http_status_codes VALUES (404, 'Not Found');
+INSERT INTO http_status_codes VALUES (405, 'Method Not Allowed');
+INSERT INTO http_status_codes VALUES (406, 'Not Acceptable');
+INSERT INTO http_status_codes VALUES (407, 'Proxy Authentication Required');
+INSERT INTO http_status_codes VALUES (408, 'Request Timeout');
+INSERT INTO http_status_codes VALUES (409, 'Conflict');
+INSERT INTO http_status_codes VALUES (410, 'Gone');
+INSERT INTO http_status_codes VALUES (411, 'Length Required');
+INSERT INTO http_status_codes VALUES (412, 'Precondition Failed');
+INSERT INTO http_status_codes VALUES (413, 'Payload Too Large');
+INSERT INTO http_status_codes VALUES (414, 'URI Too Long');
+INSERT INTO http_status_codes VALUES (415, 'Unsupported Media Type');
+INSERT INTO http_status_codes VALUES (416, 'Range Not Satisfiable');
+INSERT INTO http_status_codes VALUES (417, 'Expectation Failed');
+INSERT INTO http_status_codes VALUES (421, 'Misdirected Request');
+INSERT INTO http_status_codes VALUES (422, 'Unprocessable Entity');
+INSERT INTO http_status_codes VALUES (423, 'Locked');
+INSERT INTO http_status_codes VALUES (424, 'Failed Dependency');
+INSERT INTO http_status_codes VALUES (426, 'Upgrade Required');
+INSERT INTO http_status_codes VALUES (428, 'Precondition Required');
+INSERT INTO http_status_codes VALUES (429, 'Too Many Requests');
+INSERT INTO http_status_codes VALUES (431, 'Request Header Fields Too Large');
+INSERT INTO http_status_codes VALUES (500, 'Internal Server Error');
+INSERT INTO http_status_codes VALUES (501, 'Not Implemented');
+INSERT INTO http_status_codes VALUES (502, 'Bad Gateway');
+INSERT INTO http_status_codes VALUES (503, 'Service Unavailable');
+INSERT INTO http_status_codes VALUES (504, 'Gateway Timeout');
+INSERT INTO http_status_codes VALUES (505, 'HTTP Version Not Supported');
+INSERT INTO http_status_codes VALUES (506, 'Variant Also Negotiates');
+INSERT INTO http_status_codes VALUES (507, 'Insufficient Storage');
+INSERT INTO http_status_codes VALUES (508, 'Loop Detected');
+INSERT INTO http_status_codes VALUES (510, 'Not Extended');
+INSERT INTO http_status_codes VALUES (511, 'Network Authentication Required');
+
+CREATE TABLE lnav_example_log
+(
+ log_line INTEGER PRIMARY KEY,
+ log_part TEXT collate naturalnocase,
+ log_time datetime,
+ log_actual_time datetime hidden,
+ log_idle_msecs int,
+ log_level TEXT collate loglevel,
+ log_mark boolean,
+ log_comment TEXT,
+ log_tags TEXT,
+ log_filters TEXT,
+
+ ex_procname TEXT collate 'BINARY',
+ ex_duration INTEGER,
+
+ log_time_msecs int hidden,
+ log_path TEXT hidden collate naturalnocase,
+ log_text TEXT hidden,
+ log_body TEXT hidden
+);
+
+CREATE VIEW lnav_top_view AS
+SELECT *
+FROM lnav_views
+WHERE name = (SELECT name FROM lnav_view_stack ORDER BY rowid DESC LIMIT 1);
+
+INSERT INTO lnav_example_log
+VALUES (0, null, '2017-02-03T04:05:06.100', '2017-02-03T04:05:06.100', 0,
+ 'info', 0, null, null, null, 'hw', 2, 1486094706000, '/tmp/log',
+ '2017-02-03T04:05:06.100 hw(2): Hello, World!', 'Hello, World!'),
+ (1, null, '2017-02-03T04:05:06.200', '2017-02-03T04:05:06.200', 100,
+ 'error', 0, null, null, null, 'gw', 4, 1486094706000, '/tmp/log',
+ '2017-02-03T04:05:06.200 gw(4): Goodbye, World!', 'Goodbye, World!'),
+ (2, 'new', '2017-02-03T04:25:06.200', '2017-02-03T04:25:06.200', 1200000,
+ 'warn', 0, null, null, null, 'gw', 1, 1486095906000, '/tmp/log',
+ '2017-02-03T04:25:06.200 gw(1): Goodbye, World!', 'Goodbye, World!'),
+ (3, 'new', '2017-02-03T04:55:06.200', '2017-02-03T04:55:06.200', 1800000,
+ 'debug', 0, null, null, null, 'gw', 10, 1486097706000, '/tmp/log',
+ '2017-02-03T04:55:06.200 gw(10): Goodbye, World!', 'Goodbye, World!');
+
+CREATE TABLE lnav_user_notifications
+(
+ -- A unique identifier for the notification.
+ id TEXT NOT NULL DEFAULT 'org.lnav.user' PRIMARY KEY,
+ -- The priority of this message relative to others, the highest priority
+ -- message will be shown in the top-right corner.
+ priority INTEGER NOT NULL DEFAULT 0,
+ -- The time when this notification was created.
+ created DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ -- The time when this notification is no longer applicable.
+ expiration DATETIME DEFAULT NULL,
+ -- A JSON array with the names of the views where this notification is
+ -- applicable. Use NULL to show it in all views.
+ views JSON,
+ -- The message to display, can be null to clear the message.
+ message TEXT,
+
+ CHECK (views IS NULL OR json_type(views) = 'array')
+);
+
+INSERT INTO lnav_user_notifications (id, priority, expiration, message)
+VALUES ('org.lnav.breadcrumb.focus', -1, datetime('now', '+1 minute'),
+ 'Press ENTER to focus on the breadcrumb bar');
+
+CREATE TABLE lnav_views_echo AS
+SELECT name, top, "left", height, inner_height, top_time, search
+FROM lnav_views;
+
+CREATE UNIQUE INDEX lnav_views_echo_index ON lnav_views_echo (name);
diff --git a/src/input_dispatcher.cc b/src/input_dispatcher.cc
new file mode 100644
index 0000000..62c1f40
--- /dev/null
+++ b/src/input_dispatcher.cc
@@ -0,0 +1,178 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file input_dispatcher.cc
+ */
+
+#include <array>
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/time.h>
+
+#include "config.h"
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/curses.h>
+#elif defined HAVE_NCURSESW_H
+# include <ncursesw.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/curses.h>
+#elif defined HAVE_NCURSES_H
+# include <ncurses.h>
+#elif defined HAVE_CURSES_H
+# include <curses.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+
+#include "base/lnav_log.hh"
+#include "base/time_util.hh"
+#include "input_dispatcher.hh"
+#include "ww898/cp_utf8.hpp"
+
+using namespace ww898;
+
+template<typename A>
+static void
+to_key_seq(A& dst, const char* src)
+{
+ dst[0] = '\0';
+ for (size_t lpc = 0; src[lpc]; lpc++) {
+ snprintf(dst.data() + strlen(dst.data()),
+ dst.size() - strlen(dst.data()),
+ "x%02x",
+ src[lpc] & 0xff);
+ }
+}
+
+void
+input_dispatcher::new_input(const struct timeval& current_time, int ch)
+{
+ nonstd::optional<bool> handled = nonstd::nullopt;
+ std::array<char, 32 * 3 + 1> keyseq{0};
+
+ switch (ch) {
+ case KEY_ESCAPE:
+ this->reset_escape_buffer(ch, current_time);
+ break;
+ case KEY_MOUSE:
+ this->id_mouse_handler();
+ break;
+ default:
+ if (this->id_escape_index > 0) {
+ this->append_to_escape_buffer(ch);
+
+ if (strcmp("\x1b[", this->id_escape_buffer) == 0) {
+ } else if (strcmp("\x1b[<", this->id_escape_buffer) == 0) {
+ this->id_mouse_handler();
+ this->id_escape_index = 0;
+ } else if (this->id_escape_expected_size == -1
+ || this->id_escape_index
+ == this->id_escape_expected_size)
+ {
+ to_key_seq(keyseq, this->id_escape_buffer);
+ switch (this->id_escape_matcher(keyseq.data())) {
+ case escape_match_t::NONE: {
+ for (int lpc = 0; this->id_escape_buffer[lpc];
+ lpc++) {
+ handled = this->id_key_handler(
+ this->id_escape_buffer[lpc]);
+ }
+ this->id_escape_index = 0;
+ break;
+ }
+ case escape_match_t::PARTIAL:
+ break;
+ case escape_match_t::FULL:
+ this->id_escape_handler(keyseq.data());
+ this->id_escape_index = 0;
+ break;
+ }
+ }
+ if (this->id_escape_expected_size != -1
+ && this->id_escape_index == this->id_escape_expected_size)
+ {
+ this->id_escape_index = 0;
+ }
+ } else {
+ auto seq_size = utf::utf8::char_size([ch]() {
+ return std::make_pair(ch, 16);
+ }).unwrapOr(size_t{1});
+
+ if (seq_size == 1) {
+ snprintf(keyseq.data(), keyseq.size(), "x%02x", ch & 0xff);
+ handled = this->id_key_handler(ch);
+ } else {
+ this->reset_escape_buffer(ch, current_time, seq_size);
+ }
+ }
+ break;
+ }
+
+ if (handled && !handled.value()) {
+ this->id_unhandled_handler(keyseq.data());
+ }
+}
+
+void
+input_dispatcher::poll(const struct timeval& current_time)
+{
+ if (this->id_escape_index == 1) {
+ static const struct timeval escape_threshold = {0, 10000};
+ struct timeval diff;
+
+ timersub(&current_time, &this->id_escape_start_time, &diff);
+ if (escape_threshold < diff) {
+ this->id_key_handler(KEY_CTRL_RBRACKET);
+ this->id_escape_index = 0;
+ }
+ }
+}
+
+void
+input_dispatcher::reset_escape_buffer(int ch,
+ const timeval& current_time,
+ ssize_t expected_size)
+{
+ this->id_escape_index = 0;
+ this->append_to_escape_buffer(ch);
+ this->id_escape_expected_size = expected_size;
+ this->id_escape_start_time = current_time;
+}
+
+void
+input_dispatcher::append_to_escape_buffer(int ch)
+{
+ if (this->id_escape_index
+ < static_cast<ssize_t>(sizeof(this->id_escape_buffer) - 1))
+ {
+ this->id_escape_buffer[this->id_escape_index++] = static_cast<char>(ch);
+ this->id_escape_buffer[this->id_escape_index] = '\0';
+ }
+}
diff --git a/src/input_dispatcher.hh b/src/input_dispatcher.hh
new file mode 100644
index 0000000..6aa7059
--- /dev/null
+++ b/src/input_dispatcher.hh
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file input_dispatcher.hh
+ */
+
+#ifndef INPUT_DISPATCHER_HH
+#define INPUT_DISPATCHER_HH
+
+#include <functional>
+
+#include <sys/types.h>
+
+#define KEY_ESCAPE 0x1b
+#define KEY_CTRL_RBRACKET 0x1d
+
+class input_dispatcher {
+public:
+ void new_input(const struct timeval& current_time, int ch);
+
+ void poll(const struct timeval& current_time);
+
+ bool in_escape() const
+ {
+ return this->id_escape_index > 0;
+ }
+
+ enum class escape_match_t {
+ NONE,
+ PARTIAL,
+ FULL,
+ };
+
+ std::function<escape_match_t(const char*)> id_escape_matcher;
+ std::function<bool(int)> id_key_handler;
+ std::function<void(const char*)> id_escape_handler;
+ std::function<void()> id_mouse_handler;
+ std::function<void(const char*)> id_unhandled_handler;
+
+private:
+ void reset_escape_buffer(int ch,
+ const struct timeval& current_time,
+ ssize_t expected_size = -1);
+ void append_to_escape_buffer(int ch);
+
+ char id_escape_buffer[32];
+ ssize_t id_escape_index{0};
+ ssize_t id_escape_expected_size{-1};
+ struct timeval id_escape_start_time {
+ 0, 0
+ };
+};
+
+#endif
diff --git a/src/internals/README.md b/src/internals/README.md
new file mode 100644
index 0000000..d4a2187
--- /dev/null
+++ b/src/internals/README.md
@@ -0,0 +1,4 @@
+# Internals
+
+This directory contains documentation fragments that are generated from lnav's
+built-in help and schemas.
diff --git a/src/internals/cmd-ref.rst b/src/internals/cmd-ref.rst
new file mode 100644
index 0000000..8509286
--- /dev/null
+++ b/src/internals/cmd-ref.rst
@@ -0,0 +1,1628 @@
+
+.. _adjust_log_time:
+
+:adjust-log-time *timestamp*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Change the timestamps of the top file to be relative to the given date
+
+ **Parameters**
+ * **timestamp\*** --- The new timestamp for the top line in the view
+
+ **Examples**
+ To set the top timestamp to a given date:
+
+ .. code-block:: lnav
+
+ :adjust-log-time 2017-01-02T05:33:00
+
+ To set the top timestamp back an hour:
+
+ .. code-block:: lnav
+
+ :adjust-log-time -1h
+
+
+----
+
+
+.. _alt_msg:
+
+:alt-msg *msg*
+^^^^^^^^^^^^^^
+
+ Display a message in the alternate command position
+
+ **Parameters**
+ * **msg\*** --- The message to display
+
+ **Examples**
+ To display 'Press t to switch to the text view' on the bottom right:
+
+ .. code-block:: lnav
+
+ :alt-msg Press t to switch to the text view
+
+ **See Also**
+ :ref:`echo`, :ref:`eval`, :ref:`export_session_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_view_to`
+
+----
+
+
+.. _append_to:
+
+:append-to *path*
+^^^^^^^^^^^^^^^^^
+
+ Append marked lines in the current view to the given file
+
+ **Parameters**
+ * **path\*** --- The path to the file to append to
+
+ **Examples**
+ To append marked lines to the file /tmp/interesting-lines.txt:
+
+ .. code-block:: lnav
+
+ :append-to /tmp/interesting-lines.txt
+
+ **See Also**
+ :ref:`echo`, :ref:`echoln`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_view_to`
+
+----
+
+
+.. _clear_comment:
+
+:clear-comment
+^^^^^^^^^^^^^^
+
+ Clear the comment attached to the top log line
+
+ **See Also**
+ :ref:`comment`, :ref:`tag`
+
+----
+
+
+.. _clear_filter_expr:
+
+:clear-filter-expr
+^^^^^^^^^^^^^^^^^^
+
+ Clear the filter expression
+
+ **See Also**
+ :ref:`filter_expr`, :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`toggle_filtering`
+
+----
+
+
+.. _clear_highlight:
+
+:clear-highlight *pattern*
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Remove a previously set highlight regular expression
+
+ **Parameters**
+ * **pattern\*** --- The regular expression previously used with :highlight
+
+ **Examples**
+ To clear the highlight with the pattern 'foobar':
+
+ .. code-block:: lnav
+
+ :clear-highlight foobar
+
+ **See Also**
+ :ref:`enable_word_wrap`, :ref:`hide_fields`, :ref:`highlight`
+
+----
+
+
+.. _clear_mark_expr:
+
+:clear-mark-expr
+^^^^^^^^^^^^^^^^
+
+ Clear the mark expression
+
+ **See Also**
+ :ref:`hide_unmarked_lines`, :ref:`mark_expr`, :ref:`mark`, :ref:`next_mark`, :ref:`prev_mark`
+
+----
+
+
+.. _clear_partition:
+
+:clear-partition
+^^^^^^^^^^^^^^^^
+
+ Clear the partition the top line is a part of
+
+
+----
+
+
+.. _close:
+
+:close
+^^^^^^
+
+ Close the top file in the view
+
+
+----
+
+
+.. _comment:
+
+:comment *text*
+^^^^^^^^^^^^^^^
+
+ Attach a comment to the top log line. The comment will be displayed right below the log message it is associated with. The comment can be formatted using markdown and you can add new-lines with '\n'.
+
+ **Parameters**
+ * **text\*** --- The comment text
+
+ **Examples**
+ To add the comment 'This is where it all went wrong' to the top line:
+
+ .. code-block:: lnav
+
+ :comment This is where it all went wrong
+
+ **See Also**
+ :ref:`clear_comment`, :ref:`tag`
+
+----
+
+
+.. _config:
+
+:config *option* *\[value\]*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Read or write a configuration option
+
+ **Parameters**
+ * **option\*** --- The path to the option to read or write
+ * **value** --- The value to write. If not given, the current value is returned
+
+ **Examples**
+ To read the configuration of the '/ui/clock-format' option:
+
+ .. code-block:: lnav
+
+ :config /ui/clock-format
+
+ To set the '/ui/dim-text' option to 'false':
+
+ .. code-block:: lnav
+
+ :config /ui/dim-text false
+
+ **See Also**
+ :ref:`reset_config`
+
+----
+
+
+.. _create_logline_table:
+
+:create-logline-table *table-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Create an SQL table using the top line of the log view as a template
+
+ **Parameters**
+ * **table-name\*** --- The name for the new table
+
+ **Examples**
+ To create a logline-style table named 'task_durations':
+
+ .. code-block:: lnav
+
+ :create-logline-table task_durations
+
+ **See Also**
+ :ref:`create_search_table`, :ref:`create_search_table`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_view_to`
+
+----
+
+
+.. _create_search_table:
+
+:create-search-table *table-name* *\[pattern\]*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Create an SQL table based on a regex search
+
+ **Parameters**
+ * **table-name\*** --- The name of the table to create
+ * **pattern** --- The regular expression used to capture the table columns. If not given, the current search pattern is used.
+
+ **Examples**
+ To create a table named 'task_durations' that matches log messages with the pattern 'duration=(?<duration>\d+)':
+
+ .. code-block:: lnav
+
+ :create-search-table task_durations duration=(?<duration>\d+)
+
+ **See Also**
+ :ref:`create_logline_table`, :ref:`create_logline_table`, :ref:`delete_search_table`, :ref:`delete_search_table`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_view_to`
+
+----
+
+
+.. _current_time:
+
+:current-time
+^^^^^^^^^^^^^
+
+ Print the current time in human-readable form and seconds since the epoch
+
+
+----
+
+
+.. _delete_filter:
+
+:delete-filter *pattern*
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Delete the filter created with :filter-in or :filter-out
+
+ **Parameters**
+ * **pattern\*** --- The regular expression to match
+
+ **Examples**
+ To delete the filter with the pattern 'last message repeated':
+
+ .. code-block:: lnav
+
+ :delete-filter last message repeated
+
+ **See Also**
+ :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`toggle_filtering`
+
+----
+
+
+.. _delete_logline_table:
+
+:delete-logline-table *table-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Delete a table created with create-logline-table
+
+ **Parameters**
+ * **table-name\*** --- The name of the table to delete
+
+ **Examples**
+ To delete the logline-style table named 'task_durations':
+
+ .. code-block:: lnav
+
+ :delete-logline-table task_durations
+
+ **See Also**
+ :ref:`create_logline_table`, :ref:`create_logline_table`, :ref:`create_search_table`, :ref:`create_search_table`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_view_to`
+
+----
+
+
+.. _delete_search_table:
+
+:delete-search-table *table-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Create an SQL table based on a regex search
+
+ **Parameters**
+ * **table-name\*** --- The name of the table to create
+
+ **Examples**
+ To delete the search table named 'task_durations':
+
+ .. code-block:: lnav
+
+ :delete-search-table task_durations
+
+ **See Also**
+ :ref:`create_logline_table`, :ref:`create_logline_table`, :ref:`create_search_table`, :ref:`create_search_table`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_view_to`
+
+----
+
+
+.. _delete_tags:
+
+:delete-tags *tag*
+^^^^^^^^^^^^^^^^^^
+
+ Remove the given tags from all log lines
+
+ **Parameters**
+ * **tag** --- The tags to delete
+
+ **Examples**
+ To remove the tags '#BUG123' and '#needs-review' from all log lines:
+
+ .. code-block:: lnav
+
+ :delete-tags #BUG123 #needs-review
+
+ **See Also**
+ :ref:`comment`, :ref:`tag`
+
+----
+
+
+.. _disable_filter:
+
+:disable-filter *pattern*
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Disable a filter created with filter-in/filter-out
+
+ **Parameters**
+ * **pattern\*** --- The regular expression used in the filter command
+
+ **Examples**
+ To disable the filter with the pattern 'last message repeated':
+
+ .. code-block:: lnav
+
+ :disable-filter last message repeated
+
+ **See Also**
+ :ref:`enable_filter`, :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`toggle_filtering`
+
+----
+
+
+.. _disable_word_wrap:
+
+:disable-word-wrap
+^^^^^^^^^^^^^^^^^^
+
+ Disable word-wrapping for the current view
+
+ **See Also**
+ :ref:`enable_word_wrap`, :ref:`hide_fields`, :ref:`highlight`
+
+----
+
+
+.. _echo:
+
+:echo *\[-n\]* *msg*
+^^^^^^^^^^^^^^^^^^^^
+
+ Echo the given message to the screen or, if :redirect-to has been called, to output file specified in the redirect. Variable substitution is performed on the message. Use a backslash to escape any special characters, like '$'
+
+ **Parameters**
+ * **-n** --- Do not print a line-feed at the end of the output
+ * **msg\*** --- The message to display
+
+ **Examples**
+ To output 'Hello, World!':
+
+ .. code-block:: lnav
+
+ :echo Hello, World!
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _enable_filter:
+
+:enable-filter *pattern*
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Enable a previously created and disabled filter
+
+ **Parameters**
+ * **pattern\*** --- The regular expression used in the filter command
+
+ **Examples**
+ To enable the disabled filter with the pattern 'last message repeated':
+
+ .. code-block:: lnav
+
+ :enable-filter last message repeated
+
+ **See Also**
+ :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`toggle_filtering`
+
+----
+
+
+.. _enable_word_wrap:
+
+:enable-word-wrap
+^^^^^^^^^^^^^^^^^
+
+ Enable word-wrapping for the current view
+
+ **See Also**
+ :ref:`disable_word_wrap`, :ref:`hide_fields`, :ref:`highlight`
+
+----
+
+
+.. _eval:
+
+:eval *command*
+^^^^^^^^^^^^^^^
+
+ Evaluate the given command/query after doing environment variable substitution
+
+ **Parameters**
+ * **command\*** --- The command or query to perform substitution on.
+
+ **Examples**
+ To substitute the table name from a variable:
+
+ .. code-block:: lnav
+
+ :eval ;SELECT * FROM ${table}
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`echo`, :ref:`export_session_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_view_to`
+
+----
+
+
+.. _export_session_to:
+
+:export-session-to *path*
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Export the current lnav state to an executable lnav script file that contains the commands needed to restore the current session
+
+ **Parameters**
+ * **path\*** --- The path to the file to write
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _filter_expr:
+
+:filter-expr *expr*
+^^^^^^^^^^^^^^^^^^^
+
+ Set the filter expression
+
+ **Parameters**
+ * **expr\*** --- The SQL expression to evaluate for each log message. The message values can be accessed using column names prefixed with a colon
+
+ **Examples**
+ To set a filter expression that matched syslog messages from 'syslogd':
+
+ .. code-block:: lnav
+
+ :filter-expr :log_procname = 'syslogd'
+
+ To set a filter expression that matches log messages where 'id' is followed by a number and contains the string 'foo':
+
+ .. code-block:: lnav
+
+ :filter-expr :log_body REGEXP 'id\d+' AND :log_body REGEXP 'foo'
+
+ **See Also**
+ :ref:`clear_filter_expr`, :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`toggle_filtering`
+
+----
+
+
+.. _filter_in:
+
+:filter-in *pattern*
+^^^^^^^^^^^^^^^^^^^^
+
+ Only show lines that match the given regular expression in the current view
+
+ **Parameters**
+ * **pattern\*** --- The regular expression to match
+
+ **Examples**
+ To filter out log messages that do not have the string 'dhclient':
+
+ .. code-block:: lnav
+
+ :filter-in dhclient
+
+ **See Also**
+ :ref:`delete_filter`, :ref:`disable_filter`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`toggle_filtering`
+
+----
+
+
+.. _filter_out:
+
+:filter-out *pattern*
+^^^^^^^^^^^^^^^^^^^^^
+
+ Remove lines that match the given regular expression in the current view
+
+ **Parameters**
+ * **pattern\*** --- The regular expression to match
+
+ **Examples**
+ To filter out log messages that contain the string 'last message repeated':
+
+ .. code-block:: lnav
+
+ :filter-out last message repeated
+
+ **See Also**
+ :ref:`delete_filter`, :ref:`disable_filter`, :ref:`filter_in`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`toggle_filtering`
+
+----
+
+
+.. _goto:
+
+:goto *line#|N%|timestamp|#anchor*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Go to the given location in the top view
+
+ **Parameters**
+ * **line#|N%|timestamp|#anchor\*** --- A line number, percent into the file, timestamp, or an anchor in a text file
+
+ **Examples**
+ To go to line 22:
+
+ .. code-block:: lnav
+
+ :goto 22
+
+ To go to the line 75% of the way into the view:
+
+ .. code-block:: lnav
+
+ :goto 75%
+
+ To go to the first message on the first day of 2017:
+
+ .. code-block:: lnav
+
+ :goto 2017-01-01
+
+ To go to the Screenshots section:
+
+ .. code-block:: lnav
+
+ :goto #screenshots
+
+ **See Also**
+ :ref:`next_location`, :ref:`next_mark`, :ref:`prev_location`, :ref:`prev_mark`, :ref:`relative_goto`
+
+----
+
+
+.. _help:
+
+:help
+^^^^^
+
+ Open the help text view
+
+
+----
+
+
+.. _hide_fields:
+
+:hide-fields *field-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Hide log message fields by replacing them with an ellipsis
+
+ **Parameters**
+ * **field-name** --- The name of the field to hide in the format for the top log line. A qualified name can be used where the field name is prefixed by the format name and a dot to hide any field.
+
+ **Examples**
+ To hide the log_procname fields in all formats:
+
+ .. code-block:: lnav
+
+ :hide-fields log_procname
+
+ To hide only the log_procname field in the syslog format:
+
+ .. code-block:: lnav
+
+ :hide-fields syslog_log.log_procname
+
+ **See Also**
+ :ref:`enable_word_wrap`, :ref:`highlight`, :ref:`show_fields`
+
+----
+
+
+.. _hide_file:
+
+:hide-file *path*
+^^^^^^^^^^^^^^^^^
+
+ Hide the given file(s) and skip indexing until it is shown again. If no path is given, the current file in the view is hidden
+
+ **Parameters**
+ * **path** --- A path or glob pattern that specifies the files to hide
+
+
+----
+
+
+.. _hide_lines_after:
+
+:hide-lines-after *date*
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Hide lines that come after the given date
+
+ **Parameters**
+ * **date\*** --- An absolute or relative date
+
+ **Examples**
+ To hide the lines after the top line in the view:
+
+ .. code-block:: lnav
+
+ :hide-lines-after here
+
+ To hide the lines after 6 AM today:
+
+ .. code-block:: lnav
+
+ :hide-lines-after 6am
+
+ **See Also**
+ :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`show_lines_before_and_after`, :ref:`toggle_filtering`
+
+----
+
+
+.. _hide_lines_before:
+
+:hide-lines-before *date*
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Hide lines that come before the given date
+
+ **Parameters**
+ * **date\*** --- An absolute or relative date
+
+ **Examples**
+ To hide the lines before the top line in the view:
+
+ .. code-block:: lnav
+
+ :hide-lines-before here
+
+ To hide the log messages before 6 AM today:
+
+ .. code-block:: lnav
+
+ :hide-lines-before 6am
+
+ **See Also**
+ :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_unmarked_lines`, :ref:`show_lines_before_and_after`, :ref:`toggle_filtering`
+
+----
+
+
+.. _hide_unmarked_lines:
+
+:hide-unmarked-lines
+^^^^^^^^^^^^^^^^^^^^
+
+ Hide lines that have not been bookmarked
+
+ **See Also**
+ :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`mark`, :ref:`next_mark`, :ref:`prev_mark`, :ref:`toggle_filtering`
+
+----
+
+
+.. _highlight:
+
+:highlight *pattern*
+^^^^^^^^^^^^^^^^^^^^
+
+ Add coloring to log messages fragments that match the given regular expression
+
+ **Parameters**
+ * **pattern\*** --- The regular expression to match
+
+ **Examples**
+ To highlight numbers with three or more digits:
+
+ .. code-block:: lnav
+
+ :highlight \d{3,}
+
+ **See Also**
+ :ref:`clear_highlight`, :ref:`enable_word_wrap`, :ref:`hide_fields`
+
+----
+
+
+.. _load_session:
+
+:load-session
+^^^^^^^^^^^^^
+
+ Load the latest session state
+
+
+----
+
+
+.. _mark:
+
+:mark
+^^^^^
+
+ Toggle the bookmark state for the top line in the current view
+
+ **See Also**
+ :ref:`hide_unmarked_lines`, :ref:`next_mark`, :ref:`prev_mark`
+
+----
+
+
+.. _mark_expr:
+
+:mark-expr *expr*
+^^^^^^^^^^^^^^^^^
+
+ Set the bookmark expression
+
+ **Parameters**
+ * **expr\*** --- The SQL expression to evaluate for each log message. The message values can be accessed using column names prefixed with a colon
+
+ **Examples**
+ To mark lines from 'dhclient' that mention 'eth0':
+
+ .. code-block:: lnav
+
+ :mark-expr :log_procname = 'dhclient' AND :log_body LIKE '%eth0%'
+
+ **See Also**
+ :ref:`clear_mark_expr`, :ref:`hide_unmarked_lines`, :ref:`mark`, :ref:`next_mark`, :ref:`prev_mark`
+
+----
+
+
+.. _next_location:
+
+:next-location
+^^^^^^^^^^^^^^
+
+ Move to the next position in the location history
+
+ **See Also**
+ :ref:`goto`, :ref:`next_mark`, :ref:`prev_location`, :ref:`prev_mark`, :ref:`relative_goto`
+
+----
+
+
+.. _next_mark:
+
+:next-mark *type*
+^^^^^^^^^^^^^^^^^
+
+ Move to the next bookmark of the given type in the current view
+
+ **Parameters**
+ * **type** --- The type of bookmark -- error, warning, search, user, file, meta
+
+ **Examples**
+ To go to the next error:
+
+ .. code-block:: lnav
+
+ :next-mark error
+
+ **See Also**
+ :ref:`goto`, :ref:`hide_unmarked_lines`, :ref:`mark`, :ref:`next_location`, :ref:`prev_location`, :ref:`prev_mark`, :ref:`prev_mark`, :ref:`relative_goto`
+
+----
+
+
+.. _open:
+
+:open *path*
+^^^^^^^^^^^^
+
+ Open the given file(s) in lnav. Opening files on machines accessible via SSH can be done using the syntax: [user@]host:/path/to/logs
+
+ **Parameters**
+ * **path** --- The path to the file to open
+
+ **Examples**
+ To open the file '/path/to/file':
+
+ .. code-block:: lnav
+
+ :open /path/to/file
+
+ To open the remote file '/var/log/syslog.log':
+
+ .. code-block:: lnav
+
+ :open dean@host1.example.com:/var/log/syslog.log
+
+
+----
+
+
+.. _partition_name:
+
+:partition-name *name*
+^^^^^^^^^^^^^^^^^^^^^^
+
+ Mark the top line in the log view as the start of a new partition with the given name
+
+ **Parameters**
+ * **name\*** --- The name for the new partition
+
+ **Examples**
+ To mark the top line as the start of the partition named 'boot #1':
+
+ .. code-block:: lnav
+
+ :partition-name boot #1
+
+
+----
+
+
+.. _pipe_line_to:
+
+:pipe-line-to *shell-cmd*
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Pipe the top line to the given shell command
+
+ **Parameters**
+ * **shell-cmd\*** --- The shell command-line to execute
+
+ **Examples**
+ To write the top line to 'sed' for processing:
+
+ .. code-block:: lnav
+
+ :pipe-line-to sed -e 's/foo/bar/g'
+
+ **See Also**
+ :ref:`append_to`, :ref:`echo`, :ref:`echoln`, :ref:`export_session_to`, :ref:`pipe_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_view_to`
+
+----
+
+
+.. _pipe_to:
+
+:pipe-to *shell-cmd*
+^^^^^^^^^^^^^^^^^^^^
+
+ Pipe the marked lines to the given shell command
+
+ **Parameters**
+ * **shell-cmd\*** --- The shell command-line to execute
+
+ **Examples**
+ To write marked lines to 'sed' for processing:
+
+ .. code-block:: lnav
+
+ :pipe-to sed -e s/foo/bar/g
+
+ **See Also**
+ :ref:`append_to`, :ref:`echo`, :ref:`echoln`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_view_to`
+
+----
+
+
+.. _prev_location:
+
+:prev-location
+^^^^^^^^^^^^^^
+
+ Move to the previous position in the location history
+
+ **See Also**
+ :ref:`goto`, :ref:`next_location`, :ref:`next_mark`, :ref:`prev_mark`, :ref:`relative_goto`
+
+----
+
+
+.. _prev_mark:
+
+:prev-mark *type*
+^^^^^^^^^^^^^^^^^
+
+ Move to the previous bookmark of the given type in the current view
+
+ **Parameters**
+ * **type** --- The type of bookmark -- error, warning, search, user, file, meta
+
+ **Examples**
+ To go to the previous error:
+
+ .. code-block:: lnav
+
+ :prev-mark error
+
+ **See Also**
+ :ref:`goto`, :ref:`hide_unmarked_lines`, :ref:`mark`, :ref:`next_location`, :ref:`next_mark`, :ref:`next_mark`, :ref:`prev_location`, :ref:`relative_goto`
+
+----
+
+
+.. _prompt:
+
+:prompt *type* *\[--alt\]* *\[prompt\]* *\[initial-value\]*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Open the given prompt
+
+ **Parameters**
+ * **type\*** --- The type of prompt -- command, script, search, sql, user
+ * **--alt** --- Perform the alternate action for this prompt by default
+ * **prompt** --- The prompt to display
+ * **initial-value** --- The initial value to fill in for the prompt
+
+ **Examples**
+ To open the command prompt with 'filter-in' already filled in:
+
+ .. code-block:: lnav
+
+ :prompt command : 'filter-in '
+
+ To ask the user a question:
+
+ .. code-block:: lnav
+
+ :prompt user 'Are you sure? '
+
+
+----
+
+
+.. _quit:
+
+:quit
+^^^^^
+
+ Quit lnav
+
+
+----
+
+
+.. _rebuild:
+
+:rebuild
+^^^^^^^^
+
+ Forcefully rebuild file indexes
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`echo`, :ref:`eval`, :ref:`export_session_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_view_to`
+
+----
+
+
+.. _redirect_to:
+
+:redirect-to *\[path\]*
+^^^^^^^^^^^^^^^^^^^^^^^
+
+ Redirect the output of commands that write to stdout to the given file
+
+ **Parameters**
+ * **path** --- The path to the file to write. If not specified, the current redirect will be cleared
+
+ **Examples**
+ To write the output of lnav commands to the file /tmp/script-output.txt:
+
+ .. code-block:: lnav
+
+ :redirect-to /tmp/script-output.txt
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _redraw:
+
+:redraw
+^^^^^^^
+
+ Do a full redraw of the screen
+
+
+----
+
+
+.. _relative_goto:
+
+:relative-goto *line-count|N%*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Move the current view up or down by the given amount
+
+ **Parameters**
+ * **line-count|N%\*** --- The amount to move the view by.
+
+ **Examples**
+ To move 22 lines down in the view:
+
+ .. code-block:: lnav
+
+ :relative-goto +22
+
+ To move 10 percent back in the view:
+
+ .. code-block:: lnav
+
+ :relative-goto -10%
+
+ **See Also**
+ :ref:`goto`, :ref:`next_location`, :ref:`next_mark`, :ref:`prev_location`, :ref:`prev_mark`
+
+----
+
+
+.. _reset_config:
+
+:reset-config *option*
+^^^^^^^^^^^^^^^^^^^^^^
+
+ Reset the configuration option to its default value
+
+ **Parameters**
+ * **option\*** --- The path to the option to reset
+
+ **Examples**
+ To reset the '/ui/clock-format' option back to the builtin default:
+
+ .. code-block:: lnav
+
+ :reset-config /ui/clock-format
+
+ **See Also**
+ :ref:`config`
+
+----
+
+
+.. _reset_session:
+
+:reset-session
+^^^^^^^^^^^^^^
+
+ Reset the session state, clearing all filters, highlights, and bookmarks
+
+
+----
+
+
+.. _save_session:
+
+:save-session
+^^^^^^^^^^^^^
+
+ Save the current state as a session
+
+
+----
+
+
+.. _session:
+
+:session *lnav-command*
+^^^^^^^^^^^^^^^^^^^^^^^
+
+ Add the given command to the session file (~/.lnav/session)
+
+ **Parameters**
+ * **lnav-command\*** --- The lnav command to save.
+
+ **Examples**
+ To add the command ':highlight foobar' to the session file:
+
+ .. code-block:: lnav
+
+ :session :highlight foobar
+
+
+----
+
+
+.. _set_min_log_level:
+
+:set-min-log-level *log-level*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Set the minimum log level to display in the log view
+
+ **Parameters**
+ * **log-level\*** --- The new minimum log level
+
+ **Examples**
+ To set the minimum log level displayed to error:
+
+ .. code-block:: lnav
+
+ :set-min-log-level error
+
+
+----
+
+
+.. _show_fields:
+
+:show-fields *field-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Show log message fields that were previously hidden
+
+ **Parameters**
+ * **field-name** --- The name of the field to show
+
+ **Examples**
+ To show all the log_procname fields in all formats:
+
+ .. code-block:: lnav
+
+ :show-fields log_procname
+
+ **See Also**
+ :ref:`enable_word_wrap`, :ref:`hide_fields`, :ref:`highlight`
+
+----
+
+
+.. _show_file:
+
+:show-file *path*
+^^^^^^^^^^^^^^^^^
+
+ Show the given file(s) and resume indexing.
+
+ **Parameters**
+ * **path** --- The path or glob pattern that specifies the files to show
+
+
+----
+
+
+.. _show_lines_before_and_after:
+
+:show-lines-before-and-after
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Show lines that were hidden by the 'hide-lines' commands
+
+ **See Also**
+ :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`toggle_filtering`
+
+----
+
+
+.. _show_only_this_file:
+
+:show-only-this-file
+^^^^^^^^^^^^^^^^^^^^
+
+ Show only the file for the top line in the view
+
+
+----
+
+
+.. _show_unmarked_lines:
+
+:show-unmarked-lines
+^^^^^^^^^^^^^^^^^^^^
+
+ Show lines that have not been bookmarked
+
+ **See Also**
+ :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`, :ref:`hide_unmarked_lines`, :ref:`mark`, :ref:`next_mark`, :ref:`prev_mark`, :ref:`toggle_filtering`
+
+----
+
+
+.. _spectrogram:
+
+:spectrogram *field-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Visualize the given message field or database column using a spectrogram
+
+ **Parameters**
+ * **field-name\*** --- The name of the numeric field to visualize.
+
+ **Examples**
+ To visualize the sc_bytes field in the access_log format:
+
+ .. code-block:: lnav
+
+ :spectrogram sc_bytes
+
+
+----
+
+
+.. _summarize:
+
+:summarize *column-name*
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Execute a SQL query that computes the characteristics of the values in the given column
+
+ **Parameters**
+ * **column-name\*** --- The name of the column to analyze.
+
+ **Examples**
+ To get a summary of the sc_bytes column in the access_log table:
+
+ .. code-block:: lnav
+
+ :summarize sc_bytes
+
+
+----
+
+
+.. _switch_to_view:
+
+:switch-to-view *view-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Switch to the given view
+
+ **Parameters**
+ * **view-name\*** --- The name of the view to switch to.
+
+ **Examples**
+ To switch to the 'schema' view:
+
+ .. code-block:: lnav
+
+ :switch-to-view schema
+
+
+----
+
+
+.. _tag:
+
+:tag *tag*
+^^^^^^^^^^
+
+ Attach tags to the top log line
+
+ **Parameters**
+ * **tag** --- The tags to attach
+
+ **Examples**
+ To add the tags '#BUG123' and '#needs-review' to the top line:
+
+ .. code-block:: lnav
+
+ :tag #BUG123 #needs-review
+
+ **See Also**
+ :ref:`comment`, :ref:`delete_tags`, :ref:`untag`
+
+----
+
+
+.. _toggle_filtering:
+
+:toggle-filtering
+^^^^^^^^^^^^^^^^^
+
+ Toggle the filtering flag for the current view
+
+ **See Also**
+ :ref:`filter_in`, :ref:`filter_out`, :ref:`hide_lines_after`, :ref:`hide_lines_before`, :ref:`hide_unmarked_lines`
+
+----
+
+
+.. _toggle_view:
+
+:toggle-view *view-name*
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Switch to the given view or, if it is already displayed, switch to the previous view
+
+ **Parameters**
+ * **view-name\*** --- The name of the view to toggle the display of.
+
+ **Examples**
+ To switch to the 'schema' view if it is not displayed or switch back to the previous view:
+
+ .. code-block:: lnav
+
+ :toggle-view schema
+
+
+----
+
+
+.. _unix_time:
+
+:unix-time *seconds*
+^^^^^^^^^^^^^^^^^^^^
+
+ Convert epoch time to a human-readable form
+
+ **Parameters**
+ * **seconds\*** --- The epoch timestamp to convert
+
+ **Examples**
+ To convert the epoch time 1490191111:
+
+ .. code-block:: lnav
+
+ :unix-time 1490191111
+
+
+----
+
+
+.. _untag:
+
+:untag *tag*
+^^^^^^^^^^^^
+
+ Detach tags from the top log line
+
+ **Parameters**
+ * **tag** --- The tags to detach
+
+ **Examples**
+ To remove the tags '#BUG123' and '#needs-review' from the top line:
+
+ .. code-block:: lnav
+
+ :untag #BUG123 #needs-review
+
+ **See Also**
+ :ref:`comment`, :ref:`tag`
+
+----
+
+
+.. _write_table_to:
+
+:write-table-to *\[--anonymize\]* *path*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Write SQL results to the given file in a tabular format
+
+ **Parameters**
+ * **--anonymize** --- Anonymize the table contents
+ * **path\*** --- The path to the file to write
+
+ **Examples**
+ To write SQL results as text to /tmp/table.txt:
+
+ .. code-block:: lnav
+
+ :write-table-to /tmp/table.txt
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`create_logline_table`, :ref:`create_search_table`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_to`, :ref:`write_to`, :ref:`write_view_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _write_csv_to:
+
+:write-csv-to *\[--anonymize\]* *path*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Write SQL results to the given file in CSV format
+
+ **Parameters**
+ * **--anonymize** --- Anonymize the row contents
+ * **path\*** --- The path to the file to write
+
+ **Examples**
+ To write SQL results as CSV to /tmp/table.csv:
+
+ .. code-block:: lnav
+
+ :write-csv-to /tmp/table.csv
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`create_logline_table`, :ref:`create_search_table`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_to`, :ref:`write_view_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _write_json_to:
+
+:write-json-to *\[--anonymize\]* *path*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Write SQL results to the given file in JSON format
+
+ **Parameters**
+ * **--anonymize** --- Anonymize the JSON values
+ * **path\*** --- The path to the file to write
+
+ **Examples**
+ To write SQL results as JSON to /tmp/table.json:
+
+ .. code-block:: lnav
+
+ :write-json-to /tmp/table.json
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`create_logline_table`, :ref:`create_search_table`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_to`, :ref:`write_view_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _write_jsonlines_to:
+
+:write-jsonlines-to *\[--anonymize\]* *path*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Write SQL results to the given file in JSON Lines format
+
+ **Parameters**
+ * **--anonymize** --- Anonymize the JSON values
+ * **path\*** --- The path to the file to write
+
+ **Examples**
+ To write SQL results as JSON Lines to /tmp/table.json:
+
+ .. code-block:: lnav
+
+ :write-jsonlines-to /tmp/table.json
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`create_logline_table`, :ref:`create_search_table`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_to`, :ref:`write_view_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _write_raw_to:
+
+:write-raw-to *\[--view={log,db}\]* *\[--anonymize\]* *path*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ In the log view, write the original log file content of the marked messages to the file. In the DB view, the contents of the cells are written to the output file.
+
+ **Parameters**
+ * **--view={log,db}** --- The view to use as the source of data
+ * **--anonymize** --- Anonymize the lines
+ * **path\*** --- The path to the file to write
+
+ **Examples**
+ To write the marked lines in the log view to /tmp/table.txt:
+
+ .. code-block:: lnav
+
+ :write-raw-to /tmp/table.txt
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`create_logline_table`, :ref:`create_search_table`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_to`, :ref:`write_view_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _write_screen_to:
+
+:write-screen-to *\[--anonymize\]* *path*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Write the displayed text or SQL results to the given file without any formatting
+
+ **Parameters**
+ * **--anonymize** --- Anonymize the lines
+ * **path\*** --- The path to the file to write
+
+ **Examples**
+ To write only the displayed text to /tmp/table.txt:
+
+ .. code-block:: lnav
+
+ :write-screen-to /tmp/table.txt
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`create_logline_table`, :ref:`create_search_table`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_to`, :ref:`write_view_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _write_to:
+
+:write-to *\[--anonymize\]* *path*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Overwrite the given file with any marked lines in the current view
+
+ **Parameters**
+ * **--anonymize** --- Anonymize the lines
+ * **path\*** --- The path to the file to write
+
+ **Examples**
+ To write marked lines to the file /tmp/interesting-lines.txt:
+
+ .. code-block:: lnav
+
+ :write-to /tmp/interesting-lines.txt
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_view_to`, :ref:`write_view_to`
+
+----
+
+
+.. _write_view_to:
+
+:write-view-to *\[--anonymize\]* *path*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Write the text in the top view to the given file without any formatting
+
+ **Parameters**
+ * **--anonymize** --- Anonymize the lines
+ * **path\*** --- The path to the file to write
+
+ **Examples**
+ To write the top view to /tmp/table.txt:
+
+ .. code-block:: lnav
+
+ :write-view-to /tmp/table.txt
+
+ **See Also**
+ :ref:`alt_msg`, :ref:`append_to`, :ref:`create_logline_table`, :ref:`create_search_table`, :ref:`echo`, :ref:`echo`, :ref:`echoln`, :ref:`eval`, :ref:`export_session_to`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`rebuild`, :ref:`redirect_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_to`
+
+----
+
+
+.. _zoom_to:
+
+:zoom-to *zoom-level*
+^^^^^^^^^^^^^^^^^^^^^
+
+ Zoom the histogram view to the given level
+
+ **Parameters**
+ * **zoom-level\*** --- The zoom level
+
+ **Examples**
+ To set the zoom level to '1-week':
+
+ .. code-block:: lnav
+
+ :zoom-to 1-week
+
+
+----
+
diff --git a/src/internals/sql-ref.rst b/src/internals/sql-ref.rst
new file mode 100644
index 0000000..bf1e0a6
--- /dev/null
+++ b/src/internals/sql-ref.rst
@@ -0,0 +1,3850 @@
+
+.. _infix_between_and:
+
+expr *\[NOT\]* BETWEEN *low* AND *hi*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Test if an expression is between two values.
+
+ **Parameters**
+ * **low\*** --- The low point
+ * **hi\*** --- The high point
+
+ **Examples**
+ To check if 3 is between 5 and 10:
+
+ .. code-block:: custsqlite
+
+ ;SELECT 3 BETWEEN 5 AND 10
+ 0
+
+ To check if 10 is between 5 and 10:
+
+ .. code-block:: custsqlite
+
+ ;SELECT 10 BETWEEN 5 AND 10
+ 1
+
+
+----
+
+
+.. _attach:
+
+ATTACH DATABASE *filename* AS *schema-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Attach a database file to the current connection.
+
+ **Parameters**
+ * **filename\*** --- The path to the database file.
+ * **schema-name\*** --- The prefix for tables in this database.
+
+ **Examples**
+ To attach the database file '/tmp/customers.db' with the name customers:
+
+ .. code-block:: custsqlite
+
+ ;ATTACH DATABASE '/tmp/customers.db' AS customers
+
+
+----
+
+
+.. _create_view:
+
+CREATE *\[TEMP\]* VIEW *\[IF NOT EXISTS\]* *\[schema-name.\]* *view-name* AS *select-stmt*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Assign a name to a SELECT statement
+
+ **Parameters**
+ * **IF NOT EXISTS** --- Do not create the view if it already exists
+ * **schema-name.** --- The database to create the view in
+ * **view-name\*** --- The name of the view
+ * **select-stmt\*** --- The SELECT statement the view represents
+
+
+----
+
+
+.. _create_table:
+
+CREATE *\[TEMP\]* TABLE *\[IF NOT EXISTS\]* *\[schema-name.\]* *table-name* AS *select-stmt*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Create a table
+
+
+----
+
+
+.. _with_recursive:
+
+WITH RECURSIVE *cte-table-name* AS *select-stmt*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Create a temporary view that exists only for the duration of a SQL statement.
+
+ **Parameters**
+ * **cte-table-name\*** --- The name for the temporary table.
+ * **select-stmt\*** --- The SELECT statement used to populate the temporary table.
+
+
+----
+
+
+.. _cast:
+
+CAST(*expr* AS *type-name*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Convert the value of the given expression to a different storage class specified by type-name.
+
+ **Parameters**
+ * **expr\*** --- The value to convert.
+ * **type-name\*** --- The name of the type to convert to.
+
+ **Examples**
+ To cast the value 1.23 as an integer:
+
+ .. code-block:: custsqlite
+
+ ;SELECT CAST(1.23 AS INTEGER)
+ 1
+
+
+----
+
+
+.. _case_end:
+
+CASE *\[base-expr\]* WHEN *cmp-expr* ELSE *\[else-expr\]* END
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Evaluate a series of expressions in order until one evaluates to true and then return it's result. Similar to an IF-THEN-ELSE construct in other languages.
+
+ **Parameters**
+ * **base-expr** --- The base expression that is used for comparison in the branches
+ * **cmp-expr** --- The expression to test if this branch should be taken
+ * **else-expr** --- The result of this CASE if no branches matched.
+
+ **Examples**
+ To evaluate the number one and return the string 'one':
+
+ .. code-block:: custsqlite
+
+ ;SELECT CASE 1 WHEN 0 THEN 'zero' WHEN 1 THEN 'one' END
+ one
+
+
+----
+
+
+.. _infix_collate:
+
+expr COLLATE *collation-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Assign a collating sequence to the expression.
+
+ **Parameters**
+ * **collation-name\*** --- The name of the collator.
+
+ **Examples**
+ To change the collation method for string comparisons:
+
+ .. code-block:: custsqlite
+
+ ;SELECT ('a2' < 'a10'), ('a2' < 'a10' COLLATE naturalnocase)
+ ('a2' < 'a10') ('a2' < 'a10' COLLATE naturalnocase)
+ 0 1
+
+
+----
+
+
+.. _detach:
+
+DETACH DATABASE *schema-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Detach a database from the current connection.
+
+ **Parameters**
+ * **schema-name\*** --- The prefix for tables in this database.
+
+ **Examples**
+ To detach the database named 'customers':
+
+ .. code-block:: custsqlite
+
+ ;DETACH DATABASE customers
+
+
+----
+
+
+.. _delete:
+
+DELETE FROM *table-name* WHERE *\[cond\]*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Delete rows from a table
+
+ **Parameters**
+ * **table-name\*** --- The name of the table
+ * **cond** --- The conditions used to delete the rows.
+
+
+----
+
+
+.. _drop_index:
+
+DROP INDEX *\[IF EXISTS\]* *\[schema-name.\]* *index-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Drop an index
+
+
+----
+
+
+.. _drop_table:
+
+DROP TABLE *\[IF EXISTS\]* *\[schema-name.\]* *table-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Drop a table
+
+
+----
+
+
+.. _drop_view:
+
+DROP VIEW *\[IF EXISTS\]* *\[schema-name.\]* *view-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Drop a view
+
+
+----
+
+
+.. _drop_trigger:
+
+DROP TRIGGER *\[IF EXISTS\]* *\[schema-name.\]* *trigger-name*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Drop a trigger
+
+
+----
+
+
+.. _infix_glob:
+
+expr *\[NOT\]* GLOB *pattern*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Match an expression against a glob pattern.
+
+ **Parameters**
+ * **pattern\*** --- The glob pattern to match against.
+
+ **Examples**
+ To check if a value matches the pattern '*.log':
+
+ .. code-block:: custsqlite
+
+ ;SELECT 'foobar.log' GLOB '*.log'
+ 1
+
+
+----
+
+
+.. _infix_like:
+
+expr *\[NOT\]* LIKE *pattern*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Match an expression against a text pattern.
+
+ **Parameters**
+ * **pattern\*** --- The pattern to match against.
+
+ **Examples**
+ To check if a value matches the pattern 'Hello, %!':
+
+ .. code-block:: custsqlite
+
+ ;SELECT 'Hello, World!' LIKE 'Hello, %!'
+ 1
+
+
+----
+
+
+.. _infix_regexp:
+
+expr *\[NOT\]* REGEXP *pattern*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Match an expression against a regular expression.
+
+ **Parameters**
+ * **pattern\*** --- The regular expression to match against.
+
+ **Examples**
+ To check if a value matches the pattern 'file-\d+':
+
+ .. code-block:: custsqlite
+
+ ;SELECT 'file-23' REGEXP 'file-\d+'
+ 1
+
+
+----
+
+
+.. _select:
+
+SELECT *result-column* FROM *table* WHERE *\[cond\]* GROUP BY *grouping-expr* ORDER BY *ordering-term* LIMIT *limit-expr*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Query the database and return zero or more rows of data.
+
+ **Parameters**
+ * **result-column** --- The expression used to generate a result for this column.
+ * **table** --- The table(s) to query for data
+ * **cond** --- The conditions used to select the rows to return.
+ * **grouping-expr** --- The expression to use when grouping rows.
+ * **ordering-term** --- The values to use when ordering the result set.
+ * **limit-expr** --- The maximum number of rows to return.
+
+ **Examples**
+ To select all of the columns from the table 'syslog_log':
+
+ .. code-block:: custsqlite
+
+ ;SELECT * FROM syslog_log
+
+
+----
+
+
+.. _insert_into:
+
+INSERT INTO *\[schema-name.\]* *table-name* *column-name* VALUES *expr*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Insert rows into a table
+
+ **Examples**
+ To insert the pair containing 'MSG' and 'HELLO, WORLD!' into the 'environ' table:
+
+ .. code-block:: custsqlite
+
+ ;INSERT INTO environ VALUES ('MSG', 'HELLO, WORLD!')
+
+
+----
+
+
+.. _over:
+
+OVER(*\[base-window-name\]* PARTITION BY *expr* ORDER BY *expr*, *\[frame-spec\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Executes the preceding function over a window
+
+ **Parameters**
+ * **base-window-name** --- The name of the window definition
+ * **expr** --- The values to use for partitioning
+ * **expr** --- The values used to order the rows in the window
+ * **frame-spec** --- Determines which output rows are read by an aggregate window function
+
+
+----
+
+
+.. _over:
+
+OVER *window-name*
+^^^^^^^^^^^^^^^^^^
+
+ Executes the preceding function over a window
+
+ **Parameters**
+ * **window-name\*** --- The name of the window definition
+
+
+----
+
+
+.. _update_set:
+
+UPDATE *table* SET *column-name* WHERE *\[cond\]*
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Modify a subset of values in zero or more rows of the given table
+
+ **Parameters**
+ * **table\*** --- The table to update
+ * **column-name** --- The columns in the table to update.
+ * **cond** --- The condition used to determine whether a row should be updated.
+
+ **Examples**
+ To mark the syslog message at line 40:
+
+ .. code-block:: custsqlite
+
+ ;UPDATE syslog_log SET log_mark = 1 WHERE log_line = 40
+
+
+----
+
+
+.. _abs:
+
+abs(*x*)
+^^^^^^^^
+
+ Return the absolute value of the argument
+
+ **Parameters**
+ * **x\*** --- The number to convert
+
+ **Examples**
+ To get the absolute value of -1:
+
+ .. code-block:: custsqlite
+
+ ;SELECT abs(-1)
+ 1
+
+ **See Also**
+ :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _acos:
+
+acos(*num*)
+^^^^^^^^^^^
+
+ Returns the arccosine of a number, in radians
+
+ **Parameters**
+ * **num\*** --- A cosine value that is between -1 and 1
+
+ **Examples**
+ To get the arccosine of 0.2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT acos(0.2)
+ 1.3694384060045657
+
+ **See Also**
+ :ref:`abs`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _acosh:
+
+acosh(*num*)
+^^^^^^^^^^^^
+
+ Returns the hyperbolic arccosine of a number
+
+ **Parameters**
+ * **num\*** --- A number that is one or more
+
+ **Examples**
+ To get the hyperbolic arccosine of 1.2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT acosh(1.2)
+ 0.6223625037147786
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _anonymize:
+
+anonymize(*value*)
+^^^^^^^^^^^^^^^^^^
+
+ Replace identifying information with random values.
+
+ **Parameters**
+ * **value\*** --- The text to anonymize
+
+ **Examples**
+ To anonymize an IP address:
+
+ .. code-block:: custsqlite
+
+ ;SELECT anonymize('Hello, 192.168.1.2')
+ Aback, 10.0.0.1
+
+ **See Also**
+ :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _asin:
+
+asin(*num*)
+^^^^^^^^^^^
+
+ Returns the arcsine of a number, in radians
+
+ **Parameters**
+ * **num\*** --- A sine value that is between -1 and 1
+
+ **Examples**
+ To get the arcsine of 0.2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT asin(0.2)
+ 0.2013579207903308
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _asinh:
+
+asinh(*num*)
+^^^^^^^^^^^^
+
+ Returns the hyperbolic arcsine of a number
+
+ **Parameters**
+ * **num\*** --- The number
+
+ **Examples**
+ To get the hyperbolic arcsine of 0.2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT asinh(0.2)
+ 0.19869011034924142
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _atan:
+
+atan(*num*)
+^^^^^^^^^^^
+
+ Returns the arctangent of a number, in radians
+
+ **Parameters**
+ * **num\*** --- The number
+
+ **Examples**
+ To get the arctangent of 0.2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT atan(0.2)
+ 0.19739555984988078
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _atan2:
+
+atan2(*y*, *x*)
+^^^^^^^^^^^^^^^
+
+ Returns the angle in the plane between the positive X axis and the ray from (0, 0) to the point (x, y)
+
+ **Parameters**
+ * **y\*** --- The y coordinate of the point
+ * **x\*** --- The x coordinate of the point
+
+ **Examples**
+ To get the angle, in degrees, for the point at (5, 5):
+
+ .. code-block:: custsqlite
+
+ ;SELECT degrees(atan2(5, 5))
+ 45
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _atanh:
+
+atanh(*num*)
+^^^^^^^^^^^^
+
+ Returns the hyperbolic arctangent of a number
+
+ **Parameters**
+ * **num\*** --- The number
+
+ **Examples**
+ To get the hyperbolic arctangent of 0.2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT atanh(0.2)
+ 0.2027325540540822
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _atn2:
+
+atn2(*y*, *x*)
+^^^^^^^^^^^^^^
+
+ Returns the angle in the plane between the positive X axis and the ray from (0, 0) to the point (x, y)
+
+ **Parameters**
+ * **y\*** --- The y coordinate of the point
+ * **x\*** --- The x coordinate of the point
+
+ **Examples**
+ To get the angle, in degrees, for the point at (5, 5):
+
+ .. code-block:: custsqlite
+
+ ;SELECT degrees(atn2(5, 5))
+ 45
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _avg:
+
+avg(*X*)
+^^^^^^^^
+
+ Returns the average value of all non-NULL numbers within a group.
+
+ **Parameters**
+ * **X\*** --- The value to compute the average of.
+
+ **Examples**
+ To get the average of the column 'ex_duration' from the table 'lnav_example_log':
+
+ .. code-block:: custsqlite
+
+ ;SELECT avg(ex_duration) FROM lnav_example_log
+ 4.25
+
+ To get the average of the column 'ex_duration' from the table 'lnav_example_log' when grouped by 'ex_procname':
+
+ .. code-block:: custsqlite
+
+ ;SELECT ex_procname, avg(ex_duration) FROM lnav_example_log GROUP BY ex_procname
+ ex_procname avg(ex_duration)
+ gw 5
+ hw 2
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _basename:
+
+basename(*path*)
+^^^^^^^^^^^^^^^^
+
+ Extract the base portion of a pathname.
+
+ **Parameters**
+ * **path\*** --- The path
+
+ **Examples**
+ To get the base of a plain file name:
+
+ .. code-block:: custsqlite
+
+ ;SELECT basename('foobar')
+ foobar
+
+ To get the base of a path:
+
+ .. code-block:: custsqlite
+
+ ;SELECT basename('foo/bar')
+ bar
+
+ To get the base of a directory:
+
+ .. code-block:: custsqlite
+
+ ;SELECT basename('foo/bar/')
+ bar
+
+ To get the base of an empty string:
+
+ .. code-block:: custsqlite
+
+ ;SELECT basename('')
+ .
+
+ To get the base of a Windows path:
+
+ .. code-block:: custsqlite
+
+ ;SELECT basename('foo\bar')
+ bar
+
+ To get the base of the root directory:
+
+ .. code-block:: custsqlite
+
+ ;SELECT basename('/')
+ /
+
+ **See Also**
+ :ref:`dirname`, :ref:`joinpath`, :ref:`readlink`, :ref:`realpath`
+
+----
+
+
+.. _ceil:
+
+ceil(*num*)
+^^^^^^^^^^^
+
+ Returns the smallest integer that is not less than the argument
+
+ **Parameters**
+ * **num\*** --- The number to raise to the ceiling
+
+ **Examples**
+ To get the ceiling of 1.23:
+
+ .. code-block:: custsqlite
+
+ ;SELECT ceil(1.23)
+ 2
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _changes:
+
+changes()
+^^^^^^^^^
+
+ The number of database rows that were changed, inserted, or deleted by the most recent statement.
+
+
+----
+
+
+.. _char:
+
+char(*X*)
+^^^^^^^^^
+
+ Returns a string composed of characters having the given unicode code point values
+
+ **Parameters**
+ * **X** --- The unicode code point values
+
+ **Examples**
+ To get a string with the code points 0x48 and 0x49:
+
+ .. code-block:: custsqlite
+
+ ;SELECT char(0x48, 0x49)
+ HI
+
+ **See Also**
+ :ref:`anonymize`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _charindex:
+
+charindex(*needle*, *haystack*, *\[start\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Finds the first occurrence of the needle within the haystack and returns the number of prior characters plus 1, or 0 if Y is nowhere found within X
+
+ **Parameters**
+ * **needle\*** --- The string to look for in the haystack
+ * **haystack\*** --- The string to search within
+ * **start** --- The one-based index within the haystack to start the search
+
+ **Examples**
+ To search for the string 'abc' within 'abcabc' and starting at position 2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT charindex('abc', 'abcabc', 2)
+ 4
+
+ To search for the string 'abc' within 'abcdef' and starting at position 2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT charindex('abc', 'abcdef', 2)
+ 0
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _coalesce:
+
+coalesce(*X*, *Y*)
+^^^^^^^^^^^^^^^^^^
+
+ Returns a copy of its first non-NULL argument, or NULL if all arguments are NULL
+
+ **Parameters**
+ * **X\*** --- A value to check for NULL-ness
+ * **Y** --- A value to check for NULL-ness
+
+ **Examples**
+ To get the first non-null value from three parameters:
+
+ .. code-block:: custsqlite
+
+ ;SELECT coalesce(null, 0, null)
+ 0
+
+
+----
+
+
+.. _count:
+
+count(*X*)
+^^^^^^^^^^
+
+ If the argument is '*', the total number of rows in the group is returned. Otherwise, the number of times the argument is non-NULL.
+
+ **Parameters**
+ * **X\*** --- The value to count.
+
+ **Examples**
+ To get the count of the non-NULL rows of 'lnav_example_log':
+
+ .. code-block:: custsqlite
+
+ ;SELECT count(*) FROM lnav_example_log
+ 4
+
+ To get the count of the non-NULL values of 'log_part' from 'lnav_example_log':
+
+ .. code-block:: custsqlite
+
+ ;SELECT count(log_part) FROM lnav_example_log
+ 2
+
+
+----
+
+
+.. _cume_dist:
+
+cume_dist()
+^^^^^^^^^^^
+
+ Returns the cumulative distribution
+
+ **See Also**
+ :ref:`dense_rank`, :ref:`first_value`, :ref:`lag`, :ref:`last_value`, :ref:`lead`, :ref:`nth_value`, :ref:`ntile`, :ref:`percent_rank`, :ref:`rank`, :ref:`row_number`
+
+----
+
+
+.. _date:
+
+date(*timestring*, *modifier*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the date in this format: YYYY-MM-DD.
+
+ **Parameters**
+ * **timestring\*** --- The string to convert to a date.
+ * **modifier** --- A transformation that is applied to the value to the left.
+
+ **Examples**
+ To get the date portion of the timestamp '2017-01-02T03:04:05':
+
+ .. code-block:: custsqlite
+
+ ;SELECT date('2017-01-02T03:04:05')
+ 2017-01-02
+
+ To get the date portion of the timestamp '2017-01-02T03:04:05' plus one day:
+
+ .. code-block:: custsqlite
+
+ ;SELECT date('2017-01-02T03:04:05', '+1 day')
+ 2017-01-03
+
+ To get the date portion of the epoch timestamp 1491341842:
+
+ .. code-block:: custsqlite
+
+ ;SELECT date(1491341842, 'unixepoch')
+ 2017-04-04
+
+ **See Also**
+ :ref:`datetime`, :ref:`humanize_duration`, :ref:`julianday`, :ref:`strftime`, :ref:`time`, :ref:`timediff`, :ref:`timeslice`
+
+----
+
+
+.. _datetime:
+
+datetime(*timestring*, *modifier*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the date and time in this format: YYYY-MM-DD HH:MM:SS.
+
+ **Parameters**
+ * **timestring\*** --- The string to convert to a date with time.
+ * **modifier** --- A transformation that is applied to the value to the left.
+
+ **Examples**
+ To get the date and time portion of the timestamp '2017-01-02T03:04:05':
+
+ .. code-block:: custsqlite
+
+ ;SELECT datetime('2017-01-02T03:04:05')
+ 2017-01-02 03:04:05
+
+ To get the date and time portion of the timestamp '2017-01-02T03:04:05' plus one minute:
+
+ .. code-block:: custsqlite
+
+ ;SELECT datetime('2017-01-02T03:04:05', '+1 minute')
+ 2017-01-02 03:05:05
+
+ To get the date and time portion of the epoch timestamp 1491341842:
+
+ .. code-block:: custsqlite
+
+ ;SELECT datetime(1491341842, 'unixepoch')
+ 2017-04-04 21:37:22
+
+ **See Also**
+ :ref:`date`, :ref:`humanize_duration`, :ref:`julianday`, :ref:`strftime`, :ref:`time`, :ref:`timediff`, :ref:`timeslice`
+
+----
+
+
+.. _decode:
+
+decode(*value*, *algorithm*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Decode the value using the given algorithm
+
+ **Parameters**
+ * **value\*** --- The value to decode
+ * **algorithm\*** --- One of the following encoding algorithms: base64, hex, uri
+
+ **Examples**
+ To decode the URI-encoded string '%63%75%72%6c':
+
+ .. code-block:: custsqlite
+
+ ;SELECT decode('%63%75%72%6c', 'uri')
+ curl
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _degrees:
+
+degrees(*radians*)
+^^^^^^^^^^^^^^^^^^
+
+ Converts radians to degrees
+
+ **Parameters**
+ * **radians\*** --- The radians value to convert to degrees
+
+ **Examples**
+ To convert PI to degrees:
+
+ .. code-block:: custsqlite
+
+ ;SELECT degrees(pi())
+ 180
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _dense_rank:
+
+dense_rank()
+^^^^^^^^^^^^
+
+ Returns the row_number() of the first peer in each group without gaps
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`first_value`, :ref:`lag`, :ref:`last_value`, :ref:`lead`, :ref:`nth_value`, :ref:`ntile`, :ref:`percent_rank`, :ref:`rank`, :ref:`row_number`
+
+----
+
+
+.. _dirname:
+
+dirname(*path*)
+^^^^^^^^^^^^^^^
+
+ Extract the directory portion of a pathname.
+
+ **Parameters**
+ * **path\*** --- The path
+
+ **Examples**
+ To get the directory of a relative file path:
+
+ .. code-block:: custsqlite
+
+ ;SELECT dirname('foo/bar')
+ foo
+
+ To get the directory of an absolute file path:
+
+ .. code-block:: custsqlite
+
+ ;SELECT dirname('/foo/bar')
+ /foo
+
+ To get the directory of a file in the root directory:
+
+ .. code-block:: custsqlite
+
+ ;SELECT dirname('/bar')
+ /
+
+ To get the directory of a Windows path:
+
+ .. code-block:: custsqlite
+
+ ;SELECT dirname('foo\bar')
+ foo
+
+ To get the directory of an empty path:
+
+ .. code-block:: custsqlite
+
+ ;SELECT dirname('')
+ .
+
+ **See Also**
+ :ref:`basename`, :ref:`joinpath`, :ref:`readlink`, :ref:`realpath`
+
+----
+
+
+.. _echoln:
+
+echoln(*value*)
+^^^^^^^^^^^^^^^
+
+ Echo the argument to the current output file and return it
+
+ **Parameters**
+ * **value\*** --- The value to write to the current output file
+
+ **See Also**
+ :ref:`append_to`, :ref:`echo`, :ref:`export_session_to`, :ref:`pipe_line_to`, :ref:`pipe_to`, :ref:`redirect_to`, :ref:`write_csv_to`, :ref:`write_json_to`, :ref:`write_jsonlines_to`, :ref:`write_raw_to`, :ref:`write_screen_to`, :ref:`write_table_to`, :ref:`write_to`, :ref:`write_view_to`
+
+----
+
+
+.. _encode:
+
+encode(*value*, *algorithm*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Encode the value using the given algorithm
+
+ **Parameters**
+ * **value\*** --- The value to encode
+ * **algorithm\*** --- One of the following encoding algorithms: base64, hex, uri
+
+ **Examples**
+ To base64-encode 'Hello, World!':
+
+ .. code-block:: custsqlite
+
+ ;SELECT encode('Hello, World!', 'base64')
+ SGVsbG8sIFdvcmxkIQ==
+
+ To hex-encode 'Hello, World!':
+
+ .. code-block:: custsqlite
+
+ ;SELECT encode('Hello, World!', 'hex')
+ 48656c6c6f2c20576f726c6421
+
+ To URI-encode 'Hello, World!':
+
+ .. code-block:: custsqlite
+
+ ;SELECT encode('Hello, World!', 'uri')
+ Hello%2C%20World%21
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _endswith:
+
+endswith(*str*, *suffix*)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Test if a string ends with the given suffix
+
+ **Parameters**
+ * **str\*** --- The string to test
+ * **suffix\*** --- The suffix to check in the string
+
+ **Examples**
+ To test if the string 'notbad.jpg' ends with '.jpg':
+
+ .. code-block:: custsqlite
+
+ ;SELECT endswith('notbad.jpg', '.jpg')
+ 1
+
+ To test if the string 'notbad.png' starts with '.jpg':
+
+ .. code-block:: custsqlite
+
+ ;SELECT endswith('notbad.png', '.jpg')
+ 0
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _exp:
+
+exp(*x*)
+^^^^^^^^
+
+ Returns the value of e raised to the power of x
+
+ **Parameters**
+ * **x\*** --- The exponent
+
+ **Examples**
+ To raise e to 2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT exp(2)
+ 7.38905609893065
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _extract:
+
+extract(*str*)
+^^^^^^^^^^^^^^
+
+ Automatically Parse and extract data from a string
+
+ **Parameters**
+ * **str\*** --- The string to parse
+
+ **Examples**
+ To extract key/value pairs from a string:
+
+ .. code-block:: custsqlite
+
+ ;SELECT extract('foo=1 bar=2 name="Rolo Tomassi"')
+ {"foo":1,"bar":2,"name":"Rolo Tomassi"}
+
+ To extract columnar data from a string:
+
+ .. code-block:: custsqlite
+
+ ;SELECT extract('1.0 abc 2.0')
+ {"col_0":1.0,"col_1":2.0}
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _first_value:
+
+first_value(*expr*)
+^^^^^^^^^^^^^^^^^^^
+
+ Returns the result of evaluating the expression against the first row in the window frame.
+
+ **Parameters**
+ * **expr\*** --- The expression to execute over the first row
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`dense_rank`, :ref:`lag`, :ref:`last_value`, :ref:`lead`, :ref:`nth_value`, :ref:`ntile`, :ref:`percent_rank`, :ref:`rank`, :ref:`row_number`
+
+----
+
+
+.. _floor:
+
+floor(*num*)
+^^^^^^^^^^^^
+
+ Returns the largest integer that is not greater than the argument
+
+ **Parameters**
+ * **num\*** --- The number to lower to the floor
+
+ **Examples**
+ To get the floor of 1.23:
+
+ .. code-block:: custsqlite
+
+ ;SELECT floor(1.23)
+ 1
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _generate_series:
+
+generate_series(*start*, *stop*, *\[step\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ A table-valued-function that returns the whole numbers between a lower and upper bound, inclusive
+
+ **Parameters**
+ * **start\*** --- The starting point of the series
+ * **stop\*** --- The stopping point of the series
+ * **step** --- The increment between each value
+
+ **Examples**
+ To generate the numbers in the range [10, 14]:
+
+ .. code-block:: custsqlite
+
+ ;SELECT value FROM generate_series(10, 14)
+ value
+ 10
+ 11
+ 12
+ 13
+ 14
+
+ To generate every other number in the range [10, 14]:
+
+ .. code-block:: custsqlite
+
+ ;SELECT value FROM generate_series(10, 14, 2)
+ value
+ 10
+ 12
+ 14
+
+ To count down from five to 1:
+
+ .. code-block:: custsqlite
+
+ ;SELECT value FROM generate_series(1, 5, -1)
+ value
+ 5
+ 4
+ 3
+ 2
+ 1
+
+
+----
+
+
+.. _gethostbyaddr:
+
+gethostbyaddr(*hostname*)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Get the hostname for the given IP address
+
+ **Parameters**
+ * **hostname\*** --- The IP address to lookup.
+
+ **Examples**
+ To get the hostname for the IP '127.0.0.1':
+
+ .. code-block:: custsqlite
+
+ ;SELECT gethostbyaddr('127.0.0.1')
+ localhost
+
+ **See Also**
+ :ref:`gethostbyname`
+
+----
+
+
+.. _gethostbyname:
+
+gethostbyname(*hostname*)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Get the IP address for the given hostname
+
+ **Parameters**
+ * **hostname\*** --- The DNS hostname to lookup.
+
+ **Examples**
+ To get the IP address for 'localhost':
+
+ .. code-block:: custsqlite
+
+ ;SELECT gethostbyname('localhost')
+ 127.0.0.1
+
+ **See Also**
+ :ref:`gethostbyaddr`
+
+----
+
+
+.. _glob:
+
+glob(*pattern*, *str*)
+^^^^^^^^^^^^^^^^^^^^^^
+
+ Match a string against Unix glob pattern
+
+ **Parameters**
+ * **pattern\*** --- The glob pattern
+ * **str\*** --- The string to match
+
+ **Examples**
+ To test if the string 'abc' matches the glob 'a*':
+
+ .. code-block:: custsqlite
+
+ ;SELECT glob('a*', 'abc')
+ 1
+
+
+----
+
+
+.. _group_concat:
+
+group_concat(*X*, *\[sep\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a string which is the concatenation of all non-NULL values of X separated by a comma or the given separator.
+
+ **Parameters**
+ * **X\*** --- The value to concatenate.
+ * **sep** --- The separator to place between the values.
+
+ **Examples**
+ To concatenate the values of the column 'ex_procname' from the table 'lnav_example_log':
+
+ .. code-block:: custsqlite
+
+ ;SELECT group_concat(ex_procname) FROM lnav_example_log
+ hw,gw,gw,gw
+
+ To join the values of the column 'ex_procname' using the string ', ':
+
+ .. code-block:: custsqlite
+
+ ;SELECT group_concat(ex_procname, ', ') FROM lnav_example_log
+ hw, gw, gw, gw
+
+ To concatenate the distinct values of the column 'ex_procname' from the table 'lnav_example_log':
+
+ .. code-block:: custsqlite
+
+ ;SELECT group_concat(DISTINCT ex_procname) FROM lnav_example_log
+ hw,gw
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _group_spooky_hash_agg:
+
+group_spooky_hash(*str*)
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Compute the hash value for the given arguments
+
+ **Parameters**
+ * **str** --- The string to hash
+
+ **Examples**
+ To produce a hash of all of the values of 'column1':
+
+ .. code-block:: custsqlite
+
+ ;SELECT group_spooky_hash(column1) FROM (VALUES ('abc'), ('123'))
+ 4e7a190aead058cb123c94290f29c34a
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _gunzip:
+
+gunzip(*b*)
+^^^^^^^^^^^
+
+ Decompress a gzip file
+
+ **Parameters**
+ * **b** --- The blob to decompress
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _gzip:
+
+gzip(*value*)
+^^^^^^^^^^^^^
+
+ Compress a string into a gzip file
+
+ **Parameters**
+ * **value** --- The value to compress
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _hex:
+
+hex(*X*)
+^^^^^^^^
+
+ Returns a string which is the upper-case hexadecimal rendering of the content of its argument.
+
+ **Parameters**
+ * **X\*** --- The blob to convert to hexadecimal
+
+ **Examples**
+ To get the hexadecimal rendering of the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT hex('abc')
+ 616263
+
+
+----
+
+
+.. _humanize_duration:
+
+humanize_duration(*secs*)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Format the given seconds value as an abbreviated duration string
+
+ **Parameters**
+ * **secs\*** --- The duration in seconds
+
+ **Examples**
+ To format a duration:
+
+ .. code-block:: custsqlite
+
+ ;SELECT humanize_duration(15 * 60)
+ 15m00s
+
+ To format a sub-second value:
+
+ .. code-block:: custsqlite
+
+ ;SELECT humanize_duration(1.5)
+ 1s500
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`date`, :ref:`datetime`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`julianday`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`strftime`, :ref:`substr`, :ref:`time`, :ref:`timediff`, :ref:`timeslice`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _humanize_file_size:
+
+humanize_file_size(*value*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Format the given file size as a human-friendly string
+
+ **Parameters**
+ * **value\*** --- The file size to format
+
+ **Examples**
+ To format an amount:
+
+ .. code-block:: custsqlite
+
+ ;SELECT humanize_file_size(10 * 1024 * 1024)
+ 10.0MB
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _ifnull:
+
+ifnull(*X*, *Y*)
+^^^^^^^^^^^^^^^^
+
+ Returns a copy of its first non-NULL argument, or NULL if both arguments are NULL
+
+ **Parameters**
+ * **X\*** --- A value to check for NULL-ness
+ * **Y\*** --- A value to check for NULL-ness
+
+ **Examples**
+ To get the first non-null value between null and zero:
+
+ .. code-block:: custsqlite
+
+ ;SELECT ifnull(null, 0)
+ 0
+
+
+----
+
+
+.. _instr:
+
+instr(*haystack*, *needle*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Finds the first occurrence of the needle within the haystack and returns the number of prior characters plus 1, or 0 if the needle was not found
+
+ **Parameters**
+ * **haystack\*** --- The string to search within
+ * **needle\*** --- The string to look for in the haystack
+
+ **Examples**
+ To test get the position of 'b' in the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT instr('abc', 'b')
+ 2
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _jget:
+
+jget(*json*, *ptr*, *\[default\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Get the value from a JSON object using a JSON-Pointer.
+
+ **Parameters**
+ * **json\*** --- The JSON object to query.
+ * **ptr\*** --- The JSON-Pointer to lookup in the object.
+ * **default** --- The default value if the value was not found
+
+ **Examples**
+ To get the root of a JSON value:
+
+ .. code-block:: custsqlite
+
+ ;SELECT jget('1', '')
+ 1
+
+ To get the property named 'b' in a JSON object:
+
+ .. code-block:: custsqlite
+
+ ;SELECT jget('{ "a": 1, "b": 2 }', '/b')
+ 2
+
+ To get the 'msg' property and return a default if it does not exist:
+
+ .. code-block:: custsqlite
+
+ ;SELECT jget(null, '/msg', 'Hello')
+ Hello
+
+ **See Also**
+ :ref:`json_concat`, :ref:`json_contains`, :ref:`json_group_array`, :ref:`json_group_object`, :ref:`yaml_to_json`
+
+----
+
+
+.. _joinpath:
+
+joinpath(*path*)
+^^^^^^^^^^^^^^^^
+
+ Join components of a path together.
+
+ **Parameters**
+ * **path** --- One or more path components to join together. If an argument starts with a forward or backward slash, it will be considered an absolute path and any preceding elements will be ignored.
+
+ **Examples**
+ To join a directory and file name into a relative path:
+
+ .. code-block:: custsqlite
+
+ ;SELECT joinpath('foo', 'bar')
+ foo/bar
+
+ To join an empty component with other names into a relative path:
+
+ .. code-block:: custsqlite
+
+ ;SELECT joinpath('', 'foo', 'bar')
+ foo/bar
+
+ To create an absolute path with two path components:
+
+ .. code-block:: custsqlite
+
+ ;SELECT joinpath('/', 'foo', 'bar')
+ /foo/bar
+
+ To create an absolute path from a path component that starts with a forward slash:
+
+ .. code-block:: custsqlite
+
+ ;SELECT joinpath('/', 'foo', '/bar')
+ /bar
+
+ **See Also**
+ :ref:`basename`, :ref:`dirname`, :ref:`readlink`, :ref:`realpath`
+
+----
+
+
+.. _json_concat:
+
+json_concat(*json*, *value*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns an array with the given values concatenated onto the end. If the initial value is null, the result will be an array with the given elements. If the initial value is an array, the result will be an array with the given values at the end. If the initial value is not null or an array, the result will be an array with two elements: the initial value and the given value.
+
+ **Parameters**
+ * **json\*** --- The initial JSON value.
+ * **value** --- The value(s) to add to the end of the array.
+
+ **Examples**
+ To append the number 4 to null:
+
+ .. code-block:: custsqlite
+
+ ;SELECT json_concat(NULL, 4)
+ [4]
+
+ To append 4 and 5 to the array [1, 2, 3]:
+
+ .. code-block:: custsqlite
+
+ ;SELECT json_concat('[1, 2, 3]', 4, 5)
+ [1,2,3,4,5]
+
+ To concatenate two arrays together:
+
+ .. code-block:: custsqlite
+
+ ;SELECT json_concat('[1, 2, 3]', json('[4, 5]'))
+ [1,2,3,4,5]
+
+ **See Also**
+ :ref:`jget`, :ref:`json_contains`, :ref:`json_group_array`, :ref:`json_group_object`, :ref:`yaml_to_json`
+
+----
+
+
+.. _json_contains:
+
+json_contains(*json*, *value*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Check if a JSON value contains the given element.
+
+ **Parameters**
+ * **json\*** --- The JSON value to query.
+ * **value\*** --- The value to look for in the first argument
+
+ **Examples**
+ To test if a JSON array contains the number 4:
+
+ .. code-block:: custsqlite
+
+ ;SELECT json_contains('[1, 2, 3]', 4)
+ 0
+
+ To test if a JSON array contains the string 'def':
+
+ .. code-block:: custsqlite
+
+ ;SELECT json_contains('["abc", "def"]', 'def')
+ 1
+
+ **See Also**
+ :ref:`jget`, :ref:`json_concat`, :ref:`json_group_array`, :ref:`json_group_object`, :ref:`yaml_to_json`
+
+----
+
+
+.. _json_group_array:
+
+json_group_array(*value*)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Collect the given values from a query into a JSON array
+
+ **Parameters**
+ * **value** --- The values to append to the array
+
+ **Examples**
+ To create an array from arguments:
+
+ .. code-block:: custsqlite
+
+ ;SELECT json_group_array('one', 2, 3.4)
+ ["one",2,3.3999999999999999112]
+
+ To create an array from a column of values:
+
+ .. code-block:: custsqlite
+
+ ;SELECT json_group_array(column1) FROM (VALUES (1), (2), (3))
+ [1,2,3]
+
+ **See Also**
+ :ref:`jget`, :ref:`json_concat`, :ref:`json_contains`, :ref:`json_group_object`, :ref:`yaml_to_json`
+
+----
+
+
+.. _json_group_object:
+
+json_group_object(*name*, *value*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Collect the given values from a query into a JSON object
+
+ **Parameters**
+ * **name\*** --- The property name for the value
+ * **value** --- The value to add to the object
+
+ **Examples**
+ To create an object from arguments:
+
+ .. code-block:: custsqlite
+
+ ;SELECT json_group_object('a', 1, 'b', 2)
+ {"a":1,"b":2}
+
+ To create an object from a pair of columns:
+
+ .. code-block:: custsqlite
+
+ ;SELECT json_group_object(column1, column2) FROM (VALUES ('a', 1), ('b', 2))
+ {"a":1,"b":2}
+
+ **See Also**
+ :ref:`jget`, :ref:`json_concat`, :ref:`json_contains`, :ref:`json_group_array`, :ref:`yaml_to_json`
+
+----
+
+
+.. _julianday:
+
+julianday(*timestring*, *modifier*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the number of days since noon in Greenwich on November 24, 4714 B.C.
+
+ **Parameters**
+ * **timestring\*** --- The string to convert to a date with time.
+ * **modifier** --- A transformation that is applied to the value to the left.
+
+ **Examples**
+ To get the julian day from the timestamp '2017-01-02T03:04:05':
+
+ .. code-block:: custsqlite
+
+ ;SELECT julianday('2017-01-02T03:04:05')
+ 2457755.627835648
+
+ To get the julian day from the timestamp '2017-01-02T03:04:05' plus one minute:
+
+ .. code-block:: custsqlite
+
+ ;SELECT julianday('2017-01-02T03:04:05', '+1 minute')
+ 2457755.6285300925
+
+ To get the julian day from the timestamp 1491341842:
+
+ .. code-block:: custsqlite
+
+ ;SELECT julianday(1491341842, 'unixepoch')
+ 2457848.400949074
+
+ **See Also**
+ :ref:`date`, :ref:`datetime`, :ref:`humanize_duration`, :ref:`strftime`, :ref:`time`, :ref:`timediff`, :ref:`timeslice`
+
+----
+
+
+.. _lag:
+
+lag(*expr*, *\[offset\]*, *\[default\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the result of evaluating the expression against the previous row in the partition.
+
+ **Parameters**
+ * **expr\*** --- The expression to execute over the previous row
+ * **offset** --- The offset from the current row in the partition
+ * **default** --- The default value if the previous row does not exist instead of NULL
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`dense_rank`, :ref:`first_value`, :ref:`last_value`, :ref:`lead`, :ref:`nth_value`, :ref:`ntile`, :ref:`percent_rank`, :ref:`rank`, :ref:`row_number`
+
+----
+
+
+.. _last_insert_rowid:
+
+last_insert_rowid()
+^^^^^^^^^^^^^^^^^^^
+
+ Returns the ROWID of the last row insert from the database connection which invoked the function
+
+
+----
+
+
+.. _last_value:
+
+last_value(*expr*)
+^^^^^^^^^^^^^^^^^^
+
+ Returns the result of evaluating the expression against the last row in the window frame.
+
+ **Parameters**
+ * **expr\*** --- The expression to execute over the last row
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`dense_rank`, :ref:`first_value`, :ref:`lag`, :ref:`lead`, :ref:`nth_value`, :ref:`ntile`, :ref:`percent_rank`, :ref:`rank`, :ref:`row_number`
+
+----
+
+
+.. _lead:
+
+lead(*expr*, *\[offset\]*, *\[default\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the result of evaluating the expression against the next row in the partition.
+
+ **Parameters**
+ * **expr\*** --- The expression to execute over the next row
+ * **offset** --- The offset from the current row in the partition
+ * **default** --- The default value if the next row does not exist instead of NULL
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`dense_rank`, :ref:`first_value`, :ref:`lag`, :ref:`last_value`, :ref:`nth_value`, :ref:`ntile`, :ref:`percent_rank`, :ref:`rank`, :ref:`row_number`
+
+----
+
+
+.. _leftstr:
+
+leftstr(*str*, *N*)
+^^^^^^^^^^^^^^^^^^^
+
+ Returns the N leftmost (UTF-8) characters in the given string.
+
+ **Parameters**
+ * **str\*** --- The string to return subset.
+ * **N\*** --- The number of characters from the left side of the string to return.
+
+ **Examples**
+ To get the first character of the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT leftstr('abc', 1)
+ a
+
+ To get the first ten characters of a string, regardless of size:
+
+ .. code-block:: custsqlite
+
+ ;SELECT leftstr('abc', 10)
+ abc
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _length:
+
+length(*str*)
+^^^^^^^^^^^^^
+
+ Returns the number of characters (not bytes) in the given string prior to the first NUL character
+
+ **Parameters**
+ * **str\*** --- The string to determine the length of
+
+ **Examples**
+ To get the length of the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT length('abc')
+ 3
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _like:
+
+like(*pattern*, *str*, *\[escape\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Match a string against a pattern
+
+ **Parameters**
+ * **pattern\*** --- The pattern to match. A percent symbol (%) will match zero or more characters and an underscore (_) will match a single character.
+ * **str\*** --- The string to match
+ * **escape** --- The escape character that can be used to prefix a literal percent or underscore in the pattern.
+
+ **Examples**
+ To test if the string 'aabcc' contains the letter 'b':
+
+ .. code-block:: custsqlite
+
+ ;SELECT like('%b%', 'aabcc')
+ 1
+
+ To test if the string 'aab%' ends with 'b%':
+
+ .. code-block:: custsqlite
+
+ ;SELECT like('%b:%', 'aab%', ':')
+ 1
+
+
+----
+
+
+.. _likelihood:
+
+likelihood(*value*, *probability*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Provides a hint to the query planner that the first argument is a boolean that is true with the given probability
+
+ **Parameters**
+ * **value\*** --- The boolean value to return
+ * **probability\*** --- A floating point constant between 0.0 and 1.0
+
+
+----
+
+
+.. _likely:
+
+likely(*value*)
+^^^^^^^^^^^^^^^
+
+ Short-hand for likelihood(X,0.9375)
+
+ **Parameters**
+ * **value\*** --- The boolean value to return
+
+
+----
+
+
+.. _lnav_top_file:
+
+lnav_top_file()
+^^^^^^^^^^^^^^^
+
+ Return the name of the file that the top line in the current view came from.
+
+
+----
+
+
+.. _lnav_version:
+
+lnav_version()
+^^^^^^^^^^^^^^
+
+ Return the current version of lnav
+
+
+----
+
+
+.. _load_extension:
+
+load_extension(*path*, *\[entry-point\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Loads SQLite extensions out of the given shared library file using the given entry point.
+
+ **Parameters**
+ * **path\*** --- The path to the shared library containing the extension.
+
+
+----
+
+
+.. _log:
+
+log(*x*)
+^^^^^^^^
+
+ Returns the natural logarithm of x
+
+ **Parameters**
+ * **x\*** --- The number
+
+ **Examples**
+ To get the natual logarithm of 8:
+
+ .. code-block:: custsqlite
+
+ ;SELECT log(8)
+ 2.0794415416798357
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _log10:
+
+log10(*x*)
+^^^^^^^^^^
+
+ Returns the base-10 logarithm of X
+
+ **Parameters**
+ * **x\*** --- The number
+
+ **Examples**
+ To get the logarithm of 100:
+
+ .. code-block:: custsqlite
+
+ ;SELECT log10(100)
+ 2
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _log_top_datetime:
+
+log_top_datetime()
+^^^^^^^^^^^^^^^^^^
+
+ Return the timestamp of the line at the top of the log view.
+
+
+----
+
+
+.. _log_top_line:
+
+log_top_line()
+^^^^^^^^^^^^^^
+
+ Return the line number at the top of the log view.
+
+
+----
+
+
+.. _logfmt2json:
+
+logfmt2json(*str*)
+^^^^^^^^^^^^^^^^^^
+
+ Convert a logfmt-encoded string into JSON
+
+ **Parameters**
+ * **str\*** --- The logfmt message to parse
+
+ **Examples**
+ To extract key/value pairs from a log message:
+
+ .. code-block:: custsqlite
+
+ ;SELECT logfmt2json('foo=1 bar=2 name="Rolo Tomassi"')
+ {"foo":1,"bar":2,"name":"Rolo Tomassi"}
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _lower:
+
+lower(*str*)
+^^^^^^^^^^^^
+
+ Returns a copy of the given string with all ASCII characters converted to lower case.
+
+ **Parameters**
+ * **str\*** --- The string to convert.
+
+ **Examples**
+ To lowercase the string 'AbC':
+
+ .. code-block:: custsqlite
+
+ ;SELECT lower('AbC')
+ abc
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _ltrim:
+
+ltrim(*str*, *\[chars\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a string formed by removing any and all characters that appear in the second argument from the left side of the first.
+
+ **Parameters**
+ * **str\*** --- The string to trim characters from the left side
+ * **chars** --- The characters to trim. Defaults to spaces.
+
+ **Examples**
+ To trim the leading space characters from the string ' abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT ltrim(' abc')
+ abc
+
+ To trim the characters 'a' or 'b' from the left side of the string 'aaaabbbc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT ltrim('aaaabbbc', 'ab')
+ c
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _max:
+
+max(*X*)
+^^^^^^^^
+
+ Returns the argument with the maximum value, or return NULL if any argument is NULL.
+
+ **Parameters**
+ * **X** --- The numbers to find the maximum of. If only one argument is given, this function operates as an aggregate.
+
+ **Examples**
+ To get the largest value from the parameters:
+
+ .. code-block:: custsqlite
+
+ ;SELECT max(2, 1, 3)
+ 3
+
+ To get the largest value from an aggregate:
+
+ .. code-block:: custsqlite
+
+ ;SELECT max(status) FROM http_status_codes
+ 511
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _min:
+
+min(*X*)
+^^^^^^^^
+
+ Returns the argument with the minimum value, or return NULL if any argument is NULL.
+
+ **Parameters**
+ * **X** --- The numbers to find the minimum of. If only one argument is given, this function operates as an aggregate.
+
+ **Examples**
+ To get the smallest value from the parameters:
+
+ .. code-block:: custsqlite
+
+ ;SELECT min(2, 1, 3)
+ 1
+
+ To get the smallest value from an aggregate:
+
+ .. code-block:: custsqlite
+
+ ;SELECT min(status) FROM http_status_codes
+ 100
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _nth_value:
+
+nth_value(*expr*, *N*)
+^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the result of evaluating the expression against the nth row in the window frame.
+
+ **Parameters**
+ * **expr\*** --- The expression to execute over the nth row
+ * **N\*** --- The row number
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`dense_rank`, :ref:`first_value`, :ref:`lag`, :ref:`last_value`, :ref:`lead`, :ref:`ntile`, :ref:`percent_rank`, :ref:`rank`, :ref:`row_number`
+
+----
+
+
+.. _ntile:
+
+ntile(*groups*)
+^^^^^^^^^^^^^^^
+
+ Returns the number of the group that the current row is a part of
+
+ **Parameters**
+ * **groups\*** --- The number of groups
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`dense_rank`, :ref:`first_value`, :ref:`lag`, :ref:`last_value`, :ref:`lead`, :ref:`nth_value`, :ref:`percent_rank`, :ref:`rank`, :ref:`row_number`
+
+----
+
+
+.. _nullif:
+
+nullif(*X*, *Y*)
+^^^^^^^^^^^^^^^^
+
+ Returns its first argument if the arguments are different and NULL if the arguments are the same.
+
+ **Parameters**
+ * **X\*** --- The first argument to compare.
+ * **Y\*** --- The argument to compare against the first.
+
+ **Examples**
+ To test if 1 is different from 1:
+
+ .. code-block:: custsqlite
+
+ ;SELECT nullif(1, 1)
+ <NULL>
+
+ To test if 1 is different from 2:
+
+ .. code-block:: custsqlite
+
+ ;SELECT nullif(1, 2)
+ 1
+
+
+----
+
+
+.. _padc:
+
+padc(*str*, *len*)
+^^^^^^^^^^^^^^^^^^
+
+ Pad the given string with enough spaces to make it centered within the given length
+
+ **Parameters**
+ * **str\*** --- The string to pad
+ * **len\*** --- The minimum desired length of the output string
+
+ **Examples**
+ To pad the string 'abc' to a length of six characters:
+
+ .. code-block:: custsqlite
+
+ ;SELECT padc('abc', 6) || 'def'
+ abc def
+
+ To pad the string 'abcdef' to a length of eight characters:
+
+ .. code-block:: custsqlite
+
+ ;SELECT padc('abcdef', 8) || 'ghi'
+ abcdef ghi
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _padl:
+
+padl(*str*, *len*)
+^^^^^^^^^^^^^^^^^^
+
+ Pad the given string with leading spaces until it reaches the desired length
+
+ **Parameters**
+ * **str\*** --- The string to pad
+ * **len\*** --- The minimum desired length of the output string
+
+ **Examples**
+ To pad the string 'abc' to a length of six characters:
+
+ .. code-block:: custsqlite
+
+ ;SELECT padl('abc', 6)
+ abc
+
+ To pad the string 'abcdef' to a length of four characters:
+
+ .. code-block:: custsqlite
+
+ ;SELECT padl('abcdef', 4)
+ abcdef
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _padr:
+
+padr(*str*, *len*)
+^^^^^^^^^^^^^^^^^^
+
+ Pad the given string with trailing spaces until it reaches the desired length
+
+ **Parameters**
+ * **str\*** --- The string to pad
+ * **len\*** --- The minimum desired length of the output string
+
+ **Examples**
+ To pad the string 'abc' to a length of six characters:
+
+ .. code-block:: custsqlite
+
+ ;SELECT padr('abc', 6) || 'def'
+ abc def
+
+ To pad the string 'abcdef' to a length of four characters:
+
+ .. code-block:: custsqlite
+
+ ;SELECT padr('abcdef', 4) || 'ghi'
+ abcdefghi
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _parse_url:
+
+parse_url(*url*)
+^^^^^^^^^^^^^^^^
+
+ Parse a URL and return the components in a JSON object. Limitations: not all URL schemes are supported and repeated query parameters are not captured.
+
+ **Parameters**
+ * **url\*** --- The URL to parse
+
+ **Examples**
+ To parse the URL 'https://example.com/search?q=hello%20world':
+
+ .. code-block:: custsqlite
+
+ ;SELECT parse_url('https://example.com/search?q=hello%20world')
+ {"scheme":"https","user":null,"password":null,"host":"example.com","port":null,"path":"/search","query":"q=hello%20world","parameters":{"q":"hello world"},"fragment":null}
+
+ To parse the URL 'https://alice@[fe80::14ff:4ee5:1215:2fb2]':
+
+ .. code-block:: custsqlite
+
+ ;SELECT parse_url('https://alice@[fe80::14ff:4ee5:1215:2fb2]')
+ {"scheme":"https","user":"alice","password":null,"host":"[fe80::14ff:4ee5:1215:2fb2]","port":null,"path":"/","query":null,"parameters":null,"fragment":null}
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _percent_rank:
+
+percent_rank()
+^^^^^^^^^^^^^^
+
+ Returns (rank - 1) / (partition-rows - 1)
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`dense_rank`, :ref:`first_value`, :ref:`lag`, :ref:`last_value`, :ref:`lead`, :ref:`nth_value`, :ref:`ntile`, :ref:`rank`, :ref:`row_number`
+
+----
+
+
+.. _pi:
+
+pi()
+^^^^
+
+ Returns the value of PI
+
+ **Examples**
+ To get the value of PI:
+
+ .. code-block:: custsqlite
+
+ ;SELECT pi()
+ 3.141592653589793
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _power:
+
+power(*base*, *exp*)
+^^^^^^^^^^^^^^^^^^^^
+
+ Returns the base to the given exponent
+
+ **Parameters**
+ * **base\*** --- The base number
+ * **exp\*** --- The exponent
+
+ **Examples**
+ To raise two to the power of three:
+
+ .. code-block:: custsqlite
+
+ ;SELECT power(2, 3)
+ 8
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _printf:
+
+printf(*format*, *X*)
+^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a string with this functions arguments substituted into the given format. Substitution points are specified using percent (%) options, much like the standard C printf() function.
+
+ **Parameters**
+ * **format\*** --- The format of the string to return.
+ * **X\*** --- The argument to substitute at a given position in the format.
+
+ **Examples**
+ To substitute 'World' into the string 'Hello, %s!':
+
+ .. code-block:: custsqlite
+
+ ;SELECT printf('Hello, %s!', 'World')
+ Hello, World!
+
+ To right-align 'small' in the string 'align:' with a column width of 10:
+
+ .. code-block:: custsqlite
+
+ ;SELECT printf('align: % 10s', 'small')
+ align: small
+
+ To format 11 with a width of five characters and leading zeroes:
+
+ .. code-block:: custsqlite
+
+ ;SELECT printf('value: %05d', 11)
+ value: 00011
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _proper:
+
+proper(*str*)
+^^^^^^^^^^^^^
+
+ Capitalize the first character of words in the given string
+
+ **Parameters**
+ * **str\*** --- The string to capitalize.
+
+ **Examples**
+ To capitalize the words in the string 'hello, world!':
+
+ .. code-block:: custsqlite
+
+ ;SELECT proper('hello, world!')
+ Hello, World!
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _quote:
+
+quote(*X*)
+^^^^^^^^^^
+
+ Returns the text of an SQL literal which is the value of its argument suitable for inclusion into an SQL statement.
+
+ **Parameters**
+ * **X\*** --- The string to quote.
+
+ **Examples**
+ To quote the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT quote('abc')
+ 'abc'
+
+ To quote the string 'abc'123':
+
+ .. code-block:: custsqlite
+
+ ;SELECT quote('abc''123')
+ 'abc''123'
+
+
+----
+
+
+.. _radians:
+
+radians(*degrees*)
+^^^^^^^^^^^^^^^^^^
+
+ Converts degrees to radians
+
+ **Parameters**
+ * **degrees\*** --- The degrees value to convert to radians
+
+ **Examples**
+ To convert 180 degrees to radians:
+
+ .. code-block:: custsqlite
+
+ ;SELECT radians(180)
+ 3.141592653589793
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _raise_error:
+
+raise_error(*msg*)
+^^^^^^^^^^^^^^^^^^
+
+ Raises an error with the given message when executed
+
+ **Parameters**
+ * **msg\*** --- The error message
+
+
+----
+
+
+.. _random:
+
+random()
+^^^^^^^^
+
+ Returns a pseudo-random integer between -9223372036854775808 and +9223372036854775807.
+
+
+----
+
+
+.. _randomblob:
+
+randomblob(*N*)
+^^^^^^^^^^^^^^^
+
+ Return an N-byte blob containing pseudo-random bytes.
+
+ **Parameters**
+ * **N\*** --- The size of the blob in bytes.
+
+
+----
+
+
+.. _rank:
+
+rank()
+^^^^^^
+
+ Returns the row_number() of the first peer in each group with gaps
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`dense_rank`, :ref:`first_value`, :ref:`lag`, :ref:`last_value`, :ref:`lead`, :ref:`nth_value`, :ref:`ntile`, :ref:`percent_rank`, :ref:`row_number`
+
+----
+
+
+.. _readlink:
+
+readlink(*path*)
+^^^^^^^^^^^^^^^^
+
+ Read the target of a symbolic link.
+
+ **Parameters**
+ * **path\*** --- The path to the symbolic link.
+
+ **See Also**
+ :ref:`basename`, :ref:`dirname`, :ref:`joinpath`, :ref:`realpath`
+
+----
+
+
+.. _realpath:
+
+realpath(*path*)
+^^^^^^^^^^^^^^^^
+
+ Returns the resolved version of the given path, expanding symbolic links and resolving '.' and '..' references.
+
+ **Parameters**
+ * **path\*** --- The path to resolve.
+
+ **See Also**
+ :ref:`basename`, :ref:`dirname`, :ref:`joinpath`, :ref:`readlink`
+
+----
+
+
+.. _regexp:
+
+regexp(*re*, *str*)
+^^^^^^^^^^^^^^^^^^^
+
+ Test if a string matches a regular expression
+
+ **Parameters**
+ * **re\*** --- The regular expression to use
+ * **str\*** --- The string to test against the regular expression
+
+
+----
+
+
+.. _regexp_capture:
+
+regexp_capture(*string*, *pattern*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ A table-valued function that executes a regular-expression over a string and returns the captured values. If the regex only matches a subset of the input string, it will be rerun on the remaining parts of the string until no more matches are found.
+
+ **Parameters**
+ * **string\*** --- The string to match against the given pattern.
+ * **pattern\*** --- The regular expression to match.
+
+ **Examples**
+ To extract the key/value pairs 'a'/1 and 'b'/2 from the string 'a=1; b=2':
+
+ .. code-block:: custsqlite
+
+ ;SELECT * FROM regexp_capture('a=1; b=2', '(\w+)=(\d+)')
+ match_index capture_index capture_name capture_count range_start range_stop content
+ 0 0 <NULL> 3 1 4 a=1
+ 0 1 <NULL> 3 1 2 a
+ 0 2 <NULL> 3 3 4 1
+ 1 0 <NULL> 3 6 9 b=2
+ 1 1 <NULL> 3 6 7 b
+ 1 2 <NULL> 3 8 9 2
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _regexp_capture_into_json:
+
+regexp_capture_into_json(*string*, *pattern*, *\[options\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ A table-valued function that executes a regular-expression over a string and returns the captured values as a JSON object. If the regex only matches a subset of the input string, it will be rerun on the remaining parts of the string until no more matches are found.
+
+ **Parameters**
+ * **string\*** --- The string to match against the given pattern.
+ * **pattern\*** --- The regular expression to match.
+ * **options** --- A JSON object with the following option: convert-numbers - True (default) if text that looks like numeric data should be converted to JSON numbers, false if they should be captured as strings.
+
+ **Examples**
+ To extract the key/value pairs 'a'/1 and 'b'/2 from the string 'a=1; b=2':
+
+ .. code-block:: custsqlite
+
+ ;SELECT * FROM regexp_capture_into_json('a=1; b=2', '(\w+)=(\d+)')
+ match_index content
+ 0 {"col_0":"a","col_1":1}
+ 1 {"col_0":"b","col_1":2}
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _regexp_match:
+
+regexp_match(*re*, *str*)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Match a string against a regular expression and return the capture groups as JSON.
+
+ **Parameters**
+ * **re\*** --- The regular expression to use
+ * **str\*** --- The string to test against the regular expression
+
+ **Examples**
+ To capture the digits from the string '123':
+
+ .. code-block:: custsqlite
+
+ ;SELECT regexp_match('(\d+)', '123')
+ 123
+
+ To capture a number and word into a JSON object with the properties 'col_0' and 'col_1':
+
+ .. code-block:: custsqlite
+
+ ;SELECT regexp_match('(\d+) (\w+)', '123 four')
+ {"col_0":123,"col_1":"four"}
+
+ To capture a number and word into a JSON object with the named properties 'num' and 'str':
+
+ .. code-block:: custsqlite
+
+ ;SELECT regexp_match('(?<num>\d+) (?<str>\w+)', '123 four')
+ {"num":123,"str":"four"}
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_replace`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _regexp_replace:
+
+regexp_replace(*str*, *re*, *repl*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Replace the parts of a string that match a regular expression.
+
+ **Parameters**
+ * **str\*** --- The string to perform replacements on
+ * **re\*** --- The regular expression to match
+ * **repl\*** --- The replacement string. You can reference capture groups with a backslash followed by the number of the group, starting with 1.
+
+ **Examples**
+ To replace the word at the start of the string 'Hello, World!' with 'Goodbye':
+
+ .. code-block:: custsqlite
+
+ ;SELECT regexp_replace('Hello, World!', '^(\w+)', 'Goodbye')
+ Goodbye, World!
+
+ To wrap alphanumeric words with angle brackets:
+
+ .. code-block:: custsqlite
+
+ ;SELECT regexp_replace('123 abc', '(\w+)', '<\1>')
+ <123> <abc>
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_match`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _replace:
+
+replace(*str*, *old*, *replacement*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a string formed by substituting the replacement string for every occurrence of the old string in the given string.
+
+ **Parameters**
+ * **str\*** --- The string to perform substitutions on.
+ * **old\*** --- The string to be replaced.
+ * **replacement\*** --- The string to replace any occurrences of the old string with.
+
+ **Examples**
+ To replace the string 'x' with 'z' in 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT replace('abc', 'x', 'z')
+ abc
+
+ To replace the string 'a' with 'z' in 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT replace('abc', 'a', 'z')
+ zbc
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _replicate:
+
+replicate(*str*, *N*)
+^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the given string concatenated N times.
+
+ **Parameters**
+ * **str\*** --- The string to replicate.
+ * **N\*** --- The number of times to replicate the string.
+
+ **Examples**
+ To repeat the string 'abc' three times:
+
+ .. code-block:: custsqlite
+
+ ;SELECT replicate('abc', 3)
+ abcabcabc
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _reverse:
+
+reverse(*str*)
+^^^^^^^^^^^^^^
+
+ Returns the reverse of the given string.
+
+ **Parameters**
+ * **str\*** --- The string to reverse.
+
+ **Examples**
+ To reverse the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT reverse('abc')
+ cba
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _rightstr:
+
+rightstr(*str*, *N*)
+^^^^^^^^^^^^^^^^^^^^
+
+ Returns the N rightmost (UTF-8) characters in the given string.
+
+ **Parameters**
+ * **str\*** --- The string to return subset.
+ * **N\*** --- The number of characters from the right side of the string to return.
+
+ **Examples**
+ To get the last character of the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT rightstr('abc', 1)
+ c
+
+ To get the last ten characters of a string, regardless of size:
+
+ .. code-block:: custsqlite
+
+ ;SELECT rightstr('abc', 10)
+ abc
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _round:
+
+round(*num*, *\[digits\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a floating-point value rounded to the given number of digits to the right of the decimal point.
+
+ **Parameters**
+ * **num\*** --- The value to round.
+ * **digits** --- The number of digits to the right of the decimal to round to.
+
+ **Examples**
+ To round the number 123.456 to an integer:
+
+ .. code-block:: custsqlite
+
+ ;SELECT round(123.456)
+ 123
+
+ To round the number 123.456 to a precision of 1:
+
+ .. code-block:: custsqlite
+
+ ;SELECT round(123.456, 1)
+ 123.5
+
+ To round the number 123.456 to a precision of 5:
+
+ .. code-block:: custsqlite
+
+ ;SELECT round(123.456, 5)
+ 123.456
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`sign`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _row_number:
+
+row_number()
+^^^^^^^^^^^^
+
+ Returns the number of the row within the current partition, starting from 1.
+
+ **Examples**
+ To number messages from a process:
+
+ .. code-block:: custsqlite
+
+ ;SELECT row_number() OVER (PARTITION BY ex_procname ORDER BY log_line) AS msg_num, ex_procname, log_body FROM lnav_example_log
+ msg_num ex_procname log_body
+ 1 gw Goodbye, World!
+ 2 gw Goodbye, World!
+ 3 gw Goodbye, World!
+ 1 hw Hello, World!
+
+ **See Also**
+ :ref:`cume_dist`, :ref:`dense_rank`, :ref:`first_value`, :ref:`lag`, :ref:`last_value`, :ref:`lead`, :ref:`nth_value`, :ref:`ntile`, :ref:`percent_rank`, :ref:`rank`
+
+----
+
+
+.. _rtrim:
+
+rtrim(*str*, *\[chars\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a string formed by removing any and all characters that appear in the second argument from the right side of the first.
+
+ **Parameters**
+ * **str\*** --- The string to trim characters from the right side
+ * **chars** --- The characters to trim. Defaults to spaces.
+
+ **Examples**
+ To trim the space characters from the end of the string 'abc ':
+
+ .. code-block:: custsqlite
+
+ ;SELECT rtrim('abc ')
+ abc
+
+ To trim the characters 'b' and 'c' from the string 'abbbbcccc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT rtrim('abbbbcccc', 'bc')
+ a
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _sign:
+
+sign(*num*)
+^^^^^^^^^^^
+
+ Returns the sign of the given number as -1, 0, or 1
+
+ **Parameters**
+ * **num\*** --- The number
+
+ **Examples**
+ To get the sign of 10:
+
+ .. code-block:: custsqlite
+
+ ;SELECT sign(10)
+ 1
+
+ To get the sign of 0:
+
+ .. code-block:: custsqlite
+
+ ;SELECT sign(0)
+ 0
+
+ To get the sign of -10:
+
+ .. code-block:: custsqlite
+
+ ;SELECT sign(-10)
+ -1
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`square`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _sparkline:
+
+sparkline(*value*, *\[upper\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Function used to generate a sparkline bar chart. The non-aggregate version converts a single numeric value on a range to a bar chart character. The aggregate version returns a string with a bar character for every numeric input
+
+ **Parameters**
+ * **value\*** --- The numeric value to convert
+ * **upper** --- The upper bound of the numeric range. The non-aggregate version defaults to 100. The aggregate version uses the largest value in the inputs.
+
+ **Examples**
+ To get the unicode block element for the value 32 in the range of 0-128:
+
+ .. code-block:: custsqlite
+
+ ;SELECT sparkline(32, 128)
+ ▂
+
+ To chart the values in a JSON array:
+
+ .. code-block:: custsqlite
+
+ ;SELECT sparkline(value) FROM json_each('[0, 1, 2, 3, 4, 5, 6, 7, 8]')
+ ▁▂▃▄▅▆▇█
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _spooky_hash:
+
+spooky_hash(*str*)
+^^^^^^^^^^^^^^^^^^
+
+ Compute the hash value for the given arguments.
+
+ **Parameters**
+ * **str** --- The string to hash
+
+ **Examples**
+ To produce a hash for the string 'Hello, World!':
+
+ .. code-block:: custsqlite
+
+ ;SELECT spooky_hash('Hello, World!')
+ 0b1d52cc5427db4c6a9eed9d3e5700f4
+
+ To produce a hash for the parameters where one is NULL:
+
+ .. code-block:: custsqlite
+
+ ;SELECT spooky_hash('Hello, World!', NULL)
+ c96ee75d48e6ea444fee8af948f6da25
+
+ To produce a hash for the parameters where one is an empty string:
+
+ .. code-block:: custsqlite
+
+ ;SELECT spooky_hash('Hello, World!', '')
+ c96ee75d48e6ea444fee8af948f6da25
+
+ To produce a hash for the parameters where one is a number:
+
+ .. code-block:: custsqlite
+
+ ;SELECT spooky_hash('Hello, World!', 123)
+ f96b3d9c1a19f4394c97a1b79b1880df
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _sqlite_compileoption_get:
+
+sqlite_compileoption_get(*N*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the N-th compile-time option used to build SQLite or NULL if N is out of range.
+
+ **Parameters**
+ * **N\*** --- The option number to get
+
+
+----
+
+
+.. _sqlite_compileoption_used:
+
+sqlite_compileoption_used(*option*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns true (1) or false (0) depending on whether or not that compile-time option was used during the build.
+
+ **Parameters**
+ * **option\*** --- The name of the compile-time option.
+
+ **Examples**
+ To check if the SQLite library was compiled with ENABLE_FTS3:
+
+ .. code-block:: custsqlite
+
+ ;SELECT sqlite_compileoption_used('ENABLE_FTS3')
+ 1
+
+
+----
+
+
+.. _sqlite_source_id:
+
+sqlite_source_id()
+^^^^^^^^^^^^^^^^^^
+
+ Returns a string that identifies the specific version of the source code that was used to build the SQLite library.
+
+
+----
+
+
+.. _sqlite_version:
+
+sqlite_version()
+^^^^^^^^^^^^^^^^
+
+ Returns the version string for the SQLite library that is running.
+
+
+----
+
+
+.. _square:
+
+square(*num*)
+^^^^^^^^^^^^^
+
+ Returns the square of the argument
+
+ **Parameters**
+ * **num\*** --- The number to square
+
+ **Examples**
+ To get the square of two:
+
+ .. code-block:: custsqlite
+
+ ;SELECT square(2)
+ 4
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`sum`, :ref:`total`
+
+----
+
+
+.. _startswith:
+
+startswith(*str*, *prefix*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Test if a string begins with the given prefix
+
+ **Parameters**
+ * **str\*** --- The string to test
+ * **prefix\*** --- The prefix to check in the string
+
+ **Examples**
+ To test if the string 'foobar' starts with 'foo':
+
+ .. code-block:: custsqlite
+
+ ;SELECT startswith('foobar', 'foo')
+ 1
+
+ To test if the string 'foobar' starts with 'bar':
+
+ .. code-block:: custsqlite
+
+ ;SELECT startswith('foobar', 'bar')
+ 0
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _strfilter:
+
+strfilter(*source*, *include*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the source string with only the characters given in the second parameter
+
+ **Parameters**
+ * **source\*** --- The string to filter
+ * **include\*** --- The characters to include in the result
+
+ **Examples**
+ To get the 'b', 'c', and 'd' characters from the string 'abcabc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT strfilter('abcabc', 'bcd')
+ bcbc
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _strftime:
+
+strftime(*format*, *timestring*, *modifier*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the date formatted according to the format string specified as the first argument.
+
+ **Parameters**
+ * **format\*** --- A format string with substitutions similar to those found in the strftime() standard C library.
+ * **timestring\*** --- The string to convert to a date with time.
+ * **modifier** --- A transformation that is applied to the value to the left.
+
+ **Examples**
+ To get the year from the timestamp '2017-01-02T03:04:05':
+
+ .. code-block:: custsqlite
+
+ ;SELECT strftime('%Y', '2017-01-02T03:04:05')
+ 2017
+
+ To create a string with the time from the timestamp '2017-01-02T03:04:05' plus one minute:
+
+ .. code-block:: custsqlite
+
+ ;SELECT strftime('The time is: %H:%M:%S', '2017-01-02T03:04:05', '+1 minute')
+ The time is: 03:05:05
+
+ To create a string with the Julian day from the epoch timestamp 1491341842:
+
+ .. code-block:: custsqlite
+
+ ;SELECT strftime('Julian day: %J', 1491341842, 'unixepoch')
+ Julian day: 2457848.400949074
+
+ **See Also**
+ :ref:`date`, :ref:`datetime`, :ref:`humanize_duration`, :ref:`julianday`, :ref:`time`, :ref:`timediff`, :ref:`timeslice`
+
+----
+
+
+.. _substr:
+
+substr(*str*, *start*, *\[size\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a substring of input string X that begins with the Y-th character and which is Z characters long.
+
+ **Parameters**
+ * **str\*** --- The string to extract a substring from.
+ * **start\*** --- The index within 'str' that is the start of the substring. Indexes begin at 1. A negative value means that the substring is found by counting from the right rather than the left.
+ * **size** --- The size of the substring. If not given, then all characters through the end of the string are returned. If the value is negative, then the characters before the start are returned.
+
+ **Examples**
+ To get the substring starting at the second character until the end of the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT substr('abc', 2)
+ bc
+
+ To get the substring of size one starting at the second character of the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT substr('abc', 2, 1)
+ b
+
+ To get the substring starting at the last character until the end of the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT substr('abc', -1)
+ c
+
+ To get the substring starting at the last character and going backwards one step of the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT substr('abc', -1, -1)
+ b
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _sum:
+
+sum(*X*)
+^^^^^^^^
+
+ Returns the sum of the values in the group as an integer.
+
+ **Parameters**
+ * **X\*** --- The values to add.
+
+ **Examples**
+ To sum all of the values in the column 'ex_duration' from the table 'lnav_example_log':
+
+ .. code-block:: custsqlite
+
+ ;SELECT sum(ex_duration) FROM lnav_example_log
+ 17
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`total`
+
+----
+
+
+.. _time:
+
+time(*timestring*, *modifier*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns the time in this format: HH:MM:SS.
+
+ **Parameters**
+ * **timestring\*** --- The string to convert to a time.
+ * **modifier** --- A transformation that is applied to the value to the left.
+
+ **Examples**
+ To get the time portion of the timestamp '2017-01-02T03:04:05':
+
+ .. code-block:: custsqlite
+
+ ;SELECT time('2017-01-02T03:04:05')
+ 03:04:05
+
+ To get the time portion of the timestamp '2017-01-02T03:04:05' plus one minute:
+
+ .. code-block:: custsqlite
+
+ ;SELECT time('2017-01-02T03:04:05', '+1 minute')
+ 03:05:05
+
+ To get the time portion of the epoch timestamp 1491341842:
+
+ .. code-block:: custsqlite
+
+ ;SELECT time(1491341842, 'unixepoch')
+ 21:37:22
+
+ **See Also**
+ :ref:`date`, :ref:`datetime`, :ref:`humanize_duration`, :ref:`julianday`, :ref:`strftime`, :ref:`timediff`, :ref:`timeslice`
+
+----
+
+
+.. _timediff:
+
+timediff(*time1*, *time2*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Compute the difference between two timestamps in seconds
+
+ **Parameters**
+ * **time1\*** --- The first timestamp
+ * **time2\*** --- The timestamp to subtract from the first
+
+ **Examples**
+ To get the difference between two timestamps:
+
+ .. code-block:: custsqlite
+
+ ;SELECT timediff('2017-02-03T04:05:06', '2017-02-03T04:05:00')
+ 6
+
+ To get the difference between relative timestamps:
+
+ .. code-block:: custsqlite
+
+ ;SELECT timediff('today', 'yesterday')
+ 86400
+
+ **See Also**
+ :ref:`date`, :ref:`datetime`, :ref:`humanize_duration`, :ref:`julianday`, :ref:`strftime`, :ref:`time`, :ref:`timeslice`
+
+----
+
+
+.. _timeslice:
+
+timeslice(*time*, *slice*)
+^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Return the start of the slice of time that the given timestamp falls in. If the time falls outside of the slice, NULL is returned.
+
+ **Parameters**
+ * **time\*** --- The timestamp to get the time slice for.
+ * **slice\*** --- The size of the time slices
+
+ **Examples**
+ To get the timestamp rounded down to the start of the ten minute slice:
+
+ .. code-block:: custsqlite
+
+ ;SELECT timeslice('2017-01-01T05:05:00', '10m')
+ 2017-01-01 05:00:00.000
+
+ To group log messages into five minute buckets and count them:
+
+ .. code-block:: custsqlite
+
+ ;SELECT timeslice(log_time_msecs, '5m') AS slice, count(1)
+ FROM lnav_example_log GROUP BY slice
+ slice count(1)
+ 2017-02-03 04:05:00.000 2
+ 2017-02-03 04:25:00.000 1
+ 2017-02-03 04:55:00.000 1
+
+ To group log messages by those before 4:30am and after:
+
+ .. code-block:: custsqlite
+
+ ;SELECT timeslice(log_time_msecs, 'before 4:30am') AS slice, count(1) FROM lnav_example_log GROUP BY slice
+ slice count(1)
+ <NULL> 1
+ 2017-02-03 00:00:00.000 3
+
+ **See Also**
+ :ref:`date`, :ref:`datetime`, :ref:`humanize_duration`, :ref:`julianday`, :ref:`strftime`, :ref:`time`, :ref:`timediff`
+
+----
+
+
+.. _total:
+
+total(*X*)
+^^^^^^^^^^
+
+ Returns the sum of the values in the group as a floating-point.
+
+ **Parameters**
+ * **X\*** --- The values to add.
+
+ **Examples**
+ To total all of the values in the column 'ex_duration' from the table 'lnav_example_log':
+
+ .. code-block:: custsqlite
+
+ ;SELECT total(ex_duration) FROM lnav_example_log
+ 17
+
+ **See Also**
+ :ref:`abs`, :ref:`acos`, :ref:`acosh`, :ref:`asin`, :ref:`asinh`, :ref:`atan2`, :ref:`atan`, :ref:`atanh`, :ref:`atn2`, :ref:`avg`, :ref:`ceil`, :ref:`degrees`, :ref:`exp`, :ref:`floor`, :ref:`log10`, :ref:`log`, :ref:`max`, :ref:`min`, :ref:`pi`, :ref:`power`, :ref:`radians`, :ref:`round`, :ref:`sign`, :ref:`square`, :ref:`sum`
+
+----
+
+
+.. _total_changes:
+
+total_changes()
+^^^^^^^^^^^^^^^
+
+ Returns the number of row changes caused by INSERT, UPDATE or DELETE statements since the current database connection was opened.
+
+
+----
+
+
+.. _trim:
+
+trim(*str*, *\[chars\]*)
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+ Returns a string formed by removing any and all characters that appear in the second argument from the left and right sides of the first.
+
+ **Parameters**
+ * **str\*** --- The string to trim characters from the left and right sides.
+ * **chars** --- The characters to trim. Defaults to spaces.
+
+ **Examples**
+ To trim spaces from the start and end of the string ' abc ':
+
+ .. code-block:: custsqlite
+
+ ;SELECT trim(' abc ')
+ abc
+
+ To trim the characters '-' and '+' from the string '-+abc+-':
+
+ .. code-block:: custsqlite
+
+ ;SELECT trim('-+abc+-', '-+')
+ abc
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _typeof:
+
+typeof(*X*)
+^^^^^^^^^^^
+
+ Returns a string that indicates the datatype of the expression X: "null", "integer", "real", "text", or "blob".
+
+ **Parameters**
+ * **X\*** --- The expression to check.
+
+ **Examples**
+ To get the type of the number 1:
+
+ .. code-block:: custsqlite
+
+ ;SELECT typeof(1)
+ integer
+
+ To get the type of the string 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT typeof('abc')
+ text
+
+
+----
+
+
+.. _unicode:
+
+unicode(*X*)
+^^^^^^^^^^^^
+
+ Returns the numeric unicode code point corresponding to the first character of the string X.
+
+ **Parameters**
+ * **X\*** --- The string to examine.
+
+ **Examples**
+ To get the unicode code point for the first character of 'abc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT unicode('abc')
+ 97
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unparse_url`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _unlikely:
+
+unlikely(*value*)
+^^^^^^^^^^^^^^^^^
+
+ Short-hand for likelihood(X, 0.0625)
+
+ **Parameters**
+ * **value\*** --- The boolean value to return
+
+
+----
+
+
+.. _unparse_url:
+
+unparse_url(*obj*)
+^^^^^^^^^^^^^^^^^^
+
+ Convert a JSON object containing the parts of a URL into a URL string
+
+ **Parameters**
+ * **obj\*** --- The JSON object containing the URL parts
+
+ **Examples**
+ To unparse the object '{"scheme": "https", "host": "example.com"}':
+
+ .. code-block:: custsqlite
+
+ ;SELECT unparse_url('{"scheme": "https", "host": "example.com"}')
+ https://example.com/
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`upper`, :ref:`xpath`
+
+----
+
+
+.. _upper:
+
+upper(*str*)
+^^^^^^^^^^^^
+
+ Returns a copy of the given string with all ASCII characters converted to upper case.
+
+ **Parameters**
+ * **str\*** --- The string to convert.
+
+ **Examples**
+ To uppercase the string 'aBc':
+
+ .. code-block:: custsqlite
+
+ ;SELECT upper('aBc')
+ ABC
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`xpath`
+
+----
+
+
+.. _xpath:
+
+xpath(*xpath*, *xmldoc*)
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+ A table-valued function that executes an xpath expression over an XML string and returns the selected values.
+
+ **Parameters**
+ * **xpath\*** --- The XPATH expression to evaluate over the XML document.
+ * **xmldoc\*** --- The XML document as a string.
+
+ **Examples**
+ To select the XML nodes on the path '/abc/def':
+
+ .. code-block:: custsqlite
+
+ ;SELECT * FROM xpath('/abc/def', '<abc><def a="b">Hello</def><def>Bye</def></abc>')
+ result node_path node_attr node_text
+ <def a="b">Hello</def>␊ /abc/def[1] {"a":"b"} Hello
+ <def>Bye</def>␊ /abc/def[2] {} Bye
+
+ To select all 'a' attributes on the path '/abc/def':
+
+ .. code-block:: custsqlite
+
+ ;SELECT * FROM xpath('/abc/def/@a', '<abc><def a="b">Hello</def><def>Bye</def></abc>')
+ result node_path node_attr node_text
+ b /abc/def[1]/@a {"a":"b"} Hello
+
+ To select the text nodes on the path '/abc/def':
+
+ .. code-block:: custsqlite
+
+ ;SELECT * FROM xpath('/abc/def/text()', '<abc><def a="b">Hello &#x2605;</def></abc>')
+ result node_path node_attr node_text
+ Hello ★ /abc/def/text() {} Hello ★
+
+ **See Also**
+ :ref:`anonymize`, :ref:`char`, :ref:`charindex`, :ref:`decode`, :ref:`encode`, :ref:`endswith`, :ref:`extract`, :ref:`group_concat`, :ref:`group_spooky_hash_agg`, :ref:`gunzip`, :ref:`gzip`, :ref:`humanize_duration`, :ref:`humanize_file_size`, :ref:`instr`, :ref:`leftstr`, :ref:`length`, :ref:`logfmt2json`, :ref:`lower`, :ref:`ltrim`, :ref:`padc`, :ref:`padl`, :ref:`padr`, :ref:`parse_url`, :ref:`printf`, :ref:`proper`, :ref:`regexp_capture_into_json`, :ref:`regexp_capture`, :ref:`regexp_match`, :ref:`regexp_replace`, :ref:`replace`, :ref:`replicate`, :ref:`reverse`, :ref:`rightstr`, :ref:`rtrim`, :ref:`sparkline`, :ref:`spooky_hash`, :ref:`startswith`, :ref:`strfilter`, :ref:`substr`, :ref:`trim`, :ref:`unicode`, :ref:`unparse_url`, :ref:`upper`
+
+----
+
+
+.. _yaml_to_json:
+
+yaml_to_json(*yaml*)
+^^^^^^^^^^^^^^^^^^^^
+
+ Convert a YAML document to a JSON-encoded string
+
+ **Parameters**
+ * **yaml\*** --- The YAML value to convert to JSON.
+
+ **Examples**
+ To convert the document "abc: def":
+
+ .. code-block:: custsqlite
+
+ ;SELECT yaml_to_json('abc: def')
+ {"abc": "def"}
+
+ **See Also**
+ :ref:`jget`, :ref:`json_concat`, :ref:`json_contains`, :ref:`json_group_array`, :ref:`json_group_object`
+
+----
+
+
+.. _zeroblob:
+
+zeroblob(*N*)
+^^^^^^^^^^^^^
+
+ Returns a BLOB consisting of N bytes of 0x00.
+
+ **Parameters**
+ * **N\*** --- The size of the BLOB.
+
+
+----
+
diff --git a/src/itertools.similar.hh b/src/itertools.similar.hh
new file mode 100644
index 0000000..3f9a704
--- /dev/null
+++ b/src/itertools.similar.hh
@@ -0,0 +1,133 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_itertools_similar_hh
+#define lnav_itertools_similar_hh
+
+#include <queue>
+#include <string>
+
+#include "base/itertools.hh"
+#include "fts_fuzzy_match.hh"
+
+namespace lnav {
+namespace itertools {
+
+namespace details {
+
+template<typename F>
+struct similar_to {
+ nonstd::optional<F> st_mapper;
+ std::string st_pattern;
+ size_t st_count{5};
+};
+
+struct identity {
+ template<typename U>
+ constexpr auto operator()(U&& v) const noexcept
+ -> decltype(std::forward<U>(v))
+ {
+ return std::forward<U>(v);
+ }
+};
+
+} // namespace details
+
+template<typename F>
+inline details::similar_to<F>
+similar_to(F mapper, std::string pattern, size_t count = 5)
+{
+ return lnav::itertools::details::similar_to<F>{
+ mapper, std::move(pattern), count};
+}
+
+inline auto
+similar_to(std::string pattern, size_t count = 5)
+{
+ return similar_to(details::identity{}, std::move(pattern), count);
+}
+
+} // namespace itertools
+} // namespace lnav
+
+template<typename T, typename F>
+std::vector<typename T::value_type>
+operator|(const T& in, const lnav::itertools::details::similar_to<F>& st)
+{
+ using score_pair = std::pair<int, typename T::value_type>;
+
+ struct score_cmp {
+ bool operator()(const score_pair& lhs, const score_pair& rhs)
+ {
+ return lhs.first > rhs.first;
+ }
+ };
+
+ std::vector<std::remove_const_t<typename T::value_type>> retval;
+
+ if (st.st_pattern.empty()) {
+ retval.insert(retval.begin(), in.begin(), in.end());
+ if (retval.size() > st.st_count) {
+ retval.resize(st.st_count);
+ }
+ return retval;
+ }
+
+ std::priority_queue<score_pair, std::vector<score_pair>, score_cmp> pq;
+
+ for (const auto& elem : in) {
+ int score = 0;
+
+ if (!fts::fuzzy_match(
+ st.st_pattern.c_str(),
+ lnav::func::invoke(st.st_mapper.value(), elem).c_str(),
+ score))
+ {
+ continue;
+ }
+ if (score <= 0) {
+ continue;
+ }
+ pq.push(std::make_pair(score, elem));
+
+ if (pq.size() > st.st_count) {
+ pq.pop();
+ }
+ }
+
+ while (!pq.empty()) {
+ retval.template emplace_back(pq.top().second);
+ pq.pop();
+ }
+ std::reverse(retval.begin(), retval.end());
+
+ return retval;
+}
+
+#endif
diff --git a/src/json-extension-functions.cc b/src/json-extension-functions.cc
new file mode 100644
index 0000000..36dc290
--- /dev/null
+++ b/src/json-extension-functions.cc
@@ -0,0 +1,915 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file json-extension-functions.cc
+ */
+
+#include <cstring>
+#include <string>
+
+#include "config.h"
+#include "mapbox/variant.hpp"
+#include "scn/scn.h"
+#include "sqlite-extension-func.hh"
+#include "sqlite3.h"
+#include "vtab_module.hh"
+#include "vtab_module_json.hh"
+#include "yajl/api/yajl_gen.h"
+#include "yajlpp/json_op.hh"
+#include "yajlpp/yajlpp.hh"
+
+using namespace mapbox;
+
+#define JSON_SUBTYPE 74 /* Ascii for "J" */
+
+class sql_json_op : public json_op {
+public:
+ explicit sql_json_op(json_ptr& ptr) : json_op(ptr){};
+
+ int sjo_type{-1};
+ std::string sjo_str;
+ int64_t sjo_int{0};
+ double sjo_float{0.0};
+};
+
+static void
+null_or_default(sqlite3_context* context, int argc, sqlite3_value* argv[])
+{
+ if (argc > 2) {
+ sqlite3_result_value(context, argv[2]);
+ } else {
+ sqlite3_result_null(context);
+ }
+}
+
+struct contains_userdata {
+ util::variant<string_fragment, sqlite3_int64, bool> cu_match_value{false};
+ size_t cu_depth{0};
+ bool cu_result{false};
+};
+
+static int
+contains_string(void* ctx, const unsigned char* str, size_t len)
+{
+ auto sf = string_fragment::from_bytes(str, len);
+ auto& cu = *((contains_userdata*) ctx);
+
+ if (cu.cu_depth <= 1 && cu.cu_match_value.get<string_fragment>() == sf) {
+ cu.cu_result = true;
+ }
+
+ return 1;
+}
+
+static int
+contains_integer(void* ctx, long long value)
+{
+ auto& cu = *((contains_userdata*) ctx);
+
+ if (cu.cu_depth <= 1 && cu.cu_match_value.get<sqlite3_int64>() == value) {
+ cu.cu_result = true;
+ }
+
+ return 1;
+}
+
+static int
+contains_null(void* ctx)
+{
+ auto& cu = *((contains_userdata*) ctx);
+
+ cu.cu_result = true;
+
+ return 1;
+}
+
+static bool
+json_contains(vtab_types::nullable<const char> nullable_json_in,
+ sqlite3_value* value)
+{
+ if (nullable_json_in.n_value == nullptr
+ || nullable_json_in.n_value[0] == '\0')
+ {
+ return false;
+ }
+
+ const auto* json_in = nullable_json_in.n_value;
+ auto_mem<yajl_handle_t> handle(yajl_free);
+ yajl_callbacks cb;
+ contains_userdata cu;
+
+ memset(&cb, 0, sizeof(cb));
+ handle = yajl_alloc(&cb, nullptr, &cu);
+
+ cb.yajl_start_array = +[](void* ctx) {
+ auto& cu = *((contains_userdata*) ctx);
+
+ cu.cu_depth += 1;
+
+ return 1;
+ };
+ cb.yajl_end_array = +[](void* ctx) {
+ auto& cu = *((contains_userdata*) ctx);
+
+ cu.cu_depth -= 1;
+
+ return 1;
+ };
+ cb.yajl_start_map = +[](void* ctx) {
+ auto& cu = *((contains_userdata*) ctx);
+
+ cu.cu_depth += 2;
+
+ return 1;
+ };
+ cb.yajl_end_map = +[](void* ctx) {
+ auto& cu = *((contains_userdata*) ctx);
+
+ cu.cu_depth -= 2;
+
+ return 1;
+ };
+
+ switch (sqlite3_value_type(value)) {
+ case SQLITE3_TEXT:
+ cb.yajl_string = contains_string;
+ cu.cu_match_value = string_fragment::from_bytes(
+ sqlite3_value_text(value), sqlite3_value_bytes(value));
+ break;
+ case SQLITE_INTEGER:
+ cb.yajl_integer = contains_integer;
+ cu.cu_match_value = sqlite3_value_int64(value);
+ break;
+ case SQLITE_NULL:
+ cb.yajl_null = contains_null;
+ break;
+ }
+
+ if (yajl_parse(handle.in(), (const unsigned char*) json_in, strlen(json_in))
+ != yajl_status_ok
+ || yajl_complete_parse(handle.in()) != yajl_status_ok)
+ {
+ throw yajlpp_error(handle.in(), json_in, strlen(json_in));
+ }
+
+ return cu.cu_result;
+}
+
+static int
+gen_handle_null(void* ctx)
+{
+ sql_json_op* sjo = (sql_json_op*) ctx;
+ yajl_gen gen = (yajl_gen) sjo->jo_ptr_data;
+
+ if (sjo->jo_ptr.jp_state == json_ptr::match_state_t::DONE) {
+ sjo->sjo_type = SQLITE_NULL;
+ } else {
+ sjo->jo_ptr_error_code = yajl_gen_null(gen);
+ }
+
+ return sjo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_boolean(void* ctx, int boolVal)
+{
+ sql_json_op* sjo = (sql_json_op*) ctx;
+ yajl_gen gen = (yajl_gen) sjo->jo_ptr_data;
+
+ if (sjo->jo_ptr.jp_state == json_ptr::match_state_t::DONE) {
+ sjo->sjo_type = SQLITE_INTEGER;
+ sjo->sjo_int = boolVal;
+ } else {
+ sjo->jo_ptr_error_code = yajl_gen_bool(gen, boolVal);
+ }
+
+ return sjo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_string(void* ctx, const unsigned char* stringVal, size_t len)
+{
+ sql_json_op* sjo = (sql_json_op*) ctx;
+ yajl_gen gen = (yajl_gen) sjo->jo_ptr_data;
+
+ if (sjo->jo_ptr.jp_state == json_ptr::match_state_t::DONE) {
+ sjo->sjo_type = SQLITE3_TEXT;
+ sjo->sjo_str = std::string((char*) stringVal, len);
+ } else {
+ sjo->jo_ptr_error_code = yajl_gen_string(gen, stringVal, len);
+ }
+
+ return sjo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_number(void* ctx, const char* numval, size_t numlen)
+{
+ sql_json_op* sjo = (sql_json_op*) ctx;
+ yajl_gen gen = (yajl_gen) sjo->jo_ptr_data;
+
+ if (sjo->jo_ptr.jp_state == json_ptr::match_state_t::DONE) {
+ auto num_sv = scn::string_view{numval, numlen};
+ auto scan_int_res = scn::scan_value<int64_t>(num_sv);
+
+ if (scan_int_res && scan_int_res.empty()) {
+ sjo->sjo_int = scan_int_res.value();
+ sjo->sjo_type = SQLITE_INTEGER;
+ } else {
+ auto scan_float_res = scn::scan_value<double>(num_sv);
+
+ sjo->sjo_float = scan_float_res.value();
+ sjo->sjo_type = SQLITE_FLOAT;
+ }
+ } else {
+ sjo->jo_ptr_error_code = yajl_gen_number(gen, numval, numlen);
+ }
+
+ return sjo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static void
+sql_jget(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ if (argc < 2) {
+ sqlite3_result_error(context, "expecting JSON value and pointer", -1);
+ return;
+ }
+
+ if (sqlite3_value_type(argv[0]) == SQLITE_NULL) {
+ null_or_default(context, argc, argv);
+ return;
+ }
+
+ const char* json_in = (const char*) sqlite3_value_text(argv[0]);
+
+ if (sqlite3_value_type(argv[1]) == SQLITE_NULL) {
+ sqlite3_result_text(context, json_in, -1, SQLITE_TRANSIENT);
+ return;
+ }
+
+ const char* ptr_in = (const char*) sqlite3_value_text(argv[1]);
+ json_ptr jp(ptr_in);
+ sql_json_op jo(jp);
+ auto_mem<yajl_handle_t> handle(yajl_free);
+ unsigned char* err;
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ jo.jo_ptr_callbacks = json_op::gen_callbacks;
+ jo.jo_ptr_callbacks.yajl_null = gen_handle_null;
+ jo.jo_ptr_callbacks.yajl_boolean = gen_handle_boolean;
+ jo.jo_ptr_callbacks.yajl_string = gen_handle_string;
+ jo.jo_ptr_callbacks.yajl_number = gen_handle_number;
+ jo.jo_ptr_data = gen.get_handle();
+
+ handle.reset(yajl_alloc(&json_op::ptr_callbacks, nullptr, &jo));
+ switch (yajl_parse(
+ handle.in(), (const unsigned char*) json_in, strlen(json_in)))
+ {
+ case yajl_status_error: {
+ err = yajl_get_error(handle.in(),
+ 1,
+ (const unsigned char*) json_in,
+ strlen(json_in));
+ sqlite3_result_error(context, (const char*) err, -1);
+ yajl_free_error(handle.in(), err);
+ return;
+ }
+ case yajl_status_client_canceled:
+ if (jo.jo_ptr.jp_state
+ == json_ptr::match_state_t::ERR_INVALID_ESCAPE)
+ {
+ sqlite3_result_error(
+ context, jo.jo_ptr.error_msg().c_str(), -1);
+ } else {
+ null_or_default(context, argc, argv);
+ }
+ return;
+ default:
+ break;
+ }
+
+ switch (yajl_complete_parse(handle.in())) {
+ case yajl_status_error: {
+ err = yajl_get_error(handle.in(),
+ 1,
+ (const unsigned char*) json_in,
+ strlen(json_in));
+ sqlite3_result_error(context, (const char*) err, -1);
+ yajl_free_error(handle.in(), err);
+ return;
+ }
+ case yajl_status_client_canceled:
+ if (jo.jo_ptr.jp_state
+ == json_ptr::match_state_t::ERR_INVALID_ESCAPE)
+ {
+ sqlite3_result_error(
+ context, jo.jo_ptr.error_msg().c_str(), -1);
+ } else {
+ null_or_default(context, argc, argv);
+ }
+ return;
+ default:
+ break;
+ }
+
+ switch (jo.sjo_type) {
+ case SQLITE3_TEXT:
+ sqlite3_result_text(context,
+ jo.sjo_str.c_str(),
+ jo.sjo_str.size(),
+ SQLITE_TRANSIENT);
+ return;
+ case SQLITE_NULL:
+ sqlite3_result_null(context);
+ return;
+ case SQLITE_INTEGER:
+ sqlite3_result_int(context, jo.sjo_int);
+ return;
+ case SQLITE_FLOAT:
+ sqlite3_result_double(context, jo.sjo_float);
+ return;
+ }
+
+ string_fragment result = gen.to_string_fragment();
+
+ if (result.empty()) {
+ null_or_default(context, argc, argv);
+ return;
+ }
+
+ sqlite3_result_text(
+ context, result.data(), result.length(), SQLITE_TRANSIENT);
+#ifdef HAVE_SQLITE3_VALUE_SUBTYPE
+ sqlite3_result_subtype(context, JSON_SUBTYPE);
+#endif
+}
+
+struct concat_context {
+ concat_context(yajl_gen gen_handle) : cc_gen_handle(gen_handle) {}
+
+ yajl_gen cc_gen_handle;
+ int cc_depth{0};
+};
+
+static int
+concat_gen_null(void* ctx)
+{
+ auto* cc = static_cast<concat_context*>(ctx);
+
+ if (cc->cc_depth > 0) {
+ return yajl_gen_null(cc->cc_gen_handle) == yajl_gen_status_ok;
+ }
+
+ return 1;
+}
+
+static int
+concat_gen_boolean(void* ctx, int val)
+{
+ auto* cc = static_cast<concat_context*>(ctx);
+
+ return yajl_gen_bool(cc->cc_gen_handle, val) == yajl_gen_status_ok;
+}
+
+static int
+concat_gen_number(void* ctx, const char* val, size_t len)
+{
+ auto* cc = static_cast<concat_context*>(ctx);
+
+ return yajl_gen_number(cc->cc_gen_handle, val, len) == yajl_gen_status_ok;
+}
+
+static int
+concat_gen_string(void* ctx, const unsigned char* val, size_t len)
+{
+ auto* cc = static_cast<concat_context*>(ctx);
+
+ return yajl_gen_string(cc->cc_gen_handle, val, len) == yajl_gen_status_ok;
+}
+
+static int
+concat_gen_start_map(void* ctx)
+{
+ auto* cc = static_cast<concat_context*>(ctx);
+
+ cc->cc_depth += 1;
+ return yajl_gen_map_open(cc->cc_gen_handle) == yajl_gen_status_ok;
+}
+
+static int
+concat_gen_end_map(void* ctx)
+{
+ auto* cc = static_cast<concat_context*>(ctx);
+
+ cc->cc_depth -= 1;
+ return yajl_gen_map_close(cc->cc_gen_handle) == yajl_gen_status_ok;
+}
+
+static int
+concat_gen_map_key(void* ctx, const unsigned char* key, size_t len)
+{
+ auto* cc = static_cast<concat_context*>(ctx);
+
+ return yajl_gen_string(cc->cc_gen_handle, key, len) == yajl_gen_status_ok;
+}
+
+static int
+concat_gen_start_array(void* ctx)
+{
+ auto* cc = static_cast<concat_context*>(ctx);
+
+ cc->cc_depth += 1;
+ if (cc->cc_depth == 1) {
+ return 1;
+ }
+ return yajl_gen_array_open(cc->cc_gen_handle) == yajl_gen_status_ok;
+}
+
+static int
+concat_gen_end_array(void* ctx)
+{
+ auto* cc = static_cast<concat_context*>(ctx);
+
+ cc->cc_depth -= 1;
+ if (cc->cc_depth == 0) {
+ return 1;
+ }
+ return yajl_gen_array_close(cc->cc_gen_handle) == yajl_gen_status_ok;
+}
+
+static void
+concat_gen_elements(yajl_gen gen, const unsigned char* text, size_t len)
+{
+ auto_mem<yajl_handle_t> handle(yajl_free);
+ yajl_callbacks callbacks = {nullptr};
+ concat_context cc{gen};
+
+ callbacks.yajl_null = concat_gen_null;
+ callbacks.yajl_boolean = concat_gen_boolean;
+ callbacks.yajl_number = concat_gen_number;
+ callbacks.yajl_string = concat_gen_string;
+ callbacks.yajl_start_map = concat_gen_start_map;
+ callbacks.yajl_end_map = concat_gen_end_map;
+ callbacks.yajl_map_key = concat_gen_map_key;
+ callbacks.yajl_start_array = concat_gen_start_array;
+ callbacks.yajl_end_array = concat_gen_end_array;
+
+ handle = yajl_alloc(&callbacks, nullptr, &cc);
+ yajl_config(handle, yajl_allow_comments, 1);
+ if (yajl_parse(handle, (const unsigned char*) text, len) != yajl_status_ok
+ || yajl_complete_parse(handle) != yajl_status_ok)
+ {
+ std::unique_ptr<unsigned char, decltype(&free)> err_msg(
+ yajl_get_error(handle, 1, (const unsigned char*) text, len), free);
+
+ throw sqlite_func_error("Invalid JSON: {}",
+ (const char*) err_msg.get());
+ }
+}
+
+static json_string
+json_concat(nonstd::optional<const char*> json_in,
+ const std::vector<sqlite3_value*>& values)
+{
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_array array(gen);
+
+ if (json_in) {
+ concat_gen_elements(gen,
+ (const unsigned char*) json_in.value(),
+ strlen(json_in.value()));
+ }
+
+ for (auto* const val : values) {
+ switch (sqlite3_value_type(val)) {
+ case SQLITE_NULL:
+ array.gen();
+ break;
+ case SQLITE_INTEGER:
+ array.gen(sqlite3_value_int64(val));
+ break;
+ case SQLITE_FLOAT:
+ array.gen(sqlite3_value_double(val));
+ break;
+ case SQLITE3_TEXT: {
+ const auto* text_val = sqlite3_value_text(val);
+
+ if (sqlite3_value_subtype(val) == JSON_SUBTYPE) {
+ concat_gen_elements(
+ gen, text_val, strlen((const char*) text_val));
+ } else {
+ array.gen((const char*) text_val);
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ return json_string(gen);
+}
+
+#if 0
+static flattened_json_string
+sql_flatten_json_object(string_fragment sf)
+{
+ yajlpp_gen gen;
+
+ {
+ json_ptr jp("/");
+ json_op jo(jp);
+ auto_mem<yajl_handle_t> handle(yajl_free);
+
+ jo.jo_ptr_data = gen.get_handle();
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+ handle.reset(yajl_alloc(&json_op::gen_callbacks, nullptr, &jo));
+ switch (yajl_parse(
+ handle.in(), (const unsigned char*) sf.data(), sf.length())) {
+ case yajl_status_error:
+ case yajl_status_client_canceled:
+ throw yajlpp_error(handle.in(), sf.data(), sf.length());
+ case yajl_status_ok:
+ break;
+ }
+ switch (yajl_complete_parse(handle.in())) {
+ case yajl_status_error:
+ case yajl_status_client_canceled:
+ throw yajlpp_error(handle.in(), sf.data(), sf.length());
+ case yajl_status_ok:
+ break;
+ }
+ }
+
+ auto result = gen.to_string_fragment();
+ if (!result.startswith("{") || !result.endswith("}")) {
+ throw std::runtime_error(
+ "flatten_json_object() requires a JSON object");
+ }
+
+ return flattened_json_string(gen);
+}
+#endif
+
+struct json_agg_context {
+ yajl_gen_t* jac_yajl_gen;
+};
+
+static void
+sql_json_group_object_step(sqlite3_context* context,
+ int argc,
+ sqlite3_value** argv)
+{
+ if ((argc % 2) == 1) {
+ sqlite3_result_error(
+ context,
+ "Uneven number of arguments to json_group_object(), "
+ "expecting key and value pairs",
+ -1);
+ return;
+ }
+
+ json_agg_context* jac = (json_agg_context*) sqlite3_aggregate_context(
+ context, sizeof(json_agg_context));
+
+ if (jac->jac_yajl_gen == nullptr) {
+ jac->jac_yajl_gen = yajl_gen_alloc(nullptr);
+ yajl_gen_config(jac->jac_yajl_gen, yajl_gen_beautify, false);
+
+ yajl_gen_map_open(jac->jac_yajl_gen);
+ }
+
+ for (int lpc = 0; (lpc + 1) < argc; lpc += 2) {
+ if (sqlite3_value_type(argv[lpc]) == SQLITE_NULL) {
+ continue;
+ }
+
+ const unsigned char* key = sqlite3_value_text(argv[lpc]);
+
+ yajl_gen_string(jac->jac_yajl_gen, key, strlen((const char*) key));
+
+ switch (sqlite3_value_type(argv[lpc + 1])) {
+ case SQLITE_NULL:
+ yajl_gen_null(jac->jac_yajl_gen);
+ break;
+ case SQLITE3_TEXT: {
+ const unsigned char* value = sqlite3_value_text(argv[lpc + 1]);
+#ifdef HAVE_SQLITE3_VALUE_SUBTYPE
+ int subtype = sqlite3_value_subtype(argv[lpc + 1]);
+
+ if (subtype == JSON_SUBTYPE) {
+ yajl_gen_number(jac->jac_yajl_gen,
+ (const char*) value,
+ strlen((const char*) value));
+ } else {
+#endif
+ yajl_gen_string(
+ jac->jac_yajl_gen, value, strlen((const char*) value));
+#ifdef HAVE_SQLITE3_VALUE_SUBTYPE
+ }
+#endif
+ break;
+ }
+ case SQLITE_INTEGER: {
+ const unsigned char* value = sqlite3_value_text(argv[lpc + 1]);
+
+ yajl_gen_number(jac->jac_yajl_gen,
+ (const char*) value,
+ strlen((const char*) value));
+ break;
+ }
+ case SQLITE_FLOAT: {
+ double value = sqlite3_value_double(argv[lpc + 1]);
+
+ yajl_gen_double(jac->jac_yajl_gen, value);
+ break;
+ }
+ }
+ }
+}
+
+static void
+sql_json_group_object_final(sqlite3_context* context)
+{
+ json_agg_context* jac
+ = (json_agg_context*) sqlite3_aggregate_context(context, 0);
+
+ if (jac == nullptr) {
+ sqlite3_result_text(context, "{}", -1, SQLITE_STATIC);
+ } else {
+ const unsigned char* buf;
+ size_t len;
+
+ yajl_gen_map_close(jac->jac_yajl_gen);
+ yajl_gen_get_buf(jac->jac_yajl_gen, &buf, &len);
+ sqlite3_result_text(context, (const char*) buf, len, SQLITE_TRANSIENT);
+#ifdef HAVE_SQLITE3_VALUE_SUBTYPE
+ sqlite3_result_subtype(context, JSON_SUBTYPE);
+#endif
+ yajl_gen_free(jac->jac_yajl_gen);
+ }
+}
+
+static void
+sql_json_group_array_step(sqlite3_context* context,
+ int argc,
+ sqlite3_value** argv)
+{
+ json_agg_context* jac = (json_agg_context*) sqlite3_aggregate_context(
+ context, sizeof(json_agg_context));
+
+ if (jac->jac_yajl_gen == nullptr) {
+ jac->jac_yajl_gen = yajl_gen_alloc(nullptr);
+ yajl_gen_config(jac->jac_yajl_gen, yajl_gen_beautify, false);
+
+ yajl_gen_array_open(jac->jac_yajl_gen);
+ }
+
+ for (int lpc = 0; lpc < argc; lpc++) {
+ switch (sqlite3_value_type(argv[lpc])) {
+ case SQLITE_NULL:
+ yajl_gen_null(jac->jac_yajl_gen);
+ break;
+ case SQLITE3_TEXT: {
+ const unsigned char* value = sqlite3_value_text(argv[lpc]);
+#ifdef HAVE_SQLITE3_VALUE_SUBTYPE
+ int subtype = sqlite3_value_subtype(argv[lpc]);
+
+ if (subtype == JSON_SUBTYPE) {
+ yajl_gen_number(jac->jac_yajl_gen,
+ (const char*) value,
+ strlen((const char*) value));
+ } else {
+#endif
+ yajl_gen_string(
+ jac->jac_yajl_gen, value, strlen((const char*) value));
+#ifdef HAVE_SQLITE3_VALUE_SUBTYPE
+ }
+#endif
+ break;
+ }
+ case SQLITE_INTEGER: {
+ const unsigned char* value = sqlite3_value_text(argv[lpc]);
+
+ yajl_gen_number(jac->jac_yajl_gen,
+ (const char*) value,
+ strlen((const char*) value));
+ break;
+ }
+ case SQLITE_FLOAT: {
+ double value = sqlite3_value_double(argv[lpc]);
+
+ yajl_gen_double(jac->jac_yajl_gen, value);
+ break;
+ }
+ }
+ }
+}
+
+static void
+sql_json_group_array_final(sqlite3_context* context)
+{
+ json_agg_context* jac
+ = (json_agg_context*) sqlite3_aggregate_context(context, 0);
+
+ if (jac == nullptr) {
+ sqlite3_result_text(context, "[]", -1, SQLITE_STATIC);
+ } else {
+ const unsigned char* buf;
+ size_t len;
+
+ yajl_gen_array_close(jac->jac_yajl_gen);
+ yajl_gen_get_buf(jac->jac_yajl_gen, &buf, &len);
+ sqlite3_result_text(context, (const char*) buf, len, SQLITE_TRANSIENT);
+#ifdef HAVE_SQLITE3_VALUE_SUBTYPE
+ sqlite3_result_subtype(context, JSON_SUBTYPE);
+#endif
+ yajl_gen_free(jac->jac_yajl_gen);
+ }
+}
+
+int
+json_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs)
+{
+ static struct FuncDef json_funcs[] = {
+ sqlite_func_adapter<decltype(&json_concat), json_concat>::builder(
+ help_text("json_concat",
+ "Returns an array with the given values concatenated "
+ "onto the end. "
+ "If the initial value is null, the result will be an "
+ "array with "
+ "the given elements. If the initial value is an array, "
+ "the result "
+ "will be an array with the given values at the end. If "
+ "the initial "
+ "value is not null or an array, the result will be an "
+ "array with "
+ "two elements: the initial value and the given value.")
+ .sql_function()
+ .with_parameter({"json", "The initial JSON value."})
+ .with_parameter(
+ help_text("value",
+ "The value(s) to add to the end of the array.")
+ .one_or_more())
+ .with_tags({"json"})
+ .with_example({
+ "To append the number 4 to null",
+ "SELECT json_concat(NULL, 4)",
+ })
+ .with_example({
+ "To append 4 and 5 to the array [1, 2, 3]",
+ "SELECT json_concat('[1, 2, 3]', 4, 5)",
+ })
+ .with_example({
+ "To concatenate two arrays together",
+ "SELECT json_concat('[1, 2, 3]', json('[4, 5]'))",
+ })),
+
+ sqlite_func_adapter<decltype(&json_contains), json_contains>::builder(
+ help_text("json_contains",
+ "Check if a JSON value contains the given element.")
+ .sql_function()
+ .with_parameter({"json", "The JSON value to query."})
+ .with_parameter(
+ {"value", "The value to look for in the first argument"})
+ .with_tags({"json"})
+ .with_example({
+ "To test if a JSON array contains the number 4",
+ "SELECT json_contains('[1, 2, 3]', 4)",
+ })
+ .with_example({
+ "To test if a JSON array contains the string 'def'",
+ "SELECT json_contains('[\"abc\", \"def\"]', 'def')",
+ })),
+
+ {
+ "jget",
+ -1,
+ SQLITE_UTF8,
+ 0,
+ sql_jget,
+ help_text("jget",
+ "Get the value from a JSON object using a JSON-Pointer.")
+ .sql_function()
+ .with_parameter({"json", "The JSON object to query."})
+ .with_parameter(
+ {"ptr", "The JSON-Pointer to lookup in the object."})
+ .with_parameter(
+ help_text("default",
+ "The default value if the value was not found")
+ .optional())
+ .with_tags({"json"})
+ .with_example(
+ {"To get the root of a JSON value", "SELECT jget('1', '')"})
+ .with_example({
+ "To get the property named 'b' in a JSON object",
+ "SELECT jget('{ \"a\": 1, \"b\": 2 }', '/b')",
+ })
+ .with_example({
+ "To get the 'msg' property and return a default if "
+ "it does not exist",
+ "SELECT jget(null, '/msg', 'Hello')",
+ }),
+ },
+
+#if 0
+ sqlite_func_adapter<decltype(&sql_flatten_json_object),
+ sql_flatten_json_object>::
+ builder(help_text("flatten_json_object", "hello")
+ .sql_function()
+ .with_parameter({"json", "The JSON"})),
+#endif
+
+ {nullptr},
+ };
+
+ static struct FuncDefAgg json_agg_funcs[] = {
+ {
+ "json_group_object",
+ -1,
+ 0,
+ sql_json_group_object_step,
+ sql_json_group_object_final,
+ help_text("json_group_object")
+ .sql_function()
+ .with_summary(
+ "Collect the given values from a query into a JSON object")
+ .with_parameter(
+ help_text("name", "The property name for the value"))
+ .with_parameter(
+ help_text("value", "The value to add to the object")
+ .one_or_more())
+ .with_tags({"json"})
+ .with_example({"To create an object from arguments",
+ "SELECT json_group_object('a', 1, 'b', 2)"})
+ .with_example({
+ "To create an object from a pair of columns",
+ "SELECT json_group_object(column1, column2) FROM "
+ "(VALUES ('a', 1), ('b', 2))",
+ }),
+ },
+ {
+ "json_group_array",
+ -1,
+ 0,
+ sql_json_group_array_step,
+ sql_json_group_array_final,
+ help_text("json_group_array")
+ .sql_function()
+ .with_summary(
+ "Collect the given values from a query into a JSON array")
+ .with_parameter(
+ help_text("value", "The values to append to the array")
+ .one_or_more())
+ .with_tags({"json"})
+ .with_example({
+ "To create an array from arguments",
+ "SELECT json_group_array('one', 2, 3.4)",
+ })
+ .with_example({
+ "To create an array from a column of values",
+ "SELECT json_group_array(column1) FROM (VALUES "
+ "(1), (2), (3))",
+ }),
+ },
+
+ {nullptr},
+ };
+
+ *basic_funcs = json_funcs;
+ *agg_funcs = json_agg_funcs;
+
+ return SQLITE_OK;
+}
diff --git a/src/k_merge_tree.h b/src/k_merge_tree.h
new file mode 100644
index 0000000..2310628
--- /dev/null
+++ b/src/k_merge_tree.h
@@ -0,0 +1,471 @@
+/*
+
+ K-Way Merge Template
+ By Jordan Zimmerman
+
+*/
+
+#ifndef KMERGE_TREE_H
+#define KMERGE_TREE_H
+
+/*
+
+K-Way Merge
+
+An implementation of "k-Way Merging" as described in "Fundamentals of Data Structures" by Horowitz/Sahni.
+
+The idea is to merge k sorted arrays limiting the number of comparisons. A tree is built containing the
+results of comparing the heads of each array. The top most node is always the smallest entry. Then, its
+corresponding leaf in the tree is refilled and the tree is processed again. It's easier to see in the
+following example:
+
+Imagine 4 sorted arrays:
+{5, 10, 15, 20}
+{10, 13, 16, 19}
+{2, 19, 26, 40}
+{18, 22, 23, 24}
+
+The initial tree looks like this:
+
+
+ 2
+ / \
+ 2 5
+ / \ / \
+ 18 2 10 5
+
+The '/' and '\' represent links. The bottom row has the leaves and they contain the heads of the arrays.
+The rows above the leaves represent the smaller of the two child nodes. Thus, the top node is the smallest.
+To process the next iteration, the top node gets popped and its leaf gets refilled. Then, the new leaf's
+associated nodes are processed. So, after the 2 is taken, it is filled with 19 (the next head of its array).
+After processing, the tree looks like this:
+
+
+ 5
+ / \
+ 18 5
+ / \ / \
+ 18 19 10 5
+
+So, you can see how the number of comparisons is reduced.
+
+A good use of this is when you have a very large array that needs to be sorted. Break it up into n small
+arrays and sort those. Then use this merge sort for the final sort. This can also be done with files. If
+you have n sorted files, you can merge them into one sorted file. K Way Merging works best when comparing
+is somewhat expensive.
+
+*/
+
+#include <math.h>
+
+#include <functional>
+
+template <class T, class owner_t, class iterator_t, class comparitor = std::less<T> >
+class kmerge_tree_c
+{
+
+public:
+ // create the tree with the given number of buckets. Call add() for each of the buckets
+ // and then call execute() to build things. Call get_top() then next() until get_top returns
+ // false.
+ kmerge_tree_c(long bucket_qty);
+ ~kmerge_tree_c();
+
+ // add a sorted collection to the tree
+ //
+ // begin/end - start end of a collection
+ void add(owner_t *owner, iterator_t begin, iterator_t end);
+
+ // process the first sort
+ void execute(void);
+
+ // advance to the next entry
+ void next(void);
+
+ // return the next entry without re-processing the tree
+ // if false is returned, the merge is complete
+ bool get_top(owner_t *&owner, iterator_t& iterator)
+ {
+ if (top_node_ptr_mbr->has_iterator) {
+ owner = top_node_ptr_mbr->owner_ptr;
+ iterator = top_node_ptr_mbr->current_iterator;
+ return iterator != top_node_ptr_mbr->end_iterator;
+ }
+ else {
+ return false;
+ }
+ }
+
+private:
+ class node_rec
+ {
+
+ public:
+ node_rec(void) :
+ left_child_ptr(NULL),
+ right_child_ptr(NULL),
+ parent_ptr(NULL),
+ next_ptr(NULL),
+ previous_ptr(NULL),
+ next_leaf_ptr(NULL),
+ source_node_ptr(NULL),
+ owner_ptr(NULL),
+ has_iterator(false)
+ {
+ }
+ ~node_rec()
+ {
+ delete left_child_ptr;
+ delete right_child_ptr;
+ }
+
+ node_rec* left_child_ptr; // owned the left child of this node
+ node_rec* right_child_ptr; // owned the right child of this node
+ node_rec* parent_ptr; // copy the parent of this node
+ node_rec* next_ptr; // copy the next sibling
+ node_rec* previous_ptr; // copy the previous sibling
+ node_rec* next_leaf_ptr; // copy only for the bottom rows, a linked list of the buckets
+ node_rec* source_node_ptr; // copy ptr back to the node from whence this iterator came
+ owner_t *owner_ptr;
+ int has_iterator;
+ iterator_t current_iterator;
+ iterator_t end_iterator;
+
+ private:
+ node_rec(const node_rec&);
+ node_rec& operator=(const node_rec&);
+
+ };
+
+ void build_tree(void);
+ node_rec* build_levels(long number_of_levels);
+ void build_left_siblings(kmerge_tree_c::node_rec* node_ptr);
+ void build_right_siblings(kmerge_tree_c::node_rec* node_ptr);
+ void compare_nodes(kmerge_tree_c::node_rec* node_ptr);
+
+ comparitor comparitor_mbr;
+ long bucket_qty_mbr;
+ long number_of_levels_mbr;
+ node_rec* top_node_ptr_mbr; // owned
+ node_rec* first_leaf_ptr; // copy
+ node_rec* last_leaf_ptr; // copy
+
+};
+
+inline long kmerge_tree_brute_log2(long value)
+{
+
+ long square = 2;
+ long count = 1;
+ while ( square < value )
+ {
+ square *= 2;
+ ++count;
+ }
+
+ return count;
+
+} // kmerge_tree_brute_log2
+
+//~~~~~~~~~~class kmerge_tree_c
+
+template <class T, class owner_t, class iterator_t, class comparitor>
+kmerge_tree_c<T, owner_t, iterator_t, comparitor>::kmerge_tree_c(long bucket_qty) :
+ bucket_qty_mbr(bucket_qty),
+ number_of_levels_mbr(bucket_qty ? ::kmerge_tree_brute_log2(bucket_qty_mbr) : 0), // don't add one - build_levels is zero based
+ top_node_ptr_mbr(NULL),
+ first_leaf_ptr(NULL),
+ last_leaf_ptr(NULL)
+{
+
+ build_tree();
+
+}
+
+template <class T, class owner_t, class iterator_t, class comparitor>
+kmerge_tree_c<T, owner_t, iterator_t, comparitor>::~kmerge_tree_c()
+{
+
+ delete top_node_ptr_mbr;
+
+}
+
+/*
+ Unlike the book, I'm going to make my life easy
+ by maintaining every possible link to each node that I might need
+*/
+template <class T, class owner_t, class iterator_t, class comparitor>
+void kmerge_tree_c<T, owner_t, iterator_t, comparitor>::build_tree(void)
+{
+
+ // the book says that the number of levels is (log2 * k) + 1
+ top_node_ptr_mbr = build_levels(number_of_levels_mbr);
+ if ( top_node_ptr_mbr )
+ {
+ build_left_siblings(top_node_ptr_mbr);
+ build_right_siblings(top_node_ptr_mbr);
+ }
+
+}
+
+/*
+ highly recursive tree builder
+ as long as number_of_levels isn't zero, each node builds its own children
+ and updates the parent link for them.
+
+ If no children are to be built (i.e. number_of_levels is 0), then the leaf linked list is created
+*/
+template <class T, class owner_t, class iterator_t, class comparitor>
+typename kmerge_tree_c<T, owner_t, iterator_t, comparitor>::node_rec *
+kmerge_tree_c<T, owner_t, iterator_t, comparitor>::build_levels(long number_of_levels)
+{
+
+ node_rec* node_ptr = new node_rec;
+
+ if ( number_of_levels )
+ {
+ node_ptr->left_child_ptr = build_levels(number_of_levels - 1);
+ if ( node_ptr->left_child_ptr )
+ {
+ node_ptr->left_child_ptr->parent_ptr = node_ptr;
+ node_ptr->right_child_ptr = build_levels(number_of_levels - 1);
+ if ( node_ptr->right_child_ptr )
+ {
+ node_ptr->right_child_ptr->parent_ptr = node_ptr;
+ }
+ }
+ }
+ else
+ {
+ if ( last_leaf_ptr )
+ {
+ last_leaf_ptr->next_leaf_ptr = node_ptr;
+ last_leaf_ptr = node_ptr;
+ }
+ else
+ {
+ first_leaf_ptr = last_leaf_ptr = node_ptr;
+ }
+ }
+
+ return node_ptr;
+
+}
+
+/*
+ when we process a comparison, we'll have to compare two siblings
+ this code matches each link with its sibling
+
+ To get every node correct, I had to write two routines: one which works
+ with left nodes and one which works with right nodes. build_tree() starts it
+ off with the top node's children and then these two swing back and forth
+ between each other.
+*/
+
+template <class T, class owner_t, class iterator_t, class comparitor>
+void kmerge_tree_c<T, owner_t, iterator_t, comparitor>::build_left_siblings(kmerge_tree_c<T, owner_t, iterator_t, comparitor>::node_rec* node_ptr)
+{
+
+ if ( node_ptr->parent_ptr )
+ {
+ if ( node_ptr->parent_ptr->right_child_ptr )
+ {
+ node_ptr->next_ptr = node_ptr->parent_ptr->right_child_ptr;
+ node_ptr->parent_ptr->right_child_ptr->previous_ptr = node_ptr;
+ }
+ }
+
+ if ( node_ptr->left_child_ptr )
+ {
+ build_left_siblings(node_ptr->left_child_ptr);
+ }
+
+ if ( node_ptr->right_child_ptr )
+ {
+ build_right_siblings(node_ptr->right_child_ptr);
+ }
+
+}
+
+template <class T, class owner_t, class iterator_t, class comparitor>
+void kmerge_tree_c<T, owner_t, iterator_t, comparitor>::build_right_siblings(kmerge_tree_c<T, owner_t, iterator_t, comparitor>::node_rec* node_ptr)
+{
+
+ if ( node_ptr->parent_ptr )
+ {
+ if ( node_ptr->parent_ptr->left_child_ptr )
+ {
+ node_ptr->previous_ptr = node_ptr->parent_ptr->left_child_ptr;
+ node_ptr->parent_ptr->left_child_ptr->next_ptr = node_ptr;
+ }
+ }
+
+ if ( node_ptr->right_child_ptr )
+ {
+ build_right_siblings(node_ptr->right_child_ptr);
+ }
+
+ if ( node_ptr->left_child_ptr )
+ {
+ build_left_siblings(node_ptr->left_child_ptr);
+ }
+
+}
+
+// fill the leaf linked list
+template <class T, class owner_t, class iterator_t, class comparitor>
+void kmerge_tree_c<T, owner_t, iterator_t, comparitor>::add(owner_t *owner, iterator_t begin, iterator_t end)
+{
+
+ if ( begin == end )
+ {
+ return;
+ }
+
+ node_rec* node_ptr = first_leaf_ptr;
+ while ( node_ptr )
+ {
+ if ( node_ptr->has_iterator )
+ {
+ node_ptr = node_ptr->next_leaf_ptr;
+ }
+ else
+ {
+ node_ptr->has_iterator = true;
+ node_ptr->owner_ptr = owner;
+ node_ptr->current_iterator = begin;
+ node_ptr->end_iterator = end;
+ break;
+ }
+ }
+}
+
+/*
+ fill the initial tree by comparing each sibling in the
+ leaf linked list and then factoring that up to the parents
+ This is only done once so it doesn't have to be that efficient
+*/
+template <class T, class owner_t, class iterator_t, class comparitor>
+void kmerge_tree_c<T, owner_t, iterator_t, comparitor>::execute(void)
+{
+
+ for ( long parent_level = 0; parent_level < number_of_levels_mbr; ++parent_level )
+ {
+ // the number of nodes to skip is (parent level + 1) ^ 2 - 1
+ long skip_nodes = 2;
+ for ( long skip = 0; skip < parent_level; ++skip )
+ {
+ skip_nodes *= 2;
+ }
+ --skip_nodes;
+
+ node_rec* node_ptr = first_leaf_ptr;
+ while ( node_ptr )
+ {
+ node_rec* parent_ptr = node_ptr;
+ long i;
+ for ( i = 0; i < parent_level; ++i )
+ {
+ parent_ptr = parent_ptr->parent_ptr;
+ }
+
+ compare_nodes(parent_ptr);
+
+ for ( i = 0; i < skip_nodes; ++i )
+ {
+ node_ptr = node_ptr->next_leaf_ptr;
+ }
+
+ node_ptr = node_ptr->next_leaf_ptr;
+ }
+ }
+
+}
+
+// compare the given node and its sibling and bubble the result up to the parent
+template <class T, class owner_t, class iterator_t, class comparitor>
+void kmerge_tree_c<T, owner_t, iterator_t, comparitor>::compare_nodes(kmerge_tree_c<T, owner_t, iterator_t, comparitor>::node_rec* node_ptr)
+{
+
+// assert(node_ptr->parent_ptr);
+
+ node_rec* node1_ptr = NULL;
+ node_rec* node2_ptr = NULL;
+ if ( node_ptr->next_ptr )
+ {
+ node1_ptr = node_ptr;
+ node2_ptr = node_ptr->next_ptr;
+ }
+ else
+ {
+ node1_ptr = node_ptr->previous_ptr;
+ node2_ptr = node_ptr;
+ }
+
+ long result;
+ if ( !node2_ptr->has_iterator ) {
+ result = -1;
+ }
+ else if ( !node1_ptr->has_iterator) {
+ result = 1;
+ }
+ else if ( (node1_ptr->current_iterator != node1_ptr->end_iterator) && (node2_ptr->current_iterator != node2_ptr->end_iterator) )
+ {
+ // no need to check for exact equality - we just want the lesser of the two
+ result = comparitor_mbr(*node1_ptr->current_iterator, *node2_ptr->current_iterator) ? -1 : 1;
+ }
+ else if ( node1_ptr->current_iterator != node1_ptr->end_iterator )
+ {
+ result = -1;
+ }
+ else // if ( node2_ptr->current_iterator != node2_ptr->end_iterator )
+ {
+ result = 1;
+ }
+
+ node_rec* winner_ptr = (result <= 0) ? node1_ptr : node2_ptr;
+ node_rec* parent_ptr = node_ptr->parent_ptr;
+
+ parent_ptr->owner_ptr = winner_ptr->owner_ptr;
+ parent_ptr->has_iterator = winner_ptr->has_iterator;
+ parent_ptr->current_iterator = winner_ptr->current_iterator;
+ parent_ptr->end_iterator = winner_ptr->end_iterator;
+ parent_ptr->source_node_ptr = winner_ptr;
+
+}
+
+/*
+ pop the top node, factor it down the list to find its
+ leaf, replace its leaf and then factor that back up
+*/
+template <class T, class owner_t, class iterator_t, class comparitor>
+void kmerge_tree_c<T, owner_t, iterator_t, comparitor>::next(void)
+{
+
+ if ( top_node_ptr_mbr->current_iterator == top_node_ptr_mbr->end_iterator )
+ {
+ return;
+ }
+
+ // find the source leaf ptr
+ node_rec* node_ptr = top_node_ptr_mbr;
+ while ( node_ptr->source_node_ptr )
+ {
+ node_ptr = node_ptr->source_node_ptr;
+ }
+
+// assert(!node_ptr->left_child_ptr && !node_ptr->right_child_ptr);
+// assert(top_node_ptr_mbr->current_iterator == node_ptr->current_iterator);
+
+ ++node_ptr->current_iterator;
+
+ while ( node_ptr->parent_ptr )
+ {
+ compare_nodes(node_ptr);
+ node_ptr = node_ptr->parent_ptr;
+ }
+
+}
+
+#endif // KMERGE_TREE_H
+
diff --git a/src/keymaps/README.md b/src/keymaps/README.md
new file mode 100644
index 0000000..b30f920
--- /dev/null
+++ b/src/keymaps/README.md
@@ -0,0 +1,5 @@
+# Keymaps
+
+This directory contains the built-in keymap definitions. The files are
+turned into C using `bin2c` and compiled into the executable. New keymaps
+need to be added to the [keymaps.am](keymaps.am) file.
diff --git a/src/keymaps/de-keymap.json b/src/keymaps/de-keymap.json
new file mode 100644
index 0000000..540a278
--- /dev/null
+++ b/src/keymaps/de-keymap.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "keymap-defs": {
+ "de": {
+ "x31": {
+ "command": ":goto next 10 minutes after the hour"
+ },
+ "x32": {
+ "command": ":goto next 20 minutes after the hour"
+ },
+ "x33": {
+ "command": ":goto next 30 minutes after the hour"
+ },
+ "x34": {
+ "command": ":goto next 40 minutes after the hour"
+ },
+ "x35": {
+ "command": ":goto next 50 minutes after the hour"
+ },
+ "x36": {
+ "command": ":goto next hour"
+ },
+ "x37": {
+ "command": ":goto previous minute"
+ },
+ "x38": {
+ "command": ":goto next minute"
+ },
+ "x21": {
+ "command": ":goto last 10 minutes after the hour"
+ },
+ "x22": {
+ "command": ":goto last 20 minutes after the hour"
+ },
+ "xc2xa7": {
+ "command": ":goto last 30 minutes after the hour"
+ },
+ "x24": {
+ "command": ":goto last 40 minutes after the hour"
+ },
+ "x25": {
+ "command": ":goto last 50 minutes after the hour"
+ },
+ "x26": {
+ "command": ":goto last hour"
+ }
+ }
+ }
+ }
+}
diff --git a/src/keymaps/default-keymap.json b/src/keymaps/default-keymap.json
new file mode 100644
index 0000000..4848f3e
--- /dev/null
+++ b/src/keymaps/default-keymap.json
@@ -0,0 +1,172 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "global": {
+ "keymap_def_alt_warning": "Press ${ansi_bold}w${ansi_norm}/${ansi_bold}W${ansi_norm} to move forward/backward through ${ansi_yellow}warning${ansi_norm} messages",
+ "keymap_def_alt_hour_boundary": "Press ${ansi_bold}6${ansi_bold}/${ansi_bold}^${ansi_norm} to move to the next/previous hour boundary",
+ "keymap_def_scroll_horiz": "Press \\'${ansi_bold}>${ansi_norm}\\' or \\'${ansi_bold}<${ansi_norm}\\' to scroll horizontally to a search result",
+ "keymap_def_next_user_mark": "Press ${ansi_bold}u${ansi_norm}/${ansi_bold}U${ansi_norm} to move forward/backward through user bookmarks",
+ "keymap_def_db_view": "Press ${ansi_bold}v${ansi_norm}/${ansi_bold}V${ansi_norm} to switch to the SQL result view",
+ "keymap_def_hist_view": "Press ${ansi_bold}i${ansi_norm}/${ansi_bold}I${ansi_norm} to switch to the histogram view",
+ "keymap_def_text_view": "Press ${ansi_bold}t${ansi_norm} to switch to the text view",
+ "keymap_def_pop_view": "Press ${ansi_bold}q${ansi_norm} to return to the previous view",
+ "keymap_def_zoom": "Press ${ansi_bold}z${ansi_norm}/${ansi_bold}Z${ansi_norm} to zoom in/out",
+ "keymap_def_clear": "Press ${ansi_bold}C${ansi_norm} to clear marked messages",
+ "keymap_def_prev_location": "Press ${ansi_bold}{${ansi_norm} to move to the previous location in history",
+ "keymap_def_next_location": "Press ${ansi_bold}}${ansi_norm} to move to the next location in history",
+ "keymap_def_next_mark": "Press ${ansi_bold}c${ansi_norm} to copy marked lines to the clipboard; press ${ansi_bold}C${ansi_norm} to clear marked lines"
+ },
+ "ui": {
+ "keymap-defs": {
+ "default": {
+ "x31": {
+ "command": ":goto next 10 minutes after the hour"
+ },
+ "x32": {
+ "command": ":goto next 20 minutes after the hour"
+ },
+ "x33": {
+ "command": ":goto next 30 minutes after the hour"
+ },
+ "x34": {
+ "command": ":goto next 40 minutes after the hour"
+ },
+ "x35": {
+ "command": ":goto next 50 minutes after the hour"
+ },
+ "x36": {
+ "command": ":goto next hour"
+ },
+ "x37": {
+ "command": ":goto previous minute"
+ },
+ "x38": {
+ "command": ":goto next minute"
+ },
+ "x21": {
+ "command": ":goto last 10 minutes after the hour"
+ },
+ "x40": {
+ "command": ":goto last 20 minutes after the hour"
+ },
+ "x23": {
+ "command": ":goto last 30 minutes after the hour"
+ },
+ "x24": {
+ "command": ":goto last 40 minutes after the hour"
+ },
+ "x25": {
+ "command": ":goto last 50 minutes after the hour"
+ },
+ "x5e": {
+ "command": ":goto last hour"
+ },
+ "x06": {
+ "command": ";UPDATE lnav_view_filters SET enabled = 1 - enabled WHERE view_name = (SELECT name FROM lnav_view_stack WHERE name in ('log', 'text') ORDER BY rowid DESC LIMIT 1)"
+ },
+ "x0c": {
+ "command": ":write-screen-to -"
+ },
+ "x12": {
+ "command": ":reset-session"
+ },
+ "x18": {
+ "command": ";UPDATE lnav_views SET movement = (CASE movement WHEN 'top' THEN 'cursor' ELSE 'top' END) WHERE name = (SELECT name FROM lnav_top_view)"
+ },
+ "x04": {
+ "command": ";UPDATE lnav_views SET top = top + (height / 2), selection = (CASE movement WHEN 'top' THEN selection ELSE top + (height / 2) + (selection - top) END) WHERE name = (SELECT name FROM lnav_top_view)"
+ },
+ "x15": {
+ "command": ";UPDATE lnav_views SET top = top - (height / 2), selection = (CASE movement WHEN 'top' THEN selection ELSE top - (height / 2) + (selection - top) END) WHERE name = (SELECT name FROM lnav_top_view)"
+ },
+ "x3d": {
+ "command": ";UPDATE lnav_views SET paused = 1 - paused"
+ },
+ "x58": {
+ "command": ":close"
+ },
+ "x3a": {
+ "command": ":prompt command"
+ },
+ "x7c": {
+ "command": ":prompt script"
+ },
+ "x2f": {
+ "command": ":prompt search"
+ },
+ "x3b": {
+ "command": ":prompt sql"
+ },
+ "x45": {
+ "command": ":prev-mark error",
+ "alt-msg": "${keymap_def_alt_warning}"
+ },
+ "x65": {
+ "command": ":next-mark error",
+ "alt-msg": "${keymap_def_alt_warning}"
+ },
+ "x57": {
+ "command": ":prev-mark warning",
+ "alt-msg": "${keymap_def_alt_hour_boundary}"
+ },
+ "x77": {
+ "command": ":next-mark warning",
+ "alt-msg": "${keymap_def_alt_hour_boundary}"
+ },
+ "x63": {
+ "command": ":write-to /dev/clipboard",
+ "alt-msg": "${keymap_def_clear}"
+ },
+ "x67": {
+ "command": ":goto 0"
+ },
+ "x6d": {
+ "command": ":mark",
+ "alt-msg": "${keymap_def_next_user_mark}"
+ },
+ "x4e": {
+ "command": ":prev-mark search",
+ "alt-msg": "${keymap_def_scroll_horiz}"
+ },
+ "x6e": {
+ "command": ":next-mark search",
+ "alt-msg": "${keymap_def_scroll_horiz}"
+ },
+ "x75": {
+ "command": ":next-mark",
+ "alt-msg": "${keymap_def_next_mark}"
+ },
+ "x55": {
+ "command": ":prev-mark"
+ },
+ "x7d": {
+ "command": ":next-location",
+ "alt-msg": "${keymap_def_prev_location}"
+ },
+ "x7b": {
+ "command": ":prev-location",
+ "alt-msg": "${keymap_def_next_location}"
+ },
+ "x3f": {
+ "command": ":toggle-view help"
+ },
+ "x69": {
+ "command": ":toggle-view histogram",
+ "alt-msg": "${keymap_def_zoom}"
+ },
+ "x50": {
+ "command": ":toggle-view pretty",
+ "alt-msg": "${keymap_def_pop_view}"
+ },
+ "x76": {
+ "command": ":toggle-view db"
+ },
+ "x71": {
+ "command": "|lnav-pop-view ${keyseq}"
+ },
+ "x51": {
+ "command": "|lnav-pop-view ${keyseq}"
+ }
+ }
+ }
+ }
+}
diff --git a/src/keymaps/fr-keymap.json b/src/keymaps/fr-keymap.json
new file mode 100644
index 0000000..48a7ac6
--- /dev/null
+++ b/src/keymaps/fr-keymap.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "keymap-defs": {
+ "fr": {
+ "x26": {
+ "command": ":goto next 10 minutes after the hour"
+ },
+ "xc3xa9": {
+ "command": ":goto next 20 minutes after the hour"
+ },
+ "x22": {
+ "command": ":goto next 30 minutes after the hour"
+ },
+ "x27": {
+ "command": ":goto next 40 minutes after the hour"
+ },
+ "x28": {
+ "command": ":goto next 50 minutes after the hour"
+ },
+ "x2d": {
+ "command": ":goto next hour"
+ },
+ "xc3xa8": {
+ "command": ":goto next minute"
+ },
+ "x31": {
+ "command": ":goto last 10 minutes after the hour"
+ },
+ "x32": {
+ "command": ":goto last 20 minutes after the hour"
+ },
+ "x33": {
+ "command": ":goto last 30 minutes after the hour"
+ },
+ "x34": {
+ "command": ":goto last 40 minutes after the hour"
+ },
+ "x35": {
+ "command": ":goto last 50 minutes after the hour"
+ },
+ "x36": {
+ "command": ":goto last hour"
+ },
+ "x37": {
+ "command": ":goto previous minute"
+ }
+ }
+ }
+ }
+}
diff --git a/src/keymaps/keymaps.am b/src/keymaps/keymaps.am
new file mode 100644
index 0000000..aad0c6e
--- /dev/null
+++ b/src/keymaps/keymaps.am
@@ -0,0 +1,9 @@
+
+KEYMAP_FILES = \
+ $(srcdir)/%reldir%/de-keymap.json \
+ $(srcdir)/%reldir%/default-keymap.json \
+ $(srcdir)/%reldir%/fr-keymap.json \
+ $(srcdir)/%reldir%/sv-keymap.json \
+ $(srcdir)/%reldir%/uk-keymap.json \
+ $(srcdir)/%reldir%/us-keymap.json \
+ $()
diff --git a/src/keymaps/sv-keymap.json b/src/keymaps/sv-keymap.json
new file mode 100644
index 0000000..7474b1f
--- /dev/null
+++ b/src/keymaps/sv-keymap.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "keymap-defs": {
+ "sv": {
+ "x22": {
+ "command": ":goto last 20 minutes after the hour"
+ },
+ "xc2xa4": {
+ "command": ":goto last 40 minutes after the hour"
+ },
+ "xe2x82xac": {
+ "command": ":goto last 40 minutes after the hour"
+ },
+ "x26": {
+ "command": ":goto last hour"
+ },
+ "x3d": {
+ "command": ":goto last day"
+ },
+ "x2b": {
+ "command": ";UPDATE lnav_views SET paused = 1 - paused"
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/keymaps/uk-keymap.json b/src/keymaps/uk-keymap.json
new file mode 100644
index 0000000..1cb130e
--- /dev/null
+++ b/src/keymaps/uk-keymap.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "keymap-defs": {
+ "uk": {
+ "x22": {
+ "command": ":goto last 20 minutes after the hour"
+ },
+ "xc2xa3": {
+ "command": ":goto last 30 minutes after the hour"
+ }
+ }
+ }
+ }
+}
diff --git a/src/keymaps/us-keymap.json b/src/keymaps/us-keymap.json
new file mode 100644
index 0000000..a955958
--- /dev/null
+++ b/src/keymaps/us-keymap.json
@@ -0,0 +1,51 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "keymap-defs": {
+ "us": {
+ "x31": {
+ "command": ":goto next 10 minutes after the hour"
+ },
+ "x32": {
+ "command": ":goto next 20 minutes after the hour"
+ },
+ "x33": {
+ "command": ":goto next 30 minutes after the hour"
+ },
+ "x34": {
+ "command": ":goto next 40 minutes after the hour"
+ },
+ "x35": {
+ "command": ":goto next 50 minutes after the hour"
+ },
+ "x36": {
+ "command": ":goto next hour"
+ },
+ "x37": {
+ "command": ":goto previous minute"
+ },
+ "x38": {
+ "command": ":goto next minute"
+ },
+ "x21": {
+ "command": ":goto last 10 minutes after the hour"
+ },
+ "x40": {
+ "command": ":goto last 20 minutes after the hour"
+ },
+ "x23": {
+ "command": ":goto last 30 minutes after the hour"
+ },
+ "x24": {
+ "command": ":goto last 40 minutes after the hour"
+ },
+ "x25": {
+ "command": ":goto last 50 minutes after the hour"
+ },
+ "x5e": {
+ "command": ":goto last hour"
+ }
+ }
+ }
+ }
+}
diff --git a/src/line_buffer.cc b/src/line_buffer.cc
new file mode 100644
index 0000000..f370c02
--- /dev/null
+++ b/src/line_buffer.cc
@@ -0,0 +1,1413 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file line_buffer.cc
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#ifdef HAVE_BZLIB_H
+# include <bzlib.h>
+#endif
+
+#include <algorithm>
+#include <set>
+
+#ifdef HAVE_X86INTRIN_H
+# include "simdutf8check.h"
+#endif
+
+#include "base/auto_pid.hh"
+#include "base/fs_util.hh"
+#include "base/injector.bind.hh"
+#include "base/injector.hh"
+#include "base/is_utf8.hh"
+#include "base/isc.hh"
+#include "base/math_util.hh"
+#include "base/paths.hh"
+#include "fmtlib/fmt/format.h"
+#include "line_buffer.hh"
+#include "lnav_util.hh"
+
+using namespace std::chrono_literals;
+
+static const ssize_t INITIAL_REQUEST_SIZE = 16 * 1024;
+static const ssize_t DEFAULT_INCREMENT = 128 * 1024;
+static const ssize_t INITIAL_COMPRESSED_BUFFER_SIZE = 5 * 1024 * 1024;
+static const ssize_t MAX_COMPRESSED_BUFFER_SIZE = 32 * 1024 * 1024;
+
+const ssize_t line_buffer::DEFAULT_LINE_BUFFER_SIZE = 256 * 1024;
+const ssize_t line_buffer::MAX_LINE_BUFFER_SIZE
+ = 4 * 4 * line_buffer::DEFAULT_LINE_BUFFER_SIZE;
+
+class io_looper : public isc::service<io_looper> {};
+
+struct io_looper_tag {};
+
+static auto bound_io = injector::bind_multiple<isc::service_base>()
+ .add_singleton<io_looper, io_looper_tag>();
+
+namespace injector {
+template<>
+void
+force_linking(io_looper_tag anno)
+{
+}
+} // namespace injector
+
+/*
+ * XXX REMOVE ME
+ *
+ * The stock bzipped file code does not use pread, so we need to use a lock to
+ * get exclusive access to the file. In the future, we should just rewrite
+ * the bzipped file code to use pread.
+ */
+class lock_hack {
+public:
+ class guard {
+ public:
+ guard() : g_lock(lock_hack::singleton()) { this->g_lock.lock(); }
+
+ ~guard() { this->g_lock.unlock(); }
+
+ private:
+ lock_hack& g_lock;
+ };
+
+ static lock_hack& singleton()
+ {
+ static lock_hack retval;
+
+ return retval;
+ }
+
+ void lock() { lockf(this->lh_fd, F_LOCK, 0); }
+
+ void unlock() { lockf(this->lh_fd, F_ULOCK, 0); }
+
+private:
+ lock_hack()
+ {
+ char lockname[64];
+
+ snprintf(lockname, sizeof(lockname), "/tmp/lnav.%d.lck", getpid());
+ this->lh_fd = open(lockname, O_CREAT | O_RDWR, 0600);
+ log_perror(fcntl(this->lh_fd, F_SETFD, FD_CLOEXEC));
+ unlink(lockname);
+ }
+
+ auto_fd lh_fd;
+};
+/* XXX END */
+
+#define Z_BUFSIZE 65536U
+#define SYNCPOINT_SIZE (1024 * 1024)
+line_buffer::gz_indexed::gz_indexed()
+{
+ if ((this->inbuf = (Bytef*) malloc(Z_BUFSIZE)) == NULL) {
+ throw std::bad_alloc();
+ }
+}
+
+void
+line_buffer::gz_indexed::close()
+{
+ // Release old stream, if we were open
+ if (*this) {
+ inflateEnd(&this->strm);
+ ::close(this->gz_fd);
+ this->syncpoints.clear();
+ this->gz_fd = -1;
+ }
+}
+
+void
+line_buffer::gz_indexed::init_stream()
+{
+ if (*this) {
+ inflateEnd(&this->strm);
+ }
+
+ // initialize inflate struct
+ this->strm.zalloc = Z_NULL;
+ this->strm.zfree = Z_NULL;
+ this->strm.opaque = Z_NULL;
+ this->strm.avail_in = 0;
+ this->strm.next_in = Z_NULL;
+ this->strm.avail_out = 0;
+ int rc = inflateInit2(&strm, GZ_HEADER_MODE);
+ if (rc != Z_OK) {
+ throw(rc); // FIXME: exception wrapper
+ }
+}
+
+void
+line_buffer::gz_indexed::continue_stream()
+{
+ // Save our position and output buffer
+ auto total_in = this->strm.total_in;
+ auto total_out = this->strm.total_out;
+ auto avail_out = this->strm.avail_out;
+ auto next_out = this->strm.next_out;
+
+ init_stream();
+
+ // Restore position and output buffer
+ this->strm.total_in = total_in;
+ this->strm.total_out = total_out;
+ this->strm.avail_out = avail_out;
+ this->strm.next_out = next_out;
+}
+
+void
+line_buffer::gz_indexed::open(int fd, header_data& hd)
+{
+ this->close();
+ this->init_stream();
+ this->gz_fd = fd;
+
+ unsigned char name[1024];
+ unsigned char comment[4096];
+
+ name[0] = '\0';
+ comment[0] = '\0';
+
+ gz_header gz_hd;
+ memset(&gz_hd, 0, sizeof(gz_hd));
+ gz_hd.name = name;
+ gz_hd.name_max = sizeof(name);
+ gz_hd.comment = comment;
+ gz_hd.comm_max = sizeof(comment);
+
+ Bytef inbuf[8192];
+ Bytef outbuf[8192];
+ this->strm.next_out = outbuf;
+ this->strm.total_out = 0;
+ this->strm.avail_out = sizeof(outbuf);
+ this->strm.next_in = inbuf;
+ this->strm.total_in = 0;
+
+ if (inflateGetHeader(&this->strm, &gz_hd) == Z_OK) {
+ auto rc = pread(fd, inbuf, sizeof(inbuf), 0);
+ if (rc >= 0) {
+ this->strm.avail_in = rc;
+
+ inflate(&this->strm, Z_BLOCK);
+ inflateEnd(&this->strm);
+
+ this->strm.next_out = Z_NULL;
+ this->strm.next_in = Z_NULL;
+ this->strm.next_in = Z_NULL;
+ this->strm.total_in = 0;
+ this->strm.avail_in = 0;
+ this->init_stream();
+
+ switch (gz_hd.done) {
+ case 0:
+ log_debug("%d: no gzip header data", fd);
+ break;
+ case 1:
+ hd.hd_mtime.tv_sec = gz_hd.time;
+ hd.hd_name = std::string((char*) name);
+ hd.hd_comment = std::string((char*) comment);
+ break;
+ default:
+ log_error("%d: failed to read gzip header data", fd);
+ break;
+ }
+ } else {
+ log_error("%d: failed to read gzip header from file: %s",
+ fd,
+ strerror(errno));
+ }
+ } else {
+ log_error("%d: unable to get gzip header", fd);
+ }
+}
+
+int
+line_buffer::gz_indexed::stream_data(void* buf, size_t size)
+{
+ this->strm.avail_out = size;
+ this->strm.next_out = (unsigned char*) buf;
+
+ size_t last = this->syncpoints.empty() ? 0 : this->syncpoints.back().in;
+ while (this->strm.avail_out) {
+ if (!this->strm.avail_in) {
+ int rc = ::pread(
+ this->gz_fd, &this->inbuf[0], Z_BUFSIZE, this->strm.total_in);
+ if (rc < 0) {
+ return rc;
+ }
+ this->strm.next_in = this->inbuf;
+ this->strm.avail_in = rc;
+ }
+ if (this->strm.avail_in) {
+ int flush = last > this->strm.total_in ? Z_SYNC_FLUSH : Z_BLOCK;
+ auto err = inflate(&this->strm, flush);
+ if (err == Z_STREAM_END) {
+ // Reached end of stream; re-init for a possible subsequent
+ // stream
+ continue_stream();
+ } else if (err != Z_OK) {
+ log_error(" inflate-error: %d %s",
+ (int) err,
+ this->strm.msg ? this->strm.msg : "");
+ break;
+ }
+
+ if (this->strm.total_in >= last + SYNCPOINT_SIZE
+ && size > this->strm.avail_out + GZ_WINSIZE
+ && (this->strm.data_type & GZ_END_OF_BLOCK_MASK)
+ && !(this->strm.data_type & GZ_END_OF_FILE_MASK))
+ {
+ this->syncpoints.emplace_back(this->strm, size);
+ last = this->strm.total_out;
+ }
+ } else if (this->strm.avail_out) {
+ // Processed all the gz file data but didn't fill
+ // the output buffer. We're done, even though we
+ // produced fewer bytes than requested.
+ break;
+ }
+ }
+ return size - this->strm.avail_out;
+}
+
+void
+line_buffer::gz_indexed::seek(off_t offset)
+{
+ if ((size_t) offset == this->strm.total_out) {
+ return;
+ }
+
+ indexDict* dict = nullptr;
+ // Find highest syncpoint not past offset
+ // FIXME: Make this a binary-tree search
+ for (auto& d : this->syncpoints) {
+ if (d.out <= offset) {
+ dict = &d;
+ } else {
+ break;
+ }
+ }
+
+ // Choose highest available syncpoint, or keep current offset if it's ok
+ if ((size_t) offset < this->strm.total_out
+ || (dict && this->strm.total_out < (size_t) dict->out))
+ {
+ // Release the old z_stream
+ inflateEnd(&this->strm);
+ if (dict) {
+ dict->apply(&this->strm);
+ } else {
+ init_stream();
+ }
+ }
+
+ // Stream from compressed file until we reach our offset
+ unsigned char dummy[Z_BUFSIZE];
+ while ((size_t) offset > this->strm.total_out) {
+ size_t to_copy
+ = std::min(static_cast<size_t>(Z_BUFSIZE),
+ static_cast<size_t>(offset - this->strm.total_out));
+ auto bytes = stream_data(dummy, to_copy);
+ if (bytes <= 0) {
+ break;
+ }
+ }
+}
+
+int
+line_buffer::gz_indexed::read(void* buf, size_t offset, size_t size)
+{
+ if (offset != this->strm.total_out) {
+ // log_debug("doing seek! %d %d", offset, this->strm.total_out);
+ this->seek(offset);
+ }
+
+ int bytes = stream_data(buf, size);
+
+ return bytes;
+}
+
+line_buffer::line_buffer()
+{
+ ensure(this->invariant());
+}
+
+line_buffer::~line_buffer()
+{
+ auto empty_fd = auto_fd();
+
+ // Make sure any shared refs take ownership of the data.
+ this->lb_share_manager.invalidate_refs();
+ this->set_fd(empty_fd);
+}
+
+void
+line_buffer::set_fd(auto_fd& fd)
+{
+ file_off_t newoff = 0;
+
+ {
+ safe::WriteAccess<safe_gz_indexed> gi(this->lb_gz_file);
+
+ if (*gi) {
+ gi->close();
+ }
+ }
+
+ if (this->lb_bz_file) {
+ this->lb_bz_file = false;
+ }
+
+ if (fd != -1) {
+ /* Sync the fd's offset with the object. */
+ newoff = lseek(fd, 0, SEEK_CUR);
+ if (newoff == -1) {
+ if (errno != ESPIPE) {
+ throw error(errno);
+ }
+
+ /* It's a pipe, start with a zero offset. */
+ newoff = 0;
+ this->lb_seekable = false;
+ } else {
+ char gz_id[2 + 1 + 1 + 4];
+
+ if (pread(fd, gz_id, sizeof(gz_id), 0) == sizeof(gz_id)) {
+ if (gz_id[0] == '\037' && gz_id[1] == '\213') {
+ int gzfd = dup(fd);
+
+ log_perror(fcntl(gzfd, F_SETFD, FD_CLOEXEC));
+ if (lseek(fd, 0, SEEK_SET) < 0) {
+ close(gzfd);
+ throw error(errno);
+ }
+ this->lb_gz_file.writeAccess()->open(gzfd, this->lb_header);
+ this->lb_compressed = true;
+ this->lb_file_time = this->lb_header.hd_mtime.tv_sec;
+ if (this->lb_file_time < 0) {
+ this->lb_file_time = 0;
+ }
+ this->lb_compressed_offset
+ = lseek(this->lb_fd, 0, SEEK_CUR);
+ this->resize_buffer(INITIAL_COMPRESSED_BUFFER_SIZE);
+ }
+#ifdef HAVE_BZLIB_H
+ else if (gz_id[0] == 'B' && gz_id[1] == 'Z')
+ {
+ if (lseek(fd, 0, SEEK_SET) < 0) {
+ throw error(errno);
+ }
+ this->lb_bz_file = true;
+ this->lb_compressed = true;
+
+ /*
+ * Loading data from a bzip2 file is pretty slow, so we try
+ * to keep as much in memory as possible.
+ */
+ this->resize_buffer(INITIAL_COMPRESSED_BUFFER_SIZE);
+
+ this->lb_compressed_offset = 0;
+ }
+#endif
+ }
+ this->lb_seekable = true;
+ }
+ }
+ this->lb_file_offset = newoff;
+ this->lb_buffer.clear();
+ this->lb_fd = std::move(fd);
+
+ ensure(this->invariant());
+}
+
+void
+line_buffer::resize_buffer(size_t new_max)
+{
+ if (new_max <= MAX_LINE_BUFFER_SIZE
+ && new_max > (size_t) this->lb_buffer.capacity())
+ {
+ /* Still need more space, try a realloc. */
+ this->lb_share_manager.invalidate_refs();
+ this->lb_buffer.expand_to(new_max);
+ }
+}
+
+void
+line_buffer::ensure_available(file_off_t start, ssize_t max_length)
+{
+ ssize_t prefill, available;
+
+ require(this->lb_compressed || max_length <= MAX_LINE_BUFFER_SIZE);
+
+ // log_debug("ensure avail %d %d", start, max_length);
+
+ if (this->lb_file_size != -1) {
+ if (start + (file_off_t) max_length > this->lb_file_size) {
+ max_length = (this->lb_file_size - start);
+ }
+ }
+
+ /*
+ * Check to see if the start is inside the cached range or immediately
+ * after.
+ */
+ if (start < this->lb_file_offset
+ || start > (file_off_t) (this->lb_file_offset + this->lb_buffer.size()))
+ {
+ /*
+ * The request is outside the cached range, need to reload the
+ * whole thing.
+ */
+ this->lb_share_manager.invalidate_refs();
+ prefill = 0;
+ this->lb_buffer.clear();
+ if ((this->lb_file_size != (ssize_t) -1)
+ && (start + this->lb_buffer.capacity() > this->lb_file_size))
+ {
+ require(start <= this->lb_file_size);
+ /*
+ * If the start is near the end of the file, move the offset back a
+ * bit so we can get more of the file in the cache.
+ */
+ this->lb_file_offset = this->lb_file_size
+ - std::min(this->lb_file_size,
+ (file_ssize_t) this->lb_buffer.capacity());
+ } else {
+ this->lb_file_offset = start;
+ }
+ } else {
+ /* The request is in the cached range. Record how much extra data is in
+ * the buffer before the requested range.
+ */
+ prefill = start - this->lb_file_offset;
+ }
+ require(this->lb_file_offset <= start);
+ require(prefill <= this->lb_buffer.size());
+
+ available = this->lb_buffer.capacity() - (start - this->lb_file_offset);
+ require(available <= this->lb_buffer.capacity());
+
+ if (max_length > available) {
+ // log_debug("need more space!");
+ /*
+ * Need more space, move any existing data to the front of the
+ * buffer.
+ */
+ this->lb_share_manager.invalidate_refs();
+
+ this->lb_buffer.resize_by(-prefill);
+ this->lb_file_offset += prefill;
+ // log_debug("adjust file offset for prefill %d", this->lb_file_offset);
+ memmove(this->lb_buffer.at(0),
+ this->lb_buffer.at(prefill),
+ this->lb_buffer.size());
+
+ available = this->lb_buffer.capacity() - (start - this->lb_file_offset);
+ if (max_length > available) {
+ this->resize_buffer(roundup_size(max_length, DEFAULT_INCREMENT));
+ }
+ }
+ this->lb_line_starts.clear();
+ this->lb_line_is_utf.clear();
+}
+
+bool
+line_buffer::load_next_buffer()
+{
+ // log_debug("loader here!");
+ auto retval = false;
+ auto start = this->lb_loader_file_offset.value();
+ ssize_t rc = 0;
+ safe::WriteAccess<safe_gz_indexed> gi(this->lb_gz_file);
+
+ // log_debug("BEGIN preload read");
+ /* ... read in the new data. */
+ if (!this->lb_cached_fd && *gi) {
+ if (this->lb_file_size != (ssize_t) -1 && this->in_range(start)
+ && this->in_range(this->lb_file_size - 1))
+ {
+ rc = 0;
+ } else {
+ // log_debug("async decomp start");
+ rc = gi->read(this->lb_alt_buffer.value().end(),
+ start + this->lb_alt_buffer.value().size(),
+ this->lb_alt_buffer.value().available());
+ this->lb_compressed_offset = gi->get_source_offset();
+ if (rc != -1 && (rc < this->lb_alt_buffer.value().available())
+ && (start + this->lb_alt_buffer.value().size() + rc
+ > this->lb_file_size))
+ {
+ this->lb_file_size
+ = (start + this->lb_alt_buffer.value().size() + rc);
+ }
+#if 0
+ log_debug("async decomp end %d+%d:%d",
+ this->lb_alt_buffer->size(),
+ rc,
+ this->lb_alt_buffer->capacity());
+#endif
+ }
+ }
+#ifdef HAVE_BZLIB_H
+ else if (!this->lb_cached_fd && this->lb_bz_file)
+ {
+ if (this->lb_file_size != (ssize_t) -1
+ && (((ssize_t) start >= this->lb_file_size)
+ || (this->in_range(start)
+ && this->in_range(this->lb_file_size - 1))))
+ {
+ rc = 0;
+ } else {
+ lock_hack::guard guard;
+ char scratch[32 * 1024];
+ BZFILE* bz_file;
+ file_off_t seek_to;
+ int bzfd;
+
+ /*
+ * Unfortunately, there is no bzseek, so we need to reopen the
+ * file every time we want to do a read.
+ */
+ bzfd = dup(this->lb_fd);
+ if (lseek(this->lb_fd, 0, SEEK_SET) < 0) {
+ close(bzfd);
+ throw error(errno);
+ }
+ if ((bz_file = BZ2_bzdopen(bzfd, "r")) == nullptr) {
+ close(bzfd);
+ if (errno == 0) {
+ throw std::bad_alloc();
+ } else {
+ throw error(errno);
+ }
+ }
+
+ seek_to = start + this->lb_alt_buffer.value().size();
+ while (seek_to > 0) {
+ int count;
+
+ count = BZ2_bzread(bz_file,
+ scratch,
+ std::min((size_t) seek_to, sizeof(scratch)));
+ seek_to -= count;
+ }
+ rc = BZ2_bzread(bz_file,
+ this->lb_alt_buffer->end(),
+ this->lb_alt_buffer->available());
+ this->lb_compressed_offset = lseek(bzfd, 0, SEEK_SET);
+ BZ2_bzclose(bz_file);
+
+ if (rc != -1 && (rc < (this->lb_alt_buffer.value().available()))
+ && (start + this->lb_alt_buffer.value().size() + rc
+ > this->lb_file_size))
+ {
+ this->lb_file_size
+ = (start + this->lb_alt_buffer.value().size() + rc);
+ }
+ }
+ }
+#endif
+ else
+ {
+ rc = pread(this->lb_cached_fd ? this->lb_cached_fd.value().get()
+ : this->lb_fd.get(),
+ this->lb_alt_buffer.value().end(),
+ this->lb_alt_buffer.value().available(),
+ start + this->lb_alt_buffer.value().size());
+ }
+ // XXX For some reason, cygwin is giving us a bogus return value when
+ // up to the end of the file.
+ if (rc > (this->lb_alt_buffer.value().available())) {
+ rc = -1;
+#ifdef ENODATA
+ errno = ENODATA;
+#else
+ errno = EAGAIN;
+#endif
+ }
+ switch (rc) {
+ case 0:
+ if (start < (file_off_t) this->lb_file_size) {
+ retval = true;
+ }
+ break;
+
+ case (ssize_t) -1:
+ switch (errno) {
+#ifdef ENODATA
+ /* Cygwin seems to return this when pread reaches the end of
+ * the file. */
+ case ENODATA:
+#endif
+ case EINTR:
+ case EAGAIN:
+ break;
+
+ default:
+ throw error(errno);
+ }
+ break;
+
+ default:
+ this->lb_alt_buffer.value().resize_by(rc);
+ retval = true;
+ break;
+ }
+ // log_debug("END preload read");
+
+ if (start > this->lb_last_line_offset) {
+ const auto* line_start = this->lb_alt_buffer.value().begin();
+
+ do {
+ auto before = line_start - this->lb_alt_buffer->begin();
+ auto remaining = this->lb_alt_buffer.value().size() - before;
+ auto frag = string_fragment::from_bytes(line_start, remaining);
+ auto utf_scan_res = is_utf8(frag, '\n');
+ auto lf = utf_scan_res.remaining_ptr(frag);
+ this->lb_alt_line_starts.emplace_back(before);
+ this->lb_alt_line_is_utf.emplace_back(utf_scan_res.is_valid());
+ this->lb_alt_line_has_ansi.emplace_back(utf_scan_res.usr_has_ansi);
+
+ line_start = lf;
+ } while (line_start != nullptr
+ && line_start < this->lb_alt_buffer->end());
+ }
+
+ return retval;
+}
+
+bool
+line_buffer::fill_range(file_off_t start, ssize_t max_length)
+{
+ bool retval = false;
+
+ require(start >= 0);
+
+ // log_debug("fill range %d %d", start, max_length);
+#if 0
+ log_debug("(%p) fill range %d %d (%d) %d",
+ this,
+ start,
+ max_length,
+ this->lb_file_offset,
+ this->lb_buffer.size());
+#endif
+ if (!lnav::pid::in_child && this->lb_loader_future.valid()
+ && start >= this->lb_loader_file_offset.value())
+ {
+#if 0
+ log_debug("getting preload! %d %d",
+ start,
+ this->lb_loader_file_offset.value());
+#endif
+ nonstd::optional<std::chrono::system_clock::time_point> wait_start;
+
+ if (this->lb_loader_future.wait_for(std::chrono::seconds(0))
+ != std::future_status::ready)
+ {
+ wait_start
+ = nonstd::make_optional(std::chrono::system_clock::now());
+ }
+ retval = this->lb_loader_future.get();
+ if (false && wait_start) {
+ auto diff = std::chrono::system_clock::now() - wait_start.value();
+ log_debug("wait done! %d", diff.count());
+ }
+ // log_debug("got preload");
+ this->lb_loader_future = {};
+ this->lb_share_manager.invalidate_refs();
+ this->lb_file_offset = this->lb_loader_file_offset.value();
+ this->lb_loader_file_offset = nonstd::nullopt;
+ this->lb_buffer.swap(this->lb_alt_buffer.value());
+ this->lb_alt_buffer.value().clear();
+ this->lb_line_starts = std::move(this->lb_alt_line_starts);
+ this->lb_alt_line_starts.clear();
+ this->lb_line_is_utf = std::move(this->lb_alt_line_is_utf);
+ this->lb_alt_line_is_utf.clear();
+ this->lb_line_has_ansi = std::move(this->lb_alt_line_has_ansi);
+ this->lb_alt_line_has_ansi.clear();
+ this->lb_stats.s_used_preloads += 1;
+ }
+ if (this->in_range(start) && this->in_range(start + max_length - 1)) {
+ /* Cache already has the data, nothing to do. */
+ retval = true;
+ if (!lnav::pid::in_child && this->lb_seekable && this->lb_buffer.full()
+ && !this->lb_loader_file_offset)
+ {
+ // log_debug("loader available start=%d", start);
+ auto last_lf_iter = std::find(
+ this->lb_buffer.rbegin(), this->lb_buffer.rend(), '\n');
+ if (last_lf_iter != this->lb_buffer.rend()) {
+ auto usable_size
+ = std::distance(last_lf_iter, this->lb_buffer.rend());
+ // log_debug("found linefeed %d", usable_size);
+ if (!this->lb_alt_buffer) {
+ // log_debug("allocating new buffer!");
+ this->lb_alt_buffer
+ = auto_buffer::alloc(this->lb_buffer.capacity());
+ }
+ this->lb_alt_buffer->resize(this->lb_buffer.size()
+ - usable_size);
+ memcpy(this->lb_alt_buffer.value().begin(),
+ this->lb_buffer.at(usable_size),
+ this->lb_alt_buffer->size());
+ this->lb_loader_file_offset
+ = this->lb_file_offset + usable_size;
+#if 0
+ log_debug("load offset %d",
+ this->lb_loader_file_offset.value());
+ log_debug("launch loader");
+#endif
+ auto prom = std::make_shared<std::promise<bool>>();
+ this->lb_loader_future = prom->get_future();
+ this->lb_stats.s_requested_preloads += 1;
+ isc::to<io_looper&, io_looper_tag>().send(
+ [this, prom](auto& ioloop) mutable {
+ prom->set_value(this->load_next_buffer());
+ });
+ }
+ }
+ } else if (this->lb_fd != -1) {
+ ssize_t rc;
+
+ /* Make sure there is enough space, then */
+ this->ensure_available(start, max_length);
+
+ safe::WriteAccess<safe_gz_indexed> gi(this->lb_gz_file);
+
+ /* ... read in the new data. */
+ if (!this->lb_cached_fd && *gi) {
+ // log_debug("old decomp start");
+ if (this->lb_file_size != (ssize_t) -1 && this->in_range(start)
+ && this->in_range(this->lb_file_size - 1))
+ {
+ rc = 0;
+ } else {
+ this->lb_stats.s_decompressions += 1;
+ if (false && this->lb_last_line_offset > 0) {
+ this->lb_stats.s_hist[(this->lb_file_offset * 10)
+ / this->lb_last_line_offset]
+ += 1;
+ }
+ rc = gi->read(this->lb_buffer.end(),
+ this->lb_file_offset + this->lb_buffer.size(),
+ this->lb_buffer.available());
+ this->lb_compressed_offset = gi->get_source_offset();
+ if (rc != -1 && (rc < this->lb_buffer.available())) {
+ this->lb_file_size
+ = (this->lb_file_offset + this->lb_buffer.size() + rc);
+ }
+ }
+#if 0
+ log_debug("old decomp end -- %d+%d:%d",
+ this->lb_buffer.size(),
+ rc,
+ this->lb_buffer.capacity());
+#endif
+ }
+#ifdef HAVE_BZLIB_H
+ else if (!this->lb_cached_fd && this->lb_bz_file)
+ {
+ if (this->lb_file_size != (ssize_t) -1
+ && (((ssize_t) start >= this->lb_file_size)
+ || (this->in_range(start)
+ && this->in_range(this->lb_file_size - 1))))
+ {
+ rc = 0;
+ } else {
+ lock_hack::guard guard;
+ char scratch[32 * 1024];
+ BZFILE* bz_file;
+ file_off_t seek_to;
+ int bzfd;
+
+ /*
+ * Unfortunately, there is no bzseek, so we need to reopen the
+ * file every time we want to do a read.
+ */
+ bzfd = dup(this->lb_fd);
+ if (lseek(this->lb_fd, 0, SEEK_SET) < 0) {
+ close(bzfd);
+ throw error(errno);
+ }
+ if ((bz_file = BZ2_bzdopen(bzfd, "r")) == NULL) {
+ close(bzfd);
+ if (errno == 0) {
+ throw std::bad_alloc();
+ } else {
+ throw error(errno);
+ }
+ }
+
+ seek_to = this->lb_file_offset + this->lb_buffer.size();
+ while (seek_to > 0) {
+ int count;
+
+ count = BZ2_bzread(
+ bz_file,
+ scratch,
+ std::min((size_t) seek_to, sizeof(scratch)));
+ seek_to -= count;
+ }
+ rc = BZ2_bzread(bz_file,
+ this->lb_buffer.end(),
+ this->lb_buffer.available());
+ this->lb_compressed_offset = lseek(bzfd, 0, SEEK_SET);
+ BZ2_bzclose(bz_file);
+
+ if (rc != -1 && (rc < (this->lb_buffer.available()))) {
+ this->lb_file_size
+ = (this->lb_file_offset + this->lb_buffer.size() + rc);
+ }
+ }
+ }
+#endif
+ else if (this->lb_seekable)
+ {
+ this->lb_stats.s_preads += 1;
+ if (false && this->lb_last_line_offset > 0) {
+ this->lb_stats.s_hist[(this->lb_file_offset * 10)
+ / this->lb_last_line_offset]
+ += 1;
+ }
+#if 0
+ log_debug("%d: pread %lld",
+ this->lb_fd.get(),
+ this->lb_file_offset + this->lb_buffer.size());
+#endif
+ rc = pread(this->lb_cached_fd ? this->lb_cached_fd.value().get()
+ : this->lb_fd.get(),
+ this->lb_buffer.end(),
+ this->lb_buffer.available(),
+ this->lb_file_offset + this->lb_buffer.size());
+ // log_debug("pread rc %d", rc);
+ } else {
+ rc = read(this->lb_fd,
+ this->lb_buffer.end(),
+ this->lb_buffer.available());
+ }
+ // XXX For some reason, cygwin is giving us a bogus return value when
+ // up to the end of the file.
+ if (rc > (this->lb_buffer.available())) {
+ rc = -1;
+#ifdef ENODATA
+ errno = ENODATA;
+#else
+ errno = EAGAIN;
+#endif
+ }
+ switch (rc) {
+ case 0:
+ if (!this->lb_seekable) {
+ this->lb_file_size
+ = this->lb_file_offset + this->lb_buffer.size();
+ }
+ if (start < (file_off_t) this->lb_file_size) {
+ retval = true;
+ }
+
+ if (this->lb_compressed) {
+ /*
+ * For compressed files, increase the buffer size so we
+ * don't have to spend as much time uncompressing the data.
+ */
+ this->resize_buffer(MAX_COMPRESSED_BUFFER_SIZE);
+ }
+ break;
+
+ case (ssize_t) -1:
+ switch (errno) {
+#ifdef ENODATA
+ /* Cygwin seems to return this when pread reaches the end of
+ * the */
+ /* file. */
+ case ENODATA:
+#endif
+ case EINTR:
+ case EAGAIN:
+ break;
+
+ default:
+ throw error(errno);
+ }
+ break;
+
+ default:
+ this->lb_buffer.resize_by(rc);
+ retval = true;
+ break;
+ }
+
+ if (!lnav::pid::in_child && this->lb_seekable && this->lb_buffer.full()
+ && !this->lb_loader_file_offset)
+ {
+ // log_debug("loader available2 start=%d", start);
+ auto last_lf_iter = std::find(
+ this->lb_buffer.rbegin(), this->lb_buffer.rend(), '\n');
+ if (last_lf_iter != this->lb_buffer.rend()) {
+ auto usable_size
+ = std::distance(last_lf_iter, this->lb_buffer.rend());
+ // log_debug("found linefeed %d", usable_size);
+ if (!this->lb_alt_buffer) {
+ // log_debug("allocating new buffer!");
+ this->lb_alt_buffer
+ = auto_buffer::alloc(this->lb_buffer.capacity());
+ } else if (this->lb_alt_buffer->capacity()
+ < this->lb_buffer.capacity())
+ {
+ this->lb_alt_buffer->expand_to(this->lb_buffer.capacity());
+ }
+ this->lb_alt_buffer->resize(this->lb_buffer.size()
+ - usable_size);
+ memcpy(this->lb_alt_buffer->begin(),
+ this->lb_buffer.at(usable_size),
+ this->lb_alt_buffer->size());
+ this->lb_loader_file_offset
+ = this->lb_file_offset + usable_size;
+#if 0
+ log_debug("load offset %d",
+ this->lb_loader_file_offset.value());
+ log_debug("launch loader");
+#endif
+ auto prom = std::make_shared<std::promise<bool>>();
+ this->lb_loader_future = prom->get_future();
+ this->lb_stats.s_requested_preloads += 1;
+ isc::to<io_looper&, io_looper_tag>().send(
+ [this, prom](auto& ioloop) mutable {
+ prom->set_value(this->load_next_buffer());
+ });
+ }
+ }
+ ensure(this->lb_buffer.size() <= this->lb_buffer.capacity());
+ }
+
+ return retval;
+}
+
+Result<line_info, std::string>
+line_buffer::load_next_line(file_range prev_line)
+{
+ bool done = false;
+ line_info retval;
+
+ require(this->lb_fd != -1);
+
+ auto offset = prev_line.next_offset();
+ ssize_t request_size = INITIAL_REQUEST_SIZE;
+ retval.li_file_range.fr_offset = offset;
+ if (this->lb_buffer.empty() || !this->in_range(offset)) {
+ this->fill_range(offset, this->lb_buffer.capacity());
+ } else if (offset == this->lb_file_offset + this->lb_buffer.size()) {
+ if (!this->fill_range(offset, INITIAL_REQUEST_SIZE)) {
+ retval.li_file_range.fr_offset = offset;
+ retval.li_file_range.fr_size = 0;
+ if (this->is_pipe()) {
+ retval.li_partial = !this->is_pipe_closed();
+ } else {
+ retval.li_partial = true;
+ }
+ return Ok(retval);
+ }
+ }
+ while (!done) {
+ auto old_retval_size = retval.li_file_range.fr_size;
+ const char *line_start, *lf = nullptr;
+
+ /* Find the data in the cache and */
+ line_start = this->get_range(offset, retval.li_file_range.fr_size);
+ /* ... look for the end-of-line or end-of-file. */
+ ssize_t utf8_end = -1;
+
+ bool found_in_cache = false;
+ if (!this->lb_line_starts.empty()) {
+ auto buffer_offset = offset - this->lb_file_offset;
+
+ auto start_iter = std::lower_bound(this->lb_line_starts.begin(),
+ this->lb_line_starts.end(),
+ buffer_offset);
+ if (start_iter != this->lb_line_starts.end()) {
+ auto next_line_iter = start_iter + 1;
+
+ // log_debug("found offset %d %d", buffer_offset, *start_iter);
+ if (next_line_iter != this->lb_line_starts.end()) {
+ utf8_end = *next_line_iter - 1 - *start_iter;
+ found_in_cache = true;
+ lf = line_start + utf8_end;
+ } else {
+ // log_debug("no next iter");
+ }
+ } else {
+ // log_debug("no buffer_offset found");
+ }
+ }
+
+ if (!found_in_cache) {
+ auto frag = string_fragment::from_bytes(
+ line_start, retval.li_file_range.fr_size);
+ auto scan_res = is_utf8(frag, '\n');
+ lf = scan_res.remaining_ptr(frag);
+ if (lf != nullptr) {
+ lf -= 1;
+ }
+ retval.li_utf8_scan_result = scan_res;
+ }
+
+ auto got_new_data = old_retval_size != retval.li_file_range.fr_size;
+#if 0
+ log_debug("load next loop %p reqsize %d lsize %d",
+ lf,
+ request_size,
+ retval.li_file_range.fr_size);
+#endif
+ if (lf != nullptr
+ || (retval.li_file_range.fr_size >= MAX_LINE_BUFFER_SIZE)
+ || (request_size >= MAX_LINE_BUFFER_SIZE)
+ || (!got_new_data
+ && (!this->is_pipe() || request_size > DEFAULT_INCREMENT)))
+ {
+ if ((lf != nullptr)
+ && ((size_t) (lf - line_start) >= MAX_LINE_BUFFER_SIZE - 1))
+ {
+ lf = nullptr;
+ }
+ if (lf != nullptr) {
+ retval.li_partial = false;
+ retval.li_file_range.fr_size = lf - line_start;
+ // delim
+ retval.li_file_range.fr_size += 1;
+ if (offset >= this->lb_last_line_offset) {
+ this->lb_last_line_offset
+ = offset + retval.li_file_range.fr_size;
+ }
+ } else {
+ if (retval.li_file_range.fr_size >= MAX_LINE_BUFFER_SIZE) {
+ log_warning("Line exceeded max size: offset=%d", offset);
+ retval.li_file_range.fr_size = MAX_LINE_BUFFER_SIZE - 1;
+ retval.li_partial = false;
+ } else {
+ retval.li_partial = true;
+ }
+ this->ensure_available(offset, retval.li_file_range.fr_size);
+
+ if (retval.li_file_range.fr_size >= MAX_LINE_BUFFER_SIZE) {
+ retval.li_file_range.fr_size = MAX_LINE_BUFFER_SIZE - 1;
+ }
+ if (retval.li_partial) {
+ /*
+ * Since no delimiter was seen, we need to remember the
+ * offset of the last line in the file so we don't
+ * mistakenly return two partial lines to the caller.
+ *
+ * 1. read_line() - returns partial line
+ * 2. file is written
+ * 3. read_line() - returns the middle of partial line.
+ */
+ this->lb_last_line_offset = offset;
+ } else if (offset >= this->lb_last_line_offset) {
+ this->lb_last_line_offset
+ = offset + retval.li_file_range.fr_size;
+ }
+ }
+
+ offset += retval.li_file_range.fr_size;
+
+ done = true;
+ } else {
+ if (!this->is_pipe() || !this->is_pipe_closed()) {
+ retval.li_partial = true;
+ }
+ request_size
+ = std::min<ssize_t>(this->lb_buffer.size() + DEFAULT_INCREMENT,
+ MAX_LINE_BUFFER_SIZE);
+ }
+
+ if (!done
+ && !this->fill_range(
+ offset,
+ std::max(request_size, (ssize_t) this->lb_buffer.available())))
+ {
+ break;
+ }
+ }
+
+ ensure(retval.li_file_range.fr_size <= this->lb_buffer.size());
+ ensure(this->invariant());
+#if 0
+ log_debug("got line part %d %d",
+ retval.li_file_range.fr_offset,
+ (int) retval.li_partial);
+#endif
+
+ retval.li_file_range.fr_metadata.m_has_ansi
+ = retval.li_utf8_scan_result.usr_has_ansi;
+ retval.li_file_range.fr_metadata.m_valid_utf
+ = retval.li_utf8_scan_result.is_valid();
+ return Ok(retval);
+}
+
+Result<shared_buffer_ref, std::string>
+line_buffer::read_range(const file_range fr)
+{
+ shared_buffer_ref retval;
+ const char* line_start;
+ file_ssize_t avail;
+
+ if (this->lb_last_line_offset != -1
+ && fr.fr_offset > this->lb_last_line_offset)
+ {
+ /*
+ * Don't return anything past the last known line. The caller needs
+ * to try reading at the offset of the last line again.
+ */
+ return Err(
+ fmt::format(FMT_STRING("attempt to read past the known end of the "
+ "file: read-offset={}; last_line_offset={}"),
+ fr.fr_offset,
+ this->lb_last_line_offset));
+ }
+
+ if (!(this->in_range(fr.fr_offset)
+ && this->in_range(fr.fr_offset + fr.fr_size - 1)))
+ {
+ if (!this->fill_range(fr.fr_offset, fr.fr_size)) {
+ return Err(std::string("unable to read file"));
+ }
+ }
+ line_start = this->get_range(fr.fr_offset, avail);
+
+ if (fr.fr_size > avail) {
+ return Err(fmt::format(
+ FMT_STRING("short-read (need: {}; avail: {})"), fr.fr_size, avail));
+ }
+ retval.share(this->lb_share_manager, line_start, fr.fr_size);
+ retval.get_metadata() = fr.fr_metadata;
+
+ return Ok(std::move(retval));
+}
+
+file_range
+line_buffer::get_available()
+{
+ return {this->lb_file_offset,
+ static_cast<file_ssize_t>(this->lb_buffer.size())};
+}
+
+line_buffer::gz_indexed::indexDict::indexDict(const z_stream& s,
+ const file_size_t size)
+{
+ assert((s.data_type & GZ_END_OF_BLOCK_MASK));
+ assert(!(s.data_type & GZ_END_OF_FILE_MASK));
+ assert(size >= s.avail_out + GZ_WINSIZE);
+ this->bits = s.data_type & GZ_BORROW_BITS_MASK;
+ this->in = s.total_in;
+ this->out = s.total_out;
+ auto last_byte_in = s.next_in[-1];
+ this->in_bits = last_byte_in >> (8 - this->bits);
+ // Copy the last 32k uncompressed data (sliding window) to our
+ // index
+ memcpy(this->index, s.next_out - GZ_WINSIZE, GZ_WINSIZE);
+}
+
+int
+line_buffer::gz_indexed::indexDict::apply(z_streamp s)
+{
+ s->zalloc = Z_NULL;
+ s->zfree = Z_NULL;
+ s->opaque = Z_NULL;
+ s->avail_in = 0;
+ s->next_in = Z_NULL;
+ auto ret = inflateInit2(s, GZ_RAW_MODE);
+ if (ret != Z_OK) {
+ return ret;
+ }
+ if (this->bits) {
+ inflatePrime(s, this->bits, this->in_bits);
+ }
+ s->total_in = this->in;
+ s->total_out = this->out;
+ inflateSetDictionary(s, this->index, GZ_WINSIZE);
+ return ret;
+}
+
+bool
+line_buffer::is_likely_to_flush(file_range prev_line)
+{
+ auto avail = this->get_available();
+
+ if (prev_line.fr_offset < avail.fr_offset) {
+ return true;
+ }
+ auto prev_line_end = prev_line.fr_offset + prev_line.fr_size;
+ auto avail_end = avail.fr_offset + avail.fr_size;
+ if (avail_end < prev_line_end) {
+ return true;
+ }
+ auto remaining = avail_end - prev_line_end;
+ return remaining < INITIAL_REQUEST_SIZE;
+}
+
+void
+line_buffer::quiesce()
+{
+ if (this->lb_loader_future.valid()) {
+ this->lb_loader_future.wait();
+ }
+}
+
+static ghc::filesystem::path
+line_buffer_cache_path()
+{
+ return lnav::paths::workdir() / "buffer-cache";
+}
+
+void
+line_buffer::enable_cache()
+{
+ if (!this->lb_compressed || this->lb_cached_fd) {
+ log_info("%d: skipping cache request (compressed=%d already-cached=%d)",
+ this->lb_fd.get(),
+ this->lb_compressed,
+ (bool) this->lb_cached_fd);
+ return;
+ }
+
+ struct stat st;
+
+ if (fstat(this->lb_fd, &st) == -1) {
+ log_error("failed to fstat(%d) - %d", this->lb_fd.get(), errno);
+ return;
+ }
+
+ auto cached_base_name = hasher()
+ .update(st.st_dev)
+ .update(st.st_ino)
+ .update(st.st_size)
+ .to_string();
+ auto cache_dir = line_buffer_cache_path() / cached_base_name.substr(0, 2);
+
+ ghc::filesystem::create_directories(cache_dir);
+
+ auto cached_file_name = fmt::format(FMT_STRING("{}.bin"), cached_base_name);
+ auto cached_file_path = cache_dir / cached_file_name;
+ auto cached_done_path
+ = cache_dir / fmt::format(FMT_STRING("{}.done"), cached_base_name);
+
+ log_info(
+ "%d:cache file path: %s", this->lb_fd.get(), cached_file_path.c_str());
+
+ auto fl = lnav::filesystem::file_lock(cached_file_path);
+ auto guard = lnav::filesystem::file_lock::guard(&fl);
+
+ if (ghc::filesystem::exists(cached_done_path)) {
+ log_info("%d:using existing cache file");
+ auto open_res = lnav::filesystem::open_file(cached_file_path, O_RDWR);
+ if (open_res.isOk()) {
+ this->lb_cached_fd = open_res.unwrap();
+ return;
+ }
+ ghc::filesystem::remove(cached_done_path);
+ }
+
+ auto create_res = lnav::filesystem::create_file(
+ cached_file_path, O_RDWR | O_TRUNC, 0600);
+ if (create_res.isErr()) {
+ log_error("failed to create cache file: %s -- %s",
+ cached_file_path.c_str(),
+ create_res.unwrapErr().c_str());
+ return;
+ }
+
+ auto write_fd = create_res.unwrap();
+ auto done = false;
+
+ static const ssize_t FILL_LENGTH = 1024 * 1024;
+ auto off = file_off_t{0};
+ while (!done) {
+ log_debug("%d: caching file content at %d", this->lb_fd.get(), off);
+ if (!this->fill_range(off, FILL_LENGTH)) {
+ log_debug("%d: caching finished", this->lb_fd.get());
+ done = true;
+ } else {
+ file_ssize_t avail;
+
+ const auto* data = this->get_range(off, avail);
+ auto rc = write(write_fd, data, avail);
+ if (rc != avail) {
+ log_error("%d: short write!", this->lb_fd.get());
+ return;
+ }
+
+ off += avail;
+ }
+ }
+
+ lnav::filesystem::create_file(cached_done_path, O_WRONLY, 0600);
+
+ this->lb_cached_fd = std::move(write_fd);
+}
+
+void
+line_buffer::cleanup_cache()
+{
+ (void) std::async(std::launch::async, []() {
+ auto now = std::chrono::system_clock::now();
+ auto cache_path = line_buffer_cache_path();
+ std::vector<ghc::filesystem::path> to_remove;
+
+ for (const auto& cache_subdir :
+ ghc::filesystem::directory_iterator(cache_path))
+ {
+ for (const auto& entry :
+ ghc::filesystem::directory_iterator(cache_subdir))
+ {
+ auto mtime = ghc::filesystem::last_write_time(entry.path());
+ auto exp_time = mtime + 1h;
+ if (now < exp_time) {
+ continue;
+ }
+
+ to_remove.emplace_back(entry.path());
+ }
+ }
+
+ for (auto& entry : to_remove) {
+ log_debug("removing compressed file cache: %s", entry.c_str());
+ ghc::filesystem::remove_all(entry);
+ }
+ });
+}
diff --git a/src/line_buffer.hh b/src/line_buffer.hh
new file mode 100644
index 0000000..e0d3218
--- /dev/null
+++ b/src/line_buffer.hh
@@ -0,0 +1,370 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file line_buffer.hh
+ */
+
+#ifndef line_buffer_hh
+#define line_buffer_hh
+
+#include <array>
+#include <exception>
+#include <future>
+#include <vector>
+
+#include <errno.h>
+#include <sys/types.h>
+#include <unistd.h>
+#include <zlib.h>
+
+#include "base/auto_fd.hh"
+#include "base/auto_mem.hh"
+#include "base/file_range.hh"
+#include "base/is_utf8.hh"
+#include "base/lnav_log.hh"
+#include "base/result.h"
+#include "safe/safe.h"
+#include "shared_buffer.hh"
+
+struct line_info {
+ file_range li_file_range;
+ bool li_partial{false};
+ utf8_scan_result li_utf8_scan_result{};
+};
+
+/**
+ * Buffer for reading whole lines out of file descriptors. The class presents
+ * a stateless interface, callers specify the offset where a line starts and
+ * the class takes care of caching the surrounding range and locating the
+ * delimiter.
+ *
+ * XXX A bit of a wheel reinvention, but I'm not sure how well the libraries
+ * handle non-blocking I/O...
+ */
+class line_buffer {
+public:
+ static const ssize_t DEFAULT_LINE_BUFFER_SIZE;
+ static const ssize_t MAX_LINE_BUFFER_SIZE;
+ class error : public std::exception {
+ public:
+ explicit error(int err) : e_err(err) {}
+
+ int e_err;
+ };
+
+ struct header_data {
+ timeval hd_mtime{};
+ auto_buffer hd_extra{auto_buffer::alloc(0)};
+ std::string hd_name;
+ std::string hd_comment;
+
+ bool empty() const
+ {
+ return this->hd_mtime.tv_sec == 0 && this->hd_extra.empty()
+ && this->hd_name.empty() && this->hd_comment.empty();
+ }
+ };
+
+#define GZ_WINSIZE 32768U /*> gzip's max supported dictionary is 15-bits */
+#define GZ_RAW_MODE (-15) /*> Raw inflate data mode */
+#define GZ_HEADER_MODE (15 + 32) /*> Automatic zstd or gzip decoding */
+#define GZ_BORROW_BITS_MASK 7 /*> Bits (0-7) consumed in previous block */
+#define GZ_END_OF_BLOCK_MASK 128 /*> Stopped because reached end-of-block */
+#define GZ_END_OF_FILE_MASK 64 /*> Stopped because reached end-of-file */
+
+ /**
+ * A memoized gzip file reader that can do random file access faster than
+ * gzseek/gzread alone.
+ */
+ class gz_indexed {
+ public:
+ gz_indexed();
+ gz_indexed(gz_indexed&& other) = default;
+ ~gz_indexed() { this->close(); }
+
+ inline operator bool() const { return this->gz_fd != -1; }
+
+ uLong get_source_offset() const
+ {
+ return !!*this ? this->strm.total_in + this->strm.avail_in : 0;
+ }
+
+ void close();
+ void init_stream();
+ void continue_stream();
+ void open(int fd, header_data& hd);
+ int stream_data(void* buf, size_t size);
+ void seek(off_t offset);
+
+ /**
+ * Decompress bytes from the gz file returning at most `size` bytes.
+ * offset is the byte-offset in the decompressed data stream.
+ */
+ int read(void* buf, size_t offset, size_t size);
+
+ struct indexDict {
+ off_t in = 0;
+ off_t out = 0;
+ unsigned char bits = 0;
+ unsigned char in_bits = 0;
+ Bytef index[GZ_WINSIZE];
+ indexDict(z_stream const& s, const file_size_t size);
+
+ int apply(z_streamp s);
+ };
+
+ private:
+ z_stream strm; /*< gzip streams structure */
+ std::vector<indexDict>
+ syncpoints; /*< indexed dictionaries as discovered */
+ auto_mem<Bytef> inbuf; /*< Compressed data buffer */
+ int gz_fd = -1; /*< The file to read data from. */
+ };
+
+ /** Construct an empty line_buffer. */
+ line_buffer();
+
+ line_buffer(line_buffer&& other) = delete;
+
+ virtual ~line_buffer();
+
+ /** @param fd The file descriptor that data should be pulled from. */
+ void set_fd(auto_fd& fd);
+
+ /** @return The file descriptor that data should be pulled from. */
+ int get_fd() const { return this->lb_fd; }
+
+ time_t get_file_time() const { return this->lb_file_time; }
+
+ /**
+ * @return The size of the file or the amount of data pulled from a pipe.
+ */
+ file_ssize_t get_file_size() const { return this->lb_file_size; }
+
+ bool is_pipe() const { return !this->lb_seekable; }
+
+ bool is_pipe_closed() const
+ {
+ return !this->lb_seekable && (this->lb_file_size != -1);
+ }
+
+ bool is_compressed() const { return this->lb_compressed; }
+
+ file_off_t get_read_offset(file_off_t off) const
+ {
+ if (this->is_compressed()) {
+ return this->lb_compressed_offset;
+ }
+ return off;
+ }
+
+ bool is_data_available(file_off_t off, file_off_t stat_size) const
+ {
+ if (this->is_compressed()) {
+ return (this->lb_file_size == -1 || off < this->lb_file_size);
+ }
+ return off < stat_size;
+ }
+
+ /**
+ * Attempt to load the next line into the buffer.
+ *
+ * @param prev_line The range of the previous line.
+ * @return If the read was successful, information about the line.
+ * Otherwise, an error message.
+ */
+ Result<line_info, std::string> load_next_line(file_range prev_line = {});
+
+ Result<shared_buffer_ref, std::string> read_range(file_range fr);
+
+ file_range get_available();
+
+ bool is_likely_to_flush(file_range prev_line);
+
+ void flush_at(file_off_t off)
+ {
+ if (this->in_range(off)) {
+ this->lb_buffer.resize(off - this->lb_file_offset);
+ } else {
+ this->lb_buffer.clear();
+ }
+ }
+
+ /** Release any resources held by this object. */
+ void reset()
+ {
+ this->lb_fd.reset();
+
+ this->lb_file_offset = 0;
+ this->lb_file_size = (ssize_t) -1;
+ this->lb_buffer.resize(0);
+ this->lb_last_line_offset = -1;
+ }
+
+ /** Check the invariants for this object. */
+ bool invariant() const
+ {
+ require(this->lb_buffer.size() <= this->lb_buffer.capacity());
+
+ return true;
+ }
+
+ void quiesce();
+
+ struct stats {
+ bool empty() const
+ {
+ return this->s_decompressions == 0 && this->s_preads == 0
+ && this->s_requested_preloads == 0
+ && this->s_used_preloads == 0;
+ }
+
+ uint32_t s_decompressions{0};
+ uint32_t s_preads{0};
+ uint32_t s_requested_preloads{0};
+ uint32_t s_used_preloads{0};
+ std::array<uint32_t, 10> s_hist{};
+ };
+
+ struct stats consume_stats() { return std::exchange(this->lb_stats, {}); }
+
+ size_t get_buffer_size() const { return this->lb_buffer.size(); }
+
+ const header_data& get_header_data() const { return this->lb_header; }
+
+ void enable_cache();
+
+ static void cleanup_cache();
+
+private:
+ /**
+ * @param off The file offset to check for in the buffer.
+ * @return True if the given offset is cached in the buffer.
+ */
+ bool in_range(file_off_t off) const
+ {
+ return this->lb_file_offset <= off
+ && off
+ < (this->lb_file_offset + (file_ssize_t) this->lb_buffer.size());
+ }
+
+ void resize_buffer(size_t new_max);
+
+ /**
+ * Ensure there is enough room in the buffer to cache a range of data from
+ * the file. First, this method will check to see if there is enough room
+ * from where 'start' begins in the buffer to the maximum buffer size. If
+ * this is not enough, the currently cached data at 'start' will be moved
+ * to the beginning of the buffer, overwriting any cached data earlier in
+ * the file. Finally, if this is still not enough, the buffer will be
+ * reallocated to make more room.
+ *
+ * @param start The file offset of the start of the line.
+ * @param max_length The amount of data to be cached in the buffer.
+ */
+ void ensure_available(file_off_t start, ssize_t max_length);
+
+ /**
+ * Fill the buffer with the given range of data from the file.
+ *
+ * @param start The file offset where data should start to be read from the
+ * file.
+ * @param max_length The maximum amount of data to read from the file.
+ * @return True if any data was read from the file.
+ */
+ bool fill_range(file_off_t start, ssize_t max_length);
+
+ /**
+ * After a successful fill, the cached data can be retrieved with this
+ * method.
+ *
+ * @param start The file offset to retrieve cached data for.
+ * @param avail_out On return, the amount of data currently cached at the
+ * given offset.
+ * @return A pointer to the start of the cached data in the internal
+ * buffer.
+ */
+ const char* get_range(file_off_t start, file_ssize_t& avail_out) const
+ {
+ size_t buffer_offset = start - this->lb_file_offset;
+
+ require(buffer_offset >= 0);
+ require(this->lb_buffer.size() >= buffer_offset);
+
+ const auto* retval = this->lb_buffer.at(buffer_offset);
+ avail_out = this->lb_buffer.size() - buffer_offset;
+
+ return retval;
+ }
+
+ bool load_next_buffer();
+
+ using safe_gz_indexed = safe::Safe<gz_indexed>;
+
+ shared_buffer lb_share_manager;
+
+ auto_fd lb_fd; /*< The file to read data from. */
+ safe_gz_indexed lb_gz_file; /*< File reader for gzipped files. */
+ bool lb_bz_file{false}; /*< Flag set for bzip2 compressed files. */
+
+ auto_buffer lb_buffer{auto_buffer::alloc(DEFAULT_LINE_BUFFER_SIZE)};
+ nonstd::optional<auto_buffer> lb_alt_buffer;
+ std::vector<uint32_t> lb_alt_line_starts;
+ std::vector<bool> lb_alt_line_is_utf;
+ std::vector<bool> lb_alt_line_has_ansi;
+ std::future<bool> lb_loader_future;
+ nonstd::optional<file_off_t> lb_loader_file_offset;
+
+ file_off_t lb_compressed_offset{
+ 0}; /*< The offset into the compressed file. */
+ file_ssize_t lb_file_size{
+ -1}; /*<
+ * The size of the file. When lb_fd refers to
+ * a pipe, this is set to the amount of data
+ * read from the pipe when EOF is reached.
+ */
+ file_off_t lb_file_offset{0}; /*<
+ * Data cached in the buffer comes from this
+ * offset in the file.
+ */
+ time_t lb_file_time{0};
+ bool lb_seekable{false}; /*< Flag set for seekable file descriptors. */
+ bool lb_compressed{false};
+ file_off_t lb_last_line_offset{-1}; /*< */
+
+ std::vector<uint32_t> lb_line_starts;
+ std::vector<bool> lb_line_is_utf;
+ std::vector<bool> lb_line_has_ansi;
+ stats lb_stats;
+
+ nonstd::optional<auto_fd> lb_cached_fd;
+
+ header_data lb_header;
+};
+
+#endif
diff --git a/src/listview_curses.cc b/src/listview_curses.cc
new file mode 100644
index 0000000..eb731ea
--- /dev/null
+++ b/src/listview_curses.cc
@@ -0,0 +1,733 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file listview_curses.cc
+ */
+
+#include <cmath>
+
+#include "listview_curses.hh"
+
+#include <sys/time.h>
+#include <time.h>
+
+#include "base/lnav_log.hh"
+#include "config.h"
+
+list_gutter_source listview_curses::DEFAULT_GUTTER_SOURCE;
+
+listview_curses::listview_curses() : lv_scroll(noop_func{}) {}
+
+void
+listview_curses::update_top_from_selection()
+{
+ if (!this->lv_selectable) {
+ return;
+ }
+
+ vis_line_t height;
+ unsigned long width;
+
+ this->get_dimensions(height, width);
+
+ if (this->lv_selection < 0_vl) {
+ this->set_top(0_vl);
+ } else if (this->lv_sync_selection_and_top) {
+ this->set_top(this->lv_selection);
+ } else if (this->lv_selection == this->get_inner_height() - 1_vl) {
+ this->set_top(this->get_top_for_last_row());
+ } else if (this->lv_selection
+ >= (this->lv_top + height - this->lv_tail_space - 1_vl))
+ {
+ auto diff = this->lv_selection
+ - (this->lv_top + height - this->lv_tail_space - 1_vl);
+
+ if (height < 10 || diff < (height / 8_vl)) {
+ // for small differences between the bottom and the
+ // selection, just move a little bit.
+ this->set_top(
+ this->lv_selection - height + 1_vl + this->lv_tail_space, true);
+ } else {
+ // for large differences, put the focus in the middle
+ this->set_top(this->lv_selection - height / 2_vl, true);
+ }
+ } else if (this->lv_selection <= this->lv_top) {
+ auto diff = this->lv_top - this->lv_selection;
+
+ if (this->lv_selection > 0 && (height < 10 || diff < (height / 8_vl))) {
+ this->set_top(this->lv_selection - 1_vl);
+ } else if (this->lv_selection < height) {
+ this->set_top(0_vl);
+ } else {
+ this->set_top(this->lv_selection - height / 2_vl, true);
+ }
+ }
+}
+
+void
+listview_curses::reload_data()
+{
+ if (this->lv_source == nullptr) {
+ this->lv_top = 0_vl;
+ this->lv_left = 0;
+ } else {
+ if (this->lv_top >= this->get_inner_height()) {
+ this->lv_top
+ = std::max(0_vl, vis_line_t(this->get_inner_height() - 1));
+ }
+ if (this->lv_selectable) {
+ if (this->get_inner_height() == 0) {
+ this->set_selection(0_vl);
+ } else if (this->lv_selection >= this->get_inner_height()) {
+ this->set_selection(this->get_inner_height() - 1_vl);
+ } else {
+ auto curr_sel = this->lv_selection;
+
+ this->lv_selection = -1_vl;
+ this->set_selection(curr_sel);
+ }
+
+ this->update_top_from_selection();
+ }
+ }
+ this->vc_needs_update = true;
+ this->invoke_scroll();
+}
+
+bool
+listview_curses::handle_key(int ch)
+{
+ for (auto& lv_input_delegate : this->lv_input_delegates) {
+ if (lv_input_delegate->list_input_handle_key(*this, ch)) {
+ return true;
+ }
+ }
+
+ auto height = 0_vl;
+
+ unsigned long width;
+ bool retval = true;
+
+ this->get_dimensions(height, width);
+ switch (ch) {
+ case 'l':
+ case KEY_RIGHT:
+ this->shift_left(width / 2);
+ break;
+
+ case 'h':
+ case KEY_LEFT:
+ this->shift_left(-(width / 2));
+ break;
+ case 'L':
+ case KEY_SRIGHT:
+ this->shift_left(10);
+ break;
+ case 'H':
+ case KEY_SLEFT:
+ this->shift_left(-10);
+ break;
+
+ case '\r':
+ case 'j':
+ case KEY_DOWN:
+ if (this->is_selectable()) {
+ this->shift_selection(1);
+ } else {
+ this->shift_top(1_vl);
+ }
+ break;
+
+ case 'k':
+ case KEY_UP:
+ if (this->is_selectable()) {
+ this->shift_selection(-1);
+ } else {
+ this->shift_top(-1_vl);
+ }
+ break;
+
+ case 'b':
+ case KEY_BACKSPACE:
+ case KEY_PPAGE:
+ if (this->lv_top == 0_vl && this->lv_selectable
+ && this->lv_selection != 0_vl)
+ {
+ this->set_selection(0_vl);
+ } else {
+ this->shift_top(
+ -(this->rows_available(this->lv_top, RD_UP) - 1_vl));
+ }
+ break;
+
+ case ' ':
+ case KEY_NPAGE: {
+ auto rows_avail
+ = this->rows_available(this->lv_top, RD_DOWN) - 1_vl;
+ auto top_for_last = this->get_top_for_last_row();
+
+ if ((this->lv_top < top_for_last)
+ && (this->lv_top + rows_avail > top_for_last))
+ {
+ this->set_top(top_for_last);
+ if (this->lv_selection <= top_for_last) {
+ this->set_selection(top_for_last + 1_vl);
+ }
+ } else {
+ this->shift_top(rows_avail);
+
+ auto inner_height = this->get_inner_height();
+ if (this->lv_selectable && this->lv_top >= top_for_last
+ && inner_height > 0_vl)
+ {
+ this->set_selection(inner_height - 1_vl);
+ }
+ }
+ break;
+ }
+
+ case 'g':
+ case KEY_HOME:
+ if (this->is_selectable()) {
+ this->set_selection(0_vl);
+ } else {
+ this->set_top(0_vl);
+ }
+ break;
+
+ case 'G':
+ case KEY_END: {
+ vis_line_t last_line(this->get_inner_height() - 1);
+ vis_line_t tail_bottom(this->get_top_for_last_row());
+
+ if (this->is_selectable()) {
+ this->set_selection(last_line);
+ } else if (this->get_top() == last_line) {
+ this->set_top(tail_bottom);
+ } else if (tail_bottom <= this->get_top()) {
+ this->set_top(last_line);
+ } else {
+ this->set_top(tail_bottom);
+ }
+ } break;
+
+ case ']': {
+ double tenth = ((double) this->get_inner_height()) / 10.0;
+
+ this->shift_top(vis_line_t((int) tenth));
+ } break;
+
+ case '[':
+ case 'B': {
+ double tenth = ((double) this->get_inner_height()) / 10.0;
+
+ this->shift_top(vis_line_t((int) -tenth));
+ } break;
+
+ default:
+ retval = false;
+ break;
+ }
+
+ return retval;
+}
+
+void
+listview_curses::do_update()
+{
+ if (this->lv_window == nullptr || this->lv_height == 0) {
+ view_curses::do_update();
+ return;
+ }
+
+ vis_line_t height;
+ unsigned long width;
+
+ this->update_top_from_selection();
+ this->get_dimensions(height, width);
+ while (this->vc_needs_update) {
+ auto& vc = view_colors::singleton();
+ vis_line_t row;
+ attr_line_t overlay_line;
+ struct line_range lr;
+ unsigned long wrap_width;
+ int y = this->lv_y, bottom;
+ auto role_attrs = vc.attrs_for_role(this->vc_default_role);
+
+ if (height <= 0) {
+ return;
+ }
+
+ if (this->vc_width > 0) {
+ width = std::min((unsigned long) this->vc_width, width);
+ }
+
+ wrap_width = width;
+ if (this->lv_show_scrollbar) {
+ wrap_width -= 1;
+ }
+
+ size_t row_count = this->get_inner_height();
+ size_t blank_rows = 0;
+ row = this->lv_top;
+ bottom = y + height;
+ std::vector<attr_line_t> rows(
+ std::min((size_t) height, row_count - (int) this->lv_top));
+ this->lv_source->listview_value_for_rows(*this, row, rows);
+ while (y < bottom) {
+ lr.lr_start = this->lv_left;
+ lr.lr_end = this->lv_left + wrap_width;
+ if (this->lv_overlay_source != nullptr
+ && this->lv_overlay_source->list_value_for_overlay(
+ *this,
+ y - this->lv_y,
+ bottom - this->lv_y,
+ row,
+ overlay_line))
+ {
+ mvwattrline(this->lv_window, y, this->lv_x, overlay_line, lr);
+ overlay_line.clear();
+ ++y;
+ } else if (row < (int) row_count) {
+ auto& al = rows[row - this->lv_top];
+
+ size_t remaining = 0;
+ do {
+ remaining = mvwattrline(this->lv_window,
+ y,
+ this->lv_x,
+ al,
+ lr,
+ this->vc_default_role);
+ if (this->lv_word_wrap) {
+ mvwhline(this->lv_window,
+ y,
+ this->lv_x + wrap_width,
+ ' ',
+ width - wrap_width);
+ }
+ lr.lr_start += wrap_width;
+ lr.lr_end += wrap_width;
+ ++y;
+ } while (this->lv_word_wrap && y < bottom && remaining > 0);
+ ++row;
+ } else {
+ wattr_set(this->lv_window,
+ role_attrs.ta_attrs,
+ vc.ensure_color_pair(role_attrs.ta_fg_color,
+ role_attrs.ta_bg_color),
+ nullptr);
+ mvwhline(this->lv_window, y, this->lv_x, ' ', width);
+ ++y;
+ blank_rows += 1;
+ }
+ }
+
+ if (this->lv_selectable && this->lv_selection >= 0
+ && (row > this->lv_tail_space) && (blank_rows < this->lv_tail_space)
+ && ((row - this->lv_tail_space) < this->lv_selection))
+ {
+ this->shift_top(this->lv_selection - row + this->lv_tail_space);
+ continue;
+ }
+
+ if (this->lv_show_scrollbar) {
+ double progress = 1.0;
+ double coverage = 1.0;
+ double adjusted_height = (double) row_count / (double) height;
+ vis_line_t lines;
+
+ if (row_count > 0) {
+ progress = (double) this->lv_top / (double) row_count;
+ coverage = (double) height / (double) row_count;
+ }
+
+ y = this->lv_y + (int) (progress * (double) height);
+ lines = vis_line_t(
+ y + std::min((int) height, (int) (coverage * (double) height)));
+
+ for (unsigned int gutter_y = this->lv_y;
+ gutter_y < (this->lv_y + height);
+ gutter_y++)
+ {
+ int range_start = 0, range_end;
+ role_t role = this->vc_default_role;
+ role_t bar_role = role_t::VCR_SCROLLBAR;
+ text_attrs attrs;
+ chtype ch = ACS_VLINE;
+
+ if (row_count > 0) {
+ range_start
+ = (double) (gutter_y - this->lv_y) * adjusted_height;
+ }
+ range_end = range_start + adjusted_height;
+
+ this->lv_gutter_source->listview_gutter_value_for_range(
+ *this, range_start, range_end, ch, role, bar_role);
+ if (gutter_y >= (unsigned int) y
+ && gutter_y <= (unsigned int) lines)
+ {
+ role = bar_role;
+ }
+ attrs = vc.attrs_for_role(role);
+ wattr_set(
+ this->lv_window,
+ attrs.ta_attrs,
+ vc.ensure_color_pair(attrs.ta_fg_color, attrs.ta_bg_color),
+ nullptr);
+ mvwaddch(this->lv_window, gutter_y, this->lv_x + width - 1, ch);
+ }
+ wmove(this->lv_window, this->lv_y + height - 1, this->lv_x);
+ }
+
+ if (this->lv_show_bottom_border) {
+ cchar_t row_ch[width];
+ int y = this->lv_y + height - 1;
+
+ mvwin_wchnstr(this->lv_window, y, this->lv_x, row_ch, width - 1);
+ for (unsigned long lpc = 0; lpc < width - 1; lpc++) {
+ row_ch[lpc].attr |= A_UNDERLINE;
+ }
+ mvwadd_wchnstr(this->lv_window, y, this->lv_x, row_ch, width - 1);
+ }
+
+ this->vc_needs_update = false;
+ }
+
+ view_curses::do_update();
+
+#if 0
+ else if (this->lv_overlay_needs_update && this->lv_overlay_source != NULL) {
+ vis_line_t y(this->lv_y), height, bottom;
+ attr_line_t overlay_line;
+ unsigned long width, wrap_width;
+ struct line_range lr;
+
+ this->lv_overlay_source->list_overlay_count(*this);
+ this->get_dimensions(height, width);
+ wrap_width = width - (this->lv_word_wrap ? 1 : this->lv_show_scrollbar ? 1 : 0);
+
+ lr.lr_start = this->lv_left;
+ lr.lr_end = this->lv_left + wrap_width;
+
+ bottom = y + height;
+ while (y < bottom) {
+ if (this->lv_overlay_source->list_value_for_overlay(
+ *this,
+ y - vis_line_t(this->lv_y),
+ overlay_line)) {
+ this->mvwattrline(this->lv_window, y, this->lv_x, overlay_line, lr);
+ overlay_line.clear();
+ }
+ ++y;
+ }
+ }
+#endif
+}
+
+void
+listview_curses::shift_selection(int offset)
+{
+ vis_line_t new_selection = this->lv_selection + vis_line_t(offset);
+
+ if (new_selection >= 0_vl && new_selection < this->get_inner_height()) {
+ this->set_selection(new_selection);
+ }
+}
+
+static int
+scroll_polarity(mouse_button_t button)
+{
+ return button == mouse_button_t::BUTTON_SCROLL_UP ? -1 : 1;
+}
+
+bool
+listview_curses::handle_mouse(mouse_event& me)
+{
+ vis_line_t inner_height, height;
+ struct timeval diff;
+ unsigned long width;
+
+ timersub(&me.me_time, &this->lv_mouse_time, &diff);
+ this->get_dimensions(height, width);
+ inner_height = this->get_inner_height();
+
+ switch (me.me_button) {
+ case mouse_button_t::BUTTON_SCROLL_UP:
+ case mouse_button_t::BUTTON_SCROLL_DOWN:
+ if (diff.tv_sec > 0 || diff.tv_usec > 80000) {
+ this->lv_scroll_accel = 1;
+ this->lv_scroll_velo = 0;
+ } else {
+ this->lv_scroll_accel += 2;
+ }
+ this->lv_scroll_velo += this->lv_scroll_accel;
+
+ this->shift_top(vis_line_t(scroll_polarity(me.me_button)
+ * this->lv_scroll_velo),
+ true);
+ break;
+ default:
+ break;
+ }
+ this->lv_mouse_time = me.me_time;
+
+ if (me.me_button != mouse_button_t::BUTTON_LEFT || inner_height == 0
+ || (this->lv_mouse_mode != lv_mode_t::DRAG
+ && me.me_x < (int) (width - 2)))
+ {
+ return false;
+ }
+
+ if (me.me_state == mouse_button_state_t::BUTTON_STATE_RELEASED) {
+ this->lv_mouse_y = -1;
+ this->lv_mouse_mode = lv_mode_t::NONE;
+ return true;
+ }
+
+ int scroll_top, scroll_bottom, shift_amount = 0, new_top = 0;
+ double top_pct, bot_pct, pct;
+
+ top_pct = (double) this->get_top() / (double) inner_height;
+ bot_pct = (double) this->get_bottom() / (double) inner_height;
+ scroll_top = (this->get_y() + (int) (top_pct * (double) height));
+ scroll_bottom = (this->get_y() + (int) (bot_pct * (double) height));
+
+ if (this->lv_mouse_mode == lv_mode_t::NONE) {
+ if ((scroll_top - 1) <= me.me_y && me.me_y <= (scroll_bottom + 1)) {
+ this->lv_mouse_mode = lv_mode_t::DRAG;
+ this->lv_mouse_y = me.me_y - scroll_top;
+ } else if (me.me_y < scroll_top) {
+ this->lv_mouse_mode = lv_mode_t::UP;
+ } else {
+ this->lv_mouse_mode = lv_mode_t::DOWN;
+ }
+ }
+
+ switch (this->lv_mouse_mode) {
+ case lv_mode_t::NONE:
+ require(0);
+ break;
+
+ case lv_mode_t::UP:
+ if (me.me_y < scroll_top) {
+ shift_amount = -1 * height;
+ }
+ break;
+
+ case lv_mode_t::DOWN:
+ if (me.me_y > scroll_bottom) {
+ shift_amount = height;
+ }
+ break;
+
+ case lv_mode_t::DRAG:
+ pct = (double) inner_height / (double) height;
+ new_top = me.me_y - this->get_y() - this->lv_mouse_y;
+ new_top = (int) floor(((double) new_top * pct) + 0.5);
+ this->set_top(vis_line_t(new_top));
+ break;
+ }
+
+ if (shift_amount != 0) {
+ this->shift_top(vis_line_t(shift_amount));
+ }
+
+ return true;
+}
+
+void
+listview_curses::set_top(vis_line_t top, bool suppress_flash)
+{
+ auto inner_height = this->get_inner_height();
+
+ if (inner_height > 0 && top >= inner_height) {
+ top = vis_line_t(inner_height - 1);
+ }
+ if (top < 0 || (top > 0 && top >= inner_height)) {
+ if (suppress_flash == false) {
+ alerter::singleton().chime("invalid top");
+ }
+ } else if (this->lv_top != top) {
+ auto old_top = this->lv_top;
+ this->lv_top = top;
+ if (this->lv_selectable) {
+ if (this->lv_selection < 0_vl) {
+ } else if (this->lv_selection < top) {
+ auto sel_diff = this->lv_selection - old_top;
+ this->set_selection(top + sel_diff);
+ } else {
+ auto sel_diff = this->lv_selection - old_top;
+ auto bot = this->get_bottom();
+ unsigned long width;
+ vis_line_t height;
+
+ this->get_dimensions(height, width);
+
+ if (bot != -1_vl && (bot - top) >= (height - 1)) {
+ if (this->lv_selection > (bot - this->lv_tail_space)) {
+ this->set_selection(top + sel_diff);
+ }
+ }
+ }
+ }
+ this->invoke_scroll();
+ this->set_needs_update();
+ }
+}
+
+vis_line_t
+listview_curses::get_bottom() const
+{
+ auto retval = this->lv_top;
+ auto avail = this->rows_available(retval, RD_DOWN);
+
+ if (avail > 0) {
+ retval += vis_line_t(avail - 1);
+ }
+
+ return retval;
+}
+
+vis_line_t
+listview_curses::rows_available(vis_line_t line,
+ listview_curses::row_direction_t dir) const
+{
+ unsigned long width;
+ vis_line_t height;
+ vis_line_t retval(0);
+
+ this->get_dimensions(height, width);
+ if (this->lv_word_wrap) {
+ size_t row_count = this->lv_source->listview_rows(*this);
+
+ width -= 1;
+ while ((height > 0) && (line >= 0) && ((size_t) line < row_count)) {
+ size_t len = this->lv_source->listview_size_for_row(*this, line);
+
+ do {
+ len -= std::min((size_t) width, len);
+ --height;
+ } while (len > 0);
+ line += vis_line_t(dir);
+ if (height >= 0) {
+ ++retval;
+ }
+ }
+ } else {
+ switch (dir) {
+ case RD_UP:
+ retval = std::min(height, line + 1_vl);
+ break;
+ case RD_DOWN:
+ retval = std::min(
+ height,
+ vis_line_t(this->lv_source->listview_rows(*this) - line));
+ break;
+ }
+ }
+
+ return retval;
+}
+
+void
+listview_curses::set_selection(vis_line_t sel)
+{
+ if (this->lv_selectable) {
+ if (this->lv_selection == sel) {
+ return;
+ }
+ if (sel == -1_vl) {
+ this->lv_selection = sel;
+ this->lv_source->listview_selection_changed(*this);
+ this->set_needs_update();
+ this->invoke_scroll();
+ return;
+ }
+
+ auto inner_height = this->get_inner_height();
+ if (sel >= inner_height) {
+ sel = inner_height - 1_vl;
+ }
+ if (sel >= 0_vl) {
+ auto found = false;
+ vis_line_t step;
+
+ if (sel == 0_vl) {
+ step = 1_vl;
+ } else if (sel == inner_height - 1_vl) {
+ step = -1_vl;
+ } else if (sel < this->lv_selection) {
+ step = -1_vl;
+ } else {
+ step = 1_vl;
+ }
+ while (sel < inner_height) {
+ if (this->lv_source->listview_is_row_selectable(*this, sel)) {
+ found = true;
+ break;
+ }
+ sel += step;
+ }
+ if (found) {
+ this->lv_selection = sel;
+ if (this->lv_sync_selection_and_top) {
+ this->lv_top = sel;
+ }
+ this->lv_source->listview_selection_changed(*this);
+ this->set_needs_update();
+ this->invoke_scroll();
+ }
+ }
+ } else {
+ this->set_top(sel);
+ }
+}
+
+vis_line_t
+listview_curses::get_top_for_last_row()
+{
+ auto inner_height = this->get_inner_height();
+ auto retval = 0_vl;
+
+ if (inner_height > 0) {
+ auto last_line = inner_height - 1_vl;
+ unsigned long width;
+ vis_line_t height;
+
+ this->get_dimensions(height, width);
+ retval = last_line - this->rows_available(last_line, RD_UP) + 1_vl;
+ if (inner_height >= (height - this->lv_tail_space)
+ && (retval + this->lv_tail_space) < inner_height)
+ {
+ retval += this->lv_tail_space;
+ }
+ }
+
+ return retval;
+}
diff --git a/src/listview_curses.hh b/src/listview_curses.hh
new file mode 100644
index 0000000..bf375bc
--- /dev/null
+++ b/src/listview_curses.hh
@@ -0,0 +1,570 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file listview_curses.hh
+ */
+
+#ifndef listview_curses_hh
+#define listview_curses_hh
+
+#include <list>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <sys/types.h>
+
+#include "base/func_util.hh"
+#include "view_curses.hh"
+#include "vis_line.hh"
+
+class listview_curses;
+
+/**
+ * Data source for lines to be displayed by the listview_curses object.
+ */
+class list_data_source {
+public:
+ virtual ~list_data_source() = default;
+
+ /** @return The number of rows in the list. */
+ virtual size_t listview_rows(const listview_curses& lv) = 0;
+
+ virtual size_t listview_width(const listview_curses& lv) { return INT_MAX; }
+
+ /**
+ * Get the string value for a row in the list.
+ *
+ * @param row The row number.
+ * @param value_out The destination for the string value.
+ */
+ virtual void listview_value_for_rows(const listview_curses& lv,
+ vis_line_t start_row,
+ std::vector<attr_line_t>& rows_out)
+ = 0;
+
+ virtual size_t listview_size_for_row(const listview_curses& lv,
+ vis_line_t row)
+ = 0;
+
+ virtual std::string listview_source_name(const listview_curses& lv)
+ {
+ return "";
+ }
+
+ virtual bool listview_is_row_selectable(const listview_curses& lv,
+ vis_line_t row)
+ {
+ return true;
+ }
+
+ virtual void listview_selection_changed(const listview_curses& lv) {}
+};
+
+class list_gutter_source {
+public:
+ virtual ~list_gutter_source() = default;
+
+ virtual void listview_gutter_value_for_range(const listview_curses& lv,
+ int start,
+ int end,
+ chtype& ch_out,
+ role_t& role_out,
+ role_t& bar_role_out)
+ {
+ ch_out = ACS_VLINE;
+ }
+};
+
+class list_overlay_source {
+public:
+ virtual ~list_overlay_source() = default;
+
+ virtual bool list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t line,
+ attr_line_t& value_out)
+ = 0;
+};
+
+class list_input_delegate {
+public:
+ virtual ~list_input_delegate() = default;
+
+ virtual bool list_input_handle_key(listview_curses& lv, int ch) = 0;
+
+ virtual void list_input_handle_scroll_out(listview_curses& lv) {}
+};
+
+/**
+ * View that displays a list of lines that can optionally contain highlighting.
+ */
+class listview_curses
+ : public view_curses
+ , private log_state_dumper {
+public:
+ using action = std::function<void(listview_curses*)>;
+
+ listview_curses();
+
+ listview_curses(const listview_curses&) = delete;
+ listview_curses(listview_curses&) = delete;
+
+ void set_title(const std::string& title) { this->lv_title = title; }
+
+ const std::string& get_title() const { return this->lv_title; }
+
+ /** @param src The data source delegate. */
+ void set_data_source(list_data_source* src)
+ {
+ this->lv_source = src;
+ this->invoke_scroll();
+ this->reload_data();
+ }
+
+ /** @return The data source delegate. */
+ list_data_source* get_data_source() const { return this->lv_source; }
+
+ void set_gutter_source(list_gutter_source* src)
+ {
+ this->lv_gutter_source = src;
+ }
+
+ /** @param src The data source delegate. */
+ listview_curses& set_overlay_source(list_overlay_source* src)
+ {
+ this->lv_overlay_source = src;
+ this->reload_data();
+
+ return *this;
+ }
+
+ /** @return The overlay source delegate. */
+ list_overlay_source* get_overlay_source()
+ {
+ return this->lv_overlay_source;
+ }
+
+ listview_curses& add_input_delegate(list_input_delegate& lid)
+ {
+ this->lv_input_delegates.push_back(&lid);
+
+ return *this;
+ }
+
+ /**
+ * @param va The action to invoke when the view is scrolled.
+ * @todo Allow multiple observers.
+ */
+ void set_scroll_action(action va) { this->lv_scroll = std::move(va); }
+
+ void set_show_scrollbar(bool ss) { this->lv_show_scrollbar = ss; }
+
+ bool get_show_scrollbar() const { return this->lv_show_scrollbar; }
+
+ void set_show_bottom_border(bool val)
+ {
+ if (this->lv_show_bottom_border != val) {
+ this->lv_show_bottom_border = val;
+ this->set_needs_update();
+ }
+ }
+ bool get_show_bottom_border() const { return this->lv_show_bottom_border; }
+
+ void set_selectable(bool sel)
+ {
+ this->lv_selectable = sel;
+ this->vc_needs_update = true;
+ }
+
+ bool is_selectable() const { return this->lv_selectable; }
+
+ void set_selection(vis_line_t sel);
+
+ void shift_selection(int offset);
+
+ vis_line_t get_selection() const
+ {
+ if (this->lv_selectable) {
+ return this->lv_selection;
+ }
+ return this->lv_top;
+ }
+
+ void set_sync_selection_and_top(bool value)
+ {
+ this->lv_sync_selection_and_top = value;
+ if (value) {
+ this->set_top(this->get_selection());
+ }
+ }
+
+ listview_curses& set_word_wrap(bool ww)
+ {
+ bool scroll_down = this->lv_top >= this->get_top_for_last_row();
+
+ this->lv_word_wrap = ww;
+ if (ww && scroll_down && this->lv_top < this->get_top_for_last_row()) {
+ this->lv_top = this->get_top_for_last_row();
+ }
+ if (ww) {
+ this->lv_left = 0;
+ }
+ this->set_needs_update();
+
+ return *this;
+ }
+
+ bool get_word_wrap() const { return this->lv_word_wrap; }
+
+ enum row_direction_t {
+ RD_UP = -1,
+ RD_DOWN = 1,
+ };
+
+ vis_line_t rows_available(vis_line_t line, row_direction_t dir) const;
+
+ template<typename F>
+ auto map_top_row(F func) ->
+ typename std::result_of<F(const attr_line_t&)>::type
+ {
+ if (this->lv_top >= this->get_inner_height()) {
+ return nonstd::nullopt;
+ }
+
+ std::vector<attr_line_t> top_line{1};
+
+ this->lv_source->listview_value_for_rows(*this, this->lv_top, top_line);
+ return func(top_line[0]);
+ }
+
+ /** @param win The curses window this view is attached to. */
+ void set_window(WINDOW* win) { this->lv_window = win; }
+
+ /** @return The curses window this view is attached to. */
+ WINDOW* get_window() const { return this->lv_window; }
+
+ void set_y(unsigned int y)
+ {
+ if (y != this->lv_y) {
+ this->lv_y = y;
+ this->set_needs_update();
+ }
+ }
+
+ unsigned int get_y() const { return this->lv_y; }
+
+ void set_x(unsigned int x)
+ {
+ if (x != this->lv_x) {
+ this->lv_x = x;
+ this->set_needs_update();
+ }
+ }
+
+ unsigned int get_x() const { return this->lv_x; }
+
+ /**
+ * Set the line number to be displayed at the top of the view. If the
+ * value is invalid, flash() will be called. If the value is valid, the
+ * new value will be set and the scroll action called.
+ *
+ * @param top The new value for top.
+ * @param suppress_flash Don't call flash() if the top is out-of-bounds.
+ */
+ void set_top(vis_line_t top, bool suppress_flash = false);
+
+ /** @return The line number that is displayed at the top. */
+ vis_line_t get_top() const { return this->lv_top; }
+
+ nonstd::optional<vis_line_t> get_top_opt() const
+ {
+ if (this->get_inner_height() == 0_vl) {
+ return nonstd::nullopt;
+ }
+
+ return this->lv_top;
+ }
+
+ /** @return The line number that is displayed at the bottom. */
+ vis_line_t get_bottom() const;
+
+ vis_line_t get_top_for_last_row();
+
+ /** @return True if the given line is visible. */
+ bool is_line_visible(vis_line_t line) const
+ {
+ return this->get_top() <= line && line <= this->get_bottom();
+ }
+
+ /**
+ * Shift the value of top by the given value.
+ *
+ * @param offset The amount to change top by.
+ * @param suppress_flash Don't call flash() if the offset is out-of-bounds.
+ * @return The final value of top.
+ */
+ vis_line_t shift_top(vis_line_t offset, bool suppress_flash = false)
+ {
+ if (offset < 0 && this->lv_top == 0) {
+ if (suppress_flash == false) {
+ alerter::singleton().chime(
+ "the top of the view has been reached");
+ }
+ } else {
+ this->set_top(std::max(0_vl, this->lv_top + offset),
+ suppress_flash);
+ }
+
+ return this->lv_top;
+ }
+
+ /**
+ * Set the column number to be displayed at the left of the view. If the
+ * value is invalid, flash() will be called. If the value is valid, the
+ * new value will be set and the scroll action called.
+ *
+ * @param left The new value for left.
+ */
+ void set_left(unsigned int left)
+ {
+ if (this->lv_left == left) {
+ return;
+ }
+
+ if (left > this->lv_left) {
+ unsigned long width;
+ vis_line_t height;
+
+ this->get_dimensions(height, width);
+ if ((this->get_inner_width() - this->lv_left) <= width) {
+ alerter::singleton().chime(
+ "the maximum width of the view has been reached");
+ return;
+ }
+ }
+
+ this->lv_left = left;
+ this->invoke_scroll();
+ this->set_needs_update();
+ }
+
+ /** @return The column number that is displayed at the left. */
+ unsigned int get_left() const { return this->lv_left; }
+
+ /**
+ * Shift the value of left by the given value.
+ *
+ * @param offset The amount to change top by.
+ * @return The final value of top.
+ */
+ unsigned int shift_left(int offset)
+ {
+ if (this->lv_word_wrap) {
+ alerter::singleton().chime(
+ "cannot scroll horizontally when word wrap is enabled");
+ } else if (offset < 0 && this->lv_left < (unsigned int) -offset) {
+ this->set_left(0);
+ } else {
+ this->set_left(this->lv_left + offset);
+ }
+
+ return this->lv_left;
+ }
+
+ /**
+ * Set the height of the view. A value greater than one is considered to
+ * be an absolute size. A value less than or equal to zero makes the
+ * height relative to the size of the enclosing window.
+ *
+ * @height The new height.
+ */
+ void set_height(vis_line_t height)
+ {
+ if (this->lv_height != height) {
+ this->lv_height = height;
+ this->set_needs_update();
+ }
+ }
+
+ /** @return The absolute or relative height of the window. */
+ vis_line_t get_height() const { return this->lv_height; }
+
+ /** @return The number of rows of data in this view's source data. */
+ vis_line_t get_inner_height() const
+ {
+ return vis_line_t(this->lv_source == nullptr
+ ? 0
+ : this->lv_source->listview_rows(*this));
+ }
+
+ size_t get_inner_width() const
+ {
+ return this->lv_source == nullptr
+ ? 0
+ : this->lv_source->listview_width(*this);
+ }
+
+ void set_overlay_needs_update() { this->lv_overlay_needs_update = true; }
+
+ /**
+ * Get the actual dimensions of the view.
+ *
+ * @param height_out The actual height of the view in lines.
+ * @param width_out The actual width of the view in columns.
+ */
+ void get_dimensions(vis_line_t& height_out, unsigned long& width_out) const
+ {
+ unsigned long height;
+
+ if (this->lv_window == nullptr) {
+ height_out = std::max(this->lv_height, 1_vl);
+ if (this->lv_source) {
+ width_out = this->lv_source->listview_width(*this);
+ } else {
+ width_out = 80;
+ }
+ } else {
+ getmaxyx(this->lv_window, height, width_out);
+ if (this->lv_height < 0) {
+ height_out = vis_line_t(height) + this->lv_height
+ - vis_line_t(this->lv_y);
+ if (height_out < 0_vl) {
+ height_out = 0_vl;
+ }
+ } else {
+ height_out = this->lv_height;
+ }
+ }
+ if (this->lv_x < width_out) {
+ width_out -= this->lv_x;
+ } else {
+ width_out = 0;
+ }
+ }
+
+ std::pair<vis_line_t, unsigned long> get_dimensions() const
+ {
+ unsigned long width;
+ vis_line_t height;
+
+ this->get_dimensions(height, width);
+ return std::make_pair(height, width);
+ }
+
+ /** This method should be called when the data source has changed. */
+ virtual void reload_data();
+
+ /**
+ * @param ch The input to be handled.
+ * @return True if the key was eaten by this view.
+ */
+ bool handle_key(int ch);
+
+ /**
+ * Query the data source and draw the visible lines on the display.
+ */
+ void do_update();
+
+ bool handle_mouse(mouse_event& me);
+
+ listview_curses& set_tail_space(vis_line_t space)
+ {
+ this->lv_tail_space = space;
+
+ return *this;
+ }
+
+ vis_line_t get_tail_space() const { return this->lv_tail_space; }
+
+ void log_state()
+ {
+ log_debug("listview_curses=%p", this);
+ log_debug(" lv_title=%s", this->lv_title.c_str());
+ log_debug(" lv_y=%u", this->lv_y);
+ log_debug(" lv_top=%d", (int) this->lv_top);
+ log_debug(" lv_left=%d", (int) this->lv_left);
+ log_debug(" lv_height=%d", this->lv_height);
+ log_debug(" lv_selection=%d", (int) this->lv_selection);
+ log_debug(" inner_height=%d", (int) this->get_inner_height());
+ }
+
+ virtual void invoke_scroll() { this->lv_scroll(this); }
+
+protected:
+ void delegate_scroll_out()
+ {
+ for (auto& lv_input_delegate : this->lv_input_delegates) {
+ lv_input_delegate->list_input_handle_scroll_out(*this);
+ }
+ }
+
+ void update_top_from_selection();
+
+ enum class lv_mode_t {
+ NONE,
+ DOWN,
+ UP,
+ DRAG
+ };
+
+ static list_gutter_source DEFAULT_GUTTER_SOURCE;
+
+ std::string lv_title;
+ list_data_source* lv_source{nullptr}; /*< The data source delegate. */
+ std::list<list_input_delegate*> lv_input_delegates;
+ list_overlay_source* lv_overlay_source{nullptr};
+ action lv_scroll; /*< The scroll action. */
+ WINDOW* lv_window{nullptr}; /*< The window that contains this view. */
+ unsigned int lv_x{0};
+ unsigned int lv_y{0}; /*< The y offset of this view. */
+ vis_line_t lv_top{0}; /*< The line at the top of the view. */
+ unsigned int lv_left{0}; /*< The column at the left of the view. */
+ vis_line_t lv_height{0}; /*< The abs/rel height of the view. */
+ int lv_history_position{0};
+ bool lv_overlay_needs_update{true};
+ bool lv_show_scrollbar{true}; /*< Draw the scrollbar in the view. */
+ bool lv_show_bottom_border{false};
+ list_gutter_source* lv_gutter_source{&DEFAULT_GUTTER_SOURCE};
+ bool lv_word_wrap{false};
+ bool lv_selectable{false};
+ vis_line_t lv_selection{0};
+ bool lv_sync_selection_and_top{false};
+
+ struct timeval lv_mouse_time {
+ 0, 0
+ };
+ int lv_scroll_accel{1};
+ int lv_scroll_velo{0};
+ int lv_mouse_y{-1};
+ lv_mode_t lv_mouse_mode{lv_mode_t::NONE};
+ vis_line_t lv_tail_space{1};
+};
+
+#endif
diff --git a/src/lnav.cc b/src/lnav.cc
new file mode 100644
index 0000000..6d56b4d
--- /dev/null
+++ b/src/lnav.cc
@@ -0,0 +1,3387 @@
+/**
+ * Copyright (c) 2007-2016, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav.cc
+ *
+ * XXX This file has become a dumping ground for code and needs to be broken up
+ * a bit.
+ */
+
+#ifdef __CYGWIN__
+# include <alloca.h>
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <locale.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/stat.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <termios.h>
+#include <time.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#if defined(__OpenBSD__) && defined(__clang__) \
+ && !defined(_WCHAR_H_CPLUSPLUS_98_CONFORMANCE_)
+# define _WCHAR_H_CPLUSPLUS_98_CONFORMANCE_
+#endif
+#include <algorithm>
+#include <functional>
+#include <map>
+#include <memory>
+#include <set>
+#include <utility>
+#include <vector>
+
+#include <sqlite3.h>
+
+#ifdef HAVE_BZLIB_H
+# include <bzlib.h>
+#endif
+
+#include "all_logs_vtab.hh"
+#include "base/ansi_scrubber.hh"
+#include "base/fs_util.hh"
+#include "base/func_util.hh"
+#include "base/future_util.hh"
+#include "base/humanize.hh"
+#include "base/humanize.time.hh"
+#include "base/injector.bind.hh"
+#include "base/isc.hh"
+#include "base/itertools.hh"
+#include "base/lnav.console.hh"
+#include "base/lnav_log.hh"
+#include "base/paths.hh"
+#include "base/string_util.hh"
+#include "bookmarks.hh"
+#include "bottom_status_source.hh"
+#include "bound_tags.hh"
+#include "breadcrumb_curses.hh"
+#include "CLI/CLI.hpp"
+#include "dump_internals.hh"
+#include "environ_vtab.hh"
+#include "filter_sub_source.hh"
+#include "fstat_vtab.hh"
+#include "grep_proc.hh"
+#include "hist_source.hh"
+#include "init-sql.h"
+#include "listview_curses.hh"
+#include "lnav.events.hh"
+#include "lnav.hh"
+#include "lnav.indexing.hh"
+#include "lnav.management_cli.hh"
+#include "lnav_commands.hh"
+#include "lnav_config.hh"
+#include "lnav_util.hh"
+#include "log_data_helper.hh"
+#include "log_data_table.hh"
+#include "log_format_loader.hh"
+#include "log_gutter_source.hh"
+#include "log_vtab_impl.hh"
+#include "logfile.hh"
+#include "logfile_sub_source.hh"
+#include "piper_proc.hh"
+#include "readline_curses.hh"
+#include "readline_highlighters.hh"
+#include "regexp_vtab.hh"
+#include "scn/scn.h"
+#include "service_tags.hh"
+#include "session_data.hh"
+#include "spectro_source.hh"
+#include "sql_help.hh"
+#include "sql_util.hh"
+#include "sqlite-extension-func.hh"
+#include "sqlitepp.client.hh"
+#include "static_file_vtab.hh"
+#include "tailer/tailer.looper.hh"
+#include "term_extra.hh"
+#include "termios_guard.hh"
+#include "textfile_highlighters.hh"
+#include "textview_curses.hh"
+#include "top_status_source.hh"
+#include "view_helpers.crumbs.hh"
+#include "view_helpers.examples.hh"
+#include "view_helpers.hist.hh"
+#include "views_vtab.hh"
+#include "vt52_curses.hh"
+#include "xpath_vtab.hh"
+#include "xterm_mouse.hh"
+
+#ifdef HAVE_LIBCURL
+# include <curl/curl.h>
+#endif
+
+#include "curl_looper.hh"
+
+#if HAVE_ARCHIVE_H
+# include <archive.h>
+#endif
+
+#include "archive_manager.hh"
+#include "command_executor.hh"
+#include "field_overlay_source.hh"
+#include "hotkeys.hh"
+#include "log_actions.hh"
+#include "readline_callbacks.hh"
+#include "readline_possibilities.hh"
+#include "shlex.hh"
+#include "url_loader.hh"
+#include "yajlpp/yajlpp.hh"
+
+#ifndef SYSCONFDIR
+# define SYSCONFDIR "/usr/etc"
+#endif
+
+using namespace std::literals::chrono_literals;
+using namespace lnav::roles::literals;
+
+static std::vector<std::string> DEFAULT_FILES;
+static auto intern_lifetime = intern_string::get_table_lifetime();
+
+const int ZOOM_LEVELS[] = {
+ 1,
+ 30,
+ 60,
+ 5 * 60,
+ 15 * 60,
+ 60 * 60,
+ 4 * 60 * 60,
+ 8 * 60 * 60,
+ 24 * 60 * 60,
+ 7 * 24 * 60 * 60,
+};
+
+const ssize_t ZOOM_COUNT = sizeof(ZOOM_LEVELS) / sizeof(int);
+
+const std::vector<std::string> lnav_zoom_strings = {
+ "1-second",
+ "30-second",
+ "1-minute",
+ "5-minute",
+ "15-minute",
+ "1-hour",
+ "4-hour",
+ "8-hour",
+ "1-day",
+ "1-week",
+};
+
+static const std::vector<std::string> DEFAULT_DB_KEY_NAMES = {
+ "match_index",
+ "capture_index",
+ "capture_count",
+ "range_start",
+ "range_stop",
+ "inode",
+ "device",
+ "inode",
+ "rowid",
+ "st_dev",
+ "st_ino",
+ "st_mode",
+ "st_rdev",
+ "st_uid",
+ "st_gid",
+};
+
+const static file_ssize_t MAX_STDIN_CAPTURE_SIZE = 10 * 1024 * 1024;
+
+static auto bound_pollable_supervisor
+ = injector::bind<pollable_supervisor>::to_singleton();
+
+static auto bound_filter_sub_source
+ = injector::bind<filter_sub_source>::to_singleton();
+
+static auto bound_active_files = injector::bind<file_collection>::to_instance(
+ +[]() { return &lnav_data.ld_active_files; });
+
+static auto bound_sqlite_db
+ = injector::bind<auto_sqlite3>::to_instance(&lnav_data.ld_db);
+
+static auto bound_lnav_flags
+ = injector::bind<unsigned long, lnav_flags_tag>::to_instance(
+ &lnav_data.ld_flags);
+
+static auto bound_last_rel_time
+ = injector::bind<relative_time, last_relative_time_tag>::to_singleton();
+
+static auto bound_term_extra = injector::bind<term_extra>::to_singleton();
+
+static auto bound_xterm_mouse = injector::bind<xterm_mouse>::to_singleton();
+
+static auto bound_scripts = injector::bind<available_scripts>::to_singleton();
+
+static auto bound_curl
+ = injector::bind_multiple<isc::service_base>()
+ .add_singleton<curl_looper, services::curl_streamer_t>();
+
+static auto bound_tailer
+ = injector::bind_multiple<isc::service_base>()
+ .add_singleton<tailer::looper, services::remote_tailer_t>();
+
+static auto bound_main = injector::bind_multiple<static_service>()
+ .add_singleton<main_looper, services::main_t>();
+
+namespace injector {
+template<>
+void
+force_linking(last_relative_time_tag anno)
+{
+}
+
+template<>
+void
+force_linking(lnav_flags_tag anno)
+{
+}
+
+template<>
+void
+force_linking(services::curl_streamer_t anno)
+{
+}
+
+template<>
+void
+force_linking(services::remote_tailer_t anno)
+{
+}
+
+template<>
+void
+force_linking(services::main_t anno)
+{
+}
+} // namespace injector
+
+static breadcrumb_curses breadcrumb_view;
+
+struct lnav_data_t lnav_data;
+
+bool
+setup_logline_table(exec_context& ec)
+{
+ // Hidden columns don't show up in the table_info pragma.
+ static const char* hidden_table_columns[] = {
+ "log_time_msecs",
+ "log_path",
+ "log_text",
+ "log_body",
+
+ nullptr,
+ };
+
+ textview_curses& log_view = lnav_data.ld_views[LNV_LOG];
+ bool retval = false;
+ bool update_possibilities
+ = (lnav_data.ld_rl_view != nullptr && ec.ec_local_vars.size() == 1);
+
+ if (update_possibilities) {
+ lnav_data.ld_rl_view->clear_possibilities(ln_mode_t::SQL, "*");
+ add_view_text_possibilities(lnav_data.ld_rl_view,
+ ln_mode_t::SQL,
+ "*",
+ &log_view,
+ text_quoting::sql);
+ }
+
+ if (log_view.get_inner_height()) {
+ static intern_string_t logline = intern_string::lookup("logline");
+ vis_line_t vl = log_view.get_selection();
+ content_line_t cl = lnav_data.ld_log_source.at_base(vl);
+
+ lnav_data.ld_vtab_manager->unregister_vtab(logline);
+ lnav_data.ld_vtab_manager->register_vtab(
+ std::make_shared<log_data_table>(lnav_data.ld_log_source,
+ *lnav_data.ld_vtab_manager,
+ cl,
+ logline));
+
+ if (update_possibilities) {
+ log_data_helper ldh(lnav_data.ld_log_source);
+
+ ldh.parse_line(cl);
+
+ std::map<const intern_string_t,
+ json_ptr_walk::walk_list_t>::const_iterator pair_iter;
+ for (pair_iter = ldh.ldh_json_pairs.begin();
+ pair_iter != ldh.ldh_json_pairs.end();
+ ++pair_iter)
+ {
+ for (size_t lpc = 0; lpc < pair_iter->second.size(); lpc++) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL,
+ "*",
+ ldh.format_json_getter(pair_iter->first, lpc));
+ }
+ }
+ }
+
+ retval = true;
+ }
+
+ auto& db_key_names = lnav_data.ld_db_key_names;
+
+ db_key_names = DEFAULT_DB_KEY_NAMES;
+
+ if (update_possibilities) {
+ add_env_possibilities(ln_mode_t::SQL);
+
+ lnav_data.ld_rl_view->add_possibility(ln_mode_t::SQL,
+ "*",
+ std::begin(sql_keywords),
+ std::end(sql_keywords));
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL, "*", sql_function_names);
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL, "*", hidden_table_columns);
+
+ for (int lpc = 0; sqlite_registration_funcs[lpc]; lpc++) {
+ struct FuncDef* basic_funcs;
+ struct FuncDefAgg* agg_funcs;
+
+ sqlite_registration_funcs[lpc](&basic_funcs, &agg_funcs);
+ for (int lpc2 = 0; basic_funcs && basic_funcs[lpc2].zName; lpc2++) {
+ const FuncDef& func_def = basic_funcs[lpc2];
+
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL,
+ "*",
+ std::string(func_def.zName) + (func_def.nArg ? "(" : "()"));
+ }
+ for (int lpc2 = 0; agg_funcs && agg_funcs[lpc2].zName; lpc2++) {
+ const FuncDefAgg& func_def = agg_funcs[lpc2];
+
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL,
+ "*",
+ std::string(func_def.zName) + (func_def.nArg ? "(" : "()"));
+ }
+ }
+
+ for (const auto& pair : sqlite_function_help) {
+ switch (pair.second->ht_context) {
+ case help_context_t::HC_SQL_FUNCTION:
+ case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION: {
+ std::string poss = pair.first
+ + (pair.second->ht_parameters.empty() ? "()" : ("("));
+
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL, "*", poss);
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ walk_sqlite_metadata(lnav_data.ld_db.in(), lnav_sql_meta_callbacks);
+
+ for (const auto& iter : *lnav_data.ld_vtab_manager) {
+ iter.second->get_foreign_keys(db_key_names);
+ }
+
+ stable_sort(db_key_names.begin(), db_key_names.end());
+
+ return retval;
+}
+
+static bool
+append_default_files()
+{
+ bool retval = true;
+ auto cwd = ghc::filesystem::current_path();
+
+ for (const auto& path : DEFAULT_FILES) {
+ if (access(path.c_str(), R_OK) == 0) {
+ auto_mem<char> abspath;
+
+ auto full_path = cwd / path;
+ if ((abspath = realpath(full_path.c_str(), nullptr)) == nullptr) {
+ perror("Unable to resolve path");
+ } else {
+ lnav_data.ld_active_files.fc_file_names[abspath.in()];
+ }
+ } else if (lnav::filesystem::stat_file(path).isOk()) {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ attr_line_t("default syslog file is not readable -- ")
+ .append(lnav::roles::file(cwd))
+ .append(lnav::roles::file(path))));
+ retval = false;
+ }
+ }
+
+ return retval;
+}
+
+static void
+sigint(int sig)
+{
+ static size_t counter = 0;
+
+ lnav_data.ld_looping = false;
+ counter += 1;
+ if (counter >= 3) {
+ abort();
+ }
+}
+
+static void
+sigwinch(int sig)
+{
+ lnav_data.ld_winched = true;
+}
+
+static void
+sigchld(int sig)
+{
+ lnav_data.ld_child_terminated = true;
+}
+
+static void
+handle_rl_key(int ch)
+{
+ switch (ch) {
+ case KEY_PPAGE:
+ case KEY_NPAGE:
+ case KEY_CTRL_P:
+ handle_paging_key(ch);
+ break;
+
+ case KEY_CTRL_RBRACKET:
+ lnav_data.ld_rl_view->abort();
+ break;
+
+ default:
+ lnav_data.ld_rl_view->handle_key(ch);
+ break;
+ }
+}
+
+readline_context::command_map_t lnav_commands;
+
+static attr_line_t
+command_arg_help()
+{
+ return attr_line_t()
+ .append(
+ "command arguments must start with one of the following symbols "
+ "to denote the type of command:\n")
+ .append(" ")
+ .append(":"_symbol)
+ .append(" - ")
+ .append("an lnav command (e.g. :goto 42)\n")
+ .append(" ")
+ .append(";"_symbol)
+ .append(" - ")
+ .append("an SQL statement (e.g. ;SELECT * FROM syslog_log)\n")
+ .append(" ")
+ .append("|"_symbol)
+ .append(" - ")
+ .append("an lnav script (e.g. |rename-stdin foo)\n");
+}
+
+static void
+usage()
+{
+ attr_line_t ex1_term;
+
+ ex1_term.append(lnav::roles::ok("$"))
+ .append(" ")
+ .append(lnav::roles::file("lnav"))
+ .pad_to(40)
+ .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+
+ attr_line_t ex2_term;
+
+ ex2_term.append(lnav::roles::ok("$"))
+ .append(" ")
+ .append(lnav::roles::file("lnav"))
+ .append(" ")
+ .append(lnav::roles::file("/var/log"))
+ .pad_to(40)
+ .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+
+ attr_line_t ex3_term;
+
+ ex3_term.append(lnav::roles::ok("$"))
+ .append(" ")
+ .append(lnav::roles::file("make"))
+ .append(" 2>&1 | ")
+ .append(lnav::roles::file("lnav"))
+ .append(" ")
+ .append("-t"_symbol)
+ .pad_to(40)
+ .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+
+ attr_line_t usage_al;
+
+ usage_al.append("usage"_h1)
+ .append(": ")
+ .append(lnav::roles::file(lnav_data.ld_program_name))
+ .append(" [")
+ .append("options"_variable)
+ .append("] [")
+ .append("logfile1"_variable)
+ .append(" ")
+ .append("logfile2"_variable)
+ .append(" ")
+ .append("..."_variable)
+ .append("]\n")
+ .append(R"(
+A log file viewer for the terminal that indexes log messages to
+make it easier to navigate through files quickly.
+
+)")
+ .append("Key Bindings"_h2)
+ .append("\n")
+ .append(" ?"_symbol)
+ .append(" View/leave the online help text.\n")
+ .append(" q"_symbol)
+ .append(" Quit the program.\n")
+ .append("\n")
+ .append("Options"_h2)
+ .append("\n")
+ .append(" ")
+ .append("-h"_symbol)
+ .append(" ")
+ .append("Print this message, then exit.\n")
+ .append(" ")
+ .append("-H"_symbol)
+ .append(" ")
+ .append("Display the internal help text.\n")
+ .append("\n")
+ .append(" ")
+ .append("-I"_symbol)
+ .append(" ")
+ .append("dir"_variable)
+ .append(" ")
+ .append("An additional configuration directory.\n")
+ .append(" ")
+ .append("-W"_symbol)
+ .append(" ")
+ .append("Print warnings related to lnav's configuration.\n")
+ .append(" ")
+ .append("-u"_symbol)
+ .append(" ")
+ .append("Update formats installed from git repositories.\n")
+ .append(" ")
+ .append("-d"_symbol)
+ .append(" ")
+ .append("file"_variable)
+ .append(" ")
+ .append("Write debug messages to the given file.\n")
+ .append(" ")
+ .append("-V"_symbol)
+ .append(" ")
+ .append("Print version information.\n")
+ .append("\n")
+ .append(" ")
+ .append("-r"_symbol)
+ .append(" ")
+ .append(
+ "Recursively load files from the given directory hierarchies.\n")
+ .append(" ")
+ .append("-R"_symbol)
+ .append(" ")
+ .append("Load older rotated log files as well.\n")
+ .append(" ")
+ .append("-t"_symbol)
+ .append(" ")
+ .append(R"(Prepend timestamps to the lines of data being read in
+ from the standard input.
+)")
+ .append(" ")
+ .append("-w"_symbol)
+ .append(" ")
+ .append("file"_variable)
+ .append(" ")
+ .append("Write the contents of the standard input to this file.\n")
+ .append("\n")
+ .append(" ")
+ .append("-c"_symbol)
+ .append(" ")
+ .append("cmd"_variable)
+ .append(" ")
+ .append("Execute a command after the files have been loaded.\n")
+ .append(" ")
+ .append("-f"_symbol)
+ .append(" ")
+ .append("file"_variable)
+ .append(" ")
+ .append("Execute the commands in the given file.\n")
+ .append(" ")
+ .append("-n"_symbol)
+ .append(" ")
+ .append("Run without the curses UI. (headless mode)\n")
+ .append(" ")
+ .append("-N"_symbol)
+ .append(" ")
+ .append("Do not open the default syslog file if no files are given.\n")
+ .append(" ")
+ .append("-q"_symbol)
+ .append(" ")
+ .append(
+ R"(Do not print the log messages after executing all
+ of the commands.
+)")
+ .append("\n")
+ .append("Optional arguments"_h2)
+ .append("\n")
+ .append(" ")
+ .append("logfileN"_variable)
+ .append(R"( The log files, directories, or remote paths to view.
+ If a directory is given, all of the files in the
+ directory will be loaded.
+)")
+ .append("\n")
+ .append("Management-Mode Options"_h2)
+ .append("\n")
+ .append(" ")
+ .append("-i"_symbol)
+ .append(" ")
+ .append(R"(Install the given format files and exit. Pass 'extra'
+ to install the default set of third-party formats.
+)")
+ .append(" ")
+ .append("-m"_symbol)
+ .append(" ")
+ .append(R"(Switch to the management command-line mode. This mode
+ is used to work with lnav's configuration.
+)")
+ .append("\n")
+ .append("Examples"_h2)
+ .append("\n ")
+ .append("\u2022"_list_glyph)
+ .append(" To load and follow the syslog file:\n")
+ .append(" ")
+ .append(ex1_term)
+ .append("\n\n ")
+ .append("\u2022"_list_glyph)
+ .append(" To load all of the files in ")
+ .append(lnav::roles::file("/var/log"))
+ .append(":\n")
+ .append(" ")
+ .append(ex2_term)
+ .append("\n\n ")
+ .append("\u2022"_list_glyph)
+ .append(" To watch the output of ")
+ .append(lnav::roles::file("make"))
+ .append(" with timestamps prepended:\n")
+ .append(" ")
+ .append(ex3_term)
+ .append("\n\n")
+ .append("Paths"_h2)
+ .append("\n ")
+ .append("\u2022"_list_glyph)
+ .append(" Format files are read from:")
+ .append("\n \U0001F4C2 ")
+ .append(lnav::roles::file("/etc/lnav"))
+ .append("\n \U0001F4C2 ")
+ .append(lnav::roles::file(SYSCONFDIR "/lnav"))
+ .append("\n ")
+ .append("\u2022"_list_glyph)
+ .append(" Configuration, session, and format files are stored in:\n")
+ .append(" \U0001F4C2 ")
+ .append(lnav::roles::file(lnav::paths::dotlnav().string()))
+ .append("\n\n ")
+ .append("\u2022"_list_glyph)
+ .append(" Local copies of remote files and files extracted from\n")
+ .append(" archives are stored in:\n")
+ .append(" \U0001F4C2 ")
+ .append(lnav::roles::file(lnav::paths::workdir().string()))
+ .append("\n\n")
+ .append("Documentation"_h1)
+ .append(": https://docs.lnav.org\n")
+ .append("Contact"_h1)
+ .append("\n")
+ .append(" \U0001F4AC https://github.com/tstack/lnav/discussions\n")
+ .appendf(FMT_STRING(" \U0001F4EB {}\n"), PACKAGE_BUGREPORT)
+ .append("Version"_h1)
+ .appendf(FMT_STRING(": {}"), VCS_PACKAGE_STRING);
+
+ lnav::console::println(stderr, usage_al);
+}
+
+static void
+clear_last_user_mark(listview_curses* lv)
+{
+ textview_curses* tc = (textview_curses*) lv;
+ if (lnav_data.ld_select_start.find(tc) != lnav_data.ld_select_start.end()
+ && !tc->is_line_visible(vis_line_t(lnav_data.ld_last_user_mark[tc])))
+ {
+ lnav_data.ld_select_start.erase(tc);
+ lnav_data.ld_last_user_mark.erase(tc);
+ }
+}
+
+static void
+update_view_position(listview_curses* lv)
+{
+ lnav_data.ld_view_stack.top() | [lv](auto* top_lv) {
+ if (lv != top_lv) {
+ return;
+ }
+
+ lnav_data.ld_bottom_source.update_line_number(lv);
+ lnav_data.ld_bottom_source.update_percent(lv);
+ lnav_data.ld_bottom_source.update_marks(lv);
+ };
+}
+
+class lnav_behavior : public mouse_behavior {
+public:
+ void mouse_event(int button, bool release, int x, int y) override
+ {
+ textview_curses* tc = *(lnav_data.ld_view_stack.top());
+ struct mouse_event me;
+
+ switch (button & xterm_mouse::XT_BUTTON__MASK) {
+ case xterm_mouse::XT_BUTTON1:
+ me.me_button = mouse_button_t::BUTTON_LEFT;
+ break;
+ case xterm_mouse::XT_BUTTON2:
+ me.me_button = mouse_button_t::BUTTON_MIDDLE;
+ break;
+ case xterm_mouse::XT_BUTTON3:
+ me.me_button = mouse_button_t::BUTTON_RIGHT;
+ break;
+ case xterm_mouse::XT_SCROLL_UP:
+ me.me_button = mouse_button_t::BUTTON_SCROLL_UP;
+ break;
+ case xterm_mouse::XT_SCROLL_DOWN:
+ me.me_button = mouse_button_t::BUTTON_SCROLL_DOWN;
+ break;
+ }
+
+ if (button & xterm_mouse::XT_DRAG_FLAG) {
+ me.me_state = mouse_button_state_t::BUTTON_STATE_DRAGGED;
+ } else if (release) {
+ me.me_state = mouse_button_state_t::BUTTON_STATE_RELEASED;
+ } else {
+ me.me_state = mouse_button_state_t::BUTTON_STATE_PRESSED;
+ }
+
+ gettimeofday(&me.me_time, nullptr);
+ me.me_x = x - 1;
+ me.me_y = y - tc->get_y() - 1;
+
+ tc->handle_mouse(me);
+ }
+};
+
+static bool
+handle_config_ui_key(int ch)
+{
+ bool retval = false;
+
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::FILES:
+ retval = lnav_data.ld_files_view.handle_key(ch);
+ break;
+ case ln_mode_t::FILTER:
+ retval = lnav_data.ld_filter_view.handle_key(ch);
+ break;
+ default:
+ ensure(0);
+ }
+
+ if (retval) {
+ return retval;
+ }
+
+ nonstd::optional<ln_mode_t> new_mode;
+
+ lnav_data.ld_filter_help_status_source.fss_error.clear();
+ if (ch == 'F') {
+ new_mode = ln_mode_t::FILES;
+ } else if (ch == 'T') {
+ new_mode = ln_mode_t::FILTER;
+ } else if (ch == '\t' || ch == KEY_BTAB) {
+ if (lnav_data.ld_mode == ln_mode_t::FILES) {
+ new_mode = ln_mode_t::FILTER;
+ } else {
+ new_mode = ln_mode_t::FILES;
+ }
+ } else if (ch == 'q') {
+ new_mode = ln_mode_t::PAGING;
+ }
+
+ if (new_mode) {
+ if (new_mode.value() == ln_mode_t::FILES
+ || new_mode.value() == ln_mode_t::FILTER)
+ {
+ lnav_data.ld_last_config_mode = new_mode.value();
+ }
+ lnav_data.ld_mode = new_mode.value();
+ lnav_data.ld_files_view.reload_data();
+ lnav_data.ld_filter_view.reload_data();
+ lnav_data.ld_status[LNS_FILTER].set_needs_update();
+ } else {
+ return handle_paging_key(ch);
+ }
+
+ return true;
+}
+
+static bool
+handle_key(int ch)
+{
+ lnav_data.ld_input_state.push_back(ch);
+
+ switch (ch) {
+ case KEY_RESIZE:
+ break;
+ default: {
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::PAGING:
+ if (ch == KEY_ENTER || ch == '\n' || ch == '\r') {
+ breadcrumb_view.focus();
+ lnav_data.ld_mode = ln_mode_t::BREADCRUMBS;
+ return true;
+ }
+
+ return handle_paging_key(ch);
+
+ case ln_mode_t::BREADCRUMBS:
+ if (!breadcrumb_view.handle_key(ch)) {
+ lnav_data.ld_mode = ln_mode_t::PAGING;
+ lnav_data.ld_view_stack.set_needs_update();
+ return true;
+ }
+ return true;
+
+ case ln_mode_t::FILTER:
+ case ln_mode_t::FILES:
+ return handle_config_ui_key(ch);
+
+ case ln_mode_t::SPECTRO_DETAILS: {
+ if (ch == '\t' || ch == 'q') {
+ lnav_data.ld_mode = ln_mode_t::PAGING;
+ return true;
+ }
+ if (lnav_data.ld_spectro_details_view.handle_key(ch)) {
+ return true;
+ }
+ switch (ch) {
+ case 'n': {
+ execute_command(lnav_data.ld_exec_context,
+ "next-mark search");
+ return true;
+ }
+ case 'N': {
+ execute_command(lnav_data.ld_exec_context,
+ "prev-mark search");
+ return true;
+ }
+ case '/': {
+ execute_command(lnav_data.ld_exec_context,
+ "prompt search-spectro-details");
+ return true;
+ }
+ }
+ return false;
+ }
+
+ case ln_mode_t::COMMAND:
+ case ln_mode_t::SEARCH:
+ case ln_mode_t::SEARCH_FILTERS:
+ case ln_mode_t::SEARCH_FILES:
+ case ln_mode_t::SEARCH_SPECTRO_DETAILS:
+ case ln_mode_t::CAPTURE:
+ case ln_mode_t::SQL:
+ case ln_mode_t::EXEC:
+ case ln_mode_t::USER:
+ handle_rl_key(ch);
+ break;
+
+ case ln_mode_t::BUSY:
+ switch (ch) {
+ case KEY_CTRL_RBRACKET:
+ log_vtab_data.lvd_looping = false;
+ break;
+ }
+ break;
+
+ default:
+ require(0);
+ break;
+ }
+ }
+ }
+
+ return true;
+}
+
+static input_dispatcher::escape_match_t
+match_escape_seq(const char* keyseq)
+{
+ if (lnav_data.ld_mode != ln_mode_t::PAGING) {
+ return input_dispatcher::escape_match_t::NONE;
+ }
+
+ auto& km = lnav_config.lc_active_keymap;
+ auto iter = km.km_seq_to_cmd.find(keyseq);
+ if (iter != km.km_seq_to_cmd.end()) {
+ return input_dispatcher::escape_match_t::FULL;
+ }
+
+ auto lb = km.km_seq_to_cmd.lower_bound(keyseq);
+ if (lb == km.km_seq_to_cmd.end()) {
+ return input_dispatcher::escape_match_t::NONE;
+ }
+
+ auto ub = km.km_seq_to_cmd.upper_bound(keyseq);
+ auto longest = max_element(
+ lb, ub, [](auto l, auto r) { return l.first.size() < r.first.size(); });
+
+ if (strlen(keyseq) < longest->first.size()) {
+ return input_dispatcher::escape_match_t::PARTIAL;
+ }
+
+ return input_dispatcher::escape_match_t::NONE;
+}
+
+static void
+gather_pipers()
+{
+ for (auto iter = lnav_data.ld_pipers.begin();
+ iter != lnav_data.ld_pipers.end();)
+ {
+ pid_t child_pid = (*iter)->get_child_pid();
+ if ((*iter)->has_exited()) {
+ log_info("child piper has exited -- %d", child_pid);
+ iter = lnav_data.ld_pipers.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+
+ for (auto iter = lnav_data.ld_child_pollers.begin();
+ iter != lnav_data.ld_child_pollers.end();)
+ {
+ if (iter->poll(lnav_data.ld_active_files)
+ == child_poll_result_t::FINISHED)
+ {
+ iter = lnav_data.ld_child_pollers.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+}
+
+static void
+wait_for_pipers()
+{
+ for (;;) {
+ gather_pipers();
+ if (lnav_data.ld_pipers.empty() && lnav_data.ld_child_pollers.empty()) {
+ log_debug("all pipers finished");
+ break;
+ }
+ usleep(10000);
+ rebuild_indexes();
+
+ log_debug("%d pipers and %d children still active",
+ lnav_data.ld_pipers.size(),
+ lnav_data.ld_child_pollers.size());
+ }
+}
+
+struct refresh_status_bars {
+ refresh_status_bars(std::shared_ptr<top_status_source> top_source)
+ : rsb_top_source(std::move(top_source))
+ {
+ }
+
+ using injectable
+ = refresh_status_bars(std::shared_ptr<top_status_source> top_source);
+
+ void doit() const
+ {
+ struct timeval current_time {};
+ int ch;
+
+ gettimeofday(&current_time, nullptr);
+ while ((ch = getch()) != ERR) {
+ lnav_data.ld_user_message_source.clear();
+
+ alerter::singleton().new_input(ch);
+
+ lnav_data.ld_input_dispatcher.new_input(current_time, ch);
+
+ lnav_data.ld_view_stack.top() | [ch](auto tc) {
+ lnav_data.ld_key_repeat_history.update(ch, tc->get_top());
+ };
+
+ if (!lnav_data.ld_looping) {
+ // No reason to keep processing input after the
+ // user has quit. The view stack will also be
+ // empty, which will cause issues.
+ break;
+ }
+ }
+
+ this->rsb_top_source->update_time(current_time);
+ for (auto& sc : lnav_data.ld_status) {
+ sc.do_update();
+ }
+ lnav_data.ld_rl_view->do_update();
+ if (handle_winch()) {
+ layout_views();
+ lnav_data.ld_view_stack.do_update();
+ }
+ refresh();
+ }
+
+ std::shared_ptr<top_status_source> rsb_top_source;
+};
+
+static void
+looper()
+{
+ static auto* ps = injector::get<pollable_supervisor*>();
+ static auto* filter_source = injector::get<filter_sub_source*>();
+
+ try {
+ auto* sql_cmd_map = injector::get<readline_context::command_map_t*,
+ sql_cmd_map_tag>();
+ auto& ec = lnav_data.ld_exec_context;
+
+ readline_context command_context("cmd", &lnav_commands);
+
+ readline_context search_context("search", nullptr, false);
+ readline_context search_filters_context(
+ "search-filters", nullptr, false);
+ readline_context search_files_context("search-files", nullptr, false);
+ readline_context search_spectro_details_context(
+ "search-spectro-details", nullptr, false);
+ readline_context index_context("capture");
+ readline_context sql_context("sql", sql_cmd_map, false);
+ readline_context exec_context("exec");
+ readline_context user_context("user");
+ auto rlc = injector::get<std::shared_ptr<readline_curses>>();
+ sig_atomic_t overlay_counter = 0;
+ int lpc;
+
+ command_context.set_highlighter(readline_command_highlighter);
+ search_context.set_append_character(0).set_highlighter(
+ readline_regex_highlighter);
+ search_filters_context.set_append_character(0).set_highlighter(
+ readline_regex_highlighter);
+ search_files_context.set_append_character(0).set_highlighter(
+ readline_regex_highlighter);
+ search_spectro_details_context.set_append_character(0).set_highlighter(
+ readline_regex_highlighter);
+ sql_context.set_highlighter(readline_sqlite_highlighter)
+ .set_quote_chars("\"")
+ .with_readline_var((char**) &rl_completer_word_break_characters,
+ " \t\n(),");
+ exec_context.set_highlighter(readline_shlex_highlighter);
+
+ lnav_data.ld_log_source.lss_sorting_observer
+ = [](auto& lss, auto off, auto size) {
+ if (off == size) {
+ lnav_data.ld_bottom_source.update_loading(0, 0);
+ } else {
+ lnav_data.ld_bottom_source.update_loading(off, size);
+ }
+ do_observer_update(nullptr);
+ };
+
+ auto& sb = lnav_data.ld_scroll_broadcaster;
+ auto& vsb = lnav_data.ld_view_stack_broadcaster;
+
+ rlc->add_context(ln_mode_t::COMMAND, command_context);
+ rlc->add_context(ln_mode_t::SEARCH, search_context);
+ rlc->add_context(ln_mode_t::SEARCH_FILTERS, search_filters_context);
+ rlc->add_context(ln_mode_t::SEARCH_FILES, search_files_context);
+ rlc->add_context(ln_mode_t::SEARCH_SPECTRO_DETAILS,
+ search_spectro_details_context);
+ rlc->add_context(ln_mode_t::CAPTURE, index_context);
+ rlc->add_context(ln_mode_t::SQL, sql_context);
+ rlc->add_context(ln_mode_t::EXEC, exec_context);
+ rlc->add_context(ln_mode_t::USER, user_context);
+ rlc->set_save_history(!(lnav_data.ld_flags & LNF_SECURE_MODE));
+ rlc->start();
+
+ filter_source->fss_editor->start();
+
+ lnav_data.ld_rl_view = rlc.get();
+
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "viewname", lnav_view_strings);
+
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "zoomlevel", lnav_zoom_strings);
+
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "levelname", level_names);
+
+ auto echo_views_stmt_res = prepare_stmt(lnav_data.ld_db,
+#if SQLITE_VERSION_NUMBER < 3033000
+ R"(
+ UPDATE lnav_views_echo
+ SET top = (SELECT top FROM lnav_views WHERE lnav_views.name = lnav_views_echo.name),
+ left = (SELECT left FROM lnav_views WHERE lnav_views.name = lnav_views_echo.name),
+ height = (SELECT height FROM lnav_views WHERE lnav_views.name = lnav_views_echo.name),
+ inner_height = (SELECT inner_height FROM lnav_views WHERE lnav_views.name = lnav_views_echo.name),
+ top_time = (SELECT top_time FROM lnav_views WHERE lnav_views.name = lnav_views_echo.name),
+ search = (SELECT search FROM lnav_views WHERE lnav_views.name = lnav_views_echo.name)
+ WHERE EXISTS (SELECT * FROM lnav_views WHERE name = lnav_views_echo.name AND
+ (
+ lnav_views.top != lnav_views_echo.top OR
+ lnav_views.left != lnav_views_echo.left OR
+ lnav_views.height != lnav_views_echo.height OR
+ lnav_views.inner_height != lnav_views_echo.inner_height OR
+ lnav_views.top_time != lnav_views_echo.top_time OR
+ lnav_views.search != lnav_views_echo.search
+ ))
+ )"
+#else
+ R"(
+ UPDATE lnav_views_echo
+ SET top = orig.top,
+ left = orig.left,
+ height = orig.height,
+ inner_height = orig.inner_height,
+ top_time = orig.top_time,
+ search = orig.search
+ FROM (SELECT * FROM lnav_views) AS orig
+ WHERE orig.name = lnav_views_echo.name AND
+ (
+ orig.top != lnav_views_echo.top OR
+ orig.left != lnav_views_echo.left OR
+ orig.height != lnav_views_echo.height OR
+ orig.inner_height != lnav_views_echo.inner_height OR
+ orig.top_time != lnav_views_echo.top_time OR
+ orig.search != lnav_views_echo.search
+ )
+ )"
+#endif
+ );
+
+ if (echo_views_stmt_res.isErr()) {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ "unable to prepare UPDATE statement for lnav_views_echo "
+ "table")
+ .with_reason(echo_views_stmt_res.unwrapErr()));
+ return;
+ }
+ auto echo_views_stmt = echo_views_stmt_res.unwrap();
+
+ (void) signal(SIGINT, sigint);
+ (void) signal(SIGTERM, sigint);
+ (void) signal(SIGWINCH, sigwinch);
+ (void) signal(SIGCHLD, sigchld);
+
+ auto create_screen_res = screen_curses::create();
+
+ if (create_screen_res.isErr()) {
+ log_error("create screen failed with: %s",
+ create_screen_res.unwrapErr().c_str());
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error("unable to open TUI")
+ .with_reason(create_screen_res.unwrapErr()));
+ return;
+ }
+
+ auto sc = create_screen_res.unwrap();
+
+ auto_fd errpipe[2];
+ auto_fd::pipe(errpipe);
+
+ dup2(errpipe[1], STDERR_FILENO);
+ errpipe[1].reset();
+ log_pipe_err(errpipe[0]);
+ lnav_behavior lb;
+
+ ui_periodic_timer::singleton();
+
+ auto mouse_i = injector::get<xterm_mouse&>();
+
+ mouse_i.set_behavior(&lb);
+ mouse_i.set_enabled(check_experimental("mouse"));
+
+ lnav_data.ld_window = sc.get_window();
+ keypad(stdscr, TRUE);
+ (void) nonl();
+ (void) cbreak();
+ (void) noecho();
+ (void) nodelay(lnav_data.ld_window, 1);
+
+#ifdef VDSUSP
+ {
+ struct termios tio;
+
+ tcgetattr(STDIN_FILENO, &tio);
+ tio.c_cc[VDSUSP] = 0;
+ tcsetattr(STDIN_FILENO, TCSANOW, &tio);
+ }
+#endif
+
+ define_key("\033Od", KEY_BEG);
+ define_key("\033Oc", KEY_END);
+
+ view_colors& vc = view_colors::singleton();
+ view_colors::init(false);
+
+ auto ecb_guard
+ = lnav_data.ld_exec_context.add_error_callback([](const auto& um) {
+ auto al = um.to_attr_line().rtrim();
+
+ if (al.get_string().find('\n') == std::string::npos) {
+ if (lnav_data.ld_rl_view) {
+ lnav_data.ld_rl_view->set_attr_value(al);
+ }
+ } else {
+ lnav_data.ld_user_message_source.replace_with(al);
+ lnav_data.ld_user_message_view.reload_data();
+ lnav_data.ld_user_message_expiration
+ = std::chrono::steady_clock::now() + 20s;
+ }
+ });
+
+ {
+ setup_highlights(lnav_data.ld_views[LNV_LOG].get_highlights());
+ setup_highlights(lnav_data.ld_views[LNV_TEXT].get_highlights());
+ setup_highlights(lnav_data.ld_views[LNV_SCHEMA].get_highlights());
+ setup_highlights(lnav_data.ld_views[LNV_PRETTY].get_highlights());
+ setup_highlights(lnav_data.ld_preview_view.get_highlights());
+
+ for (const auto& format : log_format::get_root_formats()) {
+ for (auto& hl : format->lf_highlighters) {
+ if (hl.h_fg.empty() && hl.h_bg.empty()
+ && hl.h_attrs.empty())
+ {
+ hl.with_attrs(hl.h_attrs
+ | vc.attrs_for_ident(hl.h_name));
+ }
+
+ lnav_data.ld_views[LNV_LOG].get_highlights()[{
+ highlight_source_t::CONFIGURATION,
+ format->get_name().to_string() + "-" + hl.h_name}]
+ = hl;
+ }
+ }
+ }
+
+ execute_examples();
+
+ rlc->set_window(lnav_data.ld_window);
+ rlc->set_y(-1);
+ rlc->set_focus_action(rl_focus);
+ rlc->set_change_action(rl_change);
+ rlc->set_perform_action(rl_callback);
+ rlc->set_alt_perform_action(rl_alt_callback);
+ rlc->set_timeout_action(rl_search);
+ rlc->set_abort_action(lnav_rl_abort);
+ rlc->set_display_match_action(rl_display_matches);
+ rlc->set_display_next_action(rl_display_next);
+ rlc->set_blur_action(rl_blur);
+ rlc->set_completion_request_action(rl_completion_request);
+ rlc->set_alt_value(
+ HELP_MSG_2(e,
+ E,
+ "to move forward/backward through " ANSI_COLOR(
+ COLOR_RED) "error" ANSI_NORM " messages"));
+
+ (void) curs_set(0);
+
+ lnav_data.ld_view_stack.push_back(&lnav_data.ld_views[LNV_LOG]);
+
+ sb.push_back(clear_last_user_mark);
+ sb.push_back(update_view_position);
+ vsb.push_back(
+ bind_mem(&term_extra::update_title, injector::get<term_extra*>()));
+ vsb.push_back([](listview_curses* lv) {
+ auto* tc = dynamic_cast<textview_curses*>(lv);
+
+ tc->tc_state_event_handler(*tc);
+ });
+
+ vsb.push_back(sb);
+
+ breadcrumb_view.set_y(1);
+ breadcrumb_view.set_window(lnav_data.ld_window);
+ breadcrumb_view.set_line_source(lnav_crumb_source);
+ auto event_handler = [](auto&& tc) {
+ auto top_view = lnav_data.ld_view_stack.top();
+
+ if (top_view && *top_view == &tc) {
+ lnav_data.ld_bottom_source.update_search_term(tc);
+ }
+ };
+ for (lpc = 0; lpc < LNV__MAX; lpc++) {
+ lnav_data.ld_views[lpc].set_window(lnav_data.ld_window);
+ lnav_data.ld_views[lpc].set_y(2);
+ lnav_data.ld_views[lpc].set_height(
+ vis_line_t(-(rlc->get_height() + 3)));
+ lnav_data.ld_views[lpc].set_scroll_action(sb);
+ lnav_data.ld_views[lpc].set_search_action(update_hits);
+ lnav_data.ld_views[lpc].tc_cursor_role = role_t::VCR_CURSOR_LINE;
+ lnav_data.ld_views[lpc].tc_state_event_handler = event_handler;
+ }
+
+ lnav_data.ld_doc_view.set_window(lnav_data.ld_window);
+ lnav_data.ld_doc_view.set_show_scrollbar(false);
+
+ lnav_data.ld_example_view.set_window(lnav_data.ld_window);
+ lnav_data.ld_example_view.set_show_scrollbar(false);
+
+ lnav_data.ld_match_view.set_window(lnav_data.ld_window);
+
+ lnav_data.ld_preview_view.set_window(lnav_data.ld_window);
+ lnav_data.ld_preview_view.set_show_scrollbar(false);
+
+ lnav_data.ld_filter_view.set_selectable(true);
+ lnav_data.ld_filter_view.set_window(lnav_data.ld_window);
+ lnav_data.ld_filter_view.set_show_scrollbar(true);
+
+ lnav_data.ld_files_view.set_selectable(true);
+ lnav_data.ld_files_view.set_window(lnav_data.ld_window);
+ lnav_data.ld_files_view.set_show_scrollbar(true);
+ lnav_data.ld_files_view.get_disabled_highlights().insert(
+ highlight_source_t::THEME);
+ lnav_data.ld_files_view.set_overlay_source(&lnav_data.ld_files_overlay);
+
+ lnav_data.ld_user_message_view.set_window(lnav_data.ld_window);
+
+ lnav_data.ld_spectro_details_view.set_window(lnav_data.ld_window);
+ lnav_data.ld_spectro_details_view.set_show_scrollbar(true);
+ lnav_data.ld_spectro_details_view.set_height(5_vl);
+ lnav_data.ld_spectro_details_view.set_sub_source(
+ &lnav_data.ld_spectro_no_details_source);
+ lnav_data.ld_spectro_details_view.tc_state_event_handler
+ = event_handler;
+ lnav_data.ld_spectro_details_view.set_scroll_action(sb);
+ lnav_data.ld_spectro_no_details_source.replace_with(
+ attr_line_t().append(
+ lnav::roles::comment(" No details available")));
+ lnav_data.ld_spectro_source->ss_details_view
+ = &lnav_data.ld_spectro_details_view;
+ lnav_data.ld_spectro_source->ss_no_details_source
+ = &lnav_data.ld_spectro_no_details_source;
+ lnav_data.ld_spectro_source->ss_exec_context
+ = &lnav_data.ld_exec_context;
+
+ auto top_status_lifetime
+ = injector::bind<top_status_source>::to_scoped_singleton();
+
+ auto top_source = injector::get<std::shared_ptr<top_status_source>>();
+
+ lnav_data.ld_status[LNS_TOP].set_top(0);
+ lnav_data.ld_status[LNS_TOP].set_default_role(
+ role_t::VCR_INACTIVE_STATUS);
+ lnav_data.ld_status[LNS_TOP].set_data_source(top_source.get());
+ lnav_data.ld_status[LNS_BOTTOM].set_top(-(rlc->get_height() + 1));
+ for (auto& stat_bar : lnav_data.ld_status) {
+ stat_bar.set_window(lnav_data.ld_window);
+ }
+ lnav_data.ld_status[LNS_BOTTOM].set_data_source(
+ &lnav_data.ld_bottom_source);
+ lnav_data.ld_status[LNS_FILTER].set_data_source(
+ &lnav_data.ld_filter_status_source);
+ lnav_data.ld_status[LNS_FILTER_HELP].set_data_source(
+ &lnav_data.ld_filter_help_status_source);
+ lnav_data.ld_status[LNS_DOC].set_data_source(
+ &lnav_data.ld_doc_status_source);
+ lnav_data.ld_status[LNS_PREVIEW].set_data_source(
+ &lnav_data.ld_preview_status_source);
+ lnav_data.ld_spectro_status_source
+ = std::make_unique<spectro_status_source>();
+ lnav_data.ld_status[LNS_SPECTRO].set_data_source(
+ lnav_data.ld_spectro_status_source.get());
+
+ lnav_data.ld_match_view.set_show_bottom_border(true);
+ lnav_data.ld_user_message_view.set_show_bottom_border(true);
+
+ for (auto& sc : lnav_data.ld_status) {
+ sc.window_change();
+ }
+
+ auto session_path = lnav::paths::dotlnav() / "session";
+ execute_file(ec, session_path.string());
+
+ sb(*lnav_data.ld_view_stack.top());
+ vsb(*lnav_data.ld_view_stack.top());
+
+ lnav_data.ld_view_stack.vs_change_handler = [](textview_curses* tc) {
+ lnav_data.ld_view_stack_broadcaster(tc);
+ };
+
+ {
+ auto& id = lnav_data.ld_input_dispatcher;
+
+ id.id_escape_matcher = match_escape_seq;
+ id.id_escape_handler = handle_keyseq;
+ id.id_key_handler = handle_key;
+ id.id_mouse_handler
+ = std::bind(&xterm_mouse::handle_mouse, &mouse_i);
+ id.id_unhandled_handler = [](const char* keyseq) {
+ auto enc_len = lnav_config.lc_ui_keymap.size() * 2;
+ auto encoded_name = (char*) alloca(enc_len);
+
+ log_info("unbound keyseq: %s", keyseq);
+ json_ptr::encode(
+ encoded_name, enc_len, lnav_config.lc_ui_keymap.c_str());
+ // XXX we should have a hotkey for opening a prompt that is
+ // pre-filled with a suggestion that the user can complete.
+ // This quick-fix key could be used for other stuff as well
+ lnav_data.ld_rl_view->set_value(fmt::format(
+ ANSI_CSI ANSI_COLOR_PARAM(
+ COLOR_YELLOW) ";" ANSI_BOLD_PARAM ANSI_CHAR_ATTR
+ "Unrecognized key" ANSI_NORM
+ ", bind to a command using "
+ "\u2014 " ANSI_BOLD(
+ ":config") " /ui/keymap-defs/{}/{}/"
+ "command <cmd>",
+ encoded_name,
+ keyseq));
+ alerter::singleton().chime("unrecognized key");
+ };
+ }
+
+ auto refresher_lifetime
+ = injector::bind<refresh_status_bars>::to_scoped_singleton();
+
+ auto refresher = injector::get<std::shared_ptr<refresh_status_bars>>();
+
+ auto refresh_guard = lnav_data.ld_status_refresher.install(
+ [refresher]() { refresher->doit(); });
+
+ auto& timer = ui_periodic_timer::singleton();
+ struct timeval current_time;
+
+ static sig_atomic_t index_counter;
+
+ lnav_data.ld_mode = ln_mode_t::FILES;
+
+ timer.start_fade(index_counter, 1);
+
+ file_collection active_copy;
+ log_debug("rescan started %p", &active_copy);
+ active_copy.merge(lnav_data.ld_active_files);
+ active_copy.fc_progress = lnav_data.ld_active_files.fc_progress;
+ std::future<file_collection> rescan_future
+ = std::async(std::launch::async,
+ &file_collection::rescan_files,
+ std::move(active_copy),
+ false);
+ bool initial_rescan_completed = false;
+ int session_stage = 0;
+
+ // rlc.do_update();
+
+ auto next_rebuild_time = ui_clock::now();
+ auto next_status_update_time = next_rebuild_time;
+ auto next_rescan_time = next_rebuild_time;
+
+ while (lnav_data.ld_looping) {
+ auto loop_deadline
+ = ui_clock::now() + (session_stage == 0 ? 3s : 50ms);
+
+ std::vector<struct pollfd> pollfds;
+ size_t starting_view_stack_size = lnav_data.ld_view_stack.size();
+ size_t changes = 0;
+ int rc;
+
+ gettimeofday(&current_time, nullptr);
+
+ top_source->update_time(current_time);
+ lnav_data.ld_preview_view.set_needs_update();
+
+ layout_views();
+
+ auto scan_timeout = initial_rescan_completed ? 0s : 10ms;
+ if (rescan_future.valid()
+ && rescan_future.wait_for(scan_timeout)
+ == std::future_status::ready)
+ {
+ auto new_files = rescan_future.get();
+ if (!initial_rescan_completed && new_files.fc_file_names.empty()
+ && new_files.fc_files.empty()
+ && lnav_data.ld_active_files.fc_progress->readAccess()
+ ->sp_tailers.empty())
+ {
+ initial_rescan_completed = true;
+
+ log_debug("initial rescan rebuild");
+ changes += rebuild_indexes(loop_deadline);
+ load_session();
+ if (session_data.sd_save_time) {
+ std::string ago;
+
+ ago = humanize::time::point::from_tv(
+ {(time_t) session_data.sd_save_time, 0})
+ .as_time_ago();
+ auto um = lnav::console::user_message::ok(
+ attr_line_t("restored session from ")
+ .append(lnav::roles::number(ago))
+ .append("; press ")
+ .append("CTRL-R"_hotkey)
+ .append(" to reset session"));
+ lnav_data.ld_rl_view->set_attr_value(um.to_attr_line());
+ }
+
+ lnav_data.ld_session_loaded = true;
+ session_stage += 1;
+ loop_deadline = ui_clock::now();
+ log_debug("file count %d",
+ lnav_data.ld_active_files.fc_files.size())
+ }
+ update_active_files(new_files);
+ if (!initial_rescan_completed) {
+ auto& fview = lnav_data.ld_files_view;
+ auto height = fview.get_inner_height();
+
+ if (height > 0_vl) {
+ fview.set_selection(height - 1_vl);
+ }
+ }
+
+ active_copy.clear();
+ rescan_future = std::future<file_collection>{};
+ next_rescan_time = ui_clock::now() + 333ms;
+ }
+
+ if (!rescan_future.valid()
+ && (session_stage < 2 || ui_clock::now() >= next_rescan_time))
+ {
+ active_copy.clear();
+ active_copy.merge(lnav_data.ld_active_files);
+ active_copy.fc_progress = lnav_data.ld_active_files.fc_progress;
+ rescan_future = std::async(std::launch::async,
+ &file_collection::rescan_files,
+ std::move(active_copy),
+ false);
+ }
+
+ {
+ auto& mlooper = injector::get<main_looper&, services::main_t>();
+
+ mlooper.get_port().process_for(0s);
+ }
+
+ auto ui_now = ui_clock::now();
+ if (initial_rescan_completed) {
+ if (ui_now >= next_rebuild_time) {
+ auto text_file_count = lnav_data.ld_text_source.size();
+ changes += rebuild_indexes(loop_deadline);
+ if (!changes && ui_clock::now() < loop_deadline) {
+ next_rebuild_time = ui_clock::now() + 333ms;
+ }
+ if (changes && text_file_count
+ && lnav_data.ld_text_source.empty()
+ && lnav_data.ld_view_stack.top().value_or(nullptr)
+ == &lnav_data.ld_views[LNV_TEXT])
+ {
+ do {
+ lnav_data.ld_view_stack.pop_back();
+ } while (lnav_data.ld_view_stack.top().value_or(nullptr)
+ != &lnav_data.ld_views[LNV_LOG]);
+ }
+ }
+ } else {
+ lnav_data.ld_files_view.set_overlay_needs_update();
+ }
+
+ if (lnav_data.ld_mode == ln_mode_t::BREADCRUMBS
+ && breadcrumb_view.get_needs_update())
+ {
+ lnav_data.ld_view_stack.set_needs_update();
+ }
+ lnav_data.ld_view_stack.do_update();
+ lnav_data.ld_doc_view.do_update();
+ lnav_data.ld_example_view.do_update();
+ lnav_data.ld_match_view.do_update();
+ lnav_data.ld_preview_view.do_update();
+ lnav_data.ld_spectro_details_view.do_update();
+ lnav_data.ld_user_message_view.do_update();
+ if (ui_clock::now() >= next_status_update_time) {
+ echo_views_stmt.execute();
+ top_source->update_user_msg();
+ for (auto& sc : lnav_data.ld_status) {
+ sc.do_update();
+ }
+ next_status_update_time = ui_clock::now() + 100ms;
+ }
+ if (filter_source->fss_editing) {
+ filter_source->fss_match_view.set_needs_update();
+ }
+ breadcrumb_view.do_update();
+ // These updates need to be done last so their readline views can
+ // put the cursor in the right place.
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::FILTER:
+ case ln_mode_t::SEARCH_FILTERS:
+ lnav_data.ld_filter_view.set_needs_update();
+ lnav_data.ld_filter_view.do_update();
+ break;
+ case ln_mode_t::SEARCH_FILES:
+ case ln_mode_t::FILES:
+ lnav_data.ld_files_view.set_needs_update();
+ lnav_data.ld_files_view.do_update();
+ break;
+ default:
+ break;
+ }
+ if (lnav_data.ld_mode != ln_mode_t::FILTER
+ && lnav_data.ld_mode != ln_mode_t::FILES)
+ {
+ rlc->do_update();
+ }
+ refresh();
+
+ if (lnav_data.ld_session_loaded) {
+ // Only take input from the user after everything has loaded.
+ pollfds.push_back((struct pollfd){STDIN_FILENO, POLLIN, 0});
+ if (lnav_data.ld_initial_build) {
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::COMMAND:
+ case ln_mode_t::SEARCH:
+ case ln_mode_t::SEARCH_FILTERS:
+ case ln_mode_t::SEARCH_FILES:
+ case ln_mode_t::SEARCH_SPECTRO_DETAILS:
+ case ln_mode_t::SQL:
+ case ln_mode_t::EXEC:
+ case ln_mode_t::USER:
+ if (rlc->consume_ready_for_input()) {
+ // log_debug("waiting for readline input")
+ view_curses::awaiting_user_input();
+ }
+ break;
+ default:
+ // log_debug("waiting for paging input");
+ view_curses::awaiting_user_input();
+ break;
+ }
+ }
+ }
+
+ ps->update_poll_set(pollfds);
+ ui_now = ui_clock::now();
+ auto poll_to
+ = (!changes && ui_now < loop_deadline && session_stage >= 1)
+ ? std::chrono::duration_cast<std::chrono::milliseconds>(
+ loop_deadline - ui_now)
+ : 0ms;
+
+ if (initial_rescan_completed
+ && lnav_data.ld_input_dispatcher.in_escape() && poll_to > 15ms)
+ {
+ poll_to = 15ms;
+ }
+ // log_debug("poll %d %d", changes, poll_to.count());
+ rc = poll(&pollfds[0], pollfds.size(), poll_to.count());
+
+ gettimeofday(&current_time, nullptr);
+ lnav_data.ld_input_dispatcher.poll(current_time);
+
+ if (rc < 0) {
+ switch (errno) {
+ case 0:
+ case EINTR:
+ break;
+
+ default:
+ log_error("select %s", strerror(errno));
+ lnav_data.ld_looping = false;
+ break;
+ }
+ } else {
+ auto in_revents = pollfd_revents(pollfds, STDIN_FILENO);
+
+ if (in_revents & (POLLHUP | POLLNVAL)) {
+ log_info("stdin has been closed, exiting...");
+ lnav_data.ld_looping = false;
+ } else if (in_revents & POLLIN) {
+ int ch;
+
+ auto old_gen
+ = lnav_data.ld_active_files.fc_files_generation;
+ while ((ch = getch()) != ERR) {
+ lnav_data.ld_user_message_source.clear();
+
+ alerter::singleton().new_input(ch);
+
+ lnav_data.ld_input_dispatcher.new_input(current_time,
+ ch);
+
+ lnav_data.ld_view_stack.top() | [ch](auto tc) {
+ lnav_data.ld_key_repeat_history.update(
+ ch, tc->get_top());
+ };
+
+ if (!lnav_data.ld_looping) {
+ // No reason to keep processing input after the
+ // user has quit. The view stack will also be
+ // empty, which will cause issues.
+ break;
+ }
+ }
+
+ next_status_update_time = ui_clock::now();
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::PAGING:
+ case ln_mode_t::FILTER:
+ case ln_mode_t::FILES:
+ case ln_mode_t::SPECTRO_DETAILS:
+ case ln_mode_t::BUSY:
+ if (old_gen
+ == lnav_data.ld_active_files
+ .fc_files_generation)
+ {
+ next_rescan_time = next_status_update_time + 1s;
+ } else {
+ next_rescan_time = next_status_update_time;
+ }
+ break;
+ case ln_mode_t::BREADCRUMBS:
+ case ln_mode_t::COMMAND:
+ case ln_mode_t::SEARCH:
+ case ln_mode_t::SEARCH_FILTERS:
+ case ln_mode_t::SEARCH_FILES:
+ case ln_mode_t::SEARCH_SPECTRO_DETAILS:
+ case ln_mode_t::CAPTURE:
+ case ln_mode_t::SQL:
+ case ln_mode_t::EXEC:
+ case ln_mode_t::USER:
+ next_rescan_time = next_status_update_time + 1min;
+ break;
+ }
+ next_rebuild_time = next_rescan_time;
+ }
+
+ auto old_mode = lnav_data.ld_mode;
+ auto old_file_names_size
+ = lnav_data.ld_active_files.fc_file_names.size();
+
+ ps->check_poll_set(pollfds);
+ lnav_data.ld_view_stack.top() |
+ [](auto tc) { lnav_data.ld_bottom_source.update_hits(tc); };
+
+ if (lnav_data.ld_mode != old_mode) {
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::PAGING:
+ case ln_mode_t::FILTER:
+ case ln_mode_t::FILES:
+ next_rescan_time = next_status_update_time + 1s;
+ next_rebuild_time = next_rescan_time;
+ break;
+ default:
+ break;
+ }
+ }
+ if (old_file_names_size
+ != lnav_data.ld_active_files.fc_file_names.size())
+ {
+ next_rescan_time = ui_clock::now();
+ next_rebuild_time = next_rescan_time;
+ next_status_update_time = next_rescan_time;
+ }
+ }
+
+ if (timer.time_to_update(overlay_counter)) {
+ lnav_data.ld_view_stack.top() |
+ [](auto tc) { tc->set_overlay_needs_update(); };
+ }
+
+ if (initial_rescan_completed && session_stage < 2
+ && (!lnav_data.ld_initial_build
+ || timer.fade_diff(index_counter) == 0))
+ {
+ if (lnav_data.ld_mode == ln_mode_t::PAGING) {
+ timer.start_fade(index_counter, 1);
+ } else {
+ timer.start_fade(index_counter, 3);
+ }
+ // log_debug("initial build rebuild");
+ changes += rebuild_indexes(loop_deadline);
+ if (!lnav_data.ld_initial_build
+ && lnav_data.ld_log_source.text_line_count() == 0
+ && lnav_data.ld_text_source.text_line_count() > 0)
+ {
+ ensure_view(&lnav_data.ld_views[LNV_TEXT]);
+ lnav_data.ld_rl_view->set_alt_value(HELP_MSG_2(
+ f, F, "to switch to the next/previous file"));
+ }
+ if (lnav_data.ld_view_stack.top().value_or(nullptr)
+ == &lnav_data.ld_views[LNV_TEXT]
+ && lnav_data.ld_text_source.empty()
+ && lnav_data.ld_log_source.text_line_count() > 0)
+ {
+ textview_curses* tc_log = &lnav_data.ld_views[LNV_LOG];
+ lnav_data.ld_view_stack.pop_back();
+
+ lnav_data.ld_views[LNV_LOG].set_top(
+ tc_log->get_top_for_last_row());
+ }
+ if (!lnav_data.ld_initial_build
+ && lnav_data.ld_log_source.text_line_count() == 0
+ && !lnav_data.ld_active_files.fc_other_files.empty()
+ && std::any_of(
+ lnav_data.ld_active_files.fc_other_files.begin(),
+ lnav_data.ld_active_files.fc_other_files.end(),
+ [](const auto& pair) {
+ return pair.second.ofd_format
+ == file_format_t::SQLITE_DB;
+ }))
+ {
+ ensure_view(&lnav_data.ld_views[LNV_SCHEMA]);
+ }
+
+ if (!lnav_data.ld_initial_build && lnav_data.ld_show_help_view)
+ {
+ toggle_view(&lnav_data.ld_views[LNV_HELP]);
+ lnav_data.ld_initial_build = true;
+ }
+ if (!lnav_data.ld_initial_build
+ && lnav_data.ld_active_files.fc_file_names.empty())
+ {
+ lnav_data.ld_initial_build = true;
+ }
+ if (lnav_data.ld_log_source.text_line_count() > 0
+ || lnav_data.ld_text_source.text_line_count() > 0
+ || !lnav_data.ld_active_files.fc_other_files.empty())
+ {
+ lnav_data.ld_initial_build = true;
+ }
+
+ if (lnav_data.ld_initial_build) {
+ static bool ran_cleanup = false;
+ std::vector<std::pair<
+ Result<std::string, lnav::console::user_message>,
+ std::string>>
+ cmd_results;
+
+ execute_init_commands(ec, cmd_results);
+
+ if (!cmd_results.empty()) {
+ auto last_cmd_result = cmd_results.back();
+
+ if (last_cmd_result.first.isOk()) {
+ lnav_data.ld_rl_view->set_value(
+ last_cmd_result.first.unwrap());
+ } else {
+ ec.ec_error_callback_stack.back()(
+ last_cmd_result.first.unwrapErr());
+ }
+ lnav_data.ld_rl_view->set_alt_value(
+ last_cmd_result.second);
+ }
+
+ if (!ran_cleanup) {
+ line_buffer::cleanup_cache();
+ archive_manager::cleanup_cache();
+ tailer::cleanup_cache();
+ ran_cleanup = true;
+ }
+ }
+
+ if (session_stage == 1
+ && (lnav_data.ld_active_files.fc_file_names.empty()
+ || lnav_data.ld_log_source.text_line_count() > 0
+ || lnav_data.ld_text_source.text_line_count() > 0
+ || !lnav_data.ld_active_files.fc_other_files.empty()))
+ {
+ log_debug("restoring view states");
+ for (size_t view_index = 0; view_index < LNV__MAX;
+ view_index++)
+ {
+ const auto& vs
+ = session_data.sd_view_states[view_index];
+ auto& tview = lnav_data.ld_views[view_index];
+
+ if (vs.vs_top >= 0
+ && (view_index == LNV_LOG
+ || tview.get_top() == 0_vl))
+ {
+ log_info("restoring %s view top: %d",
+ lnav_view_strings[view_index],
+ vs.vs_top);
+ lnav_data.ld_views[view_index].set_top(
+ vis_line_t(vs.vs_top));
+ if (vs.vs_selection) {
+ lnav_data.ld_views[view_index].set_selection(
+ vis_line_t(vs.vs_selection.value()));
+ }
+ }
+ }
+ if (lnav_data.ld_mode == ln_mode_t::FILES) {
+ if (lnav_data.ld_active_files.fc_name_to_errors.empty())
+ {
+ log_info("switching to paging!");
+ lnav_data.ld_mode = ln_mode_t::PAGING;
+ lnav_data.ld_active_files.fc_files
+ | lnav::itertools::for_each(
+ &logfile::dump_stats);
+ } else {
+ lnav_data.ld_files_view.set_selection(0_vl);
+ }
+ }
+ session_stage += 1;
+ load_time_bookmarks();
+ }
+ }
+
+ handle_winch();
+
+ if (lnav_data.ld_child_terminated) {
+ lnav_data.ld_child_terminated = false;
+
+ log_info("checking for terminated child processes");
+ for (auto iter = lnav_data.ld_children.begin();
+ iter != lnav_data.ld_children.end();
+ ++iter)
+ {
+ int rc, child_stat;
+
+ rc = waitpid(*iter, &child_stat, WNOHANG);
+ if (rc == -1 || rc == 0) {
+ continue;
+ }
+
+ iter = lnav_data.ld_children.erase(iter);
+ }
+
+ gather_pipers();
+ }
+
+ if (lnav_data.ld_view_stack.empty()
+ || (lnav_data.ld_view_stack.size() == 1
+ && starting_view_stack_size == 2
+ && lnav_data.ld_active_files.fc_file_names.size()
+ == lnav_data.ld_text_source.size()))
+ {
+ lnav_data.ld_looping = false;
+ }
+ }
+ } catch (readline_curses::error& e) {
+ log_error("error: %s", strerror(e.e_err));
+ }
+}
+
+void
+wait_for_children()
+{
+ std::vector<struct pollfd> pollfds;
+ struct timeval to = {0, 333000};
+ static auto* ps = injector::get<pollable_supervisor*>();
+
+ do {
+ pollfds.clear();
+
+ auto update_res = ps->update_poll_set(pollfds);
+
+ if (update_res.ur_background == 0) {
+ break;
+ }
+
+ int rc = poll(&pollfds[0], pollfds.size(), to.tv_usec / 1000);
+
+ if (rc < 0) {
+ switch (errno) {
+ case 0:
+ case EINTR:
+ break;
+ default:
+ return;
+ }
+ }
+
+ ps->check_poll_set(pollfds);
+ lnav_data.ld_view_stack.top() |
+ [](auto tc) { lnav_data.ld_bottom_source.update_hits(tc); };
+ } while (lnav_data.ld_looping);
+}
+
+struct mode_flags_t {
+ bool mf_check_configs{false};
+ bool mf_install{false};
+ bool mf_update_formats{false};
+ bool mf_no_default{false};
+ bool mf_print_warnings{false};
+};
+
+static int
+print_user_msgs(std::vector<lnav::console::user_message> error_list,
+ mode_flags_t mf)
+{
+ size_t warning_count = 0;
+ int retval = EXIT_SUCCESS;
+
+ for (auto& iter : error_list) {
+ FILE* out_file;
+
+ switch (iter.um_level) {
+ case lnav::console::user_message::level::raw:
+ case lnav::console::user_message::level::ok:
+ out_file = stdout;
+ break;
+ case lnav::console::user_message::level::warning:
+ warning_count += 1;
+ if (!mf.mf_print_warnings) {
+ continue;
+ }
+ out_file = stderr;
+ break;
+ default:
+ out_file = stderr;
+ break;
+ }
+
+ lnav::console::print(out_file, iter);
+ if (iter.um_level == lnav::console::user_message::level::error) {
+ retval = EXIT_FAILURE;
+ }
+ }
+
+ if (warning_count > 0 && !mf.mf_print_warnings
+ && !(lnav_data.ld_flags & LNF_HEADLESS)
+ && (std::chrono::system_clock::now() - lnav_data.ld_last_dot_lnav_time
+ > 24h))
+ {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::warning(
+ attr_line_t()
+ .append(lnav::roles::number(fmt::to_string(warning_count)))
+ .append(" issues were detected when checking lnav's "
+ "configuration"))
+ .with_help(
+ attr_line_t("pass ")
+ .append(lnav::roles::symbol("-W"))
+ .append(" on the command line to display the issues\n")
+ .append("(this message will only be displayed once a "
+ "day)")));
+ }
+
+ return retval;
+}
+
+enum class verbosity_t : int {
+ quiet,
+ standard,
+ verbose,
+};
+
+struct stdin_options_t {
+ ghc::filesystem::path so_out;
+ bool so_timestamp{false};
+ auto_fd so_out_fd;
+};
+
+int
+main(int argc, char* argv[])
+{
+ std::vector<lnav::console::user_message> config_errors;
+ std::vector<lnav::console::user_message> loader_errors;
+ exec_context& ec = lnav_data.ld_exec_context;
+ int retval = EXIT_SUCCESS;
+
+ std::shared_ptr<piper_proc> stdin_reader;
+ stdin_options_t stdin_opts;
+ bool exec_stdin = false, load_stdin = false, stdin_captured = false;
+ mode_flags_t mode_flags;
+ const char* LANG = getenv("LANG");
+ ghc::filesystem::path stdin_tmp_path;
+ verbosity_t verbosity = verbosity_t::standard;
+
+ if (LANG == nullptr || strcmp(LANG, "C") == 0) {
+ setenv("LANG", "en_US.UTF-8", 1);
+ }
+
+ (void) signal(SIGPIPE, SIG_IGN);
+ setlocale(LC_ALL, "");
+ try {
+ std::locale::global(std::locale(""));
+ } catch (const std::runtime_error& e) {
+ log_error("unable to set locale to ''");
+ }
+ umask(027);
+
+ /* Disable Lnav from being able to execute external commands if
+ * "LNAVSECURE" environment variable is set by the user.
+ */
+ if (getenv("LNAVSECURE") != nullptr) {
+ lnav_data.ld_flags |= LNF_SECURE_MODE;
+ }
+
+ lnav_data.ld_exec_context.ec_sql_callback = sql_callback;
+ lnav_data.ld_exec_context.ec_pipe_callback = pipe_callback;
+
+ lnav_data.ld_program_name = argv[0];
+ add_ansi_vars(ec.ec_global_vars);
+
+ rl_readline_name = "lnav";
+ lnav_data.ld_db_key_names = DEFAULT_DB_KEY_NAMES;
+
+ stable_sort(lnav_data.ld_db_key_names.begin(),
+ lnav_data.ld_db_key_names.end());
+
+ auto dot_lnav_path = lnav::paths::dotlnav();
+ std::error_code last_write_ec;
+ lnav_data.ld_last_dot_lnav_time
+ = ghc::filesystem::last_write_time(dot_lnav_path, last_write_ec);
+
+ ensure_dotlnav();
+
+ log_install_handlers();
+ sql_install_logger();
+
+ if (sqlite3_open(":memory:", lnav_data.ld_db.out()) != SQLITE_OK) {
+ fprintf(stderr, "error: unable to create sqlite memory database\n");
+ exit(EXIT_FAILURE);
+ }
+
+ {
+ int register_collation_functions(sqlite3 * db);
+
+ register_sqlite_funcs(lnav_data.ld_db.in(), sqlite_registration_funcs);
+ register_collation_functions(lnav_data.ld_db.in());
+ }
+
+ register_environ_vtab(lnav_data.ld_db.in());
+ register_static_file_vtab(lnav_data.ld_db.in());
+ {
+ static auto vtab_modules
+ = injector::get<std::vector<std::shared_ptr<vtab_module_base>>>();
+
+ for (const auto& mod : vtab_modules) {
+ mod->create(lnav_data.ld_db.in());
+ }
+ }
+
+ register_views_vtab(lnav_data.ld_db.in());
+ register_regexp_vtab(lnav_data.ld_db.in());
+ register_xpath_vtab(lnav_data.ld_db.in());
+ register_fstat_vtab(lnav_data.ld_db.in());
+ lnav::events::register_events_tab(lnav_data.ld_db.in());
+
+ auto _vtab_cleanup = finally([] {
+ static const char* VIRT_TABLES = R"(
+SELECT tbl_name FROM sqlite_master WHERE sql LIKE 'CREATE VIRTUAL TABLE%'
+)";
+
+ for (auto& lf : lnav_data.ld_active_files.fc_files) {
+ lf->close();
+ }
+ rebuild_indexes(ui_clock::now());
+
+ lnav_data.ld_vtab_manager = nullptr;
+
+ std::vector<std::string> tables_to_drop;
+ {
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+ bool done = false;
+
+ sqlite3_prepare_v2(
+ lnav_data.ld_db.in(), VIRT_TABLES, -1, stmt.out(), nullptr);
+ do {
+ auto ret = sqlite3_step(stmt.in());
+
+ switch (ret) {
+ case SQLITE_OK:
+ case SQLITE_DONE:
+ done = true;
+ break;
+ case SQLITE_ROW:
+ tables_to_drop.emplace_back(fmt::format(
+ FMT_STRING("DROP TABLE {}"),
+ reinterpret_cast<const char*>(
+ sqlite3_column_text(stmt.in(), 0))));
+ break;
+ }
+ } while (!done);
+ }
+
+ // XXX
+ lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
+ lnav_data.ld_log_source.set_sql_filter("", nullptr);
+ lnav_data.ld_log_source.set_sql_marker("", nullptr);
+ lnav_config_listener::unload_all();
+
+ {
+ sqlite3_stmt* stmt_iter = nullptr;
+
+ do {
+ stmt_iter = sqlite3_next_stmt(lnav_data.ld_db.in(), stmt_iter);
+ if (stmt_iter != nullptr) {
+ const auto* stmt_sql = sqlite3_sql(stmt_iter);
+
+ log_warning("unfinalized SQL statement: %s", stmt_sql);
+ ensure(false);
+ }
+ } while (stmt_iter != nullptr);
+ }
+
+ for (auto& drop_stmt : tables_to_drop) {
+ sqlite3_exec(lnav_data.ld_db.in(),
+ drop_stmt.c_str(),
+ nullptr,
+ nullptr,
+ nullptr);
+ }
+#if defined(HAVE_SQLITE3_DROP_MODULES)
+ sqlite3_drop_modules(lnav_data.ld_db.in(), nullptr);
+#endif
+
+ lnav_data.ld_db.reset();
+ });
+
+#ifdef HAVE_LIBCURL
+ curl_global_init(CURL_GLOBAL_DEFAULT);
+#endif
+
+ static const std::string DEFAULT_DEBUG_LOG = "/dev/null";
+
+ lnav_data.ld_debug_log_name = DEFAULT_DEBUG_LOG;
+
+ std::vector<std::string> file_args;
+ std::vector<lnav::console::user_message> arg_errors;
+
+ CLI::App app{"The Logfile Navigator"};
+
+ app.add_option("-d",
+ lnav_data.ld_debug_log_name,
+ "Write debug messages to the given file.")
+ ->type_name("FILE");
+ app.add_flag("-q{0},-v{2}", verbosity, "Control the verbosity");
+ app.set_version_flag("-V,--version");
+ app.footer(fmt::format(FMT_STRING("Version: {}"), VCS_PACKAGE_STRING));
+
+ std::shared_ptr<lnav::management::operations> mmode_ops;
+
+ if (argc < 2 || strcmp(argv[1], "-m") != 0) {
+ app.add_flag("-H", lnav_data.ld_show_help_view, "show help");
+ app.add_option("-I", lnav_data.ld_config_paths, "include paths")
+ ->check(CLI::ExistingDirectory)
+ ->check([&arg_errors](std::string inc_path) -> std::string {
+ if (access(inc_path.c_str(), X_OK) != 0) {
+ arg_errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("invalid configuration directory: ")
+ .append(lnav::roles::file(inc_path)))
+ .with_errno_reason());
+ return "unreadable";
+ }
+
+ return std::string();
+ })
+ ->allow_extra_args(false);
+ app.add_flag("-C", mode_flags.mf_check_configs, "check");
+ auto* install_flag
+ = app.add_flag("-i", mode_flags.mf_install, "install");
+ app.add_flag("-u", mode_flags.mf_update_formats, "update");
+ auto* write_flag = app.add_option("-w", stdin_opts.so_out, "write");
+ auto* ts_flag
+ = app.add_flag("-t", stdin_opts.so_timestamp, "timestamp");
+ auto* no_default_flag
+ = app.add_flag("-N", mode_flags.mf_no_default, "no def");
+ auto* rotated_flag = app.add_flag(
+ "-R", lnav_data.ld_active_files.fc_rotated, "rotated");
+ auto* recurse_flag = app.add_flag(
+ "-r", lnav_data.ld_active_files.fc_recursive, "recurse");
+ app.add_flag("-W", mode_flags.mf_print_warnings);
+ auto* headless_flag = app.add_flag(
+ "-n",
+ [](size_t count) { lnav_data.ld_flags |= LNF_HEADLESS; },
+ "headless");
+ auto* file_opt = app.add_option("file", file_args, "files");
+
+ auto wait_cb = [](size_t count) {
+ char b;
+ if (isatty(STDIN_FILENO) && read(STDIN_FILENO, &b, 1) == -1) {
+ perror("Read key from STDIN");
+ }
+ };
+ app.add_flag("-S", wait_cb);
+
+ auto cmd_appender
+ = [](std::string cmd) { lnav_data.ld_commands.emplace_back(cmd); };
+ auto cmd_validator = [&arg_errors](std::string cmd) -> std::string {
+ static const auto ARG_SRC
+ = intern_string::lookup("command-line argument");
+
+ if (cmd.empty()) {
+ return "empty commands are not allowed";
+ }
+
+ switch (cmd[0]) {
+ case ':':
+ case '/':
+ case ';':
+ case '|':
+ break;
+ default:
+ cmd.push_back(' ');
+ arg_errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("invalid value for ")
+ .append_quoted("-c"_symbol)
+ .append(" option"))
+ .with_snippet(lnav::console::snippet::from(
+ ARG_SRC,
+ attr_line_t()
+ .append(" -c "_quoted_code)
+ .append(lnav::roles::quoted_code(cmd))
+ .append("\n")
+ .append(4, ' ')
+ .append(lnav::roles::error(
+ "^ command type prefix "
+ "is missing"))))
+ .with_help(command_arg_help()));
+ return "invalid prefix";
+ }
+ return std::string();
+ };
+ auto* cmd_opt = app.add_option("-c")
+ ->check(cmd_validator)
+ ->each(cmd_appender)
+ ->allow_extra_args(false)
+ ->trigger_on_parse(true);
+
+ auto file_appender = [](std::string file_path) {
+ lnav_data.ld_commands.emplace_back(
+ fmt::format(FMT_STRING("|{}"), file_path));
+ };
+ auto* exec_file_opt = app.add_option("-f")
+ ->trigger_on_parse(true)
+ ->allow_extra_args(false)
+ ->each(file_appender);
+
+ install_flag->needs(file_opt);
+ install_flag->excludes(write_flag,
+ ts_flag,
+ no_default_flag,
+ rotated_flag,
+ recurse_flag,
+ headless_flag,
+ cmd_opt,
+ exec_file_opt);
+ }
+
+ auto is_mmode = argc >= 2 && strcmp(argv[1], "-m") == 0;
+ try {
+ if (is_mmode) {
+ mmode_ops = lnav::management::describe_cli(app, argc, argv);
+ } else {
+ app.parse(argc, argv);
+ }
+ } catch (const CLI::CallForHelp& e) {
+ if (is_mmode) {
+ fmt::print("{}\n", app.help());
+ } else {
+ usage();
+ }
+ return EXIT_SUCCESS;
+ } catch (const CLI::CallForVersion& e) {
+ fmt::print("{}\n", VCS_PACKAGE_STRING);
+ return EXIT_SUCCESS;
+ } catch (const CLI::ParseError& e) {
+ if (!arg_errors.empty()) {
+ print_user_msgs(arg_errors, mode_flags);
+ return e.get_exit_code();
+ }
+
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error("invalid command-line arguments")
+ .with_reason(e.what()));
+ return e.get_exit_code();
+ }
+
+ lnav_data.ld_config_paths.insert(lnav_data.ld_config_paths.begin(),
+ lnav::paths::dotlnav());
+ lnav_data.ld_config_paths.insert(lnav_data.ld_config_paths.begin(),
+ SYSCONFDIR "/lnav");
+ lnav_data.ld_config_paths.insert(lnav_data.ld_config_paths.begin(),
+ "/etc/lnav");
+
+ if (lnav_data.ld_debug_log_name != DEFAULT_DEBUG_LOG) {
+ lnav_log_level = lnav_log_level_t::TRACE;
+ }
+
+ lnav_log_file = make_optional_from_nullable(
+ fopen(lnav_data.ld_debug_log_name.c_str(), "a"));
+ log_info("lnav started");
+
+ {
+ static auto builtin_formats
+ = injector::get<std::vector<std::shared_ptr<log_format>>>();
+ auto& root_formats = log_format::get_root_formats();
+
+ log_format::get_root_formats().insert(root_formats.begin(),
+ builtin_formats.begin(),
+ builtin_formats.end());
+ builtin_formats.clear();
+ }
+
+ load_config(lnav_data.ld_config_paths, config_errors);
+ if (!config_errors.empty()) {
+ if (print_user_msgs(config_errors, mode_flags) != EXIT_SUCCESS) {
+ return EXIT_FAILURE;
+ }
+ }
+ add_global_vars(ec);
+
+ if (mode_flags.mf_update_formats) {
+ if (!update_installs_from_git()) {
+ return EXIT_FAILURE;
+ }
+ return EXIT_SUCCESS;
+ }
+
+ if (mode_flags.mf_install) {
+ auto formats_installed_path
+ = lnav::paths::dotlnav() / "formats/installed";
+ auto configs_installed_path
+ = lnav::paths::dotlnav() / "configs/installed";
+
+ if (argc == 0) {
+ const auto install_reason
+ = attr_line_t("the ")
+ .append("-i"_symbol)
+ .append(
+ " option expects one or more log format definition "
+ "files to install in your lnav configuration "
+ "directory");
+ const auto install_help
+ = attr_line_t(
+ "log format definitions are JSON files that tell lnav "
+ "how to understand log files\n")
+ .append(
+ "See: https://docs.lnav.org/en/latest/formats.html");
+
+ lnav::console::print(stderr,
+ lnav::console::user_message::error(
+ "missing format files to install")
+ .with_reason(install_reason)
+ .with_help(install_help));
+ return EXIT_FAILURE;
+ }
+
+ for (auto& file_path : file_args) {
+ if (endswith(file_path, ".git")) {
+ if (!install_from_git(file_path)) {
+ return EXIT_FAILURE;
+ }
+ continue;
+ }
+
+ if (endswith(file_path, ".sql")) {
+ auto sql_path = ghc::filesystem::path(file_path);
+ auto read_res = lnav::filesystem::read_file(sql_path);
+ if (read_res.isErr()) {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ attr_line_t("unable to read SQL file: ")
+ .append(lnav::roles::file(file_path)))
+ .with_reason(read_res.unwrapErr()));
+ return EXIT_FAILURE;
+ }
+
+ auto dst_path = formats_installed_path / sql_path.filename();
+ auto write_res
+ = lnav::filesystem::write_file(dst_path, read_res.unwrap());
+ if (write_res.isErr()) {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ attr_line_t("unable to write SQL file: ")
+ .append(lnav::roles::file(file_path)))
+ .with_reason(write_res.unwrapErr()));
+ return EXIT_FAILURE;
+ }
+
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::ok(
+ attr_line_t("installed -- ")
+ .append(lnav::roles::file(dst_path))));
+ continue;
+ }
+
+ if (file_path == "extra") {
+ install_extra_formats();
+ continue;
+ }
+
+ auto file_type_result = detect_config_file_type(file_path);
+ if (file_type_result.isErr()) {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ attr_line_t("unable to open configuration file: ")
+ .append(lnav::roles::file(file_path)))
+ .with_reason(file_type_result.unwrapErr()));
+ return EXIT_FAILURE;
+ }
+ auto file_type = file_type_result.unwrap();
+
+ auto src_path = ghc::filesystem::path(file_path);
+ ghc::filesystem::path dst_name;
+ if (file_type == config_file_type::CONFIG) {
+ dst_name = src_path.filename();
+ } else {
+ auto format_list = load_format_file(src_path, loader_errors);
+
+ if (!loader_errors.empty()) {
+ if (print_user_msgs(loader_errors, mode_flags)
+ != EXIT_SUCCESS)
+ {
+ return EXIT_FAILURE;
+ }
+ }
+ if (format_list.empty()) {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ attr_line_t("invalid format file: ")
+ .append(lnav::roles::file(src_path.string())))
+ .with_reason("there must be at least one format "
+ "definition in the file"));
+ return EXIT_FAILURE;
+ }
+
+ dst_name = format_list[0].to_string() + ".json";
+ }
+ auto dst_path = (file_type == config_file_type::CONFIG
+ ? configs_installed_path
+ : formats_installed_path)
+ / dst_name;
+ auto_fd in_fd, out_fd;
+
+ if ((in_fd = open(file_path.c_str(), O_RDONLY)) == -1) {
+ perror("unable to open file to install");
+ } else if ((out_fd = lnav::filesystem::openp(
+ dst_path, O_WRONLY | O_CREAT | O_TRUNC, 0644))
+ == -1)
+ {
+ fprintf(stderr,
+ "error: unable to open destination: %s -- %s\n",
+ dst_path.c_str(),
+ strerror(errno));
+ } else {
+ char buffer[2048];
+ ssize_t rc;
+
+ while ((rc = read(in_fd, buffer, sizeof(buffer))) > 0) {
+ ssize_t remaining = rc, written;
+
+ while (remaining > 0) {
+ written = write(out_fd, buffer, rc);
+ if (written == -1) {
+ fprintf(stderr,
+ "error: unable to install file -- %s\n",
+ strerror(errno));
+ exit(EXIT_FAILURE);
+ }
+
+ remaining -= written;
+ }
+ }
+
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::ok(
+ attr_line_t("installed -- ")
+ .append(lnav::roles::file(dst_path))));
+ }
+ }
+ return EXIT_SUCCESS;
+ }
+
+ if (lnav_data.ld_flags & LNF_SECURE_MODE) {
+ if ((sqlite3_set_authorizer(
+ lnav_data.ld_db.in(), sqlite_authorizer, nullptr))
+ != SQLITE_OK)
+ {
+ fprintf(stderr, "error: unable to attach sqlite authorizer\n");
+ exit(EXIT_FAILURE);
+ }
+ }
+
+ /* If we statically linked against an ncurses library that had a non-
+ * standard path to the terminfo database, we need to set this variable
+ * so that it will try the default path.
+ */
+ setenv("TERMINFO_DIRS",
+ "/usr/share/terminfo:/lib/terminfo:/usr/share/lib/terminfo",
+ 0);
+
+ auto* filter_source = injector::get<filter_sub_source*>();
+ lnav_data.ld_vtab_manager = std::make_unique<log_vtab_manager>(
+ lnav_data.ld_db, lnav_data.ld_views[LNV_LOG], lnav_data.ld_log_source);
+
+ lnav_data.ld_log_source.set_exec_context(&lnav_data.ld_exec_context);
+ lnav_data.ld_views[LNV_HELP]
+ .set_sub_source(&lnav_data.ld_help_source)
+ .set_word_wrap(false);
+ auto log_fos = new field_overlay_source(lnav_data.ld_log_source,
+ lnav_data.ld_text_source);
+ if (lnav_data.ld_flags & LNF_HEADLESS) {
+ log_fos->fos_show_status = false;
+ }
+ log_fos->fos_contexts.emplace("", false, true);
+ lnav_data.ld_views[LNV_LOG]
+ .set_sub_source(&lnav_data.ld_log_source)
+ .set_delegate(std::make_shared<action_delegate>(
+ lnav_data.ld_log_source,
+ [](auto child_pid) { lnav_data.ld_children.push_back(child_pid); },
+ [](const auto& desc, auto pp) {
+ lnav_data.ld_pipers.push_back(pp);
+ lnav_data.ld_active_files.fc_file_names[desc].with_fd(
+ pp->get_fd());
+ lnav_data.ld_files_to_front.template emplace_back(desc, 0_vl);
+ }))
+ .add_input_delegate(lnav_data.ld_log_source)
+ .set_tail_space(2_vl)
+ .set_overlay_source(log_fos);
+ auto sel_reload_delegate = [](textview_curses& tc) {
+ if (lnav_config.lc_ui_movement.mode == config_movement_mode::CURSOR) {
+ tc.set_selectable(true);
+ }
+ };
+ lnav_data.ld_views[LNV_LOG].set_reload_config_delegate(sel_reload_delegate);
+ lnav_data.ld_views[LNV_PRETTY].set_reload_config_delegate(
+ sel_reload_delegate);
+ lnav_data.ld_views[LNV_TEXT].set_sub_source(&lnav_data.ld_text_source);
+ lnav_data.ld_views[LNV_TEXT].set_reload_config_delegate(
+ sel_reload_delegate);
+ lnav_data.ld_views[LNV_HISTOGRAM]
+ .set_reload_config_delegate(sel_reload_delegate)
+ .set_sub_source(&lnav_data.ld_hist_source2);
+ lnav_data.ld_views[LNV_DB].set_sub_source(&lnav_data.ld_db_row_source);
+ lnav_data.ld_db_overlay.dos_labels = &lnav_data.ld_db_row_source;
+ lnav_data.ld_views[LNV_DB]
+ .set_reload_config_delegate(sel_reload_delegate)
+ .set_overlay_source(&lnav_data.ld_db_overlay);
+ lnav_data.ld_spectro_source = std::make_unique<spectrogram_source>();
+ lnav_data.ld_views[LNV_SPECTRO]
+ .set_reload_config_delegate(sel_reload_delegate)
+ .set_sub_source(lnav_data.ld_spectro_source.get())
+ .set_overlay_source(lnav_data.ld_spectro_source.get())
+ .add_input_delegate(*lnav_data.ld_spectro_source)
+ .set_tail_space(4_vl);
+ lnav_data.ld_views[LNV_SPECTRO].set_selectable(true);
+
+ lnav_data.ld_doc_view.set_sub_source(&lnav_data.ld_doc_source);
+ lnav_data.ld_example_view.set_sub_source(&lnav_data.ld_example_source);
+ lnav_data.ld_match_view.set_sub_source(&lnav_data.ld_match_source);
+ lnav_data.ld_preview_view.set_sub_source(&lnav_data.ld_preview_source);
+ lnav_data.ld_filter_view.set_sub_source(filter_source)
+ .add_input_delegate(*filter_source)
+ .add_child_view(&filter_source->fss_match_view)
+ .add_child_view(filter_source->fss_editor.get());
+ lnav_data.ld_files_view.set_sub_source(&lnav_data.ld_files_source)
+ .add_input_delegate(lnav_data.ld_files_source);
+ lnav_data.ld_user_message_view.set_sub_source(
+ &lnav_data.ld_user_message_source);
+
+ for (int lpc = 0; lpc < LNV__MAX; lpc++) {
+ lnav_data.ld_views[lpc].set_gutter_source(new log_gutter_source());
+ }
+
+ {
+ hist_source2& hs = lnav_data.ld_hist_source2;
+
+ lnav_data.ld_log_source.set_index_delegate(new hist_index_delegate(
+ lnav_data.ld_hist_source2, lnav_data.ld_views[LNV_HISTOGRAM]));
+ hs.init();
+ lnav_data.ld_zoom_level = 3;
+ hs.set_time_slice(ZOOM_LEVELS[lnav_data.ld_zoom_level]);
+ }
+
+ for (int lpc = 0; lpc < LNV__MAX; lpc++) {
+ lnav_data.ld_views[lpc].set_title(lnav_view_titles[lpc]);
+ }
+
+ load_formats(lnav_data.ld_config_paths, loader_errors);
+
+ {
+ auto_mem<char, sqlite3_free> errmsg;
+
+ if (sqlite3_exec(lnav_data.ld_db.in(),
+ init_sql.to_string_fragment().data(),
+ nullptr,
+ nullptr,
+ errmsg.out())
+ != SQLITE_OK)
+ {
+ fprintf(stderr,
+ "error: unable to execute DB init -- %s\n",
+ errmsg.in());
+ }
+ }
+
+ lnav_data.ld_vtab_manager->register_vtab(std::make_shared<all_logs_vtab>());
+ lnav_data.ld_vtab_manager->register_vtab(
+ std::make_shared<log_format_vtab_impl>(
+ *log_format::find_root_format("generic_log")));
+
+ for (auto& iter : log_format::get_root_formats()) {
+ auto lvi = iter->get_vtab_impl();
+
+ if (lvi != nullptr) {
+ lnav_data.ld_vtab_manager->register_vtab(lvi);
+ }
+ }
+
+ load_format_extra(lnav_data.ld_db.in(),
+ ec.ec_global_vars,
+ lnav_data.ld_config_paths,
+ loader_errors);
+ load_format_vtabs(lnav_data.ld_vtab_manager.get(), loader_errors);
+
+ if (!loader_errors.empty()) {
+ if (print_user_msgs(loader_errors, mode_flags) != EXIT_SUCCESS) {
+ if (mmode_ops == nullptr) {
+ return EXIT_FAILURE;
+ }
+ }
+ }
+
+ if (mmode_ops) {
+ auto perform_res = lnav::management::perform(mmode_ops);
+
+ return print_user_msgs(perform_res, mode_flags);
+ }
+
+ if (!mode_flags.mf_check_configs && !lnav_data.ld_show_help_view) {
+ DEFAULT_FILES.emplace_back("var/log/messages");
+ DEFAULT_FILES.emplace_back("var/log/system.log");
+ DEFAULT_FILES.emplace_back("var/log/syslog");
+ DEFAULT_FILES.emplace_back("var/log/syslog.log");
+ }
+
+ init_lnav_commands(lnav_commands);
+
+ lnav_data.ld_looping = true;
+ lnav_data.ld_mode = ln_mode_t::PAGING;
+
+ if ((isatty(STDIN_FILENO) || is_dev_null(STDIN_FILENO)) && file_args.empty()
+ && !mode_flags.mf_no_default)
+ {
+ char start_dir[FILENAME_MAX];
+
+ if (getcwd(start_dir, sizeof(start_dir)) == nullptr) {
+ perror("getcwd");
+ } else {
+ do {
+ if (!append_default_files()) {
+ retval = EXIT_FAILURE;
+ }
+ } while (lnav_data.ld_active_files.fc_file_names.empty()
+ && change_to_parent_dir());
+
+ if (chdir(start_dir) == -1) {
+ perror("chdir(start_dir)");
+ }
+ }
+ }
+
+ {
+ const auto internals_dir_opt = getenv_opt("DUMP_INTERNALS_DIR");
+
+ if (internals_dir_opt) {
+ lnav::dump_internals(internals_dir_opt.value());
+
+ return EXIT_SUCCESS;
+ }
+ }
+
+ if (file_args.empty()) {
+ load_stdin = true;
+ }
+
+ for (auto& file_path : file_args) {
+ auto file_path_without_trailer = file_path;
+ auto file_loc = file_location_t{mapbox::util::no_init{}};
+ auto_mem<char> abspath;
+ struct stat st;
+
+ auto colon_index = file_path.rfind(':');
+ if (colon_index != std::string::npos) {
+ auto top_range = scn::string_view{&file_path[colon_index + 1],
+ &(*file_path.cend())};
+ auto scan_res = scn::scan_value<int>(top_range);
+
+ if (scan_res) {
+ file_path_without_trailer = file_path.substr(0, colon_index);
+ file_loc = vis_line_t(scan_res.value());
+ } else {
+ log_warning(
+ "failed to parse line number from file path with colon: %s",
+ file_path.c_str());
+ }
+ }
+ auto hash_index = file_path.rfind('#');
+ if (hash_index != std::string::npos) {
+ file_loc = file_path.substr(hash_index);
+ file_path_without_trailer = file_path.substr(0, hash_index);
+ }
+ if (stat(file_path_without_trailer.c_str(), &st) == 0) {
+ file_path = file_path_without_trailer;
+ }
+
+ if (file_path == "-") {
+ load_stdin = true;
+ }
+#ifdef HAVE_LIBCURL
+ else if (is_url(file_path))
+ {
+ auto ul = std::make_shared<url_loader>(file_path);
+
+ lnav_data.ld_active_files.fc_file_names[file_path].with_fd(
+ ul->copy_fd());
+ isc::to<curl_looper&, services::curl_streamer_t>().send(
+ [ul](auto& clooper) { clooper.add_request(ul); });
+ }
+#endif
+ else if (is_glob(file_path))
+ {
+ lnav_data.ld_active_files.fc_file_names[file_path].with_tail(
+ !(lnav_data.ld_flags & LNF_HEADLESS));
+ } else if (stat(file_path.c_str(), &st) == -1) {
+ if (file_path.find(':') != std::string::npos) {
+ lnav_data.ld_active_files.fc_file_names[file_path].with_tail(
+ !(lnav_data.ld_flags & LNF_HEADLESS));
+ } else {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ attr_line_t("unable to open file: ")
+ .append(lnav::roles::file(file_path)))
+ .with_errno_reason());
+ retval = EXIT_FAILURE;
+ }
+ } else if (access(file_path.c_str(), R_OK) == -1) {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ attr_line_t("file exists, but is not readable: ")
+ .append(lnav::roles::file(file_path)))
+ .with_errno_reason());
+ retval = EXIT_FAILURE;
+ } else if (S_ISFIFO(st.st_mode)) {
+ auto_fd fifo_fd;
+
+ if ((fifo_fd = open(file_path.c_str(), O_RDONLY)) == -1) {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ attr_line_t("cannot open fifo: ")
+ .append(lnav::roles::file(file_path)))
+ .with_errno_reason());
+ retval = EXIT_FAILURE;
+ } else {
+ auto fifo_tmp_fd
+ = lnav::filesystem::open_temp_file(
+ ghc::filesystem::temp_directory_path()
+ / "lnav.fifo.XXXXXX")
+ .map([](auto&& pair) {
+ ghc::filesystem::remove(pair.first);
+
+ return std::move(pair.second);
+ })
+ .expect("Cannot create temporary file for FIFO");
+ auto fifo_piper = std::make_shared<piper_proc>(
+ std::move(fifo_fd), false, std::move(fifo_tmp_fd));
+ auto fifo_out_fd = fifo_piper->get_fd();
+ auto desc = fmt::format(FMT_STRING("FIFO [{}]"),
+ lnav_data.ld_fifo_counter++);
+
+ lnav_data.ld_active_files.fc_file_names[desc].with_fd(
+ std::move(fifo_out_fd));
+ lnav_data.ld_pipers.push_back(fifo_piper);
+ }
+ } else if ((abspath = realpath(file_path.c_str(), nullptr)) == nullptr)
+ {
+ perror("Cannot find file");
+ retval = EXIT_FAILURE;
+ } else if (S_ISDIR(st.st_mode)) {
+ std::string dir_wild(abspath.in());
+
+ if (dir_wild[dir_wild.size() - 1] == '/') {
+ dir_wild.resize(dir_wild.size() - 1);
+ }
+ lnav_data.ld_active_files.fc_file_names.emplace(
+ dir_wild + "/*", logfile_open_options());
+ } else {
+ lnav_data.ld_active_files.fc_file_names.emplace(
+ abspath.in(), logfile_open_options());
+ if (file_loc.valid()) {
+ lnav_data.ld_files_to_front.emplace_back(abspath.in(),
+ file_loc);
+ }
+ }
+ }
+
+ if (mode_flags.mf_check_configs) {
+ rescan_files(true);
+ for (auto& lf : lnav_data.ld_active_files.fc_files) {
+ logfile::rebuild_result_t rebuild_result;
+
+ do {
+ rebuild_result = lf->rebuild_index();
+ } while (rebuild_result == logfile::rebuild_result_t::NEW_LINES
+ || rebuild_result == logfile::rebuild_result_t::NEW_ORDER);
+ auto fmt = lf->get_format();
+ if (fmt == nullptr) {
+ fprintf(stderr,
+ "error:%s:no format found for file\n",
+ lf->get_filename().c_str());
+ retval = EXIT_FAILURE;
+ continue;
+ }
+ for (auto line_iter = lf->begin(); line_iter != lf->end();
+ ++line_iter)
+ {
+ if (line_iter->get_msg_level() != log_level_t::LEVEL_INVALID) {
+ continue;
+ }
+
+ size_t partial_len;
+
+ auto read_result = lf->read_line(line_iter);
+ if (read_result.isErr()) {
+ continue;
+ }
+ shared_buffer_ref sbr = read_result.unwrap();
+ if (fmt->scan_for_partial(sbr, partial_len)) {
+ long line_number = distance(lf->begin(), line_iter);
+ std::string full_line(sbr.get_data(), sbr.length());
+ std::string partial_line(sbr.get_data(), partial_len);
+
+ fprintf(stderr,
+ "error:%s:%ld:line did not match format %s\n",
+ lf->get_filename().c_str(),
+ line_number,
+ fmt->get_pattern_path(line_number).c_str());
+ fprintf(stderr,
+ "error:%s:%ld: line -- %s\n",
+ lf->get_filename().c_str(),
+ line_number,
+ full_line.c_str());
+ if (partial_len > 0) {
+ fprintf(stderr,
+ "error:%s:%ld:partial match -- %s\n",
+ lf->get_filename().c_str(),
+ line_number,
+ partial_line.c_str());
+ } else {
+ fprintf(stderr,
+ "error:%s:%ld:no partial match found\n",
+ lf->get_filename().c_str(),
+ line_number);
+ }
+ retval = EXIT_FAILURE;
+ }
+ }
+ }
+ return retval;
+ }
+
+ if (lnav_data.ld_flags & LNF_HEADLESS || mode_flags.mf_check_configs) {
+ } else if (!isatty(STDOUT_FILENO)) {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ "unable to display interactive text UI")
+ .with_reason("stdout is not a TTY")
+ .with_help(attr_line_t("pass the ")
+ .append("-n"_symbol)
+ .append(" option to run lnav in headless mode "
+ "or don't redirect stdout")));
+ retval = EXIT_FAILURE;
+ }
+
+ if (load_stdin && !isatty(STDIN_FILENO) && !is_dev_null(STDIN_FILENO)
+ && !exec_stdin)
+ {
+ if (stdin_opts.so_out.empty()) {
+ auto pattern
+ = lnav::paths::dotlnav() / "stdin-captures/stdin.XXXXXX";
+
+ auto open_result = lnav::filesystem::open_temp_file(pattern);
+ if (open_result.isErr()) {
+ fprintf(stderr,
+ "Unable to open temporary file for stdin: %s",
+ open_result.unwrapErr().c_str());
+ return EXIT_FAILURE;
+ }
+
+ auto temp_pair = open_result.unwrap();
+ stdin_tmp_path = temp_pair.first;
+ stdin_opts.so_out_fd = std::move(temp_pair.second);
+ } else {
+ auto open_res = lnav::filesystem::create_file(
+ stdin_opts.so_out, O_RDWR | O_TRUNC, 0600);
+ if (open_res.isErr()) {
+ fmt::print(stderr, "error: {}\n", open_res.unwrapErr());
+ return EXIT_FAILURE;
+ }
+
+ stdin_opts.so_out_fd = open_res.unwrap();
+ }
+
+ stdin_captured = true;
+ stdin_reader
+ = std::make_shared<piper_proc>(auto_fd(STDIN_FILENO),
+ stdin_opts.so_timestamp,
+ std::move(stdin_opts.so_out_fd));
+ lnav_data.ld_active_files.fc_file_names["stdin"]
+ .with_fd(stdin_reader->get_fd())
+ .with_include_in_session(false);
+ lnav_data.ld_pipers.push_back(stdin_reader);
+ }
+
+ if (!isatty(STDIN_FILENO) && isatty(STDOUT_FILENO)) {
+ if (dup2(STDOUT_FILENO, STDIN_FILENO) == -1) {
+ perror("cannot dup stdout to stdin");
+ }
+ }
+
+ if (retval == EXIT_SUCCESS
+ && lnav_data.ld_active_files.fc_file_names.empty()
+ && lnav_data.ld_commands.empty()
+ && !(lnav_data.ld_show_help_view || mode_flags.mf_no_default))
+ {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error("nothing to do")
+ .with_reason("no files given or default files found")
+ .with_help(
+ attr_line_t("use the ")
+ .append_quoted(lnav::roles::keyword("-N"))
+ .append(
+ " option to open lnav without loading any files")));
+ retval = EXIT_FAILURE;
+ }
+
+ if (retval == EXIT_SUCCESS) {
+ isc::supervisor root_superv(injector::get<isc::service_list>());
+
+ try {
+ char pcre2_version[128];
+
+ pcre2_config(PCRE2_CONFIG_VERSION, pcre2_version);
+ log_info("startup: %s", VCS_PACKAGE_STRING);
+ log_host_info();
+ log_info("Libraries:");
+#ifdef HAVE_BZLIB_H
+ log_info(" bzip=%s", BZ2_bzlibVersion());
+#endif
+#ifdef HAVE_LIBCURL
+ log_info(" curl=%s (%s)", LIBCURL_VERSION, LIBCURL_TIMESTAMP);
+#endif
+#ifdef HAVE_ARCHIVE_H
+ log_info(" libarchive=%d", ARCHIVE_VERSION_NUMBER);
+ log_info(" details=%s", archive_version_details());
+#endif
+ log_info(" ncurses=%s", NCURSES_VERSION);
+ log_info(" pcre2=%s", pcre2_version);
+ log_info(" readline=%s", rl_library_version);
+ log_info(" sqlite=%s", sqlite3_version);
+ log_info(" zlib=%s", zlibVersion());
+ log_info("lnav_data:");
+ log_info(" flags=%x", lnav_data.ld_flags);
+ log_info(" commands:");
+ for (auto cmd_iter = lnav_data.ld_commands.begin();
+ cmd_iter != lnav_data.ld_commands.end();
+ ++cmd_iter)
+ {
+ log_info(" %s", cmd_iter->c_str());
+ }
+ log_info(" files:");
+ for (auto file_iter
+ = lnav_data.ld_active_files.fc_file_names.begin();
+ file_iter != lnav_data.ld_active_files.fc_file_names.end();
+ ++file_iter)
+ {
+ log_info(" %s", file_iter->first.c_str());
+ }
+
+ if (lnav_data.ld_flags & LNF_HEADLESS) {
+ std::vector<
+ std::pair<Result<std::string, lnav::console::user_message>,
+ std::string>>
+ cmd_results;
+ textview_curses *log_tc, *text_tc, *tc;
+ bool output_view = true;
+
+ view_colors::init(true);
+ rescan_files(true);
+ if (!lnav_data.ld_active_files.fc_name_to_errors.empty()) {
+ for (const auto& pair :
+ lnav_data.ld_active_files.fc_name_to_errors)
+ {
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::error(
+ attr_line_t("unable to open file: ")
+ .append(lnav::roles::file(pair.first)))
+ .with_reason(pair.second.fei_description));
+ }
+
+ return EXIT_FAILURE;
+ }
+ init_session();
+ lnav_data.ld_exec_context.set_output("stdout", stdout, nullptr);
+ alerter::singleton().enabled(false);
+
+ log_tc = &lnav_data.ld_views[LNV_LOG];
+ log_tc->set_height(24_vl);
+ lnav_data.ld_view_stack.push_back(log_tc);
+ // Read all of stdin
+ wait_for_pipers();
+ rebuild_indexes_repeatedly();
+ wait_for_children();
+
+ log_tc->set_top(0_vl);
+ text_tc = &lnav_data.ld_views[LNV_TEXT];
+ text_tc->set_height(vis_line_t(text_tc->get_inner_height()
+ - text_tc->get_top()));
+ setup_highlights(lnav_data.ld_views[LNV_TEXT].get_highlights());
+ if (lnav_data.ld_log_source.text_line_count() == 0
+ && lnav_data.ld_text_source.text_line_count() > 0)
+ {
+ ensure_view(&lnav_data.ld_views[LNV_TEXT]);
+ }
+
+ log_info("Executing initial commands");
+ execute_init_commands(lnav_data.ld_exec_context, cmd_results);
+ archive_manager::cleanup_cache();
+ tailer::cleanup_cache();
+ line_buffer::cleanup_cache();
+ wait_for_pipers();
+ isc::to<curl_looper&, services::curl_streamer_t>()
+ .send_and_wait(
+ [](auto& clooper) { clooper.process_all(); });
+ rebuild_indexes_repeatedly();
+ wait_for_children();
+ if (!lnav_data.ld_active_files.fc_name_to_errors.empty()) {
+ for (const auto& pair :
+ lnav_data.ld_active_files.fc_name_to_errors)
+ {
+ fprintf(stderr,
+ "error: unable to open file: %s -- %s\n",
+ pair.first.c_str(),
+ pair.second.fei_description.c_str());
+ }
+
+ return EXIT_FAILURE;
+ }
+
+ for (const auto& lf : lnav_data.ld_active_files.fc_files) {
+ for (const auto& note : lf->get_notes()) {
+ switch (note.first) {
+ case logfile::note_type::not_utf: {
+ auto um = lnav::console::user_message::error(
+ note.second);
+ lnav::console::print(stderr, um);
+ break;
+ }
+
+ default:
+ break;
+ }
+ }
+ }
+
+ for (auto& pair : cmd_results) {
+ if (pair.first.isErr()) {
+ lnav::console::print(stderr, pair.first.unwrapErr());
+ output_view = false;
+ } else {
+ auto msg = pair.first.unwrap();
+
+ if (startswith(msg, "info:")) {
+ if (verbosity == verbosity_t::verbose) {
+ printf("%s\n", msg.c_str());
+ }
+ } else if (!msg.empty()) {
+ printf("%s\n", msg.c_str());
+ output_view = false;
+ }
+ }
+ }
+
+ if (output_view && verbosity != verbosity_t::quiet
+ && !lnav_data.ld_view_stack.empty()
+ && !lnav_data.ld_stdout_used)
+ {
+ bool suppress_empty_lines = false;
+ unsigned long view_index;
+ vis_line_t y;
+
+ tc = *lnav_data.ld_view_stack.top();
+ view_index = tc - lnav_data.ld_views;
+ switch (view_index) {
+ case LNV_DB:
+ case LNV_HISTOGRAM:
+ suppress_empty_lines = true;
+ break;
+ default:
+ break;
+ }
+
+ auto* los = tc->get_overlay_source();
+
+ vis_line_t vl;
+ for (vl = tc->get_top(); vl < tc->get_inner_height();
+ ++vl, ++y)
+ {
+ attr_line_t al;
+
+ while (los != nullptr
+ && los->list_value_for_overlay(
+ *tc, y, tc->get_inner_height(), vl, al))
+ {
+ write_line_to(stdout, al);
+ ++y;
+ }
+
+ std::vector<attr_line_t> rows(1);
+ tc->listview_value_for_rows(*tc, vl, rows);
+ if (suppress_empty_lines && rows[0].empty()) {
+ continue;
+ }
+
+ write_line_to(stdout, rows[0]);
+ }
+ {
+ attr_line_t al;
+
+ while (los != nullptr
+ && los->list_value_for_overlay(
+ *tc, y, tc->get_inner_height(), vl, al)
+ && !al.empty())
+ {
+ write_line_to(stdout, al);
+ ++y;
+ }
+ }
+ }
+ } else {
+ init_session();
+
+ guard_termios gt(STDIN_FILENO);
+ lnav_log_orig_termios = gt.get_termios();
+
+ looper();
+
+ dup2(STDOUT_FILENO, STDERR_FILENO);
+
+ signal(SIGINT, SIG_DFL);
+
+ save_session();
+ }
+ } catch (const std::system_error& e) {
+ if (e.code().value() != EPIPE) {
+ fprintf(stderr, "error: %s\n", e.what());
+ }
+ } catch (const line_buffer::error& e) {
+ fprintf(stderr, "error: %s\n", strerror(e.e_err));
+ } catch (const std::exception& e) {
+ fprintf(stderr, "error: %s\n", e.what());
+ }
+
+ // When reading from stdin, tell the user where the capture file is
+ // stored so they can look at it later.
+ if (stdin_captured && stdin_opts.so_out.empty()
+ && !(lnav_data.ld_flags & LNF_HEADLESS))
+ {
+ auto stdin_fd = stdin_reader->get_fd();
+ struct stat stdin_stat;
+ nonstd::optional<file_ssize_t> stdin_size;
+
+ // NB: the file can be deleted by the time we get here
+ fchmod(stdin_fd.get(), S_IRUSR);
+ if (fstat(stdin_fd.get(), &stdin_stat) != -1) {
+ stdin_size = stdin_stat.st_size;
+ }
+ if (!ghc::filesystem::exists(stdin_tmp_path)
+ || verbosity == verbosity_t::quiet || !stdin_size
+ || stdin_size.value() == 0
+ || stdin_size.value() > MAX_STDIN_CAPTURE_SIZE)
+ {
+ std::error_code rm_err_code;
+
+ log_info("not saving stdin capture -- %s (size=%d)",
+ stdin_tmp_path.c_str(),
+ stdin_size.value_or(-1));
+ ghc::filesystem::remove(stdin_tmp_path, rm_err_code);
+ } else {
+ auto home = getenv_opt("HOME");
+ auto path_str = stdin_tmp_path.string();
+
+ if (home && startswith(path_str, home.value())) {
+ path_str = path_str.substr(strlen(home.value()));
+ if (path_str[0] != '/') {
+ path_str.insert(0, 1, '/');
+ }
+ path_str.insert(0, 1, '~');
+ }
+
+ lnav::console::print(
+ stderr,
+ lnav::console::user_message::info(
+ attr_line_t()
+ .append(lnav::roles::number(humanize::file_size(
+ stdin_size.value(), humanize::alignment::none)))
+ .append(" of data from stdin was captured and "
+ "will be saved for one day. You can "
+ "reopen it by running:\n")
+ .appendf(FMT_STRING(" {} "),
+ lnav_data.ld_program_name)
+ .append(lnav::roles::file(path_str))));
+ }
+ }
+ }
+
+ return retval;
+}
diff --git a/src/lnav.events.cc b/src/lnav.events.cc
new file mode 100644
index 0000000..d341493
--- /dev/null
+++ b/src/lnav.events.cc
@@ -0,0 +1,177 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "lnav.events.hh"
+
+#include "sqlitepp.client.hh"
+
+namespace lnav {
+namespace events {
+
+namespace file {
+
+const std::string open::SCHEMA_ID
+ = "https://lnav.org/event-file-open-v1.schema.json";
+
+const typed_json_path_container<open> open::handlers = typed_json_path_container<open>{
+ yajlpp::property_handler("$schema").for_field(&open::o_schema)
+ .with_example(open::SCHEMA_ID),
+ yajlpp::property_handler("filename")
+ .with_description("The path of the file that was opened")
+ .for_field(&open::o_filename),
+}
+ .with_schema_id2(open::SCHEMA_ID)
+ .with_description2("Event fired when a file is opened.");
+
+const std::string format_detected::SCHEMA_ID
+ = "https://lnav.org/event-file-format-detected-v1.schema.json";
+
+const typed_json_path_container<format_detected> format_detected::handlers = typed_json_path_container<format_detected>{
+ yajlpp::property_handler("$schema").for_field(&format_detected::fd_schema)
+ .with_example(format_detected::SCHEMA_ID),
+ yajlpp::property_handler("filename")
+ .with_description("The path of the file for which a matching format was found")
+ .for_field(&format_detected::fd_filename),
+ yajlpp::property_handler("format")
+ .with_description("The name of the format")
+ .for_field(&format_detected::fd_format),
+}
+ .with_schema_id2(format_detected::SCHEMA_ID)
+ .with_description2("Event fired when a log format is detected for a file.");
+
+} // namespace file
+
+namespace log {
+
+const std::string msg_detected::SCHEMA_ID
+ = "https://lnav.org/event-log-msg-detected-v1.schema.json";
+
+static const json_path_container msg_values_handlers = {
+ yajlpp::pattern_property_handler("(?<name>[\\w\\-]+)")
+ .with_synopsis("<name>")
+ .for_field(&msg_detected::md_values),
+};
+
+const typed_json_path_container<msg_detected> msg_detected::handlers = typed_json_path_container<msg_detected>{
+ yajlpp::property_handler("$schema").for_field(&msg_detected::md_schema)
+ .with_example(msg_detected::SCHEMA_ID),
+ yajlpp::property_handler("watch-name")
+ .with_description("The name of the watch expression that matched this log message")
+ .for_field(&msg_detected::md_watch_name),
+ yajlpp::property_handler("filename")
+ .with_description("The path of the file containing the log message")
+ .for_field(&msg_detected::md_filename),
+ yajlpp::property_handler("line-number")
+ .with_description("The line number in the file, starting from zero")
+ .for_field(&msg_detected::md_line_number),
+ yajlpp::property_handler("format")
+ .with_description("The name of the log format that matched this log message")
+ .for_field(&msg_detected::md_format),
+ yajlpp::property_handler("timestamp")
+ .with_description("The timestamp of the log message")
+ .for_field(&msg_detected::md_timestamp),
+ yajlpp::property_handler("values")
+ .with_description("The log message values captured by the log format")
+ .with_children(msg_values_handlers),
+}
+ .with_schema_id2(msg_detected::SCHEMA_ID)
+ .with_description2("Event fired when a log message is detected by a watch expression.");
+
+} // namespace log
+
+namespace session {
+
+const std::string loaded::SCHEMA_ID
+ = "https://lnav.org/event-session-loaded-v1.schema.json";
+
+const typed_json_path_container<loaded> loaded::handlers = typed_json_path_container<loaded>{
+ yajlpp::property_handler("$schema").for_field(&loaded::l_schema)
+ .with_example(loaded::SCHEMA_ID),
+}
+ .with_schema_id2(loaded::SCHEMA_ID)
+ .with_description2("Event fired when a session is loaded.");
+
+} // namespace session
+
+int
+register_events_tab(sqlite3* db)
+{
+ static const char* CREATE_EVENTS_TAB_SQL = R"(
+CREATE TABLE lnav_events (
+ ts TEXT NOT NULL DEFAULT(strftime('%Y-%m-%dT%H:%M:%f', 'now')),
+ content TEXT
+)
+)";
+ static const char* DELETE_EVENTS_TRIGGER_SQL = R"(
+CREATE TRIGGER lnav_events_cleaner AFTER INSERT ON lnav_events
+BEGIN
+ DELETE FROM lnav_events WHERE rowid <= NEW.rowid - 1000;
+END
+)";
+
+ auto_mem<char> errmsg(sqlite3_free);
+
+ if (sqlite3_exec(db, CREATE_EVENTS_TAB_SQL, nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("Unable to create events table: %s", errmsg.in());
+ }
+ if (sqlite3_exec(
+ db, DELETE_EVENTS_TRIGGER_SQL, nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("Unable to create event cleaner trigger: %s", errmsg.in());
+ }
+
+ return 0;
+}
+
+void
+details::publish(sqlite3* db, const std::string& content)
+{
+ static const char* INSERT_SQL = R"(
+INSERT INTO lnav_events (content) VALUES (?)
+)";
+
+ auto prep_res = prepare_stmt(db, INSERT_SQL, content);
+ if (prep_res.isErr()) {
+ log_error("unable to prepare event statement: %s",
+ prep_res.unwrapErr().c_str());
+ return;
+ }
+
+ auto exec_res = prep_res.unwrap().execute();
+ if (exec_res.isErr()) {
+ log_error("failed to execute insert: %s", exec_res.unwrapErr().c_str());
+ return;
+ }
+}
+
+} // namespace events
+} // namespace lnav
diff --git a/src/lnav.events.hh b/src/lnav.events.hh
new file mode 100644
index 0000000..d4a9526
--- /dev/null
+++ b/src/lnav.events.hh
@@ -0,0 +1,128 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_events_hh
+#define lnav_events_hh
+
+#include <sqlite3.h>
+
+#include "yajlpp/yajlpp_def.hh"
+
+namespace lnav {
+namespace events {
+
+namespace file {
+
+struct open {
+ std::string o_filename;
+ std::string o_schema{SCHEMA_ID};
+
+ static const std::string SCHEMA_ID;
+ static const typed_json_path_container<open> handlers;
+};
+
+struct format_detected {
+ std::string fd_filename;
+ std::string fd_format;
+ std::string fd_schema{SCHEMA_ID};
+
+ static const std::string SCHEMA_ID;
+ static const typed_json_path_container<format_detected> handlers;
+};
+
+} // namespace file
+
+namespace log {
+
+struct msg_detected {
+ std::string md_watch_name;
+ std::string md_filename;
+ std::string md_format;
+ uint32_t md_line_number;
+ std::string md_timestamp;
+ std::map<std::string, json_any_t> md_values;
+ std::string md_schema{SCHEMA_ID};
+
+ static const std::string SCHEMA_ID;
+ static const typed_json_path_container<msg_detected> handlers;
+};
+
+} // namespace log
+
+namespace session {
+
+struct loaded {
+ std::string l_schema{SCHEMA_ID};
+
+ static const std::string SCHEMA_ID;
+ static const typed_json_path_container<loaded> handlers;
+};
+
+} // namespace session
+
+int register_events_tab(sqlite3* db);
+
+namespace details {
+void publish(sqlite3* db, const std::string& content);
+} // namespace details
+
+template<typename T>
+void
+publish(sqlite3* db, T event)
+{
+ auto serialized = T::handlers.to_string(event);
+
+ details::publish(db, serialized);
+}
+
+template<typename T, typename F>
+void
+publish(sqlite3* db, const T& container, F func)
+{
+ auto_mem<char> errmsg(sqlite3_free);
+
+ if (sqlite3_exec(db, "BEGIN TRANSACTION", nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to start event transaction: %s", errmsg.in());
+ }
+ for (const auto& elem : container) {
+ publish(db, func(elem));
+ }
+ if (sqlite3_exec(db, "COMMIT TRANSACTION", nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to commit event transaction: %s", errmsg.in());
+ }
+}
+
+} // namespace events
+} // namespace lnav
+
+#endif
diff --git a/src/lnav.hh b/src/lnav.hh
new file mode 100644
index 0000000..e235067
--- /dev/null
+++ b/src/lnav.hh
@@ -0,0 +1,286 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav.hh
+ */
+
+#ifndef lnav_hh
+#define lnav_hh
+
+#include <list>
+#include <map>
+#include <memory>
+#include <set>
+#include <stack>
+#include <unordered_map>
+
+#include <signal.h>
+#include <sys/time.h>
+
+#include "archive_manager.hh"
+#include "base/ansi_scrubber.hh"
+#include "base/future_util.hh"
+#include "base/isc.hh"
+#include "bottom_status_source.hh"
+#include "bound_tags.hh"
+#include "command_executor.hh"
+#include "config.h"
+#include "db_sub_source.hh"
+#include "doc_status_source.hh"
+#include "file_collection.hh"
+#include "files_sub_source.hh"
+#include "filter_status_source.hh"
+#include "grep_highlighter.hh"
+#include "hist_source.hh"
+#include "input_dispatcher.hh"
+#include "listview_curses.hh"
+#include "log_format_loader.hh"
+#include "log_vtab_impl.hh"
+#include "logfile.hh"
+#include "piper_proc.hh"
+#include "plain_text_source.hh"
+#include "preview_status_source.hh"
+#include "readline_curses.hh"
+#include "relative_time.hh"
+#include "safe/safe.h"
+#include "sql_util.hh"
+#include "statusview_curses.hh"
+#include "textfile_sub_source.hh"
+#include "view_helpers.hh"
+
+class spectrogram_source;
+class spectro_status_source;
+
+extern const std::vector<std::string> lnav_zoom_strings;
+
+/** The status bars. */
+typedef enum {
+ LNS_TOP,
+ LNS_BOTTOM,
+ LNS_FILTER,
+ LNS_FILTER_HELP,
+ LNS_DOC,
+ LNS_PREVIEW,
+ LNS_SPECTRO,
+
+ LNS__MAX
+} lnav_status_t;
+
+using ppid_time_pair_t = std::pair<int, int>;
+using session_pair_t = std::pair<ppid_time_pair_t, ghc::filesystem::path>;
+
+class input_state_tracker : public log_state_dumper {
+public:
+ input_state_tracker()
+ {
+ memset(this->ist_recent_key_presses,
+ 0,
+ sizeof(this->ist_recent_key_presses));
+ }
+
+ void log_state() override
+ {
+ log_info("recent_key_presses: index=%d", this->ist_index);
+ for (int lpc = 0; lpc < COUNT; lpc++) {
+ log_msg_extra(" 0x%x (%c)",
+ this->ist_recent_key_presses[lpc],
+ this->ist_recent_key_presses[lpc]);
+ }
+ log_msg_extra_complete();
+ }
+
+ void push_back(int ch)
+ {
+ this->ist_recent_key_presses[this->ist_index % COUNT] = ch;
+ this->ist_index = (this->ist_index + 1) % COUNT;
+ }
+
+private:
+ static const int COUNT = 10;
+
+ int ist_recent_key_presses[COUNT];
+ size_t ist_index{0};
+};
+
+struct key_repeat_history {
+ int krh_key{0};
+ int krh_count{0};
+ vis_line_t krh_start_line{0_vl};
+ struct timeval krh_last_press_time {
+ 0, 0
+ };
+
+ void update(int ch, vis_line_t top)
+ {
+ struct timeval now, diff;
+
+ gettimeofday(&now, nullptr);
+ timersub(&now, &this->krh_last_press_time, &diff);
+ if (diff.tv_sec >= 1 || diff.tv_usec > (750 * 1000)) {
+ this->krh_key = 0;
+ this->krh_count = 0;
+ }
+ this->krh_last_press_time = now;
+
+ if (this->krh_key == ch) {
+ this->krh_count += 1;
+ } else {
+ this->krh_key = ch;
+ this->krh_count = 1;
+ this->krh_start_line = top;
+ }
+ };
+};
+
+using file_location_t = mapbox::util::variant<vis_line_t, std::string>;
+
+struct lnav_data_t {
+ std::map<std::string, std::list<session_pair_t>> ld_session_id;
+ time_t ld_session_time;
+ time_t ld_session_load_time;
+ const char* ld_program_name;
+ std::string ld_debug_log_name;
+
+ std::list<std::string> ld_commands;
+ bool ld_cmd_init_done;
+ bool ld_session_loaded;
+ std::vector<ghc::filesystem::path> ld_config_paths;
+ file_collection ld_active_files;
+ std::list<child_poller> ld_child_pollers;
+ std::list<std::pair<std::string, file_location_t>> ld_files_to_front;
+ bool ld_stdout_used;
+ sig_atomic_t ld_looping;
+ sig_atomic_t ld_winched;
+ sig_atomic_t ld_child_terminated;
+ unsigned long ld_flags;
+ WINDOW* ld_window;
+ ln_mode_t ld_mode;
+ ln_mode_t ld_last_config_mode{ln_mode_t::FILTER};
+
+ statusview_curses ld_status[LNS__MAX];
+ bottom_status_source ld_bottom_source;
+ filter_status_source ld_filter_status_source;
+ filter_help_status_source ld_filter_help_status_source;
+ doc_status_source ld_doc_status_source;
+ preview_status_source ld_preview_status_source;
+ std::unique_ptr<spectro_status_source> ld_spectro_status_source;
+ bool ld_preview_hidden;
+ int64_t ld_preview_generation{0};
+ action_broadcaster<listview_curses> ld_scroll_broadcaster;
+ action_broadcaster<listview_curses> ld_view_stack_broadcaster;
+
+ plain_text_source ld_help_source;
+
+ plain_text_source ld_doc_source;
+ textview_curses ld_doc_view;
+ textview_curses ld_filter_view;
+ files_sub_source ld_files_source;
+ files_overlay_source ld_files_overlay;
+ textview_curses ld_files_view;
+ plain_text_source ld_example_source;
+ textview_curses ld_example_view;
+ plain_text_source ld_match_source;
+ textview_curses ld_match_view;
+ plain_text_source ld_preview_source;
+ textview_curses ld_preview_view;
+ plain_text_source ld_user_message_source;
+ textview_curses ld_user_message_view;
+ std::chrono::time_point<std::chrono::steady_clock>
+ ld_user_message_expiration;
+ textview_curses ld_spectro_details_view;
+ plain_text_source ld_spectro_no_details_source;
+
+ view_stack<textview_curses> ld_view_stack;
+ textview_curses* ld_last_view;
+ textview_curses ld_views[LNV__MAX];
+ vis_line_t ld_search_start_line;
+ readline_curses* ld_rl_view;
+
+ logfile_sub_source ld_log_source;
+ hist_source2 ld_hist_source2;
+ int ld_zoom_level;
+ std::unique_ptr<spectrogram_source> ld_spectro_source;
+
+ textfile_sub_source ld_text_source;
+
+ std::map<textview_curses*, int> ld_last_user_mark;
+ std::map<textview_curses*, int> ld_select_start;
+
+ db_label_source ld_db_row_source;
+ db_overlay_source ld_db_overlay;
+ std::vector<std::string> ld_db_key_names;
+
+ vis_line_t ld_last_pretty_print_top;
+
+ std::unique_ptr<log_vtab_manager> ld_vtab_manager;
+ auto_sqlite3 ld_db;
+
+ std::unordered_map<std::string, std::string> ld_table_ddl;
+
+ std::list<pid_t> ld_children;
+ std::list<std::shared_ptr<piper_proc>> ld_pipers;
+
+ input_state_tracker ld_input_state;
+ input_dispatcher ld_input_dispatcher;
+
+ exec_context ld_exec_context;
+
+ int ld_fifo_counter;
+
+ struct key_repeat_history ld_key_repeat_history;
+
+ bool ld_initial_build{false};
+ bool ld_show_help_view{false};
+
+ lnav::func::scoped_cb ld_status_refresher;
+
+ ghc::filesystem::file_time_type ld_last_dot_lnav_time;
+};
+
+struct static_service {};
+
+class main_looper
+ : public isc::service<main_looper>
+ , public static_service {
+public:
+};
+
+extern struct lnav_data_t lnav_data;
+
+extern readline_context::command_map_t lnav_commands;
+extern const int ZOOM_LEVELS[];
+extern const ssize_t ZOOM_COUNT;
+
+#define HELP_MSG_1(x, msg) "Press '" ANSI_BOLD(#x) "' " msg
+
+#define HELP_MSG_2(x, y, msg) "Press " ANSI_BOLD(#x) "/" ANSI_BOLD(#y) " " msg
+
+bool setup_logline_table(exec_context& ec);
+void wait_for_children();
+
+#endif
diff --git a/src/lnav.indexing.cc b/src/lnav.indexing.cc
new file mode 100644
index 0000000..d2a68d7
--- /dev/null
+++ b/src/lnav.indexing.cc
@@ -0,0 +1,457 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "lnav.indexing.hh"
+
+#include "lnav.events.hh"
+#include "lnav.hh"
+#include "service_tags.hh"
+#include "session_data.hh"
+
+using namespace std::chrono_literals;
+
+/**
+ * Observer for loading progress that updates the bottom status bar.
+ */
+class loading_observer : public logfile_observer {
+public:
+ loading_observer() : lo_last_offset(0){};
+
+ indexing_result logfile_indexing(const std::shared_ptr<logfile>& lf,
+ file_off_t off,
+ file_size_t total) override
+ {
+ static sig_atomic_t index_counter = 0;
+
+ if (lnav_data.ld_window == nullptr) {
+ return indexing_result::CONTINUE;
+ }
+
+ /* XXX require(off <= total); */
+ if (off > (off_t) total) {
+ off = total;
+ }
+
+ if ((((size_t) off == total) && (this->lo_last_offset != off))
+ || ui_periodic_timer::singleton().time_to_update(index_counter))
+ {
+ if (off == total) {
+ lnav_data.ld_bottom_source.update_loading(0, 0);
+ } else {
+ lnav_data.ld_bottom_source.update_loading(off, total);
+ }
+ do_observer_update(lf);
+ this->lo_last_offset = off;
+ }
+
+ if (!lnav_data.ld_looping) {
+ return indexing_result::BREAK;
+ }
+ return indexing_result::CONTINUE;
+ }
+
+ off_t lo_last_offset;
+};
+
+void
+do_observer_update(const std::shared_ptr<logfile>& lf)
+{
+ if (isendwin()) {
+ return;
+ }
+ lnav_data.ld_status_refresher();
+ if (lf && lnav_data.ld_mode == ln_mode_t::FILES
+ && !lnav_data.ld_initial_build)
+ {
+ auto& fc = lnav_data.ld_active_files;
+ auto iter = std::find(fc.fc_files.begin(), fc.fc_files.end(), lf);
+
+ if (iter != fc.fc_files.end()) {
+ auto index = std::distance(fc.fc_files.begin(), iter);
+ lnav_data.ld_files_view.set_selection(
+ vis_line_t(fc.fc_other_files.size() + index));
+ lnav_data.ld_files_view.reload_data();
+ lnav_data.ld_files_view.do_update();
+ }
+ }
+ if (handle_winch()) {
+ layout_views();
+ lnav_data.ld_view_stack.do_update();
+ }
+ refresh();
+}
+
+void
+rebuild_hist()
+{
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ hist_source2& hs = lnav_data.ld_hist_source2;
+ int zoom = lnav_data.ld_zoom_level;
+
+ hs.set_time_slice(ZOOM_LEVELS[zoom]);
+ lss.reload_index_delegate();
+}
+
+class textfile_callback : public textfile_sub_source::scan_callback {
+public:
+ void closed_files(
+ const std::vector<std::shared_ptr<logfile>>& files) override
+ {
+ for (const auto& lf : files) {
+ log_info("closed text files: %s", lf->get_filename().c_str());
+ }
+ lnav_data.ld_active_files.close_files(files);
+ }
+
+ void promote_file(const std::shared_ptr<logfile>& lf) override
+ {
+ if (lnav_data.ld_log_source.insert_file(lf)) {
+ this->did_promotion = true;
+ log_info("promoting text file to log file: %s (%s)",
+ lf->get_filename().c_str(),
+ lf->get_content_id().c_str());
+ auto format = lf->get_format();
+ if (format->lf_is_self_describing) {
+ auto vt = format->get_vtab_impl();
+
+ if (vt != nullptr) {
+ lnav_data.ld_vtab_manager->register_vtab(vt);
+ }
+ }
+
+ auto iter = session_data.sd_file_states.find(lf->get_filename());
+ if (iter != session_data.sd_file_states.end()) {
+ log_info(" found visibility state for log file: %d",
+ iter->second.fs_is_visible);
+
+ lnav_data.ld_log_source.find_data(lf) | [&iter](auto ld) {
+ ld->set_visibility(iter->second.fs_is_visible);
+ };
+ }
+
+ lnav::events::publish(lnav_data.ld_db.in(),
+ lnav::events::file::format_detected{
+ lf->get_filename(),
+ lf->get_format_name().to_string(),
+ });
+ } else {
+ this->closed_files({lf});
+ }
+ }
+
+ void scanned_file(const std::shared_ptr<logfile>& lf) override
+ {
+ if (!lnav_data.ld_files_to_front.empty()
+ && lnav_data.ld_files_to_front.front().first == lf->get_filename())
+ {
+ this->front_file = lf;
+ this->front_top = lnav_data.ld_files_to_front.front().second;
+
+ lnav_data.ld_files_to_front.pop_front();
+ }
+ }
+
+ std::shared_ptr<logfile> front_file;
+ file_location_t front_top;
+ bool did_promotion{false};
+};
+
+size_t
+rebuild_indexes(nonstd::optional<ui_clock::time_point> deadline)
+{
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ textview_curses& log_view = lnav_data.ld_views[LNV_LOG];
+ textview_curses& text_view = lnav_data.ld_views[LNV_TEXT];
+ bool scroll_downs[LNV__MAX];
+ size_t retval = 0;
+
+ for (int lpc = 0; lpc < LNV__MAX; lpc++) {
+ auto& view = lnav_data.ld_views[lpc];
+
+ if (view.is_selectable()) {
+ auto inner_height = view.get_inner_height();
+
+ if (inner_height > 0_vl) {
+ scroll_downs[lpc]
+ = (view.get_selection() == inner_height - 1_vl)
+ && !(lnav_data.ld_flags & LNF_HEADLESS);
+ } else {
+ scroll_downs[lpc] = !(lnav_data.ld_flags & LNF_HEADLESS);
+ }
+ } else {
+ scroll_downs[lpc] = (view.get_top() >= view.get_top_for_last_row())
+ && !(lnav_data.ld_flags & LNF_HEADLESS);
+ }
+ }
+
+ {
+ auto* tss = &lnav_data.ld_text_source;
+ textfile_callback cb;
+
+ if (tss->rescan_files(cb, deadline)) {
+ text_view.reload_data();
+ retval += 1;
+ }
+
+ if (cb.front_file != nullptr) {
+ ensure_view(&text_view);
+
+ if (tss->current_file() != cb.front_file) {
+ tss->to_front(cb.front_file);
+ }
+
+ nonstd::optional<vis_line_t> new_top_opt;
+ cb.front_top.match(
+ [&new_top_opt](vis_line_t vl) {
+ log_info("file open request to jump to line: %d", (int) vl);
+ if (vl < 0_vl) {
+ vl += lnav_data.ld_views[LNV_TEXT].get_inner_height();
+ }
+ if (vl < lnav_data.ld_views[LNV_TEXT].get_inner_height()) {
+ new_top_opt = vl;
+ }
+ },
+ [&new_top_opt](const std::string& loc) {
+ log_info("file open request to jump to anchor: %s",
+ loc.c_str());
+ auto* ta = dynamic_cast<text_anchors*>(
+ lnav_data.ld_views[LNV_TEXT].get_sub_source());
+
+ if (ta != nullptr) {
+ new_top_opt = ta->row_for_anchor(loc);
+ }
+ });
+ if (new_top_opt) {
+ log_info(" setting requested top line: %d",
+ (int) new_top_opt.value());
+ text_view.set_top(new_top_opt.value());
+ log_info(" actual top is now: %d", (int) text_view.get_top());
+ scroll_downs[LNV_TEXT] = false;
+ } else {
+ log_warning("could not jump to requested line");
+ }
+ }
+ if (cb.did_promotion && deadline) {
+ // If there's a new log file, extend the deadline so it can be
+ // indexed quickly.
+ deadline = deadline.value() + 500ms;
+ }
+ }
+
+ std::vector<std::shared_ptr<logfile>> closed_files;
+ for (auto& lf : lnav_data.ld_active_files.fc_files) {
+ if ((!lf->exists() || lf->is_closed())) {
+ log_info("closed log file: %s", lf->get_filename().c_str());
+ lnav_data.ld_text_source.remove(lf);
+ lnav_data.ld_log_source.remove_file(lf);
+ closed_files.emplace_back(lf);
+ }
+ }
+ if (!closed_files.empty()) {
+ lnav_data.ld_active_files.close_files(closed_files);
+ }
+
+ auto result = lss.rebuild_index(deadline);
+ if (result != logfile_sub_source::rebuild_result::rr_no_change) {
+ size_t new_count = lss.text_line_count();
+ bool force
+ = result == logfile_sub_source::rebuild_result::rr_full_rebuild;
+
+ if ((!scroll_downs[LNV_LOG]
+ || log_view.get_top() > vis_line_t(new_count))
+ && force)
+ {
+ scroll_downs[LNV_LOG] = false;
+ }
+
+ {
+ std::unordered_map<std::string, std::list<std::shared_ptr<logfile>>>
+ id_to_files;
+ bool reload = false;
+
+ for (const auto& lf : lnav_data.ld_active_files.fc_files) {
+ id_to_files[lf->get_content_id()].push_back(lf);
+ }
+
+ for (auto& pair : id_to_files) {
+ if (pair.second.size() == 1) {
+ continue;
+ }
+
+ pair.second.sort([](const auto& left, const auto& right) {
+ return right->get_stat().st_size < left->get_stat().st_size;
+ });
+
+ auto dupe_name = pair.second.front()->get_unique_path();
+ pair.second.pop_front();
+ for_each(pair.second.begin(),
+ pair.second.end(),
+ [&dupe_name](auto& lf) {
+ log_info("Hiding duplicate file: %s",
+ lf->get_filename().c_str());
+ lf->mark_as_duplicate(dupe_name);
+ lnav_data.ld_log_source.find_data(lf) |
+ [](auto ld) { ld->set_visibility(false); };
+ });
+ reload = true;
+ }
+
+ if (reload) {
+ lss.text_filters_changed();
+ }
+ }
+
+ retval += 1;
+ }
+
+ for (int lpc = 0; lpc < LNV__MAX; lpc++) {
+ auto& scroll_view = lnav_data.ld_views[lpc];
+
+ if (scroll_downs[lpc]) {
+ if (scroll_view.is_selectable()) {
+ auto inner_height = scroll_view.get_inner_height();
+
+ if (inner_height > 0_vl) {
+ scroll_view.set_selection(inner_height - 1_vl);
+ }
+ } else if (scroll_view.get_top_for_last_row()
+ > scroll_view.get_top())
+ {
+ scroll_view.set_top(scroll_view.get_top_for_last_row());
+ }
+ }
+ }
+
+ lnav_data.ld_view_stack.top() | [](auto tc) {
+ lnav_data.ld_filter_status_source.update_filtered(tc->get_sub_source());
+ lnav_data.ld_scroll_broadcaster(tc);
+ };
+
+ return retval;
+}
+
+void
+rebuild_indexes_repeatedly()
+{
+ for (size_t attempt = 0; attempt < 10 && rebuild_indexes() > 0; attempt++) {
+ log_info("continuing to rebuild indexes...");
+ }
+}
+
+bool
+update_active_files(file_collection& new_files)
+{
+ static loading_observer obs;
+
+ if (lnav_data.ld_active_files.fc_invalidate_merge) {
+ lnav_data.ld_active_files.fc_invalidate_merge = false;
+
+ return true;
+ }
+
+ for (const auto& lf : new_files.fc_files) {
+ lf->set_logfile_observer(&obs);
+ lnav_data.ld_text_source.push_back(lf);
+ }
+ for (const auto& other_pair : new_files.fc_other_files) {
+ switch (other_pair.second.ofd_format) {
+ case file_format_t::SQLITE_DB:
+ attach_sqlite_db(lnav_data.ld_db.in(), other_pair.first);
+ break;
+ default:
+ break;
+ }
+ }
+ lnav_data.ld_active_files.merge(new_files);
+ if (!new_files.fc_files.empty() || !new_files.fc_other_files.empty()
+ || !new_files.fc_name_to_errors.empty())
+ {
+ lnav_data.ld_active_files.regenerate_unique_file_names();
+ }
+ lnav_data.ld_child_pollers.insert(
+ lnav_data.ld_child_pollers.begin(),
+ std::make_move_iterator(
+ lnav_data.ld_active_files.fc_child_pollers.begin()),
+ std::make_move_iterator(
+ lnav_data.ld_active_files.fc_child_pollers.end()));
+ lnav_data.ld_active_files.fc_child_pollers.clear();
+
+ lnav::events::publish(
+ lnav_data.ld_db.in(), new_files.fc_files, [](const auto& lf) {
+ return lnav::events::file::open{
+ lf->get_filename(),
+ };
+ });
+
+ return true;
+}
+
+bool
+rescan_files(bool req)
+{
+ auto& mlooper = injector::get<main_looper&, services::main_t>();
+ bool done = false;
+ auto delay = 0ms;
+
+ do {
+ auto fc = lnav_data.ld_active_files.rescan_files(req);
+ bool all_synced = true;
+
+ update_active_files(fc);
+ mlooper.get_port().process_for(delay);
+ for (const auto& pair : lnav_data.ld_active_files.fc_other_files) {
+ if (pair.second.ofd_format != file_format_t::REMOTE) {
+ continue;
+ }
+
+ if (lnav_data.ld_active_files.fc_name_to_errors.count(pair.first)) {
+ continue;
+ }
+
+ if (lnav_data.ld_active_files.fc_synced_files.count(pair.first)
+ == 0)
+ {
+ all_synced = false;
+ }
+ }
+ if (!lnav_data.ld_active_files.fc_name_to_errors.empty()) {
+ return false;
+ }
+ if (!all_synced) {
+ delay = 30ms;
+ }
+ done = fc.fc_file_names.empty() && all_synced;
+ if (!done && !(lnav_data.ld_flags & LNF_HEADLESS)) {
+ lnav_data.ld_files_view.set_needs_update();
+ lnav_data.ld_files_view.do_update();
+ lnav_data.ld_status_refresher();
+ }
+ } while (!done && lnav_data.ld_looping);
+ return true;
+}
diff --git a/src/lnav.indexing.hh b/src/lnav.indexing.hh
new file mode 100644
index 0000000..a37745b
--- /dev/null
+++ b/src/lnav.indexing.hh
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_indexing_hh
+#define lnav_indexing_hh
+
+#include "file_collection.hh"
+#include "logfile_fwd.hh"
+#include "optional.hpp"
+
+void rebuild_hist();
+size_t rebuild_indexes(nonstd::optional<ui_clock::time_point> deadline
+ = nonstd::nullopt);
+void rebuild_indexes_repeatedly();
+bool rescan_files(bool required = false);
+bool update_active_files(file_collection& new_files);
+void do_observer_update(const std::shared_ptr<logfile>& lf);
+
+#endif
diff --git a/src/lnav.management_cli.cc b/src/lnav.management_cli.cc
new file mode 100644
index 0000000..71d3994
--- /dev/null
+++ b/src/lnav.management_cli.cc
@@ -0,0 +1,910 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <queue>
+
+#include "lnav.management_cli.hh"
+
+#include "base/itertools.hh"
+#include "base/result.h"
+#include "base/string_util.hh"
+#include "fmt/format.h"
+#include "itertools.similar.hh"
+#include "log_format.hh"
+#include "log_format_ext.hh"
+#include "mapbox/variant.hpp"
+#include "regex101.import.hh"
+#include "session_data.hh"
+
+using namespace lnav::roles::literals;
+
+namespace lnav {
+
+namespace management {
+
+struct no_subcmd_t {
+ CLI::App* ns_root_app{nullptr};
+};
+
+inline attr_line_t&
+symbol_reducer(const std::string& elem, attr_line_t& accum)
+{
+ return accum.append("\n ").append(lnav::roles::symbol(elem));
+}
+
+inline attr_line_t&
+subcmd_reducer(const CLI::App* app, attr_line_t& accum)
+{
+ return accum.append("\n \u2022 ")
+ .append(lnav::roles::keyword(app->get_name()))
+ .append(": ")
+ .append(app->get_description());
+}
+
+struct subcmd_format_t {
+ using action_t = std::function<perform_result_t(const subcmd_format_t&)>;
+
+ CLI::App* sf_format_app{nullptr};
+ std::string sf_name;
+ CLI::App* sf_regex_app{nullptr};
+ std::string sf_regex_name;
+ CLI::App* sf_regex101_app{nullptr};
+ action_t sf_action;
+
+ subcmd_format_t& set_action(action_t act)
+ {
+ if (!this->sf_action) {
+ this->sf_action = std::move(act);
+ }
+ return *this;
+ }
+
+ Result<std::shared_ptr<log_format>, console::user_message> validate_format()
+ const
+ {
+ if (this->sf_name.empty()) {
+ auto um = console::user_message::error(
+ "expecting a format name to operate on");
+ um.with_note(
+ (log_format::get_root_formats()
+ | lnav::itertools::map(&log_format::get_name)
+ | lnav::itertools::sort_with(intern_string_t::case_lt)
+ | lnav::itertools::map(&intern_string_t::to_string)
+ | lnav::itertools::fold(symbol_reducer, attr_line_t{}))
+ .add_header("the available formats are:"));
+
+ return Err(um);
+ }
+
+ auto lformat = log_format::find_root_format(this->sf_name.c_str());
+ if (lformat == nullptr) {
+ auto um = console::user_message::error(
+ attr_line_t("unknown format: ")
+ .append(lnav::roles::symbol(this->sf_name)));
+ um.with_note(
+ (log_format::get_root_formats()
+ | lnav::itertools::map(&log_format::get_name)
+ | lnav::itertools::similar_to(this->sf_name)
+ | lnav::itertools::map(&intern_string_t::to_string)
+ | lnav::itertools::fold(symbol_reducer, attr_line_t{}))
+ .add_header("did you mean one of the following?"));
+
+ return Err(um);
+ }
+
+ return Ok(lformat);
+ }
+
+ Result<external_log_format*, console::user_message>
+ validate_external_format() const
+ {
+ auto lformat = TRY(this->validate_format());
+ auto* ext_lformat = dynamic_cast<external_log_format*>(lformat.get());
+
+ if (ext_lformat == nullptr) {
+ return Err(console::user_message::error(
+ attr_line_t()
+ .append_quoted(lnav::roles::symbol(this->sf_name))
+ .append(" is an internal format that is not defined in a "
+ "configuration file")));
+ }
+
+ return Ok(ext_lformat);
+ }
+
+ Result<std::pair<external_log_format*,
+ std::shared_ptr<external_log_format::pattern>>,
+ console::user_message>
+ validate_regex() const
+ {
+ auto* ext_lformat = TRY(this->validate_external_format());
+
+ if (this->sf_regex_name.empty()) {
+ auto um = console::user_message::error(
+ "expecting a regex name to operate on");
+ um.with_note(
+ ext_lformat->elf_pattern_order
+ | lnav::itertools::map(&external_log_format::pattern::p_name)
+ | lnav::itertools::map(&intern_string_t::to_string)
+ | lnav::itertools::fold(
+ symbol_reducer, attr_line_t{"the available regexes are:"}));
+
+ return Err(um);
+ }
+
+ for (const auto& pat : ext_lformat->elf_pattern_order) {
+ if (pat->p_name == this->sf_regex_name) {
+ return Ok(std::make_pair(ext_lformat, pat));
+ }
+ }
+
+ auto um = console::user_message::error(
+ attr_line_t("unknown regex: ")
+ .append(lnav::roles::symbol(this->sf_regex_name)));
+ um.with_note(
+ (ext_lformat->elf_pattern_order
+ | lnav::itertools::map(&external_log_format::pattern::p_name)
+ | lnav::itertools::map(&intern_string_t::to_string)
+ | lnav::itertools::similar_to(this->sf_regex_name)
+ | lnav::itertools::fold(symbol_reducer, attr_line_t{}))
+ .add_header("did you mean one of the following?"));
+
+ return Err(um);
+ }
+
+ static perform_result_t default_action(const subcmd_format_t& sf)
+ {
+ auto validate_res = sf.validate_format();
+ if (validate_res.isErr()) {
+ return {validate_res.unwrapErr()};
+ }
+
+ auto lformat = validate_res.unwrap();
+ auto* ext_format = dynamic_cast<external_log_format*>(lformat.get());
+
+ attr_line_t ext_details;
+ if (ext_format != nullptr) {
+ ext_details.append("\n ")
+ .append("Regexes"_h3)
+ .append(": ")
+ .join(ext_format->elf_pattern_order
+ | lnav::itertools::map(
+ &external_log_format::pattern::p_name)
+ | lnav::itertools::map(&intern_string_t::to_string),
+ VC_ROLE.value(role_t::VCR_SYMBOL),
+ ", ");
+ }
+
+ auto um = console::user_message::error(
+ attr_line_t("expecting an operation to perform on the ")
+ .append(lnav::roles::symbol(sf.sf_name))
+ .append(" format"));
+ um.with_note(attr_line_t()
+ .append(lnav::roles::symbol(sf.sf_name))
+ .append(": ")
+ .append(lformat->lf_description)
+ .append(ext_details));
+ um.with_help(
+ sf.sf_format_app->get_subcommands({})
+ | lnav::itertools::fold(
+ subcmd_reducer, attr_line_t{"the available operations are:"}));
+
+ return {um};
+ }
+
+ static perform_result_t default_regex_action(const subcmd_format_t& sf)
+ {
+ auto validate_res = sf.validate_regex();
+
+ if (validate_res.isErr()) {
+ return {validate_res.unwrapErr()};
+ }
+
+ auto um = console::user_message::error(
+ attr_line_t("expecting an operation to perform on the ")
+ .append(lnav::roles::symbol(sf.sf_regex_name))
+ .append(" regular expression"));
+
+ um.with_help(attr_line_t{"the available subcommands are:"}.append(
+ sf.sf_regex_app->get_subcommands({})
+ | lnav::itertools::fold(subcmd_reducer, attr_line_t{})));
+
+ return {um};
+ }
+
+ static perform_result_t get_action(const subcmd_format_t& sf)
+ {
+ auto validate_res = sf.validate_format();
+
+ if (validate_res.isErr()) {
+ return {validate_res.unwrapErr()};
+ }
+
+ auto format = validate_res.unwrap();
+
+ auto um = console::user_message::raw(
+ attr_line_t()
+ .append(lnav::roles::symbol(sf.sf_name))
+ .append(": ")
+ .append(on_blank(format->lf_description, "<no description>")));
+
+ return {um};
+ }
+
+ static perform_result_t source_action(const subcmd_format_t& sf)
+ {
+ auto validate_res = sf.validate_external_format();
+
+ if (validate_res.isErr()) {
+ return {validate_res.unwrapErr()};
+ }
+
+ auto* format = validate_res.unwrap();
+
+ if (format->elf_format_source_order.empty()) {
+ return {
+ console::user_message::error(
+ "format is builtin, there is no source file"),
+ };
+ }
+
+ auto um = console::user_message::raw(
+ format->elf_format_source_order[0].string());
+
+ return {um};
+ }
+
+ static perform_result_t sources_action(const subcmd_format_t& sf)
+ {
+ auto validate_res = sf.validate_external_format();
+
+ if (validate_res.isErr()) {
+ return {validate_res.unwrapErr()};
+ }
+
+ auto* format = validate_res.unwrap();
+
+ if (format->elf_format_source_order.empty()) {
+ return {
+ console::user_message::error(
+ "format is builtin, there is no source file"),
+ };
+ }
+
+ auto um = console::user_message::raw(
+ attr_line_t().join(format->elf_format_source_order,
+ VC_ROLE.value(role_t::VCR_TEXT),
+ "\n"));
+
+ return {um};
+ }
+
+ static perform_result_t regex101_pull_action(const subcmd_format_t& sf)
+ {
+ auto validate_res = sf.validate_regex();
+ if (validate_res.isErr()) {
+ return {validate_res.unwrapErr()};
+ }
+
+ auto format_regex_pair = validate_res.unwrap();
+ auto get_meta_res
+ = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
+
+ return get_meta_res.match(
+ [&sf](
+ const lnav::session::regex101::error& err) -> perform_result_t {
+ return {
+ console::user_message::error(
+ attr_line_t("unable to get DB entry for: ")
+ .append(lnav::roles::symbol(sf.sf_name))
+ .append("/")
+ .append(lnav::roles::symbol(sf.sf_regex_name)))
+ .with_reason(err.e_msg),
+ };
+ },
+ [&sf](
+ const lnav::session::regex101::no_entry&) -> perform_result_t {
+ return {
+ console::user_message::error(
+ attr_line_t("regex ")
+ .append_quoted(
+ lnav::roles::symbol(sf.sf_regex_name))
+ .append(" of format ")
+ .append_quoted(lnav::roles::symbol(sf.sf_name))
+ .append(" has not been pushed to regex101.com"))
+ .with_help(
+ attr_line_t("use the ")
+ .append_quoted("push"_keyword)
+ .append(" subcommand to create the regex on "
+ "regex101.com for easy editing")),
+ };
+ },
+ [&](const lnav::session::regex101::entry& en) -> perform_result_t {
+ auto retrieve_res = regex101::client::retrieve(en.re_permalink);
+
+ return retrieve_res.match(
+ [&](const console::user_message& um) -> perform_result_t {
+ return {
+ console::user_message::error(
+ attr_line_t("unable to retrieve entry ")
+ .append_quoted(
+ lnav::roles::symbol(en.re_permalink))
+ .append(" from regex101.com"))
+ .with_reason(um),
+ };
+ },
+ [&](const regex101::client::no_entry&) -> perform_result_t {
+ lnav::session::regex101::delete_entry(sf.sf_name,
+ sf.sf_regex_name);
+ return {
+ console::user_message::error(
+ attr_line_t("entry ")
+ .append_quoted(
+ lnav::roles::symbol(en.re_permalink))
+ .append(
+ " no longer exists on regex101.com"))
+ .with_help(attr_line_t("use the ")
+ .append_quoted("delete"_keyword)
+ .append(" subcommand to delete "
+ "the association")),
+ };
+ },
+ [&](const regex101::client::entry& remote_entry)
+ -> perform_result_t {
+ auto curr_entry = regex101::convert_format_pattern(
+ format_regex_pair.first, format_regex_pair.second);
+
+ if (curr_entry.e_regex == remote_entry.e_regex) {
+ return {
+ console::user_message::ok(
+ attr_line_t("local regex is in sync "
+ "with entry ")
+ .append_quoted(lnav::roles::symbol(
+ en.re_permalink))
+ .append(" on regex101.com"))
+ .with_help(
+ attr_line_t("make edits on ")
+ .append_quoted(lnav::roles::file(
+ regex101::client::to_edit_url(
+ en.re_permalink)))
+ .append(" and then run this "
+ "command again to update "
+ "the local values")),
+ };
+ }
+
+ auto patch_res
+ = regex101::patch(format_regex_pair.first,
+ sf.sf_regex_name,
+ remote_entry);
+
+ if (patch_res.isErr()) {
+ return {
+ console::user_message::error(
+ attr_line_t(
+ "unable to patch format regex: ")
+ .append(lnav::roles::symbol(sf.sf_name))
+ .append("/")
+ .append(lnav::roles::symbol(
+ sf.sf_regex_name)))
+ .with_reason(patch_res.unwrapErr()),
+ };
+ }
+
+ auto um = console::user_message::ok(
+ attr_line_t("format patch file written to: ")
+ .append(lnav::roles::file(
+ patch_res.unwrap().string())));
+ if (!format_regex_pair.first->elf_builtin_format) {
+ um.with_help(
+ attr_line_t("once the regex has been found "
+ "to be working correctly, move the "
+ "contents of the patch file to the "
+ "original file at:\n ")
+ .append(lnav::roles::file(
+ format_regex_pair.first
+ ->elf_format_source_order.front()
+ .string())));
+ }
+
+ return {um};
+ });
+ });
+ }
+
+ static perform_result_t regex101_default_action(const subcmd_format_t& sf)
+ {
+ auto validate_res = sf.validate_regex();
+
+ if (validate_res.isErr()) {
+ return {validate_res.unwrapErr()};
+ }
+
+ auto um = console::user_message::error(
+ attr_line_t("expecting an operation to perform on the ")
+ .append(lnav::roles::symbol(sf.sf_regex_name))
+ .append(" regex using regex101.com"));
+
+ auto get_res
+ = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
+ if (get_res.is<lnav::session::regex101::entry>()) {
+ auto local_entry = get_res.get<lnav::session::regex101::entry>();
+ um.with_note(
+ attr_line_t("this regex is currently associated with the "
+ "following regex101.com entry:\n ")
+ .append(lnav::roles::file(regex101::client::to_edit_url(
+ local_entry.re_permalink))));
+ }
+
+ um.with_help(attr_line_t{"the available subcommands are:"}.append(
+ sf.sf_regex101_app->get_subcommands({})
+ | lnav::itertools::fold(subcmd_reducer, attr_line_t{})));
+
+ return {um};
+ }
+
+ static perform_result_t regex101_push_action(const subcmd_format_t& sf)
+ {
+ auto validate_res = sf.validate_regex();
+ if (validate_res.isErr()) {
+ return {validate_res.unwrapErr()};
+ }
+
+ auto format_regex_pair = validate_res.unwrap();
+ auto entry = regex101::convert_format_pattern(format_regex_pair.first,
+ format_regex_pair.second);
+ auto get_meta_res
+ = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
+
+ if (get_meta_res.is<lnav::session::regex101::entry>()) {
+ auto entry_meta
+ = get_meta_res.get<lnav::session::regex101::entry>();
+ auto retrieve_res
+ = regex101::client::retrieve(entry_meta.re_permalink);
+
+ if (retrieve_res.is<regex101::client::entry>()) {
+ auto remote_entry = retrieve_res.get<regex101::client::entry>();
+
+ if (remote_entry == entry) {
+ return {
+ console::user_message::ok(
+ attr_line_t("regex101 entry ")
+ .append(lnav::roles::symbol(
+ entry_meta.re_permalink))
+ .append(" is already up-to-date")),
+ };
+ }
+ } else if (retrieve_res.is<console::user_message>()) {
+ return {
+ retrieve_res.get<console::user_message>(),
+ };
+ }
+
+ entry.e_permalink_fragment = entry_meta.re_permalink;
+ }
+
+ auto upsert_res = regex101::client::upsert(entry);
+ auto upsert_info = upsert_res.unwrap();
+
+ if (get_meta_res.is<lnav::session::regex101::no_entry>()) {
+ lnav::session::regex101::insert_entry({
+ format_regex_pair.first->get_name().to_string(),
+ format_regex_pair.second->p_name.to_string(),
+ upsert_info.cr_permalink_fragment,
+ upsert_info.cr_delete_code,
+ });
+ }
+
+ return {
+ console::user_message::ok(
+ attr_line_t("pushed regex to -- ")
+ .append(lnav::roles::file(regex101::client::to_edit_url(
+ upsert_info.cr_permalink_fragment))))
+ .with_help(attr_line_t("use the ")
+ .append_quoted("pull"_keyword)
+ .append(" subcommand to update the format after "
+ "you make changes on regex101.com")),
+ };
+ }
+
+ static perform_result_t regex101_delete_action(const subcmd_format_t& sf)
+ {
+ auto get_res
+ = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
+
+ return get_res.match(
+ [&sf](
+ const lnav::session::regex101::entry& en) -> perform_result_t {
+ {
+ auto validate_res = sf.validate_external_format();
+
+ if (validate_res.isOk()) {
+ auto ppath = regex101::patch_path(validate_res.unwrap(),
+ en.re_permalink);
+
+ if (ghc::filesystem::exists(ppath)) {
+ return {
+ console::user_message::error(
+ attr_line_t("cannot delete regex101 entry "
+ "while patch file exists"))
+ .with_note(attr_line_t(" ").append(
+ lnav::roles::file(ppath.string())))
+ .with_help(attr_line_t(
+ "move the contents of the patch file "
+ "to the main log format and then "
+ "delete the file to continue")),
+ };
+ }
+ }
+ }
+
+ perform_result_t retval;
+ if (en.re_delete_code.empty()) {
+ retval.emplace_back(
+ console::user_message::warning(
+ attr_line_t("not deleting regex101 entry ")
+ .append_quoted(
+ lnav::roles::symbol(en.re_permalink)))
+ .with_reason(
+ "delete code is not known for this entry")
+ .with_note(
+ "formats created by importing a regex101.com "
+ "entry will not have a delete code"));
+ } else {
+ auto delete_res
+ = regex101::client::delete_entry(en.re_delete_code);
+
+ if (delete_res.isErr()) {
+ return {
+ console::user_message::error(
+ "unable to delete regex101 entry")
+ .with_reason(delete_res.unwrapErr()),
+ };
+ }
+ }
+
+ lnav::session::regex101::delete_entry(sf.sf_name,
+ sf.sf_regex_name);
+
+ retval.emplace_back(console::user_message::ok(
+ attr_line_t("deleted regex101 entry: ")
+ .append(lnav::roles::symbol(en.re_permalink))));
+
+ return retval;
+ },
+ [&sf](
+ const lnav::session::regex101::no_entry&) -> perform_result_t {
+ return {
+ console::user_message::error(
+ attr_line_t("no regex101 entry for ")
+ .append(lnav::roles::symbol(sf.sf_name))
+ .append("/")
+ .append(lnav::roles::symbol(sf.sf_regex_name))),
+ };
+ },
+ [&sf](
+ const lnav::session::regex101::error& err) -> perform_result_t {
+ return {
+ console::user_message::error(
+ attr_line_t("unable to get regex101 entry for ")
+ .append(lnav::roles::symbol(sf.sf_name))
+ .append("/")
+ .append(lnav::roles::symbol(sf.sf_regex_name)))
+ .with_reason(err.e_msg),
+ };
+ });
+ }
+};
+
+struct subcmd_regex101_t {
+ using action_t = std::function<perform_result_t(const subcmd_regex101_t&)>;
+
+ CLI::App* sr_app{nullptr};
+ action_t sr_action;
+ std::string sr_import_url;
+ std::string sr_import_name;
+ std::string sr_import_regex_name{"std"};
+
+ subcmd_regex101_t& set_action(action_t act)
+ {
+ if (!this->sr_action) {
+ this->sr_action = std::move(act);
+ }
+ return *this;
+ }
+
+ static perform_result_t default_action(const subcmd_regex101_t& sr)
+ {
+ auto um = console::user_message::error(
+ "expecting an operation related to the regex101.com integration");
+ um.with_help(
+ sr.sr_app->get_subcommands({})
+ | lnav::itertools::fold(
+ subcmd_reducer, attr_line_t{"the available operations are:"}));
+
+ return {um};
+ }
+
+ static perform_result_t list_action(const subcmd_regex101_t&)
+ {
+ auto get_res = lnav::session::regex101::get_entries();
+
+ if (get_res.isErr()) {
+ return {
+ console::user_message::error(
+ "unable to read regex101 entries from DB")
+ .with_reason(get_res.unwrapErr()),
+ };
+ }
+
+ auto entries
+ = get_res.unwrap() | lnav::itertools::map([](const auto& elem) {
+ return fmt::format(
+ FMT_STRING(" format {} regex {} regex101\n"),
+ elem.re_format_name,
+ elem.re_regex_name);
+ })
+ | lnav::itertools::fold(
+ [](const auto& elem, auto& accum) {
+ return accum.append(elem);
+ },
+ attr_line_t{});
+
+ auto um = console::user_message::ok(
+ entries.add_header("the following regex101 entries were found:\n")
+ .with_default("no regex101 entries found"));
+
+ return {um};
+ }
+
+ static perform_result_t import_action(const subcmd_regex101_t& sr)
+ {
+ auto import_res = regex101::import(
+ sr.sr_import_url, sr.sr_import_name, sr.sr_import_regex_name);
+
+ if (import_res.isOk()) {
+ return {
+ lnav::console::user_message::ok(
+ attr_line_t("converted regex101 entry to format file: ")
+ .append(lnav::roles::file(import_res.unwrap())))
+ .with_note("the converted format may still have errors")
+ .with_help(
+ attr_line_t(
+ "use the following command to patch the regex as "
+ "more changes are made on regex101.com:\n")
+ .appendf(FMT_STRING(" lnav -m format {} regex {} "
+ "regex101 pull"),
+ sr.sr_import_name,
+ sr.sr_import_regex_name)),
+ };
+ }
+
+ return {
+ import_res.unwrapErr(),
+ };
+ }
+};
+
+using operations_v
+ = mapbox::util::variant<no_subcmd_t, subcmd_format_t, subcmd_regex101_t>;
+
+class operations {
+public:
+ operations_v o_ops;
+};
+
+std::shared_ptr<operations>
+describe_cli(CLI::App& app, int argc, char* argv[])
+{
+ auto retval = std::make_shared<operations>();
+
+ retval->o_ops = no_subcmd_t{
+ &app,
+ };
+
+ app.add_flag("-m", "Switch to the management CLI mode.");
+
+ subcmd_format_t format_args;
+ subcmd_regex101_t regex101_args;
+
+ {
+ auto* subcmd_format
+ = app.add_subcommand("format",
+ "perform operations on log file formats")
+ ->callback([&]() {
+ format_args.set_action(subcmd_format_t::default_action);
+ retval->o_ops = format_args;
+ });
+ format_args.sf_format_app = subcmd_format;
+ subcmd_format
+ ->add_option(
+ "format_name", format_args.sf_name, "the name of the format")
+ ->expected(1);
+
+ {
+ subcmd_format
+ ->add_subcommand("get", "print information about a format")
+ ->callback([&]() {
+ format_args.set_action(subcmd_format_t::get_action);
+ });
+ }
+
+ {
+ subcmd_format
+ ->add_subcommand("source",
+ "print the path of the first source file "
+ "containing this format")
+ ->callback([&]() {
+ format_args.set_action(subcmd_format_t::source_action);
+ });
+ }
+
+ {
+ subcmd_format
+ ->add_subcommand("sources",
+ "print the paths of all source files "
+ "containing this format")
+ ->callback([&]() {
+ format_args.set_action(subcmd_format_t::sources_action);
+ });
+ }
+
+ {
+ auto* subcmd_format_regex
+ = subcmd_format
+ ->add_subcommand(
+ "regex",
+ "operate on the format's regular expressions")
+ ->callback([&]() {
+ format_args.set_action(
+ subcmd_format_t::default_regex_action);
+ });
+ format_args.sf_regex_app = subcmd_format_regex;
+ subcmd_format_regex->add_option(
+ "regex-name",
+ format_args.sf_regex_name,
+ "the name of the regular expression to operate on");
+
+ {
+ auto* subcmd_format_regex_regex101
+ = subcmd_format_regex
+ ->add_subcommand("regex101",
+ "use regex101.com to edit this "
+ "regular expression")
+ ->callback([&]() {
+ format_args.set_action(
+ subcmd_format_t::regex101_default_action);
+ });
+ format_args.sf_regex101_app = subcmd_format_regex_regex101;
+
+ {
+ subcmd_format_regex_regex101
+ ->add_subcommand("push",
+ "create/update an entry for "
+ "this regex on regex101.com")
+ ->callback([&]() {
+ format_args.set_action(
+ subcmd_format_t::regex101_push_action);
+ });
+ subcmd_format_regex_regex101
+ ->add_subcommand(
+ "pull",
+ "create a patch format file for this "
+ "regular expression based on the entry in "
+ "regex101.com")
+ ->callback([&]() {
+ format_args.set_action(
+ subcmd_format_t::regex101_pull_action);
+ });
+ subcmd_format_regex_regex101
+ ->add_subcommand(
+ "delete",
+ "delete the entry regex101.com that was "
+ "created by a push operation")
+ ->callback([&]() {
+ format_args.set_action(
+ subcmd_format_t::regex101_delete_action);
+ });
+ }
+ }
+ }
+ }
+
+ {
+ auto* subcmd_regex101
+ = app.add_subcommand("regex101",
+ "create and edit log message regular "
+ "expressions using regex101.com")
+ ->callback([&]() {
+ regex101_args.set_action(
+ subcmd_regex101_t::default_action);
+ retval->o_ops = regex101_args;
+ });
+ regex101_args.sr_app = subcmd_regex101;
+
+ {
+ subcmd_regex101
+ ->add_subcommand("list",
+ "list the log format regular expression "
+ "linked to entries on regex101.com")
+ ->callback([&]() {
+ regex101_args.set_action(subcmd_regex101_t::list_action);
+ });
+ }
+ {
+ auto* subcmd_regex101_import
+ = subcmd_regex101
+ ->add_subcommand("import",
+ "create a new format from a regular "
+ "expression on regex101.com")
+ ->callback([&]() {
+ regex101_args.set_action(
+ subcmd_regex101_t::import_action);
+ });
+
+ subcmd_regex101_import->add_option(
+ "url",
+ regex101_args.sr_import_url,
+ "The regex101.com url to construct a log format from");
+ subcmd_regex101_import->add_option("name",
+ regex101_args.sr_import_name,
+ "The name for the log format");
+ subcmd_regex101_import
+ ->add_option("regex-name",
+ regex101_args.sr_import_regex_name,
+ "The name for the new regex")
+ ->always_capture_default();
+ }
+ }
+
+ app.parse(argc, argv);
+
+ return retval;
+}
+
+perform_result_t
+perform(std::shared_ptr<operations> opts)
+{
+ return opts->o_ops.match(
+ [](const no_subcmd_t& ns) -> perform_result_t {
+ auto um = console::user_message::error(
+ attr_line_t("expecting an operation to perform"));
+ um.with_help(ns.ns_root_app->get_subcommands({})
+ | lnav::itertools::fold(
+ subcmd_reducer,
+ attr_line_t{"the available operations are:"}));
+
+ return {um};
+ },
+ [](const subcmd_format_t& sf) { return sf.sf_action(sf); },
+ [](const subcmd_regex101_t& sr) { return sr.sr_action(sr); });
+}
+
+} // namespace management
+} // namespace lnav
diff --git a/src/lnav.management_cli.hh b/src/lnav.management_cli.hh
new file mode 100644
index 0000000..41c2615
--- /dev/null
+++ b/src/lnav.management_cli.hh
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_management_cli_hh
+#define lnav_management_cli_hh
+
+#include <memory>
+#include <vector>
+
+#include "base/lnav.console.hh"
+#include "CLI/CLI.hpp"
+
+namespace lnav {
+namespace management {
+
+class operations;
+
+std::shared_ptr<operations> describe_cli(CLI::App& app, int argc, char* argv[]);
+
+using perform_result_t = std::vector<lnav::console::user_message>;
+
+perform_result_t perform(std::shared_ptr<operations> opts);
+
+} // namespace management
+} // namespace lnav
+
+#endif
diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc
new file mode 100644
index 0000000..d766d99
--- /dev/null
+++ b/src/lnav_commands.cc
@@ -0,0 +1,5769 @@
+/**
+ * Copyright (c) 2007-2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <fstream>
+#include <regex>
+#include <string>
+#include <unordered_map>
+#include <utility>
+#include <vector>
+
+#include "lnav.hh"
+
+#include <fnmatch.h>
+#include <glob.h>
+#include <sys/stat.h>
+#include <termios.h>
+
+#include "base/attr_line.builder.hh"
+#include "base/auto_mem.hh"
+#include "base/fs_util.hh"
+#include "base/humanize.network.hh"
+#include "base/injector.hh"
+#include "base/isc.hh"
+#include "base/itertools.hh"
+#include "base/paths.hh"
+#include "base/string_util.hh"
+#include "bound_tags.hh"
+#include "command_executor.hh"
+#include "config.h"
+#include "curl_looper.hh"
+#include "db_sub_source.hh"
+#include "field_overlay_source.hh"
+#include "fmt/printf.h"
+#include "lnav.indexing.hh"
+#include "lnav_commands.hh"
+#include "lnav_config.hh"
+#include "lnav_util.hh"
+#include "log_data_helper.hh"
+#include "log_data_table.hh"
+#include "log_search_table.hh"
+#include "log_search_table_fwd.hh"
+#include "readline_callbacks.hh"
+#include "readline_curses.hh"
+#include "readline_highlighters.hh"
+#include "readline_possibilities.hh"
+#include "relative_time.hh"
+#include "scn/scn.h"
+#include "service_tags.hh"
+#include "session.export.hh"
+#include "session_data.hh"
+#include "shlex.hh"
+#include "spectro_impls.hh"
+#include "sqlite-extension-func.hh"
+#include "sysclip.hh"
+#include "tailer/tailer.looper.hh"
+#include "text_anonymizer.hh"
+#include "url_loader.hh"
+#include "yajl/api/yajl_parse.h"
+#include "yajlpp/json_op.hh"
+#include "yajlpp/yajlpp.hh"
+
+using namespace lnav::roles::literals;
+
+static std::string
+remaining_args(const std::string& cmdline,
+ const std::vector<std::string>& args,
+ size_t index = 1)
+{
+ size_t start_pos = 0;
+
+ require(index > 0);
+
+ if (index >= args.size()) {
+ return "";
+ }
+ for (size_t lpc = 0; lpc < index; lpc++) {
+ start_pos += args[lpc].length();
+ }
+
+ size_t index_in_cmdline = cmdline.find(args[index], start_pos);
+
+ require(index_in_cmdline != std::string::npos);
+
+ return cmdline.substr(index_in_cmdline);
+}
+
+static string_fragment
+remaining_args_frag(const std::string& cmdline,
+ const std::vector<std::string>& args,
+ size_t index = 1)
+{
+ size_t start_pos = 0;
+
+ require(index > 0);
+
+ if (index >= args.size()) {
+ return string_fragment{};
+ }
+ for (size_t lpc = 0; lpc < index; lpc++) {
+ start_pos += args[lpc].length();
+ }
+
+ size_t index_in_cmdline = cmdline.find(args[index], start_pos);
+
+ require(index_in_cmdline != std::string::npos);
+
+ return string_fragment::from_str_range(
+ cmdline, index_in_cmdline, cmdline.size());
+}
+
+static nonstd::optional<std::string>
+find_arg(std::vector<std::string>& args, const std::string& flag)
+{
+ auto iter = find_if(args.begin(), args.end(), [&flag](const auto elem) {
+ return startswith(elem, flag);
+ });
+
+ if (iter == args.end()) {
+ return nonstd::nullopt;
+ }
+
+ auto index = iter->find('=');
+ if (index == std::string::npos) {
+ return "";
+ }
+
+ auto retval = iter->substr(index + 1);
+
+ args.erase(iter);
+
+ return retval;
+}
+
+static bookmark_vector<vis_line_t>
+combined_user_marks(vis_bookmarks& vb)
+{
+ const auto& bv = vb[&textview_curses::BM_USER];
+ const auto& bv_expr = vb[&textview_curses::BM_USER_EXPR];
+ bookmark_vector<vis_line_t> retval;
+
+ for (const auto& row : bv) {
+ retval.insert_once(row);
+ }
+ for (const auto& row : bv_expr) {
+ retval.insert_once(row);
+ }
+ return retval;
+}
+
+static Result<std::string, lnav::console::user_message>
+com_adjust_log_time(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("line-time");
+ } else if (lnav_data.ld_views[LNV_LOG].get_inner_height() == 0) {
+ return ec.make_error("no log messages");
+ } else if (args.size() >= 2) {
+ auto& lss = lnav_data.ld_log_source;
+ struct timeval top_time, time_diff;
+ struct timeval new_time = {0, 0};
+ content_line_t top_content;
+ date_time_scanner dts;
+ vis_line_t top_line;
+ struct exttm tm;
+ struct tm base_tm;
+ std::shared_ptr<logfile> lf;
+
+ top_line = lnav_data.ld_views[LNV_LOG].get_selection();
+ top_content = lss.at(top_line);
+ lf = lss.find(top_content);
+
+ auto& ll = (*lf)[top_content];
+
+ top_time = ll.get_timeval();
+ localtime_r(&top_time.tv_sec, &base_tm);
+
+ dts.set_base_time(top_time.tv_sec, base_tm);
+ args[1] = remaining_args(cmdline, args);
+
+ auto parse_res = relative_time::from_str(args[1]);
+ if (parse_res.isOk()) {
+ new_time = parse_res.unwrap().adjust(top_time).to_timeval();
+ } else if (dts.scan(
+ args[1].c_str(), args[1].size(), nullptr, &tm, new_time)
+ != nullptr)
+ {
+ // nothing to do
+ } else {
+ return ec.make_error("could not parse timestamp -- {}", args[1]);
+ }
+
+ timersub(&new_time, &top_time, &time_diff);
+ if (ec.ec_dry_run) {
+ char buffer[1024];
+
+ snprintf(
+ buffer,
+ sizeof(buffer),
+ "info: log timestamps will be adjusted by %ld.%06ld seconds",
+ time_diff.tv_sec,
+ (long) time_diff.tv_usec);
+
+ retval = buffer;
+ } else {
+ lf->adjust_content_time(top_content, time_diff, false);
+
+ lss.set_force_rebuild();
+
+ retval = "info: adjusted time";
+ }
+ } else {
+ return ec.make_error("expecting new time value");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_unix_time(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (args.size() >= 2) {
+ bool parsed = false;
+ struct tm log_time;
+ time_t u_time;
+ size_t millis;
+ char* rest;
+
+ u_time = time(nullptr);
+ log_time = *localtime(&u_time);
+
+ log_time.tm_isdst = -1;
+
+ args[1] = remaining_args(cmdline, args);
+ if ((millis = args[1].find('.')) != std::string::npos
+ || (millis = args[1].find(',')) != std::string::npos)
+ {
+ args[1] = args[1].erase(millis, 4);
+ }
+ if (((rest = strptime(args[1].c_str(), "%b %d %H:%M:%S %Y", &log_time))
+ != nullptr
+ && (rest - args[1].c_str()) >= 20)
+ || ((rest
+ = strptime(args[1].c_str(), "%Y-%m-%d %H:%M:%S", &log_time))
+ != nullptr
+ && (rest - args[1].c_str()) >= 19))
+ {
+ u_time = mktime(&log_time);
+ parsed = true;
+ } else if (sscanf(args[1].c_str(), "%ld", &u_time)) {
+ log_time = *localtime(&u_time);
+
+ parsed = true;
+ }
+ if (parsed) {
+ char ftime[128];
+ int len;
+
+ strftime(ftime,
+ sizeof(ftime),
+ "%a %b %d %H:%M:%S %Y %z %Z",
+ localtime(&u_time));
+ len = strlen(ftime);
+ snprintf(ftime + len, sizeof(ftime) - len, " -- %ld", u_time);
+ retval = std::string(ftime);
+ } else {
+ return ec.make_error("invalid unix time -- {}", args[1]);
+ }
+ } else {
+ return ec.make_error("expecting a unix time value");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_current_time(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ char ftime[128];
+ struct tm localtm;
+ std::string retval;
+ time_t u_time;
+ size_t len;
+
+ memset(&localtm, 0, sizeof(localtm));
+ u_time = time(nullptr);
+ strftime(ftime,
+ sizeof(ftime),
+ "%a %b %d %H:%M:%S %Y %z %Z",
+ localtime_r(&u_time, &localtm));
+ len = strlen(ftime);
+ snprintf(ftime + len, sizeof(ftime) - len, " -- %ld", u_time);
+ retval = std::string(ftime);
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ static const char* INTERACTIVE_FMTS[] = {
+ "%B %e %H:%M:%S",
+ "%b %e %H:%M:%S",
+ "%B %e %H:%M",
+ "%b %e %H:%M",
+ "%B %e %I:%M%p",
+ "%b %e %I:%M%p",
+ "%B %e %I%p",
+ "%b %e %I%p",
+ "%B %e",
+ "%b %e",
+ nullptr,
+ };
+
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("move-args");
+ } else if (args.size() > 1) {
+ std::string all_args = remaining_args(cmdline, args);
+ auto* tc = *lnav_data.ld_view_stack.top();
+ nonstd::optional<vis_line_t> dst_vl;
+
+ if (startswith(all_args, "#")) {
+ auto* ta = dynamic_cast<text_anchors*>(tc->get_sub_source());
+
+ if (ta == nullptr) {
+ return ec.make_error("view does not support anchor links");
+ }
+
+ dst_vl = ta->row_for_anchor(all_args);
+ if (!dst_vl) {
+ return ec.make_error("unable to find anchor: {}", all_args);
+ }
+ }
+
+ auto* ttt = dynamic_cast<text_time_translator*>(tc->get_sub_source());
+ int line_number, consumed;
+ date_time_scanner dts;
+ const char* scan_end = nullptr;
+ struct timeval tv;
+ struct exttm tm;
+ float value;
+ auto parse_res = relative_time::from_str(all_args);
+
+ if (ttt != nullptr && tc->get_inner_height() > 0_vl) {
+ auto top_time_opt = ttt->time_for_row(tc->get_selection());
+
+ if (top_time_opt) {
+ auto top_time_tv = top_time_opt.value();
+ struct tm top_tm;
+
+ localtime_r(&top_time_tv.tv_sec, &top_tm);
+ dts.set_base_time(top_time_tv.tv_sec, top_tm);
+ }
+ }
+
+ if (dst_vl) {
+ } else if (parse_res.isOk()) {
+ if (ttt == nullptr) {
+ return ec.make_error(
+ "relative time values only work in a time-indexed view");
+ }
+ if (tc->get_inner_height() == 0_vl) {
+ return ec.make_error("view is empty");
+ }
+ auto tv_opt = ttt->time_for_row(tc->get_selection());
+ if (!tv_opt) {
+ return ec.make_error("cannot get time for the top row");
+ }
+ tv = tv_opt.value();
+
+ vis_line_t vl = tc->get_selection(), new_vl;
+ bool done = false;
+ auto rt = parse_res.unwrap();
+
+ if (rt.is_relative()) {
+ injector::get<relative_time&, last_relative_time_tag>() = rt;
+ }
+
+ do {
+ auto tm = rt.adjust(tv);
+
+ tv = tm.to_timeval();
+ auto new_vl_opt = ttt->row_for_time(tv);
+ if (!new_vl_opt) {
+ break;
+ }
+
+ new_vl = new_vl_opt.value();
+ if (new_vl == 0_vl || new_vl != vl || !rt.is_relative()) {
+ vl = new_vl;
+ done = true;
+ }
+ } while (!done);
+
+ dst_vl = vl;
+
+ if (!ec.ec_dry_run && !rt.is_absolute()
+ && lnav_data.ld_rl_view != nullptr)
+ {
+ lnav_data.ld_rl_view->set_alt_value(HELP_MSG_2(
+ r, R, "to move forward/backward the same amount of time"));
+ }
+ } else if ((scan_end = dts.scan(
+ all_args.c_str(), all_args.size(), nullptr, &tm, tv))
+ != nullptr
+ || (scan_end = dts.scan(all_args.c_str(),
+ all_args.size(),
+ INTERACTIVE_FMTS,
+ &tm,
+ tv))
+ != nullptr)
+ {
+ if (ttt == nullptr) {
+ return ec.make_error(
+ "time values only work in a time-indexed view");
+ }
+
+ size_t matched_size = scan_end - all_args.c_str();
+ if (matched_size != all_args.size()) {
+ auto um
+ = lnav::console::user_message::error(
+ attr_line_t("invalid timestamp: ").append(all_args))
+ .with_reason(
+ attr_line_t("the leading part of the timestamp "
+ "was matched, however, the trailing "
+ "text ")
+ .append_quoted(scan_end)
+ .append(" was not"))
+ .with_snippets(ec.ec_source)
+ .with_note(
+ attr_line_t("input matched time format ")
+ .append_quoted(
+ PTIMEC_FORMATS[dts.dts_fmt_lock].pf_fmt))
+ .with_help(
+ "fix the timestamp or remove the trailing text");
+
+ auto unmatched_size = all_args.size() - matched_size;
+ auto& snippet_copy = um.um_snippets.back();
+ attr_line_builder alb(snippet_copy.s_content);
+
+ alb.append("\n")
+ .append(1 + cmdline.find(all_args), ' ')
+ .append(matched_size, ' ');
+ {
+ auto attr_guard
+ = alb.with_attr(VC_ROLE.value(role_t::VCR_COMMENT));
+
+ alb.append("^");
+ if (unmatched_size > 1) {
+ alb.append(unmatched_size - 2, '-').append("^");
+ }
+ alb.append(" unrecognized input");
+ }
+ return Err(um);
+ }
+
+ if (!(tm.et_flags & ETF_DAY_SET)) {
+ tm.et_tm.tm_mday = 1;
+ }
+ if (!(tm.et_flags & ETF_HOUR_SET)) {
+ tm.et_tm.tm_hour = 0;
+ }
+ if (!(tm.et_flags & ETF_MINUTE_SET)) {
+ tm.et_tm.tm_min = 0;
+ }
+ if (!(tm.et_flags & ETF_SECOND_SET)) {
+ tm.et_tm.tm_sec = 0;
+ }
+ if (!(tm.et_flags & ETF_MICROS_SET)
+ && !(tm.et_flags & ETF_MILLIS_SET))
+ {
+ tm.et_nsec = 0;
+ }
+ tv.tv_sec = tm2sec(&tm.et_tm);
+ tv.tv_usec = tm.et_nsec / 1000;
+ dst_vl = ttt->row_for_time(tv);
+ } else if (sscanf(args[1].c_str(), "%f%n", &value, &consumed) == 1) {
+ if (args[1][consumed] == '%') {
+ line_number
+ = (int) ((double) tc->get_inner_height() * (value / 100.0));
+ } else {
+ line_number = (int) value;
+ if (line_number < 0) {
+ line_number = tc->get_inner_height() + line_number;
+ }
+ }
+
+ dst_vl = vis_line_t(line_number);
+ } else {
+ auto um = lnav::console::user_message::error(
+ attr_line_t("invalid argument: ").append(args[1]))
+ .with_reason(
+ "expecting line number/percentage, timestamp, or "
+ "relative time");
+ ec.add_error_context(um);
+ return Err(um);
+ }
+
+ dst_vl | [&ec, tc, &retval](auto new_top) {
+ if (ec.ec_dry_run) {
+ retval = "info: will move to line "
+ + std::to_string((int) new_top);
+ } else {
+ tc->get_sub_source()->get_location_history() |
+ [new_top](auto lh) { lh->loc_history_append(new_top); };
+ tc->set_selection(new_top);
+
+ retval = "";
+ }
+ };
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_relative_goto(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval = "error: ";
+
+ if (args.empty()) {
+ } else if (args.size() > 1) {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ int line_offset, consumed;
+ float value;
+
+ if (sscanf(args[1].c_str(), "%f%n", &value, &consumed) == 1) {
+ if (args[1][consumed] == '%') {
+ line_offset
+ = (int) ((double) tc->get_inner_height() * (value / 100.0));
+ } else {
+ line_offset = (int) value;
+ }
+
+ if (ec.ec_dry_run) {
+ retval = "info: shifting top by " + std::to_string(line_offset)
+ + " lines";
+ } else {
+ tc->shift_top(vis_line_t(line_offset), true);
+
+ retval = "";
+ }
+ } else {
+ return ec.make_error("invalid line number -- {}", args[1]);
+ }
+ } else {
+ return ec.make_error("expecting line number/percentage");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_mark(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty() || lnav_data.ld_view_stack.empty()) {
+ } else if (!ec.ec_dry_run) {
+ auto* tc = *lnav_data.ld_view_stack.top();
+ lnav_data.ld_last_user_mark[tc] = tc->get_selection();
+ tc->toggle_user_mark(&textview_curses::BM_USER,
+ vis_line_t(lnav_data.ld_last_user_mark[tc]));
+ tc->reload_data();
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_mark_expr(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty() || lnav_data.ld_view_stack.empty()) {
+ args.emplace_back("filter-expr-syms");
+ } else if (args.size() < 2) {
+ return ec.make_error("expecting an SQL expression");
+ } else if (*lnav_data.ld_view_stack.top() != &lnav_data.ld_views[LNV_LOG]) {
+ return ec.make_error(":mark-expr is only supported for the LOG view");
+ } else {
+ auto expr = remaining_args(cmdline, args);
+ auto stmt_str = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), expr);
+
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+#ifdef SQLITE_PREPARE_PERSISTENT
+ auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
+ stmt_str.c_str(),
+ stmt_str.size(),
+ SQLITE_PREPARE_PERSISTENT,
+ stmt.out(),
+ nullptr);
+#else
+ auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
+ stmt_str.c_str(),
+ stmt_str.size(),
+ stmt.out(),
+ nullptr);
+#endif
+ if (retcode != SQLITE_OK) {
+ const char* errmsg = sqlite3_errmsg(lnav_data.ld_db);
+ auto expr_al = attr_line_t(expr).with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ readline_sqlite_highlighter(expr_al, -1);
+ auto um
+ = lnav::console::user_message::error(
+ attr_line_t("invalid mark expression: ").append(expr_al))
+ .with_reason(errmsg)
+ .with_snippets(ec.ec_source);
+
+ return Err(um);
+ }
+
+ auto& lss = lnav_data.ld_log_source;
+ if (ec.ec_dry_run) {
+ auto set_res = lss.set_preview_sql_filter(stmt.release());
+
+ if (set_res.isErr()) {
+ return Err(set_res.unwrapErr());
+ }
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "Matches are highlighted in the text view");
+ } else {
+ auto set_res = lss.set_sql_marker(expr, stmt.release());
+
+ if (set_res.isErr()) {
+ return Err(set_res.unwrapErr());
+ }
+ }
+ }
+
+ return Ok(retval);
+}
+
+static std::string
+com_mark_expr_prompt(exec_context& ec, const std::string& cmdline)
+{
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ return "";
+ }
+
+ return fmt::format(FMT_STRING("{} {}"),
+ trim(cmdline),
+ trim(lnav_data.ld_log_source.get_sql_marker_text()));
+}
+
+static Result<std::string, lnav::console::user_message>
+com_clear_mark_expr(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else {
+ if (!ec.ec_dry_run) {
+ lnav_data.ld_log_source.set_sql_marker("", nullptr);
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_goto_mark(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("mark-type");
+ } else {
+ static const std::set<const bookmark_type_t*> DEFAULT_TYPES = {
+ &textview_curses::BM_USER,
+ &textview_curses::BM_USER_EXPR,
+ &textview_curses::BM_META,
+ };
+
+ textview_curses* tc = get_textview_for_mode(lnav_data.ld_mode);
+ std::set<const bookmark_type_t*> mark_types;
+
+ if (args.size() > 1) {
+ for (size_t lpc = 1; lpc < args.size(); lpc++) {
+ auto bt_opt = bookmark_type_t::find_type(args[lpc]);
+ if (!bt_opt) {
+ auto um
+ = lnav::console::user_message::error(
+ attr_line_t("unknown bookmark type: ")
+ .append(args[lpc]))
+ .with_snippets(ec.ec_source)
+ .with_help(
+ attr_line_t("available types: ")
+ .join(bookmark_type_t::get_all_types()
+ | lnav::itertools::map(
+ &bookmark_type_t::get_name)
+ | lnav::itertools::sorted(),
+ ", "));
+ return Err(um);
+ }
+ mark_types.insert(bt_opt.value());
+ }
+ } else {
+ mark_types = DEFAULT_TYPES;
+ }
+
+ if (!ec.ec_dry_run) {
+ nonstd::optional<vis_line_t> new_top;
+
+ if (args[0] == "next-mark") {
+ auto search_from_top = search_forward_from(tc);
+
+ for (const auto& bt : mark_types) {
+ auto bt_top
+ = next_cluster(&bookmark_vector<vis_line_t>::next,
+ bt,
+ search_from_top);
+
+ if (bt_top && (!new_top || bt_top < new_top.value())) {
+ new_top = bt_top;
+ }
+ }
+
+ if (!new_top) {
+ auto um = lnav::console::user_message::info(fmt::format(
+ FMT_STRING("no more {} bookmarks after here"),
+ fmt::join(mark_types
+ | lnav::itertools::map(
+ &bookmark_type_t::get_name),
+ ", ")));
+
+ return Err(um);
+ }
+ } else {
+ for (const auto& bt : mark_types) {
+ auto bt_top
+ = next_cluster(&bookmark_vector<vis_line_t>::prev,
+ bt,
+ tc->get_selection());
+
+ if (bt_top && (!new_top || bt_top > new_top.value())) {
+ new_top = bt_top;
+ }
+ }
+
+ if (!new_top) {
+ auto um = lnav::console::user_message::info(fmt::format(
+ FMT_STRING("no more {} bookmarks before here"),
+ fmt::join(mark_types
+ | lnav::itertools::map(
+ &bookmark_type_t::get_name),
+ ", ")));
+
+ return Err(um);
+ }
+ }
+
+ if (new_top) {
+ tc->get_sub_source()->get_location_history() |
+ [new_top](auto lh) {
+ lh->loc_history_append(new_top.value());
+ };
+ tc->set_selection(new_top.value());
+ }
+ lnav_data.ld_bottom_source.grep_error("");
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_goto_location(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ lnav_data.ld_view_stack.top() | [&args](auto tc) {
+ tc->get_sub_source()->get_location_history() |
+ [tc, &args](auto lh) {
+ return args[0] == "prev-location"
+ ? lh->loc_history_back(tc->get_selection())
+ : lh->loc_history_forward(tc->get_selection());
+ }
+ | [tc](auto new_top) { tc->set_selection(new_top); };
+ };
+ }
+
+ return Ok(retval);
+}
+
+static bool
+csv_needs_quoting(const std::string& str)
+{
+ return (str.find_first_of(",\"\r\n") != std::string::npos);
+}
+
+static std::string
+csv_quote_string(const std::string& str)
+{
+ static const std::regex csv_column_quoter("\"");
+
+ std::string retval = std::regex_replace(str, csv_column_quoter, "\"\"");
+
+ retval.insert(0, 1, '\"');
+ retval.append(1, '\"');
+
+ return retval;
+}
+
+static void
+csv_write_string(FILE* outfile, const std::string& str)
+{
+ if (csv_needs_quoting(str)) {
+ std::string quoted_str = csv_quote_string(str);
+
+ fmt::fprintf(outfile, "%s", quoted_str);
+ } else {
+ fmt::fprintf(outfile, "%s", str);
+ }
+}
+
+static void
+yajl_writer(void* context, const char* str, size_t len)
+{
+ FILE* file = (FILE*) context;
+
+ fwrite(str, len, 1, file);
+}
+
+static void
+json_write_row(yajl_gen handle,
+ int row,
+ lnav::text_anonymizer& ta,
+ bool anonymize)
+{
+ auto& dls = lnav_data.ld_db_row_source;
+ yajlpp_map obj_map(handle);
+
+ for (size_t col = 0; col < dls.dls_headers.size(); col++) {
+ obj_map.gen(dls.dls_headers[col].hm_name);
+
+ if (dls.dls_rows[row][col] == db_label_source::NULL_STR) {
+ obj_map.gen();
+ continue;
+ }
+
+ auto& hm = dls.dls_headers[col];
+
+ switch (hm.hm_column_type) {
+ case SQLITE_FLOAT:
+ case SQLITE_INTEGER: {
+ auto len = strlen(dls.dls_rows[row][col]);
+
+ if (len == 0) {
+ obj_map.gen();
+ } else {
+ yajl_gen_number(handle, dls.dls_rows[row][col], len);
+ }
+ break;
+ }
+ case SQLITE_TEXT:
+ switch (hm.hm_sub_type) {
+ case 74: {
+ auto_mem<yajl_handle_t> parse_handle(yajl_free);
+ unsigned char* err;
+ json_ptr jp("");
+ json_op jo(jp);
+
+ jo.jo_ptr_callbacks = json_op::gen_callbacks;
+ jo.jo_ptr_data = handle;
+ parse_handle.reset(
+ yajl_alloc(&json_op::ptr_callbacks, nullptr, &jo));
+
+ const unsigned char* json_in
+ = (const unsigned char*) dls.dls_rows[row][col];
+ switch (yajl_parse(parse_handle.in(),
+ json_in,
+ strlen((const char*) json_in)))
+ {
+ case yajl_status_error:
+ case yajl_status_client_canceled: {
+ err = yajl_get_error(
+ parse_handle.in(),
+ 0,
+ json_in,
+ strlen((const char*) json_in));
+ log_error("unable to parse JSON cell: %s", err);
+ obj_map.gen(dls.dls_rows[row][col]);
+ yajl_free_error(parse_handle.in(), err);
+ return;
+ }
+ default:
+ break;
+ }
+
+ switch (yajl_complete_parse(parse_handle.in())) {
+ case yajl_status_error:
+ case yajl_status_client_canceled: {
+ err = yajl_get_error(
+ parse_handle.in(),
+ 0,
+ json_in,
+ strlen((const char*) json_in));
+ log_error("unable to parse JSON cell: %s", err);
+ obj_map.gen(dls.dls_rows[row][col]);
+ yajl_free_error(parse_handle.in(), err);
+ return;
+ }
+ default:
+ break;
+ }
+ break;
+ }
+ default:
+ obj_map.gen(anonymize
+ ? ta.next(string_fragment::from_c_str(
+ dls.dls_rows[row][col]))
+ : dls.dls_rows[row][col]);
+ break;
+ }
+ break;
+ default:
+ obj_map.gen(anonymize ? ta.next(string_fragment::from_c_str(
+ dls.dls_rows[row][col]))
+ : dls.dls_rows[row][col]);
+ break;
+ }
+ }
+}
+
+static Result<std::string, lnav::console::user_message>
+com_save_to(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ FILE *outfile = nullptr, *toclose = nullptr;
+ const char* mode = "";
+ std::string fn, retval;
+ bool to_term = false;
+ bool anonymize = false;
+ int (*closer)(FILE*) = fclose;
+
+ if (args.empty()) {
+ args.emplace_back("filename");
+ return Ok(std::string());
+ }
+
+ fn = trim(remaining_args(cmdline, args));
+
+ std::vector<std::string> split_args;
+ shlex lexer(fn);
+
+ if (!lexer.split(split_args, ec.create_resolver())) {
+ return ec.make_error("unable to parse arguments");
+ }
+
+ auto anon_iter
+ = std::find(split_args.begin(), split_args.end(), "--anonymize");
+ if (anon_iter != split_args.end()) {
+ split_args.erase(anon_iter);
+ anonymize = true;
+ }
+
+ auto* tc = *lnav_data.ld_view_stack.top();
+ auto opt_view_name = find_arg(split_args, "--view");
+ if (opt_view_name) {
+ auto opt_view_index = view_from_string(opt_view_name->c_str());
+
+ if (!opt_view_index) {
+ return ec.make_error("invalid view name: {}", *opt_view_name);
+ }
+
+ tc = &lnav_data.ld_views[*opt_view_index];
+ }
+
+ if (split_args.empty()) {
+ return ec.make_error(
+ "expecting file name or '-' to write to the terminal");
+ }
+
+ if (split_args.size() > 1) {
+ return ec.make_error("more than one file name was matched");
+ }
+
+ if (args[0] == "append-to") {
+ mode = "a";
+ } else {
+ mode = "w";
+ }
+
+ auto& dls = lnav_data.ld_db_row_source;
+ bookmark_vector<vis_line_t> all_user_marks;
+ lnav::text_anonymizer ta;
+
+ if (args[0] == "write-csv-to" || args[0] == "write-json-to"
+ || args[0] == "write-jsonlines-to" || args[0] == "write-cols-to"
+ || args[0] == "write-table-to")
+ {
+ if (dls.dls_headers.empty()) {
+ return ec.make_error(
+ "no query result to write, use ';' to execute a query");
+ }
+ } else if (args[0] == "write-raw-to" && tc == &lnav_data.ld_views[LNV_DB]) {
+ } else if (args[0] != "write-screen-to" && args[0] != "write-view-to") {
+ all_user_marks = combined_user_marks(tc->get_bookmarks());
+ if (all_user_marks.empty()) {
+ return ec.make_error(
+ "no lines marked to write, use 'm' to mark lines");
+ }
+ }
+
+ if (ec.ec_dry_run) {
+ outfile = tmpfile();
+ toclose = outfile;
+ } else if (split_args[0] == "-" || split_args[0] == "/dev/stdout") {
+ auto ec_out = ec.get_output();
+
+ if (!ec_out) {
+ outfile = stdout;
+ nodelay(lnav_data.ld_window, 0);
+ endwin();
+ struct termios curr_termios;
+ tcgetattr(1, &curr_termios);
+ curr_termios.c_oflag |= ONLCR | OPOST;
+ tcsetattr(1, TCSANOW, &curr_termios);
+ setvbuf(stdout, nullptr, _IONBF, 0);
+ to_term = true;
+ fprintf(outfile,
+ "\n---------------- Press any key to exit lo-fi display "
+ "----------------\n\n");
+ } else {
+ outfile = *ec_out;
+ }
+ if (outfile == stdout) {
+ lnav_data.ld_stdout_used = true;
+ }
+ } else if (split_args[0] == "/dev/clipboard") {
+ auto open_res = sysclip::open(sysclip::type_t::GENERAL);
+ if (open_res.isErr()) {
+ alerter::singleton().chime("cannot open clipboard");
+ return ec.make_error("Unable to copy to clipboard: {}",
+ open_res.unwrapErr());
+ }
+ auto holder = open_res.unwrap();
+ toclose = outfile = holder.release();
+ closer = holder.get_free_func<int (*)(FILE*)>();
+ } else if (lnav_data.ld_flags & LNF_SECURE_MODE) {
+ return ec.make_error("{} -- unavailable in secure mode", args[0]);
+ } else if ((outfile = fopen(split_args[0].c_str(), mode)) == nullptr) {
+ return ec.make_error("unable to open file -- {}", split_args[0]);
+ } else {
+ toclose = outfile;
+ }
+
+ int line_count = 0;
+
+ if (args[0] == "write-csv-to") {
+ std::vector<std::vector<const char*>>::iterator row_iter;
+ std::vector<const char*>::iterator iter;
+ std::vector<db_label_source::header_meta>::iterator hdr_iter;
+ bool first = true;
+
+ for (hdr_iter = dls.dls_headers.begin();
+ hdr_iter != dls.dls_headers.end();
+ ++hdr_iter)
+ {
+ if (!first) {
+ fprintf(outfile, ",");
+ }
+ csv_write_string(outfile, hdr_iter->hm_name);
+ first = false;
+ }
+ fprintf(outfile, "\n");
+
+ for (row_iter = dls.dls_rows.begin(); row_iter != dls.dls_rows.end();
+ ++row_iter)
+ {
+ if (ec.ec_dry_run && distance(dls.dls_rows.begin(), row_iter) > 10)
+ {
+ break;
+ }
+
+ first = true;
+ for (iter = row_iter->begin(); iter != row_iter->end(); ++iter) {
+ if (!first) {
+ fprintf(outfile, ",");
+ }
+ csv_write_string(
+ outfile,
+ anonymize ? ta.next(string_fragment::from_c_str(*iter))
+ : *iter);
+ first = false;
+ }
+ fprintf(outfile, "\n");
+
+ line_count += 1;
+ }
+ } else if (args[0] == "write-cols-to" || args[0] == "write-table-to") {
+ bool first = true;
+
+ fprintf(outfile, "\u250f");
+ for (const auto& hdr : dls.dls_headers) {
+ auto cell_line = repeat("\u2501", hdr.hm_column_size);
+
+ if (!first) {
+ fprintf(outfile, "\u2533");
+ }
+ fprintf(outfile, "%s", cell_line.c_str());
+ first = false;
+ }
+ fprintf(outfile, "\u2513\n");
+
+ for (const auto& hdr : dls.dls_headers) {
+ auto centered_hdr = center_str(hdr.hm_name, hdr.hm_column_size);
+
+ fprintf(outfile, "\u2503");
+ fprintf(outfile, "%s", centered_hdr.c_str());
+ }
+ fprintf(outfile, "\u2503\n");
+
+ first = true;
+ fprintf(outfile, "\u2521");
+ for (const auto& hdr : dls.dls_headers) {
+ auto cell_line = repeat("\u2501", hdr.hm_column_size);
+
+ if (!first) {
+ fprintf(outfile, "\u2547");
+ }
+ fprintf(outfile, "%s", cell_line.c_str());
+ first = false;
+ }
+ fprintf(outfile, "\u2529\n");
+
+ for (size_t row = 0; row < dls.text_line_count(); row++) {
+ if (ec.ec_dry_run && row > 10) {
+ break;
+ }
+
+ for (size_t col = 0; col < dls.dls_headers.size(); col++) {
+ const auto& hdr = dls.dls_headers[col];
+
+ fprintf(outfile, "\u2502");
+
+ auto cell = std::string(dls.dls_rows[row][col]);
+ if (anonymize) {
+ cell = ta.next(cell);
+ }
+ auto cell_length
+ = utf8_string_length(cell).unwrapOr(cell.size());
+ auto padding = anonymize ? 1 : hdr.hm_column_size - cell_length;
+
+ if (hdr.hm_column_type != SQLITE3_TEXT) {
+ fprintf(outfile, "%s", std::string(padding, ' ').c_str());
+ }
+ fprintf(outfile, "%s", cell.c_str());
+ if (hdr.hm_column_type == SQLITE3_TEXT) {
+ fprintf(outfile, "%s", std::string(padding, ' ').c_str());
+ }
+ }
+ fprintf(outfile, "\u2502\n");
+
+ line_count += 1;
+ }
+
+ first = true;
+ fprintf(outfile, "\u2514");
+ for (const auto& hdr : dls.dls_headers) {
+ auto cell_line = repeat("\u2501", hdr.hm_column_size);
+
+ if (!first) {
+ fprintf(outfile, "\u2534");
+ }
+ fprintf(outfile, "%s", cell_line.c_str());
+ first = false;
+ }
+ fprintf(outfile, "\u2518\n");
+
+ } else if (args[0] == "write-json-to") {
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, 1);
+ yajl_gen_config(gen, yajl_gen_print_callback, yajl_writer, outfile);
+
+ {
+ yajlpp_array root_array(gen);
+
+ for (size_t row = 0; row < dls.dls_rows.size(); row++) {
+ if (ec.ec_dry_run && row > 10) {
+ break;
+ }
+
+ json_write_row(gen, row, ta, anonymize);
+ line_count += 1;
+ }
+ }
+ } else if (args[0] == "write-jsonlines-to") {
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, 0);
+ yajl_gen_config(gen, yajl_gen_print_callback, yajl_writer, outfile);
+
+ for (size_t row = 0; row < dls.dls_rows.size(); row++) {
+ if (ec.ec_dry_run && row > 10) {
+ break;
+ }
+
+ json_write_row(gen, row, ta, anonymize);
+ yajl_gen_reset(gen, "\n");
+ line_count += 1;
+ }
+ } else if (args[0] == "write-screen-to") {
+ bool wrapped = tc->get_word_wrap();
+ vis_line_t orig_top = tc->get_top();
+
+ tc->set_word_wrap(to_term);
+
+ vis_line_t top = tc->get_top();
+ vis_line_t bottom = tc->get_bottom();
+ if (lnav_data.ld_flags & LNF_HEADLESS && tc->get_inner_height() > 0_vl)
+ {
+ bottom = tc->get_inner_height() - 1_vl;
+ }
+ auto y = 0_vl;
+ auto wrapped_count = 0_vl;
+ std::vector<attr_line_t> rows(bottom - top + 1);
+ auto dim = tc->get_dimensions();
+ attr_line_t ov_al;
+
+ auto* los = tc->get_overlay_source();
+ tc->listview_value_for_rows(*tc, top, rows);
+ for (auto& al : rows) {
+ while (los != nullptr
+ && los->list_value_for_overlay(
+ *tc, y, tc->get_inner_height(), top, ov_al))
+ {
+ write_line_to(outfile, ov_al);
+ ++y;
+ }
+ wrapped_count += vis_line_t((al.length() - 1) / (dim.second - 2));
+ if (anonymize) {
+ al.al_attrs.clear();
+ al.al_string = ta.next(al.al_string);
+ }
+ write_line_to(outfile, al);
+
+ line_count += 1;
+ ++top;
+ ++y;
+ }
+ while (los != nullptr
+ && los->list_value_for_overlay(
+ *tc, y, tc->get_inner_height(), top, ov_al)
+ && !ov_al.empty())
+ {
+ write_line_to(outfile, ov_al);
+ ++y;
+ }
+
+ tc->set_word_wrap(wrapped);
+ tc->set_top(orig_top);
+
+ if (!(lnav_data.ld_flags & LNF_HEADLESS)) {
+ while (y + wrapped_count < dim.first + 2_vl) {
+ fmt::print(outfile, FMT_STRING("\n"));
+ ++y;
+ }
+ }
+ } else if (args[0] == "write-raw-to") {
+ if (tc == &lnav_data.ld_views[LNV_DB]) {
+ for (auto row_iter = dls.dls_rows.begin();
+ row_iter != dls.dls_rows.end();
+ ++row_iter)
+ {
+ if (ec.ec_dry_run
+ && distance(dls.dls_rows.begin(), row_iter) > 10)
+ {
+ break;
+ }
+
+ for (auto& iter : *row_iter) {
+ if (anonymize) {
+ fputs(
+ ta.next(string_fragment::from_c_str(iter)).c_str(),
+ outfile);
+ } else {
+ fputs(iter, outfile);
+ }
+ }
+ fprintf(outfile, "\n");
+
+ line_count += 1;
+ }
+ } else if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ nonstd::optional<std::pair<logfile*, content_line_t>> last_line;
+ bookmark_vector<vis_line_t> visited;
+ auto& lss = lnav_data.ld_log_source;
+ std::vector<attr_line_t> rows(1);
+ size_t count = 0;
+ std::string line;
+
+ for (auto iter = all_user_marks.begin();
+ iter != all_user_marks.end();
+ iter++, count++)
+ {
+ if (ec.ec_dry_run && count > 10) {
+ break;
+ }
+ auto cl = lss.at(*iter);
+ auto lf = lss.find(cl);
+ auto lf_iter = lf->begin() + cl;
+
+ while (lf_iter->get_sub_offset() != 0) {
+ --lf_iter;
+ }
+
+ auto line_pair = std::make_pair(
+ lf.get(),
+ content_line_t(std::distance(lf->begin(), lf_iter)));
+ if (last_line && last_line.value() == line_pair) {
+ continue;
+ }
+ last_line = line_pair;
+ auto read_res = lf->read_raw_message(lf_iter);
+ if (read_res.isErr()) {
+ log_error("unable to read message: %s",
+ read_res.unwrapErr().c_str());
+ continue;
+ }
+ auto sbr = read_res.unwrap();
+ if (anonymize) {
+ auto msg = ta.next(sbr.to_string_fragment().to_string());
+ fprintf(outfile, "%s\n", msg.c_str());
+ } else {
+ fprintf(
+ outfile, "%.*s\n", (int) sbr.length(), sbr.get_data());
+ }
+
+ line_count += 1;
+ }
+ }
+ } else if (args[0] == "write-view-to") {
+ bool wrapped = tc->get_word_wrap();
+ auto tss = tc->get_sub_source();
+
+ tc->set_word_wrap(to_term);
+
+ for (size_t lpc = 0; lpc < tss->text_line_count(); lpc++) {
+ if (ec.ec_dry_run && lpc >= 10) {
+ break;
+ }
+
+ std::string line;
+
+ tss->text_value_for_line(*tc, lpc, line, text_sub_source::RF_RAW);
+ if (anonymize) {
+ line = ta.next(line);
+ }
+ fprintf(outfile, "%s\n", line.c_str());
+
+ line_count += 1;
+ }
+
+ tc->set_word_wrap(wrapped);
+ } else {
+ auto* los = tc->get_overlay_source();
+ std::vector<attr_line_t> rows(1);
+ attr_line_t ov_al;
+ size_t count = 0;
+
+ for (auto iter = all_user_marks.begin(); iter != all_user_marks.end();
+ iter++, count++)
+ {
+ if (ec.ec_dry_run && count > 10) {
+ break;
+ }
+ tc->listview_value_for_rows(*tc, *iter, rows);
+ if (anonymize) {
+ rows[0].al_attrs.clear();
+ rows[0].al_string = ta.next(rows[0].al_string);
+ }
+ write_line_to(outfile, rows[0]);
+
+ auto y = 1_vl;
+ while (los != nullptr
+ && los->list_value_for_overlay(
+ *tc, y, tc->get_inner_height(), *iter, ov_al))
+ {
+ write_line_to(outfile, ov_al);
+ ++y;
+ }
+
+ line_count += 1;
+ }
+ }
+
+ fflush(outfile);
+
+ if (to_term) {
+ cbreak();
+ getch();
+ refresh();
+ nodelay(lnav_data.ld_window, 1);
+ }
+ if (ec.ec_dry_run) {
+ rewind(outfile);
+
+ char buffer[32 * 1024];
+ size_t rc = fread(buffer, 1, sizeof(buffer), outfile);
+
+ attr_line_t al(std::string(buffer, rc));
+
+ lnav_data.ld_preview_source.replace_with(al)
+ .set_text_format(detect_text_format(al.get_string()))
+ .truncate_to(10);
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "First lines of file: %s", split_args[0].c_str());
+ } else {
+ retval = fmt::format(FMT_STRING("info: Wrote {:L} rows to {}"),
+ line_count,
+ split_args[0]);
+ }
+ if (toclose != nullptr) {
+ closer(toclose);
+ }
+ outfile = nullptr;
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_pipe_to(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("filename");
+ return Ok(std::string());
+ }
+
+ if (lnav_data.ld_flags & LNF_SECURE_MODE) {
+ return ec.make_error("{} -- unavailable in secure mode", args[0]);
+ }
+
+ if (args.size() < 2) {
+ return ec.make_error("expecting command to execute");
+ }
+
+ if (ec.ec_dry_run) {
+ return Ok(std::string());
+ }
+
+ auto* tc = *lnav_data.ld_view_stack.top();
+ auto bv = combined_user_marks(tc->get_bookmarks());
+ bool pipe_line_to = (args[0] == "pipe-line-to");
+
+ std::string cmd = trim(remaining_args(cmdline, args));
+ auto_pipe in_pipe(STDIN_FILENO);
+ auto_pipe out_pipe(STDOUT_FILENO);
+
+ in_pipe.open();
+ out_pipe.open();
+
+ pid_t child_pid = fork();
+
+ in_pipe.after_fork(child_pid);
+ out_pipe.after_fork(child_pid);
+
+ switch (child_pid) {
+ case -1:
+ return ec.make_error("unable to fork child process -- {}",
+ strerror(errno));
+
+ case 0: {
+ const char* args[] = {
+ "sh",
+ "-c",
+ cmd.c_str(),
+ nullptr,
+ };
+ auto path_v = ec.ec_path_stack;
+ std::string path;
+
+ dup2(STDOUT_FILENO, STDERR_FILENO);
+ path_v.emplace_back(lnav::paths::dotlnav() / "formats/default");
+
+ if (pipe_line_to && tc == &lnav_data.ld_views[LNV_LOG]) {
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ log_data_helper ldh(lss);
+ char tmp_str[64];
+
+ ldh.parse_line(ec.ec_top_line, true);
+ auto format = ldh.ldh_file->get_format();
+ auto source_path = format->get_source_path();
+ path_v.insert(
+ path_v.end(), source_path.begin(), source_path.end());
+
+ snprintf(tmp_str, sizeof(tmp_str), "%d", (int) ec.ec_top_line);
+ setenv("log_line", tmp_str, 1);
+ sql_strftime(
+ tmp_str, sizeof(tmp_str), ldh.ldh_line->get_timeval());
+ setenv("log_time", tmp_str, 1);
+ setenv("log_path", ldh.ldh_file->get_filename().c_str(), 1);
+ for (auto& ldh_line_value : ldh.ldh_line_values.lvv_values) {
+ setenv(ldh_line_value.lv_meta.lvm_name.get(),
+ ldh_line_value.to_string().c_str(),
+ 1);
+ }
+ auto iter = ldh.ldh_parser->dp_pairs.begin();
+ for (size_t lpc = 0; lpc < ldh.ldh_parser->dp_pairs.size();
+ lpc++, ++iter)
+ {
+ std::string colname = ldh.ldh_parser->get_element_string(
+ iter->e_sub_elements->front());
+ colname = ldh.ldh_namer->add_column(colname).to_string();
+ std::string val = ldh.ldh_parser->get_element_string(
+ iter->e_sub_elements->back());
+ setenv(colname.c_str(), val.c_str(), 1);
+ }
+ }
+
+ setenv("PATH", lnav::filesystem::build_path(path_v).c_str(), 1);
+ execvp(args[0], (char* const*) args);
+ _exit(1);
+ break;
+ }
+
+ default:
+ bookmark_vector<vis_line_t>::iterator iter;
+ std::string line;
+
+ in_pipe.read_end().close_on_exec();
+ in_pipe.write_end().close_on_exec();
+
+ lnav_data.ld_children.push_back(child_pid);
+
+ std::future<std::string> reader;
+
+ if (out_pipe.read_end() != -1) {
+ reader = ec.ec_pipe_callback(ec, cmdline, out_pipe.read_end());
+ }
+
+ if (pipe_line_to) {
+ if (tc->get_inner_height() == 0) {
+ // Nothing to do
+ } else if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ content_line_t cl = lss.at(tc->get_top());
+ std::shared_ptr<logfile> lf = lss.find(cl);
+ shared_buffer_ref sbr;
+ lf->read_full_message(lf->message_start(lf->begin() + cl),
+ sbr);
+ if (write(in_pipe.write_end(), sbr.get_data(), sbr.length())
+ == -1)
+ {
+ return ec.make_error("Unable to write to pipe -- {}",
+ strerror(errno));
+ }
+ log_perror(write(in_pipe.write_end(), "\n", 1));
+ } else {
+ tc->grep_value_for_line(tc->get_top(), line);
+ if (write(in_pipe.write_end(), line.c_str(), line.size())
+ == -1)
+ {
+ return ec.make_error("Unable to write to pipe -- {}",
+ strerror(errno));
+ }
+ log_perror(write(in_pipe.write_end(), "\n", 1));
+ }
+ } else {
+ for (iter = bv.begin(); iter != bv.end(); iter++) {
+ tc->grep_value_for_line(*iter, line);
+ if (write(in_pipe.write_end(), line.c_str(), line.size())
+ == -1)
+ {
+ return ec.make_error("Unable to write to pipe -- {}",
+ strerror(errno));
+ }
+ log_perror(write(in_pipe.write_end(), "\n", 1));
+ }
+ }
+
+ in_pipe.write_end().reset();
+
+ if (reader.valid()) {
+ retval = reader.get();
+ } else {
+ retval = "";
+ }
+ break;
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_redirect_to(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ if (args.empty()) {
+ args.emplace_back("filename");
+ return Ok(std::string());
+ }
+
+ if (args.size() == 1) {
+ if (ec.ec_dry_run) {
+ return Ok(std::string("info: redirect will be cleared"));
+ }
+
+ ec.clear_output();
+ return Ok(std::string("info: cleared redirect"));
+ }
+
+ std::string fn = trim(remaining_args(cmdline, args));
+ std::vector<std::string> split_args;
+ shlex lexer(fn);
+ scoped_resolver scopes = {
+ &ec.ec_local_vars.top(),
+ &ec.ec_global_vars,
+ };
+
+ if (!lexer.split(split_args, scopes)) {
+ return ec.make_error("unable to parse arguments");
+ }
+ if (split_args.size() > 1) {
+ return ec.make_error("more than one file name was matched");
+ }
+
+ if (ec.ec_dry_run) {
+ return Ok("info: output will be redirected to -- " + split_args[0]);
+ }
+
+ nonstd::optional<FILE*> file;
+
+ if (split_args[0] == "-") {
+ ec.clear_output();
+ } else if (split_args[0] == "/dev/clipboard") {
+ auto out = sysclip::open(sysclip::type_t::GENERAL);
+ if (out.isErr()) {
+ alerter::singleton().chime("cannot open clipboard");
+ return ec.make_error("Unable to copy to clipboard: {}",
+ out.unwrapErr());
+ }
+
+ auto holder = out.unwrap();
+ ec.set_output(split_args[0],
+ holder.release(),
+ holder.get_free_func<int (*)(FILE*)>());
+ } else if (lnav_data.ld_flags & LNF_SECURE_MODE) {
+ return ec.make_error("{} -- unavailable in secure mode", args[0]);
+ } else {
+ FILE* file = fopen(split_args[0].c_str(), "w");
+ if (file == nullptr) {
+ return ec.make_error("unable to open file -- {}", split_args[0]);
+ }
+
+ ec.set_output(split_args[0], file, fclose);
+ }
+
+ return Ok("info: redirecting output to file -- " + split_args[0]);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_highlight(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("filter");
+ } else if (args.size() > 1) {
+ const static intern_string_t PATTERN_SRC
+ = intern_string::lookup("pattern");
+
+ auto* tc = *lnav_data.ld_view_stack.top();
+ auto& hm = tc->get_highlights();
+ auto re_frag = remaining_args_frag(cmdline, args);
+ args[1] = re_frag.to_string();
+ if (hm.find({highlight_source_t::INTERACTIVE, args[1]}) != hm.end()) {
+ return ec.make_error("highlight already exists -- {}", args[1]);
+ }
+
+ auto compile_res = lnav::pcre2pp::code::from(args[1], PCRE2_CASELESS);
+
+ if (compile_res.isErr()) {
+ auto ce = compile_res.unwrapErr();
+ auto um = lnav::console::to_user_message(PATTERN_SRC, ce);
+ return Err(um);
+ }
+ highlighter hl(compile_res.unwrap().to_shared());
+ auto hl_attrs = view_colors::singleton().attrs_for_ident(args[1]);
+
+ if (ec.ec_dry_run) {
+ hl_attrs.ta_attrs |= A_BLINK;
+ }
+
+ hl.with_attrs(hl_attrs);
+
+ if (ec.ec_dry_run) {
+ hm[{highlight_source_t::PREVIEW, "preview"}] = hl;
+
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "Matches are highlighted in the view");
+
+ retval = "";
+ } else {
+ hm[{highlight_source_t::INTERACTIVE, args[1]}] = hl;
+
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "highlight", args[1]);
+ }
+
+ retval = "info: highlight pattern now active";
+ }
+ tc->reload_data();
+ } else {
+ return ec.make_error("expecting a regular expression to highlight");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_clear_highlight(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("highlight");
+ } else if (args.size() > 1 && args[1][0] != '$') {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ auto& hm = tc->get_highlights();
+
+ args[1] = remaining_args(cmdline, args);
+ auto hm_iter = hm.find({highlight_source_t::INTERACTIVE, args[1]});
+ if (hm_iter == hm.end()) {
+ return ec.make_error("highlight does not exist -- {}", args[1]);
+ } else if (ec.ec_dry_run) {
+ retval = "";
+ } else {
+ hm.erase(hm_iter);
+ retval = "info: highlight pattern cleared";
+ tc->reload_data();
+
+ if (lnav_data.ld_rl_view != NULL) {
+ lnav_data.ld_rl_view->rem_possibility(
+ ln_mode_t::COMMAND, "highlight", args[1]);
+ }
+ }
+ } else {
+ return ec.make_error("expecting highlight expression to clear");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_help(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ ensure_view(&lnav_data.ld_views[LNV_HELP]);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message> com_enable_filter(
+ exec_context& ec, std::string cmdline, std::vector<std::string>& args);
+
+static Result<std::string, lnav::console::user_message>
+com_filter(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("filter");
+
+ return Ok(std::string());
+ }
+
+ auto tc = *lnav_data.ld_view_stack.top();
+ auto tss = tc->get_sub_source();
+
+ if (!tss->tss_supports_filtering) {
+ return ec.make_error("{} view does not support filtering",
+ lnav_view_strings[tc - lnav_data.ld_views]);
+ } else if (args.size() > 1) {
+ const static intern_string_t PATTERN_SRC
+ = intern_string::lookup("pattern");
+
+ auto* tss = tc->get_sub_source();
+ auto& fs = tss->get_filters();
+ auto re_frag = remaining_args_frag(cmdline, args);
+ args[1] = re_frag.to_string();
+ if (fs.get_filter(args[1]) != nullptr) {
+ return com_enable_filter(ec, cmdline, args);
+ }
+
+ if (fs.full()) {
+ return ec.make_error(
+ "filter limit reached, try combining "
+ "filters with a pipe symbol (e.g. foo|bar)");
+ }
+
+ auto compile_res = lnav::pcre2pp::code::from(args[1], PCRE2_CASELESS);
+
+ if (compile_res.isErr()) {
+ auto ce = compile_res.unwrapErr();
+ auto um = lnav::console::to_user_message(PATTERN_SRC, ce);
+ return Err(um);
+ }
+ if (ec.ec_dry_run) {
+ if (args[0] == "filter-in" && !fs.empty()) {
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "Match preview for :filter-in only works if there are no "
+ "other filters");
+ retval = "";
+ } else {
+ auto& hm = tc->get_highlights();
+ highlighter hl(compile_res.unwrap().to_shared());
+ auto role = (args[0] == "filter-out") ? role_t::VCR_DIFF_DELETE
+ : role_t::VCR_DIFF_ADD;
+ hl.with_role(role);
+ hl.with_attrs(text_attrs{A_BLINK | A_REVERSE});
+
+ hm[{highlight_source_t::PREVIEW, "preview"}] = hl;
+ tc->reload_data();
+
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "Matches are highlighted in %s in the text view",
+ role == role_t::VCR_DIFF_DELETE ? "red" : "green");
+
+ retval = "";
+ }
+ } else {
+ text_filter::type_t lt = (args[0] == "filter-out")
+ ? text_filter::EXCLUDE
+ : text_filter::INCLUDE;
+ auto filter_index = fs.next_index();
+ if (!filter_index) {
+ return ec.make_error("too many filters");
+ }
+ auto pf = std::make_shared<pcre_filter>(
+ lt, args[1], *filter_index, compile_res.unwrap().to_shared());
+
+ log_debug("%s [%d] %s",
+ args[0].c_str(),
+ pf->get_index(),
+ args[1].c_str());
+ fs.add_filter(pf);
+ tss->text_filters_changed();
+
+ retval = "info: filter now active";
+ }
+ } else {
+ return ec.make_error("expecting a regular expression to filter");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_delete_filter(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("all-filters");
+ } else if (args.size() > 1) {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = tc->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+
+ args[1] = remaining_args(cmdline, args);
+ if (ec.ec_dry_run) {
+ retval = "";
+ } else if (fs.delete_filter(args[1])) {
+ retval = "info: deleted filter";
+ tss->text_filters_changed();
+ } else {
+ return ec.make_error("unknown filter -- {}", args[1]);
+ }
+ } else {
+ return ec.make_error("expecting a filter to delete");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_enable_filter(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("disabled-filter");
+ } else if (args.size() > 1) {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = tc->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ std::shared_ptr<text_filter> lf;
+
+ args[1] = remaining_args(cmdline, args);
+ lf = fs.get_filter(args[1]);
+ if (lf == nullptr) {
+ return ec.make_error("no such filter -- {}", args[1]);
+ } else if (lf->is_enabled()) {
+ retval = "info: filter already enabled";
+ } else if (ec.ec_dry_run) {
+ retval = "";
+ } else {
+ fs.set_filter_enabled(lf, true);
+ tss->text_filters_changed();
+ retval = "info: filter enabled";
+ }
+ } else {
+ return ec.make_error("expecting disabled filter to enable");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_disable_filter(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("enabled-filter");
+ } else if (args.size() > 1) {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ text_sub_source* tss = tc->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ std::shared_ptr<text_filter> lf;
+
+ args[1] = remaining_args(cmdline, args);
+ lf = fs.get_filter(args[1]);
+ if (lf == nullptr) {
+ return ec.make_error("no such filter -- {}", args[1]);
+ } else if (!lf->is_enabled()) {
+ retval = "info: filter already disabled";
+ } else if (ec.ec_dry_run) {
+ retval = "";
+ } else {
+ fs.set_filter_enabled(lf, false);
+ tss->text_filters_changed();
+ retval = "info: filter disabled";
+ }
+ } else {
+ return ec.make_error("expecting enabled filter to disable");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_filter_expr(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("filter-expr-syms");
+ } else if (args.size() > 1) {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ return ec.make_error(
+ "The :filter-expr command only works in the log view");
+ }
+
+ auto expr = remaining_args(cmdline, args);
+ args[1] = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), expr);
+
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+#ifdef SQLITE_PREPARE_PERSISTENT
+ auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
+ args[1].c_str(),
+ args[1].size(),
+ SQLITE_PREPARE_PERSISTENT,
+ stmt.out(),
+ nullptr);
+#else
+ auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
+ args[1].c_str(),
+ args[1].size(),
+ stmt.out(),
+ nullptr);
+#endif
+ if (retcode != SQLITE_OK) {
+ const char* errmsg = sqlite3_errmsg(lnav_data.ld_db);
+ auto expr_al = attr_line_t(expr).with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ readline_sqlite_highlighter(expr_al, -1);
+ auto um = lnav::console::user_message::error(
+ attr_line_t("invalid filter expression: ")
+ .append(expr_al))
+ .with_reason(errmsg)
+ .with_snippets(ec.ec_source);
+
+ return Err(um);
+ }
+
+ if (ec.ec_dry_run) {
+ auto set_res = lnav_data.ld_log_source.set_preview_sql_filter(
+ stmt.release());
+
+ if (set_res.isErr()) {
+ return Err(set_res.unwrapErr());
+ }
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "Matches are highlighted in the text view");
+ } else {
+ lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
+ auto set_res
+ = lnav_data.ld_log_source.set_sql_filter(expr, stmt.release());
+
+ if (set_res.isErr()) {
+ return Err(set_res.unwrapErr());
+ }
+ }
+ lnav_data.ld_log_source.text_filters_changed();
+ tc->reload_data();
+ } else {
+ return ec.make_error("expecting an SQL expression");
+ }
+
+ return Ok(retval);
+}
+
+static std::string
+com_filter_expr_prompt(exec_context& ec, const std::string& cmdline)
+{
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ return "";
+ }
+
+ return fmt::format(FMT_STRING("{} {}"),
+ trim(cmdline),
+ trim(lnav_data.ld_log_source.get_sql_filter_text()));
+}
+
+static Result<std::string, lnav::console::user_message>
+com_clear_filter_expr(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else {
+ if (!ec.ec_dry_run) {
+ lnav_data.ld_log_source.set_sql_filter("", nullptr);
+ lnav_data.ld_log_source.text_filters_changed();
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_enable_word_wrap(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ lnav_data.ld_views[LNV_LOG].set_word_wrap(true);
+ lnav_data.ld_views[LNV_TEXT].set_word_wrap(true);
+ lnav_data.ld_views[LNV_PRETTY].set_word_wrap(true);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_disable_word_wrap(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ lnav_data.ld_views[LNV_LOG].set_word_wrap(false);
+ lnav_data.ld_views[LNV_TEXT].set_word_wrap(false);
+ lnav_data.ld_views[LNV_PRETTY].set_word_wrap(false);
+ }
+
+ return Ok(retval);
+}
+
+static std::set<std::string> custom_logline_tables;
+
+static Result<std::string, lnav::console::user_message>
+com_create_logline_table(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (args.size() == 2) {
+ textview_curses& log_view = lnav_data.ld_views[LNV_LOG];
+
+ if (log_view.get_inner_height() == 0) {
+ return ec.make_error("no log data available");
+ } else {
+ vis_line_t vl = log_view.get_selection();
+ content_line_t cl = lnav_data.ld_log_source.at_base(vl);
+ auto ldt = std::make_shared<log_data_table>(
+ lnav_data.ld_log_source,
+ *lnav_data.ld_vtab_manager,
+ cl,
+ intern_string::lookup(args[1]));
+
+ if (ec.ec_dry_run) {
+ attr_line_t al(ldt->get_table_statement());
+
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "The following table will be created:");
+ lnav_data.ld_preview_source.replace_with(al).set_text_format(
+ text_format_t::TF_SQL);
+
+ return Ok(std::string());
+ } else {
+ std::string errmsg;
+
+ errmsg = lnav_data.ld_vtab_manager->register_vtab(ldt);
+ if (errmsg.empty()) {
+ custom_logline_tables.insert(args[1]);
+ if (lnav_data.ld_rl_view != NULL) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "custom-table", args[1]);
+ }
+ retval = "info: created new log table -- " + args[1];
+ } else {
+ return ec.make_error("unable to create table -- {}",
+ errmsg);
+ }
+ }
+ }
+ } else {
+ return ec.make_error("expecting a table name");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_delete_logline_table(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("custom-table");
+ } else if (args.size() == 2) {
+ if (custom_logline_tables.find(args[1]) == custom_logline_tables.end())
+ {
+ return ec.make_error("unknown logline table -- {}", args[1]);
+ }
+
+ if (ec.ec_dry_run) {
+ return Ok(std::string());
+ }
+
+ std::string rc = lnav_data.ld_vtab_manager->unregister_vtab(
+ intern_string::lookup(args[1]));
+
+ if (rc.empty()) {
+ if (lnav_data.ld_rl_view != NULL) {
+ lnav_data.ld_rl_view->rem_possibility(
+ ln_mode_t::COMMAND, "custom-table", args[1]);
+ }
+ retval = "info: deleted logline table";
+ } else {
+ return ec.make_error("{}", rc);
+ }
+ } else {
+ return ec.make_error("expecting a table name");
+ }
+
+ return Ok(retval);
+}
+
+static std::set<std::string> custom_search_tables;
+
+static Result<std::string, lnav::console::user_message>
+com_create_search_table(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (args.size() >= 2) {
+ const static intern_string_t PATTERN_SRC
+ = intern_string::lookup("pattern");
+ string_fragment regex_frag;
+ std::string regex;
+
+ if (args.size() >= 3) {
+ regex_frag = remaining_args_frag(cmdline, args, 2);
+ regex = regex_frag.to_string();
+ } else {
+ regex = lnav_data.ld_views[LNV_LOG].get_current_search();
+ }
+
+ auto compile_res = lnav::pcre2pp::code::from(
+ regex, log_search_table_ns::PATTERN_OPTIONS);
+
+ if (compile_res.isErr()) {
+ auto re_err = compile_res.unwrapErr();
+ auto um = lnav::console::to_user_message(PATTERN_SRC, re_err)
+ .with_snippets(ec.ec_source);
+ return Err(um);
+ }
+
+ auto re = compile_res.unwrap().to_shared();
+ auto tab_name = intern_string::lookup(args[1]);
+ auto lst = std::make_shared<log_search_table>(re, tab_name);
+ if (ec.ec_dry_run) {
+ auto* tc = &lnav_data.ld_views[LNV_LOG];
+ auto& hm = tc->get_highlights();
+ highlighter hl(re);
+
+ hl.with_role(role_t::VCR_INFO);
+ hl.with_attrs(text_attrs{A_BLINK});
+
+ hm[{highlight_source_t::PREVIEW, "preview"}] = hl;
+ tc->reload_data();
+
+ attr_line_t al(lst->get_table_statement());
+
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "The following table will be created:");
+
+ lnav_data.ld_preview_source.replace_with(al).set_text_format(
+ text_format_t::TF_SQL);
+
+ return Ok(std::string());
+ }
+
+ auto tab_iter = custom_search_tables.find(args[1]);
+ if (tab_iter != custom_search_tables.end()) {
+ lnav_data.ld_vtab_manager->unregister_vtab(tab_name);
+ }
+
+ std::string errmsg;
+
+ errmsg = lnav_data.ld_vtab_manager->register_vtab(lst);
+ if (errmsg.empty()) {
+ custom_search_tables.insert(args[1]);
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "search-table", args[1]);
+ }
+ retval = "info: created new search table -- " + args[1];
+ } else {
+ return ec.make_error("unable to create table -- {}", errmsg);
+ }
+ } else {
+ return ec.make_error("expecting a table name");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_delete_search_table(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("search-table");
+ } else if (args.size() == 2) {
+ auto tab_iter = custom_search_tables.find(args[1]);
+ if (tab_iter == custom_search_tables.end()) {
+ return ec.make_error("unknown search table -- {}", args[1]);
+ }
+
+ if (ec.ec_dry_run) {
+ return Ok(std::string());
+ }
+
+ custom_search_tables.erase(tab_iter);
+ auto rc = lnav_data.ld_vtab_manager->unregister_vtab(
+ intern_string::lookup(args[1]));
+
+ if (rc.empty()) {
+ if (lnav_data.ld_rl_view != NULL) {
+ lnav_data.ld_rl_view->rem_possibility(
+ ln_mode_t::COMMAND, "search-table", args[1]);
+ }
+ retval = "info: deleted search table";
+ } else {
+ return ec.make_error("{}", rc);
+ }
+ } else {
+ return ec.make_error("expecting a table name");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_session(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (ec.ec_dry_run) {
+ retval = "";
+ } else if (args.size() >= 2) {
+ /* XXX put these in a map */
+ if (args[1] != "highlight" && args[1] != "enable-word-wrap"
+ && args[1] != "disable-word-wrap" && args[1] != "filter-in"
+ && args[1] != "filter-out" && args[1] != "enable-filter"
+ && args[1] != "disable-filter")
+ {
+ return ec.make_error(
+ "only the highlight, filter, and word-wrap commands are "
+ "supported");
+ } else if (getenv("HOME") == NULL) {
+ return ec.make_error("the HOME environment variable is not set");
+ } else {
+ auto saved_cmd = trim(remaining_args(cmdline, args));
+ auto old_file_name = lnav::paths::dotlnav() / "session";
+ auto new_file_name = lnav::paths::dotlnav() / "session.tmp";
+
+ std::ifstream session_file(old_file_name.string());
+ std::ofstream new_session_file(new_file_name.string());
+
+ if (!new_session_file) {
+ return ec.make_error("cannot write to session file");
+ } else {
+ bool added = false;
+ std::string line;
+
+ if (session_file.is_open()) {
+ while (getline(session_file, line)) {
+ if (line == saved_cmd) {
+ added = true;
+ break;
+ }
+ new_session_file << line << std::endl;
+ }
+ }
+ if (!added) {
+ new_session_file << saved_cmd << std::endl;
+
+ log_perror(
+ rename(new_file_name.c_str(), old_file_name.c_str()));
+ } else {
+ log_perror(remove(new_file_name.c_str()));
+ }
+
+ retval = "info: session file saved";
+ }
+ }
+ } else {
+ return ec.make_error("expecting a command to save to the session file");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("filename");
+ return Ok(std::string());
+ }
+
+ if (lnav_data.ld_flags & LNF_SECURE_MODE) {
+ return ec.make_error("{} -- unavailable in secure mode", args[0]);
+ }
+
+ if (args.size() < 2) {
+ return ec.make_error("expecting file name to open");
+ }
+
+ std::vector<std::string> word_exp;
+ std::string pat;
+ file_collection fc;
+
+ pat = trim(remaining_args(cmdline, args));
+
+ std::vector<std::string> split_args;
+ shlex lexer(pat);
+ scoped_resolver scopes = {
+ &ec.ec_local_vars.top(),
+ &ec.ec_global_vars,
+ };
+
+ if (!lexer.split(split_args, scopes)) {
+ return ec.make_error("unable to parse arguments");
+ }
+
+ std::vector<std::pair<std::string, file_location_t>> files_to_front;
+ std::vector<std::string> closed_files;
+
+ for (auto fn : split_args) {
+ file_location_t file_loc;
+
+ if (access(fn.c_str(), R_OK) != 0) {
+ auto colon_index = fn.rfind(':');
+ auto hash_index = fn.rfind('#');
+ if (colon_index != std::string::npos) {
+ auto top_range
+ = scn::string_view{&fn[colon_index + 1], &(*fn.cend())};
+ auto scan_res = scn::scan_value<int>(top_range);
+
+ if (scan_res) {
+ fn = fn.substr(0, colon_index);
+ file_loc = vis_line_t(scan_res.value());
+ }
+ } else if (hash_index != std::string::npos) {
+ file_loc = fn.substr(hash_index);
+ fn = fn.substr(0, hash_index);
+ }
+ }
+
+ auto file_iter = lnav_data.ld_active_files.fc_files.begin();
+ for (; file_iter != lnav_data.ld_active_files.fc_files.end();
+ ++file_iter)
+ {
+ auto lf = *file_iter;
+
+ if (lf->get_filename() == fn) {
+ if (lf->get_format() != nullptr) {
+ retval = "info: log file already loaded";
+ break;
+ }
+
+ files_to_front.emplace_back(fn, file_loc);
+ retval = "";
+ break;
+ }
+ }
+ if (file_iter == lnav_data.ld_active_files.fc_files.end()) {
+ auto_mem<char> abspath;
+ struct stat st;
+
+ if (is_url(fn.c_str())) {
+#ifndef HAVE_LIBCURL
+ retval = "error: lnav was not compiled with libcurl";
+#else
+ if (!ec.ec_dry_run) {
+ auto ul = std::make_shared<url_loader>(fn);
+
+ lnav_data.ld_active_files.fc_file_names[fn].with_fd(
+ ul->copy_fd());
+ isc::to<curl_looper&, services::curl_streamer_t>().send(
+ [ul](auto& clooper) { clooper.add_request(ul); });
+ lnav_data.ld_files_to_front.emplace_back(fn, file_loc);
+ retval = "info: opened URL";
+ } else {
+ retval = "";
+ }
+#endif
+ } else if (is_glob(fn.c_str())) {
+ fc.fc_file_names.emplace(fn, logfile_open_options());
+ retval = "info: watching -- " + fn;
+ } else if (stat(fn.c_str(), &st) == -1) {
+ if (fn.find(':') != std::string::npos) {
+ fc.fc_file_names.emplace(fn, logfile_open_options());
+ retval = "info: watching -- " + fn;
+ } else {
+ auto um = lnav::console::user_message::error(
+ attr_line_t("cannot open file: ")
+ .append(lnav::roles::file(fn)))
+ .with_errno_reason()
+ .with_snippets(ec.ec_source)
+ .with_help(
+ "make sure the file exists and is "
+ "accessible");
+ return Err(um);
+ }
+ } else if (is_dev_null(st)) {
+ return ec.make_error("cannot open /dev/null");
+ } else if (S_ISFIFO(st.st_mode)) {
+ auto_fd fifo_fd;
+
+ if ((fifo_fd = open(fn.c_str(), O_RDONLY)) == -1) {
+ auto um = lnav::console::user_message::error(
+ attr_line_t("cannot open FIFO: ")
+ .append(lnav::roles::file(fn)))
+ .with_errno_reason()
+ .with_snippets(ec.ec_source);
+ return Err(um);
+ } else if (ec.ec_dry_run) {
+ retval = "";
+ } else {
+ auto fifo_piper = std::make_shared<piper_proc>(
+ std::move(fifo_fd),
+ false,
+ lnav::filesystem::open_temp_file(
+ ghc::filesystem::temp_directory_path()
+ / "lnav.fifo.XXXXXX")
+ .map([](auto pair) {
+ ghc::filesystem::remove(pair.first);
+
+ return pair;
+ })
+ .expect("Cannot create temporary file for FIFO")
+ .second);
+ auto fifo_out_fd = fifo_piper->get_fd();
+ auto desc = fmt::format(FMT_STRING("FIFO [{}]"),
+ lnav_data.ld_fifo_counter++);
+ lnav_data.ld_active_files.fc_file_names[desc].with_fd(
+ std::move(fifo_out_fd));
+ lnav_data.ld_pipers.push_back(fifo_piper);
+ }
+ } else if ((abspath = realpath(fn.c_str(), nullptr)) == nullptr) {
+ auto um = lnav::console::user_message::error(
+ attr_line_t("cannot open file: ")
+ .append(lnav::roles::file(fn)))
+ .with_errno_reason()
+ .with_snippets(ec.ec_source)
+ .with_help(
+ "make sure the file exists and is "
+ "accessible");
+ return Err(um);
+ } else if (S_ISDIR(st.st_mode)) {
+ std::string dir_wild(abspath.in());
+
+ if (dir_wild[dir_wild.size() - 1] == '/') {
+ dir_wild.resize(dir_wild.size() - 1);
+ }
+ fc.fc_file_names.emplace(dir_wild + "/*",
+ logfile_open_options());
+ retval = "info: watching -- " + dir_wild;
+ } else if (!S_ISREG(st.st_mode)) {
+ auto um = lnav::console::user_message::error(
+ attr_line_t("cannot open file: ")
+ .append(lnav::roles::file(fn)))
+ .with_reason("not a regular file or directory")
+ .with_snippets(ec.ec_source)
+ .with_help(
+ "only regular files, directories, and FIFOs "
+ "can be opened");
+ return Err(um);
+ } else if (access(fn.c_str(), R_OK) == -1) {
+ auto um = lnav::console::user_message::error(
+ attr_line_t("cannot read file: ")
+ .append(lnav::roles::file(fn)))
+ .with_errno_reason()
+ .with_snippets(ec.ec_source)
+ .with_help(
+ "make sure the file exists and is "
+ "accessible");
+ return Err(um);
+ } else {
+ fn = abspath.in();
+ fc.fc_file_names.emplace(fn, logfile_open_options());
+ retval = "info: opened -- " + fn;
+ files_to_front.emplace_back(fn, file_loc);
+
+ closed_files.push_back(fn);
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(X, "to close the file"));
+ }
+ }
+ }
+ }
+
+ if (ec.ec_dry_run) {
+ lnav_data.ld_preview_source.clear();
+ if (!fc.fc_file_names.empty()) {
+ auto iter = fc.fc_file_names.begin();
+ std::string fn = iter->first;
+ auto_fd preview_fd;
+
+ if (fn.find(':') != std::string::npos) {
+ auto id = lnav_data.ld_preview_generation;
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(true)
+ .set_value("Loading %s...", fn.c_str());
+ lnav_data.ld_preview_source.clear();
+
+ isc::to<tailer::looper&, services::remote_tailer_t>().send(
+ [id, fn](auto& tlooper) {
+ auto rp_opt = humanize::network::path::from_str(fn);
+ if (rp_opt) {
+ tlooper.load_preview(id, *rp_opt);
+ }
+ });
+ lnav_data.ld_preview_view.set_needs_update();
+ } else if (is_glob(fn.c_str())) {
+ static_root_mem<glob_t, globfree> gl;
+
+ if (glob(fn.c_str(), GLOB_NOCHECK, nullptr, gl.inout()) == 0) {
+ attr_line_t al;
+
+ for (size_t lpc = 0; lpc < gl->gl_pathc && lpc < 10; lpc++)
+ {
+ al.append(gl->gl_pathv[lpc]).append("\n");
+ }
+ if (gl->gl_pathc > 10) {
+ al.append(" ... ")
+ .append(lnav::roles::number(
+ std::to_string(gl->gl_pathc - 10)))
+ .append(" files not shown ...");
+ }
+ lnav_data.ld_preview_status_source.get_description()
+ .set_value("The following files will be loaded:");
+ lnav_data.ld_preview_source.replace_with(al);
+ } else {
+ return ec.make_error("failed to evaluate glob -- {}", fn);
+ }
+ } else if ((preview_fd = open(fn.c_str(), O_RDONLY)) == -1) {
+ return ec.make_error(
+ "unable to open file3: {} -- {}", fn, strerror(errno));
+ } else {
+ line_buffer lb;
+ attr_line_t al;
+ file_range range;
+ std::string lines;
+
+ lb.set_fd(preview_fd);
+ for (int lpc = 0; lpc < 10; lpc++) {
+ auto load_result = lb.load_next_line(range);
+
+ if (load_result.isErr()) {
+ break;
+ }
+
+ auto li = load_result.unwrap();
+
+ range = li.li_file_range;
+ auto read_result = lb.read_range(range);
+ if (read_result.isErr()) {
+ break;
+ }
+
+ auto sbr = read_result.unwrap();
+ lines.append(sbr.get_data(), sbr.length());
+ }
+
+ lnav_data.ld_preview_source.replace_with(al.with_string(lines))
+ .set_text_format(detect_text_format(al.get_string()));
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "For file: %s", fn.c_str());
+ }
+ }
+ } else {
+ lnav_data.ld_files_to_front.insert(lnav_data.ld_files_to_front.end(),
+ files_to_front.begin(),
+ files_to_front.end());
+ for (const auto& fn : closed_files) {
+ fc.fc_closed_files.erase(fn);
+ }
+
+ lnav_data.ld_active_files.merge(fc);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_close(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ nonstd::optional<ghc::filesystem::path> actual_path;
+ std::string fn;
+
+ if (tc == &lnav_data.ld_views[LNV_TEXT]) {
+ textfile_sub_source& tss = lnav_data.ld_text_source;
+
+ if (tss.empty()) {
+ return ec.make_error("no text files are opened");
+ } else {
+ fn = tss.current_file()->get_filename();
+ lnav_data.ld_active_files.request_close(tss.current_file());
+
+ if (tss.size() == 1) {
+ lnav_data.ld_view_stack.pop_back();
+ }
+ }
+ } else if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ if (tc->get_inner_height() == 0) {
+ return ec.make_error("no log files loaded");
+ } else {
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ vis_line_t vl = tc->get_selection();
+ content_line_t cl = lss.at(vl);
+ std::shared_ptr<logfile> lf = lss.find(cl);
+
+ actual_path = lf->get_actual_path();
+ fn = lf->get_filename();
+ if (!ec.ec_dry_run) {
+ lnav_data.ld_active_files.request_close(lf);
+ }
+ }
+ } else {
+ return ec.make_error(
+ "close must be run in the log or text file views");
+ }
+ if (!fn.empty()) {
+ if (ec.ec_dry_run) {
+ retval = "";
+ } else {
+ if (is_url(fn.c_str())) {
+ isc::to<curl_looper&, services::curl_streamer_t>().send(
+ [fn](auto& clooper) { clooper.close_request(fn); });
+ }
+ if (actual_path) {
+ lnav_data.ld_active_files.fc_file_names.erase(
+ actual_path.value().string());
+ }
+ lnav_data.ld_active_files.fc_closed_files.insert(fn);
+ retval = "info: closed -- " + fn;
+ }
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_file_visibility(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ bool only_this_file = false;
+ bool make_visible;
+ std::string retval;
+
+ if (args[0] == "show-file") {
+ make_visible = true;
+ } else if (args[0] == "show-only-this-file") {
+ make_visible = true;
+ only_this_file = true;
+ } else {
+ make_visible = false;
+ }
+
+ if (args.size() == 1 || only_this_file) {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ std::shared_ptr<logfile> lf;
+
+ if (tc == &lnav_data.ld_views[LNV_TEXT]) {
+ textfile_sub_source& tss = lnav_data.ld_text_source;
+
+ if (tss.empty()) {
+ return ec.make_error("no text files are opened");
+ }
+ lf = tss.current_file();
+ } else if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ if (tc->get_inner_height() == 0) {
+ return ec.make_error("no log files loaded");
+ }
+ auto& lss = lnav_data.ld_log_source;
+ auto vl = tc->get_selection();
+ auto cl = lss.at(vl);
+ lf = lss.find(cl);
+ } else {
+ return ec.make_error(
+ ":{} must be run in the log or text file views", args[0]);
+ }
+
+ if (!ec.ec_dry_run) {
+ if (only_this_file) {
+ for (const auto& ld : lnav_data.ld_log_source) {
+ ld->set_visibility(false);
+ }
+ }
+ lnav_data.ld_log_source.find_data(lf) |
+ [make_visible](auto ld) { ld->set_visibility(make_visible); };
+ tc->get_sub_source()->text_filters_changed();
+ }
+ retval = fmt::format(FMT_STRING("info: {} file -- {}"),
+ make_visible ? "showing" : "hiding",
+ lf->get_filename());
+ } else {
+ int text_file_count = 0, log_file_count = 0;
+ auto lexer = shlex(cmdline);
+
+ lexer.split(args, ec.create_resolver());
+ args.erase(args.begin());
+
+ for (const auto& lf : lnav_data.ld_active_files.fc_files) {
+ if (lf.get() == nullptr) {
+ continue;
+ }
+
+ auto ld_opt = lnav_data.ld_log_source.find_data(lf);
+
+ if (!ld_opt || ld_opt.value()->ld_visible == make_visible) {
+ continue;
+ }
+
+ auto find_iter
+ = find_if(args.begin(), args.end(), [&lf](const auto& arg) {
+ return fnmatch(arg.c_str(), lf->get_filename().c_str(), 0)
+ == 0;
+ });
+
+ if (find_iter == args.end()) {
+ continue;
+ }
+
+ if (!ec.ec_dry_run) {
+ ld_opt | [make_visible](auto ld) {
+ ld->set_visibility(make_visible);
+ };
+ }
+ if (lf->get_format() != nullptr) {
+ log_file_count += 1;
+ } else {
+ text_file_count += 1;
+ }
+ }
+ if (!ec.ec_dry_run && log_file_count > 0) {
+ lnav_data.ld_views[LNV_LOG]
+ .get_sub_source()
+ ->text_filters_changed();
+ }
+ if (!ec.ec_dry_run && text_file_count > 0) {
+ lnav_data.ld_views[LNV_TEXT]
+ .get_sub_source()
+ ->text_filters_changed();
+ }
+ retval = fmt::format(
+ FMT_STRING("info: {} {:L} log files and {:L} text files"),
+ make_visible ? "showing" : "hiding",
+ log_file_count,
+ text_file_count);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_hide_file(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("visible-files");
+ } else {
+ return com_file_visibility(ec, cmdline, args);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_show_file(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("hidden-files");
+ } else {
+ return com_file_visibility(ec, cmdline, args);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_show_only_this_file(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else {
+ return com_file_visibility(ec, cmdline, args);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_comment(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ return Ok(std::string());
+ } else if (args.size() > 1) {
+ if (ec.ec_dry_run) {
+ return Ok(std::string());
+ }
+ auto* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ return ec.make_error(
+ "The :comment command only works in the log view");
+ }
+ auto& lss = lnav_data.ld_log_source;
+
+ args[1] = trim(remaining_args(cmdline, args));
+ auto unquoted = auto_buffer::alloc(args[1].size() + 1);
+ auto unquoted_len = unquote_content(
+ unquoted.in(), args[1].c_str(), args[1].size(), 0);
+ unquoted.resize(unquoted_len + 1);
+
+ tc->set_user_mark(&textview_curses::BM_META, tc->get_selection(), true);
+
+ auto& line_meta = lss.get_bookmark_metadata(tc->get_selection());
+
+ line_meta.bm_comment = unquoted.in();
+ lss.set_line_meta_changed();
+ lss.text_filters_changed();
+ tc->reload_data();
+
+ retval = "info: comment added to line";
+ } else {
+ return ec.make_error("expecting some comment text");
+ }
+
+ return Ok(retval);
+}
+
+static std::string
+com_comment_prompt(exec_context& ec, const std::string& cmdline)
+{
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ return "";
+ }
+ auto& lss = lnav_data.ld_log_source;
+
+ auto line_meta_opt = lss.find_bookmark_metadata(tc->get_selection());
+
+ if (line_meta_opt && !line_meta_opt.value()->bm_comment.empty()) {
+ auto trimmed_comment = trim(line_meta_opt.value()->bm_comment);
+ auto buf = auto_buffer::alloc(trimmed_comment.size() + 16);
+ quote_content(buf, trimmed_comment, 0);
+
+ return trim(cmdline) + " " + buf.to_string();
+ }
+
+ return "";
+}
+
+static Result<std::string, lnav::console::user_message>
+com_clear_comment(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ return Ok(std::string());
+ } else if (ec.ec_dry_run) {
+ return Ok(std::string());
+ } else {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ return ec.make_error(
+ "The :clear-comment command only works in the log view");
+ }
+ auto& lss = lnav_data.ld_log_source;
+
+ auto line_meta_opt = lss.find_bookmark_metadata(tc->get_selection());
+ if (line_meta_opt) {
+ bookmark_metadata& line_meta = *(line_meta_opt.value());
+
+ line_meta.bm_comment.clear();
+ if (line_meta.empty()) {
+ lss.erase_bookmark_metadata(tc->get_selection());
+ tc->set_user_mark(
+ &textview_curses::BM_META, tc->get_selection(), false);
+ }
+
+ lss.set_line_meta_changed();
+ lss.text_filters_changed();
+ tc->reload_data();
+
+ retval = "info: cleared comment";
+ }
+ tc->search_new_data();
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_tag(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("tag");
+ return Ok(std::string());
+ } else if (args.size() > 1) {
+ if (ec.ec_dry_run) {
+ return Ok(std::string());
+ }
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ return ec.make_error("The :tag command only works in the log view");
+ }
+ auto& lss = lnav_data.ld_log_source;
+
+ tc->set_user_mark(&textview_curses::BM_META, tc->get_selection(), true);
+ auto& line_meta = lss.get_bookmark_metadata(tc->get_selection());
+ for (size_t lpc = 1; lpc < args.size(); lpc++) {
+ std::string tag = args[lpc];
+
+ if (!startswith(tag, "#")) {
+ tag = "#" + tag;
+ }
+ bookmark_metadata::KNOWN_TAGS.insert(tag);
+ line_meta.add_tag(tag);
+ }
+ tc->search_new_data();
+ lss.set_line_meta_changed();
+ lss.text_filters_changed();
+ tc->reload_data();
+
+ retval = "info: tag(s) added to line";
+ } else {
+ return ec.make_error("expecting one or more tags");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_untag(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("line-tags");
+ return Ok(std::string());
+ } else if (args.size() > 1) {
+ if (ec.ec_dry_run) {
+ return Ok(std::string());
+ }
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ return ec.make_error(
+ "The :untag command only works in the log view");
+ }
+ auto& lss = lnav_data.ld_log_source;
+
+ auto line_meta_opt = lss.find_bookmark_metadata(tc->get_selection());
+ if (line_meta_opt) {
+ bookmark_metadata& line_meta = *(line_meta_opt.value());
+
+ for (size_t lpc = 1; lpc < args.size(); lpc++) {
+ std::string tag = args[lpc];
+
+ if (!startswith(tag, "#")) {
+ tag = "#" + tag;
+ }
+ line_meta.remove_tag(tag);
+ }
+ if (line_meta.empty()) {
+ tc->set_user_mark(
+ &textview_curses::BM_META, tc->get_selection(), false);
+ }
+ }
+ tc->search_new_data();
+ lss.set_line_meta_changed();
+ lss.text_filters_changed();
+ tc->reload_data();
+
+ retval = "info: tag(s) removed from line";
+ } else {
+ return ec.make_error("expecting one or more tags");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_delete_tags(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("tag");
+ return Ok(std::string());
+ } else if (args.size() > 1) {
+ if (ec.ec_dry_run) {
+ return Ok(std::string());
+ }
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ return ec.make_error(
+ "The :delete-tag command only works in the log view");
+ }
+
+ auto& known_tags = bookmark_metadata::KNOWN_TAGS;
+ std::vector<std::string> tags;
+
+ for (size_t lpc = 1; lpc < args.size(); lpc++) {
+ std::string tag = args[lpc];
+
+ if (!startswith(tag, "#")) {
+ tag = "#" + tag;
+ }
+ if (known_tags.find(tag) == known_tags.end()) {
+ return ec.make_error("Unknown tag -- {}", tag);
+ }
+
+ tags.emplace_back(tag);
+ known_tags.erase(tag);
+ }
+
+ auto& lss = lnav_data.ld_log_source;
+ auto& vbm = tc->get_bookmarks()[&textview_curses::BM_META];
+
+ for (auto iter = vbm.begin(); iter != vbm.end();) {
+ auto line_meta_opt = lss.find_bookmark_metadata(*iter);
+
+ if (!line_meta_opt) {
+ ++iter;
+ continue;
+ }
+
+ auto& line_meta = line_meta_opt.value();
+ for (const auto& tag : tags) {
+ line_meta->remove_tag(tag);
+ }
+
+ if (line_meta->empty()) {
+ lss.erase_bookmark_metadata(*iter);
+ size_t off = distance(vbm.begin(), iter);
+
+ tc->set_user_mark(&textview_curses::BM_META, *iter, false);
+ iter = next(vbm.begin(), off);
+ } else {
+ ++iter;
+ }
+ }
+
+ retval = "info: deleted tag(s)";
+ } else {
+ return ec.make_error("expecting one or more tags");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_partition_name(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ return Ok(std::string());
+ } else if (args.size() > 1) {
+ if (ec.ec_dry_run) {
+ retval = "";
+ } else {
+ textview_curses& tc = lnav_data.ld_views[LNV_LOG];
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+
+ args[1] = trim(remaining_args(cmdline, args));
+
+ tc.set_user_mark(
+ &textview_curses::BM_META, tc.get_selection(), true);
+
+ auto& line_meta = lss.get_bookmark_metadata(tc.get_selection());
+
+ line_meta.bm_name = args[1];
+ retval = "info: name set for partition";
+ }
+ } else {
+ return ec.make_error("expecting partition name");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_clear_partition(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ return Ok(std::string());
+ } else if (args.size() == 1) {
+ textview_curses& tc = lnav_data.ld_views[LNV_LOG];
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ auto& bv = tc.get_bookmarks()[&textview_curses::BM_META];
+ nonstd::optional<vis_line_t> part_start;
+
+ if (binary_search(bv.begin(), bv.end(), tc.get_selection())) {
+ part_start = tc.get_selection();
+ } else {
+ part_start = bv.prev(tc.get_selection());
+ }
+ if (!part_start) {
+ return ec.make_error("top line is not in a partition");
+ }
+
+ if (!ec.ec_dry_run) {
+ auto& line_meta = lss.get_bookmark_metadata(part_start.value());
+
+ line_meta.bm_name.clear();
+ if (line_meta.empty()) {
+ lss.erase_bookmark_metadata(part_start.value());
+ tc.set_user_mark(
+ &textview_curses::BM_META, part_start.value(), false);
+ }
+
+ retval = "info: cleared partition name";
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_summarize(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("colname");
+ return Ok(retval);
+ } else if (!setup_logline_table(ec)) {
+ return ec.make_error("no log data available");
+ } else if (args.size() == 1) {
+ return ec.make_error("no columns specified");
+ } else {
+ auto_mem<char, sqlite3_free> query_frag;
+ std::vector<std::string> other_columns;
+ std::vector<std::string> num_columns;
+ const auto& top_source = ec.ec_source.back();
+ sql_progress_guard progress_guard(sql_progress,
+ sql_progress_finished,
+ top_source.s_location,
+ top_source.s_content);
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+ int retcode;
+ std::string query;
+
+ query = "SELECT ";
+ for (size_t lpc = 1; lpc < args.size(); lpc++) {
+ if (lpc > 1)
+ query += ", ";
+ query += args[lpc];
+ }
+ query += " FROM logline ";
+
+ retcode = sqlite3_prepare_v2(
+ lnav_data.ld_db.in(), query.c_str(), -1, stmt.out(), nullptr);
+ if (retcode != SQLITE_OK) {
+ const char* errmsg = sqlite3_errmsg(lnav_data.ld_db);
+
+ return ec.make_error("{}", errmsg);
+ }
+
+ switch (sqlite3_step(stmt.in())) {
+ case SQLITE_OK:
+ case SQLITE_DONE: {
+ return ec.make_error("no data");
+ } break;
+ case SQLITE_ROW:
+ break;
+ default: {
+ const char* errmsg;
+
+ errmsg = sqlite3_errmsg(lnav_data.ld_db);
+ return ec.make_error("{}", errmsg);
+ } break;
+ }
+
+ if (ec.ec_dry_run) {
+ return Ok(std::string());
+ }
+
+ for (int lpc = 0; lpc < sqlite3_column_count(stmt.in()); lpc++) {
+ switch (sqlite3_column_type(stmt.in(), lpc)) {
+ case SQLITE_INTEGER:
+ case SQLITE_FLOAT:
+ num_columns.push_back(args[lpc + 1]);
+ break;
+ default:
+ other_columns.push_back(args[lpc + 1]);
+ break;
+ }
+ }
+
+ query = "SELECT";
+ for (auto iter = other_columns.begin(); iter != other_columns.end();
+ ++iter)
+ {
+ if (iter != other_columns.begin()) {
+ query += ",";
+ }
+ query_frag
+ = sqlite3_mprintf(" %s as \"c_%s\", count(*) as \"count_%s\"",
+ iter->c_str(),
+ iter->c_str(),
+ iter->c_str());
+ query += query_frag;
+ }
+
+ if (!other_columns.empty() && !num_columns.empty()) {
+ query += ", ";
+ }
+
+ for (auto iter = num_columns.begin(); iter != num_columns.end(); ++iter)
+ {
+ if (iter != num_columns.begin()) {
+ query += ",";
+ }
+ query_frag = sqlite3_mprintf(
+ " sum(\"%s\"), "
+ " min(\"%s\"), "
+ " avg(\"%s\"), "
+ " median(\"%s\"), "
+ " stddev(\"%s\"), "
+ " max(\"%s\") ",
+ iter->c_str(),
+ iter->c_str(),
+ iter->c_str(),
+ iter->c_str(),
+ iter->c_str(),
+ iter->c_str());
+ query += query_frag;
+ }
+
+ query
+ += (" FROM logline "
+ "WHERE (logline.log_part is null or "
+ "startswith(logline.log_part, '.') = 0) ");
+
+ for (auto iter = other_columns.begin(); iter != other_columns.end();
+ ++iter)
+ {
+ if (iter == other_columns.begin()) {
+ query += " GROUP BY ";
+ } else {
+ query += ",";
+ }
+ query_frag = sqlite3_mprintf(" \"c_%s\"", iter->c_str());
+ query += query_frag;
+ }
+
+ for (auto iter = other_columns.begin(); iter != other_columns.end();
+ ++iter)
+ {
+ if (iter == other_columns.begin()) {
+ query += " ORDER BY ";
+ } else {
+ query += ",";
+ }
+ query_frag = sqlite3_mprintf(
+ " \"count_%s\" desc, \"c_%s\" collate naturalnocase asc",
+ iter->c_str(),
+ iter->c_str());
+ query += query_frag;
+ }
+ log_debug("query %s", query.c_str());
+
+ db_label_source& dls = lnav_data.ld_db_row_source;
+
+ dls.clear();
+ retcode = sqlite3_prepare_v2(
+ lnav_data.ld_db.in(), query.c_str(), -1, stmt.out(), nullptr);
+
+ if (retcode != SQLITE_OK) {
+ const char* errmsg = sqlite3_errmsg(lnav_data.ld_db);
+
+ return ec.make_error("{}", errmsg);
+ } else if (stmt == nullptr) {
+ retval = "";
+ } else {
+ bool done = false;
+
+ while (!done) {
+ retcode = sqlite3_step(stmt.in());
+
+ switch (retcode) {
+ case SQLITE_OK:
+ case SQLITE_DONE:
+ done = true;
+ break;
+
+ case SQLITE_ROW:
+ ec.ec_sql_callback(ec, stmt.in());
+ break;
+
+ default: {
+ const char* errmsg;
+
+ errmsg = sqlite3_errmsg(lnav_data.ld_db);
+ return ec.make_error("{}", errmsg);
+ }
+ }
+ }
+
+ if (retcode == SQLITE_DONE) {
+ lnav_data.ld_views[LNV_LOG].reload_data();
+ lnav_data.ld_views[LNV_DB].reload_data();
+ lnav_data.ld_views[LNV_DB].set_left(0);
+
+ if (dls.dls_rows.size() > 0) {
+ ensure_view(&lnav_data.ld_views[LNV_DB]);
+ }
+ }
+
+ lnav_data.ld_bottom_source.update_loading(0, 0);
+ lnav_data.ld_status[LNS_BOTTOM].do_update();
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_add_test(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (args.size() > 1) {
+ return ec.make_error("not expecting any arguments");
+ } else if (ec.ec_dry_run) {
+ } else {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ bookmark_vector<vis_line_t>& bv
+ = tc->get_bookmarks()[&textview_curses::BM_USER];
+ bookmark_vector<vis_line_t>::iterator iter;
+
+ for (iter = bv.begin(); iter != bv.end(); ++iter) {
+ auto_mem<FILE> file(fclose);
+ char path[PATH_MAX];
+ std::string line;
+
+ tc->grep_value_for_line(*iter, line);
+
+ line.insert(0, 13, ' ');
+
+ snprintf(path,
+ sizeof(path),
+ "%s/test/log-samples/sample-%s.txt",
+ getenv("LNAV_SRC"),
+ hasher().update(line).to_string().c_str());
+
+ if ((file = fopen(path, "w")) == nullptr) {
+ perror("fopen failed");
+ } else {
+ fprintf(file, "%s\n", line.c_str());
+ }
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_switch_to_view(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("viewname");
+ } else if (args.size() > 1) {
+ bool found = false;
+
+ for (int lpc = 0; lnav_view_strings[lpc] && !found; lpc++) {
+ if (strcasecmp(args[1].c_str(), lnav_view_strings[lpc]) == 0) {
+ if (!ec.ec_dry_run) {
+ if (args[0] == "switch-to-view") {
+ ensure_view(&lnav_data.ld_views[lpc]);
+ } else {
+ toggle_view(&lnav_data.ld_views[lpc]);
+ }
+ }
+ found = true;
+ }
+ }
+
+ if (!found) {
+ return ec.make_error("invalid view name -- {}", args[1]);
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_toggle_filtering(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ auto tc = *lnav_data.ld_view_stack.top();
+ auto tss = tc->get_sub_source();
+
+ tss->toggle_apply_filters();
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_zoom_to(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("zoomlevel");
+ } else if (ec.ec_dry_run) {
+ } else if (args.size() > 1) {
+ bool found = false;
+
+ for (size_t lpc = 0; lpc < lnav_zoom_strings.size() && !found; lpc++) {
+ if (strcasecmp(args[1].c_str(), lnav_zoom_strings[lpc].c_str())
+ == 0)
+ {
+ auto& ss = *lnav_data.ld_spectro_source;
+ struct timeval old_time;
+
+ lnav_data.ld_zoom_level = lpc;
+
+ auto& hist_view = lnav_data.ld_views[LNV_HISTOGRAM];
+
+ if (hist_view.get_inner_height() > 0) {
+ auto old_time_opt = lnav_data.ld_hist_source2.time_for_row(
+ lnav_data.ld_views[LNV_HISTOGRAM].get_top());
+ if (old_time_opt) {
+ old_time = old_time_opt.value();
+ rebuild_hist();
+ lnav_data.ld_hist_source2.row_for_time(old_time) |
+ [](auto new_top) {
+ lnav_data.ld_views[LNV_HISTOGRAM].set_top(
+ new_top);
+ };
+ }
+ }
+
+ auto& spectro_view = lnav_data.ld_views[LNV_SPECTRO];
+
+ if (spectro_view.get_inner_height() > 0) {
+ auto old_time_opt
+ = lnav_data.ld_spectro_source->time_for_row(
+ lnav_data.ld_views[LNV_SPECTRO].get_selection());
+ ss.ss_granularity = ZOOM_LEVELS[lnav_data.ld_zoom_level];
+ ss.invalidate();
+ spectro_view.reload_data();
+ if (old_time_opt) {
+ lnav_data.ld_spectro_source->row_for_time(
+ old_time_opt.value())
+ | [](auto new_top) {
+ lnav_data.ld_views[LNV_SPECTRO].set_selection(
+ new_top);
+ };
+ }
+ }
+
+ lnav_data.ld_view_stack.set_needs_update();
+
+ found = true;
+ }
+ }
+
+ if (!found) {
+ auto um = lnav::console::user_message::error(
+ attr_line_t("invalid zoom level: ")
+ .append(lnav::roles::symbol(args[1])))
+ .with_snippets(ec.ec_source)
+ .with_help(attr_line_t("available levels: ")
+ .join(lnav_zoom_strings, ", "));
+ return Err(um);
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_reset_session(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ reset_session();
+ lnav_data.ld_views[LNV_LOG].reload_data();
+ }
+
+ return Ok(std::string());
+}
+
+static Result<std::string, lnav::console::user_message>
+com_load_session(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ load_session();
+ lnav_data.ld_views[LNV_LOG].reload_data();
+ }
+
+ return Ok(std::string());
+}
+
+static Result<std::string, lnav::console::user_message>
+com_save_session(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ save_session();
+ }
+
+ return Ok(std::string());
+}
+
+static Result<std::string, lnav::console::user_message>
+com_export_session_to(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("filename");
+ } else if (!ec.ec_dry_run) {
+ auto_mem<FILE> outfile(fclose);
+ auto fn = trim(remaining_args(cmdline, args));
+ auto to_term = false;
+
+ if (fn == "-" || fn == "/dev/stdout") {
+ auto ec_out = ec.get_output();
+
+ if (!ec_out) {
+ outfile = auto_mem<FILE>::leak(stdout);
+ nodelay(lnav_data.ld_window, 0);
+ endwin();
+ struct termios curr_termios;
+ tcgetattr(1, &curr_termios);
+ curr_termios.c_oflag |= ONLCR | OPOST;
+ tcsetattr(1, TCSANOW, &curr_termios);
+ setvbuf(stdout, nullptr, _IONBF, 0);
+ to_term = true;
+ fprintf(
+ outfile,
+ "\n---------------- Press any key to exit lo-fi display "
+ "----------------\n\n");
+ } else {
+ outfile = auto_mem<FILE>::leak(ec_out.value());
+ }
+ if (outfile.in() == stdout) {
+ lnav_data.ld_stdout_used = true;
+ }
+ } else if (fn == "/dev/clipboard") {
+ auto open_res = sysclip::open(sysclip::type_t::GENERAL);
+ if (open_res.isErr()) {
+ alerter::singleton().chime("cannot open clipboard");
+ return ec.make_error("Unable to copy to clipboard: {}",
+ open_res.unwrapErr());
+ }
+ outfile = open_res.unwrap();
+ } else if (lnav_data.ld_flags & LNF_SECURE_MODE) {
+ return ec.make_error("{} -- unavailable in secure mode", args[0]);
+ } else {
+ if ((outfile = fopen(fn.c_str(), "we")) == nullptr) {
+ return ec.make_error("unable to open file -- {}", fn);
+ }
+ fchmod(fileno(outfile.in()), S_IRWXU);
+ }
+
+ auto export_res = lnav::session::export_to(outfile.in());
+
+ fflush(outfile.in());
+ if (to_term) {
+ cbreak();
+ getch();
+ refresh();
+ nodelay(lnav_data.ld_window, 1);
+ }
+ if (export_res.isErr()) {
+ return Err(export_res.unwrapErr());
+ }
+
+ retval = fmt::format(
+ FMT_STRING("info: wrote session commands to -- {}"), fn);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_set_min_log_level(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("levelname");
+ } else if (ec.ec_dry_run) {
+ retval = "";
+ } else if (args.size() == 2) {
+ auto& lss = lnav_data.ld_log_source;
+ auto new_level = string2level(args[1].c_str(), args[1].size(), false);
+ lss.set_min_log_level(new_level);
+
+ retval = ("info: minimum log level is now -- "
+ + std::string(level_names[new_level]));
+ } else {
+ return ec.make_error("expecting a log level name");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_toggle_field(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("colname");
+ } else if (args.size() < 2) {
+ return ec.make_error("Expecting a log message field name");
+ } else {
+ auto* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc != &lnav_data.ld_views[LNV_LOG]) {
+ retval = "error: hiding fields only works in the log view";
+ } else if (ec.ec_dry_run) {
+ // TODO: highlight the fields to be hidden.
+ retval = "";
+ } else {
+ auto& lss = lnav_data.ld_log_source;
+ bool hide = args[0] == "hide-fields";
+ std::vector<std::string> found_fields, missing_fields;
+
+ for (int lpc = 1; lpc < (int) args.size(); lpc++) {
+ intern_string_t name;
+ std::shared_ptr<log_format> format;
+ size_t dot;
+
+ if ((dot = args[lpc].find('.')) != std::string::npos) {
+ const intern_string_t format_name
+ = intern_string::lookup(args[lpc].c_str(), dot);
+
+ format = log_format::find_root_format(format_name.get());
+ if (!format) {
+ return ec.make_error("unknown format -- {}",
+ format_name.to_string());
+ }
+ name = intern_string::lookup(&(args[lpc].c_str()[dot + 1]),
+ args[lpc].length() - dot - 1);
+ } else if (tc->get_inner_height() == 0) {
+ return ec.make_error("no log messages to hide");
+ } else {
+ auto cl = lss.at(tc->get_selection());
+ auto lf = lss.find(cl);
+ format = lf->get_format();
+ name = intern_string::lookup(args[lpc]);
+ }
+
+ if (format->hide_field(name, hide)) {
+ found_fields.push_back(args[lpc]);
+ if (hide) {
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(x, "to quickly show hidden fields"));
+ }
+ }
+ tc->set_needs_update();
+ } else {
+ missing_fields.push_back(args[lpc]);
+ }
+ }
+
+ if (missing_fields.empty()) {
+ auto visibility = hide ? "hiding" : "showing";
+ retval = fmt::format(FMT_STRING("info: {} field(s) -- {}"),
+ visibility,
+ fmt::join(found_fields, ", "));
+ } else {
+ return ec.make_error("unknown field(s) -- {}",
+ fmt::join(missing_fields, ", "));
+ }
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_hide_line(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("move-time");
+ } else if (args.size() == 1) {
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+
+ if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ struct timeval min_time, max_time;
+ bool have_min_time = lss.get_min_log_time(min_time);
+ bool have_max_time = lss.get_max_log_time(max_time);
+ char min_time_str[32], max_time_str[32];
+
+ if (have_min_time) {
+ sql_strftime(min_time_str, sizeof(min_time_str), min_time);
+ }
+ if (have_max_time) {
+ sql_strftime(max_time_str, sizeof(max_time_str), max_time);
+ }
+ if (have_min_time && have_max_time) {
+ retval
+ = fmt::format("info: hiding lines before {} and after {}",
+ min_time_str,
+ max_time_str);
+ } else if (have_min_time) {
+ retval
+ = fmt::format("info: hiding lines before {}", min_time_str);
+ } else if (have_max_time) {
+ retval
+ = fmt::format("info: hiding lines after {}", max_time_str);
+ } else {
+ retval
+ = "info: no lines hidden by time, pass an absolute or "
+ "relative time";
+ }
+ } else {
+ return ec.make_error(
+ "hiding lines by time only works in the log view");
+ }
+ } else if (args.size() >= 2) {
+ std::string all_args = remaining_args(cmdline, args);
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ date_time_scanner dts;
+ struct timeval tv;
+ bool tv_set = false;
+ auto parse_res = relative_time::from_str(all_args);
+
+ if (parse_res.isOk()) {
+ if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ if (tc->get_inner_height() > 0) {
+ content_line_t cl;
+ struct exttm tm;
+ vis_line_t vl;
+ logline* ll;
+
+ vl = tc->get_selection();
+ cl = lnav_data.ld_log_source.at(vl);
+ ll = lnav_data.ld_log_source.find_line(cl);
+ ll->to_exttm(tm);
+ tv = parse_res.unwrap().adjust(tm).to_timeval();
+
+ tv_set = true;
+ }
+ } else {
+ return ec.make_error(
+ "relative time values only work in the log view");
+ }
+ } else if (dts.convert_to_timeval(all_args, tv)) {
+ if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ tv_set = true;
+ } else {
+ return ec.make_error("time values only work in the log view");
+ }
+ }
+
+ if (tv_set && !ec.ec_dry_run) {
+ char time_text[256];
+ std::string relation;
+
+ sql_strftime(time_text, sizeof(time_text), tv);
+ if (args[0] == "hide-lines-before") {
+ lss.set_min_log_time(tv);
+ relation = "before";
+ } else {
+ lss.set_max_log_time(tv);
+ relation = "after";
+ }
+
+ retval = "info: hiding lines " + relation + " " + time_text;
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_show_lines(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval = "info: showing lines";
+
+ if (ec.ec_dry_run) {
+ retval = "";
+ } else if (!args.empty()) {
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ lss.clear_min_max_log_times();
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_hide_unmarked(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval = "info: hid unmarked lines";
+
+ if (args.empty()) {
+ } else if (ec.ec_dry_run) {
+ retval = "";
+ } else {
+ auto* tc = *lnav_data.ld_view_stack.top();
+ const auto& bv = tc->get_bookmarks()[&textview_curses::BM_USER];
+ const auto& bv_expr
+ = tc->get_bookmarks()[&textview_curses::BM_USER_EXPR];
+
+ if (bv.empty() && bv_expr.empty()) {
+ return ec.make_error("no lines have been marked");
+ } else {
+ lnav_data.ld_log_source.set_marked_only(true);
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_show_unmarked(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval = "info: showing unmarked lines";
+
+ if (ec.ec_dry_run) {
+ retval = "";
+ } else {
+ lnav_data.ld_log_source.set_marked_only(false);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_rebuild(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ rescan_files(true);
+ rebuild_indexes_repeatedly();
+ }
+
+ return Ok(std::string());
+}
+
+static Result<std::string, lnav::console::user_message>
+com_shexec(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ log_perror(system(cmdline.substr(args[0].size()).c_str()));
+ }
+
+ return Ok(std::string());
+}
+
+static Result<std::string, lnav::console::user_message>
+com_poll_now(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ isc::to<curl_looper&, services::curl_streamer_t>().send_and_wait(
+ [](auto& clooper) { clooper.process_all(); });
+ }
+
+ return Ok(std::string());
+}
+
+static Result<std::string, lnav::console::user_message>
+com_test_comment(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ return Ok(std::string());
+}
+
+static Result<std::string, lnav::console::user_message>
+com_redraw(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ if (args.empty()) {
+ } else if (ec.ec_dry_run) {
+ } else if (lnav_data.ld_window) {
+ redrawwin(lnav_data.ld_window);
+ }
+
+ return Ok(std::string());
+}
+
+static Result<std::string, lnav::console::user_message>
+com_echo(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ std::string retval = "error: expecting a message";
+
+ if (args.empty()) {
+ } else if (args.size() >= 1) {
+ bool lf = true;
+ std::string src;
+
+ if (args.size() > 2 && args[1] == "-n") {
+ std::string::size_type index_in_cmdline = cmdline.find(args[1]);
+
+ lf = false;
+ src = cmdline.substr(index_in_cmdline + args[1].length() + 1);
+ } else if (args.size() >= 2) {
+ src = cmdline.substr(args[0].length() + 1);
+ } else {
+ src = "";
+ }
+
+ auto lexer = shlex(src);
+ lexer.eval(retval, ec.create_resolver());
+
+ auto ec_out = ec.get_output();
+ if (ec.ec_dry_run) {
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "The text to output:");
+ lnav_data.ld_preview_source.replace_with(attr_line_t(retval));
+ retval = "";
+ } else if (ec_out) {
+ FILE* outfile = *ec_out;
+
+ if (outfile == stdout) {
+ lnav_data.ld_stdout_used = true;
+ }
+
+ fprintf(outfile, "%s", retval.c_str());
+ if (lf) {
+ putc('\n', outfile);
+ }
+ fflush(outfile);
+
+ retval = "";
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_alt_msg(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ } else if (ec.ec_dry_run) {
+ retval = "";
+ } else if (args.size() == 1) {
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->set_alt_value("");
+ }
+ retval = "";
+ } else {
+ std::string msg = remaining_args(cmdline, args);
+
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->set_alt_value(msg);
+ }
+
+ retval = "";
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_eval(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("*");
+ } else if (args.size() > 1) {
+ static intern_string_t EVAL_SRC = intern_string::lookup(":eval");
+
+ std::string all_args = remaining_args(cmdline, args);
+ std::string expanded_cmd;
+ shlex lexer(all_args.c_str(), all_args.size());
+
+ log_debug("Evaluating: %s", all_args.c_str());
+ if (!lexer.eval(expanded_cmd,
+ {
+ &ec.ec_local_vars.top(),
+ &ec.ec_global_vars,
+ }))
+ {
+ return ec.make_error("invalid arguments");
+ }
+ log_debug("Expanded command to evaluate: %s", expanded_cmd.c_str());
+
+ if (expanded_cmd.empty()) {
+ return ec.make_error("empty result after evaluation");
+ }
+
+ if (ec.ec_dry_run) {
+ attr_line_t al(expanded_cmd);
+
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "The command to be executed:");
+
+ lnav_data.ld_preview_source.replace_with(al);
+
+ return Ok(std::string());
+ }
+
+ auto src_guard = ec.enter_source(EVAL_SRC, 1, expanded_cmd);
+ std::string alt_msg;
+ switch (expanded_cmd[0]) {
+ case ':':
+ return execute_command(ec, expanded_cmd.substr(1));
+ case ';':
+ return execute_sql(ec, expanded_cmd.substr(1), alt_msg);
+ case '|':
+ return execute_file(ec, expanded_cmd.substr(1));
+ case '/': {
+ auto search_cmd = expanded_cmd.substr(1);
+ lnav_data.ld_view_stack.top() |
+ [&search_cmd](auto tc) { tc->execute_search(search_cmd); };
+ break;
+ }
+ default:
+ return ec.make_error(
+ "expecting argument to start with ':', ';', '/', "
+ "or '|' to signify a command, SQL query, or script to "
+ "execute");
+ }
+ } else {
+ return ec.make_error("expecting a command or query to evaluate");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_config(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("config-option");
+ } else if (args.size() > 1) {
+ static const auto INPUT_SRC = intern_string::lookup("input");
+
+ yajlpp_parse_context ypc(INPUT_SRC, &lnav_config_handlers);
+ std::vector<lnav::console::user_message> errors;
+ std::string option = args[1];
+
+ lnav_config = rollback_lnav_config;
+ ypc.set_path(option)
+ .with_obj(lnav_config)
+ .with_error_reporter([&errors](const auto& ypc, auto msg) {
+ errors.push_back(msg);
+ });
+ ypc.ypc_active_paths.insert(option);
+ ypc.update_callbacks();
+
+ const auto* jph = ypc.ypc_current_handler;
+
+ if (jph == nullptr && !ypc.ypc_handler_stack.empty()) {
+ jph = ypc.ypc_handler_stack.back();
+ }
+
+ if (jph != nullptr) {
+ yajlpp_gen gen;
+ yajlpp_gen_context ygc(gen, lnav_config_handlers);
+ yajl_gen_config(gen, yajl_gen_beautify, 1);
+ ygc.with_context(ypc);
+
+ if (ypc.ypc_current_handler == nullptr) {
+ ygc.gen();
+ } else {
+ jph->gen(ygc, gen);
+ }
+
+ auto old_value = gen.to_string_fragment().to_string();
+
+ if (args.size() == 2 || ypc.ypc_current_handler == nullptr) {
+ lnav_config = rollback_lnav_config;
+ reload_config(errors);
+
+ if (ec.ec_dry_run) {
+ attr_line_t al(old_value);
+
+ lnav_data.ld_preview_source.replace_with(al)
+ .set_text_format(detect_text_format(old_value))
+ .truncate_to(10);
+ lnav_data.ld_preview_status_source.get_description()
+ .set_value("Value of option: %s", option.c_str());
+
+ char help_text[1024];
+
+ snprintf(help_text,
+ sizeof(help_text),
+ ANSI_BOLD("%s") " " ANSI_UNDERLINE("%s") " -- %s",
+ jph->jph_property.c_str(),
+ jph->jph_synopsis,
+ jph->jph_description);
+
+ retval = help_text;
+ } else {
+ retval = fmt::format(
+ FMT_STRING("{} = {}"), option, trim(old_value));
+ }
+ } else if (lnav_data.ld_flags & LNF_SECURE_MODE
+ && !startswith(option, "/ui/"))
+ {
+ return ec.make_error(":config {} -- unavailable in secure mode",
+ option);
+ } else {
+ auto value = remaining_args(cmdline, args, 2);
+ bool changed = false;
+
+ if (ec.ec_dry_run) {
+ char help_text[1024];
+
+ snprintf(help_text,
+ sizeof(help_text),
+ ANSI_BOLD("%s %s") " -- %s",
+ jph->jph_property.c_str(),
+ jph->jph_synopsis,
+ jph->jph_description);
+
+ retval = help_text;
+ }
+
+ if (ypc.ypc_current_handler->jph_callbacks.yajl_string) {
+ ypc.ypc_callbacks.yajl_string(
+ &ypc,
+ (const unsigned char*) value.c_str(),
+ value.size());
+ changed = true;
+ } else if (ypc.ypc_current_handler->jph_callbacks.yajl_integer)
+ {
+ auto scan_res = scn::scan_value<int64_t>(value);
+ if (!scan_res || !scan_res.empty()) {
+ return ec.make_error("expecting an integer, found: {}",
+ value);
+ }
+ ypc.ypc_callbacks.yajl_integer(&ypc, scan_res.value());
+ changed = true;
+ } else if (ypc.ypc_current_handler->jph_callbacks.yajl_boolean)
+ {
+ bool bvalue = false;
+
+ if (strcasecmp(value.c_str(), "true") == 0) {
+ bvalue = true;
+ }
+ ypc.ypc_callbacks.yajl_boolean(&ypc, bvalue);
+ changed = true;
+ } else {
+ return ec.make_error("unhandled type");
+ }
+
+ if (!errors.empty()) {
+ return Err(errors[0]);
+ }
+
+ if (changed) {
+ intern_string_t path = intern_string::lookup(option);
+
+ lnav_config_locations[path]
+ = ec.ec_source.back().s_location;
+ reload_config(errors);
+
+ if (!errors.empty()) {
+ lnav_config = rollback_lnav_config;
+ reload_config(errors);
+ return Err(errors[0]);
+ }
+ if (!ec.ec_dry_run) {
+ retval = "info: changed config option -- " + option;
+ rollback_lnav_config = lnav_config;
+ if (!(lnav_data.ld_flags & LNF_SECURE_MODE)) {
+ save_config();
+ }
+ }
+ }
+ }
+ } else {
+ return ec.make_error("unknown configuration option -- {}", option);
+ }
+ } else {
+ return ec.make_error(
+ "expecting a configuration option to read or write");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_reset_config(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("config-option");
+ } else if (args.size() == 1) {
+ return ec.make_error("expecting a configuration option to reset");
+ } else {
+ static const auto INPUT_SRC = intern_string::lookup("input");
+
+ yajlpp_parse_context ypc(INPUT_SRC, &lnav_config_handlers);
+ std::string option = args[1];
+
+ while (!option.empty() && option.back() == '/') {
+ option.pop_back();
+ }
+ lnav_config = rollback_lnav_config;
+ ypc.set_path(option).with_obj(lnav_config);
+ ypc.ypc_active_paths.insert(option);
+ ypc.update_callbacks();
+
+ if (option == "*"
+ || (ypc.ypc_current_handler != nullptr
+ || !ypc.ypc_handler_stack.empty()))
+ {
+ if (!ec.ec_dry_run) {
+ reset_config(option);
+ rollback_lnav_config = lnav_config;
+ if (!(lnav_data.ld_flags & LNF_SECURE_MODE)) {
+ save_config();
+ }
+ }
+ if (option == "*") {
+ retval = "info: reset all options";
+ } else {
+ retval = "info: reset option -- " + option;
+ }
+ } else {
+ return ec.make_error("unknown configuration option -- {}", option);
+ }
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_spectrogram(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("numeric-colname");
+ } else if (ec.ec_dry_run) {
+ retval = "";
+ } else if (args.size() == 2) {
+ auto colname = remaining_args(cmdline, args);
+ auto& ss = *lnav_data.ld_spectro_source;
+ bool found = false;
+
+ ss.ss_granularity = ZOOM_LEVELS[lnav_data.ld_zoom_level];
+ if (ss.ss_value_source != nullptr) {
+ delete ss.ss_value_source;
+ ss.ss_value_source = nullptr;
+ }
+ ss.invalidate();
+
+ if (*lnav_data.ld_view_stack.top() == &lnav_data.ld_views[LNV_DB]) {
+ auto dsvs = std::make_unique<db_spectro_value_source>(colname);
+
+ if (dsvs->dsvs_error_msg) {
+ return Err(
+ dsvs->dsvs_error_msg.value().with_snippets(ec.ec_source));
+ }
+ ss.ss_value_source = dsvs.release();
+ found = true;
+ } else {
+ auto lsvs = std::make_unique<log_spectro_value_source>(
+ intern_string::lookup(colname));
+
+ if (!lsvs->lsvs_found) {
+ return ec.make_error("unknown numeric message field -- {}",
+ colname);
+ }
+ ss.ss_value_source = lsvs.release();
+ found = true;
+ }
+
+ if (found) {
+ ss.text_selection_changed(lnav_data.ld_views[LNV_SPECTRO]);
+ ensure_view(&lnav_data.ld_views[LNV_SPECTRO]);
+
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_2(z, Z, "to zoom in/out"));
+ }
+
+ retval = "info: visualizing field -- " + colname;
+ }
+ } else {
+ return ec.make_error("expecting a message field name");
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+com_quit(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
+{
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ lnav_data.ld_looping = false;
+ }
+ return Ok(std::string());
+}
+
+static void
+command_prompt(std::vector<std::string>& args)
+{
+ auto* tc = *lnav_data.ld_view_stack.top();
+ auto* rlc = lnav_data.ld_rl_view;
+
+ rlc->clear_possibilities(ln_mode_t::COMMAND, "move-args");
+ if (lnav_data.ld_views[LNV_LOG].get_inner_height() > 0) {
+ static const char* MOVE_TIMES[]
+ = {"here", "now", "today", "yesterday", nullptr};
+
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ textview_curses& log_view = lnav_data.ld_views[LNV_LOG];
+ content_line_t cl = lss.at(log_view.get_selection());
+ std::shared_ptr<logfile> lf = lss.find(cl);
+ auto ll = lf->begin() + cl;
+ log_data_helper ldh(lss);
+
+ lnav_data.ld_exec_context.ec_top_line = tc->get_selection();
+
+ lnav_data.ld_rl_view->clear_possibilities(ln_mode_t::COMMAND,
+ "numeric-colname");
+ lnav_data.ld_rl_view->clear_possibilities(ln_mode_t::COMMAND,
+ "colname");
+
+ ldh.parse_line(log_view.get_selection(), true);
+
+ if (tc == &lnav_data.ld_views[LNV_DB]) {
+ db_label_source& dls = lnav_data.ld_db_row_source;
+
+ for (auto& dls_header : dls.dls_headers) {
+ if (!dls_header.hm_graphable) {
+ continue;
+ }
+
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "numeric-colname", dls_header.hm_name);
+ }
+ } else {
+ for (auto& ldh_line_value : ldh.ldh_line_values.lvv_values) {
+ auto& meta = ldh_line_value.lv_meta;
+
+ if (!meta.lvm_format) {
+ continue;
+ }
+
+ const auto* stats
+ = meta.lvm_format.value()->stats_for_value(meta.lvm_name);
+
+ if (stats == nullptr) {
+ continue;
+ }
+
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND,
+ "numeric-colname",
+ meta.lvm_name.to_string());
+ }
+ }
+
+ for (auto& cn_name : ldh.ldh_namer->cn_names) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "colname", cn_name.to_string());
+ }
+ for (const auto& iter : ldh.ldh_namer->cn_builtin_names) {
+ if (iter == column_namer::BUILTIN_COL) {
+ continue;
+ }
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "colname", iter.to_string());
+ }
+
+ ldh.clear();
+
+ rlc->clear_possibilities(ln_mode_t::COMMAND, "move-time");
+ rlc->add_possibility(ln_mode_t::COMMAND, "move-time", MOVE_TIMES);
+ rlc->add_possibility(ln_mode_t::COMMAND, "move-args", MOVE_TIMES);
+ rlc->clear_possibilities(ln_mode_t::COMMAND, "line-time");
+ {
+ struct timeval tv = lf->get_time_offset();
+ char buffer[64];
+
+ sql_strftime(
+ buffer, sizeof(buffer), ll->get_time(), ll->get_millis(), 'T');
+ rlc->add_possibility(ln_mode_t::COMMAND, "line-time", buffer);
+ rlc->add_possibility(ln_mode_t::COMMAND, "move-args", buffer);
+ rlc->add_possibility(ln_mode_t::COMMAND, "move-time", buffer);
+ sql_strftime(buffer,
+ sizeof(buffer),
+ ll->get_time() - tv.tv_sec,
+ ll->get_millis() - (tv.tv_usec / 1000),
+ 'T');
+ rlc->add_possibility(ln_mode_t::COMMAND, "line-time", buffer);
+ rlc->add_possibility(ln_mode_t::COMMAND, "move-args", buffer);
+ rlc->add_possibility(ln_mode_t::COMMAND, "move-time", buffer);
+ }
+ }
+
+ rollback_lnav_config = lnav_config;
+ lnav_data.ld_doc_status_source.set_title("Command Help");
+ add_view_text_possibilities(lnav_data.ld_rl_view,
+ ln_mode_t::COMMAND,
+ "filter",
+ tc,
+ text_quoting::none);
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "filter", tc->get_current_search());
+ add_filter_possibilities(tc);
+ add_mark_possibilities();
+ add_config_possibilities();
+ add_env_possibilities(ln_mode_t::COMMAND);
+ add_tag_possibilities();
+ add_file_possibilities();
+ add_recent_netlocs_possibilities();
+
+ auto* ta = dynamic_cast<text_anchors*>(tc->get_sub_source());
+ if (ta != nullptr) {
+ rlc->add_possibility(
+ ln_mode_t::COMMAND, "move-args", ta->get_anchors());
+ }
+
+ if (tc == &lnav_data.ld_views[LNV_LOG]) {
+ add_filter_expr_possibilities(
+ lnav_data.ld_rl_view, ln_mode_t::COMMAND, "filter-expr-syms");
+ }
+ lnav_data.ld_mode = ln_mode_t::COMMAND;
+ lnav_data.ld_rl_view->focus(ln_mode_t::COMMAND,
+ cget(args, 2).value_or(":"),
+ cget(args, 3).value_or(""));
+}
+
+static void
+script_prompt(std::vector<std::string>& args)
+{
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ auto& scripts = injector::get<available_scripts&>();
+
+ lnav_data.ld_mode = ln_mode_t::EXEC;
+
+ lnav_data.ld_exec_context.ec_top_line = tc->get_selection();
+ lnav_data.ld_rl_view->clear_possibilities(ln_mode_t::EXEC, "__command");
+ find_format_scripts(lnav_data.ld_config_paths, scripts);
+ for (const auto& iter : scripts.as_scripts) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::EXEC, "__command", iter.first);
+ }
+ add_view_text_possibilities(
+ lnav_data.ld_rl_view, ln_mode_t::EXEC, "*", tc, text_quoting::regex);
+ add_env_possibilities(ln_mode_t::EXEC);
+ lnav_data.ld_rl_view->focus(ln_mode_t::EXEC,
+ cget(args, 2).value_or("|"),
+ cget(args, 3).value_or(""));
+ lnav_data.ld_bottom_source.set_prompt(
+ "Enter a script to execute: (Press " ANSI_BOLD("CTRL+]") " to abort)");
+}
+
+static void
+search_prompt(std::vector<std::string>& args)
+{
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+
+ lnav_data.ld_mode = ln_mode_t::SEARCH;
+ lnav_data.ld_search_start_line = tc->get_selection();
+ add_view_text_possibilities(
+ lnav_data.ld_rl_view, ln_mode_t::SEARCH, "*", tc, text_quoting::regex);
+ lnav_data.ld_rl_view->focus(ln_mode_t::SEARCH,
+ cget(args, 2).value_or("/"),
+ cget(args, 3).value_or(""));
+ lnav_data.ld_doc_status_source.set_title("Syntax Help");
+ rl_set_help();
+ lnav_data.ld_bottom_source.set_prompt(
+ "Search for: "
+ "(Press " ANSI_BOLD("CTRL+J") " to jump to a previous hit and "
+ ANSI_BOLD("CTRL+]") " to abort)");
+}
+
+static void
+search_filters_prompt(std::vector<std::string>& args)
+{
+ lnav_data.ld_mode = ln_mode_t::SEARCH_FILTERS;
+ lnav_data.ld_filter_view.reload_data();
+ add_view_text_possibilities(lnav_data.ld_rl_view,
+ ln_mode_t::SEARCH_FILTERS,
+ "*",
+ &lnav_data.ld_filter_view,
+ text_quoting::regex);
+ lnav_data.ld_rl_view->focus(ln_mode_t::SEARCH_FILTERS,
+ cget(args, 2).value_or("/"),
+ cget(args, 3).value_or(""));
+ lnav_data.ld_bottom_source.set_prompt(
+ "Search for: "
+ "(Press " ANSI_BOLD("CTRL+J") " to jump to a previous hit and "
+ ANSI_BOLD("CTRL+]") " to abort)");
+}
+
+static void
+search_files_prompt(std::vector<std::string>& args)
+{
+ static const std::regex re_escape(R"(([.\^$*+?()\[\]{}\\|]))");
+
+ lnav_data.ld_mode = ln_mode_t::SEARCH_FILES;
+ for (const auto& lf : lnav_data.ld_active_files.fc_files) {
+ auto path = lnav::pcre2pp::quote(lf->get_unique_path());
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SEARCH_FILES, "*", path);
+ }
+ lnav_data.ld_rl_view->focus(ln_mode_t::SEARCH_FILES,
+ cget(args, 2).value_or("/"),
+ cget(args, 3).value_or(""));
+ lnav_data.ld_bottom_source.set_prompt(
+ "Search for: "
+ "(Press " ANSI_BOLD("CTRL+J") " to jump to a previous hit and "
+ ANSI_BOLD("CTRL+]") " to abort)");
+}
+
+static void
+search_spectro_details_prompt(std::vector<std::string>& args)
+{
+ lnav_data.ld_mode = ln_mode_t::SEARCH_SPECTRO_DETAILS;
+ add_view_text_possibilities(lnav_data.ld_rl_view,
+ ln_mode_t::SEARCH_SPECTRO_DETAILS,
+ "*",
+ &lnav_data.ld_spectro_details_view,
+ text_quoting::regex);
+ lnav_data.ld_rl_view->focus(ln_mode_t::SEARCH_SPECTRO_DETAILS,
+ cget(args, 2).value_or("/"),
+ cget(args, 3).value_or(""));
+ lnav_data.ld_bottom_source.set_prompt(
+ "Search for: "
+ "(Press " ANSI_BOLD("CTRL+J") " to jump to a previous hit and "
+ ANSI_BOLD("CTRL+]") " to abort)");
+}
+
+static void
+sql_prompt(std::vector<std::string>& args)
+{
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ textview_curses& log_view = lnav_data.ld_views[LNV_LOG];
+
+ lnav_data.ld_exec_context.ec_top_line = tc->get_selection();
+
+ lnav_data.ld_mode = ln_mode_t::SQL;
+ setup_logline_table(lnav_data.ld_exec_context);
+ lnav_data.ld_rl_view->focus(ln_mode_t::SQL,
+ cget(args, 2).value_or(";"),
+ cget(args, 3).value_or(""));
+
+ lnav_data.ld_doc_status_source.set_title("Query Help");
+ rl_set_help();
+ lnav_data.ld_bottom_source.update_loading(0, 0);
+ lnav_data.ld_status[LNS_BOTTOM].do_update();
+
+ auto* fos = (field_overlay_source*) log_view.get_overlay_source();
+ fos->fos_contexts.top().c_show = true;
+ tc->set_sync_selection_and_top(true);
+ tc->reload_data();
+ lnav_data.ld_bottom_source.set_prompt(
+ "Enter an SQL query: (Press " ANSI_BOLD("CTRL+]") " to abort)");
+}
+
+static void
+user_prompt(std::vector<std::string>& args)
+{
+ textview_curses* tc = *lnav_data.ld_view_stack.top();
+ lnav_data.ld_exec_context.ec_top_line = tc->get_selection();
+
+ lnav_data.ld_mode = ln_mode_t::USER;
+ setup_logline_table(lnav_data.ld_exec_context);
+ lnav_data.ld_rl_view->focus(ln_mode_t::USER,
+ cget(args, 2).value_or("? "),
+ cget(args, 3).value_or(""));
+
+ lnav_data.ld_bottom_source.update_loading(0, 0);
+ lnav_data.ld_status[LNS_BOTTOM].do_update();
+}
+
+static Result<std::string, lnav::console::user_message>
+com_prompt(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ static std::map<std::string, std::function<void(std::vector<std::string>&)>>
+ PROMPT_TYPES = {
+ {"command", command_prompt},
+ {"script", script_prompt},
+ {"search", search_prompt},
+ {"search-filters", search_filters_prompt},
+ {"search-files", search_files_prompt},
+ {"search-spectro-details", search_spectro_details_prompt},
+ {"sql", sql_prompt},
+ {"user", user_prompt},
+ };
+
+ if (args.empty()) {
+ } else if (!ec.ec_dry_run) {
+ args.clear();
+
+ auto lexer = shlex(cmdline);
+ lexer.split(args, ec.create_resolver());
+
+ auto alt_flag = std::find(args.begin(), args.end(), "--alt");
+ auto is_alt = false;
+ if (alt_flag != args.end()) {
+ args.erase(alt_flag);
+ is_alt = true;
+ }
+
+ auto prompter = PROMPT_TYPES.find(args[1]);
+
+ if (prompter == PROMPT_TYPES.end()) {
+ return ec.make_error("Unknown prompt type: {}", args[1]);
+ }
+
+ prompter->second(args);
+ lnav_data.ld_rl_view->set_alt_focus(is_alt);
+ }
+ return Ok(std::string());
+}
+
+readline_context::command_t STD_COMMANDS[] = {
+ {"prompt",
+ com_prompt,
+
+ help_text(":prompt")
+ .with_summary("Open the given prompt")
+ .with_parameter(
+ {"type",
+ "The type of prompt -- command, script, search, sql, user"})
+ .with_parameter(
+ help_text(
+ "--alt",
+ "Perform the alternate action for this prompt by default")
+ .optional())
+ .with_parameter(
+ help_text("prompt", "The prompt to display").optional())
+ .with_parameter(
+ help_text("initial-value",
+ "The initial value to fill in for the prompt")
+ .optional())
+ .with_example({
+ "To open the command prompt with 'filter-in' already filled in",
+ "command : 'filter-in '",
+ })
+ .with_example({
+ "To ask the user a question",
+ "user 'Are you sure? '",
+ })},
+
+ {"adjust-log-time",
+ com_adjust_log_time,
+
+ help_text(":adjust-log-time")
+ .with_summary("Change the timestamps of the top file to be relative "
+ "to the given date")
+ .with_parameter(
+ help_text("timestamp",
+ "The new timestamp for the top line in the view")
+ .with_format(help_parameter_format_t::HPF_DATETIME))
+ .with_example({"To set the top timestamp to a given date",
+ "2017-01-02T05:33:00"})
+ .with_example({"To set the top timestamp back an hour", "-1h"})},
+
+ {"unix-time",
+ com_unix_time,
+
+ help_text(":unix-time")
+ .with_summary("Convert epoch time to a human-readable form")
+ .with_parameter(help_text("seconds", "The epoch timestamp to convert")
+ .with_format(help_parameter_format_t::HPF_INTEGER))
+ .with_example({"To convert the epoch time 1490191111", "1490191111"})},
+ {"current-time",
+ com_current_time,
+
+ help_text(":current-time")
+ .with_summary("Print the current time in human-readable form and "
+ "seconds since the epoch")},
+ {"goto",
+ com_goto,
+
+ help_text(":goto")
+ .with_summary("Go to the given location in the top view")
+ .with_parameter(
+ help_text("line#|N%|timestamp|#anchor",
+ "A line number, percent into the file, timestamp, "
+ "or an anchor in a text file"))
+ .with_examples(
+ {{"To go to line 22", "22"},
+ {"To go to the line 75% of the way into the view", "75%"},
+ {"To go to the first message on the first day of 2017",
+ "2017-01-01"},
+ {"To go to the Screenshots section", "#screenshots"}})
+ .with_tags({"navigation"})},
+ {"relative-goto",
+ com_relative_goto,
+
+ help_text(":relative-goto")
+ .with_summary("Move the current view up or down by the given amount")
+ .with_parameter({"line-count|N%", "The amount to move the view by."})
+ .with_examples({
+ {"To move 22 lines down in the view", "+22"},
+ {"To move 10 percent back in the view", "-10%"},
+ })
+ .with_tags({"navigation"})},
+ {"mark",
+ com_mark,
+
+ help_text(":mark")
+ .with_summary(
+ "Toggle the bookmark state for the top line in the current view")
+ .with_tags({"bookmarks"})},
+ {
+ "mark-expr",
+ com_mark_expr,
+
+ help_text(":mark-expr")
+ .with_summary("Set the bookmark expression")
+ .with_parameter(help_text(
+ "expr",
+ "The SQL expression to evaluate for each log message. "
+ "The message values can be accessed using column names "
+ "prefixed with a colon"))
+ .with_opposites({"clear-mark-expr"})
+ .with_tags({"bookmarks"})
+ .with_example(
+ {"To mark lines from 'dhclient' that mention 'eth0'",
+ ":log_procname = 'dhclient' AND :log_body LIKE '%eth0%'"}),
+
+ com_mark_expr_prompt,
+ },
+ {"clear-mark-expr",
+ com_clear_mark_expr,
+
+ help_text(":clear-mark-expr")
+ .with_summary("Clear the mark expression")
+ .with_opposites({"mark-expr"})
+ .with_tags({"bookmarks"})},
+ {"next-mark",
+ com_goto_mark,
+
+ help_text(":next-mark")
+ .with_summary(
+ "Move to the next bookmark of the given type in the current view")
+ .with_parameter(help_text("type",
+ "The type of bookmark -- error, warning, "
+ "search, user, file, meta")
+ .one_or_more())
+ .with_example({"To go to the next error", "error"})
+ .with_tags({"bookmarks", "navigation"})},
+ {"prev-mark",
+ com_goto_mark,
+
+ help_text(":prev-mark")
+ .with_summary("Move to the previous bookmark of the given type in the "
+ "current view")
+ .with_parameter(help_text("type",
+ "The type of bookmark -- error, warning, "
+ "search, user, file, meta")
+ .one_or_more())
+ .with_example({"To go to the previous error", "error"})
+ .with_tags({"bookmarks", "navigation"})},
+ {"next-location",
+ com_goto_location,
+
+ help_text(":next-location")
+ .with_summary("Move to the next position in the location history")
+ .with_tags({"navigation"})},
+ {"prev-location",
+ com_goto_location,
+
+ help_text(":prev-location")
+ .with_summary("Move to the previous position in the location history")
+ .with_tags({"navigation"})},
+ {"help",
+ com_help,
+
+ help_text(":help").with_summary("Open the help text view")},
+ {"hide-fields",
+ com_toggle_field,
+
+ help_text(":hide-fields")
+ .with_summary(
+ "Hide log message fields by replacing them with an ellipsis")
+ .with_parameter(
+ help_text("field-name",
+ "The name of the field to hide in the format for the "
+ "top log line. "
+ "A qualified name can be used where the field name is "
+ "prefixed "
+ "by the format name and a dot to hide any field.")
+ .one_or_more())
+ .with_example(
+ {"To hide the log_procname fields in all formats", "log_procname"})
+ .with_example(
+ {"To hide only the log_procname field in the syslog format",
+ "syslog_log.log_procname"})
+ .with_tags({"display"})},
+ {"show-fields",
+ com_toggle_field,
+
+ help_text(":show-fields")
+ .with_summary("Show log message fields that were previously hidden")
+ .with_parameter(
+ help_text("field-name", "The name of the field to show")
+ .one_or_more())
+ .with_example({"To show all the log_procname fields in all formats",
+ "log_procname"})
+ .with_opposites({"hide-fields"})
+ .with_tags({"display"})},
+ {"hide-lines-before",
+ com_hide_line,
+
+ help_text(":hide-lines-before")
+ .with_summary("Hide lines that come before the given date")
+ .with_parameter(help_text("date", "An absolute or relative date"))
+ .with_examples({
+ {"To hide the lines before the top line in the view", "here"},
+ {"To hide the log messages before 6 AM today", "6am"},
+ })
+ .with_tags({"filtering"})},
+ {"hide-lines-after",
+ com_hide_line,
+
+ help_text(":hide-lines-after")
+ .with_summary("Hide lines that come after the given date")
+ .with_parameter(help_text("date", "An absolute or relative date"))
+ .with_examples({
+ {"To hide the lines after the top line in the view", "here"},
+ {"To hide the lines after 6 AM today", "6am"},
+ })
+ .with_tags({"filtering"})},
+ {"show-lines-before-and-after",
+ com_show_lines,
+
+ help_text(":show-lines-before-and-after")
+ .with_summary(
+ "Show lines that were hidden by the 'hide-lines' commands")
+ .with_opposites({"hide-lines-before", "hide-lines-after"})
+ .with_tags({"filtering"})},
+ {"hide-unmarked-lines",
+ com_hide_unmarked,
+
+ help_text(":hide-unmarked-lines")
+ .with_summary("Hide lines that have not been bookmarked")
+ .with_tags({"filtering", "bookmarks"})},
+ {"show-unmarked-lines",
+ com_show_unmarked,
+
+ help_text(":show-unmarked-lines")
+ .with_summary("Show lines that have not been bookmarked")
+ .with_opposites({"show-unmarked-lines"})
+ .with_tags({"filtering", "bookmarks"})},
+ {"highlight",
+ com_highlight,
+
+ help_text(":highlight")
+ .with_summary("Add coloring to log messages fragments that match the "
+ "given regular expression")
+ .with_parameter(
+ help_text("pattern", "The regular expression to match"))
+ .with_tags({"display"})
+ .with_example(
+ {"To highlight numbers with three or more digits", R"(\d{3,})"})},
+ {"clear-highlight",
+ com_clear_highlight,
+
+ help_text(":clear-highlight")
+ .with_summary("Remove a previously set highlight regular expression")
+ .with_parameter(help_text(
+ "pattern",
+ "The regular expression previously used with :highlight"))
+ .with_tags({"display"})
+ .with_opposites({"highlight"})
+ .with_example(
+ {"To clear the highlight with the pattern 'foobar'", "foobar"})},
+ {"filter-in",
+ com_filter,
+
+ help_text(":filter-in")
+ .with_summary("Only show lines that match the given regular "
+ "expression in the current view")
+ .with_parameter(
+ help_text("pattern", "The regular expression to match"))
+ .with_tags({"filtering"})
+ .with_example({"To filter out log messages that do not have the "
+ "string 'dhclient'",
+ "dhclient"})},
+ {"filter-out",
+ com_filter,
+
+ help_text(":filter-out")
+ .with_summary("Remove lines that match the given regular expression "
+ "in the current view")
+ .with_parameter(
+ help_text("pattern", "The regular expression to match"))
+ .with_tags({"filtering"})
+ .with_example({"To filter out log messages that contain the string "
+ "'last message repeated'",
+ "last message repeated"})},
+ {"delete-filter",
+ com_delete_filter,
+
+ help_text(":delete-filter")
+ .with_summary("Delete the filter created with " ANSI_BOLD(
+ ":filter-in") " or " ANSI_BOLD(":filter-out"))
+ .with_parameter(
+ help_text("pattern", "The regular expression to match"))
+ .with_opposites({"filter-in", "filter-out"})
+ .with_tags({"filtering"})
+ .with_example(
+ {"To delete the filter with the pattern 'last message repeated'",
+ "last message repeated"})},
+ {
+ "filter-expr",
+ com_filter_expr,
+
+ help_text(":filter-expr")
+ .with_summary("Set the filter expression")
+ .with_parameter(help_text(
+ "expr",
+ "The SQL expression to evaluate for each log message. "
+ "The message values can be accessed using column names "
+ "prefixed with a colon"))
+ .with_opposites({"clear-filter-expr"})
+ .with_tags({"filtering"})
+ .with_example({"To set a filter expression that matched syslog "
+ "messages from 'syslogd'",
+ ":log_procname = 'syslogd'"})
+ .with_example(
+ {"To set a filter expression that matches log messages where "
+ "'id' is followed by a number and contains the string 'foo'",
+ ":log_body REGEXP 'id\\d+' AND :log_body REGEXP 'foo'"}),
+
+ com_filter_expr_prompt,
+ },
+ {"clear-filter-expr",
+ com_clear_filter_expr,
+
+ help_text(":clear-filter-expr")
+ .with_summary("Clear the filter expression")
+ .with_opposites({"filter-expr"})
+ .with_tags({"filtering"})},
+ {"append-to",
+ com_save_to,
+
+ help_text(":append-to")
+ .with_summary(
+ "Append marked lines in the current view to the given file")
+ .with_parameter(help_text("path", "The path to the file to append to"))
+ .with_tags({"io"})
+ .with_example(
+ {"To append marked lines to the file /tmp/interesting-lines.txt",
+ "/tmp/interesting-lines.txt"})},
+ {"write-to",
+ com_save_to,
+
+ help_text(":write-to")
+ .with_summary("Overwrite the given file with any marked lines in the "
+ "current view")
+ .with_parameter(
+ help_text("--anonymize", "Anonymize the lines").optional())
+ .with_parameter(help_text("path", "The path to the file to write"))
+ .with_tags({"io", "scripting"})
+ .with_example(
+ {"To write marked lines to the file /tmp/interesting-lines.txt",
+ "/tmp/interesting-lines.txt"})},
+ {"write-csv-to",
+ com_save_to,
+
+ help_text(":write-csv-to")
+ .with_summary("Write SQL results to the given file in CSV format")
+ .with_parameter(
+ help_text("--anonymize", "Anonymize the row contents").optional())
+ .with_parameter(help_text("path", "The path to the file to write"))
+ .with_tags({"io", "scripting", "sql"})
+ .with_example({"To write SQL results as CSV to /tmp/table.csv",
+ "/tmp/table.csv"})},
+ {"write-json-to",
+ com_save_to,
+
+ help_text(":write-json-to")
+ .with_summary("Write SQL results to the given file in JSON format")
+ .with_parameter(
+ help_text("--anonymize", "Anonymize the JSON values").optional())
+ .with_parameter(help_text("path", "The path to the file to write"))
+ .with_tags({"io", "scripting", "sql"})
+ .with_example({"To write SQL results as JSON to /tmp/table.json",
+ "/tmp/table.json"})},
+ {"write-jsonlines-to",
+ com_save_to,
+
+ help_text(":write-jsonlines-to")
+ .with_summary(
+ "Write SQL results to the given file in JSON Lines format")
+ .with_parameter(
+ help_text("--anonymize", "Anonymize the JSON values").optional())
+ .with_parameter(help_text("path", "The path to the file to write"))
+ .with_tags({"io", "scripting", "sql"})
+ .with_example({"To write SQL results as JSON Lines to /tmp/table.json",
+ "/tmp/table.json"})},
+ {"write-table-to",
+ com_save_to,
+
+ help_text(":write-table-to")
+ .with_summary(
+ "Write SQL results to the given file in a tabular format")
+ .with_parameter(
+ help_text("--anonymize", "Anonymize the table contents")
+ .optional())
+ .with_parameter(help_text("path", "The path to the file to write"))
+ .with_tags({"io", "scripting", "sql"})
+ .with_example({"To write SQL results as text to /tmp/table.txt",
+ "/tmp/table.txt"})},
+ {"write-raw-to",
+ com_save_to,
+
+ help_text(":write-raw-to")
+ .with_summary(
+ "In the log view, write the original log file content "
+ "of the marked messages to the file. In the DB view, "
+ "the contents of the cells are written to the output file.")
+ .with_parameter(help_text("--view={log,db}",
+ "The view to use as the source of data")
+ .optional())
+ .with_parameter(
+ help_text("--anonymize", "Anonymize the lines").optional())
+ .with_parameter(help_text("path", "The path to the file to write"))
+ .with_tags({"io", "scripting", "sql"})
+ .with_example(
+ {"To write the marked lines in the log view to /tmp/table.txt",
+ "/tmp/table.txt"})},
+ {"write-view-to",
+ com_save_to,
+
+ help_text(":write-view-to")
+ .with_summary("Write the text in the top view to the given file "
+ "without any formatting")
+ .with_parameter(
+ help_text("--anonymize", "Anonymize the lines").optional())
+ .with_parameter(help_text("path", "The path to the file to write"))
+ .with_tags({"io", "scripting", "sql"})
+ .with_example(
+ {"To write the top view to /tmp/table.txt", "/tmp/table.txt"})},
+ {"write-screen-to",
+ com_save_to,
+
+ help_text(":write-screen-to")
+ .with_summary("Write the displayed text or SQL results to the given "
+ "file without any formatting")
+ .with_parameter(
+ help_text("--anonymize", "Anonymize the lines").optional())
+ .with_parameter(help_text("path", "The path to the file to write"))
+ .with_tags({"io", "scripting", "sql"})
+ .with_example({"To write only the displayed text to /tmp/table.txt",
+ "/tmp/table.txt"})},
+ {"pipe-to",
+ com_pipe_to,
+
+ help_text(":pipe-to")
+ .with_summary("Pipe the marked lines to the given shell command")
+ .with_parameter(
+ help_text("shell-cmd", "The shell command-line to execute"))
+ .with_tags({"io"})
+ .with_example({"To write marked lines to 'sed' for processing",
+ "sed -e s/foo/bar/g"})},
+ {"pipe-line-to",
+ com_pipe_to,
+
+ help_text(":pipe-line-to")
+ .with_summary("Pipe the top line to the given shell command")
+ .with_parameter(
+ help_text("shell-cmd", "The shell command-line to execute"))
+ .with_tags({"io"})
+ .with_example({"To write the top line to 'sed' for processing",
+ "sed -e 's/foo/bar/g'"})},
+ {"redirect-to",
+ com_redirect_to,
+
+ help_text(":redirect-to")
+ .with_summary("Redirect the output of commands that write to "
+ "stdout to the given file")
+ .with_parameter(
+ help_text(
+ "path",
+ "The path to the file to write."
+ " If not specified, the current redirect will be cleared")
+ .optional())
+ .with_tags({"io", "scripting"})
+ .with_example({"To write the output of lnav commands to the file "
+ "/tmp/script-output.txt",
+ "/tmp/script-output.txt"})},
+ {"enable-filter",
+ com_enable_filter,
+
+ help_text(":enable-filter")
+ .with_summary("Enable a previously created and disabled filter")
+ .with_parameter(help_text(
+ "pattern", "The regular expression used in the filter command"))
+ .with_tags({"filtering"})
+ .with_opposites({"disable-filter"})
+ .with_example({"To enable the disabled filter with the pattern 'last "
+ "message repeated'",
+ "last message repeated"})},
+ {"disable-filter",
+ com_disable_filter,
+
+ help_text(":disable-filter")
+ .with_summary("Disable a filter created with filter-in/filter-out")
+ .with_parameter(help_text(
+ "pattern", "The regular expression used in the filter command"))
+ .with_tags({"filtering"})
+ .with_opposites({"filter-out", "filter-in"})
+ .with_example(
+ {"To disable the filter with the pattern 'last message repeated'",
+ "last message repeated"})},
+ {"enable-word-wrap",
+ com_enable_word_wrap,
+
+ help_text(":enable-word-wrap")
+ .with_summary("Enable word-wrapping for the current view")
+ .with_tags({"display"})},
+ {"disable-word-wrap",
+ com_disable_word_wrap,
+
+ help_text(":disable-word-wrap")
+ .with_summary("Disable word-wrapping for the current view")
+ .with_opposites({"enable-word-wrap"})
+ .with_tags({"display"})},
+ {"create-logline-table",
+ com_create_logline_table,
+
+ help_text(":create-logline-table")
+ .with_summary("Create an SQL table using the top line of the log view "
+ "as a template")
+ .with_parameter(help_text("table-name", "The name for the new table"))
+ .with_tags({"vtables", "sql"})
+ .with_example(
+ {"To create a logline-style table named 'task_durations'",
+ "task_durations"})},
+ {"delete-logline-table",
+ com_delete_logline_table,
+
+ help_text(":delete-logline-table")
+ .with_summary("Delete a table created with create-logline-table")
+ .with_parameter(
+ help_text("table-name", "The name of the table to delete"))
+ .with_opposites({"delete-logline-table"})
+ .with_tags({"vtables", "sql"})
+ .with_example(
+ {"To delete the logline-style table named 'task_durations'",
+ "task_durations"})},
+ {"create-search-table",
+ com_create_search_table,
+
+ help_text(":create-search-table")
+ .with_summary("Create an SQL table based on a regex search")
+ .with_parameter(
+ help_text("table-name", "The name of the table to create"))
+ .with_parameter(
+ help_text(
+ "pattern",
+ "The regular expression used to capture the table columns. "
+ "If not given, the current search pattern is used.")
+ .optional())
+ .with_tags({"vtables", "sql"})
+ .with_example(
+ {"To create a table named 'task_durations' that matches log "
+ "messages with the pattern 'duration=(?<duration>\\d+)'",
+ R"(task_durations duration=(?<duration>\d+))"})},
+ {"delete-search-table",
+ com_delete_search_table,
+
+ help_text(":delete-search-table")
+ .with_summary("Create an SQL table based on a regex search")
+ .with_parameter(
+ help_text("table-name", "The name of the table to create"))
+ .with_opposites({"create-search-table"})
+ .with_tags({"vtables", "sql"})
+ .with_example({"To delete the search table named 'task_durations'",
+ "task_durations"})},
+ {"open",
+ com_open,
+
+ help_text(":open")
+ .with_summary(
+ "Open the given file(s) in lnav. Opening files on machines "
+ "accessible via SSH can be done using the syntax: "
+ "[user@]host:/path/to/logs")
+ .with_parameter(
+ help_text{"path", "The path to the file to open"}.one_or_more())
+ .with_example({"To open the file '/path/to/file'", "/path/to/file"})
+ .with_example({"To open the remote file '/var/log/syslog.log'",
+ "dean@host1.example.com:/var/log/syslog.log"})},
+ {"hide-file",
+ com_hide_file,
+
+ help_text(":hide-file")
+ .with_summary("Hide the given file(s) and skip indexing until it "
+ "is shown again. If no path is given, the current "
+ "file in the view is hidden")
+ .with_parameter(help_text{
+ "path", "A path or glob pattern that specifies the files to hide"}
+ .zero_or_more())
+ .with_opposites({"show-file"})},
+ {"show-file",
+ com_show_file,
+
+ help_text(":show-file")
+ .with_summary("Show the given file(s) and resume indexing.")
+ .with_parameter(help_text{
+ "path",
+ "The path or glob pattern that specifies the files to show"}
+ .zero_or_more())
+ .with_opposites({"hide-file"})},
+ {"show-only-this-file",
+ com_show_only_this_file,
+
+ help_text(":show-only-this-file")
+ .with_summary("Show only the file for the top line in the view")
+ .with_opposites({"hide-file"})},
+ {"close",
+ com_close,
+
+ help_text(":close")
+ .with_summary("Close the top file in the view")
+ .with_opposites({"open"})},
+ {
+ "comment",
+ com_comment,
+
+ help_text(":comment")
+ .with_summary(
+ "Attach a comment to the top log line. The comment will be "
+ "displayed right below the log message it is associated with. "
+ "The comment can be formatted using markdown and you can add "
+ "new-lines with '\\n'.")
+ .with_parameter(help_text("text", "The comment text"))
+ .with_example({"To add the comment 'This is where it all went "
+ "wrong' to the top line",
+ "This is where it all went wrong"})
+ .with_tags({"metadata"}),
+
+ com_comment_prompt,
+ },
+ {"clear-comment",
+ com_clear_comment,
+
+ help_text(":clear-comment")
+ .with_summary("Clear the comment attached to the top log line")
+ .with_opposites({"comment"})
+ .with_tags({"metadata"})},
+ {
+ "tag",
+ com_tag,
+
+ help_text(":tag")
+ .with_summary("Attach tags to the top log line")
+ .with_parameter(
+ help_text("tag", "The tags to attach").one_or_more())
+ .with_example({"To add the tags '#BUG123' and '#needs-review' to "
+ "the top line",
+ "#BUG123 #needs-review"})
+ .with_tags({"metadata"}),
+ },
+ {"untag",
+ com_untag,
+
+ help_text(":untag")
+ .with_summary("Detach tags from the top log line")
+ .with_parameter(help_text("tag", "The tags to detach").one_or_more())
+ .with_example({"To remove the tags '#BUG123' and '#needs-review' from "
+ "the top line",
+ "#BUG123 #needs-review"})
+ .with_opposites({"tag"})
+ .with_tags({"metadata"})},
+ {"delete-tags",
+ com_delete_tags,
+
+ help_text(":delete-tags")
+ .with_summary("Remove the given tags from all log lines")
+ .with_parameter(help_text("tag", "The tags to delete").one_or_more())
+ .with_example({"To remove the tags '#BUG123' and '#needs-review' from "
+ "all log lines",
+ "#BUG123 #needs-review"})
+ .with_opposites({"tag"})
+ .with_tags({"metadata"})},
+ {"partition-name",
+ com_partition_name,
+
+ help_text(":partition-name")
+ .with_summary("Mark the top line in the log view as the start of a "
+ "new partition with the given name")
+ .with_parameter(help_text("name", "The name for the new partition"))
+ .with_example({"To mark the top line as the start of the partition "
+ "named 'boot #1'",
+ "boot #1"})},
+ {"clear-partition",
+ com_clear_partition,
+
+ help_text(":clear-partition")
+ .with_summary("Clear the partition the top line is a part of")
+ .with_opposites({"partition-name"})},
+ {"session",
+ com_session,
+
+ help_text(":session")
+ .with_summary(
+ "Add the given command to the session file (~/.lnav/session)")
+ .with_parameter(help_text("lnav-command", "The lnav command to save."))
+ .with_example(
+ {"To add the command ':highlight foobar' to the session file",
+ ":highlight foobar"})},
+ {"summarize",
+ com_summarize,
+
+ help_text(":summarize")
+ .with_summary("Execute a SQL query that computes the characteristics "
+ "of the values in the given column")
+ .with_parameter(
+ help_text("column-name", "The name of the column to analyze."))
+ .with_example(
+ {"To get a summary of the sc_bytes column in the access_log table",
+ "sc_bytes"})},
+ {"switch-to-view",
+ com_switch_to_view,
+
+ help_text(":switch-to-view")
+ .with_summary("Switch to the given view")
+ .with_parameter(
+ help_text("view-name", "The name of the view to switch to."))
+ .with_example({"To switch to the 'schema' view", "schema"})},
+ {"toggle-view",
+ com_switch_to_view,
+
+ help_text(":toggle-view")
+ .with_summary(
+ "Switch to the given view or, if it is already displayed, "
+ "switch to the previous view")
+ .with_parameter(help_text(
+ "view-name", "The name of the view to toggle the display of."))
+ .with_example({"To switch to the 'schema' view if it is not displayed "
+ "or switch back to the previous view",
+ "schema"})},
+ {"toggle-filtering",
+ com_toggle_filtering,
+
+ help_text(":toggle-filtering")
+ .with_summary("Toggle the filtering flag for the current view")
+ .with_tags({"filtering"})},
+ {"reset-session",
+ com_reset_session,
+
+ help_text(":reset-session")
+ .with_summary("Reset the session state, clearing all filters, "
+ "highlights, and bookmarks")},
+ {"load-session",
+ com_load_session,
+
+ help_text(":load-session").with_summary("Load the latest session state")},
+ {"save-session",
+ com_save_session,
+
+ help_text(":save-session")
+ .with_summary("Save the current state as a session")},
+ {"export-session-to",
+ com_export_session_to,
+
+ help_text(":export-session-to")
+ .with_summary("Export the current lnav state to an executable lnav "
+ "script file that contains the commands needed to "
+ "restore the current session")
+ .with_parameter(help_text("path", "The path to the file to write"))
+ .with_tags({"io", "scripting"})},
+ {"rebuild",
+ com_rebuild,
+ help_text(":rebuild")
+ .with_summary("Forcefully rebuild file indexes")
+ .with_tags({"scripting"})},
+ {"set-min-log-level",
+ com_set_min_log_level,
+
+ help_text(":set-min-log-level")
+ .with_summary("Set the minimum log level to display in the log view")
+ .with_parameter(help_text("log-level", "The new minimum log level"))
+ .with_example(
+ {"To set the minimum log level displayed to error", "error"})},
+ {"redraw",
+ com_redraw,
+
+ help_text(":redraw").with_summary("Do a full redraw of the screen")},
+ {"zoom-to",
+ com_zoom_to,
+
+ help_text(":zoom-to")
+ .with_summary("Zoom the histogram view to the given level")
+ .with_parameter(help_text("zoom-level", "The zoom level"))
+ .with_example({"To set the zoom level to '1-week'", "1-week"})},
+ {"echo",
+ com_echo,
+
+ help_text(":echo")
+ .with_summary(
+ "Echo the given message to the screen or, if :redirect-to has "
+ "been called, to output file specified in the redirect. "
+ "Variable substitution is performed on the message. Use a "
+ "backslash to escape any special characters, like '$'")
+ .with_parameter(
+ help_text("-n",
+ "Do not print a line-feed at the end of the output")
+ .optional())
+ .with_parameter(help_text("msg", "The message to display"))
+ .with_tags({"io", "scripting"})
+ .with_example({"To output 'Hello, World!'", "Hello, World!"})},
+ {"alt-msg",
+ com_alt_msg,
+
+ help_text(":alt-msg")
+ .with_summary("Display a message in the alternate command position")
+ .with_parameter(help_text("msg", "The message to display"))
+ .with_tags({"scripting"})
+ .with_example({"To display 'Press t to switch to the text view' on "
+ "the bottom right",
+ "Press t to switch to the text view"})},
+ {"eval",
+ com_eval,
+
+ help_text(":eval")
+ .with_summary("Evaluate the given command/query after doing "
+ "environment variable substitution")
+ .with_parameter(help_text(
+ "command", "The command or query to perform substitution on."))
+ .with_tags({"scripting"})
+ .with_examples({{"To substitute the table name from a variable",
+ ";SELECT * FROM ${table}"}})},
+ {"config",
+ com_config,
+
+ help_text(":config")
+ .with_summary("Read or write a configuration option")
+ .with_parameter({"option", "The path to the option to read or write"})
+ .with_parameter(help_text("value",
+ "The value to write. If not given, the "
+ "current value is returned")
+ .optional())
+ .with_example(
+ {"To read the configuration of the '/ui/clock-format' option",
+ "/ui/clock-format"})
+ .with_example({"To set the '/ui/dim-text' option to 'false'",
+ "/ui/dim-text false"})
+ .with_tags({"configuration"})},
+ {"reset-config",
+ com_reset_config,
+
+ help_text(":reset-config")
+ .with_summary("Reset the configuration option to its default value")
+ .with_parameter(help_text("option", "The path to the option to reset"))
+ .with_example({"To reset the '/ui/clock-format' option back to the "
+ "builtin default",
+ "/ui/clock-format"})
+ .with_tags({"configuration"})},
+ {"spectrogram",
+ com_spectrogram,
+
+ help_text(":spectrogram")
+ .with_summary("Visualize the given message field or database column "
+ "using a spectrogram")
+ .with_parameter(help_text(
+ "field-name", "The name of the numeric field to visualize."))
+ .with_example(
+ {"To visualize the sc_bytes field in the access_log format",
+ "sc_bytes"})},
+ {"quit",
+ com_quit,
+
+ help_text(":quit").with_summary("Quit lnav")}};
+
+static std::unordered_map<char const*, std::vector<char const*>> aliases = {
+ {"quit", {"q", "q!"}},
+ {"write-table-to", {"write-cols-to"}},
+};
+
+void
+init_lnav_commands(readline_context::command_map_t& cmd_map)
+{
+ for (auto& cmd : STD_COMMANDS) {
+ cmd.c_help.index_tags();
+ cmd_map[cmd.c_name] = &cmd;
+
+ auto itr = aliases.find(cmd.c_name);
+ if (itr != aliases.end()) {
+ for (char const* alias : itr->second) {
+ cmd_map[alias] = &cmd;
+ }
+ }
+ }
+
+ if (getenv("LNAV_SRC") != nullptr) {
+ static readline_context::command_t add_test(com_add_test);
+
+ cmd_map["add-test"] = &add_test;
+ }
+ if (getenv("lnav_test") != nullptr) {
+ static readline_context::command_t shexec(com_shexec),
+ poll_now(com_poll_now), test_comment(com_test_comment);
+
+ cmd_map["shexec"] = &shexec;
+ cmd_map["poll-now"] = &poll_now;
+ cmd_map["test-comment"] = &test_comment;
+ }
+}
diff --git a/src/lnav_commands.hh b/src/lnav_commands.hh
new file mode 100644
index 0000000..4e6ebdb
--- /dev/null
+++ b/src/lnav_commands.hh
@@ -0,0 +1,42 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav_commands.hh
+ */
+
+#ifndef lnav_commands_hh
+#define lnav_commands_hh
+
+#include "readline_curses.hh"
+
+/**
+ * Initialize the given map with the builtin lnav commands.
+ */
+void init_lnav_commands(readline_context::command_map_t& cmd_map);
+
+#endif
diff --git a/src/lnav_config.cc b/src/lnav_config.cc
new file mode 100644
index 0000000..e706f75
--- /dev/null
+++ b/src/lnav_config.cc
@@ -0,0 +1,1670 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav_config.cc
+ */
+
+#include <chrono>
+#include <iostream>
+#include <regex>
+#include <stdexcept>
+
+#include "lnav_config.hh"
+
+#include <fcntl.h>
+#include <fmt/format.h>
+#include <glob.h>
+#include <libgen.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "base/auto_fd.hh"
+#include "base/auto_mem.hh"
+#include "base/auto_pid.hh"
+#include "base/fs_util.hh"
+#include "base/injector.bind.hh"
+#include "base/injector.hh"
+#include "base/lnav_log.hh"
+#include "base/paths.hh"
+#include "base/string_util.hh"
+#include "bin2c.hh"
+#include "config.h"
+#include "default-config.h"
+#include "styling.hh"
+#include "view_curses.hh"
+#include "yajlpp/yajlpp.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+using namespace std::chrono_literals;
+
+static const int MAX_CRASH_LOG_COUNT = 16;
+static const auto STDIN_CAPTURE_RETENTION = 24h;
+
+static auto intern_lifetime = intern_string::get_table_lifetime();
+
+struct _lnav_config lnav_config;
+struct _lnav_config rollback_lnav_config;
+static struct _lnav_config lnav_default_config;
+
+std::map<intern_string_t, source_location> lnav_config_locations;
+
+lnav_config_listener* lnav_config_listener::LISTENER_LIST;
+
+static auto a = injector::bind<archive_manager::config>::to_instance(
+ +[]() { return &lnav_config.lc_archive_manager; });
+
+static auto fvc = injector::bind<file_vtab::config>::to_instance(
+ +[]() { return &lnav_config.lc_file_vtab; });
+
+static auto lc = injector::bind<lnav::logfile::config>::to_instance(
+ +[]() { return &lnav_config.lc_logfile; });
+
+static auto tc = injector::bind<tailer::config>::to_instance(
+ +[]() { return &lnav_config.lc_tailer; });
+
+static auto scc = injector::bind<sysclip::config>::to_instance(
+ +[]() { return &lnav_config.lc_sysclip; });
+
+static auto lsc = injector::bind<logfile_sub_source_ns::config>::to_instance(
+ +[]() { return &lnav_config.lc_log_source; });
+
+static auto tssc = injector::bind<top_status_source_cfg>::to_instance(
+ +[]() { return &lnav_config.lc_top_status_cfg; });
+
+bool
+check_experimental(const char* feature_name)
+{
+ const char* env_value = getenv("LNAV_EXP");
+
+ require(feature_name != nullptr);
+
+ if (env_value && strcasestr(env_value, feature_name)) {
+ return true;
+ }
+
+ return false;
+}
+
+void
+ensure_dotlnav()
+{
+ static const char* subdirs[] = {
+ "",
+ "configs",
+ "configs/default",
+ "configs/installed",
+ "formats",
+ "formats/default",
+ "formats/installed",
+ "staging",
+ "stdin-captures",
+ "crash",
+ };
+
+ auto path = lnav::paths::dotlnav();
+
+ for (const auto* sub_path : subdirs) {
+ auto full_path = path / sub_path;
+
+ log_perror(mkdir(full_path.c_str(), 0755));
+ }
+
+ auto crash_dir_path = path / "crash";
+ lnav_log_crash_dir = strdup(crash_dir_path.c_str());
+
+ {
+ static_root_mem<glob_t, globfree> gl;
+ auto crash_glob = path / "crash-*";
+
+ if (glob(crash_glob.c_str(), GLOB_NOCHECK, nullptr, gl.inout()) == 0) {
+ std::error_code ec;
+ for (size_t lpc = 0; lpc < gl->gl_pathc; lpc++) {
+ auto crash_file = ghc::filesystem::path(gl->gl_pathv[lpc]);
+
+ ghc::filesystem::rename(
+ crash_file, crash_dir_path / crash_file.filename(), ec);
+ }
+ }
+ }
+
+ {
+ static_root_mem<glob_t, globfree> gl;
+ auto crash_glob = path / "crash/*";
+
+ if (glob(crash_glob.c_str(), GLOB_NOCHECK, nullptr, gl.inout()) == 0) {
+ for (int lpc = 0; lpc < ((int) gl->gl_pathc - MAX_CRASH_LOG_COUNT);
+ lpc++)
+ {
+ log_perror(remove(gl->gl_pathv[lpc]));
+ }
+ }
+ }
+
+ {
+ static_root_mem<glob_t, globfree> gl;
+ auto cap_glob = path / "stdin-captures/*";
+
+ if (glob(cap_glob.c_str(), GLOB_NOCHECK, nullptr, gl.inout()) == 0) {
+ auto old_time
+ = std::chrono::system_clock::now() - STDIN_CAPTURE_RETENTION;
+
+ for (size_t lpc = 0; lpc < gl->gl_pathc; lpc++) {
+ struct stat st;
+
+ if (stat(gl->gl_pathv[lpc], &st) == -1) {
+ continue;
+ }
+
+ if (std::chrono::system_clock::from_time_t(st.st_mtime)
+ > old_time)
+ {
+ continue;
+ }
+
+ log_info("Removing old stdin capture: %s", gl->gl_pathv[lpc]);
+ log_perror(remove(gl->gl_pathv[lpc]));
+ }
+ }
+ }
+}
+
+bool
+install_from_git(const std::string& repo)
+{
+ static const std::regex repo_name_converter("[^\\w]");
+
+ auto formats_path = lnav::paths::dotlnav() / "formats";
+ auto configs_path = lnav::paths::dotlnav() / "configs";
+ auto staging_path = lnav::paths::dotlnav() / "staging";
+ auto local_name = std::regex_replace(repo, repo_name_converter, "_");
+
+ auto local_formats_path = formats_path / local_name;
+ auto local_configs_path = configs_path / local_name;
+ auto local_staging_path = staging_path / local_name;
+
+ auto fork_res = lnav::pid::from_fork();
+ if (fork_res.isErr()) {
+ fprintf(stderr,
+ "error: cannot fork() to run git: %s\n",
+ fork_res.unwrapErr().c_str());
+ _exit(1);
+ }
+
+ auto git_cmd = fork_res.unwrap();
+ if (git_cmd.in_child()) {
+ if (ghc::filesystem::is_directory(local_formats_path)) {
+ fmt::print("Updating format repo: {}\n", repo);
+ log_perror(chdir(local_formats_path.c_str()));
+ execlp("git", "git", "pull", nullptr);
+ } else if (ghc::filesystem::is_directory(local_configs_path)) {
+ fmt::print("Updating config repo: {}\n", repo);
+ log_perror(chdir(local_configs_path.c_str()));
+ execlp("git", "git", "pull", nullptr);
+ } else {
+ execlp("git",
+ "git",
+ "clone",
+ repo.c_str(),
+ local_staging_path.c_str(),
+ nullptr);
+ }
+ _exit(1);
+ }
+
+ auto finished_child = std::move(git_cmd).wait_for_child();
+
+ if (!finished_child.was_normal_exit() || finished_child.exit_status() != 0)
+ {
+ return false;
+ }
+
+ if (!ghc::filesystem::is_directory(local_staging_path)) {
+ auto um
+ = lnav::console::user_message::error(
+ attr_line_t("failed to install git repo: ")
+ .append(lnav::roles::file(repo)))
+ .with_reason(
+ attr_line_t("git failed to create the local directory")
+ .append(
+ lnav::roles::file(local_staging_path.string())));
+ lnav::console::print(stderr, um);
+ return false;
+ }
+
+ auto config_path = local_staging_path / "*";
+ static_root_mem<glob_t, globfree> gl;
+ int found_config_file = 0;
+ int found_format_file = 0;
+ int found_sql_file = 0;
+ int found_lnav_file = 0;
+
+ if (glob(config_path.c_str(), 0, nullptr, gl.inout()) == 0) {
+ for (size_t lpc = 0; lpc < gl->gl_pathc; lpc++) {
+ auto file_path = ghc::filesystem::path{gl->gl_pathv[lpc]};
+
+ if (file_path.extension() == ".lnav") {
+ found_lnav_file += 1;
+ continue;
+ }
+ if (file_path.extension() == ".sql") {
+ found_sql_file += 1;
+ continue;
+ }
+ if (file_path.extension() != ".json") {
+ found_sql_file += 1;
+ continue;
+ }
+
+ auto file_type_result = detect_config_file_type(file_path);
+
+ if (file_type_result.isErr()) {
+ fprintf(stderr,
+ "error: %s\n",
+ file_type_result.unwrapErr().c_str());
+ return false;
+ }
+ if (file_type_result.unwrap() == config_file_type::CONFIG) {
+ found_config_file += 1;
+ } else {
+ found_format_file += 1;
+ }
+ }
+ }
+
+ if (found_config_file == 0 && found_format_file == 0 && found_sql_file == 0
+ && found_lnav_file == 0)
+ {
+ auto um = lnav::console::user_message::error(
+ attr_line_t("invalid lnav repo: ")
+ .append(lnav::roles::file(repo)))
+ .with_reason("no .json, .sql, or .lnav files were found");
+ lnav::console::print(stderr, um);
+ return false;
+ }
+
+ auto dest_path = local_formats_path;
+ attr_line_t notes;
+ if (found_format_file > 0) {
+ notes.append("found ")
+ .append(lnav::roles::number(fmt::to_string(found_format_file)))
+ .append(" format file(s)\n");
+ }
+ if (found_config_file > 0) {
+ if (found_format_file == 0) {
+ dest_path = local_configs_path;
+ }
+ notes.append("found ")
+ .append(lnav::roles::number(fmt::to_string(found_config_file)))
+ .append(" configuration file(s)\n");
+ }
+ if (found_sql_file > 0) {
+ notes.append("found ")
+ .append(lnav::roles::number(fmt::to_string(found_sql_file)))
+ .append(" SQL file(s)\n");
+ }
+ if (found_lnav_file > 0) {
+ notes.append("found ")
+ .append(lnav::roles::number(fmt::to_string(found_lnav_file)))
+ .append(" lnav-script file(s)\n");
+ }
+ rename(local_staging_path.c_str(), dest_path.c_str());
+ auto um = lnav::console::user_message::ok(
+ attr_line_t("installed lnav repo at: ")
+ .append(lnav::roles::file(local_configs_path.string())))
+ .with_note(notes);
+ lnav::console::print(stdout, um);
+
+ return true;
+}
+
+bool
+update_installs_from_git()
+{
+ static_root_mem<glob_t, globfree> gl;
+ auto git_formats = lnav::paths::dotlnav() / "formats/*/.git";
+ bool found = false, retval = true;
+
+ if (glob(git_formats.c_str(), 0, nullptr, gl.inout()) == 0) {
+ for (int lpc = 0; lpc < (int) gl->gl_pathc; lpc++) {
+ auto git_dir
+ = ghc::filesystem::path(gl->gl_pathv[lpc]).parent_path();
+
+ printf("Updating formats in %s\n", git_dir.c_str());
+ auto pull_cmd = fmt::format(FMT_STRING("cd '{}' && git pull"),
+ git_dir.string());
+ int ret = system(pull_cmd.c_str());
+ if (ret == -1) {
+ std::cerr << "Failed to spawn command "
+ << "\"" << pull_cmd << "\": " << strerror(errno)
+ << std::endl;
+ retval = false;
+ } else if (ret > 0) {
+ std::cerr << "Command "
+ << "\"" << pull_cmd
+ << "\" failed: " << strerror(errno) << std::endl;
+ retval = false;
+ }
+ found = true;
+ }
+ }
+
+ if (!found) {
+ printf(
+ "No formats from git repositories found, "
+ "use 'lnav -i extra' to install third-party foramts\n");
+ }
+
+ return retval;
+}
+
+static int
+read_repo_path(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
+{
+ auto path = std::string((const char*) str, len);
+
+ install_from_git(path.c_str());
+
+ return 1;
+}
+
+static const struct json_path_container format_handlers = {
+ json_path_handler("format-repos#", read_repo_path),
+};
+
+void
+install_extra_formats()
+{
+ auto config_root = lnav::paths::dotlnav() / "remote-config";
+ auto_fd fd;
+
+ if (access(config_root.c_str(), R_OK) == 0) {
+ printf("Updating lnav remote config repo...\n");
+ auto pull_cmd = fmt::format(FMT_STRING("cd '{}' && git pull"),
+ config_root.string());
+ log_perror(system(pull_cmd.c_str()));
+ } else {
+ printf("Cloning lnav remote config repo...\n");
+ auto clone_cmd = fmt::format(
+ FMT_STRING(
+ "git clone https://github.com/tstack/lnav-config.git {}"),
+ config_root.string());
+ log_perror(system(clone_cmd.c_str()));
+ }
+
+ auto config_json = config_root / "remote-config.json";
+ if ((fd = lnav::filesystem::openp(config_json, O_RDONLY)) == -1) {
+ perror("Unable to open remote-config.json");
+ } else {
+ yajlpp_parse_context ypc_config(
+ intern_string::lookup(config_root.string()), &format_handlers);
+ auto_mem<yajl_handle_t> jhandle(yajl_free);
+ unsigned char buffer[4096];
+ ssize_t rc;
+
+ jhandle = yajl_alloc(&ypc_config.ypc_callbacks, nullptr, &ypc_config);
+ yajl_config(jhandle, yajl_allow_comments, 1);
+ while ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
+ if (yajl_parse(jhandle, buffer, rc) != yajl_status_ok) {
+ auto* msg = yajl_get_error(jhandle, 1, buffer, rc);
+ fprintf(
+ stderr, "Unable to parse remote-config.json -- %s", msg);
+ yajl_free_error(jhandle, msg);
+ return;
+ }
+ }
+ if (yajl_complete_parse(jhandle) != yajl_status_ok) {
+ auto* msg = yajl_get_error(jhandle, 1, buffer, rc);
+
+ fprintf(stderr, "Unable to parse remote-config.json -- %s", msg);
+ yajl_free_error(jhandle, msg);
+ }
+ }
+}
+
+struct config_userdata {
+ explicit config_userdata(std::vector<lnav::console::user_message>& errors)
+ : ud_errors(errors)
+ {
+ }
+
+ std::vector<lnav::console::user_message>& ud_errors;
+};
+
+static void
+config_error_reporter(const yajlpp_parse_context& ypc,
+ const lnav::console::user_message& msg)
+{
+ auto* ud = (config_userdata*) ypc.ypc_userdata;
+
+ ud->ud_errors.emplace_back(msg);
+}
+
+static const struct json_path_container key_command_handlers = {
+ yajlpp::property_handler("command")
+ .with_synopsis("<command>")
+ .with_description(
+ "The command to execute for the given key sequence. Use a script "
+ "to execute more complicated operations.")
+ .with_pattern("^[:|;].*")
+ .with_example(":goto next hour")
+ .for_field(&key_command::kc_cmd),
+ yajlpp::property_handler("alt-msg")
+ .with_synopsis("<msg>")
+ .with_description(
+ "The help message to display after the key is pressed.")
+ .for_field<>(&key_command::kc_alt_msg),
+};
+
+static const struct json_path_container keymap_def_handlers = {
+ yajlpp::pattern_property_handler("(?<key_seq>(?:x[0-9a-f]{2})+)")
+ .with_synopsis("<utf8-key-code-in-hex>")
+ .with_description(
+ "Map of key codes to commands to execute. The field names are "
+ "the keys to be mapped using as a hexadecimal representation of "
+ "the UTF-8 encoding. Each byte of the UTF-8 should start with "
+ "an 'x' followed by the hexadecimal representation of the byte.")
+ .with_obj_provider<key_command, key_map>(
+ [](const yajlpp_provider_context& ypc, key_map* km) {
+ auto& retval = km->km_seq_to_cmd[ypc.get_substr("key_seq")];
+
+ return &retval;
+ })
+ .with_path_provider<key_map>(
+ [](key_map* km, std::vector<std::string>& paths_out) {
+ for (const auto& iter : km->km_seq_to_cmd) {
+ paths_out.emplace_back(iter.first);
+ }
+ })
+ .with_children(key_command_handlers),
+};
+
+static const struct json_path_container keymap_defs_handlers = {
+ yajlpp::pattern_property_handler("(?<keymap_name>[\\w\\-]+)")
+ .with_description("The keymap definitions")
+ .with_obj_provider<key_map, _lnav_config>(
+ [](const yajlpp_provider_context& ypc, _lnav_config* root) {
+ key_map& retval
+ = root->lc_ui_keymaps[ypc.get_substr("keymap_name")];
+ return &retval;
+ })
+ .with_path_provider<_lnav_config>(
+ [](struct _lnav_config* cfg, std::vector<std::string>& paths_out) {
+ for (const auto& iter : cfg->lc_ui_keymaps) {
+ paths_out.emplace_back(iter.first);
+ }
+ })
+ .with_children(keymap_def_handlers),
+};
+
+static const json_path_handler_base::enum_value_t _movement_values[] = {
+ {"top", config_movement_mode::TOP},
+ {"cursor", config_movement_mode::CURSOR},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const struct json_path_container movement_handlers = {
+ yajlpp::property_handler("mode")
+ .with_synopsis("top|cursor")
+ .with_enum_values(_movement_values)
+ .with_example("top")
+ .with_example("cursor")
+ .with_description("The mode of cursor movement to use.")
+ .for_field<>(&_lnav_config::lc_ui_movement, &movement_config::mode),
+};
+
+static const struct json_path_container global_var_handlers = {
+ yajlpp::pattern_property_handler("(?<var_name>\\w+)")
+ .with_synopsis("<name>")
+ .with_description(
+ "A global variable definition. Global variables can be referenced "
+ "in scripts, SQL statements, or commands.")
+ .with_path_provider<_lnav_config>(
+ [](struct _lnav_config* cfg, std::vector<std::string>& paths_out) {
+ for (const auto& iter : cfg->lc_global_vars) {
+ paths_out.emplace_back(iter.first);
+ }
+ })
+ .for_field(&_lnav_config::lc_global_vars),
+};
+
+static const struct json_path_container style_config_handlers =
+ json_path_container{
+ yajlpp::property_handler("color")
+ .with_synopsis("#hex|color_name")
+ .with_description(
+ "The foreground color value for this style. The value can be "
+ "the name of an xterm color, the hexadecimal value, or a theme "
+ "variable reference.")
+ .with_example("#fff")
+ .with_example("Green")
+ .with_example("$black")
+ .for_field(&style_config::sc_color),
+ yajlpp::property_handler("background-color")
+ .with_synopsis("#hex|color_name")
+ .with_description(
+ "The background color value for this style. The value can be "
+ "the name of an xterm color, the hexadecimal value, or a theme "
+ "variable reference.")
+ .with_example("#2d2a2e")
+ .with_example("Green")
+ .for_field(&style_config::sc_background_color),
+ yajlpp::property_handler("underline")
+ .with_description("Indicates that the text should be underlined.")
+ .for_field(&style_config::sc_underline),
+ yajlpp::property_handler("bold")
+ .with_description("Indicates that the text should be bolded.")
+ .for_field(&style_config::sc_bold),
+ }
+ .with_definition_id("style");
+
+static const struct json_path_container theme_styles_handlers = {
+ yajlpp::property_handler("identifier")
+ .with_description("Styling for identifiers in logs")
+ .for_child(&lnav_theme::lt_style_identifier)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("text")
+ .with_description("Styling for plain text")
+ .for_child(&lnav_theme::lt_style_text)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("alt-text")
+ .with_description("Styling for plain text when alternating")
+ .for_child(&lnav_theme::lt_style_alt_text)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("error")
+ .with_description("Styling for error messages")
+ .for_child(&lnav_theme::lt_style_error)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("ok")
+ .with_description("Styling for success messages")
+ .for_child(&lnav_theme::lt_style_ok)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("info")
+ .with_description("Styling for informational messages")
+ .for_child(&lnav_theme::lt_style_info)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("warning")
+ .with_description("Styling for warning messages")
+ .for_child(&lnav_theme::lt_style_warning)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("hidden")
+ .with_description("Styling for hidden fields in logs")
+ .for_child(&lnav_theme::lt_style_hidden)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("cursor-line")
+ .with_description("Styling for the cursor line in the main view")
+ .for_child(&lnav_theme::lt_style_cursor_line)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("adjusted-time")
+ .with_description("Styling for timestamps that have been adjusted")
+ .for_child(&lnav_theme::lt_style_adjusted_time)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("skewed-time")
+ .with_description(
+ "Styling for timestamps that are different from the received time")
+ .for_child(&lnav_theme::lt_style_skewed_time)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("offset-time")
+ .with_description("Styling for hidden fields")
+ .for_child(&lnav_theme::lt_style_offset_time)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("invalid-msg")
+ .with_description("Styling for invalid log messages")
+ .for_child(&lnav_theme::lt_style_invalid_msg)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("popup")
+ .with_description("Styling for popup windows")
+ .for_child(&lnav_theme::lt_style_popup)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("focused")
+ .with_description("Styling for a focused row in a list view")
+ .for_child(&lnav_theme::lt_style_focused)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("disabled-focused")
+ .with_description("Styling for a disabled focused row in a list view")
+ .for_child(&lnav_theme::lt_style_disabled_focused)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("scrollbar")
+ .with_description("Styling for scrollbars")
+ .for_child(&lnav_theme::lt_style_scrollbar)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("h1")
+ .with_description("Styling for top-level headers")
+ .with_obj_provider<style_config, lnav_theme>(
+ [](const yajlpp_provider_context& ypc, lnav_theme* root) {
+ return &root->lt_style_header[0].pp_value;
+ })
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("h2")
+ .with_description("Styling for 2nd-level headers")
+ .with_obj_provider<style_config, lnav_theme>(
+ [](const yajlpp_provider_context& ypc, lnav_theme* root) {
+ return &root->lt_style_header[1].pp_value;
+ })
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("h3")
+ .with_description("Styling for 3rd-level headers")
+ .with_obj_provider<style_config, lnav_theme>(
+ [](const yajlpp_provider_context& ypc, lnav_theme* root) {
+ return &root->lt_style_header[2].pp_value;
+ })
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("h4")
+ .with_description("Styling for 4th-level headers")
+ .with_obj_provider<style_config, lnav_theme>(
+ [](const yajlpp_provider_context& ypc, lnav_theme* root) {
+ return &root->lt_style_header[3].pp_value;
+ })
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("h5")
+ .with_description("Styling for 5th-level headers")
+ .with_obj_provider<style_config, lnav_theme>(
+ [](const yajlpp_provider_context& ypc, lnav_theme* root) {
+ return &root->lt_style_header[4].pp_value;
+ })
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("h6")
+ .with_description("Styling for 6th-level headers")
+ .with_obj_provider<style_config, lnav_theme>(
+ [](const yajlpp_provider_context& ypc, lnav_theme* root) {
+ return &root->lt_style_header[5].pp_value;
+ })
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("hr")
+ .with_description("Styling for horizontal rules")
+ .for_child(&lnav_theme::lt_style_hr)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("hyperlink")
+ .with_description("Styling for hyperlinks")
+ .for_child(&lnav_theme::lt_style_hyperlink)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("list-glyph")
+ .with_description("Styling for glyphs that prefix a list item")
+ .for_child(&lnav_theme::lt_style_list_glyph)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("breadcrumb")
+ .with_description("Styling for the separator between breadcrumbs")
+ .for_child(&lnav_theme::lt_style_breadcrumb)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("table-border")
+ .with_description("Styling for table borders")
+ .for_child(&lnav_theme::lt_style_table_border)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("table-header")
+ .with_description("Styling for table headers")
+ .for_child(&lnav_theme::lt_style_table_header)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("quote-border")
+ .with_description("Styling for quoted-block borders")
+ .for_child(&lnav_theme::lt_style_quote_border)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("quoted-text")
+ .with_description("Styling for quoted text blocks")
+ .for_child(&lnav_theme::lt_style_quoted_text)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("footnote-border")
+ .with_description("Styling for footnote borders")
+ .for_child(&lnav_theme::lt_style_footnote_border)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("footnote-text")
+ .with_description("Styling for footnote text")
+ .for_child(&lnav_theme::lt_style_footnote_text)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("snippet-border")
+ .with_description("Styling for snippet borders")
+ .for_child(&lnav_theme::lt_style_snippet_border)
+ .with_children(style_config_handlers),
+};
+
+static const struct json_path_container theme_syntax_styles_handlers = {
+ yajlpp::property_handler("quoted-code")
+ .with_description("Styling for quoted code blocks")
+ .for_child(&lnav_theme::lt_style_quoted_code)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("code-border")
+ .with_description("Styling for quoted-code borders")
+ .for_child(&lnav_theme::lt_style_code_border)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("keyword")
+ .with_description("Styling for keywords in source files")
+ .for_child(&lnav_theme::lt_style_keyword)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("string")
+ .with_description("Styling for single/double-quoted strings in text")
+ .for_child(&lnav_theme::lt_style_string)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("comment")
+ .with_description("Styling for comments in source files")
+ .for_child(&lnav_theme::lt_style_comment)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("doc-directive")
+ .with_description(
+ "Styling for documentation directives in source files")
+ .for_child(&lnav_theme::lt_style_doc_directive)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("variable")
+ .with_description("Styling for variables in text")
+ .for_child(&lnav_theme::lt_style_variable)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("symbol")
+ .with_description("Styling for symbols in source files")
+ .for_child(&lnav_theme::lt_style_symbol)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("number")
+ .with_description("Styling for numbers in source files")
+ .for_child(&lnav_theme::lt_style_number)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("re-special")
+ .with_description(
+ "Styling for special characters in regular expressions")
+ .for_child(&lnav_theme::lt_style_re_special)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("re-repeat")
+ .with_description("Styling for repeats in regular expressions")
+ .for_child(&lnav_theme::lt_style_re_repeat)
+ .with_children(style_config_handlers),
+
+ yajlpp::property_handler("diff-delete")
+ .with_description("Styling for deleted lines in diffs")
+ .for_child(&lnav_theme::lt_style_diff_delete)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("diff-add")
+ .with_description("Styling for added lines in diffs")
+ .for_child(&lnav_theme::lt_style_diff_add)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("diff-section")
+ .with_description("Styling for diffs")
+ .for_child(&lnav_theme::lt_style_diff_section)
+ .with_children(style_config_handlers),
+
+ yajlpp::property_handler("spectrogram-low")
+ .with_description(
+ "Styling for the lower threshold values in the spectrogram view")
+ .for_child(&lnav_theme::lt_style_low_threshold)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("spectrogram-medium")
+ .with_description(
+ "Styling for the medium threshold values in the spectrogram view")
+ .for_child(&lnav_theme::lt_style_med_threshold)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("spectrogram-high")
+ .with_description(
+ "Styling for the high threshold values in the spectrogram view")
+ .for_child(&lnav_theme::lt_style_high_threshold)
+ .with_children(style_config_handlers),
+
+ yajlpp::property_handler("file")
+ .with_description("Styling for file names in source files")
+ .for_child(&lnav_theme::lt_style_file)
+ .with_children(style_config_handlers),
+};
+
+static const struct json_path_container theme_status_styles_handlers = {
+ yajlpp::property_handler("text")
+ .with_description("Styling for status bars")
+ .for_child(&lnav_theme::lt_style_status)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("warn")
+ .with_description("Styling for warnings in status bars")
+ .for_child(&lnav_theme::lt_style_warn_status)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("alert")
+ .with_description("Styling for alerts in status bars")
+ .for_child(&lnav_theme::lt_style_alert_status)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("active")
+ .with_description("Styling for activity in status bars")
+ .for_child(&lnav_theme::lt_style_active_status)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("inactive-alert")
+ .with_description("Styling for inactive alert status bars")
+ .for_child(&lnav_theme::lt_style_inactive_alert_status)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("inactive")
+ .with_description("Styling for inactive status bars")
+ .for_child(&lnav_theme::lt_style_inactive_status)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("title-hotkey")
+ .with_description("Styling for hotkey highlights in titles")
+ .for_child(&lnav_theme::lt_style_status_title_hotkey)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("title")
+ .with_description("Styling for title sections of status bars")
+ .for_child(&lnav_theme::lt_style_status_title)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("disabled-title")
+ .with_description("Styling for title sections of status bars")
+ .for_child(&lnav_theme::lt_style_status_disabled_title)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("subtitle")
+ .with_description("Styling for subtitle sections of status bars")
+ .for_child(&lnav_theme::lt_style_status_subtitle)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("info")
+ .with_description("Styling for informational messages in status bars")
+ .for_child(&lnav_theme::lt_style_status_info)
+ .with_children(style_config_handlers),
+ yajlpp::property_handler("hotkey")
+ .with_description("Styling for hotkey highlights of status bars")
+ .for_child(&lnav_theme::lt_style_status_hotkey)
+ .with_children(style_config_handlers),
+};
+
+static const struct json_path_container theme_log_level_styles_handlers = {
+ yajlpp::pattern_property_handler(
+ "(?<level>trace|debug5|debug4|debug3|debug2|debug|info|stats|notice|"
+ "warning|error|critical|fatal|invalid)")
+ .with_obj_provider<style_config, lnav_theme>(
+ [](const yajlpp_provider_context& ypc, lnav_theme* root) {
+ auto& sc = root->lt_level_styles[string2level(
+ ypc.get_substr_i("level").get())];
+
+ if (ypc.ypc_parse_context != nullptr && sc.pp_path.empty()) {
+ sc.pp_path = ypc.ypc_parse_context->get_full_path();
+ }
+
+ return &sc.pp_value;
+ })
+ .with_path_provider<lnav_theme>(
+ [](struct lnav_theme* cfg, std::vector<std::string>& paths_out) {
+ for (int lpc = LEVEL_TRACE; lpc < LEVEL__MAX; lpc++) {
+ paths_out.emplace_back(level_names[lpc]);
+ }
+ })
+ .with_children(style_config_handlers),
+};
+
+static const struct json_path_container highlighter_handlers = {
+ yajlpp::property_handler("pattern")
+ .with_synopsis("regular expression")
+ .with_description("The regular expression to highlight")
+ .for_field(&highlighter_config::hc_regex),
+
+ yajlpp::property_handler("style")
+ .with_description(
+ "The styling for the text that matches the associated pattern")
+ .for_child(&highlighter_config::hc_style)
+ .with_children(style_config_handlers),
+};
+
+static const struct json_path_container theme_highlights_handlers = {
+ yajlpp::pattern_property_handler("(?<highlight_name>[\\w\\-]+)")
+ .with_obj_provider<highlighter_config,
+ lnav_theme>([](const yajlpp_provider_context& ypc,
+ lnav_theme* root) {
+ highlighter_config& hc
+ = root->lt_highlights[ypc.get_substr_i("highlight_name").get()];
+
+ return &hc;
+ })
+ .with_path_provider<lnav_theme>(
+ [](struct lnav_theme* cfg, std::vector<std::string>& paths_out) {
+ for (const auto& pair : cfg->lt_highlights) {
+ paths_out.emplace_back(pair.first);
+ }
+ })
+ .with_children(highlighter_handlers),
+};
+
+static const struct json_path_container theme_vars_handlers = {
+ yajlpp::pattern_property_handler("(?<var_name>\\w+)")
+ .with_synopsis("name")
+ .with_description("A theme variable definition")
+ .with_path_provider<lnav_theme>(
+ [](struct lnav_theme* lt, std::vector<std::string>& paths_out) {
+ for (const auto& iter : lt->lt_vars) {
+ paths_out.emplace_back(iter.first);
+ }
+ })
+ .for_field(&lnav_theme::lt_vars),
+};
+
+static const struct json_path_container theme_def_handlers = {
+ yajlpp::property_handler("vars")
+ .with_description("Variables definitions that are used in this theme.")
+ .with_children(theme_vars_handlers),
+
+ yajlpp::property_handler("styles")
+ .with_description("Styles for log messages.")
+ .with_children(theme_styles_handlers),
+
+ yajlpp::property_handler("syntax-styles")
+ .with_description("Styles for syntax highlighting in text files.")
+ .with_children(theme_syntax_styles_handlers),
+
+ yajlpp::property_handler("status-styles")
+ .with_description("Styles for the user-interface components.")
+ .with_children(theme_status_styles_handlers),
+
+ yajlpp::property_handler("log-level-styles")
+ .with_description("Styles for each log message level.")
+ .with_children(theme_log_level_styles_handlers),
+
+ yajlpp::property_handler("highlights")
+ .with_description("Styles for text highlights.")
+ .with_children(theme_highlights_handlers),
+};
+
+static const struct json_path_container theme_defs_handlers = {
+ yajlpp::pattern_property_handler("(?<theme_name>[\\w\\-]+)")
+ .with_description("Theme definitions")
+ .with_obj_provider<lnav_theme, _lnav_config>(
+ [](const yajlpp_provider_context& ypc, _lnav_config* root) {
+ lnav_theme& lt
+ = root->lc_ui_theme_defs[ypc.get_substr("theme_name")];
+
+ return &lt;
+ })
+ .with_path_provider<_lnav_config>(
+ [](struct _lnav_config* cfg, std::vector<std::string>& paths_out) {
+ for (const auto& iter : cfg->lc_ui_theme_defs) {
+ paths_out.emplace_back(iter.first);
+ }
+ })
+ .with_obj_deleter(
+ +[](const yajlpp_provider_context& ypc, _lnav_config* root) {
+ root->lc_ui_theme_defs.erase(ypc.get_substr("theme_name"));
+ })
+ .with_children(theme_def_handlers),
+};
+
+static const struct json_path_container ui_handlers = {
+ yajlpp::property_handler("clock-format")
+ .with_synopsis("format")
+ .with_description("The format for the clock displayed in "
+ "the top-left corner using strftime(3) conversions")
+ .with_example("%a %b %d %H:%M:%S %Z")
+ .for_field(&_lnav_config::lc_top_status_cfg,
+ &top_status_source_cfg::tssc_clock_format),
+ yajlpp::property_handler("dim-text")
+ .with_synopsis("bool")
+ .with_description("Reduce the brightness of text (useful for xterms). "
+ "This setting can be useful when running in an xterm "
+ "where the white color is very bright.")
+ .for_field(&_lnav_config::lc_ui_dim_text),
+ yajlpp::property_handler("default-colors")
+ .with_synopsis("bool")
+ .with_description(
+ "Use default terminal background and foreground colors "
+ "instead of black and white for all text coloring. This setting "
+ "can be useful when transparent background or alternate color "
+ "theme terminal is used.")
+ .for_field(&_lnav_config::lc_ui_default_colors),
+ yajlpp::property_handler("keymap")
+ .with_synopsis("keymap_name")
+ .with_description("The name of the keymap to use.")
+ .for_field(&_lnav_config::lc_ui_keymap),
+ yajlpp::property_handler("theme")
+ .with_synopsis("theme_name")
+ .with_description("The name of the theme to use.")
+ .for_field(&_lnav_config::lc_ui_theme),
+ yajlpp::property_handler("theme-defs")
+ .with_description("Theme definitions.")
+ .with_children(theme_defs_handlers),
+ yajlpp::property_handler("movement")
+ .with_description("Log file cursor movement mode settings")
+ .with_children(movement_handlers),
+ yajlpp::property_handler("keymap-defs")
+ .with_description("Keymap definitions.")
+ .with_children(keymap_defs_handlers),
+};
+
+static const struct json_path_container archive_handlers = {
+ yajlpp::property_handler("min-free-space")
+ .with_synopsis("<bytes>")
+ .with_description(
+ "The minimum free space, in bytes, to maintain when unpacking "
+ "archives")
+ .with_min_value(0)
+ .for_field(&_lnav_config::lc_archive_manager,
+ &archive_manager::config::amc_min_free_space),
+ yajlpp::property_handler("cache-ttl")
+ .with_synopsis("<duration>")
+ .with_description(
+ "The time-to-live for unpacked archives, expressed as a duration "
+ "(e.g. '3d' for three days)")
+ .with_example("3d")
+ .with_example("12h")
+ .for_field(&_lnav_config::lc_archive_manager,
+ &archive_manager::config::amc_cache_ttl),
+};
+
+static const struct json_path_container file_vtab_handlers = {
+ yajlpp::property_handler("max-content-size")
+ .with_synopsis("<bytes>")
+ .with_description(
+ "The maximum allowed file size for the content column")
+ .with_min_value(0)
+ .for_field(&_lnav_config::lc_file_vtab,
+ &file_vtab::config::fvc_max_content_size),
+};
+
+static const struct json_path_container logfile_handlers = {
+ yajlpp::property_handler("max-unrecognized-lines")
+ .with_synopsis("<lines>")
+ .with_description("The maximum number of lines in a file to use when "
+ "detecting the format")
+ .with_min_value(1)
+ .for_field(&_lnav_config::lc_logfile,
+ &lnav::logfile::config::lc_max_unrecognized_lines),
+};
+
+static const struct json_path_container ssh_config_handlers = {
+ yajlpp::pattern_property_handler("(?<config_name>\\w+)")
+ .with_synopsis("name")
+ .with_description("Set an SSH configuration value")
+ .with_path_provider<_lnav_config>(
+ [](auto* m, std::vector<std::string>& paths_out) {
+ for (const auto& pair : m->lc_tailer.c_ssh_config) {
+ paths_out.emplace_back(pair.first);
+ }
+ })
+ .for_field(&_lnav_config::lc_tailer, &tailer::config::c_ssh_config),
+};
+
+static const struct json_path_container ssh_option_handlers = {
+ yajlpp::pattern_property_handler("(?<option_name>\\w+)")
+ .with_synopsis("name")
+ .with_description("Set an option to be passed to the SSH command")
+ .for_field(&_lnav_config::lc_tailer, &tailer::config::c_ssh_options),
+};
+
+static const struct json_path_container ssh_handlers = {
+ yajlpp::property_handler("command")
+ .with_synopsis("ssh-command")
+ .with_description("The SSH command to execute")
+ .for_field(&_lnav_config::lc_tailer, &tailer::config::c_ssh_cmd),
+ yajlpp::property_handler("transfer-command")
+ .with_synopsis("command")
+ .with_description(
+ "Command executed on the remote host when transferring the file")
+ .for_field(&_lnav_config::lc_tailer, &tailer::config::c_transfer_cmd),
+ yajlpp::property_handler("start-command")
+ .with_synopsis("command")
+ .with_description(
+ "Command executed on the remote host to start the tailer")
+ .for_field(&_lnav_config::lc_tailer, &tailer::config::c_start_cmd),
+ yajlpp::property_handler("flags")
+ .with_description("The flags to pass to the SSH command")
+ .for_field(&_lnav_config::lc_tailer, &tailer::config::c_ssh_flags),
+ yajlpp::property_handler("options")
+ .with_description("The options to pass to the SSH command")
+ .with_children(ssh_option_handlers),
+ yajlpp::property_handler("config")
+ .with_description(
+ "The ssh_config options to pass to SSH with the -o option")
+ .with_children(ssh_config_handlers),
+};
+
+static const struct json_path_container remote_handlers = {
+ yajlpp::property_handler("cache-ttl")
+ .with_synopsis("<duration>")
+ .with_description("The time-to-live for files copied from remote "
+ "hosts, expressed as a duration "
+ "(e.g. '3d' for three days)")
+ .with_example("3d")
+ .with_example("12h")
+ .for_field(&_lnav_config::lc_tailer, &tailer::config::c_cache_ttl),
+ yajlpp::property_handler("ssh")
+ .with_description(
+ "Settings related to the ssh command used to contact remote "
+ "machines")
+ .with_children(ssh_handlers),
+};
+
+static const struct json_path_container sysclip_impl_cmd_handlers = json_path_container{
+ yajlpp::property_handler("write")
+ .with_synopsis("<command>")
+ .with_description("The command used to write to the clipboard")
+ .with_example("pbcopy")
+ .for_field(&sysclip::clip_commands::cc_write),
+ yajlpp::property_handler("read")
+ .with_synopsis("<command>")
+ .with_description("The command used to read from the clipboard")
+ .with_example("pbpaste")
+ .for_field(&sysclip::clip_commands::cc_read),
+}
+ .with_description("Container for the commands used to read from and write to the system clipboard")
+ .with_definition_id("clip-commands");
+
+static const struct json_path_container sysclip_impl_handlers = {
+ yajlpp::property_handler("test")
+ .with_synopsis("<command>")
+ .with_description("The command that checks")
+ .with_example("command -v pbcopy")
+ .for_field(&sysclip::clipboard::c_test_command),
+ yajlpp::property_handler("general")
+ .with_description("Commands to work with the general clipboard")
+ .for_child(&sysclip::clipboard::c_general)
+ .with_children(sysclip_impl_cmd_handlers),
+ yajlpp::property_handler("find")
+ .with_description("Commands to work with the find clipboard")
+ .for_child(&sysclip::clipboard::c_find)
+ .with_children(sysclip_impl_cmd_handlers),
+};
+
+static const struct json_path_container sysclip_impls_handlers = {
+ yajlpp::pattern_property_handler("(?<clipboard_impl_name>[\\w\\-]+)")
+ .with_synopsis("<name>")
+ .with_description("Clipboard implementation")
+ .with_obj_provider<sysclip::clipboard, _lnav_config>(
+ [](const yajlpp_provider_context& ypc, _lnav_config* root) {
+ auto& retval
+ = root->lc_sysclip.c_clipboard_impls[ypc.get_substr(
+ "clipboard_impl_name")];
+ return &retval;
+ })
+ .with_path_provider<_lnav_config>(
+ [](struct _lnav_config* cfg, std::vector<std::string>& paths_out) {
+ for (const auto& iter : cfg->lc_sysclip.c_clipboard_impls) {
+ paths_out.emplace_back(iter.first);
+ }
+ })
+ .with_children(sysclip_impl_handlers),
+};
+
+static const struct json_path_container sysclip_handlers = {
+ yajlpp::property_handler("impls")
+ .with_description("Clipboard implementations")
+ .with_children(sysclip_impls_handlers),
+};
+
+static const struct json_path_container log_source_watch_expr_handlers = {
+ yajlpp::property_handler("expr")
+ .with_synopsis("<SQL-expression>")
+ .with_description("The SQL expression to execute for each input line. "
+ "If expression evaluates to true, a 'log message "
+ "detected' event will be published.")
+ .for_field(&logfile_sub_source_ns::watch_expression::we_expr),
+ yajlpp::property_handler("enabled")
+ .with_description("Indicates whether or not this expression should be "
+ "evaluated during log processing.")
+ .for_field(&logfile_sub_source_ns::watch_expression::we_enabled),
+};
+
+static const struct json_path_container log_source_watch_handlers = {
+ yajlpp::pattern_property_handler("(?<watch_name>[\\w\\-]+)")
+ .with_synopsis("<name>")
+ .with_description("A log message watch expression")
+ .with_obj_provider<logfile_sub_source_ns::watch_expression,
+ _lnav_config>(
+ [](const yajlpp_provider_context& ypc, _lnav_config* root) {
+ auto& retval = root->lc_log_source
+ .c_watch_exprs[ypc.get_substr("watch_name")];
+ return &retval;
+ })
+ .with_path_provider<_lnav_config>(
+ [](struct _lnav_config* cfg, std::vector<std::string>& paths_out) {
+ for (const auto& iter : cfg->lc_log_source.c_watch_exprs) {
+ paths_out.emplace_back(iter.first);
+ }
+ })
+ .with_obj_deleter(
+ +[](const yajlpp_provider_context& ypc, _lnav_config* root) {
+ root->lc_log_source.c_watch_exprs.erase(
+ ypc.get_substr("watch_name"));
+ })
+ .with_children(log_source_watch_expr_handlers),
+};
+
+static const struct json_path_container log_source_handlers = {
+ yajlpp::property_handler("watch-expressions")
+ .with_description("Log message watch expressions")
+ .with_children(log_source_watch_handlers),
+};
+
+static const struct json_path_container tuning_handlers = {
+ yajlpp::property_handler("archive-manager")
+ .with_description("Settings related to opening archive files")
+ .with_children(archive_handlers),
+ yajlpp::property_handler("file-vtab")
+ .with_description("Settings related to the lnav_file virtual-table")
+ .with_children(file_vtab_handlers),
+ yajlpp::property_handler("logfile")
+ .with_description("Settings related to log files")
+ .with_children(logfile_handlers),
+ yajlpp::property_handler("remote")
+ .with_description("Settings related to remote file support")
+ .with_children(remote_handlers),
+ yajlpp::property_handler("clipboard")
+ .with_description("Settings related to the clipboard")
+ .with_children(sysclip_handlers),
+};
+
+const char* DEFAULT_CONFIG_SCHEMA
+ = "https://lnav.org/schemas/config-v1.schema.json";
+
+static const std::set<std::string> SUPPORTED_CONFIG_SCHEMAS = {
+ DEFAULT_CONFIG_SCHEMA,
+};
+
+const char* DEFAULT_FORMAT_SCHEMA
+ = "https://lnav.org/schemas/format-v1.schema.json";
+
+const std::set<std::string> SUPPORTED_FORMAT_SCHEMAS = {
+ DEFAULT_FORMAT_SCHEMA,
+};
+
+static int
+read_id(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
+{
+ auto file_id = std::string((const char*) str, len);
+
+ if (SUPPORTED_CONFIG_SCHEMAS.count(file_id) == 0) {
+ const auto* handler = ypc->ypc_current_handler;
+ attr_line_t notes{"expecting one of the following $schema values:"};
+
+ for (const auto& schema : SUPPORTED_CONFIG_SCHEMAS) {
+ notes.append("\n").append(
+ lnav::roles::symbol(fmt::format(FMT_STRING(" {}"), schema)));
+ }
+ ypc->report_error(
+ lnav::console::user_message::error(
+ attr_line_t("'")
+ .append(lnav::roles::symbol(file_id))
+ .append(
+ "' is not a supported configuration $schema version"))
+ .with_snippet(ypc->get_snippet())
+ .with_note(notes)
+ .with_help(handler->get_help_text(ypc)));
+ }
+
+ return 1;
+}
+
+const json_path_container lnav_config_handlers = json_path_container {
+ json_path_handler("$schema", read_id)
+ .with_synopsis("<schema-uri>")
+ .with_description("The URI that specifies the schema that describes this type of file")
+ .with_example(DEFAULT_CONFIG_SCHEMA),
+
+ yajlpp::property_handler("tuning")
+ .with_description("Internal settings")
+ .with_children(tuning_handlers),
+
+ yajlpp::property_handler("ui")
+ .with_description("User-interface settings")
+ .with_children(ui_handlers),
+
+ yajlpp::property_handler("log")
+ .with_description("Log message settings")
+ .with_children(log_source_handlers),
+
+ yajlpp::property_handler("global")
+ .with_description("Global variable definitions")
+ .with_children(global_var_handlers),
+}
+ .with_schema_id(*SUPPORTED_CONFIG_SCHEMAS.cbegin());
+
+class active_key_map_listener : public lnav_config_listener {
+public:
+ void reload_config(error_reporter& reporter) override
+ {
+ lnav_config.lc_active_keymap = lnav_config.lc_ui_keymaps["default"];
+ for (const auto& pair :
+ lnav_config.lc_ui_keymaps[lnav_config.lc_ui_keymap].km_seq_to_cmd)
+ {
+ lnav_config.lc_active_keymap.km_seq_to_cmd[pair.first]
+ = pair.second;
+ }
+ }
+};
+
+static active_key_map_listener KEYMAP_LISTENER;
+
+Result<config_file_type, std::string>
+detect_config_file_type(const ghc::filesystem::path& path)
+{
+ static const char* id_path[] = {"$schema", nullptr};
+
+ auto content = TRY(lnav::filesystem::read_file(path));
+ if (startswith(content, "#")) {
+ content.insert(0, "//");
+ }
+
+ char error_buffer[1024];
+ auto content_tree = std::unique_ptr<yajl_val_s, decltype(&yajl_tree_free)>(
+ yajl_tree_parse(content.c_str(), error_buffer, sizeof(error_buffer)),
+ yajl_tree_free);
+ if (content_tree == nullptr) {
+ return Err(
+ fmt::format(FMT_STRING("JSON parsing failed -- {}"), error_buffer));
+ }
+
+ auto* id_val = yajl_tree_get(content_tree.get(), id_path, yajl_t_string);
+ if (id_val != nullptr) {
+ if (SUPPORTED_CONFIG_SCHEMAS.count(id_val->u.string)) {
+ return Ok(config_file_type::CONFIG);
+ }
+ if (SUPPORTED_FORMAT_SCHEMAS.count(id_val->u.string)) {
+ return Ok(config_file_type::FORMAT);
+ }
+ return Err(fmt::format(
+ FMT_STRING("unsupported configuration version in file -- {}"),
+ id_val->u.string));
+ }
+ return Ok(config_file_type::FORMAT);
+}
+
+static void
+load_config_from(_lnav_config& lconfig,
+ const ghc::filesystem::path& path,
+ std::vector<lnav::console::user_message>& errors)
+{
+ yajlpp_parse_context ypc(intern_string::lookup(path.string()),
+ &lnav_config_handlers);
+ struct config_userdata ud(errors);
+ auto_fd fd;
+
+ log_info("loading configuration from %s", path.c_str());
+ ypc.ypc_locations = &lnav_config_locations;
+ ypc.with_obj(lconfig);
+ ypc.ypc_userdata = &ud;
+ ypc.with_error_reporter(config_error_reporter);
+ if ((fd = lnav::filesystem::openp(path, O_RDONLY)) == -1) {
+ if (errno != ENOENT) {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("unable to open configuration file: ")
+ .append(lnav::roles::file(path)))
+ .with_errno_reason());
+ }
+ } else {
+ auto_mem<yajl_handle_t> handle(yajl_free);
+ char buffer[2048];
+ ssize_t rc = -1;
+
+ handle = yajl_alloc(&ypc.ypc_callbacks, nullptr, &ypc);
+ yajl_config(handle, yajl_allow_comments, 1);
+ yajl_config(handle, yajl_allow_multiple_values, 1);
+ ypc.ypc_handle = handle;
+ while (true) {
+ rc = read(fd, buffer, sizeof(buffer));
+ if (rc == 0) {
+ break;
+ }
+ if (rc == -1) {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("unable to read format file: ")
+ .append(lnav::roles::file(path)))
+ .with_errno_reason());
+ break;
+ }
+ if (ypc.parse((const unsigned char*) buffer, rc) != yajl_status_ok)
+ {
+ break;
+ }
+ }
+ if (rc == 0) {
+ ypc.complete_parse();
+ }
+ }
+}
+
+static bool
+load_default_config(struct _lnav_config& config_obj,
+ const std::string& path,
+ const bin_src_file& bsf,
+ std::vector<lnav::console::user_message>& errors)
+{
+ yajlpp_parse_context ypc_builtin(intern_string::lookup(bsf.get_name()),
+ &lnav_config_handlers);
+ auto_mem<yajl_handle_t> handle(yajl_free);
+ struct config_userdata ud(errors);
+
+ handle = yajl_alloc(&ypc_builtin.ypc_callbacks, nullptr, &ypc_builtin);
+ ypc_builtin.ypc_locations = &lnav_config_locations;
+ ypc_builtin.with_handle(handle);
+ ypc_builtin.with_obj(config_obj);
+ ypc_builtin.with_error_reporter(config_error_reporter);
+ ypc_builtin.ypc_userdata = &ud;
+
+ if (path != "*") {
+ ypc_builtin.ypc_ignore_unused = true;
+ ypc_builtin.ypc_active_paths.insert(path);
+ }
+
+ yajl_config(handle, yajl_allow_comments, 1);
+ yajl_config(handle, yajl_allow_multiple_values, 1);
+ if (ypc_builtin.parse(bsf.to_string_fragment()) == yajl_status_ok) {
+ ypc_builtin.complete_parse();
+ }
+
+ return path == "*" || ypc_builtin.ypc_active_paths.empty();
+}
+
+static bool
+load_default_configs(struct _lnav_config& config_obj,
+ const std::string& path,
+ std::vector<lnav::console::user_message>& errors)
+{
+ auto retval = false;
+
+ for (auto& bsf : lnav_config_json) {
+ retval = load_default_config(config_obj, path, bsf, errors) || retval;
+ }
+
+ return retval;
+}
+
+void
+load_config(const std::vector<ghc::filesystem::path>& extra_paths,
+ std::vector<lnav::console::user_message>& errors)
+{
+ auto user_config = lnav::paths::dotlnav() / "config.json";
+
+ for (auto& bsf : lnav_config_json) {
+ auto sample_path = lnav::paths::dotlnav() / "configs" / "default"
+ / fmt::format(FMT_STRING("{}.sample"), bsf.get_name());
+
+ auto write_res = lnav::filesystem::write_file(sample_path,
+ bsf.to_string_fragment());
+ if (write_res.isErr()) {
+ fprintf(stderr,
+ "error:unable to write default config file: %s -- %s\n",
+ sample_path.c_str(),
+ write_res.unwrapErr().c_str());
+ }
+ }
+
+ {
+ log_info("loading builtin configuration into default");
+ load_default_configs(lnav_default_config, "*", errors);
+ log_info("loading builtin configuration into base");
+ load_default_configs(lnav_config, "*", errors);
+
+ log_info("loading installed configuration files");
+ for (const auto& extra_path : extra_paths) {
+ auto config_path = extra_path / "configs/*/*.json";
+ static_root_mem<glob_t, globfree> gl;
+
+ if (glob(config_path.c_str(), 0, nullptr, gl.inout()) == 0) {
+ for (size_t lpc = 0; lpc < gl->gl_pathc; lpc++) {
+ load_config_from(lnav_config, gl->gl_pathv[lpc], errors);
+ if (errors.empty()) {
+ load_config_from(
+ lnav_default_config, gl->gl_pathv[lpc], errors);
+ }
+ }
+ }
+ }
+ for (const auto& extra_path : extra_paths) {
+ auto config_path = extra_path / "formats/*/config.*.json";
+ static_root_mem<glob_t, globfree> gl;
+
+ if (glob(config_path.c_str(), 0, nullptr, gl.inout()) == 0) {
+ for (size_t lpc = 0; lpc < gl->gl_pathc; lpc++) {
+ load_config_from(lnav_config, gl->gl_pathv[lpc], errors);
+ if (errors.empty()) {
+ load_config_from(
+ lnav_default_config, gl->gl_pathv[lpc], errors);
+ }
+ }
+ }
+ }
+
+ log_info("loading user configuration");
+ load_config_from(lnav_config, user_config, errors);
+ }
+
+ reload_config(errors);
+
+ rollback_lnav_config = lnav_config;
+}
+
+void
+reset_config(const std::string& path)
+{
+ std::vector<lnav::console::user_message> errors;
+
+ load_default_configs(lnav_config, path, errors);
+ if (path != "*") {
+ static const auto INPUT_SRC = intern_string::lookup("input");
+
+ yajlpp_parse_context ypc(INPUT_SRC, &lnav_config_handlers);
+ ypc.set_path(path)
+ .with_obj(lnav_config)
+ .with_error_reporter([&errors](const auto& ypc, auto msg) {
+ errors.push_back(msg);
+ });
+ ypc.ypc_active_paths.insert(path);
+ ypc.update_callbacks();
+ const json_path_handler_base* jph = ypc.ypc_current_handler;
+
+ if (!ypc.ypc_handler_stack.empty()) {
+ jph = ypc.ypc_handler_stack.back();
+ }
+
+ if (jph != nullptr && jph->jph_children && jph->jph_obj_deleter) {
+ auto key_start = ypc.ypc_path_index_stack.back();
+ auto path_frag = string_fragment::from_byte_range(
+ ypc.ypc_path.data(), key_start + 1, ypc.ypc_path.size());
+ auto md = jph->jph_regex->create_match_data();
+ yajlpp_provider_context provider_ctx{&md, static_cast<size_t>(-1)};
+ jph->jph_regex->capture_from(path_frag).into(md).matches();
+
+ jph->jph_obj_deleter(provider_ctx, ypc.ypc_obj_stack.top());
+ }
+ }
+
+ reload_config(errors);
+
+ for (const auto& err : errors) {
+ log_debug("reset %s", err.um_message.get_string().c_str());
+ }
+}
+
+std::string
+save_config()
+{
+ auto user_config = lnav::paths::dotlnav() / "config.json";
+
+ yajlpp_gen gen;
+ yajlpp_gen_context ygc(gen, lnav_config_handlers);
+
+ ygc.with_default_obj(lnav_default_config).with_obj(lnav_config);
+ ygc.gen();
+
+ auto config_str = gen.to_string_fragment().to_string();
+ char errbuf[1024];
+ auto_mem<yajl_val_s> tree(yajl_tree_free);
+
+ tree = yajl_tree_parse(config_str.c_str(), errbuf, sizeof(errbuf));
+
+ if (tree == nullptr) {
+ return fmt::format(
+ FMT_STRING("error: unable to save configuration -- {}"), errbuf);
+ }
+
+ yajl_cleanup_tree(tree);
+
+ yajlpp_gen clean_gen;
+
+ yajl_gen_config(clean_gen, yajl_gen_beautify, true);
+ yajl_gen_tree(clean_gen, tree);
+
+ auto write_res = lnav::filesystem::write_file(
+ user_config, clean_gen.to_string_fragment());
+ if (write_res.isErr()) {
+ return fmt::format(
+ FMT_STRING("error: unable to write configuration file: {} -- {}"),
+ user_config.string(),
+ write_res.unwrapErr());
+ }
+
+ return "info: configuration saved";
+}
+
+void
+reload_config(std::vector<lnav::console::user_message>& errors)
+{
+ lnav_config_listener* curr = lnav_config_listener::LISTENER_LIST;
+
+ while (curr != nullptr) {
+ auto reporter = [&errors](const void* cfg_value,
+ const lnav::console::user_message& errmsg) {
+ auto cb = [&cfg_value, &errors, &errmsg](
+ const json_path_handler_base& jph,
+ const std::string& path,
+ void* mem) {
+ if (mem != cfg_value) {
+ return;
+ }
+
+ auto loc_iter
+ = lnav_config_locations.find(intern_string::lookup(path));
+ if (loc_iter == lnav_config_locations.end()) {
+ return;
+ }
+
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append("invalid value for property ")
+ .append_quoted(lnav::roles::symbol(path)))
+ .with_reason(errmsg)
+ .with_snippet(
+ lnav::console::snippet::from(
+ loc_iter->second.sl_source, "")
+ .with_line(loc_iter->second.sl_line_number))
+ .with_help(jph.get_help_text(path)));
+ };
+
+ for (const auto& jph : lnav_config_handlers.jpc_children) {
+ jph.walk(cb, &lnav_config);
+ }
+ };
+
+ curr->reload_config(reporter);
+ curr = curr->lcl_next;
+ }
+}
diff --git a/src/lnav_config.hh b/src/lnav_config.hh
new file mode 100644
index 0000000..8341eca
--- /dev/null
+++ b/src/lnav_config.hh
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav_config.hh
+ */
+
+#ifndef lnav_config_hh
+#define lnav_config_hh
+
+#include <functional>
+#include <map>
+#include <set>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "archive_manager.cfg.hh"
+#include "base/file_range.hh"
+#include "base/lnav.console.hh"
+#include "base/result.h"
+#include "file_vtab.cfg.hh"
+#include "ghc/filesystem.hpp"
+#include "lnav_config_fwd.hh"
+#include "log_level.hh"
+#include "logfile.cfg.hh"
+#include "logfile_sub_source.cfg.hh"
+#include "styling.hh"
+#include "sysclip.cfg.hh"
+#include "tailer/tailer.looper.cfg.hh"
+#include "top_status_source.cfg.hh"
+
+/**
+ * Check if an experimental feature should be enabled by
+ * examining the LNAV_EXP environment variable.
+ *
+ * @param feature_name The feature name to check for in
+ * the LNAV_EXP environment variable.
+ *
+ * @return True if the feature was mentioned in the env
+ * var and should be enabled.
+ */
+bool check_experimental(const char* feature_name);
+
+/**
+ * Ensure that the '.lnav' directory exists.
+ */
+void ensure_dotlnav();
+
+bool install_from_git(const std::string& repo);
+bool update_installs_from_git();
+
+void install_extra_formats();
+
+struct key_command {
+ std::string kc_cmd;
+ std::string kc_alt_msg;
+};
+
+struct key_map {
+ std::map<std::string, key_command> km_seq_to_cmd;
+};
+
+enum class config_movement_mode : unsigned int {
+ TOP,
+ CURSOR,
+};
+
+struct movement_config {
+ config_movement_mode mode{config_movement_mode::TOP};
+};
+
+struct _lnav_config {
+ top_status_source_cfg lc_top_status_cfg;
+ bool lc_ui_dim_text;
+ bool lc_ui_default_colors{true};
+ std::string lc_ui_keymap;
+ std::string lc_ui_theme;
+ movement_config lc_ui_movement;
+ std::unordered_map<std::string, key_map> lc_ui_keymaps;
+ std::map<std::string, std::string> lc_ui_key_overrides;
+ std::map<std::string, std::string> lc_global_vars;
+ std::map<std::string, lnav_theme> lc_ui_theme_defs;
+
+ key_map lc_active_keymap;
+
+ archive_manager::config lc_archive_manager;
+ file_vtab::config lc_file_vtab;
+ lnav::logfile::config lc_logfile;
+ tailer::config lc_tailer;
+ sysclip::config lc_sysclip;
+ logfile_sub_source_ns::config lc_log_source;
+};
+
+extern struct _lnav_config lnav_config;
+extern struct _lnav_config rollback_lnav_config;
+extern std::map<intern_string_t, source_location> lnav_config_locations;
+
+extern const struct json_path_container lnav_config_handlers;
+
+enum class config_file_type {
+ FORMAT,
+ CONFIG,
+};
+
+Result<config_file_type, std::string> detect_config_file_type(
+ const ghc::filesystem::path& path);
+
+void load_config(const std::vector<ghc::filesystem::path>& extra_paths,
+ std::vector<lnav::console::user_message>& errors);
+
+void reset_config(const std::string& path);
+
+void reload_config(std::vector<lnav::console::user_message>& errors);
+
+std::string save_config();
+
+extern const char* DEFAULT_FORMAT_SCHEMA;
+extern const std::set<std::string> SUPPORTED_FORMAT_SCHEMAS;
+
+#endif
diff --git a/src/lnav_config_fwd.hh b/src/lnav_config_fwd.hh
new file mode 100644
index 0000000..0b371a3
--- /dev/null
+++ b/src/lnav_config_fwd.hh
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav_config_fwd.hh
+ */
+
+#ifndef lnav_config_fwd_hh
+#define lnav_config_fwd_hh
+
+#include <functional>
+#include <string>
+
+#include "base/lnav.console.hh"
+
+class lnav_config_listener {
+public:
+ using error_reporter = const std::function<void(
+ const void*, const lnav::console::user_message& msg)>;
+
+ lnav_config_listener()
+ {
+ this->lcl_next = LISTENER_LIST;
+ LISTENER_LIST = this;
+ }
+
+ virtual ~lnav_config_listener() = default;
+
+ virtual void reload_config(error_reporter& reporter) {}
+
+ virtual void unload_config() {}
+
+ static void unload_all() {
+ auto* lcl = LISTENER_LIST;
+ while (lcl != nullptr) {
+ lcl->unload_config();
+ lcl = lcl->lcl_next;
+ }
+ }
+
+ static lnav_config_listener* LISTENER_LIST;
+
+ lnav_config_listener* lcl_next;
+};
+
+#endif
diff --git a/src/lnav_util.cc b/src/lnav_util.cc
new file mode 100644
index 0000000..e5540d9
--- /dev/null
+++ b/src/lnav_util.cc
@@ -0,0 +1,334 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav_util.cc
+ *
+ * Dumping ground for useful functions with no other home.
+ */
+
+#include "lnav_util.hh"
+
+#include <stdio.h>
+#include <sys/stat.h>
+
+#include "base/ansi_scrubber.hh"
+#include "base/itertools.hh"
+#include "base/result.h"
+#include "bookmarks.hh"
+#include "config.h"
+#include "fmt/format.h"
+#include "log_format_fwd.hh"
+#include "view_curses.hh"
+#include "yajlpp/yajlpp.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+bool
+change_to_parent_dir()
+{
+ bool retval = false;
+ char cwd[3] = "";
+
+ if (getcwd(cwd, sizeof(cwd)) == nullptr) {
+ /* perror("getcwd"); */
+ }
+ if (strcmp(cwd, "/") != 0) {
+ if (chdir("..") == -1) {
+ perror("chdir('..')");
+ } else {
+ retval = true;
+ }
+ }
+
+ return retval;
+}
+
+bool
+is_dev_null(const struct stat& st)
+{
+ struct stat null_stat;
+
+ stat("/dev/null", &null_stat);
+
+ return st.st_dev == null_stat.st_dev && st.st_ino == null_stat.st_ino;
+}
+
+bool
+is_dev_null(int fd)
+{
+ struct stat fd_stat;
+
+ fstat(fd, &fd_stat);
+ return is_dev_null(fd_stat);
+}
+
+void
+write_line_to(FILE* outfile, const attr_line_t& al)
+{
+ const auto& al_attrs = al.get_attrs();
+ auto lr = find_string_attr_range(al_attrs, &SA_ORIGINAL_LINE);
+
+ if (!lr.is_valid() || lr.lr_start > 1) {
+ // If the line is prefixed with some extra information, include that
+ // in the output. For example, the log file name or time offset.
+ lnav::console::println(outfile, al);
+ } else {
+ lnav::console::println(outfile, al.subline(lr.lr_start, lr.length()));
+ }
+}
+
+namespace lnav {
+
+std::string
+to_json(const std::string& str)
+{
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+ yajl_gen_string(gen, str);
+
+ return gen.to_string_fragment().to_string();
+}
+
+static void
+to_json(yajlpp_gen& gen, const attr_line_t& al)
+{
+ {
+ yajlpp_map root_map(gen);
+
+ root_map.gen("str");
+ root_map.gen(al.get_string());
+
+ root_map.gen("attrs");
+ {
+ yajlpp_array attr_array(gen);
+
+ for (const auto& sa : al.get_attrs()) {
+ yajlpp_map elem_map(gen);
+
+ elem_map.gen("start");
+ elem_map.gen(sa.sa_range.lr_start);
+ elem_map.gen("end");
+ elem_map.gen(sa.sa_range.lr_end);
+ elem_map.gen("type");
+ elem_map.gen(sa.sa_type->sat_name);
+ elem_map.gen("value");
+ sa.sa_value.match(
+ [&](int64_t i) { elem_map.gen(i); },
+ [&](role_t r) {
+ elem_map.gen(lnav::enums::to_underlying(r));
+ },
+ [&](const intern_string_t& str) { elem_map.gen(str); },
+ [&](const std::string& str) { elem_map.gen(str); },
+ [&](const text_attrs& ta) { elem_map.gen(ta.ta_attrs); },
+ [&](const std::shared_ptr<logfile>& lf) {
+ elem_map.gen("");
+ },
+ [&](const bookmark_metadata* bm) { elem_map.gen(""); },
+ [&](const timespec& ts) { elem_map.gen(""); },
+ [&](const string_fragment& sf) { elem_map.gen(sf); });
+ }
+ }
+ }
+}
+
+std::string
+to_json(const attr_line_t& al)
+{
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+ to_json(gen, al);
+
+ return gen.to_string_fragment().to_string();
+}
+
+std::string
+to_json(const lnav::console::user_message& um)
+{
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_map root_map(gen);
+
+ root_map.gen("level");
+ switch (um.um_level) {
+ case console::user_message::level::raw:
+ root_map.gen("raw");
+ break;
+ case console::user_message::level::ok:
+ root_map.gen("ok");
+ break;
+ case console::user_message::level::info:
+ root_map.gen("info");
+ break;
+ case console::user_message::level::warning:
+ root_map.gen("warning");
+ break;
+ case console::user_message::level::error:
+ root_map.gen("error");
+ break;
+ }
+
+ root_map.gen("message");
+ to_json(gen, um.um_message);
+ root_map.gen("reason");
+ to_json(gen, um.um_reason);
+ root_map.gen("snippets");
+ {
+ yajlpp_array snippet_array(gen);
+
+ for (const auto& snip : um.um_snippets) {
+ yajlpp_map snip_map(gen);
+
+ snip_map.gen("source");
+ snip_map.gen(snip.s_location.sl_source);
+ snip_map.gen("line");
+ snip_map.gen(snip.s_location.sl_line_number);
+ snip_map.gen("content");
+ to_json(gen, snip.s_content);
+ }
+ }
+ root_map.gen("help");
+ to_json(gen, um.um_help);
+ }
+
+ return gen.to_string_fragment().to_string();
+}
+
+static int
+read_string_attr_type(yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len)
+{
+ auto* sa = (string_attr*) ypc->ypc_obj_stack.top();
+ auto type = std::string((const char*) str, len);
+
+ if (type == "role") {
+ sa->sa_type = &VC_ROLE;
+ } else if (type == "preformatted") {
+ sa->sa_type = &SA_PREFORMATTED;
+ } else if (type == "style") {
+ sa->sa_type = &VC_STYLE;
+ } else {
+ log_error("unhandled string_attr type: %s", type.c_str());
+ ensure(false);
+ }
+ return 1;
+}
+
+static int
+read_string_attr_int_value(yajlpp_parse_context* ypc, long long in)
+{
+ auto* sa = (string_attr*) ypc->ypc_obj_stack.top();
+
+ if (sa->sa_type == &VC_ROLE) {
+ sa->sa_value = static_cast<role_t>(in);
+ } else if (sa->sa_type == &VC_STYLE) {
+ sa->sa_value = text_attrs{
+ static_cast<int32_t>(in),
+ };
+ }
+ return 1;
+}
+
+static const struct json_path_container string_attr_handlers = {
+ yajlpp::property_handler("start").for_field(&string_attr::sa_range,
+ &line_range::lr_start),
+ yajlpp::property_handler("end").for_field(&string_attr::sa_range,
+ &line_range::lr_end),
+ yajlpp::property_handler("type").add_cb(read_string_attr_type),
+ yajlpp::property_handler("value").add_cb(read_string_attr_int_value),
+};
+
+static const typed_json_path_container<attr_line_t> attr_line_handlers = {
+ yajlpp::property_handler("str").for_field(&attr_line_t::al_string),
+ yajlpp::property_handler("attrs#")
+ .for_field(&attr_line_t::al_attrs)
+ .with_children(string_attr_handlers),
+};
+
+template<>
+Result<attr_line_t, std::vector<console::user_message>>
+from_json(const std::string& json)
+{
+ static const auto STRING_SRC = intern_string::lookup("string");
+
+ return attr_line_handlers.parser_for(STRING_SRC).of(json);
+}
+
+static const json_path_container snippet_handlers = {
+ yajlpp::property_handler("source").for_field(&console::snippet::s_location,
+ &source_location::sl_source),
+ yajlpp::property_handler("line").for_field(
+ &console::snippet::s_location, &source_location::sl_line_number),
+ yajlpp::property_handler("content")
+ .for_child(&console::snippet::s_content)
+ .with_children(attr_line_handlers),
+};
+
+static const json_path_handler_base::enum_value_t LEVEL_ENUM[] = {
+ {"raw", lnav::console::user_message::level::raw},
+ {"ok", lnav::console::user_message::level::ok},
+ {"info", lnav::console::user_message::level::info},
+ {"warning", lnav::console::user_message::level::warning},
+ {"error", lnav::console::user_message::level::error},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const typed_json_path_container<console::user_message>
+ user_message_handlers = {
+ yajlpp::property_handler("level")
+ .with_enum_values(LEVEL_ENUM)
+ .for_field(&console::user_message::um_level),
+ yajlpp::property_handler("message")
+ .for_child(&console::user_message::um_message)
+ .with_children(attr_line_handlers),
+ yajlpp::property_handler("reason")
+ .for_child(&console::user_message::um_reason)
+ .with_children(attr_line_handlers),
+ yajlpp::property_handler("snippets#")
+ .for_field(&console::user_message::um_snippets)
+ .with_children(snippet_handlers),
+ yajlpp::property_handler("help")
+ .for_child(&console::user_message::um_help)
+ .with_children(attr_line_handlers),
+};
+
+template<>
+Result<lnav::console::user_message, std::vector<console::user_message>>
+from_json(const std::string& json)
+{
+ static const auto STRING_SRC = intern_string::lookup("string");
+
+ return user_message_handlers.parser_for(STRING_SRC).of(json);
+}
+
+} // namespace lnav
diff --git a/src/lnav_util.hh b/src/lnav_util.hh
new file mode 100644
index 0000000..5edb537
--- /dev/null
+++ b/src/lnav_util.hh
@@ -0,0 +1,241 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file lnav_util.hh
+ *
+ * Dumping ground for useful functions with no other home.
+ */
+
+#ifndef lnav_util_hh
+#define lnav_util_hh
+
+#include <future>
+#include <iterator>
+#include <numeric>
+#include <string>
+#include <type_traits>
+#include <vector>
+
+#include <fcntl.h>
+#include <poll.h>
+#include <sys/resource.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <time.h>
+
+#include "base/auto_mem.hh"
+#include "base/intern_string.hh"
+#include "base/lnav.console.hh"
+#include "base/result.h"
+#include "byte_array.hh"
+#include "config.h"
+#include "fmt/format.h"
+#include "optional.hpp"
+#include "ptimec.hh"
+#include "spookyhash/SpookyV2.h"
+
+#if SIZEOF_OFF_T == 8
+# define FORMAT_OFF_T "%lld"
+#elif SIZEOF_OFF_T == 4
+# define FORMAT_OFF_T "%ld"
+#else
+# error "off_t has unhandled size..."
+#endif
+
+class hasher {
+public:
+ using array_t = byte_array<2, uint64_t>;
+ static constexpr size_t STRING_SIZE = array_t::STRING_SIZE;
+
+ hasher() { this->h_context.Init(0, 0); }
+
+ hasher& update(const std::string& str)
+ {
+ this->h_context.Update(str.data(), str.length());
+
+ return *this;
+ }
+
+ hasher& update(const string_fragment& str)
+ {
+ this->h_context.Update(str.data(), str.length());
+
+ return *this;
+ }
+
+ hasher& update(const char* bits, size_t len)
+ {
+ this->h_context.Update(bits, len);
+
+ return *this;
+ }
+
+ hasher& update(int64_t value)
+ {
+ value = SPOOKYHASH_LITTLE_ENDIAN_64(value);
+ this->h_context.Update(&value, sizeof(value));
+
+ return *this;
+ }
+
+ array_t to_array()
+ {
+ uint64_t h1;
+ uint64_t h2;
+ array_t retval;
+
+ this->h_context.Final(&h1, &h2);
+ *retval.out(0) = SPOOKYHASH_LITTLE_ENDIAN_64(h1);
+ *retval.out(1) = SPOOKYHASH_LITTLE_ENDIAN_64(h2);
+ return retval;
+ }
+
+ void to_string(auto_buffer& buf)
+ {
+ array_t bits = this->to_array();
+
+ bits.to_string(std::back_inserter(buf));
+ }
+
+ std::string to_string()
+ {
+ array_t bits = this->to_array();
+ return bits.to_string();
+ }
+
+ std::string to_uuid_string()
+ {
+ array_t bits = this->to_array();
+ return bits.to_uuid_string();
+ }
+
+private:
+ SpookyHash h_context;
+};
+
+bool change_to_parent_dir();
+
+bool next_format(const char* const fmt[], int& index, int& locked_index);
+
+namespace std {
+inline string
+to_string(const string& s)
+{
+ return s;
+}
+inline string
+to_string(const char* s)
+{
+ return s;
+}
+} // namespace std
+
+inline bool
+is_glob(const std::string& fn)
+{
+ return (fn.find('*') != std::string::npos
+ || fn.find('?') != std::string::npos
+ || fn.find('[') != std::string::npos);
+}
+
+inline void
+rusagesub(const struct rusage& left,
+ const struct rusage& right,
+ struct rusage& diff_out)
+{
+ timersub(&left.ru_utime, &right.ru_utime, &diff_out.ru_utime);
+ timersub(&left.ru_stime, &right.ru_stime, &diff_out.ru_stime);
+ diff_out.ru_maxrss = left.ru_maxrss - right.ru_maxrss;
+ diff_out.ru_ixrss = left.ru_ixrss - right.ru_ixrss;
+ diff_out.ru_idrss = left.ru_idrss - right.ru_idrss;
+ diff_out.ru_isrss = left.ru_isrss - right.ru_isrss;
+ diff_out.ru_minflt = left.ru_minflt - right.ru_minflt;
+ diff_out.ru_majflt = left.ru_majflt - right.ru_majflt;
+ diff_out.ru_nswap = left.ru_nswap - right.ru_nswap;
+ diff_out.ru_inblock = left.ru_inblock - right.ru_inblock;
+ diff_out.ru_oublock = left.ru_oublock - right.ru_oublock;
+ diff_out.ru_msgsnd = left.ru_msgsnd - right.ru_msgsnd;
+ diff_out.ru_msgrcv = left.ru_msgrcv - right.ru_msgrcv;
+ diff_out.ru_nvcsw = left.ru_nvcsw - right.ru_nvcsw;
+ diff_out.ru_nivcsw = left.ru_nivcsw - right.ru_nivcsw;
+}
+
+inline void
+rusageadd(const struct rusage& left,
+ const struct rusage& right,
+ struct rusage& diff_out)
+{
+ timeradd(&left.ru_utime, &right.ru_utime, &diff_out.ru_utime);
+ timeradd(&left.ru_stime, &right.ru_stime, &diff_out.ru_stime);
+ diff_out.ru_maxrss = left.ru_maxrss + right.ru_maxrss;
+ diff_out.ru_ixrss = left.ru_ixrss + right.ru_ixrss;
+ diff_out.ru_idrss = left.ru_idrss + right.ru_idrss;
+ diff_out.ru_isrss = left.ru_isrss + right.ru_isrss;
+ diff_out.ru_minflt = left.ru_minflt + right.ru_minflt;
+ diff_out.ru_majflt = left.ru_majflt + right.ru_majflt;
+ diff_out.ru_nswap = left.ru_nswap + right.ru_nswap;
+ diff_out.ru_inblock = left.ru_inblock + right.ru_inblock;
+ diff_out.ru_oublock = left.ru_oublock + right.ru_oublock;
+ diff_out.ru_msgsnd = left.ru_msgsnd + right.ru_msgsnd;
+ diff_out.ru_msgrcv = left.ru_msgrcv + right.ru_msgrcv;
+ diff_out.ru_nvcsw = left.ru_nvcsw + right.ru_nvcsw;
+ diff_out.ru_nivcsw = left.ru_nivcsw + right.ru_nivcsw;
+}
+
+bool is_dev_null(const struct stat& st);
+bool is_dev_null(int fd);
+
+template<typename A>
+struct final_action { // slightly simplified
+ A act;
+ final_action(A a) : act{a} {}
+ ~final_action() { act(); }
+};
+
+template<typename A>
+final_action<A>
+finally(A act) // deduce action type
+{
+ return final_action<A>{act};
+}
+
+void write_line_to(FILE* outfile, const attr_line_t& al);
+
+namespace lnav {
+
+std::string to_json(const std::string& str);
+std::string to_json(const lnav::console::user_message& um);
+std::string to_json(const attr_line_t& al);
+
+template<typename T>
+Result<T, std::vector<lnav::console::user_message>> from_json(
+ const std::string& json);
+
+} // namespace lnav
+
+#endif
diff --git a/src/log.watch.cc b/src/log.watch.cc
new file mode 100644
index 0000000..e90b2eb
--- /dev/null
+++ b/src/log.watch.cc
@@ -0,0 +1,388 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "log.watch.hh"
+
+#include <sqlite3.h>
+
+#include "base/injector.hh"
+#include "bound_tags.hh"
+#include "lnav.events.hh"
+#include "lnav_config_fwd.hh"
+#include "log_format.hh"
+#include "logfile_sub_source.cfg.hh"
+#include "readline_highlighters.hh"
+#include "sql_util.hh"
+
+namespace lnav {
+namespace log {
+namespace watch {
+
+struct compiled_watch_expr {
+ auto_mem<sqlite3_stmt> cwe_stmt{sqlite3_finalize};
+ bool cwe_enabled{true};
+};
+
+struct expressions : public lnav_config_listener {
+ void reload_config(error_reporter& reporter) override
+ {
+ auto& lnav_db = injector::get<auto_sqlite3&>();
+
+ if (lnav_db.in() == nullptr) {
+ log_warning("db not initialized yet!");
+ return;
+ }
+
+ const auto& cfg = injector::get<const logfile_sub_source_ns::config&>();
+
+ this->e_watch_exprs.clear();
+ for (const auto& pair : cfg.c_watch_exprs) {
+ auto stmt_str = fmt::format(FMT_STRING("SELECT 1 WHERE {}"),
+ pair.second.we_expr);
+ compiled_watch_expr cwe;
+
+ log_info("preparing watch expression: %s", stmt_str.c_str());
+ auto retcode = sqlite3_prepare_v2(lnav_db,
+ stmt_str.c_str(),
+ stmt_str.size(),
+ cwe.cwe_stmt.out(),
+ nullptr);
+ if (retcode != SQLITE_OK) {
+ auto sql_al = attr_line_t(pair.second.we_expr)
+ .with_attr_for_all(SA_PREFORMATTED.value())
+ .with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ readline_sqlite_highlighter(sql_al, -1);
+ intern_string_t watch_expr_path = intern_string::lookup(
+ fmt::format(FMT_STRING("/log/watch-expressions/{}/expr"),
+ pair.first));
+ auto snippet = lnav::console::snippet::from(
+ source_location(watch_expr_path), sql_al);
+
+ auto um = lnav::console::user_message::error(
+ "SQL expression is invalid")
+ .with_reason(sqlite3_errmsg(lnav_db))
+ .with_snippet(snippet);
+
+ reporter(&pair.second.we_expr, um);
+ continue;
+ }
+
+ this->e_watch_exprs.emplace(pair.first, std::move(cwe));
+ }
+ }
+
+ void unload_config() override {
+ this->e_watch_exprs.clear();
+ }
+
+ std::map<std::string, compiled_watch_expr> e_watch_exprs;
+};
+
+static expressions exprs;
+
+void
+eval_with(logfile& lf, logfile::iterator ll)
+{
+ if (std::none_of(exprs.e_watch_exprs.begin(),
+ exprs.e_watch_exprs.end(),
+ [](const auto& elem) { return elem.second.cwe_enabled; }))
+ {
+ return;
+ }
+
+ static auto& lnav_db = injector::get<auto_sqlite3&>();
+
+ char timestamp_buffer[64] = "";
+ shared_buffer_ref raw_sbr;
+ logline_value_vector values;
+ lf.read_full_message(ll, values.lvv_sbr);
+ values.lvv_sbr.erase_ansi();
+ auto format = lf.get_format();
+ string_attrs_t sa;
+ auto line_number = std::distance(lf.begin(), ll);
+ format->annotate(line_number, sa, values);
+
+ for (auto& watch_pair : exprs.e_watch_exprs) {
+ if (!watch_pair.second.cwe_enabled) {
+ continue;
+ }
+
+ auto* stmt = watch_pair.second.cwe_stmt.in();
+ sqlite3_reset(stmt);
+
+ auto count = sqlite3_bind_parameter_count(stmt);
+ auto missing_column = false;
+ for (int lpc = 0; lpc < count; lpc++) {
+ const auto* name = sqlite3_bind_parameter_name(stmt, lpc + 1);
+
+ if (name[0] == '$') {
+ const char* env_value;
+
+ if ((env_value = getenv(&name[1])) != nullptr) {
+ sqlite3_bind_text(
+ stmt, lpc + 1, env_value, -1, SQLITE_STATIC);
+ }
+ continue;
+ }
+ if (strcmp(name, ":log_level") == 0) {
+ sqlite3_bind_text(
+ stmt, lpc + 1, ll->get_level_name(), -1, SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_time") == 0) {
+ auto len = sql_strftime(timestamp_buffer,
+ sizeof(timestamp_buffer),
+ ll->get_timeval(),
+ 'T');
+ sqlite3_bind_text(
+ stmt, lpc + 1, timestamp_buffer, len, SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_time_msecs") == 0) {
+ sqlite3_bind_int64(stmt, lpc + 1, ll->get_time_in_millis());
+ continue;
+ }
+ if (strcmp(name, ":log_format") == 0) {
+ const auto format_name = format->get_name();
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ format_name.get(),
+ format_name.size(),
+ SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_format_regex") == 0) {
+ const auto pat_name = format->get_pattern_name(line_number);
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ pat_name.get(),
+ pat_name.size(),
+ SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_path") == 0) {
+ const auto& filename = lf.get_filename();
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ filename.c_str(),
+ filename.length(),
+ SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_unique_path") == 0) {
+ const auto& filename = lf.get_unique_path();
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ filename.c_str(),
+ filename.length(),
+ SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_text") == 0) {
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ values.lvv_sbr.get_data(),
+ values.lvv_sbr.length(),
+ SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_body") == 0) {
+ auto body_attr_opt = get_string_attr(sa, SA_BODY);
+ if (body_attr_opt) {
+ const auto& sar
+ = body_attr_opt.value().saw_string_attr->sa_range;
+
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ values.lvv_sbr.get_data_at(sar.lr_start),
+ sar.length(),
+ SQLITE_STATIC);
+ } else {
+ sqlite3_bind_null(stmt, lpc + 1);
+ }
+ continue;
+ }
+ if (strcmp(name, ":log_opid") == 0) {
+ auto opid_attr_opt = get_string_attr(sa, logline::L_OPID);
+ if (opid_attr_opt) {
+ const auto& sar
+ = opid_attr_opt.value().saw_string_attr->sa_range;
+
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ values.lvv_sbr.get_data_at(sar.lr_start),
+ sar.length(),
+ SQLITE_STATIC);
+ } else {
+ sqlite3_bind_null(stmt, lpc + 1);
+ }
+ continue;
+ }
+ if (strcmp(name, ":log_raw_text") == 0) {
+ auto res = lf.read_raw_message(ll);
+
+ if (res.isOk()) {
+ raw_sbr = res.unwrap();
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ raw_sbr.get_data(),
+ raw_sbr.length(),
+ SQLITE_STATIC);
+ }
+ continue;
+ }
+ if (strcmp(name, ":log_tags") == 0) {
+ const auto& bm = lf.get_bookmark_metadata();
+ auto bm_iter = bm.find(line_number);
+ if (bm_iter != bm.end() && !bm_iter->second.bm_tags.empty()) {
+ const auto& meta = bm_iter->second;
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_array arr(gen);
+
+ for (const auto& str : meta.bm_tags) {
+ arr.gen(str);
+ }
+ }
+
+ string_fragment sf = gen.to_string_fragment();
+
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ sf.data(),
+ sf.length(),
+ SQLITE_TRANSIENT);
+ }
+ continue;
+ }
+ auto found = false;
+ for (const auto& lv : values.lvv_values) {
+ if (lv.lv_meta.lvm_name != &name[1]) {
+ continue;
+ }
+
+ found = true;
+ switch (lv.lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_BOOLEAN:
+ sqlite3_bind_int64(stmt, lpc + 1, lv.lv_value.i);
+ break;
+ case value_kind_t::VALUE_FLOAT:
+ sqlite3_bind_double(stmt, lpc + 1, lv.lv_value.d);
+ break;
+ case value_kind_t::VALUE_INTEGER:
+ sqlite3_bind_int64(stmt, lpc + 1, lv.lv_value.i);
+ break;
+ case value_kind_t::VALUE_NULL:
+ sqlite3_bind_null(stmt, lpc + 1);
+ break;
+ default:
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ lv.text_value(),
+ lv.text_length(),
+ SQLITE_TRANSIENT);
+ break;
+ }
+ break;
+ }
+ if (!found) {
+ missing_column = true;
+ break;
+ }
+ }
+
+ if (missing_column) {
+ continue;
+ }
+
+ auto step_res = sqlite3_step(stmt);
+
+ switch (step_res) {
+ case SQLITE_OK:
+ case SQLITE_DONE:
+ continue;
+ case SQLITE_ROW:
+ break;
+ default: {
+ log_error("failed to execute watch expression: %s -- %s",
+ watch_pair.first.c_str(),
+ sqlite3_errmsg(lnav_db));
+ watch_pair.second.cwe_enabled = false;
+ continue;
+ }
+ }
+
+ if (!timestamp_buffer[0]) {
+ sql_strftime(timestamp_buffer,
+ sizeof(timestamp_buffer),
+ ll->get_timeval(),
+ 'T');
+ }
+ auto lmd = lnav::events::log::msg_detected{
+ watch_pair.first,
+ lf.get_filename(),
+ lf.get_format_name().to_string(),
+ (uint32_t) line_number,
+ timestamp_buffer,
+ };
+ for (const auto& lv : values.lvv_values) {
+ switch (lv.lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_NULL:
+ lmd.md_values[lv.lv_meta.lvm_name.to_string()]
+ = json_null_t{};
+ break;
+ case value_kind_t::VALUE_BOOLEAN:
+ lmd.md_values[lv.lv_meta.lvm_name.to_string()]
+ = lv.lv_value.i ? true : false;
+ break;
+ case value_kind_t::VALUE_INTEGER:
+ lmd.md_values[lv.lv_meta.lvm_name.to_string()]
+ = lv.lv_value.i;
+ break;
+ case value_kind_t::VALUE_FLOAT:
+ lmd.md_values[lv.lv_meta.lvm_name.to_string()]
+ = lv.lv_value.d;
+ break;
+ default:
+ lmd.md_values[lv.lv_meta.lvm_name.to_string()]
+ = lv.to_string();
+ break;
+ }
+ }
+ lnav::events::publish(lnav_db, lmd);
+ }
+}
+
+} // namespace watch
+} // namespace log
+} // namespace lnav
diff --git a/src/log.watch.hh b/src/log.watch.hh
new file mode 100644
index 0000000..044fe69
--- /dev/null
+++ b/src/log.watch.hh
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_log_watch_hh
+#define lnav_log_watch_hh
+
+#include "logfile.hh"
+
+namespace lnav {
+namespace log {
+namespace watch {
+
+void eval_with(logfile& lf, logfile::iterator ll);
+
+}
+} // namespace log
+} // namespace lnav
+
+#endif
diff --git a/src/log_accel.cc b/src/log_accel.cc
new file mode 100644
index 0000000..ae6bf55
--- /dev/null
+++ b/src/log_accel.cc
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_accel.cc
+ */
+
+#include <algorithm>
+
+#include "log_accel.hh"
+
+#include "config.h"
+
+const double log_accel::MIN_RANGE = 5.0;
+const double log_accel::THRESHOLD = 0.1;
+
+bool
+log_accel::add_point(int64_t point)
+{
+ require(this->la_velocity_size < HISTORY_SIZE);
+
+ if (this->la_last_point_set) {
+ // TODO Reenable this when we find the bug that causes some older
+ // timestamps to show up after more recent ones.
+ // require(this->la_last_point >= point);
+
+ // Compute the message velocity.
+ this->la_velocity[this->la_velocity_size]
+ = (this->la_last_point - point);
+
+ // Find the range of velocities so we can normalize.
+ this->la_min_velocity = std::min(
+ this->la_min_velocity, this->la_velocity[this->la_velocity_size]);
+ this->la_max_velocity = std::max(
+ this->la_max_velocity, this->la_velocity[this->la_velocity_size]);
+
+ this->la_velocity_size += 1;
+ }
+
+ this->la_last_point = point;
+ this->la_last_point_set = true;
+
+ return this->la_velocity_size < HISTORY_SIZE;
+}
+
+double
+log_accel::get_avg_accel() const
+{
+ double avg_accel = 0, total_accel = 0;
+ // Compute the range of values so we can normalize.
+ double range = (double) (this->la_max_velocity - this->la_min_velocity);
+
+ range = std::max(range, MIN_RANGE);
+ for (int lpc = 0; lpc < (this->la_velocity_size - 1); lpc++) {
+ double accel
+ = (this->la_velocity[lpc] - this->la_velocity[lpc + 1]) / range;
+ total_accel += accel;
+ }
+
+ if (this->la_velocity_size > 1) {
+ avg_accel = total_accel / (double) (this->la_velocity_size - 1);
+ }
+
+ return avg_accel;
+}
+
+log_accel::direction_t
+log_accel::get_direction() const
+{
+ double avg_accel = this->get_avg_accel();
+ direction_t retval;
+
+ if (std::fabs(avg_accel) <= THRESHOLD) {
+ retval = A_STEADY;
+ } else if (avg_accel < 0.0) {
+ retval = A_ACCEL;
+ } else {
+ retval = A_DECEL;
+ }
+
+ return retval;
+}
diff --git a/src/log_accel.hh b/src/log_accel.hh
new file mode 100644
index 0000000..0090946
--- /dev/null
+++ b/src/log_accel.hh
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or otherlist materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_accel.hh
+ */
+
+#ifndef log_accel_h
+#define log_accel_h
+
+#include <cmath>
+
+#include <stdint.h>
+
+#include "base/lnav_log.hh"
+
+/**
+ * Helper class for figuring out changes in the log message rate.
+ */
+class log_accel {
+public:
+ /*< The direction of the message rate: steady, accelerating, or decelerating
+ */
+ enum direction_t {
+ A_STEADY,
+ A_DECEL,
+ A_ACCEL,
+ };
+
+ /**
+ * Add a time point that will be used to compute velocity and then
+ * acceleration. Points should be added in reverse order, from most
+ * recent to oldest.
+ *
+ * @param point The point in time.
+ * @return True if more points can be added.
+ */
+ bool add_point(int64_t point);
+
+ /**
+ * Get the average acceleration based on the time points we've received.
+ *
+ * @return The average message acceleration.
+ */
+ double get_avg_accel() const;
+
+ /**
+ * Compute the message rate direction. If the average acceleration is less
+ * than a certain threshold, then we consider the rate to be steady.
+ * Otherwise, the message rate is increasing or decreasing.
+ *
+ * @return The direction of the message rate.
+ */
+ direction_t get_direction() const;
+
+private:
+ /**
+ * The amount of historical data to include in the average acceleration
+ * computation.
+ */
+ static const int HISTORY_SIZE = 8;
+ /**
+ * The minimum range of velocities seen. This value should limit false-
+ * positives for small millisecond level fluctuations.
+ */
+ static const double MIN_RANGE;
+ static const double THRESHOLD;
+
+ int64_t la_last_point{0};
+ bool la_last_point_set{false};
+ int64_t la_min_velocity{INT64_MAX};
+ int64_t la_max_velocity{INT64_MIN};
+ int64_t la_velocity[HISTORY_SIZE];
+ int la_velocity_size{0};
+};
+
+#endif
diff --git a/src/log_actions.cc b/src/log_actions.cc
new file mode 100644
index 0000000..6e47440
--- /dev/null
+++ b/src/log_actions.cc
@@ -0,0 +1,237 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "log_actions.hh"
+
+#include "base/fs_util.hh"
+#include "base/injector.hh"
+#include "bound_tags.hh"
+#include "config.h"
+#include "piper_proc.hh"
+
+std::string
+action_delegate::execute_action(const std::string& action_name)
+{
+ static auto& lnav_flags = injector::get<unsigned long&, lnav_flags_tag>();
+
+ if (lnav_flags & LNF_SECURE_MODE) {
+ return "unavailable in secure mode";
+ }
+
+ auto& ldh = this->ad_log_helper;
+ auto value_index = this->ad_press_value;
+ logline_value& lv = ldh.ldh_line_values.lvv_values[value_index];
+ auto lf = ldh.ldh_file;
+ const auto format = lf->get_format();
+ pid_t child_pid;
+ std::string retval;
+
+ auto iter = format->lf_action_defs.find(action_name);
+
+ const log_format::action_def& action = iter->second;
+
+ auto_pipe in_pipe(STDIN_FILENO);
+ auto_pipe out_pipe(STDOUT_FILENO);
+
+ in_pipe.open();
+ if (action.ad_capture_output) {
+ out_pipe.open();
+ }
+
+ child_pid = fork();
+
+ in_pipe.after_fork(child_pid);
+ out_pipe.after_fork(child_pid);
+
+ switch (child_pid) {
+ case -1:
+ retval = fmt::format(
+ FMT_STRING("error: unable to fork child process -- {}"),
+ strerror(errno));
+ break;
+ case 0: {
+ const char* args[action.ad_cmdline.size() + 1];
+ std::set<std::string> path_set(format->get_source_path());
+ char env_buffer[64];
+ int value_line;
+ std::string path;
+
+ dup2(STDOUT_FILENO, STDERR_FILENO);
+ setenv("LNAV_ACTION_FILE", lf->get_filename().c_str(), 1);
+ snprintf(env_buffer,
+ sizeof(env_buffer),
+ "%ld",
+ (ldh.ldh_line - lf->begin()) + 1);
+ setenv("LNAV_ACTION_FILE_LINE", env_buffer, 1);
+ snprintf(
+ env_buffer, sizeof(env_buffer), "%d", ldh.ldh_y_offset + 1);
+ setenv("LNAV_ACTION_MSG_LINE", env_buffer, 1);
+ setenv("LNAV_ACTION_VALUE_NAME", lv.lv_meta.lvm_name.get(), 1);
+ value_line = ldh.ldh_y_offset - ldh.get_value_line(lv) + 1;
+ snprintf(env_buffer, sizeof(env_buffer), "%d", value_line);
+ setenv("LNAV_ACTION_VALUE_LINE", env_buffer, 1);
+
+ for (const auto& path_iter : path_set) {
+ if (!path.empty()) {
+ path += ":";
+ }
+ path += path_iter;
+ }
+ path += ":" + std::string(getenv("PATH"));
+ setenv("PATH", path.c_str(), 1);
+ for (size_t lpc = 0; lpc < action.ad_cmdline.size(); lpc++) {
+ args[lpc] = action.ad_cmdline[lpc].c_str();
+ }
+ args[action.ad_cmdline.size()] = nullptr;
+ execvp(args[0], (char* const*) args);
+ fprintf(stderr,
+ "error: could not exec process -- %s:%s\n",
+ args[0],
+ strerror(errno));
+ _exit(0);
+ } break;
+ default: {
+ static int exec_count = 0;
+
+ const auto value = lv.to_string();
+
+ this->ad_child_cb(child_pid);
+
+ if (write(in_pipe.write_end(), value.c_str(), value.size()) == -1) {
+ perror("execute_action write");
+ }
+ in_pipe.close();
+
+ if (out_pipe.read_end() != -1) {
+ auto pp = std::make_shared<piper_proc>(
+ std::move(out_pipe.read_end()),
+ false,
+ lnav::filesystem::open_temp_file(
+ ghc::filesystem::temp_directory_path()
+ / "lnav.action.XXXXXX")
+ .map([](auto pair) {
+ ghc::filesystem::remove(pair.first);
+
+ return pair;
+ })
+ .expect("Cannot create temporary file for action")
+ .second);
+ auto desc = fmt::format(FMT_STRING("[{}] Output of {}"),
+ exec_count++,
+ action.ad_cmdline[0]);
+
+ this->ad_piper_cb(desc, pp);
+ }
+
+ return "";
+ } break;
+ }
+
+ return retval;
+}
+
+bool
+action_delegate::text_handle_mouse(textview_curses& tc, mouse_event& me)
+{
+ bool retval = false;
+
+ if (me.me_button != mouse_button_t::BUTTON_LEFT) {
+ return false;
+ }
+
+ vis_line_t mouse_line = vis_line_t(tc.get_top() + me.me_y);
+ int mouse_left = tc.get_left() + me.me_x;
+
+ switch (me.me_state) {
+ case mouse_button_state_t::BUTTON_STATE_PRESSED:
+ if (mouse_line >= 0_vl && mouse_line <= tc.get_bottom()) {
+ size_t line_end_index = 0;
+ int x_offset;
+
+ this->ad_press_line = mouse_line;
+ this->ad_log_helper.parse_line(mouse_line, true);
+
+ this->ad_log_helper.get_line_bounds(this->ad_line_index,
+ line_end_index);
+
+ struct line_range lr(this->ad_line_index, line_end_index);
+
+ this->ad_press_value = -1;
+
+ x_offset = this->ad_line_index + mouse_left;
+ if (lr.contains(x_offset)) {
+ for (size_t lpc = 0;
+ lpc < this->ad_log_helper.ldh_line_values.lvv_values
+ .size();
+ lpc++)
+ {
+ auto& lv = this->ad_log_helper.ldh_line_values
+ .lvv_values[lpc];
+
+ if (lv.lv_origin.contains(x_offset)) {
+ this->ad_press_value = lpc;
+ break;
+ }
+ }
+ }
+ }
+ break;
+ case mouse_button_state_t::BUTTON_STATE_DRAGGED:
+ if (mouse_line != this->ad_press_line) {
+ this->ad_press_value = -1;
+ }
+ if (this->ad_press_value != -1) {
+ retval = true;
+ }
+ break;
+ case mouse_button_state_t::BUTTON_STATE_RELEASED:
+ if (this->ad_press_value != -1 && this->ad_press_line == mouse_line)
+ {
+ auto& lv = this->ad_log_helper.ldh_line_values
+ .lvv_values[this->ad_press_value];
+ int x_offset = this->ad_line_index + mouse_left;
+
+ if (lv.lv_origin.contains(x_offset)) {
+ auto lf = this->ad_log_helper.ldh_file;
+ const std::vector<std::string>* actions;
+
+ actions = lf->get_format()->get_actions(lv);
+ if (actions != nullptr && !actions->empty()) {
+ const auto rc = execute_action(actions->at(0));
+
+ // lnav_data.ld_rl_view->set_value(rc);
+ }
+ }
+ retval = true;
+ }
+ break;
+ }
+
+ return retval;
+}
diff --git a/src/log_actions.hh b/src/log_actions.hh
new file mode 100644
index 0000000..02dc476
--- /dev/null
+++ b/src/log_actions.hh
@@ -0,0 +1,67 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef log_actions_hh
+#define log_actions_hh
+
+#include <functional>
+#include <utility>
+
+#include "log_data_helper.hh"
+#include "logfile_sub_source.hh"
+
+class piper_proc;
+
+class action_delegate : public text_delegate {
+public:
+ explicit action_delegate(
+ logfile_sub_source& lss,
+ std::function<void(pid_t)> child_cb,
+ std::function<void(const std::string&, std::shared_ptr<piper_proc>)>
+ piper_cb)
+ : ad_log_helper(lss), ad_child_cb(std::move(child_cb)),
+ ad_piper_cb(std::move(piper_cb))
+ {
+ }
+
+ bool text_handle_mouse(textview_curses& tc, mouse_event& me) override;
+
+private:
+ std::string execute_action(const std::string& action_name);
+
+ log_data_helper ad_log_helper;
+ std::function<void(pid_t)> ad_child_cb;
+ std::function<void(const std::string&, std::shared_ptr<piper_proc>)>
+ ad_piper_cb;
+ vis_line_t ad_press_line{-1};
+ int ad_press_value{-1};
+ size_t ad_line_index{0};
+};
+
+#endif
diff --git a/src/log_data_helper.cc b/src/log_data_helper.cc
new file mode 100644
index 0000000..ade077a
--- /dev/null
+++ b/src/log_data_helper.cc
@@ -0,0 +1,214 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_data_helper.cc
+ */
+
+#include "log_data_helper.hh"
+
+#include "config.h"
+
+void
+log_data_helper::clear()
+{
+ this->ldh_file = nullptr;
+ this->ldh_line_values.lvv_sbr.disown();
+ this->ldh_parser.reset();
+ this->ldh_scanner.reset();
+ this->ldh_namer.reset();
+ this->ldh_json_pairs.clear();
+ this->ldh_xml_pairs.clear();
+ this->ldh_line_attrs.clear();
+}
+
+bool
+log_data_helper::parse_line(content_line_t line, bool allow_middle)
+{
+ logfile::iterator ll;
+ bool retval = false;
+
+ this->ldh_source_line = this->ldh_line_index = line;
+
+ this->ldh_file = this->ldh_log_source.find(this->ldh_line_index);
+ ll = this->ldh_file->begin() + this->ldh_line_index;
+ this->ldh_y_offset = 0;
+ while (allow_middle && ll->is_continued()) {
+ --ll;
+ this->ldh_y_offset += 1;
+ }
+ this->ldh_line = ll;
+ if (!ll->is_message()) {
+ this->ldh_parser.reset();
+ this->ldh_scanner.reset();
+ this->ldh_namer.reset();
+ this->ldh_json_pairs.clear();
+ this->ldh_xml_pairs.clear();
+ this->ldh_line_attrs.clear();
+ } else {
+ auto format = this->ldh_file->get_format();
+ struct line_range body;
+ auto& sa = this->ldh_line_attrs;
+
+ this->ldh_line_attrs.clear();
+ this->ldh_line_values.clear();
+ this->ldh_file->read_full_message(ll, this->ldh_line_values.lvv_sbr);
+ this->ldh_line_values.lvv_sbr.erase_ansi();
+ format->annotate(this->ldh_line_index, sa, this->ldh_line_values);
+
+ body = find_string_attr_range(sa, &SA_BODY);
+ if (body.lr_start == -1) {
+ body.lr_start = this->ldh_line_values.lvv_sbr.length();
+ body.lr_end = this->ldh_line_values.lvv_sbr.length();
+ }
+ this->ldh_scanner = std::make_unique<data_scanner>(
+ this->ldh_line_values.lvv_sbr.to_string_fragment().sub_range(
+ body.lr_start, body.lr_end));
+ this->ldh_parser
+ = std::make_unique<data_parser>(this->ldh_scanner.get());
+ this->ldh_msg_format.clear();
+ this->ldh_parser->dp_msg_format = &this->ldh_msg_format;
+ this->ldh_parser->parse();
+ this->ldh_namer
+ = std::make_unique<column_namer>(column_namer::language::SQL);
+ this->ldh_json_pairs.clear();
+ this->ldh_xml_pairs.clear();
+
+ for (const auto& lv : this->ldh_line_values.lvv_values) {
+ this->ldh_namer->cn_builtin_names.emplace_back(
+ lv.lv_meta.lvm_name.get());
+ }
+
+ for (auto& ldh_line_value : this->ldh_line_values.lvv_values) {
+ switch (ldh_line_value.lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_JSON: {
+ json_ptr_walk jpw;
+
+ if (jpw.parse(ldh_line_value.text_value(),
+ ldh_line_value.text_length())
+ == yajl_status_ok
+ && jpw.complete_parse() == yajl_status_ok)
+ {
+ this->ldh_json_pairs[ldh_line_value.lv_meta.lvm_name]
+ = jpw.jpw_values;
+ }
+ break;
+ }
+ case value_kind_t::VALUE_XML: {
+ auto col_name = ldh_line_value.lv_meta.lvm_name;
+ pugi::xml_document doc;
+
+ auto parse_res
+ = doc.load_buffer(ldh_line_value.text_value(),
+ ldh_line_value.text_length());
+
+ if (parse_res) {
+ pugi::xpath_query query("//*");
+ auto node_set = doc.select_nodes(query);
+
+ for (const auto& xpath_node : node_set) {
+ auto node_path = lnav::pugixml::get_actual_path(
+ xpath_node.node());
+ for (auto& attr : xpath_node.node().attributes()) {
+ auto attr_path
+ = fmt::format(FMT_STRING("{}/@{}"),
+ node_path,
+ attr.name());
+
+ this->ldh_xml_pairs[std::make_pair(col_name,
+ attr_path)]
+ = attr.value();
+ }
+
+ if (xpath_node.node().text().empty()) {
+ continue;
+ }
+
+ auto text_path = fmt::format(
+ FMT_STRING("{}/text()"), node_path);
+ this->ldh_xml_pairs[std::make_pair(col_name,
+ text_path)]
+ = trim(xpath_node.node().text().get());
+ }
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ retval = true;
+ }
+
+ return retval;
+}
+
+int
+log_data_helper::get_line_bounds(size_t& line_index_out,
+ size_t& line_end_index_out) const
+{
+ int retval = 0;
+
+ line_end_index_out = 0;
+ do {
+ line_index_out = line_end_index_out;
+ const auto* line_end = (const char*) memchr(
+ this->ldh_line_values.lvv_sbr.get_data() + line_index_out + 1,
+ '\n',
+ this->ldh_line_values.lvv_sbr.length() - line_index_out - 1);
+ if (line_end != nullptr) {
+ line_end_index_out
+ = line_end - this->ldh_line_values.lvv_sbr.get_data();
+ } else {
+ line_end_index_out = std::string::npos;
+ }
+ retval += 1;
+ } while (retval <= this->ldh_y_offset);
+
+ if (line_end_index_out == std::string::npos) {
+ line_end_index_out = this->ldh_line_values.lvv_sbr.length();
+ }
+
+ return retval;
+}
+
+std::string
+log_data_helper::format_json_getter(const intern_string_t field, int index)
+{
+ auto_mem<char, sqlite3_free> qname;
+ auto_mem<char, sqlite3_free> jget;
+ std::string retval;
+
+ qname = sql_quote_ident(field.get());
+ jget = sqlite3_mprintf("jget(%s,%Q)",
+ qname.in(),
+ this->ldh_json_pairs[field][index].wt_ptr.c_str());
+ retval = std::string(jget);
+
+ return retval;
+}
diff --git a/src/log_data_helper.hh b/src/log_data_helper.hh
new file mode 100644
index 0000000..235a4d8
--- /dev/null
+++ b/src/log_data_helper.hh
@@ -0,0 +1,92 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_data_helper.hh
+ */
+
+#ifndef log_data_helper_hh
+#define log_data_helper_hh
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include <sqlite3.h>
+
+#include "base/lnav_log.hh"
+#include "column_namer.hh"
+#include "data_parser.hh"
+#include "logfile_sub_source.hh"
+#include "sql_util.hh"
+#include "xml_util.hh"
+#include "yajlpp/json_ptr.hh"
+
+class log_data_helper {
+public:
+ explicit log_data_helper(logfile_sub_source& lss) : ldh_log_source(lss) {}
+
+ void clear();
+
+ bool parse_line(vis_line_t line, bool allow_middle = false)
+ {
+ return this->parse_line(this->ldh_log_source.at(line), allow_middle);
+ }
+
+ bool parse_line(content_line_t line, bool allow_middle = false);
+
+ int get_line_bounds(size_t& line_index_out,
+ size_t& line_end_index_out) const;
+
+ int get_value_line(const logline_value& lv) const
+ {
+ return std::count(
+ this->ldh_line_values.lvv_sbr.get_data(),
+ this->ldh_line_values.lvv_sbr.get_data() + lv.lv_origin.lr_start,
+ '\n');
+ }
+
+ std::string format_json_getter(const intern_string_t field, int index);
+
+ logfile_sub_source& ldh_log_source;
+ content_line_t ldh_source_line;
+ std::shared_ptr<logfile> ldh_file;
+ int ldh_y_offset{0};
+ logfile::iterator ldh_line;
+ content_line_t ldh_line_index;
+ std::unique_ptr<data_scanner> ldh_scanner;
+ std::unique_ptr<data_parser> ldh_parser;
+ std::unique_ptr<column_namer> ldh_namer;
+ string_attrs_t ldh_line_attrs;
+ logline_value_vector ldh_line_values;
+ std::map<const intern_string_t, json_ptr_walk::walk_list_t> ldh_json_pairs;
+ std::map<std::pair<const intern_string_t, std::string>, std::string>
+ ldh_xml_pairs;
+ std::string ldh_msg_format;
+};
+
+#endif
diff --git a/src/log_data_table.cc b/src/log_data_table.cc
new file mode 100644
index 0000000..384d7ce
--- /dev/null
+++ b/src/log_data_table.cc
@@ -0,0 +1,199 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "log_data_table.hh"
+
+#include "config.h"
+#include "scn/scn.h"
+
+log_data_table::log_data_table(logfile_sub_source& lss,
+ log_vtab_manager& lvm,
+ content_line_t template_line,
+ intern_string_t table_name)
+ : log_vtab_impl(table_name), ldt_log_source(lss),
+ ldt_template_line(template_line)
+{
+ auto lf = lss.find(template_line);
+ auto format = lf->get_format();
+
+ this->vi_supports_indexes = false;
+ this->ldt_format_impl = lvm.lookup_impl(format->get_name());
+ this->get_columns_int();
+}
+
+void
+log_data_table::get_columns_int()
+{
+ auto& cols = this->ldt_cols;
+ auto& metas = this->ldt_value_metas;
+ content_line_t cl_copy = this->ldt_template_line;
+ auto lf = this->ldt_log_source.find(cl_copy);
+ struct line_range body;
+ string_attrs_t sa;
+ logline_value_vector line_values;
+ auto format = lf->get_format();
+
+ if (this->ldt_format_impl != nullptr) {
+ this->ldt_format_impl->get_columns(cols);
+ }
+ lf->read_full_message(lf->begin() + cl_copy, line_values.lvv_sbr);
+ line_values.lvv_sbr.erase_ansi();
+ format->annotate(cl_copy, sa, line_values, false);
+ body = find_string_attr_range(sa, &SA_BODY);
+ if (body.lr_end == -1) {
+ this->ldt_schema_id.clear();
+ return;
+ }
+
+ data_scanner ds(line_values.lvv_sbr, body.lr_start, body.lr_end);
+ data_parser dp(&ds);
+ column_namer cn{column_namer::language::SQL};
+
+ dp.parse();
+
+ for (auto pair_iter = dp.dp_pairs.begin(); pair_iter != dp.dp_pairs.end();
+ ++pair_iter)
+ {
+ std::string key_str
+ = dp.get_element_string(pair_iter->e_sub_elements->front());
+ auto colname = cn.add_column(key_str).to_string();
+ int sql_type = SQLITE3_TEXT;
+ value_kind_t kind = value_kind_t::VALUE_TEXT;
+ std::string collator;
+
+ switch (pair_iter->e_sub_elements->back().value_token()) {
+ case DT_IPV4_ADDRESS:
+ case DT_IPV6_ADDRESS:
+ collator = "ipaddress";
+ break;
+
+ case DT_NUMBER:
+ sql_type = SQLITE_FLOAT;
+ kind = value_kind_t::VALUE_FLOAT;
+ break;
+
+ default:
+ collator = "naturalnocase";
+ break;
+ }
+ metas.emplace_back(
+ intern_string::lookup(colname), kind, cols.size(), format.get());
+ cols.emplace_back(colname, sql_type, collator);
+ }
+ this->ldt_schema_id = dp.dp_schema_id;
+}
+
+bool
+log_data_table::next(log_cursor& lc, logfile_sub_source& lss)
+{
+ if (lc.is_eof()) {
+ return true;
+ }
+
+ content_line_t cl;
+
+ cl = lss.at(lc.lc_curr_line);
+ auto* lf = lss.find_file_ptr(cl);
+ if (lf->get_format()->get_name() != this->ldt_format_impl->get_name()) {
+ return false;
+ }
+ auto lf_iter = lf->begin() + cl;
+
+ if (!lf_iter->is_message()) {
+ return false;
+ }
+
+ if (lf_iter->has_schema() && !lf_iter->match_schema(this->ldt_schema_id)) {
+ return false;
+ }
+
+ string_attrs_t sa;
+ struct line_range body;
+ logline_value_vector line_values;
+
+ lf->read_full_message(lf_iter, line_values.lvv_sbr);
+ line_values.lvv_sbr.erase_ansi();
+ lf->get_format()->annotate(cl, sa, line_values, false);
+ body = find_string_attr_range(sa, &SA_BODY);
+ if (body.lr_end == -1) {
+ return false;
+ }
+
+ data_scanner ds(line_values.lvv_sbr, body.lr_start, body.lr_end);
+ data_parser dp(&ds);
+ dp.parse();
+
+ lf_iter->set_schema(dp.dp_schema_id);
+
+ /* The cached schema ID in the log line is not complete, so we still */
+ /* need to check for a full match. */
+ if (dp.dp_schema_id != this->ldt_schema_id) {
+ return false;
+ }
+
+ this->ldt_pairs.clear();
+ this->ldt_pairs.swap(dp.dp_pairs, __FILE__, __LINE__);
+
+ return true;
+}
+
+void
+log_data_table::extract(logfile* lf,
+ uint64_t line_number,
+ logline_value_vector& values)
+{
+ auto& line = values.lvv_sbr;
+ auto meta_iter = this->ldt_value_metas.begin();
+
+ this->ldt_format_impl->extract(lf, line_number, values);
+ for (const auto& ldt_pair : this->ldt_pairs) {
+ const auto& pvalue = ldt_pair.get_pair_value();
+ auto lr = line_range{
+ pvalue.e_capture.c_begin,
+ pvalue.e_capture.c_end,
+ };
+
+ switch (pvalue.value_token()) {
+ case DT_NUMBER: {
+ auto num_view = line.to_string_view(lr);
+ auto num_scan_res = scn::scan_value<double>(num_view);
+ auto num = num_scan_res ? num_scan_res.value() : 0.0;
+
+ values.lvv_values.emplace_back(*meta_iter, num);
+ break;
+ }
+
+ default: {
+ values.lvv_values.emplace_back(*meta_iter, line, lr);
+ break;
+ }
+ }
+ ++meta_iter;
+ }
+}
diff --git a/src/log_data_table.hh b/src/log_data_table.hh
new file mode 100644
index 0000000..1ca3942
--- /dev/null
+++ b/src/log_data_table.hh
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_data_table.hh
+ */
+
+#ifndef lnav_log_data_table_hh
+#define lnav_log_data_table_hh
+
+#include <string>
+#include <vector>
+
+#include "column_namer.hh"
+#include "data_parser.hh"
+#include "log_vtab_impl.hh"
+#include "logfile.hh"
+#include "logfile_sub_source.hh"
+
+class log_data_table : public log_vtab_impl {
+public:
+ log_data_table(logfile_sub_source& lss,
+ log_vtab_manager& lvm,
+ content_line_t template_line,
+ intern_string_t table_name);
+
+ void get_columns_int();
+
+ void get_columns(std::vector<vtab_column>& cols) const override
+ {
+ cols = this->ldt_cols;
+ }
+
+ void get_foreign_keys(std::vector<std::string>& keys_inout) const override
+ {
+ log_vtab_impl::get_foreign_keys(keys_inout);
+ }
+
+ bool next(log_cursor& lc, logfile_sub_source& lss) override;
+
+ void extract(logfile* lf,
+ uint64_t line_number,
+ logline_value_vector& values) override;
+
+private:
+ logfile_sub_source& ldt_log_source;
+ const content_line_t ldt_template_line;
+ data_parser::schema_id_t ldt_schema_id;
+ data_parser::element_list_t ldt_pairs;
+ std::shared_ptr<log_vtab_impl> ldt_format_impl;
+ std::vector<vtab_column> ldt_cols;
+ std::vector<logline_value_meta> ldt_value_metas;
+};
+
+#endif
diff --git a/src/log_format.cc b/src/log_format.cc
new file mode 100644
index 0000000..dd18f60
--- /dev/null
+++ b/src/log_format.cc
@@ -0,0 +1,3336 @@
+/**
+ * Copyright (c) 2007-2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <memory>
+
+#include <fnmatch.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "base/is_utf8.hh"
+#include "base/snippet_highlighters.hh"
+#include "base/string_util.hh"
+#include "command_executor.hh"
+#include "config.h"
+#include "fmt/format.h"
+#include "lnav_util.hh"
+#include "log_format_ext.hh"
+#include "log_search_table.hh"
+#include "log_vtab_impl.hh"
+#include "ptimec.hh"
+#include "scn/scn.h"
+#include "sql_util.hh"
+#include "yajlpp/yajlpp.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+using namespace lnav::roles::literals;
+
+static auto intern_lifetime = intern_string::get_table_lifetime();
+
+string_attr_type<void> logline::L_PREFIX("prefix");
+string_attr_type<void> logline::L_TIMESTAMP("timestamp");
+string_attr_type<std::shared_ptr<logfile>> logline::L_FILE("file");
+string_attr_type<bookmark_metadata*> logline::L_PARTITION("partition");
+string_attr_type<void> logline::L_MODULE("module");
+string_attr_type<void> logline::L_OPID("opid");
+string_attr_type<bookmark_metadata*> logline::L_META("meta");
+
+external_log_format::mod_map_t external_log_format::MODULE_FORMATS;
+std::vector<std::shared_ptr<external_log_format>>
+ external_log_format::GRAPH_ORDERED_FORMATS;
+
+struct line_range
+logline_value::origin_in_full_msg(const char* msg, ssize_t len) const
+{
+ if (this->lv_sub_offset == 0) {
+ return this->lv_origin;
+ }
+
+ if (len == -1) {
+ len = strlen(msg);
+ }
+
+ struct line_range retval = this->lv_origin;
+ const char *last = msg, *msg_end = msg + len;
+
+ for (int lpc = 0; lpc < this->lv_sub_offset; lpc++) {
+ const auto* next = (const char*) memchr(last, '\n', msg_end - last);
+ require(next != nullptr);
+
+ next += 1;
+ int amount = (next - last);
+
+ retval.lr_start += amount;
+ if (retval.lr_end != -1) {
+ retval.lr_end += amount;
+ }
+
+ last = next + 1;
+ }
+
+ if (retval.lr_end == -1) {
+ const auto* eol = (const char*) memchr(last, '\n', msg_end - last);
+
+ if (eol == nullptr) {
+ retval.lr_end = len;
+ } else {
+ retval.lr_end = eol - msg;
+ }
+ }
+
+ return retval;
+}
+
+logline_value::logline_value(logline_value_meta lvm,
+ shared_buffer_ref& sbr,
+ struct line_range origin)
+ : lv_meta(std::move(lvm)), lv_origin(origin)
+{
+ if (sbr.get_data() == nullptr) {
+ this->lv_meta.lvm_kind = value_kind_t::VALUE_NULL;
+ }
+
+ switch (this->lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_JSON:
+ case value_kind_t::VALUE_XML:
+ case value_kind_t::VALUE_STRUCT:
+ case value_kind_t::VALUE_TEXT:
+ case value_kind_t::VALUE_QUOTED:
+ case value_kind_t::VALUE_W3C_QUOTED:
+ case value_kind_t::VALUE_TIMESTAMP:
+ require(origin.lr_end != -1);
+ this->lv_frag = string_fragment::from_byte_range(
+ sbr.get_data(), origin.lr_start, origin.lr_end);
+ break;
+
+ case value_kind_t::VALUE_NULL:
+ break;
+
+ case value_kind_t::VALUE_INTEGER: {
+ auto scan_res
+ = scn::scan_value<int64_t>(sbr.to_string_view(origin));
+ if (scan_res) {
+ this->lv_value.i = scan_res.value();
+ } else {
+ this->lv_value.i = 0;
+ }
+ break;
+ }
+
+ case value_kind_t::VALUE_FLOAT: {
+ auto scan_res = scn::scan_value<double>(sbr.to_string_view(origin));
+ if (scan_res) {
+ this->lv_value.d = scan_res.value();
+ } else {
+ this->lv_value.d = 0;
+ }
+ break;
+ }
+
+ case value_kind_t::VALUE_BOOLEAN:
+ if (strncmp(
+ sbr.get_data_at(origin.lr_start), "true", origin.length())
+ == 0
+ || strncmp(
+ sbr.get_data_at(origin.lr_start), "yes", origin.length())
+ == 0)
+ {
+ this->lv_value.i = 1;
+ } else {
+ this->lv_value.i = 0;
+ }
+ break;
+
+ case value_kind_t::VALUE_UNKNOWN:
+ case value_kind_t::VALUE__MAX:
+ ensure(0);
+ break;
+ }
+}
+
+std::string
+logline_value::to_string() const
+{
+ char buffer[128];
+
+ switch (this->lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_NULL:
+ return "null";
+
+ case value_kind_t::VALUE_JSON:
+ case value_kind_t::VALUE_XML:
+ case value_kind_t::VALUE_STRUCT:
+ case value_kind_t::VALUE_TEXT:
+ case value_kind_t::VALUE_TIMESTAMP:
+ if (this->lv_str) {
+ return this->lv_str.value();
+ }
+ if (this->lv_frag.empty()) {
+ return this->lv_intern_string.to_string();
+ }
+ return this->lv_frag.to_string();
+
+ case value_kind_t::VALUE_QUOTED:
+ case value_kind_t::VALUE_W3C_QUOTED:
+ if (this->lv_frag.empty()) {
+ return "";
+ } else {
+ switch (this->lv_frag.data()[0]) {
+ case '\'':
+ case '"': {
+ auto unquote_func = this->lv_meta.lvm_kind
+ == value_kind_t::VALUE_W3C_QUOTED
+ ? unquote_w3c
+ : unquote;
+ char unquoted_str[this->lv_frag.length()];
+ size_t unquoted_len;
+
+ unquoted_len = unquote_func(unquoted_str,
+ this->lv_frag.data(),
+ this->lv_frag.length());
+ return {unquoted_str, unquoted_len};
+ }
+ default:
+ return this->lv_frag.to_string();
+ }
+ }
+ break;
+
+ case value_kind_t::VALUE_INTEGER:
+ snprintf(buffer, sizeof(buffer), "%" PRId64, this->lv_value.i);
+ break;
+
+ case value_kind_t::VALUE_FLOAT:
+ snprintf(buffer, sizeof(buffer), "%lf", this->lv_value.d);
+ break;
+
+ case value_kind_t::VALUE_BOOLEAN:
+ if (this->lv_value.i) {
+ return "true";
+ } else {
+ return "false";
+ }
+ break;
+ case value_kind_t::VALUE_UNKNOWN:
+ case value_kind_t::VALUE__MAX:
+ ensure(0);
+ break;
+ }
+
+ return {buffer};
+}
+
+std::vector<std::shared_ptr<log_format>> log_format::lf_root_formats;
+
+std::vector<std::shared_ptr<log_format>>&
+log_format::get_root_formats()
+{
+ return lf_root_formats;
+}
+
+static bool
+next_format(
+ const std::vector<std::shared_ptr<external_log_format::pattern>>& patterns,
+ int& index,
+ int& locked_index)
+{
+ bool retval = true;
+
+ if (locked_index == -1) {
+ index += 1;
+ if (index >= (int) patterns.size()) {
+ retval = false;
+ }
+ } else if (index == locked_index) {
+ retval = false;
+ } else {
+ index = locked_index;
+ }
+
+ return retval;
+}
+
+bool
+log_format::next_format(pcre_format* fmt, int& index, int& locked_index)
+{
+ bool retval = true;
+
+ if (locked_index == -1) {
+ index += 1;
+ if (fmt[index].name == nullptr) {
+ retval = false;
+ }
+ } else if (index == locked_index) {
+ retval = false;
+ } else {
+ index = locked_index;
+ }
+
+ return retval;
+}
+
+const char*
+log_format::log_scanf(uint32_t line_number,
+ string_fragment line,
+ pcre_format* fmt,
+ const char* time_fmt[],
+ struct exttm* tm_out,
+ struct timeval* tv_out,
+
+ string_fragment* ts_out,
+ nonstd::optional<string_fragment>* level_out)
+{
+ int curr_fmt = -1;
+ const char* retval = nullptr;
+ bool done = false;
+ int pat_index = this->last_pattern_index();
+
+ while (!done && next_format(fmt, curr_fmt, pat_index)) {
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+
+ auto match_res = fmt[curr_fmt]
+ .pcre->capture_from(line)
+ .into(md)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (!match_res) {
+ retval = nullptr;
+ } else {
+ auto ts = md[fmt[curr_fmt].pf_timestamp_index];
+
+ retval = this->lf_date_time.scan(
+ ts->data(), ts->length(), nullptr, tm_out, *tv_out);
+
+ if (retval) {
+ *ts_out = ts.value();
+ *level_out = md[2];
+ if (curr_fmt != pat_index) {
+ uint32_t lock_line;
+
+ if (this->lf_pattern_locks.empty()) {
+ lock_line = 0;
+ } else {
+ lock_line = line_number;
+ }
+
+ this->lf_pattern_locks.emplace_back(lock_line, curr_fmt);
+ }
+ this->lf_timestamp_flags = tm_out->et_flags;
+ done = true;
+ }
+ }
+ }
+
+ return retval;
+}
+
+void
+log_format::check_for_new_year(std::vector<logline>& dst,
+ exttm etm,
+ struct timeval log_tv)
+{
+ if (dst.empty()) {
+ return;
+ }
+
+ time_t diff = dst.back().get_time() - log_tv.tv_sec;
+ int off_year = 0, off_month = 0, off_day = 0, off_hour = 0;
+ bool do_change = true;
+
+ if (diff <= 0) {
+ return;
+ }
+ if ((etm.et_flags & ETF_MONTH_SET) && diff >= (24 * 60 * 60)) {
+ off_year = 1;
+ } else if (diff >= (24 * 60 * 60)) {
+ off_month = 1;
+ } else if (!(etm.et_flags & ETF_DAY_SET) && (diff >= (60 * 60))) {
+ off_day = 1;
+ } else if (!(etm.et_flags & ETF_DAY_SET)) {
+ off_hour = 1;
+ } else {
+ do_change = false;
+ }
+
+ if (!do_change) {
+ return;
+ }
+ log_debug("%d:detected time rollover; offsets=%d %d %d %d",
+ dst.size(),
+ off_year,
+ off_month,
+ off_day,
+ off_hour);
+ for (auto& ll : dst) {
+ time_t ot = ll.get_time();
+ struct tm otm;
+
+ gmtime_r(&ot, &otm);
+ if (otm.tm_year < off_year) {
+ otm.tm_year = 0;
+ } else {
+ otm.tm_year -= off_year;
+ }
+ otm.tm_mon -= off_month;
+ if (otm.tm_mon < 0) {
+ otm.tm_mon += 12;
+ }
+ auto new_time = tm2sec(&otm);
+ if (new_time == -1) {
+ continue;
+ }
+ new_time -= (off_day * 24 * 60 * 60) + (off_hour * 60 * 60);
+ ll.set_time(new_time);
+ }
+}
+
+/*
+ * XXX This needs some cleanup.
+ */
+struct json_log_userdata {
+ json_log_userdata(shared_buffer_ref& sbr, scan_batch_context* sbc)
+ : jlu_shared_buffer(sbr), jlu_batch_context(sbc)
+ {
+ }
+
+ void add_sub_lines_for(const intern_string_t ist,
+ bool top_level,
+ nonstd::optional<double> val = nonstd::nullopt,
+ const unsigned char* str = nullptr,
+ ssize_t len = -1)
+ {
+ auto res
+ = this->jlu_format->value_line_count(ist, top_level, val, str, len);
+ this->jlu_has_ansi |= res.vlcr_has_ansi;
+ if (!res.vlcr_valid_utf) {
+ this->jlu_valid_utf = false;
+ }
+ this->jlu_sub_line_count += res.vlcr_count;
+ this->jlu_quality += res.vlcr_line_format_count;
+ }
+
+ external_log_format* jlu_format{nullptr};
+ const logline* jlu_line{nullptr};
+ logline* jlu_base_line{nullptr};
+ int jlu_sub_line_count{1};
+ bool jlu_has_ansi{false};
+ bool jlu_valid_utf{true};
+ yajl_handle jlu_handle{nullptr};
+ const char* jlu_line_value{nullptr};
+ size_t jlu_line_size{0};
+ size_t jlu_sub_start{0};
+ uint32_t jlu_quality{0};
+ shared_buffer_ref& jlu_shared_buffer;
+ scan_batch_context* jlu_batch_context;
+};
+
+static int read_json_field(yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len);
+
+static int
+read_json_null(yajlpp_parse_context* ypc)
+{
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+ const intern_string_t field_name = ypc->get_path();
+
+ jlu->add_sub_lines_for(field_name, ypc->is_level(1));
+
+ return 1;
+}
+
+static int
+read_json_bool(yajlpp_parse_context* ypc, int val)
+{
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+ const intern_string_t field_name = ypc->get_path();
+
+ jlu->add_sub_lines_for(field_name, ypc->is_level(1));
+
+ return 1;
+}
+
+static int
+read_json_number(yajlpp_parse_context* ypc,
+ const char* numberVal,
+ size_t numberLen)
+{
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+ const intern_string_t field_name = ypc->get_path();
+
+ auto number_frag = string_fragment::from_bytes(numberVal, numberLen);
+ auto scan_res = scn::scan_value<double>(number_frag.to_string_view());
+ if (!scan_res) {
+ log_error("invalid number %.*s", numberLen, numberVal);
+ return 0;
+ }
+
+ auto val = scan_res.value();
+ if (jlu->jlu_format->lf_timestamp_field == field_name) {
+ long long divisor = jlu->jlu_format->elf_timestamp_divisor;
+ struct timeval tv;
+
+ tv.tv_sec = val / divisor;
+ tv.tv_usec = fmod(val, divisor) * (1000000.0 / divisor);
+ if (jlu->jlu_format->lf_date_time.dts_local_time) {
+ struct tm ltm;
+ localtime_r(&tv.tv_sec, &ltm);
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ ltm.tm_zone = nullptr;
+#endif
+ ltm.tm_isdst = 0;
+ tv.tv_sec = tm2sec(&ltm);
+ }
+ jlu->jlu_base_line->set_time(tv);
+ } else if (jlu->jlu_format->lf_subsecond_field == field_name) {
+ uint64_t millis = 0;
+ switch (jlu->jlu_format->lf_subsecond_unit.value()) {
+ case log_format::subsecond_unit::milli:
+ millis = val;
+ break;
+ case log_format::subsecond_unit::micro:
+ millis = std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::microseconds((int64_t) val))
+ .count();
+ break;
+ case log_format::subsecond_unit::nano:
+ millis = std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::nanoseconds((int64_t) val))
+ .count();
+ break;
+ }
+ jlu->jlu_base_line->set_millis(millis);
+ } else if (jlu->jlu_format->elf_level_field == field_name) {
+ if (jlu->jlu_format->elf_level_pairs.empty()) {
+ jlu->jlu_base_line->set_level(jlu->jlu_format->convert_level(
+ number_frag, jlu->jlu_batch_context));
+ } else {
+ int64_t level_int = val;
+
+ for (const auto& pair : jlu->jlu_format->elf_level_pairs) {
+ if (pair.first == level_int) {
+ jlu->jlu_base_line->set_level(pair.second);
+ break;
+ }
+ }
+ }
+ }
+
+ jlu->add_sub_lines_for(field_name,
+ ypc->is_level(1),
+ val,
+ (const unsigned char*) numberVal,
+ numberLen);
+
+ return 1;
+}
+
+static int
+json_array_start(void* ctx)
+{
+ yajlpp_parse_context* ypc = (yajlpp_parse_context*) ctx;
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+
+ if (ypc->ypc_path_index_stack.size() == 2) {
+ const intern_string_t field_name = ypc->get_path_fragment_i(0);
+
+ jlu->add_sub_lines_for(field_name, true);
+ jlu->jlu_sub_start = yajl_get_bytes_consumed(jlu->jlu_handle) - 1;
+ }
+
+ return 1;
+}
+
+static int
+json_array_end(void* ctx)
+{
+ yajlpp_parse_context* ypc = (yajlpp_parse_context*) ctx;
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+
+ if (ypc->ypc_path_index_stack.size() == 1) {
+ const intern_string_t field_name = ypc->get_path_fragment_i(0);
+ size_t sub_end = yajl_get_bytes_consumed(jlu->jlu_handle);
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(field_name,
+ value_kind_t::VALUE_JSON),
+ string_fragment::from_byte_range(jlu->jlu_shared_buffer.get_data(),
+ jlu->jlu_sub_start,
+ sub_end));
+ }
+
+ return 1;
+}
+
+static const struct json_path_container json_log_handlers = {
+ yajlpp::pattern_property_handler("\\w+")
+ .add_cb(read_json_null)
+ .add_cb(read_json_bool)
+ .add_cb(read_json_number)
+ .add_cb(read_json_field),
+};
+
+static int rewrite_json_field(yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len);
+
+static int
+rewrite_json_null(yajlpp_parse_context* ypc)
+{
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+ const intern_string_t field_name = ypc->get_path();
+
+ if (!ypc->is_level(1) && !jlu->jlu_format->has_value_def(field_name)) {
+ return 1;
+ }
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(field_name, value_kind_t::VALUE_NULL));
+
+ return 1;
+}
+
+static int
+rewrite_json_bool(yajlpp_parse_context* ypc, int val)
+{
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+ const intern_string_t field_name = ypc->get_path();
+
+ if (!ypc->is_level(1) && !jlu->jlu_format->has_value_def(field_name)) {
+ return 1;
+ }
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(field_name,
+ value_kind_t::VALUE_BOOLEAN),
+ (bool) val);
+ return 1;
+}
+
+static int
+rewrite_json_int(yajlpp_parse_context* ypc, long long val)
+{
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+ const intern_string_t field_name = ypc->get_path();
+
+ if (!ypc->is_level(1) && !jlu->jlu_format->has_value_def(field_name)) {
+ return 1;
+ }
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(field_name,
+ value_kind_t::VALUE_INTEGER),
+ (int64_t) val);
+ return 1;
+}
+
+static int
+rewrite_json_double(yajlpp_parse_context* ypc, double val)
+{
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+ const intern_string_t field_name = ypc->get_path();
+
+ if (!ypc->is_level(1) && !jlu->jlu_format->has_value_def(field_name)) {
+ return 1;
+ }
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(field_name, value_kind_t::VALUE_FLOAT),
+ val);
+
+ return 1;
+}
+
+static const struct json_path_container json_log_rewrite_handlers = {
+ yajlpp::pattern_property_handler("\\w+")
+ .add_cb(rewrite_json_null)
+ .add_cb(rewrite_json_bool)
+ .add_cb(rewrite_json_int)
+ .add_cb(rewrite_json_double)
+ .add_cb(rewrite_json_field),
+};
+
+bool
+external_log_format::scan_for_partial(shared_buffer_ref& sbr,
+ size_t& len_out) const
+{
+ if (this->elf_type != elf_type_t::ELF_TYPE_TEXT) {
+ return false;
+ }
+
+ const auto& pat = this->elf_pattern_order[this->last_pattern_index()];
+ if (!this->lf_multiline) {
+ len_out = pat->p_pcre.pp_value->match_partial(sbr.to_string_fragment());
+ return true;
+ }
+
+ if (pat->p_timestamp_end == -1 || pat->p_timestamp_end > (int) sbr.length())
+ {
+ len_out = 0;
+ return false;
+ }
+
+ len_out = pat->p_pcre.pp_value->match_partial(sbr.to_string_fragment());
+ return (int) len_out > pat->p_timestamp_end;
+}
+
+std::vector<lnav::console::snippet>
+external_log_format::get_snippets() const
+{
+ std::vector<lnav::console::snippet> retval;
+
+ for (const auto& src_pair : this->elf_format_sources) {
+ retval.emplace_back(lnav::console::snippet::from(src_pair.first, "")
+ .with_line(src_pair.second));
+ }
+
+ return retval;
+}
+
+log_format::scan_result_t
+external_log_format::scan(logfile& lf,
+ std::vector<logline>& dst,
+ const line_info& li,
+ shared_buffer_ref& sbr,
+ scan_batch_context& sbc)
+{
+ if (this->elf_type == elf_type_t::ELF_TYPE_JSON) {
+ logline ll(li.li_file_range.fr_offset, 0, 0, LEVEL_INFO);
+ auto line_frag = sbr.to_string_fragment();
+
+ if (!line_frag.startswith("{")) {
+ if (!this->lf_specialized) {
+ return log_format::scan_no_match{"line is not a JSON object"};
+ }
+
+ ll.set_time(dst.back().get_timeval());
+ ll.set_level(LEVEL_INVALID);
+ dst.emplace_back(ll);
+ return log_format::scan_match{0};
+ }
+
+ auto& ypc = *(this->jlf_parse_context);
+ yajl_handle handle = this->jlf_yajl_handle.get();
+ json_log_userdata jlu(sbr, &sbc);
+
+ if (!this->lf_specialized && dst.size() >= 3) {
+ return log_format::scan_no_match{"file is not JSON-lines"};
+ }
+
+ if (li.li_partial) {
+ log_debug("skipping partial line at offset %d",
+ li.li_file_range.fr_offset);
+ if (this->lf_specialized) {
+ ll.set_level(LEVEL_INVALID);
+ dst.emplace_back(ll);
+ }
+ return log_format::scan_incomplete{};
+ }
+
+ const auto* line_data = (const unsigned char*) sbr.get_data();
+
+ yajl_reset(handle);
+ ypc.set_static_handler(json_log_handlers.jpc_children[0]);
+ ypc.ypc_userdata = &jlu;
+ ypc.ypc_ignore_unused = true;
+ ypc.ypc_alt_callbacks.yajl_start_array = json_array_start;
+ ypc.ypc_alt_callbacks.yajl_start_map = json_array_start;
+ ypc.ypc_alt_callbacks.yajl_end_array = nullptr;
+ ypc.ypc_alt_callbacks.yajl_end_map = nullptr;
+ jlu.jlu_format = this;
+ jlu.jlu_base_line = &ll;
+ jlu.jlu_line_value = sbr.get_data();
+ jlu.jlu_line_size = sbr.length();
+ jlu.jlu_handle = handle;
+ if (yajl_parse(handle, line_data, sbr.length()) == yajl_status_ok
+ && yajl_complete_parse(handle) == yajl_status_ok)
+ {
+ if (ll.get_time() == 0) {
+ if (this->lf_specialized) {
+ ll.set_ignore(true);
+ dst.emplace_back(ll);
+ return log_format::scan_match{0};
+ }
+
+ return log_format::scan_no_match{
+ "JSON message does not have expected timestamp property"};
+ }
+
+ jlu.jlu_sub_line_count += this->jlf_line_format_init_count;
+ for (int lpc = 0; lpc < jlu.jlu_sub_line_count; lpc++) {
+ ll.set_sub_offset(lpc);
+ if (lpc > 0) {
+ ll.set_level((log_level_t) (ll.get_level_and_flags()
+ | LEVEL_CONTINUED));
+ }
+ ll.set_has_ansi(jlu.jlu_has_ansi);
+ ll.set_valid_utf(jlu.jlu_valid_utf);
+ dst.emplace_back(ll);
+ }
+ } else {
+ unsigned char* msg;
+ int line_count = 2;
+
+ msg = yajl_get_error(
+ handle, 1, (const unsigned char*) sbr.get_data(), sbr.length());
+ if (msg != nullptr) {
+ auto msg_frag = string_fragment::from_c_str(msg);
+ log_debug("Unable to parse line at offset %d: %s",
+ li.li_file_range.fr_offset,
+ msg);
+ line_count = msg_frag.count('\n') + 1;
+ yajl_free_error(handle, msg);
+ }
+ if (!this->lf_specialized) {
+ return log_format::scan_no_match{"JSON parsing failed"};
+ }
+ for (int lpc = 0; lpc < line_count; lpc++) {
+ log_level_t level = LEVEL_INVALID;
+
+ ll.set_time(dst.back().get_timeval());
+ if (lpc > 0) {
+ level = (log_level_t) (level | LEVEL_CONTINUED);
+ }
+ ll.set_level(level);
+ ll.set_sub_offset(lpc);
+ dst.emplace_back(ll);
+ }
+ }
+
+ return log_format::scan_match{jlu.jlu_quality};
+ }
+
+ int curr_fmt = -1, orig_lock = this->last_pattern_index();
+ int pat_index = orig_lock;
+ auto line_sf = sbr.to_string_fragment();
+
+ while (::next_format(this->elf_pattern_order, curr_fmt, pat_index)) {
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+
+ auto* fpat = this->elf_pattern_order[curr_fmt].get();
+ auto* pat = fpat->p_pcre.pp_value.get();
+
+ if (fpat->p_module_format) {
+ continue;
+ }
+
+ auto match_res = pat->capture_from(line_sf)
+ .into(md)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (!match_res) {
+ if (!this->lf_pattern_locks.empty() && pat_index != -1) {
+ curr_fmt = -1;
+ pat_index = -1;
+ }
+ continue;
+ }
+
+ auto ts = md[fpat->p_timestamp_field_index];
+ auto time_cap = md[fpat->p_time_field_index];
+ auto level_cap = md[fpat->p_level_field_index];
+ auto mod_cap = md[fpat->p_module_field_index];
+ auto opid_cap = md[fpat->p_opid_field_index];
+ auto body_cap = md[fpat->p_body_field_index];
+ const char* last;
+ struct exttm log_time_tm;
+ struct timeval log_tv;
+ uint8_t mod_index = 0, opid = 0;
+ char combined_datetime_buf[512];
+
+ if (ts && time_cap) {
+ auto ts_str_len = snprintf(combined_datetime_buf,
+ sizeof(combined_datetime_buf),
+ "%.*sT%.*s",
+ ts->length(),
+ ts->data(),
+ time_cap->length(),
+ time_cap->data());
+ ts = string_fragment::from_bytes(combined_datetime_buf, ts_str_len);
+ }
+
+ if ((last = this->lf_date_time.scan(ts->data(),
+ ts->length(),
+ this->get_timestamp_formats(),
+ &log_time_tm,
+ log_tv))
+ == nullptr)
+ {
+ this->lf_date_time.unlock();
+ if ((last = this->lf_date_time.scan(ts->data(),
+ ts->length(),
+ this->get_timestamp_formats(),
+ &log_time_tm,
+ log_tv))
+ == nullptr)
+ {
+ continue;
+ }
+ }
+
+ auto level = this->convert_level(
+ level_cap.value_or(string_fragment::invalid()), &sbc);
+
+ this->lf_timestamp_flags = log_time_tm.et_flags;
+
+ if (!((log_time_tm.et_flags & ETF_DAY_SET)
+ && (log_time_tm.et_flags & ETF_MONTH_SET)
+ && (log_time_tm.et_flags & ETF_YEAR_SET)))
+ {
+ this->check_for_new_year(dst, log_time_tm, log_tv);
+ }
+
+ if (opid_cap && !opid_cap->empty()) {
+ {
+ auto opid_iter = sbc.sbc_opids.find(opid_cap.value());
+
+ if (opid_iter == sbc.sbc_opids.end()) {
+ auto opid_copy = opid_cap->to_owned(sbc.sbc_allocator);
+ auto otr = opid_time_range{log_tv, log_tv};
+ sbc.sbc_opids.emplace(opid_copy, otr);
+ } else {
+ opid_iter->second.otr_end = log_tv;
+ }
+ }
+ opid = hash_str(opid_cap->data(), opid_cap->length());
+ }
+
+ if (mod_cap) {
+ intern_string_t mod_name = intern_string::lookup(mod_cap.value());
+ auto mod_iter = MODULE_FORMATS.find(mod_name);
+
+ if (mod_iter == MODULE_FORMATS.end()) {
+ mod_index = this->module_scan(body_cap.value(), mod_name);
+ mod_iter = MODULE_FORMATS.find(mod_name);
+ } else if (mod_iter->second.mf_mod_format) {
+ mod_index = mod_iter->second.mf_mod_format->lf_mod_index;
+ }
+
+ if (mod_index && level_cap && body_cap) {
+ auto mod_elf = std::dynamic_pointer_cast<external_log_format>(
+ mod_iter->second.mf_mod_format);
+
+ if (mod_elf) {
+ static thread_local auto mod_md
+ = lnav::pcre2pp::match_data::unitialized();
+
+ shared_buffer_ref body_ref;
+
+ body_cap->trim();
+
+ int mod_pat_index = mod_elf->last_pattern_index();
+ auto& mod_pat = *mod_elf->elf_pattern_order[mod_pat_index];
+ auto match_res = mod_pat.p_pcre.pp_value
+ ->capture_from(body_cap.value())
+ .into(mod_md)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (match_res) {
+ auto mod_level_cap
+ = mod_md[mod_pat.p_level_field_index];
+
+ level = mod_elf->convert_level(
+ mod_level_cap.value_or(string_fragment::invalid()),
+ &sbc);
+ }
+ }
+ }
+ }
+
+ for (const auto& ivd : fpat->p_value_by_index) {
+ if (!ivd.ivd_value_def->vd_meta.lvm_values_index) {
+ continue;
+ }
+
+ auto cap = md[ivd.ivd_index];
+
+ if (cap && cap->is_valid()) {
+ auto& lvs = this->lf_value_stats[ivd.ivd_value_def->vd_meta
+ .lvm_values_index.value()];
+
+ if (cap->length() > lvs.lvs_width) {
+ lvs.lvs_width = cap->length();
+ }
+ }
+ }
+
+ for (auto value_index : fpat->p_numeric_value_indexes) {
+ const indexed_value_def& ivd = fpat->p_value_by_index[value_index];
+ const value_def& vd = *ivd.ivd_value_def;
+ auto num_cap = md[ivd.ivd_index];
+
+ if (num_cap && num_cap->is_valid()) {
+ const struct scaling_factor* scaling = nullptr;
+
+ if (ivd.ivd_unit_field_index >= 0) {
+ auto unit_cap = md[ivd.ivd_unit_field_index];
+
+ if (unit_cap && unit_cap->is_valid()) {
+ intern_string_t unit_val
+ = intern_string::lookup(unit_cap.value());
+
+ auto unit_iter = vd.vd_unit_scaling.find(unit_val);
+ if (unit_iter != vd.vd_unit_scaling.end()) {
+ const auto& sf = unit_iter->second;
+
+ scaling = &sf;
+ }
+ }
+ }
+
+ auto scan_res
+ = scn::scan_value<double>(num_cap->to_string_view());
+ if (scan_res) {
+ auto dvalue = scan_res.value();
+ if (scaling != nullptr) {
+ scaling->scale(dvalue);
+ }
+ this->lf_value_stats[vd.vd_meta.lvm_values_index.value()]
+ .add_value(dvalue);
+ }
+ }
+ }
+
+ dst.emplace_back(
+ li.li_file_range.fr_offset, log_tv, level, mod_index, opid);
+
+ if (orig_lock != curr_fmt) {
+ uint32_t lock_line;
+
+ log_debug("%zu: changing pattern lock %d -> %d",
+ dst.size() - 1,
+ orig_lock,
+ curr_fmt);
+ if (this->lf_pattern_locks.empty()) {
+ lock_line = 0;
+ } else {
+ lock_line = dst.size() - 1;
+ }
+ this->lf_pattern_locks.emplace_back(lock_line, curr_fmt);
+ }
+ return log_format::scan_match{0};
+ }
+
+ if (this->lf_specialized && !this->lf_multiline) {
+ auto& last_line = dst.back();
+
+ log_debug("invalid line %d %d", dst.size(), li.li_file_range.fr_offset);
+ dst.emplace_back(li.li_file_range.fr_offset,
+ last_line.get_timeval(),
+ log_level_t::LEVEL_INVALID);
+
+ return log_format::scan_match{0};
+ }
+
+ return log_format::scan_no_match{"no patterns matched"};
+}
+
+uint8_t
+external_log_format::module_scan(string_fragment body_cap,
+ const intern_string_t& mod_name)
+{
+ uint8_t mod_index;
+ body_cap.trim();
+ auto& ext_fmts = GRAPH_ORDERED_FORMATS;
+ module_format mf;
+
+ for (auto& elf : ext_fmts) {
+ int curr_fmt = -1, fmt_lock = -1;
+
+ while (::next_format(elf->elf_pattern_order, curr_fmt, fmt_lock)) {
+ static thread_local auto md
+ = lnav::pcre2pp::match_data::unitialized();
+
+ auto& fpat = elf->elf_pattern_order[curr_fmt];
+ auto& pat = fpat->p_pcre;
+
+ if (!fpat->p_module_format) {
+ continue;
+ }
+
+ auto match_res = pat.pp_value->capture_from(body_cap)
+ .into(md)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (!match_res) {
+ continue;
+ }
+
+ log_debug("%s:module format found -- %s (%d)",
+ mod_name.get(),
+ elf->get_name().get(),
+ elf->lf_mod_index);
+
+ mod_index = elf->lf_mod_index;
+ mf.mf_mod_format = elf->specialized(curr_fmt);
+ MODULE_FORMATS[mod_name] = mf;
+
+ return mod_index;
+ }
+ }
+
+ MODULE_FORMATS[mod_name] = mf;
+
+ return 0;
+}
+
+void
+external_log_format::annotate(uint64_t line_number,
+ string_attrs_t& sa,
+ logline_value_vector& values,
+ bool annotate_module) const
+{
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+
+ auto& line = values.lvv_sbr;
+ struct line_range lr;
+
+ line.erase_ansi();
+ if (this->elf_type != elf_type_t::ELF_TYPE_TEXT) {
+ if (this->jlf_cached_full) {
+ values = this->jlf_line_values;
+ sa = this->jlf_line_attrs;
+ } else {
+ values.lvv_sbr = this->jlf_line_values.lvv_sbr;
+ for (const auto& llv : this->jlf_line_values.lvv_values) {
+ if (this->jlf_cached_sub_range.contains(llv.lv_origin)) {
+ values.lvv_values.emplace_back(llv);
+ values.lvv_values.back().lv_origin.shift(
+ this->jlf_cached_sub_range.lr_start,
+ -this->jlf_cached_sub_range.lr_start);
+ }
+ }
+ for (const auto& attr : this->jlf_line_attrs) {
+ if (this->jlf_cached_sub_range.contains(attr.sa_range)) {
+ sa.emplace_back(attr);
+ }
+ }
+ }
+ return;
+ }
+
+ if (line.empty()) {
+ return;
+ }
+
+ values.lvv_values.reserve(this->elf_value_defs.size());
+
+ int pat_index = this->pattern_index_for_line(line_number);
+ auto& pat = *this->elf_pattern_order[pat_index];
+
+ sa.reserve(pat.p_pcre.pp_value->get_capture_count());
+ auto match_res
+ = pat.p_pcre.pp_value->capture_from(line.to_string_fragment())
+ .into(md)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (!match_res) {
+ // A continued line still needs a body.
+ lr.lr_start = 0;
+ lr.lr_end = line.length();
+ sa.emplace_back(lr, SA_BODY.value());
+ if (!this->lf_multiline) {
+ auto len
+ = pat.p_pcre.pp_value->match_partial(line.to_string_fragment());
+ sa.emplace_back(
+ line_range{(int) len, -1},
+ SA_INVALID.value("Log line does not match any pattern"));
+ }
+ return;
+ }
+
+ nonstd::optional<string_fragment> module_cap;
+ if (!pat.p_module_format) {
+ auto ts_cap = md[pat.p_timestamp_field_index];
+ if (ts_cap) {
+ sa.emplace_back(to_line_range(ts_cap.value()),
+ logline::L_TIMESTAMP.value());
+ }
+
+ if (pat.p_module_field_index != -1) {
+ module_cap = md[pat.p_module_field_index];
+ if (module_cap) {
+ sa.emplace_back(to_line_range(module_cap.value()),
+ logline::L_MODULE.value());
+ }
+ }
+
+ auto opid_cap = md[pat.p_opid_field_index];
+ if (opid_cap) {
+ sa.emplace_back(to_line_range(opid_cap.value()),
+ logline::L_OPID.value());
+ }
+ }
+
+ auto body_cap = md[pat.p_body_field_index];
+
+ for (size_t lpc = 0; lpc < pat.p_value_by_index.size(); lpc++) {
+ const indexed_value_def& ivd = pat.p_value_by_index[lpc];
+ const struct scaling_factor* scaling = nullptr;
+ auto cap = md[ivd.ivd_index];
+ const auto& vd = *ivd.ivd_value_def;
+
+ if (ivd.ivd_unit_field_index >= 0) {
+ auto unit_cap = md[ivd.ivd_unit_field_index];
+
+ if (unit_cap) {
+ intern_string_t unit_val
+ = intern_string::lookup(unit_cap.value());
+ auto unit_iter = vd.vd_unit_scaling.find(unit_val);
+ if (unit_iter != vd.vd_unit_scaling.end()) {
+ const struct scaling_factor& sf = unit_iter->second;
+
+ scaling = &sf;
+ }
+ }
+ }
+
+ if (cap) {
+ values.lvv_values.emplace_back(
+ vd.vd_meta, line, to_line_range(cap.value()));
+ values.lvv_values.back().apply_scaling(scaling);
+ } else {
+ values.lvv_values.emplace_back(vd.vd_meta);
+ }
+ if (pat.p_module_format) {
+ values.lvv_values.back().lv_meta.lvm_from_module = true;
+ }
+ }
+
+ bool did_mod_annotate_body = false;
+ if (annotate_module && module_cap && body_cap && body_cap->is_valid()) {
+ intern_string_t mod_name = intern_string::lookup(module_cap.value());
+ auto mod_iter = MODULE_FORMATS.find(mod_name);
+
+ if (mod_iter != MODULE_FORMATS.end()
+ && mod_iter->second.mf_mod_format != nullptr)
+ {
+ auto& mf = mod_iter->second;
+
+ body_cap->trim();
+ auto narrow_res
+ = line.narrow(body_cap->sf_begin, body_cap->length());
+ auto pre_mod_values_size = values.lvv_values.size();
+ auto pre_mod_sa_size = sa.size();
+ mf.mf_mod_format->annotate(line_number, sa, values, false);
+ for (size_t lpc = pre_mod_values_size;
+ lpc < values.lvv_values.size();
+ lpc++)
+ {
+ values.lvv_values[lpc].lv_origin.shift(0, body_cap->sf_begin);
+ }
+ for (size_t lpc = pre_mod_sa_size; lpc < sa.size(); lpc++) {
+ sa[lpc].sa_range.shift(0, body_cap->sf_begin);
+ }
+ line.widen(narrow_res);
+ did_mod_annotate_body = true;
+ }
+ }
+ if (!did_mod_annotate_body) {
+ if (body_cap && body_cap->is_valid()) {
+ lr = to_line_range(body_cap.value());
+ } else {
+ lr.lr_start = line.length();
+ lr.lr_end = line.length();
+ }
+ sa.emplace_back(lr, SA_BODY.value());
+ }
+}
+
+void
+external_log_format::rewrite(exec_context& ec,
+ shared_buffer_ref& line,
+ string_attrs_t& sa,
+ std::string& value_out)
+{
+ std::vector<logline_value>::iterator shift_iter;
+ auto& values = *ec.ec_line_values;
+
+ value_out.assign(line.get_data(), line.length());
+
+ for (auto iter = values.lvv_values.begin(); iter != values.lvv_values.end();
+ ++iter)
+ {
+ if (!iter->lv_origin.is_valid()) {
+ log_debug("not rewriting value with invalid origin -- %s",
+ iter->lv_meta.lvm_name.get());
+ continue;
+ }
+
+ auto vd_iter = this->elf_value_defs.find(iter->lv_meta.lvm_name);
+ if (vd_iter == this->elf_value_defs.end()) {
+ log_debug("not rewriting undefined value -- %s",
+ iter->lv_meta.lvm_name.get());
+ continue;
+ }
+
+ const auto& vd = *vd_iter->second;
+
+ if (vd.vd_rewriter.empty()) {
+ continue;
+ }
+
+ auto _sg = ec.enter_source(
+ vd_iter->second->vd_rewrite_src_name, 1, vd.vd_rewriter);
+ std::string field_value;
+
+ auto exec_res = execute_any(ec, vd.vd_rewriter);
+ if (exec_res.isOk()) {
+ field_value = exec_res.unwrap();
+ } else {
+ field_value = exec_res.unwrapErr().to_attr_line().get_string();
+ }
+ value_out.erase(iter->lv_origin.lr_start, iter->lv_origin.length());
+
+ int32_t shift_amount
+ = ((int32_t) field_value.length()) - iter->lv_origin.length();
+ value_out.insert(iter->lv_origin.lr_start, field_value);
+ for (shift_iter = values.lvv_values.begin();
+ shift_iter != values.lvv_values.end();
+ ++shift_iter)
+ {
+ shift_iter->lv_origin.shift(iter->lv_origin.lr_start, shift_amount);
+ }
+ shift_string_attrs(sa, iter->lv_origin.lr_start, shift_amount);
+ }
+}
+
+static int
+read_json_field(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
+{
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+ const intern_string_t field_name = ypc->get_path();
+ struct exttm tm_out;
+ struct timeval tv_out;
+
+ if (jlu->jlu_format->lf_timestamp_field == field_name) {
+ jlu->jlu_format->lf_date_time.scan(
+ (const char*) str,
+ len,
+ jlu->jlu_format->get_timestamp_formats(),
+ &tm_out,
+ tv_out);
+ // Leave off the machine oriented flag since we convert it anyhow
+ jlu->jlu_format->lf_timestamp_flags
+ = tm_out.et_flags & ~ETF_MACHINE_ORIENTED;
+ jlu->jlu_base_line->set_time(tv_out);
+ } else if (jlu->jlu_format->elf_level_pointer.pp_value != nullptr) {
+ if (jlu->jlu_format->elf_level_pointer.pp_value
+ ->find_in(field_name.to_string_fragment(), PCRE2_NO_UTF_CHECK)
+ .ignore_error()
+ .has_value())
+ {
+ jlu->jlu_base_line->set_level(jlu->jlu_format->convert_level(
+ string_fragment::from_bytes(str, len), jlu->jlu_batch_context));
+ }
+ }
+ if (jlu->jlu_format->elf_level_field == field_name) {
+ jlu->jlu_base_line->set_level(jlu->jlu_format->convert_level(
+ string_fragment::from_bytes(str, len), jlu->jlu_batch_context));
+ }
+ if (jlu->jlu_format->elf_opid_field == field_name) {
+ uint8_t opid = hash_str((const char*) str, len);
+ jlu->jlu_base_line->set_opid(opid);
+ }
+
+ jlu->add_sub_lines_for(
+ field_name, ypc->is_level(1), nonstd::nullopt, str, len);
+
+ return 1;
+}
+
+static int
+rewrite_json_field(yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len)
+{
+ static const intern_string_t body_name = intern_string::lookup("body", -1);
+ json_log_userdata* jlu = (json_log_userdata*) ypc->ypc_userdata;
+ const intern_string_t field_name = ypc->get_path();
+
+ if (jlu->jlu_format->lf_timestamp_field == field_name) {
+ char time_buf[64];
+
+ // TODO add a timeval kind to logline_value
+ if (jlu->jlu_line->is_time_skewed()) {
+ struct timeval tv;
+ struct exttm tm;
+
+ jlu->jlu_format->lf_date_time.scan(
+ (const char*) str,
+ len,
+ jlu->jlu_format->get_timestamp_formats(),
+ &tm,
+ tv);
+ sql_strftime(time_buf, sizeof(time_buf), tv, 'T');
+ } else {
+ sql_strftime(
+ time_buf, sizeof(time_buf), jlu->jlu_line->get_timeval(), 'T');
+ }
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(field_name,
+ value_kind_t::VALUE_TEXT),
+ std::string{time_buf});
+ } else if (jlu->jlu_shared_buffer.contains((const char*) str)) {
+ auto str_offset = (int) ((const char*) str - jlu->jlu_line_value);
+ if (field_name == jlu->jlu_format->elf_body_field) {
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(body_name,
+ value_kind_t::VALUE_TEXT),
+ string_fragment::from_byte_range(
+ jlu->jlu_shared_buffer.get_data(),
+ str_offset,
+ str_offset + len));
+ }
+ if (!ypc->is_level(1) && !jlu->jlu_format->has_value_def(field_name)) {
+ return 1;
+ }
+
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(field_name,
+ value_kind_t::VALUE_TEXT),
+ string_fragment::from_byte_range(jlu->jlu_shared_buffer.get_data(),
+ str_offset,
+ str_offset + len));
+ } else {
+ if (field_name == jlu->jlu_format->elf_body_field) {
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(body_name,
+ value_kind_t::VALUE_TEXT),
+ std::string{(const char*) str, len});
+ }
+ if (!ypc->is_level(1) && !jlu->jlu_format->has_value_def(field_name)) {
+ return 1;
+ }
+
+ jlu->jlu_format->jlf_line_values.lvv_values.emplace_back(
+ jlu->jlu_format->get_value_meta(field_name,
+ value_kind_t::VALUE_TEXT),
+ std::string{(const char*) str, len});
+ }
+
+ return 1;
+}
+
+void
+external_log_format::get_subline(const logline& ll,
+ shared_buffer_ref& sbr,
+ bool full_message)
+{
+ if (this->elf_type == elf_type_t::ELF_TYPE_TEXT) {
+ return;
+ }
+
+ if (this->jlf_cached_offset != ll.get_offset()
+ || this->jlf_cached_full != full_message)
+ {
+ auto& ypc = *(this->jlf_parse_context);
+ yajl_handle handle = this->jlf_yajl_handle.get();
+ json_log_userdata jlu(sbr, nullptr);
+
+ this->jlf_share_manager.invalidate_refs();
+ this->jlf_cached_line.clear();
+ this->jlf_line_values.clear();
+ this->jlf_line_offsets.clear();
+ this->jlf_line_attrs.clear();
+
+ auto line_frag = sbr.to_string_fragment();
+
+ if (!line_frag.startswith("{")) {
+ this->jlf_cached_line.resize(line_frag.length());
+ memcpy(this->jlf_cached_line.data(),
+ line_frag.data(),
+ line_frag.length());
+ this->jlf_line_values.clear();
+ sbr.share(this->jlf_share_manager,
+ &this->jlf_cached_line[0],
+ this->jlf_cached_line.size());
+ this->jlf_line_values.lvv_sbr = sbr;
+ this->jlf_line_attrs.emplace_back(
+ line_range{0, -1},
+ SA_INVALID.value(fmt::format(
+ FMT_STRING("line at offset {} is not a JSON-line"),
+ ll.get_offset())));
+ return;
+ }
+
+ yajl_reset(handle);
+ ypc.set_static_handler(json_log_rewrite_handlers.jpc_children[0]);
+ ypc.ypc_userdata = &jlu;
+ ypc.ypc_ignore_unused = true;
+ ypc.ypc_alt_callbacks.yajl_start_array = json_array_start;
+ ypc.ypc_alt_callbacks.yajl_end_array = json_array_end;
+ ypc.ypc_alt_callbacks.yajl_start_map = json_array_start;
+ ypc.ypc_alt_callbacks.yajl_end_map = json_array_end;
+ jlu.jlu_format = this;
+ jlu.jlu_line = &ll;
+ jlu.jlu_handle = handle;
+ jlu.jlu_line_value = sbr.get_data();
+
+ yajl_status parse_status = yajl_parse(
+ handle, (const unsigned char*) sbr.get_data(), sbr.length());
+ if (parse_status != yajl_status_ok
+ || yajl_complete_parse(handle) != yajl_status_ok)
+ {
+ unsigned char* msg;
+ std::string full_msg;
+
+ msg = yajl_get_error(
+ handle, 1, (const unsigned char*) sbr.get_data(), sbr.length());
+ if (msg != nullptr) {
+ full_msg = fmt::format(
+ FMT_STRING("[offset: {}] {}\n{}"),
+ ll.get_offset(),
+ fmt::string_view{sbr.get_data(), sbr.length()},
+ reinterpret_cast<char*>(msg));
+ yajl_free_error(handle, msg);
+ }
+
+ this->jlf_cached_line.resize(full_msg.size());
+ memcpy(
+ this->jlf_cached_line.data(), full_msg.data(), full_msg.size());
+ this->jlf_line_values.clear();
+ this->jlf_line_attrs.emplace_back(
+ line_range{0, -1},
+ SA_INVALID.value("JSON line failed to parse"));
+ } else {
+ std::vector<logline_value>::iterator lv_iter;
+ bool used_values[this->jlf_line_values.lvv_values.size()];
+ struct line_range lr;
+
+ memset(used_values, 0, sizeof(used_values));
+
+ for (lv_iter = this->jlf_line_values.lvv_values.begin();
+ lv_iter != this->jlf_line_values.lvv_values.end();
+ ++lv_iter)
+ {
+ lv_iter->lv_meta.lvm_format = this;
+ }
+
+ int sub_offset = 1 + this->jlf_line_format_init_count;
+ for (const auto& jfe : this->jlf_line_format) {
+ static const intern_string_t ts_field
+ = intern_string::lookup("__timestamp__", -1);
+ static const intern_string_t level_field
+ = intern_string::lookup("__level__");
+ size_t begin_size = this->jlf_cached_line.size();
+
+ switch (jfe.jfe_type) {
+ case json_log_field::CONSTANT:
+ this->json_append_to_cache(
+ jfe.jfe_default_value.c_str(),
+ jfe.jfe_default_value.size());
+ break;
+ case json_log_field::VARIABLE:
+ lv_iter = find_if(
+ this->jlf_line_values.lvv_values.begin(),
+ this->jlf_line_values.lvv_values.end(),
+ logline_value_cmp(&jfe.jfe_value.pp_value));
+ if (lv_iter != this->jlf_line_values.lvv_values.end()) {
+ auto str = lv_iter->to_string();
+ while (endswith(str, "\n")) {
+ str.pop_back();
+ }
+ size_t nl_pos = str.find('\n');
+
+ if (!jfe.jfe_prefix.empty()) {
+ this->json_append_to_cache(jfe.jfe_prefix);
+ }
+ lr.lr_start = this->jlf_cached_line.size();
+
+ if ((int) str.size() > jfe.jfe_max_width) {
+ switch (jfe.jfe_overflow) {
+ case json_format_element::overflow_t::
+ ABBREV: {
+ this->json_append_to_cache(str.c_str(),
+ str.size());
+ size_t new_size = abbreviate_str(
+ &this->jlf_cached_line[lr.lr_start],
+ str.size(),
+ jfe.jfe_max_width);
+
+ this->jlf_cached_line.resize(
+ lr.lr_start + new_size);
+ break;
+ }
+ case json_format_element::overflow_t::
+ TRUNCATE: {
+ this->json_append_to_cache(
+ str.c_str(), jfe.jfe_max_width);
+ break;
+ }
+ case json_format_element::overflow_t::
+ DOTDOT: {
+ size_t middle
+ = (jfe.jfe_max_width / 2) - 1;
+ this->json_append_to_cache(str.c_str(),
+ middle);
+ this->json_append_to_cache("..", 2);
+ size_t rest
+ = (jfe.jfe_max_width - middle - 2);
+ this->json_append_to_cache(
+ str.c_str() + str.size() - rest,
+ rest);
+ break;
+ }
+ }
+ } else {
+ value_def* vd = nullptr;
+
+ if (lv_iter->lv_meta.lvm_values_index) {
+ vd = this->elf_value_def_order
+ [lv_iter->lv_meta.lvm_values_index
+ .value()]
+ .get();
+ }
+ sub_offset
+ += std::count(str.begin(), str.end(), '\n');
+ this->json_append(
+ jfe, vd, str.c_str(), str.size());
+ }
+
+ if (nl_pos == std::string::npos || full_message) {
+ lr.lr_end = this->jlf_cached_line.size();
+ } else {
+ lr.lr_end = lr.lr_start + nl_pos;
+ }
+
+ if (lv_iter->lv_meta.lvm_name
+ == this->lf_timestamp_field)
+ {
+ this->jlf_line_attrs.emplace_back(
+ lr, logline::L_TIMESTAMP.value());
+ } else if (lv_iter->lv_meta.lvm_name
+ == this->elf_body_field)
+ {
+ this->jlf_line_attrs.emplace_back(
+ lr, SA_BODY.value());
+ } else if (lv_iter->lv_meta.lvm_name
+ == this->elf_opid_field)
+ {
+ this->jlf_line_attrs.emplace_back(
+ lr, logline::L_OPID.value());
+ }
+ lv_iter->lv_origin = lr;
+ used_values[std::distance(
+ this->jlf_line_values.lvv_values.begin(),
+ lv_iter)]
+ = true;
+
+ if (!jfe.jfe_suffix.empty()) {
+ this->json_append_to_cache(jfe.jfe_suffix);
+ }
+ } else if (jfe.jfe_value.pp_value == ts_field) {
+ struct line_range lr;
+ ssize_t ts_len;
+ char ts[64];
+
+ if (jfe.jfe_ts_format.empty()) {
+ ts_len = sql_strftime(
+ ts, sizeof(ts), ll.get_timeval(), 'T');
+ } else {
+ struct exttm et;
+
+ ll.to_exttm(et);
+ ts_len = ftime_fmt(ts,
+ sizeof(ts),
+ jfe.jfe_ts_format.c_str(),
+ et);
+ }
+ lr.lr_start = this->jlf_cached_line.size();
+ this->json_append_to_cache(ts, ts_len);
+ lr.lr_end = this->jlf_cached_line.size();
+ this->jlf_line_attrs.emplace_back(
+ lr, logline::L_TIMESTAMP.value());
+
+ lv_iter = find_if(
+ this->jlf_line_values.lvv_values.begin(),
+ this->jlf_line_values.lvv_values.end(),
+ logline_value_cmp(&this->lf_timestamp_field));
+ if (lv_iter
+ != this->jlf_line_values.lvv_values.end())
+ {
+ used_values[distance(
+ this->jlf_line_values.lvv_values.begin(),
+ lv_iter)]
+ = true;
+ }
+ } else if (jfe.jfe_value.pp_value == level_field
+ || jfe.jfe_value.pp_value
+ == this->elf_level_field)
+ {
+ const auto* level_name = ll.get_level_name();
+ auto level_len = strlen(level_name);
+ this->json_append(
+ jfe, nullptr, level_name, level_len);
+ if (jfe.jfe_auto_width) {
+ this->json_append_to_cache(MAX_LEVEL_NAME_LEN
+ - level_len);
+ }
+ } else if (!jfe.jfe_default_value.empty()) {
+ if (!jfe.jfe_prefix.empty()) {
+ this->json_append_to_cache(jfe.jfe_prefix);
+ }
+ this->json_append(jfe,
+ nullptr,
+ jfe.jfe_default_value.c_str(),
+ jfe.jfe_default_value.size());
+ if (!jfe.jfe_suffix.empty()) {
+ this->json_append_to_cache(jfe.jfe_suffix);
+ }
+ }
+
+ switch (jfe.jfe_text_transform) {
+ case external_log_format::json_format_element::
+ transform_t::NONE:
+ break;
+ case external_log_format::json_format_element::
+ transform_t::UPPERCASE:
+ for (size_t cindex = begin_size;
+ cindex < this->jlf_cached_line.size();
+ cindex++)
+ {
+ this->jlf_cached_line[cindex] = toupper(
+ this->jlf_cached_line[cindex]);
+ }
+ break;
+ case external_log_format::json_format_element::
+ transform_t::LOWERCASE:
+ for (size_t cindex = begin_size;
+ cindex < this->jlf_cached_line.size();
+ cindex++)
+ {
+ this->jlf_cached_line[cindex] = tolower(
+ this->jlf_cached_line[cindex]);
+ }
+ break;
+ case external_log_format::json_format_element::
+ transform_t::CAPITALIZE:
+ for (size_t cindex = begin_size;
+ cindex < begin_size + 1;
+ cindex++)
+ {
+ this->jlf_cached_line[cindex] = toupper(
+ this->jlf_cached_line[cindex]);
+ }
+ for (size_t cindex = begin_size + 1;
+ cindex < this->jlf_cached_line.size();
+ cindex++)
+ {
+ this->jlf_cached_line[cindex] = tolower(
+ this->jlf_cached_line[cindex]);
+ }
+ break;
+ }
+ break;
+ }
+ }
+ this->json_append_to_cache("\n", 1);
+
+ for (size_t lpc = 0; lpc < this->jlf_line_values.lvv_values.size();
+ lpc++)
+ {
+ static const intern_string_t body_name
+ = intern_string::lookup("body", -1);
+ auto& lv = this->jlf_line_values.lvv_values[lpc];
+
+ if (lv.lv_meta.is_hidden() || used_values[lpc]
+ || body_name == lv.lv_meta.lvm_name)
+ {
+ continue;
+ }
+
+ auto str = lv.to_string();
+ while (endswith(str, "\n")) {
+ str.pop_back();
+ }
+
+ lv.lv_sub_offset = sub_offset;
+ lv.lv_origin.lr_start = this->jlf_cached_line.size() + 2
+ + lv.lv_meta.lvm_name.size() + 2;
+ auto frag = string_fragment::from_str(str);
+ while (true) {
+ auto utf_scan_res = is_utf8(frag, '\n');
+
+ this->json_append_to_cache(" ", 2);
+ this->json_append_to_cache(
+ lv.lv_meta.lvm_name.to_string_fragment());
+ this->json_append_to_cache(": ", 2);
+ this->json_append_to_cache(utf_scan_res.usr_valid_frag);
+ this->json_append_to_cache("\n", 1);
+ sub_offset += 1;
+ if (utf_scan_res.usr_remaining) {
+ frag = utf_scan_res.usr_remaining.value();
+ } else {
+ break;
+ }
+ }
+ }
+ }
+
+ this->jlf_line_offsets.push_back(0);
+ for (size_t lpc = 0; lpc < this->jlf_cached_line.size(); lpc++) {
+ if (this->jlf_cached_line[lpc] == '\n') {
+ this->jlf_line_offsets.push_back(lpc + 1);
+ }
+ }
+ this->jlf_line_offsets.push_back(this->jlf_cached_line.size());
+ this->jlf_cached_offset = ll.get_offset();
+ this->jlf_cached_full = full_message;
+ }
+
+ off_t this_off = 0, next_off = 0;
+
+ if (!this->jlf_line_offsets.empty()
+ && ll.get_sub_offset() < this->jlf_line_offsets.size())
+ {
+ require(ll.get_sub_offset() < this->jlf_line_offsets.size());
+
+ this_off = this->jlf_line_offsets[ll.get_sub_offset()];
+ if ((ll.get_sub_offset() + 1) < (int) this->jlf_line_offsets.size()) {
+ next_off = this->jlf_line_offsets[ll.get_sub_offset() + 1];
+ } else {
+ next_off = this->jlf_cached_line.size();
+ }
+ if (next_off > 0 && this->jlf_cached_line[next_off - 1] == '\n'
+ && this_off != next_off)
+ {
+ next_off -= 1;
+ }
+ }
+
+ if (full_message) {
+ sbr.share(this->jlf_share_manager,
+ &this->jlf_cached_line[0],
+ this->jlf_cached_line.size());
+ } else {
+ sbr.share(this->jlf_share_manager,
+ this->jlf_cached_line.data() + this_off,
+ next_off - this_off);
+ }
+ sbr.get_metadata().m_valid_utf = ll.is_valid_utf();
+ sbr.get_metadata().m_has_ansi = ll.has_ansi();
+ this->jlf_cached_sub_range.lr_start = this_off;
+ this->jlf_cached_sub_range.lr_end = next_off;
+ this->jlf_line_values.lvv_sbr = sbr;
+}
+
+void
+external_log_format::build(std::vector<lnav::console::user_message>& errors)
+{
+ if (!this->lf_timestamp_field.empty()) {
+ auto& vd = this->elf_value_defs[this->lf_timestamp_field];
+ if (vd.get() == nullptr) {
+ vd = std::make_shared<external_log_format::value_def>(
+ this->lf_timestamp_field, value_kind_t::VALUE_TEXT, -1, this);
+ }
+ vd->vd_meta.lvm_name = this->lf_timestamp_field;
+ vd->vd_meta.lvm_kind = value_kind_t::VALUE_TEXT;
+ vd->vd_internal = true;
+ }
+ if (startswith(this->elf_level_field.get(), "/")) {
+ this->elf_level_field
+ = intern_string::lookup(this->elf_level_field.get() + 1);
+ }
+ if (!this->elf_level_field.empty()
+ && this->elf_value_defs.find(this->elf_level_field)
+ == this->elf_value_defs.end())
+ {
+ auto& vd = this->elf_value_defs[this->elf_level_field];
+ if (vd.get() == nullptr) {
+ vd = std::make_shared<external_log_format::value_def>(
+ this->elf_level_field, value_kind_t::VALUE_TEXT, -1, this);
+ }
+ vd->vd_meta.lvm_name = this->elf_level_field;
+ vd->vd_meta.lvm_kind = value_kind_t::VALUE_TEXT;
+ vd->vd_internal = true;
+ }
+ if (!this->elf_body_field.empty()) {
+ auto& vd = this->elf_value_defs[this->elf_body_field];
+ if (vd.get() == nullptr) {
+ vd = std::make_shared<external_log_format::value_def>(
+ this->elf_body_field, value_kind_t::VALUE_TEXT, -1, this);
+ }
+ vd->vd_meta.lvm_name = this->elf_body_field;
+ vd->vd_meta.lvm_kind = value_kind_t::VALUE_TEXT;
+ vd->vd_internal = true;
+ }
+
+ if (!this->lf_timestamp_format.empty()) {
+ this->lf_timestamp_format.push_back(nullptr);
+ }
+ for (auto iter = this->elf_patterns.begin();
+ iter != this->elf_patterns.end();
+ ++iter)
+ {
+ pattern& pat = *iter->second;
+
+ if (pat.p_pcre.pp_value == nullptr) {
+ continue;
+ }
+
+ if (pat.p_module_format) {
+ this->elf_has_module_format = true;
+ }
+
+ for (auto named_cap : pat.p_pcre.pp_value->get_named_captures()) {
+ const intern_string_t name
+ = intern_string::lookup(named_cap.get_name());
+
+ if (name == this->lf_timestamp_field) {
+ pat.p_timestamp_field_index = named_cap.get_index();
+ }
+ if (name == this->lf_time_field) {
+ pat.p_time_field_index = named_cap.get_index();
+ }
+ if (name == this->elf_level_field) {
+ pat.p_level_field_index = named_cap.get_index();
+ }
+ if (name == this->elf_module_id_field) {
+ pat.p_module_field_index = named_cap.get_index();
+ }
+ if (name == this->elf_opid_field) {
+ pat.p_opid_field_index = named_cap.get_index();
+ }
+ if (name == this->elf_body_field) {
+ pat.p_body_field_index = named_cap.get_index();
+ }
+
+ auto value_iter = this->elf_value_defs.find(name);
+ if (value_iter != this->elf_value_defs.end()) {
+ auto vd = value_iter->second;
+ indexed_value_def ivd;
+
+ ivd.ivd_index = named_cap.get_index();
+ if (!vd->vd_unit_field.empty()) {
+ ivd.ivd_unit_field_index = pat.p_pcre.pp_value->name_index(
+ vd->vd_unit_field.get());
+ } else {
+ ivd.ivd_unit_field_index = -1;
+ }
+ if (!vd->vd_internal && vd->vd_meta.lvm_column == -1) {
+ vd->vd_meta.lvm_column = this->elf_column_count++;
+ }
+ ivd.ivd_value_def = vd;
+ pat.p_value_by_index.push_back(ivd);
+ }
+ }
+
+ stable_sort(pat.p_value_by_index.begin(), pat.p_value_by_index.end());
+
+ for (int lpc = 0; lpc < (int) pat.p_value_by_index.size(); lpc++) {
+ auto& ivd = pat.p_value_by_index[lpc];
+ auto vd = ivd.ivd_value_def;
+
+ if (!vd->vd_foreign_key && !vd->vd_meta.lvm_identifier) {
+ switch (vd->vd_meta.lvm_kind) {
+ case value_kind_t::VALUE_INTEGER:
+ case value_kind_t::VALUE_FLOAT:
+ pat.p_numeric_value_indexes.push_back(lpc);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ if (!this->elf_level_field.empty() && pat.p_level_field_index == -1) {
+ log_warning("%s:level field '%s' not found in pattern",
+ pat.p_config_path.c_str(),
+ this->elf_level_field.get());
+ }
+ if (!this->elf_module_id_field.empty()
+ && pat.p_module_field_index == -1)
+ {
+ log_warning("%s:module field '%s' not found in pattern",
+ pat.p_config_path.c_str(),
+ this->elf_module_id_field.get());
+ }
+ if (!this->elf_body_field.empty() && pat.p_body_field_index == -1) {
+ log_warning("%s:body field '%s' not found in pattern",
+ pat.p_config_path.c_str(),
+ this->elf_body_field.get());
+ }
+
+ this->elf_pattern_order.push_back(iter->second);
+ }
+
+ if (this->elf_type != elf_type_t::ELF_TYPE_TEXT) {
+ if (!this->elf_patterns.empty()) {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(
+ lnav::roles::symbol(this->elf_name.to_string()))
+ .append(" is not a valid log format"))
+ .with_reason("structured logs cannot have regexes")
+ .with_snippets(this->get_snippets()));
+ }
+ if (this->elf_type == elf_type_t::ELF_TYPE_JSON) {
+ this->jlf_parse_context
+ = std::make_shared<yajlpp_parse_context>(this->elf_name);
+ this->jlf_yajl_handle.reset(
+ yajl_alloc(&this->jlf_parse_context->ypc_callbacks,
+ nullptr,
+ this->jlf_parse_context.get()),
+ yajl_handle_deleter());
+ yajl_config(
+ this->jlf_yajl_handle.get(), yajl_dont_validate_strings, 1);
+ }
+
+ } else {
+ if (this->elf_patterns.empty()) {
+ errors.emplace_back(lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(lnav::roles::symbol(
+ this->elf_name.to_string()))
+ .append(" is not a valid log format"))
+ .with_reason("no regexes specified")
+ .with_snippets(this->get_snippets()));
+ }
+ }
+
+ stable_sort(this->elf_level_pairs.begin(), this->elf_level_pairs.end());
+
+ for (auto& vd : this->elf_value_def_order) {
+ std::vector<std::string>::iterator act_iter;
+
+ vd->vd_meta.lvm_format = this;
+ if (!vd->vd_internal && vd->vd_meta.lvm_column == -1) {
+ vd->vd_meta.lvm_column = this->elf_column_count++;
+ }
+
+ if (vd->vd_meta.lvm_kind == value_kind_t::VALUE_UNKNOWN) {
+ vd->vd_meta.lvm_kind = value_kind_t::VALUE_TEXT;
+ }
+
+ if (this->elf_type == elf_type_t::ELF_TYPE_TEXT) {
+ std::set<std::string> available_captures;
+
+ bool found_in_pattern = false;
+ for (const auto& pat : this->elf_patterns) {
+ if (pat.second->p_pcre.pp_value == nullptr) {
+ continue;
+ }
+
+ auto cap_index = pat.second->p_pcre.pp_value->name_index(
+ vd->vd_meta.lvm_name.get());
+ if (cap_index >= 0) {
+ found_in_pattern = true;
+ break;
+ }
+
+ for (auto named_cap :
+ pat.second->p_pcre.pp_value->get_named_captures())
+ {
+ available_captures.insert(named_cap.get_name().to_string());
+ }
+ }
+ if (!found_in_pattern) {
+ auto notes
+ = attr_line_t("the following captures are available:\n ")
+ .join(available_captures,
+ VC_ROLE.value(role_t::VCR_SYMBOL),
+ ", ");
+ errors.emplace_back(
+ lnav::console::user_message::warning(
+ attr_line_t("invalid value ")
+ .append_quoted(lnav::roles::symbol(
+ fmt::format(FMT_STRING("/{}/value/{}"),
+ this->elf_name,
+ vd->vd_meta.lvm_name.get()))))
+ .with_reason(
+ attr_line_t("no patterns have a capture named ")
+ .append_quoted(vd->vd_meta.lvm_name.get()))
+ .with_note(notes)
+ .with_snippets(this->get_snippets())
+ .with_help("values are populated from captures in "
+ "patterns, so at least one pattern must "
+ "have a capture with this value name"));
+ }
+ }
+
+ for (act_iter = vd->vd_action_list.begin();
+ act_iter != vd->vd_action_list.end();
+ ++act_iter)
+ {
+ if (this->lf_action_defs.find(*act_iter)
+ == this->lf_action_defs.end())
+ {
+#if 0
+ errors.push_back("error:" + this->elf_name.to_string() + ":"
+ + vd->vd_meta.lvm_name.get()
+ + ": cannot find action -- " + (*act_iter));
+#endif
+ }
+ }
+
+ vd->set_rewrite_src_name();
+ }
+
+ for (const auto& td_pair : this->lf_tag_defs) {
+ const auto& td = td_pair.second;
+
+ if (td->ftd_pattern.pp_value == nullptr
+ || td->ftd_pattern.pp_value->get_pattern().empty())
+ {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("invalid tag definition ")
+ .append_quoted(lnav::roles::symbol(
+ fmt::format(FMT_STRING("/{}/tags/{}"),
+ this->elf_name,
+ td_pair.first))))
+ .with_reason(
+ "tag definitions must have a non-empty pattern")
+ .with_snippets(this->get_snippets()));
+ }
+ }
+
+ if (this->elf_type == elf_type_t::ELF_TYPE_TEXT
+ && this->elf_samples.empty())
+ {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(
+ lnav::roles::symbol(this->elf_name.to_string()))
+ .append(" is not a valid log format"))
+ .with_reason("log message samples must be included in a format "
+ "definition")
+ .with_snippets(this->get_snippets()));
+ }
+
+ if (!this->lf_subsecond_field.empty()
+ && !this->lf_subsecond_unit.has_value())
+ {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(
+ lnav::roles::symbol(this->elf_name.to_string()))
+ .append(" is not a valid log format"))
+ .with_reason(attr_line_t()
+ .append_quoted("subsecond-unit"_symbol)
+ .append(" must be set when ")
+ .append_quoted("subsecond-field"_symbol)
+ .append(" is used"))
+ .with_snippets(this->get_snippets()));
+ }
+
+ for (size_t sample_index = 0; sample_index < this->elf_samples.size();
+ sample_index += 1)
+ {
+ auto& elf_sample = this->elf_samples[sample_index];
+ auto sample_lines
+ = string_fragment(elf_sample.s_line.pp_value).split_lines();
+ bool found = false;
+
+ for (auto pat_iter = this->elf_pattern_order.begin();
+ pat_iter != this->elf_pattern_order.end();
+ ++pat_iter)
+ {
+ auto& pat = *(*pat_iter);
+
+ if (!pat.p_pcre.pp_value) {
+ continue;
+ }
+
+ auto md = pat.p_pcre.pp_value->create_match_data();
+ auto match_res = pat.p_pcre.pp_value->capture_from(sample_lines[0])
+ .into(md)
+ .matches()
+ .ignore_error();
+ if (!match_res) {
+ continue;
+ }
+ found = true;
+
+ if (pat.p_module_format) {
+ continue;
+ }
+
+ elf_sample.s_matched_regexes.insert(pat.p_name.to_string());
+ pat.p_matched_samples.insert(sample_index);
+
+ if (pat.p_pcre.pp_value->name_index(this->lf_timestamp_field.get())
+ < 0)
+ {
+ attr_line_t notes;
+ bool first_note = true;
+
+ if (pat.p_pcre.pp_value->get_capture_count() > 0) {
+ notes.append("the following captures are available:\n ");
+ }
+ for (auto named_cap : pat.p_pcre.pp_value->get_named_captures())
+ {
+ if (!first_note) {
+ notes.append(", ");
+ }
+ notes.append(
+ lnav::roles::symbol(named_cap.get_name().to_string()));
+ first_note = false;
+ }
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("invalid value for property ")
+ .append_quoted(lnav::roles::symbol(
+ fmt::format(FMT_STRING("/{}/timestamp-field"),
+ this->elf_name))))
+ .with_reason(
+ attr_line_t()
+ .append_quoted(this->lf_timestamp_field)
+ .append(" was not found in the pattern at ")
+ .append(lnav::roles::symbol(pat.p_config_path)))
+ .with_note(notes)
+ .with_snippets(this->get_snippets()));
+ continue;
+ }
+
+ const auto ts_cap = md[pat.p_timestamp_field_index];
+ const auto level_cap = md[pat.p_level_field_index];
+ const char* const* custom_formats = this->get_timestamp_formats();
+ date_time_scanner dts;
+ struct timeval tv;
+ struct exttm tm;
+
+ if (ts_cap && ts_cap->sf_begin == 0) {
+ pat.p_timestamp_end = ts_cap->sf_end;
+ }
+ if (ts_cap
+ && dts.scan(ts_cap->data(),
+ ts_cap->length(),
+ custom_formats,
+ &tm,
+ tv)
+ == nullptr)
+ {
+ attr_line_t notes;
+
+ if (custom_formats == nullptr) {
+ notes.append("the following built-in formats were tried:");
+ for (int lpc = 0; PTIMEC_FORMATS[lpc].pf_fmt != nullptr;
+ lpc++)
+ {
+ off_t off = 0;
+
+ PTIMEC_FORMATS[lpc].pf_func(
+ &tm, ts_cap->data(), off, ts_cap->length());
+ notes.append("\n ")
+ .append(ts_cap.value())
+ .append("\n")
+ .append(2 + off, ' ')
+ .append("^ "_snippet_border)
+ .append_quoted(
+ lnav::roles::symbol(PTIMEC_FORMATS[lpc].pf_fmt))
+ .append(" matched up to here"_snippet_border);
+ }
+ } else {
+ notes.append("the following custom formats were tried:");
+ for (int lpc = 0; custom_formats[lpc] != nullptr; lpc++) {
+ off_t off = 0;
+
+ ptime_fmt(custom_formats[lpc],
+ &tm,
+ ts_cap->data(),
+ off,
+ ts_cap->length());
+ notes.append("\n ")
+ .append(ts_cap.value())
+ .append("\n")
+ .append(2 + off, ' ')
+ .append("^ "_snippet_border)
+ .append_quoted(
+ lnav::roles::symbol(custom_formats[lpc]))
+ .append(" matched up to here"_snippet_border);
+ }
+ }
+
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("invalid sample log message: ")
+ .append(lnav::to_json(elf_sample.s_line.pp_value)))
+ .with_reason(attr_line_t("unrecognized timestamp -- ")
+ .append(ts_cap.value()))
+ .with_snippet(elf_sample.s_line.to_snippet())
+ .with_note(notes)
+ .with_help(attr_line_t("If the timestamp format is not "
+ "supported by default, you can "
+ "add a custom format with the ")
+ .append_quoted("timestamp-format"_symbol)
+ .append(" property")));
+ }
+
+ auto level = this->convert_level(
+ level_cap.value_or(string_fragment::invalid()), nullptr);
+
+ if (elf_sample.s_level != LEVEL_UNKNOWN
+ && elf_sample.s_level != level)
+ {
+ attr_line_t note_al;
+
+ note_al.append("matched regex = ")
+ .append(lnav::roles::symbol(pat.p_name.to_string()))
+ .append("\n")
+ .append("captured level = ")
+ .append_quoted(level_cap->to_string());
+ if (level_cap && !this->elf_level_patterns.empty()) {
+ static thread_local auto md
+ = lnav::pcre2pp::match_data::unitialized();
+
+ note_al.append("\nlevel regular expression match results:");
+ for (const auto& level_pattern : this->elf_level_patterns) {
+ attr_line_t regex_al = level_pattern.second.lp_pcre
+ .pp_value->get_pattern();
+ lnav::snippets::regex_highlighter(
+ regex_al,
+ -1,
+ line_range{0, (int) regex_al.length()});
+ note_al.append("\n ")
+ .append(
+ lnav::roles::symbol(level_pattern.second.lp_pcre
+ .pp_path.to_string()))
+ .append(" = ")
+ .append(regex_al)
+ .append("\n ");
+ auto match_res = level_pattern.second.lp_pcre.pp_value
+ ->capture_from(level_cap.value())
+ .into(md)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (!match_res) {
+ note_al.append(lnav::roles::warning("no match"));
+ continue;
+ }
+
+ note_al.append(level_cap.value())
+ .append("\n ")
+ .append(md.leading().length(), ' ')
+ .append("^"_snippet_border);
+ if (match_res->f_all.length() > 2) {
+ note_al.append(
+ lnav::roles::snippet_border(std::string(
+ match_res->f_all.length() - 2, '-')));
+ }
+ if (match_res->f_all.length() > 1) {
+ note_al.append("^"_snippet_border);
+ }
+ }
+ }
+ auto um
+ = lnav::console::user_message::error(
+ attr_line_t("invalid sample log message: ")
+ .append(
+ lnav::to_json(elf_sample.s_line.pp_value)))
+ .with_reason(
+ attr_line_t()
+ .append_quoted(
+ lnav::roles::symbol(level_names[level]))
+ .append(" does not match the expected "
+ "level of ")
+ .append_quoted(lnav::roles::symbol(
+ level_names[elf_sample.s_level])))
+ .with_snippet(elf_sample.s_line.to_snippet())
+ .with_note(note_al);
+ if (!this->elf_level_patterns.empty()) {
+ um.with_help(
+ attr_line_t("Level regexes are not anchored to the "
+ "start/end of the string. Prepend ")
+ .append_quoted("^"_symbol)
+ .append(" to the expression to match from the "
+ "start of the string and append ")
+ .append_quoted("$"_symbol)
+ .append(" to match up to the end of the string."));
+ }
+ errors.emplace_back(um);
+ }
+
+ {
+ auto full_match_res
+ = pat.p_pcre.pp_value
+ ->capture_from(elf_sample.s_line.pp_value)
+ .into(md)
+ .matches()
+ .ignore_error();
+ if (!full_match_res) {
+ attr_line_t regex_al = pat.p_pcre.pp_value->get_pattern();
+ lnav::snippets::regex_highlighter(
+ regex_al, -1, line_range{0, (int) regex_al.length()});
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("invalid pattern: ")
+ .append_quoted(lnav::roles::symbol(
+ pat.p_name.to_string())))
+ .with_reason("pattern does not match entire "
+ "multiline sample message")
+ .with_snippet(elf_sample.s_line.to_snippet())
+ .with_note(attr_line_t()
+ .append(lnav::roles::symbol(
+ pat.p_name.to_string()))
+ .append(" = ")
+ .append(regex_al))
+ .with_help(
+ attr_line_t("use ").append_quoted(".*").append(
+ " to match new-lines")));
+ } else if (static_cast<size_t>(full_match_res->f_all.length())
+ != elf_sample.s_line.pp_value.length())
+ {
+ attr_line_t regex_al = pat.p_pcre.pp_value->get_pattern();
+ lnav::snippets::regex_highlighter(
+ regex_al, -1, line_range{0, (int) regex_al.length()});
+ auto match_length
+ = static_cast<size_t>(full_match_res->f_all.length());
+ attr_line_t sample_al = elf_sample.s_line.pp_value;
+ sample_al.append("\n")
+ .append(match_length, ' ')
+ .append("^ matched up to here"_error)
+ .with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ auto sample_snippet = lnav::console::snippet::from(
+ elf_sample.s_line.pp_location, sample_al);
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("invalid pattern: ")
+ .append_quoted(lnav::roles::symbol(
+ pat.p_name.to_string())))
+ .with_reason("pattern does not match entire "
+ "message")
+ .with_snippet(sample_snippet)
+ .with_note(attr_line_t()
+ .append(lnav::roles::symbol(
+ pat.p_name.to_string()))
+ .append(" = ")
+ .append(regex_al))
+ .with_help("update the regular expression to fully "
+ "capture the sample message"));
+ }
+ }
+ }
+
+ if (!found && !this->elf_pattern_order.empty()) {
+ std::vector<std::pair<ssize_t, intern_string_t>> partial_indexes;
+ attr_line_t notes;
+ size_t max_name_width = 0;
+
+ for (const auto& pat_iter : this->elf_pattern_order) {
+ auto& pat = *pat_iter;
+
+ if (!pat.p_pcre.pp_value) {
+ continue;
+ }
+
+ partial_indexes.emplace_back(
+ pat.p_pcre.pp_value->match_partial(sample_lines[0]),
+ pat.p_name);
+ max_name_width = std::max(max_name_width, pat.p_name.size());
+ }
+ for (const auto& line_frag : sample_lines) {
+ auto src_line = attr_line_t(line_frag.to_string());
+ if (!line_frag.endswith("\n")) {
+ src_line.append("\n");
+ }
+ src_line.with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ notes.append(" ").append(src_line);
+ for (auto& part_pair : partial_indexes) {
+ if (part_pair.first >= 0
+ && part_pair.first < line_frag.length())
+ {
+ notes.append(" ")
+ .append(part_pair.first, ' ')
+ .append("^ "_snippet_border)
+ .append(lnav::roles::symbol(
+ part_pair.second.to_string()))
+ .append(" matched up to here"_snippet_border)
+ .append("\n");
+ }
+ part_pair.first -= line_frag.length();
+ }
+ }
+ notes.add_header(
+ "the following shows how each pattern matched this sample:\n");
+
+ attr_line_t regex_note;
+ for (const auto& pat_iter : this->elf_pattern_order) {
+ if (!pat_iter->p_pcre.pp_value) {
+ regex_note
+ .append(
+ lnav::roles::symbol(fmt::format(FMT_STRING("{:{}}"),
+ pat_iter->p_name,
+ max_name_width)))
+ .append(" is invalid");
+ continue;
+ }
+
+ attr_line_t regex_al = pat_iter->p_pcre.pp_value->get_pattern();
+ lnav::snippets::regex_highlighter(
+ regex_al, -1, line_range{0, (int) regex_al.length()});
+
+ regex_note
+ .append(lnav::roles::symbol(fmt::format(
+ FMT_STRING("{:{}}"), pat_iter->p_name, max_name_width)))
+ .append(" = ")
+ .append_quoted(regex_al)
+ .append("\n");
+ }
+
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("invalid sample log message: ")
+ .append(lnav::to_json(elf_sample.s_line.pp_value)))
+ .with_reason("sample does not match any patterns")
+ .with_snippet(elf_sample.s_line.to_snippet())
+ .with_note(notes.rtrim())
+ .with_note(regex_note));
+ }
+ }
+
+ if (!this->elf_samples.empty()) {
+ for (const auto& elf_sample : this->elf_samples) {
+ if (elf_sample.s_matched_regexes.size() <= 1) {
+ continue;
+ }
+
+ errors.emplace_back(
+ lnav::console::user_message::warning(
+ attr_line_t("invalid log format: ")
+ .append_quoted(
+ lnav::roles::symbol(this->elf_name.to_string())))
+ .with_reason(
+ attr_line_t(
+ "sample is matched by more than one regex: ")
+ .join(elf_sample.s_matched_regexes,
+ VC_ROLE.value(role_t::VCR_SYMBOL),
+ ", "))
+ .with_snippet(lnav::console::snippet::from(
+ elf_sample.s_line.pp_location,
+ attr_line_t().append(lnav::roles::quoted_code(
+ elf_sample.s_line.pp_value))))
+ .with_help("log format regexes must match a single type "
+ "of log message"));
+ }
+
+ for (const auto& pat : this->elf_pattern_order) {
+ if (pat->p_module_format) {
+ continue;
+ }
+
+ if (pat->p_matched_samples.empty()) {
+ errors.emplace_back(
+ lnav::console::user_message::warning(
+ attr_line_t("invalid pattern: ")
+ .append_quoted(
+ lnav::roles::symbol(pat->p_config_path)))
+ .with_reason("pattern does not match any samples")
+ .with_snippet(lnav::console::snippet::from(
+ pat->p_pcre.pp_location, ""))
+ .with_help(
+ "every pattern should have at least one sample "
+ "that it matches"));
+ }
+ }
+ }
+
+ size_t value_def_index = 0;
+ for (auto& elf_value_def : this->elf_value_def_order) {
+ elf_value_def->vd_meta.lvm_values_index
+ = nonstd::make_optional(value_def_index++);
+
+ if (elf_value_def->vd_foreign_key
+ || elf_value_def->vd_meta.lvm_identifier)
+ {
+ continue;
+ }
+
+ switch (elf_value_def->vd_meta.lvm_kind) {
+ case value_kind_t::VALUE_INTEGER:
+ case value_kind_t::VALUE_FLOAT:
+ this->elf_numeric_value_defs.push_back(elf_value_def);
+ break;
+ default:
+ break;
+ }
+ }
+
+ int format_index = 0;
+ for (auto iter = this->jlf_line_format.begin();
+ iter != this->jlf_line_format.end();
+ ++iter, format_index++)
+ {
+ static const intern_string_t ts
+ = intern_string::lookup("__timestamp__");
+ static const intern_string_t level_field
+ = intern_string::lookup("__level__");
+ json_format_element& jfe = *iter;
+
+ if (startswith(jfe.jfe_value.pp_value.get(), "/")) {
+ jfe.jfe_value.pp_value
+ = intern_string::lookup(jfe.jfe_value.pp_value.get() + 1);
+ }
+ if (!jfe.jfe_ts_format.empty()) {
+ if (!jfe.jfe_value.pp_value.empty() && jfe.jfe_value.pp_value != ts)
+ {
+ log_warning(
+ "%s:line-format[%d]:ignoring field '%s' since "
+ "timestamp-format was used",
+ this->elf_name.get(),
+ format_index,
+ jfe.jfe_value.pp_value.get());
+ }
+ jfe.jfe_value.pp_value = ts;
+ }
+
+ switch (jfe.jfe_type) {
+ case json_log_field::VARIABLE: {
+ auto vd_iter
+ = this->elf_value_defs.find(jfe.jfe_value.pp_value);
+ if (jfe.jfe_value.pp_value == ts) {
+ this->elf_value_defs[this->lf_timestamp_field]
+ ->vd_meta.lvm_hidden
+ = true;
+ } else if (jfe.jfe_value.pp_value == level_field) {
+ this->elf_value_defs[this->elf_level_field]
+ ->vd_meta.lvm_hidden
+ = true;
+ } else if (vd_iter == this->elf_value_defs.end()) {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("invalid line format element ")
+ .append_quoted(lnav::roles::symbol(fmt::format(
+ FMT_STRING("/{}/line-format/{}/field"),
+ this->elf_name,
+ format_index))))
+ .with_reason(
+ attr_line_t()
+ .append_quoted(jfe.jfe_value.pp_value)
+ .append(" is not a defined value"))
+ .with_snippet(jfe.jfe_value.to_snippet()));
+ } else {
+ switch (vd_iter->second->vd_meta.lvm_kind) {
+ case value_kind_t::VALUE_INTEGER:
+ case value_kind_t::VALUE_FLOAT:
+ if (jfe.jfe_align
+ == json_format_element::align_t::NONE)
+ {
+ jfe.jfe_align
+ = json_format_element::align_t::RIGHT;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+ }
+ case json_log_field::CONSTANT:
+ this->jlf_line_format_init_count
+ += std::count(jfe.jfe_default_value.begin(),
+ jfe.jfe_default_value.end(),
+ '\n');
+ break;
+ default:
+ break;
+ }
+ }
+
+ for (auto& hd_pair : this->elf_highlighter_patterns) {
+ external_log_format::highlighter_def& hd = hd_pair.second;
+ auto fg = styling::color_unit::make_empty();
+ auto bg = styling::color_unit::make_empty();
+ text_attrs attrs;
+
+ if (!hd.hd_color.pp_value.empty()) {
+ fg = styling::color_unit::from_str(hd.hd_color.pp_value)
+ .unwrapOrElse([&](const auto& msg) {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(hd.hd_color.pp_value)
+ .append(" is not a valid color value for "
+ "property ")
+ .append_quoted(lnav::roles::symbol(
+ hd.hd_color.pp_path.to_string())))
+ .with_reason(msg)
+ .with_snippet(hd.hd_color.to_snippet()));
+ return styling::color_unit::make_empty();
+ });
+ }
+
+ if (!hd.hd_background_color.pp_value.empty()) {
+ bg = styling::color_unit::from_str(hd.hd_background_color.pp_value)
+ .unwrapOrElse([&](const auto& msg) {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(
+ hd.hd_background_color.pp_value)
+ .append(" is not a valid color value for "
+ "property ")
+ .append_quoted(lnav::roles::symbol(
+ hd.hd_background_color.pp_path
+ .to_string())))
+ .with_reason(msg)
+ .with_snippet(
+ hd.hd_background_color.to_snippet()));
+ return styling::color_unit::make_empty();
+ });
+ }
+
+ if (hd.hd_underline) {
+ attrs.ta_attrs |= A_UNDERLINE;
+ }
+ if (hd.hd_blink) {
+ attrs.ta_attrs |= A_BLINK;
+ }
+
+ if (hd.hd_pattern.pp_value != nullptr) {
+ this->lf_highlighters.emplace_back(hd.hd_pattern.pp_value);
+ this->lf_highlighters.back()
+ .with_name(hd_pair.first.to_string())
+ .with_format_name(this->elf_name)
+ .with_color(fg, bg)
+ .with_attrs(attrs);
+ }
+ }
+
+ this->lf_value_stats.resize(this->elf_value_defs.size());
+}
+
+void
+external_log_format::register_vtabs(
+ log_vtab_manager* vtab_manager,
+ std::vector<lnav::console::user_message>& errors)
+{
+ for (auto& elf_search_table : this->elf_search_tables) {
+ if (elf_search_table.second.std_pattern.pp_value == nullptr) {
+ continue;
+ }
+
+ auto lst = std::make_shared<log_search_table>(
+ elf_search_table.second.std_pattern.pp_value,
+ elf_search_table.first);
+ lst->lst_format = this;
+ lst->lst_log_path_glob = elf_search_table.second.std_glob;
+ if (elf_search_table.second.std_level != LEVEL_UNKNOWN) {
+ lst->lst_log_level = elf_search_table.second.std_level;
+ }
+ auto errmsg = vtab_manager->register_vtab(lst);
+ if (!errmsg.empty()) {
+#if 0
+ errors.push_back("error:" + this->elf_name.to_string() + ":"
+ + search_iter->first.to_string()
+ + ":unable to register table -- " + errmsg);
+#endif
+ }
+ }
+}
+
+bool
+external_log_format::match_samples(const std::vector<sample>& samples) const
+{
+ for (const auto& sample_iter : samples) {
+ for (const auto& pat_iter : this->elf_pattern_order) {
+ auto& pat = *pat_iter;
+
+ if (!pat.p_pcre.pp_value) {
+ continue;
+ }
+
+ if (pat.p_pcre.pp_value->find_in(sample_iter.s_line.pp_value)
+ .ignore_error())
+ {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+class external_log_table : public log_format_vtab_impl {
+public:
+ explicit external_log_table(const external_log_format& elf)
+ : log_format_vtab_impl(elf), elt_format(elf)
+ {
+ }
+
+ void get_columns(std::vector<vtab_column>& cols) const override
+ {
+ const auto& elf = this->elt_format;
+
+ cols.resize(elf.elf_column_count);
+ for (const auto& vd : elf.elf_value_def_order) {
+ auto type_pair = log_vtab_impl::logline_value_to_sqlite_type(
+ vd->vd_meta.lvm_kind);
+
+ if (vd->vd_meta.lvm_column == -1) {
+ continue;
+ }
+
+ require(0 <= vd->vd_meta.lvm_column
+ && vd->vd_meta.lvm_column < elf.elf_column_count);
+
+ cols[vd->vd_meta.lvm_column].vc_name = vd->vd_meta.lvm_name.get();
+ cols[vd->vd_meta.lvm_column].vc_type = type_pair.first;
+ cols[vd->vd_meta.lvm_column].vc_subtype = type_pair.second;
+ cols[vd->vd_meta.lvm_column].vc_collator = vd->vd_collate;
+ cols[vd->vd_meta.lvm_column].vc_comment = vd->vd_description;
+ }
+ }
+
+ void get_foreign_keys(std::vector<std::string>& keys_inout) const override
+ {
+ log_vtab_impl::get_foreign_keys(keys_inout);
+
+ for (const auto& elf_value_def : this->elt_format.elf_value_defs) {
+ if (elf_value_def.second->vd_foreign_key) {
+ keys_inout.emplace_back(elf_value_def.first.to_string());
+ }
+ }
+ }
+
+ bool next(log_cursor& lc, logfile_sub_source& lss) override
+ {
+ if (lc.is_eof()) {
+ return true;
+ }
+
+ content_line_t cl(lss.at(lc.lc_curr_line));
+ auto* lf = lss.find_file_ptr(cl);
+ auto lf_iter = lf->begin() + cl;
+ uint8_t mod_id = lf_iter->get_module_id();
+
+ if (lf_iter->is_continued()) {
+ return false;
+ }
+
+ this->elt_module_format.mf_mod_format = nullptr;
+ if (lf->get_format_name() == this->lfvi_format.get_name()) {
+ return true;
+ } else if (mod_id && mod_id == this->lfvi_format.lf_mod_index) {
+ auto format = lf->get_format();
+
+ return lf->read_line(lf_iter)
+ .map([this, format, cl](auto line) {
+ logline_value_vector values;
+ struct line_range mod_name_range;
+ intern_string_t mod_name;
+
+ this->vi_attrs.clear();
+ values.lvv_sbr = line;
+ format->annotate(cl, this->vi_attrs, values, false);
+ this->elt_container_body
+ = find_string_attr_range(this->vi_attrs, &SA_BODY);
+ if (!this->elt_container_body.is_valid()) {
+ return false;
+ }
+ this->elt_container_body.ltrim(line.get_data());
+ mod_name_range = find_string_attr_range(this->vi_attrs,
+ &logline::L_MODULE);
+ if (!mod_name_range.is_valid()) {
+ return false;
+ }
+ mod_name = intern_string::lookup(
+ &line.get_data()[mod_name_range.lr_start],
+ mod_name_range.length());
+ this->vi_attrs.clear();
+ this->elt_module_format
+ = external_log_format::MODULE_FORMATS[mod_name];
+ if (!this->elt_module_format.mf_mod_format) {
+ return false;
+ }
+ return this->elt_module_format.mf_mod_format->get_name()
+ == this->lfvi_format.get_name();
+ })
+ .unwrapOr(false);
+ }
+
+ return false;
+ }
+
+ void extract(logfile* lf,
+ uint64_t line_number,
+ logline_value_vector& values) override
+ {
+ auto& line = values.lvv_sbr;
+ auto format = lf->get_format();
+
+ if (this->elt_module_format.mf_mod_format != nullptr) {
+ shared_buffer_ref body_ref;
+
+ body_ref.subset(line,
+ this->elt_container_body.lr_start,
+ this->elt_container_body.length());
+ this->vi_attrs.clear();
+ auto narrow_res
+ = values.lvv_sbr.narrow(this->elt_container_body.lr_start,
+ this->elt_container_body.length());
+ values.lvv_values.clear();
+ this->elt_module_format.mf_mod_format->annotate(
+ line_number, this->vi_attrs, values, false);
+ values.lvv_sbr.widen(narrow_res);
+ } else {
+ this->vi_attrs.clear();
+ format->annotate(line_number, this->vi_attrs, values, false);
+ }
+ }
+
+ const external_log_format& elt_format;
+ module_format elt_module_format;
+ struct line_range elt_container_body;
+};
+
+std::shared_ptr<log_vtab_impl>
+external_log_format::get_vtab_impl() const
+{
+ return std::make_shared<external_log_table>(*this);
+}
+
+std::shared_ptr<log_format>
+external_log_format::specialized(int fmt_lock)
+{
+ auto retval = std::make_shared<external_log_format>(*this);
+
+ retval->lf_specialized = true;
+ this->lf_pattern_locks.clear();
+ if (fmt_lock != -1) {
+ retval->lf_pattern_locks.emplace_back(0, fmt_lock);
+ }
+
+ if (this->elf_type == elf_type_t::ELF_TYPE_JSON) {
+ this->jlf_parse_context
+ = std::make_shared<yajlpp_parse_context>(this->elf_name);
+ this->jlf_yajl_handle.reset(
+ yajl_alloc(&this->jlf_parse_context->ypc_callbacks,
+ nullptr,
+ this->jlf_parse_context.get()),
+ yajl_handle_deleter());
+ yajl_config(this->jlf_yajl_handle.get(), yajl_dont_validate_strings, 1);
+ this->jlf_cached_line.reserve(16 * 1024);
+ }
+
+ this->lf_value_stats.clear();
+ this->lf_value_stats.resize(this->elf_value_defs.size());
+ this->elf_specialized_value_defs_state = *this->elf_value_defs_state;
+
+ return retval;
+}
+
+bool
+external_log_format::match_name(const std::string& filename)
+{
+ if (this->elf_filename_pcre.pp_value == nullptr) {
+ return true;
+ }
+
+ return this->elf_filename_pcre.pp_value->find_in(filename)
+ .ignore_error()
+ .has_value();
+}
+
+bool
+external_log_format::match_mime_type(const file_format_t ff) const
+{
+ if (ff == file_format_t::UNKNOWN && this->elf_mime_types.empty()) {
+ return true;
+ }
+
+ return this->elf_mime_types.count(ff) == 1;
+}
+
+auto
+external_log_format::value_line_count(const intern_string_t ist,
+ bool top_level,
+ nonstd::optional<double> val,
+ const unsigned char* str,
+ ssize_t len) -> value_line_count_result
+{
+ const auto iter = this->elf_value_defs.find(ist);
+ value_line_count_result retval;
+ if (str != nullptr) {
+ auto frag = string_fragment::from_bytes(str, len);
+ while (frag.endswith("\n")) {
+ frag.pop_back();
+ }
+ while (!frag.empty()) {
+ auto utf_res = is_utf8(frag, '\n');
+ if (!utf_res.is_valid()) {
+ retval.vlcr_valid_utf = false;
+ }
+ retval.vlcr_has_ansi |= utf_res.usr_has_ansi;
+ if (!utf_res.usr_remaining) {
+ break;
+ }
+ frag = utf_res.usr_remaining.value();
+ retval.vlcr_count += 1;
+ }
+ }
+
+ if (iter == this->elf_value_defs.end()) {
+ if (this->jlf_hide_extra || !top_level) {
+ retval.vlcr_count = 0;
+ }
+
+ return retval;
+ }
+
+ if (iter->second->vd_meta.lvm_values_index) {
+ auto& lvs = this->lf_value_stats[iter->second->vd_meta.lvm_values_index
+ .value()];
+ if (len > lvs.lvs_width) {
+ lvs.lvs_width = len;
+ }
+ if (val) {
+ lvs.add_value(val.value());
+ }
+ }
+
+ if (std::find_if(this->jlf_line_format.begin(),
+ this->jlf_line_format.end(),
+ json_field_cmp(json_log_field::VARIABLE, ist))
+ != this->jlf_line_format.end())
+ {
+ retval.vlcr_line_format_count += 1;
+ retval.vlcr_count -= 1;
+ }
+
+ if (iter->second->vd_meta.is_hidden()) {
+ retval.vlcr_count = 0;
+ }
+
+ return retval;
+}
+
+log_level_t
+external_log_format::convert_level(string_fragment sf,
+ scan_batch_context* sbc) const
+{
+ log_level_t retval = LEVEL_INFO;
+
+ if (sf.is_valid()) {
+ if (sbc != nullptr && sbc->sbc_cached_level_count > 0) {
+ auto cached_level_iter
+ = std::find(std::begin(sbc->sbc_cached_level_strings),
+ std::begin(sbc->sbc_cached_level_strings)
+ + sbc->sbc_cached_level_count,
+ sf);
+ if (cached_level_iter
+ != std::begin(sbc->sbc_cached_level_strings)
+ + sbc->sbc_cached_level_count)
+ {
+ auto cache_index
+ = std::distance(std::begin(sbc->sbc_cached_level_strings),
+ cached_level_iter);
+ if (cache_index != 0) {
+ std::swap(sbc->sbc_cached_level_strings[cache_index],
+ sbc->sbc_cached_level_strings[0]);
+ std::swap(sbc->sbc_cached_level_values[cache_index],
+ sbc->sbc_cached_level_values[0]);
+ }
+ return sbc->sbc_cached_level_values[0];
+ }
+ }
+
+ if (this->elf_level_patterns.empty()) {
+ retval = string2level(sf.data(), sf.length());
+ } else {
+ for (const auto& elf_level_pattern : this->elf_level_patterns) {
+ if (elf_level_pattern.second.lp_pcre.pp_value
+ ->find_in(sf, PCRE2_NO_UTF_CHECK)
+ .ignore_error()
+ .has_value())
+ {
+ retval = elf_level_pattern.first;
+ break;
+ }
+ }
+ }
+
+ if (sbc != nullptr && sf.length() < 10) {
+ size_t cache_index;
+
+ if (sbc->sbc_cached_level_count == 4) {
+ cache_index = sbc->sbc_cached_level_count - 1;
+ } else {
+ cache_index = sbc->sbc_cached_level_count;
+ sbc->sbc_cached_level_count += 1;
+ }
+ sbc->sbc_cached_level_strings[cache_index] = sf.to_string();
+ sbc->sbc_cached_level_values[cache_index] = retval;
+ }
+ }
+
+ return retval;
+}
+
+logline_value_meta
+external_log_format::get_value_meta(intern_string_t field_name,
+ value_kind_t kind)
+{
+ auto iter = this->elf_value_defs.find(field_name);
+
+ if (iter == this->elf_value_defs.end()) {
+ auto retval = logline_value_meta(field_name, kind, -1, this);
+
+ retval.lvm_hidden = this->jlf_hide_extra;
+ return retval;
+ }
+
+ auto lvm = iter->second->vd_meta;
+
+ lvm.lvm_kind = kind;
+ return lvm;
+}
+
+void
+external_log_format::json_append(
+ const external_log_format::json_format_element& jfe,
+ const value_def* vd,
+ const char* value,
+ ssize_t len)
+{
+ if (len == -1) {
+ len = strlen(value);
+ }
+ if (jfe.jfe_align == json_format_element::align_t::RIGHT) {
+ if (len < jfe.jfe_min_width) {
+ this->json_append_to_cache(jfe.jfe_min_width - len);
+ } else if (jfe.jfe_auto_width && vd != nullptr
+ && len < this->lf_value_stats[vd->vd_meta.lvm_values_index
+ .value()]
+ .lvs_width)
+ {
+ this->json_append_to_cache(
+ this->lf_value_stats[vd->vd_meta.lvm_values_index.value()]
+ .lvs_width
+ - len);
+ }
+ }
+ this->json_append_to_cache(value, len);
+ if (jfe.jfe_align == json_format_element::align_t::LEFT
+ || jfe.jfe_align == json_format_element::align_t::NONE)
+ {
+ if (len < jfe.jfe_min_width) {
+ this->json_append_to_cache(jfe.jfe_min_width - len);
+ } else if (jfe.jfe_auto_width && vd != nullptr
+ && len < this->lf_value_stats[vd->vd_meta.lvm_values_index
+ .value()]
+ .lvs_width)
+ {
+ this->json_append_to_cache(
+ this->lf_value_stats[vd->vd_meta.lvm_values_index.value()]
+ .lvs_width
+ - len);
+ }
+ }
+}
+
+intern_string_t
+external_log_format::get_pattern_name(uint64_t line_number) const
+{
+ if (this->elf_type != elf_type_t::ELF_TYPE_TEXT) {
+ static auto structured = intern_string::lookup("structured");
+
+ return structured;
+ }
+ int pat_index = this->pattern_index_for_line(line_number);
+ return this->elf_pattern_order[pat_index]->p_name;
+}
+
+int
+log_format::pattern_index_for_line(uint64_t line_number) const
+{
+ auto iter = lower_bound(this->lf_pattern_locks.cbegin(),
+ this->lf_pattern_locks.cend(),
+ line_number,
+ [](const pattern_for_lines& pfl, uint32_t line) {
+ return pfl.pfl_line < line;
+ });
+
+ if (iter == this->lf_pattern_locks.end() || iter->pfl_line != line_number) {
+ --iter;
+ }
+
+ return iter->pfl_pat_index;
+}
+
+std::string
+log_format::get_pattern_path(uint64_t line_number) const
+{
+ int pat_index = this->pattern_index_for_line(line_number);
+ return fmt::format(FMT_STRING("builtin ({})"), pat_index);
+}
+
+intern_string_t
+log_format::get_pattern_name(uint64_t line_number) const
+{
+ char pat_str[128];
+
+ int pat_index = this->pattern_index_for_line(line_number);
+ snprintf(pat_str, sizeof(pat_str), "builtin (%d)", pat_index);
+ return intern_string::lookup(pat_str);
+}
+
+std::shared_ptr<log_format>
+log_format::find_root_format(const char* name)
+{
+ auto& fmts = get_root_formats();
+ for (auto& lf : fmts) {
+ if (lf->get_name() == name) {
+ return lf;
+ }
+ }
+ return nullptr;
+}
+
+log_format::pattern_for_lines::pattern_for_lines(uint32_t pfl_line,
+ uint32_t pfl_pat_index)
+ : pfl_line(pfl_line), pfl_pat_index(pfl_pat_index)
+{
+}
+
+void
+logline_value_stats::merge(const logline_value_stats& other)
+{
+ if (other.lvs_count == 0) {
+ return;
+ }
+
+ require(other.lvs_min_value <= other.lvs_max_value);
+
+ if (other.lvs_width > this->lvs_width) {
+ this->lvs_width = other.lvs_width;
+ }
+ if (other.lvs_min_value < this->lvs_min_value) {
+ this->lvs_min_value = other.lvs_min_value;
+ }
+ if (other.lvs_max_value > this->lvs_max_value) {
+ this->lvs_max_value = other.lvs_max_value;
+ }
+ this->lvs_count += other.lvs_count;
+ this->lvs_total += other.lvs_total;
+
+ ensure(this->lvs_count >= 0);
+ ensure(this->lvs_min_value <= this->lvs_max_value);
+}
+
+void
+logline_value_stats::add_value(double value)
+{
+ if (value < this->lvs_min_value) {
+ this->lvs_min_value = value;
+ }
+ if (value > this->lvs_max_value) {
+ this->lvs_max_value = value;
+ }
+ this->lvs_count += 1;
+ this->lvs_total += value;
+}
+
+std::vector<logline_value_meta>
+external_log_format::get_value_metadata() const
+{
+ std::vector<logline_value_meta> retval;
+
+ for (const auto& vd : this->elf_value_def_order) {
+ retval.emplace_back(vd->vd_meta);
+ }
+
+ return retval;
+}
+
+const logline_value_stats*
+external_log_format::stats_for_value(const intern_string_t& name) const
+{
+ auto iter = this->elf_value_defs.find(name);
+ if (iter != this->elf_value_defs.end()
+ && iter->second->vd_meta.lvm_values_index)
+ {
+ return &this->lf_value_stats[iter->second->vd_meta.lvm_values_index
+ .value()];
+ }
+
+ return nullptr;
+}
+
+std::string
+external_log_format::get_pattern_regex(uint64_t line_number) const
+{
+ if (this->elf_type != elf_type_t::ELF_TYPE_TEXT) {
+ return "";
+ }
+ int pat_index = this->pattern_index_for_line(line_number);
+ return this->elf_pattern_order[pat_index]->p_pcre.pp_value->get_pattern();
+}
+
+bool
+external_log_format::hide_field(const intern_string_t field_name, bool val)
+{
+ auto vd_iter = this->elf_value_defs.find(field_name);
+
+ if (vd_iter == this->elf_value_defs.end()) {
+ return false;
+ }
+
+ vd_iter->second->vd_meta.lvm_user_hidden = val;
+ if (this->elf_type == elf_type_t::ELF_TYPE_JSON) {
+ bool found = false;
+
+ for (const auto& jfe : this->jlf_line_format) {
+ if (jfe.jfe_value.pp_value == field_name) {
+ found = true;
+ }
+ }
+ if (!found) {
+ log_info("format field %s.%s changed, rebuilding",
+ this->elf_name.get(),
+ field_name.get());
+ this->elf_value_defs_state->vds_generation += 1;
+ }
+ }
+ return true;
+}
+
+bool
+external_log_format::format_changed()
+{
+ if (this->elf_specialized_value_defs_state.vds_generation
+ != this->elf_value_defs_state->vds_generation)
+ {
+ this->elf_specialized_value_defs_state = *this->elf_value_defs_state;
+ this->jlf_cached_offset = -1;
+ return true;
+ }
+
+ return false;
+}
+
+bool
+format_tag_def::path_restriction::matches(const char* fn) const
+{
+ return fnmatch(this->p_glob.c_str(), fn, 0) == 0;
+}
+
+/* XXX */
+#include "log_format_impls.cc"
diff --git a/src/log_format.hh b/src/log_format.hh
new file mode 100644
index 0000000..fdca056
--- /dev/null
+++ b/src/log_format.hh
@@ -0,0 +1,579 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_format.hh
+ */
+
+#ifndef log_format_hh
+#define log_format_hh
+
+#include <stdint.h>
+#include <sys/time.h>
+#include <time.h>
+#define __STDC_FORMAT_MACROS
+#include <limits>
+#include <list>
+#include <memory>
+#include <set>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <inttypes.h>
+#include <sys/types.h>
+
+#include "base/date_time_scanner.hh"
+#include "base/intern_string.hh"
+#include "base/lnav_log.hh"
+#include "file_format.hh"
+#include "highlighter.hh"
+#include "line_buffer.hh"
+#include "log_format_fwd.hh"
+#include "log_level.hh"
+#include "optional.hpp"
+#include "pcrepp/pcre2pp.hh"
+#include "shared_buffer.hh"
+
+struct sqlite3;
+class logfile;
+class log_vtab_manager;
+struct exec_context;
+
+enum class scale_op_t {
+ SO_IDENTITY,
+ SO_MULTIPLY,
+ SO_DIVIDE
+};
+
+struct scaling_factor {
+ template<typename T>
+ void scale(T& val) const
+ {
+ switch (this->sf_op) {
+ case scale_op_t::SO_IDENTITY:
+ break;
+ case scale_op_t::SO_DIVIDE:
+ val = val / (T) this->sf_value;
+ break;
+ case scale_op_t::SO_MULTIPLY:
+ val = val * (T) this->sf_value;
+ break;
+ }
+ }
+
+ scale_op_t sf_op{scale_op_t::SO_IDENTITY};
+ double sf_value{1};
+};
+
+enum class value_kind_t : int {
+ VALUE_UNKNOWN = -1,
+ VALUE_NULL,
+ VALUE_TEXT,
+ VALUE_INTEGER,
+ VALUE_FLOAT,
+ VALUE_BOOLEAN,
+ VALUE_JSON,
+ VALUE_STRUCT,
+ VALUE_QUOTED,
+ VALUE_W3C_QUOTED,
+ VALUE_TIMESTAMP,
+ VALUE_XML,
+
+ VALUE__MAX
+};
+
+struct logline_value_meta {
+ logline_value_meta(intern_string_t name,
+ value_kind_t kind,
+ int col = -1,
+ const nonstd::optional<log_format*>& format
+ = nonstd::nullopt)
+ : lvm_name(name), lvm_kind(kind), lvm_column(col), lvm_format(format)
+ {
+ }
+
+ bool is_hidden() const
+ {
+ if (this->lvm_user_hidden) {
+ return this->lvm_user_hidden.value();
+ }
+ return this->lvm_hidden;
+ }
+
+ logline_value_meta& with_struct_name(intern_string_t name)
+ {
+ this->lvm_struct_name = name;
+ return *this;
+ }
+
+ intern_string_t lvm_name;
+ value_kind_t lvm_kind;
+ int lvm_column{-1};
+ nonstd::optional<size_t> lvm_values_index;
+ bool lvm_identifier{false};
+ bool lvm_hidden{false};
+ nonstd::optional<bool> lvm_user_hidden;
+ bool lvm_from_module{false};
+ intern_string_t lvm_struct_name;
+ nonstd::optional<log_format*> lvm_format;
+};
+
+class logline_value {
+public:
+ logline_value(logline_value_meta lvm) : lv_meta(std::move(lvm))
+ {
+ this->lv_meta.lvm_kind = value_kind_t::VALUE_NULL;
+ }
+
+ logline_value(logline_value_meta lvm, bool b)
+ : lv_meta(std::move(lvm)), lv_value((int64_t) (b ? 1 : 0))
+ {
+ this->lv_meta.lvm_kind = value_kind_t::VALUE_BOOLEAN;
+ }
+
+ logline_value(logline_value_meta lvm, int64_t i)
+ : lv_meta(std::move(lvm)), lv_value(i)
+ {
+ this->lv_meta.lvm_kind = value_kind_t::VALUE_INTEGER;
+ }
+
+ logline_value(logline_value_meta lvm, double i)
+ : lv_meta(std::move(lvm)), lv_value(i)
+ {
+ this->lv_meta.lvm_kind = value_kind_t::VALUE_FLOAT;
+ }
+
+ logline_value(logline_value_meta lvm, string_fragment frag)
+ : lv_meta(std::move(lvm)), lv_frag(frag)
+ {
+ }
+
+ logline_value(logline_value_meta lvm, const intern_string_t val)
+ : lv_meta(std::move(lvm)), lv_intern_string(val)
+ {
+ }
+
+ logline_value(logline_value_meta lvm, std::string val)
+ : lv_meta(std::move(lvm)), lv_str(std::move(val))
+ {
+ }
+
+ logline_value(logline_value_meta lvm,
+ shared_buffer_ref& sbr,
+ struct line_range origin);
+
+ void apply_scaling(const scaling_factor* sf)
+ {
+ if (sf != nullptr) {
+ switch (this->lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_INTEGER:
+ sf->scale(this->lv_value.i);
+ break;
+ case value_kind_t::VALUE_FLOAT:
+ sf->scale(this->lv_value.d);
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ std::string to_string() const;
+
+ const char* text_value() const
+ {
+ if (this->lv_str) {
+ return this->lv_str->c_str();
+ }
+ if (this->lv_frag.empty()) {
+ if (this->lv_intern_string.empty()) {
+ return "";
+ }
+ return this->lv_intern_string.get();
+ }
+ return this->lv_frag.data();
+ }
+
+ size_t text_length() const
+ {
+ if (this->lv_str) {
+ return this->lv_str->size();
+ }
+ if (this->lv_frag.empty()) {
+ return this->lv_intern_string.size();
+ }
+ return this->lv_frag.length();
+ }
+
+ struct line_range origin_in_full_msg(const char* msg, ssize_t len) const;
+
+ logline_value_meta lv_meta;
+ union value_u {
+ int64_t i;
+ double d;
+
+ value_u() : i(0) {}
+ value_u(int64_t i) : i(i) {}
+ value_u(double d) : d(d) {}
+ } lv_value;
+ nonstd::optional<std::string> lv_str;
+ string_fragment lv_frag;
+ int lv_sub_offset{0};
+ intern_string_t lv_intern_string;
+ struct line_range lv_origin;
+};
+
+struct logline_value_vector {
+ void clear()
+ {
+ this->lvv_values.clear();
+ this->lvv_sbr.disown();
+ }
+
+ shared_buffer_ref lvv_sbr;
+ std::vector<logline_value> lvv_values;
+};
+
+struct logline_value_stats {
+ logline_value_stats() { this->clear(); }
+
+ void clear()
+ {
+ this->lvs_width = 0;
+ this->lvs_count = 0;
+ this->lvs_total = 0;
+ this->lvs_min_value = std::numeric_limits<double>::max();
+ this->lvs_max_value = -std::numeric_limits<double>::max();
+ }
+
+ void merge(const logline_value_stats& other);
+
+ void add_value(double value);
+
+ int64_t lvs_width;
+ int64_t lvs_count;
+ double lvs_total;
+ double lvs_min_value;
+ double lvs_max_value;
+};
+
+struct logline_value_cmp {
+ explicit logline_value_cmp(const intern_string_t* name = nullptr,
+ int col = -1)
+ : lvc_name(name), lvc_column(col)
+ {
+ }
+
+ bool operator()(const logline_value& lv) const
+ {
+ bool retval = true;
+
+ if (this->lvc_name != nullptr) {
+ retval = retval && ((*this->lvc_name) == lv.lv_meta.lvm_name);
+ }
+ if (this->lvc_column != -1) {
+ retval = retval && (this->lvc_column == lv.lv_meta.lvm_column);
+ }
+
+ return retval;
+ }
+
+ const intern_string_t* lvc_name;
+ int lvc_column;
+};
+
+class log_vtab_impl;
+
+/**
+ * Base class for implementations of log format parsers.
+ */
+class log_format {
+public:
+ /**
+ * @return The collection of builtin log formats.
+ */
+ static std::vector<std::shared_ptr<log_format>>& get_root_formats();
+
+ static std::shared_ptr<log_format> find_root_format(const char* name);
+
+ struct action_def {
+ std::string ad_name;
+ std::string ad_label;
+ std::vector<std::string> ad_cmdline;
+ bool ad_capture_output{false};
+
+ bool operator<(const action_def& rhs) const
+ {
+ return this->ad_name < rhs.ad_name;
+ }
+ };
+
+ virtual ~log_format() = default;
+
+ virtual void clear()
+ {
+ this->lf_pattern_locks.clear();
+ this->lf_date_time.clear();
+ this->lf_time_scanner.clear();
+ }
+
+ /**
+ * Get the name of this log format.
+ *
+ * @return The log format name.
+ */
+ virtual const intern_string_t get_name() const = 0;
+
+ virtual bool match_name(const std::string& filename) { return true; }
+
+ virtual bool match_mime_type(const file_format_t ff) const
+ {
+ if (ff == file_format_t::UNKNOWN) {
+ return true;
+ }
+ return false;
+ }
+
+ struct scan_match {
+ uint32_t sm_quality;
+ };
+
+ struct scan_no_match {
+ const char* snm_reason{nullptr};
+ };
+
+ struct scan_incomplete {};
+
+ using scan_result_t
+ = mapbox::util::variant<scan_match, scan_no_match, scan_incomplete>;
+
+ /**
+ * Scan a log line to see if it matches this log format.
+ *
+ * @param dst The vector of loglines that the formatter should append to
+ * if it detected a match.
+ * @param offset The offset in the file where this line is located.
+ * @param prefix The contents of the line.
+ * @param len The length of the prefix string.
+ */
+ virtual scan_result_t scan(logfile& lf,
+ std::vector<logline>& dst,
+ const line_info& li,
+ shared_buffer_ref& sbr,
+ scan_batch_context& sbc)
+ = 0;
+
+ virtual bool scan_for_partial(shared_buffer_ref& sbr, size_t& len_out) const
+ {
+ return false;
+ }
+
+ /**
+ * Remove redundant data from the log line string.
+ *
+ * XXX We should probably also add some attributes to the line here, so we
+ * can highlight things like the date.
+ *
+ * @param line The log line to edit.
+ */
+ virtual void scrub(std::string& line) {}
+
+ virtual void annotate(uint64_t line_number,
+ string_attrs_t& sa,
+ logline_value_vector& values,
+ bool annotate_module = true) const
+ {
+ }
+
+ virtual void rewrite(exec_context& ec,
+ shared_buffer_ref& line,
+ string_attrs_t& sa,
+ std::string& value_out)
+ {
+ value_out.assign(line.get_data(), line.length());
+ }
+
+ virtual const logline_value_stats* stats_for_value(
+ const intern_string_t& name) const
+ {
+ return nullptr;
+ }
+
+ virtual std::shared_ptr<log_format> specialized(int fmt_lock = -1) = 0;
+
+ virtual std::shared_ptr<log_vtab_impl> get_vtab_impl() const
+ {
+ return nullptr;
+ }
+
+ virtual void get_subline(const logline& ll,
+ shared_buffer_ref& sbr,
+ bool full_message = false)
+ {
+ }
+
+ virtual const std::vector<std::string>* get_actions(
+ const logline_value& lv) const
+ {
+ return nullptr;
+ }
+
+ virtual std::set<std::string> get_source_path() const
+ {
+ std::set<std::string> retval;
+
+ retval.insert("default");
+
+ return retval;
+ }
+
+ virtual bool hide_field(const intern_string_t field_name, bool val)
+ {
+ return false;
+ }
+
+ const char* const* get_timestamp_formats() const
+ {
+ if (this->lf_timestamp_format.empty()) {
+ return nullptr;
+ }
+
+ return &this->lf_timestamp_format[0];
+ }
+
+ void check_for_new_year(std::vector<logline>& dst,
+ exttm log_tv,
+ timeval timeval1);
+
+ virtual std::string get_pattern_path(uint64_t line_number) const;
+
+ virtual intern_string_t get_pattern_name(uint64_t line_number) const;
+
+ virtual std::string get_pattern_regex(uint64_t line_number) const
+ {
+ return "";
+ }
+
+ virtual std::vector<logline_value_meta> get_value_metadata() const
+ {
+ return {};
+ }
+
+ virtual bool format_changed() { return false; }
+
+ struct pattern_for_lines {
+ pattern_for_lines(uint32_t pfl_line, uint32_t pfl_pat_index);
+
+ uint32_t pfl_line;
+ int pfl_pat_index;
+ };
+
+ int last_pattern_index() const
+ {
+ if (this->lf_pattern_locks.empty()) {
+ return -1;
+ }
+
+ return this->lf_pattern_locks.back().pfl_pat_index;
+ }
+
+ int pattern_index_for_line(uint64_t line_number) const;
+
+ bool operator<(const log_format& rhs) const
+ {
+ return this->get_name() < rhs.get_name();
+ }
+
+ static bool name_lt(const std::shared_ptr<const log_format>& lhs,
+ const std::shared_ptr<const log_format>& rhs)
+ {
+ return intern_string_t::case_lt(lhs->get_name(), rhs->get_name());
+ }
+
+ enum class subsecond_unit {
+ milli,
+ micro,
+ nano,
+ };
+
+ std::string lf_description;
+ uint8_t lf_mod_index{0};
+ bool lf_multiline{true};
+ date_time_scanner lf_date_time;
+ date_time_scanner lf_time_scanner;
+ std::vector<pattern_for_lines> lf_pattern_locks;
+ intern_string_t lf_timestamp_field{intern_string::lookup("timestamp", -1)};
+ intern_string_t lf_subsecond_field;
+ nonstd::optional<subsecond_unit> lf_subsecond_unit;
+ intern_string_t lf_time_field;
+ std::vector<const char*> lf_timestamp_format;
+ unsigned int lf_timestamp_flags{0};
+ std::map<std::string, action_def> lf_action_defs;
+ std::vector<logline_value_stats> lf_value_stats;
+ std::vector<highlighter> lf_highlighters;
+ bool lf_is_self_describing{false};
+ bool lf_time_ordered{true};
+ bool lf_specialized{false};
+ nonstd::optional<int64_t> lf_max_unrecognized_lines;
+ std::map<const intern_string_t, std::shared_ptr<format_tag_def>>
+ lf_tag_defs;
+
+protected:
+ static std::vector<std::shared_ptr<log_format>> lf_root_formats;
+
+ struct pcre_format {
+ template<typename T, std::size_t N>
+ explicit pcre_format(const T (&regex)[N])
+ : name(regex),
+ pcre(lnav::pcre2pp::code::from_const(regex).to_shared()),
+ pf_timestamp_index(this->pcre->name_index("timestamp"))
+ {
+ }
+
+ pcre_format() = default;
+
+ const char* name{nullptr};
+ std::shared_ptr<lnav::pcre2pp::code> pcre;
+ int pf_timestamp_index{-1};
+ };
+
+ static bool next_format(pcre_format* fmt, int& index, int& locked_index);
+
+ const char* log_scanf(uint32_t line_number,
+ string_fragment line,
+ pcre_format* fmt,
+ const char* time_fmt[],
+ struct exttm* tm_out,
+ struct timeval* tv_out,
+
+ string_fragment* ts_out,
+ nonstd::optional<string_fragment>* level_out);
+};
+
+#endif
diff --git a/src/log_format_ext.hh b/src/log_format_ext.hh
new file mode 100644
index 0000000..f1022b7
--- /dev/null
+++ b/src/log_format_ext.hh
@@ -0,0 +1,434 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_format_ext.hh
+ */
+
+#ifndef lnav_log_format_ext_hh
+#define lnav_log_format_ext_hh
+
+#include <unordered_map>
+
+#include "log_format.hh"
+#include "log_search_table_fwd.hh"
+#include "yajlpp/yajlpp.hh"
+
+class module_format;
+
+class external_log_format : public log_format {
+public:
+ struct sample {
+ positioned_property<std::string> s_line;
+ std::string s_description;
+ log_level_t s_level{LEVEL_UNKNOWN};
+ std::set<std::string> s_matched_regexes;
+ };
+
+ struct value_def {
+ value_def(intern_string_t name,
+ value_kind_t kind,
+ int col,
+ log_format* format)
+ : vd_meta(name, kind, col, format)
+ {
+ }
+
+ void set_rewrite_src_name()
+ {
+ this->vd_rewrite_src_name = intern_string::lookup(
+ fmt::format(FMT_STRING("{}:{}"),
+ this->vd_meta.lvm_format.value()->get_name(),
+ this->vd_meta.lvm_name));
+ }
+
+ logline_value_meta vd_meta;
+ std::string vd_collate;
+ bool vd_foreign_key{false};
+ intern_string_t vd_unit_field;
+ std::map<const intern_string_t, scaling_factor> vd_unit_scaling;
+ bool vd_internal{false};
+ std::vector<std::string> vd_action_list;
+ std::string vd_rewriter;
+ std::string vd_description;
+ intern_string_t vd_rewrite_src_name;
+ };
+
+ struct indexed_value_def {
+ indexed_value_def(int index = -1,
+ int unit_index = -1,
+ std::shared_ptr<value_def> vd = nullptr)
+ : ivd_index(index), ivd_unit_field_index(unit_index),
+ ivd_value_def(std::move(vd))
+ {
+ }
+
+ int ivd_index;
+ int ivd_unit_field_index;
+ std::shared_ptr<value_def> ivd_value_def;
+
+ bool operator<(const indexed_value_def& rhs) const
+ {
+ return this->ivd_index < rhs.ivd_index;
+ }
+ };
+
+ struct pattern {
+ intern_string_t p_name;
+ std::string p_config_path;
+ factory_container<lnav::pcre2pp::code,
+ int>::with_default_args<PCRE2_DOTALL>
+ p_pcre;
+ std::vector<indexed_value_def> p_value_by_index;
+ std::vector<int> p_numeric_value_indexes;
+ int p_timestamp_field_index{-1};
+ int p_time_field_index{-1};
+ int p_level_field_index{-1};
+ int p_module_field_index{-1};
+ int p_opid_field_index{-1};
+ int p_body_field_index{-1};
+ int p_timestamp_end{-1};
+ bool p_module_format{false};
+ std::set<size_t> p_matched_samples;
+ };
+
+ struct level_pattern {
+ factory_container<lnav::pcre2pp::code> lp_pcre;
+ };
+
+ struct yajl_handle_deleter {
+ void operator()(yajl_handle handle) const
+ {
+ if (handle != nullptr) {
+ yajl_free(handle);
+ }
+ }
+ };
+
+ external_log_format(const intern_string_t name)
+ : elf_level_field(intern_string::lookup("level", -1)),
+ elf_body_field(intern_string::lookup("body", -1)),
+ jlf_yajl_handle(nullptr, yajl_handle_deleter()), elf_name(name)
+ {
+ this->jlf_line_offsets.reserve(128);
+ }
+
+ const intern_string_t get_name() const override { return this->elf_name; }
+
+ bool match_name(const std::string& filename) override;
+
+ bool match_mime_type(const file_format_t ff) const override;
+
+ scan_result_t scan(logfile& lf,
+ std::vector<logline>& dst,
+ const line_info& offset,
+ shared_buffer_ref& sbr,
+ scan_batch_context& sbc) override;
+
+ bool scan_for_partial(shared_buffer_ref& sbr,
+ size_t& len_out) const override;
+
+ void annotate(uint64_t line_number,
+ string_attrs_t& sa,
+ logline_value_vector& values,
+ bool annotate_module = true) const override;
+
+ void rewrite(exec_context& ec,
+ shared_buffer_ref& line,
+ string_attrs_t& sa,
+ std::string& value_out) override;
+
+ void build(std::vector<lnav::console::user_message>& errors);
+
+ void register_vtabs(log_vtab_manager* vtab_manager,
+ std::vector<lnav::console::user_message>& errors);
+
+ bool match_samples(const std::vector<sample>& samples) const;
+
+ bool hide_field(const intern_string_t field_name, bool val) override;
+
+ std::shared_ptr<log_format> specialized(int fmt_lock) override;
+
+ const logline_value_stats* stats_for_value(
+ const intern_string_t& name) const override;
+
+ void get_subline(const logline& ll,
+ shared_buffer_ref& sbr,
+ bool full_message) override;
+
+ std::shared_ptr<log_vtab_impl> get_vtab_impl() const override;
+
+ const std::vector<std::string>* get_actions(
+ const logline_value& lv) const override
+ {
+ const std::vector<std::string>* retval = nullptr;
+
+ const auto iter = this->elf_value_defs.find(lv.lv_meta.lvm_name);
+ if (iter != this->elf_value_defs.end()) {
+ retval = &iter->second->vd_action_list;
+ }
+
+ return retval;
+ }
+
+ bool format_changed() override;
+
+ std::set<std::string> get_source_path() const override
+ {
+ return this->elf_source_path;
+ }
+
+ std::vector<logline_value_meta> get_value_metadata() const override;
+
+ enum class json_log_field {
+ CONSTANT,
+ VARIABLE
+ };
+
+ struct json_format_element {
+ enum class align_t {
+ NONE,
+ LEFT,
+ RIGHT,
+ };
+
+ enum class overflow_t {
+ ABBREV,
+ TRUNCATE,
+ DOTDOT,
+ };
+
+ enum class transform_t {
+ NONE,
+ UPPERCASE,
+ LOWERCASE,
+ CAPITALIZE,
+ };
+
+ json_log_field jfe_type{json_log_field::CONSTANT};
+ positioned_property<intern_string_t> jfe_value;
+ std::string jfe_default_value{"-"};
+ long long jfe_min_width{0};
+ bool jfe_auto_width{false};
+ long long jfe_max_width{LLONG_MAX};
+ align_t jfe_align{align_t::NONE};
+ overflow_t jfe_overflow{overflow_t::ABBREV};
+ transform_t jfe_text_transform{transform_t::NONE};
+ std::string jfe_ts_format;
+ std::string jfe_prefix;
+ std::string jfe_suffix;
+ };
+
+ struct json_field_cmp {
+ json_field_cmp(json_log_field type, const intern_string_t name)
+ : jfc_type(type), jfc_field_name(name)
+ {
+ }
+
+ bool operator()(const json_format_element& jfe) const
+ {
+ return (this->jfc_type == jfe.jfe_type
+ && this->jfc_field_name == jfe.jfe_value.pp_value);
+ }
+
+ json_log_field jfc_type;
+ const intern_string_t jfc_field_name;
+ };
+
+ struct highlighter_def {
+ factory_container<lnav::pcre2pp::code> hd_pattern;
+ positioned_property<std::string> hd_color;
+ positioned_property<std::string> hd_background_color;
+ bool hd_underline{false};
+ bool hd_blink{false};
+ };
+
+ struct value_line_count_result {
+ size_t vlcr_count{1};
+ size_t vlcr_line_format_count{0};
+ bool vlcr_has_ansi{false};
+ bool vlcr_valid_utf{true};
+ };
+
+ value_line_count_result value_line_count(const intern_string_t ist,
+ bool top_level,
+ nonstd::optional<double> val
+ = nonstd::nullopt,
+ const unsigned char* str = nullptr,
+ ssize_t len = -1);
+
+ bool has_value_def(const intern_string_t ist) const
+ {
+ const auto iter = this->elf_value_defs.find(ist);
+
+ return iter != this->elf_value_defs.end();
+ }
+
+ std::string get_pattern_path(uint64_t line_number) const override
+ {
+ if (this->elf_type != elf_type_t::ELF_TYPE_TEXT) {
+ return "structured";
+ }
+ int pat_index = this->pattern_index_for_line(line_number);
+ return this->elf_pattern_order[pat_index]->p_config_path;
+ }
+
+ intern_string_t get_pattern_name(uint64_t line_number) const override;
+
+ std::string get_pattern_regex(uint64_t line_number) const override;
+
+ log_level_t convert_level(string_fragment str,
+ scan_batch_context* sbc) const;
+
+ using mod_map_t = std::map<intern_string_t, module_format>;
+ static mod_map_t MODULE_FORMATS;
+ static std::vector<std::shared_ptr<external_log_format>>
+ GRAPH_ORDERED_FORMATS;
+
+ std::set<std::string> elf_source_path;
+ std::vector<ghc::filesystem::path> elf_format_source_order;
+ std::map<intern_string_t, int> elf_format_sources;
+ std::list<intern_string_t> elf_collision;
+ std::set<file_format_t> elf_mime_types;
+ factory_container<lnav::pcre2pp::code> elf_filename_pcre;
+ std::map<std::string, std::shared_ptr<pattern>> elf_patterns;
+ std::vector<std::shared_ptr<pattern>> elf_pattern_order;
+ std::vector<sample> elf_samples;
+ std::unordered_map<const intern_string_t, std::shared_ptr<value_def>>
+ elf_value_defs;
+
+ struct value_defs_state {
+ size_t vds_generation{0};
+ };
+
+ std::shared_ptr<value_defs_state> elf_value_defs_state{
+ std::make_shared<value_defs_state>()};
+ value_defs_state elf_specialized_value_defs_state;
+
+ std::vector<std::shared_ptr<value_def>> elf_value_def_order;
+ std::vector<std::shared_ptr<value_def>> elf_numeric_value_defs;
+ int elf_column_count{0};
+ double elf_timestamp_divisor{1.0};
+ intern_string_t elf_level_field;
+ factory_container<lnav::pcre2pp::code> elf_level_pointer;
+ intern_string_t elf_body_field;
+ intern_string_t elf_module_id_field;
+ intern_string_t elf_opid_field;
+ std::map<log_level_t, level_pattern> elf_level_patterns;
+ std::vector<std::pair<int64_t, log_level_t>> elf_level_pairs;
+ bool elf_container{false};
+ bool elf_has_module_format{false};
+ bool elf_builtin_format{false};
+
+ using search_table_pcre2pp
+ = factory_container<lnav::pcre2pp::code, int>::with_default_args<
+ log_search_table_ns::PATTERN_OPTIONS>;
+
+ struct search_table_def {
+ search_table_pcre2pp std_pattern;
+ std::string std_glob;
+ log_level_t std_level{LEVEL_UNKNOWN};
+ };
+
+ std::map<intern_string_t, search_table_def> elf_search_tables;
+ std::map<const intern_string_t, highlighter_def> elf_highlighter_patterns;
+
+ enum class elf_type_t {
+ ELF_TYPE_TEXT,
+ ELF_TYPE_JSON,
+ ELF_TYPE_CSV,
+ };
+
+ elf_type_t elf_type{elf_type_t::ELF_TYPE_TEXT};
+
+ void json_append_to_cache(const char* value, ssize_t len)
+ {
+ if (len <= 0) {
+ return;
+ }
+
+ size_t old_size = this->jlf_cached_line.size();
+ if (len == -1) {
+ len = strlen(value);
+ }
+ this->jlf_cached_line.resize(old_size + len);
+ memcpy(&(this->jlf_cached_line[old_size]), value, len);
+ }
+
+ void json_append_to_cache(const string_fragment& sf)
+ {
+ this->json_append_to_cache(sf.data(), sf.length());
+ }
+
+ void json_append_to_cache(ssize_t len)
+ {
+ if (len <= 0) {
+ return;
+ }
+ size_t old_size = this->jlf_cached_line.size();
+ this->jlf_cached_line.resize(old_size + len);
+ memset(&this->jlf_cached_line[old_size], ' ', len);
+ }
+
+ void json_append(const json_format_element& jfe,
+ const value_def* vd,
+ const char* value,
+ ssize_t len);
+
+ logline_value_meta get_value_meta(intern_string_t field_name,
+ value_kind_t kind);
+
+ std::vector<lnav::console::snippet> get_snippets() const;
+
+ bool jlf_hide_extra{false};
+ std::vector<json_format_element> jlf_line_format;
+ int jlf_line_format_init_count{0};
+ shared_buffer jlf_share_manager;
+ logline_value_vector jlf_line_values;
+
+ off_t jlf_cached_offset{-1};
+ line_range jlf_cached_sub_range;
+ bool jlf_cached_full{false};
+ std::vector<off_t> jlf_line_offsets;
+ std::vector<char> jlf_cached_line;
+ string_attrs_t jlf_line_attrs;
+ std::shared_ptr<yajlpp_parse_context> jlf_parse_context;
+ std::shared_ptr<yajl_handle_t> jlf_yajl_handle;
+
+private:
+ const intern_string_t elf_name;
+
+ static uint8_t module_scan(string_fragment body_cap,
+ const intern_string_t& mod_name);
+};
+
+class module_format {
+public:
+ std::shared_ptr<log_format> mf_mod_format;
+};
+
+#endif
diff --git a/src/log_format_fwd.hh b/src/log_format_fwd.hh
new file mode 100644
index 0000000..8b9d301
--- /dev/null
+++ b/src/log_format_fwd.hh
@@ -0,0 +1,330 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_format_fwd.hh
+ */
+
+#ifndef lnav_log_format_fwd_hh
+#define lnav_log_format_fwd_hh
+
+#include <utility>
+
+#include <sys/types.h>
+
+#include "ArenaAlloc/arenaalloc.h"
+#include "base/file_range.hh"
+#include "base/string_attr_type.hh"
+#include "byte_array.hh"
+#include "log_level.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "ptimec.hh"
+#include "robin_hood/robin_hood.h"
+#include "yajlpp/yajlpp.hh"
+
+class log_format;
+
+struct opid_time_range {
+ struct timeval otr_begin;
+ struct timeval otr_end;
+};
+
+using log_opid_map = robin_hood::unordered_map<string_fragment,
+ opid_time_range,
+ frag_hasher,
+ std::equal_to<string_fragment>>;
+
+struct scan_batch_context {
+ ArenaAlloc::Alloc<char>& sbc_allocator;
+ log_opid_map sbc_opids;
+ std::string sbc_cached_level_strings[4];
+ log_level_t sbc_cached_level_values[4];
+ size_t sbc_cached_level_count{0};
+};
+
+/**
+ * Metadata for a single line in a log file.
+ */
+class logline {
+public:
+ static string_attr_type<void> L_PREFIX;
+ static string_attr_type<void> L_TIMESTAMP;
+ static string_attr_type<std::shared_ptr<logfile>> L_FILE;
+ static string_attr_type<bookmark_metadata*> L_PARTITION;
+ static string_attr_type<void> L_MODULE;
+ static string_attr_type<void> L_OPID;
+ static string_attr_type<bookmark_metadata*> L_META;
+
+ /**
+ * Construct a logline object with the given values.
+ *
+ * @param off The offset of the line in the file.
+ * @param t The timestamp for the line.
+ * @param millis The millisecond timestamp for the line.
+ * @param l The logging level.
+ */
+ logline(file_off_t off,
+ time_t t,
+ uint16_t millis,
+ log_level_t lev,
+ uint8_t mod = 0,
+ uint8_t opid = 0)
+ : ll_offset(off), ll_has_ansi(false), ll_time(t), ll_millis(millis),
+ ll_opid(opid), ll_sub_offset(0), ll_valid_utf(1), ll_level(lev),
+ ll_module_id(mod), ll_expr_mark(0)
+ {
+ memset(this->ll_schema, 0, sizeof(this->ll_schema));
+ }
+
+ logline(file_off_t off,
+ const struct timeval& tv,
+ log_level_t lev,
+ uint8_t mod = 0,
+ uint8_t opid = 0)
+ : ll_offset(off), ll_has_ansi(false), ll_opid(opid), ll_sub_offset(0),
+ ll_valid_utf(1), ll_level(lev), ll_module_id(mod), ll_expr_mark(0)
+ {
+ this->set_time(tv);
+ memset(this->ll_schema, 0, sizeof(this->ll_schema));
+ }
+
+ /** @return The offset of the line in the file. */
+ file_off_t get_offset() const { return this->ll_offset; }
+
+ uint16_t get_sub_offset() const { return this->ll_sub_offset; }
+
+ void set_sub_offset(uint16_t suboff) { this->ll_sub_offset = suboff; }
+
+ /** @return The timestamp for the line. */
+ time_t get_time() const { return this->ll_time; }
+
+ void to_exttm(struct exttm& tm_out) const
+ {
+ tm_out.et_tm = *gmtime(&this->ll_time);
+ tm_out.et_nsec = this->ll_millis * 1000 * 1000;
+ }
+
+ void set_time(time_t t) { this->ll_time = t; }
+
+ /** @return The millisecond timestamp for the line. */
+ uint16_t get_millis() const { return this->ll_millis; }
+
+ void set_millis(uint16_t m) { this->ll_millis = m; }
+
+ uint64_t get_time_in_millis() const
+ {
+ return (this->ll_time * 1000ULL + (uint64_t) this->ll_millis);
+ }
+
+ struct timeval get_timeval() const
+ {
+ struct timeval retval = {this->ll_time, this->ll_millis * 1000};
+
+ return retval;
+ }
+
+ void set_time(const struct timeval& tv)
+ {
+ this->ll_time = tv.tv_sec;
+ this->ll_millis = tv.tv_usec / 1000;
+ }
+
+ void set_ignore(bool val)
+ {
+ if (val) {
+ this->ll_level |= LEVEL_IGNORE;
+ } else {
+ this->ll_level &= ~LEVEL_IGNORE;
+ }
+ }
+
+ bool is_ignored() const { return this->ll_level & LEVEL_IGNORE; }
+
+ void set_mark(bool val)
+ {
+ if (val) {
+ this->ll_level |= LEVEL_MARK;
+ } else {
+ this->ll_level &= ~LEVEL_MARK;
+ }
+ }
+
+ bool is_marked() const { return this->ll_level & LEVEL_MARK; }
+
+ void set_expr_mark(bool val) { this->ll_expr_mark = val; }
+
+ bool is_expr_marked() const { return this->ll_expr_mark; }
+
+ void set_time_skew(bool val)
+ {
+ if (val) {
+ this->ll_level |= LEVEL_TIME_SKEW;
+ } else {
+ this->ll_level &= ~LEVEL_TIME_SKEW;
+ }
+ }
+
+ bool is_time_skewed() const { return this->ll_level & LEVEL_TIME_SKEW; }
+
+ void set_valid_utf(bool v) { this->ll_valid_utf = v; }
+
+ bool is_valid_utf() const { return this->ll_valid_utf; }
+
+ void set_has_ansi(bool v) { this->ll_has_ansi = v; }
+
+ bool has_ansi() const { return this->ll_has_ansi; }
+
+ /** @param l The logging level. */
+ void set_level(log_level_t l) { this->ll_level = l; };
+
+ /** @return The logging level. */
+ log_level_t get_level_and_flags() const
+ {
+ return (log_level_t) this->ll_level;
+ }
+
+ log_level_t get_msg_level() const
+ {
+ return (log_level_t) (this->ll_level & ~LEVEL__FLAGS);
+ }
+
+ const char* get_level_name() const
+ {
+ return level_names[this->ll_level & ~LEVEL__FLAGS];
+ }
+
+ bool is_message() const
+ {
+ return (this->ll_level & (LEVEL_IGNORE | LEVEL_CONTINUED)) == 0;
+ }
+
+ bool is_continued() const { return this->ll_level & LEVEL_CONTINUED; }
+
+ uint8_t get_module_id() const { return this->ll_module_id; }
+
+ void set_opid(uint8_t opid) { this->ll_opid = opid; }
+
+ uint8_t get_opid() const { return this->ll_opid; }
+
+ /**
+ * @return True if there is a schema value set for this log line.
+ */
+ bool has_schema() const
+ {
+ return (this->ll_schema[0] != 0 || this->ll_schema[1] != 0);
+ }
+
+ /**
+ * Set the "schema" for this log line. The schema ID is used to match log
+ * lines that have a similar format when generating the logline table. The
+ * schema is set lazily so that startup is faster.
+ *
+ * @param ba The SHA-1 hash of the constant parts of this log line.
+ */
+ void set_schema(const byte_array<2, uint64_t>& ba)
+ {
+ memcpy(this->ll_schema, ba.in(), sizeof(this->ll_schema));
+ }
+
+ char get_schema() const { return this->ll_schema[0]; }
+
+ /**
+ * Perform a partial match of the given schema against this log line.
+ * Storing the full schema is not practical, so we just keep the first four
+ * bytes.
+ *
+ * @param ba The SHA-1 hash of the constant parts of a log line.
+ * @return True if the first four bytes of the given schema match the
+ * schema stored in this log line.
+ */
+ bool match_schema(const byte_array<2, uint64_t>& ba) const
+ {
+ return memcmp(this->ll_schema, ba.in(), sizeof(this->ll_schema)) == 0;
+ }
+
+ /**
+ * Compare loglines based on their timestamp.
+ */
+ bool operator<(const logline& rhs) const
+ {
+ return (this->ll_time < rhs.ll_time)
+ || (this->ll_time == rhs.ll_time && this->ll_millis < rhs.ll_millis)
+ || (this->ll_time == rhs.ll_time && this->ll_millis == rhs.ll_millis
+ && this->ll_offset < rhs.ll_offset)
+ || (this->ll_time == rhs.ll_time && this->ll_millis == rhs.ll_millis
+ && this->ll_offset == rhs.ll_offset
+ && this->ll_sub_offset < rhs.ll_sub_offset);
+ }
+
+ bool operator<(const time_t& rhs) const { return this->ll_time < rhs; }
+
+ bool operator<(const struct timeval& rhs) const
+ {
+ return ((this->ll_time < rhs.tv_sec)
+ || ((this->ll_time == rhs.tv_sec)
+ && (this->ll_millis < (rhs.tv_usec / 1000))));
+ }
+
+ bool operator<=(const struct timeval& rhs) const
+ {
+ return ((this->ll_time < rhs.tv_sec)
+ || ((this->ll_time == rhs.tv_sec)
+ && (this->ll_millis <= (rhs.tv_usec / 1000))));
+ }
+
+private:
+ file_off_t ll_offset : 63;
+ uint8_t ll_has_ansi : 1;
+ time_t ll_time;
+ unsigned int ll_millis : 10;
+ unsigned int ll_opid : 6;
+ unsigned int ll_sub_offset : 15;
+ unsigned int ll_valid_utf : 1;
+ uint8_t ll_level;
+ uint8_t ll_module_id : 7;
+ uint8_t ll_expr_mark : 1;
+ char ll_schema[2];
+};
+
+struct format_tag_def {
+ explicit format_tag_def(std::string name) : ftd_name(std::move(name)) {}
+
+ struct path_restriction {
+ std::string p_glob;
+
+ bool matches(const char* fn) const;
+ };
+
+ std::string ftd_name;
+ std::string ftd_description;
+ std::vector<path_restriction> ftd_paths;
+ factory_container<lnav::pcre2pp::code, int>::with_default_args<PCRE2_DOTALL>
+ ftd_pattern;
+ log_level_t ftd_level{LEVEL_UNKNOWN};
+};
+
+#endif
diff --git a/src/log_format_impls.cc b/src/log_format_impls.cc
new file mode 100644
index 0000000..96d37f5
--- /dev/null
+++ b/src/log_format_impls.cc
@@ -0,0 +1,1775 @@
+/**
+ * Copyright (c) 2007-2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_format_impls.cc
+ */
+
+#include <algorithm>
+#include <utility>
+
+#include "log_format.hh"
+
+#include <stdio.h>
+
+#include "base/injector.bind.hh"
+#include "base/opt_util.hh"
+#include "config.h"
+#include "formats/logfmt/logfmt.parser.hh"
+#include "log_vtab_impl.hh"
+#include "sql_util.hh"
+#include "yajlpp/yajlpp.hh"
+
+class generic_log_format : public log_format {
+ static pcre_format* get_pcre_log_formats()
+ {
+ static pcre_format log_fmt[] = {
+ pcre_format(
+ "^(?:\\*\\*\\*\\s+)?(?<timestamp>@[0-9a-zA-Z]{16,24})(.*)"),
+ pcre_format(
+ "^(?:\\*\\*\\*\\s+)?(?<timestamp>[\\dTZ: +/\\-,\\.-]+)([^:]+)"),
+ pcre_format(
+ "^(?:\\*\\*\\*\\s+)?(?<timestamp>[\\w:+/\\.-]+) \\[\\w (.*)"),
+ pcre_format("^(?:\\*\\*\\*\\s+)?(?<timestamp>[\\w:,/\\.-]+) (.*)"),
+ pcre_format(
+ "^(?:\\*\\*\\*\\s+)?(?<timestamp>[\\w:,/\\.-]+) - (.*)"),
+ pcre_format(
+ "^(?:\\*\\*\\*\\s+)?(?<timestamp>[\\w: \\.,/-]+) - (.*)"),
+ pcre_format("^(?:\\*\\*\\*\\s+)?(?<timestamp>[\\w: "
+ "\\.,/-]+)\\[[^\\]]+\\](.*)"),
+ pcre_format("^(?:\\*\\*\\*\\s+)?(?<timestamp>[\\w: \\.,/-]+) (.*)"),
+
+ pcre_format(
+ R"(^(?:\*\*\*\s+)?\[(?<timestamp>[\w: \.,+/-]+)\]\s*(\w+):?)"),
+ pcre_format(
+ "^(?:\\*\\*\\*\\s+)?\\[(?<timestamp>[\\w: \\.,+/-]+)\\] (.*)"),
+ pcre_format("^(?:\\*\\*\\*\\s+)?\\[(?<timestamp>[\\w: "
+ "\\.,+/-]+)\\] \\[(\\w+)\\]"),
+ pcre_format("^(?:\\*\\*\\*\\s+)?\\[(?<timestamp>[\\w: "
+ "\\.,+/-]+)\\] \\w+ (.*)"),
+ pcre_format("^(?:\\*\\*\\*\\s+)?\\[(?<timestamp>[\\w: ,+/-]+)\\] "
+ "\\(\\d+\\) (.*)"),
+
+ pcre_format(),
+ };
+
+ return log_fmt;
+ }
+
+ std::string get_pattern_regex(uint64_t line_number) const override
+ {
+ int pat_index = this->pattern_index_for_line(line_number);
+ return get_pcre_log_formats()[pat_index].name;
+ }
+
+ const intern_string_t get_name() const override
+ {
+ return intern_string::lookup("generic_log");
+ }
+
+ scan_result_t scan(logfile& lf,
+ std::vector<logline>& dst,
+ const line_info& li,
+ shared_buffer_ref& sbr,
+ scan_batch_context& sbc) override
+ {
+ struct exttm log_time;
+ struct timeval log_tv;
+ string_fragment ts;
+ nonstd::optional<string_fragment> level;
+ const char* last_pos;
+
+ if ((last_pos = this->log_scanf(dst.size(),
+ sbr.to_string_fragment(),
+ get_pcre_log_formats(),
+ nullptr,
+ &log_time,
+ &log_tv,
+
+ &ts,
+ &level))
+ != nullptr)
+ {
+ log_level_t level_val = log_level_t::LEVEL_UNKNOWN;
+ if (level) {
+ level_val = string2level(level->data(), level->length());
+ }
+
+ if (!((log_time.et_flags & ETF_DAY_SET)
+ && (log_time.et_flags & ETF_MONTH_SET)
+ && (log_time.et_flags & ETF_YEAR_SET)))
+ {
+ this->check_for_new_year(dst, log_time, log_tv);
+ }
+
+ dst.emplace_back(li.li_file_range.fr_offset, log_tv, level_val);
+ return scan_match{0};
+ }
+
+ return scan_no_match{"no patterns matched"};
+ }
+
+ void annotate(uint64_t line_number,
+ string_attrs_t& sa,
+ logline_value_vector& values,
+ bool annotate_module) const override
+ {
+ auto& line = values.lvv_sbr;
+ int pat_index = this->pattern_index_for_line(line_number);
+ auto& fmt = get_pcre_log_formats()[pat_index];
+ int prefix_len = 0;
+ auto md = fmt.pcre->create_match_data();
+ auto match_res = fmt.pcre->capture_from(line.to_string_fragment())
+ .into(md)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (!match_res) {
+ return;
+ }
+
+ auto lr = to_line_range(md[fmt.pf_timestamp_index].value());
+ sa.emplace_back(lr, logline::L_TIMESTAMP.value());
+
+ prefix_len = lr.lr_end;
+ auto level_cap = md[2];
+ if (level_cap) {
+ if (string2level(level_cap->data(), level_cap->length(), true)
+ != LEVEL_UNKNOWN)
+ {
+ prefix_len = level_cap->sf_end;
+ }
+ }
+
+ lr.lr_start = 0;
+ lr.lr_end = prefix_len;
+ sa.emplace_back(lr, logline::L_PREFIX.value());
+
+ lr.lr_start = prefix_len;
+ lr.lr_end = line.length();
+ sa.emplace_back(lr, SA_BODY.value());
+ }
+
+ std::shared_ptr<log_format> specialized(int fmt_lock) override
+ {
+ auto retval = std::make_shared<generic_log_format>(*this);
+
+ retval->lf_specialized = true;
+ return retval;
+ }
+};
+
+std::string
+from_escaped_string(const char* str, size_t len)
+{
+ std::string retval;
+
+ for (size_t lpc = 0; lpc < len; lpc++) {
+ switch (str[lpc]) {
+ case '\\':
+ if ((lpc + 3) < len && str[lpc + 1] == 'x') {
+ int ch;
+
+ if (sscanf(&str[lpc + 2], "%2x", &ch) == 1) {
+ retval.append(1, (char) ch & 0xff);
+ lpc += 3;
+ }
+ }
+ break;
+ default:
+ retval.append(1, str[lpc]);
+ break;
+ }
+ }
+
+ return retval;
+}
+
+nonstd::optional<const char*>
+lnav_strnstr(const char* s, const char* find, size_t slen)
+{
+ char c, sc;
+ size_t len;
+
+ if ((c = *find++) != '\0') {
+ len = strlen(find);
+ do {
+ do {
+ if (slen < 1 || (sc = *s) == '\0') {
+ return nonstd::nullopt;
+ }
+ --slen;
+ ++s;
+ } while (sc != c);
+ if (len > slen) {
+ return nonstd::nullopt;
+ }
+ } while (strncmp(s, find, len) != 0);
+ s--;
+ }
+ return s;
+}
+
+struct separated_string {
+ const char* ss_str;
+ size_t ss_len;
+ const char* ss_separator;
+ size_t ss_separator_len;
+
+ separated_string(const char* str, size_t len)
+ : ss_str(str), ss_len(len), ss_separator(","),
+ ss_separator_len(strlen(this->ss_separator))
+ {
+ }
+
+ separated_string& with_separator(const char* sep)
+ {
+ this->ss_separator = sep;
+ this->ss_separator_len = strlen(sep);
+ return *this;
+ }
+
+ struct iterator {
+ const separated_string& i_parent;
+ const char* i_pos;
+ const char* i_next_pos;
+ size_t i_index;
+
+ iterator(const separated_string& ss, const char* pos)
+ : i_parent(ss), i_pos(pos), i_next_pos(pos), i_index(0)
+ {
+ this->update();
+ }
+
+ void update()
+ {
+ const separated_string& ss = this->i_parent;
+ auto next_field
+ = lnav_strnstr(this->i_pos,
+ ss.ss_separator,
+ ss.ss_len - (this->i_pos - ss.ss_str));
+ if (next_field) {
+ this->i_next_pos = next_field.value() + ss.ss_separator_len;
+ } else {
+ this->i_next_pos = ss.ss_str + ss.ss_len;
+ }
+ }
+
+ iterator& operator++()
+ {
+ this->i_pos = this->i_next_pos;
+ this->update();
+ this->i_index += 1;
+
+ return *this;
+ }
+
+ string_fragment operator*()
+ {
+ const auto& ss = this->i_parent;
+ int end;
+
+ if (this->i_next_pos < (ss.ss_str + ss.ss_len)) {
+ end = this->i_next_pos - ss.ss_str - ss.ss_separator_len;
+ } else {
+ end = this->i_next_pos - ss.ss_str;
+ }
+ return string_fragment::from_byte_range(
+ ss.ss_str, this->i_pos - ss.ss_str, end);
+ }
+
+ bool operator==(const iterator& other) const
+ {
+ return (&this->i_parent == &other.i_parent)
+ && (this->i_pos == other.i_pos);
+ }
+
+ bool operator!=(const iterator& other) const
+ {
+ return !(*this == other);
+ }
+
+ size_t index() const { return this->i_index; }
+ };
+
+ iterator begin() { return {*this, this->ss_str}; }
+
+ iterator end() { return {*this, this->ss_str + this->ss_len}; }
+};
+
+class bro_log_format : public log_format {
+public:
+ struct field_def {
+ logline_value_meta fd_meta;
+ std::string fd_collator;
+ nonstd::optional<size_t> fd_numeric_index;
+
+ explicit field_def(const intern_string_t name,
+ int col,
+ log_format* format)
+ : fd_meta(name, value_kind_t::VALUE_TEXT, col, format)
+ {
+ }
+
+ field_def& with_kind(value_kind_t kind,
+ bool identifier = false,
+ const std::string& collator = "")
+ {
+ this->fd_meta.lvm_kind = kind;
+ this->fd_meta.lvm_identifier = identifier;
+ this->fd_collator = collator;
+ return *this;
+ }
+
+ field_def& with_numeric_index(size_t index)
+ {
+ this->fd_numeric_index = index;
+ return *this;
+ }
+ };
+
+ bro_log_format()
+ {
+ this->lf_is_self_describing = true;
+ this->lf_time_ordered = false;
+ }
+
+ const intern_string_t get_name() const override
+ {
+ static const intern_string_t name(intern_string::lookup("bro"));
+
+ return this->blf_format_name.empty() ? name : this->blf_format_name;
+ }
+
+ void clear() override
+ {
+ this->log_format::clear();
+ this->blf_format_name.clear();
+ this->blf_field_defs.clear();
+ }
+
+ scan_result_t scan_int(std::vector<logline>& dst,
+ const line_info& li,
+ shared_buffer_ref& sbr)
+ {
+ static const intern_string_t STATUS_CODE
+ = intern_string::lookup("bro_status_code");
+ static const intern_string_t TS = intern_string::lookup("bro_ts");
+ static const intern_string_t UID = intern_string::lookup("bro_uid");
+
+ separated_string ss(sbr.get_data(), sbr.length());
+ struct timeval tv;
+ struct exttm tm;
+ bool found_ts = false;
+ log_level_t level = LEVEL_INFO;
+ uint8_t opid = 0;
+
+ ss.with_separator(this->blf_separator.get());
+
+ for (auto iter = ss.begin(); iter != ss.end(); ++iter) {
+ if (iter.index() == 0 && *iter == "#close") {
+ return scan_match{0};
+ }
+
+ if (iter.index() >= this->blf_field_defs.size()) {
+ break;
+ }
+
+ const auto& fd = this->blf_field_defs[iter.index()];
+
+ if (TS == fd.fd_meta.lvm_name) {
+ string_fragment sf = *iter;
+
+ if (this->lf_date_time.scan(
+ sf.data(), sf.length(), nullptr, &tm, tv))
+ {
+ this->lf_timestamp_flags = tm.et_flags;
+ found_ts = true;
+ }
+ } else if (STATUS_CODE == fd.fd_meta.lvm_name) {
+ string_fragment sf = *iter;
+
+ if (!sf.empty() && sf[0] >= '4') {
+ level = LEVEL_ERROR;
+ }
+ } else if (UID == fd.fd_meta.lvm_name) {
+ string_fragment sf = *iter;
+
+ opid = hash_str(sf.data(), sf.length());
+ }
+
+ if (fd.fd_numeric_index) {
+ switch (fd.fd_meta.lvm_kind) {
+ case value_kind_t::VALUE_INTEGER:
+ case value_kind_t::VALUE_FLOAT: {
+ string_fragment sf = *iter;
+ char field_copy[sf.length() + 1];
+ double val;
+
+ if (sscanf(sf.to_string(field_copy), "%lf", &val) == 1)
+ {
+ this->lf_value_stats[fd.fd_numeric_index.value()]
+ .add_value(val);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ if (found_ts) {
+ if (!this->lf_specialized) {
+ for (auto& ll : dst) {
+ ll.set_ignore(true);
+ }
+ }
+ dst.emplace_back(li.li_file_range.fr_offset, tv, level, 0, opid);
+ return scan_match{0};
+ }
+ return scan_no_match{};
+ }
+
+ scan_result_t scan(logfile& lf,
+ std::vector<logline>& dst,
+ const line_info& li,
+ shared_buffer_ref& sbr,
+ scan_batch_context& sbc) override
+ {
+ static const auto SEP_RE
+ = lnav::pcre2pp::code::from_const(R"(^#separator\s+(.+))");
+
+ if (!this->blf_format_name.empty()) {
+ return this->scan_int(dst, li, sbr);
+ }
+
+ if (dst.empty() || dst.size() > 20 || sbr.empty()
+ || sbr.get_data()[0] == '#')
+ {
+ return scan_no_match{};
+ }
+
+ auto line_iter = dst.begin();
+ auto read_result = lf.read_line(line_iter);
+
+ if (read_result.isErr()) {
+ return scan_no_match{"unable to read first line"};
+ }
+
+ auto line = read_result.unwrap();
+ auto md = SEP_RE.create_match_data();
+
+ auto match_res = SEP_RE.capture_from(line.to_string_fragment())
+ .into(md)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (!match_res) {
+ return scan_no_match{"cannot read separator header"};
+ }
+
+ this->clear();
+
+ auto sep = from_escaped_string(md[1]->data(), md[1]->length());
+ this->blf_separator = intern_string::lookup(sep);
+
+ for (++line_iter; line_iter != dst.end(); ++line_iter) {
+ auto next_read_result = lf.read_line(line_iter);
+
+ if (next_read_result.isErr()) {
+ return scan_no_match{"unable to read header line"};
+ }
+
+ line = next_read_result.unwrap();
+ separated_string ss(line.get_data(), line.length());
+
+ ss.with_separator(this->blf_separator.get());
+ auto iter = ss.begin();
+
+ string_fragment directive = *iter;
+
+ if (directive.empty() || directive[0] != '#') {
+ continue;
+ }
+
+ ++iter;
+ if (iter == ss.end()) {
+ continue;
+ }
+
+ if (directive == "#set_separator") {
+ this->blf_set_separator = intern_string::lookup(*iter);
+ } else if (directive == "#empty_field") {
+ this->blf_empty_field = intern_string::lookup(*iter);
+ } else if (directive == "#unset_field") {
+ this->blf_unset_field = intern_string::lookup(*iter);
+ } else if (directive == "#path") {
+ auto full_name = fmt::format(FMT_STRING("bro_{}_log"), *iter);
+ this->blf_format_name = intern_string::lookup(full_name);
+ } else if (directive == "#fields" && this->blf_field_defs.empty()) {
+ do {
+ this->blf_field_defs.emplace_back(
+ intern_string::lookup("bro_" + sql_safe_ident(*iter)),
+ this->blf_field_defs.size(),
+ this);
+ ++iter;
+ } while (iter != ss.end());
+ } else if (directive == "#types") {
+ static const char* KNOWN_IDS[] = {
+ "bro_conn_uids",
+ "bro_fuid",
+ "bro_host",
+ "bro_info_code",
+ "bro_method",
+ "bro_mime_type",
+ "bro_orig_fuids",
+ "bro_parent_fuid",
+ "bro_proto",
+ "bro_referrer",
+ "bro_resp_fuids",
+ "bro_service",
+ "bro_status_code",
+ "bro_uid",
+ "bro_uri",
+ "bro_user_agent",
+ "bro_username",
+ };
+
+ int numeric_count = 0;
+
+ do {
+ string_fragment field_type = *iter;
+ auto& fd = this->blf_field_defs[iter.index() - 1];
+
+ if (field_type == "time") {
+ fd.with_kind(value_kind_t::VALUE_TIMESTAMP);
+ } else if (field_type == "string") {
+ bool ident = std::binary_search(std::begin(KNOWN_IDS),
+ std::end(KNOWN_IDS),
+ fd.fd_meta.lvm_name);
+ fd.with_kind(value_kind_t::VALUE_TEXT, ident);
+ } else if (field_type == "count") {
+ bool ident = std::binary_search(std::begin(KNOWN_IDS),
+ std::end(KNOWN_IDS),
+ fd.fd_meta.lvm_name);
+ fd.with_kind(value_kind_t::VALUE_INTEGER, ident)
+ .with_numeric_index(numeric_count);
+ numeric_count += 1;
+ } else if (field_type == "bool") {
+ fd.with_kind(value_kind_t::VALUE_BOOLEAN);
+ } else if (field_type == "addr") {
+ fd.with_kind(
+ value_kind_t::VALUE_TEXT, true, "ipaddress");
+ } else if (field_type == "port") {
+ fd.with_kind(value_kind_t::VALUE_INTEGER, true);
+ } else if (field_type == "interval") {
+ fd.with_kind(value_kind_t::VALUE_FLOAT)
+ .with_numeric_index(numeric_count);
+ numeric_count += 1;
+ }
+
+ ++iter;
+ } while (iter != ss.end());
+
+ this->lf_value_stats.resize(numeric_count);
+ }
+ }
+
+ if (!this->blf_format_name.empty() && !this->blf_separator.empty()
+ && !this->blf_field_defs.empty())
+ {
+ dst.clear();
+ return this->scan_int(dst, li, sbr);
+ }
+
+ this->blf_format_name.clear();
+ this->lf_value_stats.clear();
+
+ return scan_no_match{};
+ }
+
+ void annotate(uint64_t line_number,
+ string_attrs_t& sa,
+ logline_value_vector& values,
+ bool annotate_module) const override
+ {
+ static const intern_string_t TS = intern_string::lookup("bro_ts");
+ static const intern_string_t UID = intern_string::lookup("bro_uid");
+
+ auto& sbr = values.lvv_sbr;
+ separated_string ss(sbr.get_data(), sbr.length());
+
+ ss.with_separator(this->blf_separator.get());
+
+ for (auto iter = ss.begin(); iter != ss.end(); ++iter) {
+ if (iter.index() >= this->blf_field_defs.size()) {
+ return;
+ }
+
+ const field_def& fd = this->blf_field_defs[iter.index()];
+ string_fragment sf = *iter;
+
+ if (sf == this->blf_empty_field) {
+ sf.clear();
+ } else if (sf == this->blf_unset_field) {
+ sf.invalidate();
+ }
+
+ auto lr = line_range(sf.sf_begin, sf.sf_end);
+
+ if (fd.fd_meta.lvm_name == TS) {
+ sa.emplace_back(lr, logline::L_TIMESTAMP.value());
+ } else if (fd.fd_meta.lvm_name == UID) {
+ sa.emplace_back(lr, logline::L_OPID.value());
+ }
+
+ if (lr.is_valid()) {
+ values.lvv_values.emplace_back(fd.fd_meta, sbr, lr);
+ } else {
+ values.lvv_values.emplace_back(fd.fd_meta);
+ }
+ }
+ }
+
+ const logline_value_stats* stats_for_value(
+ const intern_string_t& name) const override
+ {
+ const logline_value_stats* retval = nullptr;
+
+ for (const auto& blf_field_def : this->blf_field_defs) {
+ if (blf_field_def.fd_meta.lvm_name == name) {
+ if (!blf_field_def.fd_numeric_index) {
+ break;
+ }
+ retval = &this->lf_value_stats[blf_field_def.fd_numeric_index
+ .value()];
+ break;
+ }
+ }
+
+ return retval;
+ }
+
+ bool hide_field(const intern_string_t field_name, bool val) override
+ {
+ auto fd_iter
+ = std::find_if(this->blf_field_defs.begin(),
+ this->blf_field_defs.end(),
+ [field_name](const field_def& elem) {
+ return elem.fd_meta.lvm_name == field_name;
+ });
+ if (fd_iter == this->blf_field_defs.end()) {
+ return false;
+ }
+
+ fd_iter->fd_meta.lvm_user_hidden = val;
+ return true;
+ }
+
+ std::shared_ptr<log_format> specialized(int fmt_lock = -1) override
+ {
+ auto retval = std::make_shared<bro_log_format>(*this);
+
+ retval->lf_specialized = true;
+ return retval;
+ }
+
+ class bro_log_table : public log_format_vtab_impl {
+ public:
+ explicit bro_log_table(const bro_log_format& format)
+ : log_format_vtab_impl(format), blt_format(format)
+ {
+ }
+
+ void get_columns(std::vector<vtab_column>& cols) const override
+ {
+ for (const auto& fd : this->blt_format.blf_field_defs) {
+ std::pair<int, unsigned int> type_pair
+ = log_vtab_impl::logline_value_to_sqlite_type(
+ fd.fd_meta.lvm_kind);
+
+ cols.emplace_back(fd.fd_meta.lvm_name.to_string(),
+ type_pair.first,
+ fd.fd_collator,
+ false,
+ "",
+ type_pair.second);
+ }
+ }
+
+ void get_foreign_keys(
+ std::vector<std::string>& keys_inout) const override
+ {
+ this->log_vtab_impl::get_foreign_keys(keys_inout);
+
+ for (const auto& fd : this->blt_format.blf_field_defs) {
+ if (fd.fd_meta.lvm_identifier) {
+ keys_inout.push_back(fd.fd_meta.lvm_name.to_string());
+ }
+ }
+ }
+
+ const bro_log_format& blt_format;
+ };
+
+ static std::map<intern_string_t, std::shared_ptr<bro_log_table>>&
+ get_tables()
+ {
+ static std::map<intern_string_t, std::shared_ptr<bro_log_table>> retval;
+
+ return retval;
+ }
+
+ std::shared_ptr<log_vtab_impl> get_vtab_impl() const override
+ {
+ if (this->blf_format_name.empty()) {
+ return nullptr;
+ }
+
+ std::shared_ptr<bro_log_table> retval = nullptr;
+
+ auto& tables = get_tables();
+ auto iter = tables.find(this->blf_format_name);
+ if (iter == tables.end()) {
+ retval = std::make_shared<bro_log_table>(*this);
+ tables[this->blf_format_name] = retval;
+ }
+
+ return retval;
+ }
+
+ void get_subline(const logline& ll,
+ shared_buffer_ref& sbr,
+ bool full_message) override
+ {
+ }
+
+ intern_string_t blf_format_name;
+ intern_string_t blf_separator;
+ intern_string_t blf_set_separator;
+ intern_string_t blf_empty_field;
+ intern_string_t blf_unset_field;
+ std::vector<field_def> blf_field_defs;
+};
+
+struct ws_separated_string {
+ const char* ss_str;
+ size_t ss_len;
+
+ explicit ws_separated_string(const char* str = nullptr, size_t len = -1)
+ : ss_str(str), ss_len(len)
+ {
+ }
+
+ struct iterator {
+ enum class state_t {
+ NORMAL,
+ QUOTED,
+ };
+
+ const ws_separated_string& i_parent;
+ const char* i_pos;
+ const char* i_next_pos;
+ size_t i_index{0};
+ state_t i_state{state_t::NORMAL};
+
+ iterator(const ws_separated_string& ss, const char* pos)
+ : i_parent(ss), i_pos(pos), i_next_pos(pos)
+ {
+ this->update();
+ }
+
+ void update()
+ {
+ const auto& ss = this->i_parent;
+ bool done = false;
+
+ while (!done && this->i_next_pos < (ss.ss_str + ss.ss_len)) {
+ switch (this->i_state) {
+ case state_t::NORMAL:
+ if (*this->i_next_pos == '"') {
+ this->i_state = state_t::QUOTED;
+ } else if (isspace(*this->i_next_pos)) {
+ done = true;
+ }
+ break;
+ case state_t::QUOTED:
+ if (*this->i_next_pos == '"') {
+ this->i_state = state_t::NORMAL;
+ }
+ break;
+ }
+ if (!done) {
+ this->i_next_pos += 1;
+ }
+ }
+ }
+
+ iterator& operator++()
+ {
+ const auto& ss = this->i_parent;
+
+ this->i_pos = this->i_next_pos;
+ while (this->i_pos < (ss.ss_str + ss.ss_len)
+ && isspace(*this->i_pos))
+ {
+ this->i_pos += 1;
+ this->i_next_pos += 1;
+ }
+ this->update();
+ this->i_index += 1;
+
+ return *this;
+ }
+
+ string_fragment operator*()
+ {
+ const auto& ss = this->i_parent;
+ int end = this->i_next_pos - ss.ss_str;
+
+ return string_fragment(ss.ss_str, this->i_pos - ss.ss_str, end);
+ }
+
+ bool operator==(const iterator& other) const
+ {
+ return (&this->i_parent == &other.i_parent)
+ && (this->i_pos == other.i_pos);
+ }
+
+ bool operator!=(const iterator& other) const
+ {
+ return !(*this == other);
+ }
+
+ size_t index() const { return this->i_index; }
+ };
+
+ iterator begin() { return {*this, this->ss_str}; }
+
+ iterator end() { return {*this, this->ss_str + this->ss_len}; }
+};
+
+class w3c_log_format : public log_format {
+public:
+ struct field_def {
+ const intern_string_t fd_name;
+ logline_value_meta fd_meta;
+ std::string fd_collator;
+ nonstd::optional<size_t> fd_numeric_index;
+
+ explicit field_def(const intern_string_t name)
+ : fd_name(name), fd_meta(intern_string::lookup(sql_safe_ident(
+ name.to_string_fragment())),
+ value_kind_t::VALUE_TEXT)
+ {
+ }
+
+ field_def(const intern_string_t name, logline_value_meta meta)
+ : fd_name(name), fd_meta(meta)
+ {
+ }
+
+ field_def(int col,
+ const char* name,
+ value_kind_t kind,
+ bool ident = false,
+ std::string coll = "")
+ : fd_name(intern_string::lookup(name)),
+ fd_meta(
+ intern_string::lookup(sql_safe_ident(string_fragment(name))),
+ kind,
+ col),
+ fd_collator(std::move(coll))
+ {
+ this->fd_meta.lvm_identifier = ident;
+ }
+
+ field_def& with_kind(value_kind_t kind,
+ bool identifier = false,
+ const std::string& collator = "")
+ {
+ this->fd_meta.lvm_kind = kind;
+ this->fd_meta.lvm_identifier = identifier;
+ this->fd_collator = collator;
+ return *this;
+ }
+
+ field_def& with_numeric_index(int index)
+ {
+ this->fd_numeric_index = index;
+ return *this;
+ }
+ };
+
+ struct field_to_struct_t {
+ field_to_struct_t(const char* prefix, const char* struct_name)
+ : fs_prefix(prefix),
+ fs_struct_name(intern_string::lookup(struct_name))
+ {
+ }
+
+ const char* fs_prefix;
+ intern_string_t fs_struct_name;
+ };
+
+ static const std::vector<field_def> KNOWN_FIELDS;
+ const static std::vector<field_to_struct_t> KNOWN_STRUCT_FIELDS;
+
+ w3c_log_format()
+ {
+ this->lf_is_self_describing = true;
+ this->lf_time_ordered = false;
+ }
+
+ const intern_string_t get_name() const override
+ {
+ static const intern_string_t name(intern_string::lookup("w3c"));
+
+ return this->wlf_format_name.empty() ? name : this->wlf_format_name;
+ }
+
+ void clear() override
+ {
+ this->log_format::clear();
+ this->wlf_time_scanner.clear();
+ this->wlf_format_name.clear();
+ this->wlf_field_defs.clear();
+ }
+
+ scan_result_t scan_int(std::vector<logline>& dst,
+ const line_info& li,
+ shared_buffer_ref& sbr)
+ {
+ static const intern_string_t F_DATE = intern_string::lookup("date");
+ static const intern_string_t F_DATE_LOCAL
+ = intern_string::lookup("date-local");
+ static const intern_string_t F_DATE_UTC
+ = intern_string::lookup("date-UTC");
+ static const intern_string_t F_TIME = intern_string::lookup("time");
+ static const intern_string_t F_TIME_LOCAL
+ = intern_string::lookup("time-local");
+ static const intern_string_t F_TIME_UTC
+ = intern_string::lookup("time-UTC");
+ static const intern_string_t F_STATUS_CODE
+ = intern_string::lookup("sc-status");
+
+ ws_separated_string ss(sbr.get_data(), sbr.length());
+ struct timeval date_tv {
+ 0, 0
+ }, time_tv{0, 0};
+ struct exttm date_tm, time_tm;
+ bool found_date = false, found_time = false;
+ log_level_t level = LEVEL_INFO;
+
+ for (auto iter = ss.begin(); iter != ss.end(); ++iter) {
+ if (iter.index() >= this->wlf_field_defs.size()) {
+ level = LEVEL_INVALID;
+ break;
+ }
+
+ const field_def& fd = this->wlf_field_defs[iter.index()];
+ string_fragment sf = *iter;
+
+ if (sf.startswith("#")) {
+ if (sf == "#Date:") {
+ auto sbr_sf_opt
+ = sbr.to_string_fragment().consume_n(sf.length());
+
+ if (sbr_sf_opt) {
+ auto sbr_sf = sbr_sf_opt.value().trim();
+ date_time_scanner dts;
+ struct exttm tm;
+ struct timeval tv;
+
+ if (dts.scan(sbr_sf.data(),
+ sbr_sf.length(),
+ nullptr,
+ &tm,
+ tv))
+ {
+ this->lf_date_time.set_base_time(tv.tv_sec,
+ tm.et_tm);
+ this->wlf_time_scanner.set_base_time(tv.tv_sec,
+ tm.et_tm);
+ }
+ }
+ }
+ dst.emplace_back(
+ li.li_file_range.fr_offset, 0, 0, LEVEL_IGNORE, 0);
+ return scan_match{0};
+ }
+
+ sf = sf.trim("\" \t");
+ if (F_DATE == fd.fd_name || F_DATE_LOCAL == fd.fd_name
+ || F_DATE_UTC == fd.fd_name)
+ {
+ if (this->lf_date_time.scan(
+ sf.data(), sf.length(), nullptr, &date_tm, date_tv))
+ {
+ this->lf_timestamp_flags |= date_tm.et_flags;
+ found_date = true;
+ }
+ } else if (F_TIME == fd.fd_name || F_TIME_LOCAL == fd.fd_name
+ || F_TIME_UTC == fd.fd_name)
+ {
+ if (this->wlf_time_scanner.scan(
+ sf.data(), sf.length(), nullptr, &time_tm, time_tv))
+ {
+ this->lf_timestamp_flags |= time_tm.et_flags;
+ found_time = true;
+ }
+ } else if (F_STATUS_CODE == fd.fd_name) {
+ if (!sf.empty() && sf[0] >= '4') {
+ level = LEVEL_ERROR;
+ }
+ }
+
+ if (fd.fd_numeric_index) {
+ switch (fd.fd_meta.lvm_kind) {
+ case value_kind_t::VALUE_INTEGER:
+ case value_kind_t::VALUE_FLOAT: {
+ char field_copy[sf.length() + 1];
+ double val;
+
+ if (sscanf(sf.to_string(field_copy), "%lf", &val) == 1)
+ {
+ this->lf_value_stats[fd.fd_numeric_index.value()]
+ .add_value(val);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ if (found_time) {
+ struct exttm tm = time_tm;
+ struct timeval tv;
+
+ if (found_date) {
+ tm.et_tm.tm_year = date_tm.et_tm.tm_year;
+ tm.et_tm.tm_mday = date_tm.et_tm.tm_mday;
+ tm.et_tm.tm_mon = date_tm.et_tm.tm_mon;
+ tm.et_tm.tm_wday = date_tm.et_tm.tm_wday;
+ tm.et_tm.tm_yday = date_tm.et_tm.tm_yday;
+ }
+
+ tv.tv_sec = tm2sec(&tm.et_tm);
+ tv.tv_usec = tm.et_nsec / 1000;
+
+ if (!this->lf_specialized) {
+ for (auto& ll : dst) {
+ ll.set_ignore(true);
+ }
+ }
+ dst.emplace_back(li.li_file_range.fr_offset, tv, level, 0);
+ return scan_match{0};
+ }
+
+ return scan_no_match{};
+ }
+
+ scan_result_t scan(logfile& lf,
+ std::vector<logline>& dst,
+ const line_info& li,
+ shared_buffer_ref& sbr,
+ scan_batch_context& sbc) override
+ {
+ static const auto* W3C_LOG_NAME = intern_string::lookup("w3c_log");
+ static const auto* X_FIELDS_NAME = intern_string::lookup("x_fields");
+ static auto X_FIELDS_IDX = 0;
+
+ if (li.li_partial) {
+ return scan_incomplete{};
+ }
+
+ if (!this->wlf_format_name.empty()) {
+ return this->scan_int(dst, li, sbr);
+ }
+
+ if (dst.empty() || dst.size() > 20 || sbr.empty()
+ || sbr.get_data()[0] == '#')
+ {
+ return scan_no_match{};
+ }
+
+ this->clear();
+
+ for (auto line_iter = dst.begin(); line_iter != dst.end(); ++line_iter)
+ {
+ auto next_read_result = lf.read_line(line_iter);
+
+ if (next_read_result.isErr()) {
+ return scan_no_match{"unable to read first line"};
+ }
+
+ auto line = next_read_result.unwrap();
+ ws_separated_string ss(line.get_data(), line.length());
+ auto iter = ss.begin();
+
+ string_fragment directive = *iter;
+
+ if (directive.empty() || directive[0] != '#') {
+ continue;
+ }
+
+ ++iter;
+ if (iter == ss.end()) {
+ continue;
+ }
+
+ if (directive == "#Date:") {
+ date_time_scanner dts;
+ struct exttm tm;
+ struct timeval tv;
+
+ if (dts.scan(line.get_data_at(directive.length() + 1),
+ line.length() - directive.length() - 1,
+ nullptr,
+ &tm,
+ tv))
+ {
+ this->lf_date_time.set_base_time(tv.tv_sec, tm.et_tm);
+ this->wlf_time_scanner.set_base_time(tv.tv_sec, tm.et_tm);
+ }
+ } else if (directive == "#Fields:" && this->wlf_field_defs.empty())
+ {
+ int numeric_count = 0;
+
+ do {
+ auto sf = (*iter).trim(")");
+
+ auto field_iter = std::find_if(
+ begin(KNOWN_FIELDS),
+ end(KNOWN_FIELDS),
+ [&sf](auto elem) { return sf == elem.fd_name; });
+ if (field_iter != end(KNOWN_FIELDS)) {
+ this->wlf_field_defs.emplace_back(*field_iter);
+ } else if (sf == "date" || sf == "time") {
+ this->wlf_field_defs.emplace_back(
+ intern_string::lookup(sf));
+ } else {
+ const auto fs_iter = std::find_if(
+ begin(KNOWN_STRUCT_FIELDS),
+ end(KNOWN_STRUCT_FIELDS),
+ [&sf](auto elem) {
+ return sf.startswith(elem.fs_prefix);
+ });
+ if (fs_iter != end(KNOWN_STRUCT_FIELDS)) {
+ auto field_name
+ = intern_string::lookup(sf.substr(3));
+ this->wlf_field_defs.emplace_back(
+ field_name,
+ logline_value_meta(
+ field_name,
+ value_kind_t::VALUE_TEXT,
+ KNOWN_FIELDS.size() + 1
+ + std::distance(
+ begin(KNOWN_STRUCT_FIELDS),
+ fs_iter),
+ this)
+ .with_struct_name(fs_iter->fs_struct_name));
+ } else {
+ auto field_name = intern_string::lookup(sf);
+ this->wlf_field_defs.emplace_back(
+ field_name,
+ logline_value_meta(
+ field_name,
+ value_kind_t::VALUE_TEXT,
+ KNOWN_FIELDS.size() + X_FIELDS_IDX,
+ this)
+ .with_struct_name(X_FIELDS_NAME));
+ }
+ }
+ auto& fd = this->wlf_field_defs.back();
+ fd.fd_meta.lvm_format = nonstd::make_optional(this);
+ switch (fd.fd_meta.lvm_kind) {
+ case value_kind_t::VALUE_FLOAT:
+ case value_kind_t::VALUE_INTEGER:
+ fd.with_numeric_index(numeric_count);
+ numeric_count += 1;
+ break;
+ default:
+ break;
+ }
+
+ ++iter;
+ } while (iter != ss.end());
+
+ this->wlf_format_name = W3C_LOG_NAME;
+ this->lf_value_stats.resize(numeric_count);
+ }
+ }
+
+ if (!this->wlf_format_name.empty() && !this->wlf_field_defs.empty()) {
+ return this->scan_int(dst, li, sbr);
+ }
+
+ this->wlf_format_name.clear();
+ this->lf_value_stats.clear();
+
+ return scan_no_match{};
+ }
+
+ void annotate(uint64_t line_number,
+ string_attrs_t& sa,
+ logline_value_vector& values,
+ bool annotate_module) const override
+ {
+ auto& sbr = values.lvv_sbr;
+ ws_separated_string ss(sbr.get_data(), sbr.length());
+
+ for (auto iter = ss.begin(); iter != ss.end(); ++iter) {
+ string_fragment sf = *iter;
+
+ if (iter.index() >= this->wlf_field_defs.size()) {
+ sa.emplace_back(line_range{sf.sf_begin, -1},
+ SA_INVALID.value("extra fields detected"));
+ return;
+ }
+
+ const field_def& fd = this->wlf_field_defs[iter.index()];
+
+ if (sf == "-") {
+ sf.invalidate();
+ }
+
+ auto lr = line_range(sf.sf_begin, sf.sf_end);
+
+ if (lr.is_valid()) {
+ values.lvv_values.emplace_back(fd.fd_meta, sbr, lr);
+ if (sf.startswith("\"")) {
+ auto& meta = values.lvv_values.back().lv_meta;
+
+ if (meta.lvm_kind == value_kind_t::VALUE_TEXT) {
+ meta.lvm_kind = value_kind_t::VALUE_W3C_QUOTED;
+ } else {
+ meta.lvm_kind = value_kind_t::VALUE_NULL;
+ }
+ }
+ } else {
+ values.lvv_values.emplace_back(fd.fd_meta);
+ }
+ }
+ }
+
+ const logline_value_stats* stats_for_value(
+ const intern_string_t& name) const override
+ {
+ const logline_value_stats* retval = nullptr;
+
+ for (const auto& wlf_field_def : this->wlf_field_defs) {
+ if (wlf_field_def.fd_meta.lvm_name == name) {
+ if (!wlf_field_def.fd_numeric_index) {
+ break;
+ }
+ retval = &this->lf_value_stats[wlf_field_def.fd_numeric_index
+ .value()];
+ break;
+ }
+ }
+
+ return retval;
+ }
+
+ bool hide_field(const intern_string_t field_name, bool val) override
+ {
+ auto fd_iter
+ = std::find_if(this->wlf_field_defs.begin(),
+ this->wlf_field_defs.end(),
+ [field_name](const field_def& elem) {
+ return elem.fd_meta.lvm_name == field_name;
+ });
+ if (fd_iter == this->wlf_field_defs.end()) {
+ return false;
+ }
+
+ fd_iter->fd_meta.lvm_user_hidden = val;
+ return true;
+ }
+
+ std::shared_ptr<log_format> specialized(int fmt_lock = -1) override
+ {
+ auto retval = std::make_shared<w3c_log_format>(*this);
+
+ retval->lf_specialized = true;
+ return retval;
+ }
+
+ class w3c_log_table : public log_format_vtab_impl {
+ public:
+ explicit w3c_log_table(const w3c_log_format& format)
+ : log_format_vtab_impl(format), wlt_format(format)
+ {
+ }
+
+ void get_columns(std::vector<vtab_column>& cols) const override
+ {
+ for (const auto& fd : KNOWN_FIELDS) {
+ auto type_pair = log_vtab_impl::logline_value_to_sqlite_type(
+ fd.fd_meta.lvm_kind);
+
+ cols.emplace_back(fd.fd_meta.lvm_name.to_string(),
+ type_pair.first,
+ fd.fd_collator,
+ false,
+ "",
+ type_pair.second);
+ }
+ cols.emplace_back("x_fields");
+ cols.back().with_comment(
+ "A JSON-object that contains fields that are not first-class "
+ "columns");
+ for (const auto& fs : KNOWN_STRUCT_FIELDS) {
+ cols.emplace_back(fs.fs_struct_name.to_string());
+ }
+ };
+
+ void get_foreign_keys(
+ std::vector<std::string>& keys_inout) const override
+ {
+ this->log_vtab_impl::get_foreign_keys(keys_inout);
+
+ for (const auto& fd : KNOWN_FIELDS) {
+ if (fd.fd_meta.lvm_identifier) {
+ keys_inout.push_back(fd.fd_meta.lvm_name.to_string());
+ }
+ }
+ }
+
+ const w3c_log_format& wlt_format;
+ };
+
+ static std::map<intern_string_t, std::shared_ptr<w3c_log_table>>&
+ get_tables()
+ {
+ static std::map<intern_string_t, std::shared_ptr<w3c_log_table>> retval;
+
+ return retval;
+ }
+
+ std::shared_ptr<log_vtab_impl> get_vtab_impl() const override
+ {
+ if (this->wlf_format_name.empty()) {
+ return nullptr;
+ }
+
+ std::shared_ptr<w3c_log_table> retval = nullptr;
+
+ auto& tables = get_tables();
+ auto iter = tables.find(this->wlf_format_name);
+ if (iter == tables.end()) {
+ retval = std::make_shared<w3c_log_table>(*this);
+ tables[this->wlf_format_name] = retval;
+ }
+
+ return retval;
+ }
+
+ void get_subline(const logline& ll,
+ shared_buffer_ref& sbr,
+ bool full_message) override
+ {
+ }
+
+ date_time_scanner wlf_time_scanner;
+ intern_string_t wlf_format_name;
+ std::vector<field_def> wlf_field_defs;
+};
+
+static int KNOWN_FIELD_INDEX = 0;
+const std::vector<w3c_log_format::field_def> w3c_log_format::KNOWN_FIELDS = {
+ {
+ KNOWN_FIELD_INDEX++,
+ "cs-method",
+ value_kind_t::VALUE_TEXT,
+ true,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "c-ip",
+ value_kind_t::VALUE_TEXT,
+ true,
+ "ipaddress",
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "cs-bytes",
+ value_kind_t::VALUE_INTEGER,
+ false,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "cs-host",
+ value_kind_t::VALUE_TEXT,
+ true,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "cs-uri-stem",
+ value_kind_t::VALUE_TEXT,
+ true,
+ "naturalnocase",
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "cs-uri-query",
+ value_kind_t::VALUE_TEXT,
+ false,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "cs-username",
+ value_kind_t::VALUE_TEXT,
+ false,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "cs-version",
+ value_kind_t::VALUE_TEXT,
+ true,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "s-ip",
+ value_kind_t::VALUE_TEXT,
+ true,
+ "ipaddress",
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "s-port",
+ value_kind_t::VALUE_INTEGER,
+ true,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "s-computername",
+ value_kind_t::VALUE_TEXT,
+ true,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "s-sitename",
+ value_kind_t::VALUE_TEXT,
+ true,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "sc-bytes",
+ value_kind_t::VALUE_INTEGER,
+ false,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "sc-status",
+ value_kind_t::VALUE_INTEGER,
+ false,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "sc-substatus",
+ value_kind_t::VALUE_INTEGER,
+ false,
+ },
+ {
+ KNOWN_FIELD_INDEX++,
+ "time-taken",
+ value_kind_t::VALUE_FLOAT,
+ false,
+ },
+};
+
+const std::vector<w3c_log_format::field_to_struct_t>
+ w3c_log_format::KNOWN_STRUCT_FIELDS = {
+ {"cs(", "cs_headers"},
+ {"sc(", "sc_headers"},
+ {"rs(", "rs_headers"},
+ {"sr(", "sr_headers"},
+};
+
+struct logfmt_pair_handler {
+ explicit logfmt_pair_handler(date_time_scanner& dts) : lph_dt_scanner(dts)
+ {
+ }
+
+ bool process_value(const string_fragment& value_frag)
+ {
+ if (this->lph_key_frag == "time" || this->lph_key_frag == "ts") {
+ if (!this->lph_dt_scanner.scan(value_frag.data(),
+ value_frag.length(),
+ nullptr,
+ &this->lph_time_tm,
+ this->lph_tv))
+ {
+ return false;
+ }
+ this->lph_found_time = true;
+ } else if (this->lph_key_frag == "level") {
+ this->lph_level
+ = string2level(value_frag.data(), value_frag.length());
+ }
+ return true;
+ }
+
+ date_time_scanner& lph_dt_scanner;
+ bool lph_found_time{false};
+ struct exttm lph_time_tm {};
+ struct timeval lph_tv {
+ 0, 0
+ };
+ log_level_t lph_level{log_level_t::LEVEL_INFO};
+ string_fragment lph_key_frag{""};
+};
+
+class logfmt_format : public log_format {
+public:
+ const intern_string_t get_name() const override
+ {
+ const static auto NAME = intern_string::lookup("logfmt_log");
+
+ return NAME;
+ }
+
+ class logfmt_log_table : public log_format_vtab_impl {
+ public:
+ logfmt_log_table(const log_format& format)
+ : log_format_vtab_impl(format)
+ {
+ }
+
+ void get_columns(std::vector<vtab_column>& cols) const override
+ {
+ static const auto FIELDS = std::string("fields");
+
+ cols.emplace_back(FIELDS);
+ }
+ };
+
+ std::shared_ptr<log_vtab_impl> get_vtab_impl() const override
+ {
+ static auto retval = std::make_shared<logfmt_log_table>(*this);
+
+ return retval;
+ }
+
+ scan_result_t scan(logfile& lf,
+ std::vector<logline>& dst,
+ const line_info& li,
+ shared_buffer_ref& sbr,
+ scan_batch_context& sbc) override
+ {
+ auto p = logfmt::parser(sbr.to_string_fragment());
+ scan_result_t retval = scan_no_match{};
+ bool done = false;
+ logfmt_pair_handler lph(this->lf_date_time);
+
+ while (!done) {
+ auto parse_result = p.step();
+
+ done = parse_result.match(
+ [](const logfmt::parser::end_of_input&) { return true; },
+ [&lph](const logfmt::parser::kvpair& kvp) {
+ lph.lph_key_frag = kvp.first;
+
+ return kvp.second.match(
+ [](const logfmt::parser::bool_value& bv) {
+ return false;
+ },
+ [&lph](const logfmt::parser::float_value& fv) {
+ return lph.process_value(fv.fv_str_value);
+ },
+ [&lph](const logfmt::parser::int_value& iv) {
+ return lph.process_value(iv.iv_str_value);
+ },
+ [&lph](const logfmt::parser::quoted_value& qv) {
+ auto_mem<yajl_handle_t> handle(yajl_free);
+ yajl_callbacks cb;
+
+ memset(&cb, 0, sizeof(cb));
+ handle = yajl_alloc(&cb, nullptr, &lph);
+ cb.yajl_string = +[](void* ctx,
+ const unsigned char* str,
+ size_t len) -> int {
+ auto& lph = *((logfmt_pair_handler*) ctx);
+ string_fragment value_frag{str, 0, (int) len};
+
+ return lph.process_value(value_frag);
+ };
+
+ if (yajl_parse(
+ handle,
+ (const unsigned char*) qv.qv_value.data(),
+ qv.qv_value.length())
+ != yajl_status_ok
+ || yajl_complete_parse(handle)
+ != yajl_status_ok)
+ {
+ log_debug("json parsing failed");
+ string_fragment unq_frag{
+ qv.qv_value.sf_string,
+ qv.qv_value.sf_begin + 1,
+ qv.qv_value.sf_end - 1,
+ };
+
+ return lph.process_value(unq_frag);
+ }
+
+ return false;
+ },
+ [&lph](const logfmt::parser::unquoted_value& uv) {
+ return lph.process_value(uv.uv_value);
+ });
+ },
+ [](const logfmt::parser::error& err) {
+ // log_error("logfmt parse error: %s", err.e_msg.c_str());
+ return true;
+ });
+ }
+
+ if (lph.lph_found_time) {
+ dst.emplace_back(
+ li.li_file_range.fr_offset, lph.lph_tv, lph.lph_level);
+ retval = scan_match{0};
+ }
+
+ return retval;
+ }
+
+ void annotate(uint64_t line_number,
+ string_attrs_t& sa,
+ logline_value_vector& values,
+ bool annotate_module) const override
+ {
+ static const auto FIELDS_NAME = intern_string::lookup("fields");
+
+ auto& sbr = values.lvv_sbr;
+ auto p = logfmt::parser(sbr.to_string_fragment());
+ bool done = false;
+
+ while (!done) {
+ auto parse_result = p.step();
+
+ done = parse_result.match(
+ [](const logfmt::parser::end_of_input&) { return true; },
+ [this, &sa, &values](const logfmt::parser::kvpair& kvp) {
+ auto value_frag = kvp.second.match(
+ [this, &kvp, &values](
+ const logfmt::parser::bool_value& bv) {
+ auto lvm = logline_value_meta{intern_string::lookup(
+ kvp.first),
+ value_kind_t::
+ VALUE_INTEGER,
+ 0,
+ (log_format*) this}
+ .with_struct_name(FIELDS_NAME);
+ values.lvv_values.emplace_back(lvm, bv.bv_value);
+
+ return bv.bv_str_value;
+ },
+ [this, &kvp, &values](
+ const logfmt::parser::int_value& iv) {
+ auto lvm = logline_value_meta{intern_string::lookup(
+ kvp.first),
+ value_kind_t::
+ VALUE_INTEGER,
+ 0,
+ (log_format*) this}
+ .with_struct_name(FIELDS_NAME);
+ values.lvv_values.emplace_back(lvm, iv.iv_value);
+
+ return iv.iv_str_value;
+ },
+ [this, &kvp, &values](
+ const logfmt::parser::float_value& fv) {
+ auto lvm = logline_value_meta{intern_string::lookup(
+ kvp.first),
+ value_kind_t::
+ VALUE_INTEGER,
+ 0,
+ (log_format*) this}
+ .with_struct_name(FIELDS_NAME);
+ values.lvv_values.emplace_back(lvm, fv.fv_value);
+
+ return fv.fv_str_value;
+ },
+ [](const logfmt::parser::quoted_value& qv) {
+ return qv.qv_value;
+ },
+ [](const logfmt::parser::unquoted_value& uv) {
+ return uv.uv_value;
+ });
+ auto value_lr
+ = line_range{value_frag.sf_begin, value_frag.sf_end};
+
+ if (kvp.first == "time" || kvp.first == "ts") {
+ sa.emplace_back(value_lr, logline::L_TIMESTAMP.value());
+ } else if (kvp.first == "level") {
+ } else if (kvp.first == "msg") {
+ sa.emplace_back(value_lr, SA_BODY.value());
+ } else if (!kvp.second.is<logfmt::parser::int_value>()
+ && !kvp.second.is<logfmt::parser::bool_value>())
+ {
+ auto lvm
+ = logline_value_meta{intern_string::lookup(
+ kvp.first),
+ value_frag.startswith("\"")
+ ? value_kind_t::VALUE_JSON
+ : value_kind_t::VALUE_TEXT,
+ 0,
+ (log_format*) this}
+ .with_struct_name(FIELDS_NAME);
+ values.lvv_values.emplace_back(lvm, value_frag);
+ }
+
+ return false;
+ },
+ [line_number, &sbr](const logfmt::parser::error& err) {
+ log_error("bad line %.*s", sbr.length(), sbr.get_data());
+ log_error("%lld:logfmt parse error: %s",
+ line_number,
+ err.e_msg.c_str());
+ return true;
+ });
+ }
+ }
+
+ std::shared_ptr<log_format> specialized(int fmt_lock) override
+ {
+ auto retval = std::make_shared<logfmt_format>(*this);
+
+ retval->lf_specialized = true;
+ return retval;
+ }
+};
+
+static auto format_binder = injector::bind_multiple<log_format>()
+ .add<logfmt_format>()
+ .add<bro_log_format>()
+ .add<w3c_log_format>()
+ .add<generic_log_format>();
diff --git a/src/log_format_loader.cc b/src/log_format_loader.cc
new file mode 100644
index 0000000..4e75b0b
--- /dev/null
+++ b/src/log_format_loader.cc
@@ -0,0 +1,1488 @@
+/**
+ * Copyright (c) 2013-2016, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_format_loader.cc
+ */
+
+#include <map>
+#include <string>
+
+#include "log_format_loader.hh"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <glob.h>
+#include <libgen.h>
+#include <sys/stat.h>
+
+#include "base/auto_fd.hh"
+#include "base/fs_util.hh"
+#include "base/paths.hh"
+#include "base/string_util.hh"
+#include "bin2c.hh"
+#include "builtin-scripts.h"
+#include "builtin-sh-scripts.h"
+#include "config.h"
+#include "default-formats.h"
+#include "file_format.hh"
+#include "fmt/format.h"
+#include "lnav_config.hh"
+#include "log_format_ext.hh"
+#include "sql_util.hh"
+#include "yajlpp/yajlpp.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+static void extract_metadata(string_fragment, struct script_metadata& meta_out);
+
+using log_formats_map_t
+ = std::map<intern_string_t, std::shared_ptr<external_log_format>>;
+
+using namespace lnav::roles::literals;
+
+static auto intern_lifetime = intern_string::get_table_lifetime();
+static log_formats_map_t LOG_FORMATS;
+
+struct loader_userdata {
+ yajlpp_parse_context* ud_parse_context{nullptr};
+ std::string ud_file_schema;
+ ghc::filesystem::path ud_format_path;
+ std::vector<intern_string_t>* ud_format_names{nullptr};
+ std::vector<lnav::console::user_message>* ud_errors{nullptr};
+};
+
+static external_log_format*
+ensure_format(const yajlpp_provider_context& ypc, loader_userdata* ud)
+{
+ const intern_string_t name = ypc.get_substr_i(0);
+ auto* formats = ud->ud_format_names;
+ auto* retval = LOG_FORMATS[name].get();
+ if (retval == nullptr) {
+ LOG_FORMATS[name] = std::make_shared<external_log_format>(name);
+ retval = LOG_FORMATS[name].get();
+ log_debug("Loading format -- %s", name.get());
+ }
+ retval->elf_source_path.insert(ud->ud_format_path.parent_path().string());
+
+ if (find(formats->begin(), formats->end(), name) == formats->end()) {
+ formats->push_back(name);
+ }
+
+ if (!ud->ud_format_path.empty()) {
+ const intern_string_t i_src_path
+ = intern_string::lookup(ud->ud_format_path.string());
+ auto srcs_iter = retval->elf_format_sources.find(i_src_path);
+ if (srcs_iter == retval->elf_format_sources.end()) {
+ retval->elf_format_source_order.emplace_back(ud->ud_format_path);
+ retval->elf_format_sources[i_src_path]
+ = ud->ud_parse_context->get_line_number();
+ }
+ }
+
+ if (ud->ud_format_path.empty()) {
+ retval->elf_builtin_format = true;
+ }
+
+ return retval;
+}
+
+static external_log_format::pattern*
+pattern_provider(const yajlpp_provider_context& ypc, external_log_format* elf)
+{
+ auto regex_name = ypc.get_substr(0);
+ auto& pat = elf->elf_patterns[regex_name];
+
+ if (pat.get() == nullptr) {
+ pat = std::make_shared<external_log_format::pattern>();
+ }
+
+ if (pat->p_config_path.empty()) {
+ pat->p_name = intern_string::lookup(regex_name);
+ pat->p_config_path = fmt::format(
+ FMT_STRING("/{}/regex/{}"), elf->get_name(), regex_name);
+ }
+
+ return pat.get();
+}
+
+static external_log_format::value_def*
+value_def_provider(const yajlpp_provider_context& ypc, external_log_format* elf)
+{
+ const intern_string_t value_name = ypc.get_substr_i(0);
+
+ auto iter = elf->elf_value_defs.find(value_name);
+ std::shared_ptr<external_log_format::value_def> retval;
+
+ if (iter == elf->elf_value_defs.end()) {
+ retval = std::make_shared<external_log_format::value_def>(
+ value_name, value_kind_t::VALUE_TEXT, -1, elf);
+ elf->elf_value_defs[value_name] = retval;
+ elf->elf_value_def_order.emplace_back(retval);
+ } else {
+ retval = iter->second;
+ }
+
+ return retval.get();
+}
+
+static format_tag_def*
+format_tag_def_provider(const yajlpp_provider_context& ypc,
+ external_log_format* elf)
+{
+ const intern_string_t tag_name = ypc.get_substr_i(0);
+
+ auto iter = elf->lf_tag_defs.find(tag_name);
+ std::shared_ptr<format_tag_def> retval;
+
+ if (iter == elf->lf_tag_defs.end()) {
+ auto tag_with_hash = fmt::format(FMT_STRING("#{}"), tag_name);
+ retval = std::make_shared<format_tag_def>(tag_with_hash);
+ elf->lf_tag_defs[tag_name] = retval;
+ } else {
+ retval = iter->second;
+ }
+
+ return retval.get();
+}
+
+static scaling_factor*
+scaling_factor_provider(const yajlpp_provider_context& ypc,
+ external_log_format::value_def* value_def)
+{
+ auto scale_name = ypc.get_substr_i(0);
+ auto& retval = value_def->vd_unit_scaling[scale_name];
+
+ return &retval;
+}
+
+static external_log_format::json_format_element&
+ensure_json_format_element(external_log_format* elf, int index)
+{
+ elf->jlf_line_format.resize(index + 1);
+
+ return elf->jlf_line_format[index];
+}
+
+static external_log_format::json_format_element*
+line_format_provider(const yajlpp_provider_context& ypc,
+ external_log_format* elf)
+{
+ auto& jfe = ensure_json_format_element(elf, ypc.ypc_index);
+
+ jfe.jfe_type = external_log_format::json_log_field::VARIABLE;
+
+ return &jfe;
+}
+
+static int
+read_format_bool(yajlpp_parse_context* ypc, int val)
+{
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+ auto field_name = ypc->get_path_fragment(1);
+
+ if (field_name == "convert-to-local-time") {
+ elf->lf_date_time.dts_local_time = val;
+ } else if (field_name == "json") {
+ if (val) {
+ elf->elf_type = external_log_format::elf_type_t::ELF_TYPE_JSON;
+ }
+ }
+
+ return 1;
+}
+
+static int
+read_format_double(yajlpp_parse_context* ypc, double val)
+{
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+ auto field_name = ypc->get_path_fragment(1);
+
+ if (field_name == "timestamp-divisor") {
+ if (val <= 0) {
+ ypc->report_error(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(fmt::to_string(val))
+ .append(" is not a valid value for ")
+ .append_quoted(lnav::roles::symbol(
+ ypc->get_full_path().to_string())))
+ .with_reason("value cannot be less than or equal to zero")
+ .with_snippet(ypc->get_snippet())
+ .with_help(ypc->ypc_current_handler->get_help_text(ypc)));
+ }
+ elf->elf_timestamp_divisor = val;
+ }
+
+ return 1;
+}
+
+static int
+read_format_int(yajlpp_parse_context* ypc, long long val)
+{
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+ auto field_name = ypc->get_path_fragment(1);
+
+ if (field_name == "timestamp-divisor") {
+ if (val <= 0) {
+ ypc->report_error(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(fmt::to_string(val))
+ .append(" is not a valid value for ")
+ .append_quoted(lnav::roles::symbol(
+ ypc->get_full_path().to_string())))
+ .with_reason("value cannot be less than or equal to zero")
+ .with_snippet(ypc->get_snippet())
+ .with_help(ypc->ypc_current_handler->get_help_text(ypc)));
+ }
+ elf->elf_timestamp_divisor = val;
+ }
+
+ return 1;
+}
+
+static int
+read_format_field(yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len)
+{
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+ auto leading_slash = len > 0 && str[0] == '/';
+ auto value = std::string((const char*) (leading_slash ? str + 1 : str),
+ leading_slash ? len - 1 : len);
+ auto field_name = ypc->get_path_fragment(1);
+
+ if (field_name == "timestamp-format") {
+ elf->lf_timestamp_format.push_back(intern_string::lookup(value)->get());
+ } else if (field_name == "module-field") {
+ elf->elf_module_id_field = intern_string::lookup(value);
+ elf->elf_container = true;
+ } else if (field_name == "mime-types") {
+ auto value_opt = ypc->ypc_current_handler->to_enum_value(value);
+ if (value_opt) {
+ elf->elf_mime_types.insert((file_format_t) *value_opt);
+ }
+ }
+
+ return 1;
+}
+
+static int
+read_levels(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
+{
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+ auto regex = std::string((const char*) str, len);
+ auto level_name_or_number = ypc->get_path_fragment(2);
+ log_level_t level = string2level(level_name_or_number.c_str());
+ auto value_frag = string_fragment::from_bytes(str, len);
+
+ elf->elf_level_patterns[level].lp_pcre.pp_path = ypc->get_full_path();
+ auto compile_res = lnav::pcre2pp::code::from(value_frag);
+ if (compile_res.isErr()) {
+ static const intern_string_t PATTERN_SRC
+ = intern_string::lookup("pattern");
+ auto ce = compile_res.unwrapErr();
+ ypc->ypc_current_handler->report_error(
+ ypc,
+ value_frag.to_string(),
+ lnav::console::to_user_message(PATTERN_SRC, ce));
+ } else {
+ elf->elf_level_patterns[level].lp_pcre.pp_value
+ = compile_res.unwrap().to_shared();
+ }
+
+ return 1;
+}
+
+static int
+read_level_int(yajlpp_parse_context* ypc, long long val)
+{
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+ auto level_name_or_number = ypc->get_path_fragment(2);
+ log_level_t level = string2level(level_name_or_number.c_str());
+
+ elf->elf_level_pairs.emplace_back(val, level);
+
+ return 1;
+}
+
+static int
+read_action_def(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
+{
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+ auto action_name = ypc->get_path_fragment(2);
+ auto field_name = ypc->get_path_fragment(3);
+ auto val = std::string((const char*) str, len);
+
+ elf->lf_action_defs[action_name].ad_name = action_name;
+ if (field_name == "label") {
+ elf->lf_action_defs[action_name].ad_label = val;
+ }
+
+ return 1;
+}
+
+static int
+read_action_bool(yajlpp_parse_context* ypc, int val)
+{
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+ auto action_name = ypc->get_path_fragment(2);
+ auto field_name = ypc->get_path_fragment(3);
+
+ elf->lf_action_defs[action_name].ad_capture_output = val;
+
+ return 1;
+}
+
+static int
+read_action_cmd(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
+{
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+ auto action_name = ypc->get_path_fragment(2);
+ auto field_name = ypc->get_path_fragment(3);
+ auto val = std::string((const char*) str, len);
+
+ elf->lf_action_defs[action_name].ad_name = action_name;
+ elf->lf_action_defs[action_name].ad_cmdline.push_back(val);
+
+ return 1;
+}
+
+static external_log_format::sample&
+ensure_sample(external_log_format* elf, int index)
+{
+ elf->elf_samples.resize(index + 1);
+
+ return elf->elf_samples[index];
+}
+
+static external_log_format::sample*
+sample_provider(const yajlpp_provider_context& ypc, external_log_format* elf)
+{
+ auto& sample = ensure_sample(elf, ypc.ypc_index);
+
+ return &sample;
+}
+
+static int
+read_json_constant(yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len)
+{
+ auto val = std::string((const char*) str, len);
+ auto elf = (external_log_format*) ypc->ypc_obj_stack.top();
+
+ ypc->ypc_array_index.back() += 1;
+ auto& jfe = ensure_json_format_element(elf, ypc->ypc_array_index.back());
+ jfe.jfe_type = external_log_format::json_log_field::CONSTANT;
+ jfe.jfe_default_value = val;
+
+ return 1;
+}
+
+static const struct json_path_container pattern_handlers = {
+ yajlpp::property_handler("pattern")
+ .with_synopsis("<message-regex>")
+ .with_description(
+ "The regular expression to match a log message and capture fields.")
+ .with_min_length(1)
+ .for_field(&external_log_format::pattern::p_pcre),
+ yajlpp::property_handler("module-format")
+ .with_synopsis("<bool>")
+ .with_description(
+ "If true, this pattern will only be used to parse message bodies "
+ "of container formats, like syslog")
+ .for_field(&external_log_format::pattern::p_module_format),
+};
+
+static const json_path_handler_base::enum_value_t SUBSECOND_UNIT_ENUM[] = {
+ {"milli", log_format::subsecond_unit::milli},
+ {"micro", log_format::subsecond_unit::micro},
+ {"nano", log_format::subsecond_unit::nano},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const json_path_handler_base::enum_value_t ALIGN_ENUM[] = {
+ {"left", external_log_format::json_format_element::align_t::LEFT},
+ {"right", external_log_format::json_format_element::align_t::RIGHT},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const json_path_handler_base::enum_value_t OVERFLOW_ENUM[] = {
+ {"abbrev", external_log_format::json_format_element::overflow_t::ABBREV},
+ {"truncate",
+ external_log_format::json_format_element::overflow_t::TRUNCATE},
+ {"dot-dot", external_log_format::json_format_element::overflow_t::DOTDOT},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const json_path_handler_base::enum_value_t TRANSFORM_ENUM[] = {
+ {"none", external_log_format::json_format_element::transform_t::NONE},
+ {"uppercase",
+ external_log_format::json_format_element::transform_t::UPPERCASE},
+ {"lowercase",
+ external_log_format::json_format_element::transform_t::LOWERCASE},
+ {"capitalize",
+ external_log_format::json_format_element::transform_t::CAPITALIZE},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const struct json_path_container line_format_handlers = {
+ yajlpp::property_handler("field")
+ .with_synopsis("<field-name>")
+ .with_description(
+ "The name of the field to substitute at this position")
+ .for_field(&external_log_format::json_format_element::jfe_value),
+
+ yajlpp::property_handler("default-value")
+ .with_synopsis("<string>")
+ .with_description(
+ "The default value for this position if the field is null")
+ .for_field(
+ &external_log_format::json_format_element::jfe_default_value),
+
+ yajlpp::property_handler("timestamp-format")
+ .with_synopsis("<string>")
+ .with_min_length(1)
+ .with_description("The strftime(3) format for this field")
+ .for_field(&external_log_format::json_format_element::jfe_ts_format),
+
+ yajlpp::property_handler("min-width")
+ .with_min_value(0)
+ .with_synopsis("<size>")
+ .with_description("The minimum width of the field")
+ .for_field(&external_log_format::json_format_element::jfe_min_width),
+
+ yajlpp::property_handler("auto-width")
+ .with_description("Automatically detect the necessary width of the "
+ "field based on the observed values")
+ .for_field(&external_log_format::json_format_element::jfe_auto_width),
+
+ yajlpp::property_handler("max-width")
+ .with_min_value(0)
+ .with_synopsis("<size>")
+ .with_description("The maximum width of the field")
+ .for_field(&external_log_format::json_format_element::jfe_max_width),
+
+ yajlpp::property_handler("align")
+ .with_synopsis("left|right")
+ .with_description(
+ "Align the text in the column to the left or right side")
+ .with_enum_values(ALIGN_ENUM)
+ .for_field(&external_log_format::json_format_element::jfe_align),
+
+ yajlpp::property_handler("overflow")
+ .with_synopsis("abbrev|truncate|dot-dot")
+ .with_description("Overflow style")
+ .with_enum_values(OVERFLOW_ENUM)
+ .for_field(&external_log_format::json_format_element::jfe_overflow),
+
+ yajlpp::property_handler("text-transform")
+ .with_synopsis("none|uppercase|lowercase|capitalize")
+ .with_description("Text transformation")
+ .with_enum_values(TRANSFORM_ENUM)
+ .for_field(
+ &external_log_format::json_format_element::jfe_text_transform),
+
+ yajlpp::property_handler("prefix")
+ .with_synopsis("<str>")
+ .with_description("Text to prepend to the value")
+ .for_field(&external_log_format::json_format_element::jfe_prefix),
+
+ yajlpp::property_handler("suffix")
+ .with_synopsis("<str>")
+ .with_description("Text to append to the value")
+ .for_field(&external_log_format::json_format_element::jfe_suffix),
+};
+
+static const json_path_handler_base::enum_value_t KIND_ENUM[] = {
+ {"string", value_kind_t::VALUE_TEXT},
+ {"integer", value_kind_t::VALUE_INTEGER},
+ {"float", value_kind_t::VALUE_FLOAT},
+ {"boolean", value_kind_t::VALUE_BOOLEAN},
+ {"json", value_kind_t::VALUE_JSON},
+ {"struct", value_kind_t::VALUE_STRUCT},
+ {"quoted", value_kind_t::VALUE_QUOTED},
+ {"xml", value_kind_t::VALUE_XML},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const json_path_handler_base::enum_value_t SCALE_OP_ENUM[] = {
+ {"identity", scale_op_t::SO_IDENTITY},
+ {"multiply", scale_op_t::SO_MULTIPLY},
+ {"divide", scale_op_t::SO_DIVIDE},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const struct json_path_container scaling_factor_handlers = {
+ yajlpp::property_handler("op")
+ .with_enum_values(SCALE_OP_ENUM)
+ .for_field(&scaling_factor::sf_op),
+
+ yajlpp::property_handler("value").for_field(&scaling_factor::sf_value),
+};
+
+static const struct json_path_container scale_handlers = {
+ yajlpp::pattern_property_handler("(?<scale>[^/]+)")
+ .with_obj_provider(scaling_factor_provider)
+ .with_children(scaling_factor_handlers),
+};
+
+static const struct json_path_container unit_handlers = {
+ yajlpp::property_handler("field")
+ .with_synopsis("<field-name>")
+ .with_description(
+ "The name of the field that contains the units for this field")
+ .for_field(&external_log_format::value_def::vd_unit_field),
+
+ yajlpp::property_handler("scaling-factor")
+ .with_description("Transforms the numeric value by the given factor")
+ .with_children(scale_handlers),
+};
+
+static const struct json_path_container value_def_handlers = {
+ yajlpp::property_handler("kind")
+ .with_synopsis("<data-type>")
+ .with_description("The type of data in the field")
+ .with_enum_values(KIND_ENUM)
+ .for_field(&external_log_format::value_def::vd_meta,
+ &logline_value_meta::lvm_kind),
+
+ yajlpp::property_handler("collate")
+ .with_synopsis("<function>")
+ .with_description("The collating function to use for this column")
+ .for_field(&external_log_format::value_def::vd_collate),
+
+ yajlpp::property_handler("unit")
+ .with_description("Unit definitions for this field")
+ .with_children(unit_handlers),
+
+ yajlpp::property_handler("identifier")
+ .with_synopsis("<bool>")
+ .with_description("Indicates whether or not this field contains an "
+ "identifier that should be highlighted")
+ .for_field(&external_log_format::value_def::vd_meta,
+ &logline_value_meta::lvm_identifier),
+
+ yajlpp::property_handler("foreign-key")
+ .with_synopsis("<bool>")
+ .with_description("Indicates whether or not this field should be "
+ "treated as a foreign key for row in another table")
+ .for_field(&external_log_format::value_def::vd_foreign_key),
+
+ yajlpp::property_handler("hidden")
+ .with_synopsis("<bool>")
+ .with_description(
+ "Indicates whether or not this field should be hidden")
+ .for_field(&external_log_format::value_def::vd_meta,
+ &logline_value_meta::lvm_hidden),
+
+ yajlpp::property_handler("action-list#")
+ .with_synopsis("<string>")
+ .with_description("Actions to execute when this field is clicked on")
+ .for_field(&external_log_format::value_def::vd_action_list),
+
+ yajlpp::property_handler("rewriter")
+ .with_synopsis("<command>")
+ .with_description(
+ "A command that will rewrite this field when pretty-printing")
+ .for_field(&external_log_format::value_def::vd_rewriter)
+ .with_example(";SELECT :sc_status || ' (' || (SELECT message FROM "
+ "http_status_codes WHERE status = :sc_status) || ') '"),
+
+ yajlpp::property_handler("description")
+ .with_synopsis("<string>")
+ .with_description("A description of the field")
+ .for_field(&external_log_format::value_def::vd_description),
+};
+
+static const struct json_path_container highlighter_def_handlers = {
+ yajlpp::property_handler("pattern")
+ .with_synopsis("<regex>")
+ .with_description(
+ "A regular expression to highlight in logs of this format.")
+ .for_field(&external_log_format::highlighter_def::hd_pattern),
+
+ yajlpp::property_handler("color")
+ .with_synopsis("#<hex>|<name>")
+ .with_description("The color to use when highlighting this pattern.")
+ .for_field(&external_log_format::highlighter_def::hd_color),
+
+ yajlpp::property_handler("background-color")
+ .with_synopsis("#<hex>|<name>")
+ .with_description(
+ "The background color to use when highlighting this pattern.")
+ .for_field(&external_log_format::highlighter_def::hd_background_color),
+
+ yajlpp::property_handler("underline")
+ .with_synopsis("<enabled>")
+ .with_description("Highlight this pattern with an underline.")
+ .for_field(&external_log_format::highlighter_def::hd_underline),
+
+ yajlpp::property_handler("blink")
+ .with_synopsis("<enabled>")
+ .with_description("Highlight this pattern by blinking.")
+ .for_field(&external_log_format::highlighter_def::hd_blink),
+};
+
+static const json_path_handler_base::enum_value_t LEVEL_ENUM[] = {
+ {level_names[LEVEL_TRACE], LEVEL_TRACE},
+ {level_names[LEVEL_DEBUG5], LEVEL_DEBUG5},
+ {level_names[LEVEL_DEBUG4], LEVEL_DEBUG4},
+ {level_names[LEVEL_DEBUG3], LEVEL_DEBUG3},
+ {level_names[LEVEL_DEBUG2], LEVEL_DEBUG2},
+ {level_names[LEVEL_DEBUG], LEVEL_DEBUG},
+ {level_names[LEVEL_INFO], LEVEL_INFO},
+ {level_names[LEVEL_STATS], LEVEL_STATS},
+ {level_names[LEVEL_NOTICE], LEVEL_NOTICE},
+ {level_names[LEVEL_WARNING], LEVEL_WARNING},
+ {level_names[LEVEL_ERROR], LEVEL_ERROR},
+ {level_names[LEVEL_CRITICAL], LEVEL_CRITICAL},
+ {level_names[LEVEL_FATAL], LEVEL_FATAL},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const struct json_path_container sample_handlers = {
+ yajlpp::property_handler("description")
+ .with_synopsis("<text>")
+ .with_description("A description of this sample.")
+ .for_field(&external_log_format::sample::s_description),
+ yajlpp::property_handler("line")
+ .with_synopsis("<log-line>")
+ .with_description(
+ "A sample log line that should match a pattern in this format.")
+ .for_field(&external_log_format::sample::s_line),
+
+ yajlpp::property_handler("level")
+ .with_enum_values(LEVEL_ENUM)
+ .with_description("The expected level for this sample log line.")
+ .for_field(&external_log_format::sample::s_level),
+};
+
+static const json_path_handler_base::enum_value_t TYPE_ENUM[] = {
+ {"text", external_log_format::elf_type_t::ELF_TYPE_TEXT},
+ {"json", external_log_format::elf_type_t::ELF_TYPE_JSON},
+ {"csv", external_log_format::elf_type_t::ELF_TYPE_CSV},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const struct json_path_container regex_handlers = {
+ yajlpp::pattern_property_handler(R"((?<pattern_name>[^/]+))")
+ .with_description("The set of patterns used to match log messages")
+ .with_obj_provider(pattern_provider)
+ .with_children(pattern_handlers),
+};
+
+static const struct json_path_container level_handlers = {
+ yajlpp::pattern_property_handler("(?<level>trace|debug[2345]?|info|stats|"
+ "notice|warning|error|critical|fatal)")
+ .add_cb(read_levels)
+ .add_cb(read_level_int)
+ .with_synopsis("<pattern|integer>")
+ .with_description("The regular expression used to match the log text "
+ "for this level. "
+ "For JSON logs with numeric levels, this should be "
+ "the number for the corresponding level."),
+};
+
+static const struct json_path_container value_handlers = {
+ yajlpp::pattern_property_handler("(?<value_name>[^/]+)")
+ .with_description(
+ "The set of values captured by the log message patterns")
+ .with_obj_provider(value_def_provider)
+ .with_children(value_def_handlers),
+};
+
+static const struct json_path_container tag_path_handlers = {
+ yajlpp::property_handler("glob")
+ .with_synopsis("<glob>")
+ .with_description("The glob to match against file paths")
+ .with_example("*/system.log*")
+ .for_field(&format_tag_def::path_restriction::p_glob),
+};
+
+static const struct json_path_container format_tag_def_handlers = {
+ yajlpp::property_handler("paths#")
+ .with_description("Restrict tagging to the given paths")
+ .for_field(&format_tag_def::ftd_paths)
+ .with_children(tag_path_handlers),
+ yajlpp::property_handler("pattern")
+ .with_synopsis("<regex>")
+ .with_description("The regular expression to match against the body of "
+ "the log message")
+ .with_example("\\w+ is down")
+ .for_field(&format_tag_def::ftd_pattern),
+ yajlpp::property_handler("description")
+ .with_synopsis("<string>")
+ .with_description("A description of this tag")
+ .for_field(&format_tag_def::ftd_description),
+ json_path_handler("level")
+ .with_synopsis("<log-level>")
+ .with_description("Constrain hits to log messages with this level")
+ .with_enum_values(LEVEL_ENUM)
+ .for_field(&format_tag_def::ftd_level),
+};
+
+static const struct json_path_container tag_handlers = {
+ yajlpp::pattern_property_handler(R"((?<tag_name>[\w:;\._\-]+))")
+ .with_description("The name of the tag to apply")
+ .with_obj_provider(format_tag_def_provider)
+ .with_children(format_tag_def_handlers),
+};
+
+static const struct json_path_container highlight_handlers = {
+ yajlpp::pattern_property_handler(R"((?<highlight_name>[^/]+))")
+ .with_description("The definition of a highlight")
+ .with_obj_provider<external_log_format::highlighter_def,
+ external_log_format>(
+ [](const yajlpp_provider_context& ypc, external_log_format* root) {
+ auto* retval
+ = &(root->elf_highlighter_patterns[ypc.get_substr_i(0)]);
+
+ return retval;
+ })
+ .with_children(highlighter_def_handlers),
+};
+
+static const struct json_path_container action_def_handlers = {
+ json_path_handler("label", read_action_def),
+ json_path_handler("capture-output", read_action_bool),
+ json_path_handler("cmd#", read_action_cmd),
+};
+
+static const struct json_path_container action_handlers = {
+ json_path_handler(
+ lnav::pcre2pp::code::from_const("(?<action_name>\\w+)").to_shared(),
+ read_action_def)
+ .with_children(action_def_handlers),
+};
+
+static const struct json_path_container search_table_def_handlers = {
+ json_path_handler("pattern")
+ .with_synopsis("<regex>")
+ .with_description("The regular expression for this search table.")
+ .for_field(&external_log_format::search_table_def::std_pattern),
+ json_path_handler("glob")
+ .with_synopsis("<glob>")
+ .with_description("Glob pattern used to constrain hits to messages "
+ "that match the given pattern.")
+ .for_field(&external_log_format::search_table_def::std_glob),
+ json_path_handler("level")
+ .with_synopsis("<log-level>")
+ .with_description("Constrain hits to log messages with this level")
+ .with_enum_values(LEVEL_ENUM)
+ .for_field(&external_log_format::search_table_def::std_level),
+};
+
+static const struct json_path_container search_table_handlers = {
+ yajlpp::pattern_property_handler("(?<table_name>\\w+)")
+ .with_description(
+ "The set of search tables to be automatically defined")
+ .with_obj_provider<external_log_format::search_table_def,
+ external_log_format>(
+ [](const yajlpp_provider_context& ypc, external_log_format* root) {
+ auto* retval = &(root->elf_search_tables[ypc.get_substr_i(0)]);
+
+ return retval;
+ })
+ .with_children(search_table_def_handlers),
+};
+
+static const json_path_handler_base::enum_value_t MIME_TYPE_ENUM[] = {
+ {
+ "application/vnd.tcpdump.pcap",
+ file_format_t::PCAP,
+ },
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+const struct json_path_container format_handlers = {
+ yajlpp::property_handler("regex")
+ .with_description(
+ "The set of regular expressions used to match log messages")
+ .with_children(regex_handlers),
+
+ json_path_handler("json", read_format_bool)
+ .with_description(
+ R"(Indicates that log files are JSON-encoded (deprecated, use "file-type": "json"))"),
+ json_path_handler("convert-to-local-time", read_format_bool)
+ .with_description("Indicates that displayed timestamps should "
+ "automatically be converted to local time"),
+ json_path_handler("hide-extra", read_format_bool)
+ .with_description(
+ "Specifies whether extra values in JSON logs should be displayed")
+ .for_field(&external_log_format::jlf_hide_extra),
+ json_path_handler("multiline", read_format_bool)
+ .with_description("Indicates that log messages can span multiple lines")
+ .for_field(&log_format::lf_multiline),
+ json_path_handler("timestamp-divisor", read_format_double)
+ .add_cb(read_format_int)
+ .with_synopsis("<number>")
+ .with_description(
+ "The value to divide a numeric timestamp by in a JSON log."),
+ json_path_handler("file-pattern")
+ .with_description("A regular expression that restricts this format to "
+ "log files with a matching name")
+ .for_field(&external_log_format::elf_filename_pcre),
+ json_path_handler("mime-types#", read_format_field)
+ .with_description("A list of mime-types this format should be used for")
+ .with_enum_values(MIME_TYPE_ENUM),
+ json_path_handler("level-field")
+ .with_description(
+ "The name of the level field in the log message pattern")
+ .for_field(&external_log_format::elf_level_field),
+ json_path_handler("level-pointer")
+ .with_description("A regular-expression that matches the JSON-pointer "
+ "of the level property")
+ .for_field(&external_log_format::elf_level_pointer),
+ json_path_handler("timestamp-field")
+ .with_description(
+ "The name of the timestamp field in the log message pattern")
+ .for_field(&log_format::lf_timestamp_field),
+ json_path_handler("subsecond-field")
+ .with_description("The path to the property in a JSON-lines log "
+ "message that contains the sub-second time value")
+ .for_field(&log_format::lf_subsecond_field),
+ json_path_handler("subsecond-units")
+ .with_description("The units of the subsecond-field property value")
+ .with_enum_values(SUBSECOND_UNIT_ENUM)
+ .for_field(&log_format::lf_subsecond_unit),
+ json_path_handler("time-field")
+ .with_description(
+ "The name of the time field in the log message pattern. This "
+ "field should only be specified if the timestamp field only "
+ "contains a date.")
+ .for_field(&log_format::lf_time_field),
+ json_path_handler("body-field")
+ .with_description(
+ "The name of the body field in the log message pattern")
+ .for_field(&external_log_format::elf_body_field),
+ json_path_handler("url",
+ lnav::pcre2pp::code::from_const("^url#?").to_shared())
+ .add_cb(read_format_field)
+ .with_description("A URL with more information about this log format"),
+ json_path_handler("title", read_format_field)
+ .with_description("The human-readable name for this log format"),
+ json_path_handler("description", read_format_field)
+ .with_description("A longer description of this log format")
+ .for_field(&external_log_format::lf_description),
+ json_path_handler("timestamp-format#", read_format_field)
+ .with_description("An array of strptime(3)-like timestamp formats"),
+ json_path_handler("module-field", read_format_field)
+ .with_description(
+ "The name of the module field in the log message pattern"),
+ json_path_handler("opid-field", read_format_field)
+ .with_description(
+ "The name of the operation-id field in the log message pattern")
+ .for_field(&external_log_format::elf_opid_field),
+ yajlpp::property_handler("ordered-by-time")
+ .with_synopsis("<bool>")
+ .with_description(
+ "Indicates that the order of messages in the file is time-based.")
+ .for_field(&log_format::lf_time_ordered),
+ yajlpp::property_handler("level")
+ .with_description(
+ "The map of level names to patterns or integer values")
+ .with_children(level_handlers),
+
+ yajlpp::property_handler("value")
+ .with_description("The set of value definitions")
+ .with_children(value_handlers),
+
+ yajlpp::property_handler("tags")
+ .with_description("The tags to automatically apply to log messages")
+ .with_children(tag_handlers),
+
+ yajlpp::property_handler("action").with_children(action_handlers),
+ yajlpp::property_handler("sample#")
+ .with_description("An array of sample log messages to be tested "
+ "against the log message patterns")
+ .with_obj_provider(sample_provider)
+ .with_children(sample_handlers),
+
+ yajlpp::property_handler("line-format#")
+ .with_description("The display format for JSON-encoded log messages")
+ .with_obj_provider(line_format_provider)
+ .add_cb(read_json_constant)
+ .with_children(line_format_handlers),
+ json_path_handler("search-table")
+ .with_description(
+ "Search tables to automatically define for this log format")
+ .with_children(search_table_handlers),
+
+ yajlpp::property_handler("highlights")
+ .with_description("The set of highlight definitions")
+ .with_children(highlight_handlers),
+
+ yajlpp::property_handler("file-type")
+ .with_synopsis("text|json|csv")
+ .with_description("The type of file that contains the log messages")
+ .with_enum_values(TYPE_ENUM)
+ .for_field(&external_log_format::elf_type),
+
+ yajlpp::property_handler("max-unrecognized-lines")
+ .with_synopsis("<lines>")
+ .with_description("The maximum number of lines in a file to use when "
+ "detecting the format")
+ .with_min_value(1)
+ .for_field(&log_format::lf_max_unrecognized_lines),
+};
+
+static int
+read_id(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
+{
+ auto* ud = static_cast<loader_userdata*>(ypc->ypc_userdata);
+ auto file_id = std::string((const char*) str, len);
+
+ ud->ud_file_schema = file_id;
+ if (SUPPORTED_FORMAT_SCHEMAS.find(file_id)
+ == SUPPORTED_FORMAT_SCHEMAS.end())
+ {
+ const auto* handler = ypc->ypc_current_handler;
+ attr_line_t notes{"expecting one of the following $schema values:"};
+
+ for (const auto& schema : SUPPORTED_FORMAT_SCHEMAS) {
+ notes.append("\n").append(
+ lnav::roles::symbol(fmt::format(FMT_STRING(" {}"), schema)));
+ }
+ ypc->report_error(
+ lnav::console::user_message::error(
+ attr_line_t("'")
+ .append(lnav::roles::symbol(file_id))
+ .append("' is not a supported log format $schema version"))
+ .with_snippet(ypc->get_snippet())
+ .with_note(notes)
+ .with_help(handler->get_help_text(ypc)));
+ }
+
+ return 1;
+}
+
+const struct json_path_container root_format_handler = json_path_container{
+ json_path_handler("$schema", read_id)
+ .with_synopsis("The URI of the schema for this file")
+ .with_description("Specifies the type of this file"),
+
+ yajlpp::pattern_property_handler("(?<format_name>\\w+)")
+ .with_description("The definition of a log file format.")
+ .with_obj_provider(ensure_format)
+ .with_children(format_handlers),
+}
+ .with_schema_id(DEFAULT_FORMAT_SCHEMA);
+
+static void
+write_sample_file()
+{
+ for (const auto& bsf : lnav_format_json) {
+ auto sample_path = lnav::paths::dotlnav()
+ / fmt::format(FMT_STRING("formats/default/{}.sample"),
+ bsf.get_name());
+ auto sf = bsf.to_string_fragment();
+ auto_fd sample_fd;
+
+ if ((sample_fd = lnav::filesystem::openp(
+ sample_path, O_WRONLY | O_TRUNC | O_CREAT, 0644))
+ == -1
+ || (write(sample_fd.get(), sf.data(), sf.length()) == -1))
+ {
+ fprintf(stderr,
+ "error:unable to write default format file: %s -- %s\n",
+ sample_path.c_str(),
+ strerror(errno));
+ }
+ }
+
+ for (const auto& bsf : lnav_sh_scripts) {
+ auto sh_path = lnav::paths::dotlnav()
+ / fmt::format(FMT_STRING("formats/default/{}"), bsf.get_name());
+ auto sf = bsf.to_string_fragment();
+ auto_fd sh_fd;
+
+ if ((sh_fd = lnav::filesystem::openp(
+ sh_path, O_WRONLY | O_TRUNC | O_CREAT, 0755))
+ == -1
+ || write(sh_fd.get(), sf.data(), sf.length()) == -1)
+ {
+ fprintf(stderr,
+ "error:unable to write default text file: %s -- %s\n",
+ sh_path.c_str(),
+ strerror(errno));
+ }
+ }
+
+ for (const auto& bsf : lnav_scripts) {
+ struct script_metadata meta;
+ auto sf = bsf.to_string_fragment();
+ auto_fd script_fd;
+
+ extract_metadata(sf, meta);
+ auto path
+ = fmt::format(FMT_STRING("formats/default/{}.lnav"), meta.sm_name);
+ auto script_path = lnav::paths::dotlnav() / path;
+ auto stat_res = lnav::filesystem::stat_file(script_path);
+ if (stat_res.isOk() && stat_res.unwrap().st_size == sf.length()) {
+ // Assume it's the right contents and move on...
+ continue;
+ }
+ if ((script_fd = lnav::filesystem::openp(
+ script_path, O_WRONLY | O_TRUNC | O_CREAT, 0755))
+ == -1
+ || write(script_fd.get(), sf.data(), sf.length()) == -1)
+ {
+ fprintf(stderr,
+ "error:unable to write default text file: %s -- %s\n",
+ script_path.c_str(),
+ strerror(errno));
+ }
+ }
+}
+
+static void
+format_error_reporter(const yajlpp_parse_context& ypc,
+ const lnav::console::user_message& msg)
+{
+ struct loader_userdata* ud = (loader_userdata*) ypc.ypc_userdata;
+
+ ud->ud_errors->emplace_back(msg);
+}
+
+std::vector<intern_string_t>
+load_format_file(const ghc::filesystem::path& filename,
+ std::vector<lnav::console::user_message>& errors)
+{
+ std::vector<intern_string_t> retval;
+ struct loader_userdata ud;
+ auto_fd fd;
+
+ log_info("loading formats from file: %s", filename.c_str());
+ yajlpp_parse_context ypc(intern_string::lookup(filename.string()),
+ &root_format_handler);
+ ud.ud_parse_context = &ypc;
+ ud.ud_format_path = filename;
+ ud.ud_format_names = &retval;
+ ud.ud_errors = &errors;
+ ypc.ypc_userdata = &ud;
+ ypc.with_obj(ud);
+ if ((fd = lnav::filesystem::openp(filename, O_RDONLY)) == -1) {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("unable to open format file: ")
+ .append(lnav::roles::file(filename.string())))
+ .with_errno_reason());
+ } else {
+ auto_mem<yajl_handle_t> handle(yajl_free);
+ char buffer[2048];
+ off_t offset = 0;
+ ssize_t rc = -1;
+
+ handle = yajl_alloc(&ypc.ypc_callbacks, nullptr, &ypc);
+ ypc.with_handle(handle).with_error_reporter(format_error_reporter);
+ yajl_config(handle, yajl_allow_comments, 1);
+ while (true) {
+ rc = read(fd, buffer, sizeof(buffer));
+ if (rc == 0) {
+ break;
+ }
+ if (rc == -1) {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("unable to read format file: ")
+ .append(lnav::roles::file(filename.string())))
+ .with_errno_reason());
+ break;
+ }
+ if (offset == 0 && (rc > 2) && (buffer[0] == '#')
+ && (buffer[1] == '!'))
+ {
+ // Turn it into a JavaScript comment.
+ buffer[0] = buffer[1] = '/';
+ }
+ if (ypc.parse((const unsigned char*) buffer, rc) != yajl_status_ok)
+ {
+ break;
+ }
+ offset += rc;
+ }
+ if (rc == 0) {
+ ypc.complete_parse();
+ }
+
+ if (ud.ud_file_schema.empty()) {
+ static const auto SCHEMA_LINE
+ = attr_line_t()
+ .append(
+ fmt::format(FMT_STRING(" \"$schema\": \"{}\","),
+ *SUPPORTED_FORMAT_SCHEMAS.begin()))
+ .with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+
+ errors.emplace_back(
+ lnav::console::user_message::warning(
+ attr_line_t("format file is missing ")
+ .append_quoted("$schema"_symbol)
+ .append(" property"))
+ .with_snippet(lnav::console::snippet::from(
+ intern_string::lookup(filename.string()), ""))
+ .with_note("the schema specifies the supported format "
+ "version and can be used with editors to "
+ "automatically validate the file")
+ .with_help(attr_line_t("add the following property to the "
+ "top-level JSON object:\n")
+ .append(SCHEMA_LINE)));
+ }
+ }
+
+ return retval;
+}
+
+static void
+load_from_path(const ghc::filesystem::path& path,
+ std::vector<lnav::console::user_message>& errors)
+{
+ auto format_path = path / "formats/*/*.json";
+ static_root_mem<glob_t, globfree> gl;
+
+ log_info("loading formats from path: %s", format_path.c_str());
+ if (glob(format_path.c_str(), 0, nullptr, gl.inout()) == 0) {
+ for (int lpc = 0; lpc < (int) gl->gl_pathc; lpc++) {
+ auto filepath = ghc::filesystem::path(gl->gl_pathv[lpc]);
+
+ if (startswith(filepath.filename().string(), "config.")) {
+ log_info(" not loading config as format: %s",
+ filepath.c_str());
+ continue;
+ }
+
+ auto format_list = load_format_file(filepath, errors);
+ if (format_list.empty()) {
+ log_warning("Empty format file: %s", filepath.c_str());
+ } else {
+ log_info("contents of format file '%s':", filepath.c_str());
+ for (auto iter = format_list.begin(); iter != format_list.end();
+ ++iter)
+ {
+ log_info(" found format: %s", iter->get());
+ }
+ }
+ }
+ }
+}
+
+void
+load_formats(const std::vector<ghc::filesystem::path>& extra_paths,
+ std::vector<lnav::console::user_message>& errors)
+{
+ auto default_source = lnav::paths::dotlnav() / "default";
+ std::vector<intern_string_t> retval;
+ struct loader_userdata ud;
+ yajl_handle handle;
+
+ write_sample_file();
+
+ log_debug("Loading default formats");
+ for (const auto& bsf : lnav_format_json) {
+ yajlpp_parse_context ypc_builtin(intern_string::lookup(bsf.get_name()),
+ &root_format_handler);
+ handle = yajl_alloc(&ypc_builtin.ypc_callbacks, nullptr, &ypc_builtin);
+ ud.ud_parse_context = &ypc_builtin;
+ ud.ud_format_names = &retval;
+ ud.ud_errors = &errors;
+ ypc_builtin.with_obj(ud)
+ .with_handle(handle)
+ .with_error_reporter(format_error_reporter)
+ .ypc_userdata
+ = &ud;
+ yajl_config(handle, yajl_allow_comments, 1);
+ auto sf = bsf.to_string_fragment();
+ if (ypc_builtin.parse(sf) != yajl_status_ok) {
+ auto* msg = yajl_get_error(handle, 1, sf.udata(), sf.length());
+
+ errors.emplace_back(
+ lnav::console::user_message::error("invalid json")
+ .with_snippet(lnav::console::snippet::from(
+ ypc_builtin.ypc_source, attr_line_t((const char*) msg)))
+ .with_errno_reason());
+ yajl_free_error(handle, msg);
+ }
+ ypc_builtin.complete_parse();
+ yajl_free(handle);
+ }
+
+ for (const auto& extra_path : extra_paths) {
+ load_from_path(extra_path, errors);
+ }
+
+ uint8_t mod_counter = 0;
+
+ std::vector<std::shared_ptr<external_log_format>> alpha_ordered_formats;
+ for (auto iter = LOG_FORMATS.begin(); iter != LOG_FORMATS.end(); ++iter) {
+ auto& elf = iter->second;
+ elf->build(errors);
+
+ if (elf->elf_has_module_format) {
+ mod_counter += 1;
+ elf->lf_mod_index = mod_counter;
+ }
+
+ for (auto& check_iter : LOG_FORMATS) {
+ if (iter->first == check_iter.first) {
+ continue;
+ }
+
+ auto& check_elf = check_iter.second;
+ if (elf->match_samples(check_elf->elf_samples)) {
+ log_warning(
+ "Format collision, format '%s' matches sample from '%s'",
+ elf->get_name().get(),
+ check_elf->get_name().get());
+ elf->elf_collision.push_back(check_elf->get_name());
+ }
+ }
+
+ alpha_ordered_formats.push_back(elf);
+ }
+
+ auto& graph_ordered_formats = external_log_format::GRAPH_ORDERED_FORMATS;
+
+ while (!alpha_ordered_formats.empty()) {
+ std::vector<intern_string_t> popped_formats;
+
+ for (auto iter = alpha_ordered_formats.begin();
+ iter != alpha_ordered_formats.end();)
+ {
+ auto elf = *iter;
+ if (elf->elf_collision.empty()) {
+ iter = alpha_ordered_formats.erase(iter);
+ popped_formats.push_back(elf->get_name());
+ graph_ordered_formats.push_back(elf);
+ } else {
+ ++iter;
+ }
+ }
+
+ if (popped_formats.empty() && !alpha_ordered_formats.empty()) {
+ bool broke_cycle = false;
+
+ log_warning("Detected a cycle...");
+ for (const auto& elf : alpha_ordered_formats) {
+ if (elf->elf_builtin_format) {
+ log_warning(" Skipping builtin format -- %s",
+ elf->get_name().get());
+ } else {
+ log_warning(" Breaking cycle by picking -- %s",
+ elf->get_name().get());
+ elf->elf_collision.clear();
+ broke_cycle = true;
+ break;
+ }
+ }
+ if (!broke_cycle) {
+ alpha_ordered_formats.front()->elf_collision.clear();
+ }
+ }
+
+ for (const auto& elf : alpha_ordered_formats) {
+ for (auto& popped_format : popped_formats) {
+ elf->elf_collision.remove(popped_format);
+ }
+ }
+ }
+
+ log_info("Format order:") for (auto& graph_ordered_format :
+ graph_ordered_formats)
+ {
+ log_info(" %s", graph_ordered_format->get_name().get());
+ }
+
+ auto& roots = log_format::get_root_formats();
+ auto iter = std::find_if(roots.begin(), roots.end(), [](const auto& elem) {
+ return elem->get_name() == "generic_log";
+ });
+ roots.insert(
+ iter, graph_ordered_formats.begin(), graph_ordered_formats.end());
+}
+
+static void
+exec_sql_in_path(sqlite3* db,
+ const std::map<std::string, scoped_value_t>& global_vars,
+ const ghc::filesystem::path& path,
+ std::vector<lnav::console::user_message>& errors)
+{
+ auto format_path = path / "formats/*/*.sql";
+ static_root_mem<glob_t, globfree> gl;
+
+ log_info("executing SQL files in path: %s", format_path.c_str());
+ if (glob(format_path.c_str(), 0, nullptr, gl.inout()) == 0) {
+ for (int lpc = 0; lpc < (int) gl->gl_pathc; lpc++) {
+ auto filename = ghc::filesystem::path(gl->gl_pathv[lpc]);
+ auto read_res = lnav::filesystem::read_file(filename);
+
+ if (read_res.isOk()) {
+ log_info("Executing SQL file: %s", filename.c_str());
+ auto content = read_res.unwrap();
+
+ sql_execute_script(
+ db, global_vars, filename.c_str(), content.c_str(), errors);
+ } else {
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ attr_line_t("unable to read format file: ")
+ .append(lnav::roles::file(filename.string())))
+ .with_reason(read_res.unwrapErr()));
+ }
+ }
+ }
+}
+
+void
+load_format_extra(sqlite3* db,
+ const std::map<std::string, scoped_value_t>& global_vars,
+ const std::vector<ghc::filesystem::path>& extra_paths,
+ std::vector<lnav::console::user_message>& errors)
+{
+ for (const auto& extra_path : extra_paths) {
+ exec_sql_in_path(db, global_vars, extra_path, errors);
+ }
+}
+
+static void
+extract_metadata(string_fragment contents, struct script_metadata& meta_out)
+{
+ static const auto SYNO_RE = lnav::pcre2pp::code::from_const(
+ "^#\\s+@synopsis:(.*)$", PCRE2_MULTILINE);
+ static const auto DESC_RE = lnav::pcre2pp::code::from_const(
+ "^#\\s+@description:(.*)$", PCRE2_MULTILINE);
+
+ auto syno_md = SYNO_RE.create_match_data();
+ auto syno_match_res
+ = SYNO_RE.capture_from(contents).into(syno_md).matches().ignore_error();
+ if (syno_match_res) {
+ meta_out.sm_synopsis = syno_md[1]->trim().to_string();
+ }
+ auto desc_md = DESC_RE.create_match_data();
+ auto desc_match_res
+ = DESC_RE.capture_from(contents).into(desc_md).matches().ignore_error();
+ if (desc_match_res) {
+ meta_out.sm_description = desc_md[1]->trim().to_string();
+ }
+
+ if (!meta_out.sm_synopsis.empty()) {
+ size_t space = meta_out.sm_synopsis.find(' ');
+
+ if (space == std::string::npos) {
+ space = meta_out.sm_synopsis.size();
+ }
+ meta_out.sm_name = meta_out.sm_synopsis.substr(0, space);
+ }
+}
+
+void
+extract_metadata_from_file(struct script_metadata& meta_inout)
+{
+ auto stat_res = lnav::filesystem::stat_file(meta_inout.sm_path);
+ if (stat_res.isErr()) {
+ log_warning("unable to open script: %s -- %s",
+ meta_inout.sm_path.c_str(),
+ stat_res.unwrapErr().c_str());
+ return;
+ }
+
+ auto st = stat_res.unwrap();
+ if (!S_ISREG(st.st_mode)) {
+ log_warning("script is not a regular file -- %s",
+ meta_inout.sm_path.c_str());
+ return;
+ }
+
+ auto open_res = lnav::filesystem::open_file(meta_inout.sm_path, O_RDONLY);
+ if (open_res.isErr()) {
+ log_warning("unable to open script file: %s -- %s",
+ meta_inout.sm_path.c_str(),
+ open_res.unwrapErr().c_str());
+ return;
+ }
+
+ auto fd = open_res.unwrap();
+ char buffer[8 * 1024];
+ auto rc = read(fd, buffer, sizeof(buffer));
+ if (rc > 0) {
+ extract_metadata(string_fragment::from_bytes(buffer, rc), meta_inout);
+ }
+}
+
+static void
+find_format_in_path(const ghc::filesystem::path& path,
+ available_scripts& scripts)
+{
+ auto format_path = path / "formats/*/*.lnav";
+ static_root_mem<glob_t, globfree> gl;
+
+ log_debug("Searching for script in path: %s", format_path.c_str());
+ if (glob(format_path.c_str(), 0, nullptr, gl.inout()) == 0) {
+ for (int lpc = 0; lpc < (int) gl->gl_pathc; lpc++) {
+ const char* filename = basename(gl->gl_pathv[lpc]);
+ auto script_name = std::string(filename, strlen(filename) - 5);
+ struct script_metadata meta;
+
+ meta.sm_path = gl->gl_pathv[lpc];
+ meta.sm_name = script_name;
+ extract_metadata_from_file(meta);
+ scripts.as_scripts[script_name].push_back(meta);
+
+ log_debug(" found script: %s", meta.sm_path.c_str());
+ }
+ }
+}
+
+void
+find_format_scripts(const std::vector<ghc::filesystem::path>& extra_paths,
+ available_scripts& scripts)
+{
+ for (const auto& extra_path : extra_paths) {
+ find_format_in_path(extra_path, scripts);
+ }
+}
+
+void
+load_format_vtabs(log_vtab_manager* vtab_manager,
+ std::vector<lnav::console::user_message>& errors)
+{
+ auto& root_formats = LOG_FORMATS;
+
+ for (auto& root_format : root_formats) {
+ root_format.second->register_vtabs(vtab_manager, errors);
+ }
+}
diff --git a/src/log_format_loader.hh b/src/log_format_loader.hh
new file mode 100644
index 0000000..161ac88
--- /dev/null
+++ b/src/log_format_loader.hh
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_format_loader.hh
+ */
+
+#ifndef log_format_loader_hh
+#define log_format_loader_hh
+
+#include <string>
+#include <vector>
+
+#include <sqlite3.h>
+
+#include "base/intern_string.hh"
+#include "base/lnav.console.hh"
+#include "ghc/filesystem.hpp"
+#include "shlex.resolver.hh"
+
+class log_vtab_manager;
+
+std::vector<intern_string_t> load_format_file(
+ const ghc::filesystem::path& filename,
+ std::vector<lnav::console::user_message>& errors);
+
+void load_formats(const std::vector<ghc::filesystem::path>& extra_paths,
+ std::vector<lnav::console::user_message>& errors);
+
+void load_format_vtabs(log_vtab_manager* vtab_manager,
+ std::vector<lnav::console::user_message>& errors);
+
+void load_format_extra(sqlite3* db,
+ const std::map<std::string, scoped_value_t>& global_vars,
+ const std::vector<ghc::filesystem::path>& extra_paths,
+ std::vector<lnav::console::user_message>& errors);
+
+struct script_metadata {
+ ghc::filesystem::path sm_path;
+ std::string sm_name;
+ std::string sm_synopsis;
+ std::string sm_description;
+};
+
+void extract_metadata_from_file(struct script_metadata& meta_inout);
+
+struct available_scripts {
+ std::map<std::string, std::vector<script_metadata>> as_scripts;
+};
+
+void find_format_scripts(const std::vector<ghc::filesystem::path>& extra_paths,
+ available_scripts& scripts);
+
+extern const struct json_path_container format_handlers;
+extern const struct json_path_container root_format_handler;
+
+#endif
diff --git a/src/log_gutter_source.hh b/src/log_gutter_source.hh
new file mode 100644
index 0000000..b3aa6ff
--- /dev/null
+++ b/src/log_gutter_source.hh
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_log_gutter_source_hh
+#define lnav_log_gutter_source_hh
+
+#include "command_executor.hh"
+#include "logfile_sub_source.hh"
+
+class log_gutter_source : public list_gutter_source {
+public:
+ void listview_gutter_value_for_range(const listview_curses& lv,
+ int start,
+ int end,
+ chtype& ch,
+ role_t& role_out,
+ role_t& bar_role_out)
+ {
+ textview_curses* tc = (textview_curses*) &lv;
+ vis_bookmarks& bm = tc->get_bookmarks();
+ bool search_hit = false;
+
+ start -= 1;
+
+ auto next = bm[&textview_curses::BM_SEARCH].next(vis_line_t(start));
+ search_hit = (next && next.value() <= end);
+
+ next = bm[&textview_curses::BM_USER].next(vis_line_t(start));
+ if (!next) {
+ next = bm[&textview_curses::BM_META].next(vis_line_t(start));
+ }
+ if (next && next.value() <= end) {
+ ch = search_hit ? ACS_PLUS : ACS_LTEE;
+ } else {
+ ch = search_hit ? ACS_RTEE : ACS_VLINE;
+ }
+ next = bm[&logfile_sub_source::BM_ERRORS].next(vis_line_t(start));
+ if (next && next.value() <= end) {
+ role_out = role_t::VCR_ERROR;
+ bar_role_out = role_t::VCR_SCROLLBAR_ERROR;
+ } else {
+ next = bm[&logfile_sub_source::BM_WARNINGS].next(vis_line_t(start));
+ if (next && next.value() <= end) {
+ role_out = role_t::VCR_WARNING;
+ bar_role_out = role_t::VCR_SCROLLBAR_WARNING;
+ }
+ }
+ }
+};
+
+#endif
diff --git a/src/log_level.cc b/src/log_level.cc
new file mode 100644
index 0000000..e28d213
--- /dev/null
+++ b/src/log_level.cc
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "log_level.hh"
+
+#include <ctype.h>
+
+#include "config.h"
+
+const char* level_names[LEVEL__MAX + 1] = {
+ "unknown",
+ "trace",
+ "debug5",
+ "debug4",
+ "debug3",
+ "debug2",
+ "debug",
+ "info",
+ "stats",
+ "notice",
+ "warning",
+ "error",
+ "critical",
+ "fatal",
+ "invalid",
+
+ nullptr,
+};
+
+log_level_t
+abbrev2level(const char* levelstr, ssize_t len)
+{
+ if (len == 0 || levelstr[0] == '\0') {
+ return LEVEL_UNKNOWN;
+ }
+
+ switch (toupper(levelstr[0])) {
+ case 'T':
+ return LEVEL_TRACE;
+ case 'D':
+ case 'V':
+ if (len > 1) {
+ switch (levelstr[len - 1]) {
+ case '2':
+ return LEVEL_DEBUG2;
+ case '3':
+ return LEVEL_DEBUG3;
+ case '4':
+ return LEVEL_DEBUG4;
+ case '5':
+ return LEVEL_DEBUG5;
+ }
+ }
+ return LEVEL_DEBUG;
+ case 'I':
+ if (len == 7 && toupper(levelstr[1]) == 'N'
+ && toupper(levelstr[2]) == 'V' && toupper(levelstr[3]) == 'A'
+ && toupper(levelstr[4]) == 'L' && toupper(levelstr[5]) == 'I'
+ && toupper(levelstr[6]) == 'D')
+ {
+ return LEVEL_INVALID;
+ }
+ return LEVEL_INFO;
+ case 'S':
+ return LEVEL_STATS;
+ case 'N':
+ return LEVEL_NOTICE;
+ case 'W':
+ return LEVEL_WARNING;
+ case 'E':
+ return LEVEL_ERROR;
+ case 'C':
+ return LEVEL_CRITICAL;
+ case 'F':
+ return LEVEL_FATAL;
+ default:
+ return LEVEL_UNKNOWN;
+ }
+}
+
+int
+levelcmp(const char* l1, ssize_t l1_len, const char* l2, ssize_t l2_len)
+{
+ return abbrev2level(l1, l1_len) - abbrev2level(l2, l2_len);
+}
diff --git a/src/log_level.hh b/src/log_level.hh
new file mode 100644
index 0000000..65d5645
--- /dev/null
+++ b/src/log_level.hh
@@ -0,0 +1,51 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_level.hh
+ */
+
+#ifndef log_level_hh
+#define log_level_hh
+
+#include <sys/types.h>
+
+#include "base/log_level_enum.hh"
+
+extern const char* level_names[LEVEL__MAX + 1];
+
+constexpr size_t MAX_LEVEL_NAME_LEN = 8;
+
+log_level_t string2level(const char* levelstr,
+ ssize_t len = -1,
+ bool exact = false);
+
+log_level_t abbrev2level(const char* levelstr, ssize_t len = -1);
+
+int levelcmp(const char* l1, ssize_t l1_len, const char* l2, ssize_t l2_len);
+
+#endif
diff --git a/src/log_level_re.cc b/src/log_level_re.cc
new file mode 100644
index 0000000..d6fbeef
--- /dev/null
+++ b/src/log_level_re.cc
@@ -0,0 +1,718 @@
+/* Generated by re2c 2.0.3 on Wed Jan 27 16:33:33 2021 */
+#line 1 "../../lnav/src/log_level_re.re"
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "log_level.hh"
+
+#include <string.h>
+
+#include "config.h"
+
+log_level_t
+string2level(const char* levelstr, ssize_t len, bool exact)
+{
+ log_level_t retval = LEVEL_UNKNOWN;
+
+ if (len == (ssize_t) -1) {
+ len = strlen(levelstr);
+ }
+
+ if (((len == 1) || ((len > 1) && (levelstr[1] == ' ')))
+ && (retval = abbrev2level(levelstr, 1)) != LEVEL_UNKNOWN)
+ {
+ return retval;
+ }
+
+#define YYCTYPE unsigned char
+#define RET(tok) \
+ { \
+ return tok; \
+ }
+
+ const YYCTYPE* YYCURSOR = (const unsigned char*) levelstr;
+ const YYCTYPE* YYLIMIT = (const unsigned char*) levelstr + len;
+ const YYCTYPE* YYMARKER = YYCURSOR;
+ const YYCTYPE* debug_level = nullptr;
+
+#define YYPEEK() (YYCURSOR < YYLIMIT ? *YYCURSOR : 0)
+#define YYSKIP() ++YYCURSOR
+#define YYBACKUP() YYMARKER = YYCURSOR
+#define YYRESTORE() YYCURSOR = YYMARKER
+#define YYSTAGP(x) x = YYCURSOR - 1
+
+loop
+ :
+#line 71 "../../lnav/src/log_level_re.cc"
+{
+ YYCTYPE yych;
+ unsigned int yyaccept = 0;
+ yych = YYPEEK();
+ switch (yych) {
+ case 0x00:
+ goto yy2;
+ case 'C':
+ case 'c':
+ goto yy6;
+ case 'D':
+ case 'd':
+ goto yy7;
+ case 'E':
+ case 'e':
+ goto yy8;
+ case 'F':
+ case 'f':
+ goto yy9;
+ case 'I':
+ case 'i':
+ goto yy10;
+ case 'N':
+ case 'n':
+ goto yy11;
+ case 'S':
+ case 's':
+ goto yy12;
+ case 'T':
+ case 't':
+ goto yy13;
+ case 'W':
+ case 'w':
+ goto yy14;
+ default:
+ goto yy4;
+ }
+yy2:
+ YYSKIP();
+#line 73 "../../lnav/src/log_level_re.re"
+ {
+ RET(LEVEL_UNKNOWN);
+ }
+#line 102 "../../lnav/src/log_level_re.cc"
+yy4:
+ YYSKIP();
+yy5
+ :
+#line 100 "../../lnav/src/log_level_re.re"
+{
+ goto loop;
+}
+#line 108 "../../lnav/src/log_level_re.cc"
+yy6:
+ yyaccept = 0;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'R':
+ case 'r':
+ goto yy15;
+ default:
+ goto yy5;
+ }
+yy7:
+ yyaccept = 0;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'E':
+ case 'e':
+ goto yy17;
+ default:
+ goto yy5;
+ }
+yy8:
+ yyaccept = 0;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'R':
+ case 'r':
+ goto yy18;
+ default:
+ goto yy5;
+ }
+yy9:
+ yyaccept = 0;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'A':
+ case 'a':
+ goto yy19;
+ default:
+ goto yy5;
+ }
+yy10:
+ yyaccept = 0;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'N':
+ case 'n':
+ goto yy20;
+ default:
+ goto yy5;
+ }
+yy11:
+ yyaccept = 0;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'O':
+ case 'o':
+ goto yy21;
+ default:
+ goto yy5;
+ }
+yy12:
+ yyaccept = 0;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'E':
+ case 'e':
+ goto yy22;
+ case 'T':
+ case 't':
+ goto yy23;
+ default:
+ goto yy5;
+ }
+yy13:
+ yyaccept = 0;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'R':
+ case 'r':
+ goto yy24;
+ default:
+ goto yy5;
+ }
+yy14:
+ yyaccept = 0;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'A':
+ case 'a':
+ goto yy25;
+ default:
+ goto yy5;
+ }
+yy15:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'I':
+ case 'i':
+ goto yy26;
+ default:
+ goto yy16;
+ }
+yy16:
+ YYRESTORE();
+ switch (yyaccept) {
+ case 0:
+ goto yy5;
+ case 1:
+ goto yy29;
+ default:
+ goto yy48;
+ }
+yy17:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'B':
+ case 'b':
+ goto yy27;
+ default:
+ goto yy16;
+ }
+yy18:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'R':
+ case 'r':
+ goto yy28;
+ default:
+ goto yy16;
+ }
+yy19:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'T':
+ case 't':
+ goto yy30;
+ default:
+ goto yy16;
+ }
+yy20:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'F':
+ case 'f':
+ goto yy31;
+ default:
+ goto yy16;
+ }
+yy21:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'T':
+ case 't':
+ goto yy32;
+ default:
+ goto yy16;
+ }
+yy22:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'V':
+ case 'v':
+ goto yy33;
+ default:
+ goto yy16;
+ }
+yy23:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'A':
+ case 'a':
+ goto yy34;
+ default:
+ goto yy16;
+ }
+yy24:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'A':
+ case 'a':
+ goto yy35;
+ default:
+ goto yy16;
+ }
+yy25:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'R':
+ case 'r':
+ goto yy36;
+ default:
+ goto yy16;
+ }
+yy26:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'T':
+ case 't':
+ goto yy37;
+ default:
+ goto yy16;
+ }
+yy27:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'U':
+ case 'u':
+ goto yy38;
+ default:
+ goto yy16;
+ }
+yy28:
+ yyaccept = 1;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'O':
+ case 'o':
+ goto yy39;
+ default:
+ goto yy29;
+ }
+yy29
+ :
+#line 96 "../../lnav/src/log_level_re.re"
+{
+ RET(LEVEL_ERROR);
+}
+#line 320 "../../lnav/src/log_level_re.cc"
+yy30:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'A':
+ case 'a':
+ goto yy40;
+ default:
+ goto yy16;
+ }
+yy31:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'O':
+ case 'o':
+ goto yy41;
+ default:
+ goto yy16;
+ }
+yy32:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'I':
+ case 'i':
+ goto yy43;
+ default:
+ goto yy16;
+ }
+yy33:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'E':
+ case 'e':
+ goto yy44;
+ default:
+ goto yy16;
+ }
+yy34:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'T':
+ case 't':
+ goto yy45;
+ default:
+ goto yy16;
+ }
+yy35:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'C':
+ case 'c':
+ goto yy46;
+ default:
+ goto yy16;
+ }
+yy36:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'N':
+ case 'n':
+ goto yy47;
+ default:
+ goto yy16;
+ }
+yy37:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'I':
+ case 'i':
+ goto yy49;
+ default:
+ goto yy16;
+ }
+yy38:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'G':
+ case 'g':
+ goto yy50;
+ default:
+ goto yy16;
+ }
+yy39:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'R':
+ case 'r':
+ goto yy52;
+ default:
+ goto yy16;
+ }
+yy40:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'L':
+ case 'l':
+ goto yy53;
+ default:
+ goto yy16;
+ }
+yy41:
+ YYSKIP();
+#line 92 "../../lnav/src/log_level_re.re"
+ {
+ RET(LEVEL_INFO);
+ }
+#line 413 "../../lnav/src/log_level_re.cc"
+yy43:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'C':
+ case 'c':
+ goto yy55;
+ default:
+ goto yy16;
+ }
+yy44:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'R':
+ case 'r':
+ goto yy56;
+ default:
+ goto yy16;
+ }
+yy45:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'S':
+ case 's':
+ goto yy57;
+ default:
+ goto yy16;
+ }
+yy46:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'E':
+ case 'e':
+ goto yy59;
+ default:
+ goto yy16;
+ }
+yy47:
+ yyaccept = 2;
+ YYSKIP();
+ YYBACKUP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'I':
+ case 'i':
+ goto yy61;
+ default:
+ goto yy48;
+ }
+yy48
+ :
+#line 95 "../../lnav/src/log_level_re.re"
+{
+ RET(LEVEL_WARNING);
+}
+#line 459 "../../lnav/src/log_level_re.cc"
+yy49:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'C':
+ case 'c':
+ goto yy62;
+ default:
+ goto yy16;
+ }
+yy50:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ goto yy63;
+ default:
+ goto yy51;
+ }
+yy51:
+ YYSTAGP(debug_level);
+#line 75 "../../lnav/src/log_level_re.re"
+ {
+ if (debug_level == nullptr) {
+ RET(LEVEL_DEBUG);
+ }
+ switch (*debug_level) {
+ case '2':
+ RET(LEVEL_DEBUG2);
+ case '3':
+ RET(LEVEL_DEBUG3);
+ case '4':
+ RET(LEVEL_DEBUG4);
+ case '5':
+ RET(LEVEL_DEBUG5);
+ default:
+ RET(LEVEL_DEBUG);
+ }
+ }
+#line 498 "../../lnav/src/log_level_re.cc"
+yy52:
+ YYSKIP();
+ goto yy29;
+yy53:
+ YYSKIP();
+#line 99 "../../lnav/src/log_level_re.re"
+ {
+ RET(LEVEL_FATAL);
+ }
+#line 506 "../../lnav/src/log_level_re.cc"
+yy55:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'E':
+ case 'e':
+ goto yy64;
+ default:
+ goto yy16;
+ }
+yy56:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'E':
+ case 'e':
+ goto yy66;
+ default:
+ goto yy16;
+ }
+yy57:
+ YYSKIP();
+#line 94 "../../lnav/src/log_level_re.re"
+ {
+ RET(LEVEL_STATS);
+ }
+#line 527 "../../lnav/src/log_level_re.cc"
+yy59:
+ YYSKIP();
+#line 74 "../../lnav/src/log_level_re.re"
+ {
+ RET(LEVEL_TRACE);
+ }
+#line 532 "../../lnav/src/log_level_re.cc"
+yy61:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'N':
+ case 'n':
+ goto yy68;
+ default:
+ goto yy16;
+ }
+yy62:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'A':
+ case 'a':
+ goto yy69;
+ default:
+ goto yy16;
+ }
+yy63:
+ YYSKIP();
+ goto yy51;
+yy64:
+ YYSKIP();
+#line 93 "../../lnav/src/log_level_re.re"
+ {
+ RET(LEVEL_NOTICE);
+ }
+#line 556 "../../lnav/src/log_level_re.cc"
+yy66:
+ YYSKIP();
+#line 98 "../../lnav/src/log_level_re.re"
+ {
+ RET(LEVEL_CRITICAL);
+ }
+#line 561 "../../lnav/src/log_level_re.cc"
+yy68:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'G':
+ case 'g':
+ goto yy70;
+ default:
+ goto yy16;
+ }
+yy69:
+ YYSKIP();
+ yych = YYPEEK();
+ switch (yych) {
+ case 'L':
+ case 'l':
+ goto yy71;
+ default:
+ goto yy16;
+ }
+yy70:
+ YYSKIP();
+ goto yy48;
+yy71:
+ YYSKIP();
+#line 97 "../../lnav/src/log_level_re.re"
+ {
+ RET(LEVEL_CRITICAL);
+ }
+#line 585 "../../lnav/src/log_level_re.cc"
+}
+#line 102 "../../lnav/src/log_level_re.re"
+}
diff --git a/src/log_level_re.re b/src/log_level_re.re
new file mode 100644
index 0000000..87c9e18
--- /dev/null
+++ b/src/log_level_re.re
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#include <string.h>
+
+#include "log_level.hh"
+
+log_level_t string2level(const char *levelstr, ssize_t len, bool exact)
+{
+ log_level_t retval = LEVEL_UNKNOWN;
+
+ if (len == (ssize_t)-1) {
+ len = strlen(levelstr);
+ }
+
+ if (((len == 1) || ((len > 1) && (levelstr[1] == ' '))) &&
+ (retval = abbrev2level(levelstr, 1)) != LEVEL_UNKNOWN) {
+ return retval;
+ }
+
+# define YYCTYPE unsigned char
+# define RET(tok) { \
+ return tok; \
+ }
+
+ const YYCTYPE *YYCURSOR = (const unsigned char *) levelstr;
+ const YYCTYPE *YYLIMIT = (const unsigned char *) levelstr + len;
+ const YYCTYPE *YYMARKER = YYCURSOR;
+ const YYCTYPE *debug_level = nullptr;
+
+# define YYPEEK() (YYCURSOR < YYLIMIT ? *YYCURSOR : 0)
+# define YYSKIP() ++YYCURSOR
+# define YYBACKUP() YYMARKER = YYCURSOR
+# define YYRESTORE() YYCURSOR = YYMARKER
+# define YYSTAGP(x) x = YYCURSOR - 1
+
+ /*!stags:re2c format = 'const unsigned char *@@;'; */
+ loop:
+ /*!re2c
+ re2c:yyfill:enable = 0;
+ re2c:flags:input = custom;
+
+ EOF = "\x00";
+
+ EOF { RET(LEVEL_UNKNOWN); }
+ 'trace' { RET(LEVEL_TRACE); }
+ 'debug' [2-5]? @debug_level {
+ if (debug_level == nullptr) {
+ RET(LEVEL_DEBUG);
+ }
+ switch (*debug_level) {
+ case '2':
+ RET(LEVEL_DEBUG2);
+ case '3':
+ RET(LEVEL_DEBUG3);
+ case '4':
+ RET(LEVEL_DEBUG4);
+ case '5':
+ RET(LEVEL_DEBUG5);
+ default:
+ RET(LEVEL_DEBUG);
+ }
+ }
+ 'info' { RET(LEVEL_INFO); }
+ 'notice' { RET(LEVEL_NOTICE); }
+ 'stats' { RET(LEVEL_STATS); }
+ 'warn'|'warning' { RET(LEVEL_WARNING); }
+ 'err'|'error' { RET(LEVEL_ERROR); }
+ 'critical' { RET(LEVEL_CRITICAL); }
+ 'severe' { RET(LEVEL_CRITICAL); }
+ 'fatal' { RET(LEVEL_FATAL); }
+ * { goto loop; }
+
+ */
+}
diff --git a/src/log_search_table.cc b/src/log_search_table.cc
new file mode 100644
index 0000000..ccd53f6
--- /dev/null
+++ b/src/log_search_table.cc
@@ -0,0 +1,270 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "log_search_table.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "column_namer.hh"
+#include "config.h"
+#include "sql_util.hh"
+
+const static std::string MATCH_INDEX = "match_index";
+static auto match_index_name = intern_string::lookup("match_index");
+
+log_search_table::log_search_table(std::shared_ptr<lnav::pcre2pp::code> code,
+ intern_string_t table_name)
+ : log_vtab_impl(table_name), lst_regex(code),
+ lst_match_data(this->lst_regex->create_match_data())
+{
+}
+
+void
+log_search_table::get_columns_int(std::vector<vtab_column>& cols) const
+{
+ column_namer cn{column_namer::language::SQL};
+
+ if (this->lst_format != nullptr) {
+ this->lst_column_metas = this->lst_format->get_value_metadata();
+ this->lst_format_column_count = this->lst_column_metas.size();
+ cols.resize(this->lst_column_metas.size());
+ for (const auto& meta : this->lst_column_metas) {
+ if (meta.lvm_column == -1) {
+ continue;
+ }
+ auto type_pair
+ = log_vtab_impl::logline_value_to_sqlite_type(meta.lvm_kind);
+ cols[meta.lvm_column].vc_name = meta.lvm_name.to_string();
+ cols[meta.lvm_column].vc_type = type_pair.first;
+ cols[meta.lvm_column].vc_subtype = type_pair.second;
+ }
+ }
+
+ this->lst_column_metas.emplace_back(
+ match_index_name, value_kind_t::VALUE_INTEGER, cols.size());
+ cols.emplace_back(MATCH_INDEX, SQLITE_INTEGER);
+ cn.add_column(string_fragment::from_const("__all__"));
+ auto captures = this->lst_regex->get_captures();
+ for (size_t lpc = 0; lpc < this->lst_regex->get_capture_count(); lpc++) {
+ std::string collator;
+ int sqlite_type = SQLITE3_TEXT;
+
+ auto colname
+ = cn.add_column(string_fragment::from_c_str(
+ this->lst_regex->get_name_for_capture(lpc + 1)))
+ .to_string();
+ if (captures.size() == (size_t) this->lst_regex->get_capture_count()) {
+ auto cap_re = captures[lpc].to_string();
+ sqlite_type = guess_type_from_pcre(cap_re, collator);
+ switch (sqlite_type) {
+ case SQLITE_FLOAT:
+ this->lst_column_metas.emplace_back(
+ intern_string::lookup(colname),
+ value_kind_t::VALUE_FLOAT,
+ cols.size());
+ break;
+ case SQLITE_INTEGER:
+ this->lst_column_metas.emplace_back(
+ intern_string::lookup(colname),
+ value_kind_t::VALUE_INTEGER,
+ cols.size());
+ break;
+ default:
+ this->lst_column_metas.emplace_back(
+ intern_string::lookup(colname),
+ value_kind_t::VALUE_TEXT,
+ cols.size());
+ break;
+ }
+ }
+ cols.emplace_back(colname, sqlite_type, collator);
+ }
+}
+
+void
+log_search_table::get_foreign_keys(std::vector<std::string>& keys_inout) const
+{
+ log_vtab_impl::get_foreign_keys(keys_inout);
+ keys_inout.emplace_back(MATCH_INDEX);
+}
+
+bool
+log_search_table::next(log_cursor& lc, logfile_sub_source& lss)
+{
+ this->vi_attrs.clear();
+ this->lst_line_values_cache.lvv_values.clear();
+
+ if (this->lst_match_index >= 0) {
+ auto match_res = this->lst_regex->capture_from(this->lst_content)
+ .at(this->lst_remaining)
+ .into(this->lst_match_data)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+
+ if (match_res) {
+#if 0
+ log_debug("matched within line: %d",
+ this->lst_match_context.get_count());
+#endif
+ this->lst_remaining = match_res->f_remaining;
+ this->lst_match_index += 1;
+ return true;
+ }
+
+ // log_debug("done matching message");
+ this->lst_remaining.clear();
+ this->lst_match_index = -1;
+ return false;
+ }
+
+ this->lst_match_index = -1;
+
+ if (lc.is_eof()) {
+ return true;
+ }
+
+ if (!this->is_valid(lc, lss)) {
+ return false;
+ }
+
+ auto cl = lss.at(lc.lc_curr_line);
+ auto* lf = lss.find_file_ptr(cl);
+
+ auto lf_iter = lf->begin() + cl;
+
+ if (!lf_iter->is_message()) {
+ return false;
+ }
+
+ if (this->lst_mismatch_bitmap.is_bit_set(lc.lc_curr_line)) {
+ // log_debug("%d: mismatch, aborting", (int) lc.lc_curr_line);
+ return false;
+ }
+
+ // log_debug("%d: doing message", (int) lc.lc_curr_line);
+ auto& sbr = this->lst_line_values_cache.lvv_sbr;
+ lf->read_full_message(lf_iter, sbr);
+ sbr.erase_ansi();
+ lf->get_format()->annotate(
+ cl, this->vi_attrs, this->lst_line_values_cache, false);
+ this->lst_content
+ = this->lst_line_values_cache.lvv_sbr.to_string_fragment();
+
+ auto match_res = this->lst_regex->capture_from(this->lst_content)
+ .into(this->lst_match_data)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+
+ if (!match_res) {
+ this->lst_mismatch_bitmap.set_bit(lc.lc_curr_line);
+ return false;
+ }
+
+ this->lst_remaining = match_res->f_remaining;
+ this->lst_match_index = 0;
+
+ return true;
+}
+
+void
+log_search_table::extract(logfile* lf,
+ uint64_t line_number,
+ logline_value_vector& values)
+{
+ auto& line = values.lvv_sbr;
+ if (this->lst_format != nullptr) {
+ values = this->lst_line_values_cache;
+ }
+ values.lvv_values.emplace_back(
+ this->lst_column_metas[this->lst_format_column_count],
+ this->lst_match_index);
+ for (size_t lpc = 0; lpc < this->lst_regex->get_capture_count(); lpc++) {
+ const auto cap = this->lst_match_data[lpc + 1];
+ if (cap) {
+ values.lvv_values.emplace_back(
+ this->lst_column_metas[this->lst_format_column_count + 1 + lpc],
+ line,
+ to_line_range(cap.value()));
+ } else {
+ values.lvv_values.emplace_back(
+ this->lst_column_metas[this->lst_format_column_count + 1
+ + lpc]);
+ }
+ }
+}
+
+void
+log_search_table::get_primary_keys(std::vector<std::string>& keys_out) const
+{
+ keys_out.emplace_back("log_line");
+ keys_out.emplace_back("match_index");
+}
+
+void
+log_search_table::filter(log_cursor& lc, logfile_sub_source& lss)
+{
+ if (this->lst_format != nullptr) {
+ lc.lc_format_name = this->lst_format->get_name();
+ }
+ if (!this->lst_log_path_glob.empty()) {
+ lc.lc_log_path.emplace_back(SQLITE_INDEX_CONSTRAINT_GLOB,
+ this->lst_log_path_glob);
+ }
+ if (this->lst_log_level) {
+ lc.lc_level_constraint = log_cursor::level_constraint{
+ SQLITE_INDEX_CONSTRAINT_EQ,
+ this->lst_log_level.value(),
+ };
+ }
+ this->lst_match_index = -1;
+
+ if (lss.lss_index_generation != this->lst_index_generation) {
+ log_debug("%s:index generation changed from %d to %d, resetting...",
+ this->vi_name.c_str(),
+ this->lst_index_generation,
+ lss.lss_index_generation);
+ this->lst_mismatch_bitmap
+ = auto_buffer::alloc_bitmap(lss.text_line_count());
+ this->lst_index_generation = lss.lss_index_generation;
+ }
+
+ if (this->lst_mismatch_bitmap.bitmap_size() < lss.text_line_count()) {
+ this->lst_mismatch_bitmap.expand_bitmap_to(lss.text_line_count());
+ this->lst_mismatch_bitmap.resize_bitmap(lss.text_line_count());
+#if 1
+ log_debug("%s:bitmap resize %d:%d",
+ this->vi_name.c_str(),
+ this->lst_mismatch_bitmap.size(),
+ this->lst_mismatch_bitmap.capacity());
+#endif
+ }
+ if (!lc.lc_indexed_lines.empty()) {
+ lc.lc_curr_line = lc.lc_indexed_lines.back();
+ lc.lc_indexed_lines.pop_back();
+ }
+}
diff --git a/src/log_search_table.hh b/src/log_search_table.hh
new file mode 100644
index 0000000..1ae98d6
--- /dev/null
+++ b/src/log_search_table.hh
@@ -0,0 +1,83 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file log_search_table.hh
+ */
+
+#ifndef lnav_log_search_table_hh
+#define lnav_log_search_table_hh
+
+#include <string>
+#include <vector>
+
+#include "log_vtab_impl.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "shared_buffer.hh"
+
+class log_search_table : public log_vtab_impl {
+public:
+ log_search_table(std::shared_ptr<lnav::pcre2pp::code> code,
+ intern_string_t table_name);
+
+ void get_primary_keys(std::vector<std::string>& keys_out) const override;
+
+ void get_columns_int(std::vector<vtab_column>& cols) const;
+
+ void get_columns(std::vector<vtab_column>& cols) const override
+ {
+ this->get_columns_int(this->lst_cols);
+ cols = this->lst_cols;
+ }
+
+ void filter(log_cursor& lc, logfile_sub_source& lss) override;
+
+ void get_foreign_keys(std::vector<std::string>& keys_inout) const override;
+
+ bool next(log_cursor& lc, logfile_sub_source& lss) override;
+
+ void extract(logfile* lf,
+ uint64_t line_number,
+ logline_value_vector& values) override;
+
+ std::shared_ptr<lnav::pcre2pp::code> lst_regex;
+ lnav::pcre2pp::match_data lst_match_data;
+ string_fragment lst_content;
+ string_fragment lst_remaining;
+ log_format* lst_format{nullptr};
+ mutable size_t lst_format_column_count{0};
+ std::string lst_log_path_glob;
+ nonstd::optional<log_level_t> lst_log_level;
+ mutable std::vector<logline_value_meta> lst_column_metas;
+ int64_t lst_match_index{-1};
+ mutable std::vector<vtab_column> lst_cols;
+ logline_value_vector lst_line_values_cache;
+ auto_buffer lst_mismatch_bitmap{auto_buffer::alloc_bitmap(0)};
+ uint32_t lst_index_generation{0};
+};
+
+#endif
diff --git a/src/log_search_table_fwd.hh b/src/log_search_table_fwd.hh
new file mode 100644
index 0000000..cc758e1
--- /dev/null
+++ b/src/log_search_table_fwd.hh
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_log_search_table_fwd_hh
+#define lnav_log_search_table_fwd_hh
+
+#include "pcrepp/pcre2pp.hh"
+
+namespace log_search_table_ns {
+static constexpr int PATTERN_OPTIONS
+ = PCRE2_CASELESS | PCRE2_MULTILINE | PCRE2_DOTALL;
+}
+
+#endif
diff --git a/src/log_vtab_impl.cc b/src/log_vtab_impl.cc
new file mode 100644
index 0000000..7325675
--- /dev/null
+++ b/src/log_vtab_impl.cc
@@ -0,0 +1,2174 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "log_vtab_impl.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/itertools.hh"
+#include "base/lnav_log.hh"
+#include "base/string_util.hh"
+#include "config.h"
+#include "lnav_util.hh"
+#include "logfile_sub_source.hh"
+#include "sql_util.hh"
+#include "vtab_module.hh"
+#include "vtab_module_json.hh"
+#include "yajlpp/json_op.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+// #define DEBUG_INDEXING 1
+
+using namespace lnav::roles::literals;
+
+static auto intern_lifetime = intern_string::get_table_lifetime();
+
+static struct log_cursor log_cursor_latest;
+
+thread_local _log_vtab_data log_vtab_data;
+
+static const char* LOG_COLUMNS = R"( (
+ log_line INTEGER, -- The line number for the log message
+ log_part TEXT COLLATE naturalnocase, -- The partition the message is in
+ log_time DATETIME, -- The adjusted timestamp for the log message
+ log_actual_time DATETIME HIDDEN, -- The timestamp from the original log file for this message
+ log_idle_msecs INTEGER, -- The difference in time between this messages and the previous
+ log_level TEXT COLLATE loglevel, -- The log message level
+ log_mark BOOLEAN, -- True if the log message was marked
+ log_comment TEXT, -- The comment for this message
+ log_tags TEXT, -- A JSON list of tags for this message
+ log_filters TEXT, -- A JSON list of filter IDs that matched this message
+ -- BEGIN Format-specific fields:
+)";
+
+static const char* LOG_FOOTER_COLUMNS = R"(
+ -- END Format-specific fields
+ log_opid TEXT HIDDEN, -- The message's OPID
+ log_format TEXT HIDDEN, -- The name of the log file format
+ log_format_regex TEXT HIDDEN, -- The name of the regex used to parse this log message
+ log_time_msecs INTEGER HIDDEN, -- The adjusted timestamp for the log message as the number of milliseconds from the epoch
+ log_path TEXT HIDDEN COLLATE naturalnocase, -- The path to the log file this message is from
+ log_unique_path TEXT HIDDEN COLLATE naturalnocase, -- The unique portion of the path this message is from
+ log_text TEXT HIDDEN, -- The full text of the log message
+ log_body TEXT HIDDEN, -- The body of the log message
+ log_raw_text TEXT HIDDEN, -- The raw text from the log file
+ log_line_hash TEXT HIDDEN -- A hash of the first line of the log message
+)";
+
+enum class log_footer_columns : uint32_t {
+ opid,
+ format,
+ format_regex,
+ time_msecs,
+ path,
+ unique_path,
+ text,
+ body,
+ raw_text,
+ line_hash,
+};
+
+std::string
+log_vtab_impl::get_table_statement()
+{
+ std::vector<log_vtab_impl::vtab_column> cols;
+ std::vector<log_vtab_impl::vtab_column>::const_iterator iter;
+ std::ostringstream oss;
+ size_t max_name_len = 15;
+
+ oss << "CREATE TABLE " << this->get_name().to_string() << LOG_COLUMNS;
+ this->get_columns(cols);
+ this->vi_column_count = cols.size();
+ for (iter = cols.begin(); iter != cols.end(); iter++) {
+ max_name_len = std::max(max_name_len, iter->vc_name.length());
+ }
+ for (iter = cols.begin(); iter != cols.end(); iter++) {
+ auto_mem<char, sqlite3_free> coldecl;
+ auto_mem<char, sqlite3_free> colname;
+ std::string comment;
+
+ require(!iter->vc_name.empty());
+
+ if (!iter->vc_comment.empty()) {
+ comment.append(" -- ").append(iter->vc_comment);
+ }
+
+ colname = sql_quote_ident(iter->vc_name.c_str());
+ coldecl = sqlite3_mprintf(
+ " %-*s %-7s %s COLLATE %-15Q,%s\n",
+ max_name_len,
+ colname.in(),
+ sqlite3_type_to_string(iter->vc_type),
+ iter->vc_hidden ? "hidden" : "",
+ iter->vc_collator.empty() ? "BINARY" : iter->vc_collator.c_str(),
+ comment.c_str());
+ oss << coldecl;
+ }
+ oss << LOG_FOOTER_COLUMNS;
+
+ {
+ std::vector<std::string> primary_keys;
+
+ this->get_primary_keys(primary_keys);
+ if (!primary_keys.empty()) {
+ auto first = true;
+
+ oss << ", PRIMARY KEY (";
+ for (const auto& pkey : primary_keys) {
+ if (!first) {
+ oss << ", ";
+ }
+ oss << pkey;
+ first = false;
+ }
+ oss << ")\n";
+ } else {
+ oss << ", PRIMARY KEY (log_line)\n";
+ }
+ }
+
+ oss << ");\n";
+
+ log_debug("log_vtab_impl.get_table_statement() -> %s", oss.str().c_str());
+
+ return oss.str();
+}
+
+std::pair<int, unsigned int>
+log_vtab_impl::logline_value_to_sqlite_type(value_kind_t kind)
+{
+ int type = 0;
+ unsigned int subtype = 0;
+
+ switch (kind) {
+ case value_kind_t::VALUE_JSON:
+ type = SQLITE3_TEXT;
+ subtype = JSON_SUBTYPE;
+ break;
+ case value_kind_t::VALUE_NULL:
+ case value_kind_t::VALUE_TEXT:
+ case value_kind_t::VALUE_STRUCT:
+ case value_kind_t::VALUE_QUOTED:
+ case value_kind_t::VALUE_W3C_QUOTED:
+ case value_kind_t::VALUE_TIMESTAMP:
+ case value_kind_t::VALUE_XML:
+ type = SQLITE3_TEXT;
+ break;
+ case value_kind_t::VALUE_FLOAT:
+ type = SQLITE_FLOAT;
+ break;
+ case value_kind_t::VALUE_BOOLEAN:
+ case value_kind_t::VALUE_INTEGER:
+ type = SQLITE_INTEGER;
+ break;
+ case value_kind_t::VALUE_UNKNOWN:
+ case value_kind_t::VALUE__MAX:
+ ensure(0);
+ break;
+ }
+ return std::make_pair(type, subtype);
+}
+
+void
+log_vtab_impl::get_foreign_keys(std::vector<std::string>& keys_inout) const
+{
+ keys_inout.emplace_back("id");
+ keys_inout.emplace_back("parent");
+ keys_inout.emplace_back("notused");
+
+ keys_inout.emplace_back("log_line");
+ keys_inout.emplace_back("min(log_line)");
+ keys_inout.emplace_back("log_mark");
+ keys_inout.emplace_back("log_time_msecs");
+ keys_inout.emplace_back("log_top_line()");
+}
+
+void
+log_vtab_impl::extract(logfile* lf,
+ uint64_t line_number,
+ logline_value_vector& values)
+{
+ auto format = lf->get_format();
+
+ this->vi_attrs.clear();
+ format->annotate(line_number, this->vi_attrs, values, false);
+}
+
+bool
+log_vtab_impl::is_valid(log_cursor& lc, logfile_sub_source& lss)
+{
+ content_line_t cl(lss.at(lc.lc_curr_line));
+ auto* lf = lss.find_file_ptr(cl);
+ auto lf_iter = lf->begin() + cl;
+
+ if (!lf_iter->is_message()) {
+ return false;
+ }
+
+ if (!lc.lc_format_name.empty()
+ && lc.lc_format_name != lf->get_format_name())
+ {
+ return false;
+ }
+
+ if (!lc.lc_pattern_name.empty()
+ && lc.lc_pattern_name != lf->get_format_ptr()->get_pattern_name(cl))
+ {
+ return false;
+ }
+
+ if (lc.lc_level_constraint
+ && !lc.lc_level_constraint->matches(lf_iter->get_msg_level()))
+ {
+ return false;
+ }
+
+ if (!lc.lc_log_path.empty()) {
+ if (lf == lc.lc_last_log_path_match) {
+ } else if (lf == lc.lc_last_log_path_mismatch) {
+ return false;
+ } else {
+ for (const auto& path_cons : lc.lc_log_path) {
+ if (!path_cons.matches(lf->get_filename())) {
+ lc.lc_last_log_path_mismatch = lf;
+ return false;
+ }
+ }
+ lc.lc_last_log_path_match = lf;
+ }
+ }
+
+ if (!lc.lc_unique_path.empty()) {
+ if (lf == lc.lc_last_unique_path_match) {
+ } else if (lf == lc.lc_last_unique_path_mismatch) {
+ return false;
+ } else {
+ for (const auto& path_cons : lc.lc_unique_path) {
+ if (!path_cons.matches(lf->get_unique_path())) {
+ lc.lc_last_unique_path_mismatch = lf;
+ return false;
+ }
+ }
+ lc.lc_last_unique_path_match = lf;
+ }
+ }
+
+ if (lc.lc_opid && lf_iter->get_opid() != lc.lc_opid.value().value) {
+ return false;
+ }
+
+ return true;
+}
+
+struct log_vtab {
+ sqlite3_vtab base;
+ sqlite3* db;
+ textview_curses* tc{nullptr};
+ logfile_sub_source* lss{nullptr};
+ std::shared_ptr<log_vtab_impl> vi;
+};
+
+struct vtab_cursor {
+ void cache_msg(logfile* lf, logfile::const_iterator ll)
+ {
+ if (this->log_msg_line == this->log_cursor.lc_curr_line) {
+ return;
+ }
+ auto& sbr = this->line_values.lvv_sbr;
+ lf->read_full_message(ll, sbr);
+ sbr.erase_ansi();
+ this->log_msg_line = this->log_cursor.lc_curr_line;
+ }
+
+ sqlite3_vtab_cursor base;
+ struct log_cursor log_cursor;
+ vis_line_t log_msg_line{-1_vl};
+ logline_value_vector line_values;
+};
+
+static int vt_destructor(sqlite3_vtab* p_svt);
+
+static int
+vt_create(sqlite3* db,
+ void* pAux,
+ int argc,
+ const char* const* argv,
+ sqlite3_vtab** pp_vt,
+ char** pzErr)
+{
+ auto* vm = (log_vtab_manager*) pAux;
+ int rc = SQLITE_OK;
+ /* Allocate the sqlite3_vtab/vtab structure itself */
+ auto p_vt = std::make_unique<log_vtab>();
+
+ p_vt->db = db;
+
+ /* Declare the vtable's structure */
+ p_vt->vi = vm->lookup_impl(intern_string::lookup(argv[3]));
+ if (p_vt->vi == nullptr) {
+ return SQLITE_ERROR;
+ }
+ p_vt->tc = vm->get_view();
+ p_vt->lss = vm->get_source();
+ rc = sqlite3_declare_vtab(db, p_vt->vi->get_table_statement().c_str());
+
+ /* Success. Set *pp_vt and return */
+ auto loose_p_vt = p_vt.release();
+ *pp_vt = &loose_p_vt->base;
+
+ log_debug("creating log format table: %s = %p", argv[3], p_vt.get());
+
+ return rc;
+}
+
+static int
+vt_destructor(sqlite3_vtab* p_svt)
+{
+ log_vtab* p_vt = (log_vtab*) p_svt;
+
+ delete p_vt;
+
+ return SQLITE_OK;
+}
+
+static int
+vt_connect(sqlite3* db,
+ void* p_aux,
+ int argc,
+ const char* const* argv,
+ sqlite3_vtab** pp_vt,
+ char** pzErr)
+{
+ return vt_create(db, p_aux, argc, argv, pp_vt, pzErr);
+}
+
+static int
+vt_disconnect(sqlite3_vtab* pVtab)
+{
+ return vt_destructor(pVtab);
+}
+
+static int
+vt_destroy(sqlite3_vtab* p_vt)
+{
+ return vt_destructor(p_vt);
+}
+
+static int vt_next(sqlite3_vtab_cursor* cur);
+
+static int
+vt_open(sqlite3_vtab* p_svt, sqlite3_vtab_cursor** pp_cursor)
+{
+ log_vtab* p_vt = (log_vtab*) p_svt;
+
+ p_vt->base.zErrMsg = nullptr;
+
+ vtab_cursor* p_cur = new vtab_cursor();
+
+ *pp_cursor = (sqlite3_vtab_cursor*) p_cur;
+
+ p_cur->base.pVtab = p_svt;
+ p_cur->log_cursor.lc_opid = nonstd::nullopt;
+ p_cur->log_cursor.lc_curr_line = 0_vl;
+ p_cur->log_cursor.lc_end_line = vis_line_t(p_vt->lss->text_line_count());
+ p_cur->log_cursor.lc_sub_index = 0;
+
+ for (auto& ld : *p_vt->lss) {
+ auto* lf = ld->get_file_ptr();
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ lf->enable_cache();
+ }
+
+ return SQLITE_OK;
+}
+
+static int
+vt_close(sqlite3_vtab_cursor* cur)
+{
+ auto* p_cur = (vtab_cursor*) cur;
+
+ /* Free cursor struct. */
+ delete p_cur;
+
+ return SQLITE_OK;
+}
+
+static int
+vt_eof(sqlite3_vtab_cursor* cur)
+{
+ auto* vc = (vtab_cursor*) cur;
+
+ return vc->log_cursor.is_eof();
+}
+
+static void
+populate_indexed_columns(vtab_cursor* vc, log_vtab* vt)
+{
+ if (vc->log_cursor.is_eof() || vc->log_cursor.lc_indexed_columns.empty()) {
+ return;
+ }
+
+ logfile* lf = nullptr;
+
+ for (const auto& ic : vc->log_cursor.lc_indexed_columns) {
+ auto& ci = vt->vi->vi_column_indexes[ic.cc_column];
+
+ if (vc->log_cursor.lc_curr_line < ci.ci_max_line) {
+ continue;
+ }
+
+ if (lf == nullptr) {
+ content_line_t cl(vt->lss->at(vc->log_cursor.lc_curr_line));
+ uint64_t line_number;
+ auto ld = vt->lss->find_data(cl, line_number);
+ lf = (*ld)->get_file_ptr();
+ auto ll = lf->begin() + line_number;
+
+ vc->cache_msg(lf, ll);
+ require(vc->line_values.lvv_sbr.get_data() != nullptr);
+ vt->vi->extract(lf, line_number, vc->line_values);
+ }
+
+ int sub_col = ic.cc_column - VT_COL_MAX;
+ auto lv_iter = find_if(vc->line_values.lvv_values.begin(),
+ vc->line_values.lvv_values.end(),
+ logline_value_cmp(nullptr, sub_col));
+ if (lv_iter == vc->line_values.lvv_values.end()
+ || lv_iter->lv_meta.lvm_kind == value_kind_t::VALUE_NULL)
+ {
+ continue;
+ }
+
+ auto value = lv_iter->to_string();
+
+#ifdef DEBUG_INDEXING
+ log_debug("updated index for column %d %s -> %d",
+ ic.cc_column,
+ value.c_str(),
+ (int) vc->log_cursor.lc_curr_line);
+#endif
+
+ if (ci.ci_value_to_lines[value].empty()
+ || ci.ci_value_to_lines[value].back()
+ != vc->log_cursor.lc_curr_line)
+ {
+ ci.ci_value_to_lines[value].push_back(vc->log_cursor.lc_curr_line);
+ }
+ }
+}
+
+static int
+vt_next(sqlite3_vtab_cursor* cur)
+{
+ auto* vc = (vtab_cursor*) cur;
+ auto* vt = (log_vtab*) cur->pVtab;
+ auto done = false;
+
+ vc->line_values.clear();
+ if (!vc->log_cursor.lc_indexed_lines.empty()) {
+ vc->log_cursor.lc_curr_line = vc->log_cursor.lc_indexed_lines.back();
+ vc->log_cursor.lc_indexed_lines.pop_back();
+ } else {
+ vc->log_cursor.lc_curr_line += 1_vl;
+ }
+ vc->log_cursor.lc_sub_index = 0;
+ do {
+ log_cursor_latest = vc->log_cursor;
+ if (((log_cursor_latest.lc_curr_line % 1024) == 0)
+ && (log_vtab_data.lvd_progress != nullptr
+ && log_vtab_data.lvd_progress(log_cursor_latest)))
+ {
+ break;
+ }
+
+ while (vc->log_cursor.lc_curr_line != -1_vl && !vc->log_cursor.is_eof()
+ && !vt->vi->is_valid(vc->log_cursor, *vt->lss))
+ {
+ vc->log_cursor.lc_curr_line += 1_vl;
+ vc->log_cursor.lc_sub_index = 0;
+ }
+ if (vc->log_cursor.is_eof()) {
+ done = true;
+ } else {
+ done = vt->vi->next(vc->log_cursor, *vt->lss);
+ if (done) {
+ populate_indexed_columns(vc, vt);
+ } else {
+ if (!vc->log_cursor.lc_indexed_lines.empty()) {
+ vc->log_cursor.lc_curr_line
+ = vc->log_cursor.lc_indexed_lines.back();
+ vc->log_cursor.lc_indexed_lines.pop_back();
+ } else {
+ vc->log_cursor.lc_curr_line += 1_vl;
+ }
+ vc->log_cursor.lc_sub_index = 0;
+ }
+ }
+ } while (!done);
+
+ return SQLITE_OK;
+}
+
+static int
+vt_next_no_rowid(sqlite3_vtab_cursor* cur)
+{
+ auto* vc = (vtab_cursor*) cur;
+ auto* vt = (log_vtab*) cur->pVtab;
+ auto done = false;
+
+ vc->line_values.lvv_values.clear();
+ do {
+ log_cursor_latest = vc->log_cursor;
+ if (((log_cursor_latest.lc_curr_line % 1024) == 0)
+ && (log_vtab_data.lvd_progress != nullptr
+ && log_vtab_data.lvd_progress(log_cursor_latest)))
+ {
+ break;
+ }
+
+ done = vt->vi->next(vc->log_cursor, *vt->lss);
+ if (done) {
+ populate_indexed_columns(vc, vt);
+ } else if (vc->log_cursor.is_eof()) {
+ done = true;
+ } else {
+ require(vc->log_cursor.lc_curr_line < vt->lss->text_line_count());
+
+ if (!vc->log_cursor.lc_indexed_lines.empty()) {
+ vc->log_cursor.lc_curr_line
+ = vc->log_cursor.lc_indexed_lines.back();
+ vc->log_cursor.lc_indexed_lines.pop_back();
+#ifdef DEBUG_INDEXING
+ log_debug("going to next line from index %d",
+ (int) vc->log_cursor.lc_curr_line);
+#endif
+ } else {
+ vc->log_cursor.lc_curr_line += 1_vl;
+ }
+ vc->log_cursor.lc_sub_index = 0;
+ for (auto& col_constraint : vc->log_cursor.lc_indexed_columns) {
+ vt->vi->vi_column_indexes[col_constraint.cc_column].ci_max_line
+ = std::max(
+ vt->vi->vi_column_indexes[col_constraint.cc_column]
+ .ci_max_line,
+ vc->log_cursor.lc_curr_line);
+ }
+ }
+ } while (!done);
+
+#ifdef DEBUG_INDEXING
+ log_debug("vt_next_no_rowid() -> %d:%d",
+ vc->log_cursor.lc_curr_line,
+ vc->log_cursor.lc_end_line);
+#endif
+
+ return SQLITE_OK;
+}
+
+static int
+vt_column(sqlite3_vtab_cursor* cur, sqlite3_context* ctx, int col)
+{
+ auto* vc = (vtab_cursor*) cur;
+ auto* vt = (log_vtab*) cur->pVtab;
+
+#ifdef DEBUG_INDEXING
+ log_debug("vt_column(%s, %d:%d)",
+ vt->vi->get_name().get(),
+ (int) vc->log_cursor.lc_curr_line,
+ col);
+#endif
+
+ content_line_t cl(vt->lss->at(vc->log_cursor.lc_curr_line));
+ uint64_t line_number;
+ auto ld = vt->lss->find_data(cl, line_number);
+ auto* lf = (*ld)->get_file_ptr();
+ auto ll = lf->begin() + line_number;
+
+ require(col >= 0);
+
+ /* Just return the ordinal of the column requested. */
+ switch (col) {
+ case VT_COL_LINE_NUMBER: {
+ sqlite3_result_int64(ctx, vc->log_cursor.lc_curr_line);
+ break;
+ }
+
+ case VT_COL_PARTITION: {
+ auto& vb = vt->tc->get_bookmarks();
+ const auto& bv = vb[&textview_curses::BM_META];
+
+ if (bv.empty()) {
+ sqlite3_result_null(ctx);
+ } else {
+ vis_line_t curr_line(vc->log_cursor.lc_curr_line);
+ auto iter = lower_bound(bv.begin(), bv.end(), curr_line + 1_vl);
+
+ if (iter != bv.begin()) {
+ --iter;
+ auto line_meta_opt = vt->lss->find_bookmark_metadata(*iter);
+ if (line_meta_opt
+ && !line_meta_opt.value()->bm_name.empty())
+ {
+ sqlite3_result_text(
+ ctx,
+ line_meta_opt.value()->bm_name.c_str(),
+ line_meta_opt.value()->bm_name.size(),
+ SQLITE_TRANSIENT);
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ }
+ break;
+ }
+
+ case VT_COL_LOG_TIME: {
+ char buffer[64];
+
+ sql_strftime(
+ buffer, sizeof(buffer), ll->get_time(), ll->get_millis());
+ sqlite3_result_text(ctx, buffer, strlen(buffer), SQLITE_TRANSIENT);
+ break;
+ }
+
+ case VT_COL_LOG_ACTUAL_TIME: {
+ char buffer[64];
+
+ if (ll->is_time_skewed()) {
+ if (vc->line_values.lvv_values.empty()) {
+ vc->cache_msg(lf, ll);
+ require(vc->line_values.lvv_sbr.get_data() != nullptr);
+ vt->vi->extract(lf, line_number, vc->line_values);
+ }
+
+ struct line_range time_range;
+
+ time_range = find_string_attr_range(vt->vi->vi_attrs,
+ &logline::L_TIMESTAMP);
+
+ const auto* time_src
+ = vc->line_values.lvv_sbr.get_data() + time_range.lr_start;
+ struct timeval actual_tv;
+ struct exttm tm;
+
+ if (lf->get_format()->lf_date_time.scan(
+ time_src,
+ time_range.length(),
+ lf->get_format()->get_timestamp_formats(),
+ &tm,
+ actual_tv,
+ false))
+ {
+ sql_strftime(buffer, sizeof(buffer), actual_tv);
+ }
+ } else {
+ sql_strftime(
+ buffer, sizeof(buffer), ll->get_time(), ll->get_millis());
+ }
+ sqlite3_result_text(ctx, buffer, strlen(buffer), SQLITE_TRANSIENT);
+ break;
+ }
+
+ case VT_COL_IDLE_MSECS:
+ if (vc->log_cursor.lc_curr_line == 0) {
+ sqlite3_result_int64(ctx, 0);
+ } else {
+ content_line_t prev_cl(
+ vt->lss->at(vis_line_t(vc->log_cursor.lc_curr_line - 1)));
+ auto prev_lf = vt->lss->find(prev_cl);
+ auto prev_ll = prev_lf->begin() + prev_cl;
+ uint64_t prev_time, curr_line_time;
+
+ prev_time = prev_ll->get_time() * 1000ULL;
+ prev_time += prev_ll->get_millis();
+ curr_line_time = ll->get_time() * 1000ULL;
+ curr_line_time += ll->get_millis();
+ // require(curr_line_time >= prev_time);
+ sqlite3_result_int64(ctx, curr_line_time - prev_time);
+ }
+ break;
+
+ case VT_COL_LEVEL: {
+ const char* level_name = ll->get_level_name();
+
+ sqlite3_result_text(
+ ctx, level_name, strlen(level_name), SQLITE_STATIC);
+ break;
+ }
+
+ case VT_COL_MARK: {
+ sqlite3_result_int(ctx, ll->is_marked());
+ break;
+ }
+
+ case VT_COL_LOG_COMMENT: {
+ auto line_meta_opt
+ = vt->lss->find_bookmark_metadata(vc->log_cursor.lc_curr_line);
+ if (!line_meta_opt || line_meta_opt.value()->bm_comment.empty()) {
+ sqlite3_result_null(ctx);
+ } else {
+ const auto& meta = *(line_meta_opt.value());
+ sqlite3_result_text(ctx,
+ meta.bm_comment.c_str(),
+ meta.bm_comment.length(),
+ SQLITE_TRANSIENT);
+ }
+ break;
+ }
+
+ case VT_COL_LOG_TAGS: {
+ auto line_meta_opt
+ = vt->lss->find_bookmark_metadata(vc->log_cursor.lc_curr_line);
+ if (!line_meta_opt || line_meta_opt.value()->bm_tags.empty()) {
+ sqlite3_result_null(ctx);
+ } else {
+ const auto& meta = *(line_meta_opt.value());
+
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_array arr(gen);
+
+ for (const auto& str : meta.bm_tags) {
+ arr.gen(str);
+ }
+ }
+
+ to_sqlite(ctx, json_string(gen));
+ }
+ break;
+ }
+
+ case VT_COL_FILTERS: {
+ const auto& filter_mask
+ = (*ld)->ld_filter_state.lfo_filter_state.tfs_mask;
+
+ if (!filter_mask[line_number]) {
+ sqlite3_result_null(ctx);
+ } else {
+ const auto& filters = vt->lss->get_filters();
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_array arr(gen);
+
+ for (const auto& filter : filters) {
+ if (filter->lf_deleted) {
+ continue;
+ }
+
+ uint32_t mask = (1UL << filter->get_index());
+
+ if (filter_mask[line_number] & mask) {
+ arr.gen(filter->get_index());
+ }
+ }
+ }
+
+ to_sqlite(ctx, gen.to_string_fragment());
+ sqlite3_result_subtype(ctx, JSON_SUBTYPE);
+ }
+ break;
+ }
+
+ default:
+ if (col > (VT_COL_MAX + vt->vi->vi_column_count - 1)) {
+ auto footer_column = static_cast<log_footer_columns>(
+ col - (VT_COL_MAX + vt->vi->vi_column_count - 1) - 1);
+
+ switch (footer_column) {
+ case log_footer_columns::opid: {
+ if (vc->line_values.lvv_values.empty()) {
+ vc->cache_msg(lf, ll);
+ require(vc->line_values.lvv_sbr.get_data()
+ != nullptr);
+ vt->vi->extract(lf, line_number, vc->line_values);
+ }
+
+ auto opid_opt = get_string_attr(vt->vi->vi_attrs,
+ logline::L_OPID);
+ if (opid_opt) {
+ auto opid_range
+ = opid_opt.value().saw_string_attr->sa_range;
+
+ to_sqlite(
+ ctx,
+ vc->line_values.lvv_sbr.to_string_fragment(
+ opid_range.lr_start, opid_range.length()));
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ break;
+ }
+ case log_footer_columns::format: {
+ auto format_name = lf->get_format_name();
+ sqlite3_result_text(ctx,
+ format_name.get(),
+ format_name.size(),
+ SQLITE_STATIC);
+ break;
+ }
+ case log_footer_columns::format_regex: {
+ auto pat_name
+ = lf->get_format()->get_pattern_name(line_number);
+ sqlite3_result_text(ctx,
+ pat_name.get(),
+ pat_name.size(),
+ SQLITE_STATIC);
+ break;
+ }
+ case log_footer_columns::time_msecs: {
+ sqlite3_result_int64(ctx, ll->get_time_in_millis());
+ break;
+ }
+ case log_footer_columns::path: {
+ const auto& fn = lf->get_filename();
+
+ sqlite3_result_text(
+ ctx, fn.c_str(), fn.length(), SQLITE_STATIC);
+ break;
+ }
+ case log_footer_columns::unique_path: {
+ const auto& fn = lf->get_unique_path();
+
+ sqlite3_result_text(
+ ctx, fn.c_str(), fn.length(), SQLITE_STATIC);
+ break;
+ }
+ case log_footer_columns::text: {
+ shared_buffer_ref line;
+
+ lf->read_full_message(ll, line);
+ line.erase_ansi();
+ sqlite3_result_text(ctx,
+ line.get_data(),
+ line.length(),
+ SQLITE_TRANSIENT);
+ break;
+ }
+ case log_footer_columns::body: {
+ if (vc->line_values.lvv_values.empty()) {
+ vc->cache_msg(lf, ll);
+ require(vc->line_values.lvv_sbr.get_data()
+ != nullptr);
+ vt->vi->extract(lf, line_number, vc->line_values);
+ }
+
+ struct line_range body_range;
+
+ body_range = find_string_attr_range(vt->vi->vi_attrs,
+ &SA_BODY);
+ if (!body_range.is_valid()) {
+ sqlite3_result_null(ctx);
+ } else {
+ const char* msg_start
+ = vc->line_values.lvv_sbr.get_data();
+
+ sqlite3_result_text(ctx,
+ &msg_start[body_range.lr_start],
+ body_range.length(),
+ SQLITE_TRANSIENT);
+ }
+ break;
+ }
+ case log_footer_columns::raw_text: {
+ auto read_res = lf->read_raw_message(ll);
+
+ if (read_res.isErr()) {
+ auto msg = fmt::format(
+ FMT_STRING("unable to read line -- {}"),
+ read_res.unwrapErr());
+ sqlite3_result_error(
+ ctx, msg.c_str(), msg.length());
+ } else {
+ auto sbr = read_res.unwrap();
+
+ sqlite3_result_text(ctx,
+ sbr.get_data(),
+ sbr.length(),
+ SQLITE_TRANSIENT);
+ }
+ break;
+ }
+ case log_footer_columns::line_hash: {
+ auto read_res = lf->read_line(ll);
+
+ if (read_res.isErr()) {
+ auto msg = fmt::format(
+ FMT_STRING("unable to read line -- {}"),
+ read_res.unwrapErr());
+ sqlite3_result_error(
+ ctx, msg.c_str(), msg.length());
+ } else {
+ auto sbr = read_res.unwrap();
+ hasher line_hasher;
+
+ auto outbuf
+ = auto_buffer::alloc(3 + hasher::STRING_SIZE);
+ outbuf.push_back('v');
+ outbuf.push_back('1');
+ outbuf.push_back(':');
+ line_hasher.update(sbr.get_data(), sbr.length())
+ .update(cl)
+ .to_string(outbuf);
+ to_sqlite(ctx, text_auto_buffer{std::move(outbuf)});
+ }
+ break;
+ }
+ }
+ } else {
+ if (vc->line_values.lvv_values.empty()) {
+ vc->cache_msg(lf, ll);
+ require(vc->line_values.lvv_sbr.get_data() != nullptr);
+ vt->vi->extract(lf, line_number, vc->line_values);
+ }
+
+ int sub_col = col - VT_COL_MAX;
+ auto lv_iter = find_if(vc->line_values.lvv_values.begin(),
+ vc->line_values.lvv_values.end(),
+ logline_value_cmp(nullptr, sub_col));
+
+ if (lv_iter != vc->line_values.lvv_values.end()) {
+ if (!lv_iter->lv_meta.lvm_struct_name.empty()) {
+ yajlpp_gen gen;
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_map root(gen);
+
+ for (const auto& lv_struct :
+ vc->line_values.lvv_values)
+ {
+ if (lv_struct.lv_meta.lvm_column != sub_col) {
+ continue;
+ }
+
+ root.gen(lv_struct.lv_meta.lvm_name);
+ switch (lv_struct.lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_NULL:
+ root.gen();
+ break;
+ case value_kind_t::VALUE_BOOLEAN:
+ root.gen((bool) lv_struct.lv_value.i);
+ break;
+ case value_kind_t::VALUE_INTEGER:
+ root.gen(lv_struct.lv_value.i);
+ break;
+ case value_kind_t::VALUE_FLOAT:
+ root.gen(lv_struct.lv_value.d);
+ break;
+ case value_kind_t::VALUE_JSON: {
+ auto_mem<yajl_handle_t> parse_handle(
+ yajl_free);
+ json_ptr jp("");
+ json_op jo(jp);
+
+ jo.jo_ptr_callbacks
+ = json_op::gen_callbacks;
+ jo.jo_ptr_data = gen;
+ parse_handle.reset(
+ yajl_alloc(&json_op::ptr_callbacks,
+ nullptr,
+ &jo));
+
+ const auto* json_in
+ = (const unsigned char*)
+ lv_struct.text_value();
+ auto json_len = lv_struct.text_length();
+
+ if (yajl_parse(parse_handle.in(),
+ json_in,
+ json_len)
+ != yajl_status_ok
+ || yajl_complete_parse(
+ parse_handle.in())
+ != yajl_status_ok)
+ {
+ log_error(
+ "failed to parse json value: "
+ "%.*s",
+ lv_struct.text_length(),
+ lv_struct.text_value());
+ root.gen(lv_struct.to_string());
+ }
+ break;
+ }
+ default:
+ root.gen(lv_struct.to_string());
+ break;
+ }
+ }
+ }
+
+ auto sf = gen.to_string_fragment();
+ sqlite3_result_text(
+ ctx, sf.data(), sf.length(), SQLITE_TRANSIENT);
+ sqlite3_result_subtype(ctx, JSON_SUBTYPE);
+ } else {
+ switch (lv_iter->lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_NULL:
+ sqlite3_result_null(ctx);
+ break;
+ case value_kind_t::VALUE_JSON: {
+ sqlite3_result_text(ctx,
+ lv_iter->text_value(),
+ lv_iter->text_length(),
+ SQLITE_TRANSIENT);
+ sqlite3_result_subtype(ctx, JSON_SUBTYPE);
+ break;
+ }
+ case value_kind_t::VALUE_STRUCT:
+ case value_kind_t::VALUE_TEXT:
+ case value_kind_t::VALUE_XML:
+ case value_kind_t::VALUE_TIMESTAMP: {
+ sqlite3_result_text(ctx,
+ lv_iter->text_value(),
+ lv_iter->text_length(),
+ SQLITE_TRANSIENT);
+ break;
+ }
+ case value_kind_t::VALUE_W3C_QUOTED:
+ case value_kind_t::VALUE_QUOTED:
+ if (lv_iter->text_length() == 0) {
+ sqlite3_result_text(
+ ctx, "", 0, SQLITE_STATIC);
+ } else {
+ const char* text_value
+ = lv_iter->text_value();
+ size_t text_len = lv_iter->text_length();
+
+ switch (text_value[0]) {
+ case '\'':
+ case '"': {
+ char* val = (char*) sqlite3_malloc(
+ text_len);
+
+ if (val == nullptr) {
+ sqlite3_result_error_nomem(ctx);
+ } else {
+ auto unquote_func
+ = lv_iter->lv_meta.lvm_kind
+ == value_kind_t::
+ VALUE_W3C_QUOTED
+ ? unquote_w3c
+ : unquote;
+
+ size_t unquoted_len
+ = unquote_func(val,
+ text_value,
+ text_len);
+ sqlite3_result_text(
+ ctx,
+ val,
+ unquoted_len,
+ sqlite3_free);
+ }
+ break;
+ }
+ default: {
+ sqlite3_result_text(
+ ctx,
+ text_value,
+ lv_iter->text_length(),
+ SQLITE_TRANSIENT);
+ break;
+ }
+ }
+ }
+ break;
+
+ case value_kind_t::VALUE_BOOLEAN:
+ case value_kind_t::VALUE_INTEGER:
+ sqlite3_result_int64(ctx, lv_iter->lv_value.i);
+ break;
+
+ case value_kind_t::VALUE_FLOAT:
+ sqlite3_result_double(ctx, lv_iter->lv_value.d);
+ break;
+
+ case value_kind_t::VALUE_UNKNOWN:
+ case value_kind_t::VALUE__MAX:
+ require(0);
+ break;
+ }
+ }
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ }
+ break;
+ }
+
+ return SQLITE_OK;
+}
+
+static int
+vt_rowid(sqlite3_vtab_cursor* cur, sqlite_int64* p_rowid)
+{
+ vtab_cursor* p_cur = (vtab_cursor*) cur;
+
+ *p_rowid = (((uint64_t) p_cur->log_cursor.lc_curr_line) << 8)
+ | (p_cur->log_cursor.lc_sub_index & 0xff);
+
+ return SQLITE_OK;
+}
+
+void
+log_cursor::update(unsigned char op, vis_line_t vl, constraint_t cons)
+{
+ switch (op) {
+ case SQLITE_INDEX_CONSTRAINT_EQ:
+ if (vl < 0_vl) {
+ this->lc_curr_line = this->lc_end_line;
+ } else if (vl < this->lc_end_line) {
+ this->lc_curr_line = vl;
+ if (cons == constraint_t::unique) {
+ this->lc_end_line = this->lc_curr_line + 1_vl;
+ }
+ }
+ break;
+ case SQLITE_INDEX_CONSTRAINT_GE:
+ if (vl < 0_vl) {
+ vl = 0_vl;
+ }
+ this->lc_curr_line = vl;
+ break;
+ case SQLITE_INDEX_CONSTRAINT_GT:
+ if (vl < 0_vl) {
+ this->lc_curr_line = 0_vl;
+ } else {
+ this->lc_curr_line
+ = vl + (cons == constraint_t::unique ? 1_vl : 0_vl);
+ }
+ break;
+ case SQLITE_INDEX_CONSTRAINT_LE:
+ if (vl < 0_vl) {
+ this->lc_curr_line = this->lc_end_line;
+ } else {
+ this->lc_end_line
+ = vl + (cons == constraint_t::unique ? 1_vl : 0_vl);
+ }
+ break;
+ case SQLITE_INDEX_CONSTRAINT_LT:
+ if (vl <= 0_vl) {
+ this->lc_curr_line = this->lc_end_line;
+ } else {
+ this->lc_end_line = vl;
+ }
+ break;
+ }
+}
+
+log_cursor::string_constraint::string_constraint(unsigned char op,
+ std::string value)
+ : sc_op(op), sc_value(std::move(value))
+{
+ if (op == SQLITE_INDEX_CONSTRAINT_REGEXP) {
+ auto compile_res = lnav::pcre2pp::code::from(this->sc_value);
+
+ if (compile_res.isErr()) {
+ auto ce = compile_res.unwrapErr();
+ log_error("unable to compile regexp constraint: %s -- %s",
+ this->sc_value.c_str(),
+ ce.get_message().c_str());
+ } else {
+ this->sc_pattern = compile_res.unwrap().to_shared();
+ }
+ }
+}
+
+bool
+log_cursor::string_constraint::matches(const std::string& sf) const
+{
+ switch (this->sc_op) {
+ case SQLITE_INDEX_CONSTRAINT_EQ:
+ case SQLITE_INDEX_CONSTRAINT_IS:
+ return sf == this->sc_value;
+ case SQLITE_INDEX_CONSTRAINT_NE:
+ case SQLITE_INDEX_CONSTRAINT_ISNOT:
+ return sf != this->sc_value;
+ case SQLITE_INDEX_CONSTRAINT_GT:
+ return sf > this->sc_value;
+ case SQLITE_INDEX_CONSTRAINT_LE:
+ return sf <= this->sc_value;
+ case SQLITE_INDEX_CONSTRAINT_LT:
+ return sf < this->sc_value;
+ case SQLITE_INDEX_CONSTRAINT_GE:
+ return sf >= this->sc_value;
+ case SQLITE_INDEX_CONSTRAINT_LIKE:
+ return sqlite3_strlike(this->sc_value.c_str(), sf.data(), 0) == 0;
+ case SQLITE_INDEX_CONSTRAINT_GLOB:
+ return sqlite3_strglob(this->sc_value.c_str(), sf.data()) == 0;
+ case SQLITE_INDEX_CONSTRAINT_REGEXP: {
+ if (this->sc_pattern != nullptr) {
+ return this->sc_pattern->find_in(sf, PCRE2_NO_UTF_CHECK)
+ .ignore_error()
+ .has_value();
+ }
+ // return true here so that the regexp is actually run and fails
+ return true;
+ }
+ case SQLITE_INDEX_CONSTRAINT_ISNOTNULL:
+ return true;
+ default:
+ return false;
+ }
+}
+
+struct time_range {
+ nonstd::optional<timeval> tr_begin;
+ nonstd::optional<timeval> tr_end;
+
+ bool empty() const { return !this->tr_begin && !this->tr_end; }
+
+ void add(const timeval& tv)
+ {
+ if (!this->tr_begin || tv < this->tr_begin) {
+ this->tr_begin = tv;
+ }
+ if (!this->tr_end || this->tr_end < tv) {
+ this->tr_end = tv;
+ }
+ }
+};
+
+static int
+vt_filter(sqlite3_vtab_cursor* p_vtc,
+ int idxNum,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv)
+{
+ auto* p_cur = (vtab_cursor*) p_vtc;
+ auto* vt = (log_vtab*) p_vtc->pVtab;
+ sqlite3_index_info::sqlite3_index_constraint* index = nullptr;
+
+ if (idxStr) {
+ auto desc_len = strlen(idxStr);
+ auto index_len = idxNum * sizeof(*index);
+ auto storage_len = desc_len + 128 + index_len;
+ auto* remaining_storage = const_cast<void*>(
+ static_cast<const void*>(idxStr + desc_len + 1));
+ auto* index_storage
+ = std::align(alignof(sqlite3_index_info::sqlite3_index_constraint),
+ index_len,
+ remaining_storage,
+ storage_len);
+ index = reinterpret_cast<sqlite3_index_info::sqlite3_index_constraint*>(
+ index_storage);
+ }
+
+#ifdef DEBUG_INDEXING
+ log_debug("vt_filter(%s, %d)", vt->vi->get_name().get(), idxNum);
+#endif
+ p_cur->log_cursor.lc_format_name.clear();
+ p_cur->log_cursor.lc_pattern_name.clear();
+ p_cur->log_cursor.lc_opid = nonstd::nullopt;
+ p_cur->log_cursor.lc_level_constraint = nonstd::nullopt;
+ p_cur->log_cursor.lc_log_path.clear();
+ p_cur->log_cursor.lc_indexed_columns.clear();
+ p_cur->log_cursor.lc_last_log_path_match = nullptr;
+ p_cur->log_cursor.lc_last_log_path_mismatch = nullptr;
+ p_cur->log_cursor.lc_unique_path.clear();
+ p_cur->log_cursor.lc_last_unique_path_match = nullptr;
+ p_cur->log_cursor.lc_last_unique_path_mismatch = nullptr;
+ p_cur->log_cursor.lc_curr_line = 0_vl;
+ p_cur->log_cursor.lc_end_line = vis_line_t(vt->lss->text_line_count());
+
+ nonstd::optional<time_range> log_time_range;
+ nonstd::optional<log_cursor::opid_hash> opid_val;
+ std::vector<log_cursor::string_constraint> log_path_constraints;
+ std::vector<log_cursor::string_constraint> log_unique_path_constraints;
+
+ for (int lpc = 0; lpc < idxNum; lpc++) {
+ auto col = index[lpc].iColumn;
+ auto op = index[lpc].op;
+ switch (col) {
+ case VT_COL_LINE_NUMBER: {
+ auto vl = vis_line_t(sqlite3_value_int64(argv[lpc]));
+
+ p_cur->log_cursor.update(
+ op, vl, log_cursor::constraint_t::unique);
+ break;
+ }
+ case VT_COL_LEVEL: {
+ if (sqlite3_value_type(argv[lpc]) != SQLITE3_TEXT) {
+ continue;
+ }
+
+ auto sf = from_sqlite<string_fragment>()(argc, argv, lpc);
+ auto level = string2level(sf.data(), sf.length());
+
+ p_cur->log_cursor.lc_level_constraint
+ = log_cursor::level_constraint{
+ op,
+ level,
+ };
+ break;
+ }
+
+ case VT_COL_LOG_TIME:
+ if (sqlite3_value_type(argv[lpc]) == SQLITE3_TEXT) {
+ const auto* datestr
+ = (const char*) sqlite3_value_text(argv[lpc]);
+ auto datelen = sqlite3_value_bytes(argv[lpc]);
+ date_time_scanner dts;
+ struct timeval tv;
+ struct exttm mytm;
+
+ const auto* date_end
+ = dts.scan(datestr, datelen, nullptr, &mytm, tv);
+ if (date_end != (datestr + datelen)) {
+ log_warning(
+ " log_time constraint is not a valid datetime, "
+ "index will not be applied: %s",
+ datestr);
+ } else {
+ switch (op) {
+ case SQLITE_INDEX_CONSTRAINT_EQ:
+ case SQLITE_INDEX_CONSTRAINT_IS:
+ if (!log_time_range) {
+ log_time_range = time_range{};
+ }
+ log_time_range->add(tv);
+ break;
+ case SQLITE_INDEX_CONSTRAINT_GT:
+ case SQLITE_INDEX_CONSTRAINT_GE:
+ if (!log_time_range) {
+ log_time_range = time_range{};
+ }
+ log_time_range->tr_begin = tv;
+ break;
+ case SQLITE_INDEX_CONSTRAINT_LT:
+ case SQLITE_INDEX_CONSTRAINT_LE:
+ if (!log_time_range) {
+ log_time_range = time_range{};
+ }
+ log_time_range->tr_end = tv;
+ break;
+ }
+ }
+ } else {
+ log_warning(
+ " log_time constraint is not text, index will not be "
+ "applied: value_type(%d)=%d",
+ lpc,
+ sqlite3_value_type(argv[lpc]));
+ }
+ break;
+ default: {
+ if (col > (VT_COL_MAX + vt->vi->vi_column_count - 1)) {
+ auto footer_column = static_cast<log_footer_columns>(
+ col - (VT_COL_MAX + vt->vi->vi_column_count - 1) - 1);
+
+ switch (footer_column) {
+ case log_footer_columns::time_msecs: {
+ auto msecs = sqlite3_value_int64(argv[lpc]);
+ struct timeval tv;
+
+ tv.tv_sec = msecs / 1000;
+ tv.tv_usec = (msecs - tv.tv_sec * 1000) * 1000;
+ switch (op) {
+ case SQLITE_INDEX_CONSTRAINT_EQ:
+ case SQLITE_INDEX_CONSTRAINT_IS:
+ if (!log_time_range) {
+ log_time_range = time_range{};
+ }
+ log_time_range->add(tv);
+ break;
+ case SQLITE_INDEX_CONSTRAINT_GT:
+ case SQLITE_INDEX_CONSTRAINT_GE:
+ if (!log_time_range) {
+ log_time_range = time_range{};
+ }
+ log_time_range->tr_begin = tv;
+ break;
+ case SQLITE_INDEX_CONSTRAINT_LT:
+ case SQLITE_INDEX_CONSTRAINT_LE:
+ if (!log_time_range) {
+ log_time_range = time_range{};
+ }
+ log_time_range->tr_end = tv;
+ break;
+ }
+ break;
+ }
+ case log_footer_columns::format: {
+ const auto* format_name_str
+ = (const char*) sqlite3_value_text(argv[lpc]);
+
+ if (format_name_str != nullptr) {
+ p_cur->log_cursor.lc_format_name
+ = intern_string::lookup(format_name_str);
+ }
+ break;
+ }
+ case log_footer_columns::format_regex: {
+ const auto* pattern_name_str
+ = (const char*) sqlite3_value_text(argv[lpc]);
+
+ if (pattern_name_str != nullptr) {
+ p_cur->log_cursor.lc_pattern_name
+ = intern_string::lookup(pattern_name_str);
+ }
+ break;
+ }
+ case log_footer_columns::opid: {
+ if (sqlite3_value_type(argv[lpc]) != SQLITE3_TEXT) {
+ continue;
+ }
+ auto opid = from_sqlite<string_fragment>()(
+ argc, argv, lpc);
+ if (!log_time_range) {
+ log_time_range = time_range{};
+ }
+ for (const auto& file_data : *vt->lss) {
+ if (file_data->get_file_ptr() == nullptr) {
+ continue;
+ }
+ safe::ReadAccess<logfile::safe_opid_map>
+ r_opid_map(
+ file_data->get_file_ptr()->get_opids());
+ const auto& iter = r_opid_map->find(opid);
+ if (iter == r_opid_map->end()) {
+ continue;
+ }
+ log_time_range->add(iter->second.otr_begin);
+ log_time_range->add(iter->second.otr_end);
+ }
+
+ opid_val = log_cursor::opid_hash{
+ static_cast<unsigned int>(
+ hash_str(opid.data(), opid.length()))};
+ break;
+ }
+ case log_footer_columns::path: {
+ if (sqlite3_value_type(argv[lpc]) != SQLITE3_TEXT) {
+ continue;
+ }
+
+ const auto filename
+ = from_sqlite<std::string>()(argc, argv, lpc);
+ const auto fn_constraint
+ = log_cursor::string_constraint{op, filename};
+ auto found = false;
+
+ if (!log_time_range) {
+ log_time_range = time_range{};
+ }
+ for (const auto& file_data : *vt->lss) {
+ auto* lf = file_data->get_file_ptr();
+ if (lf == nullptr) {
+ continue;
+ }
+ if (fn_constraint.matches(lf->get_filename())) {
+ found = true;
+ log_time_range->add(
+ lf->front().get_timeval());
+ log_time_range->add(
+ lf->back().get_timeval());
+ }
+ }
+ if (found) {
+ log_path_constraints.emplace_back(
+ fn_constraint);
+ }
+ break;
+ }
+ case log_footer_columns::unique_path: {
+ if (sqlite3_value_type(argv[lpc]) != SQLITE3_TEXT) {
+ continue;
+ }
+
+ const auto filename
+ = from_sqlite<std::string>()(argc, argv, lpc);
+ const auto fn_constraint
+ = log_cursor::string_constraint{op, filename};
+ auto found = false;
+
+ if (!log_time_range) {
+ log_time_range = time_range{};
+ }
+ for (const auto& file_data : *vt->lss) {
+ auto* lf = file_data->get_file_ptr();
+ if (lf == nullptr) {
+ continue;
+ }
+ if (fn_constraint.matches(
+ lf->get_unique_path()))
+ {
+ found = true;
+ log_time_range->add(
+ lf->front().get_timeval());
+ log_time_range->add(
+ lf->back().get_timeval());
+ }
+ }
+ if (found) {
+ log_unique_path_constraints.emplace_back(
+ fn_constraint);
+ }
+ break;
+ }
+ case log_footer_columns::text:
+ case log_footer_columns::body:
+ case log_footer_columns::raw_text:
+ case log_footer_columns::line_hash:
+ break;
+ }
+ } else {
+ const auto* value
+ = (const char*) sqlite3_value_text(argv[lpc]);
+
+ if (value != nullptr) {
+ auto value_len
+ = (size_t) sqlite3_value_bytes(argv[lpc]);
+
+#ifdef DEBUG_INDEXING
+ log_debug("adding index request for column %d = %s",
+ col,
+ value);
+#endif
+
+ p_cur->log_cursor.lc_indexed_columns.emplace_back(
+ col,
+ log_cursor::string_constraint{
+ op,
+ std::string{value, value_len},
+ });
+ }
+ }
+ break;
+ }
+ }
+ }
+
+ if (!p_cur->log_cursor.lc_indexed_columns.empty()) {
+ nonstd::optional<vis_line_t> max_indexed_line;
+
+ for (const auto& icol : p_cur->log_cursor.lc_indexed_columns) {
+ auto& coli = vt->vi->vi_column_indexes[icol.cc_column];
+
+ if (coli.ci_index_generation != vt->lss->lss_index_generation) {
+ coli.ci_value_to_lines.clear();
+ coli.ci_index_generation = vt->lss->lss_index_generation;
+ coli.ci_max_line = 0_vl;
+ }
+
+ if (!max_indexed_line) {
+ max_indexed_line = coli.ci_max_line;
+ } else if (coli.ci_max_line < max_indexed_line.value()) {
+ max_indexed_line = coli.ci_max_line;
+ }
+ }
+
+ for (const auto& icol : p_cur->log_cursor.lc_indexed_columns) {
+ auto& coli = vt->vi->vi_column_indexes[icol.cc_column];
+
+ auto iter
+ = coli.ci_value_to_lines.find(icol.cc_constraint.sc_value);
+ if (iter != coli.ci_value_to_lines.end()) {
+ for (auto vl : iter->second) {
+ if (vl >= max_indexed_line.value()) {
+ continue;
+ }
+
+ if (vl < p_cur->log_cursor.lc_curr_line) {
+ continue;
+ }
+
+#ifdef DEBUG_INDEXING
+ log_debug("adding indexed line %d", (int) vl);
+#endif
+ p_cur->log_cursor.lc_indexed_lines.push_back(vl);
+ }
+ }
+ }
+
+ if (max_indexed_line && max_indexed_line.value() > 0_vl) {
+ p_cur->log_cursor.lc_indexed_lines.push_back(
+ max_indexed_line.value());
+ }
+
+ std::sort(p_cur->log_cursor.lc_indexed_lines.begin(),
+ p_cur->log_cursor.lc_indexed_lines.end(),
+ std::greater<>());
+
+ if (max_indexed_line
+ && max_indexed_line.value() < vt->lss->text_line_count())
+ {
+ log_debug("max indexed out of sync, clearing other indexes");
+ p_cur->log_cursor.lc_level_constraint = nonstd::nullopt;
+ p_cur->log_cursor.lc_curr_line = 0_vl;
+ opid_val = nonstd::nullopt;
+ log_time_range = nonstd::nullopt;
+ p_cur->log_cursor.lc_indexed_lines.clear();
+ log_path_constraints.clear();
+ log_unique_path_constraints.clear();
+ }
+
+#ifdef DEBUG_INDEXING
+ log_debug("indexed lines:");
+ for (auto indline : p_cur->log_cursor.lc_indexed_lines) {
+ log_debug(" %d", (int) indline);
+ }
+#endif
+ }
+
+ if (!log_time_range) {
+ } else if (log_time_range->empty()) {
+ p_cur->log_cursor.lc_curr_line = p_cur->log_cursor.lc_end_line;
+ } else {
+ if (log_time_range->tr_begin) {
+ auto vl_opt
+ = vt->lss->row_for_time(log_time_range->tr_begin.value());
+ if (!vl_opt) {
+ p_cur->log_cursor.lc_curr_line = p_cur->log_cursor.lc_end_line;
+ } else {
+ p_cur->log_cursor.lc_curr_line = vl_opt.value();
+ }
+ }
+ if (log_time_range->tr_end) {
+ auto vl_max_opt
+ = vt->lss->row_for_time(log_time_range->tr_end.value());
+ if (vl_max_opt) {
+ p_cur->log_cursor.lc_end_line = vl_max_opt.value();
+ for (const auto& msg_info :
+ vt->lss->window_at(vl_max_opt.value(),
+ vis_line_t(vt->lss->text_line_count())))
+ {
+ if (log_time_range->tr_end.value()
+ < msg_info.get_logline().get_timeval())
+ {
+ break;
+ }
+ p_cur->log_cursor.lc_end_line
+ = msg_info.get_vis_line() + 1_vl;
+ }
+ }
+ }
+ }
+
+ p_cur->log_cursor.lc_opid = opid_val;
+ p_cur->log_cursor.lc_log_path = std::move(log_path_constraints);
+ p_cur->log_cursor.lc_unique_path = std::move(log_unique_path_constraints);
+
+ if (p_cur->log_cursor.lc_indexed_lines.empty()) {
+ p_cur->log_cursor.lc_indexed_lines.push_back(
+ p_cur->log_cursor.lc_curr_line);
+ }
+ vt->vi->filter(p_cur->log_cursor, *vt->lss);
+
+ auto rc = vt->base.pModule->xNext(p_vtc);
+
+#ifdef DEBUG_INDEXING
+ log_debug("vt_filter() -> cursor_range(%d:%d)",
+ (int) p_cur->log_cursor.lc_curr_line,
+ (int) p_cur->log_cursor.lc_end_line);
+#endif
+
+ return rc;
+}
+
+static int
+vt_best_index(sqlite3_vtab* tab, sqlite3_index_info* p_info)
+{
+ std::vector<sqlite3_index_info::sqlite3_index_constraint> indexes;
+ std::vector<std::string> index_desc;
+ int argvInUse = 0;
+ auto* vt = (log_vtab*) tab;
+
+ log_info("vt_best_index(%s, nConstraint=%d)",
+ vt->vi->get_name().get(),
+ p_info->nConstraint);
+ if (!vt->vi->vi_supports_indexes) {
+ return SQLITE_OK;
+ }
+ for (int lpc = 0; lpc < p_info->nConstraint; lpc++) {
+ const auto& constraint = p_info->aConstraint[lpc];
+ if (!constraint.usable || constraint.op == SQLITE_INDEX_CONSTRAINT_MATCH
+#ifdef SQLITE_INDEX_CONSTRAINT_OFFSET
+ || constraint.op == SQLITE_INDEX_CONSTRAINT_OFFSET
+ || constraint.op == SQLITE_INDEX_CONSTRAINT_LIMIT
+#endif
+ )
+ {
+ log_debug(" column %d: is not usable (usable=%d, op: %s)",
+ lpc,
+ constraint.usable,
+ sql_constraint_op_name(constraint.op));
+ continue;
+ }
+
+ auto col = constraint.iColumn;
+ auto op = constraint.op;
+ log_debug(" column %d: op: %s", col, sql_constraint_op_name(op));
+ switch (col) {
+ case VT_COL_LINE_NUMBER: {
+ argvInUse += 1;
+ indexes.push_back(constraint);
+ p_info->aConstraintUsage[lpc].argvIndex = argvInUse;
+ index_desc.emplace_back(fmt::format(
+ FMT_STRING("log_line {} ?"), sql_constraint_op_name(op)));
+ break;
+ }
+ case VT_COL_LOG_TIME: {
+ argvInUse += 1;
+ indexes.push_back(p_info->aConstraint[lpc]);
+ p_info->aConstraintUsage[lpc].argvIndex = argvInUse;
+ index_desc.emplace_back(fmt::format(
+ FMT_STRING("log_time {} ?"), sql_constraint_op_name(op)));
+ break;
+ }
+ case VT_COL_LEVEL: {
+ if (log_cursor::level_constraint::op_is_supported(op)) {
+ argvInUse += 1;
+ indexes.push_back(constraint);
+ p_info->aConstraintUsage[lpc].argvIndex = argvInUse;
+ index_desc.emplace_back(
+ fmt::format(FMT_STRING("log_level {} ?"),
+ sql_constraint_op_name(op)));
+ }
+ break;
+ }
+ default: {
+ if (col > (VT_COL_MAX + vt->vi->vi_column_count - 1)) {
+ auto footer_column = static_cast<log_footer_columns>(
+ col - (VT_COL_MAX + vt->vi->vi_column_count - 1) - 1);
+
+ switch (footer_column) {
+ case log_footer_columns::time_msecs: {
+ argvInUse += 1;
+ indexes.push_back(p_info->aConstraint[lpc]);
+ p_info->aConstraintUsage[lpc].argvIndex = argvInUse;
+ index_desc.emplace_back(
+ fmt::format(FMT_STRING("log_time_msecs {} ?"),
+ sql_constraint_op_name(op)));
+ break;
+ }
+ case log_footer_columns::format: {
+ if (op == SQLITE_INDEX_CONSTRAINT_EQ) {
+ argvInUse += 1;
+ indexes.push_back(constraint);
+ p_info->aConstraintUsage[lpc].argvIndex
+ = argvInUse;
+ index_desc.emplace_back("log_format = ?");
+ }
+ break;
+ }
+ case log_footer_columns::format_regex: {
+ if (op == SQLITE_INDEX_CONSTRAINT_EQ) {
+ argvInUse += 1;
+ indexes.push_back(constraint);
+ p_info->aConstraintUsage[lpc].argvIndex
+ = argvInUse;
+ index_desc.emplace_back("log_format_regex = ?");
+ }
+ break;
+ }
+ case log_footer_columns::opid: {
+ if (op == SQLITE_INDEX_CONSTRAINT_EQ) {
+ argvInUse += 1;
+ indexes.push_back(constraint);
+ p_info->aConstraintUsage[lpc].argvIndex
+ = argvInUse;
+ index_desc.emplace_back("log_opid = ?");
+ }
+ break;
+ }
+ case log_footer_columns::path: {
+ argvInUse += 1;
+ indexes.push_back(constraint);
+ p_info->aConstraintUsage[lpc].argvIndex = argvInUse;
+ index_desc.emplace_back(
+ fmt::format(FMT_STRING("log_path {} ?"),
+ sql_constraint_op_name(op)));
+ break;
+ }
+ case log_footer_columns::unique_path: {
+ argvInUse += 1;
+ indexes.push_back(constraint);
+ p_info->aConstraintUsage[lpc].argvIndex = argvInUse;
+ index_desc.emplace_back(
+ fmt::format(FMT_STRING("log_unique_path {} ?"),
+ sql_constraint_op_name(op)));
+ break;
+ }
+ case log_footer_columns::text:
+ case log_footer_columns::body:
+ case log_footer_columns::raw_text:
+ case log_footer_columns::line_hash:
+ break;
+ }
+ } else if (op == SQLITE_INDEX_CONSTRAINT_EQ) {
+ argvInUse += 1;
+ indexes.push_back(constraint);
+ p_info->aConstraintUsage[lpc].argvIndex = argvInUse;
+ index_desc.emplace_back(
+ fmt::format(FMT_STRING("col({}) {} ?"),
+ col,
+ sql_constraint_op_name(op)));
+ }
+ break;
+ }
+ }
+ }
+
+ if (argvInUse) {
+ auto full_desc = fmt::format(FMT_STRING("SEARCH {} USING {}"),
+ vt->vi->get_name().get(),
+ fmt::join(index_desc, " AND "));
+ log_info("found index: %s", full_desc.c_str());
+
+ sqlite3_index_info::sqlite3_index_constraint* index_copy;
+ auto index_len = indexes.size() * sizeof(*index_copy);
+ size_t len = full_desc.size() + 128 + index_len;
+ auto* storage = sqlite3_malloc(len);
+ if (!storage) {
+ return SQLITE_NOMEM;
+ }
+ auto* desc_storage = static_cast<char*>(storage);
+ memcpy(desc_storage, full_desc.c_str(), full_desc.size() + 1);
+ auto* remaining_storage
+ = static_cast<void*>(desc_storage + full_desc.size() + 1);
+ len -= full_desc.size() - 1;
+ auto* index_storage
+ = std::align(alignof(sqlite3_index_info::sqlite3_index_constraint),
+ index_len,
+ remaining_storage,
+ len);
+ index_copy
+ = reinterpret_cast<sqlite3_index_info::sqlite3_index_constraint*>(
+ index_storage);
+ memcpy(index_copy, &indexes[0], index_len);
+ p_info->idxNum = argvInUse;
+ p_info->idxStr = static_cast<char*>(storage);
+ p_info->needToFreeIdxStr = 1;
+ p_info->estimatedCost = 10.0;
+ } else {
+ static char fullscan_str[] = "fullscan";
+
+ p_info->idxStr = fullscan_str;
+ p_info->estimatedCost = 1000000000.0;
+ }
+
+ return SQLITE_OK;
+}
+
+static const struct json_path_container tags_handler = {
+ json_path_handler("#")
+ .with_synopsis("tag")
+ .with_description("A tag for the log line")
+ .with_pattern(R"(^#[^\s]+$)")
+ .for_field(&bookmark_metadata::bm_tags),
+};
+
+static int
+vt_update(sqlite3_vtab* tab,
+ int argc,
+ sqlite3_value** argv,
+ sqlite_int64* rowid_out)
+{
+ auto* vt = (log_vtab*) tab;
+ int retval = SQLITE_READONLY;
+
+ if (argc > 1 && sqlite3_value_type(argv[0]) != SQLITE_NULL
+ && sqlite3_value_int64(argv[0]) == sqlite3_value_int64(argv[1]))
+ {
+ int64_t rowid = sqlite3_value_int64(argv[0]) >> 8;
+ int val = sqlite3_value_int(argv[2 + VT_COL_MARK]);
+ vis_line_t vrowid(rowid);
+
+ const auto* part_name = sqlite3_value_text(argv[2 + VT_COL_PARTITION]);
+ const auto* log_comment
+ = sqlite3_value_text(argv[2 + VT_COL_LOG_COMMENT]);
+ const auto* log_tags = sqlite3_value_text(argv[2 + VT_COL_LOG_TAGS]);
+ bookmark_metadata tmp_bm;
+
+ if (log_tags) {
+ std::vector<lnav::console::user_message> errors;
+ yajlpp_parse_context ypc(vt->vi->get_tags_name(), &tags_handler);
+ auto_mem<yajl_handle_t> handle(yajl_free);
+
+ handle = yajl_alloc(&ypc.ypc_callbacks, nullptr, &ypc);
+ ypc.ypc_userdata = &errors;
+ ypc.ypc_line_number = log_vtab_data.lvd_location.sl_line_number;
+ ypc.with_handle(handle)
+ .with_error_reporter([](const yajlpp_parse_context& ypc,
+ auto msg) {
+ auto& errors = *((std::vector<lnav::console::user_message>*)
+ ypc.ypc_userdata);
+ errors.emplace_back(msg);
+ })
+ .with_obj(tmp_bm);
+ ypc.parse_doc(string_fragment{log_tags});
+ if (!errors.empty()) {
+ auto top_error = lnav::console::user_message::error(
+ attr_line_t("invalid value for ")
+ .append_quoted("log_tags"_symbol)
+ .append(" column of table ")
+ .append_quoted(lnav::roles::symbol(
+ vt->vi->get_name().to_string())))
+ .with_reason(errors[0].to_attr_line({}));
+ auto json_error = lnav::to_json(top_error);
+ tab->zErrMsg
+ = sqlite3_mprintf("lnav-error:%s", json_error.c_str());
+ return SQLITE_ERROR;
+ }
+ }
+
+ auto& bv = vt->tc->get_bookmarks()[&textview_curses::BM_META];
+ bool has_meta = part_name != nullptr || log_comment != nullptr
+ || log_tags != nullptr;
+
+ if (binary_search(bv.begin(), bv.end(), vrowid) && !has_meta) {
+ vt->tc->set_user_mark(&textview_curses::BM_META, vrowid, false);
+ vt->lss->erase_bookmark_metadata(vrowid);
+ vt->lss->set_line_meta_changed();
+ }
+
+ if (has_meta) {
+ auto& line_meta = vt->lss->get_bookmark_metadata(vrowid);
+
+ vt->tc->set_user_mark(&textview_curses::BM_META, vrowid, true);
+ if (part_name) {
+ line_meta.bm_name = std::string((const char*) part_name);
+ } else {
+ line_meta.bm_name.clear();
+ }
+ if (log_comment) {
+ line_meta.bm_comment = std::string((const char*) log_comment);
+ } else {
+ line_meta.bm_comment.clear();
+ }
+ if (log_tags) {
+ line_meta.bm_tags.clear();
+ for (const auto& tag : tmp_bm.bm_tags) {
+ line_meta.add_tag(tag);
+ }
+
+ for (const auto& tag : line_meta.bm_tags) {
+ bookmark_metadata::KNOWN_TAGS.insert(tag);
+ }
+ } else {
+ line_meta.bm_tags.clear();
+ }
+
+ vt->lss->set_line_meta_changed();
+ }
+
+ vt->tc->set_user_mark(&textview_curses::BM_USER, vrowid, val);
+ rowid += 1;
+ while ((size_t) rowid < vt->lss->text_line_count()) {
+ vis_line_t vl(rowid);
+ content_line_t cl = vt->lss->at(vl);
+ logline* ll = vt->lss->find_line(cl);
+ if (ll->is_message()) {
+ break;
+ }
+ vt->tc->set_user_mark(&textview_curses::BM_USER, vl, val);
+ rowid += 1;
+ }
+
+ if (retval != SQLITE_ERROR) {
+ retval = SQLITE_OK;
+ }
+ }
+
+ return retval;
+}
+
+static sqlite3_module generic_vtab_module = {
+ 0, /* iVersion */
+ vt_create, /* xCreate - create a vtable */
+ vt_connect, /* xConnect - associate a vtable with a connection */
+ vt_best_index, /* xBestIndex - best index */
+ vt_disconnect, /* xDisconnect - disassociate a vtable with a connection */
+ vt_destroy, /* xDestroy - destroy a vtable */
+ vt_open, /* xOpen - open a cursor */
+ vt_close, /* xClose - close a cursor */
+ vt_filter, /* xFilter - configure scan constraints */
+ vt_next, /* xNext - advance a cursor */
+ vt_eof, /* xEof - inidicate end of result set*/
+ vt_column, /* xColumn - read data */
+ vt_rowid, /* xRowid - read data */
+ vt_update, /* xUpdate - write data */
+ nullptr, /* xBegin - begin transaction */
+ nullptr, /* xSync - sync transaction */
+ nullptr, /* xCommit - commit transaction */
+ nullptr, /* xRollback - rollback transaction */
+ nullptr, /* xFindFunction - function overloading */
+};
+
+static sqlite3_module no_rowid_vtab_module = {
+ 0, /* iVersion */
+ vt_create, /* xCreate - create a vtable */
+ vt_connect, /* xConnect - associate a vtable with a connection */
+ vt_best_index, /* xBestIndex - best index */
+ vt_disconnect, /* xDisconnect - disassociate a vtable with a connection */
+ vt_destroy, /* xDestroy - destroy a vtable */
+ vt_open, /* xOpen - open a cursor */
+ vt_close, /* xClose - close a cursor */
+ vt_filter, /* xFilter - configure scan constraints */
+ vt_next_no_rowid, /* xNext - advance a cursor */
+ vt_eof, /* xEof - inidicate end of result set*/
+ vt_column, /* xColumn - read data */
+ nullptr, /* xRowid - read data */
+ nullptr, /* xUpdate - write data */
+ nullptr, /* xBegin - begin transaction */
+ nullptr, /* xSync - sync transaction */
+ nullptr, /* xCommit - commit transaction */
+ nullptr, /* xRollback - rollback transaction */
+ nullptr, /* xFindFunction - function overloading */
+};
+
+static int
+progress_callback(void* ptr)
+{
+ int retval = 0;
+
+ if (log_vtab_data.lvd_progress != nullptr) {
+ retval = log_vtab_data.lvd_progress(log_cursor_latest);
+ }
+ if (!log_vtab_data.lvd_looping) {
+ retval = 1;
+ }
+
+ return retval;
+}
+
+log_vtab_manager::log_vtab_manager(sqlite3* memdb,
+ textview_curses& tc,
+ logfile_sub_source& lss)
+ : vm_db(memdb), vm_textview(tc), vm_source(lss)
+{
+ sqlite3_create_module(
+ this->vm_db, "log_vtab_impl", &generic_vtab_module, this);
+ sqlite3_create_module(
+ this->vm_db, "log_vtab_no_rowid_impl", &no_rowid_vtab_module, this);
+ sqlite3_progress_handler(memdb, 32, progress_callback, nullptr);
+}
+
+log_vtab_manager::~log_vtab_manager()
+{
+ while (!this->vm_impls.empty()) {
+ auto first_name = this->vm_impls.begin()->first;
+
+ this->unregister_vtab(first_name);
+ }
+}
+
+std::string
+log_vtab_manager::register_vtab(std::shared_ptr<log_vtab_impl> vi)
+{
+ std::string retval;
+
+ if (this->vm_impls.find(vi->get_name()) == this->vm_impls.end()) {
+ std::vector<std::string> primary_keys;
+ auto_mem<char, sqlite3_free> errmsg;
+ auto_mem<char, sqlite3_free> sql;
+ int rc;
+
+ this->vm_impls[vi->get_name()] = vi;
+
+ vi->get_primary_keys(primary_keys);
+
+ sql = sqlite3_mprintf(
+ "CREATE VIRTUAL TABLE %s "
+ "USING %s(%s)",
+ vi->get_name().get(),
+ primary_keys.empty() ? "log_vtab_impl" : "log_vtab_no_rowid_impl",
+ vi->get_name().get());
+ rc = sqlite3_exec(this->vm_db, sql, nullptr, nullptr, errmsg.out());
+ if (rc != SQLITE_OK) {
+ retval = errmsg;
+ }
+ } else {
+ retval = "a table with the given name already exists";
+ }
+
+ return retval;
+}
+
+std::string
+log_vtab_manager::unregister_vtab(intern_string_t name)
+{
+ std::string retval;
+
+ if (this->vm_impls.find(name) == this->vm_impls.end()) {
+ retval = fmt::format(FMT_STRING("unknown table -- {}"), name);
+ } else {
+ auto_mem<char, sqlite3_free> sql;
+ __attribute((unused)) int rc;
+
+ sql = sqlite3_mprintf("DROP TABLE %s ", name.get());
+ rc = sqlite3_exec(this->vm_db, sql, NULL, NULL, NULL);
+
+ this->vm_impls.erase(name);
+ }
+
+ return retval;
+}
+
+bool
+log_format_vtab_impl::next(log_cursor& lc, logfile_sub_source& lss)
+{
+ if (lc.is_eof()) {
+ return true;
+ }
+
+ auto cl = content_line_t(lss.at(lc.lc_curr_line));
+ auto* lf = lss.find_file_ptr(cl);
+ auto lf_iter = lf->begin() + cl;
+ uint8_t mod_id = lf_iter->get_module_id();
+
+ if (!lf_iter->is_message()) {
+ return false;
+ }
+
+ auto format = lf->get_format();
+ if (format->get_name() == this->lfvi_format.get_name()) {
+ return true;
+ }
+ if (mod_id && mod_id == this->lfvi_format.lf_mod_index) {
+ // XXX
+ return true;
+ }
+
+ return false;
+}
diff --git a/src/log_vtab_impl.hh b/src/log_vtab_impl.hh
new file mode 100644
index 0000000..8ac03eb
--- /dev/null
+++ b/src/log_vtab_impl.hh
@@ -0,0 +1,344 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef vtab_impl_hh
+#define vtab_impl_hh
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <sqlite3.h>
+
+#include "logfile_sub_source.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "robin_hood/robin_hood.h"
+
+class textview_curses;
+
+enum {
+ VT_COL_LINE_NUMBER,
+ VT_COL_PARTITION,
+ VT_COL_LOG_TIME,
+ VT_COL_LOG_ACTUAL_TIME,
+ VT_COL_IDLE_MSECS,
+ VT_COL_LEVEL,
+ VT_COL_MARK,
+ VT_COL_LOG_COMMENT,
+ VT_COL_LOG_TAGS,
+ VT_COL_FILTERS,
+ VT_COL_MAX
+};
+
+class logfile_sub_source;
+
+struct log_cursor {
+ struct opid_hash {
+ unsigned int value : 6;
+ };
+
+ struct string_constraint {
+ unsigned char sc_op;
+ std::string sc_value;
+ std::shared_ptr<lnav::pcre2pp::code> sc_pattern;
+
+ string_constraint(unsigned char op, std::string value);
+
+ bool matches(const std::string& sf) const;
+ };
+
+ template<typename T>
+ struct integral_constraint {
+ unsigned char ic_op;
+ T ic_value;
+
+ static bool op_is_supported(unsigned char op)
+ {
+ switch (op) {
+ case SQLITE_INDEX_CONSTRAINT_EQ:
+ case SQLITE_INDEX_CONSTRAINT_IS:
+ case SQLITE_INDEX_CONSTRAINT_NE:
+ case SQLITE_INDEX_CONSTRAINT_ISNOT:
+ case SQLITE_INDEX_CONSTRAINT_GT:
+ case SQLITE_INDEX_CONSTRAINT_LE:
+ case SQLITE_INDEX_CONSTRAINT_LT:
+ case SQLITE_INDEX_CONSTRAINT_GE:
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ integral_constraint(unsigned char op, T value)
+ : ic_op(op), ic_value(value)
+ {
+ }
+
+ bool matches(const T& value) const
+ {
+ switch (this->ic_op) {
+ case SQLITE_INDEX_CONSTRAINT_EQ:
+ case SQLITE_INDEX_CONSTRAINT_IS:
+ return value == this->ic_value;
+ case SQLITE_INDEX_CONSTRAINT_NE:
+ case SQLITE_INDEX_CONSTRAINT_ISNOT:
+ return value != this->ic_value;
+ case SQLITE_INDEX_CONSTRAINT_GT:
+ return value > this->ic_value;
+ case SQLITE_INDEX_CONSTRAINT_LE:
+ return value <= this->ic_value;
+ case SQLITE_INDEX_CONSTRAINT_LT:
+ return value < this->ic_value;
+ case SQLITE_INDEX_CONSTRAINT_GE:
+ return value >= this->ic_value;
+ default:
+ return false;
+ }
+ }
+ };
+
+ struct column_constraint {
+ column_constraint(int32_t col, string_constraint cons)
+ : cc_column(col), cc_constraint(std::move(cons))
+ {
+ }
+
+ int32_t cc_column;
+ string_constraint cc_constraint;
+ };
+
+ vis_line_t lc_curr_line;
+ int lc_sub_index;
+ vis_line_t lc_end_line;
+
+ using level_constraint = integral_constraint<log_level_t>;
+
+ nonstd::optional<level_constraint> lc_level_constraint;
+ intern_string_t lc_format_name;
+ intern_string_t lc_pattern_name;
+ nonstd::optional<opid_hash> lc_opid;
+ std::vector<string_constraint> lc_log_path;
+ logfile* lc_last_log_path_match{nullptr};
+ logfile* lc_last_log_path_mismatch{nullptr};
+ std::vector<string_constraint> lc_unique_path;
+ logfile* lc_last_unique_path_match{nullptr};
+ logfile* lc_last_unique_path_mismatch{nullptr};
+
+ std::vector<column_constraint> lc_indexed_columns;
+ std::vector<vis_line_t> lc_indexed_lines;
+
+ enum class constraint_t {
+ none,
+ unique,
+ };
+
+ void update(unsigned char op, vis_line_t vl, constraint_t cons);
+
+ void set_eof() { this->lc_curr_line = this->lc_end_line = 0_vl; }
+
+ bool is_eof() const
+ {
+ return this->lc_indexed_lines.empty()
+ && this->lc_curr_line >= this->lc_end_line;
+ }
+};
+
+const std::string LOG_BODY = "log_body";
+const std::string LOG_TIME = "log_time";
+
+class log_vtab_impl {
+public:
+ struct vtab_column {
+ vtab_column(const std::string name = "",
+ int type = SQLITE3_TEXT,
+ const std::string collator = "",
+ bool hidden = false,
+ const std::string comment = "",
+ unsigned int subtype = 0)
+ : vc_name(name), vc_type(type), vc_collator(collator),
+ vc_hidden(hidden), vc_comment(comment), vc_subtype(subtype)
+ {
+ }
+
+ vtab_column& with_comment(const std::string comment)
+ {
+ this->vc_comment = comment;
+ return *this;
+ }
+
+ std::string vc_name;
+ int vc_type;
+ std::string vc_collator;
+ bool vc_hidden;
+ std::string vc_comment;
+ int vc_subtype;
+ };
+
+ static std::pair<int, unsigned int> logline_value_to_sqlite_type(
+ value_kind_t kind);
+
+ log_vtab_impl(const intern_string_t name)
+ : vi_name(name), vi_tags_name(intern_string::lookup(
+ fmt::format(FMT_STRING("{}.log_tags"), name)))
+ {
+ this->vi_attrs.resize(128);
+ }
+
+ virtual ~log_vtab_impl() = default;
+
+ intern_string_t get_name() const { return this->vi_name; }
+
+ intern_string_t get_tags_name() const { return this->vi_tags_name; }
+
+ std::string get_table_statement();
+
+ virtual bool is_valid(log_cursor& lc, logfile_sub_source& lss);
+
+ virtual void filter(log_cursor& lc, logfile_sub_source& lss) {}
+
+ virtual bool next(log_cursor& lc, logfile_sub_source& lss) = 0;
+
+ virtual void get_columns(std::vector<vtab_column>& cols) const {}
+
+ virtual void get_foreign_keys(std::vector<std::string>& keys_inout) const;
+
+ virtual void get_primary_keys(std::vector<std::string>& keys_out) const {}
+
+ virtual void extract(logfile* lf,
+ uint64_t line_number,
+ logline_value_vector& values);
+
+ struct column_index {
+ robin_hood::unordered_map<std::string, std::vector<vis_line_t>>
+ ci_value_to_lines;
+ uint32_t ci_index_generation{0};
+ vis_line_t ci_max_line{0};
+ };
+
+ std::map<int32_t, column_index> vi_column_indexes;
+
+ bool vi_supports_indexes{true};
+ int vi_column_count{0};
+ string_attrs_t vi_attrs;
+
+protected:
+ const intern_string_t vi_name;
+ const intern_string_t vi_tags_name;
+};
+
+class log_format_vtab_impl : public log_vtab_impl {
+public:
+ log_format_vtab_impl(const log_format& format)
+ : log_vtab_impl(format.get_name()), lfvi_format(format)
+ {
+ }
+
+ virtual bool next(log_cursor& lc, logfile_sub_source& lss);
+
+protected:
+ const log_format& lfvi_format;
+};
+
+using sql_progress_callback_t = int (*)(const log_cursor&);
+using sql_progress_finished_callback_t = void (*)();
+
+struct _log_vtab_data {
+ bool lvd_looping{true};
+ sql_progress_callback_t lvd_progress;
+ sql_progress_finished_callback_t lvd_finished;
+ source_location lvd_location;
+ attr_line_t lvd_content;
+};
+
+extern thread_local _log_vtab_data log_vtab_data;
+
+class sql_progress_guard {
+public:
+ sql_progress_guard(sql_progress_callback_t cb,
+ sql_progress_finished_callback_t fcb,
+ source_location loc,
+ const attr_line_t& content)
+ {
+ log_vtab_data.lvd_looping = true;
+ log_vtab_data.lvd_progress = cb;
+ log_vtab_data.lvd_finished = fcb;
+ log_vtab_data.lvd_location = loc;
+ log_vtab_data.lvd_content = content;
+ }
+
+ ~sql_progress_guard()
+ {
+ if (log_vtab_data.lvd_finished) {
+ log_vtab_data.lvd_finished();
+ }
+ log_vtab_data.lvd_looping = true;
+ log_vtab_data.lvd_progress = nullptr;
+ log_vtab_data.lvd_finished = nullptr;
+ log_vtab_data.lvd_location = source_location{};
+ log_vtab_data.lvd_content.clear();
+ }
+};
+
+class log_vtab_manager {
+public:
+ using iterator = std::map<intern_string_t,
+ std::shared_ptr<log_vtab_impl>>::const_iterator;
+
+ log_vtab_manager(sqlite3* db, textview_curses& tc, logfile_sub_source& lss);
+ ~log_vtab_manager();
+
+ textview_curses* get_view() const { return &this->vm_textview; }
+
+ logfile_sub_source* get_source() { return &this->vm_source; }
+
+ std::string register_vtab(std::shared_ptr<log_vtab_impl> vi);
+ std::string unregister_vtab(intern_string_t name);
+
+ std::shared_ptr<log_vtab_impl> lookup_impl(intern_string_t name) const
+ {
+ auto iter = this->vm_impls.find(name);
+
+ if (iter != this->vm_impls.end()) {
+ return iter->second;
+ }
+ return nullptr;
+ }
+
+ iterator begin() const { return this->vm_impls.begin(); }
+
+ iterator end() const { return this->vm_impls.end(); }
+
+private:
+ sqlite3* vm_db;
+ textview_curses& vm_textview;
+ logfile_sub_source& vm_source;
+ std::map<intern_string_t, std::shared_ptr<log_vtab_impl>> vm_impls;
+};
+
+#endif
diff --git a/src/logfile.cc b/src/logfile.cc
new file mode 100644
index 0000000..32aa196
--- /dev/null
+++ b/src/logfile.cc
@@ -0,0 +1,1189 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file logfile.cc
+ */
+
+#include <utility>
+
+#include "logfile.hh"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/param.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <time.h>
+
+#include "base/ansi_scrubber.hh"
+#include "base/fs_util.hh"
+#include "base/injector.hh"
+#include "base/string_util.hh"
+#include "config.h"
+#include "lnav_util.hh"
+#include "log.watch.hh"
+#include "log_format.hh"
+#include "logfile.cfg.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+static auto intern_lifetime = intern_string::get_table_lifetime();
+
+static const size_t INDEX_RESERVE_INCREMENT = 1024;
+
+static const typed_json_path_container<line_buffer::header_data>
+ file_header_handlers = {
+ yajlpp::property_handler("name").for_field(
+ &line_buffer::header_data::hd_name),
+ yajlpp::property_handler("mtime").for_field(
+ &line_buffer::header_data::hd_mtime),
+ yajlpp::property_handler("comment").for_field(
+ &line_buffer::header_data::hd_comment),
+};
+
+Result<std::shared_ptr<logfile>, std::string>
+logfile::open(std::string filename, logfile_open_options& loo)
+{
+ require(!filename.empty());
+
+ auto lf = std::shared_ptr<logfile>(new logfile(std::move(filename), loo));
+
+ memset(&lf->lf_stat, 0, sizeof(lf->lf_stat));
+ if (lf->lf_options.loo_fd == -1) {
+ char resolved_path[PATH_MAX];
+
+ errno = 0;
+ if (realpath(lf->lf_filename.c_str(), resolved_path) == nullptr) {
+ return Err(fmt::format(FMT_STRING("realpath({}) failed with: {}"),
+ lf->lf_filename,
+ strerror(errno)));
+ }
+
+ if (stat(resolved_path, &lf->lf_stat) == -1) {
+ return Err(fmt::format(FMT_STRING("stat({}) failed with: {}"),
+ lf->lf_filename,
+ strerror(errno)));
+ }
+
+ if (!S_ISREG(lf->lf_stat.st_mode)) {
+ return Err(fmt::format(FMT_STRING("{} is not a regular file"),
+ lf->lf_filename,
+ strerror(errno)));
+ }
+
+ if ((lf->lf_options.loo_fd = ::open(resolved_path, O_RDONLY)) == -1) {
+ return Err(fmt::format(FMT_STRING("open({}) failed with: {}"),
+ lf->lf_filename,
+ strerror(errno)));
+ }
+
+ lf->lf_options.loo_fd.close_on_exec();
+
+ log_info("Creating logfile: fd=%d; size=%" PRId64 "; mtime=%" PRId64
+ "; filename=%s",
+ (int) lf->lf_options.loo_fd,
+ (long long) lf->lf_stat.st_size,
+ (long long) lf->lf_stat.st_mtime,
+ lf->lf_filename.c_str());
+
+ lf->lf_actual_path = lf->lf_filename;
+ lf->lf_valid_filename = true;
+ } else {
+ log_perror(fstat(lf->lf_options.loo_fd, &lf->lf_stat));
+ lf->lf_named_file = false;
+ lf->lf_valid_filename = false;
+ }
+
+ if (!lf->lf_options.loo_filename.empty()) {
+ lf->set_filename(lf->lf_options.loo_filename);
+ lf->lf_valid_filename = false;
+ }
+
+ lf->lf_content_id = hasher().update(lf->lf_filename).to_string();
+ lf->lf_line_buffer.set_fd(lf->lf_options.loo_fd);
+ lf->lf_index.reserve(INDEX_RESERVE_INCREMENT);
+
+ lf->lf_indexing = lf->lf_options.loo_is_visible;
+
+ const auto& hdr = lf->lf_line_buffer.get_header_data();
+ if (!hdr.empty()) {
+ lf->lf_embedded_metadata["net.zlib.gzip.header"]
+ = {text_format_t::TF_JSON, file_header_handlers.to_string(hdr)};
+ }
+
+ ensure(lf->invariant());
+
+ return Ok(lf);
+}
+
+logfile::logfile(std::string filename, logfile_open_options& loo)
+ : lf_filename(std::move(filename)), lf_options(std::move(loo))
+{
+ this->lf_opids.writeAccess()->reserve(64);
+}
+
+logfile::~logfile() {}
+
+bool
+logfile::exists() const
+{
+ if (!this->lf_actual_path) {
+ return true;
+ }
+
+ if (this->lf_options.loo_source == logfile_name_source::ARCHIVE) {
+ return true;
+ }
+
+ auto stat_res = lnav::filesystem::stat_file(this->lf_actual_path.value());
+ if (stat_res.isErr()) {
+ log_error("%s: stat failed -- %s",
+ this->lf_actual_path.value().c_str(),
+ stat_res.unwrapErr().c_str());
+ return false;
+ }
+
+ auto st = stat_res.unwrap();
+ return this->lf_stat.st_dev == st.st_dev
+ && this->lf_stat.st_ino == st.st_ino
+ && this->lf_stat.st_size <= st.st_size;
+}
+
+void
+logfile::reset_state()
+{
+ this->clear_time_offset();
+ this->lf_indexing = this->lf_options.loo_is_visible;
+}
+
+void
+logfile::set_format_base_time(log_format* lf)
+{
+ time_t file_time = this->lf_line_buffer.get_file_time();
+
+ if (file_time == 0) {
+ file_time = this->lf_stat.st_mtime;
+ }
+
+ if (!this->lf_cached_base_time
+ || this->lf_cached_base_time.value() != file_time)
+ {
+ struct tm new_base_tm;
+ this->lf_cached_base_time = file_time;
+ localtime_r(&file_time, &new_base_tm);
+ this->lf_cached_base_tm = new_base_tm;
+ }
+ lf->lf_date_time.set_base_time(this->lf_cached_base_time.value(),
+ this->lf_cached_base_tm.value());
+}
+
+bool
+logfile::process_prefix(shared_buffer_ref& sbr,
+ const line_info& li,
+ scan_batch_context& sbc)
+{
+ static auto max_unrecognized_lines
+ = injector::get<const lnav::logfile::config&>()
+ .lc_max_unrecognized_lines;
+
+ log_format::scan_result_t found = log_format::scan_no_match{};
+ size_t prescan_size = this->lf_index.size();
+ time_t prescan_time = 0;
+ bool retval = false;
+
+ if (this->lf_format.get() != nullptr) {
+ if (!this->lf_index.empty()) {
+ prescan_time = this->lf_index[prescan_size - 1].get_time();
+ }
+ /* We've locked onto a format, just use that scanner. */
+ found = this->lf_format->scan(*this, this->lf_index, li, sbr, sbc);
+ } else if (this->lf_options.loo_detect_format) {
+ const auto& root_formats = log_format::get_root_formats();
+ nonstd::optional<std::pair<log_format*, log_format::scan_match>>
+ best_match;
+ size_t scan_count = 0;
+
+ /*
+ * Try each scanner until we get a match. Fortunately, the formats
+ * tend to be sufficiently different that there are few ambiguities...
+ */
+ log_trace("logfile[%s]: scanning line %d (offset: %d; size: %d)",
+ this->lf_filename.c_str(),
+ this->lf_index.size(),
+ li.li_file_range.fr_offset,
+ li.li_file_range.fr_size);
+ size_t prev_index_size = this->lf_index.size();
+ for (const auto& curr : root_formats) {
+ if (this->lf_index.size()
+ >= curr->lf_max_unrecognized_lines.value_or(
+ max_unrecognized_lines))
+ {
+ continue;
+ }
+
+ if (this->lf_mismatched_formats.count(curr->get_name()) > 0) {
+ continue;
+ }
+
+ if (!curr->match_name(this->lf_filename)) {
+ if (li.li_file_range.fr_offset == 0) {
+ log_debug("(%s) does not match file name: %s",
+ curr->get_name().get(),
+ this->lf_filename.c_str());
+ }
+ this->lf_mismatched_formats.insert(curr->get_name());
+ continue;
+ }
+ if (!curr->match_mime_type(this->lf_options.loo_file_format)) {
+ if (li.li_file_range.fr_offset == 0) {
+ log_debug("(%s) does not match file format: %s",
+ curr->get_name().get(),
+ fmt::to_string(this->lf_options.loo_file_format)
+ .c_str());
+ }
+ continue;
+ }
+
+ scan_count += 1;
+ curr->clear();
+ this->set_format_base_time(curr.get());
+ auto scan_res = curr->scan(*this, this->lf_index, li, sbr, sbc);
+
+ scan_res.match(
+ [this, &curr, &best_match, &prev_index_size](
+ const log_format::scan_match& sm) {
+ if (!best_match
+ || sm.sm_quality > best_match->second.sm_quality)
+ {
+ log_info(
+ " scan with format (%s) matched with quality (%d)",
+ curr->get_name().c_str(),
+ sm.sm_quality);
+ if (best_match) {
+ auto last = this->lf_index.begin();
+ std::advance(last, prev_index_size);
+ this->lf_index.erase(this->lf_index.begin(), last);
+ }
+ best_match = std::make_pair(curr.get(), sm);
+ prev_index_size = this->lf_index.size();
+ } else {
+ log_info(
+ " scan with format (%s) matched, but "
+ "is low quality (%d)",
+ curr->get_name().c_str(),
+ sm.sm_quality);
+ while (this->lf_index.size() > prev_index_size) {
+ this->lf_index.pop_back();
+ }
+ }
+ },
+ [curr](const log_format::scan_incomplete& si) {
+ log_trace(
+ " scan with format (%s) is incomplete, "
+ "more data required",
+ curr->get_name().c_str());
+ },
+ [curr](const log_format::scan_no_match& snm) {
+ log_trace(" scan with format (%s) does not match -- %s",
+ curr->get_name().c_str(),
+ snm.snm_reason);
+ });
+ }
+
+ if (!scan_count) {
+ log_info("%s: no formats available to scan, no longer detecting",
+ this->lf_filename.c_str());
+ this->lf_options.loo_detect_format = false;
+ }
+
+ if (best_match) {
+ auto winner = best_match.value();
+ auto* curr = winner.first;
+ log_info("%s:%d:log format found -- %s",
+ this->lf_filename.c_str(),
+ this->lf_index.size(),
+ curr->get_name().get());
+
+ this->lf_text_format = text_format_t::TF_LOG;
+ this->lf_format = curr->specialized();
+ this->set_format_base_time(this->lf_format.get());
+ this->lf_content_id
+ = hasher().update(sbr.get_data(), sbr.length()).to_string();
+
+ for (auto& td_pair : this->lf_format->lf_tag_defs) {
+ bool matches = td_pair.second->ftd_paths.empty();
+ for (const auto& pr : td_pair.second->ftd_paths) {
+ if (pr.matches(this->lf_filename.c_str())) {
+ matches = true;
+ break;
+ }
+ }
+ if (!matches) {
+ continue;
+ }
+
+ log_info("%s: found applicable tag definition /%s/tags/%s",
+ this->lf_filename.c_str(),
+ this->lf_format->get_name().get(),
+ td_pair.second->ftd_name.c_str());
+ this->lf_applicable_taggers.emplace_back(td_pair.second);
+ }
+
+ /*
+ * We'll go ahead and assume that any previous lines were
+ * written out at the same time as the last one, so we need to
+ * go back and update everything.
+ */
+ auto& last_line = this->lf_index[this->lf_index.size() - 1];
+
+ for (size_t lpc = 0; lpc < this->lf_index.size() - 1; lpc++) {
+ if (this->lf_format->lf_multiline) {
+ this->lf_index[lpc].set_time(last_line.get_time());
+ this->lf_index[lpc].set_millis(last_line.get_millis());
+ } else {
+ this->lf_index[lpc].set_ignore(true);
+ }
+ }
+
+ found = best_match->second;
+ }
+ }
+
+ if (found.is<log_format::scan_match>()) {
+ if (!this->lf_index.empty()) {
+ auto& last_line = this->lf_index.back();
+
+ last_line.set_valid_utf(last_line.is_valid_utf()
+ && li.li_utf8_scan_result.is_valid());
+ last_line.set_has_ansi(last_line.has_ansi()
+ || li.li_utf8_scan_result.usr_has_ansi);
+ }
+ if (prescan_size > 0 && this->lf_index.size() >= prescan_size
+ && prescan_time != this->lf_index[prescan_size - 1].get_time())
+ {
+ retval = true;
+ }
+ if (prescan_size > 0 && prescan_size < this->lf_index.size()) {
+ auto& second_to_last = this->lf_index[prescan_size - 1];
+ auto& latest = this->lf_index[prescan_size];
+
+ if (!second_to_last.is_ignored() && latest < second_to_last) {
+ if (this->lf_format->lf_time_ordered) {
+ this->lf_out_of_time_order_count += 1;
+ for (size_t lpc = prescan_size; lpc < this->lf_index.size();
+ lpc++)
+ {
+ auto& line_to_update = this->lf_index[lpc];
+
+ line_to_update.set_time_skew(true);
+ line_to_update.set_time(second_to_last.get_time());
+ line_to_update.set_millis(second_to_last.get_millis());
+ }
+ } else {
+ retval = true;
+ }
+ }
+ }
+ } else if (found.is<log_format::scan_no_match>()) {
+ log_level_t last_level = LEVEL_UNKNOWN;
+ time_t last_time = this->lf_index_time;
+ short last_millis = 0;
+ uint8_t last_mod = 0, last_opid = 0;
+
+ if (!this->lf_index.empty()) {
+ logline& ll = this->lf_index.back();
+
+ /*
+ * Assume this line is part of the previous one(s) and copy the
+ * metadata over.
+ */
+ last_time = ll.get_time();
+ last_millis = ll.get_millis();
+ if (this->lf_format.get() != nullptr) {
+ last_level = (log_level_t) (ll.get_level_and_flags()
+ | LEVEL_CONTINUED);
+ }
+ last_mod = ll.get_module_id();
+ last_opid = ll.get_opid();
+ }
+ this->lf_index.emplace_back(li.li_file_range.fr_offset,
+ last_time,
+ last_millis,
+ last_level,
+ last_mod,
+ last_opid);
+ this->lf_index.back().set_valid_utf(li.li_utf8_scan_result.is_valid());
+ this->lf_index.back().set_has_ansi(li.li_utf8_scan_result.usr_has_ansi);
+ }
+
+ return retval;
+}
+
+logfile::rebuild_result_t
+logfile::rebuild_index(nonstd::optional<ui_clock::time_point> deadline)
+{
+ if (!this->lf_indexing) {
+ if (this->lf_sort_needed) {
+ this->lf_sort_needed = false;
+ return rebuild_result_t::NEW_ORDER;
+ }
+ return rebuild_result_t::NO_NEW_LINES;
+ }
+
+ if (this->lf_format != nullptr && this->lf_format->format_changed()) {
+ log_info("%s: format has changed, rebuilding",
+ this->lf_filename.c_str());
+ this->lf_index.clear();
+ this->lf_index_size = 0;
+ this->lf_partial_line = false;
+ this->lf_longest_line = 0;
+ this->lf_sort_needed = true;
+ }
+
+ auto retval = rebuild_result_t::NO_NEW_LINES;
+ struct stat st;
+
+ this->lf_activity.la_polls += 1;
+
+ if (fstat(this->lf_line_buffer.get_fd(), &st) == -1) {
+ if (errno == EINTR) {
+ return rebuild_result_t::NO_NEW_LINES;
+ }
+ return rebuild_result_t::INVALID;
+ }
+
+ const auto is_truncated = st.st_size < this->lf_stat.st_size;
+ const auto is_user_provided_and_rewritten = (
+ // files from other sources can have their mtimes monkeyed with
+ this->lf_options.loo_source == logfile_name_source::USER
+ && this->lf_stat.st_size == st.st_size
+ && this->lf_stat.st_mtime != st.st_mtime);
+
+ // Check the previous stat against the last to see if things are wonky.
+ if (this->lf_named_file && (is_truncated || is_user_provided_and_rewritten))
+ {
+ log_info("overwritten file detected, closing -- %s new: %" PRId64
+ "/%" PRId64 " old: %" PRId64 "/%" PRId64,
+ this->lf_filename.c_str(),
+ st.st_size,
+ st.st_mtime,
+ this->lf_stat.st_size,
+ this->lf_stat.st_mtime);
+ this->close();
+ return rebuild_result_t::NO_NEW_LINES;
+ } else if (this->lf_line_buffer.is_data_available(this->lf_index_size,
+ st.st_size))
+ {
+ this->lf_activity.la_reads += 1;
+
+ // We haven't reached the end of the file. Note that we use the
+ // line buffer's notion of the file size since it may be compressed.
+ bool has_format = this->lf_format.get() != nullptr;
+ struct rusage begin_rusage;
+ file_off_t off;
+ size_t begin_size = this->lf_index.size();
+ bool record_rusage = this->lf_index.size() == 1;
+ off_t begin_index_size = this->lf_index_size;
+ size_t rollback_size = 0;
+
+ if (record_rusage) {
+ getrusage(RUSAGE_SELF, &begin_rusage);
+ }
+
+ if (begin_size == 0 && !has_format) {
+ log_debug("scanning file... %s", this->lf_filename.c_str());
+ }
+
+ if (!this->lf_index.empty()) {
+ off = this->lf_index.back().get_offset();
+
+ /*
+ * Drop the last line we read since it might have been a partial
+ * read.
+ */
+ while (this->lf_index.back().get_sub_offset() != 0) {
+ this->lf_index.pop_back();
+ rollback_size += 1;
+ }
+ this->lf_index.pop_back();
+ rollback_size += 1;
+
+ if (!this->lf_index.empty()) {
+ auto last_line = this->lf_index.end();
+ --last_line;
+ auto check_line_off = last_line->get_offset();
+ auto last_length_res
+ = this->message_byte_length(last_line, false);
+ log_debug("flushing at %d", check_line_off);
+ this->lf_line_buffer.flush_at(check_line_off);
+
+ auto read_result = this->lf_line_buffer.read_range({
+ check_line_off,
+ last_length_res.mlr_length,
+ });
+
+ if (read_result.isErr()) {
+ log_info("overwritten file detected, closing -- %s (%s)",
+ this->lf_filename.c_str(),
+ read_result.unwrapErr().c_str());
+ this->close();
+ return rebuild_result_t::INVALID;
+ }
+ } else {
+ this->lf_line_buffer.flush_at(0);
+ }
+ } else {
+ this->lf_line_buffer.flush_at(0);
+ off = 0;
+ }
+ if (this->lf_logline_observer != nullptr) {
+ this->lf_logline_observer->logline_restart(*this, rollback_size);
+ }
+
+ bool sort_needed = std::exchange(this->lf_sort_needed, false);
+ size_t limit = SIZE_MAX;
+
+ if (deadline) {
+ if (ui_clock::now() > deadline.value()) {
+ if (has_format) {
+ log_warning("with format ran past deadline! -- %s",
+ this->lf_filename.c_str());
+ limit = 1000;
+ } else {
+ limit = 100;
+ }
+ } else if (!has_format) {
+ limit = 1000;
+ } else {
+ limit = 1000 * 1000;
+ }
+ }
+ if (!has_format) {
+ log_debug(
+ "loading file... %s:%d", this->lf_filename.c_str(), begin_size);
+ }
+ scan_batch_context sbc{this->lf_allocator};
+ sbc.sbc_opids.reserve(32);
+ auto prev_range = file_range{off};
+ while (limit > 0) {
+ auto load_result = this->lf_line_buffer.load_next_line(prev_range);
+
+ if (load_result.isErr()) {
+ log_error("%s: load next line failure -- %s",
+ this->lf_filename.c_str(),
+ load_result.unwrapErr().c_str());
+ this->close();
+ return rebuild_result_t::INVALID;
+ }
+
+ auto li = load_result.unwrap();
+
+ if (li.li_file_range.empty()) {
+ break;
+ }
+ prev_range = li.li_file_range;
+
+ if (this->lf_format == nullptr
+ && !this->lf_options.loo_non_utf_is_visible
+ && !li.li_utf8_scan_result.is_valid())
+ {
+ log_info("file is not utf, hiding: %s",
+ this->lf_filename.c_str());
+ this->lf_indexing = false;
+ this->lf_options.loo_is_visible = false;
+ auto note_text = fmt::format(
+ FMT_STRING("not indexing non-UTF-8 file -- line: "
+ "{}; column: {}; error: {}"),
+ this->lf_index.size() + 1,
+ li.li_utf8_scan_result.usr_valid_frag.sf_end,
+ li.li_utf8_scan_result.usr_message);
+ this->lf_notes.writeAccess()->emplace(note_type::not_utf,
+ note_text);
+ if (this->lf_logfile_observer != nullptr) {
+ this->lf_logfile_observer->logfile_indexing(
+ this->shared_from_this(), 0, 0);
+ }
+ break;
+ }
+ if (this->lf_format != nullptr
+ && !li.li_utf8_scan_result.is_valid())
+ {
+ log_warning("%s: invalid UTF-8 detected at %d:%d -- %s",
+ this->lf_filename.c_str(),
+ this->lf_index.size() + 1,
+ li.li_utf8_scan_result.usr_valid_frag.sf_end,
+ li.li_utf8_scan_result.usr_message);
+ }
+
+ size_t old_size = this->lf_index.size();
+
+ if (old_size == 0
+ && this->lf_text_format == text_format_t::TF_UNKNOWN)
+ {
+ file_range fr = this->lf_line_buffer.get_available();
+ auto avail_data = this->lf_line_buffer.read_range(fr);
+
+ this->lf_text_format
+ = avail_data
+ .map([path = this->get_path()](
+ const shared_buffer_ref& avail_sbr)
+ -> text_format_t {
+ return detect_text_format(
+ avail_sbr.to_string_fragment(), path);
+ })
+ .unwrapOr(text_format_t::TF_UNKNOWN);
+ log_debug("setting text format to %d", this->lf_text_format);
+ }
+ if (!li.li_utf8_scan_result.is_valid()
+ && this->lf_text_format != text_format_t::TF_MARKDOWN
+ && this->lf_text_format != text_format_t::TF_LOG)
+ {
+ this->lf_text_format = text_format_t::TF_BINARY;
+ }
+
+ auto read_result
+ = this->lf_line_buffer.read_range(li.li_file_range);
+ if (read_result.isErr()) {
+ log_error("%s:read failure -- %s",
+ this->lf_filename.c_str(),
+ read_result.unwrapErr().c_str());
+ this->close();
+ return rebuild_result_t::INVALID;
+ }
+
+ auto sbr = read_result.unwrap();
+ sbr.rtrim(is_line_ending);
+
+ if (li.li_utf8_scan_result.is_valid()
+ && li.li_utf8_scan_result.usr_has_ansi)
+ {
+ sbr.erase_ansi();
+ }
+
+ this->lf_longest_line
+ = std::max(this->lf_longest_line, sbr.length());
+ this->lf_partial_line = li.li_partial;
+ sort_needed = this->process_prefix(sbr, li, sbc) || sort_needed;
+
+ if (old_size > this->lf_index.size()) {
+ old_size = 0;
+ }
+
+ // Update this early so that line_length() works
+ this->lf_index_size = li.li_file_range.next_offset();
+
+ if (this->lf_logline_observer != nullptr) {
+ this->lf_logline_observer->logline_new_lines(
+ *this, this->begin() + old_size, this->end(), sbr);
+ }
+
+ if (this->lf_logfile_observer != nullptr) {
+ auto indexing_res = this->lf_logfile_observer->logfile_indexing(
+ this->shared_from_this(),
+ this->lf_line_buffer.get_read_offset(
+ li.li_file_range.next_offset()),
+ st.st_size);
+
+ if (indexing_res == logfile_observer::indexing_result::BREAK) {
+ break;
+ }
+ }
+
+ if (!has_format && this->lf_format != nullptr) {
+ break;
+ }
+ if (begin_size == 0 && !has_format
+ && li.li_file_range.fr_offset > 16 * 1024)
+ {
+ break;
+ }
+#if 0
+ if (this->lf_line_buffer.is_likely_to_flush(prev_range)
+ && this->lf_index.size() - begin_size > 1)
+ {
+ log_debug("likely to flush, breaking");
+ break;
+ }
+#endif
+ if (this->lf_format) {
+ if (!this->lf_applicable_taggers.empty()) {
+ auto sf = sbr.to_string_fragment();
+
+ for (const auto& td : this->lf_applicable_taggers) {
+ auto curr_ll = this->end() - 1;
+
+ if (td->ftd_level != LEVEL_UNKNOWN
+ && td->ftd_level != curr_ll->get_msg_level())
+ {
+ continue;
+ }
+
+ if (td->ftd_pattern.pp_value
+ ->find_in(sf, PCRE2_NO_UTF_CHECK)
+ .ignore_error()
+ .has_value())
+ {
+ curr_ll->set_mark(true);
+ while (curr_ll->is_continued()) {
+ --curr_ll;
+ }
+ auto line_number = static_cast<uint32_t>(
+ std::distance(this->begin(), curr_ll));
+
+ this->lf_bookmark_metadata[line_number].add_tag(
+ td->ftd_name);
+ }
+ }
+ }
+
+ if (!this->back().is_continued()) {
+ lnav::log::watch::eval_with(*this, this->end() - 1);
+ }
+ }
+
+ if (li.li_partial) {
+ // The last read was at the end of the file, so break. We'll
+ // need to cycle back around to pop off this partial line in
+ // order to continue reading correctly.
+ break;
+ }
+
+ limit -= 1;
+ }
+
+ if (this->lf_format == nullptr
+ && this->lf_options.loo_visible_size_limit > 0
+ && prev_range.fr_offset > 256 * 1024
+ && st.st_size >= this->lf_options.loo_visible_size_limit)
+ {
+ log_info("file has unknown format and is too large: %s",
+ this->lf_filename.c_str());
+ this->lf_indexing = false;
+ this->lf_notes.writeAccess()->emplace(
+ note_type::indexing_disabled,
+ "not indexing large file with no discernible log format");
+ if (this->lf_logfile_observer != nullptr) {
+ this->lf_logfile_observer->logfile_indexing(
+ this->shared_from_this(), 0, 0);
+ }
+ }
+
+ if (this->lf_logline_observer != nullptr) {
+ this->lf_logline_observer->logline_eof(*this);
+ }
+
+ if (record_rusage
+ && (prev_range.fr_offset - begin_index_size) > (500 * 1024))
+ {
+ struct rusage end_rusage;
+
+ getrusage(RUSAGE_SELF, &end_rusage);
+ rusagesub(end_rusage,
+ begin_rusage,
+ this->lf_activity.la_initial_index_rusage);
+ log_info("Resource usage for initial indexing of file: %s:%d-%d",
+ this->lf_filename.c_str(),
+ begin_size,
+ this->lf_index.size());
+ log_rusage(lnav_log_level_t::INFO,
+ this->lf_activity.la_initial_index_rusage);
+ }
+
+ /*
+ * The file can still grow between the above fstat and when we're
+ * doing the scanning, so use the line buffer's notion of the file
+ * size.
+ */
+ this->lf_index_size = prev_range.next_offset();
+ this->lf_stat = st;
+
+ {
+ safe::WriteAccess<logfile::safe_opid_map> writable_opid_map(
+ this->lf_opids);
+
+ for (const auto& opid_pair : sbc.sbc_opids) {
+ auto opid_iter = writable_opid_map->find(opid_pair.first);
+
+ if (opid_iter == writable_opid_map->end()) {
+ writable_opid_map->emplace(opid_pair);
+ } else {
+ if (opid_pair.second.otr_begin
+ < opid_iter->second.otr_begin)
+ {
+ opid_iter->second.otr_begin
+ = opid_pair.second.otr_begin;
+ }
+ if (opid_iter->second.otr_end < opid_pair.second.otr_end) {
+ opid_iter->second.otr_end = opid_pair.second.otr_end;
+ }
+ }
+ }
+ }
+
+ if (sort_needed) {
+ retval = rebuild_result_t::NEW_ORDER;
+ } else {
+ retval = rebuild_result_t::NEW_LINES;
+ }
+ } else if (this->lf_sort_needed) {
+ retval = rebuild_result_t::NEW_ORDER;
+ this->lf_sort_needed = false;
+ }
+
+ this->lf_index_time = this->lf_line_buffer.get_file_time();
+ if (!this->lf_index_time) {
+ this->lf_index_time = st.st_mtime;
+ }
+
+ if (this->lf_out_of_time_order_count) {
+ log_info("Detected %d out-of-time-order lines in file: %s",
+ this->lf_out_of_time_order_count,
+ this->lf_filename.c_str());
+ this->lf_out_of_time_order_count = 0;
+ }
+
+ return retval;
+}
+
+Result<shared_buffer_ref, std::string>
+logfile::read_line(logfile::iterator ll)
+{
+ try {
+ auto get_range_res = this->get_file_range(ll, false);
+ return this->lf_line_buffer.read_range(get_range_res)
+ .map([&ll, &get_range_res, this](auto sbr) {
+ sbr.rtrim(is_line_ending);
+ if (!get_range_res.fr_metadata.m_valid_utf) {
+ scrub_to_utf8(sbr.get_writable_data(), sbr.length());
+ sbr.get_metadata().m_valid_utf = true;
+ }
+
+ if (this->lf_format != nullptr) {
+ this->lf_format->get_subline(*ll, sbr);
+ }
+
+ return sbr;
+ });
+ } catch (const line_buffer::error& e) {
+ return Err(std::string(strerror(e.e_err)));
+ }
+}
+
+Result<std::string, std::string>
+logfile::read_file()
+{
+ if (this->lf_stat.st_size > line_buffer::MAX_LINE_BUFFER_SIZE) {
+ return Err(std::string("file is too large to read"));
+ }
+
+ auto retval
+ = TRY(this->lf_line_buffer.read_range({0, this->lf_stat.st_size}));
+
+ return Ok(to_string(retval));
+}
+
+void
+logfile::read_full_message(logfile::const_iterator ll,
+ shared_buffer_ref& msg_out,
+ int max_lines)
+{
+ require(ll->get_sub_offset() == 0);
+
+#if 0
+ log_debug(
+ "%s: reading msg at %d", this->lf_filename.c_str(), ll->get_offset());
+#endif
+
+ msg_out.disown();
+ auto range_for_line = this->get_file_range(ll);
+ try {
+ if (range_for_line.fr_size > line_buffer::MAX_LINE_BUFFER_SIZE) {
+ range_for_line.fr_size = line_buffer::MAX_LINE_BUFFER_SIZE;
+ }
+ auto read_result = this->lf_line_buffer.read_range(range_for_line);
+
+ if (read_result.isErr()) {
+ log_error("unable to read range %d:%d",
+ range_for_line.fr_offset,
+ range_for_line.fr_size);
+ return;
+ }
+ msg_out = read_result.unwrap();
+ msg_out.get_metadata() = range_for_line.fr_metadata;
+ if (this->lf_format.get() != nullptr) {
+ this->lf_format->get_subline(*ll, msg_out, true);
+ }
+ } catch (const line_buffer::error& e) {
+ log_error("failed to read line");
+ }
+}
+
+void
+logfile::set_logline_observer(logline_observer* llo)
+{
+ this->lf_logline_observer = llo;
+ if (llo != nullptr) {
+ this->reobserve_from(this->begin());
+ }
+}
+
+void
+logfile::reobserve_from(iterator iter)
+{
+ for (; iter != this->end(); ++iter) {
+ off_t offset = std::distance(this->begin(), iter);
+
+ if (iter->get_sub_offset() > 0) {
+ continue;
+ }
+
+ if (this->lf_logfile_observer != nullptr) {
+ auto indexing_res = this->lf_logfile_observer->logfile_indexing(
+ this->shared_from_this(), offset, this->size());
+ if (indexing_res == logfile_observer::indexing_result::BREAK) {
+ break;
+ }
+ }
+
+ this->read_line(iter).then([this, iter](auto sbr) {
+ auto iter_end = iter + 1;
+
+ while (iter_end != this->end() && iter_end->get_sub_offset() != 0) {
+ ++iter_end;
+ }
+ this->lf_logline_observer->logline_new_lines(
+ *this, iter, iter_end, sbr);
+ });
+ }
+ if (this->lf_logfile_observer != nullptr) {
+ this->lf_logfile_observer->logfile_indexing(
+ this->shared_from_this(), this->size(), this->size());
+ this->lf_logline_observer->logline_eof(*this);
+ }
+}
+
+ghc::filesystem::path
+logfile::get_path() const
+{
+ return this->lf_filename;
+}
+
+logfile::message_length_result
+logfile::message_byte_length(logfile::const_iterator ll, bool include_continues)
+{
+ auto next_line = ll;
+ file_range::metadata meta;
+ size_t retval;
+
+ if (!include_continues && this->lf_next_line_cache) {
+ if (ll->get_offset() == (*this->lf_next_line_cache).first) {
+ return {
+ (file_ssize_t) this->lf_next_line_cache->second,
+ {ll->is_valid_utf(), ll->has_ansi()},
+ };
+ }
+ }
+
+ do {
+ meta.m_has_ansi = meta.m_has_ansi || next_line->has_ansi();
+ meta.m_valid_utf = meta.m_valid_utf && next_line->is_valid_utf();
+ ++next_line;
+ } while ((next_line != this->end())
+ && ((ll->get_offset() == next_line->get_offset())
+ || (include_continues && next_line->is_continued())));
+
+ if (next_line == this->end()) {
+ retval = this->lf_index_size - ll->get_offset();
+ if (retval > line_buffer::MAX_LINE_BUFFER_SIZE) {
+ retval = line_buffer::MAX_LINE_BUFFER_SIZE;
+ }
+ if (retval > 0 && !this->lf_partial_line) {
+ retval -= 1;
+ }
+ } else {
+ retval = next_line->get_offset() - ll->get_offset() - 1;
+ if (!include_continues) {
+ this->lf_next_line_cache = nonstd::make_optional(
+ std::make_pair(ll->get_offset(), retval));
+ }
+ }
+
+ return {(file_ssize_t) retval, meta};
+}
+
+Result<shared_buffer_ref, std::string>
+logfile::read_raw_message(logfile::const_iterator ll)
+{
+ require(ll->get_sub_offset() == 0);
+
+ return this->lf_line_buffer.read_range(this->get_file_range(ll));
+}
+
+intern_string_t
+logfile::get_format_name() const
+{
+ if (this->lf_format) {
+ return this->lf_format->get_name();
+ }
+
+ return {};
+}
+
+nonstd::optional<logfile::const_iterator>
+logfile::find_from_time(const timeval& tv) const
+{
+ auto retval
+ = std::lower_bound(this->lf_index.begin(), this->lf_index.end(), tv);
+ if (retval == this->lf_index.end()) {
+ return nonstd::nullopt;
+ }
+
+ return retval;
+}
+
+void
+logfile::mark_as_duplicate(const std::string& name)
+{
+ this->lf_indexing = false;
+ this->lf_options.loo_is_visible = false;
+ this->lf_notes.writeAccess()->emplace(
+ note_type::duplicate,
+ fmt::format(FMT_STRING("hiding duplicate of {}"), name));
+}
+
+void
+logfile::adjust_content_time(int line, const timeval& tv, bool abs_offset)
+{
+ struct timeval old_time = this->lf_time_offset;
+
+ this->lf_time_offset_line = line;
+ if (abs_offset) {
+ this->lf_time_offset = tv;
+ } else {
+ timeradd(&old_time, &tv, &this->lf_time_offset);
+ }
+ for (auto& iter : *this) {
+ struct timeval curr, diff, new_time;
+
+ curr = iter.get_timeval();
+ timersub(&curr, &old_time, &diff);
+ timeradd(&diff, &this->lf_time_offset, &new_time);
+ iter.set_time(new_time);
+ }
+ this->lf_sort_needed = true;
+}
+
+void
+logfile::set_filename(const std::string& filename)
+{
+ if (this->lf_filename != filename) {
+ this->lf_filename = filename;
+ ghc::filesystem::path p(filename);
+ this->lf_basename = p.filename();
+ }
+}
+
+struct timeval
+logfile::original_line_time(logfile::iterator ll)
+{
+ if (this->is_time_adjusted()) {
+ struct timeval line_time = ll->get_timeval();
+ struct timeval retval;
+
+ timersub(&line_time, &this->lf_time_offset, &retval);
+ return retval;
+ }
+
+ return ll->get_timeval();
+}
+
+nonstd::optional<logfile::const_iterator>
+logfile::line_for_offset(file_off_t off) const
+{
+ struct cmper {
+ bool operator()(const file_off_t& lhs, const logline& rhs) const
+ {
+ return lhs < rhs.get_offset();
+ }
+
+ bool operator()(const logline& lhs, const file_off_t& rhs) const
+ {
+ return lhs.get_offset() < rhs;
+ }
+ };
+
+ if (this->lf_index.empty()) {
+ return nonstd::nullopt;
+ }
+
+ auto iter = std::lower_bound(
+ this->lf_index.begin(), this->lf_index.end(), off, cmper{});
+ if (iter == this->lf_index.end()) {
+ if (this->lf_index.back().get_offset() <= off
+ && off < this->lf_index_size)
+ {
+ return nonstd::make_optional(iter);
+ }
+ return nonstd::nullopt;
+ }
+
+ if (off < iter->get_offset() && iter != this->lf_index.begin()) {
+ --iter;
+ }
+
+ return nonstd::make_optional(iter);
+}
+
+void
+logfile::dump_stats()
+{
+ const auto buf_stats = this->lf_line_buffer.consume_stats();
+
+ if (buf_stats.empty()) {
+ return;
+ }
+ log_info("line buffer stats for file: %s", this->lf_filename.c_str());
+ log_info(" file_size=%lld", this->lf_line_buffer.get_file_size());
+ log_info(" buffer_size=%ld", this->lf_line_buffer.get_buffer_size());
+ log_info(" read_hist=[%4lu %4lu %4lu %4lu %4lu %4lu %4lu %4lu %4lu %4lu]",
+ buf_stats.s_hist[0],
+ buf_stats.s_hist[1],
+ buf_stats.s_hist[2],
+ buf_stats.s_hist[3],
+ buf_stats.s_hist[4],
+ buf_stats.s_hist[5],
+ buf_stats.s_hist[6],
+ buf_stats.s_hist[7],
+ buf_stats.s_hist[8],
+ buf_stats.s_hist[9]);
+ log_info(" decompressions=%lu", buf_stats.s_decompressions);
+ log_info(" preads=%lu", buf_stats.s_preads);
+ log_info(" requested_preloads=%lu", buf_stats.s_requested_preloads);
+ log_info(" used_preloads=%lu", buf_stats.s_used_preloads);
+}
diff --git a/src/logfile.cfg.hh b/src/logfile.cfg.hh
new file mode 100644
index 0000000..8b0f68b
--- /dev/null
+++ b/src/logfile.cfg.hh
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file logfile.cfg.hh
+ */
+
+#ifndef lnav_logfile_cfg_hh
+#define lnav_logfile_cfg_hh
+
+namespace lnav {
+namespace logfile {
+
+struct config {
+ uint64_t lc_max_unrecognized_lines{1000};
+};
+
+} // namespace logfile
+} // namespace lnav
+
+#endif
diff --git a/src/logfile.hh b/src/logfile.hh
new file mode 100644
index 0000000..8c711a0
--- /dev/null
+++ b/src/logfile.hh
@@ -0,0 +1,462 @@
+
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file logfile.hh
+ */
+
+#ifndef logfile_hh
+#define logfile_hh
+
+#include <set>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <sys/resource.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+
+#include "ArenaAlloc/arenaalloc.h"
+#include "base/lnav_log.hh"
+#include "base/result.h"
+#include "bookmarks.hh"
+#include "byte_array.hh"
+#include "ghc/filesystem.hpp"
+#include "line_buffer.hh"
+#include "log_format_fwd.hh"
+#include "logfile_fwd.hh"
+#include "safe/safe.h"
+#include "shared_buffer.hh"
+#include "text_format.hh"
+#include "unique_path.hh"
+
+/**
+ * Observer interface for logfile indexing progress.
+ *
+ * @see logfile
+ */
+class logfile_observer {
+public:
+ virtual ~logfile_observer() = default;
+
+ enum class indexing_result {
+ CONTINUE,
+ BREAK,
+ };
+
+ /**
+ * @param lf The logfile object that is doing the indexing.
+ * @param off The current offset in the file being processed.
+ * @param total The total size of the file.
+ * @return false
+ */
+ virtual indexing_result logfile_indexing(const std::shared_ptr<logfile>& lf,
+ file_off_t off,
+ file_size_t total)
+ = 0;
+};
+
+struct logfile_activity {
+ int64_t la_polls{0};
+ int64_t la_reads{0};
+ struct rusage la_initial_index_rusage {};
+};
+
+/**
+ * Container for the lines in a log file and some metadata.
+ */
+class logfile
+ : public unique_path_source
+ , public std::enable_shared_from_this<logfile> {
+public:
+ using iterator = std::vector<logline>::iterator;
+ using const_iterator = std::vector<logline>::const_iterator;
+
+ struct metadata {
+ text_format_t m_format;
+ std::string m_value;
+ };
+
+ /**
+ * Construct a logfile with the given arguments.
+ *
+ * @param filename The name of the log file.
+ * @param fd The file descriptor for accessing the file or -1 if the
+ * constructor should open the file specified by 'filename'. The
+ * descriptor needs to be seekable.
+ */
+ static Result<std::shared_ptr<logfile>, std::string> open(
+ std::string filename, logfile_open_options& loo);
+
+ ~logfile() override;
+
+ const logfile_activity& get_activity() const { return this->lf_activity; }
+
+ nonstd::optional<ghc::filesystem::path> get_actual_path() const
+ {
+ return this->lf_actual_path;
+ }
+
+ /** @return The filename as given in the constructor. */
+ const std::string& get_filename() const { return this->lf_filename; }
+
+ /** @return The filename as given in the constructor, excluding the path
+ * prefix. */
+ const std::string& get_basename() const { return this->lf_basename; }
+
+ int get_fd() const { return this->lf_line_buffer.get_fd(); }
+
+ /** @param filename The new filename for this log file. */
+ void set_filename(const std::string& filename);
+
+ const std::string& get_content_id() const { return this->lf_content_id; }
+
+ /** @return The inode for this log file. */
+ const struct stat& get_stat() const { return this->lf_stat; }
+
+ size_t get_longest_line_length() const { return this->lf_longest_line; }
+
+ bool is_compressed() const { return this->lf_line_buffer.is_compressed(); }
+
+ bool is_valid_filename() const { return this->lf_valid_filename; }
+
+ file_off_t get_index_size() const { return this->lf_index_size; }
+
+ nonstd::optional<const_iterator> line_for_offset(file_off_t off) const;
+
+ /**
+ * @return The detected format, rebuild_index() must be called before this
+ * will return a value other than NULL.
+ */
+ std::shared_ptr<log_format> get_format() const { return this->lf_format; }
+
+ log_format* get_format_ptr() const { return this->lf_format.get(); }
+
+ intern_string_t get_format_name() const;
+
+ text_format_t get_text_format() const { return this->lf_text_format; }
+
+ /**
+ * @return The last modified time of the file when the file was last
+ * indexed.
+ */
+ time_t get_modified_time() const { return this->lf_index_time; }
+
+ int get_time_offset_line() const { return this->lf_time_offset_line; }
+
+ const struct timeval& get_time_offset() const
+ {
+ return this->lf_time_offset;
+ }
+
+ void adjust_content_time(int line,
+ const struct timeval& tv,
+ bool abs_offset = true);
+
+ void clear_time_offset()
+ {
+ struct timeval tv = {0, 0};
+
+ this->adjust_content_time(-1, tv);
+ }
+
+ void mark_as_duplicate(const std::string& name);
+
+ const logfile_open_options& get_open_options() const
+ {
+ return this->lf_options;
+ }
+
+ void reset_state();
+
+ bool is_time_adjusted() const
+ {
+ return (this->lf_time_offset.tv_sec != 0
+ || this->lf_time_offset.tv_usec != 0);
+ }
+
+ iterator begin() { return this->lf_index.begin(); }
+
+ const_iterator begin() const { return this->lf_index.begin(); }
+
+ const_iterator cbegin() const { return this->lf_index.begin(); }
+
+ iterator end() { return this->lf_index.end(); }
+
+ const_iterator end() const { return this->lf_index.end(); }
+
+ const_iterator cend() const { return this->lf_index.end(); }
+
+ /** @return The number of lines in the index. */
+ size_t size() const { return this->lf_index.size(); }
+
+ nonstd::optional<const_iterator> find_from_time(
+ const struct timeval& tv) const;
+
+ logline& operator[](int index) { return this->lf_index[index]; }
+
+ logline& front() { return this->lf_index.front(); }
+
+ logline& back() { return this->lf_index.back(); }
+
+ /** @return True if this log file still exists. */
+ bool exists() const;
+
+ void close() { this->lf_is_closed = true; }
+
+ bool is_closed() const { return this->lf_is_closed; }
+
+ struct timeval original_line_time(iterator ll);
+
+ Result<shared_buffer_ref, std::string> read_line(iterator ll);
+
+ Result<std::string, std::string> read_file();
+
+ iterator line_base(iterator ll)
+ {
+ auto retval = ll;
+
+ while (retval != this->begin() && retval->get_sub_offset() != 0) {
+ --retval;
+ }
+
+ return retval;
+ }
+
+ iterator message_start(iterator ll)
+ {
+ auto retval = ll;
+
+ while (retval != this->begin()
+ && (retval->get_sub_offset() != 0 || !retval->is_message()))
+ {
+ --retval;
+ }
+
+ return retval;
+ }
+
+ struct message_length_result {
+ file_ssize_t mlr_length;
+ file_range::metadata mlr_metadata;
+ };
+
+ message_length_result message_byte_length(const_iterator ll,
+ bool include_continues = true);
+
+ file_range get_file_range(const_iterator ll, bool include_continues = true)
+ {
+ auto mlr = this->message_byte_length(ll, include_continues);
+
+ return {
+ ll->get_offset(),
+ mlr.mlr_length,
+ mlr.mlr_metadata,
+ };
+ }
+
+ void read_full_message(const_iterator ll,
+ shared_buffer_ref& msg_out,
+ int max_lines = 50);
+
+ Result<shared_buffer_ref, std::string> read_raw_message(const_iterator ll);
+
+ enum class rebuild_result_t {
+ INVALID,
+ NO_NEW_LINES,
+ NEW_LINES,
+ NEW_ORDER,
+ };
+
+ /**
+ * Index any new data in the log file.
+ *
+ * @param lo The observer object that will be called regularly during
+ * indexing.
+ * @return True if any new lines were indexed.
+ */
+ rebuild_result_t rebuild_index(
+ nonstd::optional<ui_clock::time_point> deadline = nonstd::nullopt);
+
+ void reobserve_from(iterator iter);
+
+ void set_logfile_observer(logfile_observer* lo)
+ {
+ this->lf_logfile_observer = lo;
+ }
+
+ void set_logline_observer(logline_observer* llo);
+
+ logline_observer* get_logline_observer() const
+ {
+ return this->lf_logline_observer;
+ }
+
+ bool operator<(const logfile& rhs) const
+ {
+ bool retval;
+
+ if (this->lf_index.empty()) {
+ retval = true;
+ } else if (rhs.lf_index.empty()) {
+ retval = false;
+ } else {
+ retval = this->lf_index[0].get_time() < rhs.lf_index[0].get_time();
+ }
+
+ return retval;
+ }
+
+ bool is_indexing() const { return this->lf_indexing; }
+
+ /** Check the invariants for this object. */
+ bool invariant()
+ {
+ require(!this->lf_filename.empty());
+
+ return true;
+ }
+
+ ghc::filesystem::path get_path() const override;
+
+ enum class note_type {
+ indexing_disabled,
+ duplicate,
+ not_utf,
+ };
+
+ using note_map = std::map<note_type, std::string>;
+ using safe_notes = safe::Safe<note_map>;
+
+ note_map get_notes() const { return *this->lf_notes.readAccess(); }
+
+ using safe_opid_map = safe::Safe<log_opid_map>;
+
+ safe_opid_map& get_opids() { return this->lf_opids; }
+
+ void quiesce() { this->lf_line_buffer.quiesce(); }
+
+ void enable_cache() { this->lf_line_buffer.enable_cache(); }
+
+ void dump_stats();
+
+ robin_hood::unordered_map<uint32_t, bookmark_metadata>&
+ get_bookmark_metadata()
+ {
+ return this->lf_bookmark_metadata;
+ }
+
+ std::map<std::string, metadata>& get_embedded_metadata()
+ {
+ return this->lf_embedded_metadata;
+ }
+
+ const std::map<std::string, metadata>& get_embedded_metadata() const
+ {
+ return this->lf_embedded_metadata;
+ }
+
+protected:
+ /**
+ * Process a line from the file.
+ *
+ * @param offset The offset of the line in the file.
+ * @param prefix The contents of the line.
+ * @param len The length of the 'prefix' string.
+ */
+ bool process_prefix(shared_buffer_ref& sbr,
+ const line_info& li,
+ scan_batch_context& sbc);
+
+ void set_format_base_time(log_format* lf);
+
+private:
+ logfile(std::string filename, logfile_open_options& loo);
+
+ std::string lf_filename;
+ logfile_open_options lf_options;
+ logfile_activity lf_activity;
+ bool lf_named_file{true};
+ bool lf_valid_filename{true};
+ nonstd::optional<ghc::filesystem::path> lf_actual_path;
+ std::string lf_basename;
+ std::string lf_content_id;
+ struct stat lf_stat {};
+ std::shared_ptr<log_format> lf_format;
+ std::vector<logline> lf_index;
+ time_t lf_index_time{0};
+ file_off_t lf_index_size{0};
+ bool lf_sort_needed{false};
+ line_buffer lf_line_buffer;
+ int lf_time_offset_line{0};
+ struct timeval lf_time_offset {
+ 0, 0
+ };
+ bool lf_is_closed{false};
+ bool lf_indexing{true};
+ bool lf_partial_line{false};
+ logline_observer* lf_logline_observer{nullptr};
+ logfile_observer* lf_logfile_observer{nullptr};
+ size_t lf_longest_line{0};
+ text_format_t lf_text_format{text_format_t::TF_UNKNOWN};
+ uint32_t lf_out_of_time_order_count{0};
+ safe_notes lf_notes;
+ safe_opid_map lf_opids;
+ size_t lf_watch_count{0};
+ ArenaAlloc::Alloc<char> lf_allocator{64 * 1024};
+ nonstd::optional<time_t> lf_cached_base_time;
+ nonstd::optional<tm> lf_cached_base_tm;
+
+ nonstd::optional<std::pair<file_off_t, size_t>> lf_next_line_cache;
+ std::set<intern_string_t> lf_mismatched_formats;
+ robin_hood::unordered_map<uint32_t, bookmark_metadata> lf_bookmark_metadata;
+
+ std::vector<std::shared_ptr<format_tag_def>> lf_applicable_taggers;
+ std::map<std::string, metadata> lf_embedded_metadata;
+};
+
+class logline_observer {
+public:
+ virtual ~logline_observer() = default;
+
+ virtual void logline_restart(const logfile& lf, file_size_t rollback_size)
+ = 0;
+
+ virtual void logline_new_lines(const logfile& lf,
+ logfile::const_iterator ll_begin,
+ logfile::const_iterator ll_end,
+ const shared_buffer_ref& sbr)
+ = 0;
+
+ virtual void logline_eof(const logfile& lf) = 0;
+};
+
+#endif
diff --git a/src/logfile_fwd.hh b/src/logfile_fwd.hh
new file mode 100644
index 0000000..4ea8fea
--- /dev/null
+++ b/src/logfile_fwd.hh
@@ -0,0 +1,160 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file logfile_fwd.hh
+ */
+
+#ifndef lnav_logfile_fwd_hh
+#define lnav_logfile_fwd_hh
+
+#include <chrono>
+#include <string>
+
+#include "base/auto_fd.hh"
+#include "file_format.hh"
+
+using ui_clock = std::chrono::steady_clock;
+
+class logfile;
+class logline;
+class logline_observer;
+
+using logfile_const_iterator = std::vector<logline>::const_iterator;
+
+enum class logfile_name_source {
+ USER,
+ ARCHIVE,
+ REMOTE,
+};
+
+struct logfile_open_options_base {
+ std::string loo_filename;
+ logfile_name_source loo_source{logfile_name_source::USER};
+ bool loo_temp_file{false};
+ dev_t loo_temp_dev{0};
+ ino_t loo_temp_ino{0};
+ bool loo_detect_format{true};
+ bool loo_include_in_session{true};
+ bool loo_is_visible{true};
+ bool loo_non_utf_is_visible{true};
+ ssize_t loo_visible_size_limit{-1};
+ bool loo_tail{true};
+ file_format_t loo_file_format{file_format_t::UNKNOWN};
+};
+
+struct logfile_open_options : public logfile_open_options_base {
+ logfile_open_options() = default;
+
+ explicit logfile_open_options(const logfile_open_options_base& base)
+ : logfile_open_options_base(base)
+ {
+ }
+
+ logfile_open_options& with_filename(const std::string& val)
+ {
+ this->loo_filename = val;
+
+ return *this;
+ }
+
+ logfile_open_options& with_fd(auto_fd fd)
+ {
+ this->loo_fd = std::move(fd);
+ this->loo_temp_file = true;
+
+ return *this;
+ }
+
+ logfile_open_options& with_stat_for_temp(const struct stat& st)
+ {
+ this->loo_temp_dev = st.st_dev;
+ this->loo_temp_ino = st.st_ino;
+
+ return *this;
+ }
+
+ logfile_open_options& with_source(logfile_name_source src)
+ {
+ this->loo_source = src;
+
+ return *this;
+ }
+
+ logfile_open_options& with_detect_format(bool val)
+ {
+ this->loo_detect_format = val;
+
+ return *this;
+ }
+
+ logfile_open_options& with_include_in_session(bool val)
+ {
+ this->loo_include_in_session = val;
+
+ return *this;
+ };
+
+ logfile_open_options& with_visibility(bool val)
+ {
+ this->loo_is_visible = val;
+
+ return *this;
+ }
+
+ logfile_open_options& with_non_utf_visibility(bool val)
+ {
+ this->loo_non_utf_is_visible = val;
+
+ return *this;
+ };
+
+ logfile_open_options& with_visible_size_limit(ssize_t val)
+ {
+ this->loo_visible_size_limit = val;
+
+ return *this;
+ }
+
+ logfile_open_options& with_tail(bool val)
+ {
+ this->loo_tail = val;
+
+ return *this;
+ }
+
+ logfile_open_options& with_file_format(file_format_t ff)
+ {
+ this->loo_file_format = ff;
+
+ return *this;
+ }
+
+ auto_fd loo_fd;
+};
+
+#endif
diff --git a/src/logfile_stats.hh b/src/logfile_stats.hh
new file mode 100644
index 0000000..8131e63
--- /dev/null
+++ b/src/logfile_stats.hh
@@ -0,0 +1,40 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef logfile_stats_hh
+#define logfile_stats_hh
+
+class logfile_activity {
+public:
+ logfile_activity();
+ virtual ~logfile_activity();
+
+private:
+};
+#endif
diff --git a/src/logfile_sub_source.cc b/src/logfile_sub_source.cc
new file mode 100644
index 0000000..aa356f9
--- /dev/null
+++ b/src/logfile_sub_source.cc
@@ -0,0 +1,2467 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+#include <future>
+
+#include "logfile_sub_source.hh"
+
+#include <sqlite3.h>
+
+#include "base/ansi_scrubber.hh"
+#include "base/humanize.time.hh"
+#include "base/injector.hh"
+#include "base/itertools.hh"
+#include "base/string_util.hh"
+#include "bound_tags.hh"
+#include "command_executor.hh"
+#include "config.h"
+#include "k_merge_tree.h"
+#include "log_accel.hh"
+#include "logfile_sub_source.cfg.hh"
+#include "md2attr_line.hh"
+#include "readline_highlighters.hh"
+#include "relative_time.hh"
+#include "sql_util.hh"
+#include "vtab_module.hh"
+#include "yajlpp/yajlpp.hh"
+
+const bookmark_type_t logfile_sub_source::BM_ERRORS("error");
+const bookmark_type_t logfile_sub_source::BM_WARNINGS("warning");
+const bookmark_type_t logfile_sub_source::BM_FILES("file");
+
+static int
+pretty_sql_callback(exec_context& ec, sqlite3_stmt* stmt)
+{
+ if (!sqlite3_stmt_busy(stmt)) {
+ return 0;
+ }
+
+ int ncols = sqlite3_column_count(stmt);
+
+ for (int lpc = 0; lpc < ncols; lpc++) {
+ if (!ec.ec_accumulator->empty()) {
+ ec.ec_accumulator->append(", ");
+ }
+
+ const char* res = (const char*) sqlite3_column_text(stmt, lpc);
+ if (res == nullptr) {
+ continue;
+ }
+
+ ec.ec_accumulator->append(res);
+ }
+
+ return 0;
+}
+
+static std::future<std::string>
+pretty_pipe_callback(exec_context& ec, const std::string& cmdline, auto_fd& fd)
+{
+ auto retval = std::async(std::launch::async, [&]() {
+ char buffer[1024];
+ std::ostringstream ss;
+ ssize_t rc;
+
+ while ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
+ ss.write(buffer, rc);
+ }
+
+ auto retval = ss.str();
+
+ if (endswith(retval, "\n")) {
+ retval.resize(retval.length() - 1);
+ }
+
+ return retval;
+ });
+
+ return retval;
+}
+
+logfile_sub_source::logfile_sub_source()
+ : text_sub_source(1), lss_meta_grepper(*this), lss_location_history(*this)
+{
+ this->tss_supports_filtering = true;
+ this->clear_line_size_cache();
+ this->clear_min_max_log_times();
+}
+
+std::shared_ptr<logfile>
+logfile_sub_source::find(const char* fn, content_line_t& line_base)
+{
+ iterator iter;
+ std::shared_ptr<logfile> retval = nullptr;
+
+ line_base = content_line_t(0);
+ for (iter = this->lss_files.begin();
+ iter != this->lss_files.end() && retval == nullptr;
+ iter++)
+ {
+ auto& ld = *(*iter);
+ auto* lf = ld.get_file_ptr();
+
+ if (lf == nullptr) {
+ continue;
+ }
+ if (strcmp(lf->get_filename().c_str(), fn) == 0) {
+ retval = ld.get_file();
+ } else {
+ line_base += content_line_t(MAX_LINES_PER_FILE);
+ }
+ }
+
+ return retval;
+}
+
+nonstd::optional<vis_line_t>
+logfile_sub_source::find_from_time(const struct timeval& start) const
+{
+ auto lb = lower_bound(this->lss_filtered_index.begin(),
+ this->lss_filtered_index.end(),
+ start,
+ filtered_logline_cmp(*this));
+ if (lb != this->lss_filtered_index.end()) {
+ return vis_line_t(lb - this->lss_filtered_index.begin());
+ }
+
+ return nonstd::nullopt;
+}
+
+void
+logfile_sub_source::text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& value_out,
+ line_flags_t flags)
+{
+ content_line_t line(0);
+
+ require_ge(row, 0);
+ require_lt((size_t) row, this->lss_filtered_index.size());
+
+ line = this->at(vis_line_t(row));
+
+ if (flags & RF_RAW) {
+ auto lf = this->find(line);
+ value_out = lf->read_line(lf->begin() + line)
+ .map([](auto sbr) { return to_string(sbr); })
+ .unwrapOr({});
+ return;
+ }
+
+ require_false(this->lss_in_value_for_line);
+
+ this->lss_in_value_for_line = true;
+ this->lss_token_flags = flags;
+ this->lss_token_file_data = this->find_data(line);
+ this->lss_token_file = (*this->lss_token_file_data)->get_file();
+ this->lss_token_line = this->lss_token_file->begin() + line;
+
+ this->lss_token_attrs.clear();
+ this->lss_token_values.clear();
+ this->lss_share_manager.invalidate_refs();
+ if (flags & text_sub_source::RF_FULL) {
+ shared_buffer_ref sbr;
+
+ this->lss_token_file->read_full_message(this->lss_token_line, sbr);
+ this->lss_token_value = to_string(sbr);
+ if (sbr.get_metadata().m_has_ansi) {
+ scrub_ansi_string(this->lss_token_value, &this->lss_token_attrs);
+ sbr.get_metadata().m_has_ansi = false;
+ }
+ } else {
+ this->lss_token_value
+ = this->lss_token_file->read_line(this->lss_token_line)
+ .map([](auto sbr) { return to_string(sbr); })
+ .unwrapOr({});
+ if (this->lss_token_line->has_ansi()) {
+ scrub_ansi_string(this->lss_token_value, &this->lss_token_attrs);
+ }
+ }
+ this->lss_token_shift_start = 0;
+ this->lss_token_shift_size = 0;
+
+ auto format = this->lss_token_file->get_format();
+
+ value_out = this->lss_token_value;
+ if (this->lss_flags & F_SCRUB) {
+ format->scrub(value_out);
+ }
+
+ auto& sbr = this->lss_token_values.lvv_sbr;
+
+ sbr.share(this->lss_share_manager,
+ (char*) this->lss_token_value.c_str(),
+ this->lss_token_value.size());
+ format->annotate(line, this->lss_token_attrs, this->lss_token_values);
+ if (flags & RF_REWRITE) {
+ exec_context ec(
+ &this->lss_token_values, pretty_sql_callback, pretty_pipe_callback);
+ std::string rewritten_line;
+
+ ec.with_perms(exec_context::perm_t::READ_ONLY);
+ ec.ec_local_vars.push(std::map<std::string, scoped_value_t>());
+ ec.ec_top_line = vis_line_t(row);
+ add_ansi_vars(ec.ec_global_vars);
+ add_global_vars(ec);
+ format->rewrite(ec, sbr, this->lss_token_attrs, rewritten_line);
+ this->lss_token_value.assign(rewritten_line);
+ value_out = this->lss_token_value;
+ }
+
+ if ((this->lss_token_file->is_time_adjusted()
+ || format->lf_timestamp_flags & ETF_MACHINE_ORIENTED
+ || !(format->lf_timestamp_flags & ETF_DAY_SET)
+ || !(format->lf_timestamp_flags & ETF_MONTH_SET))
+ && format->lf_date_time.dts_fmt_lock != -1)
+ {
+ auto time_attr
+ = find_string_attr(this->lss_token_attrs, &logline::L_TIMESTAMP);
+ if (time_attr != this->lss_token_attrs.end()) {
+ const struct line_range time_range = time_attr->sa_range;
+ struct timeval adjusted_time;
+ struct exttm adjusted_tm;
+ char buffer[128];
+ const char* fmt;
+ ssize_t len;
+
+ if (format->lf_timestamp_flags & ETF_MACHINE_ORIENTED
+ || !(format->lf_timestamp_flags & ETF_DAY_SET)
+ || !(format->lf_timestamp_flags & ETF_MONTH_SET))
+ {
+ adjusted_time = this->lss_token_line->get_timeval();
+ fmt = "%Y-%m-%d %H:%M:%S.%f";
+ if (format->lf_timestamp_flags & ETF_MICROS_SET) {
+ struct timeval actual_tv;
+ struct exttm tm;
+ if (format->lf_date_time.scan(
+ this->lss_token_value.data() + time_range.lr_start,
+ time_range.length(),
+ format->get_timestamp_formats(),
+ &tm,
+ actual_tv,
+ false))
+ {
+ adjusted_time.tv_usec = actual_tv.tv_usec;
+ }
+ }
+ gmtime_r(&adjusted_time.tv_sec, &adjusted_tm.et_tm);
+ adjusted_tm.et_nsec
+ = std::chrono::duration_cast<std::chrono::nanoseconds>(
+ std::chrono::microseconds{adjusted_time.tv_usec})
+ .count();
+ len = ftime_fmt(buffer, sizeof(buffer), fmt, adjusted_tm);
+ } else {
+ adjusted_time = this->lss_token_line->get_timeval();
+ gmtime_r(&adjusted_time.tv_sec, &adjusted_tm.et_tm);
+ adjusted_tm.et_nsec
+ = std::chrono::duration_cast<std::chrono::nanoseconds>(
+ std::chrono::microseconds{adjusted_time.tv_usec})
+ .count();
+ len = format->lf_date_time.ftime(
+ buffer,
+ sizeof(buffer),
+ format->get_timestamp_formats(),
+ adjusted_tm);
+ }
+
+ value_out.replace(
+ time_range.lr_start, time_range.length(), buffer, len);
+ this->lss_token_shift_start = time_range.lr_start;
+ this->lss_token_shift_size = len - time_range.length();
+ }
+ }
+
+ if (this->lss_flags & F_FILENAME || this->lss_flags & F_BASENAME) {
+ size_t file_offset_end;
+ std::string name;
+ if (this->lss_flags & F_FILENAME) {
+ file_offset_end = this->lss_filename_width;
+ name = this->lss_token_file->get_filename();
+ if (file_offset_end < name.size()) {
+ file_offset_end = name.size();
+ this->lss_filename_width = name.size();
+ }
+ } else {
+ file_offset_end = this->lss_basename_width;
+ name = this->lss_token_file->get_unique_path();
+ if (file_offset_end < name.size()) {
+ file_offset_end = name.size();
+ this->lss_basename_width = name.size();
+ }
+ }
+ value_out.insert(0, 1, '|');
+ value_out.insert(0, file_offset_end - name.size(), ' ');
+ value_out.insert(0, name);
+ } else {
+ // Insert space for the file/search-hit markers.
+ value_out.insert(0, 1, ' ');
+ }
+
+ if (this->lss_flags & F_TIME_OFFSET) {
+ auto curr_tv = this->lss_token_line->get_timeval();
+ struct timeval diff_tv;
+ auto row_vl = vis_line_t(row);
+
+ auto prev_umark
+ = tc.get_bookmarks()[&textview_curses::BM_USER].prev(row_vl);
+ auto next_umark
+ = tc.get_bookmarks()[&textview_curses::BM_USER].next(row_vl);
+ auto prev_emark
+ = tc.get_bookmarks()[&textview_curses::BM_USER_EXPR].prev(row_vl);
+ auto next_emark
+ = tc.get_bookmarks()[&textview_curses::BM_USER_EXPR].next(row_vl);
+ if (!prev_umark && !prev_emark && (next_umark || next_emark)) {
+ auto next_line = this->find_line(this->at(
+ std::max(next_umark.value_or(0), next_emark.value_or(0))));
+
+ diff_tv = curr_tv - next_line->get_timeval();
+ } else {
+ auto prev_row
+ = std::max(prev_umark.value_or(0), prev_emark.value_or(0));
+ auto first_line = this->find_line(this->at(prev_row));
+ auto start_tv = first_line->get_timeval();
+ diff_tv = curr_tv - start_tv;
+ }
+
+ auto relstr = humanize::time::duration::from_tv(diff_tv).to_string();
+ value_out = fmt::format(FMT_STRING("{: >12}|{}"), relstr, value_out);
+ }
+ this->lss_in_value_for_line = false;
+}
+
+void
+logfile_sub_source::text_attrs_for_line(textview_curses& lv,
+ int row,
+ string_attrs_t& value_out)
+{
+ view_colors& vc = view_colors::singleton();
+ logline* next_line = nullptr;
+ struct line_range lr;
+ int time_offset_end = 0;
+ text_attrs attrs;
+
+ value_out = this->lss_token_attrs;
+
+ if ((row + 1) < (int) this->lss_filtered_index.size()) {
+ next_line = this->find_line(this->at(vis_line_t(row + 1)));
+ }
+
+ if (next_line != nullptr
+ && (day_num(next_line->get_time())
+ > day_num(this->lss_token_line->get_time())))
+ {
+ attrs.ta_attrs |= A_UNDERLINE;
+ }
+
+ const auto& line_values = this->lss_token_values;
+
+ lr.lr_start = 0;
+ lr.lr_end = this->lss_token_value.length();
+ value_out.emplace_back(lr, SA_ORIGINAL_LINE.value());
+ value_out.emplace_back(
+ lr, SA_LEVEL.value(this->lss_token_line->get_msg_level()));
+
+ lr.lr_start = time_offset_end;
+ lr.lr_end = -1;
+
+ if (!attrs.empty()) {
+ value_out.emplace_back(lr, VC_STYLE.value(attrs));
+ }
+
+ if (this->lss_token_line->get_msg_level() == log_level_t::LEVEL_INVALID) {
+ for (auto& token_attr : this->lss_token_attrs) {
+ if (token_attr.sa_type != &SA_INVALID) {
+ continue;
+ }
+
+ value_out.emplace_back(token_attr.sa_range,
+ VC_ROLE.value(role_t::VCR_INVALID_MSG));
+ }
+ }
+
+ for (const auto& line_value : line_values.lvv_values) {
+ if ((!(this->lss_token_flags & RF_FULL)
+ && line_value.lv_sub_offset
+ != this->lss_token_line->get_sub_offset())
+ || !line_value.lv_origin.is_valid())
+ {
+ continue;
+ }
+
+ if (line_value.lv_meta.is_hidden()) {
+ value_out.emplace_back(line_value.lv_origin, SA_HIDDEN.value());
+ }
+
+ if (!line_value.lv_meta.lvm_identifier
+ || !line_value.lv_origin.is_valid())
+ {
+ continue;
+ }
+
+ value_out.emplace_back(line_value.lv_origin,
+ VC_ROLE.value(role_t::VCR_IDENTIFIER));
+ }
+
+ if (this->lss_token_shift_size) {
+ shift_string_attrs(value_out,
+ this->lss_token_shift_start + 1,
+ this->lss_token_shift_size);
+ }
+
+ shift_string_attrs(value_out, 0, 1);
+
+ lr.lr_start = 0;
+ lr.lr_end = 1;
+ {
+ auto& bm = lv.get_bookmarks();
+ const auto& bv = bm[&BM_FILES];
+ bool is_first_for_file
+ = binary_search(bv.begin(), bv.end(), vis_line_t(row));
+ bool is_last_for_file
+ = binary_search(bv.begin(), bv.end(), vis_line_t(row + 1));
+ chtype graph = ACS_VLINE;
+ if (is_first_for_file) {
+ if (is_last_for_file) {
+ graph = ACS_HLINE;
+ } else {
+ graph = ACS_ULCORNER;
+ }
+ } else if (is_last_for_file) {
+ graph = ACS_LLCORNER;
+ }
+ value_out.emplace_back(lr, VC_GRAPHIC.value(graph));
+
+ if (!(this->lss_token_flags & RF_FULL)) {
+ bookmark_vector<vis_line_t>& bv_search
+ = bm[&textview_curses::BM_SEARCH];
+
+ if (binary_search(std::begin(bv_search),
+ std::end(bv_search),
+ vis_line_t(row)))
+ {
+ lr.lr_start = 0;
+ lr.lr_end = 1;
+ value_out.emplace_back(lr,
+ VC_STYLE.value(text_attrs{A_REVERSE}));
+ }
+ }
+ }
+
+ value_out.emplace_back(lr,
+ VC_STYLE.value(vc.attrs_for_ident(
+ this->lss_token_file->get_filename())));
+
+ if (this->lss_flags & F_FILENAME || this->lss_flags & F_BASENAME) {
+ size_t file_offset_end = (this->lss_flags & F_FILENAME)
+ ? this->lss_filename_width
+ : this->lss_basename_width;
+
+ shift_string_attrs(value_out, 0, file_offset_end);
+
+ lr.lr_start = 0;
+ lr.lr_end = file_offset_end + 1;
+ value_out.emplace_back(lr,
+ VC_STYLE.value(vc.attrs_for_ident(
+ this->lss_token_file->get_filename())));
+ }
+
+ if (this->lss_flags & F_TIME_OFFSET) {
+ time_offset_end = 13;
+ lr.lr_start = 0;
+ lr.lr_end = time_offset_end;
+
+ shift_string_attrs(value_out, 0, time_offset_end);
+
+ value_out.emplace_back(lr, VC_ROLE.value(role_t::VCR_OFFSET_TIME));
+ value_out.emplace_back(line_range(12, 13), VC_GRAPHIC.value(ACS_VLINE));
+
+ role_t bar_role = role_t::VCR_NONE;
+
+ switch (this->get_line_accel_direction(vis_line_t(row))) {
+ case log_accel::A_STEADY:
+ break;
+ case log_accel::A_DECEL:
+ bar_role = role_t::VCR_DIFF_DELETE;
+ break;
+ case log_accel::A_ACCEL:
+ bar_role = role_t::VCR_DIFF_ADD;
+ break;
+ }
+ if (bar_role != role_t::VCR_NONE) {
+ value_out.emplace_back(line_range(12, 13), VC_ROLE.value(bar_role));
+ }
+ }
+
+ lr.lr_start = 0;
+ lr.lr_end = -1;
+ value_out.emplace_back(lr, logline::L_FILE.value(this->lss_token_file));
+ value_out.emplace_back(
+ lr, SA_FORMAT.value(this->lss_token_file->get_format()->get_name()));
+
+ {
+ const auto& bv = lv.get_bookmarks()[&textview_curses::BM_META];
+ bookmark_vector<vis_line_t>::const_iterator bv_iter;
+
+ bv_iter = lower_bound(bv.begin(), bv.end(), vis_line_t(row + 1));
+ if (bv_iter != bv.begin()) {
+ --bv_iter;
+ auto line_meta_opt = this->find_bookmark_metadata(*bv_iter);
+
+ if (line_meta_opt && !line_meta_opt.value()->bm_name.empty()) {
+ lr.lr_start = 0;
+ lr.lr_end = -1;
+ value_out.emplace_back(
+ lr, logline::L_PARTITION.value(line_meta_opt.value()));
+ }
+ }
+
+ auto line_meta_opt = this->find_bookmark_metadata(vis_line_t(row));
+
+ if (line_meta_opt) {
+ lr.lr_start = 0;
+ lr.lr_end = -1;
+ value_out.emplace_back(
+ lr, logline::L_META.value(line_meta_opt.value()));
+ }
+ }
+
+ if (this->lss_token_file->is_time_adjusted()) {
+ struct line_range time_range
+ = find_string_attr_range(value_out, &logline::L_TIMESTAMP);
+
+ if (time_range.lr_end != -1) {
+ value_out.emplace_back(time_range,
+ VC_ROLE.value(role_t::VCR_ADJUSTED_TIME));
+ }
+ }
+
+ if (this->lss_token_line->is_time_skewed()) {
+ struct line_range time_range
+ = find_string_attr_range(value_out, &logline::L_TIMESTAMP);
+
+ if (time_range.lr_end != -1) {
+ value_out.emplace_back(time_range,
+ VC_ROLE.value(role_t::VCR_SKEWED_TIME));
+ }
+ }
+
+ if (!this->lss_token_line->is_continued()) {
+ if (this->lss_preview_filter_stmt != nullptr) {
+ int color;
+ auto eval_res
+ = this->eval_sql_filter(this->lss_preview_filter_stmt.in(),
+ this->lss_token_file_data,
+ this->lss_token_line);
+ if (eval_res.isErr()) {
+ color = COLOR_YELLOW;
+ value_out.emplace_back(
+ line_range{0, -1},
+ SA_ERROR.value(
+ eval_res.unwrapErr().to_attr_line().get_string()));
+ } else {
+ auto matched = eval_res.unwrap();
+
+ if (matched) {
+ color = COLOR_GREEN;
+ } else {
+ color = COLOR_RED;
+ value_out.emplace_back(line_range{0, 1},
+ VC_STYLE.value(text_attrs{A_BLINK}));
+ }
+ }
+ value_out.emplace_back(line_range{0, 1},
+ VC_BACKGROUND.value(color));
+ }
+
+ auto sql_filter_opt = this->get_sql_filter();
+ if (sql_filter_opt) {
+ auto* sf = (sql_filter*) sql_filter_opt.value().get();
+ auto eval_res = this->eval_sql_filter(sf->sf_filter_stmt.in(),
+ this->lss_token_file_data,
+ this->lss_token_line);
+ if (eval_res.isErr()) {
+ auto msg = fmt::format(
+ FMT_STRING(
+ "filter expression evaluation failed with -- {}"),
+ eval_res.unwrapErr().to_attr_line().get_string());
+ auto color = COLOR_YELLOW;
+ value_out.emplace_back(line_range{0, -1}, SA_ERROR.value(msg));
+ value_out.emplace_back(line_range{0, 1},
+ VC_BACKGROUND.value(color));
+ }
+ }
+ }
+}
+
+logfile_sub_source::rebuild_result
+logfile_sub_source::rebuild_index(
+ nonstd::optional<ui_clock::time_point> deadline)
+{
+ if (this->tss_view == nullptr) {
+ return rebuild_result::rr_no_change;
+ }
+
+ iterator iter;
+ size_t total_lines = 0;
+ bool full_sort = false;
+ int file_count = 0;
+ bool force = this->lss_force_rebuild;
+ auto retval = rebuild_result::rr_no_change;
+ nonstd::optional<struct timeval> lowest_tv = nonstd::nullopt;
+ vis_line_t search_start = 0_vl;
+
+ this->lss_force_rebuild = false;
+ if (force) {
+ log_debug("forced to full rebuild");
+ retval = rebuild_result::rr_full_rebuild;
+ }
+
+ std::vector<size_t> file_order(this->lss_files.size());
+
+ for (size_t lpc = 0; lpc < file_order.size(); lpc++) {
+ file_order[lpc] = lpc;
+ }
+ if (!this->lss_index.empty()) {
+ std::stable_sort(file_order.begin(),
+ file_order.end(),
+ [this](const auto& left, const auto& right) {
+ const auto& left_ld = this->lss_files[left];
+ const auto& right_ld = this->lss_files[right];
+
+ if (left_ld->get_file_ptr() == nullptr) {
+ return true;
+ }
+ if (right_ld->get_file_ptr() == nullptr) {
+ return false;
+ }
+
+ return left_ld->get_file_ptr()->back()
+ < right_ld->get_file_ptr()->back();
+ });
+ }
+
+ bool time_left = true;
+ for (const auto file_index : file_order) {
+ auto& ld = *(this->lss_files[file_index]);
+ auto* lf = ld.get_file_ptr();
+
+ if (lf == nullptr) {
+ if (ld.ld_lines_indexed > 0) {
+ log_debug("%d: file closed, doing full rebuild",
+ ld.ld_file_index);
+ force = true;
+ retval = rebuild_result::rr_full_rebuild;
+ }
+ } else {
+ if (time_left && deadline && ui_clock::now() > deadline.value()) {
+ log_debug("no time left, skipping %s",
+ lf->get_filename().c_str());
+ time_left = false;
+ }
+
+ if (!this->tss_view->is_paused() && time_left) {
+ switch (lf->rebuild_index(deadline)) {
+ case logfile::rebuild_result_t::NO_NEW_LINES:
+ // No changes
+ break;
+ case logfile::rebuild_result_t::NEW_LINES:
+ if (retval == rebuild_result::rr_no_change) {
+ retval = rebuild_result::rr_appended_lines;
+ }
+ log_debug("new lines for %s:%d",
+ lf->get_filename().c_str(),
+ lf->size());
+ if (!this->lss_index.empty()
+ && lf->size() > ld.ld_lines_indexed)
+ {
+ logline& new_file_line = (*lf)[ld.ld_lines_indexed];
+ content_line_t cl = this->lss_index.back();
+ logline* last_indexed_line = this->find_line(cl);
+
+ // If there are new lines that are older than what
+ // we have in the index, we need to resort.
+ if (last_indexed_line == nullptr
+ || new_file_line
+ < last_indexed_line->get_timeval())
+ {
+ log_debug(
+ "%s:%ld: found older lines, full "
+ "rebuild: %p %lld < %lld",
+ lf->get_filename().c_str(),
+ ld.ld_lines_indexed,
+ last_indexed_line,
+ new_file_line.get_time_in_millis(),
+ last_indexed_line == nullptr
+ ? (uint64_t) -1
+ : last_indexed_line
+ ->get_time_in_millis());
+ if (retval
+ <= rebuild_result::rr_partial_rebuild)
+ {
+ retval = rebuild_result::rr_partial_rebuild;
+ if (!lowest_tv) {
+ lowest_tv = new_file_line.get_timeval();
+ } else if (new_file_line.get_timeval()
+ < lowest_tv.value())
+ {
+ lowest_tv = new_file_line.get_timeval();
+ }
+ }
+ }
+ }
+ break;
+ case logfile::rebuild_result_t::INVALID:
+ case logfile::rebuild_result_t::NEW_ORDER:
+ log_debug("%s: log file has a new order, full rebuild",
+ lf->get_filename().c_str());
+ retval = rebuild_result::rr_full_rebuild;
+ force = true;
+ full_sort = true;
+ break;
+ }
+ }
+ file_count += 1;
+ total_lines += lf->size();
+ }
+ }
+
+ if (this->lss_index.empty() && !time_left) {
+ return rebuild_result::rr_appended_lines;
+ }
+
+ if (this->lss_index.reserve(total_lines)) {
+ force = true;
+ retval = rebuild_result::rr_full_rebuild;
+ }
+
+ auto& vis_bm = this->tss_view->get_bookmarks();
+
+ if (force) {
+ for (iter = this->lss_files.begin(); iter != this->lss_files.end();
+ iter++)
+ {
+ (*iter)->ld_lines_indexed = 0;
+ }
+
+ this->lss_index.clear();
+ this->lss_filtered_index.clear();
+ this->lss_longest_line = 0;
+ this->lss_basename_width = 0;
+ this->lss_filename_width = 0;
+ vis_bm[&textview_curses::BM_USER_EXPR].clear();
+ } else if (retval == rebuild_result::rr_partial_rebuild) {
+ size_t remaining = 0;
+
+ log_debug("partial rebuild with lowest time: %ld",
+ lowest_tv.value().tv_sec);
+ for (iter = this->lss_files.begin(); iter != this->lss_files.end();
+ iter++)
+ {
+ logfile_data& ld = *(*iter);
+ auto* lf = ld.get_file_ptr();
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ auto line_iter = lf->find_from_time(lowest_tv.value());
+
+ if (line_iter) {
+ log_debug("%s: lowest line time %ld; line %ld; size %ld",
+ lf->get_filename().c_str(),
+ line_iter.value()->get_timeval().tv_sec,
+ std::distance(lf->cbegin(), line_iter.value()),
+ lf->size());
+ }
+ ld.ld_lines_indexed
+ = std::distance(lf->cbegin(), line_iter.value_or(lf->cend()));
+ remaining += lf->size() - ld.ld_lines_indexed;
+ }
+
+ auto row_iter = std::lower_bound(this->lss_index.begin(),
+ this->lss_index.end(),
+ *lowest_tv,
+ logline_cmp(*this));
+ this->lss_index.shrink_to(
+ std::distance(this->lss_index.begin(), row_iter));
+ log_debug("new index size %ld/%ld; remain %ld",
+ this->lss_index.ba_size,
+ this->lss_index.ba_capacity,
+ remaining);
+ auto filt_row_iter = lower_bound(this->lss_filtered_index.begin(),
+ this->lss_filtered_index.end(),
+ *lowest_tv,
+ filtered_logline_cmp(*this));
+ this->lss_filtered_index.resize(
+ std::distance(this->lss_filtered_index.begin(), filt_row_iter));
+ search_start = vis_line_t(this->lss_filtered_index.size());
+
+ auto bm_range = vis_bm[&textview_curses::BM_USER_EXPR].equal_range(
+ search_start, -1_vl);
+ auto bm_new_size = std::distance(
+ vis_bm[&textview_curses::BM_USER_EXPR].begin(), bm_range.first);
+ vis_bm[&textview_curses::BM_USER_EXPR].resize(bm_new_size);
+
+ if (this->lss_index_delegate) {
+ this->lss_index_delegate->index_start(*this);
+ for (const auto row_in_full_index : this->lss_filtered_index) {
+ auto cl = this->lss_index[row_in_full_index];
+ uint64_t line_number;
+ auto ld_iter = this->find_data(cl, line_number);
+ auto& ld = *ld_iter;
+ auto line_iter = ld->get_file_ptr()->begin() + line_number;
+
+ this->lss_index_delegate->index_line(
+ *this, ld->get_file_ptr(), line_iter);
+ }
+ }
+ }
+
+ if (retval != rebuild_result::rr_no_change || force) {
+ size_t index_size = 0, start_size = this->lss_index.size();
+ logline_cmp line_cmper(*this);
+
+ for (auto& ld : this->lss_files) {
+ auto* lf = ld->get_file_ptr();
+
+ if (lf == nullptr) {
+ continue;
+ }
+ this->lss_longest_line = std::max(this->lss_longest_line,
+ lf->get_longest_line_length());
+ this->lss_basename_width = std::max(this->lss_basename_width,
+ lf->get_unique_path().size());
+ this->lss_filename_width
+ = std::max(this->lss_filename_width, lf->get_filename().size());
+ }
+
+ if (full_sort) {
+ for (auto& ld : this->lss_files) {
+ auto* lf = ld->get_file_ptr();
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ for (size_t line_index = 0; line_index < lf->size();
+ line_index++)
+ {
+ if ((*lf)[line_index].is_ignored()) {
+ continue;
+ }
+
+ content_line_t con_line(
+ ld->ld_file_index * MAX_LINES_PER_FILE + line_index);
+
+ this->lss_index.push_back(con_line);
+ }
+ }
+
+ // XXX get rid of this full sort on the initial run, it's not
+ // needed unless the file is not in time-order
+ if (this->lss_sorting_observer) {
+ this->lss_sorting_observer(*this, 0, this->lss_index.size());
+ }
+ std::sort(
+ this->lss_index.begin(), this->lss_index.end(), line_cmper);
+ if (this->lss_sorting_observer) {
+ this->lss_sorting_observer(
+ *this, this->lss_index.size(), this->lss_index.size());
+ }
+ } else {
+ kmerge_tree_c<logline, logfile_data, logfile::iterator> merge(
+ file_count);
+
+ for (iter = this->lss_files.begin(); iter != this->lss_files.end();
+ iter++)
+ {
+ auto* ld = iter->get();
+ auto* lf = ld->get_file_ptr();
+ if (lf == nullptr) {
+ continue;
+ }
+
+ merge.add(ld, lf->begin() + ld->ld_lines_indexed, lf->end());
+ index_size += lf->size();
+ }
+
+ file_off_t index_off = 0;
+ merge.execute();
+ if (this->lss_sorting_observer) {
+ this->lss_sorting_observer(*this, index_off, index_size);
+ }
+ for (;;) {
+ logfile::iterator lf_iter;
+ logfile_data* ld;
+
+ if (!merge.get_top(ld, lf_iter)) {
+ break;
+ }
+
+ if (!lf_iter->is_ignored()) {
+ int file_index = ld->ld_file_index;
+ int line_index = lf_iter - ld->get_file_ptr()->begin();
+
+ content_line_t con_line(file_index * MAX_LINES_PER_FILE
+ + line_index);
+
+ if (lf_iter->is_marked()) {
+ auto start_iter = lf_iter;
+ while (start_iter->is_continued()) {
+ --start_iter;
+ }
+ int start_index
+ = start_iter - ld->get_file_ptr()->begin();
+ content_line_t start_con_line(
+ file_index * MAX_LINES_PER_FILE + start_index);
+
+ this->lss_user_marks[&textview_curses::BM_META]
+ .insert_once(start_con_line);
+ lf_iter->set_mark(false);
+ }
+ this->lss_index.push_back(con_line);
+ }
+
+ merge.next();
+ index_off += 1;
+ if (index_off % 10000 == 0 && this->lss_sorting_observer) {
+ this->lss_sorting_observer(*this, index_off, index_size);
+ }
+ }
+ if (this->lss_sorting_observer) {
+ this->lss_sorting_observer(*this, index_size, index_size);
+ }
+ }
+
+ for (iter = this->lss_files.begin(); iter != this->lss_files.end();
+ iter++)
+ {
+ auto* lf = (*iter)->get_file_ptr();
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ (*iter)->ld_lines_indexed = lf->size();
+ }
+
+ this->lss_filtered_index.reserve(this->lss_index.size());
+
+ uint32_t filter_in_mask, filter_out_mask;
+ this->get_filters().get_enabled_mask(filter_in_mask, filter_out_mask);
+
+ if (start_size == 0 && this->lss_index_delegate != nullptr) {
+ this->lss_index_delegate->index_start(*this);
+ }
+
+ for (size_t index_index = start_size;
+ index_index < this->lss_index.size();
+ index_index++)
+ {
+ content_line_t cl = (content_line_t) this->lss_index[index_index];
+ uint64_t line_number;
+ auto ld = this->find_data(cl, line_number);
+
+ if (!(*ld)->is_visible()) {
+ continue;
+ }
+
+ auto* lf = (*ld)->get_file_ptr();
+ auto line_iter = lf->begin() + line_number;
+
+ if (line_iter->is_ignored()) {
+ continue;
+ }
+
+ if (!this->tss_apply_filters
+ || (!(*ld)->ld_filter_state.excluded(
+ filter_in_mask, filter_out_mask, line_number)
+ && this->check_extra_filters(ld, line_iter)))
+ {
+ auto eval_res = this->eval_sql_filter(
+ this->lss_marker_stmt.in(), ld, line_iter);
+ if (eval_res.isErr()) {
+ line_iter->set_expr_mark(false);
+ } else {
+ auto matched = eval_res.unwrap();
+
+ if (matched) {
+ line_iter->set_expr_mark(true);
+ vis_bm[&textview_curses::BM_USER_EXPR].insert_once(
+ vis_line_t(this->lss_filtered_index.size()));
+ } else {
+ line_iter->set_expr_mark(false);
+ }
+ }
+ this->lss_filtered_index.push_back(index_index);
+ if (this->lss_index_delegate != nullptr) {
+ this->lss_index_delegate->index_line(
+ *this, lf, lf->begin() + line_number);
+ }
+ }
+ }
+
+ if (this->lss_index_delegate != nullptr) {
+ this->lss_index_delegate->index_complete(*this);
+ }
+ }
+
+ switch (retval) {
+ case rebuild_result::rr_no_change:
+ break;
+ case rebuild_result::rr_full_rebuild:
+ log_debug("redoing search");
+ this->lss_index_generation += 1;
+ this->tss_view->reload_data();
+ this->tss_view->redo_search();
+ break;
+ case rebuild_result::rr_partial_rebuild:
+ log_debug("redoing search from: %d", (int) search_start);
+ this->lss_index_generation += 1;
+ this->tss_view->reload_data();
+ this->tss_view->search_new_data(search_start);
+ break;
+ case rebuild_result::rr_appended_lines:
+ this->tss_view->reload_data();
+ this->tss_view->search_new_data();
+ break;
+ }
+
+ return retval;
+}
+
+void
+logfile_sub_source::text_update_marks(vis_bookmarks& bm)
+{
+ std::shared_ptr<logfile> last_file;
+ vis_line_t vl;
+
+ bm[&BM_WARNINGS].clear();
+ bm[&BM_ERRORS].clear();
+ bm[&BM_FILES].clear();
+
+ for (auto& lss_user_mark : this->lss_user_marks) {
+ bm[lss_user_mark.first].clear();
+ }
+
+ for (; vl < (int) this->lss_filtered_index.size(); ++vl) {
+ const content_line_t orig_cl = this->at(vl);
+ content_line_t cl = orig_cl;
+ auto lf = this->find(cl);
+
+ for (auto& lss_user_mark : this->lss_user_marks) {
+ if (binary_search(lss_user_mark.second.begin(),
+ lss_user_mark.second.end(),
+ orig_cl))
+ {
+ bm[lss_user_mark.first].insert_once(vl);
+
+ if (lss_user_mark.first == &textview_curses::BM_USER) {
+ auto ll = lf->begin() + cl;
+
+ ll->set_mark(true);
+ }
+ }
+ }
+
+ if (lf != last_file) {
+ bm[&BM_FILES].insert_once(vl);
+ }
+
+ auto line_iter = lf->begin() + cl;
+ if (line_iter->is_message()) {
+ switch (line_iter->get_msg_level()) {
+ case LEVEL_WARNING:
+ bm[&BM_WARNINGS].insert_once(vl);
+ break;
+
+ case LEVEL_FATAL:
+ case LEVEL_ERROR:
+ case LEVEL_CRITICAL:
+ bm[&BM_ERRORS].insert_once(vl);
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ last_file = lf;
+ }
+}
+
+log_accel::direction_t
+logfile_sub_source::get_line_accel_direction(vis_line_t vl)
+{
+ log_accel la;
+
+ while (vl >= 0) {
+ logline* curr_line = this->find_line(this->at(vl));
+
+ if (!curr_line->is_message()) {
+ --vl;
+ continue;
+ }
+
+ if (!la.add_point(curr_line->get_time_in_millis())) {
+ break;
+ }
+
+ --vl;
+ }
+
+ return la.get_direction();
+}
+
+void
+logfile_sub_source::text_filters_changed()
+{
+ this->lss_index_generation += 1;
+
+ if (this->lss_line_meta_changed) {
+ this->invalidate_sql_filter();
+ this->lss_line_meta_changed = false;
+ }
+
+ for (auto& ld : *this) {
+ auto* lf = ld->get_file_ptr();
+
+ if (lf != nullptr) {
+ ld->ld_filter_state.clear_deleted_filter_state();
+ lf->reobserve_from(lf->begin()
+ + ld->ld_filter_state.get_min_count(lf->size()));
+ }
+ }
+
+ auto& vis_bm = this->tss_view->get_bookmarks();
+ uint32_t filtered_in_mask, filtered_out_mask;
+
+ this->get_filters().get_enabled_mask(filtered_in_mask, filtered_out_mask);
+
+ if (this->lss_index_delegate != nullptr) {
+ this->lss_index_delegate->index_start(*this);
+ }
+ vis_bm[&textview_curses::BM_USER_EXPR].clear();
+
+ this->lss_filtered_index.clear();
+ for (size_t index_index = 0; index_index < this->lss_index.size();
+ index_index++)
+ {
+ content_line_t cl = (content_line_t) this->lss_index[index_index];
+ uint64_t line_number;
+ auto ld = this->find_data(cl, line_number);
+
+ if (!(*ld)->is_visible()) {
+ continue;
+ }
+
+ auto lf = (*ld)->get_file_ptr();
+ auto line_iter = lf->begin() + line_number;
+
+ if (!this->tss_apply_filters
+ || (!(*ld)->ld_filter_state.excluded(
+ filtered_in_mask, filtered_out_mask, line_number)
+ && this->check_extra_filters(ld, line_iter)))
+ {
+ auto eval_res = this->eval_sql_filter(
+ this->lss_marker_stmt.in(), ld, line_iter);
+ if (eval_res.isErr()) {
+ line_iter->set_expr_mark(false);
+ } else {
+ auto matched = eval_res.unwrap();
+
+ if (matched) {
+ line_iter->set_expr_mark(true);
+ vis_bm[&textview_curses::BM_USER_EXPR].insert_once(
+ vis_line_t(this->lss_filtered_index.size()));
+ } else {
+ line_iter->set_expr_mark(false);
+ }
+ }
+ this->lss_filtered_index.push_back(index_index);
+ if (this->lss_index_delegate != nullptr) {
+ this->lss_index_delegate->index_line(*this, lf, line_iter);
+ }
+ }
+ }
+
+ if (this->lss_index_delegate != nullptr) {
+ this->lss_index_delegate->index_complete(*this);
+ }
+
+ if (this->tss_view != nullptr) {
+ this->tss_view->reload_data();
+ this->tss_view->redo_search();
+ }
+}
+
+bool
+logfile_sub_source::list_input_handle_key(listview_curses& lv, int ch)
+{
+ switch (ch) {
+ case 'h':
+ case 'H':
+ case KEY_SLEFT:
+ case KEY_LEFT:
+ if (lv.get_left() == 0) {
+ this->increase_line_context();
+ lv.set_needs_update();
+ return true;
+ }
+ break;
+ case 'l':
+ case 'L':
+ case KEY_SRIGHT:
+ case KEY_RIGHT:
+ if (this->decrease_line_context()) {
+ lv.set_needs_update();
+ return true;
+ }
+ break;
+ }
+ return false;
+}
+
+nonstd::optional<
+ std::pair<grep_proc_source<vis_line_t>*, grep_proc_sink<vis_line_t>*>>
+logfile_sub_source::get_grepper()
+{
+ return std::make_pair(
+ (grep_proc_source<vis_line_t>*) &this->lss_meta_grepper,
+ (grep_proc_sink<vis_line_t>*) &this->lss_meta_grepper);
+}
+
+bool
+logfile_sub_source::insert_file(const std::shared_ptr<logfile>& lf)
+{
+ iterator existing;
+
+ require_lt(lf->size(), MAX_LINES_PER_FILE);
+
+ existing = std::find_if(this->lss_files.begin(),
+ this->lss_files.end(),
+ logfile_data_eq(nullptr));
+ if (existing == this->lss_files.end()) {
+ if (this->lss_files.size() >= MAX_FILES) {
+ return false;
+ }
+
+ auto ld = std::make_unique<logfile_data>(
+ this->lss_files.size(), this->get_filters(), lf);
+ ld->set_visibility(lf->get_open_options().loo_is_visible);
+ this->lss_files.push_back(std::move(ld));
+ } else {
+ (*existing)->set_file(lf);
+ }
+ this->lss_force_rebuild = true;
+
+ return true;
+}
+
+Result<void, lnav::console::user_message>
+logfile_sub_source::set_sql_filter(std::string stmt_str, sqlite3_stmt* stmt)
+{
+ for (auto& filt : this->tss_filters) {
+ log_debug("set filt %p %d", filt.get(), filt->lf_deleted);
+ }
+ if (stmt != nullptr && !this->lss_filtered_index.empty()) {
+ auto top_cl = this->at(0_vl);
+ auto ld = this->find_data(top_cl);
+ auto eval_res
+ = this->eval_sql_filter(stmt, ld, (*ld)->get_file_ptr()->begin());
+
+ if (eval_res.isErr()) {
+ sqlite3_finalize(stmt);
+ return Err(eval_res.unwrapErr());
+ }
+ }
+
+ for (auto& ld : *this) {
+ ld->ld_filter_state.lfo_filter_state.clear_filter_state(0);
+ }
+
+ auto old_filter = this->get_sql_filter();
+ if (stmt != nullptr) {
+ auto new_filter
+ = std::make_shared<sql_filter>(*this, std::move(stmt_str), stmt);
+
+ log_debug("fstack %p new %p", &this->tss_filters, new_filter.get());
+ if (old_filter) {
+ auto existing_iter = std::find(this->tss_filters.begin(),
+ this->tss_filters.end(),
+ old_filter.value());
+ *existing_iter = new_filter;
+ } else {
+ this->tss_filters.add_filter(new_filter);
+ }
+ } else if (old_filter) {
+ this->tss_filters.delete_filter(old_filter.value()->get_id());
+ }
+
+ return Ok();
+}
+
+Result<void, lnav::console::user_message>
+logfile_sub_source::set_sql_marker(std::string stmt_str, sqlite3_stmt* stmt)
+{
+ if (stmt != nullptr && !this->lss_filtered_index.empty()) {
+ auto top_cl = this->at(0_vl);
+ auto ld = this->find_data(top_cl);
+ auto eval_res
+ = this->eval_sql_filter(stmt, ld, (*ld)->get_file_ptr()->begin());
+
+ if (eval_res.isErr()) {
+ sqlite3_finalize(stmt);
+ return Err(eval_res.unwrapErr());
+ }
+ }
+
+ this->lss_marker_stmt_text = std::move(stmt_str);
+ this->lss_marker_stmt = stmt;
+
+ if (this->tss_view == nullptr) {
+ return Ok();
+ }
+
+ auto& vis_bm = this->tss_view->get_bookmarks();
+ auto& expr_marks_bv = vis_bm[&textview_curses::BM_USER_EXPR];
+
+ expr_marks_bv.clear();
+ if (this->lss_index_delegate) {
+ this->lss_index_delegate->index_start(*this);
+ }
+ for (auto row = 0_vl; row < vis_line_t(this->lss_filtered_index.size());
+ row += 1_vl)
+ {
+ auto cl = this->at(row);
+ auto ld = this->find_data(cl);
+ auto ll = (*ld)->get_file()->begin() + cl;
+ auto eval_res
+ = this->eval_sql_filter(this->lss_marker_stmt.in(), ld, ll);
+
+ if (eval_res.isErr()) {
+ ll->set_expr_mark(false);
+ } else {
+ auto matched = eval_res.unwrap();
+
+ if (matched) {
+ ll->set_expr_mark(true);
+ expr_marks_bv.insert_once(row);
+ } else {
+ ll->set_expr_mark(false);
+ }
+ }
+ if (this->lss_index_delegate) {
+ this->lss_index_delegate->index_line(
+ *this, (*ld)->get_file_ptr(), ll);
+ }
+ }
+ if (this->lss_index_delegate) {
+ this->lss_index_delegate->index_complete(*this);
+ }
+
+ return Ok();
+}
+
+Result<void, lnav::console::user_message>
+logfile_sub_source::set_preview_sql_filter(sqlite3_stmt* stmt)
+{
+ if (stmt != nullptr && !this->lss_filtered_index.empty()) {
+ auto top_cl = this->at(0_vl);
+ auto ld = this->find_data(top_cl);
+ auto eval_res
+ = this->eval_sql_filter(stmt, ld, (*ld)->get_file_ptr()->begin());
+
+ if (eval_res.isErr()) {
+ sqlite3_finalize(stmt);
+ return Err(eval_res.unwrapErr());
+ }
+ }
+
+ this->lss_preview_filter_stmt = stmt;
+
+ return Ok();
+}
+
+Result<bool, lnav::console::user_message>
+logfile_sub_source::eval_sql_filter(sqlite3_stmt* stmt,
+ iterator ld,
+ logfile::const_iterator ll)
+{
+ if (stmt == nullptr) {
+ return Ok(false);
+ }
+
+ auto* lf = (*ld)->get_file_ptr();
+ char timestamp_buffer[64];
+ shared_buffer_ref raw_sbr;
+ logline_value_vector values;
+ auto& sbr = values.lvv_sbr;
+ lf->read_full_message(ll, sbr);
+ sbr.erase_ansi();
+ auto format = lf->get_format();
+ string_attrs_t sa;
+ auto line_number = std::distance(lf->cbegin(), ll);
+ format->annotate(line_number, sa, values);
+
+ sqlite3_reset(stmt);
+ sqlite3_clear_bindings(stmt);
+
+ auto count = sqlite3_bind_parameter_count(stmt);
+ for (int lpc = 0; lpc < count; lpc++) {
+ const auto* name = sqlite3_bind_parameter_name(stmt, lpc + 1);
+
+ if (name[0] == '$') {
+ const char* env_value;
+
+ if ((env_value = getenv(&name[1])) != nullptr) {
+ sqlite3_bind_text(stmt, lpc + 1, env_value, -1, SQLITE_STATIC);
+ }
+ continue;
+ }
+ if (strcmp(name, ":log_level") == 0) {
+ sqlite3_bind_text(
+ stmt, lpc + 1, ll->get_level_name(), -1, SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_time") == 0) {
+ auto len = sql_strftime(timestamp_buffer,
+ sizeof(timestamp_buffer),
+ ll->get_timeval(),
+ 'T');
+ sqlite3_bind_text(
+ stmt, lpc + 1, timestamp_buffer, len, SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_time_msecs") == 0) {
+ sqlite3_bind_int64(stmt, lpc + 1, ll->get_time_in_millis());
+ continue;
+ }
+ if (strcmp(name, ":log_mark") == 0) {
+ sqlite3_bind_int(stmt, lpc + 1, ll->is_marked());
+ continue;
+ }
+ if (strcmp(name, ":log_comment") == 0) {
+ const auto& bm = lf->get_bookmark_metadata();
+ auto line_number
+ = static_cast<uint32_t>(std::distance(lf->cbegin(), ll));
+ auto bm_iter = bm.find(line_number);
+ if (bm_iter != bm.end() && !bm_iter->second.bm_comment.empty()) {
+ const auto& meta = bm_iter->second;
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ meta.bm_comment.c_str(),
+ meta.bm_comment.length(),
+ SQLITE_STATIC);
+ }
+ continue;
+ }
+ if (strcmp(name, ":log_tags") == 0) {
+ const auto& bm = lf->get_bookmark_metadata();
+ auto line_number
+ = static_cast<uint32_t>(std::distance(lf->cbegin(), ll));
+ auto bm_iter = bm.find(line_number);
+ if (bm_iter != bm.end() && !bm_iter->second.bm_tags.empty()) {
+ const auto& meta = bm_iter->second;
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_array arr(gen);
+
+ for (const auto& str : meta.bm_tags) {
+ arr.gen(str);
+ }
+ }
+
+ string_fragment sf = gen.to_string_fragment();
+
+ sqlite3_bind_text(
+ stmt, lpc + 1, sf.data(), sf.length(), SQLITE_TRANSIENT);
+ }
+ continue;
+ }
+ if (strcmp(name, ":log_format") == 0) {
+ const auto format_name = format->get_name();
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ format_name.get(),
+ format_name.size(),
+ SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_format_regex") == 0) {
+ const auto pat_name = format->get_pattern_name(line_number);
+ sqlite3_bind_text(
+ stmt, lpc + 1, pat_name.get(), pat_name.size(), SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_path") == 0) {
+ const auto& filename = lf->get_filename();
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ filename.c_str(),
+ filename.length(),
+ SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_unique_path") == 0) {
+ const auto& filename = lf->get_unique_path();
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ filename.c_str(),
+ filename.length(),
+ SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_text") == 0) {
+ sqlite3_bind_text(
+ stmt, lpc + 1, sbr.get_data(), sbr.length(), SQLITE_STATIC);
+ continue;
+ }
+ if (strcmp(name, ":log_body") == 0) {
+ auto body_attr_opt = get_string_attr(sa, SA_BODY);
+ if (body_attr_opt) {
+ const auto& sar
+ = body_attr_opt.value().saw_string_attr->sa_range;
+
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ sbr.get_data_at(sar.lr_start),
+ sar.length(),
+ SQLITE_STATIC);
+ } else {
+ sqlite3_bind_null(stmt, lpc + 1);
+ }
+ continue;
+ }
+ if (strcmp(name, ":log_opid") == 0) {
+ auto opid_attr_opt = get_string_attr(sa, logline::L_OPID);
+ if (opid_attr_opt) {
+ const auto& sar
+ = opid_attr_opt.value().saw_string_attr->sa_range;
+
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ sbr.get_data_at(sar.lr_start),
+ sar.length(),
+ SQLITE_STATIC);
+ } else {
+ sqlite3_bind_null(stmt, lpc + 1);
+ }
+ continue;
+ }
+ if (strcmp(name, ":log_raw_text") == 0) {
+ auto res = lf->read_raw_message(ll);
+
+ if (res.isOk()) {
+ raw_sbr = res.unwrap();
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ raw_sbr.get_data(),
+ raw_sbr.length(),
+ SQLITE_STATIC);
+ }
+ continue;
+ }
+ for (const auto& lv : values.lvv_values) {
+ if (lv.lv_meta.lvm_name != &name[1]) {
+ continue;
+ }
+
+ switch (lv.lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_BOOLEAN:
+ sqlite3_bind_int64(stmt, lpc + 1, lv.lv_value.i);
+ break;
+ case value_kind_t::VALUE_FLOAT:
+ sqlite3_bind_double(stmt, lpc + 1, lv.lv_value.d);
+ break;
+ case value_kind_t::VALUE_INTEGER:
+ sqlite3_bind_int64(stmt, lpc + 1, lv.lv_value.i);
+ break;
+ case value_kind_t::VALUE_NULL:
+ sqlite3_bind_null(stmt, lpc + 1);
+ break;
+ default:
+ sqlite3_bind_text(stmt,
+ lpc + 1,
+ lv.text_value(),
+ lv.text_length(),
+ SQLITE_TRANSIENT);
+ break;
+ }
+ break;
+ }
+ }
+
+ auto step_res = sqlite3_step(stmt);
+
+ sqlite3_reset(stmt);
+ sqlite3_clear_bindings(stmt);
+ switch (step_res) {
+ case SQLITE_OK:
+ case SQLITE_DONE:
+ return Ok(false);
+ case SQLITE_ROW:
+ return Ok(true);
+ default:
+ return Err(sqlite3_error_to_user_message(sqlite3_db_handle(stmt)));
+ }
+
+ return Ok(true);
+}
+
+bool
+logfile_sub_source::check_extra_filters(iterator ld, logfile::iterator ll)
+{
+ if (this->lss_marked_only && !(ll->is_marked() || ll->is_expr_marked())) {
+ return false;
+ }
+
+ if (ll->get_msg_level() < this->lss_min_log_level) {
+ return false;
+ }
+
+ if (*ll < this->lss_min_log_time) {
+ return false;
+ }
+
+ if (!(*ll <= this->lss_max_log_time)) {
+ return false;
+ }
+
+ return true;
+}
+
+void
+logfile_sub_source::invalidate_sql_filter()
+{
+ for (auto& ld : *this) {
+ ld->ld_filter_state.lfo_filter_state.clear_filter_state(0);
+ }
+}
+
+void
+logfile_sub_source::text_mark(const bookmark_type_t* bm,
+ vis_line_t line,
+ bool added)
+{
+ if (line >= (int) this->lss_index.size()) {
+ return;
+ }
+
+ content_line_t cl = this->at(line);
+ std::vector<content_line_t>::iterator lb;
+
+ if (bm == &textview_curses::BM_USER) {
+ logline* ll = this->find_line(cl);
+
+ ll->set_mark(added);
+ }
+ lb = std::lower_bound(
+ this->lss_user_marks[bm].begin(), this->lss_user_marks[bm].end(), cl);
+ if (added) {
+ if (lb == this->lss_user_marks[bm].end() || *lb != cl) {
+ this->lss_user_marks[bm].insert(lb, cl);
+ }
+ } else if (lb != this->lss_user_marks[bm].end() && *lb == cl) {
+ require(lb != this->lss_user_marks[bm].end());
+
+ this->lss_user_marks[bm].erase(lb);
+ }
+ if (bm == &textview_curses::BM_META
+ && this->lss_meta_grepper.gps_proc != nullptr)
+ {
+ this->tss_view->search_range(line, line + 1_vl);
+ this->tss_view->search_new_data();
+ }
+}
+
+void
+logfile_sub_source::text_clear_marks(const bookmark_type_t* bm)
+{
+ std::vector<content_line_t>::iterator iter;
+
+ if (bm == &textview_curses::BM_USER) {
+ for (iter = this->lss_user_marks[bm].begin();
+ iter != this->lss_user_marks[bm].end();)
+ {
+ auto line_meta_opt = this->find_bookmark_metadata(*iter);
+ if (line_meta_opt) {
+ ++iter;
+ continue;
+ }
+ this->find_line(*iter)->set_mark(false);
+ iter = this->lss_user_marks[bm].erase(iter);
+ }
+ } else {
+ this->lss_user_marks[bm].clear();
+ }
+}
+
+void
+logfile_sub_source::remove_file(std::shared_ptr<logfile> lf)
+{
+ iterator iter;
+
+ iter = std::find_if(
+ this->lss_files.begin(), this->lss_files.end(), logfile_data_eq(lf));
+ if (iter != this->lss_files.end()) {
+ bookmarks<content_line_t>::type::iterator mark_iter;
+ int file_index = iter - this->lss_files.begin();
+
+ (*iter)->clear();
+ for (mark_iter = this->lss_user_marks.begin();
+ mark_iter != this->lss_user_marks.end();
+ ++mark_iter)
+ {
+ auto mark_curr = content_line_t(file_index * MAX_LINES_PER_FILE);
+ auto mark_end
+ = content_line_t((file_index + 1) * MAX_LINES_PER_FILE);
+ auto& bv = mark_iter->second;
+ auto file_range = bv.equal_range(mark_curr, mark_end);
+
+ if (file_range.first != file_range.second) {
+ bv.erase(file_range.first, file_range.second);
+ }
+ }
+
+ this->lss_force_rebuild = true;
+ }
+}
+
+nonstd::optional<vis_line_t>
+logfile_sub_source::find_from_content(content_line_t cl)
+{
+ content_line_t line = cl;
+ std::shared_ptr<logfile> lf = this->find(line);
+
+ if (lf != nullptr) {
+ auto ll_iter = lf->begin() + line;
+ auto& ll = *ll_iter;
+ auto vis_start_opt = this->find_from_time(ll.get_timeval());
+
+ if (!vis_start_opt) {
+ return nonstd::nullopt;
+ }
+
+ auto vis_start = *vis_start_opt;
+
+ while (vis_start < vis_line_t(this->text_line_count())) {
+ content_line_t guess_cl = this->at(vis_start);
+
+ if (cl == guess_cl) {
+ return vis_start;
+ }
+
+ auto guess_line = this->find_line(guess_cl);
+
+ if (!guess_line || ll < *guess_line) {
+ return nonstd::nullopt;
+ }
+
+ ++vis_start;
+ }
+ }
+
+ return nonstd::nullopt;
+}
+
+void
+logfile_sub_source::reload_index_delegate()
+{
+ if (this->lss_index_delegate == nullptr) {
+ return;
+ }
+
+ this->lss_index_delegate->index_start(*this);
+ for (unsigned int index : this->lss_filtered_index) {
+ content_line_t cl = (content_line_t) this->lss_index[index];
+ uint64_t line_number;
+ auto ld = this->find_data(cl, line_number);
+ std::shared_ptr<logfile> lf = (*ld)->get_file();
+
+ this->lss_index_delegate->index_line(
+ *this, lf.get(), lf->begin() + line_number);
+ }
+ this->lss_index_delegate->index_complete(*this);
+}
+
+nonstd::optional<std::shared_ptr<text_filter>>
+logfile_sub_source::get_sql_filter()
+{
+ return this->tss_filters | lnav::itertools::find_if([](const auto& filt) {
+ return filt->get_index() == 0;
+ })
+ | lnav::itertools::deref();
+}
+
+void
+log_location_history::loc_history_append(vis_line_t top)
+{
+ if (top >= vis_line_t(this->llh_log_source.text_line_count())) {
+ return;
+ }
+
+ content_line_t cl = this->llh_log_source.at(top);
+
+ auto iter = this->llh_history.begin();
+ iter += this->llh_history.size() - this->lh_history_position;
+ this->llh_history.erase_from(iter);
+ this->lh_history_position = 0;
+ this->llh_history.push_back(cl);
+}
+
+nonstd::optional<vis_line_t>
+log_location_history::loc_history_back(vis_line_t current_top)
+{
+ while (this->lh_history_position < this->llh_history.size()) {
+ auto iter = this->llh_history.rbegin();
+
+ auto vis_for_pos = this->llh_log_source.find_from_content(*iter);
+
+ if (this->lh_history_position == 0 && vis_for_pos != current_top) {
+ return vis_for_pos;
+ }
+
+ if ((this->lh_history_position + 1) >= this->llh_history.size()) {
+ break;
+ }
+
+ this->lh_history_position += 1;
+
+ iter += this->lh_history_position;
+
+ vis_for_pos = this->llh_log_source.find_from_content(*iter);
+
+ if (vis_for_pos) {
+ return vis_for_pos;
+ }
+ }
+
+ return nonstd::nullopt;
+}
+
+nonstd::optional<vis_line_t>
+log_location_history::loc_history_forward(vis_line_t current_top)
+{
+ while (this->lh_history_position > 0) {
+ this->lh_history_position -= 1;
+
+ auto iter = this->llh_history.rbegin();
+
+ iter += this->lh_history_position;
+
+ auto vis_for_pos = this->llh_log_source.find_from_content(*iter);
+
+ if (vis_for_pos) {
+ return vis_for_pos;
+ }
+ }
+
+ return nonstd::nullopt;
+}
+
+bool
+sql_filter::matches(const logfile& lf,
+ logfile::const_iterator ll,
+ const shared_buffer_ref& line)
+{
+ if (!ll->is_message()) {
+ return false;
+ }
+ if (this->sf_filter_stmt == nullptr) {
+ return false;
+ }
+
+ auto lfp = lf.shared_from_this();
+ auto ld = this->sf_log_source.find_data_i(lfp);
+ if (ld == this->sf_log_source.end()) {
+ return false;
+ }
+
+ auto eval_res
+ = this->sf_log_source.eval_sql_filter(this->sf_filter_stmt, ld, ll);
+ if (eval_res.unwrapOr(true)) {
+ return false;
+ }
+
+ return true;
+}
+
+std::string
+sql_filter::to_command() const
+{
+ return fmt::format(FMT_STRING("filter-expr {}"), this->lf_id);
+}
+
+bool
+logfile_sub_source::meta_grepper::grep_value_for_line(vis_line_t line,
+ std::string& value_out)
+{
+ auto line_meta_opt = this->lmg_source.find_bookmark_metadata(line);
+ if (!line_meta_opt) {
+ value_out.clear();
+ } else {
+ auto& bm = *(line_meta_opt.value());
+
+ {
+ md2attr_line mdal;
+
+ auto parse_res = md4cpp::parse(bm.bm_comment, mdal);
+ if (parse_res.isOk()) {
+ value_out.append(parse_res.unwrap().get_string());
+ } else {
+ value_out.append(bm.bm_comment);
+ }
+ }
+
+ value_out.append("\x1c");
+ for (const auto& tag : bm.bm_tags) {
+ value_out.append(tag);
+ value_out.append("\x1c");
+ }
+ }
+
+ return !this->lmg_done;
+}
+
+vis_line_t
+logfile_sub_source::meta_grepper::grep_initial_line(vis_line_t start,
+ vis_line_t highest)
+{
+ vis_bookmarks& bm = this->lmg_source.tss_view->get_bookmarks();
+ bookmark_vector<vis_line_t>& bv = bm[&textview_curses::BM_META];
+
+ if (bv.empty()) {
+ return -1_vl;
+ }
+ return *bv.begin();
+}
+
+void
+logfile_sub_source::meta_grepper::grep_next_line(vis_line_t& line)
+{
+ vis_bookmarks& bm = this->lmg_source.tss_view->get_bookmarks();
+ bookmark_vector<vis_line_t>& bv = bm[&textview_curses::BM_META];
+
+ auto line_opt = bv.next(vis_line_t(line));
+ if (!line_opt) {
+ this->lmg_done = true;
+ }
+ line = line_opt.value_or(-1_vl);
+}
+
+void
+logfile_sub_source::meta_grepper::grep_begin(grep_proc<vis_line_t>& gp,
+ vis_line_t start,
+ vis_line_t stop)
+{
+ this->lmg_source.quiesce();
+
+ this->lmg_source.tss_view->grep_begin(gp, start, stop);
+}
+
+void
+logfile_sub_source::meta_grepper::grep_end(grep_proc<vis_line_t>& gp)
+{
+ this->lmg_source.tss_view->grep_end(gp);
+}
+
+void
+logfile_sub_source::meta_grepper::grep_match(grep_proc<vis_line_t>& gp,
+ vis_line_t line,
+ int start,
+ int end)
+{
+ this->lmg_source.tss_view->grep_match(gp, line, start, end);
+}
+
+logline_window::iterator
+logline_window::begin()
+{
+ if (this->lw_start_line < 0_vl) {
+ return this->end();
+ }
+
+ return {this->lw_source, this->lw_start_line};
+}
+
+logline_window::iterator
+logline_window::end()
+{
+ return {this->lw_source, this->lw_end_line};
+}
+
+logline_window::logmsg_info::logmsg_info(logfile_sub_source& lss, vis_line_t vl)
+ : li_source(lss), li_line(vl)
+{
+ if (this->li_line < vis_line_t(this->li_source.text_line_count())) {
+ while (true) {
+ auto pair_opt = this->li_source.find_line_with_file(vl);
+
+ if (!pair_opt) {
+ break;
+ }
+
+ auto line_pair = pair_opt.value();
+ if (line_pair.second->is_message()) {
+ this->li_file = line_pair.first.get();
+ this->li_logline = line_pair.second;
+ break;
+ } else {
+ --vl;
+ }
+ }
+ }
+}
+
+void
+logline_window::logmsg_info::next_msg()
+{
+ this->li_file = nullptr;
+ this->li_logline = logfile::iterator{};
+ this->li_string_attrs.clear();
+ this->li_line_values.clear();
+ ++this->li_line;
+ while (this->li_line < vis_line_t(this->li_source.text_line_count())) {
+ auto pair_opt = this->li_source.find_line_with_file(this->li_line);
+
+ if (!pair_opt) {
+ break;
+ }
+
+ auto line_pair = pair_opt.value();
+ if (line_pair.second->is_message()) {
+ this->li_file = line_pair.first.get();
+ this->li_logline = line_pair.second;
+ break;
+ } else {
+ ++this->li_line;
+ }
+ }
+}
+
+void
+logline_window::logmsg_info::load_msg() const
+{
+ if (!this->li_string_attrs.empty()) {
+ return;
+ }
+
+ auto format = this->li_file->get_format();
+ this->li_file->read_full_message(this->li_logline,
+ this->li_line_values.lvv_sbr);
+ if (this->li_line_values.lvv_sbr.get_metadata().m_has_ansi) {
+ auto* writable_data = this->li_line_values.lvv_sbr.get_writable_data();
+ auto str
+ = std::string{writable_data, this->li_line_values.lvv_sbr.length()};
+ scrub_ansi_string(str, &this->li_string_attrs);
+ this->li_line_values.lvv_sbr.get_metadata().m_has_ansi = false;
+ }
+ format->annotate(std::distance(this->li_file->cbegin(), this->li_logline),
+ this->li_string_attrs,
+ this->li_line_values,
+ false);
+}
+
+std::string
+logline_window::logmsg_info::to_string(const struct line_range& lr) const
+{
+ this->load_msg();
+
+ return this->li_line_values.lvv_sbr
+ .to_string_fragment(lr.lr_start, lr.length())
+ .to_string();
+}
+
+logline_window::iterator&
+logline_window::iterator::operator++()
+{
+ this->i_info.next_msg();
+
+ return *this;
+}
+
+static std::vector<breadcrumb::possibility>
+timestamp_poss()
+{
+ const static std::vector<breadcrumb::possibility> retval = {
+ breadcrumb::possibility{"-1 day"},
+ breadcrumb::possibility{"-1h"},
+ breadcrumb::possibility{"-30m"},
+ breadcrumb::possibility{"-15m"},
+ breadcrumb::possibility{"-5m"},
+ breadcrumb::possibility{"-1m"},
+ breadcrumb::possibility{"+1m"},
+ breadcrumb::possibility{"+5m"},
+ breadcrumb::possibility{"+15m"},
+ breadcrumb::possibility{"+30m"},
+ breadcrumb::possibility{"+1h"},
+ breadcrumb::possibility{"+1 day"},
+ };
+
+ return retval;
+}
+
+void
+logfile_sub_source::text_crumbs_for_line(int line,
+ std::vector<breadcrumb::crumb>& crumbs)
+{
+ text_sub_source::text_crumbs_for_line(line, crumbs);
+
+ if (this->lss_filtered_index.empty()) {
+ return;
+ }
+
+ auto line_pair_opt = this->find_line_with_file(vis_line_t(line));
+ if (!line_pair_opt) {
+ return;
+ }
+ auto line_pair = line_pair_opt.value();
+ auto& lf = line_pair.first;
+ auto format = lf->get_format();
+ char ts[64];
+
+ sql_strftime(ts, sizeof(ts), line_pair.second->get_timeval(), 'T');
+
+ crumbs.emplace_back(
+ std::string(ts),
+ timestamp_poss,
+ [ec = this->lss_exec_context](const auto& ts) {
+ ec->execute(fmt::format(FMT_STRING(":goto {}"),
+ ts.template get<std::string>()));
+ });
+ crumbs.back().c_expected_input
+ = breadcrumb::crumb::expected_input_t::anything;
+ crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
+
+ auto format_name = format->get_name().to_string();
+ crumbs.emplace_back(
+ format_name,
+ attr_line_t().append(format_name),
+ [this]() -> std::vector<breadcrumb::possibility> {
+ return this->lss_files
+ | lnav::itertools::filter_in([](const auto& file_data) {
+ return file_data->is_visible();
+ })
+ | lnav::itertools::map(&logfile_data::get_file_ptr)
+ | lnav::itertools::map(&logfile::get_format_name)
+ | lnav::itertools::unique()
+ | lnav::itertools::map([](const auto& elem) {
+ return breadcrumb::possibility{
+ elem.to_string(),
+ };
+ });
+ },
+ [ec = this->lss_exec_context](const auto& format_name) {
+ static const std::string MOVE_STMT = R"(;UPDATE lnav_views
+ SET selection = ifnull((SELECT log_line FROM all_logs WHERE log_format = $format_name LIMIT 1), top)
+ WHERE name = 'log'
+)";
+
+ ec->execute_with(
+ MOVE_STMT,
+ std::make_pair("format_name",
+ format_name.template get<std::string>()));
+ });
+
+ auto msg_start_iter = lf->message_start(line_pair.second);
+ auto file_line_number = std::distance(lf->begin(), msg_start_iter);
+ crumbs.emplace_back(
+ lf->get_unique_path(),
+ attr_line_t()
+ .append(lf->get_unique_path())
+ .appendf(FMT_STRING("[{:L}]"), file_line_number),
+ [this]() -> std::vector<breadcrumb::possibility> {
+ return this->lss_files
+ | lnav::itertools::filter_in([](const auto& file_data) {
+ return file_data->is_visible();
+ })
+ | lnav::itertools::map([](const auto& file_data) {
+ return breadcrumb::possibility{
+ file_data->get_file_ptr()->get_unique_path(),
+ attr_line_t(
+ file_data->get_file_ptr()->get_unique_path()),
+ };
+ });
+ },
+ [ec = this->lss_exec_context](const auto& uniq_path) {
+ static const std::string MOVE_STMT = R"(;UPDATE lnav_views
+ SET selection = ifnull((SELECT log_line FROM all_logs WHERE log_unique_path = $uniq_path LIMIT 1), top)
+ WHERE name = 'log'
+)";
+
+ ec->execute_with(
+ MOVE_STMT,
+ std::make_pair("uniq_path",
+ uniq_path.template get<std::string>()));
+ });
+
+ logline_value_vector values;
+ auto& sbr = values.lvv_sbr;
+
+ lf->read_full_message(msg_start_iter, sbr);
+ attr_line_t al(to_string(sbr));
+ if (sbr.get_metadata().m_has_ansi) {
+ // bleh
+ scrub_ansi_string(al.get_string(), &al.al_attrs);
+ sbr.erase_ansi();
+ }
+ format->annotate(file_line_number, al.get_attrs(), values);
+
+ auto opid_opt = get_string_attr(al.get_attrs(), logline::L_OPID);
+ if (opid_opt && !opid_opt.value().saw_string_attr->sa_range.empty()) {
+ const auto& opid_range = opid_opt.value().saw_string_attr->sa_range;
+ const auto opid_str
+ = sbr.to_string_fragment(opid_range.lr_start, opid_range.length())
+ .to_string();
+ crumbs.emplace_back(
+ opid_str,
+ attr_line_t().append(lnav::roles::identifier(opid_str)),
+ [this]() -> std::vector<breadcrumb::possibility> {
+ std::vector<breadcrumb::possibility> retval;
+
+ for (const auto& file_data : this->lss_files) {
+ if (file_data->get_file_ptr() == nullptr) {
+ continue;
+ }
+ safe::ReadAccess<logfile::safe_opid_map> r_opid_map(
+ file_data->get_file_ptr()->get_opids());
+
+ for (const auto& pair : *r_opid_map) {
+ retval.emplace_back(pair.first.to_string());
+ }
+ }
+
+ return retval;
+ },
+ [ec = this->lss_exec_context](const auto& opid) {
+ static const std::string MOVE_STMT = R"(;UPDATE lnav_views
+ SET selection = ifnull((SELECT log_line FROM all_logs WHERE log_opid = $opid LIMIT 1), top)
+ WHERE name = 'log'
+ )";
+
+ ec->execute_with(
+ MOVE_STMT,
+ std::make_pair("opid", opid.template get<std::string>()));
+ });
+ }
+
+ auto sf = sbr.to_string_fragment();
+ auto body_opt = get_string_attr(al.get_attrs(), SA_BODY);
+ auto nl_pos_opt = sf.find('\n');
+ auto msg_line_number = std::distance(msg_start_iter, line_pair.second);
+ auto line_from_top = line - msg_line_number;
+ if (body_opt && nl_pos_opt) {
+ if (this->lss_token_meta_line != file_line_number
+ || this->lss_token_meta_size != sf.length())
+ {
+ this->lss_token_meta = lnav::document::discover_structure(
+ al, body_opt.value().saw_string_attr->sa_range);
+ this->lss_token_meta_line = file_line_number;
+ this->lss_token_meta_size = sf.length();
+ }
+
+ const auto initial_size = crumbs.size();
+ auto sf_body
+ = sf.sub_range(body_opt->saw_string_attr->sa_range.lr_start,
+ body_opt->saw_string_attr->sa_range.lr_end);
+ file_off_t line_offset = 0;
+ file_off_t line_end_offset = sf.length();
+ size_t line_number = 0;
+
+ for (const auto& sf_line : sf_body.split_lines()) {
+ if (line_number >= msg_line_number) {
+ line_end_offset = line_offset + sf_line.length();
+ break;
+ }
+ line_number += 1;
+ line_offset += sf_line.length();
+ }
+
+ this->lss_token_meta.m_sections_tree.visit_overlapping(
+ line_offset,
+ line_end_offset,
+ [this,
+ initial_size,
+ meta = &this->lss_token_meta,
+ &crumbs,
+ line_from_top](const auto& iv) {
+ auto path = crumbs | lnav::itertools::skip(initial_size)
+ | lnav::itertools::map(&breadcrumb::crumb::c_key)
+ | lnav::itertools::append(iv.value);
+ auto curr_node = lnav::document::hier_node::lookup_path(
+ meta->m_sections_root.get(), path);
+
+ crumbs.template emplace_back(
+ iv.value,
+ [meta, path]() { return meta->possibility_provider(path); },
+ [this, curr_node, path, line_from_top](const auto& key) {
+ if (!curr_node) {
+ return;
+ }
+ auto* parent_node = curr_node.value()->hn_parent;
+ if (parent_node == nullptr) {
+ return;
+ }
+ key.template match(
+ [parent_node](const std::string& str) {
+ return parent_node->find_line_number(str);
+ },
+ [parent_node](size_t index) {
+ return parent_node->find_line_number(index);
+ })
+ | [this, line_from_top](auto line_number) {
+ this->tss_view->set_selection(
+ vis_line_t(line_from_top + line_number));
+ };
+ });
+ if (curr_node && !curr_node.value()->hn_parent->is_named_only())
+ {
+ auto node = lnav::document::hier_node::lookup_path(
+ meta->m_sections_root.get(), path);
+
+ crumbs.back().c_expected_input
+ = curr_node.value()
+ ->hn_parent->hn_named_children.empty()
+ ? breadcrumb::crumb::expected_input_t::index
+ : breadcrumb::crumb::expected_input_t::index_or_exact;
+ crumbs.back().with_possible_range(
+ node | lnav::itertools::map([](const auto hn) {
+ return hn->hn_parent->hn_children.size();
+ })
+ | lnav::itertools::unwrap_or(size_t{0}));
+ }
+ });
+
+ auto path = crumbs | lnav::itertools::skip(initial_size)
+ | lnav::itertools::map(&breadcrumb::crumb::c_key);
+ auto node = lnav::document::hier_node::lookup_path(
+ this->lss_token_meta.m_sections_root.get(), path);
+
+ if (node && !node.value()->hn_children.empty()) {
+ auto poss_provider = [curr_node = node.value()]() {
+ std::vector<breadcrumb::possibility> retval;
+ for (const auto& child : curr_node->hn_named_children) {
+ retval.template emplace_back(child.first);
+ }
+ return retval;
+ };
+ auto path_performer
+ = [this, curr_node = node.value(), line_from_top](
+ const breadcrumb::crumb::key_t& value) {
+ value.template match(
+ [curr_node](const std::string& str) {
+ return curr_node->find_line_number(str);
+ },
+ [curr_node](size_t index) {
+ return curr_node->find_line_number(index);
+ })
+ | [this, line_from_top](size_t line_number) {
+ this->tss_view->set_selection(
+ vis_line_t(line_from_top + line_number));
+ };
+ };
+ crumbs.emplace_back("", "\u22ef", poss_provider, path_performer);
+ crumbs.back().c_expected_input
+ = node.value()->hn_named_children.empty()
+ ? breadcrumb::crumb::expected_input_t::index
+ : breadcrumb::crumb::expected_input_t::index_or_exact;
+ }
+ }
+}
+
+void
+logfile_sub_source::quiesce()
+{
+ for (auto& ld : this->lss_files) {
+ auto* lf = ld->get_file_ptr();
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ lf->quiesce();
+ }
+}
+
+bookmark_metadata&
+logfile_sub_source::get_bookmark_metadata(content_line_t cl)
+{
+ auto line_pair = this->find_line_with_file(cl).value();
+ auto line_number = static_cast<uint32_t>(
+ std::distance(line_pair.first->begin(), line_pair.second));
+
+ return line_pair.first->get_bookmark_metadata()[line_number];
+}
+
+nonstd::optional<bookmark_metadata*>
+logfile_sub_source::find_bookmark_metadata(content_line_t cl)
+{
+ auto line_pair = this->find_line_with_file(cl).value();
+ auto line_number = static_cast<uint32_t>(
+ std::distance(line_pair.first->begin(), line_pair.second));
+
+ auto& bm = line_pair.first->get_bookmark_metadata();
+ auto bm_iter = bm.find(line_number);
+ if (bm_iter == bm.end()) {
+ return nonstd::nullopt;
+ }
+
+ return &bm_iter->second;
+}
+
+void
+logfile_sub_source::erase_bookmark_metadata(content_line_t cl)
+{
+ auto line_pair = this->find_line_with_file(cl).value();
+ auto line_number = static_cast<uint32_t>(
+ std::distance(line_pair.first->begin(), line_pair.second));
+
+ auto& bm = line_pair.first->get_bookmark_metadata();
+ auto bm_iter = bm.find(line_number);
+ if (bm_iter != bm.end()) {
+ bm.erase(bm_iter);
+ }
+}
+
+void
+logfile_sub_source::clear_bookmark_metadata()
+{
+ for (auto& ld : *this) {
+ if (ld->get_file_ptr() == nullptr) {
+ continue;
+ }
+
+ ld->get_file_ptr()->get_bookmark_metadata().clear();
+ }
+}
diff --git a/src/logfile_sub_source.cfg.hh b/src/logfile_sub_source.cfg.hh
new file mode 100644
index 0000000..5bbfdea
--- /dev/null
+++ b/src/logfile_sub_source.cfg.hh
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_logfile_sub_source_cfg_hh
+#define lnav_logfile_sub_source_cfg_hh
+
+#include <map>
+#include <string>
+
+namespace logfile_sub_source_ns {
+
+struct watch_expression {
+ std::string we_expr;
+ bool we_enabled{true};
+};
+
+struct config {
+ std::map<std::string, watch_expression> c_watch_exprs;
+};
+
+} // namespace logfile_sub_source_ns
+
+#endif
diff --git a/src/logfile_sub_source.hh b/src/logfile_sub_source.hh
new file mode 100644
index 0000000..ec032c8
--- /dev/null
+++ b/src/logfile_sub_source.hh
@@ -0,0 +1,1021 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file logfile_sub_source.hh
+ */
+
+#ifndef logfile_sub_source_hh
+#define logfile_sub_source_hh
+
+#include <array>
+#include <list>
+#include <map>
+#include <sstream>
+#include <utility>
+#include <vector>
+
+#include <limits.h>
+
+#include "base/lnav.console.hh"
+#include "base/lnav_log.hh"
+#include "base/time_util.hh"
+#include "big_array.hh"
+#include "bookmarks.hh"
+#include "document.sections.hh"
+#include "filter_observer.hh"
+#include "lnav_config_fwd.hh"
+#include "log_accel.hh"
+#include "log_format.hh"
+#include "logfile.hh"
+#include "strong_int.hh"
+#include "textview_curses.hh"
+
+STRONG_INT_TYPE(uint64_t, content_line);
+
+struct sqlite3_stmt;
+extern "C"
+{
+int sqlite3_finalize(sqlite3_stmt* pStmt);
+}
+
+class logfile_sub_source;
+
+class index_delegate {
+public:
+ virtual ~index_delegate() = default;
+
+ virtual void index_start(logfile_sub_source& lss) {}
+
+ virtual void index_line(logfile_sub_source& lss,
+ logfile* lf,
+ logfile::iterator ll)
+ {
+ }
+
+ virtual void index_complete(logfile_sub_source& lss) {}
+};
+
+class pcre_filter : public text_filter {
+public:
+ pcre_filter(type_t type,
+ const std::string& id,
+ size_t index,
+ std::shared_ptr<lnav::pcre2pp::code> code)
+ : text_filter(type, filter_lang_t::REGEX, id, index),
+ pf_pcre(std::move(code))
+ {
+ }
+
+ ~pcre_filter() override = default;
+
+ bool matches(const logfile& lf,
+ logfile::const_iterator ll,
+ const shared_buffer_ref& line) override
+ {
+ return this->pf_pcre->find_in(line.to_string_fragment())
+ .ignore_error()
+ .has_value();
+ }
+
+ std::string to_command() const override
+ {
+ return (this->lf_type == text_filter::INCLUDE ? "filter-in "
+ : "filter-out ")
+ + this->lf_id;
+ }
+
+protected:
+ std::shared_ptr<lnav::pcre2pp::code> pf_pcre;
+};
+
+class sql_filter : public text_filter {
+public:
+ sql_filter(logfile_sub_source& lss,
+ std::string stmt_str,
+ sqlite3_stmt* stmt)
+ : text_filter(EXCLUDE, filter_lang_t::SQL, std::move(stmt_str), 0),
+ sf_log_source(lss)
+ {
+ this->sf_filter_stmt = stmt;
+ }
+
+ bool matches(const logfile& lf,
+ logfile::const_iterator ll,
+ const shared_buffer_ref& line) override;
+
+ std::string to_command() const override;
+
+ auto_mem<sqlite3_stmt> sf_filter_stmt{sqlite3_finalize};
+ logfile_sub_source& sf_log_source;
+};
+
+class log_location_history : public location_history {
+public:
+ explicit log_location_history(logfile_sub_source& lss)
+ : llh_history(std::begin(this->llh_backing),
+ std::end(this->llh_backing)),
+ llh_log_source(lss)
+ {
+ }
+
+ ~log_location_history() override = default;
+
+ void loc_history_append(vis_line_t top) override;
+
+ nonstd::optional<vis_line_t> loc_history_back(
+ vis_line_t current_top) override;
+
+ nonstd::optional<vis_line_t> loc_history_forward(
+ vis_line_t current_top) override;
+
+private:
+ nonstd::ring_span<content_line_t> llh_history;
+ logfile_sub_source& llh_log_source;
+ content_line_t llh_backing[MAX_SIZE];
+};
+
+class logline_window {
+public:
+ logline_window(logfile_sub_source& lss,
+ vis_line_t start_vl,
+ vis_line_t end_vl)
+ : lw_source(lss), lw_start_line(start_vl), lw_end_line(end_vl)
+ {
+ }
+
+ class iterator;
+
+ class logmsg_info {
+ public:
+ logmsg_info(logfile_sub_source& lss, vis_line_t vl);
+
+ vis_line_t get_vis_line() const { return this->li_line; }
+
+ const logline& get_logline() const { return *this->li_logline; }
+
+ const string_attrs_t& get_attrs() const
+ {
+ this->load_msg();
+ return this->li_string_attrs;
+ }
+
+ const logline_value_vector& get_values() const
+ {
+ this->load_msg();
+ return this->li_line_values;
+ }
+
+ std::string to_string(const struct line_range& lr) const;
+
+ private:
+ friend iterator;
+
+ void next_msg();
+ void load_msg() const;
+
+ logfile_sub_source& li_source;
+ vis_line_t li_line;
+ logfile* li_file{nullptr};
+ logfile::const_iterator li_logline;
+ mutable string_attrs_t li_string_attrs;
+ mutable logline_value_vector li_line_values;
+ };
+
+ class iterator {
+ public:
+ iterator(logfile_sub_source& lss, vis_line_t vl) : i_info(lss, vl) {}
+
+ iterator& operator++();
+
+ bool operator!=(const iterator& rhs) const
+ {
+ return this->i_info.get_vis_line() != rhs.i_info.get_vis_line();
+ }
+
+ const logmsg_info& operator*() const { return this->i_info; }
+
+ private:
+ logmsg_info i_info;
+ };
+
+ iterator begin();
+
+ iterator end();
+
+private:
+ logfile_sub_source& lw_source;
+ vis_line_t lw_start_line;
+ vis_line_t lw_end_line;
+};
+
+/**
+ * Delegate class that merges the contents of multiple log files into a single
+ * source of data for a text view.
+ */
+class logfile_sub_source
+ : public text_sub_source
+ , public text_time_translator
+ , public list_input_delegate {
+public:
+ const static bookmark_type_t BM_ERRORS;
+ const static bookmark_type_t BM_WARNINGS;
+ const static bookmark_type_t BM_FILES;
+
+ virtual void text_filters_changed();
+
+ logfile_sub_source();
+
+ ~logfile_sub_source() = default;
+
+ void toggle_time_offset()
+ {
+ this->lss_flags ^= F_TIME_OFFSET;
+ this->clear_line_size_cache();
+ }
+
+ void increase_line_context()
+ {
+ auto old_flags = this->lss_flags;
+
+ if (this->lss_flags & F_FILENAME) {
+ // Nothing to do
+ } else if (this->lss_flags & F_BASENAME) {
+ this->lss_flags &= ~F_NAME_MASK;
+ this->lss_flags |= F_FILENAME;
+ } else {
+ this->lss_flags |= F_BASENAME;
+ }
+ if (old_flags != this->lss_flags) {
+ this->clear_line_size_cache();
+ }
+ }
+
+ bool decrease_line_context()
+ {
+ auto old_flags = this->lss_flags;
+
+ if (this->lss_flags & F_FILENAME) {
+ this->lss_flags &= ~F_NAME_MASK;
+ this->lss_flags |= F_BASENAME;
+ } else if (this->lss_flags & F_BASENAME) {
+ this->lss_flags &= ~F_NAME_MASK;
+ }
+ if (old_flags != this->lss_flags) {
+ this->clear_line_size_cache();
+
+ return true;
+ }
+
+ return false;
+ }
+
+ size_t get_filename_offset() const
+ {
+ if (this->lss_flags & F_FILENAME) {
+ return this->lss_filename_width;
+ } else if (this->lss_flags & F_BASENAME) {
+ return this->lss_basename_width;
+ }
+
+ return 0;
+ }
+
+ void set_time_offset(bool enabled)
+ {
+ if (enabled)
+ this->lss_flags |= F_TIME_OFFSET;
+ else
+ this->lss_flags &= ~F_TIME_OFFSET;
+ this->clear_line_size_cache();
+ }
+
+ bool is_time_offset_enabled() const
+ {
+ return (bool) (this->lss_flags & F_TIME_OFFSET);
+ }
+
+ bool is_filename_enabled() const
+ {
+ return (bool) (this->lss_flags & F_FILENAME);
+ }
+
+ bool is_basename_enabled() const
+ {
+ return (bool) (this->lss_flags & F_BASENAME);
+ }
+
+ log_level_t get_min_log_level() const { return this->lss_min_log_level; }
+
+ void set_force_rebuild() { this->lss_force_rebuild = true; }
+
+ void set_min_log_level(log_level_t level)
+ {
+ if (this->lss_min_log_level != level) {
+ this->lss_min_log_level = level;
+ this->text_filters_changed();
+ }
+ }
+
+ bool get_min_log_time(struct timeval& tv_out) const
+ {
+ tv_out = this->lss_min_log_time;
+ return (this->lss_min_log_time.tv_sec != 0
+ || this->lss_min_log_time.tv_usec != 0);
+ }
+
+ void set_min_log_time(const struct timeval& tv)
+ {
+ if (this->lss_min_log_time != tv) {
+ this->lss_min_log_time = tv;
+ this->text_filters_changed();
+ }
+ }
+
+ bool get_max_log_time(struct timeval& tv_out) const
+ {
+ tv_out = this->lss_max_log_time;
+ return (this->lss_max_log_time.tv_sec
+ != std::numeric_limits<time_t>::max()
+ || this->lss_max_log_time.tv_usec != 0);
+ }
+
+ void set_max_log_time(struct timeval& tv)
+ {
+ if (this->lss_max_log_time != tv) {
+ this->lss_max_log_time = tv;
+ this->text_filters_changed();
+ }
+ }
+
+ void clear_min_max_log_times()
+ {
+ if (this->lss_min_log_time.tv_sec != 0
+ || this->lss_min_log_time.tv_usec != 0
+ || this->lss_max_log_time.tv_sec
+ != std::numeric_limits<time_t>::max()
+ || this->lss_max_log_time.tv_usec != 0)
+ {
+ memset(&this->lss_min_log_time, 0, sizeof(this->lss_min_log_time));
+ this->lss_max_log_time.tv_sec = std::numeric_limits<time_t>::max();
+ this->lss_max_log_time.tv_usec = 0;
+ this->text_filters_changed();
+ }
+ }
+
+ bool list_input_handle_key(listview_curses& lv, int ch);
+
+ void set_marked_only(bool val)
+ {
+ if (this->lss_marked_only != val) {
+ this->lss_marked_only = val;
+ this->text_filters_changed();
+ }
+ }
+
+ bool get_marked_only() { return this->lss_marked_only; }
+
+ size_t text_line_count() { return this->lss_filtered_index.size(); }
+
+ size_t text_line_width(textview_curses& curses)
+ {
+ return this->lss_longest_line;
+ }
+
+ size_t file_count() const
+ {
+ size_t retval = 0;
+ const_iterator iter;
+
+ for (iter = this->cbegin(); iter != this->cend(); ++iter) {
+ if (*iter != nullptr && (*iter)->get_file() != nullptr) {
+ retval += 1;
+ }
+ }
+
+ return retval;
+ }
+
+ bool empty() const { return this->lss_filtered_index.empty(); }
+
+ void text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& value_out,
+ line_flags_t flags);
+
+ void text_attrs_for_line(textview_curses& tc,
+ int row,
+ string_attrs_t& value_out);
+
+ size_t text_size_for_line(textview_curses& tc, int row, line_flags_t flags)
+ {
+ size_t index = row % LINE_SIZE_CACHE_SIZE;
+
+ if (this->lss_line_size_cache[index].first != row) {
+ std::string value;
+
+ this->text_value_for_line(tc, row, value, flags);
+ this->lss_line_size_cache[index].second = value.size();
+ this->lss_line_size_cache[index].first = row;
+ }
+ return this->lss_line_size_cache[index].second;
+ }
+
+ void text_mark(const bookmark_type_t* bm, vis_line_t line, bool added);
+
+ void text_clear_marks(const bookmark_type_t* bm);
+
+ bool insert_file(const std::shared_ptr<logfile>& lf);
+
+ void remove_file(std::shared_ptr<logfile> lf);
+
+ enum class rebuild_result {
+ rr_no_change,
+ rr_appended_lines,
+ rr_partial_rebuild,
+ rr_full_rebuild,
+ };
+
+ rebuild_result rebuild_index(nonstd::optional<ui_clock::time_point> deadline
+ = nonstd::nullopt);
+
+ void text_update_marks(vis_bookmarks& bm);
+
+ void set_user_mark(const bookmark_type_t* bm, content_line_t cl)
+ {
+ this->lss_user_marks[bm].insert_once(cl);
+ }
+
+ bookmarks<content_line_t>::type& get_user_bookmarks()
+ {
+ return this->lss_user_marks;
+ }
+
+ bookmark_metadata& get_bookmark_metadata(content_line_t cl);
+
+ bookmark_metadata& get_bookmark_metadata(vis_line_t vl)
+ {
+ return this->get_bookmark_metadata(this->at(vl));
+ }
+
+ nonstd::optional<bookmark_metadata*> find_bookmark_metadata(
+ content_line_t cl);
+
+ nonstd::optional<bookmark_metadata*> find_bookmark_metadata(vis_line_t vl)
+ {
+ return this->find_bookmark_metadata(this->at(vl));
+ }
+
+ void erase_bookmark_metadata(content_line_t cl);
+
+ void erase_bookmark_metadata(vis_line_t vl)
+ {
+ this->erase_bookmark_metadata(this->at(vl));
+ }
+
+ void clear_bookmark_metadata();
+
+ int get_filtered_count() const
+ {
+ return this->lss_index.size() - this->lss_filtered_index.size();
+ }
+
+ int get_filtered_count_for(size_t filter_index) const
+ {
+ int retval = 0;
+
+ for (const auto& ld : this->lss_files) {
+ retval += ld->ld_filter_state.lfo_filter_state
+ .tfs_filter_hits[filter_index];
+ }
+
+ return retval;
+ }
+
+ Result<void, lnav::console::user_message> set_sql_filter(
+ std::string stmt_str, sqlite3_stmt* stmt);
+
+ Result<void, lnav::console::user_message> set_sql_marker(
+ std::string stmt_str, sqlite3_stmt* stmt);
+
+ Result<void, lnav::console::user_message> set_preview_sql_filter(
+ sqlite3_stmt* stmt);
+
+ std::string get_sql_filter_text()
+ {
+ auto filt = this->get_sql_filter();
+
+ if (filt) {
+ return filt.value()->get_id();
+ }
+ return "";
+ }
+
+ nonstd::optional<std::shared_ptr<text_filter>> get_sql_filter();
+
+ std::string get_sql_marker_text() const
+ {
+ return this->lss_marker_stmt_text;
+ }
+
+ std::shared_ptr<logfile> find(const char* fn, content_line_t& line_base);
+
+ std::shared_ptr<logfile> find(content_line_t& line) const
+ {
+ std::shared_ptr<logfile> retval;
+
+ retval = this->lss_files[line / MAX_LINES_PER_FILE]->get_file();
+ line = content_line_t(line % MAX_LINES_PER_FILE);
+
+ return retval;
+ }
+
+ logfile* find_file_ptr(content_line_t& line)
+ {
+ auto retval
+ = this->lss_files[line / MAX_LINES_PER_FILE]->get_file_ptr();
+ line = content_line_t(line % MAX_LINES_PER_FILE);
+
+ return retval;
+ }
+
+ logline* find_line(content_line_t line) const
+ {
+ logline* retval = nullptr;
+ std::shared_ptr<logfile> lf = this->find(line);
+
+ if (lf != nullptr) {
+ auto ll_iter = lf->begin() + line;
+
+ retval = &(*ll_iter);
+ }
+
+ return retval;
+ }
+
+ nonstd::optional<std::pair<std::shared_ptr<logfile>, logfile::iterator>>
+ find_line_with_file(content_line_t line) const
+ {
+ std::shared_ptr<logfile> lf = this->find(line);
+
+ if (lf != nullptr) {
+ auto ll_iter = lf->begin() + line;
+
+ return std::make_pair(lf, ll_iter);
+ }
+
+ return nonstd::nullopt;
+ }
+
+ nonstd::optional<std::pair<std::shared_ptr<logfile>, logfile::iterator>>
+ find_line_with_file(vis_line_t vl) const
+ {
+ if (vl >= 0_vl && vl <= vis_line_t(this->lss_filtered_index.size())) {
+ return this->find_line_with_file(this->at(vl));
+ }
+
+ return nonstd::nullopt;
+ }
+
+ nonstd::optional<vis_line_t> find_from_time(
+ const struct timeval& start) const;
+
+ nonstd::optional<vis_line_t> find_from_time(time_t start) const
+ {
+ struct timeval tv = {start, 0};
+
+ return this->find_from_time(tv);
+ }
+
+ nonstd::optional<vis_line_t> find_from_time(const exttm& etm) const
+ {
+ return this->find_from_time(etm.to_timeval());
+ }
+
+ nonstd::optional<vis_line_t> find_from_content(content_line_t cl);
+
+ nonstd::optional<struct timeval> time_for_row(vis_line_t row)
+ {
+ if (row < (ssize_t) this->text_line_count()) {
+ return this->find_line(this->at(row))->get_timeval();
+ }
+ return nonstd::nullopt;
+ }
+
+ nonstd::optional<vis_line_t> row_for_time(struct timeval time_bucket)
+ {
+ return this->find_from_time(time_bucket);
+ }
+
+ content_line_t at(vis_line_t vl) const
+ {
+ return this->lss_index[this->lss_filtered_index[vl]];
+ }
+
+ content_line_t at_base(vis_line_t vl)
+ {
+ while (this->find_line(this->at(vl))->get_sub_offset() != 0) {
+ --vl;
+ }
+
+ return this->at(vl);
+ }
+
+ logline_window window_at(vis_line_t start_vl, vis_line_t end_vl)
+ {
+ return logline_window(*this, start_vl, end_vl);
+ }
+
+ log_accel::direction_t get_line_accel_direction(vis_line_t vl);
+
+ /**
+ * Container for logfile references that keeps of how many lines in the
+ * logfile have been indexed.
+ */
+ struct logfile_data {
+ logfile_data(size_t index,
+ filter_stack& fs,
+ const std::shared_ptr<logfile>& lf)
+ : ld_file_index(index), ld_filter_state(fs, lf),
+ ld_visible(lf->is_indexing())
+ {
+ lf->set_logline_observer(&this->ld_filter_state);
+ }
+
+ void clear() { this->ld_filter_state.lfo_filter_state.clear(); }
+
+ void set_file(const std::shared_ptr<logfile>& lf)
+ {
+ this->ld_filter_state.lfo_filter_state.tfs_logfile = lf;
+ lf->set_logline_observer(&this->ld_filter_state);
+ }
+
+ std::shared_ptr<logfile> get_file() const
+ {
+ return this->ld_filter_state.lfo_filter_state.tfs_logfile;
+ }
+
+ logfile* get_file_ptr() const
+ {
+ return this->ld_filter_state.lfo_filter_state.tfs_logfile.get();
+ }
+
+ bool is_visible() const
+ {
+ return this->get_file_ptr() != nullptr && this->ld_visible;
+ }
+
+ void set_visibility(bool vis) { this->ld_visible = vis; }
+
+ size_t ld_file_index;
+ line_filter_observer ld_filter_state;
+ size_t ld_lines_indexed{0};
+ size_t ld_lines_watched{0};
+ bool ld_visible;
+ };
+
+ using iterator = std::vector<std::unique_ptr<logfile_data>>::iterator;
+ using const_iterator
+ = std::vector<std::unique_ptr<logfile_data>>::const_iterator;
+
+ iterator begin() { return this->lss_files.begin(); }
+
+ iterator end() { return this->lss_files.end(); }
+
+ const_iterator cbegin() const { return this->lss_files.begin(); }
+
+ const_iterator cend() const { return this->lss_files.end(); }
+
+ iterator find_data(content_line_t& line)
+ {
+ auto retval = this->lss_files.begin();
+ std::advance(retval, line / MAX_LINES_PER_FILE);
+ line = content_line_t(line % MAX_LINES_PER_FILE);
+
+ return retval;
+ }
+
+ iterator find_data(content_line_t line, uint64_t& offset_out)
+ {
+ auto retval = this->lss_files.begin();
+ std::advance(retval, line / MAX_LINES_PER_FILE);
+ offset_out = line % MAX_LINES_PER_FILE;
+
+ return retval;
+ }
+
+ nonstd::optional<logfile_data*> find_data(
+ const std::shared_ptr<logfile>& lf)
+ {
+ for (auto& ld : *this) {
+ if (ld->ld_filter_state.lfo_filter_state.tfs_logfile == lf) {
+ return ld.get();
+ }
+ }
+ return nonstd::nullopt;
+ }
+
+ iterator find_data_i(const std::shared_ptr<const logfile>& lf)
+ {
+ for (auto iter = this->begin(); iter != this->end(); ++iter) {
+ if ((*iter)->ld_filter_state.lfo_filter_state.tfs_logfile == lf) {
+ return iter;
+ }
+ }
+
+ return this->end();
+ }
+
+ content_line_t get_file_base_content_line(iterator iter)
+ {
+ ssize_t index = std::distance(this->begin(), iter);
+
+ return content_line_t(index * MAX_LINES_PER_FILE);
+ }
+
+ void set_index_delegate(index_delegate* id)
+ {
+ if (id != this->lss_index_delegate) {
+ this->lss_index_delegate = id;
+ this->reload_index_delegate();
+ }
+ }
+
+ index_delegate* get_index_delegate() const
+ {
+ return this->lss_index_delegate;
+ }
+
+ void reload_index_delegate();
+
+ class meta_grepper
+ : public grep_proc_source<vis_line_t>
+ , public grep_proc_sink<vis_line_t> {
+ public:
+ meta_grepper(logfile_sub_source& source) : lmg_source(source) {}
+
+ bool grep_value_for_line(vis_line_t line,
+ std::string& value_out) override;
+
+ vis_line_t grep_initial_line(vis_line_t start,
+ vis_line_t highest) override;
+
+ void grep_next_line(vis_line_t& line) override;
+
+ void grep_begin(grep_proc<vis_line_t>& gp,
+ vis_line_t start,
+ vis_line_t stop) override;
+
+ void grep_end(grep_proc<vis_line_t>& gp) override;
+
+ void grep_match(grep_proc<vis_line_t>& gp,
+ vis_line_t line,
+ int start,
+ int end) override;
+
+ logfile_sub_source& lmg_source;
+ bool lmg_done{false};
+ };
+
+ nonstd::optional<
+ std::pair<grep_proc_source<vis_line_t>*, grep_proc_sink<vis_line_t>*>>
+ get_grepper();
+
+ nonstd::optional<location_history*> get_location_history()
+ {
+ return &this->lss_location_history;
+ }
+
+ void text_crumbs_for_line(int line, std::vector<breadcrumb::crumb>& crumbs);
+
+ Result<bool, lnav::console::user_message> eval_sql_filter(
+ sqlite3_stmt* stmt, iterator ld, logfile::const_iterator ll);
+
+ void invalidate_sql_filter();
+
+ void set_line_meta_changed() { this->lss_line_meta_changed = true; }
+
+ bool is_line_meta_changed() const { return this->lss_line_meta_changed; }
+
+ void set_exec_context(exec_context* ec) { this->lss_exec_context = ec; }
+
+ static const uint64_t MAX_CONTENT_LINES = (1ULL << 40) - 1;
+ static const uint64_t MAX_LINES_PER_FILE = 256 * 1024 * 1024;
+ static const uint64_t MAX_FILES = (MAX_CONTENT_LINES / MAX_LINES_PER_FILE);
+
+ std::function<void(logfile_sub_source&, file_off_t, file_size_t)>
+ lss_sorting_observer;
+
+ uint32_t lss_index_generation{0};
+
+ void quiesce();
+
+private:
+ static const size_t LINE_SIZE_CACHE_SIZE = 512;
+
+ enum {
+ B_SCRUB,
+ B_TIME_OFFSET,
+ B_FILENAME,
+ B_BASENAME,
+ };
+
+ enum {
+ F_SCRUB = (1UL << B_SCRUB),
+ F_TIME_OFFSET = (1UL << B_TIME_OFFSET),
+ F_FILENAME = (1UL << B_FILENAME),
+ F_BASENAME = (1UL << B_BASENAME),
+
+ F_NAME_MASK = (F_FILENAME | F_BASENAME),
+ };
+
+ struct __attribute__((__packed__)) indexed_content {
+ indexed_content() = default;
+
+ indexed_content(content_line_t cl) : ic_value(cl) {}
+
+ operator content_line_t() const
+ {
+ return content_line_t(this->ic_value);
+ }
+
+ uint64_t ic_value : 40;
+ };
+
+ struct logline_cmp {
+ logline_cmp(logfile_sub_source& lc) : llss_controller(lc) {}
+
+ bool operator()(const content_line_t& lhs,
+ const content_line_t& rhs) const
+ {
+ logline* ll_lhs = this->llss_controller.find_line(lhs);
+ logline* ll_rhs = this->llss_controller.find_line(rhs);
+
+ return (*ll_lhs) < (*ll_rhs);
+ }
+
+ bool operator()(const uint32_t& lhs, const uint32_t& rhs) const
+ {
+ content_line_t cl_lhs
+ = (content_line_t) llss_controller.lss_index[lhs];
+ content_line_t cl_rhs
+ = (content_line_t) llss_controller.lss_index[rhs];
+ logline* ll_lhs = this->llss_controller.find_line(cl_lhs);
+ logline* ll_rhs = this->llss_controller.find_line(cl_rhs);
+
+ return (*ll_lhs) < (*ll_rhs);
+ }
+#if 0
+ bool operator()(const indexed_content &lhs, const indexed_content &rhs)
+ {
+ logline *ll_lhs = this->llss_controller.find_line(lhs.ic_value);
+ logline *ll_rhs = this->llss_controller.find_line(rhs.ic_value);
+
+ return (*ll_lhs) < (*ll_rhs);
+ }
+#endif
+
+ bool operator()(const content_line_t& lhs, const time_t& rhs) const
+ {
+ logline* ll_lhs = this->llss_controller.find_line(lhs);
+
+ return *ll_lhs < rhs;
+ }
+
+ bool operator()(const content_line_t& lhs,
+ const struct timeval& rhs) const
+ {
+ logline* ll_lhs = this->llss_controller.find_line(lhs);
+
+ return *ll_lhs < rhs;
+ }
+
+ logfile_sub_source& llss_controller;
+ };
+
+ struct filtered_logline_cmp {
+ filtered_logline_cmp(const logfile_sub_source& lc) : llss_controller(lc)
+ {
+ }
+
+ bool operator()(const uint32_t& lhs, const uint32_t& rhs) const
+ {
+ content_line_t cl_lhs
+ = (content_line_t) llss_controller.lss_index[lhs];
+ content_line_t cl_rhs
+ = (content_line_t) llss_controller.lss_index[rhs];
+ logline* ll_lhs = this->llss_controller.find_line(cl_lhs);
+ logline* ll_rhs = this->llss_controller.find_line(cl_rhs);
+
+ return (*ll_lhs) < (*ll_rhs);
+ }
+
+ bool operator()(const uint32_t& lhs, const struct timeval& rhs) const
+ {
+ content_line_t cl_lhs
+ = (content_line_t) llss_controller.lss_index[lhs];
+ logline* ll_lhs = this->llss_controller.find_line(cl_lhs);
+
+ return (*ll_lhs) < rhs;
+ }
+
+ const logfile_sub_source& llss_controller;
+ };
+
+ /**
+ * Functor for comparing the ld_file field of the logfile_data struct.
+ */
+ struct logfile_data_eq {
+ explicit logfile_data_eq(std::shared_ptr<logfile> lf)
+ : lde_file(std::move(lf))
+ {
+ }
+
+ bool operator()(const std::unique_ptr<logfile_data>& ld) const
+ {
+ return this->lde_file == ld->get_file();
+ }
+
+ std::shared_ptr<logfile> lde_file;
+ };
+
+ void clear_line_size_cache()
+ {
+ this->lss_line_size_cache.fill(std::make_pair(0, 0));
+ this->lss_line_size_cache[0].first = -1;
+ }
+
+ bool check_extra_filters(iterator ld, logfile::iterator ll);
+
+ size_t lss_basename_width = 0;
+ size_t lss_filename_width = 0;
+ unsigned long lss_flags{0};
+ bool lss_force_rebuild{false};
+ std::vector<std::unique_ptr<logfile_data>> lss_files;
+
+ big_array<indexed_content> lss_index;
+ std::vector<uint32_t> lss_filtered_index;
+ auto_mem<sqlite3_stmt> lss_preview_filter_stmt{sqlite3_finalize};
+
+ bookmarks<content_line_t>::type lss_user_marks;
+ auto_mem<sqlite3_stmt> lss_marker_stmt{sqlite3_finalize};
+ std::string lss_marker_stmt_text;
+
+ line_flags_t lss_token_flags{0};
+ iterator lss_token_file_data;
+ std::shared_ptr<logfile> lss_token_file;
+ std::string lss_token_value;
+ string_attrs_t lss_token_attrs;
+ lnav::document::metadata lss_token_meta;
+ int lss_token_meta_line{-1};
+ int lss_token_meta_size{0};
+ logline_value_vector lss_token_values;
+ int lss_token_shift_start{0};
+ int lss_token_shift_size{0};
+ shared_buffer lss_share_manager;
+ logfile::iterator lss_token_line;
+ std::array<std::pair<int, size_t>, LINE_SIZE_CACHE_SIZE>
+ lss_line_size_cache;
+ log_level_t lss_min_log_level{LEVEL_UNKNOWN};
+ struct timeval lss_min_log_time {
+ 0, 0
+ };
+ struct timeval lss_max_log_time {
+ std::numeric_limits<time_t>::max(), 0
+ };
+ bool lss_marked_only{false};
+ index_delegate* lss_index_delegate{nullptr};
+ size_t lss_longest_line{0};
+ meta_grepper lss_meta_grepper;
+ log_location_history lss_location_history;
+ exec_context* lss_exec_context{nullptr};
+
+ bool lss_in_value_for_line{false};
+ bool lss_line_meta_changed{false};
+};
+
+#endif
diff --git a/src/mapbox/optional.hpp b/src/mapbox/optional.hpp
new file mode 100644
index 0000000..d84705c
--- /dev/null
+++ b/src/mapbox/optional.hpp
@@ -0,0 +1,74 @@
+#ifndef MAPBOX_UTIL_OPTIONAL_HPP
+#define MAPBOX_UTIL_OPTIONAL_HPP
+
+#pragma message("This implementation of optional is deprecated. See https://github.com/mapbox/variant/issues/64.")
+
+#include <type_traits>
+#include <utility>
+
+#include <mapbox/variant.hpp>
+
+namespace mapbox {
+namespace util {
+
+template <typename T>
+class optional
+{
+ static_assert(!std::is_reference<T>::value, "optional doesn't support references");
+
+ struct none_type
+ {
+ };
+
+ variant<none_type, T> variant_;
+
+public:
+ optional() = default;
+
+ optional(optional const& rhs)
+ {
+ if (this != &rhs)
+ { // protect against invalid self-assignment
+ variant_ = rhs.variant_;
+ }
+ }
+
+ optional(T const& v) { variant_ = v; }
+
+ explicit operator bool() const noexcept { return variant_.template is<T>(); }
+
+ T const& get() const { return variant_.template get<T>(); }
+ T& get() { return variant_.template get<T>(); }
+
+ T const& operator*() const { return this->get(); }
+ T operator*() { return this->get(); }
+
+ optional& operator=(T const& v)
+ {
+ variant_ = v;
+ return *this;
+ }
+
+ optional& operator=(optional const& rhs)
+ {
+ if (this != &rhs)
+ {
+ variant_ = rhs.variant_;
+ }
+ return *this;
+ }
+
+ template <typename... Args>
+ void emplace(Args&&... args)
+ {
+ variant_ = T{std::forward<Args>(args)...};
+ }
+
+ void reset() { variant_ = none_type{}; }
+
+}; // class optional
+
+} // namespace util
+} // namespace mapbox
+
+#endif // MAPBOX_UTIL_OPTIONAL_HPP
diff --git a/src/mapbox/recursive_wrapper.hpp b/src/mapbox/recursive_wrapper.hpp
new file mode 100644
index 0000000..4ffcbd7
--- /dev/null
+++ b/src/mapbox/recursive_wrapper.hpp
@@ -0,0 +1,122 @@
+#ifndef MAPBOX_UTIL_RECURSIVE_WRAPPER_HPP
+#define MAPBOX_UTIL_RECURSIVE_WRAPPER_HPP
+
+// Based on variant/recursive_wrapper.hpp from boost.
+//
+// Original license:
+//
+// Copyright (c) 2002-2003
+// Eric Friedman, Itay Maman
+//
+// Distributed under the Boost Software License, Version 1.0. (See
+// accompanying file LICENSE_1_0.txt or copy at
+// http://www.boost.org/LICENSE_1_0.txt)
+
+#include <cassert>
+#include <utility>
+
+namespace mapbox {
+namespace util {
+
+template <typename T>
+class recursive_wrapper
+{
+
+ T* p_;
+
+ void assign(T const& rhs)
+ {
+ this->get() = rhs;
+ }
+
+public:
+ using type = T;
+
+ /**
+ * Default constructor default initializes the internally stored value.
+ * For POD types this means nothing is done and the storage is
+ * uninitialized.
+ *
+ * @throws std::bad_alloc if there is insufficient memory for an object
+ * of type T.
+ * @throws any exception thrown by the default constructur of T.
+ */
+ recursive_wrapper()
+ : p_(new T){}
+
+ ~recursive_wrapper() noexcept { delete p_; }
+
+ recursive_wrapper(recursive_wrapper const& operand)
+ : p_(new T(operand.get())) {}
+
+ recursive_wrapper(T const& operand)
+ : p_(new T(operand)) {}
+
+ recursive_wrapper(recursive_wrapper&& operand)
+ : p_(new T(std::move(operand.get()))) {}
+
+ recursive_wrapper(T&& operand)
+ : p_(new T(std::move(operand))) {}
+
+ inline recursive_wrapper& operator=(recursive_wrapper const& rhs)
+ {
+ assign(rhs.get());
+ return *this;
+ }
+
+ inline recursive_wrapper& operator=(T const& rhs)
+ {
+ assign(rhs);
+ return *this;
+ }
+
+ inline void swap(recursive_wrapper& operand) noexcept
+ {
+ T* temp = operand.p_;
+ operand.p_ = p_;
+ p_ = temp;
+ }
+
+ recursive_wrapper& operator=(recursive_wrapper&& rhs) noexcept
+ {
+ swap(rhs);
+ return *this;
+ }
+
+ recursive_wrapper& operator=(T&& rhs)
+ {
+ get() = std::move(rhs);
+ return *this;
+ }
+
+ T& get()
+ {
+ assert(p_);
+ return *get_pointer();
+ }
+
+ T const& get() const
+ {
+ assert(p_);
+ return *get_pointer();
+ }
+
+ T* get_pointer() { return p_; }
+
+ const T* get_pointer() const { return p_; }
+
+ operator T const&() const { return this->get(); }
+
+ operator T&() { return this->get(); }
+
+}; // class recursive_wrapper
+
+template <typename T>
+inline void swap(recursive_wrapper<T>& lhs, recursive_wrapper<T>& rhs) noexcept
+{
+ lhs.swap(rhs);
+}
+} // namespace util
+} // namespace mapbox
+
+#endif // MAPBOX_UTIL_RECURSIVE_WRAPPER_HPP
diff --git a/src/mapbox/variant.hpp b/src/mapbox/variant.hpp
new file mode 100644
index 0000000..06a46ab
--- /dev/null
+++ b/src/mapbox/variant.hpp
@@ -0,0 +1,1053 @@
+#ifndef MAPBOX_UTIL_VARIANT_HPP
+#define MAPBOX_UTIL_VARIANT_HPP
+
+#include <cassert>
+#include <cstddef> // size_t
+#include <new> // operator new
+#include <stdexcept> // runtime_error
+#include <string>
+#include <tuple>
+#include <type_traits>
+#include <typeinfo>
+#include <utility>
+#include <functional>
+#include <limits>
+
+#include <mapbox/recursive_wrapper.hpp>
+#include <mapbox/variant_visitor.hpp>
+
+// clang-format off
+// [[deprecated]] is only available in C++14, use this for the time being
+#if __cplusplus <= 201103L
+# ifdef __GNUC__
+# define MAPBOX_VARIANT_DEPRECATED __attribute__((deprecated))
+# elif defined(_MSC_VER)
+# define MAPBOX_VARIANT_DEPRECATED __declspec(deprecated)
+# else
+# define MAPBOX_VARIANT_DEPRECATED
+# endif
+#else
+# define MAPBOX_VARIANT_DEPRECATED [[deprecated]]
+#endif
+
+
+#ifdef _MSC_VER
+// https://msdn.microsoft.com/en-us/library/bw1hbe6y.aspx
+# ifdef NDEBUG
+# define VARIANT_INLINE __forceinline
+# else
+# define VARIANT_INLINE //__declspec(noinline)
+# endif
+#else
+# ifdef NDEBUG
+# define VARIANT_INLINE //inline __attribute__((always_inline))
+# else
+# define VARIANT_INLINE __attribute__((noinline))
+# endif
+#endif
+// clang-format on
+
+// Exceptions
+#if defined( __EXCEPTIONS) || defined( _MSC_VER)
+#define HAS_EXCEPTIONS
+#endif
+
+#define VARIANT_MAJOR_VERSION 1
+#define VARIANT_MINOR_VERSION 1
+#define VARIANT_PATCH_VERSION 0
+
+#define VARIANT_VERSION (VARIANT_MAJOR_VERSION * 100000) + (VARIANT_MINOR_VERSION * 100) + (VARIANT_PATCH_VERSION)
+
+namespace mapbox {
+namespace util {
+
+// XXX This should derive from std::logic_error instead of std::runtime_error.
+// See https://github.com/mapbox/variant/issues/48 for details.
+class bad_variant_access : public std::runtime_error
+{
+
+public:
+ explicit bad_variant_access(const std::string& what_arg)
+ : runtime_error(what_arg) {}
+
+ explicit bad_variant_access(const char* what_arg)
+ : runtime_error(what_arg) {}
+
+}; // class bad_variant_access
+
+#if !defined(MAPBOX_VARIANT_MINIMIZE_SIZE)
+using type_index_t = unsigned int;
+#else
+#if defined(MAPBOX_VARIANT_OPTIMIZE_FOR_SPEED)
+using type_index_t = std::uint_fast8_t;
+#else
+using type_index_t = std::uint_least8_t;
+#endif
+#endif
+
+namespace detail {
+
+static constexpr type_index_t invalid_value = type_index_t(-1);
+
+template <typename T, typename... Types>
+struct direct_type;
+
+template <typename T, typename First, typename... Types>
+struct direct_type<T, First, Types...>
+{
+ static constexpr type_index_t index = std::is_same<T, First>::value
+ ? sizeof...(Types)
+ : direct_type<T, Types...>::index;
+};
+
+template <typename T>
+struct direct_type<T>
+{
+ static constexpr type_index_t index = invalid_value;
+};
+
+#if __cpp_lib_logical_traits >= 201510L
+
+using std::conjunction;
+using std::disjunction;
+
+#else
+
+template <typename...>
+struct conjunction : std::true_type {};
+
+template <typename B1>
+struct conjunction<B1> : B1 {};
+
+template <typename B1, typename B2>
+struct conjunction<B1, B2> : std::conditional<B1::value, B2, B1>::type {};
+
+template <typename B1, typename... Bs>
+struct conjunction<B1, Bs...> : std::conditional<B1::value, conjunction<Bs...>, B1>::type {};
+
+template <typename...>
+struct disjunction : std::false_type {};
+
+template <typename B1>
+struct disjunction<B1> : B1 {};
+
+template <typename B1, typename B2>
+struct disjunction<B1, B2> : std::conditional<B1::value, B1, B2>::type {};
+
+template <typename B1, typename... Bs>
+struct disjunction<B1, Bs...> : std::conditional<B1::value, B1, disjunction<Bs...>>::type {};
+
+#endif
+
+template <typename T, typename... Types>
+struct convertible_type;
+
+template <typename T, typename First, typename... Types>
+struct convertible_type<T, First, Types...>
+{
+ static constexpr type_index_t index = std::is_convertible<T, First>::value
+ ? disjunction<std::is_convertible<T, Types>...>::value ? invalid_value : sizeof...(Types)
+ : convertible_type<T, Types...>::index;
+};
+
+template <typename T>
+struct convertible_type<T>
+{
+ static constexpr type_index_t index = invalid_value;
+};
+
+template <typename T, typename... Types>
+struct value_traits
+{
+ using value_type = typename std::remove_const<typename std::remove_reference<T>::type>::type;
+ using value_type_wrapper = recursive_wrapper<value_type>;
+ static constexpr type_index_t direct_index = direct_type<value_type, Types...>::index;
+ static constexpr bool is_direct = direct_index != invalid_value;
+ static constexpr type_index_t index_direct_or_wrapper = is_direct ? direct_index : direct_type<value_type_wrapper, Types...>::index;
+ static constexpr bool is_direct_or_wrapper = index_direct_or_wrapper != invalid_value;
+ static constexpr type_index_t index = is_direct_or_wrapper ? index_direct_or_wrapper : convertible_type<value_type, Types...>::index;
+ static constexpr bool is_valid = index != invalid_value;
+ static constexpr type_index_t tindex = is_valid ? sizeof...(Types)-index : 0;
+ using target_type = typename std::tuple_element<tindex, std::tuple<void, Types...>>::type;
+};
+
+template <typename Src, typename Dest>
+struct copy_cvref
+{
+ using type = Dest;
+};
+
+template <typename Src, typename Dest>
+struct copy_cvref<Src const&, Dest>
+{
+ using type = Dest const&;
+};
+
+template <typename Src, typename Dest>
+struct copy_cvref<Src&, Dest>
+{
+ using type = Dest&;
+};
+
+template <typename Src, typename Dest>
+struct copy_cvref<Src&&, Dest>
+{
+ using type = Dest&&;
+};
+
+template <typename F, typename = void>
+struct deduced_result_type
+{};
+
+template <typename F, typename... Args>
+struct deduced_result_type<F(Args...), decltype((void)std::declval<F>()(std::declval<Args>()...))>
+{
+ using type = decltype(std::declval<F>()(std::declval<Args>()...));
+};
+
+template <typename F, typename = void>
+struct visitor_result_type : deduced_result_type<F>
+{};
+
+// specialization for explicit result_type member in visitor class
+template <typename F, typename... Args>
+struct visitor_result_type<F(Args...), decltype((void)std::declval<typename std::decay<F>::type::result_type>())>
+{
+ using type = typename std::decay<F>::type::result_type;
+};
+
+template <typename F, typename T>
+using result_of_unary_visit = typename visitor_result_type<F&&(T&&)>::type;
+
+template <typename F, typename T>
+using result_of_binary_visit = typename visitor_result_type<F&&(T&&, T&&)>::type;
+
+template <type_index_t arg1, type_index_t... others>
+struct static_max;
+
+template <type_index_t arg>
+struct static_max<arg>
+{
+ static const type_index_t value = arg;
+};
+
+template <type_index_t arg1, type_index_t arg2, type_index_t... others>
+struct static_max<arg1, arg2, others...>
+{
+ static const type_index_t value = arg1 >= arg2 ? static_max<arg1, others...>::value : static_max<arg2, others...>::value;
+};
+
+template <typename... Types>
+struct variant_helper;
+
+template <typename T, typename... Types>
+struct variant_helper<T, Types...>
+{
+ VARIANT_INLINE static void destroy(const type_index_t type_index, void* data)
+ {
+ if (type_index == sizeof...(Types))
+ {
+ reinterpret_cast<T*>(data)->~T();
+ }
+ else
+ {
+ variant_helper<Types...>::destroy(type_index, data);
+ }
+ }
+
+ VARIANT_INLINE static void move(const type_index_t old_type_index, void* old_value, void* new_value)
+ {
+ if (old_type_index == sizeof...(Types))
+ {
+ new (new_value) T(std::move(*reinterpret_cast<T*>(old_value)));
+ }
+ else
+ {
+ variant_helper<Types...>::move(old_type_index, old_value, new_value);
+ }
+ }
+
+ VARIANT_INLINE static void copy(const type_index_t old_type_index, const void* old_value, void* new_value)
+ {
+ if (old_type_index == sizeof...(Types))
+ {
+ new (new_value) T(*reinterpret_cast<const T*>(old_value));
+ }
+ else
+ {
+ variant_helper<Types...>::copy(old_type_index, old_value, new_value);
+ }
+ }
+};
+
+template <>
+struct variant_helper<>
+{
+ VARIANT_INLINE static void destroy(const type_index_t, void*) {}
+ VARIANT_INLINE static void move(const type_index_t, void*, void*) {}
+ VARIANT_INLINE static void copy(const type_index_t, const void*, void*) {}
+};
+
+template <typename T>
+struct unwrapper
+{
+ using value_type = T;
+
+ template <typename V>
+ static auto apply(typename std::remove_reference<V>::type& var)
+ -> typename std::enable_if<std::is_lvalue_reference<V>::value,
+ decltype(var.template get_unchecked<T>())>::type
+ {
+ return var.template get_unchecked<T>();
+ }
+
+ template <typename V>
+ static auto apply(typename std::remove_reference<V>::type& var)
+ -> typename std::enable_if<!std::is_lvalue_reference<V>::value,
+ decltype(std::move(var.template get_unchecked<T>()))>::type
+ {
+ return std::move(var.template get_unchecked<T>());
+ }
+};
+
+template <typename T>
+struct unwrapper<recursive_wrapper<T>> : unwrapper<T>
+{};
+
+template <typename T>
+struct unwrapper<std::reference_wrapper<T>> : unwrapper<T>
+{};
+
+template <typename R, typename... Types>
+struct dispatcher;
+
+template <typename R, typename T, typename... Types>
+struct dispatcher<R, T, Types...>
+{
+ template <typename V, typename F>
+ VARIANT_INLINE static R apply(V&& v, F&& f)
+ {
+ if (v.template is<T>())
+ {
+ return std::forward<F>(f)(unwrapper<T>::template apply<V>(v));
+ }
+ else
+ {
+ return dispatcher<R, Types...>::apply(std::forward<V>(v), std::forward<F>(f));
+ }
+ }
+};
+
+template <typename R, typename T>
+struct dispatcher<R, T>
+{
+ template <typename V, typename F>
+ VARIANT_INLINE static R apply(V&& v, F&& f)
+ {
+ return std::forward<F>(f)(unwrapper<T>::template apply<V>(v));
+ }
+};
+
+template <typename R, typename T, typename... Types>
+struct binary_dispatcher_rhs;
+
+template <typename R, typename T0, typename T1, typename... Types>
+struct binary_dispatcher_rhs<R, T0, T1, Types...>
+{
+ template <typename V, typename F>
+ VARIANT_INLINE static R apply(V&& lhs, V&& rhs, F&& f)
+ {
+ if (rhs.template is<T1>()) // call binary functor
+ {
+ return std::forward<F>(f)(unwrapper<T0>::template apply<V>(lhs),
+ unwrapper<T1>::template apply<V>(rhs));
+ }
+ else
+ {
+ return binary_dispatcher_rhs<R, T0, Types...>::apply(std::forward<V>(lhs),
+ std::forward<V>(rhs),
+ std::forward<F>(f));
+ }
+ }
+};
+
+template <typename R, typename T0, typename T1>
+struct binary_dispatcher_rhs<R, T0, T1>
+{
+ template <typename V, typename F>
+ VARIANT_INLINE static R apply(V&& lhs, V&& rhs, F&& f)
+ {
+ return std::forward<F>(f)(unwrapper<T0>::template apply<V>(lhs),
+ unwrapper<T1>::template apply<V>(rhs));
+ }
+};
+
+template <typename R, typename T, typename... Types>
+struct binary_dispatcher_lhs;
+
+template <typename R, typename T0, typename T1, typename... Types>
+struct binary_dispatcher_lhs<R, T0, T1, Types...>
+{
+ template <typename V, typename F>
+ VARIANT_INLINE static R apply(V&& lhs, V&& rhs, F&& f)
+ {
+ if (lhs.template is<T1>()) // call binary functor
+ {
+ return std::forward<F>(f)(unwrapper<T1>::template apply<V>(lhs),
+ unwrapper<T0>::template apply<V>(rhs));
+ }
+ else
+ {
+ return binary_dispatcher_lhs<R, T0, Types...>::apply(std::forward<V>(lhs),
+ std::forward<V>(rhs),
+ std::forward<F>(f));
+ }
+ }
+};
+
+template <typename R, typename T0, typename T1>
+struct binary_dispatcher_lhs<R, T0, T1>
+{
+ template <typename V, typename F>
+ VARIANT_INLINE static R apply(V&& lhs, V&& rhs, F&& f)
+ {
+ return std::forward<F>(f)(unwrapper<T1>::template apply<V>(lhs),
+ unwrapper<T0>::template apply<V>(rhs));
+ }
+};
+
+template <typename R, typename... Types>
+struct binary_dispatcher;
+
+template <typename R, typename T, typename... Types>
+struct binary_dispatcher<R, T, Types...>
+{
+ template <typename V, typename F>
+ VARIANT_INLINE static R apply(V&& v0, V&& v1, F&& f)
+ {
+ if (v0.template is<T>())
+ {
+ if (v1.template is<T>())
+ {
+ return std::forward<F>(f)(unwrapper<T>::template apply<V>(v0),
+ unwrapper<T>::template apply<V>(v1)); // call binary functor
+ }
+ else
+ {
+ return binary_dispatcher_rhs<R, T, Types...>::apply(std::forward<V>(v0),
+ std::forward<V>(v1),
+ std::forward<F>(f));
+ }
+ }
+ else if (v1.template is<T>())
+ {
+ return binary_dispatcher_lhs<R, T, Types...>::apply(std::forward<V>(v0),
+ std::forward<V>(v1),
+ std::forward<F>(f));
+ }
+ return binary_dispatcher<R, Types...>::apply(std::forward<V>(v0),
+ std::forward<V>(v1),
+ std::forward<F>(f));
+ }
+};
+
+template <typename R, typename T>
+struct binary_dispatcher<R, T>
+{
+ template <typename V, typename F>
+ VARIANT_INLINE static R apply(V&& v0, V&& v1, F&& f)
+ {
+ return std::forward<F>(f)(unwrapper<T>::template apply<V>(v0),
+ unwrapper<T>::template apply<V>(v1)); // call binary functor
+ }
+};
+
+// comparator functors
+struct equal_comp
+{
+ template <typename T>
+ bool operator()(T const& lhs, T const& rhs) const
+ {
+ return lhs == rhs;
+ }
+};
+
+struct less_comp
+{
+ template <typename T>
+ bool operator()(T const& lhs, T const& rhs) const
+ {
+ return lhs < rhs;
+ }
+};
+
+template <typename Variant, typename Comp>
+class comparer
+{
+public:
+ explicit comparer(Variant const& lhs) noexcept
+ : lhs_(lhs) {}
+ comparer& operator=(comparer const&) = delete;
+ // visitor
+ template <typename T>
+ bool operator()(T const& rhs_content) const
+ {
+ T const& lhs_content = lhs_.template get_unchecked<T>();
+ return Comp()(lhs_content, rhs_content);
+ }
+
+private:
+ Variant const& lhs_;
+};
+
+// hashing visitor
+struct hasher
+{
+ template <typename T>
+ std::size_t operator()(const T& hashable) const
+ {
+ return std::hash<T>{}(hashable);
+ }
+};
+
+} // namespace detail
+
+struct no_init {};
+
+template <typename... Types>
+class variant
+{
+ static_assert(sizeof...(Types) > 0, "Template parameter type list of variant can not be empty.");
+ static_assert(!detail::disjunction<std::is_reference<Types>...>::value, "Variant can not hold reference types. Maybe use std::reference_wrapper?");
+ static_assert(!detail::disjunction<std::is_array<Types>...>::value, "Variant can not hold array types.");
+ static_assert(sizeof...(Types) < std::numeric_limits<type_index_t>::max(), "Internal index type must be able to accommodate all alternatives.");
+private:
+ static const std::size_t data_size = detail::static_max<sizeof(Types)...>::value;
+ static const std::size_t data_align = detail::static_max<alignof(Types)...>::value;
+public:
+ struct adapted_variant_tag;
+ using types = std::tuple<Types...>;
+private:
+ using first_type = typename std::tuple_element<0, types>::type;
+ using unwrap_first_type = typename detail::unwrapper<first_type>::value_type;
+ using data_type = typename std::aligned_storage<data_size, data_align>::type;
+ using helper_type = detail::variant_helper<Types...>;
+
+ template <typename V, typename T = unwrap_first_type>
+ using alternative_ref = typename detail::copy_cvref<V, T>::type;
+
+ type_index_t type_index;
+#ifdef __clang_analyzer__
+ data_type data {};
+#else
+ data_type data;
+#endif
+
+public:
+ VARIANT_INLINE variant() noexcept(std::is_nothrow_default_constructible<first_type>::value)
+ : type_index(sizeof...(Types)-1)
+ {
+ static_assert(std::is_default_constructible<first_type>::value, "First type in variant must be default constructible to allow default construction of variant.");
+ new (&data) first_type();
+ }
+
+ VARIANT_INLINE variant(no_init) noexcept
+ : type_index(detail::invalid_value) {}
+
+ // http://isocpp.org/blog/2012/11/universal-references-in-c11-scott-meyers
+ template <typename T, typename Traits = detail::value_traits<T, Types...>,
+ typename Enable = typename std::enable_if<Traits::is_valid && !std::is_same<variant<Types...>, typename Traits::value_type>::value>::type >
+ VARIANT_INLINE variant(T&& val) noexcept(std::is_nothrow_constructible<typename Traits::target_type, T&&>::value)
+ : type_index(Traits::index)
+ {
+ new (&data) typename Traits::target_type(std::forward<T>(val));
+ }
+
+ VARIANT_INLINE variant(variant<Types...> const& old)
+ : type_index(old.type_index)
+ {
+ helper_type::copy(old.type_index, &old.data, &data);
+ }
+
+ VARIANT_INLINE variant(variant<Types...>&& old)
+ noexcept(detail::conjunction<std::is_nothrow_move_constructible<Types>...>::value)
+ : type_index(old.type_index)
+ {
+ helper_type::move(old.type_index, &old.data, &data);
+ }
+
+private:
+ VARIANT_INLINE void copy_assign(variant<Types...> const& rhs)
+ {
+ helper_type::destroy(type_index, &data);
+ type_index = detail::invalid_value;
+ helper_type::copy(rhs.type_index, &rhs.data, &data);
+ type_index = rhs.type_index;
+ }
+
+ VARIANT_INLINE void move_assign(variant<Types...>&& rhs)
+ {
+ helper_type::destroy(type_index, &data);
+ type_index = detail::invalid_value;
+ helper_type::move(rhs.type_index, &rhs.data, &data);
+ type_index = rhs.type_index;
+ }
+
+public:
+ VARIANT_INLINE variant<Types...>& operator=(variant<Types...>&& other)
+ // note we check for nothrow-constructible, not nothrow-assignable, since
+ // move_assign uses move-construction via placement new.
+ noexcept(detail::conjunction<std::is_nothrow_move_constructible<Types>...>::value)
+ {
+ if (this == &other) { // playing safe in release mode, hit assertion in debug.
+ assert(false);
+ return *this;
+ }
+ move_assign(std::move(other));
+ return *this;
+ }
+
+ VARIANT_INLINE variant<Types...>& operator=(variant<Types...> const& other)
+ {
+ if (this != &other)
+ copy_assign(other);
+ return *this;
+ }
+
+ // conversions
+ // move-assign
+ template <typename T, typename Traits = detail::value_traits<T, Types...>,
+ typename Enable = typename std::enable_if<Traits::is_valid && !std::is_same<variant<Types...>, typename Traits::value_type>::value>::type >
+ VARIANT_INLINE variant<Types...>& operator=(T&& rhs)
+ // not that we check is_nothrow_constructible<T>, not is_nothrow_move_assignable<T>,
+ // since we construct a temporary
+ noexcept(std::is_nothrow_constructible<typename Traits::target_type, T&&>::value
+ && std::is_nothrow_move_assignable<variant<Types...>>::value)
+ {
+ variant<Types...> temp(std::forward<T>(rhs));
+ move_assign(std::move(temp));
+ return *this;
+ }
+
+ // copy-assign
+ template <typename T>
+ VARIANT_INLINE variant<Types...>& operator=(T const& rhs)
+ {
+ variant<Types...> temp(rhs);
+ copy_assign(temp);
+ return *this;
+ }
+
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<T, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE bool is() const
+ {
+ return type_index == detail::direct_type<T, Types...>::index;
+ }
+
+ template <typename T,typename std::enable_if<
+ (detail::direct_type<recursive_wrapper<T>, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE bool is() const
+ {
+ return type_index == detail::direct_type<recursive_wrapper<T>, Types...>::index;
+ }
+
+ VARIANT_INLINE bool valid() const
+ {
+ return type_index != detail::invalid_value;
+ }
+
+ template <typename T, typename... Args>
+ VARIANT_INLINE void set(Args&&... args)
+ {
+ helper_type::destroy(type_index, &data);
+ type_index = detail::invalid_value;
+ new (&data) T(std::forward<Args>(args)...);
+ type_index = detail::direct_type<T, Types...>::index;
+ }
+
+ // get_unchecked<T>()
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<T, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T& get_unchecked()
+ {
+ return *reinterpret_cast<T*>(&data);
+ }
+
+#ifdef HAS_EXCEPTIONS
+ // get<T>()
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<T, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T& get()
+ {
+ if (type_index == detail::direct_type<T, Types...>::index)
+ {
+ return *reinterpret_cast<T*>(&data);
+ }
+ else
+ {
+ throw bad_variant_access("in get<T>()");
+ }
+ }
+#endif
+
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<T, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T const& get_unchecked() const
+ {
+ return *reinterpret_cast<T const*>(&data);
+ }
+
+#ifdef HAS_EXCEPTIONS
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<T, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T const& get() const
+ {
+ if (type_index == detail::direct_type<T, Types...>::index)
+ {
+ return *reinterpret_cast<T const*>(&data);
+ }
+ else
+ {
+ throw bad_variant_access("in get<T>()");
+ }
+ }
+#endif
+
+ // get_unchecked<T>() - T stored as recursive_wrapper<T>
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<recursive_wrapper<T>, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T& get_unchecked()
+ {
+ return (*reinterpret_cast<recursive_wrapper<T>*>(&data)).get();
+ }
+
+#ifdef HAS_EXCEPTIONS
+ // get<T>() - T stored as recursive_wrapper<T>
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<recursive_wrapper<T>, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T& get()
+ {
+ if (type_index == detail::direct_type<recursive_wrapper<T>, Types...>::index)
+ {
+ return (*reinterpret_cast<recursive_wrapper<T>*>(&data)).get();
+ }
+ else
+ {
+ throw bad_variant_access("in get<T>()");
+ }
+ }
+#endif
+
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<recursive_wrapper<T>, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T const& get_unchecked() const
+ {
+ return (*reinterpret_cast<recursive_wrapper<T> const*>(&data)).get();
+ }
+
+#ifdef HAS_EXCEPTIONS
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<recursive_wrapper<T>, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T const& get() const
+ {
+ if (type_index == detail::direct_type<recursive_wrapper<T>, Types...>::index)
+ {
+ return (*reinterpret_cast<recursive_wrapper<T> const*>(&data)).get();
+ }
+ else
+ {
+ throw bad_variant_access("in get<T>()");
+ }
+ }
+#endif
+
+ // get_unchecked<T>() - T stored as std::reference_wrapper<T>
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<std::reference_wrapper<T>, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T& get_unchecked()
+ {
+ return (*reinterpret_cast<std::reference_wrapper<T>*>(&data)).get();
+ }
+
+#ifdef HAS_EXCEPTIONS
+ // get<T>() - T stored as std::reference_wrapper<T>
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<std::reference_wrapper<T>, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T& get()
+ {
+ if (type_index == detail::direct_type<std::reference_wrapper<T>, Types...>::index)
+ {
+ return (*reinterpret_cast<std::reference_wrapper<T>*>(&data)).get();
+ }
+ else
+ {
+ throw bad_variant_access("in get<T>()");
+ }
+ }
+#endif
+
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<std::reference_wrapper<T const>, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T const& get_unchecked() const
+ {
+ return (*reinterpret_cast<std::reference_wrapper<T const> const*>(&data)).get();
+ }
+
+#ifdef HAS_EXCEPTIONS
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<std::reference_wrapper<T const>, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE T const& get() const
+ {
+ if (type_index == detail::direct_type<std::reference_wrapper<T const>, Types...>::index)
+ {
+ return (*reinterpret_cast<std::reference_wrapper<T const> const*>(&data)).get();
+ }
+ else
+ {
+ throw bad_variant_access("in get<T>()");
+ }
+ }
+#endif
+
+ // This function is deprecated because it returns an internal index field.
+ // Use which() instead.
+ MAPBOX_VARIANT_DEPRECATED VARIANT_INLINE type_index_t get_type_index() const
+ {
+ return type_index;
+ }
+
+ VARIANT_INLINE int which() const noexcept
+ {
+ return static_cast<int>(sizeof...(Types) - type_index - 1);
+ }
+
+ template <typename T, typename std::enable_if<
+ (detail::direct_type<T, Types...>::index != detail::invalid_value)>::type* = nullptr>
+ VARIANT_INLINE static constexpr int which() noexcept
+ {
+ return static_cast<int>(sizeof...(Types)-detail::direct_type<T, Types...>::index - 1);
+ }
+
+ // visitor
+ // unary
+ template <typename F, typename V, typename T0 = alternative_ref<V>,
+ typename R = detail::result_of_unary_visit<F, T0>>
+ VARIANT_INLINE static R visit(V&& v, F&& f)
+ {
+ return detail::dispatcher<R, Types...>::apply(std::forward<V>(v), std::forward<F>(f));
+ }
+
+ // binary
+ template <typename F, typename V, typename T0 = alternative_ref<V>,
+ typename R = detail::result_of_binary_visit<F, T0>>
+ VARIANT_INLINE static R binary_visit(V&& v0, V&& v1, F&& f)
+ {
+ return detail::binary_dispatcher<R, Types...>::apply(std::forward<V>(v0),
+ std::forward<V>(v1),
+ std::forward<F>(f));
+ }
+
+ // match
+ // unary
+ template <typename... Fs>
+ auto VARIANT_INLINE match(Fs&&... fs) const&
+ -> decltype(variant::visit(*this, ::mapbox::util::make_visitor(std::forward<Fs>(fs)...)))
+ {
+ return variant::visit(*this, ::mapbox::util::make_visitor(std::forward<Fs>(fs)...));
+ }
+ // non-const
+ template <typename... Fs>
+ auto VARIANT_INLINE match(Fs&&... fs) &
+ -> decltype(variant::visit(*this, ::mapbox::util::make_visitor(std::forward<Fs>(fs)...)))
+ {
+ return variant::visit(*this, ::mapbox::util::make_visitor(std::forward<Fs>(fs)...));
+ }
+ template <typename... Fs>
+ auto VARIANT_INLINE match(Fs&&... fs) &&
+ -> decltype(variant::visit(std::move(*this), ::mapbox::util::make_visitor(std::forward<Fs>(fs)...)))
+ {
+ return variant::visit(std::move(*this), ::mapbox::util::make_visitor(std::forward<Fs>(fs)...));
+ }
+
+ ~variant() noexcept // no-throw destructor
+ {
+ helper_type::destroy(type_index, &data);
+ }
+
+ // comparison operators
+ // equality
+ VARIANT_INLINE bool operator==(variant const& rhs) const
+ {
+ assert(valid() && rhs.valid());
+ if (this->which() != rhs.which())
+ {
+ return false;
+ }
+ detail::comparer<variant, detail::equal_comp> visitor(*this);
+ return visit(rhs, visitor);
+ }
+
+ VARIANT_INLINE bool operator!=(variant const& rhs) const
+ {
+ return !(*this == rhs);
+ }
+
+ // less than
+ VARIANT_INLINE bool operator<(variant const& rhs) const
+ {
+ assert(valid() && rhs.valid());
+ if (this->which() != rhs.which())
+ {
+ return this->which() < rhs.which();
+ }
+ detail::comparer<variant, detail::less_comp> visitor(*this);
+ return visit(rhs, visitor);
+ }
+ VARIANT_INLINE bool operator>(variant const& rhs) const
+ {
+ return rhs < *this;
+ }
+ VARIANT_INLINE bool operator<=(variant const& rhs) const
+ {
+ return !(*this > rhs);
+ }
+ VARIANT_INLINE bool operator>=(variant const& rhs) const
+ {
+ return !(*this < rhs);
+ }
+};
+
+// unary visitor interface
+template <typename F, typename V>
+auto VARIANT_INLINE apply_visitor(F&& f, V&& v)
+ -> decltype(v.visit(std::forward<V>(v), std::forward<F>(f)))
+{
+ return v.visit(std::forward<V>(v), std::forward<F>(f));
+}
+
+// binary visitor interface
+template <typename F, typename V>
+auto VARIANT_INLINE apply_visitor(F&& f, V&& v0, V&& v1)
+ -> decltype(v0.binary_visit(std::forward<V>(v0), std::forward<V>(v1), std::forward<F>(f)))
+{
+ return v0.binary_visit(std::forward<V>(v0), std::forward<V>(v1), std::forward<F>(f));
+}
+
+// getter interface
+
+#ifdef HAS_EXCEPTIONS
+template <typename ResultType, typename T>
+auto get(T& var)->decltype(var.template get<ResultType>())
+{
+ return var.template get<ResultType>();
+}
+#endif
+
+template <typename ResultType, typename T>
+ResultType& get_unchecked(T& var)
+{
+ return var.template get_unchecked<ResultType>();
+}
+
+#ifdef HAS_EXCEPTIONS
+template <typename ResultType, typename T>
+auto get(T const& var)->decltype(var.template get<ResultType>())
+{
+ return var.template get<ResultType>();
+}
+#endif
+
+template <typename ResultType, typename T>
+ResultType const& get_unchecked(T const& var)
+{
+ return var.template get_unchecked<ResultType>();
+}
+// variant_size
+template <typename T>
+struct variant_size;
+
+//variable templates is c++14
+//template <typename T>
+//constexpr std::size_t variant_size_v = variant_size<T>::value;
+
+template <typename T>
+struct variant_size<const T>
+ : variant_size<T> {};
+
+template <typename T>
+struct variant_size<volatile T>
+ : variant_size<T> {};
+
+template <typename T>
+struct variant_size<const volatile T>
+ : variant_size<T> {};
+
+template <typename... Types>
+struct variant_size<variant<Types...>>
+ : std::integral_constant<std::size_t, sizeof...(Types)> {};
+
+// variant_alternative
+template <std::size_t Index, typename T>
+struct variant_alternative;
+
+#if defined(__clang__)
+#if __has_builtin(__type_pack_element)
+#define has_type_pack_element
+#endif
+#endif
+
+#if defined(has_type_pack_element)
+template <std::size_t Index, typename ...Types>
+struct variant_alternative<Index, variant<Types...>>
+{
+ static_assert(sizeof...(Types) > Index , "Index out of range");
+ using type = __type_pack_element<Index, Types...>;
+};
+#else
+template <std::size_t Index, typename First, typename...Types>
+struct variant_alternative<Index, variant<First, Types...>>
+ : variant_alternative<Index - 1, variant<Types...>>
+{
+ static_assert(sizeof...(Types) > Index -1 , "Index out of range");
+};
+
+template <typename First, typename...Types>
+struct variant_alternative<0, variant<First, Types...>>
+{
+ using type = First;
+};
+
+#endif
+
+template <size_t Index, typename T>
+using variant_alternative_t = typename variant_alternative<Index, T>::type;
+
+template <size_t Index, typename T>
+struct variant_alternative<Index, const T>
+ : std::add_const<variant_alternative<Index, T>> {};
+
+template <size_t Index, typename T>
+struct variant_alternative<Index, volatile T>
+ : std::add_volatile<variant_alternative<Index, T>> {};
+
+template <size_t Index, typename T>
+struct variant_alternative<Index, const volatile T>
+ : std::add_cv<variant_alternative<Index, T>> {};
+
+} // namespace util
+} // namespace mapbox
+
+// hashable iff underlying types are hashable
+namespace std {
+template <typename... Types>
+struct hash< ::mapbox::util::variant<Types...>> {
+ std::size_t operator()(const ::mapbox::util::variant<Types...>& v) const noexcept
+ {
+ return ::mapbox::util::apply_visitor(::mapbox::util::detail::hasher{}, v);
+ }
+};
+
+}
+
+#endif // MAPBOX_UTIL_VARIANT_HPP
diff --git a/src/mapbox/variant_cast.hpp b/src/mapbox/variant_cast.hpp
new file mode 100644
index 0000000..fe1ab35
--- /dev/null
+++ b/src/mapbox/variant_cast.hpp
@@ -0,0 +1,85 @@
+#ifndef VARIANT_CAST_HPP
+#define VARIANT_CAST_HPP
+
+#include <type_traits>
+
+namespace mapbox {
+namespace util {
+
+namespace detail {
+
+template <class T>
+class static_caster
+{
+public:
+ template <class V>
+ T& operator()(V& v) const
+ {
+ return static_cast<T&>(v);
+ }
+};
+
+template <class T>
+class dynamic_caster
+{
+public:
+ using result_type = T&;
+ template <class V>
+ T& operator()(V& v, typename std::enable_if<!std::is_polymorphic<V>::value>::type* = nullptr) const
+ {
+ throw std::bad_cast();
+ }
+ template <class V>
+ T& operator()(V& v, typename std::enable_if<std::is_polymorphic<V>::value>::type* = nullptr) const
+ {
+ return dynamic_cast<T&>(v);
+ }
+};
+
+template <class T>
+class dynamic_caster<T*>
+{
+public:
+ using result_type = T*;
+ template <class V>
+ T* operator()(V& v, typename std::enable_if<!std::is_polymorphic<V>::value>::type* = nullptr) const
+ {
+ return nullptr;
+ }
+ template <class V>
+ T* operator()(V& v, typename std::enable_if<std::is_polymorphic<V>::value>::type* = nullptr) const
+ {
+ return dynamic_cast<T*>(&v);
+ }
+};
+}
+
+template <class T, class V>
+typename detail::dynamic_caster<T>::result_type
+dynamic_variant_cast(V& v)
+{
+ return mapbox::util::apply_visitor(detail::dynamic_caster<T>(), v);
+}
+
+template <class T, class V>
+typename detail::dynamic_caster<const T>::result_type
+dynamic_variant_cast(const V& v)
+{
+ return mapbox::util::apply_visitor(detail::dynamic_caster<const T>(), v);
+}
+
+template <class T, class V>
+T& static_variant_cast(V& v)
+{
+ return mapbox::util::apply_visitor(detail::static_caster<T>(), v);
+}
+
+template <class T, class V>
+const T& static_variant_cast(const V& v)
+{
+ return mapbox::util::apply_visitor(detail::static_caster<const T>(), v);
+}
+}
+}
+
+#endif // VARIANT_CAST_HPP
diff --git a/src/mapbox/variant_io.hpp b/src/mapbox/variant_io.hpp
new file mode 100644
index 0000000..1456cc5
--- /dev/null
+++ b/src/mapbox/variant_io.hpp
@@ -0,0 +1,45 @@
+#ifndef MAPBOX_UTIL_VARIANT_IO_HPP
+#define MAPBOX_UTIL_VARIANT_IO_HPP
+
+#include <iosfwd>
+
+#include <mapbox/variant.hpp>
+
+namespace mapbox {
+namespace util {
+
+namespace detail {
+// operator<< helper
+template <typename Out>
+class printer
+{
+public:
+ explicit printer(Out& out)
+ : out_(out) {}
+ printer& operator=(printer const&) = delete;
+
+ // visitor
+ template <typename T>
+ void operator()(T const& operand) const
+ {
+ out_ << operand;
+ }
+
+private:
+ Out& out_;
+};
+}
+
+// operator<<
+template <typename CharT, typename Traits, typename... Types>
+VARIANT_INLINE std::basic_ostream<CharT, Traits>&
+operator<<(std::basic_ostream<CharT, Traits>& out, variant<Types...> const& rhs)
+{
+ detail::printer<std::basic_ostream<CharT, Traits>> visitor(out);
+ apply_visitor(visitor, rhs);
+ return out;
+}
+} // namespace util
+} // namespace mapbox
+
+#endif // MAPBOX_UTIL_VARIANT_IO_HPP
diff --git a/src/mapbox/variant_visitor.hpp b/src/mapbox/variant_visitor.hpp
new file mode 100644
index 0000000..54ddba0
--- /dev/null
+++ b/src/mapbox/variant_visitor.hpp
@@ -0,0 +1,43 @@
+#ifndef MAPBOX_UTIL_VARIANT_VISITOR_HPP
+#define MAPBOX_UTIL_VARIANT_VISITOR_HPP
+
+#include <utility>
+
+namespace mapbox {
+namespace util {
+
+template <typename... Fns>
+struct visitor;
+
+template <typename Fn>
+struct visitor<Fn> : Fn
+{
+ using Fn::operator();
+
+ template<typename T>
+ visitor(T&& fn) : Fn(std::forward<T>(fn)) {}
+};
+
+template <typename Fn, typename... Fns>
+struct visitor<Fn, Fns...> : Fn, visitor<Fns...>
+{
+ using Fn::operator();
+ using visitor<Fns...>::operator();
+
+ template<typename T, typename... Ts>
+ visitor(T&& fn, Ts&&... fns)
+ : Fn(std::forward<T>(fn))
+ , visitor<Fns...>(std::forward<Ts>(fns)...) {}
+};
+
+template <typename... Fns>
+visitor<typename std::decay<Fns>::type...> make_visitor(Fns&&... fns)
+{
+ return visitor<typename std::decay<Fns>::type...>
+ (std::forward<Fns>(fns)...);
+}
+
+} // namespace util
+} // namespace mapbox
+
+#endif // MAPBOX_UTIL_VARIANT_VISITOR_HPP
diff --git a/src/md2attr_line.cc b/src/md2attr_line.cc
new file mode 100644
index 0000000..a208616
--- /dev/null
+++ b/src/md2attr_line.cc
@@ -0,0 +1,635 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "md2attr_line.hh"
+
+#include "base/attr_line.builder.hh"
+#include "base/itertools.hh"
+#include "base/lnav_log.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "pugixml/pugixml.hpp"
+#include "readline_highlighters.hh"
+#include "view_curses.hh"
+
+using namespace lnav::roles::literals;
+
+void
+md2attr_line::flush_footnotes()
+{
+ if (this->ml_footnotes.empty()) {
+ return;
+ }
+
+ auto& block_text = this->ml_blocks.back();
+ auto longest_foot = this->ml_footnotes
+ | lnav::itertools::map(&attr_line_t::utf8_length_or_length)
+ | lnav::itertools::max(0);
+ size_t index = 1;
+
+ block_text.append("\n");
+ for (auto& foot : this->ml_footnotes) {
+ block_text.append(lnav::string::attrs::preformatted(" "))
+ .append("\u258c"_footnote_border)
+ .append(lnav::roles::footnote_text(
+ index < 10 && this->ml_footnotes.size() >= 10 ? " " : ""))
+ .append(lnav::roles::footnote_text(
+ fmt::format(FMT_STRING("[{}] - "), index)))
+ .append(foot.pad_to(longest_foot))
+ .append("\n");
+ index += 1;
+ }
+ this->ml_footnotes.clear();
+}
+
+Result<void, std::string>
+md2attr_line::enter_block(const md4cpp::event_handler::block& bl)
+{
+ if (this->ml_list_stack.empty()
+ && (bl.is<MD_BLOCK_H_DETAIL*>() || bl.is<block_hr>()
+ || bl.is<block_p>()))
+ {
+ this->flush_footnotes();
+ }
+
+ this->ml_blocks.resize(this->ml_blocks.size() + 1);
+ if (bl.is<MD_BLOCK_OL_DETAIL*>()) {
+ auto* ol_detail = bl.get<MD_BLOCK_OL_DETAIL*>();
+
+ this->ml_list_stack.emplace_back(*ol_detail);
+ } else if (bl.is<MD_BLOCK_UL_DETAIL*>()) {
+ this->ml_list_stack.emplace_back(bl.get<MD_BLOCK_UL_DETAIL*>());
+ } else if (bl.is<MD_BLOCK_TABLE_DETAIL*>()) {
+ this->ml_tables.resize(this->ml_tables.size() + 1);
+ } else if (bl.is<block_tr>()) {
+ this->ml_tables.back().t_rows.resize(
+ this->ml_tables.back().t_rows.size() + 1);
+ } else if (bl.is<MD_BLOCK_CODE_DETAIL*>()) {
+ this->ml_code_depth += 1;
+ }
+
+ return Ok();
+}
+
+Result<void, std::string>
+md2attr_line::leave_block(const md4cpp::event_handler::block& bl)
+{
+ auto block_text = std::move(this->ml_blocks.back());
+ this->ml_blocks.pop_back();
+
+ auto& last_block = this->ml_blocks.back();
+ if (!endswith(block_text.get_string(), "\n")) {
+ block_text.append("\n");
+ }
+ if (bl.is<MD_BLOCK_H_DETAIL*>()) {
+ auto* hbl = bl.get<MD_BLOCK_H_DETAIL*>();
+ auto role = role_t::VCR_TEXT;
+
+ switch (hbl->level) {
+ case 1:
+ role = role_t::VCR_H1;
+ break;
+ case 2:
+ role = role_t::VCR_H2;
+ break;
+ case 3:
+ role = role_t::VCR_H3;
+ break;
+ case 4:
+ role = role_t::VCR_H4;
+ break;
+ case 5:
+ role = role_t::VCR_H5;
+ break;
+ case 6:
+ role = role_t::VCR_H6;
+ break;
+ }
+ block_text.rtrim().with_attr_for_all(VC_ROLE.value(role));
+ last_block.append("\n").append(block_text).append("\n");
+ } else if (bl.is<block_hr>()) {
+ block_text = attr_line_t()
+ .append(lnav::roles::hr(repeat("\u2501", 70)))
+ .with_attr_for_all(SA_PREFORMATTED.value());
+ last_block.append("\n").append(block_text).append("\n");
+ } else if (bl.is<MD_BLOCK_UL_DETAIL*>() || bl.is<MD_BLOCK_OL_DETAIL*>()) {
+ this->ml_list_stack.pop_back();
+ if (last_block.empty()) {
+ last_block.append("\n");
+ } else {
+ if (!endswith(last_block.get_string(), "\n")) {
+ last_block.append("\n");
+ }
+ if (this->ml_list_stack.empty()
+ && !endswith(last_block.get_string(), "\n\n"))
+ {
+ last_block.append("\n");
+ }
+ }
+ last_block.append(block_text);
+ } else if (bl.is<MD_BLOCK_LI_DETAIL*>()) {
+ auto last_list_block = this->ml_list_stack.back();
+ text_wrap_settings tws = {0, 60};
+
+ attr_line_builder alb(last_block);
+ {
+ auto prefix = alb.with_attr(SA_PREFORMATTED.value());
+
+ alb.append(" ")
+ .append(last_list_block.match(
+ [this, &tws](const MD_BLOCK_UL_DETAIL*) {
+ tws.tws_indent = 3;
+ return this->ml_list_stack.size() % 2 == 1
+ ? "\u2022"_list_glyph
+ : "\u2014"_list_glyph;
+ },
+ [this, &tws](MD_BLOCK_OL_DETAIL ol_detail) {
+ auto retval = lnav::roles::list_glyph(
+ fmt::format(FMT_STRING("{}{}"),
+ ol_detail.start,
+ ol_detail.mark_delimiter));
+ tws.tws_indent = retval.first.length() + 2;
+
+ this->ml_list_stack.pop_back();
+ ol_detail.start += 1;
+ this->ml_list_stack.emplace_back(ol_detail);
+ return retval;
+ }))
+ .append(" ");
+ }
+
+ alb.append(block_text, &tws);
+ } else if (bl.is<MD_BLOCK_CODE_DETAIL*>()) {
+ auto* code_detail = bl.get<MD_BLOCK_CODE_DETAIL*>();
+
+ this->ml_code_depth -= 1;
+
+ auto lang_sf = string_fragment::from_bytes(code_detail->lang.text,
+ code_detail->lang.size);
+ if (lang_sf == "lnav") {
+ readline_lnav_highlighter(block_text, block_text.length());
+ } else if (lang_sf == "sql" || lang_sf == "sqlite") {
+ readline_sqlite_highlighter(block_text, block_text.length());
+ } else if (lang_sf == "shell" || lang_sf == "bash") {
+ readline_shlex_highlighter(block_text, block_text.length());
+ } else if (lang_sf == "console"
+ || lang_sf.iequal(
+ string_fragment::from_const("shellsession")))
+ {
+ static const auto SH_PROMPT
+ = lnav::pcre2pp::code::from_const(R"([^\$>#%]*[\$>#%]\s+)");
+
+ attr_line_t new_block_text;
+ attr_line_t cmd_block;
+ int prompt_size = 0;
+
+ for (auto line : block_text.split_lines()) {
+ if (!cmd_block.empty()
+ && endswith(cmd_block.get_string(), "\\\n"))
+ {
+ cmd_block.append(line).append("\n");
+ continue;
+ }
+
+ if (!cmd_block.empty()) {
+ readline_shlex_highlighter_int(
+ cmd_block,
+ cmd_block.length(),
+ line_range{prompt_size, (int) cmd_block.length()});
+ new_block_text.append(cmd_block);
+ cmd_block.clear();
+ }
+
+ auto sh_find_res
+ = SH_PROMPT.find_in(line.get_string()).ignore_error();
+
+ if (sh_find_res) {
+ prompt_size = sh_find_res->f_all.length();
+ line.with_attr(string_attr{
+ line_range{0, prompt_size},
+ VC_ROLE.value(role_t::VCR_LIST_GLYPH),
+ });
+ cmd_block.append(line).append("\n");
+ } else {
+ line.with_attr_for_all(VC_ROLE.value(role_t::VCR_COMMENT));
+ new_block_text.append(line).append("\n");
+ }
+ }
+ block_text = new_block_text;
+ }
+
+ auto code_lines = block_text.rtrim().split_lines();
+ auto max_width = code_lines
+ | lnav::itertools::map(&attr_line_t::utf8_length_or_length)
+ | lnav::itertools::max(0);
+ attr_line_t padded_text;
+
+ for (auto& line : code_lines) {
+ line.pad_to(std::max(max_width + 4, ssize_t{40}))
+ .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ padded_text.append(lnav::string::attrs::preformatted(" "))
+ .append("\u258c"_code_border)
+ .append(line)
+ .append("\n");
+ }
+ if (!padded_text.empty()) {
+ padded_text.with_attr_for_all(SA_PREFORMATTED.value());
+ last_block.append("\n").append(padded_text);
+ }
+ } else if (bl.is<block_quote>()) {
+ text_wrap_settings tws = {0, 60};
+ attr_line_t wrapped_text;
+
+ wrapped_text.append(block_text.rtrim(), &tws);
+ auto quoted_lines = wrapped_text.split_lines();
+ auto max_width = quoted_lines
+ | lnav::itertools::map(&attr_line_t::utf8_length_or_length)
+ | lnav::itertools::max(0);
+ attr_line_t padded_text;
+
+ for (auto& line : quoted_lines) {
+ line.pad_to(max_width + 1)
+ .with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_TEXT));
+ padded_text.append(" ")
+ .append("\u258c"_quote_border)
+ .append(line)
+ .append("\n");
+ }
+ if (!padded_text.empty()) {
+ padded_text.with_attr_for_all(SA_PREFORMATTED.value());
+ last_block.append("\n").append(padded_text);
+ }
+ } else if (bl.is<MD_BLOCK_TABLE_DETAIL*>()) {
+ auto* table_detail = bl.get<MD_BLOCK_TABLE_DETAIL*>();
+ auto tab = std::move(this->ml_tables.back());
+ this->ml_tables.pop_back();
+ std::vector<ssize_t> max_col_sizes;
+
+ block_text.clear();
+ block_text.append("\n");
+ max_col_sizes.resize(table_detail->col_count);
+ for (size_t lpc = 0; lpc < table_detail->col_count; lpc++) {
+ if (lpc < tab.t_headers.size()) {
+ max_col_sizes[lpc] = tab.t_headers[lpc].utf8_length_or_length();
+ tab.t_headers[lpc].with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_TABLE_HEADER));
+ }
+ }
+ for (const auto& row : tab.t_rows) {
+ for (size_t lpc = 0; lpc < table_detail->col_count; lpc++) {
+ if (lpc >= row.r_columns.size()) {
+ continue;
+ }
+ auto col_len = row.r_columns[lpc].utf8_length_or_length();
+ if (col_len > max_col_sizes[lpc]) {
+ max_col_sizes[lpc] = col_len;
+ }
+ }
+ }
+ auto col_sizes
+ = max_col_sizes | lnav::itertools::map([](const auto& elem) {
+ return std::min(elem, ssize_t{50});
+ });
+ auto full_width = col_sizes | lnav::itertools::sum();
+ text_wrap_settings tws = {0, 50};
+ std::vector<cell_lines> cells;
+ size_t max_cell_lines = 0;
+ for (size_t lpc = 0; lpc < tab.t_headers.size(); lpc++) {
+ tws.with_width(col_sizes[lpc]);
+
+ attr_line_t td_block;
+ td_block.append(tab.t_headers[lpc], &tws);
+ cells.emplace_back(td_block.rtrim().split_lines());
+ if (cells.back().cl_lines.size() > max_cell_lines) {
+ max_cell_lines = cells.back().cl_lines.size();
+ }
+ }
+ for (size_t line_index = 0; line_index < max_cell_lines; line_index++) {
+ size_t col = 0;
+ for (const auto& cell : cells) {
+ block_text.append(" ");
+ if (line_index < cell.cl_lines.size()) {
+ block_text.append(cell.cl_lines[line_index]);
+ block_text.append(
+ col_sizes[col]
+ - cell.cl_lines[line_index].utf8_length_or_length(),
+ ' ');
+ } else {
+ block_text.append(col_sizes[col], ' ');
+ }
+ col += 1;
+ }
+ block_text.append("\n")
+ .append(lnav::roles::table_border(
+ repeat("\u2550", full_width + col_sizes.size())))
+ .append("\n");
+ }
+ for (const auto& row : tab.t_rows) {
+ cells.clear();
+ max_cell_lines = 0;
+ for (size_t lpc = 0; lpc < row.r_columns.size(); lpc++) {
+ tws.with_width(col_sizes[lpc]);
+
+ attr_line_t td_block;
+ td_block.append(row.r_columns[lpc], &tws);
+ cells.emplace_back(td_block.rtrim().split_lines());
+ if (cells.back().cl_lines.size() > max_cell_lines) {
+ max_cell_lines = cells.back().cl_lines.size();
+ }
+ }
+ for (size_t line_index = 0; line_index < max_cell_lines;
+ line_index++)
+ {
+ size_t col = 0;
+ for (const auto& cell : cells) {
+ block_text.append(" ");
+ if (line_index < cell.cl_lines.size()) {
+ block_text.append(cell.cl_lines[line_index]);
+ if (col < col_sizes.size() - 1) {
+ block_text.append(
+ col_sizes[col]
+ - cell.cl_lines[line_index]
+ .utf8_length_or_length(),
+ ' ');
+ }
+ } else if (col < col_sizes.size() - 1) {
+ block_text.append(col_sizes[col], ' ');
+ }
+ col += 1;
+ }
+ block_text.append("\n");
+ }
+ }
+ if (!block_text.empty()) {
+ block_text.with_attr_for_all(SA_PREFORMATTED.value());
+ last_block.append(block_text);
+ }
+ } else if (bl.is<block_th>()) {
+ this->ml_tables.back().t_headers.push_back(block_text);
+ } else if (bl.is<MD_BLOCK_TD_DETAIL*>()) {
+ this->ml_tables.back().t_rows.back().r_columns.push_back(block_text);
+ } else {
+ if (bl.is<block_html>()) {
+ if (startswith(block_text.get_string(), "<!--")) {
+ return Ok();
+ }
+ }
+
+ text_wrap_settings tws = {0, this->ml_blocks.size() == 1 ? 70 : 10000};
+
+ if (!last_block.empty()) {
+ last_block.append("\n");
+ }
+ last_block.append(block_text, &tws);
+ }
+ if (bl.is<block_doc>()) {
+ this->flush_footnotes();
+ }
+ return Ok();
+}
+
+Result<void, std::string>
+md2attr_line::enter_span(const md4cpp::event_handler::span& sp)
+{
+ auto& last_block = this->ml_blocks.back();
+ this->ml_span_starts.push_back(last_block.length());
+ if (sp.is<span_code>()) {
+ last_block.append(" ");
+ this->ml_code_depth += 1;
+ }
+ return Ok();
+}
+
+Result<void, std::string>
+md2attr_line::leave_span(const md4cpp::event_handler::span& sp)
+{
+ auto& last_block = this->ml_blocks.back();
+ if (sp.is<span_code>()) {
+ this->ml_code_depth -= 1;
+ last_block.append(" ");
+ line_range lr{
+ static_cast<int>(this->ml_span_starts.back()),
+ static_cast<int>(last_block.length()),
+ };
+ last_block.with_attr({
+ lr,
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE),
+ });
+ last_block.with_attr({
+ lr,
+ SA_PREFORMATTED.value(),
+ });
+ } else if (sp.is<span_em>()) {
+ line_range lr{
+ static_cast<int>(this->ml_span_starts.back()),
+ static_cast<int>(last_block.length()),
+ };
+#if defined(A_ITALIC)
+ last_block.with_attr({
+ lr,
+ VC_STYLE.value(text_attrs{(int32_t) A_ITALIC}),
+ });
+#endif
+ } else if (sp.is<span_strong>()) {
+ line_range lr{
+ static_cast<int>(this->ml_span_starts.back()),
+ static_cast<int>(last_block.length()),
+ };
+ last_block.with_attr({
+ lr,
+ VC_STYLE.value(text_attrs{A_BOLD}),
+ });
+ } else if (sp.is<span_u>()) {
+ line_range lr{
+ static_cast<int>(this->ml_span_starts.back()),
+ static_cast<int>(last_block.length()),
+ };
+ last_block.with_attr({
+ lr,
+ VC_STYLE.value(text_attrs{A_UNDERLINE}),
+ });
+ } else if (sp.is<MD_SPAN_A_DETAIL*>()) {
+ auto* a_detail = sp.get<MD_SPAN_A_DETAIL*>();
+ auto href_str = std::string(a_detail->href.text, a_detail->href.size);
+
+ this->append_url_footnote(href_str);
+ } else if (sp.is<MD_SPAN_IMG_DETAIL*>()) {
+ auto* img_detail = sp.get<MD_SPAN_IMG_DETAIL*>();
+ auto src_str = std::string(img_detail->src.text, img_detail->src.size);
+
+ this->append_url_footnote(src_str);
+ }
+ this->ml_span_starts.pop_back();
+ return Ok();
+}
+
+Result<void, std::string>
+md2attr_line::text(MD_TEXTTYPE tt, const string_fragment& sf)
+{
+ static const auto& entity_map = md4cpp::get_xml_entity_map();
+ static const auto& vc = view_colors::singleton();
+
+ auto& last_block = this->ml_blocks.back();
+
+ switch (tt) {
+ case MD_TEXT_BR:
+ last_block.append("\n");
+ break;
+ case MD_TEXT_SOFTBR: {
+ if (!last_block.empty() && !isspace(last_block.get_string().back()))
+ {
+ last_block.append(" ");
+ }
+ break;
+ }
+ case MD_TEXT_ENTITY: {
+ auto xe_iter = entity_map.xem_entities.find(sf.to_string());
+
+ if (xe_iter != entity_map.xem_entities.end()) {
+ last_block.append(xe_iter->second.xe_chars);
+ }
+ break;
+ }
+ case MD_TEXT_HTML: {
+ last_block.append(sf);
+ if (sf.startswith("<span ")) {
+ this->ml_html_span_starts.push_back(last_block.length()
+ - sf.length());
+ } else if (sf == "</span>" && !this->ml_html_span_starts.empty()) {
+ std::string html_span = last_block.get_string().substr(
+ this->ml_html_span_starts.back());
+
+ pugi::xml_document doc;
+
+ auto load_res = doc.load_string(html_span.c_str());
+ if (load_res) {
+ auto span = doc.child("span");
+ if (span) {
+ auto styled_span = attr_line_t(span.text().get());
+
+ auto span_class = span.attribute("class");
+ if (span_class) {
+ auto cl_iter
+ = vc.vc_class_to_role.find(span_class.value());
+
+ if (cl_iter == vc.vc_class_to_role.end()) {
+ log_error("unknown span class: %s",
+ span_class.value());
+ } else {
+ styled_span.with_attr_for_all(cl_iter->second);
+ }
+ }
+ last_block.erase(this->ml_html_span_starts.back());
+ last_block.append(styled_span);
+ }
+ } else {
+ log_error("failed to parse: %s", load_res.description());
+ }
+ this->ml_html_span_starts.pop_back();
+ }
+ break;
+ }
+ default: {
+ static const auto REPL_RE = lnav::pcre2pp::code::from_const(
+ R"(-{2,3}|:[^:\s]*(?:::[^:\s]*)*:)");
+ static const auto& emojis = md4cpp::get_emoji_map();
+
+ if (this->ml_code_depth > 0) {
+ last_block.append(sf);
+ return Ok();
+ }
+
+ std::string span_text;
+
+ auto loop_res = REPL_RE.capture_from(sf).for_each(
+ [&span_text](lnav::pcre2pp::match_data& md) {
+ span_text += md.leading();
+
+ auto matched = *md[0];
+
+ if (matched == "--") {
+ span_text.append("\u2013");
+ } else if (matched == "---") {
+ span_text.append("\u2014");
+ } else if (matched.startswith(":")) {
+ auto em_iter = emojis.em_shortname2emoji.find(
+ matched.to_string());
+ if (em_iter == emojis.em_shortname2emoji.end()) {
+ span_text += matched;
+ } else {
+ span_text.append(em_iter->second.get().e_value);
+ }
+ }
+ });
+
+ if (loop_res.isOk()) {
+ span_text += loop_res.unwrap();
+ } else {
+ log_error("span replacement regex failed: %d",
+ loop_res.unwrapErr().e_error_code);
+ }
+
+ text_wrap_settings tws
+ = {0, this->ml_blocks.size() == 1 ? 70 : 10000};
+
+ last_block.append(span_text, &tws);
+ break;
+ }
+ }
+ return Ok();
+}
+
+void
+md2attr_line::append_url_footnote(std::string href_str)
+{
+ if (startswith(href_str, "#")) {
+ return;
+ }
+
+ auto& last_block = this->ml_blocks.back();
+ last_block.appendf(FMT_STRING("[{}]"), this->ml_footnotes.size() + 1);
+ last_block.with_attr(string_attr{
+ line_range{
+ (int) this->ml_span_starts.back(),
+ (int) last_block.length(),
+ },
+ VC_STYLE.value(text_attrs{A_UNDERLINE}),
+ });
+ if (this->ml_source_path && href_str.find(':') == std::string::npos) {
+ auto link_path = ghc::filesystem::absolute(
+ this->ml_source_path.value().parent_path() / href_str);
+
+ href_str = fmt::format(FMT_STRING("file://{}"), link_path.string());
+ }
+
+ auto href
+ = attr_line_t().append(lnav::roles::hyperlink(href_str)).append(" ");
+ href.with_attr_for_all(VC_ROLE.value(role_t::VCR_FOOTNOTE_TEXT));
+ href.with_attr_for_all(SA_PREFORMATTED.value());
+ this->ml_footnotes.emplace_back(href);
+}
diff --git a/src/md2attr_line.hh b/src/md2attr_line.hh
new file mode 100644
index 0000000..9f1f977
--- /dev/null
+++ b/src/md2attr_line.hh
@@ -0,0 +1,91 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_md2attr_line_hh
+#define lnav_md2attr_line_hh
+
+#include "base/attr_line.hh"
+#include "ghc/filesystem.hpp"
+#include "md4cpp.hh"
+
+class md2attr_line : public md4cpp::typed_event_handler<attr_line_t> {
+public:
+ md2attr_line() { this->ml_blocks.resize(1); }
+
+ md2attr_line& with_source_path(nonstd::optional<ghc::filesystem::path> path)
+ {
+ this->ml_source_path = path;
+ return *this;
+ }
+
+ Result<void, std::string> enter_block(const block& bl) override;
+
+ Result<void, std::string> leave_block(const block& bl) override;
+ Result<void, std::string> enter_span(const span& bl) override;
+ Result<void, std::string> leave_span(const span& sp) override;
+ Result<void, std::string> text(MD_TEXTTYPE tt,
+ const string_fragment& sf) override;
+
+ attr_line_t get_result() override { return this->ml_blocks.back(); }
+
+private:
+ struct table_t {
+ struct row_t {
+ std::vector<attr_line_t> r_columns;
+ };
+
+ std::vector<attr_line_t> t_headers;
+ std::vector<row_t> t_rows;
+ };
+
+ struct cell_lines {
+ cell_lines(std::vector<attr_line_t> lines) : cl_lines(std::move(lines))
+ {
+ }
+
+ std::vector<attr_line_t> cl_lines;
+ };
+
+ using list_block_t
+ = mapbox::util::variant<MD_BLOCK_UL_DETAIL*, MD_BLOCK_OL_DETAIL>;
+
+ void append_url_footnote(std::string href);
+ void flush_footnotes();
+
+ nonstd::optional<ghc::filesystem::path> ml_source_path;
+ std::vector<attr_line_t> ml_blocks;
+ std::vector<list_block_t> ml_list_stack;
+ std::vector<table_t> ml_tables;
+ std::vector<size_t> ml_span_starts;
+ std::vector<size_t> ml_html_span_starts;
+ std::vector<attr_line_t> ml_footnotes;
+ int32_t ml_code_depth{0};
+};
+
+#endif
diff --git a/src/md4cpp.cc b/src/md4cpp.cc
new file mode 100644
index 0000000..04ca900
--- /dev/null
+++ b/src/md4cpp.cc
@@ -0,0 +1,297 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "md4cpp.hh"
+
+#include "base/is_utf8.hh"
+#include "base/lnav_log.hh"
+#include "emojis-json.h"
+#include "xml-entities-json.h"
+#include "yajlpp/yajlpp_def.hh"
+
+namespace md4cpp {
+
+static const typed_json_path_container<xml_entity> xml_entity_handlers = {
+ yajlpp::property_handler("characters").for_field(&xml_entity::xe_chars),
+};
+
+static const typed_json_path_container<xml_entity_map> xml_entity_map_handlers
+ = {
+ yajlpp::pattern_property_handler("(?<var_name>\\&\\w+;?)")
+ .with_synopsis("<name>")
+ .with_path_provider<xml_entity_map>(
+ [](struct xml_entity_map* xem,
+ std::vector<std::string>& paths_out) {
+ for (const auto& iter : xem->xem_entities) {
+ paths_out.emplace_back(iter.first);
+ }
+ })
+ .with_obj_provider<xml_entity, xml_entity_map>(
+ [](const yajlpp_provider_context& ypc, xml_entity_map* xem) {
+ auto entity_name = ypc.get_substr(0);
+ return &xem->xem_entities[entity_name];
+ })
+ .with_children(xml_entity_handlers),
+};
+
+static const typed_json_path_container<emoji> emoji_handlers = {
+ yajlpp::property_handler("emoji").for_field(&emoji::e_value),
+ yajlpp::property_handler("shortname").for_field(&emoji::e_shortname),
+};
+
+static const typed_json_path_container<emoji_map> emoji_map_handlers = {
+ yajlpp::property_handler("emojis#")
+ .for_field(&emoji_map::em_emojis)
+ .with_children(emoji_handlers),
+};
+
+static xml_entity_map
+load_xml_entity_map()
+{
+ static const intern_string_t name
+ = intern_string::lookup(xml_entities_json.get_name());
+ auto parse_res
+ = xml_entity_map_handlers.parser_for(name).with_ignore_unused(true).of(
+ xml_entities_json.to_string_fragment());
+
+ assert(parse_res.isOk());
+
+ return parse_res.unwrap();
+}
+
+const xml_entity_map&
+get_xml_entity_map()
+{
+ static const auto retval = load_xml_entity_map();
+
+ return retval;
+}
+
+static emoji_map
+load_emoji_map()
+{
+ static const intern_string_t name
+ = intern_string::lookup(emojis_json.get_name());
+ auto parse_res
+ = emoji_map_handlers.parser_for(name).with_ignore_unused(true).of(
+ emojis_json.to_string_fragment());
+
+ assert(parse_res.isOk());
+
+ auto retval = parse_res.unwrap();
+ for (auto& em : retval.em_emojis) {
+ retval.em_shortname2emoji.emplace(em.e_shortname, em);
+ }
+
+ return retval;
+}
+
+const emoji_map&
+get_emoji_map()
+{
+ static const auto retval = load_emoji_map();
+
+ return retval;
+}
+
+struct parse_userdata {
+ event_handler& pu_handler;
+ std::string pu_error_msg;
+};
+
+static event_handler::block
+build_block(MD_BLOCKTYPE type, void* detail)
+{
+ switch (type) {
+ case MD_BLOCK_DOC:
+ return event_handler::block_doc{};
+ case MD_BLOCK_QUOTE:
+ return event_handler::block_quote{};
+ case MD_BLOCK_UL:
+ return static_cast<MD_BLOCK_UL_DETAIL*>(detail);
+ case MD_BLOCK_OL:
+ return static_cast<MD_BLOCK_OL_DETAIL*>(detail);
+ case MD_BLOCK_LI:
+ return static_cast<MD_BLOCK_LI_DETAIL*>(detail);
+ case MD_BLOCK_HR:
+ return event_handler::block_hr{};
+ case MD_BLOCK_H:
+ return static_cast<MD_BLOCK_H_DETAIL*>(detail);
+ case MD_BLOCK_CODE:
+ return static_cast<MD_BLOCK_CODE_DETAIL*>(detail);
+ case MD_BLOCK_HTML:
+ return event_handler::block_html{};
+ case MD_BLOCK_P:
+ return event_handler::block_p{};
+ case MD_BLOCK_TABLE:
+ return static_cast<MD_BLOCK_TABLE_DETAIL*>(detail);
+ case MD_BLOCK_THEAD:
+ return event_handler::block_thead{};
+ case MD_BLOCK_TBODY:
+ return event_handler::block_tbody{};
+ case MD_BLOCK_TR:
+ return event_handler::block_tr{};
+ case MD_BLOCK_TH:
+ return event_handler::block_th{};
+ case MD_BLOCK_TD:
+ return static_cast<MD_BLOCK_TD_DETAIL*>(detail);
+ }
+
+ return {};
+}
+
+static event_handler::span
+build_span(MD_SPANTYPE type, void* detail)
+{
+ switch (type) {
+ case MD_SPAN_EM:
+ return event_handler::span_em{};
+ case MD_SPAN_STRONG:
+ return event_handler::span_strong{};
+ case MD_SPAN_A:
+ return static_cast<MD_SPAN_A_DETAIL*>(detail);
+ case MD_SPAN_IMG:
+ return static_cast<MD_SPAN_IMG_DETAIL*>(detail);
+ case MD_SPAN_CODE:
+ return event_handler::span_code{};
+ case MD_SPAN_DEL:
+ return event_handler::span_del{};
+ case MD_SPAN_U:
+ return event_handler::span_u{};
+ default:
+ break;
+ }
+
+ return {};
+}
+
+static int
+md4cpp_enter_block(MD_BLOCKTYPE type, void* detail, void* userdata)
+{
+ auto* pu = static_cast<parse_userdata*>(userdata);
+
+ auto enter_res = pu->pu_handler.enter_block(build_block(type, detail));
+ if (enter_res.isErr()) {
+ pu->pu_error_msg = enter_res.unwrapErr();
+ return 1;
+ }
+
+ return 0;
+}
+
+static int
+md4cpp_leave_block(MD_BLOCKTYPE type, void* detail, void* userdata)
+{
+ auto* pu = static_cast<parse_userdata*>(userdata);
+
+ auto leave_res = pu->pu_handler.leave_block(build_block(type, detail));
+ if (leave_res.isErr()) {
+ pu->pu_error_msg = leave_res.unwrapErr();
+ return 1;
+ }
+
+ return 0;
+}
+
+static int
+md4cpp_enter_span(MD_SPANTYPE type, void* detail, void* userdata)
+{
+ auto* pu = static_cast<parse_userdata*>(userdata);
+
+ auto enter_res = pu->pu_handler.enter_span(build_span(type, detail));
+ if (enter_res.isErr()) {
+ pu->pu_error_msg = enter_res.unwrapErr();
+ return 1;
+ }
+
+ return 0;
+}
+
+static int
+md4cpp_leave_span(MD_SPANTYPE type, void* detail, void* userdata)
+{
+ auto* pu = static_cast<parse_userdata*>(userdata);
+
+ auto leave_res = pu->pu_handler.leave_span(build_span(type, detail));
+ if (leave_res.isErr()) {
+ pu->pu_error_msg = leave_res.unwrapErr();
+ return 1;
+ }
+
+ return 0;
+}
+
+static int
+md4cpp_text(MD_TEXTTYPE type, const MD_CHAR* text, MD_SIZE size, void* userdata)
+{
+ auto* pu = static_cast<parse_userdata*>(userdata);
+ auto text_res = pu->pu_handler.text(type, string_fragment(text, 0, size));
+ if (text_res.isErr()) {
+ pu->pu_error_msg = text_res.unwrapErr();
+ return 1;
+ }
+
+ return 0;
+}
+
+namespace details {
+Result<void, std::string>
+parse(const string_fragment& sf, event_handler& eh)
+{
+ auto scan_res = is_utf8(sf);
+ if (!scan_res.is_valid()) {
+ return Err(
+ fmt::format(FMT_STRING("file has invalid UTF-8 at offset {}: {}"),
+ scan_res.usr_valid_frag.sf_end,
+ scan_res.usr_message));
+ }
+
+ MD_PARSER parser = {0};
+ auto pu = parse_userdata{eh};
+
+ parser.abi_version = 0;
+ parser.flags = (MD_DIALECT_GITHUB | MD_FLAG_UNDERLINE)
+ & ~(MD_FLAG_PERMISSIVEAUTOLINKS);
+ parser.enter_block = md4cpp_enter_block;
+ parser.leave_block = md4cpp_leave_block;
+ parser.enter_span = md4cpp_enter_span;
+ parser.leave_span = md4cpp_leave_span;
+ parser.text = md4cpp_text;
+
+ auto rc = md_parse(sf.data(), sf.length(), &parser, &pu);
+
+ if (rc == 0) {
+ return Ok();
+ }
+
+ return Err(pu.pu_error_msg);
+}
+} // namespace details
+
+} // namespace md4cpp
diff --git a/src/md4cpp.hh b/src/md4cpp.hh
new file mode 100644
index 0000000..99d09e0
--- /dev/null
+++ b/src/md4cpp.hh
@@ -0,0 +1,144 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_md4cpp_hh
+#define lnav_md4cpp_hh
+
+#include <map>
+#include <string>
+#include <unordered_map>
+
+#include "base/intern_string.hh"
+#include "base/result.h"
+#include "mapbox/variant.hpp"
+#include "md4c/md4c.h"
+
+namespace md4cpp {
+
+struct xml_entity {
+ std::string xe_chars;
+};
+
+struct xml_entity_map {
+ std::map<std::string, xml_entity> xem_entities;
+};
+
+struct emoji {
+ std::string e_shortname;
+ std::string e_value;
+};
+
+struct emoji_map {
+ std::vector<emoji> em_emojis;
+ std::unordered_map<std::string, std::reference_wrapper<emoji>>
+ em_shortname2emoji;
+};
+
+class event_handler {
+public:
+ virtual ~event_handler() = default;
+
+ struct block_doc {};
+ struct block_quote {};
+ struct block_hr {};
+ struct block_html {};
+ struct block_p {};
+ struct block_thead {};
+ struct block_tbody {};
+ struct block_tr {};
+ struct block_th {};
+
+ using block = mapbox::util::variant<block_doc,
+ block_quote,
+ MD_BLOCK_UL_DETAIL*,
+ MD_BLOCK_OL_DETAIL*,
+ MD_BLOCK_LI_DETAIL*,
+ block_hr,
+ MD_BLOCK_H_DETAIL*,
+ MD_BLOCK_CODE_DETAIL*,
+ block_html,
+ block_p,
+ MD_BLOCK_TABLE_DETAIL*,
+ block_thead,
+ block_tbody,
+ block_tr,
+ block_th,
+ MD_BLOCK_TD_DETAIL*>;
+
+ virtual Result<void, std::string> enter_block(const block& bl) = 0;
+ virtual Result<void, std::string> leave_block(const block& bl) = 0;
+
+ struct span_em {};
+ struct span_strong {};
+ struct span_code {};
+ struct span_del {};
+ struct span_u {};
+
+ using span = mapbox::util::variant<span_em,
+ span_strong,
+ MD_SPAN_A_DETAIL*,
+ MD_SPAN_IMG_DETAIL*,
+ span_code,
+ span_del,
+ span_u>;
+
+ virtual Result<void, std::string> enter_span(const span& bl) = 0;
+ virtual Result<void, std::string> leave_span(const span& bl) = 0;
+
+ virtual Result<void, std::string> text(MD_TEXTTYPE tt,
+ const string_fragment& sf)
+ = 0;
+};
+
+namespace details {
+Result<void, std::string> parse(const string_fragment& sf, event_handler& eh);
+}
+
+template<typename T>
+class typed_event_handler : public event_handler {
+public:
+ virtual T get_result() = 0;
+};
+
+template<typename T>
+Result<T, std::string>
+parse(const string_fragment& sf, typed_event_handler<T>& eh)
+{
+ TRY(details::parse(sf, eh));
+
+ return Ok(eh.get_result());
+}
+
+const xml_entity_map& get_xml_entity_map();
+
+const emoji_map& get_emoji_map();
+
+} // namespace md4cpp
+
+#endif
diff --git a/src/network-extension-functions.cc b/src/network-extension-functions.cc
new file mode 100644
index 0000000..6164a40
--- /dev/null
+++ b/src/network-extension-functions.cc
@@ -0,0 +1,169 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file nextwork-extension-functions.cc
+ */
+
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <netinet/in.h>
+#include <string.h>
+#include <sys/socket.h>
+
+#include "base/auto_mem.hh"
+#include "config.h"
+#include "sqlite-extension-func.hh"
+#include "sqlite3.h"
+#include "vtab_module.hh"
+
+static std::string
+sql_gethostbyname(const char* name_in)
+{
+ char buffer[INET6_ADDRSTRLEN];
+ auto_mem<struct addrinfo> ai(freeaddrinfo);
+ void* addr_ptr = nullptr;
+ struct addrinfo hints;
+ int rc;
+
+ memset(&hints, 0, sizeof(hints));
+ for (auto family : {AF_INET, AF_INET6}) {
+ hints.ai_family = family;
+ while ((rc = getaddrinfo(name_in, nullptr, &hints, ai.out()))
+ == EAI_AGAIN)
+ {
+ sqlite3_sleep(10);
+ }
+ if (rc != 0) {
+ return name_in;
+ }
+
+ switch (ai.in()->ai_family) {
+ case AF_INET:
+ addr_ptr = &((struct sockaddr_in*) ai.in()->ai_addr)->sin_addr;
+ break;
+
+ case AF_INET6:
+ addr_ptr
+ = &((struct sockaddr_in6*) ai.in()->ai_addr)->sin6_addr;
+ break;
+
+ default:
+ return name_in;
+ }
+
+ inet_ntop(ai.in()->ai_family, addr_ptr, buffer, sizeof(buffer));
+ break;
+ }
+
+ return buffer;
+}
+
+static std::string
+sql_gethostbyaddr(const char* addr_str)
+{
+ union {
+ struct sockaddr_in sin;
+ struct sockaddr_in6 sin6;
+ } sa;
+ char buffer[NI_MAXHOST];
+ int family, socklen;
+ char* addr_raw;
+ int rc;
+
+ memset(&sa, 0, sizeof(sa));
+ if (strchr(addr_str, ':')) {
+ family = AF_INET6;
+ socklen = sizeof(struct sockaddr_in6);
+ sa.sin6.sin6_family = family;
+ addr_raw = (char*) &sa.sin6.sin6_addr;
+ } else {
+ family = AF_INET;
+ socklen = sizeof(struct sockaddr_in);
+ sa.sin.sin_family = family;
+ addr_raw = (char*) &sa.sin.sin_addr;
+ }
+
+ if (inet_pton(family, addr_str, addr_raw) != 1) {
+ return addr_str;
+ }
+
+ while ((rc = getnameinfo((struct sockaddr*) &sa,
+ socklen,
+ buffer,
+ sizeof(buffer),
+ NULL,
+ 0,
+ 0))
+ == EAI_AGAIN)
+ {
+ sqlite3_sleep(10);
+ }
+
+ if (rc != 0) {
+ return addr_str;
+ }
+
+ return buffer;
+}
+
+int
+network_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs)
+{
+ static struct FuncDef network_funcs[] = {
+ sqlite_func_adapter<decltype(&sql_gethostbyname), sql_gethostbyname>::
+ builder(
+ help_text("gethostbyname",
+ "Get the IP address for the given hostname")
+ .sql_function()
+ .with_parameter({"hostname", "The DNS hostname to lookup."})
+ .with_tags({"net"})
+ .with_example({
+ "To get the IP address for 'localhost'",
+ "SELECT gethostbyname('localhost')",
+ })),
+
+ sqlite_func_adapter<decltype(&sql_gethostbyaddr), sql_gethostbyaddr>::
+ builder(
+ help_text("gethostbyaddr",
+ "Get the hostname for the given IP address")
+ .sql_function()
+ .with_parameter({"hostname", "The IP address to lookup."})
+ .with_tags({"net"})
+ .with_example({
+ "To get the hostname for the IP '127.0.0.1'",
+ "SELECT gethostbyaddr('127.0.0.1')",
+ })),
+
+ {nullptr},
+ };
+
+ *basic_funcs = network_funcs;
+
+ return SQLITE_OK;
+}
diff --git a/src/optional.hpp b/src/optional.hpp
new file mode 100644
index 0000000..a5f4961
--- /dev/null
+++ b/src/optional.hpp
@@ -0,0 +1,1808 @@
+//
+// Copyright (c) 2014-2018 Martin Moene
+//
+// https://github.com/martinmoene/optional-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at
+// http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_OPTIONAL_LITE_HPP
+# define NONSTD_OPTIONAL_LITE_HPP
+
+# define optional_lite_MAJOR 3
+# define optional_lite_MINOR 4
+# define optional_lite_PATCH 0
+
+# define optional_lite_VERSION \
+ optional_STRINGIFY(optional_lite_MAJOR) "." optional_STRINGIFY( \
+ optional_lite_MINOR) "." optional_STRINGIFY(optional_lite_PATCH)
+
+#define optional_STRINGIFY( x ) optional_STRINGIFY_( x )
+#define optional_STRINGIFY_( x ) #x
+
+// optional-lite configuration:
+
+#define optional_OPTIONAL_DEFAULT 0
+#define optional_OPTIONAL_NONSTD 1
+#define optional_OPTIONAL_STD 2
+
+// tweak header support:
+
+#ifdef __has_include
+# if __has_include(<nonstd/optional.tweak.hpp>)
+# include <nonstd/optional.tweak.hpp>
+# endif
+#define optional_HAVE_TWEAK_HEADER 1
+#else
+#define optional_HAVE_TWEAK_HEADER 0
+//# pragma message("optional.hpp: Note: Tweak header not supported.")
+#endif
+
+// optional selection and configuration:
+
+#if !defined( optional_CONFIG_SELECT_OPTIONAL )
+# define optional_CONFIG_SELECT_OPTIONAL ( optional_HAVE_STD_OPTIONAL ? optional_OPTIONAL_STD : optional_OPTIONAL_NONSTD )
+#endif
+
+// Control presence of exception handling (try and auto discover):
+
+#ifndef optional_CONFIG_NO_EXCEPTIONS
+# if _MSC_VER
+# include <cstddef> // for _HAS_EXCEPTIONS
+# endif
+# if defined(__cpp_exceptions) || defined(__EXCEPTIONS) || (_HAS_EXCEPTIONS)
+# define optional_CONFIG_NO_EXCEPTIONS 0
+# else
+# define optional_CONFIG_NO_EXCEPTIONS 1
+# endif
+#endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+#ifndef optional_CPLUSPLUS
+# if defined(_MSVC_LANG ) && !defined(__clang__)
+# define optional_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG )
+# else
+# define optional_CPLUSPLUS __cplusplus
+# endif
+#endif
+
+#define optional_CPP98_OR_GREATER ( optional_CPLUSPLUS >= 199711L )
+#define optional_CPP11_OR_GREATER ( optional_CPLUSPLUS >= 201103L )
+#define optional_CPP11_OR_GREATER_ ( optional_CPLUSPLUS >= 201103L )
+#define optional_CPP14_OR_GREATER ( optional_CPLUSPLUS >= 201402L )
+#define optional_CPP17_OR_GREATER ( optional_CPLUSPLUS >= 201703L )
+#define optional_CPP20_OR_GREATER ( optional_CPLUSPLUS >= 202000L )
+
+// C++ language version (represent 98 as 3):
+
+#define optional_CPLUSPLUS_V ( optional_CPLUSPLUS / 100 - (optional_CPLUSPLUS > 200000 ? 2000 : 1994) )
+
+// Use C++17 std::optional if available and requested:
+
+#if optional_CPP17_OR_GREATER && defined(__has_include )
+# if __has_include( <optional> )
+# define optional_HAVE_STD_OPTIONAL 1
+# else
+# define optional_HAVE_STD_OPTIONAL 0
+# endif
+#else
+# define optional_HAVE_STD_OPTIONAL 0
+#endif
+
+#define optional_USES_STD_OPTIONAL ( (optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_STD) || ((optional_CONFIG_SELECT_OPTIONAL == optional_OPTIONAL_DEFAULT) && optional_HAVE_STD_OPTIONAL) )
+
+//
+// in_place: code duplicated in any-lite, expected-lite, optional-lite, value-ptr-lite, variant-lite:
+//
+
+#ifndef nonstd_lite_HAVE_IN_PLACE_TYPES
+#define nonstd_lite_HAVE_IN_PLACE_TYPES 1
+
+// C++17 std::in_place in <utility>:
+
+#if optional_CPP17_OR_GREATER
+
+#include <utility>
+
+namespace nonstd {
+
+using std::in_place;
+using std::in_place_type;
+using std::in_place_index;
+using std::in_place_t;
+using std::in_place_type_t;
+using std::in_place_index_t;
+
+#define nonstd_lite_in_place_t( T) std::in_place_t
+#define nonstd_lite_in_place_type_t( T) std::in_place_type_t<T>
+#define nonstd_lite_in_place_index_t(K) std::in_place_index_t<K>
+
+#define nonstd_lite_in_place( T) std::in_place_t{}
+#define nonstd_lite_in_place_type( T) std::in_place_type_t<T>{}
+#define nonstd_lite_in_place_index(K) std::in_place_index_t<K>{}
+
+} // namespace nonstd
+
+#else // optional_CPP17_OR_GREATER
+
+#include <cstddef>
+
+namespace nonstd {
+namespace detail {
+
+template< class T >
+struct in_place_type_tag {};
+
+template< std::size_t K >
+struct in_place_index_tag {};
+
+} // namespace detail
+
+struct in_place_t {};
+
+template< class T >
+inline in_place_t in_place( detail::in_place_type_tag<T> /*unused*/ = detail::in_place_type_tag<T>() )
+{
+ return in_place_t();
+}
+
+template< std::size_t K >
+inline in_place_t in_place( detail::in_place_index_tag<K> /*unused*/ = detail::in_place_index_tag<K>() )
+{
+ return in_place_t();
+}
+
+template< class T >
+inline in_place_t in_place_type( detail::in_place_type_tag<T> /*unused*/ = detail::in_place_type_tag<T>() )
+{
+ return in_place_t();
+}
+
+template< std::size_t K >
+inline in_place_t in_place_index( detail::in_place_index_tag<K> /*unused*/ = detail::in_place_index_tag<K>() )
+{
+ return in_place_t();
+}
+
+// mimic templated typedef:
+
+#define nonstd_lite_in_place_t( T) nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag<T> )
+#define nonstd_lite_in_place_type_t( T) nonstd::in_place_t(&)( nonstd::detail::in_place_type_tag<T> )
+#define nonstd_lite_in_place_index_t(K) nonstd::in_place_t(&)( nonstd::detail::in_place_index_tag<K> )
+
+#define nonstd_lite_in_place( T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_type( T) nonstd::in_place_type<T>
+#define nonstd_lite_in_place_index(K) nonstd::in_place_index<K>
+
+} // namespace nonstd
+
+#endif // optional_CPP17_OR_GREATER
+#endif // nonstd_lite_HAVE_IN_PLACE_TYPES
+
+//
+// Using std::optional:
+//
+
+#if optional_USES_STD_OPTIONAL
+
+#include <optional>
+
+namespace nonstd {
+
+ using std::optional;
+ using std::bad_optional_access;
+ using std::hash;
+
+ using std::nullopt;
+ using std::nullopt_t;
+
+ using std::operator==;
+ using std::operator!=;
+ using std::operator<;
+ using std::operator<=;
+ using std::operator>;
+ using std::operator>=;
+ using std::make_optional;
+ using std::swap;
+}
+
+#else // optional_USES_STD_OPTIONAL
+
+#include <cassert>
+#include <utility>
+
+// optional-lite alignment configuration:
+
+#ifndef optional_CONFIG_MAX_ALIGN_HACK
+# define optional_CONFIG_MAX_ALIGN_HACK 0
+#endif
+
+#ifndef optional_CONFIG_ALIGN_AS
+// no default, used in #if defined()
+#endif
+
+#ifndef optional_CONFIG_ALIGN_AS_FALLBACK
+# define optional_CONFIG_ALIGN_AS_FALLBACK double
+#endif
+
+// Compiler warning suppression:
+
+#if defined(__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wundef"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wundef"
+#elif defined(_MSC_VER )
+# pragma warning( push )
+#endif
+
+// half-open range [lo..hi):
+#define optional_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) )
+
+// Compiler versions:
+//
+// MSVC++ 6.0 _MSC_VER == 1200 optional_COMPILER_MSVC_VERSION == 60 (Visual Studio 6.0)
+// MSVC++ 7.0 _MSC_VER == 1300 optional_COMPILER_MSVC_VERSION == 70 (Visual Studio .NET 2002)
+// MSVC++ 7.1 _MSC_VER == 1310 optional_COMPILER_MSVC_VERSION == 71 (Visual Studio .NET 2003)
+// MSVC++ 8.0 _MSC_VER == 1400 optional_COMPILER_MSVC_VERSION == 80 (Visual Studio 2005)
+// MSVC++ 9.0 _MSC_VER == 1500 optional_COMPILER_MSVC_VERSION == 90 (Visual Studio 2008)
+// MSVC++ 10.0 _MSC_VER == 1600 optional_COMPILER_MSVC_VERSION == 100 (Visual Studio 2010)
+// MSVC++ 11.0 _MSC_VER == 1700 optional_COMPILER_MSVC_VERSION == 110 (Visual Studio 2012)
+// MSVC++ 12.0 _MSC_VER == 1800 optional_COMPILER_MSVC_VERSION == 120 (Visual Studio 2013)
+// MSVC++ 14.0 _MSC_VER == 1900 optional_COMPILER_MSVC_VERSION == 140 (Visual Studio 2015)
+// MSVC++ 14.1 _MSC_VER >= 1910 optional_COMPILER_MSVC_VERSION == 141 (Visual Studio 2017)
+// MSVC++ 14.2 _MSC_VER >= 1920 optional_COMPILER_MSVC_VERSION == 142 (Visual Studio 2019)
+
+#if defined(_MSC_VER ) && !defined(__clang__)
+# define optional_COMPILER_MSVC_VER (_MSC_VER )
+# define optional_COMPILER_MSVC_VERSION (_MSC_VER / 10 - 10 * ( 5 + (_MSC_VER < 1900 ) ) )
+#else
+# define optional_COMPILER_MSVC_VER 0
+# define optional_COMPILER_MSVC_VERSION 0
+#endif
+
+#define optional_COMPILER_VERSION( major, minor, patch ) ( 10 * (10 * (major) + (minor) ) + (patch) )
+
+#if defined(__GNUC__) && !defined(__clang__)
+# define optional_COMPILER_GNUC_VERSION optional_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#else
+# define optional_COMPILER_GNUC_VERSION 0
+#endif
+
+#if defined(__clang__)
+# define optional_COMPILER_CLANG_VERSION optional_COMPILER_VERSION(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#else
+# define optional_COMPILER_CLANG_VERSION 0
+#endif
+
+#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 140 )
+# pragma warning( disable: 4345 ) // initialization behavior changed
+#endif
+
+#if optional_BETWEEN(optional_COMPILER_MSVC_VERSION, 70, 150 )
+# pragma warning( disable: 4814 ) // in C++14 'constexpr' will not imply 'const'
+#endif
+
+// Presence of language and library features:
+
+#define optional_HAVE(FEATURE) ( optional_HAVE_##FEATURE )
+
+#ifdef _HAS_CPP0X
+# define optional_HAS_CPP0X _HAS_CPP0X
+#else
+# define optional_HAS_CPP0X 0
+#endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for optional-lite:
+
+#if optional_COMPILER_MSVC_VER >= 1900
+# undef optional_CPP11_OR_GREATER
+# define optional_CPP11_OR_GREATER 1
+#endif
+
+#define optional_CPP11_90 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1500)
+#define optional_CPP11_100 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1600)
+#define optional_CPP11_110 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1700)
+#define optional_CPP11_120 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1800)
+#define optional_CPP11_140 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1900)
+#define optional_CPP11_141 (optional_CPP11_OR_GREATER_ || optional_COMPILER_MSVC_VER >= 1910)
+
+#define optional_CPP14_000 (optional_CPP14_OR_GREATER)
+#define optional_CPP17_000 (optional_CPP17_OR_GREATER)
+
+// clang >= 2.9, gcc >= 4.9, msvc >= vc14.0/1900 (vs15):
+#define optional_CPP11_140_C290_G490 ((optional_CPP11_OR_GREATER_ && (optional_COMPILER_CLANG_VERSION >= 290 || optional_COMPILER_GNUC_VERSION >= 490)) || (optional_COMPILER_MSVC_VER >= 1900))
+
+// clang >= 3.5, msvc >= vc11 (vs12):
+#define optional_CPP11_110_C350 ( optional_CPP11_110 && !optional_BETWEEN( optional_COMPILER_CLANG_VERSION, 1, 350 ) )
+
+// clang >= 3.5, gcc >= 5.0, msvc >= vc11 (vs12):
+#define optional_CPP11_110_C350_G500 \
+ ( optional_CPP11_110 && \
+ !( optional_BETWEEN( optional_COMPILER_CLANG_VERSION, 1, 350 ) \
+ || optional_BETWEEN( optional_COMPILER_GNUC_VERSION , 1, 500 ) ) )
+
+// Presence of C++11 language features:
+
+#define optional_HAVE_CONSTEXPR_11 optional_CPP11_140
+#define optional_HAVE_IS_DEFAULT optional_CPP11_140
+#define optional_HAVE_NOEXCEPT optional_CPP11_140
+#define optional_HAVE_NULLPTR optional_CPP11_100
+#define optional_HAVE_REF_QUALIFIER optional_CPP11_140_C290_G490
+#define optional_HAVE_INITIALIZER_LIST optional_CPP11_140
+
+// Presence of C++14 language features:
+
+#define optional_HAVE_CONSTEXPR_14 optional_CPP14_000
+
+// Presence of C++17 language features:
+
+#define optional_HAVE_NODISCARD optional_CPP17_000
+
+// Presence of C++ library features:
+
+#define optional_HAVE_CONDITIONAL optional_CPP11_120
+#define optional_HAVE_REMOVE_CV optional_CPP11_120
+#define optional_HAVE_TYPE_TRAITS optional_CPP11_90
+
+#define optional_HAVE_TR1_TYPE_TRAITS (!! optional_COMPILER_GNUC_VERSION )
+#define optional_HAVE_TR1_ADD_POINTER (!! optional_COMPILER_GNUC_VERSION )
+
+#define optional_HAVE_IS_ASSIGNABLE optional_CPP11_110_C350
+#define optional_HAVE_IS_MOVE_CONSTRUCTIBLE optional_CPP11_110_C350
+#define optional_HAVE_IS_NOTHROW_MOVE_ASSIGNABLE optional_CPP11_110_C350
+#define optional_HAVE_IS_NOTHROW_MOVE_CONSTRUCTIBLE optional_CPP11_110_C350
+#define optional_HAVE_IS_TRIVIALLY_COPY_CONSTRUCTIBLE optional_CPP11_110_C350_G500
+#define optional_HAVE_IS_TRIVIALLY_MOVE_CONSTRUCTIBLE optional_CPP11_110_C350_G500
+
+// C++ feature usage:
+
+#if optional_HAVE( CONSTEXPR_11 )
+# define optional_constexpr constexpr
+#else
+# define optional_constexpr /*constexpr*/
+#endif
+
+#if optional_HAVE( IS_DEFAULT )
+# define optional_is_default = default;
+#else
+# define optional_is_default {}
+#endif
+
+#if optional_HAVE( CONSTEXPR_14 )
+# define optional_constexpr14 constexpr
+#else
+# define optional_constexpr14 /*constexpr*/
+#endif
+
+#if optional_HAVE( NODISCARD )
+# define optional_nodiscard [[nodiscard]]
+#else
+# define optional_nodiscard /*[[nodiscard]]*/
+#endif
+
+#if optional_HAVE( NOEXCEPT )
+# define optional_noexcept noexcept
+#else
+# define optional_noexcept /*noexcept*/
+#endif
+
+#if optional_HAVE( NULLPTR )
+# define optional_nullptr nullptr
+#else
+# define optional_nullptr NULL
+#endif
+
+#if optional_HAVE( REF_QUALIFIER )
+// NOLINTNEXTLINE( bugprone-macro-parentheses )
+# define optional_ref_qual &
+# define optional_refref_qual &&
+#else
+# define optional_ref_qual /*&*/
+# define optional_refref_qual /*&&*/
+#endif
+
+// additional includes:
+
+#if optional_CONFIG_NO_EXCEPTIONS
+// already included: <cassert>
+#else
+# include <stdexcept>
+#endif
+
+#if optional_CPP11_OR_GREATER
+# include <functional>
+#endif
+
+#if optional_HAVE( INITIALIZER_LIST )
+# include <initializer_list>
+#endif
+
+#if optional_HAVE( TYPE_TRAITS )
+# include <type_traits>
+#elif optional_HAVE( TR1_TYPE_TRAITS )
+# include <tr1/type_traits>
+#endif
+
+// Method enabling
+
+#if optional_CPP11_OR_GREATER
+
+#define optional_REQUIRES_0(...) \
+ template< bool B = (__VA_ARGS__), typename std::enable_if<B, int>::type = 0 >
+
+#define optional_REQUIRES_T(...) \
+ , typename std::enable_if< (__VA_ARGS__), int >::type = 0
+
+#define optional_REQUIRES_R(R, ...) \
+ typename std::enable_if< (__VA_ARGS__), R>::type
+
+#define optional_REQUIRES_A(...) \
+ , typename std::enable_if< (__VA_ARGS__), void*>::type = nullptr
+
+#endif
+
+//
+// optional:
+//
+
+namespace nonstd { namespace optional_lite {
+
+namespace std11 {
+
+template< class T, T v > struct integral_constant { enum { value = v }; };
+template< bool B > struct bool_constant : integral_constant<bool, B>{};
+
+typedef bool_constant< true > true_type;
+typedef bool_constant< false > false_type;
+
+#if optional_CPP11_OR_GREATER
+ using std::move;
+#else
+ template< typename T > T & move( T & t ) { return t; }
+#endif
+
+#if optional_HAVE( CONDITIONAL )
+ using std::conditional;
+#else
+ template< bool B, typename T, typename F > struct conditional { typedef T type; };
+ template< typename T, typename F > struct conditional<false, T, F> { typedef F type; };
+#endif // optional_HAVE_CONDITIONAL
+
+#if optional_HAVE( IS_ASSIGNABLE )
+ using std::is_assignable;
+#else
+ template< class T, class U > struct is_assignable : std11::true_type{};
+#endif
+
+#if optional_HAVE( IS_MOVE_CONSTRUCTIBLE )
+ using std::is_move_constructible;
+#else
+ template< class T > struct is_move_constructible : std11::true_type{};
+#endif
+
+#if optional_HAVE( IS_NOTHROW_MOVE_ASSIGNABLE )
+ using std::is_nothrow_move_assignable;
+#else
+ template< class T > struct is_nothrow_move_assignable : std11::true_type{};
+#endif
+
+#if optional_HAVE( IS_NOTHROW_MOVE_CONSTRUCTIBLE )
+ using std::is_nothrow_move_constructible;
+#else
+ template< class T > struct is_nothrow_move_constructible : std11::true_type{};
+#endif
+
+#if optional_HAVE( IS_TRIVIALLY_COPY_CONSTRUCTIBLE )
+ using std::is_trivially_copy_constructible;
+#else
+ template< class T > struct is_trivially_copy_constructible : std11::true_type{};
+#endif
+
+#if optional_HAVE( IS_TRIVIALLY_MOVE_CONSTRUCTIBLE )
+ using std::is_trivially_move_constructible;
+#else
+ template< class T > struct is_trivially_move_constructible : std11::true_type{};
+#endif
+
+} // namespace std11
+
+#if optional_CPP11_OR_GREATER
+
+/// type traits C++17:
+
+namespace std17 {
+
+#if optional_CPP17_OR_GREATER
+
+using std::is_swappable;
+using std::is_nothrow_swappable;
+
+#elif optional_CPP11_OR_GREATER
+
+namespace detail {
+
+using std::swap;
+
+struct is_swappable
+{
+ template< typename T, typename = decltype( swap( std::declval<T&>(), std::declval<T&>() ) ) >
+ static std11::true_type test( int /*unused*/ );
+
+ template< typename >
+ static std11::false_type test(...);
+};
+
+struct is_nothrow_swappable
+{
+ // wrap noexcept(expr) in separate function as work-around for VC140 (VS2015):
+
+ template< typename T >
+ static constexpr bool satisfies()
+ {
+ return noexcept( swap( std::declval<T&>(), std::declval<T&>() ) );
+ }
+
+ template< typename T >
+ static auto test( int /*unused*/ ) -> std11::integral_constant<bool, satisfies<T>()>{}
+
+ template< typename >
+ static auto test(...) -> std11::false_type;
+};
+
+} // namespace detail
+
+// is [nothow] swappable:
+
+template< typename T >
+struct is_swappable : decltype( detail::is_swappable::test<T>(0) ){};
+
+template< typename T >
+struct is_nothrow_swappable : decltype( detail::is_nothrow_swappable::test<T>(0) ){};
+
+#endif // optional_CPP17_OR_GREATER
+
+} // namespace std17
+
+/// type traits C++20:
+
+namespace std20 {
+
+template< typename T >
+struct remove_cvref
+{
+ typedef typename std::remove_cv< typename std::remove_reference<T>::type >::type type;
+};
+
+} // namespace std20
+
+#endif // optional_CPP11_OR_GREATER
+
+/// class optional
+
+template< typename T >
+class optional;
+
+namespace detail {
+
+// C++11 emulation:
+
+struct nulltype{};
+
+template< typename Head, typename Tail >
+struct typelist
+{
+ typedef Head head;
+ typedef Tail tail;
+};
+
+#if optional_CONFIG_MAX_ALIGN_HACK
+
+// Max align, use most restricted type for alignment:
+
+#define optional_UNIQUE( name ) optional_UNIQUE2( name, __LINE__ )
+#define optional_UNIQUE2( name, line ) optional_UNIQUE3( name, line )
+#define optional_UNIQUE3( name, line ) name ## line
+
+#define optional_ALIGN_TYPE( type ) \
+ type optional_UNIQUE( _t ); struct_t< type > optional_UNIQUE( _st )
+
+template< typename T >
+struct struct_t { T _; };
+
+union max_align_t
+{
+ optional_ALIGN_TYPE( char );
+ optional_ALIGN_TYPE( short int );
+ optional_ALIGN_TYPE( int );
+ optional_ALIGN_TYPE( long int );
+ optional_ALIGN_TYPE( float );
+ optional_ALIGN_TYPE( double );
+ optional_ALIGN_TYPE( long double );
+ optional_ALIGN_TYPE( char * );
+ optional_ALIGN_TYPE( short int * );
+ optional_ALIGN_TYPE( int * );
+ optional_ALIGN_TYPE( long int * );
+ optional_ALIGN_TYPE( float * );
+ optional_ALIGN_TYPE( double * );
+ optional_ALIGN_TYPE( long double * );
+ optional_ALIGN_TYPE( void * );
+
+#ifdef HAVE_LONG_LONG
+ optional_ALIGN_TYPE( long long );
+#endif
+
+ struct Unknown;
+
+ Unknown ( * optional_UNIQUE(_) )( Unknown );
+ Unknown * Unknown::* optional_UNIQUE(_);
+ Unknown ( Unknown::* optional_UNIQUE(_) )( Unknown );
+
+ struct_t< Unknown ( * )( Unknown) > optional_UNIQUE(_);
+ struct_t< Unknown * Unknown::* > optional_UNIQUE(_);
+ struct_t< Unknown ( Unknown::* )(Unknown) > optional_UNIQUE(_);
+};
+
+#undef optional_UNIQUE
+#undef optional_UNIQUE2
+#undef optional_UNIQUE3
+
+#undef optional_ALIGN_TYPE
+
+#elif defined( optional_CONFIG_ALIGN_AS ) // optional_CONFIG_MAX_ALIGN_HACK
+
+// Use user-specified type for alignment:
+
+#define optional_ALIGN_AS( unused ) \
+ optional_CONFIG_ALIGN_AS
+
+#else // optional_CONFIG_MAX_ALIGN_HACK
+
+// Determine POD type to use for alignment:
+
+#define optional_ALIGN_AS( to_align ) \
+ typename type_of_size< alignment_types, alignment_of< to_align >::value >::type
+
+template< typename T >
+struct alignment_of;
+
+template< typename T >
+struct alignment_of_hack
+{
+ char c;
+ T t;
+ alignment_of_hack();
+};
+
+template< size_t A, size_t S >
+struct alignment_logic
+{
+ enum { value = A < S ? A : S };
+};
+
+template< typename T >
+struct alignment_of
+{
+ enum { value = alignment_logic<
+ sizeof( alignment_of_hack<T> ) - sizeof(T), sizeof(T) >::value };
+};
+
+template< typename List, size_t N >
+struct type_of_size
+{
+ typedef typename std11::conditional<
+ N == sizeof( typename List::head ),
+ typename List::head,
+ typename type_of_size<typename List::tail, N >::type >::type type;
+};
+
+template< size_t N >
+struct type_of_size< nulltype, N >
+{
+ typedef optional_CONFIG_ALIGN_AS_FALLBACK type;
+};
+
+template< typename T>
+struct struct_t { T _; };
+
+#define optional_ALIGN_TYPE( type ) \
+ typelist< type , typelist< struct_t< type >
+
+struct Unknown;
+
+typedef
+ optional_ALIGN_TYPE( char ),
+ optional_ALIGN_TYPE( short ),
+ optional_ALIGN_TYPE( int ),
+ optional_ALIGN_TYPE( long ),
+ optional_ALIGN_TYPE( float ),
+ optional_ALIGN_TYPE( double ),
+ optional_ALIGN_TYPE( long double ),
+
+ optional_ALIGN_TYPE( char *),
+ optional_ALIGN_TYPE( short * ),
+ optional_ALIGN_TYPE( int * ),
+ optional_ALIGN_TYPE( long * ),
+ optional_ALIGN_TYPE( float * ),
+ optional_ALIGN_TYPE( double * ),
+ optional_ALIGN_TYPE( long double * ),
+
+ optional_ALIGN_TYPE( Unknown ( * )( Unknown ) ),
+ optional_ALIGN_TYPE( Unknown * Unknown::* ),
+ optional_ALIGN_TYPE( Unknown ( Unknown::* )( Unknown ) ),
+
+ nulltype
+ > > > > > > > > > > > > > >
+ > > > > > > > > > > > > > >
+ > > > > > >
+ alignment_types;
+
+#undef optional_ALIGN_TYPE
+
+#endif // optional_CONFIG_MAX_ALIGN_HACK
+
+/// C++03 constructed union to hold value.
+
+template< typename T >
+union storage_t
+{
+//private:
+// template< typename > friend class optional;
+
+ typedef T value_type;
+
+ storage_t() optional_is_default
+
+ explicit storage_t( value_type const & v )
+ {
+ construct_value( v );
+ }
+
+ void construct_value( value_type const & v )
+ {
+ ::new( value_ptr() ) value_type( v );
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ explicit storage_t( value_type && v )
+ {
+ construct_value( std::move( v ) );
+ }
+
+ void construct_value( value_type && v )
+ {
+ ::new( value_ptr() ) value_type( std::move( v ) );
+ }
+
+ template< class... Args >
+ storage_t( nonstd_lite_in_place_t(T), Args&&... args )
+ {
+ emplace( std::forward<Args>(args)... );
+ }
+
+ template< class... Args >
+ void emplace( Args&&... args )
+ {
+ ::new( value_ptr() ) value_type( std::forward<Args>(args)... );
+ }
+
+ template< class U, class... Args >
+ void emplace( std::initializer_list<U> il, Args&&... args )
+ {
+ ::new( value_ptr() ) value_type( il, std::forward<Args>(args)... );
+ }
+
+#endif
+
+ void destruct_value()
+ {
+ value_ptr()->~T();
+ }
+
+ optional_nodiscard value_type const * value_ptr() const
+ {
+ return as<value_type>();
+ }
+
+ value_type * value_ptr()
+ {
+ return as<value_type>();
+ }
+
+ optional_nodiscard value_type const & value() const optional_ref_qual
+ {
+ return * value_ptr();
+ }
+
+ value_type & value() optional_ref_qual
+ {
+ return * value_ptr();
+ }
+
+#if optional_HAVE( REF_QUALIFIER )
+
+ optional_nodiscard value_type const && value() const optional_refref_qual
+ {
+ return std::move( value() );
+ }
+
+ value_type && value() optional_refref_qual
+ {
+ return std::move( value() );
+ }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+
+ using aligned_storage_t = typename std::aligned_storage< sizeof(value_type), alignof(value_type) >::type;
+ aligned_storage_t data;
+
+#elif optional_CONFIG_MAX_ALIGN_HACK
+
+ typedef struct { unsigned char data[ sizeof(value_type) ]; } aligned_storage_t;
+
+ max_align_t hack;
+ aligned_storage_t data;
+
+#else
+ typedef optional_ALIGN_AS(value_type) align_as_type;
+
+ typedef struct { align_as_type data[ 1 + ( sizeof(value_type) - 1 ) / sizeof(align_as_type) ]; } aligned_storage_t;
+ aligned_storage_t data;
+
+# undef optional_ALIGN_AS
+
+#endif // optional_CONFIG_MAX_ALIGN_HACK
+
+ optional_nodiscard void * ptr() optional_noexcept
+ {
+ return &data;
+ }
+
+ optional_nodiscard void const * ptr() const optional_noexcept
+ {
+ return &data;
+ }
+
+ template <typename U>
+ optional_nodiscard U * as()
+ {
+ return reinterpret_cast<U*>( ptr() );
+ }
+
+ template <typename U>
+ optional_nodiscard U const * as() const
+ {
+ return reinterpret_cast<U const *>( ptr() );
+ }
+};
+
+} // namespace detail
+
+/// disengaged state tag
+
+struct nullopt_t
+{
+ struct init{};
+ explicit optional_constexpr nullopt_t( init /*unused*/ ) optional_noexcept {}
+};
+
+#if optional_HAVE( CONSTEXPR_11 )
+constexpr nullopt_t nullopt{ nullopt_t::init{} };
+#else
+// extra parenthesis to prevent the most vexing parse:
+const nullopt_t nullopt(( nullopt_t::init() ));
+#endif
+
+/// optional access error
+
+#if ! optional_CONFIG_NO_EXCEPTIONS
+
+class bad_optional_access : public std::logic_error
+{
+public:
+ explicit bad_optional_access()
+ : logic_error( "bad optional access" ) {}
+};
+
+#endif //optional_CONFIG_NO_EXCEPTIONS
+
+/// optional
+
+template< typename T>
+class optional
+{
+private:
+ template< typename > friend class optional;
+
+ typedef void (optional::*safe_bool)() const;
+
+public:
+ typedef T value_type;
+
+ // x.x.3.1, constructors
+
+ // 1a - default construct
+ optional_constexpr optional() optional_noexcept
+ : has_value_( false )
+ , contained()
+ {}
+
+ // 1b - construct explicitly empty
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ optional_constexpr optional( nullopt_t /*unused*/ ) optional_noexcept
+ : has_value_( false )
+ , contained()
+ {}
+
+ // 2 - copy-construct
+#if optional_CPP11_OR_GREATER
+ // template< typename U = T
+ // optional_REQUIRES_T(
+ // std::is_copy_constructible<U>::value
+ // || std11::is_trivially_copy_constructible<U>::value
+ // )
+ // >
+#endif
+ optional_constexpr14 optional( optional const & other )
+ : has_value_( other.has_value() )
+ {
+ if ( other.has_value() )
+ {
+ contained.construct_value( other.contained.value() );
+ }
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 3 (C++11) - move-construct from optional
+ template< typename U = T
+ optional_REQUIRES_T(
+ std11::is_move_constructible<U>::value
+ || std11::is_trivially_move_constructible<U>::value
+ )
+ >
+ optional_constexpr14 optional( optional && other )
+ // NOLINTNEXTLINE( performance-noexcept-move-constructor )
+ noexcept( std11::is_nothrow_move_constructible<T>::value )
+ : has_value_( other.has_value() )
+ {
+ if ( other.has_value() )
+ {
+ contained.construct_value( std::move( other.contained.value() ) );
+ }
+ }
+
+ // 4a (C++11) - explicit converting copy-construct from optional
+ template< typename U
+ optional_REQUIRES_T(
+ std::is_constructible<T, U const &>::value
+ && !std::is_constructible<T, optional<U> & >::value
+ && !std::is_constructible<T, optional<U> && >::value
+ && !std::is_constructible<T, optional<U> const & >::value
+ && !std::is_constructible<T, optional<U> const && >::value
+ && !std::is_convertible< optional<U> & , T>::value
+ && !std::is_convertible< optional<U> && , T>::value
+ && !std::is_convertible< optional<U> const & , T>::value
+ && !std::is_convertible< optional<U> const &&, T>::value
+ && !std::is_convertible< U const & , T>::value /*=> explicit */
+ )
+ >
+ explicit optional( optional<U> const & other )
+ : has_value_( other.has_value() )
+ {
+ if ( other.has_value() )
+ {
+ contained.construct_value( T{ other.contained.value() } );
+ }
+ }
+#endif // optional_CPP11_OR_GREATER
+
+ // 4b (C++98 and later) - non-explicit converting copy-construct from optional
+ template< typename U
+#if optional_CPP11_OR_GREATER
+ optional_REQUIRES_T(
+ std::is_constructible<T, U const &>::value
+ && !std::is_constructible<T, optional<U> & >::value
+ && !std::is_constructible<T, optional<U> && >::value
+ && !std::is_constructible<T, optional<U> const & >::value
+ && !std::is_constructible<T, optional<U> const && >::value
+ && !std::is_convertible< optional<U> & , T>::value
+ && !std::is_convertible< optional<U> && , T>::value
+ && !std::is_convertible< optional<U> const & , T>::value
+ && !std::is_convertible< optional<U> const &&, T>::value
+ && std::is_convertible< U const & , T>::value /*=> non-explicit */
+ )
+#endif // optional_CPP11_OR_GREATER
+ >
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ /*non-explicit*/ optional( optional<U> const & other )
+ : has_value_( other.has_value() )
+ {
+ if ( other.has_value() )
+ {
+ contained.construct_value( other.contained.value() );
+ }
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 5a (C++11) - explicit converting move-construct from optional
+ template< typename U
+ optional_REQUIRES_T(
+ std::is_constructible<T, U &&>::value
+ && !std::is_constructible<T, optional<U> & >::value
+ && !std::is_constructible<T, optional<U> && >::value
+ && !std::is_constructible<T, optional<U> const & >::value
+ && !std::is_constructible<T, optional<U> const && >::value
+ && !std::is_convertible< optional<U> & , T>::value
+ && !std::is_convertible< optional<U> && , T>::value
+ && !std::is_convertible< optional<U> const & , T>::value
+ && !std::is_convertible< optional<U> const &&, T>::value
+ && !std::is_convertible< U &&, T>::value /*=> explicit */
+ )
+ >
+ explicit optional( optional<U> && other
+ )
+ : has_value_( other.has_value() )
+ {
+ if ( other.has_value() )
+ {
+ contained.construct_value( T{ std::move( other.contained.value() ) } );
+ }
+ }
+
+ // 5a (C++11) - non-explicit converting move-construct from optional
+ template< typename U
+ optional_REQUIRES_T(
+ std::is_constructible<T, U &&>::value
+ && !std::is_constructible<T, optional<U> & >::value
+ && !std::is_constructible<T, optional<U> && >::value
+ && !std::is_constructible<T, optional<U> const & >::value
+ && !std::is_constructible<T, optional<U> const && >::value
+ && !std::is_convertible< optional<U> & , T>::value
+ && !std::is_convertible< optional<U> && , T>::value
+ && !std::is_convertible< optional<U> const & , T>::value
+ && !std::is_convertible< optional<U> const &&, T>::value
+ && std::is_convertible< U &&, T>::value /*=> non-explicit */
+ )
+ >
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ /*non-explicit*/ optional( optional<U> && other )
+ : has_value_( other.has_value() )
+ {
+ if ( other.has_value() )
+ {
+ contained.construct_value( std::move( other.contained.value() ) );
+ }
+ }
+
+ // 6 (C++11) - in-place construct
+ template< typename... Args
+ optional_REQUIRES_T(
+ std::is_constructible<T, Args&&...>::value
+ )
+ >
+ optional_constexpr explicit optional( nonstd_lite_in_place_t(T), Args&&... args )
+ : has_value_( true )
+ , contained( T( std::forward<Args>(args)...) )
+ {}
+
+ // 7 (C++11) - in-place construct, initializer-list
+ template< typename U, typename... Args
+ optional_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U>&, Args&&...>::value
+ )
+ >
+ optional_constexpr explicit optional( nonstd_lite_in_place_t(T), std::initializer_list<U> il, Args&&... args )
+ : has_value_( true )
+ , contained( T( il, std::forward<Args>(args)...) )
+ {}
+
+ // 8a (C++11) - explicit move construct from value
+ template< typename U = T
+ optional_REQUIRES_T(
+ std::is_constructible<T, U&&>::value
+ && !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value
+ && !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value
+ && !std::is_convertible<U&&, T>::value /*=> explicit */
+ )
+ >
+ optional_constexpr explicit optional( U && value )
+ : has_value_( true )
+ , contained( nonstd_lite_in_place(T), std::forward<U>( value ) )
+ {}
+
+ // 8b (C++11) - non-explicit move construct from value
+ template< typename U = T
+ optional_REQUIRES_T(
+ std::is_constructible<T, U&&>::value
+ && !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value
+ && !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value
+ && std::is_convertible<U&&, T>::value /*=> non-explicit */
+ )
+ >
+ // NOLINTNEXTLINE( google-explicit-constructor, hicpp-explicit-conversions )
+ optional_constexpr /*non-explicit*/ optional( U && value )
+ : has_value_( true )
+ , contained( nonstd_lite_in_place(T), std::forward<U>( value ) )
+ {}
+
+#else // optional_CPP11_OR_GREATER
+
+ // 8 (C++98)
+ optional( value_type const & value )
+ : has_value_( true )
+ , contained( value )
+ {}
+
+#endif // optional_CPP11_OR_GREATER
+
+ // x.x.3.2, destructor
+
+ ~optional()
+ {
+ if ( has_value() )
+ {
+ contained.destruct_value();
+ }
+ }
+
+ // x.x.3.3, assignment
+
+ // 1 (C++98and later) - assign explicitly empty
+ optional & operator=( nullopt_t /*unused*/) optional_noexcept
+ {
+ reset();
+ return *this;
+ }
+
+ // 2 (C++98and later) - copy-assign from optional
+#if optional_CPP11_OR_GREATER
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature, misc-unconventional-assign-operator )
+ optional_REQUIRES_R(
+ optional &,
+ true
+// std::is_copy_constructible<T>::value
+// && std::is_copy_assignable<T>::value
+ )
+ operator=( optional const & other )
+ noexcept(
+ std11::is_nothrow_move_assignable<T>::value
+ && std11::is_nothrow_move_constructible<T>::value
+ )
+#else
+ optional & operator=( optional const & other )
+#endif
+ {
+ if ( (has_value() == true ) && (other.has_value() == false) ) { reset(); }
+ else if ( (has_value() == false) && (other.has_value() == true ) ) { initialize( *other ); }
+ else if ( (has_value() == true ) && (other.has_value() == true ) ) { contained.value() = *other; }
+ return *this;
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 3 (C++11) - move-assign from optional
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature, misc-unconventional-assign-operator )
+ optional_REQUIRES_R(
+ optional &,
+ true
+// std11::is_move_constructible<T>::value
+// && std::is_move_assignable<T>::value
+ )
+ operator=( optional && other ) noexcept
+ {
+ if ( (has_value() == true ) && (other.has_value() == false) ) { reset(); }
+ else if ( (has_value() == false) && (other.has_value() == true ) ) { initialize( std::move( *other ) ); }
+ else if ( (has_value() == true ) && (other.has_value() == true ) ) { contained.value() = std::move( *other ); }
+ return *this;
+ }
+
+ // 4 (C++11) - move-assign from value
+ template< typename U = T >
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature, misc-unconventional-assign-operator )
+ optional_REQUIRES_R(
+ optional &,
+ std::is_constructible<T , U>::value
+ && std11::is_assignable<T&, U>::value
+ && !std::is_same<typename std20::remove_cvref<U>::type, nonstd_lite_in_place_t(U)>::value
+ && !std::is_same<typename std20::remove_cvref<U>::type, optional<T>>::value
+ && !(std::is_scalar<T>::value && std::is_same<T, typename std::decay<U>::type>::value)
+ )
+ operator=( U && value )
+ {
+ if ( has_value() )
+ {
+ contained.value() = std::forward<U>( value );
+ }
+ else
+ {
+ initialize( T( std::forward<U>( value ) ) );
+ }
+ return *this;
+ }
+
+#else // optional_CPP11_OR_GREATER
+
+ // 4 (C++98) - copy-assign from value
+ template< typename U /*= T*/ >
+ optional & operator=( U const & value )
+ {
+ if ( has_value() ) contained.value() = value;
+ else initialize( T( value ) );
+ return *this;
+ }
+
+#endif // optional_CPP11_OR_GREATER
+
+ // 5 (C++98 and later) - converting copy-assign from optional
+ template< typename U >
+#if optional_CPP11_OR_GREATER
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature, misc-unconventional-assign-operator )
+ optional_REQUIRES_R(
+ optional&,
+ std::is_constructible< T , U const &>::value
+ && std11::is_assignable< T&, U const &>::value
+ && !std::is_constructible<T, optional<U> & >::value
+ && !std::is_constructible<T, optional<U> && >::value
+ && !std::is_constructible<T, optional<U> const & >::value
+ && !std::is_constructible<T, optional<U> const && >::value
+ && !std::is_convertible< optional<U> & , T>::value
+ && !std::is_convertible< optional<U> && , T>::value
+ && !std::is_convertible< optional<U> const & , T>::value
+ && !std::is_convertible< optional<U> const &&, T>::value
+ && !std11::is_assignable< T&, optional<U> & >::value
+ && !std11::is_assignable< T&, optional<U> && >::value
+ && !std11::is_assignable< T&, optional<U> const & >::value
+ && !std11::is_assignable< T&, optional<U> const && >::value
+ )
+#else
+ optional&
+#endif // optional_CPP11_OR_GREATER
+ operator=( optional<U> const & other )
+ {
+ return *this = optional( other );
+ }
+
+#if optional_CPP11_OR_GREATER
+
+ // 6 (C++11) - converting move-assign from optional
+ template< typename U >
+ // NOLINTNEXTLINE( cppcoreguidelines-c-copy-assignment-signature, misc-unconventional-assign-operator )
+ optional_REQUIRES_R(
+ optional&,
+ std::is_constructible< T , U>::value
+ && std11::is_assignable< T&, U>::value
+ && !std::is_constructible<T, optional<U> & >::value
+ && !std::is_constructible<T, optional<U> && >::value
+ && !std::is_constructible<T, optional<U> const & >::value
+ && !std::is_constructible<T, optional<U> const && >::value
+ && !std::is_convertible< optional<U> & , T>::value
+ && !std::is_convertible< optional<U> && , T>::value
+ && !std::is_convertible< optional<U> const & , T>::value
+ && !std::is_convertible< optional<U> const &&, T>::value
+ && !std11::is_assignable< T&, optional<U> & >::value
+ && !std11::is_assignable< T&, optional<U> && >::value
+ && !std11::is_assignable< T&, optional<U> const & >::value
+ && !std11::is_assignable< T&, optional<U> const && >::value
+ )
+ operator=( optional<U> && other )
+ {
+ return *this = optional( std::move( other ) );
+ }
+
+ // 7 (C++11) - emplace
+ template< typename... Args
+ optional_REQUIRES_T(
+ std::is_constructible<T, Args&&...>::value
+ )
+ >
+ T& emplace( Args&&... args )
+ {
+ *this = nullopt;
+ contained.emplace( std::forward<Args>(args)... );
+ has_value_ = true;
+ return contained.value();
+ }
+
+ // 8 (C++11) - emplace, initializer-list
+ template< typename U, typename... Args
+ optional_REQUIRES_T(
+ std::is_constructible<T, std::initializer_list<U>&, Args&&...>::value
+ )
+ >
+ T& emplace( std::initializer_list<U> il, Args&&... args )
+ {
+ *this = nullopt;
+ contained.emplace( il, std::forward<Args>(args)... );
+ has_value_ = true;
+ return contained.value();
+ }
+
+#endif // optional_CPP11_OR_GREATER
+
+ // x.x.3.4, swap
+
+ void swap( optional & other )
+#if optional_CPP11_OR_GREATER
+ noexcept(
+ std11::is_nothrow_move_constructible<T>::value
+ && std17::is_nothrow_swappable<T>::value
+ )
+#endif
+ {
+ using std::swap;
+ if ( (has_value() == true ) && (other.has_value() == true ) ) { swap( **this, *other ); }
+ else if ( (has_value() == false) && (other.has_value() == true ) ) { initialize( std11::move(*other) ); other.reset(); }
+ else if ( (has_value() == true ) && (other.has_value() == false) ) { other.initialize( std11::move(**this) ); reset(); }
+ }
+
+ // x.x.3.5, observers
+
+ optional_constexpr value_type const * operator ->() const
+ {
+ return assert( has_value() ),
+ contained.value_ptr();
+ }
+
+ optional_constexpr14 value_type * operator ->()
+ {
+ return assert( has_value() ),
+ contained.value_ptr();
+ }
+
+ optional_constexpr value_type const & operator *() const optional_ref_qual
+ {
+ return assert( has_value() ),
+ contained.value();
+ }
+
+ optional_constexpr14 value_type & operator *() optional_ref_qual
+ {
+ return assert( has_value() ),
+ contained.value();
+ }
+
+#if optional_HAVE( REF_QUALIFIER )
+
+ optional_constexpr value_type const && operator *() const optional_refref_qual
+ {
+ return std::move( **this );
+ }
+
+ optional_constexpr14 value_type && operator *() optional_refref_qual
+ {
+ return std::move( **this );
+ }
+
+#endif
+
+#if optional_CPP11_OR_GREATER
+ optional_constexpr explicit operator bool() const optional_noexcept
+ {
+ return has_value();
+ }
+#else
+ optional_constexpr operator safe_bool() const optional_noexcept
+ {
+ return has_value() ? &optional::this_type_does_not_support_comparisons : 0;
+ }
+#endif
+
+ // NOLINTNEXTLINE( modernize-use-nodiscard )
+ /*optional_nodiscard*/ optional_constexpr bool has_value() const optional_noexcept
+ {
+ return has_value_;
+ }
+
+ // NOLINTNEXTLINE( modernize-use-nodiscard )
+ /*optional_nodiscard*/ optional_constexpr14 value_type const & value() const optional_ref_qual
+ {
+#if optional_CONFIG_NO_EXCEPTIONS
+ assert( has_value() );
+#else
+ if ( ! has_value() )
+ {
+ throw bad_optional_access();
+ }
+#endif
+ return contained.value();
+ }
+
+ optional_constexpr14 value_type & value() optional_ref_qual
+ {
+#if optional_CONFIG_NO_EXCEPTIONS
+ assert( has_value() );
+#else
+ if ( ! has_value() )
+ {
+ throw bad_optional_access();
+ }
+#endif
+ return contained.value();
+ }
+
+#if optional_HAVE( REF_QUALIFIER ) && ( !optional_COMPILER_GNUC_VERSION || optional_COMPILER_GNUC_VERSION >= 490 )
+
+ // NOLINTNEXTLINE( modernize-use-nodiscard )
+ /*optional_nodiscard*/ optional_constexpr value_type const && value() const optional_refref_qual
+ {
+ return std::move( value() );
+ }
+
+ optional_constexpr14 value_type && value() optional_refref_qual
+ {
+ return std::move( value() );
+ }
+
+#endif
+
+#if optional_HAVE( REF_QUALIFIER )
+
+ template< typename U >
+ optional_constexpr value_type value_or( U && v ) const optional_ref_qual
+ {
+ return has_value() ? contained.value() : static_cast<T>(std::forward<U>( v ) );
+ }
+
+ template< typename U >
+ optional_constexpr14 value_type value_or( U && v ) optional_refref_qual
+ {
+#if optional_COMPILER_CLANG_VERSION
+ return has_value() ? /*std::move*/( contained.value() ) : static_cast<T>(std::forward<U>( v ) );
+#else
+ return has_value() ? std::move( contained.value() ) : static_cast<T>(std::forward<U>( v ) );
+#endif
+ }
+
+#else
+
+ template< typename U >
+ optional_constexpr value_type value_or( U const & v ) const
+ {
+ return has_value() ? contained.value() : static_cast<value_type>( v );
+ }
+
+#endif // optional_CPP11_OR_GREATER
+
+ // x.x.3.6, modifiers
+
+ void reset() optional_noexcept
+ {
+ if ( has_value() )
+ {
+ contained.destruct_value();
+ }
+
+ has_value_ = false;
+ }
+
+ template<typename F>
+ auto map(F func) -> optional<decltype(func(this->value()))>;
+
+private:
+ void this_type_does_not_support_comparisons() const {}
+
+ template< typename V >
+ void initialize( V const & value )
+ {
+ assert( ! has_value() );
+ contained.construct_value( value );
+ has_value_ = true;
+ }
+
+#if optional_CPP11_OR_GREATER
+ template< typename V >
+ void initialize( V && value )
+ {
+ assert( ! has_value() );
+ contained.construct_value( std::move( value ) );
+ has_value_ = true;
+ }
+
+#endif
+
+private:
+ bool has_value_;
+ detail::storage_t< value_type > contained;
+
+};
+
+// Relational operators
+
+template< typename T, typename U >
+inline optional_constexpr bool operator==( optional<T> const & x, optional<U> const & y )
+{
+ return bool(x) != bool(y) ? false : !bool( x ) ? true : *x == *y;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator!=( optional<T> const & x, optional<U> const & y )
+{
+ return !(x == y);
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator<( optional<T> const & x, optional<U> const & y )
+{
+ return (!y) ? false : (!x) ? true : *x < *y;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator>( optional<T> const & x, optional<U> const & y )
+{
+ return (y < x);
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator<=( optional<T> const & x, optional<U> const & y )
+{
+ return !(y < x);
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator>=( optional<T> const & x, optional<U> const & y )
+{
+ return !(x < y);
+}
+
+// Comparison with nullopt
+
+template< typename T >
+inline optional_constexpr bool operator==( optional<T> const & x, nullopt_t /*unused*/ ) optional_noexcept
+{
+ return (!x);
+}
+
+template< typename T >
+inline optional_constexpr bool operator==( nullopt_t /*unused*/, optional<T> const & x ) optional_noexcept
+{
+ return (!x);
+}
+
+template< typename T >
+inline optional_constexpr bool operator!=( optional<T> const & x, nullopt_t /*unused*/ ) optional_noexcept
+{
+ return bool(x);
+}
+
+template< typename T >
+inline optional_constexpr bool operator!=( nullopt_t /*unused*/, optional<T> const & x ) optional_noexcept
+{
+ return bool(x);
+}
+
+template< typename T >
+inline optional_constexpr bool operator<( optional<T> const & /*unused*/, nullopt_t /*unused*/ ) optional_noexcept
+{
+ return false;
+}
+
+template< typename T >
+inline optional_constexpr bool operator<( nullopt_t /*unused*/, optional<T> const & x ) optional_noexcept
+{
+ return bool(x);
+}
+
+template< typename T >
+inline optional_constexpr bool operator<=( optional<T> const & x, nullopt_t /*unused*/ ) optional_noexcept
+{
+ return (!x);
+}
+
+template< typename T >
+inline optional_constexpr bool operator<=( nullopt_t /*unused*/, optional<T> const & /*unused*/ ) optional_noexcept
+{
+ return true;
+}
+
+template< typename T >
+inline optional_constexpr bool operator>( optional<T> const & x, nullopt_t /*unused*/ ) optional_noexcept
+{
+ return bool(x);
+}
+
+template< typename T >
+inline optional_constexpr bool operator>( nullopt_t /*unused*/, optional<T> const & /*unused*/ ) optional_noexcept
+{
+ return false;
+}
+
+template< typename T >
+inline optional_constexpr bool operator>=( optional<T> const & /*unused*/, nullopt_t /*unused*/ ) optional_noexcept
+{
+ return true;
+}
+
+template< typename T >
+inline optional_constexpr bool operator>=( nullopt_t /*unused*/, optional<T> const & x ) optional_noexcept
+{
+ return (!x);
+}
+
+// Comparison with T
+
+template< typename T, typename U >
+inline optional_constexpr bool operator==( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x == v : false;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator==( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v == *x : false;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator!=( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x != v : true;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator!=( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v != *x : true;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator<( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x < v : true;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator<( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v < *x : false;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator<=( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x <= v : true;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator<=( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v <= *x : false;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator>( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x > v : false;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator>( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v > *x : true;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator>=( optional<T> const & x, U const & v )
+{
+ return bool(x) ? *x >= v : false;
+}
+
+template< typename T, typename U >
+inline optional_constexpr bool operator>=( U const & v, optional<T> const & x )
+{
+ return bool(x) ? v >= *x : true;
+}
+
+// Specialized algorithms
+
+template< typename T
+#if optional_CPP11_OR_GREATER
+ optional_REQUIRES_T(
+ std11::is_move_constructible<T>::value
+ && std17::is_swappable<T>::value )
+#endif
+>
+void swap( optional<T> & x, optional<T> & y )
+#if optional_CPP11_OR_GREATER
+ noexcept( noexcept( x.swap(y) ) )
+#endif
+{
+ x.swap( y );
+}
+
+#if optional_CPP11_OR_GREATER
+
+template< typename T >
+optional_constexpr optional< typename std::decay<T>::type > make_optional( T && value )
+{
+ return optional< typename std::decay<T>::type >( std::forward<T>( value ) );
+}
+
+template< typename T, typename...Args >
+optional_constexpr optional<T> make_optional( Args&&... args )
+{
+ return optional<T>( nonstd_lite_in_place(T), std::forward<Args>(args)...);
+}
+
+template< typename T, typename U, typename... Args >
+optional_constexpr optional<T> make_optional( std::initializer_list<U> il, Args&&... args )
+{
+ return optional<T>( nonstd_lite_in_place(T), il, std::forward<Args>(args)...);
+}
+
+#else
+
+template< typename T >
+optional<T> make_optional( T const & value )
+{
+ return optional<T>( value );
+}
+
+#endif // optional_CPP11_OR_GREATER
+
+template<typename T>
+template<typename F>
+auto optional<T>::map(F func) -> optional<decltype(func(this->value()))>
+{
+ if (this->has_value()) {
+ return make_optional(func(this->value()));
+ }
+
+ return nullopt;
+}
+
+} // namespace optional_lite
+
+using optional_lite::optional;
+using optional_lite::nullopt_t;
+using optional_lite::nullopt;
+
+#if ! optional_CONFIG_NO_EXCEPTIONS
+using optional_lite::bad_optional_access;
+#endif
+
+using optional_lite::make_optional;
+
+} // namespace nonstd
+
+#if optional_CPP11_OR_GREATER
+
+// specialize the std::hash algorithm:
+
+namespace std {
+
+template< class T >
+struct hash< nonstd::optional<T> >
+{
+public:
+ std::size_t operator()( nonstd::optional<T> const & v ) const optional_noexcept
+ {
+ return bool( v ) ? std::hash<T>{}( *v ) : 0;
+ }
+};
+
+} //namespace std
+
+#endif // optional_CPP11_OR_GREATER
+
+#if defined(__clang__)
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#elif defined(_MSC_VER )
+# pragma warning( pop )
+#endif
+
+#endif // optional_USES_STD_OPTIONAL
+
+#endif // NONSTD_OPTIONAL_LITE_HPP
diff --git a/src/pcap_manager.cc b/src/pcap_manager.cc
new file mode 100644
index 0000000..dbe304a
--- /dev/null
+++ b/src/pcap_manager.cc
@@ -0,0 +1,139 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file pcap_manager.cc
+ */
+
+#include <memory>
+#include <thread>
+#include <vector>
+
+#include "pcap_manager.hh"
+
+#include <unistd.h>
+
+#include "base/fs_util.hh"
+#include "config.h"
+#include "line_buffer.hh"
+
+namespace pcap_manager {
+
+Result<convert_result, std::string>
+convert(const std::string& filename)
+{
+ log_info("attempting to convert pcap file -- %s", filename.c_str());
+
+ auto outfile = TRY(lnav::filesystem::open_temp_file(
+ ghc::filesystem::temp_directory_path() / "lnav.pcap.XXXXXX"));
+ ghc::filesystem::remove(outfile.first);
+ auto err_pipe = TRY(auto_pipe::for_child_fd(STDERR_FILENO));
+ auto child = TRY(lnav::pid::from_fork());
+
+ err_pipe.after_fork(child.in());
+ if (child.in_child()) {
+ auto dev_null = open("/dev/null", O_RDONLY);
+
+ dup2(dev_null, STDIN_FILENO);
+ dup2(outfile.second.release(), STDOUT_FILENO);
+ setenv("TZ", "UTC", 1);
+
+ const char* args[] = {
+ "tshark",
+ "-T",
+ "ek",
+ "-P",
+ "-V",
+ "-t",
+ "ad",
+ "-r",
+ filename.c_str(),
+ nullptr,
+ };
+
+ execvp("tshark", (char**) args);
+ if (errno == ENOENT) {
+ fprintf(stderr,
+ "pcap support requires 'tshark' v3+ to be installed\n");
+ } else {
+ fprintf(
+ stderr, "failed to execute 'tshark' -- %s\n", strerror(errno));
+ }
+ _exit(EXIT_FAILURE);
+ }
+
+ auto error_queue = std::make_shared<std::vector<std::string>>();
+ std::thread err_reader([err = std::move(err_pipe.read_end()),
+ error_queue,
+ child_pid = child.in()]() mutable {
+ line_buffer lb;
+ file_range pipe_range;
+ bool done = false;
+
+ lb.set_fd(err);
+ while (!done) {
+ auto load_res = lb.load_next_line(pipe_range);
+
+ if (load_res.isErr()) {
+ done = true;
+ } else {
+ auto li = load_res.unwrap();
+
+ pipe_range = li.li_file_range;
+ if (li.li_file_range.empty()) {
+ done = true;
+ } else {
+ lb.read_range(li.li_file_range)
+ .then([error_queue, child_pid](auto sbr) {
+ auto line_str = string_fragment(
+ sbr.get_data(), 0, sbr.length())
+ .trim("\n");
+ if (error_queue->size() < 5) {
+ error_queue->emplace_back(line_str.to_string());
+ }
+
+ log_debug("pcap[%d]: %.*s",
+ child_pid,
+ line_str.length(),
+ line_str.data());
+ });
+ }
+ }
+ }
+ });
+ err_reader.detach();
+
+ log_info("started tshark %d to process file", child.in());
+
+ return Ok(convert_result{
+ std::move(child),
+ std::move(outfile.second),
+ error_queue,
+ });
+}
+
+} // namespace pcap_manager
diff --git a/src/pcap_manager.hh b/src/pcap_manager.hh
new file mode 100644
index 0000000..319dfdc
--- /dev/null
+++ b/src/pcap_manager.hh
@@ -0,0 +1,54 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file pcap_manager.hh
+ */
+
+#ifndef lnav_pcap_manager_hh
+#define lnav_pcap_manager_hh
+
+#include <string>
+#include <vector>
+
+#include "base/auto_fd.hh"
+#include "base/auto_pid.hh"
+#include "base/result.h"
+
+namespace pcap_manager {
+
+struct convert_result {
+ auto_pid<process_state::running> cr_child;
+ auto_fd cr_destination;
+ std::shared_ptr<std::vector<std::string>> cr_error_queue;
+};
+
+Result<convert_result, std::string> convert(const std::string& filename);
+
+} // namespace pcap_manager
+
+#endif
diff --git a/src/pcrepp/CMakeLists.txt b/src/pcrepp/CMakeLists.txt
new file mode 100644
index 0000000..1af8845
--- /dev/null
+++ b/src/pcrepp/CMakeLists.txt
@@ -0,0 +1,16 @@
+add_library(pcrepp STATIC
+ ../config.h.in
+ pcre2pp.hh
+ pcre2pp.cc)
+
+target_include_directories(pcrepp PUBLIC . .. ../third-party/scnlib/include
+ ${CMAKE_CURRENT_BINARY_DIR}/..)
+target_link_libraries(pcrepp cppfmt pcre::libpcre pcre2::pcre2)
+
+add_executable(test_pcre2pp test_pcre2pp.cc)
+target_include_directories(
+ test_pcre2pp
+ PUBLIC
+ ../third-party/doctest-root)
+target_link_libraries(test_pcre2pp pcrepp)
+add_test(NAME test_pcre2pp COMMAND test_pcre2pp)
diff --git a/src/pcrepp/Makefile.am b/src/pcrepp/Makefile.am
new file mode 100644
index 0000000..72e8319
--- /dev/null
+++ b/src/pcrepp/Makefile.am
@@ -0,0 +1,33 @@
+
+include $(top_srcdir)/aminclude_static.am
+
+AM_CPPFLAGS = \
+ $(CODE_COVERAGE_CPPFLAGS) \
+ $(PCRE_CFLAGS) \
+ -Wall \
+ -I$(top_srcdir)/src \
+ -I$(top_srcdir)/src/fmtlib \
+ -I$(top_srcdir)/src/third-party/scnlib/include
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS)
+
+noinst_LIBRARIES = libpcrepp.a
+
+noinst_HEADERS = \
+ pcre2pp.hh
+
+libpcrepp_a_SOURCES = \
+ pcre2pp.cc
+
+test_pcre2pp_SOURCES = test_pcre2pp.cc
+test_pcre2pp_LDADD = \
+ libpcrepp.a \
+ $(PCRE_LIBS)
+
+check_PROGRAMS = \
+ test_pcre2pp
+
+TESTS = \
+ test_pcre2pp
diff --git a/src/pcrepp/pcre2pp.cc b/src/pcrepp/pcre2pp.cc
new file mode 100644
index 0000000..c7429d1
--- /dev/null
+++ b/src/pcrepp/pcre2pp.cc
@@ -0,0 +1,473 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file pcrepp.cc
+ */
+
+#include "pcre2pp.hh"
+
+#include "config.h"
+
+namespace lnav {
+namespace pcre2pp {
+
+std::string
+quote(const char* unquoted)
+{
+ std::string retval;
+
+ for (int lpc = 0; unquoted[lpc]; lpc++) {
+ if (isalnum(unquoted[lpc]) || unquoted[lpc] == '_'
+ || unquoted[lpc] & 0x80)
+ {
+ retval.push_back(unquoted[lpc]);
+ } else {
+ retval.push_back('\\');
+ retval.push_back(unquoted[lpc]);
+ }
+ }
+
+ return retval;
+}
+
+matcher
+capture_builder::into(lnav::pcre2pp::match_data& md) &&
+{
+ if (md.get_capacity() < this->mb_code.get_match_data_capacity()) {
+ md = this->mb_code.create_match_data();
+ }
+
+ return matcher{
+ this->mb_code,
+ this->mb_input,
+ md,
+ };
+}
+
+match_data
+code::create_match_data() const
+{
+ auto_mem<pcre2_match_data> md(pcre2_match_data_free);
+
+ md = pcre2_match_data_create_from_pattern(this->p_code, nullptr);
+
+ return match_data{std::move(md)};
+}
+
+Result<code, compile_error>
+code::from(string_fragment sf, int options)
+{
+ compile_error ce;
+ auto_mem<pcre2_code> co(pcre2_code_free);
+
+ options |= PCRE2_UTF;
+ co = pcre2_compile(
+ sf.udata(), sf.length(), options, &ce.ce_code, &ce.ce_offset, nullptr);
+
+ if (co == nullptr) {
+ ce.ce_pattern = sf.to_string();
+ return Err(ce);
+ }
+
+ auto jit_rc = pcre2_jit_compile(co, PCRE2_JIT_COMPLETE);
+ if (jit_rc < 0) {
+ // log_error("failed to JIT compile pattern: %d", jit_rc);
+ }
+
+ return Ok(code{std::move(co), sf.to_string()});
+}
+
+code::named_captures
+code::get_named_captures() const
+{
+ named_captures retval;
+
+ pcre2_pattern_info(
+ this->p_code.in(), PCRE2_INFO_NAMECOUNT, &retval.nc_count);
+ pcre2_pattern_info(
+ this->p_code.in(), PCRE2_INFO_NAMEENTRYSIZE, &retval.nc_entry_size);
+ pcre2_pattern_info(
+ this->p_code.in(), PCRE2_INFO_NAMETABLE, &retval.nc_name_table);
+
+ return retval;
+}
+
+size_t
+code::match_partial(string_fragment in) const
+{
+ auto md = this->create_match_data();
+ auto length = in.length();
+
+ do {
+ auto rc = pcre2_match(this->p_code.in(),
+ in.udata(),
+ length,
+ 0,
+ PCRE2_PARTIAL_HARD,
+ md.md_data.in(),
+ nullptr);
+
+ if (rc == PCRE2_ERROR_PARTIAL) {
+ return md.md_ovector[1];
+ }
+
+ if (length > 0) {
+ length -= 1;
+ }
+ } while (length > 0);
+
+ return 0;
+}
+
+const char*
+code::get_name_for_capture(size_t index) const
+{
+ for (const auto cap : this->get_named_captures()) {
+ if (cap.get_index() == index) {
+ return cap.get_name().data();
+ }
+ }
+
+ return nullptr;
+}
+
+size_t
+code::get_capture_count() const
+{
+ uint32_t retval;
+
+ pcre2_pattern_info(this->p_code.in(), PCRE2_INFO_CAPTURECOUNT, &retval);
+
+ return retval;
+}
+
+std::vector<string_fragment>
+code::get_captures() const
+{
+ bool in_class = false, in_escape = false, in_literal = false;
+ auto pat_frag = string_fragment::from_str(this->p_pattern);
+ std::vector<string_fragment> cap_in_progress;
+ std::vector<string_fragment> retval;
+
+ for (int lpc = 0; this->p_pattern[lpc]; lpc++) {
+ if (in_escape) {
+ in_escape = false;
+ if (this->p_pattern[lpc] == 'Q') {
+ in_literal = true;
+ }
+ } else if (in_class) {
+ if (this->p_pattern[lpc] == ']') {
+ in_class = false;
+ }
+ if (this->p_pattern[lpc] == '\\') {
+ in_escape = true;
+ }
+ } else if (in_literal) {
+ if (this->p_pattern[lpc] == '\\' && this->p_pattern[lpc + 1] == 'E')
+ {
+ in_literal = false;
+ lpc += 1;
+ }
+ } else {
+ switch (this->p_pattern[lpc]) {
+ case '\\':
+ in_escape = true;
+ break;
+ case '[':
+ in_class = true;
+ break;
+ case '(':
+ cap_in_progress.emplace_back(pat_frag.sub_range(lpc, lpc));
+ break;
+ case ')': {
+ if (!cap_in_progress.empty()) {
+ static const auto DEFINE_SF
+ = string_fragment::from_const("(?(DEFINE)");
+
+ auto& cap = cap_in_progress.back();
+ char first = '\0', second = '\0', third = '\0';
+ bool is_cap = false;
+
+ cap.sf_end = lpc + 1;
+ if (cap.length() >= 2) {
+ first = this->p_pattern[cap.sf_begin + 1];
+ }
+ if (cap.length() >= 3) {
+ second = this->p_pattern[cap.sf_begin + 2];
+ }
+ if (cap.length() >= 4) {
+ third = this->p_pattern[cap.sf_begin + 3];
+ }
+ if (cap.sf_begin >= 2) {
+ auto poss_define = string_fragment::from_str_range(
+ this->p_pattern, cap.sf_begin - 2, cap.sf_end);
+ if (poss_define == DEFINE_SF) {
+ cap_in_progress.pop_back();
+ continue;
+ }
+ }
+ if (first == '?') {
+ if (second == '\'') {
+ is_cap = true;
+ }
+ if (second == '<'
+ && (isalpha(third) || third == '_'))
+ {
+ is_cap = true;
+ }
+ if (second == 'P' && third == '<') {
+ is_cap = true;
+ }
+ } else if (first != '*') {
+ is_cap = true;
+ }
+ if (is_cap) {
+ retval.emplace_back(cap);
+ }
+ cap_in_progress.pop_back();
+ }
+ break;
+ }
+ }
+ }
+ }
+
+ assert((size_t) this->get_capture_count() == retval.size());
+
+ return retval;
+}
+
+std::string
+code::replace(string_fragment str, const char* repl) const
+{
+ std::string retval;
+ std::string::size_type start = 0;
+ string_fragment remaining = str;
+
+ auto md = this->create_match_data();
+ while (remaining.is_valid()) {
+ auto find_res = this->capture_from(str)
+ .at(remaining)
+ .into(md)
+ .matches()
+ .ignore_error();
+ if (!find_res) {
+ break;
+ }
+ auto all = find_res->f_all;
+ remaining = find_res->f_remaining;
+ bool in_escape = false;
+
+ retval.append(str.data(), start, (all.sf_begin - start));
+ start = all.sf_end;
+ for (int lpc = 0; repl[lpc]; lpc++) {
+ auto ch = repl[lpc];
+
+ if (in_escape) {
+ if (isdigit(ch)) {
+ auto capture_index = size_t(ch - '0');
+
+ if (capture_index < md.get_count()) {
+ auto cap = md[capture_index];
+ if (cap) {
+ retval.append(cap->data(), cap->length());
+ }
+ } else if (capture_index > this->get_capture_count()) {
+ retval.push_back('\\');
+ retval.push_back(ch);
+ }
+ } else {
+ if (ch != '\\') {
+ retval.push_back('\\');
+ }
+ retval.push_back(ch);
+ }
+ in_escape = false;
+ } else {
+ switch (ch) {
+ case '\\':
+ in_escape = true;
+ break;
+ default:
+ retval.push_back(ch);
+ break;
+ }
+ }
+ }
+ }
+ if (remaining.is_valid()) {
+ retval.append(str.data(), remaining.sf_begin, std::string::npos);
+ }
+
+ return retval;
+}
+
+int
+code::name_index(const char* name) const
+{
+ return pcre2_substring_number_from_name(this->p_code.in(),
+ (PCRE2_SPTR) name);
+}
+
+size_t
+code::named_capture::get_index() const
+{
+ return (this->nc_entry[0] << 8) | (this->nc_entry[1] & 0xff);
+}
+
+string_fragment
+code::named_capture::get_name() const
+{
+ return string_fragment::from_bytes(
+ &this->nc_entry[2], strlen((const char*) &this->nc_entry[2]));
+}
+
+code::named_capture
+code::named_captures::iterator::operator*() const
+{
+ return code::named_capture{this->i_entry};
+}
+
+code::named_captures::iterator&
+code::named_captures::iterator::operator++()
+{
+ this->i_entry += this->i_entry_size;
+
+ return *this;
+}
+
+bool
+code::named_captures::iterator::operator==(const iterator& other) const
+{
+ return this->i_entry == other.i_entry
+ && this->i_entry_size == other.i_entry_size;
+}
+
+bool
+code::named_captures::iterator::operator!=(const iterator& other) const
+{
+ return this->i_entry != other.i_entry
+ || this->i_entry_size != other.i_entry_size;
+}
+
+code::named_captures::iterator
+code::named_captures::begin() const
+{
+ return iterator{this->nc_entry_size, this->nc_name_table};
+}
+
+code::named_captures::iterator
+code::named_captures::end() const
+{
+ return iterator{
+ this->nc_entry_size,
+ this->nc_name_table + (this->nc_count * this->nc_entry_size),
+ };
+}
+
+matcher::matches_result
+matcher::matches(uint32_t options)
+{
+ this->mb_input.i_offset = this->mb_input.i_next_offset;
+
+ if (this->mb_input.i_offset == -1) {
+ return not_found{};
+ }
+
+ auto rc = pcre2_match(this->mb_code.p_code.in(),
+ this->mb_input.i_string.udata(),
+ this->mb_input.i_string.length(),
+ this->mb_input.i_offset,
+ options,
+ this->mb_match_data.md_data.in(),
+ nullptr);
+
+ if (rc > 0) {
+ this->mb_match_data.md_input = this->mb_input;
+ this->mb_match_data.md_code = &this->mb_code;
+ this->mb_match_data.md_capture_end = rc;
+ if (this->mb_match_data[0]->empty()
+ && this->mb_match_data[0]->sf_end >= this->mb_input.i_string.sf_end)
+ {
+ this->mb_input.i_next_offset = -1;
+ } else if (this->mb_match_data[0]->empty()) {
+ this->mb_input.i_next_offset
+ = this->mb_match_data.md_ovector[1] + 1;
+ } else {
+ this->mb_input.i_next_offset = this->mb_match_data.md_ovector[1];
+ }
+ this->mb_match_data.md_input.i_next_offset
+ = this->mb_input.i_next_offset;
+ return found{
+ this->mb_match_data[0].value(),
+ this->mb_match_data.remaining(),
+ };
+ }
+
+ this->mb_match_data.md_input = this->mb_input;
+ this->mb_match_data.md_ovector[0] = this->mb_input.i_offset;
+ this->mb_match_data.md_ovector[1] = this->mb_input.i_offset;
+ this->mb_match_data.md_capture_end = 1;
+ if (rc == PCRE2_ERROR_NOMATCH) {
+ return not_found{};
+ }
+
+ return error{&this->mb_code, rc};
+}
+
+void
+matcher::matches_result::handle_error(matcher::error err)
+{
+ unsigned char buffer[1024];
+
+ pcre2_get_error_message(err.e_error_code, buffer, sizeof(buffer));
+ // log_error("pcre2_match failure: %s", buffer);
+}
+
+std::string
+compile_error::get_message() const
+{
+ unsigned char buffer[1024];
+
+ pcre2_get_error_message(this->ce_code, buffer, sizeof(buffer));
+
+ return {(const char*) buffer};
+}
+
+std::string
+matcher::error::get_message()
+{
+ unsigned char buffer[1024];
+
+ pcre2_get_error_message(this->e_error_code, buffer, sizeof(buffer));
+
+ return {(const char*) buffer};
+}
+
+} // namespace pcre2pp
+} // namespace lnav
diff --git a/src/pcrepp/pcre2pp.hh b/src/pcrepp/pcre2pp.hh
new file mode 100644
index 0000000..59a2cf1
--- /dev/null
+++ b/src/pcrepp/pcre2pp.hh
@@ -0,0 +1,368 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_pcre2pp_hh
+#define lnav_pcre2pp_hh
+
+#define PCRE2_CODE_UNIT_WIDTH 8
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <pcre2.h>
+
+#include "base/auto_mem.hh"
+#include "base/intern_string.hh"
+#include "base/result.h"
+#include "mapbox/variant.hpp"
+
+namespace lnav {
+namespace pcre2pp {
+
+std::string quote(const char* unquoted);
+
+inline std::string
+quote(const std::string& unquoted)
+{
+ return quote(unquoted.c_str());
+}
+
+class code;
+struct capture_builder;
+class matcher;
+
+struct input {
+ string_fragment i_string;
+ int i_offset{0};
+ int i_next_offset{0};
+};
+
+class match_data {
+public:
+ static match_data unitialized() { return match_data{}; }
+
+ string_fragment leading() const
+ {
+ return this->md_input.i_string.sub_range(this->md_input.i_offset,
+ this->md_ovector[0]);
+ }
+
+ string_fragment remaining() const
+ {
+ if (this->md_capture_end == 0 || this->md_input.i_next_offset == -1) {
+ return string_fragment::invalid();
+ }
+
+ return string_fragment::from_byte_range(
+ this->md_input.i_string.sf_string,
+ this->md_input.i_string.sf_begin + this->md_input.i_next_offset,
+ this->md_input.i_string.sf_end);
+ }
+
+ nonstd::optional<string_fragment> operator[](size_t index) const
+ {
+ if (index >= this->md_capture_end) {
+ return nonstd::nullopt;
+ }
+
+ auto start = this->md_ovector[(index * 2)];
+ auto stop = this->md_ovector[(index * 2) + 1];
+ if (start == PCRE2_UNSET || stop == PCRE2_UNSET) {
+ return nonstd::nullopt;
+ }
+
+ return this->md_input.i_string.sub_range(start, stop);
+ }
+
+ template<typename T, std::size_t N>
+ nonstd::optional<string_fragment> operator[](const T (&name)[N]) const;
+
+ size_t get_count() const { return this->md_capture_end; }
+
+ uint32_t get_capacity() const { return this->md_ovector_count; }
+
+private:
+ friend matcher;
+ friend code;
+
+ match_data() = default;
+
+ explicit match_data(auto_mem<pcre2_match_data> dat)
+ : md_data(std::move(dat)),
+ md_ovector(pcre2_get_ovector_pointer(this->md_data.in())),
+ md_ovector_count(pcre2_get_ovector_count(this->md_data.in()))
+ {
+ }
+
+ auto_mem<pcre2_match_data> md_data;
+ const code* md_code{nullptr};
+ input md_input;
+ PCRE2_SIZE* md_ovector{nullptr};
+ uint32_t md_ovector_count{0};
+ size_t md_capture_end{0};
+};
+
+class matcher {
+public:
+ struct found {
+ string_fragment f_all;
+ string_fragment f_remaining;
+ };
+ struct not_found {};
+ struct error {
+ const code* e_code{nullptr};
+ int e_error_code{0};
+ std::string get_message();
+ };
+
+ class matches_result
+ : public mapbox::util::variant<found, not_found, error> {
+ public:
+ using variant::variant;
+
+ nonstd::optional<found> ignore_error()
+ {
+ return this->match(
+ [](found fo) { return nonstd::make_optional(fo); },
+ [](not_found) { return nonstd::nullopt; },
+ [](error err) {
+ handle_error(err);
+ return nonstd::nullopt;
+ });
+ }
+
+ private:
+ static void handle_error(error err);
+ };
+
+ matcher& reload_input(string_fragment sf, int next_offset)
+ {
+ this->mb_input = input{sf, next_offset, next_offset};
+
+ return *this;
+ }
+
+ matches_result matches(uint32_t options = 0);
+
+ int get_next_offset() const { return this->mb_input.i_next_offset; }
+
+private:
+ friend capture_builder;
+
+ matcher(const code& co, input& in, match_data& md)
+ : mb_code(co), mb_input(in), mb_match_data(md)
+ {
+ }
+
+ const code& mb_code;
+ input mb_input;
+ match_data& mb_match_data;
+};
+
+struct capture_builder {
+ const code& mb_code;
+ input mb_input;
+
+ capture_builder at(const string_fragment& remaining) &&
+ {
+ this->mb_input.i_offset = this->mb_input.i_next_offset
+ = remaining.sf_begin;
+ return *this;
+ }
+
+ matcher into(match_data& md) &&;
+
+ template<uint32_t Options = 0, typename F>
+ Result<string_fragment, matcher::error> for_each(F func) &&;
+};
+
+struct compile_error {
+ std::string ce_pattern;
+ int ce_code{0};
+ size_t ce_offset{0};
+
+ std::string get_message() const;
+};
+
+class code {
+public:
+ class named_capture {
+ public:
+ size_t get_index() const;
+ string_fragment get_name() const;
+
+ PCRE2_SPTR nc_entry;
+ };
+
+ class named_captures {
+ public:
+ struct iterator {
+ named_capture operator*() const;
+ iterator& operator++();
+ bool operator==(const iterator& other) const;
+ bool operator!=(const iterator& other) const;
+
+ uint32_t i_entry_size;
+ PCRE2_SPTR i_entry;
+ };
+
+ iterator begin() const;
+ iterator end() const;
+ bool empty() const { return this->nc_count == 0; }
+ size_t size() const { return this->nc_count; }
+
+ private:
+ friend code;
+
+ named_captures() = default;
+
+ uint32_t nc_count{0};
+ uint32_t nc_entry_size{0};
+ PCRE2_SPTR nc_name_table{nullptr};
+ };
+
+ static Result<code, compile_error> from(string_fragment sf,
+ int options = 0);
+
+ template<typename T, std::size_t N>
+ static code from_const(const T (&str)[N], int options = 0)
+ {
+ return from(string_fragment::from_const(str), options).unwrap();
+ }
+
+ const std::string& get_pattern() const { return this->p_pattern; }
+
+ named_captures get_named_captures() const;
+
+ const char* get_name_for_capture(size_t index) const;
+
+ size_t get_capture_count() const;
+
+ int name_index(const char* name) const;
+
+ std::vector<string_fragment> get_captures() const;
+
+ uint32_t get_match_data_capacity() const
+ {
+ return this->p_match_proto.md_ovector_count;
+ }
+
+ match_data create_match_data() const;
+
+ capture_builder capture_from(string_fragment in) const
+ {
+ return capture_builder{
+ *this,
+ input{in},
+ };
+ }
+
+ matcher::matches_result find_in(string_fragment in,
+ uint32_t options = 0) const
+ {
+ static thread_local match_data md = this->create_match_data();
+
+ if (md.md_ovector_count < this->p_match_proto.md_ovector_count) {
+ md = this->create_match_data();
+ }
+
+ return this->capture_from(in).into(md).matches(options);
+ }
+
+ size_t match_partial(string_fragment in) const;
+
+ std::string replace(string_fragment str, const char* repl) const;
+
+ std::shared_ptr<code> to_shared() &&
+ {
+ return std::make_shared<code>(std::move(this->p_code),
+ std::move(this->p_pattern));
+ }
+
+ code(auto_mem<pcre2_code> code, std::string pattern)
+ : p_code(std::move(code)), p_pattern(std::move(pattern)),
+ p_match_proto(this->create_match_data())
+ {
+ }
+
+private:
+ friend matcher;
+ friend match_data;
+
+ auto_mem<pcre2_code> p_code;
+ std::string p_pattern;
+ match_data p_match_proto;
+};
+
+template<typename T, std::size_t N>
+nonstd::optional<string_fragment>
+match_data::operator[](const T (&name)[N]) const
+{
+ auto index = pcre2_substring_number_from_name(
+ this->md_code->p_code.in(),
+ reinterpret_cast<const unsigned char*>(name));
+
+ return this->operator[](index);
+}
+
+template<uint32_t Options, typename F>
+Result<string_fragment, matcher::error>
+capture_builder::for_each(F func) &&
+{
+ auto md = this->mb_code.create_match_data();
+ auto mat = matcher{this->mb_code, this->mb_input, md};
+
+ bool done = false;
+ matcher::error eret;
+
+ while (!done) {
+ auto match_res = mat.matches(Options);
+ done = match_res.match(
+ [mat, &func](matcher::found) {
+ func(mat.mb_match_data);
+ return false;
+ },
+ [](matcher::not_found) { return true; },
+ [&eret](matcher::error err) {
+ eret = err;
+ return true;
+ });
+ }
+
+ if (eret.e_error_code == 0) {
+ return Ok(md.remaining());
+ }
+ return Err(eret);
+}
+
+} // namespace pcre2pp
+} // namespace lnav
+
+#endif
diff --git a/src/pcrepp/test_pcre2pp.cc b/src/pcrepp/test_pcre2pp.cc
new file mode 100644
index 0000000..ce5b0c5
--- /dev/null
+++ b/src/pcrepp/test_pcre2pp.cc
@@ -0,0 +1,260 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "doctest/doctest.h"
+#include "pcre2pp.hh"
+
+TEST_CASE("bad pattern")
+{
+ auto compile_res
+ = lnav::pcre2pp::code::from(string_fragment::from_const("[abc"));
+
+ CHECK(compile_res.isErr());
+ auto ce = compile_res.unwrapErr();
+ CHECK(ce.ce_offset == 4);
+}
+
+TEST_CASE("named captures")
+{
+ auto compile_res = lnav::pcre2pp::code::from(
+ string_fragment::from_const("(?<abc>a)(b)(?<def>c)"));
+
+ CHECK(compile_res.isOk());
+
+ const std::vector<std::pair<size_t, string_fragment>> expected_caps = {
+ {1, string_fragment::from_const("abc")},
+ {3, string_fragment::from_const("def")},
+ };
+
+ int caps_index = 0;
+ auto co = compile_res.unwrap();
+ for (const auto cap : co.get_named_captures()) {
+ const auto& expected_cap = expected_caps[caps_index];
+
+ CHECK(expected_cap.first == cap.get_index());
+ CHECK(expected_cap.second == cap.get_name());
+ caps_index += 1;
+ }
+}
+
+TEST_CASE("match")
+{
+ static const char INPUT[] = "key1=1234;key2=5678;";
+
+ auto co
+ = lnav::pcre2pp::code::from_const(R"((?<key>\w+)=(?<value>[^;]+);)");
+
+ co.capture_from(string_fragment::from_const(INPUT))
+ .for_each([](lnav::pcre2pp::match_data& md) {
+ printf("got '%s' %s = %s\n",
+ md[0]->to_string().c_str(),
+ md[1]->to_string().c_str(),
+ md[2]->to_string().c_str());
+ });
+}
+
+TEST_CASE("partial")
+{
+ static const char INPUT[] = "key1=1234";
+
+ auto co = lnav::pcre2pp::code::from_const(R"([a-z]+=.*)");
+ auto matched = co.match_partial(string_fragment::from_const(INPUT));
+ CHECK(matched == 3);
+}
+
+TEST_CASE("capture_name")
+{
+ auto co = lnav::pcre2pp::code::from_const("(?<abc>def)(ghi)");
+
+ CHECK(co.get_capture_count() == 2);
+ CHECK(string_fragment::from_c_str(co.get_name_for_capture(1)) == "abc");
+ CHECK(co.get_name_for_capture(2) == nullptr);
+}
+
+TEST_CASE("get_capture_count")
+{
+ auto co = lnav::pcre2pp::code::from_const("(DEFINE)");
+
+ CHECK(co.get_capture_count() == 1);
+}
+
+TEST_CASE("get_captures")
+{
+ auto co = lnav::pcre2pp::code::from_const(R"((?<abc>\w+)-(def)-)");
+
+ CHECK(co.get_capture_count() == 2);
+ const auto& caps = co.get_captures();
+ CHECK(caps.size() == 2);
+ CHECK(caps[0].to_string() == R"((?<abc>\w+))");
+ CHECK(caps[1].to_string() == R"((def))");
+}
+
+TEST_CASE("replace")
+{
+ static const char INPUT[] = "test 1 2 3";
+
+ auto co = lnav::pcre2pp::code::from_const(R"(\w*)");
+ auto in = string_fragment::from_const(INPUT);
+
+ auto res = co.replace(in, R"({\0})");
+ CHECK(res == "{test}{} {1}{} {2}{} {3}{}");
+}
+
+TEST_CASE("replace-empty")
+{
+ static const char INPUT[] = "";
+
+ auto co = lnav::pcre2pp::code::from_const(R"(\w*)");
+ auto in = string_fragment::from_const(INPUT);
+
+ auto res = co.replace(in, R"({\0})");
+ CHECK(res == "{}");
+}
+
+TEST_CASE("for_each-all")
+{
+ static const char INPUT[] = "Hello, World!\n";
+
+ auto co = lnav::pcre2pp::code::from_const(R"(.*)");
+ auto in = string_fragment::from_const(INPUT);
+
+ co.capture_from(in).for_each([](lnav::pcre2pp::match_data& md) {
+ printf("range %d:%d\n", md[0]->sf_begin, md[0]->sf_end);
+ });
+}
+
+TEST_CASE("capture_count")
+{
+ auto co = lnav::pcre2pp::code::from_const(R"(^(\w+)=([^;]+);)");
+
+ CHECK(co.get_capture_count() == 2);
+}
+
+TEST_CASE("no-caps")
+{
+ const static std::string empty_cap_regexes[] = {
+ "foo (?:bar)",
+ "foo [(]",
+ "foo \\Q(bar)\\E",
+ "(?i)",
+ };
+
+ for (auto re : empty_cap_regexes) {
+ auto co = lnav::pcre2pp::code::from(re).unwrap();
+
+ CHECK(co.get_captures().empty());
+ }
+}
+
+TEST_CASE("ipmatcher")
+{
+ auto co = lnav::pcre2pp::code::from_const(
+ R"((?(DEFINE)(?<byte>2[0-4]\d|25[0-5]|1\d\d|[1-9]?\d))\b(?&byte)(\.(?&byte)){3}\b)");
+ auto inp = string_fragment::from_const("192.168.1.1");
+
+ auto find_res = co.find_in(inp).ignore_error();
+ CHECK(find_res.has_value());
+ CHECK(find_res->f_all.sf_begin == 0);
+}
+
+TEST_CASE("get_captures-nested")
+{
+ auto re = lnav::pcre2pp::code::from_const("foo (bar (?:baz)?)");
+
+ CHECK(re.get_captures().size() == 1);
+ CHECK(re.get_captures()[0].sf_begin == 4);
+ CHECK(re.get_captures()[0].sf_end == 18);
+ CHECK(re.get_captures()[0].length() == 14);
+}
+
+TEST_CASE("get_captures-basic")
+{
+ auto re = lnav::pcre2pp::code::from_const("(a)(b)(c)");
+
+ assert(re.get_captures().size() == 3);
+ assert(re.get_captures()[0].sf_begin == 0);
+ assert(re.get_captures()[0].sf_end == 3);
+ assert(re.get_captures()[1].sf_begin == 3);
+ assert(re.get_captures()[1].sf_end == 6);
+ assert(re.get_captures()[2].sf_begin == 6);
+ assert(re.get_captures()[2].sf_end == 9);
+}
+
+TEST_CASE("get_captures-escape")
+{
+ auto re = lnav::pcre2pp::code::from_const("\\(a\\)(b)");
+
+ assert(re.get_captures().size() == 1);
+ assert(re.get_captures()[0].sf_begin == 5);
+ assert(re.get_captures()[0].sf_end == 8);
+}
+
+TEST_CASE("get_captures-named")
+{
+ auto re = lnav::pcre2pp::code::from_const("(?<named>b)");
+
+ assert(re.get_captures().size() == 1);
+ assert(re.get_captures()[0].sf_begin == 0);
+ assert(re.get_captures()[0].sf_end == 11);
+}
+
+TEST_CASE("get_captures-namedP")
+{
+ auto re = lnav::pcre2pp::code::from_const("(?P<named>b)");
+
+ assert(re.get_captures().size() == 1);
+ assert(re.get_captures()[0].sf_begin == 0);
+ assert(re.get_captures()[0].sf_end == 12);
+}
+
+TEST_CASE("get_captures-namedq")
+{
+ auto re = lnav::pcre2pp::code::from_const("(?'named'b)");
+
+ CHECK(re.get_captures().size() == 1);
+ CHECK(re.get_captures()[0].sf_begin == 0);
+ CHECK(re.get_captures()[0].sf_end == 11);
+}
+
+TEST_CASE("anchored")
+{
+ auto re = lnav::pcre2pp::code::from_const(
+ "abc", PCRE2_ANCHORED | PCRE2_ENDANCHORED);
+
+ const auto sub1 = string_fragment::from_const("abc");
+ const auto sub2 = string_fragment::from_const("abcd");
+ const auto sub3 = string_fragment::from_const("0abc");
+
+ CHECK(re.find_in(sub1).ignore_error().has_value());
+ CHECK_FALSE(re.find_in(sub2).ignore_error().has_value());
+ CHECK_FALSE(re.find_in(sub3).ignore_error().has_value());
+}
diff --git a/src/piper_proc.cc b/src/piper_proc.cc
new file mode 100644
index 0000000..396f175
--- /dev/null
+++ b/src/piper_proc.cc
@@ -0,0 +1,237 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file piper_proc.cc
+ */
+
+#include "piper_proc.hh"
+
+#include <errno.h>
+#include <fcntl.h>
+#include <poll.h>
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/time.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "base/fs_util.hh"
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "line_buffer.hh"
+
+using namespace std::chrono_literals;
+
+static const char* STDIN_EOF_MSG = "---- END-OF-STDIN ----";
+
+static ssize_t
+write_timestamp(int fd, off_t woff)
+{
+ char time_str[64];
+ struct timeval tv;
+ char ms_str[10];
+
+ gettimeofday(&tv, nullptr);
+ strftime(time_str, sizeof(time_str), "%FT%T", localtime(&tv.tv_sec));
+ snprintf(ms_str, sizeof(ms_str), ".%03d", (int) (tv.tv_usec / 1000));
+ strcat(time_str, ms_str);
+ strcat(time_str, " ");
+ return pwrite(fd, time_str, strlen(time_str), woff);
+}
+
+piper_proc::piper_proc(auto_fd pipefd, bool timestamp, auto_fd filefd)
+ : pp_fd(std::move(filefd)), pp_child(-1)
+{
+ require(pipefd.get() >= 0);
+ require(this->pp_fd.get() >= 0);
+
+ log_perror(fcntl(this->pp_fd.get(), F_SETFD, FD_CLOEXEC));
+
+ this->pp_child = fork();
+ switch (this->pp_child) {
+ case -1:
+ throw error(errno);
+
+ case 0: {
+ line_buffer lb;
+ off_t woff = 0, last_woff = 0;
+ file_range last_range;
+
+ auto open_res = lnav::filesystem::open_file("/dev/null", O_RDWR);
+ if (open_res.isErr()) {
+ fprintf(stderr,
+ "unable to open /dev/null: %s\n",
+ open_res.unwrapErr().c_str());
+ exit(EXIT_FAILURE);
+ }
+ auto nullfd = open_res.unwrap();
+ if (pipefd != STDIN_FILENO) {
+ dup2(nullfd, STDIN_FILENO);
+ }
+ dup2(nullfd, STDOUT_FILENO);
+ for (int fd_to_close = 0; fd_to_close < 1024; fd_to_close++) {
+ int flags;
+
+ if (fd_to_close == this->pp_fd.get()) {
+ continue;
+ }
+ if ((flags = fcntl(fd_to_close, F_GETFD)) == -1) {
+ continue;
+ }
+ if (flags & FD_CLOEXEC) {
+ close(fd_to_close);
+ }
+ }
+ log_perror(fcntl(pipefd.get(), F_SETFL, O_NONBLOCK));
+ lb.set_fd(pipefd);
+ do {
+ static const auto TIMEOUT
+ = std::chrono::duration_cast<std::chrono::milliseconds>(1s)
+ .count();
+ struct pollfd pfd = {lb.get_fd(), POLLIN, 0};
+
+ auto poll_rc = poll(&pfd, 1, TIMEOUT);
+ if (poll_rc == 0) {
+ // update the timestamp to keep the file alive from any
+ // cleanup processes
+ log_perror(futimes(this->pp_fd.get(), nullptr));
+ continue;
+ }
+ while (true) {
+ auto load_result = lb.load_next_line(last_range);
+
+ if (load_result.isErr()) {
+ break;
+ }
+
+ auto li = load_result.unwrap();
+
+ if (li.li_partial && !lb.is_pipe_closed()) {
+ break;
+ }
+
+ if (li.li_file_range.empty()) {
+ break;
+ }
+
+ auto read_result = lb.read_range(li.li_file_range);
+
+ if (read_result.isErr()) {
+ break;
+ }
+
+ auto sbr = read_result.unwrap();
+
+ ssize_t wrc;
+
+ last_woff = woff;
+ if (timestamp) {
+ wrc = write_timestamp(this->pp_fd, woff);
+ if (wrc == -1) {
+ perror("Unable to write to output file for stdin");
+ break;
+ }
+ woff += wrc;
+ }
+
+ /* Need to do pwrite here since the fd is used by the main
+ * lnav process as well.
+ */
+ wrc = pwrite(
+ this->pp_fd, sbr.get_data(), sbr.length(), woff);
+ if (wrc == -1) {
+ perror("Unable to write to output file for stdin");
+ break;
+ }
+ woff += wrc;
+
+ last_range = li.li_file_range;
+ if (li.li_partial
+ && sbr.get_data()[sbr.length() - 1] != '\n'
+ && (last_range.next_offset() != lb.get_file_size()))
+ {
+ woff = last_woff;
+ }
+ }
+ } while (lb.is_pipe() && !lb.is_pipe_closed());
+
+ if (timestamp) {
+ ssize_t wrc;
+
+ wrc = write_timestamp(this->pp_fd, woff);
+ if (wrc == -1) {
+ perror("Unable to write to output file for stdin");
+ break;
+ }
+ woff += wrc;
+ wrc = pwrite(
+ this->pp_fd, STDIN_EOF_MSG, strlen(STDIN_EOF_MSG), woff);
+ if (wrc == -1) {
+ perror("Unable to write to output file for stdin");
+ break;
+ }
+ }
+ }
+ _exit(0);
+ break;
+
+ default:
+ break;
+ }
+}
+
+bool
+piper_proc::has_exited()
+{
+ if (this->pp_child > 0) {
+ int rc, status;
+
+ rc = waitpid(this->pp_child, &status, WNOHANG);
+ if (rc == -1 || rc == 0) {
+ return false;
+ }
+ this->pp_child = -1;
+ }
+
+ return true;
+}
+
+piper_proc::~piper_proc()
+{
+ if (this->pp_child > 0) {
+ int status;
+
+ kill(this->pp_child, SIGTERM);
+ while (waitpid(this->pp_child, &status, 0) < 0 && (errno == EINTR)) {
+ ;
+ }
+
+ this->pp_child = -1;
+ }
+}
diff --git a/src/piper_proc.hh b/src/piper_proc.hh
new file mode 100644
index 0000000..0caf29e
--- /dev/null
+++ b/src/piper_proc.hh
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file piper_proc.hh
+ */
+
+#ifndef piper_proc_hh
+#define piper_proc_hh
+
+#include <string>
+
+#include <sys/types.h>
+
+#include "base/auto_fd.hh"
+
+/**
+ * Creates a subprocess that reads data from a pipe and writes it to a file so
+ * lnav can treat it like any other file and do preads.
+ *
+ * TODO: Add support for gzipped files.
+ */
+class piper_proc {
+public:
+ class error : public std::exception {
+ public:
+ error(int err) : e_err(err) {}
+
+ int e_err;
+ };
+
+ /**
+ * Forks a subprocess that will read data from the given file descriptor
+ * and write it to a temporary file.
+ *
+ * @param pipefd The file descriptor to read the file contents from.
+ * @param timestamp True if an ISO 8601 timestamp should be prepended onto
+ * the lines read from pipefd.
+ * @param filefd The descriptor for the backing file.
+ */
+ piper_proc(auto_fd pipefd, bool timestamp, auto_fd filefd);
+
+ bool has_exited();
+
+ /**
+ * Terminates the child process.
+ */
+ virtual ~piper_proc();
+
+ /** @return The file descriptor for the temporary file. */
+ auto_fd get_fd() { return this->pp_fd.dup(); }
+
+ pid_t get_child_pid() const { return this->pp_child; }
+
+private:
+ /** A file descriptor that refers to the temporary file. */
+ auto_fd pp_fd;
+
+ /** The child process' pid. */
+ pid_t pp_child;
+};
+#endif
diff --git a/src/plain_text_source.cc b/src/plain_text_source.cc
new file mode 100644
index 0000000..632a541
--- /dev/null
+++ b/src/plain_text_source.cc
@@ -0,0 +1,373 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "plain_text_source.hh"
+
+#include "base/itertools.hh"
+#include "config.h"
+
+static std::vector<plain_text_source::text_line>
+to_text_line(const std::vector<attr_line_t>& lines)
+{
+ file_off_t off = 0;
+
+ return lines | lnav::itertools::map([&off](const auto& elem) {
+ auto retval = plain_text_source::text_line{
+ off,
+ elem,
+ };
+
+ off += elem.length() + 1;
+ return retval;
+ });
+}
+
+plain_text_source::plain_text_source(const std::string& text)
+{
+ size_t start = 0, end;
+
+ while ((end = text.find('\n', start)) != std::string::npos) {
+ size_t len = (end - start);
+ this->tds_lines.emplace_back(start, text.substr(start, len));
+ start = end + 1;
+ }
+ if (start < text.length()) {
+ this->tds_lines.emplace_back(start, text.substr(start));
+ }
+ this->tds_longest_line = this->compute_longest_line();
+}
+
+plain_text_source::plain_text_source(const std::vector<std::string>& text_lines)
+{
+ this->replace_with(text_lines);
+}
+
+plain_text_source::plain_text_source(const std::vector<attr_line_t>& text_lines)
+ : tds_lines(to_text_line(text_lines))
+{
+ this->tds_longest_line = this->compute_longest_line();
+}
+
+plain_text_source&
+plain_text_source::replace_with(const attr_line_t& text_lines)
+{
+ this->tds_lines.clear();
+ this->tds_doc_sections = lnav::document::discover_metadata(text_lines);
+
+ file_off_t off = 0;
+ for (auto& line : text_lines.split_lines()) {
+ auto line_len = line.length() + 1;
+ this->tds_lines.emplace_back(off, std::move(line));
+ off += line_len;
+ }
+ this->tds_longest_line = this->compute_longest_line();
+ return *this;
+}
+
+plain_text_source&
+plain_text_source::replace_with(const std::vector<std::string>& text_lines)
+{
+ file_off_t off = 0;
+ for (const auto& str : text_lines) {
+ this->tds_lines.emplace_back(off, str);
+ off += str.length() + 1;
+ }
+ this->tds_longest_line = this->compute_longest_line();
+ return *this;
+}
+
+void
+plain_text_source::clear()
+{
+ this->tds_lines.clear();
+ this->tds_longest_line = 0;
+ this->tds_text_format = text_format_t::TF_UNKNOWN;
+}
+
+plain_text_source&
+plain_text_source::truncate_to(size_t max_lines)
+{
+ while (this->tds_lines.size() > max_lines) {
+ this->tds_lines.pop_back();
+ }
+ return *this;
+}
+
+size_t
+plain_text_source::text_line_width(textview_curses& curses)
+{
+ return this->tds_longest_line;
+}
+
+void
+plain_text_source::text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& value_out,
+ text_sub_source::line_flags_t flags)
+{
+ value_out = this->tds_lines[row].tl_value.get_string();
+}
+
+void
+plain_text_source::text_attrs_for_line(textview_curses& tc,
+ int line,
+ string_attrs_t& value_out)
+{
+ value_out = this->tds_lines[line].tl_value.get_attrs();
+ if (this->tds_reverse_selection && tc.is_selectable()
+ && tc.get_selection() == line)
+ {
+ value_out.emplace_back(line_range{0, -1},
+ VC_STYLE.value(text_attrs{A_REVERSE}));
+ }
+}
+
+size_t
+plain_text_source::text_size_for_line(textview_curses& tc,
+ int row,
+ text_sub_source::line_flags_t flags)
+{
+ return this->tds_lines[row].tl_value.length();
+}
+
+text_format_t
+plain_text_source::get_text_format() const
+{
+ return this->tds_text_format;
+}
+
+size_t
+plain_text_source::compute_longest_line()
+{
+ size_t retval = 0;
+ for (auto& iter : this->tds_lines) {
+ retval = std::max(retval, (size_t) iter.tl_value.length());
+ }
+ return retval;
+}
+
+nonstd::optional<vis_line_t>
+plain_text_source::line_for_offset(file_off_t off) const
+{
+ struct cmper {
+ bool operator()(const file_off_t& lhs, const text_line& rhs)
+ {
+ return lhs < rhs.tl_offset;
+ }
+
+ bool operator()(const text_line& lhs, const file_off_t& rhs)
+ {
+ return lhs.tl_offset < rhs;
+ }
+ };
+
+ if (this->tds_lines.empty()) {
+ return nonstd::nullopt;
+ }
+
+ auto iter = std::lower_bound(
+ this->tds_lines.begin(), this->tds_lines.end(), off, cmper{});
+ if (iter == this->tds_lines.end()) {
+ if (this->tds_lines.back().contains_offset(off)) {
+ return nonstd::make_optional(
+ vis_line_t(std::distance(this->tds_lines.end() - 1, iter)));
+ }
+ return nonstd::nullopt;
+ }
+
+ if (!iter->contains_offset(off) && iter != this->tds_lines.begin()) {
+ --iter;
+ }
+
+ return nonstd::make_optional(
+ vis_line_t(std::distance(this->tds_lines.begin(), iter)));
+}
+
+void
+plain_text_source::text_crumbs_for_line(int line,
+ std::vector<breadcrumb::crumb>& crumbs)
+{
+ const auto initial_size = crumbs.size();
+ const auto& tl = this->tds_lines[line];
+
+ this->tds_doc_sections.m_sections_tree.visit_overlapping(
+ tl.tl_offset,
+ [&crumbs, initial_size, meta = &this->tds_doc_sections, this](
+ const auto& iv) {
+ auto path = crumbs | lnav::itertools::skip(initial_size)
+ | lnav::itertools::map(&breadcrumb::crumb::c_key)
+ | lnav::itertools::append(iv.value);
+ crumbs.template emplace_back(
+ iv.value,
+ [meta, path]() { return meta->possibility_provider(path); },
+ [this, meta, path](const auto& key) {
+ auto curr_node = lnav::document::hier_node::lookup_path(
+ meta->m_sections_root.get(), path);
+ if (!curr_node) {
+ return;
+ }
+ auto* parent_node = curr_node.value()->hn_parent;
+
+ if (parent_node == nullptr) {
+ return;
+ }
+ key.template match(
+ [this, parent_node](const std::string& str) {
+ auto sib_iter
+ = parent_node->hn_named_children.find(str);
+ if (sib_iter
+ == parent_node->hn_named_children.end()) {
+ return;
+ }
+ this->line_for_offset(sib_iter->second->hn_start) |
+ [this](const auto new_top) {
+ this->tss_view->set_top(new_top);
+ };
+ },
+ [this, parent_node](size_t index) {
+ if (index >= parent_node->hn_children.size()) {
+ return;
+ }
+ auto sib = parent_node->hn_children[index].get();
+ this->line_for_offset(sib->hn_start) |
+ [this](const auto new_top) {
+ this->tss_view->set_selection(new_top);
+ };
+ });
+ });
+ });
+
+ auto path = crumbs | lnav::itertools::skip(initial_size)
+ | lnav::itertools::map(&breadcrumb::crumb::c_key);
+ auto node = lnav::document::hier_node::lookup_path(
+ this->tds_doc_sections.m_sections_root.get(), path);
+
+ if (node && !node.value()->hn_children.empty()) {
+ auto poss_provider = [curr_node = node.value()]() {
+ std::vector<breadcrumb::possibility> retval;
+ for (const auto& child : curr_node->hn_named_children) {
+ retval.template emplace_back(child.first);
+ }
+ return retval;
+ };
+ auto path_performer = [this, curr_node = node.value()](
+ const breadcrumb::crumb::key_t& value) {
+ value.template match(
+ [this, curr_node](const std::string& str) {
+ auto child_iter = curr_node->hn_named_children.find(str);
+ if (child_iter != curr_node->hn_named_children.end()) {
+ this->line_for_offset(child_iter->second->hn_start) |
+ [this](const auto new_top) {
+ this->tss_view->set_top(new_top);
+ };
+ }
+ },
+ [this, curr_node](size_t index) {
+ auto* child = curr_node->hn_children[index].get();
+ this->line_for_offset(child->hn_start) |
+ [this](const auto new_top) {
+ this->tss_view->set_top(new_top);
+ };
+ });
+ };
+ crumbs.emplace_back(
+ "", "\u22ef", std::move(poss_provider), std::move(path_performer));
+ crumbs.back().c_expected_input = node.value()->hn_named_children.empty()
+ ? breadcrumb::crumb::expected_input_t::index
+ : breadcrumb::crumb::expected_input_t::index_or_exact;
+ }
+}
+
+nonstd::optional<vis_line_t>
+plain_text_source::row_for_anchor(const std::string& id)
+{
+ nonstd::optional<vis_line_t> retval;
+
+ if (this->tds_doc_sections.m_sections_root == nullptr) {
+ return retval;
+ }
+
+ lnav::document::hier_node::depth_first(
+ this->tds_doc_sections.m_sections_root.get(),
+ [this, &id, &retval](const lnav::document::hier_node* node) {
+ for (const auto& child_pair : node->hn_named_children) {
+ auto child_anchor
+ = text_anchors::to_anchor_string(child_pair.first);
+
+ if (child_anchor == id) {
+ retval = this->line_for_offset(child_pair.second->hn_start);
+ }
+ }
+ });
+
+ return retval;
+}
+
+std::unordered_set<std::string>
+plain_text_source::get_anchors()
+{
+ std::unordered_set<std::string> retval;
+
+ lnav::document::hier_node::depth_first(
+ this->tds_doc_sections.m_sections_root.get(),
+ [&retval](const lnav::document::hier_node* node) {
+ for (const auto& child_pair : node->hn_named_children) {
+ retval.emplace(
+ text_anchors::to_anchor_string(child_pair.first));
+ }
+ });
+
+ return retval;
+}
+
+nonstd::optional<std::string>
+plain_text_source::anchor_for_row(vis_line_t vl)
+{
+ nonstd::optional<std::string> retval;
+
+ if (vl > this->tds_lines.size()
+ || this->tds_doc_sections.m_sections_root == nullptr)
+ {
+ return retval;
+ }
+
+ const auto& tl = this->tds_lines[vl];
+
+ this->tds_doc_sections.m_sections_tree.visit_overlapping(
+ tl.tl_offset, [&retval](const lnav::document::section_interval_t& iv) {
+ retval = iv.value.match(
+ [](const std::string& str) {
+ return nonstd::make_optional(
+ text_anchors::to_anchor_string(str));
+ },
+ [](size_t) { return nonstd::nullopt; });
+ });
+
+ return retval;
+}
diff --git a/src/plain_text_source.hh b/src/plain_text_source.hh
new file mode 100644
index 0000000..3cddb1c
--- /dev/null
+++ b/src/plain_text_source.hh
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LNAV_PLAIN_TEXT_SOURCE_HH
+#define LNAV_PLAIN_TEXT_SOURCE_HH
+
+#include <string>
+#include <vector>
+
+#include "base/attr_line.hh"
+#include "base/file_range.hh"
+#include "document.sections.hh"
+#include "textview_curses.hh"
+
+class plain_text_source
+ : public text_sub_source
+ , public vis_location_history
+ , public text_anchors {
+public:
+ struct text_line {
+ text_line(file_off_t off, attr_line_t value)
+ : tl_offset(off), tl_value(std::move(value))
+ {
+ }
+
+ bool contains_offset(file_off_t off) const
+ {
+ return (this->tl_offset <= off
+ && off < this->tl_offset + this->tl_value.length());
+ }
+
+ file_off_t tl_offset;
+ attr_line_t tl_value;
+ };
+
+ plain_text_source() = default;
+
+ plain_text_source(const std::string& text);
+
+ plain_text_source(const std::vector<std::string>& text_lines);
+
+ plain_text_source(const std::vector<attr_line_t>& text_lines);
+
+ plain_text_source& set_reverse_selection(bool val)
+ {
+ this->tds_reverse_selection = val;
+ return *this;
+ }
+
+ plain_text_source& replace_with(const attr_line_t& text_lines);
+
+ plain_text_source& replace_with(const std::vector<std::string>& text_lines);
+
+ void clear();
+
+ plain_text_source& truncate_to(size_t max_lines);
+
+ size_t text_line_count() override { return this->tds_lines.size(); }
+
+ bool empty() const { return this->tds_lines.empty(); }
+
+ size_t text_line_width(textview_curses& curses) override;
+
+ void text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& value_out,
+ line_flags_t flags) override;
+
+ void text_attrs_for_line(textview_curses& tc,
+ int line,
+ string_attrs_t& value_out) override;
+
+ size_t text_size_for_line(textview_curses& tc,
+ int row,
+ line_flags_t flags) override;
+
+ text_format_t get_text_format() const override;
+
+ const std::vector<text_line>& get_lines() const { return this->tds_lines; }
+
+ plain_text_source& set_text_format(text_format_t format)
+ {
+ this->tds_text_format = format;
+ return *this;
+ }
+
+ nonstd::optional<location_history*> get_location_history() override
+ {
+ return this;
+ }
+
+ void text_crumbs_for_line(int line,
+ std::vector<breadcrumb::crumb>& crumbs) override;
+
+ nonstd::optional<vis_line_t> row_for_anchor(const std::string& id) override;
+ nonstd::optional<std::string> anchor_for_row(vis_line_t vl) override;
+ std::unordered_set<std::string> get_anchors() override;
+
+protected:
+ size_t compute_longest_line();
+
+ nonstd::optional<vis_line_t> line_for_offset(file_off_t off) const;
+
+ std::vector<text_line> tds_lines;
+ text_format_t tds_text_format{text_format_t::TF_UNKNOWN};
+ size_t tds_longest_line{0};
+ bool tds_reverse_selection{false};
+ lnav::document::metadata tds_doc_sections;
+};
+
+#endif // LNAV_PLAIN_TEXT_SOURCE_HH
diff --git a/src/plugins.hh b/src/plugins.hh
new file mode 100644
index 0000000..1a90972
--- /dev/null
+++ b/src/plugins.hh
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef plugins_hh
+#define plugins_hh
+
+#include <string>
+
+class extension_point {
+public:
+ extension_point(std::string name);
+ ~extension_point();
+
+ std::string get_name()
+ {
+ return this->ep_name;
+ };
+
+private:
+ std::string ep_name;
+};
+#endif
diff --git a/src/pollable.cc b/src/pollable.cc
new file mode 100644
index 0000000..c3474f2
--- /dev/null
+++ b/src/pollable.cc
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+
+#include "pollable.hh"
+
+#include "base/itertools.hh"
+#include "base/lnav_log.hh"
+
+pollable::pollable(std::shared_ptr<pollable_supervisor> supervisor,
+ category cat)
+ : p_supervisor(supervisor), p_category(cat)
+{
+ log_debug("pollable attach %p to %p", this, this->p_supervisor.get());
+ this->p_supervisor->attach(this);
+}
+
+pollable::~pollable()
+{
+ log_debug("pollable detach %p from %p", this, this->p_supervisor.get());
+ this->p_supervisor->detach(this);
+}
+
+pollable_supervisor::update_result
+pollable_supervisor::update_poll_set(std::vector<struct pollfd>& pollfds)
+{
+ update_result retval;
+ size_t old_size = pollfds.size();
+
+ for (auto& pol : this->b_components) {
+ pol->update_poll_set(pollfds);
+ switch (pol->get_category()) {
+ case pollable::category::background:
+ retval.ur_background += pollfds.size() - old_size;
+ break;
+ case pollable::category::interactive:
+ retval.ur_interactive += pollfds.size() - old_size;
+ break;
+ }
+ old_size = pollfds.size();
+ }
+
+ return retval;
+}
+
+void
+pollable_supervisor::check_poll_set(const std::vector<struct pollfd>& pollfds)
+{
+ std::vector<pollable*> visited;
+ auto found_new = false;
+
+ // TODO move this loop into the superclass
+ do {
+ found_new = false;
+ for (auto* pol : this->b_components) {
+ if (std::find(visited.begin(), visited.end(), pol) == visited.end())
+ {
+ visited.emplace_back(pol);
+ pol->check_poll_set(pollfds);
+ found_new = true;
+ break;
+ }
+ }
+ } while (found_new);
+}
+
+size_t
+pollable_supervisor::count(pollable::category cat)
+{
+ size_t retval = 0;
+
+ for (const auto* pol : this->b_components) {
+ if (pol->get_category() == cat) {
+ retval += 1;
+ }
+ }
+
+ return retval;
+}
+
+short
+pollfd_revents(const std::vector<struct pollfd>& pollfds, int fd)
+{
+ return pollfds | lnav::itertools::find_if([fd](const auto& entry) {
+ return entry.fd == fd;
+ })
+ | lnav::itertools::deref() | lnav::itertools::map(&pollfd::revents)
+ | lnav::itertools::unwrap_or((short) 0);
+}
+
+bool
+pollfd_ready(const std::vector<struct pollfd>& pollfds, int fd, short events)
+{
+ return std::any_of(
+ pollfds.begin(), pollfds.end(), [fd, events](const auto& entry) {
+ return entry.fd == fd && entry.revents & events;
+ });
+}
diff --git a/src/pollable.hh b/src/pollable.hh
new file mode 100644
index 0000000..a8b89a7
--- /dev/null
+++ b/src/pollable.hh
@@ -0,0 +1,86 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_pollable_hh
+#define lnav_pollable_hh
+
+#include <memory>
+#include <vector>
+
+#include <poll.h>
+
+#include "base/bus.hh"
+
+class pollable_supervisor;
+
+class pollable {
+public:
+ enum class category {
+ background,
+ interactive,
+ };
+
+ pollable(std::shared_ptr<pollable_supervisor> supervisor, category cat);
+
+ pollable(const pollable&) = delete;
+
+ virtual ~pollable();
+
+ category get_category() const { return this->p_category; }
+
+ virtual void update_poll_set(std::vector<struct pollfd>& pollfds) = 0;
+
+ virtual void check_poll_set(const std::vector<struct pollfd>& pollfds) = 0;
+
+private:
+ std::shared_ptr<pollable_supervisor> p_supervisor;
+ const category p_category;
+};
+
+class pollable_supervisor : public bus<pollable> {
+public:
+ struct update_result {
+ size_t ur_background{0};
+ size_t ur_interactive{0};
+ };
+
+ update_result update_poll_set(std::vector<struct pollfd>& pollfds);
+
+ void check_poll_set(const std::vector<struct pollfd>& pollfds);
+
+ size_t count(pollable::category cat);
+};
+
+short pollfd_revents(const std::vector<struct pollfd>& pollfds, int fd);
+
+bool pollfd_ready(const std::vector<struct pollfd>& pollfds,
+ int fd,
+ short events = POLLIN | POLLHUP);
+
+#endif
diff --git a/src/pretty_printer.cc b/src/pretty_printer.cc
new file mode 100644
index 0000000..2ff3e11
--- /dev/null
+++ b/src/pretty_printer.cc
@@ -0,0 +1,378 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "pretty_printer.hh"
+
+#include "base/string_util.hh"
+#include "config.h"
+
+void
+pretty_printer::append_to(attr_line_t& al)
+{
+ if (this->pp_scanner->get_init_offset() > 0) {
+ data_scanner::capture_t leading_cap = {
+ 0,
+ this->pp_scanner->get_init_offset(),
+ };
+
+ // this->pp_stream << pi.get_substr(&leading_cap);
+ this->pp_values.emplace_back(DT_WORD, leading_cap);
+ }
+
+ this->pp_scanner->reset();
+ while (true) {
+ auto tok_res = this->pp_scanner->tokenize2();
+ if (!tok_res) {
+ break;
+ }
+
+ element el(tok_res->tr_token, tok_res->tr_capture);
+
+ switch (el.e_token) {
+ case DT_XML_DECL_TAG:
+ case DT_XML_EMPTY_TAG:
+ if (this->pp_is_xml && this->pp_line_length > 0) {
+ this->start_new_line();
+ }
+ this->pp_values.emplace_back(el);
+ if (this->pp_is_xml) {
+ this->start_new_line();
+ }
+ continue;
+ case DT_XML_OPEN_TAG:
+ if (this->pp_is_xml) {
+ this->start_new_line();
+ this->write_element(el);
+ this->pp_interval_state.back().is_start
+ = this->pp_stream.tellp();
+ this->pp_interval_state.back().is_name
+ = tok_res->to_string();
+ this->descend();
+ } else {
+ this->pp_values.emplace_back(el);
+ }
+ continue;
+ case DT_XML_CLOSE_TAG:
+ this->flush_values();
+ this->ascend();
+ this->append_child_node();
+ this->write_element(el);
+ this->start_new_line();
+ continue;
+ case DT_LCURLY:
+ case DT_LSQUARE:
+ case DT_LPAREN:
+ this->flush_values(true);
+ this->pp_values.emplace_back(el);
+ this->descend();
+ this->pp_interval_state.back().is_start
+ = this->pp_stream.tellp();
+ continue;
+ case DT_RCURLY:
+ case DT_RSQUARE:
+ case DT_RPAREN:
+ this->flush_values();
+ if (this->pp_body_lines.top()) {
+ this->start_new_line();
+ }
+ this->ascend();
+ this->write_element(el);
+ continue;
+ case DT_COMMA:
+ if (this->pp_depth > 0) {
+ this->flush_values(true);
+ if (!this->pp_is_xml) {
+ this->append_child_node();
+ }
+ this->write_element(el);
+ this->start_new_line();
+ this->pp_interval_state.back().is_start
+ = this->pp_stream.tellp();
+ continue;
+ }
+ break;
+ case DT_WHITE:
+ if (this->pp_values.empty() && this->pp_depth == 0
+ && this->pp_line_length == 0)
+ {
+ this->pp_leading_indent = el.e_capture.length();
+ continue;
+ }
+ break;
+ default:
+ break;
+ }
+ this->pp_values.emplace_back(el);
+ }
+ while (this->pp_depth > 0) {
+ this->ascend();
+ }
+ this->flush_values();
+
+ attr_line_t combined;
+ combined.get_string() = this->pp_stream.str();
+ combined.get_attrs() = this->pp_attrs;
+
+ if (!al.empty()) {
+ al.append("\n");
+ }
+ al.append(combined);
+
+ if (this->pp_hier_stage != nullptr) {
+ this->pp_hier_stage->hn_parent = this->pp_hier_nodes.back().get();
+ this->pp_hier_nodes.back()->hn_children.push_back(
+ std::move(this->pp_hier_stage));
+ }
+ this->pp_hier_stage = std::move(this->pp_hier_nodes.back());
+ this->pp_hier_nodes.pop_back();
+ if (this->pp_hier_stage->hn_children.size() == 1
+ && this->pp_hier_stage->hn_named_children.empty())
+ {
+ this->pp_hier_stage
+ = std::move(this->pp_hier_stage->hn_children.front());
+ this->pp_hier_stage->hn_parent = nullptr;
+ }
+}
+
+void
+pretty_printer::write_element(const pretty_printer::element& el)
+{
+ if (this->pp_leading_indent == 0 && this->pp_line_length == 0
+ && el.e_token == DT_WHITE)
+ {
+ if (this->pp_depth == 0) {
+ this->pp_soft_indent += el.e_capture.length();
+ }
+ return;
+ }
+ if (((this->pp_leading_indent == 0)
+ || (this->pp_line_length <= this->pp_leading_indent))
+ && el.e_token == DT_LINE)
+ {
+ this->pp_soft_indent = 0;
+ if (this->pp_line_length > 0) {
+ this->pp_line_length = 0;
+ this->pp_stream << std::endl;
+ this->pp_body_lines.top() += 1;
+ }
+ return;
+ }
+ if (this->pp_line_length == 0) {
+ this->append_indent();
+ }
+ ssize_t start_size = this->pp_stream.tellp();
+ if (el.e_token == DT_QUOTED_STRING) {
+ auto_mem<char> unquoted_str((char*) malloc(el.e_capture.length() + 1));
+ const char* start
+ = this->pp_scanner->to_string_fragment(el.e_capture).data();
+ auto unq_len = unquote(unquoted_str.in(), start, el.e_capture.length());
+ data_scanner ds(
+ string_fragment::from_bytes(unquoted_str.in(), unq_len));
+ string_attrs_t sa;
+ pretty_printer str_pp(
+ &ds, sa, this->pp_leading_indent + this->pp_depth * 4);
+ attr_line_t result;
+ str_pp.append_to(result);
+ if (result.get_string().find('\n') != std::string::npos) {
+ switch (start[0]) {
+ case 'r':
+ case 'u':
+ this->pp_stream << start[0];
+ this->pp_stream << start[1] << start[1];
+ break;
+ default:
+ this->pp_stream << start[0] << start[0];
+ break;
+ }
+ this->pp_stream << std::endl << result.get_string();
+ if (result.empty() || result.get_string().back() != '\n') {
+ this->pp_stream << std::endl;
+ }
+ this->pp_stream << start[el.e_capture.length() - 1]
+ << start[el.e_capture.length() - 1];
+ } else {
+ this->pp_stream
+ << this->pp_scanner->to_string_fragment(el.e_capture);
+ }
+ } else {
+ this->pp_stream << this->pp_scanner->to_string_fragment(el.e_capture);
+ int shift_amount
+ = start_size - el.e_capture.c_begin - this->pp_shift_accum;
+ shift_string_attrs(this->pp_attrs, el.e_capture.c_begin, shift_amount);
+ this->pp_shift_accum = start_size - el.e_capture.c_begin;
+ }
+ this->pp_line_length += el.e_capture.length();
+ if (el.e_token == DT_LINE) {
+ this->pp_line_length = 0;
+ this->pp_body_lines.top() += 1;
+ }
+}
+
+void
+pretty_printer::append_indent()
+{
+ this->pp_stream << std::string(
+ this->pp_leading_indent + this->pp_soft_indent, ' ');
+ this->pp_soft_indent = 0;
+ if (this->pp_stream.tellp() == this->pp_leading_indent) {
+ return;
+ }
+ for (int lpc = 0; lpc < this->pp_depth; lpc++) {
+ this->pp_stream << " ";
+ }
+}
+
+bool
+pretty_printer::flush_values(bool start_on_depth)
+{
+ nonstd::optional<data_scanner::capture_t> last_key;
+ bool retval = false;
+
+ while (!this->pp_values.empty()) {
+ {
+ auto& el = this->pp_values.front();
+ this->write_element(this->pp_values.front());
+ switch (el.e_token) {
+ case DT_SYMBOL:
+ case DT_CONSTANT:
+ case DT_WORD:
+ case DT_QUOTED_STRING:
+ last_key = el.e_capture;
+ break;
+ case DT_COLON:
+ case DT_EQUALS:
+ if (last_key) {
+ this->pp_interval_state.back().is_name
+ = this->pp_scanner
+ ->to_string_fragment(last_key.value())
+ .to_string();
+ if (!this->pp_interval_state.back().is_name.empty()) {
+ this->pp_interval_state.back().is_start
+ = static_cast<ssize_t>(this->pp_stream.tellp());
+ }
+ last_key = nonstd::nullopt;
+ }
+ break;
+ default:
+ break;
+ }
+ if (start_on_depth
+ && (el.e_token == DT_LSQUARE || el.e_token == DT_LCURLY))
+ {
+ if (this->pp_line_length > 0) {
+ this->pp_stream << std::endl;
+ }
+ this->pp_line_length = 0;
+ }
+ }
+ this->pp_values.pop_front();
+ retval = true;
+ }
+ return retval;
+}
+
+void
+pretty_printer::start_new_line()
+{
+ bool has_output;
+
+ if (this->pp_line_length > 0) {
+ this->pp_stream << std::endl;
+ this->pp_line_length = 0;
+ }
+ has_output = this->flush_values();
+ if (has_output && this->pp_line_length > 0) {
+ this->pp_stream << std::endl;
+ }
+ this->pp_line_length = 0;
+ this->pp_body_lines.top() += 1;
+}
+
+void
+pretty_printer::ascend()
+{
+ if (this->pp_depth > 0) {
+ int lines = this->pp_body_lines.top();
+ this->pp_depth -= 1;
+ this->pp_body_lines.pop();
+ this->pp_body_lines.top() += lines;
+
+ if (!this->pp_is_xml) {
+ this->append_child_node();
+ }
+ this->pp_interval_state.pop_back();
+ this->pp_hier_stage = std::move(this->pp_hier_nodes.back());
+ this->pp_hier_nodes.pop_back();
+ } else {
+ this->pp_body_lines.top() = 0;
+ }
+}
+
+void
+pretty_printer::descend()
+{
+ this->pp_depth += 1;
+ this->pp_body_lines.push(0);
+ this->pp_interval_state.resize(this->pp_depth + 1);
+ this->pp_hier_nodes.push_back(
+ std::make_unique<lnav::document::hier_node>());
+}
+
+void
+pretty_printer::append_child_node()
+{
+ auto& ivstate = this->pp_interval_state.back();
+ if (!ivstate.is_start) {
+ return;
+ }
+
+ auto* top_node = this->pp_hier_nodes.back().get();
+ auto new_key = ivstate.is_name.empty()
+ ? lnav::document::section_key_t{top_node->hn_children.size()}
+ : lnav::document::section_key_t{ivstate.is_name};
+ this->pp_intervals.emplace_back(
+ ivstate.is_start.value(),
+ static_cast<ssize_t>(this->pp_stream.tellp()),
+ new_key);
+ auto new_node = this->pp_hier_stage != nullptr
+ ? std::move(this->pp_hier_stage)
+ : std::make_unique<lnav::document::hier_node>();
+ auto* retval = new_node.get();
+ new_node->hn_parent = top_node;
+ new_node->hn_start = this->pp_intervals.back().start;
+ if (!ivstate.is_name.empty()) {
+ top_node->hn_named_children.insert({
+ ivstate.is_name,
+ retval,
+ });
+ }
+ top_node->hn_children.emplace_back(std::move(new_node));
+ ivstate.is_start = nonstd::nullopt;
+ ivstate.is_name.clear();
+}
diff --git a/src/pretty_printer.hh b/src/pretty_printer.hh
new file mode 100644
index 0000000..f10b946
--- /dev/null
+++ b/src/pretty_printer.hh
@@ -0,0 +1,137 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef pretty_printer_hh
+#define pretty_printer_hh
+
+#include <deque>
+#include <map>
+#include <sstream>
+#include <stack>
+#include <utility>
+#include <vector>
+
+#include <sys/types.h>
+
+#include "base/attr_line.hh"
+#include "base/file_range.hh"
+#include "base/opt_util.hh"
+#include "data_scanner.hh"
+#include "document.sections.hh"
+
+class pretty_printer {
+public:
+ struct element {
+ element(data_token_t token, data_scanner::capture_t& cap)
+ : e_token(token), e_capture(cap)
+ {
+ }
+
+ data_token_t e_token;
+ data_scanner::capture_t e_capture;
+ };
+
+ pretty_printer(data_scanner* ds, string_attrs_t sa, int leading_indent = 0)
+ : pp_leading_indent(leading_indent), pp_scanner(ds),
+ pp_attrs(std::move(sa))
+ {
+ this->pp_body_lines.push(0);
+ this->pp_scanner->reset();
+ while (true) {
+ auto tok_res = this->pp_scanner->tokenize2();
+ if (!tok_res) {
+ break;
+ }
+ if (tok_res->tr_token == DT_XML_CLOSE_TAG
+ || tok_res->tr_token == DT_XML_DECL_TAG)
+ {
+ pp_is_xml = true;
+ break;
+ }
+ }
+
+ this->pp_interval_state.resize(1);
+ this->pp_hier_nodes.push_back(
+ std::make_unique<lnav::document::hier_node>());
+ }
+
+ void append_to(attr_line_t& al);
+
+ std::vector<lnav::document::section_interval_t> take_intervals()
+ {
+ return std::move(this->pp_intervals);
+ }
+
+ std::unique_ptr<lnav::document::hier_node> take_hier_root()
+ {
+ if (this->pp_hier_stage == nullptr && !this->pp_hier_nodes.empty()) {
+ this->pp_hier_stage = std::move(this->pp_hier_nodes.back());
+ this->pp_hier_nodes.pop_back();
+ }
+ return std::move(this->pp_hier_stage);
+ }
+
+private:
+ void descend();
+
+ void ascend();
+
+ void start_new_line();
+
+ bool flush_values(bool start_on_depth = false);
+
+ void append_indent();
+
+ void write_element(const element& el);
+
+ void append_child_node();
+
+ struct interval_state {
+ nonstd::optional<file_off_t> is_start;
+ std::string is_name;
+ };
+
+ int pp_leading_indent;
+ int pp_depth{0};
+ int pp_line_length{0};
+ int pp_soft_indent{0};
+ std::stack<int> pp_body_lines{};
+ data_scanner* pp_scanner;
+ string_attrs_t pp_attrs;
+ std::ostringstream pp_stream;
+ std::deque<element> pp_values{};
+ int pp_shift_accum{0};
+ bool pp_is_xml{false};
+ std::vector<interval_state> pp_interval_state;
+ std::vector<lnav::document::section_interval_t> pp_intervals;
+ std::vector<std::unique_ptr<lnav::document::hier_node>> pp_hier_nodes;
+ std::unique_ptr<lnav::document::hier_node> pp_hier_stage;
+};
+
+#endif
diff --git a/src/preview_status_source.hh b/src/preview_status_source.hh
new file mode 100644
index 0000000..25622b0
--- /dev/null
+++ b/src/preview_status_source.hh
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_preview_status_source_hh
+#define lnav_preview_status_source_hh
+
+#include <string>
+
+#include "statusview_curses.hh"
+
+class preview_status_source : public status_data_source {
+public:
+ typedef enum {
+ TSF_TITLE,
+ TSF_STITCH_TITLE,
+ TSF_DESCRIPTION,
+ TSF_TOGGLE,
+
+ TSF__MAX
+ } field_t;
+
+ preview_status_source()
+ {
+ static const char TOGGLE_MSG[] = "Press CTRL+P to show/hide";
+
+ this->tss_fields[TSF_TITLE].set_width(14);
+ this->tss_fields[TSF_TITLE].set_role(role_t::VCR_STATUS_TITLE);
+ this->tss_fields[TSF_TITLE].set_value(" Preview Data ");
+ this->tss_fields[TSF_STITCH_TITLE].set_width(2);
+ this->tss_fields[TSF_STITCH_TITLE].set_stitch_value(
+ role_t::VCR_STATUS_STITCH_TITLE_TO_NORMAL,
+ role_t::VCR_STATUS_STITCH_NORMAL_TO_TITLE);
+ this->tss_fields[TSF_DESCRIPTION].set_share(1);
+ this->tss_fields[TSF_TOGGLE].set_width(strlen(TOGGLE_MSG) + 1);
+ this->tss_fields[TSF_TOGGLE].set_value(TOGGLE_MSG);
+ this->tss_fields[TSF_TOGGLE].right_justify(true);
+ };
+
+ size_t statusview_fields() override
+ {
+ return TSF__MAX;
+ };
+
+ status_field& statusview_value_for_field(int field) override
+ {
+ return this->tss_fields[field];
+ };
+
+ status_field& get_description()
+ {
+ return this->tss_fields[TSF_DESCRIPTION];
+ };
+
+private:
+ status_field tss_fields[TSF__MAX];
+};
+
+#endif
diff --git a/src/ptimec.c b/src/ptimec.c
new file mode 100644
index 0000000..45059d6
--- /dev/null
+++ b/src/ptimec.c
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file ptimec.c
+ */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+const char *PRELUDE = "\
+#include <time.h>\n\
+#include <sys/types.h>\n\
+#include \"ptimec.hh\"\n\
+\n\
+";
+
+char *escape_char(char ch)
+{
+ static char charstr[4];
+
+ if (ch == '\'') {
+ strcpy(charstr, "\\'");
+ } else {
+ charstr[0] = ch;
+ charstr[1] = '\0';
+ }
+
+ return charstr;
+}
+
+int main(int argc, char *argv[])
+{
+ int retval = EXIT_SUCCESS;
+
+ fputs(PRELUDE, stdout);
+ for (int lpc = 1; lpc < argc; lpc++) {
+ const char* arg = argv[lpc];
+
+ printf(
+ "// %s\n"
+ "bool ptime_f%d(struct exttm *dst, const char *str, off_t &off, "
+ "ssize_t len) {\n"
+ " dst->et_flags = 0;\n"
+ " // log_debug(\"ptime_f%d\");\n",
+ arg,
+ lpc,
+ lpc);
+ for (int index = 0; arg[index]; arg++) {
+ if (arg[index] == '%') {
+ switch (arg[index + 1]) {
+ case 'a':
+ case 'Z':
+ if (arg[index + 2]) {
+ printf(
+ " if (!ptime_upto('%s', str, off, len)) "
+ "return false;\n",
+ escape_char(arg[index + 2]));
+ } else {
+ printf(
+ " if (!ptime_upto_end(str, off, len)) "
+ "return false;\n");
+ }
+ arg += 1;
+ break;
+ case '@':
+ printf(
+ " if (!ptime_at(dst, str, off, len)) return "
+ "false;\n");
+ arg += 1;
+ break;
+ default:
+ printf(
+ " if (!ptime_%c(dst, str, off, len)) return "
+ "false;\n",
+ arg[index + 1]);
+ arg += 1;
+ break;
+ }
+ } else {
+ printf(" if (!ptime_char('%s', str, off, len)) return false;\n",
+ escape_char(arg[index]));
+ }
+ }
+ printf(" return true;\n");
+ printf("}\n\n");
+ }
+ for (int lpc = 1; lpc < argc; lpc++) {
+ const char *arg = argv[lpc];
+
+ printf("void ftime_f%d(char *dst, off_t &off_inout, size_t len, const struct exttm &tm) {\n",
+ lpc);
+ for (int index = 0; arg[index]; arg++) {
+ if (arg[index] == '%') {
+ switch (arg[index + 1]) {
+ case '@':
+ printf(" ftime_at(dst, off_inout, len, tm);\n");
+ arg += 1;
+ break;
+ default:
+ printf(" ftime_%c(dst, off_inout, len, tm);\n",
+ arg[index + 1]);
+ arg += 1;
+ break;
+ }
+ } else {
+ printf(" ftime_char(dst, off_inout, len, '%s');\n",
+ escape_char(arg[index]));
+ }
+ }
+ printf(" dst[off_inout] = '\\0';\n");
+ printf("}\n\n");
+ }
+
+ printf("struct ptime_fmt PTIMEC_FORMATS[] = {\n");
+ for (int lpc = 1; lpc < argc; lpc++) {
+ printf(" { \"%s\", ptime_f%d, ftime_f%d },\n", argv[lpc], lpc, lpc);
+ }
+ printf("\n");
+ printf(" { nullptr, nullptr, nullptr }\n");
+ printf("};\n");
+
+ printf("const char *PTIMEC_FORMAT_STR[] = {\n");
+ for (int lpc = 1; lpc < argc; lpc++) {
+ printf(" \"%s\",\n", argv[lpc]);
+ }
+ printf("\n");
+ printf(" nullptr\n");
+ printf("};\n");
+
+ return retval;
+}
diff --git a/src/ptimec.hh b/src/ptimec.hh
new file mode 100644
index 0000000..9db6b0c
--- /dev/null
+++ b/src/ptimec.hh
@@ -0,0 +1,1119 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file ptimec.hh
+ */
+
+#ifndef pctimec_hh
+#define pctimec_hh
+
+#include "config.h"
+
+// XXX
+#define __STDC_FORMAT_MACROS
+#include <cstdlib>
+
+#include <ctype.h>
+#include <inttypes.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <time.h>
+
+#include "base/lnav_log.hh"
+#include "base/time_util.hh"
+
+#define PTIME_CONSUME(amount, block) \
+ if ((off_inout + (amount)) > len) { \
+ return false; \
+ } \
+\
+ block \
+\
+ off_inout \
+ += (amount);
+
+#define PTIME_APPEND(ch) \
+ if ((off_inout + 2) >= len) { \
+ return; \
+ } \
+ dst[off_inout] = ch; \
+ off_inout += 1;
+
+#define ABR_TO_INT(a, b, c) (((a) << 24) | ((b) << 16) | ((c) << 8))
+
+inline bool
+ptime_upto(char ch, const char* str, off_t& off_inout, ssize_t len)
+{
+ for (; off_inout < len; off_inout++) {
+ if (str[off_inout] == ch) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+inline bool
+ptime_upto_end(const char* str, off_t& off_inout, ssize_t len)
+{
+ off_inout = len;
+
+ return true;
+}
+
+bool ptime_b_slow(struct exttm* dst,
+ const char* str,
+ off_t& off_inout,
+ ssize_t len);
+
+inline bool
+ptime_b(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ if (off_inout + 3 < len) {
+ auto month_start = (unsigned char*) &str[off_inout];
+ uint32_t month_int = ABR_TO_INT(month_start[0] & ~0x20UL,
+ month_start[1] & ~0x20UL,
+ month_start[2] & ~0x20UL);
+ int val;
+
+ switch (month_int) {
+ case ABR_TO_INT('J', 'A', 'N'):
+ val = 0;
+ break;
+ case ABR_TO_INT('F', 'E', 'B'):
+ val = 1;
+ break;
+ case ABR_TO_INT('M', 'A', 'R'):
+ val = 2;
+ break;
+ case ABR_TO_INT('A', 'P', 'R'):
+ val = 3;
+ break;
+ case ABR_TO_INT('M', 'A', 'Y'):
+ val = 4;
+ break;
+ case ABR_TO_INT('J', 'U', 'N'):
+ val = 5;
+ break;
+ case ABR_TO_INT('J', 'U', 'L'):
+ val = 6;
+ break;
+ case ABR_TO_INT('A', 'U', 'G'):
+ val = 7;
+ break;
+ case ABR_TO_INT('S', 'E', 'P'):
+ val = 8;
+ break;
+ case ABR_TO_INT('O', 'C', 'T'):
+ val = 9;
+ break;
+ case ABR_TO_INT('N', 'O', 'V'):
+ val = 10;
+ break;
+ case ABR_TO_INT('D', 'E', 'C'):
+ val = 11;
+ break;
+ default:
+ val = -1;
+ break;
+ }
+ if (val >= 0) {
+ off_inout += 3;
+ dst->et_tm.tm_mon = val;
+ dst->et_flags |= ETF_MONTH_SET;
+ return true;
+ }
+ }
+
+ return ptime_b_slow(dst, str, off_inout, len);
+}
+
+inline void
+ftime_a(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ switch (tm.et_tm.tm_wday) {
+ case 0:
+ PTIME_APPEND('S');
+ PTIME_APPEND('u');
+ PTIME_APPEND('n');
+ break;
+ case 1:
+ PTIME_APPEND('M');
+ PTIME_APPEND('o');
+ PTIME_APPEND('n');
+ break;
+ case 2:
+ PTIME_APPEND('T');
+ PTIME_APPEND('u');
+ PTIME_APPEND('e');
+ break;
+ case 3:
+ PTIME_APPEND('W');
+ PTIME_APPEND('e');
+ PTIME_APPEND('d');
+ break;
+ case 4:
+ PTIME_APPEND('T');
+ PTIME_APPEND('h');
+ PTIME_APPEND('u');
+ break;
+ case 5:
+ PTIME_APPEND('F');
+ PTIME_APPEND('r');
+ PTIME_APPEND('i');
+ break;
+ case 6:
+ PTIME_APPEND('S');
+ PTIME_APPEND('a');
+ PTIME_APPEND('t');
+ break;
+ default:
+ PTIME_APPEND('X');
+ PTIME_APPEND('X');
+ PTIME_APPEND('X');
+ break;
+ }
+}
+
+inline void
+ftime_Z(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+}
+
+inline void
+ftime_b(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ switch (tm.et_tm.tm_mon) {
+ case 0:
+ PTIME_APPEND('J');
+ PTIME_APPEND('a');
+ PTIME_APPEND('n');
+ break;
+ case 1:
+ PTIME_APPEND('F');
+ PTIME_APPEND('e');
+ PTIME_APPEND('b');
+ break;
+ case 2:
+ PTIME_APPEND('M');
+ PTIME_APPEND('a');
+ PTIME_APPEND('r');
+ break;
+ case 3:
+ PTIME_APPEND('A');
+ PTIME_APPEND('p');
+ PTIME_APPEND('r');
+ break;
+ case 4:
+ PTIME_APPEND('M');
+ PTIME_APPEND('a');
+ PTIME_APPEND('y');
+ break;
+ case 5:
+ PTIME_APPEND('J');
+ PTIME_APPEND('u');
+ PTIME_APPEND('n');
+ break;
+ case 6:
+ PTIME_APPEND('J');
+ PTIME_APPEND('u');
+ PTIME_APPEND('l');
+ break;
+ case 7:
+ PTIME_APPEND('A');
+ PTIME_APPEND('u');
+ PTIME_APPEND('g');
+ break;
+ case 8:
+ PTIME_APPEND('S');
+ PTIME_APPEND('e');
+ PTIME_APPEND('p');
+ break;
+ case 9:
+ PTIME_APPEND('O');
+ PTIME_APPEND('c');
+ PTIME_APPEND('t');
+ break;
+ case 10:
+ PTIME_APPEND('N');
+ PTIME_APPEND('o');
+ PTIME_APPEND('v');
+ break;
+ case 11:
+ PTIME_APPEND('D');
+ PTIME_APPEND('e');
+ PTIME_APPEND('c');
+ break;
+ default:
+ PTIME_APPEND('X');
+ PTIME_APPEND('X');
+ PTIME_APPEND('X');
+ break;
+ }
+}
+
+inline bool
+ptime_S(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(2, {
+ if (str[off_inout + 1] > '9') {
+ return false;
+ }
+ dst->et_tm.tm_sec
+ = (str[off_inout] - '0') * 10 + (str[off_inout + 1] - '0');
+ dst->et_flags |= ETF_SECOND_SET;
+ });
+
+ return (dst->et_tm.tm_sec >= 0 && dst->et_tm.tm_sec <= 59);
+}
+
+inline void
+ftime_S(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ PTIME_APPEND('0' + ((tm.et_tm.tm_sec / 10) % 10));
+ PTIME_APPEND('0' + ((tm.et_tm.tm_sec / 1) % 10));
+}
+
+inline bool
+ptime_s(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ off_t off_start = off_inout;
+ lnav::time64_t epoch = 0;
+
+ while (off_inout < len && isdigit(str[off_inout])) {
+ if ((off_inout - off_start) > 11) {
+ return false;
+ }
+
+ epoch *= 10;
+ epoch += str[off_inout] - '0';
+ off_inout += 1;
+ }
+
+ if (epoch >= MAX_TIME_T) {
+ return false;
+ }
+
+ secs2tm(epoch, &dst->et_tm);
+ dst->et_flags = ETF_DAY_SET | ETF_MONTH_SET | ETF_YEAR_SET | ETF_HOUR_SET
+ | ETF_MINUTE_SET | ETF_SECOND_SET | ETF_MACHINE_ORIENTED
+ | ETF_EPOCH_TIME;
+
+ return (epoch > 0);
+}
+
+inline void
+ftime_s(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ time_t t = tm2sec(&tm.et_tm);
+
+ snprintf(&dst[off_inout], len - off_inout, "%ld", t);
+ off_inout = strlen(dst);
+}
+
+inline bool
+ptime_q(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ off_t off_start = off_inout;
+ lnav::time64_t epoch = 0;
+
+ while (off_inout < len && isxdigit(str[off_inout])) {
+ if ((off_inout - off_start) > 11) {
+ return false;
+ }
+
+ epoch *= 16;
+ switch (tolower(str[off_inout])) {
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ epoch += str[off_inout] - '0';
+ break;
+ case 'a':
+ case 'b':
+ case 'c':
+ case 'd':
+ case 'e':
+ case 'f':
+ epoch += str[off_inout] - 'a' + 10;
+ break;
+ }
+ off_inout += 1;
+ }
+
+ if (epoch >= MAX_TIME_T) {
+ return false;
+ }
+
+ secs2tm(epoch, &dst->et_tm);
+ dst->et_flags = ETF_DAY_SET | ETF_MONTH_SET | ETF_YEAR_SET | ETF_HOUR_SET
+ | ETF_MINUTE_SET | ETF_SECOND_SET | ETF_MACHINE_ORIENTED
+ | ETF_EPOCH_TIME;
+
+ return (epoch > 0);
+}
+
+inline void
+ftime_q(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ time_t t = tm2sec(&tm.et_tm);
+
+ snprintf(&dst[off_inout], len - off_inout, "%lx", t);
+ off_inout = strlen(dst);
+}
+
+inline bool
+ptime_L(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ int ms = 0;
+
+ PTIME_CONSUME(3, {
+ char c0 = str[off_inout];
+ char c1 = str[off_inout + 1];
+ char c2 = str[off_inout + 2];
+ if (!isdigit(c0) || !isdigit(c1) || !isdigit(c2)) {
+ return false;
+ }
+ ms = ((str[off_inout] - '0') * 100 + (str[off_inout + 1] - '0') * 10
+ + (str[off_inout + 2] - '0'));
+ });
+
+ if ((ms >= 0 && ms <= 999)) {
+ dst->et_nsec = ms * 1000000;
+ return true;
+ }
+ return false;
+}
+
+inline void
+ftime_L(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ int millis = tm.et_nsec / 1000000;
+
+ PTIME_APPEND('0' + ((millis / 100) % 10));
+ PTIME_APPEND('0' + ((millis / 10) % 10));
+ PTIME_APPEND('0' + ((millis / 1) % 10));
+}
+
+inline bool
+ptime_M(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(2, {
+ if (str[off_inout + 1] > '9') {
+ return false;
+ }
+ dst->et_tm.tm_min
+ = (str[off_inout] - '0') * 10 + (str[off_inout + 1] - '0');
+ dst->et_flags |= ETF_MINUTE_SET;
+ });
+
+ return (dst->et_tm.tm_min >= 0 && dst->et_tm.tm_min <= 59);
+}
+
+inline void
+ftime_M(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ PTIME_APPEND('0' + ((tm.et_tm.tm_min / 10) % 10));
+ PTIME_APPEND('0' + ((tm.et_tm.tm_min / 1) % 10));
+}
+
+inline bool
+ptime_H(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(2, {
+ if (str[off_inout + 1] > '9') {
+ return false;
+ }
+ dst->et_tm.tm_hour
+ = (str[off_inout] - '0') * 10 + (str[off_inout + 1] - '0');
+ dst->et_flags |= ETF_HOUR_SET;
+ });
+
+ return (dst->et_tm.tm_hour >= 0 && dst->et_tm.tm_hour <= 23);
+}
+
+inline void
+ftime_H(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ PTIME_APPEND('0' + ((tm.et_tm.tm_hour / 10) % 10));
+ PTIME_APPEND('0' + ((tm.et_tm.tm_hour / 1) % 10));
+}
+
+inline bool
+ptime_i(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ uint64_t epoch_ms = 0;
+ lnav::time64_t epoch;
+
+ while (off_inout < len && isdigit(str[off_inout])) {
+ epoch_ms *= 10;
+ epoch_ms += str[off_inout] - '0';
+ off_inout += 1;
+ }
+
+ dst->et_nsec = (epoch_ms % 1000ULL) * 1000000;
+ epoch = (epoch_ms / 1000ULL);
+
+ if (epoch >= MAX_TIME_T) {
+ return false;
+ }
+
+ secs2tm(epoch, &dst->et_tm);
+ dst->et_flags = ETF_DAY_SET | ETF_MONTH_SET | ETF_YEAR_SET | ETF_HOUR_SET
+ | ETF_MINUTE_SET | ETF_SECOND_SET | ETF_MACHINE_ORIENTED
+ | ETF_EPOCH_TIME;
+
+ return (epoch_ms > 0);
+}
+
+inline void
+ftime_i(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ int64_t t = tm2sec(&tm.et_tm);
+
+ t += tm.et_nsec / 1000000;
+ snprintf(&dst[off_inout], len - off_inout, "%" PRId64, t);
+ off_inout = strlen(dst);
+}
+
+inline bool
+ptime_6(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ uint64_t epoch_us = 0;
+ lnav::time64_t epoch;
+
+ while (off_inout < len && isdigit(str[off_inout])) {
+ epoch_us *= 10;
+ epoch_us += str[off_inout] - '0';
+ off_inout += 1;
+ }
+
+ dst->et_nsec = (epoch_us % 1000000ULL) * 1000ULL;
+ epoch = (epoch_us / 1000000ULL);
+
+ if (epoch >= MAX_TIME_T) {
+ return false;
+ }
+
+ secs2tm(epoch, &dst->et_tm);
+ dst->et_flags = ETF_DAY_SET | ETF_MONTH_SET | ETF_YEAR_SET | ETF_HOUR_SET
+ | ETF_MINUTE_SET | ETF_SECOND_SET | ETF_MACHINE_ORIENTED
+ | ETF_EPOCH_TIME;
+
+ return (epoch_us > 0);
+}
+
+inline void
+ftime_6(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ int64_t t = tm2sec(&tm.et_tm);
+
+ t += tm.et_nsec / 1000;
+ snprintf(&dst[off_inout], len - off_inout, "%" PRId64, t);
+ off_inout = strlen(dst);
+}
+
+inline bool
+ptime_I(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(2, {
+ if (str[off_inout + 1] > '9') {
+ return false;
+ }
+ dst->et_tm.tm_hour
+ = (str[off_inout] - '0') * 10 + (str[off_inout + 1] - '0');
+
+ if (dst->et_tm.tm_hour < 1 || dst->et_tm.tm_hour > 12) {
+ return false;
+ }
+ dst->et_flags |= ETF_HOUR_SET;
+ });
+
+ return true;
+}
+
+inline void
+ftime_I(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ int hour = tm.et_tm.tm_hour;
+
+ if (hour >= 12) {
+ hour -= 12;
+ }
+ if (hour == 0) {
+ hour = 12;
+ }
+
+ PTIME_APPEND('0' + ((hour / 10) % 10));
+ PTIME_APPEND('0' + ((hour / 1) % 10));
+}
+
+inline bool
+ptime_d(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(2, {
+ if (str[off_inout] == ' ') {
+ dst->et_tm.tm_mday = 0;
+ } else {
+ dst->et_tm.tm_mday = (str[off_inout] - '0') * 10;
+ }
+ if (str[off_inout + 1] > '9') {
+ return false;
+ }
+ dst->et_tm.tm_mday += (str[off_inout + 1] - '0');
+ });
+
+ if (dst->et_tm.tm_mday >= 1 && dst->et_tm.tm_mday <= 31) {
+ dst->et_flags |= ETF_DAY_SET;
+ return true;
+ }
+ return false;
+}
+
+inline void
+ftime_d(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ PTIME_APPEND('0' + ((tm.et_tm.tm_mday / 10) % 10));
+ PTIME_APPEND('0' + ((tm.et_tm.tm_mday / 1) % 10));
+}
+
+inline bool
+ptime_e(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ dst->et_tm.tm_mday = 0;
+ PTIME_CONSUME(1, {
+ if (str[off_inout] < '0' || str[off_inout] > '9') {
+ return false;
+ }
+ dst->et_tm.tm_mday = str[off_inout] - '0';
+ });
+ if (off_inout < len) {
+ if (str[off_inout] >= '0' && str[off_inout] <= '9') {
+ dst->et_tm.tm_mday *= 10;
+ dst->et_tm.tm_mday += str[off_inout] - '0';
+ off_inout += 1;
+ }
+ }
+
+ if (dst->et_tm.tm_mday >= 1 && dst->et_tm.tm_mday <= 31) {
+ dst->et_flags |= ETF_DAY_SET;
+ return true;
+ }
+ return false;
+}
+
+inline void
+ftime_e(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ if (tm.et_tm.tm_mday < 10) {
+ PTIME_APPEND(' ');
+ } else {
+ PTIME_APPEND('0' + ((tm.et_tm.tm_mday / 10) % 10));
+ }
+ PTIME_APPEND('0' + ((tm.et_tm.tm_mday / 1) % 10));
+}
+
+inline bool
+ptime_m(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ off_t orig_off = off_inout;
+
+ dst->et_tm.tm_mon = 0;
+ PTIME_CONSUME(1, {
+ if (str[off_inout] < '0' || str[off_inout] > '9') {
+ return false;
+ }
+ dst->et_tm.tm_mon = str[off_inout] - '0';
+ });
+ if (off_inout < len) {
+ if (str[off_inout] >= '0' && str[off_inout] <= '9') {
+ dst->et_tm.tm_mon *= 10;
+ dst->et_tm.tm_mon += str[off_inout] - '0';
+ off_inout += 1;
+ }
+ }
+
+ dst->et_tm.tm_mon -= 1;
+
+ if (dst->et_tm.tm_mon >= 0 && dst->et_tm.tm_mon <= 11) {
+ dst->et_flags |= ETF_MONTH_SET;
+ return true;
+ }
+
+ off_inout = orig_off;
+ return false;
+}
+
+inline void
+ftime_m(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ PTIME_APPEND('0' + (((tm.et_tm.tm_mon + 1) / 10) % 10));
+ PTIME_APPEND('0' + (((tm.et_tm.tm_mon + 1) / 1) % 10));
+}
+
+inline bool
+ptime_k(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ dst->et_tm.tm_hour = 0;
+ PTIME_CONSUME(1, {
+ if (str[off_inout] < '0' || str[off_inout] > '9') {
+ return false;
+ }
+ dst->et_tm.tm_hour = str[off_inout] - '0';
+ });
+ if (off_inout < len) {
+ if (str[off_inout] >= '0' && str[off_inout] <= '9') {
+ dst->et_tm.tm_hour *= 10;
+ dst->et_tm.tm_hour += str[off_inout] - '0';
+ dst->et_flags |= ETF_HOUR_SET;
+ off_inout += 1;
+ }
+ }
+
+ return (dst->et_tm.tm_hour >= 0 && dst->et_tm.tm_hour <= 23);
+}
+
+inline void
+ftime_k(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ if (tm.et_tm.tm_hour < 10) {
+ PTIME_APPEND(' ');
+ } else {
+ PTIME_APPEND('0' + ((tm.et_tm.tm_hour / 10) % 10));
+ }
+ PTIME_APPEND('0' + ((tm.et_tm.tm_hour / 1) % 10));
+}
+
+inline bool
+ptime_l(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ off_t orig_off = off_inout;
+ bool consumed_space = false;
+
+ dst->et_tm.tm_hour = 0;
+
+ if ((off_inout + 1) > len) {
+ return false;
+ }
+
+ if (str[off_inout] == ' ') {
+ consumed_space = true;
+ off_inout += 1;
+ }
+
+ if ((off_inout + 1) > len) {
+ off_inout = orig_off;
+ return false;
+ }
+
+ if (str[off_inout] < '1' || str[off_inout] > '9') {
+ off_inout = orig_off;
+ return false;
+ }
+
+ dst->et_tm.tm_hour = str[off_inout] - '0';
+ off_inout += 1;
+
+ if (consumed_space || str[off_inout] < '0' || str[off_inout] > '9') {
+ return true;
+ }
+
+ dst->et_tm.tm_hour *= 10;
+ dst->et_tm.tm_hour += str[off_inout] - '0';
+ off_inout += 1;
+
+ if (dst->et_tm.tm_hour >= 0 && dst->et_tm.tm_hour <= 23) {
+ return true;
+ }
+
+ dst->et_flags |= ETF_HOUR_SET;
+
+ off_inout = orig_off;
+ return false;
+}
+
+inline void
+ftime_l(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ int hour = tm.et_tm.tm_hour;
+
+ if (hour >= 12) {
+ hour -= 12;
+ }
+ if (hour == 0) {
+ hour = 12;
+ }
+
+ if (hour < 10) {
+ PTIME_APPEND(' ');
+ } else {
+ PTIME_APPEND('0' + ((hour / 10) % 10));
+ }
+ PTIME_APPEND('0' + ((hour / 1) % 10));
+}
+
+inline bool
+ptime_p(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(2, {
+ char lead = str[off_inout];
+
+ if ((str[off_inout + 1] & 0xdf) != 'M') {
+ return false;
+ } else if ((lead & 0xdf) == 'A') {
+ if (dst->et_tm.tm_hour == 12) {
+ dst->et_tm.tm_hour = 0;
+ }
+ } else if ((lead & 0xdf) == 'P') {
+ if (dst->et_tm.tm_hour < 12) {
+ dst->et_tm.tm_hour += 12;
+ }
+ } else {
+ return false;
+ }
+ });
+
+ return true;
+}
+
+inline void
+ftime_p(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ if (tm.et_tm.tm_hour < 12) {
+ PTIME_APPEND('A');
+ } else {
+ PTIME_APPEND('P');
+ }
+ PTIME_APPEND('M');
+}
+
+inline bool
+ptime_Y(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(4, {
+ dst->et_tm.tm_year = ((str[off_inout + 0] - '0') * 1000
+ + (str[off_inout + 1] - '0') * 100
+ + (str[off_inout + 2] - '0') * 10
+ + (str[off_inout + 3] - '0') * 1)
+ - 1900;
+
+ if (dst->et_tm.tm_year < 0 || dst->et_tm.tm_year > 1100) {
+ return false;
+ }
+
+ dst->et_flags |= ETF_YEAR_SET;
+ });
+
+ return true;
+}
+
+inline void
+ftime_Y(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ int year = tm.et_tm.tm_year + 1900;
+
+ PTIME_APPEND('0' + ((year / 1000) % 10));
+ PTIME_APPEND('0' + ((year / 100) % 10));
+ PTIME_APPEND('0' + ((year / 10) % 10));
+ PTIME_APPEND('0' + ((year / 1) % 10));
+}
+
+inline bool
+ptime_y(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(2, {
+ dst->et_tm.tm_year = ((str[off_inout + 0] - '0') * 10
+ + (str[off_inout + 1] - '0') * 1);
+ });
+
+ if (dst->et_tm.tm_year >= 0 && dst->et_tm.tm_year < 100) {
+ if (dst->et_tm.tm_year < 69) {
+ dst->et_tm.tm_year += 100;
+ }
+
+ dst->et_flags |= ETF_YEAR_SET;
+ return true;
+ }
+ return false;
+}
+
+inline void
+ftime_y(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ int year = tm.et_tm.tm_year + 1900;
+
+ PTIME_APPEND('0' + ((year / 10) % 10));
+ PTIME_APPEND('0' + ((year / 1) % 10));
+}
+
+inline bool
+ptime_z(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ int consume_amount = 5;
+
+ if ((off_inout + 6) <= len && str[off_inout + 3] == ':') {
+ consume_amount = 6;
+ }
+ PTIME_CONSUME(consume_amount, {
+ long sign;
+ long hours;
+ long mins;
+ int skip_colon = (consume_amount == 6) ? 1 : 0;
+
+ if (str[off_inout] == '+') {
+ sign = 1;
+ } else if (str[off_inout] == '-') {
+ sign = -1;
+ } else {
+ return false;
+ }
+
+ hours
+ = ((str[off_inout + 1] - '0') * 10 + (str[off_inout + 2] - '0') * 1)
+ * 60 * 60;
+ mins = ((str[off_inout + skip_colon + 3] - '0') * 10
+ + (str[off_inout + skip_colon + 4] - '0') * 1)
+ * 60;
+ dst->et_gmtoff = sign * (hours + mins);
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ dst->et_tm.tm_gmtoff = sign * (hours + mins);
+#endif
+ });
+
+ return true;
+}
+
+inline void
+ftime_z(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ long gmtoff = std::abs(tm.et_gmtoff) / 60;
+
+ if (tm.et_gmtoff < 0) {
+ PTIME_APPEND('-');
+ } else {
+ PTIME_APPEND('+');
+ }
+
+ long mins = gmtoff % 60;
+ long hours = gmtoff / 60;
+
+ PTIME_APPEND('0' + ((hours / 10) % 10));
+ PTIME_APPEND('0' + ((hours / 1) % 10));
+ PTIME_APPEND('0' + ((mins / 10) % 10));
+ PTIME_APPEND('0' + ((mins / 1) % 10));
+}
+
+inline bool
+ptime_f(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(6, {
+ for (int lpc = 0; lpc < 6; lpc++) {
+ if (str[off_inout + lpc] < '0' || str[off_inout + lpc] > '9') {
+ return false;
+ }
+ }
+ dst->et_nsec = ((str[off_inout + 0] - '0') * 100000
+ + (str[off_inout + 1] - '0') * 10000
+ + (str[off_inout + 2] - '0') * 1000
+ + (str[off_inout + 3] - '0') * 100
+ + (str[off_inout + 4] - '0') * 10
+ + (str[off_inout + 5] - '0') * 1)
+ * 1000;
+ });
+
+ return true;
+}
+
+inline void
+ftime_f(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ uint32_t micros = tm.et_nsec / 1000;
+
+ PTIME_APPEND('0' + ((micros / 100000) % 10));
+ PTIME_APPEND('0' + ((micros / 10000) % 10));
+ PTIME_APPEND('0' + ((micros / 1000) % 10));
+ PTIME_APPEND('0' + ((micros / 100) % 10));
+ PTIME_APPEND('0' + ((micros / 10) % 10));
+ PTIME_APPEND('0' + ((micros / 1) % 10));
+}
+
+inline bool
+ptime_N(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(9, {
+ for (int lpc = 0; lpc < 9; lpc++) {
+ if (str[off_inout + lpc] < '0' || str[off_inout + lpc] > '9') {
+ return false;
+ }
+ }
+ dst->et_nsec = ((str[off_inout + 0] - '0') * 100000000
+ + (str[off_inout + 1] - '0') * 10000000
+ + (str[off_inout + 2] - '0') * 1000000
+ + (str[off_inout + 3] - '0') * 100000
+ + (str[off_inout + 4] - '0') * 10000
+ + (str[off_inout + 5] - '0') * 1000
+ + (str[off_inout + 6] - '0') * 100
+ + (str[off_inout + 7] - '0') * 10
+ + (str[off_inout + 8] - '0') * 1);
+ });
+
+ return true;
+}
+
+inline void
+ftime_N(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+ uint32_t nano = tm.et_nsec;
+
+ PTIME_APPEND('0' + ((nano / 100000000) % 10));
+ PTIME_APPEND('0' + ((nano / 10000000) % 10));
+ PTIME_APPEND('0' + ((nano / 1000000) % 10));
+ PTIME_APPEND('0' + ((nano / 100000) % 10));
+ PTIME_APPEND('0' + ((nano / 10000) % 10));
+ PTIME_APPEND('0' + ((nano / 1000) % 10));
+ PTIME_APPEND('0' + ((nano / 100) % 10));
+ PTIME_APPEND('0' + ((nano / 10) % 10));
+ PTIME_APPEND('0' + ((nano / 1) % 10));
+}
+
+inline bool
+ptime_char(char val, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(1, {
+ if (str[off_inout] != val) {
+ return false;
+ }
+ });
+
+ return true;
+}
+
+inline void
+ftime_char(char* dst, off_t& off_inout, ssize_t len, char ch)
+{
+ PTIME_APPEND(ch);
+}
+
+template<typename T>
+inline bool
+ptime_hex_to_quad(T& value_inout, const char quad)
+{
+ value_inout <<= 4;
+ if ('0' <= quad && quad <= '9') {
+ value_inout |= ((quad - '0') & 0x0f);
+ } else if ('a' <= quad && quad <= 'f') {
+ value_inout |= 10 + ((quad - 'a') & 0x0f);
+ } else if ('A' <= quad && quad <= 'F') {
+ value_inout |= 10 + ((quad - 'A') & 0x0f);
+ } else {
+ return false;
+ }
+
+ return true;
+}
+
+inline bool
+ptime_at(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ PTIME_CONSUME(16, {
+ int64_t secs = 0;
+
+ for (int lpc = 0; lpc < 16; lpc++) {
+ char quad = str[off_inout + lpc];
+
+ if (!ptime_hex_to_quad(secs, quad)) {
+ return false;
+ }
+ }
+ dst->et_nsec = 0;
+
+ lnav::time64_t small_secs = secs - 4611686018427387914ULL;
+
+ if (small_secs >= MAX_TIME_T) {
+ return false;
+ }
+
+ secs2tm(small_secs, &dst->et_tm);
+ });
+
+ if ((len - off_inout) == 8) {
+ PTIME_CONSUME(8, {
+ for (int lpc = 0; lpc < 8; lpc++) {
+ char quad = str[off_inout + lpc];
+
+ if (!ptime_hex_to_quad(dst->et_nsec, quad)) {
+ return false;
+ }
+ }
+ });
+ }
+
+ dst->et_flags |= ETF_DAY_SET | ETF_MONTH_SET | ETF_YEAR_SET | ETF_HOUR_SET
+ | ETF_MINUTE_SET | ETF_SECOND_SET | ETF_MACHINE_ORIENTED
+ | ETF_EPOCH_TIME;
+
+ return true;
+}
+
+inline void
+ftime_at(char* dst, off_t& off_inout, ssize_t len, const struct exttm& tm)
+{
+}
+
+using ptime_func = bool (*)(struct exttm*, const char*, off_t&, ssize_t);
+using ftime_func = void (*)(char*, off_t&, size_t, const struct exttm&);
+
+bool ptime_fmt(const char* fmt,
+ struct exttm* dst,
+ const char* str,
+ off_t& off,
+ ssize_t len);
+size_t ftime_fmt(char* dst,
+ size_t len,
+ const char* fmt,
+ const struct exttm& tm);
+
+struct ptime_fmt {
+ const char* pf_fmt;
+ ptime_func pf_func;
+ ftime_func pf_ffunc;
+};
+
+extern struct ptime_fmt PTIMEC_FORMATS[];
+
+extern const char* PTIMEC_FORMAT_STR[];
+
+#endif
diff --git a/src/ptimec_rt.cc b/src/ptimec_rt.cc
new file mode 100644
index 0000000..adceaa8
--- /dev/null
+++ b/src/ptimec_rt.cc
@@ -0,0 +1,186 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file ptimec_rt.cc
+ */
+
+#include <algorithm>
+
+#include "ptimec.hh"
+
+#include <string.h>
+
+#include "config.h"
+
+bool
+ptime_b_slow(struct exttm* dst, const char* str, off_t& off_inout, ssize_t len)
+{
+ size_t zone_len = len - off_inout;
+ char zone[zone_len + 1];
+ const char* end_of_date;
+
+ memcpy(zone, &str[off_inout], zone_len);
+ zone[zone_len] = '\0';
+ if ((end_of_date = strptime(zone, "%b", &dst->et_tm)) != NULL) {
+ off_inout += end_of_date - zone;
+ return true;
+ }
+
+ return false;
+}
+
+#define FMT_CASE(ch, c) \
+ case ch: \
+ if (!ptime_##c(dst, str, off, len)) \
+ return false; \
+ lpc += 1; \
+ break
+
+bool
+ptime_fmt(const char* fmt,
+ struct exttm* dst,
+ const char* str,
+ off_t& off,
+ ssize_t len)
+{
+ for (ssize_t lpc = 0; fmt[lpc]; lpc++) {
+ if (fmt[lpc] == '%') {
+ switch (fmt[lpc + 1]) {
+ case 'B': {
+ size_t b_len = len - off;
+ char full_month[b_len + 1];
+ const char* end_of_date;
+
+ memcpy(full_month, &str[off], b_len);
+ full_month[b_len] = '\0';
+ if ((end_of_date = strptime(full_month, "%B", &dst->et_tm))
+ != nullptr)
+ {
+ off += end_of_date - full_month;
+ lpc += 1;
+ } else {
+ return false;
+ }
+ break;
+ }
+ case 'a':
+ case 'Z':
+ if (fmt[lpc + 2]) {
+ if (!ptime_upto(fmt[lpc + 2], str, off, len)) {
+ return false;
+ }
+ lpc += 1;
+ } else {
+ if (!ptime_upto_end(str, off, len)) {
+ return false;
+ }
+ lpc += 1;
+ }
+ break;
+ FMT_CASE('b', b);
+ FMT_CASE('S', S);
+ FMT_CASE('s', s);
+ FMT_CASE('L', L);
+ FMT_CASE('M', M);
+ FMT_CASE('H', H);
+ FMT_CASE('i', i);
+ FMT_CASE('6', 6);
+ FMT_CASE('I', I);
+ FMT_CASE('d', d);
+ FMT_CASE('e', e);
+ FMT_CASE('f', f);
+ FMT_CASE('k', k);
+ FMT_CASE('l', l);
+ FMT_CASE('m', m);
+ FMT_CASE('N', N);
+ FMT_CASE('p', p);
+ FMT_CASE('q', q);
+ FMT_CASE('Y', Y);
+ FMT_CASE('y', y);
+ FMT_CASE('z', z);
+ FMT_CASE('@', at);
+ }
+ } else {
+ if (!ptime_char(fmt[lpc], str, off, len)) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+#define FTIME_FMT_CASE(ch, c) \
+ case ch: \
+ ftime_##c(dst, off_inout, len, tm); \
+ lpc += 1; \
+ break
+
+size_t
+ftime_fmt(char* dst, size_t len, const char* fmt, const struct exttm& tm)
+{
+ off_t off_inout = 0;
+
+ for (ssize_t lpc = 0; fmt[lpc]; lpc++) {
+ if (fmt[lpc] == '%') {
+ switch (fmt[lpc + 1]) {
+ case '%':
+ ftime_char(dst, off_inout, len, '%');
+ break;
+ FTIME_FMT_CASE('a', a);
+ FTIME_FMT_CASE('b', b);
+ FTIME_FMT_CASE('S', S);
+ FTIME_FMT_CASE('s', s);
+ FTIME_FMT_CASE('L', L);
+ FTIME_FMT_CASE('M', M);
+ FTIME_FMT_CASE('H', H);
+ FTIME_FMT_CASE('i', i);
+ FTIME_FMT_CASE('6', 6);
+ FTIME_FMT_CASE('I', I);
+ FTIME_FMT_CASE('d', d);
+ FTIME_FMT_CASE('e', e);
+ FTIME_FMT_CASE('f', f);
+ FTIME_FMT_CASE('k', k);
+ FTIME_FMT_CASE('l', l);
+ FTIME_FMT_CASE('m', m);
+ FTIME_FMT_CASE('N', N);
+ FTIME_FMT_CASE('p', p);
+ FTIME_FMT_CASE('q', q);
+ FTIME_FMT_CASE('Y', Y);
+ FTIME_FMT_CASE('y', y);
+ FTIME_FMT_CASE('z', z);
+ }
+ } else {
+ ftime_char(dst, off_inout, len, fmt[lpc]);
+ }
+ }
+
+ dst[off_inout] = '\0';
+
+ return (size_t) off_inout;
+}
diff --git a/src/pugixml/Makefile.am b/src/pugixml/Makefile.am
new file mode 100644
index 0000000..3944e78
--- /dev/null
+++ b/src/pugixml/Makefile.am
@@ -0,0 +1,9 @@
+
+noinst_LIBRARIES = libpugixml.a
+
+noinst_HEADERS = \
+ pugiconfig.hpp \
+ pugixml.hpp
+
+libpugixml_a_SOURCES = \
+ pugixml.cpp
diff --git a/src/pugixml/pugiconfig.hpp b/src/pugixml/pugiconfig.hpp
new file mode 100644
index 0000000..55060bc
--- /dev/null
+++ b/src/pugixml/pugiconfig.hpp
@@ -0,0 +1,77 @@
+/**
+ * pugixml parser - version 1.12
+ * --------------------------------------------------------
+ * Copyright (C) 2006-2022, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
+ * Report bugs and download new versions at https://pugixml.org/
+ *
+ * This library is distributed under the MIT License. See notice at the end
+ * of this file.
+ *
+ * This work is based on the pugxml parser, which is:
+ * Copyright (C) 2003, by Kristen Wegner (kristen@tima.net)
+ */
+
+#ifndef HEADER_PUGICONFIG_HPP
+#define HEADER_PUGICONFIG_HPP
+
+// Uncomment this to enable wchar_t mode
+// #define PUGIXML_WCHAR_MODE
+
+// Uncomment this to enable compact mode
+// #define PUGIXML_COMPACT
+
+// Uncomment this to disable XPath
+// #define PUGIXML_NO_XPATH
+
+// Uncomment this to disable STL
+// #define PUGIXML_NO_STL
+
+// Uncomment this to disable exceptions
+# define PUGIXML_NO_EXCEPTIONS
+
+// Set this to control attributes for public classes/functions, i.e.:
+// #define PUGIXML_API __declspec(dllexport) // to export all public symbols from DLL
+// #define PUGIXML_CLASS __declspec(dllimport) // to import all classes from DLL
+// #define PUGIXML_FUNCTION __fastcall // to set calling conventions to all public functions to fastcall
+// In absence of PUGIXML_CLASS/PUGIXML_FUNCTION definitions PUGIXML_API is used instead
+
+// Tune these constants to adjust memory-related behavior
+// #define PUGIXML_MEMORY_PAGE_SIZE 32768
+// #define PUGIXML_MEMORY_OUTPUT_STACK 10240
+// #define PUGIXML_MEMORY_XPATH_PAGE_SIZE 4096
+
+// Tune this constant to adjust max nesting for XPath queries
+// #define PUGIXML_XPATH_DEPTH_LIMIT 1024
+
+// Uncomment this to switch to header-only version
+// #define PUGIXML_HEADER_ONLY
+
+// Uncomment this to enable long long support
+// #define PUGIXML_HAS_LONG_LONG
+
+#endif
+
+/**
+ * Copyright (c) 2006-2022 Arseny Kapoulkine
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
diff --git a/src/pugixml/pugixml.cpp b/src/pugixml/pugixml.cpp
new file mode 100644
index 0000000..60b55da
--- /dev/null
+++ b/src/pugixml/pugixml.cpp
@@ -0,0 +1,13029 @@
+/**
+ * pugixml parser - version 1.12
+ * --------------------------------------------------------
+ * Copyright (C) 2006-2022, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
+ * Report bugs and download new versions at https://pugixml.org/
+ *
+ * This library is distributed under the MIT License. See notice at the end
+ * of this file.
+ *
+ * This work is based on the pugxml parser, which is:
+ * Copyright (C) 2003, by Kristen Wegner (kristen@tima.net)
+ */
+
+#ifndef SOURCE_PUGIXML_CPP
+#define SOURCE_PUGIXML_CPP
+
+#include "pugixml.hpp"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <assert.h>
+#include <limits.h>
+
+#ifdef PUGIXML_WCHAR_MODE
+# include <wchar.h>
+#endif
+
+#ifndef PUGIXML_NO_XPATH
+# include <math.h>
+# include <float.h>
+#endif
+
+#ifndef PUGIXML_NO_STL
+# include <istream>
+# include <ostream>
+# include <string>
+#endif
+
+// For placement new
+#include <new>
+
+#ifdef _MSC_VER
+# pragma warning(push)
+# pragma warning(disable: 4127) // conditional expression is constant
+# pragma warning(disable: 4324) // structure was padded due to __declspec(align())
+# pragma warning(disable: 4702) // unreachable code
+# pragma warning(disable: 4996) // this function or variable may be unsafe
+#endif
+
+#if defined(_MSC_VER) && defined(__c2__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wdeprecated" // this function or variable may be unsafe
+#endif
+
+#ifdef __INTEL_COMPILER
+# pragma warning(disable: 177) // function was declared but never referenced
+# pragma warning(disable: 279) // controlling expression is constant
+# pragma warning(disable: 1478 1786) // function was declared "deprecated"
+# pragma warning(disable: 1684) // conversion from pointer to same-sized integral type
+#endif
+
+#if defined(__BORLANDC__) && defined(PUGIXML_HEADER_ONLY)
+# pragma warn -8080 // symbol is declared but never used; disabling this inside push/pop bracket does not make the warning go away
+#endif
+
+#ifdef __BORLANDC__
+# pragma option push
+# pragma warn -8008 // condition is always false
+# pragma warn -8066 // unreachable code
+#endif
+
+#ifdef __SNC__
+// Using diag_push/diag_pop does not disable the warnings inside templates due to a compiler bug
+# pragma diag_suppress=178 // function was declared but never referenced
+# pragma diag_suppress=237 // controlling expression is constant
+#endif
+
+#ifdef __TI_COMPILER_VERSION__
+# pragma diag_suppress 179 // function was declared but never referenced
+#endif
+
+// Inlining controls
+#if defined(_MSC_VER) && _MSC_VER >= 1300
+# define PUGI__NO_INLINE __declspec(noinline)
+#elif defined(__GNUC__)
+# define PUGI__NO_INLINE __attribute__((noinline))
+#else
+# define PUGI__NO_INLINE
+#endif
+
+// Branch weight controls
+#if defined(__GNUC__) && !defined(__c2__)
+# define PUGI__UNLIKELY(cond) __builtin_expect(cond, 0)
+#else
+# define PUGI__UNLIKELY(cond) (cond)
+#endif
+
+// Simple static assertion
+#define PUGI__STATIC_ASSERT(cond) { static const char condition_failed[(cond) ? 1 : -1] = {0}; (void)condition_failed[0]; }
+
+// Digital Mars C++ bug workaround for passing char loaded from memory via stack
+#ifdef __DMC__
+# define PUGI__DMC_VOLATILE volatile
+#else
+# define PUGI__DMC_VOLATILE
+#endif
+
+// Integer sanitizer workaround; we only apply this for clang since gcc8 has no_sanitize but not unsigned-integer-overflow and produces "attribute directive ignored" warnings
+#if defined(__clang__) && defined(__has_attribute)
+# if __has_attribute(no_sanitize)
+# define PUGI__UNSIGNED_OVERFLOW __attribute__((no_sanitize("unsigned-integer-overflow")))
+# else
+# define PUGI__UNSIGNED_OVERFLOW
+# endif
+#else
+# define PUGI__UNSIGNED_OVERFLOW
+#endif
+
+// Borland C++ bug workaround for not defining ::memcpy depending on header include order (can't always use std::memcpy because some compilers don't have it at all)
+#if defined(__BORLANDC__) && !defined(__MEM_H_USING_LIST)
+using std::memcpy;
+using std::memmove;
+using std::memset;
+#endif
+
+// Some MinGW/GCC versions have headers that erroneously omit LLONG_MIN/LLONG_MAX/ULLONG_MAX definitions from limits.h in some configurations
+#if defined(PUGIXML_HAS_LONG_LONG) && defined(__GNUC__) && !defined(LLONG_MAX) && !defined(LLONG_MIN) && !defined(ULLONG_MAX)
+# define LLONG_MIN (-LLONG_MAX - 1LL)
+# define LLONG_MAX __LONG_LONG_MAX__
+# define ULLONG_MAX (LLONG_MAX * 2ULL + 1ULL)
+#endif
+
+// In some environments MSVC is a compiler but the CRT lacks certain MSVC-specific features
+#if defined(_MSC_VER) && !defined(__S3E__) && !defined(_WIN32_WCE)
+# define PUGI__MSVC_CRT_VERSION _MSC_VER
+#elif defined(_WIN32_WCE)
+# define PUGI__MSVC_CRT_VERSION 1310 // MSVC7.1
+#endif
+
+// Not all platforms have snprintf; we define a wrapper that uses snprintf if possible. This only works with buffers with a known size.
+#if __cplusplus >= 201103
+# define PUGI__SNPRINTF(buf, ...) snprintf(buf, sizeof(buf), __VA_ARGS__)
+#elif defined(PUGI__MSVC_CRT_VERSION) && PUGI__MSVC_CRT_VERSION >= 1400
+# define PUGI__SNPRINTF(buf, ...) _snprintf_s(buf, _countof(buf), _TRUNCATE, __VA_ARGS__)
+#else
+# define PUGI__SNPRINTF sprintf
+#endif
+
+// We put implementation details into an anonymous namespace in source mode, but have to keep it in non-anonymous namespace in header-only mode to prevent binary bloat.
+#ifdef PUGIXML_HEADER_ONLY
+# define PUGI__NS_BEGIN namespace pugi { namespace impl {
+# define PUGI__NS_END } }
+# define PUGI__FN inline
+# define PUGI__FN_NO_INLINE inline
+#else
+# if defined(_MSC_VER) && _MSC_VER < 1300 // MSVC6 seems to have an amusing bug with anonymous namespaces inside namespaces
+# define PUGI__NS_BEGIN namespace pugi { namespace impl {
+# define PUGI__NS_END } }
+# else
+# define PUGI__NS_BEGIN namespace pugi { namespace impl { namespace {
+# define PUGI__NS_END } } }
+# endif
+# define PUGI__FN
+# define PUGI__FN_NO_INLINE PUGI__NO_INLINE
+#endif
+
+// uintptr_t
+#if (defined(_MSC_VER) && _MSC_VER < 1600) || (defined(__BORLANDC__) && __BORLANDC__ < 0x561)
+namespace pugi
+{
+# ifndef _UINTPTR_T_DEFINED
+ typedef size_t uintptr_t;
+# endif
+
+ typedef unsigned __int8 uint8_t;
+ typedef unsigned __int16 uint16_t;
+ typedef unsigned __int32 uint32_t;
+}
+#else
+# include <stdint.h>
+#endif
+
+// Memory allocation
+PUGI__NS_BEGIN
+ PUGI__FN void* default_allocate(size_t size)
+ {
+ return malloc(size);
+ }
+
+ PUGI__FN void default_deallocate(void* ptr)
+ {
+ free(ptr);
+ }
+
+ template <typename T>
+ struct xml_memory_management_function_storage
+ {
+ static allocation_function allocate;
+ static deallocation_function deallocate;
+ };
+
+ // Global allocation functions are stored in class statics so that in header mode linker deduplicates them
+ // Without a template<> we'll get multiple definitions of the same static
+ template <typename T> allocation_function xml_memory_management_function_storage<T>::allocate = default_allocate;
+ template <typename T> deallocation_function xml_memory_management_function_storage<T>::deallocate = default_deallocate;
+
+ typedef xml_memory_management_function_storage<int> xml_memory;
+PUGI__NS_END
+
+// String utilities
+PUGI__NS_BEGIN
+ // Get string length
+ PUGI__FN size_t strlength(const char_t* s)
+ {
+ assert(s);
+
+ #ifdef PUGIXML_WCHAR_MODE
+ return wcslen(s);
+ #else
+ return strlen(s);
+ #endif
+ }
+
+ // Compare two strings
+ PUGI__FN bool strequal(const char_t* src, const char_t* dst)
+ {
+ assert(src && dst);
+
+ #ifdef PUGIXML_WCHAR_MODE
+ return wcscmp(src, dst) == 0;
+ #else
+ return strcmp(src, dst) == 0;
+ #endif
+ }
+
+ // Compare lhs with [rhs_begin, rhs_end)
+ PUGI__FN bool strequalrange(const char_t* lhs, const char_t* rhs, size_t count)
+ {
+ for (size_t i = 0; i < count; ++i)
+ if (lhs[i] != rhs[i])
+ return false;
+
+ return lhs[count] == 0;
+ }
+
+ // Get length of wide string, even if CRT lacks wide character support
+ PUGI__FN size_t strlength_wide(const wchar_t* s)
+ {
+ assert(s);
+
+ #ifdef PUGIXML_WCHAR_MODE
+ return wcslen(s);
+ #else
+ const wchar_t* end = s;
+ while (*end) end++;
+ return static_cast<size_t>(end - s);
+ #endif
+ }
+PUGI__NS_END
+
+// auto_ptr-like object for exception recovery
+PUGI__NS_BEGIN
+ template <typename T> struct auto_deleter
+ {
+ typedef void (*D)(T*);
+
+ T* data;
+ D deleter;
+
+ auto_deleter(T* data_, D deleter_): data(data_), deleter(deleter_)
+ {
+ }
+
+ ~auto_deleter()
+ {
+ if (data) deleter(data);
+ }
+
+ T* release()
+ {
+ T* result = data;
+ data = 0;
+ return result;
+ }
+ };
+PUGI__NS_END
+
+#ifdef PUGIXML_COMPACT
+PUGI__NS_BEGIN
+ class compact_hash_table
+ {
+ public:
+ compact_hash_table(): _items(0), _capacity(0), _count(0)
+ {
+ }
+
+ void clear()
+ {
+ if (_items)
+ {
+ xml_memory::deallocate(_items);
+ _items = 0;
+ _capacity = 0;
+ _count = 0;
+ }
+ }
+
+ void* find(const void* key)
+ {
+ if (_capacity == 0) return 0;
+
+ item_t* item = get_item(key);
+ assert(item);
+ assert(item->key == key || (item->key == 0 && item->value == 0));
+
+ return item->value;
+ }
+
+ void insert(const void* key, void* value)
+ {
+ assert(_capacity != 0 && _count < _capacity - _capacity / 4);
+
+ item_t* item = get_item(key);
+ assert(item);
+
+ if (item->key == 0)
+ {
+ _count++;
+ item->key = key;
+ }
+
+ item->value = value;
+ }
+
+ bool reserve(size_t extra = 16)
+ {
+ if (_count + extra >= _capacity - _capacity / 4)
+ return rehash(_count + extra);
+
+ return true;
+ }
+
+ private:
+ struct item_t
+ {
+ const void* key;
+ void* value;
+ };
+
+ item_t* _items;
+ size_t _capacity;
+
+ size_t _count;
+
+ bool rehash(size_t count);
+
+ item_t* get_item(const void* key)
+ {
+ assert(key);
+ assert(_capacity > 0);
+
+ size_t hashmod = _capacity - 1;
+ size_t bucket = hash(key) & hashmod;
+
+ for (size_t probe = 0; probe <= hashmod; ++probe)
+ {
+ item_t& probe_item = _items[bucket];
+
+ if (probe_item.key == key || probe_item.key == 0)
+ return &probe_item;
+
+ // hash collision, quadratic probing
+ bucket = (bucket + probe + 1) & hashmod;
+ }
+
+ assert(false && "Hash table is full"); // unreachable
+ return 0;
+ }
+
+ static PUGI__UNSIGNED_OVERFLOW unsigned int hash(const void* key)
+ {
+ unsigned int h = static_cast<unsigned int>(reinterpret_cast<uintptr_t>(key) & 0xffffffff);
+
+ // MurmurHash3 32-bit finalizer
+ h ^= h >> 16;
+ h *= 0x85ebca6bu;
+ h ^= h >> 13;
+ h *= 0xc2b2ae35u;
+ h ^= h >> 16;
+
+ return h;
+ }
+ };
+
+ PUGI__FN_NO_INLINE bool compact_hash_table::rehash(size_t count)
+ {
+ size_t capacity = 32;
+ while (count >= capacity - capacity / 4)
+ capacity *= 2;
+
+ compact_hash_table rt;
+ rt._capacity = capacity;
+ rt._items = static_cast<item_t*>(xml_memory::allocate(sizeof(item_t) * capacity));
+
+ if (!rt._items)
+ return false;
+
+ memset(rt._items, 0, sizeof(item_t) * capacity);
+
+ for (size_t i = 0; i < _capacity; ++i)
+ if (_items[i].key)
+ rt.insert(_items[i].key, _items[i].value);
+
+ if (_items)
+ xml_memory::deallocate(_items);
+
+ _capacity = capacity;
+ _items = rt._items;
+
+ assert(_count == rt._count);
+
+ return true;
+ }
+
+PUGI__NS_END
+#endif
+
+PUGI__NS_BEGIN
+#ifdef PUGIXML_COMPACT
+ static const uintptr_t xml_memory_block_alignment = 4;
+#else
+ static const uintptr_t xml_memory_block_alignment = sizeof(void*);
+#endif
+
+ // extra metadata bits
+ static const uintptr_t xml_memory_page_contents_shared_mask = 64;
+ static const uintptr_t xml_memory_page_name_allocated_mask = 32;
+ static const uintptr_t xml_memory_page_value_allocated_mask = 16;
+ static const uintptr_t xml_memory_page_type_mask = 15;
+
+ // combined masks for string uniqueness
+ static const uintptr_t xml_memory_page_name_allocated_or_shared_mask = xml_memory_page_name_allocated_mask | xml_memory_page_contents_shared_mask;
+ static const uintptr_t xml_memory_page_value_allocated_or_shared_mask = xml_memory_page_value_allocated_mask | xml_memory_page_contents_shared_mask;
+
+#ifdef PUGIXML_COMPACT
+ #define PUGI__GETHEADER_IMPL(object, page, flags) // unused
+ #define PUGI__GETPAGE_IMPL(header) (header).get_page()
+#else
+ #define PUGI__GETHEADER_IMPL(object, page, flags) (((reinterpret_cast<char*>(object) - reinterpret_cast<char*>(page)) << 8) | (flags))
+ // this macro casts pointers through void* to avoid 'cast increases required alignment of target type' warnings
+ #define PUGI__GETPAGE_IMPL(header) static_cast<impl::xml_memory_page*>(const_cast<void*>(static_cast<const void*>(reinterpret_cast<const char*>(&header) - (header >> 8))))
+#endif
+
+ #define PUGI__GETPAGE(n) PUGI__GETPAGE_IMPL((n)->header)
+ #define PUGI__NODETYPE(n) static_cast<xml_node_type>((n)->header & impl::xml_memory_page_type_mask)
+
+ struct xml_allocator;
+
+ struct xml_memory_page
+ {
+ static xml_memory_page* construct(void* memory)
+ {
+ xml_memory_page* result = static_cast<xml_memory_page*>(memory);
+
+ result->allocator = 0;
+ result->prev = 0;
+ result->next = 0;
+ result->busy_size = 0;
+ result->freed_size = 0;
+
+ #ifdef PUGIXML_COMPACT
+ result->compact_string_base = 0;
+ result->compact_shared_parent = 0;
+ result->compact_page_marker = 0;
+ #endif
+
+ return result;
+ }
+
+ xml_allocator* allocator;
+
+ xml_memory_page* prev;
+ xml_memory_page* next;
+
+ size_t busy_size;
+ size_t freed_size;
+
+ #ifdef PUGIXML_COMPACT
+ char_t* compact_string_base;
+ void* compact_shared_parent;
+ uint32_t* compact_page_marker;
+ #endif
+ };
+
+ static const size_t xml_memory_page_size =
+ #ifdef PUGIXML_MEMORY_PAGE_SIZE
+ (PUGIXML_MEMORY_PAGE_SIZE)
+ #else
+ 32768
+ #endif
+ - sizeof(xml_memory_page);
+
+ struct xml_memory_string_header
+ {
+ uint16_t page_offset; // offset from page->data
+ uint16_t full_size; // 0 if string occupies whole page
+ };
+
+ struct xml_allocator
+ {
+ xml_allocator(xml_memory_page* root): _root(root), _busy_size(root->busy_size)
+ {
+ #ifdef PUGIXML_COMPACT
+ _hash = 0;
+ #endif
+ }
+
+ xml_memory_page* allocate_page(size_t data_size)
+ {
+ size_t size = sizeof(xml_memory_page) + data_size;
+
+ // allocate block with some alignment, leaving memory for worst-case padding
+ void* memory = xml_memory::allocate(size);
+ if (!memory) return 0;
+
+ // prepare page structure
+ xml_memory_page* page = xml_memory_page::construct(memory);
+ assert(page);
+
+ assert(this == _root->allocator);
+ page->allocator = this;
+
+ return page;
+ }
+
+ static void deallocate_page(xml_memory_page* page)
+ {
+ xml_memory::deallocate(page);
+ }
+
+ void* allocate_memory_oob(size_t size, xml_memory_page*& out_page);
+
+ void* allocate_memory(size_t size, xml_memory_page*& out_page)
+ {
+ if (PUGI__UNLIKELY(_busy_size + size > xml_memory_page_size))
+ return allocate_memory_oob(size, out_page);
+
+ void* buf = reinterpret_cast<char*>(_root) + sizeof(xml_memory_page) + _busy_size;
+
+ _busy_size += size;
+
+ out_page = _root;
+
+ return buf;
+ }
+
+ #ifdef PUGIXML_COMPACT
+ void* allocate_object(size_t size, xml_memory_page*& out_page)
+ {
+ void* result = allocate_memory(size + sizeof(uint32_t), out_page);
+ if (!result) return 0;
+
+ // adjust for marker
+ ptrdiff_t offset = static_cast<char*>(result) - reinterpret_cast<char*>(out_page->compact_page_marker);
+
+ if (PUGI__UNLIKELY(static_cast<uintptr_t>(offset) >= 256 * xml_memory_block_alignment))
+ {
+ // insert new marker
+ uint32_t* marker = static_cast<uint32_t*>(result);
+
+ *marker = static_cast<uint32_t>(reinterpret_cast<char*>(marker) - reinterpret_cast<char*>(out_page));
+ out_page->compact_page_marker = marker;
+
+ // since we don't reuse the page space until we reallocate it, we can just pretend that we freed the marker block
+ // this will make sure deallocate_memory correctly tracks the size
+ out_page->freed_size += sizeof(uint32_t);
+
+ return marker + 1;
+ }
+ else
+ {
+ // roll back uint32_t part
+ _busy_size -= sizeof(uint32_t);
+
+ return result;
+ }
+ }
+ #else
+ void* allocate_object(size_t size, xml_memory_page*& out_page)
+ {
+ return allocate_memory(size, out_page);
+ }
+ #endif
+
+ void deallocate_memory(void* ptr, size_t size, xml_memory_page* page)
+ {
+ if (page == _root) page->busy_size = _busy_size;
+
+ assert(ptr >= reinterpret_cast<char*>(page) + sizeof(xml_memory_page) && ptr < reinterpret_cast<char*>(page) + sizeof(xml_memory_page) + page->busy_size);
+ (void)!ptr;
+
+ page->freed_size += size;
+ assert(page->freed_size <= page->busy_size);
+
+ if (page->freed_size == page->busy_size)
+ {
+ if (page->next == 0)
+ {
+ assert(_root == page);
+
+ // top page freed, just reset sizes
+ page->busy_size = 0;
+ page->freed_size = 0;
+
+ #ifdef PUGIXML_COMPACT
+ // reset compact state to maximize efficiency
+ page->compact_string_base = 0;
+ page->compact_shared_parent = 0;
+ page->compact_page_marker = 0;
+ #endif
+
+ _busy_size = 0;
+ }
+ else
+ {
+ assert(_root != page);
+ assert(page->prev);
+
+ // remove from the list
+ page->prev->next = page->next;
+ page->next->prev = page->prev;
+
+ // deallocate
+ deallocate_page(page);
+ }
+ }
+ }
+
+ char_t* allocate_string(size_t length)
+ {
+ static const size_t max_encoded_offset = (1 << 16) * xml_memory_block_alignment;
+
+ PUGI__STATIC_ASSERT(xml_memory_page_size <= max_encoded_offset);
+
+ // allocate memory for string and header block
+ size_t size = sizeof(xml_memory_string_header) + length * sizeof(char_t);
+
+ // round size up to block alignment boundary
+ size_t full_size = (size + (xml_memory_block_alignment - 1)) & ~(xml_memory_block_alignment - 1);
+
+ xml_memory_page* page;
+ xml_memory_string_header* header = static_cast<xml_memory_string_header*>(allocate_memory(full_size, page));
+
+ if (!header) return 0;
+
+ // setup header
+ ptrdiff_t page_offset = reinterpret_cast<char*>(header) - reinterpret_cast<char*>(page) - sizeof(xml_memory_page);
+
+ assert(page_offset % xml_memory_block_alignment == 0);
+ assert(page_offset >= 0 && static_cast<size_t>(page_offset) < max_encoded_offset);
+ header->page_offset = static_cast<uint16_t>(static_cast<size_t>(page_offset) / xml_memory_block_alignment);
+
+ // full_size == 0 for large strings that occupy the whole page
+ assert(full_size % xml_memory_block_alignment == 0);
+ assert(full_size < max_encoded_offset || (page->busy_size == full_size && page_offset == 0));
+ header->full_size = static_cast<uint16_t>(full_size < max_encoded_offset ? full_size / xml_memory_block_alignment : 0);
+
+ // round-trip through void* to avoid 'cast increases required alignment of target type' warning
+ // header is guaranteed a pointer-sized alignment, which should be enough for char_t
+ return static_cast<char_t*>(static_cast<void*>(header + 1));
+ }
+
+ void deallocate_string(char_t* string)
+ {
+ // this function casts pointers through void* to avoid 'cast increases required alignment of target type' warnings
+ // we're guaranteed the proper (pointer-sized) alignment on the input string if it was allocated via allocate_string
+
+ // get header
+ xml_memory_string_header* header = static_cast<xml_memory_string_header*>(static_cast<void*>(string)) - 1;
+ assert(header);
+
+ // deallocate
+ size_t page_offset = sizeof(xml_memory_page) + header->page_offset * xml_memory_block_alignment;
+ xml_memory_page* page = reinterpret_cast<xml_memory_page*>(static_cast<void*>(reinterpret_cast<char*>(header) - page_offset));
+
+ // if full_size == 0 then this string occupies the whole page
+ size_t full_size = header->full_size == 0 ? page->busy_size : header->full_size * xml_memory_block_alignment;
+
+ deallocate_memory(header, full_size, page);
+ }
+
+ bool reserve()
+ {
+ #ifdef PUGIXML_COMPACT
+ return _hash->reserve();
+ #else
+ return true;
+ #endif
+ }
+
+ xml_memory_page* _root;
+ size_t _busy_size;
+
+ #ifdef PUGIXML_COMPACT
+ compact_hash_table* _hash;
+ #endif
+ };
+
+ PUGI__FN_NO_INLINE void* xml_allocator::allocate_memory_oob(size_t size, xml_memory_page*& out_page)
+ {
+ const size_t large_allocation_threshold = xml_memory_page_size / 4;
+
+ xml_memory_page* page = allocate_page(size <= large_allocation_threshold ? xml_memory_page_size : size);
+ out_page = page;
+
+ if (!page) return 0;
+
+ if (size <= large_allocation_threshold)
+ {
+ _root->busy_size = _busy_size;
+
+ // insert page at the end of linked list
+ page->prev = _root;
+ _root->next = page;
+ _root = page;
+
+ _busy_size = size;
+ }
+ else
+ {
+ // insert page before the end of linked list, so that it is deleted as soon as possible
+ // the last page is not deleted even if it's empty (see deallocate_memory)
+ assert(_root->prev);
+
+ page->prev = _root->prev;
+ page->next = _root;
+
+ _root->prev->next = page;
+ _root->prev = page;
+
+ page->busy_size = size;
+ }
+
+ return reinterpret_cast<char*>(page) + sizeof(xml_memory_page);
+ }
+PUGI__NS_END
+
+#ifdef PUGIXML_COMPACT
+PUGI__NS_BEGIN
+ static const uintptr_t compact_alignment_log2 = 2;
+ static const uintptr_t compact_alignment = 1 << compact_alignment_log2;
+
+ class compact_header
+ {
+ public:
+ compact_header(xml_memory_page* page, unsigned int flags)
+ {
+ PUGI__STATIC_ASSERT(xml_memory_block_alignment == compact_alignment);
+
+ ptrdiff_t offset = (reinterpret_cast<char*>(this) - reinterpret_cast<char*>(page->compact_page_marker));
+ assert(offset % compact_alignment == 0 && static_cast<uintptr_t>(offset) < 256 * compact_alignment);
+
+ _page = static_cast<unsigned char>(offset >> compact_alignment_log2);
+ _flags = static_cast<unsigned char>(flags);
+ }
+
+ void operator&=(uintptr_t mod)
+ {
+ _flags &= static_cast<unsigned char>(mod);
+ }
+
+ void operator|=(uintptr_t mod)
+ {
+ _flags |= static_cast<unsigned char>(mod);
+ }
+
+ uintptr_t operator&(uintptr_t mod) const
+ {
+ return _flags & mod;
+ }
+
+ xml_memory_page* get_page() const
+ {
+ // round-trip through void* to silence 'cast increases required alignment of target type' warnings
+ const char* page_marker = reinterpret_cast<const char*>(this) - (_page << compact_alignment_log2);
+ const char* page = page_marker - *reinterpret_cast<const uint32_t*>(static_cast<const void*>(page_marker));
+
+ return const_cast<xml_memory_page*>(reinterpret_cast<const xml_memory_page*>(static_cast<const void*>(page)));
+ }
+
+ private:
+ unsigned char _page;
+ unsigned char _flags;
+ };
+
+ PUGI__FN xml_memory_page* compact_get_page(const void* object, int header_offset)
+ {
+ const compact_header* header = reinterpret_cast<const compact_header*>(static_cast<const char*>(object) - header_offset);
+
+ return header->get_page();
+ }
+
+ template <int header_offset, typename T> PUGI__FN_NO_INLINE T* compact_get_value(const void* object)
+ {
+ return static_cast<T*>(compact_get_page(object, header_offset)->allocator->_hash->find(object));
+ }
+
+ template <int header_offset, typename T> PUGI__FN_NO_INLINE void compact_set_value(const void* object, T* value)
+ {
+ compact_get_page(object, header_offset)->allocator->_hash->insert(object, value);
+ }
+
+ template <typename T, int header_offset, int start = -126> class compact_pointer
+ {
+ public:
+ compact_pointer(): _data(0)
+ {
+ }
+
+ void operator=(const compact_pointer& rhs)
+ {
+ *this = rhs + 0;
+ }
+
+ void operator=(T* value)
+ {
+ if (value)
+ {
+ // value is guaranteed to be compact-aligned; 'this' is not
+ // our decoding is based on 'this' aligned to compact alignment downwards (see operator T*)
+ // so for negative offsets (e.g. -3) we need to adjust the diff by compact_alignment - 1 to
+ // compensate for arithmetic shift rounding for negative values
+ ptrdiff_t diff = reinterpret_cast<char*>(value) - reinterpret_cast<char*>(this);
+ ptrdiff_t offset = ((diff + int(compact_alignment - 1)) >> compact_alignment_log2) - start;
+
+ if (static_cast<uintptr_t>(offset) <= 253)
+ _data = static_cast<unsigned char>(offset + 1);
+ else
+ {
+ compact_set_value<header_offset>(this, value);
+
+ _data = 255;
+ }
+ }
+ else
+ _data = 0;
+ }
+
+ operator T*() const
+ {
+ if (_data)
+ {
+ if (_data < 255)
+ {
+ uintptr_t base = reinterpret_cast<uintptr_t>(this) & ~(compact_alignment - 1);
+
+ return reinterpret_cast<T*>(base + (_data - 1 + start) * compact_alignment);
+ }
+ else
+ return compact_get_value<header_offset, T>(this);
+ }
+ else
+ return 0;
+ }
+
+ T* operator->() const
+ {
+ return *this;
+ }
+
+ private:
+ unsigned char _data;
+ };
+
+ template <typename T, int header_offset> class compact_pointer_parent
+ {
+ public:
+ compact_pointer_parent(): _data(0)
+ {
+ }
+
+ void operator=(const compact_pointer_parent& rhs)
+ {
+ *this = rhs + 0;
+ }
+
+ void operator=(T* value)
+ {
+ if (value)
+ {
+ // value is guaranteed to be compact-aligned; 'this' is not
+ // our decoding is based on 'this' aligned to compact alignment downwards (see operator T*)
+ // so for negative offsets (e.g. -3) we need to adjust the diff by compact_alignment - 1 to
+ // compensate for arithmetic shift behavior for negative values
+ ptrdiff_t diff = reinterpret_cast<char*>(value) - reinterpret_cast<char*>(this);
+ ptrdiff_t offset = ((diff + int(compact_alignment - 1)) >> compact_alignment_log2) + 65533;
+
+ if (static_cast<uintptr_t>(offset) <= 65533)
+ {
+ _data = static_cast<unsigned short>(offset + 1);
+ }
+ else
+ {
+ xml_memory_page* page = compact_get_page(this, header_offset);
+
+ if (PUGI__UNLIKELY(page->compact_shared_parent == 0))
+ page->compact_shared_parent = value;
+
+ if (page->compact_shared_parent == value)
+ {
+ _data = 65534;
+ }
+ else
+ {
+ compact_set_value<header_offset>(this, value);
+
+ _data = 65535;
+ }
+ }
+ }
+ else
+ {
+ _data = 0;
+ }
+ }
+
+ operator T*() const
+ {
+ if (_data)
+ {
+ if (_data < 65534)
+ {
+ uintptr_t base = reinterpret_cast<uintptr_t>(this) & ~(compact_alignment - 1);
+
+ return reinterpret_cast<T*>(base + (_data - 1 - 65533) * compact_alignment);
+ }
+ else if (_data == 65534)
+ return static_cast<T*>(compact_get_page(this, header_offset)->compact_shared_parent);
+ else
+ return compact_get_value<header_offset, T>(this);
+ }
+ else
+ return 0;
+ }
+
+ T* operator->() const
+ {
+ return *this;
+ }
+
+ private:
+ uint16_t _data;
+ };
+
+ template <int header_offset, int base_offset> class compact_string
+ {
+ public:
+ compact_string(): _data(0)
+ {
+ }
+
+ void operator=(const compact_string& rhs)
+ {
+ *this = rhs + 0;
+ }
+
+ void operator=(char_t* value)
+ {
+ if (value)
+ {
+ xml_memory_page* page = compact_get_page(this, header_offset);
+
+ if (PUGI__UNLIKELY(page->compact_string_base == 0))
+ page->compact_string_base = value;
+
+ ptrdiff_t offset = value - page->compact_string_base;
+
+ if (static_cast<uintptr_t>(offset) < (65535 << 7))
+ {
+ // round-trip through void* to silence 'cast increases required alignment of target type' warnings
+ uint16_t* base = reinterpret_cast<uint16_t*>(static_cast<void*>(reinterpret_cast<char*>(this) - base_offset));
+
+ if (*base == 0)
+ {
+ *base = static_cast<uint16_t>((offset >> 7) + 1);
+ _data = static_cast<unsigned char>((offset & 127) + 1);
+ }
+ else
+ {
+ ptrdiff_t remainder = offset - ((*base - 1) << 7);
+
+ if (static_cast<uintptr_t>(remainder) <= 253)
+ {
+ _data = static_cast<unsigned char>(remainder + 1);
+ }
+ else
+ {
+ compact_set_value<header_offset>(this, value);
+
+ _data = 255;
+ }
+ }
+ }
+ else
+ {
+ compact_set_value<header_offset>(this, value);
+
+ _data = 255;
+ }
+ }
+ else
+ {
+ _data = 0;
+ }
+ }
+
+ operator char_t*() const
+ {
+ if (_data)
+ {
+ if (_data < 255)
+ {
+ xml_memory_page* page = compact_get_page(this, header_offset);
+
+ // round-trip through void* to silence 'cast increases required alignment of target type' warnings
+ const uint16_t* base = reinterpret_cast<const uint16_t*>(static_cast<const void*>(reinterpret_cast<const char*>(this) - base_offset));
+ assert(*base);
+
+ ptrdiff_t offset = ((*base - 1) << 7) + (_data - 1);
+
+ return page->compact_string_base + offset;
+ }
+ else
+ {
+ return compact_get_value<header_offset, char_t>(this);
+ }
+ }
+ else
+ return 0;
+ }
+
+ private:
+ unsigned char _data;
+ };
+PUGI__NS_END
+#endif
+
+#ifdef PUGIXML_COMPACT
+namespace pugi
+{
+ struct xml_attribute_struct
+ {
+ xml_attribute_struct(impl::xml_memory_page* page): header(page, 0), namevalue_base(0)
+ {
+ PUGI__STATIC_ASSERT(sizeof(xml_attribute_struct) == 8);
+ }
+
+ impl::compact_header header;
+
+ uint16_t namevalue_base;
+
+ impl::compact_string<4, 2> name;
+ impl::compact_string<5, 3> value;
+
+ impl::compact_pointer<xml_attribute_struct, 6> prev_attribute_c;
+ impl::compact_pointer<xml_attribute_struct, 7, 0> next_attribute;
+ };
+
+ struct xml_node_struct
+ {
+ xml_node_struct(impl::xml_memory_page* page, xml_node_type type): header(page, type), namevalue_base(0)
+ {
+ PUGI__STATIC_ASSERT(sizeof(xml_node_struct) == 12);
+ }
+
+ impl::compact_header header;
+
+ uint16_t namevalue_base;
+
+ impl::compact_string<4, 2> name;
+ impl::compact_string<5, 3> value;
+
+ impl::compact_pointer_parent<xml_node_struct, 6> parent;
+
+ impl::compact_pointer<xml_node_struct, 8, 0> first_child;
+
+ impl::compact_pointer<xml_node_struct, 9> prev_sibling_c;
+ impl::compact_pointer<xml_node_struct, 10, 0> next_sibling;
+
+ impl::compact_pointer<xml_attribute_struct, 11, 0> first_attribute;
+ };
+}
+#else
+namespace pugi
+{
+ struct xml_attribute_struct
+ {
+ xml_attribute_struct(impl::xml_memory_page* page): name(0), value(0), prev_attribute_c(0), next_attribute(0)
+ {
+ header = PUGI__GETHEADER_IMPL(this, page, 0);
+ }
+
+ uintptr_t header;
+
+ char_t* name;
+ char_t* value;
+
+ xml_attribute_struct* prev_attribute_c;
+ xml_attribute_struct* next_attribute;
+ };
+
+ struct xml_node_struct
+ {
+ xml_node_struct(impl::xml_memory_page* page, xml_node_type type): name(0), value(0), parent(0), first_child(0), prev_sibling_c(0), next_sibling(0), first_attribute(0)
+ {
+ header = PUGI__GETHEADER_IMPL(this, page, type);
+ }
+
+ uintptr_t header;
+
+ char_t* name;
+ char_t* value;
+
+ xml_node_struct* parent;
+
+ xml_node_struct* first_child;
+
+ xml_node_struct* prev_sibling_c;
+ xml_node_struct* next_sibling;
+
+ xml_attribute_struct* first_attribute;
+ };
+}
+#endif
+
+PUGI__NS_BEGIN
+ struct xml_extra_buffer
+ {
+ char_t* buffer;
+ xml_extra_buffer* next;
+ };
+
+ struct xml_document_struct: public xml_node_struct, public xml_allocator
+ {
+ xml_document_struct(xml_memory_page* page): xml_node_struct(page, node_document), xml_allocator(page), buffer(0), extra_buffers(0)
+ {
+ }
+
+ const char_t* buffer;
+
+ xml_extra_buffer* extra_buffers;
+
+ #ifdef PUGIXML_COMPACT
+ compact_hash_table hash;
+ #endif
+ };
+
+ template <typename Object> inline xml_allocator& get_allocator(const Object* object)
+ {
+ assert(object);
+
+ return *PUGI__GETPAGE(object)->allocator;
+ }
+
+ template <typename Object> inline xml_document_struct& get_document(const Object* object)
+ {
+ assert(object);
+
+ return *static_cast<xml_document_struct*>(PUGI__GETPAGE(object)->allocator);
+ }
+PUGI__NS_END
+
+// Low-level DOM operations
+PUGI__NS_BEGIN
+ inline xml_attribute_struct* allocate_attribute(xml_allocator& alloc)
+ {
+ xml_memory_page* page;
+ void* memory = alloc.allocate_object(sizeof(xml_attribute_struct), page);
+ if (!memory) return 0;
+
+ return new (memory) xml_attribute_struct(page);
+ }
+
+ inline xml_node_struct* allocate_node(xml_allocator& alloc, xml_node_type type)
+ {
+ xml_memory_page* page;
+ void* memory = alloc.allocate_object(sizeof(xml_node_struct), page);
+ if (!memory) return 0;
+
+ return new (memory) xml_node_struct(page, type);
+ }
+
+ inline void destroy_attribute(xml_attribute_struct* a, xml_allocator& alloc)
+ {
+ if (a->header & impl::xml_memory_page_name_allocated_mask)
+ alloc.deallocate_string(a->name);
+
+ if (a->header & impl::xml_memory_page_value_allocated_mask)
+ alloc.deallocate_string(a->value);
+
+ alloc.deallocate_memory(a, sizeof(xml_attribute_struct), PUGI__GETPAGE(a));
+ }
+
+ inline void destroy_node(xml_node_struct* n, xml_allocator& alloc)
+ {
+ if (n->header & impl::xml_memory_page_name_allocated_mask)
+ alloc.deallocate_string(n->name);
+
+ if (n->header & impl::xml_memory_page_value_allocated_mask)
+ alloc.deallocate_string(n->value);
+
+ for (xml_attribute_struct* attr = n->first_attribute; attr; )
+ {
+ xml_attribute_struct* next = attr->next_attribute;
+
+ destroy_attribute(attr, alloc);
+
+ attr = next;
+ }
+
+ for (xml_node_struct* child = n->first_child; child; )
+ {
+ xml_node_struct* next = child->next_sibling;
+
+ destroy_node(child, alloc);
+
+ child = next;
+ }
+
+ alloc.deallocate_memory(n, sizeof(xml_node_struct), PUGI__GETPAGE(n));
+ }
+
+ inline void append_node(xml_node_struct* child, xml_node_struct* node)
+ {
+ child->parent = node;
+
+ xml_node_struct* head = node->first_child;
+
+ if (head)
+ {
+ xml_node_struct* tail = head->prev_sibling_c;
+
+ tail->next_sibling = child;
+ child->prev_sibling_c = tail;
+ head->prev_sibling_c = child;
+ }
+ else
+ {
+ node->first_child = child;
+ child->prev_sibling_c = child;
+ }
+ }
+
+ inline void prepend_node(xml_node_struct* child, xml_node_struct* node)
+ {
+ child->parent = node;
+
+ xml_node_struct* head = node->first_child;
+
+ if (head)
+ {
+ child->prev_sibling_c = head->prev_sibling_c;
+ head->prev_sibling_c = child;
+ }
+ else
+ child->prev_sibling_c = child;
+
+ child->next_sibling = head;
+ node->first_child = child;
+ }
+
+ inline void insert_node_after(xml_node_struct* child, xml_node_struct* node)
+ {
+ xml_node_struct* parent = node->parent;
+
+ child->parent = parent;
+
+ if (node->next_sibling)
+ node->next_sibling->prev_sibling_c = child;
+ else
+ parent->first_child->prev_sibling_c = child;
+
+ child->next_sibling = node->next_sibling;
+ child->prev_sibling_c = node;
+
+ node->next_sibling = child;
+ }
+
+ inline void insert_node_before(xml_node_struct* child, xml_node_struct* node)
+ {
+ xml_node_struct* parent = node->parent;
+
+ child->parent = parent;
+
+ if (node->prev_sibling_c->next_sibling)
+ node->prev_sibling_c->next_sibling = child;
+ else
+ parent->first_child = child;
+
+ child->prev_sibling_c = node->prev_sibling_c;
+ child->next_sibling = node;
+
+ node->prev_sibling_c = child;
+ }
+
+ inline void remove_node(xml_node_struct* node)
+ {
+ xml_node_struct* parent = node->parent;
+
+ if (node->next_sibling)
+ node->next_sibling->prev_sibling_c = node->prev_sibling_c;
+ else
+ parent->first_child->prev_sibling_c = node->prev_sibling_c;
+
+ if (node->prev_sibling_c->next_sibling)
+ node->prev_sibling_c->next_sibling = node->next_sibling;
+ else
+ parent->first_child = node->next_sibling;
+
+ node->parent = 0;
+ node->prev_sibling_c = 0;
+ node->next_sibling = 0;
+ }
+
+ inline void append_attribute(xml_attribute_struct* attr, xml_node_struct* node)
+ {
+ xml_attribute_struct* head = node->first_attribute;
+
+ if (head)
+ {
+ xml_attribute_struct* tail = head->prev_attribute_c;
+
+ tail->next_attribute = attr;
+ attr->prev_attribute_c = tail;
+ head->prev_attribute_c = attr;
+ }
+ else
+ {
+ node->first_attribute = attr;
+ attr->prev_attribute_c = attr;
+ }
+ }
+
+ inline void prepend_attribute(xml_attribute_struct* attr, xml_node_struct* node)
+ {
+ xml_attribute_struct* head = node->first_attribute;
+
+ if (head)
+ {
+ attr->prev_attribute_c = head->prev_attribute_c;
+ head->prev_attribute_c = attr;
+ }
+ else
+ attr->prev_attribute_c = attr;
+
+ attr->next_attribute = head;
+ node->first_attribute = attr;
+ }
+
+ inline void insert_attribute_after(xml_attribute_struct* attr, xml_attribute_struct* place, xml_node_struct* node)
+ {
+ if (place->next_attribute)
+ place->next_attribute->prev_attribute_c = attr;
+ else
+ node->first_attribute->prev_attribute_c = attr;
+
+ attr->next_attribute = place->next_attribute;
+ attr->prev_attribute_c = place;
+ place->next_attribute = attr;
+ }
+
+ inline void insert_attribute_before(xml_attribute_struct* attr, xml_attribute_struct* place, xml_node_struct* node)
+ {
+ if (place->prev_attribute_c->next_attribute)
+ place->prev_attribute_c->next_attribute = attr;
+ else
+ node->first_attribute = attr;
+
+ attr->prev_attribute_c = place->prev_attribute_c;
+ attr->next_attribute = place;
+ place->prev_attribute_c = attr;
+ }
+
+ inline void remove_attribute(xml_attribute_struct* attr, xml_node_struct* node)
+ {
+ if (attr->next_attribute)
+ attr->next_attribute->prev_attribute_c = attr->prev_attribute_c;
+ else
+ node->first_attribute->prev_attribute_c = attr->prev_attribute_c;
+
+ if (attr->prev_attribute_c->next_attribute)
+ attr->prev_attribute_c->next_attribute = attr->next_attribute;
+ else
+ node->first_attribute = attr->next_attribute;
+
+ attr->prev_attribute_c = 0;
+ attr->next_attribute = 0;
+ }
+
+ PUGI__FN_NO_INLINE xml_node_struct* append_new_node(xml_node_struct* node, xml_allocator& alloc, xml_node_type type = node_element)
+ {
+ if (!alloc.reserve()) return 0;
+
+ xml_node_struct* child = allocate_node(alloc, type);
+ if (!child) return 0;
+
+ append_node(child, node);
+
+ return child;
+ }
+
+ PUGI__FN_NO_INLINE xml_attribute_struct* append_new_attribute(xml_node_struct* node, xml_allocator& alloc)
+ {
+ if (!alloc.reserve()) return 0;
+
+ xml_attribute_struct* attr = allocate_attribute(alloc);
+ if (!attr) return 0;
+
+ append_attribute(attr, node);
+
+ return attr;
+ }
+PUGI__NS_END
+
+// Helper classes for code generation
+PUGI__NS_BEGIN
+ struct opt_false
+ {
+ enum { value = 0 };
+ };
+
+ struct opt_true
+ {
+ enum { value = 1 };
+ };
+PUGI__NS_END
+
+// Unicode utilities
+PUGI__NS_BEGIN
+ inline uint16_t endian_swap(uint16_t value)
+ {
+ return static_cast<uint16_t>(((value & 0xff) << 8) | (value >> 8));
+ }
+
+ inline uint32_t endian_swap(uint32_t value)
+ {
+ return ((value & 0xff) << 24) | ((value & 0xff00) << 8) | ((value & 0xff0000) >> 8) | (value >> 24);
+ }
+
+ struct utf8_counter
+ {
+ typedef size_t value_type;
+
+ static value_type low(value_type result, uint32_t ch)
+ {
+ // U+0000..U+007F
+ if (ch < 0x80) return result + 1;
+ // U+0080..U+07FF
+ else if (ch < 0x800) return result + 2;
+ // U+0800..U+FFFF
+ else return result + 3;
+ }
+
+ static value_type high(value_type result, uint32_t)
+ {
+ // U+10000..U+10FFFF
+ return result + 4;
+ }
+ };
+
+ struct utf8_writer
+ {
+ typedef uint8_t* value_type;
+
+ static value_type low(value_type result, uint32_t ch)
+ {
+ // U+0000..U+007F
+ if (ch < 0x80)
+ {
+ *result = static_cast<uint8_t>(ch);
+ return result + 1;
+ }
+ // U+0080..U+07FF
+ else if (ch < 0x800)
+ {
+ result[0] = static_cast<uint8_t>(0xC0 | (ch >> 6));
+ result[1] = static_cast<uint8_t>(0x80 | (ch & 0x3F));
+ return result + 2;
+ }
+ // U+0800..U+FFFF
+ else
+ {
+ result[0] = static_cast<uint8_t>(0xE0 | (ch >> 12));
+ result[1] = static_cast<uint8_t>(0x80 | ((ch >> 6) & 0x3F));
+ result[2] = static_cast<uint8_t>(0x80 | (ch & 0x3F));
+ return result + 3;
+ }
+ }
+
+ static value_type high(value_type result, uint32_t ch)
+ {
+ // U+10000..U+10FFFF
+ result[0] = static_cast<uint8_t>(0xF0 | (ch >> 18));
+ result[1] = static_cast<uint8_t>(0x80 | ((ch >> 12) & 0x3F));
+ result[2] = static_cast<uint8_t>(0x80 | ((ch >> 6) & 0x3F));
+ result[3] = static_cast<uint8_t>(0x80 | (ch & 0x3F));
+ return result + 4;
+ }
+
+ static value_type any(value_type result, uint32_t ch)
+ {
+ return (ch < 0x10000) ? low(result, ch) : high(result, ch);
+ }
+ };
+
+ struct utf16_counter
+ {
+ typedef size_t value_type;
+
+ static value_type low(value_type result, uint32_t)
+ {
+ return result + 1;
+ }
+
+ static value_type high(value_type result, uint32_t)
+ {
+ return result + 2;
+ }
+ };
+
+ struct utf16_writer
+ {
+ typedef uint16_t* value_type;
+
+ static value_type low(value_type result, uint32_t ch)
+ {
+ *result = static_cast<uint16_t>(ch);
+
+ return result + 1;
+ }
+
+ static value_type high(value_type result, uint32_t ch)
+ {
+ uint32_t msh = static_cast<uint32_t>(ch - 0x10000) >> 10;
+ uint32_t lsh = static_cast<uint32_t>(ch - 0x10000) & 0x3ff;
+
+ result[0] = static_cast<uint16_t>(0xD800 + msh);
+ result[1] = static_cast<uint16_t>(0xDC00 + lsh);
+
+ return result + 2;
+ }
+
+ static value_type any(value_type result, uint32_t ch)
+ {
+ return (ch < 0x10000) ? low(result, ch) : high(result, ch);
+ }
+ };
+
+ struct utf32_counter
+ {
+ typedef size_t value_type;
+
+ static value_type low(value_type result, uint32_t)
+ {
+ return result + 1;
+ }
+
+ static value_type high(value_type result, uint32_t)
+ {
+ return result + 1;
+ }
+ };
+
+ struct utf32_writer
+ {
+ typedef uint32_t* value_type;
+
+ static value_type low(value_type result, uint32_t ch)
+ {
+ *result = ch;
+
+ return result + 1;
+ }
+
+ static value_type high(value_type result, uint32_t ch)
+ {
+ *result = ch;
+
+ return result + 1;
+ }
+
+ static value_type any(value_type result, uint32_t ch)
+ {
+ *result = ch;
+
+ return result + 1;
+ }
+ };
+
+ struct latin1_writer
+ {
+ typedef uint8_t* value_type;
+
+ static value_type low(value_type result, uint32_t ch)
+ {
+ *result = static_cast<uint8_t>(ch > 255 ? '?' : ch);
+
+ return result + 1;
+ }
+
+ static value_type high(value_type result, uint32_t ch)
+ {
+ (void)ch;
+
+ *result = '?';
+
+ return result + 1;
+ }
+ };
+
+ struct utf8_decoder
+ {
+ typedef uint8_t type;
+
+ template <typename Traits> static inline typename Traits::value_type process(const uint8_t* data, size_t size, typename Traits::value_type result, Traits)
+ {
+ const uint8_t utf8_byte_mask = 0x3f;
+
+ while (size)
+ {
+ uint8_t lead = *data;
+
+ // 0xxxxxxx -> U+0000..U+007F
+ if (lead < 0x80)
+ {
+ result = Traits::low(result, lead);
+ data += 1;
+ size -= 1;
+
+ // process aligned single-byte (ascii) blocks
+ if ((reinterpret_cast<uintptr_t>(data) & 3) == 0)
+ {
+ // round-trip through void* to silence 'cast increases required alignment of target type' warnings
+ while (size >= 4 && (*static_cast<const uint32_t*>(static_cast<const void*>(data)) & 0x80808080) == 0)
+ {
+ result = Traits::low(result, data[0]);
+ result = Traits::low(result, data[1]);
+ result = Traits::low(result, data[2]);
+ result = Traits::low(result, data[3]);
+ data += 4;
+ size -= 4;
+ }
+ }
+ }
+ // 110xxxxx -> U+0080..U+07FF
+ else if (static_cast<unsigned int>(lead - 0xC0) < 0x20 && size >= 2 && (data[1] & 0xc0) == 0x80)
+ {
+ result = Traits::low(result, ((lead & ~0xC0) << 6) | (data[1] & utf8_byte_mask));
+ data += 2;
+ size -= 2;
+ }
+ // 1110xxxx -> U+0800-U+FFFF
+ else if (static_cast<unsigned int>(lead - 0xE0) < 0x10 && size >= 3 && (data[1] & 0xc0) == 0x80 && (data[2] & 0xc0) == 0x80)
+ {
+ result = Traits::low(result, ((lead & ~0xE0) << 12) | ((data[1] & utf8_byte_mask) << 6) | (data[2] & utf8_byte_mask));
+ data += 3;
+ size -= 3;
+ }
+ // 11110xxx -> U+10000..U+10FFFF
+ else if (static_cast<unsigned int>(lead - 0xF0) < 0x08 && size >= 4 && (data[1] & 0xc0) == 0x80 && (data[2] & 0xc0) == 0x80 && (data[3] & 0xc0) == 0x80)
+ {
+ result = Traits::high(result, ((lead & ~0xF0) << 18) | ((data[1] & utf8_byte_mask) << 12) | ((data[2] & utf8_byte_mask) << 6) | (data[3] & utf8_byte_mask));
+ data += 4;
+ size -= 4;
+ }
+ // 10xxxxxx or 11111xxx -> invalid
+ else
+ {
+ data += 1;
+ size -= 1;
+ }
+ }
+
+ return result;
+ }
+ };
+
+ template <typename opt_swap> struct utf16_decoder
+ {
+ typedef uint16_t type;
+
+ template <typename Traits> static inline typename Traits::value_type process(const uint16_t* data, size_t size, typename Traits::value_type result, Traits)
+ {
+ while (size)
+ {
+ uint16_t lead = opt_swap::value ? endian_swap(*data) : *data;
+
+ // U+0000..U+D7FF
+ if (lead < 0xD800)
+ {
+ result = Traits::low(result, lead);
+ data += 1;
+ size -= 1;
+ }
+ // U+E000..U+FFFF
+ else if (static_cast<unsigned int>(lead - 0xE000) < 0x2000)
+ {
+ result = Traits::low(result, lead);
+ data += 1;
+ size -= 1;
+ }
+ // surrogate pair lead
+ else if (static_cast<unsigned int>(lead - 0xD800) < 0x400 && size >= 2)
+ {
+ uint16_t next = opt_swap::value ? endian_swap(data[1]) : data[1];
+
+ if (static_cast<unsigned int>(next - 0xDC00) < 0x400)
+ {
+ result = Traits::high(result, 0x10000 + ((lead & 0x3ff) << 10) + (next & 0x3ff));
+ data += 2;
+ size -= 2;
+ }
+ else
+ {
+ data += 1;
+ size -= 1;
+ }
+ }
+ else
+ {
+ data += 1;
+ size -= 1;
+ }
+ }
+
+ return result;
+ }
+ };
+
+ template <typename opt_swap> struct utf32_decoder
+ {
+ typedef uint32_t type;
+
+ template <typename Traits> static inline typename Traits::value_type process(const uint32_t* data, size_t size, typename Traits::value_type result, Traits)
+ {
+ while (size)
+ {
+ uint32_t lead = opt_swap::value ? endian_swap(*data) : *data;
+
+ // U+0000..U+FFFF
+ if (lead < 0x10000)
+ {
+ result = Traits::low(result, lead);
+ data += 1;
+ size -= 1;
+ }
+ // U+10000..U+10FFFF
+ else
+ {
+ result = Traits::high(result, lead);
+ data += 1;
+ size -= 1;
+ }
+ }
+
+ return result;
+ }
+ };
+
+ struct latin1_decoder
+ {
+ typedef uint8_t type;
+
+ template <typename Traits> static inline typename Traits::value_type process(const uint8_t* data, size_t size, typename Traits::value_type result, Traits)
+ {
+ while (size)
+ {
+ result = Traits::low(result, *data);
+ data += 1;
+ size -= 1;
+ }
+
+ return result;
+ }
+ };
+
+ template <size_t size> struct wchar_selector;
+
+ template <> struct wchar_selector<2>
+ {
+ typedef uint16_t type;
+ typedef utf16_counter counter;
+ typedef utf16_writer writer;
+ typedef utf16_decoder<opt_false> decoder;
+ };
+
+ template <> struct wchar_selector<4>
+ {
+ typedef uint32_t type;
+ typedef utf32_counter counter;
+ typedef utf32_writer writer;
+ typedef utf32_decoder<opt_false> decoder;
+ };
+
+ typedef wchar_selector<sizeof(wchar_t)>::counter wchar_counter;
+ typedef wchar_selector<sizeof(wchar_t)>::writer wchar_writer;
+
+ struct wchar_decoder
+ {
+ typedef wchar_t type;
+
+ template <typename Traits> static inline typename Traits::value_type process(const wchar_t* data, size_t size, typename Traits::value_type result, Traits traits)
+ {
+ typedef wchar_selector<sizeof(wchar_t)>::decoder decoder;
+
+ return decoder::process(reinterpret_cast<const typename decoder::type*>(data), size, result, traits);
+ }
+ };
+
+#ifdef PUGIXML_WCHAR_MODE
+ PUGI__FN void convert_wchar_endian_swap(wchar_t* result, const wchar_t* data, size_t length)
+ {
+ for (size_t i = 0; i < length; ++i)
+ result[i] = static_cast<wchar_t>(endian_swap(static_cast<wchar_selector<sizeof(wchar_t)>::type>(data[i])));
+ }
+#endif
+PUGI__NS_END
+
+PUGI__NS_BEGIN
+ enum chartype_t
+ {
+ ct_parse_pcdata = 1, // \0, &, \r, <
+ ct_parse_attr = 2, // \0, &, \r, ', "
+ ct_parse_attr_ws = 4, // \0, &, \r, ', ", \n, tab
+ ct_space = 8, // \r, \n, space, tab
+ ct_parse_cdata = 16, // \0, ], >, \r
+ ct_parse_comment = 32, // \0, -, >, \r
+ ct_symbol = 64, // Any symbol > 127, a-z, A-Z, 0-9, _, :, -, .
+ ct_start_symbol = 128 // Any symbol > 127, a-z, A-Z, _, :
+ };
+
+ static const unsigned char chartype_table[256] =
+ {
+ 55, 0, 0, 0, 0, 0, 0, 0, 0, 12, 12, 0, 0, 63, 0, 0, // 0-15
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16-31
+ 8, 0, 6, 0, 0, 0, 7, 6, 0, 0, 0, 0, 0, 96, 64, 0, // 32-47
+ 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 192, 0, 1, 0, 48, 0, // 48-63
+ 0, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, // 64-79
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 0, 0, 16, 0, 192, // 80-95
+ 0, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, // 96-111
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 0, 0, 0, 0, 0, // 112-127
+
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, // 128+
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192,
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192,
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192,
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192,
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192,
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192,
+ 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192, 192
+ };
+
+ enum chartypex_t
+ {
+ ctx_special_pcdata = 1, // Any symbol >= 0 and < 32 (except \t, \r, \n), &, <, >
+ ctx_special_attr = 2, // Any symbol >= 0 and < 32, &, <, ", '
+ ctx_start_symbol = 4, // Any symbol > 127, a-z, A-Z, _
+ ctx_digit = 8, // 0-9
+ ctx_symbol = 16 // Any symbol > 127, a-z, A-Z, 0-9, _, -, .
+ };
+
+ static const unsigned char chartypex_table[256] =
+ {
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 2, 3, 3, 2, 3, 3, // 0-15
+ 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, // 16-31
+ 0, 0, 2, 0, 0, 0, 3, 2, 0, 0, 0, 0, 0, 16, 16, 0, // 32-47
+ 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 0, 0, 3, 0, 1, 0, // 48-63
+
+ 0, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // 64-79
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 20, // 80-95
+ 0, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // 96-111
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 0, 0, 0, 0, 0, // 112-127
+
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, // 128+
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20,
+ 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20, 20
+ };
+
+#ifdef PUGIXML_WCHAR_MODE
+ #define PUGI__IS_CHARTYPE_IMPL(c, ct, table) ((static_cast<unsigned int>(c) < 128 ? table[static_cast<unsigned int>(c)] : table[128]) & (ct))
+#else
+ #define PUGI__IS_CHARTYPE_IMPL(c, ct, table) (table[static_cast<unsigned char>(c)] & (ct))
+#endif
+
+ #define PUGI__IS_CHARTYPE(c, ct) PUGI__IS_CHARTYPE_IMPL(c, ct, chartype_table)
+ #define PUGI__IS_CHARTYPEX(c, ct) PUGI__IS_CHARTYPE_IMPL(c, ct, chartypex_table)
+
+ PUGI__FN bool is_little_endian()
+ {
+ unsigned int ui = 1;
+
+ return *reinterpret_cast<unsigned char*>(&ui) == 1;
+ }
+
+ PUGI__FN xml_encoding get_wchar_encoding()
+ {
+ PUGI__STATIC_ASSERT(sizeof(wchar_t) == 2 || sizeof(wchar_t) == 4);
+
+ if (sizeof(wchar_t) == 2)
+ return is_little_endian() ? encoding_utf16_le : encoding_utf16_be;
+ else
+ return is_little_endian() ? encoding_utf32_le : encoding_utf32_be;
+ }
+
+ PUGI__FN bool parse_declaration_encoding(const uint8_t* data, size_t size, const uint8_t*& out_encoding, size_t& out_length)
+ {
+ #define PUGI__SCANCHAR(ch) { if (offset >= size || data[offset] != ch) return false; offset++; }
+ #define PUGI__SCANCHARTYPE(ct) { while (offset < size && PUGI__IS_CHARTYPE(data[offset], ct)) offset++; }
+
+ // check if we have a non-empty XML declaration
+ if (size < 6 || !((data[0] == '<') & (data[1] == '?') & (data[2] == 'x') & (data[3] == 'm') & (data[4] == 'l') && PUGI__IS_CHARTYPE(data[5], ct_space)))
+ return false;
+
+ // scan XML declaration until the encoding field
+ for (size_t i = 6; i + 1 < size; ++i)
+ {
+ // declaration can not contain ? in quoted values
+ if (data[i] == '?')
+ return false;
+
+ if (data[i] == 'e' && data[i + 1] == 'n')
+ {
+ size_t offset = i;
+
+ // encoding follows the version field which can't contain 'en' so this has to be the encoding if XML is well formed
+ PUGI__SCANCHAR('e'); PUGI__SCANCHAR('n'); PUGI__SCANCHAR('c'); PUGI__SCANCHAR('o');
+ PUGI__SCANCHAR('d'); PUGI__SCANCHAR('i'); PUGI__SCANCHAR('n'); PUGI__SCANCHAR('g');
+
+ // S? = S?
+ PUGI__SCANCHARTYPE(ct_space);
+ PUGI__SCANCHAR('=');
+ PUGI__SCANCHARTYPE(ct_space);
+
+ // the only two valid delimiters are ' and "
+ uint8_t delimiter = (offset < size && data[offset] == '"') ? '"' : '\'';
+
+ PUGI__SCANCHAR(delimiter);
+
+ size_t start = offset;
+
+ out_encoding = data + offset;
+
+ PUGI__SCANCHARTYPE(ct_symbol);
+
+ out_length = offset - start;
+
+ PUGI__SCANCHAR(delimiter);
+
+ return true;
+ }
+ }
+
+ return false;
+
+ #undef PUGI__SCANCHAR
+ #undef PUGI__SCANCHARTYPE
+ }
+
+ PUGI__FN xml_encoding guess_buffer_encoding(const uint8_t* data, size_t size)
+ {
+ // skip encoding autodetection if input buffer is too small
+ if (size < 4) return encoding_utf8;
+
+ uint8_t d0 = data[0], d1 = data[1], d2 = data[2], d3 = data[3];
+
+ // look for BOM in first few bytes
+ if (d0 == 0 && d1 == 0 && d2 == 0xfe && d3 == 0xff) return encoding_utf32_be;
+ if (d0 == 0xff && d1 == 0xfe && d2 == 0 && d3 == 0) return encoding_utf32_le;
+ if (d0 == 0xfe && d1 == 0xff) return encoding_utf16_be;
+ if (d0 == 0xff && d1 == 0xfe) return encoding_utf16_le;
+ if (d0 == 0xef && d1 == 0xbb && d2 == 0xbf) return encoding_utf8;
+
+ // look for <, <? or <?xm in various encodings
+ if (d0 == 0 && d1 == 0 && d2 == 0 && d3 == 0x3c) return encoding_utf32_be;
+ if (d0 == 0x3c && d1 == 0 && d2 == 0 && d3 == 0) return encoding_utf32_le;
+ if (d0 == 0 && d1 == 0x3c && d2 == 0 && d3 == 0x3f) return encoding_utf16_be;
+ if (d0 == 0x3c && d1 == 0 && d2 == 0x3f && d3 == 0) return encoding_utf16_le;
+
+ // look for utf16 < followed by node name (this may fail, but is better than utf8 since it's zero terminated so early)
+ if (d0 == 0 && d1 == 0x3c) return encoding_utf16_be;
+ if (d0 == 0x3c && d1 == 0) return encoding_utf16_le;
+
+ // no known BOM detected; parse declaration
+ const uint8_t* enc = 0;
+ size_t enc_length = 0;
+
+ if (d0 == 0x3c && d1 == 0x3f && d2 == 0x78 && d3 == 0x6d && parse_declaration_encoding(data, size, enc, enc_length))
+ {
+ // iso-8859-1 (case-insensitive)
+ if (enc_length == 10
+ && (enc[0] | ' ') == 'i' && (enc[1] | ' ') == 's' && (enc[2] | ' ') == 'o'
+ && enc[3] == '-' && enc[4] == '8' && enc[5] == '8' && enc[6] == '5' && enc[7] == '9'
+ && enc[8] == '-' && enc[9] == '1')
+ return encoding_latin1;
+
+ // latin1 (case-insensitive)
+ if (enc_length == 6
+ && (enc[0] | ' ') == 'l' && (enc[1] | ' ') == 'a' && (enc[2] | ' ') == 't'
+ && (enc[3] | ' ') == 'i' && (enc[4] | ' ') == 'n'
+ && enc[5] == '1')
+ return encoding_latin1;
+ }
+
+ return encoding_utf8;
+ }
+
+ PUGI__FN xml_encoding get_buffer_encoding(xml_encoding encoding, const void* contents, size_t size)
+ {
+ // replace wchar encoding with utf implementation
+ if (encoding == encoding_wchar) return get_wchar_encoding();
+
+ // replace utf16 encoding with utf16 with specific endianness
+ if (encoding == encoding_utf16) return is_little_endian() ? encoding_utf16_le : encoding_utf16_be;
+
+ // replace utf32 encoding with utf32 with specific endianness
+ if (encoding == encoding_utf32) return is_little_endian() ? encoding_utf32_le : encoding_utf32_be;
+
+ // only do autodetection if no explicit encoding is requested
+ if (encoding != encoding_auto) return encoding;
+
+ // try to guess encoding (based on XML specification, Appendix F.1)
+ const uint8_t* data = static_cast<const uint8_t*>(contents);
+
+ return guess_buffer_encoding(data, size);
+ }
+
+ PUGI__FN bool get_mutable_buffer(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, bool is_mutable)
+ {
+ size_t length = size / sizeof(char_t);
+
+ if (is_mutable)
+ {
+ out_buffer = static_cast<char_t*>(const_cast<void*>(contents));
+ out_length = length;
+ }
+ else
+ {
+ char_t* buffer = static_cast<char_t*>(xml_memory::allocate((length + 1) * sizeof(char_t)));
+ if (!buffer) return false;
+
+ if (contents)
+ memcpy(buffer, contents, length * sizeof(char_t));
+ else
+ assert(length == 0);
+
+ buffer[length] = 0;
+
+ out_buffer = buffer;
+ out_length = length + 1;
+ }
+
+ return true;
+ }
+
+#ifdef PUGIXML_WCHAR_MODE
+ PUGI__FN bool need_endian_swap_utf(xml_encoding le, xml_encoding re)
+ {
+ return (le == encoding_utf16_be && re == encoding_utf16_le) || (le == encoding_utf16_le && re == encoding_utf16_be) ||
+ (le == encoding_utf32_be && re == encoding_utf32_le) || (le == encoding_utf32_le && re == encoding_utf32_be);
+ }
+
+ PUGI__FN bool convert_buffer_endian_swap(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, bool is_mutable)
+ {
+ const char_t* data = static_cast<const char_t*>(contents);
+ size_t length = size / sizeof(char_t);
+
+ if (is_mutable)
+ {
+ char_t* buffer = const_cast<char_t*>(data);
+
+ convert_wchar_endian_swap(buffer, data, length);
+
+ out_buffer = buffer;
+ out_length = length;
+ }
+ else
+ {
+ char_t* buffer = static_cast<char_t*>(xml_memory::allocate((length + 1) * sizeof(char_t)));
+ if (!buffer) return false;
+
+ convert_wchar_endian_swap(buffer, data, length);
+ buffer[length] = 0;
+
+ out_buffer = buffer;
+ out_length = length + 1;
+ }
+
+ return true;
+ }
+
+ template <typename D> PUGI__FN bool convert_buffer_generic(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, D)
+ {
+ const typename D::type* data = static_cast<const typename D::type*>(contents);
+ size_t data_length = size / sizeof(typename D::type);
+
+ // first pass: get length in wchar_t units
+ size_t length = D::process(data, data_length, 0, wchar_counter());
+
+ // allocate buffer of suitable length
+ char_t* buffer = static_cast<char_t*>(xml_memory::allocate((length + 1) * sizeof(char_t)));
+ if (!buffer) return false;
+
+ // second pass: convert utf16 input to wchar_t
+ wchar_writer::value_type obegin = reinterpret_cast<wchar_writer::value_type>(buffer);
+ wchar_writer::value_type oend = D::process(data, data_length, obegin, wchar_writer());
+
+ assert(oend == obegin + length);
+ *oend = 0;
+
+ out_buffer = buffer;
+ out_length = length + 1;
+
+ return true;
+ }
+
+ PUGI__FN bool convert_buffer(char_t*& out_buffer, size_t& out_length, xml_encoding encoding, const void* contents, size_t size, bool is_mutable)
+ {
+ // get native encoding
+ xml_encoding wchar_encoding = get_wchar_encoding();
+
+ // fast path: no conversion required
+ if (encoding == wchar_encoding)
+ return get_mutable_buffer(out_buffer, out_length, contents, size, is_mutable);
+
+ // only endian-swapping is required
+ if (need_endian_swap_utf(encoding, wchar_encoding))
+ return convert_buffer_endian_swap(out_buffer, out_length, contents, size, is_mutable);
+
+ // source encoding is utf8
+ if (encoding == encoding_utf8)
+ return convert_buffer_generic(out_buffer, out_length, contents, size, utf8_decoder());
+
+ // source encoding is utf16
+ if (encoding == encoding_utf16_be || encoding == encoding_utf16_le)
+ {
+ xml_encoding native_encoding = is_little_endian() ? encoding_utf16_le : encoding_utf16_be;
+
+ return (native_encoding == encoding) ?
+ convert_buffer_generic(out_buffer, out_length, contents, size, utf16_decoder<opt_false>()) :
+ convert_buffer_generic(out_buffer, out_length, contents, size, utf16_decoder<opt_true>());
+ }
+
+ // source encoding is utf32
+ if (encoding == encoding_utf32_be || encoding == encoding_utf32_le)
+ {
+ xml_encoding native_encoding = is_little_endian() ? encoding_utf32_le : encoding_utf32_be;
+
+ return (native_encoding == encoding) ?
+ convert_buffer_generic(out_buffer, out_length, contents, size, utf32_decoder<opt_false>()) :
+ convert_buffer_generic(out_buffer, out_length, contents, size, utf32_decoder<opt_true>());
+ }
+
+ // source encoding is latin1
+ if (encoding == encoding_latin1)
+ return convert_buffer_generic(out_buffer, out_length, contents, size, latin1_decoder());
+
+ assert(false && "Invalid encoding"); // unreachable
+ return false;
+ }
+#else
+ template <typename D> PUGI__FN bool convert_buffer_generic(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, D)
+ {
+ const typename D::type* data = static_cast<const typename D::type*>(contents);
+ size_t data_length = size / sizeof(typename D::type);
+
+ // first pass: get length in utf8 units
+ size_t length = D::process(data, data_length, 0, utf8_counter());
+
+ // allocate buffer of suitable length
+ char_t* buffer = static_cast<char_t*>(xml_memory::allocate((length + 1) * sizeof(char_t)));
+ if (!buffer) return false;
+
+ // second pass: convert utf16 input to utf8
+ uint8_t* obegin = reinterpret_cast<uint8_t*>(buffer);
+ uint8_t* oend = D::process(data, data_length, obegin, utf8_writer());
+
+ assert(oend == obegin + length);
+ *oend = 0;
+
+ out_buffer = buffer;
+ out_length = length + 1;
+
+ return true;
+ }
+
+ PUGI__FN size_t get_latin1_7bit_prefix_length(const uint8_t* data, size_t size)
+ {
+ for (size_t i = 0; i < size; ++i)
+ if (data[i] > 127)
+ return i;
+
+ return size;
+ }
+
+ PUGI__FN bool convert_buffer_latin1(char_t*& out_buffer, size_t& out_length, const void* contents, size_t size, bool is_mutable)
+ {
+ const uint8_t* data = static_cast<const uint8_t*>(contents);
+ size_t data_length = size;
+
+ // get size of prefix that does not need utf8 conversion
+ size_t prefix_length = get_latin1_7bit_prefix_length(data, data_length);
+ assert(prefix_length <= data_length);
+
+ const uint8_t* postfix = data + prefix_length;
+ size_t postfix_length = data_length - prefix_length;
+
+ // if no conversion is needed, just return the original buffer
+ if (postfix_length == 0) return get_mutable_buffer(out_buffer, out_length, contents, size, is_mutable);
+
+ // first pass: get length in utf8 units
+ size_t length = prefix_length + latin1_decoder::process(postfix, postfix_length, 0, utf8_counter());
+
+ // allocate buffer of suitable length
+ char_t* buffer = static_cast<char_t*>(xml_memory::allocate((length + 1) * sizeof(char_t)));
+ if (!buffer) return false;
+
+ // second pass: convert latin1 input to utf8
+ memcpy(buffer, data, prefix_length);
+
+ uint8_t* obegin = reinterpret_cast<uint8_t*>(buffer);
+ uint8_t* oend = latin1_decoder::process(postfix, postfix_length, obegin + prefix_length, utf8_writer());
+
+ assert(oend == obegin + length);
+ *oend = 0;
+
+ out_buffer = buffer;
+ out_length = length + 1;
+
+ return true;
+ }
+
+ PUGI__FN bool convert_buffer(char_t*& out_buffer, size_t& out_length, xml_encoding encoding, const void* contents, size_t size, bool is_mutable)
+ {
+ // fast path: no conversion required
+ if (encoding == encoding_utf8)
+ return get_mutable_buffer(out_buffer, out_length, contents, size, is_mutable);
+
+ // source encoding is utf16
+ if (encoding == encoding_utf16_be || encoding == encoding_utf16_le)
+ {
+ xml_encoding native_encoding = is_little_endian() ? encoding_utf16_le : encoding_utf16_be;
+
+ return (native_encoding == encoding) ?
+ convert_buffer_generic(out_buffer, out_length, contents, size, utf16_decoder<opt_false>()) :
+ convert_buffer_generic(out_buffer, out_length, contents, size, utf16_decoder<opt_true>());
+ }
+
+ // source encoding is utf32
+ if (encoding == encoding_utf32_be || encoding == encoding_utf32_le)
+ {
+ xml_encoding native_encoding = is_little_endian() ? encoding_utf32_le : encoding_utf32_be;
+
+ return (native_encoding == encoding) ?
+ convert_buffer_generic(out_buffer, out_length, contents, size, utf32_decoder<opt_false>()) :
+ convert_buffer_generic(out_buffer, out_length, contents, size, utf32_decoder<opt_true>());
+ }
+
+ // source encoding is latin1
+ if (encoding == encoding_latin1)
+ return convert_buffer_latin1(out_buffer, out_length, contents, size, is_mutable);
+
+ assert(false && "Invalid encoding"); // unreachable
+ return false;
+ }
+#endif
+
+ PUGI__FN size_t as_utf8_begin(const wchar_t* str, size_t length)
+ {
+ // get length in utf8 characters
+ return wchar_decoder::process(str, length, 0, utf8_counter());
+ }
+
+ PUGI__FN void as_utf8_end(char* buffer, size_t size, const wchar_t* str, size_t length)
+ {
+ // convert to utf8
+ uint8_t* begin = reinterpret_cast<uint8_t*>(buffer);
+ uint8_t* end = wchar_decoder::process(str, length, begin, utf8_writer());
+
+ assert(begin + size == end);
+ (void)!end;
+ (void)!size;
+ }
+
+#ifndef PUGIXML_NO_STL
+ PUGI__FN std::string as_utf8_impl(const wchar_t* str, size_t length)
+ {
+ // first pass: get length in utf8 characters
+ size_t size = as_utf8_begin(str, length);
+
+ // allocate resulting string
+ std::string result;
+ result.resize(size);
+
+ // second pass: convert to utf8
+ if (size > 0) as_utf8_end(&result[0], size, str, length);
+
+ return result;
+ }
+
+ PUGI__FN std::basic_string<wchar_t> as_wide_impl(const char* str, size_t size)
+ {
+ const uint8_t* data = reinterpret_cast<const uint8_t*>(str);
+
+ // first pass: get length in wchar_t units
+ size_t length = utf8_decoder::process(data, size, 0, wchar_counter());
+
+ // allocate resulting string
+ std::basic_string<wchar_t> result;
+ result.resize(length);
+
+ // second pass: convert to wchar_t
+ if (length > 0)
+ {
+ wchar_writer::value_type begin = reinterpret_cast<wchar_writer::value_type>(&result[0]);
+ wchar_writer::value_type end = utf8_decoder::process(data, size, begin, wchar_writer());
+
+ assert(begin + length == end);
+ (void)!end;
+ }
+
+ return result;
+ }
+#endif
+
+ template <typename Header>
+ inline bool strcpy_insitu_allow(size_t length, const Header& header, uintptr_t header_mask, char_t* target)
+ {
+ // never reuse shared memory
+ if (header & xml_memory_page_contents_shared_mask) return false;
+
+ size_t target_length = strlength(target);
+
+ // always reuse document buffer memory if possible
+ if ((header & header_mask) == 0) return target_length >= length;
+
+ // reuse heap memory if waste is not too great
+ const size_t reuse_threshold = 32;
+
+ return target_length >= length && (target_length < reuse_threshold || target_length - length < target_length / 2);
+ }
+
+ template <typename String, typename Header>
+ PUGI__FN bool strcpy_insitu(String& dest, Header& header, uintptr_t header_mask, const char_t* source, size_t source_length)
+ {
+ if (source_length == 0)
+ {
+ // empty string and null pointer are equivalent, so just deallocate old memory
+ xml_allocator* alloc = PUGI__GETPAGE_IMPL(header)->allocator;
+
+ if (header & header_mask) alloc->deallocate_string(dest);
+
+ // mark the string as not allocated
+ dest = 0;
+ header &= ~header_mask;
+
+ return true;
+ }
+ else if (dest && strcpy_insitu_allow(source_length, header, header_mask, dest))
+ {
+ // we can reuse old buffer, so just copy the new data (including zero terminator)
+ memcpy(dest, source, source_length * sizeof(char_t));
+ dest[source_length] = 0;
+
+ return true;
+ }
+ else
+ {
+ xml_allocator* alloc = PUGI__GETPAGE_IMPL(header)->allocator;
+
+ if (!alloc->reserve()) return false;
+
+ // allocate new buffer
+ char_t* buf = alloc->allocate_string(source_length + 1);
+ if (!buf) return false;
+
+ // copy the string (including zero terminator)
+ memcpy(buf, source, source_length * sizeof(char_t));
+ buf[source_length] = 0;
+
+ // deallocate old buffer (*after* the above to protect against overlapping memory and/or allocation failures)
+ if (header & header_mask) alloc->deallocate_string(dest);
+
+ // the string is now allocated, so set the flag
+ dest = buf;
+ header |= header_mask;
+
+ return true;
+ }
+ }
+
+ struct gap
+ {
+ char_t* end;
+ size_t size;
+
+ gap(): end(0), size(0)
+ {
+ }
+
+ // Push new gap, move s count bytes further (skipping the gap).
+ // Collapse previous gap.
+ void push(char_t*& s, size_t count)
+ {
+ if (end) // there was a gap already; collapse it
+ {
+ // Move [old_gap_end, new_gap_start) to [old_gap_start, ...)
+ assert(s >= end);
+ memmove(end - size, end, reinterpret_cast<char*>(s) - reinterpret_cast<char*>(end));
+ }
+
+ s += count; // end of current gap
+
+ // "merge" two gaps
+ end = s;
+ size += count;
+ }
+
+ // Collapse all gaps, return past-the-end pointer
+ char_t* flush(char_t* s)
+ {
+ if (end)
+ {
+ // Move [old_gap_end, current_pos) to [old_gap_start, ...)
+ assert(s >= end);
+ memmove(end - size, end, reinterpret_cast<char*>(s) - reinterpret_cast<char*>(end));
+
+ return s - size;
+ }
+ else return s;
+ }
+ };
+
+ PUGI__FN char_t* strconv_escape(char_t* s, gap& g)
+ {
+ char_t* stre = s + 1;
+
+ switch (*stre)
+ {
+ case '#': // &#...
+ {
+ unsigned int ucsc = 0;
+
+ if (stre[1] == 'x') // &#x... (hex code)
+ {
+ stre += 2;
+
+ char_t ch = *stre;
+
+ if (ch == ';') return stre;
+
+ for (;;)
+ {
+ if (static_cast<unsigned int>(ch - '0') <= 9)
+ ucsc = 16 * ucsc + (ch - '0');
+ else if (static_cast<unsigned int>((ch | ' ') - 'a') <= 5)
+ ucsc = 16 * ucsc + ((ch | ' ') - 'a' + 10);
+ else if (ch == ';')
+ break;
+ else // cancel
+ return stre;
+
+ ch = *++stre;
+ }
+
+ ++stre;
+ }
+ else // &#... (dec code)
+ {
+ char_t ch = *++stre;
+
+ if (ch == ';') return stre;
+
+ for (;;)
+ {
+ if (static_cast<unsigned int>(ch - '0') <= 9)
+ ucsc = 10 * ucsc + (ch - '0');
+ else if (ch == ';')
+ break;
+ else // cancel
+ return stre;
+
+ ch = *++stre;
+ }
+
+ ++stre;
+ }
+
+ #ifdef PUGIXML_WCHAR_MODE
+ s = reinterpret_cast<char_t*>(wchar_writer::any(reinterpret_cast<wchar_writer::value_type>(s), ucsc));
+ #else
+ s = reinterpret_cast<char_t*>(utf8_writer::any(reinterpret_cast<uint8_t*>(s), ucsc));
+ #endif
+
+ g.push(s, stre - s);
+ return stre;
+ }
+
+ case 'a': // &a
+ {
+ ++stre;
+
+ if (*stre == 'm') // &am
+ {
+ if (*++stre == 'p' && *++stre == ';') // &amp;
+ {
+ *s++ = '&';
+ ++stre;
+
+ g.push(s, stre - s);
+ return stre;
+ }
+ }
+ else if (*stre == 'p') // &ap
+ {
+ if (*++stre == 'o' && *++stre == 's' && *++stre == ';') // &apos;
+ {
+ *s++ = '\'';
+ ++stre;
+
+ g.push(s, stre - s);
+ return stre;
+ }
+ }
+ break;
+ }
+
+ case 'g': // &g
+ {
+ if (*++stre == 't' && *++stre == ';') // &gt;
+ {
+ *s++ = '>';
+ ++stre;
+
+ g.push(s, stre - s);
+ return stre;
+ }
+ break;
+ }
+
+ case 'l': // &l
+ {
+ if (*++stre == 't' && *++stre == ';') // &lt;
+ {
+ *s++ = '<';
+ ++stre;
+
+ g.push(s, stre - s);
+ return stre;
+ }
+ break;
+ }
+
+ case 'q': // &q
+ {
+ if (*++stre == 'u' && *++stre == 'o' && *++stre == 't' && *++stre == ';') // &quot;
+ {
+ *s++ = '"';
+ ++stre;
+
+ g.push(s, stre - s);
+ return stre;
+ }
+ break;
+ }
+
+ default:
+ break;
+ }
+
+ return stre;
+ }
+
+ // Parser utilities
+ #define PUGI__ENDSWITH(c, e) ((c) == (e) || ((c) == 0 && endch == (e)))
+ #define PUGI__SKIPWS() { while (PUGI__IS_CHARTYPE(*s, ct_space)) ++s; }
+ #define PUGI__OPTSET(OPT) ( optmsk & (OPT) )
+ #define PUGI__PUSHNODE(TYPE) { cursor = append_new_node(cursor, *alloc, TYPE); if (!cursor) PUGI__THROW_ERROR(status_out_of_memory, s); }
+ #define PUGI__POPNODE() { cursor = cursor->parent; }
+ #define PUGI__SCANFOR(X) { while (*s != 0 && !(X)) ++s; }
+ #define PUGI__SCANWHILE(X) { while (X) ++s; }
+ #define PUGI__SCANWHILE_UNROLL(X) { for (;;) { char_t ss = s[0]; if (PUGI__UNLIKELY(!(X))) { break; } ss = s[1]; if (PUGI__UNLIKELY(!(X))) { s += 1; break; } ss = s[2]; if (PUGI__UNLIKELY(!(X))) { s += 2; break; } ss = s[3]; if (PUGI__UNLIKELY(!(X))) { s += 3; break; } s += 4; } }
+ #define PUGI__ENDSEG() { ch = *s; *s = 0; ++s; }
+ #define PUGI__THROW_ERROR(err, m) return error_offset = m, error_status = err, static_cast<char_t*>(0)
+ #define PUGI__CHECK_ERROR(err, m) { if (*s == 0) PUGI__THROW_ERROR(err, m); }
+
+ PUGI__FN char_t* strconv_comment(char_t* s, char_t endch)
+ {
+ gap g;
+
+ while (true)
+ {
+ PUGI__SCANWHILE_UNROLL(!PUGI__IS_CHARTYPE(ss, ct_parse_comment));
+
+ if (*s == '\r') // Either a single 0x0d or 0x0d 0x0a pair
+ {
+ *s++ = '\n'; // replace first one with 0x0a
+
+ if (*s == '\n') g.push(s, 1);
+ }
+ else if (s[0] == '-' && s[1] == '-' && PUGI__ENDSWITH(s[2], '>')) // comment ends here
+ {
+ *g.flush(s) = 0;
+
+ return s + (s[2] == '>' ? 3 : 2);
+ }
+ else if (*s == 0)
+ {
+ return 0;
+ }
+ else ++s;
+ }
+ }
+
+ PUGI__FN char_t* strconv_cdata(char_t* s, char_t endch)
+ {
+ gap g;
+
+ while (true)
+ {
+ PUGI__SCANWHILE_UNROLL(!PUGI__IS_CHARTYPE(ss, ct_parse_cdata));
+
+ if (*s == '\r') // Either a single 0x0d or 0x0d 0x0a pair
+ {
+ *s++ = '\n'; // replace first one with 0x0a
+
+ if (*s == '\n') g.push(s, 1);
+ }
+ else if (s[0] == ']' && s[1] == ']' && PUGI__ENDSWITH(s[2], '>')) // CDATA ends here
+ {
+ *g.flush(s) = 0;
+
+ return s + 1;
+ }
+ else if (*s == 0)
+ {
+ return 0;
+ }
+ else ++s;
+ }
+ }
+
+ typedef char_t* (*strconv_pcdata_t)(char_t*);
+
+ template <typename opt_trim, typename opt_eol, typename opt_escape> struct strconv_pcdata_impl
+ {
+ static char_t* parse(char_t* s)
+ {
+ gap g;
+
+ char_t* begin = s;
+
+ while (true)
+ {
+ PUGI__SCANWHILE_UNROLL(!PUGI__IS_CHARTYPE(ss, ct_parse_pcdata));
+
+ if (*s == '<') // PCDATA ends here
+ {
+ char_t* end = g.flush(s);
+
+ if (opt_trim::value)
+ while (end > begin && PUGI__IS_CHARTYPE(end[-1], ct_space))
+ --end;
+
+ *end = 0;
+
+ return s + 1;
+ }
+ else if (opt_eol::value && *s == '\r') // Either a single 0x0d or 0x0d 0x0a pair
+ {
+ *s++ = '\n'; // replace first one with 0x0a
+
+ if (*s == '\n') g.push(s, 1);
+ }
+ else if (opt_escape::value && *s == '&')
+ {
+ s = strconv_escape(s, g);
+ }
+ else if (*s == 0)
+ {
+ char_t* end = g.flush(s);
+
+ if (opt_trim::value)
+ while (end > begin && PUGI__IS_CHARTYPE(end[-1], ct_space))
+ --end;
+
+ *end = 0;
+
+ return s;
+ }
+ else ++s;
+ }
+ }
+ };
+
+ PUGI__FN strconv_pcdata_t get_strconv_pcdata(unsigned int optmask)
+ {
+ PUGI__STATIC_ASSERT(parse_escapes == 0x10 && parse_eol == 0x20 && parse_trim_pcdata == 0x0800);
+
+ switch (((optmask >> 4) & 3) | ((optmask >> 9) & 4)) // get bitmask for flags (trim eol escapes); this simultaneously checks 3 options from assertion above
+ {
+ case 0: return strconv_pcdata_impl<opt_false, opt_false, opt_false>::parse;
+ case 1: return strconv_pcdata_impl<opt_false, opt_false, opt_true>::parse;
+ case 2: return strconv_pcdata_impl<opt_false, opt_true, opt_false>::parse;
+ case 3: return strconv_pcdata_impl<opt_false, opt_true, opt_true>::parse;
+ case 4: return strconv_pcdata_impl<opt_true, opt_false, opt_false>::parse;
+ case 5: return strconv_pcdata_impl<opt_true, opt_false, opt_true>::parse;
+ case 6: return strconv_pcdata_impl<opt_true, opt_true, opt_false>::parse;
+ case 7: return strconv_pcdata_impl<opt_true, opt_true, opt_true>::parse;
+ default: assert(false); return 0; // unreachable
+ }
+ }
+
+ typedef char_t* (*strconv_attribute_t)(char_t*, char_t);
+
+ template <typename opt_escape> struct strconv_attribute_impl
+ {
+ static char_t* parse_wnorm(char_t* s, char_t end_quote)
+ {
+ gap g;
+
+ // trim leading whitespaces
+ if (PUGI__IS_CHARTYPE(*s, ct_space))
+ {
+ char_t* str = s;
+
+ do ++str;
+ while (PUGI__IS_CHARTYPE(*str, ct_space));
+
+ g.push(s, str - s);
+ }
+
+ while (true)
+ {
+ PUGI__SCANWHILE_UNROLL(!PUGI__IS_CHARTYPE(ss, ct_parse_attr_ws | ct_space));
+
+ if (*s == end_quote)
+ {
+ char_t* str = g.flush(s);
+
+ do *str-- = 0;
+ while (PUGI__IS_CHARTYPE(*str, ct_space));
+
+ return s + 1;
+ }
+ else if (PUGI__IS_CHARTYPE(*s, ct_space))
+ {
+ *s++ = ' ';
+
+ if (PUGI__IS_CHARTYPE(*s, ct_space))
+ {
+ char_t* str = s + 1;
+ while (PUGI__IS_CHARTYPE(*str, ct_space)) ++str;
+
+ g.push(s, str - s);
+ }
+ }
+ else if (opt_escape::value && *s == '&')
+ {
+ s = strconv_escape(s, g);
+ }
+ else if (!*s)
+ {
+ return 0;
+ }
+ else ++s;
+ }
+ }
+
+ static char_t* parse_wconv(char_t* s, char_t end_quote)
+ {
+ gap g;
+
+ while (true)
+ {
+ PUGI__SCANWHILE_UNROLL(!PUGI__IS_CHARTYPE(ss, ct_parse_attr_ws));
+
+ if (*s == end_quote)
+ {
+ *g.flush(s) = 0;
+
+ return s + 1;
+ }
+ else if (PUGI__IS_CHARTYPE(*s, ct_space))
+ {
+ if (*s == '\r')
+ {
+ *s++ = ' ';
+
+ if (*s == '\n') g.push(s, 1);
+ }
+ else *s++ = ' ';
+ }
+ else if (opt_escape::value && *s == '&')
+ {
+ s = strconv_escape(s, g);
+ }
+ else if (!*s)
+ {
+ return 0;
+ }
+ else ++s;
+ }
+ }
+
+ static char_t* parse_eol(char_t* s, char_t end_quote)
+ {
+ gap g;
+
+ while (true)
+ {
+ PUGI__SCANWHILE_UNROLL(!PUGI__IS_CHARTYPE(ss, ct_parse_attr));
+
+ if (*s == end_quote)
+ {
+ *g.flush(s) = 0;
+
+ return s + 1;
+ }
+ else if (*s == '\r')
+ {
+ *s++ = '\n';
+
+ if (*s == '\n') g.push(s, 1);
+ }
+ else if (opt_escape::value && *s == '&')
+ {
+ s = strconv_escape(s, g);
+ }
+ else if (!*s)
+ {
+ return 0;
+ }
+ else ++s;
+ }
+ }
+
+ static char_t* parse_simple(char_t* s, char_t end_quote)
+ {
+ gap g;
+
+ while (true)
+ {
+ PUGI__SCANWHILE_UNROLL(!PUGI__IS_CHARTYPE(ss, ct_parse_attr));
+
+ if (*s == end_quote)
+ {
+ *g.flush(s) = 0;
+
+ return s + 1;
+ }
+ else if (opt_escape::value && *s == '&')
+ {
+ s = strconv_escape(s, g);
+ }
+ else if (!*s)
+ {
+ return 0;
+ }
+ else ++s;
+ }
+ }
+ };
+
+ PUGI__FN strconv_attribute_t get_strconv_attribute(unsigned int optmask)
+ {
+ PUGI__STATIC_ASSERT(parse_escapes == 0x10 && parse_eol == 0x20 && parse_wconv_attribute == 0x40 && parse_wnorm_attribute == 0x80);
+
+ switch ((optmask >> 4) & 15) // get bitmask for flags (wnorm wconv eol escapes); this simultaneously checks 4 options from assertion above
+ {
+ case 0: return strconv_attribute_impl<opt_false>::parse_simple;
+ case 1: return strconv_attribute_impl<opt_true>::parse_simple;
+ case 2: return strconv_attribute_impl<opt_false>::parse_eol;
+ case 3: return strconv_attribute_impl<opt_true>::parse_eol;
+ case 4: return strconv_attribute_impl<opt_false>::parse_wconv;
+ case 5: return strconv_attribute_impl<opt_true>::parse_wconv;
+ case 6: return strconv_attribute_impl<opt_false>::parse_wconv;
+ case 7: return strconv_attribute_impl<opt_true>::parse_wconv;
+ case 8: return strconv_attribute_impl<opt_false>::parse_wnorm;
+ case 9: return strconv_attribute_impl<opt_true>::parse_wnorm;
+ case 10: return strconv_attribute_impl<opt_false>::parse_wnorm;
+ case 11: return strconv_attribute_impl<opt_true>::parse_wnorm;
+ case 12: return strconv_attribute_impl<opt_false>::parse_wnorm;
+ case 13: return strconv_attribute_impl<opt_true>::parse_wnorm;
+ case 14: return strconv_attribute_impl<opt_false>::parse_wnorm;
+ case 15: return strconv_attribute_impl<opt_true>::parse_wnorm;
+ default: assert(false); return 0; // unreachable
+ }
+ }
+
+ inline xml_parse_result make_parse_result(xml_parse_status status, ptrdiff_t offset = 0)
+ {
+ xml_parse_result result;
+ result.status = status;
+ result.offset = offset;
+
+ return result;
+ }
+
+ struct xml_parser
+ {
+ xml_allocator* alloc;
+ char_t* error_offset;
+ xml_parse_status error_status;
+
+ xml_parser(xml_allocator* alloc_): alloc(alloc_), error_offset(0), error_status(status_ok)
+ {
+ }
+
+ // DOCTYPE consists of nested sections of the following possible types:
+ // <!-- ... -->, <? ... ?>, "...", '...'
+ // <![...]]>
+ // <!...>
+ // First group can not contain nested groups
+ // Second group can contain nested groups of the same type
+ // Third group can contain all other groups
+ char_t* parse_doctype_primitive(char_t* s)
+ {
+ if (*s == '"' || *s == '\'')
+ {
+ // quoted string
+ char_t ch = *s++;
+ PUGI__SCANFOR(*s == ch);
+ if (!*s) PUGI__THROW_ERROR(status_bad_doctype, s);
+
+ s++;
+ }
+ else if (s[0] == '<' && s[1] == '?')
+ {
+ // <? ... ?>
+ s += 2;
+ PUGI__SCANFOR(s[0] == '?' && s[1] == '>'); // no need for ENDSWITH because ?> can't terminate proper doctype
+ if (!*s) PUGI__THROW_ERROR(status_bad_doctype, s);
+
+ s += 2;
+ }
+ else if (s[0] == '<' && s[1] == '!' && s[2] == '-' && s[3] == '-')
+ {
+ s += 4;
+ PUGI__SCANFOR(s[0] == '-' && s[1] == '-' && s[2] == '>'); // no need for ENDSWITH because --> can't terminate proper doctype
+ if (!*s) PUGI__THROW_ERROR(status_bad_doctype, s);
+
+ s += 3;
+ }
+ else PUGI__THROW_ERROR(status_bad_doctype, s);
+
+ return s;
+ }
+
+ char_t* parse_doctype_ignore(char_t* s)
+ {
+ size_t depth = 0;
+
+ assert(s[0] == '<' && s[1] == '!' && s[2] == '[');
+ s += 3;
+
+ while (*s)
+ {
+ if (s[0] == '<' && s[1] == '!' && s[2] == '[')
+ {
+ // nested ignore section
+ s += 3;
+ depth++;
+ }
+ else if (s[0] == ']' && s[1] == ']' && s[2] == '>')
+ {
+ // ignore section end
+ s += 3;
+
+ if (depth == 0)
+ return s;
+
+ depth--;
+ }
+ else s++;
+ }
+
+ PUGI__THROW_ERROR(status_bad_doctype, s);
+ }
+
+ char_t* parse_doctype_group(char_t* s, char_t endch)
+ {
+ size_t depth = 0;
+
+ assert((s[0] == '<' || s[0] == 0) && s[1] == '!');
+ s += 2;
+
+ while (*s)
+ {
+ if (s[0] == '<' && s[1] == '!' && s[2] != '-')
+ {
+ if (s[2] == '[')
+ {
+ // ignore
+ s = parse_doctype_ignore(s);
+ if (!s) return s;
+ }
+ else
+ {
+ // some control group
+ s += 2;
+ depth++;
+ }
+ }
+ else if (s[0] == '<' || s[0] == '"' || s[0] == '\'')
+ {
+ // unknown tag (forbidden), or some primitive group
+ s = parse_doctype_primitive(s);
+ if (!s) return s;
+ }
+ else if (*s == '>')
+ {
+ if (depth == 0)
+ return s;
+
+ depth--;
+ s++;
+ }
+ else s++;
+ }
+
+ if (depth != 0 || endch != '>') PUGI__THROW_ERROR(status_bad_doctype, s);
+
+ return s;
+ }
+
+ char_t* parse_exclamation(char_t* s, xml_node_struct* cursor, unsigned int optmsk, char_t endch)
+ {
+ // parse node contents, starting with exclamation mark
+ ++s;
+
+ if (*s == '-') // '<!-...'
+ {
+ ++s;
+
+ if (*s == '-') // '<!--...'
+ {
+ ++s;
+
+ if (PUGI__OPTSET(parse_comments))
+ {
+ PUGI__PUSHNODE(node_comment); // Append a new node on the tree.
+ cursor->value = s; // Save the offset.
+ }
+
+ if (PUGI__OPTSET(parse_eol) && PUGI__OPTSET(parse_comments))
+ {
+ s = strconv_comment(s, endch);
+
+ if (!s) PUGI__THROW_ERROR(status_bad_comment, cursor->value);
+ }
+ else
+ {
+ // Scan for terminating '-->'.
+ PUGI__SCANFOR(s[0] == '-' && s[1] == '-' && PUGI__ENDSWITH(s[2], '>'));
+ PUGI__CHECK_ERROR(status_bad_comment, s);
+
+ if (PUGI__OPTSET(parse_comments))
+ *s = 0; // Zero-terminate this segment at the first terminating '-'.
+
+ s += (s[2] == '>' ? 3 : 2); // Step over the '\0->'.
+ }
+ }
+ else PUGI__THROW_ERROR(status_bad_comment, s);
+ }
+ else if (*s == '[')
+ {
+ // '<![CDATA[...'
+ if (*++s=='C' && *++s=='D' && *++s=='A' && *++s=='T' && *++s=='A' && *++s == '[')
+ {
+ ++s;
+
+ if (PUGI__OPTSET(parse_cdata))
+ {
+ PUGI__PUSHNODE(node_cdata); // Append a new node on the tree.
+ cursor->value = s; // Save the offset.
+
+ if (PUGI__OPTSET(parse_eol))
+ {
+ s = strconv_cdata(s, endch);
+
+ if (!s) PUGI__THROW_ERROR(status_bad_cdata, cursor->value);
+ }
+ else
+ {
+ // Scan for terminating ']]>'.
+ PUGI__SCANFOR(s[0] == ']' && s[1] == ']' && PUGI__ENDSWITH(s[2], '>'));
+ PUGI__CHECK_ERROR(status_bad_cdata, s);
+
+ *s++ = 0; // Zero-terminate this segment.
+ }
+ }
+ else // Flagged for discard, but we still have to scan for the terminator.
+ {
+ // Scan for terminating ']]>'.
+ PUGI__SCANFOR(s[0] == ']' && s[1] == ']' && PUGI__ENDSWITH(s[2], '>'));
+ PUGI__CHECK_ERROR(status_bad_cdata, s);
+
+ ++s;
+ }
+
+ s += (s[1] == '>' ? 2 : 1); // Step over the last ']>'.
+ }
+ else PUGI__THROW_ERROR(status_bad_cdata, s);
+ }
+ else if (s[0] == 'D' && s[1] == 'O' && s[2] == 'C' && s[3] == 'T' && s[4] == 'Y' && s[5] == 'P' && PUGI__ENDSWITH(s[6], 'E'))
+ {
+ s -= 2;
+
+ if (cursor->parent) PUGI__THROW_ERROR(status_bad_doctype, s);
+
+ char_t* mark = s + 9;
+
+ s = parse_doctype_group(s, endch);
+ if (!s) return s;
+
+ assert((*s == 0 && endch == '>') || *s == '>');
+ if (*s) *s++ = 0;
+
+ if (PUGI__OPTSET(parse_doctype))
+ {
+ while (PUGI__IS_CHARTYPE(*mark, ct_space)) ++mark;
+
+ PUGI__PUSHNODE(node_doctype);
+
+ cursor->value = mark;
+ }
+ }
+ else if (*s == 0 && endch == '-') PUGI__THROW_ERROR(status_bad_comment, s);
+ else if (*s == 0 && endch == '[') PUGI__THROW_ERROR(status_bad_cdata, s);
+ else PUGI__THROW_ERROR(status_unrecognized_tag, s);
+
+ return s;
+ }
+
+ char_t* parse_question(char_t* s, xml_node_struct*& ref_cursor, unsigned int optmsk, char_t endch)
+ {
+ // load into registers
+ xml_node_struct* cursor = ref_cursor;
+ char_t ch = 0;
+
+ // parse node contents, starting with question mark
+ ++s;
+
+ // read PI target
+ char_t* target = s;
+
+ if (!PUGI__IS_CHARTYPE(*s, ct_start_symbol)) PUGI__THROW_ERROR(status_bad_pi, s);
+
+ PUGI__SCANWHILE(PUGI__IS_CHARTYPE(*s, ct_symbol));
+ PUGI__CHECK_ERROR(status_bad_pi, s);
+
+ // determine node type; stricmp / strcasecmp is not portable
+ bool declaration = (target[0] | ' ') == 'x' && (target[1] | ' ') == 'm' && (target[2] | ' ') == 'l' && target + 3 == s;
+
+ if (declaration ? PUGI__OPTSET(parse_declaration) : PUGI__OPTSET(parse_pi))
+ {
+ if (declaration)
+ {
+ // disallow non top-level declarations
+ if (cursor->parent) PUGI__THROW_ERROR(status_bad_pi, s);
+
+ PUGI__PUSHNODE(node_declaration);
+ }
+ else
+ {
+ PUGI__PUSHNODE(node_pi);
+ }
+
+ cursor->name = target;
+
+ PUGI__ENDSEG();
+
+ // parse value/attributes
+ if (ch == '?')
+ {
+ // empty node
+ if (!PUGI__ENDSWITH(*s, '>')) PUGI__THROW_ERROR(status_bad_pi, s);
+ s += (*s == '>');
+
+ PUGI__POPNODE();
+ }
+ else if (PUGI__IS_CHARTYPE(ch, ct_space))
+ {
+ PUGI__SKIPWS();
+
+ // scan for tag end
+ char_t* value = s;
+
+ PUGI__SCANFOR(s[0] == '?' && PUGI__ENDSWITH(s[1], '>'));
+ PUGI__CHECK_ERROR(status_bad_pi, s);
+
+ if (declaration)
+ {
+ // replace ending ? with / so that 'element' terminates properly
+ *s = '/';
+
+ // we exit from this function with cursor at node_declaration, which is a signal to parse() to go to LOC_ATTRIBUTES
+ s = value;
+ }
+ else
+ {
+ // store value and step over >
+ cursor->value = value;
+
+ PUGI__POPNODE();
+
+ PUGI__ENDSEG();
+
+ s += (*s == '>');
+ }
+ }
+ else PUGI__THROW_ERROR(status_bad_pi, s);
+ }
+ else
+ {
+ // scan for tag end
+ PUGI__SCANFOR(s[0] == '?' && PUGI__ENDSWITH(s[1], '>'));
+ PUGI__CHECK_ERROR(status_bad_pi, s);
+
+ s += (s[1] == '>' ? 2 : 1);
+ }
+
+ // store from registers
+ ref_cursor = cursor;
+
+ return s;
+ }
+
+ char_t* parse_tree(char_t* s, xml_node_struct* root, unsigned int optmsk, char_t endch)
+ {
+ strconv_attribute_t strconv_attribute = get_strconv_attribute(optmsk);
+ strconv_pcdata_t strconv_pcdata = get_strconv_pcdata(optmsk);
+
+ char_t ch = 0;
+ xml_node_struct* cursor = root;
+ char_t* mark = s;
+
+ while (*s != 0)
+ {
+ if (*s == '<')
+ {
+ ++s;
+
+ LOC_TAG:
+ if (PUGI__IS_CHARTYPE(*s, ct_start_symbol)) // '<#...'
+ {
+ PUGI__PUSHNODE(node_element); // Append a new node to the tree.
+
+ cursor->name = s;
+
+ PUGI__SCANWHILE_UNROLL(PUGI__IS_CHARTYPE(ss, ct_symbol)); // Scan for a terminator.
+ PUGI__ENDSEG(); // Save char in 'ch', terminate & step over.
+
+ if (ch == '>')
+ {
+ // end of tag
+ }
+ else if (PUGI__IS_CHARTYPE(ch, ct_space))
+ {
+ LOC_ATTRIBUTES:
+ while (true)
+ {
+ PUGI__SKIPWS(); // Eat any whitespace.
+
+ if (PUGI__IS_CHARTYPE(*s, ct_start_symbol)) // <... #...
+ {
+ xml_attribute_struct* a = append_new_attribute(cursor, *alloc); // Make space for this attribute.
+ if (!a) PUGI__THROW_ERROR(status_out_of_memory, s);
+
+ a->name = s; // Save the offset.
+
+ PUGI__SCANWHILE_UNROLL(PUGI__IS_CHARTYPE(ss, ct_symbol)); // Scan for a terminator.
+ PUGI__ENDSEG(); // Save char in 'ch', terminate & step over.
+
+ if (PUGI__IS_CHARTYPE(ch, ct_space))
+ {
+ PUGI__SKIPWS(); // Eat any whitespace.
+
+ ch = *s;
+ ++s;
+ }
+
+ if (ch == '=') // '<... #=...'
+ {
+ PUGI__SKIPWS(); // Eat any whitespace.
+
+ if (*s == '"' || *s == '\'') // '<... #="...'
+ {
+ ch = *s; // Save quote char to avoid breaking on "''" -or- '""'.
+ ++s; // Step over the quote.
+ a->value = s; // Save the offset.
+
+ s = strconv_attribute(s, ch);
+
+ if (!s) PUGI__THROW_ERROR(status_bad_attribute, a->value);
+
+ // After this line the loop continues from the start;
+ // Whitespaces, / and > are ok, symbols and EOF are wrong,
+ // everything else will be detected
+ if (PUGI__IS_CHARTYPE(*s, ct_start_symbol)) PUGI__THROW_ERROR(status_bad_attribute, s);
+ }
+ else PUGI__THROW_ERROR(status_bad_attribute, s);
+ }
+ else PUGI__THROW_ERROR(status_bad_attribute, s);
+ }
+ else if (*s == '/')
+ {
+ ++s;
+
+ if (*s == '>')
+ {
+ PUGI__POPNODE();
+ s++;
+ break;
+ }
+ else if (*s == 0 && endch == '>')
+ {
+ PUGI__POPNODE();
+ break;
+ }
+ else PUGI__THROW_ERROR(status_bad_start_element, s);
+ }
+ else if (*s == '>')
+ {
+ ++s;
+
+ break;
+ }
+ else if (*s == 0 && endch == '>')
+ {
+ break;
+ }
+ else PUGI__THROW_ERROR(status_bad_start_element, s);
+ }
+
+ // !!!
+ }
+ else if (ch == '/') // '<#.../'
+ {
+ if (!PUGI__ENDSWITH(*s, '>')) PUGI__THROW_ERROR(status_bad_start_element, s);
+
+ PUGI__POPNODE(); // Pop.
+
+ s += (*s == '>');
+ }
+ else if (ch == 0)
+ {
+ // we stepped over null terminator, backtrack & handle closing tag
+ --s;
+
+ if (endch != '>') PUGI__THROW_ERROR(status_bad_start_element, s);
+ }
+ else PUGI__THROW_ERROR(status_bad_start_element, s);
+ }
+ else if (*s == '/')
+ {
+ ++s;
+
+ mark = s;
+
+ char_t* name = cursor->name;
+ if (!name) PUGI__THROW_ERROR(status_end_element_mismatch, mark);
+
+ while (PUGI__IS_CHARTYPE(*s, ct_symbol))
+ {
+ if (*s++ != *name++) PUGI__THROW_ERROR(status_end_element_mismatch, mark);
+ }
+
+ if (*name)
+ {
+ if (*s == 0 && name[0] == endch && name[1] == 0) PUGI__THROW_ERROR(status_bad_end_element, s);
+ else PUGI__THROW_ERROR(status_end_element_mismatch, mark);
+ }
+
+ PUGI__POPNODE(); // Pop.
+
+ PUGI__SKIPWS();
+
+ if (*s == 0)
+ {
+ if (endch != '>') PUGI__THROW_ERROR(status_bad_end_element, s);
+ }
+ else
+ {
+ if (*s != '>') PUGI__THROW_ERROR(status_bad_end_element, s);
+ ++s;
+ }
+ }
+ else if (*s == '?') // '<?...'
+ {
+ s = parse_question(s, cursor, optmsk, endch);
+ if (!s) return s;
+
+ assert(cursor);
+ if (PUGI__NODETYPE(cursor) == node_declaration) goto LOC_ATTRIBUTES;
+ }
+ else if (*s == '!') // '<!...'
+ {
+ s = parse_exclamation(s, cursor, optmsk, endch);
+ if (!s) return s;
+ }
+ else if (*s == 0 && endch == '?') PUGI__THROW_ERROR(status_bad_pi, s);
+ else PUGI__THROW_ERROR(status_unrecognized_tag, s);
+ }
+ else
+ {
+ mark = s; // Save this offset while searching for a terminator.
+
+ PUGI__SKIPWS(); // Eat whitespace if no genuine PCDATA here.
+
+ if (*s == '<' || !*s)
+ {
+ // We skipped some whitespace characters because otherwise we would take the tag branch instead of PCDATA one
+ assert(mark != s);
+
+ if (!PUGI__OPTSET(parse_ws_pcdata | parse_ws_pcdata_single) || PUGI__OPTSET(parse_trim_pcdata))
+ {
+ continue;
+ }
+ else if (PUGI__OPTSET(parse_ws_pcdata_single))
+ {
+ if (s[0] != '<' || s[1] != '/' || cursor->first_child) continue;
+ }
+ }
+
+ if (!PUGI__OPTSET(parse_trim_pcdata))
+ s = mark;
+
+ if (cursor->parent || PUGI__OPTSET(parse_fragment))
+ {
+ if (PUGI__OPTSET(parse_embed_pcdata) && cursor->parent && !cursor->first_child && !cursor->value)
+ {
+ cursor->value = s; // Save the offset.
+ }
+ else
+ {
+ PUGI__PUSHNODE(node_pcdata); // Append a new node on the tree.
+
+ cursor->value = s; // Save the offset.
+
+ PUGI__POPNODE(); // Pop since this is a standalone.
+ }
+
+ s = strconv_pcdata(s);
+
+ if (!*s) break;
+ }
+ else
+ {
+ PUGI__SCANFOR(*s == '<'); // '...<'
+ if (!*s) break;
+
+ ++s;
+ }
+
+ // We're after '<'
+ goto LOC_TAG;
+ }
+ }
+
+ // check that last tag is closed
+ if (cursor != root) PUGI__THROW_ERROR(status_end_element_mismatch, s);
+
+ return s;
+ }
+
+ #ifdef PUGIXML_WCHAR_MODE
+ static char_t* parse_skip_bom(char_t* s)
+ {
+ unsigned int bom = 0xfeff;
+ return (s[0] == static_cast<wchar_t>(bom)) ? s + 1 : s;
+ }
+ #else
+ static char_t* parse_skip_bom(char_t* s)
+ {
+ return (s[0] == '\xef' && s[1] == '\xbb' && s[2] == '\xbf') ? s + 3 : s;
+ }
+ #endif
+
+ static bool has_element_node_siblings(xml_node_struct* node)
+ {
+ while (node)
+ {
+ if (PUGI__NODETYPE(node) == node_element) return true;
+
+ node = node->next_sibling;
+ }
+
+ return false;
+ }
+
+ static xml_parse_result parse(char_t* buffer, size_t length, xml_document_struct* xmldoc, xml_node_struct* root, unsigned int optmsk)
+ {
+ // early-out for empty documents
+ if (length == 0)
+ return make_parse_result(PUGI__OPTSET(parse_fragment) ? status_ok : status_no_document_element);
+
+ // get last child of the root before parsing
+ xml_node_struct* last_root_child = root->first_child ? root->first_child->prev_sibling_c + 0 : 0;
+
+ // create parser on stack
+ xml_parser parser(static_cast<xml_allocator*>(xmldoc));
+
+ // save last character and make buffer zero-terminated (speeds up parsing)
+ char_t endch = buffer[length - 1];
+ buffer[length - 1] = 0;
+
+ // skip BOM to make sure it does not end up as part of parse output
+ char_t* buffer_data = parse_skip_bom(buffer);
+
+ // perform actual parsing
+ parser.parse_tree(buffer_data, root, optmsk, endch);
+
+ xml_parse_result result = make_parse_result(parser.error_status, parser.error_offset ? parser.error_offset - buffer : 0);
+ assert(result.offset >= 0 && static_cast<size_t>(result.offset) <= length);
+
+ if (result)
+ {
+ // since we removed last character, we have to handle the only possible false positive (stray <)
+ if (endch == '<')
+ return make_parse_result(status_unrecognized_tag, length - 1);
+
+ // check if there are any element nodes parsed
+ xml_node_struct* first_root_child_parsed = last_root_child ? last_root_child->next_sibling + 0 : root->first_child+ 0;
+
+ if (!PUGI__OPTSET(parse_fragment) && !has_element_node_siblings(first_root_child_parsed))
+ return make_parse_result(status_no_document_element, length - 1);
+ }
+ else
+ {
+ // roll back offset if it occurs on a null terminator in the source buffer
+ if (result.offset > 0 && static_cast<size_t>(result.offset) == length - 1 && endch == 0)
+ result.offset--;
+ }
+
+ return result;
+ }
+ };
+
+ // Output facilities
+ PUGI__FN xml_encoding get_write_native_encoding()
+ {
+ #ifdef PUGIXML_WCHAR_MODE
+ return get_wchar_encoding();
+ #else
+ return encoding_utf8;
+ #endif
+ }
+
+ PUGI__FN xml_encoding get_write_encoding(xml_encoding encoding)
+ {
+ // replace wchar encoding with utf implementation
+ if (encoding == encoding_wchar) return get_wchar_encoding();
+
+ // replace utf16 encoding with utf16 with specific endianness
+ if (encoding == encoding_utf16) return is_little_endian() ? encoding_utf16_le : encoding_utf16_be;
+
+ // replace utf32 encoding with utf32 with specific endianness
+ if (encoding == encoding_utf32) return is_little_endian() ? encoding_utf32_le : encoding_utf32_be;
+
+ // only do autodetection if no explicit encoding is requested
+ if (encoding != encoding_auto) return encoding;
+
+ // assume utf8 encoding
+ return encoding_utf8;
+ }
+
+ template <typename D, typename T> PUGI__FN size_t convert_buffer_output_generic(typename T::value_type dest, const char_t* data, size_t length, D, T)
+ {
+ PUGI__STATIC_ASSERT(sizeof(char_t) == sizeof(typename D::type));
+
+ typename T::value_type end = D::process(reinterpret_cast<const typename D::type*>(data), length, dest, T());
+
+ return static_cast<size_t>(end - dest) * sizeof(*dest);
+ }
+
+ template <typename D, typename T> PUGI__FN size_t convert_buffer_output_generic(typename T::value_type dest, const char_t* data, size_t length, D, T, bool opt_swap)
+ {
+ PUGI__STATIC_ASSERT(sizeof(char_t) == sizeof(typename D::type));
+
+ typename T::value_type end = D::process(reinterpret_cast<const typename D::type*>(data), length, dest, T());
+
+ if (opt_swap)
+ {
+ for (typename T::value_type i = dest; i != end; ++i)
+ *i = endian_swap(*i);
+ }
+
+ return static_cast<size_t>(end - dest) * sizeof(*dest);
+ }
+
+#ifdef PUGIXML_WCHAR_MODE
+ PUGI__FN size_t get_valid_length(const char_t* data, size_t length)
+ {
+ if (length < 1) return 0;
+
+ // discard last character if it's the lead of a surrogate pair
+ return (sizeof(wchar_t) == 2 && static_cast<unsigned int>(static_cast<uint16_t>(data[length - 1]) - 0xD800) < 0x400) ? length - 1 : length;
+ }
+
+ PUGI__FN size_t convert_buffer_output(char_t* r_char, uint8_t* r_u8, uint16_t* r_u16, uint32_t* r_u32, const char_t* data, size_t length, xml_encoding encoding)
+ {
+ // only endian-swapping is required
+ if (need_endian_swap_utf(encoding, get_wchar_encoding()))
+ {
+ convert_wchar_endian_swap(r_char, data, length);
+
+ return length * sizeof(char_t);
+ }
+
+ // convert to utf8
+ if (encoding == encoding_utf8)
+ return convert_buffer_output_generic(r_u8, data, length, wchar_decoder(), utf8_writer());
+
+ // convert to utf16
+ if (encoding == encoding_utf16_be || encoding == encoding_utf16_le)
+ {
+ xml_encoding native_encoding = is_little_endian() ? encoding_utf16_le : encoding_utf16_be;
+
+ return convert_buffer_output_generic(r_u16, data, length, wchar_decoder(), utf16_writer(), native_encoding != encoding);
+ }
+
+ // convert to utf32
+ if (encoding == encoding_utf32_be || encoding == encoding_utf32_le)
+ {
+ xml_encoding native_encoding = is_little_endian() ? encoding_utf32_le : encoding_utf32_be;
+
+ return convert_buffer_output_generic(r_u32, data, length, wchar_decoder(), utf32_writer(), native_encoding != encoding);
+ }
+
+ // convert to latin1
+ if (encoding == encoding_latin1)
+ return convert_buffer_output_generic(r_u8, data, length, wchar_decoder(), latin1_writer());
+
+ assert(false && "Invalid encoding"); // unreachable
+ return 0;
+ }
+#else
+ PUGI__FN size_t get_valid_length(const char_t* data, size_t length)
+ {
+ if (length < 5) return 0;
+
+ for (size_t i = 1; i <= 4; ++i)
+ {
+ uint8_t ch = static_cast<uint8_t>(data[length - i]);
+
+ // either a standalone character or a leading one
+ if ((ch & 0xc0) != 0x80) return length - i;
+ }
+
+ // there are four non-leading characters at the end, sequence tail is broken so might as well process the whole chunk
+ return length;
+ }
+
+ PUGI__FN size_t convert_buffer_output(char_t* /* r_char */, uint8_t* r_u8, uint16_t* r_u16, uint32_t* r_u32, const char_t* data, size_t length, xml_encoding encoding)
+ {
+ if (encoding == encoding_utf16_be || encoding == encoding_utf16_le)
+ {
+ xml_encoding native_encoding = is_little_endian() ? encoding_utf16_le : encoding_utf16_be;
+
+ return convert_buffer_output_generic(r_u16, data, length, utf8_decoder(), utf16_writer(), native_encoding != encoding);
+ }
+
+ if (encoding == encoding_utf32_be || encoding == encoding_utf32_le)
+ {
+ xml_encoding native_encoding = is_little_endian() ? encoding_utf32_le : encoding_utf32_be;
+
+ return convert_buffer_output_generic(r_u32, data, length, utf8_decoder(), utf32_writer(), native_encoding != encoding);
+ }
+
+ if (encoding == encoding_latin1)
+ return convert_buffer_output_generic(r_u8, data, length, utf8_decoder(), latin1_writer());
+
+ assert(false && "Invalid encoding"); // unreachable
+ return 0;
+ }
+#endif
+
+ class xml_buffered_writer
+ {
+ xml_buffered_writer(const xml_buffered_writer&);
+ xml_buffered_writer& operator=(const xml_buffered_writer&);
+
+ public:
+ xml_buffered_writer(xml_writer& writer_, xml_encoding user_encoding): writer(writer_), bufsize(0), encoding(get_write_encoding(user_encoding))
+ {
+ PUGI__STATIC_ASSERT(bufcapacity >= 8);
+ }
+
+ size_t flush()
+ {
+ flush(buffer, bufsize);
+ bufsize = 0;
+ return 0;
+ }
+
+ void flush(const char_t* data, size_t size)
+ {
+ if (size == 0) return;
+
+ // fast path, just write data
+ if (encoding == get_write_native_encoding())
+ writer.write(data, size * sizeof(char_t));
+ else
+ {
+ // convert chunk
+ size_t result = convert_buffer_output(scratch.data_char, scratch.data_u8, scratch.data_u16, scratch.data_u32, data, size, encoding);
+ assert(result <= sizeof(scratch));
+
+ // write data
+ writer.write(scratch.data_u8, result);
+ }
+ }
+
+ void write_direct(const char_t* data, size_t length)
+ {
+ // flush the remaining buffer contents
+ flush();
+
+ // handle large chunks
+ if (length > bufcapacity)
+ {
+ if (encoding == get_write_native_encoding())
+ {
+ // fast path, can just write data chunk
+ writer.write(data, length * sizeof(char_t));
+ return;
+ }
+
+ // need to convert in suitable chunks
+ while (length > bufcapacity)
+ {
+ // get chunk size by selecting such number of characters that are guaranteed to fit into scratch buffer
+ // and form a complete codepoint sequence (i.e. discard start of last codepoint if necessary)
+ size_t chunk_size = get_valid_length(data, bufcapacity);
+ assert(chunk_size);
+
+ // convert chunk and write
+ flush(data, chunk_size);
+
+ // iterate
+ data += chunk_size;
+ length -= chunk_size;
+ }
+
+ // small tail is copied below
+ bufsize = 0;
+ }
+
+ memcpy(buffer + bufsize, data, length * sizeof(char_t));
+ bufsize += length;
+ }
+
+ void write_buffer(const char_t* data, size_t length)
+ {
+ size_t offset = bufsize;
+
+ if (offset + length <= bufcapacity)
+ {
+ memcpy(buffer + offset, data, length * sizeof(char_t));
+ bufsize = offset + length;
+ }
+ else
+ {
+ write_direct(data, length);
+ }
+ }
+
+ void write_string(const char_t* data)
+ {
+ // write the part of the string that fits in the buffer
+ size_t offset = bufsize;
+
+ while (*data && offset < bufcapacity)
+ buffer[offset++] = *data++;
+
+ // write the rest
+ if (offset < bufcapacity)
+ {
+ bufsize = offset;
+ }
+ else
+ {
+ // backtrack a bit if we have split the codepoint
+ size_t length = offset - bufsize;
+ size_t extra = length - get_valid_length(data - length, length);
+
+ bufsize = offset - extra;
+
+ write_direct(data - extra, strlength(data) + extra);
+ }
+ }
+
+ void write(char_t d0)
+ {
+ size_t offset = bufsize;
+ if (offset > bufcapacity - 1) offset = flush();
+
+ buffer[offset + 0] = d0;
+ bufsize = offset + 1;
+ }
+
+ void write(char_t d0, char_t d1)
+ {
+ size_t offset = bufsize;
+ if (offset > bufcapacity - 2) offset = flush();
+
+ buffer[offset + 0] = d0;
+ buffer[offset + 1] = d1;
+ bufsize = offset + 2;
+ }
+
+ void write(char_t d0, char_t d1, char_t d2)
+ {
+ size_t offset = bufsize;
+ if (offset > bufcapacity - 3) offset = flush();
+
+ buffer[offset + 0] = d0;
+ buffer[offset + 1] = d1;
+ buffer[offset + 2] = d2;
+ bufsize = offset + 3;
+ }
+
+ void write(char_t d0, char_t d1, char_t d2, char_t d3)
+ {
+ size_t offset = bufsize;
+ if (offset > bufcapacity - 4) offset = flush();
+
+ buffer[offset + 0] = d0;
+ buffer[offset + 1] = d1;
+ buffer[offset + 2] = d2;
+ buffer[offset + 3] = d3;
+ bufsize = offset + 4;
+ }
+
+ void write(char_t d0, char_t d1, char_t d2, char_t d3, char_t d4)
+ {
+ size_t offset = bufsize;
+ if (offset > bufcapacity - 5) offset = flush();
+
+ buffer[offset + 0] = d0;
+ buffer[offset + 1] = d1;
+ buffer[offset + 2] = d2;
+ buffer[offset + 3] = d3;
+ buffer[offset + 4] = d4;
+ bufsize = offset + 5;
+ }
+
+ void write(char_t d0, char_t d1, char_t d2, char_t d3, char_t d4, char_t d5)
+ {
+ size_t offset = bufsize;
+ if (offset > bufcapacity - 6) offset = flush();
+
+ buffer[offset + 0] = d0;
+ buffer[offset + 1] = d1;
+ buffer[offset + 2] = d2;
+ buffer[offset + 3] = d3;
+ buffer[offset + 4] = d4;
+ buffer[offset + 5] = d5;
+ bufsize = offset + 6;
+ }
+
+ // utf8 maximum expansion: x4 (-> utf32)
+ // utf16 maximum expansion: x2 (-> utf32)
+ // utf32 maximum expansion: x1
+ enum
+ {
+ bufcapacitybytes =
+ #ifdef PUGIXML_MEMORY_OUTPUT_STACK
+ PUGIXML_MEMORY_OUTPUT_STACK
+ #else
+ 10240
+ #endif
+ ,
+ bufcapacity = bufcapacitybytes / (sizeof(char_t) + 4)
+ };
+
+ char_t buffer[bufcapacity];
+
+ union
+ {
+ uint8_t data_u8[4 * bufcapacity];
+ uint16_t data_u16[2 * bufcapacity];
+ uint32_t data_u32[bufcapacity];
+ char_t data_char[bufcapacity];
+ } scratch;
+
+ xml_writer& writer;
+ size_t bufsize;
+ xml_encoding encoding;
+ };
+
+ PUGI__FN void text_output_escaped(xml_buffered_writer& writer, const char_t* s, chartypex_t type, unsigned int flags)
+ {
+ while (*s)
+ {
+ const char_t* prev = s;
+
+ // While *s is a usual symbol
+ PUGI__SCANWHILE_UNROLL(!PUGI__IS_CHARTYPEX(ss, type));
+
+ writer.write_buffer(prev, static_cast<size_t>(s - prev));
+
+ switch (*s)
+ {
+ case 0: break;
+ case '&':
+ writer.write('&', 'a', 'm', 'p', ';');
+ ++s;
+ break;
+ case '<':
+ writer.write('&', 'l', 't', ';');
+ ++s;
+ break;
+ case '>':
+ writer.write('&', 'g', 't', ';');
+ ++s;
+ break;
+ case '"':
+ if (flags & format_attribute_single_quote)
+ writer.write('"');
+ else
+ writer.write('&', 'q', 'u', 'o', 't', ';');
+ ++s;
+ break;
+ case '\'':
+ if (flags & format_attribute_single_quote)
+ writer.write('&', 'a', 'p', 'o', 's', ';');
+ else
+ writer.write('\'');
+ ++s;
+ break;
+ default: // s is not a usual symbol
+ {
+ unsigned int ch = static_cast<unsigned int>(*s++);
+ assert(ch < 32);
+
+ if (!(flags & format_skip_control_chars))
+ writer.write('&', '#', static_cast<char_t>((ch / 10) + '0'), static_cast<char_t>((ch % 10) + '0'), ';');
+ }
+ }
+ }
+ }
+
+ PUGI__FN void text_output(xml_buffered_writer& writer, const char_t* s, chartypex_t type, unsigned int flags)
+ {
+ if (flags & format_no_escapes)
+ writer.write_string(s);
+ else
+ text_output_escaped(writer, s, type, flags);
+ }
+
+ PUGI__FN void text_output_cdata(xml_buffered_writer& writer, const char_t* s)
+ {
+ do
+ {
+ writer.write('<', '!', '[', 'C', 'D');
+ writer.write('A', 'T', 'A', '[');
+
+ const char_t* prev = s;
+
+ // look for ]]> sequence - we can't output it as is since it terminates CDATA
+ while (*s && !(s[0] == ']' && s[1] == ']' && s[2] == '>')) ++s;
+
+ // skip ]] if we stopped at ]]>, > will go to the next CDATA section
+ if (*s) s += 2;
+
+ writer.write_buffer(prev, static_cast<size_t>(s - prev));
+
+ writer.write(']', ']', '>');
+ }
+ while (*s);
+ }
+
+ PUGI__FN void text_output_indent(xml_buffered_writer& writer, const char_t* indent, size_t indent_length, unsigned int depth)
+ {
+ switch (indent_length)
+ {
+ case 1:
+ {
+ for (unsigned int i = 0; i < depth; ++i)
+ writer.write(indent[0]);
+ break;
+ }
+
+ case 2:
+ {
+ for (unsigned int i = 0; i < depth; ++i)
+ writer.write(indent[0], indent[1]);
+ break;
+ }
+
+ case 3:
+ {
+ for (unsigned int i = 0; i < depth; ++i)
+ writer.write(indent[0], indent[1], indent[2]);
+ break;
+ }
+
+ case 4:
+ {
+ for (unsigned int i = 0; i < depth; ++i)
+ writer.write(indent[0], indent[1], indent[2], indent[3]);
+ break;
+ }
+
+ default:
+ {
+ for (unsigned int i = 0; i < depth; ++i)
+ writer.write_buffer(indent, indent_length);
+ }
+ }
+ }
+
+ PUGI__FN void node_output_comment(xml_buffered_writer& writer, const char_t* s)
+ {
+ writer.write('<', '!', '-', '-');
+
+ while (*s)
+ {
+ const char_t* prev = s;
+
+ // look for -\0 or -- sequence - we can't output it since -- is illegal in comment body
+ while (*s && !(s[0] == '-' && (s[1] == '-' || s[1] == 0))) ++s;
+
+ writer.write_buffer(prev, static_cast<size_t>(s - prev));
+
+ if (*s)
+ {
+ assert(*s == '-');
+
+ writer.write('-', ' ');
+ ++s;
+ }
+ }
+
+ writer.write('-', '-', '>');
+ }
+
+ PUGI__FN void node_output_pi_value(xml_buffered_writer& writer, const char_t* s)
+ {
+ while (*s)
+ {
+ const char_t* prev = s;
+
+ // look for ?> sequence - we can't output it since ?> terminates PI
+ while (*s && !(s[0] == '?' && s[1] == '>')) ++s;
+
+ writer.write_buffer(prev, static_cast<size_t>(s - prev));
+
+ if (*s)
+ {
+ assert(s[0] == '?' && s[1] == '>');
+
+ writer.write('?', ' ', '>');
+ s += 2;
+ }
+ }
+ }
+
+ PUGI__FN void node_output_attributes(xml_buffered_writer& writer, xml_node_struct* node, const char_t* indent, size_t indent_length, unsigned int flags, unsigned int depth)
+ {
+ const char_t* default_name = PUGIXML_TEXT(":anonymous");
+ const char_t enquotation_char = (flags & format_attribute_single_quote) ? '\'' : '"';
+
+ for (xml_attribute_struct* a = node->first_attribute; a; a = a->next_attribute)
+ {
+ if ((flags & (format_indent_attributes | format_raw)) == format_indent_attributes)
+ {
+ writer.write('\n');
+
+ text_output_indent(writer, indent, indent_length, depth + 1);
+ }
+ else
+ {
+ writer.write(' ');
+ }
+
+ writer.write_string(a->name ? a->name + 0 : default_name);
+ writer.write('=', enquotation_char);
+
+ if (a->value)
+ text_output(writer, a->value, ctx_special_attr, flags);
+
+ writer.write(enquotation_char);
+ }
+ }
+
+ PUGI__FN bool node_output_start(xml_buffered_writer& writer, xml_node_struct* node, const char_t* indent, size_t indent_length, unsigned int flags, unsigned int depth)
+ {
+ const char_t* default_name = PUGIXML_TEXT(":anonymous");
+ const char_t* name = node->name ? node->name + 0 : default_name;
+
+ writer.write('<');
+ writer.write_string(name);
+
+ if (node->first_attribute)
+ node_output_attributes(writer, node, indent, indent_length, flags, depth);
+
+ // element nodes can have value if parse_embed_pcdata was used
+ if (!node->value)
+ {
+ if (!node->first_child)
+ {
+ if (flags & format_no_empty_element_tags)
+ {
+ writer.write('>', '<', '/');
+ writer.write_string(name);
+ writer.write('>');
+
+ return false;
+ }
+ else
+ {
+ if ((flags & format_raw) == 0)
+ writer.write(' ');
+
+ writer.write('/', '>');
+
+ return false;
+ }
+ }
+ else
+ {
+ writer.write('>');
+
+ return true;
+ }
+ }
+ else
+ {
+ writer.write('>');
+
+ text_output(writer, node->value, ctx_special_pcdata, flags);
+
+ if (!node->first_child)
+ {
+ writer.write('<', '/');
+ writer.write_string(name);
+ writer.write('>');
+
+ return false;
+ }
+ else
+ {
+ return true;
+ }
+ }
+ }
+
+ PUGI__FN void node_output_end(xml_buffered_writer& writer, xml_node_struct* node)
+ {
+ const char_t* default_name = PUGIXML_TEXT(":anonymous");
+ const char_t* name = node->name ? node->name + 0 : default_name;
+
+ writer.write('<', '/');
+ writer.write_string(name);
+ writer.write('>');
+ }
+
+ PUGI__FN void node_output_simple(xml_buffered_writer& writer, xml_node_struct* node, unsigned int flags)
+ {
+ const char_t* default_name = PUGIXML_TEXT(":anonymous");
+
+ switch (PUGI__NODETYPE(node))
+ {
+ case node_pcdata:
+ text_output(writer, node->value ? node->value + 0 : PUGIXML_TEXT(""), ctx_special_pcdata, flags);
+ break;
+
+ case node_cdata:
+ text_output_cdata(writer, node->value ? node->value + 0 : PUGIXML_TEXT(""));
+ break;
+
+ case node_comment:
+ node_output_comment(writer, node->value ? node->value + 0 : PUGIXML_TEXT(""));
+ break;
+
+ case node_pi:
+ writer.write('<', '?');
+ writer.write_string(node->name ? node->name + 0 : default_name);
+
+ if (node->value)
+ {
+ writer.write(' ');
+ node_output_pi_value(writer, node->value);
+ }
+
+ writer.write('?', '>');
+ break;
+
+ case node_declaration:
+ writer.write('<', '?');
+ writer.write_string(node->name ? node->name + 0 : default_name);
+ node_output_attributes(writer, node, PUGIXML_TEXT(""), 0, flags | format_raw, 0);
+ writer.write('?', '>');
+ break;
+
+ case node_doctype:
+ writer.write('<', '!', 'D', 'O', 'C');
+ writer.write('T', 'Y', 'P', 'E');
+
+ if (node->value)
+ {
+ writer.write(' ');
+ writer.write_string(node->value);
+ }
+
+ writer.write('>');
+ break;
+
+ default:
+ assert(false && "Invalid node type"); // unreachable
+ }
+ }
+
+ enum indent_flags_t
+ {
+ indent_newline = 1,
+ indent_indent = 2
+ };
+
+ PUGI__FN void node_output(xml_buffered_writer& writer, xml_node_struct* root, const char_t* indent, unsigned int flags, unsigned int depth)
+ {
+ size_t indent_length = ((flags & (format_indent | format_indent_attributes)) && (flags & format_raw) == 0) ? strlength(indent) : 0;
+ unsigned int indent_flags = indent_indent;
+
+ xml_node_struct* node = root;
+
+ do
+ {
+ assert(node);
+
+ // begin writing current node
+ if (PUGI__NODETYPE(node) == node_pcdata || PUGI__NODETYPE(node) == node_cdata)
+ {
+ node_output_simple(writer, node, flags);
+
+ indent_flags = 0;
+ }
+ else
+ {
+ if ((indent_flags & indent_newline) && (flags & format_raw) == 0)
+ writer.write('\n');
+
+ if ((indent_flags & indent_indent) && indent_length)
+ text_output_indent(writer, indent, indent_length, depth);
+
+ if (PUGI__NODETYPE(node) == node_element)
+ {
+ indent_flags = indent_newline | indent_indent;
+
+ if (node_output_start(writer, node, indent, indent_length, flags, depth))
+ {
+ // element nodes can have value if parse_embed_pcdata was used
+ if (node->value)
+ indent_flags = 0;
+
+ node = node->first_child;
+ depth++;
+ continue;
+ }
+ }
+ else if (PUGI__NODETYPE(node) == node_document)
+ {
+ indent_flags = indent_indent;
+
+ if (node->first_child)
+ {
+ node = node->first_child;
+ continue;
+ }
+ }
+ else
+ {
+ node_output_simple(writer, node, flags);
+
+ indent_flags = indent_newline | indent_indent;
+ }
+ }
+
+ // continue to the next node
+ while (node != root)
+ {
+ if (node->next_sibling)
+ {
+ node = node->next_sibling;
+ break;
+ }
+
+ node = node->parent;
+
+ // write closing node
+ if (PUGI__NODETYPE(node) == node_element)
+ {
+ depth--;
+
+ if ((indent_flags & indent_newline) && (flags & format_raw) == 0)
+ writer.write('\n');
+
+ if ((indent_flags & indent_indent) && indent_length)
+ text_output_indent(writer, indent, indent_length, depth);
+
+ node_output_end(writer, node);
+
+ indent_flags = indent_newline | indent_indent;
+ }
+ }
+ }
+ while (node != root);
+
+ if ((indent_flags & indent_newline) && (flags & format_raw) == 0)
+ writer.write('\n');
+ }
+
+ PUGI__FN bool has_declaration(xml_node_struct* node)
+ {
+ for (xml_node_struct* child = node->first_child; child; child = child->next_sibling)
+ {
+ xml_node_type type = PUGI__NODETYPE(child);
+
+ if (type == node_declaration) return true;
+ if (type == node_element) return false;
+ }
+
+ return false;
+ }
+
+ PUGI__FN bool is_attribute_of(xml_attribute_struct* attr, xml_node_struct* node)
+ {
+ for (xml_attribute_struct* a = node->first_attribute; a; a = a->next_attribute)
+ if (a == attr)
+ return true;
+
+ return false;
+ }
+
+ PUGI__FN bool allow_insert_attribute(xml_node_type parent)
+ {
+ return parent == node_element || parent == node_declaration;
+ }
+
+ PUGI__FN bool allow_insert_child(xml_node_type parent, xml_node_type child)
+ {
+ if (parent != node_document && parent != node_element) return false;
+ if (child == node_document || child == node_null) return false;
+ if (parent != node_document && (child == node_declaration || child == node_doctype)) return false;
+
+ return true;
+ }
+
+ PUGI__FN bool allow_move(xml_node parent, xml_node child)
+ {
+ // check that child can be a child of parent
+ if (!allow_insert_child(parent.type(), child.type()))
+ return false;
+
+ // check that node is not moved between documents
+ if (parent.root() != child.root())
+ return false;
+
+ // check that new parent is not in the child subtree
+ xml_node cur = parent;
+
+ while (cur)
+ {
+ if (cur == child)
+ return false;
+
+ cur = cur.parent();
+ }
+
+ return true;
+ }
+
+ template <typename String, typename Header>
+ PUGI__FN void node_copy_string(String& dest, Header& header, uintptr_t header_mask, char_t* source, Header& source_header, xml_allocator* alloc)
+ {
+ assert(!dest && (header & header_mask) == 0);
+
+ if (source)
+ {
+ if (alloc && (source_header & header_mask) == 0)
+ {
+ dest = source;
+
+ // since strcpy_insitu can reuse document buffer memory we need to mark both source and dest as shared
+ header |= xml_memory_page_contents_shared_mask;
+ source_header |= xml_memory_page_contents_shared_mask;
+ }
+ else
+ strcpy_insitu(dest, header, header_mask, source, strlength(source));
+ }
+ }
+
+ PUGI__FN void node_copy_contents(xml_node_struct* dn, xml_node_struct* sn, xml_allocator* shared_alloc)
+ {
+ node_copy_string(dn->name, dn->header, xml_memory_page_name_allocated_mask, sn->name, sn->header, shared_alloc);
+ node_copy_string(dn->value, dn->header, xml_memory_page_value_allocated_mask, sn->value, sn->header, shared_alloc);
+
+ for (xml_attribute_struct* sa = sn->first_attribute; sa; sa = sa->next_attribute)
+ {
+ xml_attribute_struct* da = append_new_attribute(dn, get_allocator(dn));
+
+ if (da)
+ {
+ node_copy_string(da->name, da->header, xml_memory_page_name_allocated_mask, sa->name, sa->header, shared_alloc);
+ node_copy_string(da->value, da->header, xml_memory_page_value_allocated_mask, sa->value, sa->header, shared_alloc);
+ }
+ }
+ }
+
+ PUGI__FN void node_copy_tree(xml_node_struct* dn, xml_node_struct* sn)
+ {
+ xml_allocator& alloc = get_allocator(dn);
+ xml_allocator* shared_alloc = (&alloc == &get_allocator(sn)) ? &alloc : 0;
+
+ node_copy_contents(dn, sn, shared_alloc);
+
+ xml_node_struct* dit = dn;
+ xml_node_struct* sit = sn->first_child;
+
+ while (sit && sit != sn)
+ {
+ // loop invariant: dit is inside the subtree rooted at dn
+ assert(dit);
+
+ // when a tree is copied into one of the descendants, we need to skip that subtree to avoid an infinite loop
+ if (sit != dn)
+ {
+ xml_node_struct* copy = append_new_node(dit, alloc, PUGI__NODETYPE(sit));
+
+ if (copy)
+ {
+ node_copy_contents(copy, sit, shared_alloc);
+
+ if (sit->first_child)
+ {
+ dit = copy;
+ sit = sit->first_child;
+ continue;
+ }
+ }
+ }
+
+ // continue to the next node
+ do
+ {
+ if (sit->next_sibling)
+ {
+ sit = sit->next_sibling;
+ break;
+ }
+
+ sit = sit->parent;
+ dit = dit->parent;
+
+ // loop invariant: dit is inside the subtree rooted at dn while sit is inside sn
+ assert(sit == sn || dit);
+ }
+ while (sit != sn);
+ }
+
+ assert(!sit || dit == dn->parent);
+ }
+
+ PUGI__FN void node_copy_attribute(xml_attribute_struct* da, xml_attribute_struct* sa)
+ {
+ xml_allocator& alloc = get_allocator(da);
+ xml_allocator* shared_alloc = (&alloc == &get_allocator(sa)) ? &alloc : 0;
+
+ node_copy_string(da->name, da->header, xml_memory_page_name_allocated_mask, sa->name, sa->header, shared_alloc);
+ node_copy_string(da->value, da->header, xml_memory_page_value_allocated_mask, sa->value, sa->header, shared_alloc);
+ }
+
+ inline bool is_text_node(xml_node_struct* node)
+ {
+ xml_node_type type = PUGI__NODETYPE(node);
+
+ return type == node_pcdata || type == node_cdata;
+ }
+
+ // get value with conversion functions
+ template <typename U> PUGI__FN PUGI__UNSIGNED_OVERFLOW U string_to_integer(const char_t* value, U minv, U maxv)
+ {
+ U result = 0;
+ const char_t* s = value;
+
+ while (PUGI__IS_CHARTYPE(*s, ct_space))
+ s++;
+
+ bool negative = (*s == '-');
+
+ s += (*s == '+' || *s == '-');
+
+ bool overflow = false;
+
+ if (s[0] == '0' && (s[1] | ' ') == 'x')
+ {
+ s += 2;
+
+ // since overflow detection relies on length of the sequence skip leading zeros
+ while (*s == '0')
+ s++;
+
+ const char_t* start = s;
+
+ for (;;)
+ {
+ if (static_cast<unsigned>(*s - '0') < 10)
+ result = result * 16 + (*s - '0');
+ else if (static_cast<unsigned>((*s | ' ') - 'a') < 6)
+ result = result * 16 + ((*s | ' ') - 'a' + 10);
+ else
+ break;
+
+ s++;
+ }
+
+ size_t digits = static_cast<size_t>(s - start);
+
+ overflow = digits > sizeof(U) * 2;
+ }
+ else
+ {
+ // since overflow detection relies on length of the sequence skip leading zeros
+ while (*s == '0')
+ s++;
+
+ const char_t* start = s;
+
+ for (;;)
+ {
+ if (static_cast<unsigned>(*s - '0') < 10)
+ result = result * 10 + (*s - '0');
+ else
+ break;
+
+ s++;
+ }
+
+ size_t digits = static_cast<size_t>(s - start);
+
+ PUGI__STATIC_ASSERT(sizeof(U) == 8 || sizeof(U) == 4 || sizeof(U) == 2);
+
+ const size_t max_digits10 = sizeof(U) == 8 ? 20 : sizeof(U) == 4 ? 10 : 5;
+ const char_t max_lead = sizeof(U) == 8 ? '1' : sizeof(U) == 4 ? '4' : '6';
+ const size_t high_bit = sizeof(U) * 8 - 1;
+
+ overflow = digits >= max_digits10 && !(digits == max_digits10 && (*start < max_lead || (*start == max_lead && result >> high_bit)));
+ }
+
+ if (negative)
+ {
+ // Workaround for crayc++ CC-3059: Expected no overflow in routine.
+ #ifdef _CRAYC
+ return (overflow || result > ~minv + 1) ? minv : ~result + 1;
+ #else
+ return (overflow || result > 0 - minv) ? minv : 0 - result;
+ #endif
+ }
+ else
+ return (overflow || result > maxv) ? maxv : result;
+ }
+
+ PUGI__FN int get_value_int(const char_t* value)
+ {
+ return string_to_integer<unsigned int>(value, static_cast<unsigned int>(INT_MIN), INT_MAX);
+ }
+
+ PUGI__FN unsigned int get_value_uint(const char_t* value)
+ {
+ return string_to_integer<unsigned int>(value, 0, UINT_MAX);
+ }
+
+ PUGI__FN double get_value_double(const char_t* value)
+ {
+ #ifdef PUGIXML_WCHAR_MODE
+ return wcstod(value, 0);
+ #else
+ return strtod(value, 0);
+ #endif
+ }
+
+ PUGI__FN float get_value_float(const char_t* value)
+ {
+ #ifdef PUGIXML_WCHAR_MODE
+ return static_cast<float>(wcstod(value, 0));
+ #else
+ return static_cast<float>(strtod(value, 0));
+ #endif
+ }
+
+ PUGI__FN bool get_value_bool(const char_t* value)
+ {
+ // only look at first char
+ char_t first = *value;
+
+ // 1*, t* (true), T* (True), y* (yes), Y* (YES)
+ return (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y');
+ }
+
+#ifdef PUGIXML_HAS_LONG_LONG
+ PUGI__FN long long get_value_llong(const char_t* value)
+ {
+ return string_to_integer<unsigned long long>(value, static_cast<unsigned long long>(LLONG_MIN), LLONG_MAX);
+ }
+
+ PUGI__FN unsigned long long get_value_ullong(const char_t* value)
+ {
+ return string_to_integer<unsigned long long>(value, 0, ULLONG_MAX);
+ }
+#endif
+
+ template <typename U> PUGI__FN PUGI__UNSIGNED_OVERFLOW char_t* integer_to_string(char_t* begin, char_t* end, U value, bool negative)
+ {
+ char_t* result = end - 1;
+ U rest = negative ? 0 - value : value;
+
+ do
+ {
+ *result-- = static_cast<char_t>('0' + (rest % 10));
+ rest /= 10;
+ }
+ while (rest);
+
+ assert(result >= begin);
+ (void)begin;
+
+ *result = '-';
+
+ return result + !negative;
+ }
+
+ // set value with conversion functions
+ template <typename String, typename Header>
+ PUGI__FN bool set_value_ascii(String& dest, Header& header, uintptr_t header_mask, char* buf)
+ {
+ #ifdef PUGIXML_WCHAR_MODE
+ char_t wbuf[128];
+ assert(strlen(buf) < sizeof(wbuf) / sizeof(wbuf[0]));
+
+ size_t offset = 0;
+ for (; buf[offset]; ++offset) wbuf[offset] = buf[offset];
+
+ return strcpy_insitu(dest, header, header_mask, wbuf, offset);
+ #else
+ return strcpy_insitu(dest, header, header_mask, buf, strlen(buf));
+ #endif
+ }
+
+ template <typename U, typename String, typename Header>
+ PUGI__FN bool set_value_integer(String& dest, Header& header, uintptr_t header_mask, U value, bool negative)
+ {
+ char_t buf[64];
+ char_t* end = buf + sizeof(buf) / sizeof(buf[0]);
+ char_t* begin = integer_to_string(buf, end, value, negative);
+
+ return strcpy_insitu(dest, header, header_mask, begin, end - begin);
+ }
+
+ template <typename String, typename Header>
+ PUGI__FN bool set_value_convert(String& dest, Header& header, uintptr_t header_mask, float value, int precision)
+ {
+ char buf[128];
+ PUGI__SNPRINTF(buf, "%.*g", precision, double(value));
+
+ return set_value_ascii(dest, header, header_mask, buf);
+ }
+
+ template <typename String, typename Header>
+ PUGI__FN bool set_value_convert(String& dest, Header& header, uintptr_t header_mask, double value, int precision)
+ {
+ char buf[128];
+ PUGI__SNPRINTF(buf, "%.*g", precision, value);
+
+ return set_value_ascii(dest, header, header_mask, buf);
+ }
+
+ template <typename String, typename Header>
+ PUGI__FN bool set_value_bool(String& dest, Header& header, uintptr_t header_mask, bool value)
+ {
+ return strcpy_insitu(dest, header, header_mask, value ? PUGIXML_TEXT("true") : PUGIXML_TEXT("false"), value ? 4 : 5);
+ }
+
+ PUGI__FN xml_parse_result load_buffer_impl(xml_document_struct* doc, xml_node_struct* root, void* contents, size_t size, unsigned int options, xml_encoding encoding, bool is_mutable, bool own, char_t** out_buffer)
+ {
+ // check input buffer
+ if (!contents && size) return make_parse_result(status_io_error);
+
+ // get actual encoding
+ xml_encoding buffer_encoding = impl::get_buffer_encoding(encoding, contents, size);
+
+ // get private buffer
+ char_t* buffer = 0;
+ size_t length = 0;
+
+ // coverity[var_deref_model]
+ if (!impl::convert_buffer(buffer, length, buffer_encoding, contents, size, is_mutable)) return impl::make_parse_result(status_out_of_memory);
+
+ // delete original buffer if we performed a conversion
+ if (own && buffer != contents && contents) impl::xml_memory::deallocate(contents);
+
+ // grab onto buffer if it's our buffer, user is responsible for deallocating contents himself
+ if (own || buffer != contents) *out_buffer = buffer;
+
+ // store buffer for offset_debug
+ doc->buffer = buffer;
+
+ // parse
+ xml_parse_result res = impl::xml_parser::parse(buffer, length, doc, root, options);
+
+ // remember encoding
+ res.encoding = buffer_encoding;
+
+ return res;
+ }
+
+ // we need to get length of entire file to load it in memory; the only (relatively) sane way to do it is via seek/tell trick
+ PUGI__FN xml_parse_status get_file_size(FILE* file, size_t& out_result)
+ {
+ #if defined(PUGI__MSVC_CRT_VERSION) && PUGI__MSVC_CRT_VERSION >= 1400
+ // there are 64-bit versions of fseek/ftell, let's use them
+ typedef __int64 length_type;
+
+ _fseeki64(file, 0, SEEK_END);
+ length_type length = _ftelli64(file);
+ _fseeki64(file, 0, SEEK_SET);
+ #elif defined(__MINGW32__) && !defined(__NO_MINGW_LFS) && (!defined(__STRICT_ANSI__) || defined(__MINGW64_VERSION_MAJOR))
+ // there are 64-bit versions of fseek/ftell, let's use them
+ typedef off64_t length_type;
+
+ fseeko64(file, 0, SEEK_END);
+ length_type length = ftello64(file);
+ fseeko64(file, 0, SEEK_SET);
+ #else
+ // if this is a 32-bit OS, long is enough; if this is a unix system, long is 64-bit, which is enough; otherwise we can't do anything anyway.
+ typedef long length_type;
+
+ fseek(file, 0, SEEK_END);
+ length_type length = ftell(file);
+ fseek(file, 0, SEEK_SET);
+ #endif
+
+ // check for I/O errors
+ if (length < 0) return status_io_error;
+
+ // check for overflow
+ size_t result = static_cast<size_t>(length);
+
+ if (static_cast<length_type>(result) != length) return status_out_of_memory;
+
+ // finalize
+ out_result = result;
+
+ return status_ok;
+ }
+
+ // This function assumes that buffer has extra sizeof(char_t) writable bytes after size
+ PUGI__FN size_t zero_terminate_buffer(void* buffer, size_t size, xml_encoding encoding)
+ {
+ // We only need to zero-terminate if encoding conversion does not do it for us
+ #ifdef PUGIXML_WCHAR_MODE
+ xml_encoding wchar_encoding = get_wchar_encoding();
+
+ if (encoding == wchar_encoding || need_endian_swap_utf(encoding, wchar_encoding))
+ {
+ size_t length = size / sizeof(char_t);
+
+ static_cast<char_t*>(buffer)[length] = 0;
+ return (length + 1) * sizeof(char_t);
+ }
+ #else
+ if (encoding == encoding_utf8)
+ {
+ static_cast<char*>(buffer)[size] = 0;
+ return size + 1;
+ }
+ #endif
+
+ return size;
+ }
+
+ PUGI__FN xml_parse_result load_file_impl(xml_document_struct* doc, FILE* file, unsigned int options, xml_encoding encoding, char_t** out_buffer)
+ {
+ if (!file) return make_parse_result(status_file_not_found);
+
+ // get file size (can result in I/O errors)
+ size_t size = 0;
+ xml_parse_status size_status = get_file_size(file, size);
+ if (size_status != status_ok) return make_parse_result(size_status);
+
+ size_t max_suffix_size = sizeof(char_t);
+
+ // allocate buffer for the whole file
+ char* contents = static_cast<char*>(xml_memory::allocate(size + max_suffix_size));
+ if (!contents) return make_parse_result(status_out_of_memory);
+
+ // read file in memory
+ size_t read_size = fread(contents, 1, size, file);
+
+ if (read_size != size)
+ {
+ xml_memory::deallocate(contents);
+ return make_parse_result(status_io_error);
+ }
+
+ xml_encoding real_encoding = get_buffer_encoding(encoding, contents, size);
+
+ return load_buffer_impl(doc, doc, contents, zero_terminate_buffer(contents, size, real_encoding), options, real_encoding, true, true, out_buffer);
+ }
+
+ PUGI__FN void close_file(FILE* file)
+ {
+ fclose(file);
+ }
+
+#ifndef PUGIXML_NO_STL
+ template <typename T> struct xml_stream_chunk
+ {
+ static xml_stream_chunk* create()
+ {
+ void* memory = xml_memory::allocate(sizeof(xml_stream_chunk));
+ if (!memory) return 0;
+
+ return new (memory) xml_stream_chunk();
+ }
+
+ static void destroy(xml_stream_chunk* chunk)
+ {
+ // free chunk chain
+ while (chunk)
+ {
+ xml_stream_chunk* next_ = chunk->next;
+
+ xml_memory::deallocate(chunk);
+
+ chunk = next_;
+ }
+ }
+
+ xml_stream_chunk(): next(0), size(0)
+ {
+ }
+
+ xml_stream_chunk* next;
+ size_t size;
+
+ T data[xml_memory_page_size / sizeof(T)];
+ };
+
+ template <typename T> PUGI__FN xml_parse_status load_stream_data_noseek(std::basic_istream<T>& stream, void** out_buffer, size_t* out_size)
+ {
+ auto_deleter<xml_stream_chunk<T> > chunks(0, xml_stream_chunk<T>::destroy);
+
+ // read file to a chunk list
+ size_t total = 0;
+ xml_stream_chunk<T>* last = 0;
+
+ while (!stream.eof())
+ {
+ // allocate new chunk
+ xml_stream_chunk<T>* chunk = xml_stream_chunk<T>::create();
+ if (!chunk) return status_out_of_memory;
+
+ // append chunk to list
+ if (last) last = last->next = chunk;
+ else chunks.data = last = chunk;
+
+ // read data to chunk
+ stream.read(chunk->data, static_cast<std::streamsize>(sizeof(chunk->data) / sizeof(T)));
+ chunk->size = static_cast<size_t>(stream.gcount()) * sizeof(T);
+
+ // read may set failbit | eofbit in case gcount() is less than read length, so check for other I/O errors
+ if (stream.bad() || (!stream.eof() && stream.fail())) return status_io_error;
+
+ // guard against huge files (chunk size is small enough to make this overflow check work)
+ if (total + chunk->size < total) return status_out_of_memory;
+ total += chunk->size;
+ }
+
+ size_t max_suffix_size = sizeof(char_t);
+
+ // copy chunk list to a contiguous buffer
+ char* buffer = static_cast<char*>(xml_memory::allocate(total + max_suffix_size));
+ if (!buffer) return status_out_of_memory;
+
+ char* write = buffer;
+
+ for (xml_stream_chunk<T>* chunk = chunks.data; chunk; chunk = chunk->next)
+ {
+ assert(write + chunk->size <= buffer + total);
+ memcpy(write, chunk->data, chunk->size);
+ write += chunk->size;
+ }
+
+ assert(write == buffer + total);
+
+ // return buffer
+ *out_buffer = buffer;
+ *out_size = total;
+
+ return status_ok;
+ }
+
+ template <typename T> PUGI__FN xml_parse_status load_stream_data_seek(std::basic_istream<T>& stream, void** out_buffer, size_t* out_size)
+ {
+ // get length of remaining data in stream
+ typename std::basic_istream<T>::pos_type pos = stream.tellg();
+ stream.seekg(0, std::ios::end);
+ std::streamoff length = stream.tellg() - pos;
+ stream.seekg(pos);
+
+ if (stream.fail() || pos < 0) return status_io_error;
+
+ // guard against huge files
+ size_t read_length = static_cast<size_t>(length);
+
+ if (static_cast<std::streamsize>(read_length) != length || length < 0) return status_out_of_memory;
+
+ size_t max_suffix_size = sizeof(char_t);
+
+ // read stream data into memory (guard against stream exceptions with buffer holder)
+ auto_deleter<void> buffer(xml_memory::allocate(read_length * sizeof(T) + max_suffix_size), xml_memory::deallocate);
+ if (!buffer.data) return status_out_of_memory;
+
+ stream.read(static_cast<T*>(buffer.data), static_cast<std::streamsize>(read_length));
+
+ // read may set failbit | eofbit in case gcount() is less than read_length (i.e. line ending conversion), so check for other I/O errors
+ if (stream.bad() || (!stream.eof() && stream.fail())) return status_io_error;
+
+ // return buffer
+ size_t actual_length = static_cast<size_t>(stream.gcount());
+ assert(actual_length <= read_length);
+
+ *out_buffer = buffer.release();
+ *out_size = actual_length * sizeof(T);
+
+ return status_ok;
+ }
+
+ template <typename T> PUGI__FN xml_parse_result load_stream_impl(xml_document_struct* doc, std::basic_istream<T>& stream, unsigned int options, xml_encoding encoding, char_t** out_buffer)
+ {
+ void* buffer = 0;
+ size_t size = 0;
+ xml_parse_status status = status_ok;
+
+ // if stream has an error bit set, bail out (otherwise tellg() can fail and we'll clear error bits)
+ if (stream.fail()) return make_parse_result(status_io_error);
+
+ // load stream to memory (using seek-based implementation if possible, since it's faster and takes less memory)
+ if (stream.tellg() < 0)
+ {
+ stream.clear(); // clear error flags that could be set by a failing tellg
+ status = load_stream_data_noseek(stream, &buffer, &size);
+ }
+ else
+ status = load_stream_data_seek(stream, &buffer, &size);
+
+ if (status != status_ok) return make_parse_result(status);
+
+ xml_encoding real_encoding = get_buffer_encoding(encoding, buffer, size);
+
+ return load_buffer_impl(doc, doc, buffer, zero_terminate_buffer(buffer, size, real_encoding), options, real_encoding, true, true, out_buffer);
+ }
+#endif
+
+#if defined(PUGI__MSVC_CRT_VERSION) || defined(__BORLANDC__) || (defined(__MINGW32__) && (!defined(__STRICT_ANSI__) || defined(__MINGW64_VERSION_MAJOR)))
+ PUGI__FN FILE* open_file_wide(const wchar_t* path, const wchar_t* mode)
+ {
+#if defined(PUGI__MSVC_CRT_VERSION) && PUGI__MSVC_CRT_VERSION >= 1400
+ FILE* file = 0;
+ return _wfopen_s(&file, path, mode) == 0 ? file : 0;
+#else
+ return _wfopen(path, mode);
+#endif
+ }
+#else
+ PUGI__FN char* convert_path_heap(const wchar_t* str)
+ {
+ assert(str);
+
+ // first pass: get length in utf8 characters
+ size_t length = strlength_wide(str);
+ size_t size = as_utf8_begin(str, length);
+
+ // allocate resulting string
+ char* result = static_cast<char*>(xml_memory::allocate(size + 1));
+ if (!result) return 0;
+
+ // second pass: convert to utf8
+ as_utf8_end(result, size, str, length);
+
+ // zero-terminate
+ result[size] = 0;
+
+ return result;
+ }
+
+ PUGI__FN FILE* open_file_wide(const wchar_t* path, const wchar_t* mode)
+ {
+ // there is no standard function to open wide paths, so our best bet is to try utf8 path
+ char* path_utf8 = convert_path_heap(path);
+ if (!path_utf8) return 0;
+
+ // convert mode to ASCII (we mirror _wfopen interface)
+ char mode_ascii[4] = {0};
+ for (size_t i = 0; mode[i]; ++i) mode_ascii[i] = static_cast<char>(mode[i]);
+
+ // try to open the utf8 path
+ FILE* result = fopen(path_utf8, mode_ascii);
+
+ // free dummy buffer
+ xml_memory::deallocate(path_utf8);
+
+ return result;
+ }
+#endif
+
+ PUGI__FN FILE* open_file(const char* path, const char* mode)
+ {
+#if defined(PUGI__MSVC_CRT_VERSION) && PUGI__MSVC_CRT_VERSION >= 1400
+ FILE* file = 0;
+ return fopen_s(&file, path, mode) == 0 ? file : 0;
+#else
+ return fopen(path, mode);
+#endif
+ }
+
+ PUGI__FN bool save_file_impl(const xml_document& doc, FILE* file, const char_t* indent, unsigned int flags, xml_encoding encoding)
+ {
+ if (!file) return false;
+
+ xml_writer_file writer(file);
+ doc.save(writer, indent, flags, encoding);
+
+ return ferror(file) == 0;
+ }
+
+ struct name_null_sentry
+ {
+ xml_node_struct* node;
+ char_t* name;
+
+ name_null_sentry(xml_node_struct* node_): node(node_), name(node_->name)
+ {
+ node->name = 0;
+ }
+
+ ~name_null_sentry()
+ {
+ node->name = name;
+ }
+ };
+PUGI__NS_END
+
+namespace pugi
+{
+ PUGI__FN xml_writer_file::xml_writer_file(void* file_): file(file_)
+ {
+ }
+
+ PUGI__FN void xml_writer_file::write(const void* data, size_t size)
+ {
+ size_t result = fwrite(data, 1, size, static_cast<FILE*>(file));
+ (void)!result; // unfortunately we can't do proper error handling here
+ }
+
+#ifndef PUGIXML_NO_STL
+ PUGI__FN xml_writer_stream::xml_writer_stream(std::basic_ostream<char, std::char_traits<char> >& stream): narrow_stream(&stream), wide_stream(0)
+ {
+ }
+
+ PUGI__FN xml_writer_stream::xml_writer_stream(std::basic_ostream<wchar_t, std::char_traits<wchar_t> >& stream): narrow_stream(0), wide_stream(&stream)
+ {
+ }
+
+ PUGI__FN void xml_writer_stream::write(const void* data, size_t size)
+ {
+ if (narrow_stream)
+ {
+ assert(!wide_stream);
+ narrow_stream->write(reinterpret_cast<const char*>(data), static_cast<std::streamsize>(size));
+ }
+ else
+ {
+ assert(wide_stream);
+ assert(size % sizeof(wchar_t) == 0);
+
+ wide_stream->write(reinterpret_cast<const wchar_t*>(data), static_cast<std::streamsize>(size / sizeof(wchar_t)));
+ }
+ }
+#endif
+
+ PUGI__FN xml_tree_walker::xml_tree_walker(): _depth(0)
+ {
+ }
+
+ PUGI__FN xml_tree_walker::~xml_tree_walker()
+ {
+ }
+
+ PUGI__FN int xml_tree_walker::depth() const
+ {
+ return _depth;
+ }
+
+ PUGI__FN bool xml_tree_walker::begin(xml_node&)
+ {
+ return true;
+ }
+
+ PUGI__FN bool xml_tree_walker::end(xml_node&)
+ {
+ return true;
+ }
+
+ PUGI__FN xml_attribute::xml_attribute(): _attr(0)
+ {
+ }
+
+ PUGI__FN xml_attribute::xml_attribute(xml_attribute_struct* attr): _attr(attr)
+ {
+ }
+
+ PUGI__FN static void unspecified_bool_xml_attribute(xml_attribute***)
+ {
+ }
+
+ PUGI__FN xml_attribute::operator xml_attribute::unspecified_bool_type() const
+ {
+ return _attr ? unspecified_bool_xml_attribute : 0;
+ }
+
+ PUGI__FN bool xml_attribute::operator!() const
+ {
+ return !_attr;
+ }
+
+ PUGI__FN bool xml_attribute::operator==(const xml_attribute& r) const
+ {
+ return (_attr == r._attr);
+ }
+
+ PUGI__FN bool xml_attribute::operator!=(const xml_attribute& r) const
+ {
+ return (_attr != r._attr);
+ }
+
+ PUGI__FN bool xml_attribute::operator<(const xml_attribute& r) const
+ {
+ return (_attr < r._attr);
+ }
+
+ PUGI__FN bool xml_attribute::operator>(const xml_attribute& r) const
+ {
+ return (_attr > r._attr);
+ }
+
+ PUGI__FN bool xml_attribute::operator<=(const xml_attribute& r) const
+ {
+ return (_attr <= r._attr);
+ }
+
+ PUGI__FN bool xml_attribute::operator>=(const xml_attribute& r) const
+ {
+ return (_attr >= r._attr);
+ }
+
+ PUGI__FN xml_attribute xml_attribute::next_attribute() const
+ {
+ return _attr ? xml_attribute(_attr->next_attribute) : xml_attribute();
+ }
+
+ PUGI__FN xml_attribute xml_attribute::previous_attribute() const
+ {
+ return _attr && _attr->prev_attribute_c->next_attribute ? xml_attribute(_attr->prev_attribute_c) : xml_attribute();
+ }
+
+ PUGI__FN const char_t* xml_attribute::as_string(const char_t* def) const
+ {
+ return (_attr && _attr->value) ? _attr->value + 0 : def;
+ }
+
+ PUGI__FN int xml_attribute::as_int(int def) const
+ {
+ return (_attr && _attr->value) ? impl::get_value_int(_attr->value) : def;
+ }
+
+ PUGI__FN unsigned int xml_attribute::as_uint(unsigned int def) const
+ {
+ return (_attr && _attr->value) ? impl::get_value_uint(_attr->value) : def;
+ }
+
+ PUGI__FN double xml_attribute::as_double(double def) const
+ {
+ return (_attr && _attr->value) ? impl::get_value_double(_attr->value) : def;
+ }
+
+ PUGI__FN float xml_attribute::as_float(float def) const
+ {
+ return (_attr && _attr->value) ? impl::get_value_float(_attr->value) : def;
+ }
+
+ PUGI__FN bool xml_attribute::as_bool(bool def) const
+ {
+ return (_attr && _attr->value) ? impl::get_value_bool(_attr->value) : def;
+ }
+
+#ifdef PUGIXML_HAS_LONG_LONG
+ PUGI__FN long long xml_attribute::as_llong(long long def) const
+ {
+ return (_attr && _attr->value) ? impl::get_value_llong(_attr->value) : def;
+ }
+
+ PUGI__FN unsigned long long xml_attribute::as_ullong(unsigned long long def) const
+ {
+ return (_attr && _attr->value) ? impl::get_value_ullong(_attr->value) : def;
+ }
+#endif
+
+ PUGI__FN bool xml_attribute::empty() const
+ {
+ return !_attr;
+ }
+
+ PUGI__FN const char_t* xml_attribute::name() const
+ {
+ return (_attr && _attr->name) ? _attr->name + 0 : PUGIXML_TEXT("");
+ }
+
+ PUGI__FN const char_t* xml_attribute::value() const
+ {
+ return (_attr && _attr->value) ? _attr->value + 0 : PUGIXML_TEXT("");
+ }
+
+ PUGI__FN size_t xml_attribute::hash_value() const
+ {
+ return static_cast<size_t>(reinterpret_cast<uintptr_t>(_attr) / sizeof(xml_attribute_struct));
+ }
+
+ PUGI__FN xml_attribute_struct* xml_attribute::internal_object() const
+ {
+ return _attr;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute::operator=(const char_t* rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute::operator=(int rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute::operator=(unsigned int rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute::operator=(long rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute::operator=(unsigned long rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute::operator=(double rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute::operator=(float rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute::operator=(bool rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+
+#ifdef PUGIXML_HAS_LONG_LONG
+ PUGI__FN xml_attribute& xml_attribute::operator=(long long rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute::operator=(unsigned long long rhs)
+ {
+ set_value(rhs);
+ return *this;
+ }
+#endif
+
+ PUGI__FN bool xml_attribute::set_name(const char_t* rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::strcpy_insitu(_attr->name, _attr->header, impl::xml_memory_page_name_allocated_mask, rhs, impl::strlength(rhs));
+ }
+
+ PUGI__FN bool xml_attribute::set_value(const char_t* rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::strcpy_insitu(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, impl::strlength(rhs));
+ }
+
+ PUGI__FN bool xml_attribute::set_value(int rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_integer<unsigned int>(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0);
+ }
+
+ PUGI__FN bool xml_attribute::set_value(unsigned int rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_integer<unsigned int>(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, false);
+ }
+
+ PUGI__FN bool xml_attribute::set_value(long rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_integer<unsigned long>(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0);
+ }
+
+ PUGI__FN bool xml_attribute::set_value(unsigned long rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_integer<unsigned long>(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, false);
+ }
+
+ PUGI__FN bool xml_attribute::set_value(double rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_convert(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, default_double_precision);
+ }
+
+ PUGI__FN bool xml_attribute::set_value(double rhs, int precision)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_convert(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, precision);
+ }
+
+ PUGI__FN bool xml_attribute::set_value(float rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_convert(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, default_float_precision);
+ }
+
+ PUGI__FN bool xml_attribute::set_value(float rhs, int precision)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_convert(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, precision);
+ }
+
+ PUGI__FN bool xml_attribute::set_value(bool rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_bool(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs);
+ }
+
+#ifdef PUGIXML_HAS_LONG_LONG
+ PUGI__FN bool xml_attribute::set_value(long long rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_integer<unsigned long long>(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0);
+ }
+
+ PUGI__FN bool xml_attribute::set_value(unsigned long long rhs)
+ {
+ if (!_attr) return false;
+
+ return impl::set_value_integer<unsigned long long>(_attr->value, _attr->header, impl::xml_memory_page_value_allocated_mask, rhs, false);
+ }
+#endif
+
+#ifdef __BORLANDC__
+ PUGI__FN bool operator&&(const xml_attribute& lhs, bool rhs)
+ {
+ return (bool)lhs && rhs;
+ }
+
+ PUGI__FN bool operator||(const xml_attribute& lhs, bool rhs)
+ {
+ return (bool)lhs || rhs;
+ }
+#endif
+
+ PUGI__FN xml_node::xml_node(): _root(0)
+ {
+ }
+
+ PUGI__FN xml_node::xml_node(xml_node_struct* p): _root(p)
+ {
+ }
+
+ PUGI__FN static void unspecified_bool_xml_node(xml_node***)
+ {
+ }
+
+ PUGI__FN xml_node::operator xml_node::unspecified_bool_type() const
+ {
+ return _root ? unspecified_bool_xml_node : 0;
+ }
+
+ PUGI__FN bool xml_node::operator!() const
+ {
+ return !_root;
+ }
+
+ PUGI__FN xml_node::iterator xml_node::begin() const
+ {
+ return iterator(_root ? _root->first_child + 0 : 0, _root);
+ }
+
+ PUGI__FN xml_node::iterator xml_node::end() const
+ {
+ return iterator(0, _root);
+ }
+
+ PUGI__FN xml_node::attribute_iterator xml_node::attributes_begin() const
+ {
+ return attribute_iterator(_root ? _root->first_attribute + 0 : 0, _root);
+ }
+
+ PUGI__FN xml_node::attribute_iterator xml_node::attributes_end() const
+ {
+ return attribute_iterator(0, _root);
+ }
+
+ PUGI__FN xml_object_range<xml_node_iterator> xml_node::children() const
+ {
+ return xml_object_range<xml_node_iterator>(begin(), end());
+ }
+
+ PUGI__FN xml_object_range<xml_named_node_iterator> xml_node::children(const char_t* name_) const
+ {
+ return xml_object_range<xml_named_node_iterator>(xml_named_node_iterator(child(name_)._root, _root, name_), xml_named_node_iterator(0, _root, name_));
+ }
+
+ PUGI__FN xml_object_range<xml_attribute_iterator> xml_node::attributes() const
+ {
+ return xml_object_range<xml_attribute_iterator>(attributes_begin(), attributes_end());
+ }
+
+ PUGI__FN bool xml_node::operator==(const xml_node& r) const
+ {
+ return (_root == r._root);
+ }
+
+ PUGI__FN bool xml_node::operator!=(const xml_node& r) const
+ {
+ return (_root != r._root);
+ }
+
+ PUGI__FN bool xml_node::operator<(const xml_node& r) const
+ {
+ return (_root < r._root);
+ }
+
+ PUGI__FN bool xml_node::operator>(const xml_node& r) const
+ {
+ return (_root > r._root);
+ }
+
+ PUGI__FN bool xml_node::operator<=(const xml_node& r) const
+ {
+ return (_root <= r._root);
+ }
+
+ PUGI__FN bool xml_node::operator>=(const xml_node& r) const
+ {
+ return (_root >= r._root);
+ }
+
+ PUGI__FN bool xml_node::empty() const
+ {
+ return !_root;
+ }
+
+ PUGI__FN const char_t* xml_node::name() const
+ {
+ return (_root && _root->name) ? _root->name + 0 : PUGIXML_TEXT("");
+ }
+
+ PUGI__FN xml_node_type xml_node::type() const
+ {
+ return _root ? PUGI__NODETYPE(_root) : node_null;
+ }
+
+ PUGI__FN const char_t* xml_node::value() const
+ {
+ return (_root && _root->value) ? _root->value + 0 : PUGIXML_TEXT("");
+ }
+
+ PUGI__FN xml_node xml_node::child(const char_t* name_) const
+ {
+ if (!_root) return xml_node();
+
+ for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling)
+ if (i->name && impl::strequal(name_, i->name)) return xml_node(i);
+
+ return xml_node();
+ }
+
+ PUGI__FN xml_attribute xml_node::attribute(const char_t* name_) const
+ {
+ if (!_root) return xml_attribute();
+
+ for (xml_attribute_struct* i = _root->first_attribute; i; i = i->next_attribute)
+ if (i->name && impl::strequal(name_, i->name))
+ return xml_attribute(i);
+
+ return xml_attribute();
+ }
+
+ PUGI__FN xml_node xml_node::next_sibling(const char_t* name_) const
+ {
+ if (!_root) return xml_node();
+
+ for (xml_node_struct* i = _root->next_sibling; i; i = i->next_sibling)
+ if (i->name && impl::strequal(name_, i->name)) return xml_node(i);
+
+ return xml_node();
+ }
+
+ PUGI__FN xml_node xml_node::next_sibling() const
+ {
+ return _root ? xml_node(_root->next_sibling) : xml_node();
+ }
+
+ PUGI__FN xml_node xml_node::previous_sibling(const char_t* name_) const
+ {
+ if (!_root) return xml_node();
+
+ for (xml_node_struct* i = _root->prev_sibling_c; i->next_sibling; i = i->prev_sibling_c)
+ if (i->name && impl::strequal(name_, i->name)) return xml_node(i);
+
+ return xml_node();
+ }
+
+ PUGI__FN xml_attribute xml_node::attribute(const char_t* name_, xml_attribute& hint_) const
+ {
+ xml_attribute_struct* hint = hint_._attr;
+
+ // if hint is not an attribute of node, behavior is not defined
+ assert(!hint || (_root && impl::is_attribute_of(hint, _root)));
+
+ if (!_root) return xml_attribute();
+
+ // optimistically search from hint up until the end
+ for (xml_attribute_struct* i = hint; i; i = i->next_attribute)
+ if (i->name && impl::strequal(name_, i->name))
+ {
+ // update hint to maximize efficiency of searching for consecutive attributes
+ hint_._attr = i->next_attribute;
+
+ return xml_attribute(i);
+ }
+
+ // wrap around and search from the first attribute until the hint
+ // 'j' null pointer check is technically redundant, but it prevents a crash in case the assertion above fails
+ for (xml_attribute_struct* j = _root->first_attribute; j && j != hint; j = j->next_attribute)
+ if (j->name && impl::strequal(name_, j->name))
+ {
+ // update hint to maximize efficiency of searching for consecutive attributes
+ hint_._attr = j->next_attribute;
+
+ return xml_attribute(j);
+ }
+
+ return xml_attribute();
+ }
+
+ PUGI__FN xml_node xml_node::previous_sibling() const
+ {
+ if (!_root) return xml_node();
+
+ if (_root->prev_sibling_c->next_sibling) return xml_node(_root->prev_sibling_c);
+ else return xml_node();
+ }
+
+ PUGI__FN xml_node xml_node::parent() const
+ {
+ return _root ? xml_node(_root->parent) : xml_node();
+ }
+
+ PUGI__FN xml_node xml_node::root() const
+ {
+ return _root ? xml_node(&impl::get_document(_root)) : xml_node();
+ }
+
+ PUGI__FN xml_text xml_node::text() const
+ {
+ return xml_text(_root);
+ }
+
+ PUGI__FN const char_t* xml_node::child_value() const
+ {
+ if (!_root) return PUGIXML_TEXT("");
+
+ // element nodes can have value if parse_embed_pcdata was used
+ if (PUGI__NODETYPE(_root) == node_element && _root->value)
+ return _root->value;
+
+ for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling)
+ if (impl::is_text_node(i) && i->value)
+ return i->value;
+
+ return PUGIXML_TEXT("");
+ }
+
+ PUGI__FN const char_t* xml_node::child_value(const char_t* name_) const
+ {
+ return child(name_).child_value();
+ }
+
+ PUGI__FN xml_attribute xml_node::first_attribute() const
+ {
+ return _root ? xml_attribute(_root->first_attribute) : xml_attribute();
+ }
+
+ PUGI__FN xml_attribute xml_node::last_attribute() const
+ {
+ return _root && _root->first_attribute ? xml_attribute(_root->first_attribute->prev_attribute_c) : xml_attribute();
+ }
+
+ PUGI__FN xml_node xml_node::first_child() const
+ {
+ return _root ? xml_node(_root->first_child) : xml_node();
+ }
+
+ PUGI__FN xml_node xml_node::last_child() const
+ {
+ return _root && _root->first_child ? xml_node(_root->first_child->prev_sibling_c) : xml_node();
+ }
+
+ PUGI__FN bool xml_node::set_name(const char_t* rhs)
+ {
+ xml_node_type type_ = _root ? PUGI__NODETYPE(_root) : node_null;
+
+ if (type_ != node_element && type_ != node_pi && type_ != node_declaration)
+ return false;
+
+ return impl::strcpy_insitu(_root->name, _root->header, impl::xml_memory_page_name_allocated_mask, rhs, impl::strlength(rhs));
+ }
+
+ PUGI__FN bool xml_node::set_value(const char_t* rhs)
+ {
+ xml_node_type type_ = _root ? PUGI__NODETYPE(_root) : node_null;
+
+ if (type_ != node_pcdata && type_ != node_cdata && type_ != node_comment && type_ != node_pi && type_ != node_doctype)
+ return false;
+
+ return impl::strcpy_insitu(_root->value, _root->header, impl::xml_memory_page_value_allocated_mask, rhs, impl::strlength(rhs));
+ }
+
+ PUGI__FN xml_attribute xml_node::append_attribute(const char_t* name_)
+ {
+ if (!impl::allow_insert_attribute(type())) return xml_attribute();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_attribute();
+
+ xml_attribute a(impl::allocate_attribute(alloc));
+ if (!a) return xml_attribute();
+
+ impl::append_attribute(a._attr, _root);
+
+ a.set_name(name_);
+
+ return a;
+ }
+
+ PUGI__FN xml_attribute xml_node::prepend_attribute(const char_t* name_)
+ {
+ if (!impl::allow_insert_attribute(type())) return xml_attribute();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_attribute();
+
+ xml_attribute a(impl::allocate_attribute(alloc));
+ if (!a) return xml_attribute();
+
+ impl::prepend_attribute(a._attr, _root);
+
+ a.set_name(name_);
+
+ return a;
+ }
+
+ PUGI__FN xml_attribute xml_node::insert_attribute_after(const char_t* name_, const xml_attribute& attr)
+ {
+ if (!impl::allow_insert_attribute(type())) return xml_attribute();
+ if (!attr || !impl::is_attribute_of(attr._attr, _root)) return xml_attribute();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_attribute();
+
+ xml_attribute a(impl::allocate_attribute(alloc));
+ if (!a) return xml_attribute();
+
+ impl::insert_attribute_after(a._attr, attr._attr, _root);
+
+ a.set_name(name_);
+
+ return a;
+ }
+
+ PUGI__FN xml_attribute xml_node::insert_attribute_before(const char_t* name_, const xml_attribute& attr)
+ {
+ if (!impl::allow_insert_attribute(type())) return xml_attribute();
+ if (!attr || !impl::is_attribute_of(attr._attr, _root)) return xml_attribute();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_attribute();
+
+ xml_attribute a(impl::allocate_attribute(alloc));
+ if (!a) return xml_attribute();
+
+ impl::insert_attribute_before(a._attr, attr._attr, _root);
+
+ a.set_name(name_);
+
+ return a;
+ }
+
+ PUGI__FN xml_attribute xml_node::append_copy(const xml_attribute& proto)
+ {
+ if (!proto) return xml_attribute();
+ if (!impl::allow_insert_attribute(type())) return xml_attribute();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_attribute();
+
+ xml_attribute a(impl::allocate_attribute(alloc));
+ if (!a) return xml_attribute();
+
+ impl::append_attribute(a._attr, _root);
+ impl::node_copy_attribute(a._attr, proto._attr);
+
+ return a;
+ }
+
+ PUGI__FN xml_attribute xml_node::prepend_copy(const xml_attribute& proto)
+ {
+ if (!proto) return xml_attribute();
+ if (!impl::allow_insert_attribute(type())) return xml_attribute();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_attribute();
+
+ xml_attribute a(impl::allocate_attribute(alloc));
+ if (!a) return xml_attribute();
+
+ impl::prepend_attribute(a._attr, _root);
+ impl::node_copy_attribute(a._attr, proto._attr);
+
+ return a;
+ }
+
+ PUGI__FN xml_attribute xml_node::insert_copy_after(const xml_attribute& proto, const xml_attribute& attr)
+ {
+ if (!proto) return xml_attribute();
+ if (!impl::allow_insert_attribute(type())) return xml_attribute();
+ if (!attr || !impl::is_attribute_of(attr._attr, _root)) return xml_attribute();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_attribute();
+
+ xml_attribute a(impl::allocate_attribute(alloc));
+ if (!a) return xml_attribute();
+
+ impl::insert_attribute_after(a._attr, attr._attr, _root);
+ impl::node_copy_attribute(a._attr, proto._attr);
+
+ return a;
+ }
+
+ PUGI__FN xml_attribute xml_node::insert_copy_before(const xml_attribute& proto, const xml_attribute& attr)
+ {
+ if (!proto) return xml_attribute();
+ if (!impl::allow_insert_attribute(type())) return xml_attribute();
+ if (!attr || !impl::is_attribute_of(attr._attr, _root)) return xml_attribute();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_attribute();
+
+ xml_attribute a(impl::allocate_attribute(alloc));
+ if (!a) return xml_attribute();
+
+ impl::insert_attribute_before(a._attr, attr._attr, _root);
+ impl::node_copy_attribute(a._attr, proto._attr);
+
+ return a;
+ }
+
+ PUGI__FN xml_node xml_node::append_child(xml_node_type type_)
+ {
+ if (!impl::allow_insert_child(type(), type_)) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ xml_node n(impl::allocate_node(alloc, type_));
+ if (!n) return xml_node();
+
+ impl::append_node(n._root, _root);
+
+ if (type_ == node_declaration) n.set_name(PUGIXML_TEXT("xml"));
+
+ return n;
+ }
+
+ PUGI__FN xml_node xml_node::prepend_child(xml_node_type type_)
+ {
+ if (!impl::allow_insert_child(type(), type_)) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ xml_node n(impl::allocate_node(alloc, type_));
+ if (!n) return xml_node();
+
+ impl::prepend_node(n._root, _root);
+
+ if (type_ == node_declaration) n.set_name(PUGIXML_TEXT("xml"));
+
+ return n;
+ }
+
+ PUGI__FN xml_node xml_node::insert_child_before(xml_node_type type_, const xml_node& node)
+ {
+ if (!impl::allow_insert_child(type(), type_)) return xml_node();
+ if (!node._root || node._root->parent != _root) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ xml_node n(impl::allocate_node(alloc, type_));
+ if (!n) return xml_node();
+
+ impl::insert_node_before(n._root, node._root);
+
+ if (type_ == node_declaration) n.set_name(PUGIXML_TEXT("xml"));
+
+ return n;
+ }
+
+ PUGI__FN xml_node xml_node::insert_child_after(xml_node_type type_, const xml_node& node)
+ {
+ if (!impl::allow_insert_child(type(), type_)) return xml_node();
+ if (!node._root || node._root->parent != _root) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ xml_node n(impl::allocate_node(alloc, type_));
+ if (!n) return xml_node();
+
+ impl::insert_node_after(n._root, node._root);
+
+ if (type_ == node_declaration) n.set_name(PUGIXML_TEXT("xml"));
+
+ return n;
+ }
+
+ PUGI__FN xml_node xml_node::append_child(const char_t* name_)
+ {
+ xml_node result = append_child(node_element);
+
+ result.set_name(name_);
+
+ return result;
+ }
+
+ PUGI__FN xml_node xml_node::prepend_child(const char_t* name_)
+ {
+ xml_node result = prepend_child(node_element);
+
+ result.set_name(name_);
+
+ return result;
+ }
+
+ PUGI__FN xml_node xml_node::insert_child_after(const char_t* name_, const xml_node& node)
+ {
+ xml_node result = insert_child_after(node_element, node);
+
+ result.set_name(name_);
+
+ return result;
+ }
+
+ PUGI__FN xml_node xml_node::insert_child_before(const char_t* name_, const xml_node& node)
+ {
+ xml_node result = insert_child_before(node_element, node);
+
+ result.set_name(name_);
+
+ return result;
+ }
+
+ PUGI__FN xml_node xml_node::append_copy(const xml_node& proto)
+ {
+ xml_node_type type_ = proto.type();
+ if (!impl::allow_insert_child(type(), type_)) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ xml_node n(impl::allocate_node(alloc, type_));
+ if (!n) return xml_node();
+
+ impl::append_node(n._root, _root);
+ impl::node_copy_tree(n._root, proto._root);
+
+ return n;
+ }
+
+ PUGI__FN xml_node xml_node::prepend_copy(const xml_node& proto)
+ {
+ xml_node_type type_ = proto.type();
+ if (!impl::allow_insert_child(type(), type_)) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ xml_node n(impl::allocate_node(alloc, type_));
+ if (!n) return xml_node();
+
+ impl::prepend_node(n._root, _root);
+ impl::node_copy_tree(n._root, proto._root);
+
+ return n;
+ }
+
+ PUGI__FN xml_node xml_node::insert_copy_after(const xml_node& proto, const xml_node& node)
+ {
+ xml_node_type type_ = proto.type();
+ if (!impl::allow_insert_child(type(), type_)) return xml_node();
+ if (!node._root || node._root->parent != _root) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ xml_node n(impl::allocate_node(alloc, type_));
+ if (!n) return xml_node();
+
+ impl::insert_node_after(n._root, node._root);
+ impl::node_copy_tree(n._root, proto._root);
+
+ return n;
+ }
+
+ PUGI__FN xml_node xml_node::insert_copy_before(const xml_node& proto, const xml_node& node)
+ {
+ xml_node_type type_ = proto.type();
+ if (!impl::allow_insert_child(type(), type_)) return xml_node();
+ if (!node._root || node._root->parent != _root) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ xml_node n(impl::allocate_node(alloc, type_));
+ if (!n) return xml_node();
+
+ impl::insert_node_before(n._root, node._root);
+ impl::node_copy_tree(n._root, proto._root);
+
+ return n;
+ }
+
+ PUGI__FN xml_node xml_node::append_move(const xml_node& moved)
+ {
+ if (!impl::allow_move(*this, moved)) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ // disable document_buffer_order optimization since moving nodes around changes document order without changing buffer pointers
+ impl::get_document(_root).header |= impl::xml_memory_page_contents_shared_mask;
+
+ impl::remove_node(moved._root);
+ impl::append_node(moved._root, _root);
+
+ return moved;
+ }
+
+ PUGI__FN xml_node xml_node::prepend_move(const xml_node& moved)
+ {
+ if (!impl::allow_move(*this, moved)) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ // disable document_buffer_order optimization since moving nodes around changes document order without changing buffer pointers
+ impl::get_document(_root).header |= impl::xml_memory_page_contents_shared_mask;
+
+ impl::remove_node(moved._root);
+ impl::prepend_node(moved._root, _root);
+
+ return moved;
+ }
+
+ PUGI__FN xml_node xml_node::insert_move_after(const xml_node& moved, const xml_node& node)
+ {
+ if (!impl::allow_move(*this, moved)) return xml_node();
+ if (!node._root || node._root->parent != _root) return xml_node();
+ if (moved._root == node._root) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ // disable document_buffer_order optimization since moving nodes around changes document order without changing buffer pointers
+ impl::get_document(_root).header |= impl::xml_memory_page_contents_shared_mask;
+
+ impl::remove_node(moved._root);
+ impl::insert_node_after(moved._root, node._root);
+
+ return moved;
+ }
+
+ PUGI__FN xml_node xml_node::insert_move_before(const xml_node& moved, const xml_node& node)
+ {
+ if (!impl::allow_move(*this, moved)) return xml_node();
+ if (!node._root || node._root->parent != _root) return xml_node();
+ if (moved._root == node._root) return xml_node();
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return xml_node();
+
+ // disable document_buffer_order optimization since moving nodes around changes document order without changing buffer pointers
+ impl::get_document(_root).header |= impl::xml_memory_page_contents_shared_mask;
+
+ impl::remove_node(moved._root);
+ impl::insert_node_before(moved._root, node._root);
+
+ return moved;
+ }
+
+ PUGI__FN bool xml_node::remove_attribute(const char_t* name_)
+ {
+ return remove_attribute(attribute(name_));
+ }
+
+ PUGI__FN bool xml_node::remove_attribute(const xml_attribute& a)
+ {
+ if (!_root || !a._attr) return false;
+ if (!impl::is_attribute_of(a._attr, _root)) return false;
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return false;
+
+ impl::remove_attribute(a._attr, _root);
+ impl::destroy_attribute(a._attr, alloc);
+
+ return true;
+ }
+
+ PUGI__FN bool xml_node::remove_attributes()
+ {
+ if (!_root) return false;
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return false;
+
+ for (xml_attribute_struct* attr = _root->first_attribute; attr; )
+ {
+ xml_attribute_struct* next = attr->next_attribute;
+
+ impl::destroy_attribute(attr, alloc);
+
+ attr = next;
+ }
+
+ _root->first_attribute = 0;
+
+ return true;
+ }
+
+ PUGI__FN bool xml_node::remove_child(const char_t* name_)
+ {
+ return remove_child(child(name_));
+ }
+
+ PUGI__FN bool xml_node::remove_child(const xml_node& n)
+ {
+ if (!_root || !n._root || n._root->parent != _root) return false;
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return false;
+
+ impl::remove_node(n._root);
+ impl::destroy_node(n._root, alloc);
+
+ return true;
+ }
+
+ PUGI__FN bool xml_node::remove_children()
+ {
+ if (!_root) return false;
+
+ impl::xml_allocator& alloc = impl::get_allocator(_root);
+ if (!alloc.reserve()) return false;
+
+ for (xml_node_struct* cur = _root->first_child; cur; )
+ {
+ xml_node_struct* next = cur->next_sibling;
+
+ impl::destroy_node(cur, alloc);
+
+ cur = next;
+ }
+
+ _root->first_child = 0;
+
+ return true;
+ }
+
+ PUGI__FN xml_parse_result xml_node::append_buffer(const void* contents, size_t size, unsigned int options, xml_encoding encoding)
+ {
+ // append_buffer is only valid for elements/documents
+ if (!impl::allow_insert_child(type(), node_element)) return impl::make_parse_result(status_append_invalid_root);
+
+ // get document node
+ impl::xml_document_struct* doc = &impl::get_document(_root);
+
+ // disable document_buffer_order optimization since in a document with multiple buffers comparing buffer pointers does not make sense
+ doc->header |= impl::xml_memory_page_contents_shared_mask;
+
+ // get extra buffer element (we'll store the document fragment buffer there so that we can deallocate it later)
+ impl::xml_memory_page* page = 0;
+ impl::xml_extra_buffer* extra = static_cast<impl::xml_extra_buffer*>(doc->allocate_memory(sizeof(impl::xml_extra_buffer) + sizeof(void*), page));
+ (void)page;
+
+ if (!extra) return impl::make_parse_result(status_out_of_memory);
+
+ #ifdef PUGIXML_COMPACT
+ // align the memory block to a pointer boundary; this is required for compact mode where memory allocations are only 4b aligned
+ // note that this requires up to sizeof(void*)-1 additional memory, which the allocation above takes into account
+ extra = reinterpret_cast<impl::xml_extra_buffer*>((reinterpret_cast<uintptr_t>(extra) + (sizeof(void*) - 1)) & ~(sizeof(void*) - 1));
+ #endif
+
+ // add extra buffer to the list
+ extra->buffer = 0;
+ extra->next = doc->extra_buffers;
+ doc->extra_buffers = extra;
+
+ // name of the root has to be NULL before parsing - otherwise closing node mismatches will not be detected at the top level
+ impl::name_null_sentry sentry(_root);
+
+ return impl::load_buffer_impl(doc, _root, const_cast<void*>(contents), size, options, encoding, false, false, &extra->buffer);
+ }
+
+ PUGI__FN xml_node xml_node::find_child_by_attribute(const char_t* name_, const char_t* attr_name, const char_t* attr_value) const
+ {
+ if (!_root) return xml_node();
+
+ for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling)
+ if (i->name && impl::strequal(name_, i->name))
+ {
+ for (xml_attribute_struct* a = i->first_attribute; a; a = a->next_attribute)
+ if (a->name && impl::strequal(attr_name, a->name) && impl::strequal(attr_value, a->value ? a->value + 0 : PUGIXML_TEXT("")))
+ return xml_node(i);
+ }
+
+ return xml_node();
+ }
+
+ PUGI__FN xml_node xml_node::find_child_by_attribute(const char_t* attr_name, const char_t* attr_value) const
+ {
+ if (!_root) return xml_node();
+
+ for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling)
+ for (xml_attribute_struct* a = i->first_attribute; a; a = a->next_attribute)
+ if (a->name && impl::strequal(attr_name, a->name) && impl::strequal(attr_value, a->value ? a->value + 0 : PUGIXML_TEXT("")))
+ return xml_node(i);
+
+ return xml_node();
+ }
+
+#ifndef PUGIXML_NO_STL
+ PUGI__FN string_t xml_node::path(char_t delimiter) const
+ {
+ if (!_root) return string_t();
+
+ size_t offset = 0;
+
+ for (xml_node_struct* i = _root; i; i = i->parent)
+ {
+ offset += (i != _root);
+ offset += i->name ? impl::strlength(i->name) : 0;
+ }
+
+ string_t result;
+ result.resize(offset);
+
+ for (xml_node_struct* j = _root; j; j = j->parent)
+ {
+ if (j != _root)
+ result[--offset] = delimiter;
+
+ if (j->name)
+ {
+ size_t length = impl::strlength(j->name);
+
+ offset -= length;
+ memcpy(&result[offset], j->name, length * sizeof(char_t));
+ }
+ }
+
+ assert(offset == 0);
+
+ return result;
+ }
+#endif
+
+ PUGI__FN xml_node xml_node::first_element_by_path(const char_t* path_, char_t delimiter) const
+ {
+ xml_node context = path_[0] == delimiter ? root() : *this;
+
+ if (!context._root) return xml_node();
+
+ const char_t* path_segment = path_;
+
+ while (*path_segment == delimiter) ++path_segment;
+
+ const char_t* path_segment_end = path_segment;
+
+ while (*path_segment_end && *path_segment_end != delimiter) ++path_segment_end;
+
+ if (path_segment == path_segment_end) return context;
+
+ const char_t* next_segment = path_segment_end;
+
+ while (*next_segment == delimiter) ++next_segment;
+
+ if (*path_segment == '.' && path_segment + 1 == path_segment_end)
+ return context.first_element_by_path(next_segment, delimiter);
+ else if (*path_segment == '.' && *(path_segment+1) == '.' && path_segment + 2 == path_segment_end)
+ return context.parent().first_element_by_path(next_segment, delimiter);
+ else
+ {
+ for (xml_node_struct* j = context._root->first_child; j; j = j->next_sibling)
+ {
+ if (j->name && impl::strequalrange(j->name, path_segment, static_cast<size_t>(path_segment_end - path_segment)))
+ {
+ xml_node subsearch = xml_node(j).first_element_by_path(next_segment, delimiter);
+
+ if (subsearch) return subsearch;
+ }
+ }
+
+ return xml_node();
+ }
+ }
+
+ PUGI__FN bool xml_node::traverse(xml_tree_walker& walker)
+ {
+ walker._depth = -1;
+
+ xml_node arg_begin(_root);
+ if (!walker.begin(arg_begin)) return false;
+
+ xml_node_struct* cur = _root ? _root->first_child + 0 : 0;
+
+ if (cur)
+ {
+ ++walker._depth;
+
+ do
+ {
+ xml_node arg_for_each(cur);
+ if (!walker.for_each(arg_for_each))
+ return false;
+
+ if (cur->first_child)
+ {
+ ++walker._depth;
+ cur = cur->first_child;
+ }
+ else if (cur->next_sibling)
+ cur = cur->next_sibling;
+ else
+ {
+ while (!cur->next_sibling && cur != _root && cur->parent)
+ {
+ --walker._depth;
+ cur = cur->parent;
+ }
+
+ if (cur != _root)
+ cur = cur->next_sibling;
+ }
+ }
+ while (cur && cur != _root);
+ }
+
+ assert(walker._depth == -1);
+
+ xml_node arg_end(_root);
+ return walker.end(arg_end);
+ }
+
+ PUGI__FN size_t xml_node::hash_value() const
+ {
+ return static_cast<size_t>(reinterpret_cast<uintptr_t>(_root) / sizeof(xml_node_struct));
+ }
+
+ PUGI__FN xml_node_struct* xml_node::internal_object() const
+ {
+ return _root;
+ }
+
+ PUGI__FN void xml_node::print(xml_writer& writer, const char_t* indent, unsigned int flags, xml_encoding encoding, unsigned int depth) const
+ {
+ if (!_root) return;
+
+ impl::xml_buffered_writer buffered_writer(writer, encoding);
+
+ impl::node_output(buffered_writer, _root, indent, flags, depth);
+
+ buffered_writer.flush();
+ }
+
+#ifndef PUGIXML_NO_STL
+ PUGI__FN void xml_node::print(std::basic_ostream<char, std::char_traits<char> >& stream, const char_t* indent, unsigned int flags, xml_encoding encoding, unsigned int depth) const
+ {
+ xml_writer_stream writer(stream);
+
+ print(writer, indent, flags, encoding, depth);
+ }
+
+ PUGI__FN void xml_node::print(std::basic_ostream<wchar_t, std::char_traits<wchar_t> >& stream, const char_t* indent, unsigned int flags, unsigned int depth) const
+ {
+ xml_writer_stream writer(stream);
+
+ print(writer, indent, flags, encoding_wchar, depth);
+ }
+#endif
+
+ PUGI__FN ptrdiff_t xml_node::offset_debug() const
+ {
+ if (!_root) return -1;
+
+ impl::xml_document_struct& doc = impl::get_document(_root);
+
+ // we can determine the offset reliably only if there is exactly once parse buffer
+ if (!doc.buffer || doc.extra_buffers) return -1;
+
+ switch (type())
+ {
+ case node_document:
+ return 0;
+
+ case node_element:
+ case node_declaration:
+ case node_pi:
+ return _root->name && (_root->header & impl::xml_memory_page_name_allocated_or_shared_mask) == 0 ? _root->name - doc.buffer : -1;
+
+ case node_pcdata:
+ case node_cdata:
+ case node_comment:
+ case node_doctype:
+ return _root->value && (_root->header & impl::xml_memory_page_value_allocated_or_shared_mask) == 0 ? _root->value - doc.buffer : -1;
+
+ default:
+ assert(false && "Invalid node type"); // unreachable
+ return -1;
+ }
+ }
+
+#ifdef __BORLANDC__
+ PUGI__FN bool operator&&(const xml_node& lhs, bool rhs)
+ {
+ return (bool)lhs && rhs;
+ }
+
+ PUGI__FN bool operator||(const xml_node& lhs, bool rhs)
+ {
+ return (bool)lhs || rhs;
+ }
+#endif
+
+ PUGI__FN xml_text::xml_text(xml_node_struct* root): _root(root)
+ {
+ }
+
+ PUGI__FN xml_node_struct* xml_text::_data() const
+ {
+ if (!_root || impl::is_text_node(_root)) return _root;
+
+ // element nodes can have value if parse_embed_pcdata was used
+ if (PUGI__NODETYPE(_root) == node_element && _root->value)
+ return _root;
+
+ for (xml_node_struct* node = _root->first_child; node; node = node->next_sibling)
+ if (impl::is_text_node(node))
+ return node;
+
+ return 0;
+ }
+
+ PUGI__FN xml_node_struct* xml_text::_data_new()
+ {
+ xml_node_struct* d = _data();
+ if (d) return d;
+
+ return xml_node(_root).append_child(node_pcdata).internal_object();
+ }
+
+ PUGI__FN xml_text::xml_text(): _root(0)
+ {
+ }
+
+ PUGI__FN static void unspecified_bool_xml_text(xml_text***)
+ {
+ }
+
+ PUGI__FN xml_text::operator xml_text::unspecified_bool_type() const
+ {
+ return _data() ? unspecified_bool_xml_text : 0;
+ }
+
+ PUGI__FN bool xml_text::operator!() const
+ {
+ return !_data();
+ }
+
+ PUGI__FN bool xml_text::empty() const
+ {
+ return _data() == 0;
+ }
+
+ PUGI__FN const char_t* xml_text::get() const
+ {
+ xml_node_struct* d = _data();
+
+ return (d && d->value) ? d->value + 0 : PUGIXML_TEXT("");
+ }
+
+ PUGI__FN const char_t* xml_text::as_string(const char_t* def) const
+ {
+ xml_node_struct* d = _data();
+
+ return (d && d->value) ? d->value + 0 : def;
+ }
+
+ PUGI__FN int xml_text::as_int(int def) const
+ {
+ xml_node_struct* d = _data();
+
+ return (d && d->value) ? impl::get_value_int(d->value) : def;
+ }
+
+ PUGI__FN unsigned int xml_text::as_uint(unsigned int def) const
+ {
+ xml_node_struct* d = _data();
+
+ return (d && d->value) ? impl::get_value_uint(d->value) : def;
+ }
+
+ PUGI__FN double xml_text::as_double(double def) const
+ {
+ xml_node_struct* d = _data();
+
+ return (d && d->value) ? impl::get_value_double(d->value) : def;
+ }
+
+ PUGI__FN float xml_text::as_float(float def) const
+ {
+ xml_node_struct* d = _data();
+
+ return (d && d->value) ? impl::get_value_float(d->value) : def;
+ }
+
+ PUGI__FN bool xml_text::as_bool(bool def) const
+ {
+ xml_node_struct* d = _data();
+
+ return (d && d->value) ? impl::get_value_bool(d->value) : def;
+ }
+
+#ifdef PUGIXML_HAS_LONG_LONG
+ PUGI__FN long long xml_text::as_llong(long long def) const
+ {
+ xml_node_struct* d = _data();
+
+ return (d && d->value) ? impl::get_value_llong(d->value) : def;
+ }
+
+ PUGI__FN unsigned long long xml_text::as_ullong(unsigned long long def) const
+ {
+ xml_node_struct* d = _data();
+
+ return (d && d->value) ? impl::get_value_ullong(d->value) : def;
+ }
+#endif
+
+ PUGI__FN bool xml_text::set(const char_t* rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::strcpy_insitu(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, impl::strlength(rhs)) : false;
+ }
+
+ PUGI__FN bool xml_text::set(int rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_integer<unsigned int>(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0) : false;
+ }
+
+ PUGI__FN bool xml_text::set(unsigned int rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_integer<unsigned int>(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, false) : false;
+ }
+
+ PUGI__FN bool xml_text::set(long rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_integer<unsigned long>(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0) : false;
+ }
+
+ PUGI__FN bool xml_text::set(unsigned long rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_integer<unsigned long>(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, false) : false;
+ }
+
+ PUGI__FN bool xml_text::set(float rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_convert(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, default_float_precision) : false;
+ }
+
+ PUGI__FN bool xml_text::set(float rhs, int precision)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_convert(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, precision) : false;
+ }
+
+ PUGI__FN bool xml_text::set(double rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_convert(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, default_double_precision) : false;
+ }
+
+ PUGI__FN bool xml_text::set(double rhs, int precision)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_convert(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, precision) : false;
+ }
+
+ PUGI__FN bool xml_text::set(bool rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_bool(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs) : false;
+ }
+
+#ifdef PUGIXML_HAS_LONG_LONG
+ PUGI__FN bool xml_text::set(long long rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_integer<unsigned long long>(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, rhs < 0) : false;
+ }
+
+ PUGI__FN bool xml_text::set(unsigned long long rhs)
+ {
+ xml_node_struct* dn = _data_new();
+
+ return dn ? impl::set_value_integer<unsigned long long>(dn->value, dn->header, impl::xml_memory_page_value_allocated_mask, rhs, false) : false;
+ }
+#endif
+
+ PUGI__FN xml_text& xml_text::operator=(const char_t* rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_text& xml_text::operator=(int rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_text& xml_text::operator=(unsigned int rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_text& xml_text::operator=(long rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_text& xml_text::operator=(unsigned long rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_text& xml_text::operator=(double rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_text& xml_text::operator=(float rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_text& xml_text::operator=(bool rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+
+#ifdef PUGIXML_HAS_LONG_LONG
+ PUGI__FN xml_text& xml_text::operator=(long long rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+
+ PUGI__FN xml_text& xml_text::operator=(unsigned long long rhs)
+ {
+ set(rhs);
+ return *this;
+ }
+#endif
+
+ PUGI__FN xml_node xml_text::data() const
+ {
+ return xml_node(_data());
+ }
+
+#ifdef __BORLANDC__
+ PUGI__FN bool operator&&(const xml_text& lhs, bool rhs)
+ {
+ return (bool)lhs && rhs;
+ }
+
+ PUGI__FN bool operator||(const xml_text& lhs, bool rhs)
+ {
+ return (bool)lhs || rhs;
+ }
+#endif
+
+ PUGI__FN xml_node_iterator::xml_node_iterator()
+ {
+ }
+
+ PUGI__FN xml_node_iterator::xml_node_iterator(const xml_node& node): _wrap(node), _parent(node.parent())
+ {
+ }
+
+ PUGI__FN xml_node_iterator::xml_node_iterator(xml_node_struct* ref, xml_node_struct* parent): _wrap(ref), _parent(parent)
+ {
+ }
+
+ PUGI__FN bool xml_node_iterator::operator==(const xml_node_iterator& rhs) const
+ {
+ return _wrap._root == rhs._wrap._root && _parent._root == rhs._parent._root;
+ }
+
+ PUGI__FN bool xml_node_iterator::operator!=(const xml_node_iterator& rhs) const
+ {
+ return _wrap._root != rhs._wrap._root || _parent._root != rhs._parent._root;
+ }
+
+ PUGI__FN xml_node& xml_node_iterator::operator*() const
+ {
+ assert(_wrap._root);
+ return _wrap;
+ }
+
+ PUGI__FN xml_node* xml_node_iterator::operator->() const
+ {
+ assert(_wrap._root);
+ return const_cast<xml_node*>(&_wrap); // BCC5 workaround
+ }
+
+ PUGI__FN xml_node_iterator& xml_node_iterator::operator++()
+ {
+ assert(_wrap._root);
+ _wrap._root = _wrap._root->next_sibling;
+ return *this;
+ }
+
+ PUGI__FN xml_node_iterator xml_node_iterator::operator++(int)
+ {
+ xml_node_iterator temp = *this;
+ ++*this;
+ return temp;
+ }
+
+ PUGI__FN xml_node_iterator& xml_node_iterator::operator--()
+ {
+ _wrap = _wrap._root ? _wrap.previous_sibling() : _parent.last_child();
+ return *this;
+ }
+
+ PUGI__FN xml_node_iterator xml_node_iterator::operator--(int)
+ {
+ xml_node_iterator temp = *this;
+ --*this;
+ return temp;
+ }
+
+ PUGI__FN xml_attribute_iterator::xml_attribute_iterator()
+ {
+ }
+
+ PUGI__FN xml_attribute_iterator::xml_attribute_iterator(const xml_attribute& attr, const xml_node& parent): _wrap(attr), _parent(parent)
+ {
+ }
+
+ PUGI__FN xml_attribute_iterator::xml_attribute_iterator(xml_attribute_struct* ref, xml_node_struct* parent): _wrap(ref), _parent(parent)
+ {
+ }
+
+ PUGI__FN bool xml_attribute_iterator::operator==(const xml_attribute_iterator& rhs) const
+ {
+ return _wrap._attr == rhs._wrap._attr && _parent._root == rhs._parent._root;
+ }
+
+ PUGI__FN bool xml_attribute_iterator::operator!=(const xml_attribute_iterator& rhs) const
+ {
+ return _wrap._attr != rhs._wrap._attr || _parent._root != rhs._parent._root;
+ }
+
+ PUGI__FN xml_attribute& xml_attribute_iterator::operator*() const
+ {
+ assert(_wrap._attr);
+ return _wrap;
+ }
+
+ PUGI__FN xml_attribute* xml_attribute_iterator::operator->() const
+ {
+ assert(_wrap._attr);
+ return const_cast<xml_attribute*>(&_wrap); // BCC5 workaround
+ }
+
+ PUGI__FN xml_attribute_iterator& xml_attribute_iterator::operator++()
+ {
+ assert(_wrap._attr);
+ _wrap._attr = _wrap._attr->next_attribute;
+ return *this;
+ }
+
+ PUGI__FN xml_attribute_iterator xml_attribute_iterator::operator++(int)
+ {
+ xml_attribute_iterator temp = *this;
+ ++*this;
+ return temp;
+ }
+
+ PUGI__FN xml_attribute_iterator& xml_attribute_iterator::operator--()
+ {
+ _wrap = _wrap._attr ? _wrap.previous_attribute() : _parent.last_attribute();
+ return *this;
+ }
+
+ PUGI__FN xml_attribute_iterator xml_attribute_iterator::operator--(int)
+ {
+ xml_attribute_iterator temp = *this;
+ --*this;
+ return temp;
+ }
+
+ PUGI__FN xml_named_node_iterator::xml_named_node_iterator(): _name(0)
+ {
+ }
+
+ PUGI__FN xml_named_node_iterator::xml_named_node_iterator(const xml_node& node, const char_t* name): _wrap(node), _parent(node.parent()), _name(name)
+ {
+ }
+
+ PUGI__FN xml_named_node_iterator::xml_named_node_iterator(xml_node_struct* ref, xml_node_struct* parent, const char_t* name): _wrap(ref), _parent(parent), _name(name)
+ {
+ }
+
+ PUGI__FN bool xml_named_node_iterator::operator==(const xml_named_node_iterator& rhs) const
+ {
+ return _wrap._root == rhs._wrap._root && _parent._root == rhs._parent._root;
+ }
+
+ PUGI__FN bool xml_named_node_iterator::operator!=(const xml_named_node_iterator& rhs) const
+ {
+ return _wrap._root != rhs._wrap._root || _parent._root != rhs._parent._root;
+ }
+
+ PUGI__FN xml_node& xml_named_node_iterator::operator*() const
+ {
+ assert(_wrap._root);
+ return _wrap;
+ }
+
+ PUGI__FN xml_node* xml_named_node_iterator::operator->() const
+ {
+ assert(_wrap._root);
+ return const_cast<xml_node*>(&_wrap); // BCC5 workaround
+ }
+
+ PUGI__FN xml_named_node_iterator& xml_named_node_iterator::operator++()
+ {
+ assert(_wrap._root);
+ _wrap = _wrap.next_sibling(_name);
+ return *this;
+ }
+
+ PUGI__FN xml_named_node_iterator xml_named_node_iterator::operator++(int)
+ {
+ xml_named_node_iterator temp = *this;
+ ++*this;
+ return temp;
+ }
+
+ PUGI__FN xml_named_node_iterator& xml_named_node_iterator::operator--()
+ {
+ if (_wrap._root)
+ _wrap = _wrap.previous_sibling(_name);
+ else
+ {
+ _wrap = _parent.last_child();
+
+ if (!impl::strequal(_wrap.name(), _name))
+ _wrap = _wrap.previous_sibling(_name);
+ }
+
+ return *this;
+ }
+
+ PUGI__FN xml_named_node_iterator xml_named_node_iterator::operator--(int)
+ {
+ xml_named_node_iterator temp = *this;
+ --*this;
+ return temp;
+ }
+
+ PUGI__FN xml_parse_result::xml_parse_result(): status(status_internal_error), offset(0), encoding(encoding_auto)
+ {
+ }
+
+ PUGI__FN xml_parse_result::operator bool() const
+ {
+ return status == status_ok;
+ }
+
+ PUGI__FN const char* xml_parse_result::description() const
+ {
+ switch (status)
+ {
+ case status_ok: return "No error";
+
+ case status_file_not_found: return "File was not found";
+ case status_io_error: return "Error reading from file/stream";
+ case status_out_of_memory: return "Could not allocate memory";
+ case status_internal_error: return "Internal error occurred";
+
+ case status_unrecognized_tag: return "Could not determine tag type";
+
+ case status_bad_pi: return "Error parsing document declaration/processing instruction";
+ case status_bad_comment: return "Error parsing comment";
+ case status_bad_cdata: return "Error parsing CDATA section";
+ case status_bad_doctype: return "Error parsing document type declaration";
+ case status_bad_pcdata: return "Error parsing PCDATA section";
+ case status_bad_start_element: return "Error parsing start element tag";
+ case status_bad_attribute: return "Error parsing element attribute";
+ case status_bad_end_element: return "Error parsing end element tag";
+ case status_end_element_mismatch: return "Start-end tags mismatch";
+
+ case status_append_invalid_root: return "Unable to append nodes: root is not an element or document";
+
+ case status_no_document_element: return "No document element found";
+
+ default: return "Unknown error";
+ }
+ }
+
+ PUGI__FN xml_document::xml_document(): _buffer(0)
+ {
+ _create();
+ }
+
+ PUGI__FN xml_document::~xml_document()
+ {
+ _destroy();
+ }
+
+#ifdef PUGIXML_HAS_MOVE
+ PUGI__FN xml_document::xml_document(xml_document&& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT: _buffer(0)
+ {
+ _create();
+ _move(rhs);
+ }
+
+ PUGI__FN xml_document& xml_document::operator=(xml_document&& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT
+ {
+ if (this == &rhs) return *this;
+
+ _destroy();
+ _create();
+ _move(rhs);
+
+ return *this;
+ }
+#endif
+
+ PUGI__FN void xml_document::reset()
+ {
+ _destroy();
+ _create();
+ }
+
+ PUGI__FN void xml_document::reset(const xml_document& proto)
+ {
+ reset();
+
+ impl::node_copy_tree(_root, proto._root);
+ }
+
+ PUGI__FN void xml_document::_create()
+ {
+ assert(!_root);
+
+ #ifdef PUGIXML_COMPACT
+ // space for page marker for the first page (uint32_t), rounded up to pointer size; assumes pointers are at least 32-bit
+ const size_t page_offset = sizeof(void*);
+ #else
+ const size_t page_offset = 0;
+ #endif
+
+ // initialize sentinel page
+ PUGI__STATIC_ASSERT(sizeof(impl::xml_memory_page) + sizeof(impl::xml_document_struct) + page_offset <= sizeof(_memory));
+
+ // prepare page structure
+ impl::xml_memory_page* page = impl::xml_memory_page::construct(_memory);
+ assert(page);
+
+ page->busy_size = impl::xml_memory_page_size;
+
+ // setup first page marker
+ #ifdef PUGIXML_COMPACT
+ // round-trip through void* to avoid 'cast increases required alignment of target type' warning
+ page->compact_page_marker = reinterpret_cast<uint32_t*>(static_cast<void*>(reinterpret_cast<char*>(page) + sizeof(impl::xml_memory_page)));
+ *page->compact_page_marker = sizeof(impl::xml_memory_page);
+ #endif
+
+ // allocate new root
+ _root = new (reinterpret_cast<char*>(page) + sizeof(impl::xml_memory_page) + page_offset) impl::xml_document_struct(page);
+ _root->prev_sibling_c = _root;
+
+ // setup sentinel page
+ page->allocator = static_cast<impl::xml_document_struct*>(_root);
+
+ // setup hash table pointer in allocator
+ #ifdef PUGIXML_COMPACT
+ page->allocator->_hash = &static_cast<impl::xml_document_struct*>(_root)->hash;
+ #endif
+
+ // verify the document allocation
+ assert(reinterpret_cast<char*>(_root) + sizeof(impl::xml_document_struct) <= _memory + sizeof(_memory));
+ }
+
+ PUGI__FN void xml_document::_destroy()
+ {
+ assert(_root);
+
+ // destroy static storage
+ if (_buffer)
+ {
+ impl::xml_memory::deallocate(_buffer);
+ _buffer = 0;
+ }
+
+ // destroy extra buffers (note: no need to destroy linked list nodes, they're allocated using document allocator)
+ for (impl::xml_extra_buffer* extra = static_cast<impl::xml_document_struct*>(_root)->extra_buffers; extra; extra = extra->next)
+ {
+ if (extra->buffer) impl::xml_memory::deallocate(extra->buffer);
+ }
+
+ // destroy dynamic storage, leave sentinel page (it's in static memory)
+ impl::xml_memory_page* root_page = PUGI__GETPAGE(_root);
+ assert(root_page && !root_page->prev);
+ assert(reinterpret_cast<char*>(root_page) >= _memory && reinterpret_cast<char*>(root_page) < _memory + sizeof(_memory));
+
+ for (impl::xml_memory_page* page = root_page->next; page; )
+ {
+ impl::xml_memory_page* next = page->next;
+
+ impl::xml_allocator::deallocate_page(page);
+
+ page = next;
+ }
+
+ #ifdef PUGIXML_COMPACT
+ // destroy hash table
+ static_cast<impl::xml_document_struct*>(_root)->hash.clear();
+ #endif
+
+ _root = 0;
+ }
+
+#ifdef PUGIXML_HAS_MOVE
+ PUGI__FN void xml_document::_move(xml_document& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT
+ {
+ impl::xml_document_struct* doc = static_cast<impl::xml_document_struct*>(_root);
+ impl::xml_document_struct* other = static_cast<impl::xml_document_struct*>(rhs._root);
+
+ // save first child pointer for later; this needs hash access
+ xml_node_struct* other_first_child = other->first_child;
+
+ #ifdef PUGIXML_COMPACT
+ // reserve space for the hash table up front; this is the only operation that can fail
+ // if it does, we have no choice but to throw (if we have exceptions)
+ if (other_first_child)
+ {
+ size_t other_children = 0;
+ for (xml_node_struct* node = other_first_child; node; node = node->next_sibling)
+ other_children++;
+
+ // in compact mode, each pointer assignment could result in a hash table request
+ // during move, we have to relocate document first_child and parents of all children
+ // normally there's just one child and its parent has a pointerless encoding but
+ // we assume the worst here
+ if (!other->_hash->reserve(other_children + 1))
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ return;
+ #else
+ throw std::bad_alloc();
+ #endif
+ }
+ }
+ #endif
+
+ // move allocation state
+ // note that other->_root may point to the embedded document page, in which case we should keep original (empty) state
+ if (other->_root != PUGI__GETPAGE(other))
+ {
+ doc->_root = other->_root;
+ doc->_busy_size = other->_busy_size;
+ }
+
+ // move buffer state
+ doc->buffer = other->buffer;
+ doc->extra_buffers = other->extra_buffers;
+ _buffer = rhs._buffer;
+
+ #ifdef PUGIXML_COMPACT
+ // move compact hash; note that the hash table can have pointers to other but they will be "inactive", similarly to nodes removed with remove_child
+ doc->hash = other->hash;
+ doc->_hash = &doc->hash;
+
+ // make sure we don't access other hash up until the end when we reinitialize other document
+ other->_hash = 0;
+ #endif
+
+ // move page structure
+ impl::xml_memory_page* doc_page = PUGI__GETPAGE(doc);
+ assert(doc_page && !doc_page->prev && !doc_page->next);
+
+ impl::xml_memory_page* other_page = PUGI__GETPAGE(other);
+ assert(other_page && !other_page->prev);
+
+ // relink pages since root page is embedded into xml_document
+ if (impl::xml_memory_page* page = other_page->next)
+ {
+ assert(page->prev == other_page);
+
+ page->prev = doc_page;
+
+ doc_page->next = page;
+ other_page->next = 0;
+ }
+
+ // make sure pages point to the correct document state
+ for (impl::xml_memory_page* page = doc_page->next; page; page = page->next)
+ {
+ assert(page->allocator == other);
+
+ page->allocator = doc;
+
+ #ifdef PUGIXML_COMPACT
+ // this automatically migrates most children between documents and prevents ->parent assignment from allocating
+ if (page->compact_shared_parent == other)
+ page->compact_shared_parent = doc;
+ #endif
+ }
+
+ // move tree structure
+ assert(!doc->first_child);
+
+ doc->first_child = other_first_child;
+
+ for (xml_node_struct* node = other_first_child; node; node = node->next_sibling)
+ {
+ #ifdef PUGIXML_COMPACT
+ // most children will have migrated when we reassigned compact_shared_parent
+ assert(node->parent == other || node->parent == doc);
+
+ node->parent = doc;
+ #else
+ assert(node->parent == other);
+ node->parent = doc;
+ #endif
+ }
+
+ // reset other document
+ new (other) impl::xml_document_struct(PUGI__GETPAGE(other));
+ rhs._buffer = 0;
+ }
+#endif
+
+#ifndef PUGIXML_NO_STL
+ PUGI__FN xml_parse_result xml_document::load(std::basic_istream<char, std::char_traits<char> >& stream, unsigned int options, xml_encoding encoding)
+ {
+ reset();
+
+ return impl::load_stream_impl(static_cast<impl::xml_document_struct*>(_root), stream, options, encoding, &_buffer);
+ }
+
+ PUGI__FN xml_parse_result xml_document::load(std::basic_istream<wchar_t, std::char_traits<wchar_t> >& stream, unsigned int options)
+ {
+ reset();
+
+ return impl::load_stream_impl(static_cast<impl::xml_document_struct*>(_root), stream, options, encoding_wchar, &_buffer);
+ }
+#endif
+
+ PUGI__FN xml_parse_result xml_document::load_string(const char_t* contents, unsigned int options)
+ {
+ // Force native encoding (skip autodetection)
+ #ifdef PUGIXML_WCHAR_MODE
+ xml_encoding encoding = encoding_wchar;
+ #else
+ xml_encoding encoding = encoding_utf8;
+ #endif
+
+ return load_buffer(contents, impl::strlength(contents) * sizeof(char_t), options, encoding);
+ }
+
+ PUGI__FN xml_parse_result xml_document::load(const char_t* contents, unsigned int options)
+ {
+ return load_string(contents, options);
+ }
+
+ PUGI__FN xml_parse_result xml_document::load_file(const char* path_, unsigned int options, xml_encoding encoding)
+ {
+ reset();
+
+ using impl::auto_deleter; // MSVC7 workaround
+ auto_deleter<FILE> file(impl::open_file(path_, "rb"), impl::close_file);
+
+ return impl::load_file_impl(static_cast<impl::xml_document_struct*>(_root), file.data, options, encoding, &_buffer);
+ }
+
+ PUGI__FN xml_parse_result xml_document::load_file(const wchar_t* path_, unsigned int options, xml_encoding encoding)
+ {
+ reset();
+
+ using impl::auto_deleter; // MSVC7 workaround
+ auto_deleter<FILE> file(impl::open_file_wide(path_, L"rb"), impl::close_file);
+
+ return impl::load_file_impl(static_cast<impl::xml_document_struct*>(_root), file.data, options, encoding, &_buffer);
+ }
+
+ PUGI__FN xml_parse_result xml_document::load_buffer(const void* contents, size_t size, unsigned int options, xml_encoding encoding)
+ {
+ reset();
+
+ return impl::load_buffer_impl(static_cast<impl::xml_document_struct*>(_root), _root, const_cast<void*>(contents), size, options, encoding, false, false, &_buffer);
+ }
+
+ PUGI__FN xml_parse_result xml_document::load_buffer_inplace(void* contents, size_t size, unsigned int options, xml_encoding encoding)
+ {
+ reset();
+
+ return impl::load_buffer_impl(static_cast<impl::xml_document_struct*>(_root), _root, contents, size, options, encoding, true, false, &_buffer);
+ }
+
+ PUGI__FN xml_parse_result xml_document::load_buffer_inplace_own(void* contents, size_t size, unsigned int options, xml_encoding encoding)
+ {
+ reset();
+
+ return impl::load_buffer_impl(static_cast<impl::xml_document_struct*>(_root), _root, contents, size, options, encoding, true, true, &_buffer);
+ }
+
+ PUGI__FN void xml_document::save(xml_writer& writer, const char_t* indent, unsigned int flags, xml_encoding encoding) const
+ {
+ impl::xml_buffered_writer buffered_writer(writer, encoding);
+
+ if ((flags & format_write_bom) && encoding != encoding_latin1)
+ {
+ // BOM always represents the codepoint U+FEFF, so just write it in native encoding
+ #ifdef PUGIXML_WCHAR_MODE
+ unsigned int bom = 0xfeff;
+ buffered_writer.write(static_cast<wchar_t>(bom));
+ #else
+ buffered_writer.write('\xef', '\xbb', '\xbf');
+ #endif
+ }
+
+ if (!(flags & format_no_declaration) && !impl::has_declaration(_root))
+ {
+ buffered_writer.write_string(PUGIXML_TEXT("<?xml version=\"1.0\""));
+ if (encoding == encoding_latin1) buffered_writer.write_string(PUGIXML_TEXT(" encoding=\"ISO-8859-1\""));
+ buffered_writer.write('?', '>');
+ if (!(flags & format_raw)) buffered_writer.write('\n');
+ }
+
+ impl::node_output(buffered_writer, _root, indent, flags, 0);
+
+ buffered_writer.flush();
+ }
+
+#ifndef PUGIXML_NO_STL
+ PUGI__FN void xml_document::save(std::basic_ostream<char, std::char_traits<char> >& stream, const char_t* indent, unsigned int flags, xml_encoding encoding) const
+ {
+ xml_writer_stream writer(stream);
+
+ save(writer, indent, flags, encoding);
+ }
+
+ PUGI__FN void xml_document::save(std::basic_ostream<wchar_t, std::char_traits<wchar_t> >& stream, const char_t* indent, unsigned int flags) const
+ {
+ xml_writer_stream writer(stream);
+
+ save(writer, indent, flags, encoding_wchar);
+ }
+#endif
+
+ PUGI__FN bool xml_document::save_file(const char* path_, const char_t* indent, unsigned int flags, xml_encoding encoding) const
+ {
+ using impl::auto_deleter; // MSVC7 workaround
+ auto_deleter<FILE> file(impl::open_file(path_, (flags & format_save_file_text) ? "w" : "wb"), impl::close_file);
+
+ return impl::save_file_impl(*this, file.data, indent, flags, encoding);
+ }
+
+ PUGI__FN bool xml_document::save_file(const wchar_t* path_, const char_t* indent, unsigned int flags, xml_encoding encoding) const
+ {
+ using impl::auto_deleter; // MSVC7 workaround
+ auto_deleter<FILE> file(impl::open_file_wide(path_, (flags & format_save_file_text) ? L"w" : L"wb"), impl::close_file);
+
+ return impl::save_file_impl(*this, file.data, indent, flags, encoding);
+ }
+
+ PUGI__FN xml_node xml_document::document_element() const
+ {
+ assert(_root);
+
+ for (xml_node_struct* i = _root->first_child; i; i = i->next_sibling)
+ if (PUGI__NODETYPE(i) == node_element)
+ return xml_node(i);
+
+ return xml_node();
+ }
+
+#ifndef PUGIXML_NO_STL
+ PUGI__FN std::string PUGIXML_FUNCTION as_utf8(const wchar_t* str)
+ {
+ assert(str);
+
+ return impl::as_utf8_impl(str, impl::strlength_wide(str));
+ }
+
+ PUGI__FN std::string PUGIXML_FUNCTION as_utf8(const std::basic_string<wchar_t>& str)
+ {
+ return impl::as_utf8_impl(str.c_str(), str.size());
+ }
+
+ PUGI__FN std::basic_string<wchar_t> PUGIXML_FUNCTION as_wide(const char* str)
+ {
+ assert(str);
+
+ return impl::as_wide_impl(str, strlen(str));
+ }
+
+ PUGI__FN std::basic_string<wchar_t> PUGIXML_FUNCTION as_wide(const std::string& str)
+ {
+ return impl::as_wide_impl(str.c_str(), str.size());
+ }
+#endif
+
+ PUGI__FN void PUGIXML_FUNCTION set_memory_management_functions(allocation_function allocate, deallocation_function deallocate)
+ {
+ impl::xml_memory::allocate = allocate;
+ impl::xml_memory::deallocate = deallocate;
+ }
+
+ PUGI__FN allocation_function PUGIXML_FUNCTION get_memory_allocation_function()
+ {
+ return impl::xml_memory::allocate;
+ }
+
+ PUGI__FN deallocation_function PUGIXML_FUNCTION get_memory_deallocation_function()
+ {
+ return impl::xml_memory::deallocate;
+ }
+}
+
+#if !defined(PUGIXML_NO_STL) && (defined(_MSC_VER) || defined(__ICC))
+namespace std
+{
+ // Workarounds for (non-standard) iterator category detection for older versions (MSVC7/IC8 and earlier)
+ PUGI__FN std::bidirectional_iterator_tag _Iter_cat(const pugi::xml_node_iterator&)
+ {
+ return std::bidirectional_iterator_tag();
+ }
+
+ PUGI__FN std::bidirectional_iterator_tag _Iter_cat(const pugi::xml_attribute_iterator&)
+ {
+ return std::bidirectional_iterator_tag();
+ }
+
+ PUGI__FN std::bidirectional_iterator_tag _Iter_cat(const pugi::xml_named_node_iterator&)
+ {
+ return std::bidirectional_iterator_tag();
+ }
+}
+#endif
+
+#if !defined(PUGIXML_NO_STL) && defined(__SUNPRO_CC)
+namespace std
+{
+ // Workarounds for (non-standard) iterator category detection
+ PUGI__FN std::bidirectional_iterator_tag __iterator_category(const pugi::xml_node_iterator&)
+ {
+ return std::bidirectional_iterator_tag();
+ }
+
+ PUGI__FN std::bidirectional_iterator_tag __iterator_category(const pugi::xml_attribute_iterator&)
+ {
+ return std::bidirectional_iterator_tag();
+ }
+
+ PUGI__FN std::bidirectional_iterator_tag __iterator_category(const pugi::xml_named_node_iterator&)
+ {
+ return std::bidirectional_iterator_tag();
+ }
+}
+#endif
+
+#ifndef PUGIXML_NO_XPATH
+// STL replacements
+PUGI__NS_BEGIN
+ struct equal_to
+ {
+ template <typename T> bool operator()(const T& lhs, const T& rhs) const
+ {
+ return lhs == rhs;
+ }
+ };
+
+ struct not_equal_to
+ {
+ template <typename T> bool operator()(const T& lhs, const T& rhs) const
+ {
+ return lhs != rhs;
+ }
+ };
+
+ struct less
+ {
+ template <typename T> bool operator()(const T& lhs, const T& rhs) const
+ {
+ return lhs < rhs;
+ }
+ };
+
+ struct less_equal
+ {
+ template <typename T> bool operator()(const T& lhs, const T& rhs) const
+ {
+ return lhs <= rhs;
+ }
+ };
+
+ template <typename T> inline void swap(T& lhs, T& rhs)
+ {
+ T temp = lhs;
+ lhs = rhs;
+ rhs = temp;
+ }
+
+ template <typename I, typename Pred> PUGI__FN I min_element(I begin, I end, const Pred& pred)
+ {
+ I result = begin;
+
+ for (I it = begin + 1; it != end; ++it)
+ if (pred(*it, *result))
+ result = it;
+
+ return result;
+ }
+
+ template <typename I> PUGI__FN void reverse(I begin, I end)
+ {
+ while (end - begin > 1)
+ swap(*begin++, *--end);
+ }
+
+ template <typename I> PUGI__FN I unique(I begin, I end)
+ {
+ // fast skip head
+ while (end - begin > 1 && *begin != *(begin + 1))
+ begin++;
+
+ if (begin == end)
+ return begin;
+
+ // last written element
+ I write = begin++;
+
+ // merge unique elements
+ while (begin != end)
+ {
+ if (*begin != *write)
+ *++write = *begin++;
+ else
+ begin++;
+ }
+
+ // past-the-end (write points to live element)
+ return write + 1;
+ }
+
+ template <typename T, typename Pred> PUGI__FN void insertion_sort(T* begin, T* end, const Pred& pred)
+ {
+ if (begin == end)
+ return;
+
+ for (T* it = begin + 1; it != end; ++it)
+ {
+ T val = *it;
+ T* hole = it;
+
+ // move hole backwards
+ while (hole > begin && pred(val, *(hole - 1)))
+ {
+ *hole = *(hole - 1);
+ hole--;
+ }
+
+ // fill hole with element
+ *hole = val;
+ }
+ }
+
+ template <typename I, typename Pred> inline I median3(I first, I middle, I last, const Pred& pred)
+ {
+ if (pred(*middle, *first))
+ swap(middle, first);
+ if (pred(*last, *middle))
+ swap(last, middle);
+ if (pred(*middle, *first))
+ swap(middle, first);
+
+ return middle;
+ }
+
+ template <typename T, typename Pred> PUGI__FN void partition3(T* begin, T* end, T pivot, const Pred& pred, T** out_eqbeg, T** out_eqend)
+ {
+ // invariant: array is split into 4 groups: = < ? > (each variable denotes the boundary between the groups)
+ T* eq = begin;
+ T* lt = begin;
+ T* gt = end;
+
+ while (lt < gt)
+ {
+ if (pred(*lt, pivot))
+ lt++;
+ else if (*lt == pivot)
+ swap(*eq++, *lt++);
+ else
+ swap(*lt, *--gt);
+ }
+
+ // we now have just 4 groups: = < >; move equal elements to the middle
+ T* eqbeg = gt;
+
+ for (T* it = begin; it != eq; ++it)
+ swap(*it, *--eqbeg);
+
+ *out_eqbeg = eqbeg;
+ *out_eqend = gt;
+ }
+
+ template <typename I, typename Pred> PUGI__FN void sort(I begin, I end, const Pred& pred)
+ {
+ // sort large chunks
+ while (end - begin > 16)
+ {
+ // find median element
+ I middle = begin + (end - begin) / 2;
+ I median = median3(begin, middle, end - 1, pred);
+
+ // partition in three chunks (< = >)
+ I eqbeg, eqend;
+ partition3(begin, end, *median, pred, &eqbeg, &eqend);
+
+ // loop on larger half
+ if (eqbeg - begin > end - eqend)
+ {
+ sort(eqend, end, pred);
+ end = eqbeg;
+ }
+ else
+ {
+ sort(begin, eqbeg, pred);
+ begin = eqend;
+ }
+ }
+
+ // insertion sort small chunk
+ insertion_sort(begin, end, pred);
+ }
+
+ PUGI__FN bool hash_insert(const void** table, size_t size, const void* key)
+ {
+ assert(key);
+
+ unsigned int h = static_cast<unsigned int>(reinterpret_cast<uintptr_t>(key));
+
+ // MurmurHash3 32-bit finalizer
+ h ^= h >> 16;
+ h *= 0x85ebca6bu;
+ h ^= h >> 13;
+ h *= 0xc2b2ae35u;
+ h ^= h >> 16;
+
+ size_t hashmod = size - 1;
+ size_t bucket = h & hashmod;
+
+ for (size_t probe = 0; probe <= hashmod; ++probe)
+ {
+ if (table[bucket] == 0)
+ {
+ table[bucket] = key;
+ return true;
+ }
+
+ if (table[bucket] == key)
+ return false;
+
+ // hash collision, quadratic probing
+ bucket = (bucket + probe + 1) & hashmod;
+ }
+
+ assert(false && "Hash table is full"); // unreachable
+ return false;
+ }
+PUGI__NS_END
+
+// Allocator used for AST and evaluation stacks
+PUGI__NS_BEGIN
+ static const size_t xpath_memory_page_size =
+ #ifdef PUGIXML_MEMORY_XPATH_PAGE_SIZE
+ PUGIXML_MEMORY_XPATH_PAGE_SIZE
+ #else
+ 4096
+ #endif
+ ;
+
+ static const uintptr_t xpath_memory_block_alignment = sizeof(double) > sizeof(void*) ? sizeof(double) : sizeof(void*);
+
+ struct xpath_memory_block
+ {
+ xpath_memory_block* next;
+ size_t capacity;
+
+ union
+ {
+ char data[xpath_memory_page_size];
+ double alignment;
+ };
+ };
+
+ struct xpath_allocator
+ {
+ xpath_memory_block* _root;
+ size_t _root_size;
+ bool* _error;
+
+ xpath_allocator(xpath_memory_block* root, bool* error = 0): _root(root), _root_size(0), _error(error)
+ {
+ }
+
+ void* allocate(size_t size)
+ {
+ // round size up to block alignment boundary
+ size = (size + xpath_memory_block_alignment - 1) & ~(xpath_memory_block_alignment - 1);
+
+ if (_root_size + size <= _root->capacity)
+ {
+ void* buf = &_root->data[0] + _root_size;
+ _root_size += size;
+ return buf;
+ }
+ else
+ {
+ // make sure we have at least 1/4th of the page free after allocation to satisfy subsequent allocation requests
+ size_t block_capacity_base = sizeof(_root->data);
+ size_t block_capacity_req = size + block_capacity_base / 4;
+ size_t block_capacity = (block_capacity_base > block_capacity_req) ? block_capacity_base : block_capacity_req;
+
+ size_t block_size = block_capacity + offsetof(xpath_memory_block, data);
+
+ xpath_memory_block* block = static_cast<xpath_memory_block*>(xml_memory::allocate(block_size));
+ if (!block)
+ {
+ if (_error) *_error = true;
+ return 0;
+ }
+
+ block->next = _root;
+ block->capacity = block_capacity;
+
+ _root = block;
+ _root_size = size;
+
+ return block->data;
+ }
+ }
+
+ void* reallocate(void* ptr, size_t old_size, size_t new_size)
+ {
+ // round size up to block alignment boundary
+ old_size = (old_size + xpath_memory_block_alignment - 1) & ~(xpath_memory_block_alignment - 1);
+ new_size = (new_size + xpath_memory_block_alignment - 1) & ~(xpath_memory_block_alignment - 1);
+
+ // we can only reallocate the last object
+ assert(ptr == 0 || static_cast<char*>(ptr) + old_size == &_root->data[0] + _root_size);
+
+ // try to reallocate the object inplace
+ if (ptr && _root_size - old_size + new_size <= _root->capacity)
+ {
+ _root_size = _root_size - old_size + new_size;
+ return ptr;
+ }
+
+ // allocate a new block
+ void* result = allocate(new_size);
+ if (!result) return 0;
+
+ // we have a new block
+ if (ptr)
+ {
+ // copy old data (we only support growing)
+ assert(new_size >= old_size);
+ memcpy(result, ptr, old_size);
+
+ // free the previous page if it had no other objects
+ assert(_root->data == result);
+ assert(_root->next);
+
+ if (_root->next->data == ptr)
+ {
+ // deallocate the whole page, unless it was the first one
+ xpath_memory_block* next = _root->next->next;
+
+ if (next)
+ {
+ xml_memory::deallocate(_root->next);
+ _root->next = next;
+ }
+ }
+ }
+
+ return result;
+ }
+
+ void revert(const xpath_allocator& state)
+ {
+ // free all new pages
+ xpath_memory_block* cur = _root;
+
+ while (cur != state._root)
+ {
+ xpath_memory_block* next = cur->next;
+
+ xml_memory::deallocate(cur);
+
+ cur = next;
+ }
+
+ // restore state
+ _root = state._root;
+ _root_size = state._root_size;
+ }
+
+ void release()
+ {
+ xpath_memory_block* cur = _root;
+ assert(cur);
+
+ while (cur->next)
+ {
+ xpath_memory_block* next = cur->next;
+
+ xml_memory::deallocate(cur);
+
+ cur = next;
+ }
+ }
+ };
+
+ struct xpath_allocator_capture
+ {
+ xpath_allocator_capture(xpath_allocator* alloc): _target(alloc), _state(*alloc)
+ {
+ }
+
+ ~xpath_allocator_capture()
+ {
+ _target->revert(_state);
+ }
+
+ xpath_allocator* _target;
+ xpath_allocator _state;
+ };
+
+ struct xpath_stack
+ {
+ xpath_allocator* result;
+ xpath_allocator* temp;
+ };
+
+ struct xpath_stack_data
+ {
+ xpath_memory_block blocks[2];
+ xpath_allocator result;
+ xpath_allocator temp;
+ xpath_stack stack;
+ bool oom;
+
+ xpath_stack_data(): result(blocks + 0, &oom), temp(blocks + 1, &oom), oom(false)
+ {
+ blocks[0].next = blocks[1].next = 0;
+ blocks[0].capacity = blocks[1].capacity = sizeof(blocks[0].data);
+
+ stack.result = &result;
+ stack.temp = &temp;
+ }
+
+ ~xpath_stack_data()
+ {
+ result.release();
+ temp.release();
+ }
+ };
+PUGI__NS_END
+
+// String class
+PUGI__NS_BEGIN
+ class xpath_string
+ {
+ const char_t* _buffer;
+ bool _uses_heap;
+ size_t _length_heap;
+
+ static char_t* duplicate_string(const char_t* string, size_t length, xpath_allocator* alloc)
+ {
+ char_t* result = static_cast<char_t*>(alloc->allocate((length + 1) * sizeof(char_t)));
+ if (!result) return 0;
+
+ memcpy(result, string, length * sizeof(char_t));
+ result[length] = 0;
+
+ return result;
+ }
+
+ xpath_string(const char_t* buffer, bool uses_heap_, size_t length_heap): _buffer(buffer), _uses_heap(uses_heap_), _length_heap(length_heap)
+ {
+ }
+
+ public:
+ static xpath_string from_const(const char_t* str)
+ {
+ return xpath_string(str, false, 0);
+ }
+
+ static xpath_string from_heap_preallocated(const char_t* begin, const char_t* end)
+ {
+ assert(begin <= end && *end == 0);
+
+ return xpath_string(begin, true, static_cast<size_t>(end - begin));
+ }
+
+ static xpath_string from_heap(const char_t* begin, const char_t* end, xpath_allocator* alloc)
+ {
+ assert(begin <= end);
+
+ if (begin == end)
+ return xpath_string();
+
+ size_t length = static_cast<size_t>(end - begin);
+ const char_t* data = duplicate_string(begin, length, alloc);
+
+ return data ? xpath_string(data, true, length) : xpath_string();
+ }
+
+ xpath_string(): _buffer(PUGIXML_TEXT("")), _uses_heap(false), _length_heap(0)
+ {
+ }
+
+ void append(const xpath_string& o, xpath_allocator* alloc)
+ {
+ // skip empty sources
+ if (!*o._buffer) return;
+
+ // fast append for constant empty target and constant source
+ if (!*_buffer && !_uses_heap && !o._uses_heap)
+ {
+ _buffer = o._buffer;
+ }
+ else
+ {
+ // need to make heap copy
+ size_t target_length = length();
+ size_t source_length = o.length();
+ size_t result_length = target_length + source_length;
+
+ // allocate new buffer
+ char_t* result = static_cast<char_t*>(alloc->reallocate(_uses_heap ? const_cast<char_t*>(_buffer) : 0, (target_length + 1) * sizeof(char_t), (result_length + 1) * sizeof(char_t)));
+ if (!result) return;
+
+ // append first string to the new buffer in case there was no reallocation
+ if (!_uses_heap) memcpy(result, _buffer, target_length * sizeof(char_t));
+
+ // append second string to the new buffer
+ memcpy(result + target_length, o._buffer, source_length * sizeof(char_t));
+ result[result_length] = 0;
+
+ // finalize
+ _buffer = result;
+ _uses_heap = true;
+ _length_heap = result_length;
+ }
+ }
+
+ const char_t* c_str() const
+ {
+ return _buffer;
+ }
+
+ size_t length() const
+ {
+ return _uses_heap ? _length_heap : strlength(_buffer);
+ }
+
+ char_t* data(xpath_allocator* alloc)
+ {
+ // make private heap copy
+ if (!_uses_heap)
+ {
+ size_t length_ = strlength(_buffer);
+ const char_t* data_ = duplicate_string(_buffer, length_, alloc);
+
+ if (!data_) return 0;
+
+ _buffer = data_;
+ _uses_heap = true;
+ _length_heap = length_;
+ }
+
+ return const_cast<char_t*>(_buffer);
+ }
+
+ bool empty() const
+ {
+ return *_buffer == 0;
+ }
+
+ bool operator==(const xpath_string& o) const
+ {
+ return strequal(_buffer, o._buffer);
+ }
+
+ bool operator!=(const xpath_string& o) const
+ {
+ return !strequal(_buffer, o._buffer);
+ }
+
+ bool uses_heap() const
+ {
+ return _uses_heap;
+ }
+ };
+PUGI__NS_END
+
+PUGI__NS_BEGIN
+ PUGI__FN bool starts_with(const char_t* string, const char_t* pattern)
+ {
+ while (*pattern && *string == *pattern)
+ {
+ string++;
+ pattern++;
+ }
+
+ return *pattern == 0;
+ }
+
+ PUGI__FN const char_t* find_char(const char_t* s, char_t c)
+ {
+ #ifdef PUGIXML_WCHAR_MODE
+ return wcschr(s, c);
+ #else
+ return strchr(s, c);
+ #endif
+ }
+
+ PUGI__FN const char_t* find_substring(const char_t* s, const char_t* p)
+ {
+ #ifdef PUGIXML_WCHAR_MODE
+ // MSVC6 wcsstr bug workaround (if s is empty it always returns 0)
+ return (*p == 0) ? s : wcsstr(s, p);
+ #else
+ return strstr(s, p);
+ #endif
+ }
+
+ // Converts symbol to lower case, if it is an ASCII one
+ PUGI__FN char_t tolower_ascii(char_t ch)
+ {
+ return static_cast<unsigned int>(ch - 'A') < 26 ? static_cast<char_t>(ch | ' ') : ch;
+ }
+
+ PUGI__FN xpath_string string_value(const xpath_node& na, xpath_allocator* alloc)
+ {
+ if (na.attribute())
+ return xpath_string::from_const(na.attribute().value());
+ else
+ {
+ xml_node n = na.node();
+
+ switch (n.type())
+ {
+ case node_pcdata:
+ case node_cdata:
+ case node_comment:
+ case node_pi:
+ return xpath_string::from_const(n.value());
+
+ case node_document:
+ case node_element:
+ {
+ xpath_string result;
+
+ // element nodes can have value if parse_embed_pcdata was used
+ if (n.value()[0])
+ result.append(xpath_string::from_const(n.value()), alloc);
+
+ xml_node cur = n.first_child();
+
+ while (cur && cur != n)
+ {
+ if (cur.type() == node_pcdata || cur.type() == node_cdata)
+ result.append(xpath_string::from_const(cur.value()), alloc);
+
+ if (cur.first_child())
+ cur = cur.first_child();
+ else if (cur.next_sibling())
+ cur = cur.next_sibling();
+ else
+ {
+ while (!cur.next_sibling() && cur != n)
+ cur = cur.parent();
+
+ if (cur != n) cur = cur.next_sibling();
+ }
+ }
+
+ return result;
+ }
+
+ default:
+ return xpath_string();
+ }
+ }
+ }
+
+ PUGI__FN bool node_is_before_sibling(xml_node_struct* ln, xml_node_struct* rn)
+ {
+ assert(ln->parent == rn->parent);
+
+ // there is no common ancestor (the shared parent is null), nodes are from different documents
+ if (!ln->parent) return ln < rn;
+
+ // determine sibling order
+ xml_node_struct* ls = ln;
+ xml_node_struct* rs = rn;
+
+ while (ls && rs)
+ {
+ if (ls == rn) return true;
+ if (rs == ln) return false;
+
+ ls = ls->next_sibling;
+ rs = rs->next_sibling;
+ }
+
+ // if rn sibling chain ended ln must be before rn
+ return !rs;
+ }
+
+ PUGI__FN bool node_is_before(xml_node_struct* ln, xml_node_struct* rn)
+ {
+ // find common ancestor at the same depth, if any
+ xml_node_struct* lp = ln;
+ xml_node_struct* rp = rn;
+
+ while (lp && rp && lp->parent != rp->parent)
+ {
+ lp = lp->parent;
+ rp = rp->parent;
+ }
+
+ // parents are the same!
+ if (lp && rp) return node_is_before_sibling(lp, rp);
+
+ // nodes are at different depths, need to normalize heights
+ bool left_higher = !lp;
+
+ while (lp)
+ {
+ lp = lp->parent;
+ ln = ln->parent;
+ }
+
+ while (rp)
+ {
+ rp = rp->parent;
+ rn = rn->parent;
+ }
+
+ // one node is the ancestor of the other
+ if (ln == rn) return left_higher;
+
+ // find common ancestor... again
+ while (ln->parent != rn->parent)
+ {
+ ln = ln->parent;
+ rn = rn->parent;
+ }
+
+ return node_is_before_sibling(ln, rn);
+ }
+
+ PUGI__FN bool node_is_ancestor(xml_node_struct* parent, xml_node_struct* node)
+ {
+ while (node && node != parent) node = node->parent;
+
+ return parent && node == parent;
+ }
+
+ PUGI__FN const void* document_buffer_order(const xpath_node& xnode)
+ {
+ xml_node_struct* node = xnode.node().internal_object();
+
+ if (node)
+ {
+ if ((get_document(node).header & xml_memory_page_contents_shared_mask) == 0)
+ {
+ if (node->name && (node->header & impl::xml_memory_page_name_allocated_or_shared_mask) == 0) return node->name;
+ if (node->value && (node->header & impl::xml_memory_page_value_allocated_or_shared_mask) == 0) return node->value;
+ }
+
+ return 0;
+ }
+
+ xml_attribute_struct* attr = xnode.attribute().internal_object();
+
+ if (attr)
+ {
+ if ((get_document(attr).header & xml_memory_page_contents_shared_mask) == 0)
+ {
+ if ((attr->header & impl::xml_memory_page_name_allocated_or_shared_mask) == 0) return attr->name;
+ if ((attr->header & impl::xml_memory_page_value_allocated_or_shared_mask) == 0) return attr->value;
+ }
+
+ return 0;
+ }
+
+ return 0;
+ }
+
+ struct document_order_comparator
+ {
+ bool operator()(const xpath_node& lhs, const xpath_node& rhs) const
+ {
+ // optimized document order based check
+ const void* lo = document_buffer_order(lhs);
+ const void* ro = document_buffer_order(rhs);
+
+ if (lo && ro) return lo < ro;
+
+ // slow comparison
+ xml_node ln = lhs.node(), rn = rhs.node();
+
+ // compare attributes
+ if (lhs.attribute() && rhs.attribute())
+ {
+ // shared parent
+ if (lhs.parent() == rhs.parent())
+ {
+ // determine sibling order
+ for (xml_attribute a = lhs.attribute(); a; a = a.next_attribute())
+ if (a == rhs.attribute())
+ return true;
+
+ return false;
+ }
+
+ // compare attribute parents
+ ln = lhs.parent();
+ rn = rhs.parent();
+ }
+ else if (lhs.attribute())
+ {
+ // attributes go after the parent element
+ if (lhs.parent() == rhs.node()) return false;
+
+ ln = lhs.parent();
+ }
+ else if (rhs.attribute())
+ {
+ // attributes go after the parent element
+ if (rhs.parent() == lhs.node()) return true;
+
+ rn = rhs.parent();
+ }
+
+ if (ln == rn) return false;
+
+ if (!ln || !rn) return ln < rn;
+
+ return node_is_before(ln.internal_object(), rn.internal_object());
+ }
+ };
+
+ PUGI__FN double gen_nan()
+ {
+ #if defined(__STDC_IEC_559__) || ((FLT_RADIX - 0 == 2) && (FLT_MAX_EXP - 0 == 128) && (FLT_MANT_DIG - 0 == 24))
+ PUGI__STATIC_ASSERT(sizeof(float) == sizeof(uint32_t));
+ typedef uint32_t UI; // BCC5 workaround
+ union { float f; UI i; } u;
+ u.i = 0x7fc00000;
+ return double(u.f);
+ #else
+ // fallback
+ const volatile double zero = 0.0;
+ return zero / zero;
+ #endif
+ }
+
+ PUGI__FN bool is_nan(double value)
+ {
+ #if defined(PUGI__MSVC_CRT_VERSION) || defined(__BORLANDC__)
+ return !!_isnan(value);
+ #elif defined(fpclassify) && defined(FP_NAN)
+ return fpclassify(value) == FP_NAN;
+ #else
+ // fallback
+ const volatile double v = value;
+ return v != v;
+ #endif
+ }
+
+ PUGI__FN const char_t* convert_number_to_string_special(double value)
+ {
+ #if defined(PUGI__MSVC_CRT_VERSION) || defined(__BORLANDC__)
+ if (_finite(value)) return (value == 0) ? PUGIXML_TEXT("0") : 0;
+ if (_isnan(value)) return PUGIXML_TEXT("NaN");
+ return value > 0 ? PUGIXML_TEXT("Infinity") : PUGIXML_TEXT("-Infinity");
+ #elif defined(fpclassify) && defined(FP_NAN) && defined(FP_INFINITE) && defined(FP_ZERO)
+ switch (fpclassify(value))
+ {
+ case FP_NAN:
+ return PUGIXML_TEXT("NaN");
+
+ case FP_INFINITE:
+ return value > 0 ? PUGIXML_TEXT("Infinity") : PUGIXML_TEXT("-Infinity");
+
+ case FP_ZERO:
+ return PUGIXML_TEXT("0");
+
+ default:
+ return 0;
+ }
+ #else
+ // fallback
+ const volatile double v = value;
+
+ if (v == 0) return PUGIXML_TEXT("0");
+ if (v != v) return PUGIXML_TEXT("NaN");
+ if (v * 2 == v) return value > 0 ? PUGIXML_TEXT("Infinity") : PUGIXML_TEXT("-Infinity");
+ return 0;
+ #endif
+ }
+
+ PUGI__FN bool convert_number_to_boolean(double value)
+ {
+ return (value != 0 && !is_nan(value));
+ }
+
+ PUGI__FN void truncate_zeros(char* begin, char* end)
+ {
+ while (begin != end && end[-1] == '0') end--;
+
+ *end = 0;
+ }
+
+ // gets mantissa digits in the form of 0.xxxxx with 0. implied and the exponent
+#if defined(PUGI__MSVC_CRT_VERSION) && PUGI__MSVC_CRT_VERSION >= 1400
+ PUGI__FN void convert_number_to_mantissa_exponent(double value, char (&buffer)[32], char** out_mantissa, int* out_exponent)
+ {
+ // get base values
+ int sign, exponent;
+ _ecvt_s(buffer, sizeof(buffer), value, DBL_DIG + 1, &exponent, &sign);
+
+ // truncate redundant zeros
+ truncate_zeros(buffer, buffer + strlen(buffer));
+
+ // fill results
+ *out_mantissa = buffer;
+ *out_exponent = exponent;
+ }
+#else
+ PUGI__FN void convert_number_to_mantissa_exponent(double value, char (&buffer)[32], char** out_mantissa, int* out_exponent)
+ {
+ // get a scientific notation value with IEEE DBL_DIG decimals
+ PUGI__SNPRINTF(buffer, "%.*e", DBL_DIG, value);
+
+ // get the exponent (possibly negative)
+ char* exponent_string = strchr(buffer, 'e');
+ assert(exponent_string);
+
+ int exponent = atoi(exponent_string + 1);
+
+ // extract mantissa string: skip sign
+ char* mantissa = buffer[0] == '-' ? buffer + 1 : buffer;
+ assert(mantissa[0] != '0' && mantissa[1] == '.');
+
+ // divide mantissa by 10 to eliminate integer part
+ mantissa[1] = mantissa[0];
+ mantissa++;
+ exponent++;
+
+ // remove extra mantissa digits and zero-terminate mantissa
+ truncate_zeros(mantissa, exponent_string);
+
+ // fill results
+ *out_mantissa = mantissa;
+ *out_exponent = exponent;
+ }
+#endif
+
+ PUGI__FN xpath_string convert_number_to_string(double value, xpath_allocator* alloc)
+ {
+ // try special number conversion
+ const char_t* special = convert_number_to_string_special(value);
+ if (special) return xpath_string::from_const(special);
+
+ // get mantissa + exponent form
+ char mantissa_buffer[32];
+
+ char* mantissa;
+ int exponent;
+ convert_number_to_mantissa_exponent(value, mantissa_buffer, &mantissa, &exponent);
+
+ // allocate a buffer of suitable length for the number
+ size_t result_size = strlen(mantissa_buffer) + (exponent > 0 ? exponent : -exponent) + 4;
+ char_t* result = static_cast<char_t*>(alloc->allocate(sizeof(char_t) * result_size));
+ if (!result) return xpath_string();
+
+ // make the number!
+ char_t* s = result;
+
+ // sign
+ if (value < 0) *s++ = '-';
+
+ // integer part
+ if (exponent <= 0)
+ {
+ *s++ = '0';
+ }
+ else
+ {
+ while (exponent > 0)
+ {
+ assert(*mantissa == 0 || static_cast<unsigned int>(*mantissa - '0') <= 9);
+ *s++ = *mantissa ? *mantissa++ : '0';
+ exponent--;
+ }
+ }
+
+ // fractional part
+ if (*mantissa)
+ {
+ // decimal point
+ *s++ = '.';
+
+ // extra zeroes from negative exponent
+ while (exponent < 0)
+ {
+ *s++ = '0';
+ exponent++;
+ }
+
+ // extra mantissa digits
+ while (*mantissa)
+ {
+ assert(static_cast<unsigned int>(*mantissa - '0') <= 9);
+ *s++ = *mantissa++;
+ }
+ }
+
+ // zero-terminate
+ assert(s < result + result_size);
+ *s = 0;
+
+ return xpath_string::from_heap_preallocated(result, s);
+ }
+
+ PUGI__FN bool check_string_to_number_format(const char_t* string)
+ {
+ // parse leading whitespace
+ while (PUGI__IS_CHARTYPE(*string, ct_space)) ++string;
+
+ // parse sign
+ if (*string == '-') ++string;
+
+ if (!*string) return false;
+
+ // if there is no integer part, there should be a decimal part with at least one digit
+ if (!PUGI__IS_CHARTYPEX(string[0], ctx_digit) && (string[0] != '.' || !PUGI__IS_CHARTYPEX(string[1], ctx_digit))) return false;
+
+ // parse integer part
+ while (PUGI__IS_CHARTYPEX(*string, ctx_digit)) ++string;
+
+ // parse decimal part
+ if (*string == '.')
+ {
+ ++string;
+
+ while (PUGI__IS_CHARTYPEX(*string, ctx_digit)) ++string;
+ }
+
+ // parse trailing whitespace
+ while (PUGI__IS_CHARTYPE(*string, ct_space)) ++string;
+
+ return *string == 0;
+ }
+
+ PUGI__FN double convert_string_to_number(const char_t* string)
+ {
+ // check string format
+ if (!check_string_to_number_format(string)) return gen_nan();
+
+ // parse string
+ #ifdef PUGIXML_WCHAR_MODE
+ return wcstod(string, 0);
+ #else
+ return strtod(string, 0);
+ #endif
+ }
+
+ PUGI__FN bool convert_string_to_number_scratch(char_t (&buffer)[32], const char_t* begin, const char_t* end, double* out_result)
+ {
+ size_t length = static_cast<size_t>(end - begin);
+ char_t* scratch = buffer;
+
+ if (length >= sizeof(buffer) / sizeof(buffer[0]))
+ {
+ // need to make dummy on-heap copy
+ scratch = static_cast<char_t*>(xml_memory::allocate((length + 1) * sizeof(char_t)));
+ if (!scratch) return false;
+ }
+
+ // copy string to zero-terminated buffer and perform conversion
+ memcpy(scratch, begin, length * sizeof(char_t));
+ scratch[length] = 0;
+
+ *out_result = convert_string_to_number(scratch);
+
+ // free dummy buffer
+ if (scratch != buffer) xml_memory::deallocate(scratch);
+
+ return true;
+ }
+
+ PUGI__FN double round_nearest(double value)
+ {
+ return floor(value + 0.5);
+ }
+
+ PUGI__FN double round_nearest_nzero(double value)
+ {
+ // same as round_nearest, but returns -0 for [-0.5, -0]
+ // ceil is used to differentiate between +0 and -0 (we return -0 for [-0.5, -0] and +0 for +0)
+ return (value >= -0.5 && value <= 0) ? ceil(value) : floor(value + 0.5);
+ }
+
+ PUGI__FN const char_t* qualified_name(const xpath_node& node)
+ {
+ return node.attribute() ? node.attribute().name() : node.node().name();
+ }
+
+ PUGI__FN const char_t* local_name(const xpath_node& node)
+ {
+ const char_t* name = qualified_name(node);
+ const char_t* p = find_char(name, ':');
+
+ return p ? p + 1 : name;
+ }
+
+ struct namespace_uri_predicate
+ {
+ const char_t* prefix;
+ size_t prefix_length;
+
+ namespace_uri_predicate(const char_t* name)
+ {
+ const char_t* pos = find_char(name, ':');
+
+ prefix = pos ? name : 0;
+ prefix_length = pos ? static_cast<size_t>(pos - name) : 0;
+ }
+
+ bool operator()(xml_attribute a) const
+ {
+ const char_t* name = a.name();
+
+ if (!starts_with(name, PUGIXML_TEXT("xmlns"))) return false;
+
+ return prefix ? name[5] == ':' && strequalrange(name + 6, prefix, prefix_length) : name[5] == 0;
+ }
+ };
+
+ PUGI__FN const char_t* namespace_uri(xml_node node)
+ {
+ namespace_uri_predicate pred = node.name();
+
+ xml_node p = node;
+
+ while (p)
+ {
+ xml_attribute a = p.find_attribute(pred);
+
+ if (a) return a.value();
+
+ p = p.parent();
+ }
+
+ return PUGIXML_TEXT("");
+ }
+
+ PUGI__FN const char_t* namespace_uri(xml_attribute attr, xml_node parent)
+ {
+ namespace_uri_predicate pred = attr.name();
+
+ // Default namespace does not apply to attributes
+ if (!pred.prefix) return PUGIXML_TEXT("");
+
+ xml_node p = parent;
+
+ while (p)
+ {
+ xml_attribute a = p.find_attribute(pred);
+
+ if (a) return a.value();
+
+ p = p.parent();
+ }
+
+ return PUGIXML_TEXT("");
+ }
+
+ PUGI__FN const char_t* namespace_uri(const xpath_node& node)
+ {
+ return node.attribute() ? namespace_uri(node.attribute(), node.parent()) : namespace_uri(node.node());
+ }
+
+ PUGI__FN char_t* normalize_space(char_t* buffer)
+ {
+ char_t* write = buffer;
+
+ for (char_t* it = buffer; *it; )
+ {
+ char_t ch = *it++;
+
+ if (PUGI__IS_CHARTYPE(ch, ct_space))
+ {
+ // replace whitespace sequence with single space
+ while (PUGI__IS_CHARTYPE(*it, ct_space)) it++;
+
+ // avoid leading spaces
+ if (write != buffer) *write++ = ' ';
+ }
+ else *write++ = ch;
+ }
+
+ // remove trailing space
+ if (write != buffer && PUGI__IS_CHARTYPE(write[-1], ct_space)) write--;
+
+ // zero-terminate
+ *write = 0;
+
+ return write;
+ }
+
+ PUGI__FN char_t* translate(char_t* buffer, const char_t* from, const char_t* to, size_t to_length)
+ {
+ char_t* write = buffer;
+
+ while (*buffer)
+ {
+ PUGI__DMC_VOLATILE char_t ch = *buffer++;
+
+ const char_t* pos = find_char(from, ch);
+
+ if (!pos)
+ *write++ = ch; // do not process
+ else if (static_cast<size_t>(pos - from) < to_length)
+ *write++ = to[pos - from]; // replace
+ }
+
+ // zero-terminate
+ *write = 0;
+
+ return write;
+ }
+
+ PUGI__FN unsigned char* translate_table_generate(xpath_allocator* alloc, const char_t* from, const char_t* to)
+ {
+ unsigned char table[128] = {0};
+
+ while (*from)
+ {
+ unsigned int fc = static_cast<unsigned int>(*from);
+ unsigned int tc = static_cast<unsigned int>(*to);
+
+ if (fc >= 128 || tc >= 128)
+ return 0;
+
+ // code=128 means "skip character"
+ if (!table[fc])
+ table[fc] = static_cast<unsigned char>(tc ? tc : 128);
+
+ from++;
+ if (tc) to++;
+ }
+
+ for (int i = 0; i < 128; ++i)
+ if (!table[i])
+ table[i] = static_cast<unsigned char>(i);
+
+ void* result = alloc->allocate(sizeof(table));
+ if (!result) return 0;
+
+ memcpy(result, table, sizeof(table));
+
+ return static_cast<unsigned char*>(result);
+ }
+
+ PUGI__FN char_t* translate_table(char_t* buffer, const unsigned char* table)
+ {
+ char_t* write = buffer;
+
+ while (*buffer)
+ {
+ char_t ch = *buffer++;
+ unsigned int index = static_cast<unsigned int>(ch);
+
+ if (index < 128)
+ {
+ unsigned char code = table[index];
+
+ // code=128 means "skip character" (table size is 128 so 128 can be a special value)
+ // this code skips these characters without extra branches
+ *write = static_cast<char_t>(code);
+ write += 1 - (code >> 7);
+ }
+ else
+ {
+ *write++ = ch;
+ }
+ }
+
+ // zero-terminate
+ *write = 0;
+
+ return write;
+ }
+
+ inline bool is_xpath_attribute(const char_t* name)
+ {
+ return !(starts_with(name, PUGIXML_TEXT("xmlns")) && (name[5] == 0 || name[5] == ':'));
+ }
+
+ struct xpath_variable_boolean: xpath_variable
+ {
+ xpath_variable_boolean(): xpath_variable(xpath_type_boolean), value(false)
+ {
+ }
+
+ bool value;
+ char_t name[1];
+ };
+
+ struct xpath_variable_number: xpath_variable
+ {
+ xpath_variable_number(): xpath_variable(xpath_type_number), value(0)
+ {
+ }
+
+ double value;
+ char_t name[1];
+ };
+
+ struct xpath_variable_string: xpath_variable
+ {
+ xpath_variable_string(): xpath_variable(xpath_type_string), value(0)
+ {
+ }
+
+ ~xpath_variable_string()
+ {
+ if (value) xml_memory::deallocate(value);
+ }
+
+ char_t* value;
+ char_t name[1];
+ };
+
+ struct xpath_variable_node_set: xpath_variable
+ {
+ xpath_variable_node_set(): xpath_variable(xpath_type_node_set)
+ {
+ }
+
+ xpath_node_set value;
+ char_t name[1];
+ };
+
+ static const xpath_node_set dummy_node_set;
+
+ PUGI__FN PUGI__UNSIGNED_OVERFLOW unsigned int hash_string(const char_t* str)
+ {
+ // Jenkins one-at-a-time hash (http://en.wikipedia.org/wiki/Jenkins_hash_function#one-at-a-time)
+ unsigned int result = 0;
+
+ while (*str)
+ {
+ result += static_cast<unsigned int>(*str++);
+ result += result << 10;
+ result ^= result >> 6;
+ }
+
+ result += result << 3;
+ result ^= result >> 11;
+ result += result << 15;
+
+ return result;
+ }
+
+ template <typename T> PUGI__FN T* new_xpath_variable(const char_t* name)
+ {
+ size_t length = strlength(name);
+ if (length == 0) return 0; // empty variable names are invalid
+
+ // $$ we can't use offsetof(T, name) because T is non-POD, so we just allocate additional length characters
+ void* memory = xml_memory::allocate(sizeof(T) + length * sizeof(char_t));
+ if (!memory) return 0;
+
+ T* result = new (memory) T();
+
+ memcpy(result->name, name, (length + 1) * sizeof(char_t));
+
+ return result;
+ }
+
+ PUGI__FN xpath_variable* new_xpath_variable(xpath_value_type type, const char_t* name)
+ {
+ switch (type)
+ {
+ case xpath_type_node_set:
+ return new_xpath_variable<xpath_variable_node_set>(name);
+
+ case xpath_type_number:
+ return new_xpath_variable<xpath_variable_number>(name);
+
+ case xpath_type_string:
+ return new_xpath_variable<xpath_variable_string>(name);
+
+ case xpath_type_boolean:
+ return new_xpath_variable<xpath_variable_boolean>(name);
+
+ default:
+ return 0;
+ }
+ }
+
+ template <typename T> PUGI__FN void delete_xpath_variable(T* var)
+ {
+ var->~T();
+ xml_memory::deallocate(var);
+ }
+
+ PUGI__FN void delete_xpath_variable(xpath_value_type type, xpath_variable* var)
+ {
+ switch (type)
+ {
+ case xpath_type_node_set:
+ delete_xpath_variable(static_cast<xpath_variable_node_set*>(var));
+ break;
+
+ case xpath_type_number:
+ delete_xpath_variable(static_cast<xpath_variable_number*>(var));
+ break;
+
+ case xpath_type_string:
+ delete_xpath_variable(static_cast<xpath_variable_string*>(var));
+ break;
+
+ case xpath_type_boolean:
+ delete_xpath_variable(static_cast<xpath_variable_boolean*>(var));
+ break;
+
+ default:
+ assert(false && "Invalid variable type"); // unreachable
+ }
+ }
+
+ PUGI__FN bool copy_xpath_variable(xpath_variable* lhs, const xpath_variable* rhs)
+ {
+ switch (rhs->type())
+ {
+ case xpath_type_node_set:
+ return lhs->set(static_cast<const xpath_variable_node_set*>(rhs)->value);
+
+ case xpath_type_number:
+ return lhs->set(static_cast<const xpath_variable_number*>(rhs)->value);
+
+ case xpath_type_string:
+ return lhs->set(static_cast<const xpath_variable_string*>(rhs)->value);
+
+ case xpath_type_boolean:
+ return lhs->set(static_cast<const xpath_variable_boolean*>(rhs)->value);
+
+ default:
+ assert(false && "Invalid variable type"); // unreachable
+ return false;
+ }
+ }
+
+ PUGI__FN bool get_variable_scratch(char_t (&buffer)[32], xpath_variable_set* set, const char_t* begin, const char_t* end, xpath_variable** out_result)
+ {
+ size_t length = static_cast<size_t>(end - begin);
+ char_t* scratch = buffer;
+
+ if (length >= sizeof(buffer) / sizeof(buffer[0]))
+ {
+ // need to make dummy on-heap copy
+ scratch = static_cast<char_t*>(xml_memory::allocate((length + 1) * sizeof(char_t)));
+ if (!scratch) return false;
+ }
+
+ // copy string to zero-terminated buffer and perform lookup
+ memcpy(scratch, begin, length * sizeof(char_t));
+ scratch[length] = 0;
+
+ *out_result = set->get(scratch);
+
+ // free dummy buffer
+ if (scratch != buffer) xml_memory::deallocate(scratch);
+
+ return true;
+ }
+PUGI__NS_END
+
+// Internal node set class
+PUGI__NS_BEGIN
+ PUGI__FN xpath_node_set::type_t xpath_get_order(const xpath_node* begin, const xpath_node* end)
+ {
+ if (end - begin < 2)
+ return xpath_node_set::type_sorted;
+
+ document_order_comparator cmp;
+
+ bool first = cmp(begin[0], begin[1]);
+
+ for (const xpath_node* it = begin + 1; it + 1 < end; ++it)
+ if (cmp(it[0], it[1]) != first)
+ return xpath_node_set::type_unsorted;
+
+ return first ? xpath_node_set::type_sorted : xpath_node_set::type_sorted_reverse;
+ }
+
+ PUGI__FN xpath_node_set::type_t xpath_sort(xpath_node* begin, xpath_node* end, xpath_node_set::type_t type, bool rev)
+ {
+ xpath_node_set::type_t order = rev ? xpath_node_set::type_sorted_reverse : xpath_node_set::type_sorted;
+
+ if (type == xpath_node_set::type_unsorted)
+ {
+ xpath_node_set::type_t sorted = xpath_get_order(begin, end);
+
+ if (sorted == xpath_node_set::type_unsorted)
+ {
+ sort(begin, end, document_order_comparator());
+
+ type = xpath_node_set::type_sorted;
+ }
+ else
+ type = sorted;
+ }
+
+ if (type != order) reverse(begin, end);
+
+ return order;
+ }
+
+ PUGI__FN xpath_node xpath_first(const xpath_node* begin, const xpath_node* end, xpath_node_set::type_t type)
+ {
+ if (begin == end) return xpath_node();
+
+ switch (type)
+ {
+ case xpath_node_set::type_sorted:
+ return *begin;
+
+ case xpath_node_set::type_sorted_reverse:
+ return *(end - 1);
+
+ case xpath_node_set::type_unsorted:
+ return *min_element(begin, end, document_order_comparator());
+
+ default:
+ assert(false && "Invalid node set type"); // unreachable
+ return xpath_node();
+ }
+ }
+
+ class xpath_node_set_raw
+ {
+ xpath_node_set::type_t _type;
+
+ xpath_node* _begin;
+ xpath_node* _end;
+ xpath_node* _eos;
+
+ public:
+ xpath_node_set_raw(): _type(xpath_node_set::type_unsorted), _begin(0), _end(0), _eos(0)
+ {
+ }
+
+ xpath_node* begin() const
+ {
+ return _begin;
+ }
+
+ xpath_node* end() const
+ {
+ return _end;
+ }
+
+ bool empty() const
+ {
+ return _begin == _end;
+ }
+
+ size_t size() const
+ {
+ return static_cast<size_t>(_end - _begin);
+ }
+
+ xpath_node first() const
+ {
+ return xpath_first(_begin, _end, _type);
+ }
+
+ void push_back_grow(const xpath_node& node, xpath_allocator* alloc);
+
+ void push_back(const xpath_node& node, xpath_allocator* alloc)
+ {
+ if (_end != _eos)
+ *_end++ = node;
+ else
+ push_back_grow(node, alloc);
+ }
+
+ void append(const xpath_node* begin_, const xpath_node* end_, xpath_allocator* alloc)
+ {
+ if (begin_ == end_) return;
+
+ size_t size_ = static_cast<size_t>(_end - _begin);
+ size_t capacity = static_cast<size_t>(_eos - _begin);
+ size_t count = static_cast<size_t>(end_ - begin_);
+
+ if (size_ + count > capacity)
+ {
+ // reallocate the old array or allocate a new one
+ xpath_node* data = static_cast<xpath_node*>(alloc->reallocate(_begin, capacity * sizeof(xpath_node), (size_ + count) * sizeof(xpath_node)));
+ if (!data) return;
+
+ // finalize
+ _begin = data;
+ _end = data + size_;
+ _eos = data + size_ + count;
+ }
+
+ memcpy(_end, begin_, count * sizeof(xpath_node));
+ _end += count;
+ }
+
+ void sort_do()
+ {
+ _type = xpath_sort(_begin, _end, _type, false);
+ }
+
+ void truncate(xpath_node* pos)
+ {
+ assert(_begin <= pos && pos <= _end);
+
+ _end = pos;
+ }
+
+ void remove_duplicates(xpath_allocator* alloc)
+ {
+ if (_type == xpath_node_set::type_unsorted && _end - _begin > 2)
+ {
+ xpath_allocator_capture cr(alloc);
+
+ size_t size_ = static_cast<size_t>(_end - _begin);
+
+ size_t hash_size = 1;
+ while (hash_size < size_ + size_ / 2) hash_size *= 2;
+
+ const void** hash_data = static_cast<const void**>(alloc->allocate(hash_size * sizeof(void**)));
+ if (!hash_data) return;
+
+ memset(hash_data, 0, hash_size * sizeof(const void**));
+
+ xpath_node* write = _begin;
+
+ for (xpath_node* it = _begin; it != _end; ++it)
+ {
+ const void* attr = it->attribute().internal_object();
+ const void* node = it->node().internal_object();
+ const void* key = attr ? attr : node;
+
+ if (key && hash_insert(hash_data, hash_size, key))
+ {
+ *write++ = *it;
+ }
+ }
+
+ _end = write;
+ }
+ else
+ {
+ _end = unique(_begin, _end);
+ }
+ }
+
+ xpath_node_set::type_t type() const
+ {
+ return _type;
+ }
+
+ void set_type(xpath_node_set::type_t value)
+ {
+ _type = value;
+ }
+ };
+
+ PUGI__FN_NO_INLINE void xpath_node_set_raw::push_back_grow(const xpath_node& node, xpath_allocator* alloc)
+ {
+ size_t capacity = static_cast<size_t>(_eos - _begin);
+
+ // get new capacity (1.5x rule)
+ size_t new_capacity = capacity + capacity / 2 + 1;
+
+ // reallocate the old array or allocate a new one
+ xpath_node* data = static_cast<xpath_node*>(alloc->reallocate(_begin, capacity * sizeof(xpath_node), new_capacity * sizeof(xpath_node)));
+ if (!data) return;
+
+ // finalize
+ _begin = data;
+ _end = data + capacity;
+ _eos = data + new_capacity;
+
+ // push
+ *_end++ = node;
+ }
+PUGI__NS_END
+
+PUGI__NS_BEGIN
+ struct xpath_context
+ {
+ xpath_node n;
+ size_t position, size;
+
+ xpath_context(const xpath_node& n_, size_t position_, size_t size_): n(n_), position(position_), size(size_)
+ {
+ }
+ };
+
+ enum lexeme_t
+ {
+ lex_none = 0,
+ lex_equal,
+ lex_not_equal,
+ lex_less,
+ lex_greater,
+ lex_less_or_equal,
+ lex_greater_or_equal,
+ lex_plus,
+ lex_minus,
+ lex_multiply,
+ lex_union,
+ lex_var_ref,
+ lex_open_brace,
+ lex_close_brace,
+ lex_quoted_string,
+ lex_number,
+ lex_slash,
+ lex_double_slash,
+ lex_open_square_brace,
+ lex_close_square_brace,
+ lex_string,
+ lex_comma,
+ lex_axis_attribute,
+ lex_dot,
+ lex_double_dot,
+ lex_double_colon,
+ lex_eof
+ };
+
+ struct xpath_lexer_string
+ {
+ const char_t* begin;
+ const char_t* end;
+
+ xpath_lexer_string(): begin(0), end(0)
+ {
+ }
+
+ bool operator==(const char_t* other) const
+ {
+ size_t length = static_cast<size_t>(end - begin);
+
+ return strequalrange(other, begin, length);
+ }
+ };
+
+ class xpath_lexer
+ {
+ const char_t* _cur;
+ const char_t* _cur_lexeme_pos;
+ xpath_lexer_string _cur_lexeme_contents;
+
+ lexeme_t _cur_lexeme;
+
+ public:
+ explicit xpath_lexer(const char_t* query): _cur(query)
+ {
+ next();
+ }
+
+ const char_t* state() const
+ {
+ return _cur;
+ }
+
+ void next()
+ {
+ const char_t* cur = _cur;
+
+ while (PUGI__IS_CHARTYPE(*cur, ct_space)) ++cur;
+
+ // save lexeme position for error reporting
+ _cur_lexeme_pos = cur;
+
+ switch (*cur)
+ {
+ case 0:
+ _cur_lexeme = lex_eof;
+ break;
+
+ case '>':
+ if (*(cur+1) == '=')
+ {
+ cur += 2;
+ _cur_lexeme = lex_greater_or_equal;
+ }
+ else
+ {
+ cur += 1;
+ _cur_lexeme = lex_greater;
+ }
+ break;
+
+ case '<':
+ if (*(cur+1) == '=')
+ {
+ cur += 2;
+ _cur_lexeme = lex_less_or_equal;
+ }
+ else
+ {
+ cur += 1;
+ _cur_lexeme = lex_less;
+ }
+ break;
+
+ case '!':
+ if (*(cur+1) == '=')
+ {
+ cur += 2;
+ _cur_lexeme = lex_not_equal;
+ }
+ else
+ {
+ _cur_lexeme = lex_none;
+ }
+ break;
+
+ case '=':
+ cur += 1;
+ _cur_lexeme = lex_equal;
+
+ break;
+
+ case '+':
+ cur += 1;
+ _cur_lexeme = lex_plus;
+
+ break;
+
+ case '-':
+ cur += 1;
+ _cur_lexeme = lex_minus;
+
+ break;
+
+ case '*':
+ cur += 1;
+ _cur_lexeme = lex_multiply;
+
+ break;
+
+ case '|':
+ cur += 1;
+ _cur_lexeme = lex_union;
+
+ break;
+
+ case '$':
+ cur += 1;
+
+ if (PUGI__IS_CHARTYPEX(*cur, ctx_start_symbol))
+ {
+ _cur_lexeme_contents.begin = cur;
+
+ while (PUGI__IS_CHARTYPEX(*cur, ctx_symbol)) cur++;
+
+ if (cur[0] == ':' && PUGI__IS_CHARTYPEX(cur[1], ctx_symbol)) // qname
+ {
+ cur++; // :
+
+ while (PUGI__IS_CHARTYPEX(*cur, ctx_symbol)) cur++;
+ }
+
+ _cur_lexeme_contents.end = cur;
+
+ _cur_lexeme = lex_var_ref;
+ }
+ else
+ {
+ _cur_lexeme = lex_none;
+ }
+
+ break;
+
+ case '(':
+ cur += 1;
+ _cur_lexeme = lex_open_brace;
+
+ break;
+
+ case ')':
+ cur += 1;
+ _cur_lexeme = lex_close_brace;
+
+ break;
+
+ case '[':
+ cur += 1;
+ _cur_lexeme = lex_open_square_brace;
+
+ break;
+
+ case ']':
+ cur += 1;
+ _cur_lexeme = lex_close_square_brace;
+
+ break;
+
+ case ',':
+ cur += 1;
+ _cur_lexeme = lex_comma;
+
+ break;
+
+ case '/':
+ if (*(cur+1) == '/')
+ {
+ cur += 2;
+ _cur_lexeme = lex_double_slash;
+ }
+ else
+ {
+ cur += 1;
+ _cur_lexeme = lex_slash;
+ }
+ break;
+
+ case '.':
+ if (*(cur+1) == '.')
+ {
+ cur += 2;
+ _cur_lexeme = lex_double_dot;
+ }
+ else if (PUGI__IS_CHARTYPEX(*(cur+1), ctx_digit))
+ {
+ _cur_lexeme_contents.begin = cur; // .
+
+ ++cur;
+
+ while (PUGI__IS_CHARTYPEX(*cur, ctx_digit)) cur++;
+
+ _cur_lexeme_contents.end = cur;
+
+ _cur_lexeme = lex_number;
+ }
+ else
+ {
+ cur += 1;
+ _cur_lexeme = lex_dot;
+ }
+ break;
+
+ case '@':
+ cur += 1;
+ _cur_lexeme = lex_axis_attribute;
+
+ break;
+
+ case '"':
+ case '\'':
+ {
+ char_t terminator = *cur;
+
+ ++cur;
+
+ _cur_lexeme_contents.begin = cur;
+ while (*cur && *cur != terminator) cur++;
+ _cur_lexeme_contents.end = cur;
+
+ if (!*cur)
+ _cur_lexeme = lex_none;
+ else
+ {
+ cur += 1;
+ _cur_lexeme = lex_quoted_string;
+ }
+
+ break;
+ }
+
+ case ':':
+ if (*(cur+1) == ':')
+ {
+ cur += 2;
+ _cur_lexeme = lex_double_colon;
+ }
+ else
+ {
+ _cur_lexeme = lex_none;
+ }
+ break;
+
+ default:
+ if (PUGI__IS_CHARTYPEX(*cur, ctx_digit))
+ {
+ _cur_lexeme_contents.begin = cur;
+
+ while (PUGI__IS_CHARTYPEX(*cur, ctx_digit)) cur++;
+
+ if (*cur == '.')
+ {
+ cur++;
+
+ while (PUGI__IS_CHARTYPEX(*cur, ctx_digit)) cur++;
+ }
+
+ _cur_lexeme_contents.end = cur;
+
+ _cur_lexeme = lex_number;
+ }
+ else if (PUGI__IS_CHARTYPEX(*cur, ctx_start_symbol))
+ {
+ _cur_lexeme_contents.begin = cur;
+
+ while (PUGI__IS_CHARTYPEX(*cur, ctx_symbol)) cur++;
+
+ if (cur[0] == ':')
+ {
+ if (cur[1] == '*') // namespace test ncname:*
+ {
+ cur += 2; // :*
+ }
+ else if (PUGI__IS_CHARTYPEX(cur[1], ctx_symbol)) // namespace test qname
+ {
+ cur++; // :
+
+ while (PUGI__IS_CHARTYPEX(*cur, ctx_symbol)) cur++;
+ }
+ }
+
+ _cur_lexeme_contents.end = cur;
+
+ _cur_lexeme = lex_string;
+ }
+ else
+ {
+ _cur_lexeme = lex_none;
+ }
+ }
+
+ _cur = cur;
+ }
+
+ lexeme_t current() const
+ {
+ return _cur_lexeme;
+ }
+
+ const char_t* current_pos() const
+ {
+ return _cur_lexeme_pos;
+ }
+
+ const xpath_lexer_string& contents() const
+ {
+ assert(_cur_lexeme == lex_var_ref || _cur_lexeme == lex_number || _cur_lexeme == lex_string || _cur_lexeme == lex_quoted_string);
+
+ return _cur_lexeme_contents;
+ }
+ };
+
+ enum ast_type_t
+ {
+ ast_unknown,
+ ast_op_or, // left or right
+ ast_op_and, // left and right
+ ast_op_equal, // left = right
+ ast_op_not_equal, // left != right
+ ast_op_less, // left < right
+ ast_op_greater, // left > right
+ ast_op_less_or_equal, // left <= right
+ ast_op_greater_or_equal, // left >= right
+ ast_op_add, // left + right
+ ast_op_subtract, // left - right
+ ast_op_multiply, // left * right
+ ast_op_divide, // left / right
+ ast_op_mod, // left % right
+ ast_op_negate, // left - right
+ ast_op_union, // left | right
+ ast_predicate, // apply predicate to set; next points to next predicate
+ ast_filter, // select * from left where right
+ ast_string_constant, // string constant
+ ast_number_constant, // number constant
+ ast_variable, // variable
+ ast_func_last, // last()
+ ast_func_position, // position()
+ ast_func_count, // count(left)
+ ast_func_id, // id(left)
+ ast_func_local_name_0, // local-name()
+ ast_func_local_name_1, // local-name(left)
+ ast_func_namespace_uri_0, // namespace-uri()
+ ast_func_namespace_uri_1, // namespace-uri(left)
+ ast_func_name_0, // name()
+ ast_func_name_1, // name(left)
+ ast_func_string_0, // string()
+ ast_func_string_1, // string(left)
+ ast_func_concat, // concat(left, right, siblings)
+ ast_func_starts_with, // starts_with(left, right)
+ ast_func_contains, // contains(left, right)
+ ast_func_substring_before, // substring-before(left, right)
+ ast_func_substring_after, // substring-after(left, right)
+ ast_func_substring_2, // substring(left, right)
+ ast_func_substring_3, // substring(left, right, third)
+ ast_func_string_length_0, // string-length()
+ ast_func_string_length_1, // string-length(left)
+ ast_func_normalize_space_0, // normalize-space()
+ ast_func_normalize_space_1, // normalize-space(left)
+ ast_func_translate, // translate(left, right, third)
+ ast_func_boolean, // boolean(left)
+ ast_func_not, // not(left)
+ ast_func_true, // true()
+ ast_func_false, // false()
+ ast_func_lang, // lang(left)
+ ast_func_number_0, // number()
+ ast_func_number_1, // number(left)
+ ast_func_sum, // sum(left)
+ ast_func_floor, // floor(left)
+ ast_func_ceiling, // ceiling(left)
+ ast_func_round, // round(left)
+ ast_step, // process set left with step
+ ast_step_root, // select root node
+
+ ast_opt_translate_table, // translate(left, right, third) where right/third are constants
+ ast_opt_compare_attribute // @name = 'string'
+ };
+
+ enum axis_t
+ {
+ axis_ancestor,
+ axis_ancestor_or_self,
+ axis_attribute,
+ axis_child,
+ axis_descendant,
+ axis_descendant_or_self,
+ axis_following,
+ axis_following_sibling,
+ axis_namespace,
+ axis_parent,
+ axis_preceding,
+ axis_preceding_sibling,
+ axis_self
+ };
+
+ enum nodetest_t
+ {
+ nodetest_none,
+ nodetest_name,
+ nodetest_type_node,
+ nodetest_type_comment,
+ nodetest_type_pi,
+ nodetest_type_text,
+ nodetest_pi,
+ nodetest_all,
+ nodetest_all_in_namespace
+ };
+
+ enum predicate_t
+ {
+ predicate_default,
+ predicate_posinv,
+ predicate_constant,
+ predicate_constant_one
+ };
+
+ enum nodeset_eval_t
+ {
+ nodeset_eval_all,
+ nodeset_eval_any,
+ nodeset_eval_first
+ };
+
+ template <axis_t N> struct axis_to_type
+ {
+ static const axis_t axis;
+ };
+
+ template <axis_t N> const axis_t axis_to_type<N>::axis = N;
+
+ class xpath_ast_node
+ {
+ private:
+ // node type
+ char _type;
+ char _rettype;
+
+ // for ast_step
+ char _axis;
+
+ // for ast_step/ast_predicate/ast_filter
+ char _test;
+
+ // tree node structure
+ xpath_ast_node* _left;
+ xpath_ast_node* _right;
+ xpath_ast_node* _next;
+
+ union
+ {
+ // value for ast_string_constant
+ const char_t* string;
+ // value for ast_number_constant
+ double number;
+ // variable for ast_variable
+ xpath_variable* variable;
+ // node test for ast_step (node name/namespace/node type/pi target)
+ const char_t* nodetest;
+ // table for ast_opt_translate_table
+ const unsigned char* table;
+ } _data;
+
+ xpath_ast_node(const xpath_ast_node&);
+ xpath_ast_node& operator=(const xpath_ast_node&);
+
+ template <class Comp> static bool compare_eq(xpath_ast_node* lhs, xpath_ast_node* rhs, const xpath_context& c, const xpath_stack& stack, const Comp& comp)
+ {
+ xpath_value_type lt = lhs->rettype(), rt = rhs->rettype();
+
+ if (lt != xpath_type_node_set && rt != xpath_type_node_set)
+ {
+ if (lt == xpath_type_boolean || rt == xpath_type_boolean)
+ return comp(lhs->eval_boolean(c, stack), rhs->eval_boolean(c, stack));
+ else if (lt == xpath_type_number || rt == xpath_type_number)
+ return comp(lhs->eval_number(c, stack), rhs->eval_number(c, stack));
+ else if (lt == xpath_type_string || rt == xpath_type_string)
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_string ls = lhs->eval_string(c, stack);
+ xpath_string rs = rhs->eval_string(c, stack);
+
+ return comp(ls, rs);
+ }
+ }
+ else if (lt == xpath_type_node_set && rt == xpath_type_node_set)
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_node_set_raw ls = lhs->eval_node_set(c, stack, nodeset_eval_all);
+ xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all);
+
+ for (const xpath_node* li = ls.begin(); li != ls.end(); ++li)
+ for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri)
+ {
+ xpath_allocator_capture cri(stack.result);
+
+ if (comp(string_value(*li, stack.result), string_value(*ri, stack.result)))
+ return true;
+ }
+
+ return false;
+ }
+ else
+ {
+ if (lt == xpath_type_node_set)
+ {
+ swap(lhs, rhs);
+ swap(lt, rt);
+ }
+
+ if (lt == xpath_type_boolean)
+ return comp(lhs->eval_boolean(c, stack), rhs->eval_boolean(c, stack));
+ else if (lt == xpath_type_number)
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ double l = lhs->eval_number(c, stack);
+ xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all);
+
+ for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri)
+ {
+ xpath_allocator_capture cri(stack.result);
+
+ if (comp(l, convert_string_to_number(string_value(*ri, stack.result).c_str())))
+ return true;
+ }
+
+ return false;
+ }
+ else if (lt == xpath_type_string)
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_string l = lhs->eval_string(c, stack);
+ xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all);
+
+ for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri)
+ {
+ xpath_allocator_capture cri(stack.result);
+
+ if (comp(l, string_value(*ri, stack.result)))
+ return true;
+ }
+
+ return false;
+ }
+ }
+
+ assert(false && "Wrong types"); // unreachable
+ return false;
+ }
+
+ static bool eval_once(xpath_node_set::type_t type, nodeset_eval_t eval)
+ {
+ return type == xpath_node_set::type_sorted ? eval != nodeset_eval_all : eval == nodeset_eval_any;
+ }
+
+ template <class Comp> static bool compare_rel(xpath_ast_node* lhs, xpath_ast_node* rhs, const xpath_context& c, const xpath_stack& stack, const Comp& comp)
+ {
+ xpath_value_type lt = lhs->rettype(), rt = rhs->rettype();
+
+ if (lt != xpath_type_node_set && rt != xpath_type_node_set)
+ return comp(lhs->eval_number(c, stack), rhs->eval_number(c, stack));
+ else if (lt == xpath_type_node_set && rt == xpath_type_node_set)
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_node_set_raw ls = lhs->eval_node_set(c, stack, nodeset_eval_all);
+ xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all);
+
+ for (const xpath_node* li = ls.begin(); li != ls.end(); ++li)
+ {
+ xpath_allocator_capture cri(stack.result);
+
+ double l = convert_string_to_number(string_value(*li, stack.result).c_str());
+
+ for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri)
+ {
+ xpath_allocator_capture crii(stack.result);
+
+ if (comp(l, convert_string_to_number(string_value(*ri, stack.result).c_str())))
+ return true;
+ }
+ }
+
+ return false;
+ }
+ else if (lt != xpath_type_node_set && rt == xpath_type_node_set)
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ double l = lhs->eval_number(c, stack);
+ xpath_node_set_raw rs = rhs->eval_node_set(c, stack, nodeset_eval_all);
+
+ for (const xpath_node* ri = rs.begin(); ri != rs.end(); ++ri)
+ {
+ xpath_allocator_capture cri(stack.result);
+
+ if (comp(l, convert_string_to_number(string_value(*ri, stack.result).c_str())))
+ return true;
+ }
+
+ return false;
+ }
+ else if (lt == xpath_type_node_set && rt != xpath_type_node_set)
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_node_set_raw ls = lhs->eval_node_set(c, stack, nodeset_eval_all);
+ double r = rhs->eval_number(c, stack);
+
+ for (const xpath_node* li = ls.begin(); li != ls.end(); ++li)
+ {
+ xpath_allocator_capture cri(stack.result);
+
+ if (comp(convert_string_to_number(string_value(*li, stack.result).c_str()), r))
+ return true;
+ }
+
+ return false;
+ }
+ else
+ {
+ assert(false && "Wrong types"); // unreachable
+ return false;
+ }
+ }
+
+ static void apply_predicate_boolean(xpath_node_set_raw& ns, size_t first, xpath_ast_node* expr, const xpath_stack& stack, bool once)
+ {
+ assert(ns.size() >= first);
+ assert(expr->rettype() != xpath_type_number);
+
+ size_t i = 1;
+ size_t size = ns.size() - first;
+
+ xpath_node* last = ns.begin() + first;
+
+ // remove_if... or well, sort of
+ for (xpath_node* it = last; it != ns.end(); ++it, ++i)
+ {
+ xpath_context c(*it, i, size);
+
+ if (expr->eval_boolean(c, stack))
+ {
+ *last++ = *it;
+
+ if (once) break;
+ }
+ }
+
+ ns.truncate(last);
+ }
+
+ static void apply_predicate_number(xpath_node_set_raw& ns, size_t first, xpath_ast_node* expr, const xpath_stack& stack, bool once)
+ {
+ assert(ns.size() >= first);
+ assert(expr->rettype() == xpath_type_number);
+
+ size_t i = 1;
+ size_t size = ns.size() - first;
+
+ xpath_node* last = ns.begin() + first;
+
+ // remove_if... or well, sort of
+ for (xpath_node* it = last; it != ns.end(); ++it, ++i)
+ {
+ xpath_context c(*it, i, size);
+
+ if (expr->eval_number(c, stack) == static_cast<double>(i))
+ {
+ *last++ = *it;
+
+ if (once) break;
+ }
+ }
+
+ ns.truncate(last);
+ }
+
+ static void apply_predicate_number_const(xpath_node_set_raw& ns, size_t first, xpath_ast_node* expr, const xpath_stack& stack)
+ {
+ assert(ns.size() >= first);
+ assert(expr->rettype() == xpath_type_number);
+
+ size_t size = ns.size() - first;
+
+ xpath_node* last = ns.begin() + first;
+
+ xpath_context c(xpath_node(), 1, size);
+
+ double er = expr->eval_number(c, stack);
+
+ if (er >= 1.0 && er <= static_cast<double>(size))
+ {
+ size_t eri = static_cast<size_t>(er);
+
+ if (er == static_cast<double>(eri))
+ {
+ xpath_node r = last[eri - 1];
+
+ *last++ = r;
+ }
+ }
+
+ ns.truncate(last);
+ }
+
+ void apply_predicate(xpath_node_set_raw& ns, size_t first, const xpath_stack& stack, bool once)
+ {
+ if (ns.size() == first) return;
+
+ assert(_type == ast_filter || _type == ast_predicate);
+
+ if (_test == predicate_constant || _test == predicate_constant_one)
+ apply_predicate_number_const(ns, first, _right, stack);
+ else if (_right->rettype() == xpath_type_number)
+ apply_predicate_number(ns, first, _right, stack, once);
+ else
+ apply_predicate_boolean(ns, first, _right, stack, once);
+ }
+
+ void apply_predicates(xpath_node_set_raw& ns, size_t first, const xpath_stack& stack, nodeset_eval_t eval)
+ {
+ if (ns.size() == first) return;
+
+ bool last_once = eval_once(ns.type(), eval);
+
+ for (xpath_ast_node* pred = _right; pred; pred = pred->_next)
+ pred->apply_predicate(ns, first, stack, !pred->_next && last_once);
+ }
+
+ bool step_push(xpath_node_set_raw& ns, xml_attribute_struct* a, xml_node_struct* parent, xpath_allocator* alloc)
+ {
+ assert(a);
+
+ const char_t* name = a->name ? a->name + 0 : PUGIXML_TEXT("");
+
+ switch (_test)
+ {
+ case nodetest_name:
+ if (strequal(name, _data.nodetest) && is_xpath_attribute(name))
+ {
+ ns.push_back(xpath_node(xml_attribute(a), xml_node(parent)), alloc);
+ return true;
+ }
+ break;
+
+ case nodetest_type_node:
+ case nodetest_all:
+ if (is_xpath_attribute(name))
+ {
+ ns.push_back(xpath_node(xml_attribute(a), xml_node(parent)), alloc);
+ return true;
+ }
+ break;
+
+ case nodetest_all_in_namespace:
+ if (starts_with(name, _data.nodetest) && is_xpath_attribute(name))
+ {
+ ns.push_back(xpath_node(xml_attribute(a), xml_node(parent)), alloc);
+ return true;
+ }
+ break;
+
+ default:
+ ;
+ }
+
+ return false;
+ }
+
+ bool step_push(xpath_node_set_raw& ns, xml_node_struct* n, xpath_allocator* alloc)
+ {
+ assert(n);
+
+ xml_node_type type = PUGI__NODETYPE(n);
+
+ switch (_test)
+ {
+ case nodetest_name:
+ if (type == node_element && n->name && strequal(n->name, _data.nodetest))
+ {
+ ns.push_back(xml_node(n), alloc);
+ return true;
+ }
+ break;
+
+ case nodetest_type_node:
+ ns.push_back(xml_node(n), alloc);
+ return true;
+
+ case nodetest_type_comment:
+ if (type == node_comment)
+ {
+ ns.push_back(xml_node(n), alloc);
+ return true;
+ }
+ break;
+
+ case nodetest_type_text:
+ if (type == node_pcdata || type == node_cdata)
+ {
+ ns.push_back(xml_node(n), alloc);
+ return true;
+ }
+ break;
+
+ case nodetest_type_pi:
+ if (type == node_pi)
+ {
+ ns.push_back(xml_node(n), alloc);
+ return true;
+ }
+ break;
+
+ case nodetest_pi:
+ if (type == node_pi && n->name && strequal(n->name, _data.nodetest))
+ {
+ ns.push_back(xml_node(n), alloc);
+ return true;
+ }
+ break;
+
+ case nodetest_all:
+ if (type == node_element)
+ {
+ ns.push_back(xml_node(n), alloc);
+ return true;
+ }
+ break;
+
+ case nodetest_all_in_namespace:
+ if (type == node_element && n->name && starts_with(n->name, _data.nodetest))
+ {
+ ns.push_back(xml_node(n), alloc);
+ return true;
+ }
+ break;
+
+ default:
+ assert(false && "Unknown axis"); // unreachable
+ }
+
+ return false;
+ }
+
+ template <class T> void step_fill(xpath_node_set_raw& ns, xml_node_struct* n, xpath_allocator* alloc, bool once, T)
+ {
+ const axis_t axis = T::axis;
+
+ switch (axis)
+ {
+ case axis_attribute:
+ {
+ for (xml_attribute_struct* a = n->first_attribute; a; a = a->next_attribute)
+ if (step_push(ns, a, n, alloc) & once)
+ return;
+
+ break;
+ }
+
+ case axis_child:
+ {
+ for (xml_node_struct* c = n->first_child; c; c = c->next_sibling)
+ if (step_push(ns, c, alloc) & once)
+ return;
+
+ break;
+ }
+
+ case axis_descendant:
+ case axis_descendant_or_self:
+ {
+ if (axis == axis_descendant_or_self)
+ if (step_push(ns, n, alloc) & once)
+ return;
+
+ xml_node_struct* cur = n->first_child;
+
+ while (cur)
+ {
+ if (step_push(ns, cur, alloc) & once)
+ return;
+
+ if (cur->first_child)
+ cur = cur->first_child;
+ else
+ {
+ while (!cur->next_sibling)
+ {
+ cur = cur->parent;
+
+ if (cur == n) return;
+ }
+
+ cur = cur->next_sibling;
+ }
+ }
+
+ break;
+ }
+
+ case axis_following_sibling:
+ {
+ for (xml_node_struct* c = n->next_sibling; c; c = c->next_sibling)
+ if (step_push(ns, c, alloc) & once)
+ return;
+
+ break;
+ }
+
+ case axis_preceding_sibling:
+ {
+ for (xml_node_struct* c = n->prev_sibling_c; c->next_sibling; c = c->prev_sibling_c)
+ if (step_push(ns, c, alloc) & once)
+ return;
+
+ break;
+ }
+
+ case axis_following:
+ {
+ xml_node_struct* cur = n;
+
+ // exit from this node so that we don't include descendants
+ while (!cur->next_sibling)
+ {
+ cur = cur->parent;
+
+ if (!cur) return;
+ }
+
+ cur = cur->next_sibling;
+
+ while (cur)
+ {
+ if (step_push(ns, cur, alloc) & once)
+ return;
+
+ if (cur->first_child)
+ cur = cur->first_child;
+ else
+ {
+ while (!cur->next_sibling)
+ {
+ cur = cur->parent;
+
+ if (!cur) return;
+ }
+
+ cur = cur->next_sibling;
+ }
+ }
+
+ break;
+ }
+
+ case axis_preceding:
+ {
+ xml_node_struct* cur = n;
+
+ // exit from this node so that we don't include descendants
+ while (!cur->prev_sibling_c->next_sibling)
+ {
+ cur = cur->parent;
+
+ if (!cur) return;
+ }
+
+ cur = cur->prev_sibling_c;
+
+ while (cur)
+ {
+ if (cur->first_child)
+ cur = cur->first_child->prev_sibling_c;
+ else
+ {
+ // leaf node, can't be ancestor
+ if (step_push(ns, cur, alloc) & once)
+ return;
+
+ while (!cur->prev_sibling_c->next_sibling)
+ {
+ cur = cur->parent;
+
+ if (!cur) return;
+
+ if (!node_is_ancestor(cur, n))
+ if (step_push(ns, cur, alloc) & once)
+ return;
+ }
+
+ cur = cur->prev_sibling_c;
+ }
+ }
+
+ break;
+ }
+
+ case axis_ancestor:
+ case axis_ancestor_or_self:
+ {
+ if (axis == axis_ancestor_or_self)
+ if (step_push(ns, n, alloc) & once)
+ return;
+
+ xml_node_struct* cur = n->parent;
+
+ while (cur)
+ {
+ if (step_push(ns, cur, alloc) & once)
+ return;
+
+ cur = cur->parent;
+ }
+
+ break;
+ }
+
+ case axis_self:
+ {
+ step_push(ns, n, alloc);
+
+ break;
+ }
+
+ case axis_parent:
+ {
+ if (n->parent)
+ step_push(ns, n->parent, alloc);
+
+ break;
+ }
+
+ default:
+ assert(false && "Unimplemented axis"); // unreachable
+ }
+ }
+
+ template <class T> void step_fill(xpath_node_set_raw& ns, xml_attribute_struct* a, xml_node_struct* p, xpath_allocator* alloc, bool once, T v)
+ {
+ const axis_t axis = T::axis;
+
+ switch (axis)
+ {
+ case axis_ancestor:
+ case axis_ancestor_or_self:
+ {
+ if (axis == axis_ancestor_or_self && _test == nodetest_type_node) // reject attributes based on principal node type test
+ if (step_push(ns, a, p, alloc) & once)
+ return;
+
+ xml_node_struct* cur = p;
+
+ while (cur)
+ {
+ if (step_push(ns, cur, alloc) & once)
+ return;
+
+ cur = cur->parent;
+ }
+
+ break;
+ }
+
+ case axis_descendant_or_self:
+ case axis_self:
+ {
+ if (_test == nodetest_type_node) // reject attributes based on principal node type test
+ step_push(ns, a, p, alloc);
+
+ break;
+ }
+
+ case axis_following:
+ {
+ xml_node_struct* cur = p;
+
+ while (cur)
+ {
+ if (cur->first_child)
+ cur = cur->first_child;
+ else
+ {
+ while (!cur->next_sibling)
+ {
+ cur = cur->parent;
+
+ if (!cur) return;
+ }
+
+ cur = cur->next_sibling;
+ }
+
+ if (step_push(ns, cur, alloc) & once)
+ return;
+ }
+
+ break;
+ }
+
+ case axis_parent:
+ {
+ step_push(ns, p, alloc);
+
+ break;
+ }
+
+ case axis_preceding:
+ {
+ // preceding:: axis does not include attribute nodes and attribute ancestors (they are the same as parent's ancestors), so we can reuse node preceding
+ step_fill(ns, p, alloc, once, v);
+ break;
+ }
+
+ default:
+ assert(false && "Unimplemented axis"); // unreachable
+ }
+ }
+
+ template <class T> void step_fill(xpath_node_set_raw& ns, const xpath_node& xn, xpath_allocator* alloc, bool once, T v)
+ {
+ const axis_t axis = T::axis;
+ const bool axis_has_attributes = (axis == axis_ancestor || axis == axis_ancestor_or_self || axis == axis_descendant_or_self || axis == axis_following || axis == axis_parent || axis == axis_preceding || axis == axis_self);
+
+ if (xn.node())
+ step_fill(ns, xn.node().internal_object(), alloc, once, v);
+ else if (axis_has_attributes && xn.attribute() && xn.parent())
+ step_fill(ns, xn.attribute().internal_object(), xn.parent().internal_object(), alloc, once, v);
+ }
+
+ template <class T> xpath_node_set_raw step_do(const xpath_context& c, const xpath_stack& stack, nodeset_eval_t eval, T v)
+ {
+ const axis_t axis = T::axis;
+ const bool axis_reverse = (axis == axis_ancestor || axis == axis_ancestor_or_self || axis == axis_preceding || axis == axis_preceding_sibling);
+ const xpath_node_set::type_t axis_type = axis_reverse ? xpath_node_set::type_sorted_reverse : xpath_node_set::type_sorted;
+
+ bool once =
+ (axis == axis_attribute && _test == nodetest_name) ||
+ (!_right && eval_once(axis_type, eval)) ||
+ // coverity[mixed_enums]
+ (_right && !_right->_next && _right->_test == predicate_constant_one);
+
+ xpath_node_set_raw ns;
+ ns.set_type(axis_type);
+
+ if (_left)
+ {
+ xpath_node_set_raw s = _left->eval_node_set(c, stack, nodeset_eval_all);
+
+ // self axis preserves the original order
+ if (axis == axis_self) ns.set_type(s.type());
+
+ for (const xpath_node* it = s.begin(); it != s.end(); ++it)
+ {
+ size_t size = ns.size();
+
+ // in general, all axes generate elements in a particular order, but there is no order guarantee if axis is applied to two nodes
+ if (axis != axis_self && size != 0) ns.set_type(xpath_node_set::type_unsorted);
+
+ step_fill(ns, *it, stack.result, once, v);
+ if (_right) apply_predicates(ns, size, stack, eval);
+ }
+ }
+ else
+ {
+ step_fill(ns, c.n, stack.result, once, v);
+ if (_right) apply_predicates(ns, 0, stack, eval);
+ }
+
+ // child, attribute and self axes always generate unique set of nodes
+ // for other axis, if the set stayed sorted, it stayed unique because the traversal algorithms do not visit the same node twice
+ if (axis != axis_child && axis != axis_attribute && axis != axis_self && ns.type() == xpath_node_set::type_unsorted)
+ ns.remove_duplicates(stack.temp);
+
+ return ns;
+ }
+
+ public:
+ xpath_ast_node(ast_type_t type, xpath_value_type rettype_, const char_t* value):
+ _type(static_cast<char>(type)), _rettype(static_cast<char>(rettype_)), _axis(0), _test(0), _left(0), _right(0), _next(0)
+ {
+ assert(type == ast_string_constant);
+ _data.string = value;
+ }
+
+ xpath_ast_node(ast_type_t type, xpath_value_type rettype_, double value):
+ _type(static_cast<char>(type)), _rettype(static_cast<char>(rettype_)), _axis(0), _test(0), _left(0), _right(0), _next(0)
+ {
+ assert(type == ast_number_constant);
+ _data.number = value;
+ }
+
+ xpath_ast_node(ast_type_t type, xpath_value_type rettype_, xpath_variable* value):
+ _type(static_cast<char>(type)), _rettype(static_cast<char>(rettype_)), _axis(0), _test(0), _left(0), _right(0), _next(0)
+ {
+ assert(type == ast_variable);
+ _data.variable = value;
+ }
+
+ xpath_ast_node(ast_type_t type, xpath_value_type rettype_, xpath_ast_node* left = 0, xpath_ast_node* right = 0):
+ _type(static_cast<char>(type)), _rettype(static_cast<char>(rettype_)), _axis(0), _test(0), _left(left), _right(right), _next(0)
+ {
+ }
+
+ xpath_ast_node(ast_type_t type, xpath_ast_node* left, axis_t axis, nodetest_t test, const char_t* contents):
+ _type(static_cast<char>(type)), _rettype(xpath_type_node_set), _axis(static_cast<char>(axis)), _test(static_cast<char>(test)), _left(left), _right(0), _next(0)
+ {
+ assert(type == ast_step);
+ _data.nodetest = contents;
+ }
+
+ xpath_ast_node(ast_type_t type, xpath_ast_node* left, xpath_ast_node* right, predicate_t test):
+ _type(static_cast<char>(type)), _rettype(xpath_type_node_set), _axis(0), _test(static_cast<char>(test)), _left(left), _right(right), _next(0)
+ {
+ assert(type == ast_filter || type == ast_predicate);
+ }
+
+ void set_next(xpath_ast_node* value)
+ {
+ _next = value;
+ }
+
+ void set_right(xpath_ast_node* value)
+ {
+ _right = value;
+ }
+
+ bool eval_boolean(const xpath_context& c, const xpath_stack& stack)
+ {
+ switch (_type)
+ {
+ case ast_op_or:
+ return _left->eval_boolean(c, stack) || _right->eval_boolean(c, stack);
+
+ case ast_op_and:
+ return _left->eval_boolean(c, stack) && _right->eval_boolean(c, stack);
+
+ case ast_op_equal:
+ return compare_eq(_left, _right, c, stack, equal_to());
+
+ case ast_op_not_equal:
+ return compare_eq(_left, _right, c, stack, not_equal_to());
+
+ case ast_op_less:
+ return compare_rel(_left, _right, c, stack, less());
+
+ case ast_op_greater:
+ return compare_rel(_right, _left, c, stack, less());
+
+ case ast_op_less_or_equal:
+ return compare_rel(_left, _right, c, stack, less_equal());
+
+ case ast_op_greater_or_equal:
+ return compare_rel(_right, _left, c, stack, less_equal());
+
+ case ast_func_starts_with:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_string lr = _left->eval_string(c, stack);
+ xpath_string rr = _right->eval_string(c, stack);
+
+ return starts_with(lr.c_str(), rr.c_str());
+ }
+
+ case ast_func_contains:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_string lr = _left->eval_string(c, stack);
+ xpath_string rr = _right->eval_string(c, stack);
+
+ return find_substring(lr.c_str(), rr.c_str()) != 0;
+ }
+
+ case ast_func_boolean:
+ return _left->eval_boolean(c, stack);
+
+ case ast_func_not:
+ return !_left->eval_boolean(c, stack);
+
+ case ast_func_true:
+ return true;
+
+ case ast_func_false:
+ return false;
+
+ case ast_func_lang:
+ {
+ if (c.n.attribute()) return false;
+
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_string lang = _left->eval_string(c, stack);
+
+ for (xml_node n = c.n.node(); n; n = n.parent())
+ {
+ xml_attribute a = n.attribute(PUGIXML_TEXT("xml:lang"));
+
+ if (a)
+ {
+ const char_t* value = a.value();
+
+ // strnicmp / strncasecmp is not portable
+ for (const char_t* lit = lang.c_str(); *lit; ++lit)
+ {
+ if (tolower_ascii(*lit) != tolower_ascii(*value)) return false;
+ ++value;
+ }
+
+ return *value == 0 || *value == '-';
+ }
+ }
+
+ return false;
+ }
+
+ case ast_opt_compare_attribute:
+ {
+ const char_t* value = (_right->_type == ast_string_constant) ? _right->_data.string : _right->_data.variable->get_string();
+
+ xml_attribute attr = c.n.node().attribute(_left->_data.nodetest);
+
+ return attr && strequal(attr.value(), value) && is_xpath_attribute(attr.name());
+ }
+
+ case ast_variable:
+ {
+ assert(_rettype == _data.variable->type());
+
+ if (_rettype == xpath_type_boolean)
+ return _data.variable->get_boolean();
+
+ // variable needs to be converted to the correct type, this is handled by the fallthrough block below
+ break;
+ }
+
+ default:
+ ;
+ }
+
+ // none of the ast types that return the value directly matched, we need to perform type conversion
+ switch (_rettype)
+ {
+ case xpath_type_number:
+ return convert_number_to_boolean(eval_number(c, stack));
+
+ case xpath_type_string:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ return !eval_string(c, stack).empty();
+ }
+
+ case xpath_type_node_set:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ return !eval_node_set(c, stack, nodeset_eval_any).empty();
+ }
+
+ default:
+ assert(false && "Wrong expression for return type boolean"); // unreachable
+ return false;
+ }
+ }
+
+ double eval_number(const xpath_context& c, const xpath_stack& stack)
+ {
+ switch (_type)
+ {
+ case ast_op_add:
+ return _left->eval_number(c, stack) + _right->eval_number(c, stack);
+
+ case ast_op_subtract:
+ return _left->eval_number(c, stack) - _right->eval_number(c, stack);
+
+ case ast_op_multiply:
+ return _left->eval_number(c, stack) * _right->eval_number(c, stack);
+
+ case ast_op_divide:
+ return _left->eval_number(c, stack) / _right->eval_number(c, stack);
+
+ case ast_op_mod:
+ return fmod(_left->eval_number(c, stack), _right->eval_number(c, stack));
+
+ case ast_op_negate:
+ return -_left->eval_number(c, stack);
+
+ case ast_number_constant:
+ return _data.number;
+
+ case ast_func_last:
+ return static_cast<double>(c.size);
+
+ case ast_func_position:
+ return static_cast<double>(c.position);
+
+ case ast_func_count:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ return static_cast<double>(_left->eval_node_set(c, stack, nodeset_eval_all).size());
+ }
+
+ case ast_func_string_length_0:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ return static_cast<double>(string_value(c.n, stack.result).length());
+ }
+
+ case ast_func_string_length_1:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ return static_cast<double>(_left->eval_string(c, stack).length());
+ }
+
+ case ast_func_number_0:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ return convert_string_to_number(string_value(c.n, stack.result).c_str());
+ }
+
+ case ast_func_number_1:
+ return _left->eval_number(c, stack);
+
+ case ast_func_sum:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ double r = 0;
+
+ xpath_node_set_raw ns = _left->eval_node_set(c, stack, nodeset_eval_all);
+
+ for (const xpath_node* it = ns.begin(); it != ns.end(); ++it)
+ {
+ xpath_allocator_capture cri(stack.result);
+
+ r += convert_string_to_number(string_value(*it, stack.result).c_str());
+ }
+
+ return r;
+ }
+
+ case ast_func_floor:
+ {
+ double r = _left->eval_number(c, stack);
+
+ return r == r ? floor(r) : r;
+ }
+
+ case ast_func_ceiling:
+ {
+ double r = _left->eval_number(c, stack);
+
+ return r == r ? ceil(r) : r;
+ }
+
+ case ast_func_round:
+ return round_nearest_nzero(_left->eval_number(c, stack));
+
+ case ast_variable:
+ {
+ assert(_rettype == _data.variable->type());
+
+ if (_rettype == xpath_type_number)
+ return _data.variable->get_number();
+
+ // variable needs to be converted to the correct type, this is handled by the fallthrough block below
+ break;
+ }
+
+ default:
+ ;
+ }
+
+ // none of the ast types that return the value directly matched, we need to perform type conversion
+ switch (_rettype)
+ {
+ case xpath_type_boolean:
+ return eval_boolean(c, stack) ? 1 : 0;
+
+ case xpath_type_string:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ return convert_string_to_number(eval_string(c, stack).c_str());
+ }
+
+ case xpath_type_node_set:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ return convert_string_to_number(eval_string(c, stack).c_str());
+ }
+
+ default:
+ assert(false && "Wrong expression for return type number"); // unreachable
+ return 0;
+ }
+ }
+
+ xpath_string eval_string_concat(const xpath_context& c, const xpath_stack& stack)
+ {
+ assert(_type == ast_func_concat);
+
+ xpath_allocator_capture ct(stack.temp);
+
+ // count the string number
+ size_t count = 1;
+ for (xpath_ast_node* nc = _right; nc; nc = nc->_next) count++;
+
+ // allocate a buffer for temporary string objects
+ xpath_string* buffer = static_cast<xpath_string*>(stack.temp->allocate(count * sizeof(xpath_string)));
+ if (!buffer) return xpath_string();
+
+ // evaluate all strings to temporary stack
+ xpath_stack swapped_stack = {stack.temp, stack.result};
+
+ buffer[0] = _left->eval_string(c, swapped_stack);
+
+ size_t pos = 1;
+ for (xpath_ast_node* n = _right; n; n = n->_next, ++pos) buffer[pos] = n->eval_string(c, swapped_stack);
+ assert(pos == count);
+
+ // get total length
+ size_t length = 0;
+ for (size_t i = 0; i < count; ++i) length += buffer[i].length();
+
+ // create final string
+ char_t* result = static_cast<char_t*>(stack.result->allocate((length + 1) * sizeof(char_t)));
+ if (!result) return xpath_string();
+
+ char_t* ri = result;
+
+ for (size_t j = 0; j < count; ++j)
+ for (const char_t* bi = buffer[j].c_str(); *bi; ++bi)
+ *ri++ = *bi;
+
+ *ri = 0;
+
+ return xpath_string::from_heap_preallocated(result, ri);
+ }
+
+ xpath_string eval_string(const xpath_context& c, const xpath_stack& stack)
+ {
+ switch (_type)
+ {
+ case ast_string_constant:
+ return xpath_string::from_const(_data.string);
+
+ case ast_func_local_name_0:
+ {
+ xpath_node na = c.n;
+
+ return xpath_string::from_const(local_name(na));
+ }
+
+ case ast_func_local_name_1:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_node_set_raw ns = _left->eval_node_set(c, stack, nodeset_eval_first);
+ xpath_node na = ns.first();
+
+ return xpath_string::from_const(local_name(na));
+ }
+
+ case ast_func_name_0:
+ {
+ xpath_node na = c.n;
+
+ return xpath_string::from_const(qualified_name(na));
+ }
+
+ case ast_func_name_1:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_node_set_raw ns = _left->eval_node_set(c, stack, nodeset_eval_first);
+ xpath_node na = ns.first();
+
+ return xpath_string::from_const(qualified_name(na));
+ }
+
+ case ast_func_namespace_uri_0:
+ {
+ xpath_node na = c.n;
+
+ return xpath_string::from_const(namespace_uri(na));
+ }
+
+ case ast_func_namespace_uri_1:
+ {
+ xpath_allocator_capture cr(stack.result);
+
+ xpath_node_set_raw ns = _left->eval_node_set(c, stack, nodeset_eval_first);
+ xpath_node na = ns.first();
+
+ return xpath_string::from_const(namespace_uri(na));
+ }
+
+ case ast_func_string_0:
+ return string_value(c.n, stack.result);
+
+ case ast_func_string_1:
+ return _left->eval_string(c, stack);
+
+ case ast_func_concat:
+ return eval_string_concat(c, stack);
+
+ case ast_func_substring_before:
+ {
+ xpath_allocator_capture cr(stack.temp);
+
+ xpath_stack swapped_stack = {stack.temp, stack.result};
+
+ xpath_string s = _left->eval_string(c, swapped_stack);
+ xpath_string p = _right->eval_string(c, swapped_stack);
+
+ const char_t* pos = find_substring(s.c_str(), p.c_str());
+
+ return pos ? xpath_string::from_heap(s.c_str(), pos, stack.result) : xpath_string();
+ }
+
+ case ast_func_substring_after:
+ {
+ xpath_allocator_capture cr(stack.temp);
+
+ xpath_stack swapped_stack = {stack.temp, stack.result};
+
+ xpath_string s = _left->eval_string(c, swapped_stack);
+ xpath_string p = _right->eval_string(c, swapped_stack);
+
+ const char_t* pos = find_substring(s.c_str(), p.c_str());
+ if (!pos) return xpath_string();
+
+ const char_t* rbegin = pos + p.length();
+ const char_t* rend = s.c_str() + s.length();
+
+ return s.uses_heap() ? xpath_string::from_heap(rbegin, rend, stack.result) : xpath_string::from_const(rbegin);
+ }
+
+ case ast_func_substring_2:
+ {
+ xpath_allocator_capture cr(stack.temp);
+
+ xpath_stack swapped_stack = {stack.temp, stack.result};
+
+ xpath_string s = _left->eval_string(c, swapped_stack);
+ size_t s_length = s.length();
+
+ double first = round_nearest(_right->eval_number(c, stack));
+
+ if (is_nan(first)) return xpath_string(); // NaN
+ else if (first >= static_cast<double>(s_length + 1)) return xpath_string();
+
+ size_t pos = first < 1 ? 1 : static_cast<size_t>(first);
+ assert(1 <= pos && pos <= s_length + 1);
+
+ const char_t* rbegin = s.c_str() + (pos - 1);
+ const char_t* rend = s.c_str() + s.length();
+
+ return s.uses_heap() ? xpath_string::from_heap(rbegin, rend, stack.result) : xpath_string::from_const(rbegin);
+ }
+
+ case ast_func_substring_3:
+ {
+ xpath_allocator_capture cr(stack.temp);
+
+ xpath_stack swapped_stack = {stack.temp, stack.result};
+
+ xpath_string s = _left->eval_string(c, swapped_stack);
+ size_t s_length = s.length();
+
+ double first = round_nearest(_right->eval_number(c, stack));
+ double last = first + round_nearest(_right->_next->eval_number(c, stack));
+
+ if (is_nan(first) || is_nan(last)) return xpath_string();
+ else if (first >= static_cast<double>(s_length + 1)) return xpath_string();
+ else if (first >= last) return xpath_string();
+ else if (last < 1) return xpath_string();
+
+ size_t pos = first < 1 ? 1 : static_cast<size_t>(first);
+ size_t end = last >= static_cast<double>(s_length + 1) ? s_length + 1 : static_cast<size_t>(last);
+
+ assert(1 <= pos && pos <= end && end <= s_length + 1);
+ const char_t* rbegin = s.c_str() + (pos - 1);
+ const char_t* rend = s.c_str() + (end - 1);
+
+ return (end == s_length + 1 && !s.uses_heap()) ? xpath_string::from_const(rbegin) : xpath_string::from_heap(rbegin, rend, stack.result);
+ }
+
+ case ast_func_normalize_space_0:
+ {
+ xpath_string s = string_value(c.n, stack.result);
+
+ char_t* begin = s.data(stack.result);
+ if (!begin) return xpath_string();
+
+ char_t* end = normalize_space(begin);
+
+ return xpath_string::from_heap_preallocated(begin, end);
+ }
+
+ case ast_func_normalize_space_1:
+ {
+ xpath_string s = _left->eval_string(c, stack);
+
+ char_t* begin = s.data(stack.result);
+ if (!begin) return xpath_string();
+
+ char_t* end = normalize_space(begin);
+
+ return xpath_string::from_heap_preallocated(begin, end);
+ }
+
+ case ast_func_translate:
+ {
+ xpath_allocator_capture cr(stack.temp);
+
+ xpath_stack swapped_stack = {stack.temp, stack.result};
+
+ xpath_string s = _left->eval_string(c, stack);
+ xpath_string from = _right->eval_string(c, swapped_stack);
+ xpath_string to = _right->_next->eval_string(c, swapped_stack);
+
+ char_t* begin = s.data(stack.result);
+ if (!begin) return xpath_string();
+
+ char_t* end = translate(begin, from.c_str(), to.c_str(), to.length());
+
+ return xpath_string::from_heap_preallocated(begin, end);
+ }
+
+ case ast_opt_translate_table:
+ {
+ xpath_string s = _left->eval_string(c, stack);
+
+ char_t* begin = s.data(stack.result);
+ if (!begin) return xpath_string();
+
+ char_t* end = translate_table(begin, _data.table);
+
+ return xpath_string::from_heap_preallocated(begin, end);
+ }
+
+ case ast_variable:
+ {
+ assert(_rettype == _data.variable->type());
+
+ if (_rettype == xpath_type_string)
+ return xpath_string::from_const(_data.variable->get_string());
+
+ // variable needs to be converted to the correct type, this is handled by the fallthrough block below
+ break;
+ }
+
+ default:
+ ;
+ }
+
+ // none of the ast types that return the value directly matched, we need to perform type conversion
+ switch (_rettype)
+ {
+ case xpath_type_boolean:
+ return xpath_string::from_const(eval_boolean(c, stack) ? PUGIXML_TEXT("true") : PUGIXML_TEXT("false"));
+
+ case xpath_type_number:
+ return convert_number_to_string(eval_number(c, stack), stack.result);
+
+ case xpath_type_node_set:
+ {
+ xpath_allocator_capture cr(stack.temp);
+
+ xpath_stack swapped_stack = {stack.temp, stack.result};
+
+ xpath_node_set_raw ns = eval_node_set(c, swapped_stack, nodeset_eval_first);
+ return ns.empty() ? xpath_string() : string_value(ns.first(), stack.result);
+ }
+
+ default:
+ assert(false && "Wrong expression for return type string"); // unreachable
+ return xpath_string();
+ }
+ }
+
+ xpath_node_set_raw eval_node_set(const xpath_context& c, const xpath_stack& stack, nodeset_eval_t eval)
+ {
+ switch (_type)
+ {
+ case ast_op_union:
+ {
+ xpath_allocator_capture cr(stack.temp);
+
+ xpath_stack swapped_stack = {stack.temp, stack.result};
+
+ xpath_node_set_raw ls = _left->eval_node_set(c, stack, eval);
+ xpath_node_set_raw rs = _right->eval_node_set(c, swapped_stack, eval);
+
+ // we can optimize merging two sorted sets, but this is a very rare operation, so don't bother
+ ls.set_type(xpath_node_set::type_unsorted);
+
+ ls.append(rs.begin(), rs.end(), stack.result);
+ ls.remove_duplicates(stack.temp);
+
+ return ls;
+ }
+
+ case ast_filter:
+ {
+ xpath_node_set_raw set = _left->eval_node_set(c, stack, _test == predicate_constant_one ? nodeset_eval_first : nodeset_eval_all);
+
+ // either expression is a number or it contains position() call; sort by document order
+ if (_test != predicate_posinv) set.sort_do();
+
+ bool once = eval_once(set.type(), eval);
+
+ apply_predicate(set, 0, stack, once);
+
+ return set;
+ }
+
+ case ast_func_id:
+ return xpath_node_set_raw();
+
+ case ast_step:
+ {
+ switch (_axis)
+ {
+ case axis_ancestor:
+ return step_do(c, stack, eval, axis_to_type<axis_ancestor>());
+
+ case axis_ancestor_or_self:
+ return step_do(c, stack, eval, axis_to_type<axis_ancestor_or_self>());
+
+ case axis_attribute:
+ return step_do(c, stack, eval, axis_to_type<axis_attribute>());
+
+ case axis_child:
+ return step_do(c, stack, eval, axis_to_type<axis_child>());
+
+ case axis_descendant:
+ return step_do(c, stack, eval, axis_to_type<axis_descendant>());
+
+ case axis_descendant_or_self:
+ return step_do(c, stack, eval, axis_to_type<axis_descendant_or_self>());
+
+ case axis_following:
+ return step_do(c, stack, eval, axis_to_type<axis_following>());
+
+ case axis_following_sibling:
+ return step_do(c, stack, eval, axis_to_type<axis_following_sibling>());
+
+ case axis_namespace:
+ // namespaced axis is not supported
+ return xpath_node_set_raw();
+
+ case axis_parent:
+ return step_do(c, stack, eval, axis_to_type<axis_parent>());
+
+ case axis_preceding:
+ return step_do(c, stack, eval, axis_to_type<axis_preceding>());
+
+ case axis_preceding_sibling:
+ return step_do(c, stack, eval, axis_to_type<axis_preceding_sibling>());
+
+ case axis_self:
+ return step_do(c, stack, eval, axis_to_type<axis_self>());
+
+ default:
+ assert(false && "Unknown axis"); // unreachable
+ return xpath_node_set_raw();
+ }
+ }
+
+ case ast_step_root:
+ {
+ assert(!_right); // root step can't have any predicates
+
+ xpath_node_set_raw ns;
+
+ ns.set_type(xpath_node_set::type_sorted);
+
+ if (c.n.node()) ns.push_back(c.n.node().root(), stack.result);
+ else if (c.n.attribute()) ns.push_back(c.n.parent().root(), stack.result);
+
+ return ns;
+ }
+
+ case ast_variable:
+ {
+ assert(_rettype == _data.variable->type());
+
+ if (_rettype == xpath_type_node_set)
+ {
+ const xpath_node_set& s = _data.variable->get_node_set();
+
+ xpath_node_set_raw ns;
+
+ ns.set_type(s.type());
+ ns.append(s.begin(), s.end(), stack.result);
+
+ return ns;
+ }
+
+ // variable needs to be converted to the correct type, this is handled by the fallthrough block below
+ break;
+ }
+
+ default:
+ ;
+ }
+
+ // none of the ast types that return the value directly matched, but conversions to node set are invalid
+ assert(false && "Wrong expression for return type node set"); // unreachable
+ return xpath_node_set_raw();
+ }
+
+ void optimize(xpath_allocator* alloc)
+ {
+ if (_left)
+ _left->optimize(alloc);
+
+ if (_right)
+ _right->optimize(alloc);
+
+ if (_next)
+ _next->optimize(alloc);
+
+ // coverity[var_deref_model]
+ optimize_self(alloc);
+ }
+
+ void optimize_self(xpath_allocator* alloc)
+ {
+ // Rewrite [position()=expr] with [expr]
+ // Note that this step has to go before classification to recognize [position()=1]
+ if ((_type == ast_filter || _type == ast_predicate) &&
+ _right && // workaround for clang static analyzer (_right is never null for ast_filter/ast_predicate)
+ _right->_type == ast_op_equal && _right->_left->_type == ast_func_position && _right->_right->_rettype == xpath_type_number)
+ {
+ _right = _right->_right;
+ }
+
+ // Classify filter/predicate ops to perform various optimizations during evaluation
+ if ((_type == ast_filter || _type == ast_predicate) && _right) // workaround for clang static analyzer (_right is never null for ast_filter/ast_predicate)
+ {
+ assert(_test == predicate_default);
+
+ if (_right->_type == ast_number_constant && _right->_data.number == 1.0)
+ _test = predicate_constant_one;
+ else if (_right->_rettype == xpath_type_number && (_right->_type == ast_number_constant || _right->_type == ast_variable || _right->_type == ast_func_last))
+ _test = predicate_constant;
+ else if (_right->_rettype != xpath_type_number && _right->is_posinv_expr())
+ _test = predicate_posinv;
+ }
+
+ // Rewrite descendant-or-self::node()/child::foo with descendant::foo
+ // The former is a full form of //foo, the latter is much faster since it executes the node test immediately
+ // Do a similar kind of rewrite for self/descendant/descendant-or-self axes
+ // Note that we only rewrite positionally invariant steps (//foo[1] != /descendant::foo[1])
+ if (_type == ast_step && (_axis == axis_child || _axis == axis_self || _axis == axis_descendant || _axis == axis_descendant_or_self) &&
+ _left && _left->_type == ast_step && _left->_axis == axis_descendant_or_self && _left->_test == nodetest_type_node && !_left->_right &&
+ is_posinv_step())
+ {
+ if (_axis == axis_child || _axis == axis_descendant)
+ _axis = axis_descendant;
+ else
+ _axis = axis_descendant_or_self;
+
+ _left = _left->_left;
+ }
+
+ // Use optimized lookup table implementation for translate() with constant arguments
+ if (_type == ast_func_translate &&
+ _right && // workaround for clang static analyzer (_right is never null for ast_func_translate)
+ _right->_type == ast_string_constant && _right->_next->_type == ast_string_constant)
+ {
+ unsigned char* table = translate_table_generate(alloc, _right->_data.string, _right->_next->_data.string);
+
+ if (table)
+ {
+ _type = ast_opt_translate_table;
+ _data.table = table;
+ }
+ }
+
+ // Use optimized path for @attr = 'value' or @attr = $value
+ if (_type == ast_op_equal &&
+ _left && _right && // workaround for clang static analyzer and Coverity (_left and _right are never null for ast_op_equal)
+ // coverity[mixed_enums]
+ _left->_type == ast_step && _left->_axis == axis_attribute && _left->_test == nodetest_name && !_left->_left && !_left->_right &&
+ (_right->_type == ast_string_constant || (_right->_type == ast_variable && _right->_rettype == xpath_type_string)))
+ {
+ _type = ast_opt_compare_attribute;
+ }
+ }
+
+ bool is_posinv_expr() const
+ {
+ switch (_type)
+ {
+ case ast_func_position:
+ case ast_func_last:
+ return false;
+
+ case ast_string_constant:
+ case ast_number_constant:
+ case ast_variable:
+ return true;
+
+ case ast_step:
+ case ast_step_root:
+ return true;
+
+ case ast_predicate:
+ case ast_filter:
+ return true;
+
+ default:
+ if (_left && !_left->is_posinv_expr()) return false;
+
+ for (xpath_ast_node* n = _right; n; n = n->_next)
+ if (!n->is_posinv_expr()) return false;
+
+ return true;
+ }
+ }
+
+ bool is_posinv_step() const
+ {
+ assert(_type == ast_step);
+
+ for (xpath_ast_node* n = _right; n; n = n->_next)
+ {
+ assert(n->_type == ast_predicate);
+
+ if (n->_test != predicate_posinv)
+ return false;
+ }
+
+ return true;
+ }
+
+ xpath_value_type rettype() const
+ {
+ return static_cast<xpath_value_type>(_rettype);
+ }
+ };
+
+ static const size_t xpath_ast_depth_limit =
+ #ifdef PUGIXML_XPATH_DEPTH_LIMIT
+ PUGIXML_XPATH_DEPTH_LIMIT
+ #else
+ 1024
+ #endif
+ ;
+
+ struct xpath_parser
+ {
+ xpath_allocator* _alloc;
+ xpath_lexer _lexer;
+
+ const char_t* _query;
+ xpath_variable_set* _variables;
+
+ xpath_parse_result* _result;
+
+ char_t _scratch[32];
+
+ size_t _depth;
+
+ xpath_ast_node* error(const char* message)
+ {
+ _result->error = message;
+ _result->offset = _lexer.current_pos() - _query;
+
+ return 0;
+ }
+
+ xpath_ast_node* error_oom()
+ {
+ assert(_alloc->_error);
+ *_alloc->_error = true;
+
+ return 0;
+ }
+
+ xpath_ast_node* error_rec()
+ {
+ return error("Exceeded maximum allowed query depth");
+ }
+
+ void* alloc_node()
+ {
+ return _alloc->allocate(sizeof(xpath_ast_node));
+ }
+
+ xpath_ast_node* alloc_node(ast_type_t type, xpath_value_type rettype, const char_t* value)
+ {
+ void* memory = alloc_node();
+ return memory ? new (memory) xpath_ast_node(type, rettype, value) : 0;
+ }
+
+ xpath_ast_node* alloc_node(ast_type_t type, xpath_value_type rettype, double value)
+ {
+ void* memory = alloc_node();
+ return memory ? new (memory) xpath_ast_node(type, rettype, value) : 0;
+ }
+
+ xpath_ast_node* alloc_node(ast_type_t type, xpath_value_type rettype, xpath_variable* value)
+ {
+ void* memory = alloc_node();
+ return memory ? new (memory) xpath_ast_node(type, rettype, value) : 0;
+ }
+
+ xpath_ast_node* alloc_node(ast_type_t type, xpath_value_type rettype, xpath_ast_node* left = 0, xpath_ast_node* right = 0)
+ {
+ void* memory = alloc_node();
+ return memory ? new (memory) xpath_ast_node(type, rettype, left, right) : 0;
+ }
+
+ xpath_ast_node* alloc_node(ast_type_t type, xpath_ast_node* left, axis_t axis, nodetest_t test, const char_t* contents)
+ {
+ void* memory = alloc_node();
+ return memory ? new (memory) xpath_ast_node(type, left, axis, test, contents) : 0;
+ }
+
+ xpath_ast_node* alloc_node(ast_type_t type, xpath_ast_node* left, xpath_ast_node* right, predicate_t test)
+ {
+ void* memory = alloc_node();
+ return memory ? new (memory) xpath_ast_node(type, left, right, test) : 0;
+ }
+
+ const char_t* alloc_string(const xpath_lexer_string& value)
+ {
+ if (!value.begin)
+ return PUGIXML_TEXT("");
+
+ size_t length = static_cast<size_t>(value.end - value.begin);
+
+ char_t* c = static_cast<char_t*>(_alloc->allocate((length + 1) * sizeof(char_t)));
+ if (!c) return 0;
+
+ memcpy(c, value.begin, length * sizeof(char_t));
+ c[length] = 0;
+
+ return c;
+ }
+
+ xpath_ast_node* parse_function(const xpath_lexer_string& name, size_t argc, xpath_ast_node* args[2])
+ {
+ switch (name.begin[0])
+ {
+ case 'b':
+ if (name == PUGIXML_TEXT("boolean") && argc == 1)
+ return alloc_node(ast_func_boolean, xpath_type_boolean, args[0]);
+
+ break;
+
+ case 'c':
+ if (name == PUGIXML_TEXT("count") && argc == 1)
+ {
+ if (args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set");
+ return alloc_node(ast_func_count, xpath_type_number, args[0]);
+ }
+ else if (name == PUGIXML_TEXT("contains") && argc == 2)
+ return alloc_node(ast_func_contains, xpath_type_boolean, args[0], args[1]);
+ else if (name == PUGIXML_TEXT("concat") && argc >= 2)
+ return alloc_node(ast_func_concat, xpath_type_string, args[0], args[1]);
+ else if (name == PUGIXML_TEXT("ceiling") && argc == 1)
+ return alloc_node(ast_func_ceiling, xpath_type_number, args[0]);
+
+ break;
+
+ case 'f':
+ if (name == PUGIXML_TEXT("false") && argc == 0)
+ return alloc_node(ast_func_false, xpath_type_boolean);
+ else if (name == PUGIXML_TEXT("floor") && argc == 1)
+ return alloc_node(ast_func_floor, xpath_type_number, args[0]);
+
+ break;
+
+ case 'i':
+ if (name == PUGIXML_TEXT("id") && argc == 1)
+ return alloc_node(ast_func_id, xpath_type_node_set, args[0]);
+
+ break;
+
+ case 'l':
+ if (name == PUGIXML_TEXT("last") && argc == 0)
+ return alloc_node(ast_func_last, xpath_type_number);
+ else if (name == PUGIXML_TEXT("lang") && argc == 1)
+ return alloc_node(ast_func_lang, xpath_type_boolean, args[0]);
+ else if (name == PUGIXML_TEXT("local-name") && argc <= 1)
+ {
+ if (argc == 1 && args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set");
+ return alloc_node(argc == 0 ? ast_func_local_name_0 : ast_func_local_name_1, xpath_type_string, args[0]);
+ }
+
+ break;
+
+ case 'n':
+ if (name == PUGIXML_TEXT("name") && argc <= 1)
+ {
+ if (argc == 1 && args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set");
+ return alloc_node(argc == 0 ? ast_func_name_0 : ast_func_name_1, xpath_type_string, args[0]);
+ }
+ else if (name == PUGIXML_TEXT("namespace-uri") && argc <= 1)
+ {
+ if (argc == 1 && args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set");
+ return alloc_node(argc == 0 ? ast_func_namespace_uri_0 : ast_func_namespace_uri_1, xpath_type_string, args[0]);
+ }
+ else if (name == PUGIXML_TEXT("normalize-space") && argc <= 1)
+ return alloc_node(argc == 0 ? ast_func_normalize_space_0 : ast_func_normalize_space_1, xpath_type_string, args[0], args[1]);
+ else if (name == PUGIXML_TEXT("not") && argc == 1)
+ return alloc_node(ast_func_not, xpath_type_boolean, args[0]);
+ else if (name == PUGIXML_TEXT("number") && argc <= 1)
+ return alloc_node(argc == 0 ? ast_func_number_0 : ast_func_number_1, xpath_type_number, args[0]);
+
+ break;
+
+ case 'p':
+ if (name == PUGIXML_TEXT("position") && argc == 0)
+ return alloc_node(ast_func_position, xpath_type_number);
+
+ break;
+
+ case 'r':
+ if (name == PUGIXML_TEXT("round") && argc == 1)
+ return alloc_node(ast_func_round, xpath_type_number, args[0]);
+
+ break;
+
+ case 's':
+ if (name == PUGIXML_TEXT("string") && argc <= 1)
+ return alloc_node(argc == 0 ? ast_func_string_0 : ast_func_string_1, xpath_type_string, args[0]);
+ else if (name == PUGIXML_TEXT("string-length") && argc <= 1)
+ return alloc_node(argc == 0 ? ast_func_string_length_0 : ast_func_string_length_1, xpath_type_number, args[0]);
+ else if (name == PUGIXML_TEXT("starts-with") && argc == 2)
+ return alloc_node(ast_func_starts_with, xpath_type_boolean, args[0], args[1]);
+ else if (name == PUGIXML_TEXT("substring-before") && argc == 2)
+ return alloc_node(ast_func_substring_before, xpath_type_string, args[0], args[1]);
+ else if (name == PUGIXML_TEXT("substring-after") && argc == 2)
+ return alloc_node(ast_func_substring_after, xpath_type_string, args[0], args[1]);
+ else if (name == PUGIXML_TEXT("substring") && (argc == 2 || argc == 3))
+ return alloc_node(argc == 2 ? ast_func_substring_2 : ast_func_substring_3, xpath_type_string, args[0], args[1]);
+ else if (name == PUGIXML_TEXT("sum") && argc == 1)
+ {
+ if (args[0]->rettype() != xpath_type_node_set) return error("Function has to be applied to node set");
+ return alloc_node(ast_func_sum, xpath_type_number, args[0]);
+ }
+
+ break;
+
+ case 't':
+ if (name == PUGIXML_TEXT("translate") && argc == 3)
+ return alloc_node(ast_func_translate, xpath_type_string, args[0], args[1]);
+ else if (name == PUGIXML_TEXT("true") && argc == 0)
+ return alloc_node(ast_func_true, xpath_type_boolean);
+
+ break;
+
+ default:
+ break;
+ }
+
+ return error("Unrecognized function or wrong parameter count");
+ }
+
+ axis_t parse_axis_name(const xpath_lexer_string& name, bool& specified)
+ {
+ specified = true;
+
+ switch (name.begin[0])
+ {
+ case 'a':
+ if (name == PUGIXML_TEXT("ancestor"))
+ return axis_ancestor;
+ else if (name == PUGIXML_TEXT("ancestor-or-self"))
+ return axis_ancestor_or_self;
+ else if (name == PUGIXML_TEXT("attribute"))
+ return axis_attribute;
+
+ break;
+
+ case 'c':
+ if (name == PUGIXML_TEXT("child"))
+ return axis_child;
+
+ break;
+
+ case 'd':
+ if (name == PUGIXML_TEXT("descendant"))
+ return axis_descendant;
+ else if (name == PUGIXML_TEXT("descendant-or-self"))
+ return axis_descendant_or_self;
+
+ break;
+
+ case 'f':
+ if (name == PUGIXML_TEXT("following"))
+ return axis_following;
+ else if (name == PUGIXML_TEXT("following-sibling"))
+ return axis_following_sibling;
+
+ break;
+
+ case 'n':
+ if (name == PUGIXML_TEXT("namespace"))
+ return axis_namespace;
+
+ break;
+
+ case 'p':
+ if (name == PUGIXML_TEXT("parent"))
+ return axis_parent;
+ else if (name == PUGIXML_TEXT("preceding"))
+ return axis_preceding;
+ else if (name == PUGIXML_TEXT("preceding-sibling"))
+ return axis_preceding_sibling;
+
+ break;
+
+ case 's':
+ if (name == PUGIXML_TEXT("self"))
+ return axis_self;
+
+ break;
+
+ default:
+ break;
+ }
+
+ specified = false;
+ return axis_child;
+ }
+
+ nodetest_t parse_node_test_type(const xpath_lexer_string& name)
+ {
+ switch (name.begin[0])
+ {
+ case 'c':
+ if (name == PUGIXML_TEXT("comment"))
+ return nodetest_type_comment;
+
+ break;
+
+ case 'n':
+ if (name == PUGIXML_TEXT("node"))
+ return nodetest_type_node;
+
+ break;
+
+ case 'p':
+ if (name == PUGIXML_TEXT("processing-instruction"))
+ return nodetest_type_pi;
+
+ break;
+
+ case 't':
+ if (name == PUGIXML_TEXT("text"))
+ return nodetest_type_text;
+
+ break;
+
+ default:
+ break;
+ }
+
+ return nodetest_none;
+ }
+
+ // PrimaryExpr ::= VariableReference | '(' Expr ')' | Literal | Number | FunctionCall
+ xpath_ast_node* parse_primary_expression()
+ {
+ switch (_lexer.current())
+ {
+ case lex_var_ref:
+ {
+ xpath_lexer_string name = _lexer.contents();
+
+ if (!_variables)
+ return error("Unknown variable: variable set is not provided");
+
+ xpath_variable* var = 0;
+ if (!get_variable_scratch(_scratch, _variables, name.begin, name.end, &var))
+ return error_oom();
+
+ if (!var)
+ return error("Unknown variable: variable set does not contain the given name");
+
+ _lexer.next();
+
+ return alloc_node(ast_variable, var->type(), var);
+ }
+
+ case lex_open_brace:
+ {
+ _lexer.next();
+
+ xpath_ast_node* n = parse_expression();
+ if (!n) return 0;
+
+ if (_lexer.current() != lex_close_brace)
+ return error("Expected ')' to match an opening '('");
+
+ _lexer.next();
+
+ return n;
+ }
+
+ case lex_quoted_string:
+ {
+ const char_t* value = alloc_string(_lexer.contents());
+ if (!value) return 0;
+
+ _lexer.next();
+
+ return alloc_node(ast_string_constant, xpath_type_string, value);
+ }
+
+ case lex_number:
+ {
+ double value = 0;
+
+ if (!convert_string_to_number_scratch(_scratch, _lexer.contents().begin, _lexer.contents().end, &value))
+ return error_oom();
+
+ _lexer.next();
+
+ return alloc_node(ast_number_constant, xpath_type_number, value);
+ }
+
+ case lex_string:
+ {
+ xpath_ast_node* args[2] = {0};
+ size_t argc = 0;
+
+ xpath_lexer_string function = _lexer.contents();
+ _lexer.next();
+
+ xpath_ast_node* last_arg = 0;
+
+ if (_lexer.current() != lex_open_brace)
+ return error("Unrecognized function call");
+ _lexer.next();
+
+ size_t old_depth = _depth;
+
+ while (_lexer.current() != lex_close_brace)
+ {
+ if (argc > 0)
+ {
+ if (_lexer.current() != lex_comma)
+ return error("No comma between function arguments");
+ _lexer.next();
+ }
+
+ if (++_depth > xpath_ast_depth_limit)
+ return error_rec();
+
+ xpath_ast_node* n = parse_expression();
+ if (!n) return 0;
+
+ if (argc < 2) args[argc] = n;
+ else last_arg->set_next(n);
+
+ argc++;
+ last_arg = n;
+ }
+
+ _lexer.next();
+
+ _depth = old_depth;
+
+ return parse_function(function, argc, args);
+ }
+
+ default:
+ return error("Unrecognizable primary expression");
+ }
+ }
+
+ // FilterExpr ::= PrimaryExpr | FilterExpr Predicate
+ // Predicate ::= '[' PredicateExpr ']'
+ // PredicateExpr ::= Expr
+ xpath_ast_node* parse_filter_expression()
+ {
+ xpath_ast_node* n = parse_primary_expression();
+ if (!n) return 0;
+
+ size_t old_depth = _depth;
+
+ while (_lexer.current() == lex_open_square_brace)
+ {
+ _lexer.next();
+
+ if (++_depth > xpath_ast_depth_limit)
+ return error_rec();
+
+ if (n->rettype() != xpath_type_node_set)
+ return error("Predicate has to be applied to node set");
+
+ xpath_ast_node* expr = parse_expression();
+ if (!expr) return 0;
+
+ n = alloc_node(ast_filter, n, expr, predicate_default);
+ if (!n) return 0;
+
+ if (_lexer.current() != lex_close_square_brace)
+ return error("Expected ']' to match an opening '['");
+
+ _lexer.next();
+ }
+
+ _depth = old_depth;
+
+ return n;
+ }
+
+ // Step ::= AxisSpecifier NodeTest Predicate* | AbbreviatedStep
+ // AxisSpecifier ::= AxisName '::' | '@'?
+ // NodeTest ::= NameTest | NodeType '(' ')' | 'processing-instruction' '(' Literal ')'
+ // NameTest ::= '*' | NCName ':' '*' | QName
+ // AbbreviatedStep ::= '.' | '..'
+ xpath_ast_node* parse_step(xpath_ast_node* set)
+ {
+ if (set && set->rettype() != xpath_type_node_set)
+ return error("Step has to be applied to node set");
+
+ bool axis_specified = false;
+ axis_t axis = axis_child; // implied child axis
+
+ if (_lexer.current() == lex_axis_attribute)
+ {
+ axis = axis_attribute;
+ axis_specified = true;
+
+ _lexer.next();
+ }
+ else if (_lexer.current() == lex_dot)
+ {
+ _lexer.next();
+
+ if (_lexer.current() == lex_open_square_brace)
+ return error("Predicates are not allowed after an abbreviated step");
+
+ return alloc_node(ast_step, set, axis_self, nodetest_type_node, 0);
+ }
+ else if (_lexer.current() == lex_double_dot)
+ {
+ _lexer.next();
+
+ if (_lexer.current() == lex_open_square_brace)
+ return error("Predicates are not allowed after an abbreviated step");
+
+ return alloc_node(ast_step, set, axis_parent, nodetest_type_node, 0);
+ }
+
+ nodetest_t nt_type = nodetest_none;
+ xpath_lexer_string nt_name;
+
+ if (_lexer.current() == lex_string)
+ {
+ // node name test
+ nt_name = _lexer.contents();
+ _lexer.next();
+
+ // was it an axis name?
+ if (_lexer.current() == lex_double_colon)
+ {
+ // parse axis name
+ if (axis_specified)
+ return error("Two axis specifiers in one step");
+
+ axis = parse_axis_name(nt_name, axis_specified);
+
+ if (!axis_specified)
+ return error("Unknown axis");
+
+ // read actual node test
+ _lexer.next();
+
+ if (_lexer.current() == lex_multiply)
+ {
+ nt_type = nodetest_all;
+ nt_name = xpath_lexer_string();
+ _lexer.next();
+ }
+ else if (_lexer.current() == lex_string)
+ {
+ nt_name = _lexer.contents();
+ _lexer.next();
+ }
+ else
+ {
+ return error("Unrecognized node test");
+ }
+ }
+
+ if (nt_type == nodetest_none)
+ {
+ // node type test or processing-instruction
+ if (_lexer.current() == lex_open_brace)
+ {
+ _lexer.next();
+
+ if (_lexer.current() == lex_close_brace)
+ {
+ _lexer.next();
+
+ nt_type = parse_node_test_type(nt_name);
+
+ if (nt_type == nodetest_none)
+ return error("Unrecognized node type");
+
+ nt_name = xpath_lexer_string();
+ }
+ else if (nt_name == PUGIXML_TEXT("processing-instruction"))
+ {
+ if (_lexer.current() != lex_quoted_string)
+ return error("Only literals are allowed as arguments to processing-instruction()");
+
+ nt_type = nodetest_pi;
+ nt_name = _lexer.contents();
+ _lexer.next();
+
+ if (_lexer.current() != lex_close_brace)
+ return error("Unmatched brace near processing-instruction()");
+ _lexer.next();
+ }
+ else
+ {
+ return error("Unmatched brace near node type test");
+ }
+ }
+ // QName or NCName:*
+ else
+ {
+ if (nt_name.end - nt_name.begin > 2 && nt_name.end[-2] == ':' && nt_name.end[-1] == '*') // NCName:*
+ {
+ nt_name.end--; // erase *
+
+ nt_type = nodetest_all_in_namespace;
+ }
+ else
+ {
+ nt_type = nodetest_name;
+ }
+ }
+ }
+ }
+ else if (_lexer.current() == lex_multiply)
+ {
+ nt_type = nodetest_all;
+ _lexer.next();
+ }
+ else
+ {
+ return error("Unrecognized node test");
+ }
+
+ const char_t* nt_name_copy = alloc_string(nt_name);
+ if (!nt_name_copy) return 0;
+
+ xpath_ast_node* n = alloc_node(ast_step, set, axis, nt_type, nt_name_copy);
+ if (!n) return 0;
+
+ size_t old_depth = _depth;
+
+ xpath_ast_node* last = 0;
+
+ while (_lexer.current() == lex_open_square_brace)
+ {
+ _lexer.next();
+
+ if (++_depth > xpath_ast_depth_limit)
+ return error_rec();
+
+ xpath_ast_node* expr = parse_expression();
+ if (!expr) return 0;
+
+ xpath_ast_node* pred = alloc_node(ast_predicate, 0, expr, predicate_default);
+ if (!pred) return 0;
+
+ if (_lexer.current() != lex_close_square_brace)
+ return error("Expected ']' to match an opening '['");
+ _lexer.next();
+
+ if (last) last->set_next(pred);
+ else n->set_right(pred);
+
+ last = pred;
+ }
+
+ _depth = old_depth;
+
+ return n;
+ }
+
+ // RelativeLocationPath ::= Step | RelativeLocationPath '/' Step | RelativeLocationPath '//' Step
+ xpath_ast_node* parse_relative_location_path(xpath_ast_node* set)
+ {
+ xpath_ast_node* n = parse_step(set);
+ if (!n) return 0;
+
+ size_t old_depth = _depth;
+
+ while (_lexer.current() == lex_slash || _lexer.current() == lex_double_slash)
+ {
+ lexeme_t l = _lexer.current();
+ _lexer.next();
+
+ if (l == lex_double_slash)
+ {
+ n = alloc_node(ast_step, n, axis_descendant_or_self, nodetest_type_node, 0);
+ if (!n) return 0;
+
+ ++_depth;
+ }
+
+ if (++_depth > xpath_ast_depth_limit)
+ return error_rec();
+
+ n = parse_step(n);
+ if (!n) return 0;
+ }
+
+ _depth = old_depth;
+
+ return n;
+ }
+
+ // LocationPath ::= RelativeLocationPath | AbsoluteLocationPath
+ // AbsoluteLocationPath ::= '/' RelativeLocationPath? | '//' RelativeLocationPath
+ xpath_ast_node* parse_location_path()
+ {
+ if (_lexer.current() == lex_slash)
+ {
+ _lexer.next();
+
+ xpath_ast_node* n = alloc_node(ast_step_root, xpath_type_node_set);
+ if (!n) return 0;
+
+ // relative location path can start from axis_attribute, dot, double_dot, multiply and string lexemes; any other lexeme means standalone root path
+ lexeme_t l = _lexer.current();
+
+ if (l == lex_string || l == lex_axis_attribute || l == lex_dot || l == lex_double_dot || l == lex_multiply)
+ return parse_relative_location_path(n);
+ else
+ return n;
+ }
+ else if (_lexer.current() == lex_double_slash)
+ {
+ _lexer.next();
+
+ xpath_ast_node* n = alloc_node(ast_step_root, xpath_type_node_set);
+ if (!n) return 0;
+
+ n = alloc_node(ast_step, n, axis_descendant_or_self, nodetest_type_node, 0);
+ if (!n) return 0;
+
+ return parse_relative_location_path(n);
+ }
+
+ // else clause moved outside of if because of bogus warning 'control may reach end of non-void function being inlined' in gcc 4.0.1
+ return parse_relative_location_path(0);
+ }
+
+ // PathExpr ::= LocationPath
+ // | FilterExpr
+ // | FilterExpr '/' RelativeLocationPath
+ // | FilterExpr '//' RelativeLocationPath
+ // UnionExpr ::= PathExpr | UnionExpr '|' PathExpr
+ // UnaryExpr ::= UnionExpr | '-' UnaryExpr
+ xpath_ast_node* parse_path_or_unary_expression()
+ {
+ // Clarification.
+ // PathExpr begins with either LocationPath or FilterExpr.
+ // FilterExpr begins with PrimaryExpr
+ // PrimaryExpr begins with '$' in case of it being a variable reference,
+ // '(' in case of it being an expression, string literal, number constant or
+ // function call.
+ if (_lexer.current() == lex_var_ref || _lexer.current() == lex_open_brace ||
+ _lexer.current() == lex_quoted_string || _lexer.current() == lex_number ||
+ _lexer.current() == lex_string)
+ {
+ if (_lexer.current() == lex_string)
+ {
+ // This is either a function call, or not - if not, we shall proceed with location path
+ const char_t* state = _lexer.state();
+
+ while (PUGI__IS_CHARTYPE(*state, ct_space)) ++state;
+
+ if (*state != '(')
+ return parse_location_path();
+
+ // This looks like a function call; however this still can be a node-test. Check it.
+ if (parse_node_test_type(_lexer.contents()) != nodetest_none)
+ return parse_location_path();
+ }
+
+ xpath_ast_node* n = parse_filter_expression();
+ if (!n) return 0;
+
+ if (_lexer.current() == lex_slash || _lexer.current() == lex_double_slash)
+ {
+ lexeme_t l = _lexer.current();
+ _lexer.next();
+
+ if (l == lex_double_slash)
+ {
+ if (n->rettype() != xpath_type_node_set)
+ return error("Step has to be applied to node set");
+
+ n = alloc_node(ast_step, n, axis_descendant_or_self, nodetest_type_node, 0);
+ if (!n) return 0;
+ }
+
+ // select from location path
+ return parse_relative_location_path(n);
+ }
+
+ return n;
+ }
+ else if (_lexer.current() == lex_minus)
+ {
+ _lexer.next();
+
+ // precedence 7+ - only parses union expressions
+ xpath_ast_node* n = parse_expression(7);
+ if (!n) return 0;
+
+ return alloc_node(ast_op_negate, xpath_type_number, n);
+ }
+ else
+ {
+ return parse_location_path();
+ }
+ }
+
+ struct binary_op_t
+ {
+ ast_type_t asttype;
+ xpath_value_type rettype;
+ int precedence;
+
+ binary_op_t(): asttype(ast_unknown), rettype(xpath_type_none), precedence(0)
+ {
+ }
+
+ binary_op_t(ast_type_t asttype_, xpath_value_type rettype_, int precedence_): asttype(asttype_), rettype(rettype_), precedence(precedence_)
+ {
+ }
+
+ static binary_op_t parse(xpath_lexer& lexer)
+ {
+ switch (lexer.current())
+ {
+ case lex_string:
+ if (lexer.contents() == PUGIXML_TEXT("or"))
+ return binary_op_t(ast_op_or, xpath_type_boolean, 1);
+ else if (lexer.contents() == PUGIXML_TEXT("and"))
+ return binary_op_t(ast_op_and, xpath_type_boolean, 2);
+ else if (lexer.contents() == PUGIXML_TEXT("div"))
+ return binary_op_t(ast_op_divide, xpath_type_number, 6);
+ else if (lexer.contents() == PUGIXML_TEXT("mod"))
+ return binary_op_t(ast_op_mod, xpath_type_number, 6);
+ else
+ return binary_op_t();
+
+ case lex_equal:
+ return binary_op_t(ast_op_equal, xpath_type_boolean, 3);
+
+ case lex_not_equal:
+ return binary_op_t(ast_op_not_equal, xpath_type_boolean, 3);
+
+ case lex_less:
+ return binary_op_t(ast_op_less, xpath_type_boolean, 4);
+
+ case lex_greater:
+ return binary_op_t(ast_op_greater, xpath_type_boolean, 4);
+
+ case lex_less_or_equal:
+ return binary_op_t(ast_op_less_or_equal, xpath_type_boolean, 4);
+
+ case lex_greater_or_equal:
+ return binary_op_t(ast_op_greater_or_equal, xpath_type_boolean, 4);
+
+ case lex_plus:
+ return binary_op_t(ast_op_add, xpath_type_number, 5);
+
+ case lex_minus:
+ return binary_op_t(ast_op_subtract, xpath_type_number, 5);
+
+ case lex_multiply:
+ return binary_op_t(ast_op_multiply, xpath_type_number, 6);
+
+ case lex_union:
+ return binary_op_t(ast_op_union, xpath_type_node_set, 7);
+
+ default:
+ return binary_op_t();
+ }
+ }
+ };
+
+ xpath_ast_node* parse_expression_rec(xpath_ast_node* lhs, int limit)
+ {
+ binary_op_t op = binary_op_t::parse(_lexer);
+
+ while (op.asttype != ast_unknown && op.precedence >= limit)
+ {
+ _lexer.next();
+
+ if (++_depth > xpath_ast_depth_limit)
+ return error_rec();
+
+ xpath_ast_node* rhs = parse_path_or_unary_expression();
+ if (!rhs) return 0;
+
+ binary_op_t nextop = binary_op_t::parse(_lexer);
+
+ while (nextop.asttype != ast_unknown && nextop.precedence > op.precedence)
+ {
+ rhs = parse_expression_rec(rhs, nextop.precedence);
+ if (!rhs) return 0;
+
+ nextop = binary_op_t::parse(_lexer);
+ }
+
+ if (op.asttype == ast_op_union && (lhs->rettype() != xpath_type_node_set || rhs->rettype() != xpath_type_node_set))
+ return error("Union operator has to be applied to node sets");
+
+ lhs = alloc_node(op.asttype, op.rettype, lhs, rhs);
+ if (!lhs) return 0;
+
+ op = binary_op_t::parse(_lexer);
+ }
+
+ return lhs;
+ }
+
+ // Expr ::= OrExpr
+ // OrExpr ::= AndExpr | OrExpr 'or' AndExpr
+ // AndExpr ::= EqualityExpr | AndExpr 'and' EqualityExpr
+ // EqualityExpr ::= RelationalExpr
+ // | EqualityExpr '=' RelationalExpr
+ // | EqualityExpr '!=' RelationalExpr
+ // RelationalExpr ::= AdditiveExpr
+ // | RelationalExpr '<' AdditiveExpr
+ // | RelationalExpr '>' AdditiveExpr
+ // | RelationalExpr '<=' AdditiveExpr
+ // | RelationalExpr '>=' AdditiveExpr
+ // AdditiveExpr ::= MultiplicativeExpr
+ // | AdditiveExpr '+' MultiplicativeExpr
+ // | AdditiveExpr '-' MultiplicativeExpr
+ // MultiplicativeExpr ::= UnaryExpr
+ // | MultiplicativeExpr '*' UnaryExpr
+ // | MultiplicativeExpr 'div' UnaryExpr
+ // | MultiplicativeExpr 'mod' UnaryExpr
+ xpath_ast_node* parse_expression(int limit = 0)
+ {
+ size_t old_depth = _depth;
+
+ if (++_depth > xpath_ast_depth_limit)
+ return error_rec();
+
+ xpath_ast_node* n = parse_path_or_unary_expression();
+ if (!n) return 0;
+
+ n = parse_expression_rec(n, limit);
+
+ _depth = old_depth;
+
+ return n;
+ }
+
+ xpath_parser(const char_t* query, xpath_variable_set* variables, xpath_allocator* alloc, xpath_parse_result* result): _alloc(alloc), _lexer(query), _query(query), _variables(variables), _result(result), _depth(0)
+ {
+ }
+
+ xpath_ast_node* parse()
+ {
+ xpath_ast_node* n = parse_expression();
+ if (!n) return 0;
+
+ assert(_depth == 0);
+
+ // check if there are unparsed tokens left
+ if (_lexer.current() != lex_eof)
+ return error("Incorrect query");
+
+ return n;
+ }
+
+ static xpath_ast_node* parse(const char_t* query, xpath_variable_set* variables, xpath_allocator* alloc, xpath_parse_result* result)
+ {
+ xpath_parser parser(query, variables, alloc, result);
+
+ return parser.parse();
+ }
+ };
+
+ struct xpath_query_impl
+ {
+ static xpath_query_impl* create()
+ {
+ void* memory = xml_memory::allocate(sizeof(xpath_query_impl));
+ if (!memory) return 0;
+
+ return new (memory) xpath_query_impl();
+ }
+
+ static void destroy(xpath_query_impl* impl)
+ {
+ // free all allocated pages
+ impl->alloc.release();
+
+ // free allocator memory (with the first page)
+ xml_memory::deallocate(impl);
+ }
+
+ xpath_query_impl(): root(0), alloc(&block, &oom), oom(false)
+ {
+ block.next = 0;
+ block.capacity = sizeof(block.data);
+ }
+
+ xpath_ast_node* root;
+ xpath_allocator alloc;
+ xpath_memory_block block;
+ bool oom;
+ };
+
+ PUGI__FN impl::xpath_ast_node* evaluate_node_set_prepare(xpath_query_impl* impl)
+ {
+ if (!impl) return 0;
+
+ if (impl->root->rettype() != xpath_type_node_set)
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ return 0;
+ #else
+ xpath_parse_result res;
+ res.error = "Expression does not evaluate to node set";
+
+ throw xpath_exception(res);
+ #endif
+ }
+
+ return impl->root;
+ }
+PUGI__NS_END
+
+namespace pugi
+{
+#ifndef PUGIXML_NO_EXCEPTIONS
+ PUGI__FN xpath_exception::xpath_exception(const xpath_parse_result& result_): _result(result_)
+ {
+ assert(_result.error);
+ }
+
+ PUGI__FN const char* xpath_exception::what() const throw()
+ {
+ return _result.error;
+ }
+
+ PUGI__FN const xpath_parse_result& xpath_exception::result() const
+ {
+ return _result;
+ }
+#endif
+
+ PUGI__FN xpath_node::xpath_node()
+ {
+ }
+
+ PUGI__FN xpath_node::xpath_node(const xml_node& node_): _node(node_)
+ {
+ }
+
+ PUGI__FN xpath_node::xpath_node(const xml_attribute& attribute_, const xml_node& parent_): _node(attribute_ ? parent_ : xml_node()), _attribute(attribute_)
+ {
+ }
+
+ PUGI__FN xml_node xpath_node::node() const
+ {
+ return _attribute ? xml_node() : _node;
+ }
+
+ PUGI__FN xml_attribute xpath_node::attribute() const
+ {
+ return _attribute;
+ }
+
+ PUGI__FN xml_node xpath_node::parent() const
+ {
+ return _attribute ? _node : _node.parent();
+ }
+
+ PUGI__FN static void unspecified_bool_xpath_node(xpath_node***)
+ {
+ }
+
+ PUGI__FN xpath_node::operator xpath_node::unspecified_bool_type() const
+ {
+ return (_node || _attribute) ? unspecified_bool_xpath_node : 0;
+ }
+
+ PUGI__FN bool xpath_node::operator!() const
+ {
+ return !(_node || _attribute);
+ }
+
+ PUGI__FN bool xpath_node::operator==(const xpath_node& n) const
+ {
+ return _node == n._node && _attribute == n._attribute;
+ }
+
+ PUGI__FN bool xpath_node::operator!=(const xpath_node& n) const
+ {
+ return _node != n._node || _attribute != n._attribute;
+ }
+
+#ifdef __BORLANDC__
+ PUGI__FN bool operator&&(const xpath_node& lhs, bool rhs)
+ {
+ return (bool)lhs && rhs;
+ }
+
+ PUGI__FN bool operator||(const xpath_node& lhs, bool rhs)
+ {
+ return (bool)lhs || rhs;
+ }
+#endif
+
+ PUGI__FN void xpath_node_set::_assign(const_iterator begin_, const_iterator end_, type_t type_)
+ {
+ assert(begin_ <= end_);
+
+ size_t size_ = static_cast<size_t>(end_ - begin_);
+
+ // use internal buffer for 0 or 1 elements, heap buffer otherwise
+ xpath_node* storage = (size_ <= 1) ? _storage : static_cast<xpath_node*>(impl::xml_memory::allocate(size_ * sizeof(xpath_node)));
+
+ if (!storage)
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ return;
+ #else
+ throw std::bad_alloc();
+ #endif
+ }
+
+ // deallocate old buffer
+ if (_begin != _storage)
+ impl::xml_memory::deallocate(_begin);
+
+ // size check is necessary because for begin_ = end_ = nullptr, memcpy is UB
+ if (size_)
+ memcpy(storage, begin_, size_ * sizeof(xpath_node));
+
+ _begin = storage;
+ _end = storage + size_;
+ _type = type_;
+ }
+
+#ifdef PUGIXML_HAS_MOVE
+ PUGI__FN void xpath_node_set::_move(xpath_node_set& rhs) PUGIXML_NOEXCEPT
+ {
+ _type = rhs._type;
+ _storage[0] = rhs._storage[0];
+ _begin = (rhs._begin == rhs._storage) ? _storage : rhs._begin;
+ _end = _begin + (rhs._end - rhs._begin);
+
+ rhs._type = type_unsorted;
+ rhs._begin = rhs._storage;
+ rhs._end = rhs._storage;
+ }
+#endif
+
+ PUGI__FN xpath_node_set::xpath_node_set(): _type(type_unsorted), _begin(_storage), _end(_storage)
+ {
+ }
+
+ PUGI__FN xpath_node_set::xpath_node_set(const_iterator begin_, const_iterator end_, type_t type_): _type(type_unsorted), _begin(_storage), _end(_storage)
+ {
+ _assign(begin_, end_, type_);
+ }
+
+ PUGI__FN xpath_node_set::~xpath_node_set()
+ {
+ if (_begin != _storage)
+ impl::xml_memory::deallocate(_begin);
+ }
+
+ PUGI__FN xpath_node_set::xpath_node_set(const xpath_node_set& ns): _type(type_unsorted), _begin(_storage), _end(_storage)
+ {
+ _assign(ns._begin, ns._end, ns._type);
+ }
+
+ PUGI__FN xpath_node_set& xpath_node_set::operator=(const xpath_node_set& ns)
+ {
+ if (this == &ns) return *this;
+
+ _assign(ns._begin, ns._end, ns._type);
+
+ return *this;
+ }
+
+#ifdef PUGIXML_HAS_MOVE
+ PUGI__FN xpath_node_set::xpath_node_set(xpath_node_set&& rhs) PUGIXML_NOEXCEPT: _type(type_unsorted), _begin(_storage), _end(_storage)
+ {
+ _move(rhs);
+ }
+
+ PUGI__FN xpath_node_set& xpath_node_set::operator=(xpath_node_set&& rhs) PUGIXML_NOEXCEPT
+ {
+ if (this == &rhs) return *this;
+
+ if (_begin != _storage)
+ impl::xml_memory::deallocate(_begin);
+
+ _move(rhs);
+
+ return *this;
+ }
+#endif
+
+ PUGI__FN xpath_node_set::type_t xpath_node_set::type() const
+ {
+ return _type;
+ }
+
+ PUGI__FN size_t xpath_node_set::size() const
+ {
+ return _end - _begin;
+ }
+
+ PUGI__FN bool xpath_node_set::empty() const
+ {
+ return _begin == _end;
+ }
+
+ PUGI__FN const xpath_node& xpath_node_set::operator[](size_t index) const
+ {
+ assert(index < size());
+ return _begin[index];
+ }
+
+ PUGI__FN xpath_node_set::const_iterator xpath_node_set::begin() const
+ {
+ return _begin;
+ }
+
+ PUGI__FN xpath_node_set::const_iterator xpath_node_set::end() const
+ {
+ return _end;
+ }
+
+ PUGI__FN void xpath_node_set::sort(bool reverse)
+ {
+ _type = impl::xpath_sort(_begin, _end, _type, reverse);
+ }
+
+ PUGI__FN xpath_node xpath_node_set::first() const
+ {
+ return impl::xpath_first(_begin, _end, _type);
+ }
+
+ PUGI__FN xpath_parse_result::xpath_parse_result(): error("Internal error"), offset(0)
+ {
+ }
+
+ PUGI__FN xpath_parse_result::operator bool() const
+ {
+ return error == 0;
+ }
+
+ PUGI__FN const char* xpath_parse_result::description() const
+ {
+ return error ? error : "No error";
+ }
+
+ PUGI__FN xpath_variable::xpath_variable(xpath_value_type type_): _type(type_), _next(0)
+ {
+ }
+
+ PUGI__FN const char_t* xpath_variable::name() const
+ {
+ switch (_type)
+ {
+ case xpath_type_node_set:
+ return static_cast<const impl::xpath_variable_node_set*>(this)->name;
+
+ case xpath_type_number:
+ return static_cast<const impl::xpath_variable_number*>(this)->name;
+
+ case xpath_type_string:
+ return static_cast<const impl::xpath_variable_string*>(this)->name;
+
+ case xpath_type_boolean:
+ return static_cast<const impl::xpath_variable_boolean*>(this)->name;
+
+ default:
+ assert(false && "Invalid variable type"); // unreachable
+ return 0;
+ }
+ }
+
+ PUGI__FN xpath_value_type xpath_variable::type() const
+ {
+ return _type;
+ }
+
+ PUGI__FN bool xpath_variable::get_boolean() const
+ {
+ return (_type == xpath_type_boolean) ? static_cast<const impl::xpath_variable_boolean*>(this)->value : false;
+ }
+
+ PUGI__FN double xpath_variable::get_number() const
+ {
+ return (_type == xpath_type_number) ? static_cast<const impl::xpath_variable_number*>(this)->value : impl::gen_nan();
+ }
+
+ PUGI__FN const char_t* xpath_variable::get_string() const
+ {
+ const char_t* value = (_type == xpath_type_string) ? static_cast<const impl::xpath_variable_string*>(this)->value : 0;
+ return value ? value : PUGIXML_TEXT("");
+ }
+
+ PUGI__FN const xpath_node_set& xpath_variable::get_node_set() const
+ {
+ return (_type == xpath_type_node_set) ? static_cast<const impl::xpath_variable_node_set*>(this)->value : impl::dummy_node_set;
+ }
+
+ PUGI__FN bool xpath_variable::set(bool value)
+ {
+ if (_type != xpath_type_boolean) return false;
+
+ static_cast<impl::xpath_variable_boolean*>(this)->value = value;
+ return true;
+ }
+
+ PUGI__FN bool xpath_variable::set(double value)
+ {
+ if (_type != xpath_type_number) return false;
+
+ static_cast<impl::xpath_variable_number*>(this)->value = value;
+ return true;
+ }
+
+ PUGI__FN bool xpath_variable::set(const char_t* value)
+ {
+ if (_type != xpath_type_string) return false;
+
+ impl::xpath_variable_string* var = static_cast<impl::xpath_variable_string*>(this);
+
+ // duplicate string
+ size_t size = (impl::strlength(value) + 1) * sizeof(char_t);
+
+ char_t* copy = static_cast<char_t*>(impl::xml_memory::allocate(size));
+ if (!copy) return false;
+
+ memcpy(copy, value, size);
+
+ // replace old string
+ if (var->value) impl::xml_memory::deallocate(var->value);
+ var->value = copy;
+
+ return true;
+ }
+
+ PUGI__FN bool xpath_variable::set(const xpath_node_set& value)
+ {
+ if (_type != xpath_type_node_set) return false;
+
+ static_cast<impl::xpath_variable_node_set*>(this)->value = value;
+ return true;
+ }
+
+ PUGI__FN xpath_variable_set::xpath_variable_set()
+ {
+ for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i)
+ _data[i] = 0;
+ }
+
+ PUGI__FN xpath_variable_set::~xpath_variable_set()
+ {
+ for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i)
+ _destroy(_data[i]);
+ }
+
+ PUGI__FN xpath_variable_set::xpath_variable_set(const xpath_variable_set& rhs)
+ {
+ for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i)
+ _data[i] = 0;
+
+ _assign(rhs);
+ }
+
+ PUGI__FN xpath_variable_set& xpath_variable_set::operator=(const xpath_variable_set& rhs)
+ {
+ if (this == &rhs) return *this;
+
+ _assign(rhs);
+
+ return *this;
+ }
+
+#ifdef PUGIXML_HAS_MOVE
+ PUGI__FN xpath_variable_set::xpath_variable_set(xpath_variable_set&& rhs) PUGIXML_NOEXCEPT
+ {
+ for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i)
+ {
+ _data[i] = rhs._data[i];
+ rhs._data[i] = 0;
+ }
+ }
+
+ PUGI__FN xpath_variable_set& xpath_variable_set::operator=(xpath_variable_set&& rhs) PUGIXML_NOEXCEPT
+ {
+ for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i)
+ {
+ _destroy(_data[i]);
+
+ _data[i] = rhs._data[i];
+ rhs._data[i] = 0;
+ }
+
+ return *this;
+ }
+#endif
+
+ PUGI__FN void xpath_variable_set::_assign(const xpath_variable_set& rhs)
+ {
+ xpath_variable_set temp;
+
+ for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i)
+ if (rhs._data[i] && !_clone(rhs._data[i], &temp._data[i]))
+ return;
+
+ _swap(temp);
+ }
+
+ PUGI__FN void xpath_variable_set::_swap(xpath_variable_set& rhs)
+ {
+ for (size_t i = 0; i < sizeof(_data) / sizeof(_data[0]); ++i)
+ {
+ xpath_variable* chain = _data[i];
+
+ _data[i] = rhs._data[i];
+ rhs._data[i] = chain;
+ }
+ }
+
+ PUGI__FN xpath_variable* xpath_variable_set::_find(const char_t* name) const
+ {
+ const size_t hash_size = sizeof(_data) / sizeof(_data[0]);
+ size_t hash = impl::hash_string(name) % hash_size;
+
+ // look for existing variable
+ for (xpath_variable* var = _data[hash]; var; var = var->_next)
+ if (impl::strequal(var->name(), name))
+ return var;
+
+ return 0;
+ }
+
+ PUGI__FN bool xpath_variable_set::_clone(xpath_variable* var, xpath_variable** out_result)
+ {
+ xpath_variable* last = 0;
+
+ while (var)
+ {
+ // allocate storage for new variable
+ xpath_variable* nvar = impl::new_xpath_variable(var->_type, var->name());
+ if (!nvar) return false;
+
+ // link the variable to the result immediately to handle failures gracefully
+ if (last)
+ last->_next = nvar;
+ else
+ *out_result = nvar;
+
+ last = nvar;
+
+ // copy the value; this can fail due to out-of-memory conditions
+ if (!impl::copy_xpath_variable(nvar, var)) return false;
+
+ var = var->_next;
+ }
+
+ return true;
+ }
+
+ PUGI__FN void xpath_variable_set::_destroy(xpath_variable* var)
+ {
+ while (var)
+ {
+ xpath_variable* next = var->_next;
+
+ impl::delete_xpath_variable(var->_type, var);
+
+ var = next;
+ }
+ }
+
+ PUGI__FN xpath_variable* xpath_variable_set::add(const char_t* name, xpath_value_type type)
+ {
+ const size_t hash_size = sizeof(_data) / sizeof(_data[0]);
+ size_t hash = impl::hash_string(name) % hash_size;
+
+ // look for existing variable
+ for (xpath_variable* var = _data[hash]; var; var = var->_next)
+ if (impl::strequal(var->name(), name))
+ return var->type() == type ? var : 0;
+
+ // add new variable
+ xpath_variable* result = impl::new_xpath_variable(type, name);
+
+ if (result)
+ {
+ result->_next = _data[hash];
+
+ _data[hash] = result;
+ }
+
+ return result;
+ }
+
+ PUGI__FN bool xpath_variable_set::set(const char_t* name, bool value)
+ {
+ xpath_variable* var = add(name, xpath_type_boolean);
+ return var ? var->set(value) : false;
+ }
+
+ PUGI__FN bool xpath_variable_set::set(const char_t* name, double value)
+ {
+ xpath_variable* var = add(name, xpath_type_number);
+ return var ? var->set(value) : false;
+ }
+
+ PUGI__FN bool xpath_variable_set::set(const char_t* name, const char_t* value)
+ {
+ xpath_variable* var = add(name, xpath_type_string);
+ return var ? var->set(value) : false;
+ }
+
+ PUGI__FN bool xpath_variable_set::set(const char_t* name, const xpath_node_set& value)
+ {
+ xpath_variable* var = add(name, xpath_type_node_set);
+ return var ? var->set(value) : false;
+ }
+
+ PUGI__FN xpath_variable* xpath_variable_set::get(const char_t* name)
+ {
+ return _find(name);
+ }
+
+ PUGI__FN const xpath_variable* xpath_variable_set::get(const char_t* name) const
+ {
+ return _find(name);
+ }
+
+ PUGI__FN xpath_query::xpath_query(const char_t* query, xpath_variable_set* variables): _impl(0)
+ {
+ impl::xpath_query_impl* qimpl = impl::xpath_query_impl::create();
+
+ if (!qimpl)
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ _result.error = "Out of memory";
+ #else
+ throw std::bad_alloc();
+ #endif
+ }
+ else
+ {
+ using impl::auto_deleter; // MSVC7 workaround
+ auto_deleter<impl::xpath_query_impl> impl(qimpl, impl::xpath_query_impl::destroy);
+
+ qimpl->root = impl::xpath_parser::parse(query, variables, &qimpl->alloc, &_result);
+
+ if (qimpl->root)
+ {
+ qimpl->root->optimize(&qimpl->alloc);
+
+ _impl = impl.release();
+ _result.error = 0;
+ }
+ else
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ if (qimpl->oom) _result.error = "Out of memory";
+ #else
+ if (qimpl->oom) throw std::bad_alloc();
+ throw xpath_exception(_result);
+ #endif
+ }
+ }
+ }
+
+ PUGI__FN xpath_query::xpath_query(): _impl(0)
+ {
+ }
+
+ PUGI__FN xpath_query::~xpath_query()
+ {
+ if (_impl)
+ impl::xpath_query_impl::destroy(static_cast<impl::xpath_query_impl*>(_impl));
+ }
+
+#ifdef PUGIXML_HAS_MOVE
+ PUGI__FN xpath_query::xpath_query(xpath_query&& rhs) PUGIXML_NOEXCEPT
+ {
+ _impl = rhs._impl;
+ _result = rhs._result;
+ rhs._impl = 0;
+ rhs._result = xpath_parse_result();
+ }
+
+ PUGI__FN xpath_query& xpath_query::operator=(xpath_query&& rhs) PUGIXML_NOEXCEPT
+ {
+ if (this == &rhs) return *this;
+
+ if (_impl)
+ impl::xpath_query_impl::destroy(static_cast<impl::xpath_query_impl*>(_impl));
+
+ _impl = rhs._impl;
+ _result = rhs._result;
+ rhs._impl = 0;
+ rhs._result = xpath_parse_result();
+
+ return *this;
+ }
+#endif
+
+ PUGI__FN xpath_value_type xpath_query::return_type() const
+ {
+ if (!_impl) return xpath_type_none;
+
+ return static_cast<impl::xpath_query_impl*>(_impl)->root->rettype();
+ }
+
+ PUGI__FN bool xpath_query::evaluate_boolean(const xpath_node& n) const
+ {
+ if (!_impl) return false;
+
+ impl::xpath_context c(n, 1, 1);
+ impl::xpath_stack_data sd;
+
+ bool r = static_cast<impl::xpath_query_impl*>(_impl)->root->eval_boolean(c, sd.stack);
+
+ if (sd.oom)
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ return false;
+ #else
+ throw std::bad_alloc();
+ #endif
+ }
+
+ return r;
+ }
+
+ PUGI__FN double xpath_query::evaluate_number(const xpath_node& n) const
+ {
+ if (!_impl) return impl::gen_nan();
+
+ impl::xpath_context c(n, 1, 1);
+ impl::xpath_stack_data sd;
+
+ double r = static_cast<impl::xpath_query_impl*>(_impl)->root->eval_number(c, sd.stack);
+
+ if (sd.oom)
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ return impl::gen_nan();
+ #else
+ throw std::bad_alloc();
+ #endif
+ }
+
+ return r;
+ }
+
+#ifndef PUGIXML_NO_STL
+ PUGI__FN string_t xpath_query::evaluate_string(const xpath_node& n) const
+ {
+ if (!_impl) return string_t();
+
+ impl::xpath_context c(n, 1, 1);
+ impl::xpath_stack_data sd;
+
+ impl::xpath_string r = static_cast<impl::xpath_query_impl*>(_impl)->root->eval_string(c, sd.stack);
+
+ if (sd.oom)
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ return string_t();
+ #else
+ throw std::bad_alloc();
+ #endif
+ }
+
+ return string_t(r.c_str(), r.length());
+ }
+#endif
+
+ PUGI__FN size_t xpath_query::evaluate_string(char_t* buffer, size_t capacity, const xpath_node& n) const
+ {
+ impl::xpath_context c(n, 1, 1);
+ impl::xpath_stack_data sd;
+
+ impl::xpath_string r = _impl ? static_cast<impl::xpath_query_impl*>(_impl)->root->eval_string(c, sd.stack) : impl::xpath_string();
+
+ if (sd.oom)
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ r = impl::xpath_string();
+ #else
+ throw std::bad_alloc();
+ #endif
+ }
+
+ size_t full_size = r.length() + 1;
+
+ if (capacity > 0)
+ {
+ size_t size = (full_size < capacity) ? full_size : capacity;
+ assert(size > 0);
+
+ memcpy(buffer, r.c_str(), (size - 1) * sizeof(char_t));
+ buffer[size - 1] = 0;
+ }
+
+ return full_size;
+ }
+
+ PUGI__FN xpath_node_set xpath_query::evaluate_node_set(const xpath_node& n) const
+ {
+ impl::xpath_ast_node* root = impl::evaluate_node_set_prepare(static_cast<impl::xpath_query_impl*>(_impl));
+ if (!root) return xpath_node_set();
+
+ impl::xpath_context c(n, 1, 1);
+ impl::xpath_stack_data sd;
+
+ impl::xpath_node_set_raw r = root->eval_node_set(c, sd.stack, impl::nodeset_eval_all);
+
+ if (sd.oom)
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ return xpath_node_set();
+ #else
+ throw std::bad_alloc();
+ #endif
+ }
+
+ return xpath_node_set(r.begin(), r.end(), r.type());
+ }
+
+ PUGI__FN xpath_node xpath_query::evaluate_node(const xpath_node& n) const
+ {
+ impl::xpath_ast_node* root = impl::evaluate_node_set_prepare(static_cast<impl::xpath_query_impl*>(_impl));
+ if (!root) return xpath_node();
+
+ impl::xpath_context c(n, 1, 1);
+ impl::xpath_stack_data sd;
+
+ impl::xpath_node_set_raw r = root->eval_node_set(c, sd.stack, impl::nodeset_eval_first);
+
+ if (sd.oom)
+ {
+ #ifdef PUGIXML_NO_EXCEPTIONS
+ return xpath_node();
+ #else
+ throw std::bad_alloc();
+ #endif
+ }
+
+ return r.first();
+ }
+
+ PUGI__FN const xpath_parse_result& xpath_query::result() const
+ {
+ return _result;
+ }
+
+ PUGI__FN static void unspecified_bool_xpath_query(xpath_query***)
+ {
+ }
+
+ PUGI__FN xpath_query::operator xpath_query::unspecified_bool_type() const
+ {
+ return _impl ? unspecified_bool_xpath_query : 0;
+ }
+
+ PUGI__FN bool xpath_query::operator!() const
+ {
+ return !_impl;
+ }
+
+ PUGI__FN xpath_node xml_node::select_node(const char_t* query, xpath_variable_set* variables) const
+ {
+ xpath_query q(query, variables);
+ return q.evaluate_node(*this);
+ }
+
+ PUGI__FN xpath_node xml_node::select_node(const xpath_query& query) const
+ {
+ return query.evaluate_node(*this);
+ }
+
+ PUGI__FN xpath_node_set xml_node::select_nodes(const char_t* query, xpath_variable_set* variables) const
+ {
+ xpath_query q(query, variables);
+ return q.evaluate_node_set(*this);
+ }
+
+ PUGI__FN xpath_node_set xml_node::select_nodes(const xpath_query& query) const
+ {
+ return query.evaluate_node_set(*this);
+ }
+
+ PUGI__FN xpath_node xml_node::select_single_node(const char_t* query, xpath_variable_set* variables) const
+ {
+ xpath_query q(query, variables);
+ return q.evaluate_node(*this);
+ }
+
+ PUGI__FN xpath_node xml_node::select_single_node(const xpath_query& query) const
+ {
+ return query.evaluate_node(*this);
+ }
+}
+
+#endif
+
+#ifdef __BORLANDC__
+# pragma option pop
+#endif
+
+// Intel C++ does not properly keep warning state for function templates,
+// so popping warning state at the end of translation unit leads to warnings in the middle.
+#if defined(_MSC_VER) && !defined(__INTEL_COMPILER)
+# pragma warning(pop)
+#endif
+
+#if defined(_MSC_VER) && defined(__c2__)
+# pragma clang diagnostic pop
+#endif
+
+// Undefine all local macros (makes sure we're not leaking macros in header-only mode)
+#undef PUGI__NO_INLINE
+#undef PUGI__UNLIKELY
+#undef PUGI__STATIC_ASSERT
+#undef PUGI__DMC_VOLATILE
+#undef PUGI__UNSIGNED_OVERFLOW
+#undef PUGI__MSVC_CRT_VERSION
+#undef PUGI__SNPRINTF
+#undef PUGI__NS_BEGIN
+#undef PUGI__NS_END
+#undef PUGI__FN
+#undef PUGI__FN_NO_INLINE
+#undef PUGI__GETHEADER_IMPL
+#undef PUGI__GETPAGE_IMPL
+#undef PUGI__GETPAGE
+#undef PUGI__NODETYPE
+#undef PUGI__IS_CHARTYPE_IMPL
+#undef PUGI__IS_CHARTYPE
+#undef PUGI__IS_CHARTYPEX
+#undef PUGI__ENDSWITH
+#undef PUGI__SKIPWS
+#undef PUGI__OPTSET
+#undef PUGI__PUSHNODE
+#undef PUGI__POPNODE
+#undef PUGI__SCANFOR
+#undef PUGI__SCANWHILE
+#undef PUGI__SCANWHILE_UNROLL
+#undef PUGI__ENDSEG
+#undef PUGI__THROW_ERROR
+#undef PUGI__CHECK_ERROR
+
+#endif
+
+/**
+ * Copyright (c) 2006-2022 Arseny Kapoulkine
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
diff --git a/src/pugixml/pugixml.hpp b/src/pugixml/pugixml.hpp
new file mode 100644
index 0000000..579f143
--- /dev/null
+++ b/src/pugixml/pugixml.hpp
@@ -0,0 +1,1501 @@
+/**
+ * pugixml parser - version 1.12
+ * --------------------------------------------------------
+ * Copyright (C) 2006-2022, by Arseny Kapoulkine (arseny.kapoulkine@gmail.com)
+ * Report bugs and download new versions at https://pugixml.org/
+ *
+ * This library is distributed under the MIT License. See notice at the end
+ * of this file.
+ *
+ * This work is based on the pugxml parser, which is:
+ * Copyright (C) 2003, by Kristen Wegner (kristen@tima.net)
+ */
+
+// Define version macro; evaluates to major * 1000 + minor * 10 + patch so that it's safe to use in less-than comparisons
+// Note: pugixml used major * 100 + minor * 10 + patch format up until 1.9 (which had version identifier 190); starting from pugixml 1.10, the minor version number is two digits
+#ifndef PUGIXML_VERSION
+# define PUGIXML_VERSION 1120 // 1.12
+#endif
+
+// Include user configuration file (this can define various configuration macros)
+#include "pugiconfig.hpp"
+
+#ifndef HEADER_PUGIXML_HPP
+#define HEADER_PUGIXML_HPP
+
+// Include stddef.h for size_t and ptrdiff_t
+#include <stddef.h>
+
+// Include exception header for XPath
+#if !defined(PUGIXML_NO_XPATH) && !defined(PUGIXML_NO_EXCEPTIONS)
+# include <exception>
+#endif
+
+// Include STL headers
+#ifndef PUGIXML_NO_STL
+# include <iterator>
+# include <iosfwd>
+# include <string>
+#endif
+
+// Macro for deprecated features
+#ifndef PUGIXML_DEPRECATED
+# if defined(__GNUC__)
+# define PUGIXML_DEPRECATED __attribute__((deprecated))
+# elif defined(_MSC_VER) && _MSC_VER >= 1300
+# define PUGIXML_DEPRECATED __declspec(deprecated)
+# else
+# define PUGIXML_DEPRECATED
+# endif
+#endif
+
+// If no API is defined, assume default
+#ifndef PUGIXML_API
+# define PUGIXML_API
+#endif
+
+// If no API for classes is defined, assume default
+#ifndef PUGIXML_CLASS
+# define PUGIXML_CLASS PUGIXML_API
+#endif
+
+// If no API for functions is defined, assume default
+#ifndef PUGIXML_FUNCTION
+# define PUGIXML_FUNCTION PUGIXML_API
+#endif
+
+// If the platform is known to have long long support, enable long long functions
+#ifndef PUGIXML_HAS_LONG_LONG
+# if __cplusplus >= 201103
+# define PUGIXML_HAS_LONG_LONG
+# elif defined(_MSC_VER) && _MSC_VER >= 1400
+# define PUGIXML_HAS_LONG_LONG
+# endif
+#endif
+
+// If the platform is known to have move semantics support, compile move ctor/operator implementation
+#ifndef PUGIXML_HAS_MOVE
+# if __cplusplus >= 201103
+# define PUGIXML_HAS_MOVE
+# elif defined(_MSC_VER) && _MSC_VER >= 1600
+# define PUGIXML_HAS_MOVE
+# endif
+#endif
+
+// If C++ is 2011 or higher, add 'noexcept' specifiers
+#ifndef PUGIXML_NOEXCEPT
+# if __cplusplus >= 201103
+# define PUGIXML_NOEXCEPT noexcept
+# elif defined(_MSC_VER) && _MSC_VER >= 1900
+# define PUGIXML_NOEXCEPT noexcept
+# else
+# define PUGIXML_NOEXCEPT
+# endif
+#endif
+
+// Some functions can not be noexcept in compact mode
+#ifdef PUGIXML_COMPACT
+# define PUGIXML_NOEXCEPT_IF_NOT_COMPACT
+#else
+# define PUGIXML_NOEXCEPT_IF_NOT_COMPACT PUGIXML_NOEXCEPT
+#endif
+
+// If C++ is 2011 or higher, add 'override' qualifiers
+#ifndef PUGIXML_OVERRIDE
+# if __cplusplus >= 201103
+# define PUGIXML_OVERRIDE override
+# elif defined(_MSC_VER) && _MSC_VER >= 1700
+# define PUGIXML_OVERRIDE override
+# else
+# define PUGIXML_OVERRIDE
+# endif
+#endif
+
+// If C++ is 2011 or higher, use 'nullptr'
+#ifndef PUGIXML_NULL
+# if __cplusplus >= 201103
+# define PUGIXML_NULL nullptr
+# else
+# define PUGIXML_NULL 0
+# endif
+#endif
+
+// Character interface macros
+#ifdef PUGIXML_WCHAR_MODE
+# define PUGIXML_TEXT(t) L ## t
+# define PUGIXML_CHAR wchar_t
+#else
+# define PUGIXML_TEXT(t) t
+# define PUGIXML_CHAR char
+#endif
+
+namespace pugi
+{
+ // Character type used for all internal storage and operations; depends on PUGIXML_WCHAR_MODE
+ typedef PUGIXML_CHAR char_t;
+
+#ifndef PUGIXML_NO_STL
+ // String type used for operations that work with STL string; depends on PUGIXML_WCHAR_MODE
+ typedef std::basic_string<PUGIXML_CHAR, std::char_traits<PUGIXML_CHAR>, std::allocator<PUGIXML_CHAR> > string_t;
+#endif
+}
+
+// The PugiXML namespace
+namespace pugi
+{
+ // Tree node types
+ enum xml_node_type
+ {
+ node_null, // Empty (null) node handle
+ node_document, // A document tree's absolute root
+ node_element, // Element tag, i.e. '<node/>'
+ node_pcdata, // Plain character data, i.e. 'text'
+ node_cdata, // Character data, i.e. '<![CDATA[text]]>'
+ node_comment, // Comment tag, i.e. '<!-- text -->'
+ node_pi, // Processing instruction, i.e. '<?name?>'
+ node_declaration, // Document declaration, i.e. '<?xml version="1.0"?>'
+ node_doctype // Document type declaration, i.e. '<!DOCTYPE doc>'
+ };
+
+ // Parsing options
+
+ // Minimal parsing mode (equivalent to turning all other flags off).
+ // Only elements and PCDATA sections are added to the DOM tree, no text conversions are performed.
+ const unsigned int parse_minimal = 0x0000;
+
+ // This flag determines if processing instructions (node_pi) are added to the DOM tree. This flag is off by default.
+ const unsigned int parse_pi = 0x0001;
+
+ // This flag determines if comments (node_comment) are added to the DOM tree. This flag is off by default.
+ const unsigned int parse_comments = 0x0002;
+
+ // This flag determines if CDATA sections (node_cdata) are added to the DOM tree. This flag is on by default.
+ const unsigned int parse_cdata = 0x0004;
+
+ // This flag determines if plain character data (node_pcdata) that consist only of whitespace are added to the DOM tree.
+ // This flag is off by default; turning it on usually results in slower parsing and more memory consumption.
+ const unsigned int parse_ws_pcdata = 0x0008;
+
+ // This flag determines if character and entity references are expanded during parsing. This flag is on by default.
+ const unsigned int parse_escapes = 0x0010;
+
+ // This flag determines if EOL characters are normalized (converted to #xA) during parsing. This flag is on by default.
+ const unsigned int parse_eol = 0x0020;
+
+ // This flag determines if attribute values are normalized using CDATA normalization rules during parsing. This flag is on by default.
+ const unsigned int parse_wconv_attribute = 0x0040;
+
+ // This flag determines if attribute values are normalized using NMTOKENS normalization rules during parsing. This flag is off by default.
+ const unsigned int parse_wnorm_attribute = 0x0080;
+
+ // This flag determines if document declaration (node_declaration) is added to the DOM tree. This flag is off by default.
+ const unsigned int parse_declaration = 0x0100;
+
+ // This flag determines if document type declaration (node_doctype) is added to the DOM tree. This flag is off by default.
+ const unsigned int parse_doctype = 0x0200;
+
+ // This flag determines if plain character data (node_pcdata) that is the only child of the parent node and that consists only
+ // of whitespace is added to the DOM tree.
+ // This flag is off by default; turning it on may result in slower parsing and more memory consumption.
+ const unsigned int parse_ws_pcdata_single = 0x0400;
+
+ // This flag determines if leading and trailing whitespace is to be removed from plain character data. This flag is off by default.
+ const unsigned int parse_trim_pcdata = 0x0800;
+
+ // This flag determines if plain character data that does not have a parent node is added to the DOM tree, and if an empty document
+ // is a valid document. This flag is off by default.
+ const unsigned int parse_fragment = 0x1000;
+
+ // This flag determines if plain character data is be stored in the parent element's value. This significantly changes the structure of
+ // the document; this flag is only recommended for parsing documents with many PCDATA nodes in memory-constrained environments.
+ // This flag is off by default.
+ const unsigned int parse_embed_pcdata = 0x2000;
+
+ // The default parsing mode.
+ // Elements, PCDATA and CDATA sections are added to the DOM tree, character/reference entities are expanded,
+ // End-of-Line characters are normalized, attribute values are normalized using CDATA normalization rules.
+ const unsigned int parse_default = parse_cdata | parse_escapes | parse_wconv_attribute | parse_eol;
+
+ // The full parsing mode.
+ // Nodes of all types are added to the DOM tree, character/reference entities are expanded,
+ // End-of-Line characters are normalized, attribute values are normalized using CDATA normalization rules.
+ const unsigned int parse_full = parse_default | parse_pi | parse_comments | parse_declaration | parse_doctype;
+
+ // These flags determine the encoding of input data for XML document
+ enum xml_encoding
+ {
+ encoding_auto, // Auto-detect input encoding using BOM or < / <? detection; use UTF8 if BOM is not found
+ encoding_utf8, // UTF8 encoding
+ encoding_utf16_le, // Little-endian UTF16
+ encoding_utf16_be, // Big-endian UTF16
+ encoding_utf16, // UTF16 with native endianness
+ encoding_utf32_le, // Little-endian UTF32
+ encoding_utf32_be, // Big-endian UTF32
+ encoding_utf32, // UTF32 with native endianness
+ encoding_wchar, // The same encoding wchar_t has (either UTF16 or UTF32)
+ encoding_latin1
+ };
+
+ // Formatting flags
+
+ // Indent the nodes that are written to output stream with as many indentation strings as deep the node is in DOM tree. This flag is on by default.
+ const unsigned int format_indent = 0x01;
+
+ // Write encoding-specific BOM to the output stream. This flag is off by default.
+ const unsigned int format_write_bom = 0x02;
+
+ // Use raw output mode (no indentation and no line breaks are written). This flag is off by default.
+ const unsigned int format_raw = 0x04;
+
+ // Omit default XML declaration even if there is no declaration in the document. This flag is off by default.
+ const unsigned int format_no_declaration = 0x08;
+
+ // Don't escape attribute values and PCDATA contents. This flag is off by default.
+ const unsigned int format_no_escapes = 0x10;
+
+ // Open file using text mode in xml_document::save_file. This enables special character (i.e. new-line) conversions on some systems. This flag is off by default.
+ const unsigned int format_save_file_text = 0x20;
+
+ // Write every attribute on a new line with appropriate indentation. This flag is off by default.
+ const unsigned int format_indent_attributes = 0x40;
+
+ // Don't output empty element tags, instead writing an explicit start and end tag even if there are no children. This flag is off by default.
+ const unsigned int format_no_empty_element_tags = 0x80;
+
+ // Skip characters belonging to range [0; 32) instead of "&#xNN;" encoding. This flag is off by default.
+ const unsigned int format_skip_control_chars = 0x100;
+
+ // Use single quotes ' instead of double quotes " for enclosing attribute values. This flag is off by default.
+ const unsigned int format_attribute_single_quote = 0x200;
+
+ // The default set of formatting flags.
+ // Nodes are indented depending on their depth in DOM tree, a default declaration is output if document has none.
+ const unsigned int format_default = format_indent;
+
+ const int default_double_precision = 17;
+ const int default_float_precision = 9;
+
+ // Forward declarations
+ struct xml_attribute_struct;
+ struct xml_node_struct;
+
+ class xml_node_iterator;
+ class xml_attribute_iterator;
+ class xml_named_node_iterator;
+
+ class xml_tree_walker;
+
+ struct xml_parse_result;
+
+ class xml_node;
+
+ class xml_text;
+
+ #ifndef PUGIXML_NO_XPATH
+ class xpath_node;
+ class xpath_node_set;
+ class xpath_query;
+ class xpath_variable_set;
+ #endif
+
+ // Range-based for loop support
+ template <typename It> class xml_object_range
+ {
+ public:
+ typedef It const_iterator;
+ typedef It iterator;
+
+ xml_object_range(It b, It e): _begin(b), _end(e)
+ {
+ }
+
+ It begin() const { return _begin; }
+ It end() const { return _end; }
+
+ bool empty() const { return _begin == _end; }
+
+ private:
+ It _begin, _end;
+ };
+
+ // Writer interface for node printing (see xml_node::print)
+ class PUGIXML_CLASS xml_writer
+ {
+ public:
+ virtual ~xml_writer() {}
+
+ // Write memory chunk into stream/file/whatever
+ virtual void write(const void* data, size_t size) = 0;
+ };
+
+ // xml_writer implementation for FILE*
+ class PUGIXML_CLASS xml_writer_file: public xml_writer
+ {
+ public:
+ // Construct writer from a FILE* object; void* is used to avoid header dependencies on stdio
+ xml_writer_file(void* file);
+
+ virtual void write(const void* data, size_t size) PUGIXML_OVERRIDE;
+
+ private:
+ void* file;
+ };
+
+ #ifndef PUGIXML_NO_STL
+ // xml_writer implementation for streams
+ class PUGIXML_CLASS xml_writer_stream: public xml_writer
+ {
+ public:
+ // Construct writer from an output stream object
+ xml_writer_stream(std::basic_ostream<char, std::char_traits<char> >& stream);
+ xml_writer_stream(std::basic_ostream<wchar_t, std::char_traits<wchar_t> >& stream);
+
+ virtual void write(const void* data, size_t size) PUGIXML_OVERRIDE;
+
+ private:
+ std::basic_ostream<char, std::char_traits<char> >* narrow_stream;
+ std::basic_ostream<wchar_t, std::char_traits<wchar_t> >* wide_stream;
+ };
+ #endif
+
+ // A light-weight handle for manipulating attributes in DOM tree
+ class PUGIXML_CLASS xml_attribute
+ {
+ friend class xml_attribute_iterator;
+ friend class xml_node;
+
+ private:
+ xml_attribute_struct* _attr;
+
+ typedef void (*unspecified_bool_type)(xml_attribute***);
+
+ public:
+ // Default constructor. Constructs an empty attribute.
+ xml_attribute();
+
+ // Constructs attribute from internal pointer
+ explicit xml_attribute(xml_attribute_struct* attr);
+
+ // Safe bool conversion operator
+ operator unspecified_bool_type() const;
+
+ // Borland C++ workaround
+ bool operator!() const;
+
+ // Comparison operators (compares wrapped attribute pointers)
+ bool operator==(const xml_attribute& r) const;
+ bool operator!=(const xml_attribute& r) const;
+ bool operator<(const xml_attribute& r) const;
+ bool operator>(const xml_attribute& r) const;
+ bool operator<=(const xml_attribute& r) const;
+ bool operator>=(const xml_attribute& r) const;
+
+ // Check if attribute is empty
+ bool empty() const;
+
+ // Get attribute name/value, or "" if attribute is empty
+ const char_t* name() const;
+ const char_t* value() const;
+
+ // Get attribute value, or the default value if attribute is empty
+ const char_t* as_string(const char_t* def = PUGIXML_TEXT("")) const;
+
+ // Get attribute value as a number, or the default value if conversion did not succeed or attribute is empty
+ int as_int(int def = 0) const;
+ unsigned int as_uint(unsigned int def = 0) const;
+ double as_double(double def = 0) const;
+ float as_float(float def = 0) const;
+
+ #ifdef PUGIXML_HAS_LONG_LONG
+ long long as_llong(long long def = 0) const;
+ unsigned long long as_ullong(unsigned long long def = 0) const;
+ #endif
+
+ // Get attribute value as bool (returns true if first character is in '1tTyY' set), or the default value if attribute is empty
+ bool as_bool(bool def = false) const;
+
+ // Set attribute name/value (returns false if attribute is empty or there is not enough memory)
+ bool set_name(const char_t* rhs);
+ bool set_value(const char_t* rhs);
+
+ // Set attribute value with type conversion (numbers are converted to strings, boolean is converted to "true"/"false")
+ bool set_value(int rhs);
+ bool set_value(unsigned int rhs);
+ bool set_value(long rhs);
+ bool set_value(unsigned long rhs);
+ bool set_value(double rhs);
+ bool set_value(double rhs, int precision);
+ bool set_value(float rhs);
+ bool set_value(float rhs, int precision);
+ bool set_value(bool rhs);
+
+ #ifdef PUGIXML_HAS_LONG_LONG
+ bool set_value(long long rhs);
+ bool set_value(unsigned long long rhs);
+ #endif
+
+ // Set attribute value (equivalent to set_value without error checking)
+ xml_attribute& operator=(const char_t* rhs);
+ xml_attribute& operator=(int rhs);
+ xml_attribute& operator=(unsigned int rhs);
+ xml_attribute& operator=(long rhs);
+ xml_attribute& operator=(unsigned long rhs);
+ xml_attribute& operator=(double rhs);
+ xml_attribute& operator=(float rhs);
+ xml_attribute& operator=(bool rhs);
+
+ #ifdef PUGIXML_HAS_LONG_LONG
+ xml_attribute& operator=(long long rhs);
+ xml_attribute& operator=(unsigned long long rhs);
+ #endif
+
+ // Get next/previous attribute in the attribute list of the parent node
+ xml_attribute next_attribute() const;
+ xml_attribute previous_attribute() const;
+
+ // Get hash value (unique for handles to the same object)
+ size_t hash_value() const;
+
+ // Get internal pointer
+ xml_attribute_struct* internal_object() const;
+ };
+
+#ifdef __BORLANDC__
+ // Borland C++ workaround
+ bool PUGIXML_FUNCTION operator&&(const xml_attribute& lhs, bool rhs);
+ bool PUGIXML_FUNCTION operator||(const xml_attribute& lhs, bool rhs);
+#endif
+
+ // A light-weight handle for manipulating nodes in DOM tree
+ class PUGIXML_CLASS xml_node
+ {
+ friend class xml_attribute_iterator;
+ friend class xml_node_iterator;
+ friend class xml_named_node_iterator;
+
+ protected:
+ xml_node_struct* _root;
+
+ typedef void (*unspecified_bool_type)(xml_node***);
+
+ public:
+ // Default constructor. Constructs an empty node.
+ xml_node();
+
+ // Constructs node from internal pointer
+ explicit xml_node(xml_node_struct* p);
+
+ // Safe bool conversion operator
+ operator unspecified_bool_type() const;
+
+ // Borland C++ workaround
+ bool operator!() const;
+
+ // Comparison operators (compares wrapped node pointers)
+ bool operator==(const xml_node& r) const;
+ bool operator!=(const xml_node& r) const;
+ bool operator<(const xml_node& r) const;
+ bool operator>(const xml_node& r) const;
+ bool operator<=(const xml_node& r) const;
+ bool operator>=(const xml_node& r) const;
+
+ // Check if node is empty.
+ bool empty() const;
+
+ // Get node type
+ xml_node_type type() const;
+
+ // Get node name, or "" if node is empty or it has no name
+ const char_t* name() const;
+
+ // Get node value, or "" if node is empty or it has no value
+ // Note: For <node>text</node> node.value() does not return "text"! Use child_value() or text() methods to access text inside nodes.
+ const char_t* value() const;
+
+ // Get attribute list
+ xml_attribute first_attribute() const;
+ xml_attribute last_attribute() const;
+
+ // Get children list
+ xml_node first_child() const;
+ xml_node last_child() const;
+
+ // Get next/previous sibling in the children list of the parent node
+ xml_node next_sibling() const;
+ xml_node previous_sibling() const;
+
+ // Get parent node
+ xml_node parent() const;
+
+ // Get root of DOM tree this node belongs to
+ xml_node root() const;
+
+ // Get text object for the current node
+ xml_text text() const;
+
+ // Get child, attribute or next/previous sibling with the specified name
+ xml_node child(const char_t* name) const;
+ xml_attribute attribute(const char_t* name) const;
+ xml_node next_sibling(const char_t* name) const;
+ xml_node previous_sibling(const char_t* name) const;
+
+ // Get attribute, starting the search from a hint (and updating hint so that searching for a sequence of attributes is fast)
+ xml_attribute attribute(const char_t* name, xml_attribute& hint) const;
+
+ // Get child value of current node; that is, value of the first child node of type PCDATA/CDATA
+ const char_t* child_value() const;
+
+ // Get child value of child with specified name. Equivalent to child(name).child_value().
+ const char_t* child_value(const char_t* name) const;
+
+ // Set node name/value (returns false if node is empty, there is not enough memory, or node can not have name/value)
+ bool set_name(const char_t* rhs);
+ bool set_value(const char_t* rhs);
+
+ // Add attribute with specified name. Returns added attribute, or empty attribute on errors.
+ xml_attribute append_attribute(const char_t* name);
+ xml_attribute prepend_attribute(const char_t* name);
+ xml_attribute insert_attribute_after(const char_t* name, const xml_attribute& attr);
+ xml_attribute insert_attribute_before(const char_t* name, const xml_attribute& attr);
+
+ // Add a copy of the specified attribute. Returns added attribute, or empty attribute on errors.
+ xml_attribute append_copy(const xml_attribute& proto);
+ xml_attribute prepend_copy(const xml_attribute& proto);
+ xml_attribute insert_copy_after(const xml_attribute& proto, const xml_attribute& attr);
+ xml_attribute insert_copy_before(const xml_attribute& proto, const xml_attribute& attr);
+
+ // Add child node with specified type. Returns added node, or empty node on errors.
+ xml_node append_child(xml_node_type type = node_element);
+ xml_node prepend_child(xml_node_type type = node_element);
+ xml_node insert_child_after(xml_node_type type, const xml_node& node);
+ xml_node insert_child_before(xml_node_type type, const xml_node& node);
+
+ // Add child element with specified name. Returns added node, or empty node on errors.
+ xml_node append_child(const char_t* name);
+ xml_node prepend_child(const char_t* name);
+ xml_node insert_child_after(const char_t* name, const xml_node& node);
+ xml_node insert_child_before(const char_t* name, const xml_node& node);
+
+ // Add a copy of the specified node as a child. Returns added node, or empty node on errors.
+ xml_node append_copy(const xml_node& proto);
+ xml_node prepend_copy(const xml_node& proto);
+ xml_node insert_copy_after(const xml_node& proto, const xml_node& node);
+ xml_node insert_copy_before(const xml_node& proto, const xml_node& node);
+
+ // Move the specified node to become a child of this node. Returns moved node, or empty node on errors.
+ xml_node append_move(const xml_node& moved);
+ xml_node prepend_move(const xml_node& moved);
+ xml_node insert_move_after(const xml_node& moved, const xml_node& node);
+ xml_node insert_move_before(const xml_node& moved, const xml_node& node);
+
+ // Remove specified attribute
+ bool remove_attribute(const xml_attribute& a);
+ bool remove_attribute(const char_t* name);
+
+ // Remove all attributes
+ bool remove_attributes();
+
+ // Remove specified child
+ bool remove_child(const xml_node& n);
+ bool remove_child(const char_t* name);
+
+ // Remove all children
+ bool remove_children();
+
+ // Parses buffer as an XML document fragment and appends all nodes as children of the current node.
+ // Copies/converts the buffer, so it may be deleted or changed after the function returns.
+ // Note: append_buffer allocates memory that has the lifetime of the owning document; removing the appended nodes does not immediately reclaim that memory.
+ xml_parse_result append_buffer(const void* contents, size_t size, unsigned int options = parse_default, xml_encoding encoding = encoding_auto);
+
+ // Find attribute using predicate. Returns first attribute for which predicate returned true.
+ template <typename Predicate> xml_attribute find_attribute(Predicate pred) const
+ {
+ if (!_root) return xml_attribute();
+
+ for (xml_attribute attrib = first_attribute(); attrib; attrib = attrib.next_attribute())
+ if (pred(attrib))
+ return attrib;
+
+ return xml_attribute();
+ }
+
+ // Find child node using predicate. Returns first child for which predicate returned true.
+ template <typename Predicate> xml_node find_child(Predicate pred) const
+ {
+ if (!_root) return xml_node();
+
+ for (xml_node node = first_child(); node; node = node.next_sibling())
+ if (pred(node))
+ return node;
+
+ return xml_node();
+ }
+
+ // Find node from subtree using predicate. Returns first node from subtree (depth-first), for which predicate returned true.
+ template <typename Predicate> xml_node find_node(Predicate pred) const
+ {
+ if (!_root) return xml_node();
+
+ xml_node cur = first_child();
+
+ while (cur._root && cur._root != _root)
+ {
+ if (pred(cur)) return cur;
+
+ if (cur.first_child()) cur = cur.first_child();
+ else if (cur.next_sibling()) cur = cur.next_sibling();
+ else
+ {
+ while (!cur.next_sibling() && cur._root != _root) cur = cur.parent();
+
+ if (cur._root != _root) cur = cur.next_sibling();
+ }
+ }
+
+ return xml_node();
+ }
+
+ // Find child node by attribute name/value
+ xml_node find_child_by_attribute(const char_t* name, const char_t* attr_name, const char_t* attr_value) const;
+ xml_node find_child_by_attribute(const char_t* attr_name, const char_t* attr_value) const;
+
+ #ifndef PUGIXML_NO_STL
+ // Get the absolute node path from root as a text string.
+ string_t path(char_t delimiter = '/') const;
+ #endif
+
+ // Search for a node by path consisting of node names and . or .. elements.
+ xml_node first_element_by_path(const char_t* path, char_t delimiter = '/') const;
+
+ // Recursively traverse subtree with xml_tree_walker
+ bool traverse(xml_tree_walker& walker);
+
+ #ifndef PUGIXML_NO_XPATH
+ // Select single node by evaluating XPath query. Returns first node from the resulting node set.
+ xpath_node select_node(const char_t* query, xpath_variable_set* variables = PUGIXML_NULL) const;
+ xpath_node select_node(const xpath_query& query) const;
+
+ // Select node set by evaluating XPath query
+ xpath_node_set select_nodes(const char_t* query, xpath_variable_set* variables = PUGIXML_NULL) const;
+ xpath_node_set select_nodes(const xpath_query& query) const;
+
+ // (deprecated: use select_node instead) Select single node by evaluating XPath query.
+ PUGIXML_DEPRECATED xpath_node select_single_node(const char_t* query, xpath_variable_set* variables = PUGIXML_NULL) const;
+ PUGIXML_DEPRECATED xpath_node select_single_node(const xpath_query& query) const;
+
+ #endif
+
+ // Print subtree using a writer object
+ void print(xml_writer& writer, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto, unsigned int depth = 0) const;
+
+ #ifndef PUGIXML_NO_STL
+ // Print subtree to stream
+ void print(std::basic_ostream<char, std::char_traits<char> >& os, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto, unsigned int depth = 0) const;
+ void print(std::basic_ostream<wchar_t, std::char_traits<wchar_t> >& os, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, unsigned int depth = 0) const;
+ #endif
+
+ // Child nodes iterators
+ typedef xml_node_iterator iterator;
+
+ iterator begin() const;
+ iterator end() const;
+
+ // Attribute iterators
+ typedef xml_attribute_iterator attribute_iterator;
+
+ attribute_iterator attributes_begin() const;
+ attribute_iterator attributes_end() const;
+
+ // Range-based for support
+ xml_object_range<xml_node_iterator> children() const;
+ xml_object_range<xml_named_node_iterator> children(const char_t* name) const;
+ xml_object_range<xml_attribute_iterator> attributes() const;
+
+ // Get node offset in parsed file/string (in char_t units) for debugging purposes
+ ptrdiff_t offset_debug() const;
+
+ // Get hash value (unique for handles to the same object)
+ size_t hash_value() const;
+
+ // Get internal pointer
+ xml_node_struct* internal_object() const;
+ };
+
+#ifdef __BORLANDC__
+ // Borland C++ workaround
+ bool PUGIXML_FUNCTION operator&&(const xml_node& lhs, bool rhs);
+ bool PUGIXML_FUNCTION operator||(const xml_node& lhs, bool rhs);
+#endif
+
+ // A helper for working with text inside PCDATA nodes
+ class PUGIXML_CLASS xml_text
+ {
+ friend class xml_node;
+
+ xml_node_struct* _root;
+
+ typedef void (*unspecified_bool_type)(xml_text***);
+
+ explicit xml_text(xml_node_struct* root);
+
+ xml_node_struct* _data_new();
+ xml_node_struct* _data() const;
+
+ public:
+ // Default constructor. Constructs an empty object.
+ xml_text();
+
+ // Safe bool conversion operator
+ operator unspecified_bool_type() const;
+
+ // Borland C++ workaround
+ bool operator!() const;
+
+ // Check if text object is empty
+ bool empty() const;
+
+ // Get text, or "" if object is empty
+ const char_t* get() const;
+
+ // Get text, or the default value if object is empty
+ const char_t* as_string(const char_t* def = PUGIXML_TEXT("")) const;
+
+ // Get text as a number, or the default value if conversion did not succeed or object is empty
+ int as_int(int def = 0) const;
+ unsigned int as_uint(unsigned int def = 0) const;
+ double as_double(double def = 0) const;
+ float as_float(float def = 0) const;
+
+ #ifdef PUGIXML_HAS_LONG_LONG
+ long long as_llong(long long def = 0) const;
+ unsigned long long as_ullong(unsigned long long def = 0) const;
+ #endif
+
+ // Get text as bool (returns true if first character is in '1tTyY' set), or the default value if object is empty
+ bool as_bool(bool def = false) const;
+
+ // Set text (returns false if object is empty or there is not enough memory)
+ bool set(const char_t* rhs);
+
+ // Set text with type conversion (numbers are converted to strings, boolean is converted to "true"/"false")
+ bool set(int rhs);
+ bool set(unsigned int rhs);
+ bool set(long rhs);
+ bool set(unsigned long rhs);
+ bool set(double rhs);
+ bool set(double rhs, int precision);
+ bool set(float rhs);
+ bool set(float rhs, int precision);
+ bool set(bool rhs);
+
+ #ifdef PUGIXML_HAS_LONG_LONG
+ bool set(long long rhs);
+ bool set(unsigned long long rhs);
+ #endif
+
+ // Set text (equivalent to set without error checking)
+ xml_text& operator=(const char_t* rhs);
+ xml_text& operator=(int rhs);
+ xml_text& operator=(unsigned int rhs);
+ xml_text& operator=(long rhs);
+ xml_text& operator=(unsigned long rhs);
+ xml_text& operator=(double rhs);
+ xml_text& operator=(float rhs);
+ xml_text& operator=(bool rhs);
+
+ #ifdef PUGIXML_HAS_LONG_LONG
+ xml_text& operator=(long long rhs);
+ xml_text& operator=(unsigned long long rhs);
+ #endif
+
+ // Get the data node (node_pcdata or node_cdata) for this object
+ xml_node data() const;
+ };
+
+#ifdef __BORLANDC__
+ // Borland C++ workaround
+ bool PUGIXML_FUNCTION operator&&(const xml_text& lhs, bool rhs);
+ bool PUGIXML_FUNCTION operator||(const xml_text& lhs, bool rhs);
+#endif
+
+ // Child node iterator (a bidirectional iterator over a collection of xml_node)
+ class PUGIXML_CLASS xml_node_iterator
+ {
+ friend class xml_node;
+
+ private:
+ mutable xml_node _wrap;
+ xml_node _parent;
+
+ xml_node_iterator(xml_node_struct* ref, xml_node_struct* parent);
+
+ public:
+ // Iterator traits
+ typedef ptrdiff_t difference_type;
+ typedef xml_node value_type;
+ typedef xml_node* pointer;
+ typedef xml_node& reference;
+
+ #ifndef PUGIXML_NO_STL
+ typedef std::bidirectional_iterator_tag iterator_category;
+ #endif
+
+ // Default constructor
+ xml_node_iterator();
+
+ // Construct an iterator which points to the specified node
+ xml_node_iterator(const xml_node& node);
+
+ // Iterator operators
+ bool operator==(const xml_node_iterator& rhs) const;
+ bool operator!=(const xml_node_iterator& rhs) const;
+
+ xml_node& operator*() const;
+ xml_node* operator->() const;
+
+ xml_node_iterator& operator++();
+ xml_node_iterator operator++(int);
+
+ xml_node_iterator& operator--();
+ xml_node_iterator operator--(int);
+ };
+
+ // Attribute iterator (a bidirectional iterator over a collection of xml_attribute)
+ class PUGIXML_CLASS xml_attribute_iterator
+ {
+ friend class xml_node;
+
+ private:
+ mutable xml_attribute _wrap;
+ xml_node _parent;
+
+ xml_attribute_iterator(xml_attribute_struct* ref, xml_node_struct* parent);
+
+ public:
+ // Iterator traits
+ typedef ptrdiff_t difference_type;
+ typedef xml_attribute value_type;
+ typedef xml_attribute* pointer;
+ typedef xml_attribute& reference;
+
+ #ifndef PUGIXML_NO_STL
+ typedef std::bidirectional_iterator_tag iterator_category;
+ #endif
+
+ // Default constructor
+ xml_attribute_iterator();
+
+ // Construct an iterator which points to the specified attribute
+ xml_attribute_iterator(const xml_attribute& attr, const xml_node& parent);
+
+ // Iterator operators
+ bool operator==(const xml_attribute_iterator& rhs) const;
+ bool operator!=(const xml_attribute_iterator& rhs) const;
+
+ xml_attribute& operator*() const;
+ xml_attribute* operator->() const;
+
+ xml_attribute_iterator& operator++();
+ xml_attribute_iterator operator++(int);
+
+ xml_attribute_iterator& operator--();
+ xml_attribute_iterator operator--(int);
+ };
+
+ // Named node range helper
+ class PUGIXML_CLASS xml_named_node_iterator
+ {
+ friend class xml_node;
+
+ public:
+ // Iterator traits
+ typedef ptrdiff_t difference_type;
+ typedef xml_node value_type;
+ typedef xml_node* pointer;
+ typedef xml_node& reference;
+
+ #ifndef PUGIXML_NO_STL
+ typedef std::bidirectional_iterator_tag iterator_category;
+ #endif
+
+ // Default constructor
+ xml_named_node_iterator();
+
+ // Construct an iterator which points to the specified node
+ xml_named_node_iterator(const xml_node& node, const char_t* name);
+
+ // Iterator operators
+ bool operator==(const xml_named_node_iterator& rhs) const;
+ bool operator!=(const xml_named_node_iterator& rhs) const;
+
+ xml_node& operator*() const;
+ xml_node* operator->() const;
+
+ xml_named_node_iterator& operator++();
+ xml_named_node_iterator operator++(int);
+
+ xml_named_node_iterator& operator--();
+ xml_named_node_iterator operator--(int);
+
+ private:
+ mutable xml_node _wrap;
+ xml_node _parent;
+ const char_t* _name;
+
+ xml_named_node_iterator(xml_node_struct* ref, xml_node_struct* parent, const char_t* name);
+ };
+
+ // Abstract tree walker class (see xml_node::traverse)
+ class PUGIXML_CLASS xml_tree_walker
+ {
+ friend class xml_node;
+
+ private:
+ int _depth;
+
+ protected:
+ // Get current traversal depth
+ int depth() const;
+
+ public:
+ xml_tree_walker();
+ virtual ~xml_tree_walker();
+
+ // Callback that is called when traversal begins
+ virtual bool begin(xml_node& node);
+
+ // Callback that is called for each node traversed
+ virtual bool for_each(xml_node& node) = 0;
+
+ // Callback that is called when traversal ends
+ virtual bool end(xml_node& node);
+ };
+
+ // Parsing status, returned as part of xml_parse_result object
+ enum xml_parse_status
+ {
+ status_ok = 0, // No error
+
+ status_file_not_found, // File was not found during load_file()
+ status_io_error, // Error reading from file/stream
+ status_out_of_memory, // Could not allocate memory
+ status_internal_error, // Internal error occurred
+
+ status_unrecognized_tag, // Parser could not determine tag type
+
+ status_bad_pi, // Parsing error occurred while parsing document declaration/processing instruction
+ status_bad_comment, // Parsing error occurred while parsing comment
+ status_bad_cdata, // Parsing error occurred while parsing CDATA section
+ status_bad_doctype, // Parsing error occurred while parsing document type declaration
+ status_bad_pcdata, // Parsing error occurred while parsing PCDATA section
+ status_bad_start_element, // Parsing error occurred while parsing start element tag
+ status_bad_attribute, // Parsing error occurred while parsing element attribute
+ status_bad_end_element, // Parsing error occurred while parsing end element tag
+ status_end_element_mismatch,// There was a mismatch of start-end tags (closing tag had incorrect name, some tag was not closed or there was an excessive closing tag)
+
+ status_append_invalid_root, // Unable to append nodes since root type is not node_element or node_document (exclusive to xml_node::append_buffer)
+
+ status_no_document_element // Parsing resulted in a document without element nodes
+ };
+
+ // Parsing result
+ struct PUGIXML_CLASS xml_parse_result
+ {
+ // Parsing status (see xml_parse_status)
+ xml_parse_status status;
+
+ // Last parsed offset (in char_t units from start of input data)
+ ptrdiff_t offset;
+
+ // Source document encoding
+ xml_encoding encoding;
+
+ // Default constructor, initializes object to failed state
+ xml_parse_result();
+
+ // Cast to bool operator
+ operator bool() const;
+
+ // Get error description
+ const char* description() const;
+ };
+
+ // Document class (DOM tree root)
+ class PUGIXML_CLASS xml_document: public xml_node
+ {
+ private:
+ char_t* _buffer;
+
+ char _memory[192];
+
+ // Non-copyable semantics
+ xml_document(const xml_document&);
+ xml_document& operator=(const xml_document&);
+
+ void _create();
+ void _destroy();
+ void _move(xml_document& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT;
+
+ public:
+ // Default constructor, makes empty document
+ xml_document();
+
+ // Destructor, invalidates all node/attribute handles to this document
+ ~xml_document();
+
+ #ifdef PUGIXML_HAS_MOVE
+ // Move semantics support
+ xml_document(xml_document&& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT;
+ xml_document& operator=(xml_document&& rhs) PUGIXML_NOEXCEPT_IF_NOT_COMPACT;
+ #endif
+
+ // Removes all nodes, leaving the empty document
+ void reset();
+
+ // Removes all nodes, then copies the entire contents of the specified document
+ void reset(const xml_document& proto);
+
+ #ifndef PUGIXML_NO_STL
+ // Load document from stream.
+ xml_parse_result load(std::basic_istream<char, std::char_traits<char> >& stream, unsigned int options = parse_default, xml_encoding encoding = encoding_auto);
+ xml_parse_result load(std::basic_istream<wchar_t, std::char_traits<wchar_t> >& stream, unsigned int options = parse_default);
+ #endif
+
+ // (deprecated: use load_string instead) Load document from zero-terminated string. No encoding conversions are applied.
+ PUGIXML_DEPRECATED xml_parse_result load(const char_t* contents, unsigned int options = parse_default);
+
+ // Load document from zero-terminated string. No encoding conversions are applied.
+ xml_parse_result load_string(const char_t* contents, unsigned int options = parse_default);
+
+ // Load document from file
+ xml_parse_result load_file(const char* path, unsigned int options = parse_default, xml_encoding encoding = encoding_auto);
+ xml_parse_result load_file(const wchar_t* path, unsigned int options = parse_default, xml_encoding encoding = encoding_auto);
+
+ // Load document from buffer. Copies/converts the buffer, so it may be deleted or changed after the function returns.
+ xml_parse_result load_buffer(const void* contents, size_t size, unsigned int options = parse_default, xml_encoding encoding = encoding_auto);
+
+ // Load document from buffer, using the buffer for in-place parsing (the buffer is modified and used for storage of document data).
+ // You should ensure that buffer data will persist throughout the document's lifetime, and free the buffer memory manually once document is destroyed.
+ xml_parse_result load_buffer_inplace(void* contents, size_t size, unsigned int options = parse_default, xml_encoding encoding = encoding_auto);
+
+ // Load document from buffer, using the buffer for in-place parsing (the buffer is modified and used for storage of document data).
+ // You should allocate the buffer with pugixml allocation function; document will free the buffer when it is no longer needed (you can't use it anymore).
+ xml_parse_result load_buffer_inplace_own(void* contents, size_t size, unsigned int options = parse_default, xml_encoding encoding = encoding_auto);
+
+ // Save XML document to writer (semantics is slightly different from xml_node::print, see documentation for details).
+ void save(xml_writer& writer, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto) const;
+
+ #ifndef PUGIXML_NO_STL
+ // Save XML document to stream (semantics is slightly different from xml_node::print, see documentation for details).
+ void save(std::basic_ostream<char, std::char_traits<char> >& stream, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto) const;
+ void save(std::basic_ostream<wchar_t, std::char_traits<wchar_t> >& stream, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default) const;
+ #endif
+
+ // Save XML to file
+ bool save_file(const char* path, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto) const;
+ bool save_file(const wchar_t* path, const char_t* indent = PUGIXML_TEXT("\t"), unsigned int flags = format_default, xml_encoding encoding = encoding_auto) const;
+
+ // Get document element
+ xml_node document_element() const;
+ };
+
+#ifndef PUGIXML_NO_XPATH
+ // XPath query return type
+ enum xpath_value_type
+ {
+ xpath_type_none, // Unknown type (query failed to compile)
+ xpath_type_node_set, // Node set (xpath_node_set)
+ xpath_type_number, // Number
+ xpath_type_string, // String
+ xpath_type_boolean // Boolean
+ };
+
+ // XPath parsing result
+ struct PUGIXML_CLASS xpath_parse_result
+ {
+ // Error message (0 if no error)
+ const char* error;
+
+ // Last parsed offset (in char_t units from string start)
+ ptrdiff_t offset;
+
+ // Default constructor, initializes object to failed state
+ xpath_parse_result();
+
+ // Cast to bool operator
+ operator bool() const;
+
+ // Get error description
+ const char* description() const;
+ };
+
+ // A single XPath variable
+ class PUGIXML_CLASS xpath_variable
+ {
+ friend class xpath_variable_set;
+
+ protected:
+ xpath_value_type _type;
+ xpath_variable* _next;
+
+ xpath_variable(xpath_value_type type);
+
+ // Non-copyable semantics
+ xpath_variable(const xpath_variable&);
+ xpath_variable& operator=(const xpath_variable&);
+
+ public:
+ // Get variable name
+ const char_t* name() const;
+
+ // Get variable type
+ xpath_value_type type() const;
+
+ // Get variable value; no type conversion is performed, default value (false, NaN, empty string, empty node set) is returned on type mismatch error
+ bool get_boolean() const;
+ double get_number() const;
+ const char_t* get_string() const;
+ const xpath_node_set& get_node_set() const;
+
+ // Set variable value; no type conversion is performed, false is returned on type mismatch error
+ bool set(bool value);
+ bool set(double value);
+ bool set(const char_t* value);
+ bool set(const xpath_node_set& value);
+ };
+
+ // A set of XPath variables
+ class PUGIXML_CLASS xpath_variable_set
+ {
+ private:
+ xpath_variable* _data[64];
+
+ void _assign(const xpath_variable_set& rhs);
+ void _swap(xpath_variable_set& rhs);
+
+ xpath_variable* _find(const char_t* name) const;
+
+ static bool _clone(xpath_variable* var, xpath_variable** out_result);
+ static void _destroy(xpath_variable* var);
+
+ public:
+ // Default constructor/destructor
+ xpath_variable_set();
+ ~xpath_variable_set();
+
+ // Copy constructor/assignment operator
+ xpath_variable_set(const xpath_variable_set& rhs);
+ xpath_variable_set& operator=(const xpath_variable_set& rhs);
+
+ #ifdef PUGIXML_HAS_MOVE
+ // Move semantics support
+ xpath_variable_set(xpath_variable_set&& rhs) PUGIXML_NOEXCEPT;
+ xpath_variable_set& operator=(xpath_variable_set&& rhs) PUGIXML_NOEXCEPT;
+ #endif
+
+ // Add a new variable or get the existing one, if the types match
+ xpath_variable* add(const char_t* name, xpath_value_type type);
+
+ // Set value of an existing variable; no type conversion is performed, false is returned if there is no such variable or if types mismatch
+ bool set(const char_t* name, bool value);
+ bool set(const char_t* name, double value);
+ bool set(const char_t* name, const char_t* value);
+ bool set(const char_t* name, const xpath_node_set& value);
+
+ // Get existing variable by name
+ xpath_variable* get(const char_t* name);
+ const xpath_variable* get(const char_t* name) const;
+ };
+
+ // A compiled XPath query object
+ class PUGIXML_CLASS xpath_query
+ {
+ private:
+ void* _impl;
+ xpath_parse_result _result;
+
+ typedef void (*unspecified_bool_type)(xpath_query***);
+
+ // Non-copyable semantics
+ xpath_query(const xpath_query&);
+ xpath_query& operator=(const xpath_query&);
+
+ public:
+ // Construct a compiled object from XPath expression.
+ // If PUGIXML_NO_EXCEPTIONS is not defined, throws xpath_exception on compilation errors.
+ explicit xpath_query(const char_t* query, xpath_variable_set* variables = PUGIXML_NULL);
+
+ // Constructor
+ xpath_query();
+
+ // Destructor
+ ~xpath_query();
+
+ #ifdef PUGIXML_HAS_MOVE
+ // Move semantics support
+ xpath_query(xpath_query&& rhs) PUGIXML_NOEXCEPT;
+ xpath_query& operator=(xpath_query&& rhs) PUGIXML_NOEXCEPT;
+ #endif
+
+ // Get query expression return type
+ xpath_value_type return_type() const;
+
+ // Evaluate expression as boolean value in the specified context; performs type conversion if necessary.
+ // If PUGIXML_NO_EXCEPTIONS is not defined, throws std::bad_alloc on out of memory errors.
+ bool evaluate_boolean(const xpath_node& n) const;
+
+ // Evaluate expression as double value in the specified context; performs type conversion if necessary.
+ // If PUGIXML_NO_EXCEPTIONS is not defined, throws std::bad_alloc on out of memory errors.
+ double evaluate_number(const xpath_node& n) const;
+
+ #ifndef PUGIXML_NO_STL
+ // Evaluate expression as string value in the specified context; performs type conversion if necessary.
+ // If PUGIXML_NO_EXCEPTIONS is not defined, throws std::bad_alloc on out of memory errors.
+ string_t evaluate_string(const xpath_node& n) const;
+ #endif
+
+ // Evaluate expression as string value in the specified context; performs type conversion if necessary.
+ // At most capacity characters are written to the destination buffer, full result size is returned (includes terminating zero).
+ // If PUGIXML_NO_EXCEPTIONS is not defined, throws std::bad_alloc on out of memory errors.
+ // If PUGIXML_NO_EXCEPTIONS is defined, returns empty set instead.
+ size_t evaluate_string(char_t* buffer, size_t capacity, const xpath_node& n) const;
+
+ // Evaluate expression as node set in the specified context.
+ // If PUGIXML_NO_EXCEPTIONS is not defined, throws xpath_exception on type mismatch and std::bad_alloc on out of memory errors.
+ // If PUGIXML_NO_EXCEPTIONS is defined, returns empty node set instead.
+ xpath_node_set evaluate_node_set(const xpath_node& n) const;
+
+ // Evaluate expression as node set in the specified context.
+ // Return first node in document order, or empty node if node set is empty.
+ // If PUGIXML_NO_EXCEPTIONS is not defined, throws xpath_exception on type mismatch and std::bad_alloc on out of memory errors.
+ // If PUGIXML_NO_EXCEPTIONS is defined, returns empty node instead.
+ xpath_node evaluate_node(const xpath_node& n) const;
+
+ // Get parsing result (used to get compilation errors in PUGIXML_NO_EXCEPTIONS mode)
+ const xpath_parse_result& result() const;
+
+ // Safe bool conversion operator
+ operator unspecified_bool_type() const;
+
+ // Borland C++ workaround
+ bool operator!() const;
+ };
+
+ #ifndef PUGIXML_NO_EXCEPTIONS
+ #if defined(_MSC_VER)
+ // C4275 can be ignored in Visual C++ if you are deriving
+ // from a type in the Standard C++ Library
+ #pragma warning(push)
+ #pragma warning(disable: 4275)
+ #endif
+ // XPath exception class
+ class PUGIXML_CLASS xpath_exception: public std::exception
+ {
+ private:
+ xpath_parse_result _result;
+
+ public:
+ // Construct exception from parse result
+ explicit xpath_exception(const xpath_parse_result& result);
+
+ // Get error message
+ virtual const char* what() const throw() PUGIXML_OVERRIDE;
+
+ // Get parse result
+ const xpath_parse_result& result() const;
+ };
+ #if defined(_MSC_VER)
+ #pragma warning(pop)
+ #endif
+ #endif
+
+ // XPath node class (either xml_node or xml_attribute)
+ class PUGIXML_CLASS xpath_node
+ {
+ private:
+ xml_node _node;
+ xml_attribute _attribute;
+
+ typedef void (*unspecified_bool_type)(xpath_node***);
+
+ public:
+ // Default constructor; constructs empty XPath node
+ xpath_node();
+
+ // Construct XPath node from XML node/attribute
+ xpath_node(const xml_node& node);
+ xpath_node(const xml_attribute& attribute, const xml_node& parent);
+
+ // Get node/attribute, if any
+ xml_node node() const;
+ xml_attribute attribute() const;
+
+ // Get parent of contained node/attribute
+ xml_node parent() const;
+
+ // Safe bool conversion operator
+ operator unspecified_bool_type() const;
+
+ // Borland C++ workaround
+ bool operator!() const;
+
+ // Comparison operators
+ bool operator==(const xpath_node& n) const;
+ bool operator!=(const xpath_node& n) const;
+ };
+
+#ifdef __BORLANDC__
+ // Borland C++ workaround
+ bool PUGIXML_FUNCTION operator&&(const xpath_node& lhs, bool rhs);
+ bool PUGIXML_FUNCTION operator||(const xpath_node& lhs, bool rhs);
+#endif
+
+ // A fixed-size collection of XPath nodes
+ class PUGIXML_CLASS xpath_node_set
+ {
+ public:
+ // Collection type
+ enum type_t
+ {
+ type_unsorted, // Not ordered
+ type_sorted, // Sorted by document order (ascending)
+ type_sorted_reverse // Sorted by document order (descending)
+ };
+
+ // Constant iterator type
+ typedef const xpath_node* const_iterator;
+
+ // We define non-constant iterator to be the same as constant iterator so that various generic algorithms (i.e. boost foreach) work
+ typedef const xpath_node* iterator;
+
+ // Default constructor. Constructs empty set.
+ xpath_node_set();
+
+ // Constructs a set from iterator range; data is not checked for duplicates and is not sorted according to provided type, so be careful
+ xpath_node_set(const_iterator begin, const_iterator end, type_t type = type_unsorted);
+
+ // Destructor
+ ~xpath_node_set();
+
+ // Copy constructor/assignment operator
+ xpath_node_set(const xpath_node_set& ns);
+ xpath_node_set& operator=(const xpath_node_set& ns);
+
+ #ifdef PUGIXML_HAS_MOVE
+ // Move semantics support
+ xpath_node_set(xpath_node_set&& rhs) PUGIXML_NOEXCEPT;
+ xpath_node_set& operator=(xpath_node_set&& rhs) PUGIXML_NOEXCEPT;
+ #endif
+
+ // Get collection type
+ type_t type() const;
+
+ // Get collection size
+ size_t size() const;
+
+ // Indexing operator
+ const xpath_node& operator[](size_t index) const;
+
+ // Collection iterators
+ const_iterator begin() const;
+ const_iterator end() const;
+
+ // Sort the collection in ascending/descending order by document order
+ void sort(bool reverse = false);
+
+ // Get first node in the collection by document order
+ xpath_node first() const;
+
+ // Check if collection is empty
+ bool empty() const;
+
+ private:
+ type_t _type;
+
+ xpath_node _storage[1];
+
+ xpath_node* _begin;
+ xpath_node* _end;
+
+ void _assign(const_iterator begin, const_iterator end, type_t type);
+ void _move(xpath_node_set& rhs) PUGIXML_NOEXCEPT;
+ };
+#endif
+
+#ifndef PUGIXML_NO_STL
+ // Convert wide string to UTF8
+ std::basic_string<char, std::char_traits<char>, std::allocator<char> > PUGIXML_FUNCTION as_utf8(const wchar_t* str);
+ std::basic_string<char, std::char_traits<char>, std::allocator<char> > PUGIXML_FUNCTION as_utf8(const std::basic_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> >& str);
+
+ // Convert UTF8 to wide string
+ std::basic_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> > PUGIXML_FUNCTION as_wide(const char* str);
+ std::basic_string<wchar_t, std::char_traits<wchar_t>, std::allocator<wchar_t> > PUGIXML_FUNCTION as_wide(const std::basic_string<char, std::char_traits<char>, std::allocator<char> >& str);
+#endif
+
+ // Memory allocation function interface; returns pointer to allocated memory or NULL on failure
+ typedef void* (*allocation_function)(size_t size);
+
+ // Memory deallocation function interface
+ typedef void (*deallocation_function)(void* ptr);
+
+ // Override default memory management functions. All subsequent allocations/deallocations will be performed via supplied functions.
+ void PUGIXML_FUNCTION set_memory_management_functions(allocation_function allocate, deallocation_function deallocate);
+
+ // Get current memory management functions
+ allocation_function PUGIXML_FUNCTION get_memory_allocation_function();
+ deallocation_function PUGIXML_FUNCTION get_memory_deallocation_function();
+}
+
+#if !defined(PUGIXML_NO_STL) && (defined(_MSC_VER) || defined(__ICC))
+namespace std
+{
+ // Workarounds for (non-standard) iterator category detection for older versions (MSVC7/IC8 and earlier)
+ std::bidirectional_iterator_tag PUGIXML_FUNCTION _Iter_cat(const pugi::xml_node_iterator&);
+ std::bidirectional_iterator_tag PUGIXML_FUNCTION _Iter_cat(const pugi::xml_attribute_iterator&);
+ std::bidirectional_iterator_tag PUGIXML_FUNCTION _Iter_cat(const pugi::xml_named_node_iterator&);
+}
+#endif
+
+#if !defined(PUGIXML_NO_STL) && defined(__SUNPRO_CC)
+namespace std
+{
+ // Workarounds for (non-standard) iterator category detection
+ std::bidirectional_iterator_tag PUGIXML_FUNCTION __iterator_category(const pugi::xml_node_iterator&);
+ std::bidirectional_iterator_tag PUGIXML_FUNCTION __iterator_category(const pugi::xml_attribute_iterator&);
+ std::bidirectional_iterator_tag PUGIXML_FUNCTION __iterator_category(const pugi::xml_named_node_iterator&);
+}
+#endif
+
+#endif
+
+// Make sure implementation is included in header-only mode
+// Use macro expansion in #include to work around QMake (QTBUG-11923)
+#if defined(PUGIXML_HEADER_ONLY) && !defined(PUGIXML_SOURCE)
+# define PUGIXML_SOURCE "pugixml.cpp"
+# include PUGIXML_SOURCE
+#endif
+
+/**
+ * Copyright (c) 2006-2022 Arseny Kapoulkine
+ *
+ * Permission is hereby granted, free of charge, to any person
+ * obtaining a copy of this software and associated documentation
+ * files (the "Software"), to deal in the Software without
+ * restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following
+ * conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+ * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+ * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+ * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+ * OTHER DEALINGS IN THE SOFTWARE.
+ */
diff --git a/src/readline_callbacks.cc b/src/readline_callbacks.cc
new file mode 100644
index 0000000..34a188f
--- /dev/null
+++ b/src/readline_callbacks.cc
@@ -0,0 +1,908 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "base/humanize.network.hh"
+#include "base/injector.hh"
+#include "command_executor.hh"
+#include "config.h"
+#include "field_overlay_source.hh"
+#include "help_text_formatter.hh"
+#include "lnav.hh"
+#include "lnav_config.hh"
+#include "lnav_util.hh"
+#include "log_format_loader.hh"
+#include "plain_text_source.hh"
+#include "readline_curses.hh"
+#include "readline_highlighters.hh"
+#include "service_tags.hh"
+#include "sql_help.hh"
+#include "sqlite-extension-func.hh"
+#include "tailer/tailer.looper.hh"
+#include "view_helpers.examples.hh"
+#include "vtab_module.hh"
+#include "yajlpp/yajlpp.hh"
+
+using namespace std::chrono_literals;
+
+#define ABORT_MSG "(Press " ANSI_BOLD("CTRL+]") " to abort)"
+
+#define STR_HELPER(x) #x
+#define STR(x) STR_HELPER(x)
+
+#define ANSI_RE(msg) ANSI_CSI "1;3" STR(COLOR_CYAN) "m" msg ANSI_NORM
+#define ANSI_CLS(msg) ANSI_CSI "1;3" STR(COLOR_MAGENTA) "m" msg ANSI_NORM
+#define ANSI_KW(msg) ANSI_CSI "3" STR(COLOR_BLUE) "m" msg ANSI_NORM
+#define ANSI_REV(msg) ANSI_CSI "7m" msg ANSI_NORM
+#define ANSI_STR(msg) ANSI_CSI "32m" msg ANSI_NORM
+
+const char *RE_HELP =
+ " " ANSI_RE(".") " Any character "
+ " " "a" ANSI_RE("|") "b a or b "
+ " " ANSI_RE("(?-i)") " Case-sensitive search\n"
+
+ " " ANSI_CLS("\\w") " Word character "
+ " " "a" ANSI_RE("?") " 0 or 1 a's "
+ " " ANSI_RE("$") " End of string\n"
+
+ " " ANSI_CLS("\\d") " Digit "
+ " " "a" ANSI_RE("*") " 0 or more a's "
+ " " ANSI_RE("(") "..." ANSI_RE(")") " Capture\n"
+
+ " " ANSI_CLS("\\s") " White space "
+ " " "a" ANSI_RE("+") " 1 or more a's "
+ " " ANSI_RE("^") " Start of string\n"
+
+ " " ANSI_RE("\\") " Escape character "
+ " " ANSI_RE("[^") "ab" ANSI_RE("]") " " ANSI_BOLD("Not") " a or b "
+ " " ANSI_RE("[") "ab" ANSI_RE("-") "d" ANSI_RE("]") " Any of a, b, c, or d"
+;
+
+const char *RE_EXAMPLE =
+ ANSI_UNDERLINE("Examples") "\n"
+ " abc" ANSI_RE("*") " matches "
+ ANSI_STR("'ab'") ", " ANSI_STR("'abc'") ", " ANSI_STR("'abccc'") "\n"
+
+ " key=" ANSI_RE("(\\w+)")
+ " matches key=" ANSI_REV("123") ", key=" ANSI_REV("abc") " and captures 123 and abc\n"
+
+ " " ANSI_RE("\\") "[abc" ANSI_RE("\\") "] matches " ANSI_STR("'[abc]'") "\n"
+
+ " " ANSI_RE("(?-i)") "ABC matches " ANSI_STR("'ABC'") " and " ANSI_UNDERLINE("not") " " ANSI_STR("'abc'")
+;
+
+const char *SQL_HELP =
+ " " ANSI_KW("SELECT") " Select rows from a table "
+ " " ANSI_KW("DELETE") " Delete rows from a table\n"
+ " " ANSI_KW("INSERT") " Insert rows into a table "
+ " " ANSI_KW("UPDATE") " Update rows in a table\n"
+ " " ANSI_KW("CREATE") " Create a table/index "
+ " " ANSI_KW("DROP") " Drop a table/index\n"
+ " " ANSI_KW("ATTACH") " Attach a SQLite database file "
+ " " ANSI_KW("DETACH") " Detach a SQLite database"
+;
+
+const char *SQL_EXAMPLE =
+ ANSI_UNDERLINE("Examples") "\n"
+ " SELECT * FROM %s WHERE log_level >= 'warning' LIMIT 10\n"
+ " UPDATE %s SET log_mark = 1 WHERE log_line = log_top_line()\n"
+ " SELECT * FROM logline LIMIT 10"
+;
+
+static const char* LNAV_CMD_PROMPT = "Enter an lnav command: " ABORT_MSG;
+
+void
+rl_set_help()
+{
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::SEARCH: {
+ lnav_data.ld_doc_source.replace_with(RE_HELP);
+ lnav_data.ld_example_source.replace_with(RE_EXAMPLE);
+ break;
+ }
+ case ln_mode_t::SQL: {
+ textview_curses& log_view = lnav_data.ld_views[LNV_LOG];
+ auto* lss = (logfile_sub_source*) log_view.get_sub_source();
+ attr_line_t example_al;
+
+ if (log_view.get_inner_height() > 0) {
+ auto cl = lss->at(log_view.get_top());
+ auto lf = lss->find(cl);
+ const auto* format_name = lf->get_format()->get_name().get();
+
+ example_al.with_ansi_string(
+ SQL_EXAMPLE, format_name, format_name);
+ readline_sqlite_highlighter(example_al, 0);
+ }
+
+ lnav_data.ld_doc_source.replace_with(SQL_HELP);
+ lnav_data.ld_example_source.replace_with(example_al);
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static bool
+rl_sql_help(readline_curses* rc)
+{
+ attr_line_t al(rc->get_line_buffer());
+ const string_attrs_t& sa = al.get_attrs();
+ size_t x = rc->get_x();
+ bool has_doc = false;
+
+ if (x > 0) {
+ x -= 1;
+ }
+
+ annotate_sql_statement(al);
+
+ auto avail_help = find_sql_help_for_line(al, x);
+
+ if (!avail_help.empty()) {
+ size_t help_count = avail_help.size();
+ textview_curses& dtc = lnav_data.ld_doc_view;
+ textview_curses& etc = lnav_data.ld_example_view;
+ unsigned long doc_width, ex_width;
+ vis_line_t doc_height, ex_height;
+ attr_line_t doc_al, ex_al;
+
+ dtc.get_dimensions(doc_height, doc_width);
+ etc.get_dimensions(ex_height, ex_width);
+
+ for (const auto& ht : avail_help) {
+ format_help_text_for_term(*ht,
+ std::min(70UL, doc_width),
+ doc_al,
+ help_count > 1
+ ? help_text_content::synopsis
+ : help_text_content::full);
+ if (help_count == 1) {
+ format_example_text_for_term(
+ *ht, eval_example, std::min(70UL, ex_width), ex_al);
+ } else {
+ doc_al.append("\n");
+ }
+ }
+
+ if (!doc_al.empty()) {
+ lnav_data.ld_doc_source.replace_with(doc_al);
+ dtc.reload_data();
+
+ lnav_data.ld_example_source.replace_with(ex_al);
+ etc.reload_data();
+
+ has_doc = true;
+ }
+ }
+
+ auto ident_iter = find_string_attr_containing(
+ sa, &SQL_IDENTIFIER_ATTR, al.nearest_text(x));
+ if (ident_iter != sa.end()) {
+ auto ident = al.get_substring(ident_iter->sa_range);
+ auto intern_ident = intern_string::lookup(ident);
+ auto vtab = lnav_data.ld_vtab_manager->lookup_impl(intern_ident);
+ auto vtab_module_iter = vtab_module_ddls.find(intern_ident);
+ std::string ddl;
+
+ if (vtab != nullptr) {
+ ddl = trim(vtab->get_table_statement());
+ } else if (vtab_module_iter != vtab_module_ddls.end()) {
+ ddl = vtab_module_iter->second;
+ } else {
+ auto table_ddl_iter = lnav_data.ld_table_ddl.find(ident);
+
+ if (table_ddl_iter != lnav_data.ld_table_ddl.end()) {
+ ddl = table_ddl_iter->second;
+ }
+ }
+
+ if (!ddl.empty()) {
+ lnav_data.ld_preview_source.replace_with(ddl)
+ .set_text_format(text_format_t::TF_SQL)
+ .truncate_to(30);
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "Definition for table -- %s", ident.c_str());
+ }
+ }
+
+ return has_doc;
+}
+
+void
+rl_change(readline_curses* rc)
+{
+ static const std::set<std::string> COMMANDS_WITH_SQL = {
+ "filter-expr",
+ "mark-expr",
+ };
+
+ static const std::set<std::string> COMMANDS_FOR_FIELDS = {
+ "hide-fields",
+ "show-fields",
+ };
+
+ textview_curses* tc = get_textview_for_mode(lnav_data.ld_mode);
+
+ tc->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
+ tc->get_highlights().erase({highlight_source_t::PREVIEW, "bodypreview"});
+ lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
+ lnav_data.ld_user_message_source.clear();
+ lnav_data.ld_preview_source.clear();
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(false)
+ .clear();
+
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::COMMAND: {
+ static std::string last_command;
+ static int generation = 0;
+
+ const auto line = rc->get_line_buffer();
+ std::vector<std::string> args;
+ auto iter = lnav_commands.end();
+
+ split_ws(line, args);
+
+ if (args.empty()) {
+ generation = 0;
+ } else if (args[0] != last_command) {
+ last_command = args[0];
+ generation = 0;
+ } else {
+ generation += 1;
+ }
+
+ auto* os = tc->get_overlay_source();
+ if (!args.empty() && os != nullptr) {
+ auto* fos = dynamic_cast<field_overlay_source*>(os);
+
+ if (fos != nullptr) {
+ if (generation == 0) {
+ auto& top_ctx = fos->fos_contexts.top();
+
+ if (COMMANDS_WITH_SQL.count(args[0]) > 0) {
+ top_ctx.c_prefix = ":";
+ top_ctx.c_show = true;
+ top_ctx.c_show_discovered = false;
+ } else if (COMMANDS_FOR_FIELDS.count(args[0]) > 0) {
+ top_ctx.c_prefix = "";
+ top_ctx.c_show = true;
+ top_ctx.c_show_discovered = false;
+ } else {
+ top_ctx.c_prefix = "";
+ top_ctx.c_show = false;
+ }
+ tc->set_sync_selection_and_top(top_ctx.c_show);
+ }
+ }
+ }
+
+ if (!args.empty()) {
+ iter = lnav_commands.find(args[0]);
+ }
+ if (iter == lnav_commands.end()) {
+ lnav_data.ld_doc_source.clear();
+ lnav_data.ld_example_source.clear();
+ lnav_data.ld_preview_source.clear();
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(false)
+ .clear();
+ lnav_data.ld_bottom_source.set_prompt(LNAV_CMD_PROMPT);
+ lnav_data.ld_bottom_source.grep_error("");
+ } else if (args[0] == "config" && args.size() > 1) {
+ static const auto INPUT_SRC = intern_string::lookup("input");
+ yajlpp_parse_context ypc(INPUT_SRC, &lnav_config_handlers);
+
+ ypc.set_path(args[1]).with_obj(lnav_config);
+ ypc.update_callbacks();
+
+ if (ypc.ypc_current_handler != nullptr) {
+ const json_path_handler_base* jph = ypc.ypc_current_handler;
+ char help_text[1024];
+
+ snprintf(help_text,
+ sizeof(help_text),
+ ANSI_BOLD("%s %s") " -- %s " ABORT_MSG,
+ jph->jph_property.c_str(),
+ jph->jph_synopsis,
+ jph->jph_description);
+ lnav_data.ld_bottom_source.set_prompt(help_text);
+ lnav_data.ld_bottom_source.grep_error("");
+ } else {
+ lnav_data.ld_bottom_source.grep_error(
+ "Unknown configuration option: " + args[1]);
+ }
+ } else if ((args[0] != "filter-expr" && args[0] != "mark-expr")
+ || !rl_sql_help(rc))
+ {
+ readline_context::command_t& cmd = *iter->second;
+ const help_text& ht = cmd.c_help;
+
+ if (ht.ht_name) {
+ textview_curses& dtc = lnav_data.ld_doc_view;
+ textview_curses& etc = lnav_data.ld_example_view;
+ unsigned long width;
+ vis_line_t height;
+ attr_line_t al;
+
+ dtc.get_dimensions(height, width);
+ format_help_text_for_term(ht, std::min(70UL, width), al);
+ lnav_data.ld_doc_source.replace_with(al);
+ dtc.set_needs_update();
+
+ al.clear();
+ etc.get_dimensions(height, width);
+ format_example_text_for_term(ht, eval_example, width, al);
+ lnav_data.ld_example_source.replace_with(al);
+ etc.set_needs_update();
+ }
+
+ if (cmd.c_prompt != nullptr && generation == 0
+ && trim(line) == args[0])
+ {
+ const auto new_prompt
+ = cmd.c_prompt(lnav_data.ld_exec_context, line);
+
+ if (!new_prompt.empty()) {
+ rc->rewrite_line(line.length(), new_prompt);
+ }
+ }
+
+ lnav_data.ld_bottom_source.grep_error("");
+ lnav_data.ld_status[LNS_BOTTOM].window_change();
+ }
+ break;
+ }
+ case ln_mode_t::EXEC: {
+ const auto line = rc->get_line_buffer();
+ size_t name_end = line.find(' ');
+ const auto script_name = line.substr(0, name_end);
+ auto& scripts = injector::get<available_scripts&>();
+ auto iter = scripts.as_scripts.find(script_name);
+
+ if (iter == scripts.as_scripts.end()
+ || iter->second[0].sm_description.empty())
+ {
+ lnav_data.ld_bottom_source.set_prompt(
+ "Enter a script to execute: " ABORT_MSG);
+ } else {
+ auto& meta = iter->second[0];
+ char help_text[1024];
+
+ snprintf(help_text,
+ sizeof(help_text),
+ ANSI_BOLD("%s") " -- %s " ABORT_MSG,
+ meta.sm_synopsis.c_str(),
+ meta.sm_description.c_str());
+ lnav_data.ld_bottom_source.set_prompt(help_text);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void
+rl_search_internal(readline_curses* rc, ln_mode_t mode, bool complete = false)
+{
+ textview_curses* tc = get_textview_for_mode(mode);
+ std::string term_val;
+ std::string name;
+
+ tc->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
+ tc->get_highlights().erase({highlight_source_t::PREVIEW, "bodypreview"});
+ lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
+ tc->reload_data();
+ lnav_data.ld_user_message_source.clear();
+
+ switch (mode) {
+ case ln_mode_t::SEARCH:
+ case ln_mode_t::SEARCH_FILTERS:
+ case ln_mode_t::SEARCH_FILES:
+ case ln_mode_t::SEARCH_SPECTRO_DETAILS:
+ name = "$search";
+ break;
+
+ case ln_mode_t::CAPTURE:
+ require(0);
+ name = "$capture";
+ break;
+
+ case ln_mode_t::COMMAND: {
+ lnav_data.ld_exec_context.ec_dry_run = true;
+
+ lnav_data.ld_preview_generation += 1;
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(false)
+ .clear();
+ lnav_data.ld_preview_source.clear();
+ auto result = execute_command(lnav_data.ld_exec_context,
+ rc->get_value().get_string());
+
+ if (result.isOk()) {
+ auto msg = result.unwrap();
+
+ if (msg.empty()) {
+ lnav_data.ld_bottom_source.set_prompt(LNAV_CMD_PROMPT);
+ lnav_data.ld_bottom_source.grep_error("");
+ } else {
+ lnav_data.ld_bottom_source.set_prompt(msg);
+ lnav_data.ld_bottom_source.grep_error("");
+ }
+ } else {
+ lnav_data.ld_bottom_source.set_prompt("");
+ lnav_data.ld_bottom_source.grep_error(
+ result.unwrapErr().um_message.get_string());
+ }
+
+ lnav_data.ld_preview_view.reload_data();
+
+ lnav_data.ld_exec_context.ec_dry_run = false;
+ return;
+ }
+
+ case ln_mode_t::SQL: {
+ term_val = trim(rc->get_value().get_string() + ";");
+
+ if (!term_val.empty() && term_val[0] == '.') {
+ lnav_data.ld_bottom_source.grep_error("");
+ } else if (!sqlite3_complete(term_val.c_str())) {
+ lnav_data.ld_bottom_source.grep_error(
+ "sql error: incomplete statement");
+ } else {
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+ int retcode;
+
+ retcode
+ = sqlite3_prepare_v2(lnav_data.ld_db,
+ rc->get_value().get_string().c_str(),
+ -1,
+ stmt.out(),
+ nullptr);
+ if (retcode != SQLITE_OK) {
+ const char* errmsg = sqlite3_errmsg(lnav_data.ld_db);
+
+ lnav_data.ld_bottom_source.grep_error(
+ fmt::format(FMT_STRING("sql error: {}"), errmsg));
+ } else {
+ lnav_data.ld_bottom_source.grep_error("");
+ }
+ }
+
+ if (!rl_sql_help(rc)) {
+ rl_set_help();
+ lnav_data.ld_preview_source.clear();
+ }
+ return;
+ }
+
+ case ln_mode_t::BREADCRUMBS:
+ case ln_mode_t::PAGING:
+ case ln_mode_t::FILTER:
+ case ln_mode_t::FILES:
+ case ln_mode_t::EXEC:
+ case ln_mode_t::USER:
+ case ln_mode_t::SPECTRO_DETAILS:
+ case ln_mode_t::BUSY:
+ return;
+ }
+
+ if (!complete) {
+ tc->set_selection(lnav_data.ld_search_start_line);
+ }
+ tc->execute_search(rc->get_value().get_string());
+}
+
+void
+rl_search(readline_curses* rc)
+{
+ auto* tc = get_textview_for_mode(lnav_data.ld_mode);
+
+ rl_search_internal(rc, lnav_data.ld_mode);
+ tc->set_follow_search_for(0, {});
+}
+
+void
+lnav_rl_abort(readline_curses* rc)
+{
+ textview_curses* tc = get_textview_for_mode(lnav_data.ld_mode);
+
+ lnav_data.ld_bottom_source.set_prompt("");
+ lnav_data.ld_example_source.clear();
+ lnav_data.ld_doc_source.clear();
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(false)
+ .clear();
+ lnav_data.ld_preview_source.clear();
+ tc->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
+ tc->get_highlights().erase({highlight_source_t::PREVIEW, "bodypreview"});
+ lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
+
+ std::vector<lnav::console::user_message> errors;
+ lnav_config = rollback_lnav_config;
+ reload_config(errors);
+
+ lnav_data.ld_bottom_source.grep_error("");
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::SEARCH:
+ tc->set_selection(lnav_data.ld_search_start_line);
+ tc->revert_search();
+ break;
+ case ln_mode_t::SQL:
+ tc->reload_data();
+ break;
+ default:
+ break;
+ }
+ lnav_data.ld_rl_view->set_value("");
+ lnav_data.ld_mode = ln_mode_t::PAGING;
+}
+
+static void
+rl_callback_int(readline_curses* rc, bool is_alt)
+{
+ textview_curses* tc = get_textview_for_mode(lnav_data.ld_mode);
+ exec_context& ec = lnav_data.ld_exec_context;
+ std::string alt_msg;
+
+ lnav_data.ld_bottom_source.set_prompt("");
+ lnav_data.ld_doc_source.clear();
+ lnav_data.ld_example_source.clear();
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(false)
+ .clear();
+ lnav_data.ld_preview_source.clear();
+ tc->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
+ tc->get_highlights().erase({highlight_source_t::PREVIEW, "bodypreview"});
+ lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
+
+ auto new_mode = ln_mode_t::PAGING;
+
+ switch (lnav_data.ld_mode) {
+ case ln_mode_t::SEARCH_FILTERS:
+ new_mode = ln_mode_t::FILTER;
+ break;
+ case ln_mode_t::SEARCH_FILES:
+ new_mode = ln_mode_t::FILES;
+ break;
+ case ln_mode_t::SEARCH_SPECTRO_DETAILS:
+ new_mode = ln_mode_t::SPECTRO_DETAILS;
+ break;
+ default:
+ break;
+ }
+
+ auto old_mode = std::exchange(lnav_data.ld_mode, new_mode);
+ switch (old_mode) {
+ case ln_mode_t::BREADCRUMBS:
+ case ln_mode_t::PAGING:
+ case ln_mode_t::FILTER:
+ case ln_mode_t::FILES:
+ case ln_mode_t::SPECTRO_DETAILS:
+ case ln_mode_t::BUSY:
+ require(0);
+ break;
+
+ case ln_mode_t::COMMAND: {
+ rc->set_alt_value("");
+ ec.ec_source.back().s_content
+ = fmt::format(FMT_STRING(":{}"), rc->get_value().get_string());
+ readline_lnav_highlighter(ec.ec_source.back().s_content, -1);
+ ec.ec_source.back().s_content.with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ auto exec_res = execute_command(ec, rc->get_value().get_string());
+ if (exec_res.isOk()) {
+ rc->set_value(exec_res.unwrap());
+ } else {
+ auto um = exec_res.unwrapErr();
+
+ lnav_data.ld_user_message_source.replace_with(
+ um.to_attr_line().rtrim());
+ lnav_data.ld_user_message_view.reload_data();
+ lnav_data.ld_user_message_expiration
+ = std::chrono::steady_clock::now() + 20s;
+ rc->set_value("");
+ }
+ ec.ec_source.back().s_content.clear();
+ break;
+ }
+
+ case ln_mode_t::USER:
+ rc->set_alt_value("");
+ ec.ec_local_vars.top()["value"] = rc->get_value().get_string();
+ rc->set_value("");
+ break;
+
+ case ln_mode_t::SEARCH:
+ case ln_mode_t::SEARCH_FILTERS:
+ case ln_mode_t::SEARCH_FILES:
+ case ln_mode_t::SEARCH_SPECTRO_DETAILS:
+ case ln_mode_t::CAPTURE:
+ rl_search_internal(rc, old_mode, true);
+ if (!rc->get_value().empty()) {
+ auto& bm = tc->get_bookmarks();
+ const auto& bv = bm[&textview_curses::BM_SEARCH];
+ auto vl = is_alt ? bv.prev(tc->get_selection())
+ : bv.next(tc->get_top());
+
+ if (vl) {
+ tc->set_selection(vl.value());
+ } else {
+ tc->set_follow_search_for(2000, [tc, is_alt, &bm]() {
+ if (bm[&textview_curses::BM_SEARCH].empty()) {
+ return false;
+ }
+
+ if (is_alt && tc->is_searching()) {
+ return false;
+ }
+
+ nonstd::optional<vis_line_t> first_hit;
+
+ if (is_alt) {
+ first_hit = bm[&textview_curses::BM_SEARCH].prev(
+ vis_line_t(tc->get_selection()));
+ } else {
+ first_hit = bm[&textview_curses::BM_SEARCH].next(
+ vis_line_t(tc->get_top() - 1));
+ }
+ if (first_hit) {
+ auto first_hit_vl = first_hit.value();
+ if (tc->is_selectable()) {
+ tc->set_selection(first_hit_vl);
+ } else {
+ if (first_hit_vl > 0_vl) {
+ --first_hit_vl;
+ }
+ tc->set_top(first_hit_vl);
+ }
+ }
+
+ return true;
+ });
+ }
+ rc->set_attr_value(
+ attr_line_t("search: ").append(rc->get_value()));
+ rc->set_alt_value(HELP_MSG_2(
+ n, N, "to move forward/backward through search results"));
+ }
+ break;
+
+ case ln_mode_t::SQL: {
+ ec.ec_source.back().s_content
+ = fmt::format(FMT_STRING(";{}"), rc->get_value().get_string());
+ readline_lnav_highlighter(ec.ec_source.back().s_content, -1);
+ ec.ec_source.back().s_content.with_attr_for_all(
+ VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ auto result
+ = execute_sql(ec, rc->get_value().get_string(), alt_msg);
+ auto& dls = lnav_data.ld_db_row_source;
+ attr_line_t prompt;
+
+ if (result.isOk()) {
+ auto msg = result.unwrap();
+
+ if (!msg.empty()) {
+ prompt = lnav::console::user_message::ok(
+ attr_line_t("SQL Result: ")
+ .append(attr_line_t::from_ansi_str(
+ msg.c_str())))
+ .to_attr_line();
+ if (dls.dls_rows.size() > 1) {
+ ensure_view(&lnav_data.ld_views[LNV_DB]);
+ }
+ }
+ } else {
+ auto um = result.unwrapErr();
+ lnav_data.ld_user_message_source.replace_with(
+ um.to_attr_line().rtrim());
+ lnav_data.ld_user_message_view.reload_data();
+ lnav_data.ld_user_message_expiration
+ = std::chrono::steady_clock::now() + 20s;
+ }
+ ec.ec_source.back().s_content.clear();
+
+ rc->set_attr_value(prompt);
+ rc->set_alt_value(alt_msg);
+ break;
+ }
+
+ case ln_mode_t::EXEC: {
+ auto_mem<FILE> tmpout(fclose);
+
+ tmpout = std::tmpfile();
+
+ if (!tmpout) {
+ rc->set_value(fmt::format(
+ FMT_STRING("Unable to open temporary output file: {}"),
+ strerror(errno)));
+ } else {
+ auto fd_copy = auto_fd::dup_of(fileno(tmpout));
+ char desc[256], timestamp[32];
+ time_t current_time = time(nullptr);
+ const auto path_and_args = rc->get_value();
+
+ {
+ exec_context::output_guard og(
+ ec, "tmp", std::make_pair(tmpout.release(), fclose));
+
+ auto exec_res
+ = execute_file(ec, path_and_args.get_string());
+ if (exec_res.isOk()) {
+ rc->set_value(exec_res.unwrap());
+ } else {
+ auto um = exec_res.unwrapErr();
+
+ lnav_data.ld_user_message_source.replace_with(
+ um.to_attr_line().rtrim());
+ lnav_data.ld_user_message_view.reload_data();
+ lnav_data.ld_user_message_expiration
+ = std::chrono::steady_clock::now() + 20s;
+ rc->set_value("");
+ }
+ }
+
+ struct stat st;
+
+ if (fstat(fd_copy, &st) != -1 && st.st_size > 0) {
+ strftime(timestamp,
+ sizeof(timestamp),
+ "%a %b %d %H:%M:%S %Z",
+ localtime(&current_time));
+ snprintf(desc,
+ sizeof(desc),
+ "Output of %s (%s)",
+ path_and_args.get_string().c_str(),
+ timestamp);
+ lnav_data.ld_active_files.fc_file_names[desc]
+ .with_fd(std::move(fd_copy))
+ .with_include_in_session(false)
+ .with_detect_format(false);
+ lnav_data.ld_files_to_front.emplace_back(desc, 0_vl);
+
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(X, "to close the file"));
+ }
+ }
+ }
+ break;
+ }
+ }
+}
+
+void
+rl_callback(readline_curses* rc)
+{
+ rl_callback_int(rc, false);
+}
+
+void
+rl_alt_callback(readline_curses* rc)
+{
+ rl_callback_int(rc, true);
+}
+
+void
+rl_display_matches(readline_curses* rc)
+{
+ const auto& matches = rc->get_matches();
+ auto& tc = lnav_data.ld_match_view;
+ unsigned long width;
+ __attribute((unused)) unsigned long height;
+ int max_len, cols;
+
+ getmaxyx(lnav_data.ld_window, height, width);
+
+ max_len = rc->get_max_match_length() + 2;
+ cols = std::max(1UL, width / max_len);
+
+ if (matches.empty()) {
+ lnav_data.ld_match_source.clear();
+ } else {
+ const auto current_match = rc->get_match_string();
+ int curr_col = 0;
+ attr_line_t al;
+ bool add_nl = false;
+
+ for (const auto& match : matches) {
+ if (add_nl) {
+ al.append(1, '\n');
+ add_nl = false;
+ }
+ if (match == current_match) {
+ al.append(match, VC_STYLE.value(text_attrs{A_REVERSE}));
+ } else {
+ al.append(match);
+ }
+ curr_col += 1;
+ if (curr_col < cols) {
+ int padding = max_len - match.size();
+
+ al.append(padding, ' ');
+ } else {
+ curr_col = 0;
+ add_nl = true;
+ }
+ }
+ lnav_data.ld_match_source.replace_with(al);
+ }
+
+ tc.reload_data();
+}
+
+void
+rl_display_next(readline_curses* rc)
+{
+ textview_curses& tc = lnav_data.ld_match_view;
+
+ if (tc.get_top() >= (tc.get_top_for_last_row() - 1)) {
+ tc.set_top(0_vl);
+ } else {
+ tc.shift_top(tc.get_height());
+ }
+}
+
+void
+rl_completion_request(readline_curses* rc)
+{
+ isc::to<tailer::looper&, services::remote_tailer_t>().send(
+ [rc](auto& tlooper) {
+ auto rp_opt = humanize::network::path::from_str(
+ rc->get_remote_complete_path());
+ if (rp_opt) {
+ tlooper.complete_path(*rp_opt);
+ }
+ });
+}
+
+void
+rl_focus(readline_curses* rc)
+{
+ auto fos = (field_overlay_source*) lnav_data.ld_views[LNV_LOG]
+ .get_overlay_source();
+
+ fos->fos_contexts.emplace("", false, true);
+
+ get_textview_for_mode(lnav_data.ld_mode)->save_current_search();
+}
+
+void
+rl_blur(readline_curses* rc)
+{
+ auto fos = (field_overlay_source*) lnav_data.ld_views[LNV_LOG]
+ .get_overlay_source();
+
+ fos->fos_contexts.pop();
+ lnav_data.ld_views[LNV_LOG].set_sync_selection_and_top(
+ fos->fos_contexts.top().c_show);
+ lnav_data.ld_preview_generation += 1;
+}
diff --git a/src/readline_callbacks.hh b/src/readline_callbacks.hh
new file mode 100644
index 0000000..d89f4b1
--- /dev/null
+++ b/src/readline_callbacks.hh
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LNAV_READLINE_CALLBACKS_HH
+#define LNAV_READLINE_CALLBACKS_HH
+
+void rl_set_help();
+void rl_change(readline_curses* rc);
+void rl_search(readline_curses* rc);
+void lnav_rl_abort(readline_curses* rc);
+void rl_callback(readline_curses* rc);
+void rl_alt_callback(readline_curses* rc);
+void rl_display_matches(readline_curses* rc);
+void rl_display_next(readline_curses* rc);
+void rl_completion_request(readline_curses* rc);
+void rl_focus(readline_curses* rc);
+void rl_blur(readline_curses* rc);
+
+extern const char* RE_HELP;
+extern const char* RE_EXAMPLE;
+extern const char* SQL_HELP;
+extern const char* SQL_EXAMPLE;
+
+#endif // LNAV_READLINE_CALLBACKS_HH
diff --git a/src/readline_context.hh b/src/readline_context.hh
new file mode 100644
index 0000000..047cf56
--- /dev/null
+++ b/src/readline_context.hh
@@ -0,0 +1,183 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file readline_context.hh
+ */
+
+#ifndef readline_context_hh
+#define readline_context_hh
+
+#include <set>
+#include <string>
+
+#include <readline/history.h>
+
+#include "base/lnav.console.hh"
+#include "base/result.h"
+#include "help_text.hh"
+
+class attr_line_t;
+struct exec_context;
+
+typedef void (*readline_highlighter_t)(attr_line_t& line, int x);
+
+/**
+ * Container for information related to different readline contexts. Since
+ * lnav uses readline for different inputs, we need a way to keep things like
+ * history and tab-completions separate.
+ */
+class readline_context {
+public:
+ typedef Result<std::string, lnav::console::user_message> (*command_func_t)(
+ exec_context& ec, std::string cmdline, std::vector<std::string>& args);
+ typedef std::string (*prompt_func_t)(exec_context& ec,
+ const std::string& cmdline);
+ typedef struct _command_t {
+ const char* c_name;
+ command_func_t c_func;
+
+ struct help_text c_help;
+ prompt_func_t c_prompt{nullptr};
+
+ _command_t(const char* name,
+ command_func_t func,
+ help_text help = {},
+ prompt_func_t prompt = nullptr) noexcept
+ : c_name(name), c_func(func), c_help(std::move(help)),
+ c_prompt(prompt)
+ {
+ }
+
+ _command_t(command_func_t func) noexcept : c_name("anon"), c_func(func)
+ {
+ }
+ } command_t;
+ typedef std::map<std::string, command_t*> command_map_t;
+
+ readline_context(std::string name,
+ command_map_t* commands = nullptr,
+ bool case_sensitive = true);
+
+ const std::string& get_name() const { return this->rc_name; }
+
+ void load();
+
+ void save();
+
+ void add_possibility(const std::string& type, const std::string& value)
+ {
+ this->rc_possibilities[type].insert(value);
+ }
+
+ void rem_possibility(const std::string& type, const std::string& value)
+ {
+ this->rc_possibilities[type].erase(value);
+ }
+
+ void clear_possibilities(const std::string& type)
+ {
+ this->rc_possibilities[type].clear();
+ }
+
+ bool is_case_sensitive() const { return this->rc_case_sensitive; }
+
+ readline_context& set_append_character(int ch)
+ {
+ this->rc_append_character = ch;
+
+ return *this;
+ }
+
+ int get_append_character() const
+ {
+ return this->rc_append_character;
+ }
+
+ readline_context& set_highlighter(readline_highlighter_t hl)
+ {
+ this->rc_highlighter = hl;
+ return *this;
+ }
+
+ readline_context& set_quote_chars(const char* qc)
+ {
+ this->rc_quote_chars = qc;
+
+ return *this;
+ }
+
+ readline_context& with_readline_var(char** var, const char* val)
+ {
+ this->rc_vars.emplace_back(var, val);
+
+ return *this;
+ }
+
+ readline_highlighter_t get_highlighter() const
+ {
+ return this->rc_highlighter;
+ }
+
+ static int command_complete(int, int);
+
+ std::map<std::string, std::string> rc_prefixes;
+
+private:
+ static char** attempted_completion(const char* text, int start, int end);
+ static char* completion_generator(const char* text, int state);
+
+ static readline_context* loaded_context;
+ static std::set<std::string>* arg_possibilities;
+
+ struct readline_var {
+ readline_var(char** dst, const char* val)
+ {
+ this->rv_dst.ch = dst;
+ this->rv_val.ch = val;
+ }
+
+ union {
+ char** ch;
+ } rv_dst;
+ union {
+ const char* ch;
+ } rv_val;
+ };
+
+ std::string rc_name;
+ HISTORY_STATE rc_history;
+ std::map<std::string, std::set<std::string>> rc_possibilities;
+ std::map<std::string, std::vector<std::string>> rc_prototypes;
+ bool rc_case_sensitive;
+ int rc_append_character;
+ const char* rc_quote_chars;
+ readline_highlighter_t rc_highlighter;
+ std::vector<readline_var> rc_vars;
+};
+
+#endif
diff --git a/src/readline_curses.cc b/src/readline_curses.cc
new file mode 100644
index 0000000..f74a45c
--- /dev/null
+++ b/src/readline_curses.cc
@@ -0,0 +1,1535 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file readline_curses.cc
+ */
+
+#include <errno.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#ifdef HAVE_PTY_H
+# include <pty.h>
+#endif
+
+#ifdef HAVE_UTIL_H
+# include <util.h>
+#endif
+
+#ifdef HAVE_LIBUTIL_H
+# include <libutil.h>
+#endif
+
+#include <string>
+#include <utility>
+
+#include "base/ansi_scrubber.hh"
+#include "base/auto_mem.hh"
+#include "base/lnav_log.hh"
+#include "base/paths.hh"
+#include "base/string_util.hh"
+#include "fmt/format.h"
+#include "fts_fuzzy_match.hh"
+#include "lnav_util.hh"
+#include "readline_curses.hh"
+#include "shlex.hh"
+#include "spookyhash/SpookyV2.h"
+
+static int got_line = 0;
+static int got_abort = 0;
+static bool alt_done = 0;
+static sig_atomic_t got_timeout = 0;
+static sig_atomic_t got_winch = 0;
+static readline_curses* child_this;
+static sig_atomic_t looping = 1;
+static const int HISTORY_SIZE = 256;
+static int completion_start;
+static const int FUZZY_PEER_THRESHOLD = 30;
+
+static const char* RL_INIT[] = {
+ /*
+ * XXX Need to keep the input on a single line since the display screws
+ * up if it wraps around.
+ */
+ "set horizontal-scroll-mode on",
+ "set bell-style none",
+ "set show-all-if-ambiguous on",
+ "set show-all-if-unmodified on",
+ "set menu-complete-display-prefix on",
+ "TAB: menu-complete",
+ "\"\\e[Z\": menu-complete-backward",
+};
+
+readline_context* readline_context::loaded_context;
+std::set<std::string>* readline_context::arg_possibilities;
+static std::string last_match_str;
+static bool last_match_str_valid;
+static bool arg_needs_shlex;
+static nonstd::optional<std::string> rewrite_line_start;
+
+static void
+sigalrm(int sig)
+{
+ got_timeout = 1;
+}
+
+static void
+sigwinch(int sig)
+{
+ got_winch = 1;
+}
+
+static void
+sigterm(int sig)
+{
+ looping = 0;
+}
+
+static void
+line_ready_tramp(char* line)
+{
+ child_this->line_ready(line);
+ got_line = 1;
+ rl_callback_handler_remove();
+}
+
+static int
+sendall(int sock, const char* buf, size_t len)
+{
+ off_t offset = 0;
+
+ while (len > 0) {
+ int rc = send(sock, &buf[offset], len, 0);
+
+ if (rc == -1) {
+ switch (errno) {
+ case EAGAIN:
+ case EINTR:
+ break;
+ default:
+ return -1;
+ }
+ } else {
+ len -= rc;
+ offset += rc;
+ }
+ }
+
+ return 0;
+}
+
+static int
+sendstring(int sock, const char* buf, size_t len)
+{
+ if (sendall(sock, (char*) &len, sizeof(len)) == -1) {
+ return -1;
+ } else if (sendall(sock, buf, len) == -1) {
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+sendcmd(int sock, char cmd, const char* buf, size_t len)
+{
+ size_t total_len = len + 2;
+ char prefix[2] = {cmd, ':'};
+
+ if (sendall(sock, (char*) &total_len, sizeof(total_len)) == -1) {
+ return -1;
+ } else if (sendall(sock, prefix, sizeof(prefix)) == -1
+ || sendall(sock, buf, len) == -1)
+ {
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+recvall(int sock, char* buf, size_t len)
+{
+ off_t offset = 0;
+
+ while (len > 0) {
+ ssize_t rc = recv(sock, &buf[offset], len, 0);
+
+ if (rc == -1) {
+ switch (errno) {
+ case EAGAIN:
+ case EINTR:
+ break;
+ default:
+ return -1;
+ }
+ } else if (rc == 0) {
+ errno = EIO;
+ return -1;
+ } else {
+ len -= rc;
+ offset += rc;
+ }
+ }
+
+ return 0;
+}
+
+static ssize_t
+recvstring(int sock, char* buf, size_t len)
+{
+ ssize_t retval;
+
+ if (recvall(sock, (char*) &retval, sizeof(retval)) == -1) {
+ return -1;
+ } else if (retval > (ssize_t) len) {
+ return -1;
+ } else if (recvall(sock, buf, retval) == -1) {
+ return -1;
+ }
+
+ return retval;
+}
+
+char*
+readline_context::completion_generator(const char* text_in, int state)
+{
+ static std::vector<std::string> matches;
+
+ std::vector<std::string> long_matches;
+ char* retval = nullptr;
+
+ if (state == 0) {
+ auto text_str = std::string(text_in);
+
+ if (arg_needs_shlex) {
+ shlex arg_lexer(text_str);
+ std::map<std::string, std::string> scope;
+ std::string result;
+
+ if (arg_lexer.eval(result, scope)) {
+ text_str = result;
+ }
+ }
+
+ matches.clear();
+ if (arg_possibilities != nullptr) {
+ for (const auto& poss : (*arg_possibilities)) {
+ auto cmpfunc
+ = (loaded_context->is_case_sensitive() ? strncmp
+ : strncasecmp);
+ auto poss_str = poss.c_str();
+
+ // Check for an exact match and for the quoted version.
+ if (cmpfunc(text_str.c_str(), poss_str, text_str.length()) == 0
+ || ((strchr(loaded_context->rc_quote_chars, poss_str[0])
+ != nullptr)
+ && cmpfunc(text_str.c_str(),
+ &poss_str[1],
+ text_str.length())
+ == 0))
+ {
+ auto poss_slash_count
+ = std::count(poss.begin(), poss.end(), '/');
+
+ if (endswith(poss, "/")) {
+ poss_slash_count -= 1;
+ }
+ if (std::count(text_str.begin(), text_str.end(), '/')
+ == poss_slash_count)
+ {
+ matches.emplace_back(poss);
+ } else {
+ long_matches.emplace_back(poss);
+ }
+ }
+ }
+
+ if (matches.empty()) {
+ matches = std::move(long_matches);
+ }
+
+ if (matches.empty()) {
+ std::vector<std::pair<int, std::string>> fuzzy_matches;
+ std::vector<std::pair<int, std::string>> fuzzy_long_matches;
+
+ for (const auto& poss : (*arg_possibilities)) {
+ std::string poss_str = tolower(poss);
+ int score = 0;
+
+ if (fts::fuzzy_match(
+ text_str.c_str(), poss_str.c_str(), score)
+ && score > 0)
+ {
+ if (score <= 0) {
+ continue;
+ }
+
+ auto poss_slash_count
+ = std::count(poss_str.begin(), poss_str.end(), '/');
+ if (endswith(poss, "/")) {
+ poss_slash_count -= 1;
+ }
+ if (std::count(text_str.begin(), text_str.end(), '/')
+ == poss_slash_count)
+ {
+ fuzzy_matches.emplace_back(score, poss);
+ } else {
+ fuzzy_long_matches.emplace_back(score, poss);
+ }
+ }
+ }
+
+ if (fuzzy_matches.empty()) {
+ fuzzy_matches = std::move(fuzzy_long_matches);
+ }
+
+ if (!fuzzy_matches.empty()) {
+ stable_sort(
+ begin(fuzzy_matches),
+ end(fuzzy_matches),
+ [](auto l, auto r) { return r.first < l.first; });
+
+ int highest = fuzzy_matches[0].first;
+
+ for (const auto& pair : fuzzy_matches) {
+ if (highest - pair.first < FUZZY_PEER_THRESHOLD) {
+ matches.push_back(pair.second);
+ } else {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (matches.size() == 1) {
+ if (text_str == matches[0]) {
+ matches.pop_back();
+ }
+
+ last_match_str_valid = false;
+ if (sendstring(
+ child_this->rc_command_pipe[readline_curses::RCF_SLAVE],
+ "m:0:0:0",
+ 7)
+ == -1)
+ {
+ _exit(1);
+ }
+ }
+ }
+
+ if (!matches.empty()) {
+ retval = strdup(matches.back().c_str());
+ matches.pop_back();
+ }
+
+ return retval;
+}
+
+char**
+readline_context::attempted_completion(const char* text, int start, int end)
+{
+ char** retval = nullptr;
+
+ completion_start = start;
+ if (start == 0
+ && loaded_context->rc_possibilities.find("__command")
+ != loaded_context->rc_possibilities.end())
+ {
+ arg_possibilities = &loaded_context->rc_possibilities["__command"];
+ arg_needs_shlex = false;
+ rl_completion_append_character = loaded_context->rc_append_character;
+ } else {
+ char* space;
+ std::string cmd;
+ std::vector<std::string> prefix;
+ int point = rl_point;
+ while (point > 0 && rl_line_buffer[point] != ' ') {
+ point -= 1;
+ }
+ shlex lexer(rl_line_buffer, point);
+ std::map<std::string, std::string> scope;
+
+ arg_possibilities = nullptr;
+ rl_completion_append_character = 0;
+ if (lexer.split(prefix, scope)) {
+ auto prefix2
+ = fmt::format(FMT_STRING("{}"), fmt::join(prefix, "\x1f"));
+ auto prefix_iter = loaded_context->rc_prefixes.find(prefix2);
+
+ if (prefix_iter != loaded_context->rc_prefixes.end()) {
+ arg_possibilities
+ = &(loaded_context->rc_possibilities[prefix_iter->second]);
+ arg_needs_shlex = false;
+ }
+ }
+
+ if (arg_possibilities == nullptr) {
+ space = strchr(rl_line_buffer, ' ');
+ if (space == nullptr) {
+ space = rl_line_buffer + strlen(rl_line_buffer);
+ }
+ cmd = std::string(rl_line_buffer, space - rl_line_buffer);
+
+ auto iter = loaded_context->rc_prototypes.find(cmd);
+
+ if (iter == loaded_context->rc_prototypes.end()) {
+ if (loaded_context->rc_possibilities.find("*")
+ != loaded_context->rc_possibilities.end())
+ {
+ arg_possibilities = &loaded_context->rc_possibilities["*"];
+ arg_needs_shlex = false;
+ rl_completion_append_character
+ = loaded_context->rc_append_character;
+ }
+ } else {
+ std::vector<std::string>& proto
+ = loaded_context->rc_prototypes[cmd];
+
+ if (proto.empty()) {
+ arg_possibilities = nullptr;
+ } else if (proto[0] == "filename") {
+ shlex fn_lexer(rl_line_buffer, rl_point);
+ std::vector<std::string> fn_list;
+ int found = 0;
+
+ fn_lexer.split(fn_list, scope);
+
+ const auto& last_fn = fn_list.size() <= 1 ? ""
+ : fn_list.back();
+
+ if (last_fn.find(':') != std::string::npos) {
+ auto rp_iter = loaded_context->rc_possibilities.find(
+ "remote-path");
+ if (rp_iter != loaded_context->rc_possibilities.end()) {
+ for (const auto& poss : rp_iter->second) {
+ if (startswith(poss, last_fn.c_str())) {
+ found += 1;
+ }
+ }
+ if (found) {
+ arg_possibilities = &rp_iter->second;
+ arg_needs_shlex = false;
+ }
+ }
+ if (!found || (endswith(last_fn, "/") && found == 1)) {
+ char msg[2048];
+
+ snprintf(
+ msg, sizeof(msg), "\t:%s", last_fn.c_str());
+ sendstring(child_this->rc_command_pipe[1],
+ msg,
+ strlen(msg));
+ }
+ }
+ if (!found) {
+ static std::set<std::string> file_name_set;
+
+ file_name_set.clear();
+ auto_mem<char> completed_fn;
+ int fn_state = 0;
+ auto recent_netlocs_iter
+ = loaded_context->rc_possibilities.find(
+ "recent-netlocs");
+
+ if (recent_netlocs_iter
+ != loaded_context->rc_possibilities.end())
+ {
+ file_name_set.insert(
+ recent_netlocs_iter->second.begin(),
+ recent_netlocs_iter->second.end());
+ }
+ while ((completed_fn = rl_filename_completion_function(
+ last_fn.c_str(), fn_state))
+ != nullptr)
+ {
+ file_name_set.insert(completed_fn.in());
+ fn_state += 1;
+ }
+ arg_possibilities = &file_name_set;
+ arg_needs_shlex = true;
+ }
+ } else {
+ arg_possibilities
+ = &(loaded_context->rc_possibilities[proto[0]]);
+ arg_needs_shlex = false;
+ }
+ }
+ }
+ }
+
+ retval = rl_completion_matches(text, completion_generator);
+ if (retval == nullptr) {
+ rl_attempted_completion_over = 1;
+ }
+
+ return retval;
+}
+
+static int
+rubout_char_or_abort(int count, int key)
+{
+ if (rl_line_buffer[0] == '\0') {
+ rl_done = true;
+ got_abort = 1;
+ got_line = 0;
+ return 0;
+ } else {
+ return rl_rubout(count, '\b');
+ }
+}
+
+static int
+alt_done_func(int count, int key)
+{
+ alt_done = true;
+ rl_newline(count, key);
+ return 0;
+}
+
+int
+readline_context::command_complete(int count, int key)
+{
+ if (loaded_context->rc_possibilities.find("__command")
+ != loaded_context->rc_possibilities.end())
+ {
+ char* space = strchr(rl_line_buffer, ' ');
+
+ if (space == nullptr) {
+ return rl_menu_complete(count, key);
+ }
+ }
+ return rl_insert(count, key);
+}
+
+readline_context::readline_context(std::string name,
+ readline_context::command_map_t* commands,
+ bool case_sensitive)
+ : rc_name(std::move(name)), rc_case_sensitive(case_sensitive),
+ rc_quote_chars("\"'"), rc_highlighter(nullptr)
+{
+ if (commands != nullptr) {
+ command_map_t::iterator iter;
+
+ for (iter = commands->begin(); iter != commands->end(); ++iter) {
+ std::string cmd = iter->first;
+
+ this->rc_possibilities["__command"].insert(cmd);
+ iter->second->c_func(
+ INIT_EXEC_CONTEXT, cmd, this->rc_prototypes[cmd]);
+ }
+ }
+
+ memset(&this->rc_history, 0, sizeof(this->rc_history));
+ history_set_history_state(&this->rc_history);
+
+ auto config_dir = lnav::paths::dotlnav();
+ auto hpath = (config_dir / this->rc_name).string() + ".history";
+ read_history(hpath.c_str());
+ this->save();
+
+ this->rc_append_character = ' ';
+}
+
+void
+readline_context::load()
+{
+ char buffer[128];
+
+ rl_completer_word_break_characters = (char*) " \t\n|()"; /* XXX */
+ /*
+ * XXX Need to keep the input on a single line since the display screws
+ * up if it wraps around.
+ */
+ snprintf(buffer,
+ sizeof(buffer),
+ "set completion-ignore-case %s",
+ this->rc_case_sensitive ? "off" : "on");
+ rl_parse_and_bind(buffer); /* NOTE: buffer is modified */
+
+ loaded_context = this;
+ rl_attempted_completion_function = attempted_completion;
+ history_set_history_state(&this->rc_history);
+ for (auto& rc_var : this->rc_vars) {
+ *(rc_var.rv_dst.ch) = (char*) rc_var.rv_val.ch;
+ }
+}
+
+void
+readline_context::save()
+{
+ HISTORY_STATE* hs = history_get_history_state();
+
+ this->rc_history = *hs;
+ free(hs);
+ hs = nullptr;
+}
+
+readline_curses::readline_curses(
+ std::shared_ptr<pollable_supervisor> supervisor)
+ : pollable(supervisor, pollable::category::interactive),
+ rc_focus(noop_func{}), rc_change(noop_func{}), rc_perform(noop_func{}),
+ rc_alt_perform(noop_func{}), rc_timeout(noop_func{}),
+ rc_abort(noop_func{}), rc_display_match(noop_func{}),
+ rc_display_next(noop_func{}), rc_blur(noop_func{}),
+ rc_completion_request(noop_func{})
+{
+}
+
+readline_curses::~readline_curses()
+{
+ this->rc_pty[RCF_MASTER].reset();
+ this->rc_command_pipe[RCF_MASTER].reset();
+ if (this->rc_child == 0) {
+ _exit(0);
+ } else if (this->rc_child > 0) {
+ int status;
+
+ log_debug("terminating readline child %d", this->rc_child);
+ log_perror(kill(this->rc_child, SIGTERM));
+ this->rc_child = -1;
+
+ while (wait(&status) < 0 && (errno == EINTR)) {
+ ;
+ }
+ log_debug(" child %d has exited", this->rc_child);
+ }
+}
+
+void
+readline_curses::store_matches(char** matches, int num_matches, int max_len)
+{
+ static int match_index = 0;
+ char msg[64];
+ int rc;
+
+ max_len = 0;
+ for (int lpc = 0; lpc <= num_matches; lpc++) {
+ max_len = std::max(max_len, (int) strlen(matches[lpc]));
+ }
+
+ if (last_match_str_valid && strcmp(last_match_str.c_str(), matches[0]) == 0)
+ {
+ match_index += 1;
+ rc = snprintf(msg, sizeof(msg), "n:%d", match_index);
+
+ if (sendstring(child_this->rc_command_pipe[RCF_SLAVE], msg, rc) == -1) {
+ _exit(1);
+ }
+ } else {
+ match_index = 0;
+ rc = snprintf(msg,
+ sizeof(msg),
+ "m:%d:%d:%d",
+ completion_start,
+ num_matches,
+ max_len);
+ if (sendstring(child_this->rc_command_pipe[RCF_SLAVE], msg, rc) == -1) {
+ _exit(1);
+ }
+ for (int lpc = 1; lpc <= num_matches; lpc++) {
+ if (sendstring(child_this->rc_command_pipe[RCF_SLAVE],
+ matches[lpc],
+ strlen(matches[lpc]))
+ == -1)
+ {
+ _exit(1);
+ }
+ }
+
+ last_match_str = matches[0];
+ last_match_str_valid = true;
+ }
+}
+
+void
+readline_curses::start()
+{
+ if (this->rc_child > 0) {
+ return;
+ }
+
+ struct winsize ws;
+ int sp[2];
+
+ if (socketpair(PF_UNIX, SOCK_STREAM, 0, sp) < 0) {
+ throw error(errno);
+ }
+
+ this->rc_command_pipe[RCF_MASTER] = sp[RCF_MASTER];
+ this->rc_command_pipe[RCF_SLAVE] = sp[RCF_SLAVE];
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) {
+ throw error(errno);
+ }
+
+ if (this->vc_width > 0) {
+ ws.ws_col = this->vc_width;
+ } else if (this->vc_width < 0) {
+ ws.ws_col -= this->vc_left;
+ ws.ws_col += this->vc_width;
+ }
+ if (openpty(this->rc_pty[RCF_MASTER].out(),
+ this->rc_pty[RCF_SLAVE].out(),
+ nullptr,
+ nullptr,
+ &ws)
+ < 0)
+ {
+ perror("error: failed to open terminal(openpty)");
+ throw error(errno);
+ }
+
+ if ((this->rc_child = fork()) == -1) {
+ throw error(errno);
+ }
+
+ if (this->rc_child != 0) {
+ this->rc_command_pipe[RCF_SLAVE].reset();
+ this->rc_pty[RCF_SLAVE].reset();
+ return;
+ }
+
+ {
+ char buffer[1024];
+
+ this->rc_command_pipe[RCF_MASTER].reset();
+ this->rc_pty[RCF_MASTER].reset();
+
+ signal(SIGALRM, sigalrm);
+ signal(SIGWINCH, sigwinch);
+ signal(SIGINT, sigterm);
+ signal(SIGTERM, sigterm);
+
+ dup2(this->rc_pty[RCF_SLAVE], STDIN_FILENO);
+ dup2(this->rc_pty[RCF_SLAVE], STDOUT_FILENO);
+
+ setenv("TERM", "vt52", 1);
+
+ rl_initialize();
+ using_history();
+ stifle_history(HISTORY_SIZE);
+
+ rl_add_defun("rubout-char-or-abort", rubout_char_or_abort, '\b');
+ rl_add_defun("alt-done", alt_done_func, '\x0a');
+ // rl_add_defun("command-complete", readline_context::command_complete,
+ // ' ');
+
+ for (const auto* init_cmd : RL_INIT) {
+ snprintf(buffer, sizeof(buffer), "%s", init_cmd);
+ rl_parse_and_bind(buffer); /* NOTE: buffer is modified */
+ }
+
+ child_this = this;
+ }
+
+ std::map<int, readline_context*>::iterator current_context;
+ int maxfd;
+
+ require(!this->rc_contexts.empty());
+
+ rl_completion_display_matches_hook = store_matches;
+
+ current_context = this->rc_contexts.end();
+
+ maxfd = std::max(STDIN_FILENO, this->rc_command_pipe[RCF_SLAVE].get());
+
+ while (looping) {
+ fd_set ready_rfds;
+ int rc;
+
+ FD_ZERO(&ready_rfds);
+ if (current_context != this->rc_contexts.end()) {
+ FD_SET(STDIN_FILENO, &ready_rfds);
+ }
+ FD_SET(this->rc_command_pipe[RCF_SLAVE], &ready_rfds);
+
+ rc = select(maxfd + 1, &ready_rfds, nullptr, nullptr, nullptr);
+ if (rc < 0) {
+ switch (errno) {
+ case EINTR:
+ break;
+ }
+ } else {
+ if (FD_ISSET(STDIN_FILENO, &ready_rfds)) {
+ static uint64_t last_h1, last_h2;
+
+ struct itimerval itv;
+
+ itv.it_value.tv_sec = 0;
+ itv.it_value.tv_usec = KEY_TIMEOUT;
+ itv.it_interval.tv_sec = 0;
+ itv.it_interval.tv_usec = 0;
+ setitimer(ITIMER_REAL, &itv, nullptr);
+
+ rl_callback_read_char();
+ if (RL_ISSTATE(RL_STATE_DONE) && !got_line) {
+ got_line = 1;
+ this->line_ready("");
+ rl_callback_handler_remove();
+ } else {
+ uint64_t h1 = 1, h2 = 2;
+
+ if (rl_last_func == readline_context::command_complete) {
+ rl_last_func = rl_menu_complete;
+ }
+
+ bool complete_done
+ = (rl_last_func != rl_menu_complete
+ && rl_last_func != rl_backward_menu_complete);
+
+ if (complete_done) {
+ last_match_str_valid = false;
+ } else if (rewrite_line_start
+ && !startswith(rl_line_buffer,
+ rewrite_line_start->c_str()))
+ {
+ // If the line was rewritten, the extra text stays on
+ // the screen, so we need to delete it, make sure the
+ // append character is there, and redisplay. For
+ // example, ':co<TAB>' will complete ':comment' and
+ // append the current comment. Pressing '<TAB>' again
+ // would switch to ':config' and the comment text would
+ // be left on the display.
+ rl_delete_text(rl_point, rl_end);
+ if (rl_completion_append_character
+ && rl_line_buffer[rl_point]
+ != rl_completion_append_character)
+ {
+ char buf[2]
+ = {(char) rl_completion_append_character, '\0'};
+
+ rl_insert_text(buf);
+ }
+ rl_redisplay();
+ }
+ rewrite_line_start = nonstd::nullopt;
+
+ SpookyHash::Hash128(rl_line_buffer, rl_end, &h1, &h2);
+
+ if (h1 == last_h1 && h2 == last_h2) {
+ // do nothing
+ } else if (sendcmd(this->rc_command_pipe[RCF_SLAVE],
+ complete_done ? 'l' : 'c',
+ rl_line_buffer,
+ rl_end)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ last_h1 = h1;
+ last_h2 = h2;
+ if (sendcmd(this->rc_command_pipe[RCF_SLAVE], 'w', "", 0)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ }
+ }
+ if (FD_ISSET(this->rc_command_pipe[RCF_SLAVE], &ready_rfds)) {
+ char msg[1024 + 1];
+
+ if ((rc = recvstring(this->rc_command_pipe[RCF_SLAVE],
+ msg,
+ sizeof(msg) - 1))
+ < 0)
+ {
+ looping = false;
+ } else {
+ int context, prompt_start = 0;
+ char type[1024];
+
+ msg[rc] = '\0';
+ if (sscanf(msg, "i:%d:%n", &rl_point, &prompt_start) == 1) {
+ const char* initial = &msg[prompt_start];
+
+ rl_extend_line_buffer(strlen(initial) + 1);
+ strcpy(rl_line_buffer, initial);
+ rl_end = strlen(initial);
+ rewrite_line_start
+ = std::string(rl_line_buffer, rl_point);
+ rl_redisplay();
+ if (sendcmd(this->rc_command_pipe[RCF_SLAVE],
+ 'c',
+ rl_line_buffer,
+ rl_end)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ } else if (sscanf(msg, "f:%d:%n", &context, &prompt_start)
+ == 1
+ && prompt_start != 0
+ && (current_context
+ = this->rc_contexts.find(context))
+ != this->rc_contexts.end())
+ {
+ got_abort = 0;
+ current_context->second->load();
+ rl_callback_handler_install(&msg[prompt_start],
+ line_ready_tramp);
+ last_match_str_valid = false;
+ if (sendcmd(this->rc_command_pipe[RCF_SLAVE],
+ 'l',
+ rl_line_buffer,
+ rl_end)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ if (sendcmd(
+ this->rc_command_pipe[RCF_SLAVE], 'w', "", 0)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ } else if (strcmp(msg, "a") == 0) {
+ char reply[4];
+
+ rl_done = 1;
+ got_timeout = 0;
+ got_line = 1;
+ rl_callback_handler_remove();
+
+ snprintf(reply, sizeof(reply), "a");
+
+ if (sendstring(this->rc_command_pipe[RCF_SLAVE],
+ reply,
+ strlen(reply))
+ == -1)
+ {
+ perror("abort: write failed");
+ _exit(1);
+ }
+ } else if (sscanf(msg,
+ "apre:%d:%1023[^\x1d]\x1d%n",
+ &context,
+ type,
+ &prompt_start)
+ == 2)
+ {
+ require(this->rc_contexts[context] != nullptr);
+
+ this->rc_contexts[context]
+ ->rc_prefixes[std::string(type)]
+ = std::string(&msg[prompt_start]);
+ } else if (sscanf(msg,
+ "ap:%d:%31[^:]:%n",
+ &context,
+ type,
+ &prompt_start)
+ == 2)
+ {
+ require(this->rc_contexts[context] != nullptr);
+
+ this->rc_contexts[context]->add_possibility(
+ std::string(type), std::string(&msg[prompt_start]));
+ if (rl_last_func == rl_complete
+ || rl_last_func == rl_menu_complete)
+ {
+ rl_last_func = NULL;
+ }
+ } else if (sscanf(msg,
+ "rp:%d:%31[^:]:%n",
+ &context,
+ type,
+ &prompt_start)
+ == 2)
+ {
+ require(this->rc_contexts[context] != nullptr);
+
+ this->rc_contexts[context]->rem_possibility(
+ std::string(type), std::string(&msg[prompt_start]));
+ } else if (sscanf(msg, "cpre:%d", &context) == 1) {
+ this->rc_contexts[context]->rc_prefixes.clear();
+ } else if (sscanf(msg, "cp:%d:%s", &context, type)) {
+ this->rc_contexts[context]->clear_possibilities(type);
+ } else {
+ log_error("unhandled message: %s", msg);
+ }
+ }
+ }
+ }
+
+ if (got_timeout) {
+ got_timeout = 0;
+ if (sendcmd(this->rc_command_pipe[RCF_SLAVE],
+ 't',
+ rl_line_buffer,
+ rl_end)
+ == -1)
+ {
+ _exit(1);
+ }
+ }
+ if (got_line) {
+ struct itimerval itv;
+
+ got_line = 0;
+ itv.it_value.tv_sec = 0;
+ itv.it_value.tv_usec = 0;
+ itv.it_interval.tv_sec = 0;
+ itv.it_interval.tv_usec = 0;
+ if (setitimer(ITIMER_REAL, &itv, nullptr) < 0) {
+ log_error("setitimer: %s", strerror(errno));
+ }
+ if (current_context != this->rc_contexts.end()) {
+ current_context->second->save();
+ current_context = this->rc_contexts.end();
+ }
+ }
+ if (got_winch) {
+ struct winsize new_ws;
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &new_ws) == -1) {
+ throw error(errno);
+ }
+ got_winch = 0;
+ rl_set_screen_size(new_ws.ws_row, new_ws.ws_col);
+ }
+ }
+
+ if (this->rc_save_history) {
+ auto config_dir = lnav::paths::dotlnav();
+ for (auto& pair : this->rc_contexts) {
+ pair.second->load();
+
+ auto hpath
+ = (config_dir / pair.second->get_name()).string() + ".history";
+ write_history(hpath.c_str());
+ pair.second->save();
+ }
+ }
+
+ _exit(0);
+}
+
+void
+readline_curses::line_ready(const char* line)
+{
+ auto_mem<char> expanded;
+ char msg[1024] = {0};
+ int rc;
+ const char* cmd_ch = alt_done ? "D" : "d";
+
+ alt_done = false;
+ if (got_abort || line == nullptr) {
+ snprintf(msg, sizeof(msg), "a");
+
+ if (sendstring(this->rc_command_pipe[RCF_SLAVE], msg, strlen(msg))
+ == -1)
+ {
+ perror("abort: write failed");
+ _exit(1);
+ }
+ return;
+ }
+
+ if (rl_line_buffer[0] == '^') {
+ rc = -1;
+ } else {
+ rc = history_expand(rl_line_buffer, expanded.out());
+ }
+ switch (rc) {
+#if 0
+ /* TODO: fix clash between history and pcre metacharacters */
+ case -1:
+ /* XXX */
+ snprintf(msg, sizeof(msg),
+ "e:unable to expand history -- %s",
+ expanded.in());
+ break;
+#endif
+
+ case -1:
+ snprintf(msg, sizeof(msg), "%s:%s", cmd_ch, line);
+ break;
+
+ case 0:
+ case 1:
+ case 2: /* XXX */
+ snprintf(msg, sizeof(msg), "%s:%s", cmd_ch, expanded.in());
+ break;
+ }
+
+ if (sendstring(this->rc_command_pipe[RCF_SLAVE], msg, strlen(msg)) == -1) {
+ perror("line_ready: write failed");
+ _exit(1);
+ }
+
+ {
+ HIST_ENTRY* entry;
+
+ if (line[0] != '\0'
+ && (history_length == 0
+ || (entry = history_get(history_base + history_length - 1))
+ == nullptr
+ || strcmp(entry->line, line) != 0))
+ {
+ add_history(line);
+ }
+ }
+}
+
+void
+readline_curses::check_poll_set(const std::vector<struct pollfd>& pollfds)
+{
+ int rc;
+
+ if (pollfd_ready(pollfds, this->rc_pty[RCF_MASTER])) {
+ char buffer[128];
+
+ rc = read(this->rc_pty[RCF_MASTER], buffer, sizeof(buffer));
+ if (rc > 0) {
+ int old_x = this->vc_x;
+
+ this->map_output(buffer, rc);
+ if (this->vc_x != old_x) {
+ this->rc_change(this);
+ }
+ }
+ }
+ if (pollfd_ready(pollfds, this->rc_command_pipe[RCF_MASTER])) {
+ char msg[1024 + 1];
+
+ rc = recvstring(
+ this->rc_command_pipe[RCF_MASTER], msg, sizeof(msg) - 1);
+ if (rc >= 0) {
+ msg[rc] = '\0';
+ if (this->rc_matches_remaining > 0) {
+ this->rc_matches.emplace_back(msg);
+ this->rc_matches_remaining -= 1;
+ if (this->rc_matches_remaining == 0) {
+ this->rc_display_match(this);
+ }
+ } else if (msg[0] == 'm') {
+ if (sscanf(msg,
+ "m:%d:%d:%d",
+ &this->rc_match_start,
+ &this->rc_matches_remaining,
+ &this->rc_max_match_length)
+ != 3)
+ {
+ require(0);
+ }
+ this->rc_matches.clear();
+ if (this->rc_matches_remaining == 0) {
+ this->rc_display_match(this);
+ }
+ this->rc_match_index = 0;
+ } else if (msg[0] == '\t') {
+ char path[2048];
+
+ if (sscanf(msg, "\t:%s", path) != 1) {
+ require(0);
+ }
+ this->rc_remote_complete_path = path;
+ this->rc_completion_request(this);
+ } else if (msg[0] == 'n') {
+ if (sscanf(msg, "n:%d", &this->rc_match_index) != 1) {
+ require(0);
+ }
+ this->rc_display_next(this);
+ } else {
+ switch (msg[0]) {
+ case 't':
+ case 'd':
+ case 'D':
+ this->rc_value = std::string(&msg[2]);
+ break;
+ }
+ switch (msg[0]) {
+ case 'a':
+ curs_set(0);
+ this->vc_line.clear();
+ this->rc_active_context = -1;
+ this->rc_matches.clear();
+ this->rc_abort(this);
+ this->rc_display_match(this);
+ this->rc_blur(this);
+ break;
+
+ case 't':
+ this->rc_timeout(this);
+ break;
+
+ case 'd':
+ case 'D':
+ curs_set(0);
+ this->rc_active_context = -1;
+ this->rc_matches.clear();
+ if (msg[0] == 'D' || this->rc_is_alt_focus) {
+ this->rc_alt_perform(this);
+ } else {
+ this->rc_perform(this);
+ }
+ this->rc_display_match(this);
+ this->rc_blur(this);
+ break;
+
+ case 'l':
+ this->rc_line_buffer = &msg[2];
+ if (this->rc_active_context != -1) {
+ this->rc_change(this);
+ }
+ this->rc_matches.clear();
+ if (this->rc_active_context != -1) {
+ this->rc_display_match(this);
+ }
+ break;
+
+ case 'c':
+ this->rc_line_buffer = &msg[2];
+ this->rc_change(this);
+ this->rc_display_match(this);
+ break;
+ case 'w':
+ this->rc_ready_for_input = true;
+ break;
+ }
+ }
+ }
+ }
+}
+
+void
+readline_curses::handle_key(int ch)
+{
+ const char* bch;
+ int len;
+
+ bch = this->map_input(ch, len);
+ if (write(this->rc_pty[RCF_MASTER], bch, len) == -1) {
+ perror("handle_key: write failed");
+ }
+}
+
+void
+readline_curses::focus(int context,
+ const std::string& prompt,
+ const std::string& initial)
+{
+ char buffer[1024];
+
+ curs_set(1);
+
+ this->rc_active_context = context;
+
+ snprintf(buffer, sizeof(buffer), "f:%d:%s", context, prompt.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("focus: write failed");
+ }
+ wmove(this->vc_window, this->get_actual_y(), this->vc_left);
+ wclrtoeol(this->vc_window);
+ if (!initial.empty()) {
+ this->rewrite_line(initial.size(), initial);
+ }
+ this->rc_is_alt_focus = false;
+ this->rc_focus(this);
+}
+
+void
+readline_curses::rewrite_line(int pos, const std::string& value)
+{
+ char buffer[1024];
+
+ snprintf(buffer, sizeof(buffer), "i:%d:%s", pos, value.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("rewrite_line: write failed");
+ }
+}
+
+void
+readline_curses::abort()
+{
+ char buffer[1024];
+
+ this->vc_x = 0;
+ snprintf(buffer, sizeof(buffer), "a");
+ if (sendstring(this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer))
+ == -1)
+ {
+ perror("abort: write failed");
+ }
+}
+
+void
+readline_curses::add_prefix(int context,
+ const std::vector<std::string>& prefix,
+ const std::string& value)
+{
+ char buffer[1024];
+ auto prefix_wire = fmt::format(FMT_STRING("{}"), fmt::join(prefix, "\x1f"));
+
+ snprintf(buffer,
+ sizeof(buffer),
+ "apre:%d:%s\x1d%s",
+ context,
+ prefix_wire.c_str(),
+ value.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("add_prefix: write failed");
+ }
+}
+
+void
+readline_curses::clear_prefixes(int context)
+{
+ char buffer[1024];
+
+ snprintf(buffer, sizeof(buffer), "cpre:%d", context);
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("add_possibility: write failed");
+ }
+}
+
+void
+readline_curses::add_possibility(int context,
+ const std::string& type,
+ const std::string& value)
+{
+ char buffer[1024];
+
+ if (value.empty()) {
+ return;
+ }
+
+ snprintf(buffer,
+ sizeof(buffer),
+ "ap:%d:%s:%s",
+ context,
+ type.c_str(),
+ value.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("add_possibility: write failed");
+ }
+}
+
+void
+readline_curses::rem_possibility(int context,
+ const std::string& type,
+ const std::string& value)
+{
+ char buffer[1024];
+
+ snprintf(buffer,
+ sizeof(buffer),
+ "rp:%d:%s:%s",
+ context,
+ type.c_str(),
+ value.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("rem_possiblity: write failed");
+ }
+}
+
+void
+readline_curses::clear_possibilities(int context, std::string type)
+{
+ char buffer[1024];
+
+ snprintf(buffer, sizeof(buffer), "cp:%d:%s", context, type.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("clear_possiblity: write failed");
+ }
+}
+
+void
+readline_curses::do_update()
+{
+ if (!this->vc_visible || this->vc_window == nullptr) {
+ return;
+ }
+
+ auto actual_width = this->get_actual_width();
+ if (this->rc_active_context == -1) {
+ int alt_start = -1;
+ struct line_range lr(0, 0);
+ attr_line_t alt_al;
+ auto& vc = view_colors::singleton();
+
+ wmove(this->vc_window, this->get_actual_y(), this->vc_left);
+ auto attrs = vc.attrs_for_role(role_t::VCR_TEXT);
+ wattr_set(this->vc_window,
+ attrs.ta_attrs,
+ vc.ensure_color_pair(attrs.ta_fg_color, attrs.ta_bg_color),
+ nullptr);
+ whline(this->vc_window, ' ', actual_width);
+
+ if (time(nullptr) > this->rc_value_expiration) {
+ this->rc_value.clear();
+ }
+
+ if (!this->rc_alt_value.empty()) {
+ alt_al.get_string() = this->rc_alt_value;
+ scrub_ansi_string(alt_al.get_string(), &alt_al.get_attrs());
+
+ alt_start = getmaxx(this->vc_window) - alt_al.get_string().size();
+ }
+
+ if (alt_start >= (int) (this->rc_value.length() + 5)) {
+ lr.lr_end = alt_al.get_string().length();
+ view_curses::mvwattrline(
+ this->vc_window, this->get_actual_y(), alt_start, alt_al, lr);
+ }
+
+ lr.lr_end = this->rc_value.length();
+ view_curses::mvwattrline(this->vc_window,
+ this->get_actual_y(),
+ this->vc_left,
+ this->rc_value,
+ lr);
+ this->set_x(0);
+ }
+
+ if (this->rc_active_context != -1) {
+ readline_context* rc = this->rc_contexts[this->rc_active_context];
+ readline_highlighter_t hl = rc->get_highlighter();
+ attr_line_t al = this->vc_line;
+
+ if (hl != nullptr) {
+ hl(al, this->vc_left + this->vc_x);
+ }
+ view_curses::mvwattrline(this->vc_window,
+ this->get_actual_y(),
+ this->vc_left,
+ al,
+ line_range{0, (int) actual_width});
+
+ wmove(
+ this->vc_window, this->get_actual_y(), this->vc_left + this->vc_x);
+ }
+}
+
+std::string
+readline_curses::get_match_string() const
+{
+ auto len = std::min((size_t) this->vc_x, this->rc_line_buffer.size())
+ - this->rc_match_start;
+ auto* context = this->get_active_context();
+
+ if (context->get_append_character() != 0) {
+ if (this->rc_line_buffer.length() > (this->rc_match_start + len - 1)
+ && this->rc_line_buffer[this->rc_match_start + len - 1]
+ == context->get_append_character())
+ {
+ len -= 1;
+ } else if (this->rc_line_buffer.length()
+ > (this->rc_match_start + len - 2)
+ && this->rc_line_buffer[this->rc_match_start + len - 2]
+ == context->get_append_character())
+ {
+ len -= 2;
+ }
+ }
+
+ return this->rc_line_buffer.substr(this->rc_match_start, len);
+}
+
+void
+readline_curses::set_value(const std::string& value)
+{
+ this->set_attr_value(attr_line_t::from_ansi_str(value.c_str()));
+}
+
+void
+readline_curses::set_attr_value(const attr_line_t& value)
+{
+ this->rc_value = value;
+ if (this->rc_value.length() > 1024) {
+ this->rc_value = this->rc_value.subline(0, 1024);
+ }
+ this->rc_value_expiration = time(nullptr) + VALUE_EXPIRATION;
+ this->set_needs_update();
+}
+
+void
+readline_curses::update_poll_set(std::vector<struct pollfd>& pollfds)
+{
+ if (this->rc_pty[RCF_MASTER] != -1) {
+ pollfds.push_back((struct pollfd){this->rc_pty[RCF_MASTER], POLLIN, 0});
+ }
+ if (this->rc_command_pipe[RCF_MASTER] != -1) {
+ pollfds.push_back(
+ (struct pollfd){this->rc_command_pipe[RCF_MASTER], POLLIN, 0});
+ }
+}
+
+void
+readline_curses::window_change()
+{
+ struct winsize ws;
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) {
+ throw error(errno);
+ }
+ if (this->vc_width > 0) {
+ ws.ws_col = this->vc_width;
+ } else if (this->vc_width < 0) {
+ ws.ws_col -= this->vc_left;
+ ws.ws_col += this->vc_width;
+ }
+ if (ioctl(this->rc_pty[RCF_MASTER], TIOCSWINSZ, &ws) == -1) {
+ throw error(errno);
+ }
+ kill(this->rc_child, SIGWINCH);
+}
diff --git a/src/readline_curses.hh b/src/readline_curses.hh
new file mode 100644
index 0000000..55b2ff3
--- /dev/null
+++ b/src/readline_curses.hh
@@ -0,0 +1,344 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file readline_curses.hh
+ */
+
+#ifndef readline_curses_hh
+#define readline_curses_hh
+
+#include <cstdio>
+#include <exception>
+#include <functional>
+#include <map>
+#include <set>
+#include <stack>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <errno.h>
+#include <poll.h>
+#include <readline/history.h>
+#include <readline/readline.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+#include "base/auto_fd.hh"
+#include "base/enum_util.hh"
+#include "base/func_util.hh"
+#include "base/result.h"
+#include "help_text_formatter.hh"
+#include "log_format.hh"
+#include "pollable.hh"
+#include "readline_context.hh"
+#include "vt52_curses.hh"
+
+extern exec_context INIT_EXEC_CONTEXT;
+
+/**
+ * Adapter between readline and curses. The curses and readline libraries
+ * normally do not get along. So, we need to put readline in another process
+ * and present it with a vt52 interface that we then translate to curses. The
+ * vt52 translation is done by the parent class, vt52_curses, while this class
+ * takes care of the communication between the two processes.
+ */
+class readline_curses
+ : public vt52_curses
+ , public pollable {
+public:
+ using action = std::function<void(readline_curses*)>;
+
+ class error : public std::exception {
+ public:
+ error(int err) : e_err(err) {}
+
+ int e_err;
+ };
+
+ static const int KEY_TIMEOUT = 750 * 1000;
+
+ static const int VALUE_EXPIRATION = 20;
+
+ readline_curses(std::shared_ptr<pollable_supervisor>);
+ ~readline_curses() override;
+
+ using injectable = readline_curses(std::shared_ptr<pollable_supervisor>);
+
+ void add_context(int id, readline_context& rc)
+ {
+ this->rc_contexts[id] = &rc;
+ }
+
+ template<typename T,
+ typename... Args,
+ std::enable_if_t<std::is_enum<T>::value, bool> = true>
+ void add_context(T context, Args&... args)
+ {
+ this->add_context(lnav::enums::to_underlying(context), args...);
+ }
+
+ void set_focus_action(const action& va) { this->rc_focus = va; }
+
+ void set_change_action(const action& va) { this->rc_change = va; }
+
+ void set_perform_action(const action& va) { this->rc_perform = va; }
+
+ void set_alt_perform_action(const action& va) { this->rc_alt_perform = va; }
+
+ void set_timeout_action(const action& va) { this->rc_timeout = va; }
+
+ void set_abort_action(const action& va) { this->rc_abort = va; }
+
+ void set_display_match_action(const action& va)
+ {
+ this->rc_display_match = va;
+ }
+
+ void set_display_next_action(const action& va)
+ {
+ this->rc_display_next = va;
+ }
+
+ void set_blur_action(const action& va) { this->rc_blur = va; }
+
+ void set_completion_request_action(const action& va)
+ {
+ this->rc_completion_request = va;
+ }
+
+ void set_value(const std::string& value);
+
+ void set_attr_value(const attr_line_t& al);
+
+ void clear_value() { this->rc_value.clear(); }
+
+ const attr_line_t& get_value() const { return this->rc_value; }
+
+ std::string get_line_buffer() const { return this->rc_line_buffer; }
+
+ void set_alt_value(const std::string& value) { this->rc_alt_value = value; }
+
+ std::string get_alt_value() const { return this->rc_alt_value; }
+
+ void update_poll_set(std::vector<struct pollfd>& pollfds) override;
+
+ void handle_key(int ch);
+
+ void check_poll_set(const std::vector<struct pollfd>& pollfds) override;
+
+ void focus(int context,
+ const std::string& prompt,
+ const std::string& initial = "");
+
+ template<typename T,
+ typename... Args,
+ std::enable_if_t<std::is_enum<T>::value, bool> = true>
+ void focus(T context, const Args&... args)
+ {
+ this->focus(lnav::enums::to_underlying(context), args...);
+ }
+
+ void set_alt_focus(bool alt_focus) { this->rc_is_alt_focus = alt_focus; }
+
+ void rewrite_line(int pos, const std::string& value);
+
+ readline_context* get_active_context() const
+ {
+ require(this->rc_active_context != -1);
+
+ std::map<int, readline_context*>::const_iterator iter;
+ iter = this->rc_contexts.find(this->rc_active_context);
+ return iter->second;
+ }
+
+ void abort();
+
+ void start();
+
+ void do_update() override;
+
+ void window_change();
+
+ void line_ready(const char* line);
+
+ void add_prefix(int context,
+ const std::vector<std::string>& prefix,
+ const std::string& value);
+
+ void clear_prefixes(int context);
+
+ template<typename T,
+ typename... Args,
+ std::enable_if_t<std::is_enum<T>::value, bool> = true>
+ void add_prefix(T context, const Args&... args)
+ {
+ this->add_prefix(lnav::enums::to_underlying(context), args...);
+ }
+
+ template<typename T, std::enable_if_t<std::is_enum<T>::value, bool> = true>
+ void clear_prefixes(T context)
+ {
+ this->clear_prefixes(lnav::enums::to_underlying(context));
+ }
+
+ void add_possibility(int context,
+ const std::string& type,
+ const std::string& value);
+
+ void add_possibility(int context,
+ const std::string& type,
+ const char* values[])
+ {
+ for (int lpc = 0; values[lpc]; lpc++) {
+ this->add_possibility(context, type, values[lpc]);
+ }
+ }
+
+ void add_possibility(int context,
+ const std::string& type,
+ const char** first,
+ const char** last)
+ {
+ for (; first < last; first++) {
+ this->add_possibility(context, type, *first);
+ }
+ }
+
+ template<template<typename...> class Container>
+ void add_possibility(int context,
+ const std::string& type,
+ const Container<std::string>& values)
+ {
+ for (const auto& str : values) {
+ this->add_possibility(context, type, str);
+ }
+ }
+
+ void rem_possibility(int context,
+ const std::string& type,
+ const std::string& value);
+ void clear_possibilities(int context, std::string type);
+
+ template<typename T,
+ typename... Args,
+ std::enable_if_t<std::is_enum<T>::value, bool> = true>
+ void add_possibility(T context, Args... args)
+ {
+ this->add_possibility(lnav::enums::to_underlying(context), args...);
+ }
+
+ template<typename T,
+ typename... Args,
+ std::enable_if_t<std::is_enum<T>::value, bool> = true>
+ void rem_possibility(T context, const Args&... args)
+ {
+ this->rem_possibility(lnav::enums::to_underlying(context), args...);
+ }
+
+ template<typename T,
+ typename... Args,
+ std::enable_if_t<std::is_enum<T>::value, bool> = true>
+ void clear_possibilities(T context, Args... args)
+ {
+ this->clear_possibilities(lnav::enums::to_underlying(context), args...);
+ }
+
+ const std::vector<std::string>& get_matches() const
+ {
+ return this->rc_matches;
+ }
+
+ int get_match_start() const { return this->rc_match_start; }
+
+ std::string get_match_string() const;
+
+ int get_max_match_length() const { return this->rc_max_match_length; }
+
+ bool consume_ready_for_input()
+ {
+ auto retval = this->rc_ready_for_input;
+
+ this->rc_ready_for_input = false;
+ return retval;
+ }
+
+ std::string get_remote_complete_path() const
+ {
+ return this->rc_remote_complete_path;
+ }
+
+ void set_save_history(bool value) { this->rc_save_history = value; }
+
+private:
+ enum {
+ RCF_MASTER,
+ RCF_SLAVE,
+
+ RCF_MAX_VALUE,
+ };
+
+ static void store_matches(char** matches, int num_matches, int max_len);
+
+ friend class readline_context;
+
+ bool rc_save_history{true};
+ int rc_active_context{-1};
+ pid_t rc_child{-1};
+ auto_fd rc_pty[2];
+ auto_fd rc_command_pipe[2];
+ std::map<int, readline_context*> rc_contexts;
+ attr_line_t rc_value;
+ std::string rc_line_buffer;
+ time_t rc_value_expiration{0};
+ std::string rc_alt_value;
+ int rc_match_start{0};
+ int rc_matches_remaining{0};
+ int rc_max_match_length{0};
+ int rc_match_index{0};
+ std::vector<std::string> rc_matches;
+ bool rc_is_alt_focus{false};
+ bool rc_ready_for_input{false};
+ std::string rc_remote_complete_path;
+
+ action rc_focus;
+ action rc_change;
+ action rc_perform;
+ action rc_alt_perform;
+ action rc_timeout;
+ action rc_abort;
+ action rc_display_match;
+ action rc_display_next;
+ action rc_blur;
+ action rc_completion_request;
+};
+
+#endif
diff --git a/src/readline_highlighters.cc b/src/readline_highlighters.cc
new file mode 100644
index 0000000..58c537c
--- /dev/null
+++ b/src/readline_highlighters.cc
@@ -0,0 +1,480 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file readline_highlighters.cc
+ */
+
+#include "readline_highlighters.hh"
+
+#include "base/attr_line.builder.hh"
+#include "base/snippet_highlighters.hh"
+#include "base/string_util.hh"
+#include "config.h"
+#include "pcrepp/pcre2pp.hh"
+#include "shlex.hh"
+#include "sql_help.hh"
+#include "sql_util.hh"
+#include "view_curses.hh"
+
+static void readline_sqlite_highlighter_int(attr_line_t& al,
+ int x,
+ line_range sub);
+
+static bool
+is_bracket(const std::string& str, int index, bool is_lit)
+{
+ if (is_lit && str[index - 1] == '\\') {
+ return true;
+ }
+ if (!is_lit && str[index - 1] != '\\') {
+ return true;
+ }
+ return false;
+}
+
+static void
+find_matching_bracket(
+ attr_line_t& al, int x, line_range sub, char left, char right)
+{
+ bool is_lit = (left == 'Q');
+ attr_line_builder alb(al);
+ const auto& line = al.get_string();
+ int depth = 0;
+
+ if (x < sub.lr_start || x > sub.lr_end) {
+ return;
+ }
+
+ if (line[x] == right && is_bracket(line, x, is_lit)) {
+ for (int lpc = x - 1; lpc >= sub.lr_start; lpc--) {
+ if (line[lpc] == right && is_bracket(line, lpc, is_lit)) {
+ depth += 1;
+ } else if (line[lpc] == left && is_bracket(line, lpc, is_lit)) {
+ if (depth == 0) {
+ alb.overlay_attr_for_char(
+ lpc, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr_for_char(lpc,
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ }
+ depth -= 1;
+ }
+ }
+ }
+
+ if (line[x] == left && is_bracket(line, x, is_lit)) {
+ for (size_t lpc = x + 1; lpc < sub.lr_end; lpc++) {
+ if (line[lpc] == left && is_bracket(line, lpc, is_lit)) {
+ depth += 1;
+ } else if (line[lpc] == right && is_bracket(line, lpc, is_lit)) {
+ if (depth == 0) {
+ alb.overlay_attr_for_char(
+ lpc, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr_for_char(lpc,
+ VC_ROLE.value(role_t::VCR_OK));
+ break;
+ }
+ depth -= 1;
+ }
+ }
+ }
+
+ nonstd::optional<int> first_left;
+
+ depth = 0;
+
+ for (size_t lpc = sub.lr_start; lpc < sub.lr_end; lpc++) {
+ if (line[lpc] == left && is_bracket(line, lpc, is_lit)) {
+ depth += 1;
+ if (!first_left) {
+ first_left = lpc;
+ }
+ } else if (line[lpc] == right && is_bracket(line, lpc, is_lit)) {
+ if (depth > 0) {
+ depth -= 1;
+ } else {
+ auto lr = line_range(is_lit ? lpc - 1 : lpc, lpc + 1);
+ alb.overlay_attr(
+ lr, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_ERROR));
+ }
+ }
+ }
+
+ if (depth > 0) {
+ auto lr
+ = line_range(is_lit ? first_left.value() - 1 : first_left.value(),
+ first_left.value() + 1);
+ alb.overlay_attr(lr, VC_STYLE.value(text_attrs{A_BOLD | A_REVERSE}));
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_ERROR));
+ }
+}
+
+void
+readline_regex_highlighter(attr_line_t& al, int x)
+{
+ lnav::snippets::regex_highlighter(
+ al, x, line_range{1, (int) al.get_string().size()});
+}
+
+void
+readline_command_highlighter_int(attr_line_t& al, int x, line_range sub)
+{
+ static const auto RE_PREFIXES = lnav::pcre2pp::code::from_const(
+ R"(^:(filter-in|filter-out|delete-filter|enable-filter|disable-filter|highlight|clear-highlight|create-search-table\s+[^\s]+\s+))");
+ static const auto SH_PREFIXES = lnav::pcre2pp::code::from_const(
+ "^:(eval|open|append-to|write-to|write-csv-to|write-json-to)");
+ static const auto SQL_PREFIXES
+ = lnav::pcre2pp::code::from_const("^:(filter-expr|mark-expr)");
+ static const auto IDENT_PREFIXES
+ = lnav::pcre2pp::code::from_const("^:(tag|untag|delete-tags)");
+ static const auto COLOR_PREFIXES
+ = lnav::pcre2pp::code::from_const("^:(config)");
+ static const auto COLOR_RE = lnav::pcre2pp::code::from_const(
+ "(#(?:[a-fA-F0-9]{6}|[a-fA-F0-9]{3}))");
+
+ attr_line_builder alb(al);
+ const auto& line = al.get_string();
+ auto in_frag
+ = string_fragment::from_str_range(line, sub.lr_start, sub.lr_end);
+ size_t ws_index;
+
+ ws_index = line.find(' ', sub.lr_start);
+ auto command = line.substr(sub.lr_start, ws_index);
+ if (ws_index != std::string::npos) {
+ alb.overlay_attr(line_range(sub.lr_start + 1, ws_index),
+ VC_ROLE.value(role_t::VCR_KEYWORD));
+
+ if (RE_PREFIXES.find_in(in_frag).ignore_error()) {
+ lnav::snippets::regex_highlighter(
+ al, x, line_range{(int) ws_index, sub.lr_end});
+ }
+ if (SH_PREFIXES.find_in(in_frag).ignore_error()) {
+ readline_shlex_highlighter_int(
+ al, x, line_range{(int) ws_index, sub.lr_end});
+ }
+ if (SQL_PREFIXES.find_in(in_frag).ignore_error()) {
+ readline_sqlite_highlighter_int(
+ al, x, line_range{(int) ws_index, sub.lr_end});
+ }
+ }
+ if (COLOR_PREFIXES.find_in(in_frag).ignore_error()) {
+ COLOR_RE.capture_from(in_frag).for_each(
+ [&alb](lnav::pcre2pp::match_data& md) {
+ styling::color_unit::from_str(md[0].value())
+ .then([&](const auto& rgb_fg) {
+ auto color
+ = view_colors::singleton().match_color(rgb_fg);
+ alb.template overlay_attr(to_line_range(md[0].value()),
+ VC_STYLE.value(text_attrs{
+ A_BOLD,
+ color,
+ }));
+ });
+ });
+ }
+ if (IDENT_PREFIXES.find_in(in_frag).ignore_error()
+ && ws_index != std::string::npos)
+ {
+ size_t start = ws_index, last;
+
+ do {
+ for (; start < sub.length() && isspace(line[start]); start++)
+ ;
+ for (last = start; last < sub.length() && !isspace(line[last]);
+ last++)
+ ;
+ struct line_range lr {
+ (int) start, (int) last
+ };
+
+ if (lr.length() > 0 && !lr.contains(x) && !lr.contains(x - 1)) {
+ std::string value(lr.substr(line), lr.sublen(line));
+
+ if ((command == ":tag" || command == ":untag"
+ || command == ":delete-tags")
+ && !startswith(value, "#"))
+ {
+ value = "#" + value;
+ }
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_IDENTIFIER));
+ }
+
+ start = last;
+ } while (start < sub.length());
+ }
+}
+
+void
+readline_command_highlighter(attr_line_t& al, int x)
+{
+ readline_command_highlighter_int(
+ al, x, line_range{0, (int) al.get_string().length()});
+}
+
+static void
+readline_sqlite_highlighter_int(attr_line_t& al, int x, line_range sub)
+{
+ static const char* brackets[] = {
+ "[]",
+ "()",
+
+ nullptr,
+ };
+
+ attr_line_builder alb(al);
+ const auto& line = al.get_string();
+
+ auto anno_sql = al.subline(sub.lr_start, sub.length());
+ anno_sql.get_attrs().clear();
+ annotate_sql_statement(anno_sql);
+
+ for (const auto& attr : anno_sql.al_attrs) {
+ line_range lr{
+ sub.lr_start + attr.sa_range.lr_start,
+ sub.lr_start + attr.sa_range.lr_end,
+ };
+ if (attr.sa_type == &SQL_COMMAND_ATTR
+ || attr.sa_type == &SQL_KEYWORD_ATTR)
+ {
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_KEYWORD));
+ } else if (attr.sa_type == &SQL_IDENTIFIER_ATTR) {
+ if (!attr.sa_range.contains(x) && attr.sa_range.lr_end != x) {
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_IDENTIFIER));
+ }
+ } else if (attr.sa_type == &SQL_FUNCTION_ATTR) {
+ alb.overlay_attr(
+ line_range{lr.lr_start, (int) line.find('(', lr.lr_start)},
+ VC_ROLE.value(role_t::VCR_SYMBOL));
+ } else if (attr.sa_type == &SQL_NUMBER_ATTR) {
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_NUMBER));
+ } else if (attr.sa_type == &SQL_STRING_ATTR) {
+ if (lr.length() > 1 && al.al_string[lr.lr_end - 1] == '\'') {
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_STRING));
+ } else {
+ alb.overlay_attr_for_char(
+ lr.lr_start, VC_STYLE.value(text_attrs{A_REVERSE}));
+ alb.overlay_attr_for_char(lr.lr_start,
+ VC_ROLE.value(role_t::VCR_ERROR));
+ }
+ } else if (attr.sa_type == &SQL_OPERATOR_ATTR) {
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_SYMBOL));
+ } else if (attr.sa_type == &SQL_COMMENT_ATTR) {
+ alb.overlay_attr(lr, VC_ROLE.value(role_t::VCR_COMMENT));
+ }
+ }
+
+ for (int lpc = 0; brackets[lpc]; lpc++) {
+ find_matching_bracket(al, x, sub, brackets[lpc][0], brackets[lpc][1]);
+ }
+}
+
+void
+readline_sqlite_highlighter(attr_line_t& al, int x)
+{
+ readline_sqlite_highlighter_int(
+ al, x, line_range{0, (int) al.get_string().length()});
+}
+
+void
+readline_shlex_highlighter_int(attr_line_t& al, int x, line_range sub)
+{
+ attr_line_builder alb(al);
+ const auto& str = al.get_string();
+ string_fragment cap;
+ shlex_token_t token;
+ nonstd::optional<int> quote_start;
+ shlex lexer(string_fragment{al.al_string.data(), sub.lr_start, sub.lr_end});
+
+ while (lexer.tokenize(cap, token)) {
+ switch (token) {
+ case shlex_token_t::ST_ERROR:
+ alb.overlay_attr(line_range(sub.lr_start + cap.sf_begin,
+ sub.lr_start + cap.sf_end),
+ VC_STYLE.value(text_attrs{A_REVERSE}));
+ alb.overlay_attr(line_range(sub.lr_start + cap.sf_begin,
+ sub.lr_start + cap.sf_end),
+ VC_ROLE.value(role_t::VCR_ERROR));
+ break;
+ case shlex_token_t::ST_TILDE:
+ case shlex_token_t::ST_ESCAPE:
+ alb.overlay_attr(line_range(sub.lr_start + cap.sf_begin,
+ sub.lr_start + cap.sf_end),
+ VC_ROLE.value(role_t::VCR_SYMBOL));
+ break;
+ case shlex_token_t::ST_DOUBLE_QUOTE_START:
+ case shlex_token_t::ST_SINGLE_QUOTE_START:
+ quote_start = sub.lr_start + cap.sf_begin;
+ break;
+ case shlex_token_t::ST_DOUBLE_QUOTE_END:
+ case shlex_token_t::ST_SINGLE_QUOTE_END:
+ alb.overlay_attr(
+ line_range(quote_start.value(), sub.lr_start + cap.sf_end),
+ VC_ROLE.value(role_t::VCR_STRING));
+ quote_start = nonstd::nullopt;
+ break;
+ case shlex_token_t::ST_VARIABLE_REF:
+ case shlex_token_t::ST_QUOTED_VARIABLE_REF: {
+ int extra = token == shlex_token_t::ST_VARIABLE_REF ? 0 : 1;
+ auto ident = str.substr(sub.lr_start + cap.sf_begin + 1 + extra,
+ cap.length() - 1 - extra * 2);
+ alb.overlay_attr(
+ line_range(sub.lr_start + cap.sf_begin,
+ sub.lr_start + cap.sf_begin + 1 + extra),
+ VC_ROLE.value(role_t::VCR_SYMBOL));
+ alb.overlay_attr(
+ line_range(sub.lr_start + cap.sf_begin + 1 + extra,
+ sub.lr_start + cap.sf_end - extra),
+ VC_ROLE.value(
+ x == sub.lr_start + cap.sf_end
+ || (cap.sf_begin <= x && x < cap.sf_end)
+ ? role_t::VCR_SYMBOL
+ : role_t::VCR_IDENTIFIER));
+ if (extra) {
+ alb.overlay_attr_for_char(
+ sub.lr_start + cap.sf_end - 1,
+ VC_ROLE.value(role_t::VCR_SYMBOL));
+ }
+ break;
+ }
+ case shlex_token_t::ST_WHITESPACE:
+ break;
+ }
+ }
+
+ if (quote_start) {
+ alb.overlay_attr_for_char(quote_start.value(),
+ VC_ROLE.value(role_t::VCR_ERROR));
+ }
+}
+
+void
+readline_shlex_highlighter(attr_line_t& al, int x)
+{
+ readline_shlex_highlighter_int(
+ al, x, line_range{0, (int) al.al_string.length()});
+}
+
+static void
+readline_lnav_highlighter_int(attr_line_t& al, int x, line_range sub)
+{
+ switch (al.al_string[sub.lr_start]) {
+ case ':':
+ readline_command_highlighter_int(al, x, sub);
+ break;
+ case ';':
+ readline_sqlite_highlighter_int(al,
+ x,
+ line_range{
+ sub.lr_start + 1,
+ sub.lr_end,
+ });
+ break;
+ case '|':
+ break;
+ case '/':
+ lnav::snippets::regex_highlighter(al,
+ x,
+ line_range{
+ sub.lr_start + 1,
+ sub.lr_end,
+ });
+ break;
+ }
+}
+
+void
+readline_lnav_highlighter(attr_line_t& al, int x)
+{
+ static const auto COMMENT_RE = lnav::pcre2pp::code::from_const(R"(^\s*#)");
+
+ attr_line_builder alb(al);
+ size_t start = 0, lf_pos;
+ nonstd::optional<size_t> section_start;
+
+ while ((lf_pos = al.get_string().find('\n', start)) != std::string::npos) {
+ line_range line{(int) start, (int) lf_pos};
+
+ if (line.empty()) {
+ start = lf_pos + 1;
+ continue;
+ }
+
+ auto line_frag = string_fragment::from_str_range(
+ al.al_string, line.lr_start, line.lr_end);
+
+ auto find_res = COMMENT_RE.find_in(line_frag).ignore_error();
+ if (find_res.has_value()) {
+ if (section_start) {
+ readline_lnav_highlighter_int(al,
+ x,
+ line_range{
+ (int) section_start.value(),
+ line.lr_start,
+ });
+ section_start = nonstd::nullopt;
+ }
+ alb.overlay_attr(line_range{find_res->f_all.sf_begin, line.lr_end},
+ VC_ROLE.value(role_t::VCR_COMMENT));
+ } else {
+ switch (al.al_string[line.lr_start]) {
+ case ':':
+ case ';':
+ case '|':
+ case '/':
+ if (section_start) {
+ readline_lnav_highlighter_int(
+ al,
+ x,
+ line_range{
+ (int) section_start.value(),
+ line.lr_start,
+ });
+ }
+
+ section_start = line.lr_start;
+ break;
+ }
+ }
+
+ start = lf_pos;
+ }
+
+ if (start == 0) {
+ section_start = 0;
+ }
+
+ if (section_start) {
+ readline_lnav_highlighter_int(al,
+ x,
+ line_range{
+ (int) section_start.value(),
+ (int) al.al_string.length(),
+ });
+ }
+}
diff --git a/src/readline_highlighters.hh b/src/readline_highlighters.hh
new file mode 100644
index 0000000..e2b83dd
--- /dev/null
+++ b/src/readline_highlighters.hh
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file readline_highlighters.hh
+ */
+
+#ifndef readline_highlighters_hh
+#define readline_highlighters_hh
+
+#include "base/attr_line.hh"
+
+void readline_regex_highlighter(attr_line_t& line, int x);
+
+void readline_command_highlighter(attr_line_t& line, int x);
+
+void readline_sqlite_highlighter(attr_line_t& line, int x);
+
+void readline_shlex_highlighter_int(attr_line_t& al, int x, line_range sub);
+void readline_shlex_highlighter(attr_line_t& line, int x);
+
+void readline_lnav_highlighter(attr_line_t& line, int x);
+
+#endif
diff --git a/src/readline_possibilities.cc b/src/readline_possibilities.cc
new file mode 100644
index 0000000..f0b8f17
--- /dev/null
+++ b/src/readline_possibilities.cc
@@ -0,0 +1,510 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <regex>
+#include <string>
+
+#include "readline_possibilities.hh"
+
+#include "base/isc.hh"
+#include "base/opt_util.hh"
+#include "config.h"
+#include "data_parser.hh"
+#include "lnav.hh"
+#include "lnav_config.hh"
+#include "service_tags.hh"
+#include "session_data.hh"
+#include "sql_help.hh"
+#include "sql_util.hh"
+#include "sqlite-extension-func.hh"
+#include "sysclip.hh"
+#include "tailer/tailer.looper.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+static int
+handle_collation_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL, "*", colvalues[1]);
+ }
+
+ return 0;
+}
+
+static int
+handle_db_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL, "*", colvalues[1]);
+ }
+
+ return 0;
+}
+
+static int
+handle_table_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ if (lnav_data.ld_rl_view != nullptr) {
+ std::string table_name = colvalues[0];
+
+ if (sqlite_function_help.count(table_name) == 0) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL, "*", colvalues[0]);
+ }
+
+ lnav_data.ld_table_ddl[colvalues[0]] = colvalues[1];
+ }
+
+ return 0;
+}
+
+static int
+handle_table_info(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ if (lnav_data.ld_rl_view != nullptr) {
+ auto_mem<char, sqlite3_free> quoted_name;
+
+ quoted_name = sql_quote_ident(colvalues[1]);
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::SQL, "*", std::string(quoted_name));
+ }
+ if (strcmp(colvalues[5], "1") == 0) {
+ lnav_data.ld_db_key_names.emplace_back(colvalues[1]);
+ }
+ return 0;
+}
+
+static int
+handle_foreign_key_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ lnav_data.ld_db_key_names.emplace_back(colvalues[3]);
+ lnav_data.ld_db_key_names.emplace_back(colvalues[4]);
+ return 0;
+}
+
+struct sqlite_metadata_callbacks lnav_sql_meta_callbacks = {
+ handle_collation_list,
+ handle_db_list,
+ handle_table_list,
+ handle_table_info,
+ handle_foreign_key_list,
+};
+
+static void
+add_text_possibilities(readline_curses* rlc,
+ int context,
+ const std::string& type,
+ const std::string& str,
+ text_quoting tq)
+{
+ static const std::regex re_escape(R"(([.\^$*+?()\[\]{}\\|]))");
+ static const std::regex re_escape_no_dot(R"(([\^$*+?()\[\]{}\\|]))");
+
+ data_scanner ds(str);
+
+ while (true) {
+ auto tok_res = ds.tokenize2();
+
+ if (!tok_res) {
+ break;
+ }
+ if (tok_res->tr_capture.length() < 4) {
+ continue;
+ }
+
+ switch (tok_res->tr_token) {
+ case DT_DATE:
+ case DT_TIME:
+ case DT_WHITE:
+ continue;
+ default:
+ break;
+ }
+
+ switch (tq) {
+ case text_quoting::sql: {
+ auto token_value = tok_res->to_string();
+ auto_mem<char, sqlite3_free> quoted_token;
+
+ quoted_token = sqlite3_mprintf("%Q", token_value.c_str());
+ rlc->add_possibility(context, type, std::string(quoted_token));
+ break;
+ }
+ default: {
+ auto token_value_no_dot = tok_res->to_string();
+ auto token_value = std::regex_replace(
+ token_value_no_dot, re_escape, R"(\\\1)");
+ token_value_no_dot = std::regex_replace(
+ token_value_no_dot, re_escape_no_dot, R"(\\\1)");
+ rlc->add_possibility(context, type, token_value);
+ if (token_value != token_value_no_dot) {
+ rlc->add_possibility(context, type, token_value_no_dot);
+ }
+ break;
+ }
+ }
+
+ switch (tok_res->tr_token) {
+ case DT_QUOTED_STRING:
+ add_text_possibilities(
+ rlc,
+ context,
+ type,
+ ds.to_string_fragment(tok_res->tr_inner_capture)
+ .to_string(),
+ tq);
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+void
+add_view_text_possibilities(readline_curses* rlc,
+ int context,
+ const std::string& type,
+ textview_curses* tc,
+ text_quoting tq)
+{
+ text_sub_source* tss = tc->get_sub_source();
+
+ rlc->clear_possibilities(context, type);
+
+ if (tc->get_inner_height() > 0_vl) {
+ for (vis_line_t curr_line = tc->get_top();
+ curr_line <= tc->get_bottom();
+ ++curr_line)
+ {
+ std::string line;
+
+ tss->text_value_for_line(
+ *tc, curr_line, line, text_sub_source::RF_RAW);
+
+ add_text_possibilities(rlc, context, type, line, tq);
+ }
+ }
+
+ rlc->add_possibility(context, type, bookmark_metadata::KNOWN_TAGS);
+}
+
+void
+add_filter_expr_possibilities(readline_curses* rlc,
+ int context,
+ const std::string& type)
+{
+ static const char* BUILTIN_VARS[] = {
+ ":log_level",
+ ":log_time",
+ ":log_time_msecs",
+ ":log_mark",
+ ":log_comment",
+ ":log_tags",
+ ":log_opid",
+ ":log_format",
+ ":log_path",
+ ":log_unique_path",
+ ":log_text",
+ ":log_body",
+ ":log_raw_text",
+ };
+
+ auto* tc = &lnav_data.ld_views[LNV_LOG];
+ auto& lss = lnav_data.ld_log_source;
+ auto bottom = tc->get_bottom();
+
+ rlc->clear_possibilities(context, type);
+ rlc->add_possibility(
+ context, type, std::begin(BUILTIN_VARS), std::end(BUILTIN_VARS));
+ for (auto curr_line = tc->get_top(); curr_line < bottom; ++curr_line) {
+ auto cl = lss.at(curr_line);
+ auto lf = lss.find(cl);
+ auto ll = lf->begin() + cl;
+
+ if (!ll->is_message()) {
+ continue;
+ }
+
+ auto format = lf->get_format();
+ string_attrs_t sa;
+ logline_value_vector values;
+
+ lf->read_full_message(ll, values.lvv_sbr);
+ values.lvv_sbr.erase_ansi();
+ format->annotate(cl, sa, values);
+ for (auto& lv : values.lvv_values) {
+ if (!lv.lv_meta.lvm_struct_name.empty()) {
+ continue;
+ }
+
+ auto_mem<char> ident(sqlite3_free);
+
+ ident = sql_quote_ident(lv.lv_meta.lvm_name.get());
+ auto bound_name = fmt::format(FMT_STRING(":{}"), ident.in());
+ rlc->add_possibility(context, type, bound_name);
+ switch (lv.lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_BOOLEAN:
+ case value_kind_t::VALUE_FLOAT:
+ case value_kind_t::VALUE_NULL:
+ break;
+ case value_kind_t::VALUE_INTEGER:
+ rlc->add_possibility(
+ context, type, std::to_string(lv.lv_value.i));
+ break;
+ default: {
+ auto_mem<char, sqlite3_free> str;
+
+ str = sqlite3_mprintf(
+ "%.*Q", lv.text_length(), lv.text_value());
+ rlc->add_possibility(context, type, std::string(str.in()));
+ break;
+ }
+ }
+ }
+ }
+ rlc->add_possibility(
+ context, type, std::begin(sql_keywords), std::end(sql_keywords));
+ rlc->add_possibility(context, type, sql_function_names);
+ for (int lpc = 0; sqlite_registration_funcs[lpc]; lpc++) {
+ struct FuncDef* basic_funcs;
+ struct FuncDefAgg* agg_funcs;
+
+ sqlite_registration_funcs[lpc](&basic_funcs, &agg_funcs);
+ for (int lpc2 = 0; basic_funcs && basic_funcs[lpc2].zName; lpc2++) {
+ const FuncDef& func_def = basic_funcs[lpc2];
+
+ rlc->add_possibility(
+ context,
+ type,
+ std::string(func_def.zName) + (func_def.nArg ? "(" : "()"));
+ }
+ for (int lpc2 = 0; agg_funcs && agg_funcs[lpc2].zName; lpc2++) {
+ const FuncDefAgg& func_def = agg_funcs[lpc2];
+
+ rlc->add_possibility(
+ context,
+ type,
+ std::string(func_def.zName) + (func_def.nArg ? "(" : "()"));
+ }
+ }
+}
+
+void
+add_env_possibilities(ln_mode_t context)
+{
+ extern char** environ;
+ readline_curses* rlc = lnav_data.ld_rl_view;
+
+ for (char** var = environ; *var != nullptr; var++) {
+ rlc->add_possibility(
+ context, "*", "$" + std::string(*var, strchr(*var, '=')));
+ }
+
+ exec_context& ec = lnav_data.ld_exec_context;
+
+ if (!ec.ec_local_vars.empty()) {
+ for (const auto& iter : ec.ec_local_vars.top()) {
+ rlc->add_possibility(context, "*", "$" + iter.first);
+ }
+ }
+
+ for (const auto& iter : ec.ec_global_vars) {
+ rlc->add_possibility(context, "*", "$" + iter.first);
+ }
+
+ if (lnav_data.ld_window) {
+ rlc->add_possibility(context, "*", "$LINES");
+ rlc->add_possibility(context, "*", "$COLS");
+ }
+}
+
+void
+add_filter_possibilities(textview_curses* tc)
+{
+ readline_curses* rc = lnav_data.ld_rl_view;
+ text_sub_source* tss = tc->get_sub_source();
+ filter_stack& fs = tss->get_filters();
+
+ rc->clear_possibilities(ln_mode_t::COMMAND, "all-filters");
+ rc->clear_possibilities(ln_mode_t::COMMAND, "disabled-filter");
+ rc->clear_possibilities(ln_mode_t::COMMAND, "enabled-filter");
+ for (const auto& tf : fs) {
+ rc->add_possibility(ln_mode_t::COMMAND, "all-filters", tf->get_id());
+ if (tf->is_enabled()) {
+ rc->add_possibility(
+ ln_mode_t::COMMAND, "enabled-filter", tf->get_id());
+ } else {
+ rc->add_possibility(
+ ln_mode_t::COMMAND, "disabled-filter", tf->get_id());
+ }
+ }
+}
+
+void
+add_file_possibilities()
+{
+ static const std::regex sh_escape(R"(([\s\'\"]+))");
+
+ readline_curses* rc = lnav_data.ld_rl_view;
+
+ rc->clear_possibilities(ln_mode_t::COMMAND, "visible-files");
+ rc->clear_possibilities(ln_mode_t::COMMAND, "hidden-files");
+ for (const auto& lf : lnav_data.ld_active_files.fc_files) {
+ if (lf.get() == nullptr) {
+ continue;
+ }
+
+ lnav_data.ld_log_source.find_data(lf) | [&lf, rc](auto ld) {
+ auto escaped_fn
+ = std::regex_replace(lf->get_filename(), sh_escape, R"(\\\1)");
+
+ rc->add_possibility(
+ ln_mode_t::COMMAND,
+ ld->is_visible() ? "visible-files" : "hidden-files",
+ escaped_fn);
+ };
+ }
+}
+
+void
+add_mark_possibilities()
+{
+ readline_curses* rc = lnav_data.ld_rl_view;
+
+ rc->clear_possibilities(ln_mode_t::COMMAND, "mark-type");
+ for (auto iter = bookmark_type_t::type_begin();
+ iter != bookmark_type_t::type_end();
+ ++iter)
+ {
+ bookmark_type_t* bt = (*iter);
+
+ if (bt->get_name().empty()) {
+ continue;
+ }
+ rc->add_possibility(ln_mode_t::COMMAND, "mark-type", bt->get_name());
+ }
+}
+
+void
+add_config_possibilities()
+{
+ readline_curses* rc = lnav_data.ld_rl_view;
+ std::set<std::string> visited;
+ auto cb = [rc, &visited](const json_path_handler_base& jph,
+ const std::string& path,
+ void* mem) {
+ if (jph.jph_children) {
+ const auto named_caps = jph.jph_regex->get_named_captures();
+
+ if (named_caps.empty()) {
+ rc->add_possibility(ln_mode_t::COMMAND, "config-option", path);
+ }
+ for (const auto& named_cap : named_caps) {
+ if (visited.count(named_cap.get_name().to_string()) == 0) {
+ rc->clear_possibilities(ln_mode_t::COMMAND,
+ named_cap.get_name().to_string());
+ visited.insert(named_cap.get_name().to_string());
+ }
+
+ ghc::filesystem::path path_obj(path);
+ rc->add_possibility(ln_mode_t::COMMAND,
+ named_cap.get_name().to_string(),
+ path_obj.parent_path().filename().string());
+ }
+ } else {
+ rc->add_possibility(ln_mode_t::COMMAND, "config-option", path);
+ if (jph.jph_synopsis) {
+ if (jph.jph_enum_values) {
+ rc->add_prefix(ln_mode_t::COMMAND,
+ std::vector<std::string>{"config", path},
+ path);
+ for (size_t lpc = 0;
+ jph.jph_enum_values[lpc].first != nullptr;
+ lpc++)
+ {
+ rc->add_possibility(ln_mode_t::COMMAND,
+ path,
+ jph.jph_enum_values[lpc].first);
+ }
+ } else {
+ rc->add_prefix(ln_mode_t::COMMAND,
+ std::vector<std::string>{"config", path},
+ jph.jph_synopsis);
+ }
+ }
+ }
+ };
+
+ rc->clear_possibilities(ln_mode_t::COMMAND, "config-option");
+ for (const auto& jph : lnav_config_handlers.jpc_children) {
+ jph.walk(cb, &lnav_config);
+ }
+}
+
+void
+add_tag_possibilities()
+{
+ readline_curses* rc = lnav_data.ld_rl_view;
+
+ rc->clear_possibilities(ln_mode_t::COMMAND, "tag");
+ rc->clear_possibilities(ln_mode_t::COMMAND, "line-tags");
+ rc->add_possibility(
+ ln_mode_t::COMMAND, "tag", bookmark_metadata::KNOWN_TAGS);
+ if (lnav_data.ld_view_stack.top().value_or(nullptr)
+ == &lnav_data.ld_views[LNV_LOG])
+ {
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ if (lss.text_line_count() > 0) {
+ auto line_meta_opt = lss.find_bookmark_metadata(
+ lnav_data.ld_views[LNV_LOG].get_top());
+ if (line_meta_opt) {
+ rc->add_possibility(ln_mode_t::COMMAND,
+ "line-tags",
+ line_meta_opt.value()->bm_tags);
+ }
+ }
+ }
+}
+
+void
+add_recent_netlocs_possibilities()
+{
+ readline_curses* rc = lnav_data.ld_rl_view;
+
+ rc->clear_possibilities(ln_mode_t::COMMAND, "recent-netlocs");
+ std::set<std::string> netlocs;
+
+ isc::to<tailer::looper&, services::remote_tailer_t>().send_and_wait(
+ [&netlocs](auto& tlooper) { netlocs = tlooper.active_netlocs(); });
+ netlocs.insert(session_data.sd_recent_netlocs.begin(),
+ session_data.sd_recent_netlocs.end());
+ rc->add_possibility(ln_mode_t::COMMAND, "recent-netlocs", netlocs);
+}
diff --git a/src/readline_possibilities.hh b/src/readline_possibilities.hh
new file mode 100644
index 0000000..df5255b
--- /dev/null
+++ b/src/readline_possibilities.hh
@@ -0,0 +1,85 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LNAV_READLINE_POSSIBILITIES_H
+#define LNAV_READLINE_POSSIBILITIES_H
+
+#include <string>
+
+#include "readline_curses.hh"
+#include "textview_curses.hh"
+#include "view_helpers.hh"
+
+enum class text_quoting {
+ none,
+ sql,
+ regex,
+};
+
+void add_view_text_possibilities(readline_curses* rlc,
+ int context,
+ const std::string& type,
+ textview_curses* tc,
+ text_quoting tq);
+
+template<typename T,
+ typename... Args,
+ std::enable_if_t<std::is_enum<T>::value, bool> = true>
+void
+add_view_text_possibilities(readline_curses* rlc, T context, Args... args)
+{
+ add_view_text_possibilities(
+ rlc, lnav::enums::to_underlying(context), args...);
+}
+
+void add_filter_expr_possibilities(readline_curses* rlc,
+ int context,
+ const std::string& type);
+
+template<typename T,
+ typename... Args,
+ std::enable_if_t<std::is_enum<T>::value, bool> = true>
+void
+add_filter_expr_possibilities(readline_curses* rlc, T context, Args... args)
+{
+ add_filter_expr_possibilities(
+ rlc, lnav::enums::to_underlying(context), args...);
+}
+
+void add_env_possibilities(ln_mode_t context);
+void add_filter_possibilities(textview_curses* tc);
+void add_mark_possibilities();
+void add_config_possibilities();
+void add_tag_possibilities();
+void add_file_possibilities();
+void add_recent_netlocs_possibilities();
+
+extern struct sqlite_metadata_callbacks lnav_sql_meta_callbacks;
+
+#endif // LNAV_READLINE_POSSIBILITIES_H
diff --git a/src/regex101.client.cc b/src/regex101.client.cc
new file mode 100644
index 0000000..73871cc
--- /dev/null
+++ b/src/regex101.client.cc
@@ -0,0 +1,331 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "regex101.client.hh"
+
+#include <curl/curl.h>
+
+#include "config.h"
+#include "curl_looper.hh"
+#include "ghc/filesystem.hpp"
+#include "yajlpp/yajlpp_def.hh"
+
+namespace regex101 {
+namespace client {
+
+static const json_path_handler_base::enum_value_t CRITERIA_ENUM[] = {
+ {"DOES_MATCH", unit_test::criteria::DOES_MATCH},
+ {"DOES_NOT_MATCH", unit_test::criteria::DOES_NOT_MATCH},
+
+ json_path_handler_base::ENUM_TERMINATOR,
+};
+
+static const json_path_container UNIT_TEST_HANDLERS = {
+ yajlpp::property_handler("description")
+ .for_field(&unit_test::ut_description),
+ yajlpp::property_handler("testString")
+ .for_field(&unit_test::ut_test_string),
+ yajlpp::property_handler("target").for_field(&unit_test::ut_target),
+ yajlpp::property_handler("criteria")
+ .with_enum_values(CRITERIA_ENUM)
+ .for_field(&unit_test::ut_criteria),
+};
+
+static const typed_json_path_container<entry> ENTRY_HANDLERS = {
+ yajlpp::property_handler("regex").for_field(&entry::e_regex),
+ yajlpp::property_handler("testString").for_field(&entry::e_test_string),
+ yajlpp::property_handler("flags").for_field(&entry::e_flags),
+ yajlpp::property_handler("delimiter").for_field(&entry::e_delimiter),
+ yajlpp::property_handler("flavor").for_field(&entry::e_flavor),
+ yajlpp::property_handler("unitTests#")
+ .for_field(&entry::e_unit_tests)
+ .with_children(UNIT_TEST_HANDLERS),
+ yajlpp::property_handler("permalinkFragment")
+ .for_field(&entry::e_permalink_fragment),
+};
+
+static const typed_json_path_container<upsert_response> RESPONSE_HANDLERS = {
+ yajlpp::property_handler("deleteCode")
+ .for_field(&upsert_response::cr_delete_code),
+ yajlpp::property_handler("permalinkFragment")
+ .for_field(&upsert_response::cr_permalink_fragment),
+ yajlpp::property_handler("version").for_field(&upsert_response::cr_version),
+};
+
+static const ghc::filesystem::path REGEX101_BASE_URL
+ = "https://regex101.com/api/regex";
+static const char* USER_AGENT = "lnav/" PACKAGE_VERSION;
+
+Result<upsert_response, lnav::console::user_message>
+upsert(entry& en)
+{
+ auto entry_json = ENTRY_HANDLERS.to_string(en);
+
+ curl_request cr(REGEX101_BASE_URL.string());
+
+ curl_easy_setopt(cr, CURLOPT_URL, cr.get_name().c_str());
+ curl_easy_setopt(cr, CURLOPT_POST, 1);
+ curl_easy_setopt(cr, CURLOPT_POSTFIELDS, entry_json.c_str());
+ curl_easy_setopt(cr, CURLOPT_POSTFIELDSIZE, entry_json.size());
+ curl_easy_setopt(cr, CURLOPT_USERAGENT, USER_AGENT);
+
+ auto_mem<curl_slist> list(curl_slist_free_all);
+
+ list = curl_slist_append(list, "Content-Type: application/json");
+
+ curl_easy_setopt(cr, CURLOPT_HTTPHEADER, list.in());
+
+ auto perform_res = cr.perform();
+ if (perform_res.isErr()) {
+ return Err(
+ lnav::console::user_message::error(
+ "unable to create entry on regex101.com")
+ .with_reason(curl_easy_strerror(perform_res.unwrapErr())));
+ }
+
+ auto response = perform_res.unwrap();
+ auto resp_code = cr.get_response_code();
+ if (resp_code != 200) {
+ return Err(lnav::console::user_message::error(
+ "unable to create entry on regex101.com")
+ .with_reason(attr_line_t()
+ .append("received response code ")
+ .append(lnav::roles::number(
+ fmt::to_string(resp_code)))
+ .append(" content ")
+ .append_quoted(response)));
+ }
+
+ auto parse_res
+ = RESPONSE_HANDLERS.parser_for(intern_string::lookup(cr.get_name()))
+ .of(response);
+ if (parse_res.isOk()) {
+ return Ok(parse_res.unwrap());
+ }
+
+ auto errors = parse_res.unwrapErr();
+ return Err(lnav::console::user_message::error(
+ "unable to create entry on regex101.com")
+ .with_reason(errors[0].to_attr_line({})));
+}
+
+struct retrieve_entity {
+ std::string re_permalink_fragment;
+ int32_t re_versions{1};
+};
+
+static const typed_json_path_container<retrieve_entity> RETRIEVE_ENTITY_HANDLERS
+ = {
+ yajlpp::property_handler("permalinkFragment")
+ .for_field(&retrieve_entity::re_permalink_fragment),
+ yajlpp::property_handler("versions")
+ .for_field(&retrieve_entity::re_versions),
+};
+
+retrieve_result_t
+retrieve(const std::string& permalink)
+{
+ auto entry_url = REGEX101_BASE_URL / permalink;
+ curl_request entry_req(entry_url.string());
+
+ curl_easy_setopt(entry_req, CURLOPT_URL, entry_req.get_name().c_str());
+ curl_easy_setopt(entry_req, CURLOPT_USERAGENT, USER_AGENT);
+
+ auto perform_res = entry_req.perform();
+ if (perform_res.isErr()) {
+ return lnav::console::user_message::error(
+ attr_line_t("unable to get entry ")
+ .append_quoted(lnav::roles::symbol(permalink))
+ .append(" on regex101.com"))
+ .with_reason(curl_easy_strerror(perform_res.unwrapErr()));
+ }
+
+ auto response = perform_res.unwrap();
+ auto resp_code = entry_req.get_response_code();
+ if (resp_code == 404) {
+ return no_entry{};
+ }
+ if (resp_code != 200) {
+ return lnav::console::user_message::error(
+ attr_line_t("unable to get entry ")
+ .append_quoted(lnav::roles::symbol(permalink))
+ .append(" on regex101.com"))
+ .with_reason(
+ attr_line_t()
+ .append("received response code ")
+ .append(lnav::roles::number(fmt::to_string(resp_code)))
+ .append(" content ")
+ .append_quoted(response));
+ }
+
+ auto parse_res
+ = RETRIEVE_ENTITY_HANDLERS
+ .parser_for(intern_string::lookup(entry_req.get_name()))
+ .of(response);
+
+ if (parse_res.isErr()) {
+ auto parse_errors = parse_res.unwrapErr();
+
+ return lnav::console::user_message::error(
+ attr_line_t("unable to get entry ")
+ .append_quoted(lnav::roles::symbol(permalink))
+ .append(" on regex101.com"))
+ .with_reason(parse_errors[0].to_attr_line({}));
+ }
+
+ auto entry_value = parse_res.unwrap();
+
+ if (entry_value.re_versions == 0) {
+ return no_entry{};
+ }
+
+ auto version_url = entry_url / fmt::to_string(entry_value.re_versions);
+ curl_request version_req(version_url.string());
+
+ curl_easy_setopt(version_req, CURLOPT_URL, version_req.get_name().c_str());
+ curl_easy_setopt(version_req, CURLOPT_USERAGENT, USER_AGENT);
+
+ auto version_perform_res = version_req.perform();
+ if (version_perform_res.isErr()) {
+ return lnav::console::user_message::error(
+ attr_line_t("unable to get entry version ")
+ .append_quoted(lnav::roles::symbol(version_url.string()))
+ .append(" on regex101.com"))
+ .with_reason(curl_easy_strerror(version_perform_res.unwrapErr()));
+ }
+
+ auto version_response = version_perform_res.unwrap();
+ auto version_parse_res
+ = ENTRY_HANDLERS
+ .parser_for(intern_string::lookup(version_req.get_name()))
+ .of(version_response);
+
+ if (version_parse_res.isErr()) {
+ auto parse_errors = version_parse_res.unwrapErr();
+ return lnav::console::user_message::error(
+ attr_line_t("unable to get entry version ")
+ .append_quoted(lnav::roles::symbol(version_url.string()))
+ .append(" on regex101.com"))
+ .with_reason(parse_errors[0].to_attr_line({}));
+ }
+
+ auto retval = version_parse_res.unwrap();
+
+ retval.e_permalink_fragment = permalink;
+
+ return retval;
+}
+
+struct delete_entity {
+ std::string de_delete_code;
+};
+
+static const typed_json_path_container<delete_entity> DELETE_ENTITY_HANDLERS = {
+ yajlpp::property_handler("deleteCode")
+ .for_field(&delete_entity::de_delete_code),
+};
+
+Result<void, lnav::console::user_message>
+delete_entry(const std::string& delete_code)
+{
+ curl_request cr(REGEX101_BASE_URL.string());
+ delete_entity entity{delete_code};
+ auto entity_json = DELETE_ENTITY_HANDLERS.to_string(entity);
+
+ curl_easy_setopt(cr, CURLOPT_URL, cr.get_name().c_str());
+ curl_easy_setopt(cr, CURLOPT_CUSTOMREQUEST, "DELETE");
+ curl_easy_setopt(cr, CURLOPT_USERAGENT, USER_AGENT);
+ curl_easy_setopt(cr, CURLOPT_POSTFIELDS, entity_json.c_str());
+ curl_easy_setopt(cr, CURLOPT_POSTFIELDSIZE, entity_json.size());
+
+ auto_mem<curl_slist> list(curl_slist_free_all);
+
+ list = curl_slist_append(list, "Content-Type: application/json");
+
+ curl_easy_setopt(cr, CURLOPT_HTTPHEADER, list.in());
+
+ auto perform_res = cr.perform();
+ if (perform_res.isErr()) {
+ return Err(
+ lnav::console::user_message::error(
+ "unable to delete entry on regex101.com")
+ .with_reason(curl_easy_strerror(perform_res.unwrapErr())));
+ }
+
+ auto response = perform_res.unwrap();
+ auto resp_code = cr.get_response_code();
+ if (resp_code != 200) {
+ return Err(lnav::console::user_message::error(
+ "unable to delete entry on regex101.com")
+ .with_reason(attr_line_t()
+ .append("received response code ")
+ .append(lnav::roles::number(
+ fmt::to_string(resp_code)))
+ .append(" content ")
+ .append_quoted(response)));
+ }
+
+ return Ok();
+}
+
+std::string
+to_edit_url(const std::string& permalink)
+{
+ return fmt::format(FMT_STRING("https://regex101.com/r/{}"), permalink);
+}
+
+bool
+unit_test::operator==(const unit_test& rhs) const
+{
+ return ut_description == rhs.ut_description
+ && ut_test_string == rhs.ut_test_string && ut_target == rhs.ut_target
+ && ut_criteria == rhs.ut_criteria;
+}
+
+bool
+unit_test::operator!=(const unit_test& rhs) const
+{
+ return !(rhs == *this);
+}
+
+bool
+entry::operator==(const entry& rhs) const
+{
+ return e_regex == rhs.e_regex && e_test_string == rhs.e_test_string
+ && e_flavor == rhs.e_flavor && e_unit_tests == rhs.e_unit_tests;
+}
+
+bool
+entry::operator!=(const entry& rhs) const
+{
+ return !(rhs == *this);
+}
+
+} // namespace client
+} // namespace regex101
diff --git a/src/regex101.client.hh b/src/regex101.client.hh
new file mode 100644
index 0000000..14c1ac9
--- /dev/null
+++ b/src/regex101.client.hh
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef regex101_client_hh
+#define regex101_client_hh
+
+#include <string>
+#include <vector>
+
+#include "base/lnav.console.hh"
+#include "base/result.h"
+#include "mapbox/variant.hpp"
+
+namespace regex101 {
+namespace client {
+
+struct unit_test {
+ enum class criteria {
+ DOES_MATCH,
+ DOES_NOT_MATCH,
+ };
+
+ std::string ut_description;
+ std::string ut_test_string;
+ std::string ut_target{"REGEX"};
+ criteria ut_criteria{criteria::DOES_MATCH};
+
+ bool operator==(const unit_test& rhs) const;
+ bool operator!=(const unit_test& rhs) const;
+};
+
+struct entry {
+ std::string e_regex;
+ std::string e_test_string;
+ std::string e_flags{"gs"};
+ std::string e_delimiter{"/"};
+ std::string e_flavor{"pcre"};
+ std::vector<unit_test> e_unit_tests;
+ nonstd::optional<std::string> e_permalink_fragment;
+
+ bool operator==(const entry& rhs) const;
+ bool operator!=(const entry& rhs) const;
+};
+
+struct upsert_response {
+ std::string cr_delete_code;
+ std::string cr_permalink_fragment;
+ int32_t cr_version;
+};
+
+Result<upsert_response, lnav::console::user_message> upsert(entry& en);
+
+struct no_entry {};
+
+using retrieve_result_t
+ = mapbox::util::variant<entry, no_entry, lnav::console::user_message>;
+
+retrieve_result_t retrieve(const std::string& permalink);
+
+Result<void, lnav::console::user_message> delete_entry(
+ const std::string& delete_code);
+
+std::string to_edit_url(const std::string& permalink);
+
+} // namespace client
+} // namespace regex101
+
+#endif
diff --git a/src/regex101.import.cc b/src/regex101.import.cc
new file mode 100644
index 0000000..c0e2984
--- /dev/null
+++ b/src/regex101.import.cc
@@ -0,0 +1,395 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "regex101.import.hh"
+
+#include "base/fs_util.hh"
+#include "base/itertools.hh"
+#include "base/paths.hh"
+#include "lnav_config.hh"
+#include "log_format.hh"
+#include "log_format_ext.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "regex101.client.hh"
+#include "session_data.hh"
+#include "yajlpp/yajlpp.hh"
+
+using namespace lnav::roles::literals;
+
+static const std::set<std::string> SUPPORTED_FLAVORS = {
+ "pcre",
+ "pcre2",
+};
+
+Result<ghc::filesystem::path, lnav::console::user_message>
+regex101::import(const std::string& url,
+ const std::string& name,
+ const std::string& pat_name)
+{
+ static const auto USER_URL = lnav::pcre2pp::code::from_const(
+ R"(^https://regex101.com/r/(\w+)(?:/(\d+))?)");
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+ static const auto NAME_RE = lnav::pcre2pp::code::from_const(R"(^\w+$)");
+
+ if (url.empty()) {
+ return Err(lnav::console::user_message::error(
+ "expecting a regex101.com URL to import"));
+ }
+ if (name.empty()) {
+ return Err(lnav::console::user_message::error(
+ "expecting a name for the new format"));
+ }
+
+ auto lformat = log_format::find_root_format(name.c_str());
+ bool existing_format = false;
+
+ if (lformat != nullptr) {
+ auto* ext_format = dynamic_cast<external_log_format*>(lformat.get());
+
+ if (ext_format) {
+ auto found = ext_format->elf_pattern_order
+ | lnav::itertools::find_if([&pat_name](const auto& elem) {
+ return elem->p_name == pat_name;
+ });
+ if (!found) {
+ existing_format = true;
+ }
+ }
+ }
+
+ auto name_find_res = NAME_RE.find_in(name).ignore_error();
+ if (!name_find_res) {
+ auto partial_len = NAME_RE.match_partial(name);
+ return Err(
+ lnav::console::user_message::error(
+ attr_line_t("unable to import: ")
+ .append(lnav::roles::file(url)))
+ .with_reason(attr_line_t("expecting a format name that matches "
+ "the regular expression ")
+ .append_quoted(NAME_RE.get_pattern()))
+ .with_note(attr_line_t(" ")
+ .append_quoted(name)
+ .append("\n ")
+ .append(partial_len, ' ')
+ .append("^ matched up to here"_comment)));
+ }
+
+ auto user_find_res
+ = USER_URL.capture_from(url).into(md).matches().ignore_error();
+ if (!user_find_res) {
+ auto partial_len = USER_URL.match_partial(url);
+ return Err(lnav::console::user_message::error(
+ attr_line_t("unrecognized regex101.com URL: ")
+ .append(lnav::roles::file(url)))
+ .with_reason(attr_line_t("expecting a URL that matches ")
+ .append_quoted(USER_URL.get_pattern()))
+ .with_note(attr_line_t(" ")
+ .append_quoted(url)
+ .append("\n ")
+ .append(partial_len, ' ')
+ .append("^ matched up to here"_comment)));
+ }
+
+ auto permalink = md[1]->to_string();
+
+ auto format_filename = existing_format
+ ? fmt::format(FMT_STRING("{}.regex101-{}.json"), name, permalink)
+ : fmt::format(FMT_STRING("{}.json"), name);
+ auto format_path
+ = lnav::paths::dotlnav() / "formats" / "installed" / format_filename;
+
+ if (ghc::filesystem::exists(format_path)) {
+ return Err(lnav::console::user_message::error(
+ attr_line_t("unable to import: ")
+ .append(lnav::roles::file(url)))
+ .with_reason(
+ attr_line_t("format file already exists: ")
+ .append(lnav::roles::file(format_path.string())))
+ .with_help("delete the existing file to continue"));
+ }
+
+ auto retrieve_res = regex101::client::retrieve(permalink);
+ if (retrieve_res.is<lnav::console::user_message>()) {
+ return Err(retrieve_res.get<lnav::console::user_message>());
+ }
+
+ if (retrieve_res.is<regex101::client::no_entry>()) {
+ return Err(lnav::console::user_message::error(
+ attr_line_t("unknown regex101.com entry: ")
+ .append(lnav::roles::symbol(url))));
+ }
+
+ auto entry = retrieve_res.get<regex101::client::entry>();
+
+ if (SUPPORTED_FLAVORS.count(entry.e_flavor) == 0) {
+ return Err(lnav::console::user_message::error(
+ attr_line_t("invalid regex ")
+ .append_quoted(lnav::roles::symbol(entry.e_regex))
+ .append(" from ")
+ .append_quoted(lnav::roles::symbol(url)))
+ .with_reason(attr_line_t("unsupported regex flavor: ")
+ .append_quoted(
+ lnav::roles::symbol(entry.e_flags)))
+ .with_help(attr_line_t("the supported flavors are: ")
+ .join(SUPPORTED_FLAVORS,
+ VC_ROLE.value(role_t::VCR_SYMBOL),
+ ", ")));
+ }
+
+ auto regex_res = lnav::pcre2pp::code::from(entry.e_regex);
+ if (regex_res.isErr()) {
+ auto parse_error = regex_res.unwrapErr();
+ return Err(lnav::console::user_message::error(
+ attr_line_t("invalid regex ")
+ .append_quoted(lnav::roles::symbol(entry.e_regex))
+ .append(" from ")
+ .append_quoted(lnav::roles::symbol(url)))
+ .with_reason(parse_error.get_message())
+ .with_help("fix the regex and try the import again"));
+ }
+
+ auto regex = regex_res.unwrap();
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, true);
+ {
+ yajlpp_map root_map(gen);
+
+ root_map.gen("$schema");
+ root_map.gen(DEFAULT_FORMAT_SCHEMA);
+
+ root_map.gen(name);
+ {
+ yajlpp_map format_map(gen);
+
+ if (!existing_format) {
+ format_map.gen("description");
+ format_map.gen(fmt::format(
+ FMT_STRING(
+ "Format file generated from regex101 entry -- {}"),
+ url));
+ }
+ format_map.gen("regex");
+ {
+ yajlpp_map regex_map(gen);
+
+ regex_map.gen(pat_name);
+ {
+ yajlpp_map std_map(gen);
+
+ std_map.gen("pattern");
+ std_map.gen(entry.e_regex);
+ }
+ }
+ if (!existing_format) {
+ format_map.gen("value");
+ {
+ yajlpp_map value_map(gen);
+
+ for (auto named_cap : regex.get_named_captures()) {
+ if (named_cap.get_name() == "body") {
+ // don't need to add this as a value
+ continue;
+ }
+
+ value_map.gen(named_cap.get_name());
+ {
+ yajlpp_map cap_map(gen);
+
+ cap_map.gen("kind");
+ cap_map.gen("string");
+ }
+ }
+ }
+ }
+ format_map.gen("sample");
+ {
+ yajlpp_array sample_array(gen);
+
+ if (!entry.e_test_string.empty()) {
+ yajlpp_map elem_map(gen);
+
+ elem_map.gen("line");
+ elem_map.gen(rtrim(entry.e_test_string));
+ }
+ for (const auto& ut : entry.e_unit_tests) {
+ if (ut.ut_test_string.empty()) {
+ continue;
+ }
+
+ yajlpp_map elem_map(gen);
+
+ if (!ut.ut_description.empty()) {
+ elem_map.gen("description");
+ elem_map.gen(ut.ut_description);
+ }
+ elem_map.gen("line");
+ elem_map.gen(rtrim(ut.ut_test_string));
+ }
+ }
+ }
+ }
+
+ auto format_json = gen.to_string_fragment();
+ auto write_res = lnav::filesystem::write_file(format_path, format_json);
+ if (write_res.isErr()) {
+ return Err(lnav::console::user_message::error(
+ attr_line_t("unable to create format file: ")
+ .append(lnav::roles::file(format_path)))
+ .with_reason(write_res.unwrapErr()));
+ }
+
+ lnav::session::regex101::insert_entry({name, pat_name, permalink, ""});
+
+ return Ok(format_path);
+}
+
+ghc::filesystem::path
+regex101::patch_path(const external_log_format* format,
+ const std::string& permalink)
+{
+ if (format->elf_format_source_order.empty()) {
+ return lnav::paths::dotlnav() / "formats" / "installed"
+ / fmt::format(FMT_STRING("{}.regex101-{}.json"),
+ format->get_name(),
+ permalink);
+ }
+
+ auto first_path = format->elf_format_source_order.front();
+
+ return first_path.replace_extension(
+ fmt::format(FMT_STRING("regex101-{}.json"), permalink));
+}
+
+Result<ghc::filesystem::path, lnav::console::user_message>
+regex101::patch(const external_log_format* format,
+ const std::string& pat_name,
+ const regex101::client::entry& entry)
+{
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, true);
+ {
+ yajlpp_map root_map(gen);
+
+ root_map.gen("$schema");
+ root_map.gen(DEFAULT_FORMAT_SCHEMA);
+
+ root_map.gen(format->get_name());
+ {
+ yajlpp_map format_map(gen);
+
+ format_map.gen("regex");
+ {
+ yajlpp_map regex_map(gen);
+
+ regex_map.gen(pat_name);
+ {
+ yajlpp_map pat_map(gen);
+
+ pat_map.gen("pattern");
+ pat_map.gen(entry.e_regex);
+ }
+ }
+
+ auto new_samples
+ = entry.e_unit_tests
+ | lnav::itertools::prepend(regex101::client::unit_test{
+ "",
+ entry.e_test_string,
+ })
+ | lnav::itertools::filter_out([&format](const auto& ut) {
+ if (ut.ut_test_string.empty()) {
+ return true;
+ }
+ return (format->elf_samples
+ | lnav::itertools::find_if(
+ [&ut](const auto& samp) {
+ return samp.s_line.pp_value
+ == rtrim(ut.ut_test_string);
+ }))
+ .has_value();
+ });
+
+ if (!new_samples.empty()) {
+ format_map.gen("sample");
+ {
+ yajlpp_array sample_array(gen);
+
+ for (const auto& ut : entry.e_unit_tests) {
+ yajlpp_map elem_map(gen);
+
+ if (!ut.ut_description.empty()) {
+ elem_map.gen("description");
+ elem_map.gen(ut.ut_description);
+ }
+ elem_map.gen("line");
+ elem_map.gen(rtrim(ut.ut_test_string));
+ }
+ }
+ }
+ }
+ }
+
+ auto retval
+ = regex101::patch_path(format, entry.e_permalink_fragment.value());
+ auto write_res
+ = lnav::filesystem::write_file(retval, gen.to_string_fragment());
+ if (write_res.isErr()) {
+ return Err(lnav::console::user_message::error(
+ attr_line_t("unable to write format patch file: ")
+ .append(lnav::roles::file(retval.string())))
+ .with_reason(write_res.unwrapErr()));
+ }
+
+ return Ok(retval);
+}
+
+regex101::client::entry
+regex101::convert_format_pattern(
+ const external_log_format* format,
+ std::shared_ptr<external_log_format::pattern> pattern)
+{
+ regex101::client::entry en;
+
+ en.e_regex = pattern->p_pcre.pp_value->get_pattern();
+ for (const auto& sample : format->elf_samples) {
+ if (en.e_test_string.empty()) {
+ en.e_test_string = sample.s_line.pp_value;
+ } else {
+ regex101::client::unit_test ut;
+
+ ut.ut_test_string = sample.s_line.pp_value;
+ ut.ut_description = sample.s_description;
+ en.e_unit_tests.emplace_back(ut);
+ }
+ }
+
+ return en;
+}
diff --git a/src/regex101.import.hh b/src/regex101.import.hh
new file mode 100644
index 0000000..cabaa0b
--- /dev/null
+++ b/src/regex101.import.hh
@@ -0,0 +1,61 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef regex101_import_hh
+#define regex101_import_hh
+
+#include <map>
+
+#include "base/lnav.console.hh"
+#include "ghc/filesystem.hpp"
+#include "log_format_ext.hh"
+#include "regex101.client.hh"
+
+namespace regex101 {
+
+Result<ghc::filesystem::path, lnav::console::user_message> import(
+ const std::string& url,
+ const std::string& name,
+ const std::string& pat_name);
+
+ghc::filesystem::path patch_path(const external_log_format* format,
+ const std::string& permalink);
+
+Result<ghc::filesystem::path, lnav::console::user_message> patch(
+ const external_log_format* format,
+ const std::string& pat_name,
+ const regex101::client::entry& entry);
+
+regex101::client::entry convert_format_pattern(
+ const external_log_format* format,
+ std::shared_ptr<external_log_format::pattern> pat);
+
+} // namespace regex101
+
+#endif
diff --git a/src/regexp_vtab.cc b/src/regexp_vtab.cc
new file mode 100644
index 0000000..5324194
--- /dev/null
+++ b/src/regexp_vtab.cc
@@ -0,0 +1,644 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifdef __CYGWIN__
+# include <alloca.h>
+#endif
+
+#include "base/lnav_log.hh"
+#include "column_namer.hh"
+#include "config.h"
+#include "lnav_util.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "scn/scn.h"
+#include "sql_help.hh"
+#include "sql_util.hh"
+#include "vtab_module.hh"
+#include "yajlpp/yajlpp.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+enum {
+ RC_COL_MATCH_INDEX,
+ RC_COL_INDEX,
+ RC_COL_NAME,
+ RC_COL_CAPTURE_COUNT,
+ RC_COL_RANGE_START,
+ RC_COL_RANGE_STOP,
+ RC_COL_CONTENT,
+ RC_COL_VALUE,
+ RC_COL_PATTERN,
+};
+
+struct regexp_capture {
+ static constexpr const char* NAME = "regexp_capture";
+ static constexpr const char* CREATE_STMT = R"(
+-- The regexp_capture() table-valued function allows you to execute a regular-
+-- expression over a given string and get the captured data as rows in a table.
+CREATE TABLE regexp_capture (
+ match_index INTEGER,
+ capture_index INTEGER,
+ capture_name TEXT,
+ capture_count INTEGER,
+ range_start INTEGER,
+ range_stop INTEGER,
+ content TEXT,
+ value TEXT HIDDEN,
+ pattern TEXT HIDDEN
+);
+)";
+
+ struct cursor {
+ sqlite3_vtab_cursor base;
+ std::shared_ptr<lnav::pcre2pp::code> c_pattern;
+ lnav::pcre2pp::match_data c_match_data{
+ lnav::pcre2pp::match_data::unitialized()};
+ std::string c_content;
+ string_fragment c_remaining;
+ bool c_content_as_blob{false};
+ int c_index{0};
+ bool c_matched{false};
+ int c_match_index{0};
+ sqlite3_int64 c_rowid{0};
+
+ cursor(sqlite3_vtab* vt) : base({vt}) {}
+
+ int reset() { return SQLITE_OK; }
+
+ int next()
+ {
+ if (this->c_index >= (this->c_match_data.get_count() - 1)) {
+ auto match_res = this->c_pattern->capture_from(this->c_content)
+ .at(this->c_remaining)
+ .into(this->c_match_data)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (match_res) {
+ this->c_remaining = match_res->f_remaining;
+ }
+ this->c_matched = match_res.has_value();
+ this->c_index = -1;
+ this->c_match_index += 1;
+ }
+
+ if (!this->c_matched) {
+ return SQLITE_OK;
+ }
+
+ this->c_index += 1;
+
+ return SQLITE_OK;
+ }
+
+ int eof() { return this->c_pattern == nullptr || !this->c_matched; }
+
+ int get_rowid(sqlite3_int64& rowid_out)
+ {
+ rowid_out = this->c_rowid;
+
+ return SQLITE_OK;
+ }
+ };
+
+ int get_column(const cursor& vc, sqlite3_context* ctx, int col)
+ {
+ const auto cap = vc.c_match_data[vc.c_index];
+
+ switch (col) {
+ case RC_COL_MATCH_INDEX:
+ sqlite3_result_int64(ctx, vc.c_match_index);
+ break;
+ case RC_COL_INDEX:
+ sqlite3_result_int64(ctx, vc.c_index);
+ break;
+ case RC_COL_NAME:
+ if (vc.c_index == 0) {
+ sqlite3_result_null(ctx);
+ } else {
+ to_sqlite(ctx,
+ vc.c_pattern->get_name_for_capture(vc.c_index));
+ }
+ break;
+ case RC_COL_CAPTURE_COUNT:
+ sqlite3_result_int64(ctx, vc.c_match_data.get_count());
+ break;
+ case RC_COL_RANGE_START:
+ if (cap.has_value()) {
+ sqlite3_result_int64(ctx, cap->sf_begin + 1);
+ } else {
+ sqlite3_result_int64(ctx, 0);
+ }
+ break;
+ case RC_COL_RANGE_STOP:
+ if (cap.has_value()) {
+ sqlite3_result_int64(ctx, cap->sf_end + 1);
+ } else {
+ sqlite3_result_int64(ctx, 0);
+ }
+ break;
+ case RC_COL_CONTENT:
+ if (cap.has_value()) {
+ to_sqlite(ctx, cap.value());
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ break;
+ case RC_COL_VALUE:
+ if (vc.c_content_as_blob) {
+ sqlite3_result_blob64(ctx,
+ vc.c_content.c_str(),
+ vc.c_content.length(),
+ SQLITE_STATIC);
+ } else {
+ sqlite3_result_text(ctx,
+ vc.c_content.c_str(),
+ vc.c_content.length(),
+ SQLITE_STATIC);
+ }
+ break;
+ case RC_COL_PATTERN: {
+ to_sqlite(ctx, vc.c_pattern->get_pattern());
+ break;
+ }
+ }
+
+ return SQLITE_OK;
+ }
+};
+
+static int
+rcBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo)
+{
+ vtab_index_constraints vic(pIdxInfo);
+ vtab_index_usage viu(pIdxInfo);
+
+ for (auto iter = vic.begin(); iter != vic.end(); ++iter) {
+ if (iter->op != SQLITE_INDEX_CONSTRAINT_EQ) {
+ continue;
+ }
+
+ switch (iter->iColumn) {
+ case RC_COL_VALUE:
+ case RC_COL_PATTERN:
+ viu.column_used(iter);
+ break;
+ }
+ }
+
+ viu.allocate_args(RC_COL_VALUE, RC_COL_PATTERN, 2);
+ return SQLITE_OK;
+}
+
+static int
+rcFilter(sqlite3_vtab_cursor* pVtabCursor,
+ int idxNum,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv)
+{
+ regexp_capture::cursor* pCur = (regexp_capture::cursor*) pVtabCursor;
+
+ if (argc != 2) {
+ pCur->c_content.clear();
+ pCur->c_pattern.reset();
+ return SQLITE_OK;
+ }
+
+ auto byte_count = sqlite3_value_bytes(argv[0]);
+ auto blob = (const char*) sqlite3_value_blob(argv[0]);
+
+ pCur->c_content_as_blob = (sqlite3_value_type(argv[0]) == SQLITE_BLOB);
+ pCur->c_content.assign(blob, byte_count);
+
+ auto pattern = from_sqlite<string_fragment>()(argc, argv, 1);
+ auto compile_res = lnav::pcre2pp::code::from(pattern);
+ if (compile_res.isErr()) {
+ pVtabCursor->pVtab->zErrMsg
+ = sqlite3_mprintf("Invalid regular expression: %s",
+ compile_res.unwrapErr().get_message().c_str());
+ return SQLITE_ERROR;
+ }
+
+ pCur->c_pattern = compile_res.unwrap().to_shared();
+
+ pCur->c_index = 0;
+ pCur->c_match_data = pCur->c_pattern->create_match_data();
+
+ pCur->c_remaining.clear();
+ auto match_res = pCur->c_pattern->capture_from(pCur->c_content)
+ .into(pCur->c_match_data)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (match_res) {
+ pCur->c_remaining = match_res->f_remaining;
+ }
+ pCur->c_matched = match_res.has_value();
+ pCur->c_match_index = 0;
+
+ return SQLITE_OK;
+}
+
+enum {
+ RCJ_COL_MATCH_INDEX,
+ RCJ_COL_CONTENT,
+ RCJ_COL_VALUE,
+ RCJ_COL_PATTERN,
+ RCJ_COL_FLAGS,
+};
+
+struct regexp_capture_flags {
+ bool convert_numbers{true};
+};
+
+const typed_json_path_container<regexp_capture_flags>
+ regexp_capture_flags_handlers
+ = typed_json_path_container<regexp_capture_flags>{
+ yajlpp::property_handler("convert-numbers")
+ .for_field(&regexp_capture_flags::convert_numbers),
+ };
+
+struct regexp_capture_into_json {
+ static constexpr const char* NAME = "regexp_capture_into_json";
+ static constexpr const char* CREATE_STMT = R"(
+-- The regexp_capture_into_json() table-valued function allows you to execute a
+-- regular-expression over a given string and get the captured data as rows in
+-- a table.
+CREATE TABLE regexp_capture_into_json (
+ match_index INTEGER,
+ content TEXT,
+ value TEXT HIDDEN,
+ pattern TEXT HIDDEN,
+ flags TEXT HIDDEN
+);
+)";
+
+ struct cursor {
+ sqlite3_vtab_cursor base;
+ std::shared_ptr<lnav::pcre2pp::code> c_pattern;
+ lnav::pcre2pp::match_data c_match_data{
+ lnav::pcre2pp::match_data::unitialized()};
+ std::unique_ptr<column_namer> c_namer;
+ std::string c_content;
+ string_fragment c_remaining;
+ bool c_content_as_blob{false};
+ bool c_matched{false};
+ size_t c_match_index{0};
+ sqlite3_int64 c_rowid{0};
+ std::string c_flags_string;
+ nonstd::optional<regexp_capture_flags> c_flags;
+
+ cursor(sqlite3_vtab* vt) : base({vt}) {}
+
+ int reset() { return SQLITE_OK; }
+
+ int next()
+ {
+ auto match_res = this->c_pattern->capture_from(this->c_content)
+ .at(this->c_remaining)
+ .into(this->c_match_data)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (match_res) {
+ this->c_remaining = match_res->f_remaining;
+ }
+ this->c_matched = match_res.has_value();
+ this->c_match_index += 1;
+
+ if (!this->c_matched) {
+ return SQLITE_OK;
+ }
+
+ return SQLITE_OK;
+ }
+
+ int eof() { return this->c_pattern == nullptr || !this->c_matched; }
+
+ int get_rowid(sqlite3_int64& rowid_out)
+ {
+ rowid_out = this->c_rowid;
+
+ return SQLITE_OK;
+ }
+ };
+
+ int get_column(const cursor& vc, sqlite3_context* ctx, int col)
+ {
+ switch (col) {
+ case RCJ_COL_MATCH_INDEX:
+ sqlite3_result_int64(ctx, vc.c_match_index);
+ break;
+ case RCJ_COL_CONTENT: {
+ yajlpp_gen gen;
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_map root_map(gen);
+
+ for (size_t lpc = 1; lpc < vc.c_match_data.get_count();
+ lpc++)
+ {
+ const auto& colname = vc.c_namer->cn_names[lpc];
+ const auto cap = vc.c_match_data[lpc];
+
+ if (!cap) {
+ continue;
+ }
+
+ yajl_gen_pstring(gen, colname.data(), colname.length());
+
+ if (!vc.c_flags || vc.c_flags->convert_numbers) {
+ auto cap_view = cap->to_string_view();
+ auto scan_int_res
+ = scn::scan_value<int64_t>(cap_view);
+
+ if (scan_int_res && scan_int_res.range().empty()) {
+ yajl_gen_integer(gen, scan_int_res.value());
+ continue;
+ }
+
+ auto scan_float_res
+ = scn::scan_value<double>(cap_view);
+ if (scan_float_res
+ && scan_float_res.range().empty())
+ {
+ yajl_gen_number(
+ gen, cap_view.data(), cap_view.length());
+ continue;
+ }
+
+ yajl_gen_pstring(
+ gen, cap_view.data(), cap_view.length());
+ } else {
+ yajl_gen_pstring(gen, cap->data(), cap->length());
+ }
+ }
+ }
+
+ auto sf = gen.to_string_fragment();
+ sqlite3_result_text(
+ ctx, sf.data(), sf.length(), SQLITE_TRANSIENT);
+ sqlite3_result_subtype(ctx, JSON_SUBTYPE);
+ break;
+ }
+ case RCJ_COL_VALUE:
+ if (vc.c_content_as_blob) {
+ sqlite3_result_blob64(ctx,
+ vc.c_content.c_str(),
+ vc.c_content.length(),
+ SQLITE_STATIC);
+ } else {
+ sqlite3_result_text(ctx,
+ vc.c_content.c_str(),
+ vc.c_content.length(),
+ SQLITE_STATIC);
+ }
+ break;
+ case RCJ_COL_PATTERN: {
+ to_sqlite(ctx, vc.c_pattern->get_pattern());
+ break;
+ }
+ case RCJ_COL_FLAGS: {
+ if (!vc.c_flags) {
+ sqlite3_result_null(ctx);
+ } else {
+ to_sqlite(ctx, vc.c_flags_string);
+ }
+ break;
+ }
+ }
+
+ return SQLITE_OK;
+ }
+};
+
+static int
+rcjBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo)
+{
+ vtab_index_constraints vic(pIdxInfo);
+ vtab_index_usage viu(pIdxInfo);
+
+ for (auto iter = vic.begin(); iter != vic.end(); ++iter) {
+ if (iter->op != SQLITE_INDEX_CONSTRAINT_EQ) {
+ continue;
+ }
+
+ switch (iter->iColumn) {
+ case RCJ_COL_VALUE:
+ case RCJ_COL_PATTERN:
+ case RCJ_COL_FLAGS:
+ viu.column_used(iter);
+ break;
+ }
+ }
+
+ viu.allocate_args(RCJ_COL_VALUE, RCJ_COL_FLAGS, 2);
+ return SQLITE_OK;
+}
+
+static int
+rcjFilter(sqlite3_vtab_cursor* pVtabCursor,
+ int idxNum,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv)
+{
+ auto* pCur = (regexp_capture_into_json::cursor*) pVtabCursor;
+
+ if (argc < 2 || argc > 3) {
+ pCur->c_content.clear();
+ pCur->c_pattern.reset();
+ pCur->c_flags_string.clear();
+ pCur->c_flags = nonstd::nullopt;
+ return SQLITE_OK;
+ }
+
+ auto byte_count = sqlite3_value_bytes(argv[0]);
+ const auto* blob = (const char*) sqlite3_value_blob(argv[0]);
+
+ pCur->c_content_as_blob = (sqlite3_value_type(argv[0]) == SQLITE_BLOB);
+ pCur->c_content.assign(blob, byte_count);
+
+ auto pattern = from_sqlite<string_fragment>()(argc, argv, 1);
+ auto compile_res = lnav::pcre2pp::code::from(pattern);
+ if (compile_res.isErr()) {
+ pVtabCursor->pVtab->zErrMsg
+ = sqlite3_mprintf("Invalid regular expression: %s",
+ compile_res.unwrapErr().get_message().c_str());
+ return SQLITE_ERROR;
+ }
+
+ pCur->c_flags_string.clear();
+ pCur->c_flags = nonstd::nullopt;
+ if (argc == 3) {
+ static const intern_string_t FLAGS_SRC = intern_string::lookup("flags");
+ const auto flags_json = from_sqlite<string_fragment>()(argc, argv, 2);
+
+ if (!flags_json.empty()) {
+ const auto parse_res
+ = regexp_capture_flags_handlers.parser_for(FLAGS_SRC).of(
+ flags_json);
+
+ if (parse_res.isErr()) {
+ auto um = lnav::console::user_message::error(
+ "unable to parse flags")
+ .with_reason(parse_res.unwrapErr()[0]);
+
+ pVtabCursor->pVtab->zErrMsg = sqlite3_mprintf(
+ "%s%s", sqlitepp::ERROR_PREFIX, lnav::to_json(um).c_str());
+ return SQLITE_ERROR;
+ }
+
+ pCur->c_flags_string = flags_json.to_string();
+ pCur->c_flags = parse_res.unwrap();
+ }
+ }
+
+ pCur->c_pattern = compile_res.unwrap().to_shared();
+ pCur->c_namer
+ = std::make_unique<column_namer>(column_namer::language::JSON);
+ pCur->c_namer->add_column(string_fragment::from_const("__all__"));
+ for (size_t lpc = 1; lpc <= pCur->c_pattern->get_capture_count(); lpc++) {
+ pCur->c_namer->add_column(string_fragment::from_c_str(
+ pCur->c_pattern->get_name_for_capture(lpc)));
+ }
+
+ pCur->c_match_data = pCur->c_pattern->create_match_data();
+ pCur->c_remaining.clear();
+ auto match_res = pCur->c_pattern->capture_from(pCur->c_content)
+ .into(pCur->c_match_data)
+ .matches(PCRE2_NO_UTF_CHECK)
+ .ignore_error();
+ if (match_res) {
+ pCur->c_remaining = match_res->f_remaining;
+ }
+ pCur->c_matched = match_res.has_value();
+ pCur->c_match_index = 0;
+
+ return SQLITE_OK;
+}
+
+int
+register_regexp_vtab(sqlite3* db)
+{
+ static vtab_module<tvt_no_update<regexp_capture>> REGEXP_CAPTURE_MODULE;
+ static help_text regexp_capture_help
+ = help_text("regexp_capture",
+ "A table-valued function that executes a "
+ "regular-expression over a "
+ "string and returns the captured values. If the regex "
+ "only matches a "
+ "subset of the input string, it will be rerun on the "
+ "remaining parts "
+ "of the string until no more matches are found.")
+ .sql_table_valued_function()
+ .with_parameter(
+ {"string", "The string to match against the given pattern."})
+ .with_parameter({"pattern", "The regular expression to match."})
+ .with_result({
+ "match_index",
+ "The match iteration. This value will increase "
+ "each time a new match is found in the input string.",
+ })
+ .with_result(
+ {"capture_index", "The index of the capture in the regex."})
+ .with_result(
+ {"capture_name", "The name of the capture in the regex."})
+ .with_result({"capture_count",
+ "The total number of captures in the regex."})
+ .with_result({"range_start",
+ "The start of the capture in the input string."})
+ .with_result({"range_stop",
+ "The stop of the capture in the input string."})
+ .with_result({"content", "The captured value from the string."})
+ .with_tags({"string"})
+ .with_example({
+ "To extract the key/value pairs 'a'/1 and 'b'/2 "
+ "from the string 'a=1; b=2'",
+ "SELECT * FROM regexp_capture('a=1; b=2', "
+ "'(\\w+)=(\\d+)')",
+ });
+
+ int rc;
+
+ REGEXP_CAPTURE_MODULE.vm_module.xBestIndex = rcBestIndex;
+ REGEXP_CAPTURE_MODULE.vm_module.xFilter = rcFilter;
+
+ rc = REGEXP_CAPTURE_MODULE.create(db, "regexp_capture");
+ sqlite_function_help.insert(
+ std::make_pair("regexp_capture", &regexp_capture_help));
+ regexp_capture_help.index_tags();
+
+ ensure(rc == SQLITE_OK);
+
+ static vtab_module<tvt_no_update<regexp_capture_into_json>>
+ REGEXP_CAPTURE_INTO_JSON_MODULE;
+ static help_text regexp_capture_into_json_help
+ = help_text(
+ "regexp_capture_into_json",
+ "A table-valued function that executes a "
+ "regular-expression over a string and returns the captured "
+ "values as a JSON object. If the regex only matches a "
+ "subset of the input string, it will be rerun on the "
+ "remaining parts of the string until no more matches are found.")
+ .sql_table_valued_function()
+ .with_parameter(
+ {"string", "The string to match against the given pattern."})
+ .with_parameter({"pattern", "The regular expression to match."})
+ .with_parameter(help_text{
+ "options",
+ "A JSON object with the following option: "
+ "convert-numbers - True (default) if text that looks like "
+ "numeric data should be converted to JSON numbers, "
+ "false if they should be captured as strings."}
+ .optional())
+ .with_result({
+ "match_index",
+ "The match iteration. This value will increase "
+ "each time a new match is found in the input string.",
+ })
+ .with_result({"content", "The captured values from the string."})
+ .with_tags({"string"})
+ .with_example({
+ "To extract the key/value pairs 'a'/1 and 'b'/2 "
+ "from the string 'a=1; b=2'",
+ "SELECT * FROM regexp_capture_into_json('a=1; b=2', "
+ "'(\\w+)=(\\d+)')",
+ });
+
+ REGEXP_CAPTURE_INTO_JSON_MODULE.vm_module.xBestIndex = rcjBestIndex;
+ REGEXP_CAPTURE_INTO_JSON_MODULE.vm_module.xFilter = rcjFilter;
+
+ rc = REGEXP_CAPTURE_INTO_JSON_MODULE.create(db, "regexp_capture_into_json");
+ sqlite_function_help.insert(std::make_pair("regexp_capture_into_json",
+ &regexp_capture_into_json_help));
+ regexp_capture_into_json_help.index_tags();
+
+ ensure(rc == SQLITE_OK);
+
+ return rc;
+}
diff --git a/src/regexp_vtab.hh b/src/regexp_vtab.hh
new file mode 100644
index 0000000..06eda40
--- /dev/null
+++ b/src/regexp_vtab.hh
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef regexp_vtab_hh
+#define regexp_vtab_hh
+
+#include <sqlite3.h>
+
+int register_regexp_vtab(sqlite3* db);
+
+#endif
diff --git a/src/relative_time.cc b/src/relative_time.cc
new file mode 100644
index 0000000..e6f1118
--- /dev/null
+++ b/src/relative_time.cc
@@ -0,0 +1,1134 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <unordered_set>
+
+#include "relative_time.hh"
+
+#include "base/time_util.hh"
+#include "config.h"
+#include "pcrepp/pcre2pp.hh"
+#include "scn/scn.h"
+
+using namespace std::chrono_literals;
+
+static const struct {
+ const char* name;
+ lnav::pcre2pp::code pcre;
+} MATCHERS[relative_time::RTT__MAX] = {
+ {
+ "ws",
+ lnav::pcre2pp::code::from_const("\\A\\s+\\b"),
+ },
+ {
+ "am",
+ lnav::pcre2pp::code::from_const("\\Aam|a\\.m\\.\\b"),
+ },
+ {
+ "pm",
+ lnav::pcre2pp::code::from_const("\\Apm|p\\.m\\.\\b"),
+ },
+ {
+ "a",
+ lnav::pcre2pp::code::from_const("\\Aa\\b"),
+ },
+ {
+ "an",
+ lnav::pcre2pp::code::from_const("\\Aan\\b"),
+ },
+ {
+ "at",
+ lnav::pcre2pp::code::from_const("\\Aat\\b"),
+ },
+ {
+ "time",
+ lnav::pcre2pp::code::from_const(
+ "\\A(\\d{1,2}):(\\d{2})(?::(\\d{2})(?:\\.(\\d{3,6}))?)?"),
+ },
+ {
+ "num",
+ lnav::pcre2pp::code::from_const("\\A((?:-|\\+)?\\d+)"),
+ },
+
+ {
+ "sun",
+ lnav::pcre2pp::code::from_const("\\Asun(days?)?\\b"),
+ },
+ {
+ "mon",
+ lnav::pcre2pp::code::from_const("\\Amon(days?)?\\b"),
+ },
+ {
+ "tue",
+ lnav::pcre2pp::code::from_const("\\Atue(s(days?)?)?\\b"),
+ },
+ {
+ "wed",
+ lnav::pcre2pp::code::from_const("\\Awed(nesdays?)?\\b"),
+ },
+ {
+ "thu",
+ lnav::pcre2pp::code::from_const("\\Athu(rsdays?)?\\b"),
+ },
+ {
+ "fri",
+ lnav::pcre2pp::code::from_const("\\Afri(days?)?\\b"),
+ },
+ {
+ "sat",
+ lnav::pcre2pp::code::from_const("\\Asat(urdays?)?\\b"),
+ },
+
+ {
+ "us",
+ lnav::pcre2pp::code::from_const(
+ "\\A(?:micros(?:econds?)?|us(?![a-zA-Z]))"),
+ },
+ {
+ "ms",
+ lnav::pcre2pp::code::from_const(
+ "\\A(?:millis(?:econds?)?|ms(?![a-zA-Z]))"),
+ },
+ {
+ "sec",
+ lnav::pcre2pp::code::from_const("\\As(?:ec(?:onds?)?)?(?![a-zA-Z])"),
+ },
+ {
+ "min",
+ lnav::pcre2pp::code::from_const("\\Am(?:in(?:utes?)?)?(?![a-zA-Z])"),
+ },
+ {
+ "h",
+ lnav::pcre2pp::code::from_const("\\Ah(?:ours?)?(?![a-zA-Z])"),
+ },
+ {
+ "day",
+ lnav::pcre2pp::code::from_const("\\Ad(?:ays?)?(?![a-zA-Z])"),
+ },
+ {
+ "week",
+ lnav::pcre2pp::code::from_const("\\Aw(?:eeks?)?(?![a-zA-Z])"),
+ },
+ {
+ "mon",
+ lnav::pcre2pp::code::from_const("\\Amon(?:ths?)?(?![a-zA-Z])"),
+ },
+ {
+ "year",
+ lnav::pcre2pp::code::from_const("\\Ay(?:ears?)?(?![a-zA-Z])"),
+ },
+ {
+ "today",
+ lnav::pcre2pp::code::from_const("\\Atoday\\b"),
+ },
+ {
+ "yest",
+ lnav::pcre2pp::code::from_const("\\Ayesterday\\b"),
+ },
+ {
+ "tomo",
+ lnav::pcre2pp::code::from_const("\\Atomorrow\\b"),
+ },
+ {
+ "noon",
+ lnav::pcre2pp::code::from_const("\\Anoon\\b"),
+ },
+ {
+ "and",
+ lnav::pcre2pp::code::from_const("\\Aand\\b"),
+ },
+ {
+ "the",
+ lnav::pcre2pp::code::from_const("\\Athe\\b"),
+ },
+ {
+ "ago",
+ lnav::pcre2pp::code::from_const("\\Aago\\b"),
+ },
+ {
+ "lter",
+ lnav::pcre2pp::code::from_const("\\Alater\\b"),
+ },
+ {
+ "bfor",
+ lnav::pcre2pp::code::from_const("\\Abefore\\b"),
+ },
+ {
+ "aft",
+ lnav::pcre2pp::code::from_const("\\Aafter\\b"),
+ },
+ {
+ "now",
+ lnav::pcre2pp::code::from_const("\\Anow\\b"),
+ },
+ {
+ "here",
+ lnav::pcre2pp::code::from_const("\\Ahere\\b"),
+ },
+ {
+ "next",
+ lnav::pcre2pp::code::from_const("\\Anext\\b"),
+ },
+ {
+ "previous",
+ lnav::pcre2pp::code::from_const("\\A(?:previous\\b|last\\b)"),
+ },
+};
+
+static int64_t TIME_SCALES[] = {
+ 1000 * 1000,
+ 60,
+ 60,
+ 24,
+};
+
+const char relative_time::FIELD_CHARS[] = {
+ 'u',
+ 's',
+ 'm',
+ 'h',
+ 'd',
+ 'M',
+ 'y',
+};
+
+Result<relative_time, relative_time::parse_error>
+relative_time::from_str(string_fragment str)
+{
+ int64_t number = 0;
+ bool number_set = false, number_was_set = false;
+ bool next_set = false;
+ token_t base_token = RTT_INVALID;
+ rt_field_type last_field_type = RTF__MAX;
+ relative_time retval;
+ parse_error pe_out;
+ std::unordered_set<int> seen_tokens;
+
+ pe_out.pe_column = 0;
+ pe_out.pe_msg.clear();
+
+ auto remaining = str;
+ while (true) {
+ rt_field_type curr_field_type = RTF__MAX;
+
+ if (remaining.empty()) {
+ if (number_set) {
+ if (number > 1970 && number < 2050) {
+ retval.rt_field[RTF_YEARS] = number - 1900;
+ retval.rt_absolute_field_end = RTF__MAX;
+
+ switch (base_token) {
+ case RTT_BEFORE: {
+ auto epoch = retval.to_timeval();
+ retval.rt_duration
+ = std::chrono::duration_cast<
+ std::chrono::microseconds>(
+ std::chrono::seconds(epoch.tv_sec))
+ + std::chrono::microseconds(epoch.tv_usec);
+ retval.rt_field[RTF_YEARS] = 70;
+ break;
+ }
+ case RTT_AFTER:
+ retval.rt_duration = std::chrono::duration_cast<
+ std::chrono::microseconds>(
+ std::chrono::hours(24 * 365 * 200));
+ break;
+ default:
+ break;
+ }
+ return Ok(retval);
+ }
+
+ pe_out.pe_msg = "Number given without a time unit";
+ return Err(pe_out);
+ }
+
+ if (base_token != RTT_INVALID) {
+ switch (base_token) {
+ case RTT_BEFORE:
+ pe_out.pe_msg
+ = "'before' requires a point in time (e.g. before "
+ "10am)";
+ break;
+ case RTT_AFTER:
+ pe_out.pe_msg
+ = "'after' requires a point in time (e.g. after "
+ "10am)";
+ break;
+ default:
+ ensure(false);
+ break;
+ }
+ return Err(pe_out);
+ }
+
+ retval.rollover();
+ return Ok(retval);
+ }
+
+ bool found = false;
+ for (int lpc = 0; lpc < RTT__MAX && !found; lpc++) {
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+
+ token_t token = (token_t) lpc;
+ auto match_res = MATCHERS[lpc]
+ .pcre.capture_from(remaining)
+ .into(md)
+ .matches()
+ .ignore_error();
+ if (!match_res) {
+ continue;
+ }
+
+ remaining = match_res->f_remaining;
+ pe_out.pe_column = match_res->f_all.sf_begin;
+ found = true;
+ if (RTT_MICROS <= token && token <= RTT_YEARS) {
+ if (!number_set) {
+ if (base_token != RTT_INVALID) {
+ base_token = RTT_INVALID;
+ retval.rt_absolute_field_end = RTF__MAX;
+ continue;
+ }
+ if (!retval.rt_next && !retval.rt_previous) {
+ pe_out.pe_msg = "Expecting a number before time unit";
+ return Err(pe_out);
+ }
+ }
+ number_was_set = number_set;
+ number_set = false;
+ }
+ switch (token) {
+ case RTT_YESTERDAY:
+ case RTT_TODAY:
+ case RTT_NOW: {
+ if (seen_tokens.count(token) > 0) {
+ pe_out.pe_msg
+ = "Current time reference has already been used";
+ return Err(pe_out);
+ }
+
+ seen_tokens.insert(RTT_YESTERDAY);
+ seen_tokens.insert(RTT_TODAY);
+ seen_tokens.insert(RTT_NOW);
+
+ struct timeval tv;
+ struct exttm tm;
+
+ gettimeofday(&tv, nullptr);
+ localtime_r(&tv.tv_sec, &tm.et_tm);
+ tm.et_nsec = tv.tv_usec * 1000;
+ tm = retval.adjust(tm);
+
+ retval.rt_field[RTF_YEARS] = tm.et_tm.tm_year;
+ retval.rt_field[RTF_MONTHS] = tm.et_tm.tm_mon;
+ retval.rt_field[RTF_DAYS] = tm.et_tm.tm_mday;
+ switch (token) {
+ case RTT_NOW:
+ retval.rt_field[RTF_HOURS] = tm.et_tm.tm_hour;
+ retval.rt_field[RTF_MINUTES] = tm.et_tm.tm_min;
+ retval.rt_field[RTF_SECONDS] = tm.et_tm.tm_sec;
+ retval.rt_field[RTF_MICROSECONDS]
+ = tm.et_nsec / 1000;
+ break;
+ case RTT_YESTERDAY:
+ retval.rt_field[RTF_DAYS].value -= 1;
+ case RTT_TODAY:
+ retval.rt_field[RTF_HOURS] = 0;
+ retval.rt_field[RTF_MINUTES] = 0;
+ retval.rt_field[RTF_SECONDS] = 0;
+ retval.rt_field[RTF_MICROSECONDS] = 0;
+ break;
+ default:
+ break;
+ }
+ retval.rt_absolute_field_end = RTF__MAX;
+ break;
+ }
+ case RTT_INVALID:
+ case RTT_WHITE:
+ case RTT_AND:
+ case RTT_THE:
+ curr_field_type = last_field_type;
+ break;
+ case RTT_AM:
+ case RTT_PM:
+ if (seen_tokens.count(token) > 0) {
+ pe_out.pe_msg = "Time has already been set";
+ return Err(pe_out);
+ }
+ seen_tokens.insert(RTT_AM);
+ seen_tokens.insert(RTT_PM);
+ if (number_set) {
+ retval.rt_field[RTF_HOURS] = number;
+ retval.rt_field[RTF_MINUTES] = 0;
+ retval.rt_field[RTF_SECONDS] = 0;
+ retval.rt_field[RTF_MICROSECONDS] = 0;
+ retval.rt_duration = 1min;
+ retval.rt_absolute_field_end = RTF__MAX;
+ number_set = false;
+ }
+ if (!retval.is_absolute(RTF_YEARS)) {
+ pe_out.pe_msg
+ = "Expecting absolute time with A.M. or P.M.";
+ return Err(pe_out);
+ }
+ if (token == RTT_AM) {
+ if (retval.rt_field[RTF_HOURS].value == 12) {
+ retval.rt_field[RTF_HOURS] = 0;
+ }
+ } else if (retval.rt_field[RTF_HOURS].value < 12) {
+ retval.rt_field[RTF_HOURS].value += 12;
+ }
+ if (base_token == RTT_AFTER) {
+ std::chrono::microseconds usecs = 0s;
+ uint64_t carry = 0;
+
+ if (retval.rt_field[RTF_MICROSECONDS].value > 0) {
+ usecs += std::chrono::microseconds(
+ 1000000ULL
+ - retval.rt_field[RTF_MICROSECONDS].value);
+ carry = 1;
+ }
+ if (carry || retval.rt_field[RTF_SECONDS].value > 0) {
+ usecs += std::chrono::seconds(
+ 60 - carry
+ - retval.rt_field[RTF_SECONDS].value);
+ carry = 1;
+ }
+ if (carry || retval.rt_field[RTF_MINUTES].value > 0) {
+ usecs += std::chrono::minutes(
+ 60 - carry
+ - retval.rt_field[RTF_MINUTES].value);
+ carry = 1;
+ }
+ usecs += std::chrono::hours(
+ 24 - retval.rt_field[RTF_HOURS].value);
+ retval.rt_duration = usecs;
+ }
+ if (base_token == RTT_BEFORE) {
+ retval.rt_duration
+ = std::chrono::hours(
+ retval.rt_field[RTF_HOURS].value)
+ + std::chrono::minutes(
+ retval.rt_field[RTF_MINUTES].value)
+ + std::chrono::seconds(
+ retval.rt_field[RTF_SECONDS].value)
+ + std::chrono::microseconds(
+ retval.rt_field[RTF_MICROSECONDS].value);
+ retval.rt_field[RTF_HOURS].value = 0;
+ retval.rt_field[RTF_MINUTES].value = 0;
+ retval.rt_field[RTF_SECONDS].value = 0;
+ retval.rt_field[RTF_MICROSECONDS].value = 0;
+ }
+ base_token = RTT_INVALID;
+ break;
+ case RTT_A:
+ case RTT_AN:
+ number = 1;
+ number_set = true;
+ break;
+ case RTT_AT:
+ break;
+ case RTT_TIME: {
+ const auto hstr = md[1]->to_string();
+ const auto mstr = md[2]->to_string();
+ retval.rt_field[RTF_HOURS] = atoi(hstr.c_str());
+ retval.rt_field[RTF_MINUTES] = atoi(mstr.c_str());
+ if (md[3]) {
+ const auto sstr = md[3]->to_string();
+ retval.rt_field[RTF_SECONDS] = atoi(sstr.c_str());
+ if (md[4]) {
+ const auto substr = md[4]->to_string();
+
+ switch (substr.length()) {
+ case 3:
+ retval.rt_field[RTF_MICROSECONDS]
+ = atoi(substr.c_str()) * 1000;
+ break;
+ case 6:
+ retval.rt_field[RTF_MICROSECONDS]
+ = atoi(substr.c_str());
+ break;
+ }
+ } else {
+ retval.rt_field[RTF_MICROSECONDS].clear();
+ retval.rt_duration = 1s;
+ }
+ } else {
+ retval.rt_field[RTF_SECONDS].clear();
+ retval.rt_field[RTF_MICROSECONDS].clear();
+ retval.rt_duration = 1min;
+ }
+ retval.rt_absolute_field_end = RTF__MAX;
+ break;
+ }
+ case RTT_NUMBER: {
+ if (number_set) {
+ pe_out.pe_msg
+ = "No time unit given for the previous number";
+ return Err(pe_out);
+ }
+
+ auto num_scan_res
+ = scn::scan_value<int64_t>(md[0]->to_string_view());
+
+ if (!num_scan_res) {
+ pe_out.pe_msg = fmt::format(
+ FMT_STRING("Invalid number: {}"), md[0].value());
+ return Err(pe_out);
+ }
+ number = num_scan_res.value();
+ number_set = true;
+ break;
+ }
+ case RTT_MICROS:
+ retval.rt_field[RTF_MICROSECONDS] = number;
+ break;
+ case RTT_MILLIS:
+ retval.rt_field[RTF_MICROSECONDS] = number * 1000;
+ break;
+ case RTT_SECONDS:
+ if (number_was_set) {
+ retval.rt_field[RTF_SECONDS] = number;
+ curr_field_type = RTF_SECONDS;
+ } else if (next_set) {
+ retval.rt_field[RTF_MICROSECONDS] = 0;
+ retval.rt_absolute_field_end = RTF__MAX;
+ }
+ break;
+ case RTT_MINUTES:
+ if (number_was_set) {
+ retval.rt_field[RTF_MINUTES] = number;
+ curr_field_type = RTF_MINUTES;
+ } else if (next_set) {
+ retval.rt_field[RTF_MICROSECONDS] = 0;
+ retval.rt_field[RTF_SECONDS] = 0;
+ retval.rt_absolute_field_end = RTF__MAX;
+ }
+ break;
+ case RTT_HOURS:
+ if (number_was_set) {
+ retval.rt_field[RTF_HOURS] = number;
+ curr_field_type = RTF_HOURS;
+ } else if (next_set) {
+ retval.rt_field[RTF_MICROSECONDS] = 0;
+ retval.rt_field[RTF_SECONDS] = 0;
+ retval.rt_field[RTF_MINUTES] = 0;
+ retval.rt_absolute_field_end = RTF__MAX;
+ }
+ break;
+ case RTT_DAYS:
+ if (number_was_set) {
+ retval.rt_field[RTF_DAYS] = number;
+ curr_field_type = RTF_DAYS;
+ } else if (next_set) {
+ retval.rt_field[RTF_MICROSECONDS] = 0;
+ retval.rt_field[RTF_SECONDS] = 0;
+ retval.rt_field[RTF_MINUTES] = 0;
+ retval.rt_field[RTF_HOURS] = 0;
+ retval.rt_absolute_field_end = RTF__MAX;
+ }
+ break;
+ case RTT_WEEKS:
+ retval.rt_field[RTF_DAYS] = number * 7;
+ break;
+ case RTT_MONTHS:
+ if (number_was_set) {
+ retval.rt_field[RTF_MONTHS] = number;
+ curr_field_type = RTF_MONTHS;
+ } else if (next_set) {
+ retval.rt_field[RTF_MICROSECONDS] = 0;
+ retval.rt_field[RTF_SECONDS] = 0;
+ retval.rt_field[RTF_MINUTES] = 0;
+ retval.rt_field[RTF_HOURS] = 0;
+ retval.rt_field[RTF_DAYS] = 0;
+ retval.rt_absolute_field_end = RTF__MAX;
+ }
+ break;
+ case RTT_YEARS:
+ if (number_was_set) {
+ retval.rt_field[RTF_YEARS] = number;
+ curr_field_type = RTF_YEARS;
+ } else if (next_set) {
+ retval.rt_field[RTF_MICROSECONDS] = 0;
+ retval.rt_field[RTF_SECONDS] = 0;
+ retval.rt_field[RTF_MINUTES] = 0;
+ retval.rt_field[RTF_HOURS] = 0;
+ retval.rt_field[RTF_DAYS] = 0;
+ retval.rt_field[RTF_MONTHS] = 0;
+ retval.rt_absolute_field_end = RTF__MAX;
+ }
+ break;
+ case RTT_AGO:
+ if (retval.empty()) {
+ pe_out.pe_msg = "Expecting a time unit";
+ return Err(pe_out);
+ }
+ for (int field = 0; field < RTF__MAX; field++) {
+ if (retval.rt_field[field].value > 0) {
+ retval.rt_field[field]
+ = -retval.rt_field[field].value;
+ }
+ if (last_field_type != RTF__MAX
+ && field < last_field_type)
+ {
+ retval.rt_field[field] = 0;
+ }
+ }
+ if (last_field_type != RTF__MAX) {
+ retval.rt_absolute_field_end = last_field_type;
+ }
+ break;
+ case RTT_BEFORE:
+ case RTT_AFTER:
+ if (base_token != RTT_INVALID) {
+ pe_out.pe_msg
+ = "Before/after ranges are not supported yet";
+ return Err(pe_out);
+ }
+ base_token = token;
+ break;
+ case RTT_LATER:
+ if (retval.empty()) {
+ pe_out.pe_msg = "Expecting a time unit before 'later'";
+ return Err(pe_out);
+ }
+ break;
+ case RTT_HERE:
+ break;
+ case RTT_NEXT:
+ retval.rt_next = true;
+ next_set = true;
+ break;
+ case RTT_PREVIOUS:
+ retval.rt_previous = true;
+ next_set = true;
+ break;
+ case RTT_TOMORROW:
+ retval.rt_field[RTF_DAYS] = 1;
+ break;
+ case RTT_NOON:
+ retval.rt_field[RTF_HOURS] = 12;
+ retval.rt_absolute_field_end = RTF__MAX;
+ for (int lpc2 = RTF_MICROSECONDS; lpc2 < RTF_HOURS; lpc2++)
+ {
+ retval.rt_field[lpc2] = 0;
+ }
+ break;
+
+ case RTT_SUNDAY:
+ case RTT_MONDAY:
+ case RTT_TUESDAY:
+ case RTT_WEDNESDAY:
+ case RTT_THURSDAY:
+ case RTT_FRIDAY:
+ case RTT_SATURDAY:
+ if (retval.rt_duration == 0s) {
+ switch (base_token) {
+ case RTT_BEFORE:
+ if (token == RTT_SUNDAY) {
+ pe_out.pe_msg
+ = "Sunday is the start of the week, so "
+ "there is nothing before it";
+ return Err(pe_out);
+ }
+ for (int wday = RTT_SUNDAY; wday < token;
+ wday++)
+ {
+ retval.rt_included_days.insert(
+ (token_t) wday);
+ }
+ break;
+ case RTT_AFTER:
+ if (token == RTT_SATURDAY) {
+ pe_out.pe_msg
+ = "Saturday is the end of the week, so "
+ "there is nothing after it";
+ return Err(pe_out);
+ }
+ for (int wday = RTT_SATURDAY; wday > token;
+ wday--)
+ {
+ retval.rt_included_days.insert(
+ (token_t) wday);
+ }
+ break;
+ default:
+ retval.rt_included_days.insert(token);
+ break;
+ }
+ base_token = RTT_INVALID;
+ } else {
+ retval.rt_included_days.insert(token);
+ }
+ if (retval.rt_duration == 0s) {
+ retval.rt_duration = 24h;
+ }
+ break;
+
+ case RTT__MAX:
+ ensure(false);
+ break;
+ }
+
+ if (token != RTT_NEXT && token != RTT_PREVIOUS
+ && token != RTT_WHITE)
+ {
+ next_set = false;
+ }
+
+ number_was_set = false;
+ seen_tokens.insert(token);
+ }
+
+ if (!found) {
+ pe_out.pe_msg = "Unrecognized input";
+ return Err(pe_out);
+ }
+
+ last_field_type = curr_field_type;
+ }
+}
+
+void
+relative_time::rollover()
+{
+ for (int lpc = 0; lpc < RTF_DAYS; lpc++) {
+ if (!this->rt_field[lpc].is_set) {
+ continue;
+ }
+ int64_t val = this->rt_field[lpc].value;
+ this->rt_field[lpc].value = val % TIME_SCALES[lpc];
+ this->rt_field[lpc + 1].value += val / TIME_SCALES[lpc];
+ if (this->rt_field[lpc + 1].value) {
+ this->rt_field[lpc + 1].is_set = true;
+ }
+ }
+ if (std::abs(this->rt_field[RTF_DAYS].value) > 31) {
+ int64_t val = this->rt_field[RTF_DAYS].value;
+ this->rt_field[RTF_DAYS].value = val % 31;
+ this->rt_field[RTF_MONTHS].value += val / 31;
+ if (this->rt_field[RTF_MONTHS].value) {
+ this->rt_field[RTF_MONTHS].is_set = true;
+ }
+ }
+ if (std::abs(this->rt_field[RTF_MONTHS].value) > 12) {
+ int64_t val = this->rt_field[RTF_MONTHS].value;
+ this->rt_field[RTF_MONTHS].value = val % 12;
+ this->rt_field[RTF_YEARS].value += val / 12;
+ if (this->rt_field[RTF_YEARS].value) {
+ this->rt_field[RTF_YEARS].is_set = true;
+ }
+ }
+}
+
+relative_time
+relative_time::from_timeval(const struct timeval& tv)
+{
+ relative_time retval;
+
+ retval.clear();
+ retval.rt_field[RTF_SECONDS] = tv.tv_sec;
+ retval.rt_field[RTF_MICROSECONDS] = tv.tv_usec;
+ retval.rollover();
+
+ return retval;
+}
+
+relative_time
+relative_time::from_usecs(std::chrono::microseconds usecs)
+{
+ relative_time retval;
+
+ retval.clear();
+ retval.rt_field[RTF_MICROSECONDS] = usecs.count();
+ retval.rollover();
+
+ return retval;
+}
+
+std::string
+relative_time::to_string() const
+{
+ static const char* DAYS[] = {
+ "sun",
+ "mon",
+ "tue",
+ "wed",
+ "thu",
+ "fri",
+ "sat",
+ };
+
+ char dst[128] = "";
+ char* pos = dst;
+
+ if (this->is_absolute()) {
+ for (const auto& day_token : this->rt_included_days) {
+ pos += snprintf(pos,
+ sizeof(dst) - (pos - dst),
+ "%s ",
+ DAYS[day_token - RTT_SUNDAY]);
+ }
+
+ pos += snprintf(
+ pos,
+ sizeof(dst) - (pos - dst),
+ "%s",
+ this->rt_next ? "next " : (this->rt_previous ? "last " : ""));
+ if (this->rt_field[RTF_YEARS].is_set
+ && (this->rt_next || this->rt_previous
+ || this->rt_field[RTF_YEARS].value != 0))
+ {
+ pos += snprintf(pos,
+ sizeof(dst) - (pos - dst),
+ "year %" PRId64 " ",
+ this->rt_field[RTF_YEARS].value);
+ } else if ((this->rt_next || this->rt_previous)
+ && this->rt_field[RTF_MONTHS].is_set)
+ {
+ pos += snprintf(pos, sizeof(dst) - (pos - dst), "year ");
+ }
+ if (this->rt_field[RTF_MONTHS].is_set
+ && (this->rt_next || this->rt_previous
+ || this->rt_field[RTF_MONTHS].value != 0))
+ {
+ pos += snprintf(pos,
+ sizeof(dst) - (pos - dst),
+ "month %" PRId64 " ",
+ this->rt_field[RTF_MONTHS].value);
+ } else if ((this->rt_next || this->rt_previous)
+ && this->rt_field[RTF_DAYS].is_set)
+ {
+ pos += snprintf(pos, sizeof(dst) - (pos - dst), "month ");
+ }
+ if (this->rt_field[RTF_DAYS].is_set
+ && (this->rt_next || this->rt_previous
+ || this->rt_field[RTF_DAYS].value != 0))
+ {
+ pos += snprintf(pos,
+ sizeof(dst) - (pos - dst),
+ "day %" PRId64 " ",
+ this->rt_field[RTF_DAYS].value);
+ } else if ((this->rt_next || this->rt_previous)
+ && this->rt_field[RTF_HOURS].is_set)
+ {
+ pos += snprintf(pos, sizeof(dst) - (pos - dst), "day ");
+ }
+ pos += snprintf(pos,
+ sizeof(dst) - (pos - dst),
+ "%" PRId64 ":%02" PRId64,
+ this->rt_field[RTF_HOURS].value,
+ this->rt_field[RTF_MINUTES].value);
+ if (this->rt_field[RTF_SECONDS].is_set
+ && this->rt_field[RTF_SECONDS].value != 0)
+ {
+ pos += snprintf(pos,
+ sizeof(dst) - (pos - dst),
+ ":%.02" PRId64,
+ this->rt_field[RTF_SECONDS].value);
+ if (this->rt_field[RTF_MICROSECONDS].is_set
+ && this->rt_field[RTF_MICROSECONDS].value != 0)
+ {
+ pos += snprintf(pos,
+ sizeof(dst) - (pos - dst),
+ ".%.03" PRId64,
+ this->rt_field[RTF_MICROSECONDS].value / 1000);
+ }
+ }
+ } else {
+ for (int lpc = RTF__MAX - 1; lpc >= 0; lpc--) {
+ if (this->rt_field[lpc].value == 0) {
+ continue;
+ }
+ pos += snprintf(pos,
+ sizeof(dst) - (pos - dst),
+ "%" PRId64 "%c",
+ this->rt_field[lpc].value,
+ FIELD_CHARS[lpc]);
+ }
+ }
+
+ if (dst[0] == '\0') {
+ dst[0] = '0';
+ dst[1] = 's';
+ dst[2] = '\0';
+ }
+
+ return dst;
+}
+
+struct exttm
+relative_time::adjust(const exttm& tm) const
+{
+ auto retval = tm;
+
+ if (this->rt_field[RTF_MICROSECONDS].is_set
+ && this->is_absolute(RTF_MICROSECONDS))
+ {
+ retval.et_nsec = this->rt_field[RTF_MICROSECONDS].value * 1000;
+ } else {
+ retval.et_nsec += this->rt_field[RTF_MICROSECONDS].value * 1000;
+ }
+ if (this->rt_field[RTF_SECONDS].is_set && this->is_absolute(RTF_SECONDS)) {
+ if (this->rt_next
+ && this->rt_field[RTF_SECONDS].value <= tm.et_tm.tm_sec)
+ {
+ retval.et_tm.tm_min += 1;
+ }
+ if (this->rt_previous
+ && this->rt_field[RTF_SECONDS].value >= tm.et_tm.tm_sec)
+ {
+ retval.et_tm.tm_min -= 1;
+ }
+ retval.et_tm.tm_sec = this->rt_field[RTF_SECONDS].value;
+ } else {
+ retval.et_tm.tm_sec += this->rt_field[RTF_SECONDS].value;
+ }
+ if (this->rt_field[RTF_MINUTES].is_set && this->is_absolute(RTF_MINUTES)) {
+ if (this->rt_next
+ && this->rt_field[RTF_MINUTES].value <= tm.et_tm.tm_min)
+ {
+ retval.et_tm.tm_hour += 1;
+ }
+ if (this->rt_previous
+ && (this->rt_field[RTF_MINUTES].value == 0
+ || (this->rt_field[RTF_MINUTES].value >= tm.et_tm.tm_min)))
+ {
+ retval.et_tm.tm_hour -= 1;
+ }
+ retval.et_tm.tm_min = this->rt_field[RTF_MINUTES].value;
+ } else {
+ retval.et_tm.tm_min += this->rt_field[RTF_MINUTES].value;
+ }
+ if (this->rt_field[RTF_HOURS].is_set && this->is_absolute(RTF_HOURS)) {
+ if (this->rt_next
+ && this->rt_field[RTF_HOURS].value <= tm.et_tm.tm_hour)
+ {
+ retval.et_tm.tm_mday += 1;
+ }
+ if (this->rt_previous
+ && this->rt_field[RTF_HOURS].value >= tm.et_tm.tm_hour)
+ {
+ retval.et_tm.tm_mday -= 1;
+ }
+ retval.et_tm.tm_hour = this->rt_field[RTF_HOURS].value;
+ } else {
+ retval.et_tm.tm_hour += this->rt_field[RTF_HOURS].value;
+ }
+ if (this->rt_field[RTF_DAYS].is_set && this->is_absolute(RTF_DAYS)) {
+ if (this->rt_next && this->rt_field[RTF_DAYS].value <= tm.et_tm.tm_mday)
+ {
+ retval.et_tm.tm_mon += 1;
+ }
+ if (this->rt_previous
+ && this->rt_field[RTF_DAYS].value >= tm.et_tm.tm_mday)
+ {
+ retval.et_tm.tm_mon -= 1;
+ }
+ retval.et_tm.tm_mday = this->rt_field[RTF_DAYS].value;
+ } else {
+ retval.et_tm.tm_mday += this->rt_field[RTF_DAYS].value;
+ }
+ if (this->rt_field[RTF_MONTHS].is_set && this->is_absolute(RTF_MONTHS)) {
+ if (this->rt_next
+ && this->rt_field[RTF_MONTHS].value <= tm.et_tm.tm_mon)
+ {
+ retval.et_tm.tm_year += 1;
+ }
+ if (this->rt_previous
+ && this->rt_field[RTF_MONTHS].value >= tm.et_tm.tm_mon)
+ {
+ retval.et_tm.tm_year -= 1;
+ }
+ retval.et_tm.tm_mon = this->rt_field[RTF_MONTHS].value;
+ } else {
+ retval.et_tm.tm_mon += this->rt_field[RTF_MONTHS].value;
+ }
+ if (this->rt_field[RTF_YEARS].is_set && this->is_absolute(RTF_YEARS)) {
+ retval.et_tm.tm_year = this->rt_field[RTF_YEARS].value;
+ } else {
+ retval.et_tm.tm_year += this->rt_field[RTF_YEARS].value;
+ }
+
+ return retval;
+}
+
+nonstd::optional<exttm>
+relative_time::window_start(const struct exttm& tm) const
+{
+ auto retval = tm;
+
+ if (this->is_relative()) {
+ uint64_t us, remainder;
+
+ auto tv = tm.to_timeval();
+ us = (uint64_t) tv.tv_sec * 1000000ULL + (uint64_t) tv.tv_usec;
+ remainder = us % this->to_microseconds();
+ us -= remainder;
+
+ tv.tv_sec = us / 1000000ULL;
+ tv.tv_usec = us % 1000000ULL;
+
+ retval.et_tm = *gmtime(&tv.tv_sec);
+ retval.et_nsec = tv.tv_usec * 1000ULL;
+
+ return retval;
+ }
+
+ bool clear = false;
+
+ if (this->rt_field[RTF_YEARS].is_set) {
+ if (this->rt_field[RTF_YEARS].value > tm.et_tm.tm_year) {
+ return nonstd::nullopt;
+ }
+ retval.et_tm.tm_year = this->rt_field[RTF_YEARS].value;
+ clear = true;
+ }
+
+ if (this->rt_field[RTF_MONTHS].is_set) {
+ if (this->rt_field[RTF_MONTHS].value > tm.et_tm.tm_mon) {
+ return nonstd::nullopt;
+ }
+ retval.et_tm.tm_mon = this->rt_field[RTF_MONTHS].value;
+ clear = true;
+ } else if (clear) {
+ retval.et_tm.tm_mon = 0;
+ }
+
+ if (this->rt_field[RTF_DAYS].is_set) {
+ if (this->rt_field[RTF_DAYS].value > tm.et_tm.tm_mday) {
+ return nonstd::nullopt;
+ }
+ retval.et_tm.tm_mday = this->rt_field[RTF_DAYS].value;
+ clear = true;
+ } else if (clear) {
+ retval.et_tm.tm_mday = 1;
+ }
+
+ if (!this->rt_included_days.empty()) {
+ auto iter = this->rt_included_days.find(
+ (token_t) (RTT_SUNDAY + tm.et_tm.tm_wday));
+
+ if (iter == this->rt_included_days.end()) {
+ return nonstd::nullopt;
+ }
+ clear = true;
+ }
+
+ if (this->rt_field[RTF_HOURS].is_set) {
+ if (this->rt_field[RTF_HOURS].value > tm.et_tm.tm_hour) {
+ return nonstd::nullopt;
+ }
+ retval.et_tm.tm_hour = this->rt_field[RTF_HOURS].value;
+ clear = true;
+ } else if (clear) {
+ retval.et_tm.tm_hour = 0;
+ }
+
+ if (this->rt_field[RTF_MINUTES].is_set) {
+ if (this->rt_field[RTF_MINUTES].value > tm.et_tm.tm_min) {
+ return nonstd::nullopt;
+ }
+ retval.et_tm.tm_min = this->rt_field[RTF_MINUTES].value;
+ clear = true;
+ } else if (clear) {
+ retval.et_tm.tm_min = 0;
+ }
+
+ if (this->rt_field[RTF_SECONDS].is_set) {
+ if (this->rt_field[RTF_SECONDS].value > tm.et_tm.tm_sec) {
+ return nonstd::nullopt;
+ }
+ retval.et_tm.tm_sec = this->rt_field[RTF_SECONDS].value;
+ clear = true;
+ } else if (clear) {
+ retval.et_tm.tm_sec = 0;
+ }
+
+ if (this->rt_field[RTF_MICROSECONDS].is_set) {
+ if (this->rt_field[RTF_MICROSECONDS].value > tm.et_nsec / 1000) {
+ return nonstd::nullopt;
+ }
+ retval.et_nsec = this->rt_field[RTF_MICROSECONDS].value * 1000ULL;
+ clear = true;
+ } else if (clear) {
+ retval.et_nsec = 0;
+ }
+
+ auto tv = tm.to_timeval();
+ auto start_time = retval.to_timeval();
+ auto end_time = relative_time::from_usecs(this->rt_duration)
+ .adjust(retval)
+ .to_timeval();
+
+ if (tv < start_time || end_time < tv) {
+ return nonstd::nullopt;
+ }
+
+ return retval;
+}
+
+int64_t
+relative_time::to_microseconds() const
+{
+ int64_t retval;
+
+ if (this->is_absolute()) {
+ struct exttm etm;
+
+ memset(&etm, 0, sizeof(etm));
+ etm.et_tm.tm_year = this->rt_field[RTF_YEARS].value;
+ etm.et_tm.tm_mon = this->rt_field[RTF_MONTHS].value;
+ if (this->rt_field[RTF_DAYS].is_set) {
+ etm.et_tm.tm_mday = this->rt_field[RTF_DAYS].value;
+ } else {
+ etm.et_tm.tm_mday = 1;
+ }
+ etm.et_tm.tm_min = this->rt_field[RTF_MINUTES].value;
+ etm.et_tm.tm_sec = this->rt_field[RTF_SECONDS].value;
+
+ auto epoch_secs = std::chrono::seconds(tm2sec(&etm.et_tm));
+ retval
+ = std::chrono::duration_cast<std::chrono::microseconds>(epoch_secs)
+ .count();
+ retval += this->rt_field[RTF_MICROSECONDS].value;
+ } else {
+ retval = this->rt_field[RTF_YEARS].value * 12;
+ retval = (retval + this->rt_field[RTF_MONTHS].value) * 30;
+ retval = (retval + this->rt_field[RTF_DAYS].value) * 24;
+ retval = (retval + this->rt_field[RTF_HOURS].value) * 60;
+ retval = (retval + this->rt_field[RTF_MINUTES].value) * 60;
+ retval = (retval + this->rt_field[RTF_SECONDS].value) * 1000 * 1000;
+ retval = (retval + this->rt_field[RTF_MICROSECONDS].value);
+ }
+
+ return retval;
+}
diff --git a/src/relative_time.hh b/src/relative_time.hh
new file mode 100644
index 0000000..7334217
--- /dev/null
+++ b/src/relative_time.hh
@@ -0,0 +1,263 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef LNAV_RELATIVE_TIME_HH
+#define LNAV_RELATIVE_TIME_HH
+
+#include <sys/time.h>
+
+#define __STDC_FORMAT_MACROS
+#include <array>
+#include <chrono>
+#include <set>
+#include <string>
+
+#include <inttypes.h>
+
+#include "base/intern_string.hh"
+#include "base/result.h"
+#include "ptimec.hh"
+
+class relative_time {
+public:
+ enum token_t {
+ RTT_INVALID = -1,
+
+ RTT_WHITE,
+ RTT_AM,
+ RTT_PM,
+ RTT_A,
+ RTT_AN,
+ RTT_AT,
+ RTT_TIME,
+ RTT_NUMBER,
+
+ RTT_SUNDAY,
+ RTT_MONDAY,
+ RTT_TUESDAY,
+ RTT_WEDNESDAY,
+ RTT_THURSDAY,
+ RTT_FRIDAY,
+ RTT_SATURDAY,
+
+ RTT_MICROS,
+ RTT_MILLIS,
+ RTT_SECONDS,
+ RTT_MINUTES,
+ RTT_HOURS,
+ RTT_DAYS,
+ RTT_WEEKS,
+ RTT_MONTHS,
+ RTT_YEARS,
+ RTT_TODAY,
+ RTT_YESTERDAY,
+ RTT_TOMORROW,
+ RTT_NOON,
+ RTT_AND,
+ RTT_THE,
+ RTT_AGO,
+ RTT_LATER,
+ RTT_BEFORE,
+ RTT_AFTER,
+ RTT_NOW,
+ RTT_HERE,
+ RTT_NEXT,
+ RTT_PREVIOUS,
+
+ RTT__MAX
+ };
+
+ enum rt_field_type {
+ RTF_MICROSECONDS,
+ RTF_SECONDS,
+ RTF_MINUTES,
+ RTF_HOURS,
+ RTF_DAYS,
+ RTF_MONTHS,
+ RTF_YEARS,
+
+ RTF__MAX
+ };
+
+ struct parse_error {
+ int pe_column;
+ std::string pe_msg;
+ };
+
+ static Result<relative_time, parse_error> from_str(string_fragment str);
+
+ static relative_time from_timeval(const struct timeval& tv);
+
+ static relative_time from_usecs(std::chrono::microseconds usecs);
+
+ relative_time() { this->clear(); }
+
+ void clear()
+ {
+ this->rt_field.fill({});
+ this->rt_next = false;
+ this->rt_previous = false;
+ this->rt_absolute_field_end = 0;
+ }
+
+ void negate()
+ {
+ if (this->is_absolute()) {
+ if (this->rt_next) {
+ this->rt_next = false;
+ this->rt_previous = true;
+ } else if (this->rt_previous) {
+ this->rt_next = true;
+ this->rt_previous = false;
+ }
+ return;
+ }
+ for (int lpc = 0; lpc < RTF__MAX; lpc++) {
+ if (this->rt_field[lpc].value != 0) {
+ this->rt_field[lpc].value = -this->rt_field[lpc].value;
+ }
+ }
+ }
+
+ bool is_negative() const
+ {
+ if (this->rt_previous) {
+ return true;
+ }
+ for (const auto& rtf : this->rt_field) {
+ if (rtf.value < 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool is_absolute() const
+ {
+ return !this->rt_included_days.empty()
+ || this->rt_absolute_field_end > 0;
+ }
+
+ bool is_absolute(rt_field_type rft) const
+ {
+ return rft < this->rt_absolute_field_end;
+ }
+
+ bool is_relative() const
+ {
+ return !this->is_absolute() || this->rt_next || this->rt_previous;
+ }
+
+ bool empty() const
+ {
+ if (!this->rt_included_days.empty()) {
+ return false;
+ }
+ for (const auto& rtf : this->rt_field) {
+ if (rtf.is_set) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ struct exttm adjust_now() const
+ {
+ struct exttm tm;
+ time_t now;
+
+ time(&now);
+ tm.et_tm = *gmtime(&now);
+ return this->adjust(tm);
+ }
+
+ struct exttm adjust(const struct timeval& tv) const
+ {
+ struct exttm tm;
+
+ tm.et_tm = *gmtime(&tv.tv_sec);
+ tm.et_nsec = tv.tv_usec * 1000;
+ return this->adjust(tm);
+ }
+
+ struct exttm adjust(const struct exttm& tm) const;
+
+ nonstd::optional<exttm> window_start(const struct exttm& tm) const;
+
+ int64_t to_microseconds() const;
+
+ void to_timeval(struct timeval& tv_out) const
+ {
+ int64_t us = this->to_microseconds();
+
+ tv_out.tv_sec = us / (1000 * 1000);
+ tv_out.tv_usec = us % (1000 * 1000);
+ }
+
+ struct timeval to_timeval() const
+ {
+ int64_t us = this->to_microseconds();
+ struct timeval retval;
+
+ retval.tv_sec = us / (1000 * 1000);
+ retval.tv_usec = us % (1000 * 1000);
+ return retval;
+ }
+
+ std::string to_string() const;
+
+ void rollover();
+
+ static const char FIELD_CHARS[RTF__MAX];
+
+ struct _rt_field {
+ _rt_field(int64_t value) : value(value), is_set(true) {}
+
+ _rt_field() : value(0), is_set(false) {}
+
+ void clear()
+ {
+ this->value = 0;
+ this->is_set = false;
+ }
+
+ int64_t value;
+ bool is_set;
+ };
+
+ std::array<_rt_field, RTF__MAX> rt_field;
+ std::set<token_t> rt_included_days;
+ std::chrono::microseconds rt_duration{0};
+
+ bool rt_next;
+ bool rt_previous;
+ int rt_absolute_field_end;
+};
+
+#endif // LNAV_RELATIVE_TIME_HH
diff --git a/src/remote/CMakeLists.txt b/src/remote/CMakeLists.txt
new file mode 100644
index 0000000..a29fbde
--- /dev/null
+++ b/src/remote/CMakeLists.txt
@@ -0,0 +1,5 @@
+add_library(remote STATIC ../config.h.in remote.ssh.cc remote.ssh.hh)
+
+target_include_directories(remote PUBLIC . .. ../fmtlib
+ ${CMAKE_CURRENT_BINARY_DIR}/..)
+target_link_libraries(remote cppfmt pcre::libpcre)
diff --git a/src/remote/Makefile.am b/src/remote/Makefile.am
new file mode 100644
index 0000000..0080c05
--- /dev/null
+++ b/src/remote/Makefile.am
@@ -0,0 +1,20 @@
+
+include $(top_srcdir)/aminclude_static.am
+
+AM_CPPFLAGS = \
+ $(CODE_COVERAGE_CPPFLAGS) \
+ -Wall \
+ -I$(top_srcdir)/src/ \
+ -I$(top_srcdir)/src/fmtlib
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS)
+
+noinst_LIBRARIES = libremote.a
+
+noinst_HEADERS = \
+ remote.ssh.hh
+
+libremote_a_SOURCES = \
+ remote.ssh.cc
diff --git a/src/remote/remote.ssh.cc b/src/remote/remote.ssh.cc
new file mode 100644
index 0000000..0aa95af
--- /dev/null
+++ b/src/remote/remote.ssh.cc
@@ -0,0 +1,38 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "remote.ssh.hh"
+
+#include "config.h"
+
+namespace remote {
+namespace ssh {
+
+}
+} // namespace remote
diff --git a/src/remote/remote.ssh.hh b/src/remote/remote.ssh.hh
new file mode 100644
index 0000000..cfa590f
--- /dev/null
+++ b/src/remote/remote.ssh.hh
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_remote_ssh_hh
+#define lnav_remote_ssh_hh
+
+#include "base/result.h"
+
+namespace remote {
+namespace ssh {
+
+}
+} // namespace remote
+
+#endif
diff --git a/src/ring_span.hh b/src/ring_span.hh
new file mode 100644
index 0000000..0bd07a6
--- /dev/null
+++ b/src/ring_span.hh
@@ -0,0 +1,946 @@
+//
+// Copyright (c) 2015 Arthur O'Dwyer
+// Copyright 2017-2018 by Martin Moene
+//
+// https://github.com/martinmoene/ring-span-lite
+//
+// Distributed under the Boost Software License, Version 1.0.
+// (See accompanying file LICENSE.txt or copy at
+// http://www.boost.org/LICENSE_1_0.txt)
+
+#pragma once
+
+#ifndef NONSTD_RING_SPAN_LITE_HPP
+# define NONSTD_RING_SPAN_LITE_HPP
+
+# define ring_span_lite_MAJOR 0
+# define ring_span_lite_MINOR 2
+# define ring_span_lite_PATCH 0
+
+# define ring_span_lite_VERSION \
+ nsrs_STRINGIFY(ring_span_lite_MAJOR) "." nsrs_STRINGIFY( \
+ ring_span_lite_MINOR) "." nsrs_STRINGIFY(ring_span_lite_PATCH)
+
+# define nsrs_STRINGIFY(x) nsrs_STRINGIFY_(x)
+# define nsrs_STRINGIFY_(x) # x
+
+// ring-span-lite configuration:
+
+# define nsrs_RING_SPAN_DEFAULT 0
+# define nsrs_RING_SPAN_NONSTD 1
+# define nsrs_RING_SPAN_STD 2
+
+# if !defined(nsrs_CONFIG_SELECT_RING_SPAN)
+# define nsrs_CONFIG_SELECT_RING_SPAN \
+ (nsrs_HAVE_STD_RING_SPAN ? nsrs_RING_SPAN_STD \
+ : nsrs_RING_SPAN_NONSTD)
+# endif
+
+# ifndef nsrs_CONFIG_STRICT_P0059
+# define nsrs_CONFIG_STRICT_P0059 0
+# endif
+
+# define nsrs_RING_SPAN_LITE_EXTENSION (!nsrs_CONFIG_STRICT_P0059)
+
+# ifndef nsrs_CONFIG_CONFIRMS_COMPILATION_ERRORS
+# define nsrs_CONFIG_CONFIRMS_COMPILATION_ERRORS 0
+# endif
+
+// C++ language version detection (C++20 is speculative):
+// Note: VC14.0/1900 (VS2015) lacks too much from C++14.
+
+# ifndef nsrs_CPLUSPLUS
+# if defined(_MSVC_LANG) && !defined(__clang__)
+# define nsrs_CPLUSPLUS (_MSC_VER == 1900 ? 201103L : _MSVC_LANG)
+# else
+# define nsrs_CPLUSPLUS __cplusplus
+# endif
+# endif
+
+# define nsrs_CPP98_OR_GREATER (nsrs_CPLUSPLUS >= 199711L)
+# define nsrs_CPP11_OR_GREATER (nsrs_CPLUSPLUS >= 201103L)
+# define nsrs_CPP11_OR_GREATER_ (nsrs_CPLUSPLUS >= 201103L)
+# define nsrs_CPP14_OR_GREATER (nsrs_CPLUSPLUS >= 201402L)
+# define nsrs_CPP17_OR_GREATER (nsrs_CPLUSPLUS >= 201703L)
+# define nsrs_CPP20_OR_GREATER (nsrs_CPLUSPLUS >= 202000L)
+
+// Use C++XX std::ring_span if available and requested:
+
+# define nsrs_HAVE_STD_RING_SPAN 0
+
+//#if nsrs_CPP17_OR_GREATER && defined(__has_include )
+//# if __has_include( <any> )
+//# define nsrs_HAVE_STD_RING_SPAN 1
+//# else
+//# define nsrs_HAVE_STD_RING_SPAN 0
+//# endif
+//#else
+//# define nsrs_HAVE_STD_RING_SPAN 0
+//#endif
+
+# define nsrs_USES_STD_RING_SPAN \
+ ((nsrs_CONFIG_SELECT_RING_SPAN == nsrs_RING_SPAN_STD) \
+ || ((nsrs_CONFIG_SELECT_RING_SPAN == nsrs_RING_SPAN_DEFAULT) \
+ && nsrs_HAVE_STD_RING_SPAN))
+
+// Compiler versions:
+//
+// MSVC++ 6.0 _MSC_VER == 1200 (Visual Studio 6.0)
+// MSVC++ 7.0 _MSC_VER == 1300 (Visual Studio .NET 2002)
+// MSVC++ 7.1 _MSC_VER == 1310 (Visual Studio .NET 2003)
+// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005)
+// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008)
+// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010)
+// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012)
+// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013)
+// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015)
+// ............_MSVC_LANG: 201402 for -std:c++14, default
+// MSVC++ 14.1 _MSC_VER >= 1910 (Visual Studio 2017)
+// ............_MSVC_LANG: 201402 for -std:c++14, default
+// ............_MSVC_LANG: 201703 for -std:c++17
+
+# if defined(_MSC_VER) && !defined(__clang__)
+# define nsrs_COMPILER_MSVC_VER (_MSC_VER)
+# define nsrs_COMPILER_MSVC_VERSION \
+ (_MSC_VER / 10 - 10 * (5 + (_MSC_VER < 1900)))
+# else
+# define nsrs_COMPILER_MSVC_VER 0
+# define nsrs_COMPILER_MSVC_VERSION 0
+# endif
+
+# define nsrs_COMPILER_VERSION(major, minor, patch) \
+ (10 * (10 * major + minor) + patch)
+
+# if defined(__clang__)
+# define nsrs_COMPILER_CLANG_VERSION \
+ nsrs_COMPILER_VERSION( \
+ __clang_major__, __clang_minor__, __clang_patchlevel__)
+# else
+# define nsrs_COMPILER_CLANG_VERSION 0
+# endif
+
+# if defined(__GNUC__) && !defined(__clang__)
+# define nsrs_COMPILER_GNUC_VERSION \
+ nsrs_COMPILER_VERSION(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+# else
+# define nsrs_COMPILER_GNUC_VERSION 0
+# endif
+
+// half-open range [lo..hi):
+//#define nsrs_BETWEEN( v, lo, hi ) ( (lo) <= (v) && (v) < (hi) )
+
+// Presence of language and library features:
+
+# ifdef _HAS_CPP0X
+# define nsrs_HAS_CPP0X _HAS_CPP0X
+# else
+# define nsrs_HAS_CPP0X 0
+# endif
+
+// Unless defined otherwise below, consider VC14 as C++11 for ring-span-lite:
+
+# if nsrs_COMPILER_MSVC_VER >= 1900
+# undef nsrs_CPP11_OR_GREATER
+# define nsrs_CPP11_OR_GREATER 1
+# endif
+
+# define nsrs_CPP11_90 \
+ (nsrs_CPP11_OR_GREATER_ || nsrs_COMPILER_MSVC_VER >= 1500)
+# define nsrs_CPP11_100 \
+ (nsrs_CPP11_OR_GREATER_ || nsrs_COMPILER_MSVC_VER >= 1600)
+# define nsrs_CPP11_110 \
+ (nsrs_CPP11_OR_GREATER_ || nsrs_COMPILER_MSVC_VER >= 1700)
+# define nsrs_CPP11_120 \
+ (nsrs_CPP11_OR_GREATER_ || nsrs_COMPILER_MSVC_VER >= 1800)
+# define nsrs_CPP11_140 \
+ (nsrs_CPP11_OR_GREATER_ || nsrs_COMPILER_MSVC_VER >= 1900)
+
+# define nsrs_CPP14_000 (nsrs_CPP14_OR_GREATER)
+# define nsrs_CPP17_000 (nsrs_CPP17_OR_GREATER)
+
+// Presence of C++11 language features:
+
+// half-open range [lo..hi):
+# define nsrs_BETWEEN(v, lo, hi) ((lo) <= (v) && (v) < (hi))
+
+// Presence of C++11 language features:
+
+# define nsrs_HAVE_CONSTEXPR_11 nsrs_CPP11_140
+# define nsrs_HAVE_IS_DEFAULT nsrs_CPP11_140
+# define nsrs_HAVE_IS_DELETE nsrs_CPP11_140
+# define nsrs_HAVE_NOEXCEPT nsrs_CPP11_140
+# define nsrs_HAVE_NULLPTR nsrs_CPP11_100
+
+// Presence of C++14 language features:
+
+# define nsrs_HAVE_CONSTEXPR_14 nsrs_CPP14_000
+
+// Presence of C++17 language features:
+// no tag
+
+// Presence of C++ library features:
+// no tag
+
+// Compiler warning suppression:
+
+# if defined(__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wundef"
+# define nsrs_RESTORE_WARNINGS() _Pragma("clang diagnostic pop")
+
+# elif defined __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wundef"
+# define nsrs_RESTORE_WARNINGS() _Pragma("GCC diagnostic pop")
+
+# elif nsrs_COMPILER_MSVC_VERSION >= 140
+# define nsrs_DISABLE_MSVC_WARNINGS(codes) \
+ __pragma(warning(push)) __pragma(warning(disable : codes))
+# define nsrs_RESTORE_WARNINGS() __pragma(warning(pop))
+
+// Suppress the following MSVC warnings:
+// - C4345: initialization behavior changed
+//
+// Suppress the following MSVC GSL warnings:
+// - C26439, gsl::f.6 : special function 'function' can be declared 'noexcept'
+// - C26440, gsl::f.6 : function 'function' can be declared 'noexcept'
+// - C26472, gsl::t.1 : don't use a static_cast for arithmetic conversions;
+// use brace initialization, gsl::narrow_cast or
+// gsl::narrow
+// - C26473: gsl::t.1 : don't cast between pointer types where the source type
+// and the target type are the same
+// - C26481: gsl::b.1 : don't use pointer arithmetic. Use span instead
+// - C26490: gsl::t.1 : don't use reinterpret_cast
+
+nsrs_DISABLE_MSVC_WARNINGS(4345 26439 26440 26472 26473 26481 26490)
+
+# else
+# define nsrs_RESTORE_WARNINGS() /*empty*/
+# endif
+
+// C++ feature usage:
+
+# if nsrs_HAVE_CONSTEXPR_11
+# define nsrs_constexpr constexpr
+# else
+# define nsrs_constexpr /*constexpr*/
+# endif
+
+# if nsrs_HAVE_CONSTEXPR_14
+# define nsrs_constexpr14 constexpr
+# else
+# define nsrs_constexpr14 /*constexpr*/
+# endif
+
+# if nsrs_HAVE_NOEXCEPT
+# define nsrs_noexcept noexcept
+# define nsrs_noexcept_op noexcept
+# else
+# define nsrs_noexcept /*noexcept*/
+# define nsrs_noexcept_op(expr) /*noexcept(expr)*/
+# endif
+
+# if nsrs_HAVE_NULLPTR
+# define nsrs_nullptr nullptr
+# else
+# define nsrs_nullptr NULL
+# endif
+
+// includes:
+
+# include <cassert>
+# include <iterator>
+# include <utility>
+
+// additional includes:
+
+# if !nsrs_CPP11_OR_GREATER
+# include <algorithm> // std::swap() until C++11
+# endif
+
+namespace nonstd {
+
+# if nsrs_CPP11_OR_GREATER
+using std::move;
+# else
+template<typename T>
+T const&
+move(T const& t)
+{
+ return t;
+}
+# endif
+
+template<bool B, class T, class F>
+struct conditional {
+ typedef T type;
+};
+
+template<class T, class F>
+struct conditional<false, T, F> {
+ typedef F type;
+};
+
+# if nsrs_CPP11_OR_GREATER
+
+template<bool B, class T, class F>
+using conditional_t = typename conditional<B, T, F>::type;
+
+template<bool B, class T = void>
+using enable_if_t = typename std::enable_if<B, T>::type;
+
+# endif
+
+//
+// element extraction policies:
+//
+
+template<class T>
+struct null_popper {
+ typedef void return_type;
+
+ void operator()(T&) const nsrs_noexcept {}
+};
+
+template<class T>
+struct default_popper {
+ typedef T return_type;
+
+ T operator()(T& t) const
+ {
+ return nonstd::move(t);
+ }
+};
+
+template<class T>
+struct copy_popper {
+ typedef T return_type;
+
+# if nsrs_RING_SPAN_LITE_EXTENSION
+# if nsrs_CPP11_OR_GREATER
+ copy_popper(T t) : m_copy(std::move(t)) {}
+# else
+ copy_popper(T const& t) : m_copy(t) {}
+# endif
+# else
+ copy_popper(T&& t) : copy(std::move(t)) {}
+# endif
+
+ T operator()(T& t) const
+ {
+ using std::swap;
+ T result(m_copy);
+ swap(t, result);
+ return result;
+ }
+
+ T m_copy;
+};
+
+// forward-declare iterator:
+
+namespace ring_detail {
+
+template<class, bool>
+class ring_iterator;
+
+}
+
+//
+// ring span:
+//
+template<class T, class Popper = default_popper<T> >
+class ring_span {
+public:
+ typedef T value_type;
+ typedef T* pointer;
+ typedef T& reference;
+ typedef T const& const_reference;
+
+ typedef std::size_t size_type;
+
+ typedef ring_span<T, Popper> type;
+
+ typedef ring_detail::ring_iterator<type, false> iterator;
+ typedef ring_detail::ring_iterator<type, true> const_iterator;
+
+# if nsrs_RING_SPAN_LITE_EXTENSION
+ typedef std::reverse_iterator<iterator> reverse_iterator;
+ typedef std::reverse_iterator<const_iterator> const_reverse_iterator;
+# endif
+
+ // construction:
+
+ template<class ContiguousIterator>
+ ring_span(ContiguousIterator begin,
+ ContiguousIterator end,
+ Popper popper = Popper()) nsrs_noexcept
+ : m_data(&*begin)
+ , m_size(0)
+ , m_capacity(static_cast<size_type>(end - begin))
+ , m_front_idx(0)
+ , m_popper(nonstd::move(popper))
+ {
+ }
+
+ template<class ContiguousIterator>
+ ring_span(ContiguousIterator begin,
+ ContiguousIterator end,
+ ContiguousIterator first,
+ size_type size,
+ Popper popper = Popper()) nsrs_noexcept
+ : m_data(&*begin)
+ , m_size(size)
+ , m_capacity(static_cast<size_type>(end - begin))
+ , m_front_idx(static_cast<size_type>(first - begin))
+ , m_popper(nonstd::move(popper))
+ {
+ }
+
+# if nsrs_HAVE_IS_DEFAULT
+ ring_span(ring_span&&) = default;
+ ring_span& operator=(ring_span&&) = default;
+# else
+private:
+ ring_span(ring_span const&);
+ ring_span& operator=(ring_span const&);
+
+public:
+# endif
+
+ // observers:
+
+ bool empty() const nsrs_noexcept
+ {
+ return m_size == 0;
+ }
+
+ bool full() const nsrs_noexcept
+ {
+ return m_size == m_capacity;
+ }
+
+ size_type size() const nsrs_noexcept
+ {
+ return m_size;
+ }
+
+ size_type capacity() const nsrs_noexcept
+ {
+ return m_capacity;
+ }
+
+ // element access:
+
+ reference front() nsrs_noexcept
+ {
+ return *begin();
+ }
+
+ const_reference front() const nsrs_noexcept
+ {
+ return *begin();
+ }
+
+ reference back() nsrs_noexcept
+ {
+ return *(--end());
+ }
+
+ const_reference back() const nsrs_noexcept
+ {
+ return *(--end());
+ }
+
+ // iteration:
+
+ iterator begin() nsrs_noexcept
+ {
+ return iterator(0, this);
+ }
+
+ const_iterator begin() const nsrs_noexcept
+ {
+ return cbegin();
+ }
+
+ const_iterator cbegin() const nsrs_noexcept
+ {
+ return const_iterator(0, this);
+ }
+
+ iterator end() nsrs_noexcept
+ {
+ return iterator(size(), this);
+ }
+
+ const_iterator end() const nsrs_noexcept
+ {
+ return cend();
+ }
+
+ const_iterator cend() const nsrs_noexcept
+ {
+ return const_iterator(size(), this);
+ }
+
+# if nsrs_RING_SPAN_LITE_EXTENSION
+
+ reverse_iterator rbegin() nsrs_noexcept
+ {
+ return reverse_iterator(end());
+ }
+
+ reverse_iterator rend() nsrs_noexcept
+ {
+ return reverse_iterator(begin());
+ }
+
+ const_reverse_iterator rbegin() const nsrs_noexcept
+ {
+ return crbegin();
+ }
+
+ const_reverse_iterator rend() const nsrs_noexcept
+ {
+ return crend();
+ }
+
+ const_reverse_iterator crbegin() const nsrs_noexcept
+ {
+ return const_reverse_iterator(cend());
+ }
+
+ const_reverse_iterator crend() const nsrs_noexcept
+ {
+ return const_reverse_iterator(cbegin());
+ }
+# endif
+
+ void erase_from(iterator first)
+ {
+ m_size = static_cast<size_type>(std::distance(begin(), first));
+ }
+
+ // element insertion, extraction:
+
+ typename Popper::return_type pop_front()
+ {
+ assert(!empty());
+
+ reference element = front_();
+ increment_front_();
+
+ return m_popper(element);
+ }
+
+# if nsrs_RING_SPAN_LITE_EXTENSION
+ typename Popper::return_type pop_back()
+ {
+ assert(!empty());
+
+ reference element = back_();
+ decrement_back_();
+
+ return m_popper(element);
+ }
+# endif
+
+# if nsrs_CPP11_OR_GREATER
+ template<bool b = true,
+ typename
+ = nonstd::enable_if_t<b && std::is_copy_assignable<T>::value> >
+ void push_back(value_type const& value)
+ nsrs_noexcept_op((std::is_nothrow_copy_assignable<T>::value))
+# else
+ void push_back(value_type const& value)
+# endif
+ {
+ if (full())
+ increment_front_and_back_();
+ else
+ increment_back_();
+
+ back_() = value;
+ }
+
+# if nsrs_CPP11_OR_GREATER
+ template<bool b = true,
+ typename
+ = nonstd::enable_if_t<b && std::is_move_assignable<T>::value> >
+ void push_back(value_type&& value)
+ nsrs_noexcept_op((std::is_nothrow_move_assignable<T>::value))
+ {
+ if (full())
+ increment_front_and_back_();
+ else
+ increment_back_();
+
+ back_() = std::move(value);
+ }
+
+ template<class... Args>
+ void emplace_back(Args&&... args)
+ nsrs_noexcept_op((std::is_nothrow_constructible<T, Args...>::value
+ && std::is_nothrow_move_assignable<T>::value))
+ {
+ if (full())
+ increment_front_and_back_();
+ else
+ increment_back_();
+
+ back_() = T(std::forward<Args>(args)...);
+ }
+# endif
+
+# if nsrs_RING_SPAN_LITE_EXTENSION
+
+# if nsrs_CPP11_OR_GREATER
+ template<bool b = true,
+ typename
+ = nonstd::enable_if_t<b && std::is_copy_assignable<T>::value> >
+ void push_front(T const& value)
+ nsrs_noexcept_op((std::is_nothrow_copy_assignable<T>::value))
+# else
+ void push_front(T const& value)
+# endif
+ {
+ if (full())
+ decrement_front_and_back_();
+ else
+ decrement_front_();
+
+ front_() = value;
+ }
+
+# if nsrs_CPP11_OR_GREATER
+ template<bool b = true,
+ typename
+ = nonstd::enable_if_t<b && std::is_move_assignable<T>::value> >
+ void push_front(T&& value)
+ nsrs_noexcept_op((std::is_nothrow_move_assignable<T>::value))
+ {
+ if (full())
+ decrement_front_and_back_();
+ else
+ decrement_front_();
+
+ front_() = std::move(value);
+ }
+
+ template<typename... Args>
+ void emplace_front(Args&&... args)
+ nsrs_noexcept_op((std::is_nothrow_constructible<T, Args...>::value
+ && std::is_nothrow_move_assignable<T>::value))
+ {
+ if (full())
+ decrement_front_and_back_();
+ else
+ decrement_front_();
+
+ front_() = T(std::forward<Args>(args)...);
+ }
+# endif
+# endif // nsrs_RING_SPAN_LITE_EXTENSION
+
+ // swap:
+
+ void swap(type& rhs)
+ nsrs_noexcept // nsrs_noexcept_op(std::is_nothrow_swappable<Popper>::value);
+ {
+ using std::swap;
+ swap(m_data, rhs.m_data);
+ swap(m_size, rhs.m_size);
+ swap(m_capacity, rhs.m_capacity);
+ swap(m_front_idx, rhs.m_front_idx);
+ swap(m_popper, rhs.m_popper);
+ }
+
+private:
+ friend class ring_detail::ring_iterator<ring_span,
+ true>; // const_iterator;
+ friend class ring_detail::ring_iterator<ring_span, false>; // iterator;
+
+ size_type normalize_(size_type const idx) const nsrs_noexcept
+ {
+ return idx % m_capacity;
+ }
+
+ reference at(size_type idx) nsrs_noexcept
+ {
+ return m_data[normalize_(m_front_idx + idx)];
+ }
+
+ const_reference at(size_type idx) const nsrs_noexcept
+ {
+ return m_data[normalize_(m_front_idx + idx)];
+ }
+
+ reference front_() nsrs_noexcept
+ {
+ return *(m_data + m_front_idx);
+ }
+
+ const_reference front_() const nsrs_noexcept
+ {
+ return *(m_data + m_front_idx);
+ }
+
+ reference back_() nsrs_noexcept
+ {
+ return *(m_data + normalize_(m_front_idx + m_size - 1));
+ }
+
+ const_reference back_() const nsrs_noexcept
+ {
+ return *(m_data + normalize_(m_front_idx + m_size - 1));
+ }
+
+ void increment_front_() nsrs_noexcept
+ {
+ m_front_idx = normalize_(m_front_idx + 1);
+ --m_size;
+ }
+
+ void decrement_front_() nsrs_noexcept
+ {
+ m_front_idx = normalize_(m_front_idx + m_capacity - 1);
+ ++m_size;
+ }
+
+ void increment_back_() nsrs_noexcept
+ {
+ ++m_size;
+ }
+
+ void decrement_back_() nsrs_noexcept
+ {
+ --m_size;
+ }
+
+ void increment_front_and_back_() nsrs_noexcept
+ {
+ m_front_idx = normalize_(m_front_idx + 1);
+ }
+
+ void decrement_front_and_back_() nsrs_noexcept
+ {
+ m_front_idx = normalize_(m_front_idx + m_capacity - 1);
+ }
+
+private:
+ pointer m_data;
+ size_type m_size;
+ size_type m_capacity;
+ size_type m_front_idx;
+ Popper m_popper;
+};
+
+// swap:
+
+template<class T, class Popper>
+inline void
+swap(ring_span<T, Popper>& lhs, ring_span<T, Popper>& rhs)
+ nsrs_noexcept_op(nsrs_noexcept_op(lhs.swap(rhs)))
+{
+ lhs.swap(rhs);
+}
+
+namespace ring_detail {
+
+//
+// ring iterator:
+//
+# if 0
+template< class RS, bool is_const >
+class ring_iterator : public std::iterator
+<
+ std::random_access_iterator_tag
+ , typename nonstd::conditional<is_const, const typename RS::value_type, typename RS::value_type>::type
+>
+# endif
+
+template<class RS, bool is_const>
+class ring_iterator {
+public:
+ typedef ring_iterator<RS, is_const> type;
+
+ typedef std::ptrdiff_t difference_type;
+ typedef typename RS::value_type value_type;
+
+ typedef typename nonstd::
+ conditional<is_const, const value_type, value_type>::type* pointer;
+ typedef typename nonstd::
+ conditional<is_const, const value_type, value_type>::type& reference;
+ typedef std::random_access_iterator_tag iterator_category;
+
+# if nsrs_CPP11_OR_GREATER
+ ring_iterator() = default;
+# else
+ ring_iterator() : m_idx(), m_rs() {}
+# endif
+
+# if nsrs_RING_SPAN_LITE_EXTENSION
+ // conversion to const iterator:
+
+ operator ring_iterator<RS, true>() const nsrs_noexcept
+ {
+ return ring_iterator<RS, true>(m_idx, m_rs);
+ }
+# endif
+
+ // access content:
+
+ reference operator*() const nsrs_noexcept
+ {
+ return m_rs->at(m_idx);
+ }
+
+ // advance iterator:
+
+ type& operator++() nsrs_noexcept
+ {
+ ++m_idx;
+ return *this;
+ }
+
+ type operator++(int) nsrs_noexcept
+ {
+ type r(*this);
+ ++*this;
+ return r;
+ }
+
+ type& operator--() nsrs_noexcept
+ {
+ --m_idx;
+ return *this;
+ }
+
+ type operator--(int) nsrs_noexcept
+ {
+ type r(*this);
+ --*this;
+ return r;
+ }
+
+# if defined(__clang__) || defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wsign-conversion"
+# endif
+
+ type& operator+=(int i) nsrs_noexcept
+ {
+ m_idx += i;
+ return *this;
+ }
+
+ type& operator-=(int i) nsrs_noexcept
+ {
+ m_idx -= i;
+ return *this;
+ }
+
+# if defined(__clang__) || defined(__GNUC__)
+# pragma GCC diagnostic pop
+# endif
+
+# if nsrs_RING_SPAN_LITE_EXTENSION
+
+ template<bool C>
+ difference_type operator-(ring_iterator<RS, C> const& rhs) const
+ nsrs_noexcept
+ {
+ return static_cast<difference_type>(this->m_idx)
+ - static_cast<difference_type>(rhs.m_idx);
+ }
+# endif
+
+ // comparison:
+
+ template<bool C>
+ bool operator<(ring_iterator<RS, C> const& rhs) const nsrs_noexcept
+ {
+ assert(this->m_rs == rhs.m_rs);
+ return (this->m_idx < rhs.m_idx);
+ }
+
+ template<bool C>
+ bool operator==(ring_iterator<RS, C> const& rhs) const nsrs_noexcept
+ {
+ assert(this->m_rs == rhs.m_rs);
+ return (this->m_idx == rhs.m_idx);
+ }
+
+ // other comparisons expressed in <, ==:
+
+ template<bool C>
+ inline bool operator!=(ring_iterator<RS, C> const& rhs) const nsrs_noexcept
+ {
+ return !(*this == rhs);
+ }
+
+ template<bool C>
+ inline bool operator<=(ring_iterator<RS, C> const& rhs) const nsrs_noexcept
+ {
+ return !(rhs < *this);
+ }
+
+ template<bool C>
+ inline bool operator>(ring_iterator<RS, C> const& rhs) const nsrs_noexcept
+ {
+ return rhs < *this;
+ }
+
+ template<bool C>
+ inline bool operator>=(ring_iterator<RS, C> const& rhs) const nsrs_noexcept
+ {
+ return !(*this < rhs);
+ }
+
+private:
+ friend RS; // clang: non-class friend type 'RS' is a C++11 extension
+ // [-Wc++11-extensions]
+ friend class ring_iterator<RS, !is_const>;
+
+ typedef typename RS::size_type size_type;
+ typedef
+ typename nonstd::conditional<is_const, const RS, RS>::type ring_type;
+
+ ring_iterator(size_type idx,
+ typename nonstd::conditional<is_const, const RS, RS>::type*
+ rs) nsrs_noexcept
+ : m_idx(idx)
+ , m_rs(rs)
+ {
+ }
+
+private:
+ size_type m_idx;
+ ring_type* m_rs;
+};
+
+// advanced iterator:
+
+template<class RS, bool C>
+inline ring_iterator<RS, C>
+operator+(ring_iterator<RS, C> it, int i) nsrs_noexcept
+{
+ it += i;
+ return it;
+}
+
+template<class RS, bool C>
+inline ring_iterator<RS, C>
+operator-(ring_iterator<RS, C> it, int i) nsrs_noexcept
+{
+ it -= i;
+ return it;
+}
+
+} // namespace ring_detail
+} // namespace nonstd
+
+nsrs_RESTORE_WARNINGS()
+
+#endif // NONSTD_RING_SPAN_LITE_HPP
diff --git a/src/root-config.json b/src/root-config.json
new file mode 100644
index 0000000..f907d63
--- /dev/null
+++ b/src/root-config.json
@@ -0,0 +1,79 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "clock-format": "%Y-%m-%dT%H:%M:%S %Z",
+ "dim-text": false,
+ "default-colors": true,
+ "keymap": "default",
+ "theme": "default",
+ "movement": {
+ "mode": "top"
+ }
+ },
+ "tuning": {
+ "archive-manager": {
+ "min-free-space": 33554432,
+ "cache-ttl": "2d"
+ },
+ "remote": {
+ "ssh": {
+ "command": "ssh",
+ "config": {
+ "BatchMode": "yes",
+ "ConnectTimeout": "10"
+ },
+ "start-command": "bash -c ./{0:}",
+ "transfer-command": "cat > {0:} && chmod ugo+rx ./{0:}"
+ }
+ },
+ "clipboard": {
+ "impls": {
+ "MacOS": {
+ "test": "command -v pbcopy",
+ "general": {
+ "write": "pbcopy",
+ "read": "pbpaste -Prefer txt"
+ },
+ "find": {
+ "write": "pbcopy -pboard find",
+ "read": "pbpaste -pboard find -Prefer txt"
+ }
+ },
+ "Wayland": {
+ "test": "test -n \"$WAYLAND_DISPLAY\"",
+ "general": {
+ "write": "wl-copy --foreground --type text/plain",
+ "read": "wl-paste --no-newline"
+ }
+ },
+ "X11-xclip": {
+ "test": "test -n \"$DISPLAY\" && command -v xclip",
+ "general": {
+ "write": "xclip -i -selection clipboard",
+ "read": "xclip -o -selection clipboard"
+ }
+ },
+ "tmux": {
+ "test": "test -n \"$TMUX\" -a -z \"$SSH_CLIENT\"",
+ "general": {
+ "write": "tmux load-buffer -",
+ "read": "tmux save-buffer -"
+ }
+ },
+ "NeoVim": {
+ "test": "command -v win32yank.exe",
+ "general": {
+ "write": "win32yank.exe -i --crlf",
+ "read": "win32yank.exe -o --lf"
+ }
+ },
+ "Windows": {
+ "test": "command -v clip.exe",
+ "general": {
+ "write": "clip.exe"
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/safe/accessmode.h b/src/safe/accessmode.h
new file mode 100644
index 0000000..3dd0f4f
--- /dev/null
+++ b/src/safe/accessmode.h
@@ -0,0 +1,49 @@
+/**
+ * @file accessmode.h
+ * @author L.-C. C.
+ * @brief
+ * @version 0.1
+ * @date 2019-01-20
+ *
+ * @copyright Copyright (c) 2019
+ *
+ */
+
+#pragma once
+
+#include <mutex>
+#if __cplusplus >= 201402L
+#include <shared_mutex>
+#endif // __cplusplus >= 201402L
+
+namespace safe
+{
+ enum class AccessMode
+ {
+ ReadOnly,
+ ReadWrite
+ };
+
+ template<typename LockType>
+ struct AccessTraits
+ {
+ static constexpr bool IsReadOnly = false;
+ };
+ template<typename MutexType>
+ struct AccessTraits<std::lock_guard<MutexType>>
+ {
+ static constexpr bool IsReadOnly = false;
+ };
+ template<typename MutexType>
+ struct AccessTraits<std::unique_lock<MutexType>>
+ {
+ static constexpr bool IsReadOnly = false;
+ };
+#if __cplusplus >= 201402L
+ template<typename MutexType>
+ struct AccessTraits<std::shared_lock<MutexType>>
+ {
+ static constexpr bool IsReadOnly = true;
+ };
+#endif // __cplusplus >= 201402L
+} // namespace safe
diff --git a/src/safe/defaulttypes.h b/src/safe/defaulttypes.h
new file mode 100644
index 0000000..1cb2273
--- /dev/null
+++ b/src/safe/defaulttypes.h
@@ -0,0 +1,23 @@
+/**
+ * @file defaulttypes.h
+ * @author L.-C. C.
+ * @brief
+ * @version 0.1
+ * @date 2020-01-29
+ *
+ * @copyright Copyright (c) 2020
+ *
+ */
+
+#pragma once
+
+#include <mutex>
+
+namespace safe
+{
+ using DefaultMutex = std::mutex;
+ template<typename MutexType>
+ using DefaultReadOnlyLock = std::lock_guard<MutexType>;
+ template<typename MutexType>
+ using DefaultReadWriteLock = std::lock_guard<MutexType>;
+} // namespace safe
diff --git a/src/safe/mutableref.h b/src/safe/mutableref.h
new file mode 100644
index 0000000..fb65825
--- /dev/null
+++ b/src/safe/mutableref.h
@@ -0,0 +1,40 @@
+/**
+ * @file mutableref.h
+ * @author L.-C. C.
+ * @brief
+ * @version 0.1
+ * @date 2020-01-03
+ *
+ * @copyright Copyright (c) 2020
+ *
+ */
+
+#pragma once
+
+#include <utility>
+
+namespace safe
+{
+ namespace impl
+ {
+ /**
+ * @brief A helper class that defines a member variable of type
+ * Type. The variable is defined "mutable Type" if Type is not a
+ * reference, the variable is "Type&" if Type is a reference.
+ *
+ * @tparam Type The type of the variable to define.
+ */
+ template<typename Type>
+ struct MutableIfNotReference
+ {
+ /// Mutable Type object.
+ mutable Type get;
+ };
+ template<typename Type>
+ struct MutableIfNotReference<Type&>
+ {
+ /// Reference to a Type object.
+ Type& get;
+ };
+ } // namespace impl
+} // namespace safe
diff --git a/src/safe/safe.h b/src/safe/safe.h
new file mode 100644
index 0000000..2beab55
--- /dev/null
+++ b/src/safe/safe.h
@@ -0,0 +1,359 @@
+/**
+ * @file safe.h
+ * @author L.-C. C.
+ * @brief
+ * @version 0.1
+ * @date 2018-09-21
+ *
+ * @copyright Copyright (c) 2018
+ *
+ */
+
+#pragma once
+
+#include "accessmode.h"
+#include "defaulttypes.h"
+#include "mutableref.h"
+
+#include <type_traits>
+#include <utility>
+
+#if __cplusplus >= 201703L
+#define EXPLICIT_IF_CPP17 explicit
+#define EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17 ReturnType
+#else
+#define EXPLICIT_IF_CPP17
+#define EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17
+#endif
+
+namespace safe
+{
+ /**
+ * @brief Use this tag to default construct the mutex when constructing a
+ * Safe object.
+ */
+ struct DefaultConstructMutex {};
+ static constexpr DefaultConstructMutex default_construct_mutex;
+
+ /**
+ * @brief Wraps a value together with a mutex.
+ *
+ * @tparam ValueType The type of the value to protect.
+ * @tparam MutexType The type of the mutex.
+ */
+ template<typename ValueType, typename MutexType = DefaultMutex>
+ class Safe
+ {
+ private:
+ /// Type ValueType with reference removed, if present
+ using RemoveRefValueType = typename std::remove_reference<ValueType>::type;
+ /// Type MutexType with reference removed, if present
+ using RemoveRefMutexType = typename std::remove_reference<MutexType>::type;
+
+ /**
+ * @brief Manages a mutex and gives pointer-like access to a value
+ * object.
+ *
+ * @tparam LockType The type of the lock object that manages the
+ * mutex, example: std::lock_guard.
+ * @tparam Mode Determines the access mode of the Access
+ * object. Can be either AccessMode::ReadOnly or
+ * AccessMode::ReadWrite.
+ */
+ template<template<typename> class LockType, AccessMode Mode>
+ class Access
+ {
+ // Make sure AccessMode is ReadOnly if a read-only lock is used
+ static_assert(!(AccessTraits<LockType<RemoveRefMutexType>>::IsReadOnly && Mode==AccessMode::ReadWrite), "Cannot have ReadWrite access mode with ReadOnly lock. Check the value of AccessTraits<LockType>::IsReadOnly if it exists.");
+
+ /// ValueType with const qualifier if AccessMode is ReadOnly.
+ using ConstIfReadOnlyValueType = typename std::conditional<Mode==AccessMode::ReadOnly, const RemoveRefValueType, RemoveRefValueType>::type;
+
+ public:
+ /// Pointer-to-const ValueType
+ using ConstPointerType = const ConstIfReadOnlyValueType*;
+ /// Pointer-to-const ValueType if Mode is ReadOnly, pointer to ValueType otherwise.
+ using PointerType = ConstIfReadOnlyValueType*;
+ /// Reference-to-const ValueType
+ using ConstReferenceType = const ConstIfReadOnlyValueType&;
+ /// Reference-to-const ValueType if Mode is ReadOnly, reference to ValueType otherwise.
+ using ReferenceType = ConstIfReadOnlyValueType&;
+
+ /**
+ * @brief Construct an Access object from a possibly const
+ * reference to the value object and any additionnal argument
+ * needed to construct the Lock object.
+ *
+ * @tparam LockArgs Deduced from lockArgs.
+ * @param value Reference to the value.
+ * @param lockArgs Arguments needed to construct the lock object.
+ */
+ template<typename... OtherLockArgs>
+ EXPLICIT_IF_CPP17
+ Access(ReferenceType value, MutexType& mutex, OtherLockArgs&&... otherLockArgs):
+ lock(mutex, std::forward<OtherLockArgs>(otherLockArgs)...),
+ m_value(value)
+ {}
+
+ /**
+ * @brief Construct a read-only Access object from a const
+ * safe::Safe object and any additionnal argument needed to
+ * construct the Lock object.
+ *
+ * If needed, you can provide additionnal arguments to construct
+ * the lock object (such as std::adopt_lock). The mutex from the
+ * safe::Locakble object is already passed to the lock object's
+ * constructor though, you must not provide it.
+ *
+ * @tparam OtherLockArgs Deduced from otherLockArgs.
+ * @param safe The const Safe object to give protected access to.
+ * @param otherLockArgs Other arguments needed to construct the lock
+ * object.
+ */
+ template<typename... OtherLockArgs>
+ EXPLICIT_IF_CPP17
+ Access(const Safe& safe, OtherLockArgs&&... otherLockArgs):
+ Access(safe.m_value, safe.m_mutex.get, std::forward<OtherLockArgs>(otherLockArgs)...)
+ {}
+
+ /**
+ * @brief Construct a read-write Access object from a
+ * safe::Safe object and any additionnal argument needed to
+ * construct the Lock object.
+ *
+ * If needed, you can provide additionnal arguments to construct
+ * the lock object (such as std::adopt_lock). The mutex from the
+ * safe object is already passed to the lock object's constructor
+ * though, you must not provide it.
+ *
+ * @tparam OtherLockArgs Deduced from otherLockArgs.
+ * @param safe The Safe object to give protected access to.
+ * @param otherLockArgs Other arguments needed to construct the lock
+ * object.
+ */
+ template<typename... OtherLockArgs>
+ EXPLICIT_IF_CPP17
+ Access(Safe& safe, OtherLockArgs&&... otherLockArgs):
+ Access(safe.m_value, safe.m_mutex.get, std::forward<OtherLockArgs>(otherLockArgs)...)
+ {}
+
+ /**
+ * @brief Construct an Access object from another one.
+ * OtherLockType must implement release() like std::unique_lock
+ * does.
+ *
+ * @tparam OtherLockType Deduced from otherAccess.
+ * @tparam OtherMode Deduced from otherAccess.
+ * @tparam OtherLockArgs Deduced from otherLockArgs.
+ * @param otherAccess The Access object to construct from.
+ * @param otherLockArgs Other arguments needed to construct the lock
+ * object.
+ */
+ template<template<typename> class OtherLockType, AccessMode OtherMode, typename... OtherLockArgs>
+ EXPLICIT_IF_CPP17
+ Access(Access<OtherLockType, OtherMode>& otherAccess, OtherLockArgs&&... otherLockArgs):
+ Access(*otherAccess, *otherAccess.lock.release(), std::adopt_lock, std::forward<OtherLockArgs>(otherLockArgs)...)
+ {
+ static_assert(OtherMode == AccessMode::ReadWrite || OtherMode == Mode, "Cannot construct a ReadWrite Access object from a ReadOnly one!");
+ }
+
+ /**
+ * @brief Const accessor to the value.
+ * @return ConstPointerType Const pointer to the protected value.
+ */
+ ConstPointerType operator->() const noexcept
+ {
+ return &m_value;
+ }
+
+ /**
+ * @brief Accessor to the value.
+ * @return ValuePointerType Pointer to the protected value.
+ */
+ PointerType operator->() noexcept
+ {
+ return &m_value;
+ }
+
+ /**
+ * @brief Const accessor to the value.
+ * @return ConstValueReferenceType Const reference to the protected
+ * value.
+ */
+ ConstReferenceType operator*() const noexcept
+ {
+ return m_value;
+ }
+
+ /**
+ * @brief Accessor to the value.
+ * @return ValueReferenceType Reference to the protected.
+ */
+ ReferenceType operator*() noexcept
+ {
+ return m_value;
+ }
+
+ /// The lock that manages the mutex.
+ mutable LockType<RemoveRefMutexType> lock;
+
+ private:
+ /// The protected value.
+ ReferenceType m_value;
+ };
+
+ /// Reference-to-const ValueType.
+ using ConstValueReferenceType = const RemoveRefValueType&;
+ /// Reference to ValueType.
+ using ValueReferenceType = RemoveRefValueType&;
+ /// Reference to MutexType.
+ using MutexReferenceType = RemoveRefMutexType&;
+
+ public:
+ /// Aliases to ReadAccess and WriteAccess classes for this Safe class.
+ template<template<typename> class LockType=DefaultReadOnlyLock>
+ using ReadAccess = Access<LockType, AccessMode::ReadOnly>;
+ template<template<typename> class LockType=DefaultReadWriteLock>
+ using WriteAccess = Access<LockType, AccessMode::ReadWrite>;
+
+ /**
+ * @brief Construct a Safe object
+ */
+ Safe() = default;
+
+ /**
+ * @brief Construct a Safe object with default construction of
+ * the mutex and perfect forwarding of the other arguments to
+ * construct the value object.
+ *
+ * @tparam ValueArgs Deduced from valueArgs.
+ * @param valueArgs Perfect forwarding arguments to construct the value object.
+ * @param tag Indicates that the mutex should be default constructed.
+ */
+ template<typename... ValueArgs>
+ explicit Safe(DefaultConstructMutex, ValueArgs&&... valueArgs):
+ m_mutex(),
+ m_value(std::forward<ValueArgs>(valueArgs)...)
+ {}
+ /**
+ * @brief Construct a Safe object, forwarding the first
+ * argument to construct the mutex and the other arguments to
+ * construct the value object.
+ *
+ * @tparam MutexArg Deduced from mutexArg.
+ * @tparam ValueArgs Deduced from valueArgs.
+ * @param valueArgs Perfect forwarding arguments to construct the
+ * value object.
+ * @param mutexArg Perfect forwarding argument to construct the
+ * mutex object.
+ */
+ template<typename MutexArg, typename... ValueArgs>
+ explicit Safe(MutexArg&& mutexArg, ValueArgs&&... valueArgs):
+ m_mutex{std::forward<MutexArg>(mutexArg)},
+ m_value(std::forward<ValueArgs>(valueArgs)...)
+ {}
+
+ /// Delete all copy/move construction/assignment, as these operations
+ /// require locking the mutex under the covers.
+ /// Use copy(), assign() and other defined constructors to get the behavior
+ /// you need with an explicit syntax.
+ Safe(const Safe&) = delete;
+ Safe(Safe&&) = delete;
+ Safe& operator =(const Safe&) = delete;
+ Safe& operator =(Safe&&) = delete;
+
+ template<template<typename> class LockType=DefaultReadOnlyLock, typename... LockArgs>
+ ReadAccess<LockType> readAccess(LockArgs&&... lockArgs) const
+ {
+ // using ReturnType = ReadAccess<LockType>;
+ return EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17{*this, std::forward<LockArgs>(lockArgs)...};
+ }
+
+ template<template<typename> class LockType=DefaultReadWriteLock, typename... LockArgs>
+ WriteAccess<LockType> writeAccess(LockArgs&&... lockArgs)
+ {
+ // using ReturnType = WriteAccess<LockType>;
+ return EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17{*this, std::forward<LockArgs>(lockArgs)...};
+ }
+
+ template<template<typename> class LockType=DefaultReadOnlyLock, typename... LockArgs>
+ RemoveRefValueType copy(LockArgs&&... lockArgs) const
+ {
+ return *readAccess<LockType>(std::forward<LockArgs>(lockArgs)...);
+ }
+
+ template<template<typename> class LockType=DefaultReadWriteLock, typename... LockArgs>
+ void assign(ConstValueReferenceType value, LockArgs&&... lockArgs)
+ {
+ *writeAccess<LockType>(std::forward<LockArgs>(lockArgs)...) = value;
+ }
+ template<template<typename> class LockType=DefaultReadWriteLock, typename... LockArgs>
+ void assign(RemoveRefValueType&& value, LockArgs&&... lockArgs)
+ {
+ *writeAccess<LockType>(std::forward<LockArgs>(lockArgs)...) = std::move(value);
+ }
+
+ /**
+ * @brief Unsafe const accessor to the value. If you use this
+ * function, you exit the realm of safe!
+ *
+ * @return ConstValueReferenceType Const reference to the value
+ * object.
+ */
+ ConstValueReferenceType unsafe() const noexcept
+ {
+ return m_value;
+ }
+ /**
+ * @brief Unsafe accessor to the value. If you use this function,
+ * you exit the realm of safe!
+ *
+ * @return ValueReferenceType Reference to the value object.
+ */
+ ValueReferenceType unsafe() noexcept
+ {
+ return m_value;
+ }
+
+ /**
+ * @brief Accessor to the mutex.
+ *
+ * @return MutexReferenceType Reference to the mutex.
+ */
+ MutexReferenceType mutex() const noexcept
+ {
+ return m_mutex.get;
+ }
+
+ private:
+ /// The helper object that holds the mutable mutex, or a reference to a mutex.
+ impl::MutableIfNotReference<MutexType> m_mutex;
+ /// The value to protect.
+ ValueType m_value;
+ };
+
+ /**
+ * @brief Type alias for read-only Access.
+ *
+ * @tparam SafeType The type of Safe object to give read-only access to.
+ * @tparam LockType=DefaultReadOnlyLock The type of lock.
+ */
+ template<
+ typename SafeType,
+ template<typename> class LockType=DefaultReadOnlyLock>
+ using ReadAccess = typename SafeType::template ReadAccess<LockType>;
+
+ /**
+ * @brief Type alias for read-write Access.
+ *
+ * @tparam SafeType The type of Safe object to give read-write access to.
+ * @tparam LockType=DefaultReadWriteLock The type of lock.
+ */
+ template<
+ typename SafeType,
+ template<typename> class LockType=DefaultReadWriteLock>
+ using WriteAccess = typename SafeType::template WriteAccess<LockType>;
+} // namespace safe
+
+#undef EXPLICIT_IF_CPP17
+#undef EXPLICITLY_CONSTRUCT_RETURN_TYPE_IF_CPP17
diff --git a/src/scripts/README.md b/src/scripts/README.md
new file mode 100644
index 0000000..e06d609
--- /dev/null
+++ b/src/scripts/README.md
@@ -0,0 +1,5 @@
+# Scripts
+
+This directory contains the built-in lnav scripts. The files are
+turned into C using `bin2c` and compiled into the executable. New scripts
+need to be added to the [scripts.am](scripts.am) file.
diff --git a/src/scripts/dhclient-summary.lnav b/src/scripts/dhclient-summary.lnav
new file mode 100644
index 0000000..87d0c1a
--- /dev/null
+++ b/src/scripts/dhclient-summary.lnav
@@ -0,0 +1,23 @@
+#
+# @synopsis: dhclient-summary
+# @description: Generate a summary of DHCP addresses bound in this log
+#
+
+:echo DHCP leases over time:
+:echo
+;SELECT ip AS IP, start_time AS "Start Time",
+ printf("% 24s", CASE
+ WHEN end_time IS NULL THEN printf("%s%s%s", $ansi_green, 'Active', $ansi_norm)
+ ELSE
+ printf("%s%.02f%s hours", $ansi_bold, (julianday(end_time) - julianday(start_time)) * 24, $ansi_norm)
+ END) AS Duration
+ FROM
+ (WITH lease_times AS
+ (SELECT min(log_time) AS start_time, ip FROM
+ (SELECT log_time, regexp_match('bound to (\S+) --', log_text) AS ip FROM syslog_log WHERE ip IS NOT NULL)
+ GROUP BY ip ORDER BY start_time ASC)
+ SELECT start_time,
+ (SELECT lt2.start_time AS end_time FROM lease_times AS lt2 WHERE lt1.start_time < lt2.start_time LIMIT 1) AS end_time,
+ ip
+ FROM lease_times AS lt1)
+:write-table-to -
diff --git a/src/scripts/dump-pid.sh b/src/scripts/dump-pid.sh
new file mode 100755
index 0000000..f17630c
--- /dev/null
+++ b/src/scripts/dump-pid.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+IN_PID=`cat`
+
+if test "${IN_PID}" -gt 0 > /dev/null 2>&1 && \
+ kill -0 $IN_PID > /dev/null 2>&1; then
+ echo "== ps =="
+ ps uewww -p $IN_PID
+ echo "== lsof =="
+ lsof -p $IN_PID
+else
+ echo "error: inaccessible process -- $IN_PID" > /dev/stderr
+fi
diff --git a/src/scripts/lnav-pop-view.lnav b/src/scripts/lnav-pop-view.lnav
new file mode 100644
index 0000000..89037e1
--- /dev/null
+++ b/src/scripts/lnav-pop-view.lnav
@@ -0,0 +1,21 @@
+#
+# @synopsis: lnav-pop-view
+# @description: Pop the top view on the view stack
+#
+
+;SELECT rowid as row_to_delete, name as last_view_name, CASE name
+ WHEN 'db' THEN $keymap_def_db_view
+ WHEN 'histogram' THEN $keymap_def_hist_view
+ WHEN 'text' THEN $keymap_def_text_view
+ ELSE ''
+ END as view_alt_msg FROM lnav_view_stack ORDER BY rowid DESC LIMIT 1;
+;SELECT top_time as last_top_time FROM lnav_views WHERE name = $last_view_name;
+;DELETE FROM lnav_view_stack WHERE rowid = $row_to_delete;
+;SELECT name AS new_top_view_name FROM lnav_view_stack ORDER BY rowid DESC LIMIT 1;
+;SELECT top_time AS old_top_time FROM lnav_views WHERE name = $new_top_view_name;
+;UPDATE lnav_views SET top_time = $last_top_time WHERE
+ $1 = 'x51' AND
+ name = $new_top_view_name AND
+ $last_top_time IS NOT NULL;
+:eval :alt-msg ${view_alt_msg}
+:echo
diff --git a/src/scripts/partition-by-boot.lnav b/src/scripts/partition-by-boot.lnav
new file mode 100644
index 0000000..e052774
--- /dev/null
+++ b/src/scripts/partition-by-boot.lnav
@@ -0,0 +1,12 @@
+#
+# DO NOT EDIT THIS FILE, IT WILL BE OVERWRITTEN!
+#
+# @synopsis: partition-by-boot
+# @description: Partition the log view based on boot messages from the Linux kernel.
+#
+
+;UPDATE syslog_log
+ SET log_part = 'Boot: ' || log_time
+ WHERE log_text LIKE '%kernel:%Linux version%';
+
+;SELECT 'Created ' || changes() || ' partitions(s)';
diff --git a/src/scripts/rename-stdin.lnav b/src/scripts/rename-stdin.lnav
new file mode 100644
index 0000000..10a54b6
--- /dev/null
+++ b/src/scripts/rename-stdin.lnav
@@ -0,0 +1,12 @@
+#
+# @synopsis: rename-stdin <name>
+# @description: Give a symbolic name to the standard-input file
+#
+
+;SELECT raise_error('expecting the new name for stdin as the first argument') WHERE $1 IS NULL
+;SELECT raise_error('no data was redirected to lnav''s standard-input')
+ WHERE (SELECT count(1) FROM lnav_file WHERE filepath='stdin') = 0
+
+;UPDATE lnav_file SET filepath=$1 WHERE filepath='stdin'
+
+;SELECT 'info: renamed stdin to ' || $1 AS msg
diff --git a/src/scripts/scripts.am b/src/scripts/scripts.am
new file mode 100644
index 0000000..50e10fd
--- /dev/null
+++ b/src/scripts/scripts.am
@@ -0,0 +1,12 @@
+
+BUILTIN_LNAVSCRIPTS = \
+ $(srcdir)/scripts/dhclient-summary.lnav \
+ $(srcdir)/scripts/lnav-pop-view.lnav \
+ $(srcdir)/scripts/partition-by-boot.lnav \
+ $(srcdir)/scripts/rename-stdin.lnav \
+ $(srcdir)/scripts/search-for.lnav \
+ $()
+
+BUILTIN_SHSCRIPTS = \
+ $(srcdir)/scripts/dump-pid.sh \
+ $()
diff --git a/src/scripts/search-for.lnav b/src/scripts/search-for.lnav
new file mode 100644
index 0000000..022ca78
--- /dev/null
+++ b/src/scripts/search-for.lnav
@@ -0,0 +1,7 @@
+#
+# @synopsis: search-for
+# @description: Start a search for the given string.
+#
+
+;UPDATE lnav_views SET search = $__all__ WHERE name = (
+ SELECT name FROM lnav_view_stack ORDER BY ROWID DESC LIMIT 1)
diff --git a/src/sequence_matcher.cc b/src/sequence_matcher.cc
new file mode 100644
index 0000000..9af0add
--- /dev/null
+++ b/src/sequence_matcher.cc
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "sequence_matcher.hh"
+
+#include "config.h"
+#include "spookyhash/SpookyV2.h"
+
+sequence_matcher::sequence_matcher(field_col_t& example)
+{
+ for (field_col_t::iterator col_iter = example.begin();
+ col_iter != example.end();
+ ++col_iter)
+ {
+ std::string first_value;
+ field sf;
+
+ sf.sf_value = *col_iter;
+ for (field_row_t::iterator row_iter = (*col_iter).begin();
+ row_iter != (*col_iter).end();
+ ++row_iter)
+ {
+ if (row_iter == (*col_iter).begin()) {
+ first_value = *row_iter;
+ } else if (first_value != *row_iter) {
+ sf.sf_type = FT_CONSTANT;
+ }
+ }
+ if (sf.sf_type == FT_VARIABLE) {
+ sf.sf_value.clear();
+ }
+ this->sm_fields.push_back(sf);
+ }
+ this->sm_count = example.front().size();
+}
+
+void
+sequence_matcher::identity(const std::vector<std::string>& values, id_t& id_out)
+{
+ SpookyHash context;
+ int lpc = 0;
+
+ context.Init(0, 0);
+ for (std::list<field>::iterator iter = sm_fields.begin();
+ iter != sm_fields.end();
+ ++iter, lpc++)
+ {
+ if (iter->sf_type == FT_VARIABLE) {
+ context.Update(values[lpc].c_str(), values[lpc].length() + 1);
+ }
+ }
+ context.Final(id_out.out(0), id_out.out(1));
+}
diff --git a/src/sequence_matcher.hh b/src/sequence_matcher.hh
new file mode 100644
index 0000000..992188f
--- /dev/null
+++ b/src/sequence_matcher.hh
@@ -0,0 +1,102 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef sequence_matcher_hh
+#define sequence_matcher_hh
+
+#include <list>
+#include <string>
+#include <vector>
+
+#include "byte_array.hh"
+
+class sequence_matcher {
+public:
+ typedef std::vector<std::string> field_row_t;
+ typedef std::list<field_row_t> field_col_t;
+
+ typedef byte_array<2, uint64_t> id_t;
+
+ enum field_type_t {
+ FT_VARIABLE,
+ FT_CONSTANT,
+ };
+
+ struct field {
+ field() : sf_type(FT_VARIABLE){};
+
+ field_type_t sf_type;
+ field_row_t sf_value;
+ };
+
+ sequence_matcher(field_col_t& example);
+
+ void identity(const std::vector<std::string>& values, id_t& id_out);
+
+ template<typename T>
+ bool match(const std::vector<std::string>& values,
+ std::vector<T>& state,
+ T index)
+ {
+ bool index_match = true;
+ int lpc = 0;
+
+ retry:
+ for (std::list<field>::iterator iter = this->sm_fields.begin();
+ iter != this->sm_fields.end();
+ ++iter, lpc++)
+ {
+ if (iter->sf_type != sequence_matcher::FT_CONSTANT) {
+ continue;
+ }
+
+ if (iter->sf_value[state.size()] != values[lpc]) {
+ if (!state.empty()) {
+ state.clear();
+ lpc = 0;
+ goto retry;
+ } else {
+ index_match = false;
+ break;
+ }
+ }
+ }
+
+ if (index_match) {
+ state.push_back(index);
+ }
+
+ return (size_t) this->sm_count == state.size();
+ };
+
+private:
+ int sm_count;
+ std::list<field> sm_fields;
+};
+#endif
diff --git a/src/sequence_sink.hh b/src/sequence_sink.hh
new file mode 100644
index 0000000..5c540c5
--- /dev/null
+++ b/src/sequence_sink.hh
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef sequence_sink_hh
+#define sequence_sink_hh
+
+#include <map>
+
+#include "bookmarks.hh"
+#include "grep_proc.hh"
+#include "listview_curses.hh"
+#include "sequence_matcher.hh"
+
+class sequence_sink : public grep_proc_sink<vis_line_t> {
+public:
+ sequence_sink(sequence_matcher& sm, bookmark_vector<vis_line_t>& bv)
+ : ss_matcher(sm), ss_bookmarks(bv){};
+
+ void grep_match(grep_proc<vis_line_t>& gp,
+ vis_line_t line,
+ int start,
+ int end)
+ {
+ this->ss_line_values.clear();
+ };
+
+ void grep_capture(grep_proc<vis_line_t>& gp,
+ vis_line_t line,
+ int start,
+ int end,
+ char* capture)
+ {
+ if (start == -1) {
+ this->ss_line_values.push_back("");
+ } else {
+ this->ss_line_values.push_back(std::string(capture));
+ }
+ };
+
+ void grep_match_end(grep_proc<vis_line_t>& gp, vis_line_t line)
+ {
+ sequence_matcher::id_t line_id;
+
+ this->ss_matcher.identity(this->ss_line_values, line_id);
+
+ std::vector<vis_line_t>& line_state = this->ss_state[line_id];
+ if (this->ss_matcher.match(this->ss_line_values, line_state, line)) {
+ std::vector<vis_line_t>::iterator iter;
+
+ for (iter = line_state.begin(); iter != line_state.end(); ++iter) {
+ this->ss_bookmarks.insert_once(vis_line_t(*iter));
+ }
+ line_state.clear();
+ }
+ };
+
+private:
+ sequence_matcher& ss_matcher;
+ bookmark_vector<vis_line_t>& ss_bookmarks;
+ std::vector<std::string> ss_line_values;
+ std::map<sequence_matcher::id_t, std::vector<vis_line_t> > ss_state;
+};
+#endif
diff --git a/src/service_tags.hh b/src/service_tags.hh
new file mode 100644
index 0000000..6d13427
--- /dev/null
+++ b/src/service_tags.hh
@@ -0,0 +1,48 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file service_tags.hh
+ */
+
+#ifndef lnav_service_tags_hh
+#define lnav_service_tags_hh
+
+namespace services {
+
+struct main_t {
+};
+struct ui_t {
+};
+struct curl_streamer_t {
+};
+struct remote_tailer_t {
+};
+
+} // namespace services
+
+#endif
diff --git a/src/session.export.cc b/src/session.export.cc
new file mode 100644
index 0000000..d408c50
--- /dev/null
+++ b/src/session.export.cc
@@ -0,0 +1,484 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "session.export.hh"
+
+#include "base/injector.hh"
+#include "bound_tags.hh"
+#include "lnav.hh"
+#include "sqlitepp.client.hh"
+#include "sqlitepp.hh"
+#include "textview_curses.hh"
+
+struct log_message_session_state {
+ int64_t lmss_time_msecs;
+ std::string lmss_format;
+ bool lmss_mark;
+ nonstd::optional<std::string> lmss_comment;
+ nonstd::optional<std::string> lmss_tags;
+ std::string lmss_hash;
+};
+
+template<>
+struct from_sqlite<log_message_session_state> {
+ inline log_message_session_state operator()(int argc,
+ sqlite3_value** argv,
+ int argi)
+ {
+ return {
+ from_sqlite<int64_t>()(argc, argv, argi + 0),
+ from_sqlite<std::string>()(argc, argv, argi + 1),
+ from_sqlite<bool>()(argc, argv, argi + 2),
+ from_sqlite<nonstd::optional<std::string>>()(argc, argv, argi + 3),
+ from_sqlite<nonstd::optional<std::string>>()(argc, argv, argi + 4),
+ from_sqlite<std::string>()(argc, argv, argi + 5),
+ };
+ }
+};
+
+struct log_filter_session_state {
+ std::string lfss_name;
+ bool lfss_enabled;
+ std::string lfss_type;
+ std::string lfss_language;
+ std::string lfss_pattern;
+};
+
+template<>
+struct from_sqlite<log_filter_session_state> {
+ inline log_filter_session_state operator()(int argc,
+ sqlite3_value** argv,
+ int argi)
+ {
+ return {
+ from_sqlite<std::string>()(argc, argv, argi + 0),
+ from_sqlite<bool>()(argc, argv, argi + 1),
+ from_sqlite<std::string>()(argc, argv, argi + 2),
+ from_sqlite<std::string>()(argc, argv, argi + 3),
+ from_sqlite<std::string>()(argc, argv, argi + 4),
+ };
+ }
+};
+
+struct log_file_session_state {
+ std::string lfss_content_id;
+ std::string lfss_format;
+ int64_t lfss_time_offset;
+};
+
+template<>
+struct from_sqlite<log_file_session_state> {
+ inline log_file_session_state operator()(int argc,
+ sqlite3_value** argv,
+ int argi)
+ {
+ return {
+ from_sqlite<std::string>()(argc, argv, argi + 0),
+ from_sqlite<std::string>()(argc, argv, argi + 1),
+ from_sqlite<int64_t>()(argc, argv, argi + 2),
+ };
+ }
+};
+
+namespace lnav {
+namespace session {
+
+static nonstd::optional<ghc::filesystem::path>
+find_container_dir(ghc::filesystem::path file_path)
+{
+ if (!ghc::filesystem::exists(file_path)) {
+ return nonstd::nullopt;
+ }
+
+ nonstd::optional<ghc::filesystem::path> dir_with_last_readme;
+
+ while (file_path.has_parent_path()
+ && file_path != file_path.root_directory())
+ {
+ auto parent = file_path.parent_path();
+ bool has_readme_entry = false;
+ std::error_code ec;
+
+ for (const auto& entry :
+ ghc::filesystem::directory_iterator(parent, ec))
+ {
+ if (!entry.is_regular_file()) {
+ continue;
+ }
+
+ auto entry_filename = tolower(entry.path().filename().string());
+ if (startswith(entry_filename, "readme")) {
+ has_readme_entry = true;
+ dir_with_last_readme = parent;
+ }
+ }
+ if (!has_readme_entry && dir_with_last_readme) {
+ return dir_with_last_readme;
+ }
+
+ file_path = parent;
+ }
+
+ return nonstd::nullopt;
+}
+
+static std::string
+replace_home_dir(std::string path)
+{
+ auto home_dir_opt = getenv_opt("HOME");
+
+ if (!home_dir_opt) {
+ return path;
+ }
+
+ const auto* home_dir = home_dir_opt.value();
+
+ if (startswith(path, home_dir)) {
+ auto retval = path.substr(strlen(home_dir));
+
+ if (retval.front() != '/') {
+ retval.insert(0, "/");
+ }
+ retval.insert(0, "$HOME");
+ return retval;
+ }
+
+ return path;
+}
+
+Result<void, lnav::console::user_message>
+export_to(FILE* file)
+{
+ static auto& lnav_db = injector::get<auto_sqlite3&>();
+
+ static const char* BOOKMARK_QUERY = R"(
+SELECT log_time_msecs, log_format, log_mark, log_comment, log_tags, log_line_hash
+ FROM all_logs
+ WHERE log_mark = 1 OR log_comment IS NOT NULL OR log_tags IS NOT NULL
+)";
+
+ static const char* FILTER_QUERY = R"(
+SELECT view_name, enabled, type, language, pattern FROM lnav_view_filters
+)";
+
+ static const char* FILE_QUERY = R"(
+SELECT content_id, format, time_offset FROM lnav_file
+ WHERE format IS NOT NULL AND time_offset != 0
+)";
+
+ static constexpr const char HEADER[] = R"(#!lnav -Nf
+# This file is an export of an lnav session. You can type
+# '|/path/to/this/file' in lnav to execute this file and
+# restore the state of the session.
+
+;SELECT raise_error('This session export was made with a newer version of lnav, please upgrade to ' || {0} || ' or later')
+ WHERE lnav_version() < {0} COLLATE naturalcase
+
+# The files loaded into the session were:
+
+)";
+
+ static constexpr const char LOG_DIR_INSERT[] = R"(
+# Set this environment variable to override this value or edit this script.
+;INSERT OR IGNORE INTO environ (name, value) VALUES ('LOG_DIR_{}', {})
+)";
+
+ static constexpr const char MARK_HEADER[] = R"(
+
+# The following SQL statements will restore the bookmarks,
+# comments, and tags that were added in the session.
+
+;SELECT total_changes() AS before_mark_changes
+)";
+
+ static constexpr const char MARK_FOOTER[] = R"(
+;SELECT {} - (total_changes() - $before_mark_changes) AS failed_mark_changes
+;SELECT echoln(printf('%sERROR%s: failed to restore %d bookmarks',
+ $ansi_red, $ansi_norm, $failed_mark_changes))
+ WHERE $failed_mark_changes != 0
+)";
+
+ static const char* FILTER_HEADER = R"(
+
+# The following SQL statements will restore the filters that
+# were added in the session.
+
+)";
+
+ static const char* FILE_HEADER = R"(
+
+# The following SQL statements will restore the state of the
+# files in the session.
+
+;SELECT total_changes() AS before_file_changes
+)";
+
+ static constexpr const char FILE_FOOTER[] = R"(
+;SELECT {} - (total_changes() - $before_file_changes) AS failed_file_changes
+;SELECT echoln(printf('%sERROR%s: failed to restore the state of %d files',
+ $ansi_red, $ansi_norm, $failed_file_changes))
+ WHERE $failed_file_changes != 0
+)";
+
+ static constexpr const char VIEW_HEADER[] = R"(
+
+# The following commands will restore the state of the {} view.
+
+)";
+
+ auto prep_mark_res = prepare_stmt(lnav_db.in(), BOOKMARK_QUERY);
+ if (prep_mark_res.isErr()) {
+ return Err(
+ console::user_message::error("unable to export log bookmarks")
+ .with_reason(prep_mark_res.unwrapErr()));
+ }
+
+ fmt::print(file, FMT_STRING(HEADER), sqlitepp::quote(PACKAGE_VERSION).in());
+
+ std::map<std::string, std::vector<std::string>> file_containers;
+ std::set<std::string> raw_files;
+ for (const auto& name_pair : lnav_data.ld_active_files.fc_file_names) {
+ const auto& open_opts = name_pair.second;
+
+ if (!open_opts.loo_is_visible || !open_opts.loo_include_in_session
+ || open_opts.loo_temp_file
+ || open_opts.loo_source != logfile_name_source::USER)
+ {
+ continue;
+ }
+
+ auto file_path_str = name_pair.first;
+ auto file_path = ghc::filesystem::path(file_path_str);
+ auto container_path_opt = find_container_dir(file_path);
+ if (container_path_opt) {
+ auto container_parent = container_path_opt.value().parent_path();
+ auto file_container_path
+ = ghc::filesystem::relative(file_path, container_parent)
+ .string();
+ file_containers[container_parent.string()].push_back(
+ file_container_path);
+ } else {
+ raw_files.insert(file_path_str);
+ }
+ }
+ for (const auto& file_path_str : raw_files) {
+ fmt::print(
+ file, FMT_STRING(":open {}\n"), replace_home_dir(file_path_str));
+ }
+ size_t container_index = 0;
+ for (const auto& container_pair : file_containers) {
+ fmt::print(file,
+ FMT_STRING(LOG_DIR_INSERT),
+ container_index,
+ sqlitepp::quote(container_pair.first).in());
+ for (const auto& file_path_str : container_pair.second) {
+ fmt::print(file,
+ FMT_STRING(":open $LOG_DIR_{}/{}\n"),
+ container_index,
+ file_path_str);
+ }
+ container_index += 1;
+ }
+
+ fmt::print(file, FMT_STRING("\n:rebuild\n"));
+
+ auto mark_count = 0;
+ auto each_mark_res
+ = prep_mark_res.unwrap().for_each_row<log_message_session_state>(
+ [file, &mark_count](const log_message_session_state& lmss) {
+ if (mark_count == 0) {
+ fmt::print(file, FMT_STRING(MARK_HEADER));
+ }
+ mark_count += 1;
+ fmt::print(file,
+ FMT_STRING(";UPDATE all_logs "
+ "SET log_mark = {}, "
+ "log_comment = {}, "
+ "log_tags = {} "
+ "WHERE log_time_msecs = {} AND "
+ "log_format = {} AND "
+ "log_line_hash = {}\n"),
+ lmss.lmss_mark ? "1" : "0",
+ sqlitepp::quote(lmss.lmss_comment).in(),
+ sqlitepp::quote(lmss.lmss_tags).in(),
+ lmss.lmss_time_msecs,
+ sqlitepp::quote(lmss.lmss_format).in(),
+ sqlitepp::quote(lmss.lmss_hash).in());
+ return false;
+ });
+
+ if (each_mark_res.isErr()) {
+ return Err(console::user_message::error(
+ "failed to fetch bookmark metadata for log message")
+ .with_reason(each_mark_res.unwrapErr().fe_msg));
+ }
+
+ if (mark_count > 0) {
+ fmt::print(file, FMT_STRING(MARK_FOOTER), mark_count);
+ }
+
+ auto prep_filter_res = prepare_stmt(lnav_db.in(), FILTER_QUERY);
+ if (prep_filter_res.isErr()) {
+ return Err(console::user_message::error("unable to export filter state")
+ .with_reason(prep_filter_res.unwrapErr()));
+ }
+
+ auto added_filter_header = false;
+ auto each_filter_res
+ = prep_filter_res.unwrap().for_each_row<log_filter_session_state>(
+ [file, &added_filter_header](const log_filter_session_state& lfss) {
+ if (!added_filter_header) {
+ fmt::print(file, FMT_STRING("{}"), FILTER_HEADER);
+ added_filter_header = true;
+ }
+ fmt::print(
+ file,
+ FMT_STRING(";REPLACE INTO lnav_view_filters "
+ "(view_name, enabled, type, language, pattern) "
+ "VALUES ({}, {}, {}, {}, {})\n"),
+ sqlitepp::quote(lfss.lfss_name).in(),
+ lfss.lfss_enabled ? 1 : 0,
+ sqlitepp::quote(lfss.lfss_type).in(),
+ sqlitepp::quote(lfss.lfss_language).in(),
+ sqlitepp::quote(lfss.lfss_pattern).in());
+ return false;
+ });
+
+ if (each_filter_res.isErr()) {
+ return Err(console::user_message::error(
+ "failed to fetch filter state for views")
+ .with_reason(each_filter_res.unwrapErr().fe_msg));
+ }
+
+ auto prep_file_res = prepare_stmt(lnav_db.in(), FILE_QUERY);
+ if (prep_file_res.isErr()) {
+ return Err(console::user_message::error("unable to export file state")
+ .with_reason(prep_file_res.unwrapErr()));
+ }
+
+ auto file_count = 0;
+ auto file_stmt = prep_file_res.unwrap();
+ auto each_file_res = file_stmt.for_each_row<log_file_session_state>(
+ [file, &file_count](const log_file_session_state& lfss) {
+ if (file_count == 0) {
+ fmt::print(file, FMT_STRING("{}"), FILE_HEADER);
+ }
+ file_count += 1;
+ fmt::print(file,
+ FMT_STRING(";UPDATE lnav_file "
+ "SET time_offset = {} "
+ "WHERE content_id = {} AND format = {}\n"),
+ lfss.lfss_time_offset,
+ sqlitepp::quote(lfss.lfss_content_id).in(),
+ sqlitepp::quote(lfss.lfss_format).in());
+ return false;
+ });
+
+ if (each_file_res.isErr()) {
+ return Err(console::user_message::error("failed to fetch file state")
+ .with_reason(each_file_res.unwrapErr().fe_msg));
+ }
+
+ if (file_count > 0) {
+ fmt::print(file, FMT_STRING(FILE_FOOTER), file_count);
+ }
+
+ for (auto view_index : {LNV_LOG, LNV_TEXT}) {
+ auto& tc = lnav_data.ld_views[view_index];
+ if (tc.get_inner_height() == 0_vl) {
+ continue;
+ }
+
+ fmt::print(file, FMT_STRING(VIEW_HEADER), lnav_view_titles[view_index]);
+ fmt::print(file,
+ FMT_STRING(":switch-to-view {}\n"),
+ lnav_view_strings[view_index]);
+
+ auto* tss = tc.get_sub_source();
+ auto* lss = dynamic_cast<logfile_sub_source*>(tss);
+ if (lss != nullptr) {
+ auto min_level = lss->get_min_log_level();
+
+ if (min_level != LEVEL_UNKNOWN) {
+ fmt::print(file,
+ FMT_STRING(":set-min-log-level {}\n"),
+ level_names[min_level]);
+ }
+
+ struct timeval min_time, max_time;
+ char tsbuf[128];
+ if (lss->get_min_log_time(min_time)) {
+ sql_strftime(tsbuf, sizeof(tsbuf), min_time, 'T');
+ fmt::print(file, FMT_STRING(":hide-lines-before {}\n"), tsbuf);
+ }
+ if (lss->get_max_log_time(max_time)) {
+ sql_strftime(tsbuf, sizeof(tsbuf), max_time, 'T');
+ fmt::print(file, FMT_STRING(":hide-lines-after {}\n"), tsbuf);
+ }
+ for (const auto& ld : *lss) {
+ if (ld->is_visible()) {
+ continue;
+ }
+
+ if (ld->get_file_ptr()->get_open_options().loo_source
+ == logfile_name_source::ARCHIVE)
+ {
+ continue;
+ }
+
+ auto container_path_opt
+ = find_container_dir(ld->get_file_ptr()->get_path());
+ if (!container_path_opt) {
+ fmt::print(file,
+ FMT_STRING(":hide-file {}\n"),
+ ld->get_file_ptr()->get_path().string());
+ continue;
+ }
+ auto container_parent
+ = container_path_opt.value().parent_path();
+ auto file_container_path = ghc::filesystem::relative(
+ ld->get_file_ptr()->get_path(), container_parent);
+ fmt::print(file,
+ FMT_STRING(":hide-file */{}\n"),
+ file_container_path.string());
+ }
+ }
+
+ if (!tc.get_current_search().empty()) {
+ fmt::print(file, FMT_STRING("/{}\n"), tc.get_current_search());
+ }
+
+ fmt::print(file, FMT_STRING(":goto {}\n"), (int) tc.get_top());
+ }
+
+ return Ok();
+}
+
+} // namespace session
+} // namespace lnav
diff --git a/src/session.export.hh b/src/session.export.hh
new file mode 100644
index 0000000..a913e67
--- /dev/null
+++ b/src/session.export.hh
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_session_export_hh
+#define lnav_session_export_hh
+
+#include "base/lnav.console.hh"
+
+namespace lnav {
+namespace session {
+
+Result<void, lnav::console::user_message> export_to(FILE* file);
+
+}
+} // namespace lnav
+
+#endif
diff --git a/src/session_data.cc b/src/session_data.cc
new file mode 100644
index 0000000..73cb7f8
--- /dev/null
+++ b/src/session_data.cc
@@ -0,0 +1,1864 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file session_data.cc
+ */
+
+#include <algorithm>
+#include <utility>
+
+#include "session_data.hh"
+
+#include <fcntl.h>
+#include <glob.h>
+#include <stdio.h>
+#include <sys/types.h>
+#include <yajl/api/yajl_tree.h>
+
+#include "base/fs_util.hh"
+#include "base/isc.hh"
+#include "base/opt_util.hh"
+#include "base/paths.hh"
+#include "command_executor.hh"
+#include "config.h"
+#include "lnav.events.hh"
+#include "lnav.hh"
+#include "lnav_util.hh"
+#include "log_format_ext.hh"
+#include "logfile.hh"
+#include "service_tags.hh"
+#include "sql_util.hh"
+#include "sqlitepp.client.hh"
+#include "tailer/tailer.looper.hh"
+#include "vtab_module.hh"
+#include "yajlpp/yajlpp.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+struct session_data_t session_data;
+
+static const char* LOG_METADATA_NAME = "log_metadata.db";
+
+static const char* META_TABLE_DEF = R"(
+CREATE TABLE IF NOT EXISTS bookmarks (
+ log_time datetime,
+ log_format varchar(64),
+ log_hash varchar(128),
+ session_time integer,
+ part_name text,
+ access_time datetime DEFAULT CURRENT_TIMESTAMP,
+ comment text DEFAULT '',
+ tags text DEFAULT '',
+
+ PRIMARY KEY (log_time, log_format, log_hash, session_time)
+);
+
+CREATE TABLE IF NOT EXISTS time_offset (
+ log_time datetime,
+ log_format varchar(64),
+ log_hash varchar(128),
+ session_time integer,
+ offset_sec integer,
+ offset_usec integer,
+ access_time datetime DEFAULT CURRENT_TIMESTAMP,
+
+ PRIMARY KEY (log_time, log_format, log_hash, session_time)
+);
+
+CREATE TABLE IF NOT EXISTS recent_netlocs (
+ netloc text,
+
+ access_time datetime DEFAULT CURRENT_TIMESTAMP,
+
+ PRIMARY KEY (netloc)
+);
+
+CREATE TABLE IF NOT EXISTS regex101_entries (
+ format_name text NOT NULL,
+ regex_name text NOT NULL,
+ permalink text NOT NULL,
+ delete_code text NOT NULL,
+
+ PRIMARY KEY (format_name, regex_name),
+
+ CHECK(
+ format_name <> '' AND
+ regex_name <> '' AND
+ permalink <> '')
+);
+)";
+
+static const char* BOOKMARK_LRU_STMT
+ = "DELETE FROM bookmarks WHERE access_time <= "
+ " (SELECT access_time FROM bookmarks "
+ " ORDER BY access_time DESC LIMIT 1 OFFSET 50000)";
+
+static const char* NETLOC_LRU_STMT
+ = "DELETE FROM recent_netlocs WHERE access_time <= "
+ " (SELECT access_time FROM bookmarks "
+ " ORDER BY access_time DESC LIMIT 1 OFFSET 10)";
+
+static const char* UPGRADE_STMTS[] = {
+ R"(ALTER TABLE bookmarks ADD COLUMN comment text DEFAULT '';)",
+ R"(ALTER TABLE bookmarks ADD COLUMN tags text DEFAULT '';)",
+};
+
+static const size_t MAX_SESSIONS = 8;
+static const size_t MAX_SESSION_FILE_COUNT = 256;
+
+static std::vector<content_line_t> marked_session_lines;
+static std::vector<content_line_t> offset_session_lines;
+
+static bool
+bind_line(sqlite3* db,
+ sqlite3_stmt* stmt,
+ content_line_t cl,
+ time_t session_time)
+{
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ auto lf = lss.find(cl);
+
+ if (lf == nullptr) {
+ return false;
+ }
+
+ sqlite3_clear_bindings(stmt);
+
+ auto line_iter = lf->begin() + cl;
+ auto read_result = lf->read_line(line_iter);
+
+ if (read_result.isErr()) {
+ return false;
+ }
+
+ auto line_hash = read_result
+ .map([cl](auto sbr) {
+ return hasher()
+ .update(sbr.get_data(), sbr.length())
+ .update(cl)
+ .to_string();
+ })
+ .unwrap();
+
+ return bind_values(stmt,
+ lf->original_line_time(line_iter),
+ lf->get_format()->get_name(),
+ line_hash,
+ session_time)
+ == SQLITE_OK;
+}
+
+struct session_file_info {
+ session_file_info(int timestamp, std::string id, std::string path)
+ : sfi_timestamp(timestamp), sfi_id(std::move(id)),
+ sfi_path(std::move(path)){};
+
+ bool operator<(const session_file_info& other) const
+ {
+ if (this->sfi_timestamp < other.sfi_timestamp) {
+ return true;
+ }
+ if (this->sfi_path < other.sfi_path) {
+ return true;
+ }
+ return false;
+ };
+
+ int sfi_timestamp;
+ std::string sfi_id;
+ std::string sfi_path;
+};
+
+static void
+cleanup_session_data()
+{
+ static_root_mem<glob_t, globfree> session_file_list;
+ std::list<struct session_file_info> session_info_list;
+ std::map<std::string, int> session_count;
+ auto session_file_pattern = lnav::paths::dotlnav() / "*-*.ts*.json";
+
+ if (glob(
+ session_file_pattern.c_str(), 0, nullptr, session_file_list.inout())
+ == 0)
+ {
+ for (size_t lpc = 0; lpc < session_file_list->gl_pathc; lpc++) {
+ const char* path = session_file_list->gl_pathv[lpc];
+ char hash_id[64];
+ int timestamp;
+ const char* base;
+
+ base = strrchr(path, '/');
+ if (base == nullptr) {
+ continue;
+ }
+ base += 1;
+ if (sscanf(base, "file-%63[^.].ts%d.json", hash_id, &timestamp)
+ == 2)
+ {
+ session_count[hash_id] += 1;
+ session_info_list.emplace_back(timestamp, hash_id, path);
+ }
+ if (sscanf(base,
+ "view-info-%63[^.].ts%d.ppid%*d.json",
+ hash_id,
+ &timestamp)
+ == 2)
+ {
+ session_count[hash_id] += 1;
+ session_info_list.emplace_back(timestamp, hash_id, path);
+ }
+ }
+ }
+
+ session_info_list.sort();
+
+ size_t session_loops = 0;
+
+ while (session_info_list.size() > MAX_SESSION_FILE_COUNT) {
+ const session_file_info& front = session_info_list.front();
+
+ session_loops += 1;
+ if (session_loops < MAX_SESSION_FILE_COUNT
+ && session_count[front.sfi_id] == 1)
+ {
+ session_info_list.splice(session_info_list.end(),
+ session_info_list,
+ session_info_list.begin());
+ } else {
+ if (remove(front.sfi_path.c_str()) != 0) {
+ log_error("Unable to remove session file: %s -- %s",
+ front.sfi_path.c_str(),
+ strerror(errno));
+ }
+ session_count[front.sfi_id] -= 1;
+ session_info_list.pop_front();
+ }
+ }
+
+ session_info_list.sort();
+
+ while (session_info_list.size() > MAX_SESSION_FILE_COUNT) {
+ const session_file_info& front = session_info_list.front();
+
+ if (remove(front.sfi_path.c_str()) != 0) {
+ log_error("Unable to remove session file: %s -- %s",
+ front.sfi_path.c_str(),
+ strerror(errno));
+ }
+ session_count[front.sfi_id] -= 1;
+ session_info_list.pop_front();
+ }
+}
+
+void
+init_session()
+{
+ lnav_data.ld_session_time = time(nullptr);
+ lnav_data.ld_session_id.clear();
+}
+
+static nonstd::optional<std::string>
+compute_session_id()
+{
+ bool has_files = false;
+ hasher h;
+
+ for (auto& ld_file_name : lnav_data.ld_active_files.fc_file_names) {
+ if (!ld_file_name.second.loo_include_in_session) {
+ continue;
+ }
+ has_files = true;
+ h.update(ld_file_name.first);
+ }
+ if (!has_files) {
+ return nonstd::nullopt;
+ }
+
+ return h.to_string();
+}
+
+nonstd::optional<session_pair_t>
+scan_sessions()
+{
+ static_root_mem<glob_t, globfree> view_info_list;
+
+ cleanup_session_data();
+
+ const auto session_id = compute_session_id();
+ if (!session_id) {
+ return nonstd::nullopt;
+ }
+ std::list<session_pair_t>& session_file_names
+ = lnav_data.ld_session_id[session_id.value()];
+
+ session_file_names.clear();
+
+ auto view_info_pattern_base
+ = fmt::format(FMT_STRING("view-info-{}.*.json"), session_id.value());
+ auto view_info_pattern = lnav::paths::dotlnav() / view_info_pattern_base;
+ if (glob(view_info_pattern.c_str(), 0, nullptr, view_info_list.inout())
+ == 0)
+ {
+ for (size_t lpc = 0; lpc < view_info_list->gl_pathc; lpc++) {
+ const char* path = view_info_list->gl_pathv[lpc];
+ int timestamp, ppid, rc;
+ const char* base;
+
+ base = strrchr(path, '/');
+ if (base == nullptr) {
+ continue;
+ }
+ base += 1;
+ if ((rc = sscanf(base,
+ "view-info-%*[^.].ts%d.ppid%d.json",
+ &timestamp,
+ &ppid))
+ == 2)
+ {
+ ppid_time_pair_t ptp;
+
+ ptp.first = (ppid == getppid()) ? 1 : 0;
+ ptp.second = timestamp;
+ session_file_names.emplace_back(ptp, path);
+ }
+ }
+ }
+
+ session_file_names.sort();
+
+ while (session_file_names.size() > MAX_SESSIONS) {
+ const std::string& name = session_file_names.front().second;
+
+ if (remove(name.c_str()) != 0) {
+ log_error("Unable to remove session: %s -- %s",
+ name.c_str(),
+ strerror(errno));
+ }
+ session_file_names.pop_front();
+ }
+
+ if (session_file_names.empty()) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional(session_file_names.back());
+}
+
+void
+load_time_bookmarks()
+{
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ auto_sqlite3 db;
+ auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+ logfile_sub_source::iterator file_iter;
+ bool reload_needed = false;
+ auto_mem<char, sqlite3_free> errmsg;
+
+ log_info("loading bookmark db: %s", db_path.c_str());
+
+ if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
+ return;
+ }
+
+ for (const char* upgrade_stmt : UPGRADE_STMTS) {
+ auto rc = sqlite3_exec(
+ db.in(), upgrade_stmt, nullptr, nullptr, errmsg.out());
+ if (rc != SQLITE_OK) {
+ auto exterr = sqlite3_extended_errcode(db.in());
+ log_error("unable to upgrade bookmark table -- (%d/%d) %s",
+ rc,
+ exterr,
+ errmsg.in());
+ }
+ }
+
+ {
+ auto netloc_prep_res
+ = prepare_stmt(db.in(), "SELECT netloc FROM recent_netlocs");
+ if (netloc_prep_res.isErr()) {
+ log_error("unable to get netlocs: %s",
+ netloc_prep_res.unwrapErr().c_str());
+ return;
+ }
+
+ auto netloc_stmt = netloc_prep_res.unwrap();
+ bool done = false;
+
+ while (!done) {
+ done = netloc_stmt.fetch_row<std::string>().match(
+ [](const std::string& netloc) {
+ session_data.sd_recent_netlocs.insert(netloc);
+ return false;
+ },
+ [](const prepared_stmt::fetch_error& fe) {
+ log_error("failed to fetch netloc row: %s",
+ fe.fe_msg.c_str());
+ return true;
+ },
+ [](prepared_stmt::end_of_rows) { return true; });
+ }
+ }
+
+ if (sqlite3_prepare_v2(
+ db.in(),
+ "SELECT log_time, log_format, log_hash, session_time, part_name, "
+ "access_time, comment,"
+ " tags, session_time=? as same_session FROM bookmarks WHERE "
+ " log_time between ? and ? and log_format = ? "
+ " ORDER BY same_session DESC, session_time DESC",
+ -1,
+ stmt.out(),
+ nullptr)
+ != SQLITE_OK)
+ {
+ log_error("could not prepare bookmark select statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ for (file_iter = lnav_data.ld_log_source.begin();
+ file_iter != lnav_data.ld_log_source.end();
+ ++file_iter)
+ {
+ auto lf = (*file_iter)->get_file();
+ content_line_t base_content_line;
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ base_content_line = lss.get_file_base_content_line(file_iter);
+
+ auto low_line_iter = lf->begin();
+ auto high_line_iter = lf->end();
+
+ --high_line_iter;
+
+ if (bind_values(stmt.in(),
+ lnav_data.ld_session_load_time,
+ lf->original_line_time(low_line_iter),
+ lf->original_line_time(high_line_iter),
+ lf->get_format()->get_name())
+ != SQLITE_OK)
+ {
+ return;
+ }
+
+ date_time_scanner dts;
+ bool done = false;
+ std::string line;
+ int64_t last_mark_time = -1;
+
+ while (!done) {
+ int rc = sqlite3_step(stmt.in());
+
+ switch (rc) {
+ case SQLITE_OK:
+ case SQLITE_DONE:
+ done = true;
+ break;
+
+ case SQLITE_ROW: {
+ const char* log_time
+ = (const char*) sqlite3_column_text(stmt.in(), 0);
+ const char* log_hash
+ = (const char*) sqlite3_column_text(stmt.in(), 2);
+ const char* part_name
+ = (const char*) sqlite3_column_text(stmt.in(), 4);
+ const char* comment
+ = (const char*) sqlite3_column_text(stmt.in(), 6);
+ const char* tags
+ = (const char*) sqlite3_column_text(stmt.in(), 7);
+ int64_t mark_time = sqlite3_column_int64(stmt.in(), 3);
+ struct timeval log_tv;
+ struct exttm log_tm;
+
+ if (last_mark_time == -1) {
+ last_mark_time = mark_time;
+ } else if (last_mark_time != mark_time) {
+ done = true;
+ continue;
+ }
+
+ if (part_name == nullptr) {
+ continue;
+ }
+
+ if (!dts.scan(
+ log_time, strlen(log_time), NULL, &log_tm, log_tv))
+ {
+ continue;
+ }
+
+ auto line_iter
+ = lower_bound(lf->begin(), lf->end(), log_tv);
+ while (line_iter != lf->end()) {
+ struct timeval line_tv = line_iter->get_timeval();
+
+ if ((line_tv.tv_sec != log_tv.tv_sec)
+ || (line_tv.tv_usec != log_tv.tv_usec))
+ {
+ break;
+ }
+
+ auto cl = content_line_t(
+ std::distance(lf->begin(), line_iter));
+ auto read_result = lf->read_line(line_iter);
+
+ if (read_result.isErr()) {
+ break;
+ }
+
+ auto sbr = read_result.unwrap();
+
+ auto line_hash
+ = hasher()
+ .update(sbr.get_data(), sbr.length())
+ .update(cl)
+ .to_string();
+
+ if (line_hash == log_hash) {
+ auto& bm_meta = lf->get_bookmark_metadata();
+ auto line_number = static_cast<uint32_t>(
+ std::distance(lf->begin(), line_iter));
+ content_line_t line_cl = content_line_t(
+ base_content_line + line_number);
+ bool meta = false;
+
+ if (part_name != nullptr && part_name[0] != '\0') {
+ lss.set_user_mark(&textview_curses::BM_META,
+ line_cl);
+ bm_meta[line_number].bm_name = part_name;
+ meta = true;
+ }
+ if (comment != nullptr && comment[0] != '\0') {
+ lss.set_user_mark(&textview_curses::BM_META,
+ line_cl);
+ bm_meta[line_number].bm_comment = comment;
+ meta = true;
+ }
+ if (tags != nullptr && tags[0] != '\0') {
+ auto_mem<yajl_val_s> tag_list(yajl_tree_free);
+ char error_buffer[1024];
+
+ tag_list = yajl_tree_parse(
+ tags, error_buffer, sizeof(error_buffer));
+ if (!YAJL_IS_ARRAY(tag_list.in())) {
+ log_error("invalid tags column: %s", tags);
+ } else {
+ lss.set_user_mark(&textview_curses::BM_META,
+ line_cl);
+ for (size_t lpc = 0;
+ lpc < tag_list.in()->u.array.len;
+ lpc++)
+ {
+ yajl_val elem
+ = tag_list.in()
+ ->u.array.values[lpc];
+
+ if (!YAJL_IS_STRING(elem)) {
+ continue;
+ }
+ bookmark_metadata::KNOWN_TAGS.insert(
+ elem->u.string);
+ bm_meta[line_number].add_tag(
+ elem->u.string);
+ }
+ }
+ meta = true;
+ }
+ if (!meta) {
+ marked_session_lines.push_back(line_cl);
+ lss.set_user_mark(&textview_curses::BM_USER,
+ line_cl);
+ }
+ reload_needed = true;
+ }
+
+ ++line_iter;
+ }
+ break;
+ }
+
+ default: {
+ const char* errmsg;
+
+ errmsg = sqlite3_errmsg(lnav_data.ld_db);
+ log_error(
+ "bookmark select error: code %d -- %s", rc, errmsg);
+ done = true;
+ } break;
+ }
+ }
+
+ sqlite3_reset(stmt.in());
+ }
+
+ if (sqlite3_prepare_v2(
+ db.in(),
+ "SELECT *,session_time=? as same_session FROM time_offset WHERE "
+ " log_time between ? and ? and log_format = ? "
+ " ORDER BY same_session DESC, session_time DESC",
+ -1,
+ stmt.out(),
+ nullptr)
+ != SQLITE_OK)
+ {
+ log_error("could not prepare time_offset select statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ for (file_iter = lnav_data.ld_log_source.begin();
+ file_iter != lnav_data.ld_log_source.end();
+ ++file_iter)
+ {
+ auto lf = (*file_iter)->get_file();
+ content_line_t base_content_line;
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ lss.find(lf->get_filename().c_str(), base_content_line);
+
+ auto low_line_iter = lf->begin();
+ auto high_line_iter = lf->end();
+
+ --high_line_iter;
+
+ if (bind_values(stmt.in(),
+ lnav_data.ld_session_load_time,
+ lf->original_line_time(low_line_iter),
+ lf->original_line_time(high_line_iter),
+ lf->get_format()->get_name())
+ != SQLITE_OK)
+ {
+ return;
+ }
+
+ date_time_scanner dts;
+ bool done = false;
+ std::string line;
+ int64_t last_mark_time = -1;
+
+ while (!done) {
+ int rc = sqlite3_step(stmt.in());
+
+ switch (rc) {
+ case SQLITE_OK:
+ case SQLITE_DONE:
+ done = true;
+ break;
+
+ case SQLITE_ROW: {
+ const char* log_time
+ = (const char*) sqlite3_column_text(stmt.in(), 0);
+ const char* log_hash
+ = (const char*) sqlite3_column_text(stmt.in(), 2);
+ int64_t mark_time = sqlite3_column_int64(stmt.in(), 3);
+ struct timeval log_tv;
+ struct exttm log_tm;
+
+ if (last_mark_time == -1) {
+ last_mark_time = mark_time;
+ } else if (last_mark_time != mark_time) {
+ done = true;
+ continue;
+ }
+
+ if (sqlite3_column_type(stmt.in(), 4) == SQLITE_NULL) {
+ continue;
+ }
+
+ if (!dts.scan(log_time,
+ strlen(log_time),
+ nullptr,
+ &log_tm,
+ log_tv))
+ {
+ continue;
+ }
+
+ auto line_iter
+ = lower_bound(lf->begin(), lf->end(), log_tv);
+ while (line_iter != lf->end()) {
+ struct timeval line_tv = line_iter->get_timeval();
+
+ if ((line_tv.tv_sec != log_tv.tv_sec)
+ || (line_tv.tv_usec != log_tv.tv_usec))
+ {
+ break;
+ }
+
+ if (lf->get_content_id() == log_hash) {
+ int file_line
+ = std::distance(lf->begin(), line_iter);
+ content_line_t line_cl
+ = content_line_t(base_content_line + file_line);
+ struct timeval offset;
+
+ offset_session_lines.push_back(line_cl);
+ offset.tv_sec = sqlite3_column_int64(stmt.in(), 4);
+ offset.tv_usec = sqlite3_column_int64(stmt.in(), 5);
+ lf->adjust_content_time(file_line, offset);
+
+ reload_needed = true;
+ }
+
+ ++line_iter;
+ }
+ break;
+ }
+
+ default: {
+ const char* errmsg;
+
+ errmsg = sqlite3_errmsg(lnav_data.ld_db);
+ log_error(
+ "bookmark select error: code %d -- %s", rc, errmsg);
+ done = true;
+ } break;
+ }
+ }
+
+ sqlite3_reset(stmt.in());
+ }
+
+ if (reload_needed) {
+ lnav_data.ld_views[LNV_LOG].reload_data();
+ }
+}
+
+static int
+read_files(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
+{
+ return 1;
+}
+
+static int
+read_current_search(yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len)
+{
+ const auto regex = std::string((const char*) str, len);
+ const char** view_name;
+ int view_index;
+
+ view_name = find(lnav_view_strings,
+ lnav_view_strings + LNV__MAX,
+ ypc->get_path_fragment(-2));
+ view_index = view_name - lnav_view_strings;
+
+ if (view_index < LNV__MAX && !regex.empty()) {
+ lnav_data.ld_views[view_index].execute_search(regex);
+ lnav_data.ld_views[view_index].set_follow_search_for(-1, {});
+ }
+
+ return 1;
+}
+
+static int
+read_top_line(yajlpp_parse_context* ypc, long long value)
+{
+ const char** view_name;
+ int view_index;
+
+ view_name = find(lnav_view_strings,
+ lnav_view_strings + LNV__MAX,
+ ypc->get_path_fragment(-2));
+ view_index = view_name - lnav_view_strings;
+ if (view_index < LNV__MAX) {
+ session_data.sd_view_states[view_index].vs_top = value;
+ }
+
+ return 1;
+}
+
+static int
+read_focused_line(yajlpp_parse_context* ypc, long long value)
+{
+ const char** view_name;
+ int view_index;
+
+ view_name = find(lnav_view_strings,
+ lnav_view_strings + LNV__MAX,
+ ypc->get_path_fragment(-2));
+ view_index = view_name - lnav_view_strings;
+ if (view_index < LNV__MAX) {
+ session_data.sd_view_states[view_index].vs_selection = value;
+ }
+
+ return 1;
+}
+
+static int
+read_word_wrap(yajlpp_parse_context* ypc, int value)
+{
+ const char** view_name;
+ int view_index;
+
+ view_name = find(lnav_view_strings,
+ lnav_view_strings + LNV__MAX,
+ ypc->get_path_fragment(-2));
+ view_index = view_name - lnav_view_strings;
+ if (view_index == LNV_HELP) {
+ } else if (view_index < LNV__MAX) {
+ textview_curses& tc = lnav_data.ld_views[view_index];
+
+ tc.set_word_wrap(value);
+ }
+
+ return 1;
+}
+
+static int
+read_filtering(yajlpp_parse_context* ypc, int value)
+{
+ const char** view_name;
+ int view_index;
+
+ view_name = find(lnav_view_strings,
+ lnav_view_strings + LNV__MAX,
+ ypc->get_path_fragment(-2));
+ view_index = view_name - lnav_view_strings;
+ if (view_index == LNV_HELP) {
+ } else if (view_index < LNV__MAX) {
+ textview_curses& tc = lnav_data.ld_views[view_index];
+
+ if (tc.get_sub_source() != nullptr) {
+ tc.get_sub_source()->tss_apply_filters = value;
+ }
+ }
+
+ return 1;
+}
+
+static int
+read_commands(yajlpp_parse_context* ypc, const unsigned char* str, size_t len)
+{
+ std::string cmdline = std::string((const char*) str, len);
+ const char** view_name;
+ int view_index;
+
+ view_name = find(lnav_view_strings,
+ lnav_view_strings + LNV__MAX,
+ ypc->get_path_fragment(-3));
+ view_index = view_name - lnav_view_strings;
+ bool active = ensure_view(&lnav_data.ld_views[view_index]);
+ execute_command(lnav_data.ld_exec_context, cmdline);
+ if (!active) {
+ lnav_data.ld_view_stack.pop_back();
+ }
+
+ return 1;
+}
+
+static const struct json_path_container view_def_handlers = {
+ json_path_handler("top_line", read_top_line),
+ json_path_handler("focused_line", read_focused_line),
+ json_path_handler("search", read_current_search),
+ json_path_handler("word_wrap", read_word_wrap),
+ json_path_handler("filtering", read_filtering),
+ json_path_handler("commands#", read_commands),
+};
+
+static const struct json_path_container view_handlers = {
+ yajlpp::pattern_property_handler("([^/]+)").with_children(
+ view_def_handlers),
+};
+
+static const struct json_path_container file_state_handlers = {
+ yajlpp::property_handler("visible")
+ .with_description("Indicates whether the file is visible or not")
+ .for_field(&file_state::fs_is_visible),
+};
+
+static const struct json_path_container file_states_handlers = {
+ yajlpp::pattern_property_handler(R"((?<filename>[^/]+))")
+ .with_description("Map of file names to file state objects")
+ .with_obj_provider<file_state, void>([](const auto& ypc, auto* root) {
+ auto fn = ypc.get_substr("filename");
+ return &session_data.sd_file_states[fn];
+ })
+ .with_children(file_state_handlers),
+};
+
+static const struct json_path_container view_info_handlers = {
+ yajlpp::property_handler("save-time")
+ .for_field(&session_data_t::sd_save_time),
+ yajlpp::property_handler("time-offset")
+ .for_field(&session_data_t::sd_time_offset),
+ json_path_handler("files#", read_files),
+ yajlpp::property_handler("file-states").with_children(file_states_handlers),
+ yajlpp::property_handler("views").with_children(view_handlers),
+};
+
+void
+load_session()
+{
+ load_time_bookmarks();
+ scan_sessions() | [](const auto pair) {
+ yajl_handle handle;
+ auto_fd fd;
+
+ lnav_data.ld_session_load_time = pair.first.second;
+ session_data.sd_save_time = pair.first.second;
+ const auto& view_info_path = pair.second;
+
+ yajlpp_parse_context ypc(intern_string::lookup(view_info_path.string()),
+ &view_info_handlers);
+ ypc.with_obj(session_data);
+ handle = yajl_alloc(&ypc.ypc_callbacks, nullptr, &ypc);
+
+ load_time_bookmarks();
+
+ if ((fd = lnav::filesystem::openp(view_info_path, O_RDONLY)) < 0) {
+ perror("cannot open session file");
+ } else {
+ unsigned char buffer[1024];
+ ssize_t rc;
+
+ log_info("loading session file: %s", view_info_path.c_str());
+ while ((rc = read(fd, buffer, sizeof(buffer))) > 0) {
+ yajl_parse(handle, buffer, rc);
+ }
+ yajl_complete_parse(handle);
+ }
+ yajl_free(handle);
+
+ bool log_changes = false, text_changes = false;
+
+ for (auto& lf : lnav_data.ld_active_files.fc_files) {
+ auto iter = session_data.sd_file_states.find(lf->get_filename());
+
+ if (iter == session_data.sd_file_states.end()) {
+ continue;
+ }
+
+ log_debug("found state for file: %s %d",
+ lf->get_content_id().c_str(),
+ iter->second.fs_is_visible);
+ lnav_data.ld_log_source.find_data(lf) | [iter](auto ld) {
+ ld->set_visibility(iter->second.fs_is_visible);
+ };
+ if (!iter->second.fs_is_visible) {
+ if (lf->get_format() != nullptr) {
+ log_changes = true;
+ } else {
+ text_changes = true;
+ }
+ }
+ }
+
+ if (log_changes) {
+ lnav_data.ld_log_source.text_filters_changed();
+ }
+ if (text_changes) {
+ lnav_data.ld_text_source.text_filters_changed();
+ }
+ };
+
+ lnav::events::publish(lnav_data.ld_db.in(),
+ lnav::events::session::loaded{});
+}
+
+static void
+yajl_writer(void* context, const char* str, size_t len)
+{
+ FILE* file = (FILE*) context;
+
+ fwrite(str, len, 1, file);
+}
+
+static void
+save_user_bookmarks(sqlite3* db,
+ sqlite3_stmt* stmt,
+ bookmark_vector<content_line_t>& user_marks)
+{
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ bookmark_vector<content_line_t>::iterator iter;
+
+ for (iter = user_marks.begin(); iter != user_marks.end(); ++iter) {
+ content_line_t cl = *iter;
+ auto line_meta_opt = lss.find_bookmark_metadata(cl);
+ if (!bind_line(db, stmt, cl, lnav_data.ld_session_time)) {
+ continue;
+ }
+
+ if (!line_meta_opt) {
+ if (sqlite3_bind_text(stmt, 5, "", 0, SQLITE_TRANSIENT)
+ != SQLITE_OK)
+ {
+ log_error("could not bind log hash -- %s", sqlite3_errmsg(db));
+ return;
+ }
+ } else {
+ bookmark_metadata& line_meta = *(line_meta_opt.value());
+ if (line_meta.empty()) {
+ continue;
+ }
+
+ if (sqlite3_bind_text(stmt,
+ 5,
+ line_meta.bm_name.c_str(),
+ line_meta.bm_name.length(),
+ SQLITE_TRANSIENT)
+ != SQLITE_OK)
+ {
+ log_error("could not bind part name -- %s", sqlite3_errmsg(db));
+ return;
+ }
+
+ if (sqlite3_bind_text(stmt,
+ 6,
+ line_meta.bm_comment.c_str(),
+ line_meta.bm_comment.length(),
+ SQLITE_TRANSIENT)
+ != SQLITE_OK)
+ {
+ log_error("could not bind comment -- %s", sqlite3_errmsg(db));
+ return;
+ }
+
+ std::string tags;
+
+ if (!line_meta.bm_tags.empty()) {
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_array arr(gen);
+
+ for (const auto& str : line_meta.bm_tags) {
+ arr.gen(str);
+ }
+ }
+
+ tags = gen.to_string_fragment().to_string();
+ }
+
+ if (sqlite3_bind_text(
+ stmt, 7, tags.c_str(), tags.length(), SQLITE_TRANSIENT)
+ != SQLITE_OK)
+ {
+ log_error("could not bind tags -- %s", sqlite3_errmsg(db));
+ return;
+ }
+ }
+
+ if (sqlite3_step(stmt) != SQLITE_DONE) {
+ log_error("could not execute bookmark insert statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ marked_session_lines.push_back(cl);
+
+ sqlite3_reset(stmt);
+ }
+}
+
+static void
+save_time_bookmarks()
+{
+ auto_sqlite3 db;
+ auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
+ auto_mem<char, sqlite3_free> errmsg;
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+
+ if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
+ log_error("unable to open bookmark DB -- %s", db_path.c_str());
+ return;
+ }
+
+ if (sqlite3_exec(db.in(), META_TABLE_DEF, nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to make bookmark table -- %s", errmsg.in());
+ return;
+ }
+
+ if (sqlite3_exec(
+ db.in(), "BEGIN TRANSACTION", nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to begin transaction -- %s", errmsg.in());
+ return;
+ }
+
+ {
+ static const char* UPDATE_NETLOCS_STMT
+ = R"(REPLACE INTO recent_netlocs (netloc) VALUES (?))";
+
+ std::set<std::string> netlocs;
+
+ isc::to<tailer::looper&, services::remote_tailer_t>().send_and_wait(
+ [&netlocs](auto& tlooper) { netlocs = tlooper.active_netlocs(); });
+
+ if (sqlite3_prepare_v2(
+ db.in(), UPDATE_NETLOCS_STMT, -1, stmt.out(), nullptr)
+ != SQLITE_OK)
+ {
+ log_error("could not prepare recent_netlocs statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ for (const auto& netloc : netlocs) {
+ bind_to_sqlite(stmt.in(), 1, netloc);
+
+ if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
+ log_error("could not execute bookmark insert statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ sqlite3_reset(stmt.in());
+ }
+ session_data.sd_recent_netlocs.insert(netlocs.begin(), netlocs.end());
+ }
+
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+ bookmarks<content_line_t>::type& bm = lss.get_user_bookmarks();
+
+ if (sqlite3_prepare_v2(db.in(),
+ "DELETE FROM bookmarks WHERE "
+ " log_time = ? and log_format = ? and log_hash = ? "
+ " and session_time = ?",
+ -1,
+ stmt.out(),
+ nullptr)
+ != SQLITE_OK)
+ {
+ log_error("could not prepare bookmark delete statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ for (auto& marked_session_line : marked_session_lines) {
+ if (!bind_line(db.in(),
+ stmt.in(),
+ marked_session_line,
+ lnav_data.ld_session_time))
+ {
+ continue;
+ }
+
+ if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
+ log_error("could not execute bookmark insert statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ sqlite3_reset(stmt.in());
+ }
+
+ marked_session_lines.clear();
+
+ if (sqlite3_prepare_v2(db.in(),
+ "REPLACE INTO bookmarks"
+ " (log_time, log_format, log_hash, session_time, "
+ "part_name, comment, tags)"
+ " VALUES (?, ?, ?, ?, ?, ?, ?)",
+ -1,
+ stmt.out(),
+ nullptr)
+ != SQLITE_OK)
+ {
+ log_error("could not prepare bookmark replace statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ {
+ logfile_sub_source::iterator file_iter;
+
+ for (file_iter = lnav_data.ld_log_source.begin();
+ file_iter != lnav_data.ld_log_source.end();
+ ++file_iter)
+ {
+ auto lf = (*file_iter)->get_file();
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ content_line_t base_content_line;
+ base_content_line = lss.get_file_base_content_line(file_iter);
+ base_content_line
+ = content_line_t(base_content_line + lf->size() - 1);
+
+ if (!bind_line(db.in(),
+ stmt.in(),
+ base_content_line,
+ lnav_data.ld_session_time))
+ {
+ continue;
+ }
+
+ if (sqlite3_bind_null(stmt.in(), 5) != SQLITE_OK) {
+ log_error("could not bind log hash -- %s",
+ sqlite3_errmsg(db.in()));
+ return;
+ }
+
+ if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
+ log_error("could not execute bookmark insert statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ sqlite3_reset(stmt.in());
+ }
+ }
+
+ save_user_bookmarks(db.in(), stmt.in(), bm[&textview_curses::BM_USER]);
+ save_user_bookmarks(db.in(), stmt.in(), bm[&textview_curses::BM_META]);
+
+ if (sqlite3_prepare_v2(db.in(),
+ "DELETE FROM time_offset WHERE "
+ " log_time = ? and log_format = ? and log_hash = ? "
+ " and session_time = ?",
+ -1,
+ stmt.out(),
+ NULL)
+ != SQLITE_OK)
+ {
+ log_error("could not prepare time_offset delete statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ for (auto& offset_session_line : offset_session_lines) {
+ if (!bind_line(db.in(),
+ stmt.in(),
+ offset_session_line,
+ lnav_data.ld_session_time))
+ {
+ continue;
+ }
+
+ if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
+ log_error("could not execute bookmark insert statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ sqlite3_reset(stmt.in());
+ }
+
+ offset_session_lines.clear();
+
+ if (sqlite3_prepare_v2(db.in(),
+ "REPLACE INTO time_offset"
+ " (log_time, log_format, log_hash, session_time, "
+ "offset_sec, offset_usec)"
+ " VALUES (?, ?, ?, ?, ?, ?)",
+ -1,
+ stmt.out(),
+ NULL)
+ != SQLITE_OK)
+ {
+ log_error("could not prepare time_offset replace statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ {
+ logfile_sub_source::iterator file_iter;
+
+ for (file_iter = lnav_data.ld_log_source.begin();
+ file_iter != lnav_data.ld_log_source.end();
+ ++file_iter)
+ {
+ auto lf = (*file_iter)->get_file();
+ content_line_t base_content_line;
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ base_content_line = lss.get_file_base_content_line(file_iter);
+
+ if (!bind_values(stmt,
+ lf->original_line_time(lf->begin()),
+ lf->get_format()->get_name(),
+ lf->get_content_id(),
+ lnav_data.ld_session_time))
+ {
+ continue;
+ }
+
+ if (sqlite3_bind_null(stmt.in(), 5) != SQLITE_OK) {
+ log_error("could not bind log hash -- %s",
+ sqlite3_errmsg(db.in()));
+ return;
+ }
+
+ if (sqlite3_bind_null(stmt.in(), 6) != SQLITE_OK) {
+ log_error("could not bind log hash -- %s",
+ sqlite3_errmsg(db.in()));
+ return;
+ }
+
+ if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
+ log_error("could not execute bookmark insert statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ sqlite3_reset(stmt.in());
+ }
+ }
+
+ for (auto& ls : lss) {
+ if (ls->get_file() == nullptr) {
+ continue;
+ }
+
+ auto lf = ls->get_file();
+
+ if (!lf->is_time_adjusted()) {
+ continue;
+ }
+
+ auto line_iter = lf->begin() + lf->get_time_offset_line();
+ struct timeval offset = lf->get_time_offset();
+
+ auto read_result = lf->read_line(line_iter);
+
+ if (read_result.isErr()) {
+ return;
+ }
+
+ bind_values(stmt.in(),
+ lf->original_line_time(line_iter),
+ lf->get_format()->get_name(),
+ lf->get_content_id(),
+ lnav_data.ld_session_time,
+ offset.tv_sec,
+ offset.tv_usec);
+
+ if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
+ log_error("could not execute bookmark insert statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ sqlite3_reset(stmt.in());
+ }
+
+ if (sqlite3_exec(db.in(), "COMMIT", nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to begin transaction -- %s", errmsg.in());
+ return;
+ }
+
+ if (sqlite3_exec(db.in(), BOOKMARK_LRU_STMT, nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to delete old bookmarks -- %s", errmsg.in());
+ return;
+ }
+
+ if (sqlite3_exec(db.in(), NETLOC_LRU_STMT, nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to delete old netlocs -- %s", errmsg.in());
+ return;
+ }
+}
+
+static void
+save_session_with_id(const std::string& session_id)
+{
+ auto_mem<FILE> file(fclose);
+ yajl_gen handle = nullptr;
+
+ /* TODO: save the last search query */
+
+ log_info("saving session with id: %s", session_id.c_str());
+
+ auto view_base_name
+ = fmt::format(FMT_STRING("view-info-{}.ts{}.ppid{}.json"),
+ session_id,
+ lnav_data.ld_session_time,
+ getppid());
+ auto view_file_name = lnav::paths::dotlnav() / view_base_name;
+ auto view_file_tmp_name = view_file_name.string() + ".tmp";
+
+ if ((file = fopen(view_file_tmp_name.c_str(), "w")) == nullptr) {
+ perror("Unable to open session file");
+ } else if (nullptr == (handle = yajl_gen_alloc(nullptr))) {
+ perror("Unable to create yajl_gen object");
+ } else {
+ yajl_gen_config(
+ handle, yajl_gen_print_callback, yajl_writer, file.in());
+
+ {
+ yajlpp_map root_map(handle);
+
+ root_map.gen("save-time");
+ root_map.gen((long long) time(nullptr));
+
+ root_map.gen("time-offset");
+ root_map.gen(lnav_data.ld_log_source.is_time_offset_enabled());
+
+ root_map.gen("files");
+
+ {
+ yajlpp_array file_list(handle);
+
+ for (auto& ld_file_name :
+ lnav_data.ld_active_files.fc_file_names)
+ {
+ file_list.gen(ld_file_name.first);
+ }
+ }
+
+ root_map.gen("file-states");
+
+ {
+ yajlpp_map file_states(handle);
+
+ for (auto& lf : lnav_data.ld_active_files.fc_files) {
+ auto ld_opt = lnav_data.ld_log_source.find_data(lf);
+
+ file_states.gen(lf->get_filename());
+
+ {
+ yajlpp_map file_state(handle);
+
+ file_state.gen("visible");
+ file_state.gen(!ld_opt || ld_opt.value()->ld_visible);
+ }
+ }
+ }
+
+ root_map.gen("views");
+
+ {
+ yajlpp_map top_view_map(handle);
+
+ for (int lpc = 0; lpc < LNV__MAX; lpc++) {
+ textview_curses& tc = lnav_data.ld_views[lpc];
+ unsigned long width;
+ vis_line_t height;
+
+ top_view_map.gen(lnav_view_strings[lpc]);
+
+ yajlpp_map view_map(handle);
+
+ view_map.gen("top_line");
+
+ tc.get_dimensions(height, width);
+ if (tc.get_top() >= tc.get_top_for_last_row()) {
+ view_map.gen(-1LL);
+ } else {
+ view_map.gen((long long) tc.get_top());
+ }
+
+ if (tc.is_selectable() && tc.get_selection() >= 0_vl
+ && tc.get_inner_height() > 0_vl
+ && tc.get_selection() != tc.get_inner_height() - 1)
+ {
+ view_map.gen("focused_line");
+ view_map.gen((long long) tc.get_selection());
+ }
+
+ view_map.gen("search");
+ view_map.gen(lnav_data.ld_views[lpc].get_current_search());
+
+ view_map.gen("word_wrap");
+ view_map.gen(tc.get_word_wrap());
+
+ auto tss = tc.get_sub_source();
+ if (tss == nullptr) {
+ continue;
+ }
+
+ view_map.gen("filtering");
+ view_map.gen(tss->tss_apply_filters);
+
+ filter_stack& fs = tss->get_filters();
+
+ view_map.gen("commands");
+ yajlpp_array cmd_array(handle);
+
+ for (const auto& filter : fs) {
+ auto cmd = filter->to_command();
+
+ if (cmd.empty()) {
+ continue;
+ }
+
+ cmd_array.gen(cmd);
+
+ if (!filter->is_enabled()) {
+ cmd_array.gen("disable-filter " + filter->get_id());
+ }
+ }
+
+ auto& hmap = lnav_data.ld_views[lpc].get_highlights();
+
+ for (auto& hl : hmap) {
+ if (hl.first.first != highlight_source_t::INTERACTIVE) {
+ continue;
+ }
+ cmd_array.gen("highlight " + hl.first.second);
+ }
+
+ if (lpc == LNV_LOG) {
+ for (const auto& format :
+ log_format::get_root_formats())
+ {
+ auto* elf = dynamic_cast<external_log_format*>(
+ format.get());
+
+ if (elf == nullptr) {
+ continue;
+ }
+
+ for (const auto& vd : elf->elf_value_defs) {
+ if (!vd.second->vd_meta.lvm_user_hidden) {
+ continue;
+ }
+
+ if (vd.second->vd_meta.lvm_user_hidden.value())
+ {
+ cmd_array.gen("hide-fields "
+ + elf->get_name().to_string()
+ + "." + vd.first.to_string());
+ } else if (vd.second->vd_meta.lvm_hidden) {
+ cmd_array.gen("show-fields "
+ + elf->get_name().to_string()
+ + "." + vd.first.to_string());
+ }
+ }
+ }
+
+ logfile_sub_source& lss = lnav_data.ld_log_source;
+
+ struct timeval min_time, max_time;
+ bool have_min_time = lss.get_min_log_time(min_time);
+ bool have_max_time = lss.get_max_log_time(max_time);
+ char min_time_str[32], max_time_str[32];
+
+ sql_strftime(
+ min_time_str, sizeof(min_time_str), min_time);
+ if (have_min_time) {
+ cmd_array.gen("hide-lines-before "
+ + std::string(min_time_str));
+ }
+ if (have_max_time) {
+ sql_strftime(
+ max_time_str, sizeof(max_time_str), max_time);
+ cmd_array.gen("hide-lines-after "
+ + std::string(max_time_str));
+ }
+
+ auto mark_expr = lss.get_sql_marker_text();
+ if (!mark_expr.empty()) {
+ cmd_array.gen("mark-expr " + mark_expr);
+ }
+ }
+ }
+ }
+ }
+
+ yajl_gen_clear(handle);
+ yajl_gen_free(handle);
+
+ fclose(file.release());
+
+ log_perror(rename(view_file_tmp_name.c_str(), view_file_name.c_str()));
+
+ log_info("Saved session: %s", view_file_name.c_str());
+ }
+}
+
+void
+save_session()
+{
+ if (lnav_data.ld_flags & LNF_SECURE_MODE) {
+ log_info("secure mode is enabled, not saving session");
+ return;
+ }
+
+ log_debug("BEGIN save_session");
+ save_time_bookmarks();
+
+ const auto opt_session_id = compute_session_id();
+ opt_session_id | [](auto& session_id) { save_session_with_id(session_id); };
+ for (const auto& pair : lnav_data.ld_session_id) {
+ if (opt_session_id && pair.first == opt_session_id.value()) {
+ continue;
+ }
+ save_session_with_id(pair.first);
+ }
+ log_debug("END save_session");
+}
+
+void
+reset_session()
+{
+ log_info("reset session: time=%d", lnav_data.ld_session_time);
+
+ save_session();
+
+ lnav_data.ld_session_time = time(nullptr);
+ session_data.sd_file_states.clear();
+
+ for (auto& tc : lnav_data.ld_views) {
+ auto& hmap = tc.get_highlights();
+ auto hl_iter = hmap.begin();
+
+ while (hl_iter != hmap.end()) {
+ if (hl_iter->first.first != highlight_source_t::INTERACTIVE) {
+ ++hl_iter;
+ } else {
+ hmap.erase(hl_iter++);
+ }
+ }
+ }
+
+ for (const auto& lf : lnav_data.ld_active_files.fc_files) {
+ lf->reset_state();
+ }
+
+ lnav_data.ld_log_source.set_marked_only(false);
+ lnav_data.ld_log_source.clear_min_max_log_times();
+ lnav_data.ld_log_source.set_min_log_level(LEVEL_UNKNOWN);
+ lnav_data.ld_log_source.set_sql_filter("", nullptr);
+ lnav_data.ld_log_source.set_sql_marker("", nullptr);
+
+ lnav_data.ld_log_source.clear_bookmark_metadata();
+
+ for (auto& tc : lnav_data.ld_views) {
+ text_sub_source* tss = tc.get_sub_source();
+
+ if (tss == nullptr) {
+ continue;
+ }
+ tss->get_filters().clear_filters();
+ tss->tss_apply_filters = true;
+ tss->text_filters_changed();
+ tss->text_clear_marks(&textview_curses::BM_USER);
+ tc.get_bookmarks()[&textview_curses::BM_USER].clear();
+ tss->text_clear_marks(&textview_curses::BM_META);
+ tc.get_bookmarks()[&textview_curses::BM_META].clear();
+ tc.reload_data();
+ }
+
+ lnav_data.ld_filter_view.reload_data();
+ lnav_data.ld_files_view.reload_data();
+ for (const auto& format : log_format::get_root_formats()) {
+ auto* elf = dynamic_cast<external_log_format*>(format.get());
+
+ if (elf == nullptr) {
+ continue;
+ }
+
+ bool changed = false;
+ for (const auto& vd : elf->elf_value_defs) {
+ if (vd.second->vd_meta.lvm_user_hidden) {
+ vd.second->vd_meta.lvm_user_hidden = nonstd::nullopt;
+ changed = true;
+ }
+ }
+ if (changed) {
+ elf->elf_value_defs_state->vds_generation += 1;
+ }
+ }
+}
+
+void
+lnav::session::regex101::insert_entry(const lnav::session::regex101::entry& ei)
+{
+ constexpr const char* STMT = R"(
+ INSERT INTO regex101_entries
+ (format_name, regex_name, permalink, delete_code)
+ VALUES (?, ?, ?, ?);
+)";
+
+ auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
+ auto_sqlite3 db;
+
+ if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
+ return;
+ }
+
+ auto_mem<char, sqlite3_free> errmsg;
+ if (sqlite3_exec(db.in(), META_TABLE_DEF, nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to make bookmark table -- %s", errmsg.in());
+ return;
+ }
+
+ auto prep_res = prepare_stmt(db.in(),
+ STMT,
+ ei.re_format_name,
+ ei.re_regex_name,
+ ei.re_permalink,
+ ei.re_delete_code);
+
+ if (prep_res.isErr()) {
+ return;
+ }
+
+ auto ps = prep_res.unwrap();
+
+ ps.execute();
+}
+
+template<>
+struct from_sqlite<lnav::session::regex101::entry> {
+ inline lnav::session::regex101::entry operator()(int argc,
+ sqlite3_value** argv,
+ int argi)
+ {
+ return {
+ from_sqlite<std::string>()(argc, argv, argi + 0),
+ from_sqlite<std::string>()(argc, argv, argi + 1),
+ from_sqlite<std::string>()(argc, argv, argi + 2),
+ from_sqlite<std::string>()(argc, argv, argi + 3),
+ };
+ }
+};
+
+Result<std::vector<lnav::session::regex101::entry>, std::string>
+lnav::session::regex101::get_entries()
+{
+ constexpr const char* STMT = R"(
+ SELECT * FROM regex101_entries;
+)";
+
+ auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
+ auto_sqlite3 db;
+
+ if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
+ return Err(std::string());
+ }
+
+ auto_mem<char, sqlite3_free> errmsg;
+ if (sqlite3_exec(db.in(), META_TABLE_DEF, nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to make bookmark table -- %s", errmsg.in());
+ return Err(std::string(errmsg));
+ }
+
+ auto ps = TRY(prepare_stmt(db.in(), STMT));
+ bool done = false;
+ std::vector<entry> retval;
+
+ while (!done) {
+ auto fetch_res = ps.fetch_row<entry>();
+
+ if (fetch_res.is<prepared_stmt::fetch_error>()) {
+ return Err(fetch_res.get<prepared_stmt::fetch_error>().fe_msg);
+ }
+
+ fetch_res.match(
+ [&done](const prepared_stmt::end_of_rows&) { done = true; },
+ [](const prepared_stmt::fetch_error&) {},
+ [&retval](entry en) { retval.emplace_back(en); });
+ }
+ return Ok(retval);
+}
+
+void
+lnav::session::regex101::delete_entry(const std::string& format_name,
+ const std::string& regex_name)
+{
+ constexpr const char* STMT = R"(
+ DELETE FROM regex101_entries WHERE
+ format_name = ? AND regex_name = ?;
+)";
+
+ auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
+ auto_sqlite3 db;
+
+ if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
+ return;
+ }
+
+ auto prep_res = prepare_stmt(db.in(), STMT, format_name, regex_name);
+
+ if (prep_res.isErr()) {
+ return;
+ }
+
+ auto ps = prep_res.unwrap();
+
+ ps.execute();
+}
+
+lnav::session::regex101::get_result_t
+lnav::session::regex101::get_entry(const std::string& format_name,
+ const std::string& regex_name)
+{
+ constexpr const char* STMT = R"(
+ SELECT * FROM regex101_entries WHERE
+ format_name = ? AND regex_name = ?;
+ )";
+
+ auto db_path = lnav::paths::dotlnav() / LOG_METADATA_NAME;
+ auto_sqlite3 db;
+
+ if (sqlite3_open(db_path.c_str(), db.out()) != SQLITE_OK) {
+ return error{std::string()};
+ }
+
+ auto_mem<char, sqlite3_free> errmsg;
+ if (sqlite3_exec(db.in(), META_TABLE_DEF, nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("unable to make bookmark table -- %s", errmsg.in());
+ return error{std::string(errmsg)};
+ }
+
+ auto prep_res = prepare_stmt(db.in(), STMT, format_name, regex_name);
+ if (prep_res.isErr()) {
+ return error{prep_res.unwrapErr()};
+ }
+
+ auto ps = prep_res.unwrap();
+ return ps.fetch_row<entry>().match(
+ [](const prepared_stmt::fetch_error& fe) -> get_result_t {
+ return error{fe.fe_msg};
+ },
+ [](const prepared_stmt::end_of_rows&) -> get_result_t {
+ return no_entry{};
+ },
+ [](const entry& en) -> get_result_t { return en; });
+}
diff --git a/src/session_data.hh b/src/session_data.hh
new file mode 100644
index 0000000..b380d3e
--- /dev/null
+++ b/src/session_data.hh
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file session_data.hh
+ */
+
+#ifndef lnav_session_data_hh
+#define lnav_session_data_hh
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include "mapbox/variant.hpp"
+#include "optional.hpp"
+#include "view_helpers.hh"
+
+struct file_state {
+ bool fs_is_visible{true};
+};
+
+struct view_state {
+ int64_t vs_top{0};
+ nonstd::optional<int64_t> vs_selection;
+};
+
+struct session_data_t {
+ uint64_t sd_save_time{0};
+ bool sd_time_offset{false};
+ std::map<std::string, file_state> sd_file_states;
+ std::set<std::string> sd_recent_netlocs;
+ view_state sd_view_states[LNV__MAX];
+};
+
+extern struct session_data_t session_data;
+
+void init_session();
+void load_session();
+void load_time_bookmarks();
+void save_session();
+void reset_session();
+
+namespace lnav {
+namespace session {
+namespace regex101 {
+
+struct entry {
+ std::string re_format_name;
+ std::string re_regex_name;
+ std::string re_permalink;
+ std::string re_delete_code;
+};
+
+void insert_entry(const entry& ei);
+
+struct no_entry {};
+
+struct error {
+ std::string e_msg;
+};
+
+using get_result_t = mapbox::util::variant<entry, no_entry, error>;
+
+get_result_t get_entry(const std::string& format_name,
+ const std::string& regex_name);
+void delete_entry(const std::string& format_name,
+ const std::string& regex_name);
+Result<std::vector<entry>, std::string> get_entries();
+
+} // namespace regex101
+} // namespace session
+} // namespace lnav
+
+#endif
diff --git a/src/shared_buffer.cc b/src/shared_buffer.cc
new file mode 100644
index 0000000..39e21be
--- /dev/null
+++ b/src/shared_buffer.cc
@@ -0,0 +1,203 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file shared_buffer.cc
+ */
+
+#include "config.h"
+
+#ifdef HAVE_EXECINFO_H
+# include <execinfo.h>
+#endif
+
+#include <algorithm>
+
+#include "base/ansi_scrubber.hh"
+#include "shared_buffer.hh"
+
+static const bool DEBUG_TRACE = false;
+
+void
+shared_buffer_ref::share(shared_buffer& sb, const char* data, size_t len)
+{
+#ifdef HAVE_EXECINFO_H
+ if (DEBUG_TRACE) {
+ void* frames[128];
+ int rc;
+
+ rc = backtrace(frames, 128);
+ this->sb_backtrace.reset(backtrace_symbols(frames, rc));
+ }
+#endif
+
+ this->disown();
+
+ sb.add_ref(*this);
+ this->sb_owner = &sb;
+ this->sb_data = data;
+ this->sb_length = len;
+
+ ensure(this->sb_length < (10 * 1024 * 1024));
+}
+
+bool
+shared_buffer_ref::subset(shared_buffer_ref& other, off_t offset, size_t len)
+{
+ this->disown();
+
+ if (offset != -1) {
+ this->sb_owner = other.sb_owner;
+ this->sb_length = len;
+ if (this->sb_owner == nullptr) {
+ if ((this->sb_data = (char*) malloc(this->sb_length)) == nullptr) {
+ return false;
+ }
+
+ memcpy(
+ const_cast<char*>(this->sb_data), &other.sb_data[offset], len);
+ } else {
+ this->sb_owner->add_ref(*this);
+ this->sb_data = &other.sb_data[offset];
+ }
+ }
+ return true;
+}
+
+shared_buffer_ref::shared_buffer_ref(shared_buffer_ref&& other) noexcept
+{
+ if (other.sb_data == nullptr) {
+ this->sb_owner = nullptr;
+ this->sb_data = nullptr;
+ this->sb_length = 0;
+ } else if (other.sb_owner != nullptr) {
+ auto owner_ref_iter = std::find(other.sb_owner->sb_refs.begin(),
+ other.sb_owner->sb_refs.end(),
+ &other);
+ *owner_ref_iter = this;
+ this->sb_owner = std::exchange(other.sb_owner, nullptr);
+ this->sb_data = std::exchange(other.sb_data, nullptr);
+ this->sb_length = std::exchange(other.sb_length, 0);
+ } else {
+ this->sb_owner = nullptr;
+ this->sb_data = other.sb_data;
+ this->sb_length = other.sb_length;
+ other.sb_data = nullptr;
+ other.sb_length = 0;
+ }
+ this->sb_metadata = other.sb_metadata;
+ other.sb_metadata = {};
+}
+
+bool
+shared_buffer_ref::take_ownership(size_t length)
+{
+ if ((this->sb_owner != nullptr && this->sb_data != nullptr)
+ || this->sb_length != length)
+ {
+ auto* new_data = (char*) malloc(length);
+ if (new_data == nullptr) {
+ return false;
+ }
+
+ memcpy(new_data, this->sb_data, std::min(length, this->sb_length));
+ this->sb_length = length;
+ this->sb_data = new_data;
+ this->sb_owner->sb_refs.erase(find(this->sb_owner->sb_refs.begin(),
+ this->sb_owner->sb_refs.end(),
+ this));
+ this->sb_owner = nullptr;
+ }
+ return true;
+}
+
+void
+shared_buffer_ref::disown()
+{
+ if (this->sb_owner == nullptr) {
+ if (this->sb_data != nullptr) {
+ free(const_cast<char*>(this->sb_data));
+ }
+ } else {
+ this->sb_owner->sb_refs.erase(find(this->sb_owner->sb_refs.begin(),
+ this->sb_owner->sb_refs.end(),
+ this));
+ }
+ this->sb_owner = nullptr;
+ this->sb_data = nullptr;
+ this->sb_length = 0;
+ this->sb_metadata = {};
+}
+
+void
+shared_buffer_ref::copy_ref(const shared_buffer_ref& other)
+{
+ if (other.sb_data == nullptr) {
+ this->sb_owner = nullptr;
+ this->sb_data = nullptr;
+ this->sb_length = 0;
+ } else if (other.sb_owner != nullptr) {
+ this->share(*other.sb_owner, other.sb_data, other.sb_length);
+ } else {
+ this->sb_owner = nullptr;
+ this->sb_data = (char*) malloc(other.sb_length);
+ memcpy(
+ const_cast<char*>(this->sb_data), other.sb_data, other.sb_length);
+ this->sb_length = other.sb_length;
+ }
+ this->sb_metadata = other.sb_metadata;
+}
+
+shared_buffer_ref::narrow_result
+shared_buffer_ref::narrow(size_t new_data, size_t new_length)
+{
+ return std::make_pair(
+ std::exchange(this->sb_data, this->sb_data + new_data),
+ std::exchange(this->sb_length, new_length));
+}
+
+void
+shared_buffer_ref::widen(narrow_result old_data_length)
+{
+ this->sb_data = old_data_length.first;
+ this->sb_length = old_data_length.second;
+}
+
+void
+shared_buffer_ref::erase_ansi()
+{
+ if (!this->sb_metadata.m_has_ansi) {
+ return;
+ }
+
+ auto* writable_data = this->get_writable_data();
+ auto new_len = erase_ansi_escapes(
+ string_fragment::from_bytes(writable_data, this->sb_length));
+
+ this->sb_length = new_len;
+ this->sb_metadata.m_has_ansi = false;
+}
diff --git a/src/shared_buffer.hh b/src/shared_buffer.hh
new file mode 100644
index 0000000..e0029e9
--- /dev/null
+++ b/src/shared_buffer.hh
@@ -0,0 +1,217 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file shared_buffer.hh
+ */
+
+#ifndef shared_buffer_hh
+#define shared_buffer_hh
+
+#include <string>
+#include <vector>
+
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include "base/attr_line.hh"
+#include "base/auto_mem.hh"
+#include "base/file_range.hh"
+#include "base/intern_string.hh"
+#include "base/lnav_log.hh"
+#include "scn/util/string_view.h"
+
+class shared_buffer;
+
+struct shared_buffer_ref {
+public:
+ shared_buffer_ref(char* data = nullptr, size_t len = 0)
+ : sb_owner(nullptr), sb_data(data), sb_length(len)
+ {
+ }
+
+ ~shared_buffer_ref() { this->disown(); }
+
+ shared_buffer_ref(const shared_buffer_ref& other)
+ {
+ this->sb_owner = nullptr;
+ this->sb_data = nullptr;
+ this->sb_length = 0;
+ this->sb_metadata = file_range::metadata{};
+
+ this->copy_ref(other);
+ }
+
+ shared_buffer_ref(shared_buffer_ref&& other) noexcept;
+
+ shared_buffer_ref& operator=(const shared_buffer_ref& other)
+ {
+ if (this != &other) {
+ this->disown();
+ this->copy_ref(other);
+ }
+
+ return *this;
+ }
+
+ bool empty() const
+ {
+ return this->sb_data == nullptr || this->sb_length == 0;
+ }
+
+ const char* get_data() const { return this->sb_data; }
+
+ const char* get_data_at(off_t offset) const
+ {
+ return &this->sb_data[offset];
+ }
+
+ size_t length() const { return this->sb_length; }
+
+ shared_buffer_ref& rtrim(bool pred(char))
+ {
+ while (this->sb_length > 0 && pred(this->sb_data[this->sb_length - 1]))
+ {
+ this->sb_length -= 1;
+ }
+
+ return *this;
+ }
+
+ bool contains(const char* ptr) const
+ {
+ const char* buffer_end = this->sb_data + this->sb_length;
+
+ return (this->sb_data <= ptr && ptr < buffer_end);
+ }
+
+ file_range::metadata& get_metadata() { return this->sb_metadata; }
+
+ char* get_writable_data(size_t length)
+ {
+ if (this->take_ownership(length)) {
+ return const_cast<char*>(this->sb_data);
+ }
+
+ return nullptr;
+ }
+
+ char* get_writable_data()
+ {
+ return this->get_writable_data(this->sb_length);
+ }
+
+ string_fragment to_string_fragment(off_t offset, size_t len) const
+ {
+ return string_fragment{
+ this->sb_data, (int) offset, (int) (offset + len)};
+ }
+
+ string_fragment to_string_fragment() const
+ {
+ return string_fragment::from_bytes(this->sb_data, this->length());
+ }
+
+ scn::string_view to_string_view(const line_range& lr) const
+ {
+ return scn::string_view{
+ this->get_data_at(lr.lr_start),
+ this->get_data_at(lr.lr_end),
+ };
+ }
+
+ using narrow_result = std::pair<const char*, size_t>;
+ narrow_result narrow(size_t new_data, size_t new_length);
+
+ void widen(narrow_result old_data_length);
+
+ void share(shared_buffer& sb, const char* data, size_t len);
+
+ bool subset(shared_buffer_ref& other, off_t offset, size_t len);
+
+ void erase_ansi();
+
+ bool take_ownership(size_t length);
+
+ bool take_ownership() { return this->take_ownership(this->sb_length); }
+
+ void disown();
+
+private:
+ void copy_ref(const shared_buffer_ref& other);
+
+ auto_mem<char*> sb_backtrace;
+ file_range::metadata sb_metadata;
+ shared_buffer* sb_owner;
+ const char* sb_data;
+ size_t sb_length;
+};
+
+class shared_buffer {
+public:
+ ~shared_buffer() { this->invalidate_refs(); }
+
+ void add_ref(shared_buffer_ref& ref) { this->sb_refs.push_back(&ref); }
+
+ bool invalidate_refs()
+ {
+ bool retval = true;
+
+ while (!this->sb_refs.empty()) {
+ auto iter = this->sb_refs.begin();
+
+ retval = retval && (*iter)->take_ownership();
+ }
+
+ return retval;
+ }
+
+ std::vector<shared_buffer_ref*> sb_refs;
+};
+
+struct tmp_shared_buffer {
+ explicit tmp_shared_buffer(const char* str, size_t len = -1)
+ {
+ if (len == (size_t) -1) {
+ len = strlen(str);
+ }
+
+ this->tsb_ref.share(this->tsb_manager, (char*) str, len);
+ };
+
+ shared_buffer tsb_manager;
+ shared_buffer_ref tsb_ref;
+};
+
+inline std::string
+to_string(const shared_buffer_ref& sbr)
+{
+ return {sbr.get_data(), sbr.length()};
+}
+
+#endif
diff --git a/src/shlex.cc b/src/shlex.cc
new file mode 100644
index 0000000..8da44bb
--- /dev/null
+++ b/src/shlex.cc
@@ -0,0 +1,218 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file shlex.cc
+ */
+
+#ifdef __CYGWIN__
+# include <alloca.h>
+#endif
+
+#include "config.h"
+#include "shlex.hh"
+
+bool
+shlex::tokenize(string_fragment& cap_out, shlex_token_t& token_out)
+{
+ while (this->s_index < this->s_len) {
+ switch (this->s_str[this->s_index]) {
+ case '\\':
+ cap_out.sf_begin = this->s_index;
+ if (this->s_index + 1 < this->s_len) {
+ token_out = shlex_token_t::ST_ESCAPE;
+ this->s_index += 2;
+ cap_out.sf_end = this->s_index;
+ } else {
+ this->s_index += 1;
+ cap_out.sf_end = this->s_index;
+ token_out = shlex_token_t::ST_ERROR;
+ }
+ return true;
+ case '\"':
+ if (!this->s_ignore_quotes) {
+ switch (this->s_state) {
+ case state_t::STATE_NORMAL:
+ cap_out.sf_begin = this->s_index;
+ this->s_index += 1;
+ cap_out.sf_end = this->s_index;
+ token_out = shlex_token_t::ST_DOUBLE_QUOTE_START;
+ this->s_state = state_t::STATE_IN_DOUBLE_QUOTE;
+ return true;
+ case state_t::STATE_IN_DOUBLE_QUOTE:
+ cap_out.sf_begin = this->s_index;
+ this->s_index += 1;
+ cap_out.sf_end = this->s_index;
+ token_out = shlex_token_t::ST_DOUBLE_QUOTE_END;
+ this->s_state = state_t::STATE_NORMAL;
+ return true;
+ default:
+ break;
+ }
+ }
+ break;
+ case '\'':
+ if (!this->s_ignore_quotes) {
+ switch (this->s_state) {
+ case state_t::STATE_NORMAL:
+ cap_out.sf_begin = this->s_index;
+ this->s_index += 1;
+ cap_out.sf_end = this->s_index;
+ token_out = shlex_token_t::ST_SINGLE_QUOTE_START;
+ this->s_state = state_t::STATE_IN_SINGLE_QUOTE;
+ return true;
+ case state_t::STATE_IN_SINGLE_QUOTE:
+ cap_out.sf_begin = this->s_index;
+ this->s_index += 1;
+ cap_out.sf_end = this->s_index;
+ token_out = shlex_token_t::ST_SINGLE_QUOTE_END;
+ this->s_state = state_t::STATE_NORMAL;
+ return true;
+ default:
+ break;
+ }
+ }
+ break;
+ case '$':
+ switch (this->s_state) {
+ case state_t::STATE_NORMAL:
+ case state_t::STATE_IN_DOUBLE_QUOTE:
+ this->scan_variable_ref(cap_out, token_out);
+ return true;
+ default:
+ break;
+ }
+ break;
+ case '~':
+ switch (this->s_state) {
+ case state_t::STATE_NORMAL:
+ cap_out.sf_begin = this->s_index;
+ this->s_index += 1;
+ while (this->s_index < this->s_len
+ && (isalnum(this->s_str[this->s_index])
+ || this->s_str[this->s_index] == '_'
+ || this->s_str[this->s_index] == '-'))
+ {
+ this->s_index += 1;
+ }
+ cap_out.sf_end = this->s_index;
+ token_out = shlex_token_t::ST_TILDE;
+ return true;
+ default:
+ break;
+ }
+ break;
+ case ' ':
+ case '\t':
+ switch (this->s_state) {
+ case state_t::STATE_NORMAL:
+ cap_out.sf_begin = this->s_index;
+ while (isspace(this->s_str[this->s_index])) {
+ this->s_index += 1;
+ }
+ cap_out.sf_end = this->s_index;
+ token_out = shlex_token_t::ST_WHITESPACE;
+ return true;
+ default:
+ break;
+ }
+ break;
+ default:
+ break;
+ }
+
+ this->s_index += 1;
+ }
+
+ return false;
+}
+
+void
+shlex::scan_variable_ref(string_fragment& cap_out, shlex_token_t& token_out)
+{
+ cap_out.sf_begin = this->s_index;
+ this->s_index += 1;
+ if (this->s_index >= this->s_len) {
+ cap_out.sf_end = this->s_index;
+ token_out = shlex_token_t::ST_ERROR;
+ return;
+ }
+
+ if (this->s_str[this->s_index] == '{') {
+ token_out = shlex_token_t::ST_QUOTED_VARIABLE_REF;
+ this->s_index += 1;
+ } else {
+ token_out = shlex_token_t::ST_VARIABLE_REF;
+ }
+
+ while (this->s_index < this->s_len) {
+ if (token_out == shlex_token_t::ST_VARIABLE_REF) {
+ if (isalnum(this->s_str[this->s_index])
+ || this->s_str[this->s_index] == '#'
+ || this->s_str[this->s_index] == '_')
+ {
+ this->s_index += 1;
+ } else {
+ break;
+ }
+ } else {
+ if (this->s_str[this->s_index] == '}') {
+ this->s_index += 1;
+ break;
+ }
+ this->s_index += 1;
+ }
+ }
+
+ cap_out.sf_end = this->s_index;
+ if (token_out == shlex_token_t::ST_QUOTED_VARIABLE_REF
+ && this->s_str[this->s_index - 1] != '}')
+ {
+ cap_out.sf_begin += 1;
+ cap_out.sf_end = cap_out.sf_begin + 1;
+ token_out = shlex_token_t::ST_ERROR;
+ }
+}
+
+void
+shlex::resolve_home_dir(std::string& result, string_fragment cap) const
+{
+ if (cap.length() == 1) {
+ result.append(getenv_opt("HOME").value_or("~"));
+ } else {
+ auto username = (char*) alloca(cap.length());
+
+ memcpy(username, &this->s_str[cap.sf_begin + 1], cap.length() - 1);
+ username[cap.length() - 1] = '\0';
+ auto pw = getpwnam(username);
+ if (pw != nullptr) {
+ result.append(pw->pw_dir);
+ } else {
+ result.append(&this->s_str[cap.sf_begin], cap.length());
+ }
+ }
+}
diff --git a/src/shlex.hh b/src/shlex.hh
new file mode 100644
index 0000000..2317a2c
--- /dev/null
+++ b/src/shlex.hh
@@ -0,0 +1,222 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file shlex.hh
+ */
+
+#ifndef LNAV_SHLEX_HH_H
+#define LNAV_SHLEX_HH_H
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include <pwd.h>
+
+#include "base/intern_string.hh"
+#include "base/opt_util.hh"
+#include "shlex.resolver.hh"
+
+enum class shlex_token_t {
+ ST_ERROR,
+ ST_WHITESPACE,
+ ST_ESCAPE,
+ ST_DOUBLE_QUOTE_START,
+ ST_DOUBLE_QUOTE_END,
+ ST_SINGLE_QUOTE_START,
+ ST_SINGLE_QUOTE_END,
+ ST_VARIABLE_REF,
+ ST_QUOTED_VARIABLE_REF,
+ ST_TILDE,
+};
+
+class shlex {
+public:
+ shlex(const char* str, size_t len) : s_str(str), s_len(len){};
+
+ explicit shlex(const string_fragment& sf)
+ : s_str(sf.data()), s_len(sf.length())
+ {
+ }
+
+ explicit shlex(const std::string& str)
+ : s_str(str.c_str()), s_len(str.size()){};
+
+ shlex& with_ignore_quotes(bool val)
+ {
+ this->s_ignore_quotes = val;
+ return *this;
+ }
+
+ bool tokenize(string_fragment& cap_out, shlex_token_t& token_out);
+
+ template<typename Resolver = scoped_resolver>
+ bool eval(std::string& result, const Resolver& vars)
+ {
+ result.clear();
+
+ string_fragment cap;
+ shlex_token_t token;
+ int last_index = 0;
+
+ while (this->tokenize(cap, token)) {
+ result.append(&this->s_str[last_index], cap.sf_begin - last_index);
+ switch (token) {
+ case shlex_token_t::ST_ERROR:
+ return false;
+ case shlex_token_t::ST_ESCAPE:
+ result.append(1, this->s_str[cap.sf_begin + 1]);
+ break;
+ case shlex_token_t::ST_WHITESPACE:
+ result.append(&this->s_str[cap.sf_begin], cap.length());
+ break;
+ case shlex_token_t::ST_VARIABLE_REF:
+ case shlex_token_t::ST_QUOTED_VARIABLE_REF: {
+ int extra = token == shlex_token_t::ST_VARIABLE_REF ? 0 : 1;
+ std::string var_name(&this->s_str[cap.sf_begin + 1 + extra],
+ cap.length() - 1 - extra * 2);
+ auto local_var = vars.find(var_name);
+ const char* var_value = getenv(var_name.c_str());
+
+ if (local_var != vars.end()) {
+ result.append(fmt::to_string(local_var->second));
+ } else if (var_value != nullptr) {
+ result.append(var_value);
+ }
+ break;
+ }
+ case shlex_token_t::ST_TILDE:
+ this->resolve_home_dir(result, cap);
+ break;
+ case shlex_token_t::ST_DOUBLE_QUOTE_START:
+ case shlex_token_t::ST_DOUBLE_QUOTE_END:
+ result.append("\"");
+ break;
+ case shlex_token_t::ST_SINGLE_QUOTE_START:
+ case shlex_token_t::ST_SINGLE_QUOTE_END:
+ result.append("'");
+ break;
+ default:
+ break;
+ }
+ last_index = cap.sf_end;
+ }
+
+ result.append(&this->s_str[last_index], this->s_len - last_index);
+
+ return true;
+ }
+
+ template<typename Resolver>
+ bool split(std::vector<std::string>& result, const Resolver& vars)
+ {
+ result.clear();
+
+ string_fragment cap;
+ shlex_token_t token;
+ int last_index = 0;
+ bool start_new = true;
+
+ while (isspace(this->s_str[this->s_index])) {
+ this->s_index += 1;
+ }
+ while (this->tokenize(cap, token)) {
+ if (start_new) {
+ result.emplace_back("");
+ start_new = false;
+ }
+ result.back().append(&this->s_str[last_index],
+ cap.sf_begin - last_index);
+ switch (token) {
+ case shlex_token_t::ST_ERROR:
+ return false;
+ case shlex_token_t::ST_ESCAPE:
+ result.back().append(1, this->s_str[cap.sf_begin + 1]);
+ break;
+ case shlex_token_t::ST_WHITESPACE:
+ start_new = true;
+ break;
+ case shlex_token_t::ST_VARIABLE_REF:
+ case shlex_token_t::ST_QUOTED_VARIABLE_REF: {
+ int extra = token == shlex_token_t::ST_VARIABLE_REF ? 0 : 1;
+ std::string var_name(&this->s_str[cap.sf_begin + 1 + extra],
+ cap.length() - 1 - extra * 2);
+ auto local_var = vars.find(var_name);
+ const char* var_value = getenv(var_name.c_str());
+
+ if (local_var != vars.end()) {
+ result.back().append(fmt::to_string(local_var->second));
+ } else if (var_value != nullptr) {
+ result.back().append(var_value);
+ }
+ break;
+ }
+ case shlex_token_t::ST_TILDE:
+ this->resolve_home_dir(result.back(), cap);
+ break;
+ default:
+ break;
+ }
+ last_index = cap.sf_end;
+ }
+
+ if (last_index < this->s_len) {
+ if (start_new || result.empty()) {
+ result.emplace_back("");
+ }
+ result.back().append(&this->s_str[last_index],
+ this->s_len - last_index);
+ }
+
+ return true;
+ }
+
+ void reset()
+ {
+ this->s_index = 0;
+ this->s_state = state_t::STATE_NORMAL;
+ }
+
+ void scan_variable_ref(string_fragment& cap_out, shlex_token_t& token_out);
+
+ void resolve_home_dir(std::string& result, string_fragment cap) const;
+
+ enum class state_t {
+ STATE_NORMAL,
+ STATE_IN_DOUBLE_QUOTE,
+ STATE_IN_SINGLE_QUOTE,
+ };
+
+ const char* s_str;
+ ssize_t s_len;
+ bool s_ignore_quotes{false};
+ ssize_t s_index{0};
+ state_t s_state{state_t::STATE_NORMAL};
+};
+
+#endif // LNAV_SHLEX_HH_H
diff --git a/src/shlex.resolver.hh b/src/shlex.resolver.hh
new file mode 100644
index 0000000..404d142
--- /dev/null
+++ b/src/shlex.resolver.hh
@@ -0,0 +1,94 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file shlex.resolver.hh
+ */
+
+#ifndef lnav_shlex_resolver_hh
+#define lnav_shlex_resolver_hh
+
+#include <map>
+#include <string>
+#include <vector>
+
+#include "base/intern_string.hh"
+#include "fmt/format.h"
+#include "mapbox/variant.hpp"
+
+struct null_value_t {};
+using scoped_value_t = mapbox::util::
+ variant<std::string, string_fragment, int64_t, double, null_value_t>;
+
+namespace fmt {
+template<>
+struct formatter<scoped_value_t> : formatter<std::string> {
+ template<typename FormatContext>
+ auto format(const scoped_value_t& sv, FormatContext& ctx) const
+ {
+ auto retval
+ = sv.match([](std::string str) { return str; },
+ [](string_fragment sf) { return sf.to_string(); },
+ [](null_value_t) { return std::string("<NULL>"); },
+ [](int64_t value) { return fmt::to_string(value); },
+ [](double value) { return fmt::to_string(value); });
+
+ return fmt::formatter<std::string>::format(retval, ctx);
+ }
+};
+} // namespace fmt
+
+class scoped_resolver {
+public:
+ scoped_resolver(
+ std::initializer_list<std::map<std::string, scoped_value_t>*> l)
+ {
+ this->sr_stack.insert(this->sr_stack.end(), l.begin(), l.end());
+ }
+
+ using const_iterator
+ = std::map<std::string, scoped_value_t>::const_iterator;
+
+ const_iterator find(const std::string& str) const
+ {
+ const_iterator retval;
+
+ for (const auto* scope : this->sr_stack) {
+ if ((retval = scope->find(str)) != scope->end()) {
+ return retval;
+ }
+ }
+
+ return this->end();
+ }
+
+ const_iterator end() const { return this->sr_stack.back()->end(); }
+
+ std::vector<const std::map<std::string, scoped_value_t>*> sr_stack;
+};
+
+#endif
diff --git a/src/simdutf8check.h b/src/simdutf8check.h
new file mode 100644
index 0000000..4a3a364
--- /dev/null
+++ b/src/simdutf8check.h
@@ -0,0 +1,237 @@
+/**
+ * https://github.com/lemire/fastvalidate-utf-8
+ */
+
+#ifndef SIMDUTF8CHECK_H
+#define SIMDUTF8CHECK_H
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdint.h>
+#include <x86intrin.h>
+
+#include "base/lnav_log.hh"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*
+ * legal utf-8 byte sequence
+ * http://www.unicode.org/versions/Unicode6.0.0/ch03.pdf - page 94
+ *
+ * Code Points 1st 2s 3s 4s
+ * U+0000..U+007F 00..7F
+ * U+0080..U+07FF C2..DF 80..BF
+ * U+0800..U+0FFF E0 A0..BF 80..BF
+ * U+1000..U+CFFF E1..EC 80..BF 80..BF
+ * U+D000..U+D7FF ED 80..9F 80..BF
+ * U+E000..U+FFFF EE..EF 80..BF 80..BF
+ * U+10000..U+3FFFF F0 90..BF 80..BF 80..BF
+ * U+40000..U+FFFFF F1..F3 80..BF 80..BF 80..BF
+ * U+100000..U+10FFFF F4 80..8F 80..BF 80..BF
+ *
+ */
+
+// all byte values must be no larger than 0xF4
+static void checkSmallerThan0xF4(__m128i current_bytes,
+ __m128i *has_error)
+{
+ // unsigned, saturates to 0 below max
+ *has_error = _mm_or_si128(*has_error,
+ _mm_subs_epu8(current_bytes,
+ _mm_set1_epi8(0xF4)));
+}
+
+static __m128i continuationLengths(__m128i high_nibbles)
+{
+ return _mm_shuffle_epi8(
+ _mm_setr_epi8(1, 1, 1, 1, 1, 1, 1, 1, // 0xxx (ASCII)
+ 0, 0, 0, 0, // 10xx (continuation)
+ 2, 2, // 110x
+ 3, // 1110
+ 4), // 1111, next should be 0 (not checked here)
+ high_nibbles);
+}
+
+static __m128i carryContinuations(__m128i initial_lengths,
+ __m128i previous_carries)
+{
+
+ __m128i right1 = _mm_subs_epu8(
+ _mm_alignr_epi8(initial_lengths, previous_carries, 16 - 1),
+ _mm_set1_epi8(1));
+ __m128i sum = _mm_add_epi8(initial_lengths, right1);
+
+ __m128i right2 = _mm_subs_epu8(
+ _mm_alignr_epi8(sum, previous_carries, 16 - 2),
+ _mm_set1_epi8(2));
+ return _mm_add_epi8(sum, right2);
+}
+
+static void checkContinuations(__m128i initial_lengths,
+ __m128i carries,
+ __m128i *has_error)
+{
+
+ // overlap || underlap
+ // carry > length && length > 0 || !(carry > length) && !(length > 0)
+ // (carries > length) == (lengths > 0)
+ __m128i overunder = _mm_cmpeq_epi8(
+ _mm_cmpgt_epi8(carries, initial_lengths),
+ _mm_cmpgt_epi8(initial_lengths, _mm_setzero_si128()));
+
+ *has_error = _mm_or_si128(*has_error, overunder);
+}
+
+// when 0xED is found, next byte must be no larger than 0x9F
+// when 0xF4 is found, next byte must be no larger than 0x8F
+// next byte must be continuation, ie sign bit is set, so signed < is ok
+static void checkFirstContinuationMax(__m128i current_bytes,
+ __m128i off1_current_bytes,
+ __m128i *has_error)
+{
+ __m128i maskED = _mm_cmpeq_epi8(off1_current_bytes, _mm_set1_epi8(0xED));
+ __m128i maskF4 = _mm_cmpeq_epi8(off1_current_bytes, _mm_set1_epi8(0xF4));
+
+ __m128i badfollowED = _mm_and_si128(
+ _mm_cmpgt_epi8(current_bytes, _mm_set1_epi8(0x9F)),
+ maskED);
+ __m128i badfollowF4 = _mm_and_si128(
+ _mm_cmpgt_epi8(current_bytes, _mm_set1_epi8(0x8F)),
+ maskF4);
+
+ *has_error = _mm_or_si128(*has_error,
+ _mm_or_si128(badfollowED, badfollowF4));
+}
+
+// map off1_hibits => error condition
+// hibits off1 cur
+// C => < C2 && true
+// E => < E1 && < A0
+// F => < F1 && < 90
+// else false && false
+static void checkOverlong(__m128i current_bytes,
+ __m128i off1_current_bytes,
+ __m128i hibits,
+ __m128i previous_hibits,
+ __m128i *has_error)
+{
+ __m128i off1_hibits = _mm_alignr_epi8(hibits, previous_hibits, 16 - 1);
+ __m128i initial_mins = _mm_shuffle_epi8(
+ _mm_setr_epi8(-128, -128, -128, -128, -128, -128, -128, -128,
+ -128, -128, -128, -128, // 10xx => false
+ 0xC2, -128, // 110x
+ 0xE1, // 1110
+ 0xF1),
+ off1_hibits);
+
+ __m128i initial_under = _mm_cmpgt_epi8(initial_mins, off1_current_bytes);
+
+ __m128i second_mins = _mm_shuffle_epi8(
+ _mm_setr_epi8(-128, -128, -128, -128, -128, -128, -128, -128,
+ -128, -128, -128, -128, // 10xx => false
+ 127, 127, // 110x => true
+ 0xA0, // 1110
+ 0x90),
+ off1_hibits);
+ __m128i second_under = _mm_cmpgt_epi8(second_mins, current_bytes);
+ *has_error = _mm_or_si128(*has_error,
+ _mm_and_si128(initial_under, second_under));
+}
+
+struct processed_utf_bytes {
+ __m128i rawbytes;
+ __m128i high_nibbles;
+ __m128i carried_continuations;
+};
+
+static void count_nibbles(__m128i bytes,
+ struct processed_utf_bytes *answer)
+{
+ answer->rawbytes = bytes;
+ answer->high_nibbles = _mm_and_si128(_mm_srli_epi16(bytes, 4),
+ _mm_set1_epi8(0x0F));
+}
+
+// check whether the current bytes are valid UTF-8
+// at the end of the function, previous gets updated
+static struct processed_utf_bytes
+checkUTF8Bytes(__m128i current_bytes, struct processed_utf_bytes *previous,
+ __m128i *has_error)
+{
+ struct processed_utf_bytes pb;
+ count_nibbles(current_bytes, &pb);
+
+ checkSmallerThan0xF4(current_bytes, has_error);
+
+ __m128i initial_lengths = continuationLengths(pb.high_nibbles);
+
+ pb.carried_continuations = carryContinuations(
+ initial_lengths,
+ previous->carried_continuations);
+
+ checkContinuations(initial_lengths, pb.carried_continuations, has_error);
+
+ __m128i off1_current_bytes =
+ _mm_alignr_epi8(pb.rawbytes, previous->rawbytes, 16 - 1);
+ checkFirstContinuationMax(current_bytes, off1_current_bytes,
+ has_error);
+
+ checkOverlong(current_bytes, off1_current_bytes,
+ pb.high_nibbles, previous->high_nibbles, has_error);
+ return pb;
+}
+
+static bool validate_utf8_fast(const char *src, size_t len, ssize_t *len_out)
+{
+ size_t i = 0, orig_len = len;
+ __m128i has_error = _mm_setzero_si128();
+ __m128i lfchars = _mm_set1_epi8('\n');
+ __m128i lfresult = _mm_setzero_si128();
+ struct processed_utf_bytes previous = {.rawbytes = _mm_setzero_si128(),
+ .high_nibbles = _mm_setzero_si128(),
+ .carried_continuations = _mm_setzero_si128()};
+ if (len >= 16) {
+ for (; i <= len - 16; i += 16) {
+ __m128i current_bytes = _mm_loadu_si128(
+ (const __m128i *) (src + i));
+ previous = checkUTF8Bytes(current_bytes, &previous, &has_error);
+ lfresult = _mm_cmpeq_epi8(current_bytes, lfchars);
+ if (_mm_movemask_epi8(lfresult)) {
+ for (; src[i] != '\n'; i++) {
+ }
+ len = i;
+ break;
+ }
+ }
+ }
+
+ //last part
+ if (i < len) {
+ char buffer[16];
+ memset(buffer, 0, 16);
+ memcpy(buffer, src + i, len - i);
+ __m128i current_bytes = _mm_loadu_si128((const __m128i *) (buffer));
+ previous = checkUTF8Bytes(current_bytes, &previous, &has_error);
+ for (; i < len && src[i] != '\n'; i++) {
+ }
+ } else {
+ has_error = _mm_or_si128(_mm_cmpgt_epi8(previous.carried_continuations,
+ _mm_setr_epi8(9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9,
+ 9, 9, 9, 1)),
+ has_error);
+ }
+
+ if (i < orig_len && src[i] == '\n') {
+ *len_out = i;
+ }
+
+ return _mm_testz_si128(has_error, has_error);
+}
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/spectro_impls.cc b/src/spectro_impls.cc
new file mode 100644
index 0000000..c9f9412
--- /dev/null
+++ b/src/spectro_impls.cc
@@ -0,0 +1,518 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "spectro_impls.hh"
+
+#include "base/itertools.hh"
+#include "lnav.hh"
+#include "logfile_sub_source.hh"
+#include "scn/scn.h"
+
+using namespace lnav::roles::literals;
+
+class filtered_sub_source
+ : public text_sub_source
+ , public text_time_translator
+ , public list_overlay_source {
+public:
+ size_t text_line_count() override { return this->fss_lines.size(); }
+
+ void text_value_for_line(textview_curses& tc,
+ int line,
+ std::string& value_out,
+ line_flags_t flags) override
+ {
+ this->fss_lines | lnav::itertools::nth(line)
+ | lnav::itertools::for_each([&](const auto row) {
+ this->fss_delegate->text_value_for_line(
+ tc, *row, value_out, flags);
+ });
+ }
+
+ size_t text_size_for_line(textview_curses& tc,
+ int line,
+ line_flags_t raw) override
+ {
+ return this->fss_lines | lnav::itertools::nth(line)
+ | lnav::itertools::map([&](const auto row) {
+ return this->fss_delegate->text_size_for_line(tc, *row, raw);
+ })
+ | lnav::itertools::unwrap_or(size_t{0});
+ }
+
+ void text_attrs_for_line(textview_curses& tc,
+ int line,
+ string_attrs_t& value_out) override
+ {
+ this->fss_lines | lnav::itertools::nth(line)
+ | lnav::itertools::for_each([&](const auto row) {
+ this->fss_delegate->text_attrs_for_line(tc, *row, value_out);
+ });
+ }
+
+ nonstd::optional<vis_line_t> row_for_time(
+ struct timeval time_bucket) override
+ {
+ return this->fss_time_delegate->row_for_time(time_bucket);
+ }
+
+ nonstd::optional<struct timeval> time_for_row(vis_line_t row) override
+ {
+ return this->fss_lines | lnav::itertools::nth(row)
+ | lnav::itertools::flat_map([this](const auto row) {
+ return this->fss_time_delegate->time_for_row(*row);
+ });
+ }
+
+ bool list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t line,
+ attr_line_t& value_out) override
+ {
+ if (this->fss_overlay_delegate != nullptr) {
+ return this->fss_overlay_delegate->list_value_for_overlay(
+ lv, y, bottom, line, value_out);
+ }
+ return false;
+ }
+
+ text_sub_source* fss_delegate;
+ text_time_translator* fss_time_delegate;
+ list_overlay_source* fss_overlay_delegate{nullptr};
+ std::vector<vis_line_t> fss_lines;
+};
+
+log_spectro_value_source::log_spectro_value_source(intern_string_t colname)
+ : lsvs_colname(colname)
+{
+ this->update_stats();
+}
+
+void
+log_spectro_value_source::update_stats()
+{
+ auto& lss = lnav_data.ld_log_source;
+
+ this->lsvs_begin_time = 0;
+ this->lsvs_end_time = 0;
+ this->lsvs_stats.clear();
+ for (auto& ls : lss) {
+ auto* lf = ls->get_file_ptr();
+
+ if (lf == nullptr) {
+ continue;
+ }
+
+ auto format = lf->get_format();
+ const auto* stats = format->stats_for_value(this->lsvs_colname);
+
+ if (stats == nullptr) {
+ continue;
+ }
+
+ auto ll = lf->begin();
+
+ if (this->lsvs_begin_time == 0
+ || ll->get_time() < this->lsvs_begin_time)
+ {
+ this->lsvs_begin_time = ll->get_time();
+ }
+ ll = lf->end();
+ --ll;
+ if (ll->get_time() > this->lsvs_end_time) {
+ this->lsvs_end_time = ll->get_time();
+ }
+
+ this->lsvs_found = true;
+ this->lsvs_stats.merge(*stats);
+ }
+
+ if (this->lsvs_begin_time) {
+ time_t filtered_begin_time = lss.find_line(lss.at(0_vl))->get_time();
+ time_t filtered_end_time
+ = lss.find_line(lss.at(vis_line_t(lss.text_line_count() - 1)))
+ ->get_time();
+
+ if (filtered_begin_time > this->lsvs_begin_time) {
+ this->lsvs_begin_time = filtered_begin_time;
+ }
+ if (filtered_end_time < this->lsvs_end_time) {
+ this->lsvs_end_time = filtered_end_time;
+ }
+ }
+}
+
+void
+log_spectro_value_source::spectro_bounds(spectrogram_bounds& sb_out)
+{
+ auto& lss = lnav_data.ld_log_source;
+
+ if (lss.text_line_count() == 0) {
+ return;
+ }
+
+ this->update_stats();
+
+ sb_out.sb_begin_time = this->lsvs_begin_time;
+ sb_out.sb_end_time = this->lsvs_end_time;
+ sb_out.sb_min_value_out = this->lsvs_stats.lvs_min_value;
+ sb_out.sb_max_value_out = this->lsvs_stats.lvs_max_value;
+ sb_out.sb_count = this->lsvs_stats.lvs_count;
+}
+
+void
+log_spectro_value_source::spectro_row(spectrogram_request& sr,
+ spectrogram_row& row_out)
+{
+ auto& lss = lnav_data.ld_log_source;
+ auto begin_line = lss.find_from_time(sr.sr_begin_time).value_or(0_vl);
+ auto end_line
+ = lss.find_from_time(sr.sr_end_time).value_or(lss.text_line_count());
+
+ for (const auto& msg_info : lss.window_at(begin_line, end_line)) {
+ const auto& ll = msg_info.get_logline();
+ if (ll.get_time() >= sr.sr_end_time) {
+ break;
+ }
+
+ const auto& values = msg_info.get_values();
+ auto lv_iter = find_if(values.lvv_values.begin(),
+ values.lvv_values.end(),
+ logline_value_cmp(&this->lsvs_colname));
+
+ if (lv_iter != values.lvv_values.end()) {
+ switch (lv_iter->lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_FLOAT:
+ row_out.add_value(sr, lv_iter->lv_value.d, ll.is_marked());
+ break;
+ case value_kind_t::VALUE_INTEGER: {
+ row_out.add_value(sr, lv_iter->lv_value.i, ll.is_marked());
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ row_out.sr_details_source_provider = [this](const spectrogram_request& sr,
+ double range_min,
+ double range_max) {
+ auto& lss = lnav_data.ld_log_source;
+ auto retval = std::make_unique<filtered_sub_source>();
+ auto begin_line = lss.find_from_time(sr.sr_begin_time).value_or(0_vl);
+ auto end_line = lss.find_from_time(sr.sr_end_time)
+ .value_or(lss.text_line_count());
+
+ retval->fss_delegate = &lss;
+ retval->fss_time_delegate = &lss;
+ retval->fss_overlay_delegate = nullptr;
+ for (const auto& msg_info : lss.window_at(begin_line, end_line)) {
+ const auto& ll = msg_info.get_logline();
+ if (ll.get_time() >= sr.sr_end_time) {
+ break;
+ }
+
+ const auto& values = msg_info.get_values();
+ auto lv_iter = find_if(values.lvv_values.begin(),
+ values.lvv_values.end(),
+ logline_value_cmp(&this->lsvs_colname));
+
+ if (lv_iter != values.lvv_values.end()) {
+ switch (lv_iter->lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_FLOAT:
+ if (range_min <= lv_iter->lv_value.d
+ && lv_iter->lv_value.d < range_max)
+ {
+ retval->fss_lines.emplace_back(
+ msg_info.get_vis_line());
+ }
+ break;
+ case value_kind_t::VALUE_INTEGER:
+ if (range_min <= lv_iter->lv_value.i
+ && lv_iter->lv_value.i < range_max)
+ {
+ retval->fss_lines.emplace_back(
+ msg_info.get_vis_line());
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+
+ return retval;
+ };
+}
+
+void
+log_spectro_value_source::spectro_mark(textview_curses& tc,
+ time_t begin_time,
+ time_t end_time,
+ double range_min,
+ double range_max)
+{
+ // XXX need to refactor this and the above method
+ auto& log_tc = lnav_data.ld_views[LNV_LOG];
+ auto& lss = lnav_data.ld_log_source;
+ vis_line_t begin_line = lss.find_from_time(begin_time).value_or(0_vl);
+ vis_line_t end_line
+ = lss.find_from_time(end_time).value_or(lss.text_line_count());
+ logline_value_vector values;
+ string_attrs_t sa;
+
+ for (vis_line_t curr_line = begin_line; curr_line < end_line; ++curr_line) {
+ content_line_t cl = lss.at(curr_line);
+ std::shared_ptr<logfile> lf = lss.find(cl);
+ auto ll = lf->begin() + cl;
+ auto format = lf->get_format();
+
+ if (!ll->is_message()) {
+ continue;
+ }
+
+ values.clear();
+ lf->read_full_message(ll, values.lvv_sbr);
+ values.lvv_sbr.erase_ansi();
+ sa.clear();
+ format->annotate(cl, sa, values, false);
+
+ auto lv_iter = find_if(values.lvv_values.begin(),
+ values.lvv_values.end(),
+ logline_value_cmp(&this->lsvs_colname));
+
+ if (lv_iter != values.lvv_values.end()) {
+ switch (lv_iter->lv_meta.lvm_kind) {
+ case value_kind_t::VALUE_FLOAT:
+ if (range_min <= lv_iter->lv_value.d
+ && lv_iter->lv_value.d <= range_max)
+ {
+ log_tc.toggle_user_mark(&textview_curses::BM_USER,
+ curr_line);
+ }
+ break;
+ case value_kind_t::VALUE_INTEGER:
+ if (range_min <= lv_iter->lv_value.i
+ && lv_iter->lv_value.i <= range_max)
+ {
+ log_tc.toggle_user_mark(&textview_curses::BM_USER,
+ curr_line);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+ }
+}
+
+db_spectro_value_source::db_spectro_value_source(std::string colname)
+ : dsvs_colname(std::move(colname))
+{
+ this->update_stats();
+}
+
+void
+db_spectro_value_source::update_stats()
+{
+ this->dsvs_begin_time = 0;
+ this->dsvs_end_time = 0;
+ this->dsvs_stats.clear();
+
+ auto& dls = lnav_data.ld_db_row_source;
+ auto& chart = dls.dls_chart;
+
+ this->dsvs_column_index = dls.column_name_to_index(this->dsvs_colname);
+
+ if (!dls.has_log_time_column()) {
+ if (dls.dls_time_column_invalidated_at) {
+ static const auto order_by_help = attr_line_t()
+ .append("ORDER BY"_keyword)
+ .append(" ")
+ .append("log_time"_variable)
+ .append(" ")
+ .append("ASC"_keyword);
+
+ this->dsvs_error_msg
+ = lnav::console::user_message::error(
+ "Cannot generate spectrogram for database results")
+ .with_reason(
+ attr_line_t()
+ .append("The ")
+ .append_quoted("log_time"_variable)
+ .appendf(
+ FMT_STRING(" column is not in ascending "
+ "order between rows {} and {}"),
+ dls.dls_time_column_invalidated_at.value()
+ - 1,
+ dls.dls_time_column_invalidated_at.value()))
+ .with_note(
+ attr_line_t("An ascending ")
+ .append_quoted("log_time"_variable)
+ .append(
+ " column is needed to render a spectrogram"))
+ .with_help(attr_line_t("Add an ")
+ .append_quoted(order_by_help)
+ .append(" clause to your ")
+ .append("SELECT"_keyword)
+ .append(" statement"));
+ } else {
+ this->dsvs_error_msg
+ = lnav::console::user_message::error(
+ "Cannot generate spectrogram for database results")
+ .with_reason(
+ attr_line_t()
+ .append("No ")
+ .append_quoted("log_time"_variable)
+ .append(" column found in the result set"))
+ .with_note(
+ attr_line_t("An ascending ")
+ .append_quoted("log_time"_variable)
+ .append(
+ " column is needed to render a spectrogram"))
+ .with_help(
+ attr_line_t("Include a ")
+ .append_quoted("log_time"_variable)
+ .append(" column in your ")
+ .append(" statement. Use an ")
+ .append("AS"_keyword)
+ .append(
+ " directive to alias a computed timestamp"));
+ }
+ return;
+ }
+
+ if (!this->dsvs_column_index) {
+ this->dsvs_error_msg
+ = lnav::console::user_message::error(
+ "Cannot generate spectrogram for database results")
+ .with_reason(attr_line_t("unknown column -- ")
+ .append_quoted(lnav::roles::variable(
+ this->dsvs_colname)))
+ .with_help("Expecting a numeric column to visualize");
+ return;
+ }
+
+ if (!dls.dls_headers[this->dsvs_column_index.value()].hm_graphable) {
+ this->dsvs_error_msg
+ = lnav::console::user_message::error(
+ "Cannot generate spectrogram for database results")
+ .with_reason(attr_line_t()
+ .append_quoted(lnav::roles::variable(
+ this->dsvs_colname))
+ .append(" is not a numeric column"))
+ .with_help("Only numeric columns can be visualized");
+ return;
+ }
+
+ if (dls.dls_rows.empty()) {
+ this->dsvs_error_msg
+ = lnav::console::user_message::error(
+ "Cannot generate spectrogram for database results")
+ .with_reason("Result set is empty");
+ return;
+ }
+
+ auto bs = chart.get_stats_for(this->dsvs_colname);
+
+ this->dsvs_begin_time = dls.dls_time_column.front().tv_sec;
+ this->dsvs_end_time = dls.dls_time_column.back().tv_sec;
+ this->dsvs_stats.lvs_min_value = bs.bs_min_value;
+ this->dsvs_stats.lvs_max_value = bs.bs_max_value;
+ this->dsvs_stats.lvs_count = dls.dls_rows.size();
+}
+
+void
+db_spectro_value_source::spectro_bounds(spectrogram_bounds& sb_out)
+{
+ auto& dls = lnav_data.ld_db_row_source;
+
+ if (dls.text_line_count() == 0) {
+ return;
+ }
+
+ this->update_stats();
+
+ sb_out.sb_begin_time = this->dsvs_begin_time;
+ sb_out.sb_end_time = this->dsvs_end_time;
+ sb_out.sb_min_value_out = this->dsvs_stats.lvs_min_value;
+ sb_out.sb_max_value_out = this->dsvs_stats.lvs_max_value;
+ sb_out.sb_count = this->dsvs_stats.lvs_count;
+}
+
+void
+db_spectro_value_source::spectro_row(spectrogram_request& sr,
+ spectrogram_row& row_out)
+{
+ auto& dls = lnav_data.ld_db_row_source;
+ auto begin_row = dls.row_for_time({sr.sr_begin_time, 0}).value_or(0_vl);
+ auto end_row
+ = dls.row_for_time({sr.sr_end_time, 0}).value_or(dls.dls_rows.size());
+
+ for (auto lpc = begin_row; lpc < end_row; ++lpc) {
+ auto scan_res = scn::scan_value<double>(scn::string_view{
+ dls.dls_rows[lpc][this->dsvs_column_index.value()]});
+
+ if (scan_res) {
+ row_out.add_value(sr, scan_res.value(), false);
+ }
+ }
+
+ row_out.sr_details_source_provider = [this](const spectrogram_request& sr,
+ double range_min,
+ double range_max) {
+ auto& dls = lnav_data.ld_db_row_source;
+ auto retval = std::make_unique<filtered_sub_source>();
+
+ retval->fss_delegate = &dls;
+ retval->fss_time_delegate = &dls;
+ retval->fss_overlay_delegate = &lnav_data.ld_db_overlay;
+ auto begin_row = dls.row_for_time({sr.sr_begin_time, 0}).value_or(0_vl);
+ auto end_row = dls.row_for_time({sr.sr_end_time, 0})
+ .value_or(dls.dls_rows.size());
+
+ for (auto lpc = begin_row; lpc < end_row; ++lpc) {
+ auto scan_res = scn::scan_value<double>(scn::string_view{
+ dls.dls_rows[lpc][this->dsvs_column_index.value()]});
+ if (!scan_res) {
+ continue;
+ }
+ auto value = scan_res.value();
+ if ((range_min == value)
+ || (range_min < value && value < range_max))
+ {
+ retval->fss_lines.emplace_back(lpc);
+ }
+ }
+
+ return retval;
+ };
+}
diff --git a/src/spectro_impls.hh b/src/spectro_impls.hh
new file mode 100644
index 0000000..9d4ddeb
--- /dev/null
+++ b/src/spectro_impls.hh
@@ -0,0 +1,87 @@
+/**
+ * Copyright (c) 2007-2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_spectro_impls_hh
+#define lnav_spectro_impls_hh
+
+#include "log_format.hh"
+#include "spectro_source.hh"
+
+class log_spectro_value_source : public spectrogram_value_source {
+public:
+ log_spectro_value_source(intern_string_t colname);
+
+ void update_stats();
+
+ void spectro_bounds(spectrogram_bounds& sb_out) override;
+
+ void spectro_row(spectrogram_request& sr,
+ spectrogram_row& row_out) override;
+
+ void spectro_mark(textview_curses& tc,
+ time_t begin_time,
+ time_t end_time,
+ double range_min,
+ double range_max) override;
+
+ intern_string_t lsvs_colname;
+ logline_value_stats lsvs_stats;
+ time_t lsvs_begin_time{0};
+ time_t lsvs_end_time{0};
+ bool lsvs_found{false};
+};
+
+class db_spectro_value_source : public spectrogram_value_source {
+public:
+ db_spectro_value_source(std::string colname);
+
+ void update_stats();
+
+ void spectro_bounds(spectrogram_bounds& sb_out) override;
+
+ void spectro_row(spectrogram_request& sr,
+ spectrogram_row& row_out) override;
+
+ void spectro_mark(textview_curses& tc,
+ time_t begin_time,
+ time_t end_time,
+ double range_min,
+ double range_max) override
+ {
+ }
+
+ std::string dsvs_colname;
+ logline_value_stats dsvs_stats;
+ time_t dsvs_begin_time{0};
+ time_t dsvs_end_time{0};
+ nonstd::optional<size_t> dsvs_column_index;
+ nonstd::optional<lnav::console::user_message> dsvs_error_msg;
+};
+
+#endif
diff --git a/src/spectro_source.cc b/src/spectro_source.cc
new file mode 100644
index 0000000..e0dc802
--- /dev/null
+++ b/src/spectro_source.cc
@@ -0,0 +1,615 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file spectro_source.cc
+ */
+
+#include "spectro_source.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/math_util.hh"
+#include "command_executor.hh"
+#include "config.h"
+
+nonstd::optional<size_t>
+spectrogram_row::nearest_column(size_t current) const
+{
+ nonstd::optional<size_t> retval;
+ nonstd::optional<size_t> nearest_distance;
+
+ for (size_t lpc = 0; lpc < this->sr_width; lpc++) {
+ if (this->sr_values[lpc].rb_counter == 0) {
+ continue;
+ }
+ auto curr_distance = abs_diff(lpc, current);
+
+ if (!retval || curr_distance < nearest_distance.value()) {
+ retval = lpc;
+ nearest_distance = abs_diff(lpc, current);
+ }
+ }
+
+ return retval;
+}
+
+bool
+spectrogram_source::list_input_handle_key(listview_curses& lv, int ch)
+{
+ switch (ch) {
+ case 'm': {
+ auto sel = lv.get_selection();
+ if (sel < 0 || (size_t) sel >= this->text_line_count()
+ || !this->ss_cursor_column || this->ss_value_source == nullptr)
+ {
+ alerter::singleton().chime(
+ "a value must be selected before it can be marked");
+ return true;
+ }
+
+ unsigned long width;
+ vis_line_t height;
+
+ lv.get_dimensions(height, width);
+ width -= 2;
+
+ auto& sb = this->ss_cached_bounds;
+ auto begin_time_opt = this->time_for_row_int(sel);
+ if (!begin_time_opt) {
+ return true;
+ }
+ auto begin_time = begin_time_opt.value();
+ struct timeval end_time = begin_time;
+
+ end_time.tv_sec += this->ss_granularity;
+ double range_min, range_max, column_size;
+
+ column_size = (sb.sb_max_value_out - sb.sb_min_value_out)
+ / (double) (width - 1);
+ range_min = sb.sb_min_value_out
+ + this->ss_cursor_column.value_or(0) * column_size;
+ range_max = range_min + column_size;
+ this->ss_value_source->spectro_mark((textview_curses&) lv,
+ begin_time.tv_sec,
+ end_time.tv_sec,
+ range_min,
+ range_max);
+ this->invalidate();
+ lv.reload_data();
+ return true;
+ }
+
+ case KEY_CTRL_A: {
+ if (this->ss_value_source != nullptr) {
+ this->ss_cursor_column = 0;
+ this->text_selection_changed((textview_curses&) lv);
+ lv.set_needs_update();
+ }
+ return true;
+ }
+
+ case KEY_CTRL_E: {
+ if (this->ss_value_source != nullptr) {
+ this->ss_cursor_column = INT_MAX;
+ this->text_selection_changed((textview_curses&) lv);
+ lv.set_needs_update();
+ }
+ return true;
+ }
+
+ case KEY_LEFT:
+ case KEY_RIGHT: {
+ auto sel = lv.get_selection();
+ unsigned long width;
+ vis_line_t height;
+ string_attrs_t sa;
+
+ lv.get_dimensions(height, width);
+
+ this->text_attrs_for_line((textview_curses&) lv, sel, sa);
+
+ if (sa.empty()) {
+ this->ss_cursor_column = nonstd::nullopt;
+ return true;
+ }
+
+ if (!this->ss_cursor_column) {
+ lv.set_selection(0_vl);
+ }
+ struct line_range lr(this->ss_cursor_column.value(),
+ this->ss_cursor_column.value() + 1);
+
+ auto current = find_string_attr(sa, lr);
+
+ if (current != sa.end()) {
+ if (ch == KEY_LEFT) {
+ if (current == sa.begin()) {
+ current = sa.end();
+ } else {
+ --current;
+ }
+ } else {
+ ++current;
+ }
+ }
+
+ if (current == sa.end()) {
+ if (ch == KEY_LEFT) {
+ current = sa.end();
+ --current;
+ } else {
+ current = sa.begin();
+ }
+ }
+ this->ss_cursor_column = current->sa_range.lr_start;
+
+ lv.reload_data();
+
+ return true;
+ }
+ default:
+ return false;
+ }
+}
+
+bool
+spectrogram_source::list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t row,
+ attr_line_t& value_out)
+{
+ vis_line_t height;
+ unsigned long width;
+
+ lv.get_dimensions(height, width);
+ width -= 2;
+
+ if (y > 0) {
+ auto sel = lv.get_selection();
+ auto selected_y = sel - lv.get_top() + 2;
+
+ if (y == selected_y && this->ss_cursor_column) {
+ const auto& s_row = this->load_row(lv, sel);
+ const auto& bucket
+ = s_row.sr_values[this->ss_cursor_column.value()];
+ auto& sb = this->ss_cached_bounds;
+ spectrogram_request sr(sb);
+
+ auto sel_time = rounddown(sb.sb_begin_time, this->ss_granularity)
+ + sel * this->ss_granularity;
+ sr.sr_width = width;
+ sr.sr_begin_time = sel_time;
+ sr.sr_end_time = sel_time + this->ss_granularity;
+ sr.sr_column_size = (sb.sb_max_value_out - sb.sb_min_value_out)
+ / (double) (width - 1);
+ auto range_min = sb.sb_min_value_out
+ + this->ss_cursor_column.value() * sr.sr_column_size;
+ auto range_max = range_min + sr.sr_column_size;
+
+ auto desc
+ = attr_line_t()
+ .append(lnav::roles::number(
+ fmt::to_string(bucket.rb_counter)))
+ .append(fmt::format(FMT_STRING(" value{} in the range "),
+ bucket.rb_counter == 1 ? "" : "s"))
+ .append(lnav::roles::number(
+ fmt::format(FMT_STRING("{:.2Lf}"), range_min)))
+ .append("-")
+ .append(lnav::roles::number(
+ fmt::format(FMT_STRING("{:.2Lf}"), range_max)))
+ .append(" ");
+ auto mark_offset = this->ss_cursor_column.value();
+ auto mark_is_before = true;
+
+ value_out.al_attrs.emplace_back(
+ line_range{0, -1}, VC_ROLE.value(role_t::VCR_STATUS_INFO));
+ if (desc.length() + 8 > width) {
+ desc.clear();
+ }
+
+ if (this->ss_cursor_column.value() + desc.length() + 1 > width) {
+ mark_offset -= desc.length();
+ mark_is_before = false;
+ }
+ value_out.append(mark_offset, ' ');
+ if (mark_is_before) {
+ value_out.append("\u25b2 ");
+ }
+ value_out.append(desc);
+ if (!mark_is_before) {
+ value_out.append("\u25b2 ");
+ }
+
+ if (this->ss_details_view != nullptr) {
+ if (s_row.sr_details_source_provider) {
+ auto row_details_source = s_row.sr_details_source_provider(
+ sr, range_min, range_max);
+
+ this->ss_details_view->set_sub_source(
+ row_details_source.get());
+ this->ss_details_source = std::move(row_details_source);
+ auto* overlay_source = dynamic_cast<list_overlay_source*>(
+ this->ss_details_source.get());
+ if (overlay_source != nullptr) {
+ this->ss_details_view->set_overlay_source(
+ overlay_source);
+ }
+ } else {
+ this->ss_details_view->set_sub_source(
+ this->ss_no_details_source);
+ this->ss_details_view->set_overlay_source(nullptr);
+ }
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ auto& line = value_out.get_string();
+ char buf[128];
+
+ this->cache_bounds();
+
+ if (this->ss_cached_line_count == 0) {
+ value_out
+ .append(lnav::roles::error("error: no data available, use the "))
+ .append_quoted(lnav::roles::keyword(":spectrogram"))
+ .append(lnav::roles::error(" command to visualize numeric data"));
+ return true;
+ }
+
+ auto& sb = this->ss_cached_bounds;
+ auto& st = this->ss_cached_thresholds;
+
+ snprintf(buf, sizeof(buf), "Min: %'.10lg", sb.sb_min_value_out);
+ line = buf;
+
+ snprintf(buf,
+ sizeof(buf),
+ ANSI_ROLE(" ") " 1-%'d " ANSI_ROLE(" ") " %'d-%'d " ANSI_ROLE(
+ " ") " %'d+",
+ role_t::VCR_LOW_THRESHOLD,
+ st.st_green_threshold - 1,
+ role_t::VCR_MED_THRESHOLD,
+ st.st_green_threshold,
+ st.st_yellow_threshold - 1,
+ role_t::VCR_HIGH_THRESHOLD,
+ st.st_yellow_threshold);
+ auto buflen = strlen(buf);
+ if (line.length() + buflen + 20 < width) {
+ line.append(width / 2 - buflen / 3 - line.length(), ' ');
+ } else {
+ line.append(" ");
+ }
+ line.append(buf);
+ scrub_ansi_string(line, &value_out.get_attrs());
+
+ snprintf(buf, sizeof(buf), "Max: %'.10lg", sb.sb_max_value_out);
+ buflen = strlen(buf);
+ if (line.length() + buflen + 4 < width) {
+ line.append(width - buflen - line.length() - 2, ' ');
+ } else {
+ line.append(" ");
+ }
+ line.append(buf);
+
+ value_out.with_attr(string_attr(line_range(0, -1),
+ VC_STYLE.value(text_attrs{A_UNDERLINE})));
+
+ return true;
+}
+
+size_t
+spectrogram_source::text_line_count()
+{
+ if (this->ss_value_source == nullptr) {
+ return 0;
+ }
+
+ this->cache_bounds();
+
+ return this->ss_cached_line_count;
+}
+
+size_t
+spectrogram_source::text_line_width(textview_curses& tc)
+{
+ if (tc.get_window() == nullptr) {
+ return 80;
+ }
+
+ unsigned long width;
+ vis_line_t height;
+
+ tc.get_dimensions(height, width);
+ return width;
+}
+
+nonstd::optional<struct timeval>
+spectrogram_source::time_for_row(vis_line_t row)
+{
+ if (this->ss_details_source != nullptr) {
+ auto* details_tss = dynamic_cast<text_time_translator*>(
+ this->ss_details_source.get());
+
+ if (details_tss != nullptr) {
+ return details_tss->time_for_row(this->ss_details_view->get_top());
+ }
+ }
+
+ return this->time_for_row_int(row);
+}
+
+nonstd::optional<struct timeval>
+spectrogram_source::time_for_row_int(vis_line_t row)
+{
+ struct timeval retval {
+ 0, 0
+ };
+
+ this->cache_bounds();
+ retval.tv_sec
+ = rounddown(this->ss_cached_bounds.sb_begin_time, this->ss_granularity)
+ + row * this->ss_granularity;
+
+ return retval;
+}
+
+nonstd::optional<vis_line_t>
+spectrogram_source::row_for_time(struct timeval time_bucket)
+{
+ if (this->ss_value_source == nullptr) {
+ return nonstd::nullopt;
+ }
+
+ time_t diff;
+ int retval;
+
+ this->cache_bounds();
+ auto grain_begin_time
+ = rounddown(this->ss_cached_bounds.sb_begin_time, this->ss_granularity);
+ if (time_bucket.tv_sec < grain_begin_time) {
+ return 0_vl;
+ }
+
+ diff = time_bucket.tv_sec - grain_begin_time;
+ retval = diff / this->ss_granularity;
+
+ return vis_line_t(retval);
+}
+
+void
+spectrogram_source::text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& value_out,
+ text_sub_source::line_flags_t flags)
+{
+ const auto& s_row = this->load_row(tc, row);
+ char tm_buffer[128];
+ struct tm tm;
+
+ auto row_time_opt = this->time_for_row_int(vis_line_t(row));
+ if (!row_time_opt) {
+ value_out.clear();
+ return;
+ }
+ auto row_time = row_time_opt.value();
+
+ gmtime_r(&row_time.tv_sec, &tm);
+ strftime(tm_buffer, sizeof(tm_buffer), " %a %b %d %H:%M:%S", &tm);
+
+ value_out = tm_buffer;
+ value_out.resize(s_row.sr_width, ' ');
+
+ for (size_t lpc = 0; lpc <= s_row.sr_width; lpc++) {
+ if (s_row.sr_values[lpc].rb_marks) {
+ value_out[lpc] = 'x';
+ }
+ }
+}
+
+void
+spectrogram_source::text_attrs_for_line(textview_curses& tc,
+ int row,
+ string_attrs_t& value_out)
+{
+ if (this->ss_value_source == nullptr) {
+ return;
+ }
+
+ const auto& st = this->ss_cached_thresholds;
+ const auto& s_row = this->load_row(tc, row);
+
+ for (int lpc = 0; lpc <= (int) s_row.sr_width; lpc++) {
+ int col_value = s_row.sr_values[lpc].rb_counter;
+
+ if (col_value == 0) {
+ continue;
+ }
+
+ role_t role;
+
+ if (col_value < st.st_green_threshold) {
+ role = role_t::VCR_LOW_THRESHOLD;
+ } else if (col_value < st.st_yellow_threshold) {
+ role = role_t::VCR_MED_THRESHOLD;
+ } else {
+ role = role_t::VCR_HIGH_THRESHOLD;
+ }
+ value_out.emplace_back(line_range(lpc, lpc + 1), VC_ROLE.value(role));
+ }
+}
+
+void
+spectrogram_source::reset_details_source()
+{
+ if (this->ss_details_view != nullptr) {
+ this->ss_details_view->set_sub_source(this->ss_no_details_source);
+ this->ss_details_view->set_overlay_source(nullptr);
+ }
+ this->ss_details_source.reset();
+}
+
+void
+spectrogram_source::cache_bounds()
+{
+ if (this->ss_value_source == nullptr) {
+ this->ss_cached_bounds.sb_count = 0;
+ this->ss_cached_bounds.sb_begin_time = 0;
+ this->ss_cursor_column = nonstd::nullopt;
+ this->reset_details_source();
+ return;
+ }
+
+ spectrogram_bounds sb;
+
+ this->ss_value_source->spectro_bounds(sb);
+
+ if (sb.sb_count == this->ss_cached_bounds.sb_count) {
+ return;
+ }
+
+ this->ss_cached_bounds = sb;
+
+ if (sb.sb_count == 0) {
+ this->ss_cached_line_count = 0;
+ this->ss_cursor_column = nonstd::nullopt;
+ this->reset_details_source();
+ return;
+ }
+
+ time_t grain_begin_time = rounddown(sb.sb_begin_time, this->ss_granularity);
+ time_t grain_end_time = roundup_size(sb.sb_end_time, this->ss_granularity);
+
+ time_t diff = std::max((time_t) 1, grain_end_time - grain_begin_time);
+ this->ss_cached_line_count
+ = (diff + this->ss_granularity - 1) / this->ss_granularity;
+
+ int64_t samples_per_row = sb.sb_count / this->ss_cached_line_count;
+ auto& st = this->ss_cached_thresholds;
+
+ st.st_yellow_threshold = samples_per_row / 2;
+ st.st_green_threshold = st.st_yellow_threshold / 2;
+
+ if (st.st_green_threshold <= 1) {
+ st.st_green_threshold = 2;
+ }
+ if (st.st_yellow_threshold <= st.st_green_threshold) {
+ st.st_yellow_threshold = st.st_green_threshold + 1;
+ }
+}
+
+const spectrogram_row&
+spectrogram_source::load_row(const listview_curses& tc, int row)
+{
+ this->cache_bounds();
+
+ unsigned long width;
+ vis_line_t height;
+
+ tc.get_dimensions(height, width);
+ width -= 2;
+
+ auto& sb = this->ss_cached_bounds;
+ spectrogram_request sr(sb);
+
+ sr.sr_width = width;
+ auto row_time = rounddown(sb.sb_begin_time, this->ss_granularity)
+ + row * this->ss_granularity;
+ sr.sr_begin_time = row_time;
+ sr.sr_end_time = row_time + this->ss_granularity;
+
+ sr.sr_column_size
+ = (sb.sb_max_value_out - sb.sb_min_value_out) / (double) (width - 1);
+
+ auto& s_row = this->ss_row_cache[row_time];
+
+ if (s_row.sr_values.empty() || s_row.sr_width != width
+ || s_row.sr_column_size != sr.sr_column_size)
+ {
+ s_row.sr_width = width;
+ s_row.sr_column_size = sr.sr_column_size;
+ s_row.sr_values.clear();
+ s_row.sr_values.resize(width + 1);
+ this->ss_value_source->spectro_row(sr, s_row);
+ }
+
+ return s_row;
+}
+
+bool
+spectrogram_source::text_is_row_selectable(textview_curses& tc, vis_line_t row)
+{
+ if (this->ss_value_source == nullptr) {
+ return false;
+ }
+
+ const auto& s_row = this->load_row(tc, row);
+ auto nearest_column
+ = s_row.nearest_column(this->ss_cursor_column.value_or(0));
+
+ return nearest_column.has_value();
+}
+
+void
+spectrogram_source::text_selection_changed(textview_curses& tc)
+{
+ if (this->ss_value_source == nullptr || this->text_line_count() == 0) {
+ this->ss_cursor_column = nonstd::nullopt;
+ return;
+ }
+
+ const auto& s_row = this->load_row(tc, tc.get_selection());
+ this->ss_cursor_column
+ = s_row.nearest_column(this->ss_cursor_column.value_or(0));
+}
+
+spectro_status_source::spectro_status_source()
+{
+ this->sss_fields[F_TITLE].set_width(9);
+ this->sss_fields[F_TITLE].set_role(role_t::VCR_STATUS_TITLE);
+ this->sss_fields[F_TITLE].set_value(" Details ");
+
+ this->sss_fields[F_HELP].right_justify(true);
+ this->sss_fields[F_HELP].set_width(20);
+ this->sss_fields[F_HELP].set_value("Press " ANSI_BOLD("TAB") " to focus ");
+ this->sss_fields[F_HELP].set_left_pad(1);
+}
+
+size_t
+spectro_status_source::statusview_fields()
+{
+ return F_MAX;
+}
+
+status_field&
+spectro_status_source::statusview_value_for_field(int field)
+{
+ return this->sss_fields[field];
+}
diff --git a/src/spectro_source.hh b/src/spectro_source.hh
new file mode 100644
index 0000000..c337ef1
--- /dev/null
+++ b/src/spectro_source.hh
@@ -0,0 +1,208 @@
+/**
+ * Copyright (c) 2016, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file spectroview_curses.hh
+ */
+
+#ifndef spectro_source_hh
+#define spectro_source_hh
+
+#include <unordered_map>
+#include <vector>
+
+#include <math.h>
+#include <time.h>
+
+#include "statusview_curses.hh"
+#include "textview_curses.hh"
+
+struct exec_context;
+
+struct spectrogram_bounds {
+ time_t sb_begin_time{0};
+ time_t sb_end_time{0};
+ double sb_min_value_out{0.0};
+ double sb_max_value_out{0.0};
+ int64_t sb_count{0};
+};
+
+struct spectrogram_thresholds {
+ int st_green_threshold{0};
+ int st_yellow_threshold{0};
+};
+
+struct spectrogram_request {
+ explicit spectrogram_request(spectrogram_bounds& sb) : sr_bounds(sb) {}
+
+ spectrogram_bounds& sr_bounds;
+ unsigned long sr_width{0};
+ time_t sr_begin_time{0};
+ time_t sr_end_time{0};
+ double sr_column_size{0};
+};
+
+struct spectrogram_row {
+ spectrogram_row() = default;
+ spectrogram_row(const spectrogram_row&) = delete;
+
+ struct row_bucket {
+ int rb_counter{0};
+ int rb_marks{0};
+ };
+
+ std::vector<row_bucket> sr_values;
+ unsigned long sr_width{0};
+ double sr_column_size{0.0};
+ std::function<std::unique_ptr<text_sub_source>(
+ const spectrogram_request&, double range_min, double range_max)>
+ sr_details_source_provider;
+
+ void add_value(spectrogram_request& sr, double value, bool marked)
+ {
+ long index = std::floor((value - sr.sr_bounds.sb_min_value_out)
+ / sr.sr_column_size);
+
+ this->sr_values[index].rb_counter += 1;
+ if (marked) {
+ this->sr_values[index].rb_marks += 1;
+ }
+ }
+
+ nonstd::optional<size_t> nearest_column(size_t current) const;
+};
+
+class spectrogram_value_source {
+public:
+ virtual ~spectrogram_value_source() = default;
+
+ virtual void spectro_bounds(spectrogram_bounds& sb_out) = 0;
+
+ virtual void spectro_row(spectrogram_request& sr, spectrogram_row& row_out)
+ = 0;
+
+ virtual void spectro_mark(textview_curses& tc,
+ time_t begin_time,
+ time_t end_time,
+ double range_min,
+ double range_max)
+ = 0;
+};
+
+class spectrogram_source
+ : public text_sub_source
+ , public text_time_translator
+ , public list_overlay_source
+ , public list_input_delegate {
+public:
+ ~spectrogram_source() override = default;
+
+ void invalidate()
+ {
+ this->ss_cached_bounds.sb_count = 0;
+ this->ss_row_cache.clear();
+ this->ss_cursor_column = nonstd::nullopt;
+ }
+
+ bool list_input_handle_key(listview_curses& lv, int ch) override;
+
+ bool list_value_for_overlay(const listview_curses& lv,
+ int y,
+ int bottom,
+ vis_line_t row,
+ attr_line_t& value_out) override;
+
+ size_t text_line_count() override;
+
+ size_t text_line_width(textview_curses& tc) override;
+
+ size_t text_size_for_line(textview_curses& tc,
+ int row,
+ line_flags_t flags) override
+ {
+ return 0;
+ }
+
+ bool text_is_row_selectable(textview_curses& tc, vis_line_t row) override;
+
+ void text_selection_changed(textview_curses& tc) override;
+
+ nonstd::optional<struct timeval> time_for_row(vis_line_t row) override;
+
+ nonstd::optional<vis_line_t> row_for_time(
+ struct timeval time_bucket) override;
+
+ void text_value_for_line(textview_curses& tc,
+ int row,
+ std::string& value_out,
+ line_flags_t flags) override;
+
+ void text_attrs_for_line(textview_curses& tc,
+ int row,
+ string_attrs_t& value_out) override;
+
+ void cache_bounds();
+
+ nonstd::optional<struct timeval> time_for_row_int(vis_line_t row);
+
+ const spectrogram_row& load_row(const listview_curses& lv, int row);
+
+ void reset_details_source();
+
+ textview_curses* ss_details_view;
+ text_sub_source* ss_no_details_source;
+ exec_context* ss_exec_context;
+ std::unique_ptr<text_sub_source> ss_details_source;
+ int ss_granularity{60};
+ spectrogram_value_source* ss_value_source{nullptr};
+ spectrogram_bounds ss_cached_bounds;
+ spectrogram_thresholds ss_cached_thresholds;
+ size_t ss_cached_line_count{0};
+ std::unordered_map<time_t, spectrogram_row> ss_row_cache;
+ nonstd::optional<size_t> ss_cursor_column;
+};
+
+class spectro_status_source : public status_data_source {
+public:
+ enum field_t {
+ F_TITLE,
+ F_HELP,
+
+ F_MAX
+ };
+
+ spectro_status_source();
+
+ size_t statusview_fields() override;
+
+ status_field& statusview_value_for_field(int field) override;
+
+private:
+ status_field sss_fields[F_MAX];
+};
+
+#endif
diff --git a/src/spookyhash/SpookyV2.cpp b/src/spookyhash/SpookyV2.cpp
new file mode 100644
index 0000000..f1aea49
--- /dev/null
+++ b/src/spookyhash/SpookyV2.cpp
@@ -0,0 +1,350 @@
+// Spooky Hash
+// A 128-bit noncryptographic hash, for checksums and table lookup
+// By Bob Jenkins. Public domain.
+// Oct 31 2010: published framework, disclaimer ShortHash isn't right
+// Nov 7 2010: disabled ShortHash
+// Oct 31 2011: replace End, ShortMix, ShortEnd, enable ShortHash again
+// April 10 2012: buffer overflow on platforms without unaligned reads
+// July 12 2012: was passing out variables in final to in/out in short
+// July 30 2012: I reintroduced the buffer overflow
+// August 5 2012: SpookyV2: d = should be d += in short hash, and remove extra mix from long hash
+
+#include <memory.h>
+#include "SpookyV2.h"
+
+#define ALLOW_UNALIGNED_READS 1
+
+//
+// short hash ... it could be used on any message,
+// but it's used by Spooky just for short messages.
+//
+void SpookyHash::Short(
+ const void *message,
+ size_t length,
+ uint64 *hash1,
+ uint64 *hash2)
+{
+ uint64 buf[2*sc_numVars];
+ union
+ {
+ const uint8 *p8;
+ uint32 *p32;
+ uint64 *p64;
+ size_t i;
+ } u;
+
+ u.p8 = (const uint8 *)message;
+
+ if (!ALLOW_UNALIGNED_READS && (u.i & 0x7))
+ {
+ memcpy(buf, message, length);
+ u.p64 = buf;
+ }
+
+ size_t remainder = length%32;
+ uint64 a=*hash1;
+ uint64 b=*hash2;
+ uint64 c=sc_const;
+ uint64 d=sc_const;
+
+ if (length > 15)
+ {
+ const uint64 *end = u.p64 + (length/32)*4;
+
+ // handle all complete sets of 32 bytes
+ for (; u.p64 < end; u.p64 += 4)
+ {
+ c += SPOOKYHASH_LITTLE_ENDIAN_64(u.p64[0]);
+ d += SPOOKYHASH_LITTLE_ENDIAN_64(u.p64[1]);
+ ShortMix(a,b,c,d);
+ a += SPOOKYHASH_LITTLE_ENDIAN_64(u.p64[2]);
+ b += SPOOKYHASH_LITTLE_ENDIAN_64(u.p64[3]);
+ }
+
+ //Handle the case of 16+ remaining bytes.
+ if (remainder >= 16)
+ {
+ c += SPOOKYHASH_LITTLE_ENDIAN_64(u.p64[0]);
+ d += SPOOKYHASH_LITTLE_ENDIAN_64(u.p64[1]);
+ ShortMix(a,b,c,d);
+ u.p64 += 2;
+ remainder -= 16;
+ }
+ }
+
+ // Handle the last 0..15 bytes, and its length
+ d += ((uint64)length) << 56;
+ switch (remainder)
+ {
+ case 15:
+ d += ((uint64)u.p8[14]) << 48;
+ case 14:
+ d += ((uint64)u.p8[13]) << 40;
+ case 13:
+ d += ((uint64)u.p8[12]) << 32;
+ case 12:
+ d += SPOOKYHASH_LITTLE_ENDIAN_32(u.p32[2]);
+ c += SPOOKYHASH_LITTLE_ENDIAN_64(u.p64[0]);
+ break;
+ case 11:
+ d += ((uint64)u.p8[10]) << 16;
+ case 10:
+ d += ((uint64)u.p8[9]) << 8;
+ case 9:
+ d += (uint64)u.p8[8];
+ case 8:
+ c += SPOOKYHASH_LITTLE_ENDIAN_64(u.p64[0]);
+ break;
+ case 7:
+ c += ((uint64)u.p8[6]) << 48;
+ case 6:
+ c += ((uint64)u.p8[5]) << 40;
+ case 5:
+ c += ((uint64)u.p8[4]) << 32;
+ case 4:
+ c += SPOOKYHASH_LITTLE_ENDIAN_32(u.p32[0]);
+ break;
+ case 3:
+ c += ((uint64)u.p8[2]) << 16;
+ case 2:
+ c += ((uint64)u.p8[1]) << 8;
+ case 1:
+ c += (uint64)u.p8[0];
+ break;
+ case 0:
+ c += sc_const;
+ d += sc_const;
+ }
+ ShortEnd(a,b,c,d);
+ *hash1 = a;
+ *hash2 = b;
+}
+
+
+
+
+// do the whole hash in one call
+void SpookyHash::Hash128(
+ const void *message,
+ size_t length,
+ uint64 *hash1,
+ uint64 *hash2)
+{
+ if (length < sc_bufSize)
+ {
+ Short(message, length, hash1, hash2);
+ return;
+ }
+
+ uint64 h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11;
+ uint64 buf[sc_numVars];
+ uint64 *end;
+ union
+ {
+ const uint8 *p8;
+ uint64 *p64;
+ size_t i;
+ } u;
+ size_t remainder;
+
+ h0=h3=h6=h9 = *hash1;
+ h1=h4=h7=h10 = *hash2;
+ h2=h5=h8=h11 = sc_const;
+
+ u.p8 = (const uint8 *)message;
+ end = u.p64 + (length/sc_blockSize)*sc_numVars;
+
+ // handle all whole sc_blockSize blocks of bytes
+ if (ALLOW_UNALIGNED_READS || ((u.i & 0x7) == 0))
+ {
+ while (u.p64 < end)
+ {
+ Mix(u.p64, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ u.p64 += sc_numVars;
+ }
+ }
+ else
+ {
+ while (u.p64 < end)
+ {
+ memcpy(buf, u.p64, sc_blockSize);
+ Mix(buf, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ u.p64 += sc_numVars;
+ }
+ }
+
+ // handle the last partial block of sc_blockSize bytes
+ remainder = (length - ((const uint8 *)end-(const uint8 *)message));
+ memcpy(buf, end, remainder);
+ memset(((uint8 *)buf)+remainder, 0, sc_blockSize-remainder);
+ ((uint8 *)buf)[sc_blockSize-1] = remainder;
+
+ // do some final mixing
+ End(buf, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ *hash1 = h0;
+ *hash2 = h1;
+}
+
+
+
+// init spooky state
+void SpookyHash::Init(uint64 seed1, uint64 seed2)
+{
+ m_length = 0;
+ m_remainder = 0;
+ m_state[0] = seed1;
+ m_state[1] = seed2;
+}
+
+
+// add a message fragment to the state
+void SpookyHash::Update(const void *message, size_t length)
+{
+ uint64 h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11;
+ size_t newLength = length + m_remainder;
+ uint8 remainder;
+ union
+ {
+ const uint8 *p8;
+ uint64 *p64;
+ size_t i;
+ } u;
+ const uint64 *end;
+
+ // Is this message fragment too short? If it is, stuff it away.
+ if (newLength < sc_bufSize)
+ {
+ memcpy(&((uint8 *)m_data)[m_remainder], message, length);
+ m_length = length + m_length;
+ m_remainder = (uint8)newLength;
+ return;
+ }
+
+ // init the variables
+ if (m_length < sc_bufSize)
+ {
+ h0=h3=h6=h9 = m_state[0];
+ h1=h4=h7=h10 = m_state[1];
+ h2=h5=h8=h11 = sc_const;
+ }
+ else
+ {
+ h0 = m_state[0];
+ h1 = m_state[1];
+ h2 = m_state[2];
+ h3 = m_state[3];
+ h4 = m_state[4];
+ h5 = m_state[5];
+ h6 = m_state[6];
+ h7 = m_state[7];
+ h8 = m_state[8];
+ h9 = m_state[9];
+ h10 = m_state[10];
+ h11 = m_state[11];
+ }
+ m_length = length + m_length;
+
+ // if we've got anything stuffed away, use it now
+ if (m_remainder)
+ {
+ uint8 prefix = sc_bufSize-m_remainder;
+ memcpy(&(((uint8 *)m_data)[m_remainder]), message, prefix);
+ u.p64 = m_data;
+ Mix(u.p64, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ Mix(&u.p64[sc_numVars], h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ u.p8 = ((const uint8 *)message) + prefix;
+ length -= prefix;
+ }
+ else
+ {
+ u.p8 = (const uint8 *)message;
+ }
+
+ // handle all whole blocks of sc_blockSize bytes
+ end = u.p64 + (length/sc_blockSize)*sc_numVars;
+ remainder = (uint8)(length-((const uint8 *)end-u.p8));
+ if (ALLOW_UNALIGNED_READS || (u.i & 0x7) == 0)
+ {
+ while (u.p64 < end)
+ {
+ Mix(u.p64, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ u.p64 += sc_numVars;
+ }
+ }
+ else
+ {
+ while (u.p64 < end)
+ {
+ memcpy(m_data, u.p8, sc_blockSize);
+ Mix(m_data, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ u.p64 += sc_numVars;
+ }
+ }
+
+ // stuff away the last few bytes
+ m_remainder = remainder;
+ memcpy(m_data, end, remainder);
+
+ // stuff away the variables
+ m_state[0] = h0;
+ m_state[1] = h1;
+ m_state[2] = h2;
+ m_state[3] = h3;
+ m_state[4] = h4;
+ m_state[5] = h5;
+ m_state[6] = h6;
+ m_state[7] = h7;
+ m_state[8] = h8;
+ m_state[9] = h9;
+ m_state[10] = h10;
+ m_state[11] = h11;
+}
+
+// report the hash for the concatenation of all message fragments so far
+void
+SpookyHash::Final(uint64* hash1, uint64* hash2) const
+{
+ // init the variables
+ if (m_length < sc_bufSize) {
+ *hash1 = m_state[0];
+ *hash2 = m_state[1];
+ Short(m_data, m_length, hash1, hash2);
+ return;
+ }
+
+ const uint64 *data = (const uint64 *)m_data;
+ uint8 remainder = m_remainder;
+
+ uint64 h0 = m_state[0];
+ uint64 h1 = m_state[1];
+ uint64 h2 = m_state[2];
+ uint64 h3 = m_state[3];
+ uint64 h4 = m_state[4];
+ uint64 h5 = m_state[5];
+ uint64 h6 = m_state[6];
+ uint64 h7 = m_state[7];
+ uint64 h8 = m_state[8];
+ uint64 h9 = m_state[9];
+ uint64 h10 = m_state[10];
+ uint64 h11 = m_state[11];
+
+ if (remainder >= sc_blockSize)
+ {
+ // m_data can contain two blocks; handle any whole first block
+ Mix(data, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ data += sc_numVars;
+ remainder -= sc_blockSize;
+ }
+
+ // mix in the last partial block, and the length mod sc_blockSize
+ memset(&((uint8 *)data)[remainder], 0, (sc_blockSize-remainder));
+
+ ((uint8 *)data)[sc_blockSize-1] = remainder;
+
+ // do some final mixing
+ End(data, h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+
+ *hash1 = h0;
+ *hash2 = h1;
+}
+
diff --git a/src/spookyhash/SpookyV2.h b/src/spookyhash/SpookyV2.h
new file mode 100644
index 0000000..e096e7c
--- /dev/null
+++ b/src/spookyhash/SpookyV2.h
@@ -0,0 +1,339 @@
+//
+// SpookyHash: a 128-bit noncryptographic hash function
+// By Bob Jenkins, public domain
+// Oct 31 2010: alpha, framework + SpookyHash::Mix appears right
+// Oct 31 2011: alpha again, Mix only good to 2^^69 but rest appears right
+// Dec 31 2011: beta, improved Mix, tested it for 2-bit deltas
+// Feb 2 2012: production, same bits as beta
+// Feb 5 2012: adjusted definitions of uint* to be more portable
+// Mar 30 2012: 3 bytes/cycle, not 4. Alpha was 4 but wasn't thorough enough.
+// August 5 2012: SpookyV2 (different results)
+//
+// Up to 3 bytes/cycle for long messages. Reasonably fast for short messages.
+// All 1 or 2 bit deltas achieve avalanche within 1% bias per output bit.
+//
+// This was developed for and tested on 64-bit x86-compatible processors.
+// It assumes the processor is little-endian. There is a macro
+// controlling whether unaligned reads are allowed (by default they are).
+// This should be an equally good hash on big-endian machines, but it will
+// compute different results on them than on little-endian machines.
+//
+// Google's CityHash has similar specs to SpookyHash, and CityHash is faster
+// on new Intel boxes. MD4 and MD5 also have similar specs, but they are orders
+// of magnitude slower. CRCs are two or more times slower, but unlike
+// SpookyHash, they have nice math for combining the CRCs of pieces to form
+// the CRCs of wholes. There are also cryptographic hashes, but those are even
+// slower than MD5.
+//
+
+#ifndef _SPOOKYHASH_V2_H
+#define _SPOOKYHASH_V2_H
+#include <stddef.h>
+
+#ifdef _MSC_VER
+# define INLINE __forceinline
+ typedef unsigned __int64 uint64;
+ typedef unsigned __int32 uint32;
+ typedef unsigned __int16 uint16;
+ typedef unsigned __int8 uint8;
+#else
+# include <stdint.h>
+# define INLINE inline
+ typedef uint64_t uint64;
+ typedef uint32_t uint32;
+ typedef uint16_t uint16;
+ typedef uint8_t uint8;
+#endif
+
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define SPOOKYHASH_LITTLE_ENDIAN_64(b) ((uint64_t)b)
+#define SPOOKYHASH_LITTLE_ENDIAN_32(b) ((uint32_t)b)
+#define SPOOKYHASH_LITTLE_ENDIAN_16(b) ((uint16_t)b)
+#elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
+#if __GNUC__ * 100 + __GNUC_MINOR__ >= 403 || defined(__clang__)
+#define SPOOKYHASH_LITTLE_ENDIAN_64(b) __builtin_bswap64(b)
+#define SPOOKYHASH_LITTLE_ENDIAN_32(b) __builtin_bswap32(b)
+#define SPOOKYHASH_LITTLE_ENDIAN_16(b) __builtin_bswap16(b)
+#else
+#warning Using bulk byte swap routines. Expect performance issues.
+#define SPOOKYHASH_LITTLE_ENDIAN_64(b) ((((b) & 0xFF00000000000000ull) >> 56) | (((b) & 0x00FF000000000000ull) >> 40) | (((b) & 0x0000FF0000000000ull) >> 24) | (((b) & 0x000000FF00000000ull) >> 8) | (((b) & 0x00000000FF000000ull) << 8) | (((b) & 0x0000000000FF0000ull) << 24ull) | (((b) & 0x000000000000FF00ull) << 40) | (((b) & 0x00000000000000FFull) << 56))
+#define SPOOKYHASH_LITTLE_ENDIAN_32(b) ((((b) & 0xFF000000) >> 24) | (((b) & 0x00FF0000) >> 8) | (((b) & 0x0000FF00) << 8) | (((b) & 0x000000FF) << 24))
+#define SPOOKYHASH_LITTLE_ENDIAN_16(b) ((((b) & 0xFF00) >> 8) | (((b) & 0x00FF) << 8))
+#endif
+#else
+#error Unknow endianness
+#endif
+
+
+class SpookyHash
+{
+public:
+ //
+ // SpookyHash: hash a single message in one call, produce 128-bit output
+ //
+ static void Hash128(
+ const void *message, // message to hash
+ size_t length, // length of message in bytes
+ uint64 *hash1, // in/out: in seed 1, out hash value 1
+ uint64 *hash2); // in/out: in seed 2, out hash value 2
+
+ //
+ // Hash64: hash a single message in one call, return 64-bit output
+ //
+ static uint64 Hash64(
+ const void *message, // message to hash
+ size_t length, // length of message in bytes
+ uint64 seed) // seed
+ {
+ uint64 hash1 = seed;
+ Hash128(message, length, &hash1, &seed);
+ return hash1;
+ }
+
+ //
+ // Hash32: hash a single message in one call, produce 32-bit output
+ //
+ static uint32 Hash32(
+ const void *message, // message to hash
+ size_t length, // length of message in bytes
+ uint32 seed) // seed
+ {
+ uint64 hash1 = seed, hash2 = seed;
+ Hash128(message, length, &hash1, &hash2);
+ return (uint32)hash1;
+ }
+
+ //
+ // Init: initialize the context of a SpookyHash
+ //
+ void Init(
+ uint64 seed1, // any 64-bit value will do, including 0
+ uint64 seed2); // different seeds produce independent hashes
+
+ //
+ // Update: add a piece of a message to a SpookyHash state
+ //
+ void Update(
+ const void *message, // message fragment
+ size_t length); // length of message fragment in bytes
+
+ //
+ // Final: compute the hash for the current SpookyHash state
+ //
+ // This does not modify the state; you can keep updating it afterward
+ //
+ // The result is the same as if SpookyHash() had been called with
+ // all the pieces concatenated into one message.
+ //
+ void Final(uint64* hash1, // out only: first 64 bits of hash value.
+ uint64* hash2) const; // out only: second 64 bits of hash value.
+
+ //
+ // left rotate a 64-bit value by k bytes
+ //
+ static INLINE uint64 Rot64(uint64 x, int k)
+ {
+ return (x << k) | (x >> (64 - k));
+ }
+
+ //
+ // This is used if the input is 96 bytes long or longer.
+ //
+ // The internal state is fully overwritten every 96 bytes.
+ // Every input bit appears to cause at least 128 bits of entropy
+ // before 96 other bytes are combined, when run forward or backward
+ // For every input bit,
+ // Two inputs differing in just that input bit
+ // Where "differ" means xor or subtraction
+ // And the base value is random
+ // When run forward or backwards one Mix
+ // I tried 3 pairs of each; they all differed by at least 212 bits.
+ //
+ static INLINE void Mix(
+ const uint64 *data,
+ uint64 &s0, uint64 &s1, uint64 &s2, uint64 &s3,
+ uint64 &s4, uint64 &s5, uint64 &s6, uint64 &s7,
+ uint64 &s8, uint64 &s9, uint64 &s10,uint64 &s11)
+ {
+ s0 += SPOOKYHASH_LITTLE_ENDIAN_64(data[0]);
+ s2 ^= s10; s11 ^= s0; s0 = Rot64(s0,11); s11 += s1;
+ s1 += SPOOKYHASH_LITTLE_ENDIAN_64(data[1]);
+ s3 ^= s11; s0 ^= s1; s1 = Rot64(s1,32); s0 += s2;
+ s2 += SPOOKYHASH_LITTLE_ENDIAN_64(data[2]);
+ s4 ^= s0; s1 ^= s2; s2 = Rot64(s2,43); s1 += s3;
+ s3 += SPOOKYHASH_LITTLE_ENDIAN_64(data[3]);
+ s5 ^= s1; s2 ^= s3; s3 = Rot64(s3,31); s2 += s4;
+ s4 += SPOOKYHASH_LITTLE_ENDIAN_64(data[4]);
+ s6 ^= s2; s3 ^= s4; s4 = Rot64(s4,17); s3 += s5;
+ s5 += SPOOKYHASH_LITTLE_ENDIAN_64(data[5]);
+ s7 ^= s3; s4 ^= s5; s5 = Rot64(s5,28); s4 += s6;
+ s6 += SPOOKYHASH_LITTLE_ENDIAN_64(data[6]);
+ s8 ^= s4; s5 ^= s6; s6 = Rot64(s6,39); s5 += s7;
+ s7 += SPOOKYHASH_LITTLE_ENDIAN_64(data[7]);
+ s9 ^= s5; s6 ^= s7; s7 = Rot64(s7,57); s6 += s8;
+ s8 += SPOOKYHASH_LITTLE_ENDIAN_64(data[8]);
+ s10 ^= s6; s7 ^= s8; s8 = Rot64(s8,55); s7 += s9;
+ s9 += SPOOKYHASH_LITTLE_ENDIAN_64(data[9]);
+ s11 ^= s7; s8 ^= s9; s9 = Rot64(s9,54); s8 += s10;
+ s10 += SPOOKYHASH_LITTLE_ENDIAN_64(data[10]);
+ s0 ^= s8; s9 ^= s10; s10 = Rot64(s10,22); s9 += s11;
+ s11 += SPOOKYHASH_LITTLE_ENDIAN_64(data[11]);
+ s1 ^= s9; s10 ^= s11; s11 = Rot64(s11,46); s10 += s0;
+ }
+
+ //
+ // Mix all 12 inputs together so that h0, h1 are a hash of them all.
+ //
+ // For two inputs differing in just the input bits
+ // Where "differ" means xor or subtraction
+ // And the base value is random, or a counting value starting at that bit
+ // The final result will have each bit of h0, h1 flip
+ // For every input bit,
+ // with probability 50 +- .3%
+ // For every pair of input bits,
+ // with probability 50 +- 3%
+ //
+ // This does not rely on the last Mix() call having already mixed some.
+ // Two iterations was almost good enough for a 64-bit result, but a
+ // 128-bit result is reported, so End() does three iterations.
+ //
+ static INLINE void EndPartial(
+ uint64 &h0, uint64 &h1, uint64 &h2, uint64 &h3,
+ uint64 &h4, uint64 &h5, uint64 &h6, uint64 &h7,
+ uint64 &h8, uint64 &h9, uint64 &h10,uint64 &h11)
+ {
+ h11+= h1; h2 ^= h11; h1 = Rot64(h1,44);
+ h0 += h2; h3 ^= h0; h2 = Rot64(h2,15);
+ h1 += h3; h4 ^= h1; h3 = Rot64(h3,34);
+ h2 += h4; h5 ^= h2; h4 = Rot64(h4,21);
+ h3 += h5; h6 ^= h3; h5 = Rot64(h5,38);
+ h4 += h6; h7 ^= h4; h6 = Rot64(h6,33);
+ h5 += h7; h8 ^= h5; h7 = Rot64(h7,10);
+ h6 += h8; h9 ^= h6; h8 = Rot64(h8,13);
+ h7 += h9; h10^= h7; h9 = Rot64(h9,38);
+ h8 += h10; h11^= h8; h10= Rot64(h10,53);
+ h9 += h11; h0 ^= h9; h11= Rot64(h11,42);
+ h10+= h0; h1 ^= h10; h0 = Rot64(h0,54);
+ }
+
+ static INLINE void End(
+ const uint64 *data,
+ uint64 &h0, uint64 &h1, uint64 &h2, uint64 &h3,
+ uint64 &h4, uint64 &h5, uint64 &h6, uint64 &h7,
+ uint64 &h8, uint64 &h9, uint64 &h10,uint64 &h11)
+ {
+ h0 += SPOOKYHASH_LITTLE_ENDIAN_64(data[0]);
+ h1 += SPOOKYHASH_LITTLE_ENDIAN_64(data[1]);
+ h2 += SPOOKYHASH_LITTLE_ENDIAN_64(data[2]);
+ h3 += SPOOKYHASH_LITTLE_ENDIAN_64(data[3]);
+ h4 += SPOOKYHASH_LITTLE_ENDIAN_64(data[4]);
+ h5 += SPOOKYHASH_LITTLE_ENDIAN_64(data[5]);
+ h6 += SPOOKYHASH_LITTLE_ENDIAN_64(data[6]);
+ h7 += SPOOKYHASH_LITTLE_ENDIAN_64(data[7]);
+ h8 += SPOOKYHASH_LITTLE_ENDIAN_64(data[8]);
+ h9 += SPOOKYHASH_LITTLE_ENDIAN_64(data[9]);
+ h10 += SPOOKYHASH_LITTLE_ENDIAN_64(data[10]);
+ h11 += SPOOKYHASH_LITTLE_ENDIAN_64(data[11]);
+ EndPartial(h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ EndPartial(h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ EndPartial(h0,h1,h2,h3,h4,h5,h6,h7,h8,h9,h10,h11);
+ }
+
+ //
+ // The goal is for each bit of the input to expand into 128 bits of
+ // apparent entropy before it is fully overwritten.
+ // n trials both set and cleared at least m bits of h0 h1 h2 h3
+ // n: 2 m: 29
+ // n: 3 m: 46
+ // n: 4 m: 57
+ // n: 5 m: 107
+ // n: 6 m: 146
+ // n: 7 m: 152
+ // when run forwards or backwards
+ // for all 1-bit and 2-bit diffs
+ // with diffs defined by either xor or subtraction
+ // with a base of all zeros plus a counter, or plus another bit, or random
+ //
+ static INLINE void ShortMix(uint64 &h0, uint64 &h1, uint64 &h2, uint64 &h3)
+ {
+ h2 = Rot64(h2,50); h2 += h3; h0 ^= h2;
+ h3 = Rot64(h3,52); h3 += h0; h1 ^= h3;
+ h0 = Rot64(h0,30); h0 += h1; h2 ^= h0;
+ h1 = Rot64(h1,41); h1 += h2; h3 ^= h1;
+ h2 = Rot64(h2,54); h2 += h3; h0 ^= h2;
+ h3 = Rot64(h3,48); h3 += h0; h1 ^= h3;
+ h0 = Rot64(h0,38); h0 += h1; h2 ^= h0;
+ h1 = Rot64(h1,37); h1 += h2; h3 ^= h1;
+ h2 = Rot64(h2,62); h2 += h3; h0 ^= h2;
+ h3 = Rot64(h3,34); h3 += h0; h1 ^= h3;
+ h0 = Rot64(h0,5); h0 += h1; h2 ^= h0;
+ h1 = Rot64(h1,36); h1 += h2; h3 ^= h1;
+ }
+
+ //
+ // Mix all 4 inputs together so that h0, h1 are a hash of them all.
+ //
+ // For two inputs differing in just the input bits
+ // Where "differ" means xor or subtraction
+ // And the base value is random, or a counting value starting at that bit
+ // The final result will have each bit of h0, h1 flip
+ // For every input bit,
+ // with probability 50 +- .3% (it is probably better than that)
+ // For every pair of input bits,
+ // with probability 50 +- .75% (the worst case is approximately that)
+ //
+ static INLINE void ShortEnd(uint64 &h0, uint64 &h1, uint64 &h2, uint64 &h3)
+ {
+ h3 ^= h2; h2 = Rot64(h2,15); h3 += h2;
+ h0 ^= h3; h3 = Rot64(h3,52); h0 += h3;
+ h1 ^= h0; h0 = Rot64(h0,26); h1 += h0;
+ h2 ^= h1; h1 = Rot64(h1,51); h2 += h1;
+ h3 ^= h2; h2 = Rot64(h2,28); h3 += h2;
+ h0 ^= h3; h3 = Rot64(h3,9); h0 += h3;
+ h1 ^= h0; h0 = Rot64(h0,47); h1 += h0;
+ h2 ^= h1; h1 = Rot64(h1,54); h2 += h1;
+ h3 ^= h2; h2 = Rot64(h2,32); h3 += h2;
+ h0 ^= h3; h3 = Rot64(h3,25); h0 += h3;
+ h1 ^= h0; h0 = Rot64(h0,63); h1 += h0;
+ }
+
+private:
+
+ //
+ // Short is used for messages under 192 bytes in length
+ // Short has a low startup cost, the normal mode is good for long
+ // keys, the cost crossover is at about 192 bytes. The two modes were
+ // held to the same quality bar.
+ //
+ static void Short(
+ const void *message, // message (array of bytes, not necessarily aligned)
+ size_t length, // length of message (in bytes)
+ uint64 *hash1, // in/out: in the seed, out the hash value
+ uint64 *hash2); // in/out: in the seed, out the hash value
+
+ // number of uint64's in internal state
+ static const size_t sc_numVars = 12;
+
+ // size of the internal state
+ static const size_t sc_blockSize = sc_numVars*8;
+
+ // size of buffer of unhashed data, in bytes
+ static const size_t sc_bufSize = 2*sc_blockSize;
+
+ //
+ // sc_const: a constant which:
+ // * is not zero
+ // * is odd
+ // * is a not-very-regular mix of 1's and 0's
+ // * does not need any other special mathematical properties
+ //
+ static const uint64 sc_const = 0xdeadbeefdeadbeefLL;
+
+ uint64 m_data[2*sc_numVars]; // unhashed data, for partial messages
+ uint64 m_state[sc_numVars]; // internal state of the hash
+ size_t m_length; // total length of the input so far
+ uint8 m_remainder; // length of unhashed data stashed in m_data
+};
+
+
+#endif
diff --git a/src/sql_commands.cc b/src/sql_commands.cc
new file mode 100644
index 0000000..e145331
--- /dev/null
+++ b/src/sql_commands.cc
@@ -0,0 +1,273 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "base/auto_mem.hh"
+#include "base/fs_util.hh"
+#include "base/injector.bind.hh"
+#include "base/lnav_log.hh"
+#include "bound_tags.hh"
+#include "command_executor.hh"
+#include "config.h"
+#include "readline_context.hh"
+#include "shlex.hh"
+#include "sqlite-extension-func.hh"
+#include "sqlitepp.hh"
+#include "view_helpers.hh"
+
+static Result<std::string, lnav::console::user_message>
+sql_cmd_dump(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ static auto& lnav_db = injector::get<auto_sqlite3&>();
+ static auto& lnav_flags = injector::get<unsigned long&, lnav_flags_tag>();
+
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("filename");
+ args.emplace_back("tables");
+ return Ok(retval);
+ }
+
+ if (args.size() < 2) {
+ return ec.make_error("expecting a file name to write to");
+ }
+
+ if (lnav_flags & LNF_SECURE_MODE) {
+ return ec.make_error("{} -- unavailable in secure mode", args[0]);
+ }
+
+ auto_mem<FILE> file(fclose);
+
+ if ((file = fopen(args[1].c_str(), "w+")) == nullptr) {
+ return ec.make_error(
+ "unable to open '{}' for writing: {}", args[1], strerror(errno));
+ }
+
+ for (size_t lpc = 2; lpc < args.size(); lpc++) {
+ sqlite3_db_dump(lnav_db.in(),
+ "main",
+ args[lpc].c_str(),
+ (int (*)(const char*, void*)) fputs,
+ file.in());
+ }
+
+ retval = "generated";
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+sql_cmd_read(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ static auto& lnav_db = injector::get<auto_sqlite3&>();
+ static auto& lnav_flags = injector::get<unsigned long&, lnav_flags_tag>();
+
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("filename");
+ return Ok(retval);
+ }
+
+ if (lnav_flags & LNF_SECURE_MODE) {
+ return ec.make_error("{} -- unavailable in secure mode", args[0]);
+ }
+
+ std::vector<std::string> split_args;
+ shlex lexer(cmdline);
+
+ if (!lexer.split(split_args, ec.create_resolver())) {
+ return ec.make_error("unable to parse arguments");
+ }
+
+ for (size_t lpc = 1; lpc < split_args.size(); lpc++) {
+ auto read_res = lnav::filesystem::read_file(split_args[lpc]);
+
+ if (read_res.isErr()) {
+ return ec.make_error("unable to read script file: {} -- {}",
+ split_args[lpc],
+ read_res.unwrapErr());
+ }
+
+ auto script = read_res.unwrap();
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+ const char* start = script.c_str();
+
+ do {
+ const char* tail;
+ auto rc = sqlite3_prepare_v2(
+ lnav_db.in(), start, -1, stmt.out(), &tail);
+
+ if (rc != SQLITE_OK) {
+ const char* errmsg = sqlite3_errmsg(lnav_db.in());
+
+ return ec.make_error("{}", errmsg);
+ }
+
+ if (stmt.in() != nullptr) {
+ std::string alt_msg;
+ auto exec_res = execute_sql(
+ ec, std::string(start, tail - start), alt_msg);
+ if (exec_res.isErr()) {
+ return exec_res;
+ }
+ }
+
+ start = tail;
+ } while (start[0]);
+ }
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+sql_cmd_schema(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ return Ok(retval);
+ }
+
+ ensure_view(LNV_SCHEMA);
+
+ return Ok(retval);
+}
+
+static Result<std::string, lnav::console::user_message>
+sql_cmd_generic(exec_context& ec,
+ std::string cmdline,
+ std::vector<std::string>& args)
+{
+ std::string retval;
+
+ if (args.empty()) {
+ args.emplace_back("*");
+ return Ok(retval);
+ }
+
+ return Ok(retval);
+}
+
+static readline_context::command_t sql_commands[] = {
+ {
+ ".dump",
+ sql_cmd_dump,
+ help_text(".dump", "Dump the contents of the database")
+ .sql_command()
+ .with_parameter({"path", "The path to the file to write"})
+ .with_tags({
+ "io",
+ }),
+ },
+ {
+ ".msgformats",
+ sql_cmd_schema,
+ help_text(".msgformats", "df").sql_command(),
+ },
+ {
+ ".read",
+ sql_cmd_read,
+ help_text(".read", "Execute the SQLite statements in the given file")
+ .sql_command()
+ .with_parameter({"path", "The path to the file to write"})
+ .with_tags({
+ "io",
+ }),
+ },
+ {
+ ".schema",
+ sql_cmd_schema,
+ help_text(".schema",
+ "Switch to the SCHEMA view that contains a dump of the "
+ "current database schema")
+ .sql_command(),
+ },
+ {
+ "ATTACH",
+ sql_cmd_generic,
+ },
+ {
+ "CREATE",
+ sql_cmd_generic,
+ },
+ {
+ "DELETE",
+ sql_cmd_generic,
+ },
+ {
+ "DETACH",
+ sql_cmd_generic,
+ },
+ {
+ "DROP",
+ sql_cmd_generic,
+ },
+ {
+ "INSERT",
+ sql_cmd_generic,
+ },
+ {
+ "SELECT",
+ sql_cmd_generic,
+ },
+ {
+ "UPDATE",
+ sql_cmd_generic,
+ },
+ {
+ "WITH",
+ sql_cmd_generic,
+ },
+};
+
+static readline_context::command_map_t sql_cmd_map;
+
+static auto bound_sql_cmd_map
+ = injector::bind<readline_context::command_map_t,
+ sql_cmd_map_tag>::to_instance(+[]() {
+ for (auto& cmd : sql_commands) {
+ sql_cmd_map[cmd.c_name] = &cmd;
+ }
+
+ return &sql_cmd_map;
+ });
+
+namespace injector {
+template<>
+void
+force_linking(sql_cmd_map_tag anno)
+{
+}
+} // namespace injector
diff --git a/src/sql_help.hh b/src/sql_help.hh
new file mode 100644
index 0000000..08bcc51
--- /dev/null
+++ b/src/sql_help.hh
@@ -0,0 +1,59 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file sql_help.hh
+ */
+
+#ifndef sql_help_hh
+#define sql_help_hh
+
+#include <map>
+
+#include "base/attr_line.hh"
+#include "help_text.hh"
+
+extern string_attr_type<void> SQL_COMMAND_ATTR;
+extern string_attr_type<void> SQL_KEYWORD_ATTR;
+extern string_attr_type<void> SQL_IDENTIFIER_ATTR;
+extern string_attr_type<void> SQL_FUNCTION_ATTR;
+extern string_attr_type<void> SQL_STRING_ATTR;
+extern string_attr_type<void> SQL_NUMBER_ATTR;
+extern string_attr_type<void> SQL_OPERATOR_ATTR;
+extern string_attr_type<void> SQL_PAREN_ATTR;
+extern string_attr_type<void> SQL_GARBAGE_ATTR;
+extern string_attr_type<void> SQL_COMMENT_ATTR;
+
+void annotate_sql_statement(attr_line_t& al_inout);
+
+extern std::multimap<std::string, help_text*> sqlite_function_help;
+
+std::string sql_keyword_re();
+std::vector<const help_text*> find_sql_help_for_line(const attr_line_t& al,
+ size_t x);
+
+#endif
diff --git a/src/sql_util.cc b/src/sql_util.cc
new file mode 100644
index 0000000..97a5344
--- /dev/null
+++ b/src/sql_util.cc
@@ -0,0 +1,1220 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file sql_util.cc
+ */
+
+#include <algorithm>
+#include <regex>
+#include <vector>
+
+#include "sql_util.hh"
+
+#include <ctype.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "base/auto_mem.hh"
+#include "base/injector.hh"
+#include "base/lnav_log.hh"
+#include "base/string_util.hh"
+#include "base/time_util.hh"
+#include "bound_tags.hh"
+#include "config.h"
+#include "lnav_util.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "readline_context.hh"
+#include "readline_highlighters.hh"
+#include "shlex.resolver.hh"
+#include "sql_help.hh"
+#include "sqlite-extension-func.hh"
+
+using namespace lnav::roles::literals;
+
+/**
+ * Copied from -- http://www.sqlite.org/lang_keywords.html
+ */
+const char* sql_keywords[] = {
+ "ABORT",
+ "ACTION",
+ "ADD",
+ "AFTER",
+ "ALL",
+ "ALTER",
+ "ALWAYS",
+ "ANALYZE",
+ "AND",
+ "AS",
+ "ASC",
+ "ATTACH",
+ "AUTOINCREMENT",
+ "BEFORE",
+ "BEGIN",
+ "BETWEEN",
+ "BY",
+ "CASCADE",
+ "CASE",
+ "CAST",
+ "CHECK",
+ "COLLATE",
+ "COLUMN",
+ "COMMIT",
+ "CONFLICT",
+ "CONSTRAINT",
+ "CREATE",
+ "CROSS",
+ "CURRENT",
+ "CURRENT_DATE",
+ "CURRENT_TIME",
+ "CURRENT_TIMESTAMP",
+ "DATABASE",
+ "DEFAULT",
+ "DEFERRABLE",
+ "DEFERRED",
+ "DELETE",
+ "DESC",
+ "DETACH",
+ "DISTINCT",
+ "DO",
+ "DROP",
+ "EACH",
+ "ELSE",
+ "END",
+ "ESCAPE",
+ "EXCEPT",
+ "EXCLUDE",
+ "EXCLUSIVE",
+ "EXISTS",
+ "EXPLAIN",
+ "FAIL",
+ "FILTER",
+ "FIRST",
+ "FOLLOWING",
+ "FOR",
+ "FOREIGN",
+ "FROM",
+ "FULL",
+ "GENERATED",
+ "GLOB",
+ "GROUP",
+ "GROUPS",
+ "HAVING",
+ "IF",
+ "IGNORE",
+ "IMMEDIATE",
+ "IN",
+ "INDEX",
+ "INDEXED",
+ "INITIALLY",
+ "INNER",
+ "INSERT",
+ "INSTEAD",
+ "INTERSECT",
+ "INTO",
+ "IS",
+ "ISNULL",
+ "JOIN",
+ "KEY",
+ "LAST",
+ "LEFT",
+ "LIKE",
+ "LIMIT",
+ "MATCH",
+ "NATURAL",
+ "NO",
+ "NOT",
+ "NOTHING",
+ "NOTNULL",
+ "NULL",
+ "NULLS",
+ "OF",
+ "OFFSET",
+ "ON",
+ "OR",
+ "ORDER",
+ "OTHERS",
+ "OUTER",
+ "OVER",
+ "PARTITION",
+ "PLAN",
+ "PRAGMA",
+ "PRECEDING",
+ "PRIMARY",
+ "QUERY",
+ "RAISE",
+ "RANGE",
+ "RECURSIVE",
+ "REFERENCES",
+ "REGEXP",
+ "REINDEX",
+ "RELEASE",
+ "RENAME",
+ "REPLACE",
+ "RESTRICT",
+ "RIGHT",
+ "ROLLBACK",
+ "ROW",
+ "ROWS",
+ "SAVEPOINT",
+ "SELECT",
+ "SET",
+ "TABLE",
+ "TEMP",
+ "TEMPORARY",
+ "THEN",
+ "TIES",
+ "TO",
+ "TRANSACTION",
+ "TRIGGER",
+ "UNBOUNDED",
+ "UNION",
+ "UNIQUE",
+ "UPDATE",
+ "USING",
+ "VACUUM",
+ "VALUES",
+ "VIEW",
+ "VIRTUAL",
+ "WHEN",
+ "WHERE",
+ "WINDOW",
+ "WITH",
+ "WITHOUT",
+};
+
+const char* sql_function_names[] = {
+ /* http://www.sqlite.org/lang_aggfunc.html */
+ "avg(",
+ "count(",
+ "group_concat(",
+ "max(",
+ "min(",
+ "sum(",
+ "total(",
+
+ /* http://www.sqlite.org/lang_corefunc.html */
+ "abs(",
+ "changes()",
+ "char(",
+ "coalesce(",
+ "glob(",
+ "ifnull(",
+ "instr(",
+ "hex(",
+ "last_insert_rowid()",
+ "length(",
+ "like(",
+ "load_extension(",
+ "lower(",
+ "ltrim(",
+ "nullif(",
+ "printf(",
+ "quote(",
+ "random()",
+ "randomblob(",
+ "replace(",
+ "round(",
+ "rtrim(",
+ "soundex(",
+ "sqlite_compileoption_get(",
+ "sqlite_compileoption_used(",
+ "sqlite_source_id()",
+ "sqlite_version()",
+ "substr(",
+ "total_changes()",
+ "trim(",
+ "typeof(",
+ "unicode(",
+ "upper(",
+ "zeroblob(",
+
+ /* http://www.sqlite.org/lang_datefunc.html */
+ "date(",
+ "time(",
+ "datetime(",
+ "julianday(",
+ "strftime(",
+
+ nullptr,
+};
+
+const std::unordered_map<unsigned char, const char*> sql_constraint_names = {
+ {SQLITE_INDEX_CONSTRAINT_EQ, "="},
+ {SQLITE_INDEX_CONSTRAINT_GT, ">"},
+ {SQLITE_INDEX_CONSTRAINT_LE, "<="},
+ {SQLITE_INDEX_CONSTRAINT_LT, "<"},
+ {SQLITE_INDEX_CONSTRAINT_GE, ">="},
+ {SQLITE_INDEX_CONSTRAINT_MATCH, "MATCH"},
+ {SQLITE_INDEX_CONSTRAINT_LIKE, "LIKE"},
+ {SQLITE_INDEX_CONSTRAINT_GLOB, "GLOB"},
+ {SQLITE_INDEX_CONSTRAINT_REGEXP, "REGEXP"},
+ {SQLITE_INDEX_CONSTRAINT_NE, "!="},
+ {SQLITE_INDEX_CONSTRAINT_ISNOT, "IS NOT"},
+ {SQLITE_INDEX_CONSTRAINT_ISNOTNULL, "IS NOT NULL"},
+ {SQLITE_INDEX_CONSTRAINT_ISNULL, "IS NULL"},
+ {SQLITE_INDEX_CONSTRAINT_IS, "IS"},
+#if defined(SQLITE_INDEX_CONSTRAINT_LIMIT)
+ {SQLITE_INDEX_CONSTRAINT_LIMIT, "LIMIT"},
+ {SQLITE_INDEX_CONSTRAINT_OFFSET, "OFFSET"},
+#endif
+#if defined(SQLITE_INDEX_CONSTRAINT_FUNCTION)
+ {SQLITE_INDEX_CONSTRAINT_FUNCTION, "function"},
+#endif
+};
+
+std::multimap<std::string, help_text*> sqlite_function_help;
+
+static int
+handle_db_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ struct sqlite_metadata_callbacks* smc;
+
+ smc = (struct sqlite_metadata_callbacks*) ptr;
+
+ smc->smc_db_list[colvalues[1]] = std::vector<std::string>();
+ if (!smc->smc_database_list) {
+ return 0;
+ }
+
+ return smc->smc_database_list(ptr, ncols, colvalues, colnames);
+}
+
+struct table_list_data {
+ struct sqlite_metadata_callbacks* tld_callbacks;
+ db_table_map_t::iterator* tld_iter;
+};
+
+static int
+handle_table_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ struct table_list_data* tld = (struct table_list_data*) ptr;
+
+ (*tld->tld_iter)->second.emplace_back(colvalues[0]);
+ if (!tld->tld_callbacks->smc_table_list) {
+ return 0;
+ }
+
+ return tld->tld_callbacks->smc_table_list(
+ tld->tld_callbacks, ncols, colvalues, colnames);
+}
+
+int
+walk_sqlite_metadata(sqlite3* db, struct sqlite_metadata_callbacks& smc)
+{
+ auto_mem<char, sqlite3_free> errmsg;
+ int retval;
+
+ if (smc.smc_collation_list) {
+ retval = sqlite3_exec(db,
+ "pragma collation_list",
+ smc.smc_collation_list,
+ &smc,
+ errmsg.out());
+ if (retval != SQLITE_OK) {
+ log_error("could not get collation list -- %s", errmsg.in());
+ return retval;
+ }
+ }
+
+ retval = sqlite3_exec(
+ db, "pragma database_list", handle_db_list, &smc, errmsg.out());
+ if (retval != SQLITE_OK) {
+ log_error("could not get DB list -- %s", errmsg.in());
+ return retval;
+ }
+
+ for (auto iter = smc.smc_db_list.begin(); iter != smc.smc_db_list.end();
+ ++iter)
+ {
+ struct table_list_data tld = {&smc, &iter};
+ auto_mem<char, sqlite3_free> query;
+
+ query = sqlite3_mprintf(
+ "SELECT name,sql FROM %Q.sqlite_master "
+ "WHERE type in ('table', 'view')",
+ iter->first.c_str());
+
+ retval = sqlite3_exec(db, query, handle_table_list, &tld, errmsg.out());
+ if (retval != SQLITE_OK) {
+ log_error("could not get table list -- %s", errmsg.in());
+ return retval;
+ }
+
+ for (auto table_iter = iter->second.begin();
+ table_iter != iter->second.end();
+ ++table_iter)
+ {
+ auto_mem<char, sqlite3_free> table_query;
+ std::string& table_name = *table_iter;
+
+ table_query = sqlite3_mprintf("pragma %Q.table_xinfo(%Q)",
+ iter->first.c_str(),
+ table_name.c_str());
+ if (table_query == nullptr) {
+ return SQLITE_NOMEM;
+ }
+
+ if (smc.smc_table_info) {
+ retval = sqlite3_exec(
+ db, table_query, smc.smc_table_info, &smc, errmsg.out());
+ if (retval != SQLITE_OK) {
+ log_error("could not get table info -- %s", errmsg.in());
+ return retval;
+ }
+ }
+
+ table_query = sqlite3_mprintf("pragma %Q.foreign_key_list(%Q)",
+ iter->first.c_str(),
+ table_name.c_str());
+ if (table_query == nullptr) {
+ return SQLITE_NOMEM;
+ }
+
+ if (smc.smc_foreign_key_list) {
+ retval = sqlite3_exec(db,
+ table_query,
+ smc.smc_foreign_key_list,
+ &smc,
+ errmsg.out());
+ if (retval != SQLITE_OK) {
+ log_error("could not get foreign key list -- %s",
+ errmsg.in());
+ return retval;
+ }
+ }
+ }
+ }
+
+ return retval;
+}
+
+static int
+schema_collation_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ return 0;
+}
+
+static int
+schema_db_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ struct sqlite_metadata_callbacks* smc = (sqlite_metadata_callbacks*) ptr;
+ std::string& schema_out = *((std::string*) smc->smc_userdata);
+ auto_mem<char, sqlite3_free> attach_sql;
+
+ attach_sql = sqlite3_mprintf(
+ "ATTACH DATABASE %Q AS %Q;\n", colvalues[2], colvalues[1]);
+
+ schema_out += attach_sql;
+
+ return 0;
+}
+
+static int
+schema_table_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ struct sqlite_metadata_callbacks* smc = (sqlite_metadata_callbacks*) ptr;
+ std::string& schema_out = *((std::string*) smc->smc_userdata);
+ auto_mem<char, sqlite3_free> create_sql;
+
+ create_sql = sqlite3_mprintf("%s;\n", colvalues[1]);
+
+ schema_out += create_sql;
+
+ return 0;
+}
+
+static int
+schema_table_info(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ return 0;
+}
+
+static int
+schema_foreign_key_list(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ return 0;
+}
+
+void
+dump_sqlite_schema(sqlite3* db, std::string& schema_out)
+{
+ struct sqlite_metadata_callbacks schema_sql_meta_callbacks
+ = {schema_collation_list,
+ schema_db_list,
+ schema_table_list,
+ schema_table_info,
+ schema_foreign_key_list,
+ &schema_out,
+ {}};
+
+ walk_sqlite_metadata(db, schema_sql_meta_callbacks);
+}
+
+void
+attach_sqlite_db(sqlite3* db, const std::string& filename)
+{
+ static const std::regex db_name_converter("[^\\w]");
+
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+
+ if (sqlite3_prepare_v2(db, "ATTACH DATABASE ? as ?", -1, stmt.out(), NULL)
+ != SQLITE_OK)
+ {
+ log_error("could not prepare DB attach statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ if (sqlite3_bind_text(
+ stmt.in(), 1, filename.c_str(), filename.length(), SQLITE_TRANSIENT)
+ != SQLITE_OK)
+ {
+ log_error("could not bind DB attach statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ size_t base_start = filename.find_last_of("/\\");
+ std::string db_name;
+
+ if (base_start == std::string::npos) {
+ db_name = filename;
+ } else {
+ db_name = filename.substr(base_start + 1);
+ }
+
+ db_name = std::regex_replace(db_name, db_name_converter, "_");
+
+ if (sqlite3_bind_text(
+ stmt.in(), 2, db_name.c_str(), db_name.length(), SQLITE_TRANSIENT)
+ != SQLITE_OK)
+ {
+ log_error("could not bind DB attach statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+
+ if (sqlite3_step(stmt.in()) != SQLITE_DONE) {
+ log_error("could not execute DB attach statement -- %s",
+ sqlite3_errmsg(db));
+ return;
+ }
+}
+
+static void
+sqlite_logger(void* dummy, int code, const char* msg)
+{
+ lnav_log_level_t level;
+
+ switch (code) {
+ case SQLITE_OK:
+ level = lnav_log_level_t::DEBUG;
+ break;
+#ifdef SQLITE_NOTICE
+ case SQLITE_NOTICE:
+ level = lnav_log_level_t::INFO;
+ break;
+#endif
+#ifdef SQLITE_WARNING
+ case SQLITE_WARNING:
+ level = lnav_log_level_t::WARNING;
+ break;
+#endif
+ default:
+ level = lnav_log_level_t::ERROR;
+ break;
+ }
+
+ log_msg(level, __FILE__, __LINE__, "(%d) %s", code, msg);
+
+ ensure(code != 21);
+}
+
+void
+sql_install_logger()
+{
+#ifdef SQLITE_CONFIG_LOG
+ sqlite3_config(SQLITE_CONFIG_LOG, sqlite_logger, NULL);
+#endif
+}
+
+bool
+sql_ident_needs_quote(const char* ident)
+{
+ for (int lpc = 0; ident[lpc]; lpc++) {
+ if (!isalnum(ident[lpc]) && ident[lpc] != '_') {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+char*
+sql_quote_ident(const char* ident)
+{
+ bool needs_quote = false;
+ size_t quote_count = 0, alloc_size;
+ char* retval;
+
+ for (int lpc = 0; ident[lpc]; lpc++) {
+ if ((lpc == 0 && isdigit(ident[lpc]))
+ || (!isalnum(ident[lpc]) && ident[lpc] != '_'))
+ {
+ needs_quote = true;
+ }
+ if (ident[lpc] == '"') {
+ quote_count += 1;
+ }
+ }
+
+ alloc_size = strlen(ident) + quote_count * 2 + (needs_quote ? 2 : 0) + 1;
+ if ((retval = (char*) sqlite3_malloc(alloc_size)) == NULL) {
+ retval = NULL;
+ } else {
+ char* curr = retval;
+
+ if (needs_quote) {
+ curr[0] = '"';
+ curr += 1;
+ }
+ for (size_t lpc = 0; ident[lpc] != '\0'; lpc++) {
+ switch (ident[lpc]) {
+ case '"':
+ curr[0] = '"';
+ curr += 1;
+ default:
+ curr[0] = ident[lpc];
+ break;
+ }
+ curr += 1;
+ }
+ if (needs_quote) {
+ curr[0] = '"';
+ curr += 1;
+ }
+
+ *curr = '\0';
+ }
+
+ return retval;
+}
+
+std::string
+sql_safe_ident(const string_fragment& ident)
+{
+ std::string retval = std::to_string(ident);
+
+ for (size_t lpc = 0; lpc < retval.size(); lpc++) {
+ char ch = retval[lpc];
+
+ if (isalnum(ch) || ch == '_') {
+ retval[lpc] = tolower(ch);
+ } else {
+ retval[lpc] = '_';
+ }
+ }
+
+ return retval;
+}
+
+attr_line_t
+annotate_sql_with_error(sqlite3* db, const char* sql, const char* tail)
+{
+ const auto* errmsg = sqlite3_errmsg(db);
+ attr_line_t retval;
+ int erroff = -1;
+
+#if defined(HAVE_SQLITE3_ERROR_OFFSET)
+ erroff = sqlite3_error_offset(db);
+#endif
+ if (tail != nullptr) {
+ const auto* tail_lf = strchr(tail, '\n');
+ if (tail_lf == nullptr) {
+ tail = tail + strlen(tail);
+ } else {
+ tail = tail_lf;
+ }
+ retval.append(string_fragment::from_bytes(sql, tail - sql));
+ } else {
+ retval.append(sql);
+ }
+ if (erroff >= retval.length()) {
+ erroff -= 1;
+ }
+ if (erroff != -1 && !endswith(retval.get_string(), "\n")) {
+ retval.append("\n");
+ }
+ retval.with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ readline_sqlite_highlighter(retval, retval.length());
+
+ if (erroff != -1) {
+ auto line_with_error
+ = string_fragment(retval.get_string())
+ .find_boundaries_around(erroff, string_fragment::tag1{'\n'});
+ auto erroff_in_line = erroff - line_with_error.sf_begin;
+
+ attr_line_t pointer;
+
+ pointer.append(erroff_in_line, ' ')
+ .append("^ "_snippet_border)
+ .append(lnav::roles::error(errmsg))
+ .append("\n");
+
+ retval.insert(line_with_error.sf_end + 1, pointer).rtrim();
+ }
+
+ return retval;
+}
+
+static void
+sql_execute_script(sqlite3* db,
+ const std::map<std::string, scoped_value_t>& global_vars,
+ const char* src_name,
+ sqlite3_stmt* stmt,
+ std::vector<lnav::console::user_message>& errors)
+{
+ std::map<std::string, scoped_value_t> lvars;
+ bool done = false;
+ int param_count;
+
+ sqlite3_clear_bindings(stmt);
+
+ param_count = sqlite3_bind_parameter_count(stmt);
+ for (int lpc = 0; lpc < param_count; lpc++) {
+ const char* name;
+
+ name = sqlite3_bind_parameter_name(stmt, lpc + 1);
+ if (name[0] == '$') {
+ const char* env_value;
+ auto iter = lvars.find(&name[1]);
+ if (iter != lvars.end()) {
+ mapbox::util::apply_visitor(
+ sqlitepp::bind_visitor(stmt, lpc + 1), iter->second);
+ } else {
+ auto giter = global_vars.find(&name[1]);
+ if (giter != global_vars.end()) {
+ mapbox::util::apply_visitor(
+ sqlitepp::bind_visitor(stmt, lpc + 1), giter->second);
+ } else if ((env_value = getenv(&name[1])) != nullptr) {
+ sqlite3_bind_text(
+ stmt, lpc + 1, env_value, -1, SQLITE_TRANSIENT);
+ } else {
+ sqlite3_bind_null(stmt, lpc + 1);
+ }
+ }
+ } else {
+ sqlite3_bind_null(stmt, lpc + 1);
+ }
+ }
+ while (!done) {
+ int retcode = sqlite3_step(stmt);
+ switch (retcode) {
+ case SQLITE_OK:
+ case SQLITE_DONE:
+ done = true;
+ break;
+
+ case SQLITE_ROW: {
+ int ncols = sqlite3_column_count(stmt);
+
+ for (int lpc = 0; lpc < ncols; lpc++) {
+ const char* name = sqlite3_column_name(stmt, lpc);
+ auto* raw_value = sqlite3_column_value(stmt, lpc);
+ auto value_type = sqlite3_value_type(raw_value);
+ scoped_value_t value;
+
+ switch (value_type) {
+ case SQLITE_INTEGER:
+ value = (int64_t) sqlite3_value_int64(raw_value);
+ break;
+ case SQLITE_FLOAT:
+ value = sqlite3_value_double(raw_value);
+ break;
+ case SQLITE_NULL:
+ value = null_value_t{};
+ break;
+ default:
+ value = string_fragment::from_bytes(
+ sqlite3_value_text(raw_value),
+ sqlite3_value_bytes(raw_value));
+ break;
+ }
+ lvars[name] = value;
+ }
+ break;
+ }
+
+ default: {
+ const auto* sql_str = sqlite3_sql(stmt);
+ auto sql_content
+ = annotate_sql_with_error(db, sql_str, nullptr);
+
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ "failed to execute SQL statement")
+ .with_reason(sqlite3_errmsg_to_attr_line(db))
+ .with_snippet(lnav::console::snippet::from(
+ intern_string::lookup(src_name), sql_content)));
+ done = true;
+ break;
+ }
+ }
+ }
+
+ sqlite3_reset(stmt);
+}
+
+static void
+sql_compile_script(sqlite3* db,
+ const std::map<std::string, scoped_value_t>& global_vars,
+ const char* src_name,
+ const char* script_orig,
+ std::vector<lnav::console::user_message>& errors)
+{
+ const char* script = script_orig;
+
+ while (script != nullptr && script[0]) {
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+ int line_number = 1;
+ const char* tail;
+ int retcode;
+
+ while (isspace(*script) && script[0]) {
+ script += 1;
+ }
+ for (const char* ch = script_orig; ch < script && ch[0]; ch++) {
+ if (*ch == '\n') {
+ line_number += 1;
+ }
+ }
+
+ retcode = sqlite3_prepare_v2(db, script, -1, stmt.out(), &tail);
+ log_debug("retcode %d %p %p", retcode, script, tail);
+ if (retcode != SQLITE_OK) {
+ const auto* errmsg = sqlite3_errmsg(db);
+ auto sql_content = annotate_sql_with_error(db, script, tail);
+
+ errors.emplace_back(
+ lnav::console::user_message::error(
+ "failed to compile SQL statement")
+ .with_reason(errmsg)
+ .with_snippet(
+ lnav::console::snippet::from(
+ intern_string::lookup(src_name), sql_content)
+ .with_line(line_number)));
+ break;
+ }
+ if (script == tail) {
+ break;
+ }
+ if (stmt == nullptr) {
+ } else {
+ sql_execute_script(db, global_vars, src_name, stmt.in(), errors);
+ }
+
+ script = tail;
+ }
+}
+
+void
+sql_execute_script(sqlite3* db,
+ const std::map<std::string, scoped_value_t>& global_vars,
+ const char* src_name,
+ const char* script,
+ std::vector<lnav::console::user_message>& errors)
+{
+ sql_compile_script(db, global_vars, src_name, script, errors);
+}
+
+static struct {
+ int sqlite_type;
+ const char* collator;
+ const char* sample;
+} TYPE_TEST_VALUE[] = {
+ {SQLITE3_TEXT, "", "foobar"},
+ {SQLITE_INTEGER, "", "123"},
+ {SQLITE_FLOAT, "", "123.0"},
+ {SQLITE_TEXT, "ipaddress", "127.0.0.1"},
+};
+
+int
+guess_type_from_pcre(const std::string& pattern, std::string& collator)
+{
+ static const std::vector<int> number_matches = {1, 2};
+
+ auto compile_res = lnav::pcre2pp::code::from(pattern);
+ if (compile_res.isErr()) {
+ return SQLITE3_TEXT;
+ }
+
+ auto re = compile_res.unwrap();
+ std::vector<int> matches;
+ int retval = SQLITE3_TEXT;
+ int index = 0;
+
+ collator.clear();
+ for (const auto& test_value : TYPE_TEST_VALUE) {
+ auto find_res
+ = re.find_in(string_fragment::from_c_str(test_value.sample),
+ PCRE2_ANCHORED)
+ .ignore_error();
+ if (find_res && find_res->f_all.sf_begin == 0
+ && find_res->f_remaining.empty())
+ {
+ matches.push_back(index);
+ }
+
+ index += 1;
+ }
+
+ if (matches.size() == 1) {
+ retval = TYPE_TEST_VALUE[matches.front()].sqlite_type;
+ collator = TYPE_TEST_VALUE[matches.front()].collator;
+ } else if (matches == number_matches) {
+ retval = SQLITE_FLOAT;
+ collator = "";
+ }
+
+ return retval;
+}
+
+const char*
+sqlite3_type_to_string(int type)
+{
+ switch (type) {
+ case SQLITE_FLOAT:
+ return "FLOAT";
+ case SQLITE_INTEGER:
+ return "INTEGER";
+ case SQLITE_TEXT:
+ return "TEXT";
+ case SQLITE_NULL:
+ return "NULL";
+ case SQLITE_BLOB:
+ return "BLOB";
+ }
+
+ ensure("Invalid sqlite type");
+
+ return nullptr;
+}
+
+/* XXX figure out how to do this with the template */
+void
+sqlite_close_wrapper(void* mem)
+{
+ sqlite3_close_v2((sqlite3*) mem);
+}
+
+int
+sqlite_authorizer(void* pUserData,
+ int action_code,
+ const char* detail1,
+ const char* detail2,
+ const char* detail3,
+ const char* detail4)
+{
+ if (action_code == SQLITE_ATTACH) {
+ return SQLITE_DENY;
+ }
+ return SQLITE_OK;
+}
+
+attr_line_t
+sqlite3_errmsg_to_attr_line(sqlite3* db)
+{
+ const auto* errmsg = sqlite3_errmsg(db);
+ if (startswith(errmsg, sqlitepp::ERROR_PREFIX)) {
+ auto from_res = lnav::from_json<lnav::console::user_message>(
+ &errmsg[strlen(sqlitepp::ERROR_PREFIX)]);
+
+ if (from_res.isOk()) {
+ return from_res.unwrap().to_attr_line();
+ }
+
+ return from_res.unwrapErr()[0].um_message.get_string();
+ }
+
+ return attr_line_t(errmsg);
+}
+
+std::string
+sql_keyword_re()
+{
+ std::string retval = "(?:";
+ bool first = true;
+
+ for (const char* kw : sql_keywords) {
+ if (!first) {
+ retval.append("|");
+ } else {
+ first = false;
+ }
+ retval.append("\\b");
+ retval.append(kw);
+ retval.append("\\b");
+ }
+ retval += ")";
+
+ return retval;
+}
+
+string_attr_type<void> SQL_COMMAND_ATTR("sql_command");
+string_attr_type<void> SQL_KEYWORD_ATTR("sql_keyword");
+string_attr_type<void> SQL_IDENTIFIER_ATTR("sql_ident");
+string_attr_type<void> SQL_FUNCTION_ATTR("sql_func");
+string_attr_type<void> SQL_STRING_ATTR("sql_string");
+string_attr_type<void> SQL_NUMBER_ATTR("sql_number");
+string_attr_type<void> SQL_UNTERMINATED_STRING_ATTR("sql_unstring");
+string_attr_type<void> SQL_OPERATOR_ATTR("sql_oper");
+string_attr_type<void> SQL_PAREN_ATTR("sql_paren");
+string_attr_type<void> SQL_COMMA_ATTR("sql_comma");
+string_attr_type<void> SQL_GARBAGE_ATTR("sql_garbage");
+string_attr_type<void> SQL_COMMENT_ATTR("sql_comment");
+
+void
+annotate_sql_statement(attr_line_t& al)
+{
+ static const std::string keyword_re_str = R"(\A)" + sql_keyword_re();
+
+ static const struct {
+ lnav::pcre2pp::code re;
+ string_attr_type<void>* type;
+ } PATTERNS[] = {
+ {
+ lnav::pcre2pp::code::from_const(R"(\A,)"),
+ &SQL_COMMA_ATTR,
+ },
+ {
+ lnav::pcre2pp::code::from_const(R"(\A\(|\A\))"),
+ &SQL_PAREN_ATTR,
+ },
+ {
+ lnav::pcre2pp::code::from(keyword_re_str, PCRE2_CASELESS).unwrap(),
+ &SQL_KEYWORD_ATTR,
+ },
+ {
+ lnav::pcre2pp::code::from_const(R"(\A'[^']*('(?:'[^']*')*|$))"),
+ &SQL_STRING_ATTR,
+ },
+ {
+ lnav::pcre2pp::code::from_const(
+ R"(\A-?\d+(?:\.\d*(?:[eE][\-\+]?\d+)?)?|0x[0-9a-fA-F]+$)"),
+ &SQL_NUMBER_ATTR,
+ },
+ {
+ lnav::pcre2pp::code::from_const(
+ R"(\A(((\$|:|@)?\b[a-z_]\w*)|\"([^\"]+)\"|\[([^\]]+)]))",
+ PCRE2_CASELESS),
+ &SQL_IDENTIFIER_ATTR,
+ },
+ {
+ lnav::pcre2pp::code::from_const(R"(\A--.*)"),
+ &SQL_COMMENT_ATTR,
+ },
+ {
+ lnav::pcre2pp::code::from_const(R"(\A(\*|<|>|=|!|\-|\+|\|\|))"),
+ &SQL_OPERATOR_ATTR,
+ },
+ {
+ lnav::pcre2pp::code::from_const(R"(\A.)"),
+ &SQL_GARBAGE_ATTR,
+ },
+ };
+
+ static const auto cmd_pattern
+ = lnav::pcre2pp::code::from_const(R"(^(\.\w+))");
+ static const auto ws_pattern = lnav::pcre2pp::code::from_const(R"(\A\s+)");
+
+ auto& line = al.get_string();
+ auto& sa = al.get_attrs();
+
+ auto cmd_find_res
+ = cmd_pattern.find_in(line, PCRE2_ANCHORED).ignore_error();
+ if (cmd_find_res) {
+ auto cap = cmd_find_res->f_all;
+ sa.emplace_back(line_range(cap.sf_begin, cap.sf_end),
+ SQL_COMMAND_ATTR.value());
+ return;
+ }
+
+ auto remaining = string_fragment::from_str(line);
+ while (!remaining.empty()) {
+ auto ws_find_res = ws_pattern.find_in(remaining).ignore_error();
+ if (ws_find_res) {
+ remaining = ws_find_res->f_remaining;
+ continue;
+ }
+ for (const auto& pat : PATTERNS) {
+ auto pat_find_res = pat.re.find_in(remaining).ignore_error();
+ if (pat_find_res) {
+ sa.emplace_back(to_line_range(pat_find_res->f_all),
+ pat.type->value());
+ remaining = pat_find_res->f_remaining;
+ break;
+ }
+ }
+ }
+
+ string_attrs_t::const_iterator iter;
+ int start = 0;
+
+ while ((iter = find_string_attr(sa, &SQL_IDENTIFIER_ATTR, start))
+ != sa.end())
+ {
+ string_attrs_t::const_iterator piter;
+ bool found_open = false;
+ ssize_t lpc;
+
+ start = iter->sa_range.lr_end;
+ for (lpc = iter->sa_range.lr_end; lpc < (int) line.length(); lpc++) {
+ if (line[lpc] == '(') {
+ found_open = true;
+ break;
+ }
+ if (!isspace(line[lpc])) {
+ break;
+ }
+ }
+
+ if (found_open) {
+ ssize_t pstart = lpc + 1;
+ int depth = 1;
+
+ while (depth > 0
+ && (piter = find_string_attr(sa, &SQL_PAREN_ATTR, pstart))
+ != sa.end())
+ {
+ if (line[piter->sa_range.lr_start] == '(') {
+ depth += 1;
+ } else {
+ depth -= 1;
+ }
+ pstart = piter->sa_range.lr_end;
+ }
+
+ line_range func_range{iter->sa_range.lr_start};
+ if (piter == sa.end()) {
+ func_range.lr_end = line.length();
+ } else {
+ func_range.lr_end = piter->sa_range.lr_end - 1;
+ }
+ sa.emplace_back(func_range, SQL_FUNCTION_ATTR.value());
+ }
+ }
+
+ remove_string_attr(sa, &SQL_PAREN_ATTR);
+ stable_sort(sa.begin(), sa.end());
+}
+
+std::vector<const help_text*>
+find_sql_help_for_line(const attr_line_t& al, size_t x)
+{
+ std::vector<const help_text*> retval;
+ const auto& sa = al.get_attrs();
+ std::string name;
+
+ x = al.nearest_text(x);
+
+ {
+ auto sa_opt = get_string_attr(al.get_attrs(), &SQL_COMMAND_ATTR);
+
+ if (sa_opt) {
+ auto* sql_cmd_map = injector::get<readline_context::command_map_t*,
+ sql_cmd_map_tag>();
+ auto cmd_name = al.get_substring((*sa_opt)->sa_range);
+ auto cmd_iter = sql_cmd_map->find(cmd_name);
+
+ if (cmd_iter != sql_cmd_map->end()) {
+ return {&cmd_iter->second->c_help};
+ }
+ }
+ }
+
+ std::vector<std::string> kw;
+ auto iter = rfind_string_attr_if(sa, x, [&al, &name, &kw, x](auto sa) {
+ if (sa.sa_type != &SQL_FUNCTION_ATTR && sa.sa_type != &SQL_KEYWORD_ATTR)
+ {
+ return false;
+ }
+
+ const std::string& str = al.get_string();
+ const line_range& lr = sa.sa_range;
+ int lpc;
+
+ if (sa.sa_type == &SQL_FUNCTION_ATTR) {
+ if (!sa.sa_range.contains(x)) {
+ return false;
+ }
+ }
+
+ for (lpc = lr.lr_start; lpc < lr.lr_end; lpc++) {
+ if (!isalnum(str[lpc]) && str[lpc] != '_') {
+ break;
+ }
+ }
+
+ auto tmp_name = str.substr(lr.lr_start, lpc - lr.lr_start);
+ if (sa.sa_type == &SQL_KEYWORD_ATTR) {
+ tmp_name = toupper(tmp_name);
+ }
+ bool retval = sqlite_function_help.count(tmp_name) > 0;
+
+ if (retval) {
+ kw.push_back(tmp_name);
+ name = tmp_name;
+ }
+ return retval;
+ });
+
+ if (iter != sa.end()) {
+ auto func_pair = sqlite_function_help.equal_range(name);
+ size_t help_count = std::distance(func_pair.first, func_pair.second);
+
+ if (help_count > 1 && name != func_pair.first->second->ht_name) {
+ while (func_pair.first != func_pair.second) {
+ if (find(kw.begin(), kw.end(), func_pair.first->second->ht_name)
+ == kw.end())
+ {
+ ++func_pair.first;
+ } else {
+ func_pair.second = next(func_pair.first);
+ break;
+ }
+ }
+ }
+ for (auto func_iter = func_pair.first; func_iter != func_pair.second;
+ ++func_iter)
+ {
+ retval.emplace_back(func_iter->second);
+ }
+ }
+
+ return retval;
+}
diff --git a/src/sql_util.hh b/src/sql_util.hh
new file mode 100644
index 0000000..361942c
--- /dev/null
+++ b/src/sql_util.hh
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file sql_util.hh
+ */
+
+#ifndef lnav_sql_util_hh
+#define lnav_sql_util_hh
+
+#include <map>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include <sqlite3.h>
+#include <sys/time.h>
+#include <time.h>
+
+#include "base/intern_string.hh"
+#include "base/lnav.console.hh"
+#include "base/time_util.hh"
+#include "sqlitepp.hh"
+
+extern const char* sql_keywords[145];
+extern const char* sql_function_names[];
+extern const std::unordered_map<unsigned char, const char*>
+ sql_constraint_names;
+
+inline const char*
+sql_constraint_op_name(unsigned char op)
+{
+ auto iter = sql_constraint_names.find(op);
+ if (iter == sql_constraint_names.end()) {
+ return "??";
+ }
+
+ return iter->second;
+}
+
+using sqlite_exec_callback = int (*)(void*, int, char**, char**);
+typedef std::vector<std::string> db_table_list_t;
+using db_table_map_t = std::map<std::string, db_table_list_t>;
+
+struct sqlite_metadata_callbacks {
+ sqlite_exec_callback smc_collation_list;
+ sqlite_exec_callback smc_database_list;
+ sqlite_exec_callback smc_table_list;
+ sqlite_exec_callback smc_table_info;
+ sqlite_exec_callback smc_foreign_key_list;
+ void* smc_userdata{nullptr};
+ db_table_map_t smc_db_list{};
+};
+
+int walk_sqlite_metadata(sqlite3* db, struct sqlite_metadata_callbacks& smc);
+
+void dump_sqlite_schema(sqlite3* db, std::string& schema_out);
+
+void attach_sqlite_db(sqlite3* db, const std::string& filename);
+
+inline ssize_t
+sql_strftime(char* buffer,
+ size_t buffer_size,
+ lnav::time64_t tim,
+ int millis,
+ char sep = ' ')
+{
+ return lnav::strftime_rfc3339(buffer, buffer_size, tim, millis, sep);
+}
+
+inline ssize_t
+sql_strftime(char* buffer,
+ size_t buffer_size,
+ const struct timeval& tv,
+ char sep = ' ')
+{
+ return sql_strftime(buffer, buffer_size, tv.tv_sec, tv.tv_usec / 1000, sep);
+}
+
+void sql_install_logger();
+
+bool sql_ident_needs_quote(const char* ident);
+
+char* sql_quote_ident(const char* ident);
+
+std::string sql_safe_ident(const string_fragment& ident);
+
+void sql_execute_script(
+ sqlite3* db,
+ const std::map<std::string, scoped_value_t>& global_vars,
+ const char* src_name,
+ const char* script,
+ std::vector<lnav::console::user_message>& errors);
+
+int guess_type_from_pcre(const std::string& pattern, std::string& collator);
+
+const char* sqlite3_type_to_string(int type);
+
+attr_line_t sqlite3_errmsg_to_attr_line(sqlite3* db);
+
+attr_line_t annotate_sql_with_error(sqlite3* db,
+ const char* sql,
+ const char* tail);
+
+int sqlite_authorizer(void* pUserData,
+ int action_code,
+ const char* detail1,
+ const char* detail2,
+ const char* detail3,
+ const char* detail4);
+
+#endif
diff --git a/src/sqlite-extension-func.cc b/src/sqlite-extension-func.cc
new file mode 100644
index 0000000..3a02f14
--- /dev/null
+++ b/src/sqlite-extension-func.cc
@@ -0,0 +1,1167 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file sqlite-extension-func.c
+ */
+
+#include "sqlite-extension-func.hh"
+
+#include "base/auto_mem.hh"
+#include "base/lnav_log.hh"
+#include "base/string_util.hh"
+#include "config.h"
+#include "sql_help.hh"
+
+extern "C"
+{
+struct sqlite3_api_routines;
+
+int sqlite3_series_init(sqlite3* db,
+ char** pzErrMsg,
+ const sqlite3_api_routines* pApi);
+}
+
+sqlite_registration_func_t sqlite_registration_funcs[] = {
+ common_extension_functions,
+ state_extension_functions,
+ string_extension_functions,
+ network_extension_functions,
+ fs_extension_functions,
+ json_extension_functions,
+ yaml_extension_functions,
+ time_extension_functions,
+
+ nullptr,
+};
+
+int
+register_sqlite_funcs(sqlite3* db, sqlite_registration_func_t* reg_funcs)
+{
+ int lpc;
+
+ require(db != nullptr);
+ require(reg_funcs != nullptr);
+
+ {
+ auto_mem<char> errmsg(sqlite3_free);
+
+ sqlite3_series_init(db, errmsg.out(), nullptr);
+ }
+
+ for (lpc = 0; reg_funcs[lpc]; lpc++) {
+ struct FuncDef* basic_funcs = nullptr;
+ struct FuncDefAgg* agg_funcs = nullptr;
+ int i;
+
+ reg_funcs[lpc](&basic_funcs, &agg_funcs);
+
+ for (i = 0; basic_funcs && basic_funcs[i].zName; i++) {
+ struct FuncDef& fd = basic_funcs[i];
+
+ // sqlite3CreateFunc
+ /* LMH no error checking */
+ sqlite3_create_function(db,
+ basic_funcs[i].zName,
+ basic_funcs[i].nArg,
+ basic_funcs[i].eTextRep,
+ (void*) &fd,
+ basic_funcs[i].xFunc,
+ nullptr,
+ nullptr);
+
+ if (fd.fd_help.ht_context != help_context_t::HC_NONE) {
+ help_text& ht = fd.fd_help;
+
+ sqlite_function_help.insert(std::make_pair(ht.ht_name, &ht));
+ ht.index_tags();
+ }
+ }
+
+ for (i = 0; agg_funcs && agg_funcs[i].zName; i++) {
+ struct FuncDefAgg& fda = agg_funcs[i];
+
+ // sqlite3CreateFunc
+ sqlite3_create_function(db,
+ agg_funcs[i].zName,
+ agg_funcs[i].nArg,
+ SQLITE_UTF8,
+ (void*) &agg_funcs[i],
+ nullptr,
+ agg_funcs[i].xStep,
+ agg_funcs[i].xFinalize);
+
+ if (fda.fda_help.ht_context != help_context_t::HC_NONE) {
+ help_text& ht = fda.fda_help;
+
+ sqlite_function_help.insert(std::make_pair(ht.ht_name, &ht));
+ ht.index_tags();
+ }
+ }
+ }
+
+ static help_text builtin_funcs[] = {
+ help_text("abs", "Return the absolute value of the argument")
+ .sql_function()
+ .with_parameter({"x", "The number to convert"})
+ .with_tags({"math"})
+ .with_example(
+ {"To get the absolute value of -1", "SELECT abs(-1)"}),
+
+ help_text("changes",
+ "The number of database rows that were changed, inserted, or "
+ "deleted by the most recent statement.")
+ .sql_function(),
+
+ help_text("char",
+ "Returns a string composed of characters having the given "
+ "unicode code point values")
+ .sql_function()
+ .with_parameter(
+ help_text("X", "The unicode code point values").zero_or_more())
+ .with_tags({"string"})
+ .with_example({"To get a string with the code points 0x48 and 0x49",
+ "SELECT char(0x48, 0x49)"}),
+
+ help_text("coalesce",
+ "Returns a copy of its first non-NULL argument, or NULL if "
+ "all arguments are NULL")
+ .sql_function()
+ .with_parameter({"X", "A value to check for NULL-ness"})
+ .with_parameter(
+ help_text("Y", "A value to check for NULL-ness").one_or_more())
+ .with_example(
+ {"To get the first non-null value from three parameters",
+ "SELECT coalesce(null, 0, null)"}),
+
+ help_text("glob", "Match a string against Unix glob pattern")
+ .sql_function()
+ .with_parameter({"pattern", "The glob pattern"})
+ .with_parameter({"str", "The string to match"})
+ .with_example({"To test if the string 'abc' matches the glob 'a*'",
+ "SELECT glob('a*', 'abc')"}),
+
+ help_text("hex",
+ "Returns a string which is the upper-case hexadecimal "
+ "rendering of the content of its argument.")
+ .sql_function()
+ .with_parameter({"X", "The blob to convert to hexadecimal"})
+ .with_example(
+ {"To get the hexadecimal rendering of the string 'abc'",
+ "SELECT hex('abc')"}),
+
+ help_text("ifnull",
+ "Returns a copy of its first non-NULL argument, or NULL if "
+ "both arguments are NULL")
+ .sql_function()
+ .with_parameter({"X", "A value to check for NULL-ness"})
+ .with_parameter({"Y", "A value to check for NULL-ness"})
+ .with_example(
+ {"To get the first non-null value between null and zero",
+ "SELECT ifnull(null, 0)"}),
+
+ help_text("instr",
+ "Finds the first occurrence of the needle within the "
+ "haystack and returns the number of prior characters plus 1, "
+ "or 0 if the needle was not found")
+ .sql_function()
+ .with_parameter({"haystack", "The string to search within"})
+ .with_parameter(
+ {"needle", "The string to look for in the haystack"})
+ .with_tags({"string"})
+ .with_example(
+ {"To test get the position of 'b' in the string 'abc'",
+ "SELECT instr('abc', 'b')"}),
+
+ help_text("last_insert_rowid",
+ "Returns the ROWID of the last row insert from the database "
+ "connection which invoked the function")
+ .sql_function(),
+
+ help_text("length",
+ "Returns the number of characters (not bytes) in the given "
+ "string prior to the first NUL character")
+ .sql_function()
+ .with_parameter({"str", "The string to determine the length of"})
+ .with_tags({"string"})
+ .with_example({"To get the length of the string 'abc'",
+ "SELECT length('abc')"}),
+
+ help_text("like", "Match a string against a pattern")
+ .sql_function()
+ .with_parameter(
+ {"pattern",
+ "The pattern to match. "
+ "A percent symbol (%) will match zero or more characters "
+ "and an underscore (_) will match a single character."})
+ .with_parameter({"str", "The string to match"})
+ .with_parameter(
+ help_text("escape",
+ "The escape character that can be used to prefix a "
+ "literal percent or underscore in the pattern.")
+ .optional())
+ .with_example(
+ {"To test if the string 'aabcc' contains the letter 'b'",
+ "SELECT like('%b%', 'aabcc')"})
+ .with_example({"To test if the string 'aab%' ends with 'b%'",
+ "SELECT like('%b:%', 'aab%', ':')"}),
+
+ help_text(
+ "likelihood",
+ "Provides a hint to the query planner that the first argument is a "
+ "boolean that is true with the given probability")
+ .sql_function()
+ .with_parameter({"value", "The boolean value to return"})
+ .with_parameter({"probability",
+ "A floating point constant between 0.0 and 1.0"}),
+
+ help_text("likely", "Short-hand for likelihood(X,0.9375)")
+ .sql_function()
+ .with_parameter({"value", "The boolean value to return"}),
+
+ help_text("load_extension",
+ "Loads SQLite extensions out of the given shared library "
+ "file using the given entry point.")
+ .sql_function()
+ .with_parameter(
+ {"path",
+ "The path to the shared library containing the extension."})
+ .with_parameter(help_text("entry-point", "").optional()),
+
+ help_text("lower",
+ "Returns a copy of the given string with all ASCII "
+ "characters converted to lower case.")
+ .sql_function()
+ .with_parameter({"str", "The string to convert."})
+ .with_tags({"string"})
+ .with_example(
+ {"To lowercase the string 'AbC'", "SELECT lower('AbC')"}),
+
+ help_text(
+ "ltrim",
+ "Returns a string formed by removing any and all characters that "
+ "appear in the second argument from the left side of the first.")
+ .sql_function()
+ .with_parameter(
+ {"str", "The string to trim characters from the left side"})
+ .with_parameter(
+ help_text("chars",
+ "The characters to trim. Defaults to spaces.")
+ .optional())
+ .with_tags({"string"})
+ .with_example({
+ "To trim the leading space characters from the string ' abc'",
+ "SELECT ltrim(' abc')",
+ })
+ .with_example({
+ "To trim the characters 'a' or 'b' from the left side of the "
+ "string 'aaaabbbc'",
+ "SELECT ltrim('aaaabbbc', 'ab')",
+ }),
+
+ help_text("max",
+ "Returns the argument with the maximum value, or return NULL "
+ "if any argument is NULL.")
+ .sql_function()
+ .with_parameter(help_text("X",
+ "The numbers to find the maximum of. "
+ "If only one argument is given, this "
+ "function operates as an aggregate.")
+ .one_or_more())
+ .with_tags({"math"})
+ .with_example({"To get the largest value from the parameters",
+ "SELECT max(2, 1, 3)"})
+ .with_example({"To get the largest value from an aggregate",
+ "SELECT max(status) FROM http_status_codes"}),
+
+ help_text("min",
+ "Returns the argument with the minimum value, or return NULL "
+ "if any argument is NULL.")
+ .sql_function()
+ .with_parameter(help_text("X",
+ "The numbers to find the minimum of. "
+ "If only one argument is given, this "
+ "function operates as an aggregate.")
+ .one_or_more())
+ .with_tags({"math"})
+ .with_example({"To get the smallest value from the parameters",
+ "SELECT min(2, 1, 3)"})
+ .with_example({"To get the smallest value from an aggregate",
+ "SELECT min(status) FROM http_status_codes"}),
+
+ help_text("nullif",
+ "Returns its first argument if the arguments are different "
+ "and NULL if the arguments are the same.")
+ .sql_function()
+ .with_parameter({"X", "The first argument to compare."})
+ .with_parameter({"Y", "The argument to compare against the first."})
+ .with_example(
+ {"To test if 1 is different from 1", "SELECT nullif(1, 1)"})
+ .with_example(
+ {"To test if 1 is different from 2", "SELECT nullif(1, 2)"}),
+
+ help_text("printf",
+ "Returns a string with this functions arguments substituted "
+ "into the given format. "
+ "Substitution points are specified using percent (%) "
+ "options, much like the standard C printf() function.")
+ .sql_function()
+ .with_parameter({"format", "The format of the string to return."})
+ .with_parameter(help_text("X",
+ "The argument to substitute at a given "
+ "position in the format."))
+ .with_tags({"string"})
+ .with_example({"To substitute 'World' into the string 'Hello, %s!'",
+ "SELECT printf('Hello, %s!', 'World')"})
+ .with_example({"To right-align 'small' in the string 'align:' with "
+ "a column width of 10",
+ "SELECT printf('align: % 10s', 'small')"})
+ .with_example({"To format 11 with a width of five characters and "
+ "leading zeroes",
+ "SELECT printf('value: %05d', 11)"}),
+
+ help_text("quote",
+ "Returns the text of an SQL literal which is the value of "
+ "its argument suitable for inclusion into an SQL statement.")
+ .sql_function()
+ .with_parameter({"X", "The string to quote."})
+ .with_example({"To quote the string 'abc'", "SELECT quote('abc')"})
+ .with_example(
+ {"To quote the string 'abc'123'", "SELECT quote('abc''123')"}),
+
+ help_text("random",
+ "Returns a pseudo-random integer between "
+ "-9223372036854775808 and +9223372036854775807.")
+ .sql_function(),
+
+ help_text("randomblob",
+ "Return an N-byte blob containing pseudo-random bytes.")
+ .sql_function()
+ .with_parameter({"N", "The size of the blob in bytes."}),
+
+ help_text(
+ "replace",
+ "Returns a string formed by substituting the replacement string "
+ "for every occurrence of the old string in the given string.")
+ .sql_function()
+ .with_parameter({"str", "The string to perform substitutions on."})
+ .with_parameter({"old", "The string to be replaced."})
+ .with_parameter({"replacement",
+ "The string to replace any occurrences of the old "
+ "string with."})
+ .with_tags({"string"})
+ .with_example({"To replace the string 'x' with 'z' in 'abc'",
+ "SELECT replace('abc', 'x', 'z')"})
+ .with_example({"To replace the string 'a' with 'z' in 'abc'",
+ "SELECT replace('abc', 'a', 'z')"}),
+
+ help_text("round",
+ "Returns a floating-point value rounded to the given number "
+ "of digits to the right of the decimal point.")
+ .sql_function()
+ .with_parameter({"num", "The value to round."})
+ .with_parameter(help_text("digits",
+ "The number of digits to the right of "
+ "the decimal to round to.")
+ .optional())
+ .with_tags({"math"})
+ .with_example({"To round the number 123.456 to an integer",
+ "SELECT round(123.456)"})
+ .with_example({"To round the number 123.456 to a precision of 1",
+ "SELECT round(123.456, 1)"})
+ .with_example({"To round the number 123.456 to a precision of 5",
+ "SELECT round(123.456, 5)"}),
+
+ help_text(
+ "rtrim",
+ "Returns a string formed by removing any and all characters that "
+ "appear in the second argument from the right side of the first.")
+ .sql_function()
+ .with_parameter(
+ {"str", "The string to trim characters from the right side"})
+ .with_parameter(
+ help_text("chars",
+ "The characters to trim. Defaults to spaces.")
+ .optional())
+ .with_tags({"string"})
+ .with_example({
+ "To trim the space characters from the end of the string 'abc "
+ " '",
+ "SELECT rtrim('abc ')",
+ })
+ .with_example({
+ "To trim the characters 'b' and 'c' from the string "
+ "'abbbbcccc'",
+ "SELECT rtrim('abbbbcccc', 'bc')",
+ }),
+
+ help_text("sqlite_compileoption_get",
+ "Returns the N-th compile-time option used to build SQLite "
+ "or NULL if N is out of range.")
+ .sql_function()
+ .with_parameter({"N", "The option number to get"}),
+
+ help_text("sqlite_compileoption_used",
+ "Returns true (1) or false (0) depending on whether or not "
+ "that compile-time option was used during the build.")
+ .sql_function()
+ .with_parameter({"option", "The name of the compile-time option."})
+ .with_example(
+ {"To check if the SQLite library was compiled with ENABLE_FTS3",
+ "SELECT sqlite_compileoption_used('ENABLE_FTS3')"}),
+
+ help_text("sqlite_source_id",
+ "Returns a string that identifies the specific version of "
+ "the source code that was used to build the SQLite library.")
+ .sql_function(),
+
+ help_text("sqlite_version",
+ "Returns the version string for the SQLite library that is "
+ "running.")
+ .sql_function(),
+
+ help_text("substr",
+ "Returns a substring of input string X that begins with the "
+ "Y-th character and which is Z characters long.")
+ .sql_function()
+ .with_parameter({"str", "The string to extract a substring from."})
+ .with_parameter(
+ {"start",
+ "The index within 'str' that is the start of the substring. "
+ "Indexes begin at 1. "
+ "A negative value means that the substring is found by "
+ "counting from the right rather than the left. "})
+ .with_parameter(
+ help_text("size",
+ "The size of the substring. "
+ "If not given, then all characters through the end "
+ "of the string are returned. "
+ "If the value is negative, then the characters "
+ "before the start are returned.")
+ .optional())
+ .with_tags({"string"})
+ .with_example({"To get the substring starting at the second "
+ "character until the end of the string 'abc'",
+ "SELECT substr('abc', 2)"})
+ .with_example({"To get the substring of size one starting at the "
+ "second character of the string 'abc'",
+ "SELECT substr('abc', 2, 1)"})
+ .with_example({"To get the substring starting at the last "
+ "character until the end of the string 'abc'",
+ "SELECT substr('abc', -1)"})
+ .with_example(
+ {"To get the substring starting at the last character and "
+ "going backwards one step of the string 'abc'",
+ "SELECT substr('abc', -1, -1)"}),
+
+ help_text("total_changes",
+ "Returns the number of row changes caused by INSERT, UPDATE "
+ "or DELETE statements since the current database connection "
+ "was opened.")
+ .sql_function(),
+
+ help_text("trim",
+ "Returns a string formed by removing any and all characters "
+ "that appear in the second argument from the left and right "
+ "sides of the first.")
+ .sql_function()
+ .with_parameter({"str",
+ "The string to trim characters from the left and "
+ "right sides."})
+ .with_parameter(
+ help_text("chars",
+ "The characters to trim. Defaults to spaces.")
+ .optional())
+ .with_tags({"string"})
+ .with_example({
+ "To trim spaces from the start and end of the string ' abc "
+ " '",
+ "SELECT trim(' abc ')",
+ })
+ .with_example({
+ "To trim the characters '-' and '+' from the string '-+abc+-'",
+ "SELECT trim('-+abc+-', '-+')",
+ }),
+
+ help_text(
+ "typeof",
+ "Returns a string that indicates the datatype of the expression X: "
+ "\"null\", \"integer\", \"real\", \"text\", or \"blob\".")
+ .sql_function()
+ .with_parameter({"X", "The expression to check."})
+ .with_example(
+ {"To get the type of the number 1", "SELECT typeof(1)"})
+ .with_example({"To get the type of the string 'abc'",
+ "SELECT typeof('abc')"}),
+
+ help_text("unicode",
+ "Returns the numeric unicode code point corresponding to the "
+ "first character of the string X.")
+ .sql_function()
+ .with_parameter({"X", "The string to examine."})
+ .with_tags({"string"})
+ .with_example({"To get the unicode code point for the first "
+ "character of 'abc'",
+ "SELECT unicode('abc')"}),
+
+ help_text("unlikely", "Short-hand for likelihood(X, 0.0625)")
+ .sql_function()
+ .with_parameter({"value", "The boolean value to return"}),
+
+ help_text("upper",
+ "Returns a copy of the given string with all ASCII "
+ "characters converted to upper case.")
+ .sql_function()
+ .with_parameter({"str", "The string to convert."})
+ .with_tags({"string"})
+ .with_example(
+ {"To uppercase the string 'aBc'", "SELECT upper('aBc')"}),
+
+ help_text("zeroblob", "Returns a BLOB consisting of N bytes of 0x00.")
+ .sql_function()
+ .with_parameter({"N", "The size of the BLOB."}),
+
+ help_text("date", "Returns the date in this format: YYYY-MM-DD.")
+ .sql_function()
+ .with_parameter({"timestring", "The string to convert to a date."})
+ .with_parameter(help_text("modifier",
+ "A transformation that is applied to the "
+ "value to the left.")
+ .zero_or_more())
+ .with_tags({"datetime"})
+ .with_example({"To get the date portion of the timestamp "
+ "'2017-01-02T03:04:05'",
+ "SELECT date('2017-01-02T03:04:05')"})
+ .with_example({"To get the date portion of the timestamp "
+ "'2017-01-02T03:04:05' plus one day",
+ "SELECT date('2017-01-02T03:04:05', '+1 day')"})
+ .with_example(
+ {"To get the date portion of the epoch timestamp 1491341842",
+ "SELECT date(1491341842, 'unixepoch')"}),
+
+ help_text("time", "Returns the time in this format: HH:MM:SS.")
+ .sql_function()
+ .with_parameter({"timestring", "The string to convert to a time."})
+ .with_parameter(help_text("modifier",
+ "A transformation that is applied to the "
+ "value to the left.")
+ .zero_or_more())
+ .with_tags({"datetime"})
+ .with_example({"To get the time portion of the timestamp "
+ "'2017-01-02T03:04:05'",
+ "SELECT time('2017-01-02T03:04:05')"})
+ .with_example({"To get the time portion of the timestamp "
+ "'2017-01-02T03:04:05' plus one minute",
+ "SELECT time('2017-01-02T03:04:05', '+1 minute')"})
+ .with_example(
+ {"To get the time portion of the epoch timestamp 1491341842",
+ "SELECT time(1491341842, 'unixepoch')"}),
+
+ help_text(
+ "datetime",
+ "Returns the date and time in this format: YYYY-MM-DD HH:MM:SS.")
+ .sql_function()
+ .with_parameter(
+ {"timestring", "The string to convert to a date with time."})
+ .with_parameter(help_text("modifier",
+ "A transformation that is applied to the "
+ "value to the left.")
+ .zero_or_more())
+ .with_tags({"datetime"})
+ .with_example({"To get the date and time portion of the timestamp "
+ "'2017-01-02T03:04:05'",
+ "SELECT datetime('2017-01-02T03:04:05')"})
+ .with_example(
+ {"To get the date and time portion of the timestamp "
+ "'2017-01-02T03:04:05' plus one minute",
+ "SELECT datetime('2017-01-02T03:04:05', '+1 minute')"})
+ .with_example({"To get the date and time portion of the epoch "
+ "timestamp 1491341842",
+ "SELECT datetime(1491341842, 'unixepoch')"}),
+
+ help_text("julianday",
+ "Returns the number of days since noon in Greenwich on "
+ "November 24, 4714 B.C.")
+ .sql_function()
+ .with_parameter(
+ {"timestring", "The string to convert to a date with time."})
+ .with_parameter(help_text("modifier",
+ "A transformation that is applied to the "
+ "value to the left.")
+ .zero_or_more())
+ .with_tags({"datetime"})
+ .with_example({"To get the julian day from the timestamp "
+ "'2017-01-02T03:04:05'",
+ "SELECT julianday('2017-01-02T03:04:05')"})
+ .with_example(
+ {"To get the julian day from the timestamp "
+ "'2017-01-02T03:04:05' plus one minute",
+ "SELECT julianday('2017-01-02T03:04:05', '+1 minute')"})
+ .with_example(
+ {"To get the julian day from the timestamp 1491341842",
+ "SELECT julianday(1491341842, 'unixepoch')"}),
+
+ help_text("strftime",
+ "Returns the date formatted according to the format string "
+ "specified as the first argument.")
+ .sql_function()
+ .with_parameter(
+ {"format",
+ "A format string with substitutions similar to those found in "
+ "the strftime() standard C library."})
+ .with_parameter(
+ {"timestring", "The string to convert to a date with time."})
+ .with_parameter(help_text("modifier",
+ "A transformation that is applied to the "
+ "value to the left.")
+ .zero_or_more())
+ .with_tags({"datetime"})
+ .with_example(
+ {"To get the year from the timestamp '2017-01-02T03:04:05'",
+ "SELECT strftime('%Y', '2017-01-02T03:04:05')"})
+ .with_example({"To create a string with the time from the "
+ "timestamp '2017-01-02T03:04:05' plus one minute",
+ "SELECT strftime('The time is: %H:%M:%S', "
+ "'2017-01-02T03:04:05', '+1 minute')"})
+ .with_example(
+ {"To create a string with the Julian day from the epoch "
+ "timestamp 1491341842",
+ "SELECT strftime('Julian day: %J', 1491341842, 'unixepoch')"}),
+
+ help_text(
+ "avg",
+ "Returns the average value of all non-NULL numbers within a group.")
+ .sql_function()
+ .with_parameter({"X", "The value to compute the average of."})
+ .with_tags({"math"})
+ .with_example({"To get the average of the column 'ex_duration' "
+ "from the table 'lnav_example_log'",
+ "SELECT avg(ex_duration) FROM lnav_example_log"})
+ .with_example(
+ {"To get the average of the column 'ex_duration' from the "
+ "table 'lnav_example_log' when grouped by 'ex_procname'",
+ "SELECT ex_procname, avg(ex_duration) FROM lnav_example_log "
+ "GROUP BY ex_procname"}),
+
+ help_text("count",
+ "If the argument is '*', the total number of rows in the "
+ "group is returned. "
+ "Otherwise, the number of times the argument is non-NULL.")
+ .sql_function()
+ .with_parameter({"X", "The value to count."})
+ .with_example(
+ {"To get the count of the non-NULL rows of 'lnav_example_log'",
+ "SELECT count(*) FROM lnav_example_log"})
+ .with_example({"To get the count of the non-NULL values of "
+ "'log_part' from 'lnav_example_log'",
+ "SELECT count(log_part) FROM lnav_example_log"}),
+
+ help_text("group_concat",
+ "Returns a string which is the concatenation of all non-NULL "
+ "values of X separated by a comma or the given separator.")
+ .sql_function()
+ .with_parameter({"X", "The value to concatenate."})
+ .with_parameter(
+ help_text("sep", "The separator to place between the values.")
+ .optional())
+ .with_tags({"string"})
+ .with_example(
+ {"To concatenate the values of the column 'ex_procname' from "
+ "the table 'lnav_example_log'",
+ "SELECT group_concat(ex_procname) FROM lnav_example_log"})
+ .with_example({"To join the values of the column 'ex_procname' "
+ "using the string ', '",
+ "SELECT group_concat(ex_procname, ', ') FROM "
+ "lnav_example_log"})
+ .with_example({"To concatenate the distinct values of the column "
+ "'ex_procname' from the table 'lnav_example_log'",
+ "SELECT group_concat(DISTINCT ex_procname) FROM "
+ "lnav_example_log"}),
+
+ help_text("sum",
+ "Returns the sum of the values in the group as an integer.")
+ .sql_function()
+ .with_parameter({"X", "The values to add."})
+ .with_tags({"math"})
+ .with_example({
+ "To sum all of the values in the column "
+ "'ex_duration' from the table 'lnav_example_log'",
+ "SELECT sum(ex_duration) FROM lnav_example_log",
+ }),
+
+ help_text(
+ "total",
+ "Returns the sum of the values in the group as a floating-point.")
+ .sql_function()
+ .with_parameter({"X", "The values to add."})
+ .with_tags({"math"})
+ .with_example({
+ "To total all of the values in the column "
+ "'ex_duration' from the table 'lnav_example_log'",
+ "SELECT total(ex_duration) FROM lnav_example_log",
+ }),
+
+ help_text("generate_series",
+ "A table-valued-function that returns the whole numbers "
+ "between a lower and upper bound, inclusive")
+ .sql_table_valued_function()
+ .with_parameter({"start", "The starting point of the series"})
+ .with_parameter({"stop", "The stopping point of the series"})
+ .with_parameter(
+ help_text("step", "The increment between each value")
+ .optional())
+ .with_result({"value", "The number in the series"})
+ .with_example({
+ "To generate the numbers in the range [10, 14]",
+ "SELECT value FROM generate_series(10, 14)",
+ })
+ .with_example({
+ "To generate every other number in the range [10, 14]",
+ "SELECT value FROM generate_series(10, 14, 2)",
+ })
+ .with_example({"To count down from five to 1",
+ "SELECT value FROM generate_series(1, 5, -1)"})};
+
+ for (auto& ht : builtin_funcs) {
+ sqlite_function_help.insert(std::make_pair(ht.ht_name, &ht));
+ ht.index_tags();
+ }
+
+ static help_text builtin_win_funcs[] = {
+ help_text("row_number",
+ "Returns the number of the row within the current partition, "
+ "starting from 1.")
+ .sql_function()
+ .with_tags({"window"})
+ .with_example({"To number messages from a process",
+ "SELECT row_number() OVER (PARTITION BY ex_procname "
+ "ORDER BY log_line) AS msg_num, ex_procname, "
+ "log_body FROM lnav_example_log"}),
+
+ help_text("rank",
+ "Returns the row_number() of the first peer in each group "
+ "with gaps")
+ .sql_function()
+ .with_tags({"window"}),
+
+ help_text("dense_rank",
+ "Returns the row_number() of the first peer in each group "
+ "without gaps")
+ .sql_function()
+ .with_tags({"window"}),
+
+ help_text("percent_rank", "Returns (rank - 1) / (partition-rows - 1)")
+ .sql_function()
+ .with_tags({"window"}),
+
+ help_text("cume_dist", "Returns the cumulative distribution")
+ .sql_function()
+ .with_tags({"window"}),
+
+ help_text(
+ "ntile",
+ "Returns the number of the group that the current row is a part of")
+ .sql_function()
+ .with_parameter({"groups", "The number of groups"})
+ .with_tags({"window"}),
+
+ help_text("lag",
+ "Returns the result of evaluating the expression against the "
+ "previous row in the partition.")
+ .sql_function()
+ .with_parameter(
+ {"expr", "The expression to execute over the previous row"})
+ .with_parameter(
+ help_text("offset",
+ "The offset from the current row in the partition")
+ .optional())
+ .with_parameter(help_text("default",
+ "The default value if the previous row "
+ "does not exist instead of NULL")
+ .optional())
+ .with_tags({"window"}),
+
+ help_text("lead",
+ "Returns the result of evaluating the expression against the "
+ "next row in the partition.")
+ .sql_function()
+ .with_parameter(
+ {"expr", "The expression to execute over the next row"})
+ .with_parameter(
+ help_text("offset",
+ "The offset from the current row in the partition")
+ .optional())
+ .with_parameter(help_text("default",
+ "The default value if the next row does "
+ "not exist instead of NULL")
+ .optional())
+ .with_tags({"window"}),
+
+ help_text("first_value",
+ "Returns the result of evaluating the expression against the "
+ "first row in the window frame.")
+ .sql_function()
+ .with_parameter(
+ {"expr", "The expression to execute over the first row"})
+ .with_tags({"window"}),
+
+ help_text("last_value",
+ "Returns the result of evaluating the expression against the "
+ "last row in the window frame.")
+ .sql_function()
+ .with_parameter(
+ {"expr", "The expression to execute over the last row"})
+ .with_tags({"window"}),
+
+ help_text("nth_value",
+ "Returns the result of evaluating the expression against the "
+ "nth row in the window frame.")
+ .sql_function()
+ .with_parameter(
+ {"expr", "The expression to execute over the nth row"})
+ .with_parameter({"N", "The row number"})
+ .with_tags({"window"}),
+ };
+
+ for (auto& ht : builtin_win_funcs) {
+ sqlite_function_help.insert(std::make_pair(ht.ht_name, &ht));
+ ht.index_tags();
+ }
+
+ static help_text idents[] = {
+ help_text("ATTACH", "Attach a database file to the current connection.")
+ .sql_keyword()
+ .with_parameter(
+ help_text("filename", "The path to the database file.")
+ .with_flag_name("DATABASE"))
+ .with_parameter(help_text("schema-name",
+ "The prefix for tables in this database.")
+ .with_flag_name("AS"))
+ .with_example({"To attach the database file '/tmp/customers.db' "
+ "with the name customers",
+ "ATTACH DATABASE '/tmp/customers.db' AS customers"}),
+
+ help_text("DETACH", "Detach a database from the current connection.")
+ .sql_keyword()
+ .with_parameter(help_text("schema-name",
+ "The prefix for tables in this database.")
+ .with_flag_name("DATABASE"))
+ .with_example({"To detach the database named 'customers'",
+ "DETACH DATABASE customers"}),
+
+ help_text("CREATE", "Assign a name to a SELECT statement")
+ .sql_keyword()
+ .with_parameter(help_text("TEMP").optional())
+ .with_parameter(help_text("").with_flag_name("VIEW"))
+ .with_parameter(
+ help_text("IF NOT EXISTS",
+ "Do not create the view if it already exists")
+ .optional())
+ .with_parameter(
+ help_text("schema-name.", "The database to create the view in")
+ .optional())
+ .with_parameter(help_text("view-name", "The name of the view"))
+ .with_parameter(
+ help_text("select-stmt",
+ "The SELECT statement the view represents")
+ .with_flag_name("AS")),
+
+ help_text("CREATE", "Create a table")
+ .sql_keyword()
+ .with_parameter(help_text("TEMP").optional())
+ .with_parameter(help_text("").with_flag_name("TABLE"))
+ .with_parameter(help_text("IF NOT EXISTS").optional())
+ .with_parameter(help_text("schema-name.").optional())
+ .with_parameter(help_text("table-name"))
+ .with_parameter(help_text("select-stmt").with_flag_name("AS")),
+
+ help_text("DELETE", "Delete rows from a table")
+ .sql_keyword()
+ .with_parameter(help_text("table-name", "The name of the table")
+ .with_flag_name("FROM"))
+ .with_parameter(
+ help_text("cond", "The conditions used to delete the rows.")
+ .with_flag_name("WHERE")
+ .optional()),
+
+ help_text("DROP", "Drop an index")
+ .sql_keyword()
+ .with_parameter(help_text("").with_flag_name("INDEX"))
+ .with_parameter(help_text("IF EXISTS").optional())
+ .with_parameter(help_text("schema-name.").optional())
+ .with_parameter(help_text("index-name")),
+
+ help_text("DROP", "Drop a table")
+ .sql_keyword()
+ .with_parameter(help_text("").with_flag_name("TABLE"))
+ .with_parameter(help_text("IF EXISTS").optional())
+ .with_parameter(help_text("schema-name.").optional())
+ .with_parameter(help_text("table-name")),
+
+ help_text("DROP", "Drop a view")
+ .sql_keyword()
+ .with_parameter(help_text("").with_flag_name("VIEW"))
+ .with_parameter(help_text("IF EXISTS").optional())
+ .with_parameter(help_text("schema-name.").optional())
+ .with_parameter(help_text("view-name")),
+
+ help_text("DROP", "Drop a trigger")
+ .sql_keyword()
+ .with_parameter(help_text("").with_flag_name("TRIGGER"))
+ .with_parameter(help_text("IF EXISTS").optional())
+ .with_parameter(help_text("schema-name.").optional())
+ .with_parameter(help_text("trigger-name")),
+
+ help_text("INSERT", "Insert rows into a table")
+ .sql_keyword()
+ .with_parameter(help_text("").with_flag_name("INTO"))
+ .with_parameter(help_text("schema-name.").optional())
+ .with_parameter(help_text("table-name"))
+ .with_parameter(
+ help_text("column-name").with_grouping("(", ")").zero_or_more())
+ .with_parameter(help_text("expr")
+ .with_flag_name("VALUES")
+ .with_grouping("(", ")")
+ .one_or_more())
+ .with_example(
+ {"To insert the pair containing 'MSG' and 'HELLO, WORLD!' into "
+ "the 'environ' table",
+ "INSERT INTO environ VALUES ('MSG', 'HELLO, WORLD!')"}),
+
+ help_text("SELECT",
+ "Query the database and return zero or more rows of data.")
+ .sql_keyword()
+ .with_parameter(
+ help_text(
+ "result-column",
+ "The expression used to generate a result for this column.")
+ .one_or_more())
+ .with_parameter(help_text("table", "The table(s) to query for data")
+ .with_flag_name("FROM")
+ .zero_or_more())
+ .with_parameter(
+ help_text("cond",
+ "The conditions used to select the rows to return.")
+ .with_flag_name("WHERE")
+ .optional())
+ .with_parameter(
+ help_text("grouping-expr",
+ "The expression to use when grouping rows.")
+ .with_flag_name("GROUP BY")
+ .zero_or_more())
+ .with_parameter(
+ help_text("ordering-term",
+ "The values to use when ordering the result set.")
+ .with_flag_name("ORDER BY")
+ .zero_or_more())
+ .with_parameter(
+ help_text("limit-expr", "The maximum number of rows to return.")
+ .with_flag_name("LIMIT")
+ .zero_or_more())
+ .with_example(
+ {"To select all of the columns from the table 'syslog_log'",
+ "SELECT * FROM syslog_log"}),
+
+ help_text("WITH",
+ "Create a temporary view that exists only for the duration "
+ "of a SQL statement.")
+ .sql_keyword()
+ .with_parameter(
+ help_text("").with_flag_name("RECURSIVE").optional())
+ .with_parameter(
+ {"cte-table-name", "The name for the temporary table."})
+ .with_parameter(help_text("select-stmt",
+ "The SELECT statement used to populate "
+ "the temporary table.")
+ .with_flag_name("AS")),
+
+ help_text(
+ "UPDATE",
+ "Modify a subset of values in zero or more rows of the given table")
+ .sql_keyword()
+ .with_parameter(help_text("table", "The table to update"))
+ .with_parameter(help_text("").with_flag_name("SET"))
+ .with_parameter(
+ help_text("column-name", "The columns in the table to update.")
+ .with_parameter(
+ help_text("expr",
+ "The values to place into the column.")
+ .with_flag_name("="))
+ .one_or_more())
+ .with_parameter(help_text("cond",
+ "The condition used to determine whether "
+ "a row should be updated.")
+ .with_flag_name("WHERE")
+ .optional())
+ .with_example({
+ "To mark the syslog message at line 40",
+ "UPDATE syslog_log SET log_mark = 1 WHERE log_line = 40",
+ }),
+
+ help_text("CASE",
+ "Evaluate a series of expressions in order until one "
+ "evaluates to true and then return it's result. "
+ "Similar to an IF-THEN-ELSE construct in other languages.")
+ .sql_keyword()
+ .with_parameter(help_text("base-expr",
+ "The base expression that is used for "
+ "comparison in the branches")
+ .optional())
+ .with_parameter(
+ help_text(
+ "cmp-expr",
+ "The expression to test if this branch should be taken")
+ .with_flag_name("WHEN")
+ .one_or_more()
+ .with_parameter(
+ help_text("then-expr", "The result for this branch.")
+ .with_flag_name("THEN")))
+ .with_parameter(
+ help_text("else-expr",
+ "The result of this CASE if no branches matched.")
+ .with_flag_name("ELSE")
+ .optional())
+ .with_parameter(help_text("").with_flag_name("END"))
+ .with_example({
+ "To evaluate the number one and return the string 'one'",
+ "SELECT CASE 1 WHEN 0 THEN 'zero' WHEN 1 THEN 'one' END",
+ }),
+
+ help_text("CAST",
+ "Convert the value of the given expression to a different "
+ "storage class specified by type-name.")
+ .sql_function()
+ .with_parameter({"expr", "The value to convert."})
+ .with_parameter(
+ help_text("type-name", "The name of the type to convert to.")
+ .with_flag_name("AS"))
+ .with_example({
+ "To cast the value 1.23 as an integer",
+ "SELECT CAST(1.23 AS INTEGER)",
+ }),
+
+ help_text("expr", "Match an expression against a glob pattern.")
+ .sql_infix()
+ .with_parameter(help_text("NOT").optional())
+ .with_parameter(
+ help_text("pattern", "The glob pattern to match against.")
+ .with_flag_name("GLOB"))
+ .with_example({
+ "To check if a value matches the pattern '*.log'",
+ "SELECT 'foobar.log' GLOB '*.log'",
+ }),
+
+ help_text("expr", "Match an expression against a text pattern.")
+ .sql_infix()
+ .with_parameter(help_text("NOT").optional())
+ .with_parameter(
+ help_text("pattern", "The pattern to match against.")
+ .with_flag_name("LIKE"))
+ .with_example({
+ "To check if a value matches the pattern 'Hello, %!'",
+ "SELECT 'Hello, World!' LIKE 'Hello, %!'",
+ }),
+
+ help_text("expr", "Match an expression against a regular expression.")
+ .sql_infix()
+ .with_parameter(help_text("NOT").optional())
+ .with_parameter(
+ help_text("pattern", "The regular expression to match against.")
+ .with_flag_name("REGEXP"))
+ .with_example({
+ "To check if a value matches the pattern 'file-\\d+'",
+ "SELECT 'file-23' REGEXP 'file-\\d+'",
+ }),
+
+ help_text("expr", "Assign a collating sequence to the expression.")
+ .sql_infix()
+ .with_parameter(
+ help_text("collation-name", "The name of the collator.")
+ .with_flag_name("COLLATE"))
+ .with_example({
+ "To change the collation method for string comparisons",
+ "SELECT ('a2' < 'a10'), ('a2' < 'a10' COLLATE "
+ "naturalnocase)",
+ }),
+
+ help_text("expr", "Test if an expression is between two values.")
+ .sql_infix()
+ .with_parameter(help_text("NOT").optional())
+ .with_parameter(
+ help_text("low", "The low point").with_flag_name("BETWEEN"))
+ .with_parameter(
+ help_text("hi", "The high point").with_flag_name("AND"))
+ .with_example({
+ "To check if 3 is between 5 and 10",
+ "SELECT 3 BETWEEN 5 AND 10",
+ })
+ .with_example({
+ "To check if 10 is between 5 and 10",
+ "SELECT 10 BETWEEN 5 AND 10",
+ }),
+
+ help_text("OVER", "Executes the preceding function over a window")
+ .sql_keyword()
+ .with_parameter(
+ {"window-name", "The name of the window definition"}),
+
+ help_text("OVER", "Executes the preceding function over a window")
+ .sql_function()
+ .with_parameter(help_text{
+ "base-window-name",
+ "The name of the window definition",
+ }
+ .optional())
+ .with_parameter(
+ help_text{"expr", "The values to use for partitioning"}
+ .with_flag_name("PARTITION BY")
+ .zero_or_more())
+ .with_parameter(help_text{
+ "expr", "The values used to order the rows in the window"}
+ .with_flag_name("ORDER BY")
+ .zero_or_more())
+ .with_parameter(help_text{
+ "frame-spec",
+ "Determines which output rows are read "
+ "by an aggregate window function",
+ }
+ .optional()),
+ };
+
+ for (auto& ht : idents) {
+ sqlite_function_help.insert(make_pair(toupper(ht.ht_name), &ht));
+ for (const auto& param : ht.ht_parameters) {
+ if (!param.ht_flag_name) {
+ continue;
+ }
+ sqlite_function_help.insert(
+ make_pair(toupper(param.ht_flag_name), &ht));
+ }
+ }
+
+ return 0;
+}
diff --git a/src/sqlite-extension-func.hh b/src/sqlite-extension-func.hh
new file mode 100644
index 0000000..1e62a6a
--- /dev/null
+++ b/src/sqlite-extension-func.hh
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file sqlite-extension-func.h
+ */
+
+#ifndef lnav_sqlite_extension_func_h
+#define lnav_sqlite_extension_func_h
+
+#include <map>
+#include <string>
+
+#include <sqlite3.h>
+#include <stdint.h>
+
+#include "help_text.hh"
+
+struct FuncDef {
+ const char* zName{nullptr};
+ signed char nArg{0};
+ int eTextRep{0}; /* 1: UTF-16. 0: UTF-8 */
+ uint8_t needCollSeq{0};
+ void (*xFunc)(sqlite3_context*, int, sqlite3_value**){nullptr};
+ help_text fd_help{};
+
+ FuncDef& with_flags(int flags)
+ {
+ this->eTextRep = flags;
+ return *this;
+ }
+};
+
+struct FuncDefAgg {
+ const char* zName{nullptr};
+ signed char nArg{0};
+ uint8_t needCollSeq{0};
+ void (*xStep)(sqlite3_context*, int, sqlite3_value**){nullptr};
+ void (*xFinalize)(sqlite3_context*){nullptr};
+ help_text fda_help{};
+};
+
+typedef int (*sqlite_registration_func_t)(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs);
+
+int common_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs);
+
+int state_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs);
+
+int string_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs);
+
+int network_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs);
+
+int fs_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs);
+
+int json_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs);
+
+int time_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs);
+
+int yaml_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs);
+
+extern sqlite_registration_func_t sqlite_registration_funcs[];
+
+int register_sqlite_funcs(sqlite3* db, sqlite_registration_func_t* reg_funcs);
+
+extern "C"
+{
+int sqlite3_db_dump(
+ sqlite3* db, /* The database connection */
+ const char* zSchema, /* Which schema to dump. Usually "main". */
+ const char* zTable, /* Which table to dump. NULL means everything. */
+ int (*xCallback)(const char*, void*), /* Output sent to this callback */
+ void* pArg /* Second argument of the callback */
+);
+}
+
+#endif
diff --git a/src/sqlitepp.cc b/src/sqlitepp.cc
new file mode 100644
index 0000000..906d248
--- /dev/null
+++ b/src/sqlitepp.cc
@@ -0,0 +1,36 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "sqlitepp.hh"
+
+namespace sqlitepp {
+
+const char* ERROR_PREFIX = "lnav-error:";
+
+}
diff --git a/src/sqlitepp.client.hh b/src/sqlitepp.client.hh
new file mode 100644
index 0000000..5a82756
--- /dev/null
+++ b/src/sqlitepp.client.hh
@@ -0,0 +1,209 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_sqlitepp_client_hh
+#define lnav_sqlitepp_client_hh
+
+#include <sqlite3.h>
+
+#include "base/auto_mem.hh"
+#include "base/intern_string.hh"
+#include "base/lnav_log.hh"
+#include "sql_util.hh"
+#include "vtab_module.hh"
+
+inline int
+bind_to_sqlite(sqlite3_stmt* stmt, int index, const struct timeval& tv)
+{
+ char timestamp[64];
+
+ sql_strftime(timestamp, sizeof(timestamp), tv, 'T');
+
+ return sqlite3_bind_text(stmt, index, timestamp, -1, SQLITE_TRANSIENT);
+}
+
+inline int
+bind_to_sqlite(sqlite3_stmt* stmt, int index, const char* str)
+{
+ return sqlite3_bind_text(stmt, index, str, -1, SQLITE_TRANSIENT);
+}
+
+inline int
+bind_to_sqlite(sqlite3_stmt* stmt, int index, intern_string_t ist)
+{
+ return sqlite3_bind_text(
+ stmt, index, ist.get(), ist.size(), SQLITE_TRANSIENT);
+}
+
+inline int
+bind_to_sqlite(sqlite3_stmt* stmt, int index, const std::string& str)
+{
+ return sqlite3_bind_text(
+ stmt, index, str.c_str(), str.size(), SQLITE_TRANSIENT);
+}
+
+inline int
+bind_to_sqlite(sqlite3_stmt* stmt, int index, int64_t i)
+{
+ return sqlite3_bind_int64(stmt, index, i);
+}
+
+template<typename... Args, std::size_t... Idx>
+int
+bind_values_helper(sqlite3_stmt* stmt,
+ std::index_sequence<Idx...> idxs,
+ Args... args)
+{
+ int rcs[] = {bind_to_sqlite(stmt, Idx + 1, args)...};
+
+ for (size_t lpc = 0; lpc < idxs.size(); lpc++) {
+ if (rcs[lpc] != SQLITE_OK) {
+ log_error("Failed to bind column %d in statement: %s",
+ lpc,
+ sqlite3_sql(stmt));
+ return rcs[lpc];
+ }
+ }
+
+ return SQLITE_OK;
+}
+
+template<typename... Args>
+int
+bind_values(sqlite3_stmt* stmt, Args... args)
+{
+ return bind_values_helper(
+ stmt, std::make_index_sequence<sizeof...(Args)>(), args...);
+}
+
+struct prepared_stmt {
+ prepared_stmt(auto_mem<sqlite3_stmt> stmt) : ps_stmt(std::move(stmt)) {}
+
+ Result<void, std::string> execute()
+ {
+ auto rc = sqlite3_reset(this->ps_stmt.in());
+ if (rc != SQLITE_OK) {
+ return Err(std::string(
+ sqlite3_errmsg(sqlite3_db_handle(this->ps_stmt.in()))));
+ }
+
+ rc = sqlite3_step(this->ps_stmt.in());
+ if (rc == SQLITE_OK || rc == SQLITE_DONE) {
+ return Ok();
+ }
+
+ auto msg = std::string(
+ sqlite3_errmsg(sqlite3_db_handle(this->ps_stmt.in())));
+ return Err(msg);
+ }
+
+ struct end_of_rows {};
+ struct fetch_error {
+ std::string fe_msg;
+ };
+
+ void reset() { sqlite3_reset(this->ps_stmt.in()); }
+
+ template<typename T>
+ using fetch_result = mapbox::util::variant<T, end_of_rows, fetch_error>;
+
+ template<typename T>
+ fetch_result<T> fetch_row()
+ {
+ auto rc = sqlite3_step(this->ps_stmt.in());
+ if (rc == SQLITE_OK || rc == SQLITE_DONE) {
+ return end_of_rows{};
+ }
+
+ if (rc == SQLITE_ROW) {
+ const auto argc = sqlite3_column_count(this->ps_stmt.in());
+ sqlite3_value* argv[argc];
+
+ for (int lpc = 0; lpc < argc; lpc++) {
+ argv[lpc] = sqlite3_column_value(this->ps_stmt.in(), lpc);
+ }
+
+ return from_sqlite<T>()(argc, argv, 0);
+ }
+
+ return fetch_error{
+ sqlite3_errmsg(sqlite3_db_handle(this->ps_stmt.in())),
+ };
+ }
+
+ template<typename T, typename F>
+ Result<void, fetch_error> for_each_row(F func)
+ {
+ nonstd::optional<fetch_error> err;
+ auto done = false;
+
+ while (!done) {
+ done = this->template fetch_row<T>().match(
+ func,
+ [](end_of_rows) { return true; },
+ [&err](const fetch_error& fe) {
+ err = fe;
+ return true;
+ });
+ }
+
+ if (err) {
+ return Err(err.value());
+ }
+
+ return Ok();
+ }
+
+ auto_mem<sqlite3_stmt> ps_stmt;
+};
+
+template<typename... Args>
+static Result<prepared_stmt, std::string>
+prepare_stmt(sqlite3* db, const char* sql, Args... args)
+{
+ auto_mem<sqlite3_stmt> retval(sqlite3_finalize);
+
+ if (sqlite3_prepare_v2(db, sql, -1, retval.out(), nullptr) != SQLITE_OK) {
+ return Err(
+ fmt::format(FMT_STRING("unable to prepare SQL statement: {}"),
+ sqlite3_errmsg(db)));
+ }
+
+ if (bind_values(retval.in(), args...) != SQLITE_OK) {
+ return Err(
+ fmt::format(FMT_STRING("unable to prepare SQL statement: {}"),
+ sqlite3_errmsg(db)));
+ }
+
+ return Ok(prepared_stmt{
+ std::move(retval),
+ });
+}
+
+#endif
diff --git a/src/sqlitepp.hh b/src/sqlitepp.hh
new file mode 100644
index 0000000..7845a23
--- /dev/null
+++ b/src/sqlitepp.hh
@@ -0,0 +1,110 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file sqlitepp.hh
+ */
+
+#ifndef lnav_sqlitepp_hh
+#define lnav_sqlitepp_hh
+
+#include <string>
+
+#include <sqlite3.h>
+
+#include "base/auto_mem.hh"
+#include "optional.hpp"
+#include "shlex.resolver.hh"
+
+/* XXX figure out how to do this with the template */
+void sqlite_close_wrapper(void* mem);
+
+using auto_sqlite3 = auto_mem<sqlite3, sqlite_close_wrapper>;
+
+namespace sqlitepp {
+
+inline auto_mem<char>
+quote(const nonstd::optional<std::string>& str)
+{
+ auto_mem<char> retval(sqlite3_free);
+
+ if (str) {
+ retval = sqlite3_mprintf("%Q", str.value().c_str());
+ } else {
+ retval = sqlite3_mprintf("NULL");
+ }
+
+ return retval;
+}
+
+extern const char* ERROR_PREFIX;
+
+struct bind_visitor {
+ bind_visitor(sqlite3_stmt* stmt, int index) : bv_stmt(stmt), bv_index(index)
+ {
+ }
+
+ void operator()(const std::string& str) const
+ {
+ sqlite3_bind_text(this->bv_stmt,
+ this->bv_index,
+ str.c_str(),
+ str.size(),
+ SQLITE_TRANSIENT);
+ }
+
+ void operator()(const string_fragment& str) const
+ {
+ sqlite3_bind_text(this->bv_stmt,
+ this->bv_index,
+ str.data(),
+ str.length(),
+ SQLITE_TRANSIENT);
+ }
+
+ void operator()(null_value_t) const
+ {
+ sqlite3_bind_null(this->bv_stmt, this->bv_index);
+ }
+
+ void operator()(int64_t value) const
+ {
+ sqlite3_bind_int64(this->bv_stmt, this->bv_index, value);
+ }
+
+ void operator()(double value) const
+ {
+ sqlite3_bind_double(this->bv_stmt, this->bv_index, value);
+ }
+
+ sqlite3_stmt* bv_stmt;
+ int bv_index;
+};
+
+} // namespace sqlitepp
+
+#endif
diff --git a/src/state-extension-functions.cc b/src/state-extension-functions.cc
new file mode 100644
index 0000000..b382471
--- /dev/null
+++ b/src/state-extension-functions.cc
@@ -0,0 +1,177 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file state-extension-functions.cc
+ */
+
+#include <string>
+
+#include <stdint.h>
+
+#include "base/opt_util.hh"
+#include "config.h"
+#include "lnav.hh"
+#include "sql_util.hh"
+#include "sqlite3.h"
+#include "vtab_module.hh"
+
+static nonstd::optional<int64_t>
+sql_log_top_line()
+{
+ const auto& tc = lnav_data.ld_views[LNV_LOG];
+
+ if (tc.get_inner_height() == 0_vl) {
+ return nonstd::nullopt;
+ }
+ return (int64_t) tc.get_top();
+}
+
+static nonstd::optional<std::string>
+sql_log_top_datetime()
+{
+ const auto& tc = lnav_data.ld_views[LNV_LOG];
+
+ if (tc.get_inner_height() == 0_vl) {
+ return nonstd::nullopt;
+ }
+
+ auto top_time = lnav_data.ld_log_source.time_for_row(
+ lnav_data.ld_views[LNV_LOG].get_top());
+ if (!top_time) {
+ return nonstd::nullopt;
+ }
+
+ char buffer[64];
+
+ sql_strftime(buffer, sizeof(buffer), top_time.value());
+ return buffer;
+}
+
+static nonstd::optional<std::string>
+sql_lnav_top_file()
+{
+ auto top_view_opt = lnav_data.ld_view_stack.top();
+
+ if (!top_view_opt) {
+ return nonstd::nullopt;
+ }
+
+ auto* top_view = top_view_opt.value();
+ return top_view->map_top_row([](const auto& al) {
+ return get_string_attr(al.get_attrs(), logline::L_FILE) |
+ [](const auto wrapper) {
+ auto lf = wrapper.get();
+
+ return nonstd::make_optional(lf->get_filename());
+ };
+ });
+}
+
+static const char*
+sql_lnav_version()
+{
+ return PACKAGE_VERSION;
+}
+
+static int64_t
+sql_error(const char* str)
+{
+ throw sqlite_func_error("{}", str);
+}
+
+static nonstd::optional<std::string>
+sql_echoln(nonstd::optional<std::string> arg)
+{
+ if (arg) {
+ auto& ec = lnav_data.ld_exec_context;
+ auto outfile = ec.get_output();
+
+ if (outfile) {
+ fmt::print(outfile.value(), FMT_STRING("{}\n"), arg.value());
+ if (outfile.value() == stdout) {
+ lnav_data.ld_stdout_used = true;
+ }
+ }
+ }
+
+ return arg;
+}
+
+int
+state_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs)
+{
+ static struct FuncDef state_funcs[] = {
+ sqlite_func_adapter<decltype(&sql_log_top_line), sql_log_top_line>::
+ builder(
+ help_text("log_top_line",
+ "Return the line number at the top of the log view.")
+ .sql_function()),
+
+ sqlite_func_adapter<decltype(&sql_log_top_datetime),
+ sql_log_top_datetime>::
+ builder(help_text("log_top_datetime",
+ "Return the timestamp of the line at the top of "
+ "the log view.")
+ .sql_function()),
+
+ sqlite_func_adapter<decltype(&sql_lnav_top_file), sql_lnav_top_file>::
+ builder(help_text("lnav_top_file",
+ "Return the name of the file that the top line "
+ "in the current view came from.")
+ .sql_function()),
+
+ sqlite_func_adapter<decltype(&sql_lnav_version), sql_lnav_version>::
+ builder(
+ help_text("lnav_version", "Return the current version of lnav")
+ .sql_function()),
+
+ sqlite_func_adapter<decltype(&sql_error), sql_error>::builder(
+ help_text("raise_error",
+ "Raises an error with the given message when executed")
+ .sql_function()
+ .with_parameter({"msg", "The error message"}))
+ .with_flags(SQLITE_UTF8),
+
+ sqlite_func_adapter<decltype(&sql_echoln), sql_echoln>::builder(
+ help_text("echoln",
+ "Echo the argument to the current output file and return "
+ "it")
+ .sql_function()
+ .with_parameter(
+ {"value", "The value to write to the current output file"})
+ .with_tags({"io"}))
+ .with_flags(SQLITE_UTF8),
+
+ {nullptr},
+ };
+
+ *basic_funcs = state_funcs;
+
+ return SQLITE_OK;
+}
diff --git a/src/static_file_vtab.cc b/src/static_file_vtab.cc
new file mode 100644
index 0000000..f884cb3
--- /dev/null
+++ b/src/static_file_vtab.cc
@@ -0,0 +1,333 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <map>
+
+#include "static_file_vtab.hh"
+
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/auto_mem.hh"
+#include "base/fs_util.hh"
+#include "base/lnav_log.hh"
+#include "base/paths.hh"
+#include "config.h"
+#include "ghc/filesystem.hpp"
+#include "lnav.hh"
+#include "vtab_module.hh"
+
+const char* const STATIC_FILE_CREATE_STMT = R"(
+-- Access static files in the lnav configuration directories
+CREATE TABLE lnav_static_files (
+ name TEXT PRIMARY KEY,
+ filepath TEXT,
+ content BLOB HIDDEN
+);
+)";
+
+struct static_file_vtab {
+ sqlite3_vtab base;
+ sqlite3* db;
+};
+
+struct static_file_info {
+ ghc::filesystem::path sfi_path;
+};
+
+struct sf_vtab_cursor {
+ sqlite3_vtab_cursor base;
+ std::map<std::string, static_file_info>::iterator vc_files_iter;
+ std::map<std::string, static_file_info> vc_files;
+};
+
+static int sfvt_destructor(sqlite3_vtab* p_svt);
+
+static int
+sfvt_create(sqlite3* db,
+ void* pAux,
+ int argc,
+ const char* const* argv,
+ sqlite3_vtab** pp_vt,
+ char** pzErr)
+{
+ static_file_vtab* p_vt;
+
+ /* Allocate the sqlite3_vtab/vtab structure itself */
+ p_vt = (static_file_vtab*) sqlite3_malloc(sizeof(*p_vt));
+
+ if (p_vt == nullptr) {
+ return SQLITE_NOMEM;
+ }
+
+ memset(&p_vt->base, 0, sizeof(sqlite3_vtab));
+ p_vt->db = db;
+
+ *pp_vt = &p_vt->base;
+
+ int rc = sqlite3_declare_vtab(db, STATIC_FILE_CREATE_STMT);
+
+ return rc;
+}
+
+static int
+sfvt_destructor(sqlite3_vtab* p_svt)
+{
+ static_file_vtab* p_vt = (static_file_vtab*) p_svt;
+
+ /* Free the SQLite structure */
+ sqlite3_free(p_vt);
+
+ return SQLITE_OK;
+}
+
+static int
+sfvt_connect(sqlite3* db,
+ void* p_aux,
+ int argc,
+ const char* const* argv,
+ sqlite3_vtab** pp_vt,
+ char** pzErr)
+{
+ return sfvt_create(db, p_aux, argc, argv, pp_vt, pzErr);
+}
+
+static int
+sfvt_disconnect(sqlite3_vtab* pVtab)
+{
+ return sfvt_destructor(pVtab);
+}
+
+static int
+sfvt_destroy(sqlite3_vtab* p_vt)
+{
+ return sfvt_destructor(p_vt);
+}
+
+static int sfvt_next(sqlite3_vtab_cursor* cur);
+
+static void
+find_static_files(sf_vtab_cursor* p_cur, const ghc::filesystem::path& dir)
+{
+ auto& file_map = p_cur->vc_files;
+ std::error_code ec;
+
+ for (const auto& format_dir_entry :
+ ghc::filesystem::directory_iterator(dir, ec))
+ {
+ if (!format_dir_entry.is_directory()) {
+ continue;
+ }
+ auto format_static_files_dir = format_dir_entry.path() / "static-files";
+ log_debug("format static files: %s", format_static_files_dir.c_str());
+ for (const auto& static_file_entry :
+ ghc::filesystem::recursive_directory_iterator(
+ format_static_files_dir, ec))
+ {
+ auto rel_path = ghc::filesystem::relative(static_file_entry.path(),
+ format_static_files_dir);
+
+ file_map[rel_path.string()] = {static_file_entry.path()};
+ }
+ }
+}
+
+static int
+sfvt_open(sqlite3_vtab* p_svt, sqlite3_vtab_cursor** pp_cursor)
+{
+ static_file_vtab* p_vt = (static_file_vtab*) p_svt;
+
+ p_vt->base.zErrMsg = NULL;
+
+ sf_vtab_cursor* p_cur = (sf_vtab_cursor*) new sf_vtab_cursor();
+
+ if (p_cur == nullptr) {
+ return SQLITE_NOMEM;
+ }
+
+ *pp_cursor = (sqlite3_vtab_cursor*) p_cur;
+
+ p_cur->base.pVtab = p_svt;
+
+ for (const auto& config_path : lnav_data.ld_config_paths) {
+ auto formats_root = config_path / "formats";
+ log_debug("format root: %s", formats_root.c_str());
+ find_static_files(p_cur, formats_root);
+ auto configs_root = config_path / "configs";
+ log_debug("configs root: %s", configs_root.c_str());
+ find_static_files(p_cur, configs_root);
+ }
+
+ return SQLITE_OK;
+}
+
+static int
+sfvt_close(sqlite3_vtab_cursor* cur)
+{
+ sf_vtab_cursor* p_cur = (sf_vtab_cursor*) cur;
+
+ p_cur->vc_files_iter = p_cur->vc_files.end();
+ /* Free cursor struct. */
+ delete p_cur;
+
+ return SQLITE_OK;
+}
+
+static int
+sfvt_eof(sqlite3_vtab_cursor* cur)
+{
+ sf_vtab_cursor* vc = (sf_vtab_cursor*) cur;
+
+ return vc->vc_files_iter == vc->vc_files.end();
+}
+
+static int
+sfvt_next(sqlite3_vtab_cursor* cur)
+{
+ sf_vtab_cursor* vc = (sf_vtab_cursor*) cur;
+
+ if (vc->vc_files_iter != vc->vc_files.end()) {
+ ++vc->vc_files_iter;
+ }
+
+ return SQLITE_OK;
+}
+
+static int
+sfvt_column(sqlite3_vtab_cursor* cur, sqlite3_context* ctx, int col)
+{
+ sf_vtab_cursor* vc = (sf_vtab_cursor*) cur;
+
+ switch (col) {
+ case 0:
+ to_sqlite(ctx, vc->vc_files_iter->first);
+ break;
+ case 1: {
+ sqlite3_result_text(ctx,
+ vc->vc_files_iter->second.sfi_path.c_str(),
+ -1,
+ SQLITE_TRANSIENT);
+ break;
+ }
+ case 2: {
+ auto read_res = lnav::filesystem::read_file(
+ vc->vc_files_iter->second.sfi_path);
+ if (read_res.isErr()) {
+ auto um = lnav::console::user_message::error(
+ "unable to read static file")
+ .with_reason(read_res.unwrapErr());
+
+ to_sqlite(ctx, um);
+ } else {
+ auto str = read_res.unwrap();
+
+ sqlite3_result_blob(
+ ctx, str.c_str(), str.size(), SQLITE_TRANSIENT);
+ }
+ break;
+ }
+ }
+
+ return SQLITE_OK;
+}
+
+static int
+sfvt_rowid(sqlite3_vtab_cursor* cur, sqlite_int64* p_rowid)
+{
+ sf_vtab_cursor* p_cur = (sf_vtab_cursor*) cur;
+
+ *p_rowid = std::distance(p_cur->vc_files.begin(), p_cur->vc_files_iter);
+
+ return SQLITE_OK;
+}
+
+static int
+sfvt_best_index(sqlite3_vtab* tab, sqlite3_index_info* p_info)
+{
+ return SQLITE_OK;
+}
+
+static int
+sfvt_filter(sqlite3_vtab_cursor* cur,
+ int idxNum,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv)
+{
+ sf_vtab_cursor* p_cur = (sf_vtab_cursor*) cur;
+
+ p_cur->vc_files_iter = p_cur->vc_files.begin();
+ return SQLITE_OK;
+}
+
+static sqlite3_module static_file_vtab_module = {
+ 0, /* iVersion */
+ sfvt_create, /* xCreate - create a vtable */
+ sfvt_connect, /* xConnect - associate a vtable with a connection */
+ sfvt_best_index, /* xBestIndex - best index */
+ sfvt_disconnect, /* xDisconnect - disassociate a vtable with a connection
+ */
+ sfvt_destroy, /* xDestroy - destroy a vtable */
+ sfvt_open, /* xOpen - open a cursor */
+ sfvt_close, /* xClose - close a cursor */
+ sfvt_filter, /* xFilter - configure scan constraints */
+ sfvt_next, /* xNext - advance a cursor */
+ sfvt_eof, /* xEof - inidicate end of result set*/
+ sfvt_column, /* xColumn - read data */
+ sfvt_rowid, /* xRowid - read data */
+ nullptr, /* xUpdate - write data */
+ nullptr, /* xBegin - begin transaction */
+ nullptr, /* xSync - sync transaction */
+ nullptr, /* xCommit - commit transaction */
+ nullptr, /* xRollback - rollback transaction */
+ nullptr, /* xFindFunction - function overloading */
+};
+
+int
+register_static_file_vtab(sqlite3* db)
+{
+ auto_mem<char, sqlite3_free> errmsg;
+ int rc;
+ rc = sqlite3_create_module(
+ db, "lnav_static_file_vtab_impl", &static_file_vtab_module, nullptr);
+ ensure(rc == SQLITE_OK);
+ if ((rc = sqlite3_exec(db,
+ "CREATE VIRTUAL TABLE lnav_static_files USING "
+ "lnav_static_file_vtab_impl()",
+ nullptr,
+ nullptr,
+ errmsg.out()))
+ != SQLITE_OK)
+ {
+ fprintf(stderr,
+ "unable to create lnav_static_file table %s\n",
+ errmsg.in());
+ }
+ return rc;
+}
diff --git a/src/static_file_vtab.hh b/src/static_file_vtab.hh
new file mode 100644
index 0000000..b96ea5a
--- /dev/null
+++ b/src/static_file_vtab.hh
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef static_file_vtab_hh
+#define static_file_vtab_hh
+
+#include <sqlite3.h>
+
+int register_static_file_vtab(sqlite3* db);
+
+extern const char* const STATIC_FILE_CREATE_STMT;
+
+#endif
diff --git a/src/statusview_curses.cc b/src/statusview_curses.cc
new file mode 100644
index 0000000..f27bff0
--- /dev/null
+++ b/src/statusview_curses.cc
@@ -0,0 +1,240 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file statusview_curses.cc
+ */
+
+#include <algorithm>
+#include <vector>
+
+#include "statusview_curses.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "config.h"
+
+void
+status_field::set_value(std::string value)
+{
+ auto& sa = this->sf_value.get_attrs();
+
+ sa.clear();
+
+ scrub_ansi_string(value, &sa);
+ this->sf_value.with_string(value);
+}
+
+void
+status_field::do_cylon()
+{
+ auto& sa = this->sf_value.get_attrs();
+
+ remove_string_attr(sa, &VC_STYLE);
+
+ auto cycle_pos = (this->sf_cylon_pos % (4 + this->sf_width * 2)) - 2;
+ auto start = cycle_pos < this->sf_width
+ ? cycle_pos
+ : (this->sf_width - (cycle_pos - this->sf_width) - 1);
+ auto stop = std::min(start + 3, this->sf_width);
+ struct line_range lr(std::max<long>(start, 0L), stop);
+ auto& vc = view_colors::singleton();
+
+ auto attrs = vc.attrs_for_role(role_t::VCR_ACTIVE_STATUS);
+ attrs.ta_attrs |= A_REVERSE;
+ sa.emplace_back(lr, VC_STYLE.value(attrs));
+
+ this->sf_cylon_pos += 1;
+}
+
+void
+status_field::set_stitch_value(role_t left, role_t right)
+{
+ auto& sa = this->sf_value.get_attrs();
+ struct line_range lr(0, 1);
+
+ this->sf_value.get_string() = "::";
+ sa.clear();
+ sa.emplace_back(lr, VC_ROLE.value(left));
+ lr.lr_start = 1;
+ lr.lr_end = 2;
+ sa.emplace_back(lr, VC_ROLE.value(right));
+}
+
+void
+statusview_curses::do_update()
+{
+ int top, field, field_count, left = 0, right;
+ auto& vc = view_colors::singleton();
+ unsigned long width, height;
+
+ if (!this->vc_visible || this->sc_window == nullptr) {
+ return;
+ }
+
+ getmaxyx(this->sc_window, height, width);
+ this->window_change();
+
+ top = this->sc_top < 0 ? height + this->sc_top : this->sc_top;
+ right = width;
+ auto attrs = vc.attrs_for_role(
+ this->sc_enabled ? this->sc_default_role : role_t::VCR_INACTIVE_STATUS);
+
+ auto pair = vc.ensure_color_pair(attrs.ta_fg_color, attrs.ta_bg_color);
+ wattr_set(this->sc_window, attrs.ta_attrs, pair, nullptr);
+ wmove(this->sc_window, top, 0);
+ wclrtoeol(this->sc_window);
+ whline(this->sc_window, ' ', width);
+
+ if (this->sc_source != nullptr) {
+ field_count = this->sc_source->statusview_fields();
+ for (field = 0; field < field_count; field++) {
+ auto& sf = this->sc_source->statusview_value_for_field(field);
+ struct line_range lr(0, sf.get_width());
+ int x;
+
+ if (sf.is_cylon()) {
+ sf.do_cylon();
+ }
+ auto val = sf.get_value();
+ if (!this->sc_enabled) {
+ for (auto& sa : val.get_attrs()) {
+ if (sa.sa_type == &VC_STYLE) {
+ auto sa_attrs = sa.sa_value.get<text_attrs>();
+ sa_attrs.ta_attrs &= ~(A_REVERSE | A_COLOR);
+ sa_attrs.ta_fg_color = nonstd::nullopt;
+ sa_attrs.ta_bg_color = nonstd::nullopt;
+ sa.sa_value = sa_attrs;
+ } else if (sa.sa_type == &VC_ROLE) {
+ if (sa.sa_value.get<role_t>()
+ == role_t::VCR_ALERT_STATUS)
+ {
+ sa.sa_value.get<role_t>()
+ = role_t::VCR_INACTIVE_ALERT_STATUS;
+ } else {
+ sa.sa_value = role_t::VCR_NONE;
+ }
+ }
+ }
+ }
+ if (sf.get_left_pad() > 0) {
+ val.insert(0, sf.get_left_pad(), ' ');
+ }
+
+ if (sf.is_right_justified()) {
+ val.right_justify(sf.get_width());
+
+ right -= sf.get_width();
+ x = right;
+ } else {
+ x = left;
+ left += sf.get_width();
+ }
+
+ if (val.length() > sf.get_width()) {
+ static const std::string ELLIPSIS = "\xE2\x8B\xAF";
+
+ if (sf.get_width() > 11) {
+ size_t half_width = sf.get_width() / 2 - 1;
+
+ val.erase(half_width, val.length() - (half_width * 2));
+ val.insert(half_width, ELLIPSIS);
+ } else {
+ val = val.subline(0, sf.get_width() - 1);
+ val.append(ELLIPSIS);
+ }
+ }
+
+ auto default_role = sf.get_role();
+ if (!this->sc_enabled) {
+ if (default_role == role_t::VCR_ALERT_STATUS) {
+ default_role = role_t::VCR_INACTIVE_ALERT_STATUS;
+ } else if (default_role != role_t::VCR_STATUS_INFO) {
+ default_role = role_t::VCR_INACTIVE_STATUS;
+ }
+ }
+
+ mvwattrline(this->sc_window, top, x, val, lr, default_role);
+ }
+ }
+ wmove(this->sc_window, top + 1, 0);
+}
+
+void
+statusview_curses::window_change()
+{
+ if (this->sc_source == nullptr) {
+ return;
+ }
+
+ int field_count = this->sc_source->statusview_fields();
+ int total_shares = 0;
+ unsigned long width, height;
+ double remaining = 0;
+ std::vector<status_field*> resizable;
+
+ getmaxyx(this->sc_window, height, width);
+ // Silence the compiler. Remove this if height is used at a later stage.
+ (void) height;
+ remaining = width - 2;
+
+ for (int field = 0; field < field_count; field++) {
+ auto& sf = this->sc_source->statusview_value_for_field(field);
+
+ remaining -= sf.get_share() ? sf.get_min_width() : sf.get_width();
+ total_shares += sf.get_share();
+ if (sf.get_share()) {
+ resizable.emplace_back(&sf);
+ }
+ }
+
+ if (remaining < 2) {
+ remaining = 0;
+ }
+
+ std::stable_sort(begin(resizable), end(resizable), [](auto l, auto r) {
+ return r->get_share() < l->get_share();
+ });
+ for (auto* sf : resizable) {
+ double divisor = total_shares / sf->get_share();
+ int available = remaining / divisor;
+ int actual_width;
+
+ if ((sf->get_left_pad() + sf->get_value().length())
+ < (sf->get_min_width() + available))
+ {
+ actual_width = std::max(
+ (int) sf->get_min_width(),
+ (int) (sf->get_left_pad() + sf->get_value().length()));
+ } else {
+ actual_width = sf->get_min_width() + available;
+ }
+ remaining -= (actual_width - sf->get_min_width());
+ total_shares -= sf->get_share();
+
+ sf->set_width(actual_width);
+ }
+}
diff --git a/src/statusview_curses.hh b/src/statusview_curses.hh
new file mode 100644
index 0000000..c9c5916
--- /dev/null
+++ b/src/statusview_curses.hh
@@ -0,0 +1,189 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file statusview_curses.hh
+ */
+
+#ifndef statusview_curses_hh
+#define statusview_curses_hh
+
+#include <string>
+#include <vector>
+
+#include "view_curses.hh"
+
+/**
+ * Container for individual status values.
+ */
+class status_field {
+public:
+ /**
+ * @param width The maximum width of the field in characters.
+ * @param role The color role for this field, defaults to VCR_STATUS.
+ */
+ status_field(int width = 1, role_t role = role_t::VCR_STATUS)
+ : sf_width(width), sf_role(role)
+ {
+ }
+
+ virtual ~status_field() = default;
+
+ /** @param value The new value for this field. */
+ void set_value(std::string value);
+
+ /**
+ * Set the new value for this field using a formatted string.
+ *
+ * @param fmt The format string.
+ * @param ... Arguments for the format.
+ */
+ status_field& set_value(const char* fmt, ...)
+ {
+ char buffer[256];
+ va_list args;
+
+ va_start(args, fmt);
+ vsnprintf(buffer, sizeof(buffer), fmt, args);
+ this->set_value(std::string(buffer));
+ va_end(args);
+
+ return *this;
+ }
+
+ void set_stitch_value(role_t left, role_t right);
+
+ void set_left_pad(size_t val) { this->sf_left_pad = val; }
+
+ size_t get_left_pad() const { return this->sf_left_pad; }
+
+ /** @return The string value for this field. */
+ attr_line_t& get_value() { return this->sf_value; }
+
+ void right_justify(bool yes) { this->sf_right_justify = yes; }
+ bool is_right_justified() const { return this->sf_right_justify; }
+
+ status_field& set_cylon(bool yes)
+ {
+ this->sf_cylon = yes;
+ return *this;
+ }
+
+ bool is_cylon() const { return this->sf_cylon; }
+
+ void do_cylon();
+
+ /** @return True if this field's value is an empty string. */
+ bool empty() const { return this->sf_value.get_string().empty(); }
+
+ void clear() { this->sf_value.clear(); }
+
+ /** @param role The color role for this field. */
+ void set_role(role_t role) { this->sf_role = role; }
+ /** @return The color role for this field. */
+ role_t get_role() const { return this->sf_role; }
+
+ /** @param width The maximum display width, in characters. */
+ void set_width(ssize_t width) { this->sf_width = width; }
+ /** @param width The maximum display width, in characters. */
+ ssize_t get_width() const { return this->sf_width; }
+
+ /** @param width The maximum display width, in characters. */
+ void set_min_width(int width) { this->sf_min_width = width; }
+ /** @param width The maximum display width, in characters. */
+ size_t get_min_width() const { return this->sf_min_width; }
+
+ void set_share(int share) { this->sf_share = share; }
+
+ int get_share() const { return this->sf_share; }
+
+protected:
+ ssize_t sf_width; /*< The maximum display width, in chars. */
+ ssize_t sf_min_width{0}; /*< The minimum display width, in chars. */
+ bool sf_right_justify{false};
+ bool sf_cylon{false};
+ ssize_t sf_cylon_pos{0};
+ attr_line_t sf_value; /*< The value to display for this field. */
+ role_t sf_role; /*< The color role for this field. */
+ int sf_share{0};
+ size_t sf_left_pad{0};
+};
+
+/**
+ * Data source for the fields to be displayed in a status view.
+ */
+class status_data_source {
+public:
+ virtual ~status_data_source() = default;
+
+ /**
+ * @return The number of status_fields in this source.
+ */
+ virtual size_t statusview_fields() = 0;
+
+ /**
+ * Callback used to get a particular field.
+ *
+ * @param field The index of the field to return.
+ * @return A reference to the field at the given index.
+ */
+ virtual status_field& statusview_value_for_field(int field) = 0;
+};
+
+/**
+ * A view that displays a collection of fields in a line on the display.
+ */
+class statusview_curses : public view_curses {
+public:
+ void set_data_source(status_data_source* src) { this->sc_source = src; }
+ status_data_source* get_data_source() { return this->sc_source; }
+
+ void set_top(int top) { this->sc_top = top; }
+ int get_top() const { return this->sc_top; }
+
+ void set_window(WINDOW* win) { this->sc_window = win; }
+ WINDOW* get_window() { return this->sc_window; }
+
+ void set_enabled(bool value) { this->sc_enabled = value; }
+ bool get_enabled() const { return this->sc_enabled; }
+
+ void set_default_role(role_t role) { this->sc_default_role = role; }
+ role_t get_default_role() const { return this->sc_default_role; }
+
+ void window_change();
+
+ void do_update() override;
+
+private:
+ status_data_source* sc_source{nullptr};
+ WINDOW* sc_window{nullptr};
+ int sc_top{0};
+ bool sc_enabled{true};
+ role_t sc_default_role{role_t::VCR_STATUS};
+};
+
+#endif
diff --git a/src/string-extension-functions.cc b/src/string-extension-functions.cc
new file mode 100644
index 0000000..fccc7a9
--- /dev/null
+++ b/src/string-extension-functions.cc
@@ -0,0 +1,1251 @@
+/*
+ * Written by Alexey Tourbin <at@altlinux.org>.
+ *
+ * The author has dedicated the code to the public domain. Anyone is free
+ * to copy, modify, publish, use, compile, sell, or distribute the original
+ * code, either in source code form or as a compiled binary, for any purpose,
+ * commercial or non-commercial, and by any means.
+ */
+
+#ifdef __CYGWIN__
+# include <alloca.h>
+#endif
+
+#include <unordered_map>
+
+#include <sqlite3.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/humanize.hh"
+#include "base/lnav.gzip.hh"
+#include "base/string_util.hh"
+#include "column_namer.hh"
+#include "config.h"
+#include "data_parser.hh"
+#include "data_scanner.hh"
+#include "elem_to_json.hh"
+#include "formats/logfmt/logfmt.parser.hh"
+#include "libbase64.h"
+#include "mapbox/variant.hpp"
+#include "optional.hpp"
+#include "pcrepp/pcre2pp.hh"
+#include "safe/safe.h"
+#include "scn/scn.h"
+#include "spookyhash/SpookyV2.h"
+#include "sqlite-extension-func.hh"
+#include "text_anonymizer.hh"
+#include "vtab_module.hh"
+#include "vtab_module_json.hh"
+#include "yajl/api/yajl_gen.h"
+#include "yajlpp/json_op.hh"
+#include "yajlpp/yajlpp.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+#if defined(HAVE_LIBCURL)
+# include <curl/curl.h>
+#endif
+
+using namespace mapbox;
+
+struct cache_entry {
+ std::shared_ptr<lnav::pcre2pp::code> re2;
+ std::shared_ptr<column_namer> cn{
+ std::make_shared<column_namer>(column_namer::language::JSON)};
+};
+
+static cache_entry*
+find_re(string_fragment re)
+{
+ using re_cache_t
+ = std::unordered_map<string_fragment, cache_entry, frag_hasher>;
+ static thread_local re_cache_t cache;
+
+ auto iter = cache.find(re);
+ if (iter == cache.end()) {
+ auto compile_res = lnav::pcre2pp::code::from(re);
+ if (compile_res.isErr()) {
+ const static intern_string_t SRC = intern_string::lookup("arg");
+
+ throw lnav::console::to_user_message(SRC, compile_res.unwrapErr());
+ }
+
+ cache_entry c;
+
+ c.re2 = compile_res.unwrap().to_shared();
+ auto pair = cache.insert(
+ std::make_pair(string_fragment::from_str(c.re2->get_pattern()), c));
+
+ for (size_t lpc = 0; lpc < c.re2->get_capture_count(); lpc++) {
+ c.cn->add_column(string_fragment::from_c_str(
+ c.re2->get_name_for_capture(lpc + 1)));
+ }
+
+ iter = pair.first;
+ }
+
+ return &iter->second;
+}
+
+static bool
+regexp(string_fragment re, string_fragment str)
+{
+ auto* reobj = find_re(re);
+
+ return reobj->re2->find_in(str).ignore_error().has_value();
+}
+
+static util::variant<int64_t, double, const char*, string_fragment, json_string>
+regexp_match(string_fragment re, string_fragment str)
+{
+ auto* reobj = find_re(re);
+ auto& extractor = *reobj->re2;
+
+ if (extractor.get_capture_count() == 0) {
+ throw std::runtime_error(
+ "regular expression does not have any captures");
+ }
+
+ auto md = extractor.create_match_data();
+ auto match_res = extractor.capture_from(str).into(md).matches();
+ if (match_res.is<lnav::pcre2pp::matcher::not_found>()) {
+ return static_cast<const char*>(nullptr);
+ }
+ if (match_res.is<lnav::pcre2pp::matcher::error>()) {
+ auto err = match_res.get<lnav::pcre2pp::matcher::error>();
+
+ throw std::runtime_error(err.get_message());
+ }
+
+ yajlpp_gen gen;
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ if (extractor.get_capture_count() == 1) {
+ auto cap = md[1];
+
+ if (!cap) {
+ return static_cast<const char*>(nullptr);
+ }
+
+ auto scan_int_res = scn::scan_value<int64_t>(cap->to_string_view());
+ if (scan_int_res && scan_int_res.empty()) {
+ return scan_int_res.value();
+ }
+
+ auto scan_float_res = scn::scan_value<double>(cap->to_string_view());
+ if (scan_float_res && scan_float_res.empty()) {
+ return scan_float_res.value();
+ }
+
+ return cap.value();
+ } else {
+ yajlpp_map root_map(gen);
+
+ for (size_t lpc = 0; lpc < extractor.get_capture_count(); lpc++) {
+ const auto& colname = reobj->cn->cn_names[lpc];
+ const auto cap = md[lpc + 1];
+
+ yajl_gen_pstring(gen, colname.data(), colname.length());
+
+ if (!cap) {
+ yajl_gen_null(gen);
+ } else {
+ auto scan_int_res
+ = scn::scan_value<int64_t>(cap->to_string_view());
+ if (scan_int_res && scan_int_res.empty()) {
+ yajl_gen_integer(gen, scan_int_res.value());
+ } else {
+ auto scan_float_res
+ = scn::scan_value<double>(cap->to_string_view());
+ if (scan_float_res && scan_float_res.empty()) {
+ yajl_gen_number(gen, cap->data(), cap->length());
+ } else {
+ yajl_gen_pstring(gen, cap->data(), cap->length());
+ }
+ }
+ }
+ }
+ }
+
+ return json_string(gen);
+#if 0
+ sqlite3_result_text(ctx, (const char *) buf, len, SQLITE_TRANSIENT);
+# ifdef HAVE_SQLITE3_VALUE_SUBTYPE
+ sqlite3_result_subtype(ctx, JSON_SUBTYPE);
+# endif
+#endif
+}
+
+json_string
+extract(const char* str)
+{
+ data_scanner ds(str);
+ data_parser dp(&ds);
+
+ dp.parse();
+ // dp.print(stderr, dp.dp_pairs);
+
+ yajlpp_gen gen;
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ elements_to_json(gen, dp, &dp.dp_pairs);
+
+ return json_string(gen);
+}
+
+json_string
+logfmt2json(string_fragment line)
+{
+ logfmt::parser p(line);
+ yajlpp_gen gen;
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_map root(gen);
+ bool done = false;
+
+ while (!done) {
+ auto pair = p.step();
+
+ done = pair.match(
+ [](const logfmt::parser::end_of_input& eoi) { return true; },
+ [&root, &gen](const logfmt::parser::kvpair& kvp) {
+ root.gen(kvp.first);
+
+ kvp.second.match(
+ [&root](const logfmt::parser::bool_value& bv) {
+ root.gen(bv.bv_value);
+ },
+ [&root](const logfmt::parser::int_value& iv) {
+ root.gen(iv.iv_value);
+ },
+ [&root](const logfmt::parser::float_value& fv) {
+ root.gen(fv.fv_value);
+ },
+ [&root, &gen](const logfmt::parser::quoted_value& qv) {
+ auto_mem<yajl_handle_t> parse_handle(yajl_free);
+ json_ptr jp("");
+ json_op jo(jp);
+
+ jo.jo_ptr_callbacks = json_op::gen_callbacks;
+ jo.jo_ptr_data = gen;
+ parse_handle.reset(yajl_alloc(
+ &json_op::ptr_callbacks, nullptr, &jo));
+
+ const auto* json_in
+ = (const unsigned char*) qv.qv_value.data();
+ auto json_len = qv.qv_value.length();
+
+ if (yajl_parse(parse_handle.in(), json_in, json_len)
+ != yajl_status_ok
+ || yajl_complete_parse(parse_handle.in())
+ != yajl_status_ok)
+ {
+ root.gen(qv.qv_value);
+ }
+ },
+ [&root](const logfmt::parser::unquoted_value& uv) {
+ root.gen(uv.uv_value);
+ });
+
+ return false;
+ },
+ [](const logfmt::parser::error& e) -> bool {
+ throw sqlite_func_error("Invalid logfmt: {}", e.e_msg);
+ });
+ }
+ }
+
+ return json_string(gen);
+}
+
+static std::string
+regexp_replace(string_fragment str, string_fragment re, const char* repl)
+{
+ auto* reobj = find_re(re);
+
+ return reobj->re2->replace(str, repl);
+}
+
+static std::string
+spooky_hash(const std::vector<const char*>& args)
+{
+ byte_array<2, uint64> hash;
+ SpookyHash context;
+
+ context.Init(0, 0);
+ for (const auto* const arg : args) {
+ int64_t len = arg != nullptr ? strlen(arg) : 0;
+
+ context.Update(&len, sizeof(len));
+ if (arg == nullptr) {
+ continue;
+ }
+ context.Update(arg, len);
+ }
+ context.Final(hash.out(0), hash.out(1));
+
+ return hash.to_string();
+}
+
+static void
+sql_spooky_hash_step(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ auto* hasher
+ = (SpookyHash*) sqlite3_aggregate_context(context, sizeof(SpookyHash));
+
+ for (int lpc = 0; lpc < argc; lpc++) {
+ const auto* value = sqlite3_value_text(argv[lpc]);
+ int64_t len = value != nullptr ? strlen((const char*) value) : 0;
+
+ hasher->Update(&len, sizeof(len));
+ if (value == nullptr) {
+ continue;
+ }
+ hasher->Update(value, len);
+ }
+}
+
+static void
+sql_spooky_hash_final(sqlite3_context* context)
+{
+ auto* hasher
+ = (SpookyHash*) sqlite3_aggregate_context(context, sizeof(SpookyHash));
+
+ if (hasher == nullptr) {
+ sqlite3_result_null(context);
+ } else {
+ byte_array<2, uint64> hash;
+
+ hasher->Final(hash.out(0), hash.out(1));
+
+ auto hex = hash.to_string();
+ sqlite3_result_text(
+ context, hex.c_str(), hex.length(), SQLITE_TRANSIENT);
+ }
+}
+
+struct sparkline_context {
+ bool sc_initialized{true};
+ double sc_max_value{0.0};
+ std::vector<double> sc_values;
+};
+
+static void
+sparkline_step(sqlite3_context* context, int argc, sqlite3_value** argv)
+{
+ auto* sc = (sparkline_context*) sqlite3_aggregate_context(
+ context, sizeof(sparkline_context));
+
+ if (!sc->sc_initialized) {
+ new (sc) sparkline_context;
+ }
+
+ if (argc == 0) {
+ return;
+ }
+
+ sc->sc_values.push_back(sqlite3_value_double(argv[0]));
+ sc->sc_max_value = std::max(sc->sc_max_value, sc->sc_values.back());
+
+ if (argc >= 2) {
+ sc->sc_max_value
+ = std::max(sc->sc_max_value, sqlite3_value_double(argv[1]));
+ }
+}
+
+static void
+sparkline_final(sqlite3_context* context)
+{
+ auto* sc = (sparkline_context*) sqlite3_aggregate_context(
+ context, sizeof(sparkline_context));
+
+ if (!sc->sc_initialized) {
+ sqlite3_result_text(context, "", 0, SQLITE_STATIC);
+ return;
+ }
+
+ auto* retval = (char*) malloc(sc->sc_values.size() * 3 + 1);
+ auto* start = retval;
+
+ for (const auto& value : sc->sc_values) {
+ auto bar = humanize::sparkline(value, sc->sc_max_value);
+
+ strcpy(start, bar.c_str());
+ start += bar.length();
+ }
+ *start = '\0';
+
+ sqlite3_result_text(context, retval, -1, free);
+
+ sc->~sparkline_context();
+}
+
+nonstd::optional<util::variant<blob_auto_buffer, sqlite3_int64, double>>
+sql_gunzip(sqlite3_value* val)
+{
+ switch (sqlite3_value_type(val)) {
+ case SQLITE3_TEXT:
+ case SQLITE_BLOB: {
+ const auto* buffer = sqlite3_value_blob(val);
+ auto len = sqlite3_value_bytes(val);
+
+ if (!lnav::gzip::is_gzipped((const char*) buffer, len)) {
+ return blob_auto_buffer{
+ auto_buffer::from((const char*) buffer, len)};
+ }
+
+ auto res = lnav::gzip::uncompress("", buffer, len);
+
+ if (res.isErr()) {
+ throw sqlite_func_error("unable to uncompress -- {}",
+ res.unwrapErr());
+ }
+
+ return blob_auto_buffer{res.unwrap()};
+ }
+ case SQLITE_INTEGER:
+ return sqlite3_value_int64(val);
+ case SQLITE_FLOAT:
+ return sqlite3_value_double(val);
+ }
+
+ return nonstd::nullopt;
+}
+
+nonstd::optional<blob_auto_buffer>
+sql_gzip(sqlite3_value* val)
+{
+ switch (sqlite3_value_type(val)) {
+ case SQLITE3_TEXT:
+ case SQLITE_BLOB: {
+ const auto* buffer = sqlite3_value_blob(val);
+ auto len = sqlite3_value_bytes(val);
+ auto res = lnav::gzip::compress(buffer, len);
+
+ if (res.isErr()) {
+ throw sqlite_func_error("unable to compress -- {}",
+ res.unwrapErr());
+ }
+
+ return blob_auto_buffer{res.unwrap()};
+ }
+ case SQLITE_INTEGER:
+ case SQLITE_FLOAT: {
+ const auto* buffer = sqlite3_value_text(val);
+ auto res
+ = lnav::gzip::compress(buffer, strlen((const char*) buffer));
+
+ if (res.isErr()) {
+ throw sqlite_func_error("unable to compress -- {}",
+ res.unwrapErr());
+ }
+
+ return blob_auto_buffer{res.unwrap()};
+ }
+ }
+
+ return nonstd::nullopt;
+}
+
+enum class encode_algo {
+ base64,
+ hex,
+ uri,
+};
+
+template<>
+struct from_sqlite<encode_algo> {
+ inline encode_algo operator()(int argc, sqlite3_value** val, int argi)
+ {
+ const char* algo_name = (const char*) sqlite3_value_text(val[argi]);
+
+ if (strcasecmp(algo_name, "base64") == 0) {
+ return encode_algo::base64;
+ }
+ if (strcasecmp(algo_name, "hex") == 0) {
+ return encode_algo::hex;
+ }
+ if (strcasecmp(algo_name, "uri") == 0) {
+ return encode_algo::uri;
+ }
+
+ throw from_sqlite_conversion_error("value of 'base64', 'hex', or 'uri'",
+ argi);
+ }
+};
+
+#if defined(HAVE_LIBCURL)
+static CURL*
+get_curl_easy()
+{
+ static struct curl_wrapper {
+ curl_wrapper() { this->cw_value = curl_easy_init(); }
+
+ auto_mem<CURL> cw_value{curl_easy_cleanup};
+ } retval;
+
+ return retval.cw_value.in();
+}
+#endif
+
+static mapbox::util::variant<text_auto_buffer, auto_mem<char>, null_value_t>
+sql_encode(sqlite3_value* value, encode_algo algo)
+{
+ switch (sqlite3_value_type(value)) {
+ case SQLITE_NULL: {
+ return null_value_t{};
+ }
+ case SQLITE_BLOB: {
+ const auto* blob
+ = static_cast<const char*>(sqlite3_value_blob(value));
+ auto blob_len = sqlite3_value_bytes(value);
+
+ switch (algo) {
+ case encode_algo::base64: {
+ auto buf = auto_buffer::alloc((blob_len * 5) / 3);
+ auto outlen = buf.capacity();
+
+ base64_encode(blob, blob_len, buf.in(), &outlen, 0);
+ buf.resize(outlen);
+ return text_auto_buffer{std::move(buf)};
+ }
+ case encode_algo::hex: {
+ auto buf = auto_buffer::alloc(blob_len * 2 + 1);
+
+ for (int lpc = 0; lpc < blob_len; lpc++) {
+ fmt::format_to(std::back_inserter(buf),
+ FMT_STRING("{:x}"),
+ blob[lpc]);
+ }
+
+ return text_auto_buffer{std::move(buf)};
+ }
+#if defined(HAVE_LIBCURL)
+ case encode_algo::uri: {
+ auto_mem<char> retval(curl_free);
+
+ retval = curl_easy_escape(get_curl_easy(), blob, blob_len);
+ return std::move(retval);
+ }
+#endif
+ }
+ }
+ default: {
+ const auto* text = (const char*) sqlite3_value_text(value);
+ auto text_len = sqlite3_value_bytes(value);
+
+ switch (algo) {
+ case encode_algo::base64: {
+ auto buf = auto_buffer::alloc((text_len * 5) / 3);
+ size_t outlen = buf.capacity();
+
+ base64_encode(text, text_len, buf.in(), &outlen, 0);
+ buf.resize(outlen);
+ return text_auto_buffer{std::move(buf)};
+ }
+ case encode_algo::hex: {
+ auto buf = auto_buffer::alloc(text_len * 2 + 1);
+
+ for (int lpc = 0; lpc < text_len; lpc++) {
+ fmt::format_to(std::back_inserter(buf),
+ FMT_STRING("{:x}"),
+ text[lpc]);
+ }
+
+ return text_auto_buffer{std::move(buf)};
+ }
+#if defined(HAVE_LIBCURL)
+ case encode_algo::uri: {
+ auto_mem<char> retval(curl_free);
+
+ retval = curl_easy_escape(get_curl_easy(), text, text_len);
+ return std::move(retval);
+ }
+#endif
+ }
+ }
+ }
+ ensure(false);
+}
+
+static mapbox::util::variant<blob_auto_buffer, auto_mem<char>>
+sql_decode(string_fragment str, encode_algo algo)
+{
+ switch (algo) {
+ case encode_algo::base64: {
+ auto buf = auto_buffer::alloc(str.length());
+ auto outlen = buf.capacity();
+ base64_decode(str.data(), str.length(), buf.in(), &outlen, 0);
+ buf.resize(outlen);
+
+ return blob_auto_buffer{std::move(buf)};
+ }
+ case encode_algo::hex: {
+ auto buf = auto_buffer::alloc(str.length() / 2);
+ auto sv = str.to_string_view();
+
+ while (!sv.empty()) {
+ int32_t value;
+ auto scan_res = scn::scan(sv, "{:2x}", value);
+ if (!scan_res) {
+ throw sqlite_func_error(
+ "invalid hex input at: {}",
+ std::distance(str.begin(), sv.begin()));
+ }
+ buf.push_back((char) (value & 0xff));
+ sv = scan_res.range_as_string_view();
+ }
+
+ return blob_auto_buffer{std::move(buf)};
+ }
+#if defined(HAVE_LIBCURL)
+ case encode_algo::uri: {
+ auto_mem<char> retval(curl_free);
+
+ retval = curl_easy_unescape(
+ get_curl_easy(), str.data(), str.length(), nullptr);
+
+ return std::move(retval);
+ }
+#endif
+ }
+ ensure(false);
+}
+
+std::string
+sql_humanize_file_size(file_ssize_t value)
+{
+ return humanize::file_size(value, humanize::alignment::columnar);
+}
+
+static std::string
+sql_anonymize(string_fragment frag)
+{
+ static safe::Safe<lnav::text_anonymizer> ta;
+
+ return ta.writeAccess()->next(frag);
+}
+
+#if !CURL_AT_LEAST_VERSION(7, 80, 0)
+extern "C"
+{
+const char* curl_url_strerror(CURLUcode error);
+}
+#endif
+
+static json_string
+sql_parse_url(string_fragment url_frag)
+{
+ static auto* CURL_HANDLE = get_curl_easy();
+
+ auto_mem<CURLU> cu(curl_url_cleanup);
+ cu = curl_url();
+
+ auto rc = curl_url_set(cu, CURLUPART_URL, url_frag.data(), 0);
+ if (rc != CURLUE_OK) {
+ throw lnav::console::user_message::error(
+ attr_line_t("invalid URL: ").append_quoted(url_frag.to_string()))
+ .with_reason(curl_url_strerror(rc));
+ }
+
+ auto_mem<char> url_part(curl_free);
+ yajlpp_gen gen;
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_map root(gen);
+
+ root.gen("scheme");
+ rc = curl_url_get(cu, CURLUPART_SCHEME, url_part.out(), 0);
+ if (rc == CURLUE_OK) {
+ root.gen(string_fragment::from_c_str(url_part.in()));
+ } else {
+ root.gen();
+ }
+ root.gen("user");
+ rc = curl_url_get(cu, CURLUPART_USER, url_part.out(), CURLU_URLDECODE);
+ if (rc == CURLUE_OK) {
+ root.gen(string_fragment::from_c_str(url_part.in()));
+ } else {
+ root.gen();
+ }
+ root.gen("password");
+ rc = curl_url_get(
+ cu, CURLUPART_PASSWORD, url_part.out(), CURLU_URLDECODE);
+ if (rc == CURLUE_OK) {
+ root.gen(string_fragment::from_c_str(url_part.in()));
+ } else {
+ root.gen();
+ }
+ root.gen("host");
+ rc = curl_url_get(cu, CURLUPART_HOST, url_part.out(), CURLU_URLDECODE);
+ if (rc == CURLUE_OK) {
+ root.gen(string_fragment::from_c_str(url_part.in()));
+ } else {
+ root.gen();
+ }
+ root.gen("port");
+ rc = curl_url_get(cu, CURLUPART_PORT, url_part.out(), 0);
+ if (rc == CURLUE_OK) {
+ root.gen(string_fragment::from_c_str(url_part.in()));
+ } else {
+ root.gen();
+ }
+ root.gen("path");
+ rc = curl_url_get(cu, CURLUPART_PATH, url_part.out(), CURLU_URLDECODE);
+ if (rc == CURLUE_OK) {
+ root.gen(string_fragment::from_c_str(url_part.in()));
+ } else {
+ root.gen();
+ }
+ rc = curl_url_get(cu, CURLUPART_QUERY, url_part.out(), 0);
+ if (rc == CURLUE_OK) {
+ root.gen("query");
+ root.gen(string_fragment::from_c_str(url_part.in()));
+
+ root.gen("parameters");
+ robin_hood::unordered_set<std::string> seen_keys;
+ yajlpp_map query_map(gen);
+
+ auto query_frag = string_fragment::from_c_str(url_part.in());
+ auto remaining = query_frag;
+
+ while (true) {
+ auto split_res
+ = remaining.split_when(string_fragment::tag1{'&'});
+
+ if (!split_res) {
+ break;
+ }
+
+ auto_mem<char> kv_pair(curl_free);
+ auto kv_pair_encoded = split_res->first;
+ int out_len = 0;
+
+ kv_pair = curl_easy_unescape(CURL_HANDLE,
+ kv_pair_encoded.data(),
+ kv_pair_encoded.length(),
+ &out_len);
+ auto kv_pair_frag
+ = string_fragment::from_bytes(kv_pair.in(), out_len);
+ auto eq_index_opt = kv_pair_frag.find('=');
+ if (eq_index_opt) {
+ auto key = kv_pair_frag.sub_range(0, eq_index_opt.value());
+ auto val = kv_pair_frag.substr(eq_index_opt.value() + 1);
+ auto key_str = key.to_string();
+
+ if (seen_keys.count(key_str) == 0) {
+ seen_keys.emplace(key_str);
+ query_map.gen(key);
+ query_map.gen(val);
+ }
+ } else {
+ auto val_str = split_res->first.to_string();
+
+ if (seen_keys.count(val_str) == 0) {
+ seen_keys.insert(val_str);
+ query_map.gen(split_res->first);
+ query_map.gen();
+ }
+ }
+
+ if (split_res->second.empty()) {
+ break;
+ }
+
+ remaining = split_res->second;
+ }
+ } else {
+ root.gen("query");
+ root.gen();
+ root.gen("parameters");
+ root.gen();
+ }
+ root.gen("fragment");
+ rc = curl_url_get(
+ cu, CURLUPART_FRAGMENT, url_part.out(), CURLU_URLDECODE);
+ if (rc == CURLUE_OK) {
+ root.gen(string_fragment::from_c_str(url_part.in()));
+ } else {
+ root.gen();
+ }
+ }
+
+ return json_string(gen);
+}
+
+struct url_parts {
+ nonstd::optional<std::string> up_scheme;
+ nonstd::optional<std::string> up_username;
+ nonstd::optional<std::string> up_password;
+ nonstd::optional<std::string> up_host;
+ nonstd::optional<std::string> up_port;
+ nonstd::optional<std::string> up_path;
+ nonstd::optional<std::string> up_query;
+ std::map<std::string, nonstd::optional<std::string>> up_parameters;
+ nonstd::optional<std::string> up_fragment;
+};
+
+static const json_path_container url_params_handlers = {
+ yajlpp::pattern_property_handler("(?<param>.+)")
+ .for_field(&url_parts::up_parameters),
+};
+
+static const typed_json_path_container<url_parts> url_parts_handlers = {
+ yajlpp::property_handler("scheme").for_field(&url_parts::up_scheme),
+ yajlpp::property_handler("username").for_field(&url_parts::up_username),
+ yajlpp::property_handler("password").for_field(&url_parts::up_password),
+ yajlpp::property_handler("host").for_field(&url_parts::up_host),
+ yajlpp::property_handler("port").for_field(&url_parts::up_port),
+ yajlpp::property_handler("path").for_field(&url_parts::up_path),
+ yajlpp::property_handler("query").for_field(&url_parts::up_query),
+ yajlpp::property_handler("parameters").with_children(url_params_handlers),
+ yajlpp::property_handler("fragment").for_field(&url_parts::up_fragment),
+};
+
+static auto_mem<char>
+sql_unparse_url(string_fragment in)
+{
+ static auto* CURL_HANDLE = get_curl_easy();
+ static intern_string_t SRC = intern_string::lookup("arg");
+
+ auto parse_res = url_parts_handlers.parser_for(SRC).of(in);
+ if (parse_res.isErr()) {
+ throw parse_res.unwrapErr();
+ }
+
+ auto up = parse_res.unwrap();
+ auto_mem<CURLU> cu(curl_url_cleanup);
+ cu = curl_url();
+
+ if (up.up_scheme) {
+ curl_url_set(
+ cu, CURLUPART_SCHEME, up.up_scheme->c_str(), CURLU_URLENCODE);
+ }
+ if (up.up_username) {
+ curl_url_set(
+ cu, CURLUPART_USER, up.up_username->c_str(), CURLU_URLENCODE);
+ }
+ if (up.up_password) {
+ curl_url_set(
+ cu, CURLUPART_PASSWORD, up.up_password->c_str(), CURLU_URLENCODE);
+ }
+ if (up.up_host) {
+ curl_url_set(cu, CURLUPART_HOST, up.up_host->c_str(), CURLU_URLENCODE);
+ }
+ if (up.up_port) {
+ curl_url_set(cu, CURLUPART_PORT, up.up_port->c_str(), 0);
+ }
+ if (up.up_path) {
+ curl_url_set(cu, CURLUPART_PATH, up.up_path->c_str(), CURLU_URLENCODE);
+ }
+ if (up.up_query) {
+ curl_url_set(cu, CURLUPART_QUERY, up.up_query->c_str(), 0);
+ } else if (!up.up_parameters.empty()) {
+ for (const auto& pair : up.up_parameters) {
+ auto_mem<char> key(curl_free);
+ auto_mem<char> value(curl_free);
+ std::string qparam;
+
+ key = curl_easy_escape(
+ CURL_HANDLE, pair.first.c_str(), pair.first.length());
+ if (pair.second) {
+ value = curl_easy_escape(
+ CURL_HANDLE, pair.second->c_str(), pair.second->length());
+ qparam = fmt::format(FMT_STRING("{}={}"), key.in(), value.in());
+ } else {
+ qparam = key.in();
+ }
+
+ curl_url_set(
+ cu, CURLUPART_QUERY, qparam.c_str(), CURLU_APPENDQUERY);
+ }
+ }
+ if (up.up_fragment) {
+ curl_url_set(
+ cu, CURLUPART_FRAGMENT, up.up_fragment->c_str(), CURLU_URLENCODE);
+ }
+
+ auto_mem<char> retval(curl_free);
+
+ curl_url_get(cu, CURLUPART_URL, retval.out(), 0);
+ return retval;
+}
+
+int
+string_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs)
+{
+ static struct FuncDef string_funcs[] = {
+ sqlite_func_adapter<decltype(&regexp), regexp>::builder(
+ help_text("regexp", "Test if a string matches a regular expression")
+ .sql_function()
+ .with_parameter({"re", "The regular expression to use"})
+ .with_parameter({
+ "str",
+ "The string to test against the regular expression",
+ })),
+
+ sqlite_func_adapter<decltype(&regexp_match), regexp_match>::builder(
+ help_text("regexp_match",
+ "Match a string against a regular expression and return "
+ "the capture groups as JSON.")
+ .sql_function()
+ .with_parameter({"re", "The regular expression to use"})
+ .with_parameter({
+ "str",
+ "The string to test against the regular expression",
+ })
+ .with_tags({"string", "regex"})
+ .with_example({
+ "To capture the digits from the string '123'",
+ "SELECT regexp_match('(\\d+)', '123')",
+ })
+ .with_example({
+ "To capture a number and word into a JSON object with the "
+ "properties 'col_0' and 'col_1'",
+ "SELECT regexp_match('(\\d+) (\\w+)', '123 four')",
+ })
+ .with_example({
+ "To capture a number and word into a JSON object with the "
+ "named properties 'num' and 'str'",
+ "SELECT regexp_match('(?<num>\\d+) (?<str>\\w+)', '123 "
+ "four')",
+ })),
+
+ sqlite_func_adapter<decltype(&regexp_replace), regexp_replace>::builder(
+ help_text("regexp_replace",
+ "Replace the parts of a string that match a regular "
+ "expression.")
+ .sql_function()
+ .with_parameter(
+ {"str", "The string to perform replacements on"})
+ .with_parameter({"re", "The regular expression to match"})
+ .with_parameter({
+ "repl",
+ "The replacement string. "
+ "You can reference capture groups with a "
+ "backslash followed by the number of the "
+ "group, starting with 1.",
+ })
+ .with_tags({"string", "regex"})
+ .with_example({
+ "To replace the word at the start of the string "
+ "'Hello, World!' with 'Goodbye'",
+ "SELECT regexp_replace('Hello, World!', "
+ "'^(\\w+)', 'Goodbye')",
+ })
+ .with_example({
+ "To wrap alphanumeric words with angle brackets",
+ "SELECT regexp_replace('123 abc', '(\\w+)', '<\\1>')",
+ })),
+
+ sqlite_func_adapter<decltype(&sql_humanize_file_size),
+ sql_humanize_file_size>::
+ builder(help_text(
+ "humanize_file_size",
+ "Format the given file size as a human-friendly string")
+ .sql_function()
+ .with_parameter({"value", "The file size to format"})
+ .with_tags({"string"})
+ .with_example({
+ "To format an amount",
+ "SELECT humanize_file_size(10 * 1024 * 1024)",
+ })),
+
+ sqlite_func_adapter<decltype(&humanize::sparkline),
+ humanize::sparkline>::
+ builder(
+ help_text("sparkline",
+ "Function used to generate a sparkline bar chart. "
+ "The non-aggregate version converts a single numeric "
+ "value on a range to a bar chart character. The "
+ "aggregate version returns a string with a bar "
+ "character for every numeric input")
+ .sql_function()
+ .with_parameter({"value", "The numeric value to convert"})
+ .with_parameter(help_text("upper",
+ "The upper bound of the numeric "
+ "range. The non-aggregate "
+ "version defaults to 100. The "
+ "aggregate version uses the "
+ "largest value in the inputs.")
+ .optional())
+ .with_tags({"string"})
+ .with_example({
+ "To get the unicode block element for the "
+ "value 32 in the "
+ "range of 0-128",
+ "SELECT sparkline(32, 128)",
+ })
+ .with_example({
+ "To chart the values in a JSON array",
+ "SELECT sparkline(value) FROM json_each('[0, 1, 2, 3, "
+ "4, 5, 6, 7, 8]')",
+ })),
+
+ sqlite_func_adapter<decltype(&sql_anonymize), sql_anonymize>::builder(
+ help_text("anonymize",
+ "Replace identifying information with random values.")
+ .sql_function()
+ .with_parameter({"value", "The text to anonymize"})
+ .with_tags({"string"})
+ .with_example({
+ "To anonymize an IP address",
+ "SELECT anonymize('Hello, 192.168.1.2')",
+ })),
+
+ sqlite_func_adapter<decltype(&extract), extract>::builder(
+ help_text("extract",
+ "Automatically Parse and extract data from a string")
+ .sql_function()
+ .with_parameter({"str", "The string to parse"})
+ .with_tags({"string"})
+ .with_example({
+ "To extract key/value pairs from a string",
+ "SELECT extract('foo=1 bar=2 name=\"Rolo Tomassi\"')",
+ })
+ .with_example({
+ "To extract columnar data from a string",
+ "SELECT extract('1.0 abc 2.0')",
+ })),
+
+ sqlite_func_adapter<decltype(&logfmt2json), logfmt2json>::builder(
+ help_text("logfmt2json",
+ "Convert a logfmt-encoded string into JSON")
+ .sql_function()
+ .with_parameter({"str", "The logfmt message to parse"})
+ .with_tags({"string"})
+ .with_example({
+ "To extract key/value pairs from a log message",
+ "SELECT logfmt2json('foo=1 bar=2 name=\"Rolo Tomassi\"')",
+ })),
+
+ sqlite_func_adapter<
+ decltype(static_cast<bool (*)(const char*, const char*)>(
+ &startswith)),
+ startswith>::
+ builder(help_text("startswith",
+ "Test if a string begins with the given prefix")
+ .sql_function()
+ .with_parameter({"str", "The string to test"})
+ .with_parameter(
+ {"prefix", "The prefix to check in the string"})
+ .with_tags({"string"})
+ .with_example({
+ "To test if the string 'foobar' starts with 'foo'",
+ "SELECT startswith('foobar', 'foo')",
+ })
+ .with_example({
+ "To test if the string 'foobar' starts with 'bar'",
+ "SELECT startswith('foobar', 'bar')",
+ })),
+
+ sqlite_func_adapter<decltype(static_cast<bool (*)(
+ const char*, const char*)>(&endswith)),
+ endswith>::
+ builder(
+ help_text("endswith",
+ "Test if a string ends with the given suffix")
+ .sql_function()
+ .with_parameter({"str", "The string to test"})
+ .with_parameter(
+ {"suffix", "The suffix to check in the string"})
+ .with_tags({"string"})
+ .with_example({
+ "To test if the string 'notbad.jpg' ends with '.jpg'",
+ "SELECT endswith('notbad.jpg', '.jpg')",
+ })
+ .with_example({
+ "To test if the string 'notbad.png' starts with '.jpg'",
+ "SELECT endswith('notbad.png', '.jpg')",
+ })),
+
+ sqlite_func_adapter<decltype(&spooky_hash), spooky_hash>::builder(
+ help_text("spooky_hash",
+ "Compute the hash value for the given arguments.")
+ .sql_function()
+ .with_parameter(
+ help_text("str", "The string to hash").one_or_more())
+ .with_tags({"string"})
+ .with_example({
+ "To produce a hash for the string 'Hello, World!'",
+ "SELECT spooky_hash('Hello, World!')",
+ })
+ .with_example({
+ "To produce a hash for the parameters where one is NULL",
+ "SELECT spooky_hash('Hello, World!', NULL)",
+ })
+ .with_example({
+ "To produce a hash for the parameters where one "
+ "is an empty string",
+ "SELECT spooky_hash('Hello, World!', '')",
+ })
+ .with_example({
+ "To produce a hash for the parameters where one "
+ "is a number",
+ "SELECT spooky_hash('Hello, World!', 123)",
+ })),
+
+ sqlite_func_adapter<decltype(&sql_gunzip), sql_gunzip>::builder(
+ help_text("gunzip", "Decompress a gzip file")
+ .sql_function()
+ .with_parameter(
+ help_text("b", "The blob to decompress").one_or_more())
+ .with_tags({"string"})),
+
+ sqlite_func_adapter<decltype(&sql_gzip), sql_gzip>::builder(
+ help_text("gzip", "Compress a string into a gzip file")
+ .sql_function()
+ .with_parameter(
+ help_text("value", "The value to compress").one_or_more())
+ .with_tags({"string"})),
+
+ sqlite_func_adapter<decltype(&sql_encode), sql_encode>::builder(
+ help_text("encode", "Encode the value using the given algorithm")
+ .sql_function()
+ .with_parameter(help_text("value", "The value to encode"))
+ .with_parameter(help_text("algorithm",
+ "One of the following encoding "
+ "algorithms: base64, hex, uri"))
+ .with_tags({"string"})
+ .with_example({
+ "To base64-encode 'Hello, World!'",
+ "SELECT encode('Hello, World!', 'base64')",
+ })
+ .with_example({
+ "To hex-encode 'Hello, World!'",
+ "SELECT encode('Hello, World!', 'hex')",
+ })
+ .with_example({
+ "To URI-encode 'Hello, World!'",
+ "SELECT encode('Hello, World!', 'uri')",
+ })),
+
+ sqlite_func_adapter<decltype(&sql_decode), sql_decode>::builder(
+ help_text("decode", "Decode the value using the given algorithm")
+ .sql_function()
+ .with_parameter(help_text("value", "The value to decode"))
+ .with_parameter(help_text("algorithm",
+ "One of the following encoding "
+ "algorithms: base64, hex, uri"))
+ .with_tags({"string"})
+ .with_example({
+ "To decode the URI-encoded string '%63%75%72%6c'",
+ "SELECT decode('%63%75%72%6c', 'uri')",
+ })),
+
+ sqlite_func_adapter<decltype(&sql_parse_url), sql_parse_url>::builder(
+ help_text("parse_url",
+ "Parse a URL and return the components in a JSON object. "
+ "Limitations: not all URL schemes are supported and "
+ "repeated query parameters are not captured.")
+ .sql_function()
+ .with_parameter(help_text("url", "The URL to parse"))
+ .with_result({
+ "scheme",
+ "The URL's scheme",
+ })
+ .with_result({
+ "username",
+ "The name of the user specified in the URL",
+ })
+ .with_result({
+ "password",
+ "The password specified in the URL",
+ })
+ .with_result({
+ "host",
+ "The host name / IP specified in the URL",
+ })
+ .with_result({
+ "port",
+ "The port specified in the URL",
+ })
+ .with_result({
+ "path",
+ "The path specified in the URL",
+ })
+ .with_result({
+ "query",
+ "The query string in the URL",
+ })
+ .with_result({
+ "parameters",
+ "An object containing the query parameters",
+ })
+ .with_result({
+ "fragment",
+ "The fragment specified in the URL",
+ })
+ .with_tags({"string", "url"})
+ .with_example({
+ "To parse the URL "
+ "'https://example.com/search?q=hello%20world'",
+ "SELECT "
+ "parse_url('https://example.com/search?q=hello%20world')",
+ })
+ .with_example({
+ "To parse the URL "
+ "'https://alice@[fe80::14ff:4ee5:1215:2fb2]'",
+ "SELECT "
+ "parse_url('https://alice@[fe80::14ff:4ee5:1215:2fb2]')",
+ })),
+
+ sqlite_func_adapter<decltype(&sql_unparse_url), sql_unparse_url>::
+ builder(
+ help_text("unparse_url",
+ "Convert a JSON object containing the parts of a "
+ "URL into a URL string")
+ .sql_function()
+ .with_parameter(help_text(
+ "obj", "The JSON object containing the URL parts"))
+ .with_tags({"string", "url"})
+ .with_example({
+ "To unparse the object "
+ "'{\"scheme\": \"https\", \"host\": \"example.com\"}'",
+ "SELECT "
+ "unparse_url('{\"scheme\": \"https\", \"host\": "
+ "\"example.com\"}')",
+ })),
+
+ {nullptr},
+ };
+
+ static struct FuncDefAgg str_agg_funcs[] = {
+ {
+ "group_spooky_hash",
+ -1,
+ 0,
+ sql_spooky_hash_step,
+ sql_spooky_hash_final,
+ help_text("group_spooky_hash",
+ "Compute the hash value for the given arguments")
+ .sql_agg_function()
+ .with_parameter(
+ help_text("str", "The string to hash").one_or_more())
+ .with_tags({"string"})
+ .with_example({
+ "To produce a hash of all of the values of 'column1'",
+ "SELECT group_spooky_hash(column1) FROM (VALUES ('abc'), "
+ "('123'))",
+ }),
+ },
+
+ {
+ "sparkline",
+ -1,
+ 0,
+ sparkline_step,
+ sparkline_final,
+ },
+
+ {nullptr},
+ };
+
+ *basic_funcs = string_funcs;
+ *agg_funcs = str_agg_funcs;
+
+ return SQLITE_OK;
+}
diff --git a/src/strong_int.hh b/src/strong_int.hh
new file mode 100644
index 0000000..af18da9
--- /dev/null
+++ b/src/strong_int.hh
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file strong_int.hh
+ */
+
+#ifndef strong_int_hh
+#define strong_int_hh
+
+/**
+ * Template class for "strongly-typed" integers, in other words, integers that
+ * have different semantic meaning and cannot be easily used in place of one
+ * another.
+ *
+ * @param T The integer type.
+ * @param DISTINCT An class used solely to distinguish templates that have the
+ * same integer type.
+ */
+template<typename T, class DISTINCT>
+class strong_int {
+public:
+ explicit constexpr strong_int(T v = 0) noexcept : value(v){};
+ operator const T&() const
+ {
+ return this->value;
+ };
+ strong_int operator+(const strong_int& rhs) const
+ {
+ return strong_int(this->value + rhs.value);
+ };
+ strong_int operator-(const strong_int& rhs) const
+ {
+ return strong_int(this->value - rhs.value);
+ };
+ strong_int operator/(const strong_int& rhs) const
+ {
+ return strong_int(this->value / rhs.value);
+ };
+ bool operator<(const strong_int& rhs) const
+ {
+ return this->value < rhs.value;
+ };
+ strong_int& operator+=(const strong_int& rhs)
+ {
+ this->value += rhs.value;
+ return *this;
+ };
+ strong_int& operator-=(const strong_int& rhs)
+ {
+ this->value -= rhs.value;
+ return *this;
+ };
+ strong_int& operator-()
+ {
+ this->value = -this->value;
+ return *this;
+ };
+ strong_int& operator++()
+ {
+ this->value++;
+ return *this;
+ };
+ strong_int& operator--()
+ {
+ this->value--;
+ return *this;
+ };
+ bool operator==(const strong_int& rhs) const
+ {
+ return this->value == rhs.value;
+ };
+ T* out()
+ {
+ return &this->value;
+ };
+
+private:
+ T value;
+};
+
+/**
+ * Macro that declares a strongly-typed integer and the empty class used as a
+ * distinguisher.
+ *
+ * @param T The integer type.
+ * @param name The name of the strongly-typed integer.
+ */
+#define STRONG_INT_TYPE(T, name) \
+ class __##name##_distinct; \
+ typedef strong_int<T, __##name##_distinct> name##_t
+#endif
diff --git a/src/styling.cc b/src/styling.cc
new file mode 100644
index 0000000..73e3db4
--- /dev/null
+++ b/src/styling.cc
@@ -0,0 +1,246 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <string>
+
+#include "styling.hh"
+
+#include "ansi-palette-json.h"
+#include "config.h"
+#include "fmt/format.h"
+#include "xterm-palette-json.h"
+#include "yajlpp/yajlpp.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+static const struct json_path_container term_color_rgb_handler = {
+ yajlpp::property_handler("r").for_field(&rgb_color::rc_r),
+ yajlpp::property_handler("g").for_field(&rgb_color::rc_g),
+ yajlpp::property_handler("b").for_field(&rgb_color::rc_b),
+};
+
+static const struct json_path_container term_color_handler = {
+ yajlpp::property_handler("colorId").for_field(&term_color::xc_id),
+ yajlpp::property_handler("name").for_field(&term_color::xc_name),
+ yajlpp::property_handler("hexString").for_field(&term_color::xc_hex),
+ yajlpp::property_handler("rgb")
+ .for_child(&term_color::xc_color)
+ .with_children(term_color_rgb_handler),
+};
+
+static const struct json_path_container root_color_handler = {
+ yajlpp::property_handler("#")
+ .with_obj_provider<term_color, std::vector<term_color>>(
+ [](const yajlpp_provider_context& ypc,
+ std::vector<term_color>* palette) {
+ if (ypc.ypc_index >= palette->size()) {
+ palette->resize(ypc.ypc_index + 1);
+ }
+ return &((*palette)[ypc.ypc_index]);
+ })
+ .with_children(term_color_handler),
+};
+
+term_color_palette*
+xterm_colors()
+{
+ static term_color_palette retval(xterm_palette_json.get_name(),
+ xterm_palette_json.to_string_fragment());
+
+ return &retval;
+}
+
+term_color_palette*
+ansi_colors()
+{
+ static term_color_palette retval(ansi_palette_json.get_name(),
+ ansi_palette_json.to_string_fragment());
+
+ return &retval;
+}
+
+Result<rgb_color, std::string>
+rgb_color::from_str(const string_fragment& sf)
+{
+ if (sf.empty()) {
+ return Ok(rgb_color());
+ }
+
+ rgb_color rgb_out;
+
+ if (sf[0] == '#') {
+ switch (sf.length()) {
+ case 4:
+ if (sscanf(sf.data(),
+ "#%1hx%1hx%1hx",
+ &rgb_out.rc_r,
+ &rgb_out.rc_g,
+ &rgb_out.rc_b)
+ == 3)
+ {
+ rgb_out.rc_r |= rgb_out.rc_r << 4;
+ rgb_out.rc_g |= rgb_out.rc_g << 4;
+ rgb_out.rc_b |= rgb_out.rc_b << 4;
+ return Ok(rgb_out);
+ }
+ break;
+ case 7:
+ if (sscanf(sf.data(),
+ "#%2hx%2hx%2hx",
+ &rgb_out.rc_r,
+ &rgb_out.rc_g,
+ &rgb_out.rc_b)
+ == 3)
+ {
+ return Ok(rgb_out);
+ }
+ break;
+ }
+
+ return Err(fmt::format(FMT_STRING("Could not parse color: {}"), sf));
+ }
+
+ for (const auto& xc : xterm_colors()->tc_palette) {
+ if (sf.iequal(xc.xc_name)) {
+ return Ok(xc.xc_color);
+ }
+ }
+
+ return Err(fmt::format(
+ FMT_STRING(
+ "Unknown color: '{}'. "
+ "See https://jonasjacek.github.io/colors/ for a list of supported "
+ "color names"),
+ sf));
+}
+
+bool
+rgb_color::operator<(const rgb_color& rhs) const
+{
+ if (rc_r < rhs.rc_r)
+ return true;
+ if (rhs.rc_r < rc_r)
+ return false;
+ if (rc_g < rhs.rc_g)
+ return true;
+ if (rhs.rc_g < rc_g)
+ return false;
+ return rc_b < rhs.rc_b;
+}
+
+bool
+rgb_color::operator>(const rgb_color& rhs) const
+{
+ return rhs < *this;
+}
+
+bool
+rgb_color::operator<=(const rgb_color& rhs) const
+{
+ return !(rhs < *this);
+}
+
+bool
+rgb_color::operator>=(const rgb_color& rhs) const
+{
+ return !(*this < rhs);
+}
+
+bool
+rgb_color::operator==(const rgb_color& rhs) const
+{
+ return rc_r == rhs.rc_r && rc_g == rhs.rc_g && rc_b == rhs.rc_b;
+}
+
+bool
+rgb_color::operator!=(const rgb_color& rhs) const
+{
+ return !(rhs == *this);
+}
+
+term_color_palette::term_color_palette(const char* name,
+ const string_fragment& json)
+{
+ yajlpp_parse_context ypc_xterm(intern_string::lookup(name),
+ &root_color_handler);
+ yajl_handle handle;
+
+ handle = yajl_alloc(&ypc_xterm.ypc_callbacks, nullptr, &ypc_xterm);
+ ypc_xterm.with_ignore_unused(true)
+ .with_obj(this->tc_palette)
+ .with_handle(handle);
+ yajl_status st = ypc_xterm.parse(json);
+ ensure(st == yajl_status_ok);
+ st = ypc_xterm.complete_parse();
+ ensure(st == yajl_status_ok);
+ yajl_free(handle);
+
+ for (auto& xc : this->tc_palette) {
+ xc.xc_lab_color = lab_color(xc.xc_color);
+ }
+}
+
+short
+term_color_palette::match_color(const lab_color& to_match)
+{
+ double lowest = 1000.0;
+ short lowest_id = -1;
+
+ for (auto& xc : this->tc_palette) {
+ double xc_delta = xc.xc_lab_color.deltaE(to_match);
+
+ if (lowest_id == -1) {
+ lowest = xc_delta;
+ lowest_id = xc.xc_id;
+ continue;
+ }
+
+ if (xc_delta < lowest) {
+ lowest = xc_delta;
+ lowest_id = xc.xc_id;
+ }
+ }
+
+ return lowest_id;
+}
+
+namespace styling {
+
+Result<color_unit, std::string>
+color_unit::from_str(const string_fragment& sf)
+{
+ if (sf == "semantic()") {
+ return Ok(color_unit{semantic{}});
+ }
+
+ auto retval = TRY(rgb_color::from_str(sf));
+
+ return Ok(color_unit{retval});
+}
+
+} // namespace styling
diff --git a/src/styling.hh b/src/styling.hh
new file mode 100644
index 0000000..001383a
--- /dev/null
+++ b/src/styling.hh
@@ -0,0 +1,230 @@
+/**
+ * Copyright (c) 2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef styling_hh
+#define styling_hh
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/intern_string.hh"
+#include "base/result.h"
+#include "log_level.hh"
+#include "mapbox/variant.hpp"
+#include "yajlpp/yajlpp.hh"
+
+struct rgb_color {
+ static Result<rgb_color, std::string> from_str(const string_fragment& sf);
+
+ explicit rgb_color(short r = -1, short g = -1, short b = -1)
+ : rc_r(r), rc_g(g), rc_b(b)
+ {
+ }
+
+ bool empty() const
+ {
+ return this->rc_r == -1 && this->rc_g == -1 && this->rc_b == -1;
+ }
+
+ bool operator==(const rgb_color& rhs) const;
+
+ bool operator!=(const rgb_color& rhs) const;
+
+ bool operator<(const rgb_color& rhs) const;
+
+ bool operator>(const rgb_color& rhs) const;
+
+ bool operator<=(const rgb_color& rhs) const;
+
+ bool operator>=(const rgb_color& rhs) const;
+
+ short rc_r;
+ short rc_g;
+ short rc_b;
+};
+
+struct lab_color {
+ lab_color() : lc_l(0), lc_a(0), lc_b(0) {}
+
+ explicit lab_color(const rgb_color& rgb);
+
+ double deltaE(const lab_color& other) const;
+
+ lab_color& operator=(const lab_color& other)
+ {
+ this->lc_l = other.lc_l;
+ this->lc_a = other.lc_a;
+ this->lc_b = other.lc_b;
+
+ return *this;
+ }
+
+ bool operator==(const lab_color& rhs) const;
+
+ bool operator!=(const lab_color& rhs) const;
+
+ bool operator<(const lab_color& rhs) const;
+
+ bool operator>(const lab_color& rhs) const;
+
+ bool operator<=(const lab_color& rhs) const;
+
+ bool operator>=(const lab_color& rhs) const;
+
+ double lc_l;
+ double lc_a;
+ double lc_b;
+};
+
+struct term_color {
+ short xc_id;
+ std::string xc_name;
+ std::string xc_hex;
+ rgb_color xc_color;
+ lab_color xc_lab_color;
+};
+
+struct term_color_palette {
+ term_color_palette(const char* name, const string_fragment& json);
+
+ short match_color(const lab_color& to_match);
+
+ std::vector<term_color> tc_palette;
+};
+
+namespace styling {
+
+struct semantic {};
+
+class color_unit {
+public:
+ static Result<color_unit, std::string> from_str(const string_fragment& sf);
+
+ static color_unit make_empty() { return color_unit{rgb_color{}}; }
+
+ bool empty() const
+ {
+ return this->cu_value.match(
+ [](semantic) { return false; },
+ [](const rgb_color& rc) { return rc.empty(); });
+ }
+
+ using variants_t = mapbox::util::variant<semantic, rgb_color>;
+
+ variants_t cu_value;
+
+private:
+ explicit color_unit(variants_t value) : cu_value(std::move(value)) {}
+};
+
+} // namespace styling
+
+struct style_config {
+ std::string sc_color;
+ std::string sc_background_color;
+ bool sc_underline{false};
+ bool sc_bold{false};
+};
+
+struct highlighter_config {
+ std::string hc_regex;
+ style_config hc_style;
+};
+
+struct lnav_theme {
+ std::map<std::string, std::string> lt_vars;
+ positioned_property<style_config> lt_style_identifier;
+ positioned_property<style_config> lt_style_text;
+ positioned_property<style_config> lt_style_alt_text;
+ positioned_property<style_config> lt_style_ok;
+ positioned_property<style_config> lt_style_info;
+ positioned_property<style_config> lt_style_error;
+ positioned_property<style_config> lt_style_warning;
+ positioned_property<style_config> lt_style_popup;
+ positioned_property<style_config> lt_style_focused;
+ positioned_property<style_config> lt_style_disabled_focused;
+ positioned_property<style_config> lt_style_scrollbar;
+ positioned_property<style_config> lt_style_hidden;
+ positioned_property<style_config> lt_style_cursor_line;
+ positioned_property<style_config> lt_style_adjusted_time;
+ positioned_property<style_config> lt_style_skewed_time;
+ positioned_property<style_config> lt_style_offset_time;
+ positioned_property<style_config> lt_style_invalid_msg;
+ positioned_property<style_config> lt_style_status_title;
+ positioned_property<style_config> lt_style_status_title_hotkey;
+ positioned_property<style_config> lt_style_status_disabled_title;
+ positioned_property<style_config> lt_style_status_subtitle;
+ positioned_property<style_config> lt_style_status_info;
+ positioned_property<style_config> lt_style_status_hotkey;
+ positioned_property<style_config> lt_style_quoted_code;
+ positioned_property<style_config> lt_style_code_border;
+ positioned_property<style_config> lt_style_keyword;
+ positioned_property<style_config> lt_style_string;
+ positioned_property<style_config> lt_style_comment;
+ positioned_property<style_config> lt_style_doc_directive;
+ positioned_property<style_config> lt_style_variable;
+ positioned_property<style_config> lt_style_symbol;
+ positioned_property<style_config> lt_style_number;
+ positioned_property<style_config> lt_style_re_special;
+ positioned_property<style_config> lt_style_re_repeat;
+ positioned_property<style_config> lt_style_diff_delete;
+ positioned_property<style_config> lt_style_diff_add;
+ positioned_property<style_config> lt_style_diff_section;
+ positioned_property<style_config> lt_style_low_threshold;
+ positioned_property<style_config> lt_style_med_threshold;
+ positioned_property<style_config> lt_style_high_threshold;
+ positioned_property<style_config> lt_style_status;
+ positioned_property<style_config> lt_style_warn_status;
+ positioned_property<style_config> lt_style_alert_status;
+ positioned_property<style_config> lt_style_active_status;
+ positioned_property<style_config> lt_style_inactive_status;
+ positioned_property<style_config> lt_style_inactive_alert_status;
+ positioned_property<style_config> lt_style_file;
+ positioned_property<style_config> lt_style_header[6];
+ positioned_property<style_config> lt_style_hr;
+ positioned_property<style_config> lt_style_hyperlink;
+ positioned_property<style_config> lt_style_list_glyph;
+ positioned_property<style_config> lt_style_breadcrumb;
+ positioned_property<style_config> lt_style_table_border;
+ positioned_property<style_config> lt_style_table_header;
+ positioned_property<style_config> lt_style_quote_border;
+ positioned_property<style_config> lt_style_quoted_text;
+ positioned_property<style_config> lt_style_footnote_border;
+ positioned_property<style_config> lt_style_footnote_text;
+ positioned_property<style_config> lt_style_snippet_border;
+ std::map<log_level_t, positioned_property<style_config>> lt_level_styles;
+ std::map<std::string, highlighter_config> lt_highlights;
+};
+
+extern term_color_palette* xterm_colors();
+extern term_color_palette* ansi_colors();
+
+#endif
diff --git a/src/sysclip.cc b/src/sysclip.cc
new file mode 100644
index 0000000..725e404
--- /dev/null
+++ b/src/sysclip.cc
@@ -0,0 +1,163 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file sysclip.cc
+ */
+
+#include "sysclip.hh"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include "base/injector.hh"
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "fmt/format.h"
+#include "libbase64.h"
+#include "sysclip.cfg.hh"
+
+#define ANSI_OSC "\x1b]"
+
+namespace sysclip {
+
+static nonstd::optional<clipboard>
+get_commands()
+{
+ const auto& cfg = injector::get<const config&>();
+
+ for (const auto& pair : cfg.c_clipboard_impls) {
+ const auto full_cmd = fmt::format(FMT_STRING("{} > /dev/null 2>&1"),
+ pair.second.c_test_command);
+
+ log_debug("testing clipboard impl %s using: %s",
+ pair.first.c_str(),
+ full_cmd.c_str());
+ if (system(full_cmd.c_str()) == 0) {
+ log_info("detected clipboard: %s", pair.first.c_str());
+ return pair.second;
+ }
+ }
+
+ return nonstd::nullopt;
+}
+
+static int
+osc52_close(FILE* file)
+{
+ static const char ANSI_OSC_COPY_TO_CLIP[] = ANSI_OSC "52;c;";
+
+ log_debug("writing %d bytes of clipboard data using OSC 52", ftell(file));
+ write(STDOUT_FILENO, ANSI_OSC_COPY_TO_CLIP, strlen(ANSI_OSC_COPY_TO_CLIP));
+
+ base64_state b64state{};
+ base64_stream_encode_init(&b64state, 0);
+
+ fseek(file, 0, SEEK_SET);
+
+ auto done = false;
+ while (!done) {
+ char in_buffer[1024];
+ char out_buffer[2048];
+ size_t outlen = 0;
+
+ auto rc = fread(in_buffer, 1, sizeof(in_buffer), file);
+ if (rc <= 0) {
+ base64_stream_encode_final(&b64state, out_buffer, &outlen);
+ write(STDOUT_FILENO, out_buffer, outlen);
+ break;
+ }
+
+ base64_stream_encode(&b64state, in_buffer, rc, out_buffer, &outlen);
+ write(STDOUT_FILENO, out_buffer, outlen);
+ }
+
+ write(STDOUT_FILENO, "\a", 1);
+
+ fclose(file);
+
+ return 0;
+}
+
+/* XXX For one, this code is kinda crappy. For two, we should probably link
+ * directly with X so we don't need to have xclip installed and it'll work if
+ * we're ssh'd into a box.
+ */
+Result<auto_mem<FILE>, std::string>
+open(type_t type, op_t op)
+{
+ const char* mode = op == op_t::WRITE ? "w" : "r";
+ static const auto clip_opt = sysclip::get_commands();
+
+ std::string cmd;
+
+ if (clip_opt) {
+ cmd = clip_opt.value().select(type).select(op);
+ if (cmd.empty()) {
+ log_info("configured clipboard does not support type/op");
+ }
+ } else {
+ log_info("unable to detect clipboard");
+ }
+
+ if (cmd.empty()) {
+ log_info(" ... falling back to OSC 52");
+ auto_mem<FILE> retval(osc52_close);
+
+ retval = tmpfile();
+ if (retval.in() == nullptr) {
+ return Err(
+ fmt::format(FMT_STRING("unable to open temporary file: {}"),
+ strerror(errno)));
+ }
+
+ return Ok(std::move(retval));
+ }
+
+ switch (op) {
+ case op_t::WRITE:
+ cmd = fmt::format(FMT_STRING("{} > /dev/null 2>&1"), cmd);
+ break;
+ case op_t::READ:
+ cmd = fmt::format(FMT_STRING("{} < /dev/null 2>/dev/null"), cmd);
+ break;
+ }
+
+ auto_mem<FILE> retval(pclose);
+
+ log_debug("trying detected clipboard command: %s", cmd.c_str());
+ retval = popen(cmd.c_str(), mode);
+ if (retval.in() == nullptr) {
+ return Err(fmt::format(FMT_STRING("failed to open clipboard: {} -- {}"),
+ cmd,
+ strerror(errno)));
+ }
+
+ return Ok(std::move(retval));
+}
+
+} // namespace sysclip
diff --git a/src/sysclip.cfg.hh b/src/sysclip.cfg.hh
new file mode 100644
index 0000000..238299b
--- /dev/null
+++ b/src/sysclip.cfg.hh
@@ -0,0 +1,84 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file sysclip.cfg.hh
+ */
+
+#ifndef lnav_sysclip_cfg_hh
+#define lnav_sysclip_cfg_hh
+
+#include <map>
+#include <string>
+
+#include "base/lnav_log.hh"
+#include "sysclip.hh"
+
+namespace sysclip {
+
+struct clip_commands {
+ std::string cc_write;
+ std::string cc_read;
+
+ std::string select(op_t op) const
+ {
+ switch (op) {
+ case op_t::WRITE:
+ return this->cc_write;
+ case op_t::READ:
+ return this->cc_read;
+ }
+
+ ensure(false);
+ }
+};
+
+struct clipboard {
+ std::string c_test_command;
+ clip_commands c_general;
+ clip_commands c_find;
+
+ const clip_commands& select(type_t t) const
+ {
+ switch (t) {
+ case type_t::GENERAL:
+ return this->c_general;
+ case type_t::FIND:
+ return this->c_find;
+ }
+
+ ensure(false);
+ }
+};
+
+struct config {
+ std::map<std::string, clipboard> c_clipboard_impls;
+};
+
+} // namespace sysclip
+
+#endif
diff --git a/src/sysclip.hh b/src/sysclip.hh
new file mode 100644
index 0000000..a670c47
--- /dev/null
+++ b/src/sysclip.hh
@@ -0,0 +1,57 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file sysclip.hh
+ */
+
+#ifndef sysclip_hh
+#define sysclip_hh
+
+#include <cstdio>
+#include <string>
+
+#include "base/auto_mem.hh"
+#include "base/result.h"
+
+namespace sysclip {
+
+enum class type_t {
+ GENERAL,
+ FIND,
+};
+
+enum class op_t {
+ WRITE,
+ READ,
+};
+
+Result<auto_mem<FILE>, std::string> open(type_t type, op_t op = op_t::WRITE);
+
+} // namespace sysclip
+
+#endif
diff --git a/src/tailer/CMakeLists.txt b/src/tailer/CMakeLists.txt
new file mode 100644
index 0000000..1d1fb15
--- /dev/null
+++ b/src/tailer/CMakeLists.txt
@@ -0,0 +1,24 @@
+add_library(tailercommon sha-256.c sha-256.h tailer.c tailer.h)
+
+add_executable(tailer tailer.main.c)
+
+target_link_libraries(tailer tailercommon)
+
+add_library(tailerpp tailerpp.hh tailerpp.cc)
+target_link_libraries(tailerpp base)
+
+add_custom_command(
+ OUTPUT tailerbin.h tailerbin.cc
+ COMMAND bin2c -n tailer_bin tailerbin tailer
+ DEPENDS bin2c tailer)
+
+add_library(tailerservice tailer.looper.hh tailer.looper.cc
+ tailer.looper.cfg.hh tailerbin.h tailerbin.cc)
+target_include_directories(tailerservice PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
+target_link_libraries(tailerservice base)
+
+add_executable(drive_tailer drive_tailer.cc)
+
+target_include_directories(drive_tailer PUBLIC . .. ../fmtlib
+ ${CMAKE_CURRENT_BINARY_DIR}/..)
+target_link_libraries(drive_tailer base tailercommon tailerpp ZLIB::ZLIB)
diff --git a/src/tailer/Makefile.am b/src/tailer/Makefile.am
new file mode 100644
index 0000000..bb8a39a
--- /dev/null
+++ b/src/tailer/Makefile.am
@@ -0,0 +1,111 @@
+
+include $(top_srcdir)/aminclude_static.am
+
+TESTS_ENVIRONMENT = $(SHELL) $(top_builddir)/TESTS_ENVIRONMENT
+LOG_COMPILER = $(SHELL) $(top_builddir)/TESTS_ENVIRONMENT
+
+BUILT_SOURCES = tailerbin.cc
+
+AM_CPPFLAGS = \
+ -Wall \
+ $(CODE_COVERAGE_CPPFLAGS) \
+ $(LIBARCHIVE_CFLAGS) \
+ $(READLINE_CFLAGS) \
+ $(SQLITE3_CFLAGS) \
+ $(PCRE_CFLAGS) \
+ $(LIBCURL_CPPFLAGS)
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS)
+
+dist_noinst_DATA = \
+ tailer.ape
+
+noinst_LIBRARIES = \
+ libtailercommon.a \
+ libtailerpp.a \
+ libtailerservice.a
+
+noinst_HEADERS = \
+ sha-256.h \
+ tailer.h \
+ tailer.looper.hh \
+ tailer.looper.cfg.hh \
+ tailerpp.hh
+
+libtailercommon_a_SOURCES = \
+ sha-256.c \
+ tailer.c
+
+libtailerpp_a_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(srcdir)/.. \
+ -I$(srcdir)/../fmtlib \
+ -I$(srcdir)/../third-party \
+ -I$(top_srcdir)/src/third-party/scnlib/include
+
+libtailerpp_a_SOURCES = \
+ tailerpp.cc
+
+tailerbin.cc: tailer.ape ../../tools/bin2c$(BUILD_EXEEXT)
+ ../../tools/bin2c$(BUILD_EXEEXT) -n tailer_bin tailerbin $(srcdir)/tailer.ape
+
+libtailerservice_a_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ -I$(srcdir)/.. \
+ -I$(srcdir)/../fmtlib \
+ -I$(srcdir)/../third-party \
+ -I$(top_srcdir)/src/third-party/scnlib/include
+
+libtailerservice_a_SOURCES = \
+ tailerbin.cc \
+ tailer.looper.cc
+
+check_PROGRAMS = \
+ drive_tailer \
+ tailer
+
+tailer_SOURCES = \
+ tailer.main.c
+
+tailer_LDADD = libtailercommon.a
+
+drive_tailer_CPPFLAGS = \
+ -I$(srcdir)/.. \
+ -I$(srcdir)/../fmtlib \
+ -I$(top_srcdir)/src/third-party/scnlib/include
+
+drive_tailer_SOURCES = \
+ drive_tailer.cc
+
+drive_tailer_LDADD = \
+ libtailercommon.a \
+ libtailerpp.a \
+ ../base/libbase.a \
+ ../fmtlib/libcppfmt.a
+
+dist_noinst_SCRIPTS = \
+ test_tailer.sh
+
+TESTS = \
+ test_tailer.sh
+
+DISTCLEANFILES = \
+ *.cmd \
+ *.dat \
+ *.out \
+ *.err \
+ *.db \
+ *.dpt \
+ *.diff \
+ *.index \
+ *.tmp \
+ *.outbak \
+ *.errbak \
+ *.tmpbak \
+ tailerbin.h \
+ tailerbin.cc
+
+distclean-local:
+ $(RM_V)rm -f foo
diff --git a/src/tailer/README.md b/src/tailer/README.md
new file mode 100644
index 0000000..74660f9
--- /dev/null
+++ b/src/tailer/README.md
@@ -0,0 +1,35 @@
+# Tailer
+
+This directory contains the functionality for monitoring
+[remote files](https://docs.lnav.org/en/latest/usage.html#remote-files). The
+name "tailer" refers to the binary that is transferred to the remote host that
+takes care of tailing files and sending the contents back to the host that is
+running the main lnav binary. To ease integration with lnav's existing
+functionality, the remote files are mirrored locally. The tailer also
+supports interactive use by providing previews of file contents and
+TAB-completion possibilities.
+
+## Files
+
+The important files in this directory are:
+
+- [tailer.main.c](tailer.main.c) - The main() implementation for the tailer.
+- [tailer.looper.hh](tailer.looper.hh) - The service in the main lnav binary
+ that transfers tailers to hosts and communicates with them.
+- [tailer.h](tailer.h) and [tailerpp.hh](tailerpp.hh) - Utility libraries for
+ the tailer protocol.
+- tailer.ape - The [αcτµαlly pδrταblε εxεcµταblε](https://justine.lol/ape.html)
+ build of the tailer. This binary is produced by a GitHub Action and checked
+ in so the build process doesn't need to be supported on lots of platforms.
+
+## Flow
+
+When a remote-path is passed to lnav, the
+[file_collection.hh](../file_collection.hh) logic forwards the request to the
+`tailer::looper` service. This service makes two connections to the remote
+host using the `ssh` command so that the user's custom configurations will be
+used. The first connection is used to transfer the "tailer.ape" binary and
+make it executable. The second connection starts the tailer and uses
+stdin/stdout for a binary protocol and stderr for logging. The tailer then
+waits for requests to open files, preview files, and get possible paths for
+TAB-completions.
diff --git a/src/tailer/drive_tailer.cc b/src/tailer/drive_tailer.cc
new file mode 100644
index 0000000..18ead43
--- /dev/null
+++ b/src/tailer/drive_tailer.cc
@@ -0,0 +1,288 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <thread>
+
+#include <unistd.h>
+
+#include "base/auto_fd.hh"
+#include "base/auto_pid.hh"
+#include "config.h"
+#include "ghc/filesystem.hpp"
+#include "line_buffer.hh"
+#include "tailerpp.hh"
+
+static void
+read_err_pipe(auto_fd& err, std::string& eq)
+{
+ while (true) {
+ char buffer[1024];
+ auto rc = read(err.get(), buffer, sizeof(buffer));
+
+ if (rc <= 0) {
+ break;
+ }
+
+ eq.append(buffer, rc);
+ }
+}
+
+int
+main(int argc, char* const* argv)
+{
+ if (argc != 3) {
+ fprintf(stderr, "usage: %s <cmd> <path>\n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ auto in_pipe_res = auto_pipe::for_child_fd(STDIN_FILENO);
+ if (in_pipe_res.isErr()) {
+ fprintf(stderr,
+ "cannot open stdin pipe for child: %s\n",
+ in_pipe_res.unwrapErr().c_str());
+ exit(EXIT_FAILURE);
+ }
+
+ auto out_pipe_res = auto_pipe::for_child_fd(STDOUT_FILENO);
+ if (out_pipe_res.isErr()) {
+ fprintf(stderr,
+ "cannot open stdout pipe for child: %s\n",
+ out_pipe_res.unwrapErr().c_str());
+ exit(EXIT_FAILURE);
+ }
+
+ auto err_pipe_res = auto_pipe::for_child_fd(STDERR_FILENO);
+ if (err_pipe_res.isErr()) {
+ fprintf(stderr,
+ "cannot open stderr pipe for child: %s\n",
+ err_pipe_res.unwrapErr().c_str());
+ exit(EXIT_FAILURE);
+ }
+
+ auto fork_res = lnav::pid::from_fork();
+ if (fork_res.isErr()) {
+ fprintf(
+ stderr, "cannot start tailer: %s\n", fork_res.unwrapErr().c_str());
+ exit(EXIT_FAILURE);
+ }
+
+ auto in_pipe = in_pipe_res.unwrap();
+ auto out_pipe = out_pipe_res.unwrap();
+ auto err_pipe = err_pipe_res.unwrap();
+ auto child = fork_res.unwrap();
+
+ in_pipe.after_fork(child.in());
+ out_pipe.after_fork(child.in());
+ err_pipe.after_fork(child.in());
+
+ if (child.in_child()) {
+ auto this_exe = ghc::filesystem::path(argv[0]);
+ auto exe_dir = this_exe.parent_path();
+ auto tailer_exe = exe_dir / "tailer";
+
+ execlp(tailer_exe.c_str(), tailer_exe.c_str(), "-k", nullptr);
+ exit(EXIT_FAILURE);
+ }
+
+ std::string error_queue;
+ std::thread err_reader(
+ [err = std::move(err_pipe.read_end()), &error_queue]() mutable {
+ read_err_pipe(err, error_queue);
+ });
+
+ auto& to_child = in_pipe.write_end();
+ auto& from_child = out_pipe.read_end();
+ auto cmd = std::string(argv[1]);
+
+ if (cmd == "open") {
+ send_packet(
+ to_child.get(), TPT_OPEN_PATH, TPPT_STRING, argv[2], TPPT_DONE);
+ } else if (cmd == "preview") {
+ send_packet(to_child.get(),
+ TPT_LOAD_PREVIEW,
+ TPPT_STRING,
+ argv[2],
+ TPPT_INT64,
+ int64_t{1234},
+ TPPT_DONE);
+ } else if (cmd == "possible") {
+ send_packet(
+ to_child.get(), TPT_COMPLETE_PATH, TPPT_STRING, argv[2], TPPT_DONE);
+ } else {
+ fprintf(stderr, "error: unknown command -- %s\n", cmd.c_str());
+ exit(EXIT_FAILURE);
+ }
+
+ close(to_child.get());
+
+ bool done = false;
+ while (!done) {
+ auto read_res = tailer::read_packet(from_child);
+
+ if (read_res.isErr()) {
+ fprintf(stderr, "read error: %s\n", read_res.unwrapErr().c_str());
+ exit(EXIT_FAILURE);
+ }
+
+ auto packet = read_res.unwrap();
+ packet.match(
+ [&](const tailer::packet_eof& te) {
+ printf("all done!\n");
+ done = true;
+ },
+ [&](const tailer::packet_announce& pa) {},
+ [&](const tailer::packet_log& te) {
+ printf("log: %s\n", te.pl_msg.c_str());
+ },
+ [&](const tailer::packet_error& pe) {
+ printf("Got an error: %s -- %s\n",
+ pe.pe_path.c_str(),
+ pe.pe_msg.c_str());
+
+ auto remote_path = ghc::filesystem::absolute(
+ ghc::filesystem::path(pe.pe_path))
+ .relative_path();
+
+ printf("removing %s\n", remote_path.c_str());
+ },
+ [&](const tailer::packet_offer_block& pob) {
+ printf("Got an offer: %s %lld - %lld\n",
+ pob.pob_path.c_str(),
+ pob.pob_offset,
+ pob.pob_length);
+
+ auto remote_path = ghc::filesystem::absolute(
+ ghc::filesystem::path(pob.pob_path))
+ .relative_path();
+#if 0
+ auto local_path = tmppath / remote_path;
+ auto fd = auto_fd(open(local_path.c_str(), O_RDONLY));
+
+ if (fd == -1) {
+ printf("sending need block\n");
+ send_packet(to_child.get(),
+ TPT_NEED_BLOCK,
+ TPPT_STRING, pob.pob_path.c_str(),
+ TPPT_DONE);
+ return;
+ }
+
+ struct stat st;
+
+ if (fstat(fd, &st) == -1 || !S_ISREG(st.st_mode)) {
+ ghc::filesystem::remove_all(local_path);
+ send_packet(to_child.get(),
+ TPT_NEED_BLOCK,
+ TPPT_STRING, pob.pob_path.c_str(),
+ TPPT_DONE);
+ return;
+ }
+ auto_mem<char> buffer;
+
+ buffer = (char *) malloc(pob.pob_length);
+ auto bytes_read = pread(fd, buffer, pob.pob_length,
+ pob.pob_offset);
+
+ // fprintf(stderr, "debug: bytes_read %ld\n", bytes_read);
+ if (bytes_read == pob.pob_length) {
+ tailer::hash_frag thf;
+ calc_sha_256(thf.thf_hash, buffer, bytes_read);
+
+ if (thf == pob.pob_hash) {
+ send_packet(to_child.get(),
+ TPT_ACK_BLOCK,
+ TPPT_STRING, pob.pob_path.c_str(),
+ TPPT_DONE);
+ return;
+ }
+ } else if (bytes_read == -1) {
+ ghc::filesystem::remove_all(local_path);
+ }
+ send_packet(to_child.get(),
+ TPT_NEED_BLOCK,
+ TPPT_STRING, pob.pob_path.c_str(),
+ TPPT_DONE);
+#endif
+ },
+ [&](const tailer::packet_tail_block& ptb) {
+#if 0
+ //printf("got a tail: %s %lld %ld\n", ptb.ptb_path.c_str(),
+ // ptb.ptb_offset, ptb.ptb_bits.size());
+ auto remote_path = ghc::filesystem::absolute(
+ ghc::filesystem::path(ptb.ptb_path)).relative_path();
+ auto local_path = tmppath / remote_path;
+
+ ghc::filesystem::create_directories(local_path.parent_path());
+ auto fd = auto_fd(
+ open(local_path.c_str(), O_WRONLY | O_APPEND | O_CREAT,
+ 0600));
+
+ if (fd == -1) {
+ perror("open");
+ } else {
+ ftruncate(fd, ptb.ptb_offset);
+ pwrite(fd, ptb.ptb_bits.data(), ptb.ptb_bits.size(), ptb.ptb_offset);
+ }
+#endif
+ },
+ [&](const tailer::packet_synced& ps) {
+
+ },
+ [&](const tailer::packet_link& pl) {
+ printf("link value: %s -> %s\n",
+ pl.pl_path.c_str(),
+ pl.pl_link_value.c_str());
+ },
+ [&](const tailer::packet_preview_error& ppe) {
+ fprintf(stderr,
+ "preview error: %s -- %s\n",
+ ppe.ppe_path.c_str(),
+ ppe.ppe_msg.c_str());
+ },
+ [&](const tailer::packet_preview_data& ppd) {
+ printf("preview of file: %s\n%.*s\n",
+ ppd.ppd_path.c_str(),
+ (int) ppd.ppd_bits.size(),
+ ppd.ppd_bits.data());
+ },
+ [&](const tailer::packet_possible_path& ppp) {
+ printf("possible path: %s\n", ppp.ppp_path.c_str());
+ });
+ }
+
+ auto finished_child = std::move(child).wait_for_child();
+ if (!finished_child.was_normal_exit()) {
+ fprintf(stderr, "error: child exited abnormally\n");
+ }
+
+ err_reader.join();
+
+ printf("tailer stderr:\n%s", error_queue.c_str());
+ fprintf(stderr, "tailer stderr:\n%s", error_queue.c_str());
+}
diff --git a/src/tailer/sha-256.c b/src/tailer/sha-256.c
new file mode 100644
index 0000000..8e390f8
--- /dev/null
+++ b/src/tailer/sha-256.c
@@ -0,0 +1,161 @@
+/*********************************************************************
+* Filename: sha256.c
+* Author: Brad Conte (brad AT bradconte.com)
+* Copyright:
+* Disclaimer: This code is presented "as is" without any guarantees.
+* Details: Implementation of the SHA-256 hashing algorithm.
+ SHA-256 is one of the three algorithms in the SHA2
+ specification. The others, SHA-384 and SHA-512, are not
+ offered in this implementation.
+ Algorithm specification can be found here:
+ * http://csrc.nist.gov/publications/fips/fips180-2/fips180-2withchangenotice.pdf
+ This implementation uses little endian byte order.
+*********************************************************************/
+
+/*************************** HEADER FILES ***************************/
+#ifndef __COSMOPOLITAN__
+#include <stdlib.h>
+#include <memory.h>
+#endif
+
+#include "sha-256.h"
+
+/****************************** MACROS ******************************/
+#define ROTLEFT(a,b) (((a) << (b)) | ((a) >> (32-(b))))
+#define ROTRIGHT(a,b) (((a) >> (b)) | ((a) << (32-(b))))
+
+#define CH(x,y,z) (((x) & (y)) ^ (~(x) & (z)))
+#define MAJ(x,y,z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
+#define EP0(x) (ROTRIGHT(x,2) ^ ROTRIGHT(x,13) ^ ROTRIGHT(x,22))
+#define EP1(x) (ROTRIGHT(x,6) ^ ROTRIGHT(x,11) ^ ROTRIGHT(x,25))
+#define SIG0(x) (ROTRIGHT(x,7) ^ ROTRIGHT(x,18) ^ ((x) >> 3))
+#define SIG1(x) (ROTRIGHT(x,17) ^ ROTRIGHT(x,19) ^ ((x) >> 10))
+
+/**************************** VARIABLES *****************************/
+static const WORD k[64] = {
+ 0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
+ 0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
+ 0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
+ 0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
+ 0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
+ 0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
+ 0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
+ 0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
+};
+
+/*********************** FUNCTION DEFINITIONS ***********************/
+void sha256_transform(SHA256_CTX *ctx, const BYTE data[])
+{
+ WORD a, b, c, d, e, f, g, h, i, j, t1, t2, m[64];
+
+ for (i = 0, j = 0; i < 16; ++i, j += 4)
+ m[i] = (data[j] << 24) | (data[j + 1] << 16) | (data[j + 2] << 8) | (data[j + 3]);
+ for ( ; i < 64; ++i)
+ m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16];
+
+ a = ctx->state[0];
+ b = ctx->state[1];
+ c = ctx->state[2];
+ d = ctx->state[3];
+ e = ctx->state[4];
+ f = ctx->state[5];
+ g = ctx->state[6];
+ h = ctx->state[7];
+
+ for (i = 0; i < 64; ++i) {
+ t1 = h + EP1(e) + CH(e,f,g) + k[i] + m[i];
+ t2 = EP0(a) + MAJ(a,b,c);
+ h = g;
+ g = f;
+ f = e;
+ e = d + t1;
+ d = c;
+ c = b;
+ b = a;
+ a = t1 + t2;
+ }
+
+ ctx->state[0] += a;
+ ctx->state[1] += b;
+ ctx->state[2] += c;
+ ctx->state[3] += d;
+ ctx->state[4] += e;
+ ctx->state[5] += f;
+ ctx->state[6] += g;
+ ctx->state[7] += h;
+}
+
+void sha256_init(SHA256_CTX *ctx)
+{
+ ctx->datalen = 0;
+ ctx->bitlen = 0;
+ ctx->state[0] = 0x6a09e667;
+ ctx->state[1] = 0xbb67ae85;
+ ctx->state[2] = 0x3c6ef372;
+ ctx->state[3] = 0xa54ff53a;
+ ctx->state[4] = 0x510e527f;
+ ctx->state[5] = 0x9b05688c;
+ ctx->state[6] = 0x1f83d9ab;
+ ctx->state[7] = 0x5be0cd19;
+}
+
+void sha256_update(SHA256_CTX *ctx, const BYTE data[], size_t len)
+{
+ WORD i;
+
+ for (i = 0; i < len; ++i) {
+ ctx->data[ctx->datalen] = data[i];
+ ctx->datalen++;
+ if (ctx->datalen == 64) {
+ sha256_transform(ctx, ctx->data);
+ ctx->bitlen += 512;
+ ctx->datalen = 0;
+ }
+ }
+}
+
+void sha256_final(SHA256_CTX *ctx, BYTE hash[])
+{
+ WORD i;
+
+ i = ctx->datalen;
+
+ // Pad whatever data is left in the buffer.
+ if (ctx->datalen < 56) {
+ ctx->data[i++] = 0x80;
+ while (i < 56)
+ ctx->data[i++] = 0x00;
+ }
+ else {
+ ctx->data[i++] = 0x80;
+ while (i < 64)
+ ctx->data[i++] = 0x00;
+ sha256_transform(ctx, ctx->data);
+ memset(ctx->data, 0, 56);
+ }
+
+ // Append to the padding the total message's length in bits and transform.
+ ctx->bitlen += ctx->datalen * 8;
+ ctx->data[63] = ctx->bitlen;
+ ctx->data[62] = ctx->bitlen >> 8;
+ ctx->data[61] = ctx->bitlen >> 16;
+ ctx->data[60] = ctx->bitlen >> 24;
+ ctx->data[59] = ctx->bitlen >> 32;
+ ctx->data[58] = ctx->bitlen >> 40;
+ ctx->data[57] = ctx->bitlen >> 48;
+ ctx->data[56] = ctx->bitlen >> 56;
+ sha256_transform(ctx, ctx->data);
+
+ // Since this implementation uses little endian byte ordering and SHA uses big endian,
+ // reverse all the bytes when copying the final state to the output hash.
+ for (i = 0; i < 4; ++i) {
+ hash[i] = (ctx->state[0] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 4] = (ctx->state[1] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 8] = (ctx->state[2] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 12] = (ctx->state[3] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 16] = (ctx->state[4] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 20] = (ctx->state[5] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 24] = (ctx->state[6] >> (24 - i * 8)) & 0x000000ff;
+ hash[i + 28] = (ctx->state[7] >> (24 - i * 8)) & 0x000000ff;
+ }
+}
diff --git a/src/tailer/sha-256.h b/src/tailer/sha-256.h
new file mode 100644
index 0000000..028fcfb
--- /dev/null
+++ b/src/tailer/sha-256.h
@@ -0,0 +1,44 @@
+/*********************************************************************
+* Filename: sha256.h
+* Author: Brad Conte (brad AT bradconte.com)
+* Copyright:
+* Disclaimer: This code is presented "as is" without any guarantees.
+* Details: Defines the API for the corresponding SHA1 implementation.
+*********************************************************************/
+
+#ifndef SHA256_H
+#define SHA256_H
+
+/*************************** HEADER FILES ***************************/
+#ifndef __COSMOPOLITAN__
+#include <stddef.h>
+#endif
+
+/****************************** MACROS ******************************/
+#define SHA256_BLOCK_SIZE 32 // SHA256 outputs a 32 byte digest
+
+/**************************** DATA TYPES ****************************/
+typedef unsigned char BYTE; // 8-bit byte
+typedef unsigned int WORD; // 32-bit word, change to "long" for 16-bit machines
+
+typedef struct {
+ BYTE data[64];
+ WORD datalen;
+ unsigned long long bitlen;
+ WORD state[8];
+} SHA256_CTX;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/*********************** FUNCTION DECLARATIONS **********************/
+void sha256_init(SHA256_CTX *ctx);
+void sha256_update(SHA256_CTX *ctx, const BYTE data[], size_t len);
+void sha256_final(SHA256_CTX *ctx, BYTE hash[]);
+
+#ifdef __cplusplus
+};
+#endif
+
+#endif // SHA256_H \ No newline at end of file
diff --git a/src/tailer/tailer.ape b/src/tailer/tailer.ape
new file mode 100755
index 0000000..69ebc11
--- /dev/null
+++ b/src/tailer/tailer.ape
Binary files differ
diff --git a/src/tailer/tailer.c b/src/tailer/tailer.c
new file mode 100644
index 0000000..c8fbfbf
--- /dev/null
+++ b/src/tailer/tailer.c
@@ -0,0 +1,100 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __COSMOPOLITAN__
+#include <assert.h>
+#include <string.h>
+#include <unistd.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdint.h>
+#endif
+
+#include "sha-256.h"
+#include "tailer.h"
+
+ssize_t send_packet(int fd,
+ tailer_packet_type_t tpt,
+ tailer_packet_payload_type_t payload_type,
+ ...)
+{
+ va_list args;
+ int done = 0;
+
+ va_start(args, payload_type);
+ write(fd, &tpt, sizeof(tpt));
+ do {
+ write(fd, &payload_type, sizeof(payload_type));
+ switch (payload_type) {
+ case TPPT_STRING: {
+ char *str = va_arg(args, char *);
+ uint32_t length = strlen(str);
+
+ write(fd, &length, sizeof(length));
+ write(fd, str, length);
+ break;
+ }
+ case TPPT_HASH: {
+ const char *hash = va_arg(args, const char *);
+
+ write(fd, hash, SHA256_BLOCK_SIZE);
+ break;
+ }
+ case TPPT_INT64: {
+ int64_t i = va_arg(args, int64_t);
+
+ write(fd, &i, sizeof(i));
+ break;
+ }
+ case TPPT_BITS: {
+ int32_t length = va_arg(args, int32_t);
+ const char *bits = va_arg(args, const char *);
+
+ write(fd, &length, sizeof(length));
+ write(fd, bits, length);
+ break;
+ }
+ case TPPT_DONE: {
+ done = 1;
+ break;
+ }
+ default: {
+ assert(0);
+ break;
+ }
+ }
+
+ if (!done) {
+ payload_type = va_arg(args, tailer_packet_payload_type_t);
+ }
+ } while (!done);
+ va_end(args);
+
+ return 0;
+}
diff --git a/src/tailer/tailer.h b/src/tailer/tailer.h
new file mode 100644
index 0000000..b864137
--- /dev/null
+++ b/src/tailer/tailer.h
@@ -0,0 +1,77 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_tailer_h
+#define lnav_tailer_h
+
+#ifndef __COSMOPOLITAN__
+#include <sys/types.h>
+#endif
+
+typedef enum {
+ TPPT_DONE,
+ TPPT_STRING,
+ TPPT_HASH,
+ TPPT_INT64,
+ TPPT_BITS,
+} tailer_packet_payload_type_t;
+
+typedef enum {
+ TPT_ERROR,
+ TPT_OPEN_PATH,
+ TPT_CLOSE_PATH,
+ TPT_OFFER_BLOCK,
+ TPT_NEED_BLOCK,
+ TPT_ACK_BLOCK,
+ TPT_TAIL_BLOCK,
+ TPT_LINK_BLOCK,
+ TPT_SYNCED,
+ TPT_LOG,
+ TPT_LOAD_PREVIEW,
+ TPT_PREVIEW_ERROR,
+ TPT_PREVIEW_DATA,
+ TPT_COMPLETE_PATH,
+ TPT_POSSIBLE_PATH,
+ TPT_ANNOUNCE,
+} tailer_packet_type_t;
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+ssize_t send_packet(int fd,
+ tailer_packet_type_t tpt,
+ tailer_packet_payload_type_t payload_type,
+ ...);
+
+#ifdef __cplusplus
+};
+#endif
+
+#endif
diff --git a/src/tailer/tailer.looper.cc b/src/tailer/tailer.looper.cc
new file mode 100644
index 0000000..82a9fdc
--- /dev/null
+++ b/src/tailer/tailer.looper.cc
@@ -0,0 +1,1192 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <regex>
+
+#include "tailer.looper.hh"
+
+#include "base/fs_util.hh"
+#include "base/humanize.network.hh"
+#include "base/lnav_log.hh"
+#include "base/paths.hh"
+#include "config.h"
+#include "line_buffer.hh"
+#include "lnav.hh"
+#include "lnav.indexing.hh"
+#include "service_tags.hh"
+#include "tailer.h"
+#include "tailer.looper.cfg.hh"
+#include "tailerbin.h"
+#include "tailerpp.hh"
+
+using namespace std::chrono_literals;
+
+static const auto HOST_RETRY_DELAY = 1min;
+
+static void
+read_err_pipe(const std::string& netloc,
+ auto_fd& err,
+ std::vector<std::string>& eq)
+{
+ line_buffer lb;
+ file_range pipe_range;
+ bool done = false;
+
+ log_info("stderr reader started...");
+ lb.set_fd(err);
+ while (!done) {
+ auto load_res = lb.load_next_line(pipe_range);
+
+ if (load_res.isErr()) {
+ done = true;
+ } else {
+ auto li = load_res.unwrap();
+
+ pipe_range = li.li_file_range;
+ if (li.li_file_range.empty()) {
+ done = true;
+ } else {
+ lb.read_range(li.li_file_range).then([netloc, &eq](auto sbr) {
+ auto line_str
+ = string_fragment(sbr.get_data(), 0, sbr.length())
+ .trim("\n");
+ if (eq.size() < 10) {
+ eq.template emplace_back(line_str.to_string());
+ }
+
+ auto level = line_str.startswith("error:")
+ ? lnav_log_level_t::ERROR
+ : line_str.startswith("warning:")
+ ? lnav_log_level_t::WARNING
+ : line_str.startswith("info:")
+ ? lnav_log_level_t::INFO
+ : lnav_log_level_t::DEBUG;
+ log_msg_wrapper(level,
+ "tailer[%s] %.*s",
+ netloc.c_str(),
+ line_str.length(),
+ line_str.data());
+ });
+ }
+ }
+ }
+}
+
+static void
+update_tailer_progress(const std::string& netloc, const std::string& msg)
+{
+ lnav_data.ld_active_files.fc_progress->writeAccess()
+ ->sp_tailers[netloc]
+ .tp_message
+ = msg;
+}
+
+static void
+update_tailer_description(
+ const std::string& netloc,
+ const std::map<std::string, logfile_open_options_base>& desired_paths,
+ const std::string& remote_uname)
+{
+ std::vector<std::string> paths;
+
+ for (const auto& des_pair : desired_paths) {
+ paths.emplace_back(
+ fmt::format(FMT_STRING("{}{}"), netloc, des_pair.first));
+ }
+ isc::to<main_looper&, services::main_t>().send(
+ [netloc, paths, remote_uname](auto& mlooper) {
+ auto& fc = lnav_data.ld_active_files;
+
+ for (const auto& path : paths) {
+ auto iter = fc.fc_other_files.find(path);
+
+ if (iter == fc.fc_other_files.end()) {
+ continue;
+ }
+
+ iter->second.ofd_description = remote_uname;
+ }
+ fc.fc_name_to_errors.erase(netloc);
+ });
+}
+
+void
+tailer::looper::loop_body()
+{
+ auto now = std::chrono::steady_clock::now();
+ std::vector<std::string> to_erase;
+
+ for (auto& qpair : this->l_netlocs_to_paths) {
+ auto& netloc = qpair.first;
+ auto& rpq = qpair.second;
+
+ if (now < rpq.rpq_next_attempt_time) {
+ continue;
+ }
+ if (this->l_remotes.count(netloc) == 0) {
+ auto create_res = host_tailer::for_host(netloc);
+
+ if (create_res.isErr()) {
+ report_error(netloc, create_res.unwrapErr());
+ if (std::any_of(
+ rpq.rpq_new_paths.begin(),
+ rpq.rpq_new_paths.end(),
+ [](const auto& pair) { return !pair.second.loo_tail; }))
+ {
+ rpq.send_synced_to_main(netloc);
+ to_erase.push_back(netloc);
+ } else {
+ rpq.rpq_next_attempt_time = now + HOST_RETRY_DELAY;
+ }
+ continue;
+ }
+
+ auto ht = create_res.unwrap();
+ this->l_remotes[netloc] = ht;
+ this->s_children.add_child_service(ht);
+
+ rpq.rpq_new_paths.insert(rpq.rpq_existing_paths.begin(),
+ rpq.rpq_existing_paths.end());
+ rpq.rpq_existing_paths.clear();
+ }
+
+ if (!rpq.rpq_new_paths.empty()) {
+ log_debug("%s: new paths to monitor -- %s",
+ netloc.c_str(),
+ rpq.rpq_new_paths.begin()->first.c_str());
+ this->l_remotes[netloc]->send(
+ [paths = rpq.rpq_new_paths](auto& ht) {
+ for (const auto& pair : paths) {
+ log_debug("adding path to tailer -- %s",
+ pair.first.c_str());
+ ht.open_remote_path(pair.first, std::move(pair.second));
+ }
+ });
+
+ rpq.rpq_existing_paths.insert(rpq.rpq_new_paths.begin(),
+ rpq.rpq_new_paths.end());
+ rpq.rpq_new_paths.clear();
+ }
+ }
+
+ for (const auto& netloc : to_erase) {
+ this->l_netlocs_to_paths.erase(netloc);
+ }
+}
+
+void
+tailer::looper::add_remote(const network::path& path,
+ logfile_open_options_base options)
+{
+ auto netloc_str = fmt::to_string(path.home());
+ this->l_netlocs_to_paths[netloc_str].rpq_new_paths[path.p_path]
+ = std::move(options);
+}
+
+void
+tailer::looper::load_preview(int64_t id, const network::path& path)
+{
+ auto netloc_str = fmt::to_string(path.home());
+ auto iter = this->l_remotes.find(netloc_str);
+
+ if (iter == this->l_remotes.end()) {
+ auto create_res = host_tailer::for_host(netloc_str);
+
+ if (create_res.isErr()) {
+ auto msg = create_res.unwrapErr();
+ isc::to<main_looper&, services::main_t>().send(
+ [id, msg](auto& mlooper) {
+ if (lnav_data.ld_preview_generation != id) {
+ return;
+ }
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(false)
+ .clear();
+ lnav_data.ld_preview_source.clear();
+ lnav_data.ld_bottom_source.grep_error(msg);
+ });
+ return;
+ }
+
+ auto ht = create_res.unwrap();
+ this->l_remotes[netloc_str] = ht;
+ this->s_children.add_child_service(ht);
+ }
+
+ this->l_remotes[netloc_str]->send([id, file_path = path.p_path](auto& ht) {
+ ht.load_preview(id, file_path);
+ });
+}
+
+void
+tailer::looper::complete_path(const network::path& path)
+{
+ auto netloc_str = fmt::to_string(path.home());
+ auto iter = this->l_remotes.find(netloc_str);
+
+ if (iter == this->l_remotes.end()) {
+ auto create_res = host_tailer::for_host(netloc_str);
+
+ if (create_res.isErr()) {
+ return;
+ }
+
+ auto ht = create_res.unwrap();
+ this->l_remotes[netloc_str] = ht;
+ this->s_children.add_child_service(ht);
+ }
+
+ this->l_remotes[netloc_str]->send(
+ [file_path = path.p_path](auto& ht) { ht.complete_path(file_path); });
+}
+
+static std::vector<std::string>
+create_ssh_args_from_config(const std::string& dest)
+{
+ const auto& cfg = injector::get<const tailer::config&>();
+ std::vector<std::string> retval;
+
+ retval.emplace_back(cfg.c_ssh_cmd);
+ if (!cfg.c_ssh_flags.empty()) {
+ if (startswith(cfg.c_ssh_flags, "-")) {
+ retval.emplace_back(cfg.c_ssh_flags);
+ } else {
+ retval.emplace_back(
+ fmt::format(FMT_STRING("-{}"), cfg.c_ssh_flags));
+ }
+ }
+ for (const auto& pair : cfg.c_ssh_options) {
+ if (pair.second.empty()) {
+ continue;
+ }
+ retval.emplace_back(fmt::format(FMT_STRING("-{}"), pair.first));
+ retval.emplace_back(pair.second);
+ }
+ for (const auto& pair : cfg.c_ssh_config) {
+ if (pair.second.empty()) {
+ continue;
+ }
+ retval.emplace_back(
+ fmt::format(FMT_STRING("-o{}={}"), pair.first, pair.second));
+ }
+ retval.emplace_back(dest);
+
+ return retval;
+}
+
+Result<std::shared_ptr<tailer::looper::host_tailer>, std::string>
+tailer::looper::host_tailer::for_host(const std::string& netloc)
+{
+ log_debug("tailer(%s): transferring tailer to remote", netloc.c_str());
+
+ update_tailer_progress(netloc, "Transferring tailer...");
+
+ auto& cfg = injector::get<const tailer::config&>();
+ auto tailer_bin_name = fmt::format(FMT_STRING("tailer.bin.{}"), getpid());
+
+ auto rp = humanize::network::path::from_str(netloc).value();
+ auto ssh_dest = rp.p_locality.l_hostname;
+ if (rp.p_locality.l_username.has_value()) {
+ ssh_dest = fmt::format(FMT_STRING("{}@{}"),
+ rp.p_locality.l_username.value(),
+ rp.p_locality.l_hostname);
+ }
+
+ {
+ auto in_pipe = TRY(auto_pipe::for_child_fd(STDIN_FILENO));
+ auto out_pipe = TRY(auto_pipe::for_child_fd(STDOUT_FILENO));
+ auto err_pipe = TRY(auto_pipe::for_child_fd(STDERR_FILENO));
+ auto child = TRY(lnav::pid::from_fork());
+
+ in_pipe.after_fork(child.in());
+ out_pipe.after_fork(child.in());
+ err_pipe.after_fork(child.in());
+
+ if (child.in_child()) {
+ auto arg_strs = create_ssh_args_from_config(ssh_dest);
+ std::vector<char*> args;
+
+ arg_strs.emplace_back(
+ fmt::format(cfg.c_transfer_cmd, tailer_bin_name));
+
+ fmt::print(stderr,
+ "tailer({}): executing -- {}\n",
+ netloc,
+ fmt::join(arg_strs, " "));
+ for (const auto& arg : arg_strs) {
+ args.push_back((char*) arg.data());
+ }
+ args.push_back(nullptr);
+
+ execvp(cfg.c_ssh_cmd.c_str(), args.data());
+ _exit(EXIT_FAILURE);
+ }
+
+ std::vector<std::string> error_queue;
+ log_debug("tailer(%s): starting err reader", netloc.c_str());
+ std::thread err_reader([netloc,
+ err = std::move(err_pipe.read_end()),
+ &error_queue]() mutable {
+ log_set_thread_prefix(
+ fmt::format(FMT_STRING("tailer({})"), netloc));
+ read_err_pipe(netloc, err, error_queue);
+ });
+
+ log_debug("tailer(%s): writing to child", netloc.c_str());
+ auto sf = tailer_bin[0].to_string_fragment();
+ ssize_t total_bytes = 0;
+ bool write_failed = false;
+
+ while (total_bytes < sf.length()) {
+ log_debug("attempting to write %d", sf.length() - total_bytes);
+ auto rc = write(
+ in_pipe.write_end(), sf.data(), sf.length() - total_bytes);
+
+ if (rc < 0) {
+ log_error(" tailer(%s): write failed -- %s",
+ netloc.c_str(),
+ strerror(errno));
+ write_failed = true;
+ break;
+ }
+ log_debug(" wrote %d", rc);
+ total_bytes += rc;
+ }
+
+ in_pipe.write_end().reset();
+
+ while (!write_failed) {
+ char buffer[1024];
+
+ auto rc = read(out_pipe.read_end(), buffer, sizeof(buffer));
+ if (rc < 0) {
+ break;
+ }
+ if (rc == 0) {
+ break;
+ }
+ log_debug("tailer(%s): transfer output -- %.*s",
+ netloc.c_str(),
+ rc,
+ buffer);
+ }
+
+ auto finished_child = std::move(child).wait_for_child();
+
+ err_reader.join();
+ if (!finished_child.was_normal_exit()
+ || finished_child.exit_status() != EXIT_SUCCESS)
+ {
+ auto error_msg = error_queue.empty() ? "unknown"
+ : error_queue.back();
+ return Err(fmt::format(FMT_STRING("failed to ssh to host: {}"),
+ error_msg));
+ }
+ }
+
+ update_tailer_progress(netloc, "Starting tailer...");
+
+ auto in_pipe = TRY(auto_pipe::for_child_fd(STDIN_FILENO));
+ auto out_pipe = TRY(auto_pipe::for_child_fd(STDOUT_FILENO));
+ auto err_pipe = TRY(auto_pipe::for_child_fd(STDERR_FILENO));
+ auto child = TRY(lnav::pid::from_fork());
+
+ in_pipe.after_fork(child.in());
+ out_pipe.after_fork(child.in());
+ err_pipe.after_fork(child.in());
+
+ if (child.in_child()) {
+ auto arg_strs = create_ssh_args_from_config(ssh_dest);
+ std::vector<char*> args;
+
+ arg_strs.emplace_back(fmt::format(cfg.c_start_cmd, tailer_bin_name));
+
+ fmt::print(stderr,
+ FMT_STRING("tailer({}): executing -- {}\n"),
+ netloc,
+ fmt::join(arg_strs, " "));
+ for (const auto& arg : arg_strs) {
+ args.push_back((char*) arg.data());
+ }
+ args.push_back(nullptr);
+
+ execvp(cfg.c_ssh_cmd.c_str(), args.data());
+ _exit(EXIT_FAILURE);
+ }
+
+ return Ok(std::make_shared<host_tailer>(netloc,
+ std::move(child),
+ std::move(in_pipe.write_end()),
+ std::move(out_pipe.read_end()),
+ std::move(err_pipe.read_end())));
+}
+
+static ghc::filesystem::path
+remote_cache_path()
+{
+ return lnav::paths::workdir() / "remotes";
+}
+
+ghc::filesystem::path
+tailer::looper::host_tailer::tmp_path()
+{
+ auto local_path = remote_cache_path();
+
+ ghc::filesystem::create_directories(local_path);
+ auto_mem<char> resolved_path;
+
+ resolved_path = realpath(local_path.c_str(), nullptr);
+ if (resolved_path.in() == nullptr) {
+ return local_path;
+ }
+
+ return resolved_path.in();
+}
+
+static std::string
+scrub_netloc(const std::string& netloc)
+{
+ const static std::regex TO_SCRUB(R"([^\w\.\@])");
+
+ return std::regex_replace(netloc, TO_SCRUB, "_");
+}
+
+tailer::looper::host_tailer::host_tailer(const std::string& netloc,
+ auto_pid<process_state::running> child,
+ auto_fd to_child,
+ auto_fd from_child,
+ auto_fd err_from_child)
+ : isc::service<host_tailer>(netloc), ht_netloc(netloc),
+ ht_local_path(tmp_path() / scrub_netloc(netloc)),
+ ht_error_reader([netloc,
+ err = std::move(err_from_child),
+ &eq = this->ht_error_queue]() mutable {
+ read_err_pipe(netloc, err, eq);
+ }),
+ ht_state(connected{
+ std::move(child), std::move(to_child), std::move(from_child), {}})
+{
+}
+
+void
+tailer::looper::host_tailer::open_remote_path(const std::string& path,
+ logfile_open_options_base loo)
+{
+ this->ht_state.match(
+ [&](connected& conn) {
+ conn.c_desired_paths[path] = std::move(loo);
+ send_packet(conn.ht_to_child.get(),
+ TPT_OPEN_PATH,
+ TPPT_STRING,
+ path.c_str(),
+ TPPT_DONE);
+ },
+ [&](const disconnected& d) {
+ log_warning("disconnected from host, cannot tail: %s",
+ path.c_str());
+ },
+ [&](const synced& s) {
+ log_warning("synced with host, not tailing: %s", path.c_str());
+ });
+}
+
+void
+tailer::looper::host_tailer::load_preview(int64_t id, const std::string& path)
+{
+ this->ht_state.match(
+ [&](connected& conn) {
+ send_packet(conn.ht_to_child.get(),
+ TPT_LOAD_PREVIEW,
+ TPPT_STRING,
+ path.c_str(),
+ TPPT_INT64,
+ id,
+ TPPT_DONE);
+ },
+ [&](const disconnected& d) {
+ log_warning("disconnected from host, cannot preview: %s",
+ path.c_str());
+
+ auto msg = fmt::format(FMT_STRING("error: disconnected from {}"),
+ this->ht_netloc);
+ isc::to<main_looper&, services::main_t>().send([=](auto& mlooper) {
+ if (lnav_data.ld_preview_generation != id) {
+ return;
+ }
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(false)
+ .set_value(msg);
+ });
+ },
+ [&](const synced& s) { require(false); });
+}
+
+void
+tailer::looper::host_tailer::complete_path(const std::string& path)
+{
+ this->ht_state.match(
+ [&](connected& conn) {
+ send_packet(conn.ht_to_child.get(),
+ TPT_COMPLETE_PATH,
+ TPPT_STRING,
+ path.c_str(),
+ TPPT_DONE);
+ },
+ [&](const disconnected& d) {
+ log_warning("disconnected from host, cannot preview: %s",
+ path.c_str());
+ },
+ [&](const synced& s) { require(false); });
+}
+
+void
+tailer::looper::host_tailer::loop_body()
+{
+ const static uint64_t TOUCH_FREQ = 10000;
+
+ if (!this->ht_state.is<connected>()) {
+ return;
+ }
+
+ this->ht_cycle_count += 1;
+ if (this->ht_cycle_count % TOUCH_FREQ == 0) {
+ auto now
+ = ghc::filesystem::file_time_type{std::chrono::system_clock::now()};
+ ghc::filesystem::last_write_time(this->ht_local_path, now);
+ }
+
+ auto& conn = this->ht_state.get<connected>();
+
+ pollfd pfds[1];
+
+ pfds[0].fd = conn.ht_from_child.get();
+ pfds[0].events = POLLIN;
+ pfds[0].revents = 0;
+
+ auto ready_count = poll(pfds, 1, 100);
+ if (ready_count > 0) {
+ auto read_res = tailer::read_packet(conn.ht_from_child);
+
+ if (read_res.isErr()) {
+ log_error("read error: %s", read_res.unwrapErr().c_str());
+ return;
+ }
+
+ auto packet = read_res.unwrap();
+ this->ht_state = packet.match(
+ [&](const tailer::packet_eof& te) {
+ log_debug("all done!");
+
+ auto finished_child = std::move(conn).close();
+ if (finished_child.exit_status() != 0
+ && !this->ht_error_queue.empty())
+ {
+ report_error(this->ht_netloc, this->ht_error_queue.back());
+ }
+
+ return state_v{disconnected()};
+ },
+ [&](const tailer::packet_announce& pa) {
+ update_tailer_description(
+ this->ht_netloc, conn.c_desired_paths, pa.pa_uname);
+ this->ht_uname = pa.pa_uname;
+ return std::move(this->ht_state);
+ },
+ [&](const tailer::packet_log& pl) {
+ log_debug("%s\n", pl.pl_msg.c_str());
+ return std::move(this->ht_state);
+ },
+ [&](const tailer::packet_error& pe) {
+ log_debug("Got an error: %s -- %s",
+ pe.pe_path.c_str(),
+ pe.pe_msg.c_str());
+
+ lnav_data.ld_active_files.fc_progress->writeAccess()
+ ->sp_tailers.erase(this->ht_netloc);
+
+ auto desired_iter = conn.c_desired_paths.find(pe.pe_path);
+ if (desired_iter != conn.c_desired_paths.end()) {
+ report_error(this->get_display_path(pe.pe_path), pe.pe_msg);
+ if (!desired_iter->second.loo_tail) {
+ conn.c_desired_paths.erase(desired_iter);
+ }
+ } else {
+ auto child_iter = conn.c_child_paths.find(pe.pe_path);
+
+ if (child_iter != conn.c_child_paths.end()
+ && !child_iter->second.loo_tail)
+ {
+ conn.c_child_paths.erase(child_iter);
+ }
+ }
+
+ auto remote_path = ghc::filesystem::absolute(
+ ghc::filesystem::path(pe.pe_path))
+ .relative_path();
+ auto local_path = this->ht_local_path / remote_path;
+
+ log_debug("removing %s", local_path.c_str());
+ this->ht_active_files.erase(local_path);
+ ghc::filesystem::remove_all(local_path);
+
+ if (conn.c_desired_paths.empty() && conn.c_child_paths.empty())
+ {
+ log_info("tailer(%s): all desired paths synced",
+ this->ht_netloc.c_str());
+ return state_v{synced{}};
+ }
+
+ return std::move(this->ht_state);
+ },
+ [&](const tailer::packet_offer_block& pob) {
+ log_debug("Got an offer: %s %lld - %lld",
+ pob.pob_path.c_str(),
+ pob.pob_offset,
+ pob.pob_length);
+
+ logfile_open_options_base loo;
+ if (pob.pob_path == pob.pob_root_path) {
+ auto root_iter = conn.c_desired_paths.find(pob.pob_path);
+
+ if (root_iter == conn.c_desired_paths.end()) {
+ log_warning("ignoring unknown root: %s",
+ pob.pob_root_path.c_str());
+ return std::move(this->ht_state);
+ }
+
+ loo = root_iter->second;
+ } else {
+ auto child_iter = conn.c_child_paths.find(pob.pob_path);
+ if (child_iter == conn.c_child_paths.end()) {
+ auto root_iter
+ = conn.c_desired_paths.find(pob.pob_root_path);
+
+ if (root_iter == conn.c_desired_paths.end()) {
+ log_warning("ignoring child of unknown root: %s",
+ pob.pob_root_path.c_str());
+ return std::move(this->ht_state);
+ }
+
+ conn.c_child_paths[pob.pob_path]
+ = std::move(root_iter->second);
+ child_iter = conn.c_child_paths.find(pob.pob_path);
+ }
+
+ loo = std::move(child_iter->second);
+ }
+
+ update_tailer_description(
+ this->ht_netloc, conn.c_desired_paths, this->ht_uname);
+
+ auto remote_path = ghc::filesystem::absolute(
+ ghc::filesystem::path(pob.pob_path))
+ .relative_path();
+ auto local_path = this->ht_local_path / remote_path;
+ auto open_res
+ = lnav::filesystem::open_file(local_path, O_RDONLY);
+
+ if (this->ht_active_files.count(local_path) == 0) {
+ this->ht_active_files.insert(local_path);
+
+ auto custom_name = this->get_display_path(pob.pob_path);
+ isc::to<main_looper&, services::main_t>().send(
+ [local_path,
+ custom_name,
+ loo,
+ netloc = this->ht_netloc](auto& mlooper) {
+ auto& active_fc = lnav_data.ld_active_files;
+ auto lpath_str = local_path.string();
+
+ {
+ safe::WriteAccess<safe_scan_progress> sp(
+ *active_fc.fc_progress);
+
+ sp->sp_tailers.erase(netloc);
+ }
+ if (active_fc.fc_file_names.count(lpath_str) > 0) {
+ log_debug("already in fc_file_names");
+ return;
+ }
+ if (active_fc.fc_closed_files.count(custom_name)
+ > 0) {
+ log_debug("in closed");
+ return;
+ }
+
+ file_collection fc;
+
+ fc.fc_file_names[lpath_str]
+ .with_filename(custom_name)
+ .with_source(logfile_name_source::REMOTE)
+ .with_tail(loo.loo_tail)
+ .with_non_utf_visibility(false)
+ .with_visible_size_limit(256 * 1024);
+ update_active_files(fc);
+ });
+ }
+
+ if (open_res.isErr()) {
+ log_debug("file not found (%s), sending need block",
+ open_res.unwrapErr().c_str());
+ send_packet(conn.ht_to_child.get(),
+ TPT_NEED_BLOCK,
+ TPPT_STRING,
+ pob.pob_path.c_str(),
+ TPPT_DONE);
+ return std::move(this->ht_state);
+ }
+
+ auto fd = open_res.unwrap();
+ struct stat st;
+
+ if (fstat(fd, &st) == -1 || !S_ISREG(st.st_mode)) {
+ log_debug("path changed, sending need block");
+ ghc::filesystem::remove_all(local_path);
+ send_packet(conn.ht_to_child.get(),
+ TPT_NEED_BLOCK,
+ TPPT_STRING,
+ pob.pob_path.c_str(),
+ TPPT_DONE);
+ return std::move(this->ht_state);
+ }
+
+ if (st.st_size == pob.pob_offset) {
+ log_debug("local file is synced, sending need block");
+ send_packet(conn.ht_to_child.get(),
+ TPT_NEED_BLOCK,
+ TPPT_STRING,
+ pob.pob_path.c_str(),
+ TPPT_DONE);
+ return std::move(this->ht_state);
+ }
+
+ constexpr int64_t BUFFER_SIZE = 4 * 1024 * 1024;
+ auto_mem<unsigned char> buffer;
+
+ buffer = (unsigned char*) malloc(BUFFER_SIZE);
+ auto remaining = pob.pob_length;
+ auto remaining_offset = pob.pob_offset;
+ tailer::hash_frag thf;
+ SHA256_CTX shactx;
+ sha256_init(&shactx);
+
+ log_debug("checking offer %s[%lld..+%lld]",
+ local_path.c_str(),
+ remaining_offset,
+ remaining);
+ while (remaining > 0) {
+ auto nbytes = std::min(remaining, BUFFER_SIZE);
+ auto bytes_read
+ = pread(fd, buffer, nbytes, remaining_offset);
+ if (bytes_read == -1) {
+ log_debug(
+ "unable to read file, sending need block -- %s",
+ strerror(errno));
+ ghc::filesystem::remove_all(local_path);
+ break;
+ }
+ if (bytes_read == 0) {
+ break;
+ }
+ sha256_update(&shactx, buffer.in(), bytes_read);
+ remaining -= bytes_read;
+ remaining_offset += bytes_read;
+ }
+
+ if (remaining == 0) {
+ sha256_final(&shactx, thf.thf_hash);
+
+ if (thf == pob.pob_hash) {
+ log_debug("local file block is same, sending ack");
+ send_packet(conn.ht_to_child.get(),
+ TPT_ACK_BLOCK,
+ TPPT_STRING,
+ pob.pob_path.c_str(),
+ TPPT_INT64,
+ pob.pob_offset,
+ TPPT_INT64,
+ pob.pob_length,
+ TPPT_INT64,
+ (int64_t) st.st_size,
+ TPPT_DONE);
+ return std::move(this->ht_state);
+ }
+ log_debug("local file is different, sending need block");
+ }
+ send_packet(conn.ht_to_child.get(),
+ TPT_NEED_BLOCK,
+ TPPT_STRING,
+ pob.pob_path.c_str(),
+ TPPT_DONE);
+ return std::move(this->ht_state);
+ },
+ [&](const tailer::packet_tail_block& ptb) {
+ auto remote_path = ghc::filesystem::absolute(
+ ghc::filesystem::path(ptb.ptb_path))
+ .relative_path();
+ auto local_path = this->ht_local_path / remote_path;
+
+ log_debug("writing tail to: %lld/%ld %s",
+ ptb.ptb_offset,
+ ptb.ptb_bits.size(),
+ local_path.c_str());
+ ghc::filesystem::create_directories(local_path.parent_path());
+ auto create_res = lnav::filesystem::create_file(
+ local_path, O_WRONLY | O_APPEND | O_CREAT, 0600);
+
+ if (create_res.isErr()) {
+ log_error("open: %s", create_res.unwrapErr().c_str());
+ } else {
+ auto fd = create_res.unwrap();
+ ftruncate(fd, ptb.ptb_offset);
+ pwrite(fd,
+ ptb.ptb_bits.data(),
+ ptb.ptb_bits.size(),
+ ptb.ptb_offset);
+ auto mtime = ghc::filesystem::file_time_type{
+ std::chrono::seconds{ptb.ptb_mtime}};
+ // XXX This isn't atomic with the write...
+ ghc::filesystem::last_write_time(local_path, mtime);
+ }
+ return std::move(this->ht_state);
+ },
+ [&](const tailer::packet_synced& ps) {
+ if (ps.ps_root_path == ps.ps_path) {
+ auto iter = conn.c_desired_paths.find(ps.ps_path);
+
+ if (iter != conn.c_desired_paths.end()) {
+ if (iter->second.loo_tail) {
+ conn.c_synced_desired_paths.insert(ps.ps_path);
+ } else {
+ log_info("synced desired path: %s",
+ iter->first.c_str());
+ conn.c_desired_paths.erase(iter);
+ }
+ }
+ } else {
+ auto iter = conn.c_child_paths.find(ps.ps_path);
+
+ if (iter != conn.c_child_paths.end()) {
+ if (iter->second.loo_tail) {
+ conn.c_synced_child_paths.insert(ps.ps_path);
+ } else {
+ log_info("synced child path: %s",
+ iter->first.c_str());
+ conn.c_child_paths.erase(iter);
+ }
+ }
+ }
+
+ if (conn.c_desired_paths.empty() && conn.c_child_paths.empty())
+ {
+ log_info("tailer(%s): all desired paths synced",
+ this->ht_netloc.c_str());
+ return state_v{synced{}};
+ } else if (!conn.c_initial_sync_done
+ && conn.c_desired_paths.size()
+ == conn.c_synced_desired_paths.size()
+ && conn.c_child_paths.size()
+ == conn.c_synced_child_paths.size())
+ {
+ log_info("tailer(%s): all desired paths synced",
+ this->ht_netloc.c_str());
+ conn.c_initial_sync_done = true;
+
+ std::set<std::string> synced_files;
+ for (const auto& desired_pair : conn.c_desired_paths) {
+ synced_files.emplace(fmt::format(
+ FMT_STRING("{}{}"), ht_netloc, desired_pair.first));
+ }
+ isc::to<main_looper&, services::main_t>().send(
+ [file_set = std::move(synced_files)](auto& mlooper) {
+ file_collection fc;
+
+ fc.fc_synced_files = file_set;
+ update_active_files(fc);
+ });
+ }
+
+ return std::move(this->ht_state);
+ },
+ [&](const tailer::packet_link& pl) {
+ auto remote_path = ghc::filesystem::absolute(
+ ghc::filesystem::path(pl.pl_path))
+ .relative_path();
+ auto local_path = this->ht_local_path / remote_path;
+ auto remote_link_path = ghc::filesystem::path(pl.pl_link_value);
+ std::string link_path;
+
+ if (remote_link_path.is_absolute()) {
+ auto local_link_path = this->ht_local_path
+ / remote_link_path.relative_path();
+
+ link_path = local_link_path.string();
+ } else {
+ link_path = remote_link_path.string();
+ }
+
+ log_debug("symlinking %s -> %s",
+ local_path.c_str(),
+ link_path.c_str());
+ ghc::filesystem::create_directories(local_path.parent_path());
+ ghc::filesystem::remove_all(local_path);
+ if (symlink(link_path.c_str(), local_path.c_str()) < 0) {
+ log_error("symlink failed: %s", strerror(errno));
+ }
+
+ if (pl.pl_root_path == pl.pl_path) {
+ auto iter = conn.c_desired_paths.find(pl.pl_path);
+
+ if (iter != conn.c_desired_paths.end()) {
+ if (iter->second.loo_tail) {
+ conn.c_synced_desired_paths.insert(pl.pl_path);
+ } else {
+ log_info("synced desired path: %s",
+ iter->first.c_str());
+ conn.c_desired_paths.erase(iter);
+ }
+ }
+ } else {
+ auto iter = conn.c_child_paths.find(pl.pl_path);
+
+ if (iter != conn.c_child_paths.end()) {
+ if (iter->second.loo_tail) {
+ conn.c_synced_child_paths.insert(pl.pl_path);
+ } else {
+ log_info("synced child path: %s",
+ iter->first.c_str());
+ conn.c_child_paths.erase(iter);
+ }
+ }
+ }
+
+ return std::move(this->ht_state);
+ },
+ [&](const tailer::packet_preview_error& ppe) {
+ isc::to<main_looper&, services::main_t>().send(
+ [ppe](auto& mlooper) {
+ if (lnav_data.ld_preview_generation != ppe.ppe_id) {
+ log_debug("preview ID mismatch: %lld != %lld",
+ lnav_data.ld_preview_generation,
+ ppe.ppe_id);
+ return;
+ }
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(false)
+ .clear();
+ lnav_data.ld_preview_source.clear();
+ lnav_data.ld_bottom_source.grep_error(ppe.ppe_msg);
+ });
+
+ return std::move(this->ht_state);
+ },
+ [&](const tailer::packet_preview_data& ppd) {
+ isc::to<main_looper&, services::main_t>().send(
+ [netloc = this->ht_netloc, ppd](auto& mlooper) {
+ if (lnav_data.ld_preview_generation != ppd.ppd_id) {
+ log_debug("preview ID mismatch: %lld != %lld",
+ lnav_data.ld_preview_generation,
+ ppd.ppd_id);
+ return;
+ }
+ std::string str(ppd.ppd_bits.begin(),
+ ppd.ppd_bits.end());
+ lnav_data.ld_preview_status_source.get_description()
+ .set_cylon(false)
+ .set_value("For file: %s:%s",
+ netloc.c_str(),
+ ppd.ppd_path.c_str());
+ lnav_data.ld_preview_source.replace_with(str)
+ .set_text_format(detect_text_format(str));
+ });
+ return std::move(this->ht_state);
+ },
+ [&](const tailer::packet_possible_path& ppp) {
+ log_debug("possible path: %s", ppp.ppp_path.c_str());
+ auto full_path = fmt::format(
+ FMT_STRING("{}{}"), this->ht_netloc, ppp.ppp_path);
+
+ isc::to<main_looper&, services::main_t>().send(
+ [full_path](auto& mlooper) {
+ lnav_data.ld_rl_view->add_possibility(
+ ln_mode_t::COMMAND, "remote-path", full_path);
+ });
+ return std::move(this->ht_state);
+ });
+
+ if (!this->ht_state.is<connected>()) {
+ this->s_looping = false;
+ }
+ }
+}
+
+std::chrono::milliseconds
+tailer::looper::host_tailer::compute_timeout(mstime_t current_time) const
+{
+ return 0s;
+}
+
+void
+tailer::looper::host_tailer::stopped()
+{
+ if (this->ht_state.is<connected>()) {
+ this->ht_state = disconnected();
+ }
+ if (this->ht_error_reader.joinable()) {
+ this->ht_error_reader.join();
+ }
+}
+
+std::string
+tailer::looper::host_tailer::get_display_path(
+ const std::string& remote_path) const
+{
+ return fmt::format(FMT_STRING("{}{}"), this->ht_netloc, remote_path);
+}
+
+void*
+tailer::looper::host_tailer::run()
+{
+ log_set_thread_prefix(
+ fmt::format(FMT_STRING("tailer({})"), this->ht_netloc));
+
+ return service_base::run();
+}
+
+auto_pid<process_state::finished>
+tailer::looper::host_tailer::connected::close() &&
+{
+ this->ht_to_child.reset();
+ this->ht_from_child.reset();
+
+ return std::move(this->ht_child).wait_for_child();
+}
+
+void
+tailer::looper::child_finished(std::shared_ptr<service_base> child)
+{
+ auto child_tailer = std::static_pointer_cast<host_tailer>(child);
+
+ for (auto iter = this->l_remotes.begin(); iter != this->l_remotes.end();
+ ++iter)
+ {
+ if (iter->second != child_tailer) {
+ continue;
+ }
+
+ if (child_tailer->is_synced()) {
+ log_info("synced with netloc '%s', removing", iter->first.c_str());
+ auto netloc_iter = this->l_netlocs_to_paths.find(iter->first);
+
+ if (netloc_iter != this->l_netlocs_to_paths.end()) {
+ netloc_iter->second.send_synced_to_main(netloc_iter->first);
+ this->l_netlocs_to_paths.erase(netloc_iter);
+ }
+ }
+ lnav_data.ld_active_files.fc_progress->writeAccess()->sp_tailers.erase(
+ iter->first);
+ this->l_remotes.erase(iter);
+ return;
+ }
+}
+
+void
+tailer::looper::remote_path_queue::send_synced_to_main(
+ const std::string& netloc)
+{
+ std::set<std::string> synced_files;
+
+ for (const auto& pair : this->rpq_new_paths) {
+ if (!pair.second.loo_tail) {
+ synced_files.emplace(
+ fmt::format(FMT_STRING("{}{}"), netloc, pair.first));
+ }
+ }
+ for (const auto& pair : this->rpq_existing_paths) {
+ if (!pair.second.loo_tail) {
+ synced_files.emplace(
+ fmt::format(FMT_STRING("{}{}"), netloc, pair.first));
+ }
+ }
+
+ isc::to<main_looper&, services::main_t>().send(
+ [file_set = std::move(synced_files)](auto& mlooper) {
+ file_collection fc;
+
+ fc.fc_synced_files = file_set;
+ update_active_files(fc);
+ });
+}
+
+void
+tailer::looper::report_error(std::string path, std::string msg)
+{
+ log_error("reporting error: %s -- %s", path.c_str(), msg.c_str());
+ isc::to<main_looper&, services::main_t>().send([=](auto& mlooper) {
+ file_collection fc;
+
+ fc.fc_name_to_errors.emplace(path,
+ file_error_info{
+ {},
+ msg,
+ });
+ update_active_files(fc);
+ lnav_data.ld_active_files.fc_progress->writeAccess()->sp_tailers.erase(
+ path);
+ });
+}
+
+void
+tailer::cleanup_cache()
+{
+ (void) std::async(std::launch::async, []() {
+ auto now = std::chrono::system_clock::now();
+ auto cache_path = remote_cache_path();
+ const auto& cfg = injector::get<const config&>();
+ std::vector<ghc::filesystem::path> to_remove;
+
+ log_debug("cache-ttl %d", cfg.c_cache_ttl.count());
+ for (const auto& entry :
+ ghc::filesystem::directory_iterator(cache_path))
+ {
+ auto mtime = ghc::filesystem::last_write_time(entry.path());
+ auto exp_time = mtime + cfg.c_cache_ttl;
+ if (now < exp_time) {
+ continue;
+ }
+
+ to_remove.emplace_back(entry.path());
+ }
+
+ for (auto& entry : to_remove) {
+ log_debug("removing cached remote: %s", entry.c_str());
+ ghc::filesystem::remove_all(entry);
+ }
+ });
+}
diff --git a/src/tailer/tailer.looper.cfg.hh b/src/tailer/tailer.looper.cfg.hh
new file mode 100644
index 0000000..65b5ff6
--- /dev/null
+++ b/src/tailer/tailer.looper.cfg.hh
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_tailer_looper_cfg_hh
+#define lnav_tailer_looper_cfg_hh
+
+#include <chrono>
+
+namespace tailer {
+
+struct config {
+ int64_t c_min_free_space{32 * 1024 * 1024};
+ std::chrono::seconds c_cache_ttl{std::chrono::hours(48)};
+ std::string c_transfer_cmd{"cat > {0:} && chmod ugo+rx ./{0:}"};
+ std::string c_start_cmd{"bash -c ./{0:}"};
+ std::string c_ssh_cmd{"ssh"};
+ std::string c_ssh_flags{};
+ std::map<std::string, std::string> c_ssh_options{};
+ std::map<std::string, std::string> c_ssh_config{
+ {"BatchMode", "yes"},
+ {"ConnectTimeout", "10"},
+ };
+};
+
+} // namespace tailer
+
+#endif
diff --git a/src/tailer/tailer.looper.hh b/src/tailer/tailer.looper.hh
new file mode 100644
index 0000000..1bf84d9
--- /dev/null
+++ b/src/tailer/tailer.looper.hh
@@ -0,0 +1,158 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_tailer_looper_hh
+#define lnav_tailer_looper_hh
+
+#include <set>
+
+#include <logfile_fwd.hh>
+
+#include "base/auto_fd.hh"
+#include "base/auto_pid.hh"
+#include "base/isc.hh"
+#include "base/network.tcp.hh"
+#include "ghc/filesystem.hpp"
+#include "mapbox/variant.hpp"
+
+namespace tailer {
+
+class looper : public isc::service<looper> {
+public:
+ void add_remote(const network::path& path,
+ logfile_open_options_base options);
+
+ void load_preview(int64_t id, const network::path& path);
+
+ void complete_path(const network::path& path);
+
+ bool empty() const { return this->l_netlocs_to_paths.empty(); }
+
+ std::set<std::string> active_netlocs() const
+ {
+ std::set<std::string> retval;
+
+ for (const auto& pair : this->l_remotes) {
+ retval.insert(pair.first);
+ }
+ return retval;
+ }
+
+protected:
+ void loop_body() override;
+
+ void child_finished(std::shared_ptr<service_base> child) override;
+
+private:
+ class host_tailer : public isc::service<host_tailer> {
+ public:
+ static Result<std::shared_ptr<host_tailer>, std::string> for_host(
+ const std::string& netloc);
+
+ host_tailer(const std::string& netloc,
+ auto_pid<process_state::running> child,
+ auto_fd to_child,
+ auto_fd from_child,
+ auto_fd err_from_child);
+
+ void open_remote_path(const std::string& path,
+ logfile_open_options_base loo);
+
+ void load_preview(int64_t id, const std::string& path);
+
+ void complete_path(const std::string& path);
+
+ bool is_synced() const { return this->ht_state.is<synced>(); }
+
+ protected:
+ void* run() override;
+
+ void loop_body() override;
+
+ void stopped() override;
+
+ std::chrono::milliseconds compute_timeout(
+ mstime_t current_time) const override;
+
+ private:
+ static ghc::filesystem::path tmp_path();
+
+ std::string get_display_path(const std::string& remote_path) const;
+
+ struct connected {
+ auto_pid<process_state::running> ht_child;
+ auto_fd ht_to_child;
+ auto_fd ht_from_child;
+ std::map<std::string, logfile_open_options_base> c_desired_paths;
+ std::set<std::string> c_synced_desired_paths;
+ std::map<std::string, logfile_open_options_base> c_child_paths;
+ std::set<std::string> c_synced_child_paths;
+ bool c_initial_sync_done{false};
+
+ auto_pid<process_state::finished> close() &&;
+ };
+
+ struct disconnected {};
+ struct synced {};
+
+ using state_v = mapbox::util::variant<connected, disconnected, synced>;
+
+ const std::string ht_netloc;
+ std::string ht_uname;
+ const ghc::filesystem::path ht_local_path;
+ std::set<ghc::filesystem::path> ht_active_files;
+ std::vector<std::string> ht_error_queue;
+ std::thread ht_error_reader;
+ state_v ht_state{disconnected()};
+ uint64_t ht_cycle_count{0};
+ };
+
+ static void report_error(std::string path, std::string msg);
+
+ using attempt_time_point
+ = std::chrono::time_point<std::chrono::steady_clock>;
+
+ struct remote_path_queue {
+ attempt_time_point rpq_next_attempt_time{
+ std::chrono::steady_clock::now()};
+ std::map<std::string, logfile_open_options_base> rpq_new_paths;
+ std::map<std::string, logfile_open_options_base> rpq_existing_paths;
+
+ void send_synced_to_main(const std::string& netloc);
+ };
+
+ std::map<std::string, remote_path_queue> l_netlocs_to_paths;
+ std::map<std::string, std::shared_ptr<host_tailer>> l_remotes;
+};
+
+void cleanup_cache();
+
+} // namespace tailer
+
+#endif
diff --git a/src/tailer/tailer.main.c b/src/tailer/tailer.main.c
new file mode 100644
index 0000000..445d24c
--- /dev/null
+++ b/src/tailer/tailer.main.c
@@ -0,0 +1,1072 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef __COSMOPOLITAN__
+#include <glob.h>
+#include <stdio.h>
+#include <assert.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <dirent.h>
+#include <stdarg.h>
+#include <limits.h>
+#include <poll.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/utsname.h>
+#include <ctype.h>
+#include <stdint.h>
+#endif
+
+#include "sha-256.h"
+#include "tailer.h"
+
+struct node {
+ struct node *n_succ;
+ struct node *n_pred;
+};
+
+struct list {
+ struct node *l_head;
+ struct node *l_tail;
+ struct node *l_tail_pred;
+};
+
+int is_glob(const char *fn)
+{
+ return (strchr(fn, '*') != NULL ||
+ strchr(fn, '?') != NULL ||
+ strchr(fn, '[') != NULL);
+};
+
+void list_init(struct list *l)
+{
+ l->l_head = (struct node *) &l->l_tail;
+ l->l_tail = NULL;
+ l->l_tail_pred = (struct node *) &l->l_head;
+}
+
+void list_move(struct list *dst, struct list *src)
+{
+ if (src->l_head->n_succ == NULL) {
+ list_init(dst);
+ return;
+ }
+
+ dst->l_head = src->l_head;
+ dst->l_head->n_pred = (struct node *) &dst->l_head;
+ dst->l_tail = NULL;
+ dst->l_tail_pred = src->l_tail_pred;
+ dst->l_tail_pred->n_succ = (struct node *) &dst->l_tail;
+
+ list_init(src);
+}
+
+void list_remove(struct node *n)
+{
+ n->n_pred->n_succ = n->n_succ;
+ n->n_succ->n_pred = n->n_pred;
+
+ n->n_succ = NULL;
+ n->n_pred = NULL;
+}
+
+struct node *list_remove_head(struct list *l)
+{
+ struct node *retval = NULL;
+
+ if (l->l_head->n_succ != NULL) {
+ retval = l->l_head;
+ list_remove(l->l_head);
+ }
+
+ return retval;
+}
+
+void list_append(struct list *l, struct node *n)
+{
+ n->n_pred = l->l_tail_pred;
+ n->n_succ = (struct node *) &l->l_tail;
+ l->l_tail_pred->n_succ = n;
+ l->l_tail_pred = n;
+}
+
+typedef enum {
+ CS_INIT,
+ CS_OFFERED,
+ CS_TAILING,
+ CS_SYNCED,
+} client_state_t;
+
+typedef enum {
+ PS_UNKNOWN,
+ PS_OK,
+ PS_ERROR,
+} path_state_t;
+
+struct client_path_state {
+ struct node cps_node;
+ char *cps_path;
+ path_state_t cps_last_path_state;
+ struct stat cps_last_stat;
+ int64_t cps_client_file_offset;
+ int64_t cps_client_file_size;
+ client_state_t cps_client_state;
+ struct list cps_children;
+};
+
+struct client_path_state *create_client_path_state(const char *path)
+{
+ struct client_path_state *retval = malloc(sizeof(struct client_path_state));
+
+ retval->cps_path = strdup(path);
+ retval->cps_last_path_state = PS_UNKNOWN;
+ memset(&retval->cps_last_stat, 0, sizeof(retval->cps_last_stat));
+ retval->cps_client_file_offset = -1;
+ retval->cps_client_file_size = 0;
+ retval->cps_client_state = CS_INIT;
+ list_init(&retval->cps_children);
+ return retval;
+}
+
+void delete_client_path_state(struct client_path_state *cps);
+
+void delete_client_path_list(struct list *l)
+{
+ struct client_path_state *child_cps;
+
+ while ((child_cps = (struct client_path_state *) list_remove_head(l)) != NULL) {
+ list_remove(&child_cps->cps_node);
+ delete_client_path_state(child_cps);
+ }
+}
+
+void delete_client_path_state(struct client_path_state *cps)
+{
+ free(cps->cps_path);
+ delete_client_path_list(&cps->cps_children);
+ free(cps);
+}
+
+void dump_client_path_states(struct list *path_list)
+{
+ struct client_path_state *curr = (struct client_path_state *) path_list->l_head;
+
+ while (curr->cps_node.n_succ != NULL) {
+ fprintf(stderr, "debug: path %s\n", curr->cps_path);
+ dump_client_path_states(&curr->cps_children);
+
+ curr = (struct client_path_state *) curr->cps_node.n_succ;
+ }
+
+ curr = (struct client_path_state *) path_list->l_tail_pred;
+ while (curr->cps_node.n_pred != NULL) {
+ fprintf(stderr, "debug: back path %s\n", curr->cps_path);
+ dump_client_path_states(&curr->cps_children);
+
+ curr = (struct client_path_state *) curr->cps_node.n_pred;
+ }
+}
+
+void send_error(struct client_path_state *cps, char *msg, ...)
+{
+ char buffer[1024];
+ va_list args;
+
+ va_start(args, msg);
+ vsnprintf(buffer, sizeof(buffer), msg, args);
+ va_end(args);
+
+ send_packet(STDOUT_FILENO,
+ TPT_ERROR,
+ TPPT_STRING, cps->cps_path,
+ TPPT_STRING, buffer,
+ TPPT_DONE);
+}
+
+void set_client_path_state_error(struct client_path_state *cps, const char *op)
+{
+ if (cps->cps_last_path_state != PS_ERROR) {
+ // tell client of the problem
+ send_error(cps, "unable to %s -- %s", op, strerror(errno));
+ }
+ cps->cps_last_path_state = PS_ERROR;
+ cps->cps_client_file_offset = -1;
+ cps->cps_client_state = CS_INIT;
+ delete_client_path_list(&cps->cps_children);
+}
+
+typedef enum {
+ RS_ERROR,
+ RS_PACKET_TYPE,
+ RS_PAYLOAD_TYPE,
+ RS_PAYLOAD,
+ RS_PAYLOAD_LENGTH,
+ RS_PAYLOAD_CONTENT,
+} recv_state_t;
+
+static recv_state_t readall(recv_state_t state, int sock, void *buf, size_t len)
+{
+ char *cbuf = (char *) buf;
+ off_t offset = 0;
+
+ if (state == RS_ERROR) {
+ return RS_ERROR;
+ }
+
+ while (len > 0) {
+ ssize_t rc = read(sock, &cbuf[offset], len);
+
+ if (rc == -1) {
+ if (errno == EAGAIN || errno == EINTR) {
+
+ } else {
+ return RS_ERROR;
+ }
+ }
+ else if (rc == 0) {
+ errno = EIO;
+ return RS_ERROR;
+ }
+ else {
+ len -= rc;
+ offset += rc;
+ }
+ }
+
+ switch (state) {
+ case RS_PACKET_TYPE:
+ return RS_PAYLOAD_TYPE;
+ case RS_PAYLOAD_TYPE:
+ return RS_PAYLOAD;
+ case RS_PAYLOAD_LENGTH:
+ return RS_PAYLOAD_CONTENT;
+ case RS_PAYLOAD_CONTENT:
+ return RS_PAYLOAD_TYPE;
+ default:
+ return RS_ERROR;
+ }
+}
+
+static tailer_packet_payload_type_t read_payload_type(recv_state_t *state, int sock)
+{
+ tailer_packet_payload_type_t retval = TPPT_DONE;
+
+ assert(*state == RS_PAYLOAD_TYPE);
+
+ *state = readall(*state, sock, &retval, sizeof(retval));
+ if (*state != RS_ERROR && retval == TPPT_DONE) {
+ *state = RS_PACKET_TYPE;
+ }
+ return retval;
+}
+
+static char *readstr(recv_state_t *state, int sock)
+{
+ assert(*state == RS_PAYLOAD_TYPE);
+
+ tailer_packet_payload_type_t payload_type = read_payload_type(state, sock);
+
+ if (payload_type != TPPT_STRING) {
+ fprintf(stderr, "error: expected string, got: %d\n", payload_type);
+ return NULL;
+ }
+
+ int32_t length;
+
+ *state = RS_PAYLOAD_LENGTH;
+ *state = readall(*state, sock, &length, sizeof(length));
+ if (*state == RS_ERROR) {
+ fprintf(stderr, "error: unable to read string length\n");
+ return NULL;
+ }
+
+ char *retval = malloc(length + 1);
+ if (retval == NULL) {
+ return NULL;
+ }
+
+ *state = readall(*state, sock, retval, length);
+ if (*state == RS_ERROR) {
+ fprintf(stderr, "error: unable to read string of length: %d\n", length);
+ free(retval);
+ return NULL;
+ }
+ retval[length] = '\0';
+
+ return retval;
+}
+
+static int readint64(recv_state_t *state, int sock, int64_t *i)
+{
+ tailer_packet_payload_type_t payload_type = read_payload_type(state, sock);
+
+ if (payload_type != TPPT_INT64) {
+ fprintf(stderr, "error: expected int64, got: %d\n", payload_type);
+ return -1;
+ }
+
+ *state = RS_PAYLOAD_CONTENT;
+ *state = readall(*state, sock, i, sizeof(*i));
+ if (*state == -1) {
+ fprintf(stderr, "error: unable to read int64\n");
+ return -1;
+ }
+
+ return 0;
+}
+
+struct list client_path_list;
+
+struct client_path_state *find_client_path_state(struct list *path_list, const char *path)
+{
+ struct client_path_state *curr = (struct client_path_state *) path_list->l_head;
+
+ while (curr->cps_node.n_succ != NULL) {
+ if (strcmp(curr->cps_path, path) == 0) {
+ return curr;
+ }
+
+ struct client_path_state *child =
+ find_client_path_state(&curr->cps_children, path);
+
+ if (child != NULL) {
+ return child;
+ }
+
+ curr = (struct client_path_state *) curr->cps_node.n_succ;
+ }
+
+ return NULL;
+}
+
+void send_preview_error(int64_t id, const char *path, const char *msg)
+{
+ send_packet(STDOUT_FILENO,
+ TPT_PREVIEW_ERROR,
+ TPPT_INT64, id,
+ TPPT_STRING, path,
+ TPPT_STRING, msg,
+ TPPT_DONE);
+}
+
+void send_preview_data(int64_t id, const char *path, int32_t len, const char *bits)
+{
+ send_packet(STDOUT_FILENO,
+ TPT_PREVIEW_DATA,
+ TPPT_INT64, id,
+ TPPT_STRING, path,
+ TPPT_BITS, len, bits,
+ TPPT_DONE);
+}
+
+int poll_paths(struct list *path_list, struct client_path_state *root_cps)
+{
+ struct client_path_state *curr = (struct client_path_state *) path_list->l_head;
+ int is_top = root_cps == NULL;
+ int retval = 0;
+
+ while (curr->cps_node.n_succ != NULL) {
+ if (is_top) {
+ root_cps = curr;
+ }
+
+ if (is_glob(curr->cps_path)) {
+ int changes = 0;
+ glob_t gl;
+
+ memset(&gl, 0, sizeof(gl));
+ if (glob(curr->cps_path, 0, NULL, &gl) != 0) {
+ set_client_path_state_error(curr, "glob");
+ } else {
+ struct list prev_children;
+
+ list_move(&prev_children, &curr->cps_children);
+ for (size_t lpc = 0; lpc < gl.gl_pathc; lpc++) {
+ struct client_path_state *child;
+
+ if ((child = find_client_path_state(
+ &prev_children, gl.gl_pathv[lpc])) == NULL) {
+ child = create_client_path_state(gl.gl_pathv[lpc]);
+ changes += 1;
+ } else {
+ list_remove(&child->cps_node);
+ }
+ list_append(&curr->cps_children, &child->cps_node);
+ }
+ globfree(&gl);
+
+ struct client_path_state *child;
+
+ while ((child = (struct client_path_state *) list_remove_head(
+ &prev_children)) != NULL) {
+ send_error(child, "deleted");
+ delete_client_path_state(child);
+ changes += 1;
+ }
+
+ retval += poll_paths(&curr->cps_children, root_cps);
+ }
+
+ if (changes) {
+ curr->cps_client_state = CS_INIT;
+ } else if (curr->cps_client_state != CS_SYNCED) {
+ send_packet(STDOUT_FILENO,
+ TPT_SYNCED,
+ TPPT_STRING, root_cps->cps_path,
+ TPPT_STRING, curr->cps_path,
+ TPPT_DONE);
+ curr->cps_client_state = CS_SYNCED;
+ }
+
+ curr = (struct client_path_state *) curr->cps_node.n_succ;
+ continue;
+ }
+
+ struct stat st;
+ int rc = lstat(curr->cps_path, &st);
+
+ if (rc == -1) {
+ memset(&st, 0, sizeof(st));
+ set_client_path_state_error(curr, "lstat");
+ } else if (curr->cps_client_file_offset >= 0 &&
+ ((curr->cps_last_stat.st_dev != st.st_dev &&
+ curr->cps_last_stat.st_ino != st.st_ino) ||
+ (st.st_size < curr->cps_last_stat.st_size))) {
+ send_error(curr, "replaced");
+ set_client_path_state_error(curr, "replace");
+ } else if (S_ISLNK(st.st_mode)) {
+ switch (curr->cps_client_state) {
+ case CS_INIT: {
+ char buffer[PATH_MAX];
+ ssize_t link_len;
+
+ link_len = readlink(curr->cps_path, buffer, sizeof(buffer));
+ if (link_len < 0) {
+ set_client_path_state_error(curr, "readlink");
+ } else {
+ buffer[link_len] = '\0';
+ send_packet(STDOUT_FILENO,
+ TPT_LINK_BLOCK,
+ TPPT_STRING, root_cps->cps_path,
+ TPPT_STRING, curr->cps_path,
+ TPPT_STRING, buffer,
+ TPPT_DONE);
+ curr->cps_client_state = CS_SYNCED;
+
+ if (buffer[0] == '/') {
+ struct client_path_state *child =
+ create_client_path_state(buffer);
+
+ fprintf(stderr, "info: monitoring link path %s\n",
+ buffer);
+ list_append(&curr->cps_children, &child->cps_node);
+ }
+
+ retval += 1;
+ }
+ break;
+ }
+ case CS_SYNCED:
+ break;
+ case CS_OFFERED:
+ case CS_TAILING:
+ fprintf(stderr,
+ "internal-error: unexpected state for path -- %s\n",
+ curr->cps_path);
+ break;
+ }
+
+ retval += poll_paths(&curr->cps_children, root_cps);
+
+ curr->cps_last_path_state = PS_OK;
+ } else if (S_ISREG(st.st_mode)) {
+ switch (curr->cps_client_state) {
+ case CS_INIT:
+ case CS_TAILING:
+ case CS_SYNCED: {
+ if (curr->cps_client_file_offset < st.st_size) {
+ int fd = open(curr->cps_path, O_RDONLY);
+
+ if (fd == -1) {
+ set_client_path_state_error(curr, "open");
+ } else {
+ static unsigned char buffer[4 * 1024 * 1024];
+
+ int64_t file_offset =
+ curr->cps_client_file_offset < 0 ?
+ 0 :
+ curr->cps_client_file_offset;
+ int64_t nbytes = sizeof(buffer);
+ if (curr->cps_client_state == CS_INIT) {
+ if (curr->cps_client_file_size == 0) {
+ // initial state, haven't heard from client yet.
+ nbytes = 32 * 1024;
+ } else if (file_offset < curr->cps_client_file_size) {
+ // heard from client, try to catch up
+ nbytes = curr->cps_client_file_size - file_offset;
+ if (nbytes > sizeof(buffer)) {
+ nbytes = sizeof(buffer);
+ }
+ }
+ }
+ int32_t bytes_read = pread(fd, buffer, nbytes, file_offset);
+
+ if (bytes_read == -1) {
+ set_client_path_state_error(curr, "pread");
+ } else if (curr->cps_client_state == CS_INIT &&
+ (curr->cps_client_file_offset < 0 ||
+ bytes_read > 0)) {
+ static unsigned char
+ HASH_BUFFER[4 * 1024 * 1024];
+ BYTE hash[SHA256_BLOCK_SIZE];
+ size_t remaining = 0;
+ int64_t remaining_offset
+ = file_offset + bytes_read;
+ SHA256_CTX shactx;
+
+ if (curr->cps_client_file_size > 0
+ && file_offset < curr->cps_client_file_size)
+ {
+ remaining = curr->cps_client_file_size
+ - file_offset - bytes_read;
+ }
+
+ fprintf(stderr,
+ "info: prepping offer: init=%d; "
+ "remaining=%zu; %s\n",
+ bytes_read,
+ remaining,
+ curr->cps_path);
+ sha256_init(&shactx);
+ sha256_update(&shactx, buffer, bytes_read);
+ while (remaining > 0) {
+ nbytes = sizeof(HASH_BUFFER);
+ if (remaining < nbytes) {
+ nbytes = remaining;
+ }
+ ssize_t remaining_bytes_read
+ = pread(fd,
+ HASH_BUFFER,
+ nbytes,
+ remaining_offset);
+ if (remaining_bytes_read < 0) {
+ set_client_path_state_error(curr, "pread");
+ break;
+ }
+ if (remaining_bytes_read == 0) {
+ remaining = 0;
+ break;
+ }
+ sha256_update(&shactx, HASH_BUFFER, remaining_bytes_read);
+ remaining -= remaining_bytes_read;
+ remaining_offset += remaining_bytes_read;
+ bytes_read += remaining_bytes_read;
+ }
+
+ if (remaining == 0) {
+ sha256_final(&shactx, hash);
+
+ send_packet(STDOUT_FILENO,
+ TPT_OFFER_BLOCK,
+ TPPT_STRING, root_cps->cps_path,
+ TPPT_STRING, curr->cps_path,
+ TPPT_INT64,
+ (int64_t) st.st_mtime,
+ TPPT_INT64, file_offset,
+ TPPT_INT64, (int64_t) bytes_read,
+ TPPT_HASH, hash,
+ TPPT_DONE);
+ curr->cps_client_state = CS_OFFERED;
+ }
+ } else {
+ if (curr->cps_client_file_offset < 0) {
+ curr->cps_client_file_offset = 0;
+ }
+
+ send_packet(STDOUT_FILENO,
+ TPT_TAIL_BLOCK,
+ TPPT_STRING, root_cps->cps_path,
+ TPPT_STRING, curr->cps_path,
+ TPPT_INT64, (int64_t) st.st_mtime,
+ TPPT_INT64, curr->cps_client_file_offset,
+ TPPT_BITS, bytes_read, buffer,
+ TPPT_DONE);
+ curr->cps_client_file_offset += bytes_read;
+ curr->cps_client_state = CS_TAILING;
+ }
+ close(fd);
+
+ retval = 1;
+ }
+ } else if (curr->cps_client_state != CS_SYNCED) {
+ send_packet(STDOUT_FILENO,
+ TPT_SYNCED,
+ TPPT_STRING, root_cps->cps_path,
+ TPPT_STRING, curr->cps_path,
+ TPPT_DONE);
+ curr->cps_client_state = CS_SYNCED;
+ }
+ break;
+ }
+ case CS_OFFERED: {
+ // Still waiting for the client ack
+ break;
+ }
+ }
+
+ curr->cps_last_path_state = PS_OK;
+ } else if (S_ISDIR(st.st_mode)) {
+ DIR *dir = opendir(curr->cps_path);
+
+ if (dir == NULL) {
+ set_client_path_state_error(curr, "opendir");
+ } else {
+ struct list prev_children;
+ struct dirent *entry;
+ int changes = 0;
+
+ list_move(&prev_children, &curr->cps_children);
+ while ((entry = readdir(dir)) != NULL) {
+ if (strcmp(entry->d_name, ".") == 0 ||
+ strcmp(entry->d_name, "..") == 0) {
+ continue;
+ }
+
+ if (entry->d_type != DT_REG &&
+ entry->d_type != DT_LNK) {
+ continue;
+ }
+
+ char full_path[PATH_MAX];
+
+ snprintf(full_path, sizeof(full_path),
+ "%s/%s",
+ curr->cps_path, entry->d_name);
+
+ struct client_path_state *child = find_client_path_state(&prev_children, full_path);
+
+ if (child == NULL) {
+ // new file
+ fprintf(stderr, "info: monitoring child path: %s\n", full_path);
+ child = create_client_path_state(full_path);
+ changes += 1;
+ } else {
+ list_remove(&child->cps_node);
+ }
+ list_append(&curr->cps_children, &child->cps_node);
+ }
+ closedir(dir);
+
+ struct client_path_state *child;
+
+ while ((child = (struct client_path_state *) list_remove_head(
+ &prev_children)) != NULL) {
+ send_error(child, "deleted");
+ delete_client_path_state(child);
+ changes += 1;
+ }
+
+ retval += poll_paths(&curr->cps_children, root_cps);
+
+ if (changes) {
+ curr->cps_client_state = CS_INIT;
+ } else if (curr->cps_client_state != CS_SYNCED) {
+ send_packet(STDOUT_FILENO,
+ TPT_SYNCED,
+ TPPT_STRING, root_cps->cps_path,
+ TPPT_STRING, curr->cps_path,
+ TPPT_DONE);
+ curr->cps_client_state = CS_SYNCED;
+ }
+ }
+
+ curr->cps_last_path_state = PS_OK;
+ }
+
+ curr->cps_last_stat = st;
+
+ curr = (struct client_path_state *) curr->cps_node.n_succ;
+ }
+
+ fflush(stderr);
+
+ return retval;
+}
+
+static
+void send_possible_paths(const char *glob_path, int depth)
+{
+ glob_t gl;
+
+ memset(&gl, 0, sizeof(gl));
+ if (glob(glob_path, GLOB_MARK, NULL, &gl) == 0) {
+ for (size_t lpc = 0;
+ lpc < gl.gl_pathc;
+ lpc++) {
+ const char *child_path = gl.gl_pathv[lpc];
+ size_t child_len = strlen(gl.gl_pathv[lpc]);
+
+ send_packet(STDOUT_FILENO,
+ TPT_POSSIBLE_PATH,
+ TPPT_STRING, child_path,
+ TPPT_DONE);
+
+ if (depth == 0 && child_path[child_len - 1] == '/') {
+ char *child_copy = malloc(child_len + 2);
+
+ strcpy(child_copy, child_path);
+ strcat(child_copy, "*");
+ send_possible_paths(child_copy, depth + 1);
+ free(child_copy);
+ }
+ }
+ }
+
+ globfree(&gl);
+}
+
+static
+void handle_load_preview_request(const char *path, int64_t preview_id)
+{
+ struct stat st;
+
+ fprintf(stderr, "info: load preview request -- %lld\n", preview_id);
+ if (is_glob(path)) {
+ glob_t gl;
+
+ memset(&gl, 0, sizeof(gl));
+ if (glob(path, 0, NULL, &gl) != 0) {
+ char msg[1024];
+
+ snprintf(msg, sizeof(msg),
+ "error: cannot glob %s -- %s",
+ path,
+ strerror(errno));
+ send_preview_error(preview_id, path, msg);
+ } else {
+ char *bits = malloc(1024 * 1024);
+ int lpc, line_count = 10;
+
+ bits[0] = '\0';
+ for (lpc = 0;
+ line_count > 0 && lpc < gl.gl_pathc;
+ lpc++, line_count--) {
+ strcat(bits, gl.gl_pathv[lpc]);
+ strcat(bits, "\n");
+ }
+
+ if (lpc < gl.gl_pathc) {
+ strcat(bits, " ... and more! ...\n");
+ }
+
+ send_preview_data(preview_id, path, strlen(bits), bits);
+
+ globfree(&gl);
+ free(bits);
+ }
+ }
+ else if (stat(path, &st) == -1) {
+ char msg[1024];
+
+ snprintf(msg, sizeof(msg),
+ "error: cannot open %s -- %s",
+ path,
+ strerror(errno));
+ send_preview_error(preview_id, path, msg);
+ } else if (S_ISREG(st.st_mode)) {
+ size_t capacity = 1024 * 1024;
+ char *bits = malloc(capacity);
+ FILE *file;
+
+ if ((file = fopen(path, "r")) == NULL) {
+ char msg[1024];
+
+ snprintf(msg, sizeof(msg),
+ "error: cannot open %s -- %s",
+ path,
+ strerror(errno));
+ send_preview_error(preview_id, path, msg);
+ } else {
+ int line_count = 10;
+ size_t offset = 0;
+ char *line;
+
+ while (line_count &&
+ (capacity - offset) > 1024 &&
+ (line = fgets(&bits[offset], capacity - offset, file)) != NULL) {
+ offset += strlen(line);
+ line_count -= 1;
+ }
+
+ fclose(file);
+
+ send_preview_data(preview_id, path, offset, bits);
+ }
+ free(bits);
+ } else if (S_ISDIR(st.st_mode)) {
+ DIR *dir = opendir(path);
+
+ if (dir == NULL) {
+ char msg[1024];
+
+ snprintf(msg, sizeof(msg),
+ "error: unable to open directory -- %s",
+ path);
+ send_preview_error(preview_id, path, msg);
+ } else {
+ char *bits = malloc(1024 * 1024);
+ struct dirent *entry;
+ int line_count = 10;
+
+ bits[0] = '\0';
+ while ((entry = readdir(dir)) != NULL) {
+ if (strcmp(entry->d_name, ".") == 0 ||
+ strcmp(entry->d_name, "..") == 0) {
+ continue;
+ }
+ if (entry->d_type != DT_REG &&
+ entry->d_type != DT_DIR) {
+ continue;
+ }
+ if (line_count == 1) {
+ strcat(bits, " ... and more! ...\n");
+ break;
+ }
+
+ strcat(bits, entry->d_name);
+ strcat(bits, "\n");
+
+ line_count -= 1;
+ }
+
+ closedir(dir);
+
+ send_preview_data(preview_id, path, strlen(bits), bits);
+
+ free(bits);
+ }
+ } else {
+ char msg[1024];
+
+ snprintf(msg, sizeof(msg),
+ "error: path is not a file or directory -- %s",
+ path);
+ send_preview_error(preview_id, path, msg);
+ }
+}
+
+static
+void handle_complete_path_request(const char *path)
+{
+ size_t path_len = strlen(path);
+ char *glob_path = malloc(path_len + 3);
+ struct stat st;
+
+ strcpy(glob_path, path);
+ fprintf(stderr, "complete path: %s\n", path);
+ if (path[path_len - 1] != '/' &&
+ stat(path, &st) == 0 &&
+ S_ISDIR(st.st_mode)) {
+ strcat(glob_path, "/");
+ }
+ if (path[path_len - 1] != '*') {
+ strcat(glob_path, "*");
+ }
+ fprintf(stderr, "complete glob path: %s\n", glob_path);
+ send_possible_paths(glob_path, 0);
+
+ free(glob_path);
+}
+
+int main(int argc, char *argv[])
+{
+ int done = 0, timeout = 0;
+ recv_state_t rstate = RS_PACKET_TYPE;
+
+ // No need to leave ourselves around
+ if (argc == 1) {
+ unlink(argv[0]);
+ }
+
+ list_init(&client_path_list);
+
+ {
+ FILE *unameFile = popen("uname -mrsv", "r");
+
+ if (unameFile != NULL) {
+ char buffer[1024];
+
+ fgets(buffer, sizeof(buffer), unameFile);
+ char *bufend = buffer + strlen(buffer) - 1;
+ while (isspace(*bufend)) {
+ bufend -= 1;
+ }
+ *bufend = '\0';
+ send_packet(STDOUT_FILENO,
+ TPT_ANNOUNCE,
+ TPPT_STRING, buffer,
+ TPPT_DONE);
+ pclose(unameFile);
+ }
+ }
+
+ while (!done) {
+ struct pollfd pfds[1];
+
+ pfds[0].fd = STDIN_FILENO;
+ pfds[0].events = POLLIN;
+ pfds[0].revents = 0;
+
+ int ready_count = poll(pfds, 1, timeout);
+
+ if (ready_count) {
+ tailer_packet_type_t type;
+
+ assert(rstate == RS_PACKET_TYPE);
+ rstate = readall(rstate, STDIN_FILENO, &type, sizeof(type));
+ if (rstate == RS_ERROR) {
+ fprintf(stderr, "info: exiting...\n");
+ done = 1;
+ } else {
+ switch (type) {
+ case TPT_OPEN_PATH:
+ case TPT_CLOSE_PATH:
+ case TPT_LOAD_PREVIEW:
+ case TPT_COMPLETE_PATH: {
+ char *path = readstr(&rstate, STDIN_FILENO);
+ int64_t preview_id = 0;
+
+ if (type == TPT_LOAD_PREVIEW) {
+ if (readint64(&rstate, STDIN_FILENO, &preview_id) == -1) {
+ done = 1;
+ break;
+ }
+ }
+ if (path == NULL) {
+ fprintf(stderr, "error: unable to get path to open\n");
+ done = 1;
+ } else if (read_payload_type(&rstate, STDIN_FILENO) != TPPT_DONE) {
+ fprintf(stderr, "error: invalid open packet\n");
+ done = 1;
+ } else if (type == TPT_OPEN_PATH) {
+ struct client_path_state *cps;
+
+ cps = find_client_path_state(&client_path_list, path);
+ if (cps != NULL) {
+ fprintf(stderr, "warning: already monitoring -- %s\n", path);
+ } else {
+ cps = create_client_path_state(path);
+
+ fprintf(stderr, "info: monitoring path: %s\n", path);
+ list_append(&client_path_list, &cps->cps_node);
+ }
+ } else if (type == TPT_CLOSE_PATH) {
+ struct client_path_state *cps = find_client_path_state(&client_path_list, path);
+
+ if (cps == NULL) {
+ fprintf(stderr, "warning: path is not open: %s\n", path);
+ } else {
+ list_remove(&cps->cps_node);
+ delete_client_path_state(cps);
+ }
+ } else if (type == TPT_LOAD_PREVIEW) {
+ handle_load_preview_request(path, preview_id);
+ } else if (type == TPT_COMPLETE_PATH) {
+ handle_complete_path_request(path);
+ }
+
+ free(path);
+ break;
+ }
+ case TPT_ACK_BLOCK:
+ case TPT_NEED_BLOCK: {
+ char *path = readstr(&rstate, STDIN_FILENO);
+ int64_t ack_offset = 0, ack_len = 0, client_size = 0;
+
+ if (type == TPT_ACK_BLOCK &&
+ (readint64(&rstate, STDIN_FILENO, &ack_offset) == -1 ||
+ readint64(&rstate, STDIN_FILENO, &ack_len) == -1 ||
+ readint64(&rstate, STDIN_FILENO, &client_size) == -1)) {
+ done = 1;
+ break;
+ }
+
+ // fprintf(stderr, "info: block packet path: %s\n", path);
+ if (path == NULL) {
+ fprintf(stderr, "error: unable to get block path\n");
+ done = 1;
+ } else if (read_payload_type(&rstate, STDIN_FILENO) != TPPT_DONE) {
+ fprintf(stderr, "error: invalid block packet\n");
+ done = 1;
+ } else {
+ struct client_path_state *cps = find_client_path_state(&client_path_list, path);
+
+ if (cps == NULL) {
+ fprintf(stderr, "warning: unknown path in block packet: %s\n", path);
+ } else if (type == TPT_NEED_BLOCK) {
+ fprintf(stderr, "info: client is tailing: %s\n", path);
+ cps->cps_client_state = CS_TAILING;
+ } else if (type == TPT_ACK_BLOCK) {
+ fprintf(stderr,
+ "info: client acked: %s %lld\n",
+ path,
+ client_size);
+ if (ack_len == 0) {
+ cps->cps_client_state = CS_TAILING;
+ } else {
+ cps->cps_client_file_offset = ack_offset + ack_len;
+ cps->cps_client_state = CS_INIT;
+ cps->cps_client_file_size = client_size;
+ }
+ }
+ free(path);
+ }
+ break;
+ }
+ default: {
+ assert(0);
+ }
+ }
+ }
+ }
+
+ if (!done) {
+ if (poll_paths(&client_path_list, NULL)) {
+ timeout = 0;
+ } else {
+ timeout = 1000;
+ }
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/src/tailer/tailerpp.cc b/src/tailer/tailerpp.cc
new file mode 100644
index 0000000..1ea0a9e
--- /dev/null
+++ b/src/tailer/tailerpp.cc
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "tailerpp.hh"
+
+#include <unistd.h>
+
+namespace tailer {
+
+int
+readall(int sock, void* buf, size_t len)
+{
+ char* cbuf = (char*) buf;
+ off_t offset = 0;
+
+ while (len > 0) {
+ ssize_t rc = read(sock, &cbuf[offset], len);
+
+ if (rc == -1) {
+ switch (errno) {
+ case EAGAIN:
+ case EINTR:
+ break;
+ default:
+ return -1;
+ }
+ } else if (rc == 0) {
+ errno = EIO;
+ return -1;
+ } else {
+ len -= rc;
+ offset += rc;
+ }
+ }
+
+ return 0;
+}
+
+Result<packet, std::string>
+read_packet(int fd)
+{
+ tailer_packet_type_t type;
+
+ if (readall(fd, &type, sizeof(type)) == -1) {
+ return Ok(packet{packet_eof{}});
+ }
+ switch (type) {
+ case TPT_ERROR: {
+ packet_error pe;
+
+ TRY(read_payloads_into(fd, pe.pe_path, pe.pe_msg));
+ return Ok(packet{pe});
+ }
+ case TPT_ANNOUNCE: {
+ packet_announce pa;
+
+ TRY(read_payloads_into(fd, pa.pa_uname));
+ return Ok(packet{pa});
+ }
+ case TPT_OFFER_BLOCK: {
+ packet_offer_block pob;
+
+ TRY(read_payloads_into(fd,
+ pob.pob_root_path,
+ pob.pob_path,
+ pob.pob_mtime,
+ pob.pob_offset,
+ pob.pob_length,
+ pob.pob_hash));
+ return Ok(packet{pob});
+ }
+ case TPT_TAIL_BLOCK: {
+ packet_tail_block ptb;
+
+ TRY(read_payloads_into(fd,
+ ptb.ptb_root_path,
+ ptb.ptb_path,
+ ptb.ptb_mtime,
+ ptb.ptb_offset,
+ ptb.ptb_bits));
+ return Ok(packet{ptb});
+ }
+ case TPT_SYNCED: {
+ packet_synced ps;
+
+ TRY(read_payloads_into(fd, ps.ps_root_path, ps.ps_path));
+ return Ok(packet{ps});
+ }
+ case TPT_LINK_BLOCK: {
+ packet_link pl;
+
+ TRY(read_payloads_into(
+ fd, pl.pl_root_path, pl.pl_path, pl.pl_link_value));
+ return Ok(packet{pl});
+ }
+ case TPT_PREVIEW_ERROR: {
+ packet_preview_error ppe;
+
+ TRY(read_payloads_into(fd, ppe.ppe_id, ppe.ppe_path, ppe.ppe_msg));
+ return Ok(packet{ppe});
+ }
+ case TPT_PREVIEW_DATA: {
+ packet_preview_data ppd;
+
+ TRY(read_payloads_into(fd, ppd.ppd_id, ppd.ppd_path, ppd.ppd_bits));
+ return Ok(packet{ppd});
+ }
+ case TPT_POSSIBLE_PATH: {
+ packet_possible_path ppp;
+
+ TRY(read_payloads_into(fd, ppp.ppp_path));
+ return Ok(packet{ppp});
+ }
+ default:
+ assert(0);
+ break;
+ }
+}
+
+} // namespace tailer
diff --git a/src/tailer/tailerpp.hh b/src/tailer/tailerpp.hh
new file mode 100644
index 0000000..0447379
--- /dev/null
+++ b/src/tailer/tailerpp.hh
@@ -0,0 +1,315 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_tailerpp_hh
+#define lnav_tailerpp_hh
+
+#include <string>
+#include <vector>
+
+#include "base/result.h"
+#include "fmt/format.h"
+#include "mapbox/variant.hpp"
+#include "sha-256.h"
+#include "tailer.h"
+
+namespace tailer {
+
+struct packet_eof {};
+
+struct packet_error {
+ std::string pe_path;
+ std::string pe_msg;
+};
+
+struct packet_announce {
+ std::string pa_uname;
+};
+
+struct hash_frag {
+ uint8_t thf_hash[SHA256_BLOCK_SIZE];
+
+ bool operator==(const hash_frag& other) const
+ {
+ return memcmp(this->thf_hash, other.thf_hash, sizeof(this->thf_hash))
+ == 0;
+ }
+};
+
+struct packet_log {
+ std::string pl_msg;
+};
+
+struct packet_offer_block {
+ std::string pob_root_path;
+ std::string pob_path;
+ int64_t pob_mtime;
+ int64_t pob_offset;
+ int64_t pob_length;
+ hash_frag pob_hash;
+};
+
+struct packet_tail_block {
+ std::string ptb_root_path;
+ std::string ptb_path;
+ int64_t ptb_mtime;
+ int64_t ptb_offset;
+ std::vector<uint8_t> ptb_bits;
+};
+
+struct packet_synced {
+ std::string ps_root_path;
+ std::string ps_path;
+};
+
+struct packet_link {
+ std::string pl_root_path;
+ std::string pl_path;
+ std::string pl_link_value;
+};
+
+struct packet_preview_error {
+ int64_t ppe_id;
+ std::string ppe_path;
+ std::string ppe_msg;
+};
+
+struct packet_preview_data {
+ int64_t ppd_id;
+ std::string ppd_path;
+ std::vector<uint8_t> ppd_bits;
+};
+
+struct packet_possible_path {
+ std::string ppp_path;
+};
+
+using packet = mapbox::util::variant<packet_eof,
+ packet_announce,
+ packet_error,
+ packet_offer_block,
+ packet_tail_block,
+ packet_link,
+ packet_preview_error,
+ packet_preview_data,
+ packet_possible_path,
+ packet_synced>;
+
+struct recv_payload_type {};
+struct recv_payload_length {};
+struct recv_payload_content {};
+
+int readall(int sock, void* buf, size_t len);
+
+namespace details {
+
+template<class...>
+using void_t = void;
+
+template<class, class = void>
+struct has_data : std::false_type {};
+
+template<class T>
+struct has_data<T, decltype(void(std::declval<T&>().data()))>
+ : std::true_type {};
+
+template<typename T, std::enable_if_t<has_data<T>::value, bool> = true>
+uint8_t*
+get_data(T& t)
+{
+ return (uint8_t*) t.data();
+}
+
+template<typename T, std::enable_if_t<!has_data<T>::value, bool> = true>
+uint8_t*
+get_data(T& t)
+{
+ return (uint8_t*) &t;
+}
+
+} // namespace details
+
+template<int PAYLOAD_TYPE, typename EXPECT = recv_payload_type>
+struct protocol_recv {
+ static constexpr bool HAS_LENGTH = (PAYLOAD_TYPE == TPPT_STRING)
+ || (PAYLOAD_TYPE == TPPT_BITS);
+
+ using after_type = typename std::conditional<HAS_LENGTH,
+ recv_payload_length,
+ recv_payload_content>::type;
+
+ static Result<protocol_recv<PAYLOAD_TYPE, after_type>, std::string> create(
+ int fd)
+ {
+ return protocol_recv<PAYLOAD_TYPE>(fd).read_type();
+ }
+
+ Result<protocol_recv<PAYLOAD_TYPE, after_type>, std::string> read_type() &&
+ {
+ static_assert(std::is_same<EXPECT, recv_payload_type>::value,
+ "read_type() cannot be called in this state");
+
+ tailer_packet_payload_type_t payload_type;
+
+ if (readall(this->pr_fd, &payload_type, sizeof(payload_type)) == -1) {
+ return Err(
+ fmt::format(FMT_STRING("unable to read payload type: {}"),
+ strerror(errno)));
+ }
+
+ if (payload_type != PAYLOAD_TYPE) {
+ return Err(fmt::format(
+ FMT_STRING("payload-type mismatch, got: {}; expected: {}"),
+ (int) payload_type,
+ PAYLOAD_TYPE));
+ }
+
+ return Ok(protocol_recv<PAYLOAD_TYPE, after_type>(this->pr_fd));
+ }
+
+ template<typename T>
+ Result<protocol_recv<PAYLOAD_TYPE, recv_payload_content>, std::string>
+ read_length(T& data) &&
+ {
+ static_assert(std::is_same<EXPECT, recv_payload_length>::value,
+ "read_length() cannot be called in this state");
+
+ if (readall(this->pr_fd, &this->pr_length, sizeof(this->pr_length))
+ == -1)
+ {
+ return Err(
+ fmt::format(FMT_STRING("unable to read content length: {}"),
+ strerror(errno)));
+ }
+
+ try {
+ data.resize(this->pr_length);
+ } catch (...) {
+ return Err(fmt::format(FMT_STRING("unable to resize data to {}"),
+ this->pr_length));
+ }
+
+ return Ok(protocol_recv<PAYLOAD_TYPE, recv_payload_content>(
+ this->pr_fd, this->pr_length));
+ }
+
+ template<typename T>
+ Result<void, std::string> read_content(T& data) &&
+ {
+ static_assert(std::is_same<EXPECT, recv_payload_content>::value,
+ "read_content() cannot be called in this state");
+ static_assert(!HAS_LENGTH || details::has_data<T>::value, "boo");
+
+ if (!HAS_LENGTH) {
+ this->pr_length = sizeof(T);
+ }
+ if (readall(this->pr_fd, details::get_data(data), this->pr_length)
+ == -1)
+ {
+ return Err(fmt::format(FMT_STRING("unable to read content -- {}"),
+ strerror(errno)));
+ }
+
+ return Ok();
+ }
+
+private:
+ template<int P, typename E>
+ friend struct protocol_recv;
+
+ explicit protocol_recv(int fd, int32_t length = 0)
+ : pr_fd(fd), pr_length(length)
+ {
+ }
+
+ int pr_fd;
+ int32_t pr_length;
+};
+
+inline Result<void, std::string>
+read_payloads_into(int fd)
+{
+ tailer_packet_payload_type_t payload_type;
+
+ readall(fd, &payload_type, sizeof(payload_type));
+ if (payload_type != TPPT_DONE) {
+ return Err(std::string("not done"));
+ }
+
+ return Ok();
+}
+
+template<typename... Ts>
+Result<void, std::string> read_payloads_into(int fd,
+ std::string& str,
+ Ts&... args);
+
+template<typename... Ts>
+Result<void, std::string>
+read_payloads_into(int fd, std::vector<uint8_t>& bits, Ts&... args)
+{
+ TRY(TRY(TRY(protocol_recv<TPPT_BITS>::create(fd)).read_length(bits))
+ .read_content(bits));
+
+ return read_payloads_into(fd, args...);
+}
+
+template<typename... Ts>
+Result<void, std::string>
+read_payloads_into(int fd, hash_frag& thf, Ts&... args)
+{
+ TRY(TRY(protocol_recv<TPPT_HASH>::create(fd)).read_content(thf.thf_hash));
+
+ return read_payloads_into(fd, args...);
+}
+
+template<typename... Ts>
+Result<void, std::string>
+read_payloads_into(int fd, int64_t& i, Ts&... args)
+{
+ TRY(TRY(protocol_recv<TPPT_INT64>::create(fd)).read_content(i));
+
+ return read_payloads_into(fd, args...);
+}
+
+template<typename... Ts>
+Result<void, std::string>
+read_payloads_into(int fd, std::string& str, Ts&... args)
+{
+ TRY(TRY(TRY(protocol_recv<TPPT_STRING>::create(fd)).read_length(str))
+ .read_content(str));
+
+ return read_payloads_into(fd, args...);
+}
+
+Result<packet, std::string> read_packet(int fd);
+
+} // namespace tailer
+
+#endif
diff --git a/src/tailer/test_tailer.sh b/src/tailer/test_tailer.sh
new file mode 100755
index 0000000..932fe24
--- /dev/null
+++ b/src/tailer/test_tailer.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+run_test ./drive_tailer preview nonexistent-file
+
+check_error_output "preview of nonexistent-file failed?" <<EOF
+preview error: nonexistent-file -- error: cannot open nonexistent-file -- No such file or directory
+tailer stderr:
+info: load preview request -- 1234
+info: exiting...
+EOF
+
+run_test ./drive_tailer preview ${test_dir}/logfile_access_log.0
+
+check_output "preview of file failed?" <<EOF
+preview of file: {test_dir}/logfile_access_log.0
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+
+all done!
+tailer stderr:
+info: load preview request -- 1234
+info: exiting...
+EOF
+
+run_cap_test ./drive_tailer preview "${test_dir}/remote-log-dir/*"
+
+run_test ./drive_tailer possible "${test_dir}/logfile_access_log.*"
+
+check_output "possible path list failed?" <<EOF
+possible path: {test_dir}/logfile_access_log.0
+possible path: {test_dir}/logfile_access_log.1
+all done!
+tailer stderr:
+complete path: {test_dir}/logfile_access_log.*
+complete glob path: {test_dir}/logfile_access_log.*
+info: exiting...
+EOF
+
+ln -sf bar foo
+
+run_test ./drive_tailer open foo
+
+check_output "open link not working?" <<EOF
+link value: foo -> bar
+all done!
+tailer stderr:
+info: monitoring path: foo
+info: exiting...
+EOF
diff --git a/src/term_extra.hh b/src/term_extra.hh
new file mode 100644
index 0000000..9740315
--- /dev/null
+++ b/src/term_extra.hh
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file term_extra.hh
+ */
+
+#ifndef term_extra_hh
+#define term_extra_hh
+
+#include <string>
+
+#include <pwd.h>
+#include <string.h>
+#include <sys/param.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "listview_curses.hh"
+#include "log_format.hh"
+#include "logfile.hh"
+
+class term_extra {
+public:
+ term_extra()
+ {
+ const char* term_name = getenv("TERM");
+
+ this->te_enabled
+ = (term_name != nullptr && strstr(term_name, "xterm") != nullptr);
+
+ if (getenv("SSH_CONNECTION") != nullptr) {
+ char hostname[MAXHOSTNAMELEN] = "UNKNOWN";
+ struct passwd* userent;
+
+ gethostname(hostname, sizeof(hostname));
+ this->te_prefix = hostname;
+ if ((userent = getpwuid(getuid())) != nullptr) {
+ this->te_prefix
+ = std::string(userent->pw_name) + "@" + this->te_prefix;
+ }
+ this->te_prefix += ":";
+ }
+ }
+
+ void update_title(listview_curses* lc)
+ {
+ static const char* xterm_title_fmt = "\033]0;%s\007";
+
+ if (!this->te_enabled) {
+ return;
+ }
+
+ if (lc->get_inner_height() > 0) {
+ std::vector<attr_line_t> rows(1);
+
+ lc->get_data_source()->listview_value_for_rows(
+ *lc, lc->get_top(), rows);
+ string_attrs_t& sa = rows[0].get_attrs();
+ auto line_attr_opt = get_string_attr(sa, logline::L_FILE);
+ if (line_attr_opt) {
+ auto lf = line_attr_opt.value().get();
+ const std::string& filename = lf->get_unique_path();
+
+ if (filename != this->te_last_title) {
+ std::string title = this->te_prefix + filename;
+
+ printf(xterm_title_fmt, title.c_str());
+ fflush(stdout);
+
+ this->te_last_title = filename;
+ }
+ return;
+ }
+ }
+
+ const std::string& view_title = lc->get_title();
+
+ if (view_title != this->te_last_title) {
+ std::string title = this->te_prefix + view_title;
+
+ printf(xterm_title_fmt, title.c_str());
+ fflush(stdout);
+
+ this->te_last_title = view_title;
+ }
+ }
+
+private:
+ bool te_enabled;
+ std::string te_prefix;
+ std::string te_last_title;
+};
+
+#endif
diff --git a/src/termios_guard.hh b/src/termios_guard.hh
new file mode 100644
index 0000000..64fc7df
--- /dev/null
+++ b/src/termios_guard.hh
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file termios_guard.hh
+ */
+
+#ifndef termios_guard_hh
+#define termios_guard_hh
+
+#include <stdio.h>
+#include <string.h>
+#include <termios.h>
+#include <unistd.h>
+
+/**
+ * RAII class that saves the current termios for a tty and then restores them
+ * during destruction.
+ */
+class guard_termios {
+public:
+ /**
+ * Store the TTY termios settings in this object.
+ *
+ * @param fd The tty file descriptor.
+ */
+ guard_termios(const int fd) : gt_fd(fd)
+ {
+ memset(&this->gt_termios, 0, sizeof(this->gt_termios));
+ if (isatty(this->gt_fd)
+ && tcgetattr(this->gt_fd, &this->gt_termios) == -1) {
+ perror("tcgetattr");
+ }
+ }
+
+ /**
+ * Restore the TTY termios settings that were captured when this object was
+ * instantiated.
+ */
+ ~guard_termios()
+ {
+ if (isatty(this->gt_fd)
+ && tcsetattr(this->gt_fd, TCSANOW, &this->gt_termios) == -1)
+ {
+ perror("tcsetattr");
+ }
+ }
+
+ const struct termios* get_termios() const { return &this->gt_termios; }
+
+private:
+ const int gt_fd;
+ struct termios gt_termios;
+};
+#endif
diff --git a/src/test_override.c b/src/test_override.c
new file mode 100644
index 0000000..23d8c79
--- /dev/null
+++ b/src/test_override.c
@@ -0,0 +1,56 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file test_override.c
+ */
+
+#include "config.h"
+
+#include <time.h>
+#define gettimeofday oldgtod
+#include <sys/time.h>
+#undef gettimeofday
+
+time_t time(time_t *loc)
+{
+ time_t retval = 1370546000;
+
+ if (loc != NULL) {
+ *loc = retval;
+ }
+
+ return retval;
+}
+
+int gettimeofday(struct timeval *tv, struct timezone *tz)
+{
+ tv->tv_sec = 1370546000;
+ tv->tv_usec = 123456;
+
+ return 0;
+}
diff --git a/src/text_anonymizer.cc b/src/text_anonymizer.cc
new file mode 100644
index 0000000..39403bf
--- /dev/null
+++ b/src/text_anonymizer.cc
@@ -0,0 +1,520 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "text_anonymizer.hh"
+
+#include <arpa/inet.h>
+#include <curl/curl.h>
+#include <netinet/in.h>
+
+#include "animals-json.h"
+#include "config.h"
+#include "data_scanner.hh"
+#include "diseases-json.h"
+#include "ghc/filesystem.hpp"
+#include "lnav_util.hh"
+#include "pcrepp/pcre2pp.hh"
+#include "words-json.h"
+#include "yajlpp/yajlpp_def.hh"
+
+namespace lnav {
+
+struct random_list {
+ std::vector<std::string> rl_data;
+
+ std::string at_index(size_t index) const
+ {
+ auto counter = index / this->rl_data.size();
+ auto mod = index % this->rl_data.size();
+
+ auto retval = this->rl_data[mod];
+ if (counter > 0) {
+ retval = fmt::format(FMT_STRING("{}{}"), retval, counter);
+ }
+ return retval;
+ }
+};
+
+static const typed_json_path_container<random_list> random_list_handlers = {
+ yajlpp::property_handler("data#").for_field(&random_list::rl_data),
+};
+
+static random_list
+load_word_list()
+{
+ static const intern_string_t name
+ = intern_string::lookup(words_json.get_name());
+ auto parse_res
+ = random_list_handlers.parser_for(name).with_ignore_unused(false).of(
+ words_json.to_string_fragment());
+
+ return parse_res.unwrap();
+}
+
+static const random_list&
+get_word_list()
+{
+ static const auto retval = load_word_list();
+
+ return retval;
+}
+
+static random_list
+load_animal_list()
+{
+ static const intern_string_t name
+ = intern_string::lookup(animals_json.get_name());
+ auto parse_res
+ = random_list_handlers.parser_for(name).with_ignore_unused(false).of(
+ animals_json.to_string_fragment());
+
+ return parse_res.unwrap();
+}
+
+static const random_list&
+get_animal_list()
+{
+ static const auto retval = load_animal_list();
+
+ return retval;
+}
+
+static random_list
+load_disease_list()
+{
+ static const intern_string_t name
+ = intern_string::lookup(diseases_json.get_name());
+ auto parse_res
+ = random_list_handlers.parser_for(name).with_ignore_unused(false).of(
+ diseases_json.to_string_fragment());
+
+ return parse_res.unwrap();
+}
+
+static const random_list&
+get_disease_list()
+{
+ static const auto retval = load_disease_list();
+
+ return retval;
+}
+
+std::string
+text_anonymizer::next(string_fragment line)
+{
+ data_scanner ds(line);
+ std::string retval;
+
+ while (true) {
+ auto tok_res = ds.tokenize2();
+ if (!tok_res) {
+ break;
+ }
+
+ switch (tok_res->tr_token) {
+ case DT_URL: {
+ auto url_str = tok_res->to_string();
+ auto_mem<CURLU> cu(curl_url_cleanup);
+ cu = curl_url();
+
+ if (curl_url_set(cu, CURLUPART_URL, url_str.c_str(), 0)
+ != CURLUE_OK)
+ {
+ retval += "<unparseable-url>";
+ } else {
+ auto_mem<char> url_part(curl_free);
+
+ if (curl_url_get(
+ cu, CURLUPART_USER, url_part.out(), CURLU_URLDECODE)
+ == CURLUE_OK)
+ {
+ auto anon_user = this->get_default(
+ this->ta_user_names,
+ url_part.in(),
+ [](size_t size, auto& user) {
+ return get_animal_list().at_index(size);
+ });
+ curl_url_set(cu,
+ CURLUPART_USER,
+ anon_user.c_str(),
+ CURLU_URLENCODE);
+ }
+
+ if (curl_url_get(cu,
+ CURLUPART_PASSWORD,
+ url_part.out(),
+ CURLU_URLDECODE)
+ == CURLUE_OK)
+ {
+ auto anon_pass
+ = hasher()
+ .update(url_part.in(), strlen(url_part.in()))
+ .to_string();
+ curl_url_set(cu,
+ CURLUPART_PASSWORD,
+ anon_pass.c_str(),
+ CURLU_URLENCODE);
+ }
+
+ if (curl_url_get(
+ cu, CURLUPART_HOST, url_part.out(), CURLU_URLDECODE)
+ == CURLUE_OK)
+ {
+ auto anon_host = this->get_default(
+ this->ta_host_names,
+ url_part.in(),
+ [](size_t size, auto& hn) {
+ const auto& diseases = get_disease_list();
+
+ return fmt::format(FMT_STRING("{}.example.com"),
+ diseases.at_index(size));
+ });
+ curl_url_set(cu,
+ CURLUPART_HOST,
+ anon_host.c_str(),
+ CURLU_URLENCODE);
+ }
+
+ if (curl_url_get(
+ cu, CURLUPART_PATH, url_part.out(), CURLU_URLDECODE)
+ == CURLUE_OK)
+ {
+ ghc::filesystem::path url_path(url_part.in());
+ ghc::filesystem::path anon_path;
+
+ for (const auto& comp : url_path) {
+ if (comp == comp.root_path()) {
+ anon_path = anon_path / comp;
+ continue;
+ }
+ anon_path = anon_path / this->next(comp.string());
+ }
+ curl_url_set(cu,
+ CURLUPART_PATH,
+ anon_path.c_str(),
+ CURLU_URLENCODE);
+ }
+
+ if (curl_url_get(cu,
+ CURLUPART_QUERY,
+ url_part.out(),
+ CURLU_URLDECODE)
+ == CURLUE_OK)
+ {
+ static const auto SPLIT_RE
+ = lnav::pcre2pp::code::from_const(R"((&))");
+
+ curl_url_set(cu, CURLUPART_QUERY, nullptr, 0);
+
+ auto url_query
+ = string_fragment::from_c_str(url_part.in());
+ auto replacer = [this, &cu](const std::string& comp) {
+ std::string anon_query;
+
+ auto eq_index = comp.find('=');
+ if (eq_index != std::string::npos) {
+ auto new_key
+ = this->next(comp.substr(0, eq_index));
+ auto new_value
+ = this->next(comp.substr(eq_index + 1));
+ anon_query = fmt::format(
+ FMT_STRING("{}={}"), new_key, new_value);
+ } else {
+ anon_query = this->next(comp);
+ }
+
+ curl_url_set(cu,
+ CURLUPART_QUERY,
+ anon_query.c_str(),
+ CURLU_URLENCODE | CURLU_APPENDQUERY);
+ };
+
+ auto loop_res
+ = SPLIT_RE.capture_from(url_query).for_each(
+ [&replacer](lnav::pcre2pp::match_data& md) {
+ replacer(md.leading().to_string());
+ });
+ if (loop_res.isOk()) {
+ replacer(loop_res.unwrap().to_string());
+ }
+ }
+
+ if (curl_url_get(cu,
+ CURLUPART_FRAGMENT,
+ url_part.out(),
+ CURLU_URLDECODE)
+ == CURLUE_OK)
+ {
+ auto anon_frag = this->next(
+ string_fragment::from_c_str(url_part.in()));
+
+ curl_url_set(cu,
+ CURLUPART_FRAGMENT,
+ anon_frag.c_str(),
+ CURLU_URLENCODE);
+ }
+
+ auto_mem<char> anon_url(curl_free);
+ if (curl_url_get(cu, CURLUPART_URL, anon_url.out(), 0)
+ == CURLUE_OK)
+ {
+ retval.append(anon_url.in());
+ }
+ }
+ break;
+ }
+ case DT_PATH: {
+ ghc::filesystem::path inp_path(tok_res->to_string());
+ ghc::filesystem::path anon_path;
+
+ for (const auto& comp : inp_path) {
+ auto comp_str = comp.string();
+ if (comp == comp.root_path() || comp == inp_path) {
+ anon_path = anon_path / comp;
+ continue;
+ }
+ anon_path = anon_path / this->next(comp_str);
+ }
+
+ retval += anon_path.string();
+ break;
+ }
+ case DT_CREDIT_CARD_NUMBER: {
+ auto cc = tok_res->to_string();
+ auto has_spaces = cc.size() > 16;
+ auto new_end = std::remove_if(
+ cc.begin(), cc.end(), [](auto ch) { return ch == ' '; });
+ cc.erase(new_end, cc.end());
+ auto anon_cc = hasher().update(cc).to_string().substr(0, 16);
+
+ if (has_spaces) {
+ anon_cc.insert(12, " ");
+ anon_cc.insert(8, " ");
+ anon_cc.insert(4, " ");
+ }
+
+ retval += anon_cc;
+ break;
+ }
+ case DT_MAC_ADDRESS: {
+ // 00-00-5E-00-53-00
+ auto mac_addr = tok_res->to_string();
+
+ retval += this->get_default(
+ this->ta_mac_addresses,
+ mac_addr,
+ [](size_t size, auto& inp) {
+ uint32_t base_mac = 0x5e005300;
+
+ base_mac += size;
+ auto anon_mac = byte_array<6>::from({
+ 0x00,
+ 0x00,
+ (unsigned char) ((base_mac >> 24) & 0xff),
+ (unsigned char) ((base_mac >> 16) & 0xff),
+ (unsigned char) ((base_mac >> 8) & 0xff),
+ (unsigned char) ((base_mac >> 0) & 0xff),
+ });
+
+ return anon_mac.to_string(
+ nonstd::make_optional(inp[2]));
+ });
+ break;
+ }
+ case DT_HEX_DUMP: {
+ auto hex_str = tok_res->to_string();
+ auto hash_str = hasher().update(hex_str).to_array().to_string(
+ nonstd::make_optional(hex_str[2]));
+ std::string anon_hex;
+
+ while (anon_hex.size() < hex_str.size()) {
+ anon_hex += hash_str;
+ }
+ anon_hex.resize(hex_str.size());
+
+ retval += anon_hex;
+ break;
+ }
+ case DT_IPV4_ADDRESS: {
+ auto ipv4 = tok_res->to_string();
+ retval += this->get_default(
+ this->ta_ipv4_addresses, ipv4, [](size_t size, auto& _) {
+ char anon_ipv4[INET_ADDRSTRLEN];
+ struct in_addr ia;
+
+ inet_aton("10.0.0.0", &ia);
+ ia.s_addr = htonl(ntohl(ia.s_addr) + 1 + size);
+ inet_ntop(AF_INET, &ia, anon_ipv4, sizeof(anon_ipv4));
+ return std::string{anon_ipv4};
+ });
+ break;
+ }
+ case DT_IPV6_ADDRESS: {
+ auto ipv6 = tok_res->to_string();
+ retval += this->get_default(
+ this->ta_ipv6_addresses, ipv6, [](size_t size, auto& _) {
+ char anon_ipv6[INET6_ADDRSTRLEN];
+ struct in6_addr ia;
+ uint32_t* ia6_addr32 = (uint32_t*) &ia.s6_addr[12];
+
+ inet_pton(AF_INET6, "2001:db8::", &ia);
+ *ia6_addr32 = htonl(ntohl(*ia6_addr32) + 1 + size);
+ inet_ntop(AF_INET6, &ia, anon_ipv6, sizeof(anon_ipv6));
+ return std::string{anon_ipv6};
+ });
+ break;
+ }
+ case DT_EMAIL: {
+ auto email_addr = tok_res->to_string();
+ auto at_index = email_addr.find('@');
+
+ retval += fmt::format(
+ FMT_STRING("{}@{}.example.com"),
+ this->get_default(this->ta_user_names,
+ email_addr.substr(0, at_index),
+ [](auto size, const auto& inp) {
+ return get_animal_list().at_index(
+ size);
+ }),
+ this->get_default(this->ta_host_names,
+ email_addr.substr(at_index + 1),
+ [](auto size, const auto& inp) {
+ return get_disease_list().at_index(
+ size);
+ }));
+ break;
+ }
+ case DT_WORD:
+ case DT_SYMBOL: {
+ static const auto SPLIT_RE = lnav::pcre2pp::code::from_const(
+ R"((\.|::|_|-|/|\\|\d+))");
+ auto symbol_frag = ds.to_string_fragment(tok_res->tr_capture);
+ auto sym_provider = [](auto size, const auto& inp) {
+ if (inp.size() <= 4) {
+ return inp;
+ }
+
+ auto comp_frag = string_fragment::from_str(inp);
+ return string_fragment::from_str(
+ get_word_list().at_index(size))
+ .to_string_with_case_style(
+ comp_frag.detect_text_case_style());
+ };
+
+ auto cap_res
+ = SPLIT_RE.capture_from(symbol_frag)
+ .for_each([this, &retval, &sym_provider](
+ lnav::pcre2pp::match_data& md) {
+ auto comp = md.leading().to_string();
+ retval
+ += this->get_default(
+ this->ta_symbols, comp, sym_provider)
+ + md[0]->to_string();
+ });
+ if (cap_res.isErr()) {
+ retval += "<symbol>";
+ } else {
+ auto remaining = cap_res.unwrap().to_string();
+
+ retval += this->get_default(
+ this->ta_symbols, remaining, sym_provider);
+ }
+ break;
+ }
+ case DT_QUOTED_STRING: {
+ auto anon_inner = this->next(
+ ds.to_string_fragment(tok_res->tr_inner_capture)
+ .to_string());
+
+ retval += line.sub_range(tok_res->tr_capture.c_begin,
+ tok_res->tr_inner_capture.c_begin)
+ .to_string()
+ + anon_inner
+ + ds.to_string_fragment(tok_res->tr_capture).back();
+ break;
+ }
+ case DT_XML_OPEN_TAG: {
+ auto open_tag = tok_res->to_string();
+ auto space_index = open_tag.find(' ');
+
+ if (space_index == std::string::npos) {
+ retval += open_tag;
+ } else {
+ static const auto ATTR_RE
+ = lnav::pcre2pp::code::from_const(R"([\w\-]+=)");
+ static thread_local auto md
+ = lnav::pcre2pp::match_data::unitialized();
+
+ auto remaining = string_fragment::from_str_range(
+ open_tag, space_index, open_tag.size());
+
+ retval += open_tag.substr(0, space_index + 1);
+ while (!remaining.empty()) {
+ auto cap_res = ATTR_RE.capture_from(remaining)
+ .into(md)
+ .matches()
+ .ignore_error();
+
+ if (!cap_res) {
+ break;
+ }
+
+ retval += md.leading();
+ retval += md[0]->to_string();
+ remaining = md.remaining();
+ data_scanner ds(remaining);
+ auto attr_tok_res = ds.tokenize2();
+ if (!attr_tok_res) {
+ continue;
+ }
+ retval += this->next(attr_tok_res->to_string());
+ remaining = remaining.substr(
+ attr_tok_res->tr_capture.length());
+ }
+
+ retval += remaining.to_string();
+ }
+ break;
+ }
+ case DT_UUID: {
+ retval
+ += hasher().update(tok_res->to_string()).to_uuid_string();
+ break;
+ }
+ default: {
+ retval += tok_res->to_string();
+ break;
+ }
+ }
+ }
+
+ return retval;
+}
+
+} // namespace lnav
diff --git a/src/text_anonymizer.hh b/src/text_anonymizer.hh
new file mode 100644
index 0000000..124e20d
--- /dev/null
+++ b/src/text_anonymizer.hh
@@ -0,0 +1,75 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_text_anonymizer_hh
+#define lnav_text_anonymizer_hh
+
+#include <string>
+#include <vector>
+
+#include "base/intern_string.hh"
+#include "robin_hood/robin_hood.h"
+
+namespace lnav {
+
+class text_anonymizer {
+public:
+ text_anonymizer() = default;
+
+ std::string next(string_fragment line);
+
+private:
+ template<typename F>
+ const std::string& get_default(
+ robin_hood::unordered_map<std::string, std::string>& mapping,
+ const std::string& input,
+ F provider)
+ {
+ auto iter = mapping.find(input);
+ if (iter == mapping.end()) {
+ auto emp_res = mapping.template emplace(
+ input, provider(mapping.size(), input));
+
+ iter = emp_res.first;
+ }
+
+ return iter->second;
+ }
+
+ robin_hood::unordered_map<std::string, std::string> ta_mac_addresses;
+ robin_hood::unordered_map<std::string, std::string> ta_ipv4_addresses;
+ robin_hood::unordered_map<std::string, std::string> ta_ipv6_addresses;
+ robin_hood::unordered_map<std::string, std::string> ta_user_names;
+ robin_hood::unordered_map<std::string, std::string> ta_host_names;
+ robin_hood::unordered_map<std::string, std::string> ta_symbols;
+};
+
+} // namespace lnav
+
+#endif
diff --git a/src/text_format.cc b/src/text_format.cc
new file mode 100644
index 0000000..0b72786
--- /dev/null
+++ b/src/text_format.cc
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file text_format.cc
+ */
+
+#include "text_format.hh"
+
+#include "config.h"
+#include "pcrepp/pcre2pp.hh"
+#include "yajl/api/yajl_parse.h"
+
+text_format_t
+detect_text_format(string_fragment sf,
+ nonstd::optional<ghc::filesystem::path> path)
+{
+ static const auto GZ_EXT = ghc::filesystem::path(".gz");
+ static const auto BZ2_EXT = ghc::filesystem::path(".bz2");
+ static const auto MD_EXT = ghc::filesystem::path(".md");
+ static const auto MARKDOWN_EXT = ghc::filesystem::path(".markdown");
+
+ static const auto MAN_MATCHERS = lnav::pcre2pp::code::from_const(
+ R"(^[A-Za-z][A-Za-z\-_\+0-9]+\(\d\)\s+)", PCRE2_MULTILINE);
+
+ // XXX This is a pretty crude way of detecting format...
+ static const auto PYTHON_MATCHERS = lnav::pcre2pp::code::from_const(
+ "(?:"
+ "^\\s*def\\s+\\w+\\([^)]*\\):[^\\n]*$|"
+ "^\\s*try:[^\\n]*$"
+ ")",
+ PCRE2_MULTILINE);
+
+ static const auto RUST_MATCHERS
+ = lnav::pcre2pp::code::from_const(R"(
+(?:
+^\s*use\s+[\w+:\{\}]+;$|
+^\s*(?:pub)?\s+(?:const|enum|fn)\s+\w+.*$|
+^\s*impl\s+\w+.*$
+)
+)",
+ PCRE2_MULTILINE);
+
+ static const auto JAVA_MATCHERS = lnav::pcre2pp::code::from_const(
+ "(?:"
+ "^package\\s+|"
+ "^import\\s+|"
+ "^\\s*(?:public)?\\s*class\\s*(\\w+\\s+)*\\s*{"
+ ")",
+ PCRE2_MULTILINE);
+
+ static const auto C_LIKE_MATCHERS = lnav::pcre2pp::code::from_const(
+ "(?:"
+ "^#\\s*include\\s+|"
+ "^#\\s*define\\s+|"
+ "^\\s*if\\s+\\([^)]+\\)[^\\n]*$|"
+ "^\\s*(?:\\w+\\s+)*class \\w+ {"
+ ")",
+ PCRE2_MULTILINE);
+
+ static const auto SQL_MATCHERS = lnav::pcre2pp::code::from_const(
+ "(?:"
+ "select\\s+.+\\s+from\\s+|"
+ "insert\\s+into\\s+.+\\s+values"
+ ")",
+ PCRE2_MULTILINE | PCRE2_CASELESS);
+
+ static const auto XML_MATCHERS = lnav::pcre2pp::code::from_const(
+ "(?:"
+ R"(<\?xml(\s+\w+\s*=\s*"[^"]*")*\?>|)"
+ R"(</?\w+(\s+\w+\s*=\s*"[^"]*")*\s*>)"
+ ")",
+ PCRE2_MULTILINE | PCRE2_CASELESS);
+
+ text_format_t retval = text_format_t::TF_UNKNOWN;
+
+ if (path) {
+ if (path->extension() == GZ_EXT) {
+ path = path->stem();
+ }
+ if (path->extension() == BZ2_EXT) {
+ path = path->stem();
+ }
+
+ if (path->extension() == MD_EXT || path->extension() == MARKDOWN_EXT) {
+ return text_format_t::TF_MARKDOWN;
+ }
+ }
+
+ {
+ auto_mem<yajl_handle_t> jhandle(yajl_free);
+
+ jhandle = yajl_alloc(nullptr, nullptr, nullptr);
+ if (yajl_parse(jhandle, sf.udata(), sf.length()) == yajl_status_ok) {
+ return text_format_t::TF_JSON;
+ }
+ }
+
+ if (MAN_MATCHERS.find_in(sf).ignore_error()) {
+ return text_format_t::TF_MAN;
+ }
+
+ if (PYTHON_MATCHERS.find_in(sf).ignore_error()) {
+ return text_format_t::TF_PYTHON;
+ }
+
+ if (RUST_MATCHERS.find_in(sf).ignore_error()) {
+ return text_format_t::TF_RUST;
+ }
+
+ if (JAVA_MATCHERS.find_in(sf).ignore_error()) {
+ return text_format_t::TF_JAVA;
+ }
+
+ if (C_LIKE_MATCHERS.find_in(sf).ignore_error()) {
+ return text_format_t::TF_C_LIKE;
+ }
+
+ if (SQL_MATCHERS.find_in(sf).ignore_error()) {
+ return text_format_t::TF_SQL;
+ }
+
+ if (XML_MATCHERS.find_in(sf).ignore_error()) {
+ return text_format_t::TF_XML;
+ }
+
+ return retval;
+}
diff --git a/src/text_format.hh b/src/text_format.hh
new file mode 100644
index 0000000..a923fb3
--- /dev/null
+++ b/src/text_format.hh
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file text_format.hh
+ */
+
+#ifndef text_format_hh
+#define text_format_hh
+
+#include <string>
+
+#include <sys/types.h>
+
+#include "base/intern_string.hh"
+#include "fmt/format.h"
+#include "ghc/filesystem.hpp"
+
+enum class text_format_t {
+ TF_UNKNOWN,
+ TF_BINARY,
+ TF_C_LIKE,
+ TF_JAVA,
+ TF_JSON,
+ TF_LOG,
+ TF_MAN,
+ TF_MARKDOWN,
+ TF_PYTHON,
+ TF_RUST,
+ TF_SQL,
+ TF_XML,
+ TF_YAML,
+ TF_TOML,
+};
+
+namespace fmt {
+template<>
+struct formatter<text_format_t> : formatter<string_view> {
+ template<typename FormatContext>
+ auto format(text_format_t tf, FormatContext& ctx)
+ {
+ string_view name = "unknown";
+ switch (tf) {
+ case text_format_t::TF_UNKNOWN:
+ name = "text/plain";
+ break;
+ case text_format_t::TF_BINARY:
+ name = "application/octet-stream";
+ break;
+ case text_format_t::TF_LOG:
+ name = "text/log";
+ break;
+ case text_format_t::TF_PYTHON:
+ name = "text/python";
+ break;
+ case text_format_t::TF_RUST:
+ name = "text/rust";
+ break;
+ case text_format_t::TF_JAVA:
+ name = "text/java";
+ break;
+ case text_format_t::TF_C_LIKE:
+ name = "text/c";
+ break;
+ case text_format_t::TF_SQL:
+ name = "application/sql";
+ break;
+ case text_format_t::TF_XML:
+ name = "text/xml";
+ break;
+ case text_format_t::TF_JSON:
+ name = "application/json";
+ break;
+ case text_format_t::TF_MAN:
+ name = "text/man";
+ break;
+ case text_format_t::TF_MARKDOWN:
+ name = "text/markdown";
+ break;
+ case text_format_t::TF_YAML:
+ name = "application/yaml";
+ break;
+ case text_format_t::TF_TOML:
+ name = "application/toml";
+ break;
+ }
+ return formatter<string_view>::format(name, ctx);
+ }
+};
+} // namespace fmt
+
+/**
+ * Try to detect the format of the given text file fragment.
+ *
+ * @return The detected format.
+ */
+text_format_t detect_text_format(string_fragment sf,
+ nonstd::optional<ghc::filesystem::path> path
+ = nonstd::nullopt);
+
+#endif
diff --git a/src/textfile_highlighters.cc b/src/textfile_highlighters.cc
new file mode 100644
index 0000000..b0d2c52
--- /dev/null
+++ b/src/textfile_highlighters.cc
@@ -0,0 +1,464 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <string>
+
+#include "textfile_highlighters.hh"
+
+#include "config.h"
+
+template<typename T, std::size_t N>
+static std::shared_ptr<lnav::pcre2pp::code>
+xpcre_compile(const T (&pattern)[N], int options = 0)
+{
+ return lnav::pcre2pp::code::from_const(pattern, options).to_shared();
+}
+
+void
+setup_highlights(highlight_map_t& hm)
+{
+ hm[{highlight_source_t::INTERNAL, "python"}]
+ = highlighter(xpcre_compile("(?:"
+ "\\bFalse\\b|"
+ "\\bNone\\b|"
+ "\\bTrue\\b|"
+ "\\band\\b|"
+ "\\bas\\b|"
+ "\\bassert\\b|"
+ "\\bbreak\\b|"
+ "\\bclass\\b|"
+ "\\bcontinue\\b|"
+ "\\bdef\\b|"
+ "\\bdel\\b|"
+ "\\belif\\b|"
+ "\\belse\\b|"
+ "\\bexcept\\b|"
+ "\\bfinally\\b|"
+ "\\bfor\\b|"
+ "\\bfrom\\b|"
+ "\\bglobal\\b|"
+ "\\bif\\b|"
+ "\\bimport\\b|"
+ "\\bin\\b|"
+ "\\bis\\b|"
+ "\\blambda\\b|"
+ "\\bnonlocal\\b|"
+ "\\bnot\\b|"
+ "\\bor\\b|"
+ "\\bpass\\b|"
+ "\\bprint\\b|"
+ "\\braise\\b|"
+ "\\breturn\\b|"
+ "\\btry\\b|"
+ "\\bwhile\\b|"
+ "\\bwith\\b|"
+ "\\byield\\b"
+ ")"))
+ .with_nestable(false)
+ .with_text_format(text_format_t::TF_PYTHON)
+ .with_role(role_t::VCR_KEYWORD);
+
+ hm[{highlight_source_t::INTERNAL, "rust"}]
+ = highlighter(xpcre_compile("(?:"
+ "\\bas\\b|"
+ "\\buse\\b|"
+ "\\bextern crate\\b|"
+ "\\bbreak\\b|"
+ "\\bconst\\b|"
+ "\\bcontinue\\b|"
+ "\\bcrate\\b|"
+ "\\belse\\b|"
+ "\\bif\\b|"
+ "\\bif let\\b|"
+ "\\benum\\b|"
+ "\\bextern\\b|"
+ "\\bfalse\\b|"
+ "\\bfn\\b|"
+ "\\bfor\\b|"
+ "\\bif\\b|"
+ "\\bimpl\\b|"
+ "\\bin\\b|"
+ "\\bfor\\b|"
+ "\\blet\\b|"
+ "\\bloop\\b|"
+ "\\bmatch\\b|"
+ "\\bmod\\b|"
+ "\\bmove\\b|"
+ "\\bmut\\b|"
+ "\\bpub\\b|"
+ "\\bimpl\\b|"
+ "\\bref\\b|"
+ "\\breturn\\b|"
+ "\\bSelf\\b|"
+ "\\bself\\b|"
+ "\\bstatic\\b|"
+ "\\bstruct\\b|"
+ "\\bsuper\\b|"
+ "\\btrait\\b|"
+ "\\btrue\\b|"
+ "\\btype\\b|"
+ "\\bunsafe\\b|"
+ "\\buse\\b|"
+ "\\bwhere\\b|"
+ "\\bwhile\\b|"
+ "\\babstract\\b|"
+ "\\balignof\\b|"
+ "\\bbecome\\b|"
+ "\\bbox\\b|"
+ "\\bdo\\b|"
+ "\\bfinal\\b|"
+ "\\bmacro\\b|"
+ "\\boffsetof\\b|"
+ "\\boverride\\b|"
+ "\\bpriv\\b|"
+ "\\bproc\\b|"
+ "\\bpure\\b|"
+ "\\bsizeof\\b|"
+ "\\btypeof\\b|"
+ "\\bunsized\\b|"
+ "\\bvirtual\\b|"
+ "\\byield\\b"
+ ")"))
+ .with_nestable(false)
+ .with_text_format(text_format_t::TF_RUST)
+ .with_role(role_t::VCR_KEYWORD);
+
+ hm[{highlight_source_t::INTERNAL, "clike"}]
+ = highlighter(xpcre_compile("(?:"
+ "\\babstract\\b|"
+ "\\bassert\\b|"
+ "\\basm\\b|"
+ "\\bauto\\b|"
+ "\\bbool\\b|"
+ "\\bbooleanif\\b|"
+ "\\bbreak\\b|"
+ "\\bbyte\\b|"
+ "\\bcase\\b|"
+ "\\bcatch\\b|"
+ "\\bchar\\b|"
+ "\\bclass\\b|"
+ "\\bconst\\b|"
+ "\\bconstexpr\\b|"
+ "\\bconst_cast\\b|"
+ "\\bcontinue\\b|"
+ "\\bdecltype\\b|"
+ "\\bdefault\\b|"
+ "\\bdelete\\b|"
+ "\\bdo\\b|"
+ "\\bdouble\\b|"
+ "\\bdynamic_cast\\b|"
+ "\\belse\\b|"
+ "\\benum\\b|"
+ "\\bexplicit\\b|"
+ "\\bextends\\b|"
+ "\\bextern\\b|"
+ "\\bfalse\\b|"
+ "\\bfinal\\b|"
+ "\\bfinally\\b|"
+ "\\bfloat\\b|"
+ "\\bfor\\b|"
+ "\\bfriend\\b|"
+ "\\bgoto\\b|"
+ "\\bif\\b|"
+ "\\bimplements\\b|"
+ "\\bimport\\b|"
+ "\\binline\\b|"
+ "\\binstanceof\\b|"
+ "\\bint\\b|"
+ "\\binterface\\b|"
+ "\\blong\\b|"
+ "\\bmutable\\b|"
+ "\\bnamespace\\b|"
+ "\\bnative\\b|"
+ "\\bnew\\b|"
+ "\\bnoexcept\\b|"
+ "\\bnullptr\\b|"
+ "\\boperator\\b|"
+ "\\bpackage\\b|"
+ "\\bprivate\\b|"
+ "\\bprotected\\b|"
+ "\\bpublic\\b|"
+ "\\breinterpret_cast\\b|"
+ "\\bregister\\b|"
+ "\\breturn\\b|"
+ "\\bshort\\b|"
+ "\\bsigned\\b|"
+ "\\bsizeof\\b|"
+ "\\bstatic\\b|"
+ "\\bstatic_cast\\b|"
+ "\\bstrictfp\\b|"
+ "\\bstruct\\b|"
+ "\\bsuper\\b|"
+ "\\bswitch\\b|"
+ "\\bsynchronized\\b|"
+ "\\btemplate\\b|"
+ "\\bthis\\b|"
+ "\\bthread_local\\b|"
+ "\\bthrow\\b|"
+ "\\bthrows\\b|"
+ "\\btransient\\b|"
+ "\\btry\\b|"
+ "\\btrue\\b|"
+ "\\btypedef\\b|"
+ "\\btypeid\\b|"
+ "\\btypename\\b|"
+ "\\bunion\\b|"
+ "\\bunsigned\\b|"
+ "\\busing\\b|"
+ "\\bvirtual\\b|"
+ "\\bvoid\\b|"
+ "\\bvolatile\\b|"
+ "\\bwchar_t\\b|"
+ "\\bwhile\\b"
+ ")"))
+ .with_nestable(false)
+ .with_text_format(text_format_t::TF_C_LIKE)
+ .with_text_format(text_format_t::TF_JAVA)
+ .with_role(role_t::VCR_KEYWORD);
+
+ hm[{highlight_source_t::INTERNAL, "sql.0.comment"}]
+ = highlighter(xpcre_compile("(?:(?<=[\\s;])|^)--.*"))
+ .with_text_format(text_format_t::TF_SQL)
+ .with_role(role_t::VCR_COMMENT);
+ hm[{highlight_source_t::INTERNAL, "sql.9.keyword"}]
+ = highlighter(xpcre_compile("(?:"
+ "\\bABORT\\b|"
+ "\\bACTION\\b|"
+ "\\bADD\\b|"
+ "\\bAFTER\\b|"
+ "\\bALL\\b|"
+ "\\bALTER\\b|"
+ "\\bANALYZE\\b|"
+ "\\bAND\\b|"
+ "\\bAS\\b|"
+ "\\bASC\\b|"
+ "\\bATTACH\\b|"
+ "\\bAUTOINCREMENT\\b|"
+ "\\bBEFORE\\b|"
+ "\\bBEGIN\\b|"
+ "\\bBETWEEN\\b|"
+ "\\bBOOLEAN\\b|"
+ "\\bBY\\b|"
+ "\\bCASCADE\\b|"
+ "\\bCASE\\b|"
+ "\\bCAST\\b|"
+ "\\bCHECK\\b|"
+ "\\bCOLLATE\\b|"
+ "\\bCOLUMN\\b|"
+ "\\bCOMMIT\\b|"
+ "\\bCONFLICT\\b|"
+ "\\bCONSTRAINT\\b|"
+ "\\bCREATE\\b|"
+ "\\bCROSS\\b|"
+ "\\bCURRENT_DATE\\b|"
+ "\\bCURRENT_TIME\\b|"
+ "\\bCURRENT_TIMESTAMP\\b|"
+ "\\bDATABASE\\b|"
+ "\\bDATETIME\\b|"
+ "\\bDEFAULT\\b|"
+ "\\bDEFERRABLE\\b|"
+ "\\bDEFERRED\\b|"
+ "\\bDELETE\\b|"
+ "\\bDESC\\b|"
+ "\\bDETACH\\b|"
+ "\\bDISTINCT\\b|"
+ "\\bDROP\\b|"
+ "\\bEACH\\b|"
+ "\\bELSE\\b|"
+ "\\bEND\\b|"
+ "\\bESCAPE\\b|"
+ "\\bEXCEPT\\b|"
+ "\\bEXCLUSIVE\\b|"
+ "\\bEXISTS\\b|"
+ "\\bEXPLAIN\\b|"
+ "\\bFAIL\\b|"
+ "\\bFLOAT\\b|"
+ "\\bFOR\\b|"
+ "\\bFOREIGN\\b|"
+ "\\bFROM\\b|"
+ "\\bFULL\\b|"
+ "\\bGLOB\\b|"
+ "\\bGROUP\\b|"
+ "\\bHAVING\\b|"
+ "\\bHIDDEN\\b|"
+ "\\bIF\\b|"
+ "\\bIGNORE\\b|"
+ "\\bIMMEDIATE\\b|"
+ "\\bIN\\b|"
+ "\\bINDEX\\b|"
+ "\\bINDEXED\\b|"
+ "\\bINITIALLY\\b|"
+ "\\bINNER\\b|"
+ "\\bINSERT\\b|"
+ "\\bINSTEAD\\b|"
+ "\\bINTEGER\\b|"
+ "\\bINTERSECT\\b|"
+ "\\bINTO\\b|"
+ "\\bIS\\b|"
+ "\\bISNULL\\b|"
+ "\\bJOIN\\b|"
+ "\\bKEY\\b|"
+ "\\bLEFT\\b|"
+ "\\bLIKE\\b|"
+ "\\bLIMIT\\b|"
+ "\\bMATCH\\b|"
+ "\\bNATURAL\\b|"
+ "\\bNO\\b|"
+ "\\bNOT\\b|"
+ "\\bNOTNULL\\b|"
+ "\\bNULL\\b|"
+ "\\bOF\\b|"
+ "\\bOFFSET\\b|"
+ "\\bON\\b|"
+ "\\bOR\\b|"
+ "\\bORDER\\b|"
+ "\\bOUTER\\b|"
+ "\\bPLAN\\b|"
+ "\\bPRAGMA\\b|"
+ "\\bPRIMARY\\b|"
+ "\\bQUERY\\b|"
+ "\\bRAISE\\b|"
+ "\\bRECURSIVE\\b|"
+ "\\bREFERENCES\\b|"
+ "\\bREGEXP\\b|"
+ "\\bREINDEX\\b|"
+ "\\bRELEASE\\b|"
+ "\\bRENAME\\b|"
+ "\\bREPLACE\\b|"
+ "\\bRESTRICT\\b|"
+ "\\bRIGHT\\b|"
+ "\\bROLLBACK\\b|"
+ "\\bROW\\b|"
+ "\\bSAVEPOINT\\b|"
+ "\\bSELECT\\b|"
+ "\\bSET\\b|"
+ "\\bTABLE\\b|"
+ "\\bTEMP\\b|"
+ "\\bTEMPORARY\\b|"
+ "\\bTEXT\\b|"
+ "\\bTHEN\\b|"
+ "\\bTO\\b|"
+ "\\bTRANSACTION\\b|"
+ "\\bTRIGGER\\b|"
+ "\\bUNION\\b|"
+ "\\bUNIQUE\\b|"
+ "\\bUPDATE\\b|"
+ "\\bUSING\\b|"
+ "\\bVACUUM\\b|"
+ "\\bVALUES\\b|"
+ "\\bVIEW\\b|"
+ "\\bVIRTUAL\\b|"
+ "\\bWHEN\\b|"
+ "\\bWHERE\\b|"
+ "\\bWITH\\b|"
+ "\\bWITHOUT\\b"
+ ")",
+ PCRE2_CASELESS))
+ .with_nestable(false)
+ .with_text_format(text_format_t::TF_SQL)
+ .with_role(role_t::VCR_KEYWORD);
+
+ hm[{highlight_source_t::INTERNAL, "srcfile"}]
+ = highlighter(xpcre_compile(
+ "[\\w\\-_]+\\."
+ "(?:java|a|o|so|c|cc|cpp|cxx|h|hh|hpp|hxx|py|pyc|rb):"
+ "\\d+"))
+ .with_role(role_t::VCR_FILE);
+ hm[{highlight_source_t::INTERNAL, "1.stringd"}]
+ = highlighter(xpcre_compile(R"("(?:\\.|[^"])*")"))
+ .with_nestable(false)
+ .with_role(role_t::VCR_STRING);
+ hm[{highlight_source_t::INTERNAL, "1.strings"}]
+ = highlighter(xpcre_compile(R"((?<![A-WY-Za-qstv-z])'(?:\\.|[^'])*')"))
+ .with_nestable(false)
+ .with_role(role_t::VCR_STRING);
+ hm[{highlight_source_t::INTERNAL, "1.stringb"}]
+ = highlighter(xpcre_compile("`(?:\\\\.|[^`])*`"))
+ .with_nestable(false)
+ .with_role(role_t::VCR_STRING);
+ hm[{highlight_source_t::INTERNAL, "diffp"}]
+ = highlighter(xpcre_compile("^\\+.*")).with_role(role_t::VCR_DIFF_ADD);
+ hm[{highlight_source_t::INTERNAL, "diffm"}]
+ = highlighter(xpcre_compile("^(?:--- .*|-$|-[^-].*)"))
+ .with_role(role_t::VCR_DIFF_DELETE);
+ hm[{highlight_source_t::INTERNAL, "diffs"}]
+ = highlighter(xpcre_compile("^\\@@ .*"))
+ .with_role(role_t::VCR_DIFF_SECTION);
+ hm[{highlight_source_t::INTERNAL, "0.comment"}]
+ = highlighter(
+ xpcre_compile(
+ R"((?<=[\s;])//.*|/\*.*\*/|\(\*.*\*\)|^#\s*(?!include|if|ifndef|elif|else|endif|error|pragma|define|undef).*|\s+#.*|dnl.*)"))
+ .with_nestable(false)
+ .with_role(role_t::VCR_COMMENT);
+ hm[{highlight_source_t::INTERNAL, "javadoc"}]
+ = highlighter(
+ xpcre_compile("@(?:author|deprecated|exception|file|param|return|"
+ "see|since|throws|todo|version)"))
+ .with_role(role_t::VCR_DOC_DIRECTIVE);
+ hm[{highlight_source_t::INTERNAL, "var"}]
+ = highlighter(
+ xpcre_compile("(?:"
+ "(?:var\\s+)?([\\-\\w]+)\\s*[!=+\\-*/|&^]?=|"
+ "(?<!\\$)\\$(\\w+)|"
+ "(?<!\\$)\\$\\((\\w+)\\)|"
+ "(?<!\\$)\\$\\{(\\w+)\\}"
+ ")"))
+ .with_nestable(false)
+ .with_role(role_t::VCR_VARIABLE);
+ hm[{highlight_source_t::INTERNAL, "rust.sym"}]
+ = highlighter(xpcre_compile("\\b[A-Z_][A-Z0-9_]+\\b"))
+ .with_nestable(false)
+ .with_text_format(text_format_t::TF_RUST)
+ .with_role(role_t::VCR_SYMBOL);
+ hm[{highlight_source_t::INTERNAL, "rust.num"}]
+ = highlighter(xpcre_compile(R"(\b-?(?:\d+|0x[a-zA-Z0-9]+)\b)"))
+ .with_nestable(false)
+ .with_text_format(text_format_t::TF_RUST)
+ .with_role(role_t::VCR_NUMBER);
+ hm[{highlight_source_t::INTERNAL, "sym"}]
+ = highlighter(xpcre_compile("\\b[A-Z_][A-Z0-9_]+\\b"))
+ .with_nestable(false)
+ .with_text_format(text_format_t::TF_C_LIKE)
+ .with_text_format(text_format_t::TF_JAVA)
+ .with_role(role_t::VCR_SYMBOL);
+ hm[{highlight_source_t::INTERNAL, "cpp"}]
+ = highlighter(
+ xpcre_compile(
+ R"(^#\s*(?:include|ifdef|ifndef|if|else|elif|error|endif|define|undef|pragma))"))
+ .with_nestable(false)
+ .with_text_format(text_format_t::TF_C_LIKE)
+ .with_text_format(text_format_t::TF_JAVA)
+ .with_role(role_t::VCR_KEYWORD);
+ hm[{highlight_source_t::INTERNAL, "num"}]
+ = highlighter(xpcre_compile(R"(\b-?(?:\d+|0x[a-zA-Z0-9]+)\b)"))
+ .with_nestable(false)
+ .with_text_format(text_format_t::TF_C_LIKE)
+ .with_text_format(text_format_t::TF_JAVA)
+ .with_role(role_t::VCR_NUMBER);
+}
diff --git a/src/textfile_highlighters.hh b/src/textfile_highlighters.hh
new file mode 100644
index 0000000..51c9fa7
--- /dev/null
+++ b/src/textfile_highlighters.hh
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef textfile_highlighters_hh
+#define textfile_highlighters_hh
+
+#include "textview_curses_fwd.hh"
+
+void setup_highlights(highlight_map_t& hm);
+
+#endif
diff --git a/src/textfile_sub_source.cc b/src/textfile_sub_source.cc
new file mode 100644
index 0000000..8c6ef5f
--- /dev/null
+++ b/src/textfile_sub_source.cc
@@ -0,0 +1,875 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "textfile_sub_source.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/fs_util.hh"
+#include "base/injector.hh"
+#include "base/itertools.hh"
+#include "bound_tags.hh"
+#include "config.h"
+#include "lnav.events.hh"
+#include "md2attr_line.hh"
+#include "sqlitepp.hh"
+
+using namespace lnav::roles::literals;
+
+size_t
+textfile_sub_source::text_line_count()
+{
+ size_t retval = 0;
+
+ if (!this->tss_files.empty()) {
+ std::shared_ptr<logfile> lf = this->current_file();
+ auto rend_iter = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter == this->tss_rendered_files.end()) {
+ auto* lfo = (line_filter_observer*) lf->get_logline_observer();
+ retval = lfo->lfo_filter_state.tfs_index.size();
+ } else {
+ retval = rend_iter->second.rf_text_source->text_line_count();
+ }
+ }
+
+ return retval;
+}
+
+void
+textfile_sub_source::text_value_for_line(textview_curses& tc,
+ int line,
+ std::string& value_out,
+ text_sub_source::line_flags_t flags)
+{
+ if (!this->tss_files.empty()) {
+ std::shared_ptr<logfile> lf = this->current_file();
+ auto rend_iter = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter == this->tss_rendered_files.end()) {
+ auto* lfo = dynamic_cast<line_filter_observer*>(
+ lf->get_logline_observer());
+ if (line < 0 || line >= lfo->lfo_filter_state.tfs_index.size()) {
+ value_out.clear();
+ } else {
+ auto read_result = lf->read_line(
+ lf->begin() + lfo->lfo_filter_state.tfs_index[line]);
+ if (read_result.isOk()) {
+ value_out = to_string(read_result.unwrap());
+ }
+ }
+ } else {
+ rend_iter->second.rf_text_source->text_value_for_line(
+ tc, line, value_out, flags);
+ }
+ } else {
+ value_out.clear();
+ }
+}
+
+void
+textfile_sub_source::text_attrs_for_line(textview_curses& tc,
+ int row,
+ string_attrs_t& value_out)
+{
+ auto lf = this->current_file();
+ if (lf == nullptr) {
+ return;
+ }
+
+ auto rend_iter = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter != this->tss_rendered_files.end()) {
+ rend_iter->second.rf_text_source->text_attrs_for_line(
+ tc, row, value_out);
+ }
+
+ struct line_range lr;
+
+ lr.lr_start = 0;
+ lr.lr_end = -1;
+ value_out.emplace_back(lr, logline::L_FILE.value(this->current_file()));
+}
+
+size_t
+textfile_sub_source::text_size_for_line(textview_curses& tc,
+ int line,
+ text_sub_source::line_flags_t flags)
+{
+ size_t retval = 0;
+
+ if (!this->tss_files.empty()) {
+ std::shared_ptr<logfile> lf = this->current_file();
+ auto rend_iter = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter == this->tss_rendered_files.end()) {
+ auto* lfo = dynamic_cast<line_filter_observer*>(
+ lf->get_logline_observer());
+ if (line < 0 || line >= lfo->lfo_filter_state.tfs_index.size()) {
+ } else {
+ retval
+ = lf->message_byte_length(
+ lf->begin() + lfo->lfo_filter_state.tfs_index[line])
+ .mlr_length;
+ }
+ } else {
+ retval = rend_iter->second.rf_text_source->text_size_for_line(
+ tc, line, flags);
+ }
+ }
+
+ return retval;
+}
+
+void
+textfile_sub_source::to_front(const std::shared_ptr<logfile>& lf)
+{
+ auto iter = std::find(this->tss_files.begin(), this->tss_files.end(), lf);
+ if (iter != this->tss_files.end()) {
+ this->tss_files.erase(iter);
+ } else {
+ iter = std::find(
+ this->tss_hidden_files.begin(), this->tss_hidden_files.end(), lf);
+
+ if (iter != this->tss_hidden_files.end()) {
+ this->tss_hidden_files.erase(iter);
+ }
+ }
+ this->tss_files.push_front(lf);
+ this->tss_view->reload_data();
+}
+
+void
+textfile_sub_source::rotate_left()
+{
+ if (this->tss_files.size() > 1) {
+ this->tss_files.push_back(this->tss_files.front());
+ this->tss_files.pop_front();
+ this->tss_view->reload_data();
+ this->tss_view->redo_search();
+ }
+}
+
+void
+textfile_sub_source::rotate_right()
+{
+ if (this->tss_files.size() > 1) {
+ this->tss_files.push_front(this->tss_files.back());
+ this->tss_files.pop_back();
+ this->tss_view->reload_data();
+ this->tss_view->redo_search();
+ }
+}
+
+void
+textfile_sub_source::remove(const std::shared_ptr<logfile>& lf)
+{
+ auto iter = std::find(this->tss_files.begin(), this->tss_files.end(), lf);
+ if (iter != this->tss_files.end()) {
+ this->tss_files.erase(iter);
+ detach_observer(lf);
+ } else {
+ iter = std::find(
+ this->tss_hidden_files.begin(), this->tss_hidden_files.end(), lf);
+ if (iter != this->tss_hidden_files.end()) {
+ this->tss_hidden_files.erase(iter);
+ detach_observer(lf);
+ }
+ }
+}
+
+void
+textfile_sub_source::push_back(const std::shared_ptr<logfile>& lf)
+{
+ auto* lfo = new line_filter_observer(this->get_filters(), lf);
+ lf->set_logline_observer(lfo);
+ this->tss_files.push_back(lf);
+}
+
+void
+textfile_sub_source::text_filters_changed()
+{
+ for (auto iter = this->tss_files.begin(); iter != this->tss_files.end();) {
+ ++iter;
+ }
+ for (auto iter = this->tss_hidden_files.begin();
+ iter != this->tss_hidden_files.end();)
+ {
+ ++iter;
+ }
+
+ std::shared_ptr<logfile> lf = this->current_file();
+
+ if (lf == nullptr) {
+ return;
+ }
+
+ auto* lfo = (line_filter_observer*) lf->get_logline_observer();
+ uint32_t filter_in_mask, filter_out_mask;
+
+ lfo->clear_deleted_filter_state();
+ lf->reobserve_from(lf->begin() + lfo->get_min_count(lf->size()));
+
+ this->get_filters().get_enabled_mask(filter_in_mask, filter_out_mask);
+ lfo->lfo_filter_state.tfs_index.clear();
+ for (uint32_t lpc = 0; lpc < lf->size(); lpc++) {
+ if (this->tss_apply_filters
+ && lfo->excluded(filter_in_mask, filter_out_mask, lpc))
+ {
+ continue;
+ }
+ lfo->lfo_filter_state.tfs_index.push_back(lpc);
+ }
+
+ this->tss_view->redo_search();
+}
+
+int
+textfile_sub_source::get_filtered_count() const
+{
+ std::shared_ptr<logfile> lf = this->current_file();
+ int retval = 0;
+
+ if (lf != nullptr) {
+ auto rend_iter = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter == this->tss_rendered_files.end()) {
+ auto* lfo = (line_filter_observer*) lf->get_logline_observer();
+ retval = lf->size() - lfo->lfo_filter_state.tfs_index.size();
+ }
+ }
+ return retval;
+}
+
+int
+textfile_sub_source::get_filtered_count_for(size_t filter_index) const
+{
+ std::shared_ptr<logfile> lf = this->current_file();
+
+ if (lf == nullptr) {
+ return 0;
+ }
+
+ auto* lfo = dynamic_cast<line_filter_observer*>(lf->get_logline_observer());
+ return lfo->lfo_filter_state.tfs_filter_hits[filter_index];
+}
+
+text_format_t
+textfile_sub_source::get_text_format() const
+{
+ if (this->tss_files.empty()) {
+ return text_format_t::TF_UNKNOWN;
+ }
+
+ return this->tss_files.front()->get_text_format();
+}
+
+void
+textfile_sub_source::text_crumbs_for_line(
+ int line, std::vector<breadcrumb::crumb>& crumbs)
+{
+ text_sub_source::text_crumbs_for_line(line, crumbs);
+
+ if (this->empty()) {
+ return;
+ }
+
+ auto lf = this->current_file();
+ crumbs.emplace_back(
+ lf->get_unique_path(),
+ attr_line_t().append(lf->get_unique_path()),
+ [this]() {
+ return this->tss_files | lnav::itertools::map([](const auto& lf) {
+ return breadcrumb::possibility{
+ lf->get_unique_path(),
+ attr_line_t(lf->get_unique_path()),
+ };
+ });
+ },
+ [this](const auto& key) {
+ auto lf_opt = this->tss_files
+ | lnav::itertools::find_if([&key](const auto& elem) {
+ return key.template get<std::string>()
+ == elem->get_unique_path();
+ })
+ | lnav::itertools::deref();
+
+ if (!lf_opt) {
+ return;
+ }
+
+ this->to_front(lf_opt.value());
+ this->tss_view->reload_data();
+ });
+ if (lf->size() == 0) {
+ return;
+ }
+
+ auto rend_iter = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter != this->tss_rendered_files.end()) {
+ rend_iter->second.rf_text_source->text_crumbs_for_line(line, crumbs);
+ }
+
+ auto meta_iter = this->tss_doc_metadata.find(lf->get_filename());
+ if (meta_iter != this->tss_doc_metadata.end()) {
+ auto* lfo
+ = dynamic_cast<line_filter_observer*>(lf->get_logline_observer());
+ if (line < 0 || line >= lfo->lfo_filter_state.tfs_index.size()) {
+ return;
+ }
+ auto ll_iter = lf->begin() + lfo->lfo_filter_state.tfs_index[line];
+ auto ll_next_iter = ll_iter + 1;
+ auto end_offset = (ll_next_iter == lf->end())
+ ? lf->get_index_size() - 1
+ : ll_next_iter->get_offset() - 1;
+ const auto initial_size = crumbs.size();
+
+ meta_iter->second.ms_metadata.m_sections_tree.visit_overlapping(
+ ll_iter->get_offset(),
+ end_offset,
+ [&crumbs,
+ initial_size,
+ meta = &meta_iter->second.ms_metadata,
+ this,
+ lf](const auto& iv) {
+ auto path = crumbs | lnav::itertools::skip(initial_size)
+ | lnav::itertools::map(&breadcrumb::crumb::c_key)
+ | lnav::itertools::append(iv.value);
+ auto curr_node = lnav::document::hier_node::lookup_path(
+ meta->m_sections_root.get(), path);
+ crumbs.template emplace_back(
+ iv.value,
+ [meta, path]() { return meta->possibility_provider(path); },
+ [this, curr_node, path, lf](const auto& key) {
+ if (!curr_node) {
+ return;
+ }
+ auto* parent_node = curr_node.value()->hn_parent;
+ if (parent_node == nullptr) {
+ return;
+ }
+ key.template match(
+ [this, parent_node](const std::string& str) {
+ auto sib_iter
+ = parent_node->hn_named_children.find(str);
+ if (sib_iter
+ == parent_node->hn_named_children.end()) {
+ return;
+ }
+ this->set_top_from_off(
+ sib_iter->second->hn_start);
+ },
+ [this, parent_node](size_t index) {
+ if (index >= parent_node->hn_children.size()) {
+ return;
+ }
+ auto sib
+ = parent_node->hn_children[index].get();
+ this->set_top_from_off(sib->hn_start);
+ });
+ });
+ if (curr_node
+ && curr_node.value()->hn_parent->hn_children.size()
+ != curr_node.value()
+ ->hn_parent->hn_named_children.size())
+ {
+ auto node = lnav::document::hier_node::lookup_path(
+ meta->m_sections_root.get(), path);
+
+ crumbs.back().c_expected_input
+ = curr_node.value()
+ ->hn_parent->hn_named_children.empty()
+ ? breadcrumb::crumb::expected_input_t::index
+ : breadcrumb::crumb::expected_input_t::index_or_exact;
+ crumbs.back().with_possible_range(
+ node | lnav::itertools::map([](const auto hn) {
+ return hn->hn_parent->hn_children.size();
+ })
+ | lnav::itertools::unwrap_or(size_t{0}));
+ }
+ });
+
+ auto path = crumbs | lnav::itertools::skip(initial_size)
+ | lnav::itertools::map(&breadcrumb::crumb::c_key);
+ auto node = lnav::document::hier_node::lookup_path(
+ meta_iter->second.ms_metadata.m_sections_root.get(), path);
+
+ if (node && !node.value()->hn_children.empty()) {
+ auto poss_provider = [curr_node = node.value()]() {
+ std::vector<breadcrumb::possibility> retval;
+ for (const auto& child : curr_node->hn_named_children) {
+ retval.template emplace_back(child.first);
+ }
+ return retval;
+ };
+ auto path_performer = [this, curr_node = node.value()](
+ const breadcrumb::crumb::key_t& value) {
+ value.template match(
+ [this, curr_node](const std::string& str) {
+ auto child_iter
+ = curr_node->hn_named_children.find(str);
+ if (child_iter != curr_node->hn_named_children.end()) {
+ this->set_top_from_off(
+ child_iter->second->hn_start);
+ }
+ },
+ [this, curr_node](size_t index) {
+ if (index >= curr_node->hn_children.size()) {
+ return;
+ }
+ auto* child = curr_node->hn_children[index].get();
+ this->set_top_from_off(child->hn_start);
+ });
+ };
+ crumbs.emplace_back("", "\u22ef", poss_provider, path_performer);
+ crumbs.back().c_expected_input
+ = node.value()->hn_named_children.empty()
+ ? breadcrumb::crumb::expected_input_t::index
+ : breadcrumb::crumb::expected_input_t::index_or_exact;
+ }
+ }
+}
+
+bool
+textfile_sub_source::rescan_files(
+ textfile_sub_source::scan_callback& callback,
+ nonstd::optional<ui_clock::time_point> deadline)
+{
+ static auto& lnav_db = injector::get<auto_sqlite3&>();
+
+ file_iterator iter;
+ bool retval = false;
+
+ if (this->tss_view == nullptr || this->tss_view->is_paused()) {
+ return retval;
+ }
+
+ std::vector<std::shared_ptr<logfile>> closed_files;
+ for (iter = this->tss_files.begin(); iter != this->tss_files.end();) {
+ std::shared_ptr<logfile> lf = (*iter);
+
+ if (lf->is_closed()) {
+ iter = this->tss_files.erase(iter);
+ this->tss_rendered_files.erase(lf->get_filename());
+ this->tss_doc_metadata.erase(lf->get_filename());
+ this->detach_observer(lf);
+ closed_files.template emplace_back(lf);
+ continue;
+ }
+
+ try {
+ const auto& st = lf->get_stat();
+ uint32_t old_size = lf->size();
+ auto new_text_data = lf->rebuild_index(deadline);
+
+ if (lf->get_format() != nullptr) {
+ iter = this->tss_files.erase(iter);
+ this->tss_rendered_files.erase(lf->get_filename());
+ this->tss_doc_metadata.erase(lf->get_filename());
+ this->detach_observer(lf);
+ callback.promote_file(lf);
+ continue;
+ }
+
+ switch (new_text_data) {
+ case logfile::rebuild_result_t::NEW_LINES:
+ case logfile::rebuild_result_t::NEW_ORDER:
+ retval = true;
+ break;
+ default:
+ break;
+ }
+ callback.scanned_file(lf);
+
+ if (lf->get_text_format() == text_format_t::TF_MARKDOWN) {
+ auto rend_iter
+ = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter != this->tss_rendered_files.end()) {
+ if (rend_iter->second.rf_file_size == st.st_size
+ && rend_iter->second.rf_mtime == st.st_mtime)
+ {
+ ++iter;
+ continue;
+ }
+ log_info("markdown file has been updated, re-rendering: %s",
+ lf->get_filename().c_str());
+ this->tss_rendered_files.erase(rend_iter);
+ }
+
+ auto read_res = lf->read_file();
+ if (read_res.isOk()) {
+ static const auto FRONT_MATTER_RE
+ = lnav::pcre2pp::code::from_const(
+ R"((?:^---\n(.*)\n---\n|^\+\+\+\n(.*)\n\+\+\+\n))",
+ PCRE2_MULTILINE | PCRE2_DOTALL);
+ static thread_local auto md
+ = FRONT_MATTER_RE.create_match_data();
+
+ auto content = read_res.unwrap();
+ auto content_sf = string_fragment::from_str(content);
+ std::string frontmatter;
+ text_format_t frontmatter_format{text_format_t::TF_UNKNOWN};
+
+ auto cap_res = FRONT_MATTER_RE.capture_from(content_sf)
+ .into(md)
+ .matches()
+ .ignore_error();
+ if (cap_res) {
+ if (md[1]) {
+ frontmatter_format = text_format_t::TF_YAML;
+ frontmatter = md[1]->to_string();
+ } else if (md[2]) {
+ frontmatter_format = text_format_t::TF_TOML;
+ frontmatter = md[2]->to_string();
+ }
+ content_sf = cap_res->f_remaining;
+ } else if (content_sf.startswith("{")) {
+ yajlpp_parse_context ypc(
+ intern_string::lookup(lf->get_filename()));
+ auto_mem<yajl_handle_t> handle(yajl_free);
+
+ handle = yajl_alloc(&ypc.ypc_callbacks, nullptr, &ypc);
+ yajl_config(
+ handle.in(), yajl_allow_trailing_garbage, 1);
+ ypc.with_ignore_unused(true)
+ .with_handle(handle.in())
+ .with_error_reporter(
+ [&lf](const auto& ypc, const auto& um) {
+ log_error(
+ "%s: failed to parse JSON front matter "
+ "-- %s",
+ lf->get_filename().c_str(),
+ um.um_reason.al_string.c_str());
+ });
+ if (ypc.parse_doc(content_sf)) {
+ auto consumed = ypc.ypc_total_consumed;
+ if (consumed < content_sf.length()
+ && content_sf[consumed] == '\n')
+ {
+ frontmatter_format = text_format_t::TF_JSON;
+ frontmatter = string_fragment::from_str_range(
+ content, 0, consumed)
+ .to_string();
+ content_sf = content_sf.substr(consumed);
+ }
+ }
+ }
+
+ md2attr_line mdal;
+
+ mdal.with_source_path(lf->get_actual_path());
+ auto parse_res = md4cpp::parse(content_sf, mdal);
+
+ auto& rf = this->tss_rendered_files[lf->get_filename()];
+ rf.rf_mtime = st.st_mtime;
+ rf.rf_file_size = st.st_size;
+ rf.rf_text_source = std::make_unique<plain_text_source>();
+ rf.rf_text_source->register_view(this->tss_view);
+ if (parse_res.isOk()) {
+ auto& lf_meta = lf->get_embedded_metadata();
+
+ rf.rf_text_source->replace_with(parse_res.unwrap());
+
+ if (!frontmatter.empty()) {
+ lf_meta["net.daringfireball.markdown.frontmatter"]
+ = {frontmatter_format, frontmatter};
+ }
+
+ lnav::events::publish(
+ lnav_db,
+ lnav::events::file::format_detected{
+ lf->get_filename(),
+ fmt::to_string(lf->get_text_format()),
+ });
+ } else {
+ auto view_content
+ = lnav::console::user_message::error(
+ "unable to parse markdown file")
+ .with_reason(parse_res.unwrapErr())
+ .to_attr_line();
+ view_content.append("\n").append(
+ attr_line_t::from_ansi_str(content.c_str()));
+
+ rf.rf_text_source->replace_with(view_content);
+ }
+ } else {
+ log_error("unable to read markdown file: %s -- %s",
+ lf->get_filename().c_str(),
+ read_res.unwrapErr().c_str());
+ }
+ ++iter;
+ continue;
+ }
+
+ if (!retval && lf->is_indexing()
+ && lf->get_text_format() != text_format_t::TF_BINARY)
+ {
+ auto ms_iter = this->tss_doc_metadata.find(lf->get_filename());
+
+ if (ms_iter != this->tss_doc_metadata.end()) {
+ if (st.st_mtime != ms_iter->second.ms_mtime
+ || st.st_size != ms_iter->second.ms_file_size)
+ {
+ this->tss_doc_metadata.erase(ms_iter);
+ ms_iter = this->tss_doc_metadata.end();
+ }
+ }
+
+ if (ms_iter == this->tss_doc_metadata.end()) {
+ auto read_res = lf->read_file();
+
+ if (read_res.isOk()) {
+ auto content = attr_line_t(read_res.unwrap());
+
+ log_info("generating metdata for: %s",
+ lf->get_filename().c_str());
+ scrub_ansi_string(content.get_string(),
+ &content.get_attrs());
+ this->tss_doc_metadata[lf->get_filename()]
+ = metadata_state{
+ st.st_mtime,
+ static_cast<file_ssize_t>(st.st_size),
+ lnav::document::discover_structure(
+ content, line_range{0, -1}),
+ };
+ } else {
+ log_error(
+ "%s: unable to read file for meta discover -- %s",
+ lf->get_filename().c_str(),
+ read_res.unwrapErr().c_str());
+ this->tss_doc_metadata[lf->get_filename()]
+ = metadata_state{
+ st.st_mtime,
+ static_cast<file_ssize_t>(st.st_size),
+ {},
+ };
+ }
+ }
+ }
+
+ uint32_t filter_in_mask, filter_out_mask;
+
+ this->get_filters().get_enabled_mask(filter_in_mask,
+ filter_out_mask);
+ auto* lfo = (line_filter_observer*) lf->get_logline_observer();
+ for (uint32_t lpc = old_size; lpc < lf->size(); lpc++) {
+ if (this->tss_apply_filters
+ && lfo->excluded(filter_in_mask, filter_out_mask, lpc))
+ {
+ continue;
+ }
+ lfo->lfo_filter_state.tfs_index.push_back(lpc);
+ }
+ } catch (const line_buffer::error& e) {
+ iter = this->tss_files.erase(iter);
+ this->tss_rendered_files.erase(lf->get_filename());
+ this->tss_doc_metadata.erase(lf->get_filename());
+ lf->close();
+ this->detach_observer(lf);
+ closed_files.template emplace_back(lf);
+ continue;
+ }
+
+ ++iter;
+ }
+ if (!closed_files.empty()) {
+ callback.closed_files(closed_files);
+ }
+
+ if (retval) {
+ this->tss_view->search_new_data();
+ }
+
+ return retval;
+}
+
+void
+textfile_sub_source::set_top_from_off(file_off_t off)
+{
+ auto lf = this->current_file();
+
+ lf->line_for_offset(off) | [this, lf](auto new_top_iter) {
+ auto* lfo = (line_filter_observer*) lf->get_logline_observer();
+ auto new_top_opt = lfo->lfo_filter_state.content_line_to_vis_line(
+ std::distance(lf->cbegin(), new_top_iter));
+
+ if (new_top_opt) {
+ this->tss_view->set_selection(vis_line_t(new_top_opt.value()));
+ }
+ };
+}
+
+void
+textfile_sub_source::quiesce()
+{
+ for (auto& lf : this->tss_files) {
+ lf->quiesce();
+ }
+}
+
+nonstd::optional<vis_line_t>
+textfile_sub_source::row_for_anchor(const std::string& id)
+{
+ auto lf = this->current_file();
+ if (!lf) {
+ return nonstd::nullopt;
+ }
+
+ auto rend_iter = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter != this->tss_rendered_files.end()) {
+ return rend_iter->second.rf_text_source->row_for_anchor(id);
+ }
+
+ auto iter = this->tss_doc_metadata.find(lf->get_filename());
+ if (iter == this->tss_doc_metadata.end()) {
+ return nonstd::nullopt;
+ }
+
+ const auto& meta = iter->second.ms_metadata;
+ nonstd::optional<vis_line_t> retval;
+
+ lnav::document::hier_node::depth_first(
+ meta.m_sections_root.get(),
+ [lf, &id, &retval](const lnav::document::hier_node* node) {
+ for (const auto& child_pair : node->hn_named_children) {
+ auto child_anchor
+ = text_anchors::to_anchor_string(child_pair.first);
+
+ if (child_anchor == id) {
+ auto ll_opt
+ = lf->line_for_offset(child_pair.second->hn_start);
+ if (ll_opt != lf->end()) {
+ retval = vis_line_t(
+ std::distance(lf->cbegin(), ll_opt.value()));
+ }
+ }
+ }
+ });
+
+ return retval;
+}
+
+std::unordered_set<std::string>
+textfile_sub_source::get_anchors()
+{
+ std::unordered_set<std::string> retval;
+
+ auto lf = this->current_file();
+ if (!lf) {
+ return retval;
+ }
+
+ auto rend_iter = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter != this->tss_rendered_files.end()) {
+ return rend_iter->second.rf_text_source->get_anchors();
+ }
+
+ auto iter = this->tss_doc_metadata.find(lf->get_filename());
+ if (iter == this->tss_doc_metadata.end()) {
+ return retval;
+ }
+
+ const auto& meta = iter->second.ms_metadata;
+
+ lnav::document::hier_node::depth_first(
+ meta.m_sections_root.get(),
+ [&retval](const lnav::document::hier_node* node) {
+ if (retval.size() > 100) {
+ return;
+ }
+
+ for (const auto& child_pair : node->hn_named_children) {
+ retval.emplace(
+ text_anchors::to_anchor_string(child_pair.first));
+ }
+ });
+
+ return retval;
+}
+
+nonstd::optional<std::string>
+textfile_sub_source::anchor_for_row(vis_line_t vl)
+{
+ nonstd::optional<std::string> retval;
+
+ auto lf = this->current_file();
+ if (!lf) {
+ return retval;
+ }
+
+ auto rend_iter = this->tss_rendered_files.find(lf->get_filename());
+ if (rend_iter != this->tss_rendered_files.end()) {
+ return rend_iter->second.rf_text_source->anchor_for_row(vl);
+ }
+
+ auto iter = this->tss_doc_metadata.find(lf->get_filename());
+ if (iter == this->tss_doc_metadata.end()) {
+ return retval;
+ }
+
+ auto* lfo = dynamic_cast<line_filter_observer*>(lf->get_logline_observer());
+ if (vl >= lfo->lfo_filter_state.tfs_index.size()) {
+ return retval;
+ }
+ auto ll_iter = lf->begin() + lfo->lfo_filter_state.tfs_index[vl];
+ auto ll_next_iter = ll_iter + 1;
+ auto end_offset = (ll_next_iter == lf->end())
+ ? lf->get_index_size() - 1
+ : ll_next_iter->get_offset() - 1;
+ iter->second.ms_metadata.m_sections_tree.visit_overlapping(
+ ll_iter->get_offset(),
+ end_offset,
+ [&retval](const lnav::document::section_interval_t& iv) {
+ retval = iv.value.match(
+ [](const std::string& str) {
+ return nonstd::make_optional(
+ text_anchors::to_anchor_string(str));
+ },
+ [](size_t) { return nonstd::nullopt; });
+ });
+
+ return retval;
+}
+
+bool
+textfile_sub_source::to_front(const std::string& filename)
+{
+ auto lf_opt = this->tss_files
+ | lnav::itertools::find_if([&filename](const auto& elem) {
+ return elem->get_filename() == filename;
+ });
+ if (!lf_opt) {
+ lf_opt = this->tss_hidden_files
+ | lnav::itertools::find_if([&filename](const auto& elem) {
+ return elem->get_filename() == filename;
+ });
+ }
+
+ if (!lf_opt) {
+ return false;
+ }
+
+ this->to_front(*(lf_opt.value()));
+
+ return true;
+}
diff --git a/src/textfile_sub_source.hh b/src/textfile_sub_source.hh
new file mode 100644
index 0000000..724f7c7
--- /dev/null
+++ b/src/textfile_sub_source.hh
@@ -0,0 +1,173 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef textfile_sub_source_hh
+#define textfile_sub_source_hh
+
+#include <deque>
+#include <unordered_map>
+
+#include "filter_observer.hh"
+#include "logfile.hh"
+#include "plain_text_source.hh"
+#include "textview_curses.hh"
+
+class textfile_sub_source
+ : public text_sub_source
+ , public vis_location_history
+ , public text_anchors {
+public:
+ using file_iterator = std::deque<std::shared_ptr<logfile>>::iterator;
+
+ textfile_sub_source() { this->tss_supports_filtering = true; }
+
+ ~textfile_sub_source() override = default;
+
+ bool empty() const { return this->tss_files.empty(); }
+
+ size_t size() const { return this->tss_files.size(); }
+
+ size_t text_line_count() override;
+
+ size_t text_line_width(textview_curses& curses) override
+ {
+ return this->tss_files.empty()
+ ? 0
+ : this->current_file()->get_longest_line_length();
+ }
+
+ void text_value_for_line(textview_curses& tc,
+ int line,
+ std::string& value_out,
+ line_flags_t flags) override;
+
+ void text_attrs_for_line(textview_curses& tc,
+ int row,
+ string_attrs_t& value_out) override;
+
+ size_t text_size_for_line(textview_curses& tc,
+ int line,
+ line_flags_t flags) override;
+
+ std::shared_ptr<logfile> current_file() const
+ {
+ if (this->tss_files.empty()) {
+ return nullptr;
+ }
+
+ return this->tss_files.front();
+ }
+
+ std::string text_source_name(const textview_curses& tv) override
+ {
+ if (this->tss_files.empty()) {
+ return "";
+ }
+
+ return this->tss_files.front()->get_filename();
+ }
+
+ void to_front(const std::shared_ptr<logfile>& lf);
+
+ bool to_front(const std::string& filename);
+
+ void set_top_from_off(file_off_t off);
+
+ void rotate_left();
+
+ void rotate_right();
+
+ void remove(const std::shared_ptr<logfile>& lf);
+
+ void push_back(const std::shared_ptr<logfile>& lf);
+
+ class scan_callback {
+ public:
+ virtual void closed_files(
+ const std::vector<std::shared_ptr<logfile>>& files)
+ = 0;
+ virtual void promote_file(const std::shared_ptr<logfile>& lf) = 0;
+ virtual void scanned_file(const std::shared_ptr<logfile>& lf) = 0;
+ };
+
+ bool rescan_files(scan_callback& callback,
+ nonstd::optional<ui_clock::time_point> deadline
+ = nonstd::nullopt);
+
+ void text_filters_changed() override;
+
+ int get_filtered_count() const override;
+
+ int get_filtered_count_for(size_t filter_index) const override;
+
+ text_format_t get_text_format() const override;
+
+ nonstd::optional<location_history*> get_location_history() override
+ {
+ return this;
+ }
+
+ void text_crumbs_for_line(int line,
+ std::vector<breadcrumb::crumb>& crumbs) override;
+
+ nonstd::optional<vis_line_t> row_for_anchor(const std::string& id) override;
+
+ nonstd::optional<std::string> anchor_for_row(vis_line_t vl) override;
+
+ std::unordered_set<std::string> get_anchors() override;
+
+ void quiesce() override;
+
+private:
+ void detach_observer(std::shared_ptr<logfile> lf)
+ {
+ auto* lfo = (line_filter_observer*) lf->get_logline_observer();
+ lf->set_logline_observer(nullptr);
+ delete lfo;
+ }
+
+ struct rendered_file {
+ time_t rf_mtime;
+ file_ssize_t rf_file_size;
+ std::unique_ptr<plain_text_source> rf_text_source;
+ };
+
+ struct metadata_state {
+ time_t ms_mtime;
+ file_ssize_t ms_file_size;
+ lnav::document::metadata ms_metadata;
+ };
+
+ std::deque<std::shared_ptr<logfile>> tss_files;
+ std::deque<std::shared_ptr<logfile>> tss_hidden_files;
+ std::unordered_map<std::string, rendered_file> tss_rendered_files;
+ std::unordered_map<std::string, metadata_state> tss_doc_metadata;
+};
+
+#endif
diff --git a/src/textview_curses.cc b/src/textview_curses.cc
new file mode 100644
index 0000000..6165f9e
--- /dev/null
+++ b/src/textview_curses.cc
@@ -0,0 +1,1143 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+#include <vector>
+
+#include "textview_curses.hh"
+
+#include "base/ansi_scrubber.hh"
+#include "base/injector.hh"
+#include "base/time_util.hh"
+#include "config.h"
+#include "data_parser.hh"
+#include "fmt/format.h"
+#include "lnav_config.hh"
+#include "log_format.hh"
+#include "logfile.hh"
+#include "shlex.hh"
+#include "view_curses.hh"
+
+const auto REVERSE_SEARCH_OFFSET = 2000_vl;
+
+void
+text_filter::revert_to_last(logfile_filter_state& lfs, size_t rollback_size)
+{
+ require(lfs.tfs_lines_for_message[this->lf_index] == 0);
+
+ lfs.tfs_message_matched[this->lf_index]
+ = lfs.tfs_last_message_matched[this->lf_index];
+ lfs.tfs_lines_for_message[this->lf_index]
+ = lfs.tfs_last_lines_for_message[this->lf_index];
+
+ for (size_t lpc = 0; lpc < lfs.tfs_lines_for_message[this->lf_index]; lpc++)
+ {
+ if (lfs.tfs_message_matched[this->lf_index]) {
+ lfs.tfs_filter_hits[this->lf_index] -= 1;
+ }
+ lfs.tfs_filter_count[this->lf_index] -= 1;
+ size_t line_number = lfs.tfs_filter_count[this->lf_index];
+
+ lfs.tfs_mask[line_number] &= ~(((uint32_t) 1) << this->lf_index);
+ }
+ if (lfs.tfs_lines_for_message[this->lf_index] > 0) {
+ require(lfs.tfs_lines_for_message[this->lf_index] >= rollback_size);
+
+ lfs.tfs_lines_for_message[this->lf_index] -= rollback_size;
+ }
+ if (lfs.tfs_lines_for_message[this->lf_index] == 0) {
+ lfs.tfs_message_matched[this->lf_index] = false;
+ }
+}
+
+void
+text_filter::add_line(logfile_filter_state& lfs,
+ logfile::const_iterator ll,
+ const shared_buffer_ref& line)
+{
+ bool match_state = this->matches(*lfs.tfs_logfile, ll, line);
+
+ if (ll->is_message()) {
+ this->end_of_message(lfs);
+ }
+
+ lfs.tfs_message_matched[this->lf_index]
+ = lfs.tfs_message_matched[this->lf_index] || match_state;
+ lfs.tfs_lines_for_message[this->lf_index] += 1;
+}
+
+void
+text_filter::end_of_message(logfile_filter_state& lfs)
+{
+ uint32_t mask = 0;
+
+ mask = ((uint32_t) 1U << this->lf_index);
+
+ for (size_t lpc = 0; lpc < lfs.tfs_lines_for_message[this->lf_index]; lpc++)
+ {
+ require(lfs.tfs_filter_count[this->lf_index]
+ <= lfs.tfs_logfile->size());
+
+ size_t line_number = lfs.tfs_filter_count[this->lf_index];
+
+ if (lfs.tfs_message_matched[this->lf_index]) {
+ lfs.tfs_mask[line_number] |= mask;
+ } else {
+ lfs.tfs_mask[line_number] &= ~mask;
+ }
+ lfs.tfs_filter_count[this->lf_index] += 1;
+ if (lfs.tfs_message_matched[this->lf_index]) {
+ lfs.tfs_filter_hits[this->lf_index] += 1;
+ }
+ }
+ lfs.tfs_last_message_matched[this->lf_index]
+ = lfs.tfs_message_matched[this->lf_index];
+ lfs.tfs_last_lines_for_message[this->lf_index]
+ = lfs.tfs_lines_for_message[this->lf_index];
+ lfs.tfs_message_matched[this->lf_index] = false;
+ lfs.tfs_lines_for_message[this->lf_index] = 0;
+}
+
+const bookmark_type_t textview_curses::BM_USER("user");
+const bookmark_type_t textview_curses::BM_USER_EXPR("user-expr");
+const bookmark_type_t textview_curses::BM_SEARCH("search");
+const bookmark_type_t textview_curses::BM_META("meta");
+
+textview_curses::textview_curses() : tc_search_action(noop_func{})
+{
+ this->set_data_source(this);
+}
+
+textview_curses::~textview_curses()
+{
+ this->tc_search_action = noop_func{};
+}
+
+void
+textview_curses::reload_config(error_reporter& reporter)
+{
+ const static auto DEFAULT_THEME_NAME = std::string("default");
+
+ for (auto iter = this->tc_highlights.begin();
+ iter != this->tc_highlights.end();)
+ {
+ if (iter->first.first != highlight_source_t::THEME) {
+ ++iter;
+ continue;
+ }
+
+ iter = this->tc_highlights.erase(iter);
+ }
+
+ std::map<std::string, std::string> vars;
+ auto curr_theme_iter
+ = lnav_config.lc_ui_theme_defs.find(lnav_config.lc_ui_theme);
+ if (curr_theme_iter != lnav_config.lc_ui_theme_defs.end()) {
+ vars = curr_theme_iter->second.lt_vars;
+ }
+
+ for (const auto& theme_name : {DEFAULT_THEME_NAME, lnav_config.lc_ui_theme})
+ {
+ auto theme_iter = lnav_config.lc_ui_theme_defs.find(theme_name);
+
+ if (theme_iter == lnav_config.lc_ui_theme_defs.end()) {
+ continue;
+ }
+
+ for (const auto& hl_pair : theme_iter->second.lt_highlights) {
+ if (hl_pair.second.hc_regex.empty()) {
+ continue;
+ }
+
+ auto regex = lnav::pcre2pp::code::from(hl_pair.second.hc_regex);
+
+ if (regex.isErr()) {
+ const static intern_string_t PATTERN_SRC
+ = intern_string::lookup("pattern");
+
+ auto ce = regex.unwrapErr();
+ reporter(&hl_pair.second.hc_regex,
+ lnav::console::to_user_message(PATTERN_SRC, ce));
+ continue;
+ }
+
+ const auto& sc = hl_pair.second.hc_style;
+ std::string fg1, bg1, fg_color, bg_color, errmsg;
+ bool invalid = false;
+ text_attrs attrs;
+
+ fg1 = sc.sc_color;
+ bg1 = sc.sc_background_color;
+ shlex(fg1).eval(fg_color, vars);
+ shlex(bg1).eval(bg_color, vars);
+
+ auto fg = styling::color_unit::from_str(fg_color).unwrapOrElse(
+ [&](const auto& msg) {
+ reporter(&sc.sc_color,
+ lnav::console::user_message::error(
+ attr_line_t("invalid color -- ")
+ .append_quoted(sc.sc_color))
+ .with_reason(msg));
+ invalid = true;
+ return styling::color_unit::make_empty();
+ });
+ auto bg = styling::color_unit::from_str(bg_color).unwrapOrElse(
+ [&](const auto& msg) {
+ reporter(&sc.sc_background_color,
+ lnav::console::user_message::error(
+ attr_line_t("invalid background color -- ")
+ .append_quoted(sc.sc_background_color))
+ .with_reason(msg));
+ invalid = true;
+ return styling::color_unit::make_empty();
+ });
+ if (invalid) {
+ continue;
+ }
+
+ if (sc.sc_bold) {
+ attrs.ta_attrs |= A_BOLD;
+ }
+ if (sc.sc_underline) {
+ attrs.ta_attrs |= A_UNDERLINE;
+ }
+ this->tc_highlights[{highlight_source_t::THEME, hl_pair.first}]
+ = highlighter(regex.unwrap().to_shared())
+ .with_attrs(attrs)
+ .with_color(fg, bg)
+ .with_nestable(false);
+ }
+ }
+
+ if (this->tc_reload_config_delegate) {
+ this->tc_reload_config_delegate(*this);
+ }
+}
+
+void
+textview_curses::reload_data()
+{
+ if (this->tc_sub_source != nullptr) {
+ this->tc_sub_source->text_update_marks(this->tc_bookmarks);
+ }
+ if (this->tc_sub_source != nullptr) {
+ auto* ttt = dynamic_cast<text_time_translator*>(this->tc_sub_source);
+
+ if (ttt != nullptr) {
+ ttt->data_reloaded(this);
+ }
+ }
+ listview_curses::reload_data();
+}
+
+void
+textview_curses::grep_begin(grep_proc<vis_line_t>& gp,
+ vis_line_t start,
+ vis_line_t stop)
+{
+ require(this->tc_searching >= 0);
+
+ this->tc_searching += 1;
+ this->tc_search_action(this);
+
+ if (start != -1_vl) {
+ auto& search_bv = this->tc_bookmarks[&BM_SEARCH];
+ auto pair = search_bv.equal_range(start, stop);
+
+ if (pair.first != pair.second) {
+ this->set_needs_update();
+ }
+ for (auto mark_iter = pair.first; mark_iter != pair.second; ++mark_iter)
+ {
+ if (this->tc_sub_source) {
+ this->tc_sub_source->text_mark(&BM_SEARCH, *mark_iter, false);
+ }
+ }
+ if (pair.first != pair.second) {
+ search_bv.erase(pair.first, pair.second);
+ }
+ }
+
+ listview_curses::reload_data();
+}
+
+void
+textview_curses::grep_end_batch(grep_proc<vis_line_t>& gp)
+{
+ if (this->tc_follow_deadline.tv_sec
+ && this->tc_follow_selection == this->get_selection())
+ {
+ struct timeval now;
+
+ gettimeofday(&now, nullptr);
+ if (this->tc_follow_deadline < now) {
+ } else {
+ if (this->tc_follow_func) {
+ if (this->tc_follow_func()) {
+ this->tc_follow_deadline = {0, 0};
+ }
+ } else {
+ this->tc_follow_deadline = {0, 0};
+ }
+ }
+ }
+ this->tc_search_action(this);
+}
+
+void
+textview_curses::grep_end(grep_proc<vis_line_t>& gp)
+{
+ this->tc_searching -= 1;
+ this->grep_end_batch(gp);
+
+ ensure(this->tc_searching >= 0);
+}
+
+void
+textview_curses::grep_match(grep_proc<vis_line_t>& gp,
+ vis_line_t line,
+ int start,
+ int end)
+{
+ this->tc_bookmarks[&BM_SEARCH].insert_once(vis_line_t(line));
+ if (this->tc_sub_source != nullptr) {
+ this->tc_sub_source->text_mark(&BM_SEARCH, line, true);
+ }
+
+ if (this->get_top() <= line && line <= this->get_bottom()) {
+ listview_curses::reload_data();
+ }
+}
+
+void
+textview_curses::listview_value_for_rows(const listview_curses& lv,
+ vis_line_t row,
+ std::vector<attr_line_t>& rows_out)
+{
+ for (auto& al : rows_out) {
+ this->textview_value_for_row(row, al);
+ ++row;
+ }
+}
+
+bool
+textview_curses::handle_mouse(mouse_event& me)
+{
+ unsigned long width;
+ vis_line_t height;
+
+ if (this->tc_selection_start == -1_vl && listview_curses::handle_mouse(me))
+ {
+ return true;
+ }
+
+ if (this->tc_delegate != nullptr
+ && this->tc_delegate->text_handle_mouse(*this, me))
+ {
+ return true;
+ }
+
+ if (me.me_button != mouse_button_t::BUTTON_LEFT) {
+ return false;
+ }
+
+ vis_line_t mouse_line(this->get_top() + me.me_y);
+
+ if (mouse_line > this->get_bottom()) {
+ mouse_line = this->get_bottom();
+ }
+
+ this->get_dimensions(height, width);
+
+ switch (me.me_state) {
+ case mouse_button_state_t::BUTTON_STATE_PRESSED:
+ this->tc_selection_start = mouse_line;
+ this->tc_selection_last = -1_vl;
+ this->tc_selection_cleared = false;
+ break;
+ case mouse_button_state_t::BUTTON_STATE_DRAGGED:
+ if (me.me_y <= 0) {
+ this->shift_top(-1_vl);
+ me.me_y = 0;
+ mouse_line = this->get_top();
+ }
+ if (me.me_y >= height
+ && this->get_top() < this->get_top_for_last_row())
+ {
+ this->shift_top(1_vl);
+ me.me_y = height;
+ mouse_line = this->get_bottom();
+ }
+
+ if (this->tc_selection_last == mouse_line)
+ break;
+
+ if (this->tc_selection_last != -1) {
+ this->toggle_user_mark(&textview_curses::BM_USER,
+ this->tc_selection_start,
+ this->tc_selection_last);
+ }
+ if (this->tc_selection_start == mouse_line) {
+ this->tc_selection_last = -1_vl;
+ } else {
+ if (!this->tc_selection_cleared) {
+ if (this->tc_sub_source != nullptr) {
+ this->tc_sub_source->text_clear_marks(&BM_USER);
+ }
+ this->tc_bookmarks[&BM_USER].clear();
+
+ this->tc_selection_cleared = true;
+ }
+ this->toggle_user_mark(
+ &BM_USER, this->tc_selection_start, mouse_line);
+ this->tc_selection_last = mouse_line;
+ }
+ this->reload_data();
+ break;
+ case mouse_button_state_t::BUTTON_STATE_RELEASED:
+ this->tc_selection_start = -1_vl;
+ this->tc_selection_last = -1_vl;
+ this->tc_selection_cleared = false;
+ break;
+ }
+
+ return true;
+}
+
+void
+textview_curses::textview_value_for_row(vis_line_t row, attr_line_t& value_out)
+{
+ auto& sa = value_out.get_attrs();
+ auto& str = value_out.get_string();
+ auto source_format = this->tc_sub_source->get_text_format();
+ intern_string_t format_name;
+
+ this->tc_sub_source->text_value_for_line(*this, row, str);
+ this->tc_sub_source->text_attrs_for_line(*this, row, sa);
+
+ scrub_ansi_string(str, &sa);
+ struct line_range body, orig_line;
+
+ body = find_string_attr_range(sa, &SA_BODY);
+ if (body.lr_start == -1) {
+ body.lr_start = 0;
+ body.lr_end = str.size();
+ }
+
+ orig_line = find_string_attr_range(sa, &SA_ORIGINAL_LINE);
+ if (!orig_line.is_valid()) {
+ orig_line.lr_start = 0;
+ orig_line.lr_end = str.size();
+ }
+
+ auto format_attr_opt = get_string_attr(sa, SA_FORMAT);
+ if (format_attr_opt) {
+ format_name = format_attr_opt.value().get();
+ }
+
+ if (this->is_selectable() && row == this->get_selection()
+ && this->tc_cursor_role)
+ {
+ sa.emplace_back(line_range{orig_line.lr_start, -1},
+ VC_ROLE.value(this->tc_cursor_role.value()));
+ }
+
+ for (auto& tc_highlight : this->tc_highlights) {
+ bool internal_hl
+ = tc_highlight.first.first == highlight_source_t::INTERNAL
+ || tc_highlight.first.first == highlight_source_t::THEME;
+
+ if (!tc_highlight.second.h_text_formats.empty()
+ && tc_highlight.second.h_text_formats.count(source_format) == 0)
+ {
+ continue;
+ }
+
+ if (!tc_highlight.second.h_format_name.empty()
+ && tc_highlight.second.h_format_name != format_name)
+ {
+ continue;
+ }
+
+ if (this->tc_disabled_highlights.count(tc_highlight.first.first)) {
+ continue;
+ }
+
+ // Internal highlights should only apply to the log message body so
+ // that we don't start highlighting other fields. User-provided
+ // highlights should apply only to the line itself and not any of the
+ // surrounding decorations that are added (for example, the file lines
+ // that are inserted at the beginning of the log view).
+ int start_pos = internal_hl ? body.lr_start : orig_line.lr_start;
+ tc_highlight.second.annotate(value_out, start_pos);
+ }
+
+ if (this->tc_hide_fields) {
+ value_out.apply_hide();
+ }
+
+#if 0
+ typedef std::map<std::string, role_t> key_map_t;
+ static key_map_t key_roles;
+
+ data_scanner ds(str);
+ data_parser dp(&ds);
+
+ dp.parse();
+
+ for (list<data_parser::element>::iterator iter = dp.dp_stack.begin();
+ iter != dp.dp_stack.end();
+ ++iter) {
+ view_colors &vc = view_colors::singleton();
+
+ if (iter->e_token == DNT_PAIR) {
+ list<data_parser::element>::iterator pair_iter;
+ key_map_t::iterator km_iter;
+ data_token_t value_token;
+ struct line_range lr;
+ string key;
+
+ value_token =
+ iter->e_sub_elements->back().e_sub_elements->front().e_token;
+ if (value_token == DT_STRING) {
+ continue;
+ }
+
+ lr.lr_start = iter->e_capture.c_begin;
+ lr.lr_end = iter->e_capture.c_end;
+
+ key = ds.get_input().get_substr(
+ &iter->e_sub_elements->front().e_capture);
+ if ((km_iter = key_roles.find(key)) == key_roles.end()) {
+ key_roles[key] = vc.next_highlight();
+ }
+ /* fprintf(stderr, "key = %s\n", key.c_str()); */
+ sa[lr].insert(make_string_attr("style",
+ vc.attrs_for_role(key_roles[key])));
+
+ pair_iter = iter->e_sub_elements->begin();
+ ++pair_iter;
+
+ lr.lr_start = pair_iter->e_capture.c_begin;
+ lr.lr_end = pair_iter->e_capture.c_end;
+ sa[lr].insert(make_string_attr("style",
+ COLOR_PAIR(view_colors::VC_WHITE) |
+ A_BOLD));
+ }
+ }
+#endif
+
+ const auto& user_marks = this->tc_bookmarks[&BM_USER];
+ const auto& user_expr_marks = this->tc_bookmarks[&BM_USER_EXPR];
+ if (binary_search(user_marks.begin(), user_marks.end(), row)
+ || binary_search(user_expr_marks.begin(), user_expr_marks.end(), row))
+ {
+ sa.emplace_back(line_range{orig_line.lr_start, -1},
+ VC_STYLE.value(text_attrs{A_REVERSE}));
+ }
+}
+
+void
+textview_curses::execute_search(const std::string& regex_orig)
+{
+ std::string regex = regex_orig;
+ std::shared_ptr<lnav::pcre2pp::code> code;
+
+ if ((this->tc_search_child == nullptr)
+ || (regex != this->tc_current_search))
+ {
+ this->match_reset();
+
+ this->tc_search_child.reset();
+ this->tc_source_search_child.reset();
+
+ log_debug("start search for: '%s'", regex.c_str());
+
+ if (regex.empty()) {
+ } else {
+ auto compile_res = lnav::pcre2pp::code::from(regex, PCRE2_CASELESS);
+
+ if (compile_res.isErr()) {
+ auto ce = compile_res.unwrapErr();
+ regex = lnav::pcre2pp::quote(regex);
+
+ log_info("invalid search regex (%s), using quoted: %s",
+ ce.get_message().c_str(),
+ regex.c_str());
+
+ auto compile_quote_res
+ = lnav::pcre2pp::code::from(regex, PCRE2_CASELESS);
+ if (compile_quote_res.isErr()) {
+ log_error("Unable to compile quoted regex: %s",
+ regex.c_str());
+ } else {
+ code = compile_quote_res.unwrap().to_shared();
+ }
+ } else {
+ code = compile_res.unwrap().to_shared();
+ }
+ }
+
+ if (code != nullptr) {
+ highlighter hl(code);
+
+ hl.with_role(role_t::VCR_SEARCH);
+
+ auto& hm = this->get_highlights();
+ hm[{highlight_source_t::PREVIEW, "search"}] = hl;
+
+ auto gp = injector::get<std::shared_ptr<grep_proc<vis_line_t>>>(
+ code, *this);
+
+ gp->set_sink(this);
+ auto top = this->get_top();
+ if (top < REVERSE_SEARCH_OFFSET) {
+ top = 0_vl;
+ } else {
+ top -= REVERSE_SEARCH_OFFSET;
+ }
+ gp->queue_request(top);
+ if (top > 0) {
+ gp->queue_request(0_vl, top);
+ }
+ gp->start();
+
+ this->tc_search_child = std::make_shared<grep_highlighter>(
+ gp, highlight_source_t::PREVIEW, "search", hm);
+
+ if (this->tc_sub_source != nullptr) {
+ this->tc_sub_source->get_grepper() | [this, code](auto pair) {
+ auto sgp
+ = injector::get<std::shared_ptr<grep_proc<vis_line_t>>>(
+ code, *pair.first);
+
+ sgp->set_sink(pair.second);
+ sgp->queue_request(0_vl);
+ sgp->start();
+
+ this->tc_source_search_child = sgp;
+ };
+ }
+ }
+ }
+
+ this->tc_current_search = regex;
+ if (this->tc_state_event_handler) {
+ this->tc_state_event_handler(*this);
+ }
+}
+
+nonstd::optional<std::pair<int, int>>
+textview_curses::horiz_shift(vis_line_t start, vis_line_t end, int off_start)
+{
+ auto hl_iter
+ = this->tc_highlights.find({highlight_source_t::PREVIEW, "search"});
+ if (hl_iter == this->tc_highlights.end()
+ || hl_iter->second.h_regex == nullptr)
+ {
+ return nonstd::nullopt;
+ }
+ int prev_hit = -1, next_hit = INT_MAX;
+
+ for (; start < end; ++start) {
+ std::vector<attr_line_t> rows(1);
+ this->listview_value_for_rows(*this, start, rows);
+
+ const auto& str = rows[0].get_string();
+ hl_iter->second.h_regex->capture_from(str).for_each(
+ [&](lnav::pcre2pp::match_data& md) {
+ auto cap = md[0].value();
+ if (cap.sf_begin < off_start) {
+ prev_hit = std::max(prev_hit, cap.sf_begin);
+ } else if (cap.sf_begin > off_start) {
+ next_hit = std::min(next_hit, cap.sf_begin);
+ }
+ });
+ }
+
+ if (prev_hit == -1 && next_hit == INT_MAX) {
+ return nonstd::nullopt;
+ }
+ return std::make_pair(prev_hit, next_hit);
+}
+
+void
+textview_curses::set_user_mark(const bookmark_type_t* bm,
+ vis_line_t vl,
+ bool marked)
+{
+ bookmark_vector<vis_line_t>& bv = this->tc_bookmarks[bm];
+ bookmark_vector<vis_line_t>::iterator iter;
+
+ if (marked) {
+ bv.insert_once(vl);
+ } else {
+ iter = std::lower_bound(bv.begin(), bv.end(), vl);
+ if (iter != bv.end() && *iter == vl) {
+ bv.erase(iter);
+ }
+ }
+ if (this->tc_sub_source) {
+ this->tc_sub_source->text_mark(bm, vl, marked);
+ }
+
+ if (marked) {
+ this->search_range(vl, vl + 1_vl);
+ this->search_new_data();
+ }
+ this->set_needs_update();
+}
+
+void
+textview_curses::toggle_user_mark(const bookmark_type_t* bm,
+ vis_line_t start_line,
+ vis_line_t end_line)
+{
+ if (end_line == -1) {
+ end_line = start_line;
+ }
+ if (start_line > end_line) {
+ std::swap(start_line, end_line);
+ }
+
+ if (start_line >= this->get_inner_height()) {
+ return;
+ }
+ if (end_line >= this->get_inner_height()) {
+ end_line = vis_line_t(this->get_inner_height() - 1);
+ }
+ for (vis_line_t curr_line = start_line; curr_line <= end_line; ++curr_line)
+ {
+ bookmark_vector<vis_line_t>& bv = this->tc_bookmarks[bm];
+ bookmark_vector<vis_line_t>::iterator iter;
+ bool added;
+
+ iter = bv.insert_once(curr_line);
+ if (iter == bv.end()) {
+ added = true;
+ } else {
+ bv.erase(iter);
+ added = false;
+ }
+ if (this->tc_sub_source) {
+ this->tc_sub_source->text_mark(bm, curr_line, added);
+ }
+ }
+ this->search_range(start_line, end_line + 1_vl);
+ this->search_new_data();
+}
+
+void
+textview_curses::redo_search()
+{
+ if (this->tc_search_child) {
+ auto* gp = this->tc_search_child->get_grep_proc();
+
+ gp->invalidate();
+ this->match_reset();
+ gp->queue_request(0_vl).start();
+
+ if (this->tc_source_search_child) {
+ this->tc_source_search_child->invalidate()
+ .queue_request(0_vl)
+ .start();
+ }
+ }
+}
+
+bool
+textview_curses::listview_is_row_selectable(const listview_curses& lv,
+ vis_line_t row)
+{
+ if (this->tc_sub_source != nullptr) {
+ return this->tc_sub_source->text_is_row_selectable(*this, row);
+ }
+
+ return true;
+}
+
+void
+textview_curses::listview_selection_changed(const listview_curses& lv)
+{
+ if (this->tc_sub_source != nullptr) {
+ this->tc_sub_source->text_selection_changed(*this);
+ }
+}
+
+textview_curses&
+textview_curses::set_sub_source(text_sub_source* src)
+{
+ if (this->tc_sub_source != src) {
+ this->tc_bookmarks.clear();
+ this->tc_sub_source = src;
+ if (src) {
+ src->register_view(this);
+ }
+ this->reload_data();
+ }
+ return *this;
+}
+
+bool
+textview_curses::grep_value_for_line(vis_line_t line, std::string& value_out)
+{
+ bool retval = false;
+
+ if (this->tc_sub_source
+ && line < (int) this->tc_sub_source->text_line_count())
+ {
+ this->tc_sub_source->text_value_for_line(
+ *this, line, value_out, text_sub_source::RF_RAW);
+ scrub_ansi_string(value_out, nullptr);
+ retval = true;
+ }
+
+ return retval;
+}
+
+void
+text_time_translator::scroll_invoked(textview_curses* tc)
+{
+ if (tc->get_inner_height() > 0) {
+ this->time_for_row(tc->get_selection()) |
+ [this](auto new_top_time) { this->ttt_top_time = new_top_time; };
+ }
+}
+
+void
+text_time_translator::data_reloaded(textview_curses* tc)
+{
+ if (tc->get_inner_height() == 0) {
+ return;
+ }
+ if (tc->get_selection() > tc->get_inner_height()) {
+ if (this->ttt_top_time.tv_sec != 0) {
+ this->row_for_time(this->ttt_top_time) |
+ [tc](auto new_top) { tc->set_selection(new_top); };
+ }
+ return;
+ }
+ this->time_for_row(tc->get_selection()) | [this, tc](auto top_time) {
+ if (top_time != this->ttt_top_time) {
+ if (this->ttt_top_time.tv_sec != 0) {
+ this->row_for_time(this->ttt_top_time) |
+ [tc](auto new_top) { tc->set_selection(new_top); };
+ }
+ this->time_for_row(tc->get_selection()) |
+ [this](auto new_top_time) {
+ this->ttt_top_time = new_top_time;
+ };
+ }
+ };
+}
+
+template class bookmark_vector<vis_line_t>;
+
+bool
+empty_filter::matches(const logfile& lf,
+ logfile::const_iterator ll,
+ const shared_buffer_ref& line)
+{
+ return false;
+}
+
+std::string
+empty_filter::to_command() const
+{
+ return "";
+}
+
+nonstd::optional<size_t>
+filter_stack::next_index()
+{
+ bool used[32];
+
+ memset(used, 0, sizeof(used));
+ for (auto& iter : *this) {
+ if (iter->lf_deleted) {
+ continue;
+ }
+
+ size_t index = iter->get_index();
+
+ require(used[index] == false);
+
+ used[index] = true;
+ }
+ for (size_t lpc = this->fs_reserved;
+ lpc < logfile_filter_state::MAX_FILTERS;
+ lpc++)
+ {
+ if (!used[lpc]) {
+ return lpc;
+ }
+ }
+ return nonstd::nullopt;
+}
+
+std::shared_ptr<text_filter>
+filter_stack::get_filter(const std::string& id)
+{
+ auto iter = this->fs_filters.begin();
+ std::shared_ptr<text_filter> retval;
+
+ for (; iter != this->fs_filters.end() && (*iter)->get_id() != id; iter++) {
+ }
+ if (iter != this->fs_filters.end()) {
+ retval = *iter;
+ }
+
+ return retval;
+}
+
+bool
+filter_stack::delete_filter(const std::string& id)
+{
+ auto iter = this->fs_filters.begin();
+
+ for (; iter != this->fs_filters.end() && (*iter)->get_id() != id; iter++) {
+ }
+ if (iter != this->fs_filters.end()) {
+ this->fs_filters.erase(iter);
+ return true;
+ }
+
+ return false;
+}
+
+void
+filter_stack::get_mask(uint32_t& filter_mask)
+{
+ filter_mask = 0;
+ for (auto& iter : *this) {
+ std::shared_ptr<text_filter> tf = iter;
+
+ if (tf->lf_deleted) {
+ continue;
+ }
+ if (tf->is_enabled()) {
+ uint32_t bit = (1UL << tf->get_index());
+
+ switch (tf->get_type()) {
+ case text_filter::EXCLUDE:
+ case text_filter::INCLUDE:
+ filter_mask |= bit;
+ break;
+ default:
+ ensure(0);
+ break;
+ }
+ }
+ }
+}
+
+void
+filter_stack::get_enabled_mask(uint32_t& filter_in_mask,
+ uint32_t& filter_out_mask)
+{
+ filter_in_mask = filter_out_mask = 0;
+ for (auto& iter : *this) {
+ std::shared_ptr<text_filter> tf = iter;
+
+ if (tf->lf_deleted) {
+ continue;
+ }
+ if (tf->is_enabled()) {
+ uint32_t bit = (1UL << tf->get_index());
+
+ switch (tf->get_type()) {
+ case text_filter::EXCLUDE:
+ filter_out_mask |= bit;
+ break;
+ case text_filter::INCLUDE:
+ filter_in_mask |= bit;
+ break;
+ default:
+ ensure(0);
+ break;
+ }
+ }
+ }
+}
+
+void
+filter_stack::add_filter(const std::shared_ptr<text_filter>& filter)
+{
+ this->fs_filters.push_back(filter);
+}
+
+void
+vis_location_history::loc_history_append(vis_line_t top)
+{
+ auto iter = this->vlh_history.begin();
+ iter += this->vlh_history.size() - this->lh_history_position;
+ this->vlh_history.erase_from(iter);
+ this->lh_history_position = 0;
+ this->vlh_history.push_back(top);
+}
+
+nonstd::optional<vis_line_t>
+vis_location_history::loc_history_back(vis_line_t current_top)
+{
+ if (this->lh_history_position == 0) {
+ vis_line_t history_top = this->current_position();
+ if (history_top != current_top) {
+ return history_top;
+ }
+ }
+
+ if (this->lh_history_position + 1 >= this->vlh_history.size()) {
+ return nonstd::nullopt;
+ }
+
+ this->lh_history_position += 1;
+
+ return this->current_position();
+}
+
+nonstd::optional<vis_line_t>
+vis_location_history::loc_history_forward(vis_line_t current_top)
+{
+ if (this->lh_history_position == 0) {
+ return nonstd::nullopt;
+ }
+
+ this->lh_history_position -= 1;
+
+ return this->current_position();
+}
+
+void
+text_sub_source::toggle_apply_filters()
+{
+ this->tss_apply_filters = !this->tss_apply_filters;
+ this->text_filters_changed();
+}
+
+void
+text_sub_source::text_crumbs_for_line(int line,
+ std::vector<breadcrumb::crumb>& crumbs)
+{
+}
+
+logfile_filter_state::logfile_filter_state(std::shared_ptr<logfile> lf)
+ : tfs_logfile(std::move(lf))
+{
+ memset(this->tfs_filter_count, 0, sizeof(this->tfs_filter_count));
+ memset(this->tfs_filter_hits, 0, sizeof(this->tfs_filter_hits));
+ memset(this->tfs_message_matched, 0, sizeof(this->tfs_message_matched));
+ memset(this->tfs_lines_for_message, 0, sizeof(this->tfs_lines_for_message));
+ memset(this->tfs_last_message_matched,
+ 0,
+ sizeof(this->tfs_last_message_matched));
+ memset(this->tfs_last_lines_for_message,
+ 0,
+ sizeof(this->tfs_last_lines_for_message));
+ this->tfs_mask.reserve(64 * 1024);
+}
+
+void
+logfile_filter_state::clear()
+{
+ this->tfs_logfile = nullptr;
+ memset(this->tfs_filter_count, 0, sizeof(this->tfs_filter_count));
+ memset(this->tfs_filter_hits, 0, sizeof(this->tfs_filter_hits));
+ memset(this->tfs_message_matched, 0, sizeof(this->tfs_message_matched));
+ memset(this->tfs_lines_for_message, 0, sizeof(this->tfs_lines_for_message));
+ memset(this->tfs_last_message_matched,
+ 0,
+ sizeof(this->tfs_last_message_matched));
+ memset(this->tfs_last_lines_for_message,
+ 0,
+ sizeof(this->tfs_last_lines_for_message));
+ this->tfs_mask.clear();
+ this->tfs_index.clear();
+}
+
+void
+logfile_filter_state::clear_filter_state(size_t index)
+{
+ this->tfs_filter_count[index] = 0;
+ this->tfs_filter_hits[index] = 0;
+ this->tfs_message_matched[index] = false;
+ this->tfs_lines_for_message[index] = 0;
+ this->tfs_last_message_matched[index] = false;
+ this->tfs_last_lines_for_message[index] = 0;
+}
+
+void
+logfile_filter_state::clear_deleted_filter_state(uint32_t used_mask)
+{
+ for (int lpc = 0; lpc < MAX_FILTERS; lpc++) {
+ if (!(used_mask & (1L << lpc))) {
+ this->clear_filter_state(lpc);
+ }
+ }
+ for (size_t lpc = 0; lpc < this->tfs_mask.size(); lpc++) {
+ this->tfs_mask[lpc] &= used_mask;
+ }
+}
+
+void
+logfile_filter_state::resize(size_t newsize)
+{
+ size_t old_mask_size = this->tfs_mask.size();
+
+ this->tfs_mask.resize(newsize);
+ if (newsize > old_mask_size) {
+ memset(&this->tfs_mask[old_mask_size],
+ 0,
+ sizeof(uint32_t) * (newsize - old_mask_size));
+ }
+}
+
+nonstd::optional<size_t>
+logfile_filter_state::content_line_to_vis_line(uint32_t line)
+{
+ if (this->tfs_index.empty()) {
+ return nonstd::nullopt;
+ }
+
+ auto iter = std::lower_bound(
+ this->tfs_index.begin(), this->tfs_index.end(), line);
+
+ if (iter == this->tfs_index.end() || *iter != line) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::make_optional(std::distance(this->tfs_index.begin(), iter));
+}
+
+std::string
+text_anchors::to_anchor_string(const std::string& raw)
+{
+ static const auto ANCHOR_RE = lnav::pcre2pp::code::from_const(R"([^\w]+)");
+
+ return fmt::format(FMT_STRING("#{}"), ANCHOR_RE.replace(tolower(raw), "-"));
+}
diff --git a/src/textview_curses.hh b/src/textview_curses.hh
new file mode 100644
index 0000000..1fea398
--- /dev/null
+++ b/src/textview_curses.hh
@@ -0,0 +1,776 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file textview_curses.hh
+ */
+
+#ifndef textview_curses_hh
+#define textview_curses_hh
+
+#include <utility>
+#include <vector>
+
+#include "base/func_util.hh"
+#include "base/lnav_log.hh"
+#include "bookmarks.hh"
+#include "breadcrumb.hh"
+#include "grep_proc.hh"
+#include "highlighter.hh"
+#include "listview_curses.hh"
+#include "lnav_config_fwd.hh"
+#include "logfile_fwd.hh"
+#include "ring_span.hh"
+#include "text_format.hh"
+#include "textview_curses_fwd.hh"
+
+class textview_curses;
+
+using vis_bookmarks = bookmarks<vis_line_t>::type;
+
+class logfile_filter_state {
+public:
+ logfile_filter_state(std::shared_ptr<logfile> lf = nullptr);
+
+ void clear();
+
+ void clear_filter_state(size_t index);
+
+ void clear_deleted_filter_state(uint32_t used_mask);
+
+ void resize(size_t newsize);
+
+ nonstd::optional<size_t> content_line_to_vis_line(uint32_t line);
+
+ const static int MAX_FILTERS = 32;
+
+ std::shared_ptr<logfile> tfs_logfile;
+ size_t tfs_filter_count[MAX_FILTERS];
+ int tfs_filter_hits[MAX_FILTERS];
+ bool tfs_message_matched[MAX_FILTERS];
+ size_t tfs_lines_for_message[MAX_FILTERS];
+ bool tfs_last_message_matched[MAX_FILTERS];
+ size_t tfs_last_lines_for_message[MAX_FILTERS];
+ std::vector<uint32_t> tfs_mask;
+ std::vector<uint32_t> tfs_index;
+};
+
+enum class filter_lang_t : int {
+ NONE,
+ REGEX,
+ SQL,
+};
+
+class text_filter {
+public:
+ typedef enum {
+ MAYBE,
+ INCLUDE,
+ EXCLUDE,
+
+ LFT__MAX,
+
+ LFT__MASK = (MAYBE | INCLUDE | EXCLUDE)
+ } type_t;
+
+ text_filter(type_t type, filter_lang_t lang, std::string id, size_t index)
+ : lf_type(type), lf_lang(lang), lf_id(std::move(id)), lf_index(index)
+ {
+ }
+ virtual ~text_filter() = default;
+
+ type_t get_type() const { return this->lf_type; }
+ filter_lang_t get_lang() const { return this->lf_lang; }
+ void set_type(type_t t) { this->lf_type = t; }
+ std::string get_id() const { return this->lf_id; }
+ void set_id(std::string id) { this->lf_id = std::move(id); }
+ size_t get_index() const { return this->lf_index; }
+
+ bool is_enabled() const { return this->lf_enabled; }
+ void enable() { this->lf_enabled = true; }
+ void disable() { this->lf_enabled = false; }
+ void set_enabled(bool value) { this->lf_enabled = value; }
+
+ void revert_to_last(logfile_filter_state& lfs, size_t rollback_size);
+
+ void add_line(logfile_filter_state& lfs,
+ logfile_const_iterator ll,
+ const shared_buffer_ref& line);
+
+ void end_of_message(logfile_filter_state& lfs);
+
+ virtual bool matches(const logfile& lf,
+ logfile_const_iterator ll,
+ const shared_buffer_ref& line)
+ = 0;
+
+ virtual std::string to_command() const = 0;
+
+ bool operator==(const std::string& rhs) const { return this->lf_id == rhs; }
+
+ bool lf_deleted{false};
+
+protected:
+ bool lf_enabled{true};
+ type_t lf_type;
+ filter_lang_t lf_lang;
+ std::string lf_id;
+ size_t lf_index;
+};
+
+class empty_filter : public text_filter {
+public:
+ empty_filter(type_t type, size_t index)
+ : text_filter(type, filter_lang_t::REGEX, "", index)
+ {
+ }
+
+ bool matches(const logfile& lf,
+ logfile_const_iterator ll,
+ const shared_buffer_ref& line) override;
+
+ std::string to_command() const override;
+};
+
+class filter_stack {
+public:
+ using iterator = std::vector<std::shared_ptr<text_filter>>::iterator;
+ using const_iterator
+ = std::vector<std::shared_ptr<text_filter>>::const_iterator;
+ using value_type = std::shared_ptr<text_filter>;
+
+ explicit filter_stack(size_t reserved = 0) : fs_reserved(reserved) {}
+
+ iterator begin() { return this->fs_filters.begin(); }
+
+ iterator end() { return this->fs_filters.end(); }
+
+ const_iterator begin() const { return this->fs_filters.begin(); }
+
+ const_iterator end() const { return this->fs_filters.end(); }
+
+ size_t size() const { return this->fs_filters.size(); }
+
+ bool empty() const { return this->fs_filters.empty(); };
+
+ bool full() const
+ {
+ return (this->fs_reserved + this->fs_filters.size())
+ == logfile_filter_state::MAX_FILTERS;
+ }
+
+ nonstd::optional<size_t> next_index();
+
+ void add_filter(const std::shared_ptr<text_filter>& filter);
+
+ void clear_filters()
+ {
+ while (!this->fs_filters.empty()) {
+ this->fs_filters.pop_back();
+ }
+ }
+
+ void set_filter_enabled(const std::shared_ptr<text_filter>& filter,
+ bool enabled)
+ {
+ if (enabled) {
+ filter->enable();
+ } else {
+ filter->disable();
+ }
+ }
+
+ std::shared_ptr<text_filter> get_filter(const std::string& id);
+
+ bool delete_filter(const std::string& id);
+
+ void get_mask(uint32_t& filter_mask);
+
+ void get_enabled_mask(uint32_t& filter_in_mask, uint32_t& filter_out_mask);
+
+private:
+ const size_t fs_reserved;
+ std::vector<std::shared_ptr<text_filter>> fs_filters;
+};
+
+class text_time_translator {
+public:
+ virtual ~text_time_translator() = default;
+
+ virtual nonstd::optional<vis_line_t> row_for_time(
+ struct timeval time_bucket)
+ = 0;
+
+ virtual nonstd::optional<struct timeval> time_for_row(vis_line_t row) = 0;
+
+ void scroll_invoked(textview_curses* tc);
+
+ void data_reloaded(textview_curses* tc);
+
+protected:
+ struct timeval ttt_top_time {
+ 0, 0
+ };
+};
+
+class text_anchors {
+public:
+ virtual ~text_anchors() = default;
+
+ static std::string to_anchor_string(const std::string& raw);
+
+ virtual nonstd::optional<vis_line_t> row_for_anchor(const std::string& id)
+ = 0;
+
+ virtual nonstd::optional<std::string> anchor_for_row(vis_line_t vl) = 0;
+
+ virtual std::unordered_set<std::string> get_anchors() = 0;
+};
+
+class location_history {
+public:
+ virtual ~location_history() = default;
+
+ virtual void loc_history_append(vis_line_t top) = 0;
+
+ virtual nonstd::optional<vis_line_t> loc_history_back(
+ vis_line_t current_top)
+ = 0;
+
+ virtual nonstd::optional<vis_line_t> loc_history_forward(
+ vis_line_t current_top)
+ = 0;
+
+ const static int MAX_SIZE = 100;
+
+protected:
+ size_t lh_history_position{0};
+};
+
+/**
+ * Source for the text to be shown in a textview_curses view.
+ */
+class text_sub_source {
+public:
+ virtual ~text_sub_source() = default;
+
+ enum {
+ RB_RAW,
+ RB_FULL,
+ RB_REWRITE,
+ };
+
+ enum {
+ RF_RAW = (1UL << RB_RAW),
+ RF_FULL = (1UL << RB_FULL),
+ RF_REWRITE = (1UL << RB_REWRITE),
+ };
+
+ typedef long line_flags_t;
+
+ text_sub_source(size_t reserved_filters = 0) : tss_filters(reserved_filters)
+ {
+ }
+
+ void register_view(textview_curses* tc) { this->tss_view = tc; }
+
+ /**
+ * @return The total number of lines available from the source.
+ */
+ virtual size_t text_line_count() = 0;
+
+ virtual size_t text_line_width(textview_curses& curses) { return INT_MAX; }
+
+ virtual bool text_is_row_selectable(textview_curses& tc, vis_line_t row)
+ {
+ return true;
+ }
+
+ virtual void text_selection_changed(textview_curses& tc) {}
+
+ /**
+ * Get the value for a line.
+ *
+ * @param tc The textview_curses object that is delegating control.
+ * @param line The line number to retrieve.
+ * @param value_out The string object that should be set to the line
+ * contents.
+ * @param raw Indicates that the raw contents of the line should be returned
+ * without any post processing.
+ */
+ virtual void text_value_for_line(textview_curses& tc,
+ int line,
+ std::string& value_out,
+ line_flags_t flags = 0)
+ = 0;
+
+ virtual size_t text_size_for_line(textview_curses& tc,
+ int line,
+ line_flags_t raw = 0)
+ = 0;
+
+ /**
+ * Inform the source that the given line has been marked/unmarked. This
+ * callback function can be used to translate between between visible line
+ * numbers and content line numbers. For example, when viewing a log file
+ * with filters being applied, we want the bookmarked lines to be stable
+ * across changes in the filters.
+ *
+ * @param bm The type of bookmark.
+ * @param line The line that has been marked/unmarked.
+ * @param added True if the line was bookmarked and false if it was
+ * unmarked.
+ */
+ virtual void text_mark(const bookmark_type_t* bm,
+ vis_line_t line,
+ bool added)
+ {
+ }
+
+ /**
+ * Clear the bookmarks for a particular type in the text source.
+ *
+ * @param bm The type of bookmarks to clear.
+ */
+ virtual void text_clear_marks(const bookmark_type_t* bm) {}
+
+ /**
+ * Get the attributes for a line of text.
+ *
+ * @param tc The textview_curses object that is delegating control.
+ * @param line The line number to retrieve.
+ * @param value_out A string_attrs_t object that should be updated with the
+ * attributes for the line.
+ */
+ virtual void text_attrs_for_line(textview_curses& tc,
+ int line,
+ string_attrs_t& value_out)
+ {
+ }
+
+ /**
+ * Update the bookmarks used by the text view based on the bookmarks
+ * maintained by the text source.
+ *
+ * @param bm The bookmarks data structure used by the text view.
+ */
+ virtual void text_update_marks(vis_bookmarks& bm) {}
+
+ virtual std::string text_source_name(const textview_curses& tv)
+ {
+ return "";
+ }
+
+ filter_stack& get_filters() { return this->tss_filters; }
+
+ virtual void text_filters_changed() {}
+
+ virtual int get_filtered_count() const { return 0; }
+
+ virtual int get_filtered_count_for(size_t filter_index) const { return 0; }
+
+ virtual text_format_t get_text_format() const
+ {
+ return text_format_t::TF_UNKNOWN;
+ }
+
+ virtual nonstd::optional<
+ std::pair<grep_proc_source<vis_line_t>*, grep_proc_sink<vis_line_t>*>>
+ get_grepper()
+ {
+ return nonstd::nullopt;
+ }
+
+ virtual nonstd::optional<location_history*> get_location_history()
+ {
+ return nonstd::nullopt;
+ }
+
+ void toggle_apply_filters();
+
+ virtual void text_crumbs_for_line(int line,
+ std::vector<breadcrumb::crumb>& crumbs);
+
+ virtual void quiesce() {}
+
+ bool tss_supports_filtering{false};
+ bool tss_apply_filters{true};
+
+protected:
+ textview_curses* tss_view{nullptr};
+ filter_stack tss_filters;
+};
+
+class vis_location_history : public location_history {
+public:
+ vis_location_history()
+ : vlh_history(std::begin(this->vlh_backing),
+ std::end(this->vlh_backing))
+ {
+ }
+
+ void loc_history_append(vis_line_t top) override;
+
+ nonstd::optional<vis_line_t> loc_history_back(
+ vis_line_t current_top) override;
+
+ nonstd::optional<vis_line_t> loc_history_forward(
+ vis_line_t current_top) override;
+
+ nonstd::ring_span<vis_line_t> vlh_history;
+
+private:
+ vis_line_t current_position()
+ {
+ auto iter = this->vlh_history.rbegin();
+
+ iter += this->lh_history_position;
+
+ return *iter;
+ }
+
+ vis_line_t vlh_backing[MAX_SIZE];
+};
+
+class text_delegate {
+public:
+ virtual ~text_delegate() = default;
+
+ virtual void text_overlay(textview_curses& tc) {}
+
+ virtual bool text_handle_mouse(textview_curses& tc, mouse_event& me)
+ {
+ return false;
+ }
+};
+
+/**
+ * The textview_curses class adds user bookmarks and searching to the standard
+ * list view interface.
+ */
+class textview_curses
+ : public listview_curses
+ , public list_data_source
+ , public grep_proc_source<vis_line_t>
+ , public grep_proc_sink<vis_line_t>
+ , public lnav_config_listener {
+public:
+ using action = std::function<void(textview_curses*)>;
+
+ const static bookmark_type_t BM_USER;
+ const static bookmark_type_t BM_USER_EXPR;
+ const static bookmark_type_t BM_SEARCH;
+ const static bookmark_type_t BM_META;
+
+ textview_curses();
+
+ ~textview_curses();
+
+ void reload_config(error_reporter& reporter);
+
+ void set_paused(bool paused)
+ {
+ this->tc_paused = paused;
+ if (this->tc_state_event_handler) {
+ this->tc_state_event_handler(*this);
+ }
+ }
+
+ bool is_paused() const { return this->tc_paused; }
+
+ vis_bookmarks& get_bookmarks() { return this->tc_bookmarks; }
+
+ const vis_bookmarks& get_bookmarks() const { return this->tc_bookmarks; }
+
+ void toggle_user_mark(const bookmark_type_t* bm,
+ vis_line_t start_line,
+ vis_line_t end_line = vis_line_t(-1));
+
+ void set_user_mark(const bookmark_type_t* bm, vis_line_t vl, bool marked);
+
+ textview_curses& set_sub_source(text_sub_source* src);
+
+ text_sub_source* get_sub_source() const { return this->tc_sub_source; }
+
+ textview_curses& set_delegate(std::shared_ptr<text_delegate> del)
+ {
+ this->tc_delegate = del;
+
+ return *this;
+ }
+
+ std::shared_ptr<text_delegate> get_delegate() const
+ {
+ return this->tc_delegate;
+ }
+
+ nonstd::optional<std::pair<int, int>> horiz_shift(vis_line_t start,
+ vis_line_t end,
+ int off_start);
+
+ void set_search_action(action sa)
+ {
+ this->tc_search_action = std::move(sa);
+ }
+
+ void grep_end_batch(grep_proc<vis_line_t>& gp);
+ void grep_end(grep_proc<vis_line_t>& gp);
+
+ size_t listview_rows(const listview_curses& lv)
+ {
+ return this->tc_sub_source == nullptr
+ ? 0
+ : this->tc_sub_source->text_line_count();
+ }
+
+ size_t listview_width(const listview_curses& lv)
+ {
+ return this->tc_sub_source == nullptr
+ ? 0
+ : this->tc_sub_source->text_line_width(*this);
+ }
+
+ void listview_value_for_rows(const listview_curses& lv,
+ vis_line_t line,
+ std::vector<attr_line_t>& rows_out);
+
+ void textview_value_for_row(vis_line_t line, attr_line_t& value_out);
+
+ bool listview_is_row_selectable(const listview_curses& lv, vis_line_t row);
+
+ void listview_selection_changed(const listview_curses& lv);
+
+ size_t listview_size_for_row(const listview_curses& lv, vis_line_t row)
+ {
+ return this->tc_sub_source->text_size_for_line(*this, row);
+ }
+
+ std::string listview_source_name(const listview_curses& lv)
+ {
+ return this->tc_sub_source == nullptr
+ ? ""
+ : this->tc_sub_source->text_source_name(*this);
+ }
+
+ bool grep_value_for_line(vis_line_t line, std::string& value_out);
+
+ void grep_quiesce()
+ {
+ if (this->tc_sub_source != nullptr) {
+ this->tc_sub_source->quiesce();
+ }
+ }
+
+ void grep_begin(grep_proc<vis_line_t>& gp,
+ vis_line_t start,
+ vis_line_t stop);
+ void grep_match(grep_proc<vis_line_t>& gp,
+ vis_line_t line,
+ int start,
+ int end);
+
+ bool is_searching() const { return this->tc_searching > 0; }
+
+ void set_follow_search_for(int64_t ms_to_deadline,
+ std::function<bool()> func)
+ {
+ struct timeval now, tv;
+
+ tv.tv_sec = ms_to_deadline / 1000;
+ tv.tv_usec = (ms_to_deadline % 1000) * 1000;
+ gettimeofday(&now, nullptr);
+ timeradd(&now, &tv, &this->tc_follow_deadline);
+ this->tc_follow_selection = this->get_selection();
+ this->tc_follow_func = func;
+ }
+
+ size_t get_match_count() { return this->tc_bookmarks[&BM_SEARCH].size(); }
+
+ void match_reset()
+ {
+ this->tc_bookmarks[&BM_SEARCH].clear();
+ if (this->tc_sub_source != nullptr) {
+ this->tc_sub_source->text_clear_marks(&BM_SEARCH);
+ }
+ }
+
+ highlight_map_t& get_highlights() { return this->tc_highlights; }
+
+ const highlight_map_t& get_highlights() const
+ {
+ return this->tc_highlights;
+ }
+
+ std::set<highlight_source_t>& get_disabled_highlights()
+ {
+ return this->tc_disabled_highlights;
+ }
+
+ bool handle_mouse(mouse_event& me);
+
+ void reload_data();
+
+ void do_update()
+ {
+ this->listview_curses::do_update();
+ if (this->tc_delegate != nullptr) {
+ this->tc_delegate->text_overlay(*this);
+ }
+ }
+
+ bool toggle_hide_fields()
+ {
+ bool retval = this->tc_hide_fields;
+
+ this->tc_hide_fields = !this->tc_hide_fields;
+
+ return retval;
+ }
+
+ bool get_hide_fields() const { return this->tc_hide_fields; }
+
+ void execute_search(const std::string& regex_orig);
+
+ void redo_search();
+
+ void search_range(vis_line_t start, vis_line_t stop = -1_vl)
+ {
+ if (this->tc_search_child) {
+ this->tc_search_child->get_grep_proc()->queue_request(start, stop);
+ }
+ if (this->tc_source_search_child) {
+ this->tc_source_search_child->queue_request(start, stop);
+ }
+ }
+
+ void search_new_data(vis_line_t start = -1_vl)
+ {
+ this->search_range(start);
+ if (this->tc_search_child) {
+ this->tc_search_child->get_grep_proc()->start();
+ }
+ if (this->tc_source_search_child) {
+ this->tc_source_search_child->start();
+ }
+ }
+
+ std::string get_current_search() const { return this->tc_current_search; }
+
+ void save_current_search()
+ {
+ this->tc_previous_search = this->tc_current_search;
+ }
+
+ void revert_search() { this->execute_search(this->tc_previous_search); }
+
+ void invoke_scroll()
+ {
+ if (this->tc_sub_source != nullptr) {
+ auto ttt = dynamic_cast<text_time_translator*>(this->tc_sub_source);
+
+ if (ttt != nullptr) {
+ ttt->scroll_invoked(this);
+ }
+ }
+
+ listview_curses::invoke_scroll();
+ }
+
+ textview_curses& set_reload_config_delegate(
+ std::function<void(textview_curses&)> func)
+ {
+ this->tc_reload_config_delegate = std::move(func);
+ if (this->tc_reload_config_delegate) {
+ this->tc_reload_config_delegate(*this);
+ }
+ return *this;
+ }
+
+ std::function<void(textview_curses&)> tc_state_event_handler;
+
+ nonstd::optional<role_t> tc_cursor_role;
+
+protected:
+ class grep_highlighter {
+ public:
+ grep_highlighter(std::shared_ptr<grep_proc<vis_line_t>>& gp,
+ highlight_source_t source,
+ std::string hl_name,
+ highlight_map_t& hl_map)
+ : gh_grep_proc(std::move(gp)), gh_hl_source(source),
+ gh_hl_name(std::move(hl_name)), gh_hl_map(hl_map)
+ {
+ }
+
+ ~grep_highlighter()
+ {
+ this->gh_hl_map.erase(
+ this->gh_hl_map.find({this->gh_hl_source, this->gh_hl_name}));
+ }
+
+ grep_proc<vis_line_t>* get_grep_proc()
+ {
+ return this->gh_grep_proc.get();
+ }
+
+ private:
+ std::shared_ptr<grep_proc<vis_line_t>> gh_grep_proc;
+ highlight_source_t gh_hl_source;
+ std::string gh_hl_name;
+ highlight_map_t& gh_hl_map;
+ };
+
+ text_sub_source* tc_sub_source{nullptr};
+ std::shared_ptr<text_delegate> tc_delegate;
+
+ vis_bookmarks tc_bookmarks;
+
+ int tc_searching{0};
+ struct timeval tc_follow_deadline {
+ 0, 0
+ };
+ vis_line_t tc_follow_selection{-1_vl};
+ std::function<bool()> tc_follow_func;
+ action tc_search_action;
+
+ highlight_map_t tc_highlights;
+ std::set<highlight_source_t> tc_disabled_highlights;
+
+ vis_line_t tc_selection_start{-1_vl};
+ vis_line_t tc_selection_last{-1_vl};
+ bool tc_selection_cleared{false};
+ bool tc_hide_fields{true};
+ bool tc_paused{false};
+
+ std::string tc_current_search;
+ std::string tc_previous_search;
+ std::shared_ptr<grep_highlighter> tc_search_child;
+ std::shared_ptr<grep_proc<vis_line_t>> tc_source_search_child;
+ std::function<void(textview_curses&)> tc_reload_config_delegate;
+};
+
+#endif
diff --git a/src/textview_curses_fwd.hh b/src/textview_curses_fwd.hh
new file mode 100644
index 0000000..683266b
--- /dev/null
+++ b/src/textview_curses_fwd.hh
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_textview_curses_fwd_hh
+#define lnav_textview_curses_fwd_hh
+
+#include <map>
+#include <utility>
+
+#include "highlighter.hh"
+
+enum class highlight_source_t {
+ INTERACTIVE,
+ PREVIEW,
+ CONFIGURATION,
+ THEME,
+ INTERNAL,
+};
+
+using highlight_map_t
+ = std::map<std::pair<highlight_source_t, std::string>, highlighter>;
+
+#endif
diff --git a/src/themes/README.md b/src/themes/README.md
new file mode 100644
index 0000000..f52affd
--- /dev/null
+++ b/src/themes/README.md
@@ -0,0 +1,5 @@
+# Themes
+
+This directory contains the built-in theme definitions. The files are
+turned into C using `bin2c` and compiled into the executable. New themes
+need to be added to the [themes.am](themes.am) file.
diff --git a/src/themes/default-theme.json b/src/themes/default-theme.json
new file mode 100644
index 0000000..5448ed1
--- /dev/null
+++ b/src/themes/default-theme.json
@@ -0,0 +1,245 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "theme-defs": {
+ "default": {
+ "vars": {
+ "semantic_highlight_color": "semantic()"
+ },
+ "styles": {
+ "text": {
+ "color": "Silver",
+ "background-color": "Black"
+ },
+ "identifier": {
+ "background-color": "",
+ "color": "semantic()"
+ },
+ "alt-text": {
+ "color": "Silver",
+ "bold": true
+ },
+ "ok": {
+ "color": "Green",
+ "bold": true
+ },
+ "error": {
+ "color": "Red",
+ "bold": true
+ },
+ "warning": {
+ "color": "Yellow",
+ "bold": true
+ },
+ "hidden": {
+ "color": "Yellow",
+ "bold": true
+ },
+ "cursor-line": {
+ "color": "Cyan",
+ "background-color": "Red",
+ "bold": true,
+ "underline": true
+ },
+ "adjusted-time": {
+ "color": "Maroon"
+ },
+ "skewed-time": {
+ "color": "Yellow"
+ },
+ "offset-time": {
+ "color": "Teal"
+ },
+ "invalid-msg": {
+ "color": "Yellow"
+ },
+ "popup": {
+ "color": "Silver",
+ "background-color": "Teal"
+ },
+ "scrollbar": {
+ "color": "Black",
+ "background-color": "Silver"
+ },
+ "focused": {
+ "color": "Black",
+ "background-color": "Silver"
+ },
+ "disabled-focused": {
+ "color": "Black",
+ "background-color": "#888"
+ },
+ "h1": {
+ "underline": true
+ },
+ "h2": {
+ "underline": true
+ },
+ "h3": {
+ "underline": true
+ },
+ "h4": {
+ "underline": true
+ },
+ "h5": {
+ "underline": true
+ },
+ "h6": {
+ "underline": true
+ },
+ "list-glyph": {
+ "color": "Yellow"
+ },
+ "breadcrumb": {
+ "color": "Teal"
+ }
+ },
+ "syntax-styles": {
+ "keyword": {
+ "color": "Blue"
+ },
+ "string": {
+ "color": "Green",
+ "bold": true
+ },
+ "comment": {
+ "color": "Green"
+ },
+ "doc-directive": {
+ "color": "Teal"
+ },
+ "variable": {
+ "color": "Teal"
+ },
+ "symbol": {
+ "color": "Blue"
+ },
+ "re-special": {
+ "color": "Teal"
+ },
+ "re-repeat": {
+ "color": "Yellow"
+ },
+ "diff-delete": {
+ "color": "Red"
+ },
+ "diff-add": {
+ "color": "Green"
+ },
+ "diff-section": {
+ "color": "Maroon"
+ },
+ "spectrogram-low": {
+ "background-color": "$green"
+ },
+ "spectrogram-medium": {
+ "background-color": "$yellow"
+ },
+ "spectrogram-high": {
+ "background-color": "$red"
+ },
+ "file": {
+ "color": "Blue"
+ },
+ "number": {
+ "bold": true
+ }
+ },
+ "status-styles": {
+ "title": {
+ "color": "Silver",
+ "background-color": "Blue",
+ "bold": true
+ },
+ "disabled-title": {
+ "color": "Black",
+ "background-color": "Silver",
+ "bold": true
+ },
+ "subtitle": {
+ "color": "Black",
+ "background-color": "Teal"
+ },
+ "text": {
+ "color": "Black",
+ "background-color": "Silver"
+ },
+ "warn": {
+ "color": "Yellow",
+ "background-color": "Silver"
+ },
+ "alert": {
+ "color": "Red",
+ "background-color": "Silver"
+ },
+ "active": {
+ "color": "Green",
+ "background-color": "Silver"
+ },
+ "info": {
+ "color": "Silver",
+ "background-color": "Grey37"
+ },
+ "inactive": {
+ "color": "Silver",
+ "background-color": "Grey37"
+ },
+ "inactive-alert": {
+ "color": "Red",
+ "background-color": "Grey37"
+ },
+ "title-hotkey": {
+ "color": "Teal",
+ "background-color": "Blue",
+ "underline": true
+ },
+ "hotkey": {
+ "color": "Purple",
+ "underline": true,
+ "bold": true
+ }
+ },
+ "log-level-styles": {
+ "warning": {
+ "color": "Yellow"
+ },
+ "error": {
+ "color": "Red"
+ },
+ "critical": {
+ "color": "Red"
+ },
+ "fatal": {
+ "color": "Red"
+ }
+ },
+ "highlights": {
+ "colors": {
+ "pattern": "(?:#[a-fA-F0-9]{6}|#[a-fA-F0-9]{3}\\b)",
+ "style": {
+ "color": "${semantic_highlight_color}"
+ }
+ },
+ "ipv4": {
+ "pattern": "\\b(?<!\\d\\.)\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b(?!\\.\\d)",
+ "style": {
+ "color": "${semantic_highlight_color}"
+ }
+ },
+ "xml": {
+ "pattern": "</?([^ >=!]+)[^>]*>",
+ "style": {
+ "color": "${semantic_highlight_color}"
+ }
+ },
+ "xml-decl": {
+ "pattern": "<!([^ >=!]+)[^>]*>",
+ "style": {
+ "color": "${semantic_highlight_color}"
+ }
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/themes/eldar.json b/src/themes/eldar.json
new file mode 100644
index 0000000..f46b6cf
--- /dev/null
+++ b/src/themes/eldar.json
@@ -0,0 +1,200 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "theme-defs": {
+ "eldar": {
+ "vars": {
+ "black": "#000000",
+ "yellow": "#fce94f",
+ "red": "#ef2929",
+ "magenta": "#ad7fa8",
+ "blue": "#729fcf",
+ "cyan": "#34e2e2",
+ "green": "#8ae234",
+ "white": "#ffffff",
+ "semantic_highlight_color": "semantic()"
+ },
+ "styles": {
+ "identifier": {
+ "background-color": "",
+ "color": "semantic()"
+ },
+ "text": {
+ "color": "$white",
+ "background-color": ""
+ },
+ "alt-text": {
+ "color": "$white",
+ "background-color": "",
+ "bold": true
+ },
+ "ok": {
+ "color": "$green",
+ "bold": true
+ },
+ "error": {
+ "color": "$red",
+ "bold": true
+ },
+ "warning": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "hidden": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "cursor-line": {
+ "color": "$cyan",
+ "background-color": "$red",
+ "bold": true,
+ "underline": true
+ },
+ "adjusted-time": {
+ "color": "$magenta"
+ },
+ "skewed-time": {
+ "color": "$yellow"
+ },
+ "offset-time": {
+ "color": "$cyan"
+ },
+ "invalid-msg": {
+ "color": "$yellow"
+ },
+ "popup": {
+ "color": "$black",
+ "background-color": "Grey37"
+ },
+ "scrollbar": {
+ "color": "$black",
+ "background-color": "$white"
+ },
+ "h1": {
+ "underline": true
+ },
+ "h2": {
+ "underline": true
+ },
+ "h3": {
+ "underline": true
+ },
+ "h4": {
+ "underline": true
+ },
+ "h5": {
+ "underline": true
+ },
+ "h6": {
+ "underline": true
+ }
+ },
+ "syntax-styles": {
+ "keyword": {
+ "color": "$yellow"
+ },
+ "string": {
+ "color": "$magenta",
+ "bold": true
+ },
+ "comment": {
+ "color": "$cyan"
+ },
+ "doc-directive": {
+ "color": "$green"
+ },
+ "number": {
+ "color": "$red"
+ },
+ "variable": {
+ "color": "$green"
+ },
+ "symbol": {
+ "color": "$green"
+ },
+ "re-special": {
+ "color": "$cyan"
+ },
+ "re-repeat": {
+ "color": "$yellow"
+ },
+ "diff-delete": {
+ "color": "$red"
+ },
+ "diff-add": {
+ "color": "$green"
+ },
+ "diff-section": {
+ "color": "$magenta"
+ },
+ "spectrogram-low": {
+ "background-color": "$green"
+ },
+ "spectrogram-medium": {
+ "background-color": "$yellow"
+ },
+ "spectrogram-high": {
+ "background-color": "$red"
+ },
+ "file": {
+ "color": "$blue"
+ }
+ },
+ "status-styles": {
+ "title": {
+ "color": "$black",
+ "background-color": "$blue",
+ "bold": true
+ },
+ "subtitle": {
+ "color": "$black",
+ "background-color": "$cyan",
+ "bold": true
+ },
+ "text": {
+ "color": "$black",
+ "background-color": "#999"
+ },
+ "warn": {
+ "color": "$yellow",
+ "background-color": "#999"
+ },
+ "alert": {
+ "color": "$red",
+ "background-color": "#999"
+ },
+ "active": {
+ "color": "$green",
+ "background-color": "#999"
+ },
+ "info": {
+ "color": "$black",
+ "background-color": "Grey"
+ },
+ "inactive": {
+ "color": "$black",
+ "background-color": "Grey"
+ },
+ "inactive-alert": {
+ "color": "$red",
+ "background-color": "Grey"
+ }
+ },
+ "log-level-styles": {
+ "warning": {
+ "color": "$yellow"
+ },
+ "error": {
+ "color": "$red"
+ },
+ "critical": {
+ "color": "$red"
+ },
+ "fatal": {
+ "color": "$red"
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/themes/grayscale.json b/src/themes/grayscale.json
new file mode 100644
index 0000000..4dcbe15
--- /dev/null
+++ b/src/themes/grayscale.json
@@ -0,0 +1,172 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "theme-defs": {
+ "grayscale": {
+ "vars": {
+ "black": "#2d2a2e",
+ "red": "#f92772",
+ "green": "#a7e22e",
+ "yellow": "#fe9720",
+ "blue": "#5394ec",
+ "magenta": "#ae81ff",
+ "cyan": "#66d9ee",
+ "white": "#f6f6f6",
+ "plaintext": "#ccc"
+ },
+ "styles": {
+ "identifier": {
+ "background-color": "",
+ "color": "",
+ "bold": true
+ },
+ "text": {
+ "color": "",
+ "background-color": ""
+ },
+ "alt-text": {
+ "color": "",
+ "background-color": "",
+ "bold": true
+ },
+ "ok": {
+ "color": "$green",
+ "bold": true
+ },
+ "error": {
+ "color": "$red",
+ "bold": true
+ },
+ "warning": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "hidden": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "cursor-line": {
+ "color": "$cyan",
+ "background-color": "$red",
+ "bold": true,
+ "underline": true
+ },
+ "adjusted-time": {
+ "color": "$magenta"
+ },
+ "skewed-time": {
+ "color": "$yellow"
+ },
+ "offset-time": {
+ "color": "$cyan"
+ },
+ "invalid-msg": {
+ "color": "$yellow"
+ },
+ "focused": {
+ "color": "$black",
+ "background-color": "$plaintext"
+ },
+ "disabled-focused": {
+ "color": "$plaintext",
+ "background-color": "#333"
+ },
+ "popup": {
+ "color": "$plaintext",
+ "background-color": "#626262"
+ },
+ "scrollbar": {
+ "color": "$black",
+ "background-color": "#888"
+ },
+ "h1": {
+ "underline": true
+ },
+ "h2": {
+ "underline": true
+ },
+ "h3": {
+ "underline": true
+ },
+ "h4": {
+ "underline": true
+ },
+ "h5": {
+ "underline": true
+ },
+ "h6": {
+ "underline": true
+ }
+ },
+ "status-styles": {
+ "disabled-title": {
+ "color": "#5394ec",
+ "background-color": "#353535",
+ "bold": true
+ },
+ "title": {
+ "color": "#f6f6f6",
+ "background-color": "#8a8a8a",
+ "bold": true
+ },
+ "subtitle": {
+ "color": "#e4e4e4",
+ "background-color": "#626262",
+ "bold": true
+ },
+ "title-hotkey": {
+ "color": "$black",
+ "background-color": "#5394ec",
+ "underline": true
+ },
+ "hotkey": {
+ "color": "#fff",
+ "underline": true
+ },
+ "text": {
+ "color": "#f6f6f6",
+ "background-color": "#353535"
+ },
+ "warn": {
+ "color": "$yellow",
+ "background-color": "#353535"
+ },
+ "alert": {
+ "color": "$red",
+ "background-color": "#353535"
+ },
+ "active": {
+ "color": "$green",
+ "background-color": "#353535"
+ },
+ "info": {
+ "color": "#555",
+ "background-color": "#2f2f2f"
+ },
+ "inactive": {
+ "color": "#555",
+ "background-color": "#2f2f2f"
+ },
+ "inactive-alert": {
+ "color": "$red",
+ "background-color": "#2f2f2f"
+ }
+ },
+ "log-level-styles": {
+ "warning": {
+ "color": "$yellow"
+ },
+ "error": {
+ "color": "$red"
+ },
+ "critical": {
+ "color": "$red"
+ },
+ "fatal": {
+ "color": "$red"
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/themes/monocai.json b/src/themes/monocai.json
new file mode 100644
index 0000000..6edb5ab
--- /dev/null
+++ b/src/themes/monocai.json
@@ -0,0 +1,272 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "theme-defs": {
+ "monocai": {
+ "vars": {
+ "black": "#2d2a2e",
+ "red": "#f92772",
+ "green": "#a7e22e",
+ "yellow": "#fe9720",
+ "blue": "#5394ec",
+ "magenta": "#ae81ff",
+ "cyan": "#66d9ee",
+ "white": "#808080",
+ "semantic_highlight_color": "semantic()"
+ },
+ "styles": {
+ "identifier": {
+ "color": "semantic()"
+ },
+ "text": {
+ "color": "#f6f6f6",
+ "background-color": "$black"
+ },
+ "alt-text": {
+ "color": "#f6f6f6",
+ "background-color": "$black",
+ "bold": true
+ },
+ "ok": {
+ "color": "$green",
+ "bold": true
+ },
+ "info": {
+ "color": "$magenta",
+ "bold": true
+ },
+ "error": {
+ "color": "$red",
+ "bold": true
+ },
+ "warning": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "hidden": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "cursor-line": {
+ "color": "$cyan",
+ "background-color": "$red",
+ "bold": true,
+ "underline": true
+ },
+ "adjusted-time": {
+ "color": "$magenta"
+ },
+ "skewed-time": {
+ "color": "$yellow"
+ },
+ "offset-time": {
+ "color": "$cyan"
+ },
+ "invalid-msg": {
+ "color": "$yellow"
+ },
+ "focused": {
+ "color": "$black",
+ "background-color": "$white"
+ },
+ "disabled-focused": {
+ "color": "$white",
+ "background-color": "#333"
+ },
+ "popup": {
+ "color": "$white",
+ "background-color": "$cyan"
+ },
+ "scrollbar": {
+ "color": "$black",
+ "background-color": "#888"
+ },
+ "h1": {
+ "color": "$magenta",
+ "bold": true
+ },
+ "h2": {
+ "color": "$magenta",
+ "underline": true
+ },
+ "h3": {
+ "color": "$magenta"
+ },
+ "h4": {
+ "underline": true
+ },
+ "h5": {
+ "underline": true
+ },
+ "h6": {
+ "underline": true
+ },
+ "hr": {
+ "color": "#444"
+ },
+ "hyperlink": {
+ "underline": true
+ },
+ "list-glyph": {
+ "color": "$yellow"
+ },
+ "breadcrumb": {
+ "color": "#99a"
+ },
+ "table-border": {
+ "color": "#444"
+ },
+ "table-header": {
+ "bold": true
+ },
+ "quote-border": {
+ "color": "#666",
+ "background-color": "#444"
+ },
+ "quoted-text": {
+ "background-color": "#444"
+ },
+ "footnote-border": {
+ "color": "$blue",
+ "background-color": "#444"
+ },
+ "footnote-text": {
+ "color": "#eee",
+ "background-color": "#444"
+ },
+ "snippet-border": {
+ "color": "$cyan"
+ }
+ },
+ "syntax-styles": {
+ "quoted-code": {
+ "color": "#eee",
+ "background-color": "#121212"
+ },
+ "code-border": {
+ "color": "#444",
+ "background-color": "#121212"
+ },
+ "keyword": {
+ "color": "#ff6188",
+ "bold": true
+ },
+ "string": {
+ "color": "#ffd866",
+ "bold": true
+ },
+ "comment": {
+ "color": "#949194"
+ },
+ "doc-directive": {
+ "color": "#a9dc76"
+ },
+ "variable": {
+ "color": "#a9dc76"
+ },
+ "symbol": {
+ "color": "#78dce8"
+ },
+ "re-special": {
+ "color": "$cyan"
+ },
+ "re-repeat": {
+ "color": "$yellow"
+ },
+ "diff-delete": {
+ "color": "#f00"
+ },
+ "diff-add": {
+ "color": "#0f0"
+ },
+ "diff-section": {
+ "color": "#656e76"
+ },
+ "spectrogram-low": {
+ "background-color": "$green"
+ },
+ "spectrogram-medium": {
+ "background-color": "$yellow"
+ },
+ "spectrogram-high": {
+ "background-color": "$red"
+ },
+ "file": {
+ "color": "$blue"
+ },
+ "number": {
+ "bold": true
+ }
+ },
+ "status-styles": {
+ "disabled-title": {
+ "color": "#5394ec",
+ "background-color": "#353535",
+ "bold": true
+ },
+ "title": {
+ "color": "#f6f6f6",
+ "background-color": "#5394ec",
+ "bold": true
+ },
+ "subtitle": {
+ "color": "#555",
+ "background-color": "#66d9ee",
+ "bold": true
+ },
+ "info": {
+ "color": "#aaa",
+ "background-color": "#2f2f2f"
+ },
+ "title-hotkey": {
+ "color": "$black",
+ "background-color": "#5394ec",
+ "underline": true
+ },
+ "hotkey": {
+ "color": "#fff",
+ "underline": true
+ },
+ "text": {
+ "color": "#f6f6f6",
+ "background-color": "#353535"
+ },
+ "warn": {
+ "color": "$yellow",
+ "background-color": "#353535"
+ },
+ "alert": {
+ "color": "$red",
+ "background-color": "#353535"
+ },
+ "active": {
+ "color": "$green",
+ "background-color": "#353535"
+ },
+ "inactive": {
+ "color": "#555",
+ "background-color": "#2f2f2f"
+ },
+ "inactive-alert": {
+ "color": "$red",
+ "background-color": "#2f2f2f"
+ }
+ },
+ "log-level-styles": {
+ "warning": {
+ "color": "$yellow"
+ },
+ "error": {
+ "color": "$red"
+ },
+ "critical": {
+ "color": "$red"
+ },
+ "fatal": {
+ "color": "$red"
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/themes/night-owl.json b/src/themes/night-owl.json
new file mode 100644
index 0000000..a4210c9
--- /dev/null
+++ b/src/themes/night-owl.json
@@ -0,0 +1,223 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "theme-defs": {
+ "night-owl": {
+ "vars": {
+ "black": "#011627",
+ "red": "#ff6868",
+ "green": "#007f00",
+ "yellow": "#cdcd00",
+ "blue": "#5394ec",
+ "magenta": "#ff70ff",
+ "cyan": "#33cccc",
+ "white": "#d6deeb",
+ "semantic_highlight_color": "semantic()"
+ },
+ "styles": {
+ "identifier": {
+ "background-color": "#011627",
+ "color": "semantic()"
+ },
+ "text": {
+ "color": "#d6deeb",
+ "background-color": "#011627"
+ },
+ "alt-text": {
+ "color": "#d6deeb",
+ "background-color": "#011627",
+ "bold": true
+ },
+ "ok": {
+ "color": "$green",
+ "bold": true
+ },
+ "error": {
+ "color": "#ef5350",
+ "bold": true
+ },
+ "warning": {
+ "color": "#b39554",
+ "bold": true
+ },
+ "hidden": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "cursor-line": {
+ "color": "$cyan",
+ "background-color": "$red",
+ "bold": true,
+ "underline": true
+ },
+ "adjusted-time": {
+ "color": "$magenta"
+ },
+ "skewed-time": {
+ "color": "$yellow"
+ },
+ "offset-time": {
+ "color": "$cyan"
+ },
+ "invalid-msg": {
+ "color": "$yellow"
+ },
+ "focused": {
+ "color": "$black",
+ "background-color": "#666"
+ },
+ "disabled-focused": {
+ "color": "$white",
+ "background-color": "#333"
+ },
+ "popup": {
+ "color": "$base00",
+ "background-color": "$base3"
+ },
+ "scrollbar": {
+ "color": "$black",
+ "background-color": "$white"
+ },
+ "h1": {
+ "underline": true
+ },
+ "h2": {
+ "underline": true
+ },
+ "h3": {
+ "underline": true
+ },
+ "h4": {
+ "underline": true
+ },
+ "h5": {
+ "underline": true
+ },
+ "h6": {
+ "underline": true
+ }
+ },
+ "syntax-styles": {
+ "keyword": {
+ "color": "#c792ea"
+ },
+ "string": {
+ "color": "#ecc48d",
+ "bold": true
+ },
+ "comment": {
+ "color": "#676e95"
+ },
+ "doc-directive": {
+ "color": "#addb67"
+ },
+ "variable": {
+ "color": "#addb67"
+ },
+ "symbol": {
+ "color": "#82aaff"
+ },
+ "number": {
+ "color": "#f78c6c"
+ },
+ "re-special": {
+ "color": "$cyan"
+ },
+ "re-repeat": {
+ "color": "$yellow"
+ },
+ "diff-delete": {
+ "color": "#b03435"
+ },
+ "diff-add": {
+ "color": "#264b33"
+ },
+ "diff-section": {
+ "color": "$magenta"
+ },
+ "spectrogram-low": {
+ "background-color": "$green"
+ },
+ "spectrogram-medium": {
+ "background-color": "$yellow"
+ },
+ "spectrogram-high": {
+ "background-color": "$red"
+ },
+ "file": {
+ "color": "#82aaff"
+ }
+ },
+ "status-styles": {
+ "disabled-title": {
+ "color": "#5394ec",
+ "background-color": "#353535",
+ "bold": true
+ },
+ "title": {
+ "color": "#f8f0f0",
+ "background-color": "#2d5a80",
+ "bold": true
+ },
+ "subtitle": {
+ "color": "#f8f8f0",
+ "background-color": "#005f5f"
+ },
+ "info": {
+ "color": "#aaa",
+ "background-color": "#0F1F2B"
+ },
+ "text": {
+ "color": "#f8f8f0",
+ "background-color": "#162d40"
+ },
+ "warn": {
+ "color": "#b39554",
+ "background-color": "#162d40"
+ },
+ "alert": {
+ "color": "#ef5350",
+ "background-color": "#162d40"
+ },
+ "active": {
+ "color": "#264b33",
+ "background-color": "#162d40"
+ },
+ "inactive": {
+ "color": "#f0f0f0",
+ "background-color": "#0F1F2B"
+ },
+ "inactive-alert": {
+ "color": "#ef5350",
+ "background-color": "#0F1F2B"
+ },
+ "hotkey": {
+ "color": "#2d5a80",
+ "bold": true,
+ "underline": true
+ },
+ "title-hotkey": {
+ "color": "$black",
+ "background-color": "#2d5a80",
+ "bold": true,
+ "underline": true
+ }
+ },
+ "log-level-styles": {
+ "warning": {
+ "color": "#b39554"
+ },
+ "error": {
+ "color": "#ef5350"
+ },
+ "critical": {
+ "color": "#ef5350"
+ },
+ "fatal": {
+ "color": "#ef5350"
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/themes/solarized-dark.json b/src/themes/solarized-dark.json
new file mode 100644
index 0000000..1590de0
--- /dev/null
+++ b/src/themes/solarized-dark.json
@@ -0,0 +1,214 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "theme-defs": {
+ "solarized-dark": {
+ "vars": {
+ "base03": "#002b36",
+ "base02": "#073642",
+ "base01": "#586e75",
+ "base00": "#657b83",
+ "base0": "#839496",
+ "base1": "#93a1a1",
+ "base2": "#eee8d5",
+ "base3": "#fdf6e3",
+ "black": "#002b36",
+ "yellow": "#b58900",
+ "orange": "#cb4b16",
+ "red": "#dc322f",
+ "magenta": "#d33682",
+ "violet": "#6c71c4",
+ "blue": "#268bd2",
+ "cyan": "#2aa198",
+ "green": "#859900",
+ "semantic_highlight_color": "semantic()"
+ },
+ "styles": {
+ "identifier": {
+ "background-color": "$base03",
+ "color": "semantic()"
+ },
+ "text": {
+ "color": "$base0",
+ "background-color": "$base03"
+ },
+ "alt-text": {
+ "color": "$base0",
+ "background-color": "$base03",
+ "bold": true
+ },
+ "ok": {
+ "color": "$green",
+ "bold": true
+ },
+ "error": {
+ "color": "$red",
+ "bold": true
+ },
+ "warning": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "hidden": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "cursor-line": {
+ "color": "$cyan",
+ "background-color": "$red",
+ "bold": true,
+ "underline": true
+ },
+ "adjusted-time": {
+ "color": "$magenta"
+ },
+ "skewed-time": {
+ "color": "$yellow"
+ },
+ "offset-time": {
+ "color": "$cyan"
+ },
+ "invalid-msg": {
+ "color": "$yellow"
+ },
+ "popup": {
+ "color": "$base00",
+ "background-color": "$base3"
+ },
+ "scrollbar": {
+ "color": "$base03",
+ "background-color": "$base0"
+ },
+ "focused": {
+ "color": "$base03",
+ "background-color": "$base01"
+ },
+ "disabled-focused": {
+ "color": "$base0",
+ "background-color": "$base02"
+ },
+ "h1": {
+ "underline": true
+ },
+ "h2": {
+ "underline": true
+ },
+ "h3": {
+ "underline": true
+ },
+ "h4": {
+ "underline": true
+ },
+ "h5": {
+ "underline": true
+ },
+ "h6": {
+ "underline": true
+ }
+ },
+ "syntax-styles": {
+ "keyword": {
+ "color": "$yellow"
+ },
+ "string": {
+ "color": "$cyan",
+ "bold": true
+ },
+ "comment": {
+ "color": "$base01"
+ },
+ "doc-directive": {
+ "color": "$cyan"
+ },
+ "variable": {
+ "color": "$blue"
+ },
+ "symbol": {
+ "color": "$blue"
+ },
+ "re-special": {
+ "color": "$cyan"
+ },
+ "re-repeat": {
+ "color": "$yellow"
+ },
+ "diff-delete": {
+ "color": "$red"
+ },
+ "diff-add": {
+ "color": "$green"
+ },
+ "diff-section": {
+ "color": "$magenta"
+ },
+ "spectrogram-low": {
+ "background-color": "$green"
+ },
+ "spectrogram-medium": {
+ "background-color": "$yellow"
+ },
+ "spectrogram-high": {
+ "background-color": "$red"
+ },
+ "file": {
+ "color": "$blue"
+ }
+ },
+ "status-styles": {
+ "title": {
+ "color": "$base02",
+ "background-color": "$blue",
+ "bold": true
+ },
+ "subtitle": {
+ "color": "$base00",
+ "background-color": "$cyan",
+ "bold": true
+ },
+ "text": {
+ "color": "$base2",
+ "background-color": "$base01"
+ },
+ "warn": {
+ "color": "$yellow",
+ "background-color": "$base01"
+ },
+ "alert": {
+ "color": "$red",
+ "background-color": "$base01"
+ },
+ "active": {
+ "color": "$green",
+ "background-color": "$base01"
+ },
+ "info": {
+ "color": "$base1",
+ "background-color": "$base02"
+ },
+ "inactive": {
+ "color": "$base1",
+ "background-color": "$base02"
+ },
+ "inactive-alert": {
+ "color": "$red",
+ "background-color": "$base02"
+ }
+ },
+ "log-level-styles": {
+ "warning": {
+ "color": "$yellow"
+ },
+ "error": {
+ "color": "$red"
+ },
+ "critical": {
+ "color": "$red"
+ },
+ "fatal": {
+ "color": "$red"
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/themes/solarized-light.json b/src/themes/solarized-light.json
new file mode 100644
index 0000000..3e7c575
--- /dev/null
+++ b/src/themes/solarized-light.json
@@ -0,0 +1,214 @@
+{
+ "$schema": "https://lnav.org/schemas/config-v1.schema.json",
+ "ui": {
+ "theme-defs": {
+ "solarized-light": {
+ "vars": {
+ "base03": "#002b36",
+ "base02": "#073642",
+ "base01": "#586e75",
+ "base00": "#657b83",
+ "base0": "#839496",
+ "base1": "#93a1a1",
+ "base2": "#eee8d5",
+ "base3": "#fdf6e3",
+ "black": "#002b36",
+ "yellow": "#b58900",
+ "orange": "#cb4b16",
+ "red": "#dc322f",
+ "magenta": "#d33682",
+ "violet": "#6c71c4",
+ "blue": "#268bd2",
+ "cyan": "#2aa198",
+ "green": "#859900",
+ "semantic_highlight_color": "semantic()"
+ },
+ "styles": {
+ "identifier": {
+ "background-color": "$base3",
+ "color": "semantic()"
+ },
+ "text": {
+ "color": "$base00",
+ "background-color": "$base3"
+ },
+ "alt-text": {
+ "color": "$base00",
+ "background-color": "$base3",
+ "bold": true
+ },
+ "ok": {
+ "color": "$green",
+ "bold": true
+ },
+ "error": {
+ "color": "$red",
+ "bold": true
+ },
+ "warning": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "hidden": {
+ "color": "$yellow",
+ "bold": true
+ },
+ "cursor-line": {
+ "color": "$cyan",
+ "background-color": "$red",
+ "bold": true,
+ "underline": true
+ },
+ "adjusted-time": {
+ "color": "$magenta"
+ },
+ "skewed-time": {
+ "color": "$yellow"
+ },
+ "offset-time": {
+ "color": "$cyan"
+ },
+ "invalid-msg": {
+ "color": "$yellow"
+ },
+ "popup": {
+ "color": "$base00",
+ "background-color": "$base3"
+ },
+ "scrollbar": {
+ "color": "$base3",
+ "background-color": "$base00"
+ },
+ "focused": {
+ "color": "$base03",
+ "background-color": "$base01"
+ },
+ "disabled-focused": {
+ "color": "$base0",
+ "background-color": "$base02"
+ },
+ "h1": {
+ "underline": true
+ },
+ "h2": {
+ "underline": true
+ },
+ "h3": {
+ "underline": true
+ },
+ "h4": {
+ "underline": true
+ },
+ "h5": {
+ "underline": true
+ },
+ "h6": {
+ "underline": true
+ }
+ },
+ "syntax-styles": {
+ "keyword": {
+ "color": "$yellow"
+ },
+ "string": {
+ "color": "$cyan",
+ "bold": true
+ },
+ "comment": {
+ "color": "$base1"
+ },
+ "doc-directive": {
+ "color": "$blue"
+ },
+ "variable": {
+ "color": "$blue"
+ },
+ "symbol": {
+ "color": "$blue"
+ },
+ "re-special": {
+ "color": "$cyan"
+ },
+ "re-repeat": {
+ "color": "$yellow"
+ },
+ "diff-delete": {
+ "color": "$red"
+ },
+ "diff-add": {
+ "color": "$green"
+ },
+ "diff-section": {
+ "color": "$magenta"
+ },
+ "spectrogram-low": {
+ "background-color": "$green"
+ },
+ "spectrogram-medium": {
+ "background-color": "$yellow"
+ },
+ "spectrogram-high": {
+ "background-color": "$red"
+ },
+ "file": {
+ "color": "$blue"
+ }
+ },
+ "status-styles": {
+ "title": {
+ "color": "$base2",
+ "background-color": "$base0",
+ "bold": true
+ },
+ "subtitle": {
+ "color": "$base2",
+ "background-color": "$base01",
+ "bold": true
+ },
+ "text": {
+ "color": "$base2",
+ "background-color": "$base03"
+ },
+ "warn": {
+ "color": "$yellow",
+ "background-color": "$base03"
+ },
+ "alert": {
+ "color": "$red",
+ "background-color": "$base03"
+ },
+ "active": {
+ "color": "$green",
+ "background-color": "$base03"
+ },
+ "info": {
+ "color": "$base1",
+ "background-color": "$base03"
+ },
+ "inactive": {
+ "color": "$base1",
+ "background-color": "$base03"
+ },
+ "inactive-alert": {
+ "color": "$red",
+ "background-color": "$base03"
+ }
+ },
+ "log-level-styles": {
+ "warning": {
+ "color": "$yellow"
+ },
+ "error": {
+ "color": "$red"
+ },
+ "critical": {
+ "color": "$red"
+ },
+ "fatal": {
+ "color": "$red"
+ }
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/src/themes/themes.am b/src/themes/themes.am
new file mode 100644
index 0000000..42eacb6
--- /dev/null
+++ b/src/themes/themes.am
@@ -0,0 +1,10 @@
+
+THEME_FILES = \
+ $(srcdir)/%reldir%/default-theme.json \
+ $(srcdir)/%reldir%/eldar.json \
+ $(srcdir)/%reldir%/grayscale.json \
+ $(srcdir)/%reldir%/monocai.json \
+ $(srcdir)/%reldir%/night-owl.json \
+ $(srcdir)/%reldir%/solarized-dark.json \
+ $(srcdir)/%reldir%/solarized-light.json \
+ $()
diff --git a/src/third-party/ArenaAlloc/arenaalloc.h b/src/third-party/ArenaAlloc/arenaalloc.h
new file mode 100644
index 0000000..dfd648d
--- /dev/null
+++ b/src/third-party/ArenaAlloc/arenaalloc.h
@@ -0,0 +1,186 @@
+// -*- c++ -*-
+/******************************************************************************
+ * arenaalloc.h
+ *
+ * Arena allocator based on the example logic provided by Nicolai Josuttis
+ * and available at http://www.josuttis.com/libbook/examples.html.
+ * This enhanced work is provided under the terms of the MIT license.
+ *
+ *****************************************************************************/
+
+#ifndef _ARENA_ALLOC_H
+#define _ARENA_ALLOC_H
+
+#include <limits>
+#include <memory>
+
+#if __cplusplus >= 201103L
+#include <type_traits>
+#include <utility>
+#endif
+
+// Define macro ARENA_ALLOC_DEBUG to enable some tracing of the allocator
+#include "arenaallocimpl.h"
+
+namespace ArenaAlloc
+{
+
+ struct _newAllocatorImpl
+ {
+ // these two functions should be supported by a specialized
+ // allocator for shared memory or another source of specialized
+ // memory such as device mapped memory.
+ void* allocate( size_t numBytes ) { return new char[ numBytes ]; }
+ void deallocate( void* ptr ) { delete[]( (char*)ptr ); }
+ };
+
+ template <class T,
+ class AllocatorImpl = _newAllocatorImpl,
+ class MemblockImpl = _memblockimpl<AllocatorImpl> >
+ class Alloc {
+
+ private:
+ MemblockImpl* m_impl;
+
+ public:
+ // type definitions
+ typedef T value_type;
+ typedef T* pointer;
+ typedef const T* const_pointer;
+ typedef T& reference;
+ typedef const T& const_reference;
+ typedef std::size_t size_type;
+ typedef std::ptrdiff_t difference_type;
+
+#if __cplusplus >= 201103L
+ // when containers are swapped, (i.e. vector.swap)
+ // swap the allocators also. This was not specified in c++98
+ // thus users of this code not using c++11 must
+ // exercise caution when using the swap algorithm or
+ // specialized swap member function. Specifically,
+ // don't swap containers not sharing the same
+ // allocator internal implementation in c++98. This is ok
+ // in c++11.
+ typedef std::true_type propagate_on_container_swap;
+
+ // container moves should move the allocator also.
+ typedef std::true_type propagate_on_container_move_assignment;
+#endif
+
+ // rebind allocator to type U
+ template <class U>
+ struct rebind {
+ typedef Alloc<U,AllocatorImpl,MemblockImpl> other;
+ };
+
+ // return address of values
+ pointer address (reference value) const {
+ return &value;
+ }
+ const_pointer address (const_reference value) const {
+ return &value;
+ }
+
+ Alloc( std::size_t defaultSize = 32768, AllocatorImpl allocImpl = AllocatorImpl() ) throw():
+ m_impl( MemblockImpl::create( defaultSize, allocImpl ) )
+ {
+ }
+
+ Alloc(const Alloc& src) throw():
+ m_impl( src.m_impl )
+ {
+ m_impl->incrementRefCount();
+ }
+
+ template <class U>
+ Alloc (const Alloc<U,AllocatorImpl,MemblockImpl>& src) throw():
+ m_impl( 0 )
+ {
+ MemblockImpl::assign( src, m_impl );
+ m_impl->incrementRefCount();
+ }
+
+ ~Alloc() throw()
+ {
+ m_impl->decrementRefCount();
+ }
+
+ // return maximum number of elements that can be allocated
+ size_type max_size () const throw()
+ {
+ return std::numeric_limits<std::size_t>::max() / sizeof(T);
+ }
+
+ // allocate but don't initialize num elements of type T
+ pointer allocate (size_type num, const void* = 0)
+ {
+ return reinterpret_cast<pointer>( m_impl->allocate(num*sizeof(T)) );
+ }
+
+ // initialize elements of allocated storage p with value value
+#if __cplusplus >= 201103L
+
+ // use c++11 style forwarding to construct the object
+ template< typename P, typename... Args>
+ void construct( P* obj, Args&&... args )
+ {
+ ::new((void*) obj ) P( std::forward<Args>( args )... );
+ }
+
+ template< typename P >
+ void destroy( P* obj ) { obj->~P(); }
+
+#else
+ void construct (pointer p, const T& value)
+ {
+ new((void*)p)T(value);
+ }
+ void destroy (pointer p) { p->~T(); }
+#endif
+
+ // deallocate storage p of deleted elements
+ void deallocate (pointer p, size_type num)
+ {
+ m_impl->deallocate( p );
+ }
+
+ bool equals( const MemblockImpl * impl ) const
+ {
+ return impl == m_impl;
+ }
+
+ bool operator == ( const Alloc& t2 ) const
+ {
+ return m_impl == t2.m_impl;
+ }
+
+ friend MemblockImpl;
+
+ template< typename Other >
+ bool operator == ( const Alloc< Other, AllocatorImpl, MemblockImpl >& t2 )
+ {
+ return t2.equals( m_impl );
+ }
+
+ template< typename Other >
+ bool operator != ( const Alloc< Other, AllocatorImpl, MemblockImpl >& t2 )
+ {
+ return !t2.equals( m_impl );
+ }
+
+ // These are extension functions not required for an stl allocator
+ size_t getNumAllocations() { return m_impl->getNumAllocations(); }
+ size_t getNumDeallocations() { return m_impl->getNumDeallocations(); }
+ size_t getNumBytesAllocated() { return m_impl->getNumBytesAllocated(); }
+ };
+
+ template<typename A>
+ template<typename T>
+ void _memblockimpl<A>::assign( const Alloc<T,A, _memblockimpl<A> >& src, _memblockimpl<A> *& dest )
+ {
+ dest = const_cast<_memblockimpl<A>* >(src.m_impl);
+ }
+
+}
+
+#endif
diff --git a/src/third-party/ArenaAlloc/arenaallocimpl.h b/src/third-party/ArenaAlloc/arenaallocimpl.h
new file mode 100644
index 0000000..879e0d2
--- /dev/null
+++ b/src/third-party/ArenaAlloc/arenaallocimpl.h
@@ -0,0 +1,290 @@
+// -*- c++ -*-
+/******************************************************************************
+ ** arenaallocimpl.h
+ **
+ ** Internal implementation types of the arena allocator
+ ** MIT license
+ *****************************************************************************/
+
+#ifndef _ARENA_ALLOC_IMPL_H
+#define _ARENA_ALLOC_IMPL_H
+
+#ifdef ARENA_ALLOC_DEBUG
+#include <stdio.h>
+#endif
+
+#include <stdint.h>
+
+namespace ArenaAlloc
+{
+
+ template< typename T, typename A, typename M >
+ class Alloc;
+
+ // internal structure for tracking memory blocks
+ template < typename AllocImpl >
+ struct _memblock
+ {
+ // allocations are rounded up to a multiple of the size of this
+ // struct to maintain proper alignment for any pointer and double
+ // values stored in the allocation.
+ // A future goal is to support even stricter alignment for example
+ // to support cache alignment, special device dependent mappings,
+ // or GPU ops.
+ union _roundsize {
+ double d;
+ void* p;
+ };
+
+ _memblock* m_next{nullptr}; // blocks kept link listed for cleanup at end
+ std::size_t m_bufferSize; // size of the buffer
+ std::size_t m_index; // index of next allocatable byte in the block
+ char* m_buffer; // pointer to large block to allocate from
+
+ _memblock(std::size_t bufferSize, AllocImpl& allocImpl)
+ : m_bufferSize(roundSize(bufferSize)), m_index(0),
+ m_buffer(reinterpret_cast<char*>(allocImpl.allocate(
+ bufferSize))) // this works b/c of order of decl
+ {
+ }
+
+ std::size_t roundSize( std::size_t numBytes )
+ {
+ // this is subject to overflow. calling logic should not permit
+ // an attempt to allocate a really massive size.
+ // i.e. an attempt to allocate 10s of terabytes should be an error
+ return ( ( numBytes + sizeof( _roundsize ) - 1 ) /
+ sizeof( _roundsize ) ) * sizeof( _roundsize );
+ }
+
+ char * allocate( std::size_t numBytes )
+ {
+ std::size_t roundedSize = roundSize( numBytes );
+ if( roundedSize + m_index > m_bufferSize )
+ return 0;
+
+ char * ptrToReturn = &m_buffer[ m_index ];
+ m_index += roundedSize;
+ return ptrToReturn;
+ }
+
+ void dispose( AllocImpl& impl )
+ {
+ impl.deallocate( m_buffer );
+ }
+
+ ~_memblock()
+ {
+ }
+ };
+
+ template< typename AllocatorImpl, typename Derived >
+ struct _memblockimplbase
+ {
+ AllocatorImpl m_alloc;
+ std::size_t m_refCount; // when refs -> 0 delete this
+ std::size_t m_defaultSize;
+
+ std::size_t m_numAllocate; // number of times allocate called
+ std::size_t m_numDeallocate; // number of time deallocate called
+ std::size_t m_numBytesAllocated; // A good estimate of amount of space used
+
+ _memblock<AllocatorImpl> * m_head;
+ _memblock<AllocatorImpl> * m_current;
+
+ // round up 2 next power of 2 if not already
+ // a power of 2
+ std::size_t roundpow2( std::size_t value )
+ {
+ // note this works because subtracting 1 is equivalent to
+ // inverting the lowest set bit and complementing any
+ // bits lower than that. only a power of 2
+ // will yield 0 in the following check
+ if( 0 == ( value & ( value - 1 ) ) )
+ return value; // already a power of 2
+
+ // fold t over itself. This will set all bits after the highest set bit of t to 1
+ // who said bit twiddling wasn't practical?
+ value |= value >> 1;
+ value |= value >> 2;
+ value |= value >> 4;
+ value |= value >> 8;
+ value |= value >> 16;
+#if SIZE_MAX > UINT32_MAX
+ value |= value >> 32;
+#endif
+
+ return value + 1;
+ }
+
+ _memblockimplbase( std::size_t defaultSize, AllocatorImpl& allocator ):
+ m_alloc( allocator ),
+ m_refCount( 1 ),
+ m_defaultSize( defaultSize ),
+ m_numAllocate( 0 ),
+ m_numDeallocate( 0 ),
+ m_numBytesAllocated( 0 ),
+ m_head( 0 ),
+ m_current( 0 )
+ {
+ if( m_defaultSize < 256 )
+ {
+ m_defaultSize = 256; // anything less is academic. a more practical size is 4k or more
+ }
+ else if ( m_defaultSize > 1024UL*1024*1024*16 )
+ {
+ // when this becomes a problem, this package has succeeded beyond my wildest expectations
+ m_defaultSize = 1024UL*1024*1024*16;
+ }
+
+ // for convenience block size should be a power of 2
+ // round up to next power of 2
+ m_defaultSize = roundpow2( m_defaultSize );
+ allocateNewBlock( m_defaultSize );
+ }
+
+ char * allocate( std::size_t numBytes )
+ {
+ char * ptrToReturn = m_current->allocate( numBytes );
+ if( !ptrToReturn )
+ {
+ allocateNewBlock( numBytes > m_defaultSize / 2 ? roundpow2( numBytes*2 ) :
+ m_defaultSize );
+
+ ptrToReturn = m_current->allocate( numBytes );
+ }
+
+#ifdef ARENA_ALLOC_DEBUG
+ fprintf( stdout, "_memblockimpl=%p allocated %ld bytes at address=%p\n", this, numBytes, ptrToReturn );
+#endif
+
+ ++ m_numAllocate;
+ m_numBytesAllocated += numBytes; // does not account for the small overhead in tracking the allocation
+
+ return ptrToReturn;
+ }
+
+ void allocateNewBlock( std::size_t blockSize )
+ {
+ _memblock<AllocatorImpl> * newBlock = new ( m_alloc.allocate( sizeof( _memblock<AllocatorImpl> ) ) )
+ _memblock<AllocatorImpl>( blockSize, m_alloc );
+
+#ifdef ARENA_ALLOC_DEBUG
+ fprintf( stdout, "_memblockimplbase=%p allocating a new block of size=%ld\n", this, blockSize );
+#endif
+
+ if( m_head == 0 )
+ {
+ m_head = m_current = newBlock;
+ }
+ else
+ {
+ m_current->m_next = newBlock;
+ m_current = newBlock;
+ }
+ }
+
+ void deallocate( void * ptr )
+ {
+ ++ m_numDeallocate;
+ }
+
+ size_t getNumAllocations() { return m_numAllocate; }
+ size_t getNumDeallocations() { return m_numDeallocate; }
+ size_t getNumBytesAllocated() { return m_numBytesAllocated; }
+
+ void clear()
+ {
+ _memblock<AllocatorImpl> * block = m_head;
+ while( block )
+ {
+ _memblock<AllocatorImpl> * curr = block;
+ block = block->m_next;
+ curr->dispose( m_alloc );
+ curr->~_memblock<AllocatorImpl>();
+ m_alloc.deallocate( curr );
+ }
+ }
+
+ // The ref counting model does not permit the sharing of
+ // this object across multiple threads unless an external locking mechanism is applied
+ // to ensure the atomicity of the reference count.
+ void incrementRefCount()
+ {
+ ++m_refCount;
+#ifdef ARENA_ALLOC_DEBUG
+ fprintf( stdout, "ref count on _memblockimplbase=%p incremented to %ld\n", this, m_refCount );
+#endif
+ }
+
+ void decrementRefCount()
+ {
+ --m_refCount;
+#ifdef ARENA_ALLOC_DEBUG
+ fprintf( stdout, "ref count on _memblockimplbase=%p decremented to %ld\n", this, m_refCount );
+#endif
+
+ if( m_refCount == 0 )
+ {
+ Derived::destroy( static_cast<Derived*>(this) );
+ }
+ }
+ };
+
+
+ // Each allocator points to an instance of _memblockimpl which
+ // contains the list of _memblock objects and other tracking info
+ // including a refcount.
+ // This object is instantiated in space obtained from the allocator
+ // implementation. The allocator implementation is the component
+ // on which allocate/deallocate are called to obtain storage from.
+ template< typename AllocatorImpl >
+ struct _memblockimpl : public _memblockimplbase<AllocatorImpl, _memblockimpl<AllocatorImpl> >
+ {
+ private:
+
+ typedef struct _memblockimplbase< AllocatorImpl, _memblockimpl<AllocatorImpl> > base_t;
+ friend struct _memblockimplbase< AllocatorImpl, _memblockimpl<AllocatorImpl> >;
+
+ // to get around some sticky access issues between Alloc<T1> and Alloc<T2> when sharing
+ // the implementation.
+ template <typename U, typename A, typename M >
+ friend class Alloc;
+
+ template< typename T >
+ static void assign( const Alloc<T,AllocatorImpl, _memblockimpl<AllocatorImpl> >& src,
+ _memblockimpl *& dest );
+
+ static _memblockimpl<AllocatorImpl> * create( size_t defaultSize, AllocatorImpl& alloc )
+ {
+ return new ( alloc.allocate( sizeof( _memblockimpl ) ) ) _memblockimpl<AllocatorImpl>( defaultSize,
+ alloc );
+ }
+
+ static void destroy( _memblockimpl<AllocatorImpl> * objToDestroy )
+ {
+ AllocatorImpl allocImpl = objToDestroy->m_alloc;
+ objToDestroy-> ~_memblockimpl<AllocatorImpl>();
+ allocImpl.deallocate( objToDestroy );
+ }
+
+ _memblockimpl( std::size_t defaultSize, AllocatorImpl& allocImpl ):
+ _memblockimplbase<AllocatorImpl, _memblockimpl<AllocatorImpl> >( defaultSize, allocImpl )
+ {
+#ifdef ARENA_ALLOC_DEBUG
+ fprintf( stdout, "_memblockimpl=%p constructed with default size=%ld\n", this,
+ base_t::m_defaultSize );
+#endif
+ }
+
+ ~_memblockimpl( )
+ {
+#ifdef ARENA_ALLOC_DEBUG
+ fprintf( stdout, "~memblockimpl() called on _memblockimpl=%p\n", this );
+#endif
+ base_t::clear();
+ }
+ };
+}
+
+#endif
diff --git a/src/third-party/ArenaAlloc/recyclealloc.h b/src/third-party/ArenaAlloc/recyclealloc.h
new file mode 100644
index 0000000..129e43b
--- /dev/null
+++ b/src/third-party/ArenaAlloc/recyclealloc.h
@@ -0,0 +1,184 @@
+// -*- c++ -*-
+/******************************************************************************
+ ** recyclealloc.h
+ **
+ ** Arena allocator with some modest recycling of freed resources.
+ ** MIT license
+ **
+ *****************************************************************************/
+#ifndef _RECYCLE_ALLOC_H
+#define _RECYCLE_ALLOC_H
+
+#include "arenaalloc.h"
+#include <string.h>
+#include <inttypes.h>
+
+namespace ArenaAlloc
+{
+
+ // todo:
+ // attempt refactor of boilerplate in _memblockimpl and _recycleallocimpl
+ template< typename AllocatorImpl, uint16_t StepSize = 16, uint16_t NumBuckets = 256 >
+ struct _recycleallocimpl : public _memblockimplbase<AllocatorImpl, _recycleallocimpl<AllocatorImpl> >
+ {
+ private:
+
+ static_assert( ( StepSize >= 16 && NumBuckets >= 16 ), "Min step size=16, Min num buckets=16" );
+ static_assert( !( StepSize & ( StepSize - 1 ) ), "Step size must be a power of 2" );
+
+ struct _freeEntry
+ {
+ // note: order of declaration matters
+ std::size_t m_size;
+ _freeEntry * m_next;
+ };
+
+ _freeEntry * m_buckets[ NumBuckets ]; // m_buckets[ NumBuckets - 1 ] is the oversize bucket
+
+ typedef struct _memblockimplbase< AllocatorImpl, _recycleallocimpl<AllocatorImpl> > base_t;
+ friend struct _memblockimplbase< AllocatorImpl, _recycleallocimpl<AllocatorImpl> >;
+
+ // to get around some sticky access issues between Alloc<T1> and Alloc<T2> when sharing
+ // the implementation.
+ template <typename U, typename A, typename M >
+ friend class Alloc;
+
+ template< typename T >
+ static void assign( const Alloc<T,AllocatorImpl, _recycleallocimpl<AllocatorImpl> >& src,
+ _recycleallocimpl *& dest )
+ {
+ dest = const_cast< _recycleallocimpl<AllocatorImpl>* >( src.m_impl );
+ }
+
+ static _recycleallocimpl<AllocatorImpl> * create( std::size_t defaultSize, AllocatorImpl& alloc )
+ {
+ return new (
+ alloc.allocate( sizeof( _recycleallocimpl ) ) ) _recycleallocimpl<AllocatorImpl>( defaultSize,
+ alloc );
+ }
+
+ static void destroy( _recycleallocimpl<AllocatorImpl> * objToDestroy )
+ {
+ AllocatorImpl allocImpl = objToDestroy->m_alloc;
+ objToDestroy-> ~_recycleallocimpl<AllocatorImpl>();
+ allocImpl.deallocate( objToDestroy );
+ }
+
+ _recycleallocimpl( std::size_t defaultSize, AllocatorImpl& allocImpl ):
+ _memblockimplbase<AllocatorImpl, _recycleallocimpl<AllocatorImpl> >( defaultSize, allocImpl )
+ {
+ memset( m_buckets, 0, sizeof( m_buckets ) );
+
+#ifdef ARENA_ALLOC_DEBUG
+ fprintf( stdout, "_recycleallocimpl=%p constructed with default size=%ld\n", this,
+ base_t::m_defaultSize );
+#endif
+ }
+
+ ~_recycleallocimpl( )
+ {
+#ifdef ARENA_ALLOC_DEBUG
+ fprintf( stdout, "~_recycleallocimpl() called on _recycleallocimpl=%p\n", this );
+#endif
+ base_t::clear();
+ }
+
+ char * allocate( std::size_t numBytes )
+ {
+
+ numBytes = ( (numBytes + sizeof( std::size_t ) + StepSize - 1) / StepSize ) * StepSize;
+
+ char * returnValue = allocateInternal( numBytes );
+ if( !returnValue )
+ {
+ char * allocValue = base_t::allocate( numBytes );
+
+ if( !allocValue )
+ return 0; //allocation failure
+
+ *((std::size_t*)allocValue ) = numBytes; // that includes the header
+ return allocValue + sizeof( std::size_t );
+ }
+
+ return returnValue;
+ }
+
+ void deallocate( void * ptr )
+ {
+ deallocateInternal( reinterpret_cast<char*>(ptr) );
+ base_t::deallocate( ptr ); // this is called b/c it is known this just updates stats
+ }
+
+ char * allocateInternal( std::size_t numBytes )
+ {
+ // numBytes must already be rounded to a multiple of stepsize and have an
+ // extra sizeof( std::size_t ) bytes tacked on for the header
+ // pointer returned points sizeof( std::size_t ) bytes into the allocation
+ // bucket 0 is always null in this scheme.
+
+ uint16_t bucketNumber = numBytes / StepSize;
+
+ if( bucketNumber > NumBuckets - 1 )
+ bucketNumber = NumBuckets - 1; // oversize alloc
+
+ // search max 3 consecutive buckets for an item large enough.
+ // in the oversize bucket and only in the oversize bucket,
+ // search upto 3 items into the linked list for an entry
+ // large enough for the specified size
+ for( uint16_t bkt = bucketNumber, i = 0; i < 3 && bkt < NumBuckets; ++i, ++bkt )
+ {
+ if( m_buckets[ bkt ] )
+ return allocateFrom( numBytes, m_buckets[ bkt ] );
+ }
+
+ return 0;
+ }
+
+ char * allocateFrom( std::size_t numBytes, _freeEntry *& head )
+ {
+ _freeEntry * current = head;
+ _freeEntry * prev = 0;
+
+ int count = 0;
+
+ while( current && count < 3 )
+ {
+ if( current->m_size >= numBytes )
+ {
+ if( prev == 0 )
+ head = current->m_next;
+ else
+ prev->m_next = current->m_next;
+
+ return reinterpret_cast<char*>(&current->m_next);
+ }
+
+ ++count;
+ prev = current;
+ current = current->m_next;
+ }
+
+ return 0;
+ }
+
+ void deallocateInternal( char * ptr )
+ {
+ _freeEntry * v = reinterpret_cast< _freeEntry* >( ptr - sizeof( std::size_t ) );
+ uint16_t bucketNumber = v->m_size / StepSize;
+
+ if( bucketNumber > NumBuckets - 1 )
+ bucketNumber = NumBuckets - 1;
+
+ _freeEntry * next = m_buckets[ bucketNumber ];
+ v->m_next = next;
+ m_buckets[ bucketNumber ] = v;
+ }
+
+ };
+
+ template< typename T, typename Allocator = _newAllocatorImpl >
+ using RecycleAlloc = Alloc< T, Allocator, _recycleallocimpl<Allocator> >;
+
+}
+
+#endif
diff --git a/src/third-party/CLI/App.hpp b/src/third-party/CLI/App.hpp
new file mode 100644
index 0000000..0a70bac
--- /dev/null
+++ b/src/third-party/CLI/App.hpp
@@ -0,0 +1,3246 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <algorithm>
+#include <cstdint>
+#include <functional>
+#include <iostream>
+#include <iterator>
+#include <memory>
+#include <numeric>
+#include <set>
+#include <sstream>
+#include <string>
+#include <utility>
+#include <vector>
+// [CLI11:public_includes:end]
+
+// CLI Library includes
+#include "ConfigFwd.hpp"
+#include "Error.hpp"
+#include "FormatterFwd.hpp"
+#include "Macros.hpp"
+#include "Option.hpp"
+#include "Split.hpp"
+#include "StringTools.hpp"
+#include "TypeTools.hpp"
+
+namespace CLI {
+// [CLI11:app_hpp:verbatim]
+
+#ifndef CLI11_PARSE
+#define CLI11_PARSE(app, argc, argv) \
+ try { \
+ (app).parse((argc), (argv)); \
+ } catch(const CLI::ParseError &e) { \
+ return (app).exit(e); \
+ }
+#endif
+
+namespace detail {
+enum class Classifier { NONE, POSITIONAL_MARK, SHORT, LONG, WINDOWS_STYLE, SUBCOMMAND, SUBCOMMAND_TERMINATOR };
+struct AppFriend;
+} // namespace detail
+
+namespace FailureMessage {
+std::string simple(const App *app, const Error &e);
+std::string help(const App *app, const Error &e);
+} // namespace FailureMessage
+
+/// enumeration of modes of how to deal with extras in config files
+
+enum class config_extras_mode : char { error = 0, ignore, ignore_all, capture };
+
+class App;
+
+using App_p = std::shared_ptr<App>;
+
+namespace detail {
+/// helper functions for adding in appropriate flag modifiers for add_flag
+
+template <typename T, enable_if_t<!std::is_integral<T>::value || (sizeof(T) <= 1U), detail::enabler> = detail::dummy>
+Option *default_flag_modifiers(Option *opt) {
+ return opt->always_capture_default();
+}
+
+/// summing modifiers
+template <typename T, enable_if_t<std::is_integral<T>::value && (sizeof(T) > 1U), detail::enabler> = detail::dummy>
+Option *default_flag_modifiers(Option *opt) {
+ return opt->multi_option_policy(MultiOptionPolicy::Sum)->default_str("0")->force_callback();
+}
+
+} // namespace detail
+
+class Option_group;
+/// Creates a command line program, with very few defaults.
+/** To use, create a new `Program()` instance with `argc`, `argv`, and a help description. The templated
+ * add_option methods make it easy to prepare options. Remember to call `.start` before starting your
+ * program, so that the options can be evaluated and the help option doesn't accidentally run your program. */
+class App {
+ friend Option;
+ friend detail::AppFriend;
+
+ protected:
+ // This library follows the Google style guide for member names ending in underscores
+
+ /// @name Basics
+ ///@{
+
+ /// Subcommand name or program name (from parser if name is empty)
+ std::string name_{};
+
+ /// Description of the current program/subcommand
+ std::string description_{};
+
+ /// If true, allow extra arguments (ie, don't throw an error). INHERITABLE
+ bool allow_extras_{false};
+
+ /// If ignore, allow extra arguments in the ini file (ie, don't throw an error). INHERITABLE
+ /// if error error on an extra argument, and if capture feed it to the app
+ config_extras_mode allow_config_extras_{config_extras_mode::ignore};
+
+ /// If true, return immediately on an unrecognized option (implies allow_extras) INHERITABLE
+ bool prefix_command_{false};
+
+ /// If set to true the name was automatically generated from the command line vs a user set name
+ bool has_automatic_name_{false};
+
+ /// If set to true the subcommand is required to be processed and used, ignored for main app
+ bool required_{false};
+
+ /// If set to true the subcommand is disabled and cannot be used, ignored for main app
+ bool disabled_{false};
+
+ /// Flag indicating that the pre_parse_callback has been triggered
+ bool pre_parse_called_{false};
+
+ /// Flag indicating that the callback for the subcommand should be executed immediately on parse completion which is
+ /// before help or ini files are processed. INHERITABLE
+ bool immediate_callback_{false};
+
+ /// This is a function that runs prior to the start of parsing
+ std::function<void(std::size_t)> pre_parse_callback_{};
+
+ /// This is a function that runs when parsing has finished.
+ std::function<void()> parse_complete_callback_{};
+
+ /// This is a function that runs when all processing has completed
+ std::function<void()> final_callback_{};
+
+ ///@}
+ /// @name Options
+ ///@{
+
+ /// The default values for options, customizable and changeable INHERITABLE
+ OptionDefaults option_defaults_{};
+
+ /// The list of options, stored locally
+ std::vector<Option_p> options_{};
+
+ ///@}
+ /// @name Help
+ ///@{
+
+ /// Footer to put after all options in the help output INHERITABLE
+ std::string footer_{};
+
+ /// This is a function that generates a footer to put after all other options in help output
+ std::function<std::string()> footer_callback_{};
+
+ /// A pointer to the help flag if there is one INHERITABLE
+ Option *help_ptr_{nullptr};
+
+ /// A pointer to the help all flag if there is one INHERITABLE
+ Option *help_all_ptr_{nullptr};
+
+ /// A pointer to a version flag if there is one
+ Option *version_ptr_{nullptr};
+
+ /// This is the formatter for help printing. Default provided. INHERITABLE (same pointer)
+ std::shared_ptr<FormatterBase> formatter_{new Formatter()};
+
+ /// The error message printing function INHERITABLE
+ std::function<std::string(const App *, const Error &e)> failure_message_{FailureMessage::simple};
+
+ ///@}
+ /// @name Parsing
+ ///@{
+
+ using missing_t = std::vector<std::pair<detail::Classifier, std::string>>;
+
+ /// Pair of classifier, string for missing options. (extra detail is removed on returning from parse)
+ ///
+ /// This is faster and cleaner than storing just a list of strings and reparsing. This may contain the -- separator.
+ missing_t missing_{};
+
+ /// This is a list of pointers to options with the original parse order
+ std::vector<Option *> parse_order_{};
+
+ /// This is a list of the subcommands collected, in order
+ std::vector<App *> parsed_subcommands_{};
+
+ /// this is a list of subcommands that are exclusionary to this one
+ std::set<App *> exclude_subcommands_{};
+
+ /// This is a list of options which are exclusionary to this App, if the options were used this subcommand should
+ /// not be
+ std::set<Option *> exclude_options_{};
+
+ /// this is a list of subcommands or option groups that are required by this one, the list is not mutual, the
+ /// listed subcommands do not require this one
+ std::set<App *> need_subcommands_{};
+
+ /// This is a list of options which are required by this app, the list is not mutual, listed options do not need the
+ /// subcommand not be
+ std::set<Option *> need_options_{};
+
+ ///@}
+ /// @name Subcommands
+ ///@{
+
+ /// Storage for subcommand list
+ std::vector<App_p> subcommands_{};
+
+ /// If true, the program name is not case sensitive INHERITABLE
+ bool ignore_case_{false};
+
+ /// If true, the program should ignore underscores INHERITABLE
+ bool ignore_underscore_{false};
+
+ /// Allow subcommand fallthrough, so that parent commands can collect commands after subcommand. INHERITABLE
+ bool fallthrough_{false};
+
+ /// Allow '/' for options for Windows like options. Defaults to true on Windows, false otherwise. INHERITABLE
+ bool allow_windows_style_options_{
+#ifdef _WIN32
+ true
+#else
+ false
+#endif
+ };
+ /// specify that positional arguments come at the end of the argument sequence not inheritable
+ bool positionals_at_end_{false};
+
+ enum class startup_mode : char { stable, enabled, disabled };
+ /// specify the startup mode for the app
+ /// stable=no change, enabled= startup enabled, disabled=startup disabled
+ startup_mode default_startup{startup_mode::stable};
+
+ /// if set to true the subcommand can be triggered via configuration files INHERITABLE
+ bool configurable_{false};
+
+ /// If set to true positional options are validated before assigning INHERITABLE
+ bool validate_positionals_{false};
+
+ /// If set to true optional vector arguments are validated before assigning INHERITABLE
+ bool validate_optional_arguments_{false};
+
+ /// indicator that the subcommand is silent and won't show up in subcommands list
+ /// This is potentially useful as a modifier subcommand
+ bool silent_{false};
+
+ /// Counts the number of times this command/subcommand was parsed
+ std::uint32_t parsed_{0U};
+
+ /// Minimum required subcommands (not inheritable!)
+ std::size_t require_subcommand_min_{0};
+
+ /// Max number of subcommands allowed (parsing stops after this number). 0 is unlimited INHERITABLE
+ std::size_t require_subcommand_max_{0};
+
+ /// Minimum required options (not inheritable!)
+ std::size_t require_option_min_{0};
+
+ /// Max number of options allowed. 0 is unlimited (not inheritable)
+ std::size_t require_option_max_{0};
+
+ /// A pointer to the parent if this is a subcommand
+ App *parent_{nullptr};
+
+ /// The group membership INHERITABLE
+ std::string group_{"Subcommands"};
+
+ /// Alias names for the subcommand
+ std::vector<std::string> aliases_{};
+
+ ///@}
+ /// @name Config
+ ///@{
+
+ /// Pointer to the config option
+ Option *config_ptr_{nullptr};
+
+ /// This is the formatter for help printing. Default provided. INHERITABLE (same pointer)
+ std::shared_ptr<Config> config_formatter_{new ConfigTOML()};
+
+ ///@}
+
+ /// Special private constructor for subcommand
+ App(std::string app_description, std::string app_name, App *parent)
+ : name_(std::move(app_name)), description_(std::move(app_description)), parent_(parent) {
+ // Inherit if not from a nullptr
+ if(parent_ != nullptr) {
+ if(parent_->help_ptr_ != nullptr)
+ set_help_flag(parent_->help_ptr_->get_name(false, true), parent_->help_ptr_->get_description());
+ if(parent_->help_all_ptr_ != nullptr)
+ set_help_all_flag(parent_->help_all_ptr_->get_name(false, true),
+ parent_->help_all_ptr_->get_description());
+
+ /// OptionDefaults
+ option_defaults_ = parent_->option_defaults_;
+
+ // INHERITABLE
+ failure_message_ = parent_->failure_message_;
+ allow_extras_ = parent_->allow_extras_;
+ allow_config_extras_ = parent_->allow_config_extras_;
+ prefix_command_ = parent_->prefix_command_;
+ immediate_callback_ = parent_->immediate_callback_;
+ ignore_case_ = parent_->ignore_case_;
+ ignore_underscore_ = parent_->ignore_underscore_;
+ fallthrough_ = parent_->fallthrough_;
+ validate_positionals_ = parent_->validate_positionals_;
+ validate_optional_arguments_ = parent_->validate_optional_arguments_;
+ configurable_ = parent_->configurable_;
+ allow_windows_style_options_ = parent_->allow_windows_style_options_;
+ group_ = parent_->group_;
+ footer_ = parent_->footer_;
+ formatter_ = parent_->formatter_;
+ config_formatter_ = parent_->config_formatter_;
+ require_subcommand_max_ = parent_->require_subcommand_max_;
+ }
+ }
+
+ public:
+ /// @name Basic
+ ///@{
+
+ /// Create a new program. Pass in the same arguments as main(), along with a help string.
+ explicit App(std::string app_description = "", std::string app_name = "")
+ : App(app_description, app_name, nullptr) {
+ set_help_flag("-h,--help", "Print this help message and exit");
+ }
+
+ App(const App &) = delete;
+ App &operator=(const App &) = delete;
+
+ /// virtual destructor
+ virtual ~App() = default;
+
+ /// Set a callback for execution when all parsing and processing has completed
+ ///
+ /// Due to a bug in c++11,
+ /// it is not possible to overload on std::function (fixed in c++14
+ /// and backported to c++11 on newer compilers). Use capture by reference
+ /// to get a pointer to App if needed.
+ App *callback(std::function<void()> app_callback) {
+ if(immediate_callback_) {
+ parse_complete_callback_ = std::move(app_callback);
+ } else {
+ final_callback_ = std::move(app_callback);
+ }
+ return this;
+ }
+
+ /// Set a callback for execution when all parsing and processing has completed
+ /// aliased as callback
+ App *final_callback(std::function<void()> app_callback) {
+ final_callback_ = std::move(app_callback);
+ return this;
+ }
+
+ /// Set a callback to execute when parsing has completed for the app
+ ///
+ App *parse_complete_callback(std::function<void()> pc_callback) {
+ parse_complete_callback_ = std::move(pc_callback);
+ return this;
+ }
+
+ /// Set a callback to execute prior to parsing.
+ ///
+ App *preparse_callback(std::function<void(std::size_t)> pp_callback) {
+ pre_parse_callback_ = std::move(pp_callback);
+ return this;
+ }
+
+ /// Set a name for the app (empty will use parser to set the name)
+ App *name(std::string app_name = "") {
+
+ if(parent_ != nullptr) {
+ auto oname = name_;
+ name_ = app_name;
+ auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent());
+ if(!res.empty()) {
+ name_ = oname;
+ throw(OptionAlreadyAdded(app_name + " conflicts with existing subcommand names"));
+ }
+ } else {
+ name_ = app_name;
+ }
+ has_automatic_name_ = false;
+ return this;
+ }
+
+ /// Set an alias for the app
+ App *alias(std::string app_name) {
+ if(app_name.empty() || !detail::valid_alias_name_string(app_name)) {
+ throw IncorrectConstruction("Aliases may not be empty or contain newlines or null characters");
+ }
+ if(parent_ != nullptr) {
+ aliases_.push_back(app_name);
+ auto &res = _compare_subcommand_names(*this, *_get_fallthrough_parent());
+ if(!res.empty()) {
+ aliases_.pop_back();
+ throw(OptionAlreadyAdded("alias already matches an existing subcommand: " + app_name));
+ }
+ } else {
+ aliases_.push_back(app_name);
+ }
+
+ return this;
+ }
+
+ /// Remove the error when extras are left over on the command line.
+ App *allow_extras(bool allow = true) {
+ allow_extras_ = allow;
+ return this;
+ }
+
+ /// Remove the error when extras are left over on the command line.
+ App *required(bool require = true) {
+ required_ = require;
+ return this;
+ }
+
+ /// Disable the subcommand or option group
+ App *disabled(bool disable = true) {
+ disabled_ = disable;
+ return this;
+ }
+
+ /// silence the subcommand from showing up in the processed list
+ App *silent(bool silence = true) {
+ silent_ = silence;
+ return this;
+ }
+
+ /// Set the subcommand to be disabled by default, so on clear(), at the start of each parse it is disabled
+ App *disabled_by_default(bool disable = true) {
+ if(disable) {
+ default_startup = startup_mode::disabled;
+ } else {
+ default_startup = (default_startup == startup_mode::enabled) ? startup_mode::enabled : startup_mode::stable;
+ }
+ return this;
+ }
+
+ /// Set the subcommand to be enabled by default, so on clear(), at the start of each parse it is enabled (not
+ /// disabled)
+ App *enabled_by_default(bool enable = true) {
+ if(enable) {
+ default_startup = startup_mode::enabled;
+ } else {
+ default_startup =
+ (default_startup == startup_mode::disabled) ? startup_mode::disabled : startup_mode::stable;
+ }
+ return this;
+ }
+
+ /// Set the subcommand callback to be executed immediately on subcommand completion
+ App *immediate_callback(bool immediate = true) {
+ immediate_callback_ = immediate;
+ if(immediate_callback_) {
+ if(final_callback_ && !(parse_complete_callback_)) {
+ std::swap(final_callback_, parse_complete_callback_);
+ }
+ } else if(!(final_callback_) && parse_complete_callback_) {
+ std::swap(final_callback_, parse_complete_callback_);
+ }
+ return this;
+ }
+
+ /// Set the subcommand to validate positional arguments before assigning
+ App *validate_positionals(bool validate = true) {
+ validate_positionals_ = validate;
+ return this;
+ }
+
+ /// Set the subcommand to validate optional vector arguments before assigning
+ App *validate_optional_arguments(bool validate = true) {
+ validate_optional_arguments_ = validate;
+ return this;
+ }
+
+ /// ignore extras in config files
+ App *allow_config_extras(bool allow = true) {
+ if(allow) {
+ allow_config_extras_ = config_extras_mode::capture;
+ allow_extras_ = true;
+ } else {
+ allow_config_extras_ = config_extras_mode::error;
+ }
+ return this;
+ }
+
+ /// ignore extras in config files
+ App *allow_config_extras(config_extras_mode mode) {
+ allow_config_extras_ = mode;
+ return this;
+ }
+
+ /// Do not parse anything after the first unrecognized option and return
+ App *prefix_command(bool allow = true) {
+ prefix_command_ = allow;
+ return this;
+ }
+
+ /// Ignore case. Subcommands inherit value.
+ App *ignore_case(bool value = true) {
+ if(value && !ignore_case_) {
+ ignore_case_ = true;
+ auto *p = (parent_ != nullptr) ? _get_fallthrough_parent() : this;
+ auto &match = _compare_subcommand_names(*this, *p);
+ if(!match.empty()) {
+ ignore_case_ = false; // we are throwing so need to be exception invariant
+ throw OptionAlreadyAdded("ignore case would cause subcommand name conflicts: " + match);
+ }
+ }
+ ignore_case_ = value;
+ return this;
+ }
+
+ /// Allow windows style options, such as `/opt`. First matching short or long name used. Subcommands inherit
+ /// value.
+ App *allow_windows_style_options(bool value = true) {
+ allow_windows_style_options_ = value;
+ return this;
+ }
+
+ /// Specify that the positional arguments are only at the end of the sequence
+ App *positionals_at_end(bool value = true) {
+ positionals_at_end_ = value;
+ return this;
+ }
+
+ /// Specify that the subcommand can be triggered by a config file
+ App *configurable(bool value = true) {
+ configurable_ = value;
+ return this;
+ }
+
+ /// Ignore underscore. Subcommands inherit value.
+ App *ignore_underscore(bool value = true) {
+ if(value && !ignore_underscore_) {
+ ignore_underscore_ = true;
+ auto *p = (parent_ != nullptr) ? _get_fallthrough_parent() : this;
+ auto &match = _compare_subcommand_names(*this, *p);
+ if(!match.empty()) {
+ ignore_underscore_ = false;
+ throw OptionAlreadyAdded("ignore underscore would cause subcommand name conflicts: " + match);
+ }
+ }
+ ignore_underscore_ = value;
+ return this;
+ }
+
+ /// Set the help formatter
+ App *formatter(std::shared_ptr<FormatterBase> fmt) {
+ formatter_ = fmt;
+ return this;
+ }
+
+ /// Set the help formatter
+ App *formatter_fn(std::function<std::string(const App *, std::string, AppFormatMode)> fmt) {
+ formatter_ = std::make_shared<FormatterLambda>(fmt);
+ return this;
+ }
+
+ /// Set the config formatter
+ App *config_formatter(std::shared_ptr<Config> fmt) {
+ config_formatter_ = fmt;
+ return this;
+ }
+
+ /// Check to see if this subcommand was parsed, true only if received on command line.
+ bool parsed() const { return parsed_ > 0; }
+
+ /// Get the OptionDefault object, to set option defaults
+ OptionDefaults *option_defaults() { return &option_defaults_; }
+
+ ///@}
+ /// @name Adding options
+ ///@{
+
+ /// Add an option, will automatically understand the type for common types.
+ ///
+ /// To use, create a variable with the expected type, and pass it in after the name.
+ /// After start is called, you can use count to see if the value was passed, and
+ /// the value will be initialized properly. Numbers, vectors, and strings are supported.
+ ///
+ /// ->required(), ->default, and the validators are options,
+ /// The positional options take an optional number of arguments.
+ ///
+ /// For example,
+ ///
+ /// std::string filename;
+ /// program.add_option("filename", filename, "description of filename");
+ ///
+ Option *add_option(std::string option_name,
+ callback_t option_callback,
+ std::string option_description = "",
+ bool defaulted = false,
+ std::function<std::string()> func = {}) {
+ Option myopt{option_name, option_description, option_callback, this};
+
+ if(std::find_if(std::begin(options_), std::end(options_), [&myopt](const Option_p &v) {
+ return *v == myopt;
+ }) == std::end(options_)) {
+ options_.emplace_back();
+ Option_p &option = options_.back();
+ option.reset(new Option(option_name, option_description, option_callback, this));
+
+ // Set the default string capture function
+ option->default_function(func);
+
+ // For compatibility with CLI11 1.7 and before, capture the default string here
+ if(defaulted)
+ option->capture_default_str();
+
+ // Transfer defaults to the new option
+ option_defaults_.copy_to(option.get());
+
+ // Don't bother to capture if we already did
+ if(!defaulted && option->get_always_capture_default())
+ option->capture_default_str();
+
+ return option.get();
+ }
+ // we know something matches now find what it is so we can produce more error information
+ for(auto &opt : options_) {
+ auto &matchname = opt->matching_name(myopt);
+ if(!matchname.empty()) {
+ throw(OptionAlreadyAdded("added option matched existing option name: " + matchname));
+ }
+ }
+ // this line should not be reached the above loop should trigger the throw
+ throw(OptionAlreadyAdded("added option matched existing option name")); // LCOV_EXCL_LINE
+ }
+
+ /// Add option for assigning to a variable
+ template <typename AssignTo,
+ typename ConvertTo = AssignTo,
+ enable_if_t<!std::is_const<ConvertTo>::value, detail::enabler> = detail::dummy>
+ Option *add_option(std::string option_name,
+ AssignTo &variable, ///< The variable to set
+ std::string option_description = "") {
+
+ auto fun = [&variable](const CLI::results_t &res) { // comment for spacing
+ return detail::lexical_conversion<AssignTo, ConvertTo>(res, variable);
+ };
+
+ Option *opt = add_option(option_name, fun, option_description, false, [&variable]() {
+ return CLI::detail::checked_to_string<AssignTo, ConvertTo>(variable);
+ });
+ opt->type_name(detail::type_name<ConvertTo>());
+ // these must be actual lvalues since (std::max) sometimes is defined in terms of references and references
+ // to structs used in the evaluation can be temporary so that would cause issues.
+ auto Tcount = detail::type_count<AssignTo>::value;
+ auto XCcount = detail::type_count<ConvertTo>::value;
+ opt->type_size(detail::type_count_min<ConvertTo>::value, (std::max)(Tcount, XCcount));
+ opt->expected(detail::expected_count<ConvertTo>::value);
+ opt->run_callback_for_default();
+ return opt;
+ }
+
+ /// Add option for assigning to a variable
+ template <typename AssignTo, enable_if_t<!std::is_const<AssignTo>::value, detail::enabler> = detail::dummy>
+ Option *add_option_no_stream(std::string option_name,
+ AssignTo &variable, ///< The variable to set
+ std::string option_description = "") {
+
+ auto fun = [&variable](const CLI::results_t &res) { // comment for spacing
+ return detail::lexical_conversion<AssignTo, AssignTo>(res, variable);
+ };
+
+ Option *opt = add_option(option_name, fun, option_description, false, []() { return std::string{}; });
+ opt->type_name(detail::type_name<AssignTo>());
+ opt->type_size(detail::type_count_min<AssignTo>::value, detail::type_count<AssignTo>::value);
+ opt->expected(detail::expected_count<AssignTo>::value);
+ opt->run_callback_for_default();
+ return opt;
+ }
+
+ /// Add option for a callback of a specific type
+ template <typename ArgType>
+ Option *add_option_function(std::string option_name,
+ const std::function<void(const ArgType &)> &func, ///< the callback to execute
+ std::string option_description = "") {
+
+ auto fun = [func](const CLI::results_t &res) {
+ ArgType variable;
+ bool result = detail::lexical_conversion<ArgType, ArgType>(res, variable);
+ if(result) {
+ func(variable);
+ }
+ return result;
+ };
+
+ Option *opt = add_option(option_name, std::move(fun), option_description, false);
+ opt->type_name(detail::type_name<ArgType>());
+ opt->type_size(detail::type_count_min<ArgType>::value, detail::type_count<ArgType>::value);
+ opt->expected(detail::expected_count<ArgType>::value);
+ return opt;
+ }
+
+ /// Add option with no description or variable assignment
+ Option *add_option(std::string option_name) {
+ return add_option(option_name, CLI::callback_t{}, std::string{}, false);
+ }
+
+ /// Add option with description but with no variable assignment or callback
+ template <typename T,
+ enable_if_t<std::is_const<T>::value && std::is_constructible<std::string, T>::value, detail::enabler> =
+ detail::dummy>
+ Option *add_option(std::string option_name, T &option_description) {
+ return add_option(option_name, CLI::callback_t(), option_description, false);
+ }
+
+ /// Set a help flag, replace the existing one if present
+ Option *set_help_flag(std::string flag_name = "", const std::string &help_description = "") {
+ // take flag_description by const reference otherwise add_flag tries to assign to help_description
+ if(help_ptr_ != nullptr) {
+ remove_option(help_ptr_);
+ help_ptr_ = nullptr;
+ }
+
+ // Empty name will simply remove the help flag
+ if(!flag_name.empty()) {
+ help_ptr_ = add_flag(flag_name, help_description);
+ help_ptr_->configurable(false);
+ }
+
+ return help_ptr_;
+ }
+
+ /// Set a help all flag, replaced the existing one if present
+ Option *set_help_all_flag(std::string help_name = "", const std::string &help_description = "") {
+ // take flag_description by const reference otherwise add_flag tries to assign to flag_description
+ if(help_all_ptr_ != nullptr) {
+ remove_option(help_all_ptr_);
+ help_all_ptr_ = nullptr;
+ }
+
+ // Empty name will simply remove the help all flag
+ if(!help_name.empty()) {
+ help_all_ptr_ = add_flag(help_name, help_description);
+ help_all_ptr_->configurable(false);
+ }
+
+ return help_all_ptr_;
+ }
+
+ /// Set a version flag and version display string, replace the existing one if present
+ Option *set_version_flag(std::string flag_name = "",
+ const std::string &versionString = "",
+ const std::string &version_help = "Display program version information and exit") {
+ // take flag_description by const reference otherwise add_flag tries to assign to version_description
+ if(version_ptr_ != nullptr) {
+ remove_option(version_ptr_);
+ version_ptr_ = nullptr;
+ }
+
+ // Empty name will simply remove the version flag
+ if(!flag_name.empty()) {
+ version_ptr_ = add_flag_callback(
+ flag_name, [versionString]() { throw(CLI::CallForVersion(versionString, 0)); }, version_help);
+ version_ptr_->configurable(false);
+ }
+
+ return version_ptr_;
+ }
+ /// Generate the version string through a callback function
+ Option *set_version_flag(std::string flag_name,
+ std::function<std::string()> vfunc,
+ const std::string &version_help = "Display program version information and exit") {
+ if(version_ptr_ != nullptr) {
+ remove_option(version_ptr_);
+ version_ptr_ = nullptr;
+ }
+
+ // Empty name will simply remove the version flag
+ if(!flag_name.empty()) {
+ version_ptr_ = add_flag_callback(
+ flag_name, [vfunc]() { throw(CLI::CallForVersion(vfunc(), 0)); }, version_help);
+ version_ptr_->configurable(false);
+ }
+
+ return version_ptr_;
+ }
+
+ private:
+ /// Internal function for adding a flag
+ Option *_add_flag_internal(std::string flag_name, CLI::callback_t fun, std::string flag_description) {
+ Option *opt;
+ if(detail::has_default_flag_values(flag_name)) {
+ // check for default values and if it has them
+ auto flag_defaults = detail::get_default_flag_values(flag_name);
+ detail::remove_default_flag_values(flag_name);
+ opt = add_option(std::move(flag_name), std::move(fun), std::move(flag_description), false);
+ for(const auto &fname : flag_defaults)
+ opt->fnames_.push_back(fname.first);
+ opt->default_flag_values_ = std::move(flag_defaults);
+ } else {
+ opt = add_option(std::move(flag_name), std::move(fun), std::move(flag_description), false);
+ }
+ // flags cannot have positional values
+ if(opt->get_positional()) {
+ auto pos_name = opt->get_name(true);
+ remove_option(opt);
+ throw IncorrectConstruction::PositionalFlag(pos_name);
+ }
+ opt->multi_option_policy(MultiOptionPolicy::TakeLast);
+ opt->expected(0);
+ opt->required(false);
+ return opt;
+ }
+
+ public:
+ /// Add a flag with no description or variable assignment
+ Option *add_flag(std::string flag_name) { return _add_flag_internal(flag_name, CLI::callback_t(), std::string{}); }
+
+ /// Add flag with description but with no variable assignment or callback
+ /// takes a constant string, if a variable string is passed that variable will be assigned the results from the
+ /// flag
+ template <typename T,
+ enable_if_t<std::is_const<T>::value && std::is_constructible<std::string, T>::value, detail::enabler> =
+ detail::dummy>
+ Option *add_flag(std::string flag_name, T &flag_description) {
+ return _add_flag_internal(flag_name, CLI::callback_t(), flag_description);
+ }
+
+ /// Other type version accepts all other types that are not vectors such as bool, enum, string or other classes
+ /// that can be converted from a string
+ template <typename T,
+ enable_if_t<!detail::is_mutable_container<T>::value && !std::is_const<T>::value &&
+ !std::is_constructible<std::function<void(int)>, T>::value,
+ detail::enabler> = detail::dummy>
+ Option *add_flag(std::string flag_name,
+ T &flag_result, ///< A variable holding the flag result
+ std::string flag_description = "") {
+
+ CLI::callback_t fun = [&flag_result](const CLI::results_t &res) {
+ return CLI::detail::lexical_cast(res[0], flag_result);
+ };
+ auto *opt = _add_flag_internal(flag_name, std::move(fun), std::move(flag_description));
+ return detail::default_flag_modifiers<T>(opt);
+ }
+
+ /// Vector version to capture multiple flags.
+ template <typename T,
+ enable_if_t<!std::is_assignable<std::function<void(std::int64_t)> &, T>::value, detail::enabler> =
+ detail::dummy>
+ Option *add_flag(std::string flag_name,
+ std::vector<T> &flag_results, ///< A vector of values with the flag results
+ std::string flag_description = "") {
+ CLI::callback_t fun = [&flag_results](const CLI::results_t &res) {
+ bool retval = true;
+ for(const auto &elem : res) {
+ flag_results.emplace_back();
+ retval &= detail::lexical_cast(elem, flag_results.back());
+ }
+ return retval;
+ };
+ return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description))
+ ->multi_option_policy(MultiOptionPolicy::TakeAll)
+ ->run_callback_for_default();
+ }
+
+ /// Add option for callback that is triggered with a true flag and takes no arguments
+ Option *add_flag_callback(std::string flag_name,
+ std::function<void(void)> function, ///< A function to call, void(void)
+ std::string flag_description = "") {
+
+ CLI::callback_t fun = [function](const CLI::results_t &res) {
+ bool trigger{false};
+ auto result = CLI::detail::lexical_cast(res[0], trigger);
+ if(result && trigger) {
+ function();
+ }
+ return result;
+ };
+ return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description));
+ }
+
+ /// Add option for callback with an integer value
+ Option *add_flag_function(std::string flag_name,
+ std::function<void(std::int64_t)> function, ///< A function to call, void(int)
+ std::string flag_description = "") {
+
+ CLI::callback_t fun = [function](const CLI::results_t &res) {
+ std::int64_t flag_count{0};
+ CLI::detail::lexical_cast(res[0], flag_count);
+ function(flag_count);
+ return true;
+ };
+ return _add_flag_internal(flag_name, std::move(fun), std::move(flag_description))
+ ->multi_option_policy(MultiOptionPolicy::Sum);
+ }
+
+#ifdef CLI11_CPP14
+ /// Add option for callback (C++14 or better only)
+ Option *add_flag(std::string flag_name,
+ std::function<void(std::int64_t)> function, ///< A function to call, void(std::int64_t)
+ std::string flag_description = "") {
+ return add_flag_function(std::move(flag_name), std::move(function), std::move(flag_description));
+ }
+#endif
+
+ /// Set a configuration ini file option, or clear it if no name passed
+ Option *set_config(std::string option_name = "",
+ std::string default_filename = "",
+ const std::string &help_message = "Read an ini file",
+ bool config_required = false) {
+
+ // Remove existing config if present
+ if(config_ptr_ != nullptr) {
+ remove_option(config_ptr_);
+ config_ptr_ = nullptr; // need to remove the config_ptr completely
+ }
+
+ // Only add config if option passed
+ if(!option_name.empty()) {
+ config_ptr_ = add_option(option_name, help_message);
+ if(config_required) {
+ config_ptr_->required();
+ }
+ if(!default_filename.empty()) {
+ config_ptr_->default_str(std::move(default_filename));
+ }
+ config_ptr_->configurable(false);
+ }
+
+ return config_ptr_;
+ }
+
+ /// Removes an option from the App. Takes an option pointer. Returns true if found and removed.
+ bool remove_option(Option *opt) {
+ // Make sure no links exist
+ for(Option_p &op : options_) {
+ op->remove_needs(opt);
+ op->remove_excludes(opt);
+ }
+
+ if(help_ptr_ == opt)
+ help_ptr_ = nullptr;
+ if(help_all_ptr_ == opt)
+ help_all_ptr_ = nullptr;
+
+ auto iterator =
+ std::find_if(std::begin(options_), std::end(options_), [opt](const Option_p &v) { return v.get() == opt; });
+ if(iterator != std::end(options_)) {
+ options_.erase(iterator);
+ return true;
+ }
+ return false;
+ }
+
+ /// creates an option group as part of the given app
+ template <typename T = Option_group>
+ T *add_option_group(std::string group_name, std::string group_description = "") {
+ if(!detail::valid_alias_name_string(group_name)) {
+ throw IncorrectConstruction("option group names may not contain newlines or null characters");
+ }
+ auto option_group = std::make_shared<T>(std::move(group_description), group_name, this);
+ auto ptr = option_group.get();
+ // move to App_p for overload resolution on older gcc versions
+ App_p app_ptr = std::dynamic_pointer_cast<App>(option_group);
+ add_subcommand(std::move(app_ptr));
+ return ptr;
+ }
+
+ ///@}
+ /// @name Subcommands
+ ///@{
+
+ /// Add a subcommand. Inherits INHERITABLE and OptionDefaults, and help flag
+ App *add_subcommand(std::string subcommand_name = "", std::string subcommand_description = "") {
+ if(!subcommand_name.empty() && !detail::valid_name_string(subcommand_name)) {
+ if(!detail::valid_first_char(subcommand_name[0])) {
+ throw IncorrectConstruction(
+ "Subcommand name starts with invalid character, '!' and '-' are not allowed");
+ }
+ for(auto c : subcommand_name) {
+ if(!detail::valid_later_char(c)) {
+ throw IncorrectConstruction(std::string("Subcommand name contains invalid character ('") + c +
+ "'), all characters are allowed except"
+ "'=',':','{','}', and ' '");
+ }
+ }
+ }
+ CLI::App_p subcom = std::shared_ptr<App>(new App(std::move(subcommand_description), subcommand_name, this));
+ return add_subcommand(std::move(subcom));
+ }
+
+ /// Add a previously created app as a subcommand
+ App *add_subcommand(CLI::App_p subcom) {
+ if(!subcom)
+ throw IncorrectConstruction("passed App is not valid");
+ auto ckapp = (name_.empty() && parent_ != nullptr) ? _get_fallthrough_parent() : this;
+ auto &mstrg = _compare_subcommand_names(*subcom, *ckapp);
+ if(!mstrg.empty()) {
+ throw(OptionAlreadyAdded("subcommand name or alias matches existing subcommand: " + mstrg));
+ }
+ subcom->parent_ = this;
+ subcommands_.push_back(std::move(subcom));
+ return subcommands_.back().get();
+ }
+
+ /// Removes a subcommand from the App. Takes a subcommand pointer. Returns true if found and removed.
+ bool remove_subcommand(App *subcom) {
+ // Make sure no links exist
+ for(App_p &sub : subcommands_) {
+ sub->remove_excludes(subcom);
+ sub->remove_needs(subcom);
+ }
+
+ auto iterator = std::find_if(
+ std::begin(subcommands_), std::end(subcommands_), [subcom](const App_p &v) { return v.get() == subcom; });
+ if(iterator != std::end(subcommands_)) {
+ subcommands_.erase(iterator);
+ return true;
+ }
+ return false;
+ }
+ /// Check to see if a subcommand is part of this command (doesn't have to be in command line)
+ /// returns the first subcommand if passed a nullptr
+ App *get_subcommand(const App *subcom) const {
+ if(subcom == nullptr)
+ throw OptionNotFound("nullptr passed");
+ for(const App_p &subcomptr : subcommands_)
+ if(subcomptr.get() == subcom)
+ return subcomptr.get();
+ throw OptionNotFound(subcom->get_name());
+ }
+
+ /// Check to see if a subcommand is part of this command (text version)
+ App *get_subcommand(std::string subcom) const {
+ auto subc = _find_subcommand(subcom, false, false);
+ if(subc == nullptr)
+ throw OptionNotFound(subcom);
+ return subc;
+ }
+ /// Get a pointer to subcommand by index
+ App *get_subcommand(int index = 0) const {
+ if(index >= 0) {
+ auto uindex = static_cast<unsigned>(index);
+ if(uindex < subcommands_.size())
+ return subcommands_[uindex].get();
+ }
+ throw OptionNotFound(std::to_string(index));
+ }
+
+ /// Check to see if a subcommand is part of this command and get a shared_ptr to it
+ CLI::App_p get_subcommand_ptr(App *subcom) const {
+ if(subcom == nullptr)
+ throw OptionNotFound("nullptr passed");
+ for(const App_p &subcomptr : subcommands_)
+ if(subcomptr.get() == subcom)
+ return subcomptr;
+ throw OptionNotFound(subcom->get_name());
+ }
+
+ /// Check to see if a subcommand is part of this command (text version)
+ CLI::App_p get_subcommand_ptr(std::string subcom) const {
+ for(const App_p &subcomptr : subcommands_)
+ if(subcomptr->check_name(subcom))
+ return subcomptr;
+ throw OptionNotFound(subcom);
+ }
+
+ /// Get an owning pointer to subcommand by index
+ CLI::App_p get_subcommand_ptr(int index = 0) const {
+ if(index >= 0) {
+ auto uindex = static_cast<unsigned>(index);
+ if(uindex < subcommands_.size())
+ return subcommands_[uindex];
+ }
+ throw OptionNotFound(std::to_string(index));
+ }
+
+ /// Check to see if an option group is part of this App
+ App *get_option_group(std::string group_name) const {
+ for(const App_p &app : subcommands_) {
+ if(app->name_.empty() && app->group_ == group_name) {
+ return app.get();
+ }
+ }
+ throw OptionNotFound(group_name);
+ }
+
+ /// No argument version of count counts the number of times this subcommand was
+ /// passed in. The main app will return 1. Unnamed subcommands will also return 1 unless
+ /// otherwise modified in a callback
+ std::size_t count() const { return parsed_; }
+
+ /// Get a count of all the arguments processed in options and subcommands, this excludes arguments which were
+ /// treated as extras.
+ std::size_t count_all() const {
+ std::size_t cnt{0};
+ for(auto &opt : options_) {
+ cnt += opt->count();
+ }
+ for(auto &sub : subcommands_) {
+ cnt += sub->count_all();
+ }
+ if(!get_name().empty()) { // for named subcommands add the number of times the subcommand was called
+ cnt += parsed_;
+ }
+ return cnt;
+ }
+
+ /// Changes the group membership
+ App *group(std::string group_name) {
+ group_ = group_name;
+ return this;
+ }
+
+ /// The argumentless form of require subcommand requires 1 or more subcommands
+ App *require_subcommand() {
+ require_subcommand_min_ = 1;
+ require_subcommand_max_ = 0;
+ return this;
+ }
+
+ /// Require a subcommand to be given (does not affect help call)
+ /// The number required can be given. Negative values indicate maximum
+ /// number allowed (0 for any number). Max number inheritable.
+ App *require_subcommand(int value) {
+ if(value < 0) {
+ require_subcommand_min_ = 0;
+ require_subcommand_max_ = static_cast<std::size_t>(-value);
+ } else {
+ require_subcommand_min_ = static_cast<std::size_t>(value);
+ require_subcommand_max_ = static_cast<std::size_t>(value);
+ }
+ return this;
+ }
+
+ /// Explicitly control the number of subcommands required. Setting 0
+ /// for the max means unlimited number allowed. Max number inheritable.
+ App *require_subcommand(std::size_t min, std::size_t max) {
+ require_subcommand_min_ = min;
+ require_subcommand_max_ = max;
+ return this;
+ }
+
+ /// The argumentless form of require option requires 1 or more options be used
+ App *require_option() {
+ require_option_min_ = 1;
+ require_option_max_ = 0;
+ return this;
+ }
+
+ /// Require an option to be given (does not affect help call)
+ /// The number required can be given. Negative values indicate maximum
+ /// number allowed (0 for any number).
+ App *require_option(int value) {
+ if(value < 0) {
+ require_option_min_ = 0;
+ require_option_max_ = static_cast<std::size_t>(-value);
+ } else {
+ require_option_min_ = static_cast<std::size_t>(value);
+ require_option_max_ = static_cast<std::size_t>(value);
+ }
+ return this;
+ }
+
+ /// Explicitly control the number of options required. Setting 0
+ /// for the max means unlimited number allowed. Max number inheritable.
+ App *require_option(std::size_t min, std::size_t max) {
+ require_option_min_ = min;
+ require_option_max_ = max;
+ return this;
+ }
+
+ /// Stop subcommand fallthrough, so that parent commands cannot collect commands after subcommand.
+ /// Default from parent, usually set on parent.
+ App *fallthrough(bool value = true) {
+ fallthrough_ = value;
+ return this;
+ }
+
+ /// Check to see if this subcommand was parsed, true only if received on command line.
+ /// This allows the subcommand to be directly checked.
+ explicit operator bool() const { return parsed_ > 0; }
+
+ ///@}
+ /// @name Extras for subclassing
+ ///@{
+
+ /// This allows subclasses to inject code before callbacks but after parse.
+ ///
+ /// This does not run if any errors or help is thrown.
+ virtual void pre_callback() {}
+
+ ///@}
+ /// @name Parsing
+ ///@{
+ //
+ /// Reset the parsed data
+ void clear() {
+
+ parsed_ = 0;
+ pre_parse_called_ = false;
+
+ missing_.clear();
+ parsed_subcommands_.clear();
+ for(const Option_p &opt : options_) {
+ opt->clear();
+ }
+ for(const App_p &subc : subcommands_) {
+ subc->clear();
+ }
+ }
+
+ /// Parses the command line - throws errors.
+ /// This must be called after the options are in but before the rest of the program.
+ void parse(int argc, const char *const *argv) {
+ // If the name is not set, read from command line
+ if(name_.empty() || has_automatic_name_) {
+ has_automatic_name_ = true;
+ name_ = argv[0];
+ }
+
+ std::vector<std::string> args;
+ args.reserve(static_cast<std::size_t>(argc) - 1U);
+ for(auto i = static_cast<std::size_t>(argc) - 1U; i > 0U; --i)
+ args.emplace_back(argv[i]);
+ parse(std::move(args));
+ }
+
+ /// Parse a single string as if it contained command line arguments.
+ /// This function splits the string into arguments then calls parse(std::vector<std::string> &)
+ /// the function takes an optional boolean argument specifying if the programName is included in the string to
+ /// process
+ void parse(std::string commandline, bool program_name_included = false) {
+
+ if(program_name_included) {
+ auto nstr = detail::split_program_name(commandline);
+ if((name_.empty()) || (has_automatic_name_)) {
+ has_automatic_name_ = true;
+ name_ = nstr.first;
+ }
+ commandline = std::move(nstr.second);
+ } else {
+ detail::trim(commandline);
+ }
+ // the next section of code is to deal with quoted arguments after an '=' or ':' for windows like operations
+ if(!commandline.empty()) {
+ commandline = detail::find_and_modify(commandline, "=", detail::escape_detect);
+ if(allow_windows_style_options_)
+ commandline = detail::find_and_modify(commandline, ":", detail::escape_detect);
+ }
+
+ auto args = detail::split_up(std::move(commandline));
+ // remove all empty strings
+ args.erase(std::remove(args.begin(), args.end(), std::string{}), args.end());
+ std::reverse(args.begin(), args.end());
+
+ parse(std::move(args));
+ }
+
+ /// The real work is done here. Expects a reversed vector.
+ /// Changes the vector to the remaining options.
+ void parse(std::vector<std::string> &args) {
+ // Clear if parsed
+ if(parsed_ > 0)
+ clear();
+
+ // parsed_ is incremented in commands/subcommands,
+ // but placed here to make sure this is cleared when
+ // running parse after an error is thrown, even by _validate or _configure.
+ parsed_ = 1;
+ _validate();
+ _configure();
+ // set the parent as nullptr as this object should be the top now
+ parent_ = nullptr;
+ parsed_ = 0;
+
+ _parse(args);
+ run_callback();
+ }
+
+ /// The real work is done here. Expects a reversed vector.
+ void parse(std::vector<std::string> &&args) {
+ // Clear if parsed
+ if(parsed_ > 0)
+ clear();
+
+ // parsed_ is incremented in commands/subcommands,
+ // but placed here to make sure this is cleared when
+ // running parse after an error is thrown, even by _validate or _configure.
+ parsed_ = 1;
+ _validate();
+ _configure();
+ // set the parent as nullptr as this object should be the top now
+ parent_ = nullptr;
+ parsed_ = 0;
+
+ _parse(std::move(args));
+ run_callback();
+ }
+
+ void parse_from_stream(std::istream &input) {
+ if(parsed_ == 0) {
+ _validate();
+ _configure();
+ // set the parent as nullptr as this object should be the top now
+ }
+
+ _parse_stream(input);
+ run_callback();
+ }
+ /// Provide a function to print a help message. The function gets access to the App pointer and error.
+ void failure_message(std::function<std::string(const App *, const Error &e)> function) {
+ failure_message_ = function;
+ }
+
+ /// Print a nice error message and return the exit code
+ int exit(const Error &e, std::ostream &out = std::cout, std::ostream &err = std::cerr) const {
+
+ /// Avoid printing anything if this is a CLI::RuntimeError
+ if(e.get_name() == "RuntimeError")
+ return e.get_exit_code();
+
+ if(e.get_name() == "CallForHelp") {
+ out << help();
+ return e.get_exit_code();
+ }
+
+ if(e.get_name() == "CallForAllHelp") {
+ out << help("", AppFormatMode::All);
+ return e.get_exit_code();
+ }
+
+ if(e.get_name() == "CallForVersion") {
+ out << e.what() << std::endl;
+ return e.get_exit_code();
+ }
+
+ if(e.get_exit_code() != static_cast<int>(ExitCodes::Success)) {
+ if(failure_message_)
+ err << failure_message_(this, e) << std::flush;
+ }
+
+ return e.get_exit_code();
+ }
+
+ ///@}
+ /// @name Post parsing
+ ///@{
+
+ /// Counts the number of times the given option was passed.
+ std::size_t count(std::string option_name) const { return get_option(option_name)->count(); }
+
+ /// Get a subcommand pointer list to the currently selected subcommands (after parsing by default, in command
+ /// line order; use parsed = false to get the original definition list.)
+ std::vector<App *> get_subcommands() const { return parsed_subcommands_; }
+
+ /// Get a filtered subcommand pointer list from the original definition list. An empty function will provide all
+ /// subcommands (const)
+ std::vector<const App *> get_subcommands(const std::function<bool(const App *)> &filter) const {
+ std::vector<const App *> subcomms(subcommands_.size());
+ std::transform(std::begin(subcommands_), std::end(subcommands_), std::begin(subcomms), [](const App_p &v) {
+ return v.get();
+ });
+
+ if(filter) {
+ subcomms.erase(std::remove_if(std::begin(subcomms),
+ std::end(subcomms),
+ [&filter](const App *app) { return !filter(app); }),
+ std::end(subcomms));
+ }
+
+ return subcomms;
+ }
+
+ /// Get a filtered subcommand pointer list from the original definition list. An empty function will provide all
+ /// subcommands
+ std::vector<App *> get_subcommands(const std::function<bool(App *)> &filter) {
+ std::vector<App *> subcomms(subcommands_.size());
+ std::transform(std::begin(subcommands_), std::end(subcommands_), std::begin(subcomms), [](const App_p &v) {
+ return v.get();
+ });
+
+ if(filter) {
+ subcomms.erase(
+ std::remove_if(std::begin(subcomms), std::end(subcomms), [&filter](App *app) { return !filter(app); }),
+ std::end(subcomms));
+ }
+
+ return subcomms;
+ }
+
+ /// Check to see if given subcommand was selected
+ bool got_subcommand(const App *subcom) const {
+ // get subcom needed to verify that this was a real subcommand
+ return get_subcommand(subcom)->parsed_ > 0;
+ }
+
+ /// Check with name instead of pointer to see if subcommand was selected
+ bool got_subcommand(std::string subcommand_name) const { return get_subcommand(subcommand_name)->parsed_ > 0; }
+
+ /// Sets excluded options for the subcommand
+ App *excludes(Option *opt) {
+ if(opt == nullptr) {
+ throw OptionNotFound("nullptr passed");
+ }
+ exclude_options_.insert(opt);
+ return this;
+ }
+
+ /// Sets excluded subcommands for the subcommand
+ App *excludes(App *app) {
+ if(app == nullptr) {
+ throw OptionNotFound("nullptr passed");
+ }
+ if(app == this) {
+ throw OptionNotFound("cannot self reference in needs");
+ }
+ auto res = exclude_subcommands_.insert(app);
+ // subcommand exclusion should be symmetric
+ if(res.second) {
+ app->exclude_subcommands_.insert(this);
+ }
+ return this;
+ }
+
+ App *needs(Option *opt) {
+ if(opt == nullptr) {
+ throw OptionNotFound("nullptr passed");
+ }
+ need_options_.insert(opt);
+ return this;
+ }
+
+ App *needs(App *app) {
+ if(app == nullptr) {
+ throw OptionNotFound("nullptr passed");
+ }
+ if(app == this) {
+ throw OptionNotFound("cannot self reference in needs");
+ }
+ need_subcommands_.insert(app);
+ return this;
+ }
+
+ /// Removes an option from the excludes list of this subcommand
+ bool remove_excludes(Option *opt) {
+ auto iterator = std::find(std::begin(exclude_options_), std::end(exclude_options_), opt);
+ if(iterator == std::end(exclude_options_)) {
+ return false;
+ }
+ exclude_options_.erase(iterator);
+ return true;
+ }
+
+ /// Removes a subcommand from the excludes list of this subcommand
+ bool remove_excludes(App *app) {
+ auto iterator = std::find(std::begin(exclude_subcommands_), std::end(exclude_subcommands_), app);
+ if(iterator == std::end(exclude_subcommands_)) {
+ return false;
+ }
+ auto other_app = *iterator;
+ exclude_subcommands_.erase(iterator);
+ other_app->remove_excludes(this);
+ return true;
+ }
+
+ /// Removes an option from the needs list of this subcommand
+ bool remove_needs(Option *opt) {
+ auto iterator = std::find(std::begin(need_options_), std::end(need_options_), opt);
+ if(iterator == std::end(need_options_)) {
+ return false;
+ }
+ need_options_.erase(iterator);
+ return true;
+ }
+
+ /// Removes a subcommand from the needs list of this subcommand
+ bool remove_needs(App *app) {
+ auto iterator = std::find(std::begin(need_subcommands_), std::end(need_subcommands_), app);
+ if(iterator == std::end(need_subcommands_)) {
+ return false;
+ }
+ need_subcommands_.erase(iterator);
+ return true;
+ }
+
+ ///@}
+ /// @name Help
+ ///@{
+
+ /// Set footer.
+ App *footer(std::string footer_string) {
+ footer_ = std::move(footer_string);
+ return this;
+ }
+ /// Set footer.
+ App *footer(std::function<std::string()> footer_function) {
+ footer_callback_ = std::move(footer_function);
+ return this;
+ }
+ /// Produce a string that could be read in as a config of the current values of the App. Set default_also to
+ /// include default arguments. write_descriptions will print a description for the App and for each option.
+ std::string config_to_str(bool default_also = false, bool write_description = false) const {
+ return config_formatter_->to_config(this, default_also, write_description, "");
+ }
+
+ /// Makes a help message, using the currently configured formatter
+ /// Will only do one subcommand at a time
+ std::string help(std::string prev = "", AppFormatMode mode = AppFormatMode::Normal) const {
+ if(prev.empty())
+ prev = get_name();
+ else
+ prev += " " + get_name();
+
+ // Delegate to subcommand if needed
+ auto selected_subcommands = get_subcommands();
+ if(!selected_subcommands.empty()) {
+ return selected_subcommands.at(0)->help(prev, mode);
+ }
+ return formatter_->make_help(this, prev, mode);
+ }
+
+ /// Displays a version string
+ std::string version() const {
+ std::string val;
+ if(version_ptr_ != nullptr) {
+ auto rv = version_ptr_->results();
+ version_ptr_->clear();
+ version_ptr_->add_result("true");
+ try {
+ version_ptr_->run_callback();
+ } catch(const CLI::CallForVersion &cfv) {
+ val = cfv.what();
+ }
+ version_ptr_->clear();
+ version_ptr_->add_result(rv);
+ }
+ return val;
+ }
+ ///@}
+ /// @name Getters
+ ///@{
+
+ /// Access the formatter
+ std::shared_ptr<FormatterBase> get_formatter() const { return formatter_; }
+
+ /// Access the config formatter
+ std::shared_ptr<Config> get_config_formatter() const { return config_formatter_; }
+
+ /// Access the config formatter as a configBase pointer
+ std::shared_ptr<ConfigBase> get_config_formatter_base() const {
+ // This is safer as a dynamic_cast if we have RTTI, as Config -> ConfigBase
+#if CLI11_USE_STATIC_RTTI == 0
+ return std::dynamic_pointer_cast<ConfigBase>(config_formatter_);
+#else
+ return std::static_pointer_cast<ConfigBase>(config_formatter_);
+#endif
+ }
+
+ /// Get the app or subcommand description
+ std::string get_description() const { return description_; }
+
+ /// Set the description of the app
+ App *description(std::string app_description) {
+ description_ = std::move(app_description);
+ return this;
+ }
+
+ /// Get the list of options (user facing function, so returns raw pointers), has optional filter function
+ std::vector<const Option *> get_options(const std::function<bool(const Option *)> filter = {}) const {
+ std::vector<const Option *> options(options_.size());
+ std::transform(std::begin(options_), std::end(options_), std::begin(options), [](const Option_p &val) {
+ return val.get();
+ });
+
+ if(filter) {
+ options.erase(std::remove_if(std::begin(options),
+ std::end(options),
+ [&filter](const Option *opt) { return !filter(opt); }),
+ std::end(options));
+ }
+
+ return options;
+ }
+
+ /// Non-const version of the above
+ std::vector<Option *> get_options(const std::function<bool(Option *)> filter = {}) {
+ std::vector<Option *> options(options_.size());
+ std::transform(std::begin(options_), std::end(options_), std::begin(options), [](const Option_p &val) {
+ return val.get();
+ });
+
+ if(filter) {
+ options.erase(
+ std::remove_if(std::begin(options), std::end(options), [&filter](Option *opt) { return !filter(opt); }),
+ std::end(options));
+ }
+
+ return options;
+ }
+
+ /// Get an option by name (noexcept non-const version)
+ Option *get_option_no_throw(std::string option_name) noexcept {
+ for(Option_p &opt : options_) {
+ if(opt->check_name(option_name)) {
+ return opt.get();
+ }
+ }
+ for(auto &subc : subcommands_) {
+ // also check down into nameless subcommands
+ if(subc->get_name().empty()) {
+ auto opt = subc->get_option_no_throw(option_name);
+ if(opt != nullptr) {
+ return opt;
+ }
+ }
+ }
+ return nullptr;
+ }
+
+ /// Get an option by name (noexcept const version)
+ const Option *get_option_no_throw(std::string option_name) const noexcept {
+ for(const Option_p &opt : options_) {
+ if(opt->check_name(option_name)) {
+ return opt.get();
+ }
+ }
+ for(const auto &subc : subcommands_) {
+ // also check down into nameless subcommands
+ if(subc->get_name().empty()) {
+ auto opt = subc->get_option_no_throw(option_name);
+ if(opt != nullptr) {
+ return opt;
+ }
+ }
+ }
+ return nullptr;
+ }
+
+ /// Get an option by name
+ const Option *get_option(std::string option_name) const {
+ auto opt = get_option_no_throw(option_name);
+ if(opt == nullptr) {
+ throw OptionNotFound(option_name);
+ }
+ return opt;
+ }
+
+ /// Get an option by name (non-const version)
+ Option *get_option(std::string option_name) {
+ auto opt = get_option_no_throw(option_name);
+ if(opt == nullptr) {
+ throw OptionNotFound(option_name);
+ }
+ return opt;
+ }
+
+ /// Shortcut bracket operator for getting a pointer to an option
+ const Option *operator[](const std::string &option_name) const { return get_option(option_name); }
+
+ /// Shortcut bracket operator for getting a pointer to an option
+ const Option *operator[](const char *option_name) const { return get_option(option_name); }
+
+ /// Check the status of ignore_case
+ bool get_ignore_case() const { return ignore_case_; }
+
+ /// Check the status of ignore_underscore
+ bool get_ignore_underscore() const { return ignore_underscore_; }
+
+ /// Check the status of fallthrough
+ bool get_fallthrough() const { return fallthrough_; }
+
+ /// Check the status of the allow windows style options
+ bool get_allow_windows_style_options() const { return allow_windows_style_options_; }
+
+ /// Check the status of the allow windows style options
+ bool get_positionals_at_end() const { return positionals_at_end_; }
+
+ /// Check the status of the allow windows style options
+ bool get_configurable() const { return configurable_; }
+
+ /// Get the group of this subcommand
+ const std::string &get_group() const { return group_; }
+
+ /// Generate and return the footer.
+ std::string get_footer() const { return (footer_callback_) ? footer_callback_() + '\n' + footer_ : footer_; }
+
+ /// Get the required min subcommand value
+ std::size_t get_require_subcommand_min() const { return require_subcommand_min_; }
+
+ /// Get the required max subcommand value
+ std::size_t get_require_subcommand_max() const { return require_subcommand_max_; }
+
+ /// Get the required min option value
+ std::size_t get_require_option_min() const { return require_option_min_; }
+
+ /// Get the required max option value
+ std::size_t get_require_option_max() const { return require_option_max_; }
+
+ /// Get the prefix command status
+ bool get_prefix_command() const { return prefix_command_; }
+
+ /// Get the status of allow extras
+ bool get_allow_extras() const { return allow_extras_; }
+
+ /// Get the status of required
+ bool get_required() const { return required_; }
+
+ /// Get the status of disabled
+ bool get_disabled() const { return disabled_; }
+
+ /// Get the status of silence
+ bool get_silent() const { return silent_; }
+
+ /// Get the status of disabled
+ bool get_immediate_callback() const { return immediate_callback_; }
+
+ /// Get the status of disabled by default
+ bool get_disabled_by_default() const { return (default_startup == startup_mode::disabled); }
+
+ /// Get the status of disabled by default
+ bool get_enabled_by_default() const { return (default_startup == startup_mode::enabled); }
+ /// Get the status of validating positionals
+ bool get_validate_positionals() const { return validate_positionals_; }
+ /// Get the status of validating optional vector arguments
+ bool get_validate_optional_arguments() const { return validate_optional_arguments_; }
+
+ /// Get the status of allow extras
+ config_extras_mode get_allow_config_extras() const { return allow_config_extras_; }
+
+ /// Get a pointer to the help flag.
+ Option *get_help_ptr() { return help_ptr_; }
+
+ /// Get a pointer to the help flag. (const)
+ const Option *get_help_ptr() const { return help_ptr_; }
+
+ /// Get a pointer to the help all flag. (const)
+ const Option *get_help_all_ptr() const { return help_all_ptr_; }
+
+ /// Get a pointer to the config option.
+ Option *get_config_ptr() { return config_ptr_; }
+
+ /// Get a pointer to the config option. (const)
+ const Option *get_config_ptr() const { return config_ptr_; }
+
+ /// Get a pointer to the version option.
+ Option *get_version_ptr() { return version_ptr_; }
+
+ /// Get a pointer to the version option. (const)
+ const Option *get_version_ptr() const { return version_ptr_; }
+
+ /// Get the parent of this subcommand (or nullptr if main app)
+ App *get_parent() { return parent_; }
+
+ /// Get the parent of this subcommand (or nullptr if main app) (const version)
+ const App *get_parent() const { return parent_; }
+
+ /// Get the name of the current app
+ const std::string &get_name() const { return name_; }
+
+ /// Get the aliases of the current app
+ const std::vector<std::string> &get_aliases() const { return aliases_; }
+
+ /// clear all the aliases of the current App
+ App *clear_aliases() {
+ aliases_.clear();
+ return this;
+ }
+
+ /// Get a display name for an app
+ std::string get_display_name(bool with_aliases = false) const {
+ if(name_.empty()) {
+ return std::string("[Option Group: ") + get_group() + "]";
+ }
+ if(aliases_.empty() || !with_aliases) {
+ return name_;
+ }
+ std::string dispname = name_;
+ for(const auto &lalias : aliases_) {
+ dispname.push_back(',');
+ dispname.push_back(' ');
+ dispname.append(lalias);
+ }
+ return dispname;
+ }
+
+ /// Check the name, case insensitive and underscore insensitive if set
+ bool check_name(std::string name_to_check) const {
+ std::string local_name = name_;
+ if(ignore_underscore_) {
+ local_name = detail::remove_underscore(name_);
+ name_to_check = detail::remove_underscore(name_to_check);
+ }
+ if(ignore_case_) {
+ local_name = detail::to_lower(name_);
+ name_to_check = detail::to_lower(name_to_check);
+ }
+
+ if(local_name == name_to_check) {
+ return true;
+ }
+ for(auto les : aliases_) {
+ if(ignore_underscore_) {
+ les = detail::remove_underscore(les);
+ }
+ if(ignore_case_) {
+ les = detail::to_lower(les);
+ }
+ if(les == name_to_check) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /// Get the groups available directly from this option (in order)
+ std::vector<std::string> get_groups() const {
+ std::vector<std::string> groups;
+
+ for(const Option_p &opt : options_) {
+ // Add group if it is not already in there
+ if(std::find(groups.begin(), groups.end(), opt->get_group()) == groups.end()) {
+ groups.push_back(opt->get_group());
+ }
+ }
+
+ return groups;
+ }
+
+ /// This gets a vector of pointers with the original parse order
+ const std::vector<Option *> &parse_order() const { return parse_order_; }
+
+ /// This returns the missing options from the current subcommand
+ std::vector<std::string> remaining(bool recurse = false) const {
+ std::vector<std::string> miss_list;
+ for(const std::pair<detail::Classifier, std::string> &miss : missing_) {
+ miss_list.push_back(std::get<1>(miss));
+ }
+ // Get from a subcommand that may allow extras
+ if(recurse) {
+ if(!allow_extras_) {
+ for(const auto &sub : subcommands_) {
+ if(sub->name_.empty() && !sub->missing_.empty()) {
+ for(const std::pair<detail::Classifier, std::string> &miss : sub->missing_) {
+ miss_list.push_back(std::get<1>(miss));
+ }
+ }
+ }
+ }
+ // Recurse into subcommands
+
+ for(const App *sub : parsed_subcommands_) {
+ std::vector<std::string> output = sub->remaining(recurse);
+ std::copy(std::begin(output), std::end(output), std::back_inserter(miss_list));
+ }
+ }
+ return miss_list;
+ }
+
+ /// This returns the missing options in a form ready for processing by another command line program
+ std::vector<std::string> remaining_for_passthrough(bool recurse = false) const {
+ std::vector<std::string> miss_list = remaining(recurse);
+ std::reverse(std::begin(miss_list), std::end(miss_list));
+ return miss_list;
+ }
+
+ /// This returns the number of remaining options, minus the -- separator
+ std::size_t remaining_size(bool recurse = false) const {
+ auto remaining_options = static_cast<std::size_t>(std::count_if(
+ std::begin(missing_), std::end(missing_), [](const std::pair<detail::Classifier, std::string> &val) {
+ return val.first != detail::Classifier::POSITIONAL_MARK;
+ }));
+
+ if(recurse) {
+ for(const App_p &sub : subcommands_) {
+ remaining_options += sub->remaining_size(recurse);
+ }
+ }
+ return remaining_options;
+ }
+
+ ///@}
+
+ protected:
+ /// Check the options to make sure there are no conflicts.
+ ///
+ /// Currently checks to see if multiple positionals exist with unlimited args and checks if the min and max options
+ /// are feasible
+ void _validate() const {
+ // count the number of positional only args
+ auto pcount = std::count_if(std::begin(options_), std::end(options_), [](const Option_p &opt) {
+ return opt->get_items_expected_max() >= detail::expected_max_vector_size && !opt->nonpositional();
+ });
+ if(pcount > 1) {
+ auto pcount_req = std::count_if(std::begin(options_), std::end(options_), [](const Option_p &opt) {
+ return opt->get_items_expected_max() >= detail::expected_max_vector_size && !opt->nonpositional() &&
+ opt->get_required();
+ });
+ if(pcount - pcount_req > 1) {
+ throw InvalidError(name_);
+ }
+ }
+
+ std::size_t nameless_subs{0};
+ for(const App_p &app : subcommands_) {
+ app->_validate();
+ if(app->get_name().empty())
+ ++nameless_subs;
+ }
+
+ if(require_option_min_ > 0) {
+ if(require_option_max_ > 0) {
+ if(require_option_max_ < require_option_min_) {
+ throw(InvalidError("Required min options greater than required max options",
+ ExitCodes::InvalidError));
+ }
+ }
+ if(require_option_min_ > (options_.size() + nameless_subs)) {
+ throw(InvalidError("Required min options greater than number of available options",
+ ExitCodes::InvalidError));
+ }
+ }
+ }
+
+ /// configure subcommands to enable parsing through the current object
+ /// set the correct fallthrough and prefix for nameless subcommands and manage the automatic enable or disable
+ /// makes sure parent is set correctly
+ void _configure() {
+ if(default_startup == startup_mode::enabled) {
+ disabled_ = false;
+ } else if(default_startup == startup_mode::disabled) {
+ disabled_ = true;
+ }
+ for(const App_p &app : subcommands_) {
+ if(app->has_automatic_name_) {
+ app->name_.clear();
+ }
+ if(app->name_.empty()) {
+ app->fallthrough_ = false; // make sure fallthrough_ is false to prevent infinite loop
+ app->prefix_command_ = false;
+ }
+ // make sure the parent is set to be this object in preparation for parse
+ app->parent_ = this;
+ app->_configure();
+ }
+ }
+
+ /// Internal function to run (App) callback, bottom up
+ void run_callback(bool final_mode = false, bool suppress_final_callback = false) {
+ pre_callback();
+ // in the main app if immediate_callback_ is set it runs the main callback before the used subcommands
+ if(!final_mode && parse_complete_callback_) {
+ parse_complete_callback_();
+ }
+ // run the callbacks for the received subcommands
+ for(App *subc : get_subcommands()) {
+ if(subc->parent_ == this) {
+ subc->run_callback(true, suppress_final_callback);
+ }
+ }
+ // now run callbacks for option_groups
+ for(auto &subc : subcommands_) {
+ if(subc->name_.empty() && subc->count_all() > 0) {
+ subc->run_callback(true, suppress_final_callback);
+ }
+ }
+
+ // finally run the main callback
+ if(final_callback_ && (parsed_ > 0) && (!suppress_final_callback)) {
+ if(!name_.empty() || count_all() > 0 || parent_ == nullptr) {
+ final_callback_();
+ }
+ }
+ }
+
+ /// Check to see if a subcommand is valid. Give up immediately if subcommand max has been reached.
+ bool _valid_subcommand(const std::string &current, bool ignore_used = true) const {
+ // Don't match if max has been reached - but still check parents
+ if(require_subcommand_max_ != 0 && parsed_subcommands_.size() >= require_subcommand_max_) {
+ return parent_ != nullptr && parent_->_valid_subcommand(current, ignore_used);
+ }
+ auto com = _find_subcommand(current, true, ignore_used);
+ if(com != nullptr) {
+ return true;
+ }
+ // Check parent if exists, else return false
+ return parent_ != nullptr && parent_->_valid_subcommand(current, ignore_used);
+ }
+
+ /// Selects a Classifier enum based on the type of the current argument
+ detail::Classifier _recognize(const std::string &current, bool ignore_used_subcommands = true) const {
+ std::string dummy1, dummy2;
+
+ if(current == "--")
+ return detail::Classifier::POSITIONAL_MARK;
+ if(_valid_subcommand(current, ignore_used_subcommands))
+ return detail::Classifier::SUBCOMMAND;
+ if(detail::split_long(current, dummy1, dummy2))
+ return detail::Classifier::LONG;
+ if(detail::split_short(current, dummy1, dummy2)) {
+ if(dummy1[0] >= '0' && dummy1[0] <= '9') {
+ if(get_option_no_throw(std::string{'-', dummy1[0]}) == nullptr) {
+ return detail::Classifier::NONE;
+ }
+ }
+ return detail::Classifier::SHORT;
+ }
+ if((allow_windows_style_options_) && (detail::split_windows_style(current, dummy1, dummy2)))
+ return detail::Classifier::WINDOWS_STYLE;
+ if((current == "++") && !name_.empty() && parent_ != nullptr)
+ return detail::Classifier::SUBCOMMAND_TERMINATOR;
+ return detail::Classifier::NONE;
+ }
+
+ // The parse function is now broken into several parts, and part of process
+
+ /// Read and process a configuration file (main app only)
+ void _process_config_file() {
+ if(config_ptr_ != nullptr) {
+ bool config_required = config_ptr_->get_required();
+ auto file_given = config_ptr_->count() > 0;
+ auto config_files = config_ptr_->as<std::vector<std::string>>();
+ if(config_files.empty() || config_files.front().empty()) {
+ if(config_required) {
+ throw FileError::Missing("no specified config file");
+ }
+ return;
+ }
+ for(auto rit = config_files.rbegin(); rit != config_files.rend(); ++rit) {
+ const auto &config_file = *rit;
+ auto path_result = detail::check_path(config_file.c_str());
+ if(path_result == detail::path_type::file) {
+ try {
+ std::vector<ConfigItem> values = config_formatter_->from_file(config_file);
+ _parse_config(values);
+ if(!file_given) {
+ config_ptr_->add_result(config_file);
+ }
+ } catch(const FileError &) {
+ if(config_required || file_given)
+ throw;
+ }
+ } else if(config_required || file_given) {
+ throw FileError::Missing(config_file);
+ }
+ }
+ }
+ }
+
+ /// Get envname options if not yet passed. Runs on *all* subcommands.
+ void _process_env() {
+ for(const Option_p &opt : options_) {
+ if(opt->count() == 0 && !opt->envname_.empty()) {
+ char *buffer = nullptr;
+ std::string ename_string;
+
+#ifdef _MSC_VER
+ // Windows version
+ std::size_t sz = 0;
+ if(_dupenv_s(&buffer, &sz, opt->envname_.c_str()) == 0 && buffer != nullptr) {
+ ename_string = std::string(buffer);
+ free(buffer);
+ }
+#else
+ // This also works on Windows, but gives a warning
+ buffer = std::getenv(opt->envname_.c_str());
+ if(buffer != nullptr)
+ ename_string = std::string(buffer);
+#endif
+
+ if(!ename_string.empty()) {
+ opt->add_result(ename_string);
+ }
+ }
+ }
+
+ for(App_p &sub : subcommands_) {
+ if(sub->get_name().empty() || !sub->parse_complete_callback_)
+ sub->_process_env();
+ }
+ }
+
+ /// Process callbacks. Runs on *all* subcommands.
+ void _process_callbacks() {
+
+ for(App_p &sub : subcommands_) {
+ // process the priority option_groups first
+ if(sub->get_name().empty() && sub->parse_complete_callback_) {
+ if(sub->count_all() > 0) {
+ sub->_process_callbacks();
+ sub->run_callback();
+ }
+ }
+ }
+
+ for(const Option_p &opt : options_) {
+ if((*opt) && !opt->get_callback_run()) {
+ opt->run_callback();
+ }
+ }
+ for(App_p &sub : subcommands_) {
+ if(!sub->parse_complete_callback_) {
+ sub->_process_callbacks();
+ }
+ }
+ }
+
+ /// Run help flag processing if any are found.
+ ///
+ /// The flags allow recursive calls to remember if there was a help flag on a parent.
+ void _process_help_flags(bool trigger_help = false, bool trigger_all_help = false) const {
+ const Option *help_ptr = get_help_ptr();
+ const Option *help_all_ptr = get_help_all_ptr();
+
+ if(help_ptr != nullptr && help_ptr->count() > 0)
+ trigger_help = true;
+ if(help_all_ptr != nullptr && help_all_ptr->count() > 0)
+ trigger_all_help = true;
+
+ // If there were parsed subcommands, call those. First subcommand wins if there are multiple ones.
+ if(!parsed_subcommands_.empty()) {
+ for(const App *sub : parsed_subcommands_)
+ sub->_process_help_flags(trigger_help, trigger_all_help);
+
+ // Only the final subcommand should call for help. All help wins over help.
+ } else if(trigger_all_help) {
+ throw CallForAllHelp();
+ } else if(trigger_help) {
+ throw CallForHelp();
+ }
+ }
+
+ /// Verify required options and cross requirements. Subcommands too (only if selected).
+ void _process_requirements() {
+ // check excludes
+ bool excluded{false};
+ std::string excluder;
+ for(auto &opt : exclude_options_) {
+ if(opt->count() > 0) {
+ excluded = true;
+ excluder = opt->get_name();
+ }
+ }
+ for(auto &subc : exclude_subcommands_) {
+ if(subc->count_all() > 0) {
+ excluded = true;
+ excluder = subc->get_display_name();
+ }
+ }
+ if(excluded) {
+ if(count_all() > 0) {
+ throw ExcludesError(get_display_name(), excluder);
+ }
+ // if we are excluded but didn't receive anything, just return
+ return;
+ }
+
+ // check excludes
+ bool missing_needed{false};
+ std::string missing_need;
+ for(auto &opt : need_options_) {
+ if(opt->count() == 0) {
+ missing_needed = true;
+ missing_need = opt->get_name();
+ }
+ }
+ for(auto &subc : need_subcommands_) {
+ if(subc->count_all() == 0) {
+ missing_needed = true;
+ missing_need = subc->get_display_name();
+ }
+ }
+ if(missing_needed) {
+ if(count_all() > 0) {
+ throw RequiresError(get_display_name(), missing_need);
+ }
+ // if we missing something but didn't have any options, just return
+ return;
+ }
+
+ std::size_t used_options = 0;
+ for(const Option_p &opt : options_) {
+
+ if(opt->count() != 0) {
+ ++used_options;
+ }
+ // Required but empty
+ if(opt->get_required() && opt->count() == 0) {
+ throw RequiredError(opt->get_name());
+ }
+ // Requires
+ for(const Option *opt_req : opt->needs_)
+ if(opt->count() > 0 && opt_req->count() == 0)
+ throw RequiresError(opt->get_name(), opt_req->get_name());
+ // Excludes
+ for(const Option *opt_ex : opt->excludes_)
+ if(opt->count() > 0 && opt_ex->count() != 0)
+ throw ExcludesError(opt->get_name(), opt_ex->get_name());
+ }
+ // check for the required number of subcommands
+ if(require_subcommand_min_ > 0) {
+ auto selected_subcommands = get_subcommands();
+ if(require_subcommand_min_ > selected_subcommands.size())
+ throw RequiredError::Subcommand(require_subcommand_min_);
+ }
+
+ // Max error cannot occur, the extra subcommand will parse as an ExtrasError or a remaining item.
+
+ // run this loop to check how many unnamed subcommands were actually used since they are considered options
+ // from the perspective of an App
+ for(App_p &sub : subcommands_) {
+ if(sub->disabled_)
+ continue;
+ if(sub->name_.empty() && sub->count_all() > 0) {
+ ++used_options;
+ }
+ }
+
+ if(require_option_min_ > used_options || (require_option_max_ > 0 && require_option_max_ < used_options)) {
+ auto option_list = detail::join(options_, [this](const Option_p &ptr) {
+ if(ptr.get() == help_ptr_ || ptr.get() == help_all_ptr_) {
+ return std::string{};
+ }
+ return ptr->get_name(false, true);
+ });
+
+ auto subc_list = get_subcommands([](App *app) { return ((app->get_name().empty()) && (!app->disabled_)); });
+ if(!subc_list.empty()) {
+ option_list += "," + detail::join(subc_list, [](const App *app) { return app->get_display_name(); });
+ }
+ throw RequiredError::Option(require_option_min_, require_option_max_, used_options, option_list);
+ }
+
+ // now process the requirements for subcommands if needed
+ for(App_p &sub : subcommands_) {
+ if(sub->disabled_)
+ continue;
+ if(sub->name_.empty() && sub->required_ == false) {
+ if(sub->count_all() == 0) {
+ if(require_option_min_ > 0 && require_option_min_ <= used_options) {
+ continue;
+ // if we have met the requirement and there is nothing in this option group skip checking
+ // requirements
+ }
+ if(require_option_max_ > 0 && used_options >= require_option_min_) {
+ continue;
+ // if we have met the requirement and there is nothing in this option group skip checking
+ // requirements
+ }
+ }
+ }
+ if(sub->count() > 0 || sub->name_.empty()) {
+ sub->_process_requirements();
+ }
+
+ if(sub->required_ && sub->count_all() == 0) {
+ throw(CLI::RequiredError(sub->get_display_name()));
+ }
+ }
+ }
+
+ /// Process callbacks and such.
+ void _process() {
+ try {
+ // the config file might generate a FileError but that should not be processed until later in the process
+ // to allow for help, version and other errors to generate first.
+ _process_config_file();
+
+ // process env shouldn't throw but no reason to process it if config generated an error
+ _process_env();
+ } catch(const CLI::FileError &) {
+ // callbacks and help_flags can generate exceptions which should take priority
+ // over the config file error if one exists.
+ _process_callbacks();
+ _process_help_flags();
+ throw;
+ }
+
+ _process_callbacks();
+ _process_help_flags();
+
+ _process_requirements();
+ }
+
+ /// Throw an error if anything is left over and should not be.
+ void _process_extras() {
+ if(!(allow_extras_ || prefix_command_)) {
+ std::size_t num_left_over = remaining_size();
+ if(num_left_over > 0) {
+ throw ExtrasError(name_, remaining(false));
+ }
+ }
+
+ for(App_p &sub : subcommands_) {
+ if(sub->count() > 0)
+ sub->_process_extras();
+ }
+ }
+
+ /// Throw an error if anything is left over and should not be.
+ /// Modifies the args to fill in the missing items before throwing.
+ void _process_extras(std::vector<std::string> &args) {
+ if(!(allow_extras_ || prefix_command_)) {
+ std::size_t num_left_over = remaining_size();
+ if(num_left_over > 0) {
+ args = remaining(false);
+ throw ExtrasError(name_, args);
+ }
+ }
+
+ for(App_p &sub : subcommands_) {
+ if(sub->count() > 0)
+ sub->_process_extras(args);
+ }
+ }
+
+ /// Internal function to recursively increment the parsed counter on the current app as well unnamed subcommands
+ void increment_parsed() {
+ ++parsed_;
+ for(App_p &sub : subcommands_) {
+ if(sub->get_name().empty())
+ sub->increment_parsed();
+ }
+ }
+ /// Internal parse function
+ void _parse(std::vector<std::string> &args) {
+ increment_parsed();
+ _trigger_pre_parse(args.size());
+ bool positional_only = false;
+
+ while(!args.empty()) {
+ if(!_parse_single(args, positional_only)) {
+ break;
+ }
+ }
+
+ if(parent_ == nullptr) {
+ _process();
+
+ // Throw error if any items are left over (depending on settings)
+ _process_extras(args);
+
+ // Convert missing (pairs) to extras (string only) ready for processing in another app
+ args = remaining_for_passthrough(false);
+ } else if(parse_complete_callback_) {
+ _process_env();
+ _process_callbacks();
+ _process_help_flags();
+ _process_requirements();
+ run_callback(false, true);
+ }
+ }
+
+ /// Internal parse function
+ void _parse(std::vector<std::string> &&args) {
+ // this can only be called by the top level in which case parent == nullptr by definition
+ // operation is simplified
+ increment_parsed();
+ _trigger_pre_parse(args.size());
+ bool positional_only = false;
+
+ while(!args.empty()) {
+ _parse_single(args, positional_only);
+ }
+ _process();
+
+ // Throw error if any items are left over (depending on settings)
+ _process_extras();
+ }
+
+ /// Internal function to parse a stream
+ void _parse_stream(std::istream &input) {
+ auto values = config_formatter_->from_config(input);
+ _parse_config(values);
+ increment_parsed();
+ _trigger_pre_parse(values.size());
+ _process();
+
+ // Throw error if any items are left over (depending on settings)
+ _process_extras();
+ }
+
+ /// Parse one config param, return false if not found in any subcommand, remove if it is
+ ///
+ /// If this has more than one dot.separated.name, go into the subcommand matching it
+ /// Returns true if it managed to find the option, if false you'll need to remove the arg manually.
+ void _parse_config(const std::vector<ConfigItem> &args) {
+ for(const ConfigItem &item : args) {
+ if(!_parse_single_config(item) && allow_config_extras_ == config_extras_mode::error)
+ throw ConfigError::Extras(item.fullname());
+ }
+ }
+
+ /// Fill in a single config option
+ bool _parse_single_config(const ConfigItem &item, std::size_t level = 0) {
+ if(level < item.parents.size()) {
+ try {
+ auto subcom = get_subcommand(item.parents.at(level));
+ auto result = subcom->_parse_single_config(item, level + 1);
+
+ return result;
+ } catch(const OptionNotFound &) {
+ return false;
+ }
+ }
+ // check for section open
+ if(item.name == "++") {
+ if(configurable_) {
+ increment_parsed();
+ _trigger_pre_parse(2);
+ if(parent_ != nullptr) {
+ parent_->parsed_subcommands_.push_back(this);
+ }
+ }
+ return true;
+ }
+ // check for section close
+ if(item.name == "--") {
+ if(configurable_) {
+ _process_callbacks();
+ _process_requirements();
+ run_callback();
+ }
+ return true;
+ }
+ Option *op = get_option_no_throw("--" + item.name);
+ if(op == nullptr) {
+ if(item.name.size() == 1) {
+ op = get_option_no_throw("-" + item.name);
+ }
+ }
+ if(op == nullptr) {
+ op = get_option_no_throw(item.name);
+ }
+ if(op == nullptr) {
+ // If the option was not present
+ if(get_allow_config_extras() == config_extras_mode::capture)
+ // Should we worry about classifying the extras properly?
+ missing_.emplace_back(detail::Classifier::NONE, item.fullname());
+ return false;
+ }
+
+ if(!op->get_configurable()) {
+ if(get_allow_config_extras() == config_extras_mode::ignore_all) {
+ return false;
+ }
+ throw ConfigError::NotConfigurable(item.fullname());
+ }
+
+ if(op->empty()) {
+
+ if(op->get_expected_min() == 0) {
+ // Flag parsing
+ auto res = config_formatter_->to_flag(item);
+ res = op->get_flag_value(item.name, res);
+
+ op->add_result(res);
+
+ } else {
+ op->add_result(item.inputs);
+ op->run_callback();
+ }
+ }
+
+ return true;
+ }
+
+ /// Parse "one" argument (some may eat more than one), delegate to parent if fails, add to missing if missing
+ /// from main return false if the parse has failed and needs to return to parent
+ bool _parse_single(std::vector<std::string> &args, bool &positional_only) {
+ bool retval = true;
+ detail::Classifier classifier = positional_only ? detail::Classifier::NONE : _recognize(args.back());
+ switch(classifier) {
+ case detail::Classifier::POSITIONAL_MARK:
+ args.pop_back();
+ positional_only = true;
+ if((!_has_remaining_positionals()) && (parent_ != nullptr)) {
+ retval = false;
+ } else {
+ _move_to_missing(classifier, "--");
+ }
+ break;
+ case detail::Classifier::SUBCOMMAND_TERMINATOR:
+ // treat this like a positional mark if in the parent app
+ args.pop_back();
+ retval = false;
+ break;
+ case detail::Classifier::SUBCOMMAND:
+ retval = _parse_subcommand(args);
+ break;
+ case detail::Classifier::LONG:
+ case detail::Classifier::SHORT:
+ case detail::Classifier::WINDOWS_STYLE:
+ // If already parsed a subcommand, don't accept options_
+ _parse_arg(args, classifier);
+ break;
+ case detail::Classifier::NONE:
+ // Probably a positional or something for a parent (sub)command
+ retval = _parse_positional(args, false);
+ if(retval && positionals_at_end_) {
+ positional_only = true;
+ }
+ break;
+ // LCOV_EXCL_START
+ default:
+ throw HorribleError("unrecognized classifier (you should not see this!)");
+ // LCOV_EXCL_STOP
+ }
+ return retval;
+ }
+
+ /// Count the required remaining positional arguments
+ std::size_t _count_remaining_positionals(bool required_only = false) const {
+ std::size_t retval = 0;
+ for(const Option_p &opt : options_) {
+ if(opt->get_positional() && (!required_only || opt->get_required())) {
+ if(opt->get_items_expected_min() > 0 &&
+ static_cast<int>(opt->count()) < opt->get_items_expected_min()) {
+ retval += static_cast<std::size_t>(opt->get_items_expected_min()) - opt->count();
+ }
+ }
+ }
+ return retval;
+ }
+
+ /// Count the required remaining positional arguments
+ bool _has_remaining_positionals() const {
+ for(const Option_p &opt : options_) {
+ if(opt->get_positional() && ((static_cast<int>(opt->count()) < opt->get_items_expected_min()))) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /// Parse a positional, go up the tree to check
+ /// @param haltOnSubcommand if set to true the operation will not process subcommands merely return false
+ /// Return true if the positional was used false otherwise
+ bool _parse_positional(std::vector<std::string> &args, bool haltOnSubcommand) {
+
+ const std::string &positional = args.back();
+
+ if(positionals_at_end_) {
+ // deal with the case of required arguments at the end which should take precedence over other arguments
+ auto arg_rem = args.size();
+ auto remreq = _count_remaining_positionals(true);
+ if(arg_rem <= remreq) {
+ for(const Option_p &opt : options_) {
+ if(opt->get_positional() && opt->required_) {
+ if(static_cast<int>(opt->count()) < opt->get_items_expected_min()) {
+ if(validate_positionals_) {
+ std::string pos = positional;
+ pos = opt->_validate(pos, 0);
+ if(!pos.empty()) {
+ continue;
+ }
+ }
+
+ parse_order_.push_back(opt.get());
+ /// if we require a separator add it here
+ if(opt->get_inject_separator()) {
+ if(!opt->results().empty() && !opt->results().back().empty()) {
+ opt->add_result(std::string{});
+ }
+ }
+ if(opt->get_trigger_on_parse() &&
+ opt->current_option_state_ == Option::option_state::callback_run) {
+ opt->clear();
+ }
+ opt->add_result(positional);
+ if(opt->get_trigger_on_parse()) {
+ opt->run_callback();
+ }
+ args.pop_back();
+ return true;
+ }
+ }
+ }
+ }
+ }
+ for(const Option_p &opt : options_) {
+ // Eat options, one by one, until done
+ if(opt->get_positional() &&
+ (static_cast<int>(opt->count()) < opt->get_items_expected_min() || opt->get_allow_extra_args())) {
+ if(validate_positionals_) {
+ std::string pos = positional;
+ pos = opt->_validate(pos, 0);
+ if(!pos.empty()) {
+ continue;
+ }
+ }
+ if(opt->get_inject_separator()) {
+ if(!opt->results().empty() && !opt->results().back().empty()) {
+ opt->add_result(std::string{});
+ }
+ }
+ if(opt->get_trigger_on_parse() && opt->current_option_state_ == Option::option_state::callback_run) {
+ opt->clear();
+ }
+ opt->add_result(positional);
+ if(opt->get_trigger_on_parse()) {
+ opt->run_callback();
+ }
+ parse_order_.push_back(opt.get());
+ args.pop_back();
+ return true;
+ }
+ }
+
+ for(auto &subc : subcommands_) {
+ if((subc->name_.empty()) && (!subc->disabled_)) {
+ if(subc->_parse_positional(args, false)) {
+ if(!subc->pre_parse_called_) {
+ subc->_trigger_pre_parse(args.size());
+ }
+ return true;
+ }
+ }
+ }
+ // let the parent deal with it if possible
+ if(parent_ != nullptr && fallthrough_)
+ return _get_fallthrough_parent()->_parse_positional(args, static_cast<bool>(parse_complete_callback_));
+
+ /// Try to find a local subcommand that is repeated
+ auto com = _find_subcommand(args.back(), true, false);
+ if(com != nullptr && (require_subcommand_max_ == 0 || require_subcommand_max_ > parsed_subcommands_.size())) {
+ if(haltOnSubcommand) {
+ return false;
+ }
+ args.pop_back();
+ com->_parse(args);
+ return true;
+ }
+ /// now try one last gasp at subcommands that have been executed before, go to root app and try to find a
+ /// subcommand in a broader way, if one exists let the parent deal with it
+ auto parent_app = (parent_ != nullptr) ? _get_fallthrough_parent() : this;
+ com = parent_app->_find_subcommand(args.back(), true, false);
+ if(com != nullptr && (com->parent_->require_subcommand_max_ == 0 ||
+ com->parent_->require_subcommand_max_ > com->parent_->parsed_subcommands_.size())) {
+ return false;
+ }
+
+ if(positionals_at_end_) {
+ throw CLI::ExtrasError(name_, args);
+ }
+ /// If this is an option group don't deal with it
+ if(parent_ != nullptr && name_.empty()) {
+ return false;
+ }
+ /// We are out of other options this goes to missing
+ _move_to_missing(detail::Classifier::NONE, positional);
+ args.pop_back();
+ if(prefix_command_) {
+ while(!args.empty()) {
+ _move_to_missing(detail::Classifier::NONE, args.back());
+ args.pop_back();
+ }
+ }
+
+ return true;
+ }
+
+ /// Locate a subcommand by name with two conditions, should disabled subcommands be ignored, and should used
+ /// subcommands be ignored
+ App *_find_subcommand(const std::string &subc_name, bool ignore_disabled, bool ignore_used) const noexcept {
+ for(const App_p &com : subcommands_) {
+ if(com->disabled_ && ignore_disabled)
+ continue;
+ if(com->get_name().empty()) {
+ auto subc = com->_find_subcommand(subc_name, ignore_disabled, ignore_used);
+ if(subc != nullptr) {
+ return subc;
+ }
+ }
+ if(com->check_name(subc_name)) {
+ if((!*com) || !ignore_used)
+ return com.get();
+ }
+ }
+ return nullptr;
+ }
+
+ /// Parse a subcommand, modify args and continue
+ ///
+ /// Unlike the others, this one will always allow fallthrough
+ /// return true if the subcommand was processed false otherwise
+ bool _parse_subcommand(std::vector<std::string> &args) {
+ if(_count_remaining_positionals(/* required */ true) > 0) {
+ _parse_positional(args, false);
+ return true;
+ }
+ auto com = _find_subcommand(args.back(), true, true);
+ if(com != nullptr) {
+ args.pop_back();
+ if(!com->silent_) {
+ parsed_subcommands_.push_back(com);
+ }
+ com->_parse(args);
+ auto parent_app = com->parent_;
+ while(parent_app != this) {
+ parent_app->_trigger_pre_parse(args.size());
+ if(!com->silent_) {
+ parent_app->parsed_subcommands_.push_back(com);
+ }
+ parent_app = parent_app->parent_;
+ }
+ return true;
+ }
+
+ if(parent_ == nullptr)
+ throw HorribleError("Subcommand " + args.back() + " missing");
+ return false;
+ }
+
+ /// Parse a short (false) or long (true) argument, must be at the top of the list
+ /// return true if the argument was processed or false if nothing was done
+ bool _parse_arg(std::vector<std::string> &args, detail::Classifier current_type) {
+
+ std::string current = args.back();
+
+ std::string arg_name;
+ std::string value;
+ std::string rest;
+
+ switch(current_type) {
+ case detail::Classifier::LONG:
+ if(!detail::split_long(current, arg_name, value))
+ throw HorribleError("Long parsed but missing (you should not see this):" + args.back());
+ break;
+ case detail::Classifier::SHORT:
+ if(!detail::split_short(current, arg_name, rest))
+ throw HorribleError("Short parsed but missing! You should not see this");
+ break;
+ case detail::Classifier::WINDOWS_STYLE:
+ if(!detail::split_windows_style(current, arg_name, value))
+ throw HorribleError("windows option parsed but missing! You should not see this");
+ break;
+ case detail::Classifier::SUBCOMMAND:
+ case detail::Classifier::SUBCOMMAND_TERMINATOR:
+ case detail::Classifier::POSITIONAL_MARK:
+ case detail::Classifier::NONE:
+ default:
+ throw HorribleError("parsing got called with invalid option! You should not see this");
+ }
+
+ auto op_ptr =
+ std::find_if(std::begin(options_), std::end(options_), [arg_name, current_type](const Option_p &opt) {
+ if(current_type == detail::Classifier::LONG)
+ return opt->check_lname(arg_name);
+ if(current_type == detail::Classifier::SHORT)
+ return opt->check_sname(arg_name);
+ // this will only get called for detail::Classifier::WINDOWS_STYLE
+ return opt->check_lname(arg_name) || opt->check_sname(arg_name);
+ });
+
+ // Option not found
+ if(op_ptr == std::end(options_)) {
+ for(auto &subc : subcommands_) {
+ if(subc->name_.empty() && !subc->disabled_) {
+ if(subc->_parse_arg(args, current_type)) {
+ if(!subc->pre_parse_called_) {
+ subc->_trigger_pre_parse(args.size());
+ }
+ return true;
+ }
+ }
+ }
+
+ // don't capture missing if this is a nameless subcommand and nameless subcommands can't fallthrough
+ if(parent_ != nullptr && name_.empty()) {
+ return false;
+ }
+
+ // If a subcommand, try the main command
+ if(parent_ != nullptr && fallthrough_)
+ return _get_fallthrough_parent()->_parse_arg(args, current_type);
+
+ // Otherwise, add to missing
+ args.pop_back();
+ _move_to_missing(current_type, current);
+ return true;
+ }
+
+ args.pop_back();
+
+ // Get a reference to the pointer to make syntax bearable
+ Option_p &op = *op_ptr;
+ /// if we require a separator add it here
+ if(op->get_inject_separator()) {
+ if(!op->results().empty() && !op->results().back().empty()) {
+ op->add_result(std::string{});
+ }
+ }
+ if(op->get_trigger_on_parse() && op->current_option_state_ == Option::option_state::callback_run) {
+ op->clear();
+ }
+ int min_num = (std::min)(op->get_type_size_min(), op->get_items_expected_min());
+ int max_num = op->get_items_expected_max();
+ // check container like options to limit the argument size to a single type if the allow_extra_flags argument is
+ // set. 16 is somewhat arbitrary (needs to be at least 4)
+ if(max_num >= detail::expected_max_vector_size / 16 && !op->get_allow_extra_args()) {
+ auto tmax = op->get_type_size_max();
+ max_num = detail::checked_multiply(tmax, op->get_expected_min()) ? tmax : detail::expected_max_vector_size;
+ }
+ // Make sure we always eat the minimum for unlimited vectors
+ int collected = 0; // total number of arguments collected
+ int result_count = 0; // local variable for number of results in a single arg string
+ // deal with purely flag like things
+ if(max_num == 0) {
+ auto res = op->get_flag_value(arg_name, value);
+ op->add_result(res);
+ parse_order_.push_back(op.get());
+ } else if(!value.empty()) { // --this=value
+ op->add_result(value, result_count);
+ parse_order_.push_back(op.get());
+ collected += result_count;
+ // -Trest
+ } else if(!rest.empty()) {
+ op->add_result(rest, result_count);
+ parse_order_.push_back(op.get());
+ rest = "";
+ collected += result_count;
+ }
+
+ // gather the minimum number of arguments
+ while(min_num > collected && !args.empty()) {
+ std::string current_ = args.back();
+ args.pop_back();
+ op->add_result(current_, result_count);
+ parse_order_.push_back(op.get());
+ collected += result_count;
+ }
+
+ if(min_num > collected) { // if we have run out of arguments and the minimum was not met
+ throw ArgumentMismatch::TypedAtLeast(op->get_name(), min_num, op->get_type_name());
+ }
+
+ // now check for optional arguments
+ if(max_num > collected || op->get_allow_extra_args()) { // we allow optional arguments
+ auto remreqpos = _count_remaining_positionals(true);
+ // we have met the minimum now optionally check up to the maximum
+ while((collected < max_num || op->get_allow_extra_args()) && !args.empty() &&
+ _recognize(args.back(), false) == detail::Classifier::NONE) {
+ // If any required positionals remain, don't keep eating
+ if(remreqpos >= args.size()) {
+ break;
+ }
+ if(validate_optional_arguments_) {
+ std::string optarg = args.back();
+ optarg = op->_validate(optarg, 0);
+ if(!optarg.empty()) {
+ break;
+ }
+ }
+ op->add_result(args.back(), result_count);
+ parse_order_.push_back(op.get());
+ args.pop_back();
+ collected += result_count;
+ }
+
+ // Allow -- to end an unlimited list and "eat" it
+ if(!args.empty() && _recognize(args.back()) == detail::Classifier::POSITIONAL_MARK)
+ args.pop_back();
+ // optional flag that didn't receive anything now get the default value
+ if(min_num == 0 && max_num > 0 && collected == 0) {
+ auto res = op->get_flag_value(arg_name, std::string{});
+ op->add_result(res);
+ parse_order_.push_back(op.get());
+ }
+ }
+ // if we only partially completed a type then add an empty string if allowed for later processing
+ if(min_num > 0 && (collected % op->get_type_size_max()) != 0) {
+ if(op->get_type_size_max() != op->get_type_size_min()) {
+ op->add_result(std::string{});
+ } else {
+ throw ArgumentMismatch::PartialType(op->get_name(), op->get_type_size_min(), op->get_type_name());
+ }
+ }
+ if(op->get_trigger_on_parse()) {
+ op->run_callback();
+ }
+ if(!rest.empty()) {
+ rest = "-" + rest;
+ args.push_back(rest);
+ }
+ return true;
+ }
+
+ /// Trigger the pre_parse callback if needed
+ void _trigger_pre_parse(std::size_t remaining_args) {
+ if(!pre_parse_called_) {
+ pre_parse_called_ = true;
+ if(pre_parse_callback_) {
+ pre_parse_callback_(remaining_args);
+ }
+ } else if(immediate_callback_) {
+ if(!name_.empty()) {
+ auto pcnt = parsed_;
+ auto extras = std::move(missing_);
+ clear();
+ parsed_ = pcnt;
+ pre_parse_called_ = true;
+ missing_ = std::move(extras);
+ }
+ }
+ }
+
+ /// Get the appropriate parent to fallthrough to which is the first one that has a name or the main app
+ App *_get_fallthrough_parent() {
+ if(parent_ == nullptr) {
+ throw(HorribleError("No Valid parent"));
+ }
+ auto fallthrough_parent = parent_;
+ while((fallthrough_parent->parent_ != nullptr) && (fallthrough_parent->get_name().empty())) {
+ fallthrough_parent = fallthrough_parent->parent_;
+ }
+ return fallthrough_parent;
+ }
+
+ /// Helper function to run through all possible comparisons of subcommand names to check there is no overlap
+ const std::string &_compare_subcommand_names(const App &subcom, const App &base) const {
+ static const std::string estring;
+ if(subcom.disabled_) {
+ return estring;
+ }
+ for(auto &subc : base.subcommands_) {
+ if(subc.get() != &subcom) {
+ if(subc->disabled_) {
+ continue;
+ }
+ if(!subcom.get_name().empty()) {
+ if(subc->check_name(subcom.get_name())) {
+ return subcom.get_name();
+ }
+ }
+ if(!subc->get_name().empty()) {
+ if(subcom.check_name(subc->get_name())) {
+ return subc->get_name();
+ }
+ }
+ for(const auto &les : subcom.aliases_) {
+ if(subc->check_name(les)) {
+ return les;
+ }
+ }
+ // this loop is needed in case of ignore_underscore or ignore_case on one but not the other
+ for(const auto &les : subc->aliases_) {
+ if(subcom.check_name(les)) {
+ return les;
+ }
+ }
+ // if the subcommand is an option group we need to check deeper
+ if(subc->get_name().empty()) {
+ auto &cmpres = _compare_subcommand_names(subcom, *subc);
+ if(!cmpres.empty()) {
+ return cmpres;
+ }
+ }
+ // if the test subcommand is an option group we need to check deeper
+ if(subcom.get_name().empty()) {
+ auto &cmpres = _compare_subcommand_names(*subc, subcom);
+ if(!cmpres.empty()) {
+ return cmpres;
+ }
+ }
+ }
+ }
+ return estring;
+ }
+ /// Helper function to place extra values in the most appropriate position
+ void _move_to_missing(detail::Classifier val_type, const std::string &val) {
+ if(allow_extras_ || subcommands_.empty()) {
+ missing_.emplace_back(val_type, val);
+ return;
+ }
+ // allow extra arguments to be places in an option group if it is allowed there
+ for(auto &subc : subcommands_) {
+ if(subc->name_.empty() && subc->allow_extras_) {
+ subc->missing_.emplace_back(val_type, val);
+ return;
+ }
+ }
+ // if we haven't found any place to put them yet put them in missing
+ missing_.emplace_back(val_type, val);
+ }
+
+ public:
+ /// function that could be used by subclasses of App to shift options around into subcommands
+ void _move_option(Option *opt, App *app) {
+ if(opt == nullptr) {
+ throw OptionNotFound("the option is NULL");
+ }
+ // verify that the give app is actually a subcommand
+ bool found = false;
+ for(auto &subc : subcommands_) {
+ if(app == subc.get()) {
+ found = true;
+ }
+ }
+ if(!found) {
+ throw OptionNotFound("The Given app is not a subcommand");
+ }
+
+ if((help_ptr_ == opt) || (help_all_ptr_ == opt))
+ throw OptionAlreadyAdded("cannot move help options");
+
+ if(config_ptr_ == opt)
+ throw OptionAlreadyAdded("cannot move config file options");
+
+ auto iterator =
+ std::find_if(std::begin(options_), std::end(options_), [opt](const Option_p &v) { return v.get() == opt; });
+ if(iterator != std::end(options_)) {
+ const auto &opt_p = *iterator;
+ if(std::find_if(std::begin(app->options_), std::end(app->options_), [&opt_p](const Option_p &v) {
+ return (*v == *opt_p);
+ }) == std::end(app->options_)) {
+ // only erase after the insertion was successful
+ app->options_.push_back(std::move(*iterator));
+ options_.erase(iterator);
+ } else {
+ throw OptionAlreadyAdded("option was not located: " + opt->get_name());
+ }
+ } else {
+ throw OptionNotFound("could not locate the given Option");
+ }
+ }
+}; // namespace CLI
+
+/// Extension of App to better manage groups of options
+class Option_group : public App {
+ public:
+ Option_group(std::string group_description, std::string group_name, App *parent)
+ : App(std::move(group_description), "", parent) {
+ group(group_name);
+ // option groups should have automatic fallthrough
+ }
+ using App::add_option;
+ /// Add an existing option to the Option_group
+ Option *add_option(Option *opt) {
+ if(get_parent() == nullptr) {
+ throw OptionNotFound("Unable to locate the specified option");
+ }
+ get_parent()->_move_option(opt, this);
+ return opt;
+ }
+ /// Add an existing option to the Option_group
+ void add_options(Option *opt) { add_option(opt); }
+ /// Add a bunch of options to the group
+ template <typename... Args> void add_options(Option *opt, Args... args) {
+ add_option(opt);
+ add_options(args...);
+ }
+ using App::add_subcommand;
+ /// Add an existing subcommand to be a member of an option_group
+ App *add_subcommand(App *subcom) {
+ App_p subc = subcom->get_parent()->get_subcommand_ptr(subcom);
+ subc->get_parent()->remove_subcommand(subcom);
+ add_subcommand(std::move(subc));
+ return subcom;
+ }
+};
+/// Helper function to enable one option group/subcommand when another is used
+inline void TriggerOn(App *trigger_app, App *app_to_enable) {
+ app_to_enable->enabled_by_default(false);
+ app_to_enable->disabled_by_default();
+ trigger_app->preparse_callback([app_to_enable](std::size_t) { app_to_enable->disabled(false); });
+}
+
+/// Helper function to enable one option group/subcommand when another is used
+inline void TriggerOn(App *trigger_app, std::vector<App *> apps_to_enable) {
+ for(auto &app : apps_to_enable) {
+ app->enabled_by_default(false);
+ app->disabled_by_default();
+ }
+
+ trigger_app->preparse_callback([apps_to_enable](std::size_t) {
+ for(auto &app : apps_to_enable) {
+ app->disabled(false);
+ }
+ });
+}
+
+/// Helper function to disable one option group/subcommand when another is used
+inline void TriggerOff(App *trigger_app, App *app_to_enable) {
+ app_to_enable->disabled_by_default(false);
+ app_to_enable->enabled_by_default();
+ trigger_app->preparse_callback([app_to_enable](std::size_t) { app_to_enable->disabled(); });
+}
+
+/// Helper function to disable one option group/subcommand when another is used
+inline void TriggerOff(App *trigger_app, std::vector<App *> apps_to_enable) {
+ for(auto &app : apps_to_enable) {
+ app->disabled_by_default(false);
+ app->enabled_by_default();
+ }
+
+ trigger_app->preparse_callback([apps_to_enable](std::size_t) {
+ for(auto &app : apps_to_enable) {
+ app->disabled();
+ }
+ });
+}
+
+/// Helper function to mark an option as deprecated
+inline void deprecate_option(Option *opt, const std::string &replacement = "") {
+ Validator deprecate_warning{[opt, replacement](std::string &) {
+ std::cout << opt->get_name() << " is deprecated please use '" << replacement
+ << "' instead\n";
+ return std::string();
+ },
+ "DEPRECATED"};
+ deprecate_warning.application_index(0);
+ opt->check(deprecate_warning);
+ if(!replacement.empty()) {
+ opt->description(opt->get_description() + " DEPRECATED: please use '" + replacement + "' instead");
+ }
+}
+
+/// Helper function to mark an option as deprecated
+inline void deprecate_option(App *app, const std::string &option_name, const std::string &replacement = "") {
+ auto opt = app->get_option(option_name);
+ deprecate_option(opt, replacement);
+}
+
+/// Helper function to mark an option as deprecated
+inline void deprecate_option(App &app, const std::string &option_name, const std::string &replacement = "") {
+ auto opt = app.get_option(option_name);
+ deprecate_option(opt, replacement);
+}
+
+/// Helper function to mark an option as retired
+inline void retire_option(App *app, Option *opt) {
+ App temp;
+ auto option_copy = temp.add_option(opt->get_name(false, true))
+ ->type_size(opt->get_type_size_min(), opt->get_type_size_max())
+ ->expected(opt->get_expected_min(), opt->get_expected_max())
+ ->allow_extra_args(opt->get_allow_extra_args());
+
+ app->remove_option(opt);
+ auto opt2 = app->add_option(option_copy->get_name(false, true), "option has been retired and has no effect")
+ ->type_name("RETIRED")
+ ->default_str("RETIRED")
+ ->type_size(option_copy->get_type_size_min(), option_copy->get_type_size_max())
+ ->expected(option_copy->get_expected_min(), option_copy->get_expected_max())
+ ->allow_extra_args(option_copy->get_allow_extra_args());
+
+ Validator retired_warning{[opt2](std::string &) {
+ std::cout << "WARNING " << opt2->get_name() << " is retired and has no effect\n";
+ return std::string();
+ },
+ ""};
+ retired_warning.application_index(0);
+ opt2->check(retired_warning);
+}
+
+/// Helper function to mark an option as retired
+inline void retire_option(App &app, Option *opt) { retire_option(&app, opt); }
+
+/// Helper function to mark an option as retired
+inline void retire_option(App *app, const std::string &option_name) {
+
+ auto opt = app->get_option_no_throw(option_name);
+ if(opt != nullptr) {
+ retire_option(app, opt);
+ return;
+ }
+ auto opt2 = app->add_option(option_name, "option has been retired and has no effect")
+ ->type_name("RETIRED")
+ ->expected(0, 1)
+ ->default_str("RETIRED");
+ Validator retired_warning{[opt2](std::string &) {
+ std::cout << "WARNING " << opt2->get_name() << " is retired and has no effect\n";
+ return std::string();
+ },
+ ""};
+ retired_warning.application_index(0);
+ opt2->check(retired_warning);
+}
+
+/// Helper function to mark an option as retired
+inline void retire_option(App &app, const std::string &option_name) { retire_option(&app, option_name); }
+
+namespace FailureMessage {
+
+/// Printout a clean, simple message on error (the default in CLI11 1.5+)
+inline std::string simple(const App *app, const Error &e) {
+ std::string header = std::string(e.what()) + "\n";
+ std::vector<std::string> names;
+
+ // Collect names
+ if(app->get_help_ptr() != nullptr)
+ names.push_back(app->get_help_ptr()->get_name());
+
+ if(app->get_help_all_ptr() != nullptr)
+ names.push_back(app->get_help_all_ptr()->get_name());
+
+ // If any names found, suggest those
+ if(!names.empty())
+ header += "Run with " + detail::join(names, " or ") + " for more information.\n";
+
+ return header;
+}
+
+/// Printout the full help string on error (if this fn is set, the old default for CLI11)
+inline std::string help(const App *app, const Error &e) {
+ std::string header = std::string("ERROR: ") + e.get_name() + ": " + e.what() + "\n";
+ header += app->help();
+ return header;
+}
+
+} // namespace FailureMessage
+
+namespace detail {
+/// This class is simply to allow tests access to App's protected functions
+struct AppFriend {
+#ifdef CLI11_CPP14
+
+ /// Wrap _parse_short, perfectly forward arguments and return
+ template <typename... Args> static decltype(auto) parse_arg(App *app, Args &&...args) {
+ return app->_parse_arg(std::forward<Args>(args)...);
+ }
+
+ /// Wrap _parse_subcommand, perfectly forward arguments and return
+ template <typename... Args> static decltype(auto) parse_subcommand(App *app, Args &&...args) {
+ return app->_parse_subcommand(std::forward<Args>(args)...);
+ }
+#else
+ /// Wrap _parse_short, perfectly forward arguments and return
+ template <typename... Args>
+ static auto parse_arg(App *app, Args &&...args) ->
+ typename std::result_of<decltype (&App::_parse_arg)(App, Args...)>::type {
+ return app->_parse_arg(std::forward<Args>(args)...);
+ }
+
+ /// Wrap _parse_subcommand, perfectly forward arguments and return
+ template <typename... Args>
+ static auto parse_subcommand(App *app, Args &&...args) ->
+ typename std::result_of<decltype (&App::_parse_subcommand)(App, Args...)>::type {
+ return app->_parse_subcommand(std::forward<Args>(args)...);
+ }
+#endif
+ /// Wrap the fallthrough parent function to make sure that is working correctly
+ static App *get_fallthrough_parent(App *app) { return app->_get_fallthrough_parent(); }
+};
+} // namespace detail
+
+// [CLI11:app_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/CLI.hpp b/src/third-party/CLI/CLI.hpp
new file mode 100644
index 0000000..0b6c344
--- /dev/null
+++ b/src/third-party/CLI/CLI.hpp
@@ -0,0 +1,36 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// CLI Library includes
+// Order is important for combiner script
+
+#include "Version.hpp"
+
+#include "Macros.hpp"
+
+#include "StringTools.hpp"
+
+#include "Error.hpp"
+
+#include "TypeTools.hpp"
+
+#include "Split.hpp"
+
+#include "ConfigFwd.hpp"
+
+#include "Validators.hpp"
+
+#include "FormatterFwd.hpp"
+
+#include "Option.hpp"
+
+#include "App.hpp"
+
+#include "Config.hpp"
+
+#include "Formatter.hpp"
diff --git a/src/third-party/CLI/Config.hpp b/src/third-party/CLI/Config.hpp
new file mode 100644
index 0000000..9555173
--- /dev/null
+++ b/src/third-party/CLI/Config.hpp
@@ -0,0 +1,396 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <algorithm>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <utility>
+#include <vector>
+// [CLI11:public_includes:set]
+
+#include "App.hpp"
+#include "ConfigFwd.hpp"
+#include "StringTools.hpp"
+
+namespace CLI {
+// [CLI11:config_hpp:verbatim]
+namespace detail {
+
+inline std::string convert_arg_for_ini(const std::string &arg, char stringQuote = '"', char characterQuote = '\'') {
+ if(arg.empty()) {
+ return std::string(2, stringQuote);
+ }
+ // some specifically supported strings
+ if(arg == "true" || arg == "false" || arg == "nan" || arg == "inf") {
+ return arg;
+ }
+ // floating point conversion can convert some hex codes, but don't try that here
+ if(arg.compare(0, 2, "0x") != 0 && arg.compare(0, 2, "0X") != 0) {
+ double val;
+ if(detail::lexical_cast(arg, val)) {
+ return arg;
+ }
+ }
+ // just quote a single non numeric character
+ if(arg.size() == 1) {
+ return std::string(1, characterQuote) + arg + characterQuote;
+ }
+ // handle hex, binary or octal arguments
+ if(arg.front() == '0') {
+ if(arg[1] == 'x') {
+ if(std::all_of(arg.begin() + 2, arg.end(), [](char x) {
+ return (x >= '0' && x <= '9') || (x >= 'A' && x <= 'F') || (x >= 'a' && x <= 'f');
+ })) {
+ return arg;
+ }
+ } else if(arg[1] == 'o') {
+ if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x >= '0' && x <= '7'); })) {
+ return arg;
+ }
+ } else if(arg[1] == 'b') {
+ if(std::all_of(arg.begin() + 2, arg.end(), [](char x) { return (x == '0' || x == '1'); })) {
+ return arg;
+ }
+ }
+ }
+ if(arg.find_first_of(stringQuote) == std::string::npos) {
+ return std::string(1, stringQuote) + arg + stringQuote;
+ } else {
+ return characterQuote + arg + characterQuote;
+ }
+}
+
+/// Comma separated join, adds quotes if needed
+inline std::string ini_join(const std::vector<std::string> &args,
+ char sepChar = ',',
+ char arrayStart = '[',
+ char arrayEnd = ']',
+ char stringQuote = '"',
+ char characterQuote = '\'') {
+ std::string joined;
+ if(args.size() > 1 && arrayStart != '\0') {
+ joined.push_back(arrayStart);
+ }
+ std::size_t start = 0;
+ for(const auto &arg : args) {
+ if(start++ > 0) {
+ joined.push_back(sepChar);
+ if(isspace(sepChar) == 0) {
+ joined.push_back(' ');
+ }
+ }
+ joined.append(convert_arg_for_ini(arg, stringQuote, characterQuote));
+ }
+ if(args.size() > 1 && arrayEnd != '\0') {
+ joined.push_back(arrayEnd);
+ }
+ return joined;
+}
+
+inline std::vector<std::string> generate_parents(const std::string &section, std::string &name, char parentSeparator) {
+ std::vector<std::string> parents;
+ if(detail::to_lower(section) != "default") {
+ if(section.find(parentSeparator) != std::string::npos) {
+ parents = detail::split(section, parentSeparator);
+ } else {
+ parents = {section};
+ }
+ }
+ if(name.find(parentSeparator) != std::string::npos) {
+ std::vector<std::string> plist = detail::split(name, parentSeparator);
+ name = plist.back();
+ detail::remove_quotes(name);
+ plist.pop_back();
+ parents.insert(parents.end(), plist.begin(), plist.end());
+ }
+
+ // clean up quotes on the parents
+ for(auto &parent : parents) {
+ detail::remove_quotes(parent);
+ }
+ return parents;
+}
+
+/// assuming non default segments do a check on the close and open of the segments in a configItem structure
+inline void
+checkParentSegments(std::vector<ConfigItem> &output, const std::string &currentSection, char parentSeparator) {
+
+ std::string estring;
+ auto parents = detail::generate_parents(currentSection, estring, parentSeparator);
+ if(!output.empty() && output.back().name == "--") {
+ std::size_t msize = (parents.size() > 1U) ? parents.size() : 2;
+ while(output.back().parents.size() >= msize) {
+ output.push_back(output.back());
+ output.back().parents.pop_back();
+ }
+
+ if(parents.size() > 1) {
+ std::size_t common = 0;
+ std::size_t mpair = (std::min)(output.back().parents.size(), parents.size() - 1);
+ for(std::size_t ii = 0; ii < mpair; ++ii) {
+ if(output.back().parents[ii] != parents[ii]) {
+ break;
+ }
+ ++common;
+ }
+ if(common == mpair) {
+ output.pop_back();
+ } else {
+ while(output.back().parents.size() > common + 1) {
+ output.push_back(output.back());
+ output.back().parents.pop_back();
+ }
+ }
+ for(std::size_t ii = common; ii < parents.size() - 1; ++ii) {
+ output.emplace_back();
+ output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
+ output.back().name = "++";
+ }
+ }
+ } else if(parents.size() > 1) {
+ for(std::size_t ii = 0; ii < parents.size() - 1; ++ii) {
+ output.emplace_back();
+ output.back().parents.assign(parents.begin(), parents.begin() + static_cast<std::ptrdiff_t>(ii) + 1);
+ output.back().name = "++";
+ }
+ }
+
+ // insert a section end which is just an empty items_buffer
+ output.emplace_back();
+ output.back().parents = std::move(parents);
+ output.back().name = "++";
+}
+} // namespace detail
+
+inline std::vector<ConfigItem> ConfigBase::from_config(std::istream &input) const {
+ std::string line;
+ std::string currentSection = "default";
+ std::string previousSection = "default";
+ std::vector<ConfigItem> output;
+ bool isDefaultArray = (arrayStart == '[' && arrayEnd == ']' && arraySeparator == ',');
+ bool isINIArray = (arrayStart == '\0' || arrayStart == ' ') && arrayStart == arrayEnd;
+ bool inSection{false};
+ char aStart = (isINIArray) ? '[' : arrayStart;
+ char aEnd = (isINIArray) ? ']' : arrayEnd;
+ char aSep = (isINIArray && arraySeparator == ' ') ? ',' : arraySeparator;
+ int currentSectionIndex{0};
+ while(getline(input, line)) {
+ std::vector<std::string> items_buffer;
+ std::string name;
+
+ detail::trim(line);
+ std::size_t len = line.length();
+ // lines have to be at least 3 characters to have any meaning to CLI just skip the rest
+ if(len < 3) {
+ continue;
+ }
+ if(line.front() == '[' && line.back() == ']') {
+ if(currentSection != "default") {
+ // insert a section end which is just an empty items_buffer
+ output.emplace_back();
+ output.back().parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
+ output.back().name = "--";
+ }
+ currentSection = line.substr(1, len - 2);
+ // deal with double brackets for TOML
+ if(currentSection.size() > 1 && currentSection.front() == '[' && currentSection.back() == ']') {
+ currentSection = currentSection.substr(1, currentSection.size() - 2);
+ }
+ if(detail::to_lower(currentSection) == "default") {
+ currentSection = "default";
+ } else {
+ detail::checkParentSegments(output, currentSection, parentSeparatorChar);
+ }
+ inSection = false;
+ if(currentSection == previousSection) {
+ ++currentSectionIndex;
+ } else {
+ currentSectionIndex = 0;
+ previousSection = currentSection;
+ }
+ continue;
+ }
+
+ // comment lines
+ if(line.front() == ';' || line.front() == '#' || line.front() == commentChar) {
+ continue;
+ }
+
+ // Find = in string, split and recombine
+ auto pos = line.find(valueDelimiter);
+ if(pos != std::string::npos) {
+ name = detail::trim_copy(line.substr(0, pos));
+ std::string item = detail::trim_copy(line.substr(pos + 1));
+ auto cloc = item.find(commentChar);
+ if(cloc != std::string::npos) {
+ item.erase(cloc, std::string::npos);
+ detail::trim(item);
+ }
+ if(item.size() > 1 && item.front() == aStart) {
+ for(std::string multiline; item.back() != aEnd && std::getline(input, multiline);) {
+ detail::trim(multiline);
+ item += multiline;
+ }
+ items_buffer = detail::split_up(item.substr(1, item.length() - 2), aSep);
+ } else if((isDefaultArray || isINIArray) && item.find_first_of(aSep) != std::string::npos) {
+ items_buffer = detail::split_up(item, aSep);
+ } else if((isDefaultArray || isINIArray) && item.find_first_of(' ') != std::string::npos) {
+ items_buffer = detail::split_up(item);
+ } else {
+ items_buffer = {item};
+ }
+ } else {
+ name = detail::trim_copy(line);
+ auto cloc = name.find(commentChar);
+ if(cloc != std::string::npos) {
+ name.erase(cloc, std::string::npos);
+ detail::trim(name);
+ }
+
+ items_buffer = {"true"};
+ }
+ if(name.find(parentSeparatorChar) == std::string::npos) {
+ detail::remove_quotes(name);
+ }
+ // clean up quotes on the items
+ for(auto &it : items_buffer) {
+ detail::remove_quotes(it);
+ }
+
+ std::vector<std::string> parents = detail::generate_parents(currentSection, name, parentSeparatorChar);
+ if(parents.size() > maximumLayers) {
+ continue;
+ }
+ if(!configSection.empty() && !inSection) {
+ if(parents.empty() || parents.front() != configSection) {
+ continue;
+ }
+ if(configIndex >= 0 && currentSectionIndex != configIndex) {
+ continue;
+ }
+ parents.erase(parents.begin());
+ inSection = true;
+ }
+ if(!output.empty() && name == output.back().name && parents == output.back().parents) {
+ output.back().inputs.insert(output.back().inputs.end(), items_buffer.begin(), items_buffer.end());
+ } else {
+ output.emplace_back();
+ output.back().parents = std::move(parents);
+ output.back().name = std::move(name);
+ output.back().inputs = std::move(items_buffer);
+ }
+ }
+ if(currentSection != "default") {
+ // insert a section end which is just an empty items_buffer
+ std::string ename;
+ output.emplace_back();
+ output.back().parents = detail::generate_parents(currentSection, ename, parentSeparatorChar);
+ output.back().name = "--";
+ while(output.back().parents.size() > 1) {
+ output.push_back(output.back());
+ output.back().parents.pop_back();
+ }
+ }
+ return output;
+}
+
+inline std::string
+ConfigBase::to_config(const App *app, bool default_also, bool write_description, std::string prefix) const {
+ std::stringstream out;
+ std::string commentLead;
+ commentLead.push_back(commentChar);
+ commentLead.push_back(' ');
+
+ std::vector<std::string> groups = app->get_groups();
+ bool defaultUsed = false;
+ groups.insert(groups.begin(), std::string("Options"));
+ if(write_description && (app->get_configurable() || app->get_parent() == nullptr || app->get_name().empty())) {
+ out << commentLead << detail::fix_newlines(commentLead, app->get_description()) << '\n';
+ }
+ for(auto &group : groups) {
+ if(group == "Options" || group.empty()) {
+ if(defaultUsed) {
+ continue;
+ }
+ defaultUsed = true;
+ }
+ if(write_description && group != "Options" && !group.empty()) {
+ out << '\n' << commentLead << group << " Options\n";
+ }
+ for(const Option *opt : app->get_options({})) {
+
+ // Only process options that are configurable
+ if(opt->get_configurable()) {
+ if(opt->get_group() != group) {
+ if(!(group == "Options" && opt->get_group().empty())) {
+ continue;
+ }
+ }
+ std::string name = prefix + opt->get_single_name();
+ std::string value = detail::ini_join(
+ opt->reduced_results(), arraySeparator, arrayStart, arrayEnd, stringQuote, characterQuote);
+
+ if(value.empty() && default_also) {
+ if(!opt->get_default_str().empty()) {
+ value = detail::convert_arg_for_ini(opt->get_default_str(), stringQuote, characterQuote);
+ } else if(opt->get_expected_min() == 0) {
+ value = "false";
+ } else if(opt->get_run_callback_for_default()) {
+ value = "\"\""; // empty string default value
+ }
+ }
+
+ if(!value.empty()) {
+ if(write_description && opt->has_description()) {
+ out << '\n';
+ out << commentLead << detail::fix_newlines(commentLead, opt->get_description()) << '\n';
+ }
+ out << name << valueDelimiter << value << '\n';
+ }
+ }
+ }
+ }
+ auto subcommands = app->get_subcommands({});
+ for(const App *subcom : subcommands) {
+ if(subcom->get_name().empty()) {
+ if(write_description && !subcom->get_group().empty()) {
+ out << '\n' << commentLead << subcom->get_group() << " Options\n";
+ }
+ out << to_config(subcom, default_also, write_description, prefix);
+ }
+ }
+
+ for(const App *subcom : subcommands) {
+ if(!subcom->get_name().empty()) {
+ if(subcom->get_configurable() && app->got_subcommand(subcom)) {
+ if(!prefix.empty() || app->get_parent() == nullptr) {
+ out << '[' << prefix << subcom->get_name() << "]\n";
+ } else {
+ std::string subname = app->get_name() + parentSeparatorChar + subcom->get_name();
+ auto p = app->get_parent();
+ while(p->get_parent() != nullptr) {
+ subname = p->get_name() + parentSeparatorChar + subname;
+ p = p->get_parent();
+ }
+ out << '[' << subname << "]\n";
+ }
+ out << to_config(subcom, default_also, write_description, "");
+ } else {
+ out << to_config(
+ subcom, default_also, write_description, prefix + subcom->get_name() + parentSeparatorChar);
+ }
+ }
+ }
+
+ return out.str();
+}
+
+// [CLI11:config_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/ConfigFwd.hpp b/src/third-party/CLI/ConfigFwd.hpp
new file mode 100644
index 0000000..b47133c
--- /dev/null
+++ b/src/third-party/CLI/ConfigFwd.hpp
@@ -0,0 +1,185 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <algorithm>
+#include <fstream>
+#include <iostream>
+#include <string>
+#include <vector>
+// [CLI11:public_includes:end]
+
+#include "Error.hpp"
+#include "StringTools.hpp"
+
+namespace CLI {
+// [CLI11:config_fwd_hpp:verbatim]
+
+class App;
+
+/// Holds values to load into Options
+struct ConfigItem {
+ /// This is the list of parents
+ std::vector<std::string> parents{};
+
+ /// This is the name
+ std::string name{};
+
+ /// Listing of inputs
+ std::vector<std::string> inputs{};
+
+ /// The list of parents and name joined by "."
+ std::string fullname() const {
+ std::vector<std::string> tmp = parents;
+ tmp.emplace_back(name);
+ return detail::join(tmp, ".");
+ }
+};
+
+/// This class provides a converter for configuration files.
+class Config {
+ protected:
+ std::vector<ConfigItem> items{};
+
+ public:
+ /// Convert an app into a configuration
+ virtual std::string to_config(const App *, bool, bool, std::string) const = 0;
+
+ /// Convert a configuration into an app
+ virtual std::vector<ConfigItem> from_config(std::istream &) const = 0;
+
+ /// Get a flag value
+ virtual std::string to_flag(const ConfigItem &item) const {
+ if(item.inputs.size() == 1) {
+ return item.inputs.at(0);
+ }
+ if(item.inputs.empty()) {
+ return "{}";
+ }
+ throw ConversionError::TooManyInputsFlag(item.fullname());
+ }
+
+ /// Parse a config file, throw an error (ParseError:ConfigParseError or FileError) on failure
+ std::vector<ConfigItem> from_file(const std::string &name) {
+ std::ifstream input{name};
+ if(!input.good())
+ throw FileError::Missing(name);
+
+ return from_config(input);
+ }
+
+ /// Virtual destructor
+ virtual ~Config() = default;
+};
+
+/// This converter works with INI/TOML files; to write INI files use ConfigINI
+class ConfigBase : public Config {
+ protected:
+ /// the character used for comments
+ char commentChar = '#';
+ /// the character used to start an array '\0' is a default to not use
+ char arrayStart = '[';
+ /// the character used to end an array '\0' is a default to not use
+ char arrayEnd = ']';
+ /// the character used to separate elements in an array
+ char arraySeparator = ',';
+ /// the character used separate the name from the value
+ char valueDelimiter = '=';
+ /// the character to use around strings
+ char stringQuote = '"';
+ /// the character to use around single characters
+ char characterQuote = '\'';
+ /// the maximum number of layers to allow
+ uint8_t maximumLayers{255};
+ /// the separator used to separator parent layers
+ char parentSeparatorChar{'.'};
+ /// Specify the configuration index to use for arrayed sections
+ int16_t configIndex{-1};
+ /// Specify the configuration section that should be used
+ std::string configSection{};
+
+ public:
+ std::string
+ to_config(const App * /*app*/, bool default_also, bool write_description, std::string prefix) const override;
+
+ std::vector<ConfigItem> from_config(std::istream &input) const override;
+ /// Specify the configuration for comment characters
+ ConfigBase *comment(char cchar) {
+ commentChar = cchar;
+ return this;
+ }
+ /// Specify the start and end characters for an array
+ ConfigBase *arrayBounds(char aStart, char aEnd) {
+ arrayStart = aStart;
+ arrayEnd = aEnd;
+ return this;
+ }
+ /// Specify the delimiter character for an array
+ ConfigBase *arrayDelimiter(char aSep) {
+ arraySeparator = aSep;
+ return this;
+ }
+ /// Specify the delimiter between a name and value
+ ConfigBase *valueSeparator(char vSep) {
+ valueDelimiter = vSep;
+ return this;
+ }
+ /// Specify the quote characters used around strings and characters
+ ConfigBase *quoteCharacter(char qString, char qChar) {
+ stringQuote = qString;
+ characterQuote = qChar;
+ return this;
+ }
+ /// Specify the maximum number of parents
+ ConfigBase *maxLayers(uint8_t layers) {
+ maximumLayers = layers;
+ return this;
+ }
+ /// Specify the separator to use for parent layers
+ ConfigBase *parentSeparator(char sep) {
+ parentSeparatorChar = sep;
+ return this;
+ }
+ /// get a reference to the configuration section
+ std::string &sectionRef() { return configSection; }
+ /// get the section
+ const std::string &section() const { return configSection; }
+ /// specify a particular section of the configuration file to use
+ ConfigBase *section(const std::string &sectionName) {
+ configSection = sectionName;
+ return this;
+ }
+
+ /// get a reference to the configuration index
+ int16_t &indexRef() { return configIndex; }
+ /// get the section index
+ int16_t index() const { return configIndex; }
+ /// specify a particular index in the section to use (-1) for all sections to use
+ ConfigBase *index(int16_t sectionIndex) {
+ configIndex = sectionIndex;
+ return this;
+ }
+};
+
+/// the default Config is the TOML file format
+using ConfigTOML = ConfigBase;
+
+/// ConfigINI generates a "standard" INI compliant output
+class ConfigINI : public ConfigTOML {
+
+ public:
+ ConfigINI() {
+ commentChar = ';';
+ arrayStart = '\0';
+ arrayEnd = '\0';
+ arraySeparator = ' ';
+ valueDelimiter = '=';
+ }
+};
+// [CLI11:config_fwd_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/Error.hpp b/src/third-party/CLI/Error.hpp
new file mode 100644
index 0000000..b2c078d
--- /dev/null
+++ b/src/third-party/CLI/Error.hpp
@@ -0,0 +1,355 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <exception>
+#include <stdexcept>
+#include <string>
+#include <utility>
+#include <vector>
+// [CLI11:public_includes:end]
+
+// CLI library includes
+#include "StringTools.hpp"
+
+namespace CLI {
+// [CLI11:error_hpp:verbatim]
+
+// Use one of these on all error classes.
+// These are temporary and are undef'd at the end of this file.
+#define CLI11_ERROR_DEF(parent, name) \
+ protected: \
+ name(std::string ename, std::string msg, int exit_code) : parent(std::move(ename), std::move(msg), exit_code) {} \
+ name(std::string ename, std::string msg, ExitCodes exit_code) \
+ : parent(std::move(ename), std::move(msg), exit_code) {} \
+ \
+ public: \
+ name(std::string msg, ExitCodes exit_code) : parent(#name, std::move(msg), exit_code) {} \
+ name(std::string msg, int exit_code) : parent(#name, std::move(msg), exit_code) {}
+
+// This is added after the one above if a class is used directly and builds its own message
+#define CLI11_ERROR_SIMPLE(name) \
+ explicit name(std::string msg) : name(#name, msg, ExitCodes::name) {}
+
+/// These codes are part of every error in CLI. They can be obtained from e using e.exit_code or as a quick shortcut,
+/// int values from e.get_error_code().
+enum class ExitCodes {
+ Success = 0,
+ IncorrectConstruction = 100,
+ BadNameString,
+ OptionAlreadyAdded,
+ FileError,
+ ConversionError,
+ ValidationError,
+ RequiredError,
+ RequiresError,
+ ExcludesError,
+ ExtrasError,
+ ConfigError,
+ InvalidError,
+ HorribleError,
+ OptionNotFound,
+ ArgumentMismatch,
+ BaseClass = 127
+};
+
+// Error definitions
+
+/// @defgroup error_group Errors
+/// @brief Errors thrown by CLI11
+///
+/// These are the errors that can be thrown. Some of them, like CLI::Success, are not really errors.
+/// @{
+
+/// All errors derive from this one
+class Error : public std::runtime_error {
+ int actual_exit_code;
+ std::string error_name{"Error"};
+
+ public:
+ int get_exit_code() const { return actual_exit_code; }
+
+ std::string get_name() const { return error_name; }
+
+ Error(std::string name, std::string msg, int exit_code = static_cast<int>(ExitCodes::BaseClass))
+ : runtime_error(msg), actual_exit_code(exit_code), error_name(std::move(name)) {}
+
+ Error(std::string name, std::string msg, ExitCodes exit_code) : Error(name, msg, static_cast<int>(exit_code)) {}
+};
+
+// Note: Using Error::Error constructors does not work on GCC 4.7
+
+/// Construction errors (not in parsing)
+class ConstructionError : public Error {
+ CLI11_ERROR_DEF(Error, ConstructionError)
+};
+
+/// Thrown when an option is set to conflicting values (non-vector and multi args, for example)
+class IncorrectConstruction : public ConstructionError {
+ CLI11_ERROR_DEF(ConstructionError, IncorrectConstruction)
+ CLI11_ERROR_SIMPLE(IncorrectConstruction)
+ static IncorrectConstruction PositionalFlag(std::string name) {
+ return IncorrectConstruction(name + ": Flags cannot be positional");
+ }
+ static IncorrectConstruction Set0Opt(std::string name) {
+ return IncorrectConstruction(name + ": Cannot set 0 expected, use a flag instead");
+ }
+ static IncorrectConstruction SetFlag(std::string name) {
+ return IncorrectConstruction(name + ": Cannot set an expected number for flags");
+ }
+ static IncorrectConstruction ChangeNotVector(std::string name) {
+ return IncorrectConstruction(name + ": You can only change the expected arguments for vectors");
+ }
+ static IncorrectConstruction AfterMultiOpt(std::string name) {
+ return IncorrectConstruction(
+ name + ": You can't change expected arguments after you've changed the multi option policy!");
+ }
+ static IncorrectConstruction MissingOption(std::string name) {
+ return IncorrectConstruction("Option " + name + " is not defined");
+ }
+ static IncorrectConstruction MultiOptionPolicy(std::string name) {
+ return IncorrectConstruction(name + ": multi_option_policy only works for flags and exact value options");
+ }
+};
+
+/// Thrown on construction of a bad name
+class BadNameString : public ConstructionError {
+ CLI11_ERROR_DEF(ConstructionError, BadNameString)
+ CLI11_ERROR_SIMPLE(BadNameString)
+ static BadNameString OneCharName(std::string name) { return BadNameString("Invalid one char name: " + name); }
+ static BadNameString BadLongName(std::string name) { return BadNameString("Bad long name: " + name); }
+ static BadNameString DashesOnly(std::string name) {
+ return BadNameString("Must have a name, not just dashes: " + name);
+ }
+ static BadNameString MultiPositionalNames(std::string name) {
+ return BadNameString("Only one positional name allowed, remove: " + name);
+ }
+};
+
+/// Thrown when an option already exists
+class OptionAlreadyAdded : public ConstructionError {
+ CLI11_ERROR_DEF(ConstructionError, OptionAlreadyAdded)
+ explicit OptionAlreadyAdded(std::string name)
+ : OptionAlreadyAdded(name + " is already added", ExitCodes::OptionAlreadyAdded) {}
+ static OptionAlreadyAdded Requires(std::string name, std::string other) {
+ return OptionAlreadyAdded(name + " requires " + other, ExitCodes::OptionAlreadyAdded);
+ }
+ static OptionAlreadyAdded Excludes(std::string name, std::string other) {
+ return OptionAlreadyAdded(name + " excludes " + other, ExitCodes::OptionAlreadyAdded);
+ }
+};
+
+// Parsing errors
+
+/// Anything that can error in Parse
+class ParseError : public Error {
+ CLI11_ERROR_DEF(Error, ParseError)
+};
+
+// Not really "errors"
+
+/// This is a successful completion on parsing, supposed to exit
+class Success : public ParseError {
+ CLI11_ERROR_DEF(ParseError, Success)
+ Success() : Success("Successfully completed, should be caught and quit", ExitCodes::Success) {}
+};
+
+/// -h or --help on command line
+class CallForHelp : public Success {
+ CLI11_ERROR_DEF(Success, CallForHelp)
+ CallForHelp() : CallForHelp("This should be caught in your main function, see examples", ExitCodes::Success) {}
+};
+
+/// Usually something like --help-all on command line
+class CallForAllHelp : public Success {
+ CLI11_ERROR_DEF(Success, CallForAllHelp)
+ CallForAllHelp()
+ : CallForAllHelp("This should be caught in your main function, see examples", ExitCodes::Success) {}
+};
+
+/// -v or --version on command line
+class CallForVersion : public Success {
+ CLI11_ERROR_DEF(Success, CallForVersion)
+ CallForVersion()
+ : CallForVersion("This should be caught in your main function, see examples", ExitCodes::Success) {}
+};
+
+/// Does not output a diagnostic in CLI11_PARSE, but allows main() to return with a specific error code.
+class RuntimeError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, RuntimeError)
+ explicit RuntimeError(int exit_code = 1) : RuntimeError("Runtime error", exit_code) {}
+};
+
+/// Thrown when parsing an INI file and it is missing
+class FileError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, FileError)
+ CLI11_ERROR_SIMPLE(FileError)
+ static FileError Missing(std::string name) { return FileError(name + " was not readable (missing?)"); }
+};
+
+/// Thrown when conversion call back fails, such as when an int fails to coerce to a string
+class ConversionError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ConversionError)
+ CLI11_ERROR_SIMPLE(ConversionError)
+ ConversionError(std::string member, std::string name)
+ : ConversionError("The value " + member + " is not an allowed value for " + name) {}
+ ConversionError(std::string name, std::vector<std::string> results)
+ : ConversionError("Could not convert: " + name + " = " + detail::join(results)) {}
+ static ConversionError TooManyInputsFlag(std::string name) {
+ return ConversionError(name + ": too many inputs for a flag");
+ }
+ static ConversionError TrueFalse(std::string name) {
+ return ConversionError(name + ": Should be true/false or a number");
+ }
+};
+
+/// Thrown when validation of results fails
+class ValidationError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ValidationError)
+ CLI11_ERROR_SIMPLE(ValidationError)
+ explicit ValidationError(std::string name, std::string msg) : ValidationError(name + ": " + msg) {}
+};
+
+/// Thrown when a required option is missing
+class RequiredError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, RequiredError)
+ explicit RequiredError(std::string name) : RequiredError(name + " is required", ExitCodes::RequiredError) {}
+ static RequiredError Subcommand(std::size_t min_subcom) {
+ if(min_subcom == 1) {
+ return RequiredError("A subcommand");
+ }
+ return RequiredError("Requires at least " + std::to_string(min_subcom) + " subcommands",
+ ExitCodes::RequiredError);
+ }
+ static RequiredError
+ Option(std::size_t min_option, std::size_t max_option, std::size_t used, const std::string &option_list) {
+ if((min_option == 1) && (max_option == 1) && (used == 0))
+ return RequiredError("Exactly 1 option from [" + option_list + "]");
+ if((min_option == 1) && (max_option == 1) && (used > 1)) {
+ return RequiredError("Exactly 1 option from [" + option_list + "] is required and " + std::to_string(used) +
+ " were given",
+ ExitCodes::RequiredError);
+ }
+ if((min_option == 1) && (used == 0))
+ return RequiredError("At least 1 option from [" + option_list + "]");
+ if(used < min_option) {
+ return RequiredError("Requires at least " + std::to_string(min_option) + " options used and only " +
+ std::to_string(used) + "were given from [" + option_list + "]",
+ ExitCodes::RequiredError);
+ }
+ if(max_option == 1)
+ return RequiredError("Requires at most 1 options be given from [" + option_list + "]",
+ ExitCodes::RequiredError);
+
+ return RequiredError("Requires at most " + std::to_string(max_option) + " options be used and " +
+ std::to_string(used) + "were given from [" + option_list + "]",
+ ExitCodes::RequiredError);
+ }
+};
+
+/// Thrown when the wrong number of arguments has been received
+class ArgumentMismatch : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ArgumentMismatch)
+ CLI11_ERROR_SIMPLE(ArgumentMismatch)
+ ArgumentMismatch(std::string name, int expected, std::size_t received)
+ : ArgumentMismatch(expected > 0 ? ("Expected exactly " + std::to_string(expected) + " arguments to " + name +
+ ", got " + std::to_string(received))
+ : ("Expected at least " + std::to_string(-expected) + " arguments to " + name +
+ ", got " + std::to_string(received)),
+ ExitCodes::ArgumentMismatch) {}
+
+ static ArgumentMismatch AtLeast(std::string name, int num, std::size_t received) {
+ return ArgumentMismatch(name + ": At least " + std::to_string(num) + " required but received " +
+ std::to_string(received));
+ }
+ static ArgumentMismatch AtMost(std::string name, int num, std::size_t received) {
+ return ArgumentMismatch(name + ": At Most " + std::to_string(num) + " required but received " +
+ std::to_string(received));
+ }
+ static ArgumentMismatch TypedAtLeast(std::string name, int num, std::string type) {
+ return ArgumentMismatch(name + ": " + std::to_string(num) + " required " + type + " missing");
+ }
+ static ArgumentMismatch FlagOverride(std::string name) {
+ return ArgumentMismatch(name + " was given a disallowed flag override");
+ }
+ static ArgumentMismatch PartialType(std::string name, int num, std::string type) {
+ return ArgumentMismatch(name + ": " + type + " only partially specified: " + std::to_string(num) +
+ " required for each element");
+ }
+};
+
+/// Thrown when a requires option is missing
+class RequiresError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, RequiresError)
+ RequiresError(std::string curname, std::string subname)
+ : RequiresError(curname + " requires " + subname, ExitCodes::RequiresError) {}
+};
+
+/// Thrown when an excludes option is present
+class ExcludesError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ExcludesError)
+ ExcludesError(std::string curname, std::string subname)
+ : ExcludesError(curname + " excludes " + subname, ExitCodes::ExcludesError) {}
+};
+
+/// Thrown when too many positionals or options are found
+class ExtrasError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ExtrasError)
+ explicit ExtrasError(std::vector<std::string> args)
+ : ExtrasError((args.size() > 1 ? "The following arguments were not expected: "
+ : "The following argument was not expected: ") +
+ detail::rjoin(args, " "),
+ ExitCodes::ExtrasError) {}
+ ExtrasError(const std::string &name, std::vector<std::string> args)
+ : ExtrasError(name,
+ (args.size() > 1 ? "The following arguments were not expected: "
+ : "The following argument was not expected: ") +
+ detail::rjoin(args, " "),
+ ExitCodes::ExtrasError) {}
+};
+
+/// Thrown when extra values are found in an INI file
+class ConfigError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, ConfigError)
+ CLI11_ERROR_SIMPLE(ConfigError)
+ static ConfigError Extras(std::string item) { return ConfigError("INI was not able to parse " + item); }
+ static ConfigError NotConfigurable(std::string item) {
+ return ConfigError(item + ": This option is not allowed in a configuration file");
+ }
+};
+
+/// Thrown when validation fails before parsing
+class InvalidError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, InvalidError)
+ explicit InvalidError(std::string name)
+ : InvalidError(name + ": Too many positional arguments with unlimited expected args", ExitCodes::InvalidError) {
+ }
+};
+
+/// This is just a safety check to verify selection and parsing match - you should not ever see it
+/// Strings are directly added to this error, but again, it should never be seen.
+class HorribleError : public ParseError {
+ CLI11_ERROR_DEF(ParseError, HorribleError)
+ CLI11_ERROR_SIMPLE(HorribleError)
+};
+
+// After parsing
+
+/// Thrown when counting a non-existent option
+class OptionNotFound : public Error {
+ CLI11_ERROR_DEF(Error, OptionNotFound)
+ explicit OptionNotFound(std::string name) : OptionNotFound(name + " not found", ExitCodes::OptionNotFound) {}
+};
+
+#undef CLI11_ERROR_DEF
+#undef CLI11_ERROR_SIMPLE
+
+/// @}
+
+// [CLI11:error_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/Formatter.hpp b/src/third-party/CLI/Formatter.hpp
new file mode 100644
index 0000000..4d2b5fa
--- /dev/null
+++ b/src/third-party/CLI/Formatter.hpp
@@ -0,0 +1,292 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <algorithm>
+#include <string>
+#include <vector>
+// [CLI11:public_includes:end]
+
+#include "App.hpp"
+#include "FormatterFwd.hpp"
+
+namespace CLI {
+// [CLI11:formatter_hpp:verbatim]
+
+inline std::string
+Formatter::make_group(std::string group, bool is_positional, std::vector<const Option *> opts) const {
+ std::stringstream out;
+
+ out << "\n" << group << ":\n";
+ for(const Option *opt : opts) {
+ out << make_option(opt, is_positional);
+ }
+
+ return out.str();
+}
+
+inline std::string Formatter::make_positionals(const App *app) const {
+ std::vector<const Option *> opts =
+ app->get_options([](const Option *opt) { return !opt->get_group().empty() && opt->get_positional(); });
+
+ if(opts.empty())
+ return std::string();
+
+ return make_group(get_label("Positionals"), true, opts);
+}
+
+inline std::string Formatter::make_groups(const App *app, AppFormatMode mode) const {
+ std::stringstream out;
+ std::vector<std::string> groups = app->get_groups();
+
+ // Options
+ for(const std::string &group : groups) {
+ std::vector<const Option *> opts = app->get_options([app, mode, &group](const Option *opt) {
+ return opt->get_group() == group // Must be in the right group
+ && opt->nonpositional() // Must not be a positional
+ && (mode != AppFormatMode::Sub // If mode is Sub, then
+ || (app->get_help_ptr() != opt // Ignore help pointer
+ && app->get_help_all_ptr() != opt)); // Ignore help all pointer
+ });
+ if(!group.empty() && !opts.empty()) {
+ out << make_group(group, false, opts);
+
+ if(group != groups.back())
+ out << "\n";
+ }
+ }
+
+ return out.str();
+}
+
+inline std::string Formatter::make_description(const App *app) const {
+ std::string desc = app->get_description();
+ auto min_options = app->get_require_option_min();
+ auto max_options = app->get_require_option_max();
+ if(app->get_required()) {
+ desc += " REQUIRED ";
+ }
+ if((max_options == min_options) && (min_options > 0)) {
+ if(min_options == 1) {
+ desc += " \n[Exactly 1 of the following options is required]";
+ } else {
+ desc += " \n[Exactly " + std::to_string(min_options) + "options from the following list are required]";
+ }
+ } else if(max_options > 0) {
+ if(min_options > 0) {
+ desc += " \n[Between " + std::to_string(min_options) + " and " + std::to_string(max_options) +
+ " of the follow options are required]";
+ } else {
+ desc += " \n[At most " + std::to_string(max_options) + " of the following options are allowed]";
+ }
+ } else if(min_options > 0) {
+ desc += " \n[At least " + std::to_string(min_options) + " of the following options are required]";
+ }
+ return (!desc.empty()) ? desc + "\n" : std::string{};
+}
+
+inline std::string Formatter::make_usage(const App *app, std::string name) const {
+ std::stringstream out;
+
+ out << get_label("Usage") << ":" << (name.empty() ? "" : " ") << name;
+
+ std::vector<std::string> groups = app->get_groups();
+
+ // Print an Options badge if any options exist
+ std::vector<const Option *> non_pos_options =
+ app->get_options([](const Option *opt) { return opt->nonpositional(); });
+ if(!non_pos_options.empty())
+ out << " [" << get_label("OPTIONS") << "]";
+
+ // Positionals need to be listed here
+ std::vector<const Option *> positionals = app->get_options([](const Option *opt) { return opt->get_positional(); });
+
+ // Print out positionals if any are left
+ if(!positionals.empty()) {
+ // Convert to help names
+ std::vector<std::string> positional_names(positionals.size());
+ std::transform(positionals.begin(), positionals.end(), positional_names.begin(), [this](const Option *opt) {
+ return make_option_usage(opt);
+ });
+
+ out << " " << detail::join(positional_names, " ");
+ }
+
+ // Add a marker if subcommands are expected or optional
+ if(!app->get_subcommands(
+ [](const CLI::App *subc) { return ((!subc->get_disabled()) && (!subc->get_name().empty())); })
+ .empty()) {
+ out << " " << (app->get_require_subcommand_min() == 0 ? "[" : "")
+ << get_label(app->get_require_subcommand_max() < 2 || app->get_require_subcommand_min() > 1 ? "SUBCOMMAND"
+ : "SUBCOMMANDS")
+ << (app->get_require_subcommand_min() == 0 ? "]" : "");
+ }
+
+ out << std::endl;
+
+ return out.str();
+}
+
+inline std::string Formatter::make_footer(const App *app) const {
+ std::string footer = app->get_footer();
+ if(footer.empty()) {
+ return std::string{};
+ }
+ return footer + "\n";
+}
+
+inline std::string Formatter::make_help(const App *app, std::string name, AppFormatMode mode) const {
+
+ // This immediately forwards to the make_expanded method. This is done this way so that subcommands can
+ // have overridden formatters
+ if(mode == AppFormatMode::Sub)
+ return make_expanded(app);
+
+ std::stringstream out;
+ if((app->get_name().empty()) && (app->get_parent() != nullptr)) {
+ if(app->get_group() != "Subcommands") {
+ out << app->get_group() << ':';
+ }
+ }
+
+ out << make_description(app);
+ out << make_usage(app, name);
+ out << make_positionals(app);
+ out << make_groups(app, mode);
+ out << make_subcommands(app, mode);
+ out << '\n' << make_footer(app);
+
+ return out.str();
+}
+
+inline std::string Formatter::make_subcommands(const App *app, AppFormatMode mode) const {
+ std::stringstream out;
+
+ std::vector<const App *> subcommands = app->get_subcommands({});
+
+ // Make a list in definition order of the groups seen
+ std::vector<std::string> subcmd_groups_seen;
+ for(const App *com : subcommands) {
+ if(com->get_name().empty()) {
+ if(!com->get_group().empty()) {
+ out << make_expanded(com);
+ }
+ continue;
+ }
+ std::string group_key = com->get_group();
+ if(!group_key.empty() &&
+ std::find_if(subcmd_groups_seen.begin(), subcmd_groups_seen.end(), [&group_key](std::string a) {
+ return detail::to_lower(a) == detail::to_lower(group_key);
+ }) == subcmd_groups_seen.end())
+ subcmd_groups_seen.push_back(group_key);
+ }
+
+ // For each group, filter out and print subcommands
+ for(const std::string &group : subcmd_groups_seen) {
+ out << "\n" << group << ":\n";
+ std::vector<const App *> subcommands_group = app->get_subcommands(
+ [&group](const App *sub_app) { return detail::to_lower(sub_app->get_group()) == detail::to_lower(group); });
+ for(const App *new_com : subcommands_group) {
+ if(new_com->get_name().empty())
+ continue;
+ if(mode != AppFormatMode::All) {
+ out << make_subcommand(new_com);
+ } else {
+ out << new_com->help(new_com->get_name(), AppFormatMode::Sub);
+ out << "\n";
+ }
+ }
+ }
+
+ return out.str();
+}
+
+inline std::string Formatter::make_subcommand(const App *sub) const {
+ std::stringstream out;
+ detail::format_help(out, sub->get_display_name(true), sub->get_description(), column_width_);
+ return out.str();
+}
+
+inline std::string Formatter::make_expanded(const App *sub) const {
+ std::stringstream out;
+ out << sub->get_display_name(true) << "\n";
+
+ out << make_description(sub);
+ if(sub->get_name().empty() && !sub->get_aliases().empty()) {
+ detail::format_aliases(out, sub->get_aliases(), column_width_ + 2);
+ }
+ out << make_positionals(sub);
+ out << make_groups(sub, AppFormatMode::Sub);
+ out << make_subcommands(sub, AppFormatMode::Sub);
+
+ // Drop blank spaces
+ std::string tmp = detail::find_and_replace(out.str(), "\n\n", "\n");
+ tmp = tmp.substr(0, tmp.size() - 1); // Remove the final '\n'
+
+ // Indent all but the first line (the name)
+ return detail::find_and_replace(tmp, "\n", "\n ") + "\n";
+}
+
+inline std::string Formatter::make_option_name(const Option *opt, bool is_positional) const {
+ if(is_positional)
+ return opt->get_name(true, false);
+
+ return opt->get_name(false, true);
+}
+
+inline std::string Formatter::make_option_opts(const Option *opt) const {
+ std::stringstream out;
+
+ if(!opt->get_option_text().empty()) {
+ out << " " << opt->get_option_text();
+ } else {
+ if(opt->get_type_size() != 0) {
+ if(!opt->get_type_name().empty())
+ out << " " << get_label(opt->get_type_name());
+ if(!opt->get_default_str().empty())
+ out << " [" << opt->get_default_str() << "] ";
+ if(opt->get_expected_max() == detail::expected_max_vector_size)
+ out << " ...";
+ else if(opt->get_expected_min() > 1)
+ out << " x " << opt->get_expected();
+
+ if(opt->get_required())
+ out << " " << get_label("REQUIRED");
+ }
+ if(!opt->get_envname().empty())
+ out << " (" << get_label("Env") << ":" << opt->get_envname() << ")";
+ if(!opt->get_needs().empty()) {
+ out << " " << get_label("Needs") << ":";
+ for(const Option *op : opt->get_needs())
+ out << " " << op->get_name();
+ }
+ if(!opt->get_excludes().empty()) {
+ out << " " << get_label("Excludes") << ":";
+ for(const Option *op : opt->get_excludes())
+ out << " " << op->get_name();
+ }
+ }
+ return out.str();
+}
+
+inline std::string Formatter::make_option_desc(const Option *opt) const { return opt->get_description(); }
+
+inline std::string Formatter::make_option_usage(const Option *opt) const {
+ // Note that these are positionals usages
+ std::stringstream out;
+ out << make_option_name(opt, true);
+ if(opt->get_expected_max() >= detail::expected_max_vector_size)
+ out << "...";
+ else if(opt->get_expected_max() > 1)
+ out << "(" << opt->get_expected() << "x)";
+
+ return opt->get_required() ? out.str() : "[" + out.str() + "]";
+}
+
+// [CLI11:formatter_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/FormatterFwd.hpp b/src/third-party/CLI/FormatterFwd.hpp
new file mode 100644
index 0000000..f71b3bb
--- /dev/null
+++ b/src/third-party/CLI/FormatterFwd.hpp
@@ -0,0 +1,184 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+// [CLI11:public_includes:end]
+
+#include "StringTools.hpp"
+
+namespace CLI {
+// [CLI11:formatter_fwd_hpp:verbatim]
+
+class Option;
+class App;
+
+/// This enum signifies the type of help requested
+///
+/// This is passed in by App; all user classes must accept this as
+/// the second argument.
+
+enum class AppFormatMode {
+ Normal, ///< The normal, detailed help
+ All, ///< A fully expanded help
+ Sub, ///< Used when printed as part of expanded subcommand
+};
+
+/// This is the minimum requirements to run a formatter.
+///
+/// A user can subclass this is if they do not care at all
+/// about the structure in CLI::Formatter.
+class FormatterBase {
+ protected:
+ /// @name Options
+ ///@{
+
+ /// The width of the first column
+ std::size_t column_width_{30};
+
+ /// @brief The required help printout labels (user changeable)
+ /// Values are Needs, Excludes, etc.
+ std::map<std::string, std::string> labels_{};
+
+ ///@}
+ /// @name Basic
+ ///@{
+
+ public:
+ FormatterBase() = default;
+ FormatterBase(const FormatterBase &) = default;
+ FormatterBase(FormatterBase &&) = default;
+
+ /// Adding a destructor in this form to work around bug in GCC 4.7
+ virtual ~FormatterBase() noexcept {} // NOLINT(modernize-use-equals-default)
+
+ /// This is the key method that puts together help
+ virtual std::string make_help(const App *, std::string, AppFormatMode) const = 0;
+
+ ///@}
+ /// @name Setters
+ ///@{
+
+ /// Set the "REQUIRED" label
+ void label(std::string key, std::string val) { labels_[key] = val; }
+
+ /// Set the column width
+ void column_width(std::size_t val) { column_width_ = val; }
+
+ ///@}
+ /// @name Getters
+ ///@{
+
+ /// Get the current value of a name (REQUIRED, etc.)
+ std::string get_label(std::string key) const {
+ if(labels_.find(key) == labels_.end())
+ return key;
+ else
+ return labels_.at(key);
+ }
+
+ /// Get the current column width
+ std::size_t get_column_width() const { return column_width_; }
+
+ ///@}
+};
+
+/// This is a specialty override for lambda functions
+class FormatterLambda final : public FormatterBase {
+ using funct_t = std::function<std::string(const App *, std::string, AppFormatMode)>;
+
+ /// The lambda to hold and run
+ funct_t lambda_;
+
+ public:
+ /// Create a FormatterLambda with a lambda function
+ explicit FormatterLambda(funct_t funct) : lambda_(std::move(funct)) {}
+
+ /// Adding a destructor (mostly to make GCC 4.7 happy)
+ ~FormatterLambda() noexcept override {} // NOLINT(modernize-use-equals-default)
+
+ /// This will simply call the lambda function
+ std::string make_help(const App *app, std::string name, AppFormatMode mode) const override {
+ return lambda_(app, name, mode);
+ }
+};
+
+/// This is the default Formatter for CLI11. It pretty prints help output, and is broken into quite a few
+/// overridable methods, to be highly customizable with minimal effort.
+class Formatter : public FormatterBase {
+ public:
+ Formatter() = default;
+ Formatter(const Formatter &) = default;
+ Formatter(Formatter &&) = default;
+
+ /// @name Overridables
+ ///@{
+
+ /// This prints out a group of options with title
+ ///
+ virtual std::string make_group(std::string group, bool is_positional, std::vector<const Option *> opts) const;
+
+ /// This prints out just the positionals "group"
+ virtual std::string make_positionals(const App *app) const;
+
+ /// This prints out all the groups of options
+ std::string make_groups(const App *app, AppFormatMode mode) const;
+
+ /// This prints out all the subcommands
+ virtual std::string make_subcommands(const App *app, AppFormatMode mode) const;
+
+ /// This prints out a subcommand
+ virtual std::string make_subcommand(const App *sub) const;
+
+ /// This prints out a subcommand in help-all
+ virtual std::string make_expanded(const App *sub) const;
+
+ /// This prints out all the groups of options
+ virtual std::string make_footer(const App *app) const;
+
+ /// This displays the description line
+ virtual std::string make_description(const App *app) const;
+
+ /// This displays the usage line
+ virtual std::string make_usage(const App *app, std::string name) const;
+
+ /// This puts everything together
+ std::string make_help(const App * /*app*/, std::string, AppFormatMode) const override;
+
+ ///@}
+ /// @name Options
+ ///@{
+
+ /// This prints out an option help line, either positional or optional form
+ virtual std::string make_option(const Option *opt, bool is_positional) const {
+ std::stringstream out;
+ detail::format_help(
+ out, make_option_name(opt, is_positional) + make_option_opts(opt), make_option_desc(opt), column_width_);
+ return out.str();
+ }
+
+ /// @brief This is the name part of an option, Default: left column
+ virtual std::string make_option_name(const Option *, bool) const;
+
+ /// @brief This is the options part of the name, Default: combined into left column
+ virtual std::string make_option_opts(const Option *) const;
+
+ /// @brief This is the description. Default: Right column, on new line if left column too large
+ virtual std::string make_option_desc(const Option *) const;
+
+ /// @brief This is used to print the name on the USAGE line
+ virtual std::string make_option_usage(const Option *opt) const;
+
+ ///@}
+};
+
+// [CLI11:formatter_fwd_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/Macros.hpp b/src/third-party/CLI/Macros.hpp
new file mode 100644
index 0000000..8d7663b
--- /dev/null
+++ b/src/third-party/CLI/Macros.hpp
@@ -0,0 +1,60 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:macros_hpp:verbatim]
+
+// The following version macro is very similar to the one in pybind11
+#if !(defined(_MSC_VER) && __cplusplus == 199711L) && !defined(__INTEL_COMPILER)
+#if __cplusplus >= 201402L
+#define CLI11_CPP14
+#if __cplusplus >= 201703L
+#define CLI11_CPP17
+#if __cplusplus > 201703L
+#define CLI11_CPP20
+#endif
+#endif
+#endif
+#elif defined(_MSC_VER) && __cplusplus == 199711L
+// MSVC sets _MSVC_LANG rather than __cplusplus (supposedly until the standard is fully implemented)
+// Unless you use the /Zc:__cplusplus flag on Visual Studio 2017 15.7 Preview 3 or newer
+#if _MSVC_LANG >= 201402L
+#define CLI11_CPP14
+#if _MSVC_LANG > 201402L && _MSC_VER >= 1910
+#define CLI11_CPP17
+#if _MSVC_LANG > 201703L && _MSC_VER >= 1910
+#define CLI11_CPP20
+#endif
+#endif
+#endif
+#endif
+
+#if defined(CLI11_CPP14)
+#define CLI11_DEPRECATED(reason) [[deprecated(reason)]]
+#elif defined(_MSC_VER)
+#define CLI11_DEPRECATED(reason) __declspec(deprecated(reason))
+#else
+#define CLI11_DEPRECATED(reason) __attribute__((deprecated(reason)))
+#endif
+
+/** detection of rtti */
+#ifndef CLI11_USE_STATIC_RTTI
+#if(defined(_HAS_STATIC_RTTI) && _HAS_STATIC_RTTI)
+#define CLI11_USE_STATIC_RTTI 1
+#elif defined(__cpp_rtti)
+#if(defined(_CPPRTTI) && _CPPRTTI == 0)
+#define CLI11_USE_STATIC_RTTI 1
+#else
+#define CLI11_USE_STATIC_RTTI 0
+#endif
+#elif(defined(__GCC_RTTI) && __GXX_RTTI)
+#define CLI11_USE_STATIC_RTTI 0
+#else
+#define CLI11_USE_STATIC_RTTI 1
+#endif
+#endif
+// [CLI11:macros_hpp:end]
diff --git a/src/third-party/CLI/Option.hpp b/src/third-party/CLI/Option.hpp
new file mode 100644
index 0000000..b075bbc
--- /dev/null
+++ b/src/third-party/CLI/Option.hpp
@@ -0,0 +1,1362 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <algorithm>
+#include <functional>
+#include <memory>
+#include <set>
+#include <string>
+#include <tuple>
+#include <utility>
+#include <vector>
+// [CLI11:public_includes:end]
+
+#include "Error.hpp"
+#include "Macros.hpp"
+#include "Split.hpp"
+#include "StringTools.hpp"
+#include "Validators.hpp"
+
+namespace CLI {
+// [CLI11:option_hpp:verbatim]
+
+using results_t = std::vector<std::string>;
+/// callback function definition
+using callback_t = std::function<bool(const results_t &)>;
+
+class Option;
+class App;
+
+using Option_p = std::unique_ptr<Option>;
+/// Enumeration of the multiOption Policy selection
+enum class MultiOptionPolicy : char {
+ Throw, //!< Throw an error if any extra arguments were given
+ TakeLast, //!< take only the last Expected number of arguments
+ TakeFirst, //!< take only the first Expected number of arguments
+ Join, //!< merge all the arguments together into a single string via the delimiter character default('\n')
+ TakeAll, //!< just get all the passed argument regardless
+ Sum //!< sum all the arguments together if numerical or concatenate directly without delimiter
+};
+
+/// This is the CRTP base class for Option and OptionDefaults. It was designed this way
+/// to share parts of the class; an OptionDefaults can copy to an Option.
+template <typename CRTP> class OptionBase {
+ friend App;
+
+ protected:
+ /// The group membership
+ std::string group_ = std::string("Options");
+
+ /// True if this is a required option
+ bool required_{false};
+
+ /// Ignore the case when matching (option, not value)
+ bool ignore_case_{false};
+
+ /// Ignore underscores when matching (option, not value)
+ bool ignore_underscore_{false};
+
+ /// Allow this option to be given in a configuration file
+ bool configurable_{true};
+
+ /// Disable overriding flag values with '=value'
+ bool disable_flag_override_{false};
+
+ /// Specify a delimiter character for vector arguments
+ char delimiter_{'\0'};
+
+ /// Automatically capture default value
+ bool always_capture_default_{false};
+
+ /// Policy for handling multiple arguments beyond the expected Max
+ MultiOptionPolicy multi_option_policy_{MultiOptionPolicy::Throw};
+
+ /// Copy the contents to another similar class (one based on OptionBase)
+ template <typename T> void copy_to(T *other) const {
+ other->group(group_);
+ other->required(required_);
+ other->ignore_case(ignore_case_);
+ other->ignore_underscore(ignore_underscore_);
+ other->configurable(configurable_);
+ other->disable_flag_override(disable_flag_override_);
+ other->delimiter(delimiter_);
+ other->always_capture_default(always_capture_default_);
+ other->multi_option_policy(multi_option_policy_);
+ }
+
+ public:
+ // setters
+
+ /// Changes the group membership
+ CRTP *group(const std::string &name) {
+ if(!detail::valid_alias_name_string(name)) {
+ throw IncorrectConstruction("Group names may not contain newlines or null characters");
+ }
+ group_ = name;
+ return static_cast<CRTP *>(this);
+ }
+
+ /// Set the option as required
+ CRTP *required(bool value = true) {
+ required_ = value;
+ return static_cast<CRTP *>(this);
+ }
+
+ /// Support Plumbum term
+ CRTP *mandatory(bool value = true) { return required(value); }
+
+ CRTP *always_capture_default(bool value = true) {
+ always_capture_default_ = value;
+ return static_cast<CRTP *>(this);
+ }
+
+ // Getters
+
+ /// Get the group of this option
+ const std::string &get_group() const { return group_; }
+
+ /// True if this is a required option
+ bool get_required() const { return required_; }
+
+ /// The status of ignore case
+ bool get_ignore_case() const { return ignore_case_; }
+
+ /// The status of ignore_underscore
+ bool get_ignore_underscore() const { return ignore_underscore_; }
+
+ /// The status of configurable
+ bool get_configurable() const { return configurable_; }
+
+ /// The status of configurable
+ bool get_disable_flag_override() const { return disable_flag_override_; }
+
+ /// Get the current delimiter char
+ char get_delimiter() const { return delimiter_; }
+
+ /// Return true if this will automatically capture the default value for help printing
+ bool get_always_capture_default() const { return always_capture_default_; }
+
+ /// The status of the multi option policy
+ MultiOptionPolicy get_multi_option_policy() const { return multi_option_policy_; }
+
+ // Shortcuts for multi option policy
+
+ /// Set the multi option policy to take last
+ CRTP *take_last() {
+ auto self = static_cast<CRTP *>(this);
+ self->multi_option_policy(MultiOptionPolicy::TakeLast);
+ return self;
+ }
+
+ /// Set the multi option policy to take last
+ CRTP *take_first() {
+ auto self = static_cast<CRTP *>(this);
+ self->multi_option_policy(MultiOptionPolicy::TakeFirst);
+ return self;
+ }
+
+ /// Set the multi option policy to take all arguments
+ CRTP *take_all() {
+ auto self = static_cast<CRTP *>(this);
+ self->multi_option_policy(MultiOptionPolicy::TakeAll);
+ return self;
+ }
+
+ /// Set the multi option policy to join
+ CRTP *join() {
+ auto self = static_cast<CRTP *>(this);
+ self->multi_option_policy(MultiOptionPolicy::Join);
+ return self;
+ }
+
+ /// Set the multi option policy to join with a specific delimiter
+ CRTP *join(char delim) {
+ auto self = static_cast<CRTP *>(this);
+ self->delimiter_ = delim;
+ self->multi_option_policy(MultiOptionPolicy::Join);
+ return self;
+ }
+
+ /// Allow in a configuration file
+ CRTP *configurable(bool value = true) {
+ configurable_ = value;
+ return static_cast<CRTP *>(this);
+ }
+
+ /// Allow in a configuration file
+ CRTP *delimiter(char value = '\0') {
+ delimiter_ = value;
+ return static_cast<CRTP *>(this);
+ }
+};
+
+/// This is a version of OptionBase that only supports setting values,
+/// for defaults. It is stored as the default option in an App.
+class OptionDefaults : public OptionBase<OptionDefaults> {
+ public:
+ OptionDefaults() = default;
+
+ // Methods here need a different implementation if they are Option vs. OptionDefault
+
+ /// Take the last argument if given multiple times
+ OptionDefaults *multi_option_policy(MultiOptionPolicy value = MultiOptionPolicy::Throw) {
+ multi_option_policy_ = value;
+ return this;
+ }
+
+ /// Ignore the case of the option name
+ OptionDefaults *ignore_case(bool value = true) {
+ ignore_case_ = value;
+ return this;
+ }
+
+ /// Ignore underscores in the option name
+ OptionDefaults *ignore_underscore(bool value = true) {
+ ignore_underscore_ = value;
+ return this;
+ }
+
+ /// Disable overriding flag values with an '=<value>' segment
+ OptionDefaults *disable_flag_override(bool value = true) {
+ disable_flag_override_ = value;
+ return this;
+ }
+
+ /// set a delimiter character to split up single arguments to treat as multiple inputs
+ OptionDefaults *delimiter(char value = '\0') {
+ delimiter_ = value;
+ return this;
+ }
+};
+
+class Option : public OptionBase<Option> {
+ friend App;
+
+ protected:
+ /// @name Names
+ ///@{
+
+ /// A list of the short names (`-a`) without the leading dashes
+ std::vector<std::string> snames_{};
+
+ /// A list of the long names (`--long`) without the leading dashes
+ std::vector<std::string> lnames_{};
+
+ /// A list of the flag names with the appropriate default value, the first part of the pair should be duplicates of
+ /// what is in snames or lnames but will trigger a particular response on a flag
+ std::vector<std::pair<std::string, std::string>> default_flag_values_{};
+
+ /// a list of flag names with specified default values;
+ std::vector<std::string> fnames_{};
+
+ /// A positional name
+ std::string pname_{};
+
+ /// If given, check the environment for this option
+ std::string envname_{};
+
+ ///@}
+ /// @name Help
+ ///@{
+
+ /// The description for help strings
+ std::string description_{};
+
+ /// A human readable default value, either manually set, captured, or captured by default
+ std::string default_str_{};
+
+ /// If given, replace the text that describes the option type and usage in the help text
+ std::string option_text_{};
+
+ /// A human readable type value, set when App creates this
+ ///
+ /// This is a lambda function so "types" can be dynamic, such as when a set prints its contents.
+ std::function<std::string()> type_name_{[]() { return std::string(); }};
+
+ /// Run this function to capture a default (ignore if empty)
+ std::function<std::string()> default_function_{};
+
+ ///@}
+ /// @name Configuration
+ ///@{
+
+ /// The number of arguments that make up one option. max is the nominal type size, min is the minimum number of
+ /// strings
+ int type_size_max_{1};
+ /// The minimum number of arguments an option should be expecting
+ int type_size_min_{1};
+
+ /// The minimum number of expected values
+ int expected_min_{1};
+ /// The maximum number of expected values
+ int expected_max_{1};
+
+ /// A list of Validators to run on each value parsed
+ std::vector<Validator> validators_{};
+
+ /// A list of options that are required with this option
+ std::set<Option *> needs_{};
+
+ /// A list of options that are excluded with this option
+ std::set<Option *> excludes_{};
+
+ ///@}
+ /// @name Other
+ ///@{
+
+ /// link back up to the parent App for fallthrough
+ App *parent_{nullptr};
+
+ /// Options store a callback to do all the work
+ callback_t callback_{};
+
+ ///@}
+ /// @name Parsing results
+ ///@{
+
+ /// complete Results of parsing
+ results_t results_{};
+ /// results after reduction
+ results_t proc_results_{};
+ /// enumeration for the option state machine
+ enum class option_state : char {
+ parsing = 0, //!< The option is currently collecting parsed results
+ validated = 2, //!< the results have been validated
+ reduced = 4, //!< a subset of results has been generated
+ callback_run = 6, //!< the callback has been executed
+ };
+ /// Whether the callback has run (needed for INI parsing)
+ option_state current_option_state_{option_state::parsing};
+ /// Specify that extra args beyond type_size_max should be allowed
+ bool allow_extra_args_{false};
+ /// Specify that the option should act like a flag vs regular option
+ bool flag_like_{false};
+ /// Control option to run the callback to set the default
+ bool run_callback_for_default_{false};
+ /// flag indicating a separator needs to be injected after each argument call
+ bool inject_separator_{false};
+ /// flag indicating that the option should trigger the validation and callback chain on each result when loaded
+ bool trigger_on_result_{false};
+ /// flag indicating that the option should force the callback regardless if any results present
+ bool force_callback_{false};
+ ///@}
+
+ /// Making an option by hand is not defined, it must be made by the App class
+ Option(std::string option_name, std::string option_description, callback_t callback, App *parent)
+ : description_(std::move(option_description)), parent_(parent), callback_(std::move(callback)) {
+ std::tie(snames_, lnames_, pname_) = detail::get_names(detail::split_names(option_name));
+ }
+
+ public:
+ /// @name Basic
+ ///@{
+
+ Option(const Option &) = delete;
+ Option &operator=(const Option &) = delete;
+
+ /// Count the total number of times an option was passed
+ std::size_t count() const { return results_.size(); }
+
+ /// True if the option was not passed
+ bool empty() const { return results_.empty(); }
+
+ /// This bool operator returns true if any arguments were passed or the option callback is forced
+ explicit operator bool() const { return !empty() || force_callback_; }
+
+ /// Clear the parsed results (mostly for testing)
+ void clear() {
+ results_.clear();
+ current_option_state_ = option_state::parsing;
+ }
+
+ ///@}
+ /// @name Setting options
+ ///@{
+
+ /// Set the number of expected arguments
+ Option *expected(int value) {
+ if(value < 0) {
+ expected_min_ = -value;
+ if(expected_max_ < expected_min_) {
+ expected_max_ = expected_min_;
+ }
+ allow_extra_args_ = true;
+ flag_like_ = false;
+ } else if(value == detail::expected_max_vector_size) {
+ expected_min_ = 1;
+ expected_max_ = detail::expected_max_vector_size;
+ allow_extra_args_ = true;
+ flag_like_ = false;
+ } else {
+ expected_min_ = value;
+ expected_max_ = value;
+ flag_like_ = (expected_min_ == 0);
+ }
+ return this;
+ }
+
+ /// Set the range of expected arguments
+ Option *expected(int value_min, int value_max) {
+ if(value_min < 0) {
+ value_min = -value_min;
+ }
+
+ if(value_max < 0) {
+ value_max = detail::expected_max_vector_size;
+ }
+ if(value_max < value_min) {
+ expected_min_ = value_max;
+ expected_max_ = value_min;
+ } else {
+ expected_max_ = value_max;
+ expected_min_ = value_min;
+ }
+
+ return this;
+ }
+ /// Set the value of allow_extra_args which allows extra value arguments on the flag or option to be included
+ /// with each instance
+ Option *allow_extra_args(bool value = true) {
+ allow_extra_args_ = value;
+ return this;
+ }
+ /// Get the current value of allow extra args
+ bool get_allow_extra_args() const { return allow_extra_args_; }
+ /// Set the value of trigger_on_parse which specifies that the option callback should be triggered on every parse
+ Option *trigger_on_parse(bool value = true) {
+ trigger_on_result_ = value;
+ return this;
+ }
+ /// The status of trigger on parse
+ bool get_trigger_on_parse() const { return trigger_on_result_; }
+
+ /// Set the value of force_callback
+ Option *force_callback(bool value = true) {
+ force_callback_ = value;
+ return this;
+ }
+ /// The status of force_callback
+ bool get_force_callback() const { return force_callback_; }
+
+ /// Set the value of run_callback_for_default which controls whether the callback function should be called to set
+ /// the default This is controlled automatically but could be manipulated by the user.
+ Option *run_callback_for_default(bool value = true) {
+ run_callback_for_default_ = value;
+ return this;
+ }
+ /// Get the current value of run_callback_for_default
+ bool get_run_callback_for_default() const { return run_callback_for_default_; }
+
+ /// Adds a Validator with a built in type name
+ Option *check(Validator validator, const std::string &validator_name = "") {
+ validator.non_modifying();
+ validators_.push_back(std::move(validator));
+ if(!validator_name.empty())
+ validators_.back().name(validator_name);
+ return this;
+ }
+
+ /// Adds a Validator. Takes a const string& and returns an error message (empty if conversion/check is okay).
+ Option *check(std::function<std::string(const std::string &)> Validator,
+ std::string Validator_description = "",
+ std::string Validator_name = "") {
+ validators_.emplace_back(Validator, std::move(Validator_description), std::move(Validator_name));
+ validators_.back().non_modifying();
+ return this;
+ }
+
+ /// Adds a transforming Validator with a built in type name
+ Option *transform(Validator Validator, const std::string &Validator_name = "") {
+ validators_.insert(validators_.begin(), std::move(Validator));
+ if(!Validator_name.empty())
+ validators_.front().name(Validator_name);
+ return this;
+ }
+
+ /// Adds a Validator-like function that can change result
+ Option *transform(const std::function<std::string(std::string)> &func,
+ std::string transform_description = "",
+ std::string transform_name = "") {
+ validators_.insert(validators_.begin(),
+ Validator(
+ [func](std::string &val) {
+ val = func(val);
+ return std::string{};
+ },
+ std::move(transform_description),
+ std::move(transform_name)));
+
+ return this;
+ }
+
+ /// Adds a user supplied function to run on each item passed in (communicate though lambda capture)
+ Option *each(const std::function<void(std::string)> &func) {
+ validators_.emplace_back(
+ [func](std::string &inout) {
+ func(inout);
+ return std::string{};
+ },
+ std::string{});
+ return this;
+ }
+ /// Get a named Validator
+ Validator *get_validator(const std::string &Validator_name = "") {
+ for(auto &Validator : validators_) {
+ if(Validator_name == Validator.get_name()) {
+ return &Validator;
+ }
+ }
+ if((Validator_name.empty()) && (!validators_.empty())) {
+ return &(validators_.front());
+ }
+ throw OptionNotFound(std::string{"Validator "} + Validator_name + " Not Found");
+ }
+
+ /// Get a Validator by index NOTE: this may not be the order of definition
+ Validator *get_validator(int index) {
+ // This is an signed int so that it is not equivalent to a pointer.
+ if(index >= 0 && index < static_cast<int>(validators_.size())) {
+ return &(validators_[static_cast<decltype(validators_)::size_type>(index)]);
+ }
+ throw OptionNotFound("Validator index is not valid");
+ }
+
+ /// Sets required options
+ Option *needs(Option *opt) {
+ if(opt != this) {
+ needs_.insert(opt);
+ }
+ return this;
+ }
+
+ /// Can find a string if needed
+ template <typename T = App> Option *needs(std::string opt_name) {
+ auto opt = static_cast<T *>(parent_)->get_option_no_throw(opt_name);
+ if(opt == nullptr) {
+ throw IncorrectConstruction::MissingOption(opt_name);
+ }
+ return needs(opt);
+ }
+
+ /// Any number supported, any mix of string and Opt
+ template <typename A, typename B, typename... ARG> Option *needs(A opt, B opt1, ARG... args) {
+ needs(opt);
+ return needs(opt1, args...);
+ }
+
+ /// Remove needs link from an option. Returns true if the option really was in the needs list.
+ bool remove_needs(Option *opt) {
+ auto iterator = std::find(std::begin(needs_), std::end(needs_), opt);
+
+ if(iterator == std::end(needs_)) {
+ return false;
+ }
+ needs_.erase(iterator);
+ return true;
+ }
+
+ /// Sets excluded options
+ Option *excludes(Option *opt) {
+ if(opt == this) {
+ throw(IncorrectConstruction("and option cannot exclude itself"));
+ }
+ excludes_.insert(opt);
+
+ // Help text should be symmetric - excluding a should exclude b
+ opt->excludes_.insert(this);
+
+ // Ignoring the insert return value, excluding twice is now allowed.
+ // (Mostly to allow both directions to be excluded by user, even though the library does it for you.)
+
+ return this;
+ }
+
+ /// Can find a string if needed
+ template <typename T = App> Option *excludes(std::string opt_name) {
+ auto opt = static_cast<T *>(parent_)->get_option_no_throw(opt_name);
+ if(opt == nullptr) {
+ throw IncorrectConstruction::MissingOption(opt_name);
+ }
+ return excludes(opt);
+ }
+
+ /// Any number supported, any mix of string and Opt
+ template <typename A, typename B, typename... ARG> Option *excludes(A opt, B opt1, ARG... args) {
+ excludes(opt);
+ return excludes(opt1, args...);
+ }
+
+ /// Remove needs link from an option. Returns true if the option really was in the needs list.
+ bool remove_excludes(Option *opt) {
+ auto iterator = std::find(std::begin(excludes_), std::end(excludes_), opt);
+
+ if(iterator == std::end(excludes_)) {
+ return false;
+ }
+ excludes_.erase(iterator);
+ return true;
+ }
+
+ /// Sets environment variable to read if no option given
+ Option *envname(std::string name) {
+ envname_ = std::move(name);
+ return this;
+ }
+
+ /// Ignore case
+ ///
+ /// The template hides the fact that we don't have the definition of App yet.
+ /// You are never expected to add an argument to the template here.
+ template <typename T = App> Option *ignore_case(bool value = true) {
+ if(!ignore_case_ && value) {
+ ignore_case_ = value;
+ auto *parent = static_cast<T *>(parent_);
+ for(const Option_p &opt : parent->options_) {
+ if(opt.get() == this) {
+ continue;
+ }
+ auto &omatch = opt->matching_name(*this);
+ if(!omatch.empty()) {
+ ignore_case_ = false;
+ throw OptionAlreadyAdded("adding ignore case caused a name conflict with " + omatch);
+ }
+ }
+ } else {
+ ignore_case_ = value;
+ }
+ return this;
+ }
+
+ /// Ignore underscores in the option names
+ ///
+ /// The template hides the fact that we don't have the definition of App yet.
+ /// You are never expected to add an argument to the template here.
+ template <typename T = App> Option *ignore_underscore(bool value = true) {
+
+ if(!ignore_underscore_ && value) {
+ ignore_underscore_ = value;
+ auto *parent = static_cast<T *>(parent_);
+ for(const Option_p &opt : parent->options_) {
+ if(opt.get() == this) {
+ continue;
+ }
+ auto &omatch = opt->matching_name(*this);
+ if(!omatch.empty()) {
+ ignore_underscore_ = false;
+ throw OptionAlreadyAdded("adding ignore underscore caused a name conflict with " + omatch);
+ }
+ }
+ } else {
+ ignore_underscore_ = value;
+ }
+ return this;
+ }
+
+ /// Take the last argument if given multiple times (or another policy)
+ Option *multi_option_policy(MultiOptionPolicy value = MultiOptionPolicy::Throw) {
+ if(value != multi_option_policy_) {
+ if(multi_option_policy_ == MultiOptionPolicy::Throw && expected_max_ == detail::expected_max_vector_size &&
+ expected_min_ > 1) { // this bizarre condition is to maintain backwards compatibility
+ // with the previous behavior of expected_ with vectors
+ expected_max_ = expected_min_;
+ }
+ multi_option_policy_ = value;
+ current_option_state_ = option_state::parsing;
+ }
+ return this;
+ }
+
+ /// Disable flag overrides values, e.g. --flag=<value> is not allowed
+ Option *disable_flag_override(bool value = true) {
+ disable_flag_override_ = value;
+ return this;
+ }
+ ///@}
+ /// @name Accessors
+ ///@{
+
+ /// The number of arguments the option expects
+ int get_type_size() const { return type_size_min_; }
+
+ /// The minimum number of arguments the option expects
+ int get_type_size_min() const { return type_size_min_; }
+ /// The maximum number of arguments the option expects
+ int get_type_size_max() const { return type_size_max_; }
+
+ /// Return the inject_separator flag
+ int get_inject_separator() const { return inject_separator_; }
+
+ /// The environment variable associated to this value
+ std::string get_envname() const { return envname_; }
+
+ /// The set of options needed
+ std::set<Option *> get_needs() const { return needs_; }
+
+ /// The set of options excluded
+ std::set<Option *> get_excludes() const { return excludes_; }
+
+ /// The default value (for help printing)
+ std::string get_default_str() const { return default_str_; }
+
+ /// Get the callback function
+ callback_t get_callback() const { return callback_; }
+
+ /// Get the long names
+ const std::vector<std::string> &get_lnames() const { return lnames_; }
+
+ /// Get the short names
+ const std::vector<std::string> &get_snames() const { return snames_; }
+
+ /// Get the flag names with specified default values
+ const std::vector<std::string> &get_fnames() const { return fnames_; }
+ /// Get a single name for the option, first of lname, pname, sname, envname
+ const std::string &get_single_name() const {
+ if(!lnames_.empty()) {
+ return lnames_[0];
+ }
+ if(!pname_.empty()) {
+ return pname_;
+ }
+ if(!snames_.empty()) {
+ return snames_[0];
+ }
+ return envname_;
+ }
+ /// The number of times the option expects to be included
+ int get_expected() const { return expected_min_; }
+
+ /// The number of times the option expects to be included
+ int get_expected_min() const { return expected_min_; }
+ /// The max number of times the option expects to be included
+ int get_expected_max() const { return expected_max_; }
+
+ /// The total min number of expected string values to be used
+ int get_items_expected_min() const { return type_size_min_ * expected_min_; }
+
+ /// Get the maximum number of items expected to be returned and used for the callback
+ int get_items_expected_max() const {
+ int t = type_size_max_;
+ return detail::checked_multiply(t, expected_max_) ? t : detail::expected_max_vector_size;
+ }
+ /// The total min number of expected string values to be used
+ int get_items_expected() const { return get_items_expected_min(); }
+
+ /// True if the argument can be given directly
+ bool get_positional() const { return pname_.length() > 0; }
+
+ /// True if option has at least one non-positional name
+ bool nonpositional() const { return (snames_.size() + lnames_.size()) > 0; }
+
+ /// True if option has description
+ bool has_description() const { return description_.length() > 0; }
+
+ /// Get the description
+ const std::string &get_description() const { return description_; }
+
+ /// Set the description
+ Option *description(std::string option_description) {
+ description_ = std::move(option_description);
+ return this;
+ }
+
+ Option *option_text(std::string text) {
+ option_text_ = std::move(text);
+ return this;
+ }
+
+ const std::string &get_option_text() const { return option_text_; }
+
+ ///@}
+ /// @name Help tools
+ ///@{
+
+ /// \brief Gets a comma separated list of names.
+ /// Will include / prefer the positional name if positional is true.
+ /// If all_options is false, pick just the most descriptive name to show.
+ /// Use `get_name(true)` to get the positional name (replaces `get_pname`)
+ std::string get_name(bool positional = false, ///< Show the positional name
+ bool all_options = false ///< Show every option
+ ) const {
+ if(get_group().empty())
+ return {}; // Hidden
+
+ if(all_options) {
+
+ std::vector<std::string> name_list;
+
+ /// The all list will never include a positional unless asked or that's the only name.
+ if((positional && (!pname_.empty())) || (snames_.empty() && lnames_.empty())) {
+ name_list.push_back(pname_);
+ }
+ if((get_items_expected() == 0) && (!fnames_.empty())) {
+ for(const std::string &sname : snames_) {
+ name_list.push_back("-" + sname);
+ if(check_fname(sname)) {
+ name_list.back() += "{" + get_flag_value(sname, "") + "}";
+ }
+ }
+
+ for(const std::string &lname : lnames_) {
+ name_list.push_back("--" + lname);
+ if(check_fname(lname)) {
+ name_list.back() += "{" + get_flag_value(lname, "") + "}";
+ }
+ }
+ } else {
+ for(const std::string &sname : snames_)
+ name_list.push_back("-" + sname);
+
+ for(const std::string &lname : lnames_)
+ name_list.push_back("--" + lname);
+ }
+
+ return detail::join(name_list);
+ }
+
+ // This returns the positional name no matter what
+ if(positional)
+ return pname_;
+
+ // Prefer long name
+ if(!lnames_.empty())
+ return std::string(2, '-') + lnames_[0];
+
+ // Or short name if no long name
+ if(!snames_.empty())
+ return std::string(1, '-') + snames_[0];
+
+ // If positional is the only name, it's okay to use that
+ return pname_;
+ }
+
+ ///@}
+ /// @name Parser tools
+ ///@{
+
+ /// Process the callback
+ void run_callback() {
+ if(force_callback_ && results_.empty()) {
+ add_result(default_str_);
+ }
+ if(current_option_state_ == option_state::parsing) {
+ _validate_results(results_);
+ current_option_state_ = option_state::validated;
+ }
+
+ if(current_option_state_ < option_state::reduced) {
+ _reduce_results(proc_results_, results_);
+ current_option_state_ = option_state::reduced;
+ }
+ if(current_option_state_ >= option_state::reduced) {
+ current_option_state_ = option_state::callback_run;
+ if(!(callback_)) {
+ return;
+ }
+ const results_t &send_results = proc_results_.empty() ? results_ : proc_results_;
+ bool local_result = callback_(send_results);
+
+ if(!local_result)
+ throw ConversionError(get_name(), results_);
+ }
+ }
+
+ /// If options share any of the same names, find it
+ const std::string &matching_name(const Option &other) const {
+ static const std::string estring;
+ for(const std::string &sname : snames_)
+ if(other.check_sname(sname))
+ return sname;
+ for(const std::string &lname : lnames_)
+ if(other.check_lname(lname))
+ return lname;
+
+ if(ignore_case_ ||
+ ignore_underscore_) { // We need to do the inverse, in case we are ignore_case or ignore underscore
+ for(const std::string &sname : other.snames_)
+ if(check_sname(sname))
+ return sname;
+ for(const std::string &lname : other.lnames_)
+ if(check_lname(lname))
+ return lname;
+ }
+ return estring;
+ }
+ /// If options share any of the same names, they are equal (not counting positional)
+ bool operator==(const Option &other) const { return !matching_name(other).empty(); }
+
+ /// Check a name. Requires "-" or "--" for short / long, supports positional name
+ bool check_name(const std::string &name) const {
+
+ if(name.length() > 2 && name[0] == '-' && name[1] == '-')
+ return check_lname(name.substr(2));
+ if(name.length() > 1 && name.front() == '-')
+ return check_sname(name.substr(1));
+ if(!pname_.empty()) {
+ std::string local_pname = pname_;
+ std::string local_name = name;
+ if(ignore_underscore_) {
+ local_pname = detail::remove_underscore(local_pname);
+ local_name = detail::remove_underscore(local_name);
+ }
+ if(ignore_case_) {
+ local_pname = detail::to_lower(local_pname);
+ local_name = detail::to_lower(local_name);
+ }
+ if(local_name == local_pname) {
+ return true;
+ }
+ }
+
+ if(!envname_.empty()) {
+ // this needs to be the original since envname_ shouldn't match on case insensitivity
+ return (name == envname_);
+ }
+ return false;
+ }
+
+ /// Requires "-" to be removed from string
+ bool check_sname(std::string name) const {
+ return (detail::find_member(std::move(name), snames_, ignore_case_) >= 0);
+ }
+
+ /// Requires "--" to be removed from string
+ bool check_lname(std::string name) const {
+ return (detail::find_member(std::move(name), lnames_, ignore_case_, ignore_underscore_) >= 0);
+ }
+
+ /// Requires "--" to be removed from string
+ bool check_fname(std::string name) const {
+ if(fnames_.empty()) {
+ return false;
+ }
+ return (detail::find_member(std::move(name), fnames_, ignore_case_, ignore_underscore_) >= 0);
+ }
+
+ /// Get the value that goes for a flag, nominally gets the default value but allows for overrides if not
+ /// disabled
+ std::string get_flag_value(const std::string &name, std::string input_value) const {
+ static const std::string trueString{"true"};
+ static const std::string falseString{"false"};
+ static const std::string emptyString{"{}"};
+ // check for disable flag override_
+ if(disable_flag_override_) {
+ if(!((input_value.empty()) || (input_value == emptyString))) {
+ auto default_ind = detail::find_member(name, fnames_, ignore_case_, ignore_underscore_);
+ if(default_ind >= 0) {
+ // We can static cast this to std::size_t because it is more than 0 in this block
+ if(default_flag_values_[static_cast<std::size_t>(default_ind)].second != input_value) {
+ throw(ArgumentMismatch::FlagOverride(name));
+ }
+ } else {
+ if(input_value != trueString) {
+ throw(ArgumentMismatch::FlagOverride(name));
+ }
+ }
+ }
+ }
+ auto ind = detail::find_member(name, fnames_, ignore_case_, ignore_underscore_);
+ if((input_value.empty()) || (input_value == emptyString)) {
+ if(flag_like_) {
+ return (ind < 0) ? trueString : default_flag_values_[static_cast<std::size_t>(ind)].second;
+ } else {
+ return (ind < 0) ? default_str_ : default_flag_values_[static_cast<std::size_t>(ind)].second;
+ }
+ }
+ if(ind < 0) {
+ return input_value;
+ }
+ if(default_flag_values_[static_cast<std::size_t>(ind)].second == falseString) {
+ try {
+ auto val = detail::to_flag_value(input_value);
+ return (val == 1) ? falseString : (val == (-1) ? trueString : std::to_string(-val));
+ } catch(const std::invalid_argument &) {
+ return input_value;
+ }
+ } else {
+ return input_value;
+ }
+ }
+
+ /// Puts a result at the end
+ Option *add_result(std::string s) {
+ _add_result(std::move(s), results_);
+ current_option_state_ = option_state::parsing;
+ return this;
+ }
+
+ /// Puts a result at the end and get a count of the number of arguments actually added
+ Option *add_result(std::string s, int &results_added) {
+ results_added = _add_result(std::move(s), results_);
+ current_option_state_ = option_state::parsing;
+ return this;
+ }
+
+ /// Puts a result at the end
+ Option *add_result(std::vector<std::string> s) {
+ current_option_state_ = option_state::parsing;
+ for(auto &str : s) {
+ _add_result(std::move(str), results_);
+ }
+ return this;
+ }
+
+ /// Get the current complete results set
+ const results_t &results() const { return results_; }
+
+ /// Get a copy of the results
+ results_t reduced_results() const {
+ results_t res = proc_results_.empty() ? results_ : proc_results_;
+ if(current_option_state_ < option_state::reduced) {
+ if(current_option_state_ == option_state::parsing) {
+ res = results_;
+ _validate_results(res);
+ }
+ if(!res.empty()) {
+ results_t extra;
+ _reduce_results(extra, res);
+ if(!extra.empty()) {
+ res = std::move(extra);
+ }
+ }
+ }
+ return res;
+ }
+
+ /// Get the results as a specified type
+ template <typename T> void results(T &output) const {
+ bool retval;
+ if(current_option_state_ >= option_state::reduced || (results_.size() == 1 && validators_.empty())) {
+ const results_t &res = (proc_results_.empty()) ? results_ : proc_results_;
+ retval = detail::lexical_conversion<T, T>(res, output);
+ } else {
+ results_t res;
+ if(results_.empty()) {
+ if(!default_str_.empty()) {
+ // _add_results takes an rvalue only
+ _add_result(std::string(default_str_), res);
+ _validate_results(res);
+ results_t extra;
+ _reduce_results(extra, res);
+ if(!extra.empty()) {
+ res = std::move(extra);
+ }
+ } else {
+ res.emplace_back();
+ }
+ } else {
+ res = reduced_results();
+ }
+ retval = detail::lexical_conversion<T, T>(res, output);
+ }
+ if(!retval) {
+ throw ConversionError(get_name(), results_);
+ }
+ }
+
+ /// Return the results as the specified type
+ template <typename T> T as() const {
+ T output;
+ results(output);
+ return output;
+ }
+
+ /// See if the callback has been run already
+ bool get_callback_run() const { return (current_option_state_ == option_state::callback_run); }
+
+ ///@}
+ /// @name Custom options
+ ///@{
+
+ /// Set the type function to run when displayed on this option
+ Option *type_name_fn(std::function<std::string()> typefun) {
+ type_name_ = std::move(typefun);
+ return this;
+ }
+
+ /// Set a custom option typestring
+ Option *type_name(std::string typeval) {
+ type_name_fn([typeval]() { return typeval; });
+ return this;
+ }
+
+ /// Set a custom option size
+ Option *type_size(int option_type_size) {
+ if(option_type_size < 0) {
+ // this section is included for backwards compatibility
+ type_size_max_ = -option_type_size;
+ type_size_min_ = -option_type_size;
+ expected_max_ = detail::expected_max_vector_size;
+ } else {
+ type_size_max_ = option_type_size;
+ if(type_size_max_ < detail::expected_max_vector_size) {
+ type_size_min_ = option_type_size;
+ } else {
+ inject_separator_ = true;
+ }
+ if(type_size_max_ == 0)
+ required_ = false;
+ }
+ return this;
+ }
+ /// Set a custom option type size range
+ Option *type_size(int option_type_size_min, int option_type_size_max) {
+ if(option_type_size_min < 0 || option_type_size_max < 0) {
+ // this section is included for backwards compatibility
+ expected_max_ = detail::expected_max_vector_size;
+ option_type_size_min = (std::abs)(option_type_size_min);
+ option_type_size_max = (std::abs)(option_type_size_max);
+ }
+
+ if(option_type_size_min > option_type_size_max) {
+ type_size_max_ = option_type_size_min;
+ type_size_min_ = option_type_size_max;
+ } else {
+ type_size_min_ = option_type_size_min;
+ type_size_max_ = option_type_size_max;
+ }
+ if(type_size_max_ == 0) {
+ required_ = false;
+ }
+ if(type_size_max_ >= detail::expected_max_vector_size) {
+ inject_separator_ = true;
+ }
+ return this;
+ }
+
+ /// Set the value of the separator injection flag
+ void inject_separator(bool value = true) { inject_separator_ = value; }
+
+ /// Set a capture function for the default. Mostly used by App.
+ Option *default_function(const std::function<std::string()> &func) {
+ default_function_ = func;
+ return this;
+ }
+
+ /// Capture the default value from the original value (if it can be captured)
+ Option *capture_default_str() {
+ if(default_function_) {
+ default_str_ = default_function_();
+ }
+ return this;
+ }
+
+ /// Set the default value string representation (does not change the contained value)
+ Option *default_str(std::string val) {
+ default_str_ = std::move(val);
+ return this;
+ }
+
+ /// Set the default value and validate the results and run the callback if appropriate to set the value into the
+ /// bound value only available for types that can be converted to a string
+ template <typename X> Option *default_val(const X &val) {
+ std::string val_str = detail::to_string(val);
+ auto old_option_state = current_option_state_;
+ results_t old_results{std::move(results_)};
+ results_.clear();
+ try {
+ add_result(val_str);
+ // if trigger_on_result_ is set the callback already ran
+ if(run_callback_for_default_ && !trigger_on_result_) {
+ run_callback(); // run callback sets the state, we need to reset it again
+ current_option_state_ = option_state::parsing;
+ } else {
+ _validate_results(results_);
+ current_option_state_ = old_option_state;
+ }
+ } catch(const CLI::Error &) {
+ // this should be done
+ results_ = std::move(old_results);
+ current_option_state_ = old_option_state;
+ throw;
+ }
+ results_ = std::move(old_results);
+ default_str_ = std::move(val_str);
+ return this;
+ }
+
+ /// Get the full typename for this option
+ std::string get_type_name() const {
+ std::string full_type_name = type_name_();
+ if(!validators_.empty()) {
+ for(auto &Validator : validators_) {
+ std::string vtype = Validator.get_description();
+ if(!vtype.empty()) {
+ full_type_name += ":" + vtype;
+ }
+ }
+ }
+ return full_type_name;
+ }
+
+ private:
+ /// Run the results through the Validators
+ void _validate_results(results_t &res) const {
+ // Run the Validators (can change the string)
+ if(!validators_.empty()) {
+ if(type_size_max_ > 1) { // in this context index refers to the index in the type
+ int index = 0;
+ if(get_items_expected_max() < static_cast<int>(res.size()) &&
+ multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) {
+ // create a negative index for the earliest ones
+ index = get_items_expected_max() - static_cast<int>(res.size());
+ }
+
+ for(std::string &result : res) {
+ if(detail::is_separator(result) && type_size_max_ != type_size_min_ && index >= 0) {
+ index = 0; // reset index for variable size chunks
+ continue;
+ }
+ auto err_msg = _validate(result, (index >= 0) ? (index % type_size_max_) : index);
+ if(!err_msg.empty())
+ throw ValidationError(get_name(), err_msg);
+ ++index;
+ }
+ } else {
+ int index = 0;
+ if(expected_max_ < static_cast<int>(res.size()) &&
+ multi_option_policy_ == CLI::MultiOptionPolicy::TakeLast) {
+ // create a negative index for the earliest ones
+ index = expected_max_ - static_cast<int>(res.size());
+ }
+ for(std::string &result : res) {
+ auto err_msg = _validate(result, index);
+ ++index;
+ if(!err_msg.empty())
+ throw ValidationError(get_name(), err_msg);
+ }
+ }
+ }
+ }
+
+ /** reduce the results in accordance with the MultiOptionPolicy
+ @param[out] res results are assigned to res if there if they are different
+ */
+ void _reduce_results(results_t &res, const results_t &original) const {
+
+ // max num items expected or length of vector, always at least 1
+ // Only valid for a trimming policy
+
+ res.clear();
+ // Operation depends on the policy setting
+ switch(multi_option_policy_) {
+ case MultiOptionPolicy::TakeAll:
+ break;
+ case MultiOptionPolicy::TakeLast: {
+ // Allow multi-option sizes (including 0)
+ std::size_t trim_size = std::min<std::size_t>(
+ static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());
+ if(original.size() != trim_size) {
+ res.assign(original.end() - static_cast<results_t::difference_type>(trim_size), original.end());
+ }
+ } break;
+ case MultiOptionPolicy::TakeFirst: {
+ std::size_t trim_size = std::min<std::size_t>(
+ static_cast<std::size_t>(std::max<int>(get_items_expected_max(), 1)), original.size());
+ if(original.size() != trim_size) {
+ res.assign(original.begin(), original.begin() + static_cast<results_t::difference_type>(trim_size));
+ }
+ } break;
+ case MultiOptionPolicy::Join:
+ if(results_.size() > 1) {
+ res.push_back(detail::join(original, std::string(1, (delimiter_ == '\0') ? '\n' : delimiter_)));
+ }
+ break;
+ case MultiOptionPolicy::Sum:
+ res.push_back(detail::sum_string_vector(original));
+ break;
+ case MultiOptionPolicy::Throw:
+ default: {
+ auto num_min = static_cast<std::size_t>(get_items_expected_min());
+ auto num_max = static_cast<std::size_t>(get_items_expected_max());
+ if(num_min == 0) {
+ num_min = 1;
+ }
+ if(num_max == 0) {
+ num_max = 1;
+ }
+ if(original.size() < num_min) {
+ throw ArgumentMismatch::AtLeast(get_name(), static_cast<int>(num_min), original.size());
+ }
+ if(original.size() > num_max) {
+ throw ArgumentMismatch::AtMost(get_name(), static_cast<int>(num_max), original.size());
+ }
+ break;
+ }
+ }
+ // this check is to allow an empty vector in certain circumstances but not if expected is not zero.
+ // {} is the indicator for a an empty container
+ if(res.empty()) {
+ if(original.size() == 1 && original[0] == "{}" && get_items_expected_min() > 0) {
+ res.push_back("{}");
+ res.push_back("%%");
+ }
+ } else if(res.size() == 1 && res[0] == "{}" && get_items_expected_min() > 0) {
+ res.push_back("%%");
+ }
+ }
+
+ // Run a result through the Validators
+ std::string _validate(std::string &result, int index) const {
+ std::string err_msg;
+ if(result.empty() && expected_min_ == 0) {
+ // an empty with nothing expected is allowed
+ return err_msg;
+ }
+ for(const auto &vali : validators_) {
+ auto v = vali.get_application_index();
+ if(v == -1 || v == index) {
+ try {
+ err_msg = vali(result);
+ } catch(const ValidationError &err) {
+ err_msg = err.what();
+ }
+ if(!err_msg.empty())
+ break;
+ }
+ }
+
+ return err_msg;
+ }
+
+ /// Add a single result to the result set, taking into account delimiters
+ int _add_result(std::string &&result, std::vector<std::string> &res) const {
+ int result_count = 0;
+ if(allow_extra_args_ && !result.empty() && result.front() == '[' &&
+ result.back() == ']') { // this is now a vector string likely from the default or user entry
+ result.pop_back();
+
+ for(auto &var : CLI::detail::split(result.substr(1), ',')) {
+ if(!var.empty()) {
+ result_count += _add_result(std::move(var), res);
+ }
+ }
+ return result_count;
+ }
+ if(delimiter_ == '\0') {
+ res.push_back(std::move(result));
+ ++result_count;
+ } else {
+ if((result.find_first_of(delimiter_) != std::string::npos)) {
+ for(const auto &var : CLI::detail::split(result, delimiter_)) {
+ if(!var.empty()) {
+ res.push_back(var);
+ ++result_count;
+ }
+ }
+ } else {
+ res.push_back(std::move(result));
+ ++result_count;
+ }
+ }
+ return result_count;
+ }
+};
+
+// [CLI11:option_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/Split.hpp b/src/third-party/CLI/Split.hpp
new file mode 100644
index 0000000..0ccfb57
--- /dev/null
+++ b/src/third-party/CLI/Split.hpp
@@ -0,0 +1,143 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <string>
+#include <tuple>
+#include <utility>
+#include <vector>
+// [CLI11:public_includes:end]
+
+#include "Error.hpp"
+#include "StringTools.hpp"
+
+namespace CLI {
+// [CLI11:split_hpp:verbatim]
+
+namespace detail {
+
+// Returns false if not a short option. Otherwise, sets opt name and rest and returns true
+inline bool split_short(const std::string &current, std::string &name, std::string &rest) {
+ if(current.size() > 1 && current[0] == '-' && valid_first_char(current[1])) {
+ name = current.substr(1, 1);
+ rest = current.substr(2);
+ return true;
+ }
+ return false;
+}
+
+// Returns false if not a long option. Otherwise, sets opt name and other side of = and returns true
+inline bool split_long(const std::string &current, std::string &name, std::string &value) {
+ if(current.size() > 2 && current.substr(0, 2) == "--" && valid_first_char(current[2])) {
+ auto loc = current.find_first_of('=');
+ if(loc != std::string::npos) {
+ name = current.substr(2, loc - 2);
+ value = current.substr(loc + 1);
+ } else {
+ name = current.substr(2);
+ value = "";
+ }
+ return true;
+ }
+ return false;
+}
+
+// Returns false if not a windows style option. Otherwise, sets opt name and value and returns true
+inline bool split_windows_style(const std::string &current, std::string &name, std::string &value) {
+ if(current.size() > 1 && current[0] == '/' && valid_first_char(current[1])) {
+ auto loc = current.find_first_of(':');
+ if(loc != std::string::npos) {
+ name = current.substr(1, loc - 1);
+ value = current.substr(loc + 1);
+ } else {
+ name = current.substr(1);
+ value = "";
+ }
+ return true;
+ }
+ return false;
+}
+
+// Splits a string into multiple long and short names
+inline std::vector<std::string> split_names(std::string current) {
+ std::vector<std::string> output;
+ std::size_t val;
+ while((val = current.find(",")) != std::string::npos) {
+ output.push_back(trim_copy(current.substr(0, val)));
+ current = current.substr(val + 1);
+ }
+ output.push_back(trim_copy(current));
+ return output;
+}
+
+/// extract default flag values either {def} or starting with a !
+inline std::vector<std::pair<std::string, std::string>> get_default_flag_values(const std::string &str) {
+ std::vector<std::string> flags = split_names(str);
+ flags.erase(std::remove_if(flags.begin(),
+ flags.end(),
+ [](const std::string &name) {
+ return ((name.empty()) || (!(((name.find_first_of('{') != std::string::npos) &&
+ (name.back() == '}')) ||
+ (name[0] == '!'))));
+ }),
+ flags.end());
+ std::vector<std::pair<std::string, std::string>> output;
+ output.reserve(flags.size());
+ for(auto &flag : flags) {
+ auto def_start = flag.find_first_of('{');
+ std::string defval = "false";
+ if((def_start != std::string::npos) && (flag.back() == '}')) {
+ defval = flag.substr(def_start + 1);
+ defval.pop_back();
+ flag.erase(def_start, std::string::npos);
+ }
+ flag.erase(0, flag.find_first_not_of("-!"));
+ output.emplace_back(flag, defval);
+ }
+ return output;
+}
+
+/// Get a vector of short names, one of long names, and a single name
+inline std::tuple<std::vector<std::string>, std::vector<std::string>, std::string>
+get_names(const std::vector<std::string> &input) {
+
+ std::vector<std::string> short_names;
+ std::vector<std::string> long_names;
+ std::string pos_name;
+
+ for(std::string name : input) {
+ if(name.length() == 0) {
+ continue;
+ }
+ if(name.length() > 1 && name[0] == '-' && name[1] != '-') {
+ if(name.length() == 2 && valid_first_char(name[1]))
+ short_names.emplace_back(1, name[1]);
+ else
+ throw BadNameString::OneCharName(name);
+ } else if(name.length() > 2 && name.substr(0, 2) == "--") {
+ name = name.substr(2);
+ if(valid_name_string(name))
+ long_names.push_back(name);
+ else
+ throw BadNameString::BadLongName(name);
+ } else if(name == "-" || name == "--") {
+ throw BadNameString::DashesOnly(name);
+ } else {
+ if(pos_name.length() > 0)
+ throw BadNameString::MultiPositionalNames(name);
+ pos_name = name;
+ }
+ }
+
+ return std::tuple<std::vector<std::string>, std::vector<std::string>, std::string>(
+ short_names, long_names, pos_name);
+}
+
+} // namespace detail
+// [CLI11:split_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/StringTools.hpp b/src/third-party/CLI/StringTools.hpp
new file mode 100644
index 0000000..5126645
--- /dev/null
+++ b/src/third-party/CLI/StringTools.hpp
@@ -0,0 +1,430 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <algorithm>
+#include <iomanip>
+#include <locale>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <vector>
+// [CLI11:public_includes:end]
+
+namespace CLI {
+
+// [CLI11:string_tools_hpp:verbatim]
+
+/// Include the items in this namespace to get free conversion of enums to/from streams.
+/// (This is available inside CLI as well, so CLI11 will use this without a using statement).
+namespace enums {
+
+/// output streaming for enumerations
+template <typename T, typename = typename std::enable_if<std::is_enum<T>::value>::type>
+std::ostream &operator<<(std::ostream &in, const T &item) {
+ // make sure this is out of the detail namespace otherwise it won't be found when needed
+ return in << static_cast<typename std::underlying_type<T>::type>(item);
+}
+
+} // namespace enums
+
+/// Export to CLI namespace
+using enums::operator<<;
+
+namespace detail {
+/// a constant defining an expected max vector size defined to be a big number that could be multiplied by 4 and not
+/// produce overflow for some expected uses
+constexpr int expected_max_vector_size{1 << 29};
+// Based on http://stackoverflow.com/questions/236129/split-a-string-in-c
+/// Split a string by a delim
+inline std::vector<std::string> split(const std::string &s, char delim) {
+ std::vector<std::string> elems;
+ // Check to see if empty string, give consistent result
+ if(s.empty()) {
+ elems.emplace_back();
+ } else {
+ std::stringstream ss;
+ ss.str(s);
+ std::string item;
+ while(std::getline(ss, item, delim)) {
+ elems.push_back(item);
+ }
+ }
+ return elems;
+}
+
+/// Simple function to join a string
+template <typename T> std::string join(const T &v, std::string delim = ",") {
+ std::ostringstream s;
+ auto beg = std::begin(v);
+ auto end = std::end(v);
+ if(beg != end)
+ s << *beg++;
+ while(beg != end) {
+ s << delim << *beg++;
+ }
+ return s.str();
+}
+
+/// Simple function to join a string from processed elements
+template <typename T,
+ typename Callable,
+ typename = typename std::enable_if<!std::is_constructible<std::string, Callable>::value>::type>
+std::string join(const T &v, Callable func, std::string delim = ",") {
+ std::ostringstream s;
+ auto beg = std::begin(v);
+ auto end = std::end(v);
+ auto loc = s.tellp();
+ while(beg != end) {
+ auto nloc = s.tellp();
+ if(nloc > loc) {
+ s << delim;
+ loc = nloc;
+ }
+ s << func(*beg++);
+ }
+ return s.str();
+}
+
+/// Join a string in reverse order
+template <typename T> std::string rjoin(const T &v, std::string delim = ",") {
+ std::ostringstream s;
+ for(std::size_t start = 0; start < v.size(); start++) {
+ if(start > 0)
+ s << delim;
+ s << v[v.size() - start - 1];
+ }
+ return s.str();
+}
+
+// Based roughly on http://stackoverflow.com/questions/25829143/c-trim-whitespace-from-a-string
+
+/// Trim whitespace from left of string
+inline std::string &ltrim(std::string &str) {
+ auto it = std::find_if(str.begin(), str.end(), [](char ch) { return !std::isspace<char>(ch, std::locale()); });
+ str.erase(str.begin(), it);
+ return str;
+}
+
+/// Trim anything from left of string
+inline std::string &ltrim(std::string &str, const std::string &filter) {
+ auto it = std::find_if(str.begin(), str.end(), [&filter](char ch) { return filter.find(ch) == std::string::npos; });
+ str.erase(str.begin(), it);
+ return str;
+}
+
+/// Trim whitespace from right of string
+inline std::string &rtrim(std::string &str) {
+ auto it = std::find_if(str.rbegin(), str.rend(), [](char ch) { return !std::isspace<char>(ch, std::locale()); });
+ str.erase(it.base(), str.end());
+ return str;
+}
+
+/// Trim anything from right of string
+inline std::string &rtrim(std::string &str, const std::string &filter) {
+ auto it =
+ std::find_if(str.rbegin(), str.rend(), [&filter](char ch) { return filter.find(ch) == std::string::npos; });
+ str.erase(it.base(), str.end());
+ return str;
+}
+
+/// Trim whitespace from string
+inline std::string &trim(std::string &str) { return ltrim(rtrim(str)); }
+
+/// Trim anything from string
+inline std::string &trim(std::string &str, const std::string filter) { return ltrim(rtrim(str, filter), filter); }
+
+/// Make a copy of the string and then trim it
+inline std::string trim_copy(const std::string &str) {
+ std::string s = str;
+ return trim(s);
+}
+
+/// remove quotes at the front and back of a string either '"' or '\''
+inline std::string &remove_quotes(std::string &str) {
+ if(str.length() > 1 && (str.front() == '"' || str.front() == '\'')) {
+ if(str.front() == str.back()) {
+ str.pop_back();
+ str.erase(str.begin(), str.begin() + 1);
+ }
+ }
+ return str;
+}
+
+/// Add a leader to the beginning of all new lines (nothing is added
+/// at the start of the first line). `"; "` would be for ini files
+///
+/// Can't use Regex, or this would be a subs.
+inline std::string fix_newlines(const std::string &leader, std::string input) {
+ std::string::size_type n = 0;
+ while(n != std::string::npos && n < input.size()) {
+ n = input.find('\n', n);
+ if(n != std::string::npos) {
+ input = input.substr(0, n + 1) + leader + input.substr(n + 1);
+ n += leader.size();
+ }
+ }
+ return input;
+}
+
+/// Make a copy of the string and then trim it, any filter string can be used (any char in string is filtered)
+inline std::string trim_copy(const std::string &str, const std::string &filter) {
+ std::string s = str;
+ return trim(s, filter);
+}
+/// Print a two part "help" string
+inline std::ostream &format_help(std::ostream &out, std::string name, const std::string &description, std::size_t wid) {
+ name = " " + name;
+ out << std::setw(static_cast<int>(wid)) << std::left << name;
+ if(!description.empty()) {
+ if(name.length() >= wid)
+ out << "\n" << std::setw(static_cast<int>(wid)) << "";
+ for(const char c : description) {
+ out.put(c);
+ if(c == '\n') {
+ out << std::setw(static_cast<int>(wid)) << "";
+ }
+ }
+ }
+ out << "\n";
+ return out;
+}
+
+/// Print subcommand aliases
+inline std::ostream &format_aliases(std::ostream &out, const std::vector<std::string> &aliases, std::size_t wid) {
+ if(!aliases.empty()) {
+ out << std::setw(static_cast<int>(wid)) << " aliases: ";
+ bool front = true;
+ for(const auto &alias : aliases) {
+ if(!front) {
+ out << ", ";
+ } else {
+ front = false;
+ }
+ out << detail::fix_newlines(" ", alias);
+ }
+ out << "\n";
+ }
+ return out;
+}
+
+/// Verify the first character of an option
+/// - is a trigger character, ! has special meaning and new lines would just be annoying to deal with
+template <typename T> bool valid_first_char(T c) { return ((c != '-') && (c != '!') && (c != ' ') && c != '\n'); }
+
+/// Verify following characters of an option
+template <typename T> bool valid_later_char(T c) {
+ // = and : are value separators, { has special meaning for option defaults,
+ // and \n would just be annoying to deal with in many places allowing space here has too much potential for
+ // inadvertent entry errors and bugs
+ return ((c != '=') && (c != ':') && (c != '{') && (c != ' ') && c != '\n');
+}
+
+/// Verify an option/subcommand name
+inline bool valid_name_string(const std::string &str) {
+ if(str.empty() || !valid_first_char(str[0])) {
+ return false;
+ }
+ auto e = str.end();
+ for(auto c = str.begin() + 1; c != e; ++c)
+ if(!valid_later_char(*c))
+ return false;
+ return true;
+}
+
+/// Verify an app name
+inline bool valid_alias_name_string(const std::string &str) {
+ static const std::string badChars(std::string("\n") + '\0');
+ return (str.find_first_of(badChars) == std::string::npos);
+}
+
+/// check if a string is a container segment separator (empty or "%%")
+inline bool is_separator(const std::string &str) {
+ static const std::string sep("%%");
+ return (str.empty() || str == sep);
+}
+
+/// Verify that str consists of letters only
+inline bool isalpha(const std::string &str) {
+ return std::all_of(str.begin(), str.end(), [](char c) { return std::isalpha(c, std::locale()); });
+}
+
+/// Return a lower case version of a string
+inline std::string to_lower(std::string str) {
+ std::transform(std::begin(str), std::end(str), std::begin(str), [](const std::string::value_type &x) {
+ return std::tolower(x, std::locale());
+ });
+ return str;
+}
+
+/// remove underscores from a string
+inline std::string remove_underscore(std::string str) {
+ str.erase(std::remove(std::begin(str), std::end(str), '_'), std::end(str));
+ return str;
+}
+
+/// Find and replace a substring with another substring
+inline std::string find_and_replace(std::string str, std::string from, std::string to) {
+
+ std::size_t start_pos = 0;
+
+ while((start_pos = str.find(from, start_pos)) != std::string::npos) {
+ str.replace(start_pos, from.length(), to);
+ start_pos += to.length();
+ }
+
+ return str;
+}
+
+/// check if the flag definitions has possible false flags
+inline bool has_default_flag_values(const std::string &flags) {
+ return (flags.find_first_of("{!") != std::string::npos);
+}
+
+inline void remove_default_flag_values(std::string &flags) {
+ auto loc = flags.find_first_of('{', 2);
+ while(loc != std::string::npos) {
+ auto finish = flags.find_first_of("},", loc + 1);
+ if((finish != std::string::npos) && (flags[finish] == '}')) {
+ flags.erase(flags.begin() + static_cast<std::ptrdiff_t>(loc),
+ flags.begin() + static_cast<std::ptrdiff_t>(finish) + 1);
+ }
+ loc = flags.find_first_of('{', loc + 1);
+ }
+ flags.erase(std::remove(flags.begin(), flags.end(), '!'), flags.end());
+}
+
+/// Check if a string is a member of a list of strings and optionally ignore case or ignore underscores
+inline std::ptrdiff_t find_member(std::string name,
+ const std::vector<std::string> names,
+ bool ignore_case = false,
+ bool ignore_underscore = false) {
+ auto it = std::end(names);
+ if(ignore_case) {
+ if(ignore_underscore) {
+ name = detail::to_lower(detail::remove_underscore(name));
+ it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) {
+ return detail::to_lower(detail::remove_underscore(local_name)) == name;
+ });
+ } else {
+ name = detail::to_lower(name);
+ it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) {
+ return detail::to_lower(local_name) == name;
+ });
+ }
+
+ } else if(ignore_underscore) {
+ name = detail::remove_underscore(name);
+ it = std::find_if(std::begin(names), std::end(names), [&name](std::string local_name) {
+ return detail::remove_underscore(local_name) == name;
+ });
+ } else {
+ it = std::find(std::begin(names), std::end(names), name);
+ }
+
+ return (it != std::end(names)) ? (it - std::begin(names)) : (-1);
+}
+
+/// Find a trigger string and call a modify callable function that takes the current string and starting position of the
+/// trigger and returns the position in the string to search for the next trigger string
+template <typename Callable> inline std::string find_and_modify(std::string str, std::string trigger, Callable modify) {
+ std::size_t start_pos = 0;
+ while((start_pos = str.find(trigger, start_pos)) != std::string::npos) {
+ start_pos = modify(str, start_pos);
+ }
+ return str;
+}
+
+/// Split a string '"one two" "three"' into 'one two', 'three'
+/// Quote characters can be ` ' or "
+inline std::vector<std::string> split_up(std::string str, char delimiter = '\0') {
+
+ const std::string delims("\'\"`");
+ auto find_ws = [delimiter](char ch) {
+ return (delimiter == '\0') ? (std::isspace<char>(ch, std::locale()) != 0) : (ch == delimiter);
+ };
+ trim(str);
+
+ std::vector<std::string> output;
+ bool embeddedQuote = false;
+ char keyChar = ' ';
+ while(!str.empty()) {
+ if(delims.find_first_of(str[0]) != std::string::npos) {
+ keyChar = str[0];
+ auto end = str.find_first_of(keyChar, 1);
+ while((end != std::string::npos) && (str[end - 1] == '\\')) { // deal with escaped quotes
+ end = str.find_first_of(keyChar, end + 1);
+ embeddedQuote = true;
+ }
+ if(end != std::string::npos) {
+ output.push_back(str.substr(1, end - 1));
+ if(end + 2 < str.size()) {
+ str = str.substr(end + 2);
+ } else {
+ str.clear();
+ }
+
+ } else {
+ output.push_back(str.substr(1));
+ str = "";
+ }
+ } else {
+ auto it = std::find_if(std::begin(str), std::end(str), find_ws);
+ if(it != std::end(str)) {
+ std::string value = std::string(str.begin(), it);
+ output.push_back(value);
+ str = std::string(it + 1, str.end());
+ } else {
+ output.push_back(str);
+ str = "";
+ }
+ }
+ // transform any embedded quotes into the regular character
+ if(embeddedQuote) {
+ output.back() = find_and_replace(output.back(), std::string("\\") + keyChar, std::string(1, keyChar));
+ embeddedQuote = false;
+ }
+ trim(str);
+ }
+ return output;
+}
+
+/// This function detects an equal or colon followed by an escaped quote after an argument
+/// then modifies the string to replace the equality with a space. This is needed
+/// to allow the split up function to work properly and is intended to be used with the find_and_modify function
+/// the return value is the offset+1 which is required by the find_and_modify function.
+inline std::size_t escape_detect(std::string &str, std::size_t offset) {
+ auto next = str[offset + 1];
+ if((next == '\"') || (next == '\'') || (next == '`')) {
+ auto astart = str.find_last_of("-/ \"\'`", offset - 1);
+ if(astart != std::string::npos) {
+ if(str[astart] == ((str[offset] == '=') ? '-' : '/'))
+ str[offset] = ' '; // interpret this as a space so the split_up works properly
+ }
+ }
+ return offset + 1;
+}
+
+/// Add quotes if the string contains spaces
+inline std::string &add_quotes_if_needed(std::string &str) {
+ if((str.front() != '"' && str.front() != '\'') || str.front() != str.back()) {
+ char quote = str.find('"') < str.find('\'') ? '\'' : '"';
+ if(str.find(' ') != std::string::npos) {
+ str.insert(0, 1, quote);
+ str.append(1, quote);
+ }
+ }
+ return str;
+}
+
+} // namespace detail
+
+// [CLI11:string_tools_hpp:end]
+
+} // namespace CLI
diff --git a/src/third-party/CLI/Timer.hpp b/src/third-party/CLI/Timer.hpp
new file mode 100644
index 0000000..e97b17c
--- /dev/null
+++ b/src/third-party/CLI/Timer.hpp
@@ -0,0 +1,134 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// On GCC < 4.8, the following define is often missing. Due to the
+// fact that this library only uses sleep_for, this should be safe
+#if defined(__GNUC__) && !defined(__clang__) && __GNUC__ < 5 && __GNUC_MINOR__ < 8
+#define _GLIBCXX_USE_NANOSLEEP
+#endif
+
+#include <array>
+#include <chrono>
+#include <functional>
+#include <iostream>
+#include <string>
+#include <utility>
+
+namespace CLI {
+
+/// This is a simple timer with pretty printing. Creating the timer starts counting.
+class Timer {
+ protected:
+ /// This is a typedef to make clocks easier to use
+ using clock = std::chrono::steady_clock;
+
+ /// This typedef is for points in time
+ using time_point = std::chrono::time_point<clock>;
+
+ /// This is the type of a printing function, you can make your own
+ using time_print_t = std::function<std::string(std::string, std::string)>;
+
+ /// This is the title of the timer
+ std::string title_;
+
+ /// This is the function that is used to format most of the timing message
+ time_print_t time_print_;
+
+ /// This is the starting point (when the timer was created)
+ time_point start_;
+
+ /// This is the number of times cycles (print divides by this number)
+ std::size_t cycles{1};
+
+ public:
+ /// Standard print function, this one is set by default
+ static std::string Simple(std::string title, std::string time) { return title + ": " + time; }
+
+ /// This is a fancy print function with --- headers
+ static std::string Big(std::string title, std::string time) {
+ return std::string("-----------------------------------------\n") + "| " + title + " | Time = " + time + "\n" +
+ "-----------------------------------------";
+ }
+
+ public:
+ /// Standard constructor, can set title and print function
+ explicit Timer(std::string title = "Timer", time_print_t time_print = Simple)
+ : title_(std::move(title)), time_print_(std::move(time_print)), start_(clock::now()) {}
+
+ /// Time a function by running it multiple times. Target time is the len to target.
+ std::string time_it(std::function<void()> f, double target_time = 1) {
+ time_point start = start_;
+ double total_time;
+
+ start_ = clock::now();
+ std::size_t n = 0;
+ do {
+ f();
+ std::chrono::duration<double> elapsed = clock::now() - start_;
+ total_time = elapsed.count();
+ } while(n++ < 100u && total_time < target_time);
+
+ std::string out = make_time_str(total_time / static_cast<double>(n)) + " for " + std::to_string(n) + " tries";
+ start_ = start;
+ return out;
+ }
+
+ /// This formats the numerical value for the time string
+ std::string make_time_str() const {
+ time_point stop = clock::now();
+ std::chrono::duration<double> elapsed = stop - start_;
+ double time = elapsed.count() / static_cast<double>(cycles);
+ return make_time_str(time);
+ }
+
+ // LCOV_EXCL_START
+ /// This prints out a time string from a time
+ std::string make_time_str(double time) const {
+ auto print_it = [](double x, std::string unit) {
+ const unsigned int buffer_length = 50;
+ std::array<char, buffer_length> buffer;
+ std::snprintf(buffer.data(), buffer_length, "%.5g", x);
+ return buffer.data() + std::string(" ") + unit;
+ };
+
+ if(time < .000001)
+ return print_it(time * 1000000000, "ns");
+ else if(time < .001)
+ return print_it(time * 1000000, "us");
+ else if(time < 1)
+ return print_it(time * 1000, "ms");
+ else
+ return print_it(time, "s");
+ }
+ // LCOV_EXCL_STOP
+
+ /// This is the main function, it creates a string
+ std::string to_string() const { return time_print_(title_, make_time_str()); }
+
+ /// Division sets the number of cycles to divide by (no graphical change)
+ Timer &operator/(std::size_t val) {
+ cycles = val;
+ return *this;
+ }
+};
+
+/// This class prints out the time upon destruction
+class AutoTimer : public Timer {
+ public:
+ /// Reimplementing the constructor is required in GCC 4.7
+ explicit AutoTimer(std::string title = "Timer", time_print_t time_print = Simple) : Timer(title, time_print) {}
+ // GCC 4.7 does not support using inheriting constructors.
+
+ /// This destructor prints the string
+ ~AutoTimer() { std::cout << to_string() << std::endl; }
+};
+
+} // namespace CLI
+
+/// This prints out the time if shifted into a std::cout like stream.
+inline std::ostream &operator<<(std::ostream &in, const CLI::Timer &timer) { return in << timer.to_string(); }
diff --git a/src/third-party/CLI/TypeTools.hpp b/src/third-party/CLI/TypeTools.hpp
new file mode 100644
index 0000000..c21d5e3
--- /dev/null
+++ b/src/third-party/CLI/TypeTools.hpp
@@ -0,0 +1,1558 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:public_includes:set]
+#include <cstdint>
+#include <exception>
+#include <limits>
+#include <memory>
+#include <string>
+#include <type_traits>
+#include <utility>
+#include <vector>
+// [CLI11:public_includes:end]
+
+#include "StringTools.hpp"
+
+namespace CLI {
+// [CLI11:type_tools_hpp:verbatim]
+
+// Type tools
+
+// Utilities for type enabling
+namespace detail {
+// Based generally on https://rmf.io/cxx11/almost-static-if
+/// Simple empty scoped class
+enum class enabler {};
+
+/// An instance to use in EnableIf
+constexpr enabler dummy = {};
+} // namespace detail
+
+/// A copy of enable_if_t from C++14, compatible with C++11.
+///
+/// We could check to see if C++14 is being used, but it does not hurt to redefine this
+/// (even Google does this: https://github.com/google/skia/blob/main/include/private/SkTLogic.h)
+/// It is not in the std namespace anyway, so no harm done.
+template <bool B, class T = void> using enable_if_t = typename std::enable_if<B, T>::type;
+
+/// A copy of std::void_t from C++17 (helper for C++11 and C++14)
+template <typename... Ts> struct make_void { using type = void; };
+
+/// A copy of std::void_t from C++17 - same reasoning as enable_if_t, it does not hurt to redefine
+template <typename... Ts> using void_t = typename make_void<Ts...>::type;
+
+/// A copy of std::conditional_t from C++14 - same reasoning as enable_if_t, it does not hurt to redefine
+template <bool B, class T, class F> using conditional_t = typename std::conditional<B, T, F>::type;
+
+/// Check to see if something is bool (fail check by default)
+template <typename T> struct is_bool : std::false_type {};
+
+/// Check to see if something is bool (true if actually a bool)
+template <> struct is_bool<bool> : std::true_type {};
+
+/// Check to see if something is a shared pointer
+template <typename T> struct is_shared_ptr : std::false_type {};
+
+/// Check to see if something is a shared pointer (True if really a shared pointer)
+template <typename T> struct is_shared_ptr<std::shared_ptr<T>> : std::true_type {};
+
+/// Check to see if something is a shared pointer (True if really a shared pointer)
+template <typename T> struct is_shared_ptr<const std::shared_ptr<T>> : std::true_type {};
+
+/// Check to see if something is copyable pointer
+template <typename T> struct is_copyable_ptr {
+ static bool const value = is_shared_ptr<T>::value || std::is_pointer<T>::value;
+};
+
+/// This can be specialized to override the type deduction for IsMember.
+template <typename T> struct IsMemberType { using type = T; };
+
+/// The main custom type needed here is const char * should be a string.
+template <> struct IsMemberType<const char *> { using type = std::string; };
+
+namespace detail {
+
+// These are utilities for IsMember and other transforming objects
+
+/// Handy helper to access the element_type generically. This is not part of is_copyable_ptr because it requires that
+/// pointer_traits<T> be valid.
+
+/// not a pointer
+template <typename T, typename Enable = void> struct element_type { using type = T; };
+
+template <typename T> struct element_type<T, typename std::enable_if<is_copyable_ptr<T>::value>::type> {
+ using type = typename std::pointer_traits<T>::element_type;
+};
+
+/// Combination of the element type and value type - remove pointer (including smart pointers) and get the value_type of
+/// the container
+template <typename T> struct element_value_type { using type = typename element_type<T>::type::value_type; };
+
+/// Adaptor for set-like structure: This just wraps a normal container in a few utilities that do almost nothing.
+template <typename T, typename _ = void> struct pair_adaptor : std::false_type {
+ using value_type = typename T::value_type;
+ using first_type = typename std::remove_const<value_type>::type;
+ using second_type = typename std::remove_const<value_type>::type;
+
+ /// Get the first value (really just the underlying value)
+ template <typename Q> static auto first(Q &&pair_value) -> decltype(std::forward<Q>(pair_value)) {
+ return std::forward<Q>(pair_value);
+ }
+ /// Get the second value (really just the underlying value)
+ template <typename Q> static auto second(Q &&pair_value) -> decltype(std::forward<Q>(pair_value)) {
+ return std::forward<Q>(pair_value);
+ }
+};
+
+/// Adaptor for map-like structure (true version, must have key_type and mapped_type).
+/// This wraps a mapped container in a few utilities access it in a general way.
+template <typename T>
+struct pair_adaptor<
+ T,
+ conditional_t<false, void_t<typename T::value_type::first_type, typename T::value_type::second_type>, void>>
+ : std::true_type {
+ using value_type = typename T::value_type;
+ using first_type = typename std::remove_const<typename value_type::first_type>::type;
+ using second_type = typename std::remove_const<typename value_type::second_type>::type;
+
+ /// Get the first value (really just the underlying value)
+ template <typename Q> static auto first(Q &&pair_value) -> decltype(std::get<0>(std::forward<Q>(pair_value))) {
+ return std::get<0>(std::forward<Q>(pair_value));
+ }
+ /// Get the second value (really just the underlying value)
+ template <typename Q> static auto second(Q &&pair_value) -> decltype(std::get<1>(std::forward<Q>(pair_value))) {
+ return std::get<1>(std::forward<Q>(pair_value));
+ }
+};
+
+// Warning is suppressed due to "bug" in gcc<5.0 and gcc 7.0 with c++17 enabled that generates a Wnarrowing warning
+// in the unevaluated context even if the function that was using this wasn't used. The standard says narrowing in
+// brace initialization shouldn't be allowed but for backwards compatibility gcc allows it in some contexts. It is a
+// little fuzzy what happens in template constructs and I think that was something GCC took a little while to work out.
+// But regardless some versions of gcc generate a warning when they shouldn't from the following code so that should be
+// suppressed
+#ifdef __GNUC__
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wnarrowing"
+#endif
+// check for constructibility from a specific type and copy assignable used in the parse detection
+template <typename T, typename C> class is_direct_constructible {
+ template <typename TT, typename CC>
+ static auto test(int, std::true_type) -> decltype(
+// NVCC warns about narrowing conversions here
+#ifdef __CUDACC__
+#pragma diag_suppress 2361
+#endif
+ TT { std::declval<CC>() }
+#ifdef __CUDACC__
+#pragma diag_default 2361
+#endif
+ ,
+ std::is_move_assignable<TT>());
+
+ template <typename TT, typename CC> static auto test(int, std::false_type) -> std::false_type;
+
+ template <typename, typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<T, C>(0, typename std::is_constructible<T, C>::type()))::value;
+};
+#ifdef __GNUC__
+#pragma GCC diagnostic pop
+#endif
+
+// Check for output streamability
+// Based on https://stackoverflow.com/questions/22758291/how-can-i-detect-if-a-type-can-be-streamed-to-an-stdostream
+
+template <typename T, typename S = std::ostringstream> class is_ostreamable {
+ template <typename TT, typename SS>
+ static auto test(int) -> decltype(std::declval<SS &>() << std::declval<TT>(), std::true_type());
+
+ template <typename, typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<T, S>(0))::value;
+};
+
+/// Check for input streamability
+template <typename T, typename S = std::istringstream> class is_istreamable {
+ template <typename TT, typename SS>
+ static auto test(int) -> decltype(std::declval<SS &>() >> std::declval<TT &>(), std::true_type());
+
+ template <typename, typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<T, S>(0))::value;
+};
+
+/// Check for complex
+template <typename T> class is_complex {
+ template <typename TT>
+ static auto test(int) -> decltype(std::declval<TT>().real(), std::declval<TT>().imag(), std::true_type());
+
+ template <typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<T>(0))::value;
+};
+
+/// Templated operation to get a value from a stream
+template <typename T, enable_if_t<is_istreamable<T>::value, detail::enabler> = detail::dummy>
+bool from_stream(const std::string &istring, T &obj) {
+ std::istringstream is;
+ is.str(istring);
+ is >> obj;
+ return !is.fail() && !is.rdbuf()->in_avail();
+}
+
+template <typename T, enable_if_t<!is_istreamable<T>::value, detail::enabler> = detail::dummy>
+bool from_stream(const std::string & /*istring*/, T & /*obj*/) {
+ return false;
+}
+
+// check to see if an object is a mutable container (fail by default)
+template <typename T, typename _ = void> struct is_mutable_container : std::false_type {};
+
+/// type trait to test if a type is a mutable container meaning it has a value_type, it has an iterator, a clear, and
+/// end methods and an insert function. And for our purposes we exclude std::string and types that can be constructed
+/// from a std::string
+template <typename T>
+struct is_mutable_container<
+ T,
+ conditional_t<false,
+ void_t<typename T::value_type,
+ decltype(std::declval<T>().end()),
+ decltype(std::declval<T>().clear()),
+ decltype(std::declval<T>().insert(std::declval<decltype(std::declval<T>().end())>(),
+ std::declval<const typename T::value_type &>()))>,
+ void>>
+ : public conditional_t<std::is_constructible<T, std::string>::value, std::false_type, std::true_type> {};
+
+// check to see if an object is a mutable container (fail by default)
+template <typename T, typename _ = void> struct is_readable_container : std::false_type {};
+
+/// type trait to test if a type is a container meaning it has a value_type, it has an iterator, a clear, and an end
+/// methods and an insert function. And for our purposes we exclude std::string and types that can be constructed from
+/// a std::string
+template <typename T>
+struct is_readable_container<
+ T,
+ conditional_t<false, void_t<decltype(std::declval<T>().end()), decltype(std::declval<T>().begin())>, void>>
+ : public std::true_type {};
+
+// check to see if an object is a wrapper (fail by default)
+template <typename T, typename _ = void> struct is_wrapper : std::false_type {};
+
+// check if an object is a wrapper (it has a value_type defined)
+template <typename T>
+struct is_wrapper<T, conditional_t<false, void_t<typename T::value_type>, void>> : public std::true_type {};
+
+// Check for tuple like types, as in classes with a tuple_size type trait
+template <typename S> class is_tuple_like {
+ template <typename SS>
+ // static auto test(int)
+ // -> decltype(std::conditional<(std::tuple_size<SS>::value > 0), std::true_type, std::false_type>::type());
+ static auto test(int) -> decltype(std::tuple_size<typename std::decay<SS>::type>::value, std::true_type{});
+ template <typename> static auto test(...) -> std::false_type;
+
+ public:
+ static constexpr bool value = decltype(test<S>(0))::value;
+};
+
+/// Convert an object to a string (directly forward if this can become a string)
+template <typename T, enable_if_t<std::is_convertible<T, std::string>::value, detail::enabler> = detail::dummy>
+auto to_string(T &&value) -> decltype(std::forward<T>(value)) {
+ return std::forward<T>(value);
+}
+
+/// Construct a string from the object
+template <typename T,
+ enable_if_t<std::is_constructible<std::string, T>::value && !std::is_convertible<T, std::string>::value,
+ detail::enabler> = detail::dummy>
+std::string to_string(const T &value) {
+ return std::string(value);
+}
+
+/// Convert an object to a string (streaming must be supported for that type)
+template <typename T,
+ enable_if_t<!std::is_convertible<std::string, T>::value && !std::is_constructible<std::string, T>::value &&
+ is_ostreamable<T>::value,
+ detail::enabler> = detail::dummy>
+std::string to_string(T &&value) {
+ std::stringstream stream;
+ stream << value;
+ return stream.str();
+}
+
+/// If conversion is not supported, return an empty string (streaming is not supported for that type)
+template <typename T,
+ enable_if_t<!std::is_constructible<std::string, T>::value && !is_ostreamable<T>::value &&
+ !is_readable_container<typename std::remove_const<T>::type>::value,
+ detail::enabler> = detail::dummy>
+std::string to_string(T &&) {
+ return std::string{};
+}
+
+/// convert a readable container to a string
+template <typename T,
+ enable_if_t<!std::is_constructible<std::string, T>::value && !is_ostreamable<T>::value &&
+ is_readable_container<T>::value,
+ detail::enabler> = detail::dummy>
+std::string to_string(T &&variable) {
+ auto cval = variable.begin();
+ auto end = variable.end();
+ if(cval == end) {
+ return std::string("{}");
+ }
+ std::vector<std::string> defaults;
+ while(cval != end) {
+ defaults.emplace_back(CLI::detail::to_string(*cval));
+ ++cval;
+ }
+ return std::string("[" + detail::join(defaults) + "]");
+}
+
+/// special template overload
+template <typename T1,
+ typename T2,
+ typename T,
+ enable_if_t<std::is_same<T1, T2>::value, detail::enabler> = detail::dummy>
+auto checked_to_string(T &&value) -> decltype(to_string(std::forward<T>(value))) {
+ return to_string(std::forward<T>(value));
+}
+
+/// special template overload
+template <typename T1,
+ typename T2,
+ typename T,
+ enable_if_t<!std::is_same<T1, T2>::value, detail::enabler> = detail::dummy>
+std::string checked_to_string(T &&) {
+ return std::string{};
+}
+/// get a string as a convertible value for arithmetic types
+template <typename T, enable_if_t<std::is_arithmetic<T>::value, detail::enabler> = detail::dummy>
+std::string value_string(const T &value) {
+ return std::to_string(value);
+}
+/// get a string as a convertible value for enumerations
+template <typename T, enable_if_t<std::is_enum<T>::value, detail::enabler> = detail::dummy>
+std::string value_string(const T &value) {
+ return std::to_string(static_cast<typename std::underlying_type<T>::type>(value));
+}
+/// for other types just use the regular to_string function
+template <typename T,
+ enable_if_t<!std::is_enum<T>::value && !std::is_arithmetic<T>::value, detail::enabler> = detail::dummy>
+auto value_string(const T &value) -> decltype(to_string(value)) {
+ return to_string(value);
+}
+
+/// template to get the underlying value type if it exists or use a default
+template <typename T, typename def, typename Enable = void> struct wrapped_type { using type = def; };
+
+/// Type size for regular object types that do not look like a tuple
+template <typename T, typename def> struct wrapped_type<T, def, typename std::enable_if<is_wrapper<T>::value>::type> {
+ using type = typename T::value_type;
+};
+
+/// This will only trigger for actual void type
+template <typename T, typename Enable = void> struct type_count_base { static const int value{0}; };
+
+/// Type size for regular object types that do not look like a tuple
+template <typename T>
+struct type_count_base<T,
+ typename std::enable_if<!is_tuple_like<T>::value && !is_mutable_container<T>::value &&
+ !std::is_void<T>::value>::type> {
+ static constexpr int value{1};
+};
+
+/// the base tuple size
+template <typename T>
+struct type_count_base<T, typename std::enable_if<is_tuple_like<T>::value && !is_mutable_container<T>::value>::type> {
+ static constexpr int value{std::tuple_size<T>::value};
+};
+
+/// Type count base for containers is the type_count_base of the individual element
+template <typename T> struct type_count_base<T, typename std::enable_if<is_mutable_container<T>::value>::type> {
+ static constexpr int value{type_count_base<typename T::value_type>::value};
+};
+
+/// Set of overloads to get the type size of an object
+
+/// forward declare the subtype_count structure
+template <typename T> struct subtype_count;
+
+/// forward declare the subtype_count_min structure
+template <typename T> struct subtype_count_min;
+
+/// This will only trigger for actual void type
+template <typename T, typename Enable = void> struct type_count { static const int value{0}; };
+
+/// Type size for regular object types that do not look like a tuple
+template <typename T>
+struct type_count<T,
+ typename std::enable_if<!is_wrapper<T>::value && !is_tuple_like<T>::value && !is_complex<T>::value &&
+ !std::is_void<T>::value>::type> {
+ static constexpr int value{1};
+};
+
+/// Type size for complex since it sometimes looks like a wrapper
+template <typename T> struct type_count<T, typename std::enable_if<is_complex<T>::value>::type> {
+ static constexpr int value{2};
+};
+
+/// Type size of types that are wrappers,except complex and tuples(which can also be wrappers sometimes)
+template <typename T> struct type_count<T, typename std::enable_if<is_mutable_container<T>::value>::type> {
+ static constexpr int value{subtype_count<typename T::value_type>::value};
+};
+
+/// Type size of types that are wrappers,except containers complex and tuples(which can also be wrappers sometimes)
+template <typename T>
+struct type_count<T,
+ typename std::enable_if<is_wrapper<T>::value && !is_complex<T>::value && !is_tuple_like<T>::value &&
+ !is_mutable_container<T>::value>::type> {
+ static constexpr int value{type_count<typename T::value_type>::value};
+};
+
+/// 0 if the index > tuple size
+template <typename T, std::size_t I>
+constexpr typename std::enable_if<I == type_count_base<T>::value, int>::type tuple_type_size() {
+ return 0;
+}
+
+/// Recursively generate the tuple type name
+template <typename T, std::size_t I>
+ constexpr typename std::enable_if < I<type_count_base<T>::value, int>::type tuple_type_size() {
+ return subtype_count<typename std::tuple_element<I, T>::type>::value + tuple_type_size<T, I + 1>();
+}
+
+/// Get the type size of the sum of type sizes for all the individual tuple types
+template <typename T> struct type_count<T, typename std::enable_if<is_tuple_like<T>::value>::type> {
+ static constexpr int value{tuple_type_size<T, 0>()};
+};
+
+/// definition of subtype count
+template <typename T> struct subtype_count {
+ static constexpr int value{is_mutable_container<T>::value ? expected_max_vector_size : type_count<T>::value};
+};
+
+/// This will only trigger for actual void type
+template <typename T, typename Enable = void> struct type_count_min { static const int value{0}; };
+
+/// Type size for regular object types that do not look like a tuple
+template <typename T>
+struct type_count_min<
+ T,
+ typename std::enable_if<!is_mutable_container<T>::value && !is_tuple_like<T>::value && !is_wrapper<T>::value &&
+ !is_complex<T>::value && !std::is_void<T>::value>::type> {
+ static constexpr int value{type_count<T>::value};
+};
+
+/// Type size for complex since it sometimes looks like a wrapper
+template <typename T> struct type_count_min<T, typename std::enable_if<is_complex<T>::value>::type> {
+ static constexpr int value{1};
+};
+
+/// Type size min of types that are wrappers,except complex and tuples(which can also be wrappers sometimes)
+template <typename T>
+struct type_count_min<
+ T,
+ typename std::enable_if<is_wrapper<T>::value && !is_complex<T>::value && !is_tuple_like<T>::value>::type> {
+ static constexpr int value{subtype_count_min<typename T::value_type>::value};
+};
+
+/// 0 if the index > tuple size
+template <typename T, std::size_t I>
+constexpr typename std::enable_if<I == type_count_base<T>::value, int>::type tuple_type_size_min() {
+ return 0;
+}
+
+/// Recursively generate the tuple type name
+template <typename T, std::size_t I>
+ constexpr typename std::enable_if < I<type_count_base<T>::value, int>::type tuple_type_size_min() {
+ return subtype_count_min<typename std::tuple_element<I, T>::type>::value + tuple_type_size_min<T, I + 1>();
+}
+
+/// Get the type size of the sum of type sizes for all the individual tuple types
+template <typename T> struct type_count_min<T, typename std::enable_if<is_tuple_like<T>::value>::type> {
+ static constexpr int value{tuple_type_size_min<T, 0>()};
+};
+
+/// definition of subtype count
+template <typename T> struct subtype_count_min {
+ static constexpr int value{is_mutable_container<T>::value
+ ? ((type_count<T>::value < expected_max_vector_size) ? type_count<T>::value : 0)
+ : type_count_min<T>::value};
+};
+
+/// This will only trigger for actual void type
+template <typename T, typename Enable = void> struct expected_count { static const int value{0}; };
+
+/// For most types the number of expected items is 1
+template <typename T>
+struct expected_count<T,
+ typename std::enable_if<!is_mutable_container<T>::value && !is_wrapper<T>::value &&
+ !std::is_void<T>::value>::type> {
+ static constexpr int value{1};
+};
+/// number of expected items in a vector
+template <typename T> struct expected_count<T, typename std::enable_if<is_mutable_container<T>::value>::type> {
+ static constexpr int value{expected_max_vector_size};
+};
+
+/// number of expected items in a vector
+template <typename T>
+struct expected_count<T, typename std::enable_if<!is_mutable_container<T>::value && is_wrapper<T>::value>::type> {
+ static constexpr int value{expected_count<typename T::value_type>::value};
+};
+
+// Enumeration of the different supported categorizations of objects
+enum class object_category : int {
+ char_value = 1,
+ integral_value = 2,
+ unsigned_integral = 4,
+ enumeration = 6,
+ boolean_value = 8,
+ floating_point = 10,
+ number_constructible = 12,
+ double_constructible = 14,
+ integer_constructible = 16,
+ // string like types
+ string_assignable = 23,
+ string_constructible = 24,
+ other = 45,
+ // special wrapper or container types
+ wrapper_value = 50,
+ complex_number = 60,
+ tuple_value = 70,
+ container_value = 80,
+
+};
+
+/// Set of overloads to classify an object according to type
+
+/// some type that is not otherwise recognized
+template <typename T, typename Enable = void> struct classify_object {
+ static constexpr object_category value{object_category::other};
+};
+
+/// Signed integers
+template <typename T>
+struct classify_object<
+ T,
+ typename std::enable_if<std::is_integral<T>::value && !std::is_same<T, char>::value && std::is_signed<T>::value &&
+ !is_bool<T>::value && !std::is_enum<T>::value>::type> {
+ static constexpr object_category value{object_category::integral_value};
+};
+
+/// Unsigned integers
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<std::is_integral<T>::value && std::is_unsigned<T>::value &&
+ !std::is_same<T, char>::value && !is_bool<T>::value>::type> {
+ static constexpr object_category value{object_category::unsigned_integral};
+};
+
+/// single character values
+template <typename T>
+struct classify_object<T, typename std::enable_if<std::is_same<T, char>::value && !std::is_enum<T>::value>::type> {
+ static constexpr object_category value{object_category::char_value};
+};
+
+/// Boolean values
+template <typename T> struct classify_object<T, typename std::enable_if<is_bool<T>::value>::type> {
+ static constexpr object_category value{object_category::boolean_value};
+};
+
+/// Floats
+template <typename T> struct classify_object<T, typename std::enable_if<std::is_floating_point<T>::value>::type> {
+ static constexpr object_category value{object_category::floating_point};
+};
+
+/// String and similar direct assignment
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
+ std::is_assignable<T &, std::string>::value>::type> {
+ static constexpr object_category value{object_category::string_assignable};
+};
+
+/// String and similar constructible and copy assignment
+template <typename T>
+struct classify_object<
+ T,
+ typename std::enable_if<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
+ !std::is_assignable<T &, std::string>::value && (type_count<T>::value == 1) &&
+ std::is_constructible<T, std::string>::value>::type> {
+ static constexpr object_category value{object_category::string_constructible};
+};
+
+/// Enumerations
+template <typename T> struct classify_object<T, typename std::enable_if<std::is_enum<T>::value>::type> {
+ static constexpr object_category value{object_category::enumeration};
+};
+
+template <typename T> struct classify_object<T, typename std::enable_if<is_complex<T>::value>::type> {
+ static constexpr object_category value{object_category::complex_number};
+};
+
+/// Handy helper to contain a bunch of checks that rule out many common types (integers, string like, floating point,
+/// vectors, and enumerations
+template <typename T> struct uncommon_type {
+ using type = typename std::conditional<!std::is_floating_point<T>::value && !std::is_integral<T>::value &&
+ !std::is_assignable<T &, std::string>::value &&
+ !std::is_constructible<T, std::string>::value && !is_complex<T>::value &&
+ !is_mutable_container<T>::value && !std::is_enum<T>::value,
+ std::true_type,
+ std::false_type>::type;
+ static constexpr bool value = type::value;
+};
+
+/// wrapper type
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<(!is_mutable_container<T>::value && is_wrapper<T>::value &&
+ !is_tuple_like<T>::value && uncommon_type<T>::value)>::type> {
+ static constexpr object_category value{object_category::wrapper_value};
+};
+
+/// Assignable from double or int
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<uncommon_type<T>::value && type_count<T>::value == 1 &&
+ !is_wrapper<T>::value && is_direct_constructible<T, double>::value &&
+ is_direct_constructible<T, int>::value>::type> {
+ static constexpr object_category value{object_category::number_constructible};
+};
+
+/// Assignable from int
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<uncommon_type<T>::value && type_count<T>::value == 1 &&
+ !is_wrapper<T>::value && !is_direct_constructible<T, double>::value &&
+ is_direct_constructible<T, int>::value>::type> {
+ static constexpr object_category value{object_category::integer_constructible};
+};
+
+/// Assignable from double
+template <typename T>
+struct classify_object<T,
+ typename std::enable_if<uncommon_type<T>::value && type_count<T>::value == 1 &&
+ !is_wrapper<T>::value && is_direct_constructible<T, double>::value &&
+ !is_direct_constructible<T, int>::value>::type> {
+ static constexpr object_category value{object_category::double_constructible};
+};
+
+/// Tuple type
+template <typename T>
+struct classify_object<
+ T,
+ typename std::enable_if<is_tuple_like<T>::value &&
+ ((type_count<T>::value >= 2 && !is_wrapper<T>::value) ||
+ (uncommon_type<T>::value && !is_direct_constructible<T, double>::value &&
+ !is_direct_constructible<T, int>::value))>::type> {
+ static constexpr object_category value{object_category::tuple_value};
+ // the condition on this class requires it be like a tuple, but on some compilers (like Xcode) tuples can be
+ // constructed from just the first element so tuples of <string, int,int> can be constructed from a string, which
+ // could lead to issues so there are two variants of the condition, the first isolates things with a type size >=2
+ // mainly to get tuples on Xcode with the exception of wrappers, the second is the main one and just separating out
+ // those cases that are caught by other object classifications
+};
+
+/// container type
+template <typename T> struct classify_object<T, typename std::enable_if<is_mutable_container<T>::value>::type> {
+ static constexpr object_category value{object_category::container_value};
+};
+
+// Type name print
+
+/// Was going to be based on
+/// http://stackoverflow.com/questions/1055452/c-get-name-of-type-in-template
+/// But this is cleaner and works better in this case
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::char_value, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "CHAR";
+}
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::integral_value ||
+ classify_object<T>::value == object_category::integer_constructible,
+ detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "INT";
+}
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::unsigned_integral, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "UINT";
+}
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::floating_point ||
+ classify_object<T>::value == object_category::number_constructible ||
+ classify_object<T>::value == object_category::double_constructible,
+ detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "FLOAT";
+}
+
+/// Print name for enumeration types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::enumeration, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "ENUM";
+}
+
+/// Print name for enumeration types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::boolean_value, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "BOOLEAN";
+}
+
+/// Print name for enumeration types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::complex_number, detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "COMPLEX";
+}
+
+/// Print for all other types
+template <typename T,
+ enable_if_t<classify_object<T>::value >= object_category::string_assignable &&
+ classify_object<T>::value <= object_category::other,
+ detail::enabler> = detail::dummy>
+constexpr const char *type_name() {
+ return "TEXT";
+}
+/// typename for tuple value
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::tuple_value && type_count_base<T>::value >= 2,
+ detail::enabler> = detail::dummy>
+std::string type_name(); // forward declaration
+
+/// Generate type name for a wrapper or container value
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::container_value ||
+ classify_object<T>::value == object_category::wrapper_value,
+ detail::enabler> = detail::dummy>
+std::string type_name(); // forward declaration
+
+/// Print name for single element tuple types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::tuple_value && type_count_base<T>::value == 1,
+ detail::enabler> = detail::dummy>
+inline std::string type_name() {
+ return type_name<typename std::decay<typename std::tuple_element<0, T>::type>::type>();
+}
+
+/// Empty string if the index > tuple size
+template <typename T, std::size_t I>
+inline typename std::enable_if<I == type_count_base<T>::value, std::string>::type tuple_name() {
+ return std::string{};
+}
+
+/// Recursively generate the tuple type name
+template <typename T, std::size_t I>
+inline typename std::enable_if<(I < type_count_base<T>::value), std::string>::type tuple_name() {
+ std::string str = std::string(type_name<typename std::decay<typename std::tuple_element<I, T>::type>::type>()) +
+ ',' + tuple_name<T, I + 1>();
+ if(str.back() == ',')
+ str.pop_back();
+ return str;
+}
+
+/// Print type name for tuples with 2 or more elements
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::tuple_value && type_count_base<T>::value >= 2,
+ detail::enabler>>
+inline std::string type_name() {
+ auto tname = std::string(1, '[') + tuple_name<T, 0>();
+ tname.push_back(']');
+ return tname;
+}
+
+/// get the type name for a type that has a value_type member
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::container_value ||
+ classify_object<T>::value == object_category::wrapper_value,
+ detail::enabler>>
+inline std::string type_name() {
+ return type_name<typename T::value_type>();
+}
+
+// Lexical cast
+
+/// Convert to an unsigned integral
+template <typename T, enable_if_t<std::is_unsigned<T>::value, detail::enabler> = detail::dummy>
+bool integral_conversion(const std::string &input, T &output) noexcept {
+ if(input.empty()) {
+ return false;
+ }
+ char *val = nullptr;
+ std::uint64_t output_ll = std::strtoull(input.c_str(), &val, 0);
+ output = static_cast<T>(output_ll);
+ if(val == (input.c_str() + input.size()) && static_cast<std::uint64_t>(output) == output_ll) {
+ return true;
+ }
+ val = nullptr;
+ std::int64_t output_sll = std::strtoll(input.c_str(), &val, 0);
+ if(val == (input.c_str() + input.size())) {
+ output = (output_sll < 0) ? static_cast<T>(0) : static_cast<T>(output_sll);
+ return (static_cast<std::int64_t>(output) == output_sll);
+ }
+ return false;
+}
+
+/// Convert to a signed integral
+template <typename T, enable_if_t<std::is_signed<T>::value, detail::enabler> = detail::dummy>
+bool integral_conversion(const std::string &input, T &output) noexcept {
+ if(input.empty()) {
+ return false;
+ }
+ char *val = nullptr;
+ std::int64_t output_ll = std::strtoll(input.c_str(), &val, 0);
+ output = static_cast<T>(output_ll);
+ if(val == (input.c_str() + input.size()) && static_cast<std::int64_t>(output) == output_ll) {
+ return true;
+ }
+ if(input == "true") {
+ // this is to deal with a few oddities with flags and wrapper int types
+ output = static_cast<T>(1);
+ return true;
+ }
+ return false;
+}
+
+/// Convert a flag into an integer value typically binary flags
+inline std::int64_t to_flag_value(std::string val) {
+ static const std::string trueString("true");
+ static const std::string falseString("false");
+ if(val == trueString) {
+ return 1;
+ }
+ if(val == falseString) {
+ return -1;
+ }
+ val = detail::to_lower(val);
+ std::int64_t ret;
+ if(val.size() == 1) {
+ if(val[0] >= '1' && val[0] <= '9') {
+ return (static_cast<std::int64_t>(val[0]) - '0');
+ }
+ switch(val[0]) {
+ case '0':
+ case 'f':
+ case 'n':
+ case '-':
+ ret = -1;
+ break;
+ case 't':
+ case 'y':
+ case '+':
+ ret = 1;
+ break;
+ default:
+ throw std::invalid_argument("unrecognized character");
+ }
+ return ret;
+ }
+ if(val == trueString || val == "on" || val == "yes" || val == "enable") {
+ ret = 1;
+ } else if(val == falseString || val == "off" || val == "no" || val == "disable") {
+ ret = -1;
+ } else {
+ ret = std::stoll(val);
+ }
+ return ret;
+}
+
+/// Integer conversion
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::integral_value ||
+ classify_object<T>::value == object_category::unsigned_integral,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ return integral_conversion(input, output);
+}
+
+/// char values
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::char_value, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ if(input.size() == 1) {
+ output = static_cast<T>(input[0]);
+ return true;
+ }
+ return integral_conversion(input, output);
+}
+
+/// Boolean values
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::boolean_value, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ try {
+ auto out = to_flag_value(input);
+ output = (out > 0);
+ return true;
+ } catch(const std::invalid_argument &) {
+ return false;
+ } catch(const std::out_of_range &) {
+ // if the number is out of the range of a 64 bit value then it is still a number and for this purpose is still
+ // valid all we care about the sign
+ output = (input[0] != '-');
+ return true;
+ }
+}
+
+/// Floats
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::floating_point, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ if(input.empty()) {
+ return false;
+ }
+ char *val = nullptr;
+ auto output_ld = std::strtold(input.c_str(), &val);
+ output = static_cast<T>(output_ld);
+ return val == (input.c_str() + input.size());
+}
+
+/// complex
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::complex_number, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ using XC = typename wrapped_type<T, double>::type;
+ XC x{0.0}, y{0.0};
+ auto str1 = input;
+ bool worked = false;
+ auto nloc = str1.find_last_of("+-");
+ if(nloc != std::string::npos && nloc > 0) {
+ worked = detail::lexical_cast(str1.substr(0, nloc), x);
+ str1 = str1.substr(nloc);
+ if(str1.back() == 'i' || str1.back() == 'j')
+ str1.pop_back();
+ worked = worked && detail::lexical_cast(str1, y);
+ } else {
+ if(str1.back() == 'i' || str1.back() == 'j') {
+ str1.pop_back();
+ worked = detail::lexical_cast(str1, y);
+ x = XC{0};
+ } else {
+ worked = detail::lexical_cast(str1, x);
+ y = XC{0};
+ }
+ }
+ if(worked) {
+ output = T{x, y};
+ return worked;
+ }
+ return from_stream(input, output);
+}
+
+/// String and similar direct assignment
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::string_assignable, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ output = input;
+ return true;
+}
+
+/// String and similar constructible and copy assignment
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::string_constructible, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ output = T(input);
+ return true;
+}
+
+/// Enumerations
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::enumeration, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ typename std::underlying_type<T>::type val;
+ if(!integral_conversion(input, val)) {
+ return false;
+ }
+ output = static_cast<T>(val);
+ return true;
+}
+
+/// wrapper types
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::wrapper_value &&
+ std::is_assignable<T &, typename T::value_type>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ typename T::value_type val;
+ if(lexical_cast(input, val)) {
+ output = val;
+ return true;
+ }
+ return from_stream(input, output);
+}
+
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::wrapper_value &&
+ !std::is_assignable<T &, typename T::value_type>::value && std::is_assignable<T &, T>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ typename T::value_type val;
+ if(lexical_cast(input, val)) {
+ output = T{val};
+ return true;
+ }
+ return from_stream(input, output);
+}
+
+/// Assignable from double or int
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::number_constructible, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ int val;
+ if(integral_conversion(input, val)) {
+ output = T(val);
+ return true;
+ } else {
+ double dval;
+ if(lexical_cast(input, dval)) {
+ output = T{dval};
+ return true;
+ }
+ }
+ return from_stream(input, output);
+}
+
+/// Assignable from int
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::integer_constructible, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ int val;
+ if(integral_conversion(input, val)) {
+ output = T(val);
+ return true;
+ }
+ return from_stream(input, output);
+}
+
+/// Assignable from double
+template <
+ typename T,
+ enable_if_t<classify_object<T>::value == object_category::double_constructible, detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ double val;
+ if(lexical_cast(input, val)) {
+ output = T{val};
+ return true;
+ }
+ return from_stream(input, output);
+}
+
+/// Non-string convertible from an int
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::other && std::is_assignable<T &, int>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ int val;
+ if(integral_conversion(input, val)) {
+#ifdef _MSC_VER
+#pragma warning(push)
+#pragma warning(disable : 4800)
+#endif
+ // with Atomic<XX> this could produce a warning due to the conversion but if atomic gets here it is an old style
+ // so will most likely still work
+ output = val;
+#ifdef _MSC_VER
+#pragma warning(pop)
+#endif
+ return true;
+ }
+ // LCOV_EXCL_START
+ // This version of cast is only used for odd cases in an older compilers the fail over
+ // from_stream is tested elsewhere an not relevant for coverage here
+ return from_stream(input, output);
+ // LCOV_EXCL_STOP
+}
+
+/// Non-string parsable by a stream
+template <typename T,
+ enable_if_t<classify_object<T>::value == object_category::other && !std::is_assignable<T &, int>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_cast(const std::string &input, T &output) {
+ static_assert(is_istreamable<T>::value,
+ "option object type must have a lexical cast overload or streaming input operator(>>) defined, if it "
+ "is convertible from another type use the add_option<T, XC>(...) with XC being the known type");
+ return from_stream(input, output);
+}
+
+/// Assign a value through lexical cast operations
+/// Strings can be empty so we need to do a little different
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<std::is_same<AssignTo, ConvertTo>::value &&
+ (classify_object<AssignTo>::value == object_category::string_assignable ||
+ classify_object<AssignTo>::value == object_category::string_constructible),
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ return lexical_cast(input, output);
+}
+
+/// Assign a value through lexical cast operations
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<std::is_same<AssignTo, ConvertTo>::value && std::is_assignable<AssignTo &, AssignTo>::value &&
+ classify_object<AssignTo>::value != object_category::string_assignable &&
+ classify_object<AssignTo>::value != object_category::string_constructible,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ if(input.empty()) {
+ output = AssignTo{};
+ return true;
+ }
+
+ return lexical_cast(input, output);
+}
+
+/// Assign a value through lexical cast operations
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<std::is_same<AssignTo, ConvertTo>::value && !std::is_assignable<AssignTo &, AssignTo>::value &&
+ classify_object<AssignTo>::value == object_category::wrapper_value,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ if(input.empty()) {
+ typename AssignTo::value_type emptyVal{};
+ output = emptyVal;
+ return true;
+ }
+ return lexical_cast(input, output);
+}
+
+/// Assign a value through lexical cast operations for int compatible values
+/// mainly for atomic operations on some compilers
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<std::is_same<AssignTo, ConvertTo>::value && !std::is_assignable<AssignTo &, AssignTo>::value &&
+ classify_object<AssignTo>::value != object_category::wrapper_value &&
+ std::is_assignable<AssignTo &, int>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ if(input.empty()) {
+ output = 0;
+ return true;
+ }
+ int val;
+ if(lexical_cast(input, val)) {
+ output = val;
+ return true;
+ }
+ return false;
+}
+
+/// Assign a value converted from a string in lexical cast to the output value directly
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<!std::is_same<AssignTo, ConvertTo>::value && std::is_assignable<AssignTo &, ConvertTo &>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ ConvertTo val{};
+ bool parse_result = (!input.empty()) ? lexical_cast<ConvertTo>(input, val) : true;
+ if(parse_result) {
+ output = val;
+ }
+ return parse_result;
+}
+
+/// Assign a value from a lexical cast through constructing a value and move assigning it
+template <
+ typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<!std::is_same<AssignTo, ConvertTo>::value && !std::is_assignable<AssignTo &, ConvertTo &>::value &&
+ std::is_move_assignable<AssignTo>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_assign(const std::string &input, AssignTo &output) {
+ ConvertTo val{};
+ bool parse_result = input.empty() ? true : lexical_cast<ConvertTo>(input, val);
+ if(parse_result) {
+ output = AssignTo(val); // use () form of constructor to allow some implicit conversions
+ }
+ return parse_result;
+}
+
+/// primary lexical conversion operation, 1 string to 1 type of some kind
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<classify_object<ConvertTo>::value <= object_category::other &&
+ classify_object<AssignTo>::value <= object_category::wrapper_value,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ return lexical_assign<AssignTo, ConvertTo>(strings[0], output);
+}
+
+/// Lexical conversion if there is only one element but the conversion type is for two, then call a two element
+/// constructor
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<(type_count<AssignTo>::value <= 2) && expected_count<AssignTo>::value == 1 &&
+ is_tuple_like<ConvertTo>::value && type_count_base<ConvertTo>::value == 2,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ // the remove const is to handle pair types coming from a container
+ typename std::remove_const<typename std::tuple_element<0, ConvertTo>::type>::type v1;
+ typename std::tuple_element<1, ConvertTo>::type v2;
+ bool retval = lexical_assign<decltype(v1), decltype(v1)>(strings[0], v1);
+ if(strings.size() > 1) {
+ retval = retval && lexical_assign<decltype(v2), decltype(v2)>(strings[1], v2);
+ }
+ if(retval) {
+ output = AssignTo{v1, v2};
+ }
+ return retval;
+}
+
+/// Lexical conversion of a container types of single elements
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count<ConvertTo>::value == 1,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ output.erase(output.begin(), output.end());
+ if(strings.size() == 1 && strings[0] == "{}") {
+ return true;
+ }
+ bool skip_remaining = false;
+ if(strings.size() == 2 && strings[0] == "{}" && is_separator(strings[1])) {
+ skip_remaining = true;
+ }
+ for(const auto &elem : strings) {
+ typename AssignTo::value_type out;
+ bool retval = lexical_assign<typename AssignTo::value_type, typename ConvertTo::value_type>(elem, out);
+ if(!retval) {
+ return false;
+ }
+ output.insert(output.end(), std::move(out));
+ if(skip_remaining) {
+ break;
+ }
+ }
+ return (!output.empty());
+}
+
+/// Lexical conversion for complex types
+template <class AssignTo, class ConvertTo, enable_if_t<is_complex<ConvertTo>::value, detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output) {
+
+ if(strings.size() >= 2 && !strings[1].empty()) {
+ using XC2 = typename wrapped_type<ConvertTo, double>::type;
+ XC2 x{0.0}, y{0.0};
+ auto str1 = strings[1];
+ if(str1.back() == 'i' || str1.back() == 'j') {
+ str1.pop_back();
+ }
+ auto worked = detail::lexical_cast(strings[0], x) && detail::lexical_cast(str1, y);
+ if(worked) {
+ output = ConvertTo{x, y};
+ }
+ return worked;
+ } else {
+ return lexical_assign<AssignTo, ConvertTo>(strings[0], output);
+ }
+}
+
+/// Conversion to a vector type using a particular single type as the conversion type
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && (expected_count<ConvertTo>::value == 1) &&
+ (type_count<ConvertTo>::value == 1),
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ bool retval = true;
+ output.clear();
+ output.reserve(strings.size());
+ for(const auto &elem : strings) {
+
+ output.emplace_back();
+ retval = retval && lexical_assign<typename AssignTo::value_type, ConvertTo>(elem, output.back());
+ }
+ return (!output.empty()) && retval;
+}
+
+// forward declaration
+
+/// Lexical conversion of a container types with conversion type of two elements
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count_base<ConvertTo>::value == 2,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(std::vector<std::string> strings, AssignTo &output);
+
+/// Lexical conversion of a vector types with type_size >2 forward declaration
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count_base<ConvertTo>::value != 2 &&
+ ((type_count<ConvertTo>::value > 2) ||
+ (type_count<ConvertTo>::value > type_count_base<ConvertTo>::value)),
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output);
+
+/// Conversion for tuples
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_tuple_like<AssignTo>::value && is_tuple_like<ConvertTo>::value &&
+ (type_count_base<ConvertTo>::value != type_count<ConvertTo>::value ||
+ type_count<ConvertTo>::value > 2),
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output); // forward declaration
+
+/// Conversion for operations where the assigned type is some class but the conversion is a mutable container or large
+/// tuple
+template <typename AssignTo,
+ typename ConvertTo,
+ enable_if_t<!is_tuple_like<AssignTo>::value && !is_mutable_container<AssignTo>::value &&
+ classify_object<ConvertTo>::value != object_category::wrapper_value &&
+ (is_mutable_container<ConvertTo>::value || type_count<ConvertTo>::value > 2),
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+
+ if(strings.size() > 1 || (!strings.empty() && !(strings.front().empty()))) {
+ ConvertTo val;
+ auto retval = lexical_conversion<ConvertTo, ConvertTo>(strings, val);
+ output = AssignTo{val};
+ return retval;
+ }
+ output = AssignTo{};
+ return true;
+}
+
+/// function template for converting tuples if the static Index is greater than the tuple size
+template <class AssignTo, class ConvertTo, std::size_t I>
+inline typename std::enable_if<(I >= type_count_base<AssignTo>::value), bool>::type
+tuple_conversion(const std::vector<std::string> &, AssignTo &) {
+ return true;
+}
+
+/// Conversion of a tuple element where the type size ==1 and not a mutable container
+template <class AssignTo, class ConvertTo>
+inline typename std::enable_if<!is_mutable_container<ConvertTo>::value && type_count<ConvertTo>::value == 1, bool>::type
+tuple_type_conversion(std::vector<std::string> &strings, AssignTo &output) {
+ auto retval = lexical_assign<AssignTo, ConvertTo>(strings[0], output);
+ strings.erase(strings.begin());
+ return retval;
+}
+
+/// Conversion of a tuple element where the type size !=1 but the size is fixed and not a mutable container
+template <class AssignTo, class ConvertTo>
+inline typename std::enable_if<!is_mutable_container<ConvertTo>::value && (type_count<ConvertTo>::value > 1) &&
+ type_count<ConvertTo>::value == type_count_min<ConvertTo>::value,
+ bool>::type
+tuple_type_conversion(std::vector<std::string> &strings, AssignTo &output) {
+ auto retval = lexical_conversion<AssignTo, ConvertTo>(strings, output);
+ strings.erase(strings.begin(), strings.begin() + type_count<ConvertTo>::value);
+ return retval;
+}
+
+/// Conversion of a tuple element where the type is a mutable container or a type with different min and max type sizes
+template <class AssignTo, class ConvertTo>
+inline typename std::enable_if<is_mutable_container<ConvertTo>::value ||
+ type_count<ConvertTo>::value != type_count_min<ConvertTo>::value,
+ bool>::type
+tuple_type_conversion(std::vector<std::string> &strings, AssignTo &output) {
+
+ std::size_t index{subtype_count_min<ConvertTo>::value};
+ const std::size_t mx_count{subtype_count<ConvertTo>::value};
+ const std::size_t mx{(std::max)(mx_count, strings.size())};
+
+ while(index < mx) {
+ if(is_separator(strings[index])) {
+ break;
+ }
+ ++index;
+ }
+ bool retval = lexical_conversion<AssignTo, ConvertTo>(
+ std::vector<std::string>(strings.begin(), strings.begin() + static_cast<std::ptrdiff_t>(index)), output);
+ strings.erase(strings.begin(), strings.begin() + static_cast<std::ptrdiff_t>(index) + 1);
+ return retval;
+}
+
+/// Tuple conversion operation
+template <class AssignTo, class ConvertTo, std::size_t I>
+inline typename std::enable_if<(I < type_count_base<AssignTo>::value), bool>::type
+tuple_conversion(std::vector<std::string> strings, AssignTo &output) {
+ bool retval = true;
+ using ConvertToElement = typename std::
+ conditional<is_tuple_like<ConvertTo>::value, typename std::tuple_element<I, ConvertTo>::type, ConvertTo>::type;
+ if(!strings.empty()) {
+ retval = retval && tuple_type_conversion<typename std::tuple_element<I, AssignTo>::type, ConvertToElement>(
+ strings, std::get<I>(output));
+ }
+ retval = retval && tuple_conversion<AssignTo, ConvertTo, I + 1>(std::move(strings), output);
+ return retval;
+}
+
+/// Lexical conversion of a container types with tuple elements of size 2
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count_base<ConvertTo>::value == 2,
+ detail::enabler>>
+bool lexical_conversion(std::vector<std::string> strings, AssignTo &output) {
+ output.clear();
+ while(!strings.empty()) {
+
+ typename std::remove_const<typename std::tuple_element<0, typename ConvertTo::value_type>::type>::type v1;
+ typename std::tuple_element<1, typename ConvertTo::value_type>::type v2;
+ bool retval = tuple_type_conversion<decltype(v1), decltype(v1)>(strings, v1);
+ if(!strings.empty()) {
+ retval = retval && tuple_type_conversion<decltype(v2), decltype(v2)>(strings, v2);
+ }
+ if(retval) {
+ output.insert(output.end(), typename AssignTo::value_type{v1, v2});
+ } else {
+ return false;
+ }
+ }
+ return (!output.empty());
+}
+
+/// lexical conversion of tuples with type count>2 or tuples of types of some element with a type size>=2
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_tuple_like<AssignTo>::value && is_tuple_like<ConvertTo>::value &&
+ (type_count_base<ConvertTo>::value != type_count<ConvertTo>::value ||
+ type_count<ConvertTo>::value > 2),
+ detail::enabler>>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ static_assert(
+ !is_tuple_like<ConvertTo>::value || type_count_base<AssignTo>::value == type_count_base<ConvertTo>::value,
+ "if the conversion type is defined as a tuple it must be the same size as the type you are converting to");
+ return tuple_conversion<AssignTo, ConvertTo, 0>(strings, output);
+}
+
+/// Lexical conversion of a vector types for everything but tuples of two elements and types of size 1
+template <class AssignTo,
+ class ConvertTo,
+ enable_if_t<is_mutable_container<AssignTo>::value && is_mutable_container<ConvertTo>::value &&
+ type_count_base<ConvertTo>::value != 2 &&
+ ((type_count<ConvertTo>::value > 2) ||
+ (type_count<ConvertTo>::value > type_count_base<ConvertTo>::value)),
+ detail::enabler>>
+bool lexical_conversion(const std::vector<std ::string> &strings, AssignTo &output) {
+ bool retval = true;
+ output.clear();
+ std::vector<std::string> temp;
+ std::size_t ii{0};
+ std::size_t icount{0};
+ std::size_t xcm{type_count<ConvertTo>::value};
+ auto ii_max = strings.size();
+ while(ii < ii_max) {
+ temp.push_back(strings[ii]);
+ ++ii;
+ ++icount;
+ if(icount == xcm || is_separator(temp.back()) || ii == ii_max) {
+ if(static_cast<int>(xcm) > type_count_min<ConvertTo>::value && is_separator(temp.back())) {
+ temp.pop_back();
+ }
+ typename AssignTo::value_type temp_out;
+ retval = retval &&
+ lexical_conversion<typename AssignTo::value_type, typename ConvertTo::value_type>(temp, temp_out);
+ temp.clear();
+ if(!retval) {
+ return false;
+ }
+ output.insert(output.end(), std::move(temp_out));
+ icount = 0;
+ }
+ }
+ return retval;
+}
+
+/// conversion for wrapper types
+template <typename AssignTo,
+ class ConvertTo,
+ enable_if_t<classify_object<ConvertTo>::value == object_category::wrapper_value &&
+ std::is_assignable<ConvertTo &, ConvertTo>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output) {
+ if(strings.empty() || strings.front().empty()) {
+ output = ConvertTo{};
+ return true;
+ }
+ typename ConvertTo::value_type val;
+ if(lexical_conversion<typename ConvertTo::value_type, typename ConvertTo::value_type>(strings, val)) {
+ output = ConvertTo{val};
+ return true;
+ }
+ return false;
+}
+
+/// conversion for wrapper types
+template <typename AssignTo,
+ class ConvertTo,
+ enable_if_t<classify_object<ConvertTo>::value == object_category::wrapper_value &&
+ !std::is_assignable<AssignTo &, ConvertTo>::value,
+ detail::enabler> = detail::dummy>
+bool lexical_conversion(const std::vector<std::string> &strings, AssignTo &output) {
+ using ConvertType = typename ConvertTo::value_type;
+ if(strings.empty() || strings.front().empty()) {
+ output = ConvertType{};
+ return true;
+ }
+ ConvertType val;
+ if(lexical_conversion<typename ConvertTo::value_type, typename ConvertTo::value_type>(strings, val)) {
+ output = val;
+ return true;
+ }
+ return false;
+}
+
+/// Sum a vector of strings
+inline std::string sum_string_vector(const std::vector<std::string> &values) {
+ double val{0.0};
+ bool fail{false};
+ std::string output;
+ for(const auto &arg : values) {
+ double tv{0.0};
+ auto comp = detail::lexical_cast<double>(arg, tv);
+ if(!comp) {
+ try {
+ tv = static_cast<double>(detail::to_flag_value(arg));
+ } catch(const std::exception &) {
+ fail = true;
+ break;
+ }
+ }
+ val += tv;
+ }
+ if(fail) {
+ for(const auto &arg : values) {
+ output.append(arg);
+ }
+ } else {
+ if(val <= static_cast<double>(std::numeric_limits<std::int64_t>::min()) ||
+ val >= static_cast<double>(std::numeric_limits<std::int64_t>::max()) ||
+ val == static_cast<std::int64_t>(val)) {
+ output = detail::value_string(static_cast<int64_t>(val));
+ } else {
+ output = detail::value_string(val);
+ }
+ }
+ return output;
+}
+
+} // namespace detail
+// [CLI11:type_tools_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/Validators.hpp b/src/third-party/CLI/Validators.hpp
new file mode 100644
index 0000000..1281997
--- /dev/null
+++ b/src/third-party/CLI/Validators.hpp
@@ -0,0 +1,1175 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+#include "Macros.hpp"
+#include "StringTools.hpp"
+#include "TypeTools.hpp"
+
+// [CLI11:public_includes:set]
+#include <cmath>
+#include <cstdint>
+#include <functional>
+#include <iostream>
+#include <limits>
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+// [CLI11:public_includes:end]
+
+// [CLI11:validators_hpp_filesystem:verbatim]
+
+// C standard library
+// Only needed for existence checking
+#if defined CLI11_CPP17 && defined __has_include && !defined CLI11_HAS_FILESYSTEM
+#if __has_include(<filesystem>)
+// Filesystem cannot be used if targeting macOS < 10.15
+#if defined __MAC_OS_X_VERSION_MIN_REQUIRED && __MAC_OS_X_VERSION_MIN_REQUIRED < 101500
+#define CLI11_HAS_FILESYSTEM 0
+#elif defined(__wasi__)
+// As of wasi-sdk-14, filesystem is not implemented
+#define CLI11_HAS_FILESYSTEM 0
+#else
+#include <filesystem>
+#if defined __cpp_lib_filesystem && __cpp_lib_filesystem >= 201703
+#if defined _GLIBCXX_RELEASE && _GLIBCXX_RELEASE >= 9
+#define CLI11_HAS_FILESYSTEM 1
+#elif defined(__GLIBCXX__)
+// if we are using gcc and Version <9 default to no filesystem
+#define CLI11_HAS_FILESYSTEM 0
+#else
+#define CLI11_HAS_FILESYSTEM 1
+#endif
+#else
+#define CLI11_HAS_FILESYSTEM 0
+#endif
+#endif
+#endif
+#endif
+
+#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
+#include <filesystem> // NOLINT(build/include)
+#else
+#include <sys/stat.h>
+#include <sys/types.h>
+#endif
+
+// [CLI11:validators_hpp_filesystem:end]
+
+namespace CLI {
+// [CLI11:validators_hpp:verbatim]
+
+class Option;
+
+/// @defgroup validator_group Validators
+
+/// @brief Some validators that are provided
+///
+/// These are simple `std::string(const std::string&)` validators that are useful. They return
+/// a string if the validation fails. A custom struct is provided, as well, with the same user
+/// semantics, but with the ability to provide a new type name.
+/// @{
+
+///
+class Validator {
+ protected:
+ /// This is the description function, if empty the description_ will be used
+ std::function<std::string()> desc_function_{[]() { return std::string{}; }};
+
+ /// This is the base function that is to be called.
+ /// Returns a string error message if validation fails.
+ std::function<std::string(std::string &)> func_{[](std::string &) { return std::string{}; }};
+ /// The name for search purposes of the Validator
+ std::string name_{};
+ /// A Validator will only apply to an indexed value (-1 is all elements)
+ int application_index_ = -1;
+ /// Enable for Validator to allow it to be disabled if need be
+ bool active_{true};
+ /// specify that a validator should not modify the input
+ bool non_modifying_{false};
+
+ public:
+ Validator() = default;
+ /// Construct a Validator with just the description string
+ explicit Validator(std::string validator_desc) : desc_function_([validator_desc]() { return validator_desc; }) {}
+ /// Construct Validator from basic information
+ Validator(std::function<std::string(std::string &)> op, std::string validator_desc, std::string validator_name = "")
+ : desc_function_([validator_desc]() { return validator_desc; }), func_(std::move(op)),
+ name_(std::move(validator_name)) {}
+ /// Set the Validator operation function
+ Validator &operation(std::function<std::string(std::string &)> op) {
+ func_ = std::move(op);
+ return *this;
+ }
+ /// This is the required operator for a Validator - provided to help
+ /// users (CLI11 uses the member `func` directly)
+ std::string operator()(std::string &str) const {
+ std::string retstring;
+ if(active_) {
+ if(non_modifying_) {
+ std::string value = str;
+ retstring = func_(value);
+ } else {
+ retstring = func_(str);
+ }
+ }
+ return retstring;
+ }
+
+ /// This is the required operator for a Validator - provided to help
+ /// users (CLI11 uses the member `func` directly)
+ std::string operator()(const std::string &str) const {
+ std::string value = str;
+ return (active_) ? func_(value) : std::string{};
+ }
+
+ /// Specify the type string
+ Validator &description(std::string validator_desc) {
+ desc_function_ = [validator_desc]() { return validator_desc; };
+ return *this;
+ }
+ /// Specify the type string
+ Validator description(std::string validator_desc) const {
+ Validator newval(*this);
+ newval.desc_function_ = [validator_desc]() { return validator_desc; };
+ return newval;
+ }
+ /// Generate type description information for the Validator
+ std::string get_description() const {
+ if(active_) {
+ return desc_function_();
+ }
+ return std::string{};
+ }
+ /// Specify the type string
+ Validator &name(std::string validator_name) {
+ name_ = std::move(validator_name);
+ return *this;
+ }
+ /// Specify the type string
+ Validator name(std::string validator_name) const {
+ Validator newval(*this);
+ newval.name_ = std::move(validator_name);
+ return newval;
+ }
+ /// Get the name of the Validator
+ const std::string &get_name() const { return name_; }
+ /// Specify whether the Validator is active or not
+ Validator &active(bool active_val = true) {
+ active_ = active_val;
+ return *this;
+ }
+ /// Specify whether the Validator is active or not
+ Validator active(bool active_val = true) const {
+ Validator newval(*this);
+ newval.active_ = active_val;
+ return newval;
+ }
+
+ /// Specify whether the Validator can be modifying or not
+ Validator &non_modifying(bool no_modify = true) {
+ non_modifying_ = no_modify;
+ return *this;
+ }
+ /// Specify the application index of a validator
+ Validator &application_index(int app_index) {
+ application_index_ = app_index;
+ return *this;
+ }
+ /// Specify the application index of a validator
+ Validator application_index(int app_index) const {
+ Validator newval(*this);
+ newval.application_index_ = app_index;
+ return newval;
+ }
+ /// Get the current value of the application index
+ int get_application_index() const { return application_index_; }
+ /// Get a boolean if the validator is active
+ bool get_active() const { return active_; }
+
+ /// Get a boolean if the validator is allowed to modify the input returns true if it can modify the input
+ bool get_modifying() const { return !non_modifying_; }
+
+ /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the
+ /// same.
+ Validator operator&(const Validator &other) const {
+ Validator newval;
+
+ newval._merge_description(*this, other, " AND ");
+
+ // Give references (will make a copy in lambda function)
+ const std::function<std::string(std::string & filename)> &f1 = func_;
+ const std::function<std::string(std::string & filename)> &f2 = other.func_;
+
+ newval.func_ = [f1, f2](std::string &input) {
+ std::string s1 = f1(input);
+ std::string s2 = f2(input);
+ if(!s1.empty() && !s2.empty())
+ return std::string("(") + s1 + ") AND (" + s2 + ")";
+ else
+ return s1 + s2;
+ };
+
+ newval.active_ = (active_ & other.active_);
+ newval.application_index_ = application_index_;
+ return newval;
+ }
+
+ /// Combining validators is a new validator. Type comes from left validator if function, otherwise only set if the
+ /// same.
+ Validator operator|(const Validator &other) const {
+ Validator newval;
+
+ newval._merge_description(*this, other, " OR ");
+
+ // Give references (will make a copy in lambda function)
+ const std::function<std::string(std::string &)> &f1 = func_;
+ const std::function<std::string(std::string &)> &f2 = other.func_;
+
+ newval.func_ = [f1, f2](std::string &input) {
+ std::string s1 = f1(input);
+ std::string s2 = f2(input);
+ if(s1.empty() || s2.empty())
+ return std::string();
+
+ return std::string("(") + s1 + ") OR (" + s2 + ")";
+ };
+ newval.active_ = (active_ & other.active_);
+ newval.application_index_ = application_index_;
+ return newval;
+ }
+
+ /// Create a validator that fails when a given validator succeeds
+ Validator operator!() const {
+ Validator newval;
+ const std::function<std::string()> &dfunc1 = desc_function_;
+ newval.desc_function_ = [dfunc1]() {
+ auto str = dfunc1();
+ return (!str.empty()) ? std::string("NOT ") + str : std::string{};
+ };
+ // Give references (will make a copy in lambda function)
+ const std::function<std::string(std::string & res)> &f1 = func_;
+
+ newval.func_ = [f1, dfunc1](std::string &test) -> std::string {
+ std::string s1 = f1(test);
+ if(s1.empty()) {
+ return std::string("check ") + dfunc1() + " succeeded improperly";
+ }
+ return std::string{};
+ };
+ newval.active_ = active_;
+ newval.application_index_ = application_index_;
+ return newval;
+ }
+
+ private:
+ void _merge_description(const Validator &val1, const Validator &val2, const std::string &merger) {
+
+ const std::function<std::string()> &dfunc1 = val1.desc_function_;
+ const std::function<std::string()> &dfunc2 = val2.desc_function_;
+
+ desc_function_ = [=]() {
+ std::string f1 = dfunc1();
+ std::string f2 = dfunc2();
+ if((f1.empty()) || (f2.empty())) {
+ return f1 + f2;
+ }
+ return std::string(1, '(') + f1 + ')' + merger + '(' + f2 + ')';
+ };
+ }
+}; // namespace CLI
+
+/// Class wrapping some of the accessors of Validator
+class CustomValidator : public Validator {
+ public:
+};
+// The implementation of the built in validators is using the Validator class;
+// the user is only expected to use the const (static) versions (since there's no setup).
+// Therefore, this is in detail.
+namespace detail {
+
+/// CLI enumeration of different file types
+enum class path_type { nonexistent, file, directory };
+
+#if defined CLI11_HAS_FILESYSTEM && CLI11_HAS_FILESYSTEM > 0
+/// get the type of the path from a file name
+inline path_type check_path(const char *file) noexcept {
+ std::error_code ec;
+ auto stat = std::filesystem::status(file, ec);
+ if(ec) {
+ return path_type::nonexistent;
+ }
+ switch(stat.type()) {
+ case std::filesystem::file_type::none:
+ case std::filesystem::file_type::not_found:
+ return path_type::nonexistent;
+ case std::filesystem::file_type::directory:
+ return path_type::directory;
+ case std::filesystem::file_type::symlink:
+ case std::filesystem::file_type::block:
+ case std::filesystem::file_type::character:
+ case std::filesystem::file_type::fifo:
+ case std::filesystem::file_type::socket:
+ case std::filesystem::file_type::regular:
+ case std::filesystem::file_type::unknown:
+ default:
+ return path_type::file;
+ }
+}
+#else
+/// get the type of the path from a file name
+inline path_type check_path(const char *file) noexcept {
+#if defined(_MSC_VER)
+ struct __stat64 buffer;
+ if(_stat64(file, &buffer) == 0) {
+ return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file;
+ }
+#else
+ struct stat buffer;
+ if(stat(file, &buffer) == 0) {
+ return ((buffer.st_mode & S_IFDIR) != 0) ? path_type::directory : path_type::file;
+ }
+#endif
+ return path_type::nonexistent;
+}
+#endif
+/// Check for an existing file (returns error message if check fails)
+class ExistingFileValidator : public Validator {
+ public:
+ ExistingFileValidator() : Validator("FILE") {
+ func_ = [](std::string &filename) {
+ auto path_result = check_path(filename.c_str());
+ if(path_result == path_type::nonexistent) {
+ return "File does not exist: " + filename;
+ }
+ if(path_result == path_type::directory) {
+ return "File is actually a directory: " + filename;
+ }
+ return std::string();
+ };
+ }
+};
+
+/// Check for an existing directory (returns error message if check fails)
+class ExistingDirectoryValidator : public Validator {
+ public:
+ ExistingDirectoryValidator() : Validator("DIR") {
+ func_ = [](std::string &filename) {
+ auto path_result = check_path(filename.c_str());
+ if(path_result == path_type::nonexistent) {
+ return "Directory does not exist: " + filename;
+ }
+ if(path_result == path_type::file) {
+ return "Directory is actually a file: " + filename;
+ }
+ return std::string();
+ };
+ }
+};
+
+/// Check for an existing path
+class ExistingPathValidator : public Validator {
+ public:
+ ExistingPathValidator() : Validator("PATH(existing)") {
+ func_ = [](std::string &filename) {
+ auto path_result = check_path(filename.c_str());
+ if(path_result == path_type::nonexistent) {
+ return "Path does not exist: " + filename;
+ }
+ return std::string();
+ };
+ }
+};
+
+/// Check for an non-existing path
+class NonexistentPathValidator : public Validator {
+ public:
+ NonexistentPathValidator() : Validator("PATH(non-existing)") {
+ func_ = [](std::string &filename) {
+ auto path_result = check_path(filename.c_str());
+ if(path_result != path_type::nonexistent) {
+ return "Path already exists: " + filename;
+ }
+ return std::string();
+ };
+ }
+};
+
+/// Validate the given string is a legal ipv4 address
+class IPV4Validator : public Validator {
+ public:
+ IPV4Validator() : Validator("IPV4") {
+ func_ = [](std::string &ip_addr) {
+ auto result = CLI::detail::split(ip_addr, '.');
+ if(result.size() != 4) {
+ return std::string("Invalid IPV4 address must have four parts (") + ip_addr + ')';
+ }
+ int num;
+ for(const auto &var : result) {
+ bool retval = detail::lexical_cast(var, num);
+ if(!retval) {
+ return std::string("Failed parsing number (") + var + ')';
+ }
+ if(num < 0 || num > 255) {
+ return std::string("Each IP number must be between 0 and 255 ") + var;
+ }
+ }
+ return std::string();
+ };
+ }
+};
+
+} // namespace detail
+
+// Static is not needed here, because global const implies static.
+
+/// Check for existing file (returns error message if check fails)
+const detail::ExistingFileValidator ExistingFile;
+
+/// Check for an existing directory (returns error message if check fails)
+const detail::ExistingDirectoryValidator ExistingDirectory;
+
+/// Check for an existing path
+const detail::ExistingPathValidator ExistingPath;
+
+/// Check for an non-existing path
+const detail::NonexistentPathValidator NonexistentPath;
+
+/// Check for an IP4 address
+const detail::IPV4Validator ValidIPV4;
+
+/// Validate the input as a particular type
+template <typename DesiredType> class TypeValidator : public Validator {
+ public:
+ explicit TypeValidator(const std::string &validator_name) : Validator(validator_name) {
+ func_ = [](std::string &input_string) {
+ auto val = DesiredType();
+ if(!detail::lexical_cast(input_string, val)) {
+ return std::string("Failed parsing ") + input_string + " as a " + detail::type_name<DesiredType>();
+ }
+ return std::string();
+ };
+ }
+ TypeValidator() : TypeValidator(detail::type_name<DesiredType>()) {}
+};
+
+/// Check for a number
+const TypeValidator<double> Number("NUMBER");
+
+/// Modify a path if the file is a particular default location, can be used as Check or transform
+/// with the error return optionally disabled
+class FileOnDefaultPath : public Validator {
+ public:
+ explicit FileOnDefaultPath(std::string default_path, bool enableErrorReturn = true) : Validator("FILE") {
+ func_ = [default_path, enableErrorReturn](std::string &filename) {
+ auto path_result = detail::check_path(filename.c_str());
+ if(path_result == detail::path_type::nonexistent) {
+ std::string test_file_path = default_path;
+ if(default_path.back() != '/' && default_path.back() != '\\') {
+ // Add folder separator
+ test_file_path += '/';
+ }
+ test_file_path.append(filename);
+ path_result = detail::check_path(test_file_path.c_str());
+ if(path_result == detail::path_type::file) {
+ filename = test_file_path;
+ } else {
+ if(enableErrorReturn) {
+ return "File does not exist: " + filename;
+ }
+ }
+ }
+ return std::string{};
+ };
+ }
+};
+
+/// Produce a range (factory). Min and max are inclusive.
+class Range : public Validator {
+ public:
+ /// This produces a range with min and max inclusive.
+ ///
+ /// Note that the constructor is templated, but the struct is not, so C++17 is not
+ /// needed to provide nice syntax for Range(a,b).
+ template <typename T>
+ Range(T min_val, T max_val, const std::string &validator_name = std::string{}) : Validator(validator_name) {
+ if(validator_name.empty()) {
+ std::stringstream out;
+ out << detail::type_name<T>() << " in [" << min_val << " - " << max_val << "]";
+ description(out.str());
+ }
+
+ func_ = [min_val, max_val](std::string &input) {
+ T val;
+ bool converted = detail::lexical_cast(input, val);
+ if((!converted) || (val < min_val || val > max_val)) {
+ std::stringstream out;
+ out << "Value " << input << " not in range [";
+ out << min_val << " - " << max_val << "]";
+ return out.str();
+ }
+ return std::string{};
+ };
+ }
+
+ /// Range of one value is 0 to value
+ template <typename T>
+ explicit Range(T max_val, const std::string &validator_name = std::string{})
+ : Range(static_cast<T>(0), max_val, validator_name) {}
+};
+
+/// Check for a non negative number
+const Range NonNegativeNumber((std::numeric_limits<double>::max)(), "NONNEGATIVE");
+
+/// Check for a positive valued number (val>0.0), min() her is the smallest positive number
+const Range PositiveNumber((std::numeric_limits<double>::min)(), (std::numeric_limits<double>::max)(), "POSITIVE");
+
+/// Produce a bounded range (factory). Min and max are inclusive.
+class Bound : public Validator {
+ public:
+ /// This bounds a value with min and max inclusive.
+ ///
+ /// Note that the constructor is templated, but the struct is not, so C++17 is not
+ /// needed to provide nice syntax for Range(a,b).
+ template <typename T> Bound(T min_val, T max_val) {
+ std::stringstream out;
+ out << detail::type_name<T>() << " bounded to [" << min_val << " - " << max_val << "]";
+ description(out.str());
+
+ func_ = [min_val, max_val](std::string &input) {
+ T val;
+ bool converted = detail::lexical_cast(input, val);
+ if(!converted) {
+ return std::string("Value ") + input + " could not be converted";
+ }
+ if(val < min_val)
+ input = detail::to_string(min_val);
+ else if(val > max_val)
+ input = detail::to_string(max_val);
+
+ return std::string{};
+ };
+ }
+
+ /// Range of one value is 0 to value
+ template <typename T> explicit Bound(T max_val) : Bound(static_cast<T>(0), max_val) {}
+};
+
+namespace detail {
+template <typename T,
+ enable_if_t<is_copyable_ptr<typename std::remove_reference<T>::type>::value, detail::enabler> = detail::dummy>
+auto smart_deref(T value) -> decltype(*value) {
+ return *value;
+}
+
+template <
+ typename T,
+ enable_if_t<!is_copyable_ptr<typename std::remove_reference<T>::type>::value, detail::enabler> = detail::dummy>
+typename std::remove_reference<T>::type &smart_deref(T &value) {
+ return value;
+}
+/// Generate a string representation of a set
+template <typename T> std::string generate_set(const T &set) {
+ using element_t = typename detail::element_type<T>::type;
+ using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair
+ std::string out(1, '{');
+ out.append(detail::join(
+ detail::smart_deref(set),
+ [](const iteration_type_t &v) { return detail::pair_adaptor<element_t>::first(v); },
+ ","));
+ out.push_back('}');
+ return out;
+}
+
+/// Generate a string representation of a map
+template <typename T> std::string generate_map(const T &map, bool key_only = false) {
+ using element_t = typename detail::element_type<T>::type;
+ using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair
+ std::string out(1, '{');
+ out.append(detail::join(
+ detail::smart_deref(map),
+ [key_only](const iteration_type_t &v) {
+ std::string res{detail::to_string(detail::pair_adaptor<element_t>::first(v))};
+
+ if(!key_only) {
+ res.append("->");
+ res += detail::to_string(detail::pair_adaptor<element_t>::second(v));
+ }
+ return res;
+ },
+ ","));
+ out.push_back('}');
+ return out;
+}
+
+template <typename C, typename V> struct has_find {
+ template <typename CC, typename VV>
+ static auto test(int) -> decltype(std::declval<CC>().find(std::declval<VV>()), std::true_type());
+ template <typename, typename> static auto test(...) -> decltype(std::false_type());
+
+ static const auto value = decltype(test<C, V>(0))::value;
+ using type = std::integral_constant<bool, value>;
+};
+
+/// A search function
+template <typename T, typename V, enable_if_t<!has_find<T, V>::value, detail::enabler> = detail::dummy>
+auto search(const T &set, const V &val) -> std::pair<bool, decltype(std::begin(detail::smart_deref(set)))> {
+ using element_t = typename detail::element_type<T>::type;
+ auto &setref = detail::smart_deref(set);
+ auto it = std::find_if(std::begin(setref), std::end(setref), [&val](decltype(*std::begin(setref)) v) {
+ return (detail::pair_adaptor<element_t>::first(v) == val);
+ });
+ return {(it != std::end(setref)), it};
+}
+
+/// A search function that uses the built in find function
+template <typename T, typename V, enable_if_t<has_find<T, V>::value, detail::enabler> = detail::dummy>
+auto search(const T &set, const V &val) -> std::pair<bool, decltype(std::begin(detail::smart_deref(set)))> {
+ auto &setref = detail::smart_deref(set);
+ auto it = setref.find(val);
+ return {(it != std::end(setref)), it};
+}
+
+/// A search function with a filter function
+template <typename T, typename V>
+auto search(const T &set, const V &val, const std::function<V(V)> &filter_function)
+ -> std::pair<bool, decltype(std::begin(detail::smart_deref(set)))> {
+ using element_t = typename detail::element_type<T>::type;
+ // do the potentially faster first search
+ auto res = search(set, val);
+ if((res.first) || (!(filter_function))) {
+ return res;
+ }
+ // if we haven't found it do the longer linear search with all the element translations
+ auto &setref = detail::smart_deref(set);
+ auto it = std::find_if(std::begin(setref), std::end(setref), [&](decltype(*std::begin(setref)) v) {
+ V a{detail::pair_adaptor<element_t>::first(v)};
+ a = filter_function(a);
+ return (a == val);
+ });
+ return {(it != std::end(setref)), it};
+}
+
+// the following suggestion was made by Nikita Ofitserov(@himikof)
+// done in templates to prevent compiler warnings on negation of unsigned numbers
+
+/// Do a check for overflow on signed numbers
+template <typename T>
+inline typename std::enable_if<std::is_signed<T>::value, T>::type overflowCheck(const T &a, const T &b) {
+ if((a > 0) == (b > 0)) {
+ return ((std::numeric_limits<T>::max)() / (std::abs)(a) < (std::abs)(b));
+ } else {
+ return ((std::numeric_limits<T>::min)() / (std::abs)(a) > -(std::abs)(b));
+ }
+}
+/// Do a check for overflow on unsigned numbers
+template <typename T>
+inline typename std::enable_if<!std::is_signed<T>::value, T>::type overflowCheck(const T &a, const T &b) {
+ return ((std::numeric_limits<T>::max)() / a < b);
+}
+
+/// Performs a *= b; if it doesn't cause integer overflow. Returns false otherwise.
+template <typename T> typename std::enable_if<std::is_integral<T>::value, bool>::type checked_multiply(T &a, T b) {
+ if(a == 0 || b == 0 || a == 1 || b == 1) {
+ a *= b;
+ return true;
+ }
+ if(a == (std::numeric_limits<T>::min)() || b == (std::numeric_limits<T>::min)()) {
+ return false;
+ }
+ if(overflowCheck(a, b)) {
+ return false;
+ }
+ a *= b;
+ return true;
+}
+
+/// Performs a *= b; if it doesn't equal infinity. Returns false otherwise.
+template <typename T>
+typename std::enable_if<std::is_floating_point<T>::value, bool>::type checked_multiply(T &a, T b) {
+ T c = a * b;
+ if(std::isinf(c) && !std::isinf(a) && !std::isinf(b)) {
+ return false;
+ }
+ a = c;
+ return true;
+}
+
+} // namespace detail
+/// Verify items are in a set
+class IsMember : public Validator {
+ public:
+ using filter_fn_t = std::function<std::string(std::string)>;
+
+ /// This allows in-place construction using an initializer list
+ template <typename T, typename... Args>
+ IsMember(std::initializer_list<T> values, Args &&...args)
+ : IsMember(std::vector<T>(values), std::forward<Args>(args)...) {}
+
+ /// This checks to see if an item is in a set (empty function)
+ template <typename T> explicit IsMember(T &&set) : IsMember(std::forward<T>(set), nullptr) {}
+
+ /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter
+ /// both sides of the comparison before computing the comparison.
+ template <typename T, typename F> explicit IsMember(T set, F filter_function) {
+
+ // Get the type of the contained item - requires a container have ::value_type
+ // if the type does not have first_type and second_type, these are both value_type
+ using element_t = typename detail::element_type<T>::type; // Removes (smart) pointers if needed
+ using item_t = typename detail::pair_adaptor<element_t>::first_type; // Is value_type if not a map
+
+ using local_item_t = typename IsMemberType<item_t>::type; // This will convert bad types to good ones
+ // (const char * to std::string)
+
+ // Make a local copy of the filter function, using a std::function if not one already
+ std::function<local_item_t(local_item_t)> filter_fn = filter_function;
+
+ // This is the type name for help, it will take the current version of the set contents
+ desc_function_ = [set]() { return detail::generate_set(detail::smart_deref(set)); };
+
+ // This is the function that validates
+ // It stores a copy of the set pointer-like, so shared_ptr will stay alive
+ func_ = [set, filter_fn](std::string &input) {
+ local_item_t b;
+ if(!detail::lexical_cast(input, b)) {
+ throw ValidationError(input); // name is added later
+ }
+ if(filter_fn) {
+ b = filter_fn(b);
+ }
+ auto res = detail::search(set, b, filter_fn);
+ if(res.first) {
+ // Make sure the version in the input string is identical to the one in the set
+ if(filter_fn) {
+ input = detail::value_string(detail::pair_adaptor<element_t>::first(*(res.second)));
+ }
+
+ // Return empty error string (success)
+ return std::string{};
+ }
+
+ // If you reach this point, the result was not found
+ return input + " not in " + detail::generate_set(detail::smart_deref(set));
+ };
+ }
+
+ /// You can pass in as many filter functions as you like, they nest (string only currently)
+ template <typename T, typename... Args>
+ IsMember(T &&set, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other)
+ : IsMember(
+ std::forward<T>(set),
+ [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); },
+ other...) {}
+};
+
+/// definition of the default transformation object
+template <typename T> using TransformPairs = std::vector<std::pair<std::string, T>>;
+
+/// Translate named items to other or a value set
+class Transformer : public Validator {
+ public:
+ using filter_fn_t = std::function<std::string(std::string)>;
+
+ /// This allows in-place construction
+ template <typename... Args>
+ Transformer(std::initializer_list<std::pair<std::string, std::string>> values, Args &&...args)
+ : Transformer(TransformPairs<std::string>(values), std::forward<Args>(args)...) {}
+
+ /// direct map of std::string to std::string
+ template <typename T> explicit Transformer(T &&mapping) : Transformer(std::forward<T>(mapping), nullptr) {}
+
+ /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter
+ /// both sides of the comparison before computing the comparison.
+ template <typename T, typename F> explicit Transformer(T mapping, F filter_function) {
+
+ static_assert(detail::pair_adaptor<typename detail::element_type<T>::type>::value,
+ "mapping must produce value pairs");
+ // Get the type of the contained item - requires a container have ::value_type
+ // if the type does not have first_type and second_type, these are both value_type
+ using element_t = typename detail::element_type<T>::type; // Removes (smart) pointers if needed
+ using item_t = typename detail::pair_adaptor<element_t>::first_type; // Is value_type if not a map
+ using local_item_t = typename IsMemberType<item_t>::type; // Will convert bad types to good ones
+ // (const char * to std::string)
+
+ // Make a local copy of the filter function, using a std::function if not one already
+ std::function<local_item_t(local_item_t)> filter_fn = filter_function;
+
+ // This is the type name for help, it will take the current version of the set contents
+ desc_function_ = [mapping]() { return detail::generate_map(detail::smart_deref(mapping)); };
+
+ func_ = [mapping, filter_fn](std::string &input) {
+ local_item_t b;
+ if(!detail::lexical_cast(input, b)) {
+ return std::string();
+ // there is no possible way we can match anything in the mapping if we can't convert so just return
+ }
+ if(filter_fn) {
+ b = filter_fn(b);
+ }
+ auto res = detail::search(mapping, b, filter_fn);
+ if(res.first) {
+ input = detail::value_string(detail::pair_adaptor<element_t>::second(*res.second));
+ }
+ return std::string{};
+ };
+ }
+
+ /// You can pass in as many filter functions as you like, they nest
+ template <typename T, typename... Args>
+ Transformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other)
+ : Transformer(
+ std::forward<T>(mapping),
+ [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); },
+ other...) {}
+};
+
+/// translate named items to other or a value set
+class CheckedTransformer : public Validator {
+ public:
+ using filter_fn_t = std::function<std::string(std::string)>;
+
+ /// This allows in-place construction
+ template <typename... Args>
+ CheckedTransformer(std::initializer_list<std::pair<std::string, std::string>> values, Args &&...args)
+ : CheckedTransformer(TransformPairs<std::string>(values), std::forward<Args>(args)...) {}
+
+ /// direct map of std::string to std::string
+ template <typename T> explicit CheckedTransformer(T mapping) : CheckedTransformer(std::move(mapping), nullptr) {}
+
+ /// This checks to see if an item is in a set: pointer or copy version. You can pass in a function that will filter
+ /// both sides of the comparison before computing the comparison.
+ template <typename T, typename F> explicit CheckedTransformer(T mapping, F filter_function) {
+
+ static_assert(detail::pair_adaptor<typename detail::element_type<T>::type>::value,
+ "mapping must produce value pairs");
+ // Get the type of the contained item - requires a container have ::value_type
+ // if the type does not have first_type and second_type, these are both value_type
+ using element_t = typename detail::element_type<T>::type; // Removes (smart) pointers if needed
+ using item_t = typename detail::pair_adaptor<element_t>::first_type; // Is value_type if not a map
+ using local_item_t = typename IsMemberType<item_t>::type; // Will convert bad types to good ones
+ // (const char * to std::string)
+ using iteration_type_t = typename detail::pair_adaptor<element_t>::value_type; // the type of the object pair
+
+ // Make a local copy of the filter function, using a std::function if not one already
+ std::function<local_item_t(local_item_t)> filter_fn = filter_function;
+
+ auto tfunc = [mapping]() {
+ std::string out("value in ");
+ out += detail::generate_map(detail::smart_deref(mapping)) + " OR {";
+ out += detail::join(
+ detail::smart_deref(mapping),
+ [](const iteration_type_t &v) { return detail::to_string(detail::pair_adaptor<element_t>::second(v)); },
+ ",");
+ out.push_back('}');
+ return out;
+ };
+
+ desc_function_ = tfunc;
+
+ func_ = [mapping, tfunc, filter_fn](std::string &input) {
+ local_item_t b;
+ bool converted = detail::lexical_cast(input, b);
+ if(converted) {
+ if(filter_fn) {
+ b = filter_fn(b);
+ }
+ auto res = detail::search(mapping, b, filter_fn);
+ if(res.first) {
+ input = detail::value_string(detail::pair_adaptor<element_t>::second(*res.second));
+ return std::string{};
+ }
+ }
+ for(const auto &v : detail::smart_deref(mapping)) {
+ auto output_string = detail::value_string(detail::pair_adaptor<element_t>::second(v));
+ if(output_string == input) {
+ return std::string();
+ }
+ }
+
+ return "Check " + input + " " + tfunc() + " FAILED";
+ };
+ }
+
+ /// You can pass in as many filter functions as you like, they nest
+ template <typename T, typename... Args>
+ CheckedTransformer(T &&mapping, filter_fn_t filter_fn_1, filter_fn_t filter_fn_2, Args &&...other)
+ : CheckedTransformer(
+ std::forward<T>(mapping),
+ [filter_fn_1, filter_fn_2](std::string a) { return filter_fn_2(filter_fn_1(a)); },
+ other...) {}
+};
+
+/// Helper function to allow ignore_case to be passed to IsMember or Transform
+inline std::string ignore_case(std::string item) { return detail::to_lower(item); }
+
+/// Helper function to allow ignore_underscore to be passed to IsMember or Transform
+inline std::string ignore_underscore(std::string item) { return detail::remove_underscore(item); }
+
+/// Helper function to allow checks to ignore spaces to be passed to IsMember or Transform
+inline std::string ignore_space(std::string item) {
+ item.erase(std::remove(std::begin(item), std::end(item), ' '), std::end(item));
+ item.erase(std::remove(std::begin(item), std::end(item), '\t'), std::end(item));
+ return item;
+}
+
+/// Multiply a number by a factor using given mapping.
+/// Can be used to write transforms for SIZE or DURATION inputs.
+///
+/// Example:
+/// With mapping = `{"b"->1, "kb"->1024, "mb"->1024*1024}`
+/// one can recognize inputs like "100", "12kb", "100 MB",
+/// that will be automatically transformed to 100, 14448, 104857600.
+///
+/// Output number type matches the type in the provided mapping.
+/// Therefore, if it is required to interpret real inputs like "0.42 s",
+/// the mapping should be of a type <string, float> or <string, double>.
+class AsNumberWithUnit : public Validator {
+ public:
+ /// Adjust AsNumberWithUnit behavior.
+ /// CASE_SENSITIVE/CASE_INSENSITIVE controls how units are matched.
+ /// UNIT_OPTIONAL/UNIT_REQUIRED throws ValidationError
+ /// if UNIT_REQUIRED is set and unit literal is not found.
+ enum Options {
+ CASE_SENSITIVE = 0,
+ CASE_INSENSITIVE = 1,
+ UNIT_OPTIONAL = 0,
+ UNIT_REQUIRED = 2,
+ DEFAULT = CASE_INSENSITIVE | UNIT_OPTIONAL
+ };
+
+ template <typename Number>
+ explicit AsNumberWithUnit(std::map<std::string, Number> mapping,
+ Options opts = DEFAULT,
+ const std::string &unit_name = "UNIT") {
+ description(generate_description<Number>(unit_name, opts));
+ validate_mapping(mapping, opts);
+
+ // transform function
+ func_ = [mapping, opts](std::string &input) -> std::string {
+ Number num;
+
+ detail::rtrim(input);
+ if(input.empty()) {
+ throw ValidationError("Input is empty");
+ }
+
+ // Find split position between number and prefix
+ auto unit_begin = input.end();
+ while(unit_begin > input.begin() && std::isalpha(*(unit_begin - 1), std::locale())) {
+ --unit_begin;
+ }
+
+ std::string unit{unit_begin, input.end()};
+ input.resize(static_cast<std::size_t>(std::distance(input.begin(), unit_begin)));
+ detail::trim(input);
+
+ if(opts & UNIT_REQUIRED && unit.empty()) {
+ throw ValidationError("Missing mandatory unit");
+ }
+ if(opts & CASE_INSENSITIVE) {
+ unit = detail::to_lower(unit);
+ }
+ if(unit.empty()) {
+ if(!detail::lexical_cast(input, num)) {
+ throw ValidationError(std::string("Value ") + input + " could not be converted to " +
+ detail::type_name<Number>());
+ }
+ // No need to modify input if no unit passed
+ return {};
+ }
+
+ // find corresponding factor
+ auto it = mapping.find(unit);
+ if(it == mapping.end()) {
+ throw ValidationError(unit +
+ " unit not recognized. "
+ "Allowed values: " +
+ detail::generate_map(mapping, true));
+ }
+
+ if(!input.empty()) {
+ bool converted = detail::lexical_cast(input, num);
+ if(!converted) {
+ throw ValidationError(std::string("Value ") + input + " could not be converted to " +
+ detail::type_name<Number>());
+ }
+ // perform safe multiplication
+ bool ok = detail::checked_multiply(num, it->second);
+ if(!ok) {
+ throw ValidationError(detail::to_string(num) + " multiplied by " + unit +
+ " factor would cause number overflow. Use smaller value.");
+ }
+ } else {
+ num = static_cast<Number>(it->second);
+ }
+
+ input = detail::to_string(num);
+
+ return {};
+ };
+ }
+
+ private:
+ /// Check that mapping contains valid units.
+ /// Update mapping for CASE_INSENSITIVE mode.
+ template <typename Number> static void validate_mapping(std::map<std::string, Number> &mapping, Options opts) {
+ for(auto &kv : mapping) {
+ if(kv.first.empty()) {
+ throw ValidationError("Unit must not be empty.");
+ }
+ if(!detail::isalpha(kv.first)) {
+ throw ValidationError("Unit must contain only letters.");
+ }
+ }
+
+ // make all units lowercase if CASE_INSENSITIVE
+ if(opts & CASE_INSENSITIVE) {
+ std::map<std::string, Number> lower_mapping;
+ for(auto &kv : mapping) {
+ auto s = detail::to_lower(kv.first);
+ if(lower_mapping.count(s)) {
+ throw ValidationError(std::string("Several matching lowercase unit representations are found: ") +
+ s);
+ }
+ lower_mapping[detail::to_lower(kv.first)] = kv.second;
+ }
+ mapping = std::move(lower_mapping);
+ }
+ }
+
+ /// Generate description like this: NUMBER [UNIT]
+ template <typename Number> static std::string generate_description(const std::string &name, Options opts) {
+ std::stringstream out;
+ out << detail::type_name<Number>() << ' ';
+ if(opts & UNIT_REQUIRED) {
+ out << name;
+ } else {
+ out << '[' << name << ']';
+ }
+ return out.str();
+ }
+};
+
+/// Converts a human-readable size string (with unit literal) to uin64_t size.
+/// Example:
+/// "100" => 100
+/// "1 b" => 100
+/// "10Kb" => 10240 // you can configure this to be interpreted as kilobyte (*1000) or kibibyte (*1024)
+/// "10 KB" => 10240
+/// "10 kb" => 10240
+/// "10 kib" => 10240 // *i, *ib are always interpreted as *bibyte (*1024)
+/// "10kb" => 10240
+/// "2 MB" => 2097152
+/// "2 EiB" => 2^61 // Units up to exibyte are supported
+class AsSizeValue : public AsNumberWithUnit {
+ public:
+ using result_t = std::uint64_t;
+
+ /// If kb_is_1000 is true,
+ /// interpret 'kb', 'k' as 1000 and 'kib', 'ki' as 1024
+ /// (same applies to higher order units as well).
+ /// Otherwise, interpret all literals as factors of 1024.
+ /// The first option is formally correct, but
+ /// the second interpretation is more wide-spread
+ /// (see https://en.wikipedia.org/wiki/Binary_prefix).
+ explicit AsSizeValue(bool kb_is_1000) : AsNumberWithUnit(get_mapping(kb_is_1000)) {
+ if(kb_is_1000) {
+ description("SIZE [b, kb(=1000b), kib(=1024b), ...]");
+ } else {
+ description("SIZE [b, kb(=1024b), ...]");
+ }
+ }
+
+ private:
+ /// Get <size unit, factor> mapping
+ static std::map<std::string, result_t> init_mapping(bool kb_is_1000) {
+ std::map<std::string, result_t> m;
+ result_t k_factor = kb_is_1000 ? 1000 : 1024;
+ result_t ki_factor = 1024;
+ result_t k = 1;
+ result_t ki = 1;
+ m["b"] = 1;
+ for(std::string p : {"k", "m", "g", "t", "p", "e"}) {
+ k *= k_factor;
+ ki *= ki_factor;
+ m[p] = k;
+ m[p + "b"] = k;
+ m[p + "i"] = ki;
+ m[p + "ib"] = ki;
+ }
+ return m;
+ }
+
+ /// Cache calculated mapping
+ static std::map<std::string, result_t> get_mapping(bool kb_is_1000) {
+ if(kb_is_1000) {
+ static auto m = init_mapping(true);
+ return m;
+ } else {
+ static auto m = init_mapping(false);
+ return m;
+ }
+ }
+};
+
+namespace detail {
+/// Split a string into a program name and command line arguments
+/// the string is assumed to contain a file name followed by other arguments
+/// the return value contains is a pair with the first argument containing the program name and the second
+/// everything else.
+inline std::pair<std::string, std::string> split_program_name(std::string commandline) {
+ // try to determine the programName
+ std::pair<std::string, std::string> vals;
+ trim(commandline);
+ auto esp = commandline.find_first_of(' ', 1);
+ while(detail::check_path(commandline.substr(0, esp).c_str()) != path_type::file) {
+ esp = commandline.find_first_of(' ', esp + 1);
+ if(esp == std::string::npos) {
+ // if we have reached the end and haven't found a valid file just assume the first argument is the
+ // program name
+ if(commandline[0] == '"' || commandline[0] == '\'' || commandline[0] == '`') {
+ bool embeddedQuote = false;
+ auto keyChar = commandline[0];
+ auto end = commandline.find_first_of(keyChar, 1);
+ while((end != std::string::npos) && (commandline[end - 1] == '\\')) { // deal with escaped quotes
+ end = commandline.find_first_of(keyChar, end + 1);
+ embeddedQuote = true;
+ }
+ if(end != std::string::npos) {
+ vals.first = commandline.substr(1, end - 1);
+ esp = end + 1;
+ if(embeddedQuote) {
+ vals.first = find_and_replace(vals.first, std::string("\\") + keyChar, std::string(1, keyChar));
+ }
+ } else {
+ esp = commandline.find_first_of(' ', 1);
+ }
+ } else {
+ esp = commandline.find_first_of(' ', 1);
+ }
+
+ break;
+ }
+ }
+ if(vals.first.empty()) {
+ vals.first = commandline.substr(0, esp);
+ rtrim(vals.first);
+ }
+
+ // strip the program name
+ vals.second = (esp != std::string::npos) ? commandline.substr(esp + 1) : std::string{};
+ ltrim(vals.second);
+ return vals;
+}
+
+} // namespace detail
+/// @}
+
+// [CLI11:validators_hpp:end]
+} // namespace CLI
diff --git a/src/third-party/CLI/Version.hpp b/src/third-party/CLI/Version.hpp
new file mode 100644
index 0000000..b03141b
--- /dev/null
+++ b/src/third-party/CLI/Version.hpp
@@ -0,0 +1,16 @@
+// Copyright (c) 2017-2022, University of Cincinnati, developed by Henry Schreiner
+// under NSF AWARD 1414736 and by the respective contributors.
+// All rights reserved.
+//
+// SPDX-License-Identifier: BSD-3-Clause
+
+#pragma once
+
+// [CLI11:version_hpp:verbatim]
+
+#define CLI11_VERSION_MAJOR 2
+#define CLI11_VERSION_MINOR 2
+#define CLI11_VERSION_PATCH 0
+#define CLI11_VERSION "2.2.0"
+
+// [CLI11:version_hpp:end]
diff --git a/src/third-party/backward-cpp/backward.hpp b/src/third-party/backward-cpp/backward.hpp
new file mode 100644
index 0000000..e9b4909
--- /dev/null
+++ b/src/third-party/backward-cpp/backward.hpp
@@ -0,0 +1,4460 @@
+/*
+ * backward.hpp
+ * Copyright 2013 Google Inc. All Rights Reserved.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef H_6B9572DA_A64B_49E6_B234_051480991C89
+#define H_6B9572DA_A64B_49E6_B234_051480991C89
+
+#ifndef __cplusplus
+#error "It's not going to compile without a C++ compiler..."
+#endif
+
+#if defined(BACKWARD_CXX11)
+#elif defined(BACKWARD_CXX98)
+#else
+#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1800)
+#define BACKWARD_CXX11
+#define BACKWARD_ATLEAST_CXX11
+#define BACKWARD_ATLEAST_CXX98
+#if __cplusplus >= 201703L || (defined(_MSVC_LANG) && _MSVC_LANG >= 201703L)
+#define BACKWARD_ATLEAST_CXX17
+#endif
+#else
+#define BACKWARD_CXX98
+#define BACKWARD_ATLEAST_CXX98
+#endif
+#endif
+
+// You can define one of the following (or leave it to the auto-detection):
+//
+// #define BACKWARD_SYSTEM_LINUX
+// - specialization for linux
+//
+// #define BACKWARD_SYSTEM_DARWIN
+// - specialization for Mac OS X 10.5 and later.
+//
+// #define BACKWARD_SYSTEM_WINDOWS
+// - specialization for Windows (Clang 9 and MSVC2017)
+//
+// #define BACKWARD_SYSTEM_UNKNOWN
+// - placebo implementation, does nothing.
+//
+#if defined(BACKWARD_SYSTEM_LINUX)
+#elif defined(BACKWARD_SYSTEM_DARWIN)
+#elif defined(BACKWARD_SYSTEM_UNKNOWN)
+#elif defined(BACKWARD_SYSTEM_WINDOWS)
+#else
+#if defined(__linux) || defined(__linux__)
+#define BACKWARD_SYSTEM_LINUX
+#elif defined(__APPLE__)
+#define BACKWARD_SYSTEM_DARWIN
+#elif defined(_WIN32)
+#define BACKWARD_SYSTEM_WINDOWS
+#else
+#define BACKWARD_SYSTEM_UNKNOWN
+#endif
+#endif
+
+#define NOINLINE __attribute__((noinline))
+
+#include <algorithm>
+#include <cctype>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <limits>
+#include <new>
+#include <sstream>
+#include <streambuf>
+#include <string>
+#include <vector>
+#include <exception>
+#include <iterator>
+
+#if defined(BACKWARD_SYSTEM_LINUX)
+
+// On linux, backtrace can back-trace or "walk" the stack using the following
+// libraries:
+//
+// #define BACKWARD_HAS_UNWIND 1
+// - unwind comes from libgcc, but I saw an equivalent inside clang itself.
+// - with unwind, the stacktrace is as accurate as it can possibly be, since
+// this is used by the C++ runtine in gcc/clang for stack unwinding on
+// exception.
+// - normally libgcc is already linked to your program by default.
+//
+// #define BACKWARD_HAS_LIBUNWIND 1
+// - libunwind provides, in some cases, a more accurate stacktrace as it knows
+// to decode signal handler frames and lets us edit the context registers when
+// unwinding, allowing stack traces over bad function references.
+//
+// #define BACKWARD_HAS_BACKTRACE == 1
+// - backtrace seems to be a little bit more portable than libunwind, but on
+// linux, it uses unwind anyway, but abstract away a tiny information that is
+// sadly really important in order to get perfectly accurate stack traces.
+// - backtrace is part of the (e)glib library.
+//
+// The default is:
+// #define BACKWARD_HAS_UNWIND == 1
+//
+// Note that only one of the define should be set to 1 at a time.
+//
+#if BACKWARD_HAS_UNWIND == 1
+#elif BACKWARD_HAS_LIBUNWIND == 1
+#elif BACKWARD_HAS_BACKTRACE == 1
+#else
+#undef BACKWARD_HAS_UNWIND
+#define BACKWARD_HAS_UNWIND 1
+#undef BACKWARD_HAS_LIBUNWIND
+#define BACKWARD_HAS_LIBUNWIND 0
+#undef BACKWARD_HAS_BACKTRACE
+#define BACKWARD_HAS_BACKTRACE 0
+#endif
+
+// On linux, backward can extract detailed information about a stack trace
+// using one of the following libraries:
+//
+// #define BACKWARD_HAS_DW 1
+// - libdw gives you the most juicy details out of your stack traces:
+// - object filename
+// - function name
+// - source filename
+// - line and column numbers
+// - source code snippet (assuming the file is accessible)
+// - variables name and values (if not optimized out)
+// - You need to link with the lib "dw":
+// - apt-get install libdw-dev
+// - g++/clang++ -ldw ...
+//
+// #define BACKWARD_HAS_BFD 1
+// - With libbfd, you get a fair amount of details:
+// - object filename
+// - function name
+// - source filename
+// - line numbers
+// - source code snippet (assuming the file is accessible)
+// - You need to link with the lib "bfd":
+// - apt-get install binutils-dev
+// - g++/clang++ -lbfd ...
+//
+// #define BACKWARD_HAS_DWARF 1
+// - libdwarf gives you the most juicy details out of your stack traces:
+// - object filename
+// - function name
+// - source filename
+// - line and column numbers
+// - source code snippet (assuming the file is accessible)
+// - variables name and values (if not optimized out)
+// - You need to link with the lib "dwarf":
+// - apt-get install libdwarf-dev
+// - g++/clang++ -ldwarf ...
+//
+// #define BACKWARD_HAS_BACKTRACE_SYMBOL 1
+// - backtrace provides minimal details for a stack trace:
+// - object filename
+// - function name
+// - backtrace is part of the (e)glib library.
+//
+// The default is:
+// #define BACKWARD_HAS_BACKTRACE_SYMBOL == 1
+//
+// Note that only one of the define should be set to 1 at a time.
+//
+#if BACKWARD_HAS_DW == 1
+#elif BACKWARD_HAS_BFD == 1
+#elif BACKWARD_HAS_DWARF == 1
+#elif BACKWARD_HAS_BACKTRACE_SYMBOL == 1
+#else
+#undef BACKWARD_HAS_DW
+#define BACKWARD_HAS_DW 0
+#undef BACKWARD_HAS_BFD
+#define BACKWARD_HAS_BFD 0
+#undef BACKWARD_HAS_DWARF
+#define BACKWARD_HAS_DWARF 0
+#undef BACKWARD_HAS_BACKTRACE_SYMBOL
+#define BACKWARD_HAS_BACKTRACE_SYMBOL 1
+#endif
+
+#include <cxxabi.h>
+#include <fcntl.h>
+#ifdef __ANDROID__
+// Old Android API levels define _Unwind_Ptr in both link.h and
+// unwind.h Rename the one in link.h as we are not going to be using
+// it
+#define _Unwind_Ptr _Unwind_Ptr_Custom
+#include <link.h>
+#undef _Unwind_Ptr
+#else
+#include <link.h>
+#endif
+#include <signal.h>
+#include <sys/stat.h>
+#include <syscall.h>
+#include <unistd.h>
+
+#if BACKWARD_HAS_BFD == 1
+// NOTE: defining PACKAGE{,_VERSION} is required before including
+// bfd.h on some platforms, see also:
+// https://sourceware.org/bugzilla/show_bug.cgi?id=14243
+#ifndef PACKAGE
+#define PACKAGE
+#endif
+#ifndef PACKAGE_VERSION
+#define PACKAGE_VERSION
+#endif
+#include <bfd.h>
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#include <dlfcn.h>
+#undef _GNU_SOURCE
+#else
+#include <dlfcn.h>
+#endif
+#endif
+
+#if BACKWARD_HAS_DW == 1
+#include <dwarf.h>
+#include <elfutils/libdw.h>
+#include <elfutils/libdwfl.h>
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#include <dlfcn.h>
+#undef _GNU_SOURCE
+#else
+#include <dlfcn.h>
+#endif
+#endif
+
+#if BACKWARD_HAS_DWARF == 1
+#include <algorithm>
+#include <dwarf.h>
+#include <libdwarf.h>
+#include <libelf.h>
+#include <map>
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#include <dlfcn.h>
+#undef _GNU_SOURCE
+#else
+#include <dlfcn.h>
+#endif
+#endif
+
+#if (BACKWARD_HAS_BACKTRACE == 1) || (BACKWARD_HAS_BACKTRACE_SYMBOL == 1)
+// then we shall rely on backtrace
+#include <execinfo.h>
+#endif
+
+#endif // defined(BACKWARD_SYSTEM_LINUX)
+
+#if defined(BACKWARD_SYSTEM_DARWIN)
+// On Darwin, backtrace can back-trace or "walk" the stack using the following
+// libraries:
+//
+// #define BACKWARD_HAS_UNWIND 1
+// - unwind comes from libgcc, but I saw an equivalent inside clang itself.
+// - with unwind, the stacktrace is as accurate as it can possibly be, since
+// this is used by the C++ runtine in gcc/clang for stack unwinding on
+// exception.
+// - normally libgcc is already linked to your program by default.
+//
+// #define BACKWARD_HAS_LIBUNWIND 1
+// - libunwind comes from clang, which implements an API compatible version.
+// - libunwind provides, in some cases, a more accurate stacktrace as it knows
+// to decode signal handler frames and lets us edit the context registers when
+// unwinding, allowing stack traces over bad function references.
+//
+// #define BACKWARD_HAS_BACKTRACE == 1
+// - backtrace is available by default, though it does not produce as much
+// information as another library might.
+//
+// The default is:
+// #define BACKWARD_HAS_UNWIND == 1
+//
+// Note that only one of the define should be set to 1 at a time.
+//
+#if BACKWARD_HAS_UNWIND == 1
+#elif BACKWARD_HAS_BACKTRACE == 1
+#elif BACKWARD_HAS_LIBUNWIND == 1
+#else
+#undef BACKWARD_HAS_UNWIND
+#define BACKWARD_HAS_UNWIND 1
+#undef BACKWARD_HAS_BACKTRACE
+#define BACKWARD_HAS_BACKTRACE 0
+#undef BACKWARD_HAS_LIBUNWIND
+#define BACKWARD_HAS_LIBUNWIND 0
+#endif
+
+// On Darwin, backward can extract detailed information about a stack trace
+// using one of the following libraries:
+//
+// #define BACKWARD_HAS_BACKTRACE_SYMBOL 1
+// - backtrace provides minimal details for a stack trace:
+// - object filename
+// - function name
+//
+// The default is:
+// #define BACKWARD_HAS_BACKTRACE_SYMBOL == 1
+//
+#if BACKWARD_HAS_BACKTRACE_SYMBOL == 1
+#else
+#undef BACKWARD_HAS_BACKTRACE_SYMBOL
+#define BACKWARD_HAS_BACKTRACE_SYMBOL 1
+#endif
+
+#include <cxxabi.h>
+#include <fcntl.h>
+#include <pthread.h>
+#include <signal.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#if (BACKWARD_HAS_BACKTRACE == 1) || (BACKWARD_HAS_BACKTRACE_SYMBOL == 1)
+#include <execinfo.h>
+#endif
+#endif // defined(BACKWARD_SYSTEM_DARWIN)
+
+#if defined(BACKWARD_SYSTEM_WINDOWS)
+
+#include <condition_variable>
+#include <mutex>
+#include <thread>
+
+#include <basetsd.h>
+typedef SSIZE_T ssize_t;
+
+#define NOMINMAX
+#include <windows.h>
+#include <winnt.h>
+
+#include <psapi.h>
+#include <signal.h>
+
+#ifndef __clang__
+#undef NOINLINE
+#define NOINLINE __declspec(noinline)
+#endif
+
+#pragma comment(lib, "psapi.lib")
+#pragma comment(lib, "dbghelp.lib")
+
+// Comment / packing is from stackoverflow:
+// https://stackoverflow.com/questions/6205981/windows-c-stack-trace-from-a-running-app/28276227#28276227
+// Some versions of imagehlp.dll lack the proper packing directives themselves
+// so we need to do it.
+#pragma pack(push, before_imagehlp, 8)
+#include <imagehlp.h>
+#pragma pack(pop, before_imagehlp)
+
+// TODO maybe these should be undefined somewhere else?
+#undef BACKWARD_HAS_UNWIND
+#undef BACKWARD_HAS_BACKTRACE
+#if BACKWARD_HAS_PDB_SYMBOL == 1
+#else
+#undef BACKWARD_HAS_PDB_SYMBOL
+#define BACKWARD_HAS_PDB_SYMBOL 1
+#endif
+
+#endif
+
+#if BACKWARD_HAS_UNWIND == 1
+
+#include <unwind.h>
+// while gcc's unwind.h defines something like that:
+// extern _Unwind_Ptr _Unwind_GetIP (struct _Unwind_Context *);
+// extern _Unwind_Ptr _Unwind_GetIPInfo (struct _Unwind_Context *, int *);
+//
+// clang's unwind.h defines something like this:
+// uintptr_t _Unwind_GetIP(struct _Unwind_Context* __context);
+//
+// Even if the _Unwind_GetIPInfo can be linked to, it is not declared, worse we
+// cannot just redeclare it because clang's unwind.h doesn't define _Unwind_Ptr
+// anyway.
+//
+// Luckily we can play on the fact that the guard macros have a different name:
+#ifdef __CLANG_UNWIND_H
+// In fact, this function still comes from libgcc (on my different linux boxes,
+// clang links against libgcc).
+#include <inttypes.h>
+extern "C" uintptr_t _Unwind_GetIPInfo(_Unwind_Context *, int *);
+#endif
+
+#endif // BACKWARD_HAS_UNWIND == 1
+
+#if BACKWARD_HAS_LIBUNWIND == 1
+#define UNW_LOCAL_ONLY
+#include <libunwind.h>
+#endif // BACKWARD_HAS_LIBUNWIND == 1
+
+#ifdef BACKWARD_ATLEAST_CXX11
+#include <unordered_map>
+#include <utility> // for std::swap
+namespace backward {
+namespace details {
+template <typename K, typename V> struct hashtable {
+ typedef std::unordered_map<K, V> type;
+};
+using std::move;
+} // namespace details
+} // namespace backward
+#else // NOT BACKWARD_ATLEAST_CXX11
+#define nullptr NULL
+#define override
+#include <map>
+namespace backward {
+namespace details {
+template <typename K, typename V> struct hashtable {
+ typedef std::map<K, V> type;
+};
+template <typename T> const T &move(const T &v) { return v; }
+template <typename T> T &move(T &v) { return v; }
+} // namespace details
+} // namespace backward
+#endif // BACKWARD_ATLEAST_CXX11
+
+namespace backward {
+namespace details {
+#if defined(BACKWARD_SYSTEM_WINDOWS)
+const char kBackwardPathDelimiter[] = ";";
+#else
+const char kBackwardPathDelimiter[] = ":";
+#endif
+} // namespace details
+} // namespace backward
+
+namespace backward {
+
+namespace system_tag {
+struct linux_tag; // seems that I cannot call that "linux" because the name
+// is already defined... so I am adding _tag everywhere.
+struct darwin_tag;
+struct windows_tag;
+struct unknown_tag;
+
+#if defined(BACKWARD_SYSTEM_LINUX)
+typedef linux_tag current_tag;
+#elif defined(BACKWARD_SYSTEM_DARWIN)
+typedef darwin_tag current_tag;
+#elif defined(BACKWARD_SYSTEM_WINDOWS)
+typedef windows_tag current_tag;
+#elif defined(BACKWARD_SYSTEM_UNKNOWN)
+typedef unknown_tag current_tag;
+#else
+#error "May I please get my system defines?"
+#endif
+} // namespace system_tag
+
+namespace trace_resolver_tag {
+#if defined(BACKWARD_SYSTEM_LINUX)
+struct libdw;
+struct libbfd;
+struct libdwarf;
+struct backtrace_symbol;
+
+#if BACKWARD_HAS_DW == 1
+typedef libdw current;
+#elif BACKWARD_HAS_BFD == 1
+typedef libbfd current;
+#elif BACKWARD_HAS_DWARF == 1
+typedef libdwarf current;
+#elif BACKWARD_HAS_BACKTRACE_SYMBOL == 1
+typedef backtrace_symbol current;
+#else
+#error "You shall not pass, until you know what you want."
+#endif
+#elif defined(BACKWARD_SYSTEM_DARWIN)
+struct backtrace_symbol;
+
+#if BACKWARD_HAS_BACKTRACE_SYMBOL == 1
+typedef backtrace_symbol current;
+#else
+#error "You shall not pass, until you know what you want."
+#endif
+#elif defined(BACKWARD_SYSTEM_WINDOWS)
+struct pdb_symbol;
+#if BACKWARD_HAS_PDB_SYMBOL == 1
+typedef pdb_symbol current;
+#else
+#error "You shall not pass, until you know what you want."
+#endif
+#endif
+} // namespace trace_resolver_tag
+
+namespace details {
+
+template <typename T> struct rm_ptr { typedef T type; };
+
+template <typename T> struct rm_ptr<T *> { typedef T type; };
+
+template <typename T> struct rm_ptr<const T *> { typedef const T type; };
+
+template <typename R, typename T, R (*F)(T)> struct deleter {
+ template <typename U> void operator()(U &ptr) const { (*F)(ptr); }
+};
+
+template <typename T> struct default_delete {
+ void operator()(T &ptr) const { delete ptr; }
+};
+
+template <typename T, typename Deleter = deleter<void, void *, &::free>>
+class handle {
+ struct dummy;
+ T _val;
+ bool _empty;
+
+#ifdef BACKWARD_ATLEAST_CXX11
+ handle(const handle &) = delete;
+ handle &operator=(const handle &) = delete;
+#endif
+
+public:
+ ~handle() {
+ if (!_empty) {
+ Deleter()(_val);
+ }
+ }
+
+ explicit handle() : _val(), _empty(true) {}
+ explicit handle(T val) : _val(val), _empty(false) {
+ if (!_val)
+ _empty = true;
+ }
+
+#ifdef BACKWARD_ATLEAST_CXX11
+ handle(handle &&from) : _empty(true) { swap(from); }
+ handle &operator=(handle &&from) {
+ swap(from);
+ return *this;
+ }
+#else
+ explicit handle(const handle &from) : _empty(true) {
+ // some sort of poor man's move semantic.
+ swap(const_cast<handle &>(from));
+ }
+ handle &operator=(const handle &from) {
+ // some sort of poor man's move semantic.
+ swap(const_cast<handle &>(from));
+ return *this;
+ }
+#endif
+
+ void reset(T new_val) {
+ handle tmp(new_val);
+ swap(tmp);
+ }
+
+ void update(T new_val) {
+ _val = new_val;
+ _empty = !static_cast<bool>(new_val);
+ }
+
+ operator const dummy *() const {
+ if (_empty) {
+ return nullptr;
+ }
+ return reinterpret_cast<const dummy *>(_val);
+ }
+ T get() { return _val; }
+ T release() {
+ _empty = true;
+ return _val;
+ }
+ void swap(handle &b) {
+ using std::swap;
+ swap(b._val, _val); // can throw, we are safe here.
+ swap(b._empty, _empty); // should not throw: if you cannot swap two
+ // bools without throwing... It's a lost cause anyway!
+ }
+
+ T &operator->() { return _val; }
+ const T &operator->() const { return _val; }
+
+ typedef typename rm_ptr<T>::type &ref_t;
+ typedef const typename rm_ptr<T>::type &const_ref_t;
+ ref_t operator*() { return *_val; }
+ const_ref_t operator*() const { return *_val; }
+ ref_t operator[](size_t idx) { return _val[idx]; }
+
+ // Watch out, we've got a badass over here
+ T *operator&() {
+ _empty = false;
+ return &_val;
+ }
+};
+
+// Default demangler implementation (do nothing).
+template <typename TAG> struct demangler_impl {
+ static std::string demangle(const char *funcname) { return funcname; }
+};
+
+#if defined(BACKWARD_SYSTEM_LINUX) || defined(BACKWARD_SYSTEM_DARWIN)
+
+template <> struct demangler_impl<system_tag::current_tag> {
+ demangler_impl() : _demangle_buffer_length(0) {}
+
+ std::string demangle(const char *funcname) {
+ using namespace details;
+ char *result = abi::__cxa_demangle(funcname, _demangle_buffer.get(),
+ &_demangle_buffer_length, nullptr);
+ if (result) {
+ _demangle_buffer.update(result);
+ return result;
+ }
+ return funcname;
+ }
+
+private:
+ details::handle<char *> _demangle_buffer;
+ size_t _demangle_buffer_length;
+};
+
+#endif // BACKWARD_SYSTEM_LINUX || BACKWARD_SYSTEM_DARWIN
+
+struct demangler : public demangler_impl<system_tag::current_tag> {};
+
+// Split a string on the platform's PATH delimiter. Example: if delimiter
+// is ":" then:
+// "" --> []
+// ":" --> ["",""]
+// "::" --> ["","",""]
+// "/a/b/c" --> ["/a/b/c"]
+// "/a/b/c:/d/e/f" --> ["/a/b/c","/d/e/f"]
+// etc.
+inline std::vector<std::string> split_source_prefixes(const std::string &s) {
+ std::vector<std::string> out;
+ size_t last = 0;
+ size_t next = 0;
+ size_t delimiter_size = sizeof(kBackwardPathDelimiter) - 1;
+ while ((next = s.find(kBackwardPathDelimiter, last)) != std::string::npos) {
+ out.push_back(s.substr(last, next - last));
+ last = next + delimiter_size;
+ }
+ if (last <= s.length()) {
+ out.push_back(s.substr(last));
+ }
+ return out;
+}
+
+} // namespace details
+
+/*************** A TRACE ***************/
+
+struct Trace {
+ void *addr;
+ size_t idx;
+
+ Trace() : addr(nullptr), idx(0) {}
+
+ explicit Trace(void *_addr, size_t _idx) : addr(_addr), idx(_idx) {}
+};
+
+struct ResolvedTrace : public Trace {
+
+ struct SourceLoc {
+ std::string function;
+ std::string filename;
+ unsigned line;
+ unsigned col;
+
+ SourceLoc() : line(0), col(0) {}
+
+ bool operator==(const SourceLoc &b) const {
+ return function == b.function && filename == b.filename &&
+ line == b.line && col == b.col;
+ }
+
+ bool operator!=(const SourceLoc &b) const { return !(*this == b); }
+ };
+
+ // In which binary object this trace is located.
+ std::string object_filename;
+
+ // The function in the object that contain the trace. This is not the same
+ // as source.function which can be an function inlined in object_function.
+ std::string object_function;
+
+ // The source location of this trace. It is possible for filename to be
+ // empty and for line/col to be invalid (value 0) if this information
+ // couldn't be deduced, for example if there is no debug information in the
+ // binary object.
+ SourceLoc source;
+
+ // An optionals list of "inliners". All the successive sources location
+ // from where the source location of the trace (the attribute right above)
+ // is inlined. It is especially useful when you compiled with optimization.
+ typedef std::vector<SourceLoc> source_locs_t;
+ source_locs_t inliners;
+
+ ResolvedTrace() : Trace() {}
+ ResolvedTrace(const Trace &mini_trace) : Trace(mini_trace) {}
+};
+
+/*************** STACK TRACE ***************/
+
+// default implemention.
+template <typename TAG> class StackTraceImpl {
+public:
+ size_t size() const { return 0; }
+ Trace operator[](size_t) const { return Trace(); }
+ size_t load_here(size_t = 0) { return 0; }
+ size_t load_from(void *, size_t = 0, void * = nullptr, void * = nullptr) {
+ return 0;
+ }
+ size_t thread_id() const { return 0; }
+ void skip_n_firsts(size_t) {}
+};
+
+class StackTraceImplBase {
+public:
+ StackTraceImplBase()
+ : _thread_id(0), _skip(0), _context(nullptr), _error_addr(nullptr) {}
+
+ size_t thread_id() const { return _thread_id; }
+
+ void skip_n_firsts(size_t n) { _skip = n; }
+
+protected:
+ void load_thread_info() {
+#ifdef BACKWARD_SYSTEM_LINUX
+#ifndef __ANDROID__
+ _thread_id = static_cast<size_t>(syscall(SYS_gettid));
+#else
+ _thread_id = static_cast<size_t>(gettid());
+#endif
+ if (_thread_id == static_cast<size_t>(getpid())) {
+ // If the thread is the main one, let's hide that.
+ // I like to keep little secret sometimes.
+ _thread_id = 0;
+ }
+#elif defined(BACKWARD_SYSTEM_DARWIN)
+ _thread_id = reinterpret_cast<size_t>(pthread_self());
+ if (pthread_main_np() == 1) {
+ // If the thread is the main one, let's hide that.
+ _thread_id = 0;
+ }
+#endif
+ }
+
+ void set_context(void *context) { _context = context; }
+ void *context() const { return _context; }
+
+ void set_error_addr(void *error_addr) { _error_addr = error_addr; }
+ void *error_addr() const { return _error_addr; }
+
+ size_t skip_n_firsts() const { return _skip; }
+
+private:
+ size_t _thread_id;
+ size_t _skip;
+ void *_context;
+ void *_error_addr;
+};
+
+class StackTraceImplHolder : public StackTraceImplBase {
+public:
+ size_t size() const {
+ return (_stacktrace.size() >= skip_n_firsts())
+ ? _stacktrace.size() - skip_n_firsts()
+ : 0;
+ }
+ Trace operator[](size_t idx) const {
+ if (idx >= size()) {
+ return Trace();
+ }
+ return Trace(_stacktrace[idx + skip_n_firsts()], idx);
+ }
+ void *const *begin() const {
+ if (size()) {
+ return &_stacktrace[skip_n_firsts()];
+ }
+ return nullptr;
+ }
+
+protected:
+ std::vector<void *> _stacktrace;
+};
+
+#if BACKWARD_HAS_UNWIND == 1
+
+namespace details {
+
+template <typename F> class Unwinder {
+public:
+ size_t operator()(F &f, size_t depth) {
+ _f = &f;
+ _index = -1;
+ _depth = depth;
+ _Unwind_Backtrace(&this->backtrace_trampoline, this);
+ return static_cast<size_t>(_index);
+ }
+
+private:
+ F *_f;
+ ssize_t _index;
+ size_t _depth;
+
+ static _Unwind_Reason_Code backtrace_trampoline(_Unwind_Context *ctx,
+ void *self) {
+ return (static_cast<Unwinder *>(self))->backtrace(ctx);
+ }
+
+ _Unwind_Reason_Code backtrace(_Unwind_Context *ctx) {
+ if (_index >= 0 && static_cast<size_t>(_index) >= _depth)
+ return _URC_END_OF_STACK;
+
+ int ip_before_instruction = 0;
+ uintptr_t ip = _Unwind_GetIPInfo(ctx, &ip_before_instruction);
+
+ if (!ip_before_instruction) {
+ // calculating 0-1 for unsigned, looks like a possible bug to sanitiziers,
+ // so let's do it explicitly:
+ if (ip == 0) {
+ ip = std::numeric_limits<uintptr_t>::max(); // set it to 0xffff... (as
+ // from casting 0-1)
+ } else {
+ ip -= 1; // else just normally decrement it (no overflow/underflow will
+ // happen)
+ }
+ }
+
+ if (_index >= 0) { // ignore first frame.
+ (*_f)(static_cast<size_t>(_index), reinterpret_cast<void *>(ip));
+ }
+ _index += 1;
+ return _URC_NO_REASON;
+ }
+};
+
+template <typename F> size_t unwind(F f, size_t depth) {
+ Unwinder<F> unwinder;
+ return unwinder(f, depth);
+}
+
+} // namespace details
+
+template <>
+class StackTraceImpl<system_tag::current_tag> : public StackTraceImplHolder {
+public:
+ NOINLINE
+ size_t load_here(size_t depth = 32, void *context = nullptr,
+ void *error_addr = nullptr) {
+ load_thread_info();
+ set_context(context);
+ set_error_addr(error_addr);
+ if (depth == 0) {
+ return 0;
+ }
+ _stacktrace.resize(depth);
+ size_t trace_cnt = details::unwind(callback(*this), depth);
+ _stacktrace.resize(trace_cnt);
+ skip_n_firsts(0);
+ return size();
+ }
+ size_t load_from(void *addr, size_t depth = 32, void *context = nullptr,
+ void *error_addr = nullptr) {
+ load_here(depth + 8, context, error_addr);
+
+ for (size_t i = 0; i < _stacktrace.size(); ++i) {
+ if (_stacktrace[i] == addr) {
+ skip_n_firsts(i);
+ break;
+ }
+ }
+
+ _stacktrace.resize(std::min(_stacktrace.size(), skip_n_firsts() + depth));
+ return size();
+ }
+
+private:
+ struct callback {
+ StackTraceImpl &self;
+ callback(StackTraceImpl &_self) : self(_self) {}
+
+ void operator()(size_t idx, void *addr) { self._stacktrace[idx] = addr; }
+ };
+};
+
+#elif BACKWARD_HAS_LIBUNWIND == 1
+
+template <>
+class StackTraceImpl<system_tag::current_tag> : public StackTraceImplHolder {
+public:
+ __attribute__((noinline)) size_t load_here(size_t depth = 32,
+ void *_context = nullptr,
+ void *_error_addr = nullptr) {
+ set_context(_context);
+ set_error_addr(_error_addr);
+ load_thread_info();
+ if (depth == 0) {
+ return 0;
+ }
+ _stacktrace.resize(depth + 1);
+
+ int result = 0;
+
+ unw_context_t ctx;
+ size_t index = 0;
+
+ // Add the tail call. If the Instruction Pointer is the crash address it
+ // means we got a bad function pointer dereference, so we "unwind" the
+ // bad pointer manually by using the return address pointed to by the
+ // Stack Pointer as the Instruction Pointer and letting libunwind do
+ // the rest
+
+ if (context()) {
+ ucontext_t *uctx = reinterpret_cast<ucontext_t *>(context());
+#ifdef REG_RIP // x86_64
+ if (uctx->uc_mcontext.gregs[REG_RIP] ==
+ reinterpret_cast<greg_t>(error_addr())) {
+ uctx->uc_mcontext.gregs[REG_RIP] =
+ *reinterpret_cast<size_t *>(uctx->uc_mcontext.gregs[REG_RSP]);
+ }
+ _stacktrace[index] =
+ reinterpret_cast<void *>(uctx->uc_mcontext.gregs[REG_RIP]);
+ ++index;
+ ctx = *reinterpret_cast<unw_context_t *>(uctx);
+#elif defined(REG_EIP) // x86_32
+ if (uctx->uc_mcontext.gregs[REG_EIP] ==
+ reinterpret_cast<greg_t>(error_addr())) {
+ uctx->uc_mcontext.gregs[REG_EIP] =
+ *reinterpret_cast<size_t *>(uctx->uc_mcontext.gregs[REG_ESP]);
+ }
+ _stacktrace[index] =
+ reinterpret_cast<void *>(uctx->uc_mcontext.gregs[REG_EIP]);
+ ++index;
+ ctx = *reinterpret_cast<unw_context_t *>(uctx);
+#elif defined(__arm__)
+ // libunwind uses its own context type for ARM unwinding.
+ // Copy the registers from the signal handler's context so we can
+ // unwind
+ unw_getcontext(&ctx);
+ ctx.regs[UNW_ARM_R0] = uctx->uc_mcontext.arm_r0;
+ ctx.regs[UNW_ARM_R1] = uctx->uc_mcontext.arm_r1;
+ ctx.regs[UNW_ARM_R2] = uctx->uc_mcontext.arm_r2;
+ ctx.regs[UNW_ARM_R3] = uctx->uc_mcontext.arm_r3;
+ ctx.regs[UNW_ARM_R4] = uctx->uc_mcontext.arm_r4;
+ ctx.regs[UNW_ARM_R5] = uctx->uc_mcontext.arm_r5;
+ ctx.regs[UNW_ARM_R6] = uctx->uc_mcontext.arm_r6;
+ ctx.regs[UNW_ARM_R7] = uctx->uc_mcontext.arm_r7;
+ ctx.regs[UNW_ARM_R8] = uctx->uc_mcontext.arm_r8;
+ ctx.regs[UNW_ARM_R9] = uctx->uc_mcontext.arm_r9;
+ ctx.regs[UNW_ARM_R10] = uctx->uc_mcontext.arm_r10;
+ ctx.regs[UNW_ARM_R11] = uctx->uc_mcontext.arm_fp;
+ ctx.regs[UNW_ARM_R12] = uctx->uc_mcontext.arm_ip;
+ ctx.regs[UNW_ARM_R13] = uctx->uc_mcontext.arm_sp;
+ ctx.regs[UNW_ARM_R14] = uctx->uc_mcontext.arm_lr;
+ ctx.regs[UNW_ARM_R15] = uctx->uc_mcontext.arm_pc;
+
+ // If we have crashed in the PC use the LR instead, as this was
+ // a bad function dereference
+ if (reinterpret_cast<unsigned long>(error_addr()) ==
+ uctx->uc_mcontext.arm_pc) {
+ ctx.regs[UNW_ARM_R15] =
+ uctx->uc_mcontext.arm_lr - sizeof(unsigned long);
+ }
+ _stacktrace[index] = reinterpret_cast<void *>(ctx.regs[UNW_ARM_R15]);
+ ++index;
+#elif defined(__APPLE__) && defined(__x86_64__)
+ unw_getcontext(&ctx);
+ // OS X's implementation of libunwind uses its own context object
+ // so we need to convert the passed context to libunwind's format
+ // (information about the data layout taken from unw_getcontext.s
+ // in Apple's libunwind source
+ ctx.data[0] = uctx->uc_mcontext->__ss.__rax;
+ ctx.data[1] = uctx->uc_mcontext->__ss.__rbx;
+ ctx.data[2] = uctx->uc_mcontext->__ss.__rcx;
+ ctx.data[3] = uctx->uc_mcontext->__ss.__rdx;
+ ctx.data[4] = uctx->uc_mcontext->__ss.__rdi;
+ ctx.data[5] = uctx->uc_mcontext->__ss.__rsi;
+ ctx.data[6] = uctx->uc_mcontext->__ss.__rbp;
+ ctx.data[7] = uctx->uc_mcontext->__ss.__rsp;
+ ctx.data[8] = uctx->uc_mcontext->__ss.__r8;
+ ctx.data[9] = uctx->uc_mcontext->__ss.__r9;
+ ctx.data[10] = uctx->uc_mcontext->__ss.__r10;
+ ctx.data[11] = uctx->uc_mcontext->__ss.__r11;
+ ctx.data[12] = uctx->uc_mcontext->__ss.__r12;
+ ctx.data[13] = uctx->uc_mcontext->__ss.__r13;
+ ctx.data[14] = uctx->uc_mcontext->__ss.__r14;
+ ctx.data[15] = uctx->uc_mcontext->__ss.__r15;
+ ctx.data[16] = uctx->uc_mcontext->__ss.__rip;
+
+ // If the IP is the same as the crash address we have a bad function
+ // dereference The caller's address is pointed to by %rsp, so we
+ // dereference that value and set it to be the next frame's IP.
+ if (uctx->uc_mcontext->__ss.__rip ==
+ reinterpret_cast<__uint64_t>(error_addr())) {
+ ctx.data[16] =
+ *reinterpret_cast<__uint64_t *>(uctx->uc_mcontext->__ss.__rsp);
+ }
+ _stacktrace[index] = reinterpret_cast<void *>(ctx.data[16]);
+ ++index;
+#elif defined(__APPLE__)
+ unw_getcontext(&ctx)
+ // TODO: Convert the ucontext_t to libunwind's unw_context_t like
+ // we do in 64 bits
+ if (ctx.uc_mcontext->__ss.__eip ==
+ reinterpret_cast<greg_t>(error_addr())) {
+ ctx.uc_mcontext->__ss.__eip = ctx.uc_mcontext->__ss.__esp;
+ }
+ _stacktrace[index] =
+ reinterpret_cast<void *>(ctx.uc_mcontext->__ss.__eip);
+ ++index;
+#endif
+ }
+
+ unw_cursor_t cursor;
+ if (context()) {
+ result = unw_init_local2(&cursor, &ctx, UNW_INIT_SIGNAL_FRAME);
+ } else {
+ unw_getcontext(&ctx);
+ ;
+ result = unw_init_local(&cursor, &ctx);
+ }
+
+ if (result != 0)
+ return 1;
+
+ unw_word_t ip = 0;
+
+ while (index <= depth && unw_step(&cursor) > 0) {
+ result = unw_get_reg(&cursor, UNW_REG_IP, &ip);
+ if (result == 0) {
+ _stacktrace[index] = reinterpret_cast<void *>(--ip);
+ ++index;
+ }
+ }
+ --index;
+
+ _stacktrace.resize(index + 1);
+ skip_n_firsts(0);
+ return size();
+ }
+
+ size_t load_from(void *addr, size_t depth = 32, void *context = nullptr,
+ void *error_addr = nullptr) {
+ load_here(depth + 8, context, error_addr);
+
+ for (size_t i = 0; i < _stacktrace.size(); ++i) {
+ if (_stacktrace[i] == addr) {
+ skip_n_firsts(i);
+ _stacktrace[i] = (void *)((uintptr_t)_stacktrace[i]);
+ break;
+ }
+ }
+
+ _stacktrace.resize(std::min(_stacktrace.size(), skip_n_firsts() + depth));
+ return size();
+ }
+};
+
+#elif defined(BACKWARD_HAS_BACKTRACE)
+
+template <>
+class StackTraceImpl<system_tag::current_tag> : public StackTraceImplHolder {
+public:
+ NOINLINE
+ size_t load_here(size_t depth = 32, void *context = nullptr,
+ void *error_addr = nullptr) {
+ set_context(context);
+ set_error_addr(error_addr);
+ load_thread_info();
+ if (depth == 0) {
+ return 0;
+ }
+ _stacktrace.resize(depth + 1);
+ size_t trace_cnt = backtrace(&_stacktrace[0], _stacktrace.size());
+ _stacktrace.resize(trace_cnt);
+ skip_n_firsts(1);
+ return size();
+ }
+
+ size_t load_from(void *addr, size_t depth = 32, void *context = nullptr,
+ void *error_addr = nullptr) {
+ load_here(depth + 8, contxt, error_addr);
+
+ for (size_t i = 0; i < _stacktrace.size(); ++i) {
+ if (_stacktrace[i] == addr) {
+ skip_n_firsts(i);
+ _stacktrace[i] = (void *)((uintptr_t)_stacktrace[i] + 1);
+ break;
+ }
+ }
+
+ _stacktrace.resize(std::min(_stacktrace.size(), skip_n_firsts() + depth));
+ return size();
+ }
+};
+
+#elif defined(BACKWARD_SYSTEM_WINDOWS)
+
+template <>
+class StackTraceImpl<system_tag::current_tag> : public StackTraceImplHolder {
+public:
+ // We have to load the machine type from the image info
+ // So we first initialize the resolver, and it tells us this info
+ void set_machine_type(DWORD machine_type) { machine_type_ = machine_type; }
+ void set_context(CONTEXT *ctx) { ctx_ = ctx; }
+ void set_thread_handle(HANDLE handle) { thd_ = handle; }
+
+ NOINLINE
+ size_t load_here(size_t depth = 32, void *context = nullptr,
+ void *error_addr = nullptr) {
+ set_context(static_cast<CONTEXT*>(context));
+ set_error_addr(error_addr);
+ CONTEXT localCtx; // used when no context is provided
+
+ if (depth == 0) {
+ return 0;
+ }
+
+ if (!ctx_) {
+ ctx_ = &localCtx;
+ RtlCaptureContext(ctx_);
+ }
+
+ if (!thd_) {
+ thd_ = GetCurrentThread();
+ }
+
+ HANDLE process = GetCurrentProcess();
+
+ STACKFRAME64 s;
+ memset(&s, 0, sizeof(STACKFRAME64));
+
+ // TODO: 32 bit context capture
+ s.AddrStack.Mode = AddrModeFlat;
+ s.AddrFrame.Mode = AddrModeFlat;
+ s.AddrPC.Mode = AddrModeFlat;
+#ifdef _M_X64
+ s.AddrPC.Offset = ctx_->Rip;
+ s.AddrStack.Offset = ctx_->Rsp;
+ s.AddrFrame.Offset = ctx_->Rbp;
+#else
+ s.AddrPC.Offset = ctx_->Eip;
+ s.AddrStack.Offset = ctx_->Esp;
+ s.AddrFrame.Offset = ctx_->Ebp;
+#endif
+
+ if (!machine_type_) {
+#ifdef _M_X64
+ machine_type_ = IMAGE_FILE_MACHINE_AMD64;
+#else
+ machine_type_ = IMAGE_FILE_MACHINE_I386;
+#endif
+ }
+
+ for (;;) {
+ // NOTE: this only works if PDBs are already loaded!
+ SetLastError(0);
+ if (!StackWalk64(machine_type_, process, thd_, &s, ctx_, NULL,
+ SymFunctionTableAccess64, SymGetModuleBase64, NULL))
+ break;
+
+ if (s.AddrReturn.Offset == 0)
+ break;
+
+ _stacktrace.push_back(reinterpret_cast<void *>(s.AddrPC.Offset));
+
+ if (size() >= depth)
+ break;
+ }
+
+ return size();
+ }
+
+ size_t load_from(void *addr, size_t depth = 32, void *context = nullptr,
+ void *error_addr = nullptr) {
+ load_here(depth + 8, context, error_addr);
+
+ for (size_t i = 0; i < _stacktrace.size(); ++i) {
+ if (_stacktrace[i] == addr) {
+ skip_n_firsts(i);
+ break;
+ }
+ }
+
+ _stacktrace.resize(std::min(_stacktrace.size(), skip_n_firsts() + depth));
+ return size();
+ }
+
+private:
+ DWORD machine_type_ = 0;
+ HANDLE thd_ = 0;
+ CONTEXT *ctx_ = nullptr;
+};
+
+#endif
+
+class StackTrace : public StackTraceImpl<system_tag::current_tag> {};
+
+/*************** TRACE RESOLVER ***************/
+
+class TraceResolverImplBase {
+public:
+ virtual ~TraceResolverImplBase() {}
+
+ virtual void load_addresses(void *const*addresses, int address_count) {
+ (void)addresses;
+ (void)address_count;
+ }
+
+ template <class ST> void load_stacktrace(ST &st) {
+ load_addresses(st.begin(), (int)st.size());
+ }
+
+ virtual ResolvedTrace resolve(ResolvedTrace t) { return t; }
+
+protected:
+ std::string demangle(const char *funcname) {
+ return _demangler.demangle(funcname);
+ }
+
+private:
+ details::demangler _demangler;
+};
+
+template <typename TAG> class TraceResolverImpl;
+
+#ifdef BACKWARD_SYSTEM_UNKNOWN
+
+template <> class TraceResolverImpl<system_tag::unknown_tag>
+ : public TraceResolverImplBase {};
+
+#endif
+
+#ifdef BACKWARD_SYSTEM_LINUX
+
+class TraceResolverLinuxBase : public TraceResolverImplBase {
+public:
+ TraceResolverLinuxBase()
+ : argv0_(get_argv0()), exec_path_(read_symlink("/proc/self/exe")) {}
+ std::string resolve_exec_path(Dl_info &symbol_info) const {
+ // mutates symbol_info.dli_fname to be filename to open and returns filename
+ // to display
+ if (symbol_info.dli_fname == argv0_) {
+ // dladdr returns argv[0] in dli_fname for symbols contained in
+ // the main executable, which is not a valid path if the
+ // executable was found by a search of the PATH environment
+ // variable; In that case, we actually open /proc/self/exe, which
+ // is always the actual executable (even if it was deleted/replaced!)
+ // but display the path that /proc/self/exe links to.
+ // However, this right away reduces probability of successful symbol
+ // resolution, because libbfd may try to find *.debug files in the
+ // same dir, in case symbols are stripped. As a result, it may try
+ // to find a file /proc/self/<exe_name>.debug, which obviously does
+ // not exist. /proc/self/exe is a last resort. First load attempt
+ // should go for the original executable file path.
+ symbol_info.dli_fname = "/proc/self/exe";
+ return exec_path_;
+ } else {
+ return symbol_info.dli_fname;
+ }
+ }
+
+private:
+ std::string argv0_;
+ std::string exec_path_;
+
+ static std::string get_argv0() {
+ std::string argv0;
+ std::ifstream ifs("/proc/self/cmdline");
+ std::getline(ifs, argv0, '\0');
+ return argv0;
+ }
+
+ static std::string read_symlink(std::string const &symlink_path) {
+ std::string path;
+ path.resize(100);
+
+ while (true) {
+ ssize_t len =
+ ::readlink(symlink_path.c_str(), &*path.begin(), path.size());
+ if (len < 0) {
+ return "";
+ }
+ if (static_cast<size_t>(len) == path.size()) {
+ path.resize(path.size() * 2);
+ } else {
+ path.resize(static_cast<std::string::size_type>(len));
+ break;
+ }
+ }
+
+ return path;
+ }
+};
+
+template <typename STACKTRACE_TAG> class TraceResolverLinuxImpl;
+
+#if BACKWARD_HAS_BACKTRACE_SYMBOL == 1
+
+template <>
+class TraceResolverLinuxImpl<trace_resolver_tag::backtrace_symbol>
+ : public TraceResolverLinuxBase {
+public:
+ void load_addresses(void *const*addresses, int address_count) override {
+ if (address_count == 0) {
+ return;
+ }
+ _symbols.reset(backtrace_symbols(addresses, address_count));
+ }
+
+ ResolvedTrace resolve(ResolvedTrace trace) override {
+ char *filename = _symbols[trace.idx];
+ char *funcname = filename;
+ while (*funcname && *funcname != '(') {
+ funcname += 1;
+ }
+ trace.object_filename.assign(filename,
+ funcname); // ok even if funcname is the ending
+ // \0 (then we assign entire string)
+
+ if (*funcname) { // if it's not end of string (e.g. from last frame ip==0)
+ funcname += 1;
+ char *funcname_end = funcname;
+ while (*funcname_end && *funcname_end != ')' && *funcname_end != '+') {
+ funcname_end += 1;
+ }
+ *funcname_end = '\0';
+ trace.object_function = this->demangle(funcname);
+ trace.source.function = trace.object_function; // we cannot do better.
+ }
+ return trace;
+ }
+
+private:
+ details::handle<char **> _symbols;
+};
+
+#endif // BACKWARD_HAS_BACKTRACE_SYMBOL == 1
+
+#if BACKWARD_HAS_BFD == 1
+
+template <>
+class TraceResolverLinuxImpl<trace_resolver_tag::libbfd>
+ : public TraceResolverLinuxBase {
+public:
+ TraceResolverLinuxImpl() : _bfd_loaded(false) {}
+
+ ResolvedTrace resolve(ResolvedTrace trace) override {
+ Dl_info symbol_info;
+
+ // trace.addr is a virtual address in memory pointing to some code.
+ // Let's try to find from which loaded object it comes from.
+ // The loaded object can be yourself btw.
+ if (!dladdr(trace.addr, &symbol_info)) {
+ return trace; // dat broken trace...
+ }
+
+ // Now we get in symbol_info:
+ // .dli_fname:
+ // pathname of the shared object that contains the address.
+ // .dli_fbase:
+ // where the object is loaded in memory.
+ // .dli_sname:
+ // the name of the nearest symbol to trace.addr, we expect a
+ // function name.
+ // .dli_saddr:
+ // the exact address corresponding to .dli_sname.
+
+ if (symbol_info.dli_sname) {
+ trace.object_function = demangle(symbol_info.dli_sname);
+ }
+
+ if (!symbol_info.dli_fname) {
+ return trace;
+ }
+
+ trace.object_filename = resolve_exec_path(symbol_info);
+ bfd_fileobject *fobj;
+ // Before rushing to resolution need to ensure the executable
+ // file still can be used. For that compare inode numbers of
+ // what is stored by the executable's file path, and in the
+ // dli_fname, which not necessarily equals to the executable.
+ // It can be a shared library, or /proc/self/exe, and in the
+ // latter case has drawbacks. See the exec path resolution for
+ // details. In short - the dli object should be used only as
+ // the last resort.
+ // If inode numbers are equal, it is known dli_fname and the
+ // executable file are the same. This is guaranteed by Linux,
+ // because if the executable file is changed/deleted, it will
+ // be done in a new inode. The old file will be preserved in
+ // /proc/self/exe, and may even have inode 0. The latter can
+ // happen if the inode was actually reused, and the file was
+ // kept only in the main memory.
+ //
+ struct stat obj_stat;
+ struct stat dli_stat;
+ if (stat(trace.object_filename.c_str(), &obj_stat) == 0 &&
+ stat(symbol_info.dli_fname, &dli_stat) == 0 &&
+ obj_stat.st_ino == dli_stat.st_ino) {
+ // The executable file, and the shared object containing the
+ // address are the same file. Safe to use the original path.
+ // this is preferable. Libbfd will search for stripped debug
+ // symbols in the same directory.
+ fobj = load_object_with_bfd(trace.object_filename);
+ } else{
+ // The original object file was *deleted*! The only hope is
+ // that the debug symbols are either inside the shared
+ // object file, or are in the same directory, and this is
+ // not /proc/self/exe.
+ fobj = nullptr;
+ }
+ if (fobj == nullptr || !fobj->handle) {
+ fobj = load_object_with_bfd(symbol_info.dli_fname);
+ if (!fobj->handle) {
+ return trace;
+ }
+ }
+
+ find_sym_result *details_selected; // to be filled.
+
+ // trace.addr is the next instruction to be executed after returning
+ // from the nested stack frame. In C++ this usually relate to the next
+ // statement right after the function call that leaded to a new stack
+ // frame. This is not usually what you want to see when printing out a
+ // stacktrace...
+ find_sym_result details_call_site =
+ find_symbol_details(fobj, trace.addr, symbol_info.dli_fbase);
+ details_selected = &details_call_site;
+
+#if BACKWARD_HAS_UNWIND == 0
+ // ...this is why we also try to resolve the symbol that is right
+ // before the return address. If we are lucky enough, we will get the
+ // line of the function that was called. But if the code is optimized,
+ // we might get something absolutely not related since the compiler
+ // can reschedule the return address with inline functions and
+ // tail-call optimisation (among other things that I don't even know
+ // or cannot even dream about with my tiny limited brain).
+ find_sym_result details_adjusted_call_site = find_symbol_details(
+ fobj, (void *)(uintptr_t(trace.addr) - 1), symbol_info.dli_fbase);
+
+ // In debug mode, we should always get the right thing(TM).
+ if (details_call_site.found && details_adjusted_call_site.found) {
+ // Ok, we assume that details_adjusted_call_site is a better estimation.
+ details_selected = &details_adjusted_call_site;
+ trace.addr = (void *)(uintptr_t(trace.addr) - 1);
+ }
+
+ if (details_selected == &details_call_site && details_call_site.found) {
+ // we have to re-resolve the symbol in order to reset some
+ // internal state in BFD... so we can call backtrace_inliners
+ // thereafter...
+ details_call_site =
+ find_symbol_details(fobj, trace.addr, symbol_info.dli_fbase);
+ }
+#endif // BACKWARD_HAS_UNWIND
+
+ if (details_selected->found) {
+ if (details_selected->filename) {
+ trace.source.filename = details_selected->filename;
+ }
+ trace.source.line = details_selected->line;
+
+ if (details_selected->funcname) {
+ // this time we get the name of the function where the code is
+ // located, instead of the function were the address is
+ // located. In short, if the code was inlined, we get the
+ // function correspoding to the code. Else we already got in
+ // trace.function.
+ trace.source.function = demangle(details_selected->funcname);
+
+ if (!symbol_info.dli_sname) {
+ // for the case dladdr failed to find the symbol name of
+ // the function, we might as well try to put something
+ // here.
+ trace.object_function = trace.source.function;
+ }
+ }
+
+ // Maybe the source of the trace got inlined inside the function
+ // (trace.source.function). Let's see if we can get all the inlined
+ // calls along the way up to the initial call site.
+ trace.inliners = backtrace_inliners(fobj, *details_selected);
+
+#if 0
+ if (trace.inliners.size() == 0) {
+ // Maybe the trace was not inlined... or maybe it was and we
+ // are lacking the debug information. Let's try to make the
+ // world better and see if we can get the line number of the
+ // function (trace.source.function) now.
+ //
+ // We will get the location of where the function start (to be
+ // exact: the first instruction that really start the
+ // function), not where the name of the function is defined.
+ // This can be quite far away from the name of the function
+ // btw.
+ //
+ // If the source of the function is the same as the source of
+ // the trace, we cannot say if the trace was really inlined or
+ // not. However, if the filename of the source is different
+ // between the function and the trace... we can declare it as
+ // an inliner. This is not 100% accurate, but better than
+ // nothing.
+
+ if (symbol_info.dli_saddr) {
+ find_sym_result details = find_symbol_details(fobj,
+ symbol_info.dli_saddr,
+ symbol_info.dli_fbase);
+
+ if (details.found) {
+ ResolvedTrace::SourceLoc diy_inliner;
+ diy_inliner.line = details.line;
+ if (details.filename) {
+ diy_inliner.filename = details.filename;
+ }
+ if (details.funcname) {
+ diy_inliner.function = demangle(details.funcname);
+ } else {
+ diy_inliner.function = trace.source.function;
+ }
+ if (diy_inliner != trace.source) {
+ trace.inliners.push_back(diy_inliner);
+ }
+ }
+ }
+ }
+#endif
+ }
+
+ return trace;
+ }
+
+private:
+ bool _bfd_loaded;
+
+ typedef details::handle<bfd *,
+ details::deleter<bfd_boolean, bfd *, &bfd_close>>
+ bfd_handle_t;
+
+ typedef details::handle<asymbol **> bfd_symtab_t;
+
+ struct bfd_fileobject {
+ bfd_handle_t handle;
+ bfd_vma base_addr;
+ bfd_symtab_t symtab;
+ bfd_symtab_t dynamic_symtab;
+ };
+
+ typedef details::hashtable<std::string, bfd_fileobject>::type fobj_bfd_map_t;
+ fobj_bfd_map_t _fobj_bfd_map;
+
+ bfd_fileobject *load_object_with_bfd(const std::string &filename_object) {
+ using namespace details;
+
+ if (!_bfd_loaded) {
+ using namespace details;
+ bfd_init();
+ _bfd_loaded = true;
+ }
+
+ fobj_bfd_map_t::iterator it = _fobj_bfd_map.find(filename_object);
+ if (it != _fobj_bfd_map.end()) {
+ return &it->second;
+ }
+
+ // this new object is empty for now.
+ bfd_fileobject *r = &_fobj_bfd_map[filename_object];
+
+ // we do the work temporary in this one;
+ bfd_handle_t bfd_handle;
+
+ int fd = open(filename_object.c_str(), O_RDONLY);
+ bfd_handle.reset(bfd_fdopenr(filename_object.c_str(), "default", fd));
+ if (!bfd_handle) {
+ close(fd);
+ return r;
+ }
+
+ if (!bfd_check_format(bfd_handle.get(), bfd_object)) {
+ return r; // not an object? You lose.
+ }
+
+ if ((bfd_get_file_flags(bfd_handle.get()) & HAS_SYMS) == 0) {
+ return r; // that's what happen when you forget to compile in debug.
+ }
+
+ ssize_t symtab_storage_size = bfd_get_symtab_upper_bound(bfd_handle.get());
+
+ ssize_t dyn_symtab_storage_size =
+ bfd_get_dynamic_symtab_upper_bound(bfd_handle.get());
+
+ if (symtab_storage_size <= 0 && dyn_symtab_storage_size <= 0) {
+ return r; // weird, is the file is corrupted?
+ }
+
+ bfd_symtab_t symtab, dynamic_symtab;
+ ssize_t symcount = 0, dyn_symcount = 0;
+
+ if (symtab_storage_size > 0) {
+ symtab.reset(static_cast<bfd_symbol **>(
+ malloc(static_cast<size_t>(symtab_storage_size))));
+ symcount = bfd_canonicalize_symtab(bfd_handle.get(), symtab.get());
+ }
+
+ if (dyn_symtab_storage_size > 0) {
+ dynamic_symtab.reset(static_cast<bfd_symbol **>(
+ malloc(static_cast<size_t>(dyn_symtab_storage_size))));
+ dyn_symcount = bfd_canonicalize_dynamic_symtab(bfd_handle.get(),
+ dynamic_symtab.get());
+ }
+
+ if (symcount <= 0 && dyn_symcount <= 0) {
+ return r; // damned, that's a stripped file that you got there!
+ }
+
+ r->handle = move(bfd_handle);
+ r->symtab = move(symtab);
+ r->dynamic_symtab = move(dynamic_symtab);
+ return r;
+ }
+
+ struct find_sym_result {
+ bool found;
+ const char *filename;
+ const char *funcname;
+ unsigned int line;
+ };
+
+ struct find_sym_context {
+ TraceResolverLinuxImpl *self;
+ bfd_fileobject *fobj;
+ void *addr;
+ void *base_addr;
+ find_sym_result result;
+ };
+
+ find_sym_result find_symbol_details(bfd_fileobject *fobj, void *addr,
+ void *base_addr) {
+ find_sym_context context;
+ context.self = this;
+ context.fobj = fobj;
+ context.addr = addr;
+ context.base_addr = base_addr;
+ context.result.found = false;
+ bfd_map_over_sections(fobj->handle.get(), &find_in_section_trampoline,
+ static_cast<void *>(&context));
+ return context.result;
+ }
+
+ static void find_in_section_trampoline(bfd *, asection *section, void *data) {
+ find_sym_context *context = static_cast<find_sym_context *>(data);
+ context->self->find_in_section(
+ reinterpret_cast<bfd_vma>(context->addr),
+ reinterpret_cast<bfd_vma>(context->base_addr), context->fobj, section,
+ context->result);
+ }
+
+ void find_in_section(bfd_vma addr, bfd_vma base_addr, bfd_fileobject *fobj,
+ asection *section, find_sym_result &result) {
+ if (result.found)
+ return;
+
+#ifdef bfd_get_section_flags
+ if ((bfd_get_section_flags(fobj->handle.get(), section) & SEC_ALLOC) == 0)
+#else
+ if ((bfd_section_flags(section) & SEC_ALLOC) == 0)
+#endif
+ return; // a debug section is never loaded automatically.
+
+#ifdef bfd_get_section_vma
+ bfd_vma sec_addr = bfd_get_section_vma(fobj->handle.get(), section);
+#else
+ bfd_vma sec_addr = bfd_section_vma(section);
+#endif
+#ifdef bfd_get_section_size
+ bfd_size_type size = bfd_get_section_size(section);
+#else
+ bfd_size_type size = bfd_section_size(section);
+#endif
+
+ // are we in the boundaries of the section?
+ if (addr < sec_addr || addr >= sec_addr + size) {
+ addr -= base_addr; // oups, a relocated object, lets try again...
+ if (addr < sec_addr || addr >= sec_addr + size) {
+ return;
+ }
+ }
+
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wzero-as-null-pointer-constant"
+#endif
+ if (!result.found && fobj->symtab) {
+ result.found = bfd_find_nearest_line(
+ fobj->handle.get(), section, fobj->symtab.get(), addr - sec_addr,
+ &result.filename, &result.funcname, &result.line);
+ }
+
+ if (!result.found && fobj->dynamic_symtab) {
+ result.found = bfd_find_nearest_line(
+ fobj->handle.get(), section, fobj->dynamic_symtab.get(),
+ addr - sec_addr, &result.filename, &result.funcname, &result.line);
+ }
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+ }
+
+ ResolvedTrace::source_locs_t
+ backtrace_inliners(bfd_fileobject *fobj, find_sym_result previous_result) {
+ // This function can be called ONLY after a SUCCESSFUL call to
+ // find_symbol_details. The state is global to the bfd_handle.
+ ResolvedTrace::source_locs_t results;
+ while (previous_result.found) {
+ find_sym_result result;
+ result.found = bfd_find_inliner_info(fobj->handle.get(), &result.filename,
+ &result.funcname, &result.line);
+
+ if (result
+ .found) /* and not (
+ cstrings_eq(previous_result.filename,
+ result.filename) and
+ cstrings_eq(previous_result.funcname, result.funcname)
+ and result.line == previous_result.line
+ )) */
+ {
+ ResolvedTrace::SourceLoc src_loc;
+ src_loc.line = result.line;
+ if (result.filename) {
+ src_loc.filename = result.filename;
+ }
+ if (result.funcname) {
+ src_loc.function = demangle(result.funcname);
+ }
+ results.push_back(src_loc);
+ }
+ previous_result = result;
+ }
+ return results;
+ }
+
+ bool cstrings_eq(const char *a, const char *b) {
+ if (!a || !b) {
+ return false;
+ }
+ return strcmp(a, b) == 0;
+ }
+};
+#endif // BACKWARD_HAS_BFD == 1
+
+#if BACKWARD_HAS_DW == 1
+
+template <>
+class TraceResolverLinuxImpl<trace_resolver_tag::libdw>
+ : public TraceResolverLinuxBase {
+public:
+ TraceResolverLinuxImpl() : _dwfl_handle_initialized(false) {}
+
+ ResolvedTrace resolve(ResolvedTrace trace) override {
+ using namespace details;
+
+ Dwarf_Addr trace_addr = (Dwarf_Addr)trace.addr;
+
+ if (!_dwfl_handle_initialized) {
+ // initialize dwfl...
+ _dwfl_cb.reset(new Dwfl_Callbacks);
+ _dwfl_cb->find_elf = &dwfl_linux_proc_find_elf;
+ _dwfl_cb->find_debuginfo = &dwfl_standard_find_debuginfo;
+ _dwfl_cb->debuginfo_path = 0;
+
+ _dwfl_handle.reset(dwfl_begin(_dwfl_cb.get()));
+ _dwfl_handle_initialized = true;
+
+ if (!_dwfl_handle) {
+ return trace;
+ }
+
+ // ...from the current process.
+ dwfl_report_begin(_dwfl_handle.get());
+ int r = dwfl_linux_proc_report(_dwfl_handle.get(), getpid());
+ dwfl_report_end(_dwfl_handle.get(), NULL, NULL);
+ if (r < 0) {
+ return trace;
+ }
+ }
+
+ if (!_dwfl_handle) {
+ return trace;
+ }
+
+ // find the module (binary object) that contains the trace's address.
+ // This is not using any debug information, but the addresses ranges of
+ // all the currently loaded binary object.
+ Dwfl_Module *mod = dwfl_addrmodule(_dwfl_handle.get(), trace_addr);
+ if (mod) {
+ // now that we found it, lets get the name of it, this will be the
+ // full path to the running binary or one of the loaded library.
+ const char *module_name = dwfl_module_info(mod, 0, 0, 0, 0, 0, 0, 0);
+ if (module_name) {
+ trace.object_filename = module_name;
+ }
+ // We also look after the name of the symbol, equal or before this
+ // address. This is found by walking the symtab. We should get the
+ // symbol corresponding to the function (mangled) containing the
+ // address. If the code corresponding to the address was inlined,
+ // this is the name of the out-most inliner function.
+ const char *sym_name = dwfl_module_addrname(mod, trace_addr);
+ if (sym_name) {
+ trace.object_function = demangle(sym_name);
+ }
+ }
+
+ // now let's get serious, and find out the source location (file and
+ // line number) of the address.
+
+ // This function will look in .debug_aranges for the address and map it
+ // to the location of the compilation unit DIE in .debug_info and
+ // return it.
+ Dwarf_Addr mod_bias = 0;
+ Dwarf_Die *cudie = dwfl_module_addrdie(mod, trace_addr, &mod_bias);
+
+#if 1
+ if (!cudie) {
+ // Sadly clang does not generate the section .debug_aranges, thus
+ // dwfl_module_addrdie will fail early. Clang doesn't either set
+ // the lowpc/highpc/range info for every compilation unit.
+ //
+ // So in order to save the world:
+ // for every compilation unit, we will iterate over every single
+ // DIEs. Normally functions should have a lowpc/highpc/range, which
+ // we will use to infer the compilation unit.
+
+ // note that this is probably badly inefficient.
+ while ((cudie = dwfl_module_nextcu(mod, cudie, &mod_bias))) {
+ Dwarf_Die die_mem;
+ Dwarf_Die *fundie =
+ find_fundie_by_pc(cudie, trace_addr - mod_bias, &die_mem);
+ if (fundie) {
+ break;
+ }
+ }
+ }
+#endif
+
+//#define BACKWARD_I_DO_NOT_RECOMMEND_TO_ENABLE_THIS_HORRIBLE_PIECE_OF_CODE
+#ifdef BACKWARD_I_DO_NOT_RECOMMEND_TO_ENABLE_THIS_HORRIBLE_PIECE_OF_CODE
+ if (!cudie) {
+ // If it's still not enough, lets dive deeper in the shit, and try
+ // to save the world again: for every compilation unit, we will
+ // load the corresponding .debug_line section, and see if we can
+ // find our address in it.
+
+ Dwarf_Addr cfi_bias;
+ Dwarf_CFI *cfi_cache = dwfl_module_eh_cfi(mod, &cfi_bias);
+
+ Dwarf_Addr bias;
+ while ((cudie = dwfl_module_nextcu(mod, cudie, &bias))) {
+ if (dwarf_getsrc_die(cudie, trace_addr - bias)) {
+
+ // ...but if we get a match, it might be a false positive
+ // because our (address - bias) might as well be valid in a
+ // different compilation unit. So we throw our last card on
+ // the table and lookup for the address into the .eh_frame
+ // section.
+
+ handle<Dwarf_Frame *> frame;
+ dwarf_cfi_addrframe(cfi_cache, trace_addr - cfi_bias, &frame);
+ if (frame) {
+ break;
+ }
+ }
+ }
+ }
+#endif
+
+ if (!cudie) {
+ return trace; // this time we lost the game :/
+ }
+
+ // Now that we have a compilation unit DIE, this function will be able
+ // to load the corresponding section in .debug_line (if not already
+ // loaded) and hopefully find the source location mapped to our
+ // address.
+ Dwarf_Line *srcloc = dwarf_getsrc_die(cudie, trace_addr - mod_bias);
+
+ if (srcloc) {
+ const char *srcfile = dwarf_linesrc(srcloc, 0, 0);
+ if (srcfile) {
+ trace.source.filename = srcfile;
+ }
+ int line = 0, col = 0;
+ dwarf_lineno(srcloc, &line);
+ dwarf_linecol(srcloc, &col);
+ trace.source.line = line;
+ trace.source.col = col;
+ }
+
+ deep_first_search_by_pc(cudie, trace_addr - mod_bias,
+ inliners_search_cb(trace));
+ if (trace.source.function.size() == 0) {
+ // fallback.
+ trace.source.function = trace.object_function;
+ }
+
+ return trace;
+ }
+
+private:
+ typedef details::handle<Dwfl *, details::deleter<void, Dwfl *, &dwfl_end>>
+ dwfl_handle_t;
+ details::handle<Dwfl_Callbacks *, details::default_delete<Dwfl_Callbacks *>>
+ _dwfl_cb;
+ dwfl_handle_t _dwfl_handle;
+ bool _dwfl_handle_initialized;
+
+ // defined here because in C++98, template function cannot take locally
+ // defined types... grrr.
+ struct inliners_search_cb {
+ void operator()(Dwarf_Die *die) {
+ switch (dwarf_tag(die)) {
+ const char *name;
+ case DW_TAG_subprogram:
+ if ((name = dwarf_diename(die))) {
+ trace.source.function = name;
+ }
+ break;
+
+ case DW_TAG_inlined_subroutine:
+ ResolvedTrace::SourceLoc sloc;
+ Dwarf_Attribute attr_mem;
+
+ if ((name = dwarf_diename(die))) {
+ sloc.function = name;
+ }
+ if ((name = die_call_file(die))) {
+ sloc.filename = name;
+ }
+
+ Dwarf_Word line = 0, col = 0;
+ dwarf_formudata(dwarf_attr(die, DW_AT_call_line, &attr_mem), &line);
+ dwarf_formudata(dwarf_attr(die, DW_AT_call_column, &attr_mem), &col);
+ sloc.line = (unsigned)line;
+ sloc.col = (unsigned)col;
+
+ trace.inliners.push_back(sloc);
+ break;
+ };
+ }
+ ResolvedTrace &trace;
+ inliners_search_cb(ResolvedTrace &t) : trace(t) {}
+ };
+
+ static bool die_has_pc(Dwarf_Die *die, Dwarf_Addr pc) {
+ Dwarf_Addr low, high;
+
+ // continuous range
+ if (dwarf_hasattr(die, DW_AT_low_pc) && dwarf_hasattr(die, DW_AT_high_pc)) {
+ if (dwarf_lowpc(die, &low) != 0) {
+ return false;
+ }
+ if (dwarf_highpc(die, &high) != 0) {
+ Dwarf_Attribute attr_mem;
+ Dwarf_Attribute *attr = dwarf_attr(die, DW_AT_high_pc, &attr_mem);
+ Dwarf_Word value;
+ if (dwarf_formudata(attr, &value) != 0) {
+ return false;
+ }
+ high = low + value;
+ }
+ return pc >= low && pc < high;
+ }
+
+ // non-continuous range.
+ Dwarf_Addr base;
+ ptrdiff_t offset = 0;
+ while ((offset = dwarf_ranges(die, offset, &base, &low, &high)) > 0) {
+ if (pc >= low && pc < high) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ static Dwarf_Die *find_fundie_by_pc(Dwarf_Die *parent_die, Dwarf_Addr pc,
+ Dwarf_Die *result) {
+ if (dwarf_child(parent_die, result) != 0) {
+ return 0;
+ }
+
+ Dwarf_Die *die = result;
+ do {
+ switch (dwarf_tag(die)) {
+ case DW_TAG_subprogram:
+ case DW_TAG_inlined_subroutine:
+ if (die_has_pc(die, pc)) {
+ return result;
+ }
+ };
+ bool declaration = false;
+ Dwarf_Attribute attr_mem;
+ dwarf_formflag(dwarf_attr(die, DW_AT_declaration, &attr_mem),
+ &declaration);
+ if (!declaration) {
+ // let's be curious and look deeper in the tree,
+ // function are not necessarily at the first level, but
+ // might be nested inside a namespace, structure etc.
+ Dwarf_Die die_mem;
+ Dwarf_Die *indie = find_fundie_by_pc(die, pc, &die_mem);
+ if (indie) {
+ *result = die_mem;
+ return result;
+ }
+ }
+ } while (dwarf_siblingof(die, result) == 0);
+ return 0;
+ }
+
+ template <typename CB>
+ static bool deep_first_search_by_pc(Dwarf_Die *parent_die, Dwarf_Addr pc,
+ CB cb) {
+ Dwarf_Die die_mem;
+ if (dwarf_child(parent_die, &die_mem) != 0) {
+ return false;
+ }
+
+ bool branch_has_pc = false;
+ Dwarf_Die *die = &die_mem;
+ do {
+ bool declaration = false;
+ Dwarf_Attribute attr_mem;
+ dwarf_formflag(dwarf_attr(die, DW_AT_declaration, &attr_mem),
+ &declaration);
+ if (!declaration) {
+ // let's be curious and look deeper in the tree, function are
+ // not necessarily at the first level, but might be nested
+ // inside a namespace, structure, a function, an inlined
+ // function etc.
+ branch_has_pc = deep_first_search_by_pc(die, pc, cb);
+ }
+ if (!branch_has_pc) {
+ branch_has_pc = die_has_pc(die, pc);
+ }
+ if (branch_has_pc) {
+ cb(die);
+ }
+ } while (dwarf_siblingof(die, &die_mem) == 0);
+ return branch_has_pc;
+ }
+
+ static const char *die_call_file(Dwarf_Die *die) {
+ Dwarf_Attribute attr_mem;
+ Dwarf_Word file_idx = 0;
+
+ dwarf_formudata(dwarf_attr(die, DW_AT_call_file, &attr_mem), &file_idx);
+
+ if (file_idx == 0) {
+ return 0;
+ }
+
+ Dwarf_Die die_mem;
+ Dwarf_Die *cudie = dwarf_diecu(die, &die_mem, 0, 0);
+ if (!cudie) {
+ return 0;
+ }
+
+ Dwarf_Files *files = 0;
+ size_t nfiles;
+ dwarf_getsrcfiles(cudie, &files, &nfiles);
+ if (!files) {
+ return 0;
+ }
+
+ return dwarf_filesrc(files, file_idx, 0, 0);
+ }
+};
+#endif // BACKWARD_HAS_DW == 1
+
+#if BACKWARD_HAS_DWARF == 1
+
+template <>
+class TraceResolverLinuxImpl<trace_resolver_tag::libdwarf>
+ : public TraceResolverLinuxBase {
+public:
+ TraceResolverLinuxImpl() : _dwarf_loaded(false) {}
+
+ ResolvedTrace resolve(ResolvedTrace trace) override {
+ // trace.addr is a virtual address in memory pointing to some code.
+ // Let's try to find from which loaded object it comes from.
+ // The loaded object can be yourself btw.
+
+ Dl_info symbol_info;
+ int dladdr_result = 0;
+#if defined(__GLIBC__)
+ link_map *link_map;
+ // We request the link map so we can get information about offsets
+ dladdr_result =
+ dladdr1(trace.addr, &symbol_info, reinterpret_cast<void **>(&link_map),
+ RTLD_DL_LINKMAP);
+#else
+ // Android doesn't have dladdr1. Don't use the linker map.
+ dladdr_result = dladdr(trace.addr, &symbol_info);
+#endif
+ if (!dladdr_result) {
+ return trace; // dat broken trace...
+ }
+
+ // Now we get in symbol_info:
+ // .dli_fname:
+ // pathname of the shared object that contains the address.
+ // .dli_fbase:
+ // where the object is loaded in memory.
+ // .dli_sname:
+ // the name of the nearest symbol to trace.addr, we expect a
+ // function name.
+ // .dli_saddr:
+ // the exact address corresponding to .dli_sname.
+ //
+ // And in link_map:
+ // .l_addr:
+ // difference between the address in the ELF file and the address
+ // in memory
+ // l_name:
+ // absolute pathname where the object was found
+
+ if (symbol_info.dli_sname) {
+ trace.object_function = demangle(symbol_info.dli_sname);
+ }
+
+ if (!symbol_info.dli_fname) {
+ return trace;
+ }
+
+ trace.object_filename = resolve_exec_path(symbol_info);
+ dwarf_fileobject &fobj = load_object_with_dwarf(symbol_info.dli_fname);
+ if (!fobj.dwarf_handle) {
+ return trace; // sad, we couldn't load the object :(
+ }
+
+#if defined(__GLIBC__)
+ // Convert the address to a module relative one by looking at
+ // the module's loading address in the link map
+ Dwarf_Addr address = reinterpret_cast<uintptr_t>(trace.addr) -
+ reinterpret_cast<uintptr_t>(link_map->l_addr);
+#else
+ Dwarf_Addr address = reinterpret_cast<uintptr_t>(trace.addr);
+#endif
+
+ if (trace.object_function.empty()) {
+ symbol_cache_t::iterator it = fobj.symbol_cache.lower_bound(address);
+
+ if (it != fobj.symbol_cache.end()) {
+ if (it->first != address) {
+ if (it != fobj.symbol_cache.begin()) {
+ --it;
+ }
+ }
+ trace.object_function = demangle(it->second.c_str());
+ }
+ }
+
+ // Get the Compilation Unit DIE for the address
+ Dwarf_Die die = find_die(fobj, address);
+
+ if (!die) {
+ return trace; // this time we lost the game :/
+ }
+
+ // libdwarf doesn't give us direct access to its objects, it always
+ // allocates a copy for the caller. We keep that copy alive in a cache
+ // and we deallocate it later when it's no longer required.
+ die_cache_entry &die_object = get_die_cache(fobj, die);
+ if (die_object.isEmpty())
+ return trace; // We have no line section for this DIE
+
+ die_linemap_t::iterator it = die_object.line_section.lower_bound(address);
+
+ if (it != die_object.line_section.end()) {
+ if (it->first != address) {
+ if (it == die_object.line_section.begin()) {
+ // If we are on the first item of the line section
+ // but the address does not match it means that
+ // the address is below the range of the DIE. Give up.
+ return trace;
+ } else {
+ --it;
+ }
+ }
+ } else {
+ return trace; // We didn't find the address.
+ }
+
+ // Get the Dwarf_Line that the address points to and call libdwarf
+ // to get source file, line and column info.
+ Dwarf_Line line = die_object.line_buffer[it->second];
+ Dwarf_Error error = DW_DLE_NE;
+
+ char *filename;
+ if (dwarf_linesrc(line, &filename, &error) == DW_DLV_OK) {
+ trace.source.filename = std::string(filename);
+ dwarf_dealloc(fobj.dwarf_handle.get(), filename, DW_DLA_STRING);
+ }
+
+ Dwarf_Unsigned number = 0;
+ if (dwarf_lineno(line, &number, &error) == DW_DLV_OK) {
+ trace.source.line = number;
+ } else {
+ trace.source.line = 0;
+ }
+
+ if (dwarf_lineoff_b(line, &number, &error) == DW_DLV_OK) {
+ trace.source.col = number;
+ } else {
+ trace.source.col = 0;
+ }
+
+ std::vector<std::string> namespace_stack;
+ deep_first_search_by_pc(fobj, die, address, namespace_stack,
+ inliners_search_cb(trace, fobj, die));
+
+ dwarf_dealloc(fobj.dwarf_handle.get(), die, DW_DLA_DIE);
+
+ return trace;
+ }
+
+public:
+ static int close_dwarf(Dwarf_Debug dwarf) {
+ return dwarf_finish(dwarf, NULL);
+ }
+
+private:
+ bool _dwarf_loaded;
+
+ typedef details::handle<int, details::deleter<int, int, &::close>>
+ dwarf_file_t;
+
+ typedef details::handle<Elf *, details::deleter<int, Elf *, &elf_end>>
+ dwarf_elf_t;
+
+ typedef details::handle<Dwarf_Debug,
+ details::deleter<int, Dwarf_Debug, &close_dwarf>>
+ dwarf_handle_t;
+
+ typedef std::map<Dwarf_Addr, int> die_linemap_t;
+
+ typedef std::map<Dwarf_Off, Dwarf_Off> die_specmap_t;
+
+ struct die_cache_entry {
+ die_specmap_t spec_section;
+ die_linemap_t line_section;
+ Dwarf_Line *line_buffer;
+ Dwarf_Signed line_count;
+ Dwarf_Line_Context line_context;
+
+ inline bool isEmpty() {
+ return line_buffer == NULL || line_count == 0 || line_context == NULL ||
+ line_section.empty();
+ }
+
+ die_cache_entry() : line_buffer(0), line_count(0), line_context(0) {}
+
+ ~die_cache_entry() {
+ if (line_context) {
+ dwarf_srclines_dealloc_b(line_context);
+ }
+ }
+ };
+
+ typedef std::map<Dwarf_Off, die_cache_entry> die_cache_t;
+
+ typedef std::map<uintptr_t, std::string> symbol_cache_t;
+
+ struct dwarf_fileobject {
+ dwarf_file_t file_handle;
+ dwarf_elf_t elf_handle;
+ dwarf_handle_t dwarf_handle;
+ symbol_cache_t symbol_cache;
+
+ // Die cache
+ die_cache_t die_cache;
+ die_cache_entry *current_cu;
+ };
+
+ typedef details::hashtable<std::string, dwarf_fileobject>::type
+ fobj_dwarf_map_t;
+ fobj_dwarf_map_t _fobj_dwarf_map;
+
+ static bool cstrings_eq(const char *a, const char *b) {
+ if (!a || !b) {
+ return false;
+ }
+ return strcmp(a, b) == 0;
+ }
+
+ dwarf_fileobject &load_object_with_dwarf(const std::string &filename_object) {
+
+ if (!_dwarf_loaded) {
+ // Set the ELF library operating version
+ // If that fails there's nothing we can do
+ _dwarf_loaded = elf_version(EV_CURRENT) != EV_NONE;
+ }
+
+ fobj_dwarf_map_t::iterator it = _fobj_dwarf_map.find(filename_object);
+ if (it != _fobj_dwarf_map.end()) {
+ return it->second;
+ }
+
+ // this new object is empty for now
+ dwarf_fileobject &r = _fobj_dwarf_map[filename_object];
+
+ dwarf_file_t file_handle;
+ file_handle.reset(open(filename_object.c_str(), O_RDONLY));
+ if (file_handle.get() < 0) {
+ return r;
+ }
+
+ // Try to get an ELF handle. We need to read the ELF sections
+ // because we want to see if there is a .gnu_debuglink section
+ // that points to a split debug file
+ dwarf_elf_t elf_handle;
+ elf_handle.reset(elf_begin(file_handle.get(), ELF_C_READ, NULL));
+ if (!elf_handle) {
+ return r;
+ }
+
+ const char *e_ident = elf_getident(elf_handle.get(), 0);
+ if (!e_ident) {
+ return r;
+ }
+
+ // Get the number of sections
+ // We use the new APIs as elf_getshnum is deprecated
+ size_t shdrnum = 0;
+ if (elf_getshdrnum(elf_handle.get(), &shdrnum) == -1) {
+ return r;
+ }
+
+ // Get the index to the string section
+ size_t shdrstrndx = 0;
+ if (elf_getshdrstrndx(elf_handle.get(), &shdrstrndx) == -1) {
+ return r;
+ }
+
+ std::string debuglink;
+ // Iterate through the ELF sections to try to get a gnu_debuglink
+ // note and also to cache the symbol table.
+ // We go the preprocessor way to avoid having to create templated
+ // classes or using gelf (which might throw a compiler error if 64 bit
+ // is not supported
+#define ELF_GET_DATA(ARCH) \
+ Elf_Scn *elf_section = 0; \
+ Elf_Data *elf_data = 0; \
+ Elf##ARCH##_Shdr *section_header = 0; \
+ Elf_Scn *symbol_section = 0; \
+ size_t symbol_count = 0; \
+ size_t symbol_strings = 0; \
+ Elf##ARCH##_Sym *symbol = 0; \
+ const char *section_name = 0; \
+ \
+ while ((elf_section = elf_nextscn(elf_handle.get(), elf_section)) != NULL) { \
+ section_header = elf##ARCH##_getshdr(elf_section); \
+ if (section_header == NULL) { \
+ return r; \
+ } \
+ \
+ if ((section_name = elf_strptr(elf_handle.get(), shdrstrndx, \
+ section_header->sh_name)) == NULL) { \
+ return r; \
+ } \
+ \
+ if (cstrings_eq(section_name, ".gnu_debuglink")) { \
+ elf_data = elf_getdata(elf_section, NULL); \
+ if (elf_data && elf_data->d_size > 0) { \
+ debuglink = \
+ std::string(reinterpret_cast<const char *>(elf_data->d_buf)); \
+ } \
+ } \
+ \
+ switch (section_header->sh_type) { \
+ case SHT_SYMTAB: \
+ symbol_section = elf_section; \
+ symbol_count = section_header->sh_size / section_header->sh_entsize; \
+ symbol_strings = section_header->sh_link; \
+ break; \
+ \
+ /* We use .dynsyms as a last resort, we prefer .symtab */ \
+ case SHT_DYNSYM: \
+ if (!symbol_section) { \
+ symbol_section = elf_section; \
+ symbol_count = section_header->sh_size / section_header->sh_entsize; \
+ symbol_strings = section_header->sh_link; \
+ } \
+ break; \
+ } \
+ } \
+ \
+ if (symbol_section && symbol_count && symbol_strings) { \
+ elf_data = elf_getdata(symbol_section, NULL); \
+ symbol = reinterpret_cast<Elf##ARCH##_Sym *>(elf_data->d_buf); \
+ for (size_t i = 0; i < symbol_count; ++i) { \
+ int type = ELF##ARCH##_ST_TYPE(symbol->st_info); \
+ if (type == STT_FUNC && symbol->st_value > 0) { \
+ r.symbol_cache[symbol->st_value] = std::string( \
+ elf_strptr(elf_handle.get(), symbol_strings, symbol->st_name)); \
+ } \
+ ++symbol; \
+ } \
+ }
+
+ if (e_ident[EI_CLASS] == ELFCLASS32) {
+ ELF_GET_DATA(32)
+ } else if (e_ident[EI_CLASS] == ELFCLASS64) {
+ // libelf might have been built without 64 bit support
+#if __LIBELF64
+ ELF_GET_DATA(64)
+#endif
+ }
+
+ if (!debuglink.empty()) {
+ // We have a debuglink section! Open an elf instance on that
+ // file instead. If we can't open the file, then return
+ // the elf handle we had already opened.
+ dwarf_file_t debuglink_file;
+ debuglink_file.reset(open(debuglink.c_str(), O_RDONLY));
+ if (debuglink_file.get() > 0) {
+ dwarf_elf_t debuglink_elf;
+ debuglink_elf.reset(elf_begin(debuglink_file.get(), ELF_C_READ, NULL));
+
+ // If we have a valid elf handle, return the new elf handle
+ // and file handle and discard the original ones
+ if (debuglink_elf) {
+ elf_handle = move(debuglink_elf);
+ file_handle = move(debuglink_file);
+ }
+ }
+ }
+
+ // Ok, we have a valid ELF handle, let's try to get debug symbols
+ Dwarf_Debug dwarf_debug;
+ Dwarf_Error error = DW_DLE_NE;
+ dwarf_handle_t dwarf_handle;
+
+ int dwarf_result = dwarf_elf_init(elf_handle.get(), DW_DLC_READ, NULL, NULL,
+ &dwarf_debug, &error);
+
+ // We don't do any special handling for DW_DLV_NO_ENTRY specially.
+ // If we get an error, or the file doesn't have debug information
+ // we just return.
+ if (dwarf_result != DW_DLV_OK) {
+ return r;
+ }
+
+ dwarf_handle.reset(dwarf_debug);
+
+ r.file_handle = move(file_handle);
+ r.elf_handle = move(elf_handle);
+ r.dwarf_handle = move(dwarf_handle);
+
+ return r;
+ }
+
+ die_cache_entry &get_die_cache(dwarf_fileobject &fobj, Dwarf_Die die) {
+ Dwarf_Error error = DW_DLE_NE;
+
+ // Get the die offset, we use it as the cache key
+ Dwarf_Off die_offset;
+ if (dwarf_dieoffset(die, &die_offset, &error) != DW_DLV_OK) {
+ die_offset = 0;
+ }
+
+ die_cache_t::iterator it = fobj.die_cache.find(die_offset);
+
+ if (it != fobj.die_cache.end()) {
+ fobj.current_cu = &it->second;
+ return it->second;
+ }
+
+ die_cache_entry &de = fobj.die_cache[die_offset];
+ fobj.current_cu = &de;
+
+ Dwarf_Addr line_addr;
+ Dwarf_Small table_count;
+
+ // The addresses in the line section are not fully sorted (they might
+ // be sorted by block of code belonging to the same file), which makes
+ // it necessary to do so before searching is possible.
+ //
+ // As libdwarf allocates a copy of everything, let's get the contents
+ // of the line section and keep it around. We also create a map of
+ // program counter to line table indices so we can search by address
+ // and get the line buffer index.
+ //
+ // To make things more difficult, the same address can span more than
+ // one line, so we need to keep the index pointing to the first line
+ // by using insert instead of the map's [ operator.
+
+ // Get the line context for the DIE
+ if (dwarf_srclines_b(die, 0, &table_count, &de.line_context, &error) ==
+ DW_DLV_OK) {
+ // Get the source lines for this line context, to be deallocated
+ // later
+ if (dwarf_srclines_from_linecontext(de.line_context, &de.line_buffer,
+ &de.line_count,
+ &error) == DW_DLV_OK) {
+
+ // Add all the addresses to our map
+ for (int i = 0; i < de.line_count; i++) {
+ if (dwarf_lineaddr(de.line_buffer[i], &line_addr, &error) !=
+ DW_DLV_OK) {
+ line_addr = 0;
+ }
+ de.line_section.insert(std::pair<Dwarf_Addr, int>(line_addr, i));
+ }
+ }
+ }
+
+ // For each CU, cache the function DIEs that contain the
+ // DW_AT_specification attribute. When building with -g3 the function
+ // DIEs are separated in declaration and specification, with the
+ // declaration containing only the name and parameters and the
+ // specification the low/high pc and other compiler attributes.
+ //
+ // We cache those specifications so we don't skip over the declarations,
+ // because they have no pc, and we can do namespace resolution for
+ // DWARF function names.
+ Dwarf_Debug dwarf = fobj.dwarf_handle.get();
+ Dwarf_Die current_die = 0;
+ if (dwarf_child(die, &current_die, &error) == DW_DLV_OK) {
+ for (;;) {
+ Dwarf_Die sibling_die = 0;
+
+ Dwarf_Half tag_value;
+ dwarf_tag(current_die, &tag_value, &error);
+
+ if (tag_value == DW_TAG_subprogram ||
+ tag_value == DW_TAG_inlined_subroutine) {
+
+ Dwarf_Bool has_attr = 0;
+ if (dwarf_hasattr(current_die, DW_AT_specification, &has_attr,
+ &error) == DW_DLV_OK) {
+ if (has_attr) {
+ Dwarf_Attribute attr_mem;
+ if (dwarf_attr(current_die, DW_AT_specification, &attr_mem,
+ &error) == DW_DLV_OK) {
+ Dwarf_Off spec_offset = 0;
+ if (dwarf_formref(attr_mem, &spec_offset, &error) ==
+ DW_DLV_OK) {
+ Dwarf_Off spec_die_offset;
+ if (dwarf_dieoffset(current_die, &spec_die_offset, &error) ==
+ DW_DLV_OK) {
+ de.spec_section[spec_offset] = spec_die_offset;
+ }
+ }
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+ }
+ }
+ }
+
+ int result = dwarf_siblingof(dwarf, current_die, &sibling_die, &error);
+ if (result == DW_DLV_ERROR) {
+ break;
+ } else if (result == DW_DLV_NO_ENTRY) {
+ break;
+ }
+
+ if (current_die != die) {
+ dwarf_dealloc(dwarf, current_die, DW_DLA_DIE);
+ current_die = 0;
+ }
+
+ current_die = sibling_die;
+ }
+ }
+ return de;
+ }
+
+ static Dwarf_Die get_referenced_die(Dwarf_Debug dwarf, Dwarf_Die die,
+ Dwarf_Half attr, bool global) {
+ Dwarf_Error error = DW_DLE_NE;
+ Dwarf_Attribute attr_mem;
+
+ Dwarf_Die found_die = NULL;
+ if (dwarf_attr(die, attr, &attr_mem, &error) == DW_DLV_OK) {
+ Dwarf_Off offset;
+ int result = 0;
+ if (global) {
+ result = dwarf_global_formref(attr_mem, &offset, &error);
+ } else {
+ result = dwarf_formref(attr_mem, &offset, &error);
+ }
+
+ if (result == DW_DLV_OK) {
+ if (dwarf_offdie(dwarf, offset, &found_die, &error) != DW_DLV_OK) {
+ found_die = NULL;
+ }
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+ }
+ return found_die;
+ }
+
+ static std::string get_referenced_die_name(Dwarf_Debug dwarf, Dwarf_Die die,
+ Dwarf_Half attr, bool global) {
+ Dwarf_Error error = DW_DLE_NE;
+ std::string value;
+
+ Dwarf_Die found_die = get_referenced_die(dwarf, die, attr, global);
+
+ if (found_die) {
+ char *name;
+ if (dwarf_diename(found_die, &name, &error) == DW_DLV_OK) {
+ if (name) {
+ value = std::string(name);
+ }
+ dwarf_dealloc(dwarf, name, DW_DLA_STRING);
+ }
+ dwarf_dealloc(dwarf, found_die, DW_DLA_DIE);
+ }
+
+ return value;
+ }
+
+ // Returns a spec DIE linked to the passed one. The caller should
+ // deallocate the DIE
+ static Dwarf_Die get_spec_die(dwarf_fileobject &fobj, Dwarf_Die die) {
+ Dwarf_Debug dwarf = fobj.dwarf_handle.get();
+ Dwarf_Error error = DW_DLE_NE;
+ Dwarf_Off die_offset;
+ if (fobj.current_cu &&
+ dwarf_die_CU_offset(die, &die_offset, &error) == DW_DLV_OK) {
+ die_specmap_t::iterator it =
+ fobj.current_cu->spec_section.find(die_offset);
+
+ // If we have a DIE that completes the current one, check if
+ // that one has the pc we are looking for
+ if (it != fobj.current_cu->spec_section.end()) {
+ Dwarf_Die spec_die = 0;
+ if (dwarf_offdie(dwarf, it->second, &spec_die, &error) == DW_DLV_OK) {
+ return spec_die;
+ }
+ }
+ }
+
+ // Maybe we have an abstract origin DIE with the function information?
+ return get_referenced_die(fobj.dwarf_handle.get(), die,
+ DW_AT_abstract_origin, true);
+ }
+
+ static bool die_has_pc(dwarf_fileobject &fobj, Dwarf_Die die, Dwarf_Addr pc) {
+ Dwarf_Addr low_pc = 0, high_pc = 0;
+ Dwarf_Half high_pc_form = 0;
+ Dwarf_Form_Class return_class;
+ Dwarf_Error error = DW_DLE_NE;
+ Dwarf_Debug dwarf = fobj.dwarf_handle.get();
+ bool has_lowpc = false;
+ bool has_highpc = false;
+ bool has_ranges = false;
+
+ if (dwarf_lowpc(die, &low_pc, &error) == DW_DLV_OK) {
+ // If we have a low_pc check if there is a high pc.
+ // If we don't have a high pc this might mean we have a base
+ // address for the ranges list or just an address.
+ has_lowpc = true;
+
+ if (dwarf_highpc_b(die, &high_pc, &high_pc_form, &return_class, &error) ==
+ DW_DLV_OK) {
+ // We do have a high pc. In DWARF 4+ this is an offset from the
+ // low pc, but in earlier versions it's an absolute address.
+
+ has_highpc = true;
+ // In DWARF 2/3 this would be a DW_FORM_CLASS_ADDRESS
+ if (return_class == DW_FORM_CLASS_CONSTANT) {
+ high_pc = low_pc + high_pc;
+ }
+
+ // We have low and high pc, check if our address
+ // is in that range
+ return pc >= low_pc && pc < high_pc;
+ }
+ } else {
+ // Reset the low_pc, in case dwarf_lowpc failing set it to some
+ // undefined value.
+ low_pc = 0;
+ }
+
+ // Check if DW_AT_ranges is present and search for the PC in the
+ // returned ranges list. We always add the low_pc, as it not set it will
+ // be 0, in case we had a DW_AT_low_pc and DW_AT_ranges pair
+ bool result = false;
+
+ Dwarf_Attribute attr;
+ if (dwarf_attr(die, DW_AT_ranges, &attr, &error) == DW_DLV_OK) {
+
+ Dwarf_Off offset;
+ if (dwarf_global_formref(attr, &offset, &error) == DW_DLV_OK) {
+ Dwarf_Ranges *ranges;
+ Dwarf_Signed ranges_count = 0;
+ Dwarf_Unsigned byte_count = 0;
+
+ if (dwarf_get_ranges_a(dwarf, offset, die, &ranges, &ranges_count,
+ &byte_count, &error) == DW_DLV_OK) {
+ has_ranges = ranges_count != 0;
+ for (int i = 0; i < ranges_count; i++) {
+ if (ranges[i].dwr_addr1 != 0 &&
+ pc >= ranges[i].dwr_addr1 + low_pc &&
+ pc < ranges[i].dwr_addr2 + low_pc) {
+ result = true;
+ break;
+ }
+ }
+ dwarf_ranges_dealloc(dwarf, ranges, ranges_count);
+ }
+ }
+ }
+
+ // Last attempt. We might have a single address set as low_pc.
+ if (!result && low_pc != 0 && pc == low_pc) {
+ result = true;
+ }
+
+ // If we don't have lowpc, highpc and ranges maybe this DIE is a
+ // declaration that relies on a DW_AT_specification DIE that happens
+ // later. Use the specification cache we filled when we loaded this CU.
+ if (!result && (!has_lowpc && !has_highpc && !has_ranges)) {
+ Dwarf_Die spec_die = get_spec_die(fobj, die);
+ if (spec_die) {
+ result = die_has_pc(fobj, spec_die, pc);
+ dwarf_dealloc(dwarf, spec_die, DW_DLA_DIE);
+ }
+ }
+
+ return result;
+ }
+
+ static void get_type(Dwarf_Debug dwarf, Dwarf_Die die, std::string &type) {
+ Dwarf_Error error = DW_DLE_NE;
+
+ Dwarf_Die child = 0;
+ if (dwarf_child(die, &child, &error) == DW_DLV_OK) {
+ get_type(dwarf, child, type);
+ }
+
+ if (child) {
+ type.insert(0, "::");
+ dwarf_dealloc(dwarf, child, DW_DLA_DIE);
+ }
+
+ char *name;
+ if (dwarf_diename(die, &name, &error) == DW_DLV_OK) {
+ type.insert(0, std::string(name));
+ dwarf_dealloc(dwarf, name, DW_DLA_STRING);
+ } else {
+ type.insert(0, "<unknown>");
+ }
+ }
+
+ static std::string get_type_by_signature(Dwarf_Debug dwarf, Dwarf_Die die) {
+ Dwarf_Error error = DW_DLE_NE;
+
+ Dwarf_Sig8 signature;
+ Dwarf_Bool has_attr = 0;
+ if (dwarf_hasattr(die, DW_AT_signature, &has_attr, &error) == DW_DLV_OK) {
+ if (has_attr) {
+ Dwarf_Attribute attr_mem;
+ if (dwarf_attr(die, DW_AT_signature, &attr_mem, &error) == DW_DLV_OK) {
+ if (dwarf_formsig8(attr_mem, &signature, &error) != DW_DLV_OK) {
+ return std::string("<no type signature>");
+ }
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+ }
+ }
+
+ Dwarf_Unsigned next_cu_header;
+ Dwarf_Sig8 tu_signature;
+ std::string result;
+ bool found = false;
+
+ while (dwarf_next_cu_header_d(dwarf, 0, 0, 0, 0, 0, 0, 0, &tu_signature, 0,
+ &next_cu_header, 0, &error) == DW_DLV_OK) {
+
+ if (strncmp(signature.signature, tu_signature.signature, 8) == 0) {
+ Dwarf_Die type_cu_die = 0;
+ if (dwarf_siblingof_b(dwarf, 0, 0, &type_cu_die, &error) == DW_DLV_OK) {
+ Dwarf_Die child_die = 0;
+ if (dwarf_child(type_cu_die, &child_die, &error) == DW_DLV_OK) {
+ get_type(dwarf, child_die, result);
+ found = !result.empty();
+ dwarf_dealloc(dwarf, child_die, DW_DLA_DIE);
+ }
+ dwarf_dealloc(dwarf, type_cu_die, DW_DLA_DIE);
+ }
+ }
+ }
+
+ if (found) {
+ while (dwarf_next_cu_header_d(dwarf, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ &next_cu_header, 0, &error) == DW_DLV_OK) {
+ // Reset the cu header state. Unfortunately, libdwarf's
+ // next_cu_header API keeps its own iterator per Dwarf_Debug
+ // that can't be reset. We need to keep fetching elements until
+ // the end.
+ }
+ } else {
+ // If we couldn't resolve the type just print out the signature
+ std::ostringstream string_stream;
+ string_stream << "<0x" << std::hex << std::setfill('0');
+ for (int i = 0; i < 8; ++i) {
+ string_stream << std::setw(2) << std::hex
+ << (int)(unsigned char)(signature.signature[i]);
+ }
+ string_stream << ">";
+ result = string_stream.str();
+ }
+ return result;
+ }
+
+ struct type_context_t {
+ bool is_const;
+ bool is_typedef;
+ bool has_type;
+ bool has_name;
+ std::string text;
+
+ type_context_t()
+ : is_const(false), is_typedef(false), has_type(false), has_name(false) {
+ }
+ };
+
+ // Types are resolved from right to left: we get the variable name first
+ // and then all specifiers (like const or pointer) in a chain of DW_AT_type
+ // DIEs. Call this function recursively until we get a complete type
+ // string.
+ static void set_parameter_string(dwarf_fileobject &fobj, Dwarf_Die die,
+ type_context_t &context) {
+ char *name;
+ Dwarf_Error error = DW_DLE_NE;
+
+ // typedefs contain also the base type, so we skip it and only
+ // print the typedef name
+ if (!context.is_typedef) {
+ if (dwarf_diename(die, &name, &error) == DW_DLV_OK) {
+ if (!context.text.empty()) {
+ context.text.insert(0, " ");
+ }
+ context.text.insert(0, std::string(name));
+ dwarf_dealloc(fobj.dwarf_handle.get(), name, DW_DLA_STRING);
+ }
+ } else {
+ context.is_typedef = false;
+ context.has_type = true;
+ if (context.is_const) {
+ context.text.insert(0, "const ");
+ context.is_const = false;
+ }
+ }
+
+ bool next_type_is_const = false;
+ bool is_keyword = true;
+
+ Dwarf_Half tag = 0;
+ Dwarf_Bool has_attr = 0;
+ if (dwarf_tag(die, &tag, &error) == DW_DLV_OK) {
+ switch (tag) {
+ case DW_TAG_structure_type:
+ case DW_TAG_union_type:
+ case DW_TAG_class_type:
+ case DW_TAG_enumeration_type:
+ context.has_type = true;
+ if (dwarf_hasattr(die, DW_AT_signature, &has_attr, &error) ==
+ DW_DLV_OK) {
+ // If we have a signature it means the type is defined
+ // in .debug_types, so we need to load the DIE pointed
+ // at by the signature and resolve it
+ if (has_attr) {
+ std::string type =
+ get_type_by_signature(fobj.dwarf_handle.get(), die);
+ if (context.is_const)
+ type.insert(0, "const ");
+
+ if (!context.text.empty())
+ context.text.insert(0, " ");
+ context.text.insert(0, type);
+ }
+
+ // Treat enums like typedefs, and skip printing its
+ // base type
+ context.is_typedef = (tag == DW_TAG_enumeration_type);
+ }
+ break;
+ case DW_TAG_const_type:
+ next_type_is_const = true;
+ break;
+ case DW_TAG_pointer_type:
+ context.text.insert(0, "*");
+ break;
+ case DW_TAG_reference_type:
+ context.text.insert(0, "&");
+ break;
+ case DW_TAG_restrict_type:
+ context.text.insert(0, "restrict ");
+ break;
+ case DW_TAG_rvalue_reference_type:
+ context.text.insert(0, "&&");
+ break;
+ case DW_TAG_volatile_type:
+ context.text.insert(0, "volatile ");
+ break;
+ case DW_TAG_typedef:
+ // Propagate the const-ness to the next type
+ // as typedefs are linked to its base type
+ next_type_is_const = context.is_const;
+ context.is_typedef = true;
+ context.has_type = true;
+ break;
+ case DW_TAG_base_type:
+ context.has_type = true;
+ break;
+ case DW_TAG_formal_parameter:
+ context.has_name = true;
+ break;
+ default:
+ is_keyword = false;
+ break;
+ }
+ }
+
+ if (!is_keyword && context.is_const) {
+ context.text.insert(0, "const ");
+ }
+
+ context.is_const = next_type_is_const;
+
+ Dwarf_Die ref =
+ get_referenced_die(fobj.dwarf_handle.get(), die, DW_AT_type, true);
+ if (ref) {
+ set_parameter_string(fobj, ref, context);
+ dwarf_dealloc(fobj.dwarf_handle.get(), ref, DW_DLA_DIE);
+ }
+
+ if (!context.has_type && context.has_name) {
+ context.text.insert(0, "void ");
+ context.has_type = true;
+ }
+ }
+
+ // Resolve the function return type and parameters
+ static void set_function_parameters(std::string &function_name,
+ std::vector<std::string> &ns,
+ dwarf_fileobject &fobj, Dwarf_Die die) {
+ Dwarf_Debug dwarf = fobj.dwarf_handle.get();
+ Dwarf_Error error = DW_DLE_NE;
+ Dwarf_Die current_die = 0;
+ std::string parameters;
+ bool has_spec = true;
+ // Check if we have a spec DIE. If we do we use it as it contains
+ // more information, like parameter names.
+ Dwarf_Die spec_die = get_spec_die(fobj, die);
+ if (!spec_die) {
+ has_spec = false;
+ spec_die = die;
+ }
+
+ std::vector<std::string>::const_iterator it = ns.begin();
+ std::string ns_name;
+ for (it = ns.begin(); it < ns.end(); ++it) {
+ ns_name.append(*it).append("::");
+ }
+
+ if (!ns_name.empty()) {
+ function_name.insert(0, ns_name);
+ }
+
+ // See if we have a function return type. It can be either on the
+ // current die or in its spec one (usually true for inlined functions)
+ std::string return_type =
+ get_referenced_die_name(dwarf, die, DW_AT_type, true);
+ if (return_type.empty()) {
+ return_type = get_referenced_die_name(dwarf, spec_die, DW_AT_type, true);
+ }
+ if (!return_type.empty()) {
+ return_type.append(" ");
+ function_name.insert(0, return_type);
+ }
+
+ if (dwarf_child(spec_die, &current_die, &error) == DW_DLV_OK) {
+ for (;;) {
+ Dwarf_Die sibling_die = 0;
+
+ Dwarf_Half tag_value;
+ dwarf_tag(current_die, &tag_value, &error);
+
+ if (tag_value == DW_TAG_formal_parameter) {
+ // Ignore artificial (ie, compiler generated) parameters
+ bool is_artificial = false;
+ Dwarf_Attribute attr_mem;
+ if (dwarf_attr(current_die, DW_AT_artificial, &attr_mem, &error) ==
+ DW_DLV_OK) {
+ Dwarf_Bool flag = 0;
+ if (dwarf_formflag(attr_mem, &flag, &error) == DW_DLV_OK) {
+ is_artificial = flag != 0;
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+ }
+
+ if (!is_artificial) {
+ type_context_t context;
+ set_parameter_string(fobj, current_die, context);
+
+ if (parameters.empty()) {
+ parameters.append("(");
+ } else {
+ parameters.append(", ");
+ }
+ parameters.append(context.text);
+ }
+ }
+
+ int result = dwarf_siblingof(dwarf, current_die, &sibling_die, &error);
+ if (result == DW_DLV_ERROR) {
+ break;
+ } else if (result == DW_DLV_NO_ENTRY) {
+ break;
+ }
+
+ if (current_die != die) {
+ dwarf_dealloc(dwarf, current_die, DW_DLA_DIE);
+ current_die = 0;
+ }
+
+ current_die = sibling_die;
+ }
+ }
+ if (parameters.empty())
+ parameters = "(";
+ parameters.append(")");
+
+ // If we got a spec DIE we need to deallocate it
+ if (has_spec)
+ dwarf_dealloc(dwarf, spec_die, DW_DLA_DIE);
+
+ function_name.append(parameters);
+ }
+
+ // defined here because in C++98, template function cannot take locally
+ // defined types... grrr.
+ struct inliners_search_cb {
+ void operator()(Dwarf_Die die, std::vector<std::string> &ns) {
+ Dwarf_Error error = DW_DLE_NE;
+ Dwarf_Half tag_value;
+ Dwarf_Attribute attr_mem;
+ Dwarf_Debug dwarf = fobj.dwarf_handle.get();
+
+ dwarf_tag(die, &tag_value, &error);
+
+ switch (tag_value) {
+ char *name;
+ case DW_TAG_subprogram:
+ if (!trace.source.function.empty())
+ break;
+ if (dwarf_diename(die, &name, &error) == DW_DLV_OK) {
+ trace.source.function = std::string(name);
+ dwarf_dealloc(dwarf, name, DW_DLA_STRING);
+ } else {
+ // We don't have a function name in this DIE.
+ // Check if there is a referenced non-defining
+ // declaration.
+ trace.source.function =
+ get_referenced_die_name(dwarf, die, DW_AT_abstract_origin, true);
+ if (trace.source.function.empty()) {
+ trace.source.function =
+ get_referenced_die_name(dwarf, die, DW_AT_specification, true);
+ }
+ }
+
+ // Append the function parameters, if available
+ set_function_parameters(trace.source.function, ns, fobj, die);
+
+ // If the object function name is empty, it's possible that
+ // there is no dynamic symbol table (maybe the executable
+ // was stripped or not built with -rdynamic). See if we have
+ // a DWARF linkage name to use instead. We try both
+ // linkage_name and MIPS_linkage_name because the MIPS tag
+ // was the unofficial one until it was adopted in DWARF4.
+ // Old gcc versions generate MIPS_linkage_name
+ if (trace.object_function.empty()) {
+ details::demangler demangler;
+
+ if (dwarf_attr(die, DW_AT_linkage_name, &attr_mem, &error) !=
+ DW_DLV_OK) {
+ if (dwarf_attr(die, DW_AT_MIPS_linkage_name, &attr_mem, &error) !=
+ DW_DLV_OK) {
+ break;
+ }
+ }
+
+ char *linkage;
+ if (dwarf_formstring(attr_mem, &linkage, &error) == DW_DLV_OK) {
+ trace.object_function = demangler.demangle(linkage);
+ dwarf_dealloc(dwarf, linkage, DW_DLA_STRING);
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+ }
+ break;
+
+ case DW_TAG_inlined_subroutine:
+ ResolvedTrace::SourceLoc sloc;
+
+ if (dwarf_diename(die, &name, &error) == DW_DLV_OK) {
+ sloc.function = std::string(name);
+ dwarf_dealloc(dwarf, name, DW_DLA_STRING);
+ } else {
+ // We don't have a name for this inlined DIE, it could
+ // be that there is an abstract origin instead.
+ // Get the DW_AT_abstract_origin value, which is a
+ // reference to the source DIE and try to get its name
+ sloc.function =
+ get_referenced_die_name(dwarf, die, DW_AT_abstract_origin, true);
+ }
+
+ set_function_parameters(sloc.function, ns, fobj, die);
+
+ std::string file = die_call_file(dwarf, die, cu_die);
+ if (!file.empty())
+ sloc.filename = file;
+
+ Dwarf_Unsigned number = 0;
+ if (dwarf_attr(die, DW_AT_call_line, &attr_mem, &error) == DW_DLV_OK) {
+ if (dwarf_formudata(attr_mem, &number, &error) == DW_DLV_OK) {
+ sloc.line = number;
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+ }
+
+ if (dwarf_attr(die, DW_AT_call_column, &attr_mem, &error) ==
+ DW_DLV_OK) {
+ if (dwarf_formudata(attr_mem, &number, &error) == DW_DLV_OK) {
+ sloc.col = number;
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+ }
+
+ trace.inliners.push_back(sloc);
+ break;
+ };
+ }
+ ResolvedTrace &trace;
+ dwarf_fileobject &fobj;
+ Dwarf_Die cu_die;
+ inliners_search_cb(ResolvedTrace &t, dwarf_fileobject &f, Dwarf_Die c)
+ : trace(t), fobj(f), cu_die(c) {}
+ };
+
+ static Dwarf_Die find_fundie_by_pc(dwarf_fileobject &fobj,
+ Dwarf_Die parent_die, Dwarf_Addr pc,
+ Dwarf_Die result) {
+ Dwarf_Die current_die = 0;
+ Dwarf_Error error = DW_DLE_NE;
+ Dwarf_Debug dwarf = fobj.dwarf_handle.get();
+
+ if (dwarf_child(parent_die, &current_die, &error) != DW_DLV_OK) {
+ return NULL;
+ }
+
+ for (;;) {
+ Dwarf_Die sibling_die = 0;
+ Dwarf_Half tag_value;
+ dwarf_tag(current_die, &tag_value, &error);
+
+ switch (tag_value) {
+ case DW_TAG_subprogram:
+ case DW_TAG_inlined_subroutine:
+ if (die_has_pc(fobj, current_die, pc)) {
+ return current_die;
+ }
+ };
+ bool declaration = false;
+ Dwarf_Attribute attr_mem;
+ if (dwarf_attr(current_die, DW_AT_declaration, &attr_mem, &error) ==
+ DW_DLV_OK) {
+ Dwarf_Bool flag = 0;
+ if (dwarf_formflag(attr_mem, &flag, &error) == DW_DLV_OK) {
+ declaration = flag != 0;
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+ }
+
+ if (!declaration) {
+ // let's be curious and look deeper in the tree, functions are
+ // not necessarily at the first level, but might be nested
+ // inside a namespace, structure, a function, an inlined
+ // function etc.
+ Dwarf_Die die_mem = 0;
+ Dwarf_Die indie = find_fundie_by_pc(fobj, current_die, pc, die_mem);
+ if (indie) {
+ result = die_mem;
+ return result;
+ }
+ }
+
+ int res = dwarf_siblingof(dwarf, current_die, &sibling_die, &error);
+ if (res == DW_DLV_ERROR) {
+ return NULL;
+ } else if (res == DW_DLV_NO_ENTRY) {
+ break;
+ }
+
+ if (current_die != parent_die) {
+ dwarf_dealloc(dwarf, current_die, DW_DLA_DIE);
+ current_die = 0;
+ }
+
+ current_die = sibling_die;
+ }
+ return NULL;
+ }
+
+ template <typename CB>
+ static bool deep_first_search_by_pc(dwarf_fileobject &fobj,
+ Dwarf_Die parent_die, Dwarf_Addr pc,
+ std::vector<std::string> &ns, CB cb) {
+ Dwarf_Die current_die = 0;
+ Dwarf_Debug dwarf = fobj.dwarf_handle.get();
+ Dwarf_Error error = DW_DLE_NE;
+
+ if (dwarf_child(parent_die, &current_die, &error) != DW_DLV_OK) {
+ return false;
+ }
+
+ bool branch_has_pc = false;
+ bool has_namespace = false;
+ for (;;) {
+ Dwarf_Die sibling_die = 0;
+
+ Dwarf_Half tag;
+ if (dwarf_tag(current_die, &tag, &error) == DW_DLV_OK) {
+ if (tag == DW_TAG_namespace || tag == DW_TAG_class_type) {
+ char *ns_name = NULL;
+ if (dwarf_diename(current_die, &ns_name, &error) == DW_DLV_OK) {
+ if (ns_name) {
+ ns.push_back(std::string(ns_name));
+ } else {
+ ns.push_back("<unknown>");
+ }
+ dwarf_dealloc(dwarf, ns_name, DW_DLA_STRING);
+ } else {
+ ns.push_back("<unknown>");
+ }
+ has_namespace = true;
+ }
+ }
+
+ bool declaration = false;
+ Dwarf_Attribute attr_mem;
+ if (tag != DW_TAG_class_type &&
+ dwarf_attr(current_die, DW_AT_declaration, &attr_mem, &error) ==
+ DW_DLV_OK) {
+ Dwarf_Bool flag = 0;
+ if (dwarf_formflag(attr_mem, &flag, &error) == DW_DLV_OK) {
+ declaration = flag != 0;
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+ }
+
+ if (!declaration) {
+ // let's be curious and look deeper in the tree, function are
+ // not necessarily at the first level, but might be nested
+ // inside a namespace, structure, a function, an inlined
+ // function etc.
+ branch_has_pc = deep_first_search_by_pc(fobj, current_die, pc, ns, cb);
+ }
+
+ if (!branch_has_pc) {
+ branch_has_pc = die_has_pc(fobj, current_die, pc);
+ }
+
+ if (branch_has_pc) {
+ cb(current_die, ns);
+ }
+
+ int result = dwarf_siblingof(dwarf, current_die, &sibling_die, &error);
+ if (result == DW_DLV_ERROR) {
+ return false;
+ } else if (result == DW_DLV_NO_ENTRY) {
+ break;
+ }
+
+ if (current_die != parent_die) {
+ dwarf_dealloc(dwarf, current_die, DW_DLA_DIE);
+ current_die = 0;
+ }
+
+ if (has_namespace) {
+ has_namespace = false;
+ ns.pop_back();
+ }
+ current_die = sibling_die;
+ }
+
+ if (has_namespace) {
+ ns.pop_back();
+ }
+ return branch_has_pc;
+ }
+
+ static std::string die_call_file(Dwarf_Debug dwarf, Dwarf_Die die,
+ Dwarf_Die cu_die) {
+ Dwarf_Attribute attr_mem;
+ Dwarf_Error error = DW_DLE_NE;
+ Dwarf_Unsigned file_index;
+
+ std::string file;
+
+ if (dwarf_attr(die, DW_AT_call_file, &attr_mem, &error) == DW_DLV_OK) {
+ if (dwarf_formudata(attr_mem, &file_index, &error) != DW_DLV_OK) {
+ file_index = 0;
+ }
+ dwarf_dealloc(dwarf, attr_mem, DW_DLA_ATTR);
+
+ if (file_index == 0) {
+ return file;
+ }
+
+ char **srcfiles = 0;
+ Dwarf_Signed file_count = 0;
+ if (dwarf_srcfiles(cu_die, &srcfiles, &file_count, &error) == DW_DLV_OK) {
+ if (file_count > 0 && file_index <= static_cast<Dwarf_Unsigned>(file_count)) {
+ file = std::string(srcfiles[file_index - 1]);
+ }
+
+ // Deallocate all strings!
+ for (int i = 0; i < file_count; ++i) {
+ dwarf_dealloc(dwarf, srcfiles[i], DW_DLA_STRING);
+ }
+ dwarf_dealloc(dwarf, srcfiles, DW_DLA_LIST);
+ }
+ }
+ return file;
+ }
+
+ Dwarf_Die find_die(dwarf_fileobject &fobj, Dwarf_Addr addr) {
+ // Let's get to work! First see if we have a debug_aranges section so
+ // we can speed up the search
+
+ Dwarf_Debug dwarf = fobj.dwarf_handle.get();
+ Dwarf_Error error = DW_DLE_NE;
+ Dwarf_Arange *aranges;
+ Dwarf_Signed arange_count;
+
+ Dwarf_Die returnDie;
+ bool found = false;
+ if (dwarf_get_aranges(dwarf, &aranges, &arange_count, &error) !=
+ DW_DLV_OK) {
+ aranges = NULL;
+ }
+
+ if (aranges) {
+ // We have aranges. Get the one where our address is.
+ Dwarf_Arange arange;
+ if (dwarf_get_arange(aranges, arange_count, addr, &arange, &error) ==
+ DW_DLV_OK) {
+
+ // We found our address. Get the compilation-unit DIE offset
+ // represented by the given address range.
+ Dwarf_Off cu_die_offset;
+ if (dwarf_get_cu_die_offset(arange, &cu_die_offset, &error) ==
+ DW_DLV_OK) {
+ // Get the DIE at the offset returned by the aranges search.
+ // We set is_info to 1 to specify that the offset is from
+ // the .debug_info section (and not .debug_types)
+ int dwarf_result =
+ dwarf_offdie_b(dwarf, cu_die_offset, 1, &returnDie, &error);
+
+ found = dwarf_result == DW_DLV_OK;
+ }
+ dwarf_dealloc(dwarf, arange, DW_DLA_ARANGE);
+ }
+ }
+
+ if (found)
+ return returnDie; // The caller is responsible for freeing the die
+
+ // The search for aranges failed. Try to find our address by scanning
+ // all compilation units.
+ Dwarf_Unsigned next_cu_header;
+ Dwarf_Half tag = 0;
+ returnDie = 0;
+
+ while (!found &&
+ dwarf_next_cu_header_d(dwarf, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ &next_cu_header, 0, &error) == DW_DLV_OK) {
+
+ if (returnDie)
+ dwarf_dealloc(dwarf, returnDie, DW_DLA_DIE);
+
+ if (dwarf_siblingof(dwarf, 0, &returnDie, &error) == DW_DLV_OK) {
+ if ((dwarf_tag(returnDie, &tag, &error) == DW_DLV_OK) &&
+ tag == DW_TAG_compile_unit) {
+ if (die_has_pc(fobj, returnDie, addr)) {
+ found = true;
+ }
+ }
+ }
+ }
+
+ if (found) {
+ while (dwarf_next_cu_header_d(dwarf, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ &next_cu_header, 0, &error) == DW_DLV_OK) {
+ // Reset the cu header state. Libdwarf's next_cu_header API
+ // keeps its own iterator per Dwarf_Debug that can't be reset.
+ // We need to keep fetching elements until the end.
+ }
+ }
+
+ if (found)
+ return returnDie;
+
+ // We couldn't find any compilation units with ranges or a high/low pc.
+ // Try again by looking at all DIEs in all compilation units.
+ Dwarf_Die cudie;
+ while (dwarf_next_cu_header_d(dwarf, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ &next_cu_header, 0, &error) == DW_DLV_OK) {
+ if (dwarf_siblingof(dwarf, 0, &cudie, &error) == DW_DLV_OK) {
+ Dwarf_Die die_mem = 0;
+ Dwarf_Die resultDie = find_fundie_by_pc(fobj, cudie, addr, die_mem);
+
+ if (resultDie) {
+ found = true;
+ break;
+ }
+ }
+ }
+
+ if (found) {
+ while (dwarf_next_cu_header_d(dwarf, 1, 0, 0, 0, 0, 0, 0, 0, 0,
+ &next_cu_header, 0, &error) == DW_DLV_OK) {
+ // Reset the cu header state. Libdwarf's next_cu_header API
+ // keeps its own iterator per Dwarf_Debug that can't be reset.
+ // We need to keep fetching elements until the end.
+ }
+ }
+
+ if (found)
+ return cudie;
+
+ // We failed.
+ return NULL;
+ }
+};
+#endif // BACKWARD_HAS_DWARF == 1
+
+template <>
+class TraceResolverImpl<system_tag::linux_tag>
+ : public TraceResolverLinuxImpl<trace_resolver_tag::current> {};
+
+#endif // BACKWARD_SYSTEM_LINUX
+
+#ifdef BACKWARD_SYSTEM_DARWIN
+
+template <typename STACKTRACE_TAG> class TraceResolverDarwinImpl;
+
+template <>
+class TraceResolverDarwinImpl<trace_resolver_tag::backtrace_symbol>
+ : public TraceResolverImplBase {
+public:
+ void load_addresses(void *const*addresses, int address_count) override {
+ if (address_count == 0) {
+ return;
+ }
+ _symbols.reset(backtrace_symbols(addresses, address_count));
+ }
+
+ ResolvedTrace resolve(ResolvedTrace trace) override {
+ // parse:
+ // <n> <file> <addr> <mangled-name> + <offset>
+ char *filename = _symbols[trace.idx];
+
+ // skip "<n> "
+ while (*filename && *filename != ' ')
+ filename++;
+ while (*filename == ' ')
+ filename++;
+
+ // find start of <mangled-name> from end (<file> may contain a space)
+ char *p = filename + strlen(filename) - 1;
+ // skip to start of " + <offset>"
+ while (p > filename && *p != ' ')
+ p--;
+ while (p > filename && *p == ' ')
+ p--;
+ while (p > filename && *p != ' ')
+ p--;
+ while (p > filename && *p == ' ')
+ p--;
+ char *funcname_end = p + 1;
+
+ // skip to start of "<manged-name>"
+ while (p > filename && *p != ' ')
+ p--;
+ char *funcname = p + 1;
+
+ // skip to start of " <addr> "
+ while (p > filename && *p == ' ')
+ p--;
+ while (p > filename && *p != ' ')
+ p--;
+ while (p > filename && *p == ' ')
+ p--;
+
+ // skip "<file>", handling the case where it contains a
+ char *filename_end = p + 1;
+ if (p == filename) {
+ // something went wrong, give up
+ filename_end = filename + strlen(filename);
+ funcname = filename_end;
+ }
+ trace.object_filename.assign(
+ filename, filename_end); // ok even if filename_end is the ending \0
+ // (then we assign entire string)
+
+ if (*funcname) { // if it's not end of string
+ *funcname_end = '\0';
+
+ trace.object_function = this->demangle(funcname);
+ trace.object_function += " ";
+ trace.object_function += (funcname_end + 1);
+ trace.source.function = trace.object_function; // we cannot do better.
+ }
+ return trace;
+ }
+
+private:
+ details::handle<char **> _symbols;
+};
+
+template <>
+class TraceResolverImpl<system_tag::darwin_tag>
+ : public TraceResolverDarwinImpl<trace_resolver_tag::current> {};
+
+#endif // BACKWARD_SYSTEM_DARWIN
+
+#ifdef BACKWARD_SYSTEM_WINDOWS
+
+// Load all symbol info
+// Based on:
+// https://stackoverflow.com/questions/6205981/windows-c-stack-trace-from-a-running-app/28276227#28276227
+
+struct module_data {
+ std::string image_name;
+ std::string module_name;
+ void *base_address;
+ DWORD load_size;
+};
+
+class get_mod_info {
+ HANDLE process;
+ static const int buffer_length = 4096;
+
+public:
+ get_mod_info(HANDLE h) : process(h) {}
+
+ module_data operator()(HMODULE module) {
+ module_data ret;
+ char temp[buffer_length];
+ MODULEINFO mi;
+
+ GetModuleInformation(process, module, &mi, sizeof(mi));
+ ret.base_address = mi.lpBaseOfDll;
+ ret.load_size = mi.SizeOfImage;
+
+ GetModuleFileNameExA(process, module, temp, sizeof(temp));
+ ret.image_name = temp;
+ GetModuleBaseNameA(process, module, temp, sizeof(temp));
+ ret.module_name = temp;
+ std::vector<char> img(ret.image_name.begin(), ret.image_name.end());
+ std::vector<char> mod(ret.module_name.begin(), ret.module_name.end());
+ SymLoadModule64(process, 0, &img[0], &mod[0], (DWORD64)ret.base_address,
+ ret.load_size);
+ return ret;
+ }
+};
+
+template <> class TraceResolverImpl<system_tag::windows_tag>
+ : public TraceResolverImplBase {
+public:
+ TraceResolverImpl() {
+
+ HANDLE process = GetCurrentProcess();
+
+ std::vector<module_data> modules;
+ DWORD cbNeeded;
+ std::vector<HMODULE> module_handles(1);
+ SymInitialize(process, NULL, false);
+ DWORD symOptions = SymGetOptions();
+ symOptions |= SYMOPT_LOAD_LINES | SYMOPT_UNDNAME;
+ SymSetOptions(symOptions);
+ EnumProcessModules(process, &module_handles[0],
+ module_handles.size() * sizeof(HMODULE), &cbNeeded);
+ module_handles.resize(cbNeeded / sizeof(HMODULE));
+ EnumProcessModules(process, &module_handles[0],
+ module_handles.size() * sizeof(HMODULE), &cbNeeded);
+ std::transform(module_handles.begin(), module_handles.end(),
+ std::back_inserter(modules), get_mod_info(process));
+ void *base = modules[0].base_address;
+ IMAGE_NT_HEADERS *h = ImageNtHeader(base);
+ image_type = h->FileHeader.Machine;
+ }
+
+ static const int max_sym_len = 255;
+ struct symbol_t {
+ SYMBOL_INFO sym;
+ char buffer[max_sym_len];
+ } sym;
+
+ DWORD64 displacement;
+
+ ResolvedTrace resolve(ResolvedTrace t) override {
+ HANDLE process = GetCurrentProcess();
+
+ char name[256];
+
+ memset(&sym, 0, sizeof(sym));
+ sym.sym.SizeOfStruct = sizeof(SYMBOL_INFO);
+ sym.sym.MaxNameLen = max_sym_len;
+
+ if (!SymFromAddr(process, (ULONG64)t.addr, &displacement, &sym.sym)) {
+ // TODO: error handling everywhere
+ char* lpMsgBuf;
+ DWORD dw = GetLastError();
+
+ FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER |
+ FORMAT_MESSAGE_FROM_SYSTEM |
+ FORMAT_MESSAGE_IGNORE_INSERTS,
+ NULL, dw, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
+ (char*)&lpMsgBuf, 0, NULL);
+
+ printf(lpMsgBuf);
+
+ // abort();
+ }
+ UnDecorateSymbolName(sym.sym.Name, (PSTR)name, 256, UNDNAME_COMPLETE);
+
+ DWORD offset = 0;
+ IMAGEHLP_LINE line;
+ if (SymGetLineFromAddr(process, (ULONG64)t.addr, &offset, &line)) {
+ t.object_filename = line.FileName;
+ t.source.filename = line.FileName;
+ t.source.line = line.LineNumber;
+ t.source.col = offset;
+ }
+
+ t.source.function = name;
+ t.object_filename = "";
+ t.object_function = name;
+
+ return t;
+ }
+
+ DWORD machine_type() const { return image_type; }
+
+private:
+ DWORD image_type;
+};
+
+#endif
+
+class TraceResolver : public TraceResolverImpl<system_tag::current_tag> {};
+
+/*************** CODE SNIPPET ***************/
+
+class SourceFile {
+public:
+ typedef std::vector<std::pair<unsigned, std::string>> lines_t;
+
+ SourceFile() {}
+ SourceFile(const std::string &path) {
+ // 1. If BACKWARD_CXX_SOURCE_PREFIXES is set then assume it contains
+ // a colon-separated list of path prefixes. Try prepending each
+ // to the given path until a valid file is found.
+ const std::vector<std::string> &prefixes = get_paths_from_env_variable();
+ for (size_t i = 0; i < prefixes.size(); ++i) {
+ // Double slashes (//) should not be a problem.
+ std::string new_path = prefixes[i] + '/' + path;
+ _file.reset(new std::ifstream(new_path.c_str()));
+ if (is_open())
+ break;
+ }
+ // 2. If no valid file found then fallback to opening the path as-is.
+ if (!_file || !is_open()) {
+ _file.reset(new std::ifstream(path.c_str()));
+ }
+ }
+ bool is_open() const { return _file->is_open(); }
+
+ lines_t &get_lines(unsigned line_start, unsigned line_count, lines_t &lines) {
+ using namespace std;
+ // This function make uses of the dumbest algo ever:
+ // 1) seek(0)
+ // 2) read lines one by one and discard until line_start
+ // 3) read line one by one until line_start + line_count
+ //
+ // If you are getting snippets many time from the same file, it is
+ // somewhat a waste of CPU, feel free to benchmark and propose a
+ // better solution ;)
+
+ _file->clear();
+ _file->seekg(0);
+ string line;
+ unsigned line_idx;
+
+ for (line_idx = 1; line_idx < line_start; ++line_idx) {
+ std::getline(*_file, line);
+ if (!*_file) {
+ return lines;
+ }
+ }
+
+ // think of it like a lambda in C++98 ;)
+ // but look, I will reuse it two times!
+ // What a good boy am I.
+ struct isspace {
+ bool operator()(char c) { return std::isspace(c); }
+ };
+
+ bool started = false;
+ for (; line_idx < line_start + line_count; ++line_idx) {
+ getline(*_file, line);
+ if (!*_file) {
+ return lines;
+ }
+ if (!started) {
+ if (std::find_if(line.begin(), line.end(), not_isspace()) == line.end())
+ continue;
+ started = true;
+ }
+ lines.push_back(make_pair(line_idx, line));
+ }
+
+ lines.erase(
+ std::find_if(lines.rbegin(), lines.rend(), not_isempty()).base(),
+ lines.end());
+ return lines;
+ }
+
+ lines_t get_lines(unsigned line_start, unsigned line_count) {
+ lines_t lines;
+ return get_lines(line_start, line_count, lines);
+ }
+
+ // there is no find_if_not in C++98, lets do something crappy to
+ // workaround.
+ struct not_isspace {
+ bool operator()(char c) { return !std::isspace(c); }
+ };
+ // and define this one here because C++98 is not happy with local defined
+ // struct passed to template functions, fuuuu.
+ struct not_isempty {
+ bool operator()(const lines_t::value_type &p) {
+ return !(std::find_if(p.second.begin(), p.second.end(), not_isspace()) ==
+ p.second.end());
+ }
+ };
+
+ void swap(SourceFile &b) { _file.swap(b._file); }
+
+#ifdef BACKWARD_ATLEAST_CXX11
+ SourceFile(SourceFile &&from) : _file(nullptr) { swap(from); }
+ SourceFile &operator=(SourceFile &&from) {
+ swap(from);
+ return *this;
+ }
+#else
+ explicit SourceFile(const SourceFile &from) {
+ // some sort of poor man's move semantic.
+ swap(const_cast<SourceFile &>(from));
+ }
+ SourceFile &operator=(const SourceFile &from) {
+ // some sort of poor man's move semantic.
+ swap(const_cast<SourceFile &>(from));
+ return *this;
+ }
+#endif
+
+private:
+ details::handle<std::ifstream *, details::default_delete<std::ifstream *>>
+ _file;
+
+ std::vector<std::string> get_paths_from_env_variable_impl() {
+ std::vector<std::string> paths;
+ const char *prefixes_str = std::getenv("BACKWARD_CXX_SOURCE_PREFIXES");
+ if (prefixes_str && prefixes_str[0]) {
+ paths = details::split_source_prefixes(prefixes_str);
+ }
+ return paths;
+ }
+
+ const std::vector<std::string> &get_paths_from_env_variable() {
+ static std::vector<std::string> paths = get_paths_from_env_variable_impl();
+ return paths;
+ }
+
+#ifdef BACKWARD_ATLEAST_CXX11
+ SourceFile(const SourceFile &) = delete;
+ SourceFile &operator=(const SourceFile &) = delete;
+#endif
+};
+
+class SnippetFactory {
+public:
+ typedef SourceFile::lines_t lines_t;
+
+ lines_t get_snippet(const std::string &filename, unsigned line_start,
+ unsigned context_size) {
+
+ SourceFile &src_file = get_src_file(filename);
+ unsigned start = line_start - context_size / 2;
+ return src_file.get_lines(start, context_size);
+ }
+
+ lines_t get_combined_snippet(const std::string &filename_a, unsigned line_a,
+ const std::string &filename_b, unsigned line_b,
+ unsigned context_size) {
+ SourceFile &src_file_a = get_src_file(filename_a);
+ SourceFile &src_file_b = get_src_file(filename_b);
+
+ lines_t lines =
+ src_file_a.get_lines(line_a - context_size / 4, context_size / 2);
+ src_file_b.get_lines(line_b - context_size / 4, context_size / 2, lines);
+ return lines;
+ }
+
+ lines_t get_coalesced_snippet(const std::string &filename, unsigned line_a,
+ unsigned line_b, unsigned context_size) {
+ SourceFile &src_file = get_src_file(filename);
+
+ using std::max;
+ using std::min;
+ unsigned a = min(line_a, line_b);
+ unsigned b = max(line_a, line_b);
+
+ if ((b - a) < (context_size / 3)) {
+ return src_file.get_lines((a + b - context_size + 1) / 2, context_size);
+ }
+
+ lines_t lines = src_file.get_lines(a - context_size / 4, context_size / 2);
+ src_file.get_lines(b - context_size / 4, context_size / 2, lines);
+ return lines;
+ }
+
+private:
+ typedef details::hashtable<std::string, SourceFile>::type src_files_t;
+ src_files_t _src_files;
+
+ SourceFile &get_src_file(const std::string &filename) {
+ src_files_t::iterator it = _src_files.find(filename);
+ if (it != _src_files.end()) {
+ return it->second;
+ }
+ SourceFile &new_src_file = _src_files[filename];
+ new_src_file = SourceFile(filename);
+ return new_src_file;
+ }
+};
+
+/*************** PRINTER ***************/
+
+namespace ColorMode {
+enum type { automatic, never, always };
+}
+
+class cfile_streambuf : public std::streambuf {
+public:
+ cfile_streambuf(FILE *_sink) : sink(_sink) {}
+ int_type underflow() override { return traits_type::eof(); }
+ int_type overflow(int_type ch) override {
+ if (traits_type::not_eof(ch) && fputc(ch, sink) != EOF) {
+ return ch;
+ }
+ return traits_type::eof();
+ }
+
+ std::streamsize xsputn(const char_type *s, std::streamsize count) override {
+ return static_cast<std::streamsize>(
+ fwrite(s, sizeof *s, static_cast<size_t>(count), sink));
+ }
+
+#ifdef BACKWARD_ATLEAST_CXX11
+public:
+ cfile_streambuf(const cfile_streambuf &) = delete;
+ cfile_streambuf &operator=(const cfile_streambuf &) = delete;
+#else
+private:
+ cfile_streambuf(const cfile_streambuf &);
+ cfile_streambuf &operator=(const cfile_streambuf &);
+#endif
+
+private:
+ FILE *sink;
+ std::vector<char> buffer;
+};
+
+#ifdef BACKWARD_SYSTEM_LINUX
+
+namespace Color {
+enum type { yellow = 33, purple = 35, reset = 39 };
+} // namespace Color
+
+class Colorize {
+public:
+ Colorize(std::ostream &os) : _os(os), _reset(false), _enabled(false) {}
+
+ void activate(ColorMode::type mode) { _enabled = mode == ColorMode::always; }
+
+ void activate(ColorMode::type mode, FILE *fp) { activate(mode, fileno(fp)); }
+
+ void set_color(Color::type ccode) {
+ if (!_enabled)
+ return;
+
+ // I assume that the terminal can handle basic colors. Seriously I
+ // don't want to deal with all the termcap shit.
+ _os << "\033[" << static_cast<int>(ccode) << "m";
+ _reset = (ccode != Color::reset);
+ }
+
+ ~Colorize() {
+ if (_reset) {
+ set_color(Color::reset);
+ }
+ }
+
+private:
+ void activate(ColorMode::type mode, int fd) {
+ activate(mode == ColorMode::automatic && isatty(fd) ? ColorMode::always
+ : mode);
+ }
+
+ std::ostream &_os;
+ bool _reset;
+ bool _enabled;
+};
+
+#else // ndef BACKWARD_SYSTEM_LINUX
+
+namespace Color {
+enum type { yellow = 0, purple = 0, reset = 0 };
+} // namespace Color
+
+class Colorize {
+public:
+ Colorize(std::ostream &) {}
+ void activate(ColorMode::type) {}
+ void activate(ColorMode::type, FILE *) {}
+ void set_color(Color::type) {}
+};
+
+#endif // BACKWARD_SYSTEM_LINUX
+
+class Printer {
+public:
+ bool snippet;
+ ColorMode::type color_mode;
+ bool address;
+ bool object;
+ int inliner_context_size;
+ int trace_context_size;
+
+ Printer()
+ : snippet(true), color_mode(ColorMode::automatic), address(false),
+ object(false), inliner_context_size(5), trace_context_size(7) {}
+
+ template <typename ST> FILE *print(ST &st, FILE *fp = stderr) {
+ cfile_streambuf obuf(fp);
+ std::ostream os(&obuf);
+ Colorize colorize(os);
+ colorize.activate(color_mode, fp);
+ print_stacktrace(st, os, colorize);
+ return fp;
+ }
+
+ template <typename ST> std::ostream &print(ST &st, std::ostream &os) {
+ Colorize colorize(os);
+ colorize.activate(color_mode);
+ print_stacktrace(st, os, colorize);
+ return os;
+ }
+
+ template <typename IT>
+ FILE *print(IT begin, IT end, FILE *fp = stderr, size_t thread_id = 0) {
+ cfile_streambuf obuf(fp);
+ std::ostream os(&obuf);
+ Colorize colorize(os);
+ colorize.activate(color_mode, fp);
+ print_stacktrace(begin, end, os, thread_id, colorize);
+ return fp;
+ }
+
+ template <typename IT>
+ std::ostream &print(IT begin, IT end, std::ostream &os,
+ size_t thread_id = 0) {
+ Colorize colorize(os);
+ colorize.activate(color_mode);
+ print_stacktrace(begin, end, os, thread_id, colorize);
+ return os;
+ }
+
+ TraceResolver const &resolver() const { return _resolver; }
+
+private:
+ TraceResolver _resolver;
+ SnippetFactory _snippets;
+
+ template <typename ST>
+ void print_stacktrace(ST &st, std::ostream &os, Colorize &colorize) {
+ print_header(os, st.thread_id());
+ _resolver.load_stacktrace(st);
+ for (size_t trace_idx = st.size(); trace_idx > 0; --trace_idx) {
+ print_trace(os, _resolver.resolve(st[trace_idx - 1]), colorize);
+ }
+ }
+
+ template <typename IT>
+ void print_stacktrace(IT begin, IT end, std::ostream &os, size_t thread_id,
+ Colorize &colorize) {
+ print_header(os, thread_id);
+ for (; begin != end; ++begin) {
+ print_trace(os, *begin, colorize);
+ }
+ }
+
+ void print_header(std::ostream &os, size_t thread_id) {
+ os << "Stack trace (most recent call last)";
+ if (thread_id) {
+ os << " in thread " << thread_id;
+ }
+ os << ":\n";
+ }
+
+ void print_trace(std::ostream &os, const ResolvedTrace &trace,
+ Colorize &colorize) {
+ os << "#" << std::left << std::setw(2) << trace.idx << std::right;
+ bool already_indented = true;
+
+ if (!trace.source.filename.size() || object) {
+ os << " Object \"" << trace.object_filename << "\", at " << trace.addr
+ << ", in " << trace.object_function << "\n";
+ already_indented = false;
+ }
+
+ for (size_t inliner_idx = trace.inliners.size(); inliner_idx > 0;
+ --inliner_idx) {
+ if (!already_indented) {
+ os << " ";
+ }
+ const ResolvedTrace::SourceLoc &inliner_loc =
+ trace.inliners[inliner_idx - 1];
+ print_source_loc(os, " | ", inliner_loc);
+ if (snippet) {
+ print_snippet(os, " | ", inliner_loc, colorize, Color::purple,
+ inliner_context_size);
+ }
+ already_indented = false;
+ }
+
+ if (trace.source.filename.size()) {
+ if (!already_indented) {
+ os << " ";
+ }
+ print_source_loc(os, " ", trace.source, trace.addr);
+ if (snippet) {
+ print_snippet(os, " ", trace.source, colorize, Color::yellow,
+ trace_context_size);
+ }
+ }
+ }
+
+ void print_snippet(std::ostream &os, const char *indent,
+ const ResolvedTrace::SourceLoc &source_loc,
+ Colorize &colorize, Color::type color_code,
+ int context_size) {
+ using namespace std;
+ typedef SnippetFactory::lines_t lines_t;
+
+ lines_t lines = _snippets.get_snippet(source_loc.filename, source_loc.line,
+ static_cast<unsigned>(context_size));
+
+ for (lines_t::const_iterator it = lines.begin(); it != lines.end(); ++it) {
+ if (it->first == source_loc.line) {
+ colorize.set_color(color_code);
+ os << indent << ">";
+ } else {
+ os << indent << " ";
+ }
+ os << std::setw(4) << it->first << ": " << it->second << "\n";
+ if (it->first == source_loc.line) {
+ colorize.set_color(Color::reset);
+ }
+ }
+ }
+
+ void print_source_loc(std::ostream &os, const char *indent,
+ const ResolvedTrace::SourceLoc &source_loc,
+ void *addr = nullptr) {
+ os << indent << "Source \"" << source_loc.filename << "\", line "
+ << source_loc.line << ", in " << source_loc.function;
+
+ if (address && addr != nullptr) {
+ os << " [" << addr << "]";
+ }
+ os << "\n";
+ }
+};
+
+/*************** SIGNALS HANDLING ***************/
+
+#if defined(BACKWARD_SYSTEM_LINUX) || defined(BACKWARD_SYSTEM_DARWIN)
+
+class SignalHandling {
+public:
+ static std::vector<int> make_default_signals() {
+ const int posix_signals[] = {
+ // Signals for which the default action is "Core".
+ SIGABRT, // Abort signal from abort(3)
+ SIGBUS, // Bus error (bad memory access)
+ SIGFPE, // Floating point exception
+ SIGILL, // Illegal Instruction
+ SIGIOT, // IOT trap. A synonym for SIGABRT
+ SIGQUIT, // Quit from keyboard
+ SIGSEGV, // Invalid memory reference
+ SIGSYS, // Bad argument to routine (SVr4)
+ SIGTRAP, // Trace/breakpoint trap
+ SIGXCPU, // CPU time limit exceeded (4.2BSD)
+ SIGXFSZ, // File size limit exceeded (4.2BSD)
+#if defined(BACKWARD_SYSTEM_DARWIN)
+ SIGEMT, // emulation instruction executed
+#endif
+ };
+ return std::vector<int>(posix_signals,
+ posix_signals +
+ sizeof posix_signals / sizeof posix_signals[0]);
+ }
+
+ SignalHandling(const std::vector<int> &posix_signals = make_default_signals())
+ : _loaded(false) {
+ bool success = true;
+
+ const size_t stack_size = 1024 * 1024 * 8;
+ _stack_content.reset(static_cast<char *>(malloc(stack_size)));
+ if (_stack_content) {
+ stack_t ss;
+ ss.ss_sp = _stack_content.get();
+ ss.ss_size = stack_size;
+ ss.ss_flags = 0;
+ if (sigaltstack(&ss, nullptr) < 0) {
+ success = false;
+ }
+ } else {
+ success = false;
+ }
+
+ for (size_t i = 0; i < posix_signals.size(); ++i) {
+ struct sigaction action;
+ memset(&action, 0, sizeof action);
+ action.sa_flags =
+ static_cast<int>(SA_SIGINFO | SA_ONSTACK | SA_NODEFER | SA_RESETHAND);
+ sigfillset(&action.sa_mask);
+ sigdelset(&action.sa_mask, posix_signals[i]);
+#if defined(__clang__)
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdisabled-macro-expansion"
+#endif
+ action.sa_sigaction = &sig_handler;
+#if defined(__clang__)
+#pragma clang diagnostic pop
+#endif
+
+ int r = sigaction(posix_signals[i], &action, nullptr);
+ if (r < 0)
+ success = false;
+ }
+
+ _loaded = success;
+ }
+
+ bool loaded() const { return _loaded; }
+
+ static void handleSignal(int, siginfo_t *info, void *_ctx) {
+ ucontext_t *uctx = static_cast<ucontext_t *>(_ctx);
+
+ StackTrace st;
+ void *error_addr = nullptr;
+#ifdef REG_RIP // x86_64
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext.gregs[REG_RIP]);
+#elif defined(REG_EIP) // x86_32
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext.gregs[REG_EIP]);
+#elif defined(__arm__)
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext.arm_pc);
+#elif defined(__aarch64__)
+ #if defined(__APPLE__)
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext->__ss.__pc);
+ #else
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext.pc);
+ #endif
+#elif defined(__mips__)
+ error_addr = reinterpret_cast<void *>(
+ reinterpret_cast<struct sigcontext *>(&uctx->uc_mcontext)->sc_pc);
+#elif defined(__ppc__) || defined(__powerpc) || defined(__powerpc__) || \
+ defined(__POWERPC__)
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext.regs->nip);
+#elif defined(__riscv)
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext.__gregs[REG_PC]);
+#elif defined(__s390x__)
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext.psw.addr);
+#elif defined(__APPLE__) && defined(__x86_64__)
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext->__ss.__rip);
+#elif defined(__APPLE__)
+ error_addr = reinterpret_cast<void *>(uctx->uc_mcontext->__ss.__eip);
+#else
+#warning ":/ sorry, ain't know no nothing none not of your architecture!"
+#endif
+ if (error_addr) {
+ st.load_from(error_addr, 32, reinterpret_cast<void *>(uctx),
+ info->si_addr);
+ } else {
+ st.load_here(32, reinterpret_cast<void *>(uctx), info->si_addr);
+ }
+
+ Printer printer;
+ printer.address = true;
+ printer.print(st, stderr);
+
+#if _XOPEN_SOURCE >= 700 || _POSIX_C_SOURCE >= 200809L
+ psiginfo(info, nullptr);
+#else
+ (void)info;
+#endif
+ }
+
+private:
+ details::handle<char *> _stack_content;
+ bool _loaded;
+
+#ifdef __GNUC__
+ __attribute__((noreturn))
+#endif
+ static void
+ sig_handler(int signo, siginfo_t *info, void *_ctx) {
+ handleSignal(signo, info, _ctx);
+
+ // try to forward the signal.
+ raise(info->si_signo);
+
+ // terminate the process immediately.
+ puts("watf? exit");
+ _exit(EXIT_FAILURE);
+ }
+};
+
+#endif // BACKWARD_SYSTEM_LINUX || BACKWARD_SYSTEM_DARWIN
+
+#ifdef BACKWARD_SYSTEM_WINDOWS
+
+class SignalHandling {
+public:
+ SignalHandling(const std::vector<int> & = std::vector<int>())
+ : reporter_thread_([]() {
+ /* We handle crashes in a utility thread:
+ backward structures and some Windows functions called here
+ need stack space, which we do not have when we encounter a
+ stack overflow.
+ To support reporting stack traces during a stack overflow,
+ we create a utility thread at startup, which waits until a
+ crash happens or the program exits normally. */
+
+ {
+ std::unique_lock<std::mutex> lk(mtx());
+ cv().wait(lk, [] { return crashed() != crash_status::running; });
+ }
+ if (crashed() == crash_status::crashed) {
+ handle_stacktrace(skip_recs());
+ }
+ {
+ std::unique_lock<std::mutex> lk(mtx());
+ crashed() = crash_status::ending;
+ }
+ cv().notify_one();
+ }) {
+ SetUnhandledExceptionFilter(crash_handler);
+
+ signal(SIGABRT, signal_handler);
+ _set_abort_behavior(0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
+
+ std::set_terminate(&terminator);
+#ifndef BACKWARD_ATLEAST_CXX17
+ std::set_unexpected(&terminator);
+#endif
+ _set_purecall_handler(&terminator);
+ _set_invalid_parameter_handler(&invalid_parameter_handler);
+ }
+ bool loaded() const { return true; }
+
+ ~SignalHandling() {
+ {
+ std::unique_lock<std::mutex> lk(mtx());
+ crashed() = crash_status::normal_exit;
+ }
+
+ cv().notify_one();
+
+ reporter_thread_.join();
+ }
+
+private:
+ static CONTEXT *ctx() {
+ static CONTEXT data;
+ return &data;
+ }
+
+ enum class crash_status { running, crashed, normal_exit, ending };
+
+ static crash_status &crashed() {
+ static crash_status data;
+ return data;
+ }
+
+ static std::mutex &mtx() {
+ static std::mutex data;
+ return data;
+ }
+
+ static std::condition_variable &cv() {
+ static std::condition_variable data;
+ return data;
+ }
+
+ static HANDLE &thread_handle() {
+ static HANDLE handle;
+ return handle;
+ }
+
+ std::thread reporter_thread_;
+
+ // TODO: how not to hardcode these?
+ static const constexpr int signal_skip_recs =
+#ifdef __clang__
+ // With clang, RtlCaptureContext also captures the stack frame of the
+ // current function Below that, there ar 3 internal Windows functions
+ 4
+#else
+ // With MSVC cl, RtlCaptureContext misses the stack frame of the current
+ // function The first entries during StackWalk are the 3 internal Windows
+ // functions
+ 3
+#endif
+ ;
+
+ static int &skip_recs() {
+ static int data;
+ return data;
+ }
+
+ static inline void terminator() {
+ crash_handler(signal_skip_recs);
+ abort();
+ }
+
+ static inline void signal_handler(int) {
+ crash_handler(signal_skip_recs);
+ abort();
+ }
+
+ static inline void __cdecl invalid_parameter_handler(const wchar_t *,
+ const wchar_t *,
+ const wchar_t *,
+ unsigned int,
+ uintptr_t) {
+ crash_handler(signal_skip_recs);
+ abort();
+ }
+
+ NOINLINE static LONG WINAPI crash_handler(EXCEPTION_POINTERS *info) {
+ // The exception info supplies a trace from exactly where the issue was,
+ // no need to skip records
+ crash_handler(0, info->ContextRecord);
+ return EXCEPTION_CONTINUE_SEARCH;
+ }
+
+ NOINLINE static void crash_handler(int skip, CONTEXT *ct = nullptr) {
+
+ if (ct == nullptr) {
+ RtlCaptureContext(ctx());
+ } else {
+ memcpy(ctx(), ct, sizeof(CONTEXT));
+ }
+ DuplicateHandle(GetCurrentProcess(), GetCurrentThread(),
+ GetCurrentProcess(), &thread_handle(), 0, FALSE,
+ DUPLICATE_SAME_ACCESS);
+
+ skip_recs() = skip;
+
+ {
+ std::unique_lock<std::mutex> lk(mtx());
+ crashed() = crash_status::crashed;
+ }
+
+ cv().notify_one();
+
+ {
+ std::unique_lock<std::mutex> lk(mtx());
+ cv().wait(lk, [] { return crashed() != crash_status::crashed; });
+ }
+ }
+
+ static void handle_stacktrace(int skip_frames = 0) {
+ // printer creates the TraceResolver, which can supply us a machine type
+ // for stack walking. Without this, StackTrace can only guess using some
+ // macros.
+ // StackTrace also requires that the PDBs are already loaded, which is done
+ // in the constructor of TraceResolver
+ Printer printer;
+
+ StackTrace st;
+ st.set_machine_type(printer.resolver().machine_type());
+ st.set_thread_handle(thread_handle());
+ st.load_here(32 + skip_frames, ctx());
+ st.skip_n_firsts(skip_frames);
+
+ printer.address = true;
+ printer.print(st, std::cerr);
+ }
+};
+
+#endif // BACKWARD_SYSTEM_WINDOWS
+
+#ifdef BACKWARD_SYSTEM_UNKNOWN
+
+class SignalHandling {
+public:
+ SignalHandling(const std::vector<int> & = std::vector<int>()) {}
+ bool init() { return false; }
+ bool loaded() { return false; }
+};
+
+#endif // BACKWARD_SYSTEM_UNKNOWN
+
+} // namespace backward
+
+#endif /* H_GUARD */
diff --git a/src/third-party/base64/LICENSE b/src/third-party/base64/LICENSE
new file mode 100644
index 0000000..9446393
--- /dev/null
+++ b/src/third-party/base64/LICENSE
@@ -0,0 +1,28 @@
+Copyright (c) 2005-2007, Nick Galbreath
+Copyright (c) 2013-2019, Alfred Klomp
+Copyright (c) 2015-2017, Wojciech Mula
+Copyright (c) 2016-2017, Matthieu Darbois
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+- Redistributions of source code must retain the above copyright notice,
+ this list of conditions and the following disclaimer.
+
+- Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
+IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/src/third-party/base64/include/libbase64.h b/src/third-party/base64/include/libbase64.h
new file mode 100644
index 0000000..f3e5abb
--- /dev/null
+++ b/src/third-party/base64/include/libbase64.h
@@ -0,0 +1,133 @@
+#ifndef LIBBASE64_H
+#define LIBBASE64_H
+
+#include <stddef.h> /* size_t */
+
+
+#define BASE64_SYMBOL_IMPORT
+#define BASE64_SYMBOL_EXPORT
+#define BASE64_SYMBOL_PRIVATE
+
+#if defined(BASE64_STATIC_DEFINE)
+#define BASE64_EXPORT
+#define BASE64_NO_EXPORT
+
+#else
+#if defined(BASE64_EXPORTS) // defined if we are building the shared library
+#define BASE64_EXPORT BASE64_SYMBOL_EXPORT
+
+#else
+#define BASE64_EXPORT BASE64_SYMBOL_IMPORT
+#endif
+
+#define BASE64_NO_EXPORT BASE64_SYMBOL_PRIVATE
+#endif
+
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* These are the flags that can be passed in the `flags` argument. The values
+ * below force the use of a given codec, even if that codec is a no-op in the
+ * current build. Used in testing. Set to 0 for the default behavior, which is
+ * runtime feature detection on x86, a compile-time fixed codec on ARM, and
+ * the plain codec on other platforms: */
+#define BASE64_FORCE_AVX2 (1 << 0)
+#define BASE64_FORCE_NEON32 (1 << 1)
+#define BASE64_FORCE_NEON64 (1 << 2)
+#define BASE64_FORCE_PLAIN (1 << 3)
+#define BASE64_FORCE_SSSE3 (1 << 4)
+#define BASE64_FORCE_SSE41 (1 << 5)
+#define BASE64_FORCE_SSE42 (1 << 6)
+#define BASE64_FORCE_AVX (1 << 7)
+
+struct base64_state {
+ int eof;
+ int bytes;
+ int flags;
+ unsigned char carry;
+};
+
+/* Wrapper function to encode a plain string of given length. Output is written
+ * to *out without trailing zero. Output length in bytes is written to *outlen.
+ * The buffer in `out` has been allocated by the caller and is at least 4/3 the
+ * size of the input. See above for `flags`; set to 0 for default operation: */
+void BASE64_EXPORT base64_encode
+ ( const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ , int flags
+ ) ;
+
+/* Call this before calling base64_stream_encode() to init the state. See above
+ * for `flags`; set to 0 for default operation: */
+void BASE64_EXPORT base64_stream_encode_init
+ ( struct base64_state *state
+ , int flags
+ ) ;
+
+/* Encodes the block of data of given length at `src`, into the buffer at
+ * `out`. Caller is responsible for allocating a large enough out-buffer; it
+ * must be at least 4/3 the size of the in-buffer, but take some margin. Places
+ * the number of new bytes written into `outlen` (which is set to zero when the
+ * function starts). Does not zero-terminate or finalize the output. */
+void BASE64_EXPORT base64_stream_encode
+ ( struct base64_state *state
+ , const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ ) ;
+
+/* Finalizes the output begun by previous calls to `base64_stream_encode()`.
+ * Adds the required end-of-stream markers if appropriate. `outlen` is modified
+ * and will contain the number of new bytes written at `out` (which will quite
+ * often be zero). */
+void BASE64_EXPORT base64_stream_encode_final
+ ( struct base64_state *state
+ , char *out
+ , size_t *outlen
+ ) ;
+
+/* Wrapper function to decode a plain string of given length. Output is written
+ * to *out without trailing zero. Output length in bytes is written to *outlen.
+ * The buffer in `out` has been allocated by the caller and is at least 3/4 the
+ * size of the input. See above for `flags`, set to 0 for default operation: */
+int BASE64_EXPORT base64_decode
+ ( const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ , int flags
+ ) ;
+
+/* Call this before calling base64_stream_decode() to init the state. See above
+ * for `flags`; set to 0 for default operation: */
+void BASE64_EXPORT base64_stream_decode_init
+ ( struct base64_state *state
+ , int flags
+ ) ;
+
+/* Decodes the block of data of given length at `src`, into the buffer at
+ * `out`. Caller is responsible for allocating a large enough out-buffer; it
+ * must be at least 3/4 the size of the in-buffer, but take some margin. Places
+ * the number of new bytes written into `outlen` (which is set to zero when the
+ * function starts). Does not zero-terminate the output. Returns 1 if all is
+ * well, and 0 if a decoding error was found, such as an invalid character.
+ * Returns -1 if the chosen codec is not included in the current build. Used by
+ * the test harness to check whether a codec is available for testing. */
+int BASE64_EXPORT base64_stream_decode
+ ( struct base64_state *state
+ , const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ ) ;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* LIBBASE64_H */
diff --git a/src/third-party/base64/lib/Makefile.am b/src/third-party/base64/lib/Makefile.am
new file mode 100644
index 0000000..7058b4b
--- /dev/null
+++ b/src/third-party/base64/lib/Makefile.am
@@ -0,0 +1,23 @@
+
+noinst_HEADERS = \
+ ../include/libbase64.h \
+ arch/generic/32/enc_loop.c \
+ arch/generic/32/dec_loop.c \
+ arch/generic/enc_tail.c \
+ arch/generic/dec_tail.c \
+ arch/generic/64/enc_loop.c \
+ arch/generic/enc_head.c \
+ arch/generic/dec_head.c \
+ tables/tables.h \
+ tables/table_dec_32bit.h \
+ tables/table_enc_12bit.h \
+ codecs.h \
+ config.h \
+ env.h
+
+noinst_LIBRARIES = libbase64.a
+
+libbase64_a_SOURCES = \
+ lib.c \
+ arch/generic/codec.c \
+ tables/tables.c
diff --git a/src/third-party/base64/lib/arch/avx/codec.c b/src/third-party/base64/lib/arch/avx/codec.c
new file mode 100644
index 0000000..a7a963d
--- /dev/null
+++ b/src/third-party/base64/lib/arch/avx/codec.c
@@ -0,0 +1,42 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#include "../../../include/libbase64.h"
+#include "../../tables/tables.h"
+#include "../../codecs.h"
+#include "config.h"
+#include "../../env.h"
+
+#if HAVE_AVX
+#include <immintrin.h>
+
+#include "../ssse3/dec_reshuffle.c"
+#include "../ssse3/dec_loop.c"
+#include "../ssse3/enc_translate.c"
+#include "../ssse3/enc_reshuffle.c"
+#include "../ssse3/enc_loop.c"
+
+#endif // HAVE_AVX
+
+BASE64_ENC_FUNCTION(avx)
+{
+#if HAVE_AVX
+ #include "../generic/enc_head.c"
+ enc_loop_ssse3(&s, &slen, &o, &olen);
+ #include "../generic/enc_tail.c"
+#else
+ BASE64_ENC_STUB
+#endif
+}
+
+BASE64_DEC_FUNCTION(avx)
+{
+#if HAVE_AVX
+ #include "../generic/dec_head.c"
+ dec_loop_ssse3(&s, &slen, &o, &olen);
+ #include "../generic/dec_tail.c"
+#else
+ BASE64_DEC_STUB
+#endif
+}
diff --git a/src/third-party/base64/lib/arch/avx2/codec.c b/src/third-party/base64/lib/arch/avx2/codec.c
new file mode 100644
index 0000000..0498548
--- /dev/null
+++ b/src/third-party/base64/lib/arch/avx2/codec.c
@@ -0,0 +1,42 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#include "../../../include/libbase64.h"
+#include "../../tables/tables.h"
+#include "../../codecs.h"
+#include "config.h"
+#include "../../env.h"
+
+#if HAVE_AVX2
+#include <immintrin.h>
+
+#include "dec_reshuffle.c"
+#include "dec_loop.c"
+#include "enc_translate.c"
+#include "enc_reshuffle.c"
+#include "enc_loop.c"
+
+#endif // HAVE_AVX2
+
+BASE64_ENC_FUNCTION(avx2)
+{
+#if HAVE_AVX2
+ #include "../generic/enc_head.c"
+ enc_loop_avx2(&s, &slen, &o, &olen);
+ #include "../generic/enc_tail.c"
+#else
+ BASE64_ENC_STUB
+#endif
+}
+
+BASE64_DEC_FUNCTION(avx2)
+{
+#if HAVE_AVX2
+ #include "../generic/dec_head.c"
+ dec_loop_avx2(&s, &slen, &o, &olen);
+ #include "../generic/dec_tail.c"
+#else
+ BASE64_DEC_STUB
+#endif
+}
diff --git a/src/third-party/base64/lib/arch/avx2/dec_loop.c b/src/third-party/base64/lib/arch/avx2/dec_loop.c
new file mode 100644
index 0000000..f959fc4
--- /dev/null
+++ b/src/third-party/base64/lib/arch/avx2/dec_loop.c
@@ -0,0 +1,110 @@
+static inline int
+dec_loop_avx2_inner (const uint8_t **s, uint8_t **o, size_t *rounds)
+{
+ const __m256i lut_lo = _mm256_setr_epi8(
+ 0x15, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x13, 0x1A, 0x1B, 0x1B, 0x1B, 0x1A,
+ 0x15, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x13, 0x1A, 0x1B, 0x1B, 0x1B, 0x1A);
+
+ const __m256i lut_hi = _mm256_setr_epi8(
+ 0x10, 0x10, 0x01, 0x02, 0x04, 0x08, 0x04, 0x08,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10,
+ 0x10, 0x10, 0x01, 0x02, 0x04, 0x08, 0x04, 0x08,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10);
+
+ const __m256i lut_roll = _mm256_setr_epi8(
+ 0, 16, 19, 4, -65, -65, -71, -71,
+ 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 16, 19, 4, -65, -65, -71, -71,
+ 0, 0, 0, 0, 0, 0, 0, 0);
+
+ const __m256i mask_2F = _mm256_set1_epi8(0x2F);
+
+ // Load input:
+ __m256i str = _mm256_loadu_si256((__m256i *) *s);
+
+ // See the SSSE3 decoder for an explanation of the algorithm.
+ const __m256i hi_nibbles = _mm256_and_si256(_mm256_srli_epi32(str, 4), mask_2F);
+ const __m256i lo_nibbles = _mm256_and_si256(str, mask_2F);
+ const __m256i hi = _mm256_shuffle_epi8(lut_hi, hi_nibbles);
+ const __m256i lo = _mm256_shuffle_epi8(lut_lo, lo_nibbles);
+
+ if (!_mm256_testz_si256(lo, hi)) {
+ return 0;
+ }
+
+ const __m256i eq_2F = _mm256_cmpeq_epi8(str, mask_2F);
+ const __m256i roll = _mm256_shuffle_epi8(lut_roll, _mm256_add_epi8(eq_2F, hi_nibbles));
+
+ // Now simply add the delta values to the input:
+ str = _mm256_add_epi8(str, roll);
+
+ // Reshuffle the input to packed 12-byte output format:
+ str = dec_reshuffle(str);
+
+ // Store the output:
+ _mm256_storeu_si256((__m256i *) *o, str);
+
+ *s += 32;
+ *o += 24;
+ *rounds -= 1;
+
+ return 1;
+}
+
+static inline void
+dec_loop_avx2 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ if (*slen < 45) {
+ return;
+ }
+
+ // Process blocks of 32 bytes per round. Because 8 extra zero bytes are
+ // written after the output, ensure that there will be at least 13
+ // bytes of input data left to cover the gap. (11 data bytes and up to
+ // two end-of-string markers.)
+ size_t rounds = (*slen - 13) / 32;
+
+ *slen -= rounds * 32; // 32 bytes consumed per round
+ *olen += rounds * 24; // 24 bytes produced per round
+
+ do {
+ if (rounds >= 8) {
+ if (dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds)) {
+ continue;
+ }
+ break;
+ }
+ if (rounds >= 4) {
+ if (dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds)) {
+ continue;
+ }
+ break;
+ }
+ if (rounds >= 2) {
+ if (dec_loop_avx2_inner(s, o, &rounds) &&
+ dec_loop_avx2_inner(s, o, &rounds)) {
+ continue;
+ }
+ break;
+ }
+ dec_loop_avx2_inner(s, o, &rounds);
+ break;
+
+ } while (rounds > 0);
+
+ // Adjust for any rounds that were skipped:
+ *slen += rounds * 32;
+ *olen -= rounds * 24;
+}
diff --git a/src/third-party/base64/lib/arch/avx2/dec_reshuffle.c b/src/third-party/base64/lib/arch/avx2/dec_reshuffle.c
new file mode 100644
index 0000000..f351809
--- /dev/null
+++ b/src/third-party/base64/lib/arch/avx2/dec_reshuffle.c
@@ -0,0 +1,34 @@
+static inline __m256i
+dec_reshuffle (const __m256i in)
+{
+ // in, lower lane, bits, upper case are most significant bits, lower
+ // case are least significant bits:
+ // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ
+ // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG
+ // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD
+ // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA
+
+ const __m256i merge_ab_and_bc = _mm256_maddubs_epi16(in, _mm256_set1_epi32(0x01400140));
+ // 0000kkkk LLllllll 0000JJJJ JJjjKKKK
+ // 0000hhhh IIiiiiii 0000GGGG GGggHHHH
+ // 0000eeee FFffffff 0000DDDD DDddEEEE
+ // 0000bbbb CCcccccc 0000AAAA AAaaBBBB
+
+ __m256i out = _mm256_madd_epi16(merge_ab_and_bc, _mm256_set1_epi32(0x00011000));
+ // 00000000 JJJJJJjj KKKKkkkk LLllllll
+ // 00000000 GGGGGGgg HHHHhhhh IIiiiiii
+ // 00000000 DDDDDDdd EEEEeeee FFffffff
+ // 00000000 AAAAAAaa BBBBbbbb CCcccccc
+
+ // Pack bytes together in each lane:
+ out = _mm256_shuffle_epi8(out, _mm256_setr_epi8(
+ 2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, -1, -1, -1, -1,
+ 2, 1, 0, 6, 5, 4, 10, 9, 8, 14, 13, 12, -1, -1, -1, -1));
+ // 00000000 00000000 00000000 00000000
+ // LLllllll KKKKkkkk JJJJJJjj IIiiiiii
+ // HHHHhhhh GGGGGGgg FFffffff EEEEeeee
+ // DDDDDDdd CCcccccc BBBBbbbb AAAAAAaa
+
+ // Pack lanes:
+ return _mm256_permutevar8x32_epi32(out, _mm256_setr_epi32(0, 1, 2, 4, 5, 6, -1, -1));
+}
diff --git a/src/third-party/base64/lib/arch/avx2/enc_loop.c b/src/third-party/base64/lib/arch/avx2/enc_loop.c
new file mode 100644
index 0000000..b9e2736
--- /dev/null
+++ b/src/third-party/base64/lib/arch/avx2/enc_loop.c
@@ -0,0 +1,89 @@
+static inline void
+enc_loop_avx2_inner_first (const uint8_t **s, uint8_t **o)
+{
+ // First load is done at s - 0 to not get a segfault:
+ __m256i src = _mm256_loadu_si256((__m256i *) *s);
+
+ // Shift by 4 bytes, as required by enc_reshuffle:
+ src = _mm256_permutevar8x32_epi32(src, _mm256_setr_epi32(0, 0, 1, 2, 3, 4, 5, 6));
+
+ // Reshuffle, translate, store:
+ src = enc_reshuffle(src);
+ src = enc_translate(src);
+ _mm256_storeu_si256((__m256i *) *o, src);
+
+ // Subsequent loads will be done at s - 4, set pointer for next round:
+ *s += 20;
+ *o += 32;
+}
+
+static inline void
+enc_loop_avx2_inner (const uint8_t **s, uint8_t **o)
+{
+ // Load input:
+ __m256i src = _mm256_loadu_si256((__m256i *) *s);
+
+ // Reshuffle, translate, store:
+ src = enc_reshuffle(src);
+ src = enc_translate(src);
+ _mm256_storeu_si256((__m256i *) *o, src);
+
+ *s += 24;
+ *o += 32;
+}
+
+static inline void
+enc_loop_avx2 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ if (*slen < 32) {
+ return;
+ }
+
+ // Process blocks of 24 bytes at a time. Because blocks are loaded 32
+ // bytes at a time an offset of -4, ensure that there will be at least
+ // 4 remaining bytes after the last round, so that the final read will
+ // not pass beyond the bounds of the input buffer:
+ size_t rounds = (*slen - 4) / 24;
+
+ *slen -= rounds * 24; // 24 bytes consumed per round
+ *olen += rounds * 32; // 32 bytes produced per round
+
+ // The first loop iteration requires special handling to ensure that
+ // the read, which is done at an offset, does not underflow the buffer:
+ enc_loop_avx2_inner_first(s, o);
+ rounds--;
+
+ while (rounds > 0) {
+ if (rounds >= 8) {
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ rounds -= 8;
+ continue;
+ }
+ if (rounds >= 4) {
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ rounds -= 4;
+ continue;
+ }
+ if (rounds >= 2) {
+ enc_loop_avx2_inner(s, o);
+ enc_loop_avx2_inner(s, o);
+ rounds -= 2;
+ continue;
+ }
+ enc_loop_avx2_inner(s, o);
+ break;
+ }
+
+ // Add the offset back:
+ *s += 4;
+}
diff --git a/src/third-party/base64/lib/arch/avx2/enc_reshuffle.c b/src/third-party/base64/lib/arch/avx2/enc_reshuffle.c
new file mode 100644
index 0000000..ba16690
--- /dev/null
+++ b/src/third-party/base64/lib/arch/avx2/enc_reshuffle.c
@@ -0,0 +1,83 @@
+static inline __m256i
+enc_reshuffle (const __m256i input)
+{
+ // Translation of the SSSE3 reshuffling algorithm to AVX2. This one
+ // works with shifted (4 bytes) input in order to be able to work
+ // efficiently in the two 128-bit lanes.
+
+ // Input, bytes MSB to LSB:
+ // 0 0 0 0 x w v u t s r q p o n m
+ // l k j i h g f e d c b a 0 0 0 0
+
+ const __m256i in = _mm256_shuffle_epi8(input, _mm256_set_epi8(
+ 10, 11, 9, 10,
+ 7, 8, 6, 7,
+ 4, 5, 3, 4,
+ 1, 2, 0, 1,
+
+ 14, 15, 13, 14,
+ 11, 12, 10, 11,
+ 8, 9, 7, 8,
+ 5, 6, 4, 5));
+ // in, bytes MSB to LSB:
+ // w x v w
+ // t u s t
+ // q r p q
+ // n o m n
+ // k l j k
+ // h i g h
+ // e f d e
+ // b c a b
+
+ const __m256i t0 = _mm256_and_si256(in, _mm256_set1_epi32(0x0FC0FC00));
+ // bits, upper case are most significant bits, lower case are least
+ // significant bits.
+ // 0000wwww XX000000 VVVVVV00 00000000
+ // 0000tttt UU000000 SSSSSS00 00000000
+ // 0000qqqq RR000000 PPPPPP00 00000000
+ // 0000nnnn OO000000 MMMMMM00 00000000
+ // 0000kkkk LL000000 JJJJJJ00 00000000
+ // 0000hhhh II000000 GGGGGG00 00000000
+ // 0000eeee FF000000 DDDDDD00 00000000
+ // 0000bbbb CC000000 AAAAAA00 00000000
+
+ const __m256i t1 = _mm256_mulhi_epu16(t0, _mm256_set1_epi32(0x04000040));
+ // 00000000 00wwwwXX 00000000 00VVVVVV
+ // 00000000 00ttttUU 00000000 00SSSSSS
+ // 00000000 00qqqqRR 00000000 00PPPPPP
+ // 00000000 00nnnnOO 00000000 00MMMMMM
+ // 00000000 00kkkkLL 00000000 00JJJJJJ
+ // 00000000 00hhhhII 00000000 00GGGGGG
+ // 00000000 00eeeeFF 00000000 00DDDDDD
+ // 00000000 00bbbbCC 00000000 00AAAAAA
+
+ const __m256i t2 = _mm256_and_si256(in, _mm256_set1_epi32(0x003F03F0));
+ // 00000000 00xxxxxx 000000vv WWWW0000
+ // 00000000 00uuuuuu 000000ss TTTT0000
+ // 00000000 00rrrrrr 000000pp QQQQ0000
+ // 00000000 00oooooo 000000mm NNNN0000
+ // 00000000 00llllll 000000jj KKKK0000
+ // 00000000 00iiiiii 000000gg HHHH0000
+ // 00000000 00ffffff 000000dd EEEE0000
+ // 00000000 00cccccc 000000aa BBBB0000
+
+ const __m256i t3 = _mm256_mullo_epi16(t2, _mm256_set1_epi32(0x01000010));
+ // 00xxxxxx 00000000 00vvWWWW 00000000
+ // 00uuuuuu 00000000 00ssTTTT 00000000
+ // 00rrrrrr 00000000 00ppQQQQ 00000000
+ // 00oooooo 00000000 00mmNNNN 00000000
+ // 00llllll 00000000 00jjKKKK 00000000
+ // 00iiiiii 00000000 00ggHHHH 00000000
+ // 00ffffff 00000000 00ddEEEE 00000000
+ // 00cccccc 00000000 00aaBBBB 00000000
+
+ return _mm256_or_si256(t1, t3);
+ // 00xxxxxx 00wwwwXX 00vvWWWW 00VVVVVV
+ // 00uuuuuu 00ttttUU 00ssTTTT 00SSSSSS
+ // 00rrrrrr 00qqqqRR 00ppQQQQ 00PPPPPP
+ // 00oooooo 00nnnnOO 00mmNNNN 00MMMMMM
+ // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ
+ // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG
+ // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD
+ // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA
+}
diff --git a/src/third-party/base64/lib/arch/avx2/enc_translate.c b/src/third-party/base64/lib/arch/avx2/enc_translate.c
new file mode 100644
index 0000000..46173cd
--- /dev/null
+++ b/src/third-party/base64/lib/arch/avx2/enc_translate.c
@@ -0,0 +1,30 @@
+static inline __m256i
+enc_translate (const __m256i in)
+{
+ // A lookup table containing the absolute offsets for all ranges:
+ const __m256i lut = _mm256_setr_epi8(
+ 65, 71, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -19, -16, 0, 0,
+ 65, 71, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -19, -16, 0, 0);
+
+ // Translate values 0..63 to the Base64 alphabet. There are five sets:
+ // # From To Abs Index Characters
+ // 0 [0..25] [65..90] +65 0 ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ // 1 [26..51] [97..122] +71 1 abcdefghijklmnopqrstuvwxyz
+ // 2 [52..61] [48..57] -4 [2..11] 0123456789
+ // 3 [62] [43] -19 12 +
+ // 4 [63] [47] -16 13 /
+
+ // Create LUT indices from the input. The index for range #0 is right,
+ // others are 1 less than expected:
+ __m256i indices = _mm256_subs_epu8(in, _mm256_set1_epi8(51));
+
+ // mask is 0xFF (-1) for range #[1..4] and 0x00 for range #0:
+ const __m256i mask = _mm256_cmpgt_epi8(in, _mm256_set1_epi8(25));
+
+ // Subtract -1, so add 1 to indices for range #[1..4]. All indices are
+ // now correct:
+ indices = _mm256_sub_epi8(indices, mask);
+
+ // Add offsets to input values:
+ return _mm256_add_epi8(in, _mm256_shuffle_epi8(lut, indices));
+}
diff --git a/src/third-party/base64/lib/arch/generic/32/dec_loop.c b/src/third-party/base64/lib/arch/generic/32/dec_loop.c
new file mode 100644
index 0000000..8a8260f
--- /dev/null
+++ b/src/third-party/base64/lib/arch/generic/32/dec_loop.c
@@ -0,0 +1,86 @@
+static inline int
+dec_loop_generic_32_inner (const uint8_t **s, uint8_t **o, size_t *rounds)
+{
+ const uint32_t str
+ = base64_table_dec_32bit_d0[(*s)[0]]
+ | base64_table_dec_32bit_d1[(*s)[1]]
+ | base64_table_dec_32bit_d2[(*s)[2]]
+ | base64_table_dec_32bit_d3[(*s)[3]];
+
+#if BASE64_LITTLE_ENDIAN
+
+ // LUTs for little-endian set MSB in case of invalid character:
+ if (str & UINT32_C(0x80000000)) {
+ return 0;
+ }
+#else
+ // LUTs for big-endian set LSB in case of invalid character:
+ if (str & UINT32_C(1)) {
+ return 0;
+ }
+#endif
+ // Store the output:
+ memcpy(*o, &str, sizeof (str));
+
+ *s += 4;
+ *o += 3;
+ *rounds -= 1;
+
+ return 1;
+}
+
+static inline void
+dec_loop_generic_32 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ if (*slen < 8) {
+ return;
+ }
+
+ // Process blocks of 4 bytes per round. Because one extra zero byte is
+ // written after the output, ensure that there will be at least 4 bytes
+ // of input data left to cover the gap. (Two data bytes and up to two
+ // end-of-string markers.)
+ size_t rounds = (*slen - 4) / 4;
+
+ *slen -= rounds * 4; // 4 bytes consumed per round
+ *olen += rounds * 3; // 3 bytes produced per round
+
+ do {
+ if (rounds >= 8) {
+ if (dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds)) {
+ continue;
+ }
+ break;
+ }
+ if (rounds >= 4) {
+ if (dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds)) {
+ continue;
+ }
+ break;
+ }
+ if (rounds >= 2) {
+ if (dec_loop_generic_32_inner(s, o, &rounds) &&
+ dec_loop_generic_32_inner(s, o, &rounds)) {
+ continue;
+ }
+ break;
+ }
+ dec_loop_generic_32_inner(s, o, &rounds);
+ break;
+
+ } while (rounds > 0);
+
+ // Adjust for any rounds that were skipped:
+ *slen += rounds * 4;
+ *olen -= rounds * 3;
+}
diff --git a/src/third-party/base64/lib/arch/generic/32/enc_loop.c b/src/third-party/base64/lib/arch/generic/32/enc_loop.c
new file mode 100644
index 0000000..f4870a7
--- /dev/null
+++ b/src/third-party/base64/lib/arch/generic/32/enc_loop.c
@@ -0,0 +1,73 @@
+static inline void
+enc_loop_generic_32_inner (const uint8_t **s, uint8_t **o)
+{
+ uint32_t src;
+
+ // Load input:
+ memcpy(&src, *s, sizeof (src));
+
+ // Reorder to 32-bit big-endian, if not already in that format. The
+ // workset must be in big-endian, otherwise the shifted bits do not
+ // carry over properly among adjacent bytes:
+ src = BASE64_HTOBE32(src);
+
+ // Two indices for the 12-bit lookup table:
+ const size_t index0 = (src >> 20) & 0xFFFU;
+ const size_t index1 = (src >> 8) & 0xFFFU;
+
+ // Table lookup and store:
+ memcpy(*o + 0, base64_table_enc_12bit + index0, 2);
+ memcpy(*o + 2, base64_table_enc_12bit + index1, 2);
+
+ *s += 3;
+ *o += 4;
+}
+
+static inline void
+enc_loop_generic_32 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ if (*slen < 4) {
+ return;
+ }
+
+ // Process blocks of 3 bytes at a time. Because blocks are loaded 4
+ // bytes at a time, ensure that there will be at least one remaining
+ // byte after the last round, so that the final read will not pass
+ // beyond the bounds of the input buffer:
+ size_t rounds = (*slen - 1) / 3;
+
+ *slen -= rounds * 3; // 3 bytes consumed per round
+ *olen += rounds * 4; // 4 bytes produced per round
+
+ do {
+ if (rounds >= 8) {
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ rounds -= 8;
+ continue;
+ }
+ if (rounds >= 4) {
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ rounds -= 4;
+ continue;
+ }
+ if (rounds >= 2) {
+ enc_loop_generic_32_inner(s, o);
+ enc_loop_generic_32_inner(s, o);
+ rounds -= 2;
+ continue;
+ }
+ enc_loop_generic_32_inner(s, o);
+ break;
+
+ } while (rounds > 0);
+}
diff --git a/src/third-party/base64/lib/arch/generic/64/enc_loop.c b/src/third-party/base64/lib/arch/generic/64/enc_loop.c
new file mode 100644
index 0000000..0840bc7
--- /dev/null
+++ b/src/third-party/base64/lib/arch/generic/64/enc_loop.c
@@ -0,0 +1,77 @@
+static inline void
+enc_loop_generic_64_inner (const uint8_t **s, uint8_t **o)
+{
+ uint64_t src;
+
+ // Load input:
+ memcpy(&src, *s, sizeof (src));
+
+ // Reorder to 64-bit big-endian, if not already in that format. The
+ // workset must be in big-endian, otherwise the shifted bits do not
+ // carry over properly among adjacent bytes:
+ src = BASE64_HTOBE64(src);
+
+ // Four indices for the 12-bit lookup table:
+ const size_t index0 = (src >> 52) & 0xFFFU;
+ const size_t index1 = (src >> 40) & 0xFFFU;
+ const size_t index2 = (src >> 28) & 0xFFFU;
+ const size_t index3 = (src >> 16) & 0xFFFU;
+
+ // Table lookup and store:
+ memcpy(*o + 0, base64_table_enc_12bit + index0, 2);
+ memcpy(*o + 2, base64_table_enc_12bit + index1, 2);
+ memcpy(*o + 4, base64_table_enc_12bit + index2, 2);
+ memcpy(*o + 6, base64_table_enc_12bit + index3, 2);
+
+ *s += 6;
+ *o += 8;
+}
+
+static inline void
+enc_loop_generic_64 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ if (*slen < 8) {
+ return;
+ }
+
+ // Process blocks of 6 bytes at a time. Because blocks are loaded 8
+ // bytes at a time, ensure that there will be at least 2 remaining
+ // bytes after the last round, so that the final read will not pass
+ // beyond the bounds of the input buffer:
+ size_t rounds = (*slen - 2) / 6;
+
+ *slen -= rounds * 6; // 6 bytes consumed per round
+ *olen += rounds * 8; // 8 bytes produced per round
+
+ do {
+ if (rounds >= 8) {
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ rounds -= 8;
+ continue;
+ }
+ if (rounds >= 4) {
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ rounds -= 4;
+ continue;
+ }
+ if (rounds >= 2) {
+ enc_loop_generic_64_inner(s, o);
+ enc_loop_generic_64_inner(s, o);
+ rounds -= 2;
+ continue;
+ }
+ enc_loop_generic_64_inner(s, o);
+ break;
+
+ } while (rounds > 0);
+}
diff --git a/src/third-party/base64/lib/arch/generic/codec.c b/src/third-party/base64/lib/arch/generic/codec.c
new file mode 100644
index 0000000..8dd5af2
--- /dev/null
+++ b/src/third-party/base64/lib/arch/generic/codec.c
@@ -0,0 +1,39 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "../../../include/libbase64.h"
+#include "../../tables/tables.h"
+#include "../../codecs.h"
+#include "config.h"
+#include "../../env.h"
+
+#if BASE64_WORDSIZE == 32
+# include "32/enc_loop.c"
+#elif BASE64_WORDSIZE == 64
+# include "64/enc_loop.c"
+#endif
+
+#if BASE64_WORDSIZE >= 32
+# include "32/dec_loop.c"
+#endif
+
+BASE64_ENC_FUNCTION(plain)
+{
+ #include "enc_head.c"
+#if BASE64_WORDSIZE == 32
+ enc_loop_generic_32(&s, &slen, &o, &olen);
+#elif BASE64_WORDSIZE == 64
+ enc_loop_generic_64(&s, &slen, &o, &olen);
+#endif
+ #include "enc_tail.c"
+}
+
+BASE64_DEC_FUNCTION(plain)
+{
+ #include "dec_head.c"
+#if BASE64_WORDSIZE >= 32
+ dec_loop_generic_32(&s, &slen, &o, &olen);
+#endif
+ #include "dec_tail.c"
+}
diff --git a/src/third-party/base64/lib/arch/generic/dec_head.c b/src/third-party/base64/lib/arch/generic/dec_head.c
new file mode 100644
index 0000000..179a31b
--- /dev/null
+++ b/src/third-party/base64/lib/arch/generic/dec_head.c
@@ -0,0 +1,37 @@
+int ret = 0;
+const uint8_t *s = (const uint8_t *) src;
+uint8_t *o = (uint8_t *) out;
+uint8_t q;
+
+// Use local temporaries to avoid cache thrashing:
+size_t olen = 0;
+size_t slen = srclen;
+struct base64_state st;
+st.eof = state->eof;
+st.bytes = state->bytes;
+st.carry = state->carry;
+
+// If we previously saw an EOF or an invalid character, bail out:
+if (st.eof) {
+ *outlen = 0;
+ ret = 0;
+ // If there was a trailing '=' to check, check it:
+ if (slen && (st.eof == BASE64_AEOF)) {
+ state->bytes = 0;
+ state->eof = BASE64_EOF;
+ ret = ((base64_table_dec_8bit[*s++] == 254) && (slen == 1)) ? 1 : 0;
+ }
+ return ret;
+}
+
+// Turn four 6-bit numbers into three bytes:
+// out[0] = 11111122
+// out[1] = 22223333
+// out[2] = 33444444
+
+// Duff's device again:
+switch (st.bytes)
+{
+ for (;;)
+ {
+ case 0:
diff --git a/src/third-party/base64/lib/arch/generic/dec_tail.c b/src/third-party/base64/lib/arch/generic/dec_tail.c
new file mode 100644
index 0000000..e64f724
--- /dev/null
+++ b/src/third-party/base64/lib/arch/generic/dec_tail.c
@@ -0,0 +1,91 @@
+ if (slen-- == 0) {
+ ret = 1;
+ break;
+ }
+ if ((q = base64_table_dec_8bit[*s++]) >= 254) {
+ st.eof = BASE64_EOF;
+ // Treat character '=' as invalid for byte 0:
+ break;
+ }
+ st.carry = q << 2;
+ st.bytes++;
+
+ // Deliberate fallthrough:
+ BASE64_FALLTHROUGH
+
+ case 1: if (slen-- == 0) {
+ ret = 1;
+ break;
+ }
+ if ((q = base64_table_dec_8bit[*s++]) >= 254) {
+ st.eof = BASE64_EOF;
+ // Treat character '=' as invalid for byte 1:
+ break;
+ }
+ *o++ = st.carry | (q >> 4);
+ st.carry = q << 4;
+ st.bytes++;
+ olen++;
+
+ // Deliberate fallthrough:
+ BASE64_FALLTHROUGH
+
+ case 2: if (slen-- == 0) {
+ ret = 1;
+ break;
+ }
+ if ((q = base64_table_dec_8bit[*s++]) >= 254) {
+ st.bytes++;
+ // When q == 254, the input char is '='.
+ // Check if next byte is also '=':
+ if (q == 254) {
+ if (slen-- != 0) {
+ st.bytes = 0;
+ // EOF:
+ st.eof = BASE64_EOF;
+ q = base64_table_dec_8bit[*s++];
+ ret = ((q == 254) && (slen == 0)) ? 1 : 0;
+ break;
+ }
+ else {
+ // Almost EOF
+ st.eof = BASE64_AEOF;
+ ret = 1;
+ break;
+ }
+ }
+ // If we get here, there was an error:
+ break;
+ }
+ *o++ = st.carry | (q >> 2);
+ st.carry = q << 6;
+ st.bytes++;
+ olen++;
+
+ // Deliberate fallthrough:
+ BASE64_FALLTHROUGH
+
+ case 3: if (slen-- == 0) {
+ ret = 1;
+ break;
+ }
+ if ((q = base64_table_dec_8bit[*s++]) >= 254) {
+ st.bytes = 0;
+ st.eof = BASE64_EOF;
+ // When q == 254, the input char is '='. Return 1 and EOF.
+ // When q == 255, the input char is invalid. Return 0 and EOF.
+ ret = ((q == 254) && (slen == 0)) ? 1 : 0;
+ break;
+ }
+ *o++ = st.carry | q;
+ st.carry = 0;
+ st.bytes = 0;
+ olen++;
+ }
+}
+
+state->eof = st.eof;
+state->bytes = st.bytes;
+state->carry = st.carry;
+*outlen = olen;
+return ret;
diff --git a/src/third-party/base64/lib/arch/generic/enc_head.c b/src/third-party/base64/lib/arch/generic/enc_head.c
new file mode 100644
index 0000000..38d60b2
--- /dev/null
+++ b/src/third-party/base64/lib/arch/generic/enc_head.c
@@ -0,0 +1,24 @@
+// Assume that *out is large enough to contain the output.
+// Theoretically it should be 4/3 the length of src.
+const uint8_t *s = (const uint8_t *) src;
+uint8_t *o = (uint8_t *) out;
+
+// Use local temporaries to avoid cache thrashing:
+size_t olen = 0;
+size_t slen = srclen;
+struct base64_state st;
+st.bytes = state->bytes;
+st.carry = state->carry;
+
+// Turn three bytes into four 6-bit numbers:
+// in[0] = 00111111
+// in[1] = 00112222
+// in[2] = 00222233
+// in[3] = 00333333
+
+// Duff's device, a for() loop inside a switch() statement. Legal!
+switch (st.bytes)
+{
+ for (;;)
+ {
+ case 0:
diff --git a/src/third-party/base64/lib/arch/generic/enc_tail.c b/src/third-party/base64/lib/arch/generic/enc_tail.c
new file mode 100644
index 0000000..cbd5733
--- /dev/null
+++ b/src/third-party/base64/lib/arch/generic/enc_tail.c
@@ -0,0 +1,34 @@
+ if (slen-- == 0) {
+ break;
+ }
+ *o++ = base64_table_enc_6bit[*s >> 2];
+ st.carry = (*s++ << 4) & 0x30;
+ st.bytes++;
+ olen += 1;
+
+ // Deliberate fallthrough:
+ BASE64_FALLTHROUGH
+
+ case 1: if (slen-- == 0) {
+ break;
+ }
+ *o++ = base64_table_enc_6bit[st.carry | (*s >> 4)];
+ st.carry = (*s++ << 2) & 0x3C;
+ st.bytes++;
+ olen += 1;
+
+ // Deliberate fallthrough:
+ BASE64_FALLTHROUGH
+
+ case 2: if (slen-- == 0) {
+ break;
+ }
+ *o++ = base64_table_enc_6bit[st.carry | (*s >> 6)];
+ *o++ = base64_table_enc_6bit[*s++ & 0x3F];
+ st.bytes = 0;
+ olen += 2;
+ }
+}
+state->bytes = st.bytes;
+state->carry = st.carry;
+*outlen = olen;
diff --git a/src/third-party/base64/lib/arch/neon32/codec.c b/src/third-party/base64/lib/arch/neon32/codec.c
new file mode 100644
index 0000000..a0b27f9
--- /dev/null
+++ b/src/third-party/base64/lib/arch/neon32/codec.c
@@ -0,0 +1,77 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "../../../include/libbase64.h"
+#include "../../tables/tables.h"
+#include "../../codecs.h"
+#include "config.h"
+#include "../../env.h"
+
+#ifdef __arm__
+# if (defined(__ARM_NEON__) || defined(__ARM_NEON)) && HAVE_NEON32
+# define BASE64_USE_NEON32
+# endif
+#endif
+
+#ifdef BASE64_USE_NEON32
+#include <arm_neon.h>
+
+// Only enable inline assembly on supported compilers.
+#if defined(__GNUC__) || defined(__clang__)
+#define BASE64_NEON32_USE_ASM
+#endif
+
+static inline uint8x16_t
+vqtbl1q_u8 (const uint8x16_t lut, const uint8x16_t indices)
+{
+ // NEON32 only supports 64-bit wide lookups in 128-bit tables. Emulate
+ // the NEON64 `vqtbl1q_u8` intrinsic to do 128-bit wide lookups.
+ uint8x8x2_t lut2;
+ uint8x8x2_t result;
+
+ lut2.val[0] = vget_low_u8(lut);
+ lut2.val[1] = vget_high_u8(lut);
+
+ result.val[0] = vtbl2_u8(lut2, vget_low_u8(indices));
+ result.val[1] = vtbl2_u8(lut2, vget_high_u8(indices));
+
+ return vcombine_u8(result.val[0], result.val[1]);
+}
+
+#include "../generic/32/dec_loop.c"
+#include "../generic/32/enc_loop.c"
+#include "dec_loop.c"
+#include "enc_reshuffle.c"
+#include "enc_translate.c"
+#include "enc_loop.c"
+
+#endif // BASE64_USE_NEON32
+
+// Stride size is so large on these NEON 32-bit functions
+// (48 bytes encode, 32 bytes decode) that we inline the
+// uint32 codec to stay performant on smaller inputs.
+
+BASE64_ENC_FUNCTION(neon32)
+{
+#ifdef BASE64_USE_NEON32
+ #include "../generic/enc_head.c"
+ enc_loop_neon32(&s, &slen, &o, &olen);
+ enc_loop_generic_32(&s, &slen, &o, &olen);
+ #include "../generic/enc_tail.c"
+#else
+ BASE64_ENC_STUB
+#endif
+}
+
+BASE64_DEC_FUNCTION(neon32)
+{
+#ifdef BASE64_USE_NEON32
+ #include "../generic/dec_head.c"
+ dec_loop_neon32(&s, &slen, &o, &olen);
+ dec_loop_generic_32(&s, &slen, &o, &olen);
+ #include "../generic/dec_tail.c"
+#else
+ BASE64_DEC_STUB
+#endif
+}
diff --git a/src/third-party/base64/lib/arch/neon32/dec_loop.c b/src/third-party/base64/lib/arch/neon32/dec_loop.c
new file mode 100644
index 0000000..2216b39
--- /dev/null
+++ b/src/third-party/base64/lib/arch/neon32/dec_loop.c
@@ -0,0 +1,106 @@
+static inline int
+is_nonzero (const uint8x16_t v)
+{
+ uint64_t u64;
+ const uint64x2_t v64 = vreinterpretq_u64_u8(v);
+ const uint32x2_t v32 = vqmovn_u64(v64);
+
+ vst1_u64(&u64, vreinterpret_u64_u32(v32));
+ return u64 != 0;
+}
+
+static inline uint8x16_t
+delta_lookup (const uint8x16_t v)
+{
+ const uint8x8_t lut = {
+ 0, 16, 19, 4, (uint8_t) -65, (uint8_t) -65, (uint8_t) -71, (uint8_t) -71,
+ };
+
+ return vcombine_u8(
+ vtbl1_u8(lut, vget_low_u8(v)),
+ vtbl1_u8(lut, vget_high_u8(v)));
+}
+
+static inline uint8x16_t
+dec_loop_neon32_lane (uint8x16_t *lane)
+{
+ // See the SSSE3 decoder for an explanation of the algorithm.
+ const uint8x16_t lut_lo = {
+ 0x15, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x13, 0x1A, 0x1B, 0x1B, 0x1B, 0x1A
+ };
+
+ const uint8x16_t lut_hi = {
+ 0x10, 0x10, 0x01, 0x02, 0x04, 0x08, 0x04, 0x08,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10
+ };
+
+ const uint8x16_t mask_0F = vdupq_n_u8(0x0F);
+ const uint8x16_t mask_2F = vdupq_n_u8(0x2F);
+
+ const uint8x16_t hi_nibbles = vshrq_n_u8(*lane, 4);
+ const uint8x16_t lo_nibbles = vandq_u8(*lane, mask_0F);
+ const uint8x16_t eq_2F = vceqq_u8(*lane, mask_2F);
+
+ const uint8x16_t hi = vqtbl1q_u8(lut_hi, hi_nibbles);
+ const uint8x16_t lo = vqtbl1q_u8(lut_lo, lo_nibbles);
+
+ // Now simply add the delta values to the input:
+ *lane = vaddq_u8(*lane, delta_lookup(vaddq_u8(eq_2F, hi_nibbles)));
+
+ // Return the validity mask:
+ return vandq_u8(lo, hi);
+}
+
+static inline void
+dec_loop_neon32 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ if (*slen < 64) {
+ return;
+ }
+
+ // Process blocks of 64 bytes per round. Unlike the SSE codecs, no
+ // extra trailing zero bytes are written, so it is not necessary to
+ // reserve extra input bytes:
+ size_t rounds = *slen / 64;
+
+ *slen -= rounds * 64; // 64 bytes consumed per round
+ *olen += rounds * 48; // 48 bytes produced per round
+
+ do {
+ uint8x16x3_t dec;
+
+ // Load 64 bytes and deinterleave:
+ uint8x16x4_t str = vld4q_u8(*s);
+
+ // Decode each lane, collect a mask of invalid inputs:
+ const uint8x16_t classified
+ = dec_loop_neon32_lane(&str.val[0])
+ | dec_loop_neon32_lane(&str.val[1])
+ | dec_loop_neon32_lane(&str.val[2])
+ | dec_loop_neon32_lane(&str.val[3]);
+
+ // Check for invalid input: if any of the delta values are
+ // zero, fall back on bytewise code to do error checking and
+ // reporting:
+ if (is_nonzero(classified)) {
+ break;
+ }
+
+ // Compress four bytes into three:
+ dec.val[0] = vorrq_u8(vshlq_n_u8(str.val[0], 2), vshrq_n_u8(str.val[1], 4));
+ dec.val[1] = vorrq_u8(vshlq_n_u8(str.val[1], 4), vshrq_n_u8(str.val[2], 2));
+ dec.val[2] = vorrq_u8(vshlq_n_u8(str.val[2], 6), str.val[3]);
+
+ // Interleave and store decoded result:
+ vst3q_u8(*o, dec);
+
+ *s += 64;
+ *o += 48;
+
+ } while (--rounds > 0);
+
+ // Adjust for any rounds that were skipped:
+ *slen += rounds * 64;
+ *olen -= rounds * 48;
+}
diff --git a/src/third-party/base64/lib/arch/neon32/enc_loop.c b/src/third-party/base64/lib/arch/neon32/enc_loop.c
new file mode 100644
index 0000000..e9e8e28
--- /dev/null
+++ b/src/third-party/base64/lib/arch/neon32/enc_loop.c
@@ -0,0 +1,169 @@
+#ifdef BASE64_NEON32_USE_ASM
+static inline void
+enc_loop_neon32_inner_asm (const uint8_t **s, uint8_t **o)
+{
+ // This function duplicates the functionality of enc_loop_neon32_inner,
+ // but entirely with inline assembly. This gives a significant speedup
+ // over using NEON intrinsics, which do not always generate very good
+ // code. The logic of the assembly is directly lifted from the
+ // intrinsics version, so it can be used as a guide to this code.
+
+ // Temporary registers, used as scratch space.
+ uint8x16_t tmp0, tmp1, tmp2, tmp3;
+ uint8x16_t mask0, mask1, mask2, mask3;
+
+ // A lookup table containing the absolute offsets for all ranges.
+ const uint8x16_t lut = {
+ 65U, 71U, 252U, 252U,
+ 252U, 252U, 252U, 252U,
+ 252U, 252U, 252U, 252U,
+ 237U, 240U, 0U, 0U
+ };
+
+ // Numeric constants.
+ const uint8x16_t n51 = vdupq_n_u8(51);
+ const uint8x16_t n25 = vdupq_n_u8(25);
+ const uint8x16_t n63 = vdupq_n_u8(63);
+
+ __asm__ (
+
+ // Load 48 bytes and deinterleave. The bytes are loaded to
+ // hard-coded registers q12, q13 and q14, to ensure that they
+ // are contiguous. Increment the source pointer.
+ "vld3.8 {d24, d26, d28}, [%[src]]! \n\t"
+ "vld3.8 {d25, d27, d29}, [%[src]]! \n\t"
+
+ // Reshuffle the bytes using temporaries.
+ "vshr.u8 %q[t0], q12, #2 \n\t"
+ "vshr.u8 %q[t1], q13, #4 \n\t"
+ "vshr.u8 %q[t2], q14, #6 \n\t"
+ "vsli.8 %q[t1], q12, #4 \n\t"
+ "vsli.8 %q[t2], q13, #2 \n\t"
+ "vand.u8 %q[t1], %q[t1], %q[n63] \n\t"
+ "vand.u8 %q[t2], %q[t2], %q[n63] \n\t"
+ "vand.u8 %q[t3], q14, %q[n63] \n\t"
+
+ // t0..t3 are the reshuffled inputs. Create LUT indices.
+ "vqsub.u8 q12, %q[t0], %q[n51] \n\t"
+ "vqsub.u8 q13, %q[t1], %q[n51] \n\t"
+ "vqsub.u8 q14, %q[t2], %q[n51] \n\t"
+ "vqsub.u8 q15, %q[t3], %q[n51] \n\t"
+
+ // Create the mask for range #0.
+ "vcgt.u8 %q[m0], %q[t0], %q[n25] \n\t"
+ "vcgt.u8 %q[m1], %q[t1], %q[n25] \n\t"
+ "vcgt.u8 %q[m2], %q[t2], %q[n25] \n\t"
+ "vcgt.u8 %q[m3], %q[t3], %q[n25] \n\t"
+
+ // Subtract -1 to correct the LUT indices.
+ "vsub.u8 q12, %q[m0] \n\t"
+ "vsub.u8 q13, %q[m1] \n\t"
+ "vsub.u8 q14, %q[m2] \n\t"
+ "vsub.u8 q15, %q[m3] \n\t"
+
+ // Lookup the delta values.
+ "vtbl.u8 d24, {%q[lut]}, d24 \n\t"
+ "vtbl.u8 d25, {%q[lut]}, d25 \n\t"
+ "vtbl.u8 d26, {%q[lut]}, d26 \n\t"
+ "vtbl.u8 d27, {%q[lut]}, d27 \n\t"
+ "vtbl.u8 d28, {%q[lut]}, d28 \n\t"
+ "vtbl.u8 d29, {%q[lut]}, d29 \n\t"
+ "vtbl.u8 d30, {%q[lut]}, d30 \n\t"
+ "vtbl.u8 d31, {%q[lut]}, d31 \n\t"
+
+ // Add the delta values.
+ "vadd.u8 q12, %q[t0] \n\t"
+ "vadd.u8 q13, %q[t1] \n\t"
+ "vadd.u8 q14, %q[t2] \n\t"
+ "vadd.u8 q15, %q[t3] \n\t"
+
+ // Store 64 bytes and interleave. Increment the dest pointer.
+ "vst4.8 {d24, d26, d28, d30}, [%[dst]]! \n\t"
+ "vst4.8 {d25, d27, d29, d31}, [%[dst]]! \n\t"
+
+ // Outputs (modified).
+ : [src] "+r" (*s),
+ [dst] "+r" (*o),
+ [t0] "=&w" (tmp0),
+ [t1] "=&w" (tmp1),
+ [t2] "=&w" (tmp2),
+ [t3] "=&w" (tmp3),
+ [m0] "=&w" (mask0),
+ [m1] "=&w" (mask1),
+ [m2] "=&w" (mask2),
+ [m3] "=&w" (mask3)
+
+ // Inputs (not modified).
+ : [lut] "w" (lut),
+ [n25] "w" (n25),
+ [n51] "w" (n51),
+ [n63] "w" (n63)
+
+ // Clobbers.
+ : "d24", "d25", "d26", "d27", "d28", "d29", "d30", "d31"
+ );
+}
+#endif
+
+static inline void
+enc_loop_neon32_inner (const uint8_t **s, uint8_t **o)
+{
+#ifdef BASE64_NEON32_USE_ASM
+ enc_loop_neon32_inner_asm(s, o);
+#else
+ // Load 48 bytes and deinterleave:
+ uint8x16x3_t src = vld3q_u8(*s);
+
+ // Reshuffle:
+ uint8x16x4_t out = enc_reshuffle(src);
+
+ // Translate reshuffled bytes to the Base64 alphabet:
+ out = enc_translate(out);
+
+ // Interleave and store output:
+ vst4q_u8(*o, out);
+
+ *s += 48;
+ *o += 64;
+#endif
+}
+
+static inline void
+enc_loop_neon32 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ size_t rounds = *slen / 48;
+
+ *slen -= rounds * 48; // 48 bytes consumed per round
+ *olen += rounds * 64; // 64 bytes produced per round
+
+ while (rounds > 0) {
+ if (rounds >= 8) {
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ rounds -= 8;
+ continue;
+ }
+ if (rounds >= 4) {
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ rounds -= 4;
+ continue;
+ }
+ if (rounds >= 2) {
+ enc_loop_neon32_inner(s, o);
+ enc_loop_neon32_inner(s, o);
+ rounds -= 2;
+ continue;
+ }
+ enc_loop_neon32_inner(s, o);
+ break;
+ }
+}
diff --git a/src/third-party/base64/lib/arch/neon32/enc_reshuffle.c b/src/third-party/base64/lib/arch/neon32/enc_reshuffle.c
new file mode 100644
index 0000000..d6e97cb
--- /dev/null
+++ b/src/third-party/base64/lib/arch/neon32/enc_reshuffle.c
@@ -0,0 +1,31 @@
+static inline uint8x16x4_t
+enc_reshuffle (uint8x16x3_t in)
+{
+ uint8x16x4_t out;
+
+ // Input:
+ // in[0] = a7 a6 a5 a4 a3 a2 a1 a0
+ // in[1] = b7 b6 b5 b4 b3 b2 b1 b0
+ // in[2] = c7 c6 c5 c4 c3 c2 c1 c0
+
+ // Output:
+ // out[0] = 00 00 a7 a6 a5 a4 a3 a2
+ // out[1] = 00 00 a1 a0 b7 b6 b5 b4
+ // out[2] = 00 00 b3 b2 b1 b0 c7 c6
+ // out[3] = 00 00 c5 c4 c3 c2 c1 c0
+
+ // Move the input bits to where they need to be in the outputs. Except
+ // for the first output, the high two bits are not cleared.
+ out.val[0] = vshrq_n_u8(in.val[0], 2);
+ out.val[1] = vshrq_n_u8(in.val[1], 4);
+ out.val[2] = vshrq_n_u8(in.val[2], 6);
+ out.val[1] = vsliq_n_u8(out.val[1], in.val[0], 4);
+ out.val[2] = vsliq_n_u8(out.val[2], in.val[1], 2);
+
+ // Clear the high two bits in the second, third and fourth output.
+ out.val[1] = vandq_u8(out.val[1], vdupq_n_u8(0x3F));
+ out.val[2] = vandq_u8(out.val[2], vdupq_n_u8(0x3F));
+ out.val[3] = vandq_u8(in.val[2], vdupq_n_u8(0x3F));
+
+ return out;
+}
diff --git a/src/third-party/base64/lib/arch/neon32/enc_translate.c b/src/third-party/base64/lib/arch/neon32/enc_translate.c
new file mode 100644
index 0000000..e616d54
--- /dev/null
+++ b/src/third-party/base64/lib/arch/neon32/enc_translate.c
@@ -0,0 +1,57 @@
+static inline uint8x16x4_t
+enc_translate (const uint8x16x4_t in)
+{
+ // A lookup table containing the absolute offsets for all ranges:
+ const uint8x16_t lut = {
+ 65U, 71U, 252U, 252U,
+ 252U, 252U, 252U, 252U,
+ 252U, 252U, 252U, 252U,
+ 237U, 240U, 0U, 0U
+ };
+
+ const uint8x16_t offset = vdupq_n_u8(51);
+
+ uint8x16x4_t indices, mask, delta, out;
+
+ // Translate values 0..63 to the Base64 alphabet. There are five sets:
+ // # From To Abs Index Characters
+ // 0 [0..25] [65..90] +65 0 ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ // 1 [26..51] [97..122] +71 1 abcdefghijklmnopqrstuvwxyz
+ // 2 [52..61] [48..57] -4 [2..11] 0123456789
+ // 3 [62] [43] -19 12 +
+ // 4 [63] [47] -16 13 /
+
+ // Create LUT indices from input:
+ // the index for range #0 is right, others are 1 less than expected:
+ indices.val[0] = vqsubq_u8(in.val[0], offset);
+ indices.val[1] = vqsubq_u8(in.val[1], offset);
+ indices.val[2] = vqsubq_u8(in.val[2], offset);
+ indices.val[3] = vqsubq_u8(in.val[3], offset);
+
+ // mask is 0xFF (-1) for range #[1..4] and 0x00 for range #0:
+ mask.val[0] = vcgtq_u8(in.val[0], vdupq_n_u8(25));
+ mask.val[1] = vcgtq_u8(in.val[1], vdupq_n_u8(25));
+ mask.val[2] = vcgtq_u8(in.val[2], vdupq_n_u8(25));
+ mask.val[3] = vcgtq_u8(in.val[3], vdupq_n_u8(25));
+
+ // Subtract -1, so add 1 to indices for range #[1..4], All indices are
+ // now correct:
+ indices.val[0] = vsubq_u8(indices.val[0], mask.val[0]);
+ indices.val[1] = vsubq_u8(indices.val[1], mask.val[1]);
+ indices.val[2] = vsubq_u8(indices.val[2], mask.val[2]);
+ indices.val[3] = vsubq_u8(indices.val[3], mask.val[3]);
+
+ // Lookup delta values:
+ delta.val[0] = vqtbl1q_u8(lut, indices.val[0]);
+ delta.val[1] = vqtbl1q_u8(lut, indices.val[1]);
+ delta.val[2] = vqtbl1q_u8(lut, indices.val[2]);
+ delta.val[3] = vqtbl1q_u8(lut, indices.val[3]);
+
+ // Add delta values:
+ out.val[0] = vaddq_u8(in.val[0], delta.val[0]);
+ out.val[1] = vaddq_u8(in.val[1], delta.val[1]);
+ out.val[2] = vaddq_u8(in.val[2], delta.val[2]);
+ out.val[3] = vaddq_u8(in.val[3], delta.val[3]);
+
+ return out;
+}
diff --git a/src/third-party/base64/lib/arch/neon64/codec.c b/src/third-party/base64/lib/arch/neon64/codec.c
new file mode 100644
index 0000000..fc953b2
--- /dev/null
+++ b/src/third-party/base64/lib/arch/neon64/codec.c
@@ -0,0 +1,92 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <string.h>
+
+#include "../../../include/libbase64.h"
+#include "../../tables/tables.h"
+#include "../../codecs.h"
+#include "config.h"
+#include "../../env.h"
+
+#ifdef __aarch64__
+# if (defined(__ARM_NEON__) || defined(__ARM_NEON)) && HAVE_NEON64
+# define BASE64_USE_NEON64
+# endif
+#endif
+
+#ifdef BASE64_USE_NEON64
+#include <arm_neon.h>
+
+// Only enable inline assembly on supported compilers.
+#if defined(__GNUC__) || defined(__clang__)
+#define BASE64_NEON64_USE_ASM
+#endif
+
+static inline uint8x16x4_t
+load_64byte_table (const uint8_t *p)
+{
+#ifdef BASE64_NEON64_USE_ASM
+
+ // Force the table to be loaded into contiguous registers. GCC will not
+ // normally allocate contiguous registers for a `uint8x16x4_t'. These
+ // registers are chosen to not conflict with the ones in the enc loop.
+ register uint8x16_t t0 __asm__ ("v8");
+ register uint8x16_t t1 __asm__ ("v9");
+ register uint8x16_t t2 __asm__ ("v10");
+ register uint8x16_t t3 __asm__ ("v11");
+
+ __asm__ (
+ "ld1 {%[t0].16b, %[t1].16b, %[t2].16b, %[t3].16b}, [%[src]], #64 \n\t"
+ : [src] "+r" (p),
+ [t0] "=w" (t0),
+ [t1] "=w" (t1),
+ [t2] "=w" (t2),
+ [t3] "=w" (t3)
+ );
+
+ return (uint8x16x4_t) {
+ .val[0] = t0,
+ .val[1] = t1,
+ .val[2] = t2,
+ .val[3] = t3,
+ };
+#else
+ return vld1q_u8_x4(p);
+#endif
+}
+
+#include "../generic/32/dec_loop.c"
+#include "../generic/64/enc_loop.c"
+#include "dec_loop.c"
+#include "enc_reshuffle.c"
+#include "enc_loop.c"
+
+#endif // BASE64_USE_NEON64
+
+// Stride size is so large on these NEON 64-bit functions
+// (48 bytes encode, 64 bytes decode) that we inline the
+// uint64 codec to stay performant on smaller inputs.
+
+BASE64_ENC_FUNCTION(neon64)
+{
+#ifdef BASE64_USE_NEON64
+ #include "../generic/enc_head.c"
+ enc_loop_neon64(&s, &slen, &o, &olen);
+ enc_loop_generic_64(&s, &slen, &o, &olen);
+ #include "../generic/enc_tail.c"
+#else
+ BASE64_ENC_STUB
+#endif
+}
+
+BASE64_DEC_FUNCTION(neon64)
+{
+#ifdef BASE64_USE_NEON64
+ #include "../generic/dec_head.c"
+ dec_loop_neon64(&s, &slen, &o, &olen);
+ dec_loop_generic_32(&s, &slen, &o, &olen);
+ #include "../generic/dec_tail.c"
+#else
+ BASE64_DEC_STUB
+#endif
+}
diff --git a/src/third-party/base64/lib/arch/neon64/dec_loop.c b/src/third-party/base64/lib/arch/neon64/dec_loop.c
new file mode 100644
index 0000000..48232f2
--- /dev/null
+++ b/src/third-party/base64/lib/arch/neon64/dec_loop.c
@@ -0,0 +1,129 @@
+// The input consists of five valid character sets in the Base64 alphabet,
+// which we need to map back to the 6-bit values they represent.
+// There are three ranges, two singles, and then there's the rest.
+//
+// # From To LUT Characters
+// 1 [0..42] [255] #1 invalid input
+// 2 [43] [62] #1 +
+// 3 [44..46] [255] #1 invalid input
+// 4 [47] [63] #1 /
+// 5 [48..57] [52..61] #1 0..9
+// 6 [58..63] [255] #1 invalid input
+// 7 [64] [255] #2 invalid input
+// 8 [65..90] [0..25] #2 A..Z
+// 9 [91..96] [255] #2 invalid input
+// 10 [97..122] [26..51] #2 a..z
+// 11 [123..126] [255] #2 invalid input
+// (12) Everything else => invalid input
+
+// The first LUT will use the VTBL instruction (out of range indices are set to
+// 0 in destination).
+static const uint8_t dec_lut1[] = {
+ 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U,
+ 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U,
+ 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 255U, 62U, 255U, 255U, 255U, 63U,
+ 52U, 53U, 54U, 55U, 56U, 57U, 58U, 59U, 60U, 61U, 255U, 255U, 255U, 255U, 255U, 255U,
+};
+
+// The second LUT will use the VTBX instruction (out of range indices will be
+// unchanged in destination). Input [64..126] will be mapped to index [1..63]
+// in this LUT. Index 0 means that value comes from LUT #1.
+static const uint8_t dec_lut2[] = {
+ 0U, 255U, 0U, 1U, 2U, 3U, 4U, 5U, 6U, 7U, 8U, 9U, 10U, 11U, 12U, 13U,
+ 14U, 15U, 16U, 17U, 18U, 19U, 20U, 21U, 22U, 23U, 24U, 25U, 255U, 255U, 255U, 255U,
+ 255U, 255U, 26U, 27U, 28U, 29U, 30U, 31U, 32U, 33U, 34U, 35U, 36U, 37U, 38U, 39U,
+ 40U, 41U, 42U, 43U, 44U, 45U, 46U, 47U, 48U, 49U, 50U, 51U, 255U, 255U, 255U, 255U,
+};
+
+// All input values in range for the first look-up will be 0U in the second
+// look-up result. All input values out of range for the first look-up will be
+// 0U in the first look-up result. Thus, the two results can be ORed without
+// conflicts.
+//
+// Invalid characters that are in the valid range for either look-up will be
+// set to 255U in the combined result. Other invalid characters will just be
+// passed through with the second look-up result (using the VTBX instruction).
+// Since the second LUT is 64 bytes, those passed-through values are guaranteed
+// to have a value greater than 63U. Therefore, valid characters will be mapped
+// to the valid [0..63] range and all invalid characters will be mapped to
+// values greater than 63.
+
+static inline void
+dec_loop_neon64 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ if (*slen < 64) {
+ return;
+ }
+
+ // Process blocks of 64 bytes per round. Unlike the SSE codecs, no
+ // extra trailing zero bytes are written, so it is not necessary to
+ // reserve extra input bytes:
+ size_t rounds = *slen / 64;
+
+ *slen -= rounds * 64; // 64 bytes consumed per round
+ *olen += rounds * 48; // 48 bytes produced per round
+
+ const uint8x16x4_t tbl_dec1 = load_64byte_table(dec_lut1);
+ const uint8x16x4_t tbl_dec2 = load_64byte_table(dec_lut2);
+
+ do {
+ const uint8x16_t offset = vdupq_n_u8(63U);
+ uint8x16x4_t dec1, dec2;
+ uint8x16x3_t dec;
+
+ // Load 64 bytes and deinterleave:
+ uint8x16x4_t str = vld4q_u8((uint8_t *) *s);
+
+ // Get indices for second LUT:
+ dec2.val[0] = vqsubq_u8(str.val[0], offset);
+ dec2.val[1] = vqsubq_u8(str.val[1], offset);
+ dec2.val[2] = vqsubq_u8(str.val[2], offset);
+ dec2.val[3] = vqsubq_u8(str.val[3], offset);
+
+ // Get values from first LUT:
+ dec1.val[0] = vqtbl4q_u8(tbl_dec1, str.val[0]);
+ dec1.val[1] = vqtbl4q_u8(tbl_dec1, str.val[1]);
+ dec1.val[2] = vqtbl4q_u8(tbl_dec1, str.val[2]);
+ dec1.val[3] = vqtbl4q_u8(tbl_dec1, str.val[3]);
+
+ // Get values from second LUT:
+ dec2.val[0] = vqtbx4q_u8(dec2.val[0], tbl_dec2, dec2.val[0]);
+ dec2.val[1] = vqtbx4q_u8(dec2.val[1], tbl_dec2, dec2.val[1]);
+ dec2.val[2] = vqtbx4q_u8(dec2.val[2], tbl_dec2, dec2.val[2]);
+ dec2.val[3] = vqtbx4q_u8(dec2.val[3], tbl_dec2, dec2.val[3]);
+
+ // Get final values:
+ str.val[0] = vorrq_u8(dec1.val[0], dec2.val[0]);
+ str.val[1] = vorrq_u8(dec1.val[1], dec2.val[1]);
+ str.val[2] = vorrq_u8(dec1.val[2], dec2.val[2]);
+ str.val[3] = vorrq_u8(dec1.val[3], dec2.val[3]);
+
+ // Check for invalid input, any value larger than 63:
+ const uint8x16_t classified
+ = vcgtq_u8(str.val[0], vdupq_n_u8(63))
+ | vcgtq_u8(str.val[1], vdupq_n_u8(63))
+ | vcgtq_u8(str.val[2], vdupq_n_u8(63))
+ | vcgtq_u8(str.val[3], vdupq_n_u8(63));
+
+ // Check that all bits are zero:
+ if (vmaxvq_u8(classified) != 0U) {
+ break;
+ }
+
+ // Compress four bytes into three:
+ dec.val[0] = vshlq_n_u8(str.val[0], 2) | vshrq_n_u8(str.val[1], 4);
+ dec.val[1] = vshlq_n_u8(str.val[1], 4) | vshrq_n_u8(str.val[2], 2);
+ dec.val[2] = vshlq_n_u8(str.val[2], 6) | str.val[3];
+
+ // Interleave and store decoded result:
+ vst3q_u8((uint8_t *) *o, dec);
+
+ *s += 64;
+ *o += 48;
+
+ } while (--rounds > 0);
+
+ // Adjust for any rounds that were skipped:
+ *slen += rounds * 64;
+ *olen -= rounds * 48;
+}
diff --git a/src/third-party/base64/lib/arch/neon64/enc_loop.c b/src/third-party/base64/lib/arch/neon64/enc_loop.c
new file mode 100644
index 0000000..d1862f7
--- /dev/null
+++ b/src/third-party/base64/lib/arch/neon64/enc_loop.c
@@ -0,0 +1,133 @@
+#ifdef BASE64_NEON64_USE_ASM
+static inline void
+enc_loop_neon64_inner_asm (const uint8_t **s, uint8_t **o, const uint8x16x4_t tbl_enc)
+{
+ // This function duplicates the functionality of enc_loop_neon64_inner,
+ // but entirely with inline assembly. This gives a significant speedup
+ // over using NEON intrinsics, which do not always generate very good
+ // code. The logic of the assembly is directly lifted from the
+ // intrinsics version, so it can be used as a guide to this code.
+
+ // Temporary registers, used as scratch space.
+ uint8x16_t tmp0, tmp1, tmp2, tmp3;
+
+ // Numeric constant.
+ const uint8x16_t n63 = vdupq_n_u8(63);
+
+ __asm__ (
+
+ // Load 48 bytes and deinterleave. The bytes are loaded to
+ // hard-coded registers v12, v13 and v14, to ensure that they
+ // are contiguous. Increment the source pointer.
+ "ld3 {v12.16b, v13.16b, v14.16b}, [%[src]], #48 \n\t"
+
+ // Reshuffle the bytes using temporaries.
+ "ushr %[t0].16b, v12.16b, #2 \n\t"
+ "ushr %[t1].16b, v13.16b, #4 \n\t"
+ "ushr %[t2].16b, v14.16b, #6 \n\t"
+ "sli %[t1].16b, v12.16b, #4 \n\t"
+ "sli %[t2].16b, v13.16b, #2 \n\t"
+ "and %[t1].16b, %[t1].16b, %[n63].16b \n\t"
+ "and %[t2].16b, %[t2].16b, %[n63].16b \n\t"
+ "and %[t3].16b, v14.16b, %[n63].16b \n\t"
+
+ // Translate the values to the Base64 alphabet.
+ "tbl v12.16b, {%[l0].16b, %[l1].16b, %[l2].16b, %[l3].16b}, %[t0].16b \n\t"
+ "tbl v13.16b, {%[l0].16b, %[l1].16b, %[l2].16b, %[l3].16b}, %[t1].16b \n\t"
+ "tbl v14.16b, {%[l0].16b, %[l1].16b, %[l2].16b, %[l3].16b}, %[t2].16b \n\t"
+ "tbl v15.16b, {%[l0].16b, %[l1].16b, %[l2].16b, %[l3].16b}, %[t3].16b \n\t"
+
+ // Store 64 bytes and interleave. Increment the dest pointer.
+ "st4 {v12.16b, v13.16b, v14.16b, v15.16b}, [%[dst]], #64 \n\t"
+
+ // Outputs (modified).
+ : [src] "+r" (*s),
+ [dst] "+r" (*o),
+ [t0] "=&w" (tmp0),
+ [t1] "=&w" (tmp1),
+ [t2] "=&w" (tmp2),
+ [t3] "=&w" (tmp3)
+
+ // Inputs (not modified).
+ : [n63] "w" (n63),
+ [l0] "w" (tbl_enc.val[0]),
+ [l1] "w" (tbl_enc.val[1]),
+ [l2] "w" (tbl_enc.val[2]),
+ [l3] "w" (tbl_enc.val[3])
+
+ // Clobbers.
+ : "v12", "v13", "v14", "v15"
+ );
+}
+#endif
+
+static inline void
+enc_loop_neon64_inner (const uint8_t **s, uint8_t **o, const uint8x16x4_t tbl_enc)
+{
+#ifdef BASE64_NEON64_USE_ASM
+ enc_loop_neon64_inner_asm(s, o, tbl_enc);
+#else
+ // Load 48 bytes and deinterleave:
+ uint8x16x3_t src = vld3q_u8(*s);
+
+ // Divide bits of three input bytes over four output bytes:
+ uint8x16x4_t out = enc_reshuffle(src);
+
+ // The bits have now been shifted to the right locations;
+ // translate their values 0..63 to the Base64 alphabet.
+ // Use a 64-byte table lookup:
+ out.val[0] = vqtbl4q_u8(tbl_enc, out.val[0]);
+ out.val[1] = vqtbl4q_u8(tbl_enc, out.val[1]);
+ out.val[2] = vqtbl4q_u8(tbl_enc, out.val[2]);
+ out.val[3] = vqtbl4q_u8(tbl_enc, out.val[3]);
+
+ // Interleave and store output:
+ vst4q_u8(*o, out);
+
+ *s += 48;
+ *o += 64;
+#endif
+}
+
+static inline void
+enc_loop_neon64 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ size_t rounds = *slen / 48;
+
+ *slen -= rounds * 48; // 48 bytes consumed per round
+ *olen += rounds * 64; // 64 bytes produced per round
+
+ // Load the encoding table:
+ const uint8x16x4_t tbl_enc = load_64byte_table(base64_table_enc_6bit);
+
+ while (rounds > 0) {
+ if (rounds >= 8) {
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ rounds -= 8;
+ continue;
+ }
+ if (rounds >= 4) {
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ rounds -= 4;
+ continue;
+ }
+ if (rounds >= 2) {
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ rounds -= 2;
+ continue;
+ }
+ enc_loop_neon64_inner(s, o, tbl_enc);
+ break;
+ }
+}
diff --git a/src/third-party/base64/lib/arch/neon64/enc_reshuffle.c b/src/third-party/base64/lib/arch/neon64/enc_reshuffle.c
new file mode 100644
index 0000000..ea543e0
--- /dev/null
+++ b/src/third-party/base64/lib/arch/neon64/enc_reshuffle.c
@@ -0,0 +1,31 @@
+static inline uint8x16x4_t
+enc_reshuffle (const uint8x16x3_t in)
+{
+ uint8x16x4_t out;
+
+ // Input:
+ // in[0] = a7 a6 a5 a4 a3 a2 a1 a0
+ // in[1] = b7 b6 b5 b4 b3 b2 b1 b0
+ // in[2] = c7 c6 c5 c4 c3 c2 c1 c0
+
+ // Output:
+ // out[0] = 00 00 a7 a6 a5 a4 a3 a2
+ // out[1] = 00 00 a1 a0 b7 b6 b5 b4
+ // out[2] = 00 00 b3 b2 b1 b0 c7 c6
+ // out[3] = 00 00 c5 c4 c3 c2 c1 c0
+
+ // Move the input bits to where they need to be in the outputs. Except
+ // for the first output, the high two bits are not cleared.
+ out.val[0] = vshrq_n_u8(in.val[0], 2);
+ out.val[1] = vshrq_n_u8(in.val[1], 4);
+ out.val[2] = vshrq_n_u8(in.val[2], 6);
+ out.val[1] = vsliq_n_u8(out.val[1], in.val[0], 4);
+ out.val[2] = vsliq_n_u8(out.val[2], in.val[1], 2);
+
+ // Clear the high two bits in the second, third and fourth output.
+ out.val[1] = vandq_u8(out.val[1], vdupq_n_u8(0x3F));
+ out.val[2] = vandq_u8(out.val[2], vdupq_n_u8(0x3F));
+ out.val[3] = vandq_u8(in.val[2], vdupq_n_u8(0x3F));
+
+ return out;
+}
diff --git a/src/third-party/base64/lib/arch/sse41/codec.c b/src/third-party/base64/lib/arch/sse41/codec.c
new file mode 100644
index 0000000..00645fe
--- /dev/null
+++ b/src/third-party/base64/lib/arch/sse41/codec.c
@@ -0,0 +1,42 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#include "../../../include/libbase64.h"
+#include "../../tables/tables.h"
+#include "../../codecs.h"
+#include "config.h"
+#include "../../env.h"
+
+#if HAVE_SSE41
+#include <smmintrin.h>
+
+#include "../ssse3/dec_reshuffle.c"
+#include "../ssse3/dec_loop.c"
+#include "../ssse3/enc_translate.c"
+#include "../ssse3/enc_reshuffle.c"
+#include "../ssse3/enc_loop.c"
+
+#endif // HAVE_SSE41
+
+BASE64_ENC_FUNCTION(sse41)
+{
+#if HAVE_SSE41
+ #include "../generic/enc_head.c"
+ enc_loop_ssse3(&s, &slen, &o, &olen);
+ #include "../generic/enc_tail.c"
+#else
+ BASE64_ENC_STUB
+#endif
+}
+
+BASE64_DEC_FUNCTION(sse41)
+{
+#if HAVE_SSE41
+ #include "../generic/dec_head.c"
+ dec_loop_ssse3(&s, &slen, &o, &olen);
+ #include "../generic/dec_tail.c"
+#else
+ BASE64_DEC_STUB
+#endif
+}
diff --git a/src/third-party/base64/lib/arch/sse42/codec.c b/src/third-party/base64/lib/arch/sse42/codec.c
new file mode 100644
index 0000000..cf5d97c
--- /dev/null
+++ b/src/third-party/base64/lib/arch/sse42/codec.c
@@ -0,0 +1,42 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#include "../../../include/libbase64.h"
+#include "../../tables/tables.h"
+#include "../../codecs.h"
+#include "config.h"
+#include "../../env.h"
+
+#if HAVE_SSE42
+#include <nmmintrin.h>
+
+#include "../ssse3/dec_reshuffle.c"
+#include "../ssse3/dec_loop.c"
+#include "../ssse3/enc_translate.c"
+#include "../ssse3/enc_reshuffle.c"
+#include "../ssse3/enc_loop.c"
+
+#endif // HAVE_SSE42
+
+BASE64_ENC_FUNCTION(sse42)
+{
+#if HAVE_SSE42
+ #include "../generic/enc_head.c"
+ enc_loop_ssse3(&s, &slen, &o, &olen);
+ #include "../generic/enc_tail.c"
+#else
+ BASE64_ENC_STUB
+#endif
+}
+
+BASE64_DEC_FUNCTION(sse42)
+{
+#if HAVE_SSE42
+ #include "../generic/dec_head.c"
+ dec_loop_ssse3(&s, &slen, &o, &olen);
+ #include "../generic/dec_tail.c"
+#else
+ BASE64_DEC_STUB
+#endif
+}
diff --git a/src/third-party/base64/lib/arch/ssse3/codec.c b/src/third-party/base64/lib/arch/ssse3/codec.c
new file mode 100644
index 0000000..ad14a45
--- /dev/null
+++ b/src/third-party/base64/lib/arch/ssse3/codec.c
@@ -0,0 +1,42 @@
+#include <stdint.h>
+#include <stddef.h>
+#include <stdlib.h>
+
+#include "../../../include/libbase64.h"
+#include "../../tables/tables.h"
+#include "../../codecs.h"
+#include "config.h"
+#include "../../env.h"
+
+#if HAVE_SSSE3
+#include <tmmintrin.h>
+
+#include "dec_reshuffle.c"
+#include "dec_loop.c"
+#include "enc_reshuffle.c"
+#include "enc_translate.c"
+#include "enc_loop.c"
+
+#endif // HAVE_SSSE3
+
+BASE64_ENC_FUNCTION(ssse3)
+{
+#if HAVE_SSSE3
+ #include "../generic/enc_head.c"
+ enc_loop_ssse3(&s, &slen, &o, &olen);
+ #include "../generic/enc_tail.c"
+#else
+ BASE64_ENC_STUB
+#endif
+}
+
+BASE64_DEC_FUNCTION(ssse3)
+{
+#if HAVE_SSSE3
+ #include "../generic/dec_head.c"
+ dec_loop_ssse3(&s, &slen, &o, &olen);
+ #include "../generic/dec_tail.c"
+#else
+ BASE64_DEC_STUB
+#endif
+}
diff --git a/src/third-party/base64/lib/arch/ssse3/dec_loop.c b/src/third-party/base64/lib/arch/ssse3/dec_loop.c
new file mode 100644
index 0000000..9da71ab
--- /dev/null
+++ b/src/third-party/base64/lib/arch/ssse3/dec_loop.c
@@ -0,0 +1,173 @@
+// The input consists of six character sets in the Base64 alphabet, which we
+// need to map back to the 6-bit values they represent. There are three ranges,
+// two singles, and then there's the rest.
+//
+// # From To Add Characters
+// 1 [43] [62] +19 +
+// 2 [47] [63] +16 /
+// 3 [48..57] [52..61] +4 0..9
+// 4 [65..90] [0..25] -65 A..Z
+// 5 [97..122] [26..51] -71 a..z
+// (6) Everything else => invalid input
+//
+// We will use lookup tables for character validation and offset computation.
+// Remember that 0x2X and 0x0X are the same index for _mm_shuffle_epi8, this
+// allows to mask with 0x2F instead of 0x0F and thus save one constant
+// declaration (register and/or memory access).
+//
+// For offsets:
+// Perfect hash for lut = ((src >> 4) & 0x2F) + ((src == 0x2F) ? 0xFF : 0x00)
+// 0000 = garbage
+// 0001 = /
+// 0010 = +
+// 0011 = 0-9
+// 0100 = A-Z
+// 0101 = A-Z
+// 0110 = a-z
+// 0111 = a-z
+// 1000 >= garbage
+//
+// For validation, here's the table.
+// A character is valid if and only if the AND of the 2 lookups equals 0:
+//
+// hi \ lo 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111
+// LUT 0x15 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x11 0x13 0x1A 0x1B 0x1B 0x1B 0x1A
+//
+// 0000 0x10 char NUL SOH STX ETX EOT ENQ ACK BEL BS HT LF VT FF CR SO SI
+// andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+//
+// 0001 0x10 char DLE DC1 DC2 DC3 DC4 NAK SYN ETB CAN EM SUB ESC FS GS RS US
+// andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+//
+// 0010 0x01 char ! " # $ % & ' ( ) * + , - . /
+// andlut 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x01 0x00 0x01 0x01 0x01 0x00
+//
+// 0011 0x02 char 0 1 2 3 4 5 6 7 8 9 : ; < = > ?
+// andlut 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x02 0x02 0x02 0x02 0x02 0x02
+//
+// 0100 0x04 char @ A B C D E F G H I J K L M N O
+// andlut 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+//
+// 0101 0x08 char P Q R S T U V W X Y Z [ \ ] ^ _
+// andlut 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x08 0x08 0x08 0x08 0x08
+//
+// 0110 0x04 char ` a b c d e f g h i j k l m n o
+// andlut 0x04 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
+// 0111 0x08 char p q r s t u v w x y z { | } ~
+// andlut 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x08 0x08 0x08 0x08 0x08
+//
+// 1000 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+// 1001 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+// 1010 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+// 1011 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+// 1100 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+// 1101 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+// 1110 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+// 1111 0x10 andlut 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10 0x10
+
+static inline int
+dec_loop_ssse3_inner (const uint8_t **s, uint8_t **o, size_t *rounds)
+{
+ const __m128i lut_lo = _mm_setr_epi8(
+ 0x15, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
+ 0x11, 0x11, 0x13, 0x1A, 0x1B, 0x1B, 0x1B, 0x1A);
+
+ const __m128i lut_hi = _mm_setr_epi8(
+ 0x10, 0x10, 0x01, 0x02, 0x04, 0x08, 0x04, 0x08,
+ 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10);
+
+ const __m128i lut_roll = _mm_setr_epi8(
+ 0, 16, 19, 4, -65, -65, -71, -71,
+ 0, 0, 0, 0, 0, 0, 0, 0);
+
+ const __m128i mask_2F = _mm_set1_epi8(0x2F);
+
+ // Load input:
+ __m128i str = _mm_loadu_si128((__m128i *) *s);
+
+ // Table lookups:
+ const __m128i hi_nibbles = _mm_and_si128(_mm_srli_epi32(str, 4), mask_2F);
+ const __m128i lo_nibbles = _mm_and_si128(str, mask_2F);
+ const __m128i hi = _mm_shuffle_epi8(lut_hi, hi_nibbles);
+ const __m128i lo = _mm_shuffle_epi8(lut_lo, lo_nibbles);
+
+ // Check for invalid input: if any "and" values from lo and hi are not
+ // zero, fall back on bytewise code to do error checking and reporting:
+ if (_mm_movemask_epi8(_mm_cmpgt_epi8(_mm_and_si128(lo, hi), _mm_setzero_si128())) != 0) {
+ return 0;
+ }
+
+ const __m128i eq_2F = _mm_cmpeq_epi8(str, mask_2F);
+ const __m128i roll = _mm_shuffle_epi8(lut_roll, _mm_add_epi8(eq_2F, hi_nibbles));
+
+ // Now simply add the delta values to the input:
+ str = _mm_add_epi8(str, roll);
+
+ // Reshuffle the input to packed 12-byte output format:
+ str = dec_reshuffle(str);
+
+ // Store the output:
+ _mm_storeu_si128((__m128i *) *o, str);
+
+ *s += 16;
+ *o += 12;
+ *rounds -= 1;
+
+ return 1;
+}
+
+static inline void
+dec_loop_ssse3 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ if (*slen < 24) {
+ return;
+ }
+
+ // Process blocks of 16 bytes per round. Because 4 extra zero bytes are
+ // written after the output, ensure that there will be at least 8 bytes
+ // of input data left to cover the gap. (6 data bytes and up to two
+ // end-of-string markers.)
+ size_t rounds = (*slen - 8) / 16;
+
+ *slen -= rounds * 16; // 16 bytes consumed per round
+ *olen += rounds * 12; // 12 bytes produced per round
+
+ do {
+ if (rounds >= 8) {
+ if (dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds)) {
+ continue;
+ }
+ break;
+ }
+ if (rounds >= 4) {
+ if (dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds)) {
+ continue;
+ }
+ break;
+ }
+ if (rounds >= 2) {
+ if (dec_loop_ssse3_inner(s, o, &rounds) &&
+ dec_loop_ssse3_inner(s, o, &rounds)) {
+ continue;
+ }
+ break;
+ }
+ dec_loop_ssse3_inner(s, o, &rounds);
+ break;
+
+ } while (rounds > 0);
+
+ // Adjust for any rounds that were skipped:
+ *slen += rounds * 16;
+ *olen -= rounds * 12;
+}
diff --git a/src/third-party/base64/lib/arch/ssse3/dec_reshuffle.c b/src/third-party/base64/lib/arch/ssse3/dec_reshuffle.c
new file mode 100644
index 0000000..fdf587f
--- /dev/null
+++ b/src/third-party/base64/lib/arch/ssse3/dec_reshuffle.c
@@ -0,0 +1,33 @@
+static inline __m128i
+dec_reshuffle (const __m128i in)
+{
+ // in, bits, upper case are most significant bits, lower case are least significant bits
+ // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ
+ // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG
+ // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD
+ // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA
+
+ const __m128i merge_ab_and_bc = _mm_maddubs_epi16(in, _mm_set1_epi32(0x01400140));
+ // 0000kkkk LLllllll 0000JJJJ JJjjKKKK
+ // 0000hhhh IIiiiiii 0000GGGG GGggHHHH
+ // 0000eeee FFffffff 0000DDDD DDddEEEE
+ // 0000bbbb CCcccccc 0000AAAA AAaaBBBB
+
+ const __m128i out = _mm_madd_epi16(merge_ab_and_bc, _mm_set1_epi32(0x00011000));
+ // 00000000 JJJJJJjj KKKKkkkk LLllllll
+ // 00000000 GGGGGGgg HHHHhhhh IIiiiiii
+ // 00000000 DDDDDDdd EEEEeeee FFffffff
+ // 00000000 AAAAAAaa BBBBbbbb CCcccccc
+
+ // Pack bytes together:
+ return _mm_shuffle_epi8(out, _mm_setr_epi8(
+ 2, 1, 0,
+ 6, 5, 4,
+ 10, 9, 8,
+ 14, 13, 12,
+ -1, -1, -1, -1));
+ // 00000000 00000000 00000000 00000000
+ // LLllllll KKKKkkkk JJJJJJjj IIiiiiii
+ // HHHHhhhh GGGGGGgg FFffffff EEEEeeee
+ // DDDDDDdd CCcccccc BBBBbbbb AAAAAAaa
+}
diff --git a/src/third-party/base64/lib/arch/ssse3/enc_loop.c b/src/third-party/base64/lib/arch/ssse3/enc_loop.c
new file mode 100644
index 0000000..6de652e
--- /dev/null
+++ b/src/third-party/base64/lib/arch/ssse3/enc_loop.c
@@ -0,0 +1,67 @@
+static inline void
+enc_loop_ssse3_inner (const uint8_t **s, uint8_t **o)
+{
+ // Load input:
+ __m128i str = _mm_loadu_si128((__m128i *) *s);
+
+ // Reshuffle:
+ str = enc_reshuffle(str);
+
+ // Translate reshuffled bytes to the Base64 alphabet:
+ str = enc_translate(str);
+
+ // Store:
+ _mm_storeu_si128((__m128i *) *o, str);
+
+ *s += 12;
+ *o += 16;
+}
+
+static inline void
+enc_loop_ssse3 (const uint8_t **s, size_t *slen, uint8_t **o, size_t *olen)
+{
+ if (*slen < 16) {
+ return;
+ }
+
+ // Process blocks of 12 bytes at a time. Because blocks are loaded 16
+ // bytes at a time, ensure that there will be at least 4 remaining
+ // bytes after the last round, so that the final read will not pass
+ // beyond the bounds of the input buffer:
+ size_t rounds = (*slen - 4) / 12;
+
+ *slen -= rounds * 12; // 12 bytes consumed per round
+ *olen += rounds * 16; // 16 bytes produced per round
+
+ do {
+ if (rounds >= 8) {
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ rounds -= 8;
+ continue;
+ }
+ if (rounds >= 4) {
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ rounds -= 4;
+ continue;
+ }
+ if (rounds >= 2) {
+ enc_loop_ssse3_inner(s, o);
+ enc_loop_ssse3_inner(s, o);
+ rounds -= 2;
+ continue;
+ }
+ enc_loop_ssse3_inner(s, o);
+ break;
+
+ } while (rounds > 0);
+}
diff --git a/src/third-party/base64/lib/arch/ssse3/enc_reshuffle.c b/src/third-party/base64/lib/arch/ssse3/enc_reshuffle.c
new file mode 100644
index 0000000..b738591
--- /dev/null
+++ b/src/third-party/base64/lib/arch/ssse3/enc_reshuffle.c
@@ -0,0 +1,48 @@
+static inline __m128i
+enc_reshuffle (__m128i in)
+{
+ // Input, bytes MSB to LSB:
+ // 0 0 0 0 l k j i h g f e d c b a
+
+ in = _mm_shuffle_epi8(in, _mm_set_epi8(
+ 10, 11, 9, 10,
+ 7, 8, 6, 7,
+ 4, 5, 3, 4,
+ 1, 2, 0, 1));
+ // in, bytes MSB to LSB:
+ // k l j k
+ // h i g h
+ // e f d e
+ // b c a b
+
+ const __m128i t0 = _mm_and_si128(in, _mm_set1_epi32(0x0FC0FC00));
+ // bits, upper case are most significant bits, lower case are least significant bits
+ // 0000kkkk LL000000 JJJJJJ00 00000000
+ // 0000hhhh II000000 GGGGGG00 00000000
+ // 0000eeee FF000000 DDDDDD00 00000000
+ // 0000bbbb CC000000 AAAAAA00 00000000
+
+ const __m128i t1 = _mm_mulhi_epu16(t0, _mm_set1_epi32(0x04000040));
+ // 00000000 00kkkkLL 00000000 00JJJJJJ
+ // 00000000 00hhhhII 00000000 00GGGGGG
+ // 00000000 00eeeeFF 00000000 00DDDDDD
+ // 00000000 00bbbbCC 00000000 00AAAAAA
+
+ const __m128i t2 = _mm_and_si128(in, _mm_set1_epi32(0x003F03F0));
+ // 00000000 00llllll 000000jj KKKK0000
+ // 00000000 00iiiiii 000000gg HHHH0000
+ // 00000000 00ffffff 000000dd EEEE0000
+ // 00000000 00cccccc 000000aa BBBB0000
+
+ const __m128i t3 = _mm_mullo_epi16(t2, _mm_set1_epi32(0x01000010));
+ // 00llllll 00000000 00jjKKKK 00000000
+ // 00iiiiii 00000000 00ggHHHH 00000000
+ // 00ffffff 00000000 00ddEEEE 00000000
+ // 00cccccc 00000000 00aaBBBB 00000000
+
+ return _mm_or_si128(t1, t3);
+ // 00llllll 00kkkkLL 00jjKKKK 00JJJJJJ
+ // 00iiiiii 00hhhhII 00ggHHHH 00GGGGGG
+ // 00ffffff 00eeeeFF 00ddEEEE 00DDDDDD
+ // 00cccccc 00bbbbCC 00aaBBBB 00AAAAAA
+}
diff --git a/src/third-party/base64/lib/arch/ssse3/enc_translate.c b/src/third-party/base64/lib/arch/ssse3/enc_translate.c
new file mode 100644
index 0000000..04f288f
--- /dev/null
+++ b/src/third-party/base64/lib/arch/ssse3/enc_translate.c
@@ -0,0 +1,33 @@
+static inline __m128i
+enc_translate (const __m128i in)
+{
+ // A lookup table containing the absolute offsets for all ranges:
+ const __m128i lut = _mm_setr_epi8(
+ 65, 71, -4, -4,
+ -4, -4, -4, -4,
+ -4, -4, -4, -4,
+ -19, -16, 0, 0
+ );
+
+ // Translate values 0..63 to the Base64 alphabet. There are five sets:
+ // # From To Abs Index Characters
+ // 0 [0..25] [65..90] +65 0 ABCDEFGHIJKLMNOPQRSTUVWXYZ
+ // 1 [26..51] [97..122] +71 1 abcdefghijklmnopqrstuvwxyz
+ // 2 [52..61] [48..57] -4 [2..11] 0123456789
+ // 3 [62] [43] -19 12 +
+ // 4 [63] [47] -16 13 /
+
+ // Create LUT indices from the input. The index for range #0 is right,
+ // others are 1 less than expected:
+ __m128i indices = _mm_subs_epu8(in, _mm_set1_epi8(51));
+
+ // mask is 0xFF (-1) for range #[1..4] and 0x00 for range #0:
+ __m128i mask = _mm_cmpgt_epi8(in, _mm_set1_epi8(25));
+
+ // Subtract -1, so add 1 to indices for range #[1..4]. All indices are
+ // now correct:
+ indices = _mm_sub_epi8(indices, mask);
+
+ // Add offsets to input values:
+ return _mm_add_epi8(in, _mm_shuffle_epi8(lut, indices));
+}
diff --git a/src/third-party/base64/lib/codec_choose.c b/src/third-party/base64/lib/codec_choose.c
new file mode 100644
index 0000000..6a07d6a
--- /dev/null
+++ b/src/third-party/base64/lib/codec_choose.c
@@ -0,0 +1,281 @@
+#include <stdbool.h>
+#include <stdint.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include "../include/libbase64.h"
+#include "codecs.h"
+#include "config.h"
+#include "env.h"
+
+#if (__x86_64__ || __i386__ || _M_X86 || _M_X64)
+ #define BASE64_X86
+ #if (HAVE_SSSE3 || HAVE_SSE41 || HAVE_SSE42 || HAVE_AVX || HAVE_AVX2)
+ #define BASE64_X86_SIMD
+ #endif
+#endif
+
+#ifdef BASE64_X86
+#ifdef _MSC_VER
+ #include <intrin.h>
+ #define __cpuid_count(__level, __count, __eax, __ebx, __ecx, __edx) \
+ { \
+ int info[4]; \
+ __cpuidex(info, __level, __count); \
+ __eax = info[0]; \
+ __ebx = info[1]; \
+ __ecx = info[2]; \
+ __edx = info[3]; \
+ }
+ #define __cpuid(__level, __eax, __ebx, __ecx, __edx) \
+ __cpuid_count(__level, 0, __eax, __ebx, __ecx, __edx)
+#else
+ #include <cpuid.h>
+ #if HAVE_AVX2 || HAVE_AVX
+ #if ((__GNUC__ > 4 || __GNUC__ == 4 && __GNUC_MINOR__ >= 2) || (__clang_major__ >= 3))
+ static inline uint64_t _xgetbv (uint32_t index)
+ {
+ uint32_t eax, edx;
+ __asm__ __volatile__("xgetbv" : "=a"(eax), "=d"(edx) : "c"(index));
+ return ((uint64_t)edx << 32) | eax;
+ }
+ #else
+ #error "Platform not supported"
+ #endif
+ #endif
+#endif
+
+#ifndef bit_AVX2
+#define bit_AVX2 (1 << 5)
+#endif
+#ifndef bit_SSSE3
+#define bit_SSSE3 (1 << 9)
+#endif
+#ifndef bit_SSE41
+#define bit_SSE41 (1 << 19)
+#endif
+#ifndef bit_SSE42
+#define bit_SSE42 (1 << 20)
+#endif
+#ifndef bit_AVX
+#define bit_AVX (1 << 28)
+#endif
+
+#define bit_XSAVE_XRSTORE (1 << 27)
+
+#ifndef _XCR_XFEATURE_ENABLED_MASK
+#define _XCR_XFEATURE_ENABLED_MASK 0
+#endif
+
+#define _XCR_XMM_AND_YMM_STATE_ENABLED_BY_OS 0x6
+#endif
+
+// Function declarations:
+#define BASE64_CODEC_FUNCS(arch) \
+ BASE64_ENC_FUNCTION(arch); \
+ BASE64_DEC_FUNCTION(arch); \
+
+BASE64_CODEC_FUNCS(avx2)
+BASE64_CODEC_FUNCS(neon32)
+BASE64_CODEC_FUNCS(neon64)
+BASE64_CODEC_FUNCS(plain)
+BASE64_CODEC_FUNCS(ssse3)
+BASE64_CODEC_FUNCS(sse41)
+BASE64_CODEC_FUNCS(sse42)
+BASE64_CODEC_FUNCS(avx)
+
+static bool
+codec_choose_forced (struct codec *codec, int flags)
+{
+ // If the user wants to use a certain codec,
+ // always allow it, even if the codec is a no-op.
+ // For testing purposes.
+
+ if (!(flags & 0xFF)) {
+ return false;
+ }
+ if (flags & BASE64_FORCE_AVX2) {
+ codec->enc = base64_stream_encode_avx2;
+ codec->dec = base64_stream_decode_avx2;
+ return true;
+ }
+ if (flags & BASE64_FORCE_NEON32) {
+ codec->enc = base64_stream_encode_neon32;
+ codec->dec = base64_stream_decode_neon32;
+ return true;
+ }
+ if (flags & BASE64_FORCE_NEON64) {
+ codec->enc = base64_stream_encode_neon64;
+ codec->dec = base64_stream_decode_neon64;
+ return true;
+ }
+ if (flags & BASE64_FORCE_PLAIN) {
+ codec->enc = base64_stream_encode_plain;
+ codec->dec = base64_stream_decode_plain;
+ return true;
+ }
+ if (flags & BASE64_FORCE_SSSE3) {
+ codec->enc = base64_stream_encode_ssse3;
+ codec->dec = base64_stream_decode_ssse3;
+ return true;
+ }
+ if (flags & BASE64_FORCE_SSE41) {
+ codec->enc = base64_stream_encode_sse41;
+ codec->dec = base64_stream_decode_sse41;
+ return true;
+ }
+ if (flags & BASE64_FORCE_SSE42) {
+ codec->enc = base64_stream_encode_sse42;
+ codec->dec = base64_stream_decode_sse42;
+ return true;
+ }
+ if (flags & BASE64_FORCE_AVX) {
+ codec->enc = base64_stream_encode_avx;
+ codec->dec = base64_stream_decode_avx;
+ return true;
+ }
+ return false;
+}
+
+static bool
+codec_choose_arm (struct codec *codec)
+{
+#if (defined(__ARM_NEON__) || defined(__ARM_NEON)) && ((defined(__aarch64__) && HAVE_NEON64) || HAVE_NEON32)
+
+ // Unfortunately there is no portable way to check for NEON
+ // support at runtime from userland in the same way that x86
+ // has cpuid, so just stick to the compile-time configuration:
+
+ #if defined(__aarch64__) && HAVE_NEON64
+ codec->enc = base64_stream_encode_neon64;
+ codec->dec = base64_stream_decode_neon64;
+ #else
+ codec->enc = base64_stream_encode_neon32;
+ codec->dec = base64_stream_decode_neon32;
+ #endif
+
+ return true;
+
+#else
+ (void)codec;
+ return false;
+#endif
+}
+
+static bool
+codec_choose_x86 (struct codec *codec)
+{
+#ifdef BASE64_X86_SIMD
+
+ unsigned int eax, ebx = 0, ecx = 0, edx;
+ unsigned int max_level;
+
+ #ifdef _MSC_VER
+ int info[4];
+ __cpuidex(info, 0, 0);
+ max_level = info[0];
+ #else
+ max_level = __get_cpuid_max(0, NULL);
+ #endif
+
+ #if HAVE_AVX2 || HAVE_AVX
+ // Check for AVX/AVX2 support:
+ // Checking for AVX requires 3 things:
+ // 1) CPUID indicates that the OS uses XSAVE and XRSTORE instructions
+ // (allowing saving YMM registers on context switch)
+ // 2) CPUID indicates support for AVX
+ // 3) XGETBV indicates the AVX registers will be saved and restored on
+ // context switch
+ //
+ // Note that XGETBV is only available on 686 or later CPUs, so the
+ // instruction needs to be conditionally run.
+ if (max_level >= 1) {
+ __cpuid_count(1, 0, eax, ebx, ecx, edx);
+ if (ecx & bit_XSAVE_XRSTORE) {
+ uint64_t xcr_mask;
+ xcr_mask = _xgetbv(_XCR_XFEATURE_ENABLED_MASK);
+ if (xcr_mask & _XCR_XMM_AND_YMM_STATE_ENABLED_BY_OS) {
+ #if HAVE_AVX2
+ if (max_level >= 7) {
+ __cpuid_count(7, 0, eax, ebx, ecx, edx);
+ if (ebx & bit_AVX2) {
+ codec->enc = base64_stream_encode_avx2;
+ codec->dec = base64_stream_decode_avx2;
+ return true;
+ }
+ }
+ #endif
+ #if HAVE_AVX
+ __cpuid_count(1, 0, eax, ebx, ecx, edx);
+ if (ecx & bit_AVX) {
+ codec->enc = base64_stream_encode_avx;
+ codec->dec = base64_stream_decode_avx;
+ return true;
+ }
+ #endif
+ }
+ }
+ }
+ #endif
+
+ #if HAVE_SSE42
+ // Check for SSE42 support:
+ if (max_level >= 1) {
+ __cpuid(1, eax, ebx, ecx, edx);
+ if (ecx & bit_SSE42) {
+ codec->enc = base64_stream_encode_sse42;
+ codec->dec = base64_stream_decode_sse42;
+ return true;
+ }
+ }
+ #endif
+
+ #if HAVE_SSE41
+ // Check for SSE41 support:
+ if (max_level >= 1) {
+ __cpuid(1, eax, ebx, ecx, edx);
+ if (ecx & bit_SSE41) {
+ codec->enc = base64_stream_encode_sse41;
+ codec->dec = base64_stream_decode_sse41;
+ return true;
+ }
+ }
+ #endif
+
+ #if HAVE_SSSE3
+ // Check for SSSE3 support:
+ if (max_level >= 1) {
+ __cpuid(1, eax, ebx, ecx, edx);
+ if (ecx & bit_SSSE3) {
+ codec->enc = base64_stream_encode_ssse3;
+ codec->dec = base64_stream_decode_ssse3;
+ return true;
+ }
+ }
+ #endif
+
+#else
+ (void)codec;
+#endif
+
+ return false;
+}
+
+void
+codec_choose (struct codec *codec, int flags)
+{
+ // User forced a codec:
+ if (codec_choose_forced(codec, flags)) {
+ return;
+ }
+
+ // Runtime feature detection:
+ if (codec_choose_arm(codec)) {
+ return;
+ }
+ if (codec_choose_x86(codec)) {
+ return;
+ }
+ codec->enc = base64_stream_encode_plain;
+ codec->dec = base64_stream_decode_plain;
+}
diff --git a/src/third-party/base64/lib/codecs.h b/src/third-party/base64/lib/codecs.h
new file mode 100644
index 0000000..441fd60
--- /dev/null
+++ b/src/third-party/base64/lib/codecs.h
@@ -0,0 +1,65 @@
+#include <stdint.h>
+#include <stddef.h>
+
+#include "../include/libbase64.h"
+#include "config.h"
+
+// Function parameters for encoding functions:
+#define BASE64_ENC_PARAMS \
+ ( struct base64_state *state \
+ , const char *src \
+ , size_t srclen \
+ , char *out \
+ , size_t *outlen \
+ )
+
+// Function parameters for decoding functions:
+#define BASE64_DEC_PARAMS \
+ ( struct base64_state *state \
+ , const char *src \
+ , size_t srclen \
+ , char *out \
+ , size_t *outlen \
+ )
+
+// Function signature for encoding functions:
+#define BASE64_ENC_FUNCTION(arch) \
+ void \
+ base64_stream_encode_ ## arch \
+ BASE64_ENC_PARAMS
+
+// Function signature for decoding functions:
+#define BASE64_DEC_FUNCTION(arch) \
+ int \
+ base64_stream_decode_ ## arch \
+ BASE64_DEC_PARAMS
+
+// Cast away unused variable, silence compiler:
+#define UNUSED(x) ((void)(x))
+
+// Stub function when encoder arch unsupported:
+#define BASE64_ENC_STUB \
+ UNUSED(state); \
+ UNUSED(src); \
+ UNUSED(srclen); \
+ UNUSED(out); \
+ \
+ *outlen = 0;
+
+// Stub function when decoder arch unsupported:
+#define BASE64_DEC_STUB \
+ UNUSED(state); \
+ UNUSED(src); \
+ UNUSED(srclen); \
+ UNUSED(out); \
+ UNUSED(outlen); \
+ \
+ return -1;
+
+struct codec
+{
+ void (* enc) BASE64_ENC_PARAMS;
+ int (* dec) BASE64_DEC_PARAMS;
+};
+
+extern void codec_choose (struct codec *, int flags);
diff --git a/src/third-party/base64/lib/config.h b/src/third-party/base64/lib/config.h
new file mode 100644
index 0000000..0de5358
--- /dev/null
+++ b/src/third-party/base64/lib/config.h
@@ -0,0 +1,7 @@
+#define HAVE_AVX2 0
+#define HAVE_NEON32 0
+#define HAVE_NEON64 0
+#define HAVE_SSSE3 0
+#define HAVE_SSE41 0
+#define HAVE_SSE42 0
+#define HAVE_AVX 0
diff --git a/src/third-party/base64/lib/env.h b/src/third-party/base64/lib/env.h
new file mode 100644
index 0000000..d5c2fdb
--- /dev/null
+++ b/src/third-party/base64/lib/env.h
@@ -0,0 +1,74 @@
+#ifndef BASE64_ENV_H
+#define BASE64_ENV_H
+
+// This header file contains macro definitions that describe certain aspects of
+// the compile-time environment. Compatibility and portability macros go here.
+
+// Define machine endianness. This is for GCC:
+#if (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
+# define BASE64_LITTLE_ENDIAN 1
+#else
+# define BASE64_LITTLE_ENDIAN 0
+#endif
+
+// This is for Clang:
+#ifdef __LITTLE_ENDIAN__
+# define BASE64_LITTLE_ENDIAN 1
+#endif
+
+#ifdef __BIG_ENDIAN__
+# define BASE64_LITTLE_ENDIAN 0
+#endif
+
+// MSVC++ needs intrin.h for _byteswap_uint64 (issue #68):
+#if BASE64_LITTLE_ENDIAN && defined(_MSC_VER)
+# include <intrin.h>
+#endif
+
+// Endian conversion functions:
+#if BASE64_LITTLE_ENDIAN
+# ifdef _MSC_VER
+// Microsoft Visual C++:
+# define BASE64_HTOBE32(x) _byteswap_ulong(x)
+# define BASE64_HTOBE64(x) _byteswap_uint64(x)
+# else
+// GCC and Clang:
+# define BASE64_HTOBE32(x) __builtin_bswap32(x)
+# define BASE64_HTOBE64(x) __builtin_bswap64(x)
+# endif
+#else
+// No conversion needed:
+# define BASE64_HTOBE32(x) (x)
+# define BASE64_HTOBE64(x) (x)
+#endif
+
+// Detect word size:
+#if defined (__x86_64__)
+// This also works for the x32 ABI, which has a 64-bit word size.
+# define BASE64_WORDSIZE 64
+#elif defined (_INTEGRAL_MAX_BITS)
+# define BASE64_WORDSIZE _INTEGRAL_MAX_BITS
+#elif defined (__WORDSIZE)
+# define BASE64_WORDSIZE __WORDSIZE
+#elif defined (__SIZE_WIDTH__)
+# define BASE64_WORDSIZE __SIZE_WIDTH__
+#else
+# error BASE64_WORDSIZE_NOT_DEFINED
+#endif
+
+// End-of-file definitions.
+// Almost end-of-file when waiting for the last '=' character:
+#define BASE64_AEOF 1
+// End-of-file when stream end has been reached or invalid input provided:
+#define BASE64_EOF 2
+
+// GCC 7 defaults to issuing a warning for fallthrough in switch statements,
+// unless the fallthrough cases are marked with an attribute. As we use
+// fallthrough deliberately, define an alias for the attribute:
+#if __GNUC__ >= 7
+# define BASE64_FALLTHROUGH __attribute__((fallthrough));
+#else
+# define BASE64_FALLTHROUGH
+#endif
+
+#endif // BASE64_ENV_H
diff --git a/src/third-party/base64/lib/lib.c b/src/third-party/base64/lib/lib.c
new file mode 100644
index 0000000..659b986
--- /dev/null
+++ b/src/third-party/base64/lib/lib.c
@@ -0,0 +1,175 @@
+#include <stdint.h>
+#include <stddef.h>
+#ifdef _OPENMP
+#include <omp.h>
+#endif
+
+#include "../include/libbase64.h"
+#include "tables/tables.h"
+#include "codecs.h"
+#include "env.h"
+
+// These static function pointers are initialized once when the library is
+// first used, and remain in use for the remaining lifetime of the program.
+// The idea being that CPU features don't change at runtime.
+static struct codec codec = { NULL, NULL };
+
+// Function declarations:
+#define BASE64_CODEC_FUNCS(arch) \
+ BASE64_ENC_FUNCTION(arch); \
+ BASE64_DEC_FUNCTION(arch); \
+
+BASE64_CODEC_FUNCS(plain)
+
+void
+base64_stream_encode_init (struct base64_state *state, int flags)
+{
+ // If any of the codec flags are set, redo choice:
+ if (codec.enc == NULL || flags & 0xFF) {
+ // codec_choose(&codec, flags);
+ codec.enc = base64_stream_encode_plain;
+ codec.dec = base64_stream_decode_plain;
+ }
+ state->eof = 0;
+ state->bytes = 0;
+ state->carry = 0;
+ state->flags = flags;
+}
+
+void
+base64_stream_encode
+ ( struct base64_state *state
+ , const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ )
+{
+ codec.enc(state, src, srclen, out, outlen);
+}
+
+void
+base64_stream_encode_final
+ ( struct base64_state *state
+ , char *out
+ , size_t *outlen
+ )
+{
+ uint8_t *o = (uint8_t *)out;
+
+ if (state->bytes == 1) {
+ *o++ = base64_table_enc_6bit[state->carry];
+ *o++ = '=';
+ *o++ = '=';
+ *outlen = 3;
+ return;
+ }
+ if (state->bytes == 2) {
+ *o++ = base64_table_enc_6bit[state->carry];
+ *o++ = '=';
+ *outlen = 2;
+ return;
+ }
+ *outlen = 0;
+}
+
+void
+base64_stream_decode_init (struct base64_state *state, int flags)
+{
+ // If any of the codec flags are set, redo choice:
+ if (codec.dec == NULL || flags & 0xFF) {
+ // codec_choose(&codec, flags);
+ codec.enc = base64_stream_encode_plain;
+ codec.dec = base64_stream_decode_plain;
+ }
+ state->eof = 0;
+ state->bytes = 0;
+ state->carry = 0;
+ state->flags = flags;
+}
+
+int
+base64_stream_decode
+ ( struct base64_state *state
+ , const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ )
+{
+ return codec.dec(state, src, srclen, out, outlen);
+}
+
+#ifdef _OPENMP
+
+ // Due to the overhead of initializing OpenMP and creating a team of
+ // threads, we require the data length to be larger than a threshold:
+ #define OMP_THRESHOLD 20000
+
+ // Conditionally include OpenMP-accelerated codec implementations:
+ #include "lib_openmp.c"
+#endif
+
+void
+base64_encode
+ ( const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ , int flags
+ )
+{
+ size_t s;
+ size_t t;
+ struct base64_state state;
+
+ #ifdef _OPENMP
+ if (srclen >= OMP_THRESHOLD) {
+ base64_encode_openmp(src, srclen, out, outlen, flags);
+ return;
+ }
+ #endif
+
+ // Init the stream reader:
+ base64_stream_encode_init(&state, flags);
+
+ // Feed the whole string to the stream reader:
+ base64_stream_encode(&state, src, srclen, out, &s);
+
+ // Finalize the stream by writing trailer if any:
+ base64_stream_encode_final(&state, out + s, &t);
+
+ // Final output length is stream length plus tail:
+ *outlen = s + t;
+}
+
+int
+base64_decode
+ ( const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ , int flags
+ )
+{
+ int ret;
+ struct base64_state state;
+
+ #ifdef _OPENMP
+ if (srclen >= OMP_THRESHOLD) {
+ return base64_decode_openmp(src, srclen, out, outlen, flags);
+ }
+ #endif
+
+ // Init the stream reader:
+ base64_stream_decode_init(&state, flags);
+
+ // Feed the whole string to the stream reader:
+ ret = base64_stream_decode(&state, src, srclen, out, outlen);
+
+ // If when decoding a whole block, we're still waiting for input then fail:
+ if (ret && (state.bytes == 0)) {
+ return ret;
+ }
+ return 0;
+}
diff --git a/src/third-party/base64/lib/lib_openmp.c b/src/third-party/base64/lib/lib_openmp.c
new file mode 100644
index 0000000..6b87c52
--- /dev/null
+++ b/src/third-party/base64/lib/lib_openmp.c
@@ -0,0 +1,149 @@
+// This code makes some assumptions on the implementation of
+// base64_stream_encode_init(), base64_stream_encode() and base64_stream_decode().
+// Basically these assumptions boil down to that when breaking the src into
+// parts, out parts can be written without side effects.
+// This is met when:
+// 1) base64_stream_encode() and base64_stream_decode() don't use globals;
+// 2) the shared variables src and out are not read or written outside of the
+// bounds of their parts, i.e. when base64_stream_encode() reads a multiple
+// of 3 bytes, it must write no more then a multiple of 4 bytes, not even
+// temporarily;
+// 3) the state flag can be discarded after base64_stream_encode() and
+// base64_stream_decode() on the parts.
+
+static inline void
+base64_encode_openmp
+ ( const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ , int flags
+ )
+{
+ size_t s;
+ size_t t;
+ size_t sum = 0, len, last_len;
+ struct base64_state state, initial_state;
+ int num_threads, i;
+
+ // Request a number of threads but not necessarily get them:
+ #pragma omp parallel
+ {
+ // Get the number of threads used from one thread only,
+ // as num_threads is a shared var:
+ #pragma omp single
+ {
+ num_threads = omp_get_num_threads();
+
+ // Split the input string into num_threads parts, each
+ // part a multiple of 3 bytes. The remaining bytes will
+ // be done later:
+ len = srclen / (num_threads * 3);
+ len *= 3;
+ last_len = srclen - num_threads * len;
+
+ // Init the stream reader:
+ base64_stream_encode_init(&state, flags);
+ initial_state = state;
+ }
+
+ // Single has an implicit barrier for all threads to wait here
+ // for the above to complete:
+ #pragma omp for firstprivate(state) private(s) reduction(+:sum) schedule(static,1)
+ for (i = 0; i < num_threads; i++)
+ {
+ // Feed each part of the string to the stream reader:
+ base64_stream_encode(&state, src + i * len, len, out + i * len * 4 / 3, &s);
+ sum += s;
+ }
+ }
+
+ // As encoding should never fail and we encode an exact multiple
+ // of 3 bytes, we can discard state:
+ state = initial_state;
+
+ // Encode the remaining bytes:
+ base64_stream_encode(&state, src + num_threads * len, last_len, out + num_threads * len * 4 / 3, &s);
+
+ // Finalize the stream by writing trailer if any:
+ base64_stream_encode_final(&state, out + num_threads * len * 4 / 3 + s, &t);
+
+ // Final output length is stream length plus tail:
+ sum += s + t;
+ *outlen = sum;
+}
+
+static inline int
+base64_decode_openmp
+ ( const char *src
+ , size_t srclen
+ , char *out
+ , size_t *outlen
+ , int flags
+ )
+{
+ int num_threads, result = 0, i;
+ size_t sum = 0, len, last_len, s;
+ struct base64_state state, initial_state;
+
+ // Request a number of threads but not necessarily get them:
+ #pragma omp parallel
+ {
+ // Get the number of threads used from one thread only,
+ // as num_threads is a shared var:
+ #pragma omp single
+ {
+ num_threads = omp_get_num_threads();
+
+ // Split the input string into num_threads parts, each
+ // part a multiple of 4 bytes. The remaining bytes will
+ // be done later:
+ len = srclen / (num_threads * 4);
+ len *= 4;
+ last_len = srclen - num_threads * len;
+
+ // Init the stream reader:
+ base64_stream_decode_init(&state, flags);
+
+ initial_state = state;
+ }
+
+ // Single has an implicit barrier to wait here for the above to
+ // complete:
+ #pragma omp for firstprivate(state) private(s) reduction(+:sum, result) schedule(static,1)
+ for (i = 0; i < num_threads; i++)
+ {
+ int this_result;
+
+ // Feed each part of the string to the stream reader:
+ this_result = base64_stream_decode(&state, src + i * len, len, out + i * len * 3 / 4, &s);
+ sum += s;
+ result += this_result;
+ }
+ }
+
+ // If `result' equals `-num_threads', then all threads returned -1,
+ // indicating that the requested codec is not available:
+ if (result == -num_threads) {
+ return -1;
+ }
+
+ // If `result' does not equal `num_threads', then at least one of the
+ // threads hit a decode error:
+ if (result != num_threads) {
+ return 0;
+ }
+
+ // So far so good, now decode whatever remains in the buffer. Reuse the
+ // initial state, since we are at a 4-byte boundary:
+ state = initial_state;
+ result = base64_stream_decode(&state, src + num_threads * len, last_len, out + num_threads * len * 3 / 4, &s);
+ sum += s;
+ *outlen = sum;
+
+ // If when decoding a whole block, we're still waiting for input then fail:
+ if (result && (state.bytes == 0)) {
+ return result;
+ }
+ return 0;
+}
diff --git a/src/third-party/base64/lib/tables/table_dec_32bit.h b/src/third-party/base64/lib/tables/table_dec_32bit.h
new file mode 100644
index 0000000..f5d951f
--- /dev/null
+++ b/src/third-party/base64/lib/tables/table_dec_32bit.h
@@ -0,0 +1,393 @@
+#include <stdint.h>
+#define CHAR62 '+'
+#define CHAR63 '/'
+#define CHARPAD '='
+
+
+#if BASE64_LITTLE_ENDIAN
+
+
+/* SPECIAL DECODE TABLES FOR LITTLE ENDIAN (INTEL) CPUS */
+
+const uint32_t base64_table_dec_32bit_d0[256] = {
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x000000f8, 0xffffffff, 0xffffffff, 0xffffffff, 0x000000fc,
+0x000000d0, 0x000000d4, 0x000000d8, 0x000000dc, 0x000000e0, 0x000000e4,
+0x000000e8, 0x000000ec, 0x000000f0, 0x000000f4, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
+0x00000004, 0x00000008, 0x0000000c, 0x00000010, 0x00000014, 0x00000018,
+0x0000001c, 0x00000020, 0x00000024, 0x00000028, 0x0000002c, 0x00000030,
+0x00000034, 0x00000038, 0x0000003c, 0x00000040, 0x00000044, 0x00000048,
+0x0000004c, 0x00000050, 0x00000054, 0x00000058, 0x0000005c, 0x00000060,
+0x00000064, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x00000068, 0x0000006c, 0x00000070, 0x00000074, 0x00000078,
+0x0000007c, 0x00000080, 0x00000084, 0x00000088, 0x0000008c, 0x00000090,
+0x00000094, 0x00000098, 0x0000009c, 0x000000a0, 0x000000a4, 0x000000a8,
+0x000000ac, 0x000000b0, 0x000000b4, 0x000000b8, 0x000000bc, 0x000000c0,
+0x000000c4, 0x000000c8, 0x000000cc, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff
+};
+
+
+const uint32_t base64_table_dec_32bit_d1[256] = {
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x0000e003, 0xffffffff, 0xffffffff, 0xffffffff, 0x0000f003,
+0x00004003, 0x00005003, 0x00006003, 0x00007003, 0x00008003, 0x00009003,
+0x0000a003, 0x0000b003, 0x0000c003, 0x0000d003, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
+0x00001000, 0x00002000, 0x00003000, 0x00004000, 0x00005000, 0x00006000,
+0x00007000, 0x00008000, 0x00009000, 0x0000a000, 0x0000b000, 0x0000c000,
+0x0000d000, 0x0000e000, 0x0000f000, 0x00000001, 0x00001001, 0x00002001,
+0x00003001, 0x00004001, 0x00005001, 0x00006001, 0x00007001, 0x00008001,
+0x00009001, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x0000a001, 0x0000b001, 0x0000c001, 0x0000d001, 0x0000e001,
+0x0000f001, 0x00000002, 0x00001002, 0x00002002, 0x00003002, 0x00004002,
+0x00005002, 0x00006002, 0x00007002, 0x00008002, 0x00009002, 0x0000a002,
+0x0000b002, 0x0000c002, 0x0000d002, 0x0000e002, 0x0000f002, 0x00000003,
+0x00001003, 0x00002003, 0x00003003, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff
+};
+
+
+const uint32_t base64_table_dec_32bit_d2[256] = {
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x00800f00, 0xffffffff, 0xffffffff, 0xffffffff, 0x00c00f00,
+0x00000d00, 0x00400d00, 0x00800d00, 0x00c00d00, 0x00000e00, 0x00400e00,
+0x00800e00, 0x00c00e00, 0x00000f00, 0x00400f00, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
+0x00400000, 0x00800000, 0x00c00000, 0x00000100, 0x00400100, 0x00800100,
+0x00c00100, 0x00000200, 0x00400200, 0x00800200, 0x00c00200, 0x00000300,
+0x00400300, 0x00800300, 0x00c00300, 0x00000400, 0x00400400, 0x00800400,
+0x00c00400, 0x00000500, 0x00400500, 0x00800500, 0x00c00500, 0x00000600,
+0x00400600, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x00800600, 0x00c00600, 0x00000700, 0x00400700, 0x00800700,
+0x00c00700, 0x00000800, 0x00400800, 0x00800800, 0x00c00800, 0x00000900,
+0x00400900, 0x00800900, 0x00c00900, 0x00000a00, 0x00400a00, 0x00800a00,
+0x00c00a00, 0x00000b00, 0x00400b00, 0x00800b00, 0x00c00b00, 0x00000c00,
+0x00400c00, 0x00800c00, 0x00c00c00, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff
+};
+
+
+const uint32_t base64_table_dec_32bit_d3[256] = {
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x003e0000, 0xffffffff, 0xffffffff, 0xffffffff, 0x003f0000,
+0x00340000, 0x00350000, 0x00360000, 0x00370000, 0x00380000, 0x00390000,
+0x003a0000, 0x003b0000, 0x003c0000, 0x003d0000, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
+0x00010000, 0x00020000, 0x00030000, 0x00040000, 0x00050000, 0x00060000,
+0x00070000, 0x00080000, 0x00090000, 0x000a0000, 0x000b0000, 0x000c0000,
+0x000d0000, 0x000e0000, 0x000f0000, 0x00100000, 0x00110000, 0x00120000,
+0x00130000, 0x00140000, 0x00150000, 0x00160000, 0x00170000, 0x00180000,
+0x00190000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x001a0000, 0x001b0000, 0x001c0000, 0x001d0000, 0x001e0000,
+0x001f0000, 0x00200000, 0x00210000, 0x00220000, 0x00230000, 0x00240000,
+0x00250000, 0x00260000, 0x00270000, 0x00280000, 0x00290000, 0x002a0000,
+0x002b0000, 0x002c0000, 0x002d0000, 0x002e0000, 0x002f0000, 0x00300000,
+0x00310000, 0x00320000, 0x00330000, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff
+};
+
+
+#else
+
+
+/* SPECIAL DECODE TABLES FOR BIG ENDIAN (IBM/MOTOROLA/SUN) CPUS */
+
+const uint32_t base64_table_dec_32bit_d0[256] = {
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xf8000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xfc000000,
+0xd0000000, 0xd4000000, 0xd8000000, 0xdc000000, 0xe0000000, 0xe4000000,
+0xe8000000, 0xec000000, 0xf0000000, 0xf4000000, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
+0x04000000, 0x08000000, 0x0c000000, 0x10000000, 0x14000000, 0x18000000,
+0x1c000000, 0x20000000, 0x24000000, 0x28000000, 0x2c000000, 0x30000000,
+0x34000000, 0x38000000, 0x3c000000, 0x40000000, 0x44000000, 0x48000000,
+0x4c000000, 0x50000000, 0x54000000, 0x58000000, 0x5c000000, 0x60000000,
+0x64000000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x68000000, 0x6c000000, 0x70000000, 0x74000000, 0x78000000,
+0x7c000000, 0x80000000, 0x84000000, 0x88000000, 0x8c000000, 0x90000000,
+0x94000000, 0x98000000, 0x9c000000, 0xa0000000, 0xa4000000, 0xa8000000,
+0xac000000, 0xb0000000, 0xb4000000, 0xb8000000, 0xbc000000, 0xc0000000,
+0xc4000000, 0xc8000000, 0xcc000000, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff
+};
+
+
+const uint32_t base64_table_dec_32bit_d1[256] = {
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x03e00000, 0xffffffff, 0xffffffff, 0xffffffff, 0x03f00000,
+0x03400000, 0x03500000, 0x03600000, 0x03700000, 0x03800000, 0x03900000,
+0x03a00000, 0x03b00000, 0x03c00000, 0x03d00000, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
+0x00100000, 0x00200000, 0x00300000, 0x00400000, 0x00500000, 0x00600000,
+0x00700000, 0x00800000, 0x00900000, 0x00a00000, 0x00b00000, 0x00c00000,
+0x00d00000, 0x00e00000, 0x00f00000, 0x01000000, 0x01100000, 0x01200000,
+0x01300000, 0x01400000, 0x01500000, 0x01600000, 0x01700000, 0x01800000,
+0x01900000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x01a00000, 0x01b00000, 0x01c00000, 0x01d00000, 0x01e00000,
+0x01f00000, 0x02000000, 0x02100000, 0x02200000, 0x02300000, 0x02400000,
+0x02500000, 0x02600000, 0x02700000, 0x02800000, 0x02900000, 0x02a00000,
+0x02b00000, 0x02c00000, 0x02d00000, 0x02e00000, 0x02f00000, 0x03000000,
+0x03100000, 0x03200000, 0x03300000, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff
+};
+
+
+const uint32_t base64_table_dec_32bit_d2[256] = {
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x000f8000, 0xffffffff, 0xffffffff, 0xffffffff, 0x000fc000,
+0x000d0000, 0x000d4000, 0x000d8000, 0x000dc000, 0x000e0000, 0x000e4000,
+0x000e8000, 0x000ec000, 0x000f0000, 0x000f4000, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
+0x00004000, 0x00008000, 0x0000c000, 0x00010000, 0x00014000, 0x00018000,
+0x0001c000, 0x00020000, 0x00024000, 0x00028000, 0x0002c000, 0x00030000,
+0x00034000, 0x00038000, 0x0003c000, 0x00040000, 0x00044000, 0x00048000,
+0x0004c000, 0x00050000, 0x00054000, 0x00058000, 0x0005c000, 0x00060000,
+0x00064000, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x00068000, 0x0006c000, 0x00070000, 0x00074000, 0x00078000,
+0x0007c000, 0x00080000, 0x00084000, 0x00088000, 0x0008c000, 0x00090000,
+0x00094000, 0x00098000, 0x0009c000, 0x000a0000, 0x000a4000, 0x000a8000,
+0x000ac000, 0x000b0000, 0x000b4000, 0x000b8000, 0x000bc000, 0x000c0000,
+0x000c4000, 0x000c8000, 0x000cc000, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff
+};
+
+
+const uint32_t base64_table_dec_32bit_d3[256] = {
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x00003e00, 0xffffffff, 0xffffffff, 0xffffffff, 0x00003f00,
+0x00003400, 0x00003500, 0x00003600, 0x00003700, 0x00003800, 0x00003900,
+0x00003a00, 0x00003b00, 0x00003c00, 0x00003d00, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0x00000000,
+0x00000100, 0x00000200, 0x00000300, 0x00000400, 0x00000500, 0x00000600,
+0x00000700, 0x00000800, 0x00000900, 0x00000a00, 0x00000b00, 0x00000c00,
+0x00000d00, 0x00000e00, 0x00000f00, 0x00001000, 0x00001100, 0x00001200,
+0x00001300, 0x00001400, 0x00001500, 0x00001600, 0x00001700, 0x00001800,
+0x00001900, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0x00001a00, 0x00001b00, 0x00001c00, 0x00001d00, 0x00001e00,
+0x00001f00, 0x00002000, 0x00002100, 0x00002200, 0x00002300, 0x00002400,
+0x00002500, 0x00002600, 0x00002700, 0x00002800, 0x00002900, 0x00002a00,
+0x00002b00, 0x00002c00, 0x00002d00, 0x00002e00, 0x00002f00, 0x00003000,
+0x00003100, 0x00003200, 0x00003300, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff,
+0xffffffff, 0xffffffff, 0xffffffff, 0xffffffff
+};
+
+
+#endif
diff --git a/src/third-party/base64/lib/tables/table_enc_12bit.h b/src/third-party/base64/lib/tables/table_enc_12bit.h
new file mode 100644
index 0000000..2bc0d23
--- /dev/null
+++ b/src/third-party/base64/lib/tables/table_enc_12bit.h
@@ -0,0 +1,1031 @@
+#include <stdint.h>
+
+const uint16_t base64_table_enc_12bit[] = {
+#if BASE64_LITTLE_ENDIAN
+ 0x4141U, 0x4241U, 0x4341U, 0x4441U, 0x4541U, 0x4641U, 0x4741U, 0x4841U,
+ 0x4941U, 0x4A41U, 0x4B41U, 0x4C41U, 0x4D41U, 0x4E41U, 0x4F41U, 0x5041U,
+ 0x5141U, 0x5241U, 0x5341U, 0x5441U, 0x5541U, 0x5641U, 0x5741U, 0x5841U,
+ 0x5941U, 0x5A41U, 0x6141U, 0x6241U, 0x6341U, 0x6441U, 0x6541U, 0x6641U,
+ 0x6741U, 0x6841U, 0x6941U, 0x6A41U, 0x6B41U, 0x6C41U, 0x6D41U, 0x6E41U,
+ 0x6F41U, 0x7041U, 0x7141U, 0x7241U, 0x7341U, 0x7441U, 0x7541U, 0x7641U,
+ 0x7741U, 0x7841U, 0x7941U, 0x7A41U, 0x3041U, 0x3141U, 0x3241U, 0x3341U,
+ 0x3441U, 0x3541U, 0x3641U, 0x3741U, 0x3841U, 0x3941U, 0x2B41U, 0x2F41U,
+ 0x4142U, 0x4242U, 0x4342U, 0x4442U, 0x4542U, 0x4642U, 0x4742U, 0x4842U,
+ 0x4942U, 0x4A42U, 0x4B42U, 0x4C42U, 0x4D42U, 0x4E42U, 0x4F42U, 0x5042U,
+ 0x5142U, 0x5242U, 0x5342U, 0x5442U, 0x5542U, 0x5642U, 0x5742U, 0x5842U,
+ 0x5942U, 0x5A42U, 0x6142U, 0x6242U, 0x6342U, 0x6442U, 0x6542U, 0x6642U,
+ 0x6742U, 0x6842U, 0x6942U, 0x6A42U, 0x6B42U, 0x6C42U, 0x6D42U, 0x6E42U,
+ 0x6F42U, 0x7042U, 0x7142U, 0x7242U, 0x7342U, 0x7442U, 0x7542U, 0x7642U,
+ 0x7742U, 0x7842U, 0x7942U, 0x7A42U, 0x3042U, 0x3142U, 0x3242U, 0x3342U,
+ 0x3442U, 0x3542U, 0x3642U, 0x3742U, 0x3842U, 0x3942U, 0x2B42U, 0x2F42U,
+ 0x4143U, 0x4243U, 0x4343U, 0x4443U, 0x4543U, 0x4643U, 0x4743U, 0x4843U,
+ 0x4943U, 0x4A43U, 0x4B43U, 0x4C43U, 0x4D43U, 0x4E43U, 0x4F43U, 0x5043U,
+ 0x5143U, 0x5243U, 0x5343U, 0x5443U, 0x5543U, 0x5643U, 0x5743U, 0x5843U,
+ 0x5943U, 0x5A43U, 0x6143U, 0x6243U, 0x6343U, 0x6443U, 0x6543U, 0x6643U,
+ 0x6743U, 0x6843U, 0x6943U, 0x6A43U, 0x6B43U, 0x6C43U, 0x6D43U, 0x6E43U,
+ 0x6F43U, 0x7043U, 0x7143U, 0x7243U, 0x7343U, 0x7443U, 0x7543U, 0x7643U,
+ 0x7743U, 0x7843U, 0x7943U, 0x7A43U, 0x3043U, 0x3143U, 0x3243U, 0x3343U,
+ 0x3443U, 0x3543U, 0x3643U, 0x3743U, 0x3843U, 0x3943U, 0x2B43U, 0x2F43U,
+ 0x4144U, 0x4244U, 0x4344U, 0x4444U, 0x4544U, 0x4644U, 0x4744U, 0x4844U,
+ 0x4944U, 0x4A44U, 0x4B44U, 0x4C44U, 0x4D44U, 0x4E44U, 0x4F44U, 0x5044U,
+ 0x5144U, 0x5244U, 0x5344U, 0x5444U, 0x5544U, 0x5644U, 0x5744U, 0x5844U,
+ 0x5944U, 0x5A44U, 0x6144U, 0x6244U, 0x6344U, 0x6444U, 0x6544U, 0x6644U,
+ 0x6744U, 0x6844U, 0x6944U, 0x6A44U, 0x6B44U, 0x6C44U, 0x6D44U, 0x6E44U,
+ 0x6F44U, 0x7044U, 0x7144U, 0x7244U, 0x7344U, 0x7444U, 0x7544U, 0x7644U,
+ 0x7744U, 0x7844U, 0x7944U, 0x7A44U, 0x3044U, 0x3144U, 0x3244U, 0x3344U,
+ 0x3444U, 0x3544U, 0x3644U, 0x3744U, 0x3844U, 0x3944U, 0x2B44U, 0x2F44U,
+ 0x4145U, 0x4245U, 0x4345U, 0x4445U, 0x4545U, 0x4645U, 0x4745U, 0x4845U,
+ 0x4945U, 0x4A45U, 0x4B45U, 0x4C45U, 0x4D45U, 0x4E45U, 0x4F45U, 0x5045U,
+ 0x5145U, 0x5245U, 0x5345U, 0x5445U, 0x5545U, 0x5645U, 0x5745U, 0x5845U,
+ 0x5945U, 0x5A45U, 0x6145U, 0x6245U, 0x6345U, 0x6445U, 0x6545U, 0x6645U,
+ 0x6745U, 0x6845U, 0x6945U, 0x6A45U, 0x6B45U, 0x6C45U, 0x6D45U, 0x6E45U,
+ 0x6F45U, 0x7045U, 0x7145U, 0x7245U, 0x7345U, 0x7445U, 0x7545U, 0x7645U,
+ 0x7745U, 0x7845U, 0x7945U, 0x7A45U, 0x3045U, 0x3145U, 0x3245U, 0x3345U,
+ 0x3445U, 0x3545U, 0x3645U, 0x3745U, 0x3845U, 0x3945U, 0x2B45U, 0x2F45U,
+ 0x4146U, 0x4246U, 0x4346U, 0x4446U, 0x4546U, 0x4646U, 0x4746U, 0x4846U,
+ 0x4946U, 0x4A46U, 0x4B46U, 0x4C46U, 0x4D46U, 0x4E46U, 0x4F46U, 0x5046U,
+ 0x5146U, 0x5246U, 0x5346U, 0x5446U, 0x5546U, 0x5646U, 0x5746U, 0x5846U,
+ 0x5946U, 0x5A46U, 0x6146U, 0x6246U, 0x6346U, 0x6446U, 0x6546U, 0x6646U,
+ 0x6746U, 0x6846U, 0x6946U, 0x6A46U, 0x6B46U, 0x6C46U, 0x6D46U, 0x6E46U,
+ 0x6F46U, 0x7046U, 0x7146U, 0x7246U, 0x7346U, 0x7446U, 0x7546U, 0x7646U,
+ 0x7746U, 0x7846U, 0x7946U, 0x7A46U, 0x3046U, 0x3146U, 0x3246U, 0x3346U,
+ 0x3446U, 0x3546U, 0x3646U, 0x3746U, 0x3846U, 0x3946U, 0x2B46U, 0x2F46U,
+ 0x4147U, 0x4247U, 0x4347U, 0x4447U, 0x4547U, 0x4647U, 0x4747U, 0x4847U,
+ 0x4947U, 0x4A47U, 0x4B47U, 0x4C47U, 0x4D47U, 0x4E47U, 0x4F47U, 0x5047U,
+ 0x5147U, 0x5247U, 0x5347U, 0x5447U, 0x5547U, 0x5647U, 0x5747U, 0x5847U,
+ 0x5947U, 0x5A47U, 0x6147U, 0x6247U, 0x6347U, 0x6447U, 0x6547U, 0x6647U,
+ 0x6747U, 0x6847U, 0x6947U, 0x6A47U, 0x6B47U, 0x6C47U, 0x6D47U, 0x6E47U,
+ 0x6F47U, 0x7047U, 0x7147U, 0x7247U, 0x7347U, 0x7447U, 0x7547U, 0x7647U,
+ 0x7747U, 0x7847U, 0x7947U, 0x7A47U, 0x3047U, 0x3147U, 0x3247U, 0x3347U,
+ 0x3447U, 0x3547U, 0x3647U, 0x3747U, 0x3847U, 0x3947U, 0x2B47U, 0x2F47U,
+ 0x4148U, 0x4248U, 0x4348U, 0x4448U, 0x4548U, 0x4648U, 0x4748U, 0x4848U,
+ 0x4948U, 0x4A48U, 0x4B48U, 0x4C48U, 0x4D48U, 0x4E48U, 0x4F48U, 0x5048U,
+ 0x5148U, 0x5248U, 0x5348U, 0x5448U, 0x5548U, 0x5648U, 0x5748U, 0x5848U,
+ 0x5948U, 0x5A48U, 0x6148U, 0x6248U, 0x6348U, 0x6448U, 0x6548U, 0x6648U,
+ 0x6748U, 0x6848U, 0x6948U, 0x6A48U, 0x6B48U, 0x6C48U, 0x6D48U, 0x6E48U,
+ 0x6F48U, 0x7048U, 0x7148U, 0x7248U, 0x7348U, 0x7448U, 0x7548U, 0x7648U,
+ 0x7748U, 0x7848U, 0x7948U, 0x7A48U, 0x3048U, 0x3148U, 0x3248U, 0x3348U,
+ 0x3448U, 0x3548U, 0x3648U, 0x3748U, 0x3848U, 0x3948U, 0x2B48U, 0x2F48U,
+ 0x4149U, 0x4249U, 0x4349U, 0x4449U, 0x4549U, 0x4649U, 0x4749U, 0x4849U,
+ 0x4949U, 0x4A49U, 0x4B49U, 0x4C49U, 0x4D49U, 0x4E49U, 0x4F49U, 0x5049U,
+ 0x5149U, 0x5249U, 0x5349U, 0x5449U, 0x5549U, 0x5649U, 0x5749U, 0x5849U,
+ 0x5949U, 0x5A49U, 0x6149U, 0x6249U, 0x6349U, 0x6449U, 0x6549U, 0x6649U,
+ 0x6749U, 0x6849U, 0x6949U, 0x6A49U, 0x6B49U, 0x6C49U, 0x6D49U, 0x6E49U,
+ 0x6F49U, 0x7049U, 0x7149U, 0x7249U, 0x7349U, 0x7449U, 0x7549U, 0x7649U,
+ 0x7749U, 0x7849U, 0x7949U, 0x7A49U, 0x3049U, 0x3149U, 0x3249U, 0x3349U,
+ 0x3449U, 0x3549U, 0x3649U, 0x3749U, 0x3849U, 0x3949U, 0x2B49U, 0x2F49U,
+ 0x414AU, 0x424AU, 0x434AU, 0x444AU, 0x454AU, 0x464AU, 0x474AU, 0x484AU,
+ 0x494AU, 0x4A4AU, 0x4B4AU, 0x4C4AU, 0x4D4AU, 0x4E4AU, 0x4F4AU, 0x504AU,
+ 0x514AU, 0x524AU, 0x534AU, 0x544AU, 0x554AU, 0x564AU, 0x574AU, 0x584AU,
+ 0x594AU, 0x5A4AU, 0x614AU, 0x624AU, 0x634AU, 0x644AU, 0x654AU, 0x664AU,
+ 0x674AU, 0x684AU, 0x694AU, 0x6A4AU, 0x6B4AU, 0x6C4AU, 0x6D4AU, 0x6E4AU,
+ 0x6F4AU, 0x704AU, 0x714AU, 0x724AU, 0x734AU, 0x744AU, 0x754AU, 0x764AU,
+ 0x774AU, 0x784AU, 0x794AU, 0x7A4AU, 0x304AU, 0x314AU, 0x324AU, 0x334AU,
+ 0x344AU, 0x354AU, 0x364AU, 0x374AU, 0x384AU, 0x394AU, 0x2B4AU, 0x2F4AU,
+ 0x414BU, 0x424BU, 0x434BU, 0x444BU, 0x454BU, 0x464BU, 0x474BU, 0x484BU,
+ 0x494BU, 0x4A4BU, 0x4B4BU, 0x4C4BU, 0x4D4BU, 0x4E4BU, 0x4F4BU, 0x504BU,
+ 0x514BU, 0x524BU, 0x534BU, 0x544BU, 0x554BU, 0x564BU, 0x574BU, 0x584BU,
+ 0x594BU, 0x5A4BU, 0x614BU, 0x624BU, 0x634BU, 0x644BU, 0x654BU, 0x664BU,
+ 0x674BU, 0x684BU, 0x694BU, 0x6A4BU, 0x6B4BU, 0x6C4BU, 0x6D4BU, 0x6E4BU,
+ 0x6F4BU, 0x704BU, 0x714BU, 0x724BU, 0x734BU, 0x744BU, 0x754BU, 0x764BU,
+ 0x774BU, 0x784BU, 0x794BU, 0x7A4BU, 0x304BU, 0x314BU, 0x324BU, 0x334BU,
+ 0x344BU, 0x354BU, 0x364BU, 0x374BU, 0x384BU, 0x394BU, 0x2B4BU, 0x2F4BU,
+ 0x414CU, 0x424CU, 0x434CU, 0x444CU, 0x454CU, 0x464CU, 0x474CU, 0x484CU,
+ 0x494CU, 0x4A4CU, 0x4B4CU, 0x4C4CU, 0x4D4CU, 0x4E4CU, 0x4F4CU, 0x504CU,
+ 0x514CU, 0x524CU, 0x534CU, 0x544CU, 0x554CU, 0x564CU, 0x574CU, 0x584CU,
+ 0x594CU, 0x5A4CU, 0x614CU, 0x624CU, 0x634CU, 0x644CU, 0x654CU, 0x664CU,
+ 0x674CU, 0x684CU, 0x694CU, 0x6A4CU, 0x6B4CU, 0x6C4CU, 0x6D4CU, 0x6E4CU,
+ 0x6F4CU, 0x704CU, 0x714CU, 0x724CU, 0x734CU, 0x744CU, 0x754CU, 0x764CU,
+ 0x774CU, 0x784CU, 0x794CU, 0x7A4CU, 0x304CU, 0x314CU, 0x324CU, 0x334CU,
+ 0x344CU, 0x354CU, 0x364CU, 0x374CU, 0x384CU, 0x394CU, 0x2B4CU, 0x2F4CU,
+ 0x414DU, 0x424DU, 0x434DU, 0x444DU, 0x454DU, 0x464DU, 0x474DU, 0x484DU,
+ 0x494DU, 0x4A4DU, 0x4B4DU, 0x4C4DU, 0x4D4DU, 0x4E4DU, 0x4F4DU, 0x504DU,
+ 0x514DU, 0x524DU, 0x534DU, 0x544DU, 0x554DU, 0x564DU, 0x574DU, 0x584DU,
+ 0x594DU, 0x5A4DU, 0x614DU, 0x624DU, 0x634DU, 0x644DU, 0x654DU, 0x664DU,
+ 0x674DU, 0x684DU, 0x694DU, 0x6A4DU, 0x6B4DU, 0x6C4DU, 0x6D4DU, 0x6E4DU,
+ 0x6F4DU, 0x704DU, 0x714DU, 0x724DU, 0x734DU, 0x744DU, 0x754DU, 0x764DU,
+ 0x774DU, 0x784DU, 0x794DU, 0x7A4DU, 0x304DU, 0x314DU, 0x324DU, 0x334DU,
+ 0x344DU, 0x354DU, 0x364DU, 0x374DU, 0x384DU, 0x394DU, 0x2B4DU, 0x2F4DU,
+ 0x414EU, 0x424EU, 0x434EU, 0x444EU, 0x454EU, 0x464EU, 0x474EU, 0x484EU,
+ 0x494EU, 0x4A4EU, 0x4B4EU, 0x4C4EU, 0x4D4EU, 0x4E4EU, 0x4F4EU, 0x504EU,
+ 0x514EU, 0x524EU, 0x534EU, 0x544EU, 0x554EU, 0x564EU, 0x574EU, 0x584EU,
+ 0x594EU, 0x5A4EU, 0x614EU, 0x624EU, 0x634EU, 0x644EU, 0x654EU, 0x664EU,
+ 0x674EU, 0x684EU, 0x694EU, 0x6A4EU, 0x6B4EU, 0x6C4EU, 0x6D4EU, 0x6E4EU,
+ 0x6F4EU, 0x704EU, 0x714EU, 0x724EU, 0x734EU, 0x744EU, 0x754EU, 0x764EU,
+ 0x774EU, 0x784EU, 0x794EU, 0x7A4EU, 0x304EU, 0x314EU, 0x324EU, 0x334EU,
+ 0x344EU, 0x354EU, 0x364EU, 0x374EU, 0x384EU, 0x394EU, 0x2B4EU, 0x2F4EU,
+ 0x414FU, 0x424FU, 0x434FU, 0x444FU, 0x454FU, 0x464FU, 0x474FU, 0x484FU,
+ 0x494FU, 0x4A4FU, 0x4B4FU, 0x4C4FU, 0x4D4FU, 0x4E4FU, 0x4F4FU, 0x504FU,
+ 0x514FU, 0x524FU, 0x534FU, 0x544FU, 0x554FU, 0x564FU, 0x574FU, 0x584FU,
+ 0x594FU, 0x5A4FU, 0x614FU, 0x624FU, 0x634FU, 0x644FU, 0x654FU, 0x664FU,
+ 0x674FU, 0x684FU, 0x694FU, 0x6A4FU, 0x6B4FU, 0x6C4FU, 0x6D4FU, 0x6E4FU,
+ 0x6F4FU, 0x704FU, 0x714FU, 0x724FU, 0x734FU, 0x744FU, 0x754FU, 0x764FU,
+ 0x774FU, 0x784FU, 0x794FU, 0x7A4FU, 0x304FU, 0x314FU, 0x324FU, 0x334FU,
+ 0x344FU, 0x354FU, 0x364FU, 0x374FU, 0x384FU, 0x394FU, 0x2B4FU, 0x2F4FU,
+ 0x4150U, 0x4250U, 0x4350U, 0x4450U, 0x4550U, 0x4650U, 0x4750U, 0x4850U,
+ 0x4950U, 0x4A50U, 0x4B50U, 0x4C50U, 0x4D50U, 0x4E50U, 0x4F50U, 0x5050U,
+ 0x5150U, 0x5250U, 0x5350U, 0x5450U, 0x5550U, 0x5650U, 0x5750U, 0x5850U,
+ 0x5950U, 0x5A50U, 0x6150U, 0x6250U, 0x6350U, 0x6450U, 0x6550U, 0x6650U,
+ 0x6750U, 0x6850U, 0x6950U, 0x6A50U, 0x6B50U, 0x6C50U, 0x6D50U, 0x6E50U,
+ 0x6F50U, 0x7050U, 0x7150U, 0x7250U, 0x7350U, 0x7450U, 0x7550U, 0x7650U,
+ 0x7750U, 0x7850U, 0x7950U, 0x7A50U, 0x3050U, 0x3150U, 0x3250U, 0x3350U,
+ 0x3450U, 0x3550U, 0x3650U, 0x3750U, 0x3850U, 0x3950U, 0x2B50U, 0x2F50U,
+ 0x4151U, 0x4251U, 0x4351U, 0x4451U, 0x4551U, 0x4651U, 0x4751U, 0x4851U,
+ 0x4951U, 0x4A51U, 0x4B51U, 0x4C51U, 0x4D51U, 0x4E51U, 0x4F51U, 0x5051U,
+ 0x5151U, 0x5251U, 0x5351U, 0x5451U, 0x5551U, 0x5651U, 0x5751U, 0x5851U,
+ 0x5951U, 0x5A51U, 0x6151U, 0x6251U, 0x6351U, 0x6451U, 0x6551U, 0x6651U,
+ 0x6751U, 0x6851U, 0x6951U, 0x6A51U, 0x6B51U, 0x6C51U, 0x6D51U, 0x6E51U,
+ 0x6F51U, 0x7051U, 0x7151U, 0x7251U, 0x7351U, 0x7451U, 0x7551U, 0x7651U,
+ 0x7751U, 0x7851U, 0x7951U, 0x7A51U, 0x3051U, 0x3151U, 0x3251U, 0x3351U,
+ 0x3451U, 0x3551U, 0x3651U, 0x3751U, 0x3851U, 0x3951U, 0x2B51U, 0x2F51U,
+ 0x4152U, 0x4252U, 0x4352U, 0x4452U, 0x4552U, 0x4652U, 0x4752U, 0x4852U,
+ 0x4952U, 0x4A52U, 0x4B52U, 0x4C52U, 0x4D52U, 0x4E52U, 0x4F52U, 0x5052U,
+ 0x5152U, 0x5252U, 0x5352U, 0x5452U, 0x5552U, 0x5652U, 0x5752U, 0x5852U,
+ 0x5952U, 0x5A52U, 0x6152U, 0x6252U, 0x6352U, 0x6452U, 0x6552U, 0x6652U,
+ 0x6752U, 0x6852U, 0x6952U, 0x6A52U, 0x6B52U, 0x6C52U, 0x6D52U, 0x6E52U,
+ 0x6F52U, 0x7052U, 0x7152U, 0x7252U, 0x7352U, 0x7452U, 0x7552U, 0x7652U,
+ 0x7752U, 0x7852U, 0x7952U, 0x7A52U, 0x3052U, 0x3152U, 0x3252U, 0x3352U,
+ 0x3452U, 0x3552U, 0x3652U, 0x3752U, 0x3852U, 0x3952U, 0x2B52U, 0x2F52U,
+ 0x4153U, 0x4253U, 0x4353U, 0x4453U, 0x4553U, 0x4653U, 0x4753U, 0x4853U,
+ 0x4953U, 0x4A53U, 0x4B53U, 0x4C53U, 0x4D53U, 0x4E53U, 0x4F53U, 0x5053U,
+ 0x5153U, 0x5253U, 0x5353U, 0x5453U, 0x5553U, 0x5653U, 0x5753U, 0x5853U,
+ 0x5953U, 0x5A53U, 0x6153U, 0x6253U, 0x6353U, 0x6453U, 0x6553U, 0x6653U,
+ 0x6753U, 0x6853U, 0x6953U, 0x6A53U, 0x6B53U, 0x6C53U, 0x6D53U, 0x6E53U,
+ 0x6F53U, 0x7053U, 0x7153U, 0x7253U, 0x7353U, 0x7453U, 0x7553U, 0x7653U,
+ 0x7753U, 0x7853U, 0x7953U, 0x7A53U, 0x3053U, 0x3153U, 0x3253U, 0x3353U,
+ 0x3453U, 0x3553U, 0x3653U, 0x3753U, 0x3853U, 0x3953U, 0x2B53U, 0x2F53U,
+ 0x4154U, 0x4254U, 0x4354U, 0x4454U, 0x4554U, 0x4654U, 0x4754U, 0x4854U,
+ 0x4954U, 0x4A54U, 0x4B54U, 0x4C54U, 0x4D54U, 0x4E54U, 0x4F54U, 0x5054U,
+ 0x5154U, 0x5254U, 0x5354U, 0x5454U, 0x5554U, 0x5654U, 0x5754U, 0x5854U,
+ 0x5954U, 0x5A54U, 0x6154U, 0x6254U, 0x6354U, 0x6454U, 0x6554U, 0x6654U,
+ 0x6754U, 0x6854U, 0x6954U, 0x6A54U, 0x6B54U, 0x6C54U, 0x6D54U, 0x6E54U,
+ 0x6F54U, 0x7054U, 0x7154U, 0x7254U, 0x7354U, 0x7454U, 0x7554U, 0x7654U,
+ 0x7754U, 0x7854U, 0x7954U, 0x7A54U, 0x3054U, 0x3154U, 0x3254U, 0x3354U,
+ 0x3454U, 0x3554U, 0x3654U, 0x3754U, 0x3854U, 0x3954U, 0x2B54U, 0x2F54U,
+ 0x4155U, 0x4255U, 0x4355U, 0x4455U, 0x4555U, 0x4655U, 0x4755U, 0x4855U,
+ 0x4955U, 0x4A55U, 0x4B55U, 0x4C55U, 0x4D55U, 0x4E55U, 0x4F55U, 0x5055U,
+ 0x5155U, 0x5255U, 0x5355U, 0x5455U, 0x5555U, 0x5655U, 0x5755U, 0x5855U,
+ 0x5955U, 0x5A55U, 0x6155U, 0x6255U, 0x6355U, 0x6455U, 0x6555U, 0x6655U,
+ 0x6755U, 0x6855U, 0x6955U, 0x6A55U, 0x6B55U, 0x6C55U, 0x6D55U, 0x6E55U,
+ 0x6F55U, 0x7055U, 0x7155U, 0x7255U, 0x7355U, 0x7455U, 0x7555U, 0x7655U,
+ 0x7755U, 0x7855U, 0x7955U, 0x7A55U, 0x3055U, 0x3155U, 0x3255U, 0x3355U,
+ 0x3455U, 0x3555U, 0x3655U, 0x3755U, 0x3855U, 0x3955U, 0x2B55U, 0x2F55U,
+ 0x4156U, 0x4256U, 0x4356U, 0x4456U, 0x4556U, 0x4656U, 0x4756U, 0x4856U,
+ 0x4956U, 0x4A56U, 0x4B56U, 0x4C56U, 0x4D56U, 0x4E56U, 0x4F56U, 0x5056U,
+ 0x5156U, 0x5256U, 0x5356U, 0x5456U, 0x5556U, 0x5656U, 0x5756U, 0x5856U,
+ 0x5956U, 0x5A56U, 0x6156U, 0x6256U, 0x6356U, 0x6456U, 0x6556U, 0x6656U,
+ 0x6756U, 0x6856U, 0x6956U, 0x6A56U, 0x6B56U, 0x6C56U, 0x6D56U, 0x6E56U,
+ 0x6F56U, 0x7056U, 0x7156U, 0x7256U, 0x7356U, 0x7456U, 0x7556U, 0x7656U,
+ 0x7756U, 0x7856U, 0x7956U, 0x7A56U, 0x3056U, 0x3156U, 0x3256U, 0x3356U,
+ 0x3456U, 0x3556U, 0x3656U, 0x3756U, 0x3856U, 0x3956U, 0x2B56U, 0x2F56U,
+ 0x4157U, 0x4257U, 0x4357U, 0x4457U, 0x4557U, 0x4657U, 0x4757U, 0x4857U,
+ 0x4957U, 0x4A57U, 0x4B57U, 0x4C57U, 0x4D57U, 0x4E57U, 0x4F57U, 0x5057U,
+ 0x5157U, 0x5257U, 0x5357U, 0x5457U, 0x5557U, 0x5657U, 0x5757U, 0x5857U,
+ 0x5957U, 0x5A57U, 0x6157U, 0x6257U, 0x6357U, 0x6457U, 0x6557U, 0x6657U,
+ 0x6757U, 0x6857U, 0x6957U, 0x6A57U, 0x6B57U, 0x6C57U, 0x6D57U, 0x6E57U,
+ 0x6F57U, 0x7057U, 0x7157U, 0x7257U, 0x7357U, 0x7457U, 0x7557U, 0x7657U,
+ 0x7757U, 0x7857U, 0x7957U, 0x7A57U, 0x3057U, 0x3157U, 0x3257U, 0x3357U,
+ 0x3457U, 0x3557U, 0x3657U, 0x3757U, 0x3857U, 0x3957U, 0x2B57U, 0x2F57U,
+ 0x4158U, 0x4258U, 0x4358U, 0x4458U, 0x4558U, 0x4658U, 0x4758U, 0x4858U,
+ 0x4958U, 0x4A58U, 0x4B58U, 0x4C58U, 0x4D58U, 0x4E58U, 0x4F58U, 0x5058U,
+ 0x5158U, 0x5258U, 0x5358U, 0x5458U, 0x5558U, 0x5658U, 0x5758U, 0x5858U,
+ 0x5958U, 0x5A58U, 0x6158U, 0x6258U, 0x6358U, 0x6458U, 0x6558U, 0x6658U,
+ 0x6758U, 0x6858U, 0x6958U, 0x6A58U, 0x6B58U, 0x6C58U, 0x6D58U, 0x6E58U,
+ 0x6F58U, 0x7058U, 0x7158U, 0x7258U, 0x7358U, 0x7458U, 0x7558U, 0x7658U,
+ 0x7758U, 0x7858U, 0x7958U, 0x7A58U, 0x3058U, 0x3158U, 0x3258U, 0x3358U,
+ 0x3458U, 0x3558U, 0x3658U, 0x3758U, 0x3858U, 0x3958U, 0x2B58U, 0x2F58U,
+ 0x4159U, 0x4259U, 0x4359U, 0x4459U, 0x4559U, 0x4659U, 0x4759U, 0x4859U,
+ 0x4959U, 0x4A59U, 0x4B59U, 0x4C59U, 0x4D59U, 0x4E59U, 0x4F59U, 0x5059U,
+ 0x5159U, 0x5259U, 0x5359U, 0x5459U, 0x5559U, 0x5659U, 0x5759U, 0x5859U,
+ 0x5959U, 0x5A59U, 0x6159U, 0x6259U, 0x6359U, 0x6459U, 0x6559U, 0x6659U,
+ 0x6759U, 0x6859U, 0x6959U, 0x6A59U, 0x6B59U, 0x6C59U, 0x6D59U, 0x6E59U,
+ 0x6F59U, 0x7059U, 0x7159U, 0x7259U, 0x7359U, 0x7459U, 0x7559U, 0x7659U,
+ 0x7759U, 0x7859U, 0x7959U, 0x7A59U, 0x3059U, 0x3159U, 0x3259U, 0x3359U,
+ 0x3459U, 0x3559U, 0x3659U, 0x3759U, 0x3859U, 0x3959U, 0x2B59U, 0x2F59U,
+ 0x415AU, 0x425AU, 0x435AU, 0x445AU, 0x455AU, 0x465AU, 0x475AU, 0x485AU,
+ 0x495AU, 0x4A5AU, 0x4B5AU, 0x4C5AU, 0x4D5AU, 0x4E5AU, 0x4F5AU, 0x505AU,
+ 0x515AU, 0x525AU, 0x535AU, 0x545AU, 0x555AU, 0x565AU, 0x575AU, 0x585AU,
+ 0x595AU, 0x5A5AU, 0x615AU, 0x625AU, 0x635AU, 0x645AU, 0x655AU, 0x665AU,
+ 0x675AU, 0x685AU, 0x695AU, 0x6A5AU, 0x6B5AU, 0x6C5AU, 0x6D5AU, 0x6E5AU,
+ 0x6F5AU, 0x705AU, 0x715AU, 0x725AU, 0x735AU, 0x745AU, 0x755AU, 0x765AU,
+ 0x775AU, 0x785AU, 0x795AU, 0x7A5AU, 0x305AU, 0x315AU, 0x325AU, 0x335AU,
+ 0x345AU, 0x355AU, 0x365AU, 0x375AU, 0x385AU, 0x395AU, 0x2B5AU, 0x2F5AU,
+ 0x4161U, 0x4261U, 0x4361U, 0x4461U, 0x4561U, 0x4661U, 0x4761U, 0x4861U,
+ 0x4961U, 0x4A61U, 0x4B61U, 0x4C61U, 0x4D61U, 0x4E61U, 0x4F61U, 0x5061U,
+ 0x5161U, 0x5261U, 0x5361U, 0x5461U, 0x5561U, 0x5661U, 0x5761U, 0x5861U,
+ 0x5961U, 0x5A61U, 0x6161U, 0x6261U, 0x6361U, 0x6461U, 0x6561U, 0x6661U,
+ 0x6761U, 0x6861U, 0x6961U, 0x6A61U, 0x6B61U, 0x6C61U, 0x6D61U, 0x6E61U,
+ 0x6F61U, 0x7061U, 0x7161U, 0x7261U, 0x7361U, 0x7461U, 0x7561U, 0x7661U,
+ 0x7761U, 0x7861U, 0x7961U, 0x7A61U, 0x3061U, 0x3161U, 0x3261U, 0x3361U,
+ 0x3461U, 0x3561U, 0x3661U, 0x3761U, 0x3861U, 0x3961U, 0x2B61U, 0x2F61U,
+ 0x4162U, 0x4262U, 0x4362U, 0x4462U, 0x4562U, 0x4662U, 0x4762U, 0x4862U,
+ 0x4962U, 0x4A62U, 0x4B62U, 0x4C62U, 0x4D62U, 0x4E62U, 0x4F62U, 0x5062U,
+ 0x5162U, 0x5262U, 0x5362U, 0x5462U, 0x5562U, 0x5662U, 0x5762U, 0x5862U,
+ 0x5962U, 0x5A62U, 0x6162U, 0x6262U, 0x6362U, 0x6462U, 0x6562U, 0x6662U,
+ 0x6762U, 0x6862U, 0x6962U, 0x6A62U, 0x6B62U, 0x6C62U, 0x6D62U, 0x6E62U,
+ 0x6F62U, 0x7062U, 0x7162U, 0x7262U, 0x7362U, 0x7462U, 0x7562U, 0x7662U,
+ 0x7762U, 0x7862U, 0x7962U, 0x7A62U, 0x3062U, 0x3162U, 0x3262U, 0x3362U,
+ 0x3462U, 0x3562U, 0x3662U, 0x3762U, 0x3862U, 0x3962U, 0x2B62U, 0x2F62U,
+ 0x4163U, 0x4263U, 0x4363U, 0x4463U, 0x4563U, 0x4663U, 0x4763U, 0x4863U,
+ 0x4963U, 0x4A63U, 0x4B63U, 0x4C63U, 0x4D63U, 0x4E63U, 0x4F63U, 0x5063U,
+ 0x5163U, 0x5263U, 0x5363U, 0x5463U, 0x5563U, 0x5663U, 0x5763U, 0x5863U,
+ 0x5963U, 0x5A63U, 0x6163U, 0x6263U, 0x6363U, 0x6463U, 0x6563U, 0x6663U,
+ 0x6763U, 0x6863U, 0x6963U, 0x6A63U, 0x6B63U, 0x6C63U, 0x6D63U, 0x6E63U,
+ 0x6F63U, 0x7063U, 0x7163U, 0x7263U, 0x7363U, 0x7463U, 0x7563U, 0x7663U,
+ 0x7763U, 0x7863U, 0x7963U, 0x7A63U, 0x3063U, 0x3163U, 0x3263U, 0x3363U,
+ 0x3463U, 0x3563U, 0x3663U, 0x3763U, 0x3863U, 0x3963U, 0x2B63U, 0x2F63U,
+ 0x4164U, 0x4264U, 0x4364U, 0x4464U, 0x4564U, 0x4664U, 0x4764U, 0x4864U,
+ 0x4964U, 0x4A64U, 0x4B64U, 0x4C64U, 0x4D64U, 0x4E64U, 0x4F64U, 0x5064U,
+ 0x5164U, 0x5264U, 0x5364U, 0x5464U, 0x5564U, 0x5664U, 0x5764U, 0x5864U,
+ 0x5964U, 0x5A64U, 0x6164U, 0x6264U, 0x6364U, 0x6464U, 0x6564U, 0x6664U,
+ 0x6764U, 0x6864U, 0x6964U, 0x6A64U, 0x6B64U, 0x6C64U, 0x6D64U, 0x6E64U,
+ 0x6F64U, 0x7064U, 0x7164U, 0x7264U, 0x7364U, 0x7464U, 0x7564U, 0x7664U,
+ 0x7764U, 0x7864U, 0x7964U, 0x7A64U, 0x3064U, 0x3164U, 0x3264U, 0x3364U,
+ 0x3464U, 0x3564U, 0x3664U, 0x3764U, 0x3864U, 0x3964U, 0x2B64U, 0x2F64U,
+ 0x4165U, 0x4265U, 0x4365U, 0x4465U, 0x4565U, 0x4665U, 0x4765U, 0x4865U,
+ 0x4965U, 0x4A65U, 0x4B65U, 0x4C65U, 0x4D65U, 0x4E65U, 0x4F65U, 0x5065U,
+ 0x5165U, 0x5265U, 0x5365U, 0x5465U, 0x5565U, 0x5665U, 0x5765U, 0x5865U,
+ 0x5965U, 0x5A65U, 0x6165U, 0x6265U, 0x6365U, 0x6465U, 0x6565U, 0x6665U,
+ 0x6765U, 0x6865U, 0x6965U, 0x6A65U, 0x6B65U, 0x6C65U, 0x6D65U, 0x6E65U,
+ 0x6F65U, 0x7065U, 0x7165U, 0x7265U, 0x7365U, 0x7465U, 0x7565U, 0x7665U,
+ 0x7765U, 0x7865U, 0x7965U, 0x7A65U, 0x3065U, 0x3165U, 0x3265U, 0x3365U,
+ 0x3465U, 0x3565U, 0x3665U, 0x3765U, 0x3865U, 0x3965U, 0x2B65U, 0x2F65U,
+ 0x4166U, 0x4266U, 0x4366U, 0x4466U, 0x4566U, 0x4666U, 0x4766U, 0x4866U,
+ 0x4966U, 0x4A66U, 0x4B66U, 0x4C66U, 0x4D66U, 0x4E66U, 0x4F66U, 0x5066U,
+ 0x5166U, 0x5266U, 0x5366U, 0x5466U, 0x5566U, 0x5666U, 0x5766U, 0x5866U,
+ 0x5966U, 0x5A66U, 0x6166U, 0x6266U, 0x6366U, 0x6466U, 0x6566U, 0x6666U,
+ 0x6766U, 0x6866U, 0x6966U, 0x6A66U, 0x6B66U, 0x6C66U, 0x6D66U, 0x6E66U,
+ 0x6F66U, 0x7066U, 0x7166U, 0x7266U, 0x7366U, 0x7466U, 0x7566U, 0x7666U,
+ 0x7766U, 0x7866U, 0x7966U, 0x7A66U, 0x3066U, 0x3166U, 0x3266U, 0x3366U,
+ 0x3466U, 0x3566U, 0x3666U, 0x3766U, 0x3866U, 0x3966U, 0x2B66U, 0x2F66U,
+ 0x4167U, 0x4267U, 0x4367U, 0x4467U, 0x4567U, 0x4667U, 0x4767U, 0x4867U,
+ 0x4967U, 0x4A67U, 0x4B67U, 0x4C67U, 0x4D67U, 0x4E67U, 0x4F67U, 0x5067U,
+ 0x5167U, 0x5267U, 0x5367U, 0x5467U, 0x5567U, 0x5667U, 0x5767U, 0x5867U,
+ 0x5967U, 0x5A67U, 0x6167U, 0x6267U, 0x6367U, 0x6467U, 0x6567U, 0x6667U,
+ 0x6767U, 0x6867U, 0x6967U, 0x6A67U, 0x6B67U, 0x6C67U, 0x6D67U, 0x6E67U,
+ 0x6F67U, 0x7067U, 0x7167U, 0x7267U, 0x7367U, 0x7467U, 0x7567U, 0x7667U,
+ 0x7767U, 0x7867U, 0x7967U, 0x7A67U, 0x3067U, 0x3167U, 0x3267U, 0x3367U,
+ 0x3467U, 0x3567U, 0x3667U, 0x3767U, 0x3867U, 0x3967U, 0x2B67U, 0x2F67U,
+ 0x4168U, 0x4268U, 0x4368U, 0x4468U, 0x4568U, 0x4668U, 0x4768U, 0x4868U,
+ 0x4968U, 0x4A68U, 0x4B68U, 0x4C68U, 0x4D68U, 0x4E68U, 0x4F68U, 0x5068U,
+ 0x5168U, 0x5268U, 0x5368U, 0x5468U, 0x5568U, 0x5668U, 0x5768U, 0x5868U,
+ 0x5968U, 0x5A68U, 0x6168U, 0x6268U, 0x6368U, 0x6468U, 0x6568U, 0x6668U,
+ 0x6768U, 0x6868U, 0x6968U, 0x6A68U, 0x6B68U, 0x6C68U, 0x6D68U, 0x6E68U,
+ 0x6F68U, 0x7068U, 0x7168U, 0x7268U, 0x7368U, 0x7468U, 0x7568U, 0x7668U,
+ 0x7768U, 0x7868U, 0x7968U, 0x7A68U, 0x3068U, 0x3168U, 0x3268U, 0x3368U,
+ 0x3468U, 0x3568U, 0x3668U, 0x3768U, 0x3868U, 0x3968U, 0x2B68U, 0x2F68U,
+ 0x4169U, 0x4269U, 0x4369U, 0x4469U, 0x4569U, 0x4669U, 0x4769U, 0x4869U,
+ 0x4969U, 0x4A69U, 0x4B69U, 0x4C69U, 0x4D69U, 0x4E69U, 0x4F69U, 0x5069U,
+ 0x5169U, 0x5269U, 0x5369U, 0x5469U, 0x5569U, 0x5669U, 0x5769U, 0x5869U,
+ 0x5969U, 0x5A69U, 0x6169U, 0x6269U, 0x6369U, 0x6469U, 0x6569U, 0x6669U,
+ 0x6769U, 0x6869U, 0x6969U, 0x6A69U, 0x6B69U, 0x6C69U, 0x6D69U, 0x6E69U,
+ 0x6F69U, 0x7069U, 0x7169U, 0x7269U, 0x7369U, 0x7469U, 0x7569U, 0x7669U,
+ 0x7769U, 0x7869U, 0x7969U, 0x7A69U, 0x3069U, 0x3169U, 0x3269U, 0x3369U,
+ 0x3469U, 0x3569U, 0x3669U, 0x3769U, 0x3869U, 0x3969U, 0x2B69U, 0x2F69U,
+ 0x416AU, 0x426AU, 0x436AU, 0x446AU, 0x456AU, 0x466AU, 0x476AU, 0x486AU,
+ 0x496AU, 0x4A6AU, 0x4B6AU, 0x4C6AU, 0x4D6AU, 0x4E6AU, 0x4F6AU, 0x506AU,
+ 0x516AU, 0x526AU, 0x536AU, 0x546AU, 0x556AU, 0x566AU, 0x576AU, 0x586AU,
+ 0x596AU, 0x5A6AU, 0x616AU, 0x626AU, 0x636AU, 0x646AU, 0x656AU, 0x666AU,
+ 0x676AU, 0x686AU, 0x696AU, 0x6A6AU, 0x6B6AU, 0x6C6AU, 0x6D6AU, 0x6E6AU,
+ 0x6F6AU, 0x706AU, 0x716AU, 0x726AU, 0x736AU, 0x746AU, 0x756AU, 0x766AU,
+ 0x776AU, 0x786AU, 0x796AU, 0x7A6AU, 0x306AU, 0x316AU, 0x326AU, 0x336AU,
+ 0x346AU, 0x356AU, 0x366AU, 0x376AU, 0x386AU, 0x396AU, 0x2B6AU, 0x2F6AU,
+ 0x416BU, 0x426BU, 0x436BU, 0x446BU, 0x456BU, 0x466BU, 0x476BU, 0x486BU,
+ 0x496BU, 0x4A6BU, 0x4B6BU, 0x4C6BU, 0x4D6BU, 0x4E6BU, 0x4F6BU, 0x506BU,
+ 0x516BU, 0x526BU, 0x536BU, 0x546BU, 0x556BU, 0x566BU, 0x576BU, 0x586BU,
+ 0x596BU, 0x5A6BU, 0x616BU, 0x626BU, 0x636BU, 0x646BU, 0x656BU, 0x666BU,
+ 0x676BU, 0x686BU, 0x696BU, 0x6A6BU, 0x6B6BU, 0x6C6BU, 0x6D6BU, 0x6E6BU,
+ 0x6F6BU, 0x706BU, 0x716BU, 0x726BU, 0x736BU, 0x746BU, 0x756BU, 0x766BU,
+ 0x776BU, 0x786BU, 0x796BU, 0x7A6BU, 0x306BU, 0x316BU, 0x326BU, 0x336BU,
+ 0x346BU, 0x356BU, 0x366BU, 0x376BU, 0x386BU, 0x396BU, 0x2B6BU, 0x2F6BU,
+ 0x416CU, 0x426CU, 0x436CU, 0x446CU, 0x456CU, 0x466CU, 0x476CU, 0x486CU,
+ 0x496CU, 0x4A6CU, 0x4B6CU, 0x4C6CU, 0x4D6CU, 0x4E6CU, 0x4F6CU, 0x506CU,
+ 0x516CU, 0x526CU, 0x536CU, 0x546CU, 0x556CU, 0x566CU, 0x576CU, 0x586CU,
+ 0x596CU, 0x5A6CU, 0x616CU, 0x626CU, 0x636CU, 0x646CU, 0x656CU, 0x666CU,
+ 0x676CU, 0x686CU, 0x696CU, 0x6A6CU, 0x6B6CU, 0x6C6CU, 0x6D6CU, 0x6E6CU,
+ 0x6F6CU, 0x706CU, 0x716CU, 0x726CU, 0x736CU, 0x746CU, 0x756CU, 0x766CU,
+ 0x776CU, 0x786CU, 0x796CU, 0x7A6CU, 0x306CU, 0x316CU, 0x326CU, 0x336CU,
+ 0x346CU, 0x356CU, 0x366CU, 0x376CU, 0x386CU, 0x396CU, 0x2B6CU, 0x2F6CU,
+ 0x416DU, 0x426DU, 0x436DU, 0x446DU, 0x456DU, 0x466DU, 0x476DU, 0x486DU,
+ 0x496DU, 0x4A6DU, 0x4B6DU, 0x4C6DU, 0x4D6DU, 0x4E6DU, 0x4F6DU, 0x506DU,
+ 0x516DU, 0x526DU, 0x536DU, 0x546DU, 0x556DU, 0x566DU, 0x576DU, 0x586DU,
+ 0x596DU, 0x5A6DU, 0x616DU, 0x626DU, 0x636DU, 0x646DU, 0x656DU, 0x666DU,
+ 0x676DU, 0x686DU, 0x696DU, 0x6A6DU, 0x6B6DU, 0x6C6DU, 0x6D6DU, 0x6E6DU,
+ 0x6F6DU, 0x706DU, 0x716DU, 0x726DU, 0x736DU, 0x746DU, 0x756DU, 0x766DU,
+ 0x776DU, 0x786DU, 0x796DU, 0x7A6DU, 0x306DU, 0x316DU, 0x326DU, 0x336DU,
+ 0x346DU, 0x356DU, 0x366DU, 0x376DU, 0x386DU, 0x396DU, 0x2B6DU, 0x2F6DU,
+ 0x416EU, 0x426EU, 0x436EU, 0x446EU, 0x456EU, 0x466EU, 0x476EU, 0x486EU,
+ 0x496EU, 0x4A6EU, 0x4B6EU, 0x4C6EU, 0x4D6EU, 0x4E6EU, 0x4F6EU, 0x506EU,
+ 0x516EU, 0x526EU, 0x536EU, 0x546EU, 0x556EU, 0x566EU, 0x576EU, 0x586EU,
+ 0x596EU, 0x5A6EU, 0x616EU, 0x626EU, 0x636EU, 0x646EU, 0x656EU, 0x666EU,
+ 0x676EU, 0x686EU, 0x696EU, 0x6A6EU, 0x6B6EU, 0x6C6EU, 0x6D6EU, 0x6E6EU,
+ 0x6F6EU, 0x706EU, 0x716EU, 0x726EU, 0x736EU, 0x746EU, 0x756EU, 0x766EU,
+ 0x776EU, 0x786EU, 0x796EU, 0x7A6EU, 0x306EU, 0x316EU, 0x326EU, 0x336EU,
+ 0x346EU, 0x356EU, 0x366EU, 0x376EU, 0x386EU, 0x396EU, 0x2B6EU, 0x2F6EU,
+ 0x416FU, 0x426FU, 0x436FU, 0x446FU, 0x456FU, 0x466FU, 0x476FU, 0x486FU,
+ 0x496FU, 0x4A6FU, 0x4B6FU, 0x4C6FU, 0x4D6FU, 0x4E6FU, 0x4F6FU, 0x506FU,
+ 0x516FU, 0x526FU, 0x536FU, 0x546FU, 0x556FU, 0x566FU, 0x576FU, 0x586FU,
+ 0x596FU, 0x5A6FU, 0x616FU, 0x626FU, 0x636FU, 0x646FU, 0x656FU, 0x666FU,
+ 0x676FU, 0x686FU, 0x696FU, 0x6A6FU, 0x6B6FU, 0x6C6FU, 0x6D6FU, 0x6E6FU,
+ 0x6F6FU, 0x706FU, 0x716FU, 0x726FU, 0x736FU, 0x746FU, 0x756FU, 0x766FU,
+ 0x776FU, 0x786FU, 0x796FU, 0x7A6FU, 0x306FU, 0x316FU, 0x326FU, 0x336FU,
+ 0x346FU, 0x356FU, 0x366FU, 0x376FU, 0x386FU, 0x396FU, 0x2B6FU, 0x2F6FU,
+ 0x4170U, 0x4270U, 0x4370U, 0x4470U, 0x4570U, 0x4670U, 0x4770U, 0x4870U,
+ 0x4970U, 0x4A70U, 0x4B70U, 0x4C70U, 0x4D70U, 0x4E70U, 0x4F70U, 0x5070U,
+ 0x5170U, 0x5270U, 0x5370U, 0x5470U, 0x5570U, 0x5670U, 0x5770U, 0x5870U,
+ 0x5970U, 0x5A70U, 0x6170U, 0x6270U, 0x6370U, 0x6470U, 0x6570U, 0x6670U,
+ 0x6770U, 0x6870U, 0x6970U, 0x6A70U, 0x6B70U, 0x6C70U, 0x6D70U, 0x6E70U,
+ 0x6F70U, 0x7070U, 0x7170U, 0x7270U, 0x7370U, 0x7470U, 0x7570U, 0x7670U,
+ 0x7770U, 0x7870U, 0x7970U, 0x7A70U, 0x3070U, 0x3170U, 0x3270U, 0x3370U,
+ 0x3470U, 0x3570U, 0x3670U, 0x3770U, 0x3870U, 0x3970U, 0x2B70U, 0x2F70U,
+ 0x4171U, 0x4271U, 0x4371U, 0x4471U, 0x4571U, 0x4671U, 0x4771U, 0x4871U,
+ 0x4971U, 0x4A71U, 0x4B71U, 0x4C71U, 0x4D71U, 0x4E71U, 0x4F71U, 0x5071U,
+ 0x5171U, 0x5271U, 0x5371U, 0x5471U, 0x5571U, 0x5671U, 0x5771U, 0x5871U,
+ 0x5971U, 0x5A71U, 0x6171U, 0x6271U, 0x6371U, 0x6471U, 0x6571U, 0x6671U,
+ 0x6771U, 0x6871U, 0x6971U, 0x6A71U, 0x6B71U, 0x6C71U, 0x6D71U, 0x6E71U,
+ 0x6F71U, 0x7071U, 0x7171U, 0x7271U, 0x7371U, 0x7471U, 0x7571U, 0x7671U,
+ 0x7771U, 0x7871U, 0x7971U, 0x7A71U, 0x3071U, 0x3171U, 0x3271U, 0x3371U,
+ 0x3471U, 0x3571U, 0x3671U, 0x3771U, 0x3871U, 0x3971U, 0x2B71U, 0x2F71U,
+ 0x4172U, 0x4272U, 0x4372U, 0x4472U, 0x4572U, 0x4672U, 0x4772U, 0x4872U,
+ 0x4972U, 0x4A72U, 0x4B72U, 0x4C72U, 0x4D72U, 0x4E72U, 0x4F72U, 0x5072U,
+ 0x5172U, 0x5272U, 0x5372U, 0x5472U, 0x5572U, 0x5672U, 0x5772U, 0x5872U,
+ 0x5972U, 0x5A72U, 0x6172U, 0x6272U, 0x6372U, 0x6472U, 0x6572U, 0x6672U,
+ 0x6772U, 0x6872U, 0x6972U, 0x6A72U, 0x6B72U, 0x6C72U, 0x6D72U, 0x6E72U,
+ 0x6F72U, 0x7072U, 0x7172U, 0x7272U, 0x7372U, 0x7472U, 0x7572U, 0x7672U,
+ 0x7772U, 0x7872U, 0x7972U, 0x7A72U, 0x3072U, 0x3172U, 0x3272U, 0x3372U,
+ 0x3472U, 0x3572U, 0x3672U, 0x3772U, 0x3872U, 0x3972U, 0x2B72U, 0x2F72U,
+ 0x4173U, 0x4273U, 0x4373U, 0x4473U, 0x4573U, 0x4673U, 0x4773U, 0x4873U,
+ 0x4973U, 0x4A73U, 0x4B73U, 0x4C73U, 0x4D73U, 0x4E73U, 0x4F73U, 0x5073U,
+ 0x5173U, 0x5273U, 0x5373U, 0x5473U, 0x5573U, 0x5673U, 0x5773U, 0x5873U,
+ 0x5973U, 0x5A73U, 0x6173U, 0x6273U, 0x6373U, 0x6473U, 0x6573U, 0x6673U,
+ 0x6773U, 0x6873U, 0x6973U, 0x6A73U, 0x6B73U, 0x6C73U, 0x6D73U, 0x6E73U,
+ 0x6F73U, 0x7073U, 0x7173U, 0x7273U, 0x7373U, 0x7473U, 0x7573U, 0x7673U,
+ 0x7773U, 0x7873U, 0x7973U, 0x7A73U, 0x3073U, 0x3173U, 0x3273U, 0x3373U,
+ 0x3473U, 0x3573U, 0x3673U, 0x3773U, 0x3873U, 0x3973U, 0x2B73U, 0x2F73U,
+ 0x4174U, 0x4274U, 0x4374U, 0x4474U, 0x4574U, 0x4674U, 0x4774U, 0x4874U,
+ 0x4974U, 0x4A74U, 0x4B74U, 0x4C74U, 0x4D74U, 0x4E74U, 0x4F74U, 0x5074U,
+ 0x5174U, 0x5274U, 0x5374U, 0x5474U, 0x5574U, 0x5674U, 0x5774U, 0x5874U,
+ 0x5974U, 0x5A74U, 0x6174U, 0x6274U, 0x6374U, 0x6474U, 0x6574U, 0x6674U,
+ 0x6774U, 0x6874U, 0x6974U, 0x6A74U, 0x6B74U, 0x6C74U, 0x6D74U, 0x6E74U,
+ 0x6F74U, 0x7074U, 0x7174U, 0x7274U, 0x7374U, 0x7474U, 0x7574U, 0x7674U,
+ 0x7774U, 0x7874U, 0x7974U, 0x7A74U, 0x3074U, 0x3174U, 0x3274U, 0x3374U,
+ 0x3474U, 0x3574U, 0x3674U, 0x3774U, 0x3874U, 0x3974U, 0x2B74U, 0x2F74U,
+ 0x4175U, 0x4275U, 0x4375U, 0x4475U, 0x4575U, 0x4675U, 0x4775U, 0x4875U,
+ 0x4975U, 0x4A75U, 0x4B75U, 0x4C75U, 0x4D75U, 0x4E75U, 0x4F75U, 0x5075U,
+ 0x5175U, 0x5275U, 0x5375U, 0x5475U, 0x5575U, 0x5675U, 0x5775U, 0x5875U,
+ 0x5975U, 0x5A75U, 0x6175U, 0x6275U, 0x6375U, 0x6475U, 0x6575U, 0x6675U,
+ 0x6775U, 0x6875U, 0x6975U, 0x6A75U, 0x6B75U, 0x6C75U, 0x6D75U, 0x6E75U,
+ 0x6F75U, 0x7075U, 0x7175U, 0x7275U, 0x7375U, 0x7475U, 0x7575U, 0x7675U,
+ 0x7775U, 0x7875U, 0x7975U, 0x7A75U, 0x3075U, 0x3175U, 0x3275U, 0x3375U,
+ 0x3475U, 0x3575U, 0x3675U, 0x3775U, 0x3875U, 0x3975U, 0x2B75U, 0x2F75U,
+ 0x4176U, 0x4276U, 0x4376U, 0x4476U, 0x4576U, 0x4676U, 0x4776U, 0x4876U,
+ 0x4976U, 0x4A76U, 0x4B76U, 0x4C76U, 0x4D76U, 0x4E76U, 0x4F76U, 0x5076U,
+ 0x5176U, 0x5276U, 0x5376U, 0x5476U, 0x5576U, 0x5676U, 0x5776U, 0x5876U,
+ 0x5976U, 0x5A76U, 0x6176U, 0x6276U, 0x6376U, 0x6476U, 0x6576U, 0x6676U,
+ 0x6776U, 0x6876U, 0x6976U, 0x6A76U, 0x6B76U, 0x6C76U, 0x6D76U, 0x6E76U,
+ 0x6F76U, 0x7076U, 0x7176U, 0x7276U, 0x7376U, 0x7476U, 0x7576U, 0x7676U,
+ 0x7776U, 0x7876U, 0x7976U, 0x7A76U, 0x3076U, 0x3176U, 0x3276U, 0x3376U,
+ 0x3476U, 0x3576U, 0x3676U, 0x3776U, 0x3876U, 0x3976U, 0x2B76U, 0x2F76U,
+ 0x4177U, 0x4277U, 0x4377U, 0x4477U, 0x4577U, 0x4677U, 0x4777U, 0x4877U,
+ 0x4977U, 0x4A77U, 0x4B77U, 0x4C77U, 0x4D77U, 0x4E77U, 0x4F77U, 0x5077U,
+ 0x5177U, 0x5277U, 0x5377U, 0x5477U, 0x5577U, 0x5677U, 0x5777U, 0x5877U,
+ 0x5977U, 0x5A77U, 0x6177U, 0x6277U, 0x6377U, 0x6477U, 0x6577U, 0x6677U,
+ 0x6777U, 0x6877U, 0x6977U, 0x6A77U, 0x6B77U, 0x6C77U, 0x6D77U, 0x6E77U,
+ 0x6F77U, 0x7077U, 0x7177U, 0x7277U, 0x7377U, 0x7477U, 0x7577U, 0x7677U,
+ 0x7777U, 0x7877U, 0x7977U, 0x7A77U, 0x3077U, 0x3177U, 0x3277U, 0x3377U,
+ 0x3477U, 0x3577U, 0x3677U, 0x3777U, 0x3877U, 0x3977U, 0x2B77U, 0x2F77U,
+ 0x4178U, 0x4278U, 0x4378U, 0x4478U, 0x4578U, 0x4678U, 0x4778U, 0x4878U,
+ 0x4978U, 0x4A78U, 0x4B78U, 0x4C78U, 0x4D78U, 0x4E78U, 0x4F78U, 0x5078U,
+ 0x5178U, 0x5278U, 0x5378U, 0x5478U, 0x5578U, 0x5678U, 0x5778U, 0x5878U,
+ 0x5978U, 0x5A78U, 0x6178U, 0x6278U, 0x6378U, 0x6478U, 0x6578U, 0x6678U,
+ 0x6778U, 0x6878U, 0x6978U, 0x6A78U, 0x6B78U, 0x6C78U, 0x6D78U, 0x6E78U,
+ 0x6F78U, 0x7078U, 0x7178U, 0x7278U, 0x7378U, 0x7478U, 0x7578U, 0x7678U,
+ 0x7778U, 0x7878U, 0x7978U, 0x7A78U, 0x3078U, 0x3178U, 0x3278U, 0x3378U,
+ 0x3478U, 0x3578U, 0x3678U, 0x3778U, 0x3878U, 0x3978U, 0x2B78U, 0x2F78U,
+ 0x4179U, 0x4279U, 0x4379U, 0x4479U, 0x4579U, 0x4679U, 0x4779U, 0x4879U,
+ 0x4979U, 0x4A79U, 0x4B79U, 0x4C79U, 0x4D79U, 0x4E79U, 0x4F79U, 0x5079U,
+ 0x5179U, 0x5279U, 0x5379U, 0x5479U, 0x5579U, 0x5679U, 0x5779U, 0x5879U,
+ 0x5979U, 0x5A79U, 0x6179U, 0x6279U, 0x6379U, 0x6479U, 0x6579U, 0x6679U,
+ 0x6779U, 0x6879U, 0x6979U, 0x6A79U, 0x6B79U, 0x6C79U, 0x6D79U, 0x6E79U,
+ 0x6F79U, 0x7079U, 0x7179U, 0x7279U, 0x7379U, 0x7479U, 0x7579U, 0x7679U,
+ 0x7779U, 0x7879U, 0x7979U, 0x7A79U, 0x3079U, 0x3179U, 0x3279U, 0x3379U,
+ 0x3479U, 0x3579U, 0x3679U, 0x3779U, 0x3879U, 0x3979U, 0x2B79U, 0x2F79U,
+ 0x417AU, 0x427AU, 0x437AU, 0x447AU, 0x457AU, 0x467AU, 0x477AU, 0x487AU,
+ 0x497AU, 0x4A7AU, 0x4B7AU, 0x4C7AU, 0x4D7AU, 0x4E7AU, 0x4F7AU, 0x507AU,
+ 0x517AU, 0x527AU, 0x537AU, 0x547AU, 0x557AU, 0x567AU, 0x577AU, 0x587AU,
+ 0x597AU, 0x5A7AU, 0x617AU, 0x627AU, 0x637AU, 0x647AU, 0x657AU, 0x667AU,
+ 0x677AU, 0x687AU, 0x697AU, 0x6A7AU, 0x6B7AU, 0x6C7AU, 0x6D7AU, 0x6E7AU,
+ 0x6F7AU, 0x707AU, 0x717AU, 0x727AU, 0x737AU, 0x747AU, 0x757AU, 0x767AU,
+ 0x777AU, 0x787AU, 0x797AU, 0x7A7AU, 0x307AU, 0x317AU, 0x327AU, 0x337AU,
+ 0x347AU, 0x357AU, 0x367AU, 0x377AU, 0x387AU, 0x397AU, 0x2B7AU, 0x2F7AU,
+ 0x4130U, 0x4230U, 0x4330U, 0x4430U, 0x4530U, 0x4630U, 0x4730U, 0x4830U,
+ 0x4930U, 0x4A30U, 0x4B30U, 0x4C30U, 0x4D30U, 0x4E30U, 0x4F30U, 0x5030U,
+ 0x5130U, 0x5230U, 0x5330U, 0x5430U, 0x5530U, 0x5630U, 0x5730U, 0x5830U,
+ 0x5930U, 0x5A30U, 0x6130U, 0x6230U, 0x6330U, 0x6430U, 0x6530U, 0x6630U,
+ 0x6730U, 0x6830U, 0x6930U, 0x6A30U, 0x6B30U, 0x6C30U, 0x6D30U, 0x6E30U,
+ 0x6F30U, 0x7030U, 0x7130U, 0x7230U, 0x7330U, 0x7430U, 0x7530U, 0x7630U,
+ 0x7730U, 0x7830U, 0x7930U, 0x7A30U, 0x3030U, 0x3130U, 0x3230U, 0x3330U,
+ 0x3430U, 0x3530U, 0x3630U, 0x3730U, 0x3830U, 0x3930U, 0x2B30U, 0x2F30U,
+ 0x4131U, 0x4231U, 0x4331U, 0x4431U, 0x4531U, 0x4631U, 0x4731U, 0x4831U,
+ 0x4931U, 0x4A31U, 0x4B31U, 0x4C31U, 0x4D31U, 0x4E31U, 0x4F31U, 0x5031U,
+ 0x5131U, 0x5231U, 0x5331U, 0x5431U, 0x5531U, 0x5631U, 0x5731U, 0x5831U,
+ 0x5931U, 0x5A31U, 0x6131U, 0x6231U, 0x6331U, 0x6431U, 0x6531U, 0x6631U,
+ 0x6731U, 0x6831U, 0x6931U, 0x6A31U, 0x6B31U, 0x6C31U, 0x6D31U, 0x6E31U,
+ 0x6F31U, 0x7031U, 0x7131U, 0x7231U, 0x7331U, 0x7431U, 0x7531U, 0x7631U,
+ 0x7731U, 0x7831U, 0x7931U, 0x7A31U, 0x3031U, 0x3131U, 0x3231U, 0x3331U,
+ 0x3431U, 0x3531U, 0x3631U, 0x3731U, 0x3831U, 0x3931U, 0x2B31U, 0x2F31U,
+ 0x4132U, 0x4232U, 0x4332U, 0x4432U, 0x4532U, 0x4632U, 0x4732U, 0x4832U,
+ 0x4932U, 0x4A32U, 0x4B32U, 0x4C32U, 0x4D32U, 0x4E32U, 0x4F32U, 0x5032U,
+ 0x5132U, 0x5232U, 0x5332U, 0x5432U, 0x5532U, 0x5632U, 0x5732U, 0x5832U,
+ 0x5932U, 0x5A32U, 0x6132U, 0x6232U, 0x6332U, 0x6432U, 0x6532U, 0x6632U,
+ 0x6732U, 0x6832U, 0x6932U, 0x6A32U, 0x6B32U, 0x6C32U, 0x6D32U, 0x6E32U,
+ 0x6F32U, 0x7032U, 0x7132U, 0x7232U, 0x7332U, 0x7432U, 0x7532U, 0x7632U,
+ 0x7732U, 0x7832U, 0x7932U, 0x7A32U, 0x3032U, 0x3132U, 0x3232U, 0x3332U,
+ 0x3432U, 0x3532U, 0x3632U, 0x3732U, 0x3832U, 0x3932U, 0x2B32U, 0x2F32U,
+ 0x4133U, 0x4233U, 0x4333U, 0x4433U, 0x4533U, 0x4633U, 0x4733U, 0x4833U,
+ 0x4933U, 0x4A33U, 0x4B33U, 0x4C33U, 0x4D33U, 0x4E33U, 0x4F33U, 0x5033U,
+ 0x5133U, 0x5233U, 0x5333U, 0x5433U, 0x5533U, 0x5633U, 0x5733U, 0x5833U,
+ 0x5933U, 0x5A33U, 0x6133U, 0x6233U, 0x6333U, 0x6433U, 0x6533U, 0x6633U,
+ 0x6733U, 0x6833U, 0x6933U, 0x6A33U, 0x6B33U, 0x6C33U, 0x6D33U, 0x6E33U,
+ 0x6F33U, 0x7033U, 0x7133U, 0x7233U, 0x7333U, 0x7433U, 0x7533U, 0x7633U,
+ 0x7733U, 0x7833U, 0x7933U, 0x7A33U, 0x3033U, 0x3133U, 0x3233U, 0x3333U,
+ 0x3433U, 0x3533U, 0x3633U, 0x3733U, 0x3833U, 0x3933U, 0x2B33U, 0x2F33U,
+ 0x4134U, 0x4234U, 0x4334U, 0x4434U, 0x4534U, 0x4634U, 0x4734U, 0x4834U,
+ 0x4934U, 0x4A34U, 0x4B34U, 0x4C34U, 0x4D34U, 0x4E34U, 0x4F34U, 0x5034U,
+ 0x5134U, 0x5234U, 0x5334U, 0x5434U, 0x5534U, 0x5634U, 0x5734U, 0x5834U,
+ 0x5934U, 0x5A34U, 0x6134U, 0x6234U, 0x6334U, 0x6434U, 0x6534U, 0x6634U,
+ 0x6734U, 0x6834U, 0x6934U, 0x6A34U, 0x6B34U, 0x6C34U, 0x6D34U, 0x6E34U,
+ 0x6F34U, 0x7034U, 0x7134U, 0x7234U, 0x7334U, 0x7434U, 0x7534U, 0x7634U,
+ 0x7734U, 0x7834U, 0x7934U, 0x7A34U, 0x3034U, 0x3134U, 0x3234U, 0x3334U,
+ 0x3434U, 0x3534U, 0x3634U, 0x3734U, 0x3834U, 0x3934U, 0x2B34U, 0x2F34U,
+ 0x4135U, 0x4235U, 0x4335U, 0x4435U, 0x4535U, 0x4635U, 0x4735U, 0x4835U,
+ 0x4935U, 0x4A35U, 0x4B35U, 0x4C35U, 0x4D35U, 0x4E35U, 0x4F35U, 0x5035U,
+ 0x5135U, 0x5235U, 0x5335U, 0x5435U, 0x5535U, 0x5635U, 0x5735U, 0x5835U,
+ 0x5935U, 0x5A35U, 0x6135U, 0x6235U, 0x6335U, 0x6435U, 0x6535U, 0x6635U,
+ 0x6735U, 0x6835U, 0x6935U, 0x6A35U, 0x6B35U, 0x6C35U, 0x6D35U, 0x6E35U,
+ 0x6F35U, 0x7035U, 0x7135U, 0x7235U, 0x7335U, 0x7435U, 0x7535U, 0x7635U,
+ 0x7735U, 0x7835U, 0x7935U, 0x7A35U, 0x3035U, 0x3135U, 0x3235U, 0x3335U,
+ 0x3435U, 0x3535U, 0x3635U, 0x3735U, 0x3835U, 0x3935U, 0x2B35U, 0x2F35U,
+ 0x4136U, 0x4236U, 0x4336U, 0x4436U, 0x4536U, 0x4636U, 0x4736U, 0x4836U,
+ 0x4936U, 0x4A36U, 0x4B36U, 0x4C36U, 0x4D36U, 0x4E36U, 0x4F36U, 0x5036U,
+ 0x5136U, 0x5236U, 0x5336U, 0x5436U, 0x5536U, 0x5636U, 0x5736U, 0x5836U,
+ 0x5936U, 0x5A36U, 0x6136U, 0x6236U, 0x6336U, 0x6436U, 0x6536U, 0x6636U,
+ 0x6736U, 0x6836U, 0x6936U, 0x6A36U, 0x6B36U, 0x6C36U, 0x6D36U, 0x6E36U,
+ 0x6F36U, 0x7036U, 0x7136U, 0x7236U, 0x7336U, 0x7436U, 0x7536U, 0x7636U,
+ 0x7736U, 0x7836U, 0x7936U, 0x7A36U, 0x3036U, 0x3136U, 0x3236U, 0x3336U,
+ 0x3436U, 0x3536U, 0x3636U, 0x3736U, 0x3836U, 0x3936U, 0x2B36U, 0x2F36U,
+ 0x4137U, 0x4237U, 0x4337U, 0x4437U, 0x4537U, 0x4637U, 0x4737U, 0x4837U,
+ 0x4937U, 0x4A37U, 0x4B37U, 0x4C37U, 0x4D37U, 0x4E37U, 0x4F37U, 0x5037U,
+ 0x5137U, 0x5237U, 0x5337U, 0x5437U, 0x5537U, 0x5637U, 0x5737U, 0x5837U,
+ 0x5937U, 0x5A37U, 0x6137U, 0x6237U, 0x6337U, 0x6437U, 0x6537U, 0x6637U,
+ 0x6737U, 0x6837U, 0x6937U, 0x6A37U, 0x6B37U, 0x6C37U, 0x6D37U, 0x6E37U,
+ 0x6F37U, 0x7037U, 0x7137U, 0x7237U, 0x7337U, 0x7437U, 0x7537U, 0x7637U,
+ 0x7737U, 0x7837U, 0x7937U, 0x7A37U, 0x3037U, 0x3137U, 0x3237U, 0x3337U,
+ 0x3437U, 0x3537U, 0x3637U, 0x3737U, 0x3837U, 0x3937U, 0x2B37U, 0x2F37U,
+ 0x4138U, 0x4238U, 0x4338U, 0x4438U, 0x4538U, 0x4638U, 0x4738U, 0x4838U,
+ 0x4938U, 0x4A38U, 0x4B38U, 0x4C38U, 0x4D38U, 0x4E38U, 0x4F38U, 0x5038U,
+ 0x5138U, 0x5238U, 0x5338U, 0x5438U, 0x5538U, 0x5638U, 0x5738U, 0x5838U,
+ 0x5938U, 0x5A38U, 0x6138U, 0x6238U, 0x6338U, 0x6438U, 0x6538U, 0x6638U,
+ 0x6738U, 0x6838U, 0x6938U, 0x6A38U, 0x6B38U, 0x6C38U, 0x6D38U, 0x6E38U,
+ 0x6F38U, 0x7038U, 0x7138U, 0x7238U, 0x7338U, 0x7438U, 0x7538U, 0x7638U,
+ 0x7738U, 0x7838U, 0x7938U, 0x7A38U, 0x3038U, 0x3138U, 0x3238U, 0x3338U,
+ 0x3438U, 0x3538U, 0x3638U, 0x3738U, 0x3838U, 0x3938U, 0x2B38U, 0x2F38U,
+ 0x4139U, 0x4239U, 0x4339U, 0x4439U, 0x4539U, 0x4639U, 0x4739U, 0x4839U,
+ 0x4939U, 0x4A39U, 0x4B39U, 0x4C39U, 0x4D39U, 0x4E39U, 0x4F39U, 0x5039U,
+ 0x5139U, 0x5239U, 0x5339U, 0x5439U, 0x5539U, 0x5639U, 0x5739U, 0x5839U,
+ 0x5939U, 0x5A39U, 0x6139U, 0x6239U, 0x6339U, 0x6439U, 0x6539U, 0x6639U,
+ 0x6739U, 0x6839U, 0x6939U, 0x6A39U, 0x6B39U, 0x6C39U, 0x6D39U, 0x6E39U,
+ 0x6F39U, 0x7039U, 0x7139U, 0x7239U, 0x7339U, 0x7439U, 0x7539U, 0x7639U,
+ 0x7739U, 0x7839U, 0x7939U, 0x7A39U, 0x3039U, 0x3139U, 0x3239U, 0x3339U,
+ 0x3439U, 0x3539U, 0x3639U, 0x3739U, 0x3839U, 0x3939U, 0x2B39U, 0x2F39U,
+ 0x412BU, 0x422BU, 0x432BU, 0x442BU, 0x452BU, 0x462BU, 0x472BU, 0x482BU,
+ 0x492BU, 0x4A2BU, 0x4B2BU, 0x4C2BU, 0x4D2BU, 0x4E2BU, 0x4F2BU, 0x502BU,
+ 0x512BU, 0x522BU, 0x532BU, 0x542BU, 0x552BU, 0x562BU, 0x572BU, 0x582BU,
+ 0x592BU, 0x5A2BU, 0x612BU, 0x622BU, 0x632BU, 0x642BU, 0x652BU, 0x662BU,
+ 0x672BU, 0x682BU, 0x692BU, 0x6A2BU, 0x6B2BU, 0x6C2BU, 0x6D2BU, 0x6E2BU,
+ 0x6F2BU, 0x702BU, 0x712BU, 0x722BU, 0x732BU, 0x742BU, 0x752BU, 0x762BU,
+ 0x772BU, 0x782BU, 0x792BU, 0x7A2BU, 0x302BU, 0x312BU, 0x322BU, 0x332BU,
+ 0x342BU, 0x352BU, 0x362BU, 0x372BU, 0x382BU, 0x392BU, 0x2B2BU, 0x2F2BU,
+ 0x412FU, 0x422FU, 0x432FU, 0x442FU, 0x452FU, 0x462FU, 0x472FU, 0x482FU,
+ 0x492FU, 0x4A2FU, 0x4B2FU, 0x4C2FU, 0x4D2FU, 0x4E2FU, 0x4F2FU, 0x502FU,
+ 0x512FU, 0x522FU, 0x532FU, 0x542FU, 0x552FU, 0x562FU, 0x572FU, 0x582FU,
+ 0x592FU, 0x5A2FU, 0x612FU, 0x622FU, 0x632FU, 0x642FU, 0x652FU, 0x662FU,
+ 0x672FU, 0x682FU, 0x692FU, 0x6A2FU, 0x6B2FU, 0x6C2FU, 0x6D2FU, 0x6E2FU,
+ 0x6F2FU, 0x702FU, 0x712FU, 0x722FU, 0x732FU, 0x742FU, 0x752FU, 0x762FU,
+ 0x772FU, 0x782FU, 0x792FU, 0x7A2FU, 0x302FU, 0x312FU, 0x322FU, 0x332FU,
+ 0x342FU, 0x352FU, 0x362FU, 0x372FU, 0x382FU, 0x392FU, 0x2B2FU, 0x2F2FU,
+#else
+ 0x4141U, 0x4142U, 0x4143U, 0x4144U, 0x4145U, 0x4146U, 0x4147U, 0x4148U,
+ 0x4149U, 0x414AU, 0x414BU, 0x414CU, 0x414DU, 0x414EU, 0x414FU, 0x4150U,
+ 0x4151U, 0x4152U, 0x4153U, 0x4154U, 0x4155U, 0x4156U, 0x4157U, 0x4158U,
+ 0x4159U, 0x415AU, 0x4161U, 0x4162U, 0x4163U, 0x4164U, 0x4165U, 0x4166U,
+ 0x4167U, 0x4168U, 0x4169U, 0x416AU, 0x416BU, 0x416CU, 0x416DU, 0x416EU,
+ 0x416FU, 0x4170U, 0x4171U, 0x4172U, 0x4173U, 0x4174U, 0x4175U, 0x4176U,
+ 0x4177U, 0x4178U, 0x4179U, 0x417AU, 0x4130U, 0x4131U, 0x4132U, 0x4133U,
+ 0x4134U, 0x4135U, 0x4136U, 0x4137U, 0x4138U, 0x4139U, 0x412BU, 0x412FU,
+ 0x4241U, 0x4242U, 0x4243U, 0x4244U, 0x4245U, 0x4246U, 0x4247U, 0x4248U,
+ 0x4249U, 0x424AU, 0x424BU, 0x424CU, 0x424DU, 0x424EU, 0x424FU, 0x4250U,
+ 0x4251U, 0x4252U, 0x4253U, 0x4254U, 0x4255U, 0x4256U, 0x4257U, 0x4258U,
+ 0x4259U, 0x425AU, 0x4261U, 0x4262U, 0x4263U, 0x4264U, 0x4265U, 0x4266U,
+ 0x4267U, 0x4268U, 0x4269U, 0x426AU, 0x426BU, 0x426CU, 0x426DU, 0x426EU,
+ 0x426FU, 0x4270U, 0x4271U, 0x4272U, 0x4273U, 0x4274U, 0x4275U, 0x4276U,
+ 0x4277U, 0x4278U, 0x4279U, 0x427AU, 0x4230U, 0x4231U, 0x4232U, 0x4233U,
+ 0x4234U, 0x4235U, 0x4236U, 0x4237U, 0x4238U, 0x4239U, 0x422BU, 0x422FU,
+ 0x4341U, 0x4342U, 0x4343U, 0x4344U, 0x4345U, 0x4346U, 0x4347U, 0x4348U,
+ 0x4349U, 0x434AU, 0x434BU, 0x434CU, 0x434DU, 0x434EU, 0x434FU, 0x4350U,
+ 0x4351U, 0x4352U, 0x4353U, 0x4354U, 0x4355U, 0x4356U, 0x4357U, 0x4358U,
+ 0x4359U, 0x435AU, 0x4361U, 0x4362U, 0x4363U, 0x4364U, 0x4365U, 0x4366U,
+ 0x4367U, 0x4368U, 0x4369U, 0x436AU, 0x436BU, 0x436CU, 0x436DU, 0x436EU,
+ 0x436FU, 0x4370U, 0x4371U, 0x4372U, 0x4373U, 0x4374U, 0x4375U, 0x4376U,
+ 0x4377U, 0x4378U, 0x4379U, 0x437AU, 0x4330U, 0x4331U, 0x4332U, 0x4333U,
+ 0x4334U, 0x4335U, 0x4336U, 0x4337U, 0x4338U, 0x4339U, 0x432BU, 0x432FU,
+ 0x4441U, 0x4442U, 0x4443U, 0x4444U, 0x4445U, 0x4446U, 0x4447U, 0x4448U,
+ 0x4449U, 0x444AU, 0x444BU, 0x444CU, 0x444DU, 0x444EU, 0x444FU, 0x4450U,
+ 0x4451U, 0x4452U, 0x4453U, 0x4454U, 0x4455U, 0x4456U, 0x4457U, 0x4458U,
+ 0x4459U, 0x445AU, 0x4461U, 0x4462U, 0x4463U, 0x4464U, 0x4465U, 0x4466U,
+ 0x4467U, 0x4468U, 0x4469U, 0x446AU, 0x446BU, 0x446CU, 0x446DU, 0x446EU,
+ 0x446FU, 0x4470U, 0x4471U, 0x4472U, 0x4473U, 0x4474U, 0x4475U, 0x4476U,
+ 0x4477U, 0x4478U, 0x4479U, 0x447AU, 0x4430U, 0x4431U, 0x4432U, 0x4433U,
+ 0x4434U, 0x4435U, 0x4436U, 0x4437U, 0x4438U, 0x4439U, 0x442BU, 0x442FU,
+ 0x4541U, 0x4542U, 0x4543U, 0x4544U, 0x4545U, 0x4546U, 0x4547U, 0x4548U,
+ 0x4549U, 0x454AU, 0x454BU, 0x454CU, 0x454DU, 0x454EU, 0x454FU, 0x4550U,
+ 0x4551U, 0x4552U, 0x4553U, 0x4554U, 0x4555U, 0x4556U, 0x4557U, 0x4558U,
+ 0x4559U, 0x455AU, 0x4561U, 0x4562U, 0x4563U, 0x4564U, 0x4565U, 0x4566U,
+ 0x4567U, 0x4568U, 0x4569U, 0x456AU, 0x456BU, 0x456CU, 0x456DU, 0x456EU,
+ 0x456FU, 0x4570U, 0x4571U, 0x4572U, 0x4573U, 0x4574U, 0x4575U, 0x4576U,
+ 0x4577U, 0x4578U, 0x4579U, 0x457AU, 0x4530U, 0x4531U, 0x4532U, 0x4533U,
+ 0x4534U, 0x4535U, 0x4536U, 0x4537U, 0x4538U, 0x4539U, 0x452BU, 0x452FU,
+ 0x4641U, 0x4642U, 0x4643U, 0x4644U, 0x4645U, 0x4646U, 0x4647U, 0x4648U,
+ 0x4649U, 0x464AU, 0x464BU, 0x464CU, 0x464DU, 0x464EU, 0x464FU, 0x4650U,
+ 0x4651U, 0x4652U, 0x4653U, 0x4654U, 0x4655U, 0x4656U, 0x4657U, 0x4658U,
+ 0x4659U, 0x465AU, 0x4661U, 0x4662U, 0x4663U, 0x4664U, 0x4665U, 0x4666U,
+ 0x4667U, 0x4668U, 0x4669U, 0x466AU, 0x466BU, 0x466CU, 0x466DU, 0x466EU,
+ 0x466FU, 0x4670U, 0x4671U, 0x4672U, 0x4673U, 0x4674U, 0x4675U, 0x4676U,
+ 0x4677U, 0x4678U, 0x4679U, 0x467AU, 0x4630U, 0x4631U, 0x4632U, 0x4633U,
+ 0x4634U, 0x4635U, 0x4636U, 0x4637U, 0x4638U, 0x4639U, 0x462BU, 0x462FU,
+ 0x4741U, 0x4742U, 0x4743U, 0x4744U, 0x4745U, 0x4746U, 0x4747U, 0x4748U,
+ 0x4749U, 0x474AU, 0x474BU, 0x474CU, 0x474DU, 0x474EU, 0x474FU, 0x4750U,
+ 0x4751U, 0x4752U, 0x4753U, 0x4754U, 0x4755U, 0x4756U, 0x4757U, 0x4758U,
+ 0x4759U, 0x475AU, 0x4761U, 0x4762U, 0x4763U, 0x4764U, 0x4765U, 0x4766U,
+ 0x4767U, 0x4768U, 0x4769U, 0x476AU, 0x476BU, 0x476CU, 0x476DU, 0x476EU,
+ 0x476FU, 0x4770U, 0x4771U, 0x4772U, 0x4773U, 0x4774U, 0x4775U, 0x4776U,
+ 0x4777U, 0x4778U, 0x4779U, 0x477AU, 0x4730U, 0x4731U, 0x4732U, 0x4733U,
+ 0x4734U, 0x4735U, 0x4736U, 0x4737U, 0x4738U, 0x4739U, 0x472BU, 0x472FU,
+ 0x4841U, 0x4842U, 0x4843U, 0x4844U, 0x4845U, 0x4846U, 0x4847U, 0x4848U,
+ 0x4849U, 0x484AU, 0x484BU, 0x484CU, 0x484DU, 0x484EU, 0x484FU, 0x4850U,
+ 0x4851U, 0x4852U, 0x4853U, 0x4854U, 0x4855U, 0x4856U, 0x4857U, 0x4858U,
+ 0x4859U, 0x485AU, 0x4861U, 0x4862U, 0x4863U, 0x4864U, 0x4865U, 0x4866U,
+ 0x4867U, 0x4868U, 0x4869U, 0x486AU, 0x486BU, 0x486CU, 0x486DU, 0x486EU,
+ 0x486FU, 0x4870U, 0x4871U, 0x4872U, 0x4873U, 0x4874U, 0x4875U, 0x4876U,
+ 0x4877U, 0x4878U, 0x4879U, 0x487AU, 0x4830U, 0x4831U, 0x4832U, 0x4833U,
+ 0x4834U, 0x4835U, 0x4836U, 0x4837U, 0x4838U, 0x4839U, 0x482BU, 0x482FU,
+ 0x4941U, 0x4942U, 0x4943U, 0x4944U, 0x4945U, 0x4946U, 0x4947U, 0x4948U,
+ 0x4949U, 0x494AU, 0x494BU, 0x494CU, 0x494DU, 0x494EU, 0x494FU, 0x4950U,
+ 0x4951U, 0x4952U, 0x4953U, 0x4954U, 0x4955U, 0x4956U, 0x4957U, 0x4958U,
+ 0x4959U, 0x495AU, 0x4961U, 0x4962U, 0x4963U, 0x4964U, 0x4965U, 0x4966U,
+ 0x4967U, 0x4968U, 0x4969U, 0x496AU, 0x496BU, 0x496CU, 0x496DU, 0x496EU,
+ 0x496FU, 0x4970U, 0x4971U, 0x4972U, 0x4973U, 0x4974U, 0x4975U, 0x4976U,
+ 0x4977U, 0x4978U, 0x4979U, 0x497AU, 0x4930U, 0x4931U, 0x4932U, 0x4933U,
+ 0x4934U, 0x4935U, 0x4936U, 0x4937U, 0x4938U, 0x4939U, 0x492BU, 0x492FU,
+ 0x4A41U, 0x4A42U, 0x4A43U, 0x4A44U, 0x4A45U, 0x4A46U, 0x4A47U, 0x4A48U,
+ 0x4A49U, 0x4A4AU, 0x4A4BU, 0x4A4CU, 0x4A4DU, 0x4A4EU, 0x4A4FU, 0x4A50U,
+ 0x4A51U, 0x4A52U, 0x4A53U, 0x4A54U, 0x4A55U, 0x4A56U, 0x4A57U, 0x4A58U,
+ 0x4A59U, 0x4A5AU, 0x4A61U, 0x4A62U, 0x4A63U, 0x4A64U, 0x4A65U, 0x4A66U,
+ 0x4A67U, 0x4A68U, 0x4A69U, 0x4A6AU, 0x4A6BU, 0x4A6CU, 0x4A6DU, 0x4A6EU,
+ 0x4A6FU, 0x4A70U, 0x4A71U, 0x4A72U, 0x4A73U, 0x4A74U, 0x4A75U, 0x4A76U,
+ 0x4A77U, 0x4A78U, 0x4A79U, 0x4A7AU, 0x4A30U, 0x4A31U, 0x4A32U, 0x4A33U,
+ 0x4A34U, 0x4A35U, 0x4A36U, 0x4A37U, 0x4A38U, 0x4A39U, 0x4A2BU, 0x4A2FU,
+ 0x4B41U, 0x4B42U, 0x4B43U, 0x4B44U, 0x4B45U, 0x4B46U, 0x4B47U, 0x4B48U,
+ 0x4B49U, 0x4B4AU, 0x4B4BU, 0x4B4CU, 0x4B4DU, 0x4B4EU, 0x4B4FU, 0x4B50U,
+ 0x4B51U, 0x4B52U, 0x4B53U, 0x4B54U, 0x4B55U, 0x4B56U, 0x4B57U, 0x4B58U,
+ 0x4B59U, 0x4B5AU, 0x4B61U, 0x4B62U, 0x4B63U, 0x4B64U, 0x4B65U, 0x4B66U,
+ 0x4B67U, 0x4B68U, 0x4B69U, 0x4B6AU, 0x4B6BU, 0x4B6CU, 0x4B6DU, 0x4B6EU,
+ 0x4B6FU, 0x4B70U, 0x4B71U, 0x4B72U, 0x4B73U, 0x4B74U, 0x4B75U, 0x4B76U,
+ 0x4B77U, 0x4B78U, 0x4B79U, 0x4B7AU, 0x4B30U, 0x4B31U, 0x4B32U, 0x4B33U,
+ 0x4B34U, 0x4B35U, 0x4B36U, 0x4B37U, 0x4B38U, 0x4B39U, 0x4B2BU, 0x4B2FU,
+ 0x4C41U, 0x4C42U, 0x4C43U, 0x4C44U, 0x4C45U, 0x4C46U, 0x4C47U, 0x4C48U,
+ 0x4C49U, 0x4C4AU, 0x4C4BU, 0x4C4CU, 0x4C4DU, 0x4C4EU, 0x4C4FU, 0x4C50U,
+ 0x4C51U, 0x4C52U, 0x4C53U, 0x4C54U, 0x4C55U, 0x4C56U, 0x4C57U, 0x4C58U,
+ 0x4C59U, 0x4C5AU, 0x4C61U, 0x4C62U, 0x4C63U, 0x4C64U, 0x4C65U, 0x4C66U,
+ 0x4C67U, 0x4C68U, 0x4C69U, 0x4C6AU, 0x4C6BU, 0x4C6CU, 0x4C6DU, 0x4C6EU,
+ 0x4C6FU, 0x4C70U, 0x4C71U, 0x4C72U, 0x4C73U, 0x4C74U, 0x4C75U, 0x4C76U,
+ 0x4C77U, 0x4C78U, 0x4C79U, 0x4C7AU, 0x4C30U, 0x4C31U, 0x4C32U, 0x4C33U,
+ 0x4C34U, 0x4C35U, 0x4C36U, 0x4C37U, 0x4C38U, 0x4C39U, 0x4C2BU, 0x4C2FU,
+ 0x4D41U, 0x4D42U, 0x4D43U, 0x4D44U, 0x4D45U, 0x4D46U, 0x4D47U, 0x4D48U,
+ 0x4D49U, 0x4D4AU, 0x4D4BU, 0x4D4CU, 0x4D4DU, 0x4D4EU, 0x4D4FU, 0x4D50U,
+ 0x4D51U, 0x4D52U, 0x4D53U, 0x4D54U, 0x4D55U, 0x4D56U, 0x4D57U, 0x4D58U,
+ 0x4D59U, 0x4D5AU, 0x4D61U, 0x4D62U, 0x4D63U, 0x4D64U, 0x4D65U, 0x4D66U,
+ 0x4D67U, 0x4D68U, 0x4D69U, 0x4D6AU, 0x4D6BU, 0x4D6CU, 0x4D6DU, 0x4D6EU,
+ 0x4D6FU, 0x4D70U, 0x4D71U, 0x4D72U, 0x4D73U, 0x4D74U, 0x4D75U, 0x4D76U,
+ 0x4D77U, 0x4D78U, 0x4D79U, 0x4D7AU, 0x4D30U, 0x4D31U, 0x4D32U, 0x4D33U,
+ 0x4D34U, 0x4D35U, 0x4D36U, 0x4D37U, 0x4D38U, 0x4D39U, 0x4D2BU, 0x4D2FU,
+ 0x4E41U, 0x4E42U, 0x4E43U, 0x4E44U, 0x4E45U, 0x4E46U, 0x4E47U, 0x4E48U,
+ 0x4E49U, 0x4E4AU, 0x4E4BU, 0x4E4CU, 0x4E4DU, 0x4E4EU, 0x4E4FU, 0x4E50U,
+ 0x4E51U, 0x4E52U, 0x4E53U, 0x4E54U, 0x4E55U, 0x4E56U, 0x4E57U, 0x4E58U,
+ 0x4E59U, 0x4E5AU, 0x4E61U, 0x4E62U, 0x4E63U, 0x4E64U, 0x4E65U, 0x4E66U,
+ 0x4E67U, 0x4E68U, 0x4E69U, 0x4E6AU, 0x4E6BU, 0x4E6CU, 0x4E6DU, 0x4E6EU,
+ 0x4E6FU, 0x4E70U, 0x4E71U, 0x4E72U, 0x4E73U, 0x4E74U, 0x4E75U, 0x4E76U,
+ 0x4E77U, 0x4E78U, 0x4E79U, 0x4E7AU, 0x4E30U, 0x4E31U, 0x4E32U, 0x4E33U,
+ 0x4E34U, 0x4E35U, 0x4E36U, 0x4E37U, 0x4E38U, 0x4E39U, 0x4E2BU, 0x4E2FU,
+ 0x4F41U, 0x4F42U, 0x4F43U, 0x4F44U, 0x4F45U, 0x4F46U, 0x4F47U, 0x4F48U,
+ 0x4F49U, 0x4F4AU, 0x4F4BU, 0x4F4CU, 0x4F4DU, 0x4F4EU, 0x4F4FU, 0x4F50U,
+ 0x4F51U, 0x4F52U, 0x4F53U, 0x4F54U, 0x4F55U, 0x4F56U, 0x4F57U, 0x4F58U,
+ 0x4F59U, 0x4F5AU, 0x4F61U, 0x4F62U, 0x4F63U, 0x4F64U, 0x4F65U, 0x4F66U,
+ 0x4F67U, 0x4F68U, 0x4F69U, 0x4F6AU, 0x4F6BU, 0x4F6CU, 0x4F6DU, 0x4F6EU,
+ 0x4F6FU, 0x4F70U, 0x4F71U, 0x4F72U, 0x4F73U, 0x4F74U, 0x4F75U, 0x4F76U,
+ 0x4F77U, 0x4F78U, 0x4F79U, 0x4F7AU, 0x4F30U, 0x4F31U, 0x4F32U, 0x4F33U,
+ 0x4F34U, 0x4F35U, 0x4F36U, 0x4F37U, 0x4F38U, 0x4F39U, 0x4F2BU, 0x4F2FU,
+ 0x5041U, 0x5042U, 0x5043U, 0x5044U, 0x5045U, 0x5046U, 0x5047U, 0x5048U,
+ 0x5049U, 0x504AU, 0x504BU, 0x504CU, 0x504DU, 0x504EU, 0x504FU, 0x5050U,
+ 0x5051U, 0x5052U, 0x5053U, 0x5054U, 0x5055U, 0x5056U, 0x5057U, 0x5058U,
+ 0x5059U, 0x505AU, 0x5061U, 0x5062U, 0x5063U, 0x5064U, 0x5065U, 0x5066U,
+ 0x5067U, 0x5068U, 0x5069U, 0x506AU, 0x506BU, 0x506CU, 0x506DU, 0x506EU,
+ 0x506FU, 0x5070U, 0x5071U, 0x5072U, 0x5073U, 0x5074U, 0x5075U, 0x5076U,
+ 0x5077U, 0x5078U, 0x5079U, 0x507AU, 0x5030U, 0x5031U, 0x5032U, 0x5033U,
+ 0x5034U, 0x5035U, 0x5036U, 0x5037U, 0x5038U, 0x5039U, 0x502BU, 0x502FU,
+ 0x5141U, 0x5142U, 0x5143U, 0x5144U, 0x5145U, 0x5146U, 0x5147U, 0x5148U,
+ 0x5149U, 0x514AU, 0x514BU, 0x514CU, 0x514DU, 0x514EU, 0x514FU, 0x5150U,
+ 0x5151U, 0x5152U, 0x5153U, 0x5154U, 0x5155U, 0x5156U, 0x5157U, 0x5158U,
+ 0x5159U, 0x515AU, 0x5161U, 0x5162U, 0x5163U, 0x5164U, 0x5165U, 0x5166U,
+ 0x5167U, 0x5168U, 0x5169U, 0x516AU, 0x516BU, 0x516CU, 0x516DU, 0x516EU,
+ 0x516FU, 0x5170U, 0x5171U, 0x5172U, 0x5173U, 0x5174U, 0x5175U, 0x5176U,
+ 0x5177U, 0x5178U, 0x5179U, 0x517AU, 0x5130U, 0x5131U, 0x5132U, 0x5133U,
+ 0x5134U, 0x5135U, 0x5136U, 0x5137U, 0x5138U, 0x5139U, 0x512BU, 0x512FU,
+ 0x5241U, 0x5242U, 0x5243U, 0x5244U, 0x5245U, 0x5246U, 0x5247U, 0x5248U,
+ 0x5249U, 0x524AU, 0x524BU, 0x524CU, 0x524DU, 0x524EU, 0x524FU, 0x5250U,
+ 0x5251U, 0x5252U, 0x5253U, 0x5254U, 0x5255U, 0x5256U, 0x5257U, 0x5258U,
+ 0x5259U, 0x525AU, 0x5261U, 0x5262U, 0x5263U, 0x5264U, 0x5265U, 0x5266U,
+ 0x5267U, 0x5268U, 0x5269U, 0x526AU, 0x526BU, 0x526CU, 0x526DU, 0x526EU,
+ 0x526FU, 0x5270U, 0x5271U, 0x5272U, 0x5273U, 0x5274U, 0x5275U, 0x5276U,
+ 0x5277U, 0x5278U, 0x5279U, 0x527AU, 0x5230U, 0x5231U, 0x5232U, 0x5233U,
+ 0x5234U, 0x5235U, 0x5236U, 0x5237U, 0x5238U, 0x5239U, 0x522BU, 0x522FU,
+ 0x5341U, 0x5342U, 0x5343U, 0x5344U, 0x5345U, 0x5346U, 0x5347U, 0x5348U,
+ 0x5349U, 0x534AU, 0x534BU, 0x534CU, 0x534DU, 0x534EU, 0x534FU, 0x5350U,
+ 0x5351U, 0x5352U, 0x5353U, 0x5354U, 0x5355U, 0x5356U, 0x5357U, 0x5358U,
+ 0x5359U, 0x535AU, 0x5361U, 0x5362U, 0x5363U, 0x5364U, 0x5365U, 0x5366U,
+ 0x5367U, 0x5368U, 0x5369U, 0x536AU, 0x536BU, 0x536CU, 0x536DU, 0x536EU,
+ 0x536FU, 0x5370U, 0x5371U, 0x5372U, 0x5373U, 0x5374U, 0x5375U, 0x5376U,
+ 0x5377U, 0x5378U, 0x5379U, 0x537AU, 0x5330U, 0x5331U, 0x5332U, 0x5333U,
+ 0x5334U, 0x5335U, 0x5336U, 0x5337U, 0x5338U, 0x5339U, 0x532BU, 0x532FU,
+ 0x5441U, 0x5442U, 0x5443U, 0x5444U, 0x5445U, 0x5446U, 0x5447U, 0x5448U,
+ 0x5449U, 0x544AU, 0x544BU, 0x544CU, 0x544DU, 0x544EU, 0x544FU, 0x5450U,
+ 0x5451U, 0x5452U, 0x5453U, 0x5454U, 0x5455U, 0x5456U, 0x5457U, 0x5458U,
+ 0x5459U, 0x545AU, 0x5461U, 0x5462U, 0x5463U, 0x5464U, 0x5465U, 0x5466U,
+ 0x5467U, 0x5468U, 0x5469U, 0x546AU, 0x546BU, 0x546CU, 0x546DU, 0x546EU,
+ 0x546FU, 0x5470U, 0x5471U, 0x5472U, 0x5473U, 0x5474U, 0x5475U, 0x5476U,
+ 0x5477U, 0x5478U, 0x5479U, 0x547AU, 0x5430U, 0x5431U, 0x5432U, 0x5433U,
+ 0x5434U, 0x5435U, 0x5436U, 0x5437U, 0x5438U, 0x5439U, 0x542BU, 0x542FU,
+ 0x5541U, 0x5542U, 0x5543U, 0x5544U, 0x5545U, 0x5546U, 0x5547U, 0x5548U,
+ 0x5549U, 0x554AU, 0x554BU, 0x554CU, 0x554DU, 0x554EU, 0x554FU, 0x5550U,
+ 0x5551U, 0x5552U, 0x5553U, 0x5554U, 0x5555U, 0x5556U, 0x5557U, 0x5558U,
+ 0x5559U, 0x555AU, 0x5561U, 0x5562U, 0x5563U, 0x5564U, 0x5565U, 0x5566U,
+ 0x5567U, 0x5568U, 0x5569U, 0x556AU, 0x556BU, 0x556CU, 0x556DU, 0x556EU,
+ 0x556FU, 0x5570U, 0x5571U, 0x5572U, 0x5573U, 0x5574U, 0x5575U, 0x5576U,
+ 0x5577U, 0x5578U, 0x5579U, 0x557AU, 0x5530U, 0x5531U, 0x5532U, 0x5533U,
+ 0x5534U, 0x5535U, 0x5536U, 0x5537U, 0x5538U, 0x5539U, 0x552BU, 0x552FU,
+ 0x5641U, 0x5642U, 0x5643U, 0x5644U, 0x5645U, 0x5646U, 0x5647U, 0x5648U,
+ 0x5649U, 0x564AU, 0x564BU, 0x564CU, 0x564DU, 0x564EU, 0x564FU, 0x5650U,
+ 0x5651U, 0x5652U, 0x5653U, 0x5654U, 0x5655U, 0x5656U, 0x5657U, 0x5658U,
+ 0x5659U, 0x565AU, 0x5661U, 0x5662U, 0x5663U, 0x5664U, 0x5665U, 0x5666U,
+ 0x5667U, 0x5668U, 0x5669U, 0x566AU, 0x566BU, 0x566CU, 0x566DU, 0x566EU,
+ 0x566FU, 0x5670U, 0x5671U, 0x5672U, 0x5673U, 0x5674U, 0x5675U, 0x5676U,
+ 0x5677U, 0x5678U, 0x5679U, 0x567AU, 0x5630U, 0x5631U, 0x5632U, 0x5633U,
+ 0x5634U, 0x5635U, 0x5636U, 0x5637U, 0x5638U, 0x5639U, 0x562BU, 0x562FU,
+ 0x5741U, 0x5742U, 0x5743U, 0x5744U, 0x5745U, 0x5746U, 0x5747U, 0x5748U,
+ 0x5749U, 0x574AU, 0x574BU, 0x574CU, 0x574DU, 0x574EU, 0x574FU, 0x5750U,
+ 0x5751U, 0x5752U, 0x5753U, 0x5754U, 0x5755U, 0x5756U, 0x5757U, 0x5758U,
+ 0x5759U, 0x575AU, 0x5761U, 0x5762U, 0x5763U, 0x5764U, 0x5765U, 0x5766U,
+ 0x5767U, 0x5768U, 0x5769U, 0x576AU, 0x576BU, 0x576CU, 0x576DU, 0x576EU,
+ 0x576FU, 0x5770U, 0x5771U, 0x5772U, 0x5773U, 0x5774U, 0x5775U, 0x5776U,
+ 0x5777U, 0x5778U, 0x5779U, 0x577AU, 0x5730U, 0x5731U, 0x5732U, 0x5733U,
+ 0x5734U, 0x5735U, 0x5736U, 0x5737U, 0x5738U, 0x5739U, 0x572BU, 0x572FU,
+ 0x5841U, 0x5842U, 0x5843U, 0x5844U, 0x5845U, 0x5846U, 0x5847U, 0x5848U,
+ 0x5849U, 0x584AU, 0x584BU, 0x584CU, 0x584DU, 0x584EU, 0x584FU, 0x5850U,
+ 0x5851U, 0x5852U, 0x5853U, 0x5854U, 0x5855U, 0x5856U, 0x5857U, 0x5858U,
+ 0x5859U, 0x585AU, 0x5861U, 0x5862U, 0x5863U, 0x5864U, 0x5865U, 0x5866U,
+ 0x5867U, 0x5868U, 0x5869U, 0x586AU, 0x586BU, 0x586CU, 0x586DU, 0x586EU,
+ 0x586FU, 0x5870U, 0x5871U, 0x5872U, 0x5873U, 0x5874U, 0x5875U, 0x5876U,
+ 0x5877U, 0x5878U, 0x5879U, 0x587AU, 0x5830U, 0x5831U, 0x5832U, 0x5833U,
+ 0x5834U, 0x5835U, 0x5836U, 0x5837U, 0x5838U, 0x5839U, 0x582BU, 0x582FU,
+ 0x5941U, 0x5942U, 0x5943U, 0x5944U, 0x5945U, 0x5946U, 0x5947U, 0x5948U,
+ 0x5949U, 0x594AU, 0x594BU, 0x594CU, 0x594DU, 0x594EU, 0x594FU, 0x5950U,
+ 0x5951U, 0x5952U, 0x5953U, 0x5954U, 0x5955U, 0x5956U, 0x5957U, 0x5958U,
+ 0x5959U, 0x595AU, 0x5961U, 0x5962U, 0x5963U, 0x5964U, 0x5965U, 0x5966U,
+ 0x5967U, 0x5968U, 0x5969U, 0x596AU, 0x596BU, 0x596CU, 0x596DU, 0x596EU,
+ 0x596FU, 0x5970U, 0x5971U, 0x5972U, 0x5973U, 0x5974U, 0x5975U, 0x5976U,
+ 0x5977U, 0x5978U, 0x5979U, 0x597AU, 0x5930U, 0x5931U, 0x5932U, 0x5933U,
+ 0x5934U, 0x5935U, 0x5936U, 0x5937U, 0x5938U, 0x5939U, 0x592BU, 0x592FU,
+ 0x5A41U, 0x5A42U, 0x5A43U, 0x5A44U, 0x5A45U, 0x5A46U, 0x5A47U, 0x5A48U,
+ 0x5A49U, 0x5A4AU, 0x5A4BU, 0x5A4CU, 0x5A4DU, 0x5A4EU, 0x5A4FU, 0x5A50U,
+ 0x5A51U, 0x5A52U, 0x5A53U, 0x5A54U, 0x5A55U, 0x5A56U, 0x5A57U, 0x5A58U,
+ 0x5A59U, 0x5A5AU, 0x5A61U, 0x5A62U, 0x5A63U, 0x5A64U, 0x5A65U, 0x5A66U,
+ 0x5A67U, 0x5A68U, 0x5A69U, 0x5A6AU, 0x5A6BU, 0x5A6CU, 0x5A6DU, 0x5A6EU,
+ 0x5A6FU, 0x5A70U, 0x5A71U, 0x5A72U, 0x5A73U, 0x5A74U, 0x5A75U, 0x5A76U,
+ 0x5A77U, 0x5A78U, 0x5A79U, 0x5A7AU, 0x5A30U, 0x5A31U, 0x5A32U, 0x5A33U,
+ 0x5A34U, 0x5A35U, 0x5A36U, 0x5A37U, 0x5A38U, 0x5A39U, 0x5A2BU, 0x5A2FU,
+ 0x6141U, 0x6142U, 0x6143U, 0x6144U, 0x6145U, 0x6146U, 0x6147U, 0x6148U,
+ 0x6149U, 0x614AU, 0x614BU, 0x614CU, 0x614DU, 0x614EU, 0x614FU, 0x6150U,
+ 0x6151U, 0x6152U, 0x6153U, 0x6154U, 0x6155U, 0x6156U, 0x6157U, 0x6158U,
+ 0x6159U, 0x615AU, 0x6161U, 0x6162U, 0x6163U, 0x6164U, 0x6165U, 0x6166U,
+ 0x6167U, 0x6168U, 0x6169U, 0x616AU, 0x616BU, 0x616CU, 0x616DU, 0x616EU,
+ 0x616FU, 0x6170U, 0x6171U, 0x6172U, 0x6173U, 0x6174U, 0x6175U, 0x6176U,
+ 0x6177U, 0x6178U, 0x6179U, 0x617AU, 0x6130U, 0x6131U, 0x6132U, 0x6133U,
+ 0x6134U, 0x6135U, 0x6136U, 0x6137U, 0x6138U, 0x6139U, 0x612BU, 0x612FU,
+ 0x6241U, 0x6242U, 0x6243U, 0x6244U, 0x6245U, 0x6246U, 0x6247U, 0x6248U,
+ 0x6249U, 0x624AU, 0x624BU, 0x624CU, 0x624DU, 0x624EU, 0x624FU, 0x6250U,
+ 0x6251U, 0x6252U, 0x6253U, 0x6254U, 0x6255U, 0x6256U, 0x6257U, 0x6258U,
+ 0x6259U, 0x625AU, 0x6261U, 0x6262U, 0x6263U, 0x6264U, 0x6265U, 0x6266U,
+ 0x6267U, 0x6268U, 0x6269U, 0x626AU, 0x626BU, 0x626CU, 0x626DU, 0x626EU,
+ 0x626FU, 0x6270U, 0x6271U, 0x6272U, 0x6273U, 0x6274U, 0x6275U, 0x6276U,
+ 0x6277U, 0x6278U, 0x6279U, 0x627AU, 0x6230U, 0x6231U, 0x6232U, 0x6233U,
+ 0x6234U, 0x6235U, 0x6236U, 0x6237U, 0x6238U, 0x6239U, 0x622BU, 0x622FU,
+ 0x6341U, 0x6342U, 0x6343U, 0x6344U, 0x6345U, 0x6346U, 0x6347U, 0x6348U,
+ 0x6349U, 0x634AU, 0x634BU, 0x634CU, 0x634DU, 0x634EU, 0x634FU, 0x6350U,
+ 0x6351U, 0x6352U, 0x6353U, 0x6354U, 0x6355U, 0x6356U, 0x6357U, 0x6358U,
+ 0x6359U, 0x635AU, 0x6361U, 0x6362U, 0x6363U, 0x6364U, 0x6365U, 0x6366U,
+ 0x6367U, 0x6368U, 0x6369U, 0x636AU, 0x636BU, 0x636CU, 0x636DU, 0x636EU,
+ 0x636FU, 0x6370U, 0x6371U, 0x6372U, 0x6373U, 0x6374U, 0x6375U, 0x6376U,
+ 0x6377U, 0x6378U, 0x6379U, 0x637AU, 0x6330U, 0x6331U, 0x6332U, 0x6333U,
+ 0x6334U, 0x6335U, 0x6336U, 0x6337U, 0x6338U, 0x6339U, 0x632BU, 0x632FU,
+ 0x6441U, 0x6442U, 0x6443U, 0x6444U, 0x6445U, 0x6446U, 0x6447U, 0x6448U,
+ 0x6449U, 0x644AU, 0x644BU, 0x644CU, 0x644DU, 0x644EU, 0x644FU, 0x6450U,
+ 0x6451U, 0x6452U, 0x6453U, 0x6454U, 0x6455U, 0x6456U, 0x6457U, 0x6458U,
+ 0x6459U, 0x645AU, 0x6461U, 0x6462U, 0x6463U, 0x6464U, 0x6465U, 0x6466U,
+ 0x6467U, 0x6468U, 0x6469U, 0x646AU, 0x646BU, 0x646CU, 0x646DU, 0x646EU,
+ 0x646FU, 0x6470U, 0x6471U, 0x6472U, 0x6473U, 0x6474U, 0x6475U, 0x6476U,
+ 0x6477U, 0x6478U, 0x6479U, 0x647AU, 0x6430U, 0x6431U, 0x6432U, 0x6433U,
+ 0x6434U, 0x6435U, 0x6436U, 0x6437U, 0x6438U, 0x6439U, 0x642BU, 0x642FU,
+ 0x6541U, 0x6542U, 0x6543U, 0x6544U, 0x6545U, 0x6546U, 0x6547U, 0x6548U,
+ 0x6549U, 0x654AU, 0x654BU, 0x654CU, 0x654DU, 0x654EU, 0x654FU, 0x6550U,
+ 0x6551U, 0x6552U, 0x6553U, 0x6554U, 0x6555U, 0x6556U, 0x6557U, 0x6558U,
+ 0x6559U, 0x655AU, 0x6561U, 0x6562U, 0x6563U, 0x6564U, 0x6565U, 0x6566U,
+ 0x6567U, 0x6568U, 0x6569U, 0x656AU, 0x656BU, 0x656CU, 0x656DU, 0x656EU,
+ 0x656FU, 0x6570U, 0x6571U, 0x6572U, 0x6573U, 0x6574U, 0x6575U, 0x6576U,
+ 0x6577U, 0x6578U, 0x6579U, 0x657AU, 0x6530U, 0x6531U, 0x6532U, 0x6533U,
+ 0x6534U, 0x6535U, 0x6536U, 0x6537U, 0x6538U, 0x6539U, 0x652BU, 0x652FU,
+ 0x6641U, 0x6642U, 0x6643U, 0x6644U, 0x6645U, 0x6646U, 0x6647U, 0x6648U,
+ 0x6649U, 0x664AU, 0x664BU, 0x664CU, 0x664DU, 0x664EU, 0x664FU, 0x6650U,
+ 0x6651U, 0x6652U, 0x6653U, 0x6654U, 0x6655U, 0x6656U, 0x6657U, 0x6658U,
+ 0x6659U, 0x665AU, 0x6661U, 0x6662U, 0x6663U, 0x6664U, 0x6665U, 0x6666U,
+ 0x6667U, 0x6668U, 0x6669U, 0x666AU, 0x666BU, 0x666CU, 0x666DU, 0x666EU,
+ 0x666FU, 0x6670U, 0x6671U, 0x6672U, 0x6673U, 0x6674U, 0x6675U, 0x6676U,
+ 0x6677U, 0x6678U, 0x6679U, 0x667AU, 0x6630U, 0x6631U, 0x6632U, 0x6633U,
+ 0x6634U, 0x6635U, 0x6636U, 0x6637U, 0x6638U, 0x6639U, 0x662BU, 0x662FU,
+ 0x6741U, 0x6742U, 0x6743U, 0x6744U, 0x6745U, 0x6746U, 0x6747U, 0x6748U,
+ 0x6749U, 0x674AU, 0x674BU, 0x674CU, 0x674DU, 0x674EU, 0x674FU, 0x6750U,
+ 0x6751U, 0x6752U, 0x6753U, 0x6754U, 0x6755U, 0x6756U, 0x6757U, 0x6758U,
+ 0x6759U, 0x675AU, 0x6761U, 0x6762U, 0x6763U, 0x6764U, 0x6765U, 0x6766U,
+ 0x6767U, 0x6768U, 0x6769U, 0x676AU, 0x676BU, 0x676CU, 0x676DU, 0x676EU,
+ 0x676FU, 0x6770U, 0x6771U, 0x6772U, 0x6773U, 0x6774U, 0x6775U, 0x6776U,
+ 0x6777U, 0x6778U, 0x6779U, 0x677AU, 0x6730U, 0x6731U, 0x6732U, 0x6733U,
+ 0x6734U, 0x6735U, 0x6736U, 0x6737U, 0x6738U, 0x6739U, 0x672BU, 0x672FU,
+ 0x6841U, 0x6842U, 0x6843U, 0x6844U, 0x6845U, 0x6846U, 0x6847U, 0x6848U,
+ 0x6849U, 0x684AU, 0x684BU, 0x684CU, 0x684DU, 0x684EU, 0x684FU, 0x6850U,
+ 0x6851U, 0x6852U, 0x6853U, 0x6854U, 0x6855U, 0x6856U, 0x6857U, 0x6858U,
+ 0x6859U, 0x685AU, 0x6861U, 0x6862U, 0x6863U, 0x6864U, 0x6865U, 0x6866U,
+ 0x6867U, 0x6868U, 0x6869U, 0x686AU, 0x686BU, 0x686CU, 0x686DU, 0x686EU,
+ 0x686FU, 0x6870U, 0x6871U, 0x6872U, 0x6873U, 0x6874U, 0x6875U, 0x6876U,
+ 0x6877U, 0x6878U, 0x6879U, 0x687AU, 0x6830U, 0x6831U, 0x6832U, 0x6833U,
+ 0x6834U, 0x6835U, 0x6836U, 0x6837U, 0x6838U, 0x6839U, 0x682BU, 0x682FU,
+ 0x6941U, 0x6942U, 0x6943U, 0x6944U, 0x6945U, 0x6946U, 0x6947U, 0x6948U,
+ 0x6949U, 0x694AU, 0x694BU, 0x694CU, 0x694DU, 0x694EU, 0x694FU, 0x6950U,
+ 0x6951U, 0x6952U, 0x6953U, 0x6954U, 0x6955U, 0x6956U, 0x6957U, 0x6958U,
+ 0x6959U, 0x695AU, 0x6961U, 0x6962U, 0x6963U, 0x6964U, 0x6965U, 0x6966U,
+ 0x6967U, 0x6968U, 0x6969U, 0x696AU, 0x696BU, 0x696CU, 0x696DU, 0x696EU,
+ 0x696FU, 0x6970U, 0x6971U, 0x6972U, 0x6973U, 0x6974U, 0x6975U, 0x6976U,
+ 0x6977U, 0x6978U, 0x6979U, 0x697AU, 0x6930U, 0x6931U, 0x6932U, 0x6933U,
+ 0x6934U, 0x6935U, 0x6936U, 0x6937U, 0x6938U, 0x6939U, 0x692BU, 0x692FU,
+ 0x6A41U, 0x6A42U, 0x6A43U, 0x6A44U, 0x6A45U, 0x6A46U, 0x6A47U, 0x6A48U,
+ 0x6A49U, 0x6A4AU, 0x6A4BU, 0x6A4CU, 0x6A4DU, 0x6A4EU, 0x6A4FU, 0x6A50U,
+ 0x6A51U, 0x6A52U, 0x6A53U, 0x6A54U, 0x6A55U, 0x6A56U, 0x6A57U, 0x6A58U,
+ 0x6A59U, 0x6A5AU, 0x6A61U, 0x6A62U, 0x6A63U, 0x6A64U, 0x6A65U, 0x6A66U,
+ 0x6A67U, 0x6A68U, 0x6A69U, 0x6A6AU, 0x6A6BU, 0x6A6CU, 0x6A6DU, 0x6A6EU,
+ 0x6A6FU, 0x6A70U, 0x6A71U, 0x6A72U, 0x6A73U, 0x6A74U, 0x6A75U, 0x6A76U,
+ 0x6A77U, 0x6A78U, 0x6A79U, 0x6A7AU, 0x6A30U, 0x6A31U, 0x6A32U, 0x6A33U,
+ 0x6A34U, 0x6A35U, 0x6A36U, 0x6A37U, 0x6A38U, 0x6A39U, 0x6A2BU, 0x6A2FU,
+ 0x6B41U, 0x6B42U, 0x6B43U, 0x6B44U, 0x6B45U, 0x6B46U, 0x6B47U, 0x6B48U,
+ 0x6B49U, 0x6B4AU, 0x6B4BU, 0x6B4CU, 0x6B4DU, 0x6B4EU, 0x6B4FU, 0x6B50U,
+ 0x6B51U, 0x6B52U, 0x6B53U, 0x6B54U, 0x6B55U, 0x6B56U, 0x6B57U, 0x6B58U,
+ 0x6B59U, 0x6B5AU, 0x6B61U, 0x6B62U, 0x6B63U, 0x6B64U, 0x6B65U, 0x6B66U,
+ 0x6B67U, 0x6B68U, 0x6B69U, 0x6B6AU, 0x6B6BU, 0x6B6CU, 0x6B6DU, 0x6B6EU,
+ 0x6B6FU, 0x6B70U, 0x6B71U, 0x6B72U, 0x6B73U, 0x6B74U, 0x6B75U, 0x6B76U,
+ 0x6B77U, 0x6B78U, 0x6B79U, 0x6B7AU, 0x6B30U, 0x6B31U, 0x6B32U, 0x6B33U,
+ 0x6B34U, 0x6B35U, 0x6B36U, 0x6B37U, 0x6B38U, 0x6B39U, 0x6B2BU, 0x6B2FU,
+ 0x6C41U, 0x6C42U, 0x6C43U, 0x6C44U, 0x6C45U, 0x6C46U, 0x6C47U, 0x6C48U,
+ 0x6C49U, 0x6C4AU, 0x6C4BU, 0x6C4CU, 0x6C4DU, 0x6C4EU, 0x6C4FU, 0x6C50U,
+ 0x6C51U, 0x6C52U, 0x6C53U, 0x6C54U, 0x6C55U, 0x6C56U, 0x6C57U, 0x6C58U,
+ 0x6C59U, 0x6C5AU, 0x6C61U, 0x6C62U, 0x6C63U, 0x6C64U, 0x6C65U, 0x6C66U,
+ 0x6C67U, 0x6C68U, 0x6C69U, 0x6C6AU, 0x6C6BU, 0x6C6CU, 0x6C6DU, 0x6C6EU,
+ 0x6C6FU, 0x6C70U, 0x6C71U, 0x6C72U, 0x6C73U, 0x6C74U, 0x6C75U, 0x6C76U,
+ 0x6C77U, 0x6C78U, 0x6C79U, 0x6C7AU, 0x6C30U, 0x6C31U, 0x6C32U, 0x6C33U,
+ 0x6C34U, 0x6C35U, 0x6C36U, 0x6C37U, 0x6C38U, 0x6C39U, 0x6C2BU, 0x6C2FU,
+ 0x6D41U, 0x6D42U, 0x6D43U, 0x6D44U, 0x6D45U, 0x6D46U, 0x6D47U, 0x6D48U,
+ 0x6D49U, 0x6D4AU, 0x6D4BU, 0x6D4CU, 0x6D4DU, 0x6D4EU, 0x6D4FU, 0x6D50U,
+ 0x6D51U, 0x6D52U, 0x6D53U, 0x6D54U, 0x6D55U, 0x6D56U, 0x6D57U, 0x6D58U,
+ 0x6D59U, 0x6D5AU, 0x6D61U, 0x6D62U, 0x6D63U, 0x6D64U, 0x6D65U, 0x6D66U,
+ 0x6D67U, 0x6D68U, 0x6D69U, 0x6D6AU, 0x6D6BU, 0x6D6CU, 0x6D6DU, 0x6D6EU,
+ 0x6D6FU, 0x6D70U, 0x6D71U, 0x6D72U, 0x6D73U, 0x6D74U, 0x6D75U, 0x6D76U,
+ 0x6D77U, 0x6D78U, 0x6D79U, 0x6D7AU, 0x6D30U, 0x6D31U, 0x6D32U, 0x6D33U,
+ 0x6D34U, 0x6D35U, 0x6D36U, 0x6D37U, 0x6D38U, 0x6D39U, 0x6D2BU, 0x6D2FU,
+ 0x6E41U, 0x6E42U, 0x6E43U, 0x6E44U, 0x6E45U, 0x6E46U, 0x6E47U, 0x6E48U,
+ 0x6E49U, 0x6E4AU, 0x6E4BU, 0x6E4CU, 0x6E4DU, 0x6E4EU, 0x6E4FU, 0x6E50U,
+ 0x6E51U, 0x6E52U, 0x6E53U, 0x6E54U, 0x6E55U, 0x6E56U, 0x6E57U, 0x6E58U,
+ 0x6E59U, 0x6E5AU, 0x6E61U, 0x6E62U, 0x6E63U, 0x6E64U, 0x6E65U, 0x6E66U,
+ 0x6E67U, 0x6E68U, 0x6E69U, 0x6E6AU, 0x6E6BU, 0x6E6CU, 0x6E6DU, 0x6E6EU,
+ 0x6E6FU, 0x6E70U, 0x6E71U, 0x6E72U, 0x6E73U, 0x6E74U, 0x6E75U, 0x6E76U,
+ 0x6E77U, 0x6E78U, 0x6E79U, 0x6E7AU, 0x6E30U, 0x6E31U, 0x6E32U, 0x6E33U,
+ 0x6E34U, 0x6E35U, 0x6E36U, 0x6E37U, 0x6E38U, 0x6E39U, 0x6E2BU, 0x6E2FU,
+ 0x6F41U, 0x6F42U, 0x6F43U, 0x6F44U, 0x6F45U, 0x6F46U, 0x6F47U, 0x6F48U,
+ 0x6F49U, 0x6F4AU, 0x6F4BU, 0x6F4CU, 0x6F4DU, 0x6F4EU, 0x6F4FU, 0x6F50U,
+ 0x6F51U, 0x6F52U, 0x6F53U, 0x6F54U, 0x6F55U, 0x6F56U, 0x6F57U, 0x6F58U,
+ 0x6F59U, 0x6F5AU, 0x6F61U, 0x6F62U, 0x6F63U, 0x6F64U, 0x6F65U, 0x6F66U,
+ 0x6F67U, 0x6F68U, 0x6F69U, 0x6F6AU, 0x6F6BU, 0x6F6CU, 0x6F6DU, 0x6F6EU,
+ 0x6F6FU, 0x6F70U, 0x6F71U, 0x6F72U, 0x6F73U, 0x6F74U, 0x6F75U, 0x6F76U,
+ 0x6F77U, 0x6F78U, 0x6F79U, 0x6F7AU, 0x6F30U, 0x6F31U, 0x6F32U, 0x6F33U,
+ 0x6F34U, 0x6F35U, 0x6F36U, 0x6F37U, 0x6F38U, 0x6F39U, 0x6F2BU, 0x6F2FU,
+ 0x7041U, 0x7042U, 0x7043U, 0x7044U, 0x7045U, 0x7046U, 0x7047U, 0x7048U,
+ 0x7049U, 0x704AU, 0x704BU, 0x704CU, 0x704DU, 0x704EU, 0x704FU, 0x7050U,
+ 0x7051U, 0x7052U, 0x7053U, 0x7054U, 0x7055U, 0x7056U, 0x7057U, 0x7058U,
+ 0x7059U, 0x705AU, 0x7061U, 0x7062U, 0x7063U, 0x7064U, 0x7065U, 0x7066U,
+ 0x7067U, 0x7068U, 0x7069U, 0x706AU, 0x706BU, 0x706CU, 0x706DU, 0x706EU,
+ 0x706FU, 0x7070U, 0x7071U, 0x7072U, 0x7073U, 0x7074U, 0x7075U, 0x7076U,
+ 0x7077U, 0x7078U, 0x7079U, 0x707AU, 0x7030U, 0x7031U, 0x7032U, 0x7033U,
+ 0x7034U, 0x7035U, 0x7036U, 0x7037U, 0x7038U, 0x7039U, 0x702BU, 0x702FU,
+ 0x7141U, 0x7142U, 0x7143U, 0x7144U, 0x7145U, 0x7146U, 0x7147U, 0x7148U,
+ 0x7149U, 0x714AU, 0x714BU, 0x714CU, 0x714DU, 0x714EU, 0x714FU, 0x7150U,
+ 0x7151U, 0x7152U, 0x7153U, 0x7154U, 0x7155U, 0x7156U, 0x7157U, 0x7158U,
+ 0x7159U, 0x715AU, 0x7161U, 0x7162U, 0x7163U, 0x7164U, 0x7165U, 0x7166U,
+ 0x7167U, 0x7168U, 0x7169U, 0x716AU, 0x716BU, 0x716CU, 0x716DU, 0x716EU,
+ 0x716FU, 0x7170U, 0x7171U, 0x7172U, 0x7173U, 0x7174U, 0x7175U, 0x7176U,
+ 0x7177U, 0x7178U, 0x7179U, 0x717AU, 0x7130U, 0x7131U, 0x7132U, 0x7133U,
+ 0x7134U, 0x7135U, 0x7136U, 0x7137U, 0x7138U, 0x7139U, 0x712BU, 0x712FU,
+ 0x7241U, 0x7242U, 0x7243U, 0x7244U, 0x7245U, 0x7246U, 0x7247U, 0x7248U,
+ 0x7249U, 0x724AU, 0x724BU, 0x724CU, 0x724DU, 0x724EU, 0x724FU, 0x7250U,
+ 0x7251U, 0x7252U, 0x7253U, 0x7254U, 0x7255U, 0x7256U, 0x7257U, 0x7258U,
+ 0x7259U, 0x725AU, 0x7261U, 0x7262U, 0x7263U, 0x7264U, 0x7265U, 0x7266U,
+ 0x7267U, 0x7268U, 0x7269U, 0x726AU, 0x726BU, 0x726CU, 0x726DU, 0x726EU,
+ 0x726FU, 0x7270U, 0x7271U, 0x7272U, 0x7273U, 0x7274U, 0x7275U, 0x7276U,
+ 0x7277U, 0x7278U, 0x7279U, 0x727AU, 0x7230U, 0x7231U, 0x7232U, 0x7233U,
+ 0x7234U, 0x7235U, 0x7236U, 0x7237U, 0x7238U, 0x7239U, 0x722BU, 0x722FU,
+ 0x7341U, 0x7342U, 0x7343U, 0x7344U, 0x7345U, 0x7346U, 0x7347U, 0x7348U,
+ 0x7349U, 0x734AU, 0x734BU, 0x734CU, 0x734DU, 0x734EU, 0x734FU, 0x7350U,
+ 0x7351U, 0x7352U, 0x7353U, 0x7354U, 0x7355U, 0x7356U, 0x7357U, 0x7358U,
+ 0x7359U, 0x735AU, 0x7361U, 0x7362U, 0x7363U, 0x7364U, 0x7365U, 0x7366U,
+ 0x7367U, 0x7368U, 0x7369U, 0x736AU, 0x736BU, 0x736CU, 0x736DU, 0x736EU,
+ 0x736FU, 0x7370U, 0x7371U, 0x7372U, 0x7373U, 0x7374U, 0x7375U, 0x7376U,
+ 0x7377U, 0x7378U, 0x7379U, 0x737AU, 0x7330U, 0x7331U, 0x7332U, 0x7333U,
+ 0x7334U, 0x7335U, 0x7336U, 0x7337U, 0x7338U, 0x7339U, 0x732BU, 0x732FU,
+ 0x7441U, 0x7442U, 0x7443U, 0x7444U, 0x7445U, 0x7446U, 0x7447U, 0x7448U,
+ 0x7449U, 0x744AU, 0x744BU, 0x744CU, 0x744DU, 0x744EU, 0x744FU, 0x7450U,
+ 0x7451U, 0x7452U, 0x7453U, 0x7454U, 0x7455U, 0x7456U, 0x7457U, 0x7458U,
+ 0x7459U, 0x745AU, 0x7461U, 0x7462U, 0x7463U, 0x7464U, 0x7465U, 0x7466U,
+ 0x7467U, 0x7468U, 0x7469U, 0x746AU, 0x746BU, 0x746CU, 0x746DU, 0x746EU,
+ 0x746FU, 0x7470U, 0x7471U, 0x7472U, 0x7473U, 0x7474U, 0x7475U, 0x7476U,
+ 0x7477U, 0x7478U, 0x7479U, 0x747AU, 0x7430U, 0x7431U, 0x7432U, 0x7433U,
+ 0x7434U, 0x7435U, 0x7436U, 0x7437U, 0x7438U, 0x7439U, 0x742BU, 0x742FU,
+ 0x7541U, 0x7542U, 0x7543U, 0x7544U, 0x7545U, 0x7546U, 0x7547U, 0x7548U,
+ 0x7549U, 0x754AU, 0x754BU, 0x754CU, 0x754DU, 0x754EU, 0x754FU, 0x7550U,
+ 0x7551U, 0x7552U, 0x7553U, 0x7554U, 0x7555U, 0x7556U, 0x7557U, 0x7558U,
+ 0x7559U, 0x755AU, 0x7561U, 0x7562U, 0x7563U, 0x7564U, 0x7565U, 0x7566U,
+ 0x7567U, 0x7568U, 0x7569U, 0x756AU, 0x756BU, 0x756CU, 0x756DU, 0x756EU,
+ 0x756FU, 0x7570U, 0x7571U, 0x7572U, 0x7573U, 0x7574U, 0x7575U, 0x7576U,
+ 0x7577U, 0x7578U, 0x7579U, 0x757AU, 0x7530U, 0x7531U, 0x7532U, 0x7533U,
+ 0x7534U, 0x7535U, 0x7536U, 0x7537U, 0x7538U, 0x7539U, 0x752BU, 0x752FU,
+ 0x7641U, 0x7642U, 0x7643U, 0x7644U, 0x7645U, 0x7646U, 0x7647U, 0x7648U,
+ 0x7649U, 0x764AU, 0x764BU, 0x764CU, 0x764DU, 0x764EU, 0x764FU, 0x7650U,
+ 0x7651U, 0x7652U, 0x7653U, 0x7654U, 0x7655U, 0x7656U, 0x7657U, 0x7658U,
+ 0x7659U, 0x765AU, 0x7661U, 0x7662U, 0x7663U, 0x7664U, 0x7665U, 0x7666U,
+ 0x7667U, 0x7668U, 0x7669U, 0x766AU, 0x766BU, 0x766CU, 0x766DU, 0x766EU,
+ 0x766FU, 0x7670U, 0x7671U, 0x7672U, 0x7673U, 0x7674U, 0x7675U, 0x7676U,
+ 0x7677U, 0x7678U, 0x7679U, 0x767AU, 0x7630U, 0x7631U, 0x7632U, 0x7633U,
+ 0x7634U, 0x7635U, 0x7636U, 0x7637U, 0x7638U, 0x7639U, 0x762BU, 0x762FU,
+ 0x7741U, 0x7742U, 0x7743U, 0x7744U, 0x7745U, 0x7746U, 0x7747U, 0x7748U,
+ 0x7749U, 0x774AU, 0x774BU, 0x774CU, 0x774DU, 0x774EU, 0x774FU, 0x7750U,
+ 0x7751U, 0x7752U, 0x7753U, 0x7754U, 0x7755U, 0x7756U, 0x7757U, 0x7758U,
+ 0x7759U, 0x775AU, 0x7761U, 0x7762U, 0x7763U, 0x7764U, 0x7765U, 0x7766U,
+ 0x7767U, 0x7768U, 0x7769U, 0x776AU, 0x776BU, 0x776CU, 0x776DU, 0x776EU,
+ 0x776FU, 0x7770U, 0x7771U, 0x7772U, 0x7773U, 0x7774U, 0x7775U, 0x7776U,
+ 0x7777U, 0x7778U, 0x7779U, 0x777AU, 0x7730U, 0x7731U, 0x7732U, 0x7733U,
+ 0x7734U, 0x7735U, 0x7736U, 0x7737U, 0x7738U, 0x7739U, 0x772BU, 0x772FU,
+ 0x7841U, 0x7842U, 0x7843U, 0x7844U, 0x7845U, 0x7846U, 0x7847U, 0x7848U,
+ 0x7849U, 0x784AU, 0x784BU, 0x784CU, 0x784DU, 0x784EU, 0x784FU, 0x7850U,
+ 0x7851U, 0x7852U, 0x7853U, 0x7854U, 0x7855U, 0x7856U, 0x7857U, 0x7858U,
+ 0x7859U, 0x785AU, 0x7861U, 0x7862U, 0x7863U, 0x7864U, 0x7865U, 0x7866U,
+ 0x7867U, 0x7868U, 0x7869U, 0x786AU, 0x786BU, 0x786CU, 0x786DU, 0x786EU,
+ 0x786FU, 0x7870U, 0x7871U, 0x7872U, 0x7873U, 0x7874U, 0x7875U, 0x7876U,
+ 0x7877U, 0x7878U, 0x7879U, 0x787AU, 0x7830U, 0x7831U, 0x7832U, 0x7833U,
+ 0x7834U, 0x7835U, 0x7836U, 0x7837U, 0x7838U, 0x7839U, 0x782BU, 0x782FU,
+ 0x7941U, 0x7942U, 0x7943U, 0x7944U, 0x7945U, 0x7946U, 0x7947U, 0x7948U,
+ 0x7949U, 0x794AU, 0x794BU, 0x794CU, 0x794DU, 0x794EU, 0x794FU, 0x7950U,
+ 0x7951U, 0x7952U, 0x7953U, 0x7954U, 0x7955U, 0x7956U, 0x7957U, 0x7958U,
+ 0x7959U, 0x795AU, 0x7961U, 0x7962U, 0x7963U, 0x7964U, 0x7965U, 0x7966U,
+ 0x7967U, 0x7968U, 0x7969U, 0x796AU, 0x796BU, 0x796CU, 0x796DU, 0x796EU,
+ 0x796FU, 0x7970U, 0x7971U, 0x7972U, 0x7973U, 0x7974U, 0x7975U, 0x7976U,
+ 0x7977U, 0x7978U, 0x7979U, 0x797AU, 0x7930U, 0x7931U, 0x7932U, 0x7933U,
+ 0x7934U, 0x7935U, 0x7936U, 0x7937U, 0x7938U, 0x7939U, 0x792BU, 0x792FU,
+ 0x7A41U, 0x7A42U, 0x7A43U, 0x7A44U, 0x7A45U, 0x7A46U, 0x7A47U, 0x7A48U,
+ 0x7A49U, 0x7A4AU, 0x7A4BU, 0x7A4CU, 0x7A4DU, 0x7A4EU, 0x7A4FU, 0x7A50U,
+ 0x7A51U, 0x7A52U, 0x7A53U, 0x7A54U, 0x7A55U, 0x7A56U, 0x7A57U, 0x7A58U,
+ 0x7A59U, 0x7A5AU, 0x7A61U, 0x7A62U, 0x7A63U, 0x7A64U, 0x7A65U, 0x7A66U,
+ 0x7A67U, 0x7A68U, 0x7A69U, 0x7A6AU, 0x7A6BU, 0x7A6CU, 0x7A6DU, 0x7A6EU,
+ 0x7A6FU, 0x7A70U, 0x7A71U, 0x7A72U, 0x7A73U, 0x7A74U, 0x7A75U, 0x7A76U,
+ 0x7A77U, 0x7A78U, 0x7A79U, 0x7A7AU, 0x7A30U, 0x7A31U, 0x7A32U, 0x7A33U,
+ 0x7A34U, 0x7A35U, 0x7A36U, 0x7A37U, 0x7A38U, 0x7A39U, 0x7A2BU, 0x7A2FU,
+ 0x3041U, 0x3042U, 0x3043U, 0x3044U, 0x3045U, 0x3046U, 0x3047U, 0x3048U,
+ 0x3049U, 0x304AU, 0x304BU, 0x304CU, 0x304DU, 0x304EU, 0x304FU, 0x3050U,
+ 0x3051U, 0x3052U, 0x3053U, 0x3054U, 0x3055U, 0x3056U, 0x3057U, 0x3058U,
+ 0x3059U, 0x305AU, 0x3061U, 0x3062U, 0x3063U, 0x3064U, 0x3065U, 0x3066U,
+ 0x3067U, 0x3068U, 0x3069U, 0x306AU, 0x306BU, 0x306CU, 0x306DU, 0x306EU,
+ 0x306FU, 0x3070U, 0x3071U, 0x3072U, 0x3073U, 0x3074U, 0x3075U, 0x3076U,
+ 0x3077U, 0x3078U, 0x3079U, 0x307AU, 0x3030U, 0x3031U, 0x3032U, 0x3033U,
+ 0x3034U, 0x3035U, 0x3036U, 0x3037U, 0x3038U, 0x3039U, 0x302BU, 0x302FU,
+ 0x3141U, 0x3142U, 0x3143U, 0x3144U, 0x3145U, 0x3146U, 0x3147U, 0x3148U,
+ 0x3149U, 0x314AU, 0x314BU, 0x314CU, 0x314DU, 0x314EU, 0x314FU, 0x3150U,
+ 0x3151U, 0x3152U, 0x3153U, 0x3154U, 0x3155U, 0x3156U, 0x3157U, 0x3158U,
+ 0x3159U, 0x315AU, 0x3161U, 0x3162U, 0x3163U, 0x3164U, 0x3165U, 0x3166U,
+ 0x3167U, 0x3168U, 0x3169U, 0x316AU, 0x316BU, 0x316CU, 0x316DU, 0x316EU,
+ 0x316FU, 0x3170U, 0x3171U, 0x3172U, 0x3173U, 0x3174U, 0x3175U, 0x3176U,
+ 0x3177U, 0x3178U, 0x3179U, 0x317AU, 0x3130U, 0x3131U, 0x3132U, 0x3133U,
+ 0x3134U, 0x3135U, 0x3136U, 0x3137U, 0x3138U, 0x3139U, 0x312BU, 0x312FU,
+ 0x3241U, 0x3242U, 0x3243U, 0x3244U, 0x3245U, 0x3246U, 0x3247U, 0x3248U,
+ 0x3249U, 0x324AU, 0x324BU, 0x324CU, 0x324DU, 0x324EU, 0x324FU, 0x3250U,
+ 0x3251U, 0x3252U, 0x3253U, 0x3254U, 0x3255U, 0x3256U, 0x3257U, 0x3258U,
+ 0x3259U, 0x325AU, 0x3261U, 0x3262U, 0x3263U, 0x3264U, 0x3265U, 0x3266U,
+ 0x3267U, 0x3268U, 0x3269U, 0x326AU, 0x326BU, 0x326CU, 0x326DU, 0x326EU,
+ 0x326FU, 0x3270U, 0x3271U, 0x3272U, 0x3273U, 0x3274U, 0x3275U, 0x3276U,
+ 0x3277U, 0x3278U, 0x3279U, 0x327AU, 0x3230U, 0x3231U, 0x3232U, 0x3233U,
+ 0x3234U, 0x3235U, 0x3236U, 0x3237U, 0x3238U, 0x3239U, 0x322BU, 0x322FU,
+ 0x3341U, 0x3342U, 0x3343U, 0x3344U, 0x3345U, 0x3346U, 0x3347U, 0x3348U,
+ 0x3349U, 0x334AU, 0x334BU, 0x334CU, 0x334DU, 0x334EU, 0x334FU, 0x3350U,
+ 0x3351U, 0x3352U, 0x3353U, 0x3354U, 0x3355U, 0x3356U, 0x3357U, 0x3358U,
+ 0x3359U, 0x335AU, 0x3361U, 0x3362U, 0x3363U, 0x3364U, 0x3365U, 0x3366U,
+ 0x3367U, 0x3368U, 0x3369U, 0x336AU, 0x336BU, 0x336CU, 0x336DU, 0x336EU,
+ 0x336FU, 0x3370U, 0x3371U, 0x3372U, 0x3373U, 0x3374U, 0x3375U, 0x3376U,
+ 0x3377U, 0x3378U, 0x3379U, 0x337AU, 0x3330U, 0x3331U, 0x3332U, 0x3333U,
+ 0x3334U, 0x3335U, 0x3336U, 0x3337U, 0x3338U, 0x3339U, 0x332BU, 0x332FU,
+ 0x3441U, 0x3442U, 0x3443U, 0x3444U, 0x3445U, 0x3446U, 0x3447U, 0x3448U,
+ 0x3449U, 0x344AU, 0x344BU, 0x344CU, 0x344DU, 0x344EU, 0x344FU, 0x3450U,
+ 0x3451U, 0x3452U, 0x3453U, 0x3454U, 0x3455U, 0x3456U, 0x3457U, 0x3458U,
+ 0x3459U, 0x345AU, 0x3461U, 0x3462U, 0x3463U, 0x3464U, 0x3465U, 0x3466U,
+ 0x3467U, 0x3468U, 0x3469U, 0x346AU, 0x346BU, 0x346CU, 0x346DU, 0x346EU,
+ 0x346FU, 0x3470U, 0x3471U, 0x3472U, 0x3473U, 0x3474U, 0x3475U, 0x3476U,
+ 0x3477U, 0x3478U, 0x3479U, 0x347AU, 0x3430U, 0x3431U, 0x3432U, 0x3433U,
+ 0x3434U, 0x3435U, 0x3436U, 0x3437U, 0x3438U, 0x3439U, 0x342BU, 0x342FU,
+ 0x3541U, 0x3542U, 0x3543U, 0x3544U, 0x3545U, 0x3546U, 0x3547U, 0x3548U,
+ 0x3549U, 0x354AU, 0x354BU, 0x354CU, 0x354DU, 0x354EU, 0x354FU, 0x3550U,
+ 0x3551U, 0x3552U, 0x3553U, 0x3554U, 0x3555U, 0x3556U, 0x3557U, 0x3558U,
+ 0x3559U, 0x355AU, 0x3561U, 0x3562U, 0x3563U, 0x3564U, 0x3565U, 0x3566U,
+ 0x3567U, 0x3568U, 0x3569U, 0x356AU, 0x356BU, 0x356CU, 0x356DU, 0x356EU,
+ 0x356FU, 0x3570U, 0x3571U, 0x3572U, 0x3573U, 0x3574U, 0x3575U, 0x3576U,
+ 0x3577U, 0x3578U, 0x3579U, 0x357AU, 0x3530U, 0x3531U, 0x3532U, 0x3533U,
+ 0x3534U, 0x3535U, 0x3536U, 0x3537U, 0x3538U, 0x3539U, 0x352BU, 0x352FU,
+ 0x3641U, 0x3642U, 0x3643U, 0x3644U, 0x3645U, 0x3646U, 0x3647U, 0x3648U,
+ 0x3649U, 0x364AU, 0x364BU, 0x364CU, 0x364DU, 0x364EU, 0x364FU, 0x3650U,
+ 0x3651U, 0x3652U, 0x3653U, 0x3654U, 0x3655U, 0x3656U, 0x3657U, 0x3658U,
+ 0x3659U, 0x365AU, 0x3661U, 0x3662U, 0x3663U, 0x3664U, 0x3665U, 0x3666U,
+ 0x3667U, 0x3668U, 0x3669U, 0x366AU, 0x366BU, 0x366CU, 0x366DU, 0x366EU,
+ 0x366FU, 0x3670U, 0x3671U, 0x3672U, 0x3673U, 0x3674U, 0x3675U, 0x3676U,
+ 0x3677U, 0x3678U, 0x3679U, 0x367AU, 0x3630U, 0x3631U, 0x3632U, 0x3633U,
+ 0x3634U, 0x3635U, 0x3636U, 0x3637U, 0x3638U, 0x3639U, 0x362BU, 0x362FU,
+ 0x3741U, 0x3742U, 0x3743U, 0x3744U, 0x3745U, 0x3746U, 0x3747U, 0x3748U,
+ 0x3749U, 0x374AU, 0x374BU, 0x374CU, 0x374DU, 0x374EU, 0x374FU, 0x3750U,
+ 0x3751U, 0x3752U, 0x3753U, 0x3754U, 0x3755U, 0x3756U, 0x3757U, 0x3758U,
+ 0x3759U, 0x375AU, 0x3761U, 0x3762U, 0x3763U, 0x3764U, 0x3765U, 0x3766U,
+ 0x3767U, 0x3768U, 0x3769U, 0x376AU, 0x376BU, 0x376CU, 0x376DU, 0x376EU,
+ 0x376FU, 0x3770U, 0x3771U, 0x3772U, 0x3773U, 0x3774U, 0x3775U, 0x3776U,
+ 0x3777U, 0x3778U, 0x3779U, 0x377AU, 0x3730U, 0x3731U, 0x3732U, 0x3733U,
+ 0x3734U, 0x3735U, 0x3736U, 0x3737U, 0x3738U, 0x3739U, 0x372BU, 0x372FU,
+ 0x3841U, 0x3842U, 0x3843U, 0x3844U, 0x3845U, 0x3846U, 0x3847U, 0x3848U,
+ 0x3849U, 0x384AU, 0x384BU, 0x384CU, 0x384DU, 0x384EU, 0x384FU, 0x3850U,
+ 0x3851U, 0x3852U, 0x3853U, 0x3854U, 0x3855U, 0x3856U, 0x3857U, 0x3858U,
+ 0x3859U, 0x385AU, 0x3861U, 0x3862U, 0x3863U, 0x3864U, 0x3865U, 0x3866U,
+ 0x3867U, 0x3868U, 0x3869U, 0x386AU, 0x386BU, 0x386CU, 0x386DU, 0x386EU,
+ 0x386FU, 0x3870U, 0x3871U, 0x3872U, 0x3873U, 0x3874U, 0x3875U, 0x3876U,
+ 0x3877U, 0x3878U, 0x3879U, 0x387AU, 0x3830U, 0x3831U, 0x3832U, 0x3833U,
+ 0x3834U, 0x3835U, 0x3836U, 0x3837U, 0x3838U, 0x3839U, 0x382BU, 0x382FU,
+ 0x3941U, 0x3942U, 0x3943U, 0x3944U, 0x3945U, 0x3946U, 0x3947U, 0x3948U,
+ 0x3949U, 0x394AU, 0x394BU, 0x394CU, 0x394DU, 0x394EU, 0x394FU, 0x3950U,
+ 0x3951U, 0x3952U, 0x3953U, 0x3954U, 0x3955U, 0x3956U, 0x3957U, 0x3958U,
+ 0x3959U, 0x395AU, 0x3961U, 0x3962U, 0x3963U, 0x3964U, 0x3965U, 0x3966U,
+ 0x3967U, 0x3968U, 0x3969U, 0x396AU, 0x396BU, 0x396CU, 0x396DU, 0x396EU,
+ 0x396FU, 0x3970U, 0x3971U, 0x3972U, 0x3973U, 0x3974U, 0x3975U, 0x3976U,
+ 0x3977U, 0x3978U, 0x3979U, 0x397AU, 0x3930U, 0x3931U, 0x3932U, 0x3933U,
+ 0x3934U, 0x3935U, 0x3936U, 0x3937U, 0x3938U, 0x3939U, 0x392BU, 0x392FU,
+ 0x2B41U, 0x2B42U, 0x2B43U, 0x2B44U, 0x2B45U, 0x2B46U, 0x2B47U, 0x2B48U,
+ 0x2B49U, 0x2B4AU, 0x2B4BU, 0x2B4CU, 0x2B4DU, 0x2B4EU, 0x2B4FU, 0x2B50U,
+ 0x2B51U, 0x2B52U, 0x2B53U, 0x2B54U, 0x2B55U, 0x2B56U, 0x2B57U, 0x2B58U,
+ 0x2B59U, 0x2B5AU, 0x2B61U, 0x2B62U, 0x2B63U, 0x2B64U, 0x2B65U, 0x2B66U,
+ 0x2B67U, 0x2B68U, 0x2B69U, 0x2B6AU, 0x2B6BU, 0x2B6CU, 0x2B6DU, 0x2B6EU,
+ 0x2B6FU, 0x2B70U, 0x2B71U, 0x2B72U, 0x2B73U, 0x2B74U, 0x2B75U, 0x2B76U,
+ 0x2B77U, 0x2B78U, 0x2B79U, 0x2B7AU, 0x2B30U, 0x2B31U, 0x2B32U, 0x2B33U,
+ 0x2B34U, 0x2B35U, 0x2B36U, 0x2B37U, 0x2B38U, 0x2B39U, 0x2B2BU, 0x2B2FU,
+ 0x2F41U, 0x2F42U, 0x2F43U, 0x2F44U, 0x2F45U, 0x2F46U, 0x2F47U, 0x2F48U,
+ 0x2F49U, 0x2F4AU, 0x2F4BU, 0x2F4CU, 0x2F4DU, 0x2F4EU, 0x2F4FU, 0x2F50U,
+ 0x2F51U, 0x2F52U, 0x2F53U, 0x2F54U, 0x2F55U, 0x2F56U, 0x2F57U, 0x2F58U,
+ 0x2F59U, 0x2F5AU, 0x2F61U, 0x2F62U, 0x2F63U, 0x2F64U, 0x2F65U, 0x2F66U,
+ 0x2F67U, 0x2F68U, 0x2F69U, 0x2F6AU, 0x2F6BU, 0x2F6CU, 0x2F6DU, 0x2F6EU,
+ 0x2F6FU, 0x2F70U, 0x2F71U, 0x2F72U, 0x2F73U, 0x2F74U, 0x2F75U, 0x2F76U,
+ 0x2F77U, 0x2F78U, 0x2F79U, 0x2F7AU, 0x2F30U, 0x2F31U, 0x2F32U, 0x2F33U,
+ 0x2F34U, 0x2F35U, 0x2F36U, 0x2F37U, 0x2F38U, 0x2F39U, 0x2F2BU, 0x2F2FU,
+#endif
+};
diff --git a/src/third-party/base64/lib/tables/tables.c b/src/third-party/base64/lib/tables/tables.c
new file mode 100644
index 0000000..45778b6
--- /dev/null
+++ b/src/third-party/base64/lib/tables/tables.c
@@ -0,0 +1,40 @@
+#include "tables.h"
+
+const uint8_t
+base64_table_enc_6bit[] =
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "abcdefghijklmnopqrstuvwxyz"
+ "0123456789"
+ "+/";
+
+// In the lookup table below, note that the value for '=' (character 61) is
+// 254, not 255. This character is used for in-band signaling of the end of
+// the datastream, and we will use that later. The characters A-Z, a-z, 0-9
+// and + / are mapped to their "decoded" values. The other bytes all map to
+// the value 255, which flags them as "invalid input".
+
+const uint8_t
+base64_table_dec_8bit[] =
+{
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 0..15
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 16..31
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 62, 255, 255, 255, 63, // 32..47
+ 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 255, 255, 255, 254, 255, 255, // 48..63
+ 255, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, // 64..79
+ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 255, 255, 255, 255, 255, // 80..95
+ 255, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, // 96..111
+ 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 255, 255, 255, 255, 255, // 112..127
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, // 128..143
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+};
+
+#if BASE64_WORDSIZE >= 32
+# include "table_dec_32bit.h"
+# include "table_enc_12bit.h"
+#endif
diff --git a/src/third-party/base64/lib/tables/tables.h b/src/third-party/base64/lib/tables/tables.h
new file mode 100644
index 0000000..cb74268
--- /dev/null
+++ b/src/third-party/base64/lib/tables/tables.h
@@ -0,0 +1,23 @@
+#ifndef BASE64_TABLES_H
+#define BASE64_TABLES_H
+
+#include <stdint.h>
+
+#include "../env.h"
+
+// These tables are used by all codecs for fallback plain encoding/decoding:
+extern const uint8_t base64_table_enc_6bit[];
+extern const uint8_t base64_table_dec_8bit[];
+
+// These tables are used for the 32-bit and 64-bit generic decoders:
+#if BASE64_WORDSIZE >= 32
+extern const uint32_t base64_table_dec_32bit_d0[];
+extern const uint32_t base64_table_dec_32bit_d1[];
+extern const uint32_t base64_table_dec_32bit_d2[];
+extern const uint32_t base64_table_dec_32bit_d3[];
+
+// This table is used by the 32 and 64-bit generic encoders:
+extern const uint16_t base64_table_enc_12bit[];
+#endif
+
+#endif // BASE64_TABLES_H
diff --git a/src/third-party/doctest-root/doctest/doctest.h b/src/third-party/doctest-root/doctest/doctest.h
new file mode 100644
index 0000000..aa2724c
--- /dev/null
+++ b/src/third-party/doctest-root/doctest/doctest.h
@@ -0,0 +1,7019 @@
+// ====================================================================== lgtm [cpp/missing-header-guard]
+// == DO NOT MODIFY THIS FILE BY HAND - IT IS AUTO GENERATED BY CMAKE! ==
+// ======================================================================
+//
+// doctest.h - the lightest feature-rich C++ single-header testing framework for unit tests and TDD
+//
+// Copyright (c) 2016-2021 Viktor Kirilov
+//
+// Distributed under the MIT Software License
+// See accompanying file LICENSE.txt or copy at
+// https://opensource.org/licenses/MIT
+//
+// The documentation can be found at the library's page:
+// https://github.com/doctest/doctest/blob/master/doc/markdown/readme.md
+//
+// =================================================================================================
+// =================================================================================================
+// =================================================================================================
+//
+// The library is heavily influenced by Catch - https://github.com/catchorg/Catch2
+// which uses the Boost Software License - Version 1.0
+// see here - https://github.com/catchorg/Catch2/blob/master/LICENSE.txt
+//
+// The concept of subcases (sections in Catch) and expression decomposition are from there.
+// Some parts of the code are taken directly:
+// - stringification - the detection of "ostream& operator<<(ostream&, const T&)" and StringMaker<>
+// - the Approx() helper class for floating point comparison
+// - colors in the console
+// - breaking into a debugger
+// - signal / SEH handling
+// - timer
+// - XmlWriter class - thanks to Phil Nash for allowing the direct reuse (AKA copy/paste)
+//
+// The expression decomposing templates are taken from lest - https://github.com/martinmoene/lest
+// which uses the Boost Software License - Version 1.0
+// see here - https://github.com/martinmoene/lest/blob/master/LICENSE.txt
+//
+// =================================================================================================
+// =================================================================================================
+// =================================================================================================
+
+#ifndef DOCTEST_LIBRARY_INCLUDED
+#define DOCTEST_LIBRARY_INCLUDED
+
+// =================================================================================================
+// == VERSION ======================================================================================
+// =================================================================================================
+
+#define DOCTEST_VERSION_MAJOR 2
+#define DOCTEST_VERSION_MINOR 4
+#define DOCTEST_VERSION_PATCH 9
+
+// util we need here
+#define DOCTEST_TOSTR_IMPL(x) #x
+#define DOCTEST_TOSTR(x) DOCTEST_TOSTR_IMPL(x)
+
+#define DOCTEST_VERSION_STR \
+ DOCTEST_TOSTR(DOCTEST_VERSION_MAJOR) "." \
+ DOCTEST_TOSTR(DOCTEST_VERSION_MINOR) "." \
+ DOCTEST_TOSTR(DOCTEST_VERSION_PATCH)
+
+#define DOCTEST_VERSION \
+ (DOCTEST_VERSION_MAJOR * 10000 + DOCTEST_VERSION_MINOR * 100 + DOCTEST_VERSION_PATCH)
+
+// =================================================================================================
+// == COMPILER VERSION =============================================================================
+// =================================================================================================
+
+// ideas for the version stuff are taken from here: https://github.com/cxxstuff/cxx_detect
+
+#ifdef _MSC_VER
+#define DOCTEST_CPLUSPLUS _MSVC_LANG
+#else
+#define DOCTEST_CPLUSPLUS __cplusplus
+#endif
+
+#define DOCTEST_COMPILER(MAJOR, MINOR, PATCH) ((MAJOR)*10000000 + (MINOR)*100000 + (PATCH))
+
+// GCC/Clang and GCC/MSVC are mutually exclusive, but Clang/MSVC are not because of clang-cl...
+#if defined(_MSC_VER) && defined(_MSC_FULL_VER)
+#if _MSC_VER == _MSC_FULL_VER / 10000
+#define DOCTEST_MSVC DOCTEST_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000)
+#else // MSVC
+#define DOCTEST_MSVC \
+ DOCTEST_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 100, _MSC_FULL_VER % 100000)
+#endif // MSVC
+#endif // MSVC
+#if defined(__clang__) && defined(__clang_minor__)
+#define DOCTEST_CLANG DOCTEST_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && defined(__GNUC_PATCHLEVEL__) && \
+ !defined(__INTEL_COMPILER)
+#define DOCTEST_GCC DOCTEST_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#endif // GCC
+
+#ifndef DOCTEST_MSVC
+#define DOCTEST_MSVC 0
+#endif // DOCTEST_MSVC
+#ifndef DOCTEST_CLANG
+#define DOCTEST_CLANG 0
+#endif // DOCTEST_CLANG
+#ifndef DOCTEST_GCC
+#define DOCTEST_GCC 0
+#endif // DOCTEST_GCC
+
+// =================================================================================================
+// == COMPILER WARNINGS HELPERS ====================================================================
+// =================================================================================================
+
+#if DOCTEST_CLANG
+#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x)
+#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH _Pragma("clang diagnostic push")
+#define DOCTEST_CLANG_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(clang diagnostic ignored w)
+#define DOCTEST_CLANG_SUPPRESS_WARNING_POP _Pragma("clang diagnostic pop")
+#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w) \
+ DOCTEST_CLANG_SUPPRESS_WARNING_PUSH DOCTEST_CLANG_SUPPRESS_WARNING(w)
+#else // DOCTEST_CLANG
+#define DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
+#define DOCTEST_CLANG_SUPPRESS_WARNING(w)
+#define DOCTEST_CLANG_SUPPRESS_WARNING_POP
+#define DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH(w)
+#endif // DOCTEST_CLANG
+
+#if DOCTEST_GCC
+#define DOCTEST_PRAGMA_TO_STR(x) _Pragma(#x)
+#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH _Pragma("GCC diagnostic push")
+#define DOCTEST_GCC_SUPPRESS_WARNING(w) DOCTEST_PRAGMA_TO_STR(GCC diagnostic ignored w)
+#define DOCTEST_GCC_SUPPRESS_WARNING_POP _Pragma("GCC diagnostic pop")
+#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w) \
+ DOCTEST_GCC_SUPPRESS_WARNING_PUSH DOCTEST_GCC_SUPPRESS_WARNING(w)
+#else // DOCTEST_GCC
+#define DOCTEST_GCC_SUPPRESS_WARNING_PUSH
+#define DOCTEST_GCC_SUPPRESS_WARNING(w)
+#define DOCTEST_GCC_SUPPRESS_WARNING_POP
+#define DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH(w)
+#endif // DOCTEST_GCC
+
+#if DOCTEST_MSVC
+#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH __pragma(warning(push))
+#define DOCTEST_MSVC_SUPPRESS_WARNING(w) __pragma(warning(disable : w))
+#define DOCTEST_MSVC_SUPPRESS_WARNING_POP __pragma(warning(pop))
+#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w) \
+ DOCTEST_MSVC_SUPPRESS_WARNING_PUSH DOCTEST_MSVC_SUPPRESS_WARNING(w)
+#else // DOCTEST_MSVC
+#define DOCTEST_MSVC_SUPPRESS_WARNING_PUSH
+#define DOCTEST_MSVC_SUPPRESS_WARNING(w)
+#define DOCTEST_MSVC_SUPPRESS_WARNING_POP
+#define DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(w)
+#endif // DOCTEST_MSVC
+
+// =================================================================================================
+// == COMPILER WARNINGS ============================================================================
+// =================================================================================================
+
+// both the header and the implementation suppress all of these,
+// so it only makes sense to aggregrate them like so
+#define DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH \
+ DOCTEST_CLANG_SUPPRESS_WARNING_PUSH \
+ DOCTEST_CLANG_SUPPRESS_WARNING("-Wunknown-pragmas") \
+ DOCTEST_CLANG_SUPPRESS_WARNING("-Wweak-vtables") \
+ DOCTEST_CLANG_SUPPRESS_WARNING("-Wpadded") \
+ DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-prototypes") \
+ DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat") \
+ DOCTEST_CLANG_SUPPRESS_WARNING("-Wc++98-compat-pedantic") \
+ \
+ DOCTEST_GCC_SUPPRESS_WARNING_PUSH \
+ DOCTEST_GCC_SUPPRESS_WARNING("-Wunknown-pragmas") \
+ DOCTEST_GCC_SUPPRESS_WARNING("-Wpragmas") \
+ DOCTEST_GCC_SUPPRESS_WARNING("-Weffc++") \
+ DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-overflow") \
+ DOCTEST_GCC_SUPPRESS_WARNING("-Wstrict-aliasing") \
+ DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-declarations") \
+ DOCTEST_GCC_SUPPRESS_WARNING("-Wuseless-cast") \
+ DOCTEST_GCC_SUPPRESS_WARNING("-Wnoexcept") \
+ \
+ DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \
+ /* these 4 also disabled globally via cmake: */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4514) /* unreferenced inline function has been removed */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4571) /* SEH related */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4710) /* function not inlined */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4711) /* function selected for inline expansion*/ \
+ /* */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4616) /* invalid compiler warning */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4619) /* invalid compiler warning */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4996) /* The compiler encountered a deprecated declaration */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4706) /* assignment within conditional expression */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4512) /* 'class' : assignment operator could not be generated */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4127) /* conditional expression is constant */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4640) /* construction of local static object not thread-safe */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \
+ /* static analysis */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(26439) /* Function may not throw. Declare it 'noexcept' */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(26495) /* Always initialize a member variable */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(26451) /* Arithmetic overflow ... */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(26444) /* Avoid unnamed objects with custom ctor and dtor... */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(26812) /* Prefer 'enum class' over 'enum' */
+
+#define DOCTEST_SUPPRESS_COMMON_WARNINGS_POP \
+ DOCTEST_CLANG_SUPPRESS_WARNING_POP \
+ DOCTEST_GCC_SUPPRESS_WARNING_POP \
+ DOCTEST_MSVC_SUPPRESS_WARNING_POP
+
+DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH
+
+DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wnon-virtual-dtor")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wdeprecated")
+
+DOCTEST_GCC_SUPPRESS_WARNING_PUSH
+DOCTEST_GCC_SUPPRESS_WARNING("-Wctor-dtor-privacy")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wnon-virtual-dtor")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-promo")
+
+DOCTEST_MSVC_SUPPRESS_WARNING_PUSH
+DOCTEST_MSVC_SUPPRESS_WARNING(4623) // default constructor was implicitly defined as deleted
+
+#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN \
+ DOCTEST_MSVC_SUPPRESS_WARNING_PUSH \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4548) /* before comma no effect; expected side - effect */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4265) /* virtual functions, but destructor is not virtual */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4986) /* exception specification does not match previous */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4350) /* 'member1' called instead of 'member2' */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4668) /* not defined as a preprocessor macro */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4365) /* signed/unsigned mismatch */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4774) /* format string not a string literal */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4820) /* padding */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4625) /* copy constructor was implicitly deleted */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4626) /* assignment operator was implicitly deleted */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(5027) /* move assignment operator implicitly deleted */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(5026) /* move constructor was implicitly deleted */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4623) /* default constructor was implicitly deleted */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(5039) /* pointer to pot. throwing function passed to extern C */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(5045) /* Spectre mitigation for memory load */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(5105) /* macro producing 'defined' has undefined behavior */ \
+ DOCTEST_MSVC_SUPPRESS_WARNING(4738) /* storing float result in memory, loss of performance */
+
+#define DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END DOCTEST_MSVC_SUPPRESS_WARNING_POP
+
+// =================================================================================================
+// == FEATURE DETECTION ============================================================================
+// =================================================================================================
+
+// general compiler feature support table: https://en.cppreference.com/w/cpp/compiler_support
+// MSVC C++11 feature support table: https://msdn.microsoft.com/en-us/library/hh567368.aspx
+// GCC C++11 feature support table: https://gcc.gnu.org/projects/cxx-status.html
+// MSVC version table:
+// https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#Internal_version_numbering
+// MSVC++ 14.3 (17) _MSC_VER == 1930 (Visual Studio 2022)
+// MSVC++ 14.2 (16) _MSC_VER == 1920 (Visual Studio 2019)
+// MSVC++ 14.1 (15) _MSC_VER == 1910 (Visual Studio 2017)
+// MSVC++ 14.0 _MSC_VER == 1900 (Visual Studio 2015)
+// MSVC++ 12.0 _MSC_VER == 1800 (Visual Studio 2013)
+// MSVC++ 11.0 _MSC_VER == 1700 (Visual Studio 2012)
+// MSVC++ 10.0 _MSC_VER == 1600 (Visual Studio 2010)
+// MSVC++ 9.0 _MSC_VER == 1500 (Visual Studio 2008)
+// MSVC++ 8.0 _MSC_VER == 1400 (Visual Studio 2005)
+
+// Universal Windows Platform support
+#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP)
+#define DOCTEST_CONFIG_NO_WINDOWS_SEH
+#endif // WINAPI_FAMILY
+#if DOCTEST_MSVC && !defined(DOCTEST_CONFIG_WINDOWS_SEH)
+#define DOCTEST_CONFIG_WINDOWS_SEH
+#endif // MSVC
+#if defined(DOCTEST_CONFIG_NO_WINDOWS_SEH) && defined(DOCTEST_CONFIG_WINDOWS_SEH)
+#undef DOCTEST_CONFIG_WINDOWS_SEH
+#endif // DOCTEST_CONFIG_NO_WINDOWS_SEH
+
+#if !defined(_WIN32) && !defined(__QNX__) && !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && \
+ !defined(__EMSCRIPTEN__) && !defined(__wasi__)
+#define DOCTEST_CONFIG_POSIX_SIGNALS
+#endif // _WIN32
+#if defined(DOCTEST_CONFIG_NO_POSIX_SIGNALS) && defined(DOCTEST_CONFIG_POSIX_SIGNALS)
+#undef DOCTEST_CONFIG_POSIX_SIGNALS
+#endif // DOCTEST_CONFIG_NO_POSIX_SIGNALS
+
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND) \
+ || defined(__wasi__)
+#define DOCTEST_CONFIG_NO_EXCEPTIONS
+#endif // no exceptions
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+
+#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+#define DOCTEST_CONFIG_NO_EXCEPTIONS
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
+
+#if defined(DOCTEST_CONFIG_NO_EXCEPTIONS) && !defined(DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS)
+#define DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS && !DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS
+
+#ifdef __wasi__
+#define DOCTEST_CONFIG_NO_MULTITHREADING
+#endif
+
+#if defined(DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN) && !defined(DOCTEST_CONFIG_IMPLEMENT)
+#define DOCTEST_CONFIG_IMPLEMENT
+#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+
+#if defined(_WIN32) || defined(__CYGWIN__)
+#if DOCTEST_MSVC
+#define DOCTEST_SYMBOL_EXPORT __declspec(dllexport)
+#define DOCTEST_SYMBOL_IMPORT __declspec(dllimport)
+#else // MSVC
+#define DOCTEST_SYMBOL_EXPORT __attribute__((dllexport))
+#define DOCTEST_SYMBOL_IMPORT __attribute__((dllimport))
+#endif // MSVC
+#else // _WIN32
+#define DOCTEST_SYMBOL_EXPORT __attribute__((visibility("default")))
+#define DOCTEST_SYMBOL_IMPORT
+#endif // _WIN32
+
+#ifdef DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#ifdef DOCTEST_CONFIG_IMPLEMENT
+#define DOCTEST_INTERFACE DOCTEST_SYMBOL_EXPORT
+#else // DOCTEST_CONFIG_IMPLEMENT
+#define DOCTEST_INTERFACE DOCTEST_SYMBOL_IMPORT
+#endif // DOCTEST_CONFIG_IMPLEMENT
+#else // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+#define DOCTEST_INTERFACE
+#endif // DOCTEST_CONFIG_IMPLEMENTATION_IN_DLL
+
+// needed for extern template instantiations
+// see https://github.com/fmtlib/fmt/issues/2228
+#if DOCTEST_MSVC
+#define DOCTEST_INTERFACE_DECL
+#define DOCTEST_INTERFACE_DEF DOCTEST_INTERFACE
+#else // DOCTEST_MSVC
+#define DOCTEST_INTERFACE_DECL DOCTEST_INTERFACE
+#define DOCTEST_INTERFACE_DEF
+#endif // DOCTEST_MSVC
+
+#define DOCTEST_EMPTY
+
+#if DOCTEST_MSVC
+#define DOCTEST_NOINLINE __declspec(noinline)
+#define DOCTEST_UNUSED
+#define DOCTEST_ALIGNMENT(x)
+#elif DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 5, 0)
+#define DOCTEST_NOINLINE
+#define DOCTEST_UNUSED
+#define DOCTEST_ALIGNMENT(x)
+#else
+#define DOCTEST_NOINLINE __attribute__((noinline))
+#define DOCTEST_UNUSED __attribute__((unused))
+#define DOCTEST_ALIGNMENT(x) __attribute__((aligned(x)))
+#endif
+
+#ifndef DOCTEST_NORETURN
+#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0))
+#define DOCTEST_NORETURN
+#else // DOCTEST_MSVC
+#define DOCTEST_NORETURN [[noreturn]]
+#endif // DOCTEST_MSVC
+#endif // DOCTEST_NORETURN
+
+#ifndef DOCTEST_NOEXCEPT
+#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0))
+#define DOCTEST_NOEXCEPT
+#else // DOCTEST_MSVC
+#define DOCTEST_NOEXCEPT noexcept
+#endif // DOCTEST_MSVC
+#endif // DOCTEST_NOEXCEPT
+
+#ifndef DOCTEST_CONSTEXPR
+#if DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0))
+#define DOCTEST_CONSTEXPR const
+#define DOCTEST_CONSTEXPR_FUNC inline
+#else // DOCTEST_MSVC
+#define DOCTEST_CONSTEXPR constexpr
+#define DOCTEST_CONSTEXPR_FUNC constexpr
+#endif // DOCTEST_MSVC
+#endif // DOCTEST_CONSTEXPR
+
+// =================================================================================================
+// == FEATURE DETECTION END ========================================================================
+// =================================================================================================
+
+#define DOCTEST_DECLARE_INTERFACE(name) \
+ virtual ~name(); \
+ name() = default; \
+ name(const name&) = delete; \
+ name(name&&) = delete; \
+ name& operator=(const name&) = delete; \
+ name& operator=(name&&) = delete;
+
+#define DOCTEST_DEFINE_INTERFACE(name) \
+ name::~name() = default;
+
+// internal macros for string concatenation and anonymous variable name generation
+#define DOCTEST_CAT_IMPL(s1, s2) s1##s2
+#define DOCTEST_CAT(s1, s2) DOCTEST_CAT_IMPL(s1, s2)
+#ifdef __COUNTER__ // not standard and may be missing for some compilers
+#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __COUNTER__)
+#else // __COUNTER__
+#define DOCTEST_ANONYMOUS(x) DOCTEST_CAT(x, __LINE__)
+#endif // __COUNTER__
+
+#ifndef DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE
+#define DOCTEST_REF_WRAP(x) x&
+#else // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE
+#define DOCTEST_REF_WRAP(x) x
+#endif // DOCTEST_CONFIG_ASSERTION_PARAMETERS_BY_VALUE
+
+// not using __APPLE__ because... this is how Catch does it
+#ifdef __MAC_OS_X_VERSION_MIN_REQUIRED
+#define DOCTEST_PLATFORM_MAC
+#elif defined(__IPHONE_OS_VERSION_MIN_REQUIRED)
+#define DOCTEST_PLATFORM_IPHONE
+#elif defined(_WIN32)
+#define DOCTEST_PLATFORM_WINDOWS
+#elif defined(__wasi__)
+#define DOCTEST_PLATFORM_WASI
+#else // DOCTEST_PLATFORM
+#define DOCTEST_PLATFORM_LINUX
+#endif // DOCTEST_PLATFORM
+
+namespace doctest { namespace detail {
+ static DOCTEST_CONSTEXPR int consume(const int*, int) noexcept { return 0; }
+}}
+
+#define DOCTEST_GLOBAL_NO_WARNINGS(var, ...) \
+ DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wglobal-constructors") \
+ static const int var = doctest::detail::consume(&var, __VA_ARGS__); \
+ DOCTEST_CLANG_SUPPRESS_WARNING_POP
+
+#ifndef DOCTEST_BREAK_INTO_DEBUGGER
+// should probably take a look at https://github.com/scottt/debugbreak
+#ifdef DOCTEST_PLATFORM_LINUX
+#if defined(__GNUC__) && (defined(__i386) || defined(__x86_64))
+// Break at the location of the failing check if possible
+#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT(hicpp-no-assembler)
+#else
+#include <signal.h>
+#define DOCTEST_BREAK_INTO_DEBUGGER() raise(SIGTRAP)
+#endif
+#elif defined(DOCTEST_PLATFORM_MAC)
+#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64__) || defined(__i386)
+#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("int $3\n" : :) // NOLINT(hicpp-no-assembler)
+#elif defined(__ppc__) || defined(__ppc64__)
+// https://www.cocoawithlove.com/2008/03/break-into-debugger.html
+#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("li r0, 20\nsc\nnop\nli r0, 37\nli r4, 2\nsc\nnop\n": : : "memory","r0","r3","r4") // NOLINT(hicpp-no-assembler)
+#else
+#define DOCTEST_BREAK_INTO_DEBUGGER() __asm__("brk #0"); // NOLINT(hicpp-no-assembler)
+#endif
+#elif DOCTEST_MSVC
+#define DOCTEST_BREAK_INTO_DEBUGGER() __debugbreak()
+#elif defined(__MINGW32__)
+DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wredundant-decls")
+extern "C" __declspec(dllimport) void __stdcall DebugBreak();
+DOCTEST_GCC_SUPPRESS_WARNING_POP
+#define DOCTEST_BREAK_INTO_DEBUGGER() ::DebugBreak()
+#else // linux
+#define DOCTEST_BREAK_INTO_DEBUGGER() (static_cast<void>(0))
+#endif // linux
+#endif // DOCTEST_BREAK_INTO_DEBUGGER
+
+// this is kept here for backwards compatibility since the config option was changed
+#ifdef DOCTEST_CONFIG_USE_IOSFWD
+#ifndef DOCTEST_CONFIG_USE_STD_HEADERS
+#define DOCTEST_CONFIG_USE_STD_HEADERS
+#endif
+#endif // DOCTEST_CONFIG_USE_IOSFWD
+
+// for clang - always include ciso646 (which drags some std stuff) because
+// we want to check if we are using libc++ with the _LIBCPP_VERSION macro in
+// which case we don't want to forward declare stuff from std - for reference:
+// https://github.com/doctest/doctest/issues/126
+// https://github.com/doctest/doctest/issues/356
+#if DOCTEST_CLANG
+#include <ciso646>
+#ifdef _LIBCPP_VERSION
+#ifndef DOCTEST_CONFIG_USE_STD_HEADERS
+#define DOCTEST_CONFIG_USE_STD_HEADERS
+#endif
+#endif // _LIBCPP_VERSION
+#endif // clang
+
+#ifdef DOCTEST_CONFIG_USE_STD_HEADERS
+#ifndef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+#define DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN
+#include <cstddef>
+#include <ostream>
+#include <istream>
+DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END
+#else // DOCTEST_CONFIG_USE_STD_HEADERS
+
+// Forward declaring 'X' in namespace std is not permitted by the C++ Standard.
+DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4643)
+
+namespace std { // NOLINT(cert-dcl58-cpp)
+typedef decltype(nullptr) nullptr_t; // NOLINT(modernize-use-using)
+typedef decltype(sizeof(void*)) size_t; // NOLINT(modernize-use-using)
+template <class charT>
+struct char_traits;
+template <>
+struct char_traits<char>;
+template <class charT, class traits>
+class basic_ostream; // NOLINT(fuchsia-virtual-inheritance)
+typedef basic_ostream<char, char_traits<char>> ostream; // NOLINT(modernize-use-using)
+template<class traits>
+// NOLINTNEXTLINE
+basic_ostream<char, traits>& operator<<(basic_ostream<char, traits>&, const char*);
+template <class charT, class traits>
+class basic_istream;
+typedef basic_istream<char, char_traits<char>> istream; // NOLINT(modernize-use-using)
+template <class... Types>
+class tuple;
+#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0)
+// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183
+template <class Ty>
+class allocator;
+template <class Elem, class Traits, class Alloc>
+class basic_string;
+using string = basic_string<char, char_traits<char>, allocator<char>>;
+#endif // VS 2019
+} // namespace std
+
+DOCTEST_MSVC_SUPPRESS_WARNING_POP
+
+#endif // DOCTEST_CONFIG_USE_STD_HEADERS
+
+#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+#include <type_traits>
+#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+
+namespace doctest {
+
+using std::size_t;
+
+DOCTEST_INTERFACE extern bool is_running_in_test;
+
+#ifndef DOCTEST_CONFIG_STRING_SIZE_TYPE
+#define DOCTEST_CONFIG_STRING_SIZE_TYPE unsigned
+#endif
+
+// A 24 byte string class (can be as small as 17 for x64 and 13 for x86) that can hold strings with length
+// of up to 23 chars on the stack before going on the heap - the last byte of the buffer is used for:
+// - "is small" bit - the highest bit - if "0" then it is small - otherwise its "1" (128)
+// - if small - capacity left before going on the heap - using the lowest 5 bits
+// - if small - 2 bits are left unused - the second and third highest ones
+// - if small - acts as a null terminator if strlen() is 23 (24 including the null terminator)
+// and the "is small" bit remains "0" ("as well as the capacity left") so its OK
+// Idea taken from this lecture about the string implementation of facebook/folly - fbstring
+// https://www.youtube.com/watch?v=kPR8h4-qZdk
+// TODO:
+// - optimizations - like not deleting memory unnecessarily in operator= and etc.
+// - resize/reserve/clear
+// - replace
+// - back/front
+// - iterator stuff
+// - find & friends
+// - push_back/pop_back
+// - assign/insert/erase
+// - relational operators as free functions - taking const char* as one of the params
+class DOCTEST_INTERFACE String
+{
+public:
+ using size_type = DOCTEST_CONFIG_STRING_SIZE_TYPE;
+
+private:
+ static DOCTEST_CONSTEXPR size_type len = 24; //!OCLINT avoid private static members
+ static DOCTEST_CONSTEXPR size_type last = len - 1; //!OCLINT avoid private static members
+
+ struct view // len should be more than sizeof(view) - because of the final byte for flags
+ {
+ char* ptr;
+ size_type size;
+ size_type capacity;
+ };
+
+ union
+ {
+ char buf[len]; // NOLINT(*-avoid-c-arrays)
+ view data;
+ };
+
+ char* allocate(size_type sz);
+
+ bool isOnStack() const noexcept { return (buf[last] & 128) == 0; }
+ void setOnHeap() noexcept;
+ void setLast(size_type in = last) noexcept;
+ void setSize(size_type sz) noexcept;
+
+ void copy(const String& other);
+
+public:
+ static DOCTEST_CONSTEXPR size_type npos = static_cast<size_type>(-1);
+
+ String() noexcept;
+ ~String();
+
+ // cppcheck-suppress noExplicitConstructor
+ String(const char* in);
+ String(const char* in, size_type in_size);
+
+ String(std::istream& in, size_type in_size);
+
+ String(const String& other);
+ String& operator=(const String& other);
+
+ String& operator+=(const String& other);
+
+ String(String&& other) noexcept;
+ String& operator=(String&& other) noexcept;
+
+ char operator[](size_type i) const;
+ char& operator[](size_type i);
+
+ // the only functions I'm willing to leave in the interface - available for inlining
+ const char* c_str() const { return const_cast<String*>(this)->c_str(); } // NOLINT
+ char* c_str() {
+ if (isOnStack()) {
+ return reinterpret_cast<char*>(buf);
+ }
+ return data.ptr;
+ }
+
+ size_type size() const;
+ size_type capacity() const;
+
+ String substr(size_type pos, size_type cnt = npos) &&;
+ String substr(size_type pos, size_type cnt = npos) const &;
+
+ size_type find(char ch, size_type pos = 0) const;
+ size_type rfind(char ch, size_type pos = npos) const;
+
+ int compare(const char* other, bool no_case = false) const;
+ int compare(const String& other, bool no_case = false) const;
+
+friend DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, const String& in);
+};
+
+DOCTEST_INTERFACE String operator+(const String& lhs, const String& rhs);
+
+DOCTEST_INTERFACE bool operator==(const String& lhs, const String& rhs);
+DOCTEST_INTERFACE bool operator!=(const String& lhs, const String& rhs);
+DOCTEST_INTERFACE bool operator<(const String& lhs, const String& rhs);
+DOCTEST_INTERFACE bool operator>(const String& lhs, const String& rhs);
+DOCTEST_INTERFACE bool operator<=(const String& lhs, const String& rhs);
+DOCTEST_INTERFACE bool operator>=(const String& lhs, const String& rhs);
+
+class DOCTEST_INTERFACE Contains {
+public:
+ explicit Contains(const String& string);
+
+ bool checkWith(const String& other) const;
+
+ String string;
+};
+
+DOCTEST_INTERFACE String toString(const Contains& in);
+
+DOCTEST_INTERFACE bool operator==(const String& lhs, const Contains& rhs);
+DOCTEST_INTERFACE bool operator==(const Contains& lhs, const String& rhs);
+DOCTEST_INTERFACE bool operator!=(const String& lhs, const Contains& rhs);
+DOCTEST_INTERFACE bool operator!=(const Contains& lhs, const String& rhs);
+
+namespace Color {
+ enum Enum
+ {
+ None = 0,
+ White,
+ Red,
+ Green,
+ Blue,
+ Cyan,
+ Yellow,
+ Grey,
+
+ Bright = 0x10,
+
+ BrightRed = Bright | Red,
+ BrightGreen = Bright | Green,
+ LightGrey = Bright | Grey,
+ BrightWhite = Bright | White
+ };
+
+ DOCTEST_INTERFACE std::ostream& operator<<(std::ostream& s, Color::Enum code);
+} // namespace Color
+
+namespace assertType {
+ enum Enum
+ {
+ // macro traits
+
+ is_warn = 1,
+ is_check = 2 * is_warn,
+ is_require = 2 * is_check,
+
+ is_normal = 2 * is_require,
+ is_throws = 2 * is_normal,
+ is_throws_as = 2 * is_throws,
+ is_throws_with = 2 * is_throws_as,
+ is_nothrow = 2 * is_throws_with,
+
+ is_false = 2 * is_nothrow,
+ is_unary = 2 * is_false, // not checked anywhere - used just to distinguish the types
+
+ is_eq = 2 * is_unary,
+ is_ne = 2 * is_eq,
+
+ is_lt = 2 * is_ne,
+ is_gt = 2 * is_lt,
+
+ is_ge = 2 * is_gt,
+ is_le = 2 * is_ge,
+
+ // macro types
+
+ DT_WARN = is_normal | is_warn,
+ DT_CHECK = is_normal | is_check,
+ DT_REQUIRE = is_normal | is_require,
+
+ DT_WARN_FALSE = is_normal | is_false | is_warn,
+ DT_CHECK_FALSE = is_normal | is_false | is_check,
+ DT_REQUIRE_FALSE = is_normal | is_false | is_require,
+
+ DT_WARN_THROWS = is_throws | is_warn,
+ DT_CHECK_THROWS = is_throws | is_check,
+ DT_REQUIRE_THROWS = is_throws | is_require,
+
+ DT_WARN_THROWS_AS = is_throws_as | is_warn,
+ DT_CHECK_THROWS_AS = is_throws_as | is_check,
+ DT_REQUIRE_THROWS_AS = is_throws_as | is_require,
+
+ DT_WARN_THROWS_WITH = is_throws_with | is_warn,
+ DT_CHECK_THROWS_WITH = is_throws_with | is_check,
+ DT_REQUIRE_THROWS_WITH = is_throws_with | is_require,
+
+ DT_WARN_THROWS_WITH_AS = is_throws_with | is_throws_as | is_warn,
+ DT_CHECK_THROWS_WITH_AS = is_throws_with | is_throws_as | is_check,
+ DT_REQUIRE_THROWS_WITH_AS = is_throws_with | is_throws_as | is_require,
+
+ DT_WARN_NOTHROW = is_nothrow | is_warn,
+ DT_CHECK_NOTHROW = is_nothrow | is_check,
+ DT_REQUIRE_NOTHROW = is_nothrow | is_require,
+
+ DT_WARN_EQ = is_normal | is_eq | is_warn,
+ DT_CHECK_EQ = is_normal | is_eq | is_check,
+ DT_REQUIRE_EQ = is_normal | is_eq | is_require,
+
+ DT_WARN_NE = is_normal | is_ne | is_warn,
+ DT_CHECK_NE = is_normal | is_ne | is_check,
+ DT_REQUIRE_NE = is_normal | is_ne | is_require,
+
+ DT_WARN_GT = is_normal | is_gt | is_warn,
+ DT_CHECK_GT = is_normal | is_gt | is_check,
+ DT_REQUIRE_GT = is_normal | is_gt | is_require,
+
+ DT_WARN_LT = is_normal | is_lt | is_warn,
+ DT_CHECK_LT = is_normal | is_lt | is_check,
+ DT_REQUIRE_LT = is_normal | is_lt | is_require,
+
+ DT_WARN_GE = is_normal | is_ge | is_warn,
+ DT_CHECK_GE = is_normal | is_ge | is_check,
+ DT_REQUIRE_GE = is_normal | is_ge | is_require,
+
+ DT_WARN_LE = is_normal | is_le | is_warn,
+ DT_CHECK_LE = is_normal | is_le | is_check,
+ DT_REQUIRE_LE = is_normal | is_le | is_require,
+
+ DT_WARN_UNARY = is_normal | is_unary | is_warn,
+ DT_CHECK_UNARY = is_normal | is_unary | is_check,
+ DT_REQUIRE_UNARY = is_normal | is_unary | is_require,
+
+ DT_WARN_UNARY_FALSE = is_normal | is_false | is_unary | is_warn,
+ DT_CHECK_UNARY_FALSE = is_normal | is_false | is_unary | is_check,
+ DT_REQUIRE_UNARY_FALSE = is_normal | is_false | is_unary | is_require,
+ };
+} // namespace assertType
+
+DOCTEST_INTERFACE const char* assertString(assertType::Enum at);
+DOCTEST_INTERFACE const char* failureString(assertType::Enum at);
+DOCTEST_INTERFACE const char* skipPathFromFilename(const char* file);
+
+struct DOCTEST_INTERFACE TestCaseData
+{
+ String m_file; // the file in which the test was registered (using String - see #350)
+ unsigned m_line; // the line where the test was registered
+ const char* m_name; // name of the test case
+ const char* m_test_suite; // the test suite in which the test was added
+ const char* m_description;
+ bool m_skip;
+ bool m_no_breaks;
+ bool m_no_output;
+ bool m_may_fail;
+ bool m_should_fail;
+ int m_expected_failures;
+ double m_timeout;
+};
+
+struct DOCTEST_INTERFACE AssertData
+{
+ // common - for all asserts
+ const TestCaseData* m_test_case;
+ assertType::Enum m_at;
+ const char* m_file;
+ int m_line;
+ const char* m_expr;
+ bool m_failed;
+
+ // exception-related - for all asserts
+ bool m_threw;
+ String m_exception;
+
+ // for normal asserts
+ String m_decomp;
+
+ // for specific exception-related asserts
+ bool m_threw_as;
+ const char* m_exception_type;
+
+ class DOCTEST_INTERFACE StringContains {
+ private:
+ Contains content;
+ bool isContains;
+
+ public:
+ StringContains(const String& str) : content(str), isContains(false) { }
+ StringContains(Contains cntn) : content(static_cast<Contains&&>(cntn)), isContains(true) { }
+
+ bool check(const String& str) { return isContains ? (content == str) : (content.string == str); }
+
+ operator const String&() const { return content.string; }
+
+ const char* c_str() const { return content.string.c_str(); }
+ } m_exception_string;
+
+ AssertData(assertType::Enum at, const char* file, int line, const char* expr,
+ const char* exception_type, const StringContains& exception_string);
+};
+
+struct DOCTEST_INTERFACE MessageData
+{
+ String m_string;
+ const char* m_file;
+ int m_line;
+ assertType::Enum m_severity;
+};
+
+struct DOCTEST_INTERFACE SubcaseSignature
+{
+ String m_name;
+ const char* m_file;
+ int m_line;
+
+ bool operator==(const SubcaseSignature& other) const;
+ bool operator<(const SubcaseSignature& other) const;
+};
+
+struct DOCTEST_INTERFACE IContextScope
+{
+ DOCTEST_DECLARE_INTERFACE(IContextScope)
+ virtual void stringify(std::ostream*) const = 0;
+};
+
+namespace detail {
+ struct DOCTEST_INTERFACE TestCase;
+} // namespace detail
+
+struct ContextOptions //!OCLINT too many fields
+{
+ std::ostream* cout = nullptr; // stdout stream
+ String binary_name; // the test binary name
+
+ const detail::TestCase* currentTest = nullptr;
+
+ // == parameters from the command line
+ String out; // output filename
+ String order_by; // how tests should be ordered
+ unsigned rand_seed; // the seed for rand ordering
+
+ unsigned first; // the first (matching) test to be executed
+ unsigned last; // the last (matching) test to be executed
+
+ int abort_after; // stop tests after this many failed assertions
+ int subcase_filter_levels; // apply the subcase filters for the first N levels
+
+ bool success; // include successful assertions in output
+ bool case_sensitive; // if filtering should be case sensitive
+ bool exit; // if the program should be exited after the tests are ran/whatever
+ bool duration; // print the time duration of each test case
+ bool minimal; // minimal console output (only test failures)
+ bool quiet; // no console output
+ bool no_throw; // to skip exceptions-related assertion macros
+ bool no_exitcode; // if the framework should return 0 as the exitcode
+ bool no_run; // to not run the tests at all (can be done with an "*" exclude)
+ bool no_intro; // to not print the intro of the framework
+ bool no_version; // to not print the version of the framework
+ bool no_colors; // if output to the console should be colorized
+ bool force_colors; // forces the use of colors even when a tty cannot be detected
+ bool no_breaks; // to not break into the debugger
+ bool no_skip; // don't skip test cases which are marked to be skipped
+ bool gnu_file_line; // if line numbers should be surrounded with :x: and not (x):
+ bool no_path_in_filenames; // if the path to files should be removed from the output
+ bool no_line_numbers; // if source code line numbers should be omitted from the output
+ bool no_debug_output; // no output in the debug console when a debugger is attached
+ bool no_skipped_summary; // don't print "skipped" in the summary !!! UNDOCUMENTED !!!
+ bool no_time_in_output; // omit any time/timestamps from output !!! UNDOCUMENTED !!!
+
+ bool help; // to print the help
+ bool version; // to print the version
+ bool count; // if only the count of matching tests is to be retrieved
+ bool list_test_cases; // to list all tests matching the filters
+ bool list_test_suites; // to list all suites matching the filters
+ bool list_reporters; // lists all registered reporters
+};
+
+namespace detail {
+ namespace types {
+#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+ using namespace std;
+#else
+ template <bool COND, typename T = void>
+ struct enable_if { };
+
+ template <typename T>
+ struct enable_if<true, T> { using type = T; };
+
+ struct true_type { static DOCTEST_CONSTEXPR bool value = true; };
+ struct false_type { static DOCTEST_CONSTEXPR bool value = false; };
+
+ template <typename T> struct remove_reference { using type = T; };
+ template <typename T> struct remove_reference<T&> { using type = T; };
+ template <typename T> struct remove_reference<T&&> { using type = T; };
+
+ template <typename T> struct is_rvalue_reference : false_type { };
+ template <typename T> struct is_rvalue_reference<T&&> : true_type { };
+
+ template<typename T> struct remove_const { using type = T; };
+ template <typename T> struct remove_const<const T> { using type = T; };
+
+ // Compiler intrinsics
+ template <typename T> struct is_enum { static DOCTEST_CONSTEXPR bool value = __is_enum(T); };
+ template <typename T> struct underlying_type { using type = __underlying_type(T); };
+
+ template <typename T> struct is_pointer : false_type { };
+ template <typename T> struct is_pointer<T*> : true_type { };
+
+ template <typename T> struct is_array : false_type { };
+ // NOLINTNEXTLINE(*-avoid-c-arrays)
+ template <typename T, size_t SIZE> struct is_array<T[SIZE]> : true_type { };
+#endif
+ }
+
+ // <utility>
+ template <typename T>
+ T&& declval();
+
+ template <class T>
+ DOCTEST_CONSTEXPR_FUNC T&& forward(typename types::remove_reference<T>::type& t) DOCTEST_NOEXCEPT {
+ return static_cast<T&&>(t);
+ }
+
+ template <class T>
+ DOCTEST_CONSTEXPR_FUNC T&& forward(typename types::remove_reference<T>::type&& t) DOCTEST_NOEXCEPT {
+ return static_cast<T&&>(t);
+ }
+
+ template <typename T>
+ struct deferred_false : types::false_type { };
+
+// MSVS 2015 :(
+#if defined(_MSC_VER) && _MSC_VER <= 1900
+ template <typename T, typename = void>
+ struct has_global_insertion_operator : types::false_type { };
+
+ template <typename T>
+ struct has_global_insertion_operator<T, decltype(::operator<<(declval<std::ostream&>(), declval<const T&>()), void())> : types::true_type { };
+
+ template <typename T, typename = void>
+ struct has_insertion_operator { static DOCTEST_CONSTEXPR bool value = has_global_insertion_operator<T>::value; };
+
+ template <typename T, bool global>
+ struct insert_hack;
+
+ template <typename T>
+ struct insert_hack<T, true> {
+ static void insert(std::ostream& os, const T& t) { ::operator<<(os, t); }
+ };
+
+ template <typename T>
+ struct insert_hack<T, false> {
+ static void insert(std::ostream& os, const T& t) { operator<<(os, t); }
+ };
+
+ template <typename T>
+ using insert_hack_t = insert_hack<T, has_global_insertion_operator<T>::value>;
+#else
+ template <typename T, typename = void>
+ struct has_insertion_operator : types::false_type { };
+#endif
+
+template <typename T>
+struct has_insertion_operator<T, decltype(operator<<(declval<std::ostream&>(), declval<const T&>()), void())> : types::true_type { };
+
+ DOCTEST_INTERFACE std::ostream* tlssPush();
+ DOCTEST_INTERFACE String tlssPop();
+
+ template <bool C>
+ struct StringMakerBase {
+ template <typename T>
+ static String convert(const DOCTEST_REF_WRAP(T)) {
+#ifdef DOCTEST_CONFIG_REQUIRE_STRINGIFICATION_FOR_ALL_USED_TYPES
+ static_assert(deferred_false<T>::value, "No stringification detected for type T. See string conversion manual");
+#endif
+ return "{?}";
+ }
+ };
+
+ template <typename T>
+ struct filldata;
+
+ template <typename T>
+ void filloss(std::ostream* stream, const T& in) {
+ filldata<T>::fill(stream, in);
+ }
+
+ template <typename T, size_t N>
+ void filloss(std::ostream* stream, const T (&in)[N]) { // NOLINT(*-avoid-c-arrays)
+ // T[N], T(&)[N], T(&&)[N] have same behaviour.
+ // Hence remove reference.
+ filloss<typename types::remove_reference<decltype(in)>::type>(stream, in);
+ }
+
+ template <typename T>
+ String toStream(const T& in) {
+ std::ostream* stream = tlssPush();
+ filloss(stream, in);
+ return tlssPop();
+ }
+
+ template <>
+ struct StringMakerBase<true> {
+ template <typename T>
+ static String convert(const DOCTEST_REF_WRAP(T) in) {
+ return toStream(in);
+ }
+ };
+} // namespace detail
+
+template <typename T>
+struct StringMaker : public detail::StringMakerBase<
+ detail::has_insertion_operator<T>::value || detail::types::is_pointer<T>::value || detail::types::is_array<T>::value>
+{};
+
+#ifndef DOCTEST_STRINGIFY
+#ifdef DOCTEST_CONFIG_DOUBLE_STRINGIFY
+#define DOCTEST_STRINGIFY(...) toString(toString(__VA_ARGS__))
+#else
+#define DOCTEST_STRINGIFY(...) toString(__VA_ARGS__)
+#endif
+#endif
+
+template <typename T>
+String toString() {
+#if DOCTEST_MSVC >= 0 && DOCTEST_CLANG == 0 && DOCTEST_GCC == 0
+ String ret = __FUNCSIG__; // class doctest::String __cdecl doctest::toString<TYPE>(void)
+ String::size_type beginPos = ret.find('<');
+ return ret.substr(beginPos + 1, ret.size() - beginPos - static_cast<String::size_type>(sizeof(">(void)")));
+#else
+ String ret = __PRETTY_FUNCTION__; // doctest::String toString() [with T = TYPE]
+ String::size_type begin = ret.find('=') + 2;
+ return ret.substr(begin, ret.size() - begin - 1);
+#endif
+}
+
+template <typename T, typename detail::types::enable_if<!detail::types::is_enum<T>::value, bool>::type = true>
+String toString(const DOCTEST_REF_WRAP(T) value) {
+ return StringMaker<T>::convert(value);
+}
+
+#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+DOCTEST_INTERFACE String toString(const char* in);
+#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+
+#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0)
+// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183
+DOCTEST_INTERFACE String toString(const std::string& in);
+#endif // VS 2019
+
+DOCTEST_INTERFACE String toString(String in);
+
+DOCTEST_INTERFACE String toString(std::nullptr_t);
+
+DOCTEST_INTERFACE String toString(bool in);
+
+DOCTEST_INTERFACE String toString(float in);
+DOCTEST_INTERFACE String toString(double in);
+DOCTEST_INTERFACE String toString(double long in);
+
+DOCTEST_INTERFACE String toString(char in);
+DOCTEST_INTERFACE String toString(char signed in);
+DOCTEST_INTERFACE String toString(char unsigned in);
+DOCTEST_INTERFACE String toString(short in);
+DOCTEST_INTERFACE String toString(short unsigned in);
+DOCTEST_INTERFACE String toString(signed in);
+DOCTEST_INTERFACE String toString(unsigned in);
+DOCTEST_INTERFACE String toString(long in);
+DOCTEST_INTERFACE String toString(long unsigned in);
+DOCTEST_INTERFACE String toString(long long in);
+DOCTEST_INTERFACE String toString(long long unsigned in);
+
+template <typename T, typename detail::types::enable_if<detail::types::is_enum<T>::value, bool>::type = true>
+String toString(const DOCTEST_REF_WRAP(T) value) {
+ using UT = typename detail::types::underlying_type<T>::type;
+ return (DOCTEST_STRINGIFY(static_cast<UT>(value)));
+}
+
+namespace detail {
+ template <typename T>
+ struct filldata
+ {
+ static void fill(std::ostream* stream, const T& in) {
+#if defined(_MSC_VER) && _MSC_VER <= 1900
+ insert_hack_t<T>::insert(*stream, in);
+#else
+ operator<<(*stream, in);
+#endif
+ }
+ };
+
+DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866)
+// NOLINTBEGIN(*-avoid-c-arrays)
+ template <typename T, size_t N>
+ struct filldata<T[N]> {
+ static void fill(std::ostream* stream, const T(&in)[N]) {
+ *stream << "[";
+ for (size_t i = 0; i < N; i++) {
+ if (i != 0) { *stream << ", "; }
+ *stream << (DOCTEST_STRINGIFY(in[i]));
+ }
+ *stream << "]";
+ }
+ };
+// NOLINTEND(*-avoid-c-arrays)
+DOCTEST_MSVC_SUPPRESS_WARNING_POP
+
+ // Specialized since we don't want the terminating null byte!
+// NOLINTBEGIN(*-avoid-c-arrays)
+ template <size_t N>
+ struct filldata<const char[N]> {
+ static void fill(std::ostream* stream, const char (&in)[N]) {
+ *stream << String(in, in[N - 1] ? N : N - 1);
+ } // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
+ };
+// NOLINTEND(*-avoid-c-arrays)
+
+ template <>
+ struct filldata<const void*> {
+ static void fill(std::ostream* stream, const void* in);
+ };
+
+ template <typename T>
+ struct filldata<T*> {
+ static void fill(std::ostream* stream, const T* in) {
+ filldata<const void*>::fill(stream, in);
+ }
+ };
+}
+
+struct DOCTEST_INTERFACE Approx
+{
+ Approx(double value);
+
+ Approx operator()(double value) const;
+
+#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+ template <typename T>
+ explicit Approx(const T& value,
+ typename detail::types::enable_if<std::is_constructible<double, T>::value>::type* =
+ static_cast<T*>(nullptr)) {
+ *this = static_cast<double>(value);
+ }
+#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+
+ Approx& epsilon(double newEpsilon);
+
+#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+ template <typename T>
+ typename std::enable_if<std::is_constructible<double, T>::value, Approx&>::type epsilon(
+ const T& newEpsilon) {
+ m_epsilon = static_cast<double>(newEpsilon);
+ return *this;
+ }
+#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+
+ Approx& scale(double newScale);
+
+#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+ template <typename T>
+ typename std::enable_if<std::is_constructible<double, T>::value, Approx&>::type scale(
+ const T& newScale) {
+ m_scale = static_cast<double>(newScale);
+ return *this;
+ }
+#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+
+ // clang-format off
+ DOCTEST_INTERFACE friend bool operator==(double lhs, const Approx & rhs);
+ DOCTEST_INTERFACE friend bool operator==(const Approx & lhs, double rhs);
+ DOCTEST_INTERFACE friend bool operator!=(double lhs, const Approx & rhs);
+ DOCTEST_INTERFACE friend bool operator!=(const Approx & lhs, double rhs);
+ DOCTEST_INTERFACE friend bool operator<=(double lhs, const Approx & rhs);
+ DOCTEST_INTERFACE friend bool operator<=(const Approx & lhs, double rhs);
+ DOCTEST_INTERFACE friend bool operator>=(double lhs, const Approx & rhs);
+ DOCTEST_INTERFACE friend bool operator>=(const Approx & lhs, double rhs);
+ DOCTEST_INTERFACE friend bool operator< (double lhs, const Approx & rhs);
+ DOCTEST_INTERFACE friend bool operator< (const Approx & lhs, double rhs);
+ DOCTEST_INTERFACE friend bool operator> (double lhs, const Approx & rhs);
+ DOCTEST_INTERFACE friend bool operator> (const Approx & lhs, double rhs);
+
+#ifdef DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+#define DOCTEST_APPROX_PREFIX \
+ template <typename T> friend typename std::enable_if<std::is_constructible<double, T>::value, bool>::type
+
+ DOCTEST_APPROX_PREFIX operator==(const T& lhs, const Approx& rhs) { return operator==(static_cast<double>(lhs), rhs); }
+ DOCTEST_APPROX_PREFIX operator==(const Approx& lhs, const T& rhs) { return operator==(rhs, lhs); }
+ DOCTEST_APPROX_PREFIX operator!=(const T& lhs, const Approx& rhs) { return !operator==(lhs, rhs); }
+ DOCTEST_APPROX_PREFIX operator!=(const Approx& lhs, const T& rhs) { return !operator==(rhs, lhs); }
+ DOCTEST_APPROX_PREFIX operator<=(const T& lhs, const Approx& rhs) { return static_cast<double>(lhs) < rhs.m_value || lhs == rhs; }
+ DOCTEST_APPROX_PREFIX operator<=(const Approx& lhs, const T& rhs) { return lhs.m_value < static_cast<double>(rhs) || lhs == rhs; }
+ DOCTEST_APPROX_PREFIX operator>=(const T& lhs, const Approx& rhs) { return static_cast<double>(lhs) > rhs.m_value || lhs == rhs; }
+ DOCTEST_APPROX_PREFIX operator>=(const Approx& lhs, const T& rhs) { return lhs.m_value > static_cast<double>(rhs) || lhs == rhs; }
+ DOCTEST_APPROX_PREFIX operator< (const T& lhs, const Approx& rhs) { return static_cast<double>(lhs) < rhs.m_value && lhs != rhs; }
+ DOCTEST_APPROX_PREFIX operator< (const Approx& lhs, const T& rhs) { return lhs.m_value < static_cast<double>(rhs) && lhs != rhs; }
+ DOCTEST_APPROX_PREFIX operator> (const T& lhs, const Approx& rhs) { return static_cast<double>(lhs) > rhs.m_value && lhs != rhs; }
+ DOCTEST_APPROX_PREFIX operator> (const Approx& lhs, const T& rhs) { return lhs.m_value > static_cast<double>(rhs) && lhs != rhs; }
+#undef DOCTEST_APPROX_PREFIX
+#endif // DOCTEST_CONFIG_INCLUDE_TYPE_TRAITS
+
+ // clang-format on
+
+ double m_epsilon;
+ double m_scale;
+ double m_value;
+};
+
+DOCTEST_INTERFACE String toString(const Approx& in);
+
+DOCTEST_INTERFACE const ContextOptions* getContextOptions();
+
+template <typename F>
+struct DOCTEST_INTERFACE_DECL IsNaN
+{
+ F value; bool flipped;
+ IsNaN(F f, bool flip = false) : value(f), flipped(flip) { }
+ IsNaN<F> operator!() const { return { value, !flipped }; }
+ operator bool() const;
+};
+#ifndef __MINGW32__
+extern template struct DOCTEST_INTERFACE_DECL IsNaN<float>;
+extern template struct DOCTEST_INTERFACE_DECL IsNaN<double>;
+extern template struct DOCTEST_INTERFACE_DECL IsNaN<long double>;
+#endif
+DOCTEST_INTERFACE String toString(IsNaN<float> in);
+DOCTEST_INTERFACE String toString(IsNaN<double> in);
+DOCTEST_INTERFACE String toString(IsNaN<double long> in);
+
+#ifndef DOCTEST_CONFIG_DISABLE
+
+namespace detail {
+ // clang-format off
+#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+ template<class T> struct decay_array { using type = T; };
+ template<class T, unsigned N> struct decay_array<T[N]> { using type = T*; };
+ template<class T> struct decay_array<T[]> { using type = T*; };
+
+ template<class T> struct not_char_pointer { static DOCTEST_CONSTEXPR value = 1; };
+ template<> struct not_char_pointer<char*> { static DOCTEST_CONSTEXPR value = 0; };
+ template<> struct not_char_pointer<const char*> { static DOCTEST_CONSTEXPR value = 0; };
+
+ template<class T> struct can_use_op : public not_char_pointer<typename decay_array<T>::type> {};
+#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+ // clang-format on
+
+ struct DOCTEST_INTERFACE TestFailureException
+ {
+ };
+
+ DOCTEST_INTERFACE bool checkIfShouldThrow(assertType::Enum at);
+
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+ DOCTEST_NORETURN
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+ DOCTEST_INTERFACE void throwException();
+
+ struct DOCTEST_INTERFACE Subcase
+ {
+ SubcaseSignature m_signature;
+ bool m_entered = false;
+
+ Subcase(const String& name, const char* file, int line);
+ Subcase(const Subcase&) = delete;
+ Subcase(Subcase&&) = delete;
+ Subcase& operator=(const Subcase&) = delete;
+ Subcase& operator=(Subcase&&) = delete;
+ ~Subcase();
+
+ operator bool() const;
+
+ private:
+ bool checkFilters();
+ };
+
+ template <typename L, typename R>
+ String stringifyBinaryExpr(const DOCTEST_REF_WRAP(L) lhs, const char* op,
+ const DOCTEST_REF_WRAP(R) rhs) {
+ return (DOCTEST_STRINGIFY(lhs)) + op + (DOCTEST_STRINGIFY(rhs));
+ }
+
+#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0)
+DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-comparison")
+#endif
+
+// This will check if there is any way it could find a operator like member or friend and uses it.
+// If not it doesn't find the operator or if the operator at global scope is defined after
+// this template, the template won't be instantiated due to SFINAE. Once the template is not
+// instantiated it can look for global operator using normal conversions.
+#define SFINAE_OP(ret,op) decltype((void)(doctest::detail::declval<L>() op doctest::detail::declval<R>()),ret{})
+
+#define DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(op, op_str, op_macro) \
+ template <typename R> \
+ DOCTEST_NOINLINE SFINAE_OP(Result,op) operator op(R&& rhs) { \
+ bool res = op_macro(doctest::detail::forward<const L>(lhs), doctest::detail::forward<R>(rhs)); \
+ if(m_at & assertType::is_false) \
+ res = !res; \
+ if(!res || doctest::getContextOptions()->success) \
+ return Result(res, stringifyBinaryExpr(lhs, op_str, rhs)); \
+ return Result(res); \
+ }
+
+ // more checks could be added - like in Catch:
+ // https://github.com/catchorg/Catch2/pull/1480/files
+ // https://github.com/catchorg/Catch2/pull/1481/files
+#define DOCTEST_FORBIT_EXPRESSION(rt, op) \
+ template <typename R> \
+ rt& operator op(const R&) { \
+ static_assert(deferred_false<R>::value, \
+ "Expression Too Complex Please Rewrite As Binary Comparison!"); \
+ return *this; \
+ }
+
+ struct DOCTEST_INTERFACE Result // NOLINT(*-member-init)
+ {
+ bool m_passed;
+ String m_decomp;
+
+ Result() = default; // TODO: Why do we need this? (To remove NOLINT)
+ Result(bool passed, const String& decomposition = String());
+
+ // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence
+ DOCTEST_FORBIT_EXPRESSION(Result, &)
+ DOCTEST_FORBIT_EXPRESSION(Result, ^)
+ DOCTEST_FORBIT_EXPRESSION(Result, |)
+ DOCTEST_FORBIT_EXPRESSION(Result, &&)
+ DOCTEST_FORBIT_EXPRESSION(Result, ||)
+ DOCTEST_FORBIT_EXPRESSION(Result, ==)
+ DOCTEST_FORBIT_EXPRESSION(Result, !=)
+ DOCTEST_FORBIT_EXPRESSION(Result, <)
+ DOCTEST_FORBIT_EXPRESSION(Result, >)
+ DOCTEST_FORBIT_EXPRESSION(Result, <=)
+ DOCTEST_FORBIT_EXPRESSION(Result, >=)
+ DOCTEST_FORBIT_EXPRESSION(Result, =)
+ DOCTEST_FORBIT_EXPRESSION(Result, +=)
+ DOCTEST_FORBIT_EXPRESSION(Result, -=)
+ DOCTEST_FORBIT_EXPRESSION(Result, *=)
+ DOCTEST_FORBIT_EXPRESSION(Result, /=)
+ DOCTEST_FORBIT_EXPRESSION(Result, %=)
+ DOCTEST_FORBIT_EXPRESSION(Result, <<=)
+ DOCTEST_FORBIT_EXPRESSION(Result, >>=)
+ DOCTEST_FORBIT_EXPRESSION(Result, &=)
+ DOCTEST_FORBIT_EXPRESSION(Result, ^=)
+ DOCTEST_FORBIT_EXPRESSION(Result, |=)
+ };
+
+#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION
+
+ DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
+ DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion")
+ DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-compare")
+ //DOCTEST_CLANG_SUPPRESS_WARNING("-Wdouble-promotion")
+ //DOCTEST_CLANG_SUPPRESS_WARNING("-Wconversion")
+ //DOCTEST_CLANG_SUPPRESS_WARNING("-Wfloat-equal")
+
+ DOCTEST_GCC_SUPPRESS_WARNING_PUSH
+ DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion")
+ DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-compare")
+ //DOCTEST_GCC_SUPPRESS_WARNING("-Wdouble-promotion")
+ //DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion")
+ //DOCTEST_GCC_SUPPRESS_WARNING("-Wfloat-equal")
+
+ DOCTEST_MSVC_SUPPRESS_WARNING_PUSH
+ // https://stackoverflow.com/questions/39479163 what's the difference between 4018 and 4389
+ DOCTEST_MSVC_SUPPRESS_WARNING(4388) // signed/unsigned mismatch
+ DOCTEST_MSVC_SUPPRESS_WARNING(4389) // 'operator' : signed/unsigned mismatch
+ DOCTEST_MSVC_SUPPRESS_WARNING(4018) // 'expression' : signed/unsigned mismatch
+ //DOCTEST_MSVC_SUPPRESS_WARNING(4805) // 'operation' : unsafe mix of type 'type' and type 'type' in operation
+
+#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION
+
+ // clang-format off
+#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+#define DOCTEST_COMPARISON_RETURN_TYPE bool
+#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+#define DOCTEST_COMPARISON_RETURN_TYPE typename types::enable_if<can_use_op<L>::value || can_use_op<R>::value, bool>::type
+ inline bool eq(const char* lhs, const char* rhs) { return String(lhs) == String(rhs); }
+ inline bool ne(const char* lhs, const char* rhs) { return String(lhs) != String(rhs); }
+ inline bool lt(const char* lhs, const char* rhs) { return String(lhs) < String(rhs); }
+ inline bool gt(const char* lhs, const char* rhs) { return String(lhs) > String(rhs); }
+ inline bool le(const char* lhs, const char* rhs) { return String(lhs) <= String(rhs); }
+ inline bool ge(const char* lhs, const char* rhs) { return String(lhs) >= String(rhs); }
+#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+ // clang-format on
+
+#define DOCTEST_RELATIONAL_OP(name, op) \
+ template <typename L, typename R> \
+ DOCTEST_COMPARISON_RETURN_TYPE name(const DOCTEST_REF_WRAP(L) lhs, \
+ const DOCTEST_REF_WRAP(R) rhs) { \
+ return lhs op rhs; \
+ }
+
+ DOCTEST_RELATIONAL_OP(eq, ==)
+ DOCTEST_RELATIONAL_OP(ne, !=)
+ DOCTEST_RELATIONAL_OP(lt, <)
+ DOCTEST_RELATIONAL_OP(gt, >)
+ DOCTEST_RELATIONAL_OP(le, <=)
+ DOCTEST_RELATIONAL_OP(ge, >=)
+
+#ifndef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+#define DOCTEST_CMP_EQ(l, r) l == r
+#define DOCTEST_CMP_NE(l, r) l != r
+#define DOCTEST_CMP_GT(l, r) l > r
+#define DOCTEST_CMP_LT(l, r) l < r
+#define DOCTEST_CMP_GE(l, r) l >= r
+#define DOCTEST_CMP_LE(l, r) l <= r
+#else // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+#define DOCTEST_CMP_EQ(l, r) eq(l, r)
+#define DOCTEST_CMP_NE(l, r) ne(l, r)
+#define DOCTEST_CMP_GT(l, r) gt(l, r)
+#define DOCTEST_CMP_LT(l, r) lt(l, r)
+#define DOCTEST_CMP_GE(l, r) ge(l, r)
+#define DOCTEST_CMP_LE(l, r) le(l, r)
+#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+
+ template <typename L>
+ // cppcheck-suppress copyCtorAndEqOperator
+ struct Expression_lhs
+ {
+ L lhs;
+ assertType::Enum m_at;
+
+ explicit Expression_lhs(L&& in, assertType::Enum at)
+ : lhs(static_cast<L&&>(in))
+ , m_at(at) {}
+
+ DOCTEST_NOINLINE operator Result() {
+// this is needed only for MSVC 2015
+DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4800) // 'int': forcing value to bool
+ bool res = static_cast<bool>(lhs);
+DOCTEST_MSVC_SUPPRESS_WARNING_POP
+ if(m_at & assertType::is_false) { //!OCLINT bitwise operator in conditional
+ res = !res;
+ }
+
+ if(!res || getContextOptions()->success) {
+ return { res, (DOCTEST_STRINGIFY(lhs)) };
+ }
+ return { res };
+ }
+
+ /* This is required for user-defined conversions from Expression_lhs to L */
+ operator L() const { return lhs; }
+
+ // clang-format off
+ DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(==, " == ", DOCTEST_CMP_EQ) //!OCLINT bitwise operator in conditional
+ DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(!=, " != ", DOCTEST_CMP_NE) //!OCLINT bitwise operator in conditional
+ DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>, " > ", DOCTEST_CMP_GT) //!OCLINT bitwise operator in conditional
+ DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<, " < ", DOCTEST_CMP_LT) //!OCLINT bitwise operator in conditional
+ DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(>=, " >= ", DOCTEST_CMP_GE) //!OCLINT bitwise operator in conditional
+ DOCTEST_DO_BINARY_EXPRESSION_COMPARISON(<=, " <= ", DOCTEST_CMP_LE) //!OCLINT bitwise operator in conditional
+ // clang-format on
+
+ // forbidding some expressions based on this table: https://en.cppreference.com/w/cpp/language/operator_precedence
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &&)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ||)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, =)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, +=)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, -=)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, *=)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, /=)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, %=)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<=)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>=)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, &=)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, ^=)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, |=)
+ // these 2 are unfortunate because they should be allowed - they have higher precedence over the comparisons, but the
+ // ExpressionDecomposer class uses the left shift operator to capture the left operand of the binary expression...
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, <<)
+ DOCTEST_FORBIT_EXPRESSION(Expression_lhs, >>)
+ };
+
+#ifndef DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION
+
+ DOCTEST_CLANG_SUPPRESS_WARNING_POP
+ DOCTEST_MSVC_SUPPRESS_WARNING_POP
+ DOCTEST_GCC_SUPPRESS_WARNING_POP
+
+#endif // DOCTEST_CONFIG_NO_COMPARISON_WARNING_SUPPRESSION
+
+#if DOCTEST_CLANG && DOCTEST_CLANG < DOCTEST_COMPILER(3, 6, 0)
+DOCTEST_CLANG_SUPPRESS_WARNING_POP
+#endif
+
+ struct DOCTEST_INTERFACE ExpressionDecomposer
+ {
+ assertType::Enum m_at;
+
+ ExpressionDecomposer(assertType::Enum at);
+
+ // The right operator for capturing expressions is "<=" instead of "<<" (based on the operator precedence table)
+ // but then there will be warnings from GCC about "-Wparentheses" and since "_Pragma()" is problematic this will stay for now...
+ // https://github.com/catchorg/Catch2/issues/870
+ // https://github.com/catchorg/Catch2/issues/565
+ template <typename L>
+ Expression_lhs<L> operator<<(L&& operand) {
+ return Expression_lhs<L>(static_cast<L&&>(operand), m_at);
+ }
+
+ template <typename L,typename types::enable_if<!doctest::detail::types::is_rvalue_reference<L>::value,void >::type* = nullptr>
+ Expression_lhs<const L&> operator<<(const L &operand) {
+ return Expression_lhs<const L&>(operand, m_at);
+ }
+ };
+
+ struct DOCTEST_INTERFACE TestSuite
+ {
+ const char* m_test_suite = nullptr;
+ const char* m_description = nullptr;
+ bool m_skip = false;
+ bool m_no_breaks = false;
+ bool m_no_output = false;
+ bool m_may_fail = false;
+ bool m_should_fail = false;
+ int m_expected_failures = 0;
+ double m_timeout = 0;
+
+ TestSuite& operator*(const char* in);
+
+ template <typename T>
+ TestSuite& operator*(const T& in) {
+ in.fill(*this);
+ return *this;
+ }
+ };
+
+ using funcType = void (*)();
+
+ struct DOCTEST_INTERFACE TestCase : public TestCaseData
+ {
+ funcType m_test; // a function pointer to the test case
+
+ String m_type; // for templated test cases - gets appended to the real name
+ int m_template_id; // an ID used to distinguish between the different versions of a templated test case
+ String m_full_name; // contains the name (only for templated test cases!) + the template type
+
+ TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite,
+ const String& type = String(), int template_id = -1);
+
+ TestCase(const TestCase& other);
+ TestCase(TestCase&&) = delete;
+
+ DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function
+ TestCase& operator=(const TestCase& other);
+ DOCTEST_MSVC_SUPPRESS_WARNING_POP
+
+ TestCase& operator=(TestCase&&) = delete;
+
+ TestCase& operator*(const char* in);
+
+ template <typename T>
+ TestCase& operator*(const T& in) {
+ in.fill(*this);
+ return *this;
+ }
+
+ bool operator<(const TestCase& other) const;
+
+ ~TestCase() = default;
+ };
+
+ // forward declarations of functions used by the macros
+ DOCTEST_INTERFACE int regTest(const TestCase& tc);
+ DOCTEST_INTERFACE int setTestSuite(const TestSuite& ts);
+ DOCTEST_INTERFACE bool isDebuggerActive();
+
+ template<typename T>
+ int instantiationHelper(const T&) { return 0; }
+
+ namespace binaryAssertComparison {
+ enum Enum
+ {
+ eq = 0,
+ ne,
+ gt,
+ lt,
+ ge,
+ le
+ };
+ } // namespace binaryAssertComparison
+
+ // clang-format off
+ template <int, class L, class R> struct RelationalComparator { bool operator()(const DOCTEST_REF_WRAP(L), const DOCTEST_REF_WRAP(R) ) const { return false; } };
+
+#define DOCTEST_BINARY_RELATIONAL_OP(n, op) \
+ template <class L, class R> struct RelationalComparator<n, L, R> { bool operator()(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) const { return op(lhs, rhs); } };
+ // clang-format on
+
+ DOCTEST_BINARY_RELATIONAL_OP(0, doctest::detail::eq)
+ DOCTEST_BINARY_RELATIONAL_OP(1, doctest::detail::ne)
+ DOCTEST_BINARY_RELATIONAL_OP(2, doctest::detail::gt)
+ DOCTEST_BINARY_RELATIONAL_OP(3, doctest::detail::lt)
+ DOCTEST_BINARY_RELATIONAL_OP(4, doctest::detail::ge)
+ DOCTEST_BINARY_RELATIONAL_OP(5, doctest::detail::le)
+
+ struct DOCTEST_INTERFACE ResultBuilder : public AssertData
+ {
+ ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
+ const char* exception_type = "", const String& exception_string = "");
+
+ ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
+ const char* exception_type, const Contains& exception_string);
+
+ void setResult(const Result& res);
+
+ template <int comparison, typename L, typename R>
+ DOCTEST_NOINLINE bool binary_assert(const DOCTEST_REF_WRAP(L) lhs,
+ const DOCTEST_REF_WRAP(R) rhs) {
+ m_failed = !RelationalComparator<comparison, L, R>()(lhs, rhs);
+ if (m_failed || getContextOptions()->success) {
+ m_decomp = stringifyBinaryExpr(lhs, ", ", rhs);
+ }
+ return !m_failed;
+ }
+
+ template <typename L>
+ DOCTEST_NOINLINE bool unary_assert(const DOCTEST_REF_WRAP(L) val) {
+ m_failed = !val;
+
+ if (m_at & assertType::is_false) { //!OCLINT bitwise operator in conditional
+ m_failed = !m_failed;
+ }
+
+ if (m_failed || getContextOptions()->success) {
+ m_decomp = (DOCTEST_STRINGIFY(val));
+ }
+
+ return !m_failed;
+ }
+
+ void translateException();
+
+ bool log();
+ void react() const;
+ };
+
+ namespace assertAction {
+ enum Enum
+ {
+ nothing = 0,
+ dbgbreak = 1,
+ shouldthrow = 2
+ };
+ } // namespace assertAction
+
+ DOCTEST_INTERFACE void failed_out_of_a_testing_context(const AssertData& ad);
+
+ DOCTEST_INTERFACE bool decomp_assert(assertType::Enum at, const char* file, int line,
+ const char* expr, const Result& result);
+
+#define DOCTEST_ASSERT_OUT_OF_TESTS(decomp) \
+ do { \
+ if(!is_running_in_test) { \
+ if(failed) { \
+ ResultBuilder rb(at, file, line, expr); \
+ rb.m_failed = failed; \
+ rb.m_decomp = decomp; \
+ failed_out_of_a_testing_context(rb); \
+ if(isDebuggerActive() && !getContextOptions()->no_breaks) \
+ DOCTEST_BREAK_INTO_DEBUGGER(); \
+ if(checkIfShouldThrow(at)) \
+ throwException(); \
+ } \
+ return !failed; \
+ } \
+ } while(false)
+
+#define DOCTEST_ASSERT_IN_TESTS(decomp) \
+ ResultBuilder rb(at, file, line, expr); \
+ rb.m_failed = failed; \
+ if(rb.m_failed || getContextOptions()->success) \
+ rb.m_decomp = decomp; \
+ if(rb.log()) \
+ DOCTEST_BREAK_INTO_DEBUGGER(); \
+ if(rb.m_failed && checkIfShouldThrow(at)) \
+ throwException()
+
+ template <int comparison, typename L, typename R>
+ DOCTEST_NOINLINE bool binary_assert(assertType::Enum at, const char* file, int line,
+ const char* expr, const DOCTEST_REF_WRAP(L) lhs,
+ const DOCTEST_REF_WRAP(R) rhs) {
+ bool failed = !RelationalComparator<comparison, L, R>()(lhs, rhs);
+
+ // ###################################################################################
+ // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT
+ // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED
+ // ###################################################################################
+ DOCTEST_ASSERT_OUT_OF_TESTS(stringifyBinaryExpr(lhs, ", ", rhs));
+ DOCTEST_ASSERT_IN_TESTS(stringifyBinaryExpr(lhs, ", ", rhs));
+ return !failed;
+ }
+
+ template <typename L>
+ DOCTEST_NOINLINE bool unary_assert(assertType::Enum at, const char* file, int line,
+ const char* expr, const DOCTEST_REF_WRAP(L) val) {
+ bool failed = !val;
+
+ if(at & assertType::is_false) //!OCLINT bitwise operator in conditional
+ failed = !failed;
+
+ // ###################################################################################
+ // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT
+ // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED
+ // ###################################################################################
+ DOCTEST_ASSERT_OUT_OF_TESTS((DOCTEST_STRINGIFY(val)));
+ DOCTEST_ASSERT_IN_TESTS((DOCTEST_STRINGIFY(val)));
+ return !failed;
+ }
+
+ struct DOCTEST_INTERFACE IExceptionTranslator
+ {
+ DOCTEST_DECLARE_INTERFACE(IExceptionTranslator)
+ virtual bool translate(String&) const = 0;
+ };
+
+ template <typename T>
+ class ExceptionTranslator : public IExceptionTranslator //!OCLINT destructor of virtual class
+ {
+ public:
+ explicit ExceptionTranslator(String (*translateFunction)(T))
+ : m_translateFunction(translateFunction) {}
+
+ bool translate(String& res) const override {
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+ try {
+ throw; // lgtm [cpp/rethrow-no-exception]
+ // cppcheck-suppress catchExceptionByValue
+ } catch(const T& ex) {
+ res = m_translateFunction(ex); //!OCLINT parameter reassignment
+ return true;
+ } catch(...) {} //!OCLINT - empty catch statement
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+ static_cast<void>(res); // to silence -Wunused-parameter
+ return false;
+ }
+
+ private:
+ String (*m_translateFunction)(T);
+ };
+
+ DOCTEST_INTERFACE void registerExceptionTranslatorImpl(const IExceptionTranslator* et);
+
+ // ContextScope base class used to allow implementing methods of ContextScope
+ // that don't depend on the template parameter in doctest.cpp.
+ struct DOCTEST_INTERFACE ContextScopeBase : public IContextScope {
+ ContextScopeBase(const ContextScopeBase&) = delete;
+
+ ContextScopeBase& operator=(const ContextScopeBase&) = delete;
+ ContextScopeBase& operator=(ContextScopeBase&&) = delete;
+
+ ~ContextScopeBase() override = default;
+
+ protected:
+ ContextScopeBase();
+ ContextScopeBase(ContextScopeBase&& other) noexcept;
+
+ void destroy();
+ bool need_to_destroy{true};
+ };
+
+ template <typename L> class ContextScope : public ContextScopeBase
+ {
+ L lambda_;
+
+ public:
+ explicit ContextScope(const L &lambda) : lambda_(lambda) {}
+ explicit ContextScope(L&& lambda) : lambda_(static_cast<L&&>(lambda)) { }
+
+ ContextScope(const ContextScope&) = delete;
+ ContextScope(ContextScope&&) noexcept = default;
+
+ ContextScope& operator=(const ContextScope&) = delete;
+ ContextScope& operator=(ContextScope&&) = delete;
+
+ void stringify(std::ostream* s) const override { lambda_(s); }
+
+ ~ContextScope() override {
+ if (need_to_destroy) {
+ destroy();
+ }
+ }
+ };
+
+ struct DOCTEST_INTERFACE MessageBuilder : public MessageData
+ {
+ std::ostream* m_stream;
+ bool logged = false;
+
+ MessageBuilder(const char* file, int line, assertType::Enum severity);
+
+ MessageBuilder(const MessageBuilder&) = delete;
+ MessageBuilder(MessageBuilder&&) = delete;
+
+ MessageBuilder& operator=(const MessageBuilder&) = delete;
+ MessageBuilder& operator=(MessageBuilder&&) = delete;
+
+ ~MessageBuilder();
+
+ // the preferred way of chaining parameters for stringification
+DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4866)
+ template <typename T>
+ MessageBuilder& operator,(const T& in) {
+ *m_stream << (DOCTEST_STRINGIFY(in));
+ return *this;
+ }
+DOCTEST_MSVC_SUPPRESS_WARNING_POP
+
+ // kept here just for backwards-compatibility - the comma operator should be preferred now
+ template <typename T>
+ MessageBuilder& operator<<(const T& in) { return this->operator,(in); }
+
+ // the `,` operator has the lowest operator precedence - if `<<` is used by the user then
+ // the `,` operator will be called last which is not what we want and thus the `*` operator
+ // is used first (has higher operator precedence compared to `<<`) so that we guarantee that
+ // an operator of the MessageBuilder class is called first before the rest of the parameters
+ template <typename T>
+ MessageBuilder& operator*(const T& in) { return this->operator,(in); }
+
+ bool log();
+ void react();
+ };
+
+ template <typename L>
+ ContextScope<L> MakeContextScope(const L &lambda) {
+ return ContextScope<L>(lambda);
+ }
+} // namespace detail
+
+#define DOCTEST_DEFINE_DECORATOR(name, type, def) \
+ struct name \
+ { \
+ type data; \
+ name(type in = def) \
+ : data(in) {} \
+ void fill(detail::TestCase& state) const { state.DOCTEST_CAT(m_, name) = data; } \
+ void fill(detail::TestSuite& state) const { state.DOCTEST_CAT(m_, name) = data; } \
+ }
+
+DOCTEST_DEFINE_DECORATOR(test_suite, const char*, "");
+DOCTEST_DEFINE_DECORATOR(description, const char*, "");
+DOCTEST_DEFINE_DECORATOR(skip, bool, true);
+DOCTEST_DEFINE_DECORATOR(no_breaks, bool, true);
+DOCTEST_DEFINE_DECORATOR(no_output, bool, true);
+DOCTEST_DEFINE_DECORATOR(timeout, double, 0);
+DOCTEST_DEFINE_DECORATOR(may_fail, bool, true);
+DOCTEST_DEFINE_DECORATOR(should_fail, bool, true);
+DOCTEST_DEFINE_DECORATOR(expected_failures, int, 0);
+
+template <typename T>
+int registerExceptionTranslator(String (*translateFunction)(T)) {
+ DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors")
+ static detail::ExceptionTranslator<T> exceptionTranslator(translateFunction);
+ DOCTEST_CLANG_SUPPRESS_WARNING_POP
+ detail::registerExceptionTranslatorImpl(&exceptionTranslator);
+ return 0;
+}
+
+} // namespace doctest
+
+// in a separate namespace outside of doctest because the DOCTEST_TEST_SUITE macro
+// introduces an anonymous namespace in which getCurrentTestSuite gets overridden
+namespace doctest_detail_test_suite_ns {
+DOCTEST_INTERFACE doctest::detail::TestSuite& getCurrentTestSuite();
+} // namespace doctest_detail_test_suite_ns
+
+namespace doctest {
+#else // DOCTEST_CONFIG_DISABLE
+template <typename T>
+int registerExceptionTranslator(String (*)(T)) {
+ return 0;
+}
+#endif // DOCTEST_CONFIG_DISABLE
+
+namespace detail {
+ using assert_handler = void (*)(const AssertData&);
+ struct ContextState;
+} // namespace detail
+
+class DOCTEST_INTERFACE Context
+{
+ detail::ContextState* p;
+
+ void parseArgs(int argc, const char* const* argv, bool withDefaults = false);
+
+public:
+ explicit Context(int argc = 0, const char* const* argv = nullptr);
+
+ Context(const Context&) = delete;
+ Context(Context&&) = delete;
+
+ Context& operator=(const Context&) = delete;
+ Context& operator=(Context&&) = delete;
+
+ ~Context(); // NOLINT(performance-trivially-destructible)
+
+ void applyCommandLine(int argc, const char* const* argv);
+
+ void addFilter(const char* filter, const char* value);
+ void clearFilters();
+ void setOption(const char* option, bool value);
+ void setOption(const char* option, int value);
+ void setOption(const char* option, const char* value);
+
+ bool shouldExit();
+
+ void setAsDefaultForAssertsOutOfTestCases();
+
+ void setAssertHandler(detail::assert_handler ah);
+
+ void setCout(std::ostream* out);
+
+ int run();
+};
+
+namespace TestCaseFailureReason {
+ enum Enum
+ {
+ None = 0,
+ AssertFailure = 1, // an assertion has failed in the test case
+ Exception = 2, // test case threw an exception
+ Crash = 4, // a crash...
+ TooManyFailedAsserts = 8, // the abort-after option
+ Timeout = 16, // see the timeout decorator
+ ShouldHaveFailedButDidnt = 32, // see the should_fail decorator
+ ShouldHaveFailedAndDid = 64, // see the should_fail decorator
+ DidntFailExactlyNumTimes = 128, // see the expected_failures decorator
+ FailedExactlyNumTimes = 256, // see the expected_failures decorator
+ CouldHaveFailedAndDid = 512 // see the may_fail decorator
+ };
+} // namespace TestCaseFailureReason
+
+struct DOCTEST_INTERFACE CurrentTestCaseStats
+{
+ int numAssertsCurrentTest;
+ int numAssertsFailedCurrentTest;
+ double seconds;
+ int failure_flags; // use TestCaseFailureReason::Enum
+ bool testCaseSuccess;
+};
+
+struct DOCTEST_INTERFACE TestCaseException
+{
+ String error_string;
+ bool is_crash;
+};
+
+struct DOCTEST_INTERFACE TestRunStats
+{
+ unsigned numTestCases;
+ unsigned numTestCasesPassingFilters;
+ unsigned numTestSuitesPassingFilters;
+ unsigned numTestCasesFailed;
+ int numAsserts;
+ int numAssertsFailed;
+};
+
+struct QueryData
+{
+ const TestRunStats* run_stats = nullptr;
+ const TestCaseData** data = nullptr;
+ unsigned num_data = 0;
+};
+
+struct DOCTEST_INTERFACE IReporter
+{
+ // The constructor has to accept "const ContextOptions&" as a single argument
+ // which has most of the options for the run + a pointer to the stdout stream
+ // Reporter(const ContextOptions& in)
+
+ // called when a query should be reported (listing test cases, printing the version, etc.)
+ virtual void report_query(const QueryData&) = 0;
+
+ // called when the whole test run starts
+ virtual void test_run_start() = 0;
+ // called when the whole test run ends (caching a pointer to the input doesn't make sense here)
+ virtual void test_run_end(const TestRunStats&) = 0;
+
+ // called when a test case is started (safe to cache a pointer to the input)
+ virtual void test_case_start(const TestCaseData&) = 0;
+ // called when a test case is reentered because of unfinished subcases (safe to cache a pointer to the input)
+ virtual void test_case_reenter(const TestCaseData&) = 0;
+ // called when a test case has ended
+ virtual void test_case_end(const CurrentTestCaseStats&) = 0;
+
+ // called when an exception is thrown from the test case (or it crashes)
+ virtual void test_case_exception(const TestCaseException&) = 0;
+
+ // called whenever a subcase is entered (don't cache pointers to the input)
+ virtual void subcase_start(const SubcaseSignature&) = 0;
+ // called whenever a subcase is exited (don't cache pointers to the input)
+ virtual void subcase_end() = 0;
+
+ // called for each assert (don't cache pointers to the input)
+ virtual void log_assert(const AssertData&) = 0;
+ // called for each message (don't cache pointers to the input)
+ virtual void log_message(const MessageData&) = 0;
+
+ // called when a test case is skipped either because it doesn't pass the filters, has a skip decorator
+ // or isn't in the execution range (between first and last) (safe to cache a pointer to the input)
+ virtual void test_case_skipped(const TestCaseData&) = 0;
+
+ DOCTEST_DECLARE_INTERFACE(IReporter)
+
+ // can obtain all currently active contexts and stringify them if one wishes to do so
+ static int get_num_active_contexts();
+ static const IContextScope* const* get_active_contexts();
+
+ // can iterate through contexts which have been stringified automatically in their destructors when an exception has been thrown
+ static int get_num_stringified_contexts();
+ static const String* get_stringified_contexts();
+};
+
+namespace detail {
+ using reporterCreatorFunc = IReporter* (*)(const ContextOptions&);
+
+ DOCTEST_INTERFACE void registerReporterImpl(const char* name, int prio, reporterCreatorFunc c, bool isReporter);
+
+ template <typename Reporter>
+ IReporter* reporterCreator(const ContextOptions& o) {
+ return new Reporter(o);
+ }
+} // namespace detail
+
+template <typename Reporter>
+int registerReporter(const char* name, int priority, bool isReporter) {
+ detail::registerReporterImpl(name, priority, detail::reporterCreator<Reporter>, isReporter);
+ return 0;
+}
+} // namespace doctest
+
+#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES
+#define DOCTEST_FUNC_EMPTY [] { return false; }()
+#else
+#define DOCTEST_FUNC_EMPTY (void)0
+#endif
+
+// if registering is not disabled
+#ifndef DOCTEST_CONFIG_DISABLE
+
+#ifdef DOCTEST_CONFIG_ASSERTS_RETURN_VALUES
+#define DOCTEST_FUNC_SCOPE_BEGIN [&]
+#define DOCTEST_FUNC_SCOPE_END ()
+#define DOCTEST_FUNC_SCOPE_RET(v) return v
+#else
+#define DOCTEST_FUNC_SCOPE_BEGIN do
+#define DOCTEST_FUNC_SCOPE_END while(false)
+#define DOCTEST_FUNC_SCOPE_RET(v) (void)0
+#endif
+
+// common code in asserts - for convenience
+#define DOCTEST_ASSERT_LOG_REACT_RETURN(b) \
+ if(b.log()) DOCTEST_BREAK_INTO_DEBUGGER(); \
+ b.react(); \
+ DOCTEST_FUNC_SCOPE_RET(!b.m_failed)
+
+#ifdef DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS
+#define DOCTEST_WRAP_IN_TRY(x) x;
+#else // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS
+#define DOCTEST_WRAP_IN_TRY(x) \
+ try { \
+ x; \
+ } catch(...) { DOCTEST_RB.translateException(); }
+#endif // DOCTEST_CONFIG_NO_TRY_CATCH_IN_ASSERTS
+
+#ifdef DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS
+#define DOCTEST_CAST_TO_VOID(...) \
+ DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wuseless-cast") \
+ static_cast<void>(__VA_ARGS__); \
+ DOCTEST_GCC_SUPPRESS_WARNING_POP
+#else // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS
+#define DOCTEST_CAST_TO_VOID(...) __VA_ARGS__;
+#endif // DOCTEST_CONFIG_VOID_CAST_EXPRESSIONS
+
+// registers the test by initializing a dummy var with a function
+#define DOCTEST_REGISTER_FUNCTION(global_prefix, f, decorators) \
+ global_prefix DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT */ \
+ doctest::detail::regTest( \
+ doctest::detail::TestCase( \
+ f, __FILE__, __LINE__, \
+ doctest_detail_test_suite_ns::getCurrentTestSuite()) * \
+ decorators))
+
+#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, decorators) \
+ namespace { /* NOLINT */ \
+ struct der : public base \
+ { \
+ void f(); \
+ }; \
+ static inline DOCTEST_NOINLINE void func() { \
+ der v; \
+ v.f(); \
+ } \
+ DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, func, decorators) \
+ } \
+ inline DOCTEST_NOINLINE void der::f() // NOLINT(misc-definitions-in-headers)
+
+#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, decorators) \
+ static void f(); \
+ DOCTEST_REGISTER_FUNCTION(DOCTEST_EMPTY, f, decorators) \
+ static void f()
+
+#define DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(f, proxy, decorators) \
+ static doctest::detail::funcType proxy() { return f; } \
+ DOCTEST_REGISTER_FUNCTION(inline, proxy(), decorators) \
+ static void f()
+
+// for registering tests
+#define DOCTEST_TEST_CASE(decorators) \
+ DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators)
+
+// for registering tests in classes - requires C++17 for inline variables!
+#if DOCTEST_CPLUSPLUS >= 201703L
+#define DOCTEST_TEST_CASE_CLASS(decorators) \
+ DOCTEST_CREATE_AND_REGISTER_FUNCTION_IN_CLASS(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), \
+ DOCTEST_ANONYMOUS(DOCTEST_ANON_PROXY_), \
+ decorators)
+#else // DOCTEST_TEST_CASE_CLASS
+#define DOCTEST_TEST_CASE_CLASS(...) \
+ TEST_CASES_CAN_BE_REGISTERED_IN_CLASSES_ONLY_IN_CPP17_MODE_OR_WITH_VS_2017_OR_NEWER
+#endif // DOCTEST_TEST_CASE_CLASS
+
+// for registering tests with a fixture
+#define DOCTEST_TEST_CASE_FIXTURE(c, decorators) \
+ DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), c, \
+ DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), decorators)
+
+// for converting types to strings without the <typeinfo> header and demangling
+#define DOCTEST_TYPE_TO_STRING_AS(str, ...) \
+ namespace doctest { \
+ template <> \
+ inline String toString<__VA_ARGS__>() { \
+ return str; \
+ } \
+ } \
+ static_assert(true, "")
+
+#define DOCTEST_TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING_AS(#__VA_ARGS__, __VA_ARGS__)
+
+#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, iter, func) \
+ template <typename T> \
+ static void func(); \
+ namespace { /* NOLINT */ \
+ template <typename Tuple> \
+ struct iter; \
+ template <typename Type, typename... Rest> \
+ struct iter<std::tuple<Type, Rest...>> \
+ { \
+ iter(const char* file, unsigned line, int index) { \
+ doctest::detail::regTest(doctest::detail::TestCase(func<Type>, file, line, \
+ doctest_detail_test_suite_ns::getCurrentTestSuite(), \
+ doctest::toString<Type>(), \
+ int(line) * 1000 + index) \
+ * dec); \
+ iter<std::tuple<Rest...>>(file, line, index + 1); \
+ } \
+ }; \
+ template <> \
+ struct iter<std::tuple<>> \
+ { \
+ iter(const char*, unsigned, int) {} \
+ }; \
+ } \
+ template <typename T> \
+ static void func()
+
+#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(dec, T, id) \
+ DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(id, ITERATOR), \
+ DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_))
+
+#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, anon, ...) \
+ DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_CAT(anon, DUMMY), /* NOLINT(cert-err58-cpp, fuchsia-statically-constructed-objects) */ \
+ doctest::detail::instantiationHelper( \
+ DOCTEST_CAT(id, ITERATOR)<__VA_ARGS__>(__FILE__, __LINE__, 0)))
+
+#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) \
+ DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), std::tuple<__VA_ARGS__>) \
+ static_assert(true, "")
+
+#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) \
+ DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(id, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__) \
+ static_assert(true, "")
+
+#define DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, anon, ...) \
+ DOCTEST_TEST_CASE_TEMPLATE_DEFINE_IMPL(dec, T, DOCTEST_CAT(anon, ITERATOR), anon); \
+ DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE_IMPL(anon, anon, std::tuple<__VA_ARGS__>) \
+ template <typename T> \
+ static void anon()
+
+#define DOCTEST_TEST_CASE_TEMPLATE(dec, T, ...) \
+ DOCTEST_TEST_CASE_TEMPLATE_IMPL(dec, T, DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_), __VA_ARGS__)
+
+// for subcases
+#define DOCTEST_SUBCASE(name) \
+ if(const doctest::detail::Subcase & DOCTEST_ANONYMOUS(DOCTEST_ANON_SUBCASE_) DOCTEST_UNUSED = \
+ doctest::detail::Subcase(name, __FILE__, __LINE__))
+
+// for grouping tests in test suites by using code blocks
+#define DOCTEST_TEST_SUITE_IMPL(decorators, ns_name) \
+ namespace ns_name { namespace doctest_detail_test_suite_ns { \
+ static DOCTEST_NOINLINE doctest::detail::TestSuite& getCurrentTestSuite() noexcept { \
+ DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4640) \
+ DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wexit-time-destructors") \
+ DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmissing-field-initializers") \
+ static doctest::detail::TestSuite data{}; \
+ static bool inited = false; \
+ DOCTEST_MSVC_SUPPRESS_WARNING_POP \
+ DOCTEST_CLANG_SUPPRESS_WARNING_POP \
+ DOCTEST_GCC_SUPPRESS_WARNING_POP \
+ if(!inited) { \
+ data* decorators; \
+ inited = true; \
+ } \
+ return data; \
+ } \
+ } \
+ } \
+ namespace ns_name
+
+#define DOCTEST_TEST_SUITE(decorators) \
+ DOCTEST_TEST_SUITE_IMPL(decorators, DOCTEST_ANONYMOUS(DOCTEST_ANON_SUITE_))
+
+// for starting a testsuite block
+#define DOCTEST_TEST_SUITE_BEGIN(decorators) \
+ DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \
+ doctest::detail::setTestSuite(doctest::detail::TestSuite() * decorators)) \
+ static_assert(true, "")
+
+// for ending a testsuite block
+#define DOCTEST_TEST_SUITE_END \
+ DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_VAR_), /* NOLINT(cert-err58-cpp) */ \
+ doctest::detail::setTestSuite(doctest::detail::TestSuite() * "")) \
+ using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int
+
+// for registering exception translators
+#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(translatorName, signature) \
+ inline doctest::String translatorName(signature); \
+ DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), /* NOLINT(cert-err58-cpp) */ \
+ doctest::registerExceptionTranslator(translatorName)) \
+ doctest::String translatorName(signature)
+
+#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \
+ DOCTEST_REGISTER_EXCEPTION_TRANSLATOR_IMPL(DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_), \
+ signature)
+
+// for registering reporters
+#define DOCTEST_REGISTER_REPORTER(name, priority, reporter) \
+ DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), /* NOLINT(cert-err58-cpp) */ \
+ doctest::registerReporter<reporter>(name, priority, true)) \
+ static_assert(true, "")
+
+// for registering listeners
+#define DOCTEST_REGISTER_LISTENER(name, priority, reporter) \
+ DOCTEST_GLOBAL_NO_WARNINGS(DOCTEST_ANONYMOUS(DOCTEST_ANON_REPORTER_), /* NOLINT(cert-err58-cpp) */ \
+ doctest::registerReporter<reporter>(name, priority, false)) \
+ static_assert(true, "")
+
+// clang-format off
+// for logging - disabling formatting because it's important to have these on 2 separate lines - see PR #557
+#define DOCTEST_INFO(...) \
+ DOCTEST_INFO_IMPL(DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_), \
+ DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_OTHER_), \
+ __VA_ARGS__)
+// clang-format on
+
+#define DOCTEST_INFO_IMPL(mb_name, s_name, ...) \
+ auto DOCTEST_ANONYMOUS(DOCTEST_CAPTURE_) = doctest::detail::MakeContextScope( \
+ [&](std::ostream* s_name) { \
+ doctest::detail::MessageBuilder mb_name(__FILE__, __LINE__, doctest::assertType::is_warn); \
+ mb_name.m_stream = s_name; \
+ mb_name * __VA_ARGS__; \
+ })
+
+#define DOCTEST_CAPTURE(x) DOCTEST_INFO(#x " := ", x)
+
+#define DOCTEST_ADD_AT_IMPL(type, file, line, mb, ...) \
+ DOCTEST_FUNC_SCOPE_BEGIN { \
+ doctest::detail::MessageBuilder mb(file, line, doctest::assertType::type); \
+ mb * __VA_ARGS__; \
+ if(mb.log()) \
+ DOCTEST_BREAK_INTO_DEBUGGER(); \
+ mb.react(); \
+ } DOCTEST_FUNC_SCOPE_END
+
+// clang-format off
+#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_warn, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__)
+#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_check, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__)
+#define DOCTEST_ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_AT_IMPL(is_require, file, line, DOCTEST_ANONYMOUS(DOCTEST_MESSAGE_), __VA_ARGS__)
+// clang-format on
+
+#define DOCTEST_MESSAGE(...) DOCTEST_ADD_MESSAGE_AT(__FILE__, __LINE__, __VA_ARGS__)
+#define DOCTEST_FAIL_CHECK(...) DOCTEST_ADD_FAIL_CHECK_AT(__FILE__, __LINE__, __VA_ARGS__)
+#define DOCTEST_FAIL(...) DOCTEST_ADD_FAIL_AT(__FILE__, __LINE__, __VA_ARGS__)
+
+#define DOCTEST_TO_LVALUE(...) __VA_ARGS__ // Not removed to keep backwards compatibility.
+
+#ifndef DOCTEST_CONFIG_SUPER_FAST_ASSERTS
+
+#define DOCTEST_ASSERT_IMPLEMENT_2(assert_type, ...) \
+ DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \
+ /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \
+ doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \
+ __LINE__, #__VA_ARGS__); \
+ DOCTEST_WRAP_IN_TRY(DOCTEST_RB.setResult( \
+ doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \
+ << __VA_ARGS__)) /* NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) */ \
+ DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB) \
+ DOCTEST_CLANG_SUPPRESS_WARNING_POP
+
+#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \
+ DOCTEST_FUNC_SCOPE_BEGIN { \
+ DOCTEST_ASSERT_IMPLEMENT_2(assert_type, __VA_ARGS__); \
+ } DOCTEST_FUNC_SCOPE_END // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
+
+#define DOCTEST_BINARY_ASSERT(assert_type, comp, ...) \
+ DOCTEST_FUNC_SCOPE_BEGIN { \
+ doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \
+ __LINE__, #__VA_ARGS__); \
+ DOCTEST_WRAP_IN_TRY( \
+ DOCTEST_RB.binary_assert<doctest::detail::binaryAssertComparison::comp>( \
+ __VA_ARGS__)) \
+ DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \
+ } DOCTEST_FUNC_SCOPE_END
+
+#define DOCTEST_UNARY_ASSERT(assert_type, ...) \
+ DOCTEST_FUNC_SCOPE_BEGIN { \
+ doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \
+ __LINE__, #__VA_ARGS__); \
+ DOCTEST_WRAP_IN_TRY(DOCTEST_RB.unary_assert(__VA_ARGS__)) \
+ DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \
+ } DOCTEST_FUNC_SCOPE_END
+
+#else // DOCTEST_CONFIG_SUPER_FAST_ASSERTS
+
+// necessary for <ASSERT>_MESSAGE
+#define DOCTEST_ASSERT_IMPLEMENT_2 DOCTEST_ASSERT_IMPLEMENT_1
+
+#define DOCTEST_ASSERT_IMPLEMENT_1(assert_type, ...) \
+ DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Woverloaded-shift-op-parentheses") \
+ doctest::detail::decomp_assert( \
+ doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, \
+ doctest::detail::ExpressionDecomposer(doctest::assertType::assert_type) \
+ << __VA_ARGS__) DOCTEST_CLANG_SUPPRESS_WARNING_POP
+
+#define DOCTEST_BINARY_ASSERT(assert_type, comparison, ...) \
+ doctest::detail::binary_assert<doctest::detail::binaryAssertComparison::comparison>( \
+ doctest::assertType::assert_type, __FILE__, __LINE__, #__VA_ARGS__, __VA_ARGS__)
+
+#define DOCTEST_UNARY_ASSERT(assert_type, ...) \
+ doctest::detail::unary_assert(doctest::assertType::assert_type, __FILE__, __LINE__, \
+ #__VA_ARGS__, __VA_ARGS__)
+
+#endif // DOCTEST_CONFIG_SUPER_FAST_ASSERTS
+
+#define DOCTEST_WARN(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN, __VA_ARGS__)
+#define DOCTEST_CHECK(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK, __VA_ARGS__)
+#define DOCTEST_REQUIRE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE, __VA_ARGS__)
+#define DOCTEST_WARN_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_WARN_FALSE, __VA_ARGS__)
+#define DOCTEST_CHECK_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_CHECK_FALSE, __VA_ARGS__)
+#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_ASSERT_IMPLEMENT_1(DT_REQUIRE_FALSE, __VA_ARGS__)
+
+// clang-format off
+#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN, cond); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK, cond); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE, cond); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_WARN_FALSE, cond); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_CHECK_FALSE, cond); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_ASSERT_IMPLEMENT_2(DT_REQUIRE_FALSE, cond); } DOCTEST_FUNC_SCOPE_END
+// clang-format on
+
+#define DOCTEST_WARN_EQ(...) DOCTEST_BINARY_ASSERT(DT_WARN_EQ, eq, __VA_ARGS__)
+#define DOCTEST_CHECK_EQ(...) DOCTEST_BINARY_ASSERT(DT_CHECK_EQ, eq, __VA_ARGS__)
+#define DOCTEST_REQUIRE_EQ(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_EQ, eq, __VA_ARGS__)
+#define DOCTEST_WARN_NE(...) DOCTEST_BINARY_ASSERT(DT_WARN_NE, ne, __VA_ARGS__)
+#define DOCTEST_CHECK_NE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_NE, ne, __VA_ARGS__)
+#define DOCTEST_REQUIRE_NE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_NE, ne, __VA_ARGS__)
+#define DOCTEST_WARN_GT(...) DOCTEST_BINARY_ASSERT(DT_WARN_GT, gt, __VA_ARGS__)
+#define DOCTEST_CHECK_GT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GT, gt, __VA_ARGS__)
+#define DOCTEST_REQUIRE_GT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GT, gt, __VA_ARGS__)
+#define DOCTEST_WARN_LT(...) DOCTEST_BINARY_ASSERT(DT_WARN_LT, lt, __VA_ARGS__)
+#define DOCTEST_CHECK_LT(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LT, lt, __VA_ARGS__)
+#define DOCTEST_REQUIRE_LT(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LT, lt, __VA_ARGS__)
+#define DOCTEST_WARN_GE(...) DOCTEST_BINARY_ASSERT(DT_WARN_GE, ge, __VA_ARGS__)
+#define DOCTEST_CHECK_GE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_GE, ge, __VA_ARGS__)
+#define DOCTEST_REQUIRE_GE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_GE, ge, __VA_ARGS__)
+#define DOCTEST_WARN_LE(...) DOCTEST_BINARY_ASSERT(DT_WARN_LE, le, __VA_ARGS__)
+#define DOCTEST_CHECK_LE(...) DOCTEST_BINARY_ASSERT(DT_CHECK_LE, le, __VA_ARGS__)
+#define DOCTEST_REQUIRE_LE(...) DOCTEST_BINARY_ASSERT(DT_REQUIRE_LE, le, __VA_ARGS__)
+
+#define DOCTEST_WARN_UNARY(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY, __VA_ARGS__)
+#define DOCTEST_CHECK_UNARY(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY, __VA_ARGS__)
+#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY, __VA_ARGS__)
+#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_WARN_UNARY_FALSE, __VA_ARGS__)
+#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_CHECK_UNARY_FALSE, __VA_ARGS__)
+#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_UNARY_ASSERT(DT_REQUIRE_UNARY_FALSE, __VA_ARGS__)
+
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+
+#define DOCTEST_ASSERT_THROWS_AS(expr, assert_type, message, ...) \
+ DOCTEST_FUNC_SCOPE_BEGIN { \
+ if(!doctest::getContextOptions()->no_throw) { \
+ doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \
+ __LINE__, #expr, #__VA_ARGS__, message); \
+ try { \
+ DOCTEST_CAST_TO_VOID(expr) \
+ } catch(const typename doctest::detail::types::remove_const< \
+ typename doctest::detail::types::remove_reference<__VA_ARGS__>::type>::type&) {\
+ DOCTEST_RB.translateException(); \
+ DOCTEST_RB.m_threw_as = true; \
+ } catch(...) { DOCTEST_RB.translateException(); } \
+ DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \
+ } else { /* NOLINT(*-else-after-return) */ \
+ DOCTEST_FUNC_SCOPE_RET(false); \
+ } \
+ } DOCTEST_FUNC_SCOPE_END
+
+#define DOCTEST_ASSERT_THROWS_WITH(expr, expr_str, assert_type, ...) \
+ DOCTEST_FUNC_SCOPE_BEGIN { \
+ if(!doctest::getContextOptions()->no_throw) { \
+ doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \
+ __LINE__, expr_str, "", __VA_ARGS__); \
+ try { \
+ DOCTEST_CAST_TO_VOID(expr) \
+ } catch(...) { DOCTEST_RB.translateException(); } \
+ DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \
+ } else { /* NOLINT(*-else-after-return) */ \
+ DOCTEST_FUNC_SCOPE_RET(false); \
+ } \
+ } DOCTEST_FUNC_SCOPE_END
+
+#define DOCTEST_ASSERT_NOTHROW(assert_type, ...) \
+ DOCTEST_FUNC_SCOPE_BEGIN { \
+ doctest::detail::ResultBuilder DOCTEST_RB(doctest::assertType::assert_type, __FILE__, \
+ __LINE__, #__VA_ARGS__); \
+ try { \
+ DOCTEST_CAST_TO_VOID(__VA_ARGS__) \
+ } catch(...) { DOCTEST_RB.translateException(); } \
+ DOCTEST_ASSERT_LOG_REACT_RETURN(DOCTEST_RB); \
+ } DOCTEST_FUNC_SCOPE_END
+
+// clang-format off
+#define DOCTEST_WARN_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_WARN_THROWS, "")
+#define DOCTEST_CHECK_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_CHECK_THROWS, "")
+#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_ASSERT_THROWS_WITH((__VA_ARGS__), #__VA_ARGS__, DT_REQUIRE_THROWS, "")
+
+#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_AS, "", __VA_ARGS__)
+#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_AS, "", __VA_ARGS__)
+#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_AS, "", __VA_ARGS__)
+
+#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_WARN_THROWS_WITH, __VA_ARGS__)
+#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_CHECK_THROWS_WITH, __VA_ARGS__)
+#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_ASSERT_THROWS_WITH(expr, #expr, DT_REQUIRE_THROWS_WITH, __VA_ARGS__)
+
+#define DOCTEST_WARN_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_WARN_THROWS_WITH_AS, message, __VA_ARGS__)
+#define DOCTEST_CHECK_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_CHECK_THROWS_WITH_AS, message, __VA_ARGS__)
+#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, message, ...) DOCTEST_ASSERT_THROWS_AS(expr, DT_REQUIRE_THROWS_WITH_AS, message, __VA_ARGS__)
+
+#define DOCTEST_WARN_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_WARN_NOTHROW, __VA_ARGS__)
+#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_CHECK_NOTHROW, __VA_ARGS__)
+#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_ASSERT_NOTHROW(DT_REQUIRE_NOTHROW, __VA_ARGS__)
+
+#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS(expr); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS(expr); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS(expr); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_AS(expr, ex); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH(expr, with); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_WARN_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_CHECK_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END
+#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_SCOPE_BEGIN { DOCTEST_INFO(__VA_ARGS__); DOCTEST_REQUIRE_NOTHROW(expr); } DOCTEST_FUNC_SCOPE_END
+// clang-format on
+
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+
+// =================================================================================================
+// == WHAT FOLLOWS IS VERSIONS OF THE MACROS THAT DO NOT DO ANY REGISTERING! ==
+// == THIS CAN BE ENABLED BY DEFINING DOCTEST_CONFIG_DISABLE GLOBALLY! ==
+// =================================================================================================
+#else // DOCTEST_CONFIG_DISABLE
+
+#define DOCTEST_IMPLEMENT_FIXTURE(der, base, func, name) \
+ namespace /* NOLINT */ { \
+ template <typename DOCTEST_UNUSED_TEMPLATE_TYPE> \
+ struct der : public base \
+ { void f(); }; \
+ } \
+ template <typename DOCTEST_UNUSED_TEMPLATE_TYPE> \
+ inline void der<DOCTEST_UNUSED_TEMPLATE_TYPE>::f()
+
+#define DOCTEST_CREATE_AND_REGISTER_FUNCTION(f, name) \
+ template <typename DOCTEST_UNUSED_TEMPLATE_TYPE> \
+ static inline void f()
+
+// for registering tests
+#define DOCTEST_TEST_CASE(name) \
+ DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name)
+
+// for registering tests in classes
+#define DOCTEST_TEST_CASE_CLASS(name) \
+ DOCTEST_CREATE_AND_REGISTER_FUNCTION(DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name)
+
+// for registering tests with a fixture
+#define DOCTEST_TEST_CASE_FIXTURE(x, name) \
+ DOCTEST_IMPLEMENT_FIXTURE(DOCTEST_ANONYMOUS(DOCTEST_ANON_CLASS_), x, \
+ DOCTEST_ANONYMOUS(DOCTEST_ANON_FUNC_), name)
+
+// for converting types to strings without the <typeinfo> header and demangling
+#define DOCTEST_TYPE_TO_STRING_AS(str, ...) static_assert(true, "")
+#define DOCTEST_TYPE_TO_STRING(...) static_assert(true, "")
+
+// for typed tests
+#define DOCTEST_TEST_CASE_TEMPLATE(name, type, ...) \
+ template <typename type> \
+ inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)()
+
+#define DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, type, id) \
+ template <typename type> \
+ inline void DOCTEST_ANONYMOUS(DOCTEST_ANON_TMP_)()
+
+#define DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, ...) static_assert(true, "")
+#define DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, ...) static_assert(true, "")
+
+// for subcases
+#define DOCTEST_SUBCASE(name)
+
+// for a testsuite block
+#define DOCTEST_TEST_SUITE(name) namespace // NOLINT
+
+// for starting a testsuite block
+#define DOCTEST_TEST_SUITE_BEGIN(name) static_assert(true, "")
+
+// for ending a testsuite block
+#define DOCTEST_TEST_SUITE_END using DOCTEST_ANONYMOUS(DOCTEST_ANON_FOR_SEMICOLON_) = int
+
+#define DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature) \
+ template <typename DOCTEST_UNUSED_TEMPLATE_TYPE> \
+ static inline doctest::String DOCTEST_ANONYMOUS(DOCTEST_ANON_TRANSLATOR_)(signature)
+
+#define DOCTEST_REGISTER_REPORTER(name, priority, reporter)
+#define DOCTEST_REGISTER_LISTENER(name, priority, reporter)
+
+#define DOCTEST_INFO(...) (static_cast<void>(0))
+#define DOCTEST_CAPTURE(x) (static_cast<void>(0))
+#define DOCTEST_ADD_MESSAGE_AT(file, line, ...) (static_cast<void>(0))
+#define DOCTEST_ADD_FAIL_CHECK_AT(file, line, ...) (static_cast<void>(0))
+#define DOCTEST_ADD_FAIL_AT(file, line, ...) (static_cast<void>(0))
+#define DOCTEST_MESSAGE(...) (static_cast<void>(0))
+#define DOCTEST_FAIL_CHECK(...) (static_cast<void>(0))
+#define DOCTEST_FAIL(...) (static_cast<void>(0))
+
+#if defined(DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED) \
+ && defined(DOCTEST_CONFIG_ASSERTS_RETURN_VALUES)
+
+#define DOCTEST_WARN(...) [&] { return __VA_ARGS__; }()
+#define DOCTEST_CHECK(...) [&] { return __VA_ARGS__; }()
+#define DOCTEST_REQUIRE(...) [&] { return __VA_ARGS__; }()
+#define DOCTEST_WARN_FALSE(...) [&] { return !(__VA_ARGS__); }()
+#define DOCTEST_CHECK_FALSE(...) [&] { return !(__VA_ARGS__); }()
+#define DOCTEST_REQUIRE_FALSE(...) [&] { return !(__VA_ARGS__); }()
+
+#define DOCTEST_WARN_MESSAGE(cond, ...) [&] { return cond; }()
+#define DOCTEST_CHECK_MESSAGE(cond, ...) [&] { return cond; }()
+#define DOCTEST_REQUIRE_MESSAGE(cond, ...) [&] { return cond; }()
+#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }()
+#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }()
+#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) [&] { return !(cond); }()
+
+namespace doctest {
+namespace detail {
+#define DOCTEST_RELATIONAL_OP(name, op) \
+ template <typename L, typename R> \
+ bool name(const DOCTEST_REF_WRAP(L) lhs, const DOCTEST_REF_WRAP(R) rhs) { return lhs op rhs; }
+
+ DOCTEST_RELATIONAL_OP(eq, ==)
+ DOCTEST_RELATIONAL_OP(ne, !=)
+ DOCTEST_RELATIONAL_OP(lt, <)
+ DOCTEST_RELATIONAL_OP(gt, >)
+ DOCTEST_RELATIONAL_OP(le, <=)
+ DOCTEST_RELATIONAL_OP(ge, >=)
+} // namespace detail
+} // namespace doctest
+
+#define DOCTEST_WARN_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }()
+#define DOCTEST_CHECK_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }()
+#define DOCTEST_REQUIRE_EQ(...) [&] { return doctest::detail::eq(__VA_ARGS__); }()
+#define DOCTEST_WARN_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }()
+#define DOCTEST_CHECK_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }()
+#define DOCTEST_REQUIRE_NE(...) [&] { return doctest::detail::ne(__VA_ARGS__); }()
+#define DOCTEST_WARN_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }()
+#define DOCTEST_CHECK_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }()
+#define DOCTEST_REQUIRE_LT(...) [&] { return doctest::detail::lt(__VA_ARGS__); }()
+#define DOCTEST_WARN_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }()
+#define DOCTEST_CHECK_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }()
+#define DOCTEST_REQUIRE_GT(...) [&] { return doctest::detail::gt(__VA_ARGS__); }()
+#define DOCTEST_WARN_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }()
+#define DOCTEST_CHECK_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }()
+#define DOCTEST_REQUIRE_LE(...) [&] { return doctest::detail::le(__VA_ARGS__); }()
+#define DOCTEST_WARN_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }()
+#define DOCTEST_CHECK_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }()
+#define DOCTEST_REQUIRE_GE(...) [&] { return doctest::detail::ge(__VA_ARGS__); }()
+#define DOCTEST_WARN_UNARY(...) [&] { return __VA_ARGS__; }()
+#define DOCTEST_CHECK_UNARY(...) [&] { return __VA_ARGS__; }()
+#define DOCTEST_REQUIRE_UNARY(...) [&] { return __VA_ARGS__; }()
+#define DOCTEST_WARN_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }()
+#define DOCTEST_CHECK_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }()
+#define DOCTEST_REQUIRE_UNARY_FALSE(...) [&] { return !(__VA_ARGS__); }()
+
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+
+#define DOCTEST_WARN_THROWS_WITH(expr, with, ...) [] { static_assert(false, "Exception translation is not available when doctest is disabled."); return false; }()
+#define DOCTEST_CHECK_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,)
+#define DOCTEST_REQUIRE_THROWS_WITH(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,)
+#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,)
+#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,)
+#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,)
+
+#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,)
+#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,)
+#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH(,,)
+#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,)
+#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,)
+#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH(,,)
+
+#define DOCTEST_WARN_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }()
+#define DOCTEST_CHECK_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }()
+#define DOCTEST_REQUIRE_THROWS(...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }()
+#define DOCTEST_WARN_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }()
+#define DOCTEST_CHECK_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }()
+#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }()
+#define DOCTEST_WARN_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }()
+#define DOCTEST_CHECK_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }()
+#define DOCTEST_REQUIRE_NOTHROW(...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }()
+
+#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }()
+#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }()
+#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return false; } catch (...) { return true; } }()
+#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }()
+#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }()
+#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) [&] { try { expr; } catch (__VA_ARGS__) { return true; } catch (...) { } return false; }()
+#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }()
+#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }()
+#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) [&] { try { __VA_ARGS__; return true; } catch (...) { return false; } }()
+
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+
+#else // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED
+
+#define DOCTEST_WARN(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_FALSE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_FALSE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_FALSE(...) DOCTEST_FUNC_EMPTY
+
+#define DOCTEST_WARN_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_FUNC_EMPTY
+
+#define DOCTEST_WARN_EQ(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_EQ(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_EQ(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_NE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_NE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_NE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_GT(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_GT(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_GT(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_LT(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_LT(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_LT(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_GE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_GE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_GE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_LE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_LE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_LE(...) DOCTEST_FUNC_EMPTY
+
+#define DOCTEST_WARN_UNARY(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_UNARY(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_UNARY(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_UNARY_FALSE(...) DOCTEST_FUNC_EMPTY
+
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+
+#define DOCTEST_WARN_THROWS(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_THROWS(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_NOTHROW(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_FUNC_EMPTY
+
+#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY
+#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_FUNC_EMPTY
+
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+
+#endif // DOCTEST_CONFIG_EVALUATE_ASSERTS_EVEN_WHEN_DISABLED
+
+#endif // DOCTEST_CONFIG_DISABLE
+
+#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS
+
+#ifdef DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
+#define DOCTEST_EXCEPTION_EMPTY_FUNC DOCTEST_FUNC_EMPTY
+#else // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
+#define DOCTEST_EXCEPTION_EMPTY_FUNC [] { static_assert(false, "Exceptions are disabled! " \
+ "Use DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS if you want to compile with exceptions disabled."); return false; }()
+
+#undef DOCTEST_REQUIRE
+#undef DOCTEST_REQUIRE_FALSE
+#undef DOCTEST_REQUIRE_MESSAGE
+#undef DOCTEST_REQUIRE_FALSE_MESSAGE
+#undef DOCTEST_REQUIRE_EQ
+#undef DOCTEST_REQUIRE_NE
+#undef DOCTEST_REQUIRE_GT
+#undef DOCTEST_REQUIRE_LT
+#undef DOCTEST_REQUIRE_GE
+#undef DOCTEST_REQUIRE_LE
+#undef DOCTEST_REQUIRE_UNARY
+#undef DOCTEST_REQUIRE_UNARY_FALSE
+
+#define DOCTEST_REQUIRE DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_FALSE_MESSAGE DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_EQ DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_NE DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_GT DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_LT DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_GE DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_LE DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_UNARY DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_UNARY_FALSE DOCTEST_EXCEPTION_EMPTY_FUNC
+
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS_BUT_WITH_ALL_ASSERTS
+
+#define DOCTEST_WARN_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_THROWS(...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_WARN_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_THROWS_AS(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_WARN_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_THROWS_WITH(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_WARN_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_NOTHROW(...) DOCTEST_EXCEPTION_EMPTY_FUNC
+
+#define DOCTEST_WARN_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+#define DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_EXCEPTION_EMPTY_FUNC
+
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+
+// clang-format off
+// KEPT FOR BACKWARDS COMPATIBILITY - FORWARDING TO THE RIGHT MACROS
+#define DOCTEST_FAST_WARN_EQ DOCTEST_WARN_EQ
+#define DOCTEST_FAST_CHECK_EQ DOCTEST_CHECK_EQ
+#define DOCTEST_FAST_REQUIRE_EQ DOCTEST_REQUIRE_EQ
+#define DOCTEST_FAST_WARN_NE DOCTEST_WARN_NE
+#define DOCTEST_FAST_CHECK_NE DOCTEST_CHECK_NE
+#define DOCTEST_FAST_REQUIRE_NE DOCTEST_REQUIRE_NE
+#define DOCTEST_FAST_WARN_GT DOCTEST_WARN_GT
+#define DOCTEST_FAST_CHECK_GT DOCTEST_CHECK_GT
+#define DOCTEST_FAST_REQUIRE_GT DOCTEST_REQUIRE_GT
+#define DOCTEST_FAST_WARN_LT DOCTEST_WARN_LT
+#define DOCTEST_FAST_CHECK_LT DOCTEST_CHECK_LT
+#define DOCTEST_FAST_REQUIRE_LT DOCTEST_REQUIRE_LT
+#define DOCTEST_FAST_WARN_GE DOCTEST_WARN_GE
+#define DOCTEST_FAST_CHECK_GE DOCTEST_CHECK_GE
+#define DOCTEST_FAST_REQUIRE_GE DOCTEST_REQUIRE_GE
+#define DOCTEST_FAST_WARN_LE DOCTEST_WARN_LE
+#define DOCTEST_FAST_CHECK_LE DOCTEST_CHECK_LE
+#define DOCTEST_FAST_REQUIRE_LE DOCTEST_REQUIRE_LE
+
+#define DOCTEST_FAST_WARN_UNARY DOCTEST_WARN_UNARY
+#define DOCTEST_FAST_CHECK_UNARY DOCTEST_CHECK_UNARY
+#define DOCTEST_FAST_REQUIRE_UNARY DOCTEST_REQUIRE_UNARY
+#define DOCTEST_FAST_WARN_UNARY_FALSE DOCTEST_WARN_UNARY_FALSE
+#define DOCTEST_FAST_CHECK_UNARY_FALSE DOCTEST_CHECK_UNARY_FALSE
+#define DOCTEST_FAST_REQUIRE_UNARY_FALSE DOCTEST_REQUIRE_UNARY_FALSE
+
+#define DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id,__VA_ARGS__)
+// clang-format on
+
+// BDD style macros
+// clang-format off
+#define DOCTEST_SCENARIO(name) DOCTEST_TEST_CASE(" Scenario: " name)
+#define DOCTEST_SCENARIO_CLASS(name) DOCTEST_TEST_CASE_CLASS(" Scenario: " name)
+#define DOCTEST_SCENARIO_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(" Scenario: " name, T, __VA_ARGS__)
+#define DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(" Scenario: " name, T, id)
+
+#define DOCTEST_GIVEN(name) DOCTEST_SUBCASE(" Given: " name)
+#define DOCTEST_WHEN(name) DOCTEST_SUBCASE(" When: " name)
+#define DOCTEST_AND_WHEN(name) DOCTEST_SUBCASE("And when: " name)
+#define DOCTEST_THEN(name) DOCTEST_SUBCASE(" Then: " name)
+#define DOCTEST_AND_THEN(name) DOCTEST_SUBCASE(" And: " name)
+// clang-format on
+
+// == SHORT VERSIONS OF THE MACROS
+#ifndef DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES
+
+#define TEST_CASE(name) DOCTEST_TEST_CASE(name)
+#define TEST_CASE_CLASS(name) DOCTEST_TEST_CASE_CLASS(name)
+#define TEST_CASE_FIXTURE(x, name) DOCTEST_TEST_CASE_FIXTURE(x, name)
+#define TYPE_TO_STRING_AS(str, ...) DOCTEST_TYPE_TO_STRING_AS(str, __VA_ARGS__)
+#define TYPE_TO_STRING(...) DOCTEST_TYPE_TO_STRING(__VA_ARGS__)
+#define TEST_CASE_TEMPLATE(name, T, ...) DOCTEST_TEST_CASE_TEMPLATE(name, T, __VA_ARGS__)
+#define TEST_CASE_TEMPLATE_DEFINE(name, T, id) DOCTEST_TEST_CASE_TEMPLATE_DEFINE(name, T, id)
+#define TEST_CASE_TEMPLATE_INVOKE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INVOKE(id, __VA_ARGS__)
+#define TEST_CASE_TEMPLATE_APPLY(id, ...) DOCTEST_TEST_CASE_TEMPLATE_APPLY(id, __VA_ARGS__)
+#define SUBCASE(name) DOCTEST_SUBCASE(name)
+#define TEST_SUITE(decorators) DOCTEST_TEST_SUITE(decorators)
+#define TEST_SUITE_BEGIN(name) DOCTEST_TEST_SUITE_BEGIN(name)
+#define TEST_SUITE_END DOCTEST_TEST_SUITE_END
+#define REGISTER_EXCEPTION_TRANSLATOR(signature) DOCTEST_REGISTER_EXCEPTION_TRANSLATOR(signature)
+#define REGISTER_REPORTER(name, priority, reporter) DOCTEST_REGISTER_REPORTER(name, priority, reporter)
+#define REGISTER_LISTENER(name, priority, reporter) DOCTEST_REGISTER_LISTENER(name, priority, reporter)
+#define INFO(...) DOCTEST_INFO(__VA_ARGS__)
+#define CAPTURE(x) DOCTEST_CAPTURE(x)
+#define ADD_MESSAGE_AT(file, line, ...) DOCTEST_ADD_MESSAGE_AT(file, line, __VA_ARGS__)
+#define ADD_FAIL_CHECK_AT(file, line, ...) DOCTEST_ADD_FAIL_CHECK_AT(file, line, __VA_ARGS__)
+#define ADD_FAIL_AT(file, line, ...) DOCTEST_ADD_FAIL_AT(file, line, __VA_ARGS__)
+#define MESSAGE(...) DOCTEST_MESSAGE(__VA_ARGS__)
+#define FAIL_CHECK(...) DOCTEST_FAIL_CHECK(__VA_ARGS__)
+#define FAIL(...) DOCTEST_FAIL(__VA_ARGS__)
+#define TO_LVALUE(...) DOCTEST_TO_LVALUE(__VA_ARGS__)
+
+#define WARN(...) DOCTEST_WARN(__VA_ARGS__)
+#define WARN_FALSE(...) DOCTEST_WARN_FALSE(__VA_ARGS__)
+#define WARN_THROWS(...) DOCTEST_WARN_THROWS(__VA_ARGS__)
+#define WARN_THROWS_AS(expr, ...) DOCTEST_WARN_THROWS_AS(expr, __VA_ARGS__)
+#define WARN_THROWS_WITH(expr, ...) DOCTEST_WARN_THROWS_WITH(expr, __VA_ARGS__)
+#define WARN_THROWS_WITH_AS(expr, with, ...) DOCTEST_WARN_THROWS_WITH_AS(expr, with, __VA_ARGS__)
+#define WARN_NOTHROW(...) DOCTEST_WARN_NOTHROW(__VA_ARGS__)
+#define CHECK(...) DOCTEST_CHECK(__VA_ARGS__)
+#define CHECK_FALSE(...) DOCTEST_CHECK_FALSE(__VA_ARGS__)
+#define CHECK_THROWS(...) DOCTEST_CHECK_THROWS(__VA_ARGS__)
+#define CHECK_THROWS_AS(expr, ...) DOCTEST_CHECK_THROWS_AS(expr, __VA_ARGS__)
+#define CHECK_THROWS_WITH(expr, ...) DOCTEST_CHECK_THROWS_WITH(expr, __VA_ARGS__)
+#define CHECK_THROWS_WITH_AS(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_AS(expr, with, __VA_ARGS__)
+#define CHECK_NOTHROW(...) DOCTEST_CHECK_NOTHROW(__VA_ARGS__)
+#define REQUIRE(...) DOCTEST_REQUIRE(__VA_ARGS__)
+#define REQUIRE_FALSE(...) DOCTEST_REQUIRE_FALSE(__VA_ARGS__)
+#define REQUIRE_THROWS(...) DOCTEST_REQUIRE_THROWS(__VA_ARGS__)
+#define REQUIRE_THROWS_AS(expr, ...) DOCTEST_REQUIRE_THROWS_AS(expr, __VA_ARGS__)
+#define REQUIRE_THROWS_WITH(expr, ...) DOCTEST_REQUIRE_THROWS_WITH(expr, __VA_ARGS__)
+#define REQUIRE_THROWS_WITH_AS(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_AS(expr, with, __VA_ARGS__)
+#define REQUIRE_NOTHROW(...) DOCTEST_REQUIRE_NOTHROW(__VA_ARGS__)
+
+#define WARN_MESSAGE(cond, ...) DOCTEST_WARN_MESSAGE(cond, __VA_ARGS__)
+#define WARN_FALSE_MESSAGE(cond, ...) DOCTEST_WARN_FALSE_MESSAGE(cond, __VA_ARGS__)
+#define WARN_THROWS_MESSAGE(expr, ...) DOCTEST_WARN_THROWS_MESSAGE(expr, __VA_ARGS__)
+#define WARN_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_WARN_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__)
+#define WARN_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_WARN_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__)
+#define WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_WARN_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__)
+#define WARN_NOTHROW_MESSAGE(expr, ...) DOCTEST_WARN_NOTHROW_MESSAGE(expr, __VA_ARGS__)
+#define CHECK_MESSAGE(cond, ...) DOCTEST_CHECK_MESSAGE(cond, __VA_ARGS__)
+#define CHECK_FALSE_MESSAGE(cond, ...) DOCTEST_CHECK_FALSE_MESSAGE(cond, __VA_ARGS__)
+#define CHECK_THROWS_MESSAGE(expr, ...) DOCTEST_CHECK_THROWS_MESSAGE(expr, __VA_ARGS__)
+#define CHECK_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_CHECK_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__)
+#define CHECK_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_CHECK_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__)
+#define CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_CHECK_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__)
+#define CHECK_NOTHROW_MESSAGE(expr, ...) DOCTEST_CHECK_NOTHROW_MESSAGE(expr, __VA_ARGS__)
+#define REQUIRE_MESSAGE(cond, ...) DOCTEST_REQUIRE_MESSAGE(cond, __VA_ARGS__)
+#define REQUIRE_FALSE_MESSAGE(cond, ...) DOCTEST_REQUIRE_FALSE_MESSAGE(cond, __VA_ARGS__)
+#define REQUIRE_THROWS_MESSAGE(expr, ...) DOCTEST_REQUIRE_THROWS_MESSAGE(expr, __VA_ARGS__)
+#define REQUIRE_THROWS_AS_MESSAGE(expr, ex, ...) DOCTEST_REQUIRE_THROWS_AS_MESSAGE(expr, ex, __VA_ARGS__)
+#define REQUIRE_THROWS_WITH_MESSAGE(expr, with, ...) DOCTEST_REQUIRE_THROWS_WITH_MESSAGE(expr, with, __VA_ARGS__)
+#define REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, ...) DOCTEST_REQUIRE_THROWS_WITH_AS_MESSAGE(expr, with, ex, __VA_ARGS__)
+#define REQUIRE_NOTHROW_MESSAGE(expr, ...) DOCTEST_REQUIRE_NOTHROW_MESSAGE(expr, __VA_ARGS__)
+
+#define SCENARIO(name) DOCTEST_SCENARIO(name)
+#define SCENARIO_CLASS(name) DOCTEST_SCENARIO_CLASS(name)
+#define SCENARIO_TEMPLATE(name, T, ...) DOCTEST_SCENARIO_TEMPLATE(name, T, __VA_ARGS__)
+#define SCENARIO_TEMPLATE_DEFINE(name, T, id) DOCTEST_SCENARIO_TEMPLATE_DEFINE(name, T, id)
+#define GIVEN(name) DOCTEST_GIVEN(name)
+#define WHEN(name) DOCTEST_WHEN(name)
+#define AND_WHEN(name) DOCTEST_AND_WHEN(name)
+#define THEN(name) DOCTEST_THEN(name)
+#define AND_THEN(name) DOCTEST_AND_THEN(name)
+
+#define WARN_EQ(...) DOCTEST_WARN_EQ(__VA_ARGS__)
+#define CHECK_EQ(...) DOCTEST_CHECK_EQ(__VA_ARGS__)
+#define REQUIRE_EQ(...) DOCTEST_REQUIRE_EQ(__VA_ARGS__)
+#define WARN_NE(...) DOCTEST_WARN_NE(__VA_ARGS__)
+#define CHECK_NE(...) DOCTEST_CHECK_NE(__VA_ARGS__)
+#define REQUIRE_NE(...) DOCTEST_REQUIRE_NE(__VA_ARGS__)
+#define WARN_GT(...) DOCTEST_WARN_GT(__VA_ARGS__)
+#define CHECK_GT(...) DOCTEST_CHECK_GT(__VA_ARGS__)
+#define REQUIRE_GT(...) DOCTEST_REQUIRE_GT(__VA_ARGS__)
+#define WARN_LT(...) DOCTEST_WARN_LT(__VA_ARGS__)
+#define CHECK_LT(...) DOCTEST_CHECK_LT(__VA_ARGS__)
+#define REQUIRE_LT(...) DOCTEST_REQUIRE_LT(__VA_ARGS__)
+#define WARN_GE(...) DOCTEST_WARN_GE(__VA_ARGS__)
+#define CHECK_GE(...) DOCTEST_CHECK_GE(__VA_ARGS__)
+#define REQUIRE_GE(...) DOCTEST_REQUIRE_GE(__VA_ARGS__)
+#define WARN_LE(...) DOCTEST_WARN_LE(__VA_ARGS__)
+#define CHECK_LE(...) DOCTEST_CHECK_LE(__VA_ARGS__)
+#define REQUIRE_LE(...) DOCTEST_REQUIRE_LE(__VA_ARGS__)
+#define WARN_UNARY(...) DOCTEST_WARN_UNARY(__VA_ARGS__)
+#define CHECK_UNARY(...) DOCTEST_CHECK_UNARY(__VA_ARGS__)
+#define REQUIRE_UNARY(...) DOCTEST_REQUIRE_UNARY(__VA_ARGS__)
+#define WARN_UNARY_FALSE(...) DOCTEST_WARN_UNARY_FALSE(__VA_ARGS__)
+#define CHECK_UNARY_FALSE(...) DOCTEST_CHECK_UNARY_FALSE(__VA_ARGS__)
+#define REQUIRE_UNARY_FALSE(...) DOCTEST_REQUIRE_UNARY_FALSE(__VA_ARGS__)
+
+// KEPT FOR BACKWARDS COMPATIBILITY
+#define FAST_WARN_EQ(...) DOCTEST_FAST_WARN_EQ(__VA_ARGS__)
+#define FAST_CHECK_EQ(...) DOCTEST_FAST_CHECK_EQ(__VA_ARGS__)
+#define FAST_REQUIRE_EQ(...) DOCTEST_FAST_REQUIRE_EQ(__VA_ARGS__)
+#define FAST_WARN_NE(...) DOCTEST_FAST_WARN_NE(__VA_ARGS__)
+#define FAST_CHECK_NE(...) DOCTEST_FAST_CHECK_NE(__VA_ARGS__)
+#define FAST_REQUIRE_NE(...) DOCTEST_FAST_REQUIRE_NE(__VA_ARGS__)
+#define FAST_WARN_GT(...) DOCTEST_FAST_WARN_GT(__VA_ARGS__)
+#define FAST_CHECK_GT(...) DOCTEST_FAST_CHECK_GT(__VA_ARGS__)
+#define FAST_REQUIRE_GT(...) DOCTEST_FAST_REQUIRE_GT(__VA_ARGS__)
+#define FAST_WARN_LT(...) DOCTEST_FAST_WARN_LT(__VA_ARGS__)
+#define FAST_CHECK_LT(...) DOCTEST_FAST_CHECK_LT(__VA_ARGS__)
+#define FAST_REQUIRE_LT(...) DOCTEST_FAST_REQUIRE_LT(__VA_ARGS__)
+#define FAST_WARN_GE(...) DOCTEST_FAST_WARN_GE(__VA_ARGS__)
+#define FAST_CHECK_GE(...) DOCTEST_FAST_CHECK_GE(__VA_ARGS__)
+#define FAST_REQUIRE_GE(...) DOCTEST_FAST_REQUIRE_GE(__VA_ARGS__)
+#define FAST_WARN_LE(...) DOCTEST_FAST_WARN_LE(__VA_ARGS__)
+#define FAST_CHECK_LE(...) DOCTEST_FAST_CHECK_LE(__VA_ARGS__)
+#define FAST_REQUIRE_LE(...) DOCTEST_FAST_REQUIRE_LE(__VA_ARGS__)
+
+#define FAST_WARN_UNARY(...) DOCTEST_FAST_WARN_UNARY(__VA_ARGS__)
+#define FAST_CHECK_UNARY(...) DOCTEST_FAST_CHECK_UNARY(__VA_ARGS__)
+#define FAST_REQUIRE_UNARY(...) DOCTEST_FAST_REQUIRE_UNARY(__VA_ARGS__)
+#define FAST_WARN_UNARY_FALSE(...) DOCTEST_FAST_WARN_UNARY_FALSE(__VA_ARGS__)
+#define FAST_CHECK_UNARY_FALSE(...) DOCTEST_FAST_CHECK_UNARY_FALSE(__VA_ARGS__)
+#define FAST_REQUIRE_UNARY_FALSE(...) DOCTEST_FAST_REQUIRE_UNARY_FALSE(__VA_ARGS__)
+
+#define TEST_CASE_TEMPLATE_INSTANTIATE(id, ...) DOCTEST_TEST_CASE_TEMPLATE_INSTANTIATE(id, __VA_ARGS__)
+
+#endif // DOCTEST_CONFIG_NO_SHORT_MACRO_NAMES
+
+#ifndef DOCTEST_CONFIG_DISABLE
+
+// this is here to clear the 'current test suite' for the current translation unit - at the top
+DOCTEST_TEST_SUITE_END();
+
+#endif // DOCTEST_CONFIG_DISABLE
+
+DOCTEST_CLANG_SUPPRESS_WARNING_POP
+DOCTEST_MSVC_SUPPRESS_WARNING_POP
+DOCTEST_GCC_SUPPRESS_WARNING_POP
+
+DOCTEST_SUPPRESS_COMMON_WARNINGS_POP
+
+#endif // DOCTEST_LIBRARY_INCLUDED
+
+#ifndef DOCTEST_SINGLE_HEADER
+#define DOCTEST_SINGLE_HEADER
+#endif // DOCTEST_SINGLE_HEADER
+
+#if defined(DOCTEST_CONFIG_IMPLEMENT) || !defined(DOCTEST_SINGLE_HEADER)
+
+#ifndef DOCTEST_SINGLE_HEADER
+#include "doctest_fwd.h"
+#endif // DOCTEST_SINGLE_HEADER
+
+DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wunused-macros")
+
+#ifndef DOCTEST_LIBRARY_IMPLEMENTATION
+#define DOCTEST_LIBRARY_IMPLEMENTATION
+
+DOCTEST_CLANG_SUPPRESS_WARNING_POP
+
+DOCTEST_SUPPRESS_COMMON_WARNINGS_PUSH
+
+DOCTEST_CLANG_SUPPRESS_WARNING_PUSH
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wglobal-constructors")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wexit-time-destructors")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wsign-conversion")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wshorten-64-to-32")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-variable-declarations")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wswitch-enum")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wcovered-switch-default")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-noreturn")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wdisabled-macro-expansion")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-braces")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wmissing-field-initializers")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function")
+DOCTEST_CLANG_SUPPRESS_WARNING("-Wnonportable-system-include-path")
+
+DOCTEST_GCC_SUPPRESS_WARNING_PUSH
+DOCTEST_GCC_SUPPRESS_WARNING("-Wconversion")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wsign-conversion")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-field-initializers")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wmissing-braces")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-enum")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wswitch-default")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wunsafe-loop-optimizations")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wold-style-cast")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wunused-function")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wmultiple-inheritance")
+DOCTEST_GCC_SUPPRESS_WARNING("-Wsuggest-attribute")
+
+DOCTEST_MSVC_SUPPRESS_WARNING_PUSH
+DOCTEST_MSVC_SUPPRESS_WARNING(4267) // 'var' : conversion from 'x' to 'y', possible loss of data
+DOCTEST_MSVC_SUPPRESS_WARNING(4530) // C++ exception handler used, but unwind semantics not enabled
+DOCTEST_MSVC_SUPPRESS_WARNING(4577) // 'noexcept' used with no exception handling mode specified
+DOCTEST_MSVC_SUPPRESS_WARNING(4774) // format string expected in argument is not a string literal
+DOCTEST_MSVC_SUPPRESS_WARNING(4365) // conversion from 'int' to 'unsigned', signed/unsigned mismatch
+DOCTEST_MSVC_SUPPRESS_WARNING(5039) // pointer to potentially throwing function passed to extern C
+DOCTEST_MSVC_SUPPRESS_WARNING(4800) // forcing value to bool 'true' or 'false' (performance warning)
+DOCTEST_MSVC_SUPPRESS_WARNING(5245) // unreferenced function with internal linkage has been removed
+
+DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_BEGIN
+
+// required includes - will go only in one translation unit!
+#include <ctime>
+#include <cmath>
+#include <climits>
+// borland (Embarcadero) compiler requires math.h and not cmath - https://github.com/doctest/doctest/pull/37
+#ifdef __BORLANDC__
+#include <math.h>
+#endif // __BORLANDC__
+#include <new>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <limits>
+#include <utility>
+#include <fstream>
+#include <sstream>
+#include <iostream>
+#include <algorithm>
+#include <iomanip>
+#include <vector>
+#ifndef DOCTEST_CONFIG_NO_MULTITHREADING
+#include <atomic>
+#include <mutex>
+#define DOCTEST_DECLARE_MUTEX(name) std::mutex name;
+#define DOCTEST_DECLARE_STATIC_MUTEX(name) static DOCTEST_DECLARE_MUTEX(name)
+#define DOCTEST_LOCK_MUTEX(name) std::lock_guard<std::mutex> DOCTEST_ANONYMOUS(DOCTEST_ANON_LOCK_)(name);
+#else // DOCTEST_CONFIG_NO_MULTITHREADING
+#define DOCTEST_DECLARE_MUTEX(name)
+#define DOCTEST_DECLARE_STATIC_MUTEX(name)
+#define DOCTEST_LOCK_MUTEX(name)
+#endif // DOCTEST_CONFIG_NO_MULTITHREADING
+#include <set>
+#include <map>
+#include <unordered_set>
+#include <exception>
+#include <stdexcept>
+#include <csignal>
+#include <cfloat>
+#include <cctype>
+#include <cstdint>
+#include <string>
+
+#ifdef DOCTEST_PLATFORM_MAC
+#include <sys/types.h>
+#include <unistd.h>
+#include <sys/sysctl.h>
+#endif // DOCTEST_PLATFORM_MAC
+
+#ifdef DOCTEST_PLATFORM_WINDOWS
+
+// defines for a leaner windows.h
+#ifndef WIN32_LEAN_AND_MEAN
+#define WIN32_LEAN_AND_MEAN
+#endif // WIN32_LEAN_AND_MEAN
+#ifndef NOMINMAX
+#define NOMINMAX
+#endif // NOMINMAX
+
+// not sure what AfxWin.h is for - here I do what Catch does
+#ifdef __AFXDLL
+#include <AfxWin.h>
+#else
+#include <windows.h>
+#endif
+#include <io.h>
+
+#else // DOCTEST_PLATFORM_WINDOWS
+
+#include <sys/time.h>
+#include <unistd.h>
+
+#endif // DOCTEST_PLATFORM_WINDOWS
+
+// this is a fix for https://github.com/doctest/doctest/issues/348
+// https://mail.gnome.org/archives/xml/2012-January/msg00000.html
+#if !defined(HAVE_UNISTD_H) && !defined(STDOUT_FILENO)
+#define STDOUT_FILENO fileno(stdout)
+#endif // HAVE_UNISTD_H
+
+DOCTEST_MAKE_STD_HEADERS_CLEAN_FROM_WARNINGS_ON_WALL_END
+
+// counts the number of elements in a C array
+#define DOCTEST_COUNTOF(x) (sizeof(x) / sizeof(x[0]))
+
+#ifdef DOCTEST_CONFIG_DISABLE
+#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_disabled
+#else // DOCTEST_CONFIG_DISABLE
+#define DOCTEST_BRANCH_ON_DISABLED(if_disabled, if_not_disabled) if_not_disabled
+#endif // DOCTEST_CONFIG_DISABLE
+
+#ifndef DOCTEST_CONFIG_OPTIONS_PREFIX
+#define DOCTEST_CONFIG_OPTIONS_PREFIX "dt-"
+#endif
+
+#ifndef DOCTEST_THREAD_LOCAL
+#if defined(DOCTEST_CONFIG_NO_MULTITHREADING) || DOCTEST_MSVC && (DOCTEST_MSVC < DOCTEST_COMPILER(19, 0, 0))
+#define DOCTEST_THREAD_LOCAL
+#else // DOCTEST_MSVC
+#define DOCTEST_THREAD_LOCAL thread_local
+#endif // DOCTEST_MSVC
+#endif // DOCTEST_THREAD_LOCAL
+
+#ifndef DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES
+#define DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES 32
+#endif
+
+#ifndef DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE
+#define DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE 64
+#endif
+
+#ifdef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS
+#define DOCTEST_OPTIONS_PREFIX_DISPLAY DOCTEST_CONFIG_OPTIONS_PREFIX
+#else
+#define DOCTEST_OPTIONS_PREFIX_DISPLAY ""
+#endif
+
+#if defined(WINAPI_FAMILY) && (WINAPI_FAMILY == WINAPI_FAMILY_APP)
+#define DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS
+#endif
+
+#ifndef DOCTEST_CDECL
+#define DOCTEST_CDECL __cdecl
+#endif
+
+namespace doctest {
+
+bool is_running_in_test = false;
+
+namespace {
+ using namespace detail;
+
+ template <typename Ex>
+ DOCTEST_NORETURN void throw_exception(Ex const& e) {
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+ throw e;
+#else // DOCTEST_CONFIG_NO_EXCEPTIONS
+ std::cerr << "doctest will terminate because it needed to throw an exception.\n"
+ << "The message was: " << e.what() << '\n';
+ std::terminate();
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+ }
+
+#ifndef DOCTEST_INTERNAL_ERROR
+#define DOCTEST_INTERNAL_ERROR(msg) \
+ throw_exception(std::logic_error( \
+ __FILE__ ":" DOCTEST_TOSTR(__LINE__) ": Internal doctest error: " msg))
+#endif // DOCTEST_INTERNAL_ERROR
+
+ // case insensitive strcmp
+ int stricmp(const char* a, const char* b) {
+ for(;; a++, b++) {
+ const int d = tolower(*a) - tolower(*b);
+ if(d != 0 || !*a)
+ return d;
+ }
+ }
+
+ struct Endianness
+ {
+ enum Arch
+ {
+ Big,
+ Little
+ };
+
+ static Arch which() {
+ int x = 1;
+ // casting any data pointer to char* is allowed
+ auto ptr = reinterpret_cast<char*>(&x);
+ if(*ptr)
+ return Little;
+ return Big;
+ }
+ };
+} // namespace
+
+namespace detail {
+ DOCTEST_THREAD_LOCAL class
+ {
+ std::vector<std::streampos> stack;
+ std::stringstream ss;
+
+ public:
+ std::ostream* push() {
+ stack.push_back(ss.tellp());
+ return &ss;
+ }
+
+ String pop() {
+ if (stack.empty())
+ DOCTEST_INTERNAL_ERROR("TLSS was empty when trying to pop!");
+
+ std::streampos pos = stack.back();
+ stack.pop_back();
+ unsigned sz = static_cast<unsigned>(ss.tellp() - pos);
+ ss.rdbuf()->pubseekpos(pos, std::ios::in | std::ios::out);
+ return String(ss, sz);
+ }
+ } g_oss;
+
+ std::ostream* tlssPush() {
+ return g_oss.push();
+ }
+
+ String tlssPop() {
+ return g_oss.pop();
+ }
+
+#ifndef DOCTEST_CONFIG_DISABLE
+
+namespace timer_large_integer
+{
+
+#if defined(DOCTEST_PLATFORM_WINDOWS)
+ using type = ULONGLONG;
+#else // DOCTEST_PLATFORM_WINDOWS
+ using type = std::uint64_t;
+#endif // DOCTEST_PLATFORM_WINDOWS
+}
+
+using ticks_t = timer_large_integer::type;
+
+#ifdef DOCTEST_CONFIG_GETCURRENTTICKS
+ ticks_t getCurrentTicks() { return DOCTEST_CONFIG_GETCURRENTTICKS(); }
+#elif defined(DOCTEST_PLATFORM_WINDOWS)
+ ticks_t getCurrentTicks() {
+ static LARGE_INTEGER hz = { {0} }, hzo = { {0} };
+ if(!hz.QuadPart) {
+ QueryPerformanceFrequency(&hz);
+ QueryPerformanceCounter(&hzo);
+ }
+ LARGE_INTEGER t;
+ QueryPerformanceCounter(&t);
+ return ((t.QuadPart - hzo.QuadPart) * LONGLONG(1000000)) / hz.QuadPart;
+ }
+#else // DOCTEST_PLATFORM_WINDOWS
+ ticks_t getCurrentTicks() {
+ timeval t;
+ gettimeofday(&t, nullptr);
+ return static_cast<ticks_t>(t.tv_sec) * 1000000 + static_cast<ticks_t>(t.tv_usec);
+ }
+#endif // DOCTEST_PLATFORM_WINDOWS
+
+ struct Timer
+ {
+ void start() { m_ticks = getCurrentTicks(); }
+ unsigned int getElapsedMicroseconds() const {
+ return static_cast<unsigned int>(getCurrentTicks() - m_ticks);
+ }
+ //unsigned int getElapsedMilliseconds() const {
+ // return static_cast<unsigned int>(getElapsedMicroseconds() / 1000);
+ //}
+ double getElapsedSeconds() const { return static_cast<double>(getCurrentTicks() - m_ticks) / 1000000.0; }
+
+ private:
+ ticks_t m_ticks = 0;
+ };
+
+#ifdef DOCTEST_CONFIG_NO_MULTITHREADING
+ template <typename T>
+ using Atomic = T;
+#else // DOCTEST_CONFIG_NO_MULTITHREADING
+ template <typename T>
+ using Atomic = std::atomic<T>;
+#endif // DOCTEST_CONFIG_NO_MULTITHREADING
+
+#if defined(DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS) || defined(DOCTEST_CONFIG_NO_MULTITHREADING)
+ template <typename T>
+ using MultiLaneAtomic = Atomic<T>;
+#else // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS
+ // Provides a multilane implementation of an atomic variable that supports add, sub, load,
+ // store. Instead of using a single atomic variable, this splits up into multiple ones,
+ // each sitting on a separate cache line. The goal is to provide a speedup when most
+ // operations are modifying. It achieves this with two properties:
+ //
+ // * Multiple atomics are used, so chance of congestion from the same atomic is reduced.
+ // * Each atomic sits on a separate cache line, so false sharing is reduced.
+ //
+ // The disadvantage is that there is a small overhead due to the use of TLS, and load/store
+ // is slower because all atomics have to be accessed.
+ template <typename T>
+ class MultiLaneAtomic
+ {
+ struct CacheLineAlignedAtomic
+ {
+ Atomic<T> atomic{};
+ char padding[DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE - sizeof(Atomic<T>)];
+ };
+ CacheLineAlignedAtomic m_atomics[DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES];
+
+ static_assert(sizeof(CacheLineAlignedAtomic) == DOCTEST_MULTI_LANE_ATOMICS_CACHE_LINE_SIZE,
+ "guarantee one atomic takes exactly one cache line");
+
+ public:
+ T operator++() DOCTEST_NOEXCEPT { return fetch_add(1) + 1; }
+
+ T operator++(int) DOCTEST_NOEXCEPT { return fetch_add(1); }
+
+ T fetch_add(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT {
+ return myAtomic().fetch_add(arg, order);
+ }
+
+ T fetch_sub(T arg, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT {
+ return myAtomic().fetch_sub(arg, order);
+ }
+
+ operator T() const DOCTEST_NOEXCEPT { return load(); }
+
+ T load(std::memory_order order = std::memory_order_seq_cst) const DOCTEST_NOEXCEPT {
+ auto result = T();
+ for(auto const& c : m_atomics) {
+ result += c.atomic.load(order);
+ }
+ return result;
+ }
+
+ T operator=(T desired) DOCTEST_NOEXCEPT { // lgtm [cpp/assignment-does-not-return-this]
+ store(desired);
+ return desired;
+ }
+
+ void store(T desired, std::memory_order order = std::memory_order_seq_cst) DOCTEST_NOEXCEPT {
+ // first value becomes desired", all others become 0.
+ for(auto& c : m_atomics) {
+ c.atomic.store(desired, order);
+ desired = {};
+ }
+ }
+
+ private:
+ // Each thread has a different atomic that it operates on. If more than NumLanes threads
+ // use this, some will use the same atomic. So performance will degrade a bit, but still
+ // everything will work.
+ //
+ // The logic here is a bit tricky. The call should be as fast as possible, so that there
+ // is minimal to no overhead in determining the correct atomic for the current thread.
+ //
+ // 1. A global static counter laneCounter counts continuously up.
+ // 2. Each successive thread will use modulo operation of that counter so it gets an atomic
+ // assigned in a round-robin fashion.
+ // 3. This tlsLaneIdx is stored in the thread local data, so it is directly available with
+ // little overhead.
+ Atomic<T>& myAtomic() DOCTEST_NOEXCEPT {
+ static Atomic<size_t> laneCounter;
+ DOCTEST_THREAD_LOCAL size_t tlsLaneIdx =
+ laneCounter++ % DOCTEST_MULTI_LANE_ATOMICS_THREAD_LANES;
+
+ return m_atomics[tlsLaneIdx].atomic;
+ }
+ };
+#endif // DOCTEST_CONFIG_NO_MULTI_LANE_ATOMICS
+
+ // this holds both parameters from the command line and runtime data for tests
+ struct ContextState : ContextOptions, TestRunStats, CurrentTestCaseStats
+ {
+ MultiLaneAtomic<int> numAssertsCurrentTest_atomic;
+ MultiLaneAtomic<int> numAssertsFailedCurrentTest_atomic;
+
+ std::vector<std::vector<String>> filters = decltype(filters)(9); // 9 different filters
+
+ std::vector<IReporter*> reporters_currently_used;
+
+ assert_handler ah = nullptr;
+
+ Timer timer;
+
+ std::vector<String> stringifiedContexts; // logging from INFO() due to an exception
+
+ // stuff for subcases
+ bool reachedLeaf;
+ std::vector<SubcaseSignature> subcaseStack;
+ std::vector<SubcaseSignature> nextSubcaseStack;
+ std::unordered_set<unsigned long long> fullyTraversedSubcases;
+ size_t currentSubcaseDepth;
+ Atomic<bool> shouldLogCurrentException;
+
+ void resetRunData() {
+ numTestCases = 0;
+ numTestCasesPassingFilters = 0;
+ numTestSuitesPassingFilters = 0;
+ numTestCasesFailed = 0;
+ numAsserts = 0;
+ numAssertsFailed = 0;
+ numAssertsCurrentTest = 0;
+ numAssertsFailedCurrentTest = 0;
+ }
+
+ void finalizeTestCaseData() {
+ seconds = timer.getElapsedSeconds();
+
+ // update the non-atomic counters
+ numAsserts += numAssertsCurrentTest_atomic;
+ numAssertsFailed += numAssertsFailedCurrentTest_atomic;
+ numAssertsCurrentTest = numAssertsCurrentTest_atomic;
+ numAssertsFailedCurrentTest = numAssertsFailedCurrentTest_atomic;
+
+ if(numAssertsFailedCurrentTest)
+ failure_flags |= TestCaseFailureReason::AssertFailure;
+
+ if(Approx(currentTest->m_timeout).epsilon(DBL_EPSILON) != 0 &&
+ Approx(seconds).epsilon(DBL_EPSILON) > currentTest->m_timeout)
+ failure_flags |= TestCaseFailureReason::Timeout;
+
+ if(currentTest->m_should_fail) {
+ if(failure_flags) {
+ failure_flags |= TestCaseFailureReason::ShouldHaveFailedAndDid;
+ } else {
+ failure_flags |= TestCaseFailureReason::ShouldHaveFailedButDidnt;
+ }
+ } else if(failure_flags && currentTest->m_may_fail) {
+ failure_flags |= TestCaseFailureReason::CouldHaveFailedAndDid;
+ } else if(currentTest->m_expected_failures > 0) {
+ if(numAssertsFailedCurrentTest == currentTest->m_expected_failures) {
+ failure_flags |= TestCaseFailureReason::FailedExactlyNumTimes;
+ } else {
+ failure_flags |= TestCaseFailureReason::DidntFailExactlyNumTimes;
+ }
+ }
+
+ bool ok_to_fail = (TestCaseFailureReason::ShouldHaveFailedAndDid & failure_flags) ||
+ (TestCaseFailureReason::CouldHaveFailedAndDid & failure_flags) ||
+ (TestCaseFailureReason::FailedExactlyNumTimes & failure_flags);
+
+ // if any subcase has failed - the whole test case has failed
+ testCaseSuccess = !(failure_flags && !ok_to_fail);
+ if(!testCaseSuccess)
+ numTestCasesFailed++;
+ }
+ };
+
+ ContextState* g_cs = nullptr;
+
+ // used to avoid locks for the debug output
+ // TODO: figure out if this is indeed necessary/correct - seems like either there still
+ // could be a race or that there wouldn't be a race even if using the context directly
+ DOCTEST_THREAD_LOCAL bool g_no_colors;
+
+#endif // DOCTEST_CONFIG_DISABLE
+} // namespace detail
+
+char* String::allocate(size_type sz) {
+ if (sz <= last) {
+ buf[sz] = '\0';
+ setLast(last - sz);
+ return buf;
+ } else {
+ setOnHeap();
+ data.size = sz;
+ data.capacity = data.size + 1;
+ data.ptr = new char[data.capacity];
+ data.ptr[sz] = '\0';
+ return data.ptr;
+ }
+}
+
+void String::setOnHeap() noexcept { *reinterpret_cast<unsigned char*>(&buf[last]) = 128; }
+void String::setLast(size_type in) noexcept { buf[last] = char(in); }
+void String::setSize(size_type sz) noexcept {
+ if (isOnStack()) { buf[sz] = '\0'; setLast(last - sz); }
+ else { data.ptr[sz] = '\0'; data.size = sz; }
+}
+
+void String::copy(const String& other) {
+ if(other.isOnStack()) {
+ memcpy(buf, other.buf, len);
+ } else {
+ memcpy(allocate(other.data.size), other.data.ptr, other.data.size);
+ }
+}
+
+String::String() noexcept {
+ buf[0] = '\0';
+ setLast();
+}
+
+String::~String() {
+ if(!isOnStack())
+ delete[] data.ptr;
+} // NOLINT(clang-analyzer-cplusplus.NewDeleteLeaks)
+
+String::String(const char* in)
+ : String(in, strlen(in)) {}
+
+String::String(const char* in, size_type in_size) {
+ memcpy(allocate(in_size), in, in_size);
+}
+
+String::String(std::istream& in, size_type in_size) {
+ in.read(allocate(in_size), in_size);
+}
+
+String::String(const String& other) { copy(other); }
+
+String& String::operator=(const String& other) {
+ if(this != &other) {
+ if(!isOnStack())
+ delete[] data.ptr;
+
+ copy(other);
+ }
+
+ return *this;
+}
+
+String& String::operator+=(const String& other) {
+ const size_type my_old_size = size();
+ const size_type other_size = other.size();
+ const size_type total_size = my_old_size + other_size;
+ if(isOnStack()) {
+ if(total_size < len) {
+ // append to the current stack space
+ memcpy(buf + my_old_size, other.c_str(), other_size + 1);
+ // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks)
+ setLast(last - total_size);
+ } else {
+ // alloc new chunk
+ char* temp = new char[total_size + 1];
+ // copy current data to new location before writing in the union
+ memcpy(temp, buf, my_old_size); // skip the +1 ('\0') for speed
+ // update data in union
+ setOnHeap();
+ data.size = total_size;
+ data.capacity = data.size + 1;
+ data.ptr = temp;
+ // transfer the rest of the data
+ memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1);
+ }
+ } else {
+ if(data.capacity > total_size) {
+ // append to the current heap block
+ data.size = total_size;
+ memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1);
+ } else {
+ // resize
+ data.capacity *= 2;
+ if(data.capacity <= total_size)
+ data.capacity = total_size + 1;
+ // alloc new chunk
+ char* temp = new char[data.capacity];
+ // copy current data to new location before releasing it
+ memcpy(temp, data.ptr, my_old_size); // skip the +1 ('\0') for speed
+ // release old chunk
+ delete[] data.ptr;
+ // update the rest of the union members
+ data.size = total_size;
+ data.ptr = temp;
+ // transfer the rest of the data
+ memcpy(data.ptr + my_old_size, other.c_str(), other_size + 1);
+ }
+ }
+
+ return *this;
+}
+
+String::String(String&& other) noexcept {
+ memcpy(buf, other.buf, len);
+ other.buf[0] = '\0';
+ other.setLast();
+}
+
+String& String::operator=(String&& other) noexcept {
+ if(this != &other) {
+ if(!isOnStack())
+ delete[] data.ptr;
+ memcpy(buf, other.buf, len);
+ other.buf[0] = '\0';
+ other.setLast();
+ }
+ return *this;
+}
+
+char String::operator[](size_type i) const {
+ return const_cast<String*>(this)->operator[](i);
+}
+
+char& String::operator[](size_type i) {
+ if(isOnStack())
+ return reinterpret_cast<char*>(buf)[i];
+ return data.ptr[i];
+}
+
+DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wmaybe-uninitialized")
+String::size_type String::size() const {
+ if(isOnStack())
+ return last - (size_type(buf[last]) & 31); // using "last" would work only if "len" is 32
+ return data.size;
+}
+DOCTEST_GCC_SUPPRESS_WARNING_POP
+
+String::size_type String::capacity() const {
+ if(isOnStack())
+ return len;
+ return data.capacity;
+}
+
+String String::substr(size_type pos, size_type cnt) && {
+ cnt = std::min(cnt, size() - 1 - pos);
+ char* cptr = c_str();
+ memmove(cptr, cptr + pos, cnt);
+ setSize(cnt);
+ return std::move(*this);
+}
+
+String String::substr(size_type pos, size_type cnt) const & {
+ cnt = std::min(cnt, size() - 1 - pos);
+ return String{ c_str() + pos, cnt };
+}
+
+String::size_type String::find(char ch, size_type pos) const {
+ const char* begin = c_str();
+ const char* end = begin + size();
+ const char* it = begin + pos;
+ for (; it < end && *it != ch; it++);
+ if (it < end) { return static_cast<size_type>(it - begin); }
+ else { return npos; }
+}
+
+String::size_type String::rfind(char ch, size_type pos) const {
+ const char* begin = c_str();
+ const char* it = begin + std::min(pos, size() - 1);
+ for (; it >= begin && *it != ch; it--);
+ if (it >= begin) { return static_cast<size_type>(it - begin); }
+ else { return npos; }
+}
+
+int String::compare(const char* other, bool no_case) const {
+ if(no_case)
+ return doctest::stricmp(c_str(), other);
+ return std::strcmp(c_str(), other);
+}
+
+int String::compare(const String& other, bool no_case) const {
+ return compare(other.c_str(), no_case);
+}
+
+String operator+(const String& lhs, const String& rhs) { return String(lhs) += rhs; }
+
+bool operator==(const String& lhs, const String& rhs) { return lhs.compare(rhs) == 0; }
+bool operator!=(const String& lhs, const String& rhs) { return lhs.compare(rhs) != 0; }
+bool operator< (const String& lhs, const String& rhs) { return lhs.compare(rhs) < 0; }
+bool operator> (const String& lhs, const String& rhs) { return lhs.compare(rhs) > 0; }
+bool operator<=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) < 0 : true; }
+bool operator>=(const String& lhs, const String& rhs) { return (lhs != rhs) ? lhs.compare(rhs) > 0 : true; }
+
+std::ostream& operator<<(std::ostream& s, const String& in) { return s << in.c_str(); }
+
+Contains::Contains(const String& str) : string(str) { }
+
+bool Contains::checkWith(const String& other) const {
+ return strstr(other.c_str(), string.c_str()) != nullptr;
+}
+
+String toString(const Contains& in) {
+ return "Contains( " + in.string + " )";
+}
+
+bool operator==(const String& lhs, const Contains& rhs) { return rhs.checkWith(lhs); }
+bool operator==(const Contains& lhs, const String& rhs) { return lhs.checkWith(rhs); }
+bool operator!=(const String& lhs, const Contains& rhs) { return !rhs.checkWith(lhs); }
+bool operator!=(const Contains& lhs, const String& rhs) { return !lhs.checkWith(rhs); }
+
+namespace {
+ void color_to_stream(std::ostream&, Color::Enum) DOCTEST_BRANCH_ON_DISABLED({}, ;)
+} // namespace
+
+namespace Color {
+ std::ostream& operator<<(std::ostream& s, Color::Enum code) {
+ color_to_stream(s, code);
+ return s;
+ }
+} // namespace Color
+
+// clang-format off
+const char* assertString(assertType::Enum at) {
+ DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4061) // enum 'x' in switch of enum 'y' is not explicitely handled
+ #define DOCTEST_GENERATE_ASSERT_TYPE_CASE(assert_type) case assertType::DT_ ## assert_type: return #assert_type
+ #define DOCTEST_GENERATE_ASSERT_TYPE_CASES(assert_type) \
+ DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN_ ## assert_type); \
+ DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK_ ## assert_type); \
+ DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE_ ## assert_type)
+ switch(at) {
+ DOCTEST_GENERATE_ASSERT_TYPE_CASE(WARN);
+ DOCTEST_GENERATE_ASSERT_TYPE_CASE(CHECK);
+ DOCTEST_GENERATE_ASSERT_TYPE_CASE(REQUIRE);
+
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(FALSE);
+
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS);
+
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_AS);
+
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH);
+
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(THROWS_WITH_AS);
+
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(NOTHROW);
+
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(EQ);
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(NE);
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(GT);
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(LT);
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(GE);
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(LE);
+
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY);
+ DOCTEST_GENERATE_ASSERT_TYPE_CASES(UNARY_FALSE);
+
+ default: DOCTEST_INTERNAL_ERROR("Tried stringifying invalid assert type!");
+ }
+ DOCTEST_MSVC_SUPPRESS_WARNING_POP
+}
+// clang-format on
+
+const char* failureString(assertType::Enum at) {
+ if(at & assertType::is_warn) //!OCLINT bitwise operator in conditional
+ return "WARNING";
+ if(at & assertType::is_check) //!OCLINT bitwise operator in conditional
+ return "ERROR";
+ if(at & assertType::is_require) //!OCLINT bitwise operator in conditional
+ return "FATAL ERROR";
+ return "";
+}
+
+DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference")
+DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wnull-dereference")
+// depending on the current options this will remove the path of filenames
+const char* skipPathFromFilename(const char* file) {
+#ifndef DOCTEST_CONFIG_DISABLE
+ if(getContextOptions()->no_path_in_filenames) {
+ auto back = std::strrchr(file, '\\');
+ auto forward = std::strrchr(file, '/');
+ if(back || forward) {
+ if(back > forward)
+ forward = back;
+ return forward + 1;
+ }
+ }
+#endif // DOCTEST_CONFIG_DISABLE
+ return file;
+}
+DOCTEST_CLANG_SUPPRESS_WARNING_POP
+DOCTEST_GCC_SUPPRESS_WARNING_POP
+
+bool SubcaseSignature::operator==(const SubcaseSignature& other) const {
+ return m_line == other.m_line
+ && std::strcmp(m_file, other.m_file) == 0
+ && m_name == other.m_name;
+}
+
+bool SubcaseSignature::operator<(const SubcaseSignature& other) const {
+ if(m_line != other.m_line)
+ return m_line < other.m_line;
+ if(std::strcmp(m_file, other.m_file) != 0)
+ return std::strcmp(m_file, other.m_file) < 0;
+ return m_name.compare(other.m_name) < 0;
+}
+
+DOCTEST_DEFINE_INTERFACE(IContextScope)
+
+namespace detail {
+ void filldata<const void*>::fill(std::ostream* stream, const void* in) {
+ if (in) { *stream << in; }
+ else { *stream << "nullptr"; }
+ }
+
+ template <typename T>
+ String toStreamLit(T t) {
+ std::ostream* os = tlssPush();
+ os->operator<<(t);
+ return tlssPop();
+ }
+}
+
+#ifdef DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+String toString(const char* in) { return String("\"") + (in ? in : "{null string}") + "\""; }
+#endif // DOCTEST_CONFIG_TREAT_CHAR_STAR_AS_STRING
+
+#if DOCTEST_MSVC >= DOCTEST_COMPILER(19, 20, 0)
+// see this issue on why this is needed: https://github.com/doctest/doctest/issues/183
+String toString(const std::string& in) { return in.c_str(); }
+#endif // VS 2019
+
+String toString(String in) { return in; }
+
+String toString(std::nullptr_t) { return "nullptr"; }
+
+String toString(bool in) { return in ? "true" : "false"; }
+
+String toString(float in) { return toStreamLit(in); }
+String toString(double in) { return toStreamLit(in); }
+String toString(double long in) { return toStreamLit(in); }
+
+String toString(char in) { return toStreamLit(static_cast<signed>(in)); }
+String toString(char signed in) { return toStreamLit(static_cast<signed>(in)); }
+String toString(char unsigned in) { return toStreamLit(static_cast<unsigned>(in)); }
+String toString(short in) { return toStreamLit(in); }
+String toString(short unsigned in) { return toStreamLit(in); }
+String toString(signed in) { return toStreamLit(in); }
+String toString(unsigned in) { return toStreamLit(in); }
+String toString(long in) { return toStreamLit(in); }
+String toString(long unsigned in) { return toStreamLit(in); }
+String toString(long long in) { return toStreamLit(in); }
+String toString(long long unsigned in) { return toStreamLit(in); }
+
+Approx::Approx(double value)
+ : m_epsilon(static_cast<double>(std::numeric_limits<float>::epsilon()) * 100)
+ , m_scale(1.0)
+ , m_value(value) {}
+
+Approx Approx::operator()(double value) const {
+ Approx approx(value);
+ approx.epsilon(m_epsilon);
+ approx.scale(m_scale);
+ return approx;
+}
+
+Approx& Approx::epsilon(double newEpsilon) {
+ m_epsilon = newEpsilon;
+ return *this;
+}
+Approx& Approx::scale(double newScale) {
+ m_scale = newScale;
+ return *this;
+}
+
+bool operator==(double lhs, const Approx& rhs) {
+ // Thanks to Richard Harris for his help refining this formula
+ return std::fabs(lhs - rhs.m_value) <
+ rhs.m_epsilon * (rhs.m_scale + std::max<double>(std::fabs(lhs), std::fabs(rhs.m_value)));
+}
+bool operator==(const Approx& lhs, double rhs) { return operator==(rhs, lhs); }
+bool operator!=(double lhs, const Approx& rhs) { return !operator==(lhs, rhs); }
+bool operator!=(const Approx& lhs, double rhs) { return !operator==(rhs, lhs); }
+bool operator<=(double lhs, const Approx& rhs) { return lhs < rhs.m_value || lhs == rhs; }
+bool operator<=(const Approx& lhs, double rhs) { return lhs.m_value < rhs || lhs == rhs; }
+bool operator>=(double lhs, const Approx& rhs) { return lhs > rhs.m_value || lhs == rhs; }
+bool operator>=(const Approx& lhs, double rhs) { return lhs.m_value > rhs || lhs == rhs; }
+bool operator<(double lhs, const Approx& rhs) { return lhs < rhs.m_value && lhs != rhs; }
+bool operator<(const Approx& lhs, double rhs) { return lhs.m_value < rhs && lhs != rhs; }
+bool operator>(double lhs, const Approx& rhs) { return lhs > rhs.m_value && lhs != rhs; }
+bool operator>(const Approx& lhs, double rhs) { return lhs.m_value > rhs && lhs != rhs; }
+
+String toString(const Approx& in) {
+ return "Approx( " + doctest::toString(in.m_value) + " )";
+}
+const ContextOptions* getContextOptions() { return DOCTEST_BRANCH_ON_DISABLED(nullptr, g_cs); }
+
+DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4738)
+template <typename F>
+IsNaN<F>::operator bool() const {
+ return std::isnan(value) ^ flipped;
+}
+DOCTEST_MSVC_SUPPRESS_WARNING_POP
+template struct DOCTEST_INTERFACE_DEF IsNaN<float>;
+template struct DOCTEST_INTERFACE_DEF IsNaN<double>;
+template struct DOCTEST_INTERFACE_DEF IsNaN<long double>;
+template <typename F>
+String toString(IsNaN<F> in) { return String(in.flipped ? "! " : "") + "IsNaN( " + doctest::toString(in.value) + " )"; }
+String toString(IsNaN<float> in) { return toString<float>(in); }
+String toString(IsNaN<double> in) { return toString<double>(in); }
+String toString(IsNaN<double long> in) { return toString<double long>(in); }
+
+} // namespace doctest
+
+#ifdef DOCTEST_CONFIG_DISABLE
+namespace doctest {
+Context::Context(int, const char* const*) {}
+Context::~Context() = default;
+void Context::applyCommandLine(int, const char* const*) {}
+void Context::addFilter(const char*, const char*) {}
+void Context::clearFilters() {}
+void Context::setOption(const char*, bool) {}
+void Context::setOption(const char*, int) {}
+void Context::setOption(const char*, const char*) {}
+bool Context::shouldExit() { return false; }
+void Context::setAsDefaultForAssertsOutOfTestCases() {}
+void Context::setAssertHandler(detail::assert_handler) {}
+void Context::setCout(std::ostream*) {}
+int Context::run() { return 0; }
+
+int IReporter::get_num_active_contexts() { return 0; }
+const IContextScope* const* IReporter::get_active_contexts() { return nullptr; }
+int IReporter::get_num_stringified_contexts() { return 0; }
+const String* IReporter::get_stringified_contexts() { return nullptr; }
+
+int registerReporter(const char*, int, IReporter*) { return 0; }
+
+} // namespace doctest
+#else // DOCTEST_CONFIG_DISABLE
+
+#if !defined(DOCTEST_CONFIG_COLORS_NONE)
+#if !defined(DOCTEST_CONFIG_COLORS_WINDOWS) && !defined(DOCTEST_CONFIG_COLORS_ANSI)
+#ifdef DOCTEST_PLATFORM_WINDOWS
+#define DOCTEST_CONFIG_COLORS_WINDOWS
+#else // linux
+#define DOCTEST_CONFIG_COLORS_ANSI
+#endif // platform
+#endif // DOCTEST_CONFIG_COLORS_WINDOWS && DOCTEST_CONFIG_COLORS_ANSI
+#endif // DOCTEST_CONFIG_COLORS_NONE
+
+namespace doctest_detail_test_suite_ns {
+// holds the current test suite
+doctest::detail::TestSuite& getCurrentTestSuite() {
+ static doctest::detail::TestSuite data{};
+ return data;
+}
+} // namespace doctest_detail_test_suite_ns
+
+namespace doctest {
+namespace {
+ // the int (priority) is part of the key for automatic sorting - sadly one can register a
+ // reporter with a duplicate name and a different priority but hopefully that won't happen often :|
+ using reporterMap = std::map<std::pair<int, String>, reporterCreatorFunc>;
+
+ reporterMap& getReporters() {
+ static reporterMap data;
+ return data;
+ }
+ reporterMap& getListeners() {
+ static reporterMap data;
+ return data;
+ }
+} // namespace
+namespace detail {
+#define DOCTEST_ITERATE_THROUGH_REPORTERS(function, ...) \
+ for(auto& curr_rep : g_cs->reporters_currently_used) \
+ curr_rep->function(__VA_ARGS__)
+
+ bool checkIfShouldThrow(assertType::Enum at) {
+ if(at & assertType::is_require) //!OCLINT bitwise operator in conditional
+ return true;
+
+ if((at & assertType::is_check) //!OCLINT bitwise operator in conditional
+ && getContextOptions()->abort_after > 0 &&
+ (g_cs->numAssertsFailed + g_cs->numAssertsFailedCurrentTest_atomic) >=
+ getContextOptions()->abort_after)
+ return true;
+
+ return false;
+ }
+
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+ DOCTEST_NORETURN void throwException() {
+ g_cs->shouldLogCurrentException = false;
+ throw TestFailureException(); // NOLINT(hicpp-exception-baseclass)
+ }
+#else // DOCTEST_CONFIG_NO_EXCEPTIONS
+ void throwException() {}
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+} // namespace detail
+
+namespace {
+ using namespace detail;
+ // matching of a string against a wildcard mask (case sensitivity configurable) taken from
+ // https://www.codeproject.com/Articles/1088/Wildcard-string-compare-globbing
+ int wildcmp(const char* str, const char* wild, bool caseSensitive) {
+ const char* cp = str;
+ const char* mp = wild;
+
+ while((*str) && (*wild != '*')) {
+ if((caseSensitive ? (*wild != *str) : (tolower(*wild) != tolower(*str))) &&
+ (*wild != '?')) {
+ return 0;
+ }
+ wild++;
+ str++;
+ }
+
+ while(*str) {
+ if(*wild == '*') {
+ if(!*++wild) {
+ return 1;
+ }
+ mp = wild;
+ cp = str + 1;
+ } else if((caseSensitive ? (*wild == *str) : (tolower(*wild) == tolower(*str))) ||
+ (*wild == '?')) {
+ wild++;
+ str++;
+ } else {
+ wild = mp; //!OCLINT parameter reassignment
+ str = cp++; //!OCLINT parameter reassignment
+ }
+ }
+
+ while(*wild == '*') {
+ wild++;
+ }
+ return !*wild;
+ }
+
+ // checks if the name matches any of the filters (and can be configured what to do when empty)
+ bool matchesAny(const char* name, const std::vector<String>& filters, bool matchEmpty,
+ bool caseSensitive) {
+ if (filters.empty() && matchEmpty)
+ return true;
+ for (auto& curr : filters)
+ if (wildcmp(name, curr.c_str(), caseSensitive))
+ return true;
+ return false;
+ }
+
+ unsigned long long hash(unsigned long long a, unsigned long long b) {
+ return (a << 5) + b;
+ }
+
+ // C string hash function (djb2) - taken from http://www.cse.yorku.ca/~oz/hash.html
+ unsigned long long hash(const char* str) {
+ unsigned long long hash = 5381;
+ char c;
+ while ((c = *str++))
+ hash = ((hash << 5) + hash) + c; // hash * 33 + c
+ return hash;
+ }
+
+ unsigned long long hash(const SubcaseSignature& sig) {
+ return hash(hash(hash(sig.m_file), hash(sig.m_name.c_str())), sig.m_line);
+ }
+
+ unsigned long long hash(const std::vector<SubcaseSignature>& sigs, size_t count) {
+ unsigned long long running = 0;
+ auto end = sigs.begin() + count;
+ for (auto it = sigs.begin(); it != end; it++) {
+ running = hash(running, hash(*it));
+ }
+ return running;
+ }
+
+ unsigned long long hash(const std::vector<SubcaseSignature>& sigs) {
+ unsigned long long running = 0;
+ for (const SubcaseSignature& sig : sigs) {
+ running = hash(running, hash(sig));
+ }
+ return running;
+ }
+} // namespace
+namespace detail {
+ bool Subcase::checkFilters() {
+ if (g_cs->subcaseStack.size() < size_t(g_cs->subcase_filter_levels)) {
+ if (!matchesAny(m_signature.m_name.c_str(), g_cs->filters[6], true, g_cs->case_sensitive))
+ return true;
+ if (matchesAny(m_signature.m_name.c_str(), g_cs->filters[7], false, g_cs->case_sensitive))
+ return true;
+ }
+ return false;
+ }
+
+ Subcase::Subcase(const String& name, const char* file, int line)
+ : m_signature({name, file, line}) {
+ if (!g_cs->reachedLeaf) {
+ if (g_cs->nextSubcaseStack.size() <= g_cs->subcaseStack.size()
+ || g_cs->nextSubcaseStack[g_cs->subcaseStack.size()] == m_signature) {
+ // Going down.
+ if (checkFilters()) { return; }
+
+ g_cs->subcaseStack.push_back(m_signature);
+ g_cs->currentSubcaseDepth++;
+ m_entered = true;
+ DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature);
+ }
+ } else {
+ if (g_cs->subcaseStack[g_cs->currentSubcaseDepth] == m_signature) {
+ // This subcase is reentered via control flow.
+ g_cs->currentSubcaseDepth++;
+ m_entered = true;
+ DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_start, m_signature);
+ } else if (g_cs->nextSubcaseStack.size() <= g_cs->currentSubcaseDepth
+ && g_cs->fullyTraversedSubcases.find(hash(hash(g_cs->subcaseStack, g_cs->currentSubcaseDepth), hash(m_signature)))
+ == g_cs->fullyTraversedSubcases.end()) {
+ if (checkFilters()) { return; }
+ // This subcase is part of the one to be executed next.
+ g_cs->nextSubcaseStack.clear();
+ g_cs->nextSubcaseStack.insert(g_cs->nextSubcaseStack.end(),
+ g_cs->subcaseStack.begin(), g_cs->subcaseStack.begin() + g_cs->currentSubcaseDepth);
+ g_cs->nextSubcaseStack.push_back(m_signature);
+ }
+ }
+ }
+
+ DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17
+ DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations")
+ DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations")
+
+ Subcase::~Subcase() {
+ if (m_entered) {
+ g_cs->currentSubcaseDepth--;
+
+ if (!g_cs->reachedLeaf) {
+ // Leaf.
+ g_cs->fullyTraversedSubcases.insert(hash(g_cs->subcaseStack));
+ g_cs->nextSubcaseStack.clear();
+ g_cs->reachedLeaf = true;
+ } else if (g_cs->nextSubcaseStack.empty()) {
+ // All children are finished.
+ g_cs->fullyTraversedSubcases.insert(hash(g_cs->subcaseStack));
+ }
+
+#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200)
+ if(std::uncaught_exceptions() > 0
+#else
+ if(std::uncaught_exception()
+#endif
+ && g_cs->shouldLogCurrentException) {
+ DOCTEST_ITERATE_THROUGH_REPORTERS(
+ test_case_exception, {"exception thrown in subcase - will translate later "
+ "when the whole test case has been exited (cannot "
+ "translate while there is an active exception)",
+ false});
+ g_cs->shouldLogCurrentException = false;
+ }
+
+ DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY);
+ }
+ }
+
+ DOCTEST_CLANG_SUPPRESS_WARNING_POP
+ DOCTEST_GCC_SUPPRESS_WARNING_POP
+ DOCTEST_MSVC_SUPPRESS_WARNING_POP
+
+ Subcase::operator bool() const { return m_entered; }
+
+ Result::Result(bool passed, const String& decomposition)
+ : m_passed(passed)
+ , m_decomp(decomposition) {}
+
+ ExpressionDecomposer::ExpressionDecomposer(assertType::Enum at)
+ : m_at(at) {}
+
+ TestSuite& TestSuite::operator*(const char* in) {
+ m_test_suite = in;
+ return *this;
+ }
+
+ TestCase::TestCase(funcType test, const char* file, unsigned line, const TestSuite& test_suite,
+ const String& type, int template_id) {
+ m_file = file;
+ m_line = line;
+ m_name = nullptr; // will be later overridden in operator*
+ m_test_suite = test_suite.m_test_suite;
+ m_description = test_suite.m_description;
+ m_skip = test_suite.m_skip;
+ m_no_breaks = test_suite.m_no_breaks;
+ m_no_output = test_suite.m_no_output;
+ m_may_fail = test_suite.m_may_fail;
+ m_should_fail = test_suite.m_should_fail;
+ m_expected_failures = test_suite.m_expected_failures;
+ m_timeout = test_suite.m_timeout;
+
+ m_test = test;
+ m_type = type;
+ m_template_id = template_id;
+ }
+
+ TestCase::TestCase(const TestCase& other)
+ : TestCaseData() {
+ *this = other;
+ }
+
+ DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(26434) // hides a non-virtual function
+ TestCase& TestCase::operator=(const TestCase& other) {
+ TestCaseData::operator=(other);
+ m_test = other.m_test;
+ m_type = other.m_type;
+ m_template_id = other.m_template_id;
+ m_full_name = other.m_full_name;
+
+ if(m_template_id != -1)
+ m_name = m_full_name.c_str();
+ return *this;
+ }
+ DOCTEST_MSVC_SUPPRESS_WARNING_POP
+
+ TestCase& TestCase::operator*(const char* in) {
+ m_name = in;
+ // make a new name with an appended type for templated test case
+ if(m_template_id != -1) {
+ m_full_name = String(m_name) + "<" + m_type + ">";
+ // redirect the name to point to the newly constructed full name
+ m_name = m_full_name.c_str();
+ }
+ return *this;
+ }
+
+ bool TestCase::operator<(const TestCase& other) const {
+ // this will be used only to differentiate between test cases - not relevant for sorting
+ if(m_line != other.m_line)
+ return m_line < other.m_line;
+ const int name_cmp = strcmp(m_name, other.m_name);
+ if(name_cmp != 0)
+ return name_cmp < 0;
+ const int file_cmp = m_file.compare(other.m_file);
+ if(file_cmp != 0)
+ return file_cmp < 0;
+ return m_template_id < other.m_template_id;
+ }
+
+ // all the registered tests
+ std::set<TestCase>& getRegisteredTests() {
+ static std::set<TestCase> data;
+ return data;
+ }
+} // namespace detail
+namespace {
+ using namespace detail;
+ // for sorting tests by file/line
+ bool fileOrderComparator(const TestCase* lhs, const TestCase* rhs) {
+ // this is needed because MSVC gives different case for drive letters
+ // for __FILE__ when evaluated in a header and a source file
+ const int res = lhs->m_file.compare(rhs->m_file, bool(DOCTEST_MSVC));
+ if(res != 0)
+ return res < 0;
+ if(lhs->m_line != rhs->m_line)
+ return lhs->m_line < rhs->m_line;
+ return lhs->m_template_id < rhs->m_template_id;
+ }
+
+ // for sorting tests by suite/file/line
+ bool suiteOrderComparator(const TestCase* lhs, const TestCase* rhs) {
+ const int res = std::strcmp(lhs->m_test_suite, rhs->m_test_suite);
+ if(res != 0)
+ return res < 0;
+ return fileOrderComparator(lhs, rhs);
+ }
+
+ // for sorting tests by name/suite/file/line
+ bool nameOrderComparator(const TestCase* lhs, const TestCase* rhs) {
+ const int res = std::strcmp(lhs->m_name, rhs->m_name);
+ if(res != 0)
+ return res < 0;
+ return suiteOrderComparator(lhs, rhs);
+ }
+
+ DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations")
+ void color_to_stream(std::ostream& s, Color::Enum code) {
+ static_cast<void>(s); // for DOCTEST_CONFIG_COLORS_NONE or DOCTEST_CONFIG_COLORS_WINDOWS
+ static_cast<void>(code); // for DOCTEST_CONFIG_COLORS_NONE
+#ifdef DOCTEST_CONFIG_COLORS_ANSI
+ if(g_no_colors ||
+ (isatty(STDOUT_FILENO) == false && getContextOptions()->force_colors == false))
+ return;
+
+ auto col = "";
+ // clang-format off
+ switch(code) { //!OCLINT missing break in switch statement / unnecessary default statement in covered switch statement
+ case Color::Red: col = "[0;31m"; break;
+ case Color::Green: col = "[0;32m"; break;
+ case Color::Blue: col = "[0;34m"; break;
+ case Color::Cyan: col = "[0;36m"; break;
+ case Color::Yellow: col = "[0;33m"; break;
+ case Color::Grey: col = "[1;30m"; break;
+ case Color::LightGrey: col = "[0;37m"; break;
+ case Color::BrightRed: col = "[1;31m"; break;
+ case Color::BrightGreen: col = "[1;32m"; break;
+ case Color::BrightWhite: col = "[1;37m"; break;
+ case Color::Bright: // invalid
+ case Color::None:
+ case Color::White:
+ default: col = "[0m";
+ }
+ // clang-format on
+ s << "\033" << col;
+#endif // DOCTEST_CONFIG_COLORS_ANSI
+
+#ifdef DOCTEST_CONFIG_COLORS_WINDOWS
+ if(g_no_colors ||
+ (_isatty(_fileno(stdout)) == false && getContextOptions()->force_colors == false))
+ return;
+
+ static struct ConsoleHelper {
+ HANDLE stdoutHandle;
+ WORD origFgAttrs;
+ WORD origBgAttrs;
+
+ ConsoleHelper() {
+ stdoutHandle = GetStdHandle(STD_OUTPUT_HANDLE);
+ CONSOLE_SCREEN_BUFFER_INFO csbiInfo;
+ GetConsoleScreenBufferInfo(stdoutHandle, &csbiInfo);
+ origFgAttrs = csbiInfo.wAttributes & ~(BACKGROUND_GREEN | BACKGROUND_RED |
+ BACKGROUND_BLUE | BACKGROUND_INTENSITY);
+ origBgAttrs = csbiInfo.wAttributes & ~(FOREGROUND_GREEN | FOREGROUND_RED |
+ FOREGROUND_BLUE | FOREGROUND_INTENSITY);
+ }
+ } ch;
+
+#define DOCTEST_SET_ATTR(x) SetConsoleTextAttribute(ch.stdoutHandle, x | ch.origBgAttrs)
+
+ // clang-format off
+ switch (code) {
+ case Color::White: DOCTEST_SET_ATTR(FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break;
+ case Color::Red: DOCTEST_SET_ATTR(FOREGROUND_RED); break;
+ case Color::Green: DOCTEST_SET_ATTR(FOREGROUND_GREEN); break;
+ case Color::Blue: DOCTEST_SET_ATTR(FOREGROUND_BLUE); break;
+ case Color::Cyan: DOCTEST_SET_ATTR(FOREGROUND_BLUE | FOREGROUND_GREEN); break;
+ case Color::Yellow: DOCTEST_SET_ATTR(FOREGROUND_RED | FOREGROUND_GREEN); break;
+ case Color::Grey: DOCTEST_SET_ATTR(0); break;
+ case Color::LightGrey: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY); break;
+ case Color::BrightRed: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_RED); break;
+ case Color::BrightGreen: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN); break;
+ case Color::BrightWhite: DOCTEST_SET_ATTR(FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE); break;
+ case Color::None:
+ case Color::Bright: // invalid
+ default: DOCTEST_SET_ATTR(ch.origFgAttrs);
+ }
+ // clang-format on
+#endif // DOCTEST_CONFIG_COLORS_WINDOWS
+ }
+ DOCTEST_CLANG_SUPPRESS_WARNING_POP
+
+ std::vector<const IExceptionTranslator*>& getExceptionTranslators() {
+ static std::vector<const IExceptionTranslator*> data;
+ return data;
+ }
+
+ String translateActiveException() {
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+ String res;
+ auto& translators = getExceptionTranslators();
+ for(auto& curr : translators)
+ if(curr->translate(res))
+ return res;
+ // clang-format off
+ DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wcatch-value")
+ try {
+ throw;
+ } catch(std::exception& ex) {
+ return ex.what();
+ } catch(std::string& msg) {
+ return msg.c_str();
+ } catch(const char* msg) {
+ return msg;
+ } catch(...) {
+ return "unknown exception";
+ }
+ DOCTEST_GCC_SUPPRESS_WARNING_POP
+// clang-format on
+#else // DOCTEST_CONFIG_NO_EXCEPTIONS
+ return "";
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+ }
+} // namespace
+
+namespace detail {
+ // used by the macros for registering tests
+ int regTest(const TestCase& tc) {
+ getRegisteredTests().insert(tc);
+ return 0;
+ }
+
+ // sets the current test suite
+ int setTestSuite(const TestSuite& ts) {
+ doctest_detail_test_suite_ns::getCurrentTestSuite() = ts;
+ return 0;
+ }
+
+#ifdef DOCTEST_IS_DEBUGGER_ACTIVE
+ bool isDebuggerActive() { return DOCTEST_IS_DEBUGGER_ACTIVE(); }
+#else // DOCTEST_IS_DEBUGGER_ACTIVE
+#ifdef DOCTEST_PLATFORM_LINUX
+ class ErrnoGuard {
+ public:
+ ErrnoGuard() : m_oldErrno(errno) {}
+ ~ErrnoGuard() { errno = m_oldErrno; }
+ private:
+ int m_oldErrno;
+ };
+ // See the comments in Catch2 for the reasoning behind this implementation:
+ // https://github.com/catchorg/Catch2/blob/v2.13.1/include/internal/catch_debugger.cpp#L79-L102
+ bool isDebuggerActive() {
+ ErrnoGuard guard;
+ std::ifstream in("/proc/self/status");
+ for(std::string line; std::getline(in, line);) {
+ static const int PREFIX_LEN = 11;
+ if(line.compare(0, PREFIX_LEN, "TracerPid:\t") == 0) {
+ return line.length() > PREFIX_LEN && line[PREFIX_LEN] != '0';
+ }
+ }
+ return false;
+ }
+#elif defined(DOCTEST_PLATFORM_MAC)
+ // The following function is taken directly from the following technical note:
+ // https://developer.apple.com/library/archive/qa/qa1361/_index.html
+ // Returns true if the current process is being debugged (either
+ // running under the debugger or has a debugger attached post facto).
+ bool isDebuggerActive() {
+ int mib[4];
+ kinfo_proc info;
+ size_t size;
+ // Initialize the flags so that, if sysctl fails for some bizarre
+ // reason, we get a predictable result.
+ info.kp_proc.p_flag = 0;
+ // Initialize mib, which tells sysctl the info we want, in this case
+ // we're looking for information about a specific process ID.
+ mib[0] = CTL_KERN;
+ mib[1] = KERN_PROC;
+ mib[2] = KERN_PROC_PID;
+ mib[3] = getpid();
+ // Call sysctl.
+ size = sizeof(info);
+ if(sysctl(mib, DOCTEST_COUNTOF(mib), &info, &size, 0, 0) != 0) {
+ std::cerr << "\nCall to sysctl failed - unable to determine if debugger is active **\n";
+ return false;
+ }
+ // We're being debugged if the P_TRACED flag is set.
+ return ((info.kp_proc.p_flag & P_TRACED) != 0);
+ }
+#elif DOCTEST_MSVC || defined(__MINGW32__) || defined(__MINGW64__)
+ bool isDebuggerActive() { return ::IsDebuggerPresent() != 0; }
+#else
+ bool isDebuggerActive() { return false; }
+#endif // Platform
+#endif // DOCTEST_IS_DEBUGGER_ACTIVE
+
+ void registerExceptionTranslatorImpl(const IExceptionTranslator* et) {
+ if(std::find(getExceptionTranslators().begin(), getExceptionTranslators().end(), et) ==
+ getExceptionTranslators().end())
+ getExceptionTranslators().push_back(et);
+ }
+
+ DOCTEST_THREAD_LOCAL std::vector<IContextScope*> g_infoContexts; // for logging with INFO()
+
+ ContextScopeBase::ContextScopeBase() {
+ g_infoContexts.push_back(this);
+ }
+
+ ContextScopeBase::ContextScopeBase(ContextScopeBase&& other) noexcept {
+ if (other.need_to_destroy) {
+ other.destroy();
+ }
+ other.need_to_destroy = false;
+ g_infoContexts.push_back(this);
+ }
+
+ DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4996) // std::uncaught_exception is deprecated in C++17
+ DOCTEST_GCC_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations")
+ DOCTEST_CLANG_SUPPRESS_WARNING_WITH_PUSH("-Wdeprecated-declarations")
+
+ // destroy cannot be inlined into the destructor because that would mean calling stringify after
+ // ContextScope has been destroyed (base class destructors run after derived class destructors).
+ // Instead, ContextScope calls this method directly from its destructor.
+ void ContextScopeBase::destroy() {
+#if defined(__cpp_lib_uncaught_exceptions) && __cpp_lib_uncaught_exceptions >= 201411L && (!defined(__MAC_OS_X_VERSION_MIN_REQUIRED) || __MAC_OS_X_VERSION_MIN_REQUIRED >= 101200)
+ if(std::uncaught_exceptions() > 0) {
+#else
+ if(std::uncaught_exception()) {
+#endif
+ std::ostringstream s;
+ this->stringify(&s);
+ g_cs->stringifiedContexts.push_back(s.str().c_str());
+ }
+ g_infoContexts.pop_back();
+ }
+
+ DOCTEST_CLANG_SUPPRESS_WARNING_POP
+ DOCTEST_GCC_SUPPRESS_WARNING_POP
+ DOCTEST_MSVC_SUPPRESS_WARNING_POP
+} // namespace detail
+namespace {
+ using namespace detail;
+
+#if !defined(DOCTEST_CONFIG_POSIX_SIGNALS) && !defined(DOCTEST_CONFIG_WINDOWS_SEH)
+ struct FatalConditionHandler
+ {
+ static void reset() {}
+ static void allocateAltStackMem() {}
+ static void freeAltStackMem() {}
+ };
+#else // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH
+
+ void reportFatal(const std::string&);
+
+#ifdef DOCTEST_PLATFORM_WINDOWS
+
+ struct SignalDefs
+ {
+ DWORD id;
+ const char* name;
+ };
+ // There is no 1-1 mapping between signals and windows exceptions.
+ // Windows can easily distinguish between SO and SigSegV,
+ // but SigInt, SigTerm, etc are handled differently.
+ SignalDefs signalDefs[] = {
+ {static_cast<DWORD>(EXCEPTION_ILLEGAL_INSTRUCTION),
+ "SIGILL - Illegal instruction signal"},
+ {static_cast<DWORD>(EXCEPTION_STACK_OVERFLOW), "SIGSEGV - Stack overflow"},
+ {static_cast<DWORD>(EXCEPTION_ACCESS_VIOLATION),
+ "SIGSEGV - Segmentation violation signal"},
+ {static_cast<DWORD>(EXCEPTION_INT_DIVIDE_BY_ZERO), "Divide by zero error"},
+ };
+
+ struct FatalConditionHandler
+ {
+ static LONG CALLBACK handleException(PEXCEPTION_POINTERS ExceptionInfo) {
+ // Multiple threads may enter this filter/handler at once. We want the error message to be printed on the
+ // console just once no matter how many threads have crashed.
+ DOCTEST_DECLARE_STATIC_MUTEX(mutex)
+ static bool execute = true;
+ {
+ DOCTEST_LOCK_MUTEX(mutex)
+ if(execute) {
+ bool reported = false;
+ for(size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) {
+ if(ExceptionInfo->ExceptionRecord->ExceptionCode == signalDefs[i].id) {
+ reportFatal(signalDefs[i].name);
+ reported = true;
+ break;
+ }
+ }
+ if(reported == false)
+ reportFatal("Unhandled SEH exception caught");
+ if(isDebuggerActive() && !g_cs->no_breaks)
+ DOCTEST_BREAK_INTO_DEBUGGER();
+ }
+ execute = false;
+ }
+ std::exit(EXIT_FAILURE);
+ }
+
+ static void allocateAltStackMem() {}
+ static void freeAltStackMem() {}
+
+ FatalConditionHandler() {
+ isSet = true;
+ // 32k seems enough for doctest to handle stack overflow,
+ // but the value was found experimentally, so there is no strong guarantee
+ guaranteeSize = 32 * 1024;
+ // Register an unhandled exception filter
+ previousTop = SetUnhandledExceptionFilter(handleException);
+ // Pass in guarantee size to be filled
+ SetThreadStackGuarantee(&guaranteeSize);
+
+ // On Windows uncaught exceptions from another thread, exceptions from
+ // destructors, or calls to std::terminate are not a SEH exception
+
+ // The terminal handler gets called when:
+ // - std::terminate is called FROM THE TEST RUNNER THREAD
+ // - an exception is thrown from a destructor FROM THE TEST RUNNER THREAD
+ original_terminate_handler = std::get_terminate();
+ std::set_terminate([]() DOCTEST_NOEXCEPT {
+ reportFatal("Terminate handler called");
+ if(isDebuggerActive() && !g_cs->no_breaks)
+ DOCTEST_BREAK_INTO_DEBUGGER();
+ std::exit(EXIT_FAILURE); // explicitly exit - otherwise the SIGABRT handler may be called as well
+ });
+
+ // SIGABRT is raised when:
+ // - std::terminate is called FROM A DIFFERENT THREAD
+ // - an exception is thrown from a destructor FROM A DIFFERENT THREAD
+ // - an uncaught exception is thrown FROM A DIFFERENT THREAD
+ prev_sigabrt_handler = std::signal(SIGABRT, [](int signal) DOCTEST_NOEXCEPT {
+ if(signal == SIGABRT) {
+ reportFatal("SIGABRT - Abort (abnormal termination) signal");
+ if(isDebuggerActive() && !g_cs->no_breaks)
+ DOCTEST_BREAK_INTO_DEBUGGER();
+ std::exit(EXIT_FAILURE);
+ }
+ });
+
+ // The following settings are taken from google test, and more
+ // specifically from UnitTest::Run() inside of gtest.cc
+
+ // the user does not want to see pop-up dialogs about crashes
+ prev_error_mode_1 = SetErrorMode(SEM_FAILCRITICALERRORS | SEM_NOALIGNMENTFAULTEXCEPT |
+ SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);
+ // This forces the abort message to go to stderr in all circumstances.
+ prev_error_mode_2 = _set_error_mode(_OUT_TO_STDERR);
+ // In the debug version, Visual Studio pops up a separate dialog
+ // offering a choice to debug the aborted program - we want to disable that.
+ prev_abort_behavior = _set_abort_behavior(0x0, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
+ // In debug mode, the Windows CRT can crash with an assertion over invalid
+ // input (e.g. passing an invalid file descriptor). The default handling
+ // for these assertions is to pop up a dialog and wait for user input.
+ // Instead ask the CRT to dump such assertions to stderr non-interactively.
+ prev_report_mode = _CrtSetReportMode(_CRT_ASSERT, _CRTDBG_MODE_FILE | _CRTDBG_MODE_DEBUG);
+ prev_report_file = _CrtSetReportFile(_CRT_ASSERT, _CRTDBG_FILE_STDERR);
+ }
+
+ static void reset() {
+ if(isSet) {
+ // Unregister handler and restore the old guarantee
+ SetUnhandledExceptionFilter(previousTop);
+ SetThreadStackGuarantee(&guaranteeSize);
+ std::set_terminate(original_terminate_handler);
+ std::signal(SIGABRT, prev_sigabrt_handler);
+ SetErrorMode(prev_error_mode_1);
+ _set_error_mode(prev_error_mode_2);
+ _set_abort_behavior(prev_abort_behavior, _WRITE_ABORT_MSG | _CALL_REPORTFAULT);
+ static_cast<void>(_CrtSetReportMode(_CRT_ASSERT, prev_report_mode));
+ static_cast<void>(_CrtSetReportFile(_CRT_ASSERT, prev_report_file));
+ isSet = false;
+ }
+ }
+
+ ~FatalConditionHandler() { reset(); }
+
+ private:
+ static UINT prev_error_mode_1;
+ static int prev_error_mode_2;
+ static unsigned int prev_abort_behavior;
+ static int prev_report_mode;
+ static _HFILE prev_report_file;
+ static void (DOCTEST_CDECL *prev_sigabrt_handler)(int);
+ static std::terminate_handler original_terminate_handler;
+ static bool isSet;
+ static ULONG guaranteeSize;
+ static LPTOP_LEVEL_EXCEPTION_FILTER previousTop;
+ };
+
+ UINT FatalConditionHandler::prev_error_mode_1;
+ int FatalConditionHandler::prev_error_mode_2;
+ unsigned int FatalConditionHandler::prev_abort_behavior;
+ int FatalConditionHandler::prev_report_mode;
+ _HFILE FatalConditionHandler::prev_report_file;
+ void (DOCTEST_CDECL *FatalConditionHandler::prev_sigabrt_handler)(int);
+ std::terminate_handler FatalConditionHandler::original_terminate_handler;
+ bool FatalConditionHandler::isSet = false;
+ ULONG FatalConditionHandler::guaranteeSize = 0;
+ LPTOP_LEVEL_EXCEPTION_FILTER FatalConditionHandler::previousTop = nullptr;
+
+#else // DOCTEST_PLATFORM_WINDOWS
+
+ struct SignalDefs
+ {
+ int id;
+ const char* name;
+ };
+ SignalDefs signalDefs[] = {{SIGINT, "SIGINT - Terminal interrupt signal"},
+ {SIGILL, "SIGILL - Illegal instruction signal"},
+ {SIGFPE, "SIGFPE - Floating point error signal"},
+ {SIGSEGV, "SIGSEGV - Segmentation violation signal"},
+ {SIGTERM, "SIGTERM - Termination request signal"},
+ {SIGABRT, "SIGABRT - Abort (abnormal termination) signal"}};
+
+ struct FatalConditionHandler
+ {
+ static bool isSet;
+ static struct sigaction oldSigActions[DOCTEST_COUNTOF(signalDefs)];
+ static stack_t oldSigStack;
+ static size_t altStackSize;
+ static char* altStackMem;
+
+ static void handleSignal(int sig) {
+ const char* name = "<unknown signal>";
+ for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) {
+ SignalDefs& def = signalDefs[i];
+ if(sig == def.id) {
+ name = def.name;
+ break;
+ }
+ }
+ reset();
+ reportFatal(name);
+ raise(sig);
+ }
+
+ static void allocateAltStackMem() {
+ altStackMem = new char[altStackSize];
+ }
+
+ static void freeAltStackMem() {
+ delete[] altStackMem;
+ }
+
+ FatalConditionHandler() {
+ isSet = true;
+ stack_t sigStack;
+ sigStack.ss_sp = altStackMem;
+ sigStack.ss_size = altStackSize;
+ sigStack.ss_flags = 0;
+ sigaltstack(&sigStack, &oldSigStack);
+ struct sigaction sa = {};
+ sa.sa_handler = handleSignal;
+ sa.sa_flags = SA_ONSTACK;
+ for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) {
+ sigaction(signalDefs[i].id, &sa, &oldSigActions[i]);
+ }
+ }
+
+ ~FatalConditionHandler() { reset(); }
+ static void reset() {
+ if(isSet) {
+ // Set signals back to previous values -- hopefully nobody overwrote them in the meantime
+ for(std::size_t i = 0; i < DOCTEST_COUNTOF(signalDefs); ++i) {
+ sigaction(signalDefs[i].id, &oldSigActions[i], nullptr);
+ }
+ // Return the old stack
+ sigaltstack(&oldSigStack, nullptr);
+ isSet = false;
+ }
+ }
+ };
+
+ bool FatalConditionHandler::isSet = false;
+ struct sigaction FatalConditionHandler::oldSigActions[DOCTEST_COUNTOF(signalDefs)] = {};
+ stack_t FatalConditionHandler::oldSigStack = {};
+ size_t FatalConditionHandler::altStackSize = 4 * SIGSTKSZ;
+ char* FatalConditionHandler::altStackMem = nullptr;
+
+#endif // DOCTEST_PLATFORM_WINDOWS
+#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH
+
+} // namespace
+
+namespace {
+ using namespace detail;
+
+#ifdef DOCTEST_PLATFORM_WINDOWS
+#define DOCTEST_OUTPUT_DEBUG_STRING(text) ::OutputDebugStringA(text)
+#else
+ // TODO: integration with XCode and other IDEs
+#define DOCTEST_OUTPUT_DEBUG_STRING(text)
+#endif // Platform
+
+ void addAssert(assertType::Enum at) {
+ if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional
+ g_cs->numAssertsCurrentTest_atomic++;
+ }
+
+ void addFailedAssert(assertType::Enum at) {
+ if((at & assertType::is_warn) == 0) //!OCLINT bitwise operator in conditional
+ g_cs->numAssertsFailedCurrentTest_atomic++;
+ }
+
+#if defined(DOCTEST_CONFIG_POSIX_SIGNALS) || defined(DOCTEST_CONFIG_WINDOWS_SEH)
+ void reportFatal(const std::string& message) {
+ g_cs->failure_flags |= TestCaseFailureReason::Crash;
+
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception, {message.c_str(), true});
+
+ while (g_cs->subcaseStack.size()) {
+ g_cs->subcaseStack.pop_back();
+ DOCTEST_ITERATE_THROUGH_REPORTERS(subcase_end, DOCTEST_EMPTY);
+ }
+
+ g_cs->finalizeTestCaseData();
+
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs);
+
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs);
+ }
+#endif // DOCTEST_CONFIG_POSIX_SIGNALS || DOCTEST_CONFIG_WINDOWS_SEH
+} // namespace
+
+AssertData::AssertData(assertType::Enum at, const char* file, int line, const char* expr,
+ const char* exception_type, const StringContains& exception_string)
+ : m_test_case(g_cs->currentTest), m_at(at), m_file(file), m_line(line), m_expr(expr),
+ m_failed(true), m_threw(false), m_threw_as(false), m_exception_type(exception_type),
+ m_exception_string(exception_string) {
+#if DOCTEST_MSVC
+ if (m_expr[0] == ' ') // this happens when variadic macros are disabled under MSVC
+ ++m_expr;
+#endif // MSVC
+}
+
+namespace detail {
+ ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
+ const char* exception_type, const String& exception_string)
+ : AssertData(at, file, line, expr, exception_type, exception_string) { }
+
+ ResultBuilder::ResultBuilder(assertType::Enum at, const char* file, int line, const char* expr,
+ const char* exception_type, const Contains& exception_string)
+ : AssertData(at, file, line, expr, exception_type, exception_string) { }
+
+ void ResultBuilder::setResult(const Result& res) {
+ m_decomp = res.m_decomp;
+ m_failed = !res.m_passed;
+ }
+
+ void ResultBuilder::translateException() {
+ m_threw = true;
+ m_exception = translateActiveException();
+ }
+
+ bool ResultBuilder::log() {
+ if(m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional
+ m_failed = !m_threw;
+ } else if((m_at & assertType::is_throws_as) && (m_at & assertType::is_throws_with)) { //!OCLINT
+ m_failed = !m_threw_as || !m_exception_string.check(m_exception);
+ } else if(m_at & assertType::is_throws_as) { //!OCLINT bitwise operator in conditional
+ m_failed = !m_threw_as;
+ } else if(m_at & assertType::is_throws_with) { //!OCLINT bitwise operator in conditional
+ m_failed = !m_exception_string.check(m_exception);
+ } else if(m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional
+ m_failed = m_threw;
+ }
+
+ if(m_exception.size())
+ m_exception = "\"" + m_exception + "\"";
+
+ if(is_running_in_test) {
+ addAssert(m_at);
+ DOCTEST_ITERATE_THROUGH_REPORTERS(log_assert, *this);
+
+ if(m_failed)
+ addFailedAssert(m_at);
+ } else if(m_failed) {
+ failed_out_of_a_testing_context(*this);
+ }
+
+ return m_failed && isDebuggerActive() && !getContextOptions()->no_breaks &&
+ (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger
+ }
+
+ void ResultBuilder::react() const {
+ if(m_failed && checkIfShouldThrow(m_at))
+ throwException();
+ }
+
+ void failed_out_of_a_testing_context(const AssertData& ad) {
+ if(g_cs->ah)
+ g_cs->ah(ad);
+ else
+ std::abort();
+ }
+
+ bool decomp_assert(assertType::Enum at, const char* file, int line, const char* expr,
+ const Result& result) {
+ bool failed = !result.m_passed;
+
+ // ###################################################################################
+ // IF THE DEBUGGER BREAKS HERE - GO 1 LEVEL UP IN THE CALLSTACK FOR THE FAILING ASSERT
+ // THIS IS THE EFFECT OF HAVING 'DOCTEST_CONFIG_SUPER_FAST_ASSERTS' DEFINED
+ // ###################################################################################
+ DOCTEST_ASSERT_OUT_OF_TESTS(result.m_decomp);
+ DOCTEST_ASSERT_IN_TESTS(result.m_decomp);
+ return !failed;
+ }
+
+ MessageBuilder::MessageBuilder(const char* file, int line, assertType::Enum severity) {
+ m_stream = tlssPush();
+ m_file = file;
+ m_line = line;
+ m_severity = severity;
+ }
+
+ MessageBuilder::~MessageBuilder() {
+ if (!logged)
+ tlssPop();
+ }
+
+ DOCTEST_DEFINE_INTERFACE(IExceptionTranslator)
+
+ bool MessageBuilder::log() {
+ if (!logged) {
+ m_string = tlssPop();
+ logged = true;
+ }
+
+ DOCTEST_ITERATE_THROUGH_REPORTERS(log_message, *this);
+
+ const bool isWarn = m_severity & assertType::is_warn;
+
+ // warn is just a message in this context so we don't treat it as an assert
+ if(!isWarn) {
+ addAssert(m_severity);
+ addFailedAssert(m_severity);
+ }
+
+ return isDebuggerActive() && !getContextOptions()->no_breaks && !isWarn &&
+ (g_cs->currentTest == nullptr || !g_cs->currentTest->m_no_breaks); // break into debugger
+ }
+
+ void MessageBuilder::react() {
+ if(m_severity & assertType::is_require) //!OCLINT bitwise operator in conditional
+ throwException();
+ }
+} // namespace detail
+namespace {
+ using namespace detail;
+
+ // clang-format off
+
+// =================================================================================================
+// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp
+// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched.
+// =================================================================================================
+
+ class XmlEncode {
+ public:
+ enum ForWhat { ForTextNodes, ForAttributes };
+
+ XmlEncode( std::string const& str, ForWhat forWhat = ForTextNodes );
+
+ void encodeTo( std::ostream& os ) const;
+
+ friend std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode );
+
+ private:
+ std::string m_str;
+ ForWhat m_forWhat;
+ };
+
+ class XmlWriter {
+ public:
+
+ class ScopedElement {
+ public:
+ ScopedElement( XmlWriter* writer );
+
+ ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT;
+ ScopedElement& operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT;
+
+ ~ScopedElement();
+
+ ScopedElement& writeText( std::string const& text, bool indent = true );
+
+ template<typename T>
+ ScopedElement& writeAttribute( std::string const& name, T const& attribute ) {
+ m_writer->writeAttribute( name, attribute );
+ return *this;
+ }
+
+ private:
+ mutable XmlWriter* m_writer = nullptr;
+ };
+
+ XmlWriter( std::ostream& os = std::cout );
+ ~XmlWriter();
+
+ XmlWriter( XmlWriter const& ) = delete;
+ XmlWriter& operator=( XmlWriter const& ) = delete;
+
+ XmlWriter& startElement( std::string const& name );
+
+ ScopedElement scopedElement( std::string const& name );
+
+ XmlWriter& endElement();
+
+ XmlWriter& writeAttribute( std::string const& name, std::string const& attribute );
+
+ XmlWriter& writeAttribute( std::string const& name, const char* attribute );
+
+ XmlWriter& writeAttribute( std::string const& name, bool attribute );
+
+ template<typename T>
+ XmlWriter& writeAttribute( std::string const& name, T const& attribute ) {
+ std::stringstream rss;
+ rss << attribute;
+ return writeAttribute( name, rss.str() );
+ }
+
+ XmlWriter& writeText( std::string const& text, bool indent = true );
+
+ //XmlWriter& writeComment( std::string const& text );
+
+ //void writeStylesheetRef( std::string const& url );
+
+ //XmlWriter& writeBlankLine();
+
+ void ensureTagClosed();
+
+ void writeDeclaration();
+
+ private:
+
+ void newlineIfNecessary();
+
+ bool m_tagIsOpen = false;
+ bool m_needsNewline = false;
+ std::vector<std::string> m_tags;
+ std::string m_indent;
+ std::ostream& m_os;
+ };
+
+// =================================================================================================
+// The following code has been taken verbatim from Catch2/include/internal/catch_xmlwriter.h/cpp
+// This is done so cherry-picking bug fixes is trivial - even the style/formatting is untouched.
+// =================================================================================================
+
+using uchar = unsigned char;
+
+namespace {
+
+ size_t trailingBytes(unsigned char c) {
+ if ((c & 0xE0) == 0xC0) {
+ return 2;
+ }
+ if ((c & 0xF0) == 0xE0) {
+ return 3;
+ }
+ if ((c & 0xF8) == 0xF0) {
+ return 4;
+ }
+ DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered");
+ }
+
+ uint32_t headerValue(unsigned char c) {
+ if ((c & 0xE0) == 0xC0) {
+ return c & 0x1F;
+ }
+ if ((c & 0xF0) == 0xE0) {
+ return c & 0x0F;
+ }
+ if ((c & 0xF8) == 0xF0) {
+ return c & 0x07;
+ }
+ DOCTEST_INTERNAL_ERROR("Invalid multibyte utf-8 start byte encountered");
+ }
+
+ void hexEscapeChar(std::ostream& os, unsigned char c) {
+ std::ios_base::fmtflags f(os.flags());
+ os << "\\x"
+ << std::uppercase << std::hex << std::setfill('0') << std::setw(2)
+ << static_cast<int>(c);
+ os.flags(f);
+ }
+
+} // anonymous namespace
+
+ XmlEncode::XmlEncode( std::string const& str, ForWhat forWhat )
+ : m_str( str ),
+ m_forWhat( forWhat )
+ {}
+
+ void XmlEncode::encodeTo( std::ostream& os ) const {
+ // Apostrophe escaping not necessary if we always use " to write attributes
+ // (see: https://www.w3.org/TR/xml/#syntax)
+
+ for( std::size_t idx = 0; idx < m_str.size(); ++ idx ) {
+ uchar c = m_str[idx];
+ switch (c) {
+ case '<': os << "&lt;"; break;
+ case '&': os << "&amp;"; break;
+
+ case '>':
+ // See: https://www.w3.org/TR/xml/#syntax
+ if (idx > 2 && m_str[idx - 1] == ']' && m_str[idx - 2] == ']')
+ os << "&gt;";
+ else
+ os << c;
+ break;
+
+ case '\"':
+ if (m_forWhat == ForAttributes)
+ os << "&quot;";
+ else
+ os << c;
+ break;
+
+ default:
+ // Check for control characters and invalid utf-8
+
+ // Escape control characters in standard ascii
+ // see https://stackoverflow.com/questions/404107/why-are-control-characters-illegal-in-xml-1-0
+ if (c < 0x09 || (c > 0x0D && c < 0x20) || c == 0x7F) {
+ hexEscapeChar(os, c);
+ break;
+ }
+
+ // Plain ASCII: Write it to stream
+ if (c < 0x7F) {
+ os << c;
+ break;
+ }
+
+ // UTF-8 territory
+ // Check if the encoding is valid and if it is not, hex escape bytes.
+ // Important: We do not check the exact decoded values for validity, only the encoding format
+ // First check that this bytes is a valid lead byte:
+ // This means that it is not encoded as 1111 1XXX
+ // Or as 10XX XXXX
+ if (c < 0xC0 ||
+ c >= 0xF8) {
+ hexEscapeChar(os, c);
+ break;
+ }
+
+ auto encBytes = trailingBytes(c);
+ // Are there enough bytes left to avoid accessing out-of-bounds memory?
+ if (idx + encBytes - 1 >= m_str.size()) {
+ hexEscapeChar(os, c);
+ break;
+ }
+ // The header is valid, check data
+ // The next encBytes bytes must together be a valid utf-8
+ // This means: bitpattern 10XX XXXX and the extracted value is sane (ish)
+ bool valid = true;
+ uint32_t value = headerValue(c);
+ for (std::size_t n = 1; n < encBytes; ++n) {
+ uchar nc = m_str[idx + n];
+ valid &= ((nc & 0xC0) == 0x80);
+ value = (value << 6) | (nc & 0x3F);
+ }
+
+ if (
+ // Wrong bit pattern of following bytes
+ (!valid) ||
+ // Overlong encodings
+ (value < 0x80) ||
+ ( value < 0x800 && encBytes > 2) || // removed "0x80 <= value &&" because redundant
+ (0x800 < value && value < 0x10000 && encBytes > 3) ||
+ // Encoded value out of range
+ (value >= 0x110000)
+ ) {
+ hexEscapeChar(os, c);
+ break;
+ }
+
+ // If we got here, this is in fact a valid(ish) utf-8 sequence
+ for (std::size_t n = 0; n < encBytes; ++n) {
+ os << m_str[idx + n];
+ }
+ idx += encBytes - 1;
+ break;
+ }
+ }
+ }
+
+ std::ostream& operator << ( std::ostream& os, XmlEncode const& xmlEncode ) {
+ xmlEncode.encodeTo( os );
+ return os;
+ }
+
+ XmlWriter::ScopedElement::ScopedElement( XmlWriter* writer )
+ : m_writer( writer )
+ {}
+
+ XmlWriter::ScopedElement::ScopedElement( ScopedElement&& other ) DOCTEST_NOEXCEPT
+ : m_writer( other.m_writer ){
+ other.m_writer = nullptr;
+ }
+ XmlWriter::ScopedElement& XmlWriter::ScopedElement::operator=( ScopedElement&& other ) DOCTEST_NOEXCEPT {
+ if ( m_writer ) {
+ m_writer->endElement();
+ }
+ m_writer = other.m_writer;
+ other.m_writer = nullptr;
+ return *this;
+ }
+
+
+ XmlWriter::ScopedElement::~ScopedElement() {
+ if( m_writer )
+ m_writer->endElement();
+ }
+
+ XmlWriter::ScopedElement& XmlWriter::ScopedElement::writeText( std::string const& text, bool indent ) {
+ m_writer->writeText( text, indent );
+ return *this;
+ }
+
+ XmlWriter::XmlWriter( std::ostream& os ) : m_os( os )
+ {
+ // writeDeclaration(); // called explicitly by the reporters that use the writer class - see issue #627
+ }
+
+ XmlWriter::~XmlWriter() {
+ while( !m_tags.empty() )
+ endElement();
+ }
+
+ XmlWriter& XmlWriter::startElement( std::string const& name ) {
+ ensureTagClosed();
+ newlineIfNecessary();
+ m_os << m_indent << '<' << name;
+ m_tags.push_back( name );
+ m_indent += " ";
+ m_tagIsOpen = true;
+ return *this;
+ }
+
+ XmlWriter::ScopedElement XmlWriter::scopedElement( std::string const& name ) {
+ ScopedElement scoped( this );
+ startElement( name );
+ return scoped;
+ }
+
+ XmlWriter& XmlWriter::endElement() {
+ newlineIfNecessary();
+ m_indent = m_indent.substr( 0, m_indent.size()-2 );
+ if( m_tagIsOpen ) {
+ m_os << "/>";
+ m_tagIsOpen = false;
+ }
+ else {
+ m_os << m_indent << "</" << m_tags.back() << ">";
+ }
+ m_os << std::endl;
+ m_tags.pop_back();
+ return *this;
+ }
+
+ XmlWriter& XmlWriter::writeAttribute( std::string const& name, std::string const& attribute ) {
+ if( !name.empty() && !attribute.empty() )
+ m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"';
+ return *this;
+ }
+
+ XmlWriter& XmlWriter::writeAttribute( std::string const& name, const char* attribute ) {
+ if( !name.empty() && attribute && attribute[0] != '\0' )
+ m_os << ' ' << name << "=\"" << XmlEncode( attribute, XmlEncode::ForAttributes ) << '"';
+ return *this;
+ }
+
+ XmlWriter& XmlWriter::writeAttribute( std::string const& name, bool attribute ) {
+ m_os << ' ' << name << "=\"" << ( attribute ? "true" : "false" ) << '"';
+ return *this;
+ }
+
+ XmlWriter& XmlWriter::writeText( std::string const& text, bool indent ) {
+ if( !text.empty() ){
+ bool tagWasOpen = m_tagIsOpen;
+ ensureTagClosed();
+ if( tagWasOpen && indent )
+ m_os << m_indent;
+ m_os << XmlEncode( text );
+ m_needsNewline = true;
+ }
+ return *this;
+ }
+
+ //XmlWriter& XmlWriter::writeComment( std::string const& text ) {
+ // ensureTagClosed();
+ // m_os << m_indent << "<!--" << text << "-->";
+ // m_needsNewline = true;
+ // return *this;
+ //}
+
+ //void XmlWriter::writeStylesheetRef( std::string const& url ) {
+ // m_os << "<?xml-stylesheet type=\"text/xsl\" href=\"" << url << "\"?>\n";
+ //}
+
+ //XmlWriter& XmlWriter::writeBlankLine() {
+ // ensureTagClosed();
+ // m_os << '\n';
+ // return *this;
+ //}
+
+ void XmlWriter::ensureTagClosed() {
+ if( m_tagIsOpen ) {
+ m_os << ">" << std::endl;
+ m_tagIsOpen = false;
+ }
+ }
+
+ void XmlWriter::writeDeclaration() {
+ m_os << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
+ }
+
+ void XmlWriter::newlineIfNecessary() {
+ if( m_needsNewline ) {
+ m_os << std::endl;
+ m_needsNewline = false;
+ }
+ }
+
+// =================================================================================================
+// End of copy-pasted code from Catch
+// =================================================================================================
+
+ // clang-format on
+
+ struct XmlReporter : public IReporter
+ {
+ XmlWriter xml;
+ DOCTEST_DECLARE_MUTEX(mutex)
+
+ // caching pointers/references to objects of these types - safe to do
+ const ContextOptions& opt;
+ const TestCaseData* tc = nullptr;
+
+ XmlReporter(const ContextOptions& co)
+ : xml(*co.cout)
+ , opt(co) {}
+
+ void log_contexts() {
+ int num_contexts = get_num_active_contexts();
+ if(num_contexts) {
+ auto contexts = get_active_contexts();
+ std::stringstream ss;
+ for(int i = 0; i < num_contexts; ++i) {
+ contexts[i]->stringify(&ss);
+ xml.scopedElement("Info").writeText(ss.str());
+ ss.str("");
+ }
+ }
+ }
+
+ unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; }
+
+ void test_case_start_impl(const TestCaseData& in) {
+ bool open_ts_tag = false;
+ if(tc != nullptr) { // we have already opened a test suite
+ if(std::strcmp(tc->m_test_suite, in.m_test_suite) != 0) {
+ xml.endElement();
+ open_ts_tag = true;
+ }
+ }
+ else {
+ open_ts_tag = true; // first test case ==> first test suite
+ }
+
+ if(open_ts_tag) {
+ xml.startElement("TestSuite");
+ xml.writeAttribute("name", in.m_test_suite);
+ }
+
+ tc = &in;
+ xml.startElement("TestCase")
+ .writeAttribute("name", in.m_name)
+ .writeAttribute("filename", skipPathFromFilename(in.m_file.c_str()))
+ .writeAttribute("line", line(in.m_line))
+ .writeAttribute("description", in.m_description);
+
+ if(Approx(in.m_timeout) != 0)
+ xml.writeAttribute("timeout", in.m_timeout);
+ if(in.m_may_fail)
+ xml.writeAttribute("may_fail", true);
+ if(in.m_should_fail)
+ xml.writeAttribute("should_fail", true);
+ }
+
+ // =========================================================================================
+ // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE
+ // =========================================================================================
+
+ void report_query(const QueryData& in) override {
+ test_run_start();
+ if(opt.list_reporters) {
+ for(auto& curr : getListeners())
+ xml.scopedElement("Listener")
+ .writeAttribute("priority", curr.first.first)
+ .writeAttribute("name", curr.first.second);
+ for(auto& curr : getReporters())
+ xml.scopedElement("Reporter")
+ .writeAttribute("priority", curr.first.first)
+ .writeAttribute("name", curr.first.second);
+ } else if(opt.count || opt.list_test_cases) {
+ for(unsigned i = 0; i < in.num_data; ++i) {
+ xml.scopedElement("TestCase").writeAttribute("name", in.data[i]->m_name)
+ .writeAttribute("testsuite", in.data[i]->m_test_suite)
+ .writeAttribute("filename", skipPathFromFilename(in.data[i]->m_file.c_str()))
+ .writeAttribute("line", line(in.data[i]->m_line))
+ .writeAttribute("skipped", in.data[i]->m_skip);
+ }
+ xml.scopedElement("OverallResultsTestCases")
+ .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters);
+ } else if(opt.list_test_suites) {
+ for(unsigned i = 0; i < in.num_data; ++i)
+ xml.scopedElement("TestSuite").writeAttribute("name", in.data[i]->m_test_suite);
+ xml.scopedElement("OverallResultsTestCases")
+ .writeAttribute("unskipped", in.run_stats->numTestCasesPassingFilters);
+ xml.scopedElement("OverallResultsTestSuites")
+ .writeAttribute("unskipped", in.run_stats->numTestSuitesPassingFilters);
+ }
+ xml.endElement();
+ }
+
+ void test_run_start() override {
+ xml.writeDeclaration();
+
+ // remove .exe extension - mainly to have the same output on UNIX and Windows
+ std::string binary_name = skipPathFromFilename(opt.binary_name.c_str());
+#ifdef DOCTEST_PLATFORM_WINDOWS
+ if(binary_name.rfind(".exe") != std::string::npos)
+ binary_name = binary_name.substr(0, binary_name.length() - 4);
+#endif // DOCTEST_PLATFORM_WINDOWS
+
+ xml.startElement("doctest").writeAttribute("binary", binary_name);
+ if(opt.no_version == false)
+ xml.writeAttribute("version", DOCTEST_VERSION_STR);
+
+ // only the consequential ones (TODO: filters)
+ xml.scopedElement("Options")
+ .writeAttribute("order_by", opt.order_by.c_str())
+ .writeAttribute("rand_seed", opt.rand_seed)
+ .writeAttribute("first", opt.first)
+ .writeAttribute("last", opt.last)
+ .writeAttribute("abort_after", opt.abort_after)
+ .writeAttribute("subcase_filter_levels", opt.subcase_filter_levels)
+ .writeAttribute("case_sensitive", opt.case_sensitive)
+ .writeAttribute("no_throw", opt.no_throw)
+ .writeAttribute("no_skip", opt.no_skip);
+ }
+
+ void test_run_end(const TestRunStats& p) override {
+ if(tc) // the TestSuite tag - only if there has been at least 1 test case
+ xml.endElement();
+
+ xml.scopedElement("OverallResultsAsserts")
+ .writeAttribute("successes", p.numAsserts - p.numAssertsFailed)
+ .writeAttribute("failures", p.numAssertsFailed);
+
+ xml.startElement("OverallResultsTestCases")
+ .writeAttribute("successes",
+ p.numTestCasesPassingFilters - p.numTestCasesFailed)
+ .writeAttribute("failures", p.numTestCasesFailed);
+ if(opt.no_skipped_summary == false)
+ xml.writeAttribute("skipped", p.numTestCases - p.numTestCasesPassingFilters);
+ xml.endElement();
+
+ xml.endElement();
+ }
+
+ void test_case_start(const TestCaseData& in) override {
+ test_case_start_impl(in);
+ xml.ensureTagClosed();
+ }
+
+ void test_case_reenter(const TestCaseData&) override {}
+
+ void test_case_end(const CurrentTestCaseStats& st) override {
+ xml.startElement("OverallResultsAsserts")
+ .writeAttribute("successes",
+ st.numAssertsCurrentTest - st.numAssertsFailedCurrentTest)
+ .writeAttribute("failures", st.numAssertsFailedCurrentTest)
+ .writeAttribute("test_case_success", st.testCaseSuccess);
+ if(opt.duration)
+ xml.writeAttribute("duration", st.seconds);
+ if(tc->m_expected_failures)
+ xml.writeAttribute("expected_failures", tc->m_expected_failures);
+ xml.endElement();
+
+ xml.endElement();
+ }
+
+ void test_case_exception(const TestCaseException& e) override {
+ DOCTEST_LOCK_MUTEX(mutex)
+
+ xml.scopedElement("Exception")
+ .writeAttribute("crash", e.is_crash)
+ .writeText(e.error_string.c_str());
+ }
+
+ void subcase_start(const SubcaseSignature& in) override {
+ xml.startElement("SubCase")
+ .writeAttribute("name", in.m_name)
+ .writeAttribute("filename", skipPathFromFilename(in.m_file))
+ .writeAttribute("line", line(in.m_line));
+ xml.ensureTagClosed();
+ }
+
+ void subcase_end() override { xml.endElement(); }
+
+ void log_assert(const AssertData& rb) override {
+ if(!rb.m_failed && !opt.success)
+ return;
+
+ DOCTEST_LOCK_MUTEX(mutex)
+
+ xml.startElement("Expression")
+ .writeAttribute("success", !rb.m_failed)
+ .writeAttribute("type", assertString(rb.m_at))
+ .writeAttribute("filename", skipPathFromFilename(rb.m_file))
+ .writeAttribute("line", line(rb.m_line));
+
+ xml.scopedElement("Original").writeText(rb.m_expr);
+
+ if(rb.m_threw)
+ xml.scopedElement("Exception").writeText(rb.m_exception.c_str());
+
+ if(rb.m_at & assertType::is_throws_as)
+ xml.scopedElement("ExpectedException").writeText(rb.m_exception_type);
+ if(rb.m_at & assertType::is_throws_with)
+ xml.scopedElement("ExpectedExceptionString").writeText(rb.m_exception_string.c_str());
+ if((rb.m_at & assertType::is_normal) && !rb.m_threw)
+ xml.scopedElement("Expanded").writeText(rb.m_decomp.c_str());
+
+ log_contexts();
+
+ xml.endElement();
+ }
+
+ void log_message(const MessageData& mb) override {
+ DOCTEST_LOCK_MUTEX(mutex)
+
+ xml.startElement("Message")
+ .writeAttribute("type", failureString(mb.m_severity))
+ .writeAttribute("filename", skipPathFromFilename(mb.m_file))
+ .writeAttribute("line", line(mb.m_line));
+
+ xml.scopedElement("Text").writeText(mb.m_string.c_str());
+
+ log_contexts();
+
+ xml.endElement();
+ }
+
+ void test_case_skipped(const TestCaseData& in) override {
+ if(opt.no_skipped_summary == false) {
+ test_case_start_impl(in);
+ xml.writeAttribute("skipped", "true");
+ xml.endElement();
+ }
+ }
+ };
+
+ DOCTEST_REGISTER_REPORTER("xml", 0, XmlReporter);
+
+ void fulltext_log_assert_to_stream(std::ostream& s, const AssertData& rb) {
+ if((rb.m_at & (assertType::is_throws_as | assertType::is_throws_with)) ==
+ 0) //!OCLINT bitwise operator in conditional
+ s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << " ) "
+ << Color::None;
+
+ if(rb.m_at & assertType::is_throws) { //!OCLINT bitwise operator in conditional
+ s << (rb.m_threw ? "threw as expected!" : "did NOT throw at all!") << "\n";
+ } else if((rb.m_at & assertType::is_throws_as) &&
+ (rb.m_at & assertType::is_throws_with)) { //!OCLINT
+ s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \""
+ << rb.m_exception_string.c_str()
+ << "\", " << rb.m_exception_type << " ) " << Color::None;
+ if(rb.m_threw) {
+ if(!rb.m_failed) {
+ s << "threw as expected!\n";
+ } else {
+ s << "threw a DIFFERENT exception! (contents: " << rb.m_exception << ")\n";
+ }
+ } else {
+ s << "did NOT throw at all!\n";
+ }
+ } else if(rb.m_at &
+ assertType::is_throws_as) { //!OCLINT bitwise operator in conditional
+ s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", "
+ << rb.m_exception_type << " ) " << Color::None
+ << (rb.m_threw ? (rb.m_threw_as ? "threw as expected!" :
+ "threw a DIFFERENT exception: ") :
+ "did NOT throw at all!")
+ << Color::Cyan << rb.m_exception << "\n";
+ } else if(rb.m_at &
+ assertType::is_throws_with) { //!OCLINT bitwise operator in conditional
+ s << Color::Cyan << assertString(rb.m_at) << "( " << rb.m_expr << ", \""
+ << rb.m_exception_string.c_str()
+ << "\" ) " << Color::None
+ << (rb.m_threw ? (!rb.m_failed ? "threw as expected!" :
+ "threw a DIFFERENT exception: ") :
+ "did NOT throw at all!")
+ << Color::Cyan << rb.m_exception << "\n";
+ } else if(rb.m_at & assertType::is_nothrow) { //!OCLINT bitwise operator in conditional
+ s << (rb.m_threw ? "THREW exception: " : "didn't throw!") << Color::Cyan
+ << rb.m_exception << "\n";
+ } else {
+ s << (rb.m_threw ? "THREW exception: " :
+ (!rb.m_failed ? "is correct!\n" : "is NOT correct!\n"));
+ if(rb.m_threw)
+ s << rb.m_exception << "\n";
+ else
+ s << " values: " << assertString(rb.m_at) << "( " << rb.m_decomp << " )\n";
+ }
+ }
+
+ // TODO:
+ // - log_message()
+ // - respond to queries
+ // - honor remaining options
+ // - more attributes in tags
+ struct JUnitReporter : public IReporter
+ {
+ XmlWriter xml;
+ DOCTEST_DECLARE_MUTEX(mutex)
+ Timer timer;
+ std::vector<String> deepestSubcaseStackNames;
+
+ struct JUnitTestCaseData
+ {
+ static std::string getCurrentTimestamp() {
+ // Beware, this is not reentrant because of backward compatibility issues
+ // Also, UTC only, again because of backward compatibility (%z is C++11)
+ time_t rawtime;
+ std::time(&rawtime);
+ auto const timeStampSize = sizeof("2017-01-16T17:06:45Z");
+
+ std::tm timeInfo;
+#ifdef DOCTEST_PLATFORM_WINDOWS
+ gmtime_s(&timeInfo, &rawtime);
+#else // DOCTEST_PLATFORM_WINDOWS
+ gmtime_r(&rawtime, &timeInfo);
+#endif // DOCTEST_PLATFORM_WINDOWS
+
+ char timeStamp[timeStampSize];
+ const char* const fmt = "%Y-%m-%dT%H:%M:%SZ";
+
+ std::strftime(timeStamp, timeStampSize, fmt, &timeInfo);
+ return std::string(timeStamp);
+ }
+
+ struct JUnitTestMessage
+ {
+ JUnitTestMessage(const std::string& _message, const std::string& _type, const std::string& _details)
+ : message(_message), type(_type), details(_details) {}
+
+ JUnitTestMessage(const std::string& _message, const std::string& _details)
+ : message(_message), type(), details(_details) {}
+
+ std::string message, type, details;
+ };
+
+ struct JUnitTestCase
+ {
+ JUnitTestCase(const std::string& _classname, const std::string& _name)
+ : classname(_classname), name(_name), time(0), failures() {}
+
+ std::string classname, name;
+ double time;
+ std::vector<JUnitTestMessage> failures, errors;
+ };
+
+ void add(const std::string& classname, const std::string& name) {
+ testcases.emplace_back(classname, name);
+ }
+
+ void appendSubcaseNamesToLastTestcase(std::vector<String> nameStack) {
+ for(auto& curr: nameStack)
+ if(curr.size())
+ testcases.back().name += std::string("/") + curr.c_str();
+ }
+
+ void addTime(double time) {
+ if(time < 1e-4)
+ time = 0;
+ testcases.back().time = time;
+ totalSeconds += time;
+ }
+
+ void addFailure(const std::string& message, const std::string& type, const std::string& details) {
+ testcases.back().failures.emplace_back(message, type, details);
+ ++totalFailures;
+ }
+
+ void addError(const std::string& message, const std::string& details) {
+ testcases.back().errors.emplace_back(message, details);
+ ++totalErrors;
+ }
+
+ std::vector<JUnitTestCase> testcases;
+ double totalSeconds = 0;
+ int totalErrors = 0, totalFailures = 0;
+ };
+
+ JUnitTestCaseData testCaseData;
+
+ // caching pointers/references to objects of these types - safe to do
+ const ContextOptions& opt;
+ const TestCaseData* tc = nullptr;
+
+ JUnitReporter(const ContextOptions& co)
+ : xml(*co.cout)
+ , opt(co) {}
+
+ unsigned line(unsigned l) const { return opt.no_line_numbers ? 0 : l; }
+
+ // =========================================================================================
+ // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE
+ // =========================================================================================
+
+ void report_query(const QueryData&) override {
+ xml.writeDeclaration();
+ }
+
+ void test_run_start() override {
+ xml.writeDeclaration();
+ }
+
+ void test_run_end(const TestRunStats& p) override {
+ // remove .exe extension - mainly to have the same output on UNIX and Windows
+ std::string binary_name = skipPathFromFilename(opt.binary_name.c_str());
+#ifdef DOCTEST_PLATFORM_WINDOWS
+ if(binary_name.rfind(".exe") != std::string::npos)
+ binary_name = binary_name.substr(0, binary_name.length() - 4);
+#endif // DOCTEST_PLATFORM_WINDOWS
+ xml.startElement("testsuites");
+ xml.startElement("testsuite").writeAttribute("name", binary_name)
+ .writeAttribute("errors", testCaseData.totalErrors)
+ .writeAttribute("failures", testCaseData.totalFailures)
+ .writeAttribute("tests", p.numAsserts);
+ if(opt.no_time_in_output == false) {
+ xml.writeAttribute("time", testCaseData.totalSeconds);
+ xml.writeAttribute("timestamp", JUnitTestCaseData::getCurrentTimestamp());
+ }
+ if(opt.no_version == false)
+ xml.writeAttribute("doctest_version", DOCTEST_VERSION_STR);
+
+ for(const auto& testCase : testCaseData.testcases) {
+ xml.startElement("testcase")
+ .writeAttribute("classname", testCase.classname)
+ .writeAttribute("name", testCase.name);
+ if(opt.no_time_in_output == false)
+ xml.writeAttribute("time", testCase.time);
+ // This is not ideal, but it should be enough to mimic gtest's junit output.
+ xml.writeAttribute("status", "run");
+
+ for(const auto& failure : testCase.failures) {
+ xml.scopedElement("failure")
+ .writeAttribute("message", failure.message)
+ .writeAttribute("type", failure.type)
+ .writeText(failure.details, false);
+ }
+
+ for(const auto& error : testCase.errors) {
+ xml.scopedElement("error")
+ .writeAttribute("message", error.message)
+ .writeText(error.details);
+ }
+
+ xml.endElement();
+ }
+ xml.endElement();
+ xml.endElement();
+ }
+
+ void test_case_start(const TestCaseData& in) override {
+ testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name);
+ timer.start();
+ }
+
+ void test_case_reenter(const TestCaseData& in) override {
+ testCaseData.addTime(timer.getElapsedSeconds());
+ testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames);
+ deepestSubcaseStackNames.clear();
+
+ timer.start();
+ testCaseData.add(skipPathFromFilename(in.m_file.c_str()), in.m_name);
+ }
+
+ void test_case_end(const CurrentTestCaseStats&) override {
+ testCaseData.addTime(timer.getElapsedSeconds());
+ testCaseData.appendSubcaseNamesToLastTestcase(deepestSubcaseStackNames);
+ deepestSubcaseStackNames.clear();
+ }
+
+ void test_case_exception(const TestCaseException& e) override {
+ DOCTEST_LOCK_MUTEX(mutex)
+ testCaseData.addError("exception", e.error_string.c_str());
+ }
+
+ void subcase_start(const SubcaseSignature& in) override {
+ deepestSubcaseStackNames.push_back(in.m_name);
+ }
+
+ void subcase_end() override {}
+
+ void log_assert(const AssertData& rb) override {
+ if(!rb.m_failed) // report only failures & ignore the `success` option
+ return;
+
+ DOCTEST_LOCK_MUTEX(mutex)
+
+ std::ostringstream os;
+ os << skipPathFromFilename(rb.m_file) << (opt.gnu_file_line ? ":" : "(")
+ << line(rb.m_line) << (opt.gnu_file_line ? ":" : "):") << std::endl;
+
+ fulltext_log_assert_to_stream(os, rb);
+ log_contexts(os);
+ testCaseData.addFailure(rb.m_decomp.c_str(), assertString(rb.m_at), os.str());
+ }
+
+ void log_message(const MessageData&) override {}
+
+ void test_case_skipped(const TestCaseData&) override {}
+
+ void log_contexts(std::ostringstream& s) {
+ int num_contexts = get_num_active_contexts();
+ if(num_contexts) {
+ auto contexts = get_active_contexts();
+
+ s << " logged: ";
+ for(int i = 0; i < num_contexts; ++i) {
+ s << (i == 0 ? "" : " ");
+ contexts[i]->stringify(&s);
+ s << std::endl;
+ }
+ }
+ }
+ };
+
+ DOCTEST_REGISTER_REPORTER("junit", 0, JUnitReporter);
+
+ struct Whitespace
+ {
+ int nrSpaces;
+ explicit Whitespace(int nr)
+ : nrSpaces(nr) {}
+ };
+
+ std::ostream& operator<<(std::ostream& out, const Whitespace& ws) {
+ if(ws.nrSpaces != 0)
+ out << std::setw(ws.nrSpaces) << ' ';
+ return out;
+ }
+
+ struct ConsoleReporter : public IReporter
+ {
+ std::ostream& s;
+ bool hasLoggedCurrentTestStart;
+ std::vector<SubcaseSignature> subcasesStack;
+ size_t currentSubcaseLevel;
+ DOCTEST_DECLARE_MUTEX(mutex)
+
+ // caching pointers/references to objects of these types - safe to do
+ const ContextOptions& opt;
+ const TestCaseData* tc;
+
+ ConsoleReporter(const ContextOptions& co)
+ : s(*co.cout)
+ , opt(co) {}
+
+ ConsoleReporter(const ContextOptions& co, std::ostream& ostr)
+ : s(ostr)
+ , opt(co) {}
+
+ // =========================================================================================
+ // WHAT FOLLOWS ARE HELPERS USED BY THE OVERRIDES OF THE VIRTUAL METHODS OF THE INTERFACE
+ // =========================================================================================
+
+ void separator_to_stream() {
+ s << Color::Yellow
+ << "==============================================================================="
+ "\n";
+ }
+
+ const char* getSuccessOrFailString(bool success, assertType::Enum at,
+ const char* success_str) {
+ if(success)
+ return success_str;
+ return failureString(at);
+ }
+
+ Color::Enum getSuccessOrFailColor(bool success, assertType::Enum at) {
+ return success ? Color::BrightGreen :
+ (at & assertType::is_warn) ? Color::Yellow : Color::Red;
+ }
+
+ void successOrFailColoredStringToStream(bool success, assertType::Enum at,
+ const char* success_str = "SUCCESS") {
+ s << getSuccessOrFailColor(success, at)
+ << getSuccessOrFailString(success, at, success_str) << ": ";
+ }
+
+ void log_contexts() {
+ int num_contexts = get_num_active_contexts();
+ if(num_contexts) {
+ auto contexts = get_active_contexts();
+
+ s << Color::None << " logged: ";
+ for(int i = 0; i < num_contexts; ++i) {
+ s << (i == 0 ? "" : " ");
+ contexts[i]->stringify(&s);
+ s << "\n";
+ }
+ }
+
+ s << "\n";
+ }
+
+ // this was requested to be made virtual so users could override it
+ virtual void file_line_to_stream(const char* file, int line,
+ const char* tail = "") {
+ s << Color::LightGrey << skipPathFromFilename(file) << (opt.gnu_file_line ? ":" : "(")
+ << (opt.no_line_numbers ? 0 : line) // 0 or the real num depending on the option
+ << (opt.gnu_file_line ? ":" : "):") << tail;
+ }
+
+ void logTestStart() {
+ if(hasLoggedCurrentTestStart)
+ return;
+
+ separator_to_stream();
+ file_line_to_stream(tc->m_file.c_str(), tc->m_line, "\n");
+ if(tc->m_description)
+ s << Color::Yellow << "DESCRIPTION: " << Color::None << tc->m_description << "\n";
+ if(tc->m_test_suite && tc->m_test_suite[0] != '\0')
+ s << Color::Yellow << "TEST SUITE: " << Color::None << tc->m_test_suite << "\n";
+ if(strncmp(tc->m_name, " Scenario:", 11) != 0)
+ s << Color::Yellow << "TEST CASE: ";
+ s << Color::None << tc->m_name << "\n";
+
+ for(size_t i = 0; i < currentSubcaseLevel; ++i) {
+ if(subcasesStack[i].m_name[0] != '\0')
+ s << " " << subcasesStack[i].m_name << "\n";
+ }
+
+ if(currentSubcaseLevel != subcasesStack.size()) {
+ s << Color::Yellow << "\nDEEPEST SUBCASE STACK REACHED (DIFFERENT FROM THE CURRENT ONE):\n" << Color::None;
+ for(size_t i = 0; i < subcasesStack.size(); ++i) {
+ if(subcasesStack[i].m_name[0] != '\0')
+ s << " " << subcasesStack[i].m_name << "\n";
+ }
+ }
+
+ s << "\n";
+
+ hasLoggedCurrentTestStart = true;
+ }
+
+ void printVersion() {
+ if(opt.no_version == false)
+ s << Color::Cyan << "[doctest] " << Color::None << "doctest version is \""
+ << DOCTEST_VERSION_STR << "\"\n";
+ }
+
+ void printIntro() {
+ if(opt.no_intro == false) {
+ printVersion();
+ s << Color::Cyan << "[doctest] " << Color::None
+ << "run with \"--" DOCTEST_OPTIONS_PREFIX_DISPLAY "help\" for options\n";
+ }
+ }
+
+ void printHelp() {
+ int sizePrefixDisplay = static_cast<int>(strlen(DOCTEST_OPTIONS_PREFIX_DISPLAY));
+ printVersion();
+ // clang-format off
+ s << Color::Cyan << "[doctest]\n" << Color::None;
+ s << Color::Cyan << "[doctest] " << Color::None;
+ s << "boolean values: \"1/on/yes/true\" or \"0/off/no/false\"\n";
+ s << Color::Cyan << "[doctest] " << Color::None;
+ s << "filter values: \"str1,str2,str3\" (comma separated strings)\n";
+ s << Color::Cyan << "[doctest]\n" << Color::None;
+ s << Color::Cyan << "[doctest] " << Color::None;
+ s << "filters use wildcards for matching strings\n";
+ s << Color::Cyan << "[doctest] " << Color::None;
+ s << "something passes a filter if any of the strings in a filter matches\n";
+#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS
+ s << Color::Cyan << "[doctest]\n" << Color::None;
+ s << Color::Cyan << "[doctest] " << Color::None;
+ s << "ALL FLAGS, OPTIONS AND FILTERS ALSO AVAILABLE WITH A \"" DOCTEST_CONFIG_OPTIONS_PREFIX "\" PREFIX!!!\n";
+#endif
+ s << Color::Cyan << "[doctest]\n" << Color::None;
+ s << Color::Cyan << "[doctest] " << Color::None;
+ s << "Query flags - the program quits after them. Available:\n\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "?, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "help, -" DOCTEST_OPTIONS_PREFIX_DISPLAY "h "
+ << Whitespace(sizePrefixDisplay*0) << "prints this message\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "v, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "version "
+ << Whitespace(sizePrefixDisplay*1) << "prints the version\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "c, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "count "
+ << Whitespace(sizePrefixDisplay*1) << "prints the number of matching tests\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ltc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-cases "
+ << Whitespace(sizePrefixDisplay*1) << "lists all matching tests by name\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-test-suites "
+ << Whitespace(sizePrefixDisplay*1) << "lists all matching test suites\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "lr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "list-reporters "
+ << Whitespace(sizePrefixDisplay*1) << "lists all registered reporters\n\n";
+ // ================================================================================== << 79
+ s << Color::Cyan << "[doctest] " << Color::None;
+ s << "The available <int>/<string> options/filters are:\n\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case=<filters> "
+ << Whitespace(sizePrefixDisplay*1) << "filters tests by their name\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-case-exclude=<filters> "
+ << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their name\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file=<filters> "
+ << Whitespace(sizePrefixDisplay*1) << "filters tests by their file\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sfe, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "source-file-exclude=<filters> "
+ << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their file\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ts, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite=<filters> "
+ << Whitespace(sizePrefixDisplay*1) << "filters tests by their test suite\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "tse, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "test-suite-exclude=<filters> "
+ << Whitespace(sizePrefixDisplay*1) << "filters OUT tests by their test suite\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase=<filters> "
+ << Whitespace(sizePrefixDisplay*1) << "filters subcases by their name\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "sce, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-exclude=<filters> "
+ << Whitespace(sizePrefixDisplay*1) << "filters OUT subcases by their name\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "r, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "reporters=<filters> "
+ << Whitespace(sizePrefixDisplay*1) << "reporters to use (console is default)\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "o, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "out=<string> "
+ << Whitespace(sizePrefixDisplay*1) << "output filename\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ob, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "order-by=<string> "
+ << Whitespace(sizePrefixDisplay*1) << "how the tests should be ordered\n";
+ s << Whitespace(sizePrefixDisplay*3) << " <string> - [file/suite/name/rand/none]\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "rs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "rand-seed=<int> "
+ << Whitespace(sizePrefixDisplay*1) << "seed for random ordering\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "f, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "first=<int> "
+ << Whitespace(sizePrefixDisplay*1) << "the first test passing the filters to\n";
+ s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "l, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "last=<int> "
+ << Whitespace(sizePrefixDisplay*1) << "the last test passing the filters to\n";
+ s << Whitespace(sizePrefixDisplay*3) << " execute - for range-based execution\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "aa, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "abort-after=<int> "
+ << Whitespace(sizePrefixDisplay*1) << "stop after <int> failed assertions\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "scfl,--" DOCTEST_OPTIONS_PREFIX_DISPLAY "subcase-filter-levels=<int> "
+ << Whitespace(sizePrefixDisplay*1) << "apply filters for the first <int> levels\n";
+ s << Color::Cyan << "\n[doctest] " << Color::None;
+ s << "Bool options - can be used like flags and true is assumed. Available:\n\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "s, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "success=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "include successful assertions in output\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "cs, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "case-sensitive=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "filters being treated as case sensitive\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "e, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "exit=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "exits after the tests finish\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "d, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "duration=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "prints the time duration of each test\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "m, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "minimal=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "minimal console output (only failures)\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "q, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "quiet=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "no console output\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nt, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-throw=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "skips exceptions-related assert checks\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ne, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-exitcode=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "returns (or exits) always with success\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nr, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-run=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "skips all runtime doctest operations\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ni, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-intro=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "omit the framework intro in the output\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nv, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-version=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "omit the framework version in the output\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-colors=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "disables colors in output\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "fc, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "force-colors=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "use colors even when not in a tty\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nb, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-breaks=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "disables breakpoints in debuggers\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "ns, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-skip=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "don't skip test cases marked as skip\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "gfl, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "gnu-file-line=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << ":n: vs (n): for line numbers in output\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "npf, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-path-filenames=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "only filenames and no paths in output\n";
+ s << " -" DOCTEST_OPTIONS_PREFIX_DISPLAY "nln, --" DOCTEST_OPTIONS_PREFIX_DISPLAY "no-line-numbers=<bool> "
+ << Whitespace(sizePrefixDisplay*1) << "0 instead of real line numbers in output\n";
+ // ================================================================================== << 79
+ // clang-format on
+
+ s << Color::Cyan << "\n[doctest] " << Color::None;
+ s << "for more information visit the project documentation\n\n";
+ }
+
+ void printRegisteredReporters() {
+ printVersion();
+ auto printReporters = [this] (const reporterMap& reporters, const char* type) {
+ if(reporters.size()) {
+ s << Color::Cyan << "[doctest] " << Color::None << "listing all registered " << type << "\n";
+ for(auto& curr : reporters)
+ s << "priority: " << std::setw(5) << curr.first.first
+ << " name: " << curr.first.second << "\n";
+ }
+ };
+ printReporters(getListeners(), "listeners");
+ printReporters(getReporters(), "reporters");
+ }
+
+ // =========================================================================================
+ // WHAT FOLLOWS ARE OVERRIDES OF THE VIRTUAL METHODS OF THE REPORTER INTERFACE
+ // =========================================================================================
+
+ void report_query(const QueryData& in) override {
+ if(opt.version) {
+ printVersion();
+ } else if(opt.help) {
+ printHelp();
+ } else if(opt.list_reporters) {
+ printRegisteredReporters();
+ } else if(opt.count || opt.list_test_cases) {
+ if(opt.list_test_cases) {
+ s << Color::Cyan << "[doctest] " << Color::None
+ << "listing all test case names\n";
+ separator_to_stream();
+ }
+
+ for(unsigned i = 0; i < in.num_data; ++i)
+ s << Color::None << in.data[i]->m_name << "\n";
+
+ separator_to_stream();
+
+ s << Color::Cyan << "[doctest] " << Color::None
+ << "unskipped test cases passing the current filters: "
+ << g_cs->numTestCasesPassingFilters << "\n";
+
+ } else if(opt.list_test_suites) {
+ s << Color::Cyan << "[doctest] " << Color::None << "listing all test suites\n";
+ separator_to_stream();
+
+ for(unsigned i = 0; i < in.num_data; ++i)
+ s << Color::None << in.data[i]->m_test_suite << "\n";
+
+ separator_to_stream();
+
+ s << Color::Cyan << "[doctest] " << Color::None
+ << "unskipped test cases passing the current filters: "
+ << g_cs->numTestCasesPassingFilters << "\n";
+ s << Color::Cyan << "[doctest] " << Color::None
+ << "test suites with unskipped test cases passing the current filters: "
+ << g_cs->numTestSuitesPassingFilters << "\n";
+ }
+ }
+
+ void test_run_start() override {
+ if(!opt.minimal)
+ printIntro();
+ }
+
+ void test_run_end(const TestRunStats& p) override {
+ if(opt.minimal && p.numTestCasesFailed == 0)
+ return;
+
+ separator_to_stream();
+ s << std::dec;
+
+ auto totwidth = int(std::ceil(log10((std::max(p.numTestCasesPassingFilters, static_cast<unsigned>(p.numAsserts))) + 1)));
+ auto passwidth = int(std::ceil(log10((std::max(p.numTestCasesPassingFilters - p.numTestCasesFailed, static_cast<unsigned>(p.numAsserts - p.numAssertsFailed))) + 1)));
+ auto failwidth = int(std::ceil(log10((std::max(p.numTestCasesFailed, static_cast<unsigned>(p.numAssertsFailed))) + 1)));
+ const bool anythingFailed = p.numTestCasesFailed > 0 || p.numAssertsFailed > 0;
+ s << Color::Cyan << "[doctest] " << Color::None << "test cases: " << std::setw(totwidth)
+ << p.numTestCasesPassingFilters << " | "
+ << ((p.numTestCasesPassingFilters == 0 || anythingFailed) ? Color::None :
+ Color::Green)
+ << std::setw(passwidth) << p.numTestCasesPassingFilters - p.numTestCasesFailed << " passed"
+ << Color::None << " | " << (p.numTestCasesFailed > 0 ? Color::Red : Color::None)
+ << std::setw(failwidth) << p.numTestCasesFailed << " failed" << Color::None << " |";
+ if(opt.no_skipped_summary == false) {
+ const int numSkipped = p.numTestCases - p.numTestCasesPassingFilters;
+ s << " " << (numSkipped == 0 ? Color::None : Color::Yellow) << numSkipped
+ << " skipped" << Color::None;
+ }
+ s << "\n";
+ s << Color::Cyan << "[doctest] " << Color::None << "assertions: " << std::setw(totwidth)
+ << p.numAsserts << " | "
+ << ((p.numAsserts == 0 || anythingFailed) ? Color::None : Color::Green)
+ << std::setw(passwidth) << (p.numAsserts - p.numAssertsFailed) << " passed" << Color::None
+ << " | " << (p.numAssertsFailed > 0 ? Color::Red : Color::None) << std::setw(failwidth)
+ << p.numAssertsFailed << " failed" << Color::None << " |\n";
+ s << Color::Cyan << "[doctest] " << Color::None
+ << "Status: " << (p.numTestCasesFailed > 0 ? Color::Red : Color::Green)
+ << ((p.numTestCasesFailed > 0) ? "FAILURE!" : "SUCCESS!") << Color::None << std::endl;
+ }
+
+ void test_case_start(const TestCaseData& in) override {
+ hasLoggedCurrentTestStart = false;
+ tc = &in;
+ subcasesStack.clear();
+ currentSubcaseLevel = 0;
+ }
+
+ void test_case_reenter(const TestCaseData&) override {
+ subcasesStack.clear();
+ }
+
+ void test_case_end(const CurrentTestCaseStats& st) override {
+ if(tc->m_no_output)
+ return;
+
+ // log the preamble of the test case only if there is something
+ // else to print - something other than that an assert has failed
+ if(opt.duration ||
+ (st.failure_flags && st.failure_flags != static_cast<int>(TestCaseFailureReason::AssertFailure)))
+ logTestStart();
+
+ if(opt.duration)
+ s << Color::None << std::setprecision(6) << std::fixed << st.seconds
+ << " s: " << tc->m_name << "\n";
+
+ if(st.failure_flags & TestCaseFailureReason::Timeout)
+ s << Color::Red << "Test case exceeded time limit of " << std::setprecision(6)
+ << std::fixed << tc->m_timeout << "!\n";
+
+ if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedButDidnt) {
+ s << Color::Red << "Should have failed but didn't! Marking it as failed!\n";
+ } else if(st.failure_flags & TestCaseFailureReason::ShouldHaveFailedAndDid) {
+ s << Color::Yellow << "Failed as expected so marking it as not failed\n";
+ } else if(st.failure_flags & TestCaseFailureReason::CouldHaveFailedAndDid) {
+ s << Color::Yellow << "Allowed to fail so marking it as not failed\n";
+ } else if(st.failure_flags & TestCaseFailureReason::DidntFailExactlyNumTimes) {
+ s << Color::Red << "Didn't fail exactly " << tc->m_expected_failures
+ << " times so marking it as failed!\n";
+ } else if(st.failure_flags & TestCaseFailureReason::FailedExactlyNumTimes) {
+ s << Color::Yellow << "Failed exactly " << tc->m_expected_failures
+ << " times as expected so marking it as not failed!\n";
+ }
+ if(st.failure_flags & TestCaseFailureReason::TooManyFailedAsserts) {
+ s << Color::Red << "Aborting - too many failed asserts!\n";
+ }
+ s << Color::None; // lgtm [cpp/useless-expression]
+ }
+
+ void test_case_exception(const TestCaseException& e) override {
+ DOCTEST_LOCK_MUTEX(mutex)
+ if(tc->m_no_output)
+ return;
+
+ logTestStart();
+
+ file_line_to_stream(tc->m_file.c_str(), tc->m_line, " ");
+ successOrFailColoredStringToStream(false, e.is_crash ? assertType::is_require :
+ assertType::is_check);
+ s << Color::Red << (e.is_crash ? "test case CRASHED: " : "test case THREW exception: ")
+ << Color::Cyan << e.error_string << "\n";
+
+ int num_stringified_contexts = get_num_stringified_contexts();
+ if(num_stringified_contexts) {
+ auto stringified_contexts = get_stringified_contexts();
+ s << Color::None << " logged: ";
+ for(int i = num_stringified_contexts; i > 0; --i) {
+ s << (i == num_stringified_contexts ? "" : " ")
+ << stringified_contexts[i - 1] << "\n";
+ }
+ }
+ s << "\n" << Color::None;
+ }
+
+ void subcase_start(const SubcaseSignature& subc) override {
+ subcasesStack.push_back(subc);
+ ++currentSubcaseLevel;
+ hasLoggedCurrentTestStart = false;
+ }
+
+ void subcase_end() override {
+ --currentSubcaseLevel;
+ hasLoggedCurrentTestStart = false;
+ }
+
+ void log_assert(const AssertData& rb) override {
+ if((!rb.m_failed && !opt.success) || tc->m_no_output)
+ return;
+
+ DOCTEST_LOCK_MUTEX(mutex)
+
+ logTestStart();
+
+ file_line_to_stream(rb.m_file, rb.m_line, " ");
+ successOrFailColoredStringToStream(!rb.m_failed, rb.m_at);
+
+ fulltext_log_assert_to_stream(s, rb);
+
+ log_contexts();
+ }
+
+ void log_message(const MessageData& mb) override {
+ if(tc->m_no_output)
+ return;
+
+ DOCTEST_LOCK_MUTEX(mutex)
+
+ logTestStart();
+
+ file_line_to_stream(mb.m_file, mb.m_line, " ");
+ s << getSuccessOrFailColor(false, mb.m_severity)
+ << getSuccessOrFailString(mb.m_severity & assertType::is_warn, mb.m_severity,
+ "MESSAGE") << ": ";
+ s << Color::None << mb.m_string << "\n";
+ log_contexts();
+ }
+
+ void test_case_skipped(const TestCaseData&) override {}
+ };
+
+ DOCTEST_REGISTER_REPORTER("console", 0, ConsoleReporter);
+
+#ifdef DOCTEST_PLATFORM_WINDOWS
+ struct DebugOutputWindowReporter : public ConsoleReporter
+ {
+ DOCTEST_THREAD_LOCAL static std::ostringstream oss;
+
+ DebugOutputWindowReporter(const ContextOptions& co)
+ : ConsoleReporter(co, oss) {}
+
+#define DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(func, type, arg) \
+ void func(type arg) override { \
+ bool with_col = g_no_colors; \
+ g_no_colors = false; \
+ ConsoleReporter::func(arg); \
+ if(oss.tellp() != std::streampos{}) { \
+ DOCTEST_OUTPUT_DEBUG_STRING(oss.str().c_str()); \
+ oss.str(""); \
+ } \
+ g_no_colors = with_col; \
+ }
+
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_start, DOCTEST_EMPTY, DOCTEST_EMPTY)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_run_end, const TestRunStats&, in)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_start, const TestCaseData&, in)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_reenter, const TestCaseData&, in)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_end, const CurrentTestCaseStats&, in)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_exception, const TestCaseException&, in)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_start, const SubcaseSignature&, in)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(subcase_end, DOCTEST_EMPTY, DOCTEST_EMPTY)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_assert, const AssertData&, in)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(log_message, const MessageData&, in)
+ DOCTEST_DEBUG_OUTPUT_REPORTER_OVERRIDE(test_case_skipped, const TestCaseData&, in)
+ };
+
+ DOCTEST_THREAD_LOCAL std::ostringstream DebugOutputWindowReporter::oss;
+#endif // DOCTEST_PLATFORM_WINDOWS
+
+ // the implementation of parseOption()
+ bool parseOptionImpl(int argc, const char* const* argv, const char* pattern, String* value) {
+ // going from the end to the beginning and stopping on the first occurrence from the end
+ for(int i = argc; i > 0; --i) {
+ auto index = i - 1;
+ auto temp = std::strstr(argv[index], pattern);
+ if(temp && (value || strlen(temp) == strlen(pattern))) { //!OCLINT prefer early exits and continue
+ // eliminate matches in which the chars before the option are not '-'
+ bool noBadCharsFound = true;
+ auto curr = argv[index];
+ while(curr != temp) {
+ if(*curr++ != '-') {
+ noBadCharsFound = false;
+ break;
+ }
+ }
+ if(noBadCharsFound && argv[index][0] == '-') {
+ if(value) {
+ // parsing the value of an option
+ temp += strlen(pattern);
+ const unsigned len = strlen(temp);
+ if(len) {
+ *value = temp;
+ return true;
+ }
+ } else {
+ // just a flag - no value
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ // parses an option and returns the string after the '=' character
+ bool parseOption(int argc, const char* const* argv, const char* pattern, String* value = nullptr,
+ const String& defaultVal = String()) {
+ if(value)
+ *value = defaultVal;
+#ifndef DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS
+ // offset (normally 3 for "dt-") to skip prefix
+ if(parseOptionImpl(argc, argv, pattern + strlen(DOCTEST_CONFIG_OPTIONS_PREFIX), value))
+ return true;
+#endif // DOCTEST_CONFIG_NO_UNPREFIXED_OPTIONS
+ return parseOptionImpl(argc, argv, pattern, value);
+ }
+
+ // locates a flag on the command line
+ bool parseFlag(int argc, const char* const* argv, const char* pattern) {
+ return parseOption(argc, argv, pattern);
+ }
+
+ // parses a comma separated list of words after a pattern in one of the arguments in argv
+ bool parseCommaSepArgs(int argc, const char* const* argv, const char* pattern,
+ std::vector<String>& res) {
+ String filtersString;
+ if(parseOption(argc, argv, pattern, &filtersString)) {
+ // tokenize with "," as a separator, unless escaped with backslash
+ std::ostringstream s;
+ auto flush = [&s, &res]() {
+ auto string = s.str();
+ if(string.size() > 0) {
+ res.push_back(string.c_str());
+ }
+ s.str("");
+ };
+
+ bool seenBackslash = false;
+ const char* current = filtersString.c_str();
+ const char* end = current + strlen(current);
+ while(current != end) {
+ char character = *current++;
+ if(seenBackslash) {
+ seenBackslash = false;
+ if(character == ',' || character == '\\') {
+ s.put(character);
+ continue;
+ }
+ s.put('\\');
+ }
+ if(character == '\\') {
+ seenBackslash = true;
+ } else if(character == ',') {
+ flush();
+ } else {
+ s.put(character);
+ }
+ }
+
+ if(seenBackslash) {
+ s.put('\\');
+ }
+ flush();
+ return true;
+ }
+ return false;
+ }
+
+ enum optionType
+ {
+ option_bool,
+ option_int
+ };
+
+ // parses an int/bool option from the command line
+ bool parseIntOption(int argc, const char* const* argv, const char* pattern, optionType type,
+ int& res) {
+ String parsedValue;
+ if(!parseOption(argc, argv, pattern, &parsedValue))
+ return false;
+
+ if(type) {
+ // integer
+ // TODO: change this to use std::stoi or something else! currently it uses undefined behavior - assumes '0' on failed parse...
+ int theInt = std::atoi(parsedValue.c_str());
+ if (theInt != 0) {
+ res = theInt; //!OCLINT parameter reassignment
+ return true;
+ }
+ } else {
+ // boolean
+ const char positive[][5] = { "1", "true", "on", "yes" }; // 5 - strlen("true") + 1
+ const char negative[][6] = { "0", "false", "off", "no" }; // 6 - strlen("false") + 1
+
+ // if the value matches any of the positive/negative possibilities
+ for (unsigned i = 0; i < 4; i++) {
+ if (parsedValue.compare(positive[i], true) == 0) {
+ res = 1; //!OCLINT parameter reassignment
+ return true;
+ }
+ if (parsedValue.compare(negative[i], true) == 0) {
+ res = 0; //!OCLINT parameter reassignment
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+} // namespace
+
+Context::Context(int argc, const char* const* argv)
+ : p(new detail::ContextState) {
+ parseArgs(argc, argv, true);
+ if(argc)
+ p->binary_name = argv[0];
+}
+
+Context::~Context() {
+ if(g_cs == p)
+ g_cs = nullptr;
+ delete p;
+}
+
+void Context::applyCommandLine(int argc, const char* const* argv) {
+ parseArgs(argc, argv);
+ if(argc)
+ p->binary_name = argv[0];
+}
+
+// parses args
+void Context::parseArgs(int argc, const char* const* argv, bool withDefaults) {
+ using namespace detail;
+
+ // clang-format off
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file=", p->filters[0]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sf=", p->filters[0]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "source-file-exclude=",p->filters[1]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sfe=", p->filters[1]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite=", p->filters[2]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ts=", p->filters[2]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-suite-exclude=", p->filters[3]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tse=", p->filters[3]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case=", p->filters[4]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tc=", p->filters[4]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "test-case-exclude=", p->filters[5]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "tce=", p->filters[5]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase=", p->filters[6]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sc=", p->filters[6]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "subcase-exclude=", p->filters[7]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "sce=", p->filters[7]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "reporters=", p->filters[8]);
+ parseCommaSepArgs(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "r=", p->filters[8]);
+ // clang-format on
+
+ int intRes = 0;
+ String strRes;
+
+#define DOCTEST_PARSE_AS_BOOL_OR_FLAG(name, sname, var, default) \
+ if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_bool, intRes) || \
+ parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_bool, intRes)) \
+ p->var = static_cast<bool>(intRes); \
+ else if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name) || \
+ parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname)) \
+ p->var = true; \
+ else if(withDefaults) \
+ p->var = default
+
+#define DOCTEST_PARSE_INT_OPTION(name, sname, var, default) \
+ if(parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", option_int, intRes) || \
+ parseIntOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", option_int, intRes)) \
+ p->var = intRes; \
+ else if(withDefaults) \
+ p->var = default
+
+#define DOCTEST_PARSE_STR_OPTION(name, sname, var, default) \
+ if(parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX name "=", &strRes, default) || \
+ parseOption(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX sname "=", &strRes, default) || \
+ withDefaults) \
+ p->var = strRes
+
+ // clang-format off
+ DOCTEST_PARSE_STR_OPTION("out", "o", out, "");
+ DOCTEST_PARSE_STR_OPTION("order-by", "ob", order_by, "file");
+ DOCTEST_PARSE_INT_OPTION("rand-seed", "rs", rand_seed, 0);
+
+ DOCTEST_PARSE_INT_OPTION("first", "f", first, 0);
+ DOCTEST_PARSE_INT_OPTION("last", "l", last, UINT_MAX);
+
+ DOCTEST_PARSE_INT_OPTION("abort-after", "aa", abort_after, 0);
+ DOCTEST_PARSE_INT_OPTION("subcase-filter-levels", "scfl", subcase_filter_levels, INT_MAX);
+
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("success", "s", success, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("case-sensitive", "cs", case_sensitive, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("exit", "e", exit, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("duration", "d", duration, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("minimal", "m", minimal, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("quiet", "q", quiet, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-throw", "nt", no_throw, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-exitcode", "ne", no_exitcode, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-run", "nr", no_run, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-intro", "ni", no_intro, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-version", "nv", no_version, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-colors", "nc", no_colors, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("force-colors", "fc", force_colors, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-breaks", "nb", no_breaks, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skip", "ns", no_skip, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("gnu-file-line", "gfl", gnu_file_line, !bool(DOCTEST_MSVC));
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-path-filenames", "npf", no_path_in_filenames, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-line-numbers", "nln", no_line_numbers, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-debug-output", "ndo", no_debug_output, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-skipped-summary", "nss", no_skipped_summary, false);
+ DOCTEST_PARSE_AS_BOOL_OR_FLAG("no-time-in-output", "ntio", no_time_in_output, false);
+ // clang-format on
+
+ if(withDefaults) {
+ p->help = false;
+ p->version = false;
+ p->count = false;
+ p->list_test_cases = false;
+ p->list_test_suites = false;
+ p->list_reporters = false;
+ }
+ if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "help") ||
+ parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "h") ||
+ parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "?")) {
+ p->help = true;
+ p->exit = true;
+ }
+ if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "version") ||
+ parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "v")) {
+ p->version = true;
+ p->exit = true;
+ }
+ if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "count") ||
+ parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "c")) {
+ p->count = true;
+ p->exit = true;
+ }
+ if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-cases") ||
+ parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "ltc")) {
+ p->list_test_cases = true;
+ p->exit = true;
+ }
+ if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-test-suites") ||
+ parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lts")) {
+ p->list_test_suites = true;
+ p->exit = true;
+ }
+ if(parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "list-reporters") ||
+ parseFlag(argc, argv, DOCTEST_CONFIG_OPTIONS_PREFIX "lr")) {
+ p->list_reporters = true;
+ p->exit = true;
+ }
+}
+
+// allows the user to add procedurally to the filters from the command line
+void Context::addFilter(const char* filter, const char* value) { setOption(filter, value); }
+
+// allows the user to clear all filters from the command line
+void Context::clearFilters() {
+ for(auto& curr : p->filters)
+ curr.clear();
+}
+
+// allows the user to override procedurally the bool options from the command line
+void Context::setOption(const char* option, bool value) {
+ setOption(option, value ? "true" : "false");
+}
+
+// allows the user to override procedurally the int options from the command line
+void Context::setOption(const char* option, int value) {
+ setOption(option, toString(value).c_str());
+}
+
+// allows the user to override procedurally the string options from the command line
+void Context::setOption(const char* option, const char* value) {
+ auto argv = String("-") + option + "=" + value;
+ auto lvalue = argv.c_str();
+ parseArgs(1, &lvalue);
+}
+
+// users should query this in their main() and exit the program if true
+bool Context::shouldExit() { return p->exit; }
+
+void Context::setAsDefaultForAssertsOutOfTestCases() { g_cs = p; }
+
+void Context::setAssertHandler(detail::assert_handler ah) { p->ah = ah; }
+
+void Context::setCout(std::ostream* out) { p->cout = out; }
+
+static class DiscardOStream : public std::ostream
+{
+private:
+ class : public std::streambuf
+ {
+ private:
+ // allowing some buffering decreases the amount of calls to overflow
+ char buf[1024];
+
+ protected:
+ std::streamsize xsputn(const char_type*, std::streamsize count) override { return count; }
+
+ int_type overflow(int_type ch) override {
+ setp(std::begin(buf), std::end(buf));
+ return traits_type::not_eof(ch);
+ }
+ } discardBuf;
+
+public:
+ DiscardOStream()
+ : std::ostream(&discardBuf) {}
+} discardOut;
+
+// the main function that does all the filtering and test running
+int Context::run() {
+ using namespace detail;
+
+ // save the old context state in case such was setup - for using asserts out of a testing context
+ auto old_cs = g_cs;
+ // this is the current contest
+ g_cs = p;
+ is_running_in_test = true;
+
+ g_no_colors = p->no_colors;
+ p->resetRunData();
+
+ std::fstream fstr;
+ if(p->cout == nullptr) {
+ if(p->quiet) {
+ p->cout = &discardOut;
+ } else if(p->out.size()) {
+ // to a file if specified
+ fstr.open(p->out.c_str(), std::fstream::out);
+ p->cout = &fstr;
+ } else {
+ // stdout by default
+ p->cout = &std::cout;
+ }
+ }
+
+ FatalConditionHandler::allocateAltStackMem();
+
+ auto cleanup_and_return = [&]() {
+ FatalConditionHandler::freeAltStackMem();
+
+ if(fstr.is_open())
+ fstr.close();
+
+ // restore context
+ g_cs = old_cs;
+ is_running_in_test = false;
+
+ // we have to free the reporters which were allocated when the run started
+ for(auto& curr : p->reporters_currently_used)
+ delete curr;
+ p->reporters_currently_used.clear();
+
+ if(p->numTestCasesFailed && !p->no_exitcode)
+ return EXIT_FAILURE;
+ return EXIT_SUCCESS;
+ };
+
+ // setup default reporter if none is given through the command line
+ if(p->filters[8].empty())
+ p->filters[8].push_back("console");
+
+ // check to see if any of the registered reporters has been selected
+ for(auto& curr : getReporters()) {
+ if(matchesAny(curr.first.second.c_str(), p->filters[8], false, p->case_sensitive))
+ p->reporters_currently_used.push_back(curr.second(*g_cs));
+ }
+
+ // TODO: check if there is nothing in reporters_currently_used
+
+ // prepend all listeners
+ for(auto& curr : getListeners())
+ p->reporters_currently_used.insert(p->reporters_currently_used.begin(), curr.second(*g_cs));
+
+#ifdef DOCTEST_PLATFORM_WINDOWS
+ if(isDebuggerActive() && p->no_debug_output == false)
+ p->reporters_currently_used.push_back(new DebugOutputWindowReporter(*g_cs));
+#endif // DOCTEST_PLATFORM_WINDOWS
+
+ // handle version, help and no_run
+ if(p->no_run || p->version || p->help || p->list_reporters) {
+ DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, QueryData());
+
+ return cleanup_and_return();
+ }
+
+ std::vector<const TestCase*> testArray;
+ for(auto& curr : getRegisteredTests())
+ testArray.push_back(&curr);
+ p->numTestCases = testArray.size();
+
+ // sort the collected records
+ if(!testArray.empty()) {
+ if(p->order_by.compare("file", true) == 0) {
+ std::sort(testArray.begin(), testArray.end(), fileOrderComparator);
+ } else if(p->order_by.compare("suite", true) == 0) {
+ std::sort(testArray.begin(), testArray.end(), suiteOrderComparator);
+ } else if(p->order_by.compare("name", true) == 0) {
+ std::sort(testArray.begin(), testArray.end(), nameOrderComparator);
+ } else if(p->order_by.compare("rand", true) == 0) {
+ std::srand(p->rand_seed);
+
+ // random_shuffle implementation
+ const auto first = &testArray[0];
+ for(size_t i = testArray.size() - 1; i > 0; --i) {
+ int idxToSwap = std::rand() % (i + 1);
+
+ const auto temp = first[i];
+
+ first[i] = first[idxToSwap];
+ first[idxToSwap] = temp;
+ }
+ } else if(p->order_by.compare("none", true) == 0) {
+ // means no sorting - beneficial for death tests which call into the executable
+ // with a specific test case in mind - we don't want to slow down the startup times
+ }
+ }
+
+ std::set<String> testSuitesPassingFilt;
+
+ bool query_mode = p->count || p->list_test_cases || p->list_test_suites;
+ std::vector<const TestCaseData*> queryResults;
+
+ if(!query_mode)
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_start, DOCTEST_EMPTY);
+
+ // invoke the registered functions if they match the filter criteria (or just count them)
+ for(auto& curr : testArray) {
+ const auto& tc = *curr;
+
+ bool skip_me = false;
+ if(tc.m_skip && !p->no_skip)
+ skip_me = true;
+
+ if(!matchesAny(tc.m_file.c_str(), p->filters[0], true, p->case_sensitive))
+ skip_me = true;
+ if(matchesAny(tc.m_file.c_str(), p->filters[1], false, p->case_sensitive))
+ skip_me = true;
+ if(!matchesAny(tc.m_test_suite, p->filters[2], true, p->case_sensitive))
+ skip_me = true;
+ if(matchesAny(tc.m_test_suite, p->filters[3], false, p->case_sensitive))
+ skip_me = true;
+ if(!matchesAny(tc.m_name, p->filters[4], true, p->case_sensitive))
+ skip_me = true;
+ if(matchesAny(tc.m_name, p->filters[5], false, p->case_sensitive))
+ skip_me = true;
+
+ if(!skip_me)
+ p->numTestCasesPassingFilters++;
+
+ // skip the test if it is not in the execution range
+ if((p->last < p->numTestCasesPassingFilters && p->first <= p->last) ||
+ (p->first > p->numTestCasesPassingFilters))
+ skip_me = true;
+
+ if(skip_me) {
+ if(!query_mode)
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_skipped, tc);
+ continue;
+ }
+
+ // do not execute the test if we are to only count the number of filter passing tests
+ if(p->count)
+ continue;
+
+ // print the name of the test and don't execute it
+ if(p->list_test_cases) {
+ queryResults.push_back(&tc);
+ continue;
+ }
+
+ // print the name of the test suite if not done already and don't execute it
+ if(p->list_test_suites) {
+ if((testSuitesPassingFilt.count(tc.m_test_suite) == 0) && tc.m_test_suite[0] != '\0') {
+ queryResults.push_back(&tc);
+ testSuitesPassingFilt.insert(tc.m_test_suite);
+ p->numTestSuitesPassingFilters++;
+ }
+ continue;
+ }
+
+ // execute the test if it passes all the filtering
+ {
+ p->currentTest = &tc;
+
+ p->failure_flags = TestCaseFailureReason::None;
+ p->seconds = 0;
+
+ // reset atomic counters
+ p->numAssertsFailedCurrentTest_atomic = 0;
+ p->numAssertsCurrentTest_atomic = 0;
+
+ p->fullyTraversedSubcases.clear();
+
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_start, tc);
+
+ p->timer.start();
+
+ bool run_test = true;
+
+ do {
+ // reset some of the fields for subcases (except for the set of fully passed ones)
+ p->reachedLeaf = false;
+ // May not be empty if previous subcase exited via exception.
+ p->subcaseStack.clear();
+ p->currentSubcaseDepth = 0;
+
+ p->shouldLogCurrentException = true;
+
+ // reset stuff for logging with INFO()
+ p->stringifiedContexts.clear();
+
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+ try {
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+// MSVC 2015 diagnoses fatalConditionHandler as unused (because reset() is a static method)
+DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4101) // unreferenced local variable
+ FatalConditionHandler fatalConditionHandler; // Handle signals
+ // execute the test
+ tc.m_test();
+ fatalConditionHandler.reset();
+DOCTEST_MSVC_SUPPRESS_WARNING_POP
+#ifndef DOCTEST_CONFIG_NO_EXCEPTIONS
+ } catch(const TestFailureException&) {
+ p->failure_flags |= TestCaseFailureReason::AssertFailure;
+ } catch(...) {
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_exception,
+ {translateActiveException(), false});
+ p->failure_flags |= TestCaseFailureReason::Exception;
+ }
+#endif // DOCTEST_CONFIG_NO_EXCEPTIONS
+
+ // exit this loop if enough assertions have failed - even if there are more subcases
+ if(p->abort_after > 0 &&
+ p->numAssertsFailed + p->numAssertsFailedCurrentTest_atomic >= p->abort_after) {
+ run_test = false;
+ p->failure_flags |= TestCaseFailureReason::TooManyFailedAsserts;
+ }
+
+ if(!p->nextSubcaseStack.empty() && run_test)
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_reenter, tc);
+ if(p->nextSubcaseStack.empty())
+ run_test = false;
+ } while(run_test);
+
+ p->finalizeTestCaseData();
+
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_case_end, *g_cs);
+
+ p->currentTest = nullptr;
+
+ // stop executing tests if enough assertions have failed
+ if(p->abort_after > 0 && p->numAssertsFailed >= p->abort_after)
+ break;
+ }
+ }
+
+ if(!query_mode) {
+ DOCTEST_ITERATE_THROUGH_REPORTERS(test_run_end, *g_cs);
+ } else {
+ QueryData qdata;
+ qdata.run_stats = g_cs;
+ qdata.data = queryResults.data();
+ qdata.num_data = unsigned(queryResults.size());
+ DOCTEST_ITERATE_THROUGH_REPORTERS(report_query, qdata);
+ }
+
+ return cleanup_and_return();
+}
+
+DOCTEST_DEFINE_INTERFACE(IReporter)
+
+int IReporter::get_num_active_contexts() { return detail::g_infoContexts.size(); }
+const IContextScope* const* IReporter::get_active_contexts() {
+ return get_num_active_contexts() ? &detail::g_infoContexts[0] : nullptr;
+}
+
+int IReporter::get_num_stringified_contexts() { return detail::g_cs->stringifiedContexts.size(); }
+const String* IReporter::get_stringified_contexts() {
+ return get_num_stringified_contexts() ? &detail::g_cs->stringifiedContexts[0] : nullptr;
+}
+
+namespace detail {
+ void registerReporterImpl(const char* name, int priority, reporterCreatorFunc c, bool isReporter) {
+ if(isReporter)
+ getReporters().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c));
+ else
+ getListeners().insert(reporterMap::value_type(reporterMap::key_type(priority, name), c));
+ }
+} // namespace detail
+
+} // namespace doctest
+
+#endif // DOCTEST_CONFIG_DISABLE
+
+#ifdef DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+DOCTEST_MSVC_SUPPRESS_WARNING_WITH_PUSH(4007) // 'function' : must be 'attribute' - see issue #182
+int main(int argc, char** argv) { return doctest::Context(argc, argv).run(); }
+DOCTEST_MSVC_SUPPRESS_WARNING_POP
+#endif // DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+
+DOCTEST_CLANG_SUPPRESS_WARNING_POP
+DOCTEST_MSVC_SUPPRESS_WARNING_POP
+DOCTEST_GCC_SUPPRESS_WARNING_POP
+
+DOCTEST_SUPPRESS_COMMON_WARNINGS_POP
+
+#endif // DOCTEST_LIBRARY_IMPLEMENTATION
+#endif // DOCTEST_CONFIG_IMPLEMENT
diff --git a/src/third-party/intervaltree/IntervalTree.h b/src/third-party/intervaltree/IntervalTree.h
new file mode 100644
index 0000000..d631b5f
--- /dev/null
+++ b/src/third-party/intervaltree/IntervalTree.h
@@ -0,0 +1,346 @@
+/**
+ * Origin: https://github.com/ekg/intervaltree
+ */
+
+#ifndef __INTERVAL_TREE_H
+#define __INTERVAL_TREE_H
+
+#include <algorithm>
+#include <cassert>
+#include <iostream>
+#include <limits>
+#include <memory>
+#include <vector>
+
+namespace interval_tree {
+
+template <class Scalar, typename Value>
+class Interval {
+public:
+ Scalar start;
+ Scalar stop;
+ Value value;
+ Interval(const Scalar& s, const Scalar& e, const Value& v)
+ : start(std::min(s, e))
+ , stop(std::max(s, e))
+ , value(v)
+ {}
+};
+
+template <class Scalar, typename Value>
+Value intervalStart(const Interval<Scalar,Value>& i) {
+ return i.start;
+}
+
+template <class Scalar, typename Value>
+Value intervalStop(const Interval<Scalar, Value>& i) {
+ return i.stop;
+}
+
+template <class Scalar, typename Value>
+std::ostream& operator<<(std::ostream& out, const Interval<Scalar, Value>& i) {
+ out << "Interval(" << i.start << ", " << i.stop << "): " << i.value;
+ return out;
+}
+
+template <class Scalar, class Value>
+class IntervalTree {
+public:
+ typedef Interval<Scalar, Value> interval;
+ typedef std::vector<interval> interval_vector;
+
+
+ struct IntervalStartCmp {
+ bool operator()(const interval& a, const interval& b) {
+ return a.start < b.start;
+ }
+ };
+
+ struct IntervalStopCmp {
+ bool operator()(const interval& a, const interval& b) {
+ return a.stop < b.stop;
+ }
+ };
+
+ IntervalTree()
+ : left(nullptr)
+ , right(nullptr)
+ , center(0)
+ {}
+
+ ~IntervalTree() = default;
+
+ std::unique_ptr<IntervalTree> clone() const {
+ return std::unique_ptr<IntervalTree>(new IntervalTree(*this));
+ }
+
+ IntervalTree(const IntervalTree& other)
+ : intervals(other.intervals),
+ left(other.left ? other.left->clone() : nullptr),
+ right(other.right ? other.right->clone() : nullptr),
+ center(other.center)
+ {}
+
+ IntervalTree& operator=(IntervalTree&&) = default;
+ IntervalTree(IntervalTree&&) = default;
+
+ IntervalTree& operator=(const IntervalTree& other) {
+ center = other.center;
+ intervals = other.intervals;
+ left = other.left ? other.left->clone() : nullptr;
+ right = other.right ? other.right->clone() : nullptr;
+ return *this;
+ }
+
+ IntervalTree(
+ interval_vector&& ivals,
+ std::size_t depth = 16,
+ std::size_t minbucket = 64,
+ std::size_t maxbucket = 512,
+ Scalar leftextent = 0,
+ Scalar rightextent = 0)
+ : left(nullptr)
+ , right(nullptr)
+ {
+ --depth;
+ const auto minmaxStop = std::minmax_element(ivals.begin(), ivals.end(),
+ IntervalStopCmp());
+ const auto minmaxStart = std::minmax_element(ivals.begin(), ivals.end(),
+ IntervalStartCmp());
+ if (!ivals.empty()) {
+ center = (minmaxStart.first->start + minmaxStop.second->stop) / 2;
+ }
+ if (leftextent == 0 && rightextent == 0) {
+ // sort intervals by start
+ std::sort(ivals.begin(), ivals.end(), IntervalStartCmp());
+ } else {
+ assert(std::is_sorted(ivals.begin(), ivals.end(), IntervalStartCmp()));
+ }
+ if (depth == 0 || (ivals.size() < minbucket && ivals.size() < maxbucket)) {
+ std::sort(ivals.begin(), ivals.end(), IntervalStartCmp());
+ intervals = std::move(ivals);
+ assert(is_valid().first);
+ return;
+ } else {
+ Scalar leftp = 0;
+ Scalar rightp = 0;
+
+ if (leftextent || rightextent) {
+ leftp = leftextent;
+ rightp = rightextent;
+ } else {
+ leftp = ivals.front().start;
+ rightp = std::max_element(ivals.begin(), ivals.end(),
+ IntervalStopCmp())->stop;
+ }
+
+ interval_vector lefts;
+ interval_vector rights;
+
+ for (typename interval_vector::const_iterator i = ivals.begin();
+ i != ivals.end(); ++i) {
+ const interval& interval = *i;
+ if (interval.stop < center) {
+ lefts.push_back(interval);
+ } else if (interval.start > center) {
+ rights.push_back(interval);
+ } else {
+ assert(interval.start <= center);
+ assert(center <= interval.stop);
+ intervals.push_back(interval);
+ }
+ }
+
+ if (!lefts.empty()) {
+ left.reset(new IntervalTree(std::move(lefts),
+ depth, minbucket, maxbucket,
+ leftp, center));
+ }
+ if (!rights.empty()) {
+ right.reset(new IntervalTree(std::move(rights),
+ depth, minbucket, maxbucket,
+ center, rightp));
+ }
+ }
+ assert(is_valid().first);
+ }
+
+ // Call f on all intervals near the range [start, stop]:
+ template <class UnaryFunction>
+ void visit_near(const Scalar& start, const Scalar& stop, UnaryFunction f) const {
+ if (!intervals.empty() && ! (stop < intervals.front().start)) {
+ for (auto & i : intervals) {
+ f(i);
+ }
+ }
+ if (left && start <= center) {
+ left->visit_near(start, stop, f);
+ }
+ if (right && stop >= center) {
+ right->visit_near(start, stop, f);
+ }
+ }
+
+ // Call f on all intervals crossing pos
+ template <class UnaryFunction>
+ void visit_overlapping(const Scalar& pos, UnaryFunction f) const {
+ visit_overlapping(pos, pos, f);
+ }
+
+ // Call f on all intervals overlapping [start, stop]
+ template <class UnaryFunction>
+ void visit_overlapping(const Scalar& start, const Scalar& stop, UnaryFunction f) const {
+ auto filterF = [&](const interval& interval) {
+ if (interval.stop >= start && interval.start <= stop) {
+ // Only apply f if overlapping
+ f(interval);
+ }
+ };
+ visit_near(start, stop, filterF);
+ }
+
+ // Call f on all intervals contained within [start, stop]
+ template <class UnaryFunction>
+ void visit_contained(const Scalar& start, const Scalar& stop, UnaryFunction f) const {
+ auto filterF = [&](const interval& interval) {
+ if (start <= interval.start && interval.stop <= stop) {
+ f(interval);
+ }
+ };
+ visit_near(start, stop, filterF);
+ }
+
+ interval_vector findOverlapping(const Scalar& start, const Scalar& stop) const {
+ interval_vector result;
+ visit_overlapping(start, stop,
+ [&](const interval& interval) {
+ result.emplace_back(interval);
+ });
+ return result;
+ }
+
+ interval_vector findContained(const Scalar& start, const Scalar& stop) const {
+ interval_vector result;
+ visit_contained(start, stop,
+ [&](const interval& interval) {
+ result.push_back(interval);
+ });
+ return result;
+ }
+ bool empty() const {
+ if (left && !left->empty()) {
+ return false;
+ }
+ if (!intervals.empty()) {
+ return false;
+ }
+ if (right && !right->empty()) {
+ return false;
+ }
+ return true;
+ }
+
+ template <class UnaryFunction>
+ void visit_all(UnaryFunction f) const {
+ if (left) {
+ left->visit_all(f);
+ }
+ std::for_each(intervals.begin(), intervals.end(), f);
+ if (right) {
+ right->visit_all(f);
+ }
+ }
+
+ std::pair<Scalar, Scalar> extentBruitForce() const {
+ struct Extent {
+ std::pair<Scalar, Scalar> x = {std::numeric_limits<Scalar>::max(),
+ std::numeric_limits<Scalar>::min() };
+ void operator()(const interval & interval) {
+ x.first = std::min(x.first, interval.start);
+ x.second = std::max(x.second, interval.stop);
+ }
+ };
+ Extent extent;
+
+ visit_all([&](const interval & interval) { extent(interval); });
+ return extent.x;
+ }
+
+ // Check all constraints.
+ // If first is false, second is invalid.
+ std::pair<bool, std::pair<Scalar, Scalar>> is_valid() const {
+ const auto minmaxStop = std::minmax_element(intervals.begin(), intervals.end(),
+ IntervalStopCmp());
+ const auto minmaxStart = std::minmax_element(intervals.begin(), intervals.end(),
+ IntervalStartCmp());
+
+ std::pair<bool, std::pair<Scalar, Scalar>> result = {true, { std::numeric_limits<Scalar>::max(),
+ std::numeric_limits<Scalar>::min() }};
+ if (!intervals.empty()) {
+ result.second.first = std::min(result.second.first, minmaxStart.first->start);
+ result.second.second = std::min(result.second.second, minmaxStop.second->stop);
+ }
+ if (left) {
+ auto valid = left->is_valid();
+ result.first &= valid.first;
+ result.second.first = std::min(result.second.first, valid.second.first);
+ result.second.second = std::min(result.second.second, valid.second.second);
+ if (!result.first) { return result; }
+ if (valid.second.second >= center) {
+ result.first = false;
+ return result;
+ }
+ }
+ if (right) {
+ auto valid = right->is_valid();
+ result.first &= valid.first;
+ result.second.first = std::min(result.second.first, valid.second.first);
+ result.second.second = std::min(result.second.second, valid.second.second);
+ if (!result.first) { return result; }
+ if (valid.second.first <= center) {
+ result.first = false;
+ return result;
+ }
+ }
+ if (!std::is_sorted(intervals.begin(), intervals.end(), IntervalStartCmp())) {
+ result.first = false;
+ }
+ return result;
+ }
+
+ friend std::ostream& operator<<(std::ostream& os, const IntervalTree& itree) {
+ return writeOut(os, itree);
+ }
+
+ friend std::ostream& writeOut(std::ostream& os, const IntervalTree& itree,
+ std::size_t depth = 0) {
+ auto pad = [&]() { for (std::size_t i = 0; i != depth; ++i) { os << ' '; } };
+ pad(); os << "center: " << itree.center << '\n';
+ for (const interval & inter : itree.intervals) {
+ pad(); os << inter << '\n';
+ }
+ if (itree.left) {
+ pad(); os << "left:\n";
+ writeOut(os, *itree.left, depth + 1);
+ } else {
+ pad(); os << "left: nullptr\n";
+ }
+ if (itree.right) {
+ pad(); os << "right:\n";
+ writeOut(os, *itree.right, depth + 1);
+ } else {
+ pad(); os << "right: nullptr\n";
+ }
+ return os;
+ }
+
+private:
+ interval_vector intervals;
+ std::unique_ptr<IntervalTree> left;
+ std::unique_ptr<IntervalTree> right;
+ Scalar center;
+};
+
+}
+
+#endif
diff --git a/src/third-party/md4c/md4c.c b/src/third-party/md4c/md4c.c
new file mode 100644
index 0000000..3677c0e
--- /dev/null
+++ b/src/third-party/md4c/md4c.c
@@ -0,0 +1,6410 @@
+/*
+ * MD4C: Markdown parser for C
+ * (http://github.com/mity/md4c)
+ *
+ * Copyright (c) 2016-2020 Martin Mitas
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include "md4c.h"
+
+#include <limits.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+
+/*****************************
+ *** Miscellaneous Stuff ***
+ *****************************/
+
+#if !defined(__STDC_VERSION__) || __STDC_VERSION__ < 199409L
+ /* C89/90 or old compilers in general may not understand "inline". */
+ #if defined __GNUC__
+ #define inline __inline__
+ #elif defined _MSC_VER
+ #define inline __inline
+ #else
+ #define inline
+ #endif
+#endif
+
+/* Make the UTF-8 support the default. */
+#if !defined MD4C_USE_ASCII && !defined MD4C_USE_UTF8 && !defined MD4C_USE_UTF16
+ #define MD4C_USE_UTF8
+#endif
+
+/* Magic for making wide literals with MD4C_USE_UTF16. */
+#ifdef _T
+ #undef _T
+#endif
+#if defined MD4C_USE_UTF16
+ #define _T(x) L##x
+#else
+ #define _T(x) x
+#endif
+
+/* Misc. macros. */
+#define SIZEOF_ARRAY(a) (sizeof(a) / sizeof(a[0]))
+
+#define STRINGIZE_(x) #x
+#define STRINGIZE(x) STRINGIZE_(x)
+
+#ifndef TRUE
+ #define TRUE 1
+ #define FALSE 0
+#endif
+
+#define MD_LOG(msg) \
+ do { \
+ if(ctx->parser.debug_log != NULL) \
+ ctx->parser.debug_log((msg), ctx->userdata); \
+ } while(0)
+
+#ifdef DEBUG
+ #define MD_ASSERT(cond) \
+ do { \
+ if(!(cond)) { \
+ MD_LOG(__FILE__ ":" STRINGIZE(__LINE__) ": " \
+ "Assertion '" STRINGIZE(cond) "' failed."); \
+ exit(1); \
+ } \
+ } while(0)
+
+ #define MD_UNREACHABLE() MD_ASSERT(1 == 0)
+#else
+ #ifdef __GNUC__
+ #define MD_ASSERT(cond) do { if(!(cond)) __builtin_unreachable(); } while(0)
+ #define MD_UNREACHABLE() do { __builtin_unreachable(); } while(0)
+ #elif defined _MSC_VER && _MSC_VER > 120
+ #define MD_ASSERT(cond) do { __assume(cond); } while(0)
+ #define MD_UNREACHABLE() do { __assume(0); } while(0)
+ #else
+ #define MD_ASSERT(cond) do {} while(0)
+ #define MD_UNREACHABLE() do {} while(0)
+ #endif
+#endif
+
+/* For falling through case labels in switch statements. */
+#if defined __clang__ && __clang_major__ >= 12
+ #define MD_FALLTHROUGH() __attribute__((fallthrough))
+#elif defined __GNUC__ && __GNUC__ >= 7
+ #define MD_FALLTHROUGH() __attribute__((fallthrough))
+#else
+ #define MD_FALLTHROUGH() ((void)0)
+#endif
+
+/* Suppress "unused parameter" warnings. */
+#define MD_UNUSED(x) ((void)x)
+
+
+/************************
+ *** Internal Types ***
+ ************************/
+
+/* These are omnipresent so lets save some typing. */
+#define CHAR MD_CHAR
+#define SZ MD_SIZE
+#define OFF MD_OFFSET
+
+typedef struct MD_MARK_tag MD_MARK;
+typedef struct MD_BLOCK_tag MD_BLOCK;
+typedef struct MD_CONTAINER_tag MD_CONTAINER;
+typedef struct MD_REF_DEF_tag MD_REF_DEF;
+
+
+/* During analyzes of inline marks, we need to manage some "mark chains",
+ * of (yet unresolved) openers. This structure holds start/end of the chain.
+ * The chain internals are then realized through MD_MARK::prev and ::next.
+ */
+typedef struct MD_MARKCHAIN_tag MD_MARKCHAIN;
+struct MD_MARKCHAIN_tag {
+ int head; /* Index of first mark in the chain, or -1 if empty. */
+ int tail; /* Index of last mark in the chain, or -1 if empty. */
+};
+
+/* Context propagated through all the parsing. */
+typedef struct MD_CTX_tag MD_CTX;
+struct MD_CTX_tag {
+ /* Immutable stuff (parameters of md_parse()). */
+ const CHAR* text;
+ SZ size;
+ MD_PARSER parser;
+ void* userdata;
+
+ /* When this is true, it allows some optimizations. */
+ int doc_ends_with_newline;
+
+ /* Helper temporary growing buffer. */
+ CHAR* buffer;
+ unsigned alloc_buffer;
+
+ /* Reference definitions. */
+ MD_REF_DEF* ref_defs;
+ int n_ref_defs;
+ int alloc_ref_defs;
+ void** ref_def_hashtable;
+ int ref_def_hashtable_size;
+
+ /* Stack of inline/span markers.
+ * This is only used for parsing a single block contents but by storing it
+ * here we may reuse the stack for subsequent blocks; i.e. we have fewer
+ * (re)allocations. */
+ MD_MARK* marks;
+ int n_marks;
+ int alloc_marks;
+
+#if defined MD4C_USE_UTF16
+ char mark_char_map[128];
+#else
+ char mark_char_map[256];
+#endif
+
+ /* For resolving of inline spans. */
+ MD_MARKCHAIN mark_chains[13];
+#define PTR_CHAIN (ctx->mark_chains[0])
+#define TABLECELLBOUNDARIES (ctx->mark_chains[1])
+#define ASTERISK_OPENERS_extraword_mod3_0 (ctx->mark_chains[2])
+#define ASTERISK_OPENERS_extraword_mod3_1 (ctx->mark_chains[3])
+#define ASTERISK_OPENERS_extraword_mod3_2 (ctx->mark_chains[4])
+#define ASTERISK_OPENERS_intraword_mod3_0 (ctx->mark_chains[5])
+#define ASTERISK_OPENERS_intraword_mod3_1 (ctx->mark_chains[6])
+#define ASTERISK_OPENERS_intraword_mod3_2 (ctx->mark_chains[7])
+#define UNDERSCORE_OPENERS (ctx->mark_chains[8])
+#define TILDE_OPENERS_1 (ctx->mark_chains[9])
+#define TILDE_OPENERS_2 (ctx->mark_chains[10])
+#define BRACKET_OPENERS (ctx->mark_chains[11])
+#define DOLLAR_OPENERS (ctx->mark_chains[12])
+#define OPENERS_CHAIN_FIRST 1
+#define OPENERS_CHAIN_LAST 12
+
+ int n_table_cell_boundaries;
+
+ /* For resolving links. */
+ int unresolved_link_head;
+ int unresolved_link_tail;
+
+ /* For resolving raw HTML. */
+ OFF html_comment_horizon;
+ OFF html_proc_instr_horizon;
+ OFF html_decl_horizon;
+ OFF html_cdata_horizon;
+
+ /* For block analysis.
+ * Notes:
+ * -- It holds MD_BLOCK as well as MD_LINE structures. After each
+ * MD_BLOCK, its (multiple) MD_LINE(s) follow.
+ * -- For MD_BLOCK_HTML and MD_BLOCK_CODE, MD_VERBATIMLINE(s) are used
+ * instead of MD_LINE(s).
+ */
+ void* block_bytes;
+ MD_BLOCK* current_block;
+ int n_block_bytes;
+ int alloc_block_bytes;
+
+ /* For container block analysis. */
+ MD_CONTAINER* containers;
+ int n_containers;
+ int alloc_containers;
+
+ /* Minimal indentation to call the block "indented code block". */
+ unsigned code_indent_offset;
+
+ /* Contextual info for line analysis. */
+ SZ code_fence_length; /* For checking closing fence length. */
+ int html_block_type; /* For checking closing raw HTML condition. */
+ int last_line_has_list_loosening_effect;
+ int last_list_item_starts_with_two_blank_lines;
+};
+
+enum MD_LINETYPE_tag {
+ MD_LINE_BLANK,
+ MD_LINE_HR,
+ MD_LINE_ATXHEADER,
+ MD_LINE_SETEXTHEADER,
+ MD_LINE_SETEXTUNDERLINE,
+ MD_LINE_INDENTEDCODE,
+ MD_LINE_FENCEDCODE,
+ MD_LINE_HTML,
+ MD_LINE_TEXT,
+ MD_LINE_TABLE,
+ MD_LINE_TABLEUNDERLINE
+};
+typedef enum MD_LINETYPE_tag MD_LINETYPE;
+
+typedef struct MD_LINE_ANALYSIS_tag MD_LINE_ANALYSIS;
+struct MD_LINE_ANALYSIS_tag {
+ MD_LINETYPE type : 16;
+ unsigned data : 16;
+ OFF beg;
+ OFF end;
+ unsigned indent; /* Indentation level. */
+};
+
+typedef struct MD_LINE_tag MD_LINE;
+struct MD_LINE_tag {
+ OFF beg;
+ OFF end;
+};
+
+typedef struct MD_VERBATIMLINE_tag MD_VERBATIMLINE;
+struct MD_VERBATIMLINE_tag {
+ OFF beg;
+ OFF end;
+ OFF indent;
+};
+
+
+/*****************
+ *** Helpers ***
+ *****************/
+
+/* Character accessors. */
+#define CH(off) (ctx->text[(off)])
+#define STR(off) (ctx->text + (off))
+
+/* Character classification.
+ * Note we assume ASCII compatibility of code points < 128 here. */
+#define ISIN_(ch, ch_min, ch_max) ((ch_min) <= (unsigned)(ch) && (unsigned)(ch) <= (ch_max))
+#define ISANYOF_(ch, palette) ((ch) != _T('\0') && md_strchr((palette), (ch)) != NULL)
+#define ISANYOF2_(ch, ch1, ch2) ((ch) == (ch1) || (ch) == (ch2))
+#define ISANYOF3_(ch, ch1, ch2, ch3) ((ch) == (ch1) || (ch) == (ch2) || (ch) == (ch3))
+#define ISASCII_(ch) ((unsigned)(ch) <= 127)
+#define ISBLANK_(ch) (ISANYOF2_((ch), _T(' '), _T('\t')))
+#define ISNEWLINE_(ch) (ISANYOF2_((ch), _T('\r'), _T('\n')))
+#define ISWHITESPACE_(ch) (ISBLANK_(ch) || ISANYOF2_((ch), _T('\v'), _T('\f')))
+#define ISCNTRL_(ch) ((unsigned)(ch) <= 31 || (unsigned)(ch) == 127)
+#define ISPUNCT_(ch) (ISIN_(ch, 33, 47) || ISIN_(ch, 58, 64) || ISIN_(ch, 91, 96) || ISIN_(ch, 123, 126))
+#define ISUPPER_(ch) (ISIN_(ch, _T('A'), _T('Z')))
+#define ISLOWER_(ch) (ISIN_(ch, _T('a'), _T('z')))
+#define ISALPHA_(ch) (ISUPPER_(ch) || ISLOWER_(ch))
+#define ISDIGIT_(ch) (ISIN_(ch, _T('0'), _T('9')))
+#define ISXDIGIT_(ch) (ISDIGIT_(ch) || ISIN_(ch, _T('A'), _T('F')) || ISIN_(ch, _T('a'), _T('f')))
+#define ISALNUM_(ch) (ISALPHA_(ch) || ISDIGIT_(ch))
+
+#define ISANYOF(off, palette) ISANYOF_(CH(off), (palette))
+#define ISANYOF2(off, ch1, ch2) ISANYOF2_(CH(off), (ch1), (ch2))
+#define ISANYOF3(off, ch1, ch2, ch3) ISANYOF3_(CH(off), (ch1), (ch2), (ch3))
+#define ISASCII(off) ISASCII_(CH(off))
+#define ISBLANK(off) ISBLANK_(CH(off))
+#define ISNEWLINE(off) ISNEWLINE_(CH(off))
+#define ISWHITESPACE(off) ISWHITESPACE_(CH(off))
+#define ISCNTRL(off) ISCNTRL_(CH(off))
+#define ISPUNCT(off) ISPUNCT_(CH(off))
+#define ISUPPER(off) ISUPPER_(CH(off))
+#define ISLOWER(off) ISLOWER_(CH(off))
+#define ISALPHA(off) ISALPHA_(CH(off))
+#define ISDIGIT(off) ISDIGIT_(CH(off))
+#define ISXDIGIT(off) ISXDIGIT_(CH(off))
+#define ISALNUM(off) ISALNUM_(CH(off))
+
+
+#if defined MD4C_USE_UTF16
+ #define md_strchr wcschr
+#else
+ #define md_strchr strchr
+#endif
+
+
+/* Case insensitive check of string equality. */
+static inline int
+md_ascii_case_eq(const CHAR* s1, const CHAR* s2, SZ n)
+{
+ OFF i;
+ for(i = 0; i < n; i++) {
+ CHAR ch1 = s1[i];
+ CHAR ch2 = s2[i];
+
+ if(ISLOWER_(ch1))
+ ch1 += ('A'-'a');
+ if(ISLOWER_(ch2))
+ ch2 += ('A'-'a');
+ if(ch1 != ch2)
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static inline int
+md_ascii_eq(const CHAR* s1, const CHAR* s2, SZ n)
+{
+ return memcmp(s1, s2, n * sizeof(CHAR)) == 0;
+}
+
+static int
+md_text_with_null_replacement(MD_CTX* ctx, MD_TEXTTYPE type, const CHAR* str, SZ size)
+{
+ OFF off = 0;
+ int ret = 0;
+
+ while(1) {
+ while(off < size && str[off] != _T('\0'))
+ off++;
+
+ if(off > 0) {
+ ret = ctx->parser.text(type, str, off, ctx->userdata);
+ if(ret != 0)
+ return ret;
+
+ str += off;
+ size -= off;
+ off = 0;
+ }
+
+ if(off >= size)
+ return 0;
+
+ ret = ctx->parser.text(MD_TEXT_NULLCHAR, _T(""), 1, ctx->userdata);
+ if(ret != 0)
+ return ret;
+ off++;
+ }
+}
+
+
+#define MD_CHECK(func) \
+ do { \
+ ret = (func); \
+ if(ret < 0) \
+ goto abort; \
+ } while(0)
+
+
+#define MD_TEMP_BUFFER(sz) \
+ do { \
+ if(sz > ctx->alloc_buffer) { \
+ CHAR* new_buffer; \
+ SZ new_size = ((sz) + (sz) / 2 + 128) & ~127; \
+ \
+ new_buffer = realloc(ctx->buffer, new_size); \
+ if(new_buffer == NULL) { \
+ MD_LOG("realloc() failed."); \
+ ret = -1; \
+ goto abort; \
+ } \
+ \
+ ctx->buffer = new_buffer; \
+ ctx->alloc_buffer = new_size; \
+ } \
+ } while(0)
+
+
+#define MD_ENTER_BLOCK(type, arg) \
+ do { \
+ ret = ctx->parser.enter_block((type), (arg), ctx->userdata); \
+ if(ret != 0) { \
+ MD_LOG("Aborted from enter_block() callback."); \
+ goto abort; \
+ } \
+ } while(0)
+
+#define MD_LEAVE_BLOCK(type, arg) \
+ do { \
+ ret = ctx->parser.leave_block((type), (arg), ctx->userdata); \
+ if(ret != 0) { \
+ MD_LOG("Aborted from leave_block() callback."); \
+ goto abort; \
+ } \
+ } while(0)
+
+#define MD_ENTER_SPAN(type, arg) \
+ do { \
+ ret = ctx->parser.enter_span((type), (arg), ctx->userdata); \
+ if(ret != 0) { \
+ MD_LOG("Aborted from enter_span() callback."); \
+ goto abort; \
+ } \
+ } while(0)
+
+#define MD_LEAVE_SPAN(type, arg) \
+ do { \
+ ret = ctx->parser.leave_span((type), (arg), ctx->userdata); \
+ if(ret != 0) { \
+ MD_LOG("Aborted from leave_span() callback."); \
+ goto abort; \
+ } \
+ } while(0)
+
+#define MD_TEXT(type, str, size) \
+ do { \
+ if(size > 0) { \
+ ret = ctx->parser.text((type), (str), (size), ctx->userdata); \
+ if(ret != 0) { \
+ MD_LOG("Aborted from text() callback."); \
+ goto abort; \
+ } \
+ } \
+ } while(0)
+
+#define MD_TEXT_INSECURE(type, str, size) \
+ do { \
+ if(size > 0) { \
+ ret = md_text_with_null_replacement(ctx, type, str, size); \
+ if(ret != 0) { \
+ MD_LOG("Aborted from text() callback."); \
+ goto abort; \
+ } \
+ } \
+ } while(0)
+
+
+/* If the offset falls into a gap between line, we return the following
+ * line. */
+static const MD_LINE*
+md_lookup_line(OFF off, const MD_LINE* lines, int n_lines)
+{
+ int lo, hi;
+ int pivot;
+ const MD_LINE* line;
+
+ lo = 0;
+ hi = n_lines - 1;
+ while(lo <= hi) {
+ pivot = (lo + hi) / 2;
+ line = &lines[pivot];
+
+ if(off < line->beg) {
+ hi = pivot - 1;
+ if(hi < 0 || lines[hi].end <= off)
+ return line;
+ } else if(off > line->end) {
+ lo = pivot + 1;
+ } else {
+ return line;
+ }
+ }
+
+ return NULL;
+}
+
+
+/*************************
+ *** Unicode Support ***
+ *************************/
+
+typedef struct MD_UNICODE_FOLD_INFO_tag MD_UNICODE_FOLD_INFO;
+struct MD_UNICODE_FOLD_INFO_tag {
+ unsigned codepoints[3];
+ unsigned n_codepoints;
+};
+
+
+#if defined MD4C_USE_UTF16 || defined MD4C_USE_UTF8
+ /* Binary search over sorted "map" of codepoints. Consecutive sequences
+ * of codepoints may be encoded in the map by just using the
+ * (MIN_CODEPOINT | 0x40000000) and (MAX_CODEPOINT | 0x80000000).
+ *
+ * Returns index of the found record in the map (in the case of ranges,
+ * the minimal value is used); or -1 on failure. */
+ static int
+ md_unicode_bsearch__(unsigned codepoint, const unsigned* map, size_t map_size)
+ {
+ int beg, end;
+ int pivot_beg, pivot_end;
+
+ beg = 0;
+ end = (int) map_size-1;
+ while(beg <= end) {
+ /* Pivot may be a range, not just a single value. */
+ pivot_beg = pivot_end = (beg + end) / 2;
+ if(map[pivot_end] & 0x40000000)
+ pivot_end++;
+ if(map[pivot_beg] & 0x80000000)
+ pivot_beg--;
+
+ if(codepoint < (map[pivot_beg] & 0x00ffffff))
+ end = pivot_beg - 1;
+ else if(codepoint > (map[pivot_end] & 0x00ffffff))
+ beg = pivot_end + 1;
+ else
+ return pivot_beg;
+ }
+
+ return -1;
+ }
+
+ static int
+ md_is_unicode_whitespace__(unsigned codepoint)
+ {
+#define R(cp_min, cp_max) ((cp_min) | 0x40000000), ((cp_max) | 0x80000000)
+#define S(cp) (cp)
+ /* Unicode "Zs" category.
+ * (generated by scripts/build_whitespace_map.py) */
+ static const unsigned WHITESPACE_MAP[] = {
+ S(0x0020), S(0x00a0), S(0x1680), R(0x2000,0x200a), S(0x202f), S(0x205f), S(0x3000)
+ };
+#undef R
+#undef S
+
+ /* The ASCII ones are the most frequently used ones, also CommonMark
+ * specification requests few more in this range. */
+ if(codepoint <= 0x7f)
+ return ISWHITESPACE_(codepoint);
+
+ return (md_unicode_bsearch__(codepoint, WHITESPACE_MAP, SIZEOF_ARRAY(WHITESPACE_MAP)) >= 0);
+ }
+
+ static int
+ md_is_unicode_punct__(unsigned codepoint)
+ {
+#define R(cp_min, cp_max) ((cp_min) | 0x40000000), ((cp_max) | 0x80000000)
+#define S(cp) (cp)
+ /* Unicode "Pc", "Pd", "Pe", "Pf", "Pi", "Po", "Ps" categories.
+ * (generated by scripts/build_punct_map.py) */
+ static const unsigned PUNCT_MAP[] = {
+ R(0x0021,0x0023), R(0x0025,0x002a), R(0x002c,0x002f), R(0x003a,0x003b), R(0x003f,0x0040),
+ R(0x005b,0x005d), S(0x005f), S(0x007b), S(0x007d), S(0x00a1), S(0x00a7), S(0x00ab), R(0x00b6,0x00b7),
+ S(0x00bb), S(0x00bf), S(0x037e), S(0x0387), R(0x055a,0x055f), R(0x0589,0x058a), S(0x05be), S(0x05c0),
+ S(0x05c3), S(0x05c6), R(0x05f3,0x05f4), R(0x0609,0x060a), R(0x060c,0x060d), S(0x061b), R(0x061e,0x061f),
+ R(0x066a,0x066d), S(0x06d4), R(0x0700,0x070d), R(0x07f7,0x07f9), R(0x0830,0x083e), S(0x085e),
+ R(0x0964,0x0965), S(0x0970), S(0x09fd), S(0x0a76), S(0x0af0), S(0x0c77), S(0x0c84), S(0x0df4), S(0x0e4f),
+ R(0x0e5a,0x0e5b), R(0x0f04,0x0f12), S(0x0f14), R(0x0f3a,0x0f3d), S(0x0f85), R(0x0fd0,0x0fd4),
+ R(0x0fd9,0x0fda), R(0x104a,0x104f), S(0x10fb), R(0x1360,0x1368), S(0x1400), S(0x166e), R(0x169b,0x169c),
+ R(0x16eb,0x16ed), R(0x1735,0x1736), R(0x17d4,0x17d6), R(0x17d8,0x17da), R(0x1800,0x180a),
+ R(0x1944,0x1945), R(0x1a1e,0x1a1f), R(0x1aa0,0x1aa6), R(0x1aa8,0x1aad), R(0x1b5a,0x1b60),
+ R(0x1bfc,0x1bff), R(0x1c3b,0x1c3f), R(0x1c7e,0x1c7f), R(0x1cc0,0x1cc7), S(0x1cd3), R(0x2010,0x2027),
+ R(0x2030,0x2043), R(0x2045,0x2051), R(0x2053,0x205e), R(0x207d,0x207e), R(0x208d,0x208e),
+ R(0x2308,0x230b), R(0x2329,0x232a), R(0x2768,0x2775), R(0x27c5,0x27c6), R(0x27e6,0x27ef),
+ R(0x2983,0x2998), R(0x29d8,0x29db), R(0x29fc,0x29fd), R(0x2cf9,0x2cfc), R(0x2cfe,0x2cff), S(0x2d70),
+ R(0x2e00,0x2e2e), R(0x2e30,0x2e4f), S(0x2e52), R(0x3001,0x3003), R(0x3008,0x3011), R(0x3014,0x301f),
+ S(0x3030), S(0x303d), S(0x30a0), S(0x30fb), R(0xa4fe,0xa4ff), R(0xa60d,0xa60f), S(0xa673), S(0xa67e),
+ R(0xa6f2,0xa6f7), R(0xa874,0xa877), R(0xa8ce,0xa8cf), R(0xa8f8,0xa8fa), S(0xa8fc), R(0xa92e,0xa92f),
+ S(0xa95f), R(0xa9c1,0xa9cd), R(0xa9de,0xa9df), R(0xaa5c,0xaa5f), R(0xaade,0xaadf), R(0xaaf0,0xaaf1),
+ S(0xabeb), R(0xfd3e,0xfd3f), R(0xfe10,0xfe19), R(0xfe30,0xfe52), R(0xfe54,0xfe61), S(0xfe63), S(0xfe68),
+ R(0xfe6a,0xfe6b), R(0xff01,0xff03), R(0xff05,0xff0a), R(0xff0c,0xff0f), R(0xff1a,0xff1b),
+ R(0xff1f,0xff20), R(0xff3b,0xff3d), S(0xff3f), S(0xff5b), S(0xff5d), R(0xff5f,0xff65), R(0x10100,0x10102),
+ S(0x1039f), S(0x103d0), S(0x1056f), S(0x10857), S(0x1091f), S(0x1093f), R(0x10a50,0x10a58), S(0x10a7f),
+ R(0x10af0,0x10af6), R(0x10b39,0x10b3f), R(0x10b99,0x10b9c), S(0x10ead), R(0x10f55,0x10f59),
+ R(0x11047,0x1104d), R(0x110bb,0x110bc), R(0x110be,0x110c1), R(0x11140,0x11143), R(0x11174,0x11175),
+ R(0x111c5,0x111c8), S(0x111cd), S(0x111db), R(0x111dd,0x111df), R(0x11238,0x1123d), S(0x112a9),
+ R(0x1144b,0x1144f), R(0x1145a,0x1145b), S(0x1145d), S(0x114c6), R(0x115c1,0x115d7), R(0x11641,0x11643),
+ R(0x11660,0x1166c), R(0x1173c,0x1173e), S(0x1183b), R(0x11944,0x11946), S(0x119e2), R(0x11a3f,0x11a46),
+ R(0x11a9a,0x11a9c), R(0x11a9e,0x11aa2), R(0x11c41,0x11c45), R(0x11c70,0x11c71), R(0x11ef7,0x11ef8),
+ S(0x11fff), R(0x12470,0x12474), R(0x16a6e,0x16a6f), S(0x16af5), R(0x16b37,0x16b3b), S(0x16b44),
+ R(0x16e97,0x16e9a), S(0x16fe2), S(0x1bc9f), R(0x1da87,0x1da8b), R(0x1e95e,0x1e95f)
+ };
+#undef R
+#undef S
+
+ /* The ASCII ones are the most frequently used ones, also CommonMark
+ * specification requests few more in this range. */
+ if(codepoint <= 0x7f)
+ return ISPUNCT_(codepoint);
+
+ return (md_unicode_bsearch__(codepoint, PUNCT_MAP, SIZEOF_ARRAY(PUNCT_MAP)) >= 0);
+ }
+
+ static void
+ md_get_unicode_fold_info(unsigned codepoint, MD_UNICODE_FOLD_INFO* info)
+ {
+#define R(cp_min, cp_max) ((cp_min) | 0x40000000), ((cp_max) | 0x80000000)
+#define S(cp) (cp)
+ /* Unicode "Pc", "Pd", "Pe", "Pf", "Pi", "Po", "Ps" categories.
+ * (generated by scripts/build_folding_map.py) */
+ static const unsigned FOLD_MAP_1[] = {
+ R(0x0041,0x005a), S(0x00b5), R(0x00c0,0x00d6), R(0x00d8,0x00de), R(0x0100,0x012e), R(0x0132,0x0136),
+ R(0x0139,0x0147), R(0x014a,0x0176), S(0x0178), R(0x0179,0x017d), S(0x017f), S(0x0181), S(0x0182),
+ S(0x0184), S(0x0186), S(0x0187), S(0x0189), S(0x018a), S(0x018b), S(0x018e), S(0x018f), S(0x0190),
+ S(0x0191), S(0x0193), S(0x0194), S(0x0196), S(0x0197), S(0x0198), S(0x019c), S(0x019d), S(0x019f),
+ R(0x01a0,0x01a4), S(0x01a6), S(0x01a7), S(0x01a9), S(0x01ac), S(0x01ae), S(0x01af), S(0x01b1), S(0x01b2),
+ S(0x01b3), S(0x01b5), S(0x01b7), S(0x01b8), S(0x01bc), S(0x01c4), S(0x01c5), S(0x01c7), S(0x01c8),
+ S(0x01ca), R(0x01cb,0x01db), R(0x01de,0x01ee), S(0x01f1), S(0x01f2), S(0x01f4), S(0x01f6), S(0x01f7),
+ R(0x01f8,0x021e), S(0x0220), R(0x0222,0x0232), S(0x023a), S(0x023b), S(0x023d), S(0x023e), S(0x0241),
+ S(0x0243), S(0x0244), S(0x0245), R(0x0246,0x024e), S(0x0345), S(0x0370), S(0x0372), S(0x0376), S(0x037f),
+ S(0x0386), R(0x0388,0x038a), S(0x038c), S(0x038e), S(0x038f), R(0x0391,0x03a1), R(0x03a3,0x03ab),
+ S(0x03c2), S(0x03cf), S(0x03d0), S(0x03d1), S(0x03d5), S(0x03d6), R(0x03d8,0x03ee), S(0x03f0), S(0x03f1),
+ S(0x03f4), S(0x03f5), S(0x03f7), S(0x03f9), S(0x03fa), R(0x03fd,0x03ff), R(0x0400,0x040f),
+ R(0x0410,0x042f), R(0x0460,0x0480), R(0x048a,0x04be), S(0x04c0), R(0x04c1,0x04cd), R(0x04d0,0x052e),
+ R(0x0531,0x0556), R(0x10a0,0x10c5), S(0x10c7), S(0x10cd), R(0x13f8,0x13fd), S(0x1c80), S(0x1c81),
+ S(0x1c82), S(0x1c83), S(0x1c84), S(0x1c85), S(0x1c86), S(0x1c87), S(0x1c88), R(0x1c90,0x1cba),
+ R(0x1cbd,0x1cbf), R(0x1e00,0x1e94), S(0x1e9b), R(0x1ea0,0x1efe), R(0x1f08,0x1f0f), R(0x1f18,0x1f1d),
+ R(0x1f28,0x1f2f), R(0x1f38,0x1f3f), R(0x1f48,0x1f4d), S(0x1f59), S(0x1f5b), S(0x1f5d), S(0x1f5f),
+ R(0x1f68,0x1f6f), S(0x1fb8), S(0x1fb9), S(0x1fba), S(0x1fbb), S(0x1fbe), R(0x1fc8,0x1fcb), S(0x1fd8),
+ S(0x1fd9), S(0x1fda), S(0x1fdb), S(0x1fe8), S(0x1fe9), S(0x1fea), S(0x1feb), S(0x1fec), S(0x1ff8),
+ S(0x1ff9), S(0x1ffa), S(0x1ffb), S(0x2126), S(0x212a), S(0x212b), S(0x2132), R(0x2160,0x216f), S(0x2183),
+ R(0x24b6,0x24cf), R(0x2c00,0x2c2e), S(0x2c60), S(0x2c62), S(0x2c63), S(0x2c64), R(0x2c67,0x2c6b),
+ S(0x2c6d), S(0x2c6e), S(0x2c6f), S(0x2c70), S(0x2c72), S(0x2c75), S(0x2c7e), S(0x2c7f), R(0x2c80,0x2ce2),
+ S(0x2ceb), S(0x2ced), S(0x2cf2), R(0xa640,0xa66c), R(0xa680,0xa69a), R(0xa722,0xa72e), R(0xa732,0xa76e),
+ S(0xa779), S(0xa77b), S(0xa77d), R(0xa77e,0xa786), S(0xa78b), S(0xa78d), S(0xa790), S(0xa792),
+ R(0xa796,0xa7a8), S(0xa7aa), S(0xa7ab), S(0xa7ac), S(0xa7ad), S(0xa7ae), S(0xa7b0), S(0xa7b1), S(0xa7b2),
+ S(0xa7b3), R(0xa7b4,0xa7be), S(0xa7c2), S(0xa7c4), S(0xa7c5), S(0xa7c6), S(0xa7c7), S(0xa7c9), S(0xa7f5),
+ R(0xab70,0xabbf), R(0xff21,0xff3a), R(0x10400,0x10427), R(0x104b0,0x104d3), R(0x10c80,0x10cb2),
+ R(0x118a0,0x118bf), R(0x16e40,0x16e5f), R(0x1e900,0x1e921)
+ };
+ static const unsigned FOLD_MAP_1_DATA[] = {
+ 0x0061, 0x007a, 0x03bc, 0x00e0, 0x00f6, 0x00f8, 0x00fe, 0x0101, 0x012f, 0x0133, 0x0137, 0x013a, 0x0148,
+ 0x014b, 0x0177, 0x00ff, 0x017a, 0x017e, 0x0073, 0x0253, 0x0183, 0x0185, 0x0254, 0x0188, 0x0256, 0x0257,
+ 0x018c, 0x01dd, 0x0259, 0x025b, 0x0192, 0x0260, 0x0263, 0x0269, 0x0268, 0x0199, 0x026f, 0x0272, 0x0275,
+ 0x01a1, 0x01a5, 0x0280, 0x01a8, 0x0283, 0x01ad, 0x0288, 0x01b0, 0x028a, 0x028b, 0x01b4, 0x01b6, 0x0292,
+ 0x01b9, 0x01bd, 0x01c6, 0x01c6, 0x01c9, 0x01c9, 0x01cc, 0x01cc, 0x01dc, 0x01df, 0x01ef, 0x01f3, 0x01f3,
+ 0x01f5, 0x0195, 0x01bf, 0x01f9, 0x021f, 0x019e, 0x0223, 0x0233, 0x2c65, 0x023c, 0x019a, 0x2c66, 0x0242,
+ 0x0180, 0x0289, 0x028c, 0x0247, 0x024f, 0x03b9, 0x0371, 0x0373, 0x0377, 0x03f3, 0x03ac, 0x03ad, 0x03af,
+ 0x03cc, 0x03cd, 0x03ce, 0x03b1, 0x03c1, 0x03c3, 0x03cb, 0x03c3, 0x03d7, 0x03b2, 0x03b8, 0x03c6, 0x03c0,
+ 0x03d9, 0x03ef, 0x03ba, 0x03c1, 0x03b8, 0x03b5, 0x03f8, 0x03f2, 0x03fb, 0x037b, 0x037d, 0x0450, 0x045f,
+ 0x0430, 0x044f, 0x0461, 0x0481, 0x048b, 0x04bf, 0x04cf, 0x04c2, 0x04ce, 0x04d1, 0x052f, 0x0561, 0x0586,
+ 0x2d00, 0x2d25, 0x2d27, 0x2d2d, 0x13f0, 0x13f5, 0x0432, 0x0434, 0x043e, 0x0441, 0x0442, 0x0442, 0x044a,
+ 0x0463, 0xa64b, 0x10d0, 0x10fa, 0x10fd, 0x10ff, 0x1e01, 0x1e95, 0x1e61, 0x1ea1, 0x1eff, 0x1f00, 0x1f07,
+ 0x1f10, 0x1f15, 0x1f20, 0x1f27, 0x1f30, 0x1f37, 0x1f40, 0x1f45, 0x1f51, 0x1f53, 0x1f55, 0x1f57, 0x1f60,
+ 0x1f67, 0x1fb0, 0x1fb1, 0x1f70, 0x1f71, 0x03b9, 0x1f72, 0x1f75, 0x1fd0, 0x1fd1, 0x1f76, 0x1f77, 0x1fe0,
+ 0x1fe1, 0x1f7a, 0x1f7b, 0x1fe5, 0x1f78, 0x1f79, 0x1f7c, 0x1f7d, 0x03c9, 0x006b, 0x00e5, 0x214e, 0x2170,
+ 0x217f, 0x2184, 0x24d0, 0x24e9, 0x2c30, 0x2c5e, 0x2c61, 0x026b, 0x1d7d, 0x027d, 0x2c68, 0x2c6c, 0x0251,
+ 0x0271, 0x0250, 0x0252, 0x2c73, 0x2c76, 0x023f, 0x0240, 0x2c81, 0x2ce3, 0x2cec, 0x2cee, 0x2cf3, 0xa641,
+ 0xa66d, 0xa681, 0xa69b, 0xa723, 0xa72f, 0xa733, 0xa76f, 0xa77a, 0xa77c, 0x1d79, 0xa77f, 0xa787, 0xa78c,
+ 0x0265, 0xa791, 0xa793, 0xa797, 0xa7a9, 0x0266, 0x025c, 0x0261, 0x026c, 0x026a, 0x029e, 0x0287, 0x029d,
+ 0xab53, 0xa7b5, 0xa7bf, 0xa7c3, 0xa794, 0x0282, 0x1d8e, 0xa7c8, 0xa7ca, 0xa7f6, 0x13a0, 0x13ef, 0xff41,
+ 0xff5a, 0x10428, 0x1044f, 0x104d8, 0x104fb, 0x10cc0, 0x10cf2, 0x118c0, 0x118df, 0x16e60, 0x16e7f, 0x1e922,
+ 0x1e943
+ };
+ static const unsigned FOLD_MAP_2[] = {
+ S(0x00df), S(0x0130), S(0x0149), S(0x01f0), S(0x0587), S(0x1e96), S(0x1e97), S(0x1e98), S(0x1e99),
+ S(0x1e9a), S(0x1e9e), S(0x1f50), R(0x1f80,0x1f87), R(0x1f88,0x1f8f), R(0x1f90,0x1f97), R(0x1f98,0x1f9f),
+ R(0x1fa0,0x1fa7), R(0x1fa8,0x1faf), S(0x1fb2), S(0x1fb3), S(0x1fb4), S(0x1fb6), S(0x1fbc), S(0x1fc2),
+ S(0x1fc3), S(0x1fc4), S(0x1fc6), S(0x1fcc), S(0x1fd6), S(0x1fe4), S(0x1fe6), S(0x1ff2), S(0x1ff3),
+ S(0x1ff4), S(0x1ff6), S(0x1ffc), S(0xfb00), S(0xfb01), S(0xfb02), S(0xfb05), S(0xfb06), S(0xfb13),
+ S(0xfb14), S(0xfb15), S(0xfb16), S(0xfb17)
+ };
+ static const unsigned FOLD_MAP_2_DATA[] = {
+ 0x0073,0x0073, 0x0069,0x0307, 0x02bc,0x006e, 0x006a,0x030c, 0x0565,0x0582, 0x0068,0x0331, 0x0074,0x0308,
+ 0x0077,0x030a, 0x0079,0x030a, 0x0061,0x02be, 0x0073,0x0073, 0x03c5,0x0313, 0x1f00,0x03b9, 0x1f07,0x03b9,
+ 0x1f00,0x03b9, 0x1f07,0x03b9, 0x1f20,0x03b9, 0x1f27,0x03b9, 0x1f20,0x03b9, 0x1f27,0x03b9, 0x1f60,0x03b9,
+ 0x1f67,0x03b9, 0x1f60,0x03b9, 0x1f67,0x03b9, 0x1f70,0x03b9, 0x03b1,0x03b9, 0x03ac,0x03b9, 0x03b1,0x0342,
+ 0x03b1,0x03b9, 0x1f74,0x03b9, 0x03b7,0x03b9, 0x03ae,0x03b9, 0x03b7,0x0342, 0x03b7,0x03b9, 0x03b9,0x0342,
+ 0x03c1,0x0313, 0x03c5,0x0342, 0x1f7c,0x03b9, 0x03c9,0x03b9, 0x03ce,0x03b9, 0x03c9,0x0342, 0x03c9,0x03b9,
+ 0x0066,0x0066, 0x0066,0x0069, 0x0066,0x006c, 0x0073,0x0074, 0x0073,0x0074, 0x0574,0x0576, 0x0574,0x0565,
+ 0x0574,0x056b, 0x057e,0x0576, 0x0574,0x056d
+ };
+ static const unsigned FOLD_MAP_3[] = {
+ S(0x0390), S(0x03b0), S(0x1f52), S(0x1f54), S(0x1f56), S(0x1fb7), S(0x1fc7), S(0x1fd2), S(0x1fd3),
+ S(0x1fd7), S(0x1fe2), S(0x1fe3), S(0x1fe7), S(0x1ff7), S(0xfb03), S(0xfb04)
+ };
+ static const unsigned FOLD_MAP_3_DATA[] = {
+ 0x03b9,0x0308,0x0301, 0x03c5,0x0308,0x0301, 0x03c5,0x0313,0x0300, 0x03c5,0x0313,0x0301,
+ 0x03c5,0x0313,0x0342, 0x03b1,0x0342,0x03b9, 0x03b7,0x0342,0x03b9, 0x03b9,0x0308,0x0300,
+ 0x03b9,0x0308,0x0301, 0x03b9,0x0308,0x0342, 0x03c5,0x0308,0x0300, 0x03c5,0x0308,0x0301,
+ 0x03c5,0x0308,0x0342, 0x03c9,0x0342,0x03b9, 0x0066,0x0066,0x0069, 0x0066,0x0066,0x006c
+ };
+#undef R
+#undef S
+ static const struct {
+ const unsigned* map;
+ const unsigned* data;
+ size_t map_size;
+ unsigned n_codepoints;
+ } FOLD_MAP_LIST[] = {
+ { FOLD_MAP_1, FOLD_MAP_1_DATA, SIZEOF_ARRAY(FOLD_MAP_1), 1 },
+ { FOLD_MAP_2, FOLD_MAP_2_DATA, SIZEOF_ARRAY(FOLD_MAP_2), 2 },
+ { FOLD_MAP_3, FOLD_MAP_3_DATA, SIZEOF_ARRAY(FOLD_MAP_3), 3 }
+ };
+
+ int i;
+
+ /* Fast path for ASCII characters. */
+ if(codepoint <= 0x7f) {
+ info->codepoints[0] = codepoint;
+ if(ISUPPER_(codepoint))
+ info->codepoints[0] += 'a' - 'A';
+ info->n_codepoints = 1;
+ return;
+ }
+
+ /* Try to locate the codepoint in any of the maps. */
+ for(i = 0; i < (int) SIZEOF_ARRAY(FOLD_MAP_LIST); i++) {
+ int index;
+
+ index = md_unicode_bsearch__(codepoint, FOLD_MAP_LIST[i].map, FOLD_MAP_LIST[i].map_size);
+ if(index >= 0) {
+ /* Found the mapping. */
+ unsigned n_codepoints = FOLD_MAP_LIST[i].n_codepoints;
+ const unsigned* map = FOLD_MAP_LIST[i].map;
+ const unsigned* codepoints = FOLD_MAP_LIST[i].data + (index * n_codepoints);
+
+ memcpy(info->codepoints, codepoints, sizeof(unsigned) * n_codepoints);
+ info->n_codepoints = n_codepoints;
+
+ if(FOLD_MAP_LIST[i].map[index] != codepoint) {
+ /* The found mapping maps whole range of codepoints,
+ * i.e. we have to offset info->codepoints[0] accordingly. */
+ if((map[index] & 0x00ffffff)+1 == codepoints[0]) {
+ /* Alternating type of the range. */
+ info->codepoints[0] = codepoint + ((codepoint & 0x1) == (map[index] & 0x1) ? 1 : 0);
+ } else {
+ /* Range to range kind of mapping. */
+ info->codepoints[0] += (codepoint - (map[index] & 0x00ffffff));
+ }
+ }
+
+ return;
+ }
+ }
+
+ /* No mapping found. Map the codepoint to itself. */
+ info->codepoints[0] = codepoint;
+ info->n_codepoints = 1;
+ }
+#endif
+
+
+#if defined MD4C_USE_UTF16
+ #define IS_UTF16_SURROGATE_HI(word) (((WORD)(word) & 0xfc00) == 0xd800)
+ #define IS_UTF16_SURROGATE_LO(word) (((WORD)(word) & 0xfc00) == 0xdc00)
+ #define UTF16_DECODE_SURROGATE(hi, lo) (0x10000 + ((((unsigned)(hi) & 0x3ff) << 10) | (((unsigned)(lo) & 0x3ff) << 0)))
+
+ static unsigned
+ md_decode_utf16le__(const CHAR* str, SZ str_size, SZ* p_size)
+ {
+ if(IS_UTF16_SURROGATE_HI(str[0])) {
+ if(1 < str_size && IS_UTF16_SURROGATE_LO(str[1])) {
+ if(p_size != NULL)
+ *p_size = 2;
+ return UTF16_DECODE_SURROGATE(str[0], str[1]);
+ }
+ }
+
+ if(p_size != NULL)
+ *p_size = 1;
+ return str[0];
+ }
+
+ static unsigned
+ md_decode_utf16le_before__(MD_CTX* ctx, OFF off)
+ {
+ if(off > 2 && IS_UTF16_SURROGATE_HI(CH(off-2)) && IS_UTF16_SURROGATE_LO(CH(off-1)))
+ return UTF16_DECODE_SURROGATE(CH(off-2), CH(off-1));
+
+ return CH(off);
+ }
+
+ /* No whitespace uses surrogates, so no decoding needed here. */
+ #define ISUNICODEWHITESPACE_(codepoint) md_is_unicode_whitespace__(codepoint)
+ #define ISUNICODEWHITESPACE(off) md_is_unicode_whitespace__(CH(off))
+ #define ISUNICODEWHITESPACEBEFORE(off) md_is_unicode_whitespace__(CH((off)-1))
+
+ #define ISUNICODEPUNCT(off) md_is_unicode_punct__(md_decode_utf16le__(STR(off), ctx->size - (off), NULL))
+ #define ISUNICODEPUNCTBEFORE(off) md_is_unicode_punct__(md_decode_utf16le_before__(ctx, off))
+
+ static inline int
+ md_decode_unicode(const CHAR* str, OFF off, SZ str_size, SZ* p_char_size)
+ {
+ return md_decode_utf16le__(str+off, str_size-off, p_char_size);
+ }
+#elif defined MD4C_USE_UTF8
+ #define IS_UTF8_LEAD1(byte) ((unsigned char)(byte) <= 0x7f)
+ #define IS_UTF8_LEAD2(byte) (((unsigned char)(byte) & 0xe0) == 0xc0)
+ #define IS_UTF8_LEAD3(byte) (((unsigned char)(byte) & 0xf0) == 0xe0)
+ #define IS_UTF8_LEAD4(byte) (((unsigned char)(byte) & 0xf8) == 0xf0)
+ #define IS_UTF8_TAIL(byte) (((unsigned char)(byte) & 0xc0) == 0x80)
+
+ static unsigned
+ md_decode_utf8__(const CHAR* str, SZ str_size, SZ* p_size)
+ {
+ if(!IS_UTF8_LEAD1(str[0])) {
+ if(IS_UTF8_LEAD2(str[0])) {
+ if(1 < str_size && IS_UTF8_TAIL(str[1])) {
+ if(p_size != NULL)
+ *p_size = 2;
+
+ return (((unsigned int)str[0] & 0x1f) << 6) |
+ (((unsigned int)str[1] & 0x3f) << 0);
+ }
+ } else if(IS_UTF8_LEAD3(str[0])) {
+ if(2 < str_size && IS_UTF8_TAIL(str[1]) && IS_UTF8_TAIL(str[2])) {
+ if(p_size != NULL)
+ *p_size = 3;
+
+ return (((unsigned int)str[0] & 0x0f) << 12) |
+ (((unsigned int)str[1] & 0x3f) << 6) |
+ (((unsigned int)str[2] & 0x3f) << 0);
+ }
+ } else if(IS_UTF8_LEAD4(str[0])) {
+ if(3 < str_size && IS_UTF8_TAIL(str[1]) && IS_UTF8_TAIL(str[2]) && IS_UTF8_TAIL(str[3])) {
+ if(p_size != NULL)
+ *p_size = 4;
+
+ return (((unsigned int)str[0] & 0x07) << 18) |
+ (((unsigned int)str[1] & 0x3f) << 12) |
+ (((unsigned int)str[2] & 0x3f) << 6) |
+ (((unsigned int)str[3] & 0x3f) << 0);
+ }
+ }
+ }
+
+ if(p_size != NULL)
+ *p_size = 1;
+ return (unsigned) str[0];
+ }
+
+ static unsigned
+ md_decode_utf8_before__(MD_CTX* ctx, OFF off)
+ {
+ if(!IS_UTF8_LEAD1(CH(off-1))) {
+ if(off > 1 && IS_UTF8_LEAD2(CH(off-2)) && IS_UTF8_TAIL(CH(off-1)))
+ return (((unsigned int)CH(off-2) & 0x1f) << 6) |
+ (((unsigned int)CH(off-1) & 0x3f) << 0);
+
+ if(off > 2 && IS_UTF8_LEAD3(CH(off-3)) && IS_UTF8_TAIL(CH(off-2)) && IS_UTF8_TAIL(CH(off-1)))
+ return (((unsigned int)CH(off-3) & 0x0f) << 12) |
+ (((unsigned int)CH(off-2) & 0x3f) << 6) |
+ (((unsigned int)CH(off-1) & 0x3f) << 0);
+
+ if(off > 3 && IS_UTF8_LEAD4(CH(off-4)) && IS_UTF8_TAIL(CH(off-3)) && IS_UTF8_TAIL(CH(off-2)) && IS_UTF8_TAIL(CH(off-1)))
+ return (((unsigned int)CH(off-4) & 0x07) << 18) |
+ (((unsigned int)CH(off-3) & 0x3f) << 12) |
+ (((unsigned int)CH(off-2) & 0x3f) << 6) |
+ (((unsigned int)CH(off-1) & 0x3f) << 0);
+ }
+
+ return (unsigned) CH(off-1);
+ }
+
+ #define ISUNICODEWHITESPACE_(codepoint) md_is_unicode_whitespace__(codepoint)
+ #define ISUNICODEWHITESPACE(off) md_is_unicode_whitespace__(md_decode_utf8__(STR(off), ctx->size - (off), NULL))
+ #define ISUNICODEWHITESPACEBEFORE(off) md_is_unicode_whitespace__(md_decode_utf8_before__(ctx, off))
+
+ #define ISUNICODEPUNCT(off) md_is_unicode_punct__(md_decode_utf8__(STR(off), ctx->size - (off), NULL))
+ #define ISUNICODEPUNCTBEFORE(off) md_is_unicode_punct__(md_decode_utf8_before__(ctx, off))
+
+ static inline unsigned
+ md_decode_unicode(const CHAR* str, OFF off, SZ str_size, SZ* p_char_size)
+ {
+ return md_decode_utf8__(str+off, str_size-off, p_char_size);
+ }
+#else
+ #define ISUNICODEWHITESPACE_(codepoint) ISWHITESPACE_(codepoint)
+ #define ISUNICODEWHITESPACE(off) ISWHITESPACE(off)
+ #define ISUNICODEWHITESPACEBEFORE(off) ISWHITESPACE((off)-1)
+
+ #define ISUNICODEPUNCT(off) ISPUNCT(off)
+ #define ISUNICODEPUNCTBEFORE(off) ISPUNCT((off)-1)
+
+ static inline void
+ md_get_unicode_fold_info(unsigned codepoint, MD_UNICODE_FOLD_INFO* info)
+ {
+ info->codepoints[0] = codepoint;
+ if(ISUPPER_(codepoint))
+ info->codepoints[0] += 'a' - 'A';
+ info->n_codepoints = 1;
+ }
+
+ static inline unsigned
+ md_decode_unicode(const CHAR* str, OFF off, SZ str_size, SZ* p_size)
+ {
+ *p_size = 1;
+ return (unsigned) str[off];
+ }
+#endif
+
+
+/*************************************
+ *** Helper string manipulations ***
+ *************************************/
+
+/* Fill buffer with copy of the string between 'beg' and 'end' but replace any
+ * line breaks with given replacement character.
+ *
+ * NOTE: Caller is responsible to make sure the buffer is large enough.
+ * (Given the output is always shorter then input, (end - beg) is good idea
+ * what the caller should allocate.)
+ */
+static void
+md_merge_lines(MD_CTX* ctx, OFF beg, OFF end, const MD_LINE* lines, int n_lines,
+ CHAR line_break_replacement_char, CHAR* buffer, SZ* p_size)
+{
+ CHAR* ptr = buffer;
+ int line_index = 0;
+ OFF off = beg;
+
+ MD_UNUSED(n_lines);
+
+ while(1) {
+ const MD_LINE* line = &lines[line_index];
+ OFF line_end = line->end;
+ if(end < line_end)
+ line_end = end;
+
+ while(off < line_end) {
+ *ptr = CH(off);
+ ptr++;
+ off++;
+ }
+
+ if(off >= end) {
+ *p_size = (MD_SIZE)(ptr - buffer);
+ return;
+ }
+
+ *ptr = line_break_replacement_char;
+ ptr++;
+
+ line_index++;
+ off = lines[line_index].beg;
+ }
+}
+
+/* Wrapper of md_merge_lines() which allocates new buffer for the output string.
+ */
+static int
+md_merge_lines_alloc(MD_CTX* ctx, OFF beg, OFF end, const MD_LINE* lines, int n_lines,
+ CHAR line_break_replacement_char, CHAR** p_str, SZ* p_size)
+{
+ CHAR* buffer;
+
+ buffer = (CHAR*) malloc(sizeof(CHAR) * (end - beg));
+ if(buffer == NULL) {
+ MD_LOG("malloc() failed.");
+ return -1;
+ }
+
+ md_merge_lines(ctx, beg, end, lines, n_lines,
+ line_break_replacement_char, buffer, p_size);
+
+ *p_str = buffer;
+ return 0;
+}
+
+static OFF
+md_skip_unicode_whitespace(const CHAR* label, OFF off, SZ size)
+{
+ SZ char_size;
+ unsigned codepoint;
+
+ while(off < size) {
+ codepoint = md_decode_unicode(label, off, size, &char_size);
+ if(!ISUNICODEWHITESPACE_(codepoint) && !ISNEWLINE_(label[off]))
+ break;
+ off += char_size;
+ }
+
+ return off;
+}
+
+
+/******************************
+ *** Recognizing raw HTML ***
+ ******************************/
+
+/* md_is_html_tag() may be called when processing inlines (inline raw HTML)
+ * or when breaking document to blocks (checking for start of HTML block type 7).
+ *
+ * When breaking document to blocks, we do not yet know line boundaries, but
+ * in that case the whole tag has to live on a single line. We distinguish this
+ * by n_lines == 0.
+ */
+static int
+md_is_html_tag(MD_CTX* ctx, const MD_LINE* lines, int n_lines, OFF beg, OFF max_end, OFF* p_end)
+{
+ int attr_state;
+ OFF off = beg;
+ OFF line_end = (n_lines > 0) ? lines[0].end : ctx->size;
+ int i = 0;
+
+ MD_ASSERT(CH(beg) == _T('<'));
+
+ if(off + 1 >= line_end)
+ return FALSE;
+ off++;
+
+ /* For parsing attributes, we need a little state automaton below.
+ * State -1: no attributes are allowed.
+ * State 0: attribute could follow after some whitespace.
+ * State 1: after a whitespace (attribute name may follow).
+ * State 2: after attribute name ('=' MAY follow).
+ * State 3: after '=' (value specification MUST follow).
+ * State 41: in middle of unquoted attribute value.
+ * State 42: in middle of single-quoted attribute value.
+ * State 43: in middle of double-quoted attribute value.
+ */
+ attr_state = 0;
+
+ if(CH(off) == _T('/')) {
+ /* Closer tag "</ ... >". No attributes may be present. */
+ attr_state = -1;
+ off++;
+ }
+
+ /* Tag name */
+ if(off >= line_end || !ISALPHA(off))
+ return FALSE;
+ off++;
+ while(off < line_end && (ISALNUM(off) || CH(off) == _T('-')))
+ off++;
+
+ /* (Optional) attributes (if not closer), (optional) '/' (if not closer)
+ * and final '>'. */
+ while(1) {
+ while(off < line_end && !ISNEWLINE(off)) {
+ if(attr_state > 40) {
+ if(attr_state == 41 && (ISBLANK(off) || ISANYOF(off, _T("\"'=<>`")))) {
+ attr_state = 0;
+ off--; /* Put the char back for re-inspection in the new state. */
+ } else if(attr_state == 42 && CH(off) == _T('\'')) {
+ attr_state = 0;
+ } else if(attr_state == 43 && CH(off) == _T('"')) {
+ attr_state = 0;
+ }
+ off++;
+ } else if(ISWHITESPACE(off)) {
+ if(attr_state == 0)
+ attr_state = 1;
+ off++;
+ } else if(attr_state <= 2 && CH(off) == _T('>')) {
+ /* End. */
+ goto done;
+ } else if(attr_state <= 2 && CH(off) == _T('/') && off+1 < line_end && CH(off+1) == _T('>')) {
+ /* End with digraph '/>' */
+ off++;
+ goto done;
+ } else if((attr_state == 1 || attr_state == 2) && (ISALPHA(off) || CH(off) == _T('_') || CH(off) == _T(':'))) {
+ off++;
+ /* Attribute name */
+ while(off < line_end && (ISALNUM(off) || ISANYOF(off, _T("_.:-"))))
+ off++;
+ attr_state = 2;
+ } else if(attr_state == 2 && CH(off) == _T('=')) {
+ /* Attribute assignment sign */
+ off++;
+ attr_state = 3;
+ } else if(attr_state == 3) {
+ /* Expecting start of attribute value. */
+ if(CH(off) == _T('"'))
+ attr_state = 43;
+ else if(CH(off) == _T('\''))
+ attr_state = 42;
+ else if(!ISANYOF(off, _T("\"'=<>`")) && !ISNEWLINE(off))
+ attr_state = 41;
+ else
+ return FALSE;
+ off++;
+ } else {
+ /* Anything unexpected. */
+ return FALSE;
+ }
+ }
+
+ /* We have to be on a single line. See definition of start condition
+ * of HTML block, type 7. */
+ if(n_lines == 0)
+ return FALSE;
+
+ i++;
+ if(i >= n_lines)
+ return FALSE;
+
+ off = lines[i].beg;
+ line_end = lines[i].end;
+
+ if(attr_state == 0 || attr_state == 41)
+ attr_state = 1;
+
+ if(off >= max_end)
+ return FALSE;
+ }
+
+done:
+ if(off >= max_end)
+ return FALSE;
+
+ *p_end = off+1;
+ return TRUE;
+}
+
+static int
+md_scan_for_html_closer(MD_CTX* ctx, const MD_CHAR* str, MD_SIZE len,
+ const MD_LINE* lines, int n_lines,
+ OFF beg, OFF max_end, OFF* p_end,
+ OFF* p_scan_horizon)
+{
+ OFF off = beg;
+ int i = 0;
+
+ if(off < *p_scan_horizon && *p_scan_horizon >= max_end - len) {
+ /* We have already scanned the range up to the max_end so we know
+ * there is nothing to see. */
+ return FALSE;
+ }
+
+ while(TRUE) {
+ while(off + len <= lines[i].end && off + len <= max_end) {
+ if(md_ascii_eq(STR(off), str, len)) {
+ /* Success. */
+ *p_end = off + len;
+ return TRUE;
+ }
+ off++;
+ }
+
+ i++;
+ if(off >= max_end || i >= n_lines) {
+ /* Failure. */
+ *p_scan_horizon = off;
+ return FALSE;
+ }
+
+ off = lines[i].beg;
+ }
+}
+
+static int
+md_is_html_comment(MD_CTX* ctx, const MD_LINE* lines, int n_lines, OFF beg, OFF max_end, OFF* p_end)
+{
+ OFF off = beg;
+
+ MD_ASSERT(CH(beg) == _T('<'));
+
+ if(off + 4 >= lines[0].end)
+ return FALSE;
+ if(CH(off+1) != _T('!') || CH(off+2) != _T('-') || CH(off+3) != _T('-'))
+ return FALSE;
+ off += 4;
+
+ /* ">" and "->" must not follow the opening. */
+ if(off < lines[0].end && CH(off) == _T('>'))
+ return FALSE;
+ if(off+1 < lines[0].end && CH(off) == _T('-') && CH(off+1) == _T('>'))
+ return FALSE;
+
+ /* HTML comment must not contain "--", so we scan just for "--" instead
+ * of "-->" and verify manually that '>' follows. */
+ if(md_scan_for_html_closer(ctx, _T("--"), 2,
+ lines, n_lines, off, max_end, p_end, &ctx->html_comment_horizon))
+ {
+ if(*p_end < max_end && CH(*p_end) == _T('>')) {
+ *p_end = *p_end + 1;
+ return TRUE;
+ }
+ }
+
+ return FALSE;
+}
+
+static int
+md_is_html_processing_instruction(MD_CTX* ctx, const MD_LINE* lines, int n_lines, OFF beg, OFF max_end, OFF* p_end)
+{
+ OFF off = beg;
+
+ if(off + 2 >= lines[0].end)
+ return FALSE;
+ if(CH(off+1) != _T('?'))
+ return FALSE;
+ off += 2;
+
+ return md_scan_for_html_closer(ctx, _T("?>"), 2,
+ lines, n_lines, off, max_end, p_end, &ctx->html_proc_instr_horizon);
+}
+
+static int
+md_is_html_declaration(MD_CTX* ctx, const MD_LINE* lines, int n_lines, OFF beg, OFF max_end, OFF* p_end)
+{
+ OFF off = beg;
+
+ if(off + 2 >= lines[0].end)
+ return FALSE;
+ if(CH(off+1) != _T('!'))
+ return FALSE;
+ off += 2;
+
+ /* Declaration name. */
+ if(off >= lines[0].end || !ISALPHA(off))
+ return FALSE;
+ off++;
+ while(off < lines[0].end && ISALPHA(off))
+ off++;
+ if(off < lines[0].end && !ISWHITESPACE(off))
+ return FALSE;
+
+ return md_scan_for_html_closer(ctx, _T(">"), 1,
+ lines, n_lines, off, max_end, p_end, &ctx->html_decl_horizon);
+}
+
+static int
+md_is_html_cdata(MD_CTX* ctx, const MD_LINE* lines, int n_lines, OFF beg, OFF max_end, OFF* p_end)
+{
+ static const CHAR open_str[] = _T("<![CDATA[");
+ static const SZ open_size = SIZEOF_ARRAY(open_str) - 1;
+
+ OFF off = beg;
+
+ if(off + open_size >= lines[0].end)
+ return FALSE;
+ if(memcmp(STR(off), open_str, open_size) != 0)
+ return FALSE;
+ off += open_size;
+
+ if(lines[n_lines-1].end < max_end)
+ max_end = lines[n_lines-1].end - 2;
+
+ return md_scan_for_html_closer(ctx, _T("]]>"), 3,
+ lines, n_lines, off, max_end, p_end, &ctx->html_cdata_horizon);
+}
+
+static int
+md_is_html_any(MD_CTX* ctx, const MD_LINE* lines, int n_lines, OFF beg, OFF max_end, OFF* p_end)
+{
+ MD_ASSERT(CH(beg) == _T('<'));
+ return (md_is_html_tag(ctx, lines, n_lines, beg, max_end, p_end) ||
+ md_is_html_comment(ctx, lines, n_lines, beg, max_end, p_end) ||
+ md_is_html_processing_instruction(ctx, lines, n_lines, beg, max_end, p_end) ||
+ md_is_html_declaration(ctx, lines, n_lines, beg, max_end, p_end) ||
+ md_is_html_cdata(ctx, lines, n_lines, beg, max_end, p_end));
+}
+
+
+/****************************
+ *** Recognizing Entity ***
+ ****************************/
+
+static int
+md_is_hex_entity_contents(MD_CTX* ctx, const CHAR* text, OFF beg, OFF max_end, OFF* p_end)
+{
+ OFF off = beg;
+ MD_UNUSED(ctx);
+
+ while(off < max_end && ISXDIGIT_(text[off]) && off - beg <= 8)
+ off++;
+
+ if(1 <= off - beg && off - beg <= 6) {
+ *p_end = off;
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+}
+
+static int
+md_is_dec_entity_contents(MD_CTX* ctx, const CHAR* text, OFF beg, OFF max_end, OFF* p_end)
+{
+ OFF off = beg;
+ MD_UNUSED(ctx);
+
+ while(off < max_end && ISDIGIT_(text[off]) && off - beg <= 8)
+ off++;
+
+ if(1 <= off - beg && off - beg <= 7) {
+ *p_end = off;
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+}
+
+static int
+md_is_named_entity_contents(MD_CTX* ctx, const CHAR* text, OFF beg, OFF max_end, OFF* p_end)
+{
+ OFF off = beg;
+ MD_UNUSED(ctx);
+
+ if(off < max_end && ISALPHA_(text[off]))
+ off++;
+ else
+ return FALSE;
+
+ while(off < max_end && ISALNUM_(text[off]) && off - beg <= 48)
+ off++;
+
+ if(2 <= off - beg && off - beg <= 48) {
+ *p_end = off;
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+}
+
+static int
+md_is_entity_str(MD_CTX* ctx, const CHAR* text, OFF beg, OFF max_end, OFF* p_end)
+{
+ int is_contents;
+ OFF off = beg;
+
+ MD_ASSERT(text[off] == _T('&'));
+ off++;
+
+ if(off+2 < max_end && text[off] == _T('#') && (text[off+1] == _T('x') || text[off+1] == _T('X')))
+ is_contents = md_is_hex_entity_contents(ctx, text, off+2, max_end, &off);
+ else if(off+1 < max_end && text[off] == _T('#'))
+ is_contents = md_is_dec_entity_contents(ctx, text, off+1, max_end, &off);
+ else
+ is_contents = md_is_named_entity_contents(ctx, text, off, max_end, &off);
+
+ if(is_contents && off < max_end && text[off] == _T(';')) {
+ *p_end = off+1;
+ return TRUE;
+ } else {
+ return FALSE;
+ }
+}
+
+static inline int
+md_is_entity(MD_CTX* ctx, OFF beg, OFF max_end, OFF* p_end)
+{
+ return md_is_entity_str(ctx, ctx->text, beg, max_end, p_end);
+}
+
+
+/******************************
+ *** Attribute Management ***
+ ******************************/
+
+typedef struct MD_ATTRIBUTE_BUILD_tag MD_ATTRIBUTE_BUILD;
+struct MD_ATTRIBUTE_BUILD_tag {
+ CHAR* text;
+ MD_TEXTTYPE* substr_types;
+ OFF* substr_offsets;
+ int substr_count;
+ int substr_alloc;
+ MD_TEXTTYPE trivial_types[1];
+ OFF trivial_offsets[2];
+};
+
+
+#define MD_BUILD_ATTR_NO_ESCAPES 0x0001
+
+static int
+md_build_attr_append_substr(MD_CTX* ctx, MD_ATTRIBUTE_BUILD* build,
+ MD_TEXTTYPE type, OFF off)
+{
+ if(build->substr_count >= build->substr_alloc) {
+ MD_TEXTTYPE* new_substr_types;
+ OFF* new_substr_offsets;
+
+ build->substr_alloc = (build->substr_alloc > 0
+ ? build->substr_alloc + build->substr_alloc / 2
+ : 8);
+ new_substr_types = (MD_TEXTTYPE*) realloc(build->substr_types,
+ build->substr_alloc * sizeof(MD_TEXTTYPE));
+ if(new_substr_types == NULL) {
+ MD_LOG("realloc() failed.");
+ return -1;
+ }
+ /* Note +1 to reserve space for final offset (== raw_size). */
+ new_substr_offsets = (OFF*) realloc(build->substr_offsets,
+ (build->substr_alloc+1) * sizeof(OFF));
+ if(new_substr_offsets == NULL) {
+ MD_LOG("realloc() failed.");
+ free(new_substr_types);
+ return -1;
+ }
+
+ build->substr_types = new_substr_types;
+ build->substr_offsets = new_substr_offsets;
+ }
+
+ build->substr_types[build->substr_count] = type;
+ build->substr_offsets[build->substr_count] = off;
+ build->substr_count++;
+ return 0;
+}
+
+static void
+md_free_attribute(MD_CTX* ctx, MD_ATTRIBUTE_BUILD* build)
+{
+ MD_UNUSED(ctx);
+
+ if(build->substr_alloc > 0) {
+ free(build->text);
+ free(build->substr_types);
+ free(build->substr_offsets);
+ }
+}
+
+static int
+md_build_attribute(MD_CTX* ctx, const CHAR* raw_text, SZ raw_size,
+ unsigned flags, MD_ATTRIBUTE* attr, MD_ATTRIBUTE_BUILD* build)
+{
+ OFF raw_off, off;
+ int is_trivial;
+ int ret = 0;
+
+ memset(build, 0, sizeof(MD_ATTRIBUTE_BUILD));
+
+ /* If there is no backslash and no ampersand, build trivial attribute
+ * without any malloc(). */
+ is_trivial = TRUE;
+ for(raw_off = 0; raw_off < raw_size; raw_off++) {
+ if(ISANYOF3_(raw_text[raw_off], _T('\\'), _T('&'), _T('\0'))) {
+ is_trivial = FALSE;
+ break;
+ }
+ }
+
+ if(is_trivial) {
+ build->text = (CHAR*) (raw_size ? raw_text : NULL);
+ build->substr_types = build->trivial_types;
+ build->substr_offsets = build->trivial_offsets;
+ build->substr_count = 1;
+ build->substr_alloc = 0;
+ build->trivial_types[0] = MD_TEXT_NORMAL;
+ build->trivial_offsets[0] = 0;
+ build->trivial_offsets[1] = raw_size;
+ off = raw_size;
+ } else {
+ build->text = (CHAR*) malloc(raw_size * sizeof(CHAR));
+ if(build->text == NULL) {
+ MD_LOG("malloc() failed.");
+ goto abort;
+ }
+
+ raw_off = 0;
+ off = 0;
+
+ while(raw_off < raw_size) {
+ if(raw_text[raw_off] == _T('\0')) {
+ MD_CHECK(md_build_attr_append_substr(ctx, build, MD_TEXT_NULLCHAR, off));
+ memcpy(build->text + off, raw_text + raw_off, 1);
+ off++;
+ raw_off++;
+ continue;
+ }
+
+ if(raw_text[raw_off] == _T('&')) {
+ OFF ent_end;
+
+ if(md_is_entity_str(ctx, raw_text, raw_off, raw_size, &ent_end)) {
+ MD_CHECK(md_build_attr_append_substr(ctx, build, MD_TEXT_ENTITY, off));
+ memcpy(build->text + off, raw_text + raw_off, ent_end - raw_off);
+ off += ent_end - raw_off;
+ raw_off = ent_end;
+ continue;
+ }
+ }
+
+ if(build->substr_count == 0 || build->substr_types[build->substr_count-1] != MD_TEXT_NORMAL)
+ MD_CHECK(md_build_attr_append_substr(ctx, build, MD_TEXT_NORMAL, off));
+
+ if(!(flags & MD_BUILD_ATTR_NO_ESCAPES) &&
+ raw_text[raw_off] == _T('\\') && raw_off+1 < raw_size &&
+ (ISPUNCT_(raw_text[raw_off+1]) || ISNEWLINE_(raw_text[raw_off+1])))
+ raw_off++;
+
+ build->text[off++] = raw_text[raw_off++];
+ }
+ build->substr_offsets[build->substr_count] = off;
+ }
+
+ attr->text = build->text;
+ attr->size = off;
+ attr->substr_offsets = build->substr_offsets;
+ attr->substr_types = build->substr_types;
+ return 0;
+
+abort:
+ md_free_attribute(ctx, build);
+ return -1;
+}
+
+
+/*********************************************
+ *** Dictionary of Reference Definitions ***
+ *********************************************/
+
+#define MD_FNV1A_BASE 2166136261U
+#define MD_FNV1A_PRIME 16777619U
+
+static inline unsigned
+md_fnv1a(unsigned base, const void* data, size_t n)
+{
+ const unsigned char* buf = (const unsigned char*) data;
+ unsigned hash = base;
+ size_t i;
+
+ for(i = 0; i < n; i++) {
+ hash ^= buf[i];
+ hash *= MD_FNV1A_PRIME;
+ }
+
+ return hash;
+}
+
+
+struct MD_REF_DEF_tag {
+ CHAR* label;
+ CHAR* title;
+ unsigned hash;
+ SZ label_size;
+ SZ title_size;
+ OFF dest_beg;
+ OFF dest_end;
+ unsigned char label_needs_free : 1;
+ unsigned char title_needs_free : 1;
+};
+
+/* Label equivalence is quite complicated with regards to whitespace and case
+ * folding. This complicates computing a hash of it as well as direct comparison
+ * of two labels. */
+
+static unsigned
+md_link_label_hash(const CHAR* label, SZ size)
+{
+ unsigned hash = MD_FNV1A_BASE;
+ OFF off;
+ unsigned codepoint;
+ int is_whitespace = FALSE;
+
+ off = md_skip_unicode_whitespace(label, 0, size);
+ while(off < size) {
+ SZ char_size;
+
+ codepoint = md_decode_unicode(label, off, size, &char_size);
+ is_whitespace = ISUNICODEWHITESPACE_(codepoint) || ISNEWLINE_(label[off]);
+
+ if(is_whitespace) {
+ codepoint = ' ';
+ hash = md_fnv1a(hash, &codepoint, sizeof(unsigned));
+ off = md_skip_unicode_whitespace(label, off, size);
+ } else {
+ MD_UNICODE_FOLD_INFO fold_info;
+
+ md_get_unicode_fold_info(codepoint, &fold_info);
+ hash = md_fnv1a(hash, fold_info.codepoints, fold_info.n_codepoints * sizeof(unsigned));
+ off += char_size;
+ }
+ }
+
+ return hash;
+}
+
+static OFF
+md_link_label_cmp_load_fold_info(const CHAR* label, OFF off, SZ size,
+ MD_UNICODE_FOLD_INFO* fold_info)
+{
+ unsigned codepoint;
+ SZ char_size;
+
+ if(off >= size) {
+ /* Treat end of a link label as a whitespace. */
+ goto whitespace;
+ }
+
+ codepoint = md_decode_unicode(label, off, size, &char_size);
+ off += char_size;
+ if(ISUNICODEWHITESPACE_(codepoint)) {
+ /* Treat all whitespace as equivalent */
+ goto whitespace;
+ }
+
+ /* Get real folding info. */
+ md_get_unicode_fold_info(codepoint, fold_info);
+ return off;
+
+whitespace:
+ fold_info->codepoints[0] = _T(' ');
+ fold_info->n_codepoints = 1;
+ return md_skip_unicode_whitespace(label, off, size);
+}
+
+static int
+md_link_label_cmp(const CHAR* a_label, SZ a_size, const CHAR* b_label, SZ b_size)
+{
+ OFF a_off;
+ OFF b_off;
+ MD_UNICODE_FOLD_INFO a_fi = { { 0 }, 0 };
+ MD_UNICODE_FOLD_INFO b_fi = { { 0 }, 0 };
+ OFF a_fi_off = 0;
+ OFF b_fi_off = 0;
+ int cmp;
+
+ a_off = md_skip_unicode_whitespace(a_label, 0, a_size);
+ b_off = md_skip_unicode_whitespace(b_label, 0, b_size);
+ while(a_off < a_size || a_fi_off < a_fi.n_codepoints ||
+ b_off < b_size || b_fi_off < b_fi.n_codepoints)
+ {
+ /* If needed, load fold info for next char. */
+ if(a_fi_off >= a_fi.n_codepoints) {
+ a_fi_off = 0;
+ a_off = md_link_label_cmp_load_fold_info(a_label, a_off, a_size, &a_fi);
+ }
+ if(b_fi_off >= b_fi.n_codepoints) {
+ b_fi_off = 0;
+ b_off = md_link_label_cmp_load_fold_info(b_label, b_off, b_size, &b_fi);
+ }
+
+ cmp = b_fi.codepoints[b_fi_off] - a_fi.codepoints[a_fi_off];
+ if(cmp != 0)
+ return cmp;
+
+ a_fi_off++;
+ b_fi_off++;
+ }
+
+ return 0;
+}
+
+typedef struct MD_REF_DEF_LIST_tag MD_REF_DEF_LIST;
+struct MD_REF_DEF_LIST_tag {
+ int n_ref_defs;
+ int alloc_ref_defs;
+ MD_REF_DEF* ref_defs[]; /* Valid items always point into ctx->ref_defs[] */
+};
+
+static int
+md_ref_def_cmp(const void* a, const void* b)
+{
+ const MD_REF_DEF* a_ref = *(const MD_REF_DEF**)a;
+ const MD_REF_DEF* b_ref = *(const MD_REF_DEF**)b;
+
+ if(a_ref->hash < b_ref->hash)
+ return -1;
+ else if(a_ref->hash > b_ref->hash)
+ return +1;
+ else
+ return md_link_label_cmp(a_ref->label, a_ref->label_size, b_ref->label, b_ref->label_size);
+}
+
+static int
+md_ref_def_cmp_for_sort(const void* a, const void* b)
+{
+ int cmp;
+
+ cmp = md_ref_def_cmp(a, b);
+
+ /* Ensure stability of the sorting. */
+ if(cmp == 0) {
+ const MD_REF_DEF* a_ref = *(const MD_REF_DEF**)a;
+ const MD_REF_DEF* b_ref = *(const MD_REF_DEF**)b;
+
+ if(a_ref < b_ref)
+ cmp = -1;
+ else if(a_ref > b_ref)
+ cmp = +1;
+ else
+ cmp = 0;
+ }
+
+ return cmp;
+}
+
+static int
+md_build_ref_def_hashtable(MD_CTX* ctx)
+{
+ int i, j;
+
+ if(ctx->n_ref_defs == 0)
+ return 0;
+
+ ctx->ref_def_hashtable_size = (ctx->n_ref_defs * 5) / 4;
+ ctx->ref_def_hashtable = malloc(ctx->ref_def_hashtable_size * sizeof(void*));
+ if(ctx->ref_def_hashtable == NULL) {
+ MD_LOG("malloc() failed.");
+ goto abort;
+ }
+ memset(ctx->ref_def_hashtable, 0, ctx->ref_def_hashtable_size * sizeof(void*));
+
+ /* Each member of ctx->ref_def_hashtable[] can be:
+ * -- NULL,
+ * -- pointer to the MD_REF_DEF in ctx->ref_defs[], or
+ * -- pointer to a MD_REF_DEF_LIST, which holds multiple pointers to
+ * such MD_REF_DEFs.
+ */
+ for(i = 0; i < ctx->n_ref_defs; i++) {
+ MD_REF_DEF* def = &ctx->ref_defs[i];
+ void* bucket;
+ MD_REF_DEF_LIST* list;
+
+ def->hash = md_link_label_hash(def->label, def->label_size);
+ bucket = ctx->ref_def_hashtable[def->hash % ctx->ref_def_hashtable_size];
+
+ if(bucket == NULL) {
+ /* The bucket is empty. Make it just point to the def. */
+ ctx->ref_def_hashtable[def->hash % ctx->ref_def_hashtable_size] = def;
+ continue;
+ }
+
+ if(ctx->ref_defs <= (MD_REF_DEF*) bucket && (MD_REF_DEF*) bucket < ctx->ref_defs + ctx->n_ref_defs) {
+ /* The bucket already contains one ref. def. Lets see whether it
+ * is the same label (ref. def. duplicate) or different one
+ * (hash conflict). */
+ MD_REF_DEF* old_def = (MD_REF_DEF*) bucket;
+
+ if(md_link_label_cmp(def->label, def->label_size, old_def->label, old_def->label_size) == 0) {
+ /* Duplicate label: Ignore this ref. def. */
+ continue;
+ }
+
+ /* Make the bucket complex, i.e. able to hold more ref. defs. */
+ list = (MD_REF_DEF_LIST*) malloc(sizeof(MD_REF_DEF_LIST) + 2 * sizeof(MD_REF_DEF*));
+ if(list == NULL) {
+ MD_LOG("malloc() failed.");
+ goto abort;
+ }
+ list->ref_defs[0] = old_def;
+ list->ref_defs[1] = def;
+ list->n_ref_defs = 2;
+ list->alloc_ref_defs = 2;
+ ctx->ref_def_hashtable[def->hash % ctx->ref_def_hashtable_size] = list;
+ continue;
+ }
+
+ /* Append the def to the complex bucket list.
+ *
+ * Note in this case we ignore potential duplicates to avoid expensive
+ * iterating over the complex bucket. Below, we revisit all the complex
+ * buckets and handle it more cheaply after the complex bucket contents
+ * is sorted. */
+ list = (MD_REF_DEF_LIST*) bucket;
+ if(list->n_ref_defs >= list->alloc_ref_defs) {
+ int alloc_ref_defs = list->alloc_ref_defs + list->alloc_ref_defs / 2;
+ MD_REF_DEF_LIST* list_tmp = (MD_REF_DEF_LIST*) realloc(list,
+ sizeof(MD_REF_DEF_LIST) + alloc_ref_defs * sizeof(MD_REF_DEF*));
+ if(list_tmp == NULL) {
+ MD_LOG("realloc() failed.");
+ goto abort;
+ }
+ list = list_tmp;
+ list->alloc_ref_defs = alloc_ref_defs;
+ ctx->ref_def_hashtable[def->hash % ctx->ref_def_hashtable_size] = list;
+ }
+
+ list->ref_defs[list->n_ref_defs] = def;
+ list->n_ref_defs++;
+ }
+
+ /* Sort the complex buckets so we can use bsearch() with them. */
+ for(i = 0; i < ctx->ref_def_hashtable_size; i++) {
+ void* bucket = ctx->ref_def_hashtable[i];
+ MD_REF_DEF_LIST* list;
+
+ if(bucket == NULL)
+ continue;
+ if(ctx->ref_defs <= (MD_REF_DEF*) bucket && (MD_REF_DEF*) bucket < ctx->ref_defs + ctx->n_ref_defs)
+ continue;
+
+ list = (MD_REF_DEF_LIST*) bucket;
+ qsort(list->ref_defs, list->n_ref_defs, sizeof(MD_REF_DEF*), md_ref_def_cmp_for_sort);
+
+ /* Disable all duplicates in the complex bucket by forcing all such
+ * records to point to the 1st such ref. def. I.e. no matter which
+ * record is found during the lookup, it will always point to the right
+ * ref. def. in ctx->ref_defs[]. */
+ for(j = 1; j < list->n_ref_defs; j++) {
+ if(md_ref_def_cmp(&list->ref_defs[j-1], &list->ref_defs[j]) == 0)
+ list->ref_defs[j] = list->ref_defs[j-1];
+ }
+ }
+
+ return 0;
+
+abort:
+ return -1;
+}
+
+static void
+md_free_ref_def_hashtable(MD_CTX* ctx)
+{
+ if(ctx->ref_def_hashtable != NULL) {
+ int i;
+
+ for(i = 0; i < ctx->ref_def_hashtable_size; i++) {
+ void* bucket = ctx->ref_def_hashtable[i];
+ if(bucket == NULL)
+ continue;
+ if(ctx->ref_defs <= (MD_REF_DEF*) bucket && (MD_REF_DEF*) bucket < ctx->ref_defs + ctx->n_ref_defs)
+ continue;
+ free(bucket);
+ }
+
+ free(ctx->ref_def_hashtable);
+ }
+}
+
+static const MD_REF_DEF*
+md_lookup_ref_def(MD_CTX* ctx, const CHAR* label, SZ label_size)
+{
+ unsigned hash;
+ void* bucket;
+
+ if(ctx->ref_def_hashtable_size == 0)
+ return NULL;
+
+ hash = md_link_label_hash(label, label_size);
+ bucket = ctx->ref_def_hashtable[hash % ctx->ref_def_hashtable_size];
+
+ if(bucket == NULL) {
+ return NULL;
+ } else if(ctx->ref_defs <= (MD_REF_DEF*) bucket && (MD_REF_DEF*) bucket < ctx->ref_defs + ctx->n_ref_defs) {
+ const MD_REF_DEF* def = (MD_REF_DEF*) bucket;
+
+ if(md_link_label_cmp(def->label, def->label_size, label, label_size) == 0)
+ return def;
+ else
+ return NULL;
+ } else {
+ MD_REF_DEF_LIST* list = (MD_REF_DEF_LIST*) bucket;
+ MD_REF_DEF key_buf;
+ const MD_REF_DEF* key = &key_buf;
+ const MD_REF_DEF** ret;
+
+ key_buf.label = (CHAR*) label;
+ key_buf.label_size = label_size;
+ key_buf.hash = md_link_label_hash(key_buf.label, key_buf.label_size);
+
+ ret = (const MD_REF_DEF**) bsearch(&key, list->ref_defs,
+ list->n_ref_defs, sizeof(MD_REF_DEF*), md_ref_def_cmp);
+ if(ret != NULL)
+ return *ret;
+ else
+ return NULL;
+ }
+}
+
+
+/***************************
+ *** Recognizing Links ***
+ ***************************/
+
+/* Note this code is partially shared between processing inlines and blocks
+ * as reference definitions and links share some helper parser functions.
+ */
+
+typedef struct MD_LINK_ATTR_tag MD_LINK_ATTR;
+struct MD_LINK_ATTR_tag {
+ OFF dest_beg;
+ OFF dest_end;
+
+ CHAR* title;
+ SZ title_size;
+ int title_needs_free;
+};
+
+
+static int
+md_is_link_label(MD_CTX* ctx, const MD_LINE* lines, int n_lines, OFF beg,
+ OFF* p_end, int* p_beg_line_index, int* p_end_line_index,
+ OFF* p_contents_beg, OFF* p_contents_end)
+{
+ OFF off = beg;
+ OFF contents_beg = 0;
+ OFF contents_end = 0;
+ int line_index = 0;
+ int len = 0;
+
+ if(CH(off) != _T('['))
+ return FALSE;
+ off++;
+
+ while(1) {
+ OFF line_end = lines[line_index].end;
+
+ while(off < line_end) {
+ if(CH(off) == _T('\\') && off+1 < ctx->size && (ISPUNCT(off+1) || ISNEWLINE(off+1))) {
+ if(contents_end == 0) {
+ contents_beg = off;
+ *p_beg_line_index = line_index;
+ }
+ contents_end = off + 2;
+ off += 2;
+ } else if(CH(off) == _T('[')) {
+ return FALSE;
+ } else if(CH(off) == _T(']')) {
+ if(contents_beg < contents_end) {
+ /* Success. */
+ *p_contents_beg = contents_beg;
+ *p_contents_end = contents_end;
+ *p_end = off+1;
+ *p_end_line_index = line_index;
+ return TRUE;
+ } else {
+ /* Link label must have some non-whitespace contents. */
+ return FALSE;
+ }
+ } else {
+ unsigned codepoint;
+ SZ char_size;
+
+ codepoint = md_decode_unicode(ctx->text, off, ctx->size, &char_size);
+ if(!ISUNICODEWHITESPACE_(codepoint)) {
+ if(contents_end == 0) {
+ contents_beg = off;
+ *p_beg_line_index = line_index;
+ }
+ contents_end = off + char_size;
+ }
+
+ off += char_size;
+ }
+
+ len++;
+ if(len > 999)
+ return FALSE;
+ }
+
+ line_index++;
+ len++;
+ if(line_index < n_lines)
+ off = lines[line_index].beg;
+ else
+ break;
+ }
+
+ return FALSE;
+}
+
+static int
+md_is_link_destination_A(MD_CTX* ctx, OFF beg, OFF max_end, OFF* p_end,
+ OFF* p_contents_beg, OFF* p_contents_end)
+{
+ OFF off = beg;
+
+ if(off >= max_end || CH(off) != _T('<'))
+ return FALSE;
+ off++;
+
+ while(off < max_end) {
+ if(CH(off) == _T('\\') && off+1 < max_end && ISPUNCT(off+1)) {
+ off += 2;
+ continue;
+ }
+
+ if(ISNEWLINE(off) || CH(off) == _T('<'))
+ return FALSE;
+
+ if(CH(off) == _T('>')) {
+ /* Success. */
+ *p_contents_beg = beg+1;
+ *p_contents_end = off;
+ *p_end = off+1;
+ return TRUE;
+ }
+
+ off++;
+ }
+
+ return FALSE;
+}
+
+static int
+md_is_link_destination_B(MD_CTX* ctx, OFF beg, OFF max_end, OFF* p_end,
+ OFF* p_contents_beg, OFF* p_contents_end)
+{
+ OFF off = beg;
+ int parenthesis_level = 0;
+
+ while(off < max_end) {
+ if(CH(off) == _T('\\') && off+1 < max_end && ISPUNCT(off+1)) {
+ off += 2;
+ continue;
+ }
+
+ if(ISWHITESPACE(off) || ISCNTRL(off))
+ break;
+
+ /* Link destination may include balanced pairs of unescaped '(' ')'.
+ * Note we limit the maximal nesting level by 32 to protect us from
+ * https://github.com/jgm/cmark/issues/214 */
+ if(CH(off) == _T('(')) {
+ parenthesis_level++;
+ if(parenthesis_level > 32)
+ return FALSE;
+ } else if(CH(off) == _T(')')) {
+ if(parenthesis_level == 0)
+ break;
+ parenthesis_level--;
+ }
+
+ off++;
+ }
+
+ if(parenthesis_level != 0 || off == beg)
+ return FALSE;
+
+ /* Success. */
+ *p_contents_beg = beg;
+ *p_contents_end = off;
+ *p_end = off;
+ return TRUE;
+}
+
+static inline int
+md_is_link_destination(MD_CTX* ctx, OFF beg, OFF max_end, OFF* p_end,
+ OFF* p_contents_beg, OFF* p_contents_end)
+{
+ if(CH(beg) == _T('<'))
+ return md_is_link_destination_A(ctx, beg, max_end, p_end, p_contents_beg, p_contents_end);
+ else
+ return md_is_link_destination_B(ctx, beg, max_end, p_end, p_contents_beg, p_contents_end);
+}
+
+static int
+md_is_link_title(MD_CTX* ctx, const MD_LINE* lines, int n_lines, OFF beg,
+ OFF* p_end, int* p_beg_line_index, int* p_end_line_index,
+ OFF* p_contents_beg, OFF* p_contents_end)
+{
+ OFF off = beg;
+ CHAR closer_char;
+ int line_index = 0;
+
+ /* White space with up to one line break. */
+ while(off < lines[line_index].end && ISWHITESPACE(off))
+ off++;
+ if(off >= lines[line_index].end) {
+ line_index++;
+ if(line_index >= n_lines)
+ return FALSE;
+ off = lines[line_index].beg;
+ }
+ if(off == beg)
+ return FALSE;
+
+ *p_beg_line_index = line_index;
+
+ /* First char determines how to detect end of it. */
+ switch(CH(off)) {
+ case _T('"'): closer_char = _T('"'); break;
+ case _T('\''): closer_char = _T('\''); break;
+ case _T('('): closer_char = _T(')'); break;
+ default: return FALSE;
+ }
+ off++;
+
+ *p_contents_beg = off;
+
+ while(line_index < n_lines) {
+ OFF line_end = lines[line_index].end;
+
+ while(off < line_end) {
+ if(CH(off) == _T('\\') && off+1 < ctx->size && (ISPUNCT(off+1) || ISNEWLINE(off+1))) {
+ off++;
+ } else if(CH(off) == closer_char) {
+ /* Success. */
+ *p_contents_end = off;
+ *p_end = off+1;
+ *p_end_line_index = line_index;
+ return TRUE;
+ } else if(closer_char == _T(')') && CH(off) == _T('(')) {
+ /* ()-style title cannot contain (unescaped '(')) */
+ return FALSE;
+ }
+
+ off++;
+ }
+
+ line_index++;
+ }
+
+ return FALSE;
+}
+
+/* Returns 0 if it is not a reference definition.
+ *
+ * Returns N > 0 if it is a reference definition. N then corresponds to the
+ * number of lines forming it). In this case the definition is stored for
+ * resolving any links referring to it.
+ *
+ * Returns -1 in case of an error (out of memory).
+ */
+static int
+md_is_link_reference_definition(MD_CTX* ctx, const MD_LINE* lines, int n_lines)
+{
+ OFF label_contents_beg;
+ OFF label_contents_end;
+ int label_contents_line_index = -1;
+ int label_is_multiline = FALSE;
+ OFF dest_contents_beg;
+ OFF dest_contents_end;
+ OFF title_contents_beg;
+ OFF title_contents_end;
+ int title_contents_line_index;
+ int title_is_multiline = FALSE;
+ OFF off;
+ int line_index = 0;
+ int tmp_line_index;
+ MD_REF_DEF* def = NULL;
+ int ret = 0;
+
+ /* Link label. */
+ if(!md_is_link_label(ctx, lines, n_lines, lines[0].beg,
+ &off, &label_contents_line_index, &line_index,
+ &label_contents_beg, &label_contents_end))
+ return FALSE;
+ label_is_multiline = (label_contents_line_index != line_index);
+
+ /* Colon. */
+ if(off >= lines[line_index].end || CH(off) != _T(':'))
+ return FALSE;
+ off++;
+
+ /* Optional white space with up to one line break. */
+ while(off < lines[line_index].end && ISWHITESPACE(off))
+ off++;
+ if(off >= lines[line_index].end) {
+ line_index++;
+ if(line_index >= n_lines)
+ return FALSE;
+ off = lines[line_index].beg;
+ }
+
+ /* Link destination. */
+ if(!md_is_link_destination(ctx, off, lines[line_index].end,
+ &off, &dest_contents_beg, &dest_contents_end))
+ return FALSE;
+
+ /* (Optional) title. Note we interpret it as an title only if nothing
+ * more follows on its last line. */
+ if(md_is_link_title(ctx, lines + line_index, n_lines - line_index, off,
+ &off, &title_contents_line_index, &tmp_line_index,
+ &title_contents_beg, &title_contents_end)
+ && off >= lines[line_index + tmp_line_index].end)
+ {
+ title_is_multiline = (tmp_line_index != title_contents_line_index);
+ title_contents_line_index += line_index;
+ line_index += tmp_line_index;
+ } else {
+ /* Not a title. */
+ title_is_multiline = FALSE;
+ title_contents_beg = off;
+ title_contents_end = off;
+ title_contents_line_index = 0;
+ }
+
+ /* Nothing more can follow on the last line. */
+ if(off < lines[line_index].end)
+ return FALSE;
+
+ /* So, it _is_ a reference definition. Remember it. */
+ if(ctx->n_ref_defs >= ctx->alloc_ref_defs) {
+ MD_REF_DEF* new_defs;
+
+ ctx->alloc_ref_defs = (ctx->alloc_ref_defs > 0
+ ? ctx->alloc_ref_defs + ctx->alloc_ref_defs / 2
+ : 16);
+ new_defs = (MD_REF_DEF*) realloc(ctx->ref_defs, ctx->alloc_ref_defs * sizeof(MD_REF_DEF));
+ if(new_defs == NULL) {
+ MD_LOG("realloc() failed.");
+ goto abort;
+ }
+
+ ctx->ref_defs = new_defs;
+ }
+ def = &ctx->ref_defs[ctx->n_ref_defs];
+ memset(def, 0, sizeof(MD_REF_DEF));
+
+ if(label_is_multiline) {
+ MD_CHECK(md_merge_lines_alloc(ctx, label_contents_beg, label_contents_end,
+ lines + label_contents_line_index, n_lines - label_contents_line_index,
+ _T(' '), &def->label, &def->label_size));
+ def->label_needs_free = TRUE;
+ } else {
+ def->label = (CHAR*) STR(label_contents_beg);
+ def->label_size = label_contents_end - label_contents_beg;
+ }
+
+ if(title_is_multiline) {
+ MD_CHECK(md_merge_lines_alloc(ctx, title_contents_beg, title_contents_end,
+ lines + title_contents_line_index, n_lines - title_contents_line_index,
+ _T('\n'), &def->title, &def->title_size));
+ def->title_needs_free = TRUE;
+ } else {
+ def->title = (CHAR*) STR(title_contents_beg);
+ def->title_size = title_contents_end - title_contents_beg;
+ }
+
+ def->dest_beg = dest_contents_beg;
+ def->dest_end = dest_contents_end;
+
+ /* Success. */
+ ctx->n_ref_defs++;
+ return line_index + 1;
+
+abort:
+ /* Failure. */
+ if(def != NULL && def->label_needs_free)
+ free(def->label);
+ if(def != NULL && def->title_needs_free)
+ free(def->title);
+ return ret;
+}
+
+static int
+md_is_link_reference(MD_CTX* ctx, const MD_LINE* lines, int n_lines,
+ OFF beg, OFF end, MD_LINK_ATTR* attr)
+{
+ const MD_REF_DEF* def;
+ const MD_LINE* beg_line;
+ int is_multiline;
+ CHAR* label;
+ SZ label_size;
+ int ret;
+
+ MD_ASSERT(CH(beg) == _T('[') || CH(beg) == _T('!'));
+ MD_ASSERT(CH(end-1) == _T(']'));
+
+ beg += (CH(beg) == _T('!') ? 2 : 1);
+ end--;
+
+ /* Find lines corresponding to the beg and end positions. */
+ beg_line = md_lookup_line(beg, lines, n_lines);
+ is_multiline = (end > beg_line->end);
+
+ if(is_multiline) {
+ MD_CHECK(md_merge_lines_alloc(ctx, beg, end, beg_line,
+ (int)(n_lines - (beg_line - lines)), _T(' '), &label, &label_size));
+ } else {
+ label = (CHAR*) STR(beg);
+ label_size = end - beg;
+ }
+
+ def = md_lookup_ref_def(ctx, label, label_size);
+ if(def != NULL) {
+ attr->dest_beg = def->dest_beg;
+ attr->dest_end = def->dest_end;
+ attr->title = def->title;
+ attr->title_size = def->title_size;
+ attr->title_needs_free = FALSE;
+ }
+
+ if(is_multiline)
+ free(label);
+
+ ret = (def != NULL);
+
+abort:
+ return ret;
+}
+
+static int
+md_is_inline_link_spec(MD_CTX* ctx, const MD_LINE* lines, int n_lines,
+ OFF beg, OFF* p_end, MD_LINK_ATTR* attr)
+{
+ int line_index = 0;
+ int tmp_line_index;
+ OFF title_contents_beg;
+ OFF title_contents_end;
+ int title_contents_line_index;
+ int title_is_multiline;
+ OFF off = beg;
+ int ret = FALSE;
+
+ while(off >= lines[line_index].end)
+ line_index++;
+
+ MD_ASSERT(CH(off) == _T('('));
+ off++;
+
+ /* Optional white space with up to one line break. */
+ while(off < lines[line_index].end && ISWHITESPACE(off))
+ off++;
+ if(off >= lines[line_index].end && (off >= ctx->size || ISNEWLINE(off))) {
+ line_index++;
+ if(line_index >= n_lines)
+ return FALSE;
+ off = lines[line_index].beg;
+ }
+
+ /* Link destination may be omitted, but only when not also having a title. */
+ if(off < ctx->size && CH(off) == _T(')')) {
+ attr->dest_beg = off;
+ attr->dest_end = off;
+ attr->title = NULL;
+ attr->title_size = 0;
+ attr->title_needs_free = FALSE;
+ off++;
+ *p_end = off;
+ return TRUE;
+ }
+
+ /* Link destination. */
+ if(!md_is_link_destination(ctx, off, lines[line_index].end,
+ &off, &attr->dest_beg, &attr->dest_end))
+ return FALSE;
+
+ /* (Optional) title. */
+ if(md_is_link_title(ctx, lines + line_index, n_lines - line_index, off,
+ &off, &title_contents_line_index, &tmp_line_index,
+ &title_contents_beg, &title_contents_end))
+ {
+ title_is_multiline = (tmp_line_index != title_contents_line_index);
+ title_contents_line_index += line_index;
+ line_index += tmp_line_index;
+ } else {
+ /* Not a title. */
+ title_is_multiline = FALSE;
+ title_contents_beg = off;
+ title_contents_end = off;
+ title_contents_line_index = 0;
+ }
+
+ /* Optional whitespace followed with final ')'. */
+ while(off < lines[line_index].end && ISWHITESPACE(off))
+ off++;
+ if (off >= lines[line_index].end && (off >= ctx->size || ISNEWLINE(off))) {
+ line_index++;
+ if(line_index >= n_lines)
+ return FALSE;
+ off = lines[line_index].beg;
+ }
+ if(CH(off) != _T(')'))
+ goto abort;
+ off++;
+
+ if(title_contents_beg >= title_contents_end) {
+ attr->title = NULL;
+ attr->title_size = 0;
+ attr->title_needs_free = FALSE;
+ } else if(!title_is_multiline) {
+ attr->title = (CHAR*) STR(title_contents_beg);
+ attr->title_size = title_contents_end - title_contents_beg;
+ attr->title_needs_free = FALSE;
+ } else {
+ MD_CHECK(md_merge_lines_alloc(ctx, title_contents_beg, title_contents_end,
+ lines + title_contents_line_index, n_lines - title_contents_line_index,
+ _T('\n'), &attr->title, &attr->title_size));
+ attr->title_needs_free = TRUE;
+ }
+
+ *p_end = off;
+ ret = TRUE;
+
+abort:
+ return ret;
+}
+
+static void
+md_free_ref_defs(MD_CTX* ctx)
+{
+ int i;
+
+ for(i = 0; i < ctx->n_ref_defs; i++) {
+ MD_REF_DEF* def = &ctx->ref_defs[i];
+
+ if(def->label_needs_free)
+ free(def->label);
+ if(def->title_needs_free)
+ free(def->title);
+ }
+
+ free(ctx->ref_defs);
+}
+
+
+/******************************************
+ *** Processing Inlines (a.k.a Spans) ***
+ ******************************************/
+
+/* We process inlines in few phases:
+ *
+ * (1) We go through the block text and collect all significant characters
+ * which may start/end a span or some other significant position into
+ * ctx->marks[]. Core of this is what md_collect_marks() does.
+ *
+ * We also do some very brief preliminary context-less analysis, whether
+ * it might be opener or closer (e.g. of an emphasis span).
+ *
+ * This speeds the other steps as we do not need to re-iterate over all
+ * characters anymore.
+ *
+ * (2) We analyze each potential mark types, in order by their precedence.
+ *
+ * In each md_analyze_XXX() function, we re-iterate list of the marks,
+ * skipping already resolved regions (in preceding precedences) and try to
+ * resolve them.
+ *
+ * (2.1) For trivial marks, which are single (e.g. HTML entity), we just mark
+ * them as resolved.
+ *
+ * (2.2) For range-type marks, we analyze whether the mark could be closer
+ * and, if yes, whether there is some preceding opener it could satisfy.
+ *
+ * If not we check whether it could be really an opener and if yes, we
+ * remember it so subsequent closers may resolve it.
+ *
+ * (3) Finally, when all marks were analyzed, we render the block contents
+ * by calling MD_RENDERER::text() callback, interrupting by ::enter_span()
+ * or ::close_span() whenever we reach a resolved mark.
+ */
+
+
+/* The mark structure.
+ *
+ * '\\': Maybe escape sequence.
+ * '\0': NULL char.
+ * '*': Maybe (strong) emphasis start/end.
+ * '_': Maybe (strong) emphasis start/end.
+ * '~': Maybe strikethrough start/end (needs MD_FLAG_STRIKETHROUGH).
+ * '`': Maybe code span start/end.
+ * '&': Maybe start of entity.
+ * ';': Maybe end of entity.
+ * '<': Maybe start of raw HTML or autolink.
+ * '>': Maybe end of raw HTML or autolink.
+ * '[': Maybe start of link label or link text.
+ * '!': Equivalent of '[' for image.
+ * ']': Maybe end of link label or link text.
+ * '@': Maybe permissive e-mail auto-link (needs MD_FLAG_PERMISSIVEEMAILAUTOLINKS).
+ * ':': Maybe permissive URL auto-link (needs MD_FLAG_PERMISSIVEURLAUTOLINKS).
+ * '.': Maybe permissive WWW auto-link (needs MD_FLAG_PERMISSIVEWWWAUTOLINKS).
+ * 'D': Dummy mark, it reserves a space for splitting a previous mark
+ * (e.g. emphasis) or to make more space for storing some special data
+ * related to the preceding mark (e.g. link).
+ *
+ * Note that not all instances of these chars in the text imply creation of the
+ * structure. Only those which have (or may have, after we see more context)
+ * the special meaning.
+ *
+ * (Keep this struct as small as possible to fit as much of them into CPU
+ * cache line.)
+ */
+struct MD_MARK_tag {
+ OFF beg;
+ OFF end;
+
+ /* For unresolved openers, 'prev' and 'next' form the chain of open openers
+ * of given type 'ch'.
+ *
+ * During resolving, we disconnect from the chain and point to the
+ * corresponding counterpart so opener points to its closer and vice versa.
+ */
+ int prev;
+ int next;
+ CHAR ch;
+ unsigned char flags;
+};
+
+/* Mark flags (these apply to ALL mark types). */
+#define MD_MARK_POTENTIAL_OPENER 0x01 /* Maybe opener. */
+#define MD_MARK_POTENTIAL_CLOSER 0x02 /* Maybe closer. */
+#define MD_MARK_OPENER 0x04 /* Definitely opener. */
+#define MD_MARK_CLOSER 0x08 /* Definitely closer. */
+#define MD_MARK_RESOLVED 0x10 /* Resolved in any definite way. */
+
+/* Mark flags specific for various mark types (so they can share bits). */
+#define MD_MARK_EMPH_INTRAWORD 0x20 /* Helper for the "rule of 3". */
+#define MD_MARK_EMPH_MOD3_0 0x40
+#define MD_MARK_EMPH_MOD3_1 0x80
+#define MD_MARK_EMPH_MOD3_2 (0x40 | 0x80)
+#define MD_MARK_EMPH_MOD3_MASK (0x40 | 0x80)
+#define MD_MARK_AUTOLINK 0x20 /* Distinguisher for '<', '>'. */
+#define MD_MARK_VALIDPERMISSIVEAUTOLINK 0x20 /* For permissive autolinks. */
+#define MD_MARK_HASNESTEDBRACKETS 0x20 /* For '[' to rule out invalid link labels early */
+
+static MD_MARKCHAIN*
+md_asterisk_chain(MD_CTX* ctx, unsigned flags)
+{
+ switch(flags & (MD_MARK_EMPH_INTRAWORD | MD_MARK_EMPH_MOD3_MASK)) {
+ case MD_MARK_EMPH_INTRAWORD | MD_MARK_EMPH_MOD3_0: return &ASTERISK_OPENERS_intraword_mod3_0;
+ case MD_MARK_EMPH_INTRAWORD | MD_MARK_EMPH_MOD3_1: return &ASTERISK_OPENERS_intraword_mod3_1;
+ case MD_MARK_EMPH_INTRAWORD | MD_MARK_EMPH_MOD3_2: return &ASTERISK_OPENERS_intraword_mod3_2;
+ case MD_MARK_EMPH_MOD3_0: return &ASTERISK_OPENERS_extraword_mod3_0;
+ case MD_MARK_EMPH_MOD3_1: return &ASTERISK_OPENERS_extraword_mod3_1;
+ case MD_MARK_EMPH_MOD3_2: return &ASTERISK_OPENERS_extraword_mod3_2;
+ default: MD_UNREACHABLE();
+ }
+ return NULL;
+}
+
+static MD_MARKCHAIN*
+md_mark_chain(MD_CTX* ctx, int mark_index)
+{
+ MD_MARK* mark = &ctx->marks[mark_index];
+
+ switch(mark->ch) {
+ case _T('*'): return md_asterisk_chain(ctx, mark->flags);
+ case _T('_'): return &UNDERSCORE_OPENERS;
+ case _T('~'): return (mark->end - mark->beg == 1) ? &TILDE_OPENERS_1 : &TILDE_OPENERS_2;
+ case _T('!'): MD_FALLTHROUGH();
+ case _T('['): return &BRACKET_OPENERS;
+ case _T('|'): return &TABLECELLBOUNDARIES;
+ default: return NULL;
+ }
+}
+
+static MD_MARK*
+md_push_mark(MD_CTX* ctx)
+{
+ if(ctx->n_marks >= ctx->alloc_marks) {
+ MD_MARK* new_marks;
+
+ ctx->alloc_marks = (ctx->alloc_marks > 0
+ ? ctx->alloc_marks + ctx->alloc_marks / 2
+ : 64);
+ new_marks = realloc(ctx->marks, ctx->alloc_marks * sizeof(MD_MARK));
+ if(new_marks == NULL) {
+ MD_LOG("realloc() failed.");
+ return NULL;
+ }
+
+ ctx->marks = new_marks;
+ }
+
+ return &ctx->marks[ctx->n_marks++];
+}
+
+#define PUSH_MARK_() \
+ do { \
+ mark = md_push_mark(ctx); \
+ if(mark == NULL) { \
+ ret = -1; \
+ goto abort; \
+ } \
+ } while(0)
+
+#define PUSH_MARK(ch_, beg_, end_, flags_) \
+ do { \
+ PUSH_MARK_(); \
+ mark->beg = (beg_); \
+ mark->end = (end_); \
+ mark->prev = -1; \
+ mark->next = -1; \
+ mark->ch = (char)(ch_); \
+ mark->flags = (flags_); \
+ } while(0)
+
+
+static void
+md_mark_chain_append(MD_CTX* ctx, MD_MARKCHAIN* chain, int mark_index)
+{
+ if(chain->tail >= 0)
+ ctx->marks[chain->tail].next = mark_index;
+ else
+ chain->head = mark_index;
+
+ ctx->marks[mark_index].prev = chain->tail;
+ ctx->marks[mark_index].next = -1;
+ chain->tail = mark_index;
+}
+
+/* Sometimes, we need to store a pointer into the mark. It is quite rare
+ * so we do not bother to make MD_MARK use union, and it can only happen
+ * for dummy marks. */
+static inline void
+md_mark_store_ptr(MD_CTX* ctx, int mark_index, void* ptr)
+{
+ MD_MARK* mark = &ctx->marks[mark_index];
+ MD_ASSERT(mark->ch == 'D');
+
+ /* Check only members beg and end are misused for this. */
+ MD_ASSERT(sizeof(void*) <= 2 * sizeof(OFF));
+ memcpy(mark, &ptr, sizeof(void*));
+}
+
+static inline void*
+md_mark_get_ptr(MD_CTX* ctx, int mark_index)
+{
+ void* ptr;
+ MD_MARK* mark = &ctx->marks[mark_index];
+ MD_ASSERT(mark->ch == 'D');
+ memcpy(&ptr, mark, sizeof(void*));
+ return ptr;
+}
+
+static void
+md_resolve_range(MD_CTX* ctx, MD_MARKCHAIN* chain, int opener_index, int closer_index)
+{
+ MD_MARK* opener = &ctx->marks[opener_index];
+ MD_MARK* closer = &ctx->marks[closer_index];
+
+ /* Remove opener from the list of openers. */
+ if(chain != NULL) {
+ if(opener->prev >= 0)
+ ctx->marks[opener->prev].next = opener->next;
+ else
+ chain->head = opener->next;
+
+ if(opener->next >= 0)
+ ctx->marks[opener->next].prev = opener->prev;
+ else
+ chain->tail = opener->prev;
+ }
+
+ /* Interconnect opener and closer and mark both as resolved. */
+ opener->next = closer_index;
+ opener->flags |= MD_MARK_OPENER | MD_MARK_RESOLVED;
+ closer->prev = opener_index;
+ closer->flags |= MD_MARK_CLOSER | MD_MARK_RESOLVED;
+}
+
+
+#define MD_ROLLBACK_ALL 0
+#define MD_ROLLBACK_CROSSING 1
+
+/* In the range ctx->marks[opener_index] ... [closer_index], undo some or all
+ * resolvings accordingly to these rules:
+ *
+ * (1) All openers BEFORE the range corresponding to any closer inside the
+ * range are un-resolved and they are re-added to their respective chains
+ * of unresolved openers. This ensures we can reuse the opener for closers
+ * AFTER the range.
+ *
+ * (2) If 'how' is MD_ROLLBACK_ALL, then ALL resolved marks inside the range
+ * are discarded.
+ *
+ * (3) If 'how' is MD_ROLLBACK_CROSSING, only closers with openers handled
+ * in (1) are discarded. I.e. pairs of openers and closers which are both
+ * inside the range are retained as well as any unpaired marks.
+ */
+static void
+md_rollback(MD_CTX* ctx, int opener_index, int closer_index, int how)
+{
+ int i;
+ int mark_index;
+
+ /* Cut all unresolved openers at the mark index. */
+ for(i = OPENERS_CHAIN_FIRST; i < OPENERS_CHAIN_LAST+1; i++) {
+ MD_MARKCHAIN* chain = &ctx->mark_chains[i];
+
+ while(chain->tail >= opener_index) {
+ int same = chain->tail == opener_index;
+ chain->tail = ctx->marks[chain->tail].prev;
+ if (same) break;
+ }
+
+ if(chain->tail >= 0)
+ ctx->marks[chain->tail].next = -1;
+ else
+ chain->head = -1;
+ }
+
+ /* Go backwards so that unresolved openers are re-added into their
+ * respective chains, in the right order. */
+ mark_index = closer_index - 1;
+ while(mark_index > opener_index) {
+ MD_MARK* mark = &ctx->marks[mark_index];
+ int mark_flags = mark->flags;
+ int discard_flag = (how == MD_ROLLBACK_ALL);
+
+ if(mark->flags & MD_MARK_CLOSER) {
+ int mark_opener_index = mark->prev;
+
+ /* Undo opener BEFORE the range. */
+ if(mark_opener_index < opener_index) {
+ MD_MARK* mark_opener = &ctx->marks[mark_opener_index];
+ MD_MARKCHAIN* chain;
+
+ mark_opener->flags &= ~(MD_MARK_OPENER | MD_MARK_CLOSER | MD_MARK_RESOLVED);
+ chain = md_mark_chain(ctx, opener_index);
+ if(chain != NULL) {
+ md_mark_chain_append(ctx, chain, mark_opener_index);
+ discard_flag = 1;
+ }
+ }
+ }
+
+ /* And reset our flags. */
+ if(discard_flag) {
+ /* Make zero-length closer a dummy mark as that's how it was born */
+ if((mark->flags & MD_MARK_CLOSER) && mark->beg == mark->end)
+ mark->ch = 'D';
+
+ mark->flags &= ~(MD_MARK_OPENER | MD_MARK_CLOSER | MD_MARK_RESOLVED);
+ }
+
+ /* Jump as far as we can over unresolved or non-interesting marks. */
+ switch(how) {
+ case MD_ROLLBACK_CROSSING:
+ if((mark_flags & MD_MARK_CLOSER) && mark->prev > opener_index) {
+ /* If we are closer with opener INSIDE the range, there may
+ * not be any other crosser inside the subrange. */
+ mark_index = mark->prev;
+ break;
+ }
+ MD_FALLTHROUGH();
+ default:
+ mark_index--;
+ break;
+ }
+ }
+}
+
+static void
+md_build_mark_char_map(MD_CTX* ctx)
+{
+ memset(ctx->mark_char_map, 0, sizeof(ctx->mark_char_map));
+
+ ctx->mark_char_map['\\'] = 1;
+ ctx->mark_char_map['*'] = 1;
+ ctx->mark_char_map['_'] = 1;
+ ctx->mark_char_map['`'] = 1;
+ ctx->mark_char_map['&'] = 1;
+ ctx->mark_char_map[';'] = 1;
+ ctx->mark_char_map['<'] = 1;
+ ctx->mark_char_map['>'] = 1;
+ ctx->mark_char_map['['] = 1;
+ ctx->mark_char_map['!'] = 1;
+ ctx->mark_char_map[']'] = 1;
+ ctx->mark_char_map['\0'] = 1;
+
+ if(ctx->parser.flags & MD_FLAG_STRIKETHROUGH)
+ ctx->mark_char_map['~'] = 1;
+
+ if(ctx->parser.flags & MD_FLAG_LATEXMATHSPANS)
+ ctx->mark_char_map['$'] = 1;
+
+ if(ctx->parser.flags & MD_FLAG_PERMISSIVEEMAILAUTOLINKS)
+ ctx->mark_char_map['@'] = 1;
+
+ if(ctx->parser.flags & MD_FLAG_PERMISSIVEURLAUTOLINKS)
+ ctx->mark_char_map[':'] = 1;
+
+ if(ctx->parser.flags & MD_FLAG_PERMISSIVEWWWAUTOLINKS)
+ ctx->mark_char_map['.'] = 1;
+
+ if((ctx->parser.flags & MD_FLAG_TABLES) || (ctx->parser.flags & MD_FLAG_WIKILINKS))
+ ctx->mark_char_map['|'] = 1;
+
+ if(ctx->parser.flags & MD_FLAG_COLLAPSEWHITESPACE) {
+ int i;
+
+ for(i = 0; i < (int) sizeof(ctx->mark_char_map); i++) {
+ if(ISWHITESPACE_(i))
+ ctx->mark_char_map[i] = 1;
+ }
+ }
+}
+
+/* We limit code span marks to lower than 32 backticks. This solves the
+ * pathologic case of too many openers, each of different length: Their
+ * resolving would be then O(n^2). */
+#define CODESPAN_MARK_MAXLEN 32
+
+static int
+md_is_code_span(MD_CTX* ctx, const MD_LINE* lines, int n_lines, OFF beg,
+ OFF* p_opener_beg, OFF* p_opener_end,
+ OFF* p_closer_beg, OFF* p_closer_end,
+ OFF last_potential_closers[CODESPAN_MARK_MAXLEN],
+ int* p_reached_paragraph_end)
+{
+ OFF opener_beg = beg;
+ OFF opener_end;
+ OFF closer_beg;
+ OFF closer_end;
+ SZ mark_len;
+ OFF line_end;
+ int has_space_after_opener = FALSE;
+ int has_eol_after_opener = FALSE;
+ int has_space_before_closer = FALSE;
+ int has_eol_before_closer = FALSE;
+ int has_only_space = TRUE;
+ int line_index = 0;
+
+ line_end = lines[0].end;
+ opener_end = opener_beg;
+ while(opener_end < line_end && CH(opener_end) == _T('`'))
+ opener_end++;
+ has_space_after_opener = (opener_end < line_end && CH(opener_end) == _T(' '));
+ has_eol_after_opener = (opener_end == line_end);
+
+ /* The caller needs to know end of the opening mark even if we fail. */
+ *p_opener_end = opener_end;
+
+ mark_len = opener_end - opener_beg;
+ if(mark_len > CODESPAN_MARK_MAXLEN)
+ return FALSE;
+
+ /* Check whether we already know there is no closer of this length.
+ * If so, re-scan does no sense. This fixes issue #59. */
+ if(last_potential_closers[mark_len-1] >= lines[n_lines-1].end ||
+ (*p_reached_paragraph_end && last_potential_closers[mark_len-1] < opener_end))
+ return FALSE;
+
+ closer_beg = opener_end;
+ closer_end = opener_end;
+
+ /* Find closer mark. */
+ while(TRUE) {
+ while(closer_beg < line_end && CH(closer_beg) != _T('`')) {
+ if(CH(closer_beg) != _T(' '))
+ has_only_space = FALSE;
+ closer_beg++;
+ }
+ closer_end = closer_beg;
+ while(closer_end < line_end && CH(closer_end) == _T('`'))
+ closer_end++;
+
+ if(closer_end - closer_beg == mark_len) {
+ /* Success. */
+ has_space_before_closer = (closer_beg > lines[line_index].beg && CH(closer_beg-1) == _T(' '));
+ has_eol_before_closer = (closer_beg == lines[line_index].beg);
+ break;
+ }
+
+ if(closer_end - closer_beg > 0) {
+ /* We have found a back-tick which is not part of the closer. */
+ has_only_space = FALSE;
+
+ /* But if we eventually fail, remember it as a potential closer
+ * of its own length for future attempts. This mitigates needs for
+ * rescans. */
+ if(closer_end - closer_beg < CODESPAN_MARK_MAXLEN) {
+ if(closer_beg > last_potential_closers[closer_end - closer_beg - 1])
+ last_potential_closers[closer_end - closer_beg - 1] = closer_beg;
+ }
+ }
+
+ if(closer_end >= line_end) {
+ line_index++;
+ if(line_index >= n_lines) {
+ /* Reached end of the paragraph and still nothing. */
+ *p_reached_paragraph_end = TRUE;
+ return FALSE;
+ }
+ /* Try on the next line. */
+ line_end = lines[line_index].end;
+ closer_beg = lines[line_index].beg;
+ } else {
+ closer_beg = closer_end;
+ }
+ }
+
+ /* If there is a space or a new line both after and before the opener
+ * (and if the code span is not made of spaces only), consume one initial
+ * and one trailing space as part of the marks. */
+ if(!has_only_space &&
+ (has_space_after_opener || has_eol_after_opener) &&
+ (has_space_before_closer || has_eol_before_closer))
+ {
+ if(has_space_after_opener)
+ opener_end++;
+ else
+ opener_end = lines[1].beg;
+
+ if(has_space_before_closer)
+ closer_beg--;
+ else {
+ closer_beg = lines[line_index-1].end;
+ /* We need to eat the preceding "\r\n" but not any line trailing
+ * spaces. */
+ while(closer_beg < ctx->size && ISBLANK(closer_beg))
+ closer_beg++;
+ }
+ }
+
+ *p_opener_beg = opener_beg;
+ *p_opener_end = opener_end;
+ *p_closer_beg = closer_beg;
+ *p_closer_end = closer_end;
+ return TRUE;
+}
+
+static int
+md_is_autolink_uri(MD_CTX* ctx, OFF beg, OFF max_end, OFF* p_end)
+{
+ OFF off = beg+1;
+
+ MD_ASSERT(CH(beg) == _T('<'));
+
+ /* Check for scheme. */
+ if(off >= max_end || !ISASCII(off))
+ return FALSE;
+ off++;
+ while(1) {
+ if(off >= max_end)
+ return FALSE;
+ if(off - beg > 32)
+ return FALSE;
+ if(CH(off) == _T(':') && off - beg >= 3)
+ break;
+ if(!ISALNUM(off) && CH(off) != _T('+') && CH(off) != _T('-') && CH(off) != _T('.'))
+ return FALSE;
+ off++;
+ }
+
+ /* Check the path after the scheme. */
+ while(off < max_end && CH(off) != _T('>')) {
+ if(ISWHITESPACE(off) || ISCNTRL(off) || CH(off) == _T('<'))
+ return FALSE;
+ off++;
+ }
+
+ if(off >= max_end)
+ return FALSE;
+
+ MD_ASSERT(CH(off) == _T('>'));
+ *p_end = off+1;
+ return TRUE;
+}
+
+static int
+md_is_autolink_email(MD_CTX* ctx, OFF beg, OFF max_end, OFF* p_end)
+{
+ OFF off = beg + 1;
+ int label_len;
+
+ MD_ASSERT(CH(beg) == _T('<'));
+
+ /* The code should correspond to this regexp:
+ /^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+
+ @[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?
+ (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/
+ */
+
+ /* Username (before '@'). */
+ while(off < max_end && (ISALNUM(off) || ISANYOF(off, _T(".!#$%&'*+/=?^_`{|}~-"))))
+ off++;
+ if(off <= beg+1)
+ return FALSE;
+
+ /* '@' */
+ if(off >= max_end || CH(off) != _T('@'))
+ return FALSE;
+ off++;
+
+ /* Labels delimited with '.'; each label is sequence of 1 - 63 alnum
+ * characters or '-', but '-' is not allowed as first or last char. */
+ label_len = 0;
+ while(off < max_end) {
+ if(ISALNUM(off))
+ label_len++;
+ else if(CH(off) == _T('-') && label_len > 0)
+ label_len++;
+ else if(CH(off) == _T('.') && label_len > 0 && CH(off-1) != _T('-'))
+ label_len = 0;
+ else
+ break;
+
+ if(label_len > 63)
+ return FALSE;
+
+ off++;
+ }
+
+ if(label_len <= 0 || off >= max_end || CH(off) != _T('>') || CH(off-1) == _T('-'))
+ return FALSE;
+
+ *p_end = off+1;
+ return TRUE;
+}
+
+static int
+md_is_autolink(MD_CTX* ctx, OFF beg, OFF max_end, OFF* p_end, int* p_missing_mailto)
+{
+ if(md_is_autolink_uri(ctx, beg, max_end, p_end)) {
+ *p_missing_mailto = FALSE;
+ return TRUE;
+ }
+
+ if(md_is_autolink_email(ctx, beg, max_end, p_end)) {
+ *p_missing_mailto = TRUE;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static int
+md_collect_marks(MD_CTX* ctx, const MD_LINE* lines, int n_lines, int table_mode)
+{
+ const MD_LINE* line_term = lines + n_lines;
+ const MD_LINE* line;
+ int ret = 0;
+ MD_MARK* mark;
+ OFF codespan_last_potential_closers[CODESPAN_MARK_MAXLEN] = { 0 };
+ int codespan_scanned_till_paragraph_end = FALSE;
+
+ for(line = lines; line < line_term; line++) {
+ OFF off = line->beg;
+ OFF line_end = line->end;
+
+ while(TRUE) {
+ CHAR ch;
+
+#ifdef MD4C_USE_UTF16
+ /* For UTF-16, mark_char_map[] covers only ASCII. */
+ #define IS_MARK_CHAR(off) ((CH(off) < SIZEOF_ARRAY(ctx->mark_char_map)) && \
+ (ctx->mark_char_map[(unsigned char) CH(off)]))
+#else
+ /* For 8-bit encodings, mark_char_map[] covers all 256 elements. */
+ #define IS_MARK_CHAR(off) (ctx->mark_char_map[(unsigned char) CH(off)])
+#endif
+
+ /* Optimization: Use some loop unrolling. */
+ while(off + 3 < line_end && !IS_MARK_CHAR(off+0) && !IS_MARK_CHAR(off+1)
+ && !IS_MARK_CHAR(off+2) && !IS_MARK_CHAR(off+3))
+ off += 4;
+ while(off < line_end && !IS_MARK_CHAR(off+0))
+ off++;
+
+ if(off >= line_end)
+ break;
+
+ ch = CH(off);
+
+ /* A backslash escape.
+ * It can go beyond line->end as it may involve escaped new
+ * line to form a hard break. */
+ if(ch == _T('\\') && off+1 < ctx->size && (ISPUNCT(off+1) || ISNEWLINE(off+1))) {
+ /* Hard-break cannot be on the last line of the block. */
+ if(!ISNEWLINE(off+1) || line+1 < line_term)
+ PUSH_MARK(ch, off, off+2, MD_MARK_RESOLVED);
+ off += 2;
+ continue;
+ }
+
+ /* A potential (string) emphasis start/end. */
+ if(ch == _T('*') || ch == _T('_')) {
+ OFF tmp = off+1;
+ int left_level; /* What precedes: 0 = whitespace; 1 = punctuation; 2 = other char. */
+ int right_level; /* What follows: 0 = whitespace; 1 = punctuation; 2 = other char. */
+
+ while(tmp < line_end && CH(tmp) == ch)
+ tmp++;
+
+ if(off == line->beg || ISUNICODEWHITESPACEBEFORE(off))
+ left_level = 0;
+ else if(ISUNICODEPUNCTBEFORE(off))
+ left_level = 1;
+ else
+ left_level = 2;
+
+ if(tmp == line_end || ISUNICODEWHITESPACE(tmp))
+ right_level = 0;
+ else if(ISUNICODEPUNCT(tmp))
+ right_level = 1;
+ else
+ right_level = 2;
+
+ /* Intra-word underscore doesn't have special meaning. */
+ if(ch == _T('_') && left_level == 2 && right_level == 2) {
+ left_level = 0;
+ right_level = 0;
+ }
+
+ if(left_level != 0 || right_level != 0) {
+ unsigned flags = 0;
+
+ if(left_level > 0 && left_level >= right_level)
+ flags |= MD_MARK_POTENTIAL_CLOSER;
+ if(right_level > 0 && right_level >= left_level)
+ flags |= MD_MARK_POTENTIAL_OPENER;
+ if(left_level == 2 && right_level == 2)
+ flags |= MD_MARK_EMPH_INTRAWORD;
+
+ /* For "the rule of three" we need to remember the original
+ * size of the mark (modulo three), before we potentially
+ * split the mark when being later resolved partially by some
+ * shorter closer. */
+ switch((tmp - off) % 3) {
+ case 0: flags |= MD_MARK_EMPH_MOD3_0; break;
+ case 1: flags |= MD_MARK_EMPH_MOD3_1; break;
+ case 2: flags |= MD_MARK_EMPH_MOD3_2; break;
+ }
+
+ PUSH_MARK(ch, off, tmp, flags);
+
+ /* During resolving, multiple asterisks may have to be
+ * split into independent span start/ends. Consider e.g.
+ * "**foo* bar*". Therefore we push also some empty dummy
+ * marks to have enough space for that. */
+ off++;
+ while(off < tmp) {
+ PUSH_MARK('D', off, off, 0);
+ off++;
+ }
+ continue;
+ }
+
+ off = tmp;
+ continue;
+ }
+
+ /* A potential code span start/end. */
+ if(ch == _T('`')) {
+ OFF opener_beg, opener_end;
+ OFF closer_beg, closer_end;
+ int is_code_span;
+
+ is_code_span = md_is_code_span(ctx, line, line_term - line, off,
+ &opener_beg, &opener_end, &closer_beg, &closer_end,
+ codespan_last_potential_closers,
+ &codespan_scanned_till_paragraph_end);
+ if(is_code_span) {
+ PUSH_MARK(_T('`'), opener_beg, opener_end, MD_MARK_OPENER | MD_MARK_RESOLVED);
+ PUSH_MARK(_T('`'), closer_beg, closer_end, MD_MARK_CLOSER | MD_MARK_RESOLVED);
+ ctx->marks[ctx->n_marks-2].next = ctx->n_marks-1;
+ ctx->marks[ctx->n_marks-1].prev = ctx->n_marks-2;
+
+ off = closer_end;
+
+ /* Advance the current line accordingly. */
+ if(off > line_end) {
+ line = md_lookup_line(off, line, line_term - line);
+ line_end = line->end;
+ }
+ continue;
+ }
+
+ off = opener_end;
+ continue;
+ }
+
+ /* A potential entity start. */
+ if(ch == _T('&')) {
+ PUSH_MARK(ch, off, off+1, MD_MARK_POTENTIAL_OPENER);
+ off++;
+ continue;
+ }
+
+ /* A potential entity end. */
+ if(ch == _T(';')) {
+ /* We surely cannot be entity unless the previous mark is '&'. */
+ if(ctx->n_marks > 0 && ctx->marks[ctx->n_marks-1].ch == _T('&'))
+ PUSH_MARK(ch, off, off+1, MD_MARK_POTENTIAL_CLOSER);
+
+ off++;
+ continue;
+ }
+
+ /* A potential autolink or raw HTML start/end. */
+ if(ch == _T('<')) {
+ int is_autolink;
+ OFF autolink_end;
+ int missing_mailto;
+
+ if(!(ctx->parser.flags & MD_FLAG_NOHTMLSPANS)) {
+ int is_html;
+ OFF html_end;
+
+ /* Given the nature of the raw HTML, we have to recognize
+ * it here. Doing so later in md_analyze_lt_gt() could
+ * open can of worms of quadratic complexity. */
+ is_html = md_is_html_any(ctx, line, line_term - line, off,
+ lines[n_lines-1].end, &html_end);
+ if(is_html) {
+ PUSH_MARK(_T('<'), off, off, MD_MARK_OPENER | MD_MARK_RESOLVED);
+ PUSH_MARK(_T('>'), html_end, html_end, MD_MARK_CLOSER | MD_MARK_RESOLVED);
+ ctx->marks[ctx->n_marks-2].next = ctx->n_marks-1;
+ ctx->marks[ctx->n_marks-1].prev = ctx->n_marks-2;
+ off = html_end;
+
+ /* Advance the current line accordingly. */
+ if(off > line_end) {
+ line = md_lookup_line(off, line, line_term - line);
+ line_end = line->end;
+ }
+ continue;
+ }
+ }
+
+ is_autolink = md_is_autolink(ctx, off, lines[n_lines-1].end,
+ &autolink_end, &missing_mailto);
+ if(is_autolink) {
+ PUSH_MARK((missing_mailto ? _T('@') : _T('<')), off, off+1,
+ MD_MARK_OPENER | MD_MARK_RESOLVED | MD_MARK_AUTOLINK);
+ PUSH_MARK(_T('>'), autolink_end-1, autolink_end,
+ MD_MARK_CLOSER | MD_MARK_RESOLVED | MD_MARK_AUTOLINK);
+ ctx->marks[ctx->n_marks-2].next = ctx->n_marks-1;
+ ctx->marks[ctx->n_marks-1].prev = ctx->n_marks-2;
+ off = autolink_end;
+ continue;
+ }
+
+ off++;
+ continue;
+ }
+
+ /* A potential link or its part. */
+ if(ch == _T('[') || (ch == _T('!') && off+1 < line_end && CH(off+1) == _T('['))) {
+ OFF tmp = (ch == _T('[') ? off+1 : off+2);
+ PUSH_MARK(ch, off, tmp, MD_MARK_POTENTIAL_OPENER);
+ off = tmp;
+ /* Two dummies to make enough place for data we need if it is
+ * a link. */
+ PUSH_MARK('D', off, off, 0);
+ PUSH_MARK('D', off, off, 0);
+ continue;
+ }
+ if(ch == _T(']')) {
+ PUSH_MARK(ch, off, off+1, MD_MARK_POTENTIAL_CLOSER);
+ off++;
+ continue;
+ }
+
+ /* A potential permissive e-mail autolink. */
+ if(ch == _T('@')) {
+ if(line->beg + 1 <= off && ISALNUM(off-1) &&
+ off + 3 < line->end && ISALNUM(off+1))
+ {
+ PUSH_MARK(ch, off, off+1, MD_MARK_POTENTIAL_OPENER);
+ /* Push a dummy as a reserve for a closer. */
+ PUSH_MARK('D', off, off, 0);
+ }
+
+ off++;
+ continue;
+ }
+
+ /* A potential permissive URL autolink. */
+ if(ch == _T(':')) {
+ static struct {
+ const CHAR* scheme;
+ SZ scheme_size;
+ const CHAR* suffix;
+ SZ suffix_size;
+ } scheme_map[] = {
+ /* In the order from the most frequently used, arguably. */
+ { _T("http"), 4, _T("//"), 2 },
+ { _T("https"), 5, _T("//"), 2 },
+ { _T("ftp"), 3, _T("//"), 2 }
+ };
+ int scheme_index;
+
+ for(scheme_index = 0; scheme_index < (int) SIZEOF_ARRAY(scheme_map); scheme_index++) {
+ const CHAR* scheme = scheme_map[scheme_index].scheme;
+ const SZ scheme_size = scheme_map[scheme_index].scheme_size;
+ const CHAR* suffix = scheme_map[scheme_index].suffix;
+ const SZ suffix_size = scheme_map[scheme_index].suffix_size;
+
+ if(line->beg + scheme_size <= off && md_ascii_eq(STR(off-scheme_size), scheme, scheme_size) &&
+ (line->beg + scheme_size == off || ISWHITESPACE(off-scheme_size-1) || ISANYOF(off-scheme_size-1, _T("*_~(["))) &&
+ off + 1 + suffix_size < line->end && md_ascii_eq(STR(off+1), suffix, suffix_size))
+ {
+ PUSH_MARK(ch, off-scheme_size, off+1+suffix_size, MD_MARK_POTENTIAL_OPENER);
+ /* Push a dummy as a reserve for a closer. */
+ PUSH_MARK('D', off, off, 0);
+ off += 1 + suffix_size;
+ break;
+ }
+ }
+
+ off++;
+ continue;
+ }
+
+ /* A potential permissive WWW autolink. */
+ if(ch == _T('.')) {
+ if(line->beg + 3 <= off && md_ascii_eq(STR(off-3), _T("www"), 3) &&
+ (line->beg + 3 == off || ISWHITESPACE(off-4) || ISANYOF(off-4, _T("*_~(["))) &&
+ off + 1 < line_end)
+ {
+ PUSH_MARK(ch, off-3, off+1, MD_MARK_POTENTIAL_OPENER);
+ /* Push a dummy as a reserve for a closer. */
+ PUSH_MARK('D', off, off, 0);
+ off++;
+ continue;
+ }
+
+ off++;
+ continue;
+ }
+
+ /* A potential table cell boundary or wiki link label delimiter. */
+ if((table_mode || ctx->parser.flags & MD_FLAG_WIKILINKS) && ch == _T('|')) {
+ PUSH_MARK(ch, off, off+1, 0);
+ off++;
+ continue;
+ }
+
+ /* A potential strikethrough start/end. */
+ if(ch == _T('~')) {
+ OFF tmp = off+1;
+
+ while(tmp < line_end && CH(tmp) == _T('~'))
+ tmp++;
+
+ if(tmp - off < 3) {
+ unsigned flags = 0;
+
+ if(tmp < line_end && !ISUNICODEWHITESPACE(tmp))
+ flags |= MD_MARK_POTENTIAL_OPENER;
+ if(off > line->beg && !ISUNICODEWHITESPACEBEFORE(off))
+ flags |= MD_MARK_POTENTIAL_CLOSER;
+ if(flags != 0)
+ PUSH_MARK(ch, off, tmp, flags);
+ }
+
+ off = tmp;
+ continue;
+ }
+
+ /* A potential equation start/end */
+ if(ch == _T('$')) {
+ /* We can have at most two consecutive $ signs,
+ * where two dollar signs signify a display equation. */
+ OFF tmp = off+1;
+
+ while(tmp < line_end && CH(tmp) == _T('$'))
+ tmp++;
+
+ if (tmp - off <= 2)
+ PUSH_MARK(ch, off, tmp, MD_MARK_POTENTIAL_OPENER | MD_MARK_POTENTIAL_CLOSER);
+ off = tmp;
+ continue;
+ }
+
+ /* Turn non-trivial whitespace into single space. */
+ if(ISWHITESPACE_(ch)) {
+ OFF tmp = off+1;
+
+ while(tmp < line_end && ISWHITESPACE(tmp))
+ tmp++;
+
+ if(tmp - off > 1 || ch != _T(' '))
+ PUSH_MARK(ch, off, tmp, MD_MARK_RESOLVED);
+
+ off = tmp;
+ continue;
+ }
+
+ /* NULL character. */
+ if(ch == _T('\0')) {
+ PUSH_MARK(ch, off, off+1, MD_MARK_RESOLVED);
+ off++;
+ continue;
+ }
+
+ off++;
+ }
+ }
+
+ /* Add a dummy mark at the end of the mark vector to simplify
+ * process_inlines(). */
+ PUSH_MARK(127, ctx->size, ctx->size, MD_MARK_RESOLVED);
+
+abort:
+ return ret;
+}
+
+static void
+md_analyze_bracket(MD_CTX* ctx, int mark_index)
+{
+ /* We cannot really resolve links here as for that we would need
+ * more context. E.g. a following pair of brackets (reference link),
+ * or enclosing pair of brackets (if the inner is the link, the outer
+ * one cannot be.)
+ *
+ * Therefore we here only construct a list of '[' ']' pairs ordered by
+ * position of the closer. This allows us to analyze what is or is not
+ * link in the right order, from inside to outside in case of nested
+ * brackets.
+ *
+ * The resolving itself is deferred to md_resolve_links().
+ */
+
+ MD_MARK* mark = &ctx->marks[mark_index];
+
+ if(mark->flags & MD_MARK_POTENTIAL_OPENER) {
+ if(BRACKET_OPENERS.head != -1)
+ ctx->marks[BRACKET_OPENERS.tail].flags |= MD_MARK_HASNESTEDBRACKETS;
+
+ md_mark_chain_append(ctx, &BRACKET_OPENERS, mark_index);
+ return;
+ }
+
+ if(BRACKET_OPENERS.tail >= 0) {
+ /* Pop the opener from the chain. */
+ int opener_index = BRACKET_OPENERS.tail;
+ MD_MARK* opener = &ctx->marks[opener_index];
+ if(opener->prev >= 0)
+ ctx->marks[opener->prev].next = -1;
+ else
+ BRACKET_OPENERS.head = -1;
+ BRACKET_OPENERS.tail = opener->prev;
+
+ /* Interconnect the opener and closer. */
+ opener->next = mark_index;
+ mark->prev = opener_index;
+
+ /* Add the pair into chain of potential links for md_resolve_links().
+ * Note we misuse opener->prev for this as opener->next points to its
+ * closer. */
+ if(ctx->unresolved_link_tail >= 0)
+ ctx->marks[ctx->unresolved_link_tail].prev = opener_index;
+ else
+ ctx->unresolved_link_head = opener_index;
+ ctx->unresolved_link_tail = opener_index;
+ opener->prev = -1;
+ }
+}
+
+/* Forward declaration. */
+static void md_analyze_link_contents(MD_CTX* ctx, const MD_LINE* lines, int n_lines,
+ int mark_beg, int mark_end);
+
+static int
+md_resolve_links(MD_CTX* ctx, const MD_LINE* lines, int n_lines)
+{
+ int opener_index = ctx->unresolved_link_head;
+ OFF last_link_beg = 0;
+ OFF last_link_end = 0;
+ OFF last_img_beg = 0;
+ OFF last_img_end = 0;
+
+ while(opener_index >= 0) {
+ MD_MARK* opener = &ctx->marks[opener_index];
+ int closer_index = opener->next;
+ MD_MARK* closer = &ctx->marks[closer_index];
+ int next_index = opener->prev;
+ MD_MARK* next_opener;
+ MD_MARK* next_closer;
+ MD_LINK_ATTR attr;
+ int is_link = FALSE;
+
+ if(next_index >= 0) {
+ next_opener = &ctx->marks[next_index];
+ next_closer = &ctx->marks[next_opener->next];
+ } else {
+ next_opener = NULL;
+ next_closer = NULL;
+ }
+
+ /* If nested ("[ [ ] ]"), we need to make sure that:
+ * - The outer does not end inside of (...) belonging to the inner.
+ * - The outer cannot be link if the inner is link (i.e. not image).
+ *
+ * (Note we here analyze from inner to outer as the marks are ordered
+ * by closer->beg.)
+ */
+ if((opener->beg < last_link_beg && closer->end < last_link_end) ||
+ (opener->beg < last_img_beg && closer->end < last_img_end) ||
+ (opener->beg < last_link_end && opener->ch == '['))
+ {
+ opener_index = next_index;
+ continue;
+ }
+
+ /* Recognize and resolve wiki links.
+ * Wiki-links maybe '[[destination]]' or '[[destination|label]]'.
+ */
+ if ((ctx->parser.flags & MD_FLAG_WIKILINKS) &&
+ (opener->end - opener->beg == 1) && /* not image */
+ next_opener != NULL && /* double '[' opener */
+ next_opener->ch == '[' &&
+ (next_opener->beg == opener->beg - 1) &&
+ (next_opener->end - next_opener->beg == 1) &&
+ next_closer != NULL && /* double ']' closer */
+ next_closer->ch == ']' &&
+ (next_closer->beg == closer->beg + 1) &&
+ (next_closer->end - next_closer->beg == 1))
+ {
+ MD_MARK* delim = NULL;
+ int delim_index;
+ OFF dest_beg, dest_end;
+
+ is_link = TRUE;
+
+ /* We don't allow destination to be longer than 100 characters.
+ * Lets scan to see whether there is '|'. (If not then the whole
+ * wiki-link has to be below the 100 characters.) */
+ delim_index = opener_index + 1;
+ while(delim_index < closer_index) {
+ MD_MARK* m = &ctx->marks[delim_index];
+ if(m->ch == '|') {
+ delim = m;
+ break;
+ }
+ if(m->ch != 'D' && m->beg - opener->end > 100)
+ break;
+ delim_index++;
+ }
+ dest_beg = opener->end;
+ dest_end = (delim != NULL) ? delim->beg : closer->beg;
+ if(dest_end - dest_beg == 0 || dest_end - dest_beg > 100)
+ is_link = FALSE;
+
+ /* There may not be any new line in the destination. */
+ if(is_link) {
+ OFF off;
+ for(off = dest_beg; off < dest_end; off++) {
+ if(ISNEWLINE(off)) {
+ is_link = FALSE;
+ break;
+ }
+ }
+ }
+
+ if(is_link) {
+ if(delim != NULL) {
+ if(delim->end < closer->beg) {
+ md_rollback(ctx, opener_index, delim_index, MD_ROLLBACK_ALL);
+ md_rollback(ctx, delim_index, closer_index, MD_ROLLBACK_CROSSING);
+ delim->flags |= MD_MARK_RESOLVED;
+ opener->end = delim->beg;
+ } else {
+ /* The pipe is just before the closer: [[foo|]] */
+ md_rollback(ctx, opener_index, closer_index, MD_ROLLBACK_ALL);
+ closer->beg = delim->beg;
+ delim = NULL;
+ }
+ }
+
+ opener->beg = next_opener->beg;
+ opener->next = closer_index;
+ opener->flags |= MD_MARK_OPENER | MD_MARK_RESOLVED;
+
+ closer->end = next_closer->end;
+ closer->prev = opener_index;
+ closer->flags |= MD_MARK_CLOSER | MD_MARK_RESOLVED;
+
+ last_link_beg = opener->beg;
+ last_link_end = closer->end;
+
+ if(delim != NULL)
+ md_analyze_link_contents(ctx, lines, n_lines, delim_index+1, closer_index);
+
+ opener_index = next_opener->prev;
+ continue;
+ }
+ }
+
+ if(next_opener != NULL && next_opener->beg == closer->end) {
+ if(next_closer->beg > closer->end + 1) {
+ /* Might be full reference link. */
+ if(!(next_opener->flags & MD_MARK_HASNESTEDBRACKETS))
+ is_link = md_is_link_reference(ctx, lines, n_lines, next_opener->beg, next_closer->end, &attr);
+ } else {
+ /* Might be shortcut reference link. */
+ if(!(opener->flags & MD_MARK_HASNESTEDBRACKETS))
+ is_link = md_is_link_reference(ctx, lines, n_lines, opener->beg, closer->end, &attr);
+ }
+
+ if(is_link < 0)
+ return -1;
+
+ if(is_link) {
+ /* Eat the 2nd "[...]". */
+ closer->end = next_closer->end;
+
+ /* Do not analyze the label as a standalone link in the next
+ * iteration. */
+ next_index = ctx->marks[next_index].prev;
+ }
+ } else {
+ if(closer->end < ctx->size && CH(closer->end) == _T('(')) {
+ /* Might be inline link. */
+ OFF inline_link_end = UINT_MAX;
+
+ is_link = md_is_inline_link_spec(ctx, lines, n_lines, closer->end, &inline_link_end, &attr);
+ if(is_link < 0)
+ return -1;
+
+ /* Check the closing ')' is not inside an already resolved range
+ * (i.e. a range with a higher priority), e.g. a code span. */
+ if(is_link) {
+ int i = closer_index + 1;
+
+ while(i < ctx->n_marks) {
+ MD_MARK* mark = &ctx->marks[i];
+
+ if(mark->beg >= inline_link_end)
+ break;
+ if((mark->flags & (MD_MARK_OPENER | MD_MARK_RESOLVED)) == (MD_MARK_OPENER | MD_MARK_RESOLVED)) {
+ if(ctx->marks[mark->next].beg >= inline_link_end) {
+ /* Cancel the link status. */
+ if(attr.title_needs_free)
+ free(attr.title);
+ is_link = FALSE;
+ break;
+ }
+
+ i = mark->next + 1;
+ } else {
+ i++;
+ }
+ }
+ }
+
+ if(is_link) {
+ /* Eat the "(...)" */
+ closer->end = inline_link_end;
+ }
+ }
+
+ if(!is_link) {
+ /* Might be collapsed reference link. */
+ if(!(opener->flags & MD_MARK_HASNESTEDBRACKETS))
+ is_link = md_is_link_reference(ctx, lines, n_lines, opener->beg, closer->end, &attr);
+ if(is_link < 0)
+ return -1;
+ }
+ }
+
+ if(is_link) {
+ /* Resolve the brackets as a link. */
+ opener->flags |= MD_MARK_OPENER | MD_MARK_RESOLVED;
+ closer->flags |= MD_MARK_CLOSER | MD_MARK_RESOLVED;
+
+ /* If it is a link, we store the destination and title in the two
+ * dummy marks after the opener. */
+ MD_ASSERT(ctx->marks[opener_index+1].ch == 'D');
+ ctx->marks[opener_index+1].beg = attr.dest_beg;
+ ctx->marks[opener_index+1].end = attr.dest_end;
+
+ MD_ASSERT(ctx->marks[opener_index+2].ch == 'D');
+ md_mark_store_ptr(ctx, opener_index+2, attr.title);
+ /* The title might or might not have been allocated for us. */
+ if(attr.title_needs_free)
+ md_mark_chain_append(ctx, &PTR_CHAIN, opener_index+2);
+ ctx->marks[opener_index+2].prev = attr.title_size;
+
+ if(opener->ch == '[') {
+ last_link_beg = opener->beg;
+ last_link_end = closer->end;
+ } else {
+ last_img_beg = opener->beg;
+ last_img_end = closer->end;
+ }
+
+ md_analyze_link_contents(ctx, lines, n_lines, opener_index+1, closer_index);
+
+ /* If the link text is formed by nothing but permissive autolink,
+ * suppress the autolink.
+ * See https://github.com/mity/md4c/issues/152 for more info. */
+ if(ctx->parser.flags & MD_FLAG_PERMISSIVEAUTOLINKS) {
+ MD_MARK* first_nested;
+ MD_MARK* last_nested;
+
+ first_nested = opener + 1;
+ while(first_nested->ch == _T('D') && first_nested < closer)
+ first_nested++;
+
+ last_nested = closer - 1;
+ while(first_nested->ch == _T('D') && last_nested > opener)
+ last_nested--;
+
+ if((first_nested->flags & MD_MARK_RESOLVED) &&
+ first_nested->beg == opener->end &&
+ ISANYOF_(first_nested->ch, _T("@:.")) &&
+ first_nested->next == (last_nested - ctx->marks) &&
+ last_nested->end == closer->beg)
+ {
+ first_nested->ch = _T('D');
+ first_nested->flags &= ~MD_MARK_RESOLVED;
+ last_nested->ch = _T('D');
+ last_nested->flags &= ~MD_MARK_RESOLVED;
+ }
+ }
+ }
+
+ opener_index = next_index;
+ }
+
+ return 0;
+}
+
+/* Analyze whether the mark '&' starts a HTML entity.
+ * If so, update its flags as well as flags of corresponding closer ';'. */
+static void
+md_analyze_entity(MD_CTX* ctx, int mark_index)
+{
+ MD_MARK* opener = &ctx->marks[mark_index];
+ MD_MARK* closer;
+ OFF off;
+
+ /* Cannot be entity if there is no closer as the next mark.
+ * (Any other mark between would mean strange character which cannot be
+ * part of the entity.
+ *
+ * So we can do all the work on '&' and do not call this later for the
+ * closing mark ';'.
+ */
+ if(mark_index + 1 >= ctx->n_marks)
+ return;
+ closer = &ctx->marks[mark_index+1];
+ if(closer->ch != ';')
+ return;
+
+ if(md_is_entity(ctx, opener->beg, closer->end, &off)) {
+ MD_ASSERT(off == closer->end);
+
+ md_resolve_range(ctx, NULL, mark_index, mark_index+1);
+ opener->end = closer->end;
+ }
+}
+
+static void
+md_analyze_table_cell_boundary(MD_CTX* ctx, int mark_index)
+{
+ MD_MARK* mark = &ctx->marks[mark_index];
+ mark->flags |= MD_MARK_RESOLVED;
+
+ md_mark_chain_append(ctx, &TABLECELLBOUNDARIES, mark_index);
+ ctx->n_table_cell_boundaries++;
+}
+
+/* Split a longer mark into two. The new mark takes the given count of
+ * characters. May only be called if an adequate number of dummy 'D' marks
+ * follows.
+ */
+static int
+md_split_emph_mark(MD_CTX* ctx, int mark_index, SZ n)
+{
+ MD_MARK* mark = &ctx->marks[mark_index];
+ int new_mark_index = mark_index + (mark->end - mark->beg - n);
+ MD_MARK* dummy = &ctx->marks[new_mark_index];
+
+ MD_ASSERT(mark->end - mark->beg > n);
+ MD_ASSERT(dummy->ch == 'D');
+
+ memcpy(dummy, mark, sizeof(MD_MARK));
+ mark->end -= n;
+ dummy->beg = mark->end;
+
+ return new_mark_index;
+}
+
+static void
+md_analyze_emph(MD_CTX* ctx, int mark_index)
+{
+ MD_MARK* mark = &ctx->marks[mark_index];
+ MD_MARKCHAIN* chain = md_mark_chain(ctx, mark_index);
+
+ /* If we can be a closer, try to resolve with the preceding opener. */
+ if(mark->flags & MD_MARK_POTENTIAL_CLOSER) {
+ MD_MARK* opener = NULL;
+ int opener_index = 0;
+
+ if(mark->ch == _T('*')) {
+ MD_MARKCHAIN* opener_chains[6];
+ int i, n_opener_chains;
+ unsigned flags = mark->flags;
+
+ /* Apply the "rule of three". */
+ n_opener_chains = 0;
+ opener_chains[n_opener_chains++] = &ASTERISK_OPENERS_intraword_mod3_0;
+ if((flags & MD_MARK_EMPH_MOD3_MASK) != MD_MARK_EMPH_MOD3_2)
+ opener_chains[n_opener_chains++] = &ASTERISK_OPENERS_intraword_mod3_1;
+ if((flags & MD_MARK_EMPH_MOD3_MASK) != MD_MARK_EMPH_MOD3_1)
+ opener_chains[n_opener_chains++] = &ASTERISK_OPENERS_intraword_mod3_2;
+ opener_chains[n_opener_chains++] = &ASTERISK_OPENERS_extraword_mod3_0;
+ if(!(flags & MD_MARK_EMPH_INTRAWORD) || (flags & MD_MARK_EMPH_MOD3_MASK) != MD_MARK_EMPH_MOD3_2)
+ opener_chains[n_opener_chains++] = &ASTERISK_OPENERS_extraword_mod3_1;
+ if(!(flags & MD_MARK_EMPH_INTRAWORD) || (flags & MD_MARK_EMPH_MOD3_MASK) != MD_MARK_EMPH_MOD3_1)
+ opener_chains[n_opener_chains++] = &ASTERISK_OPENERS_extraword_mod3_2;
+
+ /* Opener is the most recent mark from the allowed chains. */
+ for(i = 0; i < n_opener_chains; i++) {
+ if(opener_chains[i]->tail >= 0) {
+ int tmp_index = opener_chains[i]->tail;
+ MD_MARK* tmp_mark = &ctx->marks[tmp_index];
+ if(opener == NULL || tmp_mark->end > opener->end) {
+ opener_index = tmp_index;
+ opener = tmp_mark;
+ }
+ }
+ }
+ } else {
+ /* Simple emph. mark */
+ if(chain->tail >= 0) {
+ opener_index = chain->tail;
+ opener = &ctx->marks[opener_index];
+ }
+ }
+
+ /* Resolve, if we have found matching opener. */
+ if(opener != NULL) {
+ SZ opener_size = opener->end - opener->beg;
+ SZ closer_size = mark->end - mark->beg;
+ MD_MARKCHAIN* opener_chain = md_mark_chain(ctx, opener_index);
+
+ if(opener_size > closer_size) {
+ opener_index = md_split_emph_mark(ctx, opener_index, closer_size);
+ md_mark_chain_append(ctx, opener_chain, opener_index);
+ } else if(opener_size < closer_size) {
+ md_split_emph_mark(ctx, mark_index, closer_size - opener_size);
+ }
+
+ md_rollback(ctx, opener_index, mark_index, MD_ROLLBACK_CROSSING);
+ md_resolve_range(ctx, opener_chain, opener_index, mark_index);
+ return;
+ }
+ }
+
+ /* If we could not resolve as closer, we may be yet be an opener. */
+ if(mark->flags & MD_MARK_POTENTIAL_OPENER)
+ md_mark_chain_append(ctx, chain, mark_index);
+}
+
+static void
+md_analyze_tilde(MD_CTX* ctx, int mark_index)
+{
+ MD_MARK* mark = &ctx->marks[mark_index];
+ MD_MARKCHAIN* chain = md_mark_chain(ctx, mark_index);
+
+ /* We attempt to be Github Flavored Markdown compatible here. GFM accepts
+ * only tildes sequences of length 1 and 2, and the length of the opener
+ * and closer has to match. */
+
+ if((mark->flags & MD_MARK_POTENTIAL_CLOSER) && chain->head >= 0) {
+ int opener_index = chain->head;
+
+ md_rollback(ctx, opener_index, mark_index, MD_ROLLBACK_CROSSING);
+ md_resolve_range(ctx, chain, opener_index, mark_index);
+ return;
+ }
+
+ if(mark->flags & MD_MARK_POTENTIAL_OPENER)
+ md_mark_chain_append(ctx, chain, mark_index);
+}
+
+static void
+md_analyze_dollar(MD_CTX* ctx, int mark_index)
+{
+ /* This should mimic the way inline equations work in LaTeX, so there
+ * can only ever be one item in the chain (i.e. the dollars can't be
+ * nested). This is basically the same as the md_analyze_tilde function,
+ * except that we require matching openers and closers to be of the same
+ * length.
+ *
+ * E.g.: $abc$$def$$ => abc (display equation) def (end equation) */
+ if(DOLLAR_OPENERS.head >= 0) {
+ /* If the potential closer has a non-matching number of $, discard */
+ MD_MARK* open = &ctx->marks[DOLLAR_OPENERS.head];
+ MD_MARK* close = &ctx->marks[mark_index];
+
+ int opener_index = DOLLAR_OPENERS.head;
+ md_rollback(ctx, opener_index, mark_index, MD_ROLLBACK_ALL);
+ if (open->end - open->beg == close->end - close->beg) {
+ /* We are the matching closer */
+ md_resolve_range(ctx, &DOLLAR_OPENERS, opener_index, mark_index);
+ return;
+ }
+ }
+
+ md_mark_chain_append(ctx, &DOLLAR_OPENERS, mark_index);
+}
+
+static void
+md_analyze_permissive_url_autolink(MD_CTX* ctx, int mark_index)
+{
+ MD_MARK* opener = &ctx->marks[mark_index];
+ int closer_index = mark_index + 1;
+ MD_MARK* closer = &ctx->marks[closer_index];
+ MD_MARK* next_resolved_mark;
+ OFF off = opener->end;
+ int n_dots = FALSE;
+ int has_underscore_in_last_seg = FALSE;
+ int has_underscore_in_next_to_last_seg = FALSE;
+ int n_opened_parenthesis = 0;
+ int n_excess_parenthesis = 0;
+
+ /* Check for domain. */
+ while(off < ctx->size) {
+ if(ISALNUM(off) || CH(off) == _T('-')) {
+ off++;
+ } else if(CH(off) == _T('.')) {
+ /* We must see at least one period. */
+ n_dots++;
+ has_underscore_in_next_to_last_seg = has_underscore_in_last_seg;
+ has_underscore_in_last_seg = FALSE;
+ off++;
+ } else if(CH(off) == _T('_')) {
+ /* No underscore may be present in the last two domain segments. */
+ has_underscore_in_last_seg = TRUE;
+ off++;
+ } else {
+ break;
+ }
+ }
+ if(off > opener->end && CH(off-1) == _T('.')) {
+ off--;
+ n_dots--;
+ }
+ if(off <= opener->end || n_dots == 0 || has_underscore_in_next_to_last_seg || has_underscore_in_last_seg)
+ return;
+
+ /* Check for path. */
+ next_resolved_mark = closer + 1;
+ while(next_resolved_mark->ch == 'D' || !(next_resolved_mark->flags & MD_MARK_RESOLVED))
+ next_resolved_mark++;
+ while(off < next_resolved_mark->beg && CH(off) != _T('<') && !ISWHITESPACE(off) && !ISNEWLINE(off)) {
+ /* Parenthesis must be balanced. */
+ if(CH(off) == _T('(')) {
+ n_opened_parenthesis++;
+ } else if(CH(off) == _T(')')) {
+ if(n_opened_parenthesis > 0)
+ n_opened_parenthesis--;
+ else
+ n_excess_parenthesis++;
+ }
+
+ off++;
+ }
+
+ /* Trim a trailing punctuation from the end. */
+ while(TRUE) {
+ if(ISANYOF(off-1, _T("?!.,:*_~"))) {
+ off--;
+ } else if(CH(off-1) == ')' && n_excess_parenthesis > 0) {
+ /* Unmatched ')' can be in an interior of the path but not at the
+ * of it, so the auto-link may be safely nested in a parenthesis
+ * pair. */
+ off--;
+ n_excess_parenthesis--;
+ } else {
+ break;
+ }
+ }
+
+ /* Ok. Lets call it an auto-link. Adapt opener and create closer to zero
+ * length so all the contents becomes the link text. */
+ MD_ASSERT(closer->ch == 'D' ||
+ ((ctx->parser.flags & MD_FLAG_PERMISSIVEWWWAUTOLINKS) &&
+ (closer->ch == '.' || closer->ch == ':' || closer->ch == '@')));
+ opener->end = opener->beg;
+ closer->ch = opener->ch;
+ closer->beg = off;
+ closer->end = off;
+ md_resolve_range(ctx, NULL, mark_index, closer_index);
+}
+
+/* The permissive autolinks do not have to be enclosed in '<' '>' but we
+ * instead impose stricter rules what is understood as an e-mail address
+ * here. Actually any non-alphanumeric characters with exception of '.'
+ * are prohibited both in username and after '@'. */
+static void
+md_analyze_permissive_email_autolink(MD_CTX* ctx, int mark_index)
+{
+ MD_MARK* opener = &ctx->marks[mark_index];
+ int closer_index;
+ MD_MARK* closer;
+ OFF beg = opener->beg;
+ OFF end = opener->end;
+ int dot_count = 0;
+
+ MD_ASSERT(opener->ch == _T('@'));
+
+ /* Scan for name before '@'. */
+ while(beg > 0 && (ISALNUM(beg-1) || ISANYOF(beg-1, _T(".-_+"))))
+ beg--;
+
+ /* Scan for domain after '@'. */
+ while(end < ctx->size && (ISALNUM(end) || ISANYOF(end, _T(".-_")))) {
+ if(CH(end) == _T('.'))
+ dot_count++;
+ end++;
+ }
+ if(CH(end-1) == _T('.')) { /* Final '.' not part of it. */
+ dot_count--;
+ end--;
+ }
+ else if(ISANYOF2(end-1, _T('-'), _T('_'))) /* These are forbidden at the end. */
+ return;
+ if(CH(end-1) == _T('@') || dot_count == 0)
+ return;
+
+ /* Ok. Lets call it auto-link. Adapt opener and create closer to zero
+ * length so all the contents becomes the link text. */
+ closer_index = mark_index + 1;
+ closer = &ctx->marks[closer_index];
+ if (closer->ch != 'D') return;
+
+ opener->beg = beg;
+ opener->end = beg;
+ closer->ch = opener->ch;
+ closer->beg = end;
+ closer->end = end;
+ md_resolve_range(ctx, NULL, mark_index, closer_index);
+}
+
+static inline void
+md_analyze_marks(MD_CTX* ctx, const MD_LINE* lines, int n_lines,
+ int mark_beg, int mark_end, const CHAR* mark_chars)
+{
+ int i = mark_beg;
+ MD_UNUSED(lines);
+ MD_UNUSED(n_lines);
+
+ while(i < mark_end) {
+ MD_MARK* mark = &ctx->marks[i];
+
+ /* Skip resolved spans. */
+ if(mark->flags & MD_MARK_RESOLVED) {
+ if(mark->flags & MD_MARK_OPENER) {
+ MD_ASSERT(i < mark->next);
+ i = mark->next + 1;
+ } else {
+ i++;
+ }
+ continue;
+ }
+
+ /* Skip marks we do not want to deal with. */
+ if(!ISANYOF_(mark->ch, mark_chars)) {
+ i++;
+ continue;
+ }
+
+ /* Analyze the mark. */
+ switch(mark->ch) {
+ case '[': /* Pass through. */
+ case '!': /* Pass through. */
+ case ']': md_analyze_bracket(ctx, i); break;
+ case '&': md_analyze_entity(ctx, i); break;
+ case '|': md_analyze_table_cell_boundary(ctx, i); break;
+ case '_': /* Pass through. */
+ case '*': md_analyze_emph(ctx, i); break;
+ case '~': md_analyze_tilde(ctx, i); break;
+ case '$': md_analyze_dollar(ctx, i); break;
+ case '.': /* Pass through. */
+ case ':': md_analyze_permissive_url_autolink(ctx, i); break;
+ case '@': md_analyze_permissive_email_autolink(ctx, i); break;
+ }
+
+ i++;
+ }
+}
+
+/* Analyze marks (build ctx->marks). */
+static int
+md_analyze_inlines(MD_CTX* ctx, const MD_LINE* lines, int n_lines, int table_mode)
+{
+ int ret;
+
+ /* Reset the previously collected stack of marks. */
+ ctx->n_marks = 0;
+
+ /* Collect all marks. */
+ MD_CHECK(md_collect_marks(ctx, lines, n_lines, table_mode));
+
+ /* (1) Links. */
+ md_analyze_marks(ctx, lines, n_lines, 0, ctx->n_marks, _T("[]!"));
+ MD_CHECK(md_resolve_links(ctx, lines, n_lines));
+ BRACKET_OPENERS.head = -1;
+ BRACKET_OPENERS.tail = -1;
+ ctx->unresolved_link_head = -1;
+ ctx->unresolved_link_tail = -1;
+
+ if(table_mode) {
+ /* (2) Analyze table cell boundaries.
+ * Note we reset TABLECELLBOUNDARIES chain prior to the call md_analyze_marks(),
+ * not after, because caller may need it. */
+ MD_ASSERT(n_lines == 1);
+ TABLECELLBOUNDARIES.head = -1;
+ TABLECELLBOUNDARIES.tail = -1;
+ ctx->n_table_cell_boundaries = 0;
+ md_analyze_marks(ctx, lines, n_lines, 0, ctx->n_marks, _T("|"));
+ return ret;
+ }
+
+ /* (3) Emphasis and strong emphasis; permissive autolinks. */
+ md_analyze_link_contents(ctx, lines, n_lines, 0, ctx->n_marks);
+
+abort:
+ return ret;
+}
+
+static void
+md_analyze_link_contents(MD_CTX* ctx, const MD_LINE* lines, int n_lines,
+ int mark_beg, int mark_end)
+{
+ int i;
+
+ md_analyze_marks(ctx, lines, n_lines, mark_beg, mark_end, _T("&"));
+ md_analyze_marks(ctx, lines, n_lines, mark_beg, mark_end, _T("*_~$@:."));
+
+ for(i = OPENERS_CHAIN_FIRST; i <= OPENERS_CHAIN_LAST; i++) {
+ ctx->mark_chains[i].head = -1;
+ ctx->mark_chains[i].tail = -1;
+ }
+}
+
+static int
+md_enter_leave_span_a(MD_CTX* ctx, int enter, MD_SPANTYPE type,
+ const CHAR* dest, SZ dest_size, int prohibit_escapes_in_dest,
+ const CHAR* title, SZ title_size)
+{
+ MD_ATTRIBUTE_BUILD href_build = { 0 };
+ MD_ATTRIBUTE_BUILD title_build = { 0 };
+ MD_SPAN_A_DETAIL det;
+ int ret = 0;
+
+ /* Note we here rely on fact that MD_SPAN_A_DETAIL and
+ * MD_SPAN_IMG_DETAIL are binary-compatible. */
+ memset(&det, 0, sizeof(MD_SPAN_A_DETAIL));
+ MD_CHECK(md_build_attribute(ctx, dest, dest_size,
+ (prohibit_escapes_in_dest ? MD_BUILD_ATTR_NO_ESCAPES : 0),
+ &det.href, &href_build));
+ MD_CHECK(md_build_attribute(ctx, title, title_size, 0, &det.title, &title_build));
+
+ if(enter)
+ MD_ENTER_SPAN(type, &det);
+ else
+ MD_LEAVE_SPAN(type, &det);
+
+abort:
+ md_free_attribute(ctx, &href_build);
+ md_free_attribute(ctx, &title_build);
+ return ret;
+}
+
+static int
+md_enter_leave_span_wikilink(MD_CTX* ctx, int enter, const CHAR* target, SZ target_size)
+{
+ MD_ATTRIBUTE_BUILD target_build = { 0 };
+ MD_SPAN_WIKILINK_DETAIL det;
+ int ret = 0;
+
+ memset(&det, 0, sizeof(MD_SPAN_WIKILINK_DETAIL));
+ MD_CHECK(md_build_attribute(ctx, target, target_size, 0, &det.target, &target_build));
+
+ if (enter)
+ MD_ENTER_SPAN(MD_SPAN_WIKILINK, &det);
+ else
+ MD_LEAVE_SPAN(MD_SPAN_WIKILINK, &det);
+
+abort:
+ md_free_attribute(ctx, &target_build);
+ return ret;
+}
+
+
+/* Render the output, accordingly to the analyzed ctx->marks. */
+static int
+md_process_inlines(MD_CTX* ctx, const MD_LINE* lines, int n_lines)
+{
+ MD_TEXTTYPE text_type;
+ const MD_LINE* line = lines;
+ MD_MARK* prev_mark = NULL;
+ MD_MARK* mark;
+ OFF off = lines[0].beg;
+ OFF end = lines[n_lines-1].end;
+ int enforce_hardbreak = 0;
+ int ret = 0;
+
+ /* Find first resolved mark. Note there is always at least one resolved
+ * mark, the dummy last one after the end of the latest line we actually
+ * never really reach. This saves us of a lot of special checks and cases
+ * in this function. */
+ mark = ctx->marks;
+ while(!(mark->flags & MD_MARK_RESOLVED))
+ mark++;
+
+ text_type = MD_TEXT_NORMAL;
+
+ while(1) {
+ /* Process the text up to the next mark or end-of-line. */
+ OFF tmp = (line->end < mark->beg ? line->end : mark->beg);
+ if(tmp > off) {
+ MD_TEXT(text_type, STR(off), tmp - off);
+ off = tmp;
+ }
+
+ /* If reached the mark, process it and move to next one. */
+ if(off >= mark->beg) {
+ switch(mark->ch) {
+ case '\\': /* Backslash escape. */
+ if(ISNEWLINE(mark->beg+1))
+ enforce_hardbreak = 1;
+ else
+ MD_TEXT(text_type, STR(mark->beg+1), 1);
+ break;
+
+ case ' ': /* Non-trivial space. */
+ MD_TEXT(text_type, _T(" "), 1);
+ break;
+
+ case '`': /* Code span. */
+ if(mark->flags & MD_MARK_OPENER) {
+ MD_ENTER_SPAN(MD_SPAN_CODE, NULL);
+ text_type = MD_TEXT_CODE;
+ } else {
+ MD_LEAVE_SPAN(MD_SPAN_CODE, NULL);
+ text_type = MD_TEXT_NORMAL;
+ }
+ break;
+
+ case '_': /* Underline (or emphasis if we fall through). */
+ if(ctx->parser.flags & MD_FLAG_UNDERLINE) {
+ if(mark->flags & MD_MARK_OPENER) {
+ while(off < mark->end) {
+ MD_ENTER_SPAN(MD_SPAN_U, NULL);
+ off++;
+ }
+ } else {
+ while(off < mark->end) {
+ MD_LEAVE_SPAN(MD_SPAN_U, NULL);
+ off++;
+ }
+ }
+ break;
+ }
+ MD_FALLTHROUGH();
+
+ case '*': /* Emphasis, strong emphasis. */
+ if(mark->flags & MD_MARK_OPENER) {
+ if((mark->end - off) % 2) {
+ MD_ENTER_SPAN(MD_SPAN_EM, NULL);
+ off++;
+ }
+ while(off + 1 < mark->end) {
+ MD_ENTER_SPAN(MD_SPAN_STRONG, NULL);
+ off += 2;
+ }
+ } else {
+ while(off + 1 < mark->end) {
+ MD_LEAVE_SPAN(MD_SPAN_STRONG, NULL);
+ off += 2;
+ }
+ if((mark->end - off) % 2) {
+ MD_LEAVE_SPAN(MD_SPAN_EM, NULL);
+ off++;
+ }
+ }
+ break;
+
+ case '~':
+ if(mark->flags & MD_MARK_OPENER)
+ MD_ENTER_SPAN(MD_SPAN_DEL, NULL);
+ else
+ MD_LEAVE_SPAN(MD_SPAN_DEL, NULL);
+ break;
+
+ case '$':
+ if(mark->flags & MD_MARK_OPENER) {
+ MD_ENTER_SPAN((mark->end - off) % 2 ? MD_SPAN_LATEXMATH : MD_SPAN_LATEXMATH_DISPLAY, NULL);
+ text_type = MD_TEXT_LATEXMATH;
+ } else {
+ MD_LEAVE_SPAN((mark->end - off) % 2 ? MD_SPAN_LATEXMATH : MD_SPAN_LATEXMATH_DISPLAY, NULL);
+ text_type = MD_TEXT_NORMAL;
+ }
+ break;
+
+ case '[': /* Link, wiki link, image. */
+ case '!':
+ case ']':
+ {
+ const MD_MARK* opener = (mark->ch != ']' ? mark : &ctx->marks[mark->prev]);
+ const MD_MARK* closer = &ctx->marks[opener->next];
+ const MD_MARK* dest_mark;
+ const MD_MARK* title_mark;
+
+ if ((opener->ch == '[' && closer->ch == ']') &&
+ opener->end - opener->beg >= 2 &&
+ closer->end - closer->beg >= 2)
+ {
+ int has_label = (opener->end - opener->beg > 2);
+ SZ target_sz;
+
+ if(has_label)
+ target_sz = opener->end - (opener->beg+2);
+ else
+ target_sz = closer->beg - opener->end;
+
+ MD_CHECK(md_enter_leave_span_wikilink(ctx, (mark->ch != ']'),
+ has_label ? STR(opener->beg+2) : STR(opener->end),
+ target_sz));
+
+ break;
+ }
+
+ dest_mark = opener+1;
+ MD_ASSERT(dest_mark->ch == 'D');
+ title_mark = opener+2;
+ if (title_mark->ch != 'D') break;
+
+ MD_CHECK(md_enter_leave_span_a(ctx, (mark->ch != ']'),
+ (opener->ch == '!' ? MD_SPAN_IMG : MD_SPAN_A),
+ STR(dest_mark->beg), dest_mark->end - dest_mark->beg, FALSE,
+ md_mark_get_ptr(ctx, (int)(title_mark - ctx->marks)),
+ title_mark->prev));
+
+ /* link/image closer may span multiple lines. */
+ if(mark->ch == ']') {
+ while(mark->end > line->end)
+ line++;
+ }
+
+ break;
+ }
+
+ case '<':
+ case '>': /* Autolink or raw HTML. */
+ if(!(mark->flags & MD_MARK_AUTOLINK)) {
+ /* Raw HTML. */
+ if(mark->flags & MD_MARK_OPENER)
+ text_type = MD_TEXT_HTML;
+ else
+ text_type = MD_TEXT_NORMAL;
+ break;
+ }
+ /* Pass through, if auto-link. */
+ MD_FALLTHROUGH();
+
+ case '@': /* Permissive e-mail autolink. */
+ case ':': /* Permissive URL autolink. */
+ case '.': /* Permissive WWW autolink. */
+ {
+ MD_MARK* opener = ((mark->flags & MD_MARK_OPENER) ? mark : &ctx->marks[mark->prev]);
+ MD_MARK* closer = &ctx->marks[opener->next];
+ const CHAR* dest = STR(opener->end);
+ SZ dest_size = closer->beg - opener->end;
+
+ /* For permissive auto-links we do not know closer mark
+ * position at the time of md_collect_marks(), therefore
+ * it can be out-of-order in ctx->marks[].
+ *
+ * With this flag, we make sure that we output the closer
+ * only if we processed the opener. */
+ if(mark->flags & MD_MARK_OPENER)
+ closer->flags |= MD_MARK_VALIDPERMISSIVEAUTOLINK;
+
+ if(opener->ch == '@' || opener->ch == '.') {
+ dest_size += 7;
+ MD_TEMP_BUFFER(dest_size * sizeof(CHAR));
+ memcpy(ctx->buffer,
+ (opener->ch == '@' ? _T("mailto:") : _T("http://")),
+ 7 * sizeof(CHAR));
+ memcpy(ctx->buffer + 7, dest, (dest_size-7) * sizeof(CHAR));
+ dest = ctx->buffer;
+ }
+
+ if(closer->flags & MD_MARK_VALIDPERMISSIVEAUTOLINK)
+ MD_CHECK(md_enter_leave_span_a(ctx, (mark->flags & MD_MARK_OPENER),
+ MD_SPAN_A, dest, dest_size, TRUE, NULL, 0));
+ break;
+ }
+
+ case '&': /* Entity. */
+ MD_TEXT(MD_TEXT_ENTITY, STR(mark->beg), mark->end - mark->beg);
+ break;
+
+ case '\0':
+ MD_TEXT(MD_TEXT_NULLCHAR, _T(""), 1);
+ break;
+
+ case 127:
+ goto abort;
+ }
+
+ off = mark->end;
+
+ /* Move to next resolved mark. */
+ prev_mark = mark;
+ mark++;
+ while(!(mark->flags & MD_MARK_RESOLVED) || mark->beg < off)
+ mark++;
+ }
+
+ /* If reached end of line, move to next one. */
+ if(off >= line->end) {
+ /* If it is the last line, we are done. */
+ if(off >= end)
+ break;
+
+ if(text_type == MD_TEXT_CODE || text_type == MD_TEXT_LATEXMATH) {
+ OFF tmp;
+
+ MD_ASSERT(prev_mark != NULL);
+ MD_ASSERT(ISANYOF2_(prev_mark->ch, '`', '$') && (prev_mark->flags & MD_MARK_OPENER));
+ MD_ASSERT(ISANYOF2_(mark->ch, '`', '$') && (mark->flags & MD_MARK_CLOSER));
+
+ /* Inside a code span, trailing line whitespace has to be
+ * outputted. */
+ tmp = off;
+ while(off < ctx->size && ISBLANK(off))
+ off++;
+ if(off > tmp)
+ MD_TEXT(text_type, STR(tmp), off-tmp);
+
+ /* and new lines are transformed into single spaces. */
+ if(prev_mark->end < off && off < mark->beg)
+ MD_TEXT(text_type, _T(" "), 1);
+ } else if(text_type == MD_TEXT_HTML) {
+ /* Inside raw HTML, we output the new line verbatim, including
+ * any trailing spaces. */
+ OFF tmp = off;
+
+ while(tmp < end && ISBLANK(tmp))
+ tmp++;
+ if(tmp > off)
+ MD_TEXT(MD_TEXT_HTML, STR(off), tmp - off);
+ MD_TEXT(MD_TEXT_HTML, _T("\n"), 1);
+ } else {
+ /* Output soft or hard line break. */
+ MD_TEXTTYPE break_type = MD_TEXT_SOFTBR;
+
+ if(text_type == MD_TEXT_NORMAL) {
+ if(enforce_hardbreak)
+ break_type = MD_TEXT_BR;
+ else if((CH(line->end) == _T(' ') && CH(line->end+1) == _T(' ')))
+ break_type = MD_TEXT_BR;
+ }
+
+ MD_TEXT(break_type, _T("\n"), 1);
+ }
+
+ /* Move to the next line. */
+ line++;
+ off = line->beg;
+
+ enforce_hardbreak = 0;
+ }
+ }
+
+abort:
+ return ret;
+}
+
+
+/***************************
+ *** Processing Tables ***
+ ***************************/
+
+static void
+md_analyze_table_alignment(MD_CTX* ctx, OFF beg, OFF end, MD_ALIGN* align, int n_align)
+{
+ static const MD_ALIGN align_map[] = { MD_ALIGN_DEFAULT, MD_ALIGN_LEFT, MD_ALIGN_RIGHT, MD_ALIGN_CENTER };
+ OFF off = beg;
+
+ while(n_align > 0) {
+ int index = 0; /* index into align_map[] */
+
+ while(CH(off) != _T('-'))
+ off++;
+ if(off > beg && CH(off-1) == _T(':'))
+ index |= 1;
+ while(off < end && CH(off) == _T('-'))
+ off++;
+ if(off < end && CH(off) == _T(':'))
+ index |= 2;
+
+ *align = align_map[index];
+ align++;
+ n_align--;
+ }
+
+}
+
+/* Forward declaration. */
+static int md_process_normal_block_contents(MD_CTX* ctx, const MD_LINE* lines, int n_lines);
+
+static int
+md_process_table_cell(MD_CTX* ctx, MD_BLOCKTYPE cell_type, MD_ALIGN align, OFF beg, OFF end)
+{
+ MD_LINE line;
+ MD_BLOCK_TD_DETAIL det;
+ int ret = 0;
+
+ while(beg < end && ISWHITESPACE(beg))
+ beg++;
+ while(end > beg && ISWHITESPACE(end-1))
+ end--;
+
+ det.align = align;
+ line.beg = beg;
+ line.end = end;
+
+ MD_ENTER_BLOCK(cell_type, &det);
+ MD_CHECK(md_process_normal_block_contents(ctx, &line, 1));
+ MD_LEAVE_BLOCK(cell_type, &det);
+
+abort:
+ return ret;
+}
+
+static int
+md_process_table_row(MD_CTX* ctx, MD_BLOCKTYPE cell_type, OFF beg, OFF end,
+ const MD_ALIGN* align, int col_count)
+{
+ MD_LINE line;
+ OFF* pipe_offs = NULL;
+ int i, j, k, n;
+ int ret = 0;
+
+ line.beg = beg;
+ line.end = end;
+
+ /* Break the line into table cells by identifying pipe characters who
+ * form the cell boundary. */
+ MD_CHECK(md_analyze_inlines(ctx, &line, 1, TRUE));
+
+ /* We have to remember the cell boundaries in local buffer because
+ * ctx->marks[] shall be reused during cell contents processing. */
+ n = ctx->n_table_cell_boundaries + 2;
+ pipe_offs = (OFF*) malloc(n * sizeof(OFF));
+ if(pipe_offs == NULL) {
+ MD_LOG("malloc() failed.");
+ ret = -1;
+ goto abort;
+ }
+ j = 0;
+ pipe_offs[j++] = beg;
+ for(i = TABLECELLBOUNDARIES.head; i >= 0; i = ctx->marks[i].next) {
+ MD_MARK* mark = &ctx->marks[i];
+ pipe_offs[j++] = mark->end;
+ }
+ pipe_offs[j++] = end+1;
+
+ /* Process cells. */
+ MD_ENTER_BLOCK(MD_BLOCK_TR, NULL);
+ k = 0;
+ for(i = 0; i < j-1 && k < col_count; i++) {
+ if(pipe_offs[i] < pipe_offs[i+1]-1)
+ MD_CHECK(md_process_table_cell(ctx, cell_type, align[k++], pipe_offs[i], pipe_offs[i+1]-1));
+ }
+ /* Make sure we call enough table cells even if the current table contains
+ * too few of them. */
+ while(k < col_count)
+ MD_CHECK(md_process_table_cell(ctx, cell_type, align[k++], 0, 0));
+ MD_LEAVE_BLOCK(MD_BLOCK_TR, NULL);
+
+abort:
+ free(pipe_offs);
+
+ /* Free any temporary memory blocks stored within some dummy marks. */
+ for(i = PTR_CHAIN.head; i >= 0; i = ctx->marks[i].next)
+ free(md_mark_get_ptr(ctx, i));
+ PTR_CHAIN.head = -1;
+ PTR_CHAIN.tail = -1;
+
+ return ret;
+}
+
+static int
+md_process_table_block_contents(MD_CTX* ctx, int col_count, const MD_LINE* lines, int n_lines)
+{
+ MD_ALIGN* align;
+ int i;
+ int ret = 0;
+
+ /* At least two lines have to be present: The column headers and the line
+ * with the underlines. */
+ MD_ASSERT(n_lines >= 2);
+
+ align = malloc(col_count * sizeof(MD_ALIGN));
+ if(align == NULL) {
+ MD_LOG("malloc() failed.");
+ ret = -1;
+ goto abort;
+ }
+
+ md_analyze_table_alignment(ctx, lines[1].beg, lines[1].end, align, col_count);
+
+ MD_ENTER_BLOCK(MD_BLOCK_THEAD, NULL);
+ MD_CHECK(md_process_table_row(ctx, MD_BLOCK_TH,
+ lines[0].beg, lines[0].end, align, col_count));
+ MD_LEAVE_BLOCK(MD_BLOCK_THEAD, NULL);
+
+ if(n_lines > 2) {
+ MD_ENTER_BLOCK(MD_BLOCK_TBODY, NULL);
+ for(i = 2; i < n_lines; i++) {
+ MD_CHECK(md_process_table_row(ctx, MD_BLOCK_TD,
+ lines[i].beg, lines[i].end, align, col_count));
+ }
+ MD_LEAVE_BLOCK(MD_BLOCK_TBODY, NULL);
+ }
+
+abort:
+ free(align);
+ return ret;
+}
+
+
+/**************************
+ *** Processing Block ***
+ **************************/
+
+#define MD_BLOCK_CONTAINER_OPENER 0x01
+#define MD_BLOCK_CONTAINER_CLOSER 0x02
+#define MD_BLOCK_CONTAINER (MD_BLOCK_CONTAINER_OPENER | MD_BLOCK_CONTAINER_CLOSER)
+#define MD_BLOCK_LOOSE_LIST 0x04
+#define MD_BLOCK_SETEXT_HEADER 0x08
+
+struct MD_BLOCK_tag {
+ MD_BLOCKTYPE type : 8;
+ unsigned flags : 8;
+
+ /* MD_BLOCK_H: Header level (1 - 6)
+ * MD_BLOCK_CODE: Non-zero if fenced, zero if indented.
+ * MD_BLOCK_LI: Task mark character (0 if not task list item, 'x', 'X' or ' ').
+ * MD_BLOCK_TABLE: Column count (as determined by the table underline).
+ */
+ unsigned data : 16;
+
+ /* Leaf blocks: Count of lines (MD_LINE or MD_VERBATIMLINE) on the block.
+ * MD_BLOCK_LI: Task mark offset in the input doc.
+ * MD_BLOCK_OL: Start item number.
+ */
+ unsigned n_lines;
+};
+
+struct MD_CONTAINER_tag {
+ CHAR ch;
+ unsigned is_loose : 8;
+ unsigned is_task : 8;
+ unsigned start;
+ unsigned mark_indent;
+ unsigned contents_indent;
+ OFF block_byte_off;
+ OFF task_mark_off;
+};
+
+
+static int
+md_process_normal_block_contents(MD_CTX* ctx, const MD_LINE* lines, int n_lines)
+{
+ int i;
+ int ret;
+
+ MD_CHECK(md_analyze_inlines(ctx, lines, n_lines, FALSE));
+ MD_CHECK(md_process_inlines(ctx, lines, n_lines));
+
+abort:
+ /* Free any temporary memory blocks stored within some dummy marks. */
+ for(i = PTR_CHAIN.head; i >= 0; i = ctx->marks[i].next)
+ free(md_mark_get_ptr(ctx, i));
+ PTR_CHAIN.head = -1;
+ PTR_CHAIN.tail = -1;
+
+ return ret;
+}
+
+static int
+md_process_verbatim_block_contents(MD_CTX* ctx, MD_TEXTTYPE text_type, const MD_VERBATIMLINE* lines, int n_lines)
+{
+ static const CHAR indent_chunk_str[] = _T(" ");
+ static const SZ indent_chunk_size = SIZEOF_ARRAY(indent_chunk_str) - 1;
+
+ int i;
+ int ret = 0;
+
+ for(i = 0; i < n_lines; i++) {
+ const MD_VERBATIMLINE* line = &lines[i];
+ int indent = line->indent;
+
+ MD_ASSERT(indent >= 0);
+
+ /* Output code indentation. */
+ while(indent > (int) indent_chunk_size) {
+ MD_TEXT(text_type, indent_chunk_str, indent_chunk_size);
+ indent -= indent_chunk_size;
+ }
+ if(indent > 0)
+ MD_TEXT(text_type, indent_chunk_str, indent);
+
+ /* Output the code line itself. */
+ MD_TEXT_INSECURE(text_type, STR(line->beg), line->end - line->beg);
+
+ /* Enforce end-of-line. */
+ MD_TEXT(text_type, _T("\n"), 1);
+ }
+
+abort:
+ return ret;
+}
+
+static int
+md_process_code_block_contents(MD_CTX* ctx, int is_fenced, const MD_VERBATIMLINE* lines, int n_lines)
+{
+ if(is_fenced) {
+ /* Skip the first line in case of fenced code: It is the fence.
+ * (Only the starting fence is present due to logic in md_analyze_line().) */
+ lines++;
+ n_lines--;
+ } else {
+ /* Ignore blank lines at start/end of indented code block. */
+ while(n_lines > 0 && lines[0].beg == lines[0].end) {
+ lines++;
+ n_lines--;
+ }
+ while(n_lines > 0 && lines[n_lines-1].beg == lines[n_lines-1].end) {
+ n_lines--;
+ }
+ }
+
+ if(n_lines == 0)
+ return 0;
+
+ return md_process_verbatim_block_contents(ctx, MD_TEXT_CODE, lines, n_lines);
+}
+
+static int
+md_setup_fenced_code_detail(MD_CTX* ctx, const MD_BLOCK* block, MD_BLOCK_CODE_DETAIL* det,
+ MD_ATTRIBUTE_BUILD* info_build, MD_ATTRIBUTE_BUILD* lang_build)
+{
+ const MD_VERBATIMLINE* fence_line = (const MD_VERBATIMLINE*)(block + 1);
+ OFF beg = fence_line->beg;
+ OFF end = fence_line->end;
+ OFF lang_end;
+ CHAR fence_ch = CH(fence_line->beg);
+ int ret = 0;
+
+ /* Skip the fence itself. */
+ while(beg < ctx->size && CH(beg) == fence_ch)
+ beg++;
+ /* Trim initial spaces. */
+ while(beg < ctx->size && CH(beg) == _T(' '))
+ beg++;
+
+ /* Trim trailing spaces. */
+ while(end > beg && CH(end-1) == _T(' '))
+ end--;
+
+ /* Build info string attribute. */
+ MD_CHECK(md_build_attribute(ctx, STR(beg), end - beg, 0, &det->info, info_build));
+
+ /* Build info string attribute. */
+ lang_end = beg;
+ while(lang_end < end && !ISWHITESPACE(lang_end))
+ lang_end++;
+ MD_CHECK(md_build_attribute(ctx, STR(beg), lang_end - beg, 0, &det->lang, lang_build));
+
+ det->fence_char = fence_ch;
+
+abort:
+ return ret;
+}
+
+static int
+md_process_leaf_block(MD_CTX* ctx, const MD_BLOCK* block)
+{
+ union {
+ MD_BLOCK_H_DETAIL header;
+ MD_BLOCK_CODE_DETAIL code;
+ MD_BLOCK_TABLE_DETAIL table;
+ } det;
+ MD_ATTRIBUTE_BUILD info_build;
+ MD_ATTRIBUTE_BUILD lang_build;
+ int is_in_tight_list;
+ int clean_fence_code_detail = FALSE;
+ int ret = 0;
+
+ memset(&det, 0, sizeof(det));
+
+ if(ctx->n_containers == 0)
+ is_in_tight_list = FALSE;
+ else
+ is_in_tight_list = !ctx->containers[ctx->n_containers-1].is_loose;
+
+ switch(block->type) {
+ case MD_BLOCK_H:
+ det.header.level = block->data;
+ break;
+
+ case MD_BLOCK_CODE:
+ /* For fenced code block, we may need to set the info string. */
+ if(block->data != 0) {
+ memset(&det.code, 0, sizeof(MD_BLOCK_CODE_DETAIL));
+ clean_fence_code_detail = TRUE;
+ MD_CHECK(md_setup_fenced_code_detail(ctx, block, &det.code, &info_build, &lang_build));
+ }
+ break;
+
+ case MD_BLOCK_TABLE:
+ det.table.col_count = block->data;
+ det.table.head_row_count = 1;
+ det.table.body_row_count = block->n_lines - 2;
+ break;
+
+ default:
+ /* Noop. */
+ break;
+ }
+
+ if(!is_in_tight_list || block->type != MD_BLOCK_P)
+ MD_ENTER_BLOCK(block->type, (void*) &det);
+
+ /* Process the block contents accordingly to is type. */
+ switch(block->type) {
+ case MD_BLOCK_HR:
+ /* noop */
+ break;
+
+ case MD_BLOCK_CODE:
+ MD_CHECK(md_process_code_block_contents(ctx, (block->data != 0),
+ (const MD_VERBATIMLINE*)(block + 1), block->n_lines));
+ break;
+
+ case MD_BLOCK_HTML:
+ MD_CHECK(md_process_verbatim_block_contents(ctx, MD_TEXT_HTML,
+ (const MD_VERBATIMLINE*)(block + 1), block->n_lines));
+ break;
+
+ case MD_BLOCK_TABLE:
+ MD_CHECK(md_process_table_block_contents(ctx, block->data,
+ (const MD_LINE*)(block + 1), block->n_lines));
+ break;
+
+ default:
+ MD_CHECK(md_process_normal_block_contents(ctx,
+ (const MD_LINE*)(block + 1), block->n_lines));
+ break;
+ }
+
+ if(!is_in_tight_list || block->type != MD_BLOCK_P)
+ MD_LEAVE_BLOCK(block->type, (void*) &det);
+
+abort:
+ if(clean_fence_code_detail) {
+ md_free_attribute(ctx, &info_build);
+ md_free_attribute(ctx, &lang_build);
+ }
+ return ret;
+}
+
+static int
+md_process_all_blocks(MD_CTX* ctx)
+{
+ int byte_off = 0;
+ int ret = 0;
+
+ /* ctx->containers now is not needed for detection of lists and list items
+ * so we reuse it for tracking what lists are loose or tight. We rely
+ * on the fact the vector is large enough to hold the deepest nesting
+ * level of lists. */
+ ctx->n_containers = 0;
+
+ while(byte_off < ctx->n_block_bytes) {
+ MD_BLOCK* block = (MD_BLOCK*)((char*)ctx->block_bytes + byte_off);
+ union {
+ MD_BLOCK_UL_DETAIL ul;
+ MD_BLOCK_OL_DETAIL ol;
+ MD_BLOCK_LI_DETAIL li;
+ } det;
+
+ switch(block->type) {
+ case MD_BLOCK_UL:
+ det.ul.is_tight = (block->flags & MD_BLOCK_LOOSE_LIST) ? FALSE : TRUE;
+ det.ul.mark = (CHAR) block->data;
+ break;
+
+ case MD_BLOCK_OL:
+ det.ol.start = block->n_lines;
+ det.ol.is_tight = (block->flags & MD_BLOCK_LOOSE_LIST) ? FALSE : TRUE;
+ det.ol.mark_delimiter = (CHAR) block->data;
+ break;
+
+ case MD_BLOCK_LI:
+ det.li.is_task = (block->data != 0);
+ det.li.task_mark = (CHAR) block->data;
+ det.li.task_mark_offset = (OFF) block->n_lines;
+ break;
+
+ default:
+ /* noop */
+ break;
+ }
+
+ if(block->flags & MD_BLOCK_CONTAINER) {
+ if(block->flags & MD_BLOCK_CONTAINER_CLOSER) {
+ MD_LEAVE_BLOCK(block->type, &det);
+
+ if(block->type == MD_BLOCK_UL || block->type == MD_BLOCK_OL || block->type == MD_BLOCK_QUOTE)
+ ctx->n_containers--;
+ }
+
+ if(block->flags & MD_BLOCK_CONTAINER_OPENER) {
+ MD_ENTER_BLOCK(block->type, &det);
+
+ if(block->type == MD_BLOCK_UL || block->type == MD_BLOCK_OL) {
+ ctx->containers[ctx->n_containers].is_loose = (block->flags & MD_BLOCK_LOOSE_LIST);
+ ctx->n_containers++;
+ } else if(block->type == MD_BLOCK_QUOTE) {
+ /* This causes that any text in a block quote, even if
+ * nested inside a tight list item, is wrapped with
+ * <p>...</p>. */
+ ctx->containers[ctx->n_containers].is_loose = TRUE;
+ ctx->n_containers++;
+ }
+ }
+ } else {
+ MD_CHECK(md_process_leaf_block(ctx, block));
+
+ if(block->type == MD_BLOCK_CODE || block->type == MD_BLOCK_HTML)
+ byte_off += block->n_lines * sizeof(MD_VERBATIMLINE);
+ else
+ byte_off += block->n_lines * sizeof(MD_LINE);
+ }
+
+ byte_off += sizeof(MD_BLOCK);
+ }
+
+ ctx->n_block_bytes = 0;
+
+abort:
+ return ret;
+}
+
+
+/************************************
+ *** Grouping Lines into Blocks ***
+ ************************************/
+
+static void*
+md_push_block_bytes(MD_CTX* ctx, int n_bytes)
+{
+ void* ptr;
+
+ if(ctx->n_block_bytes + n_bytes > ctx->alloc_block_bytes) {
+ void* new_block_bytes;
+
+ ctx->alloc_block_bytes = (ctx->alloc_block_bytes > 0
+ ? ctx->alloc_block_bytes + ctx->alloc_block_bytes / 2
+ : 512);
+ new_block_bytes = realloc(ctx->block_bytes, ctx->alloc_block_bytes);
+ if(new_block_bytes == NULL) {
+ MD_LOG("realloc() failed.");
+ return NULL;
+ }
+
+ /* Fix the ->current_block after the reallocation. */
+ if(ctx->current_block != NULL) {
+ OFF off_current_block = (OFF) ((char*) ctx->current_block - (char*) ctx->block_bytes);
+ ctx->current_block = (MD_BLOCK*) ((char*) new_block_bytes + off_current_block);
+ }
+
+ ctx->block_bytes = new_block_bytes;
+ }
+
+ ptr = (char*)ctx->block_bytes + ctx->n_block_bytes;
+ ctx->n_block_bytes += n_bytes;
+ return ptr;
+}
+
+static int
+md_start_new_block(MD_CTX* ctx, const MD_LINE_ANALYSIS* line)
+{
+ MD_BLOCK* block;
+
+ MD_ASSERT(ctx->current_block == NULL);
+
+ block = (MD_BLOCK*) md_push_block_bytes(ctx, sizeof(MD_BLOCK));
+ if(block == NULL)
+ return -1;
+
+ switch(line->type) {
+ case MD_LINE_HR:
+ block->type = MD_BLOCK_HR;
+ break;
+
+ case MD_LINE_ATXHEADER:
+ case MD_LINE_SETEXTHEADER:
+ block->type = MD_BLOCK_H;
+ break;
+
+ case MD_LINE_FENCEDCODE:
+ case MD_LINE_INDENTEDCODE:
+ block->type = MD_BLOCK_CODE;
+ break;
+
+ case MD_LINE_TEXT:
+ block->type = MD_BLOCK_P;
+ break;
+
+ case MD_LINE_HTML:
+ block->type = MD_BLOCK_HTML;
+ break;
+
+ case MD_LINE_BLANK:
+ case MD_LINE_SETEXTUNDERLINE:
+ case MD_LINE_TABLEUNDERLINE:
+ default:
+ MD_UNREACHABLE();
+ break;
+ }
+
+ block->flags = 0;
+ block->data = line->data;
+ block->n_lines = 0;
+
+ ctx->current_block = block;
+ return 0;
+}
+
+/* Eat from start of current (textual) block any reference definitions and
+ * remember them so we can resolve any links referring to them.
+ *
+ * (Reference definitions can only be at start of it as they cannot break
+ * a paragraph.)
+ */
+static int
+md_consume_link_reference_definitions(MD_CTX* ctx)
+{
+ MD_LINE* lines = (MD_LINE*) (ctx->current_block + 1);
+ int n_lines = ctx->current_block->n_lines;
+ int n = 0;
+
+ /* Compute how many lines at the start of the block form one or more
+ * reference definitions. */
+ while(n < n_lines) {
+ int n_link_ref_lines;
+
+ n_link_ref_lines = md_is_link_reference_definition(ctx,
+ lines + n, n_lines - n);
+ /* Not a reference definition? */
+ if(n_link_ref_lines == 0)
+ break;
+
+ /* We fail if it is the ref. def. but it could not be stored due
+ * a memory allocation error. */
+ if(n_link_ref_lines < 0)
+ return -1;
+
+ n += n_link_ref_lines;
+ }
+
+ /* If there was at least one reference definition, we need to remove
+ * its lines from the block, or perhaps even the whole block. */
+ if(n > 0) {
+ if(n == n_lines) {
+ /* Remove complete block. */
+ ctx->n_block_bytes -= n * sizeof(MD_LINE);
+ ctx->n_block_bytes -= sizeof(MD_BLOCK);
+ ctx->current_block = NULL;
+ } else {
+ /* Remove just some initial lines from the block. */
+ memmove(lines, lines + n, (n_lines - n) * sizeof(MD_LINE));
+ ctx->current_block->n_lines -= n;
+ ctx->n_block_bytes -= n * sizeof(MD_LINE);
+ }
+ }
+
+ return 0;
+}
+
+static int
+md_end_current_block(MD_CTX* ctx)
+{
+ int ret = 0;
+
+ if(ctx->current_block == NULL)
+ return ret;
+
+ /* Check whether there is a reference definition. (We do this here instead
+ * of in md_analyze_line() because reference definition can take multiple
+ * lines.) */
+ if(ctx->current_block->type == MD_BLOCK_P ||
+ (ctx->current_block->type == MD_BLOCK_H && (ctx->current_block->flags & MD_BLOCK_SETEXT_HEADER)))
+ {
+ MD_LINE* lines = (MD_LINE*) (ctx->current_block + 1);
+ if(CH(lines[0].beg) == _T('[')) {
+ MD_CHECK(md_consume_link_reference_definitions(ctx));
+ if(ctx->current_block == NULL)
+ return ret;
+ }
+ }
+
+ if(ctx->current_block->type == MD_BLOCK_H && (ctx->current_block->flags & MD_BLOCK_SETEXT_HEADER)) {
+ int n_lines = ctx->current_block->n_lines;
+
+ if(n_lines > 1) {
+ /* Get rid of the underline. */
+ ctx->current_block->n_lines--;
+ ctx->n_block_bytes -= sizeof(MD_LINE);
+ } else {
+ /* Only the underline has left after eating the ref. defs.
+ * Keep the line as beginning of a new ordinary paragraph. */
+ ctx->current_block->type = MD_BLOCK_P;
+ return 0;
+ }
+ }
+
+ /* Mark we are not building any block anymore. */
+ ctx->current_block = NULL;
+
+abort:
+ return ret;
+}
+
+static int
+md_add_line_into_current_block(MD_CTX* ctx, const MD_LINE_ANALYSIS* analysis)
+{
+ MD_ASSERT(ctx->current_block != NULL);
+
+ if(ctx->current_block->type == MD_BLOCK_CODE || ctx->current_block->type == MD_BLOCK_HTML) {
+ MD_VERBATIMLINE* line;
+
+ line = (MD_VERBATIMLINE*) md_push_block_bytes(ctx, sizeof(MD_VERBATIMLINE));
+ if(line == NULL)
+ return -1;
+
+ line->indent = analysis->indent;
+ line->beg = analysis->beg;
+ line->end = analysis->end;
+ } else {
+ MD_LINE* line;
+
+ line = (MD_LINE*) md_push_block_bytes(ctx, sizeof(MD_LINE));
+ if(line == NULL)
+ return -1;
+
+ line->beg = analysis->beg;
+ line->end = analysis->end;
+ }
+ ctx->current_block->n_lines++;
+
+ return 0;
+}
+
+static int
+md_push_container_bytes(MD_CTX* ctx, MD_BLOCKTYPE type, unsigned start,
+ unsigned data, unsigned flags)
+{
+ MD_BLOCK* block;
+ int ret = 0;
+
+ MD_CHECK(md_end_current_block(ctx));
+
+ block = (MD_BLOCK*) md_push_block_bytes(ctx, sizeof(MD_BLOCK));
+ if(block == NULL)
+ return -1;
+
+ block->type = type;
+ block->flags = flags;
+ block->data = data;
+ block->n_lines = start;
+
+abort:
+ return ret;
+}
+
+
+
+/***********************
+ *** Line Analysis ***
+ ***********************/
+
+static int
+md_is_hr_line(MD_CTX* ctx, OFF beg, OFF* p_end, OFF* p_killer)
+{
+ OFF off = beg + 1;
+ int n = 1;
+
+ while(off < ctx->size && (CH(off) == CH(beg) || CH(off) == _T(' ') || CH(off) == _T('\t'))) {
+ if(CH(off) == CH(beg))
+ n++;
+ off++;
+ }
+
+ if(n < 3) {
+ *p_killer = off;
+ return FALSE;
+ }
+
+ /* Nothing else can be present on the line. */
+ if(off < ctx->size && !ISNEWLINE(off)) {
+ *p_killer = off;
+ return FALSE;
+ }
+
+ *p_end = off;
+ return TRUE;
+}
+
+static int
+md_is_atxheader_line(MD_CTX* ctx, OFF beg, OFF* p_beg, OFF* p_end, unsigned* p_level)
+{
+ int n;
+ OFF off = beg + 1;
+
+ while(off < ctx->size && CH(off) == _T('#') && off - beg < 7)
+ off++;
+ n = off - beg;
+
+ if(n > 6)
+ return FALSE;
+ *p_level = n;
+
+ if(!(ctx->parser.flags & MD_FLAG_PERMISSIVEATXHEADERS) && off < ctx->size &&
+ CH(off) != _T(' ') && CH(off) != _T('\t') && !ISNEWLINE(off))
+ return FALSE;
+
+ while(off < ctx->size && CH(off) == _T(' '))
+ off++;
+ *p_beg = off;
+ *p_end = off;
+ return TRUE;
+}
+
+static int
+md_is_setext_underline(MD_CTX* ctx, OFF beg, OFF* p_end, unsigned* p_level)
+{
+ OFF off = beg + 1;
+
+ while(off < ctx->size && CH(off) == CH(beg))
+ off++;
+
+ /* Optionally, space(s) can follow. */
+ while(off < ctx->size && CH(off) == _T(' '))
+ off++;
+
+ /* But nothing more is allowed on the line. */
+ if(off < ctx->size && !ISNEWLINE(off))
+ return FALSE;
+
+ *p_level = (CH(beg) == _T('=') ? 1 : 2);
+ *p_end = off;
+ return TRUE;
+}
+
+static int
+md_is_table_underline(MD_CTX* ctx, OFF beg, OFF* p_end, unsigned* p_col_count)
+{
+ OFF off = beg;
+ int found_pipe = FALSE;
+ unsigned col_count = 0;
+
+ if(off < ctx->size && CH(off) == _T('|')) {
+ found_pipe = TRUE;
+ off++;
+ while(off < ctx->size && ISWHITESPACE(off))
+ off++;
+ }
+
+ while(1) {
+ int delimited = FALSE;
+
+ /* Cell underline ("-----", ":----", "----:" or ":----:") */
+ if(off < ctx->size && CH(off) == _T(':'))
+ off++;
+ if(off >= ctx->size || CH(off) != _T('-'))
+ return FALSE;
+ while(off < ctx->size && CH(off) == _T('-'))
+ off++;
+ if(off < ctx->size && CH(off) == _T(':'))
+ off++;
+
+ col_count++;
+
+ /* Pipe delimiter (optional at the end of line). */
+ while(off < ctx->size && ISWHITESPACE(off))
+ off++;
+ if(off < ctx->size && CH(off) == _T('|')) {
+ delimited = TRUE;
+ found_pipe = TRUE;
+ off++;
+ while(off < ctx->size && ISWHITESPACE(off))
+ off++;
+ }
+
+ /* Success, if we reach end of line. */
+ if(off >= ctx->size || ISNEWLINE(off))
+ break;
+
+ if(!delimited)
+ return FALSE;
+ }
+
+ if(!found_pipe)
+ return FALSE;
+
+ *p_end = off;
+ *p_col_count = col_count;
+ return TRUE;
+}
+
+static int
+md_is_opening_code_fence(MD_CTX* ctx, OFF beg, OFF* p_end)
+{
+ OFF off = beg;
+
+ while(off < ctx->size && CH(off) == CH(beg))
+ off++;
+
+ /* Fence must have at least three characters. */
+ if(off - beg < 3)
+ return FALSE;
+
+ ctx->code_fence_length = off - beg;
+
+ /* Optionally, space(s) can follow. */
+ while(off < ctx->size && CH(off) == _T(' '))
+ off++;
+
+ /* Optionally, an info string can follow. */
+ while(off < ctx->size && !ISNEWLINE(off)) {
+ /* Backtick-based fence must not contain '`' in the info string. */
+ if(CH(beg) == _T('`') && CH(off) == _T('`'))
+ return FALSE;
+ off++;
+ }
+
+ *p_end = off;
+ return TRUE;
+}
+
+static int
+md_is_closing_code_fence(MD_CTX* ctx, CHAR ch, OFF beg, OFF* p_end)
+{
+ OFF off = beg;
+ int ret = FALSE;
+
+ /* Closing fence must have at least the same length and use same char as
+ * opening one. */
+ while(off < ctx->size && CH(off) == ch)
+ off++;
+ if(off - beg < ctx->code_fence_length)
+ goto out;
+
+ /* Optionally, space(s) can follow */
+ while(off < ctx->size && CH(off) == _T(' '))
+ off++;
+
+ /* But nothing more is allowed on the line. */
+ if(off < ctx->size && !ISNEWLINE(off))
+ goto out;
+
+ ret = TRUE;
+
+out:
+ /* Note we set *p_end even on failure: If we are not closing fence, caller
+ * would eat the line anyway without any parsing. */
+ *p_end = off;
+ return ret;
+}
+
+/* Returns type of the raw HTML block, or FALSE if it is not HTML block.
+ * (Refer to CommonMark specification for details about the types.)
+ */
+static int
+md_is_html_block_start_condition(MD_CTX* ctx, OFF beg)
+{
+ typedef struct TAG_tag TAG;
+ struct TAG_tag {
+ const CHAR* name;
+ unsigned len : 8;
+ };
+
+ /* Type 6 is started by a long list of allowed tags. We use two-level
+ * tree to speed-up the search. */
+#ifdef X
+ #undef X
+#endif
+#define X(name) { _T(name), (sizeof(name)-1) / sizeof(CHAR) }
+#define Xend { NULL, 0 }
+ static const TAG t1[] = { X("pre"), X("script"), X("style"), X("textarea"), Xend };
+
+ static const TAG a6[] = { X("address"), X("article"), X("aside"), Xend };
+ static const TAG b6[] = { X("base"), X("basefont"), X("blockquote"), X("body"), Xend };
+ static const TAG c6[] = { X("caption"), X("center"), X("col"), X("colgroup"), Xend };
+ static const TAG d6[] = { X("dd"), X("details"), X("dialog"), X("dir"),
+ X("div"), X("dl"), X("dt"), Xend };
+ static const TAG f6[] = { X("fieldset"), X("figcaption"), X("figure"), X("footer"),
+ X("form"), X("frame"), X("frameset"), Xend };
+ static const TAG h6[] = { X("h1"), X("head"), X("header"), X("hr"), X("html"), Xend };
+ static const TAG i6[] = { X("iframe"), Xend };
+ static const TAG l6[] = { X("legend"), X("li"), X("link"), Xend };
+ static const TAG m6[] = { X("main"), X("menu"), X("menuitem"), Xend };
+ static const TAG n6[] = { X("nav"), X("noframes"), Xend };
+ static const TAG o6[] = { X("ol"), X("optgroup"), X("option"), Xend };
+ static const TAG p6[] = { X("p"), X("param"), Xend };
+ static const TAG s6[] = { X("section"), X("source"), X("summary"), Xend };
+ static const TAG t6[] = { X("table"), X("tbody"), X("td"), X("tfoot"), X("th"),
+ X("thead"), X("title"), X("tr"), X("track"), Xend };
+ static const TAG u6[] = { X("ul"), Xend };
+ static const TAG xx[] = { Xend };
+#undef X
+
+ static const TAG* map6[26] = {
+ a6, b6, c6, d6, xx, f6, xx, h6, i6, xx, xx, l6, m6,
+ n6, o6, p6, xx, xx, s6, t6, u6, xx, xx, xx, xx, xx
+ };
+ OFF off = beg + 1;
+ int i;
+
+ /* Check for type 1: <script, <pre, or <style */
+ for(i = 0; t1[i].name != NULL; i++) {
+ if(off + t1[i].len <= ctx->size) {
+ if(md_ascii_case_eq(STR(off), t1[i].name, t1[i].len))
+ return 1;
+ }
+ }
+
+ /* Check for type 2: <!-- */
+ if(off + 3 < ctx->size && CH(off) == _T('!') && CH(off+1) == _T('-') && CH(off+2) == _T('-'))
+ return 2;
+
+ /* Check for type 3: <? */
+ if(off < ctx->size && CH(off) == _T('?'))
+ return 3;
+
+ /* Check for type 4 or 5: <! */
+ if(off < ctx->size && CH(off) == _T('!')) {
+ /* Check for type 4: <! followed by uppercase letter. */
+ if(off + 1 < ctx->size && ISASCII(off+1))
+ return 4;
+
+ /* Check for type 5: <![CDATA[ */
+ if(off + 8 < ctx->size) {
+ if(md_ascii_eq(STR(off), _T("![CDATA["), 8))
+ return 5;
+ }
+ }
+
+ /* Check for type 6: Many possible starting tags listed above. */
+ if(off + 1 < ctx->size && (ISALPHA(off) || (CH(off) == _T('/') && ISALPHA(off+1)))) {
+ int slot;
+ const TAG* tags;
+
+ if(CH(off) == _T('/'))
+ off++;
+
+ slot = (ISUPPER(off) ? CH(off) - 'A' : CH(off) - 'a');
+ tags = map6[slot];
+
+ for(i = 0; tags[i].name != NULL; i++) {
+ if(off + tags[i].len <= ctx->size) {
+ if(md_ascii_case_eq(STR(off), tags[i].name, tags[i].len)) {
+ OFF tmp = off + tags[i].len;
+ if(tmp >= ctx->size)
+ return 6;
+ if(ISBLANK(tmp) || ISNEWLINE(tmp) || CH(tmp) == _T('>'))
+ return 6;
+ if(tmp+1 < ctx->size && CH(tmp) == _T('/') && CH(tmp+1) == _T('>'))
+ return 6;
+ break;
+ }
+ }
+ }
+ }
+
+ /* Check for type 7: any COMPLETE other opening or closing tag. */
+ if(off + 1 < ctx->size) {
+ OFF end;
+
+ if(md_is_html_tag(ctx, NULL, 0, beg, ctx->size, &end)) {
+ /* Only optional whitespace and new line may follow. */
+ while(end < ctx->size && ISWHITESPACE(end))
+ end++;
+ if(end >= ctx->size || ISNEWLINE(end))
+ return 7;
+ }
+ }
+
+ return FALSE;
+}
+
+/* Case sensitive check whether there is a substring 'what' between 'beg'
+ * and end of line. */
+static int
+md_line_contains(MD_CTX* ctx, OFF beg, const CHAR* what, SZ what_len, OFF* p_end)
+{
+ OFF i;
+ for(i = beg; i + what_len < ctx->size; i++) {
+ if(ISNEWLINE(i))
+ break;
+ if(memcmp(STR(i), what, what_len * sizeof(CHAR)) == 0) {
+ *p_end = i + what_len;
+ return TRUE;
+ }
+ }
+
+ *p_end = i;
+ return FALSE;
+}
+
+/* Returns type of HTML block end condition or FALSE if not an end condition.
+ *
+ * Note it fills p_end even when it is not end condition as the caller
+ * does not need to analyze contents of a raw HTML block.
+ */
+static int
+md_is_html_block_end_condition(MD_CTX* ctx, OFF beg, OFF* p_end)
+{
+ switch(ctx->html_block_type) {
+ case 1:
+ {
+ OFF off = beg;
+
+ while(off < ctx->size && !ISNEWLINE(off)) {
+ if(CH(off) == _T('<')) {
+ #define FIND_TAG_END(string, length) \
+ if(off + length <= ctx->size && \
+ md_ascii_case_eq(STR(off), _T(string), length)) { \
+ *p_end = off + length; \
+ return TRUE; \
+ }
+ FIND_TAG_END("</script>", 9)
+ FIND_TAG_END("</style>", 8)
+ FIND_TAG_END("</pre>", 6)
+ #undef FIND_TAG_END
+ }
+
+ off++;
+ }
+ *p_end = off;
+ return FALSE;
+ }
+
+ case 2:
+ return (md_line_contains(ctx, beg, _T("-->"), 3, p_end) ? 2 : FALSE);
+
+ case 3:
+ return (md_line_contains(ctx, beg, _T("?>"), 2, p_end) ? 3 : FALSE);
+
+ case 4:
+ return (md_line_contains(ctx, beg, _T(">"), 1, p_end) ? 4 : FALSE);
+
+ case 5:
+ return (md_line_contains(ctx, beg, _T("]]>"), 3, p_end) ? 5 : FALSE);
+
+ case 6: /* Pass through */
+ case 7:
+ *p_end = beg;
+ return (beg >= ctx->size || ISNEWLINE(beg) ? ctx->html_block_type : FALSE);
+
+ default:
+ MD_UNREACHABLE();
+ }
+ return FALSE;
+}
+
+
+static int
+md_is_container_compatible(const MD_CONTAINER* pivot, const MD_CONTAINER* container)
+{
+ /* Block quote has no "items" like lists. */
+ if(container->ch == _T('>'))
+ return FALSE;
+
+ if(container->ch != pivot->ch)
+ return FALSE;
+ if(container->mark_indent > pivot->contents_indent)
+ return FALSE;
+
+ return TRUE;
+}
+
+static int
+md_push_container(MD_CTX* ctx, const MD_CONTAINER* container)
+{
+ if(ctx->n_containers >= ctx->alloc_containers) {
+ MD_CONTAINER* new_containers;
+
+ ctx->alloc_containers = (ctx->alloc_containers > 0
+ ? ctx->alloc_containers + ctx->alloc_containers / 2
+ : 16);
+ new_containers = realloc(ctx->containers, ctx->alloc_containers * sizeof(MD_CONTAINER));
+ if(new_containers == NULL) {
+ MD_LOG("realloc() failed.");
+ return -1;
+ }
+
+ ctx->containers = new_containers;
+ }
+
+ memcpy(&ctx->containers[ctx->n_containers++], container, sizeof(MD_CONTAINER));
+ return 0;
+}
+
+static int
+md_enter_child_containers(MD_CTX* ctx, int n_children)
+{
+ int i;
+ int ret = 0;
+
+ for(i = ctx->n_containers - n_children; i < ctx->n_containers; i++) {
+ MD_CONTAINER* c = &ctx->containers[i];
+ int is_ordered_list = FALSE;
+
+ switch(c->ch) {
+ case _T(')'):
+ case _T('.'):
+ is_ordered_list = TRUE;
+ MD_FALLTHROUGH();
+
+ case _T('-'):
+ case _T('+'):
+ case _T('*'):
+ /* Remember offset in ctx->block_bytes so we can revisit the
+ * block if we detect it is a loose list. */
+ md_end_current_block(ctx);
+ c->block_byte_off = ctx->n_block_bytes;
+
+ MD_CHECK(md_push_container_bytes(ctx,
+ (is_ordered_list ? MD_BLOCK_OL : MD_BLOCK_UL),
+ c->start, c->ch, MD_BLOCK_CONTAINER_OPENER));
+ MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_LI,
+ c->task_mark_off,
+ (c->is_task ? CH(c->task_mark_off) : 0),
+ MD_BLOCK_CONTAINER_OPENER));
+ break;
+
+ case _T('>'):
+ MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_QUOTE, 0, 0, MD_BLOCK_CONTAINER_OPENER));
+ break;
+
+ default:
+ MD_UNREACHABLE();
+ break;
+ }
+ }
+
+abort:
+ return ret;
+}
+
+static int
+md_leave_child_containers(MD_CTX* ctx, int n_keep)
+{
+ int ret = 0;
+
+ while(ctx->n_containers > n_keep) {
+ MD_CONTAINER* c = &ctx->containers[ctx->n_containers-1];
+ int is_ordered_list = FALSE;
+
+ switch(c->ch) {
+ case _T(')'):
+ case _T('.'):
+ is_ordered_list = TRUE;
+ MD_FALLTHROUGH();
+
+ case _T('-'):
+ case _T('+'):
+ case _T('*'):
+ MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_LI,
+ c->task_mark_off, (c->is_task ? CH(c->task_mark_off) : 0),
+ MD_BLOCK_CONTAINER_CLOSER));
+ MD_CHECK(md_push_container_bytes(ctx,
+ (is_ordered_list ? MD_BLOCK_OL : MD_BLOCK_UL), 0,
+ c->ch, MD_BLOCK_CONTAINER_CLOSER));
+ break;
+
+ case _T('>'):
+ MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_QUOTE, 0,
+ 0, MD_BLOCK_CONTAINER_CLOSER));
+ break;
+
+ default:
+ MD_UNREACHABLE();
+ break;
+ }
+
+ ctx->n_containers--;
+ }
+
+abort:
+ return ret;
+}
+
+static int
+md_is_container_mark(MD_CTX* ctx, unsigned indent, OFF beg, OFF* p_end, MD_CONTAINER* p_container)
+{
+ OFF off = beg;
+ OFF max_end;
+
+ if(off >= ctx->size || indent >= ctx->code_indent_offset)
+ return FALSE;
+
+ /* Check for block quote mark. */
+ if(CH(off) == _T('>')) {
+ off++;
+ p_container->ch = _T('>');
+ p_container->is_loose = FALSE;
+ p_container->is_task = FALSE;
+ p_container->mark_indent = indent;
+ p_container->contents_indent = indent + 1;
+ *p_end = off;
+ return TRUE;
+ }
+
+ /* Check for list item bullet mark. */
+ if(ISANYOF(off, _T("-+*")) && (off+1 >= ctx->size || ISBLANK(off+1) || ISNEWLINE(off+1))) {
+ p_container->ch = CH(off);
+ p_container->is_loose = FALSE;
+ p_container->is_task = FALSE;
+ p_container->mark_indent = indent;
+ p_container->contents_indent = indent + 1;
+ *p_end = off+1;
+ return TRUE;
+ }
+
+ /* Check for ordered list item marks. */
+ max_end = off + 9;
+ if(max_end > ctx->size)
+ max_end = ctx->size;
+ p_container->start = 0;
+ while(off < max_end && ISDIGIT(off)) {
+ p_container->start = p_container->start * 10 + CH(off) - _T('0');
+ off++;
+ }
+ if(off > beg &&
+ off < ctx->size &&
+ (CH(off) == _T('.') || CH(off) == _T(')')) &&
+ (off+1 >= ctx->size || ISBLANK(off+1) || ISNEWLINE(off+1)))
+ {
+ p_container->ch = CH(off);
+ p_container->is_loose = FALSE;
+ p_container->is_task = FALSE;
+ p_container->mark_indent = indent;
+ p_container->contents_indent = indent + off - beg + 1;
+ *p_end = off+1;
+ return TRUE;
+ }
+
+ return FALSE;
+}
+
+static unsigned
+md_line_indentation(MD_CTX* ctx, unsigned total_indent, OFF beg, OFF* p_end)
+{
+ OFF off = beg;
+ unsigned indent = total_indent;
+
+ while(off < ctx->size && ISBLANK(off)) {
+ if(CH(off) == _T('\t'))
+ indent = (indent + 4) & ~3;
+ else
+ indent++;
+ off++;
+ }
+
+ *p_end = off;
+ return indent - total_indent;
+}
+
+static const MD_LINE_ANALYSIS md_dummy_blank_line = { MD_LINE_BLANK, 0, 0, 0, 0 };
+
+/* Analyze type of the line and find some its properties. This serves as a
+ * main input for determining type and boundaries of a block. */
+static int
+md_analyze_line(MD_CTX* ctx, OFF beg, OFF* p_end,
+ const MD_LINE_ANALYSIS* pivot_line, MD_LINE_ANALYSIS* line)
+{
+ unsigned total_indent = 0;
+ int n_parents = 0;
+ int n_brothers = 0;
+ int n_children = 0;
+ MD_CONTAINER container = { 0 };
+ int prev_line_has_list_loosening_effect = ctx->last_line_has_list_loosening_effect;
+ OFF off = beg;
+ OFF hr_killer = 0;
+ int ret = 0;
+
+ line->indent = md_line_indentation(ctx, total_indent, off, &off);
+ total_indent += line->indent;
+ line->beg = off;
+
+ /* Given the indentation and block quote marks '>', determine how many of
+ * the current containers are our parents. */
+ while(n_parents < ctx->n_containers) {
+ MD_CONTAINER* c = &ctx->containers[n_parents];
+
+ if(c->ch == _T('>') && line->indent < ctx->code_indent_offset &&
+ off < ctx->size && CH(off) == _T('>'))
+ {
+ /* Block quote mark. */
+ off++;
+ total_indent++;
+ line->indent = md_line_indentation(ctx, total_indent, off, &off);
+ total_indent += line->indent;
+
+ /* The optional 1st space after '>' is part of the block quote mark. */
+ if(line->indent > 0)
+ line->indent--;
+
+ line->beg = off;
+
+ } else if(c->ch != _T('>') && line->indent >= c->contents_indent) {
+ /* List. */
+ line->indent -= c->contents_indent;
+ } else {
+ break;
+ }
+
+ n_parents++;
+ }
+
+ if(off >= ctx->size || ISNEWLINE(off)) {
+ /* Blank line does not need any real indentation to be nested inside
+ * a list. */
+ if(n_brothers + n_children == 0) {
+ while(n_parents < ctx->n_containers && ctx->containers[n_parents].ch != _T('>'))
+ n_parents++;
+ }
+ }
+
+ while(TRUE) {
+ /* Check whether we are fenced code continuation. */
+ if(pivot_line->type == MD_LINE_FENCEDCODE) {
+ line->beg = off;
+
+ /* We are another MD_LINE_FENCEDCODE unless we are closing fence
+ * which we transform into MD_LINE_BLANK. */
+ if(line->indent < ctx->code_indent_offset) {
+ if(md_is_closing_code_fence(ctx, CH(pivot_line->beg), off, &off)) {
+ line->type = MD_LINE_BLANK;
+ ctx->last_line_has_list_loosening_effect = FALSE;
+ break;
+ }
+ }
+
+ /* Change indentation accordingly to the initial code fence. */
+ if(n_parents == ctx->n_containers) {
+ if(line->indent > pivot_line->indent)
+ line->indent -= pivot_line->indent;
+ else
+ line->indent = 0;
+
+ line->type = MD_LINE_FENCEDCODE;
+ break;
+ }
+ }
+
+ /* Check whether we are HTML block continuation. */
+ if(pivot_line->type == MD_LINE_HTML && ctx->html_block_type > 0) {
+ if(n_parents < ctx->n_containers) {
+ /* HTML block is implicitly ended if the enclosing container
+ * block ends. */
+ ctx->html_block_type = 0;
+ } else {
+ int html_block_type;
+
+ html_block_type = md_is_html_block_end_condition(ctx, off, &off);
+ if(html_block_type > 0) {
+ MD_ASSERT(html_block_type == ctx->html_block_type);
+
+ /* Make sure this is the last line of the block. */
+ ctx->html_block_type = 0;
+
+ /* Some end conditions serve as blank lines at the same time. */
+ if(html_block_type == 6 || html_block_type == 7) {
+ line->type = MD_LINE_BLANK;
+ line->indent = 0;
+ break;
+ }
+ }
+
+ line->type = MD_LINE_HTML;
+ n_parents = ctx->n_containers;
+ break;
+ }
+ }
+
+ /* Check for blank line. */
+ if(off >= ctx->size || ISNEWLINE(off)) {
+ if(pivot_line->type == MD_LINE_INDENTEDCODE && n_parents == ctx->n_containers) {
+ line->type = MD_LINE_INDENTEDCODE;
+ if(line->indent > ctx->code_indent_offset)
+ line->indent -= ctx->code_indent_offset;
+ else
+ line->indent = 0;
+ ctx->last_line_has_list_loosening_effect = FALSE;
+ } else {
+ line->type = MD_LINE_BLANK;
+ ctx->last_line_has_list_loosening_effect = (n_parents > 0 &&
+ n_brothers + n_children == 0 &&
+ ctx->containers[n_parents-1].ch != _T('>'));
+
+ #if 1
+ /* See https://github.com/mity/md4c/issues/6
+ *
+ * This ugly checking tests we are in (yet empty) list item but
+ * not its very first line (i.e. not the line with the list
+ * item mark).
+ *
+ * If we are such a blank line, then any following non-blank
+ * line which would be part of the list item actually has to
+ * end the list because according to the specification, "a list
+ * item can begin with at most one blank line."
+ */
+ if(n_parents > 0 && ctx->containers[n_parents-1].ch != _T('>') &&
+ n_brothers + n_children == 0 && ctx->current_block == NULL &&
+ ctx->n_block_bytes > (int) sizeof(MD_BLOCK))
+ {
+ MD_BLOCK* top_block = (MD_BLOCK*) ((char*)ctx->block_bytes + ctx->n_block_bytes - sizeof(MD_BLOCK));
+ if(top_block->type == MD_BLOCK_LI)
+ ctx->last_list_item_starts_with_two_blank_lines = TRUE;
+ }
+ #endif
+ }
+ break;
+ } else {
+ #if 1
+ /* This is the 2nd half of the hack. If the flag is set (i.e. there
+ * was a 2nd blank line at the beginning of the list item) and if
+ * we would otherwise still belong to the list item, we enforce
+ * the end of the list. */
+ ctx->last_line_has_list_loosening_effect = FALSE;
+ if(ctx->last_list_item_starts_with_two_blank_lines) {
+ if(n_parents > 0 && ctx->containers[n_parents-1].ch != _T('>') &&
+ n_brothers + n_children == 0 && ctx->current_block == NULL &&
+ ctx->n_block_bytes > (int) sizeof(MD_BLOCK))
+ {
+ MD_BLOCK* top_block = (MD_BLOCK*) ((char*)ctx->block_bytes + ctx->n_block_bytes - sizeof(MD_BLOCK));
+ if(top_block->type == MD_BLOCK_LI)
+ n_parents--;
+ }
+
+ ctx->last_list_item_starts_with_two_blank_lines = FALSE;
+ }
+ #endif
+ }
+
+ /* Check whether we are Setext underline. */
+ if(line->indent < ctx->code_indent_offset && pivot_line->type == MD_LINE_TEXT
+ && off < ctx->size && ISANYOF2(off, _T('='), _T('-'))
+ && (n_parents == ctx->n_containers))
+ {
+ unsigned level;
+
+ if(md_is_setext_underline(ctx, off, &off, &level)) {
+ line->type = MD_LINE_SETEXTUNDERLINE;
+ line->data = level;
+ break;
+ }
+ }
+
+ /* Check for thematic break line. */
+ if(line->indent < ctx->code_indent_offset
+ && off < ctx->size && off >= hr_killer
+ && ISANYOF(off, _T("-_*")))
+ {
+ if(md_is_hr_line(ctx, off, &off, &hr_killer)) {
+ line->type = MD_LINE_HR;
+ break;
+ }
+ }
+
+ /* Check for "brother" container. I.e. whether we are another list item
+ * in already started list. */
+ if(n_parents < ctx->n_containers && n_brothers + n_children == 0) {
+ OFF tmp;
+
+ if(md_is_container_mark(ctx, line->indent, off, &tmp, &container) &&
+ md_is_container_compatible(&ctx->containers[n_parents], &container))
+ {
+ pivot_line = &md_dummy_blank_line;
+
+ off = tmp;
+
+ total_indent += container.contents_indent - container.mark_indent;
+ line->indent = md_line_indentation(ctx, total_indent, off, &off);
+ total_indent += line->indent;
+ line->beg = off;
+
+ /* Some of the following whitespace actually still belongs to the mark. */
+ if(off >= ctx->size || ISNEWLINE(off)) {
+ container.contents_indent++;
+ } else if(line->indent <= ctx->code_indent_offset) {
+ container.contents_indent += line->indent;
+ line->indent = 0;
+ } else {
+ container.contents_indent += 1;
+ line->indent--;
+ }
+
+ ctx->containers[n_parents].mark_indent = container.mark_indent;
+ ctx->containers[n_parents].contents_indent = container.contents_indent;
+
+ n_brothers++;
+ continue;
+ }
+ }
+
+ /* Check for indented code.
+ * Note indented code block cannot interrupt a paragraph. */
+ if(line->indent >= ctx->code_indent_offset &&
+ (pivot_line->type == MD_LINE_BLANK || pivot_line->type == MD_LINE_INDENTEDCODE))
+ {
+ line->type = MD_LINE_INDENTEDCODE;
+ MD_ASSERT(line->indent >= ctx->code_indent_offset);
+ line->indent -= ctx->code_indent_offset;
+ line->data = 0;
+ break;
+ }
+
+ /* Check for start of a new container block. */
+ if(line->indent < ctx->code_indent_offset &&
+ md_is_container_mark(ctx, line->indent, off, &off, &container))
+ {
+ if(pivot_line->type == MD_LINE_TEXT && n_parents == ctx->n_containers &&
+ (off >= ctx->size || ISNEWLINE(off)) && container.ch != _T('>'))
+ {
+ /* Noop. List mark followed by a blank line cannot interrupt a paragraph. */
+ } else if(pivot_line->type == MD_LINE_TEXT && n_parents == ctx->n_containers &&
+ ISANYOF2_(container.ch, _T('.'), _T(')')) && container.start != 1)
+ {
+ /* Noop. Ordered list cannot interrupt a paragraph unless the start index is 1. */
+ } else {
+ total_indent += container.contents_indent - container.mark_indent;
+ line->indent = md_line_indentation(ctx, total_indent, off, &off);
+ total_indent += line->indent;
+
+ line->beg = off;
+ line->data = container.ch;
+
+ /* Some of the following whitespace actually still belongs to the mark. */
+ if(off >= ctx->size || ISNEWLINE(off)) {
+ container.contents_indent++;
+ } else if(line->indent <= ctx->code_indent_offset) {
+ container.contents_indent += line->indent;
+ line->indent = 0;
+ } else {
+ container.contents_indent += 1;
+ line->indent--;
+ }
+
+ if(n_brothers + n_children == 0)
+ pivot_line = &md_dummy_blank_line;
+
+ if(n_children == 0)
+ MD_CHECK(md_leave_child_containers(ctx, n_parents + n_brothers));
+
+ n_children++;
+ MD_CHECK(md_push_container(ctx, &container));
+ continue;
+ }
+ }
+
+ /* Check whether we are table continuation. */
+ if(pivot_line->type == MD_LINE_TABLE && n_parents == ctx->n_containers) {
+ line->type = MD_LINE_TABLE;
+ break;
+ }
+
+ /* Check for ATX header. */
+ if(line->indent < ctx->code_indent_offset &&
+ off < ctx->size && CH(off) == _T('#'))
+ {
+ unsigned level;
+
+ if(md_is_atxheader_line(ctx, off, &line->beg, &off, &level)) {
+ line->type = MD_LINE_ATXHEADER;
+ line->data = level;
+ break;
+ }
+ }
+
+ /* Check whether we are starting code fence. */
+ if(off < ctx->size && ISANYOF2(off, _T('`'), _T('~'))) {
+ if(md_is_opening_code_fence(ctx, off, &off)) {
+ line->type = MD_LINE_FENCEDCODE;
+ line->data = 1;
+ break;
+ }
+ }
+
+ /* Check for start of raw HTML block. */
+ if(off < ctx->size && CH(off) == _T('<')
+ && !(ctx->parser.flags & MD_FLAG_NOHTMLBLOCKS))
+ {
+ ctx->html_block_type = md_is_html_block_start_condition(ctx, off);
+
+ /* HTML block type 7 cannot interrupt paragraph. */
+ if(ctx->html_block_type == 7 && pivot_line->type == MD_LINE_TEXT)
+ ctx->html_block_type = 0;
+
+ if(ctx->html_block_type > 0) {
+ /* The line itself also may immediately close the block. */
+ if(md_is_html_block_end_condition(ctx, off, &off) == ctx->html_block_type) {
+ /* Make sure this is the last line of the block. */
+ ctx->html_block_type = 0;
+ }
+
+ line->type = MD_LINE_HTML;
+ break;
+ }
+ }
+
+ /* Check for table underline. */
+ if((ctx->parser.flags & MD_FLAG_TABLES) && pivot_line->type == MD_LINE_TEXT
+ && off < ctx->size && ISANYOF3(off, _T('|'), _T('-'), _T(':'))
+ && n_parents == ctx->n_containers)
+ {
+ unsigned col_count;
+
+ if(ctx->current_block != NULL && ctx->current_block->n_lines == 1 &&
+ md_is_table_underline(ctx, off, &off, &col_count))
+ {
+ line->data = col_count;
+ line->type = MD_LINE_TABLEUNDERLINE;
+ break;
+ }
+ }
+
+ /* By default, we are normal text line. */
+ line->type = MD_LINE_TEXT;
+ if(pivot_line->type == MD_LINE_TEXT && n_brothers + n_children == 0) {
+ /* Lazy continuation. */
+ n_parents = ctx->n_containers;
+ }
+
+ /* Check for task mark. */
+ if((ctx->parser.flags & MD_FLAG_TASKLISTS) && n_brothers + n_children > 0 &&
+ ISANYOF_(ctx->containers[ctx->n_containers-1].ch, _T("-+*.)")))
+ {
+ OFF tmp = off;
+
+ while(tmp < ctx->size && tmp < off + 3 && ISBLANK(tmp))
+ tmp++;
+ if(tmp + 2 < ctx->size && CH(tmp) == _T('[') &&
+ ISANYOF(tmp+1, _T("xX ")) && CH(tmp+2) == _T(']') &&
+ (tmp + 3 == ctx->size || ISBLANK(tmp+3) || ISNEWLINE(tmp+3)))
+ {
+ MD_CONTAINER* task_container = (n_children > 0 ? &ctx->containers[ctx->n_containers-1] : &container);
+ task_container->is_task = TRUE;
+ task_container->task_mark_off = tmp + 1;
+ off = tmp + 3;
+ while(off < ctx->size && ISWHITESPACE(off))
+ off++;
+ if (off == ctx->size) break;
+ line->beg = off;
+ }
+ }
+
+ break;
+ }
+
+ /* Scan for end of the line.
+ *
+ * Note this is quite a bottleneck of the parsing as we here iterate almost
+ * over compete document.
+ */
+#if defined __linux__ && !defined MD4C_USE_UTF16
+ /* Recent glibc versions have superbly optimized strcspn(), even using
+ * vectorization if available. */
+ if(ctx->doc_ends_with_newline && off < ctx->size) {
+ while(TRUE) {
+ off += (OFF) strcspn(STR(off), "\r\n");
+
+ /* strcspn() can stop on zero terminator; but that can appear
+ * anywhere in the Markfown input... */
+ if(CH(off) == _T('\0'))
+ off++;
+ else
+ break;
+ }
+ } else
+#endif
+ {
+ /* Optimization: Use some loop unrolling. */
+ while(off + 3 < ctx->size && !ISNEWLINE(off+0) && !ISNEWLINE(off+1)
+ && !ISNEWLINE(off+2) && !ISNEWLINE(off+3))
+ off += 4;
+ while(off < ctx->size && !ISNEWLINE(off))
+ off++;
+ }
+
+ /* Set end of the line. */
+ line->end = off;
+
+ /* But for ATX header, we should exclude the optional trailing mark. */
+ if(line->type == MD_LINE_ATXHEADER) {
+ OFF tmp = line->end;
+ while(tmp > line->beg && CH(tmp-1) == _T(' '))
+ tmp--;
+ while(tmp > line->beg && CH(tmp-1) == _T('#'))
+ tmp--;
+ if(tmp == line->beg || CH(tmp-1) == _T(' ') || (ctx->parser.flags & MD_FLAG_PERMISSIVEATXHEADERS))
+ line->end = tmp;
+ }
+
+ /* Trim trailing spaces. */
+ if(line->type != MD_LINE_INDENTEDCODE && line->type != MD_LINE_FENCEDCODE) {
+ while(line->end > line->beg && CH(line->end-1) == _T(' '))
+ line->end--;
+ }
+
+ /* Eat also the new line. */
+ if(off < ctx->size && CH(off) == _T('\r'))
+ off++;
+ if(off < ctx->size && CH(off) == _T('\n'))
+ off++;
+
+ *p_end = off;
+
+ /* If we belong to a list after seeing a blank line, the list is loose. */
+ if(prev_line_has_list_loosening_effect && line->type != MD_LINE_BLANK && n_parents + n_brothers > 0) {
+ MD_CONTAINER* c = &ctx->containers[n_parents + n_brothers - 1];
+ if(c->ch != _T('>')) {
+ MD_BLOCK* block = (MD_BLOCK*) (((char*)ctx->block_bytes) + c->block_byte_off);
+ block->flags |= MD_BLOCK_LOOSE_LIST;
+ }
+ }
+
+ /* Leave any containers we are not part of anymore. */
+ if(n_children == 0 && n_parents + n_brothers < ctx->n_containers)
+ MD_CHECK(md_leave_child_containers(ctx, n_parents + n_brothers));
+
+ /* Enter any container we found a mark for. */
+ if(n_brothers > 0) {
+ MD_ASSERT(n_brothers == 1);
+ MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_LI,
+ ctx->containers[n_parents].task_mark_off,
+ (ctx->containers[n_parents].is_task ? CH(ctx->containers[n_parents].task_mark_off) : 0),
+ MD_BLOCK_CONTAINER_CLOSER));
+ MD_CHECK(md_push_container_bytes(ctx, MD_BLOCK_LI,
+ container.task_mark_off,
+ (container.is_task ? CH(container.task_mark_off) : 0),
+ MD_BLOCK_CONTAINER_OPENER));
+ ctx->containers[n_parents].is_task = container.is_task;
+ ctx->containers[n_parents].task_mark_off = container.task_mark_off;
+ }
+
+ if(n_children > 0)
+ MD_CHECK(md_enter_child_containers(ctx, n_children));
+
+abort:
+ return ret;
+}
+
+static int
+md_process_line(MD_CTX* ctx, const MD_LINE_ANALYSIS** p_pivot_line, MD_LINE_ANALYSIS* line)
+{
+ const MD_LINE_ANALYSIS* pivot_line = *p_pivot_line;
+ int ret = 0;
+
+ /* Blank line ends current leaf block. */
+ if(line->type == MD_LINE_BLANK) {
+ MD_CHECK(md_end_current_block(ctx));
+ *p_pivot_line = &md_dummy_blank_line;
+ return 0;
+ }
+
+ /* Some line types form block on their own. */
+ if(line->type == MD_LINE_HR || line->type == MD_LINE_ATXHEADER) {
+ MD_CHECK(md_end_current_block(ctx));
+
+ /* Add our single-line block. */
+ MD_CHECK(md_start_new_block(ctx, line));
+ MD_CHECK(md_add_line_into_current_block(ctx, line));
+ MD_CHECK(md_end_current_block(ctx));
+ *p_pivot_line = &md_dummy_blank_line;
+ return 0;
+ }
+
+ /* MD_LINE_SETEXTUNDERLINE changes meaning of the current block and ends it. */
+ if(line->type == MD_LINE_SETEXTUNDERLINE) {
+ MD_ASSERT(ctx->current_block != NULL);
+ ctx->current_block->type = MD_BLOCK_H;
+ ctx->current_block->data = line->data;
+ ctx->current_block->flags |= MD_BLOCK_SETEXT_HEADER;
+ MD_CHECK(md_add_line_into_current_block(ctx, line));
+ MD_CHECK(md_end_current_block(ctx));
+ if(ctx->current_block == NULL) {
+ *p_pivot_line = &md_dummy_blank_line;
+ } else {
+ /* This happens if we have consumed all the body as link ref. defs.
+ * and downgraded the underline into start of a new paragraph block. */
+ line->type = MD_LINE_TEXT;
+ *p_pivot_line = line;
+ }
+ return 0;
+ }
+
+ /* MD_LINE_TABLEUNDERLINE changes meaning of the current block. */
+ if(line->type == MD_LINE_TABLEUNDERLINE) {
+ MD_ASSERT(ctx->current_block != NULL);
+ MD_ASSERT(ctx->current_block->n_lines == 1);
+ ctx->current_block->type = MD_BLOCK_TABLE;
+ ctx->current_block->data = line->data;
+ MD_ASSERT(pivot_line != &md_dummy_blank_line);
+ ((MD_LINE_ANALYSIS*)pivot_line)->type = MD_LINE_TABLE;
+ MD_CHECK(md_add_line_into_current_block(ctx, line));
+ return 0;
+ }
+
+ /* The current block also ends if the line has different type. */
+ if(line->type != pivot_line->type)
+ MD_CHECK(md_end_current_block(ctx));
+
+ /* The current line may start a new block. */
+ if(ctx->current_block == NULL) {
+ MD_CHECK(md_start_new_block(ctx, line));
+ *p_pivot_line = line;
+ }
+
+ /* In all other cases the line is just a continuation of the current block. */
+ MD_CHECK(md_add_line_into_current_block(ctx, line));
+
+abort:
+ return ret;
+}
+
+static int
+md_process_doc(MD_CTX *ctx)
+{
+ const MD_LINE_ANALYSIS* pivot_line = &md_dummy_blank_line;
+ MD_LINE_ANALYSIS line_buf[2];
+ MD_LINE_ANALYSIS* line = &line_buf[0];
+ OFF off = 0;
+ int ret = 0;
+
+ MD_ENTER_BLOCK(MD_BLOCK_DOC, NULL);
+
+ while(off < ctx->size) {
+ if(line == pivot_line)
+ line = (line == &line_buf[0] ? &line_buf[1] : &line_buf[0]);
+
+ MD_CHECK(md_analyze_line(ctx, off, &off, pivot_line, line));
+ MD_CHECK(md_process_line(ctx, &pivot_line, line));
+ }
+
+ md_end_current_block(ctx);
+
+ MD_CHECK(md_build_ref_def_hashtable(ctx));
+
+ /* Process all blocks. */
+ MD_CHECK(md_leave_child_containers(ctx, 0));
+ MD_CHECK(md_process_all_blocks(ctx));
+
+ MD_LEAVE_BLOCK(MD_BLOCK_DOC, NULL);
+
+abort:
+
+#if 0
+ /* Output some memory consumption statistics. */
+ {
+ char buffer[256];
+ sprintf(buffer, "Alloced %u bytes for block buffer.",
+ (unsigned)(ctx->alloc_block_bytes));
+ MD_LOG(buffer);
+
+ sprintf(buffer, "Alloced %u bytes for containers buffer.",
+ (unsigned)(ctx->alloc_containers * sizeof(MD_CONTAINER)));
+ MD_LOG(buffer);
+
+ sprintf(buffer, "Alloced %u bytes for marks buffer.",
+ (unsigned)(ctx->alloc_marks * sizeof(MD_MARK)));
+ MD_LOG(buffer);
+
+ sprintf(buffer, "Alloced %u bytes for aux. buffer.",
+ (unsigned)(ctx->alloc_buffer * sizeof(MD_CHAR)));
+ MD_LOG(buffer);
+ }
+#endif
+
+ return ret;
+}
+
+
+/********************
+ *** Public API ***
+ ********************/
+
+int
+md_parse(const MD_CHAR* text, MD_SIZE size, const MD_PARSER* parser, void* userdata)
+{
+ MD_CTX ctx;
+ int i;
+ int ret;
+
+ if(parser->abi_version != 0) {
+ if(parser->debug_log != NULL)
+ parser->debug_log("Unsupported abi_version.", userdata);
+ return -1;
+ }
+
+ /* Setup context structure. */
+ memset(&ctx, 0, sizeof(MD_CTX));
+ ctx.text = text;
+ ctx.size = size;
+ memcpy(&ctx.parser, parser, sizeof(MD_PARSER));
+ ctx.userdata = userdata;
+ ctx.code_indent_offset = (ctx.parser.flags & MD_FLAG_NOINDENTEDCODEBLOCKS) ? (OFF)(-1) : 4;
+ md_build_mark_char_map(&ctx);
+ ctx.doc_ends_with_newline = (size > 0 && ISNEWLINE_(text[size-1]));
+
+ /* Reset all unresolved opener mark chains. */
+ for(i = 0; i < (int) SIZEOF_ARRAY(ctx.mark_chains); i++) {
+ ctx.mark_chains[i].head = -1;
+ ctx.mark_chains[i].tail = -1;
+ }
+ ctx.unresolved_link_head = -1;
+ ctx.unresolved_link_tail = -1;
+
+ /* All the work. */
+ ret = md_process_doc(&ctx);
+
+ /* Clean-up. */
+ md_free_ref_defs(&ctx);
+ md_free_ref_def_hashtable(&ctx);
+ free(ctx.buffer);
+ free(ctx.marks);
+ free(ctx.block_bytes);
+ free(ctx.containers);
+
+ return ret;
+}
diff --git a/src/third-party/md4c/md4c.h b/src/third-party/md4c/md4c.h
new file mode 100644
index 0000000..95f78f9
--- /dev/null
+++ b/src/third-party/md4c/md4c.h
@@ -0,0 +1,405 @@
+/*
+ * MD4C: Markdown parser for C
+ * (http://github.com/mity/md4c)
+ *
+ * Copyright (c) 2016-2020 Martin Mitas
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+ * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#ifndef MD4C_H
+#define MD4C_H
+
+#ifdef __cplusplus
+ extern "C" {
+#endif
+
+#if defined MD4C_USE_UTF16
+ /* Magic to support UTF-16. Note that in order to use it, you have to define
+ * the macro MD4C_USE_UTF16 both when building MD4C as well as when
+ * including this header in your code. */
+ #ifdef _WIN32
+ #include <windows.h>
+ typedef WCHAR MD_CHAR;
+ #else
+ #error MD4C_USE_UTF16 is only supported on Windows.
+ #endif
+#else
+ typedef char MD_CHAR;
+#endif
+
+typedef unsigned MD_SIZE;
+typedef unsigned MD_OFFSET;
+
+
+/* Block represents a part of document hierarchy structure like a paragraph
+ * or list item.
+ */
+typedef enum MD_BLOCKTYPE {
+ /* <body>...</body> */
+ MD_BLOCK_DOC = 0,
+
+ /* <blockquote>...</blockquote> */
+ MD_BLOCK_QUOTE,
+
+ /* <ul>...</ul>
+ * Detail: Structure MD_BLOCK_UL_DETAIL. */
+ MD_BLOCK_UL,
+
+ /* <ol>...</ol>
+ * Detail: Structure MD_BLOCK_OL_DETAIL. */
+ MD_BLOCK_OL,
+
+ /* <li>...</li>
+ * Detail: Structure MD_BLOCK_LI_DETAIL. */
+ MD_BLOCK_LI,
+
+ /* <hr> */
+ MD_BLOCK_HR,
+
+ /* <h1>...</h1> (for levels up to 6)
+ * Detail: Structure MD_BLOCK_H_DETAIL. */
+ MD_BLOCK_H,
+
+ /* <pre><code>...</code></pre>
+ * Note the text lines within code blocks are terminated with '\n'
+ * instead of explicit MD_TEXT_BR. */
+ MD_BLOCK_CODE,
+
+ /* Raw HTML block. This itself does not correspond to any particular HTML
+ * tag. The contents of it _is_ raw HTML source intended to be put
+ * in verbatim form to the HTML output. */
+ MD_BLOCK_HTML,
+
+ /* <p>...</p> */
+ MD_BLOCK_P,
+
+ /* <table>...</table> and its contents.
+ * Detail: Structure MD_BLOCK_TABLE_DETAIL (for MD_BLOCK_TABLE),
+ * structure MD_BLOCK_TD_DETAIL (for MD_BLOCK_TH and MD_BLOCK_TD)
+ * Note all of these are used only if extension MD_FLAG_TABLES is enabled. */
+ MD_BLOCK_TABLE,
+ MD_BLOCK_THEAD,
+ MD_BLOCK_TBODY,
+ MD_BLOCK_TR,
+ MD_BLOCK_TH,
+ MD_BLOCK_TD
+} MD_BLOCKTYPE;
+
+/* Span represents an in-line piece of a document which should be rendered with
+ * the same font, color and other attributes. A sequence of spans forms a block
+ * like paragraph or list item. */
+typedef enum MD_SPANTYPE {
+ /* <em>...</em> */
+ MD_SPAN_EM,
+
+ /* <strong>...</strong> */
+ MD_SPAN_STRONG,
+
+ /* <a href="xxx">...</a>
+ * Detail: Structure MD_SPAN_A_DETAIL. */
+ MD_SPAN_A,
+
+ /* <img src="xxx">...</a>
+ * Detail: Structure MD_SPAN_IMG_DETAIL.
+ * Note: Image text can contain nested spans and even nested images.
+ * If rendered into ALT attribute of HTML <IMG> tag, it's responsibility
+ * of the parser to deal with it.
+ */
+ MD_SPAN_IMG,
+
+ /* <code>...</code> */
+ MD_SPAN_CODE,
+
+ /* <del>...</del>
+ * Note: Recognized only when MD_FLAG_STRIKETHROUGH is enabled.
+ */
+ MD_SPAN_DEL,
+
+ /* For recognizing inline ($) and display ($$) equations
+ * Note: Recognized only when MD_FLAG_LATEXMATHSPANS is enabled.
+ */
+ MD_SPAN_LATEXMATH,
+ MD_SPAN_LATEXMATH_DISPLAY,
+
+ /* Wiki links
+ * Note: Recognized only when MD_FLAG_WIKILINKS is enabled.
+ */
+ MD_SPAN_WIKILINK,
+
+ /* <u>...</u>
+ * Note: Recognized only when MD_FLAG_UNDERLINE is enabled. */
+ MD_SPAN_U
+} MD_SPANTYPE;
+
+/* Text is the actual textual contents of span. */
+typedef enum MD_TEXTTYPE {
+ /* Normal text. */
+ MD_TEXT_NORMAL = 0,
+
+ /* NULL character. CommonMark requires replacing NULL character with
+ * the replacement char U+FFFD, so this allows caller to do that easily. */
+ MD_TEXT_NULLCHAR,
+
+ /* Line breaks.
+ * Note these are not sent from blocks with verbatim output (MD_BLOCK_CODE
+ * or MD_BLOCK_HTML). In such cases, '\n' is part of the text itself. */
+ MD_TEXT_BR, /* <br> (hard break) */
+ MD_TEXT_SOFTBR, /* '\n' in source text where it is not semantically meaningful (soft break) */
+
+ /* Entity.
+ * (a) Named entity, e.g. &nbsp;
+ * (Note MD4C does not have a list of known entities.
+ * Anything matching the regexp /&[A-Za-z][A-Za-z0-9]{1,47};/ is
+ * treated as a named entity.)
+ * (b) Numerical entity, e.g. &#1234;
+ * (c) Hexadecimal entity, e.g. &#x12AB;
+ *
+ * As MD4C is mostly encoding agnostic, application gets the verbatim
+ * entity text into the MD_PARSER::text_callback(). */
+ MD_TEXT_ENTITY,
+
+ /* Text in a code block (inside MD_BLOCK_CODE) or inlined code (`code`).
+ * If it is inside MD_BLOCK_CODE, it includes spaces for indentation and
+ * '\n' for new lines. MD_TEXT_BR and MD_TEXT_SOFTBR are not sent for this
+ * kind of text. */
+ MD_TEXT_CODE,
+
+ /* Text is a raw HTML. If it is contents of a raw HTML block (i.e. not
+ * an inline raw HTML), then MD_TEXT_BR and MD_TEXT_SOFTBR are not used.
+ * The text contains verbatim '\n' for the new lines. */
+ MD_TEXT_HTML,
+
+ /* Text is inside an equation. This is processed the same way as inlined code
+ * spans (`code`). */
+ MD_TEXT_LATEXMATH
+} MD_TEXTTYPE;
+
+
+/* Alignment enumeration. */
+typedef enum MD_ALIGN {
+ MD_ALIGN_DEFAULT = 0, /* When unspecified. */
+ MD_ALIGN_LEFT,
+ MD_ALIGN_CENTER,
+ MD_ALIGN_RIGHT
+} MD_ALIGN;
+
+
+/* String attribute.
+ *
+ * This wraps strings which are outside of a normal text flow and which are
+ * propagated within various detailed structures, but which still may contain
+ * string portions of different types like e.g. entities.
+ *
+ * So, for example, lets consider this image:
+ *
+ * ![image alt text](http://example.org/image.png 'foo &quot; bar')
+ *
+ * The image alt text is propagated as a normal text via the MD_PARSER::text()
+ * callback. However, the image title ('foo &quot; bar') is propagated as
+ * MD_ATTRIBUTE in MD_SPAN_IMG_DETAIL::title.
+ *
+ * Then the attribute MD_SPAN_IMG_DETAIL::title shall provide the following:
+ * -- [0]: "foo " (substr_types[0] == MD_TEXT_NORMAL; substr_offsets[0] == 0)
+ * -- [1]: "&quot;" (substr_types[1] == MD_TEXT_ENTITY; substr_offsets[1] == 4)
+ * -- [2]: " bar" (substr_types[2] == MD_TEXT_NORMAL; substr_offsets[2] == 10)
+ * -- [3]: (n/a) (n/a ; substr_offsets[3] == 14)
+ *
+ * Note that these invariants are always guaranteed:
+ * -- substr_offsets[0] == 0
+ * -- substr_offsets[LAST+1] == size
+ * -- Currently, only MD_TEXT_NORMAL, MD_TEXT_ENTITY, MD_TEXT_NULLCHAR
+ * substrings can appear. This could change only of the specification
+ * changes.
+ */
+typedef struct MD_ATTRIBUTE {
+ const MD_CHAR* text;
+ MD_SIZE size;
+ const MD_TEXTTYPE* substr_types;
+ const MD_OFFSET* substr_offsets;
+} MD_ATTRIBUTE;
+
+
+/* Detailed info for MD_BLOCK_UL. */
+typedef struct MD_BLOCK_UL_DETAIL {
+ int is_tight; /* Non-zero if tight list, zero if loose. */
+ MD_CHAR mark; /* Item bullet character in MarkDown source of the list, e.g. '-', '+', '*'. */
+} MD_BLOCK_UL_DETAIL;
+
+/* Detailed info for MD_BLOCK_OL. */
+typedef struct MD_BLOCK_OL_DETAIL {
+ unsigned start; /* Start index of the ordered list. */
+ int is_tight; /* Non-zero if tight list, zero if loose. */
+ MD_CHAR mark_delimiter; /* Character delimiting the item marks in MarkDown source, e.g. '.' or ')' */
+} MD_BLOCK_OL_DETAIL;
+
+/* Detailed info for MD_BLOCK_LI. */
+typedef struct MD_BLOCK_LI_DETAIL {
+ int is_task; /* Can be non-zero only with MD_FLAG_TASKLISTS */
+ MD_CHAR task_mark; /* If is_task, then one of 'x', 'X' or ' '. Undefined otherwise. */
+ MD_OFFSET task_mark_offset; /* If is_task, then offset in the input of the char between '[' and ']'. */
+} MD_BLOCK_LI_DETAIL;
+
+/* Detailed info for MD_BLOCK_H. */
+typedef struct MD_BLOCK_H_DETAIL {
+ unsigned level; /* Header level (1 - 6) */
+} MD_BLOCK_H_DETAIL;
+
+/* Detailed info for MD_BLOCK_CODE. */
+typedef struct MD_BLOCK_CODE_DETAIL {
+ MD_ATTRIBUTE info;
+ MD_ATTRIBUTE lang;
+ MD_CHAR fence_char; /* The character used for fenced code block; or zero for indented code block. */
+} MD_BLOCK_CODE_DETAIL;
+
+/* Detailed info for MD_BLOCK_TABLE. */
+typedef struct MD_BLOCK_TABLE_DETAIL {
+ unsigned col_count; /* Count of columns in the table. */
+ unsigned head_row_count; /* Count of rows in the table header (currently always 1) */
+ unsigned body_row_count; /* Count of rows in the table body */
+} MD_BLOCK_TABLE_DETAIL;
+
+/* Detailed info for MD_BLOCK_TH and MD_BLOCK_TD. */
+typedef struct MD_BLOCK_TD_DETAIL {
+ MD_ALIGN align;
+} MD_BLOCK_TD_DETAIL;
+
+/* Detailed info for MD_SPAN_A. */
+typedef struct MD_SPAN_A_DETAIL {
+ MD_ATTRIBUTE href;
+ MD_ATTRIBUTE title;
+} MD_SPAN_A_DETAIL;
+
+/* Detailed info for MD_SPAN_IMG. */
+typedef struct MD_SPAN_IMG_DETAIL {
+ MD_ATTRIBUTE src;
+ MD_ATTRIBUTE title;
+} MD_SPAN_IMG_DETAIL;
+
+/* Detailed info for MD_SPAN_WIKILINK. */
+typedef struct MD_SPAN_WIKILINK {
+ MD_ATTRIBUTE target;
+} MD_SPAN_WIKILINK_DETAIL;
+
+/* Flags specifying extensions/deviations from CommonMark specification.
+ *
+ * By default (when MD_PARSER::flags == 0), we follow CommonMark specification.
+ * The following flags may allow some extensions or deviations from it.
+ */
+#define MD_FLAG_COLLAPSEWHITESPACE 0x0001 /* In MD_TEXT_NORMAL, collapse non-trivial whitespace into single ' ' */
+#define MD_FLAG_PERMISSIVEATXHEADERS 0x0002 /* Do not require space in ATX headers ( ###header ) */
+#define MD_FLAG_PERMISSIVEURLAUTOLINKS 0x0004 /* Recognize URLs as autolinks even without '<', '>' */
+#define MD_FLAG_PERMISSIVEEMAILAUTOLINKS 0x0008 /* Recognize e-mails as autolinks even without '<', '>' and 'mailto:' */
+#define MD_FLAG_NOINDENTEDCODEBLOCKS 0x0010 /* Disable indented code blocks. (Only fenced code works.) */
+#define MD_FLAG_NOHTMLBLOCKS 0x0020 /* Disable raw HTML blocks. */
+#define MD_FLAG_NOHTMLSPANS 0x0040 /* Disable raw HTML (inline). */
+#define MD_FLAG_TABLES 0x0100 /* Enable tables extension. */
+#define MD_FLAG_STRIKETHROUGH 0x0200 /* Enable strikethrough extension. */
+#define MD_FLAG_PERMISSIVEWWWAUTOLINKS 0x0400 /* Enable WWW autolinks (even without any scheme prefix, if they begin with 'www.') */
+#define MD_FLAG_TASKLISTS 0x0800 /* Enable task list extension. */
+#define MD_FLAG_LATEXMATHSPANS 0x1000 /* Enable $ and $$ containing LaTeX equations. */
+#define MD_FLAG_WIKILINKS 0x2000 /* Enable wiki links extension. */
+#define MD_FLAG_UNDERLINE 0x4000 /* Enable underline extension (and disables '_' for normal emphasis). */
+
+#define MD_FLAG_PERMISSIVEAUTOLINKS (MD_FLAG_PERMISSIVEEMAILAUTOLINKS | MD_FLAG_PERMISSIVEURLAUTOLINKS | MD_FLAG_PERMISSIVEWWWAUTOLINKS)
+#define MD_FLAG_NOHTML (MD_FLAG_NOHTMLBLOCKS | MD_FLAG_NOHTMLSPANS)
+
+/* Convenient sets of flags corresponding to well-known Markdown dialects.
+ *
+ * Note we may only support subset of features of the referred dialect.
+ * The constant just enables those extensions which bring us as close as
+ * possible given what features we implement.
+ *
+ * ABI compatibility note: Meaning of these can change in time as new
+ * extensions, bringing the dialect closer to the original, are implemented.
+ */
+#define MD_DIALECT_COMMONMARK 0
+#define MD_DIALECT_GITHUB (MD_FLAG_PERMISSIVEAUTOLINKS | MD_FLAG_TABLES | MD_FLAG_STRIKETHROUGH | MD_FLAG_TASKLISTS)
+
+/* Parser structure.
+ */
+typedef struct MD_PARSER {
+ /* Reserved. Set to zero.
+ */
+ unsigned abi_version;
+
+ /* Dialect options. Bitmask of MD_FLAG_xxxx values.
+ */
+ unsigned flags;
+
+ /* Caller-provided rendering callbacks.
+ *
+ * For some block/span types, more detailed information is provided in a
+ * type-specific structure pointed by the argument 'detail'.
+ *
+ * The last argument of all callbacks, 'userdata', is just propagated from
+ * md_parse() and is available for any use by the application.
+ *
+ * Note any strings provided to the callbacks as their arguments or as
+ * members of any detail structure are generally not zero-terminated.
+ * Application has to take the respective size information into account.
+ *
+ * Any rendering callback may abort further parsing of the document by
+ * returning non-zero.
+ */
+ int (*enter_block)(MD_BLOCKTYPE /*type*/, void* /*detail*/, void* /*userdata*/);
+ int (*leave_block)(MD_BLOCKTYPE /*type*/, void* /*detail*/, void* /*userdata*/);
+
+ int (*enter_span)(MD_SPANTYPE /*type*/, void* /*detail*/, void* /*userdata*/);
+ int (*leave_span)(MD_SPANTYPE /*type*/, void* /*detail*/, void* /*userdata*/);
+
+ int (*text)(MD_TEXTTYPE /*type*/, const MD_CHAR* /*text*/, MD_SIZE /*size*/, void* /*userdata*/);
+
+ /* Debug callback. Optional (may be NULL).
+ *
+ * If provided and something goes wrong, this function gets called.
+ * This is intended for debugging and problem diagnosis for developers;
+ * it is not intended to provide any errors suitable for displaying to an
+ * end user.
+ */
+ void (*debug_log)(const char* /*msg*/, void* /*userdata*/);
+
+ /* Reserved. Set to NULL.
+ */
+ void (*syntax)(void);
+} MD_PARSER;
+
+
+/* For backward compatibility. Do not use in new code.
+ */
+typedef MD_PARSER MD_RENDERER;
+
+
+/* Parse the Markdown document stored in the string 'text' of size 'size'.
+ * The parser provides callbacks to be called during the parsing so the
+ * caller can render the document on the screen or convert the Markdown
+ * to another format.
+ *
+ * Zero is returned on success. If a runtime error occurs (e.g. a memory
+ * fails), -1 is returned. If the processing is aborted due any callback
+ * returning non-zero, the return value of the callback is returned.
+ */
+int md_parse(const MD_CHAR* text, MD_SIZE size, const MD_PARSER* parser, void* userdata);
+
+
+#ifdef __cplusplus
+ } /* extern "C" { */
+#endif
+
+#endif /* MD4C_H */
diff --git a/src/third-party/rapidyaml/ryml_all.hpp b/src/third-party/rapidyaml/ryml_all.hpp
new file mode 100644
index 0000000..27ed7a6
--- /dev/null
+++ b/src/third-party/rapidyaml/ryml_all.hpp
@@ -0,0 +1,30945 @@
+#ifndef _RYML_SINGLE_HEADER_AMALGAMATED_HPP_
+//
+// Rapid YAML - a library to parse and emit YAML, and do it fast.
+//
+// https://github.com/biojppm/rapidyaml
+//
+// DO NOT EDIT. This file is generated automatically.
+// This is an amalgamated single-header version of the library.
+//
+// INSTRUCTIONS:
+// - Include at will in any header of your project
+// - In one (and only one) of your project source files,
+// #define RYML_SINGLE_HDR_DEFINE_NOW and then include this header.
+// This will enable the function and class definitions in
+// the header file.
+// - To compile into a shared library, just define the
+// preprocessor symbol RYML_SHARED . This will take
+// care of symbol export/import.
+//
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// LICENSE.txt
+// https://github.com/biojppm/rapidyaml/LICENSE.txt
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+// Copyright (c) 2018, Joao Paulo Magalhaes <dev@jpmag.me>
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+//
+
+ // shared library: export when defining
+#if defined(RYML_SHARED) && defined(RYML_SINGLE_HDR_DEFINE_NOW) && !defined(RYML_EXPORTS)
+#define RYML_EXPORTS
+#endif
+
+
+ // propagate defines to c4core
+#if defined(RYML_SINGLE_HDR_DEFINE_NOW) && !defined(C4CORE_SINGLE_HDR_DEFINE_NOW)
+#define C4CORE_SINGLE_HDR_DEFINE_NOW
+#endif
+
+#if defined(RYML_EXPORTS) && !defined(C4CORE_EXPORTS)
+#define C4CORE_EXPORTS
+#endif
+
+#if defined(RYML_SHARED) && !defined(C4CORE_SHARED)
+#define C4CORE_SHARED
+#endif
+
+// workaround for include removal while amalgamating
+// resulting in <stdarg.h> missing in arm-none-eabi-g++
+// https://github.com/biojppm/rapidyaml/issues/193
+#include <stdarg.h>
+
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/c4core_all.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/c4core_all.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4CORE_SINGLE_HEADER_AMALGAMATED_HPP_
+//
+// c4core - C++ utilities
+//
+// https://github.com/biojppm/c4core
+//
+// DO NOT EDIT. This file is generated automatically.
+// This is an amalgamated single-header version of the library.
+//
+// INSTRUCTIONS:
+// - Include at will in any header of your project
+// - In one (and only one) of your project source files,
+// #define C4CORE_SINGLE_HDR_DEFINE_NOW and then include this header.
+// This will enable the function and class definitions in
+// the header file.
+// - To compile into a shared library, just define the
+// preprocessor symbol C4CORE_SHARED . This will take
+// care of symbol export/import.
+//
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// LICENSE.txt
+// https://github.com/biojppm/c4core/LICENSE.txt
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+// Copyright (c) 2018, Joao Paulo Magalhaes <dev@jpmag.me>
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+//
+
+// shared library: export when defining
+#if defined(C4CORE_SHARED) && defined(C4CORE_SINGLE_HDR_DEFINE_NOW) && !defined(C4CORE_EXPORTS)
+#define C4CORE_EXPORTS
+#endif
+
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/export.hpp
+// https://github.com/biojppm/c4core/src/c4/export.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef C4_EXPORT_HPP_
+#define C4_EXPORT_HPP_
+
+#ifdef _WIN32
+ #ifdef C4CORE_SHARED
+ #ifdef C4CORE_EXPORTS
+ #define C4CORE_EXPORT __declspec(dllexport)
+ #else
+ #define C4CORE_EXPORT __declspec(dllimport)
+ #endif
+ #else
+ #define C4CORE_EXPORT
+ #endif
+#else
+ #define C4CORE_EXPORT
+#endif
+
+#endif /* C4CORE_EXPORT_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/export.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/preprocessor.hpp
+// https://github.com/biojppm/c4core/src/c4/preprocessor.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_PREPROCESSOR_HPP_
+#define _C4_PREPROCESSOR_HPP_
+
+/** @file preprocessor.hpp Contains basic macros and preprocessor utilities.
+ * @ingroup basic_headers */
+
+#ifdef __clang__
+ /* NOTE: using , ## __VA_ARGS__ to deal with zero-args calls to
+ * variadic macros is not portable, but works in clang, gcc, msvc, icc.
+ * clang requires switching off compiler warnings for pedantic mode.
+ * @see http://stackoverflow.com/questions/32047685/variadic-macro-without-arguments */
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" // warning: token pasting of ',' and __VA_ARGS__ is a GNU extension
+#elif defined(__GNUC__)
+ /* GCC also issues a warning for zero-args calls to variadic macros.
+ * This warning is switched on with -pedantic and apparently there is no
+ * easy way to turn it off as with clang. But marking this as a system
+ * header works.
+ * @see https://gcc.gnu.org/onlinedocs/cpp/System-Headers.html
+ * @see http://stackoverflow.com/questions/35587137/ */
+# pragma GCC system_header
+#endif
+
+#define C4_WIDEN(str) L"" str
+
+#define C4_COUNTOF(arr) (sizeof(arr)/sizeof((arr)[0]))
+
+#define C4_EXPAND(arg) arg
+
+/** useful in some macro calls with template arguments */
+#define C4_COMMA ,
+/** useful in some macro calls with template arguments
+ * @see C4_COMMA */
+#define C4_COMMA_X C4_COMMA
+
+/** expand and quote */
+#define C4_XQUOTE(arg) _C4_XQUOTE(arg)
+#define _C4_XQUOTE(arg) C4_QUOTE(arg)
+#define C4_QUOTE(arg) #arg
+
+/** expand and concatenate */
+#define C4_XCAT(arg1, arg2) _C4_XCAT(arg1, arg2)
+#define _C4_XCAT(arg1, arg2) C4_CAT(arg1, arg2)
+#define C4_CAT(arg1, arg2) arg1##arg2
+
+#define C4_VERSION_CAT(major, minor, patch) ((major)*10000 + (minor)*100 + (patch))
+
+/** A preprocessor foreach. Spectacular trick taken from:
+ * http://stackoverflow.com/a/1872506/5875572
+ * The first argument is for a macro receiving a single argument,
+ * which will be called with every subsequent argument. There is
+ * currently a limit of 32 arguments, and at least 1 must be provided.
+ *
+Example:
+@code{.cpp}
+struct Example {
+ int a;
+ int b;
+ int c;
+};
+// define a one-arg macro to be called
+#define PRN_STRUCT_OFFSETS(field) PRN_STRUCT_OFFSETS_(Example, field)
+#define PRN_STRUCT_OFFSETS_(structure, field) printf(C4_XQUOTE(structure) ":" C4_XQUOTE(field)" - offset=%zu\n", offsetof(structure, field));
+
+// now call the macro for a, b and c
+C4_FOR_EACH(PRN_STRUCT_OFFSETS, a, b, c);
+@endcode */
+#define C4_FOR_EACH(what, ...) C4_FOR_EACH_SEP(what, ;, __VA_ARGS__)
+
+/** same as C4_FOR_EACH(), but use a custom separator between statements.
+ * If a comma is needed as the separator, use the C4_COMMA macro.
+ * @see C4_FOR_EACH
+ * @see C4_COMMA
+ */
+#define C4_FOR_EACH_SEP(what, sep, ...) _C4_FOR_EACH_(_C4_FOR_EACH_NARG(__VA_ARGS__), what, sep, __VA_ARGS__)
+
+/// @cond dev
+
+#define _C4_FOR_EACH_01(what, sep, x) what(x) sep
+#define _C4_FOR_EACH_02(what, sep, x, ...) what(x) sep _C4_FOR_EACH_01(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_03(what, sep, x, ...) what(x) sep _C4_FOR_EACH_02(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_04(what, sep, x, ...) what(x) sep _C4_FOR_EACH_03(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_05(what, sep, x, ...) what(x) sep _C4_FOR_EACH_04(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_06(what, sep, x, ...) what(x) sep _C4_FOR_EACH_05(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_07(what, sep, x, ...) what(x) sep _C4_FOR_EACH_06(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_08(what, sep, x, ...) what(x) sep _C4_FOR_EACH_07(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_09(what, sep, x, ...) what(x) sep _C4_FOR_EACH_08(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_10(what, sep, x, ...) what(x) sep _C4_FOR_EACH_09(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_11(what, sep, x, ...) what(x) sep _C4_FOR_EACH_10(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_12(what, sep, x, ...) what(x) sep _C4_FOR_EACH_11(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_13(what, sep, x, ...) what(x) sep _C4_FOR_EACH_12(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_14(what, sep, x, ...) what(x) sep _C4_FOR_EACH_13(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_15(what, sep, x, ...) what(x) sep _C4_FOR_EACH_14(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_16(what, sep, x, ...) what(x) sep _C4_FOR_EACH_15(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_17(what, sep, x, ...) what(x) sep _C4_FOR_EACH_16(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_18(what, sep, x, ...) what(x) sep _C4_FOR_EACH_17(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_19(what, sep, x, ...) what(x) sep _C4_FOR_EACH_18(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_20(what, sep, x, ...) what(x) sep _C4_FOR_EACH_19(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_21(what, sep, x, ...) what(x) sep _C4_FOR_EACH_20(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_22(what, sep, x, ...) what(x) sep _C4_FOR_EACH_21(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_23(what, sep, x, ...) what(x) sep _C4_FOR_EACH_22(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_24(what, sep, x, ...) what(x) sep _C4_FOR_EACH_23(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_25(what, sep, x, ...) what(x) sep _C4_FOR_EACH_24(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_26(what, sep, x, ...) what(x) sep _C4_FOR_EACH_25(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_27(what, sep, x, ...) what(x) sep _C4_FOR_EACH_26(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_28(what, sep, x, ...) what(x) sep _C4_FOR_EACH_27(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_29(what, sep, x, ...) what(x) sep _C4_FOR_EACH_28(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_30(what, sep, x, ...) what(x) sep _C4_FOR_EACH_29(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_31(what, sep, x, ...) what(x) sep _C4_FOR_EACH_30(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_32(what, sep, x, ...) what(x) sep _C4_FOR_EACH_31(what, sep, __VA_ARGS__)
+#define _C4_FOR_EACH_NARG(...) _C4_FOR_EACH_NARG_(__VA_ARGS__, _C4_FOR_EACH_RSEQ_N())
+#define _C4_FOR_EACH_NARG_(...) _C4_FOR_EACH_ARG_N(__VA_ARGS__)
+#define _C4_FOR_EACH_ARG_N(_01, _02, _03, _04, _05, _06, _07, _08, _09, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, _20, _21, _22, _23, _24, _25, _26, _27, _28, _29, _30, _31, _32, N, ...) N
+#define _C4_FOR_EACH_RSEQ_N() 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 09, 08, 07, 06, 05, 04, 03, 02, 01
+#define _C4_FOR_EACH_(N, what, sep, ...) C4_XCAT(_C4_FOR_EACH_, N)(what, sep, __VA_ARGS__)
+
+/// @endcond
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
+
+#endif /* _C4_PREPROCESSOR_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/preprocessor.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/platform.hpp
+// https://github.com/biojppm/c4core/src/c4/platform.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_PLATFORM_HPP_
+#define _C4_PLATFORM_HPP_
+
+/** @file platform.hpp Provides platform information macros
+ * @ingroup basic_headers */
+
+// see also https://sourceforge.net/p/predef/wiki/OperatingSystems/
+
+#if defined(_WIN64)
+# define C4_WIN
+# define C4_WIN64
+#elif defined(_WIN32)
+# define C4_WIN
+# define C4_WIN32
+#elif defined(__ANDROID__)
+# define C4_ANDROID
+#elif defined(__APPLE__)
+# include "TargetConditionals.h"
+# if TARGET_OS_IPHONE || TARGET_IPHONE_SIMULATOR
+# define C4_IOS
+# elif TARGET_OS_MAC || TARGET_OS_OSX
+# define C4_MACOS
+# else
+# error "Unknown Apple platform"
+# endif
+#elif defined(__linux)
+# define C4_UNIX
+# define C4_LINUX
+#elif defined(__unix)
+# define C4_UNIX
+#elif defined(__arm__) || defined(__aarch64__)
+# define C4_ARM
+#elif defined(SWIG)
+# define C4_SWIG
+#else
+# error "unknown platform"
+#endif
+
+#if defined(__posix) || defined(__unix__) || defined(__linux)
+# define C4_POSIX
+#endif
+
+
+#endif /* _C4_PLATFORM_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/platform.hpp)
+
+
+#if 0
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/cpu.hpp
+// https://github.com/biojppm/c4core/src/c4/cpu.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_CPU_HPP_
+#define _C4_CPU_HPP_
+
+/** @file cpu.hpp Provides processor information macros
+ * @ingroup basic_headers */
+
+// see also https://sourceforge.net/p/predef/wiki/Architectures/
+// see also https://sourceforge.net/p/predef/wiki/Endianness/
+// see also https://github.com/googlesamples/android-ndk/blob/android-mk/hello-jni/jni/hello-jni.c
+// see http://code.qt.io/cgit/qt/qtbase.git/tree/src/corelib/global/qprocessordetection.h
+
+#ifdef __ORDER_LITTLE_ENDIAN__
+ #define _C4EL __ORDER_LITTLE_ENDIAN__
+#else
+ #define _C4EL 1234
+#endif
+
+#ifdef __ORDER_BIG_ENDIAN__
+ #define _C4EB __ORDER_BIG_ENDIAN__
+#else
+ #define _C4EB 4321
+#endif
+
+// mixed byte order (eg, PowerPC or ia64)
+#define _C4EM 1111
+
+#if defined(__x86_64) || defined(__x86_64__) || defined(__amd64) || defined(_M_X64)
+ #define C4_CPU_X86_64
+ #define C4_WORDSIZE 8
+ #define C4_BYTE_ORDER _C4EL
+
+#elif defined(__i386) || defined(__i386__) || defined(_M_IX86)
+ #define C4_CPU_X86
+ #define C4_WORDSIZE 4
+ #define C4_BYTE_ORDER _C4EL
+
+#elif defined(__arm__) || defined(_M_ARM) \
+ || defined(__TARGET_ARCH_ARM) || defined(__aarch64__) || defined(_M_ARM64)
+ #if defined(__aarch64__) || defined(_M_ARM64)
+ #define C4_CPU_ARM64
+ #define C4_CPU_ARMV8
+ #define C4_WORDSIZE 8
+ #else
+ #define C4_CPU_ARM
+ #define C4_WORDSIZE 4
+ #if defined(__ARM_ARCH_8__) || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 8)
+ #define C4_CPU_ARMV8
+ #elif defined(__ARM_ARCH_7__) || defined(_ARM_ARCH_7) \
+ || defined(__ARM_ARCH_7A__) || defined(__ARM_ARCH_7R__) \
+ || defined(__ARM_ARCH_7M__) || defined(__ARM_ARCH_7S__) \
+ || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 7) \
+ || (defined(_M_ARM) && _M_ARM >= 7)
+ #define C4_CPU_ARMV7
+ #elif defined(__ARM_ARCH_6__) || defined(__ARM_ARCH_6J__) \
+ || defined(__ARM_ARCH_6T2__) || defined(__ARM_ARCH_6Z__) \
+ || defined(__ARM_ARCH_6K__) || defined(__ARM_ARCH_6ZK__) \
+ || defined(__ARM_ARCH_6M__) \
+ || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 6)
+ #define C4_CPU_ARMV6
+ #elif defined(__ARM_ARCH_5TEJ__) \
+ || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 5)
+ #define C4_CPU_ARMV5
+ #elif defined(__ARM_ARCH_4T__) \
+ || (defined(__TARGET_ARCH_ARM) && __TARGET_ARCH_ARM >= 4)
+ #define C4_CPU_ARMV4
+ #else
+ #error "unknown CPU architecture: ARM"
+ #endif
+ #endif
+ #if defined(__ARMEL__) || defined(__LITTLE_ENDIAN__) || defined(__AARCH64EL__) \
+ || (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__))
+ #define C4_BYTE_ORDER _C4EL
+ #elif defined(__ARMEB__) || defined(__BIG_ENDIAN__) || defined(__AARCH64EB__) \
+ || (defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__))
+ #define C4_BYTE_ORDER _C4EB
+ #elif defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_PDP_ENDIAN__)
+ #define C4_BYTE_ORDER _C4EM
+ #else
+ #error "unknown endianness"
+ #endif
+
+#elif defined(__ia64) || defined(__ia64__) || defined(_M_IA64)
+ #define C4_CPU_IA64
+ #define C4_WORDSIZE 8
+ #define C4_BYTE_ORDER _C4EM
+ // itanium is bi-endian - check byte order below
+
+#elif defined(__ppc__) || defined(__ppc) || defined(__powerpc__) \
+ || defined(_ARCH_COM) || defined(_ARCH_PWR) || defined(_ARCH_PPC) \
+ || defined(_M_MPPC) || defined(_M_PPC)
+ #if defined(__ppc64__) || defined(__powerpc64__) || defined(__64BIT__)
+ #define C4_CPU_PPC64
+ #define C4_WORDSIZE 8
+ #else
+ #define C4_CPU_PPC
+ #define C4_WORDSIZE 4
+ #endif
+ #define C4_BYTE_ORDER _C4EM
+ // ppc is bi-endian - check byte order below
+
+#elif defined(__s390x__) || defined(__zarch__) || defined(__SYSC_ZARCH_)
+# define C4_CPU_S390_X
+# define C4_WORDSIZE 8
+# define C4_BYTE_ORDER _C4EB
+
+#elif defined(__riscv)
+ #if __riscv_xlen == 64
+ #define C4_CPU_RISCV64
+ #define C4_WORDSIZE 8
+ #else
+ #define C4_CPU_RISCV32
+ #define C4_WORDSIZE 4
+ #endif
+ #define C4_BYTE_ORDER _C4EL
+
+#elif defined(__EMSCRIPTEN__)
+# define C4_BYTE_ORDER _C4EL
+# define C4_WORDSIZE 4
+
+#elif defined(SWIG)
+ #error "please define CPU architecture macros when compiling with swig"
+
+#else
+ #error "unknown CPU architecture"
+#endif
+
+#define C4_LITTLE_ENDIAN (C4_BYTE_ORDER == _C4EL)
+#define C4_BIG_ENDIAN (C4_BYTE_ORDER == _C4EB)
+#define C4_MIXED_ENDIAN (C4_BYTE_ORDER == _C4EM)
+
+#endif /* _C4_CPU_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/cpu.hpp)
+#endif
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/compiler.hpp
+// https://github.com/biojppm/c4core/src/c4/compiler.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_COMPILER_HPP_
+#define _C4_COMPILER_HPP_
+
+/** @file compiler.hpp Provides compiler information macros
+ * @ingroup basic_headers */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/platform.hpp
+//#include "c4/platform.hpp"
+#if !defined(C4_PLATFORM_HPP_) && !defined(_C4_PLATFORM_HPP_)
+#error "amalgamate: file c4/platform.hpp must have been included at this point"
+#endif /* C4_PLATFORM_HPP_ */
+
+
+// Compilers:
+// C4_MSVC
+// Visual Studio 2022: MSVC++ 17, 1930
+// Visual Studio 2019: MSVC++ 16, 1920
+// Visual Studio 2017: MSVC++ 15
+// Visual Studio 2015: MSVC++ 14
+// Visual Studio 2013: MSVC++ 13
+// Visual Studio 2013: MSVC++ 12
+// Visual Studio 2012: MSVC++ 11
+// Visual Studio 2010: MSVC++ 10
+// Visual Studio 2008: MSVC++ 09
+// Visual Studio 2005: MSVC++ 08
+// C4_CLANG
+// C4_GCC
+// C4_ICC (intel compiler)
+/** @see http://sourceforge.net/p/predef/wiki/Compilers/ for a list of compiler identifier macros */
+/** @see https://msdn.microsoft.com/en-us/library/b0084kay.aspx for VS2013 predefined macros */
+
+#if defined(_MSC_VER)// && (defined(C4_WIN) || defined(C4_XBOX) || defined(C4_UE4))
+# define C4_MSVC
+# define C4_MSVC_VERSION_2022 17
+# define C4_MSVC_VERSION_2019 16
+# define C4_MSVC_VERSION_2017 15
+# define C4_MSVC_VERSION_2015 14
+# define C4_MSVC_VERSION_2013 12
+# define C4_MSVC_VERSION_2012 11
+# if _MSC_VER >= 1930
+# define C4_MSVC_VERSION C4_MSVC_VERSION_2022 // visual studio 2022
+# define C4_MSVC_2022
+# elif _MSC_VER >= 1920
+# define C4_MSVC_VERSION C_4MSVC_VERSION_2019 // visual studio 2019
+# define C4_MSVC_2019
+# elif _MSC_VER >= 1910
+# define C4_MSVC_VERSION C4_MSVC_VERSION_2017 // visual studio 2017
+# define C4_MSVC_2017
+# elif _MSC_VER == 1900
+# define C4_MSVC_VERSION C4_MSVC_VERSION_2015 // visual studio 2015
+# define C4_MSVC_2015
+# elif _MSC_VER == 1800
+# error "MSVC version not supported"
+# define C4_MSVC_VERSION C4_MSVC_VERSION_2013 // visual studio 2013
+# define C4_MSVC_2013
+# elif _MSC_VER == 1700
+# error "MSVC version not supported"
+# define C4_MSVC_VERSION C4_MSVC_VERSION_2012 // visual studio 2012
+# define C4_MSVC_2012
+# elif _MSC_VER == 1600
+# error "MSVC version not supported"
+# define C4_MSVC_VERSION 10 // visual studio 2010
+# define C4_MSVC_2010
+# elif _MSC_VER == 1500
+# error "MSVC version not supported"
+# define C4_MSVC_VERSION 09 // visual studio 2008
+# define C4_MSVC_2008
+# elif _MSC_VER == 1400
+# error "MSVC version not supported"
+# define C4_MSVC_VERSION 08 // visual studio 2005
+# define C4_MSVC_2005
+# else
+# error "MSVC version not supported"
+# endif // _MSC_VER
+#else
+# define C4_MSVC_VERSION 0 // visual studio not present
+# define C4_GCC_LIKE
+# ifdef __INTEL_COMPILER // check ICC before checking GCC, as ICC defines __GNUC__ too
+# define C4_ICC
+# define C4_ICC_VERSION __INTEL_COMPILER
+# elif defined(__APPLE_CC__)
+# define C4_XCODE
+# if defined(__clang__)
+# define C4_CLANG
+# ifndef __apple_build_version__
+# define C4_CLANG_VERSION C4_VERSION_ENCODED(__clang_major__, __clang_minor__, __clang_patchlevel__)
+# else
+# define C4_CLANG_VERSION __apple_build_version__
+# endif
+# else
+# define C4_XCODE_VERSION __APPLE_CC__
+# endif
+# elif defined(__clang__)
+# define C4_CLANG
+# ifndef __apple_build_version__
+# define C4_CLANG_VERSION C4_VERSION_ENCODED(__clang_major__, __clang_minor__, __clang_patchlevel__)
+# else
+# define C4_CLANG_VERSION __apple_build_version__
+# endif
+# elif defined(__GNUC__)
+# define C4_GCC
+# if defined(__GNUC_PATCHLEVEL__)
+# define C4_GCC_VERSION C4_VERSION_ENCODED(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+# else
+# define C4_GCC_VERSION C4_VERSION_ENCODED(__GNUC__, __GNUC_MINOR__, 0)
+# endif
+# if __GNUC__ < 5
+# if __GNUC__ == 4 && __GNUC_MINOR__ >= 8
+// provided by cmake sub-project
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/gcc-4.8.hpp
+//# include "c4/gcc-4.8.hpp"
+#if !defined(C4_GCC-4_8_HPP_) && !defined(_C4_GCC-4_8_HPP_)
+#error "amalgamate: file c4/gcc-4.8.hpp must have been included at this point"
+#endif /* C4_GCC-4_8_HPP_ */
+
+# else
+// we do not support GCC < 4.8:
+// * misses std::is_trivially_copyable
+// * misses std::align
+// * -Wshadow has false positives when a local function parameter has the same name as a method
+# error "GCC < 4.8 is not supported"
+# endif
+# endif
+# endif
+#endif // defined(C4_WIN) && defined(_MSC_VER)
+
+#endif /* _C4_COMPILER_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/compiler.hpp)
+
+// these includes are needed to work around conditional
+// includes in the gcc4.8 shim
+#include <cstdint>
+#include <type_traits>
+#include <cstring>
+
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// cmake/compat/c4/gcc-4.8.hpp
+// https://github.com/biojppm/c4core/cmake/compat/c4/gcc-4.8.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_COMPAT_GCC_4_8_HPP_
+#define _C4_COMPAT_GCC_4_8_HPP_
+
+#if __GNUC__ == 4 && __GNUC_MINOR__ >= 8
+/* STL polyfills for old GNU compilers */
+
+_Pragma("GCC diagnostic ignored \"-Wshadow\"")
+_Pragma("GCC diagnostic ignored \"-Wmissing-field-initializers\"")
+
+#if __cplusplus
+//included above:
+//#include <cstdint>
+//included above:
+//#include <type_traits>
+
+namespace std {
+
+template<typename _Tp>
+struct is_trivially_copyable : public integral_constant<bool,
+ is_destructible<_Tp>::value && __has_trivial_destructor(_Tp) &&
+ (__has_trivial_constructor(_Tp) || __has_trivial_copy(_Tp) || __has_trivial_assign(_Tp))>
+{ };
+
+template<typename _Tp>
+using is_trivially_copy_constructible = has_trivial_copy_constructor<_Tp>;
+
+template<typename _Tp>
+using is_trivially_default_constructible = has_trivial_default_constructor<_Tp>;
+
+template<typename _Tp>
+using is_trivially_copy_assignable = has_trivial_copy_assign<_Tp>;
+
+/* not supported */
+template<typename _Tp>
+struct is_trivially_move_constructible : false_type
+{ };
+
+/* not supported */
+template<typename _Tp>
+struct is_trivially_move_assignable : false_type
+{ };
+
+inline void *align(size_t __align, size_t __size, void*& __ptr, size_t& __space) noexcept
+{
+ if (__space < __size)
+ return nullptr;
+ const auto __intptr = reinterpret_cast<uintptr_t>(__ptr);
+ const auto __aligned = (__intptr - 1u + __align) & -__align;
+ const auto __diff = __aligned - __intptr;
+ if (__diff > (__space - __size))
+ return nullptr;
+ else
+ {
+ __space -= __diff;
+ return __ptr = reinterpret_cast<void*>(__aligned);
+ }
+}
+typedef long double max_align_t ;
+
+}
+#else // __cplusplus
+
+//included above:
+//#include <string.h>
+// see https://sourceware.org/bugzilla/show_bug.cgi?id=25399 (ubuntu gcc-4.8)
+#define memset(s, c, count) __builtin_memset(s, c, count)
+
+#endif // __cplusplus
+
+#endif // __GNUC__ == 4 && __GNUC_MINOR__ >= 8
+
+#endif // _C4_COMPAT_GCC_4_8_HPP_
+
+
+// (end https://github.com/biojppm/c4core/cmake/compat/c4/gcc-4.8.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/language.hpp
+// https://github.com/biojppm/c4core/src/c4/language.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_LANGUAGE_HPP_
+#define _C4_LANGUAGE_HPP_
+
+/** @file language.hpp Provides language standard information macros and
+ * compiler agnostic utility macros: namespace facilities, function attributes,
+ * variable attributes, etc.
+ * @ingroup basic_headers */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/preprocessor.hpp
+//#include "c4/preprocessor.hpp"
+#if !defined(C4_PREPROCESSOR_HPP_) && !defined(_C4_PREPROCESSOR_HPP_)
+#error "amalgamate: file c4/preprocessor.hpp must have been included at this point"
+#endif /* C4_PREPROCESSOR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/compiler.hpp
+//#include "c4/compiler.hpp"
+#if !defined(C4_COMPILER_HPP_) && !defined(_C4_COMPILER_HPP_)
+#error "amalgamate: file c4/compiler.hpp must have been included at this point"
+#endif /* C4_COMPILER_HPP_ */
+
+
+/* Detect C++ standard.
+ * @see http://stackoverflow.com/a/7132549/5875572 */
+#ifndef C4_CPP
+# ifdef _MSC_VER
+# if _MSC_VER >= 1910 // >VS2015: VS2017, VS2019
+# if (!defined(_MSVC_LANG))
+# error _MSVC not defined
+# endif
+# if _MSVC_LANG >= 201705L
+# define C4_CPP 20
+# define C4_CPP20
+# elif _MSVC_LANG == 201703L
+# define C4_CPP 17
+# define C4_CPP17
+# elif _MSVC_LANG >= 201402L
+# define C4_CPP 14
+# define C4_CPP14
+# elif _MSVC_LANG >= 201103L
+# define C4_CPP 11
+# define C4_CPP11
+# else
+# error C++ lesser than C++11 not supported
+# endif
+# else
+# if _MSC_VER == 1900
+# define C4_CPP 14 // VS2015 is c++14 https://devblogs.microsoft.com/cppblog/c111417-features-in-vs-2015-rtm/
+# define C4_CPP14
+# elif _MSC_VER == 1800 // VS2013
+# define C4_CPP 11
+# define C4_CPP11
+# else
+# error C++ lesser than C++11 not supported
+# endif
+# endif
+# elif defined(__INTEL_COMPILER) // https://software.intel.com/en-us/node/524490
+# ifdef __INTEL_CXX20_MODE__ // not sure about this
+# define C4_CPP 20
+# define C4_CPP20
+# elif defined __INTEL_CXX17_MODE__ // not sure about this
+# define C4_CPP 17
+# define C4_CPP17
+# elif defined __INTEL_CXX14_MODE__ // not sure about this
+# define C4_CPP 14
+# define C4_CPP14
+# elif defined __INTEL_CXX11_MODE__
+# define C4_CPP 11
+# define C4_CPP11
+# else
+# error C++ lesser than C++11 not supported
+# endif
+# else
+# ifndef __cplusplus
+# error __cplusplus is not defined?
+# endif
+# if __cplusplus == 1
+# error cannot handle __cplusplus==1
+# elif __cplusplus >= 201709L
+# define C4_CPP 20
+# define C4_CPP20
+# elif __cplusplus >= 201703L
+# define C4_CPP 17
+# define C4_CPP17
+# elif __cplusplus >= 201402L
+# define C4_CPP 14
+# define C4_CPP14
+# elif __cplusplus >= 201103L
+# define C4_CPP 11
+# define C4_CPP11
+# elif __cplusplus >= 199711L
+# error C++ lesser than C++11 not supported
+# endif
+# endif
+#else
+# ifdef C4_CPP == 20
+# define C4_CPP20
+# elif C4_CPP == 17
+# define C4_CPP17
+# elif C4_CPP == 14
+# define C4_CPP14
+# elif C4_CPP == 11
+# define C4_CPP11
+# elif C4_CPP == 98
+# define C4_CPP98
+# error C++ lesser than C++11 not supported
+# else
+# error C4_CPP must be one of 20, 17, 14, 11, 98
+# endif
+#endif
+
+#ifdef C4_CPP20
+# define C4_CPP17
+# define C4_CPP14
+# define C4_CPP11
+#elif defined(C4_CPP17)
+# define C4_CPP14
+# define C4_CPP11
+#elif defined(C4_CPP14)
+# define C4_CPP11
+#endif
+
+/** lifted from this answer: http://stackoverflow.com/a/20170989/5875572 */
+#ifndef _MSC_VER
+# if __cplusplus < 201103
+# define C4_CONSTEXPR11
+# define C4_CONSTEXPR14
+//# define C4_NOEXCEPT
+# elif __cplusplus == 201103
+# define C4_CONSTEXPR11 constexpr
+# define C4_CONSTEXPR14
+//# define C4_NOEXCEPT noexcept
+# else
+# define C4_CONSTEXPR11 constexpr
+# define C4_CONSTEXPR14 constexpr
+//# define C4_NOEXCEPT noexcept
+# endif
+#else // _MSC_VER
+# if _MSC_VER < 1900
+# define C4_CONSTEXPR11
+# define C4_CONSTEXPR14
+//# define C4_NOEXCEPT
+# elif _MSC_VER < 2000
+# define C4_CONSTEXPR11 constexpr
+# define C4_CONSTEXPR14
+//# define C4_NOEXCEPT noexcept
+# else
+# define C4_CONSTEXPR11 constexpr
+# define C4_CONSTEXPR14 constexpr
+//# define C4_NOEXCEPT noexcept
+# endif
+#endif // _MSC_VER
+
+
+#if C4_CPP < 17
+#define C4_IF_CONSTEXPR
+#define C4_INLINE_CONSTEXPR constexpr
+#else
+#define C4_IF_CONSTEXPR constexpr
+#define C4_INLINE_CONSTEXPR inline constexpr
+#endif
+
+
+//------------------------------------------------------------
+
+#define _C4_BEGIN_NAMESPACE(ns) namespace ns {
+#define _C4_END_NAMESPACE(ns) }
+
+// MSVC cant handle the C4_FOR_EACH macro... need to fix this
+//#define C4_BEGIN_NAMESPACE(...) C4_FOR_EACH_SEP(_C4_BEGIN_NAMESPACE, , __VA_ARGS__)
+//#define C4_END_NAMESPACE(...) C4_FOR_EACH_SEP(_C4_END_NAMESPACE, , __VA_ARGS__)
+#define C4_BEGIN_NAMESPACE(ns) namespace ns {
+#define C4_END_NAMESPACE(ns) }
+
+#define C4_BEGIN_HIDDEN_NAMESPACE namespace /*hidden*/ {
+#define C4_END_HIDDEN_NAMESPACE } /* namespace hidden */
+
+//------------------------------------------------------------
+
+#ifndef C4_API
+# if defined(_MSC_VER)
+# if defined(C4_EXPORT)
+# define C4_API __declspec(dllexport)
+# elif defined(C4_IMPORT)
+# define C4_API __declspec(dllimport)
+# else
+# define C4_API
+# endif
+# else
+# define C4_API
+# endif
+#endif
+
+#ifndef _MSC_VER ///< @todo assuming gcc-like compiler. check it is actually so.
+/** for function attributes in GCC,
+ * @see https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes */
+/** for __builtin functions in GCC,
+ * @see https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html */
+# define C4_RESTRICT __restrict__
+# define C4_RESTRICT_FN __attribute__((restrict))
+# define C4_NO_INLINE __attribute__((noinline))
+# define C4_ALWAYS_INLINE inline __attribute__((always_inline))
+/** force inlining of every callee function */
+# define C4_FLATTEN __atribute__((flatten))
+/** mark a function as hot, ie as having a visible impact in CPU time
+ * thus making it more likely to inline, etc
+ * @see http://stackoverflow.com/questions/15028990/semantics-of-gcc-hot-attribute */
+# define C4_HOT __attribute__((hot))
+/** mark a function as cold, ie as NOT having a visible impact in CPU time
+ * @see http://stackoverflow.com/questions/15028990/semantics-of-gcc-hot-attribute */
+# define C4_COLD __attribute__((cold))
+# define C4_EXPECT(x, y) __builtin_expect(x, y) ///< @see https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html
+# define C4_LIKELY(x) __builtin_expect(x, 1)
+# define C4_UNLIKELY(x) __builtin_expect(x, 0)
+# define C4_UNREACHABLE() __builtin_unreachable()
+# define C4_ATTR_FORMAT(...) //__attribute__((format (__VA_ARGS__))) ///< @see https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes
+# define C4_NORETURN __attribute__((noreturn))
+#else
+# define C4_RESTRICT __restrict
+# define C4_RESTRICT_FN __declspec(restrict)
+# define C4_NO_INLINE __declspec(noinline)
+# define C4_ALWAYS_INLINE inline __forceinline
+/** these are not available in VS AFAIK */
+# define C4_FLATTEN
+# define C4_HOT /** @todo */
+# define C4_COLD /** @todo */
+# define C4_EXPECT(x, y) x /** @todo */
+# define C4_LIKELY(x) x /** @todo */
+# define C4_UNLIKELY(x) x /** @todo */
+# define C4_UNREACHABLE() /** @todo */
+# define C4_ATTR_FORMAT(...) /** */
+# define C4_NORETURN /** @todo */
+#endif
+
+#ifndef _MSC_VER
+# define C4_FUNC __FUNCTION__
+# define C4_PRETTY_FUNC __PRETTY_FUNCTION__
+#else /// @todo assuming gcc-like compiler. check it is actually so.
+# define C4_FUNC __FUNCTION__
+# define C4_PRETTY_FUNC __FUNCSIG__
+#endif
+
+/** prevent compiler warnings about a specific var being unused */
+#define C4_UNUSED(var) (void)var
+
+#if C4_CPP >= 17
+#define C4_STATIC_ASSERT(cond) static_assert(cond)
+#else
+#define C4_STATIC_ASSERT(cond) static_assert((cond), #cond)
+#endif
+#define C4_STATIC_ASSERT_MSG(cond, msg) static_assert((cond), #cond ": " msg)
+
+/** @def C4_DONT_OPTIMIZE idea lifted from GoogleBenchmark.
+ * @see https://github.com/google/benchmark/blob/master/include/benchmark/benchmark_api.h */
+namespace c4 {
+namespace detail {
+#ifdef __GNUC__
+# define C4_DONT_OPTIMIZE(var) c4::detail::dont_optimize(var)
+template< class T >
+C4_ALWAYS_INLINE void dont_optimize(T const& value) { asm volatile("" : : "g"(value) : "memory"); }
+#else
+# define C4_DONT_OPTIMIZE(var) c4::detail::use_char_pointer(reinterpret_cast< const char* >(&var))
+void use_char_pointer(char const volatile*);
+#endif
+} // namespace detail
+} // namespace c4
+
+/** @def C4_KEEP_EMPTY_LOOP prevent an empty loop from being optimized out.
+ * @see http://stackoverflow.com/a/7084193/5875572 */
+#ifndef _MSC_VER
+# define C4_KEEP_EMPTY_LOOP { asm(""); }
+#else
+# define C4_KEEP_EMPTY_LOOP { char c; C4_DONT_OPTIMIZE(c); }
+#endif
+
+/** @def C4_VA_LIST_REUSE_MUST_COPY
+ * @todo <jpmag> I strongly suspect that this is actually only in UNIX platforms. revisit this. */
+#ifdef __GNUC__
+# define C4_VA_LIST_REUSE_MUST_COPY
+#endif
+
+#endif /* _C4_LANGUAGE_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/language.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/types.hpp
+// https://github.com/biojppm/c4core/src/c4/types.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_TYPES_HPP_
+#define _C4_TYPES_HPP_
+
+//included above:
+//#include <stdint.h>
+#include <stddef.h>
+//included above:
+//#include <type_traits>
+
+#if __cplusplus >= 201103L
+#include <utility> // for integer_sequence and friends
+#endif
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/preprocessor.hpp
+//#include "c4/preprocessor.hpp"
+#if !defined(C4_PREPROCESSOR_HPP_) && !defined(_C4_PREPROCESSOR_HPP_)
+#error "amalgamate: file c4/preprocessor.hpp must have been included at this point"
+#endif /* C4_PREPROCESSOR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/language.hpp
+//#include "c4/language.hpp"
+#if !defined(C4_LANGUAGE_HPP_) && !defined(_C4_LANGUAGE_HPP_)
+#error "amalgamate: file c4/language.hpp must have been included at this point"
+#endif /* C4_LANGUAGE_HPP_ */
+
+
+/** @file types.hpp basic types, and utility macros and traits for types.
+ * @ingroup basic_headers */
+
+/** @defgroup types Type utilities */
+
+namespace c4 {
+
+/** @defgroup intrinsic_types Intrinsic types
+ * @ingroup types
+ * @{ */
+
+using cbyte = const char; /**< a constant byte */
+using byte = char; /**< a mutable byte */
+
+using i8 = int8_t;
+using i16 = int16_t;
+using i32 = int32_t;
+using i64 = int64_t;
+using u8 = uint8_t;
+using u16 = uint16_t;
+using u32 = uint32_t;
+using u64 = uint64_t;
+
+using f32 = float;
+using f64 = double;
+
+using ssize_t = typename std::make_signed<size_t>::type;
+
+/** @} */
+
+//--------------------------------------------------
+
+/** @defgroup utility_types Utility types
+ * @ingroup types
+ * @{ */
+
+// some tag types
+
+/** a tag type for initializing the containers with variadic arguments a la
+ * initializer_list, minus the initializer_list overload problems.
+ */
+struct aggregate_t {};
+/** @see aggregate_t */
+constexpr const aggregate_t aggregate{};
+
+/** a tag type for specifying the initial capacity of allocatable contiguous storage */
+struct with_capacity_t {};
+/** @see with_capacity_t */
+constexpr const with_capacity_t with_capacity{};
+
+/** a tag type for disambiguating template parameter packs in variadic template overloads */
+struct varargs_t {};
+/** @see with_capacity_t */
+constexpr const varargs_t varargs{};
+
+
+//--------------------------------------------------
+
+/** whether a value should be used in place of a const-reference in argument passing. */
+template<class T>
+struct cref_uses_val
+{
+ enum { value = (
+ std::is_scalar<T>::value
+ ||
+ (
+#if C4_CPP >= 20
+ (std::is_trivially_copyable<T>::value && std::is_standard_layout<T>::value)
+#else
+ std::is_pod<T>::value
+#endif
+ &&
+ sizeof(T) <= sizeof(size_t))) };
+};
+/** utility macro to override the default behaviour for c4::fastcref<T>
+ @see fastcref */
+#define C4_CREF_USES_VAL(T) \
+template<> \
+struct cref_uses_val<T> \
+{ \
+ enum { value = true }; \
+};
+
+/** Whether to use pass-by-value or pass-by-const-reference in a function argument
+ * or return type. */
+template<class T>
+using fastcref = typename std::conditional<c4::cref_uses_val<T>::value, T, T const&>::type;
+
+//--------------------------------------------------
+
+/** Just what its name says. Useful sometimes as a default empty policy class. */
+struct EmptyStruct
+{
+ template<class... T> EmptyStruct(T && ...){}
+};
+
+/** Just what its name says. Useful sometimes as a default policy class to
+ * be inherited from. */
+struct EmptyStructVirtual
+{
+ virtual ~EmptyStructVirtual() = default;
+ template<class... T> EmptyStructVirtual(T && ...){}
+};
+
+
+/** */
+template<class T>
+struct inheritfrom : public T {};
+
+//--------------------------------------------------
+// Utilities to make a class obey size restrictions (eg, min size or size multiple of).
+// DirectX usually makes this restriction with uniform buffers.
+// This is also useful for padding to prevent false-sharing.
+
+/** how many bytes must be added to size such that the result is at least minsize? */
+C4_ALWAYS_INLINE constexpr size_t min_remainder(size_t size, size_t minsize) noexcept
+{
+ return size < minsize ? minsize-size : 0;
+}
+
+/** how many bytes must be added to size such that the result is a multiple of multipleof? */
+C4_ALWAYS_INLINE constexpr size_t mult_remainder(size_t size, size_t multipleof) noexcept
+{
+ return (((size % multipleof) != 0) ? (multipleof-(size % multipleof)) : 0);
+}
+
+/* force the following class to be tightly packed. */
+#pragma pack(push, 1)
+/** pad a class with more bytes at the end.
+ * @see http://stackoverflow.com/questions/21092415/force-c-structure-to-pack-tightly */
+template<class T, size_t BytesToPadAtEnd>
+struct Padded : public T
+{
+ using T::T;
+ using T::operator=;
+ Padded(T const& val) : T(val) {}
+ Padded(T && val) : T(val) {}
+ char ___c4padspace___[BytesToPadAtEnd];
+};
+#pragma pack(pop)
+/** When the padding argument is 0, we cannot declare the char[] array. */
+template<class T>
+struct Padded<T, 0> : public T
+{
+ using T::T;
+ using T::operator=;
+ Padded(T const& val) : T(val) {}
+ Padded(T && val) : T(val) {}
+};
+
+/** make T have a size which is at least Min bytes */
+template<class T, size_t Min>
+using MinSized = Padded<T, min_remainder(sizeof(T), Min)>;
+
+/** make T have a size which is a multiple of Mult bytes */
+template<class T, size_t Mult>
+using MultSized = Padded<T, mult_remainder(sizeof(T), Mult)>;
+
+/** make T have a size which is simultaneously:
+ * -bigger or equal than Min
+ * -a multiple of Mult */
+template<class T, size_t Min, size_t Mult>
+using MinMultSized = MultSized<MinSized<T, Min>, Mult>;
+
+/** make T be suitable for use as a uniform buffer. (at least with DirectX). */
+template<class T>
+using UbufSized = MinMultSized<T, 64, 16>;
+
+
+//-----------------------------------------------------------------------------
+
+#define C4_NO_COPY_CTOR(ty) ty(ty const&) = delete
+#define C4_NO_MOVE_CTOR(ty) ty(ty &&) = delete
+#define C4_NO_COPY_ASSIGN(ty) ty& operator=(ty const&) = delete
+#define C4_NO_MOVE_ASSIGN(ty) ty& operator=(ty &&) = delete
+#define C4_DEFAULT_COPY_CTOR(ty) ty(ty const&) noexcept = default
+#define C4_DEFAULT_MOVE_CTOR(ty) ty(ty &&) noexcept = default
+#define C4_DEFAULT_COPY_ASSIGN(ty) ty& operator=(ty const&) noexcept = default
+#define C4_DEFAULT_MOVE_ASSIGN(ty) ty& operator=(ty &&) noexcept = default
+
+#define C4_NO_COPY_OR_MOVE_CTOR(ty) \
+ C4_NO_COPY_CTOR(ty); \
+ C4_NO_MOVE_CTOR(ty)
+
+#define C4_NO_COPY_OR_MOVE_ASSIGN(ty) \
+ C4_NO_COPY_ASSIGN(ty); \
+ C4_NO_MOVE_ASSIGN(ty)
+
+#define C4_NO_COPY_OR_MOVE(ty) \
+ C4_NO_COPY_OR_MOVE_CTOR(ty); \
+ C4_NO_COPY_OR_MOVE_ASSIGN(ty)
+
+#define C4_DEFAULT_COPY_AND_MOVE_CTOR(ty) \
+ C4_DEFAULT_COPY_CTOR(ty); \
+ C4_DEFAULT_MOVE_CTOR(ty)
+
+#define C4_DEFAULT_COPY_AND_MOVE_ASSIGN(ty) \
+ C4_DEFAULT_COPY_ASSIGN(ty); \
+ C4_DEFAULT_MOVE_ASSIGN(ty)
+
+#define C4_DEFAULT_COPY_AND_MOVE(ty) \
+ C4_DEFAULT_COPY_AND_MOVE_CTOR(ty); \
+ C4_DEFAULT_COPY_AND_MOVE_ASSIGN(ty)
+
+/** @see https://en.cppreference.com/w/cpp/named_req/TriviallyCopyable */
+#define C4_MUST_BE_TRIVIAL_COPY(ty) \
+ static_assert(std::is_trivially_copyable<ty>::value, #ty " must be trivially copyable")
+
+/** @} */
+
+
+//-----------------------------------------------------------------------------
+
+/** @defgroup traits_types Type traits utilities
+ * @ingroup types
+ * @{ */
+
+// http://stackoverflow.com/questions/10821380/is-t-an-instance-of-a-template-in-c
+template<template<typename...> class X, typename T> struct is_instance_of_tpl : std::false_type {};
+template<template<typename...> class X, typename... Y> struct is_instance_of_tpl<X, X<Y...>> : std::true_type {};
+
+//-----------------------------------------------------------------------------
+
+/** SFINAE. use this macro to enable a template function overload
+based on a compile-time condition.
+@code
+// define an overload for a non-pod type
+template<class T, C4_REQUIRE_T(std::is_pod<T>::value)>
+void foo() { std::cout << "pod type\n"; }
+
+// define an overload for a non-pod type
+template<class T, C4_REQUIRE_T(!std::is_pod<T>::value)>
+void foo() { std::cout << "nonpod type\n"; }
+
+struct non_pod
+{
+ non_pod() : name("asdfkjhasdkjh") {}
+ const char *name;
+};
+
+int main()
+{
+ foo<float>(); // prints "pod type"
+ foo<non_pod>(); // prints "nonpod type"
+}
+@endcode */
+#define C4_REQUIRE_T(cond) typename std::enable_if<cond, bool>::type* = nullptr
+
+/** enable_if for a return type
+ * @see C4_REQUIRE_T */
+#define C4_REQUIRE_R(cond, type_) typename std::enable_if<cond, type_>::type
+
+//-----------------------------------------------------------------------------
+/** define a traits class reporting whether a type provides a member typedef */
+#define C4_DEFINE_HAS_TYPEDEF(member_typedef) \
+template<typename T> \
+struct has_##stype \
+{ \
+private: \
+ \
+ typedef char yes; \
+ typedef struct { char array[2]; } no; \
+ \
+ template<typename C> \
+ static yes _test(typename C::member_typedef*); \
+ \
+ template<typename C> \
+ static no _test(...); \
+ \
+public: \
+ \
+ enum { value = (sizeof(_test<T>(0)) == sizeof(yes)) }; \
+ \
+}
+
+
+/** @} */
+
+
+//-----------------------------------------------------------------------------
+
+
+/** @defgroup type_declarations Type declaration utilities
+ * @ingroup types
+ * @{ */
+
+#define _c4_DEFINE_ARRAY_TYPES_WITHOUT_ITERATOR(T, I) \
+ \
+ using size_type = I; \
+ using ssize_type = typename std::make_signed<I>::type; \
+ using difference_type = typename std::make_signed<I>::type; \
+ \
+ using value_type = T; \
+ using pointer = T*; \
+ using const_pointer = T const*; \
+ using reference = T&; \
+ using const_reference = T const&
+
+#define _c4_DEFINE_TUPLE_ARRAY_TYPES_WITHOUT_ITERATOR(interior_types, I) \
+ \
+ using size_type = I; \
+ using ssize_type = typename std::make_signed<I>::type; \
+ using difference_type = typename std::make_signed<I>::type; \
+ \
+ template<I n> using value_type = typename std::tuple_element< n, std::tuple<interior_types...>>::type; \
+ template<I n> using pointer = value_type<n>*; \
+ template<I n> using const_pointer = value_type<n> const*; \
+ template<I n> using reference = value_type<n>&; \
+ template<I n> using const_reference = value_type<n> const&
+
+
+#define _c4_DEFINE_ARRAY_TYPES(T, I) \
+ \
+ _c4_DEFINE_ARRAY_TYPES_WITHOUT_ITERATOR(T, I); \
+ \
+ using iterator = T*; \
+ using const_iterator = T const*; \
+ using reverse_iterator = std::reverse_iterator<T*>; \
+ using const_reverse_iterator = std::reverse_iterator<T const*>
+
+
+#define _c4_DEFINE_TUPLE_ARRAY_TYPES(interior_types, I) \
+ \
+ _c4_DEFINE_TUPLE_ARRAY_TYPES_WITHOUT_ITERATOR(interior_types, I); \
+ \
+ template<I n> using iterator = value_type<n>*; \
+ template<I n> using const_iterator = value_type<n> const*; \
+ template<I n> using reverse_iterator = std::reverse_iterator< value_type<n>*>; \
+ template<I n> using const_reverse_iterator = std::reverse_iterator< value_type<n> const*>
+
+
+
+/** @} */
+
+
+//-----------------------------------------------------------------------------
+
+
+/** @defgroup compatility_utilities Backport implementation of some Modern C++ utilities
+ * @ingroup types
+ * @{ */
+
+//-----------------------------------------------------------------------------
+// index_sequence and friends are available only for C++14 and later.
+// A C++11 implementation is provided here.
+// This implementation was copied over from clang.
+// see http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687
+
+#if __cplusplus > 201103L
+
+using std::integer_sequence;
+using std::index_sequence;
+using std::make_integer_sequence;
+using std::make_index_sequence;
+using std::index_sequence_for;
+
+#else
+
+/** C++11 implementation of integer sequence
+ * @see https://en.cppreference.com/w/cpp/utility/integer_sequence
+ * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */
+template<class _Tp, _Tp... _Ip>
+struct integer_sequence
+{
+ static_assert(std::is_integral<_Tp>::value,
+ "std::integer_sequence can only be instantiated with an integral type" );
+ using value_type = _Tp;
+ static constexpr size_t size() noexcept { return sizeof...(_Ip); }
+};
+
+/** C++11 implementation of index sequence
+ * @see https://en.cppreference.com/w/cpp/utility/integer_sequence
+ * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */
+template<size_t... _Ip>
+using index_sequence = integer_sequence<size_t, _Ip...>;
+
+/** @cond DONT_DOCUMENT_THIS */
+namespace __detail {
+
+template<typename _Tp, size_t ..._Extra>
+struct __repeat;
+
+template<typename _Tp, _Tp ..._Np, size_t ..._Extra>
+struct __repeat<integer_sequence<_Tp, _Np...>, _Extra...>
+{
+ using type = integer_sequence<_Tp,
+ _Np...,
+ sizeof...(_Np) + _Np...,
+ 2 * sizeof...(_Np) + _Np...,
+ 3 * sizeof...(_Np) + _Np...,
+ 4 * sizeof...(_Np) + _Np...,
+ 5 * sizeof...(_Np) + _Np...,
+ 6 * sizeof...(_Np) + _Np...,
+ 7 * sizeof...(_Np) + _Np...,
+ _Extra...>;
+};
+
+template<size_t _Np> struct __parity;
+template<size_t _Np> struct __make : __parity<_Np % 8>::template __pmake<_Np> {};
+
+template<> struct __make<0> { using type = integer_sequence<size_t>; };
+template<> struct __make<1> { using type = integer_sequence<size_t, 0>; };
+template<> struct __make<2> { using type = integer_sequence<size_t, 0, 1>; };
+template<> struct __make<3> { using type = integer_sequence<size_t, 0, 1, 2>; };
+template<> struct __make<4> { using type = integer_sequence<size_t, 0, 1, 2, 3>; };
+template<> struct __make<5> { using type = integer_sequence<size_t, 0, 1, 2, 3, 4>; };
+template<> struct __make<6> { using type = integer_sequence<size_t, 0, 1, 2, 3, 4, 5>; };
+template<> struct __make<7> { using type = integer_sequence<size_t, 0, 1, 2, 3, 4, 5, 6>; };
+
+template<> struct __parity<0> { template<size_t _Np> struct __pmake : __repeat<typename __make<_Np / 8>::type> {}; };
+template<> struct __parity<1> { template<size_t _Np> struct __pmake : __repeat<typename __make<_Np / 8>::type, _Np - 1> {}; };
+template<> struct __parity<2> { template<size_t _Np> struct __pmake : __repeat<typename __make<_Np / 8>::type, _Np - 2, _Np - 1> {}; };
+template<> struct __parity<3> { template<size_t _Np> struct __pmake : __repeat<typename __make<_Np / 8>::type, _Np - 3, _Np - 2, _Np - 1> {}; };
+template<> struct __parity<4> { template<size_t _Np> struct __pmake : __repeat<typename __make<_Np / 8>::type, _Np - 4, _Np - 3, _Np - 2, _Np - 1> {}; };
+template<> struct __parity<5> { template<size_t _Np> struct __pmake : __repeat<typename __make<_Np / 8>::type, _Np - 5, _Np - 4, _Np - 3, _Np - 2, _Np - 1> {}; };
+template<> struct __parity<6> { template<size_t _Np> struct __pmake : __repeat<typename __make<_Np / 8>::type, _Np - 6, _Np - 5, _Np - 4, _Np - 3, _Np - 2, _Np - 1> {}; };
+template<> struct __parity<7> { template<size_t _Np> struct __pmake : __repeat<typename __make<_Np / 8>::type, _Np - 7, _Np - 6, _Np - 5, _Np - 4, _Np - 3, _Np - 2, _Np - 1> {}; };
+
+template<typename _Tp, typename _Up>
+struct __convert
+{
+ template<typename> struct __result;
+ template<_Tp ..._Np> struct __result<integer_sequence<_Tp, _Np...>>
+ {
+ using type = integer_sequence<_Up, _Np...>;
+ };
+};
+
+template<typename _Tp>
+struct __convert<_Tp, _Tp>
+{
+ template<typename _Up> struct __result
+ {
+ using type = _Up;
+ };
+};
+
+template<typename _Tp, _Tp _Np>
+using __make_integer_sequence_unchecked = typename __detail::__convert<size_t, _Tp>::template __result<typename __detail::__make<_Np>::type>::type;
+
+template<class _Tp, _Tp _Ep>
+struct __make_integer_sequence
+{
+ static_assert(std::is_integral<_Tp>::value,
+ "std::make_integer_sequence can only be instantiated with an integral type" );
+ static_assert(0 <= _Ep, "std::make_integer_sequence input shall not be negative");
+ typedef __make_integer_sequence_unchecked<_Tp, _Ep> type;
+};
+
+} // namespace __detail
+/** @endcond */
+
+
+/** C++11 implementation of index sequence
+ * @see https://en.cppreference.com/w/cpp/utility/integer_sequence
+ * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */
+template<class _Tp, _Tp _Np>
+using make_integer_sequence = typename __detail::__make_integer_sequence<_Tp, _Np>::type;
+
+/** C++11 implementation of index sequence
+ * @see https://en.cppreference.com/w/cpp/utility/integer_sequence
+ * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */
+template<size_t _Np>
+using make_index_sequence = make_integer_sequence<size_t, _Np>;
+
+/** C++11 implementation of index sequence
+ * @see https://en.cppreference.com/w/cpp/utility/integer_sequence
+ * @see taken from clang: http://llvm.org/viewvc/llvm-project/libcxx/trunk/include/utility?revision=211563&view=markup#l687 */
+template<class... _Tp>
+using index_sequence_for = make_index_sequence<sizeof...(_Tp)>;
+#endif
+
+/** @} */
+
+
+} // namespace c4
+
+#endif /* _C4_TYPES_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/types.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/config.hpp
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_CONFIG_HPP_
+#define _C4_CONFIG_HPP_
+
+/** @defgroup basic_headers Basic headers
+ * @brief Headers providing basic macros, platform+cpu+compiler information,
+ * C++ facilities and basic typedefs. */
+
+/** @file config.hpp Contains configuration defines and includes the basic_headers.
+ * @ingroup basic_headers */
+
+//#define C4_DEBUG
+
+#define C4_ERROR_SHOWS_FILELINE
+//#define C4_ERROR_SHOWS_FUNC
+//#define C4_ERROR_THROWS_EXCEPTION
+//#define C4_NO_ALLOC_DEFAULTS
+//#define C4_REDEFINE_CPPNEW
+
+#ifndef C4_SIZE_TYPE
+# define C4_SIZE_TYPE size_t
+#endif
+
+#ifndef C4_STR_SIZE_TYPE
+# define C4_STR_SIZE_TYPE C4_SIZE_TYPE
+#endif
+
+#ifndef C4_TIME_TYPE
+# define C4_TIME_TYPE double
+#endif
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/export.hpp
+//#include "c4/export.hpp"
+#if !defined(C4_EXPORT_HPP_) && !defined(_C4_EXPORT_HPP_)
+#error "amalgamate: file c4/export.hpp must have been included at this point"
+#endif /* C4_EXPORT_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/preprocessor.hpp
+//#include "c4/preprocessor.hpp"
+#if !defined(C4_PREPROCESSOR_HPP_) && !defined(_C4_PREPROCESSOR_HPP_)
+#error "amalgamate: file c4/preprocessor.hpp must have been included at this point"
+#endif /* C4_PREPROCESSOR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/platform.hpp
+//#include "c4/platform.hpp"
+#if !defined(C4_PLATFORM_HPP_) && !defined(_C4_PLATFORM_HPP_)
+#error "amalgamate: file c4/platform.hpp must have been included at this point"
+#endif /* C4_PLATFORM_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/cpu.hpp
+//#include "c4/cpu.hpp"
+//#if !defined(C4_CPU_HPP_) && !defined(_C4_CPU_HPP_)
+//#error "amalgamate: file c4/cpu.hpp must have been included at this point"
+//#endif /* C4_CPU_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/compiler.hpp
+//#include "c4/compiler.hpp"
+#if !defined(C4_COMPILER_HPP_) && !defined(_C4_COMPILER_HPP_)
+#error "amalgamate: file c4/compiler.hpp must have been included at this point"
+#endif /* C4_COMPILER_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/language.hpp
+//#include "c4/language.hpp"
+#if !defined(C4_LANGUAGE_HPP_) && !defined(_C4_LANGUAGE_HPP_)
+#error "amalgamate: file c4/language.hpp must have been included at this point"
+#endif /* C4_LANGUAGE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/types.hpp
+//#include "c4/types.hpp"
+#if !defined(C4_TYPES_HPP_) && !defined(_C4_TYPES_HPP_)
+#error "amalgamate: file c4/types.hpp must have been included at this point"
+#endif /* C4_TYPES_HPP_ */
+
+
+#endif // _C4_CONFIG_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/config.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/ext/debugbreak/debugbreak.h
+// https://github.com/biojppm/c4core/src/c4/ext/debugbreak/debugbreak.h
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+/* Copyright (c) 2011-2021, Scott Tsai
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ */
+#ifndef DEBUG_BREAK_H
+#define DEBUG_BREAK_H
+
+#ifdef _MSC_VER
+
+#define debug_break __debugbreak
+
+#else
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define DEBUG_BREAK_USE_TRAP_INSTRUCTION 1
+#define DEBUG_BREAK_USE_BULTIN_TRAP 2
+#define DEBUG_BREAK_USE_SIGTRAP 3
+
+#if defined(__i386__) || defined(__x86_64__)
+ #define DEBUG_BREAK_IMPL DEBUG_BREAK_USE_TRAP_INSTRUCTION
+__inline__ static void trap_instruction(void)
+{
+ __asm__ volatile("int $0x03");
+}
+#elif defined(__thumb__)
+ #define DEBUG_BREAK_IMPL DEBUG_BREAK_USE_TRAP_INSTRUCTION
+/* FIXME: handle __THUMB_INTERWORK__ */
+__attribute__((always_inline))
+__inline__ static void trap_instruction(void)
+{
+ /* See 'arm-linux-tdep.c' in GDB source.
+ * Both instruction sequences below work. */
+#if 1
+ /* 'eabi_linux_thumb_le_breakpoint' */
+ __asm__ volatile(".inst 0xde01");
+#else
+ /* 'eabi_linux_thumb2_le_breakpoint' */
+ __asm__ volatile(".inst.w 0xf7f0a000");
+#endif
+
+ /* Known problem:
+ * After a breakpoint hit, can't 'stepi', 'step', or 'continue' in GDB.
+ * 'step' would keep getting stuck on the same instruction.
+ *
+ * Workaround: use the new GDB commands 'debugbreak-step' and
+ * 'debugbreak-continue' that become available
+ * after you source the script from GDB:
+ *
+ * $ gdb -x debugbreak-gdb.py <... USUAL ARGUMENTS ...>
+ *
+ * 'debugbreak-step' would jump over the breakpoint instruction with
+ * roughly equivalent of:
+ * (gdb) set $instruction_len = 2
+ * (gdb) tbreak *($pc + $instruction_len)
+ * (gdb) jump *($pc + $instruction_len)
+ */
+}
+#elif defined(__arm__) && !defined(__thumb__)
+ #define DEBUG_BREAK_IMPL DEBUG_BREAK_USE_TRAP_INSTRUCTION
+__attribute__((always_inline))
+__inline__ static void trap_instruction(void)
+{
+ /* See 'arm-linux-tdep.c' in GDB source,
+ * 'eabi_linux_arm_le_breakpoint' */
+ __asm__ volatile(".inst 0xe7f001f0");
+ /* Known problem:
+ * Same problem and workaround as Thumb mode */
+}
+#elif defined(__aarch64__) && defined(__APPLE__)
+ #define DEBUG_BREAK_IMPL DEBUG_BREAK_USE_BULTIN_TRAP
+#elif defined(__aarch64__)
+ #define DEBUG_BREAK_IMPL DEBUG_BREAK_USE_TRAP_INSTRUCTION
+__attribute__((always_inline))
+__inline__ static void trap_instruction(void)
+{
+ /* See 'aarch64-tdep.c' in GDB source,
+ * 'aarch64_default_breakpoint' */
+ __asm__ volatile(".inst 0xd4200000");
+}
+#elif defined(__powerpc__)
+ /* PPC 32 or 64-bit, big or little endian */
+ #define DEBUG_BREAK_IMPL DEBUG_BREAK_USE_TRAP_INSTRUCTION
+__attribute__((always_inline))
+__inline__ static void trap_instruction(void)
+{
+ /* See 'rs6000-tdep.c' in GDB source,
+ * 'rs6000_breakpoint' */
+ __asm__ volatile(".4byte 0x7d821008");
+
+ /* Known problem:
+ * After a breakpoint hit, can't 'stepi', 'step', or 'continue' in GDB.
+ * 'step' stuck on the same instruction ("twge r2,r2").
+ *
+ * The workaround is the same as ARM Thumb mode: use debugbreak-gdb.py
+ * or manually jump over the instruction. */
+}
+#elif defined(__riscv)
+ /* RISC-V 32 or 64-bit, whether the "C" extension
+ * for compressed, 16-bit instructions are supported or not */
+ #define DEBUG_BREAK_IMPL DEBUG_BREAK_USE_TRAP_INSTRUCTION
+__attribute__((always_inline))
+__inline__ static void trap_instruction(void)
+{
+ /* See 'riscv-tdep.c' in GDB source,
+ * 'riscv_sw_breakpoint_from_kind' */
+ __asm__ volatile(".4byte 0x00100073");
+}
+#else
+ #define DEBUG_BREAK_IMPL DEBUG_BREAK_USE_SIGTRAP
+#endif
+
+
+#ifndef DEBUG_BREAK_IMPL
+#error "debugbreak.h is not supported on this target"
+#elif DEBUG_BREAK_IMPL == DEBUG_BREAK_USE_TRAP_INSTRUCTION
+__attribute__((always_inline))
+__inline__ static void debug_break(void)
+{
+ trap_instruction();
+}
+#elif DEBUG_BREAK_IMPL == DEBUG_BREAK_USE_BULTIN_DEBUGTRAP
+__attribute__((always_inline))
+__inline__ static void debug_break(void)
+{
+ __builtin_debugtrap();
+}
+#elif DEBUG_BREAK_IMPL == DEBUG_BREAK_USE_BULTIN_TRAP
+__attribute__((always_inline))
+__inline__ static void debug_break(void)
+{
+ __builtin_trap();
+}
+#elif DEBUG_BREAK_IMPL == DEBUG_BREAK_USE_SIGTRAP
+#include <signal.h>
+__attribute__((always_inline))
+__inline__ static void debug_break(void)
+{
+ raise(SIGTRAP);
+}
+#else
+#error "invalid DEBUG_BREAK_IMPL value"
+#endif
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ifdef _MSC_VER */
+
+#endif /* ifndef DEBUG_BREAK_H */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/ext/debugbreak/debugbreak.h)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/error.hpp
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_ERROR_HPP_
+#define _C4_ERROR_HPP_
+
+/** @file error.hpp Facilities for error reporting and runtime assertions. */
+
+/** @defgroup error_checking Error checking */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//#include "c4/config.hpp"
+#if !defined(C4_CONFIG_HPP_) && !defined(_C4_CONFIG_HPP_)
+#error "amalgamate: file c4/config.hpp must have been included at this point"
+#endif /* C4_CONFIG_HPP_ */
+
+
+#ifdef _DOXYGEN_
+ /** if this is defined and exceptions are enabled, then calls to C4_ERROR()
+ * will throw an exception
+ * @ingroup error_checking */
+# define C4_EXCEPTIONS_ENABLED
+ /** if this is defined and exceptions are enabled, then calls to C4_ERROR()
+ * will throw an exception
+ * @see C4_EXCEPTIONS_ENABLED
+ * @ingroup error_checking */
+# define C4_ERROR_THROWS_EXCEPTION
+ /** evaluates to noexcept when C4_ERROR might be called and
+ * exceptions are disabled. Otherwise, defaults to nothing.
+ * @ingroup error_checking */
+# define C4_NOEXCEPT
+#endif // _DOXYGEN_
+
+#if defined(C4_EXCEPTIONS_ENABLED) && defined(C4_ERROR_THROWS_EXCEPTION)
+# define C4_NOEXCEPT
+#else
+# define C4_NOEXCEPT noexcept
+#endif
+
+
+namespace c4 {
+namespace detail {
+struct fail_type__ {};
+} // detail
+} // c4
+#define C4_STATIC_ERROR(dummy_type, errmsg) \
+ static_assert(std::is_same<dummy_type, c4::detail::fail_type__>::value, errmsg)
+
+
+//-----------------------------------------------------------------------------
+
+#define C4_ASSERT_SAME_TYPE(ty1, ty2) \
+ C4_STATIC_ASSERT(std::is_same<ty1 C4_COMMA_X ty2>::value)
+
+#define C4_ASSERT_DIFF_TYPE(ty1, ty2) \
+ C4_STATIC_ASSERT( ! std::is_same<ty1 C4_COMMA_X ty2>::value)
+
+
+//-----------------------------------------------------------------------------
+
+#ifdef _DOXYGEN_
+/** utility macro that triggers a breakpoint when
+ * the debugger is attached and NDEBUG is not defined.
+ * @ingroup error_checking */
+# define C4_DEBUG_BREAK()
+#endif // _DOXYGEN_
+
+
+#ifdef NDEBUG
+# define C4_DEBUG_BREAK()
+#else
+# ifdef __clang__
+# pragma clang diagnostic push
+# if !defined(__APPLE_CC__)
+# if __clang_major__ >= 10
+# pragma clang diagnostic ignored "-Wgnu-inline-cpp-without-extern" // debugbreak/debugbreak.h:50:16: error: 'gnu_inline' attribute without 'extern' in C++ treated as externally available, this changed in Clang 10 [-Werror,-Wgnu-inline-cpp-without-extern]
+# endif
+# else
+# if __clang_major__ >= 13
+# pragma clang diagnostic ignored "-Wgnu-inline-cpp-without-extern" // debugbreak/debugbreak.h:50:16: error: 'gnu_inline' attribute without 'extern' in C++ treated as externally available, this changed in Clang 10 [-Werror,-Wgnu-inline-cpp-without-extern]
+# endif
+# endif
+# elif defined(__GNUC__)
+# endif
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/ext/debugbreak/debugbreak.h
+//# include <c4/ext/debugbreak/debugbreak.h>
+#if !defined(DEBUG_BREAK_H) && !defined(_DEBUG_BREAK_H)
+#error "amalgamate: file c4/ext/debugbreak/debugbreak.h must have been included at this point"
+#endif /* DEBUG_BREAK_H */
+
+# define C4_DEBUG_BREAK() if(c4::is_debugger_attached()) { ::debug_break(); }
+# ifdef __clang__
+# pragma clang diagnostic pop
+# elif defined(__GNUC__)
+# endif
+#endif
+
+namespace c4 {
+C4CORE_EXPORT bool is_debugger_attached();
+} // namespace c4
+
+
+//-----------------------------------------------------------------------------
+
+#ifdef __clang__
+ /* NOTE: using , ## __VA_ARGS__ to deal with zero-args calls to
+ * variadic macros is not portable, but works in clang, gcc, msvc, icc.
+ * clang requires switching off compiler warnings for pedantic mode.
+ * @see http://stackoverflow.com/questions/32047685/variadic-macro-without-arguments */
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" // warning: token pasting of ',' and __VA_ARGS__ is a GNU extension
+#elif defined(__GNUC__)
+ /* GCC also issues a warning for zero-args calls to variadic macros.
+ * This warning is switched on with -pedantic and apparently there is no
+ * easy way to turn it off as with clang. But marking this as a system
+ * header works.
+ * @see https://gcc.gnu.org/onlinedocs/cpp/System-Headers.html
+ * @see http://stackoverflow.com/questions/35587137/ */
+# pragma GCC system_header
+#endif
+
+
+//-----------------------------------------------------------------------------
+
+namespace c4 {
+
+typedef enum : uint32_t {
+ /** when an error happens and the debugger is attached, call C4_DEBUG_BREAK().
+ * Without effect otherwise. */
+ ON_ERROR_DEBUGBREAK = 0x01 << 0,
+ /** when an error happens log a message. */
+ ON_ERROR_LOG = 0x01 << 1,
+ /** when an error happens invoke a callback if it was set with
+ * set_error_callback(). */
+ ON_ERROR_CALLBACK = 0x01 << 2,
+ /** when an error happens call std::terminate(). */
+ ON_ERROR_ABORT = 0x01 << 3,
+ /** when an error happens and exceptions are enabled throw an exception.
+ * Without effect otherwise. */
+ ON_ERROR_THROW = 0x01 << 4,
+ /** the default flags. */
+ ON_ERROR_DEFAULTS = ON_ERROR_DEBUGBREAK|ON_ERROR_LOG|ON_ERROR_CALLBACK|ON_ERROR_ABORT
+} ErrorFlags_e;
+using error_flags = uint32_t;
+C4CORE_EXPORT void set_error_flags(error_flags f);
+C4CORE_EXPORT error_flags get_error_flags();
+
+
+using error_callback_type = void (*)(const char* msg, size_t msg_size);
+C4CORE_EXPORT void set_error_callback(error_callback_type cb);
+C4CORE_EXPORT error_callback_type get_error_callback();
+
+
+//-----------------------------------------------------------------------------
+/** RAII class controling the error settings inside a scope. */
+struct ScopedErrorSettings
+{
+ error_flags m_flags;
+ error_callback_type m_callback;
+
+ explicit ScopedErrorSettings(error_callback_type cb)
+ : m_flags(get_error_flags()),
+ m_callback(get_error_callback())
+ {
+ set_error_callback(cb);
+ }
+ explicit ScopedErrorSettings(error_flags flags)
+ : m_flags(get_error_flags()),
+ m_callback(get_error_callback())
+ {
+ set_error_flags(flags);
+ }
+ explicit ScopedErrorSettings(error_flags flags, error_callback_type cb)
+ : m_flags(get_error_flags()),
+ m_callback(get_error_callback())
+ {
+ set_error_flags(flags);
+ set_error_callback(cb);
+ }
+ ~ScopedErrorSettings()
+ {
+ set_error_flags(m_flags);
+ set_error_callback(m_callback);
+ }
+};
+
+
+//-----------------------------------------------------------------------------
+
+/** source location */
+struct srcloc;
+
+C4CORE_EXPORT void handle_error(srcloc s, const char *fmt, ...);
+C4CORE_EXPORT void handle_warning(srcloc s, const char *fmt, ...);
+
+
+# define C4_ERROR(msg, ...) \
+ do { \
+ if(c4::get_error_flags() & c4::ON_ERROR_DEBUGBREAK) \
+ { \
+ C4_DEBUG_BREAK() \
+ } \
+ c4::handle_error(C4_SRCLOC(), msg, ## __VA_ARGS__); \
+ } while(0)
+
+
+# define C4_WARNING(msg, ...) \
+ c4::handle_warning(C4_SRCLOC(), msg, ## __VA_ARGS__)
+
+
+#if defined(C4_ERROR_SHOWS_FILELINE) && defined(C4_ERROR_SHOWS_FUNC)
+
+struct srcloc
+{
+ const char *file = "";
+ const char *func = "";
+ int line = 0;
+};
+#define C4_SRCLOC() c4::srcloc{__FILE__, C4_PRETTY_FUNC, __LINE__}
+
+#elif defined(C4_ERROR_SHOWS_FILELINE)
+
+struct srcloc
+{
+ const char *file;
+ int line;
+};
+#define C4_SRCLOC() c4::srcloc{__FILE__, __LINE__}
+
+#elif ! defined(C4_ERROR_SHOWS_FUNC)
+
+struct srcloc
+{
+};
+#define C4_SRCLOC() c4::srcloc()
+
+#else
+# error not implemented
+#endif
+
+
+//-----------------------------------------------------------------------------
+// assertions
+
+// Doxygen needs this so that only one definition counts
+#ifdef _DOXYGEN_
+ /** Explicitly enables assertions, independently of NDEBUG status.
+ * This is meant to allow enabling assertions even when NDEBUG is defined.
+ * Defaults to undefined.
+ * @ingroup error_checking */
+# define C4_USE_ASSERT
+ /** assert that a condition is true; this is turned off when NDEBUG
+ * is defined and C4_USE_ASSERT is not true.
+ * @ingroup error_checking */
+# define C4_ASSERT
+ /** same as C4_ASSERT(), additionally prints a printf-formatted message
+ * @ingroup error_checking */
+# define C4_ASSERT_MSG
+ /** evaluates to C4_NOEXCEPT when C4_XASSERT is disabled; otherwise, defaults
+ * to noexcept
+ * @ingroup error_checking */
+# define C4_NOEXCEPT_A
+#endif // _DOXYGEN_
+
+#ifndef C4_USE_ASSERT
+# ifdef NDEBUG
+# define C4_USE_ASSERT 0
+# else
+# define C4_USE_ASSERT 1
+# endif
+#endif
+
+#if C4_USE_ASSERT
+# define C4_ASSERT(cond) C4_CHECK(cond)
+# define C4_ASSERT_MSG(cond, /*fmt, */...) C4_CHECK_MSG(cond, ## __VA_ARGS__)
+# define C4_ASSERT_IF(predicate, cond) if(predicate) { C4_ASSERT(cond); }
+# define C4_NOEXCEPT_A C4_NOEXCEPT
+#else
+# define C4_ASSERT(cond)
+# define C4_ASSERT_MSG(cond, /*fmt, */...)
+# define C4_ASSERT_IF(predicate, cond)
+# define C4_NOEXCEPT_A noexcept
+#endif
+
+
+//-----------------------------------------------------------------------------
+// extreme assertions
+
+// Doxygen needs this so that only one definition counts
+#ifdef _DOXYGEN_
+ /** Explicitly enables extreme assertions; this is meant to allow enabling
+ * assertions even when NDEBUG is defined. Defaults to undefined.
+ * @ingroup error_checking */
+# define C4_USE_XASSERT
+ /** extreme assertion: can be switched off independently of
+ * the regular assertion; use for example for bounds checking in hot code.
+ * Turned on only when C4_USE_XASSERT is defined
+ * @ingroup error_checking */
+# define C4_XASSERT
+ /** same as C4_XASSERT(), and additionally prints a printf-formatted message
+ * @ingroup error_checking */
+# define C4_XASSERT_MSG
+ /** evaluates to C4_NOEXCEPT when C4_XASSERT is disabled; otherwise, defaults to noexcept
+ * @ingroup error_checking */
+# define C4_NOEXCEPT_X
+#endif // _DOXYGEN_
+
+#ifndef C4_USE_XASSERT
+# define C4_USE_XASSERT C4_USE_ASSERT
+#endif
+
+#if C4_USE_XASSERT
+# define C4_XASSERT(cond) C4_CHECK(cond)
+# define C4_XASSERT_MSG(cond, /*fmt, */...) C4_CHECK_MSG(cond, ## __VA_ARGS__)
+# define C4_XASSERT_IF(predicate, cond) if(predicate) { C4_XASSERT(cond); }
+# define C4_NOEXCEPT_X C4_NOEXCEPT
+#else
+# define C4_XASSERT(cond)
+# define C4_XASSERT_MSG(cond, /*fmt, */...)
+# define C4_XASSERT_IF(predicate, cond)
+# define C4_NOEXCEPT_X noexcept
+#endif
+
+
+//-----------------------------------------------------------------------------
+// checks: never switched-off
+
+/** Check that a condition is true, or raise an error when not
+ * true. Unlike C4_ASSERT(), this check is not disabled in non-debug
+ * builds.
+ * @see C4_ASSERT
+ * @ingroup error_checking
+ *
+ * @todo add constexpr-compatible compile-time assert:
+ * https://akrzemi1.wordpress.com/2017/05/18/asserts-in-constexpr-functions/
+ */
+#define C4_CHECK(cond) \
+ do { \
+ if(C4_UNLIKELY(!(cond))) \
+ { \
+ C4_ERROR("check failed: %s", #cond); \
+ } \
+ } while(0)
+
+
+/** like C4_CHECK(), and additionally log a printf-style message.
+ * @see C4_CHECK
+ * @ingroup error_checking */
+#define C4_CHECK_MSG(cond, fmt, ...) \
+ do { \
+ if(C4_UNLIKELY(!(cond))) \
+ { \
+ C4_ERROR("check failed: " #cond "\n" fmt, ## __VA_ARGS__); \
+ } \
+ } while(0)
+
+
+//-----------------------------------------------------------------------------
+// Common error conditions
+
+#define C4_NOT_IMPLEMENTED() C4_ERROR("NOT IMPLEMENTED")
+#define C4_NOT_IMPLEMENTED_MSG(/*msg, */...) C4_ERROR("NOT IMPLEMENTED: " ## __VA_ARGS__)
+#define C4_NOT_IMPLEMENTED_IF(condition) do { if(C4_UNLIKELY(condition)) { C4_ERROR("NOT IMPLEMENTED"); } } while(0)
+#define C4_NOT_IMPLEMENTED_IF_MSG(condition, /*msg, */...) do { if(C4_UNLIKELY(condition)) { C4_ERROR("NOT IMPLEMENTED: " ## __VA_ARGS__); } } while(0)
+
+#define C4_NEVER_REACH() do { C4_ERROR("never reach this point"); C4_UNREACHABLE(); } while(0)
+#define C4_NEVER_REACH_MSG(/*msg, */...) do { C4_ERROR("never reach this point: " ## __VA_ARGS__); C4_UNREACHABLE(); } while(0)
+
+
+
+//-----------------------------------------------------------------------------
+// helpers for warning suppression
+// idea adapted from https://github.com/onqtam/doctest/
+
+
+#ifdef C4_MSVC
+#define C4_SUPPRESS_WARNING_MSVC_PUSH __pragma(warning(push))
+#define C4_SUPPRESS_WARNING_MSVC(w) __pragma(warning(disable : w))
+#define C4_SUPPRESS_WARNING_MSVC_POP __pragma(warning(pop))
+#define C4_SUPPRESS_WARNING_MSVC_WITH_PUSH(w) \
+ C4_SUPPRESS_WARNING_MSVC_PUSH \
+ C4_SUPPRESS_WARNING_MSVC(w)
+#else // C4_MSVC
+#define C4_SUPPRESS_WARNING_MSVC_PUSH
+#define C4_SUPPRESS_WARNING_MSVC(w)
+#define C4_SUPPRESS_WARNING_MSVC_POP
+#define C4_SUPPRESS_WARNING_MSVC_WITH_PUSH(w)
+#endif // C4_MSVC
+
+
+#ifdef C4_CLANG
+#define C4_PRAGMA_TO_STR(x) _Pragma(#x)
+#define C4_SUPPRESS_WARNING_CLANG_PUSH _Pragma("clang diagnostic push")
+#define C4_SUPPRESS_WARNING_CLANG(w) C4_PRAGMA_TO_STR(clang diagnostic ignored w)
+#define C4_SUPPRESS_WARNING_CLANG_POP _Pragma("clang diagnostic pop")
+#define C4_SUPPRESS_WARNING_CLANG_WITH_PUSH(w) \
+ C4_SUPPRESS_WARNING_CLANG_PUSH \
+ C4_SUPPRESS_WARNING_CLANG(w)
+#else // C4_CLANG
+#define C4_SUPPRESS_WARNING_CLANG_PUSH
+#define C4_SUPPRESS_WARNING_CLANG(w)
+#define C4_SUPPRESS_WARNING_CLANG_POP
+#define C4_SUPPRESS_WARNING_CLANG_WITH_PUSH(w)
+#endif // C4_CLANG
+
+
+#ifdef C4_GCC
+#define C4_PRAGMA_TO_STR(x) _Pragma(#x)
+#define C4_SUPPRESS_WARNING_GCC_PUSH _Pragma("GCC diagnostic push")
+#define C4_SUPPRESS_WARNING_GCC(w) C4_PRAGMA_TO_STR(GCC diagnostic ignored w)
+#define C4_SUPPRESS_WARNING_GCC_POP _Pragma("GCC diagnostic pop")
+#define C4_SUPPRESS_WARNING_GCC_WITH_PUSH(w) \
+ C4_SUPPRESS_WARNING_GCC_PUSH \
+ C4_SUPPRESS_WARNING_GCC(w)
+#else // C4_GCC
+#define C4_SUPPRESS_WARNING_GCC_PUSH
+#define C4_SUPPRESS_WARNING_GCC(w)
+#define C4_SUPPRESS_WARNING_GCC_POP
+#define C4_SUPPRESS_WARNING_GCC_WITH_PUSH(w)
+#endif // C4_GCC
+
+
+#define C4_SUPPRESS_WARNING_GCC_CLANG_PUSH \
+ C4_SUPPRESS_WARNING_GCC_PUSH \
+ C4_SUPPRESS_WARNING_CLANG_PUSH
+
+#define C4_SUPPRESS_WARNING_GCC_CLANG(w) \
+ C4_SUPPRESS_WARNING_GCC(w) \
+ C4_SUPPRESS_WARNING_CLANG(w)
+
+#define C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH(w) \
+ C4_SUPPRESS_WARNING_GCC_WITH_PUSH(w) \
+ C4_SUPPRESS_WARNING_CLANG_WITH_PUSH(w)
+
+#define C4_SUPPRESS_WARNING_GCC_CLANG_POP \
+ C4_SUPPRESS_WARNING_GCC_POP \
+ C4_SUPPRESS_WARNING_CLANG_POP
+
+} // namespace c4
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#endif
+
+#endif /* _C4_ERROR_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/error.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/memory_util.hpp
+// https://github.com/biojppm/c4core/src/c4/memory_util.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_MEMORY_UTIL_HPP_
+#define _C4_MEMORY_UTIL_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//#include "c4/config.hpp"
+#if !defined(C4_CONFIG_HPP_) && !defined(_C4_CONFIG_HPP_)
+#error "amalgamate: file c4/config.hpp must have been included at this point"
+#endif /* C4_CONFIG_HPP_ */
+
+
+//included above:
+//#include <string.h>
+
+/** @file memory_util.hpp Some memory utilities. */
+
+namespace c4 {
+
+/** set the given memory to zero */
+C4_ALWAYS_INLINE void mem_zero(void* mem, size_t num_bytes)
+{
+ memset(mem, 0, num_bytes);
+}
+/** set the given memory to zero */
+template<class T>
+C4_ALWAYS_INLINE void mem_zero(T* mem, size_t num_elms)
+{
+ memset(mem, 0, sizeof(T) * num_elms);
+}
+/** set the given memory to zero */
+template<class T>
+C4_ALWAYS_INLINE void mem_zero(T* mem)
+{
+ memset(mem, 0, sizeof(T));
+}
+
+bool mem_overlaps(void const* a, void const* b, size_t sza, size_t szb);
+
+void mem_repeat(void* dest, void const* pattern, size_t pattern_size, size_t num_times);
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+template<class T>
+bool is_aligned(T *ptr, size_t alignment=alignof(T))
+{
+ return (uintptr_t(ptr) & (alignment - 1)) == 0u;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// least significant bit
+
+/** least significant bit; this function is constexpr-14 because of the local
+ * variable */
+template<class I>
+C4_CONSTEXPR14 I lsb(I v)
+{
+ if(!v) return 0;
+ I b = 0;
+ while((v & I(1)) == I(0))
+ {
+ v >>= 1;
+ ++b;
+ }
+ return b;
+}
+
+namespace detail {
+
+template<class I, I val, I num_bits, bool finished>
+struct _lsb11;
+
+template<class I, I val, I num_bits>
+struct _lsb11< I, val, num_bits, false>
+{
+ enum : I { num = _lsb11<I, (val>>1), num_bits+I(1), (((val>>1)&I(1))!=I(0))>::num };
+};
+
+template<class I, I val, I num_bits>
+struct _lsb11<I, val, num_bits, true>
+{
+ enum : I { num = num_bits };
+};
+
+} // namespace detail
+
+
+/** TMP version of lsb(); this needs to be implemented with template
+ * meta-programming because C++11 cannot use a constexpr function with
+ * local variables
+ * @see lsb */
+template<class I, I number>
+struct lsb11
+{
+ static_assert(number != 0, "lsb: number must be nonzero");
+ enum : I { value = detail::_lsb11<I, number, 0, ((number&I(1))!=I(0))>::num};
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// most significant bit
+
+/** most significant bit; this function is constexpr-14 because of the local
+ * variable
+ * @todo implement faster version
+ * @see https://stackoverflow.com/questions/2589096/find-most-significant-bit-left-most-that-is-set-in-a-bit-array
+ */
+template<class I>
+C4_CONSTEXPR14 I msb(I v)
+{
+ // TODO:
+ //
+ //int n;
+ //if(input_num & uint64_t(0xffffffff00000000)) input_num >>= 32, n |= 32;
+ //if(input_num & uint64_t( 0xffff0000)) input_num >>= 16, n |= 16;
+ //if(input_num & uint64_t( 0xff00)) input_num >>= 8, n |= 8;
+ //if(input_num & uint64_t( 0xf0)) input_num >>= 4, n |= 4;
+ //if(input_num & uint64_t( 0xc)) input_num >>= 2, n |= 2;
+ //if(input_num & uint64_t( 0x2)) input_num >>= 1, n |= 1;
+ if(!v) return static_cast<I>(-1);
+ I b = 0;
+ while(v != 0)
+ {
+ v >>= 1;
+ ++b;
+ }
+ return b-1;
+}
+
+namespace detail {
+
+template<class I, I val, I num_bits, bool finished>
+struct _msb11;
+
+template<class I, I val, I num_bits>
+struct _msb11< I, val, num_bits, false>
+{
+ enum : I { num = _msb11<I, (val>>1), num_bits+I(1), ((val>>1)==I(0))>::num };
+};
+
+template<class I, I val, I num_bits>
+struct _msb11<I, val, num_bits, true>
+{
+ static_assert(val == 0, "bad implementation");
+ enum : I { num = num_bits-1 };
+};
+
+} // namespace detail
+
+
+/** TMP version of msb(); this needs to be implemented with template
+ * meta-programming because C++11 cannot use a constexpr function with
+ * local variables
+ * @see msb */
+template<class I, I number>
+struct msb11
+{
+ enum : I { value = detail::_msb11<I, number, 0, (number==I(0))>::num };
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** return a mask with all bits set [first_bit,last_bit[; this function
+ * is constexpr-14 because of the local variables */
+template<class I>
+C4_CONSTEXPR14 I contiguous_mask(I first_bit, I last_bit)
+{
+ I r = 0;
+ constexpr const I o = 1;
+ for(I i = first_bit; i < last_bit; ++i)
+ {
+ r |= (o << i);
+ }
+ return r;
+}
+
+
+namespace detail {
+
+template<class I, I val, I first, I last, bool finished>
+struct _ctgmsk11;
+
+template<class I, I val, I first, I last>
+struct _ctgmsk11< I, val, first, last, true>
+{
+ enum : I { value = _ctgmsk11<I, val|(I(1)<<first), first+I(1), last, (first+1!=last)>::value };
+};
+
+template<class I, I val, I first, I last>
+struct _ctgmsk11< I, val, first, last, false>
+{
+ enum : I { value = val };
+};
+
+} // namespace detail
+
+
+/** TMP version of contiguous_mask(); this needs to be implemented with template
+ * meta-programming because C++11 cannot use a constexpr function with
+ * local variables
+ * @see contiguous_mask */
+template<class I, I first_bit, I last_bit>
+struct contiguous_mask11
+{
+ enum : I { value = detail::_ctgmsk11<I, I(0), first_bit, last_bit, (first_bit!=last_bit)>::value };
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** use Empty Base Class Optimization to reduce the size of a pair of
+ * potentially empty types*/
+
+namespace detail {
+typedef enum {
+ tpc_same,
+ tpc_same_empty,
+ tpc_both_empty,
+ tpc_first_empty,
+ tpc_second_empty,
+ tpc_general
+} TightPairCase_e;
+
+template<class First, class Second>
+constexpr TightPairCase_e tpc_which_case()
+{
+ return std::is_same<First, Second>::value ?
+ std::is_empty<First>::value ?
+ tpc_same_empty
+ :
+ tpc_same
+ :
+ std::is_empty<First>::value && std::is_empty<Second>::value ?
+ tpc_both_empty
+ :
+ std::is_empty<First>::value ?
+ tpc_first_empty
+ :
+ std::is_empty<Second>::value ?
+ tpc_second_empty
+ :
+ tpc_general
+ ;
+}
+
+template<class First, class Second, TightPairCase_e Case>
+struct tight_pair
+{
+private:
+
+ First m_first;
+ Second m_second;
+
+public:
+
+ using first_type = First;
+ using second_type = Second;
+
+ tight_pair() : m_first(), m_second() {}
+ tight_pair(First const& f, Second const& s) : m_first(f), m_second(s) {}
+
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return m_first; }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return m_first; }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return m_second; }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return m_second; }
+};
+
+template<class First, class Second>
+struct tight_pair<First, Second, tpc_same_empty> : public First
+{
+ static_assert(std::is_same<First, Second>::value, "bad implementation");
+
+ using first_type = First;
+ using second_type = Second;
+
+ tight_pair() : First() {}
+ tight_pair(First const& f, Second const& /*s*/) : First(f) {}
+
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return static_cast<First &>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return static_cast<First const&>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return reinterpret_cast<Second &>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return reinterpret_cast<Second const&>(*this); }
+};
+
+template<class First, class Second>
+struct tight_pair<First, Second, tpc_both_empty> : public First, public Second
+{
+ using first_type = First;
+ using second_type = Second;
+
+ tight_pair() : First(), Second() {}
+ tight_pair(First const& f, Second const& s) : First(f), Second(s) {}
+
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return static_cast<First &>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return static_cast<First const&>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return static_cast<Second &>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return static_cast<Second const&>(*this); }
+};
+
+template<class First, class Second>
+struct tight_pair<First, Second, tpc_same> : public First
+{
+ Second m_second;
+
+ using first_type = First;
+ using second_type = Second;
+
+ tight_pair() : First() {}
+ tight_pair(First const& f, Second const& s) : First(f), m_second(s) {}
+
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return static_cast<First &>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return static_cast<First const&>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return m_second; }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return m_second; }
+};
+
+template<class First, class Second>
+struct tight_pair<First, Second, tpc_first_empty> : public First
+{
+ Second m_second;
+
+ using first_type = First;
+ using second_type = Second;
+
+ tight_pair() : First(), m_second() {}
+ tight_pair(First const& f, Second const& s) : First(f), m_second(s) {}
+
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return static_cast<First &>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return static_cast<First const&>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return m_second; }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return m_second; }
+};
+
+template<class First, class Second>
+struct tight_pair<First, Second, tpc_second_empty> : public Second
+{
+ First m_first;
+
+ using first_type = First;
+ using second_type = Second;
+
+ tight_pair() : Second(), m_first() {}
+ tight_pair(First const& f, Second const& s) : Second(s), m_first(f) {}
+
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First & first () { return m_first; }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 First const& first () const { return m_first; }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second & second() { return static_cast<Second &>(*this); }
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 Second const& second() const { return static_cast<Second const&>(*this); }
+};
+
+} // namespace detail
+
+template<class First, class Second>
+using tight_pair = detail::tight_pair<First, Second, detail::tpc_which_case<First,Second>()>;
+
+} // namespace c4
+
+#endif /* _C4_MEMORY_UTIL_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/memory_util.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/memory_resource.hpp
+// https://github.com/biojppm/c4core/src/c4/memory_resource.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_MEMORY_RESOURCE_HPP_
+#define _C4_MEMORY_RESOURCE_HPP_
+
+/** @file memory_resource.hpp Provides facilities to allocate typeless
+ * memory, via the memory resource model consecrated with C++17. */
+
+/** @defgroup memory memory utilities */
+
+/** @defgroup raw_memory_alloc Raw memory allocation
+ * @ingroup memory
+ */
+
+/** @defgroup memory_resources Memory resources
+ * @ingroup memory
+ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//#include "c4/config.hpp"
+#if !defined(C4_CONFIG_HPP_) && !defined(_C4_CONFIG_HPP_)
+#error "amalgamate: file c4/config.hpp must have been included at this point"
+#endif /* C4_CONFIG_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+
+namespace c4 {
+
+// need these forward decls here
+struct MemoryResource;
+struct MemoryResourceMalloc;
+struct MemoryResourceStack;
+MemoryResourceMalloc* get_memory_resource_malloc();
+MemoryResourceStack* get_memory_resource_stack();
+namespace detail { MemoryResource*& get_memory_resource(); }
+
+
+// c-style allocation ---------------------------------------------------------
+
+// this API provides aligned allocation functions.
+// These functions forward the call to a user-modifiable function.
+
+
+// aligned allocation.
+
+/** Aligned allocation. Merely calls the current get_aalloc() function.
+ * @see get_aalloc()
+ * @ingroup raw_memory_alloc */
+void* aalloc(size_t sz, size_t alignment);
+
+/** Aligned free. Merely calls the current get_afree() function.
+ * @see get_afree()
+ * @ingroup raw_memory_alloc */
+void afree(void* ptr);
+
+/** Aligned reallocation. Merely calls the current get_arealloc() function.
+ * @see get_arealloc()
+ * @ingroup raw_memory_alloc */
+void* arealloc(void* ptr, size_t oldsz, size_t newsz, size_t alignment);
+
+
+// allocation setup facilities.
+
+/** Function pointer type for aligned allocation
+ * @see set_aalloc()
+ * @ingroup raw_memory_alloc */
+using aalloc_pfn = void* (*)(size_t size, size_t alignment);
+
+/** Function pointer type for aligned deallocation
+ * @see set_afree()
+ * @ingroup raw_memory_alloc */
+using afree_pfn = void (*)(void *ptr);
+
+/** Function pointer type for aligned reallocation
+ * @see set_arealloc()
+ * @ingroup raw_memory_alloc */
+using arealloc_pfn = void* (*)(void *ptr, size_t oldsz, size_t newsz, size_t alignment);
+
+
+// allocation function pointer setters/getters
+
+/** Set the global aligned allocation function.
+ * @see aalloc()
+ * @see get_aalloc()
+ * @ingroup raw_memory_alloc */
+void set_aalloc(aalloc_pfn fn);
+
+/** Set the global aligned deallocation function.
+ * @see afree()
+ * @see get_afree()
+ * @ingroup raw_memory_alloc */
+void set_afree(afree_pfn fn);
+
+/** Set the global aligned reallocation function.
+ * @see arealloc()
+ * @see get_arealloc()
+ * @ingroup raw_memory_alloc */
+void set_arealloc(arealloc_pfn fn);
+
+
+/** Get the global aligned reallocation function.
+ * @see arealloc()
+ * @ingroup raw_memory_alloc */
+aalloc_pfn get_aalloc();
+
+/** Get the global aligned deallocation function.
+ * @see afree()
+ * @ingroup raw_memory_alloc */
+afree_pfn get_afree();
+
+/** Get the global aligned reallocation function.
+ * @see arealloc()
+ * @ingroup raw_memory_alloc */
+arealloc_pfn get_arealloc();
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// c++-style allocation -------------------------------------------------------
+
+/** C++17-style memory_resource base class. See http://en.cppreference.com/w/cpp/experimental/memory_resource
+ * @ingroup memory_resources */
+struct MemoryResource
+{
+ const char *name = nullptr;
+ virtual ~MemoryResource() {}
+
+ void* allocate(size_t sz, size_t alignment=alignof(max_align_t), void *hint=nullptr)
+ {
+ void *mem = this->do_allocate(sz, alignment, hint);
+ C4_CHECK_MSG(mem != nullptr, "could not allocate %lu bytes", sz);
+ return mem;
+ }
+
+ void* reallocate(void* ptr, size_t oldsz, size_t newsz, size_t alignment=alignof(max_align_t))
+ {
+ void *mem = this->do_reallocate(ptr, oldsz, newsz, alignment);
+ C4_CHECK_MSG(mem != nullptr, "could not reallocate from %lu to %lu bytes", oldsz, newsz);
+ return mem;
+ }
+
+ void deallocate(void* ptr, size_t sz, size_t alignment=alignof(max_align_t))
+ {
+ this->do_deallocate(ptr, sz, alignment);
+ }
+
+protected:
+
+ virtual void* do_allocate(size_t sz, size_t alignment, void* hint) = 0;
+ virtual void* do_reallocate(void* ptr, size_t oldsz, size_t newsz, size_t alignment) = 0;
+ virtual void do_deallocate(void* ptr, size_t sz, size_t alignment) = 0;
+
+};
+
+/** get the current global memory resource. To avoid static initialization
+ * order problems, this is implemented using a function call to ensure
+ * that it is available when first used.
+ * @ingroup memory_resources */
+C4_ALWAYS_INLINE MemoryResource* get_memory_resource()
+{
+ return detail::get_memory_resource();
+}
+
+/** set the global memory resource
+ * @ingroup memory_resources */
+C4_ALWAYS_INLINE void set_memory_resource(MemoryResource* mr)
+{
+ C4_ASSERT(mr != nullptr);
+ detail::get_memory_resource() = mr;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** A c4::aalloc-based memory resource. Thread-safe if the implementation
+ * called by c4::aalloc() is safe.
+ * @ingroup memory_resources */
+struct MemoryResourceMalloc : public MemoryResource
+{
+
+ MemoryResourceMalloc() { name = "malloc"; }
+ virtual ~MemoryResourceMalloc() override {}
+
+protected:
+
+ virtual void* do_allocate(size_t sz, size_t alignment, void *hint) override
+ {
+ C4_UNUSED(hint);
+ return c4::aalloc(sz, alignment);
+ }
+
+ virtual void do_deallocate(void* ptr, size_t sz, size_t alignment) override
+ {
+ C4_UNUSED(sz);
+ C4_UNUSED(alignment);
+ c4::afree(ptr);
+ }
+
+ virtual void* do_reallocate(void* ptr, size_t oldsz, size_t newsz, size_t alignment) override
+ {
+ return c4::arealloc(ptr, oldsz, newsz, alignment);
+ }
+
+};
+
+/** returns a malloc-based memory resource
+ * @ingroup memory_resources */
+C4_ALWAYS_INLINE MemoryResourceMalloc* get_memory_resource_malloc()
+{
+ /** @todo use a nifty counter:
+ * https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Nifty_Counter */
+ static MemoryResourceMalloc mr;
+ return &mr;
+}
+
+namespace detail {
+C4_ALWAYS_INLINE MemoryResource* & get_memory_resource()
+{
+ /** @todo use a nifty counter:
+ * https://en.wikibooks.org/wiki/More_C%2B%2B_Idioms/Nifty_Counter */
+ thread_local static MemoryResource* mr = get_memory_resource_malloc();
+ return mr;
+}
+} // namespace detail
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+namespace detail {
+
+/** Allows a memory resource to obtain its memory from another memory resource.
+ * @ingroup memory_resources */
+struct DerivedMemoryResource : public MemoryResource
+{
+public:
+
+ DerivedMemoryResource(MemoryResource *mr_=nullptr) : m_local(mr_ ? mr_ : get_memory_resource()) {}
+
+private:
+
+ MemoryResource *m_local;
+
+protected:
+
+ virtual void* do_allocate(size_t sz, size_t alignment, void* hint) override
+ {
+ return m_local->allocate(sz, alignment, hint);
+ }
+
+ virtual void* do_reallocate(void* ptr, size_t oldsz, size_t newsz, size_t alignment) override
+ {
+ return m_local->reallocate(ptr, oldsz, newsz, alignment);
+ }
+
+ virtual void do_deallocate(void* ptr, size_t sz, size_t alignment) override
+ {
+ return m_local->deallocate(ptr, sz, alignment);
+ }
+};
+
+/** Provides common facilities for memory resource consisting of a single memory block
+ * @ingroup memory_resources */
+struct _MemoryResourceSingleChunk : public DerivedMemoryResource
+{
+
+ C4_NO_COPY_OR_MOVE(_MemoryResourceSingleChunk);
+
+ using impl_type = DerivedMemoryResource;
+
+public:
+
+ _MemoryResourceSingleChunk(MemoryResource *impl=nullptr) : DerivedMemoryResource(impl) { name = "linear_malloc"; }
+
+ /** initialize with owned memory, allocated from the given (or the global) memory resource */
+ _MemoryResourceSingleChunk(size_t sz, MemoryResource *impl=nullptr) : _MemoryResourceSingleChunk(impl) { acquire(sz); }
+ /** initialize with borrowed memory */
+ _MemoryResourceSingleChunk(void *mem, size_t sz) : _MemoryResourceSingleChunk() { acquire(mem, sz); }
+
+ virtual ~_MemoryResourceSingleChunk() override { release(); }
+
+public:
+
+ void const* mem() const { return m_mem; }
+
+ size_t capacity() const { return m_size; }
+ size_t size() const { return m_pos; }
+ size_t slack() const { C4_ASSERT(m_size >= m_pos); return m_size - m_pos; }
+
+public:
+
+ char *m_mem{nullptr};
+ size_t m_size{0};
+ size_t m_pos{0};
+ bool m_owner;
+
+public:
+
+ /** set the internal pointer to the beginning of the linear buffer */
+ void clear() { m_pos = 0; }
+
+ /** initialize with owned memory, allocated from the global memory resource */
+ void acquire(size_t sz);
+ /** initialize with borrowed memory */
+ void acquire(void *mem, size_t sz);
+ /** release the memory */
+ void release();
+
+};
+
+} // namespace detail
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** provides a linear memory resource. Allocates incrementally from a linear
+ * buffer, without ever deallocating. Deallocations are a no-op, and the
+ * memory is freed only when the resource is release()d. The memory used by
+ * this object can be either owned or borrowed. When borrowed, no calls to
+ * malloc/free take place.
+ *
+ * @ingroup memory_resources */
+struct MemoryResourceLinear : public detail::_MemoryResourceSingleChunk
+{
+
+ C4_NO_COPY_OR_MOVE(MemoryResourceLinear);
+
+public:
+
+ using detail::_MemoryResourceSingleChunk::_MemoryResourceSingleChunk;
+
+protected:
+
+ virtual void* do_allocate(size_t sz, size_t alignment, void *hint) override;
+ virtual void do_deallocate(void* ptr, size_t sz, size_t alignment) override;
+ virtual void* do_reallocate(void* ptr, size_t oldsz, size_t newsz, size_t alignment) override;
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** provides a stack-type malloc-based memory resource.
+ * @ingroup memory_resources */
+struct MemoryResourceStack : public detail::_MemoryResourceSingleChunk
+{
+
+ C4_NO_COPY_OR_MOVE(MemoryResourceStack);
+
+public:
+
+ using detail::_MemoryResourceSingleChunk::_MemoryResourceSingleChunk;
+
+protected:
+
+ virtual void* do_allocate(size_t sz, size_t alignment, void *hint) override;
+ virtual void do_deallocate(void* ptr, size_t sz, size_t alignment) override;
+ virtual void* do_reallocate(void* ptr, size_t oldsz, size_t newsz, size_t alignment) override;
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** provides a linear array-based memory resource.
+ * @see MemoryResourceLinear
+ * @ingroup memory_resources */
+template<size_t N>
+struct MemoryResourceLinearArr : public MemoryResourceLinear
+{
+ #ifdef _MSC_VER
+ #pragma warning(push)
+ #pragma warning(disable: 4324) // structure was padded due to alignment specifier
+ #endif
+ alignas(alignof(max_align_t)) char m_arr[N];
+ #ifdef _MSC_VER
+ #pragma warning(pop)
+ #endif
+ MemoryResourceLinearArr() : MemoryResourceLinear(m_arr, N) { name = "linear_arr"; }
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+struct AllocationCounts
+{
+ struct Item
+ {
+ ssize_t allocs;
+ ssize_t size;
+
+ void add(size_t sz)
+ {
+ ++allocs;
+ size += static_cast<ssize_t>(sz);
+ }
+ void rem(size_t sz)
+ {
+ --allocs;
+ size -= static_cast<ssize_t>(sz);
+ }
+ Item max(Item const& that) const
+ {
+ Item r(*this);
+ r.allocs = r.allocs > that.allocs ? r.allocs : that.allocs;
+ r.size = r.size > that.size ? r.size : that.size;
+ return r;
+ }
+ };
+
+ Item curr = {0, 0};
+ Item total = {0, 0};
+ Item max = {0, 0};
+
+ void clear_counts()
+ {
+ curr = {0, 0};
+ total = {0, 0};
+ max = {0, 0};
+ }
+
+ void update(AllocationCounts const& that)
+ {
+ curr.allocs += that.curr.allocs;
+ curr.size += that.curr.size;
+ total.allocs += that.total.allocs;
+ total.size += that.total.size;
+ max.allocs += that.max.allocs;
+ max.size += that.max.size;
+ }
+
+ void add_counts(void* ptr, size_t sz)
+ {
+ if(ptr == nullptr) return;
+ curr.add(sz);
+ total.add(sz);
+ max = max.max(curr);
+ }
+
+ void rem_counts(void *ptr, size_t sz)
+ {
+ if(ptr == nullptr) return;
+ curr.rem(sz);
+ }
+
+ AllocationCounts operator- (AllocationCounts const& that) const
+ {
+ AllocationCounts r(*this);
+ r.curr.allocs -= that.curr.allocs;
+ r.curr.size -= that.curr.size;
+ r.total.allocs -= that.total.allocs;
+ r.total.size -= that.total.size;
+ r.max.allocs -= that.max.allocs;
+ r.max.size -= that.max.size;
+ return r;
+ }
+
+ AllocationCounts operator+ (AllocationCounts const& that) const
+ {
+ AllocationCounts r(*this);
+ r.curr.allocs += that.curr.allocs;
+ r.curr.size += that.curr.size;
+ r.total.allocs += that.total.allocs;
+ r.total.size += that.total.size;
+ r.max.allocs += that.max.allocs;
+ r.max.size += that.max.size;
+ return r;
+ }
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** a MemoryResource which latches onto another MemoryResource
+ * and counts allocations and sizes.
+ * @ingroup memory_resources */
+class MemoryResourceCounts : public MemoryResource
+{
+public:
+
+ MemoryResourceCounts() : m_resource(get_memory_resource())
+ {
+ C4_ASSERT(m_resource != this);
+ name = "MemoryResourceCounts";
+ }
+ MemoryResourceCounts(MemoryResource *res) : m_resource(res)
+ {
+ C4_ASSERT(m_resource != this);
+ name = "MemoryResourceCounts";
+ }
+
+ MemoryResource *resource() { return m_resource; }
+ AllocationCounts const& counts() const { return m_counts; }
+
+protected:
+
+ MemoryResource *m_resource;
+ AllocationCounts m_counts;
+
+protected:
+
+ virtual void* do_allocate(size_t sz, size_t alignment, void * /*hint*/) override
+ {
+ void *ptr = m_resource->allocate(sz, alignment);
+ m_counts.add_counts(ptr, sz);
+ return ptr;
+ }
+
+ virtual void do_deallocate(void* ptr, size_t sz, size_t alignment) override
+ {
+ m_counts.rem_counts(ptr, sz);
+ m_resource->deallocate(ptr, sz, alignment);
+ }
+
+ virtual void* do_reallocate(void* ptr, size_t oldsz, size_t newsz, size_t alignment) override
+ {
+ m_counts.rem_counts(ptr, oldsz);
+ void* nptr = m_resource->reallocate(ptr, oldsz, newsz, alignment);
+ m_counts.add_counts(nptr, newsz);
+ return nptr;
+ }
+
+};
+
+//-----------------------------------------------------------------------------
+/** RAII class which binds a memory resource with a scope duration.
+ * @ingroup memory_resources */
+struct ScopedMemoryResource
+{
+ MemoryResource *m_original;
+
+ ScopedMemoryResource(MemoryResource *r)
+ :
+ m_original(get_memory_resource())
+ {
+ set_memory_resource(r);
+ }
+
+ ~ScopedMemoryResource()
+ {
+ set_memory_resource(m_original);
+ }
+};
+
+//-----------------------------------------------------------------------------
+/** RAII class which counts allocations and frees inside a scope. Can
+ * optionally set also the memory resource to be used.
+ * @ingroup memory_resources */
+struct ScopedMemoryResourceCounts
+{
+ MemoryResourceCounts mr;
+
+ ScopedMemoryResourceCounts() : mr()
+ {
+ set_memory_resource(&mr);
+ }
+ ScopedMemoryResourceCounts(MemoryResource *m) : mr(m)
+ {
+ set_memory_resource(&mr);
+ }
+ ~ScopedMemoryResourceCounts()
+ {
+ set_memory_resource(mr.resource());
+ }
+};
+
+} // namespace c4
+
+#endif /* _C4_MEMORY_RESOURCE_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/memory_resource.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/ctor_dtor.hpp
+// https://github.com/biojppm/c4core/src/c4/ctor_dtor.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_CTOR_DTOR_HPP_
+#define _C4_CTOR_DTOR_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/preprocessor.hpp
+//#include "c4/preprocessor.hpp"
+#if !defined(C4_PREPROCESSOR_HPP_) && !defined(_C4_PREPROCESSOR_HPP_)
+#error "amalgamate: file c4/preprocessor.hpp must have been included at this point"
+#endif /* C4_PREPROCESSOR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/language.hpp
+//#include "c4/language.hpp"
+#if !defined(C4_LANGUAGE_HPP_) && !defined(_C4_LANGUAGE_HPP_)
+#error "amalgamate: file c4/language.hpp must have been included at this point"
+#endif /* C4_LANGUAGE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/memory_util.hpp
+//#include "c4/memory_util.hpp"
+#if !defined(C4_MEMORY_UTIL_HPP_) && !defined(_C4_MEMORY_UTIL_HPP_)
+#error "amalgamate: file c4/memory_util.hpp must have been included at this point"
+#endif /* C4_MEMORY_UTIL_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+
+//included above:
+//#include <type_traits>
+//included above:
+//#include <utility> // std::forward
+
+/** @file ctor_dtor.hpp object construction and destruction facilities.
+ * Some of these are not yet available in C++11. */
+
+namespace c4 {
+
+/** default-construct an object, trivial version */
+template <class U> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_default_constructible<U>::value, void>::type
+construct(U *ptr) noexcept
+{
+ memset(ptr, 0, sizeof(U));
+}
+/** default-construct an object, non-trivial version */
+template<class U> C4_ALWAYS_INLINE typename std ::enable_if< ! std::is_trivially_default_constructible<U>::value, void>::type
+construct(U* ptr) noexcept
+{
+ new ((void*)ptr) U();
+}
+
+/** default-construct n objects, trivial version */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_default_constructible<U>::value, void>::type
+construct_n(U* ptr, I n) noexcept
+{
+ memset(ptr, 0, n * sizeof(U));
+}
+/** default-construct n objects, non-trivial version */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_default_constructible<U>::value, void>::type
+construct_n(U* ptr, I n) noexcept
+{
+ for(I i = 0; i < n; ++i)
+ {
+ new ((void*)(ptr + i)) U();
+ }
+}
+
+#ifdef __clang__
+# pragma clang diagnostic push
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# if __GNUC__ >= 6
+# pragma GCC diagnostic ignored "-Wnull-dereference"
+# endif
+#endif
+
+template<class U, class ...Args>
+inline void construct(U* ptr, Args&&... args)
+{
+ new ((void*)ptr) U(std::forward<Args>(args)...);
+}
+template<class U, class I, class ...Args>
+inline void construct_n(U* ptr, I n, Args&&... args)
+{
+ for(I i = 0; i < n; ++i)
+ {
+ new ((void*)(ptr + i)) U(args...);
+ }
+}
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+
+//-----------------------------------------------------------------------------
+// copy-construct
+
+template<class U> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_copy_constructible<U>::value, void>::type
+copy_construct(U* dst, U const* src) noexcept
+{
+ C4_ASSERT(dst != src);
+ memcpy(dst, src, sizeof(U));
+}
+template<class U> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_copy_constructible<U>::value, void>::type
+copy_construct(U* dst, U const* src)
+{
+ C4_ASSERT(dst != src);
+ new ((void*)dst) U(*src);
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_copy_constructible<U>::value, void>::type
+copy_construct_n(U* dst, U const* src, I n) noexcept
+{
+ C4_ASSERT(dst != src);
+ memcpy(dst, src, n * sizeof(U));
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_copy_constructible<U>::value, void>::type
+copy_construct_n(U* dst, U const* src, I n)
+{
+ C4_ASSERT(dst != src);
+ for(I i = 0; i < n; ++i)
+ {
+ new ((void*)(dst + i)) U(*(src + i));
+ }
+}
+
+template<class U> C4_ALWAYS_INLINE typename std::enable_if<std::is_scalar<U>::value, void>::type
+copy_construct(U* dst, U src) noexcept // pass by value for scalar types
+{
+ *dst = src;
+}
+template<class U> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_scalar<U>::value, void>::type
+copy_construct(U* dst, U const& src) // pass by reference for non-scalar types
+{
+ C4_ASSERT(dst != &src);
+ new ((void*)dst) U(src);
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_scalar<U>::value, void>::type
+copy_construct_n(U* dst, U src, I n) noexcept // pass by value for scalar types
+{
+ for(I i = 0; i < n; ++i)
+ {
+ dst[i] = src;
+ }
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_scalar<U>::value, void>::type
+copy_construct_n(U* dst, U const& src, I n) // pass by reference for non-scalar types
+{
+ C4_ASSERT(dst != &src);
+ for(I i = 0; i < n; ++i)
+ {
+ new ((void*)(dst + i)) U(src);
+ }
+}
+
+template<class U, size_t N>
+C4_ALWAYS_INLINE void copy_construct(U (&dst)[N], U const (&src)[N]) noexcept
+{
+ copy_construct_n(dst, src, N);
+}
+
+//-----------------------------------------------------------------------------
+// copy-assign
+
+template<class U> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_copy_assignable<U>::value, void>::type
+copy_assign(U* dst, U const* src) noexcept
+{
+ C4_ASSERT(dst != src);
+ memcpy(dst, src, sizeof(U));
+}
+template<class U> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_copy_assignable<U>::value, void>::type
+copy_assign(U* dst, U const* src) noexcept
+{
+ C4_ASSERT(dst != src);
+ *dst = *src;
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_copy_assignable<U>::value, void>::type
+copy_assign_n(U* dst, U const* src, I n) noexcept
+{
+ C4_ASSERT(dst != src);
+ memcpy(dst, src, n * sizeof(U));
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_copy_assignable<U>::value, void>::type
+copy_assign_n(U* dst, U const* src, I n) noexcept
+{
+ C4_ASSERT(dst != src);
+ for(I i = 0; i < n; ++i)
+ {
+ dst[i] = src[i];
+ }
+}
+
+template<class U> C4_ALWAYS_INLINE typename std::enable_if<std::is_scalar<U>::value, void>::type
+copy_assign(U* dst, U src) noexcept // pass by value for scalar types
+{
+ *dst = src;
+}
+template<class U> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_scalar<U>::value, void>::type
+copy_assign(U* dst, U const& src) noexcept // pass by reference for non-scalar types
+{
+ C4_ASSERT(dst != &src);
+ *dst = src;
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_scalar<U>::value, void>::type
+copy_assign_n(U* dst, U src, I n) noexcept // pass by value for scalar types
+{
+ for(I i = 0; i < n; ++i)
+ {
+ dst[i] = src;
+ }
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_scalar<U>::value, void>::type
+copy_assign_n(U* dst, U const& src, I n) noexcept // pass by reference for non-scalar types
+{
+ C4_ASSERT(dst != &src);
+ for(I i = 0; i < n; ++i)
+ {
+ dst[i] = src;
+ }
+}
+
+template<class U, size_t N>
+C4_ALWAYS_INLINE void copy_assign(U (&dst)[N], U const (&src)[N]) noexcept
+{
+ copy_assign_n(dst, src, N);
+}
+
+//-----------------------------------------------------------------------------
+// move-construct
+
+template<class U> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_move_constructible<U>::value, void>::type
+move_construct(U* dst, U* src) noexcept
+{
+ C4_ASSERT(dst != src);
+ memcpy(dst, src, sizeof(U));
+}
+template<class U> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_move_constructible<U>::value, void>::type
+move_construct(U* dst, U* src) noexcept
+{
+ C4_ASSERT(dst != src);
+ new ((void*)dst) U(std::move(*src));
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_move_constructible<U>::value, void>::type
+move_construct_n(U* dst, U* src, I n) noexcept
+{
+ C4_ASSERT(dst != src);
+ memcpy(dst, src, n * sizeof(U));
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_move_constructible<U>::value, void>::type
+move_construct_n(U* dst, U* src, I n) noexcept
+{
+ C4_ASSERT(dst != src);
+ for(I i = 0; i < n; ++i)
+ {
+ new ((void*)(dst + i)) U(std::move(src[i]));
+ }
+}
+
+//-----------------------------------------------------------------------------
+// move-assign
+
+template<class U> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_move_assignable<U>::value, void>::type
+move_assign(U* dst, U* src) noexcept
+{
+ C4_ASSERT(dst != src);
+ memcpy(dst, src, sizeof(U));
+}
+template<class U> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_move_assignable<U>::value, void>::type
+move_assign(U* dst, U* src) noexcept
+{
+ C4_ASSERT(dst != src);
+ *dst = std::move(*src);
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_move_assignable<U>::value, void>::type
+move_assign_n(U* dst, U* src, I n) noexcept
+{
+ C4_ASSERT(dst != src);
+ memcpy(dst, src, n * sizeof(U));
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_move_assignable<U>::value, void>::type
+move_assign_n(U* dst, U* src, I n) noexcept
+{
+ C4_ASSERT(dst != src);
+ for(I i = 0; i < n; ++i)
+ {
+ *(dst + i) = std::move(*(src + i));
+ }
+}
+
+//-----------------------------------------------------------------------------
+// destroy
+
+template<class U> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_destructible<U>::value, void>::type
+destroy(U* ptr) noexcept
+{
+ C4_UNUSED(ptr); // nothing to do
+}
+template<class U> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_destructible<U>::value, void>::type
+destroy(U* ptr) noexcept
+{
+ ptr->~U();
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_destructible<U>::value, void>::type
+destroy_n(U* ptr, I n) noexcept
+{
+ C4_UNUSED(ptr);
+ C4_UNUSED(n); // nothing to do
+}
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_destructible<U>::value, void>::type
+destroy_n(U* ptr, I n) noexcept
+{
+ for(I i = 0; i <n; ++i)
+ {
+ ptr[i].~U();
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+/** makes room at the beginning of buf, which has a current size of n */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_move_constructible<U>::value, void>::type
+make_room(U *buf, I bufsz, I room) C4_NOEXCEPT_A
+{
+ C4_ASSERT(bufsz >= 0 && room >= 0);
+ if(room >= bufsz)
+ {
+ memcpy (buf + room, buf, bufsz * sizeof(U));
+ }
+ else
+ {
+ memmove(buf + room, buf, bufsz * sizeof(U));
+ }
+}
+/** makes room at the beginning of buf, which has a current size of bufsz */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_move_constructible<U>::value, void>::type
+make_room(U *buf, I bufsz, I room) C4_NOEXCEPT_A
+{
+ C4_ASSERT(bufsz >= 0 && room >= 0);
+ if(room >= bufsz)
+ {
+ for(I i = 0; i < bufsz; ++i)
+ {
+ new ((void*)(buf + (i + room))) U(std::move(buf[i]));
+ }
+ }
+ else
+ {
+ for(I i = 0; i < bufsz; ++i)
+ {
+ I w = bufsz-1 - i; // do a backwards loop
+ new ((void*)(buf + (w + room))) U(std::move(buf[w]));
+ }
+ }
+}
+
+/** make room to the right of pos */
+template<class U, class I>
+C4_ALWAYS_INLINE void make_room(U *buf, I bufsz, I currsz, I pos, I room)
+{
+ C4_ASSERT(pos >= 0 && pos <= currsz);
+ C4_ASSERT(currsz <= bufsz);
+ C4_ASSERT(room + currsz <= bufsz);
+ C4_UNUSED(bufsz);
+ make_room(buf + pos, currsz - pos, room);
+}
+
+
+/** make room to the right of pos, copying to the beginning of a different buffer */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_move_constructible<U>::value, void>::type
+make_room(U *dst, U const* src, I srcsz, I room, I pos) C4_NOEXCEPT_A
+{
+ C4_ASSERT(srcsz >= 0 && room >= 0 && pos >= 0);
+ C4_ASSERT(pos < srcsz || (pos == 0 && srcsz == 0));
+ memcpy(dst , src , pos * sizeof(U));
+ memcpy(dst + room + pos, src + pos, (srcsz - pos) * sizeof(U));
+}
+/** make room to the right of pos, copying to the beginning of a different buffer */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_move_constructible<U>::value, void>::type
+make_room(U *dst, U const* src, I srcsz, I room, I pos)
+{
+ C4_ASSERT(srcsz >= 0 && room >= 0 && pos >= 0);
+ C4_ASSERT(pos < srcsz || (pos == 0 && srcsz == 0));
+ for(I i = 0; i < pos; ++i)
+ {
+ new ((void*)(dst + i)) U(std::move(src[i]));
+ }
+ src += pos;
+ dst += room + pos;
+ for(I i = 0, e = srcsz - pos; i < e; ++i)
+ {
+ new ((void*)(dst + i)) U(std::move(src[i]));
+ }
+}
+
+template<class U, class I>
+C4_ALWAYS_INLINE void make_room
+(
+ U * dst, I dstsz,
+ U const* src, I srcsz,
+ I room, I pos
+)
+{
+ C4_ASSERT(pos >= 0 && pos < srcsz || (srcsz == 0 && pos == 0));
+ C4_ASSERT(pos >= 0 && pos < dstsz || (dstsz == 0 && pos == 0));
+ C4_ASSERT(srcsz+room <= dstsz);
+ C4_UNUSED(dstsz);
+ make_room(dst, src, srcsz, room, pos);
+}
+
+
+//-----------------------------------------------------------------------------
+/** destroy room at the beginning of buf, which has a current size of n */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_scalar<U>::value || (std::is_standard_layout<U>::value && std::is_trivial<U>::value), void>::type
+destroy_room(U *buf, I n, I room) C4_NOEXCEPT_A
+{
+ C4_ASSERT(n >= 0 && room >= 0);
+ C4_ASSERT(room <= n);
+ if(room < n)
+ {
+ memmove(buf, buf + room, (n - room) * sizeof(U));
+ }
+ else
+ {
+ // nothing to do - no need to destroy scalar types
+ }
+}
+/** destroy room at the beginning of buf, which has a current size of n */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! (std::is_scalar<U>::value || (std::is_standard_layout<U>::value && std::is_trivial<U>::value)), void>::type
+destroy_room(U *buf, I n, I room)
+{
+ C4_ASSERT(n >= 0 && room >= 0);
+ C4_ASSERT(room <= n);
+ if(room < n)
+ {
+ for(I i = 0, e = n - room; i < e; ++i)
+ {
+ buf[i] = std::move(buf[i + room]);
+ }
+ }
+ else
+ {
+ for(I i = 0; i < n; ++i)
+ {
+ buf[i].~U();
+ }
+ }
+}
+
+/** destroy room to the right of pos, copying to a different buffer */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if<std::is_trivially_move_constructible<U>::value, void>::type
+destroy_room(U *dst, U const* src, I n, I room, I pos) C4_NOEXCEPT_A
+{
+ C4_ASSERT(n >= 0 && room >= 0 && pos >= 0);
+ C4_ASSERT(pos <n);
+ C4_ASSERT(pos + room <= n);
+ memcpy(dst, src, pos * sizeof(U));
+ memcpy(dst + pos, src + room + pos, (n - pos - room) * sizeof(U));
+}
+/** destroy room to the right of pos, copying to a different buffer */
+template<class U, class I> C4_ALWAYS_INLINE typename std::enable_if< ! std::is_trivially_move_constructible<U>::value, void>::type
+destroy_room(U *dst, U const* src, I n, I room, I pos)
+{
+ C4_ASSERT(n >= 0 && room >= 0 && pos >= 0);
+ C4_ASSERT(pos < n);
+ C4_ASSERT(pos + room <= n);
+ for(I i = 0; i < pos; ++i)
+ {
+ new ((void*)(dst + i)) U(std::move(src[i]));
+ }
+ src += room + pos;
+ dst += pos;
+ for(I i = 0, e = n - pos - room; i < e; ++i)
+ {
+ new ((void*)(dst + i)) U(std::move(src[i]));
+ }
+}
+
+} // namespace c4
+
+#undef _C4REQUIRE
+
+#endif /* _C4_CTOR_DTOR_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/ctor_dtor.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/allocator.hpp
+// https://github.com/biojppm/c4core/src/c4/allocator.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_ALLOCATOR_HPP_
+#define _C4_ALLOCATOR_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/memory_resource.hpp
+//#include "c4/memory_resource.hpp"
+#if !defined(C4_MEMORY_RESOURCE_HPP_) && !defined(_C4_MEMORY_RESOURCE_HPP_)
+#error "amalgamate: file c4/memory_resource.hpp must have been included at this point"
+#endif /* C4_MEMORY_RESOURCE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/ctor_dtor.hpp
+//#include "c4/ctor_dtor.hpp"
+#if !defined(C4_CTOR_DTOR_HPP_) && !defined(_C4_CTOR_DTOR_HPP_)
+#error "amalgamate: file c4/ctor_dtor.hpp must have been included at this point"
+#endif /* C4_CTOR_DTOR_HPP_ */
+
+
+#include <memory> // std::allocator_traits
+//included above:
+//#include <type_traits>
+
+/** @file allocator.hpp Contains classes to make typeful allocations (note
+ * that memory resources are typeless) */
+
+/** @defgroup mem_res_providers Memory resource providers
+ * @brief Policy classes which provide a memory resource for
+ * use in an allocator.
+ * @ingroup memory
+ */
+
+/** @defgroup allocators Allocators
+ * @brief Lightweight classes that act as handles to specific memory
+ * resources and provide typeful memory.
+ * @ingroup memory
+ */
+
+namespace c4 {
+
+namespace detail {
+template<class T> inline size_t size_for (size_t num_objs) noexcept { return num_objs * sizeof(T); }
+template< > inline size_t size_for<void>(size_t num_objs) noexcept { return num_objs; }
+} // namespace detail
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** provides a per-allocator memory resource
+ * @ingroup mem_res_providers */
+class MemRes
+{
+public:
+
+ MemRes() : m_resource(get_memory_resource()) {}
+ MemRes(MemoryResource* r) noexcept : m_resource(r ? r : get_memory_resource()) {}
+
+ inline MemoryResource* resource() const { return m_resource; }
+
+private:
+
+ MemoryResource* m_resource;
+
+};
+
+
+/** the allocators using this will default to the global memory resource
+ * @ingroup mem_res_providers */
+class MemResGlobal
+{
+public:
+
+ MemResGlobal() {}
+ MemResGlobal(MemoryResource* r) noexcept { C4_UNUSED(r); C4_ASSERT(r == get_memory_resource()); }
+
+ inline MemoryResource* resource() const { return get_memory_resource(); }
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+namespace detail {
+template<class MemRes>
+struct _AllocatorUtil;
+
+template<class T, class ...Args>
+struct has_no_alloc
+ : public std::integral_constant<bool,
+ !(std::uses_allocator<T, MemoryResource*>::value)
+ && std::is_constructible<T, Args...>::value> {};
+
+// std::uses_allocator_v<U, MemoryResource> && std::is_constructible<U, std::allocator_arg_t, MemoryResource*, Args...>
+// ie can construct(std::allocator_arg_t, MemoryResource*, Args...)
+template<class T, class ...Args>
+struct has_alloc_arg
+ : public std::integral_constant<bool,
+ std::uses_allocator<T, MemoryResource*>::value
+ && std::is_constructible<T, std::allocator_arg_t, MemoryResource*, Args...>::value> {};
+// std::uses_allocator<U> && std::is_constructible<U, Args..., MemoryResource*>
+// ie, can construct(Args..., MemoryResource*)
+template<class T, class ...Args>
+struct has_alloc
+ : public std::integral_constant<bool,
+ std::uses_allocator<T, MemoryResource*>::value
+ && std::is_constructible<T, Args..., MemoryResource*>::value> {};
+
+} // namespace detail
+
+
+template<class MemRes>
+struct detail::_AllocatorUtil : public MemRes
+{
+ using MemRes::MemRes;
+
+ /** for construct:
+ * @see http://en.cppreference.com/w/cpp/experimental/polymorphic_allocator/construct */
+
+ // 1. types with no allocators
+ template <class U, class... Args>
+ C4_ALWAYS_INLINE typename std::enable_if<detail::has_no_alloc<U, Args...>::value, void>::type
+ construct(U *ptr, Args &&...args)
+ {
+ c4::construct(ptr, std::forward<Args>(args)...);
+ }
+ template<class U, class I, class... Args>
+ C4_ALWAYS_INLINE typename std::enable_if<detail::has_no_alloc<U, Args...>::value, void>::type
+ construct_n(U* ptr, I n, Args&&... args)
+ {
+ c4::construct_n(ptr, n, std::forward<Args>(args)...);
+ }
+
+ // 2. types using allocators (ie, containers)
+
+ // 2.1. can construct(std::allocator_arg_t, MemoryResource*, Args...)
+ template<class U, class... Args>
+ C4_ALWAYS_INLINE typename std::enable_if<detail::has_alloc_arg<U, Args...>::value, void>::type
+ construct(U* ptr, Args&&... args)
+ {
+ c4::construct(ptr, std::allocator_arg, this->resource(), std::forward<Args>(args)...);
+ }
+ template<class U, class I, class... Args>
+ C4_ALWAYS_INLINE typename std::enable_if<detail::has_alloc_arg<U, Args...>::value, void>::type
+ construct_n(U* ptr, I n, Args&&... args)
+ {
+ c4::construct_n(ptr, n, std::allocator_arg, this->resource(), std::forward<Args>(args)...);
+ }
+
+ // 2.2. can construct(Args..., MemoryResource*)
+ template<class U, class... Args>
+ C4_ALWAYS_INLINE typename std::enable_if<detail::has_alloc<U, Args...>::value, void>::type
+ construct(U* ptr, Args&&... args)
+ {
+ c4::construct(ptr, std::forward<Args>(args)..., this->resource());
+ }
+ template<class U, class I, class... Args>
+ C4_ALWAYS_INLINE typename std::enable_if<detail::has_alloc<U, Args...>::value, void>::type
+ construct_n(U* ptr, I n, Args&&... args)
+ {
+ c4::construct_n(ptr, n, std::forward<Args>(args)..., this->resource());
+ }
+
+ template<class U>
+ static C4_ALWAYS_INLINE void destroy(U* ptr)
+ {
+ c4::destroy(ptr);
+ }
+ template<class U, class I>
+ static C4_ALWAYS_INLINE void destroy_n(U* ptr, I n)
+ {
+ c4::destroy_n(ptr, n);
+ }
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** An allocator is simply a proxy to a memory resource.
+ * @param T
+ * @param MemResProvider
+ * @ingroup allocators */
+template<class T, class MemResProvider=MemResGlobal>
+class Allocator : public detail::_AllocatorUtil<MemResProvider>
+{
+public:
+
+ using impl_type = detail::_AllocatorUtil<MemResProvider>;
+
+ using value_type = T;
+ using pointer = T*;
+ using const_pointer = T const*;
+ using reference = T&;
+ using const_reference = T const&;
+ using size_type = size_t;
+ using difference_type = std::ptrdiff_t;
+ using propagate_on_container_move_assigment = std::true_type;
+
+public:
+
+ template<class U, class MRProv>
+ bool operator== (Allocator<U, MRProv> const& that) const
+ {
+ return this->resource() == that.resource();
+ }
+ template<class U, class MRProv>
+ bool operator!= (Allocator<U, MRProv> const& that) const
+ {
+ return this->resource() != that.resource();
+ }
+
+public:
+
+ template<class U, class MRProv> friend class Allocator;
+ template<class U>
+ struct rebind
+ {
+ using other = Allocator<U, MemResProvider>;
+ };
+ template<class U>
+ typename rebind<U>::other rebound()
+ {
+ return typename rebind<U>::other(*this);
+ }
+
+public:
+
+ using impl_type::impl_type;
+ Allocator() : impl_type() {} // VS demands this
+
+ template<class U> Allocator(Allocator<U, MemResProvider> const& that) : impl_type(that.resource()) {}
+
+ Allocator(Allocator const&) = default;
+ Allocator(Allocator &&) = default;
+
+ Allocator& operator= (Allocator const&) = default; // WTF? why? @see http://en.cppreference.com/w/cpp/memory/polymorphic_allocator
+ Allocator& operator= (Allocator &&) = default;
+
+ /** returns a default-constructed polymorphic allocator object
+ * @see http://en.cppreference.com/w/cpp/memory/polymorphic_allocator/select_on_container_copy_construction */
+ Allocator select_on_container_copy_construct() const { return Allocator(*this); }
+
+ T* allocate(size_t num_objs, size_t alignment=alignof(T))
+ {
+ C4_ASSERT(this->resource() != nullptr);
+ C4_ASSERT(alignment >= alignof(T));
+ void* vmem = this->resource()->allocate(detail::size_for<T>(num_objs), alignment);
+ T* mem = static_cast<T*>(vmem);
+ return mem;
+ }
+
+ void deallocate(T * ptr, size_t num_objs, size_t alignment=alignof(T))
+ {
+ C4_ASSERT(this->resource() != nullptr);
+ C4_ASSERT(alignment>= alignof(T));
+ this->resource()->deallocate(ptr, detail::size_for<T>(num_objs), alignment);
+ }
+
+ T* reallocate(T* ptr, size_t oldnum, size_t newnum, size_t alignment=alignof(T))
+ {
+ C4_ASSERT(this->resource() != nullptr);
+ C4_ASSERT(alignment >= alignof(T));
+ void* vmem = this->resource()->reallocate(ptr, detail::size_for<T>(oldnum), detail::size_for<T>(newnum), alignment);
+ T* mem = static_cast<T*>(vmem);
+ return mem;
+ }
+
+};
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** @ingroup allocators */
+template<class T, size_t N=16, size_t Alignment=alignof(T), class MemResProvider=MemResGlobal>
+class SmallAllocator : public detail::_AllocatorUtil<MemResProvider>
+{
+ static_assert(Alignment >= alignof(T), "invalid alignment");
+
+ using impl_type = detail::_AllocatorUtil<MemResProvider>;
+
+ alignas(Alignment) char m_arr[N * sizeof(T)];
+ size_t m_num{0};
+
+public:
+
+ using value_type = T;
+ using pointer = T*;
+ using const_pointer = T const*;
+ using reference = T&;
+ using const_reference = T const&;
+ using size_type = size_t;
+ using difference_type = std::ptrdiff_t;
+ using propagate_on_container_move_assigment = std::true_type;
+
+ template<class U>
+ bool operator== (SmallAllocator<U,N,Alignment,MemResProvider> const&) const
+ {
+ return false;
+ }
+ template<class U>
+ bool operator!= (SmallAllocator<U,N,Alignment,MemResProvider> const&) const
+ {
+ return true;
+ }
+
+public:
+
+ template<class U, size_t, size_t, class> friend class SmallAllocator;
+ template<class U>
+ struct rebind
+ {
+ using other = SmallAllocator<U, N, alignof(U), MemResProvider>;
+ };
+ template<class U>
+ typename rebind<U>::other rebound()
+ {
+ return typename rebind<U>::other(*this);
+ }
+
+public:
+
+ using impl_type::impl_type;
+ SmallAllocator() : impl_type() {} // VS demands this
+
+ template<class U, size_t N2, size_t A2, class MP2>
+ SmallAllocator(SmallAllocator<U,N2,A2,MP2> const& that) : impl_type(that.resource())
+ {
+ C4_ASSERT(that.m_num == 0);
+ }
+
+ SmallAllocator(SmallAllocator const&) = default;
+ SmallAllocator(SmallAllocator &&) = default;
+
+ SmallAllocator& operator= (SmallAllocator const&) = default; // WTF? why? @see http://en.cppreference.com/w/cpp/memory/polymorphic_allocator
+ SmallAllocator& operator= (SmallAllocator &&) = default;
+
+ /** returns a default-constructed polymorphic allocator object
+ * @see http://en.cppreference.com/w/cpp/memory/polymorphic_allocator/select_on_container_copy_construction */
+ SmallAllocator select_on_container_copy_construct() const { return SmallAllocator(*this); }
+
+ T* allocate(size_t num_objs, size_t alignment=Alignment)
+ {
+ C4_ASSERT(this->resource() != nullptr);
+ C4_ASSERT(alignment >= alignof(T));
+ void *vmem;
+ if(m_num + num_objs <= N)
+ {
+ vmem = (m_arr + m_num * sizeof(T));
+ }
+ else
+ {
+ vmem = this->resource()->allocate(num_objs * sizeof(T), alignment);
+ }
+ m_num += num_objs;
+ T *mem = static_cast<T*>(vmem);
+ return mem;
+ }
+
+ void deallocate(T * ptr, size_t num_objs, size_t alignment=Alignment)
+ {
+ C4_ASSERT(m_num >= num_objs);
+ m_num -= num_objs;
+ if((char*)ptr >= m_arr && (char*)ptr < m_arr + (N * sizeof(T)))
+ {
+ return;
+ }
+ C4_ASSERT(this->resource() != nullptr);
+ C4_ASSERT(alignment >= alignof(T));
+ this->resource()->deallocate(ptr, num_objs * sizeof(T), alignment);
+ }
+
+ T* reallocate(T * ptr, size_t oldnum, size_t newnum, size_t alignment=Alignment)
+ {
+ C4_ASSERT(this->resource() != nullptr);
+ C4_ASSERT(alignment >= alignof(T));
+ if(oldnum <= N && newnum <= N)
+ {
+ return m_arr;
+ }
+ else if(oldnum <= N && newnum > N)
+ {
+ return allocate(newnum, alignment);
+ }
+ else if(oldnum > N && newnum <= N)
+ {
+ deallocate(ptr, oldnum, alignment);
+ return m_arr;
+ }
+ void* vmem = this->resource()->reallocate(ptr, oldnum * sizeof(T), newnum * sizeof(T), alignment);
+ T* mem = static_cast<T*>(vmem);
+ return mem;
+ }
+
+};
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** An allocator making use of the global memory resource.
+ * @ingroup allocators */
+template<class T> using allocator = Allocator<T, MemResGlobal>;
+/** An allocator with a per-instance memory resource
+ * @ingroup allocators */
+template<class T> using allocator_mr = Allocator<T, MemRes>;
+
+/** @ingroup allocators */
+template<class T, size_t N=16, size_t Alignment=alignof(T)> using small_allocator = SmallAllocator<T, N, Alignment, MemResGlobal>;
+/** @ingroup allocators */
+template<class T, size_t N=16, size_t Alignment=alignof(T)> using small_allocator_mr = SmallAllocator<T, N, Alignment, MemRes>;
+
+} // namespace c4
+
+#endif /* _C4_ALLOCATOR_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/allocator.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/char_traits.hpp
+// https://github.com/biojppm/c4core/src/c4/char_traits.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_CHAR_TRAITS_HPP_
+#define _C4_CHAR_TRAITS_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//#include "c4/config.hpp"
+#if !defined(C4_CONFIG_HPP_) && !defined(_C4_CONFIG_HPP_)
+#error "amalgamate: file c4/config.hpp must have been included at this point"
+#endif /* C4_CONFIG_HPP_ */
+
+
+#include <string> // needed because of std::char_traits
+#include <cctype>
+#include <cwctype>
+
+namespace c4 {
+
+C4_ALWAYS_INLINE bool isspace(char c) { return std::isspace(c) != 0; }
+C4_ALWAYS_INLINE bool isspace(wchar_t c) { return std::iswspace(static_cast<wint_t>(c)) != 0; }
+
+//-----------------------------------------------------------------------------
+template<typename C>
+struct char_traits;
+
+template<>
+struct char_traits<char> : public std::char_traits<char>
+{
+ constexpr static const char whitespace_chars[] = " \f\n\r\t\v";
+ constexpr static const size_t num_whitespace_chars = sizeof(whitespace_chars) - 1;
+};
+
+template<>
+struct char_traits<wchar_t> : public std::char_traits<wchar_t>
+{
+ constexpr static const wchar_t whitespace_chars[] = L" \f\n\r\t\v";
+ constexpr static const size_t num_whitespace_chars = sizeof(whitespace_chars) - 1;
+};
+
+
+//-----------------------------------------------------------------------------
+namespace detail {
+template<typename C>
+struct needed_chars;
+template<>
+struct needed_chars<char>
+{
+ template<class SizeType>
+ C4_ALWAYS_INLINE constexpr static SizeType for_bytes(SizeType num_bytes)
+ {
+ return num_bytes;
+ }
+};
+template<>
+struct needed_chars<wchar_t>
+{
+ template<class SizeType>
+ C4_ALWAYS_INLINE constexpr static SizeType for_bytes(SizeType num_bytes)
+ {
+ // wchar_t is not necessarily 2 bytes.
+ return (num_bytes / static_cast<SizeType>(sizeof(wchar_t))) + ((num_bytes & static_cast<SizeType>(SizeType(sizeof(wchar_t)) - SizeType(1))) != 0);
+ }
+};
+} // namespace detail
+
+/** get the number of C characters needed to store a number of bytes */
+template<typename C, typename SizeType>
+C4_ALWAYS_INLINE constexpr SizeType num_needed_chars(SizeType num_bytes)
+{
+ return detail::needed_chars<C>::for_bytes(num_bytes);
+}
+
+
+//-----------------------------------------------------------------------------
+
+/** get the given text string as either char or wchar_t according to the given type */
+#define C4_TXTTY(txt, type) \
+ /* is there a smarter way to do this? */\
+ c4::detail::literal_as<type>::get(txt, C4_WIDEN(txt))
+
+namespace detail {
+template<typename C>
+struct literal_as;
+
+template<>
+struct literal_as<char>
+{
+ C4_ALWAYS_INLINE static constexpr const char* get(const char* str, const wchar_t *)
+ {
+ return str;
+ }
+};
+template<>
+struct literal_as<wchar_t>
+{
+ C4_ALWAYS_INLINE static constexpr const wchar_t* get(const char*, const wchar_t *wstr)
+ {
+ return wstr;
+ }
+};
+} // namespace detail
+
+} // namespace c4
+
+#endif /* _C4_CHAR_TRAITS_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/char_traits.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/hash.hpp
+// https://github.com/biojppm/c4core/src/c4/hash.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_HASH_HPP_
+#define _C4_HASH_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//#include "c4/config.hpp"
+#if !defined(C4_CONFIG_HPP_) && !defined(_C4_CONFIG_HPP_)
+#error "amalgamate: file c4/config.hpp must have been included at this point"
+#endif /* C4_CONFIG_HPP_ */
+
+#include <climits>
+
+/** @file hash.hpp */
+
+/** @defgroup hash Hash utils
+ * @see http://aras-p.info/blog/2016/08/02/Hash-Functions-all-the-way-down/ */
+
+namespace c4 {
+
+namespace detail {
+
+/** @internal
+ * @ingroup hash
+ * @see this was taken a great answer in stackoverflow:
+ * https://stackoverflow.com/a/34597785/5875572
+ * @see http://aras-p.info/blog/2016/08/02/Hash-Functions-all-the-way-down/ */
+template<typename ResultT, ResultT OffsetBasis, ResultT Prime>
+class basic_fnv1a final
+{
+
+ static_assert(std::is_unsigned<ResultT>::value, "need unsigned integer");
+
+public:
+
+ using result_type = ResultT;
+
+private:
+
+ result_type state_ {};
+
+public:
+
+ C4_CONSTEXPR14 basic_fnv1a() noexcept : state_ {OffsetBasis} {}
+
+ C4_CONSTEXPR14 void update(const void *const data, const size_t size) noexcept
+ {
+ auto cdata = static_cast<const unsigned char *>(data);
+ auto acc = this->state_;
+ for(size_t i = 0; i < size; ++i)
+ {
+ const auto next = size_t(cdata[i]);
+ acc = (acc ^ next) * Prime;
+ }
+ this->state_ = acc;
+ }
+
+ C4_CONSTEXPR14 result_type digest() const noexcept
+ {
+ return this->state_;
+ }
+
+};
+
+using fnv1a_32 = basic_fnv1a<uint32_t, UINT32_C( 2166136261), UINT32_C( 16777619)>;
+using fnv1a_64 = basic_fnv1a<uint64_t, UINT64_C(14695981039346656037), UINT64_C(1099511628211)>;
+
+template<size_t Bits> struct fnv1a;
+template<> struct fnv1a<32> { using type = fnv1a_32; };
+template<> struct fnv1a<64> { using type = fnv1a_64; };
+
+} // namespace detail
+
+
+/** @ingroup hash */
+template<size_t Bits>
+using fnv1a_t = typename detail::fnv1a<Bits>::type;
+
+
+/** @ingroup hash */
+C4_CONSTEXPR14 inline size_t hash_bytes(const void *const data, const size_t size) noexcept
+{
+ fnv1a_t<CHAR_BIT * sizeof(size_t)> fn{};
+ fn.update(data, size);
+ return fn.digest();
+}
+
+/**
+ * @overload hash_bytes
+ * @ingroup hash */
+template<size_t N>
+C4_CONSTEXPR14 inline size_t hash_bytes(const char (&str)[N]) noexcept
+{
+ fnv1a_t<CHAR_BIT * sizeof(size_t)> fn{};
+ fn.update(str, N);
+ return fn.digest();
+}
+
+} // namespace c4
+
+
+#endif // _C4_HASH_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/hash.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/szconv.hpp
+// https://github.com/biojppm/c4core/src/c4/szconv.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_SZCONV_HPP_
+#define _C4_SZCONV_HPP_
+
+/** @file szconv.hpp utilities to deal safely with narrowing conversions */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//#include "c4/config.hpp"
+#if !defined(C4_CONFIG_HPP_) && !defined(_C4_CONFIG_HPP_)
+#error "amalgamate: file c4/config.hpp must have been included at this point"
+#endif /* C4_CONFIG_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+
+#include <limits>
+
+namespace c4 {
+
+/** @todo this would be so much easier with calls to numeric_limits::max()... */
+template<class SizeOut, class SizeIn>
+struct is_narrower_size : std::conditional
+<
+ (std::is_signed<SizeOut>::value == std::is_signed<SizeIn>::value)
+ ?
+ (sizeof(SizeOut) < sizeof(SizeIn))
+ :
+ (
+ (sizeof(SizeOut) < sizeof(SizeIn))
+ ||
+ (
+ (sizeof(SizeOut) == sizeof(SizeIn))
+ &&
+ (std::is_signed<SizeOut>::value && std::is_unsigned<SizeIn>::value)
+ )
+ ),
+ std::true_type,
+ std::false_type
+>::type
+{
+ static_assert(std::is_integral<SizeIn >::value, "must be integral type");
+ static_assert(std::is_integral<SizeOut>::value, "must be integral type");
+};
+
+
+/** when SizeOut is wider than SizeIn, assignment can occur without reservations */
+template<class SizeOut, class SizeIn>
+C4_ALWAYS_INLINE
+typename std::enable_if< ! is_narrower_size<SizeOut, SizeIn>::value, SizeOut>::type
+szconv(SizeIn sz) noexcept
+{
+ return static_cast<SizeOut>(sz);
+}
+
+/** when SizeOut is narrower than SizeIn, narrowing will occur, so we check
+ * for overflow. Note that this check is done only if C4_XASSERT is enabled.
+ * @see C4_XASSERT */
+template<class SizeOut, class SizeIn>
+C4_ALWAYS_INLINE
+typename std::enable_if<is_narrower_size<SizeOut, SizeIn>::value, SizeOut>::type
+szconv(SizeIn sz) C4_NOEXCEPT_X
+{
+ C4_XASSERT(sz >= 0);
+ C4_XASSERT_MSG((SizeIn)sz <= (SizeIn)std::numeric_limits<SizeOut>::max(), "size conversion overflow: in=%zu", (size_t)sz);
+ SizeOut szo = static_cast<SizeOut>(sz);
+ return szo;
+}
+
+} // namespace c4
+
+#endif /* _C4_SZCONV_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/szconv.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/blob.hpp
+// https://github.com/biojppm/c4core/src/c4/blob.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_BLOB_HPP_
+#define _C4_BLOB_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/types.hpp
+//#include "c4/types.hpp"
+#if !defined(C4_TYPES_HPP_) && !defined(_C4_TYPES_HPP_)
+#error "amalgamate: file c4/types.hpp must have been included at this point"
+#endif /* C4_TYPES_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+
+/** @file blob.hpp Mutable and immutable binary data blobs.
+*/
+
+namespace c4 {
+
+template<class T>
+struct blob_
+{
+ T * buf;
+ size_t len;
+
+ C4_ALWAYS_INLINE blob_() noexcept : buf(), len() {}
+
+ C4_ALWAYS_INLINE blob_(blob_ const& that) noexcept = default;
+ C4_ALWAYS_INLINE blob_(blob_ && that) noexcept = default;
+ C4_ALWAYS_INLINE blob_& operator=(blob_ && that) noexcept = default;
+ C4_ALWAYS_INLINE blob_& operator=(blob_ const& that) noexcept = default;
+
+ // need to sfinae out copy constructors! (why? isn't the above sufficient?)
+ #define _C4_REQUIRE_NOT_SAME class=typename std::enable_if<( ! std::is_same<U, blob_>::value) && ( ! std::is_pointer<U>::value), T>::type
+ template<class U, _C4_REQUIRE_NOT_SAME> C4_ALWAYS_INLINE blob_(U &var) noexcept : buf(reinterpret_cast<T*>(&var)), len(sizeof(U)) {}
+ template<class U, _C4_REQUIRE_NOT_SAME> C4_ALWAYS_INLINE blob_& operator= (U &var) noexcept { buf = reinterpret_cast<T*>(&var); len = sizeof(U); return *this; }
+ #undef _C4_REQUIRE_NOT_SAME
+
+ template<class U, size_t N> C4_ALWAYS_INLINE blob_(U (&arr)[N]) noexcept : buf(reinterpret_cast<T*>(arr)), len(sizeof(U) * N) {}
+ template<class U, size_t N> C4_ALWAYS_INLINE blob_& operator= (U (&arr)[N]) noexcept { buf = reinterpret_cast<T*>(arr); len = sizeof(U) * N; return *this; }
+
+ template<class U>
+ C4_ALWAYS_INLINE blob_(U *ptr, size_t n) noexcept : buf(reinterpret_cast<T*>(ptr)), len(sizeof(U) * n) { C4_ASSERT(is_aligned(ptr)); }
+ C4_ALWAYS_INLINE blob_(void *ptr, size_t n) noexcept : buf(reinterpret_cast<T*>(ptr)), len(n) {}
+ C4_ALWAYS_INLINE blob_(void const *ptr, size_t n) noexcept : buf(reinterpret_cast<T*>(ptr)), len(n) {}
+};
+
+/** an immutable binary blob */
+using cblob = blob_<cbyte>;
+/** a mutable binary blob */
+using blob = blob_< byte>;
+
+C4_MUST_BE_TRIVIAL_COPY(blob);
+C4_MUST_BE_TRIVIAL_COPY(cblob);
+
+} // namespace c4
+
+#endif // _C4_BLOB_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/blob.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/substr_fwd.hpp
+// https://github.com/biojppm/c4core/src/c4/substr_fwd.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_SUBSTR_FWD_HPP_
+#define _C4_SUBSTR_FWD_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/export.hpp
+//#include "c4/export.hpp"
+#if !defined(C4_EXPORT_HPP_) && !defined(_C4_EXPORT_HPP_)
+#error "amalgamate: file c4/export.hpp must have been included at this point"
+#endif /* C4_EXPORT_HPP_ */
+
+
+namespace c4 {
+
+#ifndef DOXYGEN
+template<class C> struct basic_substring;
+using csubstr = C4CORE_EXPORT basic_substring<const char>;
+using substr = C4CORE_EXPORT basic_substring<char>;
+#endif // !DOXYGEN
+
+} // namespace c4
+
+#endif /* _C4_SUBSTR_FWD_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/substr_fwd.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/substr.hpp
+// https://github.com/biojppm/c4core/src/c4/substr.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_SUBSTR_HPP_
+#define _C4_SUBSTR_HPP_
+
+/** @file substr.hpp read+write string views */
+
+//included above:
+//#include <string.h>
+//included above:
+//#include <ctype.h>
+//included above:
+//#include <type_traits>
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//#include "c4/config.hpp"
+#if !defined(C4_CONFIG_HPP_) && !defined(_C4_CONFIG_HPP_)
+#error "amalgamate: file c4/config.hpp must have been included at this point"
+#endif /* C4_CONFIG_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/substr_fwd.hpp
+//#include "c4/substr_fwd.hpp"
+#if !defined(C4_SUBSTR_FWD_HPP_) && !defined(_C4_SUBSTR_FWD_HPP_)
+#error "amalgamate: file c4/substr_fwd.hpp must have been included at this point"
+#endif /* C4_SUBSTR_FWD_HPP_ */
+
+
+#ifdef __clang__
+# pragma clang diagnostic push
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wtype-limits" // disable warnings on size_t>=0, used heavily in assertions below. These assertions are a preparation step for providing the index type as a template parameter.
+# pragma GCC diagnostic ignored "-Wuseless-cast"
+#endif
+
+
+namespace c4 {
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+namespace detail {
+
+template<typename C>
+static inline void _do_reverse(C *C4_RESTRICT first, C *C4_RESTRICT last)
+{
+ while(last > first)
+ {
+ C tmp = *last;
+ *last-- = *first;
+ *first++ = tmp;
+ }
+}
+
+} // namespace detail
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+// utility macros to deuglify SFINAE code; undefined after the class.
+// https://stackoverflow.com/questions/43051882/how-to-disable-a-class-member-funrtion-for-certain-template-types
+#define C4_REQUIRE_RW(ret_type) \
+ template <typename U=C> \
+ typename std::enable_if< ! std::is_const<U>::value, ret_type>::type
+// non-const-to-const
+#define C4_NC2C(ty) \
+ typename std::enable_if<std::is_const<C>::value && ( ! std::is_const<ty>::value), ty>::type
+
+
+/** a non-owning string-view, consisting of a character pointer
+ * and a length.
+ *
+ * @note The pointer is explicitly restricted.
+ * @note Because of a C++ limitation, there cannot coexist overloads for
+ * constructing from a char[N] and a char*; the latter will always be chosen
+ * by the compiler. To construct an object of this type, call to_substr() or
+ * to_csubstr(). For a more detailed explanation on why the overloads cannot
+ * coexist, see http://cplusplus.bordoon.com/specializeForCharacterArrays.html
+ *
+ * @see to_substr()
+ * @see to_csubstr()
+ */
+template<class C>
+struct C4CORE_EXPORT basic_substring
+{
+public:
+
+ /** a restricted pointer to the first character of the substring */
+ C * C4_RESTRICT str;
+ /** the length of the substring */
+ size_t len;
+
+public:
+
+ /** @name Types */
+ /** @{ */
+
+ using CC = typename std::add_const<C>::type; //!< CC=const char
+ using NCC_ = typename std::remove_const<C>::type; //!< NCC_=non const char
+
+ using ro_substr = basic_substring<CC>;
+ using rw_substr = basic_substring<NCC_>;
+
+ using char_type = C;
+ using size_type = size_t;
+
+ using iterator = C*;
+ using const_iterator = CC*;
+
+ enum : size_t { npos = (size_t)-1, NONE = (size_t)-1 };
+
+ /// convert automatically to substring of const C
+ operator ro_substr () const { ro_substr s(str, len); return s; }
+
+ /** @} */
+
+public:
+
+ /** @name Default construction and assignment */
+ /** @{ */
+
+ constexpr basic_substring() : str(nullptr), len(0) {}
+
+ constexpr basic_substring(basic_substring const&) = default;
+ constexpr basic_substring(basic_substring &&) = default;
+ constexpr basic_substring(std::nullptr_t) : str(nullptr), len(0) {}
+
+ basic_substring& operator= (basic_substring const&) = default;
+ basic_substring& operator= (basic_substring &&) = default;
+ basic_substring& operator= (std::nullptr_t) { str = nullptr; len = 0; return *this; }
+
+ /** @} */
+
+public:
+
+ /** @name Construction and assignment from characters with the same type */
+ /** @{ */
+
+ //basic_substring(C *s_) : str(s_), len(s_ ? strlen(s_) : 0) {}
+ /** the overload for receiving a single C* pointer will always
+ * hide the array[N] overload. So it is disabled. If you want to
+ * construct a substr from a single pointer containing a C-style string,
+ * you can call c4::to_substr()/c4::to_csubstr().
+ * @see c4::to_substr()
+ * @see c4::to_csubstr() */
+ template<size_t N>
+ constexpr basic_substring(C (&s_)[N]) noexcept : str(s_), len(N-1) {}
+ basic_substring(C *s_, size_t len_) : str(s_), len(len_) { C4_ASSERT(str || !len_); }
+ basic_substring(C *beg_, C *end_) : str(beg_), len(static_cast<size_t>(end_ - beg_)) { C4_ASSERT(end_ >= beg_); }
+
+ //basic_substring& operator= (C *s_) { this->assign(s_); return *this; }
+ template<size_t N>
+ basic_substring& operator= (C (&s_)[N]) { this->assign<N>(s_); return *this; }
+
+ //void assign(C *s_) { str = (s_); len = (s_ ? strlen(s_) : 0); }
+ /** the overload for receiving a single C* pointer will always
+ * hide the array[N] overload. So it is disabled. If you want to
+ * construct a substr from a single pointer containing a C-style string,
+ * you can call c4::to_substr()/c4::to_csubstr().
+ * @see c4::to_substr()
+ * @see c4::to_csubstr() */
+ template<size_t N>
+ void assign(C (&s_)[N]) { str = (s_); len = (N-1); }
+ void assign(C *s_, size_t len_) { str = s_; len = len_; C4_ASSERT(str || !len_); }
+ void assign(C *beg_, C *end_) { C4_ASSERT(end_ >= beg_); str = (beg_); len = (end_ - beg_); }
+
+ void clear() { str = nullptr; len = 0; }
+
+ /** @} */
+
+public:
+
+ /** @name Construction from non-const characters */
+ /** @{ */
+
+ // when the char type is const, allow construction and assignment from non-const chars
+
+ /** only available when the char type is const */
+ template<size_t N, class U=NCC_> explicit basic_substring(C4_NC2C(U) (&s_)[N]) { str = s_; len = N-1; }
+ /** only available when the char type is const */
+ template< class U=NCC_> basic_substring(C4_NC2C(U) *s_, size_t len_) { str = s_; len = len_; }
+ /** only available when the char type is const */
+ template< class U=NCC_> basic_substring(C4_NC2C(U) *beg_, C4_NC2C(U) *end_) { C4_ASSERT(end_ >= beg_); str = beg_; len = end_ - beg_; }
+
+ /** only available when the char type is const */
+ template<size_t N, class U=NCC_> void assign(C4_NC2C(U) (&s_)[N]) { str = s_; len = N-1; }
+ /** only available when the char type is const */
+ template< class U=NCC_> void assign(C4_NC2C(U) *s_, size_t len_) { str = s_; len = len_; }
+ /** only available when the char type is const */
+ template< class U=NCC_> void assign(C4_NC2C(U) *beg_, C4_NC2C(U) *end_) { C4_ASSERT(end_ >= beg_); str = beg_; len = end_ - beg_; }
+
+ /** only available when the char type is const */
+ template<size_t N, class U=NCC_>
+ basic_substring& operator=(C4_NC2C(U) (&s_)[N]) { str = s_; len = N-1; return *this; }
+
+ /** @} */
+
+public:
+
+ /** @name Standard accessor methods */
+ /** @{ */
+
+ bool has_str() const { return ! empty() && str[0] != C(0); }
+ bool empty() const { return (len == 0 || str == nullptr); }
+ bool not_empty() const { return (len != 0 && str != nullptr); }
+ size_t size() const { return len; }
+
+ iterator begin() { return str; }
+ iterator end () { return str + len; }
+
+ const_iterator begin() const { return str; }
+ const_iterator end () const { return str + len; }
+
+ C * data() { return str; }
+ C const* data() const { return str; }
+
+ inline C & operator[] (size_t i) { C4_ASSERT(i >= 0 && i < len); return str[i]; }
+ inline C const& operator[] (size_t i) const { C4_ASSERT(i >= 0 && i < len); return str[i]; }
+
+ inline C & front() { C4_ASSERT(len > 0 && str != nullptr); return *str; }
+ inline C const& front() const { C4_ASSERT(len > 0 && str != nullptr); return *str; }
+
+ inline C & back() { C4_ASSERT(len > 0 && str != nullptr); return *(str + len - 1); }
+ inline C const& back() const { C4_ASSERT(len > 0 && str != nullptr); return *(str + len - 1); }
+
+ /** @} */
+
+public:
+
+ /** @name Comparison methods */
+ /** @{ */
+
+ int compare(C const c) const
+ {
+ C4_XASSERT((str != nullptr) || len == 0);
+ if( ! len)
+ return -1;
+ if(*str == c)
+ return static_cast<int>(len - 1);
+ return *str - c;
+ }
+
+ int compare(const char *that, size_t sz) const
+ {
+ C4_XASSERT(that || sz == 0);
+ C4_XASSERT(str || len == 0);
+ if(C4_LIKELY(str && that))
+ {
+ int ret = strncmp(str, that, len < sz ? len : sz);
+ if(ret == 0 && len != sz)
+ ret = len < sz ? -1 : 1;
+ return ret;
+ }
+ if((!str && !that) || (len == sz))
+ {
+ C4_XASSERT(len == 0 && sz == 0);
+ return 0;
+ }
+ return len < sz ? -1 : 1;
+ }
+
+ C4_ALWAYS_INLINE int compare(ro_substr const that) const { return this->compare(that.str, that.len); }
+
+ C4_ALWAYS_INLINE bool operator== (std::nullptr_t) const { return str == nullptr || len == 0; }
+ C4_ALWAYS_INLINE bool operator!= (std::nullptr_t) const { return str != nullptr || len == 0; }
+
+ C4_ALWAYS_INLINE bool operator== (C const c) const { return this->compare(c) == 0; }
+ C4_ALWAYS_INLINE bool operator!= (C const c) const { return this->compare(c) != 0; }
+ C4_ALWAYS_INLINE bool operator< (C const c) const { return this->compare(c) < 0; }
+ C4_ALWAYS_INLINE bool operator> (C const c) const { return this->compare(c) > 0; }
+ C4_ALWAYS_INLINE bool operator<= (C const c) const { return this->compare(c) <= 0; }
+ C4_ALWAYS_INLINE bool operator>= (C const c) const { return this->compare(c) >= 0; }
+
+ template<class U> C4_ALWAYS_INLINE bool operator== (basic_substring<U> const that) const { return this->compare(that) == 0; }
+ template<class U> C4_ALWAYS_INLINE bool operator!= (basic_substring<U> const that) const { return this->compare(that) != 0; }
+ template<class U> C4_ALWAYS_INLINE bool operator< (basic_substring<U> const that) const { return this->compare(that) < 0; }
+ template<class U> C4_ALWAYS_INLINE bool operator> (basic_substring<U> const that) const { return this->compare(that) > 0; }
+ template<class U> C4_ALWAYS_INLINE bool operator<= (basic_substring<U> const that) const { return this->compare(that) <= 0; }
+ template<class U> C4_ALWAYS_INLINE bool operator>= (basic_substring<U> const that) const { return this->compare(that) >= 0; }
+
+ template<size_t N> C4_ALWAYS_INLINE bool operator== (const char (&that)[N]) const { return this->compare(that, N-1) == 0; }
+ template<size_t N> C4_ALWAYS_INLINE bool operator!= (const char (&that)[N]) const { return this->compare(that, N-1) != 0; }
+ template<size_t N> C4_ALWAYS_INLINE bool operator< (const char (&that)[N]) const { return this->compare(that, N-1) < 0; }
+ template<size_t N> C4_ALWAYS_INLINE bool operator> (const char (&that)[N]) const { return this->compare(that, N-1) > 0; }
+ template<size_t N> C4_ALWAYS_INLINE bool operator<= (const char (&that)[N]) const { return this->compare(that, N-1) <= 0; }
+ template<size_t N> C4_ALWAYS_INLINE bool operator>= (const char (&that)[N]) const { return this->compare(that, N-1) >= 0; }
+
+ /** @} */
+
+public:
+
+ /** @name Sub-selection methods */
+ /** @{ */
+
+ /** true if *this is a substring of that (ie, from the same buffer) */
+ inline bool is_sub(ro_substr const that) const
+ {
+ return that.is_super(*this);
+ }
+
+ /** true if that is a substring of *this (ie, from the same buffer) */
+ inline bool is_super(ro_substr const that) const
+ {
+ if(C4_UNLIKELY(len == 0))
+ {
+ return that.len == 0 && that.str == str && str != nullptr;
+ }
+ return that.begin() >= begin() && that.end() <= end();
+ }
+
+ /** true if there is overlap of at least one element between that and *this */
+ inline bool overlaps(ro_substr const that) const
+ {
+ // thanks @timwynants
+ return (that.end() > begin() && that.begin() < end());
+ }
+
+public:
+
+ /** return [first,len[ */
+ basic_substring sub(size_t first) const
+ {
+ C4_ASSERT(first >= 0 && first <= len);
+ return basic_substring(str + first, len - first);
+ }
+
+ /** return [first,first+num[. If num==npos, return [first,len[ */
+ basic_substring sub(size_t first, size_t num) const
+ {
+ C4_ASSERT(first >= 0 && first <= len);
+ C4_ASSERT((num >= 0 && num <= len) || (num == npos));
+ size_t rnum = num != npos ? num : len - first;
+ C4_ASSERT((first >= 0 && first + rnum <= len) || (num == 0));
+ return basic_substring(str + first, rnum);
+ }
+
+ /** return [first,last[. If last==npos, return [first,len[ */
+ basic_substring range(size_t first, size_t last=npos) const
+ {
+ C4_ASSERT(first >= 0 && first <= len);
+ last = last != npos ? last : len;
+ C4_ASSERT(first <= last);
+ C4_ASSERT(last >= 0 && last <= len);
+ return basic_substring(str + first, last - first);
+ }
+
+ /** return [0,num[*/
+ basic_substring first(size_t num) const
+ {
+ return sub(0, num);
+ }
+
+ /** return [len-num,len[*/
+ basic_substring last(size_t num) const
+ {
+ if(num == npos)
+ return *this;
+ return sub(len - num);
+ }
+
+ /** offset from the ends: return [left,len-right[ ; ie, trim a
+ number of characters from the left and right. This is
+ equivalent to python's negative list indices. */
+ basic_substring offs(size_t left, size_t right) const
+ {
+ C4_ASSERT(left >= 0 && left <= len);
+ C4_ASSERT(right >= 0 && right <= len);
+ C4_ASSERT(left <= len - right + 1);
+ return basic_substring(str + left, len - right - left);
+ }
+
+ /** return [0, pos+include_pos[ */
+ basic_substring left_of(size_t pos, bool include_pos=false) const
+ {
+ if(pos == npos)
+ return *this;
+ return first(pos + include_pos);
+ }
+
+ /** return [pos+!include_pos, len[ */
+ basic_substring right_of(size_t pos, bool include_pos=false) const
+ {
+ if(pos == npos)
+ return sub(len, 0);
+ return sub(pos + !include_pos);
+ }
+
+public:
+
+ /** given @p subs a substring of the current string, get the
+ * portion of the current string to the left of it */
+ basic_substring left_of(ro_substr const subs) const
+ {
+ C4_ASSERT(is_super(subs) || subs.empty());
+ auto ssb = subs.begin();
+ auto b = begin();
+ auto e = end();
+ if(ssb >= b && ssb <= e)
+ return sub(0, static_cast<size_t>(ssb - b));
+ else
+ return sub(0, 0);
+ }
+
+ /** given @p subs a substring of the current string, get the
+ * portion of the current string to the right of it */
+ basic_substring right_of(ro_substr const subs) const
+ {
+ C4_ASSERT(is_super(subs) || subs.empty());
+ auto sse = subs.end();
+ auto b = begin();
+ auto e = end();
+ if(sse >= b && sse <= e)
+ return sub(static_cast<size_t>(sse - b), static_cast<size_t>(e - sse));
+ else
+ return sub(0, 0);
+ }
+
+ /** @} */
+
+public:
+
+ /** @name Removing characters (trim()) / patterns (strip()) from the tips of the string */
+ /** @{ */
+
+ /** trim left */
+ basic_substring triml(const C c) const
+ {
+ if( ! empty())
+ {
+ size_t pos = first_not_of(c);
+ if(pos != npos)
+ return sub(pos);
+ }
+ return sub(0, 0);
+ }
+ /** trim left ANY of the characters.
+ * @see stripl() to remove a pattern from the left */
+ basic_substring triml(ro_substr chars) const
+ {
+ if( ! empty())
+ {
+ size_t pos = first_not_of(chars);
+ if(pos != npos)
+ return sub(pos);
+ }
+ return sub(0, 0);
+ }
+
+ /** trim the character c from the right */
+ basic_substring trimr(const C c) const
+ {
+ if( ! empty())
+ {
+ size_t pos = last_not_of(c, npos);
+ if(pos != npos)
+ return sub(0, pos+1);
+ }
+ return sub(0, 0);
+ }
+ /** trim right ANY of the characters
+ * @see stripr() to remove a pattern from the right */
+ basic_substring trimr(ro_substr chars) const
+ {
+ if( ! empty())
+ {
+ size_t pos = last_not_of(chars, npos);
+ if(pos != npos)
+ return sub(0, pos+1);
+ }
+ return sub(0, 0);
+ }
+
+ /** trim the character c left and right */
+ basic_substring trim(const C c) const
+ {
+ return triml(c).trimr(c);
+ }
+ /** trim left and right ANY of the characters
+ * @see strip() to remove a pattern from the left and right */
+ basic_substring trim(ro_substr const chars) const
+ {
+ return triml(chars).trimr(chars);
+ }
+
+ /** remove a pattern from the left
+ * @see triml() to remove characters*/
+ basic_substring stripl(ro_substr pattern) const
+ {
+ if( ! begins_with(pattern))
+ return *this;
+ return sub(pattern.len < len ? pattern.len : len);
+ }
+
+ /** remove a pattern from the right
+ * @see trimr() to remove characters*/
+ basic_substring stripr(ro_substr pattern) const
+ {
+ if( ! ends_with(pattern))
+ return *this;
+ return left_of(len - (pattern.len < len ? pattern.len : len));
+ }
+
+ /** @} */
+
+public:
+
+ /** @name Lookup methods */
+ /** @{ */
+
+ inline size_t find(const C c, size_t start_pos=0) const
+ {
+ return first_of(c, start_pos);
+ }
+ inline size_t find(ro_substr pattern, size_t start_pos=0) const
+ {
+ C4_ASSERT(start_pos == npos || (start_pos >= 0 && start_pos <= len));
+ if(len < pattern.len) return npos;
+ for(size_t i = start_pos, e = len - pattern.len + 1; i < e; ++i)
+ {
+ bool gotit = true;
+ for(size_t j = 0; j < pattern.len; ++j)
+ {
+ C4_ASSERT(i + j < len);
+ if(str[i + j] != pattern.str[j])
+ {
+ gotit = false;
+ break;
+ }
+ }
+ if(gotit)
+ {
+ return i;
+ }
+ }
+ return npos;
+ }
+
+public:
+
+ /** count the number of occurrences of c */
+ inline size_t count(const C c, size_t pos=0) const
+ {
+ C4_ASSERT(pos >= 0 && pos <= len);
+ size_t num = 0;
+ pos = find(c, pos);
+ while(pos != npos)
+ {
+ ++num;
+ pos = find(c, pos + 1);
+ }
+ return num;
+ }
+
+ /** count the number of occurrences of s */
+ inline size_t count(ro_substr c, size_t pos=0) const
+ {
+ C4_ASSERT(pos >= 0 && pos <= len);
+ size_t num = 0;
+ pos = find(c, pos);
+ while(pos != npos)
+ {
+ ++num;
+ pos = find(c, pos + c.len);
+ }
+ return num;
+ }
+
+ /** get the substr consisting of the first occurrence of @p c after @p pos, or an empty substr if none occurs */
+ inline basic_substring select(const C c, size_t pos=0) const
+ {
+ pos = find(c, pos);
+ return pos != npos ? sub(pos, 1) : basic_substring();
+ }
+
+ /** get the substr consisting of the first occurrence of @p pattern after @p pos, or an empty substr if none occurs */
+ inline basic_substring select(ro_substr pattern, size_t pos=0) const
+ {
+ pos = find(pattern, pos);
+ return pos != npos ? sub(pos, pattern.len) : basic_substring();
+ }
+
+public:
+
+ struct first_of_any_result
+ {
+ size_t which;
+ size_t pos;
+ inline operator bool() const { return which != NONE && pos != npos; }
+ };
+
+ first_of_any_result first_of_any(ro_substr s0, ro_substr s1) const
+ {
+ ro_substr s[2] = {s0, s1};
+ return first_of_any_iter(&s[0], &s[0] + 2);
+ }
+
+ first_of_any_result first_of_any(ro_substr s0, ro_substr s1, ro_substr s2) const
+ {
+ ro_substr s[3] = {s0, s1, s2};
+ return first_of_any_iter(&s[0], &s[0] + 3);
+ }
+
+ first_of_any_result first_of_any(ro_substr s0, ro_substr s1, ro_substr s2, ro_substr s3) const
+ {
+ ro_substr s[4] = {s0, s1, s2, s3};
+ return first_of_any_iter(&s[0], &s[0] + 4);
+ }
+
+ first_of_any_result first_of_any(ro_substr s0, ro_substr s1, ro_substr s2, ro_substr s3, ro_substr s4) const
+ {
+ ro_substr s[5] = {s0, s1, s2, s3, s4};
+ return first_of_any_iter(&s[0], &s[0] + 5);
+ }
+
+ template<class It>
+ first_of_any_result first_of_any_iter(It first_span, It last_span) const
+ {
+ for(size_t i = 0; i < len; ++i)
+ {
+ size_t curr = 0;
+ for(It it = first_span; it != last_span; ++curr, ++it)
+ {
+ auto const& chars = *it;
+ if((i + chars.len) > len) continue;
+ bool gotit = true;
+ for(size_t j = 0; j < chars.len; ++j)
+ {
+ C4_ASSERT(i + j < len);
+ if(str[i + j] != chars[j])
+ {
+ gotit = false;
+ break;
+ }
+ }
+ if(gotit)
+ {
+ return {curr, i};
+ }
+ }
+ }
+ return {NONE, npos};
+ }
+
+public:
+
+ /** true if the first character of the string is @p c */
+ bool begins_with(const C c) const
+ {
+ return len > 0 ? str[0] == c : false;
+ }
+
+ /** true if the first @p num characters of the string are @p c */
+ bool begins_with(const C c, size_t num) const
+ {
+ if(len < num)
+ {
+ return false;
+ }
+ for(size_t i = 0; i < num; ++i)
+ {
+ if(str[i] != c)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** true if the string begins with the given @p pattern */
+ bool begins_with(ro_substr pattern) const
+ {
+ if(len < pattern.len)
+ {
+ return false;
+ }
+ for(size_t i = 0; i < pattern.len; ++i)
+ {
+ if(str[i] != pattern[i])
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** true if the first character of the string is any of the given @p chars */
+ bool begins_with_any(ro_substr chars) const
+ {
+ if(len == 0)
+ {
+ return false;
+ }
+ for(size_t i = 0; i < chars.len; ++i)
+ {
+ if(str[0] == chars.str[i])
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /** true if the last character of the string is @p c */
+ bool ends_with(const C c) const
+ {
+ return len > 0 ? str[len-1] == c : false;
+ }
+
+ /** true if the last @p num characters of the string are @p c */
+ bool ends_with(const C c, size_t num) const
+ {
+ if(len < num)
+ {
+ return false;
+ }
+ for(size_t i = len - num; i < len; ++i)
+ {
+ if(str[i] != c)
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** true if the string ends with the given @p pattern */
+ bool ends_with(ro_substr pattern) const
+ {
+ if(len < pattern.len)
+ {
+ return false;
+ }
+ for(size_t i = 0, s = len-pattern.len; i < pattern.len; ++i)
+ {
+ if(str[s+i] != pattern[i])
+ {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /** true if the last character of the string is any of the given @p chars */
+ bool ends_with_any(ro_substr chars) const
+ {
+ if(len == 0)
+ {
+ return false;
+ }
+ for(size_t i = 0; i < chars.len; ++i)
+ {
+ if(str[len - 1] == chars[i])
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+public:
+
+ /** @return the first position where c is found in the string, or npos if none is found */
+ size_t first_of(const C c, size_t start=0) const
+ {
+ C4_ASSERT(start == npos || (start >= 0 && start <= len));
+ for(size_t i = start; i < len; ++i)
+ {
+ if(str[i] == c)
+ return i;
+ }
+ return npos;
+ }
+
+ /** @return the last position where c is found in the string, or npos if none is found */
+ size_t last_of(const C c, size_t start=npos) const
+ {
+ C4_ASSERT(start == npos || (start >= 0 && start <= len));
+ if(start == npos)
+ start = len;
+ for(size_t i = start-1; i != size_t(-1); --i)
+ {
+ if(str[i] == c)
+ return i;
+ }
+ return npos;
+ }
+
+ /** @return the first position where ANY of the chars is found in the string, or npos if none is found */
+ size_t first_of(ro_substr chars, size_t start=0) const
+ {
+ C4_ASSERT(start == npos || (start >= 0 && start <= len));
+ for(size_t i = start; i < len; ++i)
+ {
+ for(size_t j = 0; j < chars.len; ++j)
+ {
+ if(str[i] == chars[j])
+ return i;
+ }
+ }
+ return npos;
+ }
+
+ /** @return the last position where ANY of the chars is found in the string, or npos if none is found */
+ size_t last_of(ro_substr chars, size_t start=npos) const
+ {
+ C4_ASSERT(start == npos || (start >= 0 && start <= len));
+ if(start == npos)
+ start = len;
+ for(size_t i = start-1; i != size_t(-1); --i)
+ {
+ for(size_t j = 0; j < chars.len; ++j)
+ {
+ if(str[i] == chars[j])
+ return i;
+ }
+ }
+ return npos;
+ }
+
+public:
+
+ size_t first_not_of(const C c, size_t start=0) const
+ {
+ C4_ASSERT((start >= 0 && start <= len) || (start == len && len == 0));
+ for(size_t i = start; i < len; ++i)
+ {
+ if(str[i] != c)
+ return i;
+ }
+ return npos;
+ }
+
+ size_t last_not_of(const C c, size_t start=npos) const
+ {
+ C4_ASSERT(start == npos || (start >= 0 && start <= len));
+ if(start == npos)
+ start = len;
+ for(size_t i = start-1; i != size_t(-1); --i)
+ {
+ if(str[i] != c)
+ return i;
+ }
+ return npos;
+ }
+
+ size_t first_not_of(ro_substr chars, size_t start=0) const
+ {
+ C4_ASSERT((start >= 0 && start <= len) || (start == len && len == 0));
+ for(size_t i = start; i < len; ++i)
+ {
+ bool gotit = true;
+ for(size_t j = 0; j < chars.len; ++j)
+ {
+ if(str[i] == chars.str[j])
+ {
+ gotit = false;
+ break;
+ }
+ }
+ if(gotit)
+ {
+ return i;
+ }
+ }
+ return npos;
+ }
+
+ size_t last_not_of(ro_substr chars, size_t start=npos) const
+ {
+ C4_ASSERT(start == npos || (start >= 0 && start <= len));
+ if(start == npos)
+ start = len;
+ for(size_t i = start-1; i != size_t(-1); --i)
+ {
+ bool gotit = true;
+ for(size_t j = 0; j < chars.len; ++j)
+ {
+ if(str[i] == chars.str[j])
+ {
+ gotit = false;
+ break;
+ }
+ }
+ if(gotit)
+ {
+ return i;
+ }
+ }
+ return npos;
+ }
+
+ /** @} */
+
+public:
+
+ /** @name Range lookup methods */
+ /** @{ */
+
+ /** get the range delimited by an open-close pair of characters.
+ * @note There must be no nested pairs.
+ * @note No checks for escapes are performed. */
+ basic_substring pair_range(CC open, CC close) const
+ {
+ size_t b = find(open);
+ if(b == npos)
+ return basic_substring();
+ size_t e = find(close, b+1);
+ if(e == npos)
+ return basic_substring();
+ basic_substring ret = range(b, e+1);
+ C4_ASSERT(ret.sub(1).find(open) == npos);
+ return ret;
+ }
+
+ /** get the range delimited by a single open-close character (eg, quotes).
+ * @note The open-close character can be escaped. */
+ basic_substring pair_range_esc(CC open_close, CC escape=CC('\\'))
+ {
+ size_t b = find(open_close);
+ if(b == npos) return basic_substring();
+ for(size_t i = b+1; i < len; ++i)
+ {
+ CC c = str[i];
+ if(c == open_close)
+ {
+ if(str[i-1] != escape)
+ {
+ return range(b, i+1);
+ }
+ }
+ }
+ return basic_substring();
+ }
+
+ /** get the range delimited by an open-close pair of characters,
+ * with possibly nested occurrences. No checks for escapes are
+ * performed. */
+ basic_substring pair_range_nested(CC open, CC close) const
+ {
+ size_t b = find(open);
+ if(b == npos) return basic_substring();
+ size_t e, curr = b+1, count = 0;
+ const char both[] = {open, close, '\0'};
+ while((e = first_of(both, curr)) != npos)
+ {
+ if(str[e] == open)
+ {
+ ++count;
+ curr = e+1;
+ }
+ else if(str[e] == close)
+ {
+ if(count == 0) return range(b, e+1);
+ --count;
+ curr = e+1;
+ }
+ }
+ return basic_substring();
+ }
+
+ basic_substring unquoted() const
+ {
+ constexpr const C dq('"'), sq('\'');
+ if(len >= 2 && (str[len - 2] != C('\\')) &&
+ ((begins_with(sq) && ends_with(sq))
+ ||
+ (begins_with(dq) && ends_with(dq))))
+ {
+ return range(1, len -1);
+ }
+ return *this;
+ }
+
+ /** @} */
+
+public:
+
+ /** @name Number-matching query methods */
+ /** @{ */
+
+ /** @return true if the substring contents are a floating-point or integer number.
+ * @note any leading or trailing whitespace will return false. */
+ bool is_number() const
+ {
+ if(empty() || (first_non_empty_span().empty()))
+ return false;
+ if(first_uint_span() == *this)
+ return true;
+ if(first_int_span() == *this)
+ return true;
+ if(first_real_span() == *this)
+ return true;
+ return false;
+ }
+
+ /** @return true if the substring contents are a real number.
+ * @note any leading or trailing whitespace will return false. */
+ bool is_real() const
+ {
+ if(empty() || (first_non_empty_span().empty()))
+ return false;
+ if(first_real_span() == *this)
+ return true;
+ return false;
+ }
+
+ /** @return true if the substring contents are an integer number.
+ * @note any leading or trailing whitespace will return false. */
+ bool is_integer() const
+ {
+ if(empty() || (first_non_empty_span().empty()))
+ return false;
+ if(first_uint_span() == *this)
+ return true;
+ if(first_int_span() == *this)
+ return true;
+ return false;
+ }
+
+ /** @return true if the substring contents are an unsigned integer number.
+ * @note any leading or trailing whitespace will return false. */
+ bool is_unsigned_integer() const
+ {
+ if(empty() || (first_non_empty_span().empty()))
+ return false;
+ if(first_uint_span() == *this)
+ return true;
+ return false;
+ }
+
+ /** get the first span consisting exclusively of non-empty characters */
+ basic_substring first_non_empty_span() const
+ {
+ constexpr const ro_substr empty_chars(" \n\r\t");
+ size_t pos = first_not_of(empty_chars);
+ if(pos == npos)
+ return first(0);
+ auto ret = sub(pos);
+ pos = ret.first_of(empty_chars);
+ return ret.first(pos);
+ }
+
+ /** get the first span which can be interpreted as an unsigned integer */
+ basic_substring first_uint_span() const
+ {
+ basic_substring ne = first_non_empty_span();
+ if(ne.empty())
+ return ne;
+ if(ne.str[0] == '-')
+ return first(0);
+ size_t skip_start = (ne.str[0] == '+') ? 1 : 0;
+ return ne._first_integral_span(skip_start);
+ }
+
+ /** get the first span which can be interpreted as a signed integer */
+ basic_substring first_int_span() const
+ {
+ basic_substring ne = first_non_empty_span();
+ if(ne.empty())
+ return ne;
+ size_t skip_start = (ne.str[0] == '+' || ne.str[0] == '-') ? 1 : 0;
+ return ne._first_integral_span(skip_start);
+ }
+
+ basic_substring _first_integral_span(size_t skip_start) const
+ {
+ C4_ASSERT(!empty());
+ if(skip_start == len) {
+ return first(0);
+ }
+ C4_ASSERT(skip_start < len);
+ if(first_of_any("0x", "0X")) // hexadecimal
+ {
+ skip_start += 2;
+ if(len == skip_start)
+ return first(0);
+ for(size_t i = skip_start; i < len; ++i)
+ {
+ if( ! _is_hex_char(str[i]))
+ return _is_delim_char(str[i]) ? first(i) : first(0);
+ }
+ }
+ else if(first_of_any("0o", "0O")) // octal
+ {
+ skip_start += 2;
+ if(len == skip_start)
+ return first(0);
+ for(size_t i = skip_start; i < len; ++i)
+ {
+ char c = str[i];
+ if(c < '0' || c > '7')
+ return _is_delim_char(str[i]) ? first(i) : first(0);
+ }
+ }
+ else if(first_of_any("0b", "0B")) // binary
+ {
+ skip_start += 2;
+ if(len == skip_start)
+ return first(0);
+ for(size_t i = skip_start; i < len; ++i)
+ {
+ char c = str[i];
+ if(c != '0' && c != '1')
+ return _is_delim_char(c) ? first(i) : first(0);
+ }
+ }
+ else // otherwise, decimal
+ {
+ if(len == skip_start)
+ return first(0);
+ for(size_t i = skip_start; i < len; ++i)
+ {
+ char c = str[i];
+ if(c < '0' || c > '9')
+ return _is_delim_char(c) ? first(i) : first(0);
+ }
+ }
+ return *this;
+ }
+
+ /** get the first span which can be interpreted as a real (floating-point) number */
+ basic_substring first_real_span() const
+ {
+ basic_substring ne = first_non_empty_span();
+ if(ne.empty())
+ return ne;
+ size_t skip_start = (ne.str[0] == '+' || ne.str[0] == '-') ? 1 : 0;
+ if(ne.first_of_any("0x", "0X")) // hexadecimal
+ {
+ skip_start += 2;
+ if(ne.len == skip_start)
+ return ne.first(0);
+ for(size_t i = skip_start; i < ne.len; ++i)
+ {
+ char c = ne.str[i];
+ if(( ! _is_hex_char(c)) && c != '.' && c != 'p' && c != 'P')
+ {
+ if(c == '-' || c == '+')
+ {
+ // we can also have a sign for the exponent
+ if(i > 1 && (ne[i-1] == 'p' || ne[i-1] == 'P'))
+ {
+ continue;
+ }
+ }
+ return _is_delim_char(c) ? ne.first(i) : ne.first(0);
+ }
+ }
+ }
+ else if(ne.first_of_any("0b", "0B")) // binary
+ {
+ skip_start += 2;
+ if(ne.len == skip_start)
+ return ne.first(0);
+ for(size_t i = skip_start; i < ne.len; ++i)
+ {
+ char c = ne.str[i];
+ if(c != '0' && c != '1' && c != '.')
+ {
+ return _is_delim_char(c) ? ne.first(i) : ne.first(0);
+ }
+ }
+ }
+ else if(ne.first_of_any("0o", "0O")) // octal
+ {
+ skip_start += 2;
+ if(ne.len == skip_start)
+ return ne.first(0);
+ for(size_t i = skip_start; i < ne.len; ++i)
+ {
+ char c = ne.str[i];
+ if((c < '0' || c > '7') && c != '.')
+ {
+ return _is_delim_char(c) ? ne.first(i) : ne.first(0);
+ }
+ }
+ }
+ else // assume decimal
+ {
+ if(ne.len == skip_start)
+ return ne.first(0);
+ for(size_t i = skip_start; i < ne.len; ++i)
+ {
+ char c = ne.str[i];
+ if((c < '0' || c > '9') && (c != '.' && c != 'e' && c != 'E'))
+ {
+ if(c == '-' || c == '+')
+ {
+ // we can also have a sign for the exponent
+ if(i > 1 && (ne[i-1] == 'e' || ne[i-1] == 'E'))
+ {
+ continue;
+ }
+ }
+ else if(i == skip_start)
+ {
+ if(c == 'i')
+ {
+ if(ne.len >= skip_start + 8 && ne.sub(skip_start, 8) == "infinity")
+ return _is_delim_char(ne.str[skip_start + 8]) ? ne.first(skip_start + 8) : ne.first(0);
+ else if(ne.len >= skip_start + 3 && ne.sub(skip_start, 3) == "inf")
+ return _is_delim_char(ne.str[skip_start + 3]) ? ne.first(skip_start + 3) : ne.first(0);
+ else
+ return ne.first(0);
+ }
+ else if(c == 'n')
+ {
+ if(ne.len >= skip_start + 3 && ne.sub(skip_start, 3) == "nan")
+ return _is_delim_char(ne.str[skip_start + 3]) ? ne.first(skip_start + 3) : ne.first(0);
+ else
+ return ne.first(0);
+ }
+ else
+ {
+ return ne.first(0);
+ }
+ }
+ else
+ {
+ return _is_delim_char(c) ? ne.first(i) : ne.first(0);
+ }
+ }
+ }
+ }
+ return ne;
+ }
+
+ /** true if the character is a delimiter character *at the end* */
+ static constexpr C4_ALWAYS_INLINE bool _is_delim_char(char c) noexcept
+ {
+ return c == ' ' || c == '\n' || c == '\r' || c == '\t' || c == '\0'
+ || c == ']' || c == ')' || c == '}'
+ || c == ',' || c == ';';
+ }
+
+ /** true if the character is in [0-9a-fA-F] */
+ static constexpr C4_ALWAYS_INLINE bool _is_hex_char(char c) noexcept
+ {
+ return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
+ }
+
+ /** true if the character is in [0-9a-fA-F] */
+ static constexpr C4_ALWAYS_INLINE bool _is_oct_char(char c) noexcept
+ {
+ return (c >= '0' && c <= '7');
+ }
+
+ /** @} */
+
+public:
+
+ /** @name Splitting methods */
+ /** @{ */
+
+ /** returns true if the string has not been exhausted yet, meaning
+ * it's ok to call next_split() again. When no instance of sep
+ * exists in the string, returns the full string. When the input
+ * is an empty string, the output string is the empty string. */
+ bool next_split(C sep, size_t *C4_RESTRICT start_pos, basic_substring *C4_RESTRICT out) const
+ {
+ if(C4_LIKELY(*start_pos < len))
+ {
+ for(size_t i = *start_pos, e = len; i < e; i++)
+ {
+ if(str[i] == sep)
+ {
+ out->assign(str + *start_pos, i - *start_pos);
+ *start_pos = i+1;
+ return true;
+ }
+ }
+ out->assign(str + *start_pos, len - *start_pos);
+ *start_pos = len + 1;
+ return true;
+ }
+ else
+ {
+ bool valid = len > 0 && (*start_pos == len);
+ if(valid && !empty() && str[len-1] == sep)
+ {
+ out->assign(str + len, (size_t)0); // the cast is needed to prevent overload ambiguity
+ }
+ else
+ {
+ out->assign(str + len + 1, (size_t)0); // the cast is needed to prevent overload ambiguity
+ }
+ *start_pos = len + 1;
+ return valid;
+ }
+ }
+
+private:
+
+ struct split_proxy_impl
+ {
+ struct split_iterator_impl
+ {
+ split_proxy_impl const* m_proxy;
+ basic_substring m_str;
+ size_t m_pos;
+ NCC_ m_sep;
+
+ split_iterator_impl(split_proxy_impl const* proxy, size_t pos, C sep)
+ : m_proxy(proxy), m_pos(pos), m_sep(sep)
+ {
+ _tick();
+ }
+
+ void _tick()
+ {
+ m_proxy->m_str.next_split(m_sep, &m_pos, &m_str);
+ }
+
+ split_iterator_impl& operator++ () { _tick(); return *this; }
+ split_iterator_impl operator++ (int) { split_iterator_impl it = *this; _tick(); return it; }
+
+ basic_substring& operator* () { return m_str; }
+ basic_substring* operator-> () { return &m_str; }
+
+ bool operator!= (split_iterator_impl const& that) const
+ {
+ return !(this->operator==(that));
+ }
+ bool operator== (split_iterator_impl const& that) const
+ {
+ C4_XASSERT((m_sep == that.m_sep) && "cannot compare split iterators with different separators");
+ if(m_str.size() != that.m_str.size())
+ return false;
+ if(m_str.data() != that.m_str.data())
+ return false;
+ return m_pos == that.m_pos;
+ }
+ };
+
+ basic_substring m_str;
+ size_t m_start_pos;
+ C m_sep;
+
+ split_proxy_impl(basic_substring str_, size_t start_pos, C sep)
+ : m_str(str_), m_start_pos(start_pos), m_sep(sep)
+ {
+ }
+
+ split_iterator_impl begin() const
+ {
+ auto it = split_iterator_impl(this, m_start_pos, m_sep);
+ return it;
+ }
+ split_iterator_impl end() const
+ {
+ size_t pos = m_str.size() + 1;
+ auto it = split_iterator_impl(this, pos, m_sep);
+ return it;
+ }
+ };
+
+public:
+
+ using split_proxy = split_proxy_impl;
+
+ /** a view into the splits */
+ split_proxy split(C sep, size_t start_pos=0) const
+ {
+ C4_XASSERT((start_pos >= 0 && start_pos < len) || empty());
+ auto ss = sub(0, len);
+ auto it = split_proxy(ss, start_pos, sep);
+ return it;
+ }
+
+public:
+
+ /** pop right: return the first split from the right. Use
+ * gpop_left() to get the reciprocal part.
+ */
+ basic_substring pop_right(C sep=C('/'), bool skip_empty=false) const
+ {
+ if(C4_LIKELY(len > 1))
+ {
+ auto pos = last_of(sep);
+ if(pos != npos)
+ {
+ if(pos + 1 < len) // does not end with sep
+ {
+ return sub(pos + 1); // return from sep to end
+ }
+ else // the string ends with sep
+ {
+ if( ! skip_empty)
+ {
+ return sub(pos + 1, 0);
+ }
+ auto ppos = last_not_of(sep); // skip repeated seps
+ if(ppos == npos) // the string is all made of seps
+ {
+ return sub(0, 0);
+ }
+ // find the previous sep
+ auto pos0 = last_of(sep, ppos);
+ if(pos0 == npos) // only the last sep exists
+ {
+ return sub(0); // return the full string (because skip_empty is true)
+ }
+ ++pos0;
+ return sub(pos0);
+ }
+ }
+ else // no sep was found, return the full string
+ {
+ return *this;
+ }
+ }
+ else if(len == 1)
+ {
+ if(begins_with(sep))
+ {
+ return sub(0, 0);
+ }
+ return *this;
+ }
+ else // an empty string
+ {
+ return basic_substring();
+ }
+ }
+
+ /** return the first split from the left. Use gpop_right() to get
+ * the reciprocal part. */
+ basic_substring pop_left(C sep = C('/'), bool skip_empty=false) const
+ {
+ if(C4_LIKELY(len > 1))
+ {
+ auto pos = first_of(sep);
+ if(pos != npos)
+ {
+ if(pos > 0) // does not start with sep
+ {
+ return sub(0, pos); // return everything up to it
+ }
+ else // the string starts with sep
+ {
+ if( ! skip_empty)
+ {
+ return sub(0, 0);
+ }
+ auto ppos = first_not_of(sep); // skip repeated seps
+ if(ppos == npos) // the string is all made of seps
+ {
+ return sub(0, 0);
+ }
+ // find the next sep
+ auto pos0 = first_of(sep, ppos);
+ if(pos0 == npos) // only the first sep exists
+ {
+ return sub(0); // return the full string (because skip_empty is true)
+ }
+ C4_XASSERT(pos0 > 0);
+ // return everything up to the second sep
+ return sub(0, pos0);
+ }
+ }
+ else // no sep was found, return the full string
+ {
+ return sub(0);
+ }
+ }
+ else if(len == 1)
+ {
+ if(begins_with(sep))
+ {
+ return sub(0, 0);
+ }
+ return sub(0);
+ }
+ else // an empty string
+ {
+ return basic_substring();
+ }
+ }
+
+public:
+
+ /** greedy pop left. eg, csubstr("a/b/c").gpop_left('/')="c" */
+ basic_substring gpop_left(C sep = C('/'), bool skip_empty=false) const
+ {
+ auto ss = pop_right(sep, skip_empty);
+ ss = left_of(ss);
+ if(ss.find(sep) != npos)
+ {
+ if(ss.ends_with(sep))
+ {
+ if(skip_empty)
+ {
+ ss = ss.trimr(sep);
+ }
+ else
+ {
+ ss = ss.sub(0, ss.len-1); // safe to subtract because ends_with(sep) is true
+ }
+ }
+ }
+ return ss;
+ }
+
+ /** greedy pop right. eg, csubstr("a/b/c").gpop_right('/')="a" */
+ basic_substring gpop_right(C sep = C('/'), bool skip_empty=false) const
+ {
+ auto ss = pop_left(sep, skip_empty);
+ ss = right_of(ss);
+ if(ss.find(sep) != npos)
+ {
+ if(ss.begins_with(sep))
+ {
+ if(skip_empty)
+ {
+ ss = ss.triml(sep);
+ }
+ else
+ {
+ ss = ss.sub(1);
+ }
+ }
+ }
+ return ss;
+ }
+
+ /** @} */
+
+public:
+
+ /** @name Path-like manipulation methods */
+ /** @{ */
+
+ basic_substring basename(C sep=C('/')) const
+ {
+ auto ss = pop_right(sep, /*skip_empty*/true);
+ ss = ss.trimr(sep);
+ return ss;
+ }
+
+ basic_substring dirname(C sep=C('/')) const
+ {
+ auto ss = basename(sep);
+ ss = ss.empty() ? *this : left_of(ss);
+ return ss;
+ }
+
+ C4_ALWAYS_INLINE basic_substring name_wo_extshort() const
+ {
+ return gpop_left('.');
+ }
+
+ C4_ALWAYS_INLINE basic_substring name_wo_extlong() const
+ {
+ return pop_left('.');
+ }
+
+ C4_ALWAYS_INLINE basic_substring extshort() const
+ {
+ return pop_right('.');
+ }
+
+ C4_ALWAYS_INLINE basic_substring extlong() const
+ {
+ return gpop_right('.');
+ }
+
+ /** @} */
+
+public:
+
+ /** @name Content-modification methods (only for non-const C) */
+ /** @{ */
+
+ /** convert the string to upper-case
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(void) toupper()
+ {
+ for(size_t i = 0; i < len; ++i)
+ {
+ str[i] = static_cast<C>(::toupper(str[i]));
+ }
+ }
+
+ /** convert the string to lower-case
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(void) tolower()
+ {
+ for(size_t i = 0; i < len; ++i)
+ {
+ str[i] = static_cast<C>(::tolower(str[i]));
+ }
+ }
+
+public:
+
+ /** fill the entire contents with the given @p val
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(void) fill(C val)
+ {
+ for(size_t i = 0; i < len; ++i)
+ {
+ str[i] = val;
+ }
+ }
+
+public:
+
+ /** set the current substring to a copy of the given csubstr
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(void) copy_from(ro_substr that, size_t ifirst=0, size_t num=npos)
+ {
+ C4_ASSERT(ifirst >= 0 && ifirst <= len);
+ num = num != npos ? num : len - ifirst;
+ num = num < that.len ? num : that.len;
+ C4_ASSERT(ifirst + num >= 0 && ifirst + num <= len);
+ memcpy(str + sizeof(C) * ifirst, that.str, sizeof(C) * num);
+ }
+
+public:
+
+ /** reverse in place
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(void) reverse()
+ {
+ if(len == 0) return;
+ detail::_do_reverse(str, str + len - 1);
+ }
+
+ /** revert a subpart in place
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(void) reverse_sub(size_t ifirst, size_t num)
+ {
+ C4_ASSERT(ifirst >= 0 && ifirst <= len);
+ C4_ASSERT(ifirst + num >= 0 && ifirst + num <= len);
+ if(num == 0) return;
+ detail::_do_reverse(str + ifirst, str + ifirst + num - 1);
+ }
+
+ /** revert a range in place
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(void) reverse_range(size_t ifirst, size_t ilast)
+ {
+ C4_ASSERT(ifirst >= 0 && ifirst <= len);
+ C4_ASSERT(ilast >= 0 && ilast <= len);
+ if(ifirst == ilast) return;
+ detail::_do_reverse(str + ifirst, str + ilast - 1);
+ }
+
+public:
+
+ /** erase part of the string. eg, with char s[] = "0123456789",
+ * substr(s).erase(3, 2) = "01256789", and s is now "01245678989"
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(basic_substring) erase(size_t pos, size_t num)
+ {
+ C4_ASSERT(pos >= 0 && pos+num <= len);
+ size_t num_to_move = len - pos - num;
+ memmove(str + pos, str + pos + num, sizeof(C) * num_to_move);
+ return basic_substring{str, len - num};
+ }
+
+ /** @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(basic_substring) erase_range(size_t first, size_t last)
+ {
+ C4_ASSERT(first <= last);
+ return erase(first, static_cast<size_t>(last-first));
+ }
+
+ /** erase a part of the string.
+ * @note @p sub must be a substring of this string
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(basic_substring) erase(ro_substr sub)
+ {
+ C4_ASSERT(is_super(sub));
+ C4_ASSERT(sub.str >= str);
+ return erase(static_cast<size_t>(sub.str - str), sub.len);
+ }
+
+public:
+
+ /** replace every occurrence of character @p value with the character @p repl
+ * @return the number of characters that were replaced
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(size_t) replace(C value, C repl, size_t pos=0)
+ {
+ C4_ASSERT((pos >= 0 && pos <= len) || pos == npos);
+ size_t did_it = 0;
+ while((pos = find(value, pos)) != npos)
+ {
+ str[pos++] = repl;
+ ++did_it;
+ }
+ return did_it;
+ }
+
+ /** replace every occurrence of each character in @p value with
+ * the character @p repl.
+ * @return the number of characters that were replaced
+ * @note this method requires that the string memory is writeable and is SFINAEd out for const C */
+ C4_REQUIRE_RW(size_t) replace(ro_substr chars, C repl, size_t pos=0)
+ {
+ C4_ASSERT((pos >= 0 && pos <= len) || pos == npos);
+ size_t did_it = 0;
+ while((pos = first_of(chars, pos)) != npos)
+ {
+ str[pos++] = repl;
+ ++did_it;
+ }
+ return did_it;
+ }
+
+ /** replace @p pattern with @p repl, and write the result into
+ * @dst. pattern and repl don't need equal sizes.
+ *
+ * @return the required size for dst. No overflow occurs if
+ * dst.len is smaller than the required size; this can be used to
+ * determine the required size for an existing container. */
+ size_t replace_all(rw_substr dst, ro_substr pattern, ro_substr repl, size_t pos=0) const
+ {
+ C4_ASSERT( ! pattern.empty()); //!< @todo relax this precondition
+ C4_ASSERT( ! this ->overlaps(dst)); //!< @todo relax this precondition
+ C4_ASSERT( ! pattern.overlaps(dst));
+ C4_ASSERT( ! repl .overlaps(dst));
+ C4_ASSERT((pos >= 0 && pos <= len) || pos == npos);
+ C4_SUPPRESS_WARNING_GCC_PUSH
+ C4_SUPPRESS_WARNING_GCC("-Warray-bounds") // gcc11 has a false positive here
+ #if (!defined(__clang__)) && (defined(__GNUC__) && (__GNUC__ >= 7))
+ C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow") // gcc11 has a false positive here
+ #endif
+ #define _c4append(first, last) \
+ { \
+ C4_ASSERT((last) >= (first)); \
+ size_t num = static_cast<size_t>((last) - (first)); \
+ if(sz + num <= dst.len) \
+ { \
+ memcpy(dst.str + sz, first, num * sizeof(C)); \
+ } \
+ sz += num; \
+ }
+ size_t sz = 0;
+ size_t b = pos;
+ _c4append(str, str + pos);
+ do {
+ size_t e = find(pattern, b);
+ if(e == npos)
+ {
+ _c4append(str + b, str + len);
+ break;
+ }
+ _c4append(str + b, str + e);
+ _c4append(repl.begin(), repl.end());
+ b = e + pattern.size();
+ } while(b < len && b != npos);
+ return sz;
+ #undef _c4append
+ C4_SUPPRESS_WARNING_GCC_POP
+ }
+
+ /** @} */
+
+}; // template class basic_substring
+
+
+#undef C4_REQUIRE_RW
+#undef C4_REQUIRE_RO
+#undef C4_NC2C
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** Because of a C++ limitation, substr cannot provide simultaneous
+ * overloads for constructing from a char[N] and a char*; the latter
+ * will always be chosen by the compiler. So this specialization is
+ * provided to simplify obtaining a substr from a char*. Being a
+ * function has the advantage of highlighting the strlen() cost.
+ *
+ * @see to_csubstr
+ * @see For a more detailed explanation on why the overloads cannot
+ * coexist, see http://cplusplus.bordoon.com/specializeForCharacterArrays.html */
+inline substr to_substr(char *s)
+{
+ return substr(s, s ? strlen(s) : 0);
+}
+
+/** Because of a C++ limitation, substr cannot provide simultaneous
+ * overloads for constructing from a char[N] and a char*; the latter
+ * will always be chosen by the compiler. So this specialization is
+ * provided to simplify obtaining a substr from a char*. Being a
+ * function has the advantage of highlighting the strlen() cost.
+ *
+ * @see to_substr
+ * @see For a more detailed explanation on why the overloads cannot
+ * coexist, see http://cplusplus.bordoon.com/specializeForCharacterArrays.html */
+inline csubstr to_csubstr(char *s)
+{
+ return csubstr(s, s ? strlen(s) : 0);
+}
+
+/** Because of a C++ limitation, substr cannot provide simultaneous
+ * overloads for constructing from a const char[N] and a const char*;
+ * the latter will always be chosen by the compiler. So this
+ * specialization is provided to simplify obtaining a substr from a
+ * char*. Being a function has the advantage of highlighting the
+ * strlen() cost.
+ *
+ * @overload to_csubstr
+ * @see to_substr
+ * @see For a more detailed explanation on why the overloads cannot
+ * coexist, see http://cplusplus.bordoon.com/specializeForCharacterArrays.html */
+inline csubstr to_csubstr(const char *s)
+{
+ return csubstr(s, s ? strlen(s) : 0);
+}
+
+
+/** neutral version for use in generic code */
+inline csubstr to_csubstr(csubstr s)
+{
+ return s;
+}
+
+/** neutral version for use in generic code */
+inline csubstr to_csubstr(substr s)
+{
+ return s;
+}
+
+/** neutral version for use in generic code */
+inline substr to_substr(substr s)
+{
+ return s;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+template<typename C, size_t N> inline bool operator== (const C (&s)[N], basic_substring<C> const that) { return that.compare(s) == 0; }
+template<typename C, size_t N> inline bool operator!= (const C (&s)[N], basic_substring<C> const that) { return that.compare(s) != 0; }
+template<typename C, size_t N> inline bool operator< (const C (&s)[N], basic_substring<C> const that) { return that.compare(s) > 0; }
+template<typename C, size_t N> inline bool operator> (const C (&s)[N], basic_substring<C> const that) { return that.compare(s) < 0; }
+template<typename C, size_t N> inline bool operator<= (const C (&s)[N], basic_substring<C> const that) { return that.compare(s) >= 0; }
+template<typename C, size_t N> inline bool operator>= (const C (&s)[N], basic_substring<C> const that) { return that.compare(s) <= 0; }
+
+template<typename C> inline bool operator== (C const c, basic_substring<C> const that) { return that.compare(c) == 0; }
+template<typename C> inline bool operator!= (C const c, basic_substring<C> const that) { return that.compare(c) != 0; }
+template<typename C> inline bool operator< (C const c, basic_substring<C> const that) { return that.compare(c) > 0; }
+template<typename C> inline bool operator> (C const c, basic_substring<C> const that) { return that.compare(c) < 0; }
+template<typename C> inline bool operator<= (C const c, basic_substring<C> const that) { return that.compare(c) >= 0; }
+template<typename C> inline bool operator>= (C const c, basic_substring<C> const that) { return that.compare(c) <= 0; }
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** @define C4_SUBSTR_NO_OSTREAM_LSHIFT doctest does not deal well with
+ * template operator<<
+ * @see https://github.com/onqtam/doctest/pull/431 */
+#ifndef C4_SUBSTR_NO_OSTREAM_LSHIFT
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wsign-conversion"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wsign-conversion"
+#endif
+
+/** output the string to a stream */
+template<class OStream, class C>
+inline OStream& operator<< (OStream& os, basic_substring<C> s)
+{
+ os.write(s.str, s.len);
+ return os;
+}
+
+// this causes ambiguity
+///** this is used by google test */
+//template<class OStream, class C>
+//inline void PrintTo(basic_substring<C> s, OStream* os)
+//{
+// os->write(s.str, s.len);
+//}
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+#endif // !C4_SUBSTR_NO_OSTREAM_LSHIFT
+
+} // namespace c4
+
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#endif /* _C4_SUBSTR_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/substr.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/ext/fast_float.hpp
+// https://github.com/biojppm/c4core/src/c4/ext/fast_float.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_EXT_FAST_FLOAT_HPP_
+#define _C4_EXT_FAST_FLOAT_HPP_
+
+#ifdef _MSC_VER
+# pragma warning(push)
+# pragma warning(disable: 4996) // snprintf/scanf: this function or variable may be unsafe
+#elif defined(__clang__) || defined(__APPLE_CC__) || defined(_LIBCPP_VERSION)
+# pragma clang diagnostic push
+# if (defined(__clang_major__) && _clang_major__ >= 9) || defined(__APPLE_CC__)
+# pragma clang diagnostic ignored "-Wfortify-source"
+# endif
+# pragma clang diagnostic ignored "-Wshift-count-overflow"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wuseless-cast"
+#endif
+
+// fast_float by Daniel Lemire
+// fast_float by João Paulo Magalhaes
+//
+// with contributions from Eugene Golushkov
+// with contributions from Maksim Kita
+// with contributions from Marcin Wojdyr
+// with contributions from Neal Richardson
+// with contributions from Tim Paine
+// with contributions from Fabio Pellacini
+//
+// MIT License Notice
+//
+// MIT License
+//
+// Copyright (c) 2021 The fast_float authors
+//
+// Permission is hereby granted, free of charge, to any
+// person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the
+// Software without restriction, including without
+// limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice
+// shall be included in all copies or substantial portions
+// of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+//
+
+#ifndef FASTFLOAT_FAST_FLOAT_H
+#define FASTFLOAT_FAST_FLOAT_H
+
+#include <system_error>
+
+namespace fast_float {
+enum chars_format {
+ scientific = 1<<0,
+ fixed = 1<<2,
+ hex = 1<<3,
+ general = fixed | scientific
+};
+
+
+struct from_chars_result {
+ const char *ptr;
+ std::errc ec;
+};
+
+struct parse_options {
+ constexpr explicit parse_options(chars_format fmt = chars_format::general,
+ char dot = '.')
+ : format(fmt), decimal_point(dot) {}
+
+ /** Which number formats are accepted */
+ chars_format format;
+ /** The character used as decimal point */
+ char decimal_point;
+};
+
+/**
+ * This function parses the character sequence [first,last) for a number. It parses floating-point numbers expecting
+ * a locale-indepent format equivalent to what is used by std::strtod in the default ("C") locale.
+ * The resulting floating-point value is the closest floating-point values (using either float or double),
+ * using the "round to even" convention for values that would otherwise fall right in-between two values.
+ * That is, we provide exact parsing according to the IEEE standard.
+ *
+ * Given a successful parse, the pointer (`ptr`) in the returned value is set to point right after the
+ * parsed number, and the `value` referenced is set to the parsed value. In case of error, the returned
+ * `ec` contains a representative error, otherwise the default (`std::errc()`) value is stored.
+ *
+ * The implementation does not throw and does not allocate memory (e.g., with `new` or `malloc`).
+ *
+ * Like the C++17 standard, the `fast_float::from_chars` functions take an optional last argument of
+ * the type `fast_float::chars_format`. It is a bitset value: we check whether
+ * `fmt & fast_float::chars_format::fixed` and `fmt & fast_float::chars_format::scientific` are set
+ * to determine whether we allowe the fixed point and scientific notation respectively.
+ * The default is `fast_float::chars_format::general` which allows both `fixed` and `scientific`.
+ */
+template<typename T>
+from_chars_result from_chars(const char *first, const char *last,
+ T &value, chars_format fmt = chars_format::general) noexcept;
+
+/**
+ * Like from_chars, but accepts an `options` argument to govern number parsing.
+ */
+template<typename T>
+from_chars_result from_chars_advanced(const char *first, const char *last,
+ T &value, parse_options options) noexcept;
+
+}
+#endif // FASTFLOAT_FAST_FLOAT_H
+
+#ifndef FASTFLOAT_FLOAT_COMMON_H
+#define FASTFLOAT_FLOAT_COMMON_H
+
+#include <cfloat>
+//included above:
+//#include <cstdint>
+#include <cassert>
+//included above:
+//#include <cstring>
+//included above:
+//#include <type_traits>
+
+#if (defined(__x86_64) || defined(__x86_64__) || defined(_M_X64) \
+ || defined(__amd64) || defined(__aarch64__) || defined(_M_ARM64) \
+ || defined(__MINGW64__) \
+ || defined(__s390x__) \
+ || (defined(__ppc64__) || defined(__PPC64__) || defined(__ppc64le__) || defined(__PPC64LE__)) \
+ || defined(__EMSCRIPTEN__))
+#define FASTFLOAT_64BIT
+#elif (defined(__i386) || defined(__i386__) || defined(_M_IX86) \
+ || defined(__arm__) || defined(_M_ARM) \
+ || defined(__MINGW32__))
+#define FASTFLOAT_32BIT
+#else
+ // Need to check incrementally, since SIZE_MAX is a size_t, avoid overflow.
+ // We can never tell the register width, but the SIZE_MAX is a good approximation.
+ // UINTPTR_MAX and INTPTR_MAX are optional, so avoid them for max portability.
+ #if SIZE_MAX == 0xffff
+ #error Unknown platform (16-bit, unsupported)
+ #elif SIZE_MAX == 0xffffffff
+ #define FASTFLOAT_32BIT
+ #elif SIZE_MAX == 0xffffffffffffffff
+ #define FASTFLOAT_64BIT
+ #else
+ #error Unknown platform (not 32-bit, not 64-bit?)
+ #endif
+#endif
+
+#if ((defined(_WIN32) || defined(_WIN64)) && !defined(__clang__))
+#include <intrin.h>
+#endif
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define FASTFLOAT_VISUAL_STUDIO 1
+#endif
+
+#if defined __BYTE_ORDER__ && defined __ORDER_BIG_ENDIAN__
+#define FASTFLOAT_IS_BIG_ENDIAN (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
+#elif defined _WIN32
+#define FASTFLOAT_IS_BIG_ENDIAN 0
+#else
+#if defined(__APPLE__) || defined(__FreeBSD__)
+#include <machine/endian.h>
+#elif defined(sun) || defined(__sun)
+#include <sys/byteorder.h>
+#else
+#include <endian.h>
+#endif
+#
+#ifndef __BYTE_ORDER__
+// safe choice
+#define FASTFLOAT_IS_BIG_ENDIAN 0
+#endif
+#
+#ifndef __ORDER_LITTLE_ENDIAN__
+// safe choice
+#define FASTFLOAT_IS_BIG_ENDIAN 0
+#endif
+#
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define FASTFLOAT_IS_BIG_ENDIAN 0
+#else
+#define FASTFLOAT_IS_BIG_ENDIAN 1
+#endif
+#endif
+
+#ifdef FASTFLOAT_VISUAL_STUDIO
+#define fastfloat_really_inline __forceinline
+#else
+#define fastfloat_really_inline inline __attribute__((always_inline))
+#endif
+
+#ifndef FASTFLOAT_ASSERT
+#define FASTFLOAT_ASSERT(x) { if (!(x)) abort(); }
+#endif
+
+#ifndef FASTFLOAT_DEBUG_ASSERT
+//included above:
+//#include <cassert>
+#define FASTFLOAT_DEBUG_ASSERT(x) assert(x)
+#endif
+
+// rust style `try!()` macro, or `?` operator
+#define FASTFLOAT_TRY(x) { if (!(x)) return false; }
+
+namespace fast_float {
+
+// Compares two ASCII strings in a case insensitive manner.
+inline bool fastfloat_strncasecmp(const char *input1, const char *input2,
+ size_t length) {
+ char running_diff{0};
+ for (size_t i = 0; i < length; i++) {
+ running_diff |= (input1[i] ^ input2[i]);
+ }
+ return (running_diff == 0) || (running_diff == 32);
+}
+
+#ifndef FLT_EVAL_METHOD
+#error "FLT_EVAL_METHOD should be defined, please include cfloat."
+#endif
+
+// a pointer and a length to a contiguous block of memory
+template <typename T>
+struct span {
+ const T* ptr;
+ size_t length;
+ span(const T* _ptr, size_t _length) : ptr(_ptr), length(_length) {}
+ span() : ptr(nullptr), length(0) {}
+
+ constexpr size_t len() const noexcept {
+ return length;
+ }
+
+ const T& operator[](size_t index) const noexcept {
+ FASTFLOAT_DEBUG_ASSERT(index < length);
+ return ptr[index];
+ }
+};
+
+struct value128 {
+ uint64_t low;
+ uint64_t high;
+ value128(uint64_t _low, uint64_t _high) : low(_low), high(_high) {}
+ value128() : low(0), high(0) {}
+};
+
+/* result might be undefined when input_num is zero */
+fastfloat_really_inline int leading_zeroes(uint64_t input_num) {
+ assert(input_num > 0);
+#ifdef FASTFLOAT_VISUAL_STUDIO
+ #if defined(_M_X64) || defined(_M_ARM64)
+ unsigned long leading_zero = 0;
+ // Search the mask data from most significant bit (MSB)
+ // to least significant bit (LSB) for a set bit (1).
+ _BitScanReverse64(&leading_zero, input_num);
+ return (int)(63 - leading_zero);
+ #else
+ int last_bit = 0;
+ if(input_num & uint64_t(0xffffffff00000000)) input_num >>= 32, last_bit |= 32;
+ if(input_num & uint64_t( 0xffff0000)) input_num >>= 16, last_bit |= 16;
+ if(input_num & uint64_t( 0xff00)) input_num >>= 8, last_bit |= 8;
+ if(input_num & uint64_t( 0xf0)) input_num >>= 4, last_bit |= 4;
+ if(input_num & uint64_t( 0xc)) input_num >>= 2, last_bit |= 2;
+ if(input_num & uint64_t( 0x2)) input_num >>= 1, last_bit |= 1;
+ return 63 - last_bit;
+ #endif
+#else
+ return __builtin_clzll(input_num);
+#endif
+}
+
+#ifdef FASTFLOAT_32BIT
+
+// slow emulation routine for 32-bit
+fastfloat_really_inline uint64_t emulu(uint32_t x, uint32_t y) {
+ return x * (uint64_t)y;
+}
+
+// slow emulation routine for 32-bit
+#if !defined(__MINGW64__)
+fastfloat_really_inline uint64_t _umul128(uint64_t ab, uint64_t cd,
+ uint64_t *hi) {
+ uint64_t ad = emulu((uint32_t)(ab >> 32), (uint32_t)cd);
+ uint64_t bd = emulu((uint32_t)ab, (uint32_t)cd);
+ uint64_t adbc = ad + emulu((uint32_t)ab, (uint32_t)(cd >> 32));
+ uint64_t adbc_carry = !!(adbc < ad);
+ uint64_t lo = bd + (adbc << 32);
+ *hi = emulu((uint32_t)(ab >> 32), (uint32_t)(cd >> 32)) + (adbc >> 32) +
+ (adbc_carry << 32) + !!(lo < bd);
+ return lo;
+}
+#endif // !__MINGW64__
+
+#endif // FASTFLOAT_32BIT
+
+
+// compute 64-bit a*b
+fastfloat_really_inline value128 full_multiplication(uint64_t a,
+ uint64_t b) {
+ value128 answer;
+#ifdef _M_ARM64
+ // ARM64 has native support for 64-bit multiplications, no need to emulate
+ answer.high = __umulh(a, b);
+ answer.low = a * b;
+#elif defined(FASTFLOAT_32BIT) || (defined(_WIN64) && !defined(__clang__))
+ answer.low = _umul128(a, b, &answer.high); // _umul128 not available on ARM64
+#elif defined(FASTFLOAT_64BIT)
+ __uint128_t r = ((__uint128_t)a) * b;
+ answer.low = uint64_t(r);
+ answer.high = uint64_t(r >> 64);
+#else
+ #error Not implemented
+#endif
+ return answer;
+}
+
+struct adjusted_mantissa {
+ uint64_t mantissa{0};
+ int32_t power2{0}; // a negative value indicates an invalid result
+ adjusted_mantissa() = default;
+ bool operator==(const adjusted_mantissa &o) const {
+ return mantissa == o.mantissa && power2 == o.power2;
+ }
+ bool operator!=(const adjusted_mantissa &o) const {
+ return mantissa != o.mantissa || power2 != o.power2;
+ }
+};
+
+// Bias so we can get the real exponent with an invalid adjusted_mantissa.
+constexpr static int32_t invalid_am_bias = -0x8000;
+
+constexpr static double powers_of_ten_double[] = {
+ 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
+ 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
+constexpr static float powers_of_ten_float[] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5,
+ 1e6, 1e7, 1e8, 1e9, 1e10};
+
+template <typename T> struct binary_format {
+ using equiv_uint = typename std::conditional<sizeof(T) == 4, uint32_t, uint64_t>::type;
+
+ static inline constexpr int mantissa_explicit_bits();
+ static inline constexpr int minimum_exponent();
+ static inline constexpr int infinite_power();
+ static inline constexpr int sign_index();
+ static inline constexpr int min_exponent_fast_path();
+ static inline constexpr int max_exponent_fast_path();
+ static inline constexpr int max_exponent_round_to_even();
+ static inline constexpr int min_exponent_round_to_even();
+ static inline constexpr uint64_t max_mantissa_fast_path();
+ static inline constexpr int largest_power_of_ten();
+ static inline constexpr int smallest_power_of_ten();
+ static inline constexpr T exact_power_of_ten(int64_t power);
+ static inline constexpr size_t max_digits();
+ static inline constexpr equiv_uint exponent_mask();
+ static inline constexpr equiv_uint mantissa_mask();
+ static inline constexpr equiv_uint hidden_bit_mask();
+};
+
+template <> inline constexpr int binary_format<double>::mantissa_explicit_bits() {
+ return 52;
+}
+template <> inline constexpr int binary_format<float>::mantissa_explicit_bits() {
+ return 23;
+}
+
+template <> inline constexpr int binary_format<double>::max_exponent_round_to_even() {
+ return 23;
+}
+
+template <> inline constexpr int binary_format<float>::max_exponent_round_to_even() {
+ return 10;
+}
+
+template <> inline constexpr int binary_format<double>::min_exponent_round_to_even() {
+ return -4;
+}
+
+template <> inline constexpr int binary_format<float>::min_exponent_round_to_even() {
+ return -17;
+}
+
+template <> inline constexpr int binary_format<double>::minimum_exponent() {
+ return -1023;
+}
+template <> inline constexpr int binary_format<float>::minimum_exponent() {
+ return -127;
+}
+
+template <> inline constexpr int binary_format<double>::infinite_power() {
+ return 0x7FF;
+}
+template <> inline constexpr int binary_format<float>::infinite_power() {
+ return 0xFF;
+}
+
+template <> inline constexpr int binary_format<double>::sign_index() { return 63; }
+template <> inline constexpr int binary_format<float>::sign_index() { return 31; }
+
+template <> inline constexpr int binary_format<double>::min_exponent_fast_path() {
+#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0)
+ return 0;
+#else
+ return -22;
+#endif
+}
+template <> inline constexpr int binary_format<float>::min_exponent_fast_path() {
+#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0)
+ return 0;
+#else
+ return -10;
+#endif
+}
+
+template <> inline constexpr int binary_format<double>::max_exponent_fast_path() {
+ return 22;
+}
+template <> inline constexpr int binary_format<float>::max_exponent_fast_path() {
+ return 10;
+}
+
+template <> inline constexpr uint64_t binary_format<double>::max_mantissa_fast_path() {
+ return uint64_t(2) << mantissa_explicit_bits();
+}
+template <> inline constexpr uint64_t binary_format<float>::max_mantissa_fast_path() {
+ return uint64_t(2) << mantissa_explicit_bits();
+}
+
+template <>
+inline constexpr double binary_format<double>::exact_power_of_ten(int64_t power) {
+ return powers_of_ten_double[power];
+}
+template <>
+inline constexpr float binary_format<float>::exact_power_of_ten(int64_t power) {
+
+ return powers_of_ten_float[power];
+}
+
+
+template <>
+inline constexpr int binary_format<double>::largest_power_of_ten() {
+ return 308;
+}
+template <>
+inline constexpr int binary_format<float>::largest_power_of_ten() {
+ return 38;
+}
+
+template <>
+inline constexpr int binary_format<double>::smallest_power_of_ten() {
+ return -342;
+}
+template <>
+inline constexpr int binary_format<float>::smallest_power_of_ten() {
+ return -65;
+}
+
+template <> inline constexpr size_t binary_format<double>::max_digits() {
+ return 769;
+}
+template <> inline constexpr size_t binary_format<float>::max_digits() {
+ return 114;
+}
+
+template <> inline constexpr binary_format<float>::equiv_uint
+ binary_format<float>::exponent_mask() {
+ return 0x7F800000;
+}
+template <> inline constexpr binary_format<double>::equiv_uint
+ binary_format<double>::exponent_mask() {
+ return 0x7FF0000000000000;
+}
+
+template <> inline constexpr binary_format<float>::equiv_uint
+ binary_format<float>::mantissa_mask() {
+ return 0x007FFFFF;
+}
+template <> inline constexpr binary_format<double>::equiv_uint
+ binary_format<double>::mantissa_mask() {
+ return 0x000FFFFFFFFFFFFF;
+}
+
+template <> inline constexpr binary_format<float>::equiv_uint
+ binary_format<float>::hidden_bit_mask() {
+ return 0x00800000;
+}
+template <> inline constexpr binary_format<double>::equiv_uint
+ binary_format<double>::hidden_bit_mask() {
+ return 0x0010000000000000;
+}
+
+template<typename T>
+fastfloat_really_inline void to_float(bool negative, adjusted_mantissa am, T &value) {
+ uint64_t word = am.mantissa;
+ word |= uint64_t(am.power2) << binary_format<T>::mantissa_explicit_bits();
+ word = negative
+ ? word | (uint64_t(1) << binary_format<T>::sign_index()) : word;
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ if (std::is_same<T, float>::value) {
+ ::memcpy(&value, (char *)&word + 4, sizeof(T)); // extract value at offset 4-7 if float on big-endian
+ } else {
+ ::memcpy(&value, &word, sizeof(T));
+ }
+#else
+ // For little-endian systems:
+ ::memcpy(&value, &word, sizeof(T));
+#endif
+}
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_ASCII_NUMBER_H
+#define FASTFLOAT_ASCII_NUMBER_H
+
+//included above:
+//#include <cctype>
+//included above:
+//#include <cstdint>
+//included above:
+//#include <cstring>
+#include <iterator>
+
+
+namespace fast_float {
+
+// Next function can be micro-optimized, but compilers are entirely
+// able to optimize it well.
+fastfloat_really_inline bool is_integer(char c) noexcept { return c >= '0' && c <= '9'; }
+
+fastfloat_really_inline uint64_t byteswap(uint64_t val) {
+ return (val & 0xFF00000000000000) >> 56
+ | (val & 0x00FF000000000000) >> 40
+ | (val & 0x0000FF0000000000) >> 24
+ | (val & 0x000000FF00000000) >> 8
+ | (val & 0x00000000FF000000) << 8
+ | (val & 0x0000000000FF0000) << 24
+ | (val & 0x000000000000FF00) << 40
+ | (val & 0x00000000000000FF) << 56;
+}
+
+fastfloat_really_inline uint64_t read_u64(const char *chars) {
+ uint64_t val;
+ ::memcpy(&val, chars, sizeof(uint64_t));
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ // Need to read as-if the number was in little-endian order.
+ val = byteswap(val);
+#endif
+ return val;
+}
+
+fastfloat_really_inline void write_u64(uint8_t *chars, uint64_t val) {
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ // Need to read as-if the number was in little-endian order.
+ val = byteswap(val);
+#endif
+ ::memcpy(chars, &val, sizeof(uint64_t));
+}
+
+// credit @aqrit
+fastfloat_really_inline uint32_t parse_eight_digits_unrolled(uint64_t val) {
+ const uint64_t mask = 0x000000FF000000FF;
+ const uint64_t mul1 = 0x000F424000000064; // 100 + (1000000ULL << 32)
+ const uint64_t mul2 = 0x0000271000000001; // 1 + (10000ULL << 32)
+ val -= 0x3030303030303030;
+ val = (val * 10) + (val >> 8); // val = (val * 2561) >> 8;
+ val = (((val & mask) * mul1) + (((val >> 16) & mask) * mul2)) >> 32;
+ return uint32_t(val);
+}
+
+fastfloat_really_inline uint32_t parse_eight_digits_unrolled(const char *chars) noexcept {
+ return parse_eight_digits_unrolled(read_u64(chars));
+}
+
+// credit @aqrit
+fastfloat_really_inline bool is_made_of_eight_digits_fast(uint64_t val) noexcept {
+ return !((((val + 0x4646464646464646) | (val - 0x3030303030303030)) &
+ 0x8080808080808080));
+}
+
+fastfloat_really_inline bool is_made_of_eight_digits_fast(const char *chars) noexcept {
+ return is_made_of_eight_digits_fast(read_u64(chars));
+}
+
+typedef span<const char> byte_span;
+
+struct parsed_number_string {
+ int64_t exponent{0};
+ uint64_t mantissa{0};
+ const char *lastmatch{nullptr};
+ bool negative{false};
+ bool valid{false};
+ bool too_many_digits{false};
+ // contains the range of the significant digits
+ byte_span integer{}; // non-nullable
+ byte_span fraction{}; // nullable
+};
+
+// Assuming that you use no more than 19 digits, this will
+// parse an ASCII string.
+fastfloat_really_inline
+parsed_number_string parse_number_string(const char *p, const char *pend, parse_options options) noexcept {
+ const chars_format fmt = options.format;
+ const char decimal_point = options.decimal_point;
+
+ parsed_number_string answer;
+ answer.valid = false;
+ answer.too_many_digits = false;
+ answer.negative = (*p == '-');
+ if (*p == '-') { // C++17 20.19.3.(7.1) explicitly forbids '+' sign here
+ ++p;
+ if (p == pend) {
+ return answer;
+ }
+ if (!is_integer(*p) && (*p != decimal_point)) { // a sign must be followed by an integer or the dot
+ return answer;
+ }
+ }
+ const char *const start_digits = p;
+
+ uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad)
+
+ while ((std::distance(p, pend) >= 8) && is_made_of_eight_digits_fast(p)) {
+ i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok
+ p += 8;
+ }
+ while ((p != pend) && is_integer(*p)) {
+ // a multiplication by 10 is cheaper than an arbitrary integer
+ // multiplication
+ i = 10 * i +
+ uint64_t(*p - '0'); // might overflow, we will handle the overflow later
+ ++p;
+ }
+ const char *const end_of_integer_part = p;
+ int64_t digit_count = int64_t(end_of_integer_part - start_digits);
+ answer.integer = byte_span(start_digits, size_t(digit_count));
+ int64_t exponent = 0;
+ if ((p != pend) && (*p == decimal_point)) {
+ ++p;
+ const char* before = p;
+ // can occur at most twice without overflowing, but let it occur more, since
+ // for integers with many digits, digit parsing is the primary bottleneck.
+ while ((std::distance(p, pend) >= 8) && is_made_of_eight_digits_fast(p)) {
+ i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok
+ p += 8;
+ }
+ while ((p != pend) && is_integer(*p)) {
+ uint8_t digit = uint8_t(*p - '0');
+ ++p;
+ i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
+ }
+ exponent = before - p;
+ answer.fraction = byte_span(before, size_t(p - before));
+ digit_count -= exponent;
+ }
+ // we must have encountered at least one integer!
+ if (digit_count == 0) {
+ return answer;
+ }
+ int64_t exp_number = 0; // explicit exponential part
+ if ((fmt & chars_format::scientific) && (p != pend) && (('e' == *p) || ('E' == *p))) {
+ const char * location_of_e = p;
+ ++p;
+ bool neg_exp = false;
+ if ((p != pend) && ('-' == *p)) {
+ neg_exp = true;
+ ++p;
+ } else if ((p != pend) && ('+' == *p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1)
+ ++p;
+ }
+ if ((p == pend) || !is_integer(*p)) {
+ if(!(fmt & chars_format::fixed)) {
+ // We are in error.
+ return answer;
+ }
+ // Otherwise, we will be ignoring the 'e'.
+ p = location_of_e;
+ } else {
+ while ((p != pend) && is_integer(*p)) {
+ uint8_t digit = uint8_t(*p - '0');
+ if (exp_number < 0x10000000) {
+ exp_number = 10 * exp_number + digit;
+ }
+ ++p;
+ }
+ if(neg_exp) { exp_number = - exp_number; }
+ exponent += exp_number;
+ }
+ } else {
+ // If it scientific and not fixed, we have to bail out.
+ if((fmt & chars_format::scientific) && !(fmt & chars_format::fixed)) { return answer; }
+ }
+ answer.lastmatch = p;
+ answer.valid = true;
+
+ // If we frequently had to deal with long strings of digits,
+ // we could extend our code by using a 128-bit integer instead
+ // of a 64-bit integer. However, this is uncommon.
+ //
+ // We can deal with up to 19 digits.
+ if (digit_count > 19) { // this is uncommon
+ // It is possible that the integer had an overflow.
+ // We have to handle the case where we have 0.0000somenumber.
+ // We need to be mindful of the case where we only have zeroes...
+ // E.g., 0.000000000...000.
+ const char *start = start_digits;
+ while ((start != pend) && (*start == '0' || *start == decimal_point)) {
+ if(*start == '0') { digit_count --; }
+ start++;
+ }
+ if (digit_count > 19) {
+ answer.too_many_digits = true;
+ // Let us start again, this time, avoiding overflows.
+ // We don't need to check if is_integer, since we use the
+ // pre-tokenized spans from above.
+ i = 0;
+ p = answer.integer.ptr;
+ const char* int_end = p + answer.integer.len();
+ const uint64_t minimal_nineteen_digit_integer{1000000000000000000};
+ while((i < minimal_nineteen_digit_integer) && (p != int_end)) {
+ i = i * 10 + uint64_t(*p - '0');
+ ++p;
+ }
+ if (i >= minimal_nineteen_digit_integer) { // We have a big integers
+ exponent = end_of_integer_part - p + exp_number;
+ } else { // We have a value with a fractional component.
+ p = answer.fraction.ptr;
+ const char* frac_end = p + answer.fraction.len();
+ while((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
+ i = i * 10 + uint64_t(*p - '0');
+ ++p;
+ }
+ exponent = answer.fraction.ptr - p + exp_number;
+ }
+ // We have now corrected both exponent and i, to a truncated value
+ }
+ }
+ answer.exponent = exponent;
+ answer.mantissa = i;
+ return answer;
+}
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_FAST_TABLE_H
+#define FASTFLOAT_FAST_TABLE_H
+
+//included above:
+//#include <cstdint>
+
+namespace fast_float {
+
+/**
+ * When mapping numbers from decimal to binary,
+ * we go from w * 10^q to m * 2^p but we have
+ * 10^q = 5^q * 2^q, so effectively
+ * we are trying to match
+ * w * 2^q * 5^q to m * 2^p. Thus the powers of two
+ * are not a concern since they can be represented
+ * exactly using the binary notation, only the powers of five
+ * affect the binary significand.
+ */
+
+/**
+ * The smallest non-zero float (binary64) is 2^−1074.
+ * We take as input numbers of the form w x 10^q where w < 2^64.
+ * We have that w * 10^-343 < 2^(64-344) 5^-343 < 2^-1076.
+ * However, we have that
+ * (2^64-1) * 10^-342 = (2^64-1) * 2^-342 * 5^-342 > 2^−1074.
+ * Thus it is possible for a number of the form w * 10^-342 where
+ * w is a 64-bit value to be a non-zero floating-point number.
+ *********
+ * Any number of form w * 10^309 where w>= 1 is going to be
+ * infinite in binary64 so we never need to worry about powers
+ * of 5 greater than 308.
+ */
+template <class unused = void>
+struct powers_template {
+
+constexpr static int smallest_power_of_five = binary_format<double>::smallest_power_of_ten();
+constexpr static int largest_power_of_five = binary_format<double>::largest_power_of_ten();
+constexpr static int number_of_entries = 2 * (largest_power_of_five - smallest_power_of_five + 1);
+// Powers of five from 5^-342 all the way to 5^308 rounded toward one.
+static const uint64_t power_of_five_128[number_of_entries];
+};
+
+template <class unused>
+const uint64_t powers_template<unused>::power_of_five_128[number_of_entries] = {
+ 0xeef453d6923bd65a,0x113faa2906a13b3f,
+ 0x9558b4661b6565f8,0x4ac7ca59a424c507,
+ 0xbaaee17fa23ebf76,0x5d79bcf00d2df649,
+ 0xe95a99df8ace6f53,0xf4d82c2c107973dc,
+ 0x91d8a02bb6c10594,0x79071b9b8a4be869,
+ 0xb64ec836a47146f9,0x9748e2826cdee284,
+ 0xe3e27a444d8d98b7,0xfd1b1b2308169b25,
+ 0x8e6d8c6ab0787f72,0xfe30f0f5e50e20f7,
+ 0xb208ef855c969f4f,0xbdbd2d335e51a935,
+ 0xde8b2b66b3bc4723,0xad2c788035e61382,
+ 0x8b16fb203055ac76,0x4c3bcb5021afcc31,
+ 0xaddcb9e83c6b1793,0xdf4abe242a1bbf3d,
+ 0xd953e8624b85dd78,0xd71d6dad34a2af0d,
+ 0x87d4713d6f33aa6b,0x8672648c40e5ad68,
+ 0xa9c98d8ccb009506,0x680efdaf511f18c2,
+ 0xd43bf0effdc0ba48,0x212bd1b2566def2,
+ 0x84a57695fe98746d,0x14bb630f7604b57,
+ 0xa5ced43b7e3e9188,0x419ea3bd35385e2d,
+ 0xcf42894a5dce35ea,0x52064cac828675b9,
+ 0x818995ce7aa0e1b2,0x7343efebd1940993,
+ 0xa1ebfb4219491a1f,0x1014ebe6c5f90bf8,
+ 0xca66fa129f9b60a6,0xd41a26e077774ef6,
+ 0xfd00b897478238d0,0x8920b098955522b4,
+ 0x9e20735e8cb16382,0x55b46e5f5d5535b0,
+ 0xc5a890362fddbc62,0xeb2189f734aa831d,
+ 0xf712b443bbd52b7b,0xa5e9ec7501d523e4,
+ 0x9a6bb0aa55653b2d,0x47b233c92125366e,
+ 0xc1069cd4eabe89f8,0x999ec0bb696e840a,
+ 0xf148440a256e2c76,0xc00670ea43ca250d,
+ 0x96cd2a865764dbca,0x380406926a5e5728,
+ 0xbc807527ed3e12bc,0xc605083704f5ecf2,
+ 0xeba09271e88d976b,0xf7864a44c633682e,
+ 0x93445b8731587ea3,0x7ab3ee6afbe0211d,
+ 0xb8157268fdae9e4c,0x5960ea05bad82964,
+ 0xe61acf033d1a45df,0x6fb92487298e33bd,
+ 0x8fd0c16206306bab,0xa5d3b6d479f8e056,
+ 0xb3c4f1ba87bc8696,0x8f48a4899877186c,
+ 0xe0b62e2929aba83c,0x331acdabfe94de87,
+ 0x8c71dcd9ba0b4925,0x9ff0c08b7f1d0b14,
+ 0xaf8e5410288e1b6f,0x7ecf0ae5ee44dd9,
+ 0xdb71e91432b1a24a,0xc9e82cd9f69d6150,
+ 0x892731ac9faf056e,0xbe311c083a225cd2,
+ 0xab70fe17c79ac6ca,0x6dbd630a48aaf406,
+ 0xd64d3d9db981787d,0x92cbbccdad5b108,
+ 0x85f0468293f0eb4e,0x25bbf56008c58ea5,
+ 0xa76c582338ed2621,0xaf2af2b80af6f24e,
+ 0xd1476e2c07286faa,0x1af5af660db4aee1,
+ 0x82cca4db847945ca,0x50d98d9fc890ed4d,
+ 0xa37fce126597973c,0xe50ff107bab528a0,
+ 0xcc5fc196fefd7d0c,0x1e53ed49a96272c8,
+ 0xff77b1fcbebcdc4f,0x25e8e89c13bb0f7a,
+ 0x9faacf3df73609b1,0x77b191618c54e9ac,
+ 0xc795830d75038c1d,0xd59df5b9ef6a2417,
+ 0xf97ae3d0d2446f25,0x4b0573286b44ad1d,
+ 0x9becce62836ac577,0x4ee367f9430aec32,
+ 0xc2e801fb244576d5,0x229c41f793cda73f,
+ 0xf3a20279ed56d48a,0x6b43527578c1110f,
+ 0x9845418c345644d6,0x830a13896b78aaa9,
+ 0xbe5691ef416bd60c,0x23cc986bc656d553,
+ 0xedec366b11c6cb8f,0x2cbfbe86b7ec8aa8,
+ 0x94b3a202eb1c3f39,0x7bf7d71432f3d6a9,
+ 0xb9e08a83a5e34f07,0xdaf5ccd93fb0cc53,
+ 0xe858ad248f5c22c9,0xd1b3400f8f9cff68,
+ 0x91376c36d99995be,0x23100809b9c21fa1,
+ 0xb58547448ffffb2d,0xabd40a0c2832a78a,
+ 0xe2e69915b3fff9f9,0x16c90c8f323f516c,
+ 0x8dd01fad907ffc3b,0xae3da7d97f6792e3,
+ 0xb1442798f49ffb4a,0x99cd11cfdf41779c,
+ 0xdd95317f31c7fa1d,0x40405643d711d583,
+ 0x8a7d3eef7f1cfc52,0x482835ea666b2572,
+ 0xad1c8eab5ee43b66,0xda3243650005eecf,
+ 0xd863b256369d4a40,0x90bed43e40076a82,
+ 0x873e4f75e2224e68,0x5a7744a6e804a291,
+ 0xa90de3535aaae202,0x711515d0a205cb36,
+ 0xd3515c2831559a83,0xd5a5b44ca873e03,
+ 0x8412d9991ed58091,0xe858790afe9486c2,
+ 0xa5178fff668ae0b6,0x626e974dbe39a872,
+ 0xce5d73ff402d98e3,0xfb0a3d212dc8128f,
+ 0x80fa687f881c7f8e,0x7ce66634bc9d0b99,
+ 0xa139029f6a239f72,0x1c1fffc1ebc44e80,
+ 0xc987434744ac874e,0xa327ffb266b56220,
+ 0xfbe9141915d7a922,0x4bf1ff9f0062baa8,
+ 0x9d71ac8fada6c9b5,0x6f773fc3603db4a9,
+ 0xc4ce17b399107c22,0xcb550fb4384d21d3,
+ 0xf6019da07f549b2b,0x7e2a53a146606a48,
+ 0x99c102844f94e0fb,0x2eda7444cbfc426d,
+ 0xc0314325637a1939,0xfa911155fefb5308,
+ 0xf03d93eebc589f88,0x793555ab7eba27ca,
+ 0x96267c7535b763b5,0x4bc1558b2f3458de,
+ 0xbbb01b9283253ca2,0x9eb1aaedfb016f16,
+ 0xea9c227723ee8bcb,0x465e15a979c1cadc,
+ 0x92a1958a7675175f,0xbfacd89ec191ec9,
+ 0xb749faed14125d36,0xcef980ec671f667b,
+ 0xe51c79a85916f484,0x82b7e12780e7401a,
+ 0x8f31cc0937ae58d2,0xd1b2ecb8b0908810,
+ 0xb2fe3f0b8599ef07,0x861fa7e6dcb4aa15,
+ 0xdfbdcece67006ac9,0x67a791e093e1d49a,
+ 0x8bd6a141006042bd,0xe0c8bb2c5c6d24e0,
+ 0xaecc49914078536d,0x58fae9f773886e18,
+ 0xda7f5bf590966848,0xaf39a475506a899e,
+ 0x888f99797a5e012d,0x6d8406c952429603,
+ 0xaab37fd7d8f58178,0xc8e5087ba6d33b83,
+ 0xd5605fcdcf32e1d6,0xfb1e4a9a90880a64,
+ 0x855c3be0a17fcd26,0x5cf2eea09a55067f,
+ 0xa6b34ad8c9dfc06f,0xf42faa48c0ea481e,
+ 0xd0601d8efc57b08b,0xf13b94daf124da26,
+ 0x823c12795db6ce57,0x76c53d08d6b70858,
+ 0xa2cb1717b52481ed,0x54768c4b0c64ca6e,
+ 0xcb7ddcdda26da268,0xa9942f5dcf7dfd09,
+ 0xfe5d54150b090b02,0xd3f93b35435d7c4c,
+ 0x9efa548d26e5a6e1,0xc47bc5014a1a6daf,
+ 0xc6b8e9b0709f109a,0x359ab6419ca1091b,
+ 0xf867241c8cc6d4c0,0xc30163d203c94b62,
+ 0x9b407691d7fc44f8,0x79e0de63425dcf1d,
+ 0xc21094364dfb5636,0x985915fc12f542e4,
+ 0xf294b943e17a2bc4,0x3e6f5b7b17b2939d,
+ 0x979cf3ca6cec5b5a,0xa705992ceecf9c42,
+ 0xbd8430bd08277231,0x50c6ff782a838353,
+ 0xece53cec4a314ebd,0xa4f8bf5635246428,
+ 0x940f4613ae5ed136,0x871b7795e136be99,
+ 0xb913179899f68584,0x28e2557b59846e3f,
+ 0xe757dd7ec07426e5,0x331aeada2fe589cf,
+ 0x9096ea6f3848984f,0x3ff0d2c85def7621,
+ 0xb4bca50b065abe63,0xfed077a756b53a9,
+ 0xe1ebce4dc7f16dfb,0xd3e8495912c62894,
+ 0x8d3360f09cf6e4bd,0x64712dd7abbbd95c,
+ 0xb080392cc4349dec,0xbd8d794d96aacfb3,
+ 0xdca04777f541c567,0xecf0d7a0fc5583a0,
+ 0x89e42caaf9491b60,0xf41686c49db57244,
+ 0xac5d37d5b79b6239,0x311c2875c522ced5,
+ 0xd77485cb25823ac7,0x7d633293366b828b,
+ 0x86a8d39ef77164bc,0xae5dff9c02033197,
+ 0xa8530886b54dbdeb,0xd9f57f830283fdfc,
+ 0xd267caa862a12d66,0xd072df63c324fd7b,
+ 0x8380dea93da4bc60,0x4247cb9e59f71e6d,
+ 0xa46116538d0deb78,0x52d9be85f074e608,
+ 0xcd795be870516656,0x67902e276c921f8b,
+ 0x806bd9714632dff6,0xba1cd8a3db53b6,
+ 0xa086cfcd97bf97f3,0x80e8a40eccd228a4,
+ 0xc8a883c0fdaf7df0,0x6122cd128006b2cd,
+ 0xfad2a4b13d1b5d6c,0x796b805720085f81,
+ 0x9cc3a6eec6311a63,0xcbe3303674053bb0,
+ 0xc3f490aa77bd60fc,0xbedbfc4411068a9c,
+ 0xf4f1b4d515acb93b,0xee92fb5515482d44,
+ 0x991711052d8bf3c5,0x751bdd152d4d1c4a,
+ 0xbf5cd54678eef0b6,0xd262d45a78a0635d,
+ 0xef340a98172aace4,0x86fb897116c87c34,
+ 0x9580869f0e7aac0e,0xd45d35e6ae3d4da0,
+ 0xbae0a846d2195712,0x8974836059cca109,
+ 0xe998d258869facd7,0x2bd1a438703fc94b,
+ 0x91ff83775423cc06,0x7b6306a34627ddcf,
+ 0xb67f6455292cbf08,0x1a3bc84c17b1d542,
+ 0xe41f3d6a7377eeca,0x20caba5f1d9e4a93,
+ 0x8e938662882af53e,0x547eb47b7282ee9c,
+ 0xb23867fb2a35b28d,0xe99e619a4f23aa43,
+ 0xdec681f9f4c31f31,0x6405fa00e2ec94d4,
+ 0x8b3c113c38f9f37e,0xde83bc408dd3dd04,
+ 0xae0b158b4738705e,0x9624ab50b148d445,
+ 0xd98ddaee19068c76,0x3badd624dd9b0957,
+ 0x87f8a8d4cfa417c9,0xe54ca5d70a80e5d6,
+ 0xa9f6d30a038d1dbc,0x5e9fcf4ccd211f4c,
+ 0xd47487cc8470652b,0x7647c3200069671f,
+ 0x84c8d4dfd2c63f3b,0x29ecd9f40041e073,
+ 0xa5fb0a17c777cf09,0xf468107100525890,
+ 0xcf79cc9db955c2cc,0x7182148d4066eeb4,
+ 0x81ac1fe293d599bf,0xc6f14cd848405530,
+ 0xa21727db38cb002f,0xb8ada00e5a506a7c,
+ 0xca9cf1d206fdc03b,0xa6d90811f0e4851c,
+ 0xfd442e4688bd304a,0x908f4a166d1da663,
+ 0x9e4a9cec15763e2e,0x9a598e4e043287fe,
+ 0xc5dd44271ad3cdba,0x40eff1e1853f29fd,
+ 0xf7549530e188c128,0xd12bee59e68ef47c,
+ 0x9a94dd3e8cf578b9,0x82bb74f8301958ce,
+ 0xc13a148e3032d6e7,0xe36a52363c1faf01,
+ 0xf18899b1bc3f8ca1,0xdc44e6c3cb279ac1,
+ 0x96f5600f15a7b7e5,0x29ab103a5ef8c0b9,
+ 0xbcb2b812db11a5de,0x7415d448f6b6f0e7,
+ 0xebdf661791d60f56,0x111b495b3464ad21,
+ 0x936b9fcebb25c995,0xcab10dd900beec34,
+ 0xb84687c269ef3bfb,0x3d5d514f40eea742,
+ 0xe65829b3046b0afa,0xcb4a5a3112a5112,
+ 0x8ff71a0fe2c2e6dc,0x47f0e785eaba72ab,
+ 0xb3f4e093db73a093,0x59ed216765690f56,
+ 0xe0f218b8d25088b8,0x306869c13ec3532c,
+ 0x8c974f7383725573,0x1e414218c73a13fb,
+ 0xafbd2350644eeacf,0xe5d1929ef90898fa,
+ 0xdbac6c247d62a583,0xdf45f746b74abf39,
+ 0x894bc396ce5da772,0x6b8bba8c328eb783,
+ 0xab9eb47c81f5114f,0x66ea92f3f326564,
+ 0xd686619ba27255a2,0xc80a537b0efefebd,
+ 0x8613fd0145877585,0xbd06742ce95f5f36,
+ 0xa798fc4196e952e7,0x2c48113823b73704,
+ 0xd17f3b51fca3a7a0,0xf75a15862ca504c5,
+ 0x82ef85133de648c4,0x9a984d73dbe722fb,
+ 0xa3ab66580d5fdaf5,0xc13e60d0d2e0ebba,
+ 0xcc963fee10b7d1b3,0x318df905079926a8,
+ 0xffbbcfe994e5c61f,0xfdf17746497f7052,
+ 0x9fd561f1fd0f9bd3,0xfeb6ea8bedefa633,
+ 0xc7caba6e7c5382c8,0xfe64a52ee96b8fc0,
+ 0xf9bd690a1b68637b,0x3dfdce7aa3c673b0,
+ 0x9c1661a651213e2d,0x6bea10ca65c084e,
+ 0xc31bfa0fe5698db8,0x486e494fcff30a62,
+ 0xf3e2f893dec3f126,0x5a89dba3c3efccfa,
+ 0x986ddb5c6b3a76b7,0xf89629465a75e01c,
+ 0xbe89523386091465,0xf6bbb397f1135823,
+ 0xee2ba6c0678b597f,0x746aa07ded582e2c,
+ 0x94db483840b717ef,0xa8c2a44eb4571cdc,
+ 0xba121a4650e4ddeb,0x92f34d62616ce413,
+ 0xe896a0d7e51e1566,0x77b020baf9c81d17,
+ 0x915e2486ef32cd60,0xace1474dc1d122e,
+ 0xb5b5ada8aaff80b8,0xd819992132456ba,
+ 0xe3231912d5bf60e6,0x10e1fff697ed6c69,
+ 0x8df5efabc5979c8f,0xca8d3ffa1ef463c1,
+ 0xb1736b96b6fd83b3,0xbd308ff8a6b17cb2,
+ 0xddd0467c64bce4a0,0xac7cb3f6d05ddbde,
+ 0x8aa22c0dbef60ee4,0x6bcdf07a423aa96b,
+ 0xad4ab7112eb3929d,0x86c16c98d2c953c6,
+ 0xd89d64d57a607744,0xe871c7bf077ba8b7,
+ 0x87625f056c7c4a8b,0x11471cd764ad4972,
+ 0xa93af6c6c79b5d2d,0xd598e40d3dd89bcf,
+ 0xd389b47879823479,0x4aff1d108d4ec2c3,
+ 0x843610cb4bf160cb,0xcedf722a585139ba,
+ 0xa54394fe1eedb8fe,0xc2974eb4ee658828,
+ 0xce947a3da6a9273e,0x733d226229feea32,
+ 0x811ccc668829b887,0x806357d5a3f525f,
+ 0xa163ff802a3426a8,0xca07c2dcb0cf26f7,
+ 0xc9bcff6034c13052,0xfc89b393dd02f0b5,
+ 0xfc2c3f3841f17c67,0xbbac2078d443ace2,
+ 0x9d9ba7832936edc0,0xd54b944b84aa4c0d,
+ 0xc5029163f384a931,0xa9e795e65d4df11,
+ 0xf64335bcf065d37d,0x4d4617b5ff4a16d5,
+ 0x99ea0196163fa42e,0x504bced1bf8e4e45,
+ 0xc06481fb9bcf8d39,0xe45ec2862f71e1d6,
+ 0xf07da27a82c37088,0x5d767327bb4e5a4c,
+ 0x964e858c91ba2655,0x3a6a07f8d510f86f,
+ 0xbbe226efb628afea,0x890489f70a55368b,
+ 0xeadab0aba3b2dbe5,0x2b45ac74ccea842e,
+ 0x92c8ae6b464fc96f,0x3b0b8bc90012929d,
+ 0xb77ada0617e3bbcb,0x9ce6ebb40173744,
+ 0xe55990879ddcaabd,0xcc420a6a101d0515,
+ 0x8f57fa54c2a9eab6,0x9fa946824a12232d,
+ 0xb32df8e9f3546564,0x47939822dc96abf9,
+ 0xdff9772470297ebd,0x59787e2b93bc56f7,
+ 0x8bfbea76c619ef36,0x57eb4edb3c55b65a,
+ 0xaefae51477a06b03,0xede622920b6b23f1,
+ 0xdab99e59958885c4,0xe95fab368e45eced,
+ 0x88b402f7fd75539b,0x11dbcb0218ebb414,
+ 0xaae103b5fcd2a881,0xd652bdc29f26a119,
+ 0xd59944a37c0752a2,0x4be76d3346f0495f,
+ 0x857fcae62d8493a5,0x6f70a4400c562ddb,
+ 0xa6dfbd9fb8e5b88e,0xcb4ccd500f6bb952,
+ 0xd097ad07a71f26b2,0x7e2000a41346a7a7,
+ 0x825ecc24c873782f,0x8ed400668c0c28c8,
+ 0xa2f67f2dfa90563b,0x728900802f0f32fa,
+ 0xcbb41ef979346bca,0x4f2b40a03ad2ffb9,
+ 0xfea126b7d78186bc,0xe2f610c84987bfa8,
+ 0x9f24b832e6b0f436,0xdd9ca7d2df4d7c9,
+ 0xc6ede63fa05d3143,0x91503d1c79720dbb,
+ 0xf8a95fcf88747d94,0x75a44c6397ce912a,
+ 0x9b69dbe1b548ce7c,0xc986afbe3ee11aba,
+ 0xc24452da229b021b,0xfbe85badce996168,
+ 0xf2d56790ab41c2a2,0xfae27299423fb9c3,
+ 0x97c560ba6b0919a5,0xdccd879fc967d41a,
+ 0xbdb6b8e905cb600f,0x5400e987bbc1c920,
+ 0xed246723473e3813,0x290123e9aab23b68,
+ 0x9436c0760c86e30b,0xf9a0b6720aaf6521,
+ 0xb94470938fa89bce,0xf808e40e8d5b3e69,
+ 0xe7958cb87392c2c2,0xb60b1d1230b20e04,
+ 0x90bd77f3483bb9b9,0xb1c6f22b5e6f48c2,
+ 0xb4ecd5f01a4aa828,0x1e38aeb6360b1af3,
+ 0xe2280b6c20dd5232,0x25c6da63c38de1b0,
+ 0x8d590723948a535f,0x579c487e5a38ad0e,
+ 0xb0af48ec79ace837,0x2d835a9df0c6d851,
+ 0xdcdb1b2798182244,0xf8e431456cf88e65,
+ 0x8a08f0f8bf0f156b,0x1b8e9ecb641b58ff,
+ 0xac8b2d36eed2dac5,0xe272467e3d222f3f,
+ 0xd7adf884aa879177,0x5b0ed81dcc6abb0f,
+ 0x86ccbb52ea94baea,0x98e947129fc2b4e9,
+ 0xa87fea27a539e9a5,0x3f2398d747b36224,
+ 0xd29fe4b18e88640e,0x8eec7f0d19a03aad,
+ 0x83a3eeeef9153e89,0x1953cf68300424ac,
+ 0xa48ceaaab75a8e2b,0x5fa8c3423c052dd7,
+ 0xcdb02555653131b6,0x3792f412cb06794d,
+ 0x808e17555f3ebf11,0xe2bbd88bbee40bd0,
+ 0xa0b19d2ab70e6ed6,0x5b6aceaeae9d0ec4,
+ 0xc8de047564d20a8b,0xf245825a5a445275,
+ 0xfb158592be068d2e,0xeed6e2f0f0d56712,
+ 0x9ced737bb6c4183d,0x55464dd69685606b,
+ 0xc428d05aa4751e4c,0xaa97e14c3c26b886,
+ 0xf53304714d9265df,0xd53dd99f4b3066a8,
+ 0x993fe2c6d07b7fab,0xe546a8038efe4029,
+ 0xbf8fdb78849a5f96,0xde98520472bdd033,
+ 0xef73d256a5c0f77c,0x963e66858f6d4440,
+ 0x95a8637627989aad,0xdde7001379a44aa8,
+ 0xbb127c53b17ec159,0x5560c018580d5d52,
+ 0xe9d71b689dde71af,0xaab8f01e6e10b4a6,
+ 0x9226712162ab070d,0xcab3961304ca70e8,
+ 0xb6b00d69bb55c8d1,0x3d607b97c5fd0d22,
+ 0xe45c10c42a2b3b05,0x8cb89a7db77c506a,
+ 0x8eb98a7a9a5b04e3,0x77f3608e92adb242,
+ 0xb267ed1940f1c61c,0x55f038b237591ed3,
+ 0xdf01e85f912e37a3,0x6b6c46dec52f6688,
+ 0x8b61313bbabce2c6,0x2323ac4b3b3da015,
+ 0xae397d8aa96c1b77,0xabec975e0a0d081a,
+ 0xd9c7dced53c72255,0x96e7bd358c904a21,
+ 0x881cea14545c7575,0x7e50d64177da2e54,
+ 0xaa242499697392d2,0xdde50bd1d5d0b9e9,
+ 0xd4ad2dbfc3d07787,0x955e4ec64b44e864,
+ 0x84ec3c97da624ab4,0xbd5af13bef0b113e,
+ 0xa6274bbdd0fadd61,0xecb1ad8aeacdd58e,
+ 0xcfb11ead453994ba,0x67de18eda5814af2,
+ 0x81ceb32c4b43fcf4,0x80eacf948770ced7,
+ 0xa2425ff75e14fc31,0xa1258379a94d028d,
+ 0xcad2f7f5359a3b3e,0x96ee45813a04330,
+ 0xfd87b5f28300ca0d,0x8bca9d6e188853fc,
+ 0x9e74d1b791e07e48,0x775ea264cf55347e,
+ 0xc612062576589dda,0x95364afe032a819e,
+ 0xf79687aed3eec551,0x3a83ddbd83f52205,
+ 0x9abe14cd44753b52,0xc4926a9672793543,
+ 0xc16d9a0095928a27,0x75b7053c0f178294,
+ 0xf1c90080baf72cb1,0x5324c68b12dd6339,
+ 0x971da05074da7bee,0xd3f6fc16ebca5e04,
+ 0xbce5086492111aea,0x88f4bb1ca6bcf585,
+ 0xec1e4a7db69561a5,0x2b31e9e3d06c32e6,
+ 0x9392ee8e921d5d07,0x3aff322e62439fd0,
+ 0xb877aa3236a4b449,0x9befeb9fad487c3,
+ 0xe69594bec44de15b,0x4c2ebe687989a9b4,
+ 0x901d7cf73ab0acd9,0xf9d37014bf60a11,
+ 0xb424dc35095cd80f,0x538484c19ef38c95,
+ 0xe12e13424bb40e13,0x2865a5f206b06fba,
+ 0x8cbccc096f5088cb,0xf93f87b7442e45d4,
+ 0xafebff0bcb24aafe,0xf78f69a51539d749,
+ 0xdbe6fecebdedd5be,0xb573440e5a884d1c,
+ 0x89705f4136b4a597,0x31680a88f8953031,
+ 0xabcc77118461cefc,0xfdc20d2b36ba7c3e,
+ 0xd6bf94d5e57a42bc,0x3d32907604691b4d,
+ 0x8637bd05af6c69b5,0xa63f9a49c2c1b110,
+ 0xa7c5ac471b478423,0xfcf80dc33721d54,
+ 0xd1b71758e219652b,0xd3c36113404ea4a9,
+ 0x83126e978d4fdf3b,0x645a1cac083126ea,
+ 0xa3d70a3d70a3d70a,0x3d70a3d70a3d70a4,
+ 0xcccccccccccccccc,0xcccccccccccccccd,
+ 0x8000000000000000,0x0,
+ 0xa000000000000000,0x0,
+ 0xc800000000000000,0x0,
+ 0xfa00000000000000,0x0,
+ 0x9c40000000000000,0x0,
+ 0xc350000000000000,0x0,
+ 0xf424000000000000,0x0,
+ 0x9896800000000000,0x0,
+ 0xbebc200000000000,0x0,
+ 0xee6b280000000000,0x0,
+ 0x9502f90000000000,0x0,
+ 0xba43b74000000000,0x0,
+ 0xe8d4a51000000000,0x0,
+ 0x9184e72a00000000,0x0,
+ 0xb5e620f480000000,0x0,
+ 0xe35fa931a0000000,0x0,
+ 0x8e1bc9bf04000000,0x0,
+ 0xb1a2bc2ec5000000,0x0,
+ 0xde0b6b3a76400000,0x0,
+ 0x8ac7230489e80000,0x0,
+ 0xad78ebc5ac620000,0x0,
+ 0xd8d726b7177a8000,0x0,
+ 0x878678326eac9000,0x0,
+ 0xa968163f0a57b400,0x0,
+ 0xd3c21bcecceda100,0x0,
+ 0x84595161401484a0,0x0,
+ 0xa56fa5b99019a5c8,0x0,
+ 0xcecb8f27f4200f3a,0x0,
+ 0x813f3978f8940984,0x4000000000000000,
+ 0xa18f07d736b90be5,0x5000000000000000,
+ 0xc9f2c9cd04674ede,0xa400000000000000,
+ 0xfc6f7c4045812296,0x4d00000000000000,
+ 0x9dc5ada82b70b59d,0xf020000000000000,
+ 0xc5371912364ce305,0x6c28000000000000,
+ 0xf684df56c3e01bc6,0xc732000000000000,
+ 0x9a130b963a6c115c,0x3c7f400000000000,
+ 0xc097ce7bc90715b3,0x4b9f100000000000,
+ 0xf0bdc21abb48db20,0x1e86d40000000000,
+ 0x96769950b50d88f4,0x1314448000000000,
+ 0xbc143fa4e250eb31,0x17d955a000000000,
+ 0xeb194f8e1ae525fd,0x5dcfab0800000000,
+ 0x92efd1b8d0cf37be,0x5aa1cae500000000,
+ 0xb7abc627050305ad,0xf14a3d9e40000000,
+ 0xe596b7b0c643c719,0x6d9ccd05d0000000,
+ 0x8f7e32ce7bea5c6f,0xe4820023a2000000,
+ 0xb35dbf821ae4f38b,0xdda2802c8a800000,
+ 0xe0352f62a19e306e,0xd50b2037ad200000,
+ 0x8c213d9da502de45,0x4526f422cc340000,
+ 0xaf298d050e4395d6,0x9670b12b7f410000,
+ 0xdaf3f04651d47b4c,0x3c0cdd765f114000,
+ 0x88d8762bf324cd0f,0xa5880a69fb6ac800,
+ 0xab0e93b6efee0053,0x8eea0d047a457a00,
+ 0xd5d238a4abe98068,0x72a4904598d6d880,
+ 0x85a36366eb71f041,0x47a6da2b7f864750,
+ 0xa70c3c40a64e6c51,0x999090b65f67d924,
+ 0xd0cf4b50cfe20765,0xfff4b4e3f741cf6d,
+ 0x82818f1281ed449f,0xbff8f10e7a8921a4,
+ 0xa321f2d7226895c7,0xaff72d52192b6a0d,
+ 0xcbea6f8ceb02bb39,0x9bf4f8a69f764490,
+ 0xfee50b7025c36a08,0x2f236d04753d5b4,
+ 0x9f4f2726179a2245,0x1d762422c946590,
+ 0xc722f0ef9d80aad6,0x424d3ad2b7b97ef5,
+ 0xf8ebad2b84e0d58b,0xd2e0898765a7deb2,
+ 0x9b934c3b330c8577,0x63cc55f49f88eb2f,
+ 0xc2781f49ffcfa6d5,0x3cbf6b71c76b25fb,
+ 0xf316271c7fc3908a,0x8bef464e3945ef7a,
+ 0x97edd871cfda3a56,0x97758bf0e3cbb5ac,
+ 0xbde94e8e43d0c8ec,0x3d52eeed1cbea317,
+ 0xed63a231d4c4fb27,0x4ca7aaa863ee4bdd,
+ 0x945e455f24fb1cf8,0x8fe8caa93e74ef6a,
+ 0xb975d6b6ee39e436,0xb3e2fd538e122b44,
+ 0xe7d34c64a9c85d44,0x60dbbca87196b616,
+ 0x90e40fbeea1d3a4a,0xbc8955e946fe31cd,
+ 0xb51d13aea4a488dd,0x6babab6398bdbe41,
+ 0xe264589a4dcdab14,0xc696963c7eed2dd1,
+ 0x8d7eb76070a08aec,0xfc1e1de5cf543ca2,
+ 0xb0de65388cc8ada8,0x3b25a55f43294bcb,
+ 0xdd15fe86affad912,0x49ef0eb713f39ebe,
+ 0x8a2dbf142dfcc7ab,0x6e3569326c784337,
+ 0xacb92ed9397bf996,0x49c2c37f07965404,
+ 0xd7e77a8f87daf7fb,0xdc33745ec97be906,
+ 0x86f0ac99b4e8dafd,0x69a028bb3ded71a3,
+ 0xa8acd7c0222311bc,0xc40832ea0d68ce0c,
+ 0xd2d80db02aabd62b,0xf50a3fa490c30190,
+ 0x83c7088e1aab65db,0x792667c6da79e0fa,
+ 0xa4b8cab1a1563f52,0x577001b891185938,
+ 0xcde6fd5e09abcf26,0xed4c0226b55e6f86,
+ 0x80b05e5ac60b6178,0x544f8158315b05b4,
+ 0xa0dc75f1778e39d6,0x696361ae3db1c721,
+ 0xc913936dd571c84c,0x3bc3a19cd1e38e9,
+ 0xfb5878494ace3a5f,0x4ab48a04065c723,
+ 0x9d174b2dcec0e47b,0x62eb0d64283f9c76,
+ 0xc45d1df942711d9a,0x3ba5d0bd324f8394,
+ 0xf5746577930d6500,0xca8f44ec7ee36479,
+ 0x9968bf6abbe85f20,0x7e998b13cf4e1ecb,
+ 0xbfc2ef456ae276e8,0x9e3fedd8c321a67e,
+ 0xefb3ab16c59b14a2,0xc5cfe94ef3ea101e,
+ 0x95d04aee3b80ece5,0xbba1f1d158724a12,
+ 0xbb445da9ca61281f,0x2a8a6e45ae8edc97,
+ 0xea1575143cf97226,0xf52d09d71a3293bd,
+ 0x924d692ca61be758,0x593c2626705f9c56,
+ 0xb6e0c377cfa2e12e,0x6f8b2fb00c77836c,
+ 0xe498f455c38b997a,0xb6dfb9c0f956447,
+ 0x8edf98b59a373fec,0x4724bd4189bd5eac,
+ 0xb2977ee300c50fe7,0x58edec91ec2cb657,
+ 0xdf3d5e9bc0f653e1,0x2f2967b66737e3ed,
+ 0x8b865b215899f46c,0xbd79e0d20082ee74,
+ 0xae67f1e9aec07187,0xecd8590680a3aa11,
+ 0xda01ee641a708de9,0xe80e6f4820cc9495,
+ 0x884134fe908658b2,0x3109058d147fdcdd,
+ 0xaa51823e34a7eede,0xbd4b46f0599fd415,
+ 0xd4e5e2cdc1d1ea96,0x6c9e18ac7007c91a,
+ 0x850fadc09923329e,0x3e2cf6bc604ddb0,
+ 0xa6539930bf6bff45,0x84db8346b786151c,
+ 0xcfe87f7cef46ff16,0xe612641865679a63,
+ 0x81f14fae158c5f6e,0x4fcb7e8f3f60c07e,
+ 0xa26da3999aef7749,0xe3be5e330f38f09d,
+ 0xcb090c8001ab551c,0x5cadf5bfd3072cc5,
+ 0xfdcb4fa002162a63,0x73d9732fc7c8f7f6,
+ 0x9e9f11c4014dda7e,0x2867e7fddcdd9afa,
+ 0xc646d63501a1511d,0xb281e1fd541501b8,
+ 0xf7d88bc24209a565,0x1f225a7ca91a4226,
+ 0x9ae757596946075f,0x3375788de9b06958,
+ 0xc1a12d2fc3978937,0x52d6b1641c83ae,
+ 0xf209787bb47d6b84,0xc0678c5dbd23a49a,
+ 0x9745eb4d50ce6332,0xf840b7ba963646e0,
+ 0xbd176620a501fbff,0xb650e5a93bc3d898,
+ 0xec5d3fa8ce427aff,0xa3e51f138ab4cebe,
+ 0x93ba47c980e98cdf,0xc66f336c36b10137,
+ 0xb8a8d9bbe123f017,0xb80b0047445d4184,
+ 0xe6d3102ad96cec1d,0xa60dc059157491e5,
+ 0x9043ea1ac7e41392,0x87c89837ad68db2f,
+ 0xb454e4a179dd1877,0x29babe4598c311fb,
+ 0xe16a1dc9d8545e94,0xf4296dd6fef3d67a,
+ 0x8ce2529e2734bb1d,0x1899e4a65f58660c,
+ 0xb01ae745b101e9e4,0x5ec05dcff72e7f8f,
+ 0xdc21a1171d42645d,0x76707543f4fa1f73,
+ 0x899504ae72497eba,0x6a06494a791c53a8,
+ 0xabfa45da0edbde69,0x487db9d17636892,
+ 0xd6f8d7509292d603,0x45a9d2845d3c42b6,
+ 0x865b86925b9bc5c2,0xb8a2392ba45a9b2,
+ 0xa7f26836f282b732,0x8e6cac7768d7141e,
+ 0xd1ef0244af2364ff,0x3207d795430cd926,
+ 0x8335616aed761f1f,0x7f44e6bd49e807b8,
+ 0xa402b9c5a8d3a6e7,0x5f16206c9c6209a6,
+ 0xcd036837130890a1,0x36dba887c37a8c0f,
+ 0x802221226be55a64,0xc2494954da2c9789,
+ 0xa02aa96b06deb0fd,0xf2db9baa10b7bd6c,
+ 0xc83553c5c8965d3d,0x6f92829494e5acc7,
+ 0xfa42a8b73abbf48c,0xcb772339ba1f17f9,
+ 0x9c69a97284b578d7,0xff2a760414536efb,
+ 0xc38413cf25e2d70d,0xfef5138519684aba,
+ 0xf46518c2ef5b8cd1,0x7eb258665fc25d69,
+ 0x98bf2f79d5993802,0xef2f773ffbd97a61,
+ 0xbeeefb584aff8603,0xaafb550ffacfd8fa,
+ 0xeeaaba2e5dbf6784,0x95ba2a53f983cf38,
+ 0x952ab45cfa97a0b2,0xdd945a747bf26183,
+ 0xba756174393d88df,0x94f971119aeef9e4,
+ 0xe912b9d1478ceb17,0x7a37cd5601aab85d,
+ 0x91abb422ccb812ee,0xac62e055c10ab33a,
+ 0xb616a12b7fe617aa,0x577b986b314d6009,
+ 0xe39c49765fdf9d94,0xed5a7e85fda0b80b,
+ 0x8e41ade9fbebc27d,0x14588f13be847307,
+ 0xb1d219647ae6b31c,0x596eb2d8ae258fc8,
+ 0xde469fbd99a05fe3,0x6fca5f8ed9aef3bb,
+ 0x8aec23d680043bee,0x25de7bb9480d5854,
+ 0xada72ccc20054ae9,0xaf561aa79a10ae6a,
+ 0xd910f7ff28069da4,0x1b2ba1518094da04,
+ 0x87aa9aff79042286,0x90fb44d2f05d0842,
+ 0xa99541bf57452b28,0x353a1607ac744a53,
+ 0xd3fa922f2d1675f2,0x42889b8997915ce8,
+ 0x847c9b5d7c2e09b7,0x69956135febada11,
+ 0xa59bc234db398c25,0x43fab9837e699095,
+ 0xcf02b2c21207ef2e,0x94f967e45e03f4bb,
+ 0x8161afb94b44f57d,0x1d1be0eebac278f5,
+ 0xa1ba1ba79e1632dc,0x6462d92a69731732,
+ 0xca28a291859bbf93,0x7d7b8f7503cfdcfe,
+ 0xfcb2cb35e702af78,0x5cda735244c3d43e,
+ 0x9defbf01b061adab,0x3a0888136afa64a7,
+ 0xc56baec21c7a1916,0x88aaa1845b8fdd0,
+ 0xf6c69a72a3989f5b,0x8aad549e57273d45,
+ 0x9a3c2087a63f6399,0x36ac54e2f678864b,
+ 0xc0cb28a98fcf3c7f,0x84576a1bb416a7dd,
+ 0xf0fdf2d3f3c30b9f,0x656d44a2a11c51d5,
+ 0x969eb7c47859e743,0x9f644ae5a4b1b325,
+ 0xbc4665b596706114,0x873d5d9f0dde1fee,
+ 0xeb57ff22fc0c7959,0xa90cb506d155a7ea,
+ 0x9316ff75dd87cbd8,0x9a7f12442d588f2,
+ 0xb7dcbf5354e9bece,0xc11ed6d538aeb2f,
+ 0xe5d3ef282a242e81,0x8f1668c8a86da5fa,
+ 0x8fa475791a569d10,0xf96e017d694487bc,
+ 0xb38d92d760ec4455,0x37c981dcc395a9ac,
+ 0xe070f78d3927556a,0x85bbe253f47b1417,
+ 0x8c469ab843b89562,0x93956d7478ccec8e,
+ 0xaf58416654a6babb,0x387ac8d1970027b2,
+ 0xdb2e51bfe9d0696a,0x6997b05fcc0319e,
+ 0x88fcf317f22241e2,0x441fece3bdf81f03,
+ 0xab3c2fddeeaad25a,0xd527e81cad7626c3,
+ 0xd60b3bd56a5586f1,0x8a71e223d8d3b074,
+ 0x85c7056562757456,0xf6872d5667844e49,
+ 0xa738c6bebb12d16c,0xb428f8ac016561db,
+ 0xd106f86e69d785c7,0xe13336d701beba52,
+ 0x82a45b450226b39c,0xecc0024661173473,
+ 0xa34d721642b06084,0x27f002d7f95d0190,
+ 0xcc20ce9bd35c78a5,0x31ec038df7b441f4,
+ 0xff290242c83396ce,0x7e67047175a15271,
+ 0x9f79a169bd203e41,0xf0062c6e984d386,
+ 0xc75809c42c684dd1,0x52c07b78a3e60868,
+ 0xf92e0c3537826145,0xa7709a56ccdf8a82,
+ 0x9bbcc7a142b17ccb,0x88a66076400bb691,
+ 0xc2abf989935ddbfe,0x6acff893d00ea435,
+ 0xf356f7ebf83552fe,0x583f6b8c4124d43,
+ 0x98165af37b2153de,0xc3727a337a8b704a,
+ 0xbe1bf1b059e9a8d6,0x744f18c0592e4c5c,
+ 0xeda2ee1c7064130c,0x1162def06f79df73,
+ 0x9485d4d1c63e8be7,0x8addcb5645ac2ba8,
+ 0xb9a74a0637ce2ee1,0x6d953e2bd7173692,
+ 0xe8111c87c5c1ba99,0xc8fa8db6ccdd0437,
+ 0x910ab1d4db9914a0,0x1d9c9892400a22a2,
+ 0xb54d5e4a127f59c8,0x2503beb6d00cab4b,
+ 0xe2a0b5dc971f303a,0x2e44ae64840fd61d,
+ 0x8da471a9de737e24,0x5ceaecfed289e5d2,
+ 0xb10d8e1456105dad,0x7425a83e872c5f47,
+ 0xdd50f1996b947518,0xd12f124e28f77719,
+ 0x8a5296ffe33cc92f,0x82bd6b70d99aaa6f,
+ 0xace73cbfdc0bfb7b,0x636cc64d1001550b,
+ 0xd8210befd30efa5a,0x3c47f7e05401aa4e,
+ 0x8714a775e3e95c78,0x65acfaec34810a71,
+ 0xa8d9d1535ce3b396,0x7f1839a741a14d0d,
+ 0xd31045a8341ca07c,0x1ede48111209a050,
+ 0x83ea2b892091e44d,0x934aed0aab460432,
+ 0xa4e4b66b68b65d60,0xf81da84d5617853f,
+ 0xce1de40642e3f4b9,0x36251260ab9d668e,
+ 0x80d2ae83e9ce78f3,0xc1d72b7c6b426019,
+ 0xa1075a24e4421730,0xb24cf65b8612f81f,
+ 0xc94930ae1d529cfc,0xdee033f26797b627,
+ 0xfb9b7cd9a4a7443c,0x169840ef017da3b1,
+ 0x9d412e0806e88aa5,0x8e1f289560ee864e,
+ 0xc491798a08a2ad4e,0xf1a6f2bab92a27e2,
+ 0xf5b5d7ec8acb58a2,0xae10af696774b1db,
+ 0x9991a6f3d6bf1765,0xacca6da1e0a8ef29,
+ 0xbff610b0cc6edd3f,0x17fd090a58d32af3,
+ 0xeff394dcff8a948e,0xddfc4b4cef07f5b0,
+ 0x95f83d0a1fb69cd9,0x4abdaf101564f98e,
+ 0xbb764c4ca7a4440f,0x9d6d1ad41abe37f1,
+ 0xea53df5fd18d5513,0x84c86189216dc5ed,
+ 0x92746b9be2f8552c,0x32fd3cf5b4e49bb4,
+ 0xb7118682dbb66a77,0x3fbc8c33221dc2a1,
+ 0xe4d5e82392a40515,0xfabaf3feaa5334a,
+ 0x8f05b1163ba6832d,0x29cb4d87f2a7400e,
+ 0xb2c71d5bca9023f8,0x743e20e9ef511012,
+ 0xdf78e4b2bd342cf6,0x914da9246b255416,
+ 0x8bab8eefb6409c1a,0x1ad089b6c2f7548e,
+ 0xae9672aba3d0c320,0xa184ac2473b529b1,
+ 0xda3c0f568cc4f3e8,0xc9e5d72d90a2741e,
+ 0x8865899617fb1871,0x7e2fa67c7a658892,
+ 0xaa7eebfb9df9de8d,0xddbb901b98feeab7,
+ 0xd51ea6fa85785631,0x552a74227f3ea565,
+ 0x8533285c936b35de,0xd53a88958f87275f,
+ 0xa67ff273b8460356,0x8a892abaf368f137,
+ 0xd01fef10a657842c,0x2d2b7569b0432d85,
+ 0x8213f56a67f6b29b,0x9c3b29620e29fc73,
+ 0xa298f2c501f45f42,0x8349f3ba91b47b8f,
+ 0xcb3f2f7642717713,0x241c70a936219a73,
+ 0xfe0efb53d30dd4d7,0xed238cd383aa0110,
+ 0x9ec95d1463e8a506,0xf4363804324a40aa,
+ 0xc67bb4597ce2ce48,0xb143c6053edcd0d5,
+ 0xf81aa16fdc1b81da,0xdd94b7868e94050a,
+ 0x9b10a4e5e9913128,0xca7cf2b4191c8326,
+ 0xc1d4ce1f63f57d72,0xfd1c2f611f63a3f0,
+ 0xf24a01a73cf2dccf,0xbc633b39673c8cec,
+ 0x976e41088617ca01,0xd5be0503e085d813,
+ 0xbd49d14aa79dbc82,0x4b2d8644d8a74e18,
+ 0xec9c459d51852ba2,0xddf8e7d60ed1219e,
+ 0x93e1ab8252f33b45,0xcabb90e5c942b503,
+ 0xb8da1662e7b00a17,0x3d6a751f3b936243,
+ 0xe7109bfba19c0c9d,0xcc512670a783ad4,
+ 0x906a617d450187e2,0x27fb2b80668b24c5,
+ 0xb484f9dc9641e9da,0xb1f9f660802dedf6,
+ 0xe1a63853bbd26451,0x5e7873f8a0396973,
+ 0x8d07e33455637eb2,0xdb0b487b6423e1e8,
+ 0xb049dc016abc5e5f,0x91ce1a9a3d2cda62,
+ 0xdc5c5301c56b75f7,0x7641a140cc7810fb,
+ 0x89b9b3e11b6329ba,0xa9e904c87fcb0a9d,
+ 0xac2820d9623bf429,0x546345fa9fbdcd44,
+ 0xd732290fbacaf133,0xa97c177947ad4095,
+ 0x867f59a9d4bed6c0,0x49ed8eabcccc485d,
+ 0xa81f301449ee8c70,0x5c68f256bfff5a74,
+ 0xd226fc195c6a2f8c,0x73832eec6fff3111,
+ 0x83585d8fd9c25db7,0xc831fd53c5ff7eab,
+ 0xa42e74f3d032f525,0xba3e7ca8b77f5e55,
+ 0xcd3a1230c43fb26f,0x28ce1bd2e55f35eb,
+ 0x80444b5e7aa7cf85,0x7980d163cf5b81b3,
+ 0xa0555e361951c366,0xd7e105bcc332621f,
+ 0xc86ab5c39fa63440,0x8dd9472bf3fefaa7,
+ 0xfa856334878fc150,0xb14f98f6f0feb951,
+ 0x9c935e00d4b9d8d2,0x6ed1bf9a569f33d3,
+ 0xc3b8358109e84f07,0xa862f80ec4700c8,
+ 0xf4a642e14c6262c8,0xcd27bb612758c0fa,
+ 0x98e7e9cccfbd7dbd,0x8038d51cb897789c,
+ 0xbf21e44003acdd2c,0xe0470a63e6bd56c3,
+ 0xeeea5d5004981478,0x1858ccfce06cac74,
+ 0x95527a5202df0ccb,0xf37801e0c43ebc8,
+ 0xbaa718e68396cffd,0xd30560258f54e6ba,
+ 0xe950df20247c83fd,0x47c6b82ef32a2069,
+ 0x91d28b7416cdd27e,0x4cdc331d57fa5441,
+ 0xb6472e511c81471d,0xe0133fe4adf8e952,
+ 0xe3d8f9e563a198e5,0x58180fddd97723a6,
+ 0x8e679c2f5e44ff8f,0x570f09eaa7ea7648,};
+using powers = powers_template<>;
+
+}
+
+#endif
+
+#ifndef FASTFLOAT_DECIMAL_TO_BINARY_H
+#define FASTFLOAT_DECIMAL_TO_BINARY_H
+
+//included above:
+//#include <cfloat>
+#include <cinttypes>
+#include <cmath>
+//included above:
+//#include <cstdint>
+#include <cstdlib>
+//included above:
+//#include <cstring>
+
+namespace fast_float {
+
+// This will compute or rather approximate w * 5**q and return a pair of 64-bit words approximating
+// the result, with the "high" part corresponding to the most significant bits and the
+// low part corresponding to the least significant bits.
+//
+template <int bit_precision>
+fastfloat_really_inline
+value128 compute_product_approximation(int64_t q, uint64_t w) {
+ const int index = 2 * int(q - powers::smallest_power_of_five);
+ // For small values of q, e.g., q in [0,27], the answer is always exact because
+ // The line value128 firstproduct = full_multiplication(w, power_of_five_128[index]);
+ // gives the exact answer.
+ value128 firstproduct = full_multiplication(w, powers::power_of_five_128[index]);
+ static_assert((bit_precision >= 0) && (bit_precision <= 64), " precision should be in (0,64]");
+ constexpr uint64_t precision_mask = (bit_precision < 64) ?
+ (uint64_t(0xFFFFFFFFFFFFFFFF) >> bit_precision)
+ : uint64_t(0xFFFFFFFFFFFFFFFF);
+ if((firstproduct.high & precision_mask) == precision_mask) { // could further guard with (lower + w < lower)
+ // regarding the second product, we only need secondproduct.high, but our expectation is that the compiler will optimize this extra work away if needed.
+ value128 secondproduct = full_multiplication(w, powers::power_of_five_128[index + 1]);
+ firstproduct.low += secondproduct.high;
+ if(secondproduct.high > firstproduct.low) {
+ firstproduct.high++;
+ }
+ }
+ return firstproduct;
+}
+
+namespace detail {
+/**
+ * For q in (0,350), we have that
+ * f = (((152170 + 65536) * q ) >> 16);
+ * is equal to
+ * floor(p) + q
+ * where
+ * p = log(5**q)/log(2) = q * log(5)/log(2)
+ *
+ * For negative values of q in (-400,0), we have that
+ * f = (((152170 + 65536) * q ) >> 16);
+ * is equal to
+ * -ceil(p) + q
+ * where
+ * p = log(5**-q)/log(2) = -q * log(5)/log(2)
+ */
+ constexpr fastfloat_really_inline int32_t power(int32_t q) noexcept {
+ return (((152170 + 65536) * q) >> 16) + 63;
+ }
+} // namespace detail
+
+// create an adjusted mantissa, biased by the invalid power2
+// for significant digits already multiplied by 10 ** q.
+template <typename binary>
+fastfloat_really_inline
+adjusted_mantissa compute_error_scaled(int64_t q, uint64_t w, int lz) noexcept {
+ int hilz = int(w >> 63) ^ 1;
+ adjusted_mantissa answer;
+ answer.mantissa = w << hilz;
+ int bias = binary::mantissa_explicit_bits() - binary::minimum_exponent();
+ answer.power2 = int32_t(detail::power(int32_t(q)) + bias - hilz - lz - 62 + invalid_am_bias);
+ return answer;
+}
+
+// w * 10 ** q, without rounding the representation up.
+// the power2 in the exponent will be adjusted by invalid_am_bias.
+template <typename binary>
+fastfloat_really_inline
+adjusted_mantissa compute_error(int64_t q, uint64_t w) noexcept {
+ int lz = leading_zeroes(w);
+ w <<= lz;
+ value128 product = compute_product_approximation<binary::mantissa_explicit_bits() + 3>(q, w);
+ return compute_error_scaled<binary>(q, product.high, lz);
+}
+
+// w * 10 ** q
+// The returned value should be a valid ieee64 number that simply need to be packed.
+// However, in some very rare cases, the computation will fail. In such cases, we
+// return an adjusted_mantissa with a negative power of 2: the caller should recompute
+// in such cases.
+template <typename binary>
+fastfloat_really_inline
+adjusted_mantissa compute_float(int64_t q, uint64_t w) noexcept {
+ adjusted_mantissa answer;
+ if ((w == 0) || (q < binary::smallest_power_of_ten())) {
+ answer.power2 = 0;
+ answer.mantissa = 0;
+ // result should be zero
+ return answer;
+ }
+ if (q > binary::largest_power_of_ten()) {
+ // we want to get infinity:
+ answer.power2 = binary::infinite_power();
+ answer.mantissa = 0;
+ return answer;
+ }
+ // At this point in time q is in [powers::smallest_power_of_five, powers::largest_power_of_five].
+
+ // We want the most significant bit of i to be 1. Shift if needed.
+ int lz = leading_zeroes(w);
+ w <<= lz;
+
+ // The required precision is binary::mantissa_explicit_bits() + 3 because
+ // 1. We need the implicit bit
+ // 2. We need an extra bit for rounding purposes
+ // 3. We might lose a bit due to the "upperbit" routine (result too small, requiring a shift)
+
+ value128 product = compute_product_approximation<binary::mantissa_explicit_bits() + 3>(q, w);
+ if(product.low == 0xFFFFFFFFFFFFFFFF) { // could guard it further
+ // In some very rare cases, this could happen, in which case we might need a more accurate
+ // computation that what we can provide cheaply. This is very, very unlikely.
+ //
+ const bool inside_safe_exponent = (q >= -27) && (q <= 55); // always good because 5**q <2**128 when q>=0,
+ // and otherwise, for q<0, we have 5**-q<2**64 and the 128-bit reciprocal allows for exact computation.
+ if(!inside_safe_exponent) {
+ return compute_error_scaled<binary>(q, product.high, lz);
+ }
+ }
+ // The "compute_product_approximation" function can be slightly slower than a branchless approach:
+ // value128 product = compute_product(q, w);
+ // but in practice, we can win big with the compute_product_approximation if its additional branch
+ // is easily predicted. Which is best is data specific.
+ int upperbit = int(product.high >> 63);
+
+ answer.mantissa = product.high >> (upperbit + 64 - binary::mantissa_explicit_bits() - 3);
+
+ answer.power2 = int32_t(detail::power(int32_t(q)) + upperbit - lz - binary::minimum_exponent());
+ if (answer.power2 <= 0) { // we have a subnormal?
+ // Here have that answer.power2 <= 0 so -answer.power2 >= 0
+ if(-answer.power2 + 1 >= 64) { // if we have more than 64 bits below the minimum exponent, you have a zero for sure.
+ answer.power2 = 0;
+ answer.mantissa = 0;
+ // result should be zero
+ return answer;
+ }
+ // next line is safe because -answer.power2 + 1 < 64
+ answer.mantissa >>= -answer.power2 + 1;
+ // Thankfully, we can't have both "round-to-even" and subnormals because
+ // "round-to-even" only occurs for powers close to 0.
+ answer.mantissa += (answer.mantissa & 1); // round up
+ answer.mantissa >>= 1;
+ // There is a weird scenario where we don't have a subnormal but just.
+ // Suppose we start with 2.2250738585072013e-308, we end up
+ // with 0x3fffffffffffff x 2^-1023-53 which is technically subnormal
+ // whereas 0x40000000000000 x 2^-1023-53 is normal. Now, we need to round
+ // up 0x3fffffffffffff x 2^-1023-53 and once we do, we are no longer
+ // subnormal, but we can only know this after rounding.
+ // So we only declare a subnormal if we are smaller than the threshold.
+ answer.power2 = (answer.mantissa < (uint64_t(1) << binary::mantissa_explicit_bits())) ? 0 : 1;
+ return answer;
+ }
+
+ // usually, we round *up*, but if we fall right in between and and we have an
+ // even basis, we need to round down
+ // We are only concerned with the cases where 5**q fits in single 64-bit word.
+ if ((product.low <= 1) && (q >= binary::min_exponent_round_to_even()) && (q <= binary::max_exponent_round_to_even()) &&
+ ((answer.mantissa & 3) == 1) ) { // we may fall between two floats!
+ // To be in-between two floats we need that in doing
+ // answer.mantissa = product.high >> (upperbit + 64 - binary::mantissa_explicit_bits() - 3);
+ // ... we dropped out only zeroes. But if this happened, then we can go back!!!
+ if((answer.mantissa << (upperbit + 64 - binary::mantissa_explicit_bits() - 3)) == product.high) {
+ answer.mantissa &= ~uint64_t(1); // flip it so that we do not round up
+ }
+ }
+
+ answer.mantissa += (answer.mantissa & 1); // round up
+ answer.mantissa >>= 1;
+ if (answer.mantissa >= (uint64_t(2) << binary::mantissa_explicit_bits())) {
+ answer.mantissa = (uint64_t(1) << binary::mantissa_explicit_bits());
+ answer.power2++; // undo previous addition
+ }
+
+ answer.mantissa &= ~(uint64_t(1) << binary::mantissa_explicit_bits());
+ if (answer.power2 >= binary::infinite_power()) { // infinity
+ answer.power2 = binary::infinite_power();
+ answer.mantissa = 0;
+ }
+ return answer;
+}
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_BIGINT_H
+#define FASTFLOAT_BIGINT_H
+
+#include <algorithm>
+//included above:
+//#include <cstdint>
+//included above:
+//#include <climits>
+//included above:
+//#include <cstring>
+
+
+namespace fast_float {
+
+// the limb width: we want efficient multiplication of double the bits in
+// limb, or for 64-bit limbs, at least 64-bit multiplication where we can
+// extract the high and low parts efficiently. this is every 64-bit
+// architecture except for sparc, which emulates 128-bit multiplication.
+// we might have platforms where `CHAR_BIT` is not 8, so let's avoid
+// doing `8 * sizeof(limb)`.
+#if defined(FASTFLOAT_64BIT) && !defined(__sparc)
+#define FASTFLOAT_64BIT_LIMB
+typedef uint64_t limb;
+constexpr size_t limb_bits = 64;
+#else
+#define FASTFLOAT_32BIT_LIMB
+typedef uint32_t limb;
+constexpr size_t limb_bits = 32;
+#endif
+
+typedef span<limb> limb_span;
+
+// number of bits in a bigint. this needs to be at least the number
+// of bits required to store the largest bigint, which is
+// `log2(10**(digits + max_exp))`, or `log2(10**(767 + 342))`, or
+// ~3600 bits, so we round to 4000.
+constexpr size_t bigint_bits = 4000;
+constexpr size_t bigint_limbs = bigint_bits / limb_bits;
+
+// vector-like type that is allocated on the stack. the entire
+// buffer is pre-allocated, and only the length changes.
+template <uint16_t size>
+struct stackvec {
+ limb data[size];
+ // we never need more than 150 limbs
+ uint16_t length{0};
+
+ stackvec() = default;
+ stackvec(const stackvec &) = delete;
+ stackvec &operator=(const stackvec &) = delete;
+ stackvec(stackvec &&) = delete;
+ stackvec &operator=(stackvec &&other) = delete;
+
+ // create stack vector from existing limb span.
+ stackvec(limb_span s) {
+ FASTFLOAT_ASSERT(try_extend(s));
+ }
+
+ limb& operator[](size_t index) noexcept {
+ FASTFLOAT_DEBUG_ASSERT(index < length);
+ return data[index];
+ }
+ const limb& operator[](size_t index) const noexcept {
+ FASTFLOAT_DEBUG_ASSERT(index < length);
+ return data[index];
+ }
+ // index from the end of the container
+ const limb& rindex(size_t index) const noexcept {
+ FASTFLOAT_DEBUG_ASSERT(index < length);
+ size_t rindex = length - index - 1;
+ return data[rindex];
+ }
+
+ // set the length, without bounds checking.
+ void set_len(size_t len) noexcept {
+ length = uint16_t(len);
+ }
+ constexpr size_t len() const noexcept {
+ return length;
+ }
+ constexpr bool is_empty() const noexcept {
+ return length == 0;
+ }
+ constexpr size_t capacity() const noexcept {
+ return size;
+ }
+ // append item to vector, without bounds checking
+ void push_unchecked(limb value) noexcept {
+ data[length] = value;
+ length++;
+ }
+ // append item to vector, returning if item was added
+ bool try_push(limb value) noexcept {
+ if (len() < capacity()) {
+ push_unchecked(value);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ // add items to the vector, from a span, without bounds checking
+ void extend_unchecked(limb_span s) noexcept {
+ limb* ptr = data + length;
+ ::memcpy((void*)ptr, (const void*)s.ptr, sizeof(limb) * s.len());
+ set_len(len() + s.len());
+ }
+ // try to add items to the vector, returning if items were added
+ bool try_extend(limb_span s) noexcept {
+ if (len() + s.len() <= capacity()) {
+ extend_unchecked(s);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ // resize the vector, without bounds checking
+ // if the new size is longer than the vector, assign value to each
+ // appended item.
+ void resize_unchecked(size_t new_len, limb value) noexcept {
+ if (new_len > len()) {
+ size_t count = new_len - len();
+ limb* first = data + len();
+ limb* last = first + count;
+ ::std::fill(first, last, value);
+ set_len(new_len);
+ } else {
+ set_len(new_len);
+ }
+ }
+ // try to resize the vector, returning if the vector was resized.
+ bool try_resize(size_t new_len, limb value) noexcept {
+ if (new_len > capacity()) {
+ return false;
+ } else {
+ resize_unchecked(new_len, value);
+ return true;
+ }
+ }
+ // check if any limbs are non-zero after the given index.
+ // this needs to be done in reverse order, since the index
+ // is relative to the most significant limbs.
+ bool nonzero(size_t index) const noexcept {
+ while (index < len()) {
+ if (rindex(index) != 0) {
+ return true;
+ }
+ index++;
+ }
+ return false;
+ }
+ // normalize the big integer, so most-significant zero limbs are removed.
+ void normalize() noexcept {
+ while (len() > 0 && rindex(0) == 0) {
+ length--;
+ }
+ }
+};
+
+fastfloat_really_inline
+uint64_t empty_hi64(bool& truncated) noexcept {
+ truncated = false;
+ return 0;
+}
+
+fastfloat_really_inline
+uint64_t uint64_hi64(uint64_t r0, bool& truncated) noexcept {
+ truncated = false;
+ int shl = leading_zeroes(r0);
+ return r0 << shl;
+}
+
+fastfloat_really_inline
+uint64_t uint64_hi64(uint64_t r0, uint64_t r1, bool& truncated) noexcept {
+ int shl = leading_zeroes(r0);
+ if (shl == 0) {
+ truncated = r1 != 0;
+ return r0;
+ } else {
+ int shr = 64 - shl;
+ truncated = (r1 << shl) != 0;
+ return (r0 << shl) | (r1 >> shr);
+ }
+}
+
+fastfloat_really_inline
+uint64_t uint32_hi64(uint32_t r0, bool& truncated) noexcept {
+ return uint64_hi64(r0, truncated);
+}
+
+fastfloat_really_inline
+uint64_t uint32_hi64(uint32_t r0, uint32_t r1, bool& truncated) noexcept {
+ uint64_t x0 = r0;
+ uint64_t x1 = r1;
+ return uint64_hi64((x0 << 32) | x1, truncated);
+}
+
+fastfloat_really_inline
+uint64_t uint32_hi64(uint32_t r0, uint32_t r1, uint32_t r2, bool& truncated) noexcept {
+ uint64_t x0 = r0;
+ uint64_t x1 = r1;
+ uint64_t x2 = r2;
+ return uint64_hi64(x0, (x1 << 32) | x2, truncated);
+}
+
+// add two small integers, checking for overflow.
+// we want an efficient operation. for msvc, where
+// we don't have built-in intrinsics, this is still
+// pretty fast.
+fastfloat_really_inline
+limb scalar_add(limb x, limb y, bool& overflow) noexcept {
+ limb z;
+
+// gcc and clang
+#if defined(__has_builtin)
+ #if __has_builtin(__builtin_add_overflow)
+ overflow = __builtin_add_overflow(x, y, &z);
+ return z;
+ #endif
+#endif
+
+ // generic, this still optimizes correctly on MSVC.
+ z = x + y;
+ overflow = z < x;
+ return z;
+}
+
+// multiply two small integers, getting both the high and low bits.
+fastfloat_really_inline
+limb scalar_mul(limb x, limb y, limb& carry) noexcept {
+#ifdef FASTFLOAT_64BIT_LIMB
+ #if defined(__SIZEOF_INT128__)
+ // GCC and clang both define it as an extension.
+ __uint128_t z = __uint128_t(x) * __uint128_t(y) + __uint128_t(carry);
+ carry = limb(z >> limb_bits);
+ return limb(z);
+ #else
+ // fallback, no native 128-bit integer multiplication with carry.
+ // on msvc, this optimizes identically, somehow.
+ value128 z = full_multiplication(x, y);
+ bool overflow;
+ z.low = scalar_add(z.low, carry, overflow);
+ z.high += uint64_t(overflow); // cannot overflow
+ carry = z.high;
+ return z.low;
+ #endif
+#else
+ uint64_t z = uint64_t(x) * uint64_t(y) + uint64_t(carry);
+ carry = limb(z >> limb_bits);
+ return limb(z);
+#endif
+}
+
+// add scalar value to bigint starting from offset.
+// used in grade school multiplication
+template <uint16_t size>
+inline bool small_add_from(stackvec<size>& vec, limb y, size_t start) noexcept {
+ size_t index = start;
+ limb carry = y;
+ bool overflow;
+ while (carry != 0 && index < vec.len()) {
+ vec[index] = scalar_add(vec[index], carry, overflow);
+ carry = limb(overflow);
+ index += 1;
+ }
+ if (carry != 0) {
+ FASTFLOAT_TRY(vec.try_push(carry));
+ }
+ return true;
+}
+
+// add scalar value to bigint.
+template <uint16_t size>
+fastfloat_really_inline bool small_add(stackvec<size>& vec, limb y) noexcept {
+ return small_add_from(vec, y, 0);
+}
+
+// multiply bigint by scalar value.
+template <uint16_t size>
+inline bool small_mul(stackvec<size>& vec, limb y) noexcept {
+ limb carry = 0;
+ for (size_t index = 0; index < vec.len(); index++) {
+ vec[index] = scalar_mul(vec[index], y, carry);
+ }
+ if (carry != 0) {
+ FASTFLOAT_TRY(vec.try_push(carry));
+ }
+ return true;
+}
+
+// add bigint to bigint starting from index.
+// used in grade school multiplication
+template <uint16_t size>
+bool large_add_from(stackvec<size>& x, limb_span y, size_t start) noexcept {
+ // the effective x buffer is from `xstart..x.len()`, so exit early
+ // if we can't get that current range.
+ if (x.len() < start || y.len() > x.len() - start) {
+ FASTFLOAT_TRY(x.try_resize(y.len() + start, 0));
+ }
+
+ bool carry = false;
+ for (size_t index = 0; index < y.len(); index++) {
+ limb xi = x[index + start];
+ limb yi = y[index];
+ bool c1 = false;
+ bool c2 = false;
+ xi = scalar_add(xi, yi, c1);
+ if (carry) {
+ xi = scalar_add(xi, 1, c2);
+ }
+ x[index + start] = xi;
+ carry = c1 | c2;
+ }
+
+ // handle overflow
+ if (carry) {
+ FASTFLOAT_TRY(small_add_from(x, 1, y.len() + start));
+ }
+ return true;
+}
+
+// add bigint to bigint.
+template <uint16_t size>
+fastfloat_really_inline bool large_add_from(stackvec<size>& x, limb_span y) noexcept {
+ return large_add_from(x, y, 0);
+}
+
+// grade-school multiplication algorithm
+template <uint16_t size>
+bool long_mul(stackvec<size>& x, limb_span y) noexcept {
+ limb_span xs = limb_span(x.data, x.len());
+ stackvec<size> z(xs);
+ limb_span zs = limb_span(z.data, z.len());
+
+ if (y.len() != 0) {
+ limb y0 = y[0];
+ FASTFLOAT_TRY(small_mul(x, y0));
+ for (size_t index = 1; index < y.len(); index++) {
+ limb yi = y[index];
+ stackvec<size> zi;
+ if (yi != 0) {
+ // re-use the same buffer throughout
+ zi.set_len(0);
+ FASTFLOAT_TRY(zi.try_extend(zs));
+ FASTFLOAT_TRY(small_mul(zi, yi));
+ limb_span zis = limb_span(zi.data, zi.len());
+ FASTFLOAT_TRY(large_add_from(x, zis, index));
+ }
+ }
+ }
+
+ x.normalize();
+ return true;
+}
+
+// grade-school multiplication algorithm
+template <uint16_t size>
+bool large_mul(stackvec<size>& x, limb_span y) noexcept {
+ if (y.len() == 1) {
+ FASTFLOAT_TRY(small_mul(x, y[0]));
+ } else {
+ FASTFLOAT_TRY(long_mul(x, y));
+ }
+ return true;
+}
+
+// big integer type. implements a small subset of big integer
+// arithmetic, using simple algorithms since asymptotically
+// faster algorithms are slower for a small number of limbs.
+// all operations assume the big-integer is normalized.
+struct bigint {
+ // storage of the limbs, in little-endian order.
+ stackvec<bigint_limbs> vec;
+
+ bigint(): vec() {}
+ bigint(const bigint &) = delete;
+ bigint &operator=(const bigint &) = delete;
+ bigint(bigint &&) = delete;
+ bigint &operator=(bigint &&other) = delete;
+
+ bigint(uint64_t value): vec() {
+#ifdef FASTFLOAT_64BIT_LIMB
+ vec.push_unchecked(value);
+#else
+ vec.push_unchecked(uint32_t(value));
+ vec.push_unchecked(uint32_t(value >> 32));
+#endif
+ vec.normalize();
+ }
+
+ // get the high 64 bits from the vector, and if bits were truncated.
+ // this is to get the significant digits for the float.
+ uint64_t hi64(bool& truncated) const noexcept {
+#ifdef FASTFLOAT_64BIT_LIMB
+ if (vec.len() == 0) {
+ return empty_hi64(truncated);
+ } else if (vec.len() == 1) {
+ return uint64_hi64(vec.rindex(0), truncated);
+ } else {
+ uint64_t result = uint64_hi64(vec.rindex(0), vec.rindex(1), truncated);
+ truncated |= vec.nonzero(2);
+ return result;
+ }
+#else
+ if (vec.len() == 0) {
+ return empty_hi64(truncated);
+ } else if (vec.len() == 1) {
+ return uint32_hi64(vec.rindex(0), truncated);
+ } else if (vec.len() == 2) {
+ return uint32_hi64(vec.rindex(0), vec.rindex(1), truncated);
+ } else {
+ uint64_t result = uint32_hi64(vec.rindex(0), vec.rindex(1), vec.rindex(2), truncated);
+ truncated |= vec.nonzero(3);
+ return result;
+ }
+#endif
+ }
+
+ // compare two big integers, returning the large value.
+ // assumes both are normalized. if the return value is
+ // negative, other is larger, if the return value is
+ // positive, this is larger, otherwise they are equal.
+ // the limbs are stored in little-endian order, so we
+ // must compare the limbs in ever order.
+ int compare(const bigint& other) const noexcept {
+ if (vec.len() > other.vec.len()) {
+ return 1;
+ } else if (vec.len() < other.vec.len()) {
+ return -1;
+ } else {
+ for (size_t index = vec.len(); index > 0; index--) {
+ limb xi = vec[index - 1];
+ limb yi = other.vec[index - 1];
+ if (xi > yi) {
+ return 1;
+ } else if (xi < yi) {
+ return -1;
+ }
+ }
+ return 0;
+ }
+ }
+
+ // shift left each limb n bits, carrying over to the new limb
+ // returns true if we were able to shift all the digits.
+ bool shl_bits(size_t n) noexcept {
+ // Internally, for each item, we shift left by n, and add the previous
+ // right shifted limb-bits.
+ // For example, we transform (for u8) shifted left 2, to:
+ // b10100100 b01000010
+ // b10 b10010001 b00001000
+ FASTFLOAT_DEBUG_ASSERT(n != 0);
+ FASTFLOAT_DEBUG_ASSERT(n < sizeof(limb) * 8);
+
+ size_t shl = n;
+ size_t shr = limb_bits - shl;
+ limb prev = 0;
+ for (size_t index = 0; index < vec.len(); index++) {
+ limb xi = vec[index];
+ vec[index] = (xi << shl) | (prev >> shr);
+ prev = xi;
+ }
+
+ limb carry = prev >> shr;
+ if (carry != 0) {
+ return vec.try_push(carry);
+ }
+ return true;
+ }
+
+ // move the limbs left by `n` limbs.
+ bool shl_limbs(size_t n) noexcept {
+ FASTFLOAT_DEBUG_ASSERT(n != 0);
+ if (n + vec.len() > vec.capacity()) {
+ return false;
+ } else if (!vec.is_empty()) {
+ // move limbs
+ limb* dst = vec.data + n;
+ const limb* src = vec.data;
+ ::memmove(dst, src, sizeof(limb) * vec.len());
+ // fill in empty limbs
+ limb* first = vec.data;
+ limb* last = first + n;
+ ::std::fill(first, last, 0);
+ vec.set_len(n + vec.len());
+ return true;
+ } else {
+ return true;
+ }
+ }
+
+ // move the limbs left by `n` bits.
+ bool shl(size_t n) noexcept {
+ size_t rem = n % limb_bits;
+ size_t div = n / limb_bits;
+ if (rem != 0) {
+ FASTFLOAT_TRY(shl_bits(rem));
+ }
+ if (div != 0) {
+ FASTFLOAT_TRY(shl_limbs(div));
+ }
+ return true;
+ }
+
+ // get the number of leading zeros in the bigint.
+ int ctlz() const noexcept {
+ if (vec.is_empty()) {
+ return 0;
+ } else {
+#ifdef FASTFLOAT_64BIT_LIMB
+ return leading_zeroes(vec.rindex(0));
+#else
+ // no use defining a specialized leading_zeroes for a 32-bit type.
+ uint64_t r0 = vec.rindex(0);
+ return leading_zeroes(r0 << 32);
+#endif
+ }
+ }
+
+ // get the number of bits in the bigint.
+ int bit_length() const noexcept {
+ int lz = ctlz();
+ return int(limb_bits * vec.len()) - lz;
+ }
+
+ bool mul(limb y) noexcept {
+ return small_mul(vec, y);
+ }
+
+ bool add(limb y) noexcept {
+ return small_add(vec, y);
+ }
+
+ // multiply as if by 2 raised to a power.
+ bool pow2(uint32_t exp) noexcept {
+ return shl(exp);
+ }
+
+ // multiply as if by 5 raised to a power.
+ bool pow5(uint32_t exp) noexcept {
+ // multiply by a power of 5
+ static constexpr uint32_t large_step = 135;
+ static constexpr uint64_t small_power_of_5[] = {
+ 1UL, 5UL, 25UL, 125UL, 625UL, 3125UL, 15625UL, 78125UL, 390625UL,
+ 1953125UL, 9765625UL, 48828125UL, 244140625UL, 1220703125UL,
+ 6103515625UL, 30517578125UL, 152587890625UL, 762939453125UL,
+ 3814697265625UL, 19073486328125UL, 95367431640625UL, 476837158203125UL,
+ 2384185791015625UL, 11920928955078125UL, 59604644775390625UL,
+ 298023223876953125UL, 1490116119384765625UL, 7450580596923828125UL,
+ };
+#ifdef FASTFLOAT_64BIT_LIMB
+ constexpr static limb large_power_of_5[] = {
+ 1414648277510068013UL, 9180637584431281687UL, 4539964771860779200UL,
+ 10482974169319127550UL, 198276706040285095UL};
+#else
+ constexpr static limb large_power_of_5[] = {
+ 4279965485U, 329373468U, 4020270615U, 2137533757U, 4287402176U,
+ 1057042919U, 1071430142U, 2440757623U, 381945767U, 46164893U};
+#endif
+ size_t large_length = sizeof(large_power_of_5) / sizeof(limb);
+ limb_span large = limb_span(large_power_of_5, large_length);
+ while (exp >= large_step) {
+ FASTFLOAT_TRY(large_mul(vec, large));
+ exp -= large_step;
+ }
+#ifdef FASTFLOAT_64BIT_LIMB
+ uint32_t small_step = 27;
+ limb max_native = 7450580596923828125UL;
+#else
+ uint32_t small_step = 13;
+ limb max_native = 1220703125U;
+#endif
+ while (exp >= small_step) {
+ FASTFLOAT_TRY(small_mul(vec, max_native));
+ exp -= small_step;
+ }
+ if (exp != 0) {
+ FASTFLOAT_TRY(small_mul(vec, limb(small_power_of_5[exp])));
+ }
+
+ return true;
+ }
+
+ // multiply as if by 10 raised to a power.
+ bool pow10(uint32_t exp) noexcept {
+ FASTFLOAT_TRY(pow5(exp));
+ return pow2(exp);
+ }
+};
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_ASCII_NUMBER_H
+#define FASTFLOAT_ASCII_NUMBER_H
+
+//included above:
+//#include <cctype>
+//included above:
+//#include <cstdint>
+//included above:
+//#include <cstring>
+//included above:
+//#include <iterator>
+
+
+namespace fast_float {
+
+// Next function can be micro-optimized, but compilers are entirely
+// able to optimize it well.
+fastfloat_really_inline bool is_integer(char c) noexcept { return c >= '0' && c <= '9'; }
+
+fastfloat_really_inline uint64_t byteswap(uint64_t val) {
+ return (val & 0xFF00000000000000) >> 56
+ | (val & 0x00FF000000000000) >> 40
+ | (val & 0x0000FF0000000000) >> 24
+ | (val & 0x000000FF00000000) >> 8
+ | (val & 0x00000000FF000000) << 8
+ | (val & 0x0000000000FF0000) << 24
+ | (val & 0x000000000000FF00) << 40
+ | (val & 0x00000000000000FF) << 56;
+}
+
+fastfloat_really_inline uint64_t read_u64(const char *chars) {
+ uint64_t val;
+ ::memcpy(&val, chars, sizeof(uint64_t));
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ // Need to read as-if the number was in little-endian order.
+ val = byteswap(val);
+#endif
+ return val;
+}
+
+fastfloat_really_inline void write_u64(uint8_t *chars, uint64_t val) {
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ // Need to read as-if the number was in little-endian order.
+ val = byteswap(val);
+#endif
+ ::memcpy(chars, &val, sizeof(uint64_t));
+}
+
+// credit @aqrit
+fastfloat_really_inline uint32_t parse_eight_digits_unrolled(uint64_t val) {
+ const uint64_t mask = 0x000000FF000000FF;
+ const uint64_t mul1 = 0x000F424000000064; // 100 + (1000000ULL << 32)
+ const uint64_t mul2 = 0x0000271000000001; // 1 + (10000ULL << 32)
+ val -= 0x3030303030303030;
+ val = (val * 10) + (val >> 8); // val = (val * 2561) >> 8;
+ val = (((val & mask) * mul1) + (((val >> 16) & mask) * mul2)) >> 32;
+ return uint32_t(val);
+}
+
+fastfloat_really_inline uint32_t parse_eight_digits_unrolled(const char *chars) noexcept {
+ return parse_eight_digits_unrolled(read_u64(chars));
+}
+
+// credit @aqrit
+fastfloat_really_inline bool is_made_of_eight_digits_fast(uint64_t val) noexcept {
+ return !((((val + 0x4646464646464646) | (val - 0x3030303030303030)) &
+ 0x8080808080808080));
+}
+
+fastfloat_really_inline bool is_made_of_eight_digits_fast(const char *chars) noexcept {
+ return is_made_of_eight_digits_fast(read_u64(chars));
+}
+
+typedef span<const char> byte_span;
+
+struct parsed_number_string {
+ int64_t exponent{0};
+ uint64_t mantissa{0};
+ const char *lastmatch{nullptr};
+ bool negative{false};
+ bool valid{false};
+ bool too_many_digits{false};
+ // contains the range of the significant digits
+ byte_span integer{}; // non-nullable
+ byte_span fraction{}; // nullable
+};
+
+// Assuming that you use no more than 19 digits, this will
+// parse an ASCII string.
+fastfloat_really_inline
+parsed_number_string parse_number_string(const char *p, const char *pend, parse_options options) noexcept {
+ const chars_format fmt = options.format;
+ const char decimal_point = options.decimal_point;
+
+ parsed_number_string answer;
+ answer.valid = false;
+ answer.too_many_digits = false;
+ answer.negative = (*p == '-');
+ if (*p == '-') { // C++17 20.19.3.(7.1) explicitly forbids '+' sign here
+ ++p;
+ if (p == pend) {
+ return answer;
+ }
+ if (!is_integer(*p) && (*p != decimal_point)) { // a sign must be followed by an integer or the dot
+ return answer;
+ }
+ }
+ const char *const start_digits = p;
+
+ uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad)
+
+ while ((std::distance(p, pend) >= 8) && is_made_of_eight_digits_fast(p)) {
+ i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok
+ p += 8;
+ }
+ while ((p != pend) && is_integer(*p)) {
+ // a multiplication by 10 is cheaper than an arbitrary integer
+ // multiplication
+ i = 10 * i +
+ uint64_t(*p - '0'); // might overflow, we will handle the overflow later
+ ++p;
+ }
+ const char *const end_of_integer_part = p;
+ int64_t digit_count = int64_t(end_of_integer_part - start_digits);
+ answer.integer = byte_span(start_digits, size_t(digit_count));
+ int64_t exponent = 0;
+ if ((p != pend) && (*p == decimal_point)) {
+ ++p;
+ const char* before = p;
+ // can occur at most twice without overflowing, but let it occur more, since
+ // for integers with many digits, digit parsing is the primary bottleneck.
+ while ((std::distance(p, pend) >= 8) && is_made_of_eight_digits_fast(p)) {
+ i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok
+ p += 8;
+ }
+ while ((p != pend) && is_integer(*p)) {
+ uint8_t digit = uint8_t(*p - '0');
+ ++p;
+ i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
+ }
+ exponent = before - p;
+ answer.fraction = byte_span(before, size_t(p - before));
+ digit_count -= exponent;
+ }
+ // we must have encountered at least one integer!
+ if (digit_count == 0) {
+ return answer;
+ }
+ int64_t exp_number = 0; // explicit exponential part
+ if ((fmt & chars_format::scientific) && (p != pend) && (('e' == *p) || ('E' == *p))) {
+ const char * location_of_e = p;
+ ++p;
+ bool neg_exp = false;
+ if ((p != pend) && ('-' == *p)) {
+ neg_exp = true;
+ ++p;
+ } else if ((p != pend) && ('+' == *p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1)
+ ++p;
+ }
+ if ((p == pend) || !is_integer(*p)) {
+ if(!(fmt & chars_format::fixed)) {
+ // We are in error.
+ return answer;
+ }
+ // Otherwise, we will be ignoring the 'e'.
+ p = location_of_e;
+ } else {
+ while ((p != pend) && is_integer(*p)) {
+ uint8_t digit = uint8_t(*p - '0');
+ if (exp_number < 0x10000000) {
+ exp_number = 10 * exp_number + digit;
+ }
+ ++p;
+ }
+ if(neg_exp) { exp_number = - exp_number; }
+ exponent += exp_number;
+ }
+ } else {
+ // If it scientific and not fixed, we have to bail out.
+ if((fmt & chars_format::scientific) && !(fmt & chars_format::fixed)) { return answer; }
+ }
+ answer.lastmatch = p;
+ answer.valid = true;
+
+ // If we frequently had to deal with long strings of digits,
+ // we could extend our code by using a 128-bit integer instead
+ // of a 64-bit integer. However, this is uncommon.
+ //
+ // We can deal with up to 19 digits.
+ if (digit_count > 19) { // this is uncommon
+ // It is possible that the integer had an overflow.
+ // We have to handle the case where we have 0.0000somenumber.
+ // We need to be mindful of the case where we only have zeroes...
+ // E.g., 0.000000000...000.
+ const char *start = start_digits;
+ while ((start != pend) && (*start == '0' || *start == decimal_point)) {
+ if(*start == '0') { digit_count --; }
+ start++;
+ }
+ if (digit_count > 19) {
+ answer.too_many_digits = true;
+ // Let us start again, this time, avoiding overflows.
+ // We don't need to check if is_integer, since we use the
+ // pre-tokenized spans from above.
+ i = 0;
+ p = answer.integer.ptr;
+ const char* int_end = p + answer.integer.len();
+ const uint64_t minimal_nineteen_digit_integer{1000000000000000000};
+ while((i < minimal_nineteen_digit_integer) && (p != int_end)) {
+ i = i * 10 + uint64_t(*p - '0');
+ ++p;
+ }
+ if (i >= minimal_nineteen_digit_integer) { // We have a big integers
+ exponent = end_of_integer_part - p + exp_number;
+ } else { // We have a value with a fractional component.
+ p = answer.fraction.ptr;
+ const char* frac_end = p + answer.fraction.len();
+ while((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
+ i = i * 10 + uint64_t(*p - '0');
+ ++p;
+ }
+ exponent = answer.fraction.ptr - p + exp_number;
+ }
+ // We have now corrected both exponent and i, to a truncated value
+ }
+ }
+ answer.exponent = exponent;
+ answer.mantissa = i;
+ return answer;
+}
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_DIGIT_COMPARISON_H
+#define FASTFLOAT_DIGIT_COMPARISON_H
+
+//included above:
+//#include <algorithm>
+//included above:
+//#include <cstdint>
+//included above:
+//#include <cstring>
+//included above:
+//#include <iterator>
+
+
+namespace fast_float {
+
+// 1e0 to 1e19
+constexpr static uint64_t powers_of_ten_uint64[] = {
+ 1UL, 10UL, 100UL, 1000UL, 10000UL, 100000UL, 1000000UL, 10000000UL, 100000000UL,
+ 1000000000UL, 10000000000UL, 100000000000UL, 1000000000000UL, 10000000000000UL,
+ 100000000000000UL, 1000000000000000UL, 10000000000000000UL, 100000000000000000UL,
+ 1000000000000000000UL, 10000000000000000000UL};
+
+// calculate the exponent, in scientific notation, of the number.
+// this algorithm is not even close to optimized, but it has no practical
+// effect on performance: in order to have a faster algorithm, we'd need
+// to slow down performance for faster algorithms, and this is still fast.
+fastfloat_really_inline int32_t scientific_exponent(parsed_number_string& num) noexcept {
+ uint64_t mantissa = num.mantissa;
+ int32_t exponent = int32_t(num.exponent);
+ while (mantissa >= 10000) {
+ mantissa /= 10000;
+ exponent += 4;
+ }
+ while (mantissa >= 100) {
+ mantissa /= 100;
+ exponent += 2;
+ }
+ while (mantissa >= 10) {
+ mantissa /= 10;
+ exponent += 1;
+ }
+ return exponent;
+}
+
+// this converts a native floating-point number to an extended-precision float.
+template <typename T>
+fastfloat_really_inline adjusted_mantissa to_extended(T value) noexcept {
+ using equiv_uint = typename binary_format<T>::equiv_uint;
+ constexpr equiv_uint exponent_mask = binary_format<T>::exponent_mask();
+ constexpr equiv_uint mantissa_mask = binary_format<T>::mantissa_mask();
+ constexpr equiv_uint hidden_bit_mask = binary_format<T>::hidden_bit_mask();
+
+ adjusted_mantissa am;
+ int32_t bias = binary_format<T>::mantissa_explicit_bits() - binary_format<T>::minimum_exponent();
+ equiv_uint bits;
+ ::memcpy(&bits, &value, sizeof(T));
+ if ((bits & exponent_mask) == 0) {
+ // denormal
+ am.power2 = 1 - bias;
+ am.mantissa = bits & mantissa_mask;
+ } else {
+ // normal
+ am.power2 = int32_t((bits & exponent_mask) >> binary_format<T>::mantissa_explicit_bits());
+ am.power2 -= bias;
+ am.mantissa = (bits & mantissa_mask) | hidden_bit_mask;
+ }
+
+ return am;
+}
+
+// get the extended precision value of the halfway point between b and b+u.
+// we are given a native float that represents b, so we need to adjust it
+// halfway between b and b+u.
+template <typename T>
+fastfloat_really_inline adjusted_mantissa to_extended_halfway(T value) noexcept {
+ adjusted_mantissa am = to_extended(value);
+ am.mantissa <<= 1;
+ am.mantissa += 1;
+ am.power2 -= 1;
+ return am;
+}
+
+// round an extended-precision float to the nearest machine float.
+template <typename T, typename callback>
+fastfloat_really_inline void round(adjusted_mantissa& am, callback cb) noexcept {
+ int32_t mantissa_shift = 64 - binary_format<T>::mantissa_explicit_bits() - 1;
+ if (-am.power2 >= mantissa_shift) {
+ // have a denormal float
+ int32_t shift = -am.power2 + 1;
+ cb(am, std::min<int32_t>(shift, 64));
+ // check for round-up: if rounding-nearest carried us to the hidden bit.
+ am.power2 = (am.mantissa < (uint64_t(1) << binary_format<T>::mantissa_explicit_bits())) ? 0 : 1;
+ return;
+ }
+
+ // have a normal float, use the default shift.
+ cb(am, mantissa_shift);
+
+ // check for carry
+ if (am.mantissa >= (uint64_t(2) << binary_format<T>::mantissa_explicit_bits())) {
+ am.mantissa = (uint64_t(1) << binary_format<T>::mantissa_explicit_bits());
+ am.power2++;
+ }
+
+ // check for infinite: we could have carried to an infinite power
+ am.mantissa &= ~(uint64_t(1) << binary_format<T>::mantissa_explicit_bits());
+ if (am.power2 >= binary_format<T>::infinite_power()) {
+ am.power2 = binary_format<T>::infinite_power();
+ am.mantissa = 0;
+ }
+}
+
+template <typename callback>
+fastfloat_really_inline
+void round_nearest_tie_even(adjusted_mantissa& am, int32_t shift, callback cb) noexcept {
+ uint64_t mask;
+ uint64_t halfway;
+ if (shift == 64) {
+ mask = UINT64_MAX;
+ } else {
+ mask = (uint64_t(1) << shift) - 1;
+ }
+ if (shift == 0) {
+ halfway = 0;
+ } else {
+ halfway = uint64_t(1) << (shift - 1);
+ }
+ uint64_t truncated_bits = am.mantissa & mask;
+ uint64_t is_above = truncated_bits > halfway;
+ uint64_t is_halfway = truncated_bits == halfway;
+
+ // shift digits into position
+ if (shift == 64) {
+ am.mantissa = 0;
+ } else {
+ am.mantissa >>= shift;
+ }
+ am.power2 += shift;
+
+ bool is_odd = (am.mantissa & 1) == 1;
+ am.mantissa += uint64_t(cb(is_odd, is_halfway, is_above));
+}
+
+fastfloat_really_inline void round_down(adjusted_mantissa& am, int32_t shift) noexcept {
+ if (shift == 64) {
+ am.mantissa = 0;
+ } else {
+ am.mantissa >>= shift;
+ }
+ am.power2 += shift;
+}
+
+fastfloat_really_inline void skip_zeros(const char*& first, const char* last) noexcept {
+ uint64_t val;
+ while (std::distance(first, last) >= 8) {
+ ::memcpy(&val, first, sizeof(uint64_t));
+ if (val != 0x3030303030303030) {
+ break;
+ }
+ first += 8;
+ }
+ while (first != last) {
+ if (*first != '0') {
+ break;
+ }
+ first++;
+ }
+}
+
+// determine if any non-zero digits were truncated.
+// all characters must be valid digits.
+fastfloat_really_inline bool is_truncated(const char* first, const char* last) noexcept {
+ // do 8-bit optimizations, can just compare to 8 literal 0s.
+ uint64_t val;
+ while (std::distance(first, last) >= 8) {
+ ::memcpy(&val, first, sizeof(uint64_t));
+ if (val != 0x3030303030303030) {
+ return true;
+ }
+ first += 8;
+ }
+ while (first != last) {
+ if (*first != '0') {
+ return true;
+ }
+ first++;
+ }
+ return false;
+}
+
+fastfloat_really_inline bool is_truncated(byte_span s) noexcept {
+ return is_truncated(s.ptr, s.ptr + s.len());
+}
+
+fastfloat_really_inline
+void parse_eight_digits(const char*& p, limb& value, size_t& counter, size_t& count) noexcept {
+ value = value * 100000000 + parse_eight_digits_unrolled(p);
+ p += 8;
+ counter += 8;
+ count += 8;
+}
+
+fastfloat_really_inline
+void parse_one_digit(const char*& p, limb& value, size_t& counter, size_t& count) noexcept {
+ value = value * 10 + limb(*p - '0');
+ p++;
+ counter++;
+ count++;
+}
+
+fastfloat_really_inline
+void add_native(bigint& big, limb power, limb value) noexcept {
+ big.mul(power);
+ big.add(value);
+}
+
+fastfloat_really_inline void round_up_bigint(bigint& big, size_t& count) noexcept {
+ // need to round-up the digits, but need to avoid rounding
+ // ....9999 to ...10000, which could cause a false halfway point.
+ add_native(big, 10, 1);
+ count++;
+}
+
+// parse the significant digits into a big integer
+inline void parse_mantissa(bigint& result, parsed_number_string& num, size_t max_digits, size_t& digits) noexcept {
+ // try to minimize the number of big integer and scalar multiplication.
+ // therefore, try to parse 8 digits at a time, and multiply by the largest
+ // scalar value (9 or 19 digits) for each step.
+ size_t counter = 0;
+ digits = 0;
+ limb value = 0;
+#ifdef FASTFLOAT_64BIT_LIMB
+ size_t step = 19;
+#else
+ size_t step = 9;
+#endif
+
+ // process all integer digits.
+ const char* p = num.integer.ptr;
+ const char* pend = p + num.integer.len();
+ skip_zeros(p, pend);
+ // process all digits, in increments of step per loop
+ while (p != pend) {
+ while ((std::distance(p, pend) >= 8) && (step - counter >= 8) && (max_digits - digits >= 8)) {
+ parse_eight_digits(p, value, counter, digits);
+ }
+ while (counter < step && p != pend && digits < max_digits) {
+ parse_one_digit(p, value, counter, digits);
+ }
+ if (digits == max_digits) {
+ // add the temporary value, then check if we've truncated any digits
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ bool truncated = is_truncated(p, pend);
+ if (num.fraction.ptr != nullptr) {
+ truncated |= is_truncated(num.fraction);
+ }
+ if (truncated) {
+ round_up_bigint(result, digits);
+ }
+ return;
+ } else {
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ counter = 0;
+ value = 0;
+ }
+ }
+
+ // add our fraction digits, if they're available.
+ if (num.fraction.ptr != nullptr) {
+ p = num.fraction.ptr;
+ pend = p + num.fraction.len();
+ if (digits == 0) {
+ skip_zeros(p, pend);
+ }
+ // process all digits, in increments of step per loop
+ while (p != pend) {
+ while ((std::distance(p, pend) >= 8) && (step - counter >= 8) && (max_digits - digits >= 8)) {
+ parse_eight_digits(p, value, counter, digits);
+ }
+ while (counter < step && p != pend && digits < max_digits) {
+ parse_one_digit(p, value, counter, digits);
+ }
+ if (digits == max_digits) {
+ // add the temporary value, then check if we've truncated any digits
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ bool truncated = is_truncated(p, pend);
+ if (truncated) {
+ round_up_bigint(result, digits);
+ }
+ return;
+ } else {
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ counter = 0;
+ value = 0;
+ }
+ }
+ }
+
+ if (counter != 0) {
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ }
+}
+
+template <typename T>
+inline adjusted_mantissa positive_digit_comp(bigint& bigmant, int32_t exponent) noexcept {
+ FASTFLOAT_ASSERT(bigmant.pow10(uint32_t(exponent)));
+ adjusted_mantissa answer;
+ bool truncated;
+ answer.mantissa = bigmant.hi64(truncated);
+ int bias = binary_format<T>::mantissa_explicit_bits() - binary_format<T>::minimum_exponent();
+ answer.power2 = bigmant.bit_length() - 64 + bias;
+
+ round<T>(answer, [truncated](adjusted_mantissa& a, int32_t shift) {
+ round_nearest_tie_even(a, shift, [truncated](bool is_odd, bool is_halfway, bool is_above) -> bool {
+ return is_above || (is_halfway && truncated) || (is_odd && is_halfway);
+ });
+ });
+
+ return answer;
+}
+
+// the scaling here is quite simple: we have, for the real digits `m * 10^e`,
+// and for the theoretical digits `n * 2^f`. Since `e` is always negative,
+// to scale them identically, we do `n * 2^f * 5^-f`, so we now have `m * 2^e`.
+// we then need to scale by `2^(f- e)`, and then the two significant digits
+// are of the same magnitude.
+template <typename T>
+inline adjusted_mantissa negative_digit_comp(bigint& bigmant, adjusted_mantissa am, int32_t exponent) noexcept {
+ bigint& real_digits = bigmant;
+ int32_t real_exp = exponent;
+
+ // get the value of `b`, rounded down, and get a bigint representation of b+h
+ adjusted_mantissa am_b = am;
+ // gcc7 buf: use a lambda to remove the noexcept qualifier bug with -Wnoexcept-type.
+ round<T>(am_b, [](adjusted_mantissa&a, int32_t shift) { round_down(a, shift); });
+ T b;
+ to_float(false, am_b, b);
+ adjusted_mantissa theor = to_extended_halfway(b);
+ bigint theor_digits(theor.mantissa);
+ int32_t theor_exp = theor.power2;
+
+ // scale real digits and theor digits to be same power.
+ int32_t pow2_exp = theor_exp - real_exp;
+ uint32_t pow5_exp = uint32_t(-real_exp);
+ if (pow5_exp != 0) {
+ FASTFLOAT_ASSERT(theor_digits.pow5(pow5_exp));
+ }
+ if (pow2_exp > 0) {
+ FASTFLOAT_ASSERT(theor_digits.pow2(uint32_t(pow2_exp)));
+ } else if (pow2_exp < 0) {
+ FASTFLOAT_ASSERT(real_digits.pow2(uint32_t(-pow2_exp)));
+ }
+
+ // compare digits, and use it to director rounding
+ int ord = real_digits.compare(theor_digits);
+ adjusted_mantissa answer = am;
+ round<T>(answer, [ord](adjusted_mantissa& a, int32_t shift) {
+ round_nearest_tie_even(a, shift, [ord](bool is_odd, bool _, bool __) -> bool {
+ (void)_; // not needed, since we've done our comparison
+ (void)__; // not needed, since we've done our comparison
+ if (ord > 0) {
+ return true;
+ } else if (ord < 0) {
+ return false;
+ } else {
+ return is_odd;
+ }
+ });
+ });
+
+ return answer;
+}
+
+// parse the significant digits as a big integer to unambiguously round the
+// the significant digits. here, we are trying to determine how to round
+// an extended float representation close to `b+h`, halfway between `b`
+// (the float rounded-down) and `b+u`, the next positive float. this
+// algorithm is always correct, and uses one of two approaches. when
+// the exponent is positive relative to the significant digits (such as
+// 1234), we create a big-integer representation, get the high 64-bits,
+// determine if any lower bits are truncated, and use that to direct
+// rounding. in case of a negative exponent relative to the significant
+// digits (such as 1.2345), we create a theoretical representation of
+// `b` as a big-integer type, scaled to the same binary exponent as
+// the actual digits. we then compare the big integer representations
+// of both, and use that to direct rounding.
+template <typename T>
+inline adjusted_mantissa digit_comp(parsed_number_string& num, adjusted_mantissa am) noexcept {
+ // remove the invalid exponent bias
+ am.power2 -= invalid_am_bias;
+
+ int32_t sci_exp = scientific_exponent(num);
+ size_t max_digits = binary_format<T>::max_digits();
+ size_t digits = 0;
+ bigint bigmant;
+ parse_mantissa(bigmant, num, max_digits, digits);
+ // can't underflow, since digits is at most max_digits.
+ int32_t exponent = sci_exp + 1 - int32_t(digits);
+ if (exponent >= 0) {
+ return positive_digit_comp<T>(bigmant, exponent);
+ } else {
+ return negative_digit_comp<T>(bigmant, am, exponent);
+ }
+}
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_PARSE_NUMBER_H
+#define FASTFLOAT_PARSE_NUMBER_H
+
+
+//included above:
+//#include <cmath>
+//included above:
+//#include <cstring>
+//included above:
+//#include <limits>
+//included above:
+//#include <system_error>
+
+namespace fast_float {
+
+
+namespace detail {
+/**
+ * Special case +inf, -inf, nan, infinity, -infinity.
+ * The case comparisons could be made much faster given that we know that the
+ * strings a null-free and fixed.
+ **/
+template <typename T>
+from_chars_result parse_infnan(const char *first, const char *last, T &value) noexcept {
+ from_chars_result answer;
+ answer.ptr = first;
+ answer.ec = std::errc(); // be optimistic
+ bool minusSign = false;
+ if (*first == '-') { // assume first < last, so dereference without checks; C++17 20.19.3.(7.1) explicitly forbids '+' here
+ minusSign = true;
+ ++first;
+ }
+ if (last - first >= 3) {
+ if (fastfloat_strncasecmp(first, "nan", 3)) {
+ answer.ptr = (first += 3);
+ value = minusSign ? -std::numeric_limits<T>::quiet_NaN() : std::numeric_limits<T>::quiet_NaN();
+ // Check for possible nan(n-char-seq-opt), C++17 20.19.3.7, C11 7.20.1.3.3. At least MSVC produces nan(ind) and nan(snan).
+ if(first != last && *first == '(') {
+ for(const char* ptr = first + 1; ptr != last; ++ptr) {
+ if (*ptr == ')') {
+ answer.ptr = ptr + 1; // valid nan(n-char-seq-opt)
+ break;
+ }
+ else if(!(('a' <= *ptr && *ptr <= 'z') || ('A' <= *ptr && *ptr <= 'Z') || ('0' <= *ptr && *ptr <= '9') || *ptr == '_'))
+ break; // forbidden char, not nan(n-char-seq-opt)
+ }
+ }
+ return answer;
+ }
+ if (fastfloat_strncasecmp(first, "inf", 3)) {
+ if ((last - first >= 8) && fastfloat_strncasecmp(first + 3, "inity", 5)) {
+ answer.ptr = first + 8;
+ } else {
+ answer.ptr = first + 3;
+ }
+ value = minusSign ? -std::numeric_limits<T>::infinity() : std::numeric_limits<T>::infinity();
+ return answer;
+ }
+ }
+ answer.ec = std::errc::invalid_argument;
+ return answer;
+}
+
+} // namespace detail
+
+template<typename T>
+from_chars_result from_chars(const char *first, const char *last,
+ T &value, chars_format fmt /*= chars_format::general*/) noexcept {
+ return from_chars_advanced(first, last, value, parse_options{fmt});
+}
+
+template<typename T>
+from_chars_result from_chars_advanced(const char *first, const char *last,
+ T &value, parse_options options) noexcept {
+
+ static_assert (std::is_same<T, double>::value || std::is_same<T, float>::value, "only float and double are supported");
+
+
+ from_chars_result answer;
+ if (first == last) {
+ answer.ec = std::errc::invalid_argument;
+ answer.ptr = first;
+ return answer;
+ }
+ parsed_number_string pns = parse_number_string(first, last, options);
+ if (!pns.valid) {
+ return detail::parse_infnan(first, last, value);
+ }
+ answer.ec = std::errc(); // be optimistic
+ answer.ptr = pns.lastmatch;
+ // Next is Clinger's fast path.
+ if (binary_format<T>::min_exponent_fast_path() <= pns.exponent && pns.exponent <= binary_format<T>::max_exponent_fast_path() && pns.mantissa <=binary_format<T>::max_mantissa_fast_path() && !pns.too_many_digits) {
+ value = T(pns.mantissa);
+ if (pns.exponent < 0) { value = value / binary_format<T>::exact_power_of_ten(-pns.exponent); }
+ else { value = value * binary_format<T>::exact_power_of_ten(pns.exponent); }
+ if (pns.negative) { value = -value; }
+ return answer;
+ }
+ adjusted_mantissa am = compute_float<binary_format<T>>(pns.exponent, pns.mantissa);
+ if(pns.too_many_digits && am.power2 >= 0) {
+ if(am != compute_float<binary_format<T>>(pns.exponent, pns.mantissa + 1)) {
+ am = compute_error<binary_format<T>>(pns.exponent, pns.mantissa);
+ }
+ }
+ // If we called compute_float<binary_format<T>>(pns.exponent, pns.mantissa) and we have an invalid power (am.power2 < 0),
+ // then we need to go the long way around again. This is very uncommon.
+ if(am.power2 < 0) { am = digit_comp<T>(pns, am); }
+ to_float(pns.negative, am, value);
+ return answer;
+}
+
+} // namespace fast_float
+
+#endif
+
+#ifdef _MSC_VER
+# pragma warning(pop)
+#elif defined(__clang__) || defined(__APPLE_CC__)
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#endif // _C4_EXT_FAST_FLOAT_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/ext/fast_float.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/std/vector_fwd.hpp
+// https://github.com/biojppm/c4core/src/c4/std/vector_fwd.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_STD_VECTOR_FWD_HPP_
+#define _C4_STD_VECTOR_FWD_HPP_
+
+/** @file vector_fwd.hpp */
+
+//included above:
+//#include <cstddef>
+
+// forward declarations for std::vector
+#if defined(__GLIBCXX__) || defined(__GLIBCPP__) || defined(_MSC_VER)
+namespace std {
+template<typename> class allocator;
+template<typename T, typename Alloc> class vector;
+} // namespace std
+#elif defined(_LIBCPP_ABI_NAMESPACE)
+namespace std {
+inline namespace _LIBCPP_ABI_NAMESPACE {
+template<typename> class allocator;
+template<typename T, typename Alloc> class vector;
+} // namespace _LIBCPP_ABI_NAMESPACE
+} // namespace std
+#else
+#error "unknown standard library"
+#endif
+
+#ifndef C4CORE_SINGLE_HEADER
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/substr_fwd.hpp
+//#include "c4/substr_fwd.hpp"
+#if !defined(C4_SUBSTR_FWD_HPP_) && !defined(_C4_SUBSTR_FWD_HPP_)
+#error "amalgamate: file c4/substr_fwd.hpp must have been included at this point"
+#endif /* C4_SUBSTR_FWD_HPP_ */
+
+#endif
+
+namespace c4 {
+
+template<class Alloc> c4::substr to_substr(std::vector<char, Alloc> &vec);
+template<class Alloc> c4::csubstr to_csubstr(std::vector<char, Alloc> const& vec);
+
+template<class Alloc> bool operator!= (c4::csubstr ss, std::vector<char, Alloc> const& s);
+template<class Alloc> bool operator== (c4::csubstr ss, std::vector<char, Alloc> const& s);
+template<class Alloc> bool operator>= (c4::csubstr ss, std::vector<char, Alloc> const& s);
+template<class Alloc> bool operator> (c4::csubstr ss, std::vector<char, Alloc> const& s);
+template<class Alloc> bool operator<= (c4::csubstr ss, std::vector<char, Alloc> const& s);
+template<class Alloc> bool operator< (c4::csubstr ss, std::vector<char, Alloc> const& s);
+
+template<class Alloc> bool operator!= (std::vector<char, Alloc> const& s, c4::csubstr ss);
+template<class Alloc> bool operator== (std::vector<char, Alloc> const& s, c4::csubstr ss);
+template<class Alloc> bool operator>= (std::vector<char, Alloc> const& s, c4::csubstr ss);
+template<class Alloc> bool operator> (std::vector<char, Alloc> const& s, c4::csubstr ss);
+template<class Alloc> bool operator<= (std::vector<char, Alloc> const& s, c4::csubstr ss);
+template<class Alloc> bool operator< (std::vector<char, Alloc> const& s, c4::csubstr ss);
+
+template<class Alloc> size_t to_chars(c4::substr buf, std::vector<char, Alloc> const& s);
+template<class Alloc> bool from_chars(c4::csubstr buf, std::vector<char, Alloc> * s);
+
+} // namespace c4
+
+#endif // _C4_STD_VECTOR_FWD_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/std/vector_fwd.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/std/string_fwd.hpp
+// https://github.com/biojppm/c4core/src/c4/std/string_fwd.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_STD_STRING_FWD_HPP_
+#define _C4_STD_STRING_FWD_HPP_
+
+/** @file string_fwd.hpp */
+
+#ifndef DOXYGEN
+
+#ifndef C4CORE_SINGLE_HEADER
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/substr_fwd.hpp
+//#include "c4/substr_fwd.hpp"
+#if !defined(C4_SUBSTR_FWD_HPP_) && !defined(_C4_SUBSTR_FWD_HPP_)
+#error "amalgamate: file c4/substr_fwd.hpp must have been included at this point"
+#endif /* C4_SUBSTR_FWD_HPP_ */
+
+#endif
+
+//included above:
+//#include <cstddef>
+
+// forward declarations for std::string
+#if defined(__GLIBCXX__) || defined(__GLIBCPP__)
+#include <bits/stringfwd.h> // use the fwd header in glibcxx
+#elif defined(_LIBCPP_VERSION) || defined(__APPLE_CC__)
+#include <iosfwd> // use the fwd header in stdlibc++
+#elif defined(_MSC_VER)
+//! @todo is there a fwd header in msvc?
+namespace std {
+template<typename> struct char_traits;
+template<typename> class allocator;
+template<typename _CharT, typename _Traits, typename _Alloc> class basic_string;
+using string = basic_string<char, char_traits<char>, allocator<char>>;
+} /* namespace std */
+#else
+#error "unknown standard library"
+#endif
+
+namespace c4 {
+
+c4::substr to_substr(std::string &s);
+c4::csubstr to_csubstr(std::string const& s);
+
+bool operator== (c4::csubstr ss, std::string const& s);
+bool operator!= (c4::csubstr ss, std::string const& s);
+bool operator>= (c4::csubstr ss, std::string const& s);
+bool operator> (c4::csubstr ss, std::string const& s);
+bool operator<= (c4::csubstr ss, std::string const& s);
+bool operator< (c4::csubstr ss, std::string const& s);
+
+bool operator== (std::string const& s, c4::csubstr ss);
+bool operator!= (std::string const& s, c4::csubstr ss);
+bool operator>= (std::string const& s, c4::csubstr ss);
+bool operator> (std::string const& s, c4::csubstr ss);
+bool operator<= (std::string const& s, c4::csubstr ss);
+bool operator< (std::string const& s, c4::csubstr ss);
+
+size_t to_chars(c4::substr buf, std::string const& s);
+bool from_chars(c4::csubstr buf, std::string * s);
+
+} // namespace c4
+
+#endif // DOXYGEN
+#endif // _C4_STD_STRING_FWD_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/std/string_fwd.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/std/std_fwd.hpp
+// https://github.com/biojppm/c4core/src/c4/std/std_fwd.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_STD_STD_FWD_HPP_
+#define _C4_STD_STD_FWD_HPP_
+
+/** @file std_fwd.hpp includes all c4-std interop fwd files */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/std/vector_fwd.hpp
+//#include "c4/std/vector_fwd.hpp"
+#if !defined(C4_STD_VECTOR_FWD_HPP_) && !defined(_C4_STD_VECTOR_FWD_HPP_)
+#error "amalgamate: file c4/std/vector_fwd.hpp must have been included at this point"
+#endif /* C4_STD_VECTOR_FWD_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/std/string_fwd.hpp
+//#include "c4/std/string_fwd.hpp"
+#if !defined(C4_STD_STRING_FWD_HPP_) && !defined(_C4_STD_STRING_FWD_HPP_)
+#error "amalgamate: file c4/std/string_fwd.hpp must have been included at this point"
+#endif /* C4_STD_STRING_FWD_HPP_ */
+
+//#include "c4/std/tuple_fwd.hpp"
+
+#endif // _C4_STD_STD_FWD_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/std/std_fwd.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/charconv.hpp
+// https://github.com/biojppm/c4core/src/c4/charconv.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_CHARCONV_HPP_
+#define _C4_CHARCONV_HPP_
+
+/** @file charconv.hpp Lightweight generic type-safe wrappers for
+ * converting individual values to/from strings.
+ *
+ * These are the main functions:
+ *
+ * @code{.cpp}
+ * // Convert the given value, writing into the string.
+ * // The resulting string will NOT be null-terminated.
+ * // Return the number of characters needed.
+ * // This function is safe to call when the string is too small -
+ * // no writes will occur beyond the string's last character.
+ * template<class T> size_t c4::to_chars(substr buf, T const& C4_RESTRICT val);
+ *
+ *
+ * // Convert the given value to a string using to_chars(), and
+ * // return the resulting string, up to and including the last
+ * // written character.
+ * template<class T> substr c4::to_chars_sub(substr buf, T const& C4_RESTRICT val);
+ *
+ *
+ * // Read a value from the string, which must be
+ * // trimmed to the value (ie, no leading/trailing whitespace).
+ * // return true if the conversion succeeded.
+ * template<class T> bool c4::from_chars(csubstr buf, T * C4_RESTRICT val);
+ *
+ *
+ * // Read the first valid sequence of characters from the string,
+ * // skipping leading whitespace, and convert it using from_chars().
+ * // Return the number of characters read for converting.
+ * template<class T> size_t c4::from_chars_first(csubstr buf, T * C4_RESTRICT val);
+ * @endcode
+ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/language.hpp
+//#include "c4/language.hpp"
+#if !defined(C4_LANGUAGE_HPP_) && !defined(_C4_LANGUAGE_HPP_)
+#error "amalgamate: file c4/language.hpp must have been included at this point"
+#endif /* C4_LANGUAGE_HPP_ */
+
+//included above:
+//#include <inttypes.h>
+//included above:
+//#include <type_traits>
+//included above:
+//#include <climits>
+//included above:
+//#include <limits>
+//included above:
+//#include <utility>
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//#include "c4/config.hpp"
+#if !defined(C4_CONFIG_HPP_) && !defined(_C4_CONFIG_HPP_)
+#error "amalgamate: file c4/config.hpp must have been included at this point"
+#endif /* C4_CONFIG_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/substr.hpp
+//#include "c4/substr.hpp"
+#if !defined(C4_SUBSTR_HPP_) && !defined(_C4_SUBSTR_HPP_)
+#error "amalgamate: file c4/substr.hpp must have been included at this point"
+#endif /* C4_SUBSTR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/std/std_fwd.hpp
+//#include "c4/std/std_fwd.hpp"
+#if !defined(C4_STD_STD_FWD_HPP_) && !defined(_C4_STD_STD_FWD_HPP_)
+#error "amalgamate: file c4/std/std_fwd.hpp must have been included at this point"
+#endif /* C4_STD_STD_FWD_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/memory_util.hpp
+//#include "c4/memory_util.hpp"
+#if !defined(C4_MEMORY_UTIL_HPP_) && !defined(_C4_MEMORY_UTIL_HPP_)
+#error "amalgamate: file c4/memory_util.hpp must have been included at this point"
+#endif /* C4_MEMORY_UTIL_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/szconv.hpp
+//#include "c4/szconv.hpp"
+#if !defined(C4_SZCONV_HPP_) && !defined(_C4_SZCONV_HPP_)
+#error "amalgamate: file c4/szconv.hpp must have been included at this point"
+#endif /* C4_SZCONV_HPP_ */
+
+
+#ifndef C4CORE_NO_FAST_FLOAT
+ C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wsign-conversion")
+ C4_SUPPRESS_WARNING_GCC("-Warray-bounds")
+#if __GNUC__ >= 5
+ C4_SUPPRESS_WARNING_GCC("-Wshift-count-overflow")
+#endif
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/ext/fast_float.hpp
+//# include "c4/ext/fast_float.hpp"
+#if !defined(C4_EXT_FAST_FLOAT_HPP_) && !defined(_C4_EXT_FAST_FLOAT_HPP_)
+#error "amalgamate: file c4/ext/fast_float.hpp must have been included at this point"
+#endif /* C4_EXT_FAST_FLOAT_HPP_ */
+
+ C4_SUPPRESS_WARNING_GCC_POP
+# define C4CORE_HAVE_FAST_FLOAT 1
+# define C4CORE_HAVE_STD_FROMCHARS 0
+# if (C4_CPP >= 17)
+# if defined(_MSC_VER)
+# if (C4_MSVC_VERSION >= C4_MSVC_VERSION_2019)
+# include <charconv>
+# define C4CORE_HAVE_STD_TOCHARS 1
+# else
+# define C4CORE_HAVE_STD_TOCHARS 0
+# endif
+# else // VS2017 and lower do not have these macros
+# if __has_include(<charconv>) && __cpp_lib_to_chars
+# define C4CORE_HAVE_STD_TOCHARS 1
+//included above:
+//# include <charconv>
+# else
+# define C4CORE_HAVE_STD_TOCHARS 0
+# endif
+# endif
+# else
+# define C4CORE_HAVE_STD_TOCHARS 0
+# endif
+#elif (C4_CPP >= 17)
+# if defined(_MSC_VER)
+# if (C4_MSVC_VERSION >= C4_MSVC_VERSION_2019)
+//included above:
+//# include <charconv>
+# define C4CORE_HAVE_STD_TOCHARS 1
+# define C4CORE_HAVE_STD_FROMCHARS 1
+# else
+# define C4CORE_HAVE_STD_TOCHARS 0
+# define C4CORE_HAVE_STD_FROMCHARS 0
+# endif
+# else // VS2017 and lower do not have these macros
+# if __has_include(<charconv>) && __cpp_lib_to_chars
+# define C4CORE_HAVE_STD_TOCHARS 1
+# define C4CORE_HAVE_STD_FROMCHARS 1
+//included above:
+//# include <charconv>
+# else
+# define C4CORE_HAVE_STD_TOCHARS 0
+# define C4CORE_HAVE_STD_FROMCHARS 0
+# endif
+# endif
+#else
+# define C4CORE_HAVE_STD_TOCHARS 0
+# define C4CORE_HAVE_STD_FROMCHARS 0
+#endif
+
+
+#if !C4CORE_HAVE_STD_FROMCHARS && !defined(C4CORE_HAVE_FAST_FLOAT)
+#include <cstdio>
+#endif
+
+
+#ifdef _MSC_VER
+# pragma warning(push)
+# if C4_MSVC_VERSION != C4_MSVC_VERSION_2017
+# pragma warning(disable: 4800) //'int': forcing value to bool 'true' or 'false' (performance warning)
+# endif
+# pragma warning(disable: 4996) // snprintf/scanf: this function or variable may be unsafe
+#elif defined(__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wtautological-constant-out-of-range-compare"
+# pragma clang diagnostic ignored "-Wformat-nonliteral"
+# pragma clang diagnostic ignored "-Wdouble-promotion" // implicit conversion increases floating-point precision
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wformat-nonliteral"
+# pragma GCC diagnostic ignored "-Wdouble-promotion" // implicit conversion increases floating-point precision
+# pragma GCC diagnostic ignored "-Wuseless-cast"
+#endif
+
+
+namespace c4 {
+
+typedef enum : uint8_t {
+ /** print the real number in floating point format (like %f) */
+ FTOA_FLOAT = 0,
+ /** print the real number in scientific format (like %e) */
+ FTOA_SCIENT = 1,
+ /** print the real number in flexible format (like %g) */
+ FTOA_FLEX = 2,
+ /** print the real number in hexadecimal format (like %a) */
+ FTOA_HEXA = 3,
+ _FTOA_COUNT
+} RealFormat_e;
+
+
+inline C4_CONSTEXPR14 char to_c_fmt(RealFormat_e f)
+{
+ constexpr const char fmt[] = {
+ 'f', // FTOA_FLOAT
+ 'e', // FTOA_SCIENT
+ 'g', // FTOA_FLEX
+ 'a', // FTOA_HEXA
+ };
+ C4_STATIC_ASSERT(C4_COUNTOF(fmt) == _FTOA_COUNT);
+ #if C4_CPP > 14
+ C4_ASSERT(f < _FTOA_COUNT);
+ #endif
+ return fmt[f];
+}
+
+
+#if C4CORE_HAVE_STD_TOCHARS
+inline C4_CONSTEXPR14 std::chars_format to_std_fmt(RealFormat_e f)
+{
+ constexpr const std::chars_format fmt[] = {
+ std::chars_format::fixed, // FTOA_FLOAT
+ std::chars_format::scientific, // FTOA_SCIENT
+ std::chars_format::general, // FTOA_FLEX
+ std::chars_format::hex, // FTOA_HEXA
+ };
+ C4_STATIC_ASSERT(C4_COUNTOF(fmt) == _FTOA_COUNT);
+ #if C4_CPP >= 14
+ C4_ASSERT(f < _FTOA_COUNT);
+ #endif
+ return fmt[f];
+}
+#endif // C4CORE_HAVE_STD_TOCHARS
+
+/** in some platforms, int,unsigned int
+ * are not any of int8_t...int64_t and
+ * long,unsigned long are not any of uint8_t...uint64_t */
+template<class T>
+struct is_fixed_length
+{
+ enum : bool {
+ /** true if T is one of the fixed length signed types */
+ value_i = (std::is_integral<T>::value
+ && (std::is_same<T, int8_t>::value
+ || std::is_same<T, int16_t>::value
+ || std::is_same<T, int32_t>::value
+ || std::is_same<T, int64_t>::value)),
+ /** true if T is one of the fixed length unsigned types */
+ value_u = (std::is_integral<T>::value
+ && (std::is_same<T, uint8_t>::value
+ || std::is_same<T, uint16_t>::value
+ || std::is_same<T, uint32_t>::value
+ || std::is_same<T, uint64_t>::value)),
+ /** true if T is one of the fixed length signed or unsigned types */
+ value = value_i || value_u
+ };
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+#ifdef _MSC_VER
+# pragma warning(push)
+#elif defined(__clang__)
+# pragma clang diagnostic push
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wconversion"
+# if __GNUC__ >= 6
+# pragma GCC diagnostic ignored "-Wnull-dereference"
+# endif
+#endif
+
+// Helper macros, undefined below
+
+#define _c4append(c) { if(C4_LIKELY(pos < buf.len)) { buf.str[pos++] = static_cast<char>(c); } else { ++pos; } }
+#define _c4appendhex(i) { if(C4_LIKELY(pos < buf.len)) { buf.str[pos++] = hexchars[i]; } else { ++pos; } }
+
+}
+#include <iostream>
+namespace c4 {
+C4_INLINE_CONSTEXPR const char hexchars[] = "0123456789abcdef";
+
+/** write an integer to a string in decimal format. This is the
+ * lowest level (and the fastest) function to do this task.
+ * @note does not accept negative numbers
+ * @return the number of characters required for the string,
+ * even if the string is not long enough for the result.
+ * No writes are done past the end of the string. */
+template<class T>
+size_t write_dec(substr buf, T v)
+{
+ C4_STATIC_ASSERT(std::is_integral<T>::value);
+ C4_ASSERT(v >= 0);
+ size_t pos = 0;
+ do {
+ _c4append('0' + (v % T(10)));
+ v /= T(10);
+ } while(v);
+ buf.reverse_range(0, pos <= buf.len ? pos : buf.len);
+ return pos;
+}
+
+
+/** write an integer to a string in hexadecimal format. This is the
+ * lowest level (and the fastest) function to do this task.
+ * @note does not accept negative numbers
+ * @return the number of characters required for the string,
+ * even if the string is not long enough for the result.
+ * No writes are done past the end of the string. */
+template<class T>
+size_t write_hex(substr buf, T v)
+{
+ C4_STATIC_ASSERT(std::is_integral<T>::value);
+ C4_ASSERT(v >= 0);
+ size_t pos = 0;
+ do {
+ _c4appendhex(v & T(15));
+ v >>= 4;
+ } while(v);
+ buf.reverse_range(0, pos <= buf.len ? pos : buf.len);
+ return pos;
+}
+
+/** write an integer to a string in octal format. This is the
+ * lowest level (and the fastest) function to do this task.
+ * @note does not accept negative numbers
+ * @note does not prefix with 0o
+ * @return the number of characters required for the string,
+ * even if the string is not long enough for the result.
+ * No writes are done past the end of the string. */
+template<class T>
+size_t write_oct(substr buf, T v)
+{
+ C4_STATIC_ASSERT(std::is_integral<T>::value);
+ C4_ASSERT(v >= 0);
+ size_t pos = 0;
+ do {
+ _c4append('0' + (v & T(7)));
+ v >>= 3;
+ } while(v);
+ buf.reverse_range(0, pos <= buf.len ? pos : buf.len);
+ return pos;
+}
+
+/** write an integer to a string in binary format. This is the
+ * lowest level (and the fastest) function to do this task.
+ * @note does not accept negative numbers
+ * @note does not prefix with 0b
+ * @return the number of characters required for the string,
+ * even if the string is not long enough for the result.
+ * No writes are done past the end of the string. */
+template<class T>
+size_t write_bin(substr buf, T v)
+{
+ C4_STATIC_ASSERT(std::is_integral<T>::value);
+ C4_ASSERT(v >= 0);
+ size_t pos = 0;
+ do {
+ _c4append('0' + (v & T(1)));
+ v >>= 1;
+ } while(v);
+ buf.reverse_range(0, pos <= buf.len ? pos : buf.len);
+ return pos;
+}
+
+
+namespace detail {
+template<class U> using NumberWriter = size_t (*)(substr, U);
+/** @todo pass the writer as a template parameter */
+template<class T, NumberWriter<T> writer>
+size_t write_num_digits(substr buf, T v, size_t num_digits)
+{
+ C4_STATIC_ASSERT(std::is_integral<T>::value);
+ size_t ret = writer(buf, v);
+ if(ret >= num_digits)
+ return ret;
+ else if(ret >= buf.len || num_digits > buf.len)
+ return num_digits;
+ C4_ASSERT(num_digits >= ret);
+ size_t delta = static_cast<size_t>(num_digits - ret);
+ memmove(buf.str + delta, buf.str, ret);
+ memset(buf.str, '0', delta);
+ return num_digits;
+}
+} // namespace detail
+
+
+/** same as c4::write_dec(), but pad with zeroes on the left
+ * such that the resulting string is @p num_digits wide.
+ * If the given number is wider than num_digits, then the number prevails. */
+template<class T>
+size_t write_dec(substr buf, T val, size_t num_digits)
+{
+ return detail::write_num_digits<T, &write_dec<T>>(buf, val, num_digits);
+}
+
+/** same as c4::write_hex(), but pad with zeroes on the left
+ * such that the resulting string is @p num_digits wide.
+ * If the given number is wider than num_digits, then the number prevails. */
+template<class T>
+size_t write_hex(substr buf, T val, size_t num_digits)
+{
+ return detail::write_num_digits<T, &write_hex<T>>(buf, val, num_digits);
+}
+
+/** same as c4::write_bin(), but pad with zeroes on the left
+ * such that the resulting string is @p num_digits wide.
+ * If the given number is wider than num_digits, then the number prevails. */
+template<class T>
+size_t write_bin(substr buf, T val, size_t num_digits)
+{
+ return detail::write_num_digits<T, &write_bin<T>>(buf, val, num_digits);
+}
+
+/** same as c4::write_oct(), but pad with zeroes on the left
+ * such that the resulting string is @p num_digits wide.
+ * If the given number is wider than num_digits, then the number prevails. */
+template<class T>
+size_t write_oct(substr buf, T val, size_t num_digits)
+{
+ return detail::write_num_digits<T, &write_oct<T>>(buf, val, num_digits);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** read a decimal integer from a string. This is the
+ * lowest level (and the fastest) function to do this task.
+ * @note does not accept negative numbers
+ * @note The string must be trimmed. Whitespace is not accepted.
+ * @return true if the conversion was successful */
+template<class I>
+C4_ALWAYS_INLINE bool read_dec(csubstr s, I *C4_RESTRICT v)
+{
+ C4_STATIC_ASSERT(std::is_integral<I>::value);
+ *v = 0;
+ for(char c : s)
+ {
+ if(C4_UNLIKELY(c < '0' || c > '9'))
+ return false;
+ *v = (*v) * I(10) + (I(c) - I('0'));
+ }
+ return true;
+}
+
+/** read an hexadecimal integer from a string. This is the
+ * lowest level (and the fastest) function to do this task.
+ * @note does not accept negative numbers
+ * @note does not accept leading 0x or 0X
+ * @note the string must be trimmed. Whitespace is not accepted.
+ * @return true if the conversion was successful */
+template<class I>
+C4_ALWAYS_INLINE bool read_hex(csubstr s, I *C4_RESTRICT v)
+{
+ C4_STATIC_ASSERT(std::is_integral<I>::value);
+ *v = 0;
+ for(char c : s)
+ {
+ I cv;
+ if(c >= '0' && c <= '9')
+ cv = I(c) - I('0');
+ else if(c >= 'a' && c <= 'f')
+ cv = I(10) + (I(c) - I('a'));
+ else if(c >= 'A' && c <= 'F')
+ cv = I(10) + (I(c) - I('A'));
+ else
+ return false;
+ *v = (*v) * I(16) + cv;
+ }
+ return true;
+}
+
+/** read a binary integer from a string. This is the
+ * lowest level (and the fastest) function to do this task.
+ * @note does not accept negative numbers
+ * @note does not accept leading 0b or 0B
+ * @note the string must be trimmed. Whitespace is not accepted.
+ * @return true if the conversion was successful */
+template<class I>
+C4_ALWAYS_INLINE bool read_bin(csubstr s, I *C4_RESTRICT v)
+{
+ C4_STATIC_ASSERT(std::is_integral<I>::value);
+ *v = 0;
+ for(char c : s)
+ {
+ *v <<= 1;
+ if(c == '1')
+ *v |= 1;
+ else if(c != '0')
+ return false;
+ }
+ return true;
+}
+
+/** read an octal integer from a string. This is the
+ * lowest level (and the fastest) function to do this task.
+ * @note does not accept negative numbers
+ * @note does not accept leading 0o or 0O
+ * @note the string must be trimmed. Whitespace is not accepted.
+ * @return true if the conversion was successful */
+template<class I>
+C4_ALWAYS_INLINE bool read_oct(csubstr s, I *C4_RESTRICT v)
+{
+ C4_STATIC_ASSERT(std::is_integral<I>::value);
+ *v = 0;
+ for(char c : s)
+ {
+ if(C4_UNLIKELY(c < '0' || c > '7'))
+ return false;
+ *v = (*v) * I(8) + (I(c) - I('0'));
+ }
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+namespace detail {
+// do not use the type as the template argument because in some
+// platforms long!=int32 and long!=int64. Just use the numbytes
+// which is more generic and spares lengthy SFINAE code.
+template<size_t numbytes> struct itoa_min;
+template<> struct itoa_min<1>
+{
+ static csubstr value_dec() { return csubstr("128"); }
+ static csubstr value_hex() { return csubstr("80"); }
+ static csubstr value_oct() { return csubstr("200"); }
+ static csubstr value_bin() { return csubstr("10000000"); }
+};
+template<> struct itoa_min<2>
+{
+ static csubstr value_dec() { return csubstr("32768"); }
+ static csubstr value_hex() { return csubstr("8000"); }
+ static csubstr value_oct() { return csubstr("100000"); }
+ static csubstr value_bin() { return csubstr("1000000000000000"); }
+};
+template<> struct itoa_min<4>
+{
+ static csubstr value_dec() { return csubstr("2147483648"); }
+ static csubstr value_hex() { return csubstr("80000000"); }
+ static csubstr value_oct() { return csubstr("20000000000"); }
+ static csubstr value_bin() { return csubstr("10000000000000000000000000000000"); }
+};
+template<> struct itoa_min<8>
+{
+ static csubstr value_dec() { return csubstr("9223372036854775808"); }
+ static csubstr value_hex() { return csubstr("8000000000000000"); }
+ static csubstr value_oct() { return csubstr("1000000000000000000000"); }
+ static csubstr value_bin() { return csubstr("1000000000000000000000000000000000000000000000000000000000000000"); }
+};
+inline size_t _itoa2buf(substr buf, size_t pos, csubstr val)
+{
+ if(C4_LIKELY(pos + val.len <= buf.len))
+ memcpy(buf.str + pos, val.str, val.len);
+ return pos + val.len;
+}
+inline size_t _itoa2bufwithdigits(substr buf, size_t pos, size_t num_digits, csubstr val)
+{
+ num_digits = num_digits > val.len ? num_digits - val.len : 0;
+ for(size_t i = 0; i < num_digits; ++i)
+ _c4append('0');
+ return _itoa2buf(buf, pos, val);
+}
+template<class T>
+size_t _itoadec2buf(substr buf)
+{
+ if(C4_LIKELY(buf.len > 0))
+ {
+ buf.str[0] = '-';
+ return detail::_itoa2buf(buf, 1, detail::itoa_min<sizeof(T)>::value_dec());
+ }
+ else
+ {
+ return detail::_itoa2buf({}, 1, detail::itoa_min<sizeof(T)>::value_dec());
+ }
+ C4_UNREACHABLE();
+}
+template<class I>
+size_t _itoa2buf(substr buf, I radix)
+{
+ size_t pos = 0;
+ _c4append('-');
+ switch(radix)
+ {
+ case I(10):
+ /*...........................*/ return _itoa2buf(buf, pos, itoa_min<sizeof(I)>::value_dec());
+ case I(16):
+ _c4append('0'); _c4append('x'); return _itoa2buf(buf, pos, itoa_min<sizeof(I)>::value_hex());
+ case I( 2):
+ _c4append('0'); _c4append('b'); return _itoa2buf(buf, pos, itoa_min<sizeof(I)>::value_bin());
+ case I( 8):
+ _c4append('0'); _c4append('o'); return _itoa2buf(buf, pos, itoa_min<sizeof(I)>::value_oct());
+ }
+ C4_ERROR("unknown radix");
+ return 0;
+}
+template<class I>
+size_t _itoa2buf(substr buf, I radix, size_t num_digits)
+{
+ size_t pos = 0;
+ _c4append('-');
+ switch(radix)
+ {
+ case I(10):
+ /*...........................*/ return _itoa2bufwithdigits(buf, pos, num_digits, itoa_min<sizeof(I)>::value_dec());
+ case I(16):
+ _c4append('0'); _c4append('x'); return _itoa2bufwithdigits(buf, pos, num_digits, itoa_min<sizeof(I)>::value_hex());
+ case I( 2):
+ _c4append('0'); _c4append('b'); return _itoa2bufwithdigits(buf, pos, num_digits, itoa_min<sizeof(I)>::value_bin());
+ case I( 8):
+ _c4append('0'); _c4append('o'); return _itoa2bufwithdigits(buf, pos, num_digits, itoa_min<sizeof(I)>::value_oct());
+ }
+ C4_ERROR("unknown radix");
+ return 0;
+}
+} // namespace detail
+
+
+/** convert an integral signed decimal to a string.
+ * The resulting string is NOT zero-terminated.
+ * Writing stops at the buffer's end.
+ * @return the number of characters needed for the result, even if the buffer size is insufficient */
+template<class T>
+size_t itoa(substr buf, T v)
+{
+ C4_STATIC_ASSERT(std::is_signed<T>::value);
+ if(v >= 0)
+ {
+ return write_dec(buf, v);
+ }
+ else
+ {
+ if(C4_LIKELY(v != std::numeric_limits<T>::min()))
+ {
+ if(C4_LIKELY(buf.len > 0))
+ {
+ buf.str[0] = '-';
+ return size_t(1) + write_dec(buf.sub(1), -v);
+ }
+ else
+ {
+ return size_t(1) + write_dec({}, -v);
+ }
+ C4_UNREACHABLE();
+ }
+ else
+ {
+ // when T is the min value (eg i8: -128), negating it
+ // will overflow. so we just use the explicit value
+ return detail::_itoadec2buf<T>(buf);
+ }
+ C4_UNREACHABLE();
+ }
+ C4_UNREACHABLE();
+}
+
+/** convert an integral signed integer to a string, using a specific
+ * radix. The radix must be 2, 8, 10 or 16.
+ *
+ * The resulting string is NOT zero-terminated.
+ * Writing stops at the buffer's end.
+ * @return the number of characters needed for the result, even if the buffer size is insufficient */
+template<class T>
+size_t itoa(substr buf, T v, T radix)
+{
+ C4_STATIC_ASSERT(std::is_signed<T>::value);
+ C4_ASSERT(radix == 2 || radix == 8 || radix == 10 || radix == 16);
+ // when T is the min value (eg i8: -128), negating it
+ // will overflow
+ if(C4_LIKELY(v != std::numeric_limits<T>::min()))
+ {
+ size_t pos = 0;
+ if(v < 0)
+ {
+ v = -v;
+ _c4append('-');
+ }
+ switch(radix)
+ {
+ case 10:
+ /*............................*/return pos + write_dec(pos < buf.len ? buf.sub(pos) : substr(), v);
+ case 16:
+ _c4append('0'); _c4append('x'); return pos + write_hex(pos < buf.len ? buf.sub(pos) : substr(), v);
+ case 2:
+ _c4append('0'); _c4append('b'); return pos + write_bin(pos < buf.len ? buf.sub(pos) : substr(), v);
+ case 8:
+ _c4append('0'); _c4append('o'); return pos + write_oct(pos < buf.len ? buf.sub(pos) : substr(), v);
+ }
+ }
+ // when T is the min value (eg i8: -128), negating it
+ // will overflow
+ return detail::_itoa2buf<T>(buf, radix);
+}
+
+
+/** same as c4::itoa(), but pad with zeroes on the left such that the
+ * resulting string is @p num_digits wide. The @p radix must be 2,
+ * 8, 10 or 16. The resulting string is NOT zero-terminated. Writing
+ * stops at the buffer's end.
+ *
+ * @return the number of characters needed for the result, even if
+ * the buffer size is insufficient */
+template<class T>
+size_t itoa(substr buf, T v, T radix, size_t num_digits)
+{
+ C4_STATIC_ASSERT(std::is_signed<T>::value);
+ C4_ASSERT(radix == 2 || radix == 8 || radix == 10 || radix == 16);
+ if(C4_LIKELY(v != std::numeric_limits<T>::min()))
+ {
+ size_t pos = 0;
+ if(v < 0)
+ {
+ v = -v;
+ _c4append('-');
+ }
+ switch(radix)
+ {
+ case 10:
+ /*............................*/return pos + write_dec(pos < buf.len ? buf.sub(pos) : substr(), v, num_digits);
+ case 16:
+ _c4append('0'); _c4append('x'); return pos + write_hex(pos < buf.len ? buf.sub(pos) : substr(), v, num_digits);
+ case 2:
+ _c4append('0'); _c4append('b'); return pos + write_bin(pos < buf.len ? buf.sub(pos) : substr(), v, num_digits);
+ case 8:
+ _c4append('0'); _c4append('o'); return pos + write_oct(pos < buf.len ? buf.sub(pos) : substr(), v, num_digits);
+ }
+ }
+ // when T is the min value (eg i8: -128), negating it
+ // will overflow
+ return detail::_itoa2buf<T>(buf, radix, num_digits);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** convert an integral unsigned decimal to a string.
+ * The resulting string is NOT zero-terminated.
+ * Writing stops at the buffer's end.
+ * @return the number of characters needed for the result, even if the buffer size is insufficient */
+template<class T>
+size_t utoa(substr buf, T v)
+{
+ C4_STATIC_ASSERT(std::is_unsigned<T>::value);
+ return write_dec(buf, v);
+}
+
+/** convert an integral unsigned integer to a string, using a specific radix. The radix must be 2, 8, 10 or 16.
+ * The resulting string is NOT zero-terminated.
+ * Writing stops at the buffer's end.
+ * @return the number of characters needed for the result, even if the buffer size is insufficient */
+template<class T>
+size_t utoa(substr buf, T v, T radix)
+{
+ C4_STATIC_ASSERT(std::is_unsigned<T>::value);
+ C4_ASSERT(radix == 10 || radix == 16 || radix == 2 || radix == 8);
+ size_t pos = 0;
+ switch(radix)
+ {
+ case 10:
+ /*............................*/return pos + write_dec(pos < buf.len ? buf.sub(pos) : substr(), v);
+ case 16:
+ _c4append('0'); _c4append('x'); return pos + write_hex(pos < buf.len ? buf.sub(pos) : substr(), v);
+ case 2:
+ _c4append('0'); _c4append('b'); return pos + write_bin(pos < buf.len ? buf.sub(pos) : substr(), v);
+ case 8:
+ _c4append('0'); _c4append('o'); return pos + write_oct(pos < buf.len ? buf.sub(pos) : substr(), v);
+ }
+ C4_UNREACHABLE();
+ return substr::npos;
+}
+
+/** same as c4::utoa(), but pad with zeroes on the left such that the
+ * resulting string is @p num_digits wide. The @p radix must be 2,
+ * 8, 10 or 16. The resulting string is NOT zero-terminated. Writing
+ * stops at the buffer's end.
+ *
+ * @return the number of characters needed for the result, even if
+ * the buffer size is insufficient */
+template<class T>
+size_t utoa(substr buf, T v, T radix, size_t num_digits)
+{
+ C4_STATIC_ASSERT(std::is_unsigned<T>::value);
+ C4_ASSERT(radix == 10 || radix == 16 || radix == 2 || radix == 8);
+ size_t pos = 0;
+ switch(radix)
+ {
+ case 10:
+ /*............................*/return pos + write_dec(pos < buf.len ? buf.sub(pos) : substr(), v, num_digits);
+ case 16:
+ _c4append('0'); _c4append('x'); return pos + write_hex(pos < buf.len ? buf.sub(pos) : substr(), v, num_digits);
+ case 2:
+ _c4append('0'); _c4append('b'); return pos + write_bin(pos < buf.len ? buf.sub(pos) : substr(), v, num_digits);
+ case 8:
+ _c4append('0'); _c4append('o'); return pos + write_oct(pos < buf.len ? buf.sub(pos) : substr(), v, num_digits);
+ }
+ C4_UNREACHABLE();
+ return substr::npos;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** Convert a trimmed string to a signed integral value. The string
+ * can be formatted as decimal, binary (prefix 0b or 0B), octal
+ * (prefix 0o or 0O) or hexadecimal (prefix 0x or 0X). Strings with
+ * leading zeroes are considered as decimal. Every character in the
+ * input string is read for the conversion; it must not contain any
+ * leading or trailing whitespace.
+ *
+ * @return true if the conversion was successful.
+ *
+ * @note overflow is not detected: the return status is true even if
+ * the conversion would return a value outside of the type's range, in
+ * which case the result will wrap around the type's range.
+ * This is similar to native behavior.
+ *
+ * @see atoi_first() if the string is not trimmed to the value to read. */
+template<class T>
+bool atoi(csubstr str, T * C4_RESTRICT v)
+{
+ C4_STATIC_ASSERT(std::is_integral<T>::value);
+ C4_STATIC_ASSERT(std::is_signed<T>::value);
+
+ if(C4_UNLIKELY(str.len == 0))
+ return false;
+
+ T sign = 1;
+ size_t start = 0;
+ if(str.str[0] == '-')
+ {
+ if(C4_UNLIKELY(str.len == 1))
+ return false;
+ ++start;
+ sign = -1;
+ }
+
+ if(str.str[start] != '0')
+ {
+ if(C4_UNLIKELY( ! read_dec(str.sub(start), v)))
+ return false;
+ }
+ else
+ {
+ if(str.len == start+1)
+ {
+ *v = 0; // because the first character is 0
+ return true;
+ }
+ else
+ {
+ char pfx = str.str[start+1];
+ if(pfx == 'x' || pfx == 'X') // hexadecimal
+ {
+ if(C4_UNLIKELY(str.len <= start + 2))
+ return false;
+ if(C4_UNLIKELY( ! read_hex(str.sub(start + 2), v)))
+ return false;
+ }
+ else if(pfx == 'b' || pfx == 'B') // binary
+ {
+ if(C4_UNLIKELY(str.len <= start + 2))
+ return false;
+ if(C4_UNLIKELY( ! read_bin(str.sub(start + 2), v)))
+ return false;
+ }
+ else if(pfx == 'o' || pfx == 'O') // octal
+ {
+ if(C4_UNLIKELY(str.len <= start + 2))
+ return false;
+ if(C4_UNLIKELY( ! read_oct(str.sub(start + 2), v)))
+ return false;
+ }
+ else
+ {
+ // we know the first character is 0
+ auto fno = str.first_not_of('0', start + 1);
+ if(fno == csubstr::npos)
+ {
+ *v = 0;
+ return true;
+ }
+ if(C4_UNLIKELY( ! read_dec(str.sub(fno), v)))
+ {
+ return false;
+ }
+ }
+ }
+ }
+ *v *= sign;
+ return true;
+}
+
+
+/** Select the next range of characters in the string that can be parsed
+ * as a signed integral value, and convert it using atoi(). Leading
+ * whitespace (space, newline, tabs) is skipped.
+ * @return the number of characters read for conversion, or csubstr::npos if the conversion failed
+ * @see atoi() if the string is already trimmed to the value to read.
+ * @see csubstr::first_int_span() */
+template<class T>
+inline size_t atoi_first(csubstr str, T * C4_RESTRICT v)
+{
+ csubstr trimmed = str.first_int_span();
+ if(trimmed.len == 0)
+ return csubstr::npos;
+ if(atoi(trimmed, v))
+ return static_cast<size_t>(trimmed.end() - str.begin());
+ return csubstr::npos;
+}
+
+
+//-----------------------------------------------------------------------------
+
+/** Convert a trimmed string to an unsigned integral value. The string can be
+ * formatted as decimal, binary (prefix 0b or 0B), octal (prefix 0o or 0O)
+ * or hexadecimal (prefix 0x or 0X). Every character in the input string is read
+ * for the conversion; it must not contain any leading or trailing whitespace.
+ *
+ * @return true if the conversion was successful.
+ *
+ * @note overflow is not detected: the return status is true even if
+ * the conversion would return a value outside of the type's range, in
+ * which case the result will wrap around the type's range.
+ *
+ * @note If the string has a minus character, the return status
+ * will be false.
+ *
+ * @see atou_first() if the string is not trimmed to the value to read. */
+template<class T>
+bool atou(csubstr str, T * C4_RESTRICT v)
+{
+ C4_STATIC_ASSERT(std::is_integral<T>::value);
+
+ if(C4_UNLIKELY(str.len == 0 || str.front() == '-'))
+ return false;
+
+ if(str.str[0] != '0')
+ {
+ if(C4_UNLIKELY( ! read_dec(str, v)))
+ return false;
+ }
+ else
+ {
+ if(str.len == 1)
+ {
+ *v = 0; // we know the first character is 0
+ return true;
+ }
+ else
+ {
+ char pfx = str.str[1];
+ if(pfx == 'x' || pfx == 'X') // hexadecimal
+ {
+ if(C4_UNLIKELY(str.len <= 2))
+ return false;
+ return read_hex(str.sub(2), v);
+ }
+ else if(pfx == 'b' || pfx == 'B') // binary
+ {
+ if(C4_UNLIKELY(str.len <= 2))
+ return false;
+ return read_bin(str.sub(2), v);
+ }
+ else if(pfx == 'o' || pfx == 'O') // octal
+ {
+ if(C4_UNLIKELY(str.len <= 2))
+ return false;
+ return read_oct(str.sub(2), v);
+ }
+ else
+ {
+ // we know the first character is 0
+ auto fno = str.first_not_of('0');
+ if(fno == csubstr::npos)
+ {
+ *v = 0;
+ return true;
+ }
+ return read_dec(str.sub(fno), v);
+ }
+ }
+ }
+ return true;
+}
+
+
+/** Select the next range of characters in the string that can be parsed
+ * as an unsigned integral value, and convert it using atou(). Leading
+ * whitespace (space, newline, tabs) is skipped.
+ * @return the number of characters read for conversion, or csubstr::npos if the conversion faileds
+ * @see atou() if the string is already trimmed to the value to read.
+ * @see csubstr::first_uint_span() */
+template<class T>
+inline size_t atou_first(csubstr str, T *v)
+{
+ csubstr trimmed = str.first_uint_span();
+ if(trimmed.len == 0)
+ return csubstr::npos;
+ if(atou(trimmed, v))
+ return static_cast<size_t>(trimmed.end() - str.begin());
+ return csubstr::npos;
+}
+
+
+#ifdef _MSC_VER
+# pragma warning(pop)
+#elif defined(__clang__)
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+namespace detail {
+
+
+/** @see http://www.exploringbinary.com/ for many good examples on float-str conversion */
+template<size_t N>
+void get_real_format_str(char (& C4_RESTRICT fmt)[N], int precision, RealFormat_e formatting, const char* length_modifier="")
+{
+ int iret;
+ if(precision == -1)
+ iret = snprintf(fmt, sizeof(fmt), "%%%s%c", length_modifier, to_c_fmt(formatting));
+ else if(precision == 0)
+ iret = snprintf(fmt, sizeof(fmt), "%%.%s%c", length_modifier, to_c_fmt(formatting));
+ else
+ iret = snprintf(fmt, sizeof(fmt), "%%.%d%s%c", precision, length_modifier, to_c_fmt(formatting));
+ C4_ASSERT(iret >= 2 && size_t(iret) < sizeof(fmt));
+ C4_UNUSED(iret);
+}
+
+
+/** @todo we're depending on snprintf()/sscanf() for converting to/from
+ * floating point numbers. Apparently, this increases the binary size
+ * by a considerable amount. There are some lightweight printf
+ * implementations:
+ *
+ * @see http://www.sparetimelabs.com/tinyprintf/tinyprintf.php (BSD)
+ * @see https://github.com/weiss/c99-snprintf
+ * @see https://github.com/nothings/stb/blob/master/stb_sprintf.h
+ * @see http://www.exploringbinary.com/
+ * @see https://blog.benoitblanchon.fr/lightweight-float-to-string/
+ * @see http://www.ryanjuckett.com/programming/printing-floating-point-numbers/
+ */
+template<class T>
+size_t print_one(substr str, const char* full_fmt, T v)
+{
+#ifdef _MSC_VER
+ /** use _snprintf() to prevent early termination of the output
+ * for writing the null character at the last position
+ * @see https://msdn.microsoft.com/en-us/library/2ts7cx93.aspx */
+ int iret = _snprintf(str.str, str.len, full_fmt, v);
+ if(iret < 0)
+ {
+ /* when buf.len is not enough, VS returns a negative value.
+ * so call it again with a negative value for getting an
+ * actual length of the string */
+ iret = snprintf(nullptr, 0, full_fmt, v);
+ C4_ASSERT(iret > 0);
+ }
+ size_t ret = (size_t) iret;
+ return ret;
+#else
+ int iret = snprintf(str.str, str.len, full_fmt, v);
+ C4_ASSERT(iret >= 0);
+ size_t ret = (size_t) iret;
+ if(ret >= str.len)
+ ++ret; /* snprintf() reserves the last character to write \0 */
+ return ret;
+#endif
+}
+
+#if !C4CORE_HAVE_STD_FROMCHARS && !defined(C4CORE_HAVE_FAST_FLOAT)
+/** scans a string using the given type format, while at the same time
+ * allowing non-null-terminated strings AND guaranteeing that the given
+ * string length is strictly respected, so that no buffer overflows
+ * might occur. */
+template<typename T>
+inline size_t scan_one(csubstr str, const char *type_fmt, T *v)
+{
+ /* snscanf() is absolutely needed here as we must be sure that
+ * str.len is strictly respected, because substr is
+ * generally not null-terminated.
+ *
+ * Alas, there is no snscanf().
+ *
+ * So we fake it by using a dynamic format with an explicit
+ * field size set to the length of the given span.
+ * This trick is taken from:
+ * https://stackoverflow.com/a/18368910/5875572 */
+
+ /* this is the actual format we'll use for scanning */
+ char fmt[16];
+
+ /* write the length into it. Eg "%12f".
+ * Also, get the number of characters read from the string.
+ * So the final format ends up as "%12f%n"*/
+ int iret = std::snprintf(fmt, sizeof(fmt), "%%" "%zu" "%s" "%%n", str.len, type_fmt);
+ /* no nasty surprises, please! */
+ C4_ASSERT(iret >= 0 && size_t(iret) < C4_COUNTOF(fmt));
+
+ /* now we scan with confidence that the span length is respected */
+ int num_chars;
+ iret = std::sscanf(str.str, fmt, v, &num_chars);
+ /* scanf returns the number of successful conversions */
+ if(iret != 1) return csubstr::npos;
+ C4_ASSERT(num_chars >= 0);
+ return (size_t)(num_chars);
+}
+#endif
+
+
+#if C4CORE_HAVE_STD_TOCHARS
+template<class T>
+size_t rtoa(substr buf, T v, int precision=-1, RealFormat_e formatting=FTOA_FLEX)
+{
+ std::to_chars_result result;
+ size_t pos = 0;
+ if(formatting == FTOA_HEXA)
+ {
+ _c4append('0');
+ _c4append('x');
+ }
+ if(precision == -1)
+ result = std::to_chars(buf.str + pos, buf.str + buf.len, v, to_std_fmt(formatting));
+ else
+ result = std::to_chars(buf.str + pos, buf.str + buf.len, v, to_std_fmt(formatting), precision);
+ if(result.ec == std::errc())
+ {
+ // all good, no errors.
+ C4_ASSERT(result.ptr >= buf.str);
+ ptrdiff_t delta = result.ptr - buf.str;
+ return static_cast<size_t>(delta);
+ }
+ C4_ASSERT(result.ec == std::errc::value_too_large);
+ // This is unfortunate.
+ //
+ // When the result can't fit in the given buffer,
+ // std::to_chars() returns the end pointer it was originally
+ // given, which is useless because here we would like to know
+ // _exactly_ how many characters the buffer must have to fit
+ // the result.
+ //
+ // So we take the pessimistic view, and assume as many digits
+ // as could ever be required:
+ size_t ret = static_cast<size_t>(std::numeric_limits<T>::max_digits10);
+ return ret > buf.len ? ret : buf.len + 1;
+}
+#endif // C4CORE_HAVE_STD_TOCHARS
+
+} // namespace detail
+
+
+#undef _c4appendhex
+#undef _c4append
+
+
+/** Convert a single-precision real number to string.
+ * The string will in general be NOT null-terminated.
+ * For FTOA_FLEX, \p precision is the number of significand digits. Otherwise
+ * \p precision is the number of decimals. */
+inline size_t ftoa(substr str, float v, int precision=-1, RealFormat_e formatting=FTOA_FLEX)
+{
+#if C4CORE_HAVE_STD_TOCHARS
+ return detail::rtoa(str, v, precision, formatting);
+#else
+ char fmt[16];
+ detail::get_real_format_str(fmt, precision, formatting, /*length_modifier*/"");
+ return detail::print_one(str, fmt, v);
+#endif
+}
+
+
+/** Convert a double-precision real number to string.
+ * The string will in general be NOT null-terminated.
+ * For FTOA_FLEX, \p precision is the number of significand digits. Otherwise
+ * \p precision is the number of decimals.
+ *
+ * @return the number of characters written.
+ */
+inline size_t dtoa(substr str, double v, int precision=-1, RealFormat_e formatting=FTOA_FLEX)
+{
+#if C4CORE_HAVE_STD_TOCHARS
+ return detail::rtoa(str, v, precision, formatting);
+#else
+ char fmt[16];
+ detail::get_real_format_str(fmt, precision, formatting, /*length_modifier*/"l");
+ return detail::print_one(str, fmt, v);
+#endif
+}
+
+
+/** Convert a string to a single precision real number.
+ * The input string must be trimmed to the value, ie
+ * no leading or trailing whitespace can be present.
+ * @return true iff the conversion succeeded
+ * @see atof_first() if the string is not trimmed
+ */
+inline bool atof(csubstr str, float * C4_RESTRICT v)
+{
+ C4_ASSERT(str.triml(" \r\t\n").len == str.len);
+#if C4CORE_HAVE_FAST_FLOAT
+ fast_float::from_chars_result result;
+ result = fast_float::from_chars(str.str, str.str + str.len, *v);
+ return result.ec == std::errc();
+#elif C4CORE_HAVE_STD_FROMCHARS
+ std::from_chars_result result;
+ result = std::from_chars(str.str, str.str + str.len, *v);
+ return result.ec == std::errc();
+#else
+ size_t ret = detail::scan_one(str, "f", v);
+ return ret != csubstr::npos;
+#endif
+}
+
+
+/** Convert a string to a double precision real number.
+ * The input string must be trimmed to the value, ie
+ * no leading or trailing whitespace can be present.
+ * @return true iff the conversion succeeded
+ * @see atod_first() if the string is not trimmed
+ */
+inline bool atod(csubstr str, double * C4_RESTRICT v)
+{
+ C4_ASSERT(str.triml(" \r\t\n").len == str.len);
+#if C4CORE_HAVE_FAST_FLOAT
+ fast_float::from_chars_result result;
+ result = fast_float::from_chars(str.str, str.str + str.len, *v);
+ return result.ec == std::errc();
+#elif C4CORE_HAVE_STD_FROMCHARS
+ std::from_chars_result result;
+ result = std::from_chars(str.str, str.str + str.len, *v);
+ return result.ec == std::errc();
+#else
+ size_t ret = detail::scan_one(str, "lf", v);
+ return ret != csubstr::npos;
+#endif
+}
+
+
+/** Convert a string to a single precision real number.
+ * Leading whitespace is skipped until valid characters are found.
+ * @return the number of characters read from the string, or npos if
+ * conversion was not successful or if the string was empty */
+inline size_t atof_first(csubstr str, float * C4_RESTRICT v)
+{
+ csubstr trimmed = str.first_real_span();
+ if(trimmed.len == 0)
+ return csubstr::npos;
+ if(atof(trimmed, v))
+ return static_cast<size_t>(trimmed.end() - str.begin());
+ return csubstr::npos;
+}
+
+
+/** Convert a string to a double precision real number.
+ * Leading whitespace is skipped until valid characters are found.
+ * @return the number of characters read from the string, or npos if
+ * conversion was not successful or if the string was empty */
+inline size_t atod_first(csubstr str, double * C4_RESTRICT v)
+{
+ csubstr trimmed = str.first_real_span();
+ if(trimmed.len == 0)
+ return csubstr::npos;
+ if(atod(trimmed, v))
+ return static_cast<size_t>(trimmed.end() - str.begin());
+ return csubstr::npos;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// generic versions
+
+C4_ALWAYS_INLINE size_t xtoa(substr s, uint8_t v) { return utoa(s, v); }
+C4_ALWAYS_INLINE size_t xtoa(substr s, uint16_t v) { return utoa(s, v); }
+C4_ALWAYS_INLINE size_t xtoa(substr s, uint32_t v) { return utoa(s, v); }
+C4_ALWAYS_INLINE size_t xtoa(substr s, uint64_t v) { return utoa(s, v); }
+C4_ALWAYS_INLINE size_t xtoa(substr s, int8_t v) { return itoa(s, v); }
+C4_ALWAYS_INLINE size_t xtoa(substr s, int16_t v) { return itoa(s, v); }
+C4_ALWAYS_INLINE size_t xtoa(substr s, int32_t v) { return itoa(s, v); }
+C4_ALWAYS_INLINE size_t xtoa(substr s, int64_t v) { return itoa(s, v); }
+C4_ALWAYS_INLINE size_t xtoa(substr s, float v) { return ftoa(s, v); }
+C4_ALWAYS_INLINE size_t xtoa(substr s, double v) { return dtoa(s, v); }
+
+C4_ALWAYS_INLINE bool atox(csubstr s, uint8_t *C4_RESTRICT v) { return atou(s, v); }
+C4_ALWAYS_INLINE bool atox(csubstr s, uint16_t *C4_RESTRICT v) { return atou(s, v); }
+C4_ALWAYS_INLINE bool atox(csubstr s, uint32_t *C4_RESTRICT v) { return atou(s, v); }
+C4_ALWAYS_INLINE bool atox(csubstr s, uint64_t *C4_RESTRICT v) { return atou(s, v); }
+C4_ALWAYS_INLINE bool atox(csubstr s, int8_t *C4_RESTRICT v) { return atoi(s, v); }
+C4_ALWAYS_INLINE bool atox(csubstr s, int16_t *C4_RESTRICT v) { return atoi(s, v); }
+C4_ALWAYS_INLINE bool atox(csubstr s, int32_t *C4_RESTRICT v) { return atoi(s, v); }
+C4_ALWAYS_INLINE bool atox(csubstr s, int64_t *C4_RESTRICT v) { return atoi(s, v); }
+C4_ALWAYS_INLINE bool atox(csubstr s, float *C4_RESTRICT v) { return atof(s, v); }
+C4_ALWAYS_INLINE bool atox(csubstr s, double *C4_RESTRICT v) { return atod(s, v); }
+
+C4_ALWAYS_INLINE size_t to_chars(substr buf, uint8_t v) { return utoa(buf, v); }
+C4_ALWAYS_INLINE size_t to_chars(substr buf, uint16_t v) { return utoa(buf, v); }
+C4_ALWAYS_INLINE size_t to_chars(substr buf, uint32_t v) { return utoa(buf, v); }
+C4_ALWAYS_INLINE size_t to_chars(substr buf, uint64_t v) { return utoa(buf, v); }
+C4_ALWAYS_INLINE size_t to_chars(substr buf, int8_t v) { return itoa(buf, v); }
+C4_ALWAYS_INLINE size_t to_chars(substr buf, int16_t v) { return itoa(buf, v); }
+C4_ALWAYS_INLINE size_t to_chars(substr buf, int32_t v) { return itoa(buf, v); }
+C4_ALWAYS_INLINE size_t to_chars(substr buf, int64_t v) { return itoa(buf, v); }
+C4_ALWAYS_INLINE size_t to_chars(substr buf, float v) { return ftoa(buf, v); }
+C4_ALWAYS_INLINE size_t to_chars(substr buf, double v) { return dtoa(buf, v); }
+
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint8_t *C4_RESTRICT v) { return atou(buf, v); }
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint16_t *C4_RESTRICT v) { return atou(buf, v); }
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint32_t *C4_RESTRICT v) { return atou(buf, v); }
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, uint64_t *C4_RESTRICT v) { return atou(buf, v); }
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, int8_t *C4_RESTRICT v) { return atoi(buf, v); }
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, int16_t *C4_RESTRICT v) { return atoi(buf, v); }
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, int32_t *C4_RESTRICT v) { return atoi(buf, v); }
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, int64_t *C4_RESTRICT v) { return atoi(buf, v); }
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, float *C4_RESTRICT v) { return atof(buf, v); }
+C4_ALWAYS_INLINE bool from_chars(csubstr buf, double *C4_RESTRICT v) { return atod(buf, v); }
+
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint8_t *C4_RESTRICT v) { return atou_first(buf, v); }
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint16_t *C4_RESTRICT v) { return atou_first(buf, v); }
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint32_t *C4_RESTRICT v) { return atou_first(buf, v); }
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, uint64_t *C4_RESTRICT v) { return atou_first(buf, v); }
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, int8_t *C4_RESTRICT v) { return atoi_first(buf, v); }
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, int16_t *C4_RESTRICT v) { return atoi_first(buf, v); }
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, int32_t *C4_RESTRICT v) { return atoi_first(buf, v); }
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, int64_t *C4_RESTRICT v) { return atoi_first(buf, v); }
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, float *C4_RESTRICT v) { return atof_first(buf, v); }
+C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, double *C4_RESTRICT v) { return atod_first(buf, v); }
+
+
+//-----------------------------------------------------------------------------
+// on some platforms, (unsigned) int and (unsigned) long
+// are not any of the fixed length types above
+
+#define _C4_IF_NOT_FIXED_LENGTH_I(T, ty) C4_ALWAYS_INLINE typename std::enable_if<std:: is_signed<T>::value && !is_fixed_length<T>::value_i, ty>
+#define _C4_IF_NOT_FIXED_LENGTH_U(T, ty) C4_ALWAYS_INLINE typename std::enable_if<std::is_unsigned<T>::value && !is_fixed_length<T>::value_u, ty>
+
+template <class T> _C4_IF_NOT_FIXED_LENGTH_I(T, size_t)::type xtoa(substr buf, T v) { return itoa(buf, v); }
+template <class T> _C4_IF_NOT_FIXED_LENGTH_U(T, size_t)::type xtoa(substr buf, T v) { return utoa(buf, v); }
+
+template <class T> _C4_IF_NOT_FIXED_LENGTH_I(T, bool )::type atox(csubstr buf, T *C4_RESTRICT v) { return atoi(buf, v); }
+template <class T> _C4_IF_NOT_FIXED_LENGTH_U(T, bool )::type atox(csubstr buf, T *C4_RESTRICT v) { return atou(buf, v); }
+
+template <class T> _C4_IF_NOT_FIXED_LENGTH_I(T, size_t)::type to_chars(substr buf, T v) { return itoa(buf, v); }
+template <class T> _C4_IF_NOT_FIXED_LENGTH_U(T, size_t)::type to_chars(substr buf, T v) { return utoa(buf, v); }
+
+template <class T> _C4_IF_NOT_FIXED_LENGTH_I(T, bool )::type from_chars(csubstr buf, T *C4_RESTRICT v) { return atoi(buf, v); }
+template <class T> _C4_IF_NOT_FIXED_LENGTH_U(T, bool )::type from_chars(csubstr buf, T *C4_RESTRICT v) { return atou(buf, v); }
+
+template <class T> _C4_IF_NOT_FIXED_LENGTH_I(T, size_t)::type from_chars_first(csubstr buf, T *C4_RESTRICT v) { return atoi_first(buf, v); }
+template <class T> _C4_IF_NOT_FIXED_LENGTH_U(T, size_t)::type from_chars_first(csubstr buf, T *C4_RESTRICT v) { return atou_first(buf, v); }
+
+#undef _C4_IF_NOT_FIXED_LENGTH_I
+#undef _C4_IF_NOT_FIXED_LENGTH_U
+
+
+//-----------------------------------------------------------------------------
+// for pointers
+
+template <class T> C4_ALWAYS_INLINE size_t xtoa(substr s, T *v) { return itoa(s, (intptr_t)v, (intptr_t)16); }
+template <class T> C4_ALWAYS_INLINE bool atox(csubstr s, T **v) { intptr_t tmp; bool ret = atox(s, &tmp); if(ret) { *v = (T*)tmp; } return ret; }
+template <class T> C4_ALWAYS_INLINE size_t to_chars(substr s, T *v) { return itoa(s, (intptr_t)v, (intptr_t)16); }
+template <class T> C4_ALWAYS_INLINE bool from_chars(csubstr buf, T **v) { intptr_t tmp; bool ret = from_chars(buf, &tmp); if(ret) { *v = (T*)tmp; } return ret; }
+template <class T> C4_ALWAYS_INLINE size_t from_chars_first(csubstr buf, T **v) { intptr_t tmp; bool ret = from_chars_first(buf, &tmp); if(ret) { *v = (T*)tmp; } return ret; }
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** call to_chars() and return a substr consisting of the
+ * written portion of the input buffer. Ie, same as to_chars(),
+ * but return a substr instead of a size_t.
+ *
+ * @see to_chars() */
+template<class T>
+inline substr to_chars_sub(substr buf, T const& C4_RESTRICT v)
+{
+ size_t sz = to_chars(buf, v);
+ return buf.left_of(sz <= buf.len ? sz : buf.len);
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// bool implementation
+
+inline size_t to_chars(substr buf, bool v)
+{
+ int val = v;
+ return to_chars(buf, val);
+}
+
+inline bool from_chars(csubstr buf, bool * C4_RESTRICT v)
+{
+ if(buf == '0')
+ {
+ *v = false; return true;
+ }
+ else if(buf == '1')
+ {
+ *v = true; return true;
+ }
+ else if(buf == "false")
+ {
+ *v = false; return true;
+ }
+ else if(buf == "true")
+ {
+ *v = true; return true;
+ }
+ else if(buf == "False")
+ {
+ *v = false; return true;
+ }
+ else if(buf == "True")
+ {
+ *v = true; return true;
+ }
+ else if(buf == "FALSE")
+ {
+ *v = false; return true;
+ }
+ else if(buf == "TRUE")
+ {
+ *v = true; return true;
+ }
+ // fallback to c-style int bools
+ int val = 0;
+ bool ret = from_chars(buf, &val);
+ if(C4_LIKELY(ret))
+ {
+ *v = (val != 0);
+ }
+ return ret;
+}
+
+inline size_t from_chars_first(csubstr buf, bool * C4_RESTRICT v)
+{
+ csubstr trimmed = buf.first_non_empty_span();
+ if(trimmed.len == 0 || !from_chars(buf, v))
+ return csubstr::npos;
+ return trimmed.len;
+}
+
+
+//-----------------------------------------------------------------------------
+// single-char implementation
+
+inline size_t to_chars(substr buf, char v)
+{
+ if(buf.len > 0)
+ buf[0] = v;
+ return 1;
+}
+
+/** extract a single character from a substring
+ * @note to extract a string instead and not just a single character, use the csubstr overload */
+inline bool from_chars(csubstr buf, char * C4_RESTRICT v)
+{
+ if(buf.len != 1)
+ return false;
+ *v = buf[0];
+ return true;
+}
+
+inline size_t from_chars_first(csubstr buf, char * C4_RESTRICT v)
+{
+ if(buf.len < 1)
+ return csubstr::npos;
+ *v = buf[0];
+ return 1;
+}
+
+
+//-----------------------------------------------------------------------------
+// csubstr implementation
+
+inline size_t to_chars(substr buf, csubstr v)
+{
+ C4_ASSERT(!buf.overlaps(v));
+ size_t len = buf.len < v.len ? buf.len : v.len;
+ memcpy(buf.str, v.str, len);
+ return v.len;
+}
+
+inline bool from_chars(csubstr buf, csubstr *C4_RESTRICT v)
+{
+ *v = buf;
+ return true;
+}
+
+inline size_t from_chars_first(substr buf, csubstr * C4_RESTRICT v)
+{
+ csubstr trimmed = buf.first_non_empty_span();
+ if(trimmed.len == 0)
+ return csubstr::npos;
+ *v = trimmed;
+ return static_cast<size_t>(trimmed.end() - buf.begin());
+}
+
+
+//-----------------------------------------------------------------------------
+// substr
+
+inline size_t to_chars(substr buf, substr v)
+{
+ C4_ASSERT(!buf.overlaps(v));
+ size_t len = buf.len < v.len ? buf.len : v.len;
+ memcpy(buf.str, v.str, len);
+ return v.len;
+}
+
+inline bool from_chars(csubstr buf, substr * C4_RESTRICT v)
+{
+ C4_ASSERT(!buf.overlaps(*v));
+ if(buf.len <= v->len)
+ {
+ memcpy(v->str, buf.str, buf.len);
+ v->len = buf.len;
+ return true;
+ }
+ memcpy(v->str, buf.str, v->len);
+ return false;
+}
+
+inline size_t from_chars_first(csubstr buf, substr * C4_RESTRICT v)
+{
+ csubstr trimmed = buf.first_non_empty_span();
+ C4_ASSERT(!trimmed.overlaps(*v));
+ if(C4_UNLIKELY(trimmed.len == 0))
+ return csubstr::npos;
+ size_t len = trimmed.len > v->len ? v->len : trimmed.len;
+ memcpy(v->str, trimmed.str, len);
+ if(C4_UNLIKELY(trimmed.len > v->len))
+ return csubstr::npos;
+ return static_cast<size_t>(trimmed.end() - buf.begin());
+}
+
+
+//-----------------------------------------------------------------------------
+
+template<size_t N>
+inline size_t to_chars(substr buf, const char (& C4_RESTRICT v)[N])
+{
+ csubstr sp(v);
+ return to_chars(buf, sp);
+}
+
+inline size_t to_chars(substr buf, const char * C4_RESTRICT v)
+{
+ return to_chars(buf, to_csubstr(v));
+}
+
+} // namespace c4
+
+#ifdef _MSC_VER
+# pragma warning(pop)
+#elif defined(__clang__)
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#endif /* _C4_CHARCONV_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/charconv.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/utf.hpp
+// https://github.com/biojppm/c4core/src/c4/utf.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef C4_UTF_HPP_
+#define C4_UTF_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/language.hpp
+//#include "c4/language.hpp"
+#if !defined(C4_LANGUAGE_HPP_) && !defined(_C4_LANGUAGE_HPP_)
+#error "amalgamate: file c4/language.hpp must have been included at this point"
+#endif /* C4_LANGUAGE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/substr_fwd.hpp
+//#include "c4/substr_fwd.hpp"
+#if !defined(C4_SUBSTR_FWD_HPP_) && !defined(_C4_SUBSTR_FWD_HPP_)
+#error "amalgamate: file c4/substr_fwd.hpp must have been included at this point"
+#endif /* C4_SUBSTR_FWD_HPP_ */
+
+//included above:
+//#include <stddef.h>
+//included above:
+//#include <stdint.h>
+
+namespace c4 {
+
+substr decode_code_point(substr out, csubstr code_point);
+size_t decode_code_point(uint8_t *C4_RESTRICT buf, size_t buflen, const uint32_t code);
+
+} // namespace c4
+
+#endif // C4_UTF_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/utf.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/format.hpp
+// https://github.com/biojppm/c4core/src/c4/format.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_FORMAT_HPP_
+#define _C4_FORMAT_HPP_
+
+/** @file format.hpp provides type-safe facilities for formatting arguments
+ * to string buffers */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/charconv.hpp
+//#include "c4/charconv.hpp"
+#if !defined(C4_CHARCONV_HPP_) && !defined(_C4_CHARCONV_HPP_)
+#error "amalgamate: file c4/charconv.hpp must have been included at this point"
+#endif /* C4_CHARCONV_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/blob.hpp
+//#include "c4/blob.hpp"
+#if !defined(C4_BLOB_HPP_) && !defined(_C4_BLOB_HPP_)
+#error "amalgamate: file c4/blob.hpp must have been included at this point"
+#endif /* C4_BLOB_HPP_ */
+
+
+
+#ifdef _MSC_VER
+# pragma warning(push)
+# if C4_MSVC_VERSION != C4_MSVC_VERSION_2017
+# pragma warning(disable: 4800) // forcing value to bool 'true' or 'false' (performance warning)
+# endif
+# pragma warning(disable: 4996) // snprintf/scanf: this function or variable may be unsafe
+#elif defined(__clang__)
+# pragma clang diagnostic push
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wuseless-cast"
+#endif
+
+namespace c4 {
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// formatting truthy types as booleans
+
+namespace fmt {
+
+/** write a variable as an alphabetic boolean, ie as either true or false
+ * @param strict_read */
+template<class T>
+struct boolalpha_
+{
+ boolalpha_(T val_, bool strict_read_=false) : val(val_ ? true : false), strict_read(strict_read_) {}
+ bool val;
+ bool strict_read;
+};
+
+template<class T>
+boolalpha_<T> boolalpha(T const& val, bool strict_read=false)
+{
+ return boolalpha_<T>(val, strict_read);
+}
+
+} // namespace fmt
+
+/** write a variable as an alphabetic boolean, ie as either true or false */
+template<class T>
+inline size_t to_chars(substr buf, fmt::boolalpha_<T> fmt)
+{
+ return to_chars(buf, fmt.val ? "true" : "false");
+}
+
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// formatting integral types
+
+namespace fmt {
+
+/** format an integral type with a custom radix */
+template<typename T>
+struct integral_
+{
+ T val;
+ T radix;
+ C4_ALWAYS_INLINE integral_(T val_, T radix_) : val(val_), radix(radix_) {}
+};
+
+/** format an integral type with a custom radix, and pad with zeroes on the left */
+template<typename T>
+struct integral_padded_
+{
+ T val;
+ T radix;
+ size_t num_digits;
+ C4_ALWAYS_INLINE integral_padded_(T val_, T radix_, size_t nd) : val(val_), radix(radix_), num_digits(nd) {}
+};
+
+/** format an integral type with a custom radix */
+template<class T>
+C4_ALWAYS_INLINE integral_<T> integral(T val, T radix=10)
+{
+ return integral_<T>(val, radix);
+}
+/** format an integral type with a custom radix */
+template<class T>
+C4_ALWAYS_INLINE integral_<intptr_t> integral(T const* val, T radix=10)
+{
+ return integral_<intptr_t>(reinterpret_cast<intptr_t>(val), static_cast<intptr_t>(radix));
+}
+/** format an integral type with a custom radix */
+template<class T>
+C4_ALWAYS_INLINE integral_<intptr_t> integral(std::nullptr_t, T radix=10)
+{
+ return integral_<intptr_t>(intptr_t(0), static_cast<intptr_t>(radix));
+}
+/** pad the argument with zeroes on the left, with decimal radix */
+template<class T>
+C4_ALWAYS_INLINE integral_padded_<T> zpad(T val, size_t num_digits)
+{
+ return integral_padded_<T>(val, T(10), num_digits);
+}
+/** pad the argument with zeroes on the left */
+template<class T>
+C4_ALWAYS_INLINE integral_padded_<T> zpad(integral_<T> val, size_t num_digits)
+{
+ return integral_padded_<T>(val.val, val.radix, num_digits);
+}
+/** pad the argument with zeroes on the left */
+C4_ALWAYS_INLINE integral_padded_<intptr_t> zpad(std::nullptr_t, size_t num_digits)
+{
+ return integral_padded_<intptr_t>(0, 16, num_digits);
+}
+/** pad the argument with zeroes on the left */
+template<class T>
+C4_ALWAYS_INLINE integral_padded_<intptr_t> zpad(T const* val, size_t num_digits)
+{
+ return integral_padded_<intptr_t>(reinterpret_cast<intptr_t>(val), 16, num_digits);
+}
+template<class T>
+C4_ALWAYS_INLINE integral_padded_<intptr_t> zpad(T * val, size_t num_digits)
+{
+ return integral_padded_<intptr_t>(reinterpret_cast<intptr_t>(val), 16, num_digits);
+}
+
+
+/** format the pointer as an hexadecimal value */
+template<class T>
+inline integral_<intptr_t> hex(T * v)
+{
+ return integral_<intptr_t>(reinterpret_cast<intptr_t>(v), intptr_t(16));
+}
+/** format the pointer as an hexadecimal value */
+template<class T>
+inline integral_<intptr_t> hex(T const* v)
+{
+ return integral_<intptr_t>(reinterpret_cast<intptr_t>(v), intptr_t(16));
+}
+/** format null as an hexadecimal value
+ * @overload hex */
+inline integral_<intptr_t> hex(std::nullptr_t)
+{
+ return integral_<intptr_t>(0, intptr_t(16));
+}
+/** format the integral_ argument as an hexadecimal value
+ * @overload hex */
+template<class T>
+inline integral_<T> hex(T v)
+{
+ return integral_<T>(v, T(16));
+}
+
+/** format the pointer as an octal value */
+template<class T>
+inline integral_<intptr_t> oct(T const* v)
+{
+ return integral_<intptr_t>(reinterpret_cast<intptr_t>(v), intptr_t(8));
+}
+/** format the pointer as an octal value */
+template<class T>
+inline integral_<intptr_t> oct(T * v)
+{
+ return integral_<intptr_t>(reinterpret_cast<intptr_t>(v), intptr_t(8));
+}
+/** format null as an octal value */
+inline integral_<intptr_t> oct(std::nullptr_t)
+{
+ return integral_<intptr_t>(intptr_t(0), intptr_t(8));
+}
+/** format the integral_ argument as an octal value */
+template<class T>
+inline integral_<T> oct(T v)
+{
+ return integral_<T>(v, T(8));
+}
+
+/** format the pointer as a binary 0-1 value
+ * @see c4::raw() if you want to use a binary memcpy instead of 0-1 formatting */
+template<class T>
+inline integral_<intptr_t> bin(T const* v)
+{
+ return integral_<intptr_t>(reinterpret_cast<intptr_t>(v), intptr_t(2));
+}
+/** format the pointer as a binary 0-1 value
+ * @see c4::raw() if you want to use a binary memcpy instead of 0-1 formatting */
+template<class T>
+inline integral_<intptr_t> bin(T * v)
+{
+ return integral_<intptr_t>(reinterpret_cast<intptr_t>(v), intptr_t(2));
+}
+/** format null as a binary 0-1 value
+ * @see c4::raw() if you want to use a binary memcpy instead of 0-1 formatting */
+inline integral_<intptr_t> bin(std::nullptr_t)
+{
+ return integral_<intptr_t>(intptr_t(0), intptr_t(2));
+}
+/** format the integral_ argument as a binary 0-1 value
+ * @see c4::raw() if you want to use a binary memcpy instead of 0-1 formatting */
+template<class T>
+inline integral_<T> bin(T v)
+{
+ return integral_<T>(v, T(2));
+}
+
+} // namespace fmt
+
+
+/** format an integral_ signed type */
+template<typename T>
+C4_ALWAYS_INLINE
+typename std::enable_if<std::is_signed<T>::value, size_t>::type
+to_chars(substr buf, fmt::integral_<T> fmt)
+{
+ return itoa(buf, fmt.val, fmt.radix);
+}
+/** format an integral_ signed type, pad with zeroes */
+template<typename T>
+C4_ALWAYS_INLINE
+typename std::enable_if<std::is_signed<T>::value, size_t>::type
+to_chars(substr buf, fmt::integral_padded_<T> fmt)
+{
+ return itoa(buf, fmt.val, fmt.radix, fmt.num_digits);
+}
+
+/** format an integral_ unsigned type */
+template<typename T>
+C4_ALWAYS_INLINE
+typename std::enable_if<std::is_unsigned<T>::value, size_t>::type
+to_chars(substr buf, fmt::integral_<T> fmt)
+{
+ return utoa(buf, fmt.val, fmt.radix);
+}
+/** format an integral_ unsigned type, pad with zeroes */
+template<typename T>
+C4_ALWAYS_INLINE
+typename std::enable_if<std::is_unsigned<T>::value, size_t>::type
+to_chars(substr buf, fmt::integral_padded_<T> fmt)
+{
+ return utoa(buf, fmt.val, fmt.radix, fmt.num_digits);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// formatting real types
+
+namespace fmt {
+
+template<class T>
+struct real_
+{
+ T val;
+ int precision;
+ RealFormat_e fmt;
+ real_(T v, int prec=-1, RealFormat_e f=FTOA_FLOAT) : val(v), precision(prec), fmt(f) {}
+};
+
+template<class T>
+real_<T> real(T val, int precision, RealFormat_e fmt=FTOA_FLOAT)
+{
+ return real_<T>(val, precision, fmt);
+}
+
+} // namespace fmt
+
+inline size_t to_chars(substr buf, fmt::real_< float> fmt) { return ftoa(buf, fmt.val, fmt.precision, fmt.fmt); }
+inline size_t to_chars(substr buf, fmt::real_<double> fmt) { return dtoa(buf, fmt.val, fmt.precision, fmt.fmt); }
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// writing raw binary data
+
+namespace fmt {
+
+/** @see blob_ */
+template<class T>
+struct raw_wrapper_ : public blob_<T>
+{
+ size_t alignment;
+
+ C4_ALWAYS_INLINE raw_wrapper_(blob_<T> data, size_t alignment_) noexcept
+ :
+ blob_<T>(data),
+ alignment(alignment_)
+ {
+ C4_ASSERT_MSG(alignment > 0 && (alignment & (alignment - 1)) == 0, "alignment must be a power of two");
+ }
+};
+
+using const_raw_wrapper = raw_wrapper_<cbyte>;
+using raw_wrapper = raw_wrapper_<byte>;
+
+/** mark a variable to be written in raw binary format, using memcpy
+ * @see blob_ */
+inline const_raw_wrapper craw(cblob data, size_t alignment=alignof(max_align_t))
+{
+ return const_raw_wrapper(data, alignment);
+}
+/** mark a variable to be written in raw binary format, using memcpy
+ * @see blob_ */
+inline const_raw_wrapper raw(cblob data, size_t alignment=alignof(max_align_t))
+{
+ return const_raw_wrapper(data, alignment);
+}
+/** mark a variable to be written in raw binary format, using memcpy
+ * @see blob_ */
+template<class T>
+inline const_raw_wrapper craw(T const& C4_RESTRICT data, size_t alignment=alignof(T))
+{
+ return const_raw_wrapper(cblob(data), alignment);
+}
+/** mark a variable to be written in raw binary format, using memcpy
+ * @see blob_ */
+template<class T>
+inline const_raw_wrapper raw(T const& C4_RESTRICT data, size_t alignment=alignof(T))
+{
+ return const_raw_wrapper(cblob(data), alignment);
+}
+
+/** mark a variable to be read in raw binary format, using memcpy */
+inline raw_wrapper raw(blob data, size_t alignment=alignof(max_align_t))
+{
+ return raw_wrapper(data, alignment);
+}
+/** mark a variable to be read in raw binary format, using memcpy */
+template<class T>
+inline raw_wrapper raw(T & C4_RESTRICT data, size_t alignment=alignof(T))
+{
+ return raw_wrapper(blob(data), alignment);
+}
+
+} // namespace fmt
+
+
+/** write a variable in raw binary format, using memcpy */
+C4CORE_EXPORT size_t to_chars(substr buf, fmt::const_raw_wrapper r);
+
+/** read a variable in raw binary format, using memcpy */
+C4CORE_EXPORT bool from_chars(csubstr buf, fmt::raw_wrapper *r);
+/** read a variable in raw binary format, using memcpy */
+inline bool from_chars(csubstr buf, fmt::raw_wrapper r)
+{
+ return from_chars(buf, &r);
+}
+
+/** read a variable in raw binary format, using memcpy */
+inline size_t from_chars_first(csubstr buf, fmt::raw_wrapper *r)
+{
+ return from_chars(buf, r);
+}
+/** read a variable in raw binary format, using memcpy */
+inline size_t from_chars_first(csubstr buf, fmt::raw_wrapper r)
+{
+ return from_chars(buf, &r);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+// formatting aligned to left/right
+
+namespace fmt {
+
+template<class T>
+struct left_
+{
+ T val;
+ size_t width;
+ char pad;
+ left_(T v, size_t w, char p) : val(v), width(w), pad(p) {}
+};
+
+template<class T>
+struct right_
+{
+ T val;
+ size_t width;
+ char pad;
+ right_(T v, size_t w, char p) : val(v), width(w), pad(p) {}
+};
+
+/** mark an argument to be aligned left */
+template<class T>
+left_<T> left(T val, size_t width, char padchar=' ')
+{
+ return left_<T>(val, width, padchar);
+}
+
+/** mark an argument to be aligned right */
+template<class T>
+right_<T> right(T val, size_t width, char padchar=' ')
+{
+ return right_<T>(val, width, padchar);
+}
+
+} // namespace fmt
+
+
+template<class T>
+size_t to_chars(substr buf, fmt::left_<T> const& C4_RESTRICT align)
+{
+ size_t ret = to_chars(buf, align.val);
+ if(ret >= buf.len || ret >= align.width)
+ return ret > align.width ? ret : align.width;
+ buf.first(align.width).sub(ret).fill(align.pad);
+ to_chars(buf, align.val);
+ return align.width;
+}
+
+template<class T>
+size_t to_chars(substr buf, fmt::right_<T> const& C4_RESTRICT align)
+{
+ size_t ret = to_chars(buf, align.val);
+ if(ret >= buf.len || ret >= align.width)
+ return ret > align.width ? ret : align.width;
+ size_t rem = static_cast<size_t>(align.width - ret);
+ buf.first(rem).fill(align.pad);
+ to_chars(buf.sub(rem), align.val);
+ return align.width;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+// terminates the variadic recursion
+inline size_t cat(substr /*buf*/)
+{
+ return 0;
+}
+/// @endcond
+
+
+/** serialize the arguments, concatenating them to the given fixed-size buffer.
+ * The buffer size is strictly respected: no writes will occur beyond its end.
+ * @return the number of characters needed to write all the arguments into the buffer.
+ * @see c4::catrs() if instead of a fixed-size buffer, a resizeable container is desired
+ * @see c4::uncat() for the inverse function
+ * @see c4::catsep() if a separator between each argument is to be used
+ * @see c4::format() if a format string is desired */
+template<class Arg, class... Args>
+size_t cat(substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ size_t num = to_chars(buf, a);
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num += cat(buf, more...);
+ return num;
+}
+
+/** like c4::cat() but return a substr instead of a size */
+template<class... Args>
+substr cat_sub(substr buf, Args && ...args)
+{
+ size_t sz = cat(buf, std::forward<Args>(args)...);
+ C4_CHECK(sz <= buf.len);
+ return {buf.str, sz <= buf.len ? sz : buf.len};
+}
+
+
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+// terminates the variadic recursion
+inline size_t uncat(csubstr /*buf*/)
+{
+ return 0;
+}
+/// @endcond
+
+
+/** deserialize the arguments from the given buffer.
+ *
+ * @return the number of characters read from the buffer, or csubstr::npos
+ * if a conversion was not successful.
+ * @see c4::cat(). c4::uncat() is the inverse of c4::cat(). */
+template<class Arg, class... Args>
+size_t uncat(csubstr buf, Arg & C4_RESTRICT a, Args & C4_RESTRICT ...more)
+{
+ size_t out = from_chars_first(buf, &a);
+ if(C4_UNLIKELY(out == csubstr::npos))
+ return csubstr::npos;
+ buf = buf.len >= out ? buf.sub(out) : substr{};
+ size_t num = uncat(buf, more...);
+ if(C4_UNLIKELY(num == csubstr::npos))
+ return csubstr::npos;
+ return out + num;
+}
+
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+namespace detail {
+
+template<class Sep>
+inline size_t catsep_more(substr /*buf*/, Sep const& C4_RESTRICT /*sep*/)
+{
+ return 0;
+}
+
+template<class Sep, class Arg, class... Args>
+size_t catsep_more(substr buf, Sep const& C4_RESTRICT sep, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ size_t ret = to_chars(buf, sep), num = ret;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = to_chars(buf, a);
+ num += ret;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = catsep_more(buf, sep, more...);
+ num += ret;
+ return num;
+}
+
+template<class Sep>
+inline size_t uncatsep_more(csubstr /*buf*/, Sep & /*sep*/)
+{
+ return 0;
+}
+
+template<class Sep, class Arg, class... Args>
+size_t uncatsep_more(csubstr buf, Sep & C4_RESTRICT sep, Arg & C4_RESTRICT a, Args & C4_RESTRICT ...more)
+{
+ size_t ret = from_chars_first(buf, &sep), num = ret;
+ if(C4_UNLIKELY(ret == csubstr::npos))
+ return csubstr::npos;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = from_chars_first(buf, &a);
+ if(C4_UNLIKELY(ret == csubstr::npos))
+ return csubstr::npos;
+ num += ret;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = uncatsep_more(buf, sep, more...);
+ if(C4_UNLIKELY(ret == csubstr::npos))
+ return csubstr::npos;
+ num += ret;
+ return num;
+}
+
+} // namespace detail
+
+
+/** serialize the arguments, concatenating them to the given fixed-size
+ * buffer, using a separator between each argument.
+ * The buffer size is strictly respected: no writes will occur beyond its end.
+ * @return the number of characters needed to write all the arguments into the buffer.
+ * @see c4::catseprs() if instead of a fixed-size buffer, a resizeable container is desired
+ * @see c4::uncatsep() for the inverse function (ie, reading instead of writing)
+ * @see c4::cat() if no separator is needed
+ * @see c4::format() if a format string is desired */
+template<class Sep, class Arg, class... Args>
+size_t catsep(substr buf, Sep const& C4_RESTRICT sep, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ size_t num = to_chars(buf, a);
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num += detail::catsep_more(buf, sep, more...);
+ return num;
+}
+
+/** like c4::catsep() but return a substr instead of a size
+ * @see c4::catsep(). c4::uncatsep() is the inverse of c4::catsep(). */
+template<class... Args>
+substr catsep_sub(substr buf, Args && ...args)
+{
+ size_t sz = catsep(buf, std::forward<Args>(args)...);
+ C4_CHECK(sz <= buf.len);
+ return {buf.str, sz <= buf.len ? sz : buf.len};
+}
+
+/** deserialize the arguments from the given buffer, using a separator.
+ *
+ * @return the number of characters read from the buffer, or csubstr::npos
+ * if a conversion was not successful
+ * @see c4::catsep(). c4::uncatsep() is the inverse of c4::catsep(). */
+template<class Sep, class Arg, class... Args>
+size_t uncatsep(csubstr buf, Sep & C4_RESTRICT sep, Arg & C4_RESTRICT a, Args & C4_RESTRICT ...more)
+{
+ size_t ret = from_chars_first(buf, &a), num = ret;
+ if(C4_UNLIKELY(ret == csubstr::npos))
+ return csubstr::npos;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = detail::uncatsep_more(buf, sep, more...);
+ if(C4_UNLIKELY(ret == csubstr::npos))
+ return csubstr::npos;
+ num += ret;
+ return num;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+// terminates the variadic recursion
+inline size_t format(substr buf, csubstr fmt)
+{
+ return to_chars(buf, fmt);
+}
+/// @endcond
+
+
+/** using a format string, serialize the arguments into the given
+ * fixed-size buffer.
+ * The buffer size is strictly respected: no writes will occur beyond its end.
+ * In the format string, each argument is marked with a compact
+ * curly-bracket pair: {}. Arguments beyond the last curly bracket pair
+ * are silently ignored. For example:
+ * @code{.cpp}
+ * c4::format(buf, "the {} drank {} {}", "partier", 5, "beers"); // the partier drank 5 beers
+ * c4::format(buf, "the {} drank {} {}", "programmer", 6, "coffees"); // the programmer drank 6 coffees
+ * @endcode
+ * @return the number of characters needed to write into the buffer.
+ * @see c4::formatrs() if instead of a fixed-size buffer, a resizeable container is desired
+ * @see c4::unformat() for the inverse function
+ * @see c4::cat() if no format or separator is needed
+ * @see c4::catsep() if no format is needed, but a separator must be used */
+template<class Arg, class... Args>
+size_t format(substr buf, csubstr fmt, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ size_t pos = fmt.find("{}"); // @todo use _find_fmt()
+ if(C4_UNLIKELY(pos == csubstr::npos))
+ return to_chars(buf, fmt);
+ size_t num = to_chars(buf, fmt.sub(0, pos));
+ size_t out = num;
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num = to_chars(buf, a);
+ out += num;
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num = format(buf, fmt.sub(pos + 2), more...);
+ out += num;
+ return out;
+}
+
+/** like c4::format() but return a substr instead of a size
+ * @see c4::format()
+ * @see c4::catsep(). uncatsep() is the inverse of catsep(). */
+template<class... Args>
+substr format_sub(substr buf, csubstr fmt, Args const& C4_RESTRICT ...args)
+{
+ size_t sz = c4::format(buf, fmt, args...);
+ C4_CHECK(sz <= buf.len);
+ return {buf.str, sz <= buf.len ? sz : buf.len};
+}
+
+
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+// terminates the variadic recursion
+inline size_t unformat(csubstr /*buf*/, csubstr fmt)
+{
+ return fmt.len;
+}
+/// @endcond
+
+
+/** using a format string, deserialize the arguments from the given
+ * buffer.
+ * @return the number of characters read from the buffer, or npos if a conversion failed.
+ * @see c4::format(). c4::unformat() is the inverse function to format(). */
+template<class Arg, class... Args>
+size_t unformat(csubstr buf, csubstr fmt, Arg & C4_RESTRICT a, Args & C4_RESTRICT ...more)
+{
+ const size_t pos = fmt.find("{}");
+ if(C4_UNLIKELY(pos == csubstr::npos))
+ return unformat(buf, fmt);
+ size_t num = pos;
+ size_t out = num;
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num = from_chars_first(buf, &a);
+ if(C4_UNLIKELY(num == csubstr::npos))
+ return csubstr::npos;
+ out += num;
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num = unformat(buf, fmt.sub(pos + 2), more...);
+ if(C4_UNLIKELY(num == csubstr::npos))
+ return csubstr::npos;
+ out += num;
+ return out;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** a tag type for marking append to container
+ * @see c4::catrs() */
+struct append_t {};
+
+/** a tag variable
+ * @see c4::catrs() */
+constexpr const append_t append = {};
+
+
+//-----------------------------------------------------------------------------
+
+/** like c4::cat(), but receives a container, and resizes it as needed to contain
+ * the result. The container is overwritten. To append to it, use the append
+ * overload.
+ * @see c4::cat() */
+template<class CharOwningContainer, class... Args>
+inline void catrs(CharOwningContainer * C4_RESTRICT cont, Args const& C4_RESTRICT ...args)
+{
+retry:
+ substr buf = to_substr(*cont);
+ size_t ret = cat(buf, args...);
+ cont->resize(ret);
+ if(ret > buf.len)
+ goto retry;
+}
+
+/** like c4::cat(), but creates and returns a new container sized as needed to contain
+ * the result.
+ * @see c4::cat() */
+template<class CharOwningContainer, class... Args>
+inline CharOwningContainer catrs(Args const& C4_RESTRICT ...args)
+{
+ CharOwningContainer cont;
+ catrs(&cont, args...);
+ return cont;
+}
+
+/** like c4::cat(), but receives a container, and appends to it instead of
+ * overwriting it. The container is resized as needed to contain the result.
+ * @return the region newly appended to the original container
+ * @see c4::cat()
+ * @see c4::catrs() */
+template<class CharOwningContainer, class... Args>
+inline csubstr catrs(append_t, CharOwningContainer * C4_RESTRICT cont, Args const& C4_RESTRICT ...args)
+{
+ const size_t pos = cont->size();
+retry:
+ substr buf = to_substr(*cont).sub(pos);
+ size_t ret = cat(buf, args...);
+ cont->resize(pos + ret);
+ if(ret > buf.len)
+ goto retry;
+ return to_csubstr(*cont).range(pos, cont->size());
+}
+
+
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+// terminates the recursion
+template<class CharOwningContainer, class Sep, class... Args>
+inline void catseprs(CharOwningContainer * C4_RESTRICT, Sep const& C4_RESTRICT)
+{
+ return;
+}
+/// @end cond
+
+
+/** like c4::catsep(), but receives a container, and resizes it as needed to contain the result.
+ * The container is overwritten. To append to the container use the append overload.
+ * @see c4::catsep() */
+template<class CharOwningContainer, class Sep, class... Args>
+inline void catseprs(CharOwningContainer * C4_RESTRICT cont, Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...args)
+{
+retry:
+ substr buf = to_substr(*cont);
+ size_t ret = catsep(buf, sep, args...);
+ cont->resize(ret);
+ if(ret > buf.len)
+ goto retry;
+}
+
+/** like c4::catsep(), but create a new container with the result.
+ * @return the requested container */
+template<class CharOwningContainer, class Sep, class... Args>
+inline CharOwningContainer catseprs(Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...args)
+{
+ CharOwningContainer cont;
+ catseprs(&cont, sep, args...);
+ return cont;
+}
+
+
+/// @cond dev
+// terminates the recursion
+template<class CharOwningContainer, class Sep, class... Args>
+inline csubstr catseprs(append_t, CharOwningContainer * C4_RESTRICT, Sep const& C4_RESTRICT)
+{
+ csubstr s;
+ return s;
+}
+/// @endcond
+
+/** like catsep(), but receives a container, and appends the arguments, resizing the
+ * container as needed to contain the result. The buffer is appended to.
+ * @return a csubstr of the appended part
+ * @ingroup formatting_functions */
+template<class CharOwningContainer, class Sep, class... Args>
+inline csubstr catseprs(append_t, CharOwningContainer * C4_RESTRICT cont, Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...args)
+{
+ const size_t pos = cont->size();
+retry:
+ substr buf = to_substr(*cont).sub(pos);
+ size_t ret = catsep(buf, sep, args...);
+ cont->resize(pos + ret);
+ if(ret > buf.len)
+ goto retry;
+ return to_csubstr(*cont).range(pos, cont->size());
+}
+
+
+//-----------------------------------------------------------------------------
+
+/** like c4::format(), but receives a container, and resizes it as needed
+ * to contain the result. The container is overwritten. To append to
+ * the container use the append overload.
+ * @see c4::format() */
+template<class CharOwningContainer, class... Args>
+inline void formatrs(CharOwningContainer * C4_RESTRICT cont, csubstr fmt, Args const& C4_RESTRICT ...args)
+{
+retry:
+ substr buf = to_substr(*cont);
+ size_t ret = format(buf, fmt, args...);
+ cont->resize(ret);
+ if(ret > buf.len)
+ goto retry;
+}
+
+/** like c4::format(), but create a new container with the result.
+ * @return the requested container */
+template<class CharOwningContainer, class... Args>
+inline CharOwningContainer formatrs(csubstr fmt, Args const& C4_RESTRICT ...args)
+{
+ CharOwningContainer cont;
+ formatrs(&cont, fmt, args...);
+ return cont;
+}
+
+/** like format(), but receives a container, and appends the
+ * arguments, resizing the container as needed to contain the
+ * result. The buffer is appended to.
+ * @return the region newly appended to the original container
+ * @ingroup formatting_functions */
+template<class CharOwningContainer, class... Args>
+inline csubstr formatrs(append_t, CharOwningContainer * C4_RESTRICT cont, csubstr fmt, Args const& C4_RESTRICT ...args)
+{
+ const size_t pos = cont->size();
+retry:
+ substr buf = to_substr(*cont).sub(pos);
+ size_t ret = format(buf, fmt, args...);
+ cont->resize(pos + ret);
+ if(ret > buf.len)
+ goto retry;
+ return to_csubstr(*cont).range(pos, cont->size());
+}
+
+} // namespace c4
+
+#ifdef _MSC_VER
+# pragma warning(pop)
+#elif defined(__clang__)
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#endif /* _C4_FORMAT_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/format.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/dump.hpp
+// https://github.com/biojppm/c4core/src/c4/dump.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef C4_DUMP_HPP_
+#define C4_DUMP_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/substr.hpp
+//#include <c4/substr.hpp>
+#if !defined(C4_SUBSTR_HPP_) && !defined(_C4_SUBSTR_HPP_)
+#error "amalgamate: file c4/substr.hpp must have been included at this point"
+#endif /* C4_SUBSTR_HPP_ */
+
+
+namespace c4 {
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** type of the function to dump characters */
+using DumperPfn = void (*)(csubstr buf);
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+template<DumperPfn dumpfn, class Arg>
+inline size_t dump(substr buf, Arg const& a)
+{
+ size_t sz = to_chars(buf, a); // need to serialize to the buffer
+ if(C4_LIKELY(sz <= buf.len))
+ dumpfn(buf.first(sz));
+ return sz;
+}
+
+template<class DumperFn, class Arg>
+inline size_t dump(DumperFn &&dumpfn, substr buf, Arg const& a)
+{
+ size_t sz = to_chars(buf, a); // need to serialize to the buffer
+ if(C4_LIKELY(sz <= buf.len))
+ dumpfn(buf.first(sz));
+ return sz;
+}
+
+template<DumperPfn dumpfn>
+inline size_t dump(substr buf, csubstr a)
+{
+ if(buf.len)
+ dumpfn(a); // dump directly, no need to serialize to the buffer
+ return 0; // no space was used in the buffer
+}
+
+template<class DumperFn>
+inline size_t dump(DumperFn &&dumpfn, substr buf, csubstr a)
+{
+ if(buf.len)
+ dumpfn(a); // dump directly, no need to serialize to the buffer
+ return 0; // no space was used in the buffer
+}
+
+template<DumperPfn dumpfn, size_t N>
+inline size_t dump(substr buf, const char (&a)[N])
+{
+ if(buf.len)
+ dumpfn(csubstr(a)); // dump directly, no need to serialize to the buffer
+ return 0; // no space was used in the buffer
+}
+
+template<class DumperFn, size_t N>
+inline size_t dump(DumperFn &&dumpfn, substr buf, const char (&a)[N])
+{
+ if(buf.len)
+ dumpfn(csubstr(a)); // dump directly, no need to serialize to the buffer
+ return 0; // no space was used in the buffer
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** */
+struct DumpResults
+{
+ enum : size_t { noarg = (size_t)-1 };
+ size_t bufsize = 0;
+ size_t lastok = noarg;
+ bool success_until(size_t expected) const { return lastok == noarg ? false : lastok >= expected; }
+ bool write_arg(size_t arg) const { return lastok == noarg || arg > lastok; }
+ size_t argfail() const { return lastok + 1; }
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+// terminates the variadic recursion
+template<class DumperFn>
+size_t cat_dump(DumperFn &&, substr)
+{
+ return 0;
+}
+
+// terminates the variadic recursion
+template<DumperPfn dumpfn>
+size_t cat_dump(substr)
+{
+ return 0;
+}
+/// @endcond
+
+/** take the function pointer as a function argument */
+template<class DumperFn, class Arg, class... Args>
+size_t cat_dump(DumperFn &&dumpfn, substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ size_t size_for_a = dump(dumpfn, buf, a);
+ if(C4_UNLIKELY(size_for_a > buf.len))
+ buf = buf.first(0); // ensure no more calls
+ size_t size_for_more = cat_dump(dumpfn, buf, more...);
+ return size_for_more > size_for_a ? size_for_more : size_for_a;
+}
+
+/** take the function pointer as a template argument */
+template<DumperPfn dumpfn,class Arg, class... Args>
+size_t cat_dump(substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ size_t size_for_a = dump<dumpfn>(buf, a);
+ if(C4_LIKELY(size_for_a > buf.len))
+ buf = buf.first(0); // ensure no more calls
+ size_t size_for_more = cat_dump<dumpfn>(buf, more...);
+ return size_for_more > size_for_a ? size_for_more : size_for_a;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+namespace detail {
+
+// terminates the variadic recursion
+template<DumperPfn dumpfn, class Arg>
+DumpResults cat_dump_resume(size_t currarg, DumpResults results, substr buf, Arg const& C4_RESTRICT a)
+{
+ if(C4_LIKELY(results.write_arg(currarg)))
+ {
+ size_t sz = dump<dumpfn>(buf, a); // yield to the specialized function
+ if(currarg == results.lastok + 1 && sz <= buf.len)
+ results.lastok = currarg;
+ results.bufsize = sz > results.bufsize ? sz : results.bufsize;
+ }
+ return results;
+}
+
+// terminates the variadic recursion
+template<class DumperFn, class Arg>
+DumpResults cat_dump_resume(size_t currarg, DumperFn &&dumpfn, DumpResults results, substr buf, Arg const& C4_RESTRICT a)
+{
+ if(C4_LIKELY(results.write_arg(currarg)))
+ {
+ size_t sz = dump(dumpfn, buf, a); // yield to the specialized function
+ if(currarg == results.lastok + 1 && sz <= buf.len)
+ results.lastok = currarg;
+ results.bufsize = sz > results.bufsize ? sz : results.bufsize;
+ }
+ return results;
+}
+
+template<DumperPfn dumpfn, class Arg, class... Args>
+DumpResults cat_dump_resume(size_t currarg, DumpResults results, substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ results = detail::cat_dump_resume<dumpfn>(currarg, results, buf, a);
+ return detail::cat_dump_resume<dumpfn>(currarg + 1u, results, buf, more...);
+}
+
+template<class DumperFn, class Arg, class... Args>
+DumpResults cat_dump_resume(size_t currarg, DumperFn &&dumpfn, DumpResults results, substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ results = detail::cat_dump_resume(currarg, dumpfn, results, buf, a);
+ return detail::cat_dump_resume(currarg + 1u, dumpfn, results, buf, more...);
+}
+} // namespace detail
+/// @endcond
+
+
+template<DumperPfn dumpfn, class Arg, class... Args>
+C4_ALWAYS_INLINE DumpResults cat_dump_resume(DumpResults results, substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ if(results.bufsize > buf.len)
+ return results;
+ return detail::cat_dump_resume<dumpfn>(0u, results, buf, a, more...);
+}
+
+template<class DumperFn, class Arg, class... Args>
+C4_ALWAYS_INLINE DumpResults cat_dump_resume(DumperFn &&dumpfn, DumpResults results, substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ if(results.bufsize > buf.len)
+ return results;
+ return detail::cat_dump_resume(0u, dumpfn, results, buf, a, more...);
+}
+
+template<DumperPfn dumpfn, class Arg, class... Args>
+C4_ALWAYS_INLINE DumpResults cat_dump_resume(substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ return detail::cat_dump_resume<dumpfn>(0u, DumpResults{}, buf, a, more...);
+}
+
+template<class DumperFn, class Arg, class... Args>
+C4_ALWAYS_INLINE DumpResults cat_dump_resume(DumperFn &&dumpfn, substr buf, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ return detail::cat_dump_resume(0u, dumpfn, DumpResults{}, buf, a, more...);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+// terminate the recursion
+template<class DumperFn, class Sep>
+size_t catsep_dump(DumperFn &&, substr, Sep const& C4_RESTRICT)
+{
+ return 0;
+}
+
+// terminate the recursion
+template<DumperPfn dumpfn, class Sep>
+size_t catsep_dump(substr, Sep const& C4_RESTRICT)
+{
+ return 0;
+}
+/// @endcond
+
+/** take the function pointer as a function argument */
+template<class DumperFn, class Sep, class Arg, class... Args>
+size_t catsep_dump(DumperFn &&dumpfn, substr buf, Sep const& C4_RESTRICT sep, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ size_t sz = dump(dumpfn, buf, a);
+ if(C4_UNLIKELY(sz > buf.len))
+ buf = buf.first(0); // ensure no more calls
+ if C4_IF_CONSTEXPR (sizeof...(more) > 0)
+ {
+ size_t szsep = dump(dumpfn, buf, sep);
+ if(C4_UNLIKELY(szsep > buf.len))
+ buf = buf.first(0); // ensure no more calls
+ sz = sz > szsep ? sz : szsep;
+ }
+ size_t size_for_more = catsep_dump(dumpfn, buf, sep, more...);
+ return size_for_more > sz ? size_for_more : sz;
+}
+
+/** take the function pointer as a template argument */
+template<DumperPfn dumpfn, class Sep, class Arg, class... Args>
+size_t catsep_dump(substr buf, Sep const& C4_RESTRICT sep, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ size_t sz = dump<dumpfn>(buf, a);
+ if(C4_UNLIKELY(sz > buf.len))
+ buf = buf.first(0); // ensure no more calls
+ if C4_IF_CONSTEXPR (sizeof...(more) > 0)
+ {
+ size_t szsep = dump<dumpfn>(buf, sep);
+ if(C4_UNLIKELY(szsep > buf.len))
+ buf = buf.first(0); // ensure no more calls
+ sz = sz > szsep ? sz : szsep;
+ }
+ size_t size_for_more = catsep_dump<dumpfn>(buf, sep, more...);
+ return size_for_more > sz ? size_for_more : sz;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+namespace detail {
+template<DumperPfn dumpfn, class Arg>
+void catsep_dump_resume_(size_t currarg, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Arg const& C4_RESTRICT a)
+{
+ if(C4_LIKELY(results->write_arg(currarg)))
+ {
+ size_t sz = dump<dumpfn>(*buf, a);
+ results->bufsize = sz > results->bufsize ? sz : results->bufsize;
+ if(C4_LIKELY(sz <= buf->len))
+ results->lastok = currarg;
+ else
+ buf->len = 0;
+ }
+}
+
+template<class DumperFn, class Arg>
+void catsep_dump_resume_(size_t currarg, DumperFn &&dumpfn, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Arg const& C4_RESTRICT a)
+{
+ if(C4_LIKELY(results->write_arg(currarg)))
+ {
+ size_t sz = dump(dumpfn, *buf, a);
+ results->bufsize = sz > results->bufsize ? sz : results->bufsize;
+ if(C4_LIKELY(sz <= buf->len))
+ results->lastok = currarg;
+ else
+ buf->len = 0;
+ }
+}
+
+template<DumperPfn dumpfn, class Sep, class Arg>
+C4_ALWAYS_INLINE void catsep_dump_resume(size_t currarg, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Sep const& C4_RESTRICT, Arg const& C4_RESTRICT a)
+{
+ detail::catsep_dump_resume_<dumpfn>(currarg, results, buf, a);
+}
+
+template<class DumperFn, class Sep, class Arg>
+C4_ALWAYS_INLINE void catsep_dump_resume(size_t currarg, DumperFn &&dumpfn, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Sep const& C4_RESTRICT, Arg const& C4_RESTRICT a)
+{
+ detail::catsep_dump_resume_(currarg, dumpfn, results, buf, a);
+}
+
+template<DumperPfn dumpfn, class Sep, class Arg, class... Args>
+C4_ALWAYS_INLINE void catsep_dump_resume(size_t currarg, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Sep const& C4_RESTRICT sep, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ detail::catsep_dump_resume_<dumpfn>(currarg , results, buf, a);
+ detail::catsep_dump_resume_<dumpfn>(currarg + 1u, results, buf, sep);
+ detail::catsep_dump_resume <dumpfn>(currarg + 2u, results, buf, sep, more...);
+}
+
+template<class DumperFn, class Sep, class Arg, class... Args>
+C4_ALWAYS_INLINE void catsep_dump_resume(size_t currarg, DumperFn &&dumpfn, DumpResults *C4_RESTRICT results, substr *C4_RESTRICT buf, Sep const& C4_RESTRICT sep, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ detail::catsep_dump_resume_(currarg , dumpfn, results, buf, a);
+ detail::catsep_dump_resume_(currarg + 1u, dumpfn, results, buf, sep);
+ detail::catsep_dump_resume (currarg + 2u, dumpfn, results, buf, sep, more...);
+}
+} // namespace detail
+/// @endcond
+
+
+template<DumperPfn dumpfn, class Sep, class... Args>
+C4_ALWAYS_INLINE DumpResults catsep_dump_resume(DumpResults results, substr buf, Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...more)
+{
+ detail::catsep_dump_resume<dumpfn>(0u, &results, &buf, sep, more...);
+ return results;
+}
+
+template<class DumperFn, class Sep, class... Args>
+C4_ALWAYS_INLINE DumpResults catsep_dump_resume(DumperFn &&dumpfn, DumpResults results, substr buf, Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...more)
+{
+ detail::catsep_dump_resume(0u, dumpfn, &results, &buf, sep, more...);
+ return results;
+}
+
+template<DumperPfn dumpfn, class Sep, class... Args>
+C4_ALWAYS_INLINE DumpResults catsep_dump_resume(substr buf, Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...more)
+{
+ DumpResults results;
+ detail::catsep_dump_resume<dumpfn>(0u, &results, &buf, sep, more...);
+ return results;
+}
+
+template<class DumperFn, class Sep, class... Args>
+C4_ALWAYS_INLINE DumpResults catsep_dump_resume(DumperFn &&dumpfn, substr buf, Sep const& C4_RESTRICT sep, Args const& C4_RESTRICT ...more)
+{
+ DumpResults results;
+ detail::catsep_dump_resume(0u, dumpfn, &results, &buf, sep, more...);
+ return results;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** take the function pointer as a function argument */
+template<class DumperFn>
+C4_ALWAYS_INLINE size_t format_dump(DumperFn &&dumpfn, substr buf, csubstr fmt)
+{
+ // we can dump without using buf
+ // but we'll only dump if the buffer is ok
+ if(C4_LIKELY(buf.len > 0 && fmt.len))
+ dumpfn(fmt);
+ return 0u;
+}
+
+/** take the function pointer as a function argument */
+template<DumperPfn dumpfn>
+C4_ALWAYS_INLINE size_t format_dump(substr buf, csubstr fmt)
+{
+ // we can dump without using buf
+ // but we'll only dump if the buffer is ok
+ if(C4_LIKELY(buf.len > 0 && fmt.len > 0))
+ dumpfn(fmt);
+ return 0u;
+}
+
+/** take the function pointer as a function argument */
+template<class DumperFn, class Arg, class... Args>
+size_t format_dump(DumperFn &&dumpfn, substr buf, csubstr fmt, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ // we can dump without using buf
+ // but we'll only dump if the buffer is ok
+ size_t pos = fmt.find("{}"); // @todo use _find_fmt()
+ if(C4_UNLIKELY(pos == csubstr::npos))
+ {
+ if(C4_LIKELY(buf.len > 0 && fmt.len > 0))
+ dumpfn(fmt);
+ return 0u;
+ }
+ if(C4_LIKELY(buf.len > 0 && pos > 0))
+ dumpfn(fmt.first(pos)); // we can dump without using buf
+ fmt = fmt.sub(pos + 2); // skip {} do this before assigning to pos again
+ pos = dump(dumpfn, buf, a);
+ if(C4_UNLIKELY(pos > buf.len))
+ buf.len = 0; // ensure no more calls to dump
+ size_t size_for_more = format_dump(dumpfn, buf, fmt, more...);
+ return size_for_more > pos ? size_for_more : pos;
+}
+
+/** take the function pointer as a template argument */
+template<DumperPfn dumpfn, class Arg, class... Args>
+size_t format_dump(substr buf, csubstr fmt, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ // we can dump without using buf
+ // but we'll only dump if the buffer is ok
+ size_t pos = fmt.find("{}"); // @todo use _find_fmt()
+ if(C4_UNLIKELY(pos == csubstr::npos))
+ {
+ if(C4_LIKELY(buf.len > 0 && fmt.len > 0))
+ dumpfn(fmt);
+ return 0u;
+ }
+ if(C4_LIKELY(buf.len > 0 && pos > 0))
+ dumpfn(fmt.first(pos)); // we can dump without using buf
+ fmt = fmt.sub(pos + 2); // skip {} do this before assigning to pos again
+ pos = dump<dumpfn>(buf, a);
+ if(C4_UNLIKELY(pos > buf.len))
+ buf.len = 0; // ensure no more calls to dump
+ size_t size_for_more = format_dump<dumpfn>(buf, fmt, more...);
+ return size_for_more > pos ? size_for_more : pos;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/// @cond dev
+namespace detail {
+
+template<DumperPfn dumpfn>
+DumpResults format_dump_resume(size_t currarg, DumpResults results, substr buf, csubstr fmt)
+{
+ // we can dump without using buf
+ // but we'll only dump if the buffer is ok
+ if(C4_LIKELY(buf.len > 0))
+ {
+ dumpfn(fmt);
+ results.lastok = currarg;
+ }
+ return results;
+}
+
+template<class DumperFn>
+DumpResults format_dump_resume(size_t currarg, DumperFn &&dumpfn, DumpResults results, substr buf, csubstr fmt)
+{
+ // we can dump without using buf
+ // but we'll only dump if the buffer is ok
+ if(C4_LIKELY(buf.len > 0))
+ {
+ dumpfn(fmt);
+ results.lastok = currarg;
+ }
+ return results;
+}
+
+template<DumperPfn dumpfn, class Arg, class... Args>
+DumpResults format_dump_resume(size_t currarg, DumpResults results, substr buf, csubstr fmt, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ // we need to process the format even if we're not
+ // going to print the first arguments because we're resuming
+ size_t pos = fmt.find("{}"); // @todo use _find_fmt()
+ // we can dump without using buf
+ // but we'll only dump if the buffer is ok
+ if(C4_LIKELY(results.write_arg(currarg)))
+ {
+ if(C4_UNLIKELY(pos == csubstr::npos))
+ {
+ if(C4_LIKELY(buf.len > 0))
+ {
+ results.lastok = currarg;
+ dumpfn(fmt);
+ }
+ return results;
+ }
+ if(C4_LIKELY(buf.len > 0))
+ {
+ results.lastok = currarg;
+ dumpfn(fmt.first(pos));
+ }
+ }
+ fmt = fmt.sub(pos + 2);
+ if(C4_LIKELY(results.write_arg(currarg + 1)))
+ {
+ pos = dump<dumpfn>(buf, a);
+ results.bufsize = pos > results.bufsize ? pos : results.bufsize;
+ if(C4_LIKELY(pos <= buf.len))
+ results.lastok = currarg + 1;
+ else
+ buf.len = 0;
+ }
+ return detail::format_dump_resume<dumpfn>(currarg + 2u, results, buf, fmt, more...);
+}
+/// @endcond
+
+
+template<class DumperFn, class Arg, class... Args>
+DumpResults format_dump_resume(size_t currarg, DumperFn &&dumpfn, DumpResults results, substr buf, csubstr fmt, Arg const& C4_RESTRICT a, Args const& C4_RESTRICT ...more)
+{
+ // we need to process the format even if we're not
+ // going to print the first arguments because we're resuming
+ size_t pos = fmt.find("{}"); // @todo use _find_fmt()
+ // we can dump without using buf
+ // but we'll only dump if the buffer is ok
+ if(C4_LIKELY(results.write_arg(currarg)))
+ {
+ if(C4_UNLIKELY(pos == csubstr::npos))
+ {
+ if(C4_LIKELY(buf.len > 0))
+ {
+ results.lastok = currarg;
+ dumpfn(fmt);
+ }
+ return results;
+ }
+ if(C4_LIKELY(buf.len > 0))
+ {
+ results.lastok = currarg;
+ dumpfn(fmt.first(pos));
+ }
+ }
+ fmt = fmt.sub(pos + 2);
+ if(C4_LIKELY(results.write_arg(currarg + 1)))
+ {
+ pos = dump(dumpfn, buf, a);
+ results.bufsize = pos > results.bufsize ? pos : results.bufsize;
+ if(C4_LIKELY(pos <= buf.len))
+ results.lastok = currarg + 1;
+ else
+ buf.len = 0;
+ }
+ return detail::format_dump_resume(currarg + 2u, dumpfn, results, buf, fmt, more...);
+}
+} // namespace detail
+
+
+template<DumperPfn dumpfn, class... Args>
+C4_ALWAYS_INLINE DumpResults format_dump_resume(DumpResults results, substr buf, csubstr fmt, Args const& C4_RESTRICT ...more)
+{
+ return detail::format_dump_resume<dumpfn>(0u, results, buf, fmt, more...);
+}
+
+template<class DumperFn, class... Args>
+C4_ALWAYS_INLINE DumpResults format_dump_resume(DumperFn &&dumpfn, DumpResults results, substr buf, csubstr fmt, Args const& C4_RESTRICT ...more)
+{
+ return detail::format_dump_resume(0u, dumpfn, results, buf, fmt, more...);
+}
+
+
+template<DumperPfn dumpfn, class... Args>
+C4_ALWAYS_INLINE DumpResults format_dump_resume(substr buf, csubstr fmt, Args const& C4_RESTRICT ...more)
+{
+ return detail::format_dump_resume<dumpfn>(0u, DumpResults{}, buf, fmt, more...);
+}
+
+template<class DumperFn, class... Args>
+C4_ALWAYS_INLINE DumpResults format_dump_resume(DumperFn &&dumpfn, substr buf, csubstr fmt, Args const& C4_RESTRICT ...more)
+{
+ return detail::format_dump_resume(0u, dumpfn, DumpResults{}, buf, fmt, more...);
+}
+
+
+} // namespace c4
+
+
+#endif /* C4_DUMP_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/dump.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/enum.hpp
+// https://github.com/biojppm/c4core/src/c4/enum.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_ENUM_HPP_
+#define _C4_ENUM_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+//included above:
+//#include <string.h>
+
+/** @file enum.hpp utilities for enums: convert to/from string
+ */
+
+
+namespace c4 {
+
+//! taken from http://stackoverflow.com/questions/15586163/c11-type-trait-to-differentiate-between-enum-class-and-regular-enum
+template<typename Enum>
+using is_scoped_enum = std::integral_constant<bool, std::is_enum<Enum>::value && !std::is_convertible<Enum, int>::value>;
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+typedef enum {
+ EOFFS_NONE = 0, ///< no offset
+ EOFFS_CLS = 1, ///< get the enum offset for the class name. @see eoffs_cls()
+ EOFFS_PFX = 2, ///< get the enum offset for the enum prefix. @see eoffs_pfx()
+ _EOFFS_LAST ///< reserved
+} EnumOffsetType;
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** A simple (proxy) container for the value-name pairs of an enum type.
+ * Uses linear search for finds; this could be improved for time-critical
+ * code. */
+template<class Enum>
+class EnumSymbols
+{
+public:
+
+ struct Sym
+ {
+ Enum value;
+ const char *name;
+
+ bool cmp(const char *s) const;
+ bool cmp(const char *s, size_t len) const;
+
+ const char *name_offs(EnumOffsetType t) const;
+ };
+
+ using const_iterator = Sym const*;
+
+public:
+
+ template<size_t N>
+ EnumSymbols(Sym const (&p)[N]) : m_symbols(p), m_num(N) {}
+
+ size_t size() const { return m_num; }
+ bool empty() const { return m_num == 0; }
+
+ Sym const* get(Enum v) const { auto p = find(v); C4_CHECK_MSG(p != nullptr, "could not find symbol=%zd", (std::ptrdiff_t)v); return p; }
+ Sym const* get(const char *s) const { auto p = find(s); C4_CHECK_MSG(p != nullptr, "could not find symbol \"%s\"", s); return p; }
+ Sym const* get(const char *s, size_t len) const { auto p = find(s, len); C4_CHECK_MSG(p != nullptr, "could not find symbol \"%.*s\"", len, s); return p; }
+
+ Sym const* find(Enum v) const;
+ Sym const* find(const char *s) const;
+ Sym const* find(const char *s, size_t len) const;
+
+ Sym const& operator[] (size_t i) const { C4_CHECK(i < m_num); return m_symbols[i]; }
+
+ Sym const* begin() const { return m_symbols; }
+ Sym const* end () const { return m_symbols + m_num; }
+
+private:
+
+ Sym const* m_symbols;
+ size_t const m_num;
+
+};
+
+//-----------------------------------------------------------------------------
+/** return an EnumSymbols object for the enum type T
+ *
+ * @warning SPECIALIZE! This needs to be specialized for each enum
+ * type. Failure to provide a specialization will cause a linker
+ * error. */
+template<class Enum>
+EnumSymbols<Enum> const esyms();
+
+
+/** return the offset for an enum symbol class. For example,
+ * eoffs_cls<MyEnumClass>() would be 13=strlen("MyEnumClass::").
+ *
+ * With this function you can announce that the full prefix (including
+ * an eventual enclosing class or C++11 enum class) is of a certain
+ * length.
+ *
+ * @warning Needs to be specialized for each enum class type that
+ * wants to use this. When no specialization is given, will return
+ * 0. */
+template<class Enum>
+size_t eoffs_cls()
+{
+ return 0;
+}
+
+
+/** return the offset for an enum symbol prefix. This includes
+ * eoffs_cls(). With this function you can announce that the full
+ * prefix (including an eventual enclosing class or C++11 enum class
+ * plus the string prefix) is of a certain length.
+ *
+ * @warning Needs to be specialized for each enum class type that
+ * wants to use this. When no specialization is given, will return
+ * 0. */
+template<class Enum>
+size_t eoffs_pfx()
+{
+ return 0;
+}
+
+
+template<class Enum>
+size_t eoffs(EnumOffsetType which)
+{
+ switch(which)
+ {
+ case EOFFS_NONE:
+ return 0;
+ case EOFFS_CLS:
+ return eoffs_cls<Enum>();
+ case EOFFS_PFX:
+ {
+ size_t pfx = eoffs_pfx<Enum>();
+ return pfx > 0 ? pfx : eoffs_cls<Enum>();
+ }
+ default:
+ C4_ERROR("unknown offset type %d", (int)which);
+ return 0;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+/** get the enum value corresponding to a c-string */
+
+#ifdef __clang__
+# pragma clang diagnostic push
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# if __GNUC__ >= 6
+# pragma GCC diagnostic ignored "-Wnull-dereference"
+# endif
+#endif
+
+template<class Enum>
+Enum str2e(const char* str)
+{
+ auto pairs = esyms<Enum>();
+ auto *p = pairs.get(str);
+ C4_CHECK_MSG(p != nullptr, "no valid enum pair name for '%s'", str);
+ return p->value;
+}
+
+/** get the c-string corresponding to an enum value */
+template<class Enum>
+const char* e2str(Enum e)
+{
+ auto es = esyms<Enum>();
+ auto *p = es.get(e);
+ C4_CHECK_MSG(p != nullptr, "no valid enum pair name");
+ return p->name;
+}
+
+/** like e2str(), but add an offset. */
+template<class Enum>
+const char* e2stroffs(Enum e, EnumOffsetType ot=EOFFS_PFX)
+{
+ const char *s = e2str<Enum>(e) + eoffs<Enum>(ot);
+ return s;
+}
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+//-----------------------------------------------------------------------------
+/** Find a symbol by value. Returns nullptr when none is found */
+template<class Enum>
+typename EnumSymbols<Enum>::Sym const* EnumSymbols<Enum>::find(Enum v) const
+{
+ for(Sym const* p = this->m_symbols, *e = p+this->m_num; p < e; ++p)
+ if(p->value == v)
+ return p;
+ return nullptr;
+}
+
+/** Find a symbol by name. Returns nullptr when none is found */
+template<class Enum>
+typename EnumSymbols<Enum>::Sym const* EnumSymbols<Enum>::find(const char *s) const
+{
+ for(Sym const* p = this->m_symbols, *e = p+this->m_num; p < e; ++p)
+ if(p->cmp(s))
+ return p;
+ return nullptr;
+}
+
+/** Find a symbol by name. Returns nullptr when none is found */
+template<class Enum>
+typename EnumSymbols<Enum>::Sym const* EnumSymbols<Enum>::find(const char *s, size_t len) const
+{
+ for(Sym const* p = this->m_symbols, *e = p+this->m_num; p < e; ++p)
+ if(p->cmp(s, len))
+ return p;
+ return nullptr;
+}
+
+//-----------------------------------------------------------------------------
+template<class Enum>
+bool EnumSymbols<Enum>::Sym::cmp(const char *s) const
+{
+ if(strcmp(name, s) == 0)
+ return true;
+
+ for(int i = 1; i < _EOFFS_LAST; ++i)
+ {
+ auto o = eoffs<Enum>((EnumOffsetType)i);
+ if(o > 0)
+ if(strcmp(name + o, s) == 0)
+ return true;
+ }
+
+ return false;
+}
+
+template<class Enum>
+bool EnumSymbols<Enum>::Sym::cmp(const char *s, size_t len) const
+{
+ if(strncmp(name, s, len) == 0)
+ return true;
+
+ size_t nlen = 0;
+ for(int i = 1; i <_EOFFS_LAST; ++i)
+ {
+ auto o = eoffs<Enum>((EnumOffsetType)i);
+ if(o > 0)
+ {
+ if(!nlen)
+ {
+ nlen = strlen(name);
+ }
+ C4_ASSERT(o < nlen);
+ size_t rem = nlen - o;
+ auto m = len > rem ? len : rem;
+ if(len >= m && strncmp(name + o, s, m) == 0)
+ return true;
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+template<class Enum>
+const char* EnumSymbols<Enum>::Sym::name_offs(EnumOffsetType t) const
+{
+ C4_ASSERT(eoffs<Enum>(t) < strlen(name));
+ return name + eoffs<Enum>(t);
+}
+
+} // namespace c4
+
+#endif // _C4_ENUM_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/enum.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/bitmask.hpp
+// https://github.com/biojppm/c4core/src/c4/bitmask.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_BITMASK_HPP_
+#define _C4_BITMASK_HPP_
+
+/** @file bitmask.hpp bitmask utilities */
+
+//included above:
+//#include <cstring>
+//included above:
+//#include <type_traits>
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/enum.hpp
+//#include "c4/enum.hpp"
+#if !defined(C4_ENUM_HPP_) && !defined(_C4_ENUM_HPP_)
+#error "amalgamate: file c4/enum.hpp must have been included at this point"
+#endif /* C4_ENUM_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/format.hpp
+//#include "c4/format.hpp"
+#if !defined(C4_FORMAT_HPP_) && !defined(_C4_FORMAT_HPP_)
+#error "amalgamate: file c4/format.hpp must have been included at this point"
+#endif /* C4_FORMAT_HPP_ */
+
+
+#ifdef _MSC_VER
+# pragma warning(push)
+# pragma warning(disable : 4996) // 'strncpy', fopen, etc: This function or variable may be unsafe
+#elif defined(__clang__)
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# if __GNUC__ >= 8
+# pragma GCC diagnostic ignored "-Wstringop-truncation"
+# pragma GCC diagnostic ignored "-Wstringop-overflow"
+# endif
+#endif
+
+namespace c4 {
+
+//-----------------------------------------------------------------------------
+/** write a bitmask to a stream, formatted as a string */
+
+template<class Enum, class Stream>
+Stream& bm2stream(Stream &s, typename std::underlying_type<Enum>::type bits, EnumOffsetType offst=EOFFS_PFX)
+{
+ using I = typename std::underlying_type<Enum>::type;
+ bool written = false;
+
+ auto const& pairs = esyms<Enum>();
+
+ // write non null value
+ if(bits)
+ {
+ // do reverse iteration to give preference to composite enum symbols,
+ // which are likely to appear at the end of the enum sequence
+ for(size_t i = pairs.size() - 1; i != size_t(-1); --i)
+ {
+ auto p = pairs[i];
+ I b(static_cast<I>(p.value));
+ if(b && (bits & b) == b)
+ {
+ if(written) s << '|'; // append bit-or character
+ written = true;
+ s << p.name_offs(offst); // append bit string
+ bits &= ~b;
+ }
+ }
+ return s;
+ }
+ else
+ {
+ // write a null value
+ for(size_t i = pairs.size() - 1; i != size_t(-1); --i)
+ {
+ auto p = pairs[i];
+ I b(static_cast<I>(p.value));
+ if(b == 0)
+ {
+ s << p.name_offs(offst);
+ written = true;
+ break;
+ }
+ }
+ }
+ if(!written)
+ {
+ s << '0';
+ }
+ return s;
+}
+
+template<class Enum, class Stream>
+typename std::enable_if<is_scoped_enum<Enum>::value, Stream&>::type
+bm2stream(Stream &s, Enum value, EnumOffsetType offst=EOFFS_PFX)
+{
+ using I = typename std::underlying_type<Enum>::type;
+ return bm2stream<Enum>(s, static_cast<I>(value), offst);
+}
+
+
+//-----------------------------------------------------------------------------
+
+// some utility macros, undefed below
+
+/// @cond dev
+
+/* Execute `code` if the `num` of characters is available in the str
+ * buffer. This macro simplifies the code for bm2str().
+ * @todo improve performance by writing from the end and moving only once. */
+#define _c4prependchars(code, num) \
+ if(str && (pos + num <= sz)) \
+ { \
+ /* move the current string to the right */ \
+ memmove(str + num, str, pos); \
+ /* now write in the beginning of the string */ \
+ code; \
+ } \
+ else if(str && sz) \
+ { \
+ C4_ERROR("cannot write to string pos=%d num=%d sz=%d", \
+ (int)pos, (int)num, (int)sz); \
+ } \
+ pos += num
+
+/* Execute `code` if the `num` of characters is available in the str
+ * buffer. This macro simplifies the code for bm2str(). */
+#define _c4appendchars(code, num) \
+ if(str && (pos + num <= sz)) \
+ { \
+ code; \
+ } \
+ else if(str && sz) \
+ { \
+ C4_ERROR("cannot write to string pos=%d num=%d sz=%d", \
+ (int)pos, (int)num, (int)sz); \
+ } \
+ pos += num
+
+/// @endcond
+
+
+/** convert a bitmask to string.
+ * return the number of characters written. To find the needed size,
+ * call first with str=nullptr and sz=0 */
+template<class Enum>
+size_t bm2str
+(
+ typename std::underlying_type<Enum>::type bits,
+ char *str=nullptr,
+ size_t sz=0,
+ EnumOffsetType offst=EOFFS_PFX
+)
+{
+ using I = typename std::underlying_type<Enum>::type;
+ C4_ASSERT((str == nullptr) == (sz == 0));
+
+ auto syms = esyms<Enum>();
+ size_t pos = 0;
+ typename EnumSymbols<Enum>::Sym const* C4_RESTRICT zero = nullptr;
+
+ // do reverse iteration to give preference to composite enum symbols,
+ // which are likely to appear later in the enum sequence
+ for(size_t i = syms.size()-1; i != size_t(-1); --i)
+ {
+ auto const &C4_RESTRICT p = syms[i]; // do not copy, we are assigning to `zero`
+ I b = static_cast<I>(p.value);
+ if(b == 0)
+ {
+ zero = &p; // save this symbol for later
+ }
+ else if((bits & b) == b)
+ {
+ bits &= ~b;
+ // append bit-or character
+ if(pos > 0)
+ {
+ _c4prependchars(*str = '|', 1);
+ }
+ // append bit string
+ const char *pname = p.name_offs(offst);
+ size_t len = strlen(pname);
+ _c4prependchars(strncpy(str, pname, len), len);
+ }
+ }
+
+ C4_CHECK_MSG(bits == 0, "could not find all bits");
+ if(pos == 0) // make sure at least something is written
+ {
+ if(zero) // if we have a zero symbol, use that
+ {
+ const char *pname = zero->name_offs(offst);
+ size_t len = strlen(pname);
+ _c4prependchars(strncpy(str, pname, len), len);
+ }
+ else // otherwise just write an integer zero
+ {
+ _c4prependchars(*str = '0', 1);
+ }
+ }
+ _c4appendchars(str[pos] = '\0', 1);
+
+ return pos;
+}
+
+
+// cleanup!
+#undef _c4appendchars
+#undef _c4prependchars
+
+
+/** scoped enums do not convert automatically to their underlying type,
+ * so this SFINAE overload will accept scoped enum symbols and cast them
+ * to the underlying type */
+template<class Enum>
+typename std::enable_if<is_scoped_enum<Enum>::value, size_t>::type
+bm2str
+(
+ Enum bits,
+ char *str=nullptr,
+ size_t sz=0,
+ EnumOffsetType offst=EOFFS_PFX
+)
+{
+ using I = typename std::underlying_type<Enum>::type;
+ return bm2str<Enum>(static_cast<I>(bits), str, sz, offst);
+}
+
+
+//-----------------------------------------------------------------------------
+
+namespace detail {
+
+#ifdef __clang__
+# pragma clang diagnostic push
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# if __GNUC__ >= 6
+# pragma GCC diagnostic ignored "-Wnull-dereference"
+# endif
+#endif
+
+template<class Enum>
+typename std::underlying_type<Enum>::type str2bm_read_one(const char *str, size_t sz, bool alnum)
+{
+ using I = typename std::underlying_type<Enum>::type;
+ auto pairs = esyms<Enum>();
+ if(alnum)
+ {
+ auto *p = pairs.find(str, sz);
+ C4_CHECK_MSG(p != nullptr, "no valid enum pair name for '%.*s'", (int)sz, str);
+ return static_cast<I>(p->value);
+ }
+ I tmp;
+ size_t len = uncat(csubstr(str, sz), tmp);
+ C4_CHECK_MSG(len != csubstr::npos, "could not read string as an integral type: '%.*s'", (int)sz, str);
+ return tmp;
+}
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+} // namespace detail
+
+/** convert a string to a bitmask */
+template<class Enum>
+typename std::underlying_type<Enum>::type str2bm(const char *str, size_t sz)
+{
+ using I = typename std::underlying_type<Enum>::type;
+
+ I val = 0;
+ bool started = false;
+ bool alnum = false, num = false;
+ const char *f = nullptr, *pc = str;
+ for( ; pc < str+sz; ++pc)
+ {
+ const char c = *pc;
+ if((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_')
+ {
+ C4_CHECK(( ! num) || ((pc - f) == 1 && (c == 'x' || c == 'X'))); // accept hexadecimal numbers
+ if( ! started)
+ {
+ f = pc;
+ alnum = started = true;
+ }
+ }
+ else if(c >= '0' && c <= '9')
+ {
+ C4_CHECK( ! alnum);
+ if(!started)
+ {
+ f = pc;
+ num = started = true;
+ }
+ }
+ else if(c == ':' || c == ' ')
+ {
+ // skip this char
+ }
+ else if(c == '|' || c == '\0')
+ {
+ C4_ASSERT(num != alnum);
+ C4_ASSERT(pc >= f);
+ val |= detail::str2bm_read_one<Enum>(f, static_cast<size_t>(pc-f), alnum);
+ started = num = alnum = false;
+ if(c == '\0')
+ {
+ return val;
+ }
+ }
+ else
+ {
+ C4_ERROR("bad character '%c' in bitmask string", c);
+ }
+ }
+
+ if(f)
+ {
+ C4_ASSERT(num != alnum);
+ C4_ASSERT(pc >= f);
+ val |= detail::str2bm_read_one<Enum>(f, static_cast<size_t>(pc-f), alnum);
+ }
+
+ return val;
+}
+
+/** convert a string to a bitmask */
+template<class Enum>
+typename std::underlying_type<Enum>::type str2bm(const char *str)
+{
+ return str2bm<Enum>(str, strlen(str));
+}
+
+} // namespace c4
+
+#ifdef _MSC_VER
+# pragma warning(pop)
+#elif defined(__clang__)
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#endif // _C4_BITMASK_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/bitmask.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/span.hpp
+// https://github.com/biojppm/c4core/src/c4/span.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_SPAN_HPP_
+#define _C4_SPAN_HPP_
+
+/** @file span.hpp Provides span classes. */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/config.hpp
+//#include "c4/config.hpp"
+#if !defined(C4_CONFIG_HPP_) && !defined(_C4_CONFIG_HPP_)
+#error "amalgamate: file c4/config.hpp must have been included at this point"
+#endif /* C4_CONFIG_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/szconv.hpp
+//#include "c4/szconv.hpp"
+#if !defined(C4_SZCONV_HPP_) && !defined(_C4_SZCONV_HPP_)
+#error "amalgamate: file c4/szconv.hpp must have been included at this point"
+#endif /* C4_SZCONV_HPP_ */
+
+
+//included above:
+//#include <algorithm>
+
+namespace c4 {
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** a crtp base for implementing span classes
+ *
+ * A span is a non-owning range of elements contiguously stored in memory.
+ * Unlike STL's array_view, the span allows write-access to its members.
+ *
+ * To obtain subspans from a span, the following const member functions
+ * are available:
+ * - subspan(first, num)
+ * - range(first, last)
+ * - first(num)
+ * - last(num)
+ *
+ * A span can also be resized via the following non-const member functions:
+ * - resize(sz)
+ * - ltrim(num)
+ * - rtrim(num)
+ *
+ * @see span
+ * @see cspan
+ * @see spanrs
+ * @see cspanrs
+ * @see spanrsl
+ * @see cspanrsl
+ */
+template<class T, class I, class SpanImpl>
+class span_crtp
+{
+// some utility defines, undefined at the end of this class
+#define _c4this ((SpanImpl *)this)
+#define _c4cthis ((SpanImpl const*)this)
+#define _c4ptr ((SpanImpl *)this)->m_ptr
+#define _c4cptr ((SpanImpl const*)this)->m_ptr
+#define _c4sz ((SpanImpl *)this)->m_size
+#define _c4csz ((SpanImpl const*)this)->m_size
+
+public:
+
+ _c4_DEFINE_ARRAY_TYPES(T, I);
+
+public:
+
+ C4_ALWAYS_INLINE constexpr I value_size() const noexcept { return sizeof(T); }
+ C4_ALWAYS_INLINE constexpr I elm_size () const noexcept { return sizeof(T); }
+ C4_ALWAYS_INLINE constexpr I type_size () const noexcept { return sizeof(T); }
+ C4_ALWAYS_INLINE I byte_size () const noexcept { return _c4csz*sizeof(T); }
+
+ C4_ALWAYS_INLINE bool empty() const noexcept { return _c4csz == 0; }
+ C4_ALWAYS_INLINE I size() const noexcept { return _c4csz; }
+ //C4_ALWAYS_INLINE I capacity() const noexcept { return _c4sz; } // this must be defined by impl classes
+
+ C4_ALWAYS_INLINE void clear() noexcept { _c4sz = 0; }
+
+ C4_ALWAYS_INLINE T * data() noexcept { return _c4ptr; }
+ C4_ALWAYS_INLINE T const* data() const noexcept { return _c4cptr; }
+
+ C4_ALWAYS_INLINE iterator begin() noexcept { return _c4ptr; }
+ C4_ALWAYS_INLINE const_iterator begin() const noexcept { return _c4cptr; }
+ C4_ALWAYS_INLINE const_iterator cbegin() const noexcept { return _c4cptr; }
+
+ C4_ALWAYS_INLINE iterator end() noexcept { return _c4ptr + _c4sz; }
+ C4_ALWAYS_INLINE const_iterator end() const noexcept { return _c4cptr + _c4csz; }
+ C4_ALWAYS_INLINE const_iterator cend() const noexcept { return _c4cptr + _c4csz; }
+
+ C4_ALWAYS_INLINE reverse_iterator rbegin() noexcept { return reverse_iterator(_c4ptr + _c4sz); }
+ C4_ALWAYS_INLINE const_reverse_iterator rbegin() const noexcept { return reverse_iterator(_c4cptr + _c4sz); }
+ C4_ALWAYS_INLINE const_reverse_iterator crbegin() const noexcept { return reverse_iterator(_c4cptr + _c4sz); }
+
+ C4_ALWAYS_INLINE reverse_iterator rend() noexcept { return const_reverse_iterator(_c4ptr); }
+ C4_ALWAYS_INLINE const_reverse_iterator rend() const noexcept { return const_reverse_iterator(_c4cptr); }
+ C4_ALWAYS_INLINE const_reverse_iterator crend() const noexcept { return const_reverse_iterator(_c4cptr); }
+
+ C4_ALWAYS_INLINE T & front() C4_NOEXCEPT_X { C4_XASSERT(!empty()); return _c4ptr [0]; }
+ C4_ALWAYS_INLINE T const& front() const C4_NOEXCEPT_X { C4_XASSERT(!empty()); return _c4cptr[0]; }
+
+ C4_ALWAYS_INLINE T & back() C4_NOEXCEPT_X { C4_XASSERT(!empty()); return _c4ptr [_c4sz - 1]; }
+ C4_ALWAYS_INLINE T const& back() const C4_NOEXCEPT_X { C4_XASSERT(!empty()); return _c4cptr[_c4csz - 1]; }
+
+ C4_ALWAYS_INLINE T & operator[] (I i) C4_NOEXCEPT_X { C4_XASSERT(i >= 0 && i < _c4sz ); return _c4ptr [i]; }
+ C4_ALWAYS_INLINE T const& operator[] (I i) const C4_NOEXCEPT_X { C4_XASSERT(i >= 0 && i < _c4csz); return _c4cptr[i]; }
+
+ C4_ALWAYS_INLINE SpanImpl subspan(I first, I num) const C4_NOEXCEPT_X
+ {
+ C4_XASSERT((first >= 0 && first < _c4csz) || (first == _c4csz && num == 0));
+ C4_XASSERT((first + num >= 0) && (first + num <= _c4csz));
+ return _c4cthis->_select(_c4cptr + first, num);
+ }
+ C4_ALWAYS_INLINE SpanImpl subspan(I first) const C4_NOEXCEPT_X ///< goes up until the end of the span
+ {
+ C4_XASSERT(first >= 0 && first <= _c4csz);
+ return _c4cthis->_select(_c4cptr + first, _c4csz - first);
+ }
+
+ C4_ALWAYS_INLINE SpanImpl range(I first, I last) const C4_NOEXCEPT_X ///< last element is NOT included
+ {
+ C4_XASSERT(((first >= 0) && (first < _c4csz)) || (first == _c4csz && first == last));
+ C4_XASSERT((last >= 0) && (last <= _c4csz));
+ C4_XASSERT(last >= first);
+ return _c4cthis->_select(_c4cptr + first, last - first);
+ }
+ C4_ALWAYS_INLINE SpanImpl range(I first) const C4_NOEXCEPT_X ///< goes up until the end of the span
+ {
+ C4_XASSERT(((first >= 0) && (first <= _c4csz)));
+ return _c4cthis->_select(_c4cptr + first, _c4csz - first);
+ }
+
+ C4_ALWAYS_INLINE SpanImpl first(I num) const C4_NOEXCEPT_X ///< get the first num elements, starting at 0
+ {
+ C4_XASSERT((num >= 0) && (num <= _c4csz));
+ return _c4cthis->_select(_c4cptr, num);
+ }
+ C4_ALWAYS_INLINE SpanImpl last(I num) const C4_NOEXCEPT_X ///< get the last num elements, starting at size()-num
+ {
+ C4_XASSERT((num >= 0) && (num <= _c4csz));
+ return _c4cthis->_select(_c4cptr + _c4csz - num, num);
+ }
+
+ bool is_subspan(span_crtp const& ss) const noexcept
+ {
+ if(_c4cptr == nullptr) return false;
+ auto *b = begin(), *e = end();
+ auto *ssb = ss.begin(), *sse = ss.end();
+ if(ssb >= b && sse <= e)
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ /** COMPLement Left: return the complement to the left of the beginning of the given subspan.
+ * If ss does not begin inside this, returns an empty substring. */
+ SpanImpl compll(span_crtp const& ss) const C4_NOEXCEPT_X
+ {
+ auto ssb = ss.begin();
+ auto b = begin();
+ auto e = end();
+ if(ssb >= b && ssb <= e)
+ {
+ return subspan(0, static_cast<size_t>(ssb - b));
+ }
+ else
+ {
+ return subspan(0, 0);
+ }
+ }
+
+ /** COMPLement Right: return the complement to the right of the end of the given subspan.
+ * If ss does not end inside this, returns an empty substring. */
+ SpanImpl complr(span_crtp const& ss) const C4_NOEXCEPT_X
+ {
+ auto sse = ss.end();
+ auto b = begin();
+ auto e = end();
+ if(sse >= b && sse <= e)
+ {
+ return subspan(static_cast<size_t>(sse - b), static_cast<size_t>(e - sse));
+ }
+ else
+ {
+ return subspan(0, 0);
+ }
+ }
+
+ C4_ALWAYS_INLINE bool same_span(span_crtp const& that) const noexcept
+ {
+ return size() == that.size() && data() == that.data();
+ }
+ template<class I2, class Impl2>
+ C4_ALWAYS_INLINE bool same_span(span_crtp<T, I2, Impl2> const& that) const C4_NOEXCEPT_X
+ {
+ I tsz = szconv<I>(that.size()); // x-asserts that the size does not overflow
+ return size() == tsz && data() == that.data();
+ }
+
+#undef _c4this
+#undef _c4cthis
+#undef _c4ptr
+#undef _c4cptr
+#undef _c4sz
+#undef _c4csz
+};
+
+//-----------------------------------------------------------------------------
+template<class T, class Il, class Ir, class _Impll, class _Implr>
+inline constexpr bool operator==
+(
+ span_crtp<T, Il, _Impll> const& l,
+ span_crtp<T, Ir, _Implr> const& r
+)
+{
+#if C4_CPP >= 14
+ return std::equal(l.begin(), l.end(), r.begin(), r.end());
+#else
+ return l.same_span(r) || std::equal(l.begin(), l.end(), r.begin());
+#endif
+}
+
+template<class T, class Il, class Ir, class _Impll, class _Implr>
+inline constexpr bool operator!=
+(
+ span_crtp<T, Il, _Impll> const& l,
+ span_crtp<T, Ir, _Implr> const& r
+)
+{
+ return ! (l == r);
+}
+
+//-----------------------------------------------------------------------------
+template<class T, class Il, class Ir, class _Impll, class _Implr>
+inline constexpr bool operator<
+(
+ span_crtp<T, Il, _Impll> const& l,
+ span_crtp<T, Ir, _Implr> const& r
+)
+{
+ return std::lexicographical_compare(l.begin(), l.end(), r.begin(), r.end());
+}
+
+template<class T, class Il, class Ir, class _Impll, class _Implr>
+inline constexpr bool operator<=
+(
+ span_crtp<T, Il, _Impll> const& l,
+ span_crtp<T, Ir, _Implr> const& r
+)
+{
+ return ! (l > r);
+}
+
+//-----------------------------------------------------------------------------
+template<class T, class Il, class Ir, class _Impll, class _Implr>
+inline constexpr bool operator>
+(
+ span_crtp<T, Il, _Impll> const& l,
+ span_crtp<T, Ir, _Implr> const& r
+)
+{
+ return r < l;
+}
+
+//-----------------------------------------------------------------------------
+template<class T, class Il, class Ir, class _Impll, class _Implr>
+inline constexpr bool operator>=
+(
+ span_crtp<T, Il, _Impll> const& l,
+ span_crtp<T, Ir, _Implr> const& r
+)
+{
+ return ! (l < r);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** A non-owning span of elements contiguously stored in memory. */
+template<class T, class I=C4_SIZE_TYPE>
+class span : public span_crtp<T, I, span<T, I>>
+{
+ friend class span_crtp<T, I, span<T, I>>;
+
+ T * C4_RESTRICT m_ptr;
+ I m_size;
+
+ C4_ALWAYS_INLINE span _select(T *p, I sz) const { return span(p, sz); }
+
+public:
+
+ _c4_DEFINE_ARRAY_TYPES(T, I);
+ using NCT = typename std::remove_const<T>::type; //!< NCT=non const type
+ using CT = typename std::add_const<T>::type; //!< CT=const type
+ using const_type = span<CT, I>;
+
+ /// convert automatically to span of const T
+ operator span<CT, I> () const { span<CT, I> s(m_ptr, m_size); return s; }
+
+public:
+
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 span() noexcept : m_ptr{nullptr}, m_size{0} {}
+
+ span(span const&) = default;
+ span(span &&) = default;
+
+ span& operator= (span const&) = default;
+ span& operator= (span &&) = default;
+
+public:
+
+ /** @name Construction and assignment from same type */
+ /** @{ */
+
+ template<size_t N> C4_ALWAYS_INLINE C4_CONSTEXPR14 span (T (&arr)[N]) noexcept : m_ptr{arr}, m_size{N} {}
+ template<size_t N> C4_ALWAYS_INLINE C4_CONSTEXPR14 void assign(T (&arr)[N]) noexcept { m_ptr = arr; m_size = N; }
+
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 span(T *p, I sz) noexcept : m_ptr{p}, m_size{sz} {}
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 void assign(T *p, I sz) noexcept { m_ptr = p; m_size = sz; }
+
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 span (c4::aggregate_t, std::initializer_list<T> il) noexcept : m_ptr{&*il.begin()}, m_size{il.size()} {}
+ C4_ALWAYS_INLINE C4_CONSTEXPR14 void assign(c4::aggregate_t, std::initializer_list<T> il) noexcept { m_ptr = &*il.begin(); m_size = il.size(); }
+
+ /** @} */
+
+public:
+
+ C4_ALWAYS_INLINE I capacity() const noexcept { return m_size; }
+
+ C4_ALWAYS_INLINE void resize(I sz) C4_NOEXCEPT_A { C4_ASSERT(sz <= m_size); m_size = sz; }
+ C4_ALWAYS_INLINE void rtrim (I n ) C4_NOEXCEPT_A { C4_ASSERT(n >= 0 && n < m_size); m_size -= n; }
+ C4_ALWAYS_INLINE void ltrim (I n ) C4_NOEXCEPT_A { C4_ASSERT(n >= 0 && n < m_size); m_size -= n; m_ptr += n; }
+
+};
+template<class T, class I=C4_SIZE_TYPE> using cspan = span<const T, I>;
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** A non-owning span resizeable up to a capacity. Subselection or resizing
+ * will keep the original provided it starts at begin(). If subselection or
+ * resizing change the pointer, then the original capacity information will
+ * be lost.
+ *
+ * Thus, resizing via resize() and ltrim() and subselecting via first()
+ * or any of subspan() or range() when starting from the beginning will keep
+ * the original capacity. OTOH, using last(), or any of subspan() or range()
+ * with an offset from the start will remove from capacity (shifting the
+ * pointer) by the corresponding offset. If this is undesired, then consider
+ * using spanrsl.
+ *
+ * @see spanrs for a span resizeable on the right
+ * @see spanrsl for a span resizeable on the right and left
+ */
+
+template<class T, class I=C4_SIZE_TYPE>
+class spanrs : public span_crtp<T, I, spanrs<T, I>>
+{
+ friend class span_crtp<T, I, spanrs<T, I>>;
+
+ T * C4_RESTRICT m_ptr;
+ I m_size;
+ I m_capacity;
+
+ C4_ALWAYS_INLINE spanrs _select(T *p, I sz) const noexcept
+ {
+ C4_ASSERT(p >= m_ptr);
+ size_t delta = static_cast<size_t>(p - m_ptr);
+ C4_ASSERT(m_capacity >= delta);
+ return spanrs(p, sz, static_cast<size_t>(m_capacity - delta));
+ }
+
+public:
+
+ _c4_DEFINE_ARRAY_TYPES(T, I);
+ using NCT = typename std::remove_const<T>::type; //!< NCT=non const type
+ using CT = typename std::add_const<T>::type; //!< CT=const type
+ using const_type = spanrs<CT, I>;
+
+ /// convert automatically to span of T
+ C4_ALWAYS_INLINE operator span<T, I > () const noexcept { return span<T, I>(m_ptr, m_size); }
+ /// convert automatically to span of const T
+ //C4_ALWAYS_INLINE operator span<CT, I> () const noexcept { span<CT, I> s(m_ptr, m_size); return s; }
+ /// convert automatically to spanrs of const T
+ C4_ALWAYS_INLINE operator spanrs<CT, I> () const noexcept { spanrs<CT, I> s(m_ptr, m_size, m_capacity); return s; }
+
+public:
+
+ C4_ALWAYS_INLINE spanrs() noexcept : m_ptr{nullptr}, m_size{0}, m_capacity{0} {}
+
+ spanrs(spanrs const&) = default;
+ spanrs(spanrs &&) = default;
+
+ spanrs& operator= (spanrs const&) = default;
+ spanrs& operator= (spanrs &&) = default;
+
+public:
+
+ /** @name Construction and assignment from same type */
+ /** @{ */
+
+ C4_ALWAYS_INLINE spanrs(T *p, I sz) noexcept : m_ptr{p}, m_size{sz}, m_capacity{sz} {}
+ /** @warning will reset the capacity to sz */
+ C4_ALWAYS_INLINE void assign(T *p, I sz) noexcept { m_ptr = p; m_size = sz; m_capacity = sz; }
+
+ C4_ALWAYS_INLINE spanrs(T *p, I sz, I cap) noexcept : m_ptr{p}, m_size{sz}, m_capacity{cap} {}
+ C4_ALWAYS_INLINE void assign(T *p, I sz, I cap) noexcept { m_ptr = p; m_size = sz; m_capacity = cap; }
+
+ template<size_t N> C4_ALWAYS_INLINE spanrs(T (&arr)[N]) noexcept : m_ptr{arr}, m_size{N}, m_capacity{N} {}
+ template<size_t N> C4_ALWAYS_INLINE void assign(T (&arr)[N]) noexcept { m_ptr = arr; m_size = N; m_capacity = N; }
+
+ C4_ALWAYS_INLINE spanrs(c4::aggregate_t, std::initializer_list<T> il) noexcept : m_ptr{il.begin()}, m_size{il.size()}, m_capacity{il.size()} {}
+ C4_ALWAYS_INLINE void assign(c4::aggregate_t, std::initializer_list<T> il) noexcept { m_ptr = il.begin(); m_size = il.size(); m_capacity = il.size(); }
+
+ /** @} */
+
+public:
+
+ C4_ALWAYS_INLINE I capacity() const noexcept { return m_capacity; }
+
+ C4_ALWAYS_INLINE void resize(I sz) C4_NOEXCEPT_A { C4_ASSERT(sz <= m_capacity); m_size = sz; }
+ C4_ALWAYS_INLINE void rtrim (I n ) C4_NOEXCEPT_A { C4_ASSERT(n >= 0 && n < m_size); m_size -= n; }
+ C4_ALWAYS_INLINE void ltrim (I n ) C4_NOEXCEPT_A { C4_ASSERT(n >= 0 && n < m_size); m_size -= n; m_ptr += n; m_capacity -= n; }
+
+};
+template<class T, class I=C4_SIZE_TYPE> using cspanrs = spanrs<const T, I>;
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** A non-owning span which always retains the capacity of the original
+ * range it was taken from (though it may loose its original size).
+ * The resizing methods resize(), ltrim(), rtrim() as well
+ * as the subselection methods subspan(), range(), first() and last() can be
+ * used at will without loosing the original capacity; the full capacity span
+ * can always be recovered by calling original().
+ */
+template<class T, class I=C4_SIZE_TYPE>
+class spanrsl : public span_crtp<T, I, spanrsl<T, I>>
+{
+ friend class span_crtp<T, I, spanrsl<T, I>>;
+
+ T *C4_RESTRICT m_ptr; ///< the current ptr. the original ptr is (m_ptr - m_offset).
+ I m_size; ///< the current size. the original size is unrecoverable.
+ I m_capacity; ///< the current capacity. the original capacity is (m_capacity + m_offset).
+ I m_offset; ///< the offset of the current m_ptr to the start of the original memory block.
+
+ C4_ALWAYS_INLINE spanrsl _select(T *p, I sz) const noexcept
+ {
+ C4_ASSERT(p >= m_ptr);
+ I delta = static_cast<I>(p - m_ptr);
+ C4_ASSERT(m_capacity >= delta);
+ return spanrsl(p, sz, static_cast<I>(m_capacity - delta), m_offset + delta);
+ }
+
+public:
+
+ _c4_DEFINE_ARRAY_TYPES(T, I);
+ using NCT = typename std::remove_const<T>::type; //!< NCT=non const type
+ using CT = typename std::add_const<T>::type; //!< CT=const type
+ using const_type = spanrsl<CT, I>;
+
+ C4_ALWAYS_INLINE operator span<T, I> () const noexcept { return span<T, I>(m_ptr, m_size); }
+ C4_ALWAYS_INLINE operator spanrs<T, I> () const noexcept { return spanrs<T, I>(m_ptr, m_size, m_capacity); }
+ C4_ALWAYS_INLINE operator spanrsl<CT, I> () const noexcept { return spanrsl<CT, I>(m_ptr, m_size, m_capacity, m_offset); }
+
+public:
+
+ C4_ALWAYS_INLINE spanrsl() noexcept : m_ptr{nullptr}, m_size{0}, m_capacity{0}, m_offset{0} {}
+
+ spanrsl(spanrsl const&) = default;
+ spanrsl(spanrsl &&) = default;
+
+ spanrsl& operator= (spanrsl const&) = default;
+ spanrsl& operator= (spanrsl &&) = default;
+
+public:
+
+ C4_ALWAYS_INLINE spanrsl(T *p, I sz) noexcept : m_ptr{p}, m_size{sz}, m_capacity{sz}, m_offset{0} {}
+ C4_ALWAYS_INLINE void assign(T *p, I sz) noexcept { m_ptr = p; m_size = sz; m_capacity = sz; m_offset = 0; }
+
+ C4_ALWAYS_INLINE spanrsl(T *p, I sz, I cap) noexcept : m_ptr{p}, m_size{sz}, m_capacity{cap}, m_offset{0} {}
+ C4_ALWAYS_INLINE void assign(T *p, I sz, I cap) noexcept { m_ptr = p; m_size = sz; m_capacity = cap; m_offset = 0; }
+
+ C4_ALWAYS_INLINE spanrsl(T *p, I sz, I cap, I offs) noexcept : m_ptr{p}, m_size{sz}, m_capacity{cap}, m_offset{offs} {}
+ C4_ALWAYS_INLINE void assign(T *p, I sz, I cap, I offs) noexcept { m_ptr = p; m_size = sz; m_capacity = cap; m_offset = offs; }
+
+ template<size_t N> C4_ALWAYS_INLINE spanrsl(T (&arr)[N]) noexcept : m_ptr{arr}, m_size{N}, m_capacity{N}, m_offset{0} {}
+ template<size_t N> C4_ALWAYS_INLINE void assign(T (&arr)[N]) noexcept { m_ptr = arr; m_size = N; m_capacity = N; m_offset = 0; }
+
+ C4_ALWAYS_INLINE spanrsl(c4::aggregate_t, std::initializer_list<T> il) noexcept : m_ptr{il.begin()}, m_size{il.size()}, m_capacity{il.size()}, m_offset{0} {}
+ C4_ALWAYS_INLINE void assign (c4::aggregate_t, std::initializer_list<T> il) noexcept { m_ptr = il.begin(); m_size = il.size(); m_capacity = il.size(); m_offset = 0; }
+
+public:
+
+ C4_ALWAYS_INLINE I offset() const noexcept { return m_offset; }
+ C4_ALWAYS_INLINE I capacity() const noexcept { return m_capacity; }
+
+ C4_ALWAYS_INLINE void resize(I sz) C4_NOEXCEPT_A { C4_ASSERT(sz <= m_capacity); m_size = sz; }
+ C4_ALWAYS_INLINE void rtrim (I n ) C4_NOEXCEPT_A { C4_ASSERT(n >= 0 && n < m_size); m_size -= n; }
+ C4_ALWAYS_INLINE void ltrim (I n ) C4_NOEXCEPT_A { C4_ASSERT(n >= 0 && n < m_size); m_size -= n; m_ptr += n; m_offset += n; m_capacity -= n; }
+
+ /** recover the original span as an spanrsl */
+ C4_ALWAYS_INLINE spanrsl original() const
+ {
+ return spanrsl(m_ptr - m_offset, m_capacity + m_offset, m_capacity + m_offset, 0);
+ }
+ /** recover the original span as a different span type. Example: spanrs<...> orig = s.original<spanrs>(); */
+ template<template<class, class> class OtherSpanType>
+ C4_ALWAYS_INLINE OtherSpanType<T, I> original()
+ {
+ return OtherSpanType<T, I>(m_ptr - m_offset, m_capacity + m_offset);
+ }
+};
+template<class T, class I=C4_SIZE_TYPE> using cspanrsl = spanrsl<const T, I>;
+
+
+} // namespace c4
+
+
+#endif /* _C4_SPAN_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/span.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/type_name.hpp
+// https://github.com/biojppm/c4core/src/c4/type_name.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_TYPENAME_HPP_
+#define _C4_TYPENAME_HPP_
+
+/** @file type_name.hpp compile-time type name */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/span.hpp
+//#include "c4/span.hpp"
+#if !defined(C4_SPAN_HPP_) && !defined(_C4_SPAN_HPP_)
+#error "amalgamate: file c4/span.hpp must have been included at this point"
+#endif /* C4_SPAN_HPP_ */
+
+
+/// @cond dev
+struct _c4t
+{
+ const char *str;
+ size_t sz;
+ template<size_t N>
+ constexpr _c4t(const char (&s)[N]) : str(s), sz(N-1) {} // take off the \0
+};
+// this is a more abbreviated way of getting the type name
+// (if we used span in the return type, the name would involve
+// templates and would create longer type name strings,
+// as well as larger differences between compilers)
+template<class T>
+C4_CONSTEXPR14 C4_ALWAYS_INLINE
+_c4t _c4tn()
+{
+ auto p = _c4t(C4_PRETTY_FUNC);
+ return p;
+}
+/// @endcond
+
+
+namespace c4 {
+
+/** compile-time type name
+ * @see http://stackoverflow.com/a/20170989/5875572 */
+template<class T>
+C4_CONSTEXPR14 cspan<char> type_name()
+{
+ const _c4t p = _c4tn<T>();
+
+#if (0) // _C4_THIS_IS_A_DEBUG_SCAFFOLD
+ for(size_t index = 0; index < p.sz; ++index)
+ {
+ printf(" %2c", p.str[index]);
+ }
+ printf("\n");
+ for(size_t index = 0; index < p.sz; ++index)
+ {
+ printf(" %2d", (int)index);
+ }
+ printf("\n");
+#endif
+
+#if defined(_MSC_VER)
+# if defined(__clang__) // Visual Studio has the clang toolset
+ // example:
+ // ..........................xxx.
+ // _c4t __cdecl _c4tn() [T = int]
+ enum : size_t { tstart = 26, tend = 1};
+
+# elif defined(C4_MSVC_2015) || defined(C4_MSVC_2017) || defined(C4_MSVC_2019) || defined(C4_MSVC_2022)
+ // Note: subtract 7 at the end because the function terminates with ">(void)" in VS2015+
+ cspan<char>::size_type tstart = 26, tend = 7;
+
+ const char *s = p.str + tstart; // look at the start
+
+ // we're not using strcmp() or memcmp() to spare the #include
+
+ // does it start with 'class '?
+ if(p.sz > 6 && s[0] == 'c' && s[1] == 'l' && s[2] == 'a' && s[3] == 's' && s[4] == 's' && s[5] == ' ')
+ {
+ tstart += 6;
+ }
+ // does it start with 'struct '?
+ else if(p.sz > 7 && s[0] == 's' && s[1] == 't' && s[2] == 'r' && s[3] == 'u' && s[4] == 'c' && s[5] == 't' && s[6] == ' ')
+ {
+ tstart += 7;
+ }
+
+# else
+ C4_NOT_IMPLEMENTED();
+# endif
+
+#elif defined(__ICC)
+ // example:
+ // ........................xxx.
+ // "_c4t _c4tn() [with T = int]"
+ enum : size_t { tstart = 23, tend = 1};
+
+#elif defined(__clang__)
+ // example:
+ // ...................xxx.
+ // "_c4t _c4tn() [T = int]"
+ enum : size_t { tstart = 18, tend = 1};
+
+#elif defined(__GNUC__)
+ #if __GNUC__ >= 7 && C4_CPP >= 14
+ // example:
+ // ..................................xxx.
+ // "constexpr _c4t _c4tn() [with T = int]"
+ enum : size_t { tstart = 33, tend = 1 };
+ #else
+ // example:
+ // ........................xxx.
+ // "_c4t _c4tn() [with T = int]"
+ enum : size_t { tstart = 23, tend = 1 };
+ #endif
+#else
+ C4_NOT_IMPLEMENTED();
+#endif
+
+ cspan<char> o(p.str + tstart, p.sz - tstart - tend);
+
+ return o;
+}
+
+/** compile-time type name
+ * @overload */
+template<class T>
+C4_CONSTEXPR14 C4_ALWAYS_INLINE cspan<char> type_name(T const&)
+{
+ return type_name<T>();
+}
+
+} // namespace c4
+
+#endif //_C4_TYPENAME_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/type_name.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/base64.hpp
+// https://github.com/biojppm/c4core/src/c4/base64.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_BASE64_HPP_
+#define _C4_BASE64_HPP_
+
+/** @file base64.hpp encoding/decoding for base64.
+ * @see https://en.wikipedia.org/wiki/Base64
+ * @see https://www.base64encode.org/
+ * */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/charconv.hpp
+//#include "c4/charconv.hpp"
+#if !defined(C4_CHARCONV_HPP_) && !defined(_C4_CHARCONV_HPP_)
+#error "amalgamate: file c4/charconv.hpp must have been included at this point"
+#endif /* C4_CHARCONV_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/blob.hpp
+//#include "c4/blob.hpp"
+#if !defined(C4_BLOB_HPP_) && !defined(_C4_BLOB_HPP_)
+#error "amalgamate: file c4/blob.hpp must have been included at this point"
+#endif /* C4_BLOB_HPP_ */
+
+
+namespace c4 {
+
+/** check that the given buffer is a valid base64 encoding
+ * @see https://en.wikipedia.org/wiki/Base64 */
+bool base64_valid(csubstr encoded);
+
+/** base64-encode binary data.
+ * @param encoded [out] output buffer for encoded data
+ * @param data [in] the input buffer with the binary data
+ * @return the number of bytes needed to return the output. No writes occur beyond the end of the output buffer.
+ * @see https://en.wikipedia.org/wiki/Base64 */
+size_t base64_encode(substr encoded, cblob data);
+
+/** decode the base64 encoding in the given buffer
+ * @param encoded [in] the encoded base64
+ * @param data [out] the output buffer
+ * @return the number of bytes needed to return the output.. No writes occur beyond the end of the output buffer.
+ * @see https://en.wikipedia.org/wiki/Base64 */
+size_t base64_decode(csubstr encoded, blob data);
+
+
+namespace fmt {
+
+template<typename CharOrConstChar>
+struct base64_wrapper_
+{
+ blob_<CharOrConstChar> data;
+ base64_wrapper_() : data() {}
+ base64_wrapper_(blob_<CharOrConstChar> blob) : data(blob) {}
+};
+using const_base64_wrapper = base64_wrapper_<cbyte>;
+using base64_wrapper = base64_wrapper_<byte>;
+
+
+/** mark a variable to be written in base64 format */
+template<class ...Args>
+C4_ALWAYS_INLINE const_base64_wrapper cbase64(Args const& C4_RESTRICT ...args)
+{
+ return const_base64_wrapper(cblob(args...));
+}
+/** mark a csubstr to be written in base64 format */
+C4_ALWAYS_INLINE const_base64_wrapper cbase64(csubstr s)
+{
+ return const_base64_wrapper(cblob(s.str, s.len));
+}
+/** mark a variable to be written in base64 format */
+template<class ...Args>
+C4_ALWAYS_INLINE const_base64_wrapper base64(Args const& C4_RESTRICT ...args)
+{
+ return const_base64_wrapper(cblob(args...));
+}
+/** mark a csubstr to be written in base64 format */
+C4_ALWAYS_INLINE const_base64_wrapper base64(csubstr s)
+{
+ return const_base64_wrapper(cblob(s.str, s.len));
+}
+
+/** mark a variable to be read in base64 format */
+template<class ...Args>
+C4_ALWAYS_INLINE base64_wrapper base64(Args &... args)
+{
+ return base64_wrapper(blob(args...));
+}
+/** mark a variable to be read in base64 format */
+C4_ALWAYS_INLINE base64_wrapper base64(substr s)
+{
+ return base64_wrapper(blob(s.str, s.len));
+}
+
+} // namespace fmt
+
+
+/** write a variable in base64 format */
+inline size_t to_chars(substr buf, fmt::const_base64_wrapper b)
+{
+ return base64_encode(buf, b.data);
+}
+
+/** read a variable in base64 format */
+inline size_t from_chars(csubstr buf, fmt::base64_wrapper *b)
+{
+ return base64_decode(buf, b->data);
+}
+
+} // namespace c4
+
+#endif /* _C4_BASE64_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/base64.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/std/string.hpp
+// https://github.com/biojppm/c4core/src/c4/std/string.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_STD_STRING_HPP_
+#define _C4_STD_STRING_HPP_
+
+/** @file string.hpp */
+
+#ifndef C4CORE_SINGLE_HEADER
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/substr.hpp
+//#include "c4/substr.hpp"
+#if !defined(C4_SUBSTR_HPP_) && !defined(_C4_SUBSTR_HPP_)
+#error "amalgamate: file c4/substr.hpp must have been included at this point"
+#endif /* C4_SUBSTR_HPP_ */
+
+#endif
+
+//included above:
+//#include <string>
+
+namespace c4 {
+
+//-----------------------------------------------------------------------------
+
+/** get a writeable view to an existing std::string */
+inline c4::substr to_substr(std::string &s)
+{
+ char* data = ! s.empty() ? &s[0] : nullptr;
+ return c4::substr(data, s.size());
+}
+
+/** get a readonly view to an existing std::string */
+inline c4::csubstr to_csubstr(std::string const& s)
+{
+ const char* data = ! s.empty() ? &s[0] : nullptr;
+ return c4::csubstr(data, s.size());
+}
+
+//-----------------------------------------------------------------------------
+
+C4_ALWAYS_INLINE bool operator== (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) == 0; }
+C4_ALWAYS_INLINE bool operator!= (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) != 0; }
+C4_ALWAYS_INLINE bool operator>= (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) >= 0; }
+C4_ALWAYS_INLINE bool operator> (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) > 0; }
+C4_ALWAYS_INLINE bool operator<= (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) <= 0; }
+C4_ALWAYS_INLINE bool operator< (c4::csubstr ss, std::string const& s) { return ss.compare(to_csubstr(s)) < 0; }
+
+C4_ALWAYS_INLINE bool operator== (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) == 0; }
+C4_ALWAYS_INLINE bool operator!= (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) != 0; }
+C4_ALWAYS_INLINE bool operator>= (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) <= 0; }
+C4_ALWAYS_INLINE bool operator> (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) < 0; }
+C4_ALWAYS_INLINE bool operator<= (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) >= 0; }
+C4_ALWAYS_INLINE bool operator< (std::string const& s, c4::csubstr ss) { return ss.compare(to_csubstr(s)) > 0; }
+
+//-----------------------------------------------------------------------------
+
+/** copy an std::string to a writeable string view */
+inline size_t to_chars(c4::substr buf, std::string const& s)
+{
+ C4_ASSERT(!buf.overlaps(to_csubstr(s)));
+ size_t len = buf.len < s.size() ? buf.len : s.size();
+ memcpy(buf.str, s.data(), len);
+ return s.size(); // return the number of needed chars
+}
+
+/** copy a string view to an existing std::string */
+inline bool from_chars(c4::csubstr buf, std::string * s)
+{
+ s->resize(buf.len);
+ C4_ASSERT(!buf.overlaps(to_csubstr(*s)));
+ memcpy(&(*s)[0], buf.str, buf.len);
+ return true;
+}
+
+} // namespace c4
+
+#endif // _C4_STD_STRING_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/std/string.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/std/vector.hpp
+// https://github.com/biojppm/c4core/src/c4/std/vector.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_STD_VECTOR_HPP_
+#define _C4_STD_VECTOR_HPP_
+
+/** @file vector.hpp provides conversion and comparison facilities
+ * from/between std::vector<char> to c4::substr and c4::csubstr.
+ * @todo add to_span() and friends
+ */
+
+#ifndef C4CORE_SINGLE_HEADER
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/substr.hpp
+//#include "c4/substr.hpp"
+#if !defined(C4_SUBSTR_HPP_) && !defined(_C4_SUBSTR_HPP_)
+#error "amalgamate: file c4/substr.hpp must have been included at this point"
+#endif /* C4_SUBSTR_HPP_ */
+
+#endif
+
+#include <vector>
+
+namespace c4 {
+
+//-----------------------------------------------------------------------------
+
+/** get a substr (writeable string view) of an existing std::vector<char> */
+template<class Alloc>
+c4::substr to_substr(std::vector<char, Alloc> &vec)
+{
+ char *data = vec.empty() ? nullptr : vec.data(); // data() may or may not return a null pointer.
+ return c4::substr(data, vec.size());
+}
+
+/** get a csubstr (read-only string) view of an existing std::vector<char> */
+template<class Alloc>
+c4::csubstr to_csubstr(std::vector<char, Alloc> const& vec)
+{
+ const char *data = vec.empty() ? nullptr : vec.data(); // data() may or may not return a null pointer.
+ return c4::csubstr(data, vec.size());
+}
+
+//-----------------------------------------------------------------------------
+// comparisons between substrings and std::vector<char>
+
+template<class Alloc> C4_ALWAYS_INLINE bool operator!= (c4::csubstr ss, std::vector<char, Alloc> const& s) { return ss != to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator== (c4::csubstr ss, std::vector<char, Alloc> const& s) { return ss == to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator>= (c4::csubstr ss, std::vector<char, Alloc> const& s) { return ss >= to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator> (c4::csubstr ss, std::vector<char, Alloc> const& s) { return ss > to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator<= (c4::csubstr ss, std::vector<char, Alloc> const& s) { return ss <= to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator< (c4::csubstr ss, std::vector<char, Alloc> const& s) { return ss < to_csubstr(s); }
+
+template<class Alloc> C4_ALWAYS_INLINE bool operator!= (std::vector<char, Alloc> const& s, c4::csubstr ss) { return ss != to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator== (std::vector<char, Alloc> const& s, c4::csubstr ss) { return ss == to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator>= (std::vector<char, Alloc> const& s, c4::csubstr ss) { return ss <= to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator> (std::vector<char, Alloc> const& s, c4::csubstr ss) { return ss < to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator<= (std::vector<char, Alloc> const& s, c4::csubstr ss) { return ss >= to_csubstr(s); }
+template<class Alloc> C4_ALWAYS_INLINE bool operator< (std::vector<char, Alloc> const& s, c4::csubstr ss) { return ss > to_csubstr(s); }
+
+//-----------------------------------------------------------------------------
+
+/** copy a std::vector<char> to a writeable string view */
+template<class Alloc>
+inline size_t to_chars(c4::substr buf, std::vector<char, Alloc> const& s)
+{
+ C4_ASSERT(!buf.overlaps(to_csubstr(s)));
+ size_t len = buf.len < s.size() ? buf.len : s.size();
+ memcpy(buf.str, s.data(), len);
+ return s.size(); // return the number of needed chars
+}
+
+/** copy a string view to an existing std::vector<char> */
+template<class Alloc>
+inline bool from_chars(c4::csubstr buf, std::vector<char, Alloc> * s)
+{
+ s->resize(buf.len);
+ C4_ASSERT(!buf.overlaps(to_csubstr(*s)));
+ memcpy(&(*s)[0], buf.str, buf.len);
+ return true;
+}
+
+} // namespace c4
+
+#endif // _C4_STD_VECTOR_HPP_
+
+
+// (end https://github.com/biojppm/c4core/src/c4/std/vector.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/std/tuple.hpp
+// https://github.com/biojppm/c4core/src/c4/std/tuple.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_STD_TUPLE_HPP_
+#define _C4_STD_TUPLE_HPP_
+
+/** @file tuple.hpp */
+
+#ifndef C4CORE_SINGLE_HEADER
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/format.hpp
+//#include "c4/format.hpp"
+#if !defined(C4_FORMAT_HPP_) && !defined(_C4_FORMAT_HPP_)
+#error "amalgamate: file c4/format.hpp must have been included at this point"
+#endif /* C4_FORMAT_HPP_ */
+
+#endif
+
+#include <tuple>
+
+/** this is a work in progress */
+#undef C4_TUPLE_TO_CHARS
+
+namespace c4 {
+
+#ifdef C4_TUPLE_TO_CHARS
+namespace detail {
+
+template< size_t Curr, class... Types >
+struct tuple_helper
+{
+ static size_t do_cat(substr buf, std::tuple< Types... > const& tp)
+ {
+ size_t num = to_chars(buf, std::get<Curr>(tp));
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num += tuple_helper< Curr+1, Types... >::do_cat(buf, tp);
+ return num;
+ }
+
+ static size_t do_uncat(csubstr buf, std::tuple< Types... > & tp)
+ {
+ size_t num = from_str_trim(buf, &std::get<Curr>(tp));
+ if(num == csubstr::npos) return csubstr::npos;
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num += tuple_helper< Curr+1, Types... >::do_uncat(buf, tp);
+ return num;
+ }
+
+ template< class Sep >
+ static size_t do_catsep_more(substr buf, Sep const& sep, std::tuple< Types... > const& tp)
+ {
+ size_t ret = to_chars(buf, sep), num = ret;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = to_chars(buf, std::get<Curr>(tp));
+ num += ret;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = tuple_helper< Curr+1, Types... >::do_catsep_more(buf, sep, tp);
+ num += ret;
+ return num;
+ }
+
+ template< class Sep >
+ static size_t do_uncatsep_more(csubstr buf, Sep & sep, std::tuple< Types... > & tp)
+ {
+ size_t ret = from_str_trim(buf, &sep), num = ret;
+ if(ret == csubstr::npos) return csubstr::npos;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = from_str_trim(buf, &std::get<Curr>(tp));
+ if(ret == csubstr::npos) return csubstr::npos;
+ num += ret;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = tuple_helper< Curr+1, Types... >::do_uncatsep_more(buf, sep, tp);
+ if(ret == csubstr::npos) return csubstr::npos;
+ num += ret;
+ return num;
+ }
+
+ static size_t do_format(substr buf, csubstr fmt, std::tuple< Types... > const& tp)
+ {
+ auto pos = fmt.find("{}");
+ if(pos != csubstr::npos)
+ {
+ size_t num = to_chars(buf, fmt.sub(0, pos));
+ size_t out = num;
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num = to_chars(buf, std::get<Curr>(tp));
+ out += num;
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num = tuple_helper< Curr+1, Types... >::do_format(buf, fmt.sub(pos + 2), tp);
+ out += num;
+ return out;
+ }
+ else
+ {
+ return format(buf, fmt);
+ }
+ }
+
+ static size_t do_unformat(csubstr buf, csubstr fmt, std::tuple< Types... > & tp)
+ {
+ auto pos = fmt.find("{}");
+ if(pos != csubstr::npos)
+ {
+ size_t num = pos;
+ size_t out = num;
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num = from_str_trim(buf, &std::get<Curr>(tp));
+ out += num;
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num = tuple_helper< Curr+1, Types... >::do_unformat(buf, fmt.sub(pos + 2), tp);
+ out += num;
+ return out;
+ }
+ else
+ {
+ return tuple_helper< sizeof...(Types), Types... >::do_unformat(buf, fmt, tp);
+ }
+ }
+
+};
+
+/** @todo VS compilation fails for this class */
+template< class... Types >
+struct tuple_helper< sizeof...(Types), Types... >
+{
+ static size_t do_cat(substr /*buf*/, std::tuple<Types...> const& /*tp*/) { return 0; }
+ static size_t do_uncat(csubstr /*buf*/, std::tuple<Types...> & /*tp*/) { return 0; }
+
+ template< class Sep > static size_t do_catsep_more(substr /*buf*/, Sep const& /*sep*/, std::tuple<Types...> const& /*tp*/) { return 0; }
+ template< class Sep > static size_t do_uncatsep_more(csubstr /*buf*/, Sep & /*sep*/, std::tuple<Types...> & /*tp*/) { return 0; }
+
+ static size_t do_format(substr buf, csubstr fmt, std::tuple<Types...> const& /*tp*/)
+ {
+ return to_chars(buf, fmt);
+ }
+
+ static size_t do_unformat(csubstr buf, csubstr fmt, std::tuple<Types...> const& /*tp*/)
+ {
+ return 0;
+ }
+};
+
+} // namespace detail
+
+template< class... Types >
+inline size_t cat(substr buf, std::tuple< Types... > const& tp)
+{
+ return detail::tuple_helper< 0, Types... >::do_cat(buf, tp);
+}
+
+template< class... Types >
+inline size_t uncat(csubstr buf, std::tuple< Types... > & tp)
+{
+ return detail::tuple_helper< 0, Types... >::do_uncat(buf, tp);
+}
+
+template< class Sep, class... Types >
+inline size_t catsep(substr buf, Sep const& sep, std::tuple< Types... > const& tp)
+{
+ size_t num = to_chars(buf, std::cref(std::get<0>(tp)));
+ buf = buf.len >= num ? buf.sub(num) : substr{};
+ num += detail::tuple_helper< 1, Types... >::do_catsep_more(buf, sep, tp);
+ return num;
+}
+
+template< class Sep, class... Types >
+inline size_t uncatsep(csubstr buf, Sep & sep, std::tuple< Types... > & tp)
+{
+ size_t ret = from_str_trim(buf, &std::get<0>(tp)), num = ret;
+ if(ret == csubstr::npos) return csubstr::npos;
+ buf = buf.len >= ret ? buf.sub(ret) : substr{};
+ ret = detail::tuple_helper< 1, Types... >::do_uncatsep_more(buf, sep, tp);
+ if(ret == csubstr::npos) return csubstr::npos;
+ num += ret;
+ return num;
+}
+
+template< class... Types >
+inline size_t format(substr buf, csubstr fmt, std::tuple< Types... > const& tp)
+{
+ return detail::tuple_helper< 0, Types... >::do_format(buf, fmt, tp);
+}
+
+template< class... Types >
+inline size_t unformat(csubstr buf, csubstr fmt, std::tuple< Types... > & tp)
+{
+ return detail::tuple_helper< 0, Types... >::do_unformat(buf, fmt, tp);
+}
+#endif // C4_TUPLE_TO_CHARS
+
+} // namespace c4
+
+#endif /* _C4_STD_TUPLE_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/std/tuple.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/ext/rng/rng.hpp
+// https://github.com/biojppm/c4core/src/c4/ext/rng/rng.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+/* Copyright (c) 2018 Arvid Gerstmann.
+ *
+ * https://arvid.io/2018/07/02/better-cxx-prng/
+ *
+ * This code is licensed under MIT license. */
+#ifndef AG_RANDOM_H
+#define AG_RANDOM_H
+
+//included above:
+//#include <stdint.h>
+#include <random>
+
+
+namespace c4 {
+namespace rng {
+
+
+class splitmix
+{
+public:
+ using result_type = uint32_t;
+ static constexpr result_type (min)() { return 0; }
+ static constexpr result_type (max)() { return UINT32_MAX; }
+ friend bool operator==(splitmix const &, splitmix const &);
+ friend bool operator!=(splitmix const &, splitmix const &);
+
+ splitmix() : m_seed(1) {}
+ explicit splitmix(std::random_device &rd)
+ {
+ seed(rd);
+ }
+
+ void seed(std::random_device &rd)
+ {
+ m_seed = uint64_t(rd()) << 31 | uint64_t(rd());
+ }
+
+ result_type operator()()
+ {
+ uint64_t z = (m_seed += UINT64_C(0x9E3779B97F4A7C15));
+ z = (z ^ (z >> 30)) * UINT64_C(0xBF58476D1CE4E5B9);
+ z = (z ^ (z >> 27)) * UINT64_C(0x94D049BB133111EB);
+ return result_type((z ^ (z >> 31)) >> 31);
+ }
+
+ void discard(unsigned long long n)
+ {
+ for (unsigned long long i = 0; i < n; ++i)
+ operator()();
+ }
+
+private:
+ uint64_t m_seed;
+};
+
+inline bool operator==(splitmix const &lhs, splitmix const &rhs)
+{
+ return lhs.m_seed == rhs.m_seed;
+}
+inline bool operator!=(splitmix const &lhs, splitmix const &rhs)
+{
+ return lhs.m_seed != rhs.m_seed;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+class xorshift
+{
+public:
+ using result_type = uint32_t;
+ static constexpr result_type (min)() { return 0; }
+ static constexpr result_type (max)() { return UINT32_MAX; }
+ friend bool operator==(xorshift const &, xorshift const &);
+ friend bool operator!=(xorshift const &, xorshift const &);
+
+ xorshift() : m_seed(0xc1f651c67c62c6e0ull) {}
+ explicit xorshift(std::random_device &rd)
+ {
+ seed(rd);
+ }
+
+ void seed(std::random_device &rd)
+ {
+ m_seed = uint64_t(rd()) << 31 | uint64_t(rd());
+ }
+
+ result_type operator()()
+ {
+ uint64_t result = m_seed * 0xd989bcacc137dcd5ull;
+ m_seed ^= m_seed >> 11;
+ m_seed ^= m_seed << 31;
+ m_seed ^= m_seed >> 18;
+ return uint32_t(result >> 32ull);
+ }
+
+ void discard(unsigned long long n)
+ {
+ for (unsigned long long i = 0; i < n; ++i)
+ operator()();
+ }
+
+private:
+ uint64_t m_seed;
+};
+
+inline bool operator==(xorshift const &lhs, xorshift const &rhs)
+{
+ return lhs.m_seed == rhs.m_seed;
+}
+inline bool operator!=(xorshift const &lhs, xorshift const &rhs)
+{
+ return lhs.m_seed != rhs.m_seed;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+class pcg
+{
+public:
+ using result_type = uint32_t;
+ static constexpr result_type (min)() { return 0; }
+ static constexpr result_type (max)() { return UINT32_MAX; }
+ friend bool operator==(pcg const &, pcg const &);
+ friend bool operator!=(pcg const &, pcg const &);
+
+ pcg()
+ : m_state(0x853c49e6748fea9bULL)
+ , m_inc(0xda3e39cb94b95bdbULL)
+ {}
+ explicit pcg(std::random_device &rd)
+ {
+ seed(rd);
+ }
+
+ void seed(std::random_device &rd)
+ {
+ uint64_t s0 = uint64_t(rd()) << 31 | uint64_t(rd());
+ uint64_t s1 = uint64_t(rd()) << 31 | uint64_t(rd());
+
+ m_state = 0;
+ m_inc = (s1 << 1) | 1;
+ (void)operator()();
+ m_state += s0;
+ (void)operator()();
+ }
+
+ result_type operator()()
+ {
+ uint64_t oldstate = m_state;
+ m_state = oldstate * 6364136223846793005ULL + m_inc;
+ uint32_t xorshifted = uint32_t(((oldstate >> 18u) ^ oldstate) >> 27u);
+ //int rot = oldstate >> 59u; // the original. error?
+ int64_t rot = (int64_t)oldstate >> 59u; // error?
+ return (xorshifted >> rot) | (xorshifted << ((uint64_t)(-rot) & 31));
+ }
+
+ void discard(unsigned long long n)
+ {
+ for (unsigned long long i = 0; i < n; ++i)
+ operator()();
+ }
+
+private:
+ uint64_t m_state;
+ uint64_t m_inc;
+};
+
+inline bool operator==(pcg const &lhs, pcg const &rhs)
+{
+ return lhs.m_state == rhs.m_state
+ && lhs.m_inc == rhs.m_inc;
+}
+inline bool operator!=(pcg const &lhs, pcg const &rhs)
+{
+ return lhs.m_state != rhs.m_state
+ || lhs.m_inc != rhs.m_inc;
+}
+
+} // namespace rng
+} // namespace c4
+
+#endif /* AG_RANDOM_H */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/ext/rng/rng.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/ext/sg14/inplace_function.h
+// https://github.com/biojppm/c4core/src/c4/ext/sg14/inplace_function.h
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+/*
+ * Boost Software License - Version 1.0 - August 17th, 2003
+ *
+ * Permission is hereby granted, free of charge, to any person or organization
+ * obtaining a copy of the software and accompanying documentation covered by
+ * this license (the "Software") to use, reproduce, display, distribute,
+ * execute, and transmit the Software, and to prepare derivative works of the
+ * Software, and to permit third-parties to whom the Software is furnished to
+ * do so, all subject to the following:
+ *
+ * The copyright notices in the Software and this entire statement, including
+ * the above license grant, this restriction and the following disclaimer,
+ * must be included in all copies of the Software, in whole or in part, and
+ * all derivative works of the Software, unless such copies or derivative
+ * works are solely in the form of machine-executable object code generated by
+ * a source language processor.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+ * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+ * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+#ifndef _C4_EXT_SG14_INPLACE_FUNCTION_H_
+#define _C4_EXT_SG14_INPLACE_FUNCTION_H_
+
+//included above:
+//#include <type_traits>
+//included above:
+//#include <utility>
+#include <functional>
+
+namespace stdext {
+
+namespace inplace_function_detail {
+
+static constexpr size_t InplaceFunctionDefaultCapacity = 32;
+
+#if defined(__GLIBCXX__) // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=61458
+template<size_t Cap>
+union aligned_storage_helper {
+ struct double1 { double a; };
+ struct double4 { double a[4]; };
+ template<class T> using maybe = typename std::conditional<(Cap >= sizeof(T)), T, char>::type;
+ char real_data[Cap];
+ maybe<int> a;
+ maybe<long> b;
+ maybe<long long> c;
+ maybe<void*> d;
+ maybe<void(*)()> e;
+ maybe<double1> f;
+ maybe<double4> g;
+ maybe<long double> h;
+};
+
+template<size_t Cap, size_t Align = std::alignment_of<aligned_storage_helper<Cap>>::value>
+struct aligned_storage {
+ using type = typename std::aligned_storage<Cap, Align>::type;
+};
+#else
+using std::aligned_storage;
+#endif
+
+template<typename T> struct wrapper
+{
+ using type = T;
+};
+
+template<typename R, typename... Args> struct vtable
+{
+ using storage_ptr_t = void*;
+
+ using invoke_ptr_t = R(*)(storage_ptr_t, Args&&...);
+ using process_ptr_t = void(*)(storage_ptr_t, storage_ptr_t);
+ using destructor_ptr_t = void(*)(storage_ptr_t);
+
+ const invoke_ptr_t invoke_ptr;
+ const process_ptr_t copy_ptr;
+ const process_ptr_t move_ptr;
+ const destructor_ptr_t destructor_ptr;
+
+ explicit constexpr vtable() noexcept :
+ invoke_ptr{ [](storage_ptr_t, Args&&...) -> R
+ { throw std::bad_function_call(); }
+ },
+ copy_ptr{ [](storage_ptr_t, storage_ptr_t) noexcept -> void {} },
+ move_ptr{ [](storage_ptr_t, storage_ptr_t) noexcept -> void {} },
+ destructor_ptr{ [](storage_ptr_t) noexcept -> void {} }
+ {}
+
+ template<typename C> explicit constexpr vtable(wrapper<C>) noexcept :
+ invoke_ptr{ [](storage_ptr_t storage_ptr, Args&&... args)
+ noexcept(noexcept(std::declval<C>()(args...))) -> R
+ { return (*static_cast<C*>(storage_ptr))(
+ std::forward<Args>(args)...
+ ); }
+ },
+ copy_ptr{ [](storage_ptr_t dst_ptr, storage_ptr_t src_ptr)
+ noexcept(std::is_nothrow_copy_constructible<C>::value) -> void
+ { new (dst_ptr) C{ (*static_cast<C*>(src_ptr)) }; }
+ },
+ move_ptr{ [](storage_ptr_t dst_ptr, storage_ptr_t src_ptr)
+ noexcept(std::is_nothrow_move_constructible<C>::value) -> void
+ { new (dst_ptr) C{ std::move(*static_cast<C*>(src_ptr)) }; }
+ },
+ destructor_ptr{ [](storage_ptr_t storage_ptr)
+ noexcept -> void
+ { static_cast<C*>(storage_ptr)->~C(); }
+ }
+ {}
+
+ vtable(const vtable&) = delete;
+ vtable(vtable&&) = delete;
+
+ vtable& operator= (const vtable&) = delete;
+ vtable& operator= (vtable&&) = delete;
+
+ ~vtable() = default;
+};
+
+template<size_t DstCap, size_t DstAlign, size_t SrcCap, size_t SrcAlign>
+struct is_valid_inplace_dst : std::true_type
+{
+ static_assert(DstCap >= SrcCap,
+ "Can't squeeze larger inplace_function into a smaller one"
+ );
+
+ static_assert(DstAlign % SrcAlign == 0,
+ "Incompatible inplace_function alignments"
+ );
+};
+
+} // namespace inplace_function_detail
+
+template<
+ typename Signature,
+ size_t Capacity = inplace_function_detail::InplaceFunctionDefaultCapacity,
+ size_t Alignment = std::alignment_of<typename inplace_function_detail::aligned_storage<Capacity>::type>::value
+>
+class inplace_function; // unspecified
+
+template<
+ typename R,
+ typename... Args,
+ size_t Capacity,
+ size_t Alignment
+>
+class inplace_function<R(Args...), Capacity, Alignment>
+{
+ static const constexpr inplace_function_detail::vtable<R, Args...> empty_vtable{};
+public:
+ using capacity = std::integral_constant<size_t, Capacity>;
+ using alignment = std::integral_constant<size_t, Alignment>;
+
+ using storage_t = typename inplace_function_detail::aligned_storage<Capacity, Alignment>::type;
+ using vtable_t = inplace_function_detail::vtable<R, Args...>;
+ using vtable_ptr_t = const vtable_t*;
+
+ template <typename, size_t, size_t> friend class inplace_function;
+
+ inplace_function() noexcept :
+ vtable_ptr_{std::addressof(empty_vtable)}
+ {}
+
+ template<
+ typename T,
+ typename C = typename std::decay<T>::type,
+ typename = typename std::enable_if<
+ !(std::is_same<C, inplace_function>::value
+ || std::is_convertible<C, inplace_function>::value)
+ >::type
+ >
+ inplace_function(T&& closure)
+ {
+#if __cplusplus >= 201703L
+ static_assert(std::is_invocable_r<R, C, Args...>::value,
+ "inplace_function cannot be constructed from non-callable type"
+ );
+#endif
+ static_assert(std::is_copy_constructible<C>::value,
+ "inplace_function cannot be constructed from non-copyable type"
+ );
+
+ static_assert(sizeof(C) <= Capacity,
+ "inplace_function cannot be constructed from object with this (large) size"
+ );
+
+ static_assert(Alignment % std::alignment_of<C>::value == 0,
+ "inplace_function cannot be constructed from object with this (large) alignment"
+ );
+
+ static const vtable_t vt{inplace_function_detail::wrapper<C>{}};
+ vtable_ptr_ = std::addressof(vt);
+
+ new (std::addressof(storage_)) C{std::forward<T>(closure)};
+ }
+
+ inplace_function(std::nullptr_t) noexcept :
+ vtable_ptr_{std::addressof(empty_vtable)}
+ {}
+
+ inplace_function(const inplace_function& other) :
+ vtable_ptr_{other.vtable_ptr_}
+ {
+ vtable_ptr_->copy_ptr(
+ std::addressof(storage_),
+ std::addressof(other.storage_)
+ );
+ }
+
+ inplace_function(inplace_function&& other) :
+ vtable_ptr_{other.vtable_ptr_}
+ {
+ vtable_ptr_->move_ptr(
+ std::addressof(storage_),
+ std::addressof(other.storage_)
+ );
+ }
+
+ inplace_function& operator= (std::nullptr_t) noexcept
+ {
+ vtable_ptr_->destructor_ptr(std::addressof(storage_));
+ vtable_ptr_ = std::addressof(empty_vtable);
+ return *this;
+ }
+
+ inplace_function& operator= (const inplace_function& other)
+ {
+ if(this != std::addressof(other))
+ {
+ vtable_ptr_->destructor_ptr(std::addressof(storage_));
+
+ vtable_ptr_ = other.vtable_ptr_;
+ vtable_ptr_->copy_ptr(
+ std::addressof(storage_),
+ std::addressof(other.storage_)
+ );
+ }
+ return *this;
+ }
+
+ inplace_function& operator= (inplace_function&& other)
+ {
+ if(this != std::addressof(other))
+ {
+ vtable_ptr_->destructor_ptr(std::addressof(storage_));
+
+ vtable_ptr_ = other.vtable_ptr_;
+ vtable_ptr_->move_ptr(
+ std::addressof(storage_),
+ std::addressof(other.storage_)
+ );
+ }
+ return *this;
+ }
+
+ ~inplace_function()
+ {
+ vtable_ptr_->destructor_ptr(std::addressof(storage_));
+ }
+
+ R operator() (Args... args) const
+ {
+ return vtable_ptr_->invoke_ptr(
+ std::addressof(storage_),
+ std::forward<Args>(args)...
+ );
+ }
+
+ constexpr bool operator== (std::nullptr_t) const noexcept
+ {
+ return !operator bool();
+ }
+
+ constexpr bool operator!= (std::nullptr_t) const noexcept
+ {
+ return operator bool();
+ }
+
+ explicit constexpr operator bool() const noexcept
+ {
+ return vtable_ptr_ != std::addressof(empty_vtable);
+ }
+
+ template<size_t Cap, size_t Align>
+ operator inplace_function<R(Args...), Cap, Align>() const&
+ {
+ static_assert(inplace_function_detail::is_valid_inplace_dst<
+ Cap, Align, Capacity, Alignment
+ >::value, "conversion not allowed");
+
+ return {vtable_ptr_, vtable_ptr_->copy_ptr, std::addressof(storage_)};
+ }
+
+ template<size_t Cap, size_t Align>
+ operator inplace_function<R(Args...), Cap, Align>() &&
+ {
+ static_assert(inplace_function_detail::is_valid_inplace_dst<
+ Cap, Align, Capacity, Alignment
+ >::value, "conversion not allowed");
+
+ return {vtable_ptr_, vtable_ptr_->move_ptr, std::addressof(storage_)};
+ }
+
+ void swap(inplace_function& other)
+ {
+ if (this == std::addressof(other)) return;
+
+ storage_t tmp;
+ vtable_ptr_->move_ptr(
+ std::addressof(tmp),
+ std::addressof(storage_)
+ );
+ vtable_ptr_->destructor_ptr(std::addressof(storage_));
+
+ other.vtable_ptr_->move_ptr(
+ std::addressof(storage_),
+ std::addressof(other.storage_)
+ );
+ other.vtable_ptr_->destructor_ptr(std::addressof(other.storage_));
+
+ vtable_ptr_->move_ptr(
+ std::addressof(other.storage_),
+ std::addressof(tmp)
+ );
+ vtable_ptr_->destructor_ptr(std::addressof(tmp));
+
+ std::swap(vtable_ptr_, other.vtable_ptr_);
+ }
+
+private:
+ vtable_ptr_t vtable_ptr_;
+ mutable storage_t storage_;
+
+ inplace_function(
+ vtable_ptr_t vtable_ptr,
+ typename vtable_t::process_ptr_t process_ptr,
+ typename vtable_t::storage_ptr_t storage_ptr
+ ) : vtable_ptr_{vtable_ptr}
+ {
+ process_ptr(std::addressof(storage_), storage_ptr);
+ }
+};
+
+} // namespace stdext
+
+#endif /* _C4_EXT_SG14_INPLACE_FUNCTION_H_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/ext/sg14/inplace_function.h)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/language.cpp
+// https://github.com/biojppm/c4core/src/c4/language.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef C4CORE_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/language.hpp
+//#include "c4/language.hpp"
+#if !defined(C4_LANGUAGE_HPP_) && !defined(_C4_LANGUAGE_HPP_)
+#error "amalgamate: file c4/language.hpp must have been included at this point"
+#endif /* C4_LANGUAGE_HPP_ */
+
+
+namespace c4 {
+namespace detail {
+
+#ifndef __GNUC__
+void use_char_pointer(char const volatile* v)
+{
+ C4_UNUSED(v);
+}
+#else
+void foo() {} // to avoid empty file warning from the linker
+#endif
+
+} // namespace detail
+} // namespace c4
+
+#endif /* C4CORE_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/language.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/format.cpp
+// https://github.com/biojppm/c4core/src/c4/format.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef C4CORE_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/format.hpp
+//#include "c4/format.hpp"
+#if !defined(C4_FORMAT_HPP_) && !defined(_C4_FORMAT_HPP_)
+#error "amalgamate: file c4/format.hpp must have been included at this point"
+#endif /* C4_FORMAT_HPP_ */
+
+
+//included above:
+//#include <memory> // for std::align
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wformat-nonliteral"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wformat-nonliteral"
+#endif
+
+namespace c4 {
+
+
+size_t to_chars(substr buf, fmt::const_raw_wrapper r)
+{
+ void * vptr = buf.str;
+ size_t space = buf.len;
+ auto ptr = (decltype(buf.str)) std::align(r.alignment, r.len, vptr, space);
+ if(ptr == nullptr)
+ {
+ // if it was not possible to align, return a conservative estimate
+ // of the required space
+ return r.alignment + r.len;
+ }
+ C4_CHECK(ptr >= buf.begin() && ptr <= buf.end());
+ size_t sz = static_cast<size_t>(ptr - buf.str) + r.len;
+ if(sz <= buf.len)
+ {
+ memcpy(ptr, r.buf, r.len);
+ }
+ return sz;
+}
+
+
+bool from_chars(csubstr buf, fmt::raw_wrapper *r)
+{
+ void * vptr = (void*)buf.str;
+ size_t space = buf.len;
+ auto ptr = (decltype(buf.str)) std::align(r->alignment, r->len, vptr, space);
+ C4_CHECK(ptr != nullptr);
+ C4_CHECK(ptr >= buf.begin() && ptr <= buf.end());
+ //size_t dim = (ptr - buf.str) + r->len;
+ memcpy(r->buf, ptr, r->len);
+ return true;
+}
+
+
+} // namespace c4
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#endif /* C4CORE_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/format.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/memory_util.cpp
+// https://github.com/biojppm/c4core/src/c4/memory_util.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef C4CORE_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/memory_util.hpp
+//#include "c4/memory_util.hpp"
+#if !defined(C4_MEMORY_UTIL_HPP_) && !defined(_C4_MEMORY_UTIL_HPP_)
+#error "amalgamate: file c4/memory_util.hpp must have been included at this point"
+#endif /* C4_MEMORY_UTIL_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+
+namespace c4 {
+
+/** returns true if the memory overlaps */
+bool mem_overlaps(void const* a, void const* b, size_t sza, size_t szb)
+{
+ if(a < b)
+ {
+ if(size_t(a) + sza > size_t(b))
+ return true;
+ }
+ else if(a > b)
+ {
+ if(size_t(b) + szb > size_t(a))
+ return true;
+ }
+ else if(a == b)
+ {
+ if(sza != 0 && szb != 0)
+ return true;
+ }
+ return false;
+}
+
+/** Fills 'dest' with the first 'pattern_size' bytes at 'pattern', 'num_times'. */
+void mem_repeat(void* dest, void const* pattern, size_t pattern_size, size_t num_times)
+{
+ if(C4_UNLIKELY(num_times == 0))
+ return;
+ C4_ASSERT( ! mem_overlaps(dest, pattern, num_times*pattern_size, pattern_size));
+ char *begin = (char*)dest;
+ char *end = begin + num_times * pattern_size;
+ // copy the pattern once
+ ::memcpy(begin, pattern, pattern_size);
+ // now copy from dest to itself, doubling up every time
+ size_t n = pattern_size;
+ while(begin + 2*n < end)
+ {
+ ::memcpy(begin + n, begin, n);
+ n <<= 1; // double n
+ }
+ // copy the missing part
+ if(begin + n < end)
+ {
+ ::memcpy(begin + n, begin, static_cast<size_t>(end - (begin + n)));
+ }
+}
+
+} // namespace c4
+
+#endif /* C4CORE_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/memory_util.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/char_traits.cpp
+// https://github.com/biojppm/c4core/src/c4/char_traits.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef C4CORE_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/char_traits.hpp
+//#include "c4/char_traits.hpp"
+#if !defined(C4_CHAR_TRAITS_HPP_) && !defined(_C4_CHAR_TRAITS_HPP_)
+#error "amalgamate: file c4/char_traits.hpp must have been included at this point"
+#endif /* C4_CHAR_TRAITS_HPP_ */
+
+
+namespace c4 {
+
+constexpr const char char_traits< char >::whitespace_chars[];
+constexpr const size_t char_traits< char >::num_whitespace_chars;
+constexpr const wchar_t char_traits< wchar_t >::whitespace_chars[];
+constexpr const size_t char_traits< wchar_t >::num_whitespace_chars;
+
+} // namespace c4
+
+#endif /* C4CORE_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/char_traits.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/memory_resource.cpp
+// https://github.com/biojppm/c4core/src/c4/memory_resource.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef C4CORE_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/memory_resource.hpp
+//#include "c4/memory_resource.hpp"
+#if !defined(C4_MEMORY_RESOURCE_HPP_) && !defined(_C4_MEMORY_RESOURCE_HPP_)
+#error "amalgamate: file c4/memory_resource.hpp must have been included at this point"
+#endif /* C4_MEMORY_RESOURCE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/memory_util.hpp
+//#include "c4/memory_util.hpp"
+#if !defined(C4_MEMORY_UTIL_HPP_) && !defined(_C4_MEMORY_UTIL_HPP_)
+#error "amalgamate: file c4/memory_util.hpp must have been included at this point"
+#endif /* C4_MEMORY_UTIL_HPP_ */
+
+
+//included above:
+//#include <stdlib.h>
+//included above:
+//#include <string.h>
+#if defined(C4_POSIX) || defined(C4_IOS) || defined(C4_MACOS) || defined(C4_ARM)
+# include <errno.h>
+#endif
+#if defined(C4_ARM)
+# include <malloc.h>
+#endif
+
+//included above:
+//#include <memory>
+
+namespace c4 {
+
+namespace detail {
+
+
+#ifdef C4_NO_ALLOC_DEFAULTS
+aalloc_pfn s_aalloc = nullptr;
+free_pfn s_afree = nullptr;
+arealloc_pfn s_arealloc = nullptr;
+#else
+
+
+void afree_impl(void *ptr)
+{
+#if defined(C4_WIN) || defined(C4_XBOX)
+ ::_aligned_free(ptr);
+#else
+ ::free(ptr);
+#endif
+}
+
+
+void* aalloc_impl(size_t size, size_t alignment)
+{
+ void *mem;
+#if defined(C4_WIN) || defined(C4_XBOX)
+ mem = ::_aligned_malloc(size, alignment);
+ C4_CHECK(mem != nullptr || size == 0);
+#elif defined(C4_ARM)
+ // https://stackoverflow.com/questions/53614538/undefined-reference-to-posix-memalign-in-arm-gcc
+ // https://electronics.stackexchange.com/questions/467382/e2-studio-undefined-reference-to-posix-memalign/467753
+ mem = memalign(alignment, size);
+ C4_CHECK(mem != nullptr || size == 0);
+#elif defined(C4_POSIX) || defined(C4_IOS) || defined(C4_MACOS)
+ // NOTE: alignment needs to be sized in multiples of sizeof(void*)
+ size_t amult = alignment;
+ if(C4_UNLIKELY(alignment < sizeof(void*)))
+ {
+ amult = sizeof(void*);
+ }
+ int ret = ::posix_memalign(&mem, amult, size);
+ if(C4_UNLIKELY(ret))
+ {
+ if(ret == EINVAL)
+ {
+ C4_ERROR("The alignment argument %zu was not a power of two, "
+ "or was not a multiple of sizeof(void*)", alignment);
+ }
+ else if(ret == ENOMEM)
+ {
+ C4_ERROR("There was insufficient memory to fulfill the "
+ "allocation request of %zu bytes (alignment=%lu)", size, size);
+ }
+ return nullptr;
+ }
+#else
+ C4_NOT_IMPLEMENTED_MSG("need to implement an aligned allocation for this platform");
+#endif
+ C4_ASSERT_MSG((uintptr_t(mem) & (alignment-1)) == 0, "address %p is not aligned to %zu boundary", mem, alignment);
+ return mem;
+}
+
+
+void* arealloc_impl(void* ptr, size_t oldsz, size_t newsz, size_t alignment)
+{
+ /** @todo make this more efficient
+ * @see https://stackoverflow.com/questions/9078259/does-realloc-keep-the-memory-alignment-of-posix-memalign
+ * @see look for qReallocAligned() in http://code.qt.io/cgit/qt/qtbase.git/tree/src/corelib/global/qmalloc.cpp
+ */
+ void *tmp = aalloc(newsz, alignment);
+ size_t min = newsz < oldsz ? newsz : oldsz;
+ if(mem_overlaps(ptr, tmp, oldsz, newsz))
+ {
+ ::memmove(tmp, ptr, min);
+ }
+ else
+ {
+ ::memcpy(tmp, ptr, min);
+ }
+ afree(ptr);
+ return tmp;
+}
+
+aalloc_pfn s_aalloc = aalloc_impl;
+afree_pfn s_afree = afree_impl;
+arealloc_pfn s_arealloc = arealloc_impl;
+
+#endif // C4_NO_ALLOC_DEFAULTS
+
+} // namespace detail
+
+
+aalloc_pfn get_aalloc()
+{
+ return detail::s_aalloc;
+}
+void set_aalloc(aalloc_pfn fn)
+{
+ detail::s_aalloc = fn;
+}
+
+afree_pfn get_afree()
+{
+ return detail::s_afree;
+}
+void set_afree(afree_pfn fn)
+{
+ detail::s_afree = fn;
+}
+
+arealloc_pfn get_arealloc()
+{
+ return detail::s_arealloc;
+}
+void set_arealloc(arealloc_pfn fn)
+{
+ detail::s_arealloc = fn;
+}
+
+
+void* aalloc(size_t sz, size_t alignment)
+{
+ C4_ASSERT_MSG(c4::get_aalloc() != nullptr, "did you forget to call set_aalloc()?");
+ auto fn = c4::get_aalloc();
+ void* ptr = fn(sz, alignment);
+ return ptr;
+}
+
+void afree(void* ptr)
+{
+ C4_ASSERT_MSG(c4::get_afree() != nullptr, "did you forget to call set_afree()?");
+ auto fn = c4::get_afree();
+ fn(ptr);
+}
+
+void* arealloc(void *ptr, size_t oldsz, size_t newsz, size_t alignment)
+{
+ C4_ASSERT_MSG(c4::get_arealloc() != nullptr, "did you forget to call set_arealloc()?");
+ auto fn = c4::get_arealloc();
+ void* nptr = fn(ptr, oldsz, newsz, alignment);
+ return nptr;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+void detail::_MemoryResourceSingleChunk::release()
+{
+ if(m_mem && m_owner)
+ {
+ impl_type::deallocate(m_mem, m_size);
+ }
+ m_mem = nullptr;
+ m_size = 0;
+ m_owner = false;
+ m_pos = 0;
+}
+
+void detail::_MemoryResourceSingleChunk::acquire(size_t sz)
+{
+ clear();
+ m_owner = true;
+ m_mem = (char*) impl_type::allocate(sz, alignof(max_align_t));
+ m_size = sz;
+ m_pos = 0;
+}
+
+void detail::_MemoryResourceSingleChunk::acquire(void *mem, size_t sz)
+{
+ clear();
+ m_owner = false;
+ m_mem = (char*) mem;
+ m_size = sz;
+ m_pos = 0;
+}
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+void* MemoryResourceLinear::do_allocate(size_t sz, size_t alignment, void *hint)
+{
+ C4_UNUSED(hint);
+ if(sz == 0) return nullptr;
+ // make sure there's enough room to allocate
+ if(m_pos + sz > m_size)
+ {
+ C4_ERROR("out of memory");
+ return nullptr;
+ }
+ void *mem = m_mem + m_pos;
+ size_t space = m_size - m_pos;
+ if(std::align(alignment, sz, mem, space))
+ {
+ C4_ASSERT(m_pos <= m_size);
+ C4_ASSERT(m_size - m_pos >= space);
+ m_pos += (m_size - m_pos) - space;
+ m_pos += sz;
+ C4_ASSERT(m_pos <= m_size);
+ }
+ else
+ {
+ C4_ERROR("could not align memory");
+ mem = nullptr;
+ }
+ return mem;
+}
+
+void MemoryResourceLinear::do_deallocate(void* ptr, size_t sz, size_t alignment)
+{
+ C4_UNUSED(ptr);
+ C4_UNUSED(sz);
+ C4_UNUSED(alignment);
+ // nothing to do!!
+}
+
+void* MemoryResourceLinear::do_reallocate(void* ptr, size_t oldsz, size_t newsz, size_t alignment)
+{
+ if(newsz == oldsz) return ptr;
+ // is ptr the most recently allocated (MRA) block?
+ char *cptr = (char*)ptr;
+ bool same_pos = (m_mem + m_pos == cptr + oldsz);
+ // no need to get more memory when shrinking
+ if(newsz < oldsz)
+ {
+ // if this is the MRA, we can safely shrink the position
+ if(same_pos)
+ {
+ m_pos -= oldsz - newsz;
+ }
+ return ptr;
+ }
+ // we're growing the block, and it fits in size
+ else if(same_pos && cptr + newsz <= m_mem + m_size)
+ {
+ // if this is the MRA, we can safely shrink the position
+ m_pos += newsz - oldsz;
+ return ptr;
+ }
+ // we're growing the block or it doesn't fit -
+ // delegate any of these situations to do_deallocate()
+ return do_allocate(newsz, alignment, ptr);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** @todo add a free list allocator. A good candidate because of its
+ * small size is TLSF.
+ *
+ * @see https://github.com/mattconte/tlsf
+ *
+ * Comparisons:
+ *
+ * @see https://www.researchgate.net/publication/262375150_A_Comparative_Study_on_Memory_Allocators_in_Multicore_and_Multithreaded_Applications_-_SBESC_2011_-_Presentation_Slides
+ * @see http://webkit.sed.hu/blog/20100324/war-allocators-tlsf-action
+ * @see https://github.com/emeryberger/Malloc-Implementations/tree/master/allocators
+ *
+ * */
+
+} // namespace c4
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+#ifdef C4_REDEFINE_CPPNEW
+#include <new>
+void* operator new(size_t size)
+{
+ auto *mr = ::c4::get_memory_resource();
+ return mr->allocate(size);
+}
+void operator delete(void *p) noexcept
+{
+ C4_NEVER_REACH();
+}
+void operator delete(void *p, size_t size)
+{
+ auto *mr = ::c4::get_memory_resource();
+ mr->deallocate(p, size);
+}
+void* operator new[](size_t size)
+{
+ return operator new(size);
+}
+void operator delete[](void *p) noexcept
+{
+ operator delete(p);
+}
+void operator delete[](void *p, size_t size)
+{
+ operator delete(p, size);
+}
+void* operator new(size_t size, std::nothrow_t)
+{
+ return operator new(size);
+}
+void operator delete(void *p, std::nothrow_t)
+{
+ operator delete(p);
+}
+void operator delete(void *p, size_t size, std::nothrow_t)
+{
+ operator delete(p, size);
+}
+void* operator new[](size_t size, std::nothrow_t)
+{
+ return operator new(size);
+}
+void operator delete[](void *p, std::nothrow_t)
+{
+ operator delete(p);
+}
+void operator delete[](void *p, size_t, std::nothrow_t)
+{
+ operator delete(p, size);
+}
+#endif // C4_REDEFINE_CPPNEW
+
+#endif /* C4CORE_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/memory_resource.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/utf.cpp
+// https://github.com/biojppm/c4core/src/c4/utf.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef C4CORE_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/utf.hpp
+//#include "c4/utf.hpp"
+#if !defined(C4_UTF_HPP_) && !defined(_C4_UTF_HPP_)
+#error "amalgamate: file c4/utf.hpp must have been included at this point"
+#endif /* C4_UTF_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/charconv.hpp
+//#include "c4/charconv.hpp"
+#if !defined(C4_CHARCONV_HPP_) && !defined(_C4_CHARCONV_HPP_)
+#error "amalgamate: file c4/charconv.hpp must have been included at this point"
+#endif /* C4_CHARCONV_HPP_ */
+
+
+namespace c4 {
+
+size_t decode_code_point(uint8_t *C4_RESTRICT buf, size_t buflen, const uint32_t code)
+{
+ C4_UNUSED(buflen);
+ C4_ASSERT(buflen >= 4);
+ if (code <= UINT32_C(0x7f))
+ {
+ buf[0] = (uint8_t)code;
+ return 1u;
+ }
+ else if(code <= UINT32_C(0x7ff))
+ {
+ buf[0] = (uint8_t)(UINT32_C(0xc0) | (code >> 6)); /* 110xxxxx */
+ buf[1] = (uint8_t)(UINT32_C(0x80) | (code & UINT32_C(0x3f))); /* 10xxxxxx */
+ return 2u;
+ }
+ else if(code <= UINT32_C(0xffff))
+ {
+ buf[0] = (uint8_t)(UINT32_C(0xe0) | ((code >> 12))); /* 1110xxxx */
+ buf[1] = (uint8_t)(UINT32_C(0x80) | ((code >> 6) & UINT32_C(0x3f))); /* 10xxxxxx */
+ buf[2] = (uint8_t)(UINT32_C(0x80) | ((code ) & UINT32_C(0x3f))); /* 10xxxxxx */
+ return 3u;
+ }
+ else if(code <= UINT32_C(0x10ffff))
+ {
+ buf[0] = (uint8_t)(UINT32_C(0xf0) | ((code >> 18))); /* 11110xxx */
+ buf[1] = (uint8_t)(UINT32_C(0x80) | ((code >> 12) & UINT32_C(0x3f))); /* 10xxxxxx */
+ buf[2] = (uint8_t)(UINT32_C(0x80) | ((code >> 6) & UINT32_C(0x3f))); /* 10xxxxxx */
+ buf[3] = (uint8_t)(UINT32_C(0x80) | ((code ) & UINT32_C(0x3f))); /* 10xxxxxx */
+ return 4u;
+ }
+ return 0;
+}
+
+substr decode_code_point(substr out, csubstr code_point)
+{
+ C4_ASSERT(out.len >= 4);
+ C4_ASSERT(!code_point.begins_with("U+"));
+ C4_ASSERT(!code_point.begins_with("\\x"));
+ C4_ASSERT(!code_point.begins_with("\\u"));
+ C4_ASSERT(!code_point.begins_with("\\U"));
+ C4_ASSERT(!code_point.begins_with('0'));
+ C4_ASSERT(code_point.len <= 8);
+ uint32_t code_point_val;
+ C4_CHECK(read_hex(code_point, &code_point_val));
+ size_t ret = decode_code_point((uint8_t*)out.str, out.len, code_point_val);
+ C4_ASSERT(ret <= 4);
+ return out.first(ret);
+}
+
+} // namespace c4
+
+#endif /* C4CORE_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/utf.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/base64.cpp
+// https://github.com/biojppm/c4core/src/c4/base64.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef C4CORE_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/base64.hpp
+//#include "c4/base64.hpp"
+#if !defined(C4_BASE64_HPP_) && !defined(_C4_BASE64_HPP_)
+#error "amalgamate: file c4/base64.hpp must have been included at this point"
+#endif /* C4_BASE64_HPP_ */
+
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wchar-subscripts" // array subscript is of type 'char'
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wchar-subscripts"
+# pragma GCC diagnostic ignored "-Wtype-limits"
+#endif
+
+namespace c4 {
+
+namespace detail {
+
+constexpr static const char base64_sextet_to_char_[64] = {
+ /* 0/ 65*/ 'A', /* 1/ 66*/ 'B', /* 2/ 67*/ 'C', /* 3/ 68*/ 'D',
+ /* 4/ 69*/ 'E', /* 5/ 70*/ 'F', /* 6/ 71*/ 'G', /* 7/ 72*/ 'H',
+ /* 8/ 73*/ 'I', /* 9/ 74*/ 'J', /*10/ 75*/ 'K', /*11/ 74*/ 'L',
+ /*12/ 77*/ 'M', /*13/ 78*/ 'N', /*14/ 79*/ 'O', /*15/ 78*/ 'P',
+ /*16/ 81*/ 'Q', /*17/ 82*/ 'R', /*18/ 83*/ 'S', /*19/ 82*/ 'T',
+ /*20/ 85*/ 'U', /*21/ 86*/ 'V', /*22/ 87*/ 'W', /*23/ 88*/ 'X',
+ /*24/ 89*/ 'Y', /*25/ 90*/ 'Z', /*26/ 97*/ 'a', /*27/ 98*/ 'b',
+ /*28/ 99*/ 'c', /*29/100*/ 'd', /*30/101*/ 'e', /*31/102*/ 'f',
+ /*32/103*/ 'g', /*33/104*/ 'h', /*34/105*/ 'i', /*35/106*/ 'j',
+ /*36/107*/ 'k', /*37/108*/ 'l', /*38/109*/ 'm', /*39/110*/ 'n',
+ /*40/111*/ 'o', /*41/112*/ 'p', /*42/113*/ 'q', /*43/114*/ 'r',
+ /*44/115*/ 's', /*45/116*/ 't', /*46/117*/ 'u', /*47/118*/ 'v',
+ /*48/119*/ 'w', /*49/120*/ 'x', /*50/121*/ 'y', /*51/122*/ 'z',
+ /*52/ 48*/ '0', /*53/ 49*/ '1', /*54/ 50*/ '2', /*55/ 51*/ '3',
+ /*56/ 52*/ '4', /*57/ 53*/ '5', /*58/ 54*/ '6', /*59/ 55*/ '7',
+ /*60/ 56*/ '8', /*61/ 57*/ '9', /*62/ 43*/ '+', /*63/ 47*/ '/',
+};
+
+// https://www.cs.cmu.edu/~pattis/15-1XX/common/handouts/ascii.html
+constexpr static const char base64_char_to_sextet_[128] = {
+ #define __ char(-1) // undefined below
+ /* 0 NUL*/ __, /* 1 SOH*/ __, /* 2 STX*/ __, /* 3 ETX*/ __,
+ /* 4 EOT*/ __, /* 5 ENQ*/ __, /* 6 ACK*/ __, /* 7 BEL*/ __,
+ /* 8 BS */ __, /* 9 TAB*/ __, /* 10 LF */ __, /* 11 VT */ __,
+ /* 12 FF */ __, /* 13 CR */ __, /* 14 SO */ __, /* 15 SI */ __,
+ /* 16 DLE*/ __, /* 17 DC1*/ __, /* 18 DC2*/ __, /* 19 DC3*/ __,
+ /* 20 DC4*/ __, /* 21 NAK*/ __, /* 22 SYN*/ __, /* 23 ETB*/ __,
+ /* 24 CAN*/ __, /* 25 EM */ __, /* 26 SUB*/ __, /* 27 ESC*/ __,
+ /* 28 FS */ __, /* 29 GS */ __, /* 30 RS */ __, /* 31 US */ __,
+ /* 32 SPC*/ __, /* 33 ! */ __, /* 34 " */ __, /* 35 # */ __,
+ /* 36 $ */ __, /* 37 % */ __, /* 38 & */ __, /* 39 ' */ __,
+ /* 40 ( */ __, /* 41 ) */ __, /* 42 * */ __, /* 43 + */ 62,
+ /* 44 , */ __, /* 45 - */ __, /* 46 . */ __, /* 47 / */ 63,
+ /* 48 0 */ 52, /* 49 1 */ 53, /* 50 2 */ 54, /* 51 3 */ 55,
+ /* 52 4 */ 56, /* 53 5 */ 57, /* 54 6 */ 58, /* 55 7 */ 59,
+ /* 56 8 */ 60, /* 57 9 */ 61, /* 58 : */ __, /* 59 ; */ __,
+ /* 60 < */ __, /* 61 = */ __, /* 62 > */ __, /* 63 ? */ __,
+ /* 64 @ */ __, /* 65 A */ 0, /* 66 B */ 1, /* 67 C */ 2,
+ /* 68 D */ 3, /* 69 E */ 4, /* 70 F */ 5, /* 71 G */ 6,
+ /* 72 H */ 7, /* 73 I */ 8, /* 74 J */ 9, /* 75 K */ 10,
+ /* 76 L */ 11, /* 77 M */ 12, /* 78 N */ 13, /* 79 O */ 14,
+ /* 80 P */ 15, /* 81 Q */ 16, /* 82 R */ 17, /* 83 S */ 18,
+ /* 84 T */ 19, /* 85 U */ 20, /* 86 V */ 21, /* 87 W */ 22,
+ /* 88 X */ 23, /* 89 Y */ 24, /* 90 Z */ 25, /* 91 [ */ __,
+ /* 92 \ */ __, /* 93 ] */ __, /* 94 ^ */ __, /* 95 _ */ __,
+ /* 96 ` */ __, /* 97 a */ 26, /* 98 b */ 27, /* 99 c */ 28,
+ /*100 d */ 29, /*101 e */ 30, /*102 f */ 31, /*103 g */ 32,
+ /*104 h */ 33, /*105 i */ 34, /*106 j */ 35, /*107 k */ 36,
+ /*108 l */ 37, /*109 m */ 38, /*110 n */ 39, /*111 o */ 40,
+ /*112 p */ 41, /*113 q */ 42, /*114 r */ 43, /*115 s */ 44,
+ /*116 t */ 45, /*117 u */ 46, /*118 v */ 47, /*119 w */ 48,
+ /*120 x */ 49, /*121 y */ 50, /*122 z */ 51, /*123 { */ __,
+ /*124 | */ __, /*125 } */ __, /*126 ~ */ __, /*127 DEL*/ __,
+ #undef __
+};
+
+#ifndef NDEBUG
+void base64_test_tables()
+{
+ for(size_t i = 0; i < C4_COUNTOF(detail::base64_sextet_to_char_); ++i)
+ {
+ char s2c = base64_sextet_to_char_[i];
+ char c2s = base64_char_to_sextet_[(int)s2c];
+ C4_CHECK((size_t)c2s == i);
+ }
+ for(size_t i = 0; i < C4_COUNTOF(detail::base64_char_to_sextet_); ++i)
+ {
+ char c2s = base64_char_to_sextet_[i];
+ if(c2s == char(-1))
+ continue;
+ char s2c = base64_sextet_to_char_[(int)c2s];
+ C4_CHECK((size_t)s2c == i);
+ }
+}
+#endif
+} // namespace detail
+
+
+bool base64_valid(csubstr encoded)
+{
+ if(encoded.len % 4) return false;
+ for(const char c : encoded)
+ {
+ if(c < 0/* || c >= 128*/)
+ return false;
+ if(c == '=')
+ continue;
+ if(detail::base64_char_to_sextet_[c] == char(-1))
+ return false;
+ }
+ return true;
+}
+
+
+size_t base64_encode(substr buf, cblob data)
+{
+ #define c4append_(c) { if(pos < buf.len) { buf.str[pos] = (c); } ++pos; }
+ #define c4append_idx_(char_idx) \
+ {\
+ C4_XASSERT((char_idx) < sizeof(detail::base64_sextet_to_char_));\
+ c4append_(detail::base64_sextet_to_char_[(char_idx)]);\
+ }
+
+ size_t rem, pos = 0;
+ constexpr const uint32_t sextet_mask = uint32_t(1 << 6) - 1;
+ const unsigned char *C4_RESTRICT d = (unsigned char *) data.buf; // cast to unsigned to avoid wrapping high-bits
+ for(rem = data.len; rem >= 3; rem -= 3, d += 3)
+ {
+ const uint32_t val = ((uint32_t(d[0]) << 16) | (uint32_t(d[1]) << 8) | (uint32_t(d[2])));
+ c4append_idx_((val >> 18) & sextet_mask);
+ c4append_idx_((val >> 12) & sextet_mask);
+ c4append_idx_((val >> 6) & sextet_mask);
+ c4append_idx_((val ) & sextet_mask);
+ }
+ C4_ASSERT(rem < 3);
+ if(rem == 2)
+ {
+ const uint32_t val = ((uint32_t(d[0]) << 16) | (uint32_t(d[1]) << 8));
+ c4append_idx_((val >> 18) & sextet_mask);
+ c4append_idx_((val >> 12) & sextet_mask);
+ c4append_idx_((val >> 6) & sextet_mask);
+ c4append_('=');
+ }
+ else if(rem == 1)
+ {
+ const uint32_t val = ((uint32_t(d[0]) << 16));
+ c4append_idx_((val >> 18) & sextet_mask);
+ c4append_idx_((val >> 12) & sextet_mask);
+ c4append_('=');
+ c4append_('=');
+ }
+ return pos;
+
+ #undef c4append_
+ #undef c4append_idx_
+}
+
+
+size_t base64_decode(csubstr encoded, blob data)
+{
+ #define c4append_(c) { if(wpos < data.len) { data.buf[wpos] = static_cast<c4::byte>(c); } ++wpos; }
+ #define c4appendval_(c, shift)\
+ {\
+ C4_XASSERT(c >= 0);\
+ C4_XASSERT(size_t(c) < sizeof(detail::base64_char_to_sextet_));\
+ val |= static_cast<uint32_t>(detail::base64_char_to_sextet_[(c)]) << ((shift) * 6);\
+ }
+
+ C4_ASSERT(base64_valid(encoded));
+ C4_CHECK(encoded.len % 4 == 0);
+ size_t wpos = 0; // the write position
+ const char *C4_RESTRICT d = encoded.str;
+ constexpr const uint32_t full_byte = 0xff;
+ // process every quartet of input 6 bits --> triplet of output bytes
+ for(size_t rpos = 0; rpos < encoded.len; rpos += 4, d += 4)
+ {
+ if(d[2] == '=' || d[3] == '=') // skip the last quartet if it is padded
+ {
+ C4_ASSERT(d + 4 == encoded.str + encoded.len);
+ break;
+ }
+ uint32_t val = 0;
+ c4appendval_(d[3], 0);
+ c4appendval_(d[2], 1);
+ c4appendval_(d[1], 2);
+ c4appendval_(d[0], 3);
+ c4append_((val >> (2 * 8)) & full_byte);
+ c4append_((val >> (1 * 8)) & full_byte);
+ c4append_((val ) & full_byte);
+ }
+ // deal with the last quartet when it is padded
+ if(d == encoded.str + encoded.len)
+ return wpos;
+ if(d[2] == '=') // 2 padding chars
+ {
+ C4_ASSERT(d + 4 == encoded.str + encoded.len);
+ C4_ASSERT(d[3] == '=');
+ uint32_t val = 0;
+ c4appendval_(d[1], 2);
+ c4appendval_(d[0], 3);
+ c4append_((val >> (2 * 8)) & full_byte);
+ }
+ else if(d[3] == '=') // 1 padding char
+ {
+ C4_ASSERT(d + 4 == encoded.str + encoded.len);
+ uint32_t val = 0;
+ c4appendval_(d[2], 1);
+ c4appendval_(d[1], 2);
+ c4appendval_(d[0], 3);
+ c4append_((val >> (2 * 8)) & full_byte);
+ c4append_((val >> (1 * 8)) & full_byte);
+ }
+ return wpos;
+ #undef c4append_
+ #undef c4appendval_
+}
+
+} // namespace c4
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#endif /* C4CORE_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/base64.cpp)
+
+#define C4_WINDOWS_POP_HPP_
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/windows_push.hpp
+// https://github.com/biojppm/c4core/src/c4/windows_push.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_WINDOWS_PUSH_HPP_
+#define _C4_WINDOWS_PUSH_HPP_
+
+/** @file windows_push.hpp sets up macros to include windows header files
+ * without pulling in all of <windows.h>
+ *
+ * @see #include windows_pop.hpp to undefine these macros
+ *
+ * @see https://aras-p.info/blog/2018/01/12/Minimizing-windows.h/ */
+
+
+#if defined(_WIN64) || defined(_WIN32)
+
+#if defined(_M_AMD64)
+# ifndef _AMD64_
+# define _c4_AMD64_
+# define _AMD64_
+# endif
+#elif defined(_M_IX86)
+# ifndef _X86_
+# define _c4_X86_
+# define _X86_
+# endif
+#elif defined(_M_ARM64)
+# ifndef _ARM64_
+# define _c4_ARM64_
+# define _ARM64_
+# endif
+#elif defined(_M_ARM)
+# ifndef _ARM_
+# define _c4_ARM_
+# define _ARM_
+# endif
+#endif
+
+#ifndef NOMINMAX
+# define _c4_NOMINMAX
+# define NOMINMAX
+#endif
+
+#ifndef NOGDI
+# define _c4_NOGDI
+# define NOGDI
+#endif
+
+#ifndef VC_EXTRALEAN
+# define _c4_VC_EXTRALEAN
+# define VC_EXTRALEAN
+#endif
+
+#ifndef WIN32_LEAN_AND_MEAN
+# define _c4_WIN32_LEAN_AND_MEAN
+# define WIN32_LEAN_AND_MEAN
+#endif
+
+/* If defined, the following flags inhibit definition
+ * of the indicated items.
+ *
+ * NOGDICAPMASKS - CC_*, LC_*, PC_*, CP_*, TC_*, RC_
+ * NOVIRTUALKEYCODES - VK_*
+ * NOWINMESSAGES - WM_*, EM_*, LB_*, CB_*
+ * NOWINSTYLES - WS_*, CS_*, ES_*, LBS_*, SBS_*, CBS_*
+ * NOSYSMETRICS - SM_*
+ * NOMENUS - MF_*
+ * NOICONS - IDI_*
+ * NOKEYSTATES - MK_*
+ * NOSYSCOMMANDS - SC_*
+ * NORASTEROPS - Binary and Tertiary raster ops
+ * NOSHOWWINDOW - SW_*
+ * OEMRESOURCE - OEM Resource values
+ * NOATOM - Atom Manager routines
+ * NOCLIPBOARD - Clipboard routines
+ * NOCOLOR - Screen colors
+ * NOCTLMGR - Control and Dialog routines
+ * NODRAWTEXT - DrawText() and DT_*
+ * NOGDI - All GDI defines and routines
+ * NOKERNEL - All KERNEL defines and routines
+ * NOUSER - All USER defines and routines
+ * NONLS - All NLS defines and routines
+ * NOMB - MB_* and MessageBox()
+ * NOMEMMGR - GMEM_*, LMEM_*, GHND, LHND, associated routines
+ * NOMETAFILE - typedef METAFILEPICT
+ * NOMINMAX - Macros min(a,b) and max(a,b)
+ * NOMSG - typedef MSG and associated routines
+ * NOOPENFILE - OpenFile(), OemToAnsi, AnsiToOem, and OF_*
+ * NOSCROLL - SB_* and scrolling routines
+ * NOSERVICE - All Service Controller routines, SERVICE_ equates, etc.
+ * NOSOUND - Sound driver routines
+ * NOTEXTMETRIC - typedef TEXTMETRIC and associated routines
+ * NOWH - SetWindowsHook and WH_*
+ * NOWINOFFSETS - GWL_*, GCL_*, associated routines
+ * NOCOMM - COMM driver routines
+ * NOKANJI - Kanji support stuff.
+ * NOHELP - Help engine interface.
+ * NOPROFILER - Profiler interface.
+ * NODEFERWINDOWPOS - DeferWindowPos routines
+ * NOMCX - Modem Configuration Extensions
+ */
+
+#endif /* defined(_WIN64) || defined(_WIN32) */
+
+#endif /* _C4_WINDOWS_PUSH_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/windows_push.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/windows.hpp
+// https://github.com/biojppm/c4core/src/c4/windows.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_WINDOWS_HPP_
+#define _C4_WINDOWS_HPP_
+
+#if defined(_WIN64) || defined(_WIN32)
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/windows_push.hpp
+//#include "c4/windows_push.hpp"
+#if !defined(C4_WINDOWS_PUSH_HPP_) && !defined(_C4_WINDOWS_PUSH_HPP_)
+#error "amalgamate: file c4/windows_push.hpp must have been included at this point"
+#endif /* C4_WINDOWS_PUSH_HPP_ */
+
+#include <windows.h>
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/windows_pop.hpp
+//#include "c4/windows_pop.hpp"
+#if !defined(C4_WINDOWS_POP_HPP_) && !defined(_C4_WINDOWS_POP_HPP_)
+#error "amalgamate: file c4/windows_pop.hpp must have been included at this point"
+#endif /* C4_WINDOWS_POP_HPP_ */
+
+#endif
+
+#endif /* _C4_WINDOWS_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/windows.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/windows_pop.hpp
+// https://github.com/biojppm/c4core/src/c4/windows_pop.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_WINDOWS_POP_HPP_
+#define _C4_WINDOWS_POP_HPP_
+
+#if defined(_WIN64) || defined(_WIN32)
+
+#ifdef _c4_AMD64_
+# undef _c4_AMD64_
+# undef _AMD64_
+#endif
+#ifdef _c4_X86_
+# undef _c4_X86_
+# undef _X86_
+#endif
+#ifdef _c4_ARM_
+# undef _c4_ARM_
+# undef _ARM_
+#endif
+
+#ifdef _c4_NOMINMAX
+# undef _c4_NOMINMAX
+# undef NOMINMAX
+#endif
+
+#ifdef NOGDI
+# undef _c4_NOGDI
+# undef NOGDI
+#endif
+
+#ifdef VC_EXTRALEAN
+# undef _c4_VC_EXTRALEAN
+# undef VC_EXTRALEAN
+#endif
+
+#ifdef WIN32_LEAN_AND_MEAN
+# undef _c4_WIN32_LEAN_AND_MEAN
+# undef WIN32_LEAN_AND_MEAN
+#endif
+
+#endif /* defined(_WIN64) || defined(_WIN32) */
+
+#endif /* _C4_WINDOWS_POP_HPP_ */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/windows_pop.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/error.cpp
+// https://github.com/biojppm/c4core/src/c4/error.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef C4CORE_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+
+//included above:
+//#include <stdlib.h>
+//included above:
+//#include <stdio.h>
+//included above:
+//#include <stdarg.h>
+
+#define C4_LOGF_ERR(...) fprintf(stderr, __VA_ARGS__); fflush(stderr)
+#define C4_LOGF_WARN(...) fprintf(stderr, __VA_ARGS__); fflush(stderr)
+#define C4_LOGP(msg, ...) printf(msg)
+
+#if defined(C4_XBOX) || (defined(C4_WIN) && defined(C4_MSVC))
+// amalgamate: removed include of
+// https://github.com/biojppm/c4core/src/c4/windows.hpp
+//# include "c4/windows.hpp"
+#if !defined(C4_WINDOWS_HPP_) && !defined(_C4_WINDOWS_HPP_)
+#error "amalgamate: file c4/windows.hpp must have been included at this point"
+#endif /* C4_WINDOWS_HPP_ */
+
+#elif defined(C4_PS4)
+# include <libdbg.h>
+#elif defined(C4_UNIX) || defined(C4_LINUX)
+# include <sys/stat.h>
+//included above:
+//# include <cstring>
+# include <fcntl.h>
+#elif defined(C4_MACOS) || defined(C4_IOS)
+//included above:
+//# include <assert.h>
+# include <stdbool.h>
+# include <sys/types.h>
+# include <sys/sysctl.h>
+#endif
+// the amalgamation tool is dumb and was omitting this include under MACOS.
+// So do it only once:
+#if defined(C4_UNIX) || defined(C4_LINUX) || defined(C4_MACOS) || defined(C4_IOS)
+# include <unistd.h>
+#endif
+
+#if defined(C4_EXCEPTIONS_ENABLED) && defined(C4_ERROR_THROWS_EXCEPTION)
+# include <exception>
+#endif
+
+#ifdef __clang__
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wformat-nonliteral"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wformat-nonliteral"
+#endif
+
+
+//-----------------------------------------------------------------------------
+namespace c4 {
+
+static error_flags s_error_flags = ON_ERROR_DEFAULTS;
+static error_callback_type s_error_callback = nullptr;
+
+//-----------------------------------------------------------------------------
+
+error_flags get_error_flags()
+{
+ return s_error_flags;
+}
+void set_error_flags(error_flags flags)
+{
+ s_error_flags = flags;
+}
+
+error_callback_type get_error_callback()
+{
+ return s_error_callback;
+}
+/** Set the function which is called when an error occurs. */
+void set_error_callback(error_callback_type cb)
+{
+ s_error_callback = cb;
+}
+
+//-----------------------------------------------------------------------------
+
+void handle_error(srcloc where, const char *fmt, ...)
+{
+ char buf[1024];
+ size_t msglen = 0;
+ if(s_error_flags & (ON_ERROR_LOG|ON_ERROR_CALLBACK))
+ {
+ va_list args;
+ va_start(args, fmt);
+ int ilen = vsnprintf(buf, sizeof(buf), fmt, args); // ss.vprintf(fmt, args);
+ va_end(args);
+ msglen = ilen >= 0 && ilen < (int)sizeof(buf) ? static_cast<size_t>(ilen) : sizeof(buf)-1;
+ }
+
+ if(s_error_flags & ON_ERROR_LOG)
+ {
+ C4_LOGF_ERR("\n");
+#if defined(C4_ERROR_SHOWS_FILELINE) && defined(C4_ERROR_SHOWS_FUNC)
+ C4_LOGF_ERR("%s:%d: ERROR: %s\n", where.file, where.line, buf);
+ C4_LOGF_ERR("%s:%d: ERROR here: %s\n", where.file, where.line, where.func);
+#elif defined(C4_ERROR_SHOWS_FILELINE)
+ C4_LOGF_ERR("%s:%d: ERROR: %s\n", where.file, where.line, buf);
+#elif ! defined(C4_ERROR_SHOWS_FUNC)
+ C4_LOGF_ERR("ERROR: %s\n", buf);
+#endif
+ }
+
+ if(s_error_flags & ON_ERROR_CALLBACK)
+ {
+ if(s_error_callback)
+ {
+ s_error_callback(buf, msglen/*ss.c_strp(), ss.tellp()*/);
+ }
+ }
+
+ if(s_error_flags & ON_ERROR_ABORT)
+ {
+ abort();
+ }
+
+ if(s_error_flags & ON_ERROR_THROW)
+ {
+#if defined(C4_EXCEPTIONS_ENABLED) && defined(C4_ERROR_THROWS_EXCEPTION)
+ throw Exception(buf);
+#else
+ abort();
+#endif
+ }
+}
+
+//-----------------------------------------------------------------------------
+
+void handle_warning(srcloc where, const char *fmt, ...)
+{
+ va_list args;
+ char buf[1024]; //sstream<c4::string> ss;
+ va_start(args, fmt);
+ vsnprintf(buf, sizeof(buf), fmt, args);
+ va_end(args);
+ C4_LOGF_WARN("\n");
+#if defined(C4_ERROR_SHOWS_FILELINE) && defined(C4_ERROR_SHOWS_FUNC)
+ C4_LOGF_WARN("%s:%d: WARNING: %s\n", where.file, where.line, buf/*ss.c_strp()*/);
+ C4_LOGF_WARN("%s:%d: WARNING: here: %s\n", where.file, where.line, where.func);
+#elif defined(C4_ERROR_SHOWS_FILELINE)
+ C4_LOGF_WARN("%s:%d: WARNING: %s\n", where.file, where.line, buf/*ss.c_strp()*/);
+#elif ! defined(C4_ERROR_SHOWS_FUNC)
+ C4_LOGF_WARN("WARNING: %s\n", buf/*ss.c_strp()*/);
+#endif
+ //c4::log.flush();
+}
+
+//-----------------------------------------------------------------------------
+bool is_debugger_attached()
+{
+#if defined(C4_UNIX) || defined(C4_LINUX)
+ static bool first_call = true;
+ static bool first_call_result = false;
+ if(first_call)
+ {
+ first_call = false;
+ //! @see http://stackoverflow.com/questions/3596781/how-to-detect-if-the-current-process-is-being-run-by-gdb
+ //! (this answer: http://stackoverflow.com/a/24969863/3968589 )
+ char buf[1024] = "";
+
+ int status_fd = open("/proc/self/status", O_RDONLY);
+ if (status_fd == -1)
+ {
+ return 0;
+ }
+
+ ssize_t num_read = ::read(status_fd, buf, sizeof(buf));
+
+ if (num_read > 0)
+ {
+ static const char TracerPid[] = "TracerPid:";
+ char *tracer_pid;
+
+ if(num_read < 1024)
+ {
+ buf[num_read] = 0;
+ }
+ tracer_pid = strstr(buf, TracerPid);
+ if (tracer_pid)
+ {
+ first_call_result = !!::atoi(tracer_pid + sizeof(TracerPid) - 1);
+ }
+ }
+ }
+ return first_call_result;
+#elif defined(C4_PS4)
+ return (sceDbgIsDebuggerAttached() != 0);
+#elif defined(C4_XBOX) || (defined(C4_WIN) && defined(C4_MSVC))
+ return IsDebuggerPresent() != 0;
+#elif defined(C4_MACOS) || defined(C4_IOS)
+ // https://stackoverflow.com/questions/2200277/detecting-debugger-on-mac-os-x
+ // Returns true if the current process is being debugged (either
+ // running under the debugger or has a debugger attached post facto).
+ int junk;
+ int mib[4];
+ struct kinfo_proc info;
+ size_t size;
+
+ // Initialize the flags so that, if sysctl fails for some bizarre
+ // reason, we get a predictable result.
+
+ info.kp_proc.p_flag = 0;
+
+ // Initialize mib, which tells sysctl the info we want, in this case
+ // we're looking for information about a specific process ID.
+
+ mib[0] = CTL_KERN;
+ mib[1] = KERN_PROC;
+ mib[2] = KERN_PROC_PID;
+ mib[3] = getpid();
+
+ // Call sysctl.
+
+ size = sizeof(info);
+ junk = sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0);
+ assert(junk == 0);
+
+ // We're being debugged if the P_TRACED flag is set.
+ return ((info.kp_proc.p_flag & P_TRACED) != 0);
+#else
+ return false;
+#endif
+} // is_debugger_attached()
+
+} // namespace c4
+
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#endif /* C4CORE_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/c4core/src/c4/error.cpp)
+
+#endif /* _C4CORE_SINGLE_HEADER_AMALGAMATED_HPP_ */
+
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/c4core_all.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/export.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/export.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef C4_YML_EXPORT_HPP_
+#define C4_YML_EXPORT_HPP_
+
+#ifdef _WIN32
+ #ifdef RYML_SHARED
+ #ifdef RYML_EXPORTS
+ #define RYML_EXPORT __declspec(dllexport)
+ #else
+ #define RYML_EXPORT __declspec(dllimport)
+ #endif
+ #else
+ #define RYML_EXPORT
+ #endif
+#else
+ #define RYML_EXPORT
+#endif
+
+#endif /* C4_YML_EXPORT_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/export.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/common.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/common.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_COMMON_HPP_
+#define _C4_YML_COMMON_HPP_
+
+//included above:
+//#include <cstddef>
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/substr.hpp
+//#include <c4/substr.hpp>
+#if !defined(C4_SUBSTR_HPP_) && !defined(_C4_SUBSTR_HPP_)
+#error "amalgamate: file c4/substr.hpp must have been included at this point"
+#endif /* C4_SUBSTR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/export.hpp
+//#include <c4/yml/export.hpp>
+#if !defined(C4_YML_EXPORT_HPP_) && !defined(_C4_YML_EXPORT_HPP_)
+#error "amalgamate: file c4/yml/export.hpp must have been included at this point"
+#endif /* C4_YML_EXPORT_HPP_ */
+
+
+
+#ifndef RYML_USE_ASSERT
+# define RYML_USE_ASSERT C4_USE_ASSERT
+#endif
+
+
+#if RYML_USE_ASSERT
+# define RYML_ASSERT(cond) RYML_CHECK(cond)
+# define RYML_ASSERT_MSG(cond, msg) RYML_CHECK_MSG(cond, msg)
+#else
+# define RYML_ASSERT(cond)
+# define RYML_ASSERT_MSG(cond, msg)
+#endif
+
+
+#define RYML_CHECK(cond) \
+ do { \
+ if(!(cond)) \
+ { \
+ C4_DEBUG_BREAK(); \
+ c4::yml::error("check failed: " #cond, c4::yml::Location(__FILE__, __LINE__, 0)); \
+ } \
+ } while(0)
+
+#define RYML_CHECK_MSG(cond, msg) \
+ do \
+ { \
+ if(!(cond)) \
+ { \
+ C4_DEBUG_BREAK(); \
+ c4::yml::error(msg ": check failed: " #cond, c4::yml::Location(__FILE__, __LINE__, 0)); \
+ } \
+ } while(0)
+
+
+#if C4_CPP >= 14
+# define RYML_DEPRECATED(msg) [[deprecated(msg)]]
+#else
+# if defined(_MSC_VER)
+# define RYML_DEPRECATED(msg) __declspec(deprecated)
+# else // defined(__GNUC__) || defined(__clang__)
+# define RYML_DEPRECATED(msg) __attribute__((deprecated))
+# endif
+#endif
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+namespace c4 {
+namespace yml {
+
+enum : size_t {
+ /** a null position */
+ npos = size_t(-1),
+ /** an index to none */
+ NONE = size_t(-1)
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+//! holds a position into a source buffer
+struct RYML_EXPORT LineCol
+{
+ //! number of bytes from the beginning of the source buffer
+ size_t offset;
+ //! line
+ size_t line;
+ //! column
+ size_t col;
+
+ LineCol() : offset(), line(), col() {}
+ //! construct from line and column
+ LineCol(size_t l, size_t c) : offset(0), line(l), col(c) {}
+ //! construct from offset, line and column
+ LineCol(size_t o, size_t l, size_t c) : offset(o), line(l), col(c) {}
+};
+
+
+//! a source file position
+struct RYML_EXPORT Location : public LineCol
+{
+ csubstr name;
+
+ operator bool () const { return !name.empty() || line != 0 || offset != 0; }
+
+ Location() : LineCol(), name() {}
+ Location( size_t l, size_t c) : LineCol{ l, c}, name( ) {}
+ Location( csubstr n, size_t l, size_t c) : LineCol{ l, c}, name(n) {}
+ Location( csubstr n, size_t b, size_t l, size_t c) : LineCol{b, l, c}, name(n) {}
+ Location(const char *n, size_t l, size_t c) : LineCol{ l, c}, name(to_csubstr(n)) {}
+ Location(const char *n, size_t b, size_t l, size_t c) : LineCol{b, l, c}, name(to_csubstr(n)) {}
+};
+
+
+//-----------------------------------------------------------------------------
+
+/** the type of the function used to report errors. This function must
+ * interrupt execution, either by raising an exception or calling
+ * std::abort(). */
+using pfn_error = void (*)(const char* msg, size_t msg_len, Location location, void *user_data);
+/** the type of the function used to allocate memory */
+using pfn_allocate = void* (*)(size_t len, void* hint, void *user_data);
+/** the type of the function used to free memory */
+using pfn_free = void (*)(void* mem, size_t size, void *user_data);
+
+/** trigger an error: call the current error callback. */
+RYML_EXPORT void error(const char *msg, size_t msg_len, Location loc);
+/** @overload error */
+inline void error(const char *msg, size_t msg_len)
+{
+ error(msg, msg_len, Location{});
+}
+/** @overload error */
+template<size_t N>
+inline void error(const char (&msg)[N], Location loc)
+{
+ error(msg, N-1, loc);
+}
+/** @overload error */
+template<size_t N>
+inline void error(const char (&msg)[N])
+{
+ error(msg, N-1, Location{});
+}
+
+//-----------------------------------------------------------------------------
+
+/// a c-style callbacks class
+struct RYML_EXPORT Callbacks
+{
+ void * m_user_data;
+ pfn_allocate m_allocate;
+ pfn_free m_free;
+ pfn_error m_error;
+
+ Callbacks();
+ Callbacks(void *user_data, pfn_allocate alloc, pfn_free free, pfn_error error_);
+
+ bool operator!= (Callbacks const& that) const { return !operator==(that); }
+ bool operator== (Callbacks const& that) const
+ {
+ return (m_user_data == that.m_user_data &&
+ m_allocate == that.m_allocate &&
+ m_free == that.m_free &&
+ m_error == that.m_error);
+ }
+};
+
+/// get the global callbacks
+RYML_EXPORT Callbacks const& get_callbacks();
+/// set the global callbacks
+RYML_EXPORT void set_callbacks(Callbacks const& c);
+/// set the global callbacks to their defaults
+RYML_EXPORT void reset_callbacks();
+
+/// @cond dev
+#define _RYML_CB_ERR(cb, msg_literal) \
+do \
+{ \
+ const char msg[] = msg_literal; \
+ C4_DEBUG_BREAK(); \
+ (cb).m_error(msg, sizeof(msg), c4::yml::Location(__FILE__, 0, __LINE__, 0), (cb).m_user_data); \
+} while(0)
+#define _RYML_CB_CHECK(cb, cond) \
+ do \
+ { \
+ if(!(cond)) \
+ { \
+ const char msg[] = "check failed: " #cond; \
+ C4_DEBUG_BREAK(); \
+ (cb).m_error(msg, sizeof(msg), c4::yml::Location(__FILE__, 0, __LINE__, 0), (cb).m_user_data); \
+ } \
+ } while(0)
+#ifdef RYML_USE_ASSERT
+#define _RYML_CB_ASSERT(cb, cond) _RYML_CB_CHECK((cb), (cond))
+#else
+#define _RYML_CB_ASSERT(cb, cond) do {} while(0)
+#endif
+#define _RYML_CB_ALLOC_HINT(cb, T, num, hint) (T*) (cb).m_allocate((num) * sizeof(T), (hint), (cb).m_user_data)
+#define _RYML_CB_ALLOC(cb, T, num) _RYML_CB_ALLOC_HINT((cb), (T), (num), nullptr)
+#define _RYML_CB_FREE(cb, buf, T, num) \
+ do { \
+ (cb).m_free((buf), (num) * sizeof(T), (cb).m_user_data); \
+ (buf) = nullptr; \
+ } while(0)
+
+
+
+namespace detail {
+template<int8_t signedval, uint8_t unsignedval>
+struct _charconstant_t
+ : public std::conditional<std::is_signed<char>::value,
+ std::integral_constant<int8_t, signedval>,
+ std::integral_constant<uint8_t, unsignedval>>::type
+{};
+#define _RYML_CHCONST(signedval, unsignedval) ::c4::yml::detail::_charconstant_t<INT8_C(signedval), UINT8_C(unsignedval)>::value
+} // namespace detail
+
+
+namespace detail {
+struct _SubstrWriter
+{
+ substr buf;
+ size_t pos;
+ _SubstrWriter(substr buf_, size_t pos_=0) : buf(buf_), pos(pos_) {}
+ void append(csubstr s)
+ {
+ C4_ASSERT(!s.overlaps(buf));
+ if(pos + s.len <= buf.len)
+ memcpy(buf.str + pos, s.str, s.len);
+ pos += s.len;
+ }
+ void append(char c)
+ {
+ if(pos < buf.len)
+ buf.str[pos] = c;
+ ++pos;
+ }
+ void append_n(char c, size_t numtimes)
+ {
+ if(pos + numtimes < buf.len)
+ memset(buf.str + pos, c, numtimes);
+ pos += numtimes;
+ }
+ size_t slack() const { return pos <= buf.len ? buf.len - pos : 0; }
+ size_t excess() const { return pos > buf.len ? pos - buf.len : 0; }
+ //! get the part written so far
+ csubstr curr() const { return pos <= buf.len ? buf.first(pos) : buf; }
+ //! get the part that is still free to write to (the remainder)
+ substr rem() { return pos < buf.len ? buf.sub(pos) : buf.last(0); }
+
+ size_t advance(size_t more) { pos += more; return pos; }
+};
+} // namespace detail
+
+/// @endcond
+
+} // namespace yml
+} // namespace c4
+
+#endif /* _C4_YML_COMMON_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/common.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/tree.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/tree.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_TREE_HPP_
+#define _C4_YML_TREE_HPP_
+
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/types.hpp
+//#include "c4/types.hpp"
+#if !defined(C4_TYPES_HPP_) && !defined(_C4_TYPES_HPP_)
+#error "amalgamate: file c4/types.hpp must have been included at this point"
+#endif /* C4_TYPES_HPP_ */
+
+#ifndef _C4_YML_COMMON_HPP_
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/common.hpp
+//#include "c4/yml/common.hpp"
+#if !defined(C4_YML_COMMON_HPP_) && !defined(_C4_YML_COMMON_HPP_)
+#error "amalgamate: file c4/yml/common.hpp must have been included at this point"
+#endif /* C4_YML_COMMON_HPP_ */
+
+#endif
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/charconv.hpp
+//#include <c4/charconv.hpp>
+#if !defined(C4_CHARCONV_HPP_) && !defined(_C4_CHARCONV_HPP_)
+#error "amalgamate: file c4/charconv.hpp must have been included at this point"
+#endif /* C4_CHARCONV_HPP_ */
+
+//included above:
+//#include <cmath>
+//included above:
+//#include <limits>
+
+
+C4_SUPPRESS_WARNING_MSVC_PUSH
+C4_SUPPRESS_WARNING_MSVC(4251) // needs to have dll-interface to be used by clients of struct
+C4_SUPPRESS_WARNING_MSVC(4296) // expression is always 'boolean_value'
+C4_SUPPRESS_WARNING_GCC_CLANG_PUSH
+C4_SUPPRESS_WARNING_GCC("-Wtype-limits")
+
+
+namespace c4 {
+namespace yml {
+
+struct NodeScalar;
+struct NodeInit;
+struct NodeData;
+class NodeRef;
+class Tree;
+
+
+/** encode a floating point value to a string. */
+template<class T>
+size_t to_chars_float(substr buf, T val)
+{
+ C4_SUPPRESS_WARNING_GCC_CLANG_WITH_PUSH("-Wfloat-equal");
+ static_assert(std::is_floating_point<T>::value, "must be floating point");
+ if(C4_UNLIKELY(std::isnan(val)))
+ return to_chars(buf, csubstr(".nan"));
+ else if(C4_UNLIKELY(val == std::numeric_limits<T>::infinity()))
+ return to_chars(buf, csubstr(".inf"));
+ else if(C4_UNLIKELY(val == -std::numeric_limits<T>::infinity()))
+ return to_chars(buf, csubstr("-.inf"));
+ return to_chars(buf, val);
+ C4_SUPPRESS_WARNING_GCC_CLANG_POP
+}
+
+
+/** decode a floating point from string. Accepts special values: .nan,
+ * .inf, -.inf */
+template<class T>
+bool from_chars_float(csubstr buf, T *C4_RESTRICT val)
+{
+ static_assert(std::is_floating_point<T>::value, "must be floating point");
+ if(C4_LIKELY(from_chars(buf, val)))
+ {
+ return true;
+ }
+ else if(C4_UNLIKELY(buf == ".nan" || buf == ".NaN" || buf == ".NAN"))
+ {
+ *val = std::numeric_limits<T>::quiet_NaN();
+ return true;
+ }
+ else if(C4_UNLIKELY(buf == ".inf" || buf == ".Inf" || buf == ".INF"))
+ {
+ *val = std::numeric_limits<T>::infinity();
+ return true;
+ }
+ else if(C4_UNLIKELY(buf == "-.inf" || buf == "-.Inf" || buf == "-.INF"))
+ {
+ *val = -std::numeric_limits<T>::infinity();
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** the integral type necessary to cover all the bits marking node tags */
+using tag_bits = uint16_t;
+
+/** a bit mask for marking tags for types */
+typedef enum : tag_bits {
+ // container types
+ TAG_NONE = 0,
+ TAG_MAP = 1, /**< !!map Unordered set of key: value pairs without duplicates. @see https://yaml.org/type/map.html */
+ TAG_OMAP = 2, /**< !!omap Ordered sequence of key: value pairs without duplicates. @see https://yaml.org/type/omap.html */
+ TAG_PAIRS = 3, /**< !!pairs Ordered sequence of key: value pairs allowing duplicates. @see https://yaml.org/type/pairs.html */
+ TAG_SET = 4, /**< !!set Unordered set of non-equal values. @see https://yaml.org/type/set.html */
+ TAG_SEQ = 5, /**< !!seq Sequence of arbitrary values. @see https://yaml.org/type/seq.html */
+ // scalar types
+ TAG_BINARY = 6, /**< !!binary A sequence of zero or more octets (8 bit values). @see https://yaml.org/type/binary.html */
+ TAG_BOOL = 7, /**< !!bool Mathematical Booleans. @see https://yaml.org/type/bool.html */
+ TAG_FLOAT = 8, /**< !!float Floating-point approximation to real numbers. https://yaml.org/type/float.html */
+ TAG_INT = 9, /**< !!float Mathematical integers. https://yaml.org/type/int.html */
+ TAG_MERGE = 10, /**< !!merge Specify one or more mapping to be merged with the current one. https://yaml.org/type/merge.html */
+ TAG_NULL = 11, /**< !!null Devoid of value. https://yaml.org/type/null.html */
+ TAG_STR = 12, /**< !!str A sequence of zero or more Unicode characters. https://yaml.org/type/str.html */
+ TAG_TIMESTAMP = 13, /**< !!timestamp A point in time https://yaml.org/type/timestamp.html */
+ TAG_VALUE = 14, /**< !!value Specify the default value of a mapping https://yaml.org/type/value.html */
+ TAG_YAML = 15, /**< !!yaml Specify the default value of a mapping https://yaml.org/type/yaml.html */
+} YamlTag_e;
+
+YamlTag_e to_tag(csubstr tag);
+csubstr from_tag(YamlTag_e tag);
+csubstr from_tag_long(YamlTag_e tag);
+csubstr normalize_tag(csubstr tag);
+csubstr normalize_tag_long(csubstr tag);
+
+struct TagDirective
+{
+ /** Eg `!e!` in `%TAG !e! tag:example.com,2000:app/` */
+ csubstr handle;
+ /** Eg `tag:example.com,2000:app/` in `%TAG !e! tag:example.com,2000:app/` */
+ csubstr prefix;
+ /** The next node to which this tag directive applies */
+ size_t next_node_id;
+};
+
+#ifndef RYML_MAX_TAG_DIRECTIVES
+/** the maximum number of tag directives in a Tree */
+#define RYML_MAX_TAG_DIRECTIVES 4
+#endif
+
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+
+/** the integral type necessary to cover all the bits marking node types */
+using type_bits = uint64_t;
+
+
+/** a bit mask for marking node types */
+typedef enum : type_bits {
+ // a convenience define, undefined below
+ #define c4bit(v) (type_bits(1) << v)
+ NOTYPE = 0, ///< no node type is set
+ VAL = c4bit(0), ///< a leaf node, has a (possibly empty) value
+ KEY = c4bit(1), ///< is member of a map, must have non-empty key
+ MAP = c4bit(2), ///< a map: a parent of keyvals
+ SEQ = c4bit(3), ///< a seq: a parent of vals
+ DOC = c4bit(4), ///< a document
+ STREAM = c4bit(5)|SEQ, ///< a stream: a seq of docs
+ KEYREF = c4bit(6), ///< a *reference: the key references an &anchor
+ VALREF = c4bit(7), ///< a *reference: the val references an &anchor
+ KEYANCH = c4bit(8), ///< the key has an &anchor
+ VALANCH = c4bit(9), ///< the val has an &anchor
+ KEYTAG = c4bit(10), ///< the key has an explicit tag/type
+ VALTAG = c4bit(11), ///< the val has an explicit tag/type
+ _TYMASK = c4bit(12)-1, // all the bits up to here
+ VALQUO = c4bit(12), ///< the val is quoted by '', "", > or |
+ KEYQUO = c4bit(13), ///< the key is quoted by '', "", > or |
+ KEYVAL = KEY|VAL,
+ KEYSEQ = KEY|SEQ,
+ KEYMAP = KEY|MAP,
+ DOCMAP = DOC|MAP,
+ DOCSEQ = DOC|SEQ,
+ DOCVAL = DOC|VAL,
+ // these flags are from a work in progress and should not be used yet
+ _WIP_STYLE_FLOW_SL = c4bit(14), ///< mark container with single-line flow format (seqs as '[val1,val2], maps as '{key: val, key2: val2}')
+ _WIP_STYLE_FLOW_ML = c4bit(15), ///< mark container with multi-line flow format (seqs as '[val1,\nval2], maps as '{key: val,\nkey2: val2}')
+ _WIP_STYLE_BLOCK = c4bit(16), ///< mark container with block format (seqs as '- val\n', maps as 'key: val')
+ _WIP_KEY_LITERAL = c4bit(17), ///< mark key scalar as multiline, block literal |
+ _WIP_VAL_LITERAL = c4bit(18), ///< mark val scalar as multiline, block literal |
+ _WIP_KEY_FOLDED = c4bit(19), ///< mark key scalar as multiline, block folded >
+ _WIP_VAL_FOLDED = c4bit(20), ///< mark val scalar as multiline, block folded >
+ _WIP_KEY_SQUO = c4bit(21), ///< mark key scalar as single quoted
+ _WIP_VAL_SQUO = c4bit(22), ///< mark val scalar as single quoted
+ _WIP_KEY_DQUO = c4bit(23), ///< mark key scalar as double quoted
+ _WIP_VAL_DQUO = c4bit(24), ///< mark val scalar as double quoted
+ _WIP_KEY_PLAIN = c4bit(25), ///< mark key scalar as plain scalar (unquoted, even when multiline)
+ _WIP_VAL_PLAIN = c4bit(26), ///< mark val scalar as plain scalar (unquoted, even when multiline)
+ _WIP_KEY_STYLE = _WIP_KEY_LITERAL|_WIP_KEY_FOLDED|_WIP_KEY_SQUO|_WIP_KEY_DQUO|_WIP_KEY_PLAIN,
+ _WIP_VAL_STYLE = _WIP_VAL_LITERAL|_WIP_VAL_FOLDED|_WIP_VAL_SQUO|_WIP_VAL_DQUO|_WIP_VAL_PLAIN,
+ _WIP_KEY_FT_NL = c4bit(27), ///< features: mark key scalar as having \n in its contents
+ _WIP_VAL_FT_NL = c4bit(28), ///< features: mark val scalar as having \n in its contents
+ _WIP_KEY_FT_SQ = c4bit(29), ///< features: mark key scalar as having single quotes in its contents
+ _WIP_VAL_FT_SQ = c4bit(30), ///< features: mark val scalar as having single quotes in its contents
+ _WIP_KEY_FT_DQ = c4bit(31), ///< features: mark key scalar as having double quotes in its contents
+ _WIP_VAL_FT_DQ = c4bit(32), ///< features: mark val scalar as having double quotes in its contents
+ #undef c4bit
+} NodeType_e;
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** wraps a NodeType_e element with some syntactic sugar and predicates */
+struct NodeType
+{
+public:
+
+ NodeType_e type;
+
+public:
+
+ C4_ALWAYS_INLINE operator NodeType_e & C4_RESTRICT () { return type; }
+ C4_ALWAYS_INLINE operator NodeType_e const& C4_RESTRICT () const { return type; }
+
+ C4_ALWAYS_INLINE NodeType() : type(NOTYPE) {}
+ C4_ALWAYS_INLINE NodeType(NodeType_e t) : type(t) {}
+ C4_ALWAYS_INLINE NodeType(type_bits t) : type((NodeType_e)t) {}
+
+ C4_ALWAYS_INLINE const char *type_str() const { return type_str(type); }
+ static const char* type_str(NodeType_e t);
+
+ C4_ALWAYS_INLINE void set(NodeType_e t) { type = t; }
+ C4_ALWAYS_INLINE void set(type_bits t) { type = (NodeType_e)t; }
+
+ C4_ALWAYS_INLINE void add(NodeType_e t) { type = (NodeType_e)(type|t); }
+ C4_ALWAYS_INLINE void add(type_bits t) { type = (NodeType_e)(type|t); }
+
+ C4_ALWAYS_INLINE void rem(NodeType_e t) { type = (NodeType_e)(type & ~t); }
+ C4_ALWAYS_INLINE void rem(type_bits t) { type = (NodeType_e)(type & ~t); }
+
+ C4_ALWAYS_INLINE void clear() { type = NOTYPE; }
+
+public:
+
+ #if defined(__clang__)
+ # pragma clang diagnostic push
+ # pragma clang diagnostic ignored "-Wnull-dereference"
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic push
+ # if __GNUC__ >= 6
+ # pragma GCC diagnostic ignored "-Wnull-dereference"
+ # endif
+ #endif
+
+ C4_ALWAYS_INLINE bool is_stream() const { return ((type & STREAM) == STREAM) != 0; }
+ C4_ALWAYS_INLINE bool is_doc() const { return (type & DOC) != 0; }
+ C4_ALWAYS_INLINE bool is_container() const { return (type & (MAP|SEQ|STREAM)) != 0; }
+ C4_ALWAYS_INLINE bool is_map() const { return (type & MAP) != 0; }
+ C4_ALWAYS_INLINE bool is_seq() const { return (type & SEQ) != 0; }
+ C4_ALWAYS_INLINE bool has_val() const { return (type & VAL) != 0; }
+ C4_ALWAYS_INLINE bool has_key() const { return (type & KEY) != 0; }
+ C4_ALWAYS_INLINE bool is_val() const { return (type & (KEYVAL)) == VAL; }
+ C4_ALWAYS_INLINE bool is_keyval() const { return (type & KEYVAL) == KEYVAL; }
+ C4_ALWAYS_INLINE bool has_key_tag() const { return (type & (KEY|KEYTAG)) == (KEY|KEYTAG); }
+ C4_ALWAYS_INLINE bool has_val_tag() const { return ((type & (VALTAG)) && (type & (VAL|MAP|SEQ))); }
+ C4_ALWAYS_INLINE bool has_key_anchor() const { return (type & (KEY|KEYANCH)) == (KEY|KEYANCH); }
+ C4_ALWAYS_INLINE bool is_key_anchor() const { return (type & (KEY|KEYANCH)) == (KEY|KEYANCH); }
+ C4_ALWAYS_INLINE bool has_val_anchor() const { return (type & VALANCH) != 0 && (type & (VAL|SEQ|MAP)) != 0; }
+ C4_ALWAYS_INLINE bool is_val_anchor() const { return (type & VALANCH) != 0 && (type & (VAL|SEQ|MAP)) != 0; }
+ C4_ALWAYS_INLINE bool has_anchor() const { return (type & (KEYANCH|VALANCH)) != 0; }
+ C4_ALWAYS_INLINE bool is_anchor() const { return (type & (KEYANCH|VALANCH)) != 0; }
+ C4_ALWAYS_INLINE bool is_key_ref() const { return (type & KEYREF) != 0; }
+ C4_ALWAYS_INLINE bool is_val_ref() const { return (type & VALREF) != 0; }
+ C4_ALWAYS_INLINE bool is_ref() const { return (type & (KEYREF|VALREF)) != 0; }
+ C4_ALWAYS_INLINE bool is_anchor_or_ref() const { return (type & (KEYANCH|VALANCH|KEYREF|VALREF)) != 0; }
+ C4_ALWAYS_INLINE bool is_key_quoted() const { return (type & (KEY|KEYQUO)) == (KEY|KEYQUO); }
+ C4_ALWAYS_INLINE bool is_val_quoted() const { return (type & (VAL|VALQUO)) == (VAL|VALQUO); }
+ C4_ALWAYS_INLINE bool is_quoted() const { return (type & (KEY|KEYQUO)) == (KEY|KEYQUO) || (type & (VAL|VALQUO)) == (VAL|VALQUO); }
+
+ // these predicates are a work in progress and subject to change. Don't use yet.
+ C4_ALWAYS_INLINE bool default_block() const { return (type & (_WIP_STYLE_BLOCK|_WIP_STYLE_FLOW_ML|_WIP_STYLE_FLOW_SL)) == 0; }
+ C4_ALWAYS_INLINE bool marked_block() const { return (type & (_WIP_STYLE_BLOCK)) != 0; }
+ C4_ALWAYS_INLINE bool marked_flow_sl() const { return (type & (_WIP_STYLE_FLOW_SL)) != 0; }
+ C4_ALWAYS_INLINE bool marked_flow_ml() const { return (type & (_WIP_STYLE_FLOW_ML)) != 0; }
+ C4_ALWAYS_INLINE bool marked_flow() const { return (type & (_WIP_STYLE_FLOW_ML|_WIP_STYLE_FLOW_SL)) != 0; }
+ C4_ALWAYS_INLINE bool key_marked_literal() const { return (type & (_WIP_KEY_LITERAL)) != 0; }
+ C4_ALWAYS_INLINE bool val_marked_literal() const { return (type & (_WIP_VAL_LITERAL)) != 0; }
+ C4_ALWAYS_INLINE bool key_marked_folded() const { return (type & (_WIP_KEY_FOLDED)) != 0; }
+ C4_ALWAYS_INLINE bool val_marked_folded() const { return (type & (_WIP_VAL_FOLDED)) != 0; }
+ C4_ALWAYS_INLINE bool key_marked_squo() const { return (type & (_WIP_KEY_SQUO)) != 0; }
+ C4_ALWAYS_INLINE bool val_marked_squo() const { return (type & (_WIP_VAL_SQUO)) != 0; }
+ C4_ALWAYS_INLINE bool key_marked_dquo() const { return (type & (_WIP_KEY_DQUO)) != 0; }
+ C4_ALWAYS_INLINE bool val_marked_dquo() const { return (type & (_WIP_VAL_DQUO)) != 0; }
+ C4_ALWAYS_INLINE bool key_marked_plain() const { return (type & (_WIP_KEY_PLAIN)) != 0; }
+ C4_ALWAYS_INLINE bool val_marked_plain() const { return (type & (_WIP_VAL_PLAIN)) != 0; }
+
+ #if defined(__clang__)
+ # pragma clang diagnostic pop
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic pop
+ #endif
+
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** a node scalar is a csubstr, which may be tagged and anchored. */
+struct NodeScalar
+{
+ csubstr tag;
+ csubstr scalar;
+ csubstr anchor;
+
+public:
+
+ /// initialize as an empty scalar
+ inline NodeScalar() noexcept : tag(), scalar(), anchor() {}
+
+ /// initialize as an untagged scalar
+ template<size_t N>
+ inline NodeScalar(const char (&s)[N]) noexcept : tag(), scalar(s), anchor() {}
+ inline NodeScalar(csubstr s ) noexcept : tag(), scalar(s), anchor() {}
+
+ /// initialize as a tagged scalar
+ template<size_t N, size_t M>
+ inline NodeScalar(const char (&t)[N], const char (&s)[N]) noexcept : tag(t), scalar(s), anchor() {}
+ inline NodeScalar(csubstr t , csubstr s ) noexcept : tag(t), scalar(s), anchor() {}
+
+public:
+
+ ~NodeScalar() noexcept = default;
+ NodeScalar(NodeScalar &&) noexcept = default;
+ NodeScalar(NodeScalar const&) noexcept = default;
+ NodeScalar& operator= (NodeScalar &&) noexcept = default;
+ NodeScalar& operator= (NodeScalar const&) noexcept = default;
+
+public:
+
+ bool empty() const noexcept { return tag.empty() && scalar.empty() && anchor.empty(); }
+
+ void clear() noexcept { tag.clear(); scalar.clear(); anchor.clear(); }
+
+ void set_ref_maybe_replacing_scalar(csubstr ref, bool has_scalar) noexcept
+ {
+ csubstr trimmed = ref.begins_with('*') ? ref.sub(1) : ref;
+ anchor = trimmed;
+ if((!has_scalar) || !scalar.ends_with(trimmed))
+ scalar = ref;
+ }
+};
+C4_MUST_BE_TRIVIAL_COPY(NodeScalar);
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** convenience class to initialize nodes */
+struct NodeInit
+{
+
+ NodeType type;
+ NodeScalar key;
+ NodeScalar val;
+
+public:
+
+ /// initialize as an empty node
+ NodeInit() : type(NOTYPE), key(), val() {}
+ /// initialize as a typed node
+ NodeInit(NodeType_e t) : type(t), key(), val() {}
+ /// initialize as a sequence member
+ NodeInit(NodeScalar const& v) : type(VAL), key(), val(v) { _add_flags(); }
+ /// initialize as a mapping member
+ NodeInit( NodeScalar const& k, NodeScalar const& v) : type(KEYVAL), key(k.tag, k.scalar), val(v.tag, v.scalar) { _add_flags(); }
+ /// initialize as a mapping member with explicit type
+ NodeInit(NodeType_e t, NodeScalar const& k, NodeScalar const& v) : type(t ), key(k.tag, k.scalar), val(v.tag, v.scalar) { _add_flags(); }
+ /// initialize as a mapping member with explicit type (eg SEQ or MAP)
+ NodeInit(NodeType_e t, NodeScalar const& k ) : type(t ), key(k.tag, k.scalar), val( ) { _add_flags(KEY); }
+
+public:
+
+ void clear()
+ {
+ type.clear();
+ key.clear();
+ val.clear();
+ }
+
+ void _add_flags(type_bits more_flags=0)
+ {
+ type = (type|more_flags);
+ if( ! key.tag.empty())
+ type = (type|KEYTAG);
+ if( ! val.tag.empty())
+ type = (type|VALTAG);
+ if( ! key.anchor.empty())
+ type = (type|KEYANCH);
+ if( ! val.anchor.empty())
+ type = (type|VALANCH);
+ }
+
+ bool _check() const
+ {
+ // key cannot be empty
+ RYML_ASSERT(key.scalar.empty() == ((type & KEY) == 0));
+ // key tag cannot be empty
+ RYML_ASSERT(key.tag.empty() == ((type & KEYTAG) == 0));
+ // val may be empty even though VAL is set. But when VAL is not set, val must be empty
+ RYML_ASSERT(((type & VAL) != 0) || val.scalar.empty());
+ // val tag cannot be empty
+ RYML_ASSERT(val.tag.empty() == ((type & VALTAG) == 0));
+ return true;
+ }
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** contains the data for each YAML node. */
+struct NodeData
+{
+ NodeType m_type;
+
+ NodeScalar m_key;
+ NodeScalar m_val;
+
+ size_t m_parent;
+ size_t m_first_child;
+ size_t m_last_child;
+ size_t m_next_sibling;
+ size_t m_prev_sibling;
+};
+C4_MUST_BE_TRIVIAL_COPY(NodeData);
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+class RYML_EXPORT Tree
+{
+public:
+
+ /** @name construction and assignment */
+ /** @{ */
+
+ Tree() : Tree(get_callbacks()) {}
+ Tree(Callbacks const& cb);
+ Tree(size_t node_capacity, size_t arena_capacity=0) : Tree(node_capacity, arena_capacity, get_callbacks()) {}
+ Tree(size_t node_capacity, size_t arena_capacity, Callbacks const& cb);
+
+ ~Tree();
+
+ Tree(Tree const& that) noexcept;
+ Tree(Tree && that) noexcept;
+
+ Tree& operator= (Tree const& that) noexcept;
+ Tree& operator= (Tree && that) noexcept;
+
+ /** @} */
+
+public:
+
+ /** @name memory and sizing */
+ /** @{ */
+
+ void reserve(size_t node_capacity);
+
+ /** clear the tree and zero every node
+ * @note does NOT clear the arena
+ * @see clear_arena() */
+ void clear();
+ inline void clear_arena() { m_arena_pos = 0; }
+
+ inline bool empty() const { return m_size == 0; }
+
+ inline size_t size () const { return m_size; }
+ inline size_t capacity() const { return m_cap; }
+ inline size_t slack() const { RYML_ASSERT(m_cap >= m_size); return m_cap - m_size; }
+
+ inline size_t arena_size() const { return m_arena_pos; }
+ inline size_t arena_capacity() const { return m_arena.len; }
+ inline size_t arena_slack() const { RYML_ASSERT(m_arena.len >= m_arena_pos); return m_arena.len - m_arena_pos; }
+
+ Callbacks const& callbacks() const { return m_callbacks; }
+ void callbacks(Callbacks const& cb) { m_callbacks = cb; }
+
+ /** @} */
+
+public:
+
+ /** @name node getters */
+ /** @{ */
+
+ //! get the index of a node belonging to this tree.
+ //! @p n can be nullptr, in which case a
+ size_t id(NodeData const* n) const
+ {
+ if( ! n)
+ {
+ return NONE;
+ }
+ RYML_ASSERT(n >= m_buf && n < m_buf + m_cap);
+ return static_cast<size_t>(n - m_buf);
+ }
+
+ //! get a pointer to a node's NodeData.
+ //! i can be NONE, in which case a nullptr is returned
+ inline NodeData *get(size_t i)
+ {
+ if(i == NONE)
+ return nullptr;
+ RYML_ASSERT(i >= 0 && i < m_cap);
+ return m_buf + i;
+ }
+ //! get a pointer to a node's NodeData.
+ //! i can be NONE, in which case a nullptr is returned.
+ inline NodeData const *get(size_t i) const
+ {
+ if(i == NONE)
+ return nullptr;
+ RYML_ASSERT(i >= 0 && i < m_cap);
+ return m_buf + i;
+ }
+
+ //! An if-less form of get() that demands a valid node index.
+ //! This function is implementation only; use at your own risk.
+ inline NodeData * _p(size_t i) { RYML_ASSERT(i != NONE && i >= 0 && i < m_cap); return m_buf + i; }
+ //! An if-less form of get() that demands a valid node index.
+ //! This function is implementation only; use at your own risk.
+ inline NodeData const * _p(size_t i) const { RYML_ASSERT(i != NONE && i >= 0 && i < m_cap); return m_buf + i; }
+
+ //! Get the id of the root node
+ size_t root_id() { if(m_cap == 0) { reserve(16); } RYML_ASSERT(m_cap > 0 && m_size > 0); return 0; }
+ //! Get the id of the root node
+ size_t root_id() const { RYML_ASSERT(m_cap > 0 && m_size > 0); return 0; }
+
+ //! Get a NodeRef of a node by id
+ NodeRef ref(size_t id);
+ //! Get a NodeRef of a node by id
+ NodeRef const ref(size_t id) const;
+
+ //! Get the root as a NodeRef
+ NodeRef rootref();
+ //! Get the root as a NodeRef
+ NodeRef const rootref() const;
+
+ //! find a root child by name, return it as a NodeRef
+ //! @note requires the root to be a map.
+ NodeRef operator[] (csubstr key);
+ //! find a root child by name, return it as a NodeRef
+ //! @note requires the root to be a map.
+ NodeRef const operator[] (csubstr key) const;
+
+ //! find a root child by index: return the root node's @p i-th child as a NodeRef
+ //! @note @i is NOT the node id, but the child's position
+ NodeRef operator[] (size_t i);
+ //! find a root child by index: return the root node's @p i-th child as a NodeRef
+ //! @note @i is NOT the node id, but the child's position
+ NodeRef const operator[] (size_t i) const;
+
+ //! get the i-th document of the stream
+ //! @note @i is NOT the node id, but the doc position within the stream
+ NodeRef docref(size_t i);
+ //! get the i-th document of the stream
+ //! @note @i is NOT the node id, but the doc position within the stream
+ NodeRef const docref(size_t i) const;
+
+ /** @} */
+
+public:
+
+ /** @name node property getters */
+ /** @{ */
+
+ NodeType type(size_t node) const { return _p(node)->m_type; }
+ const char* type_str(size_t node) const { return NodeType::type_str(_p(node)->m_type); }
+
+ csubstr const& key (size_t node) const { RYML_ASSERT(has_key(node)); return _p(node)->m_key.scalar; }
+ csubstr const& key_tag (size_t node) const { RYML_ASSERT(has_key_tag(node)); return _p(node)->m_key.tag; }
+ csubstr const& key_ref (size_t node) const { RYML_ASSERT(is_key_ref(node) && ! has_key_anchor(node)); return _p(node)->m_key.anchor; }
+ csubstr const& key_anchor(size_t node) const { RYML_ASSERT( ! is_key_ref(node) && has_key_anchor(node)); return _p(node)->m_key.anchor; }
+ NodeScalar const& keysc (size_t node) const { RYML_ASSERT(has_key(node)); return _p(node)->m_key; }
+
+ csubstr const& val (size_t node) const { RYML_ASSERT(has_val(node)); return _p(node)->m_val.scalar; }
+ csubstr const& val_tag (size_t node) const { RYML_ASSERT(has_val_tag(node)); return _p(node)->m_val.tag; }
+ csubstr const& val_ref (size_t node) const { RYML_ASSERT(is_val_ref(node) && ! has_val_anchor(node)); return _p(node)->m_val.anchor; }
+ csubstr const& val_anchor(size_t node) const { RYML_ASSERT( ! is_val_ref(node) && has_val_anchor(node)); return _p(node)->m_val.anchor; }
+ NodeScalar const& valsc (size_t node) const { RYML_ASSERT(has_val(node)); return _p(node)->m_val; }
+
+ bool key_is_null(size_t node) const { RYML_ASSERT(has_key(node)); if(is_key_quoted(node)) return false; csubstr s = _p(node)->m_key.scalar; return s == nullptr || s == "~" || s == "null" || s == "Null" || s == "NULL"; }
+ bool val_is_null(size_t node) const { RYML_ASSERT(has_val(node)); if(is_val_quoted(node)) return false; csubstr s = _p(node)->m_val.scalar; return s == nullptr || s == "~" || s == "null" || s == "Null" || s == "NULL"; }
+
+ /** @} */
+
+public:
+
+ /** @name node type predicates */
+ /** @{ */
+
+ C4_ALWAYS_INLINE bool is_stream(size_t node) const { return _p(node)->m_type.is_stream(); }
+ C4_ALWAYS_INLINE bool is_doc(size_t node) const { return _p(node)->m_type.is_doc(); }
+ C4_ALWAYS_INLINE bool is_container(size_t node) const { return _p(node)->m_type.is_container(); }
+ C4_ALWAYS_INLINE bool is_map(size_t node) const { return _p(node)->m_type.is_map(); }
+ C4_ALWAYS_INLINE bool is_seq(size_t node) const { return _p(node)->m_type.is_seq(); }
+ C4_ALWAYS_INLINE bool has_key(size_t node) const { return _p(node)->m_type.has_key(); }
+ C4_ALWAYS_INLINE bool has_val(size_t node) const { return _p(node)->m_type.has_val(); }
+ C4_ALWAYS_INLINE bool is_val(size_t node) const { return _p(node)->m_type.is_val(); }
+ C4_ALWAYS_INLINE bool is_keyval(size_t node) const { return _p(node)->m_type.is_keyval(); }
+ C4_ALWAYS_INLINE bool has_key_tag(size_t node) const { return _p(node)->m_type.has_key_tag(); }
+ C4_ALWAYS_INLINE bool has_val_tag(size_t node) const { return _p(node)->m_type.has_val_tag(); }
+ C4_ALWAYS_INLINE bool has_key_anchor(size_t node) const { return _p(node)->m_type.has_key_anchor(); }
+ C4_ALWAYS_INLINE bool is_key_anchor(size_t node) const { return _p(node)->m_type.is_key_anchor(); }
+ C4_ALWAYS_INLINE bool has_val_anchor(size_t node) const { return _p(node)->m_type.has_val_anchor(); }
+ C4_ALWAYS_INLINE bool is_val_anchor(size_t node) const { return _p(node)->m_type.is_val_anchor(); }
+ C4_ALWAYS_INLINE bool has_anchor(size_t node) const { return _p(node)->m_type.has_anchor(); }
+ C4_ALWAYS_INLINE bool is_anchor(size_t node) const { return _p(node)->m_type.is_anchor(); }
+ C4_ALWAYS_INLINE bool is_key_ref(size_t node) const { return _p(node)->m_type.is_key_ref(); }
+ C4_ALWAYS_INLINE bool is_val_ref(size_t node) const { return _p(node)->m_type.is_val_ref(); }
+ C4_ALWAYS_INLINE bool is_ref(size_t node) const { return _p(node)->m_type.is_ref(); }
+ C4_ALWAYS_INLINE bool is_anchor_or_ref(size_t node) const { return _p(node)->m_type.is_anchor_or_ref(); }
+ C4_ALWAYS_INLINE bool is_key_quoted(size_t node) const { return _p(node)->m_type.is_key_quoted(); }
+ C4_ALWAYS_INLINE bool is_val_quoted(size_t node) const { return _p(node)->m_type.is_val_quoted(); }
+ C4_ALWAYS_INLINE bool is_quoted(size_t node) const { return _p(node)->m_type.is_quoted(); }
+
+ C4_ALWAYS_INLINE bool parent_is_seq(size_t node) const { RYML_ASSERT(has_parent(node)); return is_seq(_p(node)->m_parent); }
+ C4_ALWAYS_INLINE bool parent_is_map(size_t node) const { RYML_ASSERT(has_parent(node)); return is_map(_p(node)->m_parent); }
+
+ /** true when key and val are empty, and has no children */
+ bool empty(size_t node) const { return ! has_children(node) && _p(node)->m_key.empty() && (( ! (_p(node)->m_type & VAL)) || _p(node)->m_val.empty()); }
+ /** true when the node has an anchor named a */
+ bool has_anchor(size_t node, csubstr a) const { return _p(node)->m_key.anchor == a || _p(node)->m_val.anchor == a; }
+
+ /** @} */
+
+public:
+
+ /** @name hierarchy predicates */
+ /** @{ */
+
+ bool is_root(size_t node) const { RYML_ASSERT(_p(node)->m_parent != NONE || node == 0); return _p(node)->m_parent == NONE; }
+
+ bool has_parent(size_t node) const { return _p(node)->m_parent != NONE; }
+
+ bool has_child(size_t node, csubstr key) const { return find_child(node, key) != npos; }
+ bool has_child(size_t node, size_t ch) const { return child_pos(node, ch) != npos; }
+ bool has_children(size_t node) const { return _p(node)->m_first_child != NONE; }
+
+ bool has_sibling(size_t node, size_t sib) const { return is_root(node) ? sib==node : child_pos(_p(node)->m_parent, sib) != npos; }
+ bool has_sibling(size_t node, csubstr key) const { return find_sibling(node, key) != npos; }
+ /** counts with *this */
+ bool has_siblings(size_t /*node*/) const { return true; }
+ /** does not count with *this */
+ bool has_other_siblings(size_t node) const { return is_root(node) ? false : (_p(_p(node)->m_parent)->m_first_child != _p(_p(node)->m_parent)->m_last_child); }
+
+ /** @} */
+
+public:
+
+ /** @name hierarchy getters */
+ /** @{ */
+
+ size_t parent(size_t node) const { return _p(node)->m_parent; }
+
+ size_t prev_sibling(size_t node) const { return _p(node)->m_prev_sibling; }
+ size_t next_sibling(size_t node) const { return _p(node)->m_next_sibling; }
+
+ /** O(#num_children) */
+ size_t num_children(size_t node) const;
+ size_t child_pos(size_t node, size_t ch) const;
+ size_t first_child(size_t node) const { return _p(node)->m_first_child; }
+ size_t last_child(size_t node) const { return _p(node)->m_last_child; }
+ size_t child(size_t node, size_t pos) const;
+ size_t find_child(size_t node, csubstr const& key) const;
+
+ /** O(#num_siblings) */
+ /** counts with this */
+ size_t num_siblings(size_t node) const { return is_root(node) ? 1 : num_children(_p(node)->m_parent); }
+ /** does not count with this */
+ size_t num_other_siblings(size_t node) const { size_t ns = num_siblings(node); RYML_ASSERT(ns > 0); return ns-1; }
+ size_t sibling_pos(size_t node, size_t sib) const { RYML_ASSERT( ! is_root(node) || node == root_id()); return child_pos(_p(node)->m_parent, sib); }
+ size_t first_sibling(size_t node) const { return is_root(node) ? node : _p(_p(node)->m_parent)->m_first_child; }
+ size_t last_sibling(size_t node) const { return is_root(node) ? node : _p(_p(node)->m_parent)->m_last_child; }
+ size_t sibling(size_t node, size_t pos) const { return child(_p(node)->m_parent, pos); }
+ size_t find_sibling(size_t node, csubstr const& key) const { return find_child(_p(node)->m_parent, key); }
+
+ size_t doc(size_t i) const { size_t rid = root_id(); RYML_ASSERT(is_stream(rid)); return child(rid, i); } //!< gets the @p i document node index. requires that the root node is a stream.
+
+ /** @} */
+
+public:
+
+ /** @name node modifiers */
+ /** @{ */
+
+ void to_keyval(size_t node, csubstr key, csubstr val, type_bits more_flags=0);
+ void to_map(size_t node, csubstr key, type_bits more_flags=0);
+ void to_seq(size_t node, csubstr key, type_bits more_flags=0);
+ void to_val(size_t node, csubstr val, type_bits more_flags=0);
+ void to_map(size_t node, type_bits more_flags=0);
+ void to_seq(size_t node, type_bits more_flags=0);
+ void to_doc(size_t node, type_bits more_flags=0);
+ void to_stream(size_t node, type_bits more_flags=0);
+
+ void set_key(size_t node, csubstr key) { RYML_ASSERT(has_key(node)); _p(node)->m_key.scalar = key; }
+ void set_val(size_t node, csubstr val) { RYML_ASSERT(has_val(node)); _p(node)->m_val.scalar = val; }
+
+ void set_key_tag(size_t node, csubstr tag) { RYML_ASSERT(has_key(node)); _p(node)->m_key.tag = tag; _add_flags(node, KEYTAG); }
+ void set_val_tag(size_t node, csubstr tag) { RYML_ASSERT(has_val(node) || is_container(node)); _p(node)->m_val.tag = tag; _add_flags(node, VALTAG); }
+
+ void set_key_anchor(size_t node, csubstr anchor) { RYML_ASSERT( ! is_key_ref(node)); _p(node)->m_key.anchor = anchor.triml('&'); _add_flags(node, KEYANCH); }
+ void set_val_anchor(size_t node, csubstr anchor) { RYML_ASSERT( ! is_val_ref(node)); _p(node)->m_val.anchor = anchor.triml('&'); _add_flags(node, VALANCH); }
+ void set_key_ref (size_t node, csubstr ref ) { RYML_ASSERT( ! has_key_anchor(node)); NodeData* C4_RESTRICT n = _p(node); n->m_key.set_ref_maybe_replacing_scalar(ref, n->m_type.has_key()); _add_flags(node, KEY|KEYREF); }
+ void set_val_ref (size_t node, csubstr ref ) { RYML_ASSERT( ! has_val_anchor(node)); NodeData* C4_RESTRICT n = _p(node); n->m_val.set_ref_maybe_replacing_scalar(ref, n->m_type.has_val()); _add_flags(node, VAL|VALREF); }
+
+ void rem_key_anchor(size_t node) { _p(node)->m_key.anchor.clear(); _rem_flags(node, KEYANCH); }
+ void rem_val_anchor(size_t node) { _p(node)->m_val.anchor.clear(); _rem_flags(node, VALANCH); }
+ void rem_key_ref (size_t node) { _p(node)->m_key.anchor.clear(); _rem_flags(node, KEYREF); }
+ void rem_val_ref (size_t node) { _p(node)->m_val.anchor.clear(); _rem_flags(node, VALREF); }
+ void rem_anchor_ref(size_t node) { _p(node)->m_key.anchor.clear(); _p(node)->m_val.anchor.clear(); _rem_flags(node, KEYANCH|VALANCH|KEYREF|VALREF); }
+
+ /** @} */
+
+public:
+
+ /** @name tree modifiers */
+ /** @{ */
+
+ /** reorder the tree in memory so that all the nodes are stored
+ * in a linear sequence when visited in depth-first order.
+ * This will invalidate existing ids, since the node id is its
+ * position in the node array. */
+ void reorder();
+
+ /** Resolve references (aliases <- anchors) in the tree.
+ *
+ * Dereferencing is opt-in; after parsing, Tree::resolve()
+ * has to be called explicitly for obtaining resolved references in the
+ * tree. This method will resolve all references and substitute the
+ * anchored values in place of the reference.
+ *
+ * This method first does a full traversal of the tree to gather all
+ * anchors and references in a separate collection, then it goes through
+ * that collection to locate the names, which it does by obeying the YAML
+ * standard diktat that "an alias node refers to the most recent node in
+ * the serialization having the specified anchor"
+ *
+ * So, depending on the number of anchor/alias nodes, this is a
+ * potentially expensive operation, with a best-case linear complexity
+ * (from the initial traversal). This potential cost is the reason for
+ * requiring an explicit call.
+ */
+ void resolve();
+
+ /** @} */
+
+public:
+
+ /** @name tag directives */
+ /** @{ */
+
+ void resolve_tags();
+
+ size_t num_tag_directives() const;
+ size_t add_tag_directive(TagDirective const& td);
+ void clear_tag_directives();
+
+ size_t resolve_tag(substr output, csubstr tag, size_t node_id) const;
+ csubstr resolve_tag_sub(substr output, csubstr tag, size_t node_id) const
+ {
+ size_t needed = resolve_tag(output, tag, node_id);
+ return needed <= output.len ? output.first(needed) : output;
+ }
+
+ using tag_directive_const_iterator = TagDirective const*;
+ tag_directive_const_iterator begin_tag_directives() const { return m_tag_directives; }
+ tag_directive_const_iterator end_tag_directives() const { return m_tag_directives + num_tag_directives(); }
+
+ struct TagDirectiveProxy
+ {
+ tag_directive_const_iterator b, e;
+ tag_directive_const_iterator begin() const { return b; }
+ tag_directive_const_iterator end() const { return e; }
+ };
+
+ TagDirectiveProxy tag_directives() const { return TagDirectiveProxy{begin_tag_directives(), end_tag_directives()}; }
+
+ /** @} */
+
+public:
+
+ /** @name modifying hierarchy */
+ /** @{ */
+
+ /** create and insert a new child of "parent". insert after the (to-be)
+ * sibling "after", which must be a child of "parent". To insert as the
+ * first child, set after to NONE */
+ inline size_t insert_child(size_t parent, size_t after)
+ {
+ RYML_ASSERT(parent != NONE);
+ RYML_ASSERT(is_container(parent) || is_root(parent));
+ RYML_ASSERT(after == NONE || has_child(parent, after));
+ size_t child = _claim();
+ _set_hierarchy(child, parent, after);
+ return child;
+ }
+ inline size_t prepend_child(size_t parent) { return insert_child(parent, NONE); }
+ inline size_t append_child(size_t parent) { return insert_child(parent, last_child(parent)); }
+
+public:
+
+ #if defined(__clang__)
+ # pragma clang diagnostic push
+ # pragma clang diagnostic ignored "-Wnull-dereference"
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic push
+ # if __GNUC__ >= 6
+ # pragma GCC diagnostic ignored "-Wnull-dereference"
+ # endif
+ #endif
+
+ //! create and insert a new sibling of n. insert after "after"
+ inline size_t insert_sibling(size_t node, size_t after)
+ {
+ RYML_ASSERT(node != NONE);
+ RYML_ASSERT( ! is_root(node));
+ RYML_ASSERT(parent(node) != NONE);
+ RYML_ASSERT(after == NONE || (has_sibling(node, after) && has_sibling(after, node)));
+ RYML_ASSERT(get(node) != nullptr);
+ return insert_child(get(node)->m_parent, after);
+ }
+ inline size_t prepend_sibling(size_t node) { return insert_sibling(node, NONE); }
+ inline size_t append_sibling(size_t node) { return insert_sibling(node, last_sibling(node)); }
+
+public:
+
+ /** remove an entire branch at once: ie remove the children and the node itself */
+ inline void remove(size_t node)
+ {
+ remove_children(node);
+ _release(node);
+ }
+
+ /** remove all the node's children, but keep the node itself */
+ void remove_children(size_t node);
+
+ /** change the @p type of the node to one of MAP, SEQ or VAL. @p
+ * type must have one and only one of MAP,SEQ,VAL; @p type may
+ * possibly have KEY, but if it does, then the @p node must also
+ * have KEY. Changing to the same type is a no-op. Otherwise,
+ * changing to a different type will initialize the node with an
+ * empty value of the desired type: changing to VAL will
+ * initialize with a null scalar (~), changing to MAP will
+ * initialize with an empty map ({}), and changing to SEQ will
+ * initialize with an empty seq ([]). */
+ bool change_type(size_t node, NodeType type);
+
+ bool change_type(size_t node, type_bits type)
+ {
+ return change_type(node, (NodeType)type);
+ }
+
+ #if defined(__clang__)
+ # pragma clang diagnostic pop
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic pop
+ #endif
+
+public:
+
+ /** change the node's position in the parent */
+ void move(size_t node, size_t after);
+
+ /** change the node's parent and position */
+ void move(size_t node, size_t new_parent, size_t after);
+
+ /** change the node's parent and position to a different tree
+ * @return the index of the new node in the destination tree */
+ size_t move(Tree * src, size_t node, size_t new_parent, size_t after);
+
+ /** ensure the first node is a stream. Eg, change this tree
+ *
+ * DOCMAP
+ * MAP
+ * KEYVAL
+ * KEYVAL
+ * SEQ
+ * VAL
+ *
+ * to
+ *
+ * STREAM
+ * DOCMAP
+ * MAP
+ * KEYVAL
+ * KEYVAL
+ * SEQ
+ * VAL
+ *
+ * If the root is already a stream, this is a no-op.
+ */
+ void set_root_as_stream();
+
+public:
+
+ /** recursively duplicate a node from this tree into a new parent,
+ * placing it after one of its children
+ * @return the index of the copy */
+ size_t duplicate(size_t node, size_t new_parent, size_t after);
+ /** recursively duplicate a node from a different tree into a new parent,
+ * placing it after one of its children
+ * @return the index of the copy */
+ size_t duplicate(Tree const* src, size_t node, size_t new_parent, size_t after);
+
+ /** recursively duplicate the node's children (but not the node)
+ * @return the index of the last duplicated child */
+ size_t duplicate_children(size_t node, size_t parent, size_t after);
+ /** recursively duplicate the node's children (but not the node), where
+ * the node is from a different tree
+ * @return the index of the last duplicated child */
+ size_t duplicate_children(Tree const* src, size_t node, size_t parent, size_t after);
+
+ void duplicate_contents(size_t node, size_t where);
+ void duplicate_contents(Tree const* src, size_t node, size_t where);
+
+ /** duplicate the node's children (but not the node) in a new parent, but
+ * omit repetitions where a duplicated node has the same key (in maps) or
+ * value (in seqs). If one of the duplicated children has the same key
+ * (in maps) or value (in seqs) as one of the parent's children, the one
+ * that is placed closest to the end will prevail. */
+ size_t duplicate_children_no_rep(size_t node, size_t parent, size_t after);
+ size_t duplicate_children_no_rep(Tree const* src, size_t node, size_t parent, size_t after);
+
+public:
+
+ void merge_with(Tree const* src, size_t src_node=NONE, size_t dst_root=NONE);
+
+ /** @} */
+
+public:
+
+ /** @name internal string arena */
+ /** @{ */
+
+ /** get the current size of the tree's internal arena */
+ size_t arena_pos() const { return m_arena_pos; }
+
+ /** get the current arena */
+ substr arena() const { return m_arena.first(m_arena_pos); }
+
+ /** return true if the given substring is part of the tree's string arena */
+ bool in_arena(csubstr s) const
+ {
+ return m_arena.is_super(s);
+ }
+
+ /** serialize the given non-floating-point variable to the tree's arena, growing it as
+ * needed to accomodate the serialization.
+ * @note Growing the arena may cause relocation of the entire
+ * existing arena, and thus change the contents of individual nodes.
+ * @see alloc_arena() */
+ template<class T>
+ typename std::enable_if<!std::is_floating_point<T>::value, csubstr>::type
+ to_arena(T const& C4_RESTRICT a)
+ {
+ substr rem(m_arena.sub(m_arena_pos));
+ size_t num = to_chars(rem, a);
+ if(num > rem.len)
+ {
+ rem = _grow_arena(num);
+ num = to_chars(rem, a);
+ RYML_ASSERT(num <= rem.len);
+ }
+ rem = _request_span(num);
+ return rem;
+ }
+
+ /** serialize the given floating-point variable to the tree's arena, growing it as
+ * needed to accomodate the serialization.
+ * @note Growing the arena may cause relocation of the entire
+ * existing arena, and thus change the contents of individual nodes.
+ * @see alloc_arena() */
+ template<class T>
+ typename std::enable_if<std::is_floating_point<T>::value, csubstr>::type
+ to_arena(T const& C4_RESTRICT a)
+ {
+ substr rem(m_arena.sub(m_arena_pos));
+ size_t num = to_chars_float(rem, a);
+ if(num > rem.len)
+ {
+ rem = _grow_arena(num);
+ num = to_chars_float(rem, a);
+ RYML_ASSERT(num <= rem.len);
+ }
+ rem = _request_span(num);
+ return rem;
+ }
+
+ /** copy the given substr to the tree's arena, growing it by the required size
+ * @note Growing the arena may cause relocation of the entire
+ * existing arena, and thus change the contents of individual nodes.
+ * @see alloc_arena() */
+ substr copy_to_arena(csubstr s)
+ {
+ substr cp = alloc_arena(s.len);
+ RYML_ASSERT(cp.len == s.len);
+ RYML_ASSERT(!s.overlaps(cp));
+ #if (!defined(__clang__)) && (defined(__GNUC__) && __GNUC__ >= 10)
+ C4_SUPPRESS_WARNING_GCC_PUSH
+ C4_SUPPRESS_WARNING_GCC("-Wstringop-overflow=") // no need for terminating \0
+ C4_SUPPRESS_WARNING_GCC( "-Wrestrict") // there's an assert to ensure no violation of restrict behavior
+ #endif
+ memcpy(cp.str, s.str, s.len);
+ #if (!defined(__clang__)) && (defined(__GNUC__) && __GNUC__ >= 10)
+ C4_SUPPRESS_WARNING_GCC_POP
+ #endif
+ return cp;
+ }
+
+ /** grow the tree's string arena by the given size and return a substr
+ * of the added portion
+ * @note Growing the arena may cause relocation of the entire
+ * existing arena, and thus change the contents of individual nodes. */
+ substr alloc_arena(size_t sz)
+ {
+ if(sz > arena_slack())
+ _grow_arena(sz - arena_slack());
+ substr s = _request_span(sz);
+ return s;
+ }
+
+ /** ensure the tree's internal string arena is at least the given capacity
+ * @note Growing the arena may cause relocation of the entire
+ * existing arena, and thus change the contents of individual nodes. */
+ void reserve_arena(size_t arena_cap)
+ {
+ if(arena_cap > m_arena.len)
+ {
+ substr buf;
+ buf.str = (char*) m_callbacks.m_allocate(arena_cap, m_arena.str, m_callbacks.m_user_data);
+ buf.len = arena_cap;
+ if(m_arena.str)
+ {
+ RYML_ASSERT(m_arena.len >= 0);
+ _relocate(buf); // does a memcpy and changes nodes using the arena
+ m_callbacks.m_free(m_arena.str, m_arena.len, m_callbacks.m_user_data);
+ }
+ m_arena = buf;
+ }
+ }
+
+ /** @} */
+
+private:
+
+ substr _grow_arena(size_t more)
+ {
+ size_t cap = m_arena_pos + more;
+ cap = cap < 2 * m_arena.len ? 2 * m_arena.len : cap;
+ cap = cap < 64 ? 64 : cap;
+ reserve_arena(cap);
+ return m_arena.sub(m_arena_pos);
+ }
+
+ substr _request_span(size_t sz)
+ {
+ substr s;
+ s = m_arena.sub(m_arena_pos, sz);
+ m_arena_pos += sz;
+ return s;
+ }
+
+ substr _relocated(csubstr s, substr next_arena) const
+ {
+ RYML_ASSERT(m_arena.is_super(s));
+ RYML_ASSERT(m_arena.sub(0, m_arena_pos).is_super(s));
+ auto pos = (s.str - m_arena.str);
+ substr r(next_arena.str + pos, s.len);
+ RYML_ASSERT(r.str - next_arena.str == pos);
+ RYML_ASSERT(next_arena.sub(0, m_arena_pos).is_super(r));
+ return r;
+ }
+
+public:
+
+ /** @name lookup */
+ /** @{ */
+
+ struct lookup_result
+ {
+ size_t target;
+ size_t closest;
+ size_t path_pos;
+ csubstr path;
+
+ inline operator bool() const { return target != NONE; }
+
+ lookup_result() : target(NONE), closest(NONE), path_pos(0), path() {}
+ lookup_result(csubstr path_, size_t start) : target(NONE), closest(start), path_pos(0), path(path_) {}
+
+ /** get the part ot the input path that was resolved */
+ csubstr resolved() const;
+ /** get the part ot the input path that was unresolved */
+ csubstr unresolved() const;
+ };
+
+ /** for example foo.bar[0].baz */
+ lookup_result lookup_path(csubstr path, size_t start=NONE) const;
+
+ /** defaulted lookup: lookup @p path; if the lookup fails, recursively modify
+ * the tree so that the corresponding lookup_path() would return the
+ * default value.
+ * @see lookup_path() */
+ size_t lookup_path_or_modify(csubstr default_value, csubstr path, size_t start=NONE);
+
+ /** defaulted lookup: lookup @p path; if the lookup fails, recursively modify
+ * the tree so that the corresponding lookup_path() would return the
+ * branch @p src_node (from the tree @p src).
+ * @see lookup_path() */
+ size_t lookup_path_or_modify(Tree const *src, size_t src_node, csubstr path, size_t start=NONE);
+
+ /** @} */
+
+private:
+
+ struct _lookup_path_token
+ {
+ csubstr value;
+ NodeType type;
+ _lookup_path_token() : value(), type() {}
+ _lookup_path_token(csubstr v, NodeType t) : value(v), type(t) {}
+ inline operator bool() const { return type != NOTYPE; }
+ bool is_index() const { return value.begins_with('[') && value.ends_with(']'); }
+ };
+
+ size_t _lookup_path_or_create(csubstr path, size_t start);
+
+ void _lookup_path (lookup_result *r) const;
+ void _lookup_path_modify(lookup_result *r);
+
+ size_t _next_node (lookup_result *r, _lookup_path_token *parent) const;
+ size_t _next_node_modify(lookup_result *r, _lookup_path_token *parent);
+
+ void _advance(lookup_result *r, size_t more) const;
+
+ _lookup_path_token _next_token(lookup_result *r, _lookup_path_token const& parent) const;
+
+private:
+
+ void _clear();
+ void _free();
+ void _copy(Tree const& that);
+ void _move(Tree & that);
+
+ void _relocate(substr next_arena);
+
+public:
+
+ #if ! RYML_USE_ASSERT
+ C4_ALWAYS_INLINE void _check_next_flags(size_t, type_bits) {}
+ #else
+ void _check_next_flags(size_t node, type_bits f)
+ {
+ auto n = _p(node);
+ type_bits o = n->m_type; // old
+ C4_UNUSED(o);
+ if(f & MAP)
+ {
+ RYML_ASSERT_MSG((f & SEQ) == 0, "cannot mark simultaneously as map and seq");
+ RYML_ASSERT_MSG((f & VAL) == 0, "cannot mark simultaneously as map and val");
+ RYML_ASSERT_MSG((o & SEQ) == 0, "cannot turn a seq into a map; clear first");
+ RYML_ASSERT_MSG((o & VAL) == 0, "cannot turn a val into a map; clear first");
+ }
+ else if(f & SEQ)
+ {
+ RYML_ASSERT_MSG((f & MAP) == 0, "cannot mark simultaneously as seq and map");
+ RYML_ASSERT_MSG((f & VAL) == 0, "cannot mark simultaneously as seq and val");
+ RYML_ASSERT_MSG((o & MAP) == 0, "cannot turn a map into a seq; clear first");
+ RYML_ASSERT_MSG((o & VAL) == 0, "cannot turn a val into a seq; clear first");
+ }
+ if(f & KEY)
+ {
+ RYML_ASSERT(!is_root(node));
+ auto pid = parent(node); C4_UNUSED(pid);
+ RYML_ASSERT(is_map(pid));
+ }
+ if((f & VAL) && !is_root(node))
+ {
+ auto pid = parent(node); C4_UNUSED(pid);
+ RYML_ASSERT(is_map(pid) || is_seq(pid));
+ }
+ }
+ #endif
+
+ inline void _set_flags(size_t node, NodeType_e f) { _check_next_flags(node, f); _p(node)->m_type = f; }
+ inline void _set_flags(size_t node, type_bits f) { _check_next_flags(node, f); _p(node)->m_type = f; }
+
+ inline void _add_flags(size_t node, NodeType_e f) { NodeData *d = _p(node); type_bits fb = f | d->m_type; _check_next_flags(node, fb); d->m_type = (NodeType_e) fb; }
+ inline void _add_flags(size_t node, type_bits f) { NodeData *d = _p(node); f |= d->m_type; _check_next_flags(node, f); d->m_type = f; }
+
+ inline void _rem_flags(size_t node, NodeType_e f) { NodeData *d = _p(node); type_bits fb = d->m_type & ~f; _check_next_flags(node, fb); d->m_type = (NodeType_e) fb; }
+ inline void _rem_flags(size_t node, type_bits f) { NodeData *d = _p(node); f = d->m_type & ~f; _check_next_flags(node, f); d->m_type = f; }
+
+ void _set_key(size_t node, csubstr key, type_bits more_flags=0)
+ {
+ _p(node)->m_key.scalar = key;
+ _add_flags(node, KEY|more_flags);
+ }
+ void _set_key(size_t node, NodeScalar const& key, type_bits more_flags=0)
+ {
+ _p(node)->m_key = key;
+ _add_flags(node, KEY|more_flags);
+ }
+
+ void _set_val(size_t node, csubstr val, type_bits more_flags=0)
+ {
+ RYML_ASSERT(num_children(node) == 0);
+ RYML_ASSERT(!is_seq(node) && !is_map(node));
+ _p(node)->m_val.scalar = val;
+ _add_flags(node, VAL|more_flags);
+ }
+ void _set_val(size_t node, NodeScalar const& val, type_bits more_flags=0)
+ {
+ RYML_ASSERT(num_children(node) == 0);
+ RYML_ASSERT( ! is_container(node));
+ _p(node)->m_val = val;
+ _add_flags(node, VAL|more_flags);
+ }
+
+ void _set(size_t node, NodeInit const& i)
+ {
+ RYML_ASSERT(i._check());
+ NodeData *n = _p(node);
+ RYML_ASSERT(n->m_key.scalar.empty() || i.key.scalar.empty() || i.key.scalar == n->m_key.scalar);
+ _add_flags(node, i.type);
+ if(n->m_key.scalar.empty())
+ {
+ if( ! i.key.scalar.empty())
+ {
+ _set_key(node, i.key.scalar);
+ }
+ }
+ n->m_key.tag = i.key.tag;
+ n->m_val = i.val;
+ }
+
+ void _set_parent_as_container_if_needed(size_t in)
+ {
+ NodeData const* n = _p(in);
+ size_t ip = parent(in);
+ if(ip != NONE)
+ {
+ if( ! (is_seq(ip) || is_map(ip)))
+ {
+ if((in == first_child(ip)) && (in == last_child(ip)))
+ {
+ if( ! n->m_key.empty() || has_key(in))
+ {
+ _add_flags(ip, MAP);
+ }
+ else
+ {
+ _add_flags(ip, SEQ);
+ }
+ }
+ }
+ }
+ }
+
+ void _seq2map(size_t node)
+ {
+ RYML_ASSERT(is_seq(node));
+ for(size_t i = first_child(node); i != NONE; i = next_sibling(i))
+ {
+ NodeData *C4_RESTRICT ch = _p(i);
+ if(ch->m_type.is_keyval())
+ continue;
+ ch->m_type.add(KEY);
+ ch->m_key = ch->m_val;
+ }
+ auto *C4_RESTRICT n = _p(node);
+ n->m_type.rem(SEQ);
+ n->m_type.add(MAP);
+ }
+
+ size_t _do_reorder(size_t *node, size_t count);
+
+ void _swap(size_t n_, size_t m_);
+ void _swap_props(size_t n_, size_t m_);
+ void _swap_hierarchy(size_t n_, size_t m_);
+ void _copy_hierarchy(size_t dst_, size_t src_);
+
+ void _copy_props(size_t dst_, size_t src_)
+ {
+ auto & C4_RESTRICT dst = *_p(dst_);
+ auto const& C4_RESTRICT src = *_p(src_);
+ dst.m_type = src.m_type;
+ dst.m_key = src.m_key;
+ dst.m_val = src.m_val;
+ }
+
+ void _copy_props_wo_key(size_t dst_, size_t src_)
+ {
+ auto & C4_RESTRICT dst = *_p(dst_);
+ auto const& C4_RESTRICT src = *_p(src_);
+ dst.m_type = src.m_type;
+ dst.m_val = src.m_val;
+ }
+
+ void _copy_props(size_t dst_, Tree const* that_tree, size_t src_)
+ {
+ auto & C4_RESTRICT dst = *_p(dst_);
+ auto const& C4_RESTRICT src = *that_tree->_p(src_);
+ dst.m_type = src.m_type;
+ dst.m_key = src.m_key;
+ dst.m_val = src.m_val;
+ }
+
+ void _copy_props_wo_key(size_t dst_, Tree const* that_tree, size_t src_)
+ {
+ auto & C4_RESTRICT dst = *_p(dst_);
+ auto const& C4_RESTRICT src = *that_tree->_p(src_);
+ dst.m_type = src.m_type;
+ dst.m_val = src.m_val;
+ }
+
+ inline void _clear_type(size_t node)
+ {
+ _p(node)->m_type = NOTYPE;
+ }
+
+ inline void _clear(size_t node)
+ {
+ auto *C4_RESTRICT n = _p(node);
+ n->m_type = NOTYPE;
+ n->m_key.clear();
+ n->m_val.clear();
+ n->m_parent = NONE;
+ n->m_first_child = NONE;
+ n->m_last_child = NONE;
+ }
+
+ inline void _clear_key(size_t node)
+ {
+ _p(node)->m_key.clear();
+ _rem_flags(node, KEY);
+ }
+
+ inline void _clear_val(size_t node)
+ {
+ _p(node)->m_key.clear();
+ _rem_flags(node, VAL);
+ }
+
+private:
+
+ void _clear_range(size_t first, size_t num);
+
+ size_t _claim();
+ void _claim_root();
+ void _release(size_t node);
+ void _free_list_add(size_t node);
+ void _free_list_rem(size_t node);
+
+ void _set_hierarchy(size_t node, size_t parent, size_t after_sibling);
+ void _rem_hierarchy(size_t node);
+
+public:
+
+ // members are exposed, but you should NOT access them directly
+
+ NodeData * m_buf;
+ size_t m_cap;
+
+ size_t m_size;
+
+ size_t m_free_head;
+ size_t m_free_tail;
+
+ substr m_arena;
+ size_t m_arena_pos;
+
+ Callbacks m_callbacks;
+
+ TagDirective m_tag_directives[RYML_MAX_TAG_DIRECTIVES];
+
+};
+
+} // namespace yml
+} // namespace c4
+
+
+C4_SUPPRESS_WARNING_MSVC_POP
+C4_SUPPRESS_WARNING_GCC_CLANG_POP
+
+
+#endif /* _C4_YML_TREE_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/tree.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/node.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/node.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_NODE_HPP_
+#define _C4_YML_NODE_HPP_
+
+/** @file node.hpp
+ * @see NodeRef */
+
+//included above:
+//#include <cstddef>
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/tree.hpp
+//#include "c4/yml/tree.hpp"
+#if !defined(C4_YML_TREE_HPP_) && !defined(_C4_YML_TREE_HPP_)
+#error "amalgamate: file c4/yml/tree.hpp must have been included at this point"
+#endif /* C4_YML_TREE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/base64.hpp
+//#include "c4/base64.hpp"
+#if !defined(C4_BASE64_HPP_) && !defined(_C4_BASE64_HPP_)
+#error "amalgamate: file c4/base64.hpp must have been included at this point"
+#endif /* C4_BASE64_HPP_ */
+
+
+#ifdef __GNUC__
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wtype-limits"
+#endif
+
+#if defined(_MSC_VER)
+# pragma warning(push)
+# pragma warning(disable: 4251/*needs to have dll-interface to be used by clients of struct*/)
+# pragma warning(disable: 4296/*expression is always 'boolean_value'*/)
+#endif
+
+namespace c4 {
+namespace yml {
+
+template<class K> struct Key { K & k; };
+template<> struct Key<fmt::const_base64_wrapper> { fmt::const_base64_wrapper wrapper; };
+template<> struct Key<fmt::base64_wrapper> { fmt::base64_wrapper wrapper; };
+
+template<class K> C4_ALWAYS_INLINE Key<K> key(K & k) { return Key<K>{k}; }
+C4_ALWAYS_INLINE Key<fmt::const_base64_wrapper> key(fmt::const_base64_wrapper w) { return {w}; }
+C4_ALWAYS_INLINE Key<fmt::base64_wrapper> key(fmt::base64_wrapper w) { return {w}; }
+
+template<class T> void write(NodeRef *n, T const& v);
+
+template<class T>
+typename std::enable_if< ! std::is_floating_point<T>::value, bool>::type
+read(NodeRef const& n, T *v);
+
+template<class T>
+typename std::enable_if< std::is_floating_point<T>::value, bool>::type
+read(NodeRef const& n, T *v);
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** a reference to a node in an existing yaml tree, offering a more
+ * convenient API than the index-based API used in the tree. */
+class RYML_EXPORT NodeRef
+{
+private:
+
+ // require valid: a helper macro, undefined at the end
+ #define _C4RV() RYML_ASSERT(valid() && !is_seed())
+
+ Tree *C4_RESTRICT m_tree;
+ size_t m_id;
+
+ /** This member is used to enable lazy operator[] writing. When a child
+ * with a key or index is not found, m_id is set to the id of the parent
+ * and the asked-for key or index are stored in this member until a write
+ * does happen. Then it is given as key or index for creating the child.
+ * When a key is used, the csubstr stores it (so the csubstr's string is
+ * non-null and the csubstr's size is different from NONE). When an index is
+ * used instead, the csubstr's string is set to null, and only the csubstr's
+ * size is set to a value different from NONE. Otherwise, when operator[]
+ * does find the child then this member is empty: the string is null and
+ * the size is NONE. */
+ csubstr m_seed;
+
+public:
+
+ /** @name node construction */
+ /** @{ */
+
+ NodeRef() : m_tree(nullptr), m_id(NONE), m_seed() { _clear_seed(); }
+ NodeRef(Tree &t) : m_tree(&t), m_id(t .root_id()), m_seed() { _clear_seed(); }
+ NodeRef(Tree *t) : m_tree(t ), m_id(t->root_id()), m_seed() { _clear_seed(); }
+ NodeRef(Tree *t, size_t id) : m_tree(t), m_id(id), m_seed() { _clear_seed(); }
+ NodeRef(Tree *t, size_t id, size_t seed_pos) : m_tree(t), m_id(id), m_seed() { m_seed.str = nullptr; m_seed.len = seed_pos; }
+ NodeRef(Tree *t, size_t id, csubstr seed_key) : m_tree(t), m_id(id), m_seed(seed_key) {}
+ NodeRef(std::nullptr_t) : m_tree(nullptr), m_id(NONE), m_seed() {}
+
+ NodeRef(NodeRef const&) = default;
+ NodeRef(NodeRef &&) = default;
+
+ NodeRef& operator= (NodeRef const&) = default;
+ NodeRef& operator= (NodeRef &&) = default;
+
+ /** @} */
+
+public:
+
+ inline Tree * tree() { return m_tree; }
+ inline Tree const* tree() const { return m_tree; }
+
+ inline size_t id() const { return m_id; }
+
+ inline NodeData * get() { return m_tree->get(m_id); }
+ inline NodeData const* get() const { return m_tree->get(m_id); }
+
+ inline bool operator== (NodeRef const& that) const { _C4RV(); RYML_ASSERT(that.valid() && !that.is_seed()); RYML_ASSERT(that.m_tree == m_tree); return m_id == that.m_id; }
+ inline bool operator!= (NodeRef const& that) const { return ! this->operator==(that); }
+
+ inline bool operator== (std::nullptr_t) const { return m_tree == nullptr || m_id == NONE || is_seed(); }
+ inline bool operator!= (std::nullptr_t) const { return ! this->operator== (nullptr); }
+
+ inline bool operator== (csubstr val) const { _C4RV(); RYML_ASSERT(has_val()); return m_tree->val(m_id) == val; }
+ inline bool operator!= (csubstr val) const { _C4RV(); RYML_ASSERT(has_val()); return m_tree->val(m_id) != val; }
+
+ //inline operator bool () const { return m_tree == nullptr || m_id == NONE || is_seed(); }
+
+public:
+
+ inline bool valid() const { return m_tree != nullptr && m_id != NONE; }
+ inline bool is_seed() const { return m_seed.str != nullptr || m_seed.len != NONE; }
+
+ inline void _clear_seed() { /*do this manually or an assert is triggered*/ m_seed.str = nullptr; m_seed.len = NONE; }
+
+public:
+
+ /** @name node property getters */
+ /** @{ */
+
+ inline NodeType type() const { _C4RV(); return m_tree->type(m_id); }
+ inline const char* type_str() const { _C4RV(); RYML_ASSERT(valid() && ! is_seed()); return m_tree->type_str(m_id); }
+
+ inline csubstr key() const { _C4RV(); return m_tree->key(m_id); }
+ inline csubstr key_tag() const { _C4RV(); return m_tree->key_tag(m_id); }
+ inline csubstr key_ref() const { _C4RV(); return m_tree->key_ref(m_id); }
+ inline csubstr key_anchor() const { _C4RV(); return m_tree->key_anchor(m_id); }
+ inline NodeScalar keysc() const { _C4RV(); return m_tree->keysc(m_id); }
+
+ inline csubstr val() const { _C4RV(); return m_tree->val(m_id); }
+ inline csubstr val_tag() const { _C4RV(); return m_tree->val_tag(m_id); }
+ inline csubstr val_ref() const { _C4RV(); return m_tree->val_ref(m_id); }
+ inline csubstr val_anchor() const { _C4RV(); return m_tree->val_anchor(m_id); }
+ inline NodeScalar valsc() const { _C4RV(); return m_tree->valsc(m_id); }
+
+ inline bool key_is_null() const { _C4RV(); return m_tree->key_is_null(m_id); }
+ inline bool val_is_null() const { _C4RV(); return m_tree->val_is_null(m_id); }
+
+ /** decode the base64-encoded key deserialize and assign the
+ * decoded blob to the given buffer/
+ * @return the size of base64-decoded blob */
+ size_t deserialize_key(fmt::base64_wrapper v) const;
+ /** decode the base64-encoded key deserialize and assign the
+ * decoded blob to the given buffer/
+ * @return the size of base64-decoded blob */
+ size_t deserialize_val(fmt::base64_wrapper v) const;
+
+ /** @} */
+
+public:
+
+ /** @name node property predicates */
+ /** @{ */
+
+ C4_ALWAYS_INLINE bool is_stream() const { _C4RV(); return m_tree->is_stream(m_id); }
+ C4_ALWAYS_INLINE bool is_doc() const { _C4RV(); return m_tree->is_doc(m_id); }
+ C4_ALWAYS_INLINE bool is_container() const { _C4RV(); return m_tree->is_container(m_id); }
+ C4_ALWAYS_INLINE bool is_map() const { _C4RV(); return m_tree->is_map(m_id); }
+ C4_ALWAYS_INLINE bool is_seq() const { _C4RV(); return m_tree->is_seq(m_id); }
+ C4_ALWAYS_INLINE bool has_val() const { _C4RV(); return m_tree->has_val(m_id); }
+ C4_ALWAYS_INLINE bool has_key() const { _C4RV(); return m_tree->has_key(m_id); }
+ C4_ALWAYS_INLINE bool is_val() const { _C4RV(); return m_tree->is_val(m_id); }
+ C4_ALWAYS_INLINE bool is_keyval() const { _C4RV(); return m_tree->is_keyval(m_id); }
+ C4_ALWAYS_INLINE bool has_key_tag() const { _C4RV(); return m_tree->has_key_tag(m_id); }
+ C4_ALWAYS_INLINE bool has_val_tag() const { _C4RV(); return m_tree->has_val_tag(m_id); }
+ C4_ALWAYS_INLINE bool has_key_anchor() const { _C4RV(); return m_tree->has_key_anchor(m_id); }
+ C4_ALWAYS_INLINE bool is_key_anchor() const { _C4RV(); return m_tree->is_key_anchor(m_id); }
+ C4_ALWAYS_INLINE bool has_val_anchor() const { _C4RV(); return m_tree->has_val_anchor(m_id); }
+ C4_ALWAYS_INLINE bool is_val_anchor() const { _C4RV(); return m_tree->is_val_anchor(m_id); }
+ C4_ALWAYS_INLINE bool has_anchor() const { _C4RV(); return m_tree->has_anchor(m_id); }
+ C4_ALWAYS_INLINE bool is_anchor() const { _C4RV(); return m_tree->is_anchor(m_id); }
+ C4_ALWAYS_INLINE bool is_key_ref() const { _C4RV(); return m_tree->is_key_ref(m_id); }
+ C4_ALWAYS_INLINE bool is_val_ref() const { _C4RV(); return m_tree->is_val_ref(m_id); }
+ C4_ALWAYS_INLINE bool is_ref() const { _C4RV(); return m_tree->is_ref(m_id); }
+ C4_ALWAYS_INLINE bool is_anchor_or_ref() const { _C4RV(); return m_tree->is_anchor_or_ref(m_id); }
+ C4_ALWAYS_INLINE bool is_key_quoted() const { _C4RV(); return m_tree->is_key_quoted(m_id); }
+ C4_ALWAYS_INLINE bool is_val_quoted() const { _C4RV(); return m_tree->is_val_quoted(m_id); }
+ C4_ALWAYS_INLINE bool is_quoted() const { _C4RV(); return m_tree->is_quoted(m_id); }
+
+ C4_ALWAYS_INLINE bool parent_is_seq() const { _C4RV(); return m_tree->parent_is_seq(m_id); }
+ C4_ALWAYS_INLINE bool parent_is_map() const { _C4RV(); return m_tree->parent_is_map(m_id); }
+
+ /** true when name and value are empty, and has no children */
+ C4_ALWAYS_INLINE bool empty() const { _C4RV(); return m_tree->empty(m_id); }
+
+ /** @} */
+
+public:
+
+ /** @name hierarchy predicates */
+ /** @{ */
+
+ inline bool is_root() const { _C4RV(); return m_tree->is_root(m_id); }
+ inline bool has_parent() const { _C4RV(); return m_tree->has_parent(m_id); }
+
+ inline bool has_child(NodeRef const& ch) const { _C4RV(); return m_tree->has_child(m_id, ch.m_id); }
+ inline bool has_child(csubstr name) const { _C4RV(); return m_tree->has_child(m_id, name); }
+ inline bool has_children() const { _C4RV(); return m_tree->has_children(m_id); }
+
+ inline bool has_sibling(NodeRef const& n) const { _C4RV(); return m_tree->has_sibling(m_id, n.m_id); }
+ inline bool has_sibling(csubstr name) const { _C4RV(); return m_tree->has_sibling(m_id, name); }
+ /** counts with this */
+ inline bool has_siblings() const { _C4RV(); return m_tree->has_siblings(m_id); }
+ /** does not count with this */
+ inline bool has_other_siblings() const { _C4RV(); return m_tree->has_other_siblings(m_id); }
+
+ /** @} */
+
+public:
+
+ /** @name hierarchy getters */
+ /** @{ */
+
+ NodeRef parent() { _C4RV(); return {m_tree, m_tree->parent(m_id)}; }
+ NodeRef const parent() const { _C4RV(); return {m_tree, m_tree->parent(m_id)}; }
+
+ NodeRef prev_sibling() { _C4RV(); return {m_tree, m_tree->prev_sibling(m_id)}; }
+ NodeRef const prev_sibling() const { _C4RV(); return {m_tree, m_tree->prev_sibling(m_id)}; }
+
+ NodeRef next_sibling() { _C4RV(); return {m_tree, m_tree->next_sibling(m_id)}; }
+ NodeRef const next_sibling() const { _C4RV(); return {m_tree, m_tree->next_sibling(m_id)}; }
+
+ /** O(#num_children) */
+ size_t num_children() const { _C4RV(); return m_tree->num_children(m_id); }
+ size_t child_pos(NodeRef const& n) const { _C4RV(); return m_tree->child_pos(m_id, n.m_id); }
+ NodeRef first_child() { _C4RV(); return {m_tree, m_tree->first_child(m_id)}; }
+ NodeRef const first_child() const { _C4RV(); return {m_tree, m_tree->first_child(m_id)}; }
+ NodeRef last_child () { _C4RV(); return {m_tree, m_tree->last_child (m_id)}; }
+ NodeRef const last_child () const { _C4RV(); return {m_tree, m_tree->last_child (m_id)}; }
+ NodeRef child(size_t pos) { _C4RV(); return {m_tree, m_tree->child(m_id, pos)}; }
+ NodeRef const child(size_t pos) const { _C4RV(); return {m_tree, m_tree->child(m_id, pos)}; }
+ NodeRef find_child(csubstr name) { _C4RV(); return {m_tree, m_tree->find_child(m_id, name)}; }
+ NodeRef const find_child(csubstr name) const { _C4RV(); return {m_tree, m_tree->find_child(m_id, name)}; }
+
+ /** O(#num_siblings) */
+ size_t num_siblings() const { _C4RV(); return m_tree->num_siblings(m_id); }
+ size_t num_other_siblings() const { _C4RV(); return m_tree->num_other_siblings(m_id); }
+ size_t sibling_pos(NodeRef const& n) const { _C4RV(); return m_tree->child_pos(m_tree->parent(m_id), n.m_id); }
+ NodeRef first_sibling() { _C4RV(); return {m_tree, m_tree->first_sibling(m_id)}; }
+ NodeRef const first_sibling() const { _C4RV(); return {m_tree, m_tree->first_sibling(m_id)}; }
+ NodeRef last_sibling () { _C4RV(); return {m_tree, m_tree->last_sibling(m_id)}; }
+ NodeRef const last_sibling () const { _C4RV(); return {m_tree, m_tree->last_sibling(m_id)}; }
+ NodeRef sibling(size_t pos) { _C4RV(); return {m_tree, m_tree->sibling(m_id, pos)}; }
+ NodeRef const sibling(size_t pos) const { _C4RV(); return {m_tree, m_tree->sibling(m_id, pos)}; }
+ NodeRef find_sibling(csubstr name) { _C4RV(); return {m_tree, m_tree->find_sibling(m_id, name)}; }
+ NodeRef const find_sibling(csubstr name) const { _C4RV(); return {m_tree, m_tree->find_sibling(m_id, name)}; }
+
+ NodeRef doc(size_t num) { _C4RV(); return {m_tree, m_tree->doc(num)}; }
+ NodeRef const doc(size_t num) const { _C4RV(); return {m_tree, m_tree->doc(num)}; }
+
+ /** @} */
+
+public:
+
+ /** @name node modifiers */
+ /** @{ */
+
+ void change_type(NodeType t) { _C4RV(); m_tree->change_type(m_id, t); }
+ void set_type(NodeType t) { _C4RV(); m_tree->_set_flags(m_id, t); }
+ void set_key(csubstr key) { _C4RV(); m_tree->_set_key(m_id, key); }
+ void set_val(csubstr val) { _C4RV(); m_tree->_set_val(m_id, val); }
+ void set_key_tag(csubstr key_tag) { _C4RV(); m_tree->set_key_tag(m_id, key_tag); }
+ void set_val_tag(csubstr val_tag) { _C4RV(); m_tree->set_val_tag(m_id, val_tag); }
+ void set_key_anchor(csubstr key_anchor) { _C4RV(); m_tree->set_key_anchor(m_id, key_anchor); }
+ void set_val_anchor(csubstr val_anchor) { _C4RV(); m_tree->set_val_anchor(m_id, val_anchor); }
+ void set_key_ref(csubstr key_ref) { _C4RV(); m_tree->set_key_ref(m_id, key_ref); }
+ void set_val_ref(csubstr val_ref) { _C4RV(); m_tree->set_val_ref(m_id, val_ref); }
+
+ template<class T>
+ size_t set_key_serialized(T const& C4_RESTRICT k)
+ {
+ _C4RV();
+ csubstr s = m_tree->to_arena(k);
+ m_tree->_set_key(m_id, s);
+ return s.len;
+ }
+ template<class T>
+ size_t set_val_serialized(T const& C4_RESTRICT v)
+ {
+ _C4RV();
+ csubstr s = m_tree->to_arena(v);
+ m_tree->_set_val(m_id, s);
+ return s.len;
+ }
+
+ /** encode a blob as base64, then assign the result to the node's key
+ * @return the size of base64-encoded blob */
+ size_t set_key_serialized(fmt::const_base64_wrapper w);
+ /** encode a blob as base64, then assign the result to the node's val
+ * @return the size of base64-encoded blob */
+ size_t set_val_serialized(fmt::const_base64_wrapper w);
+
+public:
+
+ inline void clear()
+ {
+ if(is_seed())
+ return;
+ m_tree->remove_children(m_id);
+ m_tree->_clear(m_id);
+ }
+
+ inline void clear_key()
+ {
+ if(is_seed())
+ return;
+ m_tree->_clear_key(m_id);
+ }
+
+ inline void clear_val()
+ {
+ if(is_seed())
+ return;
+ m_tree->_clear_val(m_id);
+ }
+
+ inline void clear_children()
+ {
+ if(is_seed())
+ return;
+ m_tree->remove_children(m_id);
+ }
+
+ /** @} */
+
+public:
+
+ /** hierarchy getters */
+ /** @{ */
+
+ /** O(num_children) */
+ NodeRef operator[] (csubstr k)
+ {
+ RYML_ASSERT( ! is_seed());
+ RYML_ASSERT(valid());
+ size_t ch = m_tree->find_child(m_id, k);
+ NodeRef r = ch != NONE ? NodeRef(m_tree, ch) : NodeRef(m_tree, m_id, k);
+ return r;
+ }
+
+ /** O(num_children) */
+ NodeRef const operator[] (csubstr k) const
+ {
+ RYML_ASSERT( ! is_seed());
+ RYML_ASSERT(valid());
+ size_t ch = m_tree->find_child(m_id, k);
+ RYML_ASSERT(ch != NONE);
+ NodeRef const r(m_tree, ch);
+ return r;
+ }
+
+ /** O(num_children) */
+ NodeRef operator[] (size_t pos)
+ {
+ RYML_ASSERT( ! is_seed());
+ RYML_ASSERT(valid());
+ size_t ch = m_tree->child(m_id, pos);
+ NodeRef r = ch != NONE ? NodeRef(m_tree, ch) : NodeRef(m_tree, m_id, pos);
+ return r;
+ }
+
+ /** O(num_children) */
+ NodeRef const operator[] (size_t pos) const
+ {
+ RYML_ASSERT( ! is_seed());
+ RYML_ASSERT(valid());
+ size_t ch = m_tree->child(m_id, pos);
+ RYML_ASSERT(ch != NONE);
+ NodeRef const r(m_tree, ch);
+ return r;
+ }
+
+ /** @} */
+
+public:
+
+ /** node modification */
+ /** @{ */
+
+ void create() { _apply_seed(); }
+
+ inline void operator= (NodeType_e t)
+ {
+ _apply_seed();
+ m_tree->_add_flags(m_id, t);
+ }
+
+ inline void operator|= (NodeType_e t)
+ {
+ _apply_seed();
+ m_tree->_add_flags(m_id, t);
+ }
+
+ inline void operator= (NodeInit const& v)
+ {
+ _apply_seed();
+ _apply(v);
+ }
+
+ inline void operator= (NodeScalar const& v)
+ {
+ _apply_seed();
+ _apply(v);
+ }
+
+ inline void operator= (csubstr v)
+ {
+ _apply_seed();
+ _apply(v);
+ }
+
+ template<size_t N>
+ inline void operator= (const char (&v)[N])
+ {
+ _apply_seed();
+ csubstr sv;
+ sv.assign<N>(v);
+ _apply(sv);
+ }
+
+ /** @} */
+
+public:
+
+ /** serialize a variable to the arena */
+ template<class T>
+ inline csubstr to_arena(T const& C4_RESTRICT s) const
+ {
+ _C4RV();
+ return m_tree->to_arena(s);
+ }
+
+ /** serialize a variable, then assign the result to the node's val */
+ inline NodeRef& operator<< (csubstr s)
+ {
+ // this overload is needed to prevent ambiguity (there's also
+ // operator<< for writing a substr to a stream)
+ _apply_seed();
+ write(this, s);
+ RYML_ASSERT(val() == s);
+ return *this;
+ }
+
+ template<class T>
+ inline NodeRef& operator<< (T const& C4_RESTRICT v)
+ {
+ _apply_seed();
+ write(this, v);
+ return *this;
+ }
+
+ template<class T>
+ inline NodeRef const& operator>> (T &v) const
+ {
+ RYML_ASSERT( ! is_seed());
+ RYML_ASSERT(valid());
+ RYML_ASSERT(get() != nullptr);
+ if( ! read(*this, &v))
+ {
+ c4::yml::error("could not deserialize value");
+ }
+ return *this;
+ }
+
+public:
+
+ /** serialize a variable, then assign the result to the node's key */
+ template<class T>
+ inline NodeRef& operator<< (Key<const T> const& C4_RESTRICT v)
+ {
+ _apply_seed();
+ set_key_serialized(v.k);
+ return *this;
+ }
+
+ /** serialize a variable, then assign the result to the node's key */
+ template<class T>
+ inline NodeRef& operator<< (Key<T> const& C4_RESTRICT v)
+ {
+ _apply_seed();
+ set_key_serialized(v.k);
+ return *this;
+ }
+
+ /** deserialize the node's key to the given variable */
+ template<class T>
+ inline NodeRef const& operator>> (Key<T> v) const
+ {
+ RYML_ASSERT( ! is_seed());
+ RYML_ASSERT(valid());
+ RYML_ASSERT(get() != nullptr);
+ from_chars(key(), &v.k);
+ return *this;
+ }
+
+public:
+
+ NodeRef& operator<< (Key<fmt::const_base64_wrapper> w)
+ {
+ set_key_serialized(w.wrapper);
+ return *this;
+ }
+
+ NodeRef& operator<< (fmt::const_base64_wrapper w)
+ {
+ set_val_serialized(w);
+ return *this;
+ }
+
+ NodeRef const& operator>> (Key<fmt::base64_wrapper> w) const
+ {
+ deserialize_key(w.wrapper);
+ return *this;
+ }
+
+ NodeRef const& operator>> (fmt::base64_wrapper w) const
+ {
+ deserialize_val(w);
+ return *this;
+ }
+
+public:
+
+ template<class T>
+ void get_if(csubstr name, T *var) const
+ {
+ auto ch = find_child(name);
+ if(ch.valid())
+ {
+ ch >> *var;
+ }
+ }
+
+ template<class T>
+ void get_if(csubstr name, T *var, T fallback) const
+ {
+ auto ch = find_child(name);
+ if(ch.valid())
+ {
+ ch >> *var;
+ }
+ else
+ {
+ *var = fallback;
+ }
+ }
+
+private:
+
+ void _apply_seed()
+ {
+ if(m_seed.str) // we have a seed key: use it to create the new child
+ {
+ //RYML_ASSERT(i.key.scalar.empty() || m_key == i.key.scalar || m_key.empty());
+ m_id = m_tree->append_child(m_id);
+ m_tree->_set_key(m_id, m_seed);
+ m_seed.str = nullptr;
+ m_seed.len = NONE;
+ }
+ else if(m_seed.len != NONE) // we have a seed index: create a child at that position
+ {
+ RYML_ASSERT(m_tree->num_children(m_id) == m_seed.len);
+ m_id = m_tree->append_child(m_id);
+ m_seed.str = nullptr;
+ m_seed.len = NONE;
+ }
+ else
+ {
+ RYML_ASSERT(valid());
+ }
+ }
+
+ inline void _apply(csubstr v)
+ {
+ m_tree->_set_val(m_id, v);
+ }
+
+ inline void _apply(NodeScalar const& v)
+ {
+ m_tree->_set_val(m_id, v);
+ }
+
+ inline void _apply(NodeInit const& i)
+ {
+ m_tree->_set(m_id, i);
+ }
+
+public:
+
+ inline NodeRef insert_child(NodeRef after)
+ {
+ _C4RV();
+ RYML_ASSERT(after.m_tree == m_tree);
+ NodeRef r(m_tree, m_tree->insert_child(m_id, after.m_id));
+ return r;
+ }
+
+ inline NodeRef insert_child(NodeInit const& i, NodeRef after)
+ {
+ _C4RV();
+ RYML_ASSERT(after.m_tree == m_tree);
+ NodeRef r(m_tree, m_tree->insert_child(m_id, after.m_id));
+ r._apply(i);
+ return r;
+ }
+
+ inline NodeRef prepend_child()
+ {
+ _C4RV();
+ NodeRef r(m_tree, m_tree->insert_child(m_id, NONE));
+ return r;
+ }
+
+ inline NodeRef prepend_child(NodeInit const& i)
+ {
+ _C4RV();
+ NodeRef r(m_tree, m_tree->insert_child(m_id, NONE));
+ r._apply(i);
+ return r;
+ }
+
+ inline NodeRef append_child()
+ {
+ _C4RV();
+ NodeRef r(m_tree, m_tree->append_child(m_id));
+ return r;
+ }
+
+ inline NodeRef append_child(NodeInit const& i)
+ {
+ _C4RV();
+ NodeRef r(m_tree, m_tree->append_child(m_id));
+ r._apply(i);
+ return r;
+ }
+
+public:
+
+ inline NodeRef insert_sibling(NodeRef const after)
+ {
+ _C4RV();
+ RYML_ASSERT(after.m_tree == m_tree);
+ NodeRef r(m_tree, m_tree->insert_sibling(m_id, after.m_id));
+ return r;
+ }
+
+ inline NodeRef insert_sibling(NodeInit const& i, NodeRef const after)
+ {
+ _C4RV();
+ RYML_ASSERT(after.m_tree == m_tree);
+ NodeRef r(m_tree, m_tree->insert_sibling(m_id, after.m_id));
+ r._apply(i);
+ return r;
+ }
+
+ inline NodeRef prepend_sibling()
+ {
+ _C4RV();
+ NodeRef r(m_tree, m_tree->prepend_sibling(m_id));
+ return r;
+ }
+
+ inline NodeRef prepend_sibling(NodeInit const& i)
+ {
+ _C4RV();
+ NodeRef r(m_tree, m_tree->prepend_sibling(m_id));
+ r._apply(i);
+ return r;
+ }
+
+ inline NodeRef append_sibling()
+ {
+ _C4RV();
+ NodeRef r(m_tree, m_tree->append_sibling(m_id));
+ return r;
+ }
+
+ inline NodeRef append_sibling(NodeInit const& i)
+ {
+ _C4RV();
+ NodeRef r(m_tree, m_tree->append_sibling(m_id));
+ r._apply(i);
+ return r;
+ }
+
+public:
+
+ inline void remove_child(NodeRef & child)
+ {
+ _C4RV();
+ RYML_ASSERT(has_child(child));
+ RYML_ASSERT(child.parent().id() == id());
+ m_tree->remove(child.id());
+ child.clear();
+ }
+
+ //! remove the nth child of this node
+ inline void remove_child(size_t pos)
+ {
+ _C4RV();
+ RYML_ASSERT(pos >= 0 && pos < num_children());
+ size_t child = m_tree->child(m_id, pos);
+ RYML_ASSERT(child != NONE);
+ m_tree->remove(child);
+ }
+
+ //! remove a child by name
+ inline void remove_child(csubstr key)
+ {
+ _C4RV();
+ size_t child = m_tree->find_child(m_id, key);
+ RYML_ASSERT(child != NONE);
+ m_tree->remove(child);
+ }
+
+public:
+
+ /** change the node's position within its parent */
+ inline void move(NodeRef const after)
+ {
+ _C4RV();
+ m_tree->move(m_id, after.m_id);
+ }
+
+ /** move the node to a different parent, which may belong to a different
+ * tree. When this is the case, then this node's tree pointer is reset to
+ * the tree of the parent node. */
+ inline void move(NodeRef const parent, NodeRef const after)
+ {
+ _C4RV();
+ RYML_ASSERT(parent.m_tree == after.m_tree);
+ if(parent.m_tree == m_tree)
+ {
+ m_tree->move(m_id, parent.m_id, after.m_id);
+ }
+ else
+ {
+ parent.m_tree->move(m_tree, m_id, parent.m_id, after.m_id);
+ m_tree = parent.m_tree;
+ }
+ }
+
+ inline NodeRef duplicate(NodeRef const parent, NodeRef const after) const
+ {
+ _C4RV();
+ RYML_ASSERT(parent.m_tree == after.m_tree);
+ if(parent.m_tree == m_tree)
+ {
+ size_t dup = m_tree->duplicate(m_id, parent.m_id, after.m_id);
+ NodeRef r(m_tree, dup);
+ return r;
+ }
+ else
+ {
+ size_t dup = parent.m_tree->duplicate(m_tree, m_id, parent.m_id, after.m_id);
+ NodeRef r(parent.m_tree, dup);
+ return r;
+ }
+ }
+
+ inline void duplicate_children(NodeRef const parent, NodeRef const after) const
+ {
+ _C4RV();
+ RYML_ASSERT(parent.m_tree == after.m_tree);
+ if(parent.m_tree == m_tree)
+ {
+ m_tree->duplicate_children(m_id, parent.m_id, after.m_id);
+ }
+ else
+ {
+ parent.m_tree->duplicate_children(m_tree, m_id, parent.m_id, after.m_id);
+ }
+ }
+
+private:
+
+ template<class Nd>
+ struct child_iterator
+ {
+ Tree * m_tree;
+ size_t m_child_id;
+
+ using value_type = NodeRef;
+
+ child_iterator(Tree * t, size_t id) : m_tree(t), m_child_id(id) {}
+
+ child_iterator& operator++ () { RYML_ASSERT(m_child_id != NONE); m_child_id = m_tree->next_sibling(m_child_id); return *this; }
+ child_iterator& operator-- () { RYML_ASSERT(m_child_id != NONE); m_child_id = m_tree->prev_sibling(m_child_id); return *this; }
+
+ Nd operator* () const { return Nd(m_tree, m_child_id); }
+ Nd operator-> () const { return Nd(m_tree, m_child_id); }
+
+ bool operator!= (child_iterator that) const { RYML_ASSERT(m_tree == that.m_tree); return m_child_id != that.m_child_id; }
+ bool operator== (child_iterator that) const { RYML_ASSERT(m_tree == that.m_tree); return m_child_id == that.m_child_id; }
+ };
+
+public:
+
+ using iterator = child_iterator< NodeRef>;
+ using const_iterator = child_iterator<const NodeRef>;
+
+ inline iterator begin() { return iterator(m_tree, m_tree->first_child(m_id)); }
+ inline iterator end () { return iterator(m_tree, NONE); }
+
+ inline const_iterator begin() const { return const_iterator(m_tree, m_tree->first_child(m_id)); }
+ inline const_iterator end () const { return const_iterator(m_tree, NONE); }
+
+private:
+
+ template<class Nd>
+ struct children_view_
+ {
+ using n_iterator = child_iterator<Nd>;
+
+ n_iterator b, e;
+
+ inline children_view_(n_iterator const& b_, n_iterator const& e_) : b(b_), e(e_) {}
+
+ inline n_iterator begin() const { return b; }
+ inline n_iterator end () const { return e; }
+ };
+
+public:
+
+ using children_view = children_view_< NodeRef>;
+ using const_children_view = children_view_<const NodeRef>;
+
+ children_view children() { return children_view(begin(), end()); }
+ const_children_view children() const { return const_children_view(begin(), end()); }
+
+ #if defined(__clang__)
+ # pragma clang diagnostic push
+ # pragma clang diagnostic ignored "-Wnull-dereference"
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic push
+ # if __GNUC__ >= 6
+ # pragma GCC diagnostic ignored "-Wnull-dereference"
+ # endif
+ #endif
+
+ children_view siblings() { if(is_root()) { return children_view(end(), end()); } else { size_t p = get()->m_parent; return children_view(iterator(m_tree, m_tree->get(p)->m_first_child), iterator(m_tree, NONE)); } }
+ const_children_view siblings() const { if(is_root()) { return const_children_view(end(), end()); } else { size_t p = get()->m_parent; return const_children_view(const_iterator(m_tree, m_tree->get(p)->m_first_child), const_iterator(m_tree, NONE)); } }
+
+ #if defined(__clang__)
+ # pragma clang diagnostic pop
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic pop
+ #endif
+
+public:
+
+ /** visit every child node calling fn(node) */
+ template<class Visitor> bool visit(Visitor fn, size_t indentation_level=0, bool skip_root=true);
+ /** visit every child node calling fn(node) */
+ template<class Visitor> bool visit(Visitor fn, size_t indentation_level=0, bool skip_root=true) const;
+
+ /** visit every child node calling fn(node, level) */
+ template<class Visitor> bool visit_stacked(Visitor fn, size_t indentation_level=0, bool skip_root=true);
+ /** visit every child node calling fn(node, level) */
+ template<class Visitor> bool visit_stacked(Visitor fn, size_t indentation_level=0, bool skip_root=true) const;
+
+#undef _C4RV
+};
+
+//-----------------------------------------------------------------------------
+template<class T>
+inline void write(NodeRef *n, T const& v)
+{
+ n->set_val_serialized(v);
+}
+
+template<class T>
+typename std::enable_if< ! std::is_floating_point<T>::value, bool>::type
+inline read(NodeRef const& n, T *v)
+{
+ return from_chars(n.val(), v);
+}
+
+template<class T>
+typename std::enable_if< std::is_floating_point<T>::value, bool>::type
+inline read(NodeRef const& n, T *v)
+{
+ return from_chars_float(n.val(), v);
+}
+
+
+//-----------------------------------------------------------------------------
+template<class Visitor>
+bool NodeRef::visit(Visitor fn, size_t indentation_level, bool skip_root)
+{
+ return const_cast<NodeRef const*>(this)->visit(fn, indentation_level, skip_root);
+}
+
+template<class Visitor>
+bool NodeRef::visit(Visitor fn, size_t indentation_level, bool skip_root) const
+{
+ size_t increment = 0;
+ if( ! (is_root() && skip_root))
+ {
+ if(fn(this, indentation_level))
+ {
+ return true;
+ }
+ ++increment;
+ }
+ if(has_children())
+ {
+ for(auto ch : children())
+ {
+ if(ch.visit(fn, indentation_level + increment)) // no need to forward skip_root as it won't be root
+ {
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+
+template<class Visitor>
+bool NodeRef::visit_stacked(Visitor fn, size_t indentation_level, bool skip_root)
+{
+ return const_cast< NodeRef const* >(this)->visit_stacked(fn, indentation_level, skip_root);
+}
+
+template<class Visitor>
+bool NodeRef::visit_stacked(Visitor fn, size_t indentation_level, bool skip_root) const
+{
+ size_t increment = 0;
+ if( ! (is_root() && skip_root))
+ {
+ if(fn(this, indentation_level))
+ {
+ return true;
+ }
+ ++increment;
+ }
+ if(has_children())
+ {
+ fn.push(this, indentation_level);
+ for(auto ch : children())
+ {
+ if(ch.visit(fn, indentation_level + increment)) // no need to forward skip_root as it won't be root
+ {
+ fn.pop(this, indentation_level);
+ return true;
+ }
+ }
+ fn.pop(this, indentation_level);
+ }
+ return false;
+}
+
+} // namespace yml
+} // namespace c4
+
+
+#if defined(_MSC_VER)
+# pragma warning(pop)
+#endif
+
+#ifdef __GNUC__
+# pragma GCC diagnostic pop
+#endif
+
+#endif /* _C4_YML_NODE_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/node.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/writer.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/writer.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_WRITER_HPP_
+#define _C4_YML_WRITER_HPP_
+
+#ifndef _C4_YML_COMMON_HPP_
+#include "./common.hpp"
+#endif
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/substr.hpp
+//#include <c4/substr.hpp>
+#if !defined(C4_SUBSTR_HPP_) && !defined(_C4_SUBSTR_HPP_)
+#error "amalgamate: file c4/substr.hpp must have been included at this point"
+#endif /* C4_SUBSTR_HPP_ */
+
+//included above:
+//#include <stdio.h> // fwrite(), fputc()
+//included above:
+//#include <string.h> // memcpy()
+
+
+namespace c4 {
+namespace yml {
+
+
+/** Repeat-Character: a character to be written a number of times. */
+struct RepC
+{
+ char c;
+ size_t num_times;
+};
+inline RepC indent_to(size_t num_levels)
+{
+ return {' ', size_t(2) * num_levels};
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** A writer that outputs to a file. Defaults to stdout. */
+struct WriterFile
+{
+ FILE * m_file;
+ size_t m_pos;
+
+ WriterFile(FILE *f = nullptr) : m_file(f ? f : stdout), m_pos(0) {}
+
+ inline substr _get(bool /*error_on_excess*/)
+ {
+ substr sp;
+ sp.str = nullptr;
+ sp.len = m_pos;
+ return sp;
+ }
+
+ template<size_t N>
+ inline void _do_write(const char (&a)[N])
+ {
+ fwrite(a, sizeof(char), N - 1, m_file);
+ m_pos += N - 1;
+ }
+
+ inline void _do_write(csubstr sp)
+ {
+ #if defined(__clang__)
+ # pragma clang diagnostic push
+ # pragma GCC diagnostic ignored "-Wsign-conversion"
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic push
+ # pragma GCC diagnostic ignored "-Wsign-conversion"
+ #endif
+ if(sp.empty()) return;
+ fwrite(sp.str, sizeof(csubstr::char_type), sp.len, m_file);
+ m_pos += sp.len;
+ #if defined(__clang__)
+ # pragma clang diagnostic pop
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic pop
+ #endif
+ }
+
+ inline void _do_write(const char c)
+ {
+ fputc(c, m_file);
+ ++m_pos;
+ }
+
+ inline void _do_write(RepC const rc)
+ {
+ for(size_t i = 0; i < rc.num_times; ++i)
+ {
+ fputc(rc.c, m_file);
+ }
+ m_pos += rc.num_times;
+ }
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** A writer that outputs to an STL-like ostream. */
+template<class OStream>
+struct WriterOStream
+{
+ OStream& m_stream;
+ size_t m_pos;
+
+ WriterOStream(OStream &s) : m_stream(s), m_pos(0) {}
+
+ inline substr _get(bool /*error_on_excess*/)
+ {
+ substr sp;
+ sp.str = nullptr;
+ sp.len = m_pos;
+ return sp;
+ }
+
+ template<size_t N>
+ inline void _do_write(const char (&a)[N])
+ {
+ m_stream.write(a, N - 1);
+ m_pos += N - 1;
+ }
+
+ inline void _do_write(csubstr sp)
+ {
+ #if defined(__clang__)
+ # pragma clang diagnostic push
+ # pragma GCC diagnostic ignored "-Wsign-conversion"
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic push
+ # pragma GCC diagnostic ignored "-Wsign-conversion"
+ #endif
+ if(sp.empty()) return;
+ m_stream.write(sp.str, sp.len);
+ m_pos += sp.len;
+ #if defined(__clang__)
+ # pragma clang diagnostic pop
+ #elif defined(__GNUC__)
+ # pragma GCC diagnostic pop
+ #endif
+ }
+
+ inline void _do_write(const char c)
+ {
+ m_stream.put(c);
+ ++m_pos;
+ }
+
+ inline void _do_write(RepC const rc)
+ {
+ for(size_t i = 0; i < rc.num_times; ++i)
+ {
+ m_stream.put(rc.c);
+ }
+ m_pos += rc.num_times;
+ }
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+/** a writer to a substr */
+struct WriterBuf
+{
+ substr m_buf;
+ size_t m_pos;
+
+ WriterBuf(substr sp) : m_buf(sp), m_pos(0) {}
+
+ inline substr _get(bool error_on_excess)
+ {
+ if(m_pos <= m_buf.len)
+ {
+ return m_buf.first(m_pos);
+ }
+ if(error_on_excess)
+ {
+ c4::yml::error("not enough space in the given buffer");
+ }
+ substr sp;
+ sp.str = nullptr;
+ sp.len = m_pos;
+ return sp;
+ }
+
+ template<size_t N>
+ inline void _do_write(const char (&a)[N])
+ {
+ RYML_ASSERT( ! m_buf.overlaps(a));
+ if(m_pos + N-1 <= m_buf.len)
+ {
+ memcpy(&(m_buf[m_pos]), a, N-1);
+ }
+ m_pos += N-1;
+ }
+
+ inline void _do_write(csubstr sp)
+ {
+ if(sp.empty()) return;
+ RYML_ASSERT( ! sp.overlaps(m_buf));
+ if(m_pos + sp.len <= m_buf.len)
+ {
+ memcpy(&(m_buf[m_pos]), sp.str, sp.len);
+ }
+ m_pos += sp.len;
+ }
+
+ inline void _do_write(const char c)
+ {
+ if(m_pos + 1 <= m_buf.len)
+ {
+ m_buf[m_pos] = c;
+ }
+ ++m_pos;
+ }
+
+ inline void _do_write(RepC const rc)
+ {
+ if(m_pos + rc.num_times <= m_buf.len)
+ {
+ for(size_t i = 0; i < rc.num_times; ++i)
+ {
+ m_buf[m_pos + i] = rc.c;
+ }
+ }
+ m_pos += rc.num_times;
+ }
+};
+
+
+} // namespace yml
+} // namespace c4
+
+#endif /* _C4_YML_WRITER_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/writer.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/detail/parser_dbg.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/parser_dbg.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_DETAIL_PARSER_DBG_HPP_
+#define _C4_YML_DETAIL_PARSER_DBG_HPP_
+
+#ifndef _C4_YML_COMMON_HPP_
+#include "../common.hpp"
+#endif
+//included above:
+//#include <cstdio>
+
+//-----------------------------------------------------------------------------
+// some debugging scaffolds
+
+#if defined(_MSC_VER)
+# pragma warning(push)
+# pragma warning(disable: 4068/*unknown pragma*/)
+#endif
+
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wunknown-pragmas"
+//#pragma GCC diagnostic ignored "-Wpragma-system-header-outside-header"
+#pragma GCC system_header
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Werror"
+#pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments"
+
+// some debugging scaffolds
+#ifdef RYML_DBG
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/dump.hpp
+//#include <c4/dump.hpp>
+#if !defined(C4_DUMP_HPP_) && !defined(_C4_DUMP_HPP_)
+#error "amalgamate: file c4/dump.hpp must have been included at this point"
+#endif /* C4_DUMP_HPP_ */
+
+namespace c4 {
+inline void _dbg_dumper(csubstr s) { fwrite(s.str, 1, s.len, stdout); };
+template<class ...Args>
+void _dbg_printf(c4::csubstr fmt, Args&& ...args)
+{
+ static char writebuf[256];
+ auto results = c4::format_dump_resume<&_dbg_dumper>(writebuf, fmt, std::forward<Args>(args)...);
+ // resume writing if the results failed to fit the buffer
+ if(C4_UNLIKELY(results.bufsize > sizeof(writebuf))) // bufsize will be that of the largest element serialized. Eg int(1), will require 1 byte.
+ {
+ results = format_dump_resume<&_dbg_dumper>(results, writebuf, fmt, std::forward<Args>(args)...);
+ if(C4_UNLIKELY(results.bufsize > sizeof(writebuf)))
+ {
+ results = format_dump_resume<&_dbg_dumper>(results, writebuf, fmt, std::forward<Args>(args)...);
+ }
+ }
+}
+} // namespace c4
+
+# define _c4dbgt(fmt, ...) this->_dbg ("{}:{}: " fmt , __FILE__, __LINE__, ## __VA_ARGS__)
+# define _c4dbgpf(fmt, ...) _dbg_printf("{}:{}: " fmt "\n", __FILE__, __LINE__, ## __VA_ARGS__)
+# define _c4dbgp(msg) _dbg_printf("{}:{}: " msg "\n", __FILE__, __LINE__ )
+# define _c4dbgq(msg) _dbg_printf(msg "\n")
+# define _c4err(fmt, ...) \
+ do { if(c4::is_debugger_attached()) { C4_DEBUG_BREAK(); } \
+ this->_err("ERROR:\n" "{}:{}: " fmt, __FILE__, __LINE__, ## __VA_ARGS__); } while(0)
+#else
+# define _c4dbgt(fmt, ...)
+# define _c4dbgpf(fmt, ...)
+# define _c4dbgp(msg)
+# define _c4dbgq(msg)
+# define _c4err(fmt, ...) \
+ do { if(c4::is_debugger_attached()) { C4_DEBUG_BREAK(); } \
+ this->_err("ERROR: " fmt, ## __VA_ARGS__); } while(0)
+#endif
+
+#define _c4prsp(sp) sp
+#define _c4presc(s) __c4presc(s.str, s.len)
+inline c4::csubstr _c4prc(const char &C4_RESTRICT c)
+{
+ switch(c)
+ {
+ case '\n': return c4::csubstr("\\n");
+ case '\t': return c4::csubstr("\\t");
+ case '\0': return c4::csubstr("\\0");
+ case '\r': return c4::csubstr("\\r");
+ case '\f': return c4::csubstr("\\f");
+ case '\b': return c4::csubstr("\\b");
+ case '\v': return c4::csubstr("\\v");
+ case '\a': return c4::csubstr("\\a");
+ default: return c4::csubstr(&c, 1);
+ }
+}
+inline void __c4presc(const char *s, size_t len)
+{
+ size_t prev = 0;
+ for(size_t i = 0; i < len; ++i)
+ {
+ switch(s[i])
+ {
+ case '\n' : fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('n'); putchar('\n'); prev = i+1; break;
+ case '\t' : fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('t'); prev = i+1; break;
+ case '\0' : fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('0'); prev = i+1; break;
+ case '\r' : fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('r'); prev = i+1; break;
+ case '\f' : fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('f'); prev = i+1; break;
+ case '\b' : fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('b'); prev = i+1; break;
+ case '\v' : fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('v'); prev = i+1; break;
+ case '\a' : fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('a'); prev = i+1; break;
+ case '\x1b': fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('e'); prev = i+1; break;
+ case -0x3e/*0xc2u*/:
+ if(i+1 < len)
+ {
+ if(s[i+1] == -0x60/*0xa0u*/)
+ {
+ fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('_'); prev = i+2; ++i;
+ }
+ else if(s[i+1] == -0x7b/*0x85u*/)
+ {
+ fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('N'); prev = i+2; ++i;
+ }
+ break;
+ }
+ case -0x1e/*0xe2u*/:
+ if(i+2 < len && s[i+1] == -0x80/*0x80u*/)
+ {
+ if(s[i+2] == -0x58/*0xa8u*/)
+ {
+ fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('L'); prev = i+3; i += 2;
+ }
+ else if(s[i+2] == -0x57/*0xa9u*/)
+ {
+ fwrite(s+prev, 1, i-prev, stdout); putchar('\\'); putchar('P'); prev = i+3; i += 2;
+ }
+ break;
+ }
+ }
+ }
+ fwrite(s + prev, 1, len - prev, stdout);
+}
+
+#pragma clang diagnostic pop
+#pragma GCC diagnostic pop
+
+#if defined(_MSC_VER)
+# pragma warning(pop)
+#endif
+
+
+#endif /* _C4_YML_DETAIL_PARSER_DBG_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/detail/parser_dbg.hpp)
+
+#define C4_YML_EMIT_DEF_HPP_
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/emit.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/emit.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_EMIT_HPP_
+#define _C4_YML_EMIT_HPP_
+
+#ifndef _C4_YML_WRITER_HPP_
+#include "./writer.hpp"
+#endif
+
+#ifndef _C4_YML_TREE_HPP_
+#include "./tree.hpp"
+#endif
+
+#ifndef _C4_YML_NODE_HPP_
+#include "./node.hpp"
+#endif
+
+namespace c4 {
+namespace yml {
+
+template<class Writer> class Emitter;
+
+template<class OStream>
+using EmitterOStream = Emitter<WriterOStream<OStream>>;
+using EmitterFile = Emitter<WriterFile>;
+using EmitterBuf = Emitter<WriterBuf>;
+
+typedef enum {
+ EMIT_YAML = 0,
+ EMIT_JSON = 1
+} EmitType_e;
+
+
+/** mark a tree or node to be emitted as json */
+struct as_json
+{
+ Tree const* tree;
+ size_t node;
+ as_json(Tree const& t) : tree(&t), node(t.empty() ? NONE : t.root_id()) {}
+ as_json(Tree const& t, size_t id) : tree(&t), node(id) {}
+ as_json(NodeRef const& n) : tree(n.tree()), node(n.id()) {}
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+template<class Writer>
+class Emitter : public Writer
+{
+public:
+
+ using Writer::Writer;
+
+ /** emit!
+ *
+ * When writing to a buffer, returns a substr of the emitted YAML.
+ * If the given buffer has insufficient space, the returned span will
+ * be null and its size will be the needed space. No writes are done
+ * after the end of the buffer.
+ *
+ * When writing to a file, the returned substr will be null, but its
+ * length will be set to the number of bytes written. */
+ substr emit(EmitType_e type, Tree const& t, size_t id, bool error_on_excess);
+ /** emit starting at the root node */
+ substr emit(EmitType_e type, Tree const& t, bool error_on_excess=true);
+ /** emit the given node */
+ substr emit(EmitType_e type, NodeRef const& n, bool error_on_excess=true);
+
+private:
+
+ Tree const* C4_RESTRICT m_tree;
+
+ void _emit_yaml(size_t id);
+ void _do_visit_flow_sl(size_t id, size_t ilevel=0);
+ void _do_visit_flow_ml(size_t id, size_t ilevel=0, size_t do_indent=1);
+ void _do_visit_block(size_t id, size_t ilevel=0, size_t do_indent=1);
+ void _do_visit_block_container(size_t id, size_t next_level, size_t do_indent);
+ void _do_visit_json(size_t id);
+
+private:
+
+ void _write(NodeScalar const& C4_RESTRICT sc, NodeType flags, size_t level);
+ void _write_json(NodeScalar const& C4_RESTRICT sc, NodeType flags);
+
+ void _write_doc(size_t id);
+ void _write_scalar(csubstr s, bool was_quoted);
+ void _write_scalar_json(csubstr s, bool as_key, bool was_quoted);
+ void _write_scalar_literal(csubstr s, size_t level, bool as_key, bool explicit_indentation=false);
+ void _write_scalar_folded(csubstr s, size_t level, bool as_key);
+ void _write_scalar_squo(csubstr s, size_t level);
+ void _write_scalar_dquo(csubstr s, size_t level);
+ void _write_scalar_plain(csubstr s, size_t level);
+
+ void _write_tag(csubstr tag)
+ {
+ if(!tag.begins_with('!'))
+ this->Writer::_do_write('!');
+ this->Writer::_do_write(tag);
+ }
+
+ enum : type_bits {
+ _keysc = (KEY|KEYREF|KEYANCH|KEYQUO|_WIP_KEY_STYLE) | ~(VAL|VALREF|VALANCH|VALQUO|_WIP_VAL_STYLE),
+ _valsc = ~(KEY|KEYREF|KEYANCH|KEYQUO|_WIP_KEY_STYLE) | (VAL|VALREF|VALANCH|VALQUO|_WIP_VAL_STYLE),
+ _keysc_json = (KEY) | ~(VAL),
+ _valsc_json = ~(KEY) | (VAL),
+ };
+
+ C4_ALWAYS_INLINE void _writek(size_t id, size_t level) { _write(m_tree->keysc(id), m_tree->_p(id)->m_type.type & ~_valsc, level); }
+ C4_ALWAYS_INLINE void _writev(size_t id, size_t level) { _write(m_tree->valsc(id), m_tree->_p(id)->m_type.type & ~_keysc, level); }
+
+ C4_ALWAYS_INLINE void _writek_json(size_t id) { _write_json(m_tree->keysc(id), m_tree->_p(id)->m_type.type & ~(VAL)); }
+ C4_ALWAYS_INLINE void _writev_json(size_t id) { _write_json(m_tree->valsc(id), m_tree->_p(id)->m_type.type & ~(KEY)); }
+
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** emit YAML to the given file. A null file defaults to stdout.
+ * Return the number of bytes written. */
+inline size_t emit(Tree const& t, size_t id, FILE *f)
+{
+ EmitterFile em(f);
+ return em.emit(EMIT_YAML, t, id, /*error_on_excess*/true).len;
+}
+/** emit JSON to the given file. A null file defaults to stdout.
+ * Return the number of bytes written. */
+inline size_t emit_json(Tree const& t, size_t id, FILE *f)
+{
+ EmitterFile em(f);
+ return em.emit(EMIT_JSON, t, id, /*error_on_excess*/true).len;
+}
+
+
+/** emit YAML to the given file. A null file defaults to stdout.
+ * Return the number of bytes written.
+ * @overload */
+inline size_t emit(Tree const& t, FILE *f=nullptr)
+{
+ EmitterFile em(f);
+ return em.emit(EMIT_YAML, t, /*error_on_excess*/true).len;
+}
+
+/** emit JSON to the given file. A null file defaults to stdout.
+ * Return the number of bytes written.
+ * @overload */
+inline size_t emit_json(Tree const& t, FILE *f=nullptr)
+{
+ EmitterFile em(f);
+ return em.emit(EMIT_JSON, t, /*error_on_excess*/true).len;
+}
+
+
+/** emit YAML to the given file. A null file defaults to stdout.
+ * Return the number of bytes written.
+ * @overload */
+inline size_t emit(NodeRef const& r, FILE *f=nullptr)
+{
+ EmitterFile em(f);
+ return em.emit(EMIT_YAML, r, /*error_on_excess*/true).len;
+}
+
+/** emit JSON to the given file. A null file defaults to stdout.
+ * Return the number of bytes written.
+ * @overload */
+inline size_t emit_json(NodeRef const& r, FILE *f=nullptr)
+{
+ EmitterFile em(f);
+ return em.emit(EMIT_JSON, r, /*error_on_excess*/true).len;
+}
+
+
+//-----------------------------------------------------------------------------
+
+/** emit YAML to an STL-like ostream */
+template<class OStream>
+inline OStream& operator<< (OStream& s, Tree const& t)
+{
+ EmitterOStream<OStream> em(s);
+ em.emit(EMIT_YAML, t);
+ return s;
+}
+
+/** emit YAML to an STL-like ostream
+ * @overload */
+template<class OStream>
+inline OStream& operator<< (OStream& s, NodeRef const& n)
+{
+ EmitterOStream<OStream> em(s);
+ em.emit(EMIT_YAML, n);
+ return s;
+}
+
+/** emit json to an STL-like stream */
+template<class OStream>
+inline OStream& operator<< (OStream& s, as_json const& j)
+{
+ EmitterOStream<OStream> em(s);
+ em.emit(EMIT_JSON, *j.tree, j.node, true);
+ return s;
+}
+
+
+//-----------------------------------------------------------------------------
+
+
+/** emit YAML to the given buffer. Return a substr trimmed to the emitted YAML.
+ * @param error_on_excess Raise an error if the space in the buffer is insufficient.
+ * @overload */
+inline substr emit(Tree const& t, size_t id, substr buf, bool error_on_excess=true)
+{
+ EmitterBuf em(buf);
+ return em.emit(EMIT_YAML, t, id, error_on_excess);
+}
+
+/** emit JSON to the given buffer. Return a substr trimmed to the emitted JSON.
+ * @param error_on_excess Raise an error if the space in the buffer is insufficient.
+ * @overload */
+inline substr emit_json(Tree const& t, size_t id, substr buf, bool error_on_excess=true)
+{
+ EmitterBuf em(buf);
+ return em.emit(EMIT_JSON, t, id, error_on_excess);
+}
+
+
+/** emit YAML to the given buffer. Return a substr trimmed to the emitted YAML.
+ * @param error_on_excess Raise an error if the space in the buffer is insufficient.
+ * @overload */
+inline substr emit(Tree const& t, substr buf, bool error_on_excess=true)
+{
+ EmitterBuf em(buf);
+ return em.emit(EMIT_YAML, t, error_on_excess);
+}
+
+/** emit JSON to the given buffer. Return a substr trimmed to the emitted JSON.
+ * @param error_on_excess Raise an error if the space in the buffer is insufficient.
+ * @overload */
+inline substr emit_json(Tree const& t, substr buf, bool error_on_excess=true)
+{
+ EmitterBuf em(buf);
+ return em.emit(EMIT_JSON, t, error_on_excess);
+}
+
+
+/** emit YAML to the given buffer. Return a substr trimmed to the emitted YAML.
+ * @param error_on_excess Raise an error if the space in the buffer is insufficient.
+ * @overload
+ */
+inline substr emit(NodeRef const& r, substr buf, bool error_on_excess=true)
+{
+ EmitterBuf em(buf);
+ return em.emit(EMIT_YAML, r, error_on_excess);
+}
+
+/** emit JSON to the given buffer. Return a substr trimmed to the emitted JSON.
+ * @param error_on_excess Raise an error if the space in the buffer is insufficient.
+ * @overload
+ */
+inline substr emit_json(NodeRef const& r, substr buf, bool error_on_excess=true)
+{
+ EmitterBuf em(buf);
+ return em.emit(EMIT_JSON, r, error_on_excess);
+}
+
+
+//-----------------------------------------------------------------------------
+
+/** emit+resize: emit YAML to the given std::string/std::vector-like
+ * container, resizing it as needed to fit the emitted YAML. */
+template<class CharOwningContainer>
+substr emitrs(Tree const& t, size_t id, CharOwningContainer * cont)
+{
+ substr buf = to_substr(*cont);
+ substr ret = emit(t, id, buf, /*error_on_excess*/false);
+ if(ret.str == nullptr && ret.len > 0)
+ {
+ cont->resize(ret.len);
+ buf = to_substr(*cont);
+ ret = emit(t, id, buf, /*error_on_excess*/true);
+ }
+ return ret;
+}
+
+/** emit+resize: emit JSON to the given std::string/std::vector-like
+ * container, resizing it as needed to fit the emitted JSON. */
+template<class CharOwningContainer>
+substr emitrs_json(Tree const& t, size_t id, CharOwningContainer * cont)
+{
+ substr buf = to_substr(*cont);
+ substr ret = emit_json(t, id, buf, /*error_on_excess*/false);
+ if(ret.str == nullptr && ret.len > 0)
+ {
+ cont->resize(ret.len);
+ buf = to_substr(*cont);
+ ret = emit_json(t, id, buf, /*error_on_excess*/true);
+ }
+ return ret;
+}
+
+
+/** emit+resize: emit YAML to the given std::string/std::vector-like
+ * container, resizing it as needed to fit the emitted YAML. */
+template<class CharOwningContainer>
+CharOwningContainer emitrs(Tree const& t, size_t id)
+{
+ CharOwningContainer c;
+ emitrs(t, id, &c);
+ return c;
+}
+
+/** emit+resize: emit JSON to the given std::string/std::vector-like container,
+ * resizing it as needed to fit the emitted JSON. */
+template<class CharOwningContainer>
+CharOwningContainer emitrs_json(Tree const& t, size_t id)
+{
+ CharOwningContainer c;
+ emitrs_json(t, id, &c);
+ return c;
+}
+
+
+/** emit+resize: YAML to the given std::string/std::vector-like container,
+ * resizing it as needed to fit the emitted YAML. */
+template<class CharOwningContainer>
+substr emitrs(Tree const& t, CharOwningContainer * cont)
+{
+ if(t.empty())
+ return {};
+ return emitrs(t, t.root_id(), cont);
+}
+
+/** emit+resize: JSON to the given std::string/std::vector-like container,
+ * resizing it as needed to fit the emitted JSON. */
+template<class CharOwningContainer>
+substr emitrs_json(Tree const& t, CharOwningContainer * cont)
+{
+ if(t.empty())
+ return {};
+ return emitrs_json(t, t.root_id(), cont);
+}
+
+
+/** emit+resize: YAML to the given std::string/std::vector-like container,
+ * resizing it as needed to fit the emitted YAML. */
+template<class CharOwningContainer>
+CharOwningContainer emitrs(Tree const& t)
+{
+ CharOwningContainer c;
+ if(t.empty())
+ return c;
+ emitrs(t, t.root_id(), &c);
+ return c;
+}
+
+/** emit+resize: JSON to the given std::string/std::vector-like container,
+ * resizing it as needed to fit the emitted JSON. */
+template<class CharOwningContainer>
+CharOwningContainer emitrs_json(Tree const& t)
+{
+ CharOwningContainer c;
+ if(t.empty())
+ return c;
+ emitrs_json(t, t.root_id(), &c);
+ return c;
+}
+
+
+/** emit+resize: YAML to the given std::string/std::vector-like container,
+ * resizing it as needed to fit the emitted YAML. */
+template<class CharOwningContainer>
+substr emitrs(NodeRef const& n, CharOwningContainer * cont)
+{
+ _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+ return emitrs(*n.tree(), n.id(), cont);
+}
+
+/** emit+resize: JSON to the given std::string/std::vector-like container,
+ * resizing it as needed to fit the emitted JSON. */
+template<class CharOwningContainer>
+substr emitrs_json(NodeRef const& n, CharOwningContainer * cont)
+{
+ _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+ return emitrs_json(*n.tree(), n.id(), cont);
+}
+
+
+/** emit+resize: YAML to the given std::string/std::vector-like container,
+ * resizing it as needed to fit the emitted YAML. */
+template<class CharOwningContainer>
+CharOwningContainer emitrs(NodeRef const& n)
+{
+ _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+ CharOwningContainer c;
+ emitrs(*n.tree(), n.id(), &c);
+ return c;
+}
+
+/** emit+resize: JSON to the given std::string/std::vector-like container,
+ * resizing it as needed to fit the emitted JSON. */
+template<class CharOwningContainer>
+CharOwningContainer emitrs_json(NodeRef const& n)
+{
+ _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+ CharOwningContainer c;
+ emitrs_json(*n.tree(), n.id(), &c);
+ return c;
+}
+
+} // namespace yml
+} // namespace c4
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/emit.def.hpp
+//#include "c4/yml/emit.def.hpp"
+#if !defined(C4_YML_EMIT_DEF_HPP_) && !defined(_C4_YML_EMIT_DEF_HPP_)
+#error "amalgamate: file c4/yml/emit.def.hpp must have been included at this point"
+#endif /* C4_YML_EMIT_DEF_HPP_ */
+
+
+#endif /* _C4_YML_EMIT_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/emit.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/emit.def.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/emit.def.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_EMIT_DEF_HPP_
+#define _C4_YML_EMIT_DEF_HPP_
+
+#ifndef _C4_YML_EMIT_HPP_
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/emit.hpp
+//#include "c4/yml/emit.hpp"
+#if !defined(C4_YML_EMIT_HPP_) && !defined(_C4_YML_EMIT_HPP_)
+#error "amalgamate: file c4/yml/emit.hpp must have been included at this point"
+#endif /* C4_YML_EMIT_HPP_ */
+
+#endif
+
+namespace c4 {
+namespace yml {
+
+template<class Writer>
+substr Emitter<Writer>::emit(EmitType_e type, Tree const& t, size_t id, bool error_on_excess)
+{
+ if(t.empty())
+ {
+ _RYML_CB_ASSERT(t.callbacks(), id == NONE);
+ return {};
+ }
+ _RYML_CB_CHECK(t.callbacks(), id < t.size());
+ m_tree = &t;
+ if(type == EMIT_YAML)
+ _emit_yaml(id);
+ else if(type == EMIT_JSON)
+ _do_visit_json(id);
+ else
+ _RYML_CB_ERR(m_tree->callbacks(), "unknown emit type");
+ return this->Writer::_get(error_on_excess);
+}
+
+template<class Writer>
+substr Emitter<Writer>::emit(EmitType_e type, Tree const& t, bool error_on_excess)
+{
+ if(t.empty())
+ return {};
+ return emit(type, t, t.root_id(), error_on_excess);
+}
+
+template<class Writer>
+substr Emitter<Writer>::emit(EmitType_e type, NodeRef const& n, bool error_on_excess)
+{
+ _RYML_CB_CHECK(n.tree()->callbacks(), n.valid());
+ return emit(type, *n.tree(), n.id(), error_on_excess);
+}
+
+
+//-----------------------------------------------------------------------------
+
+template<class Writer>
+void Emitter<Writer>::_emit_yaml(size_t id)
+{
+ // save branches in the visitor by doing the initial stream/doc
+ // logic here, sparing the need to check stream/val/keyval inside
+ // the visitor functions
+ auto dispatch = [this](size_t node){
+ NodeType ty = m_tree->type(node);
+ if(ty.marked_flow_sl())
+ _do_visit_flow_sl(node, 0);
+ else if(ty.marked_flow_ml())
+ _do_visit_flow_ml(node, 0);
+ else
+ {
+ _do_visit_block(node, 0);
+ }
+ };
+ if(!m_tree->is_root(id))
+ {
+ if(m_tree->is_container(id) && !m_tree->type(id).marked_flow())
+ {
+ size_t ilevel = 0;
+ if(m_tree->has_key(id))
+ {
+ this->Writer::_do_write(m_tree->key(id));
+ this->Writer::_do_write(":\n");
+ ++ilevel;
+ }
+ _do_visit_block_container(id, ilevel, ilevel);
+ return;
+ }
+ }
+
+ auto *btd = m_tree->tag_directives().b;
+ auto *etd = m_tree->tag_directives().e;
+ auto write_tag_directives = [&btd, etd, this](size_t next_node){
+ auto end = btd;
+ while(end < etd)
+ {
+ if(end->next_node_id > next_node)
+ break;
+ ++end;
+ }
+ for( ; btd != end; ++btd)
+ {
+ if(next_node != m_tree->first_child(m_tree->parent(next_node)))
+ this->Writer::_do_write("...\n");
+ this->Writer::_do_write("%TAG ");
+ this->Writer::_do_write(btd->handle);
+ this->Writer::_do_write(' ');
+ this->Writer::_do_write(btd->prefix);
+ this->Writer::_do_write('\n');
+ }
+ };
+ if(m_tree->is_stream(id))
+ {
+ if(m_tree->first_child(id) != NONE)
+ write_tag_directives(m_tree->first_child(id));
+ for(size_t child = m_tree->first_child(id); child != NONE; child = m_tree->next_sibling(child))
+ {
+ dispatch(child);
+ if(m_tree->next_sibling(child) != NONE)
+ write_tag_directives(m_tree->next_sibling(child));
+ }
+ }
+ else if(m_tree->is_container(id))
+ {
+ dispatch(id);
+ }
+ else if(m_tree->is_doc(id))
+ {
+ _RYML_CB_ASSERT(m_tree->callbacks(), !m_tree->is_container(id)); // checked above
+ _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_val(id)); // so it must be a val
+ _write_doc(id);
+ }
+ else if(m_tree->is_keyval(id))
+ {
+ _writek(id, 0);
+ this->Writer::_do_write(": ");
+ _writev(id, 0);
+ if(!m_tree->type(id).marked_flow())
+ this->Writer::_do_write('\n');
+ }
+ else if(m_tree->is_val(id))
+ {
+ //this->Writer::_do_write("- ");
+ _writev(id, 0);
+ if(!m_tree->type(id).marked_flow())
+ this->Writer::_do_write('\n');
+ }
+ else if(m_tree->type(id) == NOTYPE)
+ {
+ ;
+ }
+ else
+ {
+ _RYML_CB_ERR(m_tree->callbacks(), "unknown type");
+ }
+}
+
+template<class Writer>
+void Emitter<Writer>::_write_doc(size_t id)
+{
+ RYML_ASSERT(m_tree->is_doc(id));
+ if(!m_tree->is_root(id))
+ {
+ RYML_ASSERT(m_tree->is_stream(m_tree->parent(id)));
+ this->Writer::_do_write("---");
+ }
+ if(!m_tree->has_val(id)) // this is more frequent
+ {
+ if(m_tree->has_val_tag(id))
+ {
+ if(!m_tree->is_root(id))
+ this->Writer::_do_write(' ');
+ _write_tag(m_tree->val_tag(id));
+ }
+ if(m_tree->has_val_anchor(id))
+ {
+ if(!m_tree->is_root(id))
+ this->Writer::_do_write(' ');
+ this->Writer::_do_write('&');
+ this->Writer::_do_write(m_tree->val_anchor(id));
+ }
+ }
+ else // docval
+ {
+ RYML_ASSERT(m_tree->has_val(id));
+ RYML_ASSERT(!m_tree->has_key(id));
+ if(!m_tree->is_root(id))
+ this->Writer::_do_write(' ');
+ _writev(id, 0);
+ }
+ this->Writer::_do_write('\n');
+}
+
+template<class Writer>
+void Emitter<Writer>::_do_visit_flow_sl(size_t node, size_t ilevel)
+{
+ RYML_ASSERT(!m_tree->is_stream(node));
+ RYML_ASSERT(m_tree->is_container(node) || m_tree->is_doc(node));
+ RYML_ASSERT(m_tree->is_root(node) || (m_tree->parent_is_map(node) || m_tree->parent_is_seq(node)));
+
+ if(m_tree->is_doc(node))
+ {
+ _write_doc(node);
+ if(!m_tree->has_children(node))
+ return;
+ }
+ else if(m_tree->is_container(node))
+ {
+ RYML_ASSERT(m_tree->is_map(node) || m_tree->is_seq(node));
+
+ bool spc = false; // write a space
+
+ if(m_tree->has_key(node))
+ {
+ _writek(node, ilevel);
+ this->Writer::_do_write(':');
+ spc = true;
+ }
+
+ if(m_tree->has_val_tag(node))
+ {
+ if(spc)
+ this->Writer::_do_write(' ');
+ _write_tag(m_tree->val_tag(node));
+ spc = true;
+ }
+
+ if(m_tree->has_val_anchor(node))
+ {
+ if(spc)
+ this->Writer::_do_write(' ');
+ this->Writer::_do_write('&');
+ this->Writer::_do_write(m_tree->val_anchor(node));
+ spc = true;
+ }
+
+ if(spc)
+ this->Writer::_do_write(' ');
+
+ if(m_tree->is_map(node))
+ {
+ this->Writer::_do_write('{');
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_seq(node));
+ this->Writer::_do_write('[');
+ }
+ } // container
+
+ for(size_t child = m_tree->first_child(node), count = 0; child != NONE; child = m_tree->next_sibling(child))
+ {
+ if(count++)
+ this->Writer::_do_write(',');
+ if(m_tree->is_keyval(child))
+ {
+ _writek(child, ilevel);
+ this->Writer::_do_write(": ");
+ _writev(child, ilevel);
+ }
+ else if(m_tree->is_val(child))
+ {
+ _writev(child, ilevel);
+ }
+ else
+ {
+ // with single-line flow, we can never go back to block
+ _do_visit_flow_sl(child, ilevel + 1);
+ }
+ }
+
+ if(m_tree->is_map(node))
+ {
+ this->Writer::_do_write('}');
+ }
+ else if(m_tree->is_seq(node))
+ {
+ this->Writer::_do_write(']');
+ }
+}
+
+template<class Writer>
+void Emitter<Writer>::_do_visit_flow_ml(size_t id, size_t ilevel, size_t do_indent)
+{
+ C4_UNUSED(id);
+ C4_UNUSED(ilevel);
+ C4_UNUSED(do_indent);
+ RYML_CHECK(false/*not implemented*/);
+}
+
+template<class Writer>
+void Emitter<Writer>::_do_visit_block_container(size_t node, size_t next_level, size_t do_indent)
+{
+ RepC ind = indent_to(do_indent * next_level);
+
+ if(m_tree->is_seq(node))
+ {
+ for(size_t child = m_tree->first_child(node); child != NONE; child = m_tree->next_sibling(child))
+ {
+ _RYML_CB_ASSERT(m_tree->callbacks(), !m_tree->has_key(child));
+ if(m_tree->is_val(child))
+ {
+ this->Writer::_do_write(ind);
+ this->Writer::_do_write("- ");
+ _writev(child, next_level);
+ this->Writer::_do_write('\n');
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_container(child));
+ NodeType ty = m_tree->type(child);
+ if(ty.marked_flow_sl())
+ {
+ this->Writer::_do_write(ind);
+ this->Writer::_do_write("- ");
+ _do_visit_flow_sl(child, 0u);
+ this->Writer::_do_write('\n');
+ }
+ else if(ty.marked_flow_ml())
+ {
+ this->Writer::_do_write(ind);
+ this->Writer::_do_write("- ");
+ _do_visit_flow_ml(child, next_level, do_indent);
+ this->Writer::_do_write('\n');
+ }
+ else
+ {
+ _do_visit_block(child, next_level, do_indent);
+ }
+ }
+ do_indent = true;
+ ind = indent_to(do_indent * next_level);
+ }
+ }
+ else // map
+ {
+ _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_map(node));
+ for(size_t ich = m_tree->first_child(node); ich != NONE; ich = m_tree->next_sibling(ich))
+ {
+ _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->has_key(ich));
+ if(m_tree->is_keyval(ich))
+ {
+ this->Writer::_do_write(ind);
+ _writek(ich, next_level);
+ this->Writer::_do_write(": ");
+ _writev(ich, next_level);
+ this->Writer::_do_write('\n');
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_tree->callbacks(), m_tree->is_container(ich));
+ NodeType ty = m_tree->type(ich);
+ if(ty.marked_flow_sl())
+ {
+ this->Writer::_do_write(ind);
+ _do_visit_flow_sl(ich, 0u);
+ this->Writer::_do_write('\n');
+ }
+ else if(ty.marked_flow_ml())
+ {
+ this->Writer::_do_write(ind);
+ _do_visit_flow_ml(ich, 0u);
+ this->Writer::_do_write('\n');
+ }
+ else
+ {
+ _do_visit_block(ich, next_level, do_indent);
+ }
+ }
+ do_indent = true;
+ ind = indent_to(do_indent * next_level);
+ }
+ }
+}
+
+template<class Writer>
+void Emitter<Writer>::_do_visit_block(size_t node, size_t ilevel, size_t do_indent)
+{
+ RYML_ASSERT(!m_tree->is_stream(node));
+ RYML_ASSERT(m_tree->is_container(node) || m_tree->is_doc(node));
+ RYML_ASSERT(m_tree->is_root(node) || (m_tree->parent_is_map(node) || m_tree->parent_is_seq(node)));
+ RepC ind = indent_to(do_indent * ilevel);
+
+ if(m_tree->is_doc(node))
+ {
+ _write_doc(node);
+ if(!m_tree->has_children(node))
+ return;
+ }
+ else if(m_tree->is_container(node))
+ {
+ RYML_ASSERT(m_tree->is_map(node) || m_tree->is_seq(node));
+
+ bool spc = false; // write a space
+ bool nl = false; // write a newline
+
+ if(m_tree->has_key(node))
+ {
+ this->Writer::_do_write(ind);
+ _writek(node, ilevel);
+ this->Writer::_do_write(':');
+ spc = true;
+ }
+ else if(!m_tree->is_root(node))
+ {
+ this->Writer::_do_write(ind);
+ this->Writer::_do_write('-');
+ spc = true;
+ }
+
+ if(m_tree->has_val_tag(node))
+ {
+ if(spc)
+ this->Writer::_do_write(' ');
+ _write_tag(m_tree->val_tag(node));
+ spc = true;
+ nl = true;
+ }
+
+ if(m_tree->has_val_anchor(node))
+ {
+ if(spc)
+ this->Writer::_do_write(' ');
+ this->Writer::_do_write('&');
+ this->Writer::_do_write(m_tree->val_anchor(node));
+ spc = true;
+ nl = true;
+ }
+
+ if(m_tree->has_children(node))
+ {
+ if(m_tree->has_key(node))
+ nl = true;
+ else
+ if(!m_tree->is_root(node) && !nl)
+ spc = true;
+ }
+ else
+ {
+ if(m_tree->is_seq(node))
+ this->Writer::_do_write(" []\n");
+ else if(m_tree->is_map(node))
+ this->Writer::_do_write(" {}\n");
+ return;
+ }
+
+ if(spc && !nl)
+ this->Writer::_do_write(' ');
+
+ do_indent = 0;
+ if(nl)
+ {
+ this->Writer::_do_write('\n');
+ do_indent = 1;
+ }
+ } // container
+
+ size_t next_level = ilevel + 1;
+ if(m_tree->is_root(node) || m_tree->is_doc(node))
+ next_level = ilevel; // do not indent at top level
+
+ _do_visit_block_container(node, next_level, do_indent);
+}
+
+template<class Writer>
+void Emitter<Writer>::_do_visit_json(size_t id)
+{
+ _RYML_CB_CHECK(m_tree->callbacks(), !m_tree->is_stream(id)); // JSON does not have streams
+ if(m_tree->is_keyval(id))
+ {
+ _writek_json(id);
+ this->Writer::_do_write(": ");
+ _writev_json(id);
+ }
+ else if(m_tree->is_val(id))
+ {
+ _writev_json(id);
+ }
+ else if(m_tree->is_container(id))
+ {
+ if(m_tree->has_key(id))
+ {
+ _writek_json(id);
+ this->Writer::_do_write(": ");
+ }
+ if(m_tree->is_seq(id))
+ this->Writer::_do_write('[');
+ else if(m_tree->is_map(id))
+ this->Writer::_do_write('{');
+ } // container
+
+ for(size_t ich = m_tree->first_child(id); ich != NONE; ich = m_tree->next_sibling(ich))
+ {
+ if(ich != m_tree->first_child(id))
+ this->Writer::_do_write(',');
+ _do_visit_json(ich);
+ }
+
+ if(m_tree->is_seq(id))
+ this->Writer::_do_write(']');
+ else if(m_tree->is_map(id))
+ this->Writer::_do_write('}');
+}
+
+template<class Writer>
+void Emitter<Writer>::_write(NodeScalar const& C4_RESTRICT sc, NodeType flags, size_t ilevel)
+{
+ if( ! sc.tag.empty())
+ {
+ _write_tag(sc.tag);
+ this->Writer::_do_write(' ');
+ }
+ if(flags.has_anchor())
+ {
+ RYML_ASSERT(flags.is_ref() != flags.has_anchor());
+ RYML_ASSERT( ! sc.anchor.empty());
+ this->Writer::_do_write('&');
+ this->Writer::_do_write(sc.anchor);
+ this->Writer::_do_write(' ');
+ }
+ else if(flags.is_ref())
+ {
+ if(sc.anchor != "<<")
+ this->Writer::_do_write('*');
+ this->Writer::_do_write(sc.anchor);
+ return;
+ }
+
+ // ensure the style flags only have one of KEY or VAL
+ _RYML_CB_ASSERT(m_tree->callbacks(), ((flags & (_WIP_KEY_STYLE|_WIP_VAL_STYLE)) == 0) || (((flags&_WIP_KEY_STYLE) == 0) != ((flags&_WIP_VAL_STYLE) == 0)));
+
+ auto style_marks = flags & (_WIP_KEY_STYLE|_WIP_VAL_STYLE);
+ if(style_marks & (_WIP_KEY_LITERAL|_WIP_VAL_LITERAL))
+ {
+ _write_scalar_literal(sc.scalar, ilevel, flags.has_key());
+ }
+ else if(style_marks & (_WIP_KEY_FOLDED|_WIP_VAL_FOLDED))
+ {
+ _write_scalar_folded(sc.scalar, ilevel, flags.has_key());
+ }
+ else if(style_marks & (_WIP_KEY_SQUO|_WIP_VAL_SQUO))
+ {
+ _write_scalar_squo(sc.scalar, ilevel);
+ }
+ else if(style_marks & (_WIP_KEY_DQUO|_WIP_VAL_DQUO))
+ {
+ _write_scalar_dquo(sc.scalar, ilevel);
+ }
+ else if(style_marks & (_WIP_KEY_PLAIN|_WIP_VAL_PLAIN))
+ {
+ _write_scalar_plain(sc.scalar, ilevel);
+ }
+ else if(!style_marks)
+ {
+ size_t first_non_nl = sc.scalar.first_not_of('\n');
+ bool all_newlines = first_non_nl == npos;
+ bool has_leading_ws = (!all_newlines) && sc.scalar.sub(first_non_nl).begins_with_any(" \t");
+ bool do_literal = ((!sc.scalar.empty() && all_newlines) || (has_leading_ws && !sc.scalar.trim(' ').empty()));
+ if(do_literal)
+ {
+ _write_scalar_literal(sc.scalar, ilevel, flags.has_key(), /*explicit_indentation*/has_leading_ws);
+ }
+ else
+ {
+ for(size_t i = 0; i < sc.scalar.len; ++i)
+ {
+ if(sc.scalar.str[i] == '\n')
+ {
+ _write_scalar_literal(sc.scalar, ilevel, flags.has_key(), /*explicit_indentation*/has_leading_ws);
+ goto wrote_special;
+ }
+ // todo: check for escaped characters requiring double quotes
+ }
+ _write_scalar(sc.scalar, flags.is_quoted());
+ wrote_special:
+ ;
+ }
+ }
+ else
+ {
+ _RYML_CB_ERR(m_tree->callbacks(), "not implemented");
+ }
+}
+template<class Writer>
+void Emitter<Writer>::_write_json(NodeScalar const& C4_RESTRICT sc, NodeType flags)
+{
+ if(C4_UNLIKELY( ! sc.tag.empty()))
+ _RYML_CB_ERR(m_tree->callbacks(), "JSON does not have tags");
+ if(C4_UNLIKELY(flags.has_anchor()))
+ _RYML_CB_ERR(m_tree->callbacks(), "JSON does not have anchors");
+ _write_scalar_json(sc.scalar, flags.has_key(), flags.is_quoted());
+}
+
+#define _rymlindent_nextline() for(size_t lv = 0; lv < ilevel+1; ++lv) { this->Writer::_do_write(' '); this->Writer::_do_write(' '); }
+
+template<class Writer>
+void Emitter<Writer>::_write_scalar_literal(csubstr s, size_t ilevel, bool explicit_key, bool explicit_indentation)
+{
+ if(explicit_key)
+ this->Writer::_do_write("? ");
+ csubstr trimmed = s.trimr("\n\r");
+ size_t numnewlines_at_end = s.len - trimmed.len - s.sub(trimmed.len).count('\r');
+ //
+ if(!explicit_indentation)
+ this->Writer::_do_write('|');
+ else
+ this->Writer::_do_write("|2");
+ //
+ if(numnewlines_at_end > 1 || (trimmed.len == 0 && s.len > 0)/*only newlines*/)
+ this->Writer::_do_write("+\n");
+ else if(numnewlines_at_end == 1)
+ this->Writer::_do_write('\n');
+ else
+ this->Writer::_do_write("-\n");
+ //
+ if(trimmed.len)
+ {
+ size_t pos = 0; // tracks the last character that was already written
+ for(size_t i = 0; i < trimmed.len; ++i)
+ {
+ if(trimmed[i] != '\n')
+ continue;
+ // write everything up to this point
+ csubstr since_pos = trimmed.range(pos, i+1); // include the newline
+ _rymlindent_nextline()
+ this->Writer::_do_write(since_pos);
+ pos = i+1; // already written
+ }
+ if(pos < trimmed.len)
+ {
+ _rymlindent_nextline()
+ this->Writer::_do_write(trimmed.sub(pos));
+ }
+ if(numnewlines_at_end)
+ {
+ this->Writer::_do_write('\n');
+ --numnewlines_at_end;
+ }
+ }
+ for(size_t i = 0; i < numnewlines_at_end; ++i)
+ {
+ _rymlindent_nextline()
+ if(i+1 < numnewlines_at_end || explicit_key)
+ this->Writer::_do_write('\n');
+ }
+ if(explicit_key && !numnewlines_at_end)
+ this->Writer::_do_write('\n');
+}
+
+template<class Writer>
+void Emitter<Writer>::_write_scalar_folded(csubstr s, size_t ilevel, bool explicit_key)
+{
+ if(explicit_key)
+ {
+ this->Writer::_do_write("? ");
+ }
+ RYML_ASSERT(s.find("\r") == csubstr::npos);
+ csubstr trimmed = s.trimr('\n');
+ size_t numnewlines_at_end = s.len - trimmed.len;
+ if(numnewlines_at_end == 0)
+ {
+ this->Writer::_do_write(">-\n");
+ }
+ else if(numnewlines_at_end == 1)
+ {
+ this->Writer::_do_write(">\n");
+ }
+ else if(numnewlines_at_end > 1)
+ {
+ this->Writer::_do_write(">+\n");
+ }
+ if(trimmed.len)
+ {
+ size_t pos = 0; // tracks the last character that was already written
+ for(size_t i = 0; i < trimmed.len; ++i)
+ {
+ if(trimmed[i] != '\n')
+ continue;
+ // write everything up to this point
+ csubstr since_pos = trimmed.range(pos, i+1); // include the newline
+ pos = i+1; // because of the newline
+ _rymlindent_nextline()
+ this->Writer::_do_write(since_pos);
+ this->Writer::_do_write('\n'); // write the newline twice
+ }
+ if(pos < trimmed.len)
+ {
+ _rymlindent_nextline()
+ this->Writer::_do_write(trimmed.sub(pos));
+ }
+ if(numnewlines_at_end)
+ {
+ this->Writer::_do_write('\n');
+ --numnewlines_at_end;
+ }
+ }
+ for(size_t i = 0; i < numnewlines_at_end; ++i)
+ {
+ _rymlindent_nextline()
+ if(i+1 < numnewlines_at_end || explicit_key)
+ this->Writer::_do_write('\n');
+ }
+ if(explicit_key && !numnewlines_at_end)
+ this->Writer::_do_write('\n');
+}
+
+template<class Writer>
+void Emitter<Writer>::_write_scalar_squo(csubstr s, size_t ilevel)
+{
+ size_t pos = 0; // tracks the last character that was already written
+ this->Writer::_do_write('\'');
+ for(size_t i = 0; i < s.len; ++i)
+ {
+ if(s[i] == '\n')
+ {
+ csubstr sub = s.range(pos, i+1);
+ this->Writer::_do_write(sub); // write everything up to (including) this char
+ this->Writer::_do_write('\n'); // write the character again
+ if(i + 1 < s.len)
+ _rymlindent_nextline() // indent the next line
+ pos = i+1;
+ }
+ else if(s[i] == '\'')
+ {
+ csubstr sub = s.range(pos, i+1);
+ this->Writer::_do_write(sub); // write everything up to (including) this char
+ this->Writer::_do_write('\''); // write the character again
+ pos = i+1;
+ }
+ }
+ // write missing characters at the end of the string
+ if(pos < s.len)
+ this->Writer::_do_write(s.sub(pos));
+ this->Writer::_do_write('\'');
+}
+
+template<class Writer>
+void Emitter<Writer>::_write_scalar_dquo(csubstr s, size_t ilevel)
+{
+ size_t pos = 0; // tracks the last character that was already written
+ this->Writer::_do_write('"');
+ for(size_t i = 0; i < s.len; ++i)
+ {
+ const char curr = s.str[i];
+ if(curr == '"' || curr == '\\')
+ {
+ csubstr sub = s.range(pos, i);
+ this->Writer::_do_write(sub); // write everything up to (excluding) this char
+ this->Writer::_do_write('\\'); // write the escape
+ this->Writer::_do_write(curr); // write the char
+ pos = i+1;
+ }
+ else if(s[i] == '\n')
+ {
+ csubstr sub = s.range(pos, i+1);
+ this->Writer::_do_write(sub); // write everything up to (including) this newline
+ this->Writer::_do_write('\n'); // write the newline again
+ if(i + 1 < s.len)
+ _rymlindent_nextline() // indent the next line
+ pos = i+1;
+ if(i+1 < s.len) // escape leading whitespace after the newline
+ {
+ const char next = s.str[i+1];
+ if(next == ' ' || next == '\t')
+ this->Writer::_do_write('\\');
+ }
+ }
+ else if(curr == ' ' || curr == '\t')
+ {
+ // escape trailing whitespace before a newline
+ size_t next = s.first_not_of(" \t\r", i);
+ if(next != npos && s[next] == '\n')
+ {
+ csubstr sub = s.range(pos, i);
+ this->Writer::_do_write(sub); // write everything up to (excluding) this char
+ this->Writer::_do_write('\\'); // escape the whitespace
+ pos = i;
+ }
+ }
+ }
+ // write missing characters at the end of the string
+ if(pos < s.len)
+ {
+ csubstr sub = s.sub(pos);
+ this->Writer::_do_write(sub);
+ }
+ this->Writer::_do_write('"');
+}
+
+template<class Writer>
+void Emitter<Writer>::_write_scalar_plain(csubstr s, size_t ilevel)
+{
+ size_t pos = 0; // tracks the last character that was already written
+ for(size_t i = 0; i < s.len; ++i)
+ {
+ const char curr = s.str[i];
+ if(curr == '\n')
+ {
+ csubstr sub = s.range(pos, i+1);
+ this->Writer::_do_write(sub); // write everything up to (including) this newline
+ this->Writer::_do_write('\n'); // write the newline again
+ if(i + 1 < s.len)
+ _rymlindent_nextline() // indent the next line
+ pos = i+1;
+ }
+ }
+ // write missing characters at the end of the string
+ if(pos < s.len)
+ {
+ csubstr sub = s.sub(pos);
+ this->Writer::_do_write(sub);
+ }
+}
+
+#undef _rymlindent_nextline
+
+template<class Writer>
+void Emitter<Writer>::_write_scalar(csubstr s, bool was_quoted)
+{
+ // this block of code needed to be moved to before the needs_quotes
+ // assignment to work around a g++ optimizer bug where (s.str != nullptr)
+ // was evaluated as true even if s.str was actually a nullptr (!!!)
+ if(s.len == size_t(0))
+ {
+ if(was_quoted)
+ this->Writer::_do_write("''");
+ return;
+ }
+
+ const bool needs_quotes = (
+ was_quoted
+ ||
+ (
+ ( ! s.is_number())
+ &&
+ (
+ // has leading whitespace
+ s.begins_with_any(" \n\t\r")
+ ||
+ // looks like reference or anchor or would be treated as a directive
+ s.begins_with_any("*&%")
+ ||
+ s.begins_with("<<")
+ ||
+ // has trailing whitespace
+ s.ends_with_any(" \n\t\r")
+ ||
+ // has special chars
+ (s.first_of("#:-?,\n{}[]'\"") != npos)
+ )
+ )
+ );
+
+ if( ! needs_quotes)
+ {
+ this->Writer::_do_write(s);
+ }
+ else
+ {
+ const bool has_dquotes = s.first_of( '"') != npos;
+ const bool has_squotes = s.first_of('\'') != npos;
+ if(!has_squotes && has_dquotes)
+ {
+ this->Writer::_do_write('\'');
+ this->Writer::_do_write(s);
+ this->Writer::_do_write('\'');
+ }
+ else if(has_squotes && !has_dquotes)
+ {
+ RYML_ASSERT(s.count('\n') == 0);
+ this->Writer::_do_write('"');
+ this->Writer::_do_write(s);
+ this->Writer::_do_write('"');
+ }
+ else
+ {
+ _write_scalar_squo(s, /*FIXME FIXME FIXME*/0);
+ }
+ }
+}
+template<class Writer>
+void Emitter<Writer>::_write_scalar_json(csubstr s, bool as_key, bool was_quoted)
+{
+ if(was_quoted)
+ {
+ this->Writer::_do_write('"');
+ this->Writer::_do_write(s);
+ this->Writer::_do_write('"');
+ }
+ // json only allows strings as keys
+ else if(!as_key && (s.is_number() || s == "true" || s == "null" || s == "false"))
+ {
+ this->Writer::_do_write(s);
+ }
+ else
+ {
+ size_t pos = 0;
+ this->Writer::_do_write('"');
+ for(size_t i = 0; i < s.len; ++i)
+ {
+ switch (s[i])
+ {
+ case '"':
+ case '\n': {
+ if(i > 0)
+ {
+ csubstr sub = s.range(pos, i);
+ this->Writer::_do_write(sub);
+ }
+ pos = i + 1;
+ switch (s[i]) {
+ case '"':
+ this->Writer::_do_write("\\\"");
+ break;
+ case '\n':
+ this->Writer::_do_write("\\n");
+ break;
+ }
+ break;
+ }
+ }
+ }
+ if(pos < s.len)
+ {
+ csubstr sub = s.sub(pos);
+ this->Writer::_do_write(sub);
+ }
+ this->Writer::_do_write('"');
+ }
+}
+
+} // namespace yml
+} // namespace c4
+
+#endif /* _C4_YML_EMIT_DEF_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/emit.def.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/detail/stack.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/stack.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_DETAIL_STACK_HPP_
+#define _C4_YML_DETAIL_STACK_HPP_
+
+#ifndef _C4_YML_COMMON_HPP_
+//included above:
+//#include "../common.hpp"
+#endif
+
+#ifdef RYML_DBG
+//included above:
+//# include <type_traits>
+#endif
+
+//included above:
+//#include <string.h>
+
+namespace c4 {
+namespace yml {
+namespace detail {
+
+/** A lightweight contiguous stack with SSO. This avoids a dependency on std. */
+template<class T, size_t N=16>
+class stack
+{
+ static_assert(std::is_trivially_copyable<T>::value, "T must be trivially copyable");
+ static_assert(std::is_trivially_destructible<T>::value, "T must be trivially destructible");
+
+ enum : size_t { sso_size = N };
+
+public:
+
+ T m_buf[N];
+ T * m_stack;
+ size_t m_size;
+ size_t m_capacity;
+ Callbacks m_callbacks;
+
+public:
+
+ constexpr static bool is_contiguous() { return true; }
+
+ stack(Callbacks const& cb)
+ : m_buf()
+ , m_stack(m_buf)
+ , m_size(0)
+ , m_capacity(N)
+ , m_callbacks(cb) {}
+ stack() : stack(get_callbacks()) {}
+ ~stack()
+ {
+ _free();
+ }
+
+ stack(stack const& that) noexcept : stack(that.m_callbacks)
+ {
+ resize(that.m_size);
+ _cp(&that);
+ }
+
+ stack(stack &&that) noexcept : stack(that.m_callbacks)
+ {
+ _mv(&that);
+ }
+
+ stack& operator= (stack const& that) noexcept
+ {
+ _cb(that.m_callbacks);
+ resize(that.m_size);
+ _cp(&that);
+ return *this;
+ }
+
+ stack& operator= (stack &&that) noexcept
+ {
+ _cb(that.m_callbacks);
+ _mv(&that);
+ return *this;
+ }
+
+public:
+
+ size_t size() const { return m_size; }
+ size_t empty() const { return m_size == 0; }
+ size_t capacity() const { return m_capacity; }
+
+ void clear()
+ {
+ m_size = 0;
+ }
+
+ void resize(size_t sz)
+ {
+ reserve(sz);
+ m_size = sz;
+ }
+
+ void reserve(size_t sz);
+
+ void push(T const& C4_RESTRICT n)
+ {
+ RYML_ASSERT((const char*)&n + sizeof(T) < (const char*)m_stack || &n > m_stack + m_capacity);
+ if(m_size == m_capacity)
+ {
+ size_t cap = m_capacity == 0 ? N : 2 * m_capacity;
+ reserve(cap);
+ }
+ m_stack[m_size] = n;
+ ++m_size;
+ }
+
+ void push_top()
+ {
+ RYML_ASSERT(m_size > 0);
+ if(m_size == m_capacity)
+ {
+ size_t cap = m_capacity == 0 ? N : 2 * m_capacity;
+ reserve(cap);
+ }
+ m_stack[m_size] = m_stack[m_size - 1];
+ ++m_size;
+ }
+
+ T const& C4_RESTRICT pop()
+ {
+ RYML_ASSERT(m_size > 0);
+ --m_size;
+ return m_stack[m_size];
+ }
+
+ C4_ALWAYS_INLINE T const& C4_RESTRICT top() const { RYML_ASSERT(m_size > 0); return m_stack[m_size - 1]; }
+ C4_ALWAYS_INLINE T & C4_RESTRICT top() { RYML_ASSERT(m_size > 0); return m_stack[m_size - 1]; }
+
+ C4_ALWAYS_INLINE T const& C4_RESTRICT bottom() const { RYML_ASSERT(m_size > 0); return m_stack[0]; }
+ C4_ALWAYS_INLINE T & C4_RESTRICT bottom() { RYML_ASSERT(m_size > 0); return m_stack[0]; }
+
+ C4_ALWAYS_INLINE T const& C4_RESTRICT top(size_t i) const { RYML_ASSERT(i < m_size); return m_stack[m_size - 1 - i]; }
+ C4_ALWAYS_INLINE T & C4_RESTRICT top(size_t i) { RYML_ASSERT(i < m_size); return m_stack[m_size - 1 - i]; }
+
+ C4_ALWAYS_INLINE T const& C4_RESTRICT bottom(size_t i) const { RYML_ASSERT(i < m_size); return m_stack[i]; }
+ C4_ALWAYS_INLINE T & C4_RESTRICT bottom(size_t i) { RYML_ASSERT(i < m_size); return m_stack[i]; }
+
+ C4_ALWAYS_INLINE T const& C4_RESTRICT operator[](size_t i) const { RYML_ASSERT(i < m_size); return m_stack[i]; }
+ C4_ALWAYS_INLINE T & C4_RESTRICT operator[](size_t i) { RYML_ASSERT(i < m_size); return m_stack[i]; }
+
+public:
+
+ using iterator = T *;
+ using const_iterator = T const *;
+
+ iterator begin() { return m_stack; }
+ iterator end () { return m_stack + m_size; }
+
+ const_iterator begin() const { return (const_iterator)m_stack; }
+ const_iterator end () const { return (const_iterator)m_stack + m_size; }
+
+public:
+ void _free();
+ void _cp(stack const* C4_RESTRICT that);
+ void _mv(stack * that);
+ void _cb(Callbacks const& cb);
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+template<class T, size_t N>
+void stack<T, N>::reserve(size_t sz)
+{
+ if(sz <= m_size)
+ return;
+ if(sz <= N)
+ {
+ m_stack = m_buf;
+ m_capacity = N;
+ return;
+ }
+ T *buf = (T*) m_callbacks.m_allocate(sz * sizeof(T), m_stack, m_callbacks.m_user_data);
+ memcpy(buf, m_stack, m_size * sizeof(T));
+ if(m_stack != m_buf)
+ {
+ m_callbacks.m_free(m_stack, m_capacity * sizeof(T), m_callbacks.m_user_data);
+ }
+ m_stack = buf;
+ m_capacity = sz;
+}
+
+
+//-----------------------------------------------------------------------------
+
+template<class T, size_t N>
+void stack<T, N>::_free()
+{
+ RYML_ASSERT(m_stack != nullptr); // this structure cannot be memset() to zero
+ if(m_stack != m_buf)
+ {
+ m_callbacks.m_free(m_stack, m_capacity * sizeof(T), m_callbacks.m_user_data);
+ m_stack = m_buf;
+ m_size = N;
+ m_capacity = N;
+ }
+ else
+ {
+ RYML_ASSERT(m_capacity == N);
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+
+template<class T, size_t N>
+void stack<T, N>::_cp(stack const* C4_RESTRICT that)
+{
+ if(that->m_stack != that->m_buf)
+ {
+ RYML_ASSERT(that->m_capacity > N);
+ RYML_ASSERT(that->m_size <= that->m_capacity);
+ }
+ else
+ {
+ RYML_ASSERT(that->m_capacity <= N);
+ RYML_ASSERT(that->m_size <= that->m_capacity);
+ }
+ memcpy(m_stack, that->m_stack, that->m_size * sizeof(T));
+ m_size = that->m_size;
+ m_capacity = that->m_size < N ? N : that->m_size;
+ m_callbacks = that->m_callbacks;
+}
+
+
+//-----------------------------------------------------------------------------
+
+template<class T, size_t N>
+void stack<T, N>::_mv(stack * that)
+{
+ if(that->m_stack != that->m_buf)
+ {
+ RYML_ASSERT(that->m_capacity > N);
+ RYML_ASSERT(that->m_size <= that->m_capacity);
+ m_stack = that->m_stack;
+ }
+ else
+ {
+ RYML_ASSERT(that->m_capacity <= N);
+ RYML_ASSERT(that->m_size <= that->m_capacity);
+ memcpy(m_buf, that->m_buf, that->m_size * sizeof(T));
+ m_stack = m_buf;
+ }
+ m_size = that->m_size;
+ m_capacity = that->m_capacity;
+ m_callbacks = that->m_callbacks;
+ // make sure no deallocation happens on destruction
+ RYML_ASSERT(that->m_stack != m_buf);
+ that->m_stack = that->m_buf;
+ that->m_capacity = N;
+ that->m_size = 0;
+}
+
+
+//-----------------------------------------------------------------------------
+
+template<class T, size_t N>
+void stack<T, N>::_cb(Callbacks const& cb)
+{
+ if(cb != m_callbacks)
+ {
+ _free();
+ m_callbacks = cb;
+ }
+}
+
+} // namespace detail
+} // namespace yml
+} // namespace c4
+
+#endif /* _C4_YML_DETAIL_STACK_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/detail/stack.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/parse.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/parse.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_PARSE_HPP_
+#define _C4_YML_PARSE_HPP_
+
+#ifndef _C4_YML_TREE_HPP_
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/tree.hpp
+//#include "c4/yml/tree.hpp"
+#if !defined(C4_YML_TREE_HPP_) && !defined(_C4_YML_TREE_HPP_)
+#error "amalgamate: file c4/yml/tree.hpp must have been included at this point"
+#endif /* C4_YML_TREE_HPP_ */
+
+#endif
+
+#ifndef _C4_YML_NODE_HPP_
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/node.hpp
+//#include "c4/yml/node.hpp"
+#if !defined(C4_YML_NODE_HPP_) && !defined(_C4_YML_NODE_HPP_)
+#error "amalgamate: file c4/yml/node.hpp must have been included at this point"
+#endif /* C4_YML_NODE_HPP_ */
+
+#endif
+
+#ifndef _C4_YML_DETAIL_STACK_HPP_
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/stack.hpp
+//#include "c4/yml/detail/stack.hpp"
+#if !defined(C4_YML_DETAIL_STACK_HPP_) && !defined(_C4_YML_DETAIL_STACK_HPP_)
+#error "amalgamate: file c4/yml/detail/stack.hpp must have been included at this point"
+#endif /* C4_YML_DETAIL_STACK_HPP_ */
+
+#endif
+
+//included above:
+//#include <stdarg.h>
+
+#if defined(_MSC_VER)
+# pragma warning(push)
+# pragma warning(disable: 4251/*needs to have dll-interface to be used by clients of struct*/)
+#endif
+
+namespace c4 {
+namespace yml {
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+class RYML_EXPORT Parser
+{
+public:
+
+ /** @name construction and assignment */
+ /** @{ */
+
+ Parser() : Parser(get_callbacks()) {}
+ Parser(Callbacks const& cb);
+ ~Parser();
+
+ Parser(Parser &&);
+ Parser(Parser const&);
+ Parser& operator=(Parser &&);
+ Parser& operator=(Parser const&);
+
+ /** @} */
+
+public:
+
+ /** @name modifiers */
+ /** @{ */
+
+ /** Reserve a certain capacity for the parsing stack.
+ * This should be larger than the expected depth of the parsed
+ * YAML tree.
+ *
+ * The parsing stack is the only (potential) heap memory used by
+ * the parser.
+ *
+ * If the requested capacity is below the default
+ * stack size of 16, the memory is used directly in the parser
+ * object; otherwise it will be allocated from the heap.
+ *
+ * @note this reserves memory only for the parser itself; all the
+ * allocations for the parsed tree will go through the tree's
+ * allocator.
+ *
+ * @note the tree and the arena can (and should) also be reserved. */
+ void reserve_stack(size_t capacity)
+ {
+ m_stack.reserve(capacity);
+ }
+
+ /** Reserve a certain capacity for the array used to track node
+ * locations in the source buffer. */
+ void reserve_locations(size_t num_source_lines)
+ {
+ _resize_locations(num_source_lines);
+ }
+
+ /** Reserve a certain capacity for the character arena used to
+ * filter scalars. */
+ void reserve_filter_arena(size_t num_characters)
+ {
+ _resize_filter_arena(num_characters);
+ }
+
+ /** @} */
+
+public:
+
+ /** @name getters and modifiers */
+ /** @{ */
+
+ /** Get the current callbacks in the parser. */
+ Callbacks callbacks() const { return m_stack.m_callbacks; }
+
+ /** Get the name of the latest file parsed by this object. */
+ csubstr filename() const { return m_file; }
+
+ /** Get the latest YAML buffer parsed by this object. */
+ csubstr source() const { return m_buf; }
+
+ size_t stack_capacity() const { return m_stack.capacity(); }
+ size_t locations_capacity() const { return m_newline_offsets_capacity; }
+ size_t filter_arena_capacity() const { return m_filter_arena.len; }
+
+ /** @} */
+
+public:
+
+ /** @name parse_in_place */
+ /** @{ */
+
+ /** Create a new tree and parse into its root.
+ * The tree is created with the callbacks currently in the parser. */
+ Tree parse_in_place(csubstr filename, substr src)
+ {
+ Tree t(callbacks());
+ t.reserve(_estimate_capacity(src));
+ this->parse_in_place(filename, src, &t, t.root_id());
+ return t;
+ }
+
+ /** Parse into an existing tree, starting at its root node.
+ * The callbacks in the tree are kept, and used to allocate
+ * the tree members, if any allocation is required. */
+ void parse_in_place(csubstr filename, substr src, Tree *t)
+ {
+ this->parse_in_place(filename, src, t, t->root_id());
+ }
+
+ /** Parse into an existing node.
+ * The callbacks in the tree are kept, and used to allocate
+ * the tree members, if any allocation is required. */
+ void parse_in_place(csubstr filename, substr src, Tree *t, size_t node_id);
+ // ^^^^^^^^^^^^^ this is the workhorse overload; everything else is syntactic candy
+
+ /** Parse into an existing node.
+ * The callbacks in the tree are kept, and used to allocate
+ * the tree members, if any allocation is required. */
+ void parse_in_place(csubstr filename, substr src, NodeRef node)
+ {
+ this->parse_in_place(filename, src, node.tree(), node.id());
+ }
+
+ RYML_DEPRECATED("use parse_in_place() instead") Tree parse(csubstr filename, substr src) { return parse_in_place(filename, src); }
+ RYML_DEPRECATED("use parse_in_place() instead") void parse(csubstr filename, substr src, Tree *t) { parse_in_place(filename, src, t); }
+ RYML_DEPRECATED("use parse_in_place() instead") void parse(csubstr filename, substr src, Tree *t, size_t node_id) { parse_in_place(filename, src, t, node_id); }
+ RYML_DEPRECATED("use parse_in_place() instead") void parse(csubstr filename, substr src, NodeRef node) { parse_in_place(filename, src, node); }
+
+ /** @} */
+
+public:
+
+ /** @name parse_in_arena: copy the YAML source buffer to the
+ * tree's arena, then parse the copy in situ
+ *
+ * @note overloads receiving a substr YAML buffer are intentionally
+ * left undefined, such that calling parse_in_arena() with a substr
+ * will cause a linker error. This is to prevent an accidental
+ * copy of the source buffer to the tree's arena, because substr
+ * is implicitly convertible to csubstr. If you really intend to parse
+ * a mutable buffer in the tree's arena, convert it first to immutable
+ * by assigning the substr to a csubstr prior to calling parse_in_arena().
+ * This is not needed for parse_in_place() because csubstr is not
+ * implicitly convertible to substr. */
+ /** @{ */
+
+ // READ THE NOTE ABOVE!
+ #define RYML_DONT_PARSE_SUBSTR_IN_ARENA "Do not pass a (mutable) substr to parse_in_arena(); if you have a substr, it should be parsed in place. Consider using parse_in_place() instead, or convert the buffer to csubstr prior to calling. This function is deliberately left undefined and will cause a compiler error."
+ RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_in_arena(csubstr filename, substr csrc);
+ RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(csubstr filename, substr csrc, Tree *t);
+ RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(csubstr filename, substr csrc, Tree *t, size_t node_id);
+ RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(csubstr filename, substr csrc, NodeRef node);
+
+ /** Create a new tree and parse into its root.
+ * The immutable YAML source is first copied to the tree's arena,
+ * and parsed from there.
+ * The callbacks in the tree are kept, and used to allocate
+ * the tree members, if any allocation is required. */
+ Tree parse_in_arena(csubstr filename, csubstr csrc)
+ {
+ Tree t(callbacks());
+ substr src = t.copy_to_arena(csrc);
+ t.reserve(_estimate_capacity(csrc));
+ this->parse_in_place(filename, src, &t, t.root_id());
+ return t;
+ }
+
+ /** Parse into an existing tree, starting at its root node.
+ * The immutable YAML source is first copied to the tree's arena,
+ * and parsed from there.
+ * The callbacks in the tree are kept, and used to allocate
+ * the tree members, if any allocation is required. */
+ void parse_in_arena(csubstr filename, csubstr csrc, Tree *t)
+ {
+ substr src = t->copy_to_arena(csrc);
+ this->parse_in_place(filename, src, t, t->root_id());
+ }
+
+ /** Parse into a specific node in an existing tree.
+ * The immutable YAML source is first copied to the tree's arena,
+ * and parsed from there.
+ * The callbacks in the tree are kept, and used to allocate
+ * the tree members, if any allocation is required. */
+ void parse_in_arena(csubstr filename, csubstr csrc, Tree *t, size_t node_id)
+ {
+ substr src = t->copy_to_arena(csrc);
+ this->parse_in_place(filename, src, t, node_id);
+ }
+
+ /** Parse into a specific node in an existing tree.
+ * The immutable YAML source is first copied to the tree's arena,
+ * and parsed from there.
+ * The callbacks in the tree are kept, and used to allocate
+ * the tree members, if any allocation is required. */
+ void parse_in_arena(csubstr filename, csubstr csrc, NodeRef node)
+ {
+ substr src = node.tree()->copy_to_arena(csrc);
+ this->parse_in_place(filename, src, node.tree(), node.id());
+ }
+
+ RYML_DEPRECATED("use parse_in_arena() instead") Tree parse(csubstr filename, csubstr csrc) { return parse_in_arena(filename, csrc); }
+ RYML_DEPRECATED("use parse_in_arena() instead") void parse(csubstr filename, csubstr csrc, Tree *t) { parse_in_arena(filename, csrc, t); }
+ RYML_DEPRECATED("use parse_in_arena() instead") void parse(csubstr filename, csubstr csrc, Tree *t, size_t node_id) { parse_in_arena(filename, csrc, t, node_id); }
+ RYML_DEPRECATED("use parse_in_arena() instead") void parse(csubstr filename, csubstr csrc, NodeRef node) { parse_in_arena(filename, csrc, node); }
+
+ /** @} */
+
+public:
+
+ /** @name locations */
+ /** @{ */
+
+ /** Get the location of a node of the last tree to be parsed by this parser. */
+ Location location(Tree const& tree, size_t node_id) const;
+ /** Get the location of a node of the last tree to be parsed by this parser. */
+ Location location(NodeRef node) const;
+ /** Get the string starting at a particular location, to the end
+ * of the parsed source buffer. */
+ csubstr location_contents(Location const& loc) const;
+ /** Given a pointer to a buffer position, get the location. @p val
+ * must be pointing to somewhere in the source buffer that was
+ * last parsed by this object. */
+ Location val_location(const char *val) const;
+
+ /** @} */
+
+private:
+
+ typedef enum {
+ BLOCK_LITERAL, //!< keep newlines (|)
+ BLOCK_FOLD //!< replace newline with single space (>)
+ } BlockStyle_e;
+
+ typedef enum {
+ CHOMP_CLIP, //!< single newline at end (default)
+ CHOMP_STRIP, //!< no newline at end (-)
+ CHOMP_KEEP //!< all newlines from end (+)
+ } BlockChomp_e;
+
+private:
+
+ using flag_t = int;
+
+ static size_t _estimate_capacity(csubstr src) { size_t c = _count_nlines(src); c = c >= 16 ? c : 16; return c; }
+
+ void _reset();
+
+ bool _finished_file() const;
+ bool _finished_line() const;
+
+ csubstr _peek_next_line(size_t pos=npos) const;
+ bool _advance_to_peeked();
+ void _scan_line();
+
+ csubstr _slurp_doc_scalar();
+
+ /**
+ * @param [out] quoted
+ * Will only be written to if this method returns true.
+ * Will be set to true if the scanned scalar was quoted, by '', "", > or |.
+ */
+ bool _scan_scalar(csubstr *C4_RESTRICT scalar, bool *C4_RESTRICT quoted);
+
+ csubstr _scan_comment();
+ csubstr _scan_squot_scalar();
+ csubstr _scan_dquot_scalar();
+ csubstr _scan_block();
+ substr _scan_plain_scalar_blck(csubstr currscalar, csubstr peeked_line, size_t indentation);
+ substr _scan_plain_scalar_flow(csubstr currscalar, csubstr peeked_line);
+ substr _scan_complex_key(csubstr currscalar, csubstr peeked_line);
+ csubstr _scan_to_next_nonempty_line(size_t indentation);
+ csubstr _extend_scanned_scalar(csubstr currscalar);
+
+ csubstr _filter_squot_scalar(const substr s);
+ csubstr _filter_dquot_scalar(substr s);
+ csubstr _filter_plain_scalar(substr s, size_t indentation);
+ csubstr _filter_block_scalar(substr s, BlockStyle_e style, BlockChomp_e chomp, size_t indentation);
+ template<bool backslash_is_escape, bool keep_trailing_whitespace>
+ bool _filter_nl(substr scalar, size_t *C4_RESTRICT pos, size_t *C4_RESTRICT filter_arena_pos, size_t indentation);
+ template<bool keep_trailing_whitespace>
+ void _filter_ws(substr scalar, size_t *C4_RESTRICT pos, size_t *C4_RESTRICT filter_arena_pos);
+ bool _apply_chomp(substr buf, size_t *C4_RESTRICT pos, BlockChomp_e chomp);
+
+ void _handle_finished_file();
+ void _handle_line();
+
+ bool _handle_indentation();
+
+ bool _handle_unk();
+ bool _handle_map_flow();
+ bool _handle_map_blck();
+ bool _handle_seq_flow();
+ bool _handle_seq_blck();
+ bool _handle_top();
+ bool _handle_types();
+ bool _handle_key_anchors_and_refs();
+ bool _handle_val_anchors_and_refs();
+ void _move_val_tag_to_key_tag();
+ void _move_key_tag_to_val_tag();
+ void _move_key_tag2_to_key_tag();
+ void _move_val_anchor_to_key_anchor();
+ void _move_key_anchor_to_val_anchor();
+
+ void _push_level(bool explicit_flow_chars = false);
+ void _pop_level();
+
+ void _start_unk(bool as_child=true);
+
+ void _start_map(bool as_child=true);
+ void _start_map_unk(bool as_child);
+ void _stop_map();
+
+ void _start_seq(bool as_child=true);
+ void _stop_seq();
+
+ void _start_seqimap();
+ void _stop_seqimap();
+
+ void _start_doc(bool as_child=true);
+ void _stop_doc();
+ void _start_new_doc(csubstr rem);
+ void _end_stream();
+
+ NodeData* _append_val(csubstr val, flag_t quoted=false);
+ NodeData* _append_key_val(csubstr val, flag_t val_quoted=false);
+ bool _rval_dash_start_or_continue_seq();
+
+ void _store_scalar(csubstr s, flag_t is_quoted);
+ csubstr _consume_scalar();
+ void _move_scalar_from_top();
+
+ inline NodeData* _append_val_null(const char *str) { _RYML_CB_ASSERT(m_stack.m_callbacks, str >= m_buf.begin() && str <= m_buf.end()); return _append_val({str, size_t(0)}); }
+ inline NodeData* _append_key_val_null(const char *str) { _RYML_CB_ASSERT(m_stack.m_callbacks, str >= m_buf.begin() && str <= m_buf.end()); return _append_key_val({str, size_t(0)}); }
+ inline void _store_scalar_null(const char *str) { _RYML_CB_ASSERT(m_stack.m_callbacks, str >= m_buf.begin() && str <= m_buf.end()); _store_scalar({str, size_t(0)}, false); }
+
+ void _set_indentation(size_t behind);
+ void _save_indentation(size_t behind=0);
+ bool _maybe_set_indentation_from_anchor_or_tag();
+
+ void _write_key_anchor(size_t node_id);
+ void _write_val_anchor(size_t node_id);
+
+ void _handle_directive(csubstr directive);
+
+ void _skipchars(char c);
+ template<size_t N>
+ void _skipchars(const char (&chars)[N]);
+
+private:
+
+ static size_t _count_nlines(csubstr src);
+
+private:
+
+ typedef enum : flag_t {
+ RTOP = 0x01 << 0, ///< reading at top level
+ RUNK = 0x01 << 1, ///< reading an unknown: must determine whether scalar, map or seq
+ RMAP = 0x01 << 2, ///< reading a map
+ RSEQ = 0x01 << 3, ///< reading a seq
+ FLOW = 0x01 << 4, ///< reading is inside explicit flow chars: [] or {}
+ QMRK = 0x01 << 5, ///< reading an explicit key (`? key`)
+ RKEY = 0x01 << 6, ///< reading a scalar as key
+ RVAL = 0x01 << 7, ///< reading a scalar as val
+ RNXT = 0x01 << 8, ///< read next val or keyval
+ SSCL = 0x01 << 9, ///< there's a stored scalar
+ QSCL = 0x01 << 10, ///< stored scalar was quoted
+ RSET = 0x01 << 11, ///< the (implicit) map being read is a !!set. @see https://yaml.org/type/set.html
+ NDOC = 0x01 << 12, ///< no document mode. a document has ended and another has not started yet.
+ //! reading an implicit map nested in an explicit seq.
+ //! eg, {key: [key2: value2, key3: value3]}
+ //! is parsed as {key: [{key2: value2}, {key3: value3}]}
+ RSEQIMAP = 0x01 << 13,
+ } State_e;
+
+ struct LineContents
+ {
+ csubstr full; ///< the full line, including newlines on the right
+ csubstr stripped; ///< the stripped line, excluding newlines on the right
+ csubstr rem; ///< the stripped line remainder; initially starts at the first non-space character
+ size_t indentation; ///< the number of spaces on the beginning of the line
+
+ LineContents() : full(), stripped(), rem(), indentation() {}
+
+ void reset_with_next_line(csubstr buf, size_t pos);
+
+ void reset(csubstr full_, csubstr stripped_)
+ {
+ full = full_;
+ stripped = stripped_;
+ rem = stripped_;
+ // find the first column where the character is not a space
+ indentation = full.first_not_of(' ');
+ }
+
+ size_t current_col() const
+ {
+ return current_col(rem);
+ }
+
+ size_t current_col(csubstr s) const
+ {
+ RYML_ASSERT(s.str >= full.str);
+ RYML_ASSERT(full.is_super(s));
+ size_t col = static_cast<size_t>(s.str - full.str);
+ return col;
+ }
+ };
+
+ struct State
+ {
+ flag_t flags;
+ size_t level;
+ size_t node_id; // don't hold a pointer to the node as it will be relocated during tree resizes
+ csubstr scalar;
+ size_t scalar_col; // the column where the scalar (or its quotes) begin
+
+ Location pos;
+ LineContents line_contents;
+ size_t indref;
+
+ State() : flags(), level(), node_id(), scalar(), scalar_col(), pos(), line_contents(), indref() {}
+
+ void reset(const char *file, size_t node_id_)
+ {
+ flags = RUNK|RTOP;
+ level = 0;
+ pos.name = to_csubstr(file);
+ pos.offset = 0;
+ pos.line = 1;
+ pos.col = 1;
+ node_id = node_id_;
+ scalar_col = 0;
+ scalar.clear();
+ indref = 0;
+ }
+ };
+
+ void _line_progressed(size_t ahead);
+ void _line_ended();
+ void _line_ended_undo();
+
+ void _prepare_pop()
+ {
+ RYML_ASSERT(m_stack.size() > 1);
+ State const& curr = m_stack.top();
+ State & next = m_stack.top(1);
+ next.pos = curr.pos;
+ next.line_contents = curr.line_contents;
+ next.scalar = curr.scalar;
+ }
+
+ inline bool _at_line_begin() const
+ {
+ return m_state->line_contents.rem.begin() == m_state->line_contents.full.begin();
+ }
+ inline bool _at_line_end() const
+ {
+ csubstr r = m_state->line_contents.rem;
+ return r.empty() || r.begins_with(' ', r.len);
+ }
+ inline bool _token_is_from_this_line(csubstr token) const
+ {
+ return token.is_sub(m_state->line_contents.full);
+ }
+
+ inline NodeData * node(State const* s) const { return m_tree->get(s->node_id); }
+ inline NodeData * node(State const& s) const { return m_tree->get(s .node_id); }
+ inline NodeData * node(size_t node_id) const { return m_tree->get( node_id); }
+
+ inline bool has_all(flag_t f) const { return (m_state->flags & f) == f; }
+ inline bool has_any(flag_t f) const { return (m_state->flags & f) != 0; }
+ inline bool has_none(flag_t f) const { return (m_state->flags & f) == 0; }
+
+ static inline bool has_all(flag_t f, State const* s) { return (s->flags & f) == f; }
+ static inline bool has_any(flag_t f, State const* s) { return (s->flags & f) != 0; }
+ static inline bool has_none(flag_t f, State const* s) { return (s->flags & f) == 0; }
+
+ inline void set_flags(flag_t f) { set_flags(f, m_state); }
+ inline void add_flags(flag_t on) { add_flags(on, m_state); }
+ inline void addrem_flags(flag_t on, flag_t off) { addrem_flags(on, off, m_state); }
+ inline void rem_flags(flag_t off) { rem_flags(off, m_state); }
+
+ void set_flags(flag_t f, State * s);
+ void add_flags(flag_t on, State * s);
+ void addrem_flags(flag_t on, flag_t off, State * s);
+ void rem_flags(flag_t off, State * s);
+
+ void _resize_filter_arena(size_t num_characters);
+ void _grow_filter_arena(size_t num_characters);
+ substr _finish_filter_arena(substr dst, size_t pos);
+
+ void _prepare_locations() const; // only changes mutable members
+ void _resize_locations(size_t sz) const; // only changes mutable members
+ void _mark_locations_dirty();
+ bool _locations_dirty() const;
+
+private:
+
+ void _free();
+ void _clr();
+ void _cp(Parser const* that);
+ void _mv(Parser *that);
+
+#ifdef RYML_DBG
+ template<class ...Args> void _dbg(csubstr fmt, Args const& C4_RESTRICT ...args) const;
+#endif
+ template<class ...Args> void _err(csubstr fmt, Args const& C4_RESTRICT ...args) const;
+ template<class DumpFn> void _fmt_msg(DumpFn &&dumpfn) const;
+ static csubstr _prfl(substr buf, flag_t v);
+
+private:
+
+ csubstr m_file;
+ substr m_buf;
+
+ size_t m_root_id;
+ Tree * m_tree;
+
+ detail::stack<State> m_stack;
+ State * m_state;
+
+ size_t m_key_tag_indentation;
+ size_t m_key_tag2_indentation;
+ csubstr m_key_tag;
+ csubstr m_key_tag2;
+ size_t m_val_tag_indentation;
+ csubstr m_val_tag;
+
+ bool m_key_anchor_was_before;
+ size_t m_key_anchor_indentation;
+ csubstr m_key_anchor;
+ size_t m_val_anchor_indentation;
+ csubstr m_val_anchor;
+
+ substr m_filter_arena;
+
+ mutable size_t *m_newline_offsets;
+ mutable size_t m_newline_offsets_size;
+ mutable size_t m_newline_offsets_capacity;
+ mutable csubstr m_newline_offsets_buf;
+};
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+/** @name parse_in_place
+ *
+ * @desc parse a mutable YAML source buffer.
+ *
+ * @note These freestanding functions use a temporary parser object,
+ * and are convenience functions to easily parse YAML without the need
+ * to instantiate a separate parser. Note that some properties
+ * (notably node locations in the original source code) are only
+ * available through the parser object after it has parsed the
+ * code. If you need access to any of these properties, use
+ * Parser::parse_in_place() */
+/** @{ */
+
+inline Tree parse_in_place( substr yaml ) { Parser np; return np.parse_in_place({} , yaml); } //!< parse in-situ a modifiable YAML source buffer.
+inline Tree parse_in_place(csubstr filename, substr yaml ) { Parser np; return np.parse_in_place(filename, yaml); } //!< parse in-situ a modifiable YAML source buffer, providing a filename for error messages.
+inline void parse_in_place( substr yaml, Tree *t ) { Parser np; np.parse_in_place({} , yaml, t); } //!< reusing the YAML tree, parse in-situ a modifiable YAML source buffer
+inline void parse_in_place(csubstr filename, substr yaml, Tree *t ) { Parser np; np.parse_in_place(filename, yaml, t); } //!< reusing the YAML tree, parse in-situ a modifiable YAML source buffer, providing a filename for error messages.
+inline void parse_in_place( substr yaml, Tree *t, size_t node_id) { Parser np; np.parse_in_place({} , yaml, t, node_id); } //!< reusing the YAML tree, parse in-situ a modifiable YAML source buffer
+inline void parse_in_place(csubstr filename, substr yaml, Tree *t, size_t node_id) { Parser np; np.parse_in_place(filename, yaml, t, node_id); } //!< reusing the YAML tree, parse in-situ a modifiable YAML source buffer, providing a filename for error messages.
+inline void parse_in_place( substr yaml, NodeRef node ) { Parser np; np.parse_in_place({} , yaml, node); } //!< reusing the YAML tree, parse in-situ a modifiable YAML source buffer
+inline void parse_in_place(csubstr filename, substr yaml, NodeRef node ) { Parser np; np.parse_in_place(filename, yaml, node); } //!< reusing the YAML tree, parse in-situ a modifiable YAML source buffer, providing a filename for error messages.
+
+RYML_DEPRECATED("use parse_in_place() instead") inline Tree parse( substr yaml ) { Parser np; return np.parse_in_place({} , yaml); }
+RYML_DEPRECATED("use parse_in_place() instead") inline Tree parse(csubstr filename, substr yaml ) { Parser np; return np.parse_in_place(filename, yaml); }
+RYML_DEPRECATED("use parse_in_place() instead") inline void parse( substr yaml, Tree *t ) { Parser np; np.parse_in_place({} , yaml, t); }
+RYML_DEPRECATED("use parse_in_place() instead") inline void parse(csubstr filename, substr yaml, Tree *t ) { Parser np; np.parse_in_place(filename, yaml, t); }
+RYML_DEPRECATED("use parse_in_place() instead") inline void parse( substr yaml, Tree *t, size_t node_id) { Parser np; np.parse_in_place({} , yaml, t, node_id); }
+RYML_DEPRECATED("use parse_in_place() instead") inline void parse(csubstr filename, substr yaml, Tree *t, size_t node_id) { Parser np; np.parse_in_place(filename, yaml, t, node_id); }
+RYML_DEPRECATED("use parse_in_place() instead") inline void parse( substr yaml, NodeRef node ) { Parser np; np.parse_in_place({} , yaml, node); }
+RYML_DEPRECATED("use parse_in_place() instead") inline void parse(csubstr filename, substr yaml, NodeRef node ) { Parser np; np.parse_in_place(filename, yaml, node); }
+
+/** @} */
+
+
+//-----------------------------------------------------------------------------
+
+/** @name parse_in_arena
+ * @desc parse a read-only YAML source buffer, copying it first to the tree's arena.
+ *
+ * @note These freestanding functions use a temporary parser object,
+ * and are convenience functions to easily parse YAML without the need
+ * to instantiate a separate parser. Note that some properties
+ * (notably node locations in the original source code) are only
+ * available through the parser object after it has parsed the
+ * code. If you need access to any of these properties, use
+ * Parser::parse_in_arena().
+ *
+ * @note overloads receiving a substr YAML buffer are intentionally
+ * left undefined, such that calling parse_in_arena() with a substr
+ * will cause a linker error. This is to prevent an accidental
+ * copy of the source buffer to the tree's arena, because substr
+ * is implicitly convertible to csubstr. If you really intend to parse
+ * a mutable buffer in the tree's arena, convert it first to immutable
+ * by assigning the substr to a csubstr prior to calling parse_in_arena().
+ * This is not needed for parse_in_place() because csubstr is not
+ * implicitly convertible to substr. */
+/** @{ */
+
+/* READ THE NOTE ABOVE! */
+RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_in_arena( substr yaml );
+RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) Tree parse_in_arena(csubstr filename, substr yaml );
+RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena( substr yaml, Tree *t );
+RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(csubstr filename, substr yaml, Tree *t );
+RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena( substr yaml, Tree *t, size_t node_id);
+RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(csubstr filename, substr yaml, Tree *t, size_t node_id);
+RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena( substr yaml, NodeRef node );
+RYML_DEPRECATED(RYML_DONT_PARSE_SUBSTR_IN_ARENA) void parse_in_arena(csubstr filename, substr yaml, NodeRef node );
+
+inline Tree parse_in_arena( csubstr yaml ) { Parser np; return np.parse_in_arena({} , yaml); } //!< parse a read-only YAML source buffer, copying it first to the tree's source arena.
+inline Tree parse_in_arena(csubstr filename, csubstr yaml ) { Parser np; return np.parse_in_arena(filename, yaml); } //!< parse a read-only YAML source buffer, copying it first to the tree's source arena, providing a filename for error messages.
+inline void parse_in_arena( csubstr yaml, Tree *t ) { Parser np; np.parse_in_arena({} , yaml, t); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena.
+inline void parse_in_arena(csubstr filename, csubstr yaml, Tree *t ) { Parser np; np.parse_in_arena(filename, yaml, t); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena, providing a filename for error messages.
+inline void parse_in_arena( csubstr yaml, Tree *t, size_t node_id) { Parser np; np.parse_in_arena({} , yaml, t, node_id); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena.
+inline void parse_in_arena(csubstr filename, csubstr yaml, Tree *t, size_t node_id) { Parser np; np.parse_in_arena(filename, yaml, t, node_id); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena, providing a filename for error messages.
+inline void parse_in_arena( csubstr yaml, NodeRef node ) { Parser np; np.parse_in_arena({} , yaml, node); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena.
+inline void parse_in_arena(csubstr filename, csubstr yaml, NodeRef node ) { Parser np; np.parse_in_arena(filename, yaml, node); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena, providing a filename for error messages.
+
+RYML_DEPRECATED("use parse_in_arena() instead") inline Tree parse( csubstr yaml ) { Parser np; return np.parse_in_arena({} , yaml); } //!< parse a read-only YAML source buffer, copying it first to the tree's source arena.
+RYML_DEPRECATED("use parse_in_arena() instead") inline Tree parse(csubstr filename, csubstr yaml ) { Parser np; return np.parse_in_arena(filename, yaml); } //!< parse a read-only YAML source buffer, copying it first to the tree's source arena, providing a filename for error messages.
+RYML_DEPRECATED("use parse_in_arena() instead") inline void parse( csubstr yaml, Tree *t ) { Parser np; np.parse_in_arena({} , yaml, t); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena.
+RYML_DEPRECATED("use parse_in_arena() instead") inline void parse(csubstr filename, csubstr yaml, Tree *t ) { Parser np; np.parse_in_arena(filename, yaml, t); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena, providing a filename for error messages.
+RYML_DEPRECATED("use parse_in_arena() instead") inline void parse( csubstr yaml, Tree *t, size_t node_id) { Parser np; np.parse_in_arena({} , yaml, t, node_id); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena.
+RYML_DEPRECATED("use parse_in_arena() instead") inline void parse(csubstr filename, csubstr yaml, Tree *t, size_t node_id) { Parser np; np.parse_in_arena(filename, yaml, t, node_id); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena, providing a filename for error messages.
+RYML_DEPRECATED("use parse_in_arena() instead") inline void parse( csubstr yaml, NodeRef node ) { Parser np; np.parse_in_arena({} , yaml, node); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena.
+RYML_DEPRECATED("use parse_in_arena() instead") inline void parse(csubstr filename, csubstr yaml, NodeRef node ) { Parser np; np.parse_in_arena(filename, yaml, node); } //!< reusing the YAML tree, parse a read-only YAML source buffer, copying it first to the tree's source arena, providing a filename for error messages.
+
+/** @} */
+
+} // namespace yml
+} // namespace c4
+
+#if defined(_MSC_VER)
+# pragma warning(pop)
+#endif
+
+#endif /* _C4_YML_PARSE_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/parse.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/std/map.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/std/map.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_STD_MAP_HPP_
+#define _C4_YML_STD_MAP_HPP_
+
+/** @file map.hpp write/read std::map to/from a YAML tree. */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/node.hpp
+//#include "c4/yml/node.hpp"
+#if !defined(C4_YML_NODE_HPP_) && !defined(_C4_YML_NODE_HPP_)
+#error "amalgamate: file c4/yml/node.hpp must have been included at this point"
+#endif /* C4_YML_NODE_HPP_ */
+
+#include <map>
+
+namespace c4 {
+namespace yml {
+
+// std::map requires child nodes in the data
+// tree hierarchy (a MAP node in ryml parlance).
+// So it should be serialized via write()/read().
+
+template<class K, class V, class Less, class Alloc>
+void write(c4::yml::NodeRef *n, std::map<K, V, Less, Alloc> const& m)
+{
+ *n |= c4::yml::MAP;
+ for(auto const& p : m)
+ {
+ auto ch = n->append_child();
+ ch << c4::yml::key(p.first);
+ ch << p.second;
+ }
+}
+
+template<class K, class V, class Less, class Alloc>
+bool read(c4::yml::NodeRef const& n, std::map<K, V, Less, Alloc> * m)
+{
+ K k{};
+ V v;
+ for(auto const ch : n)
+ {
+ ch >> c4::yml::key(k);
+ ch >> v;
+ m->emplace(std::make_pair(std::move(k), std::move(v)));
+ }
+ return true;
+}
+
+} // namespace yml
+} // namespace c4
+
+#endif // _C4_YML_STD_MAP_HPP_
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/std/map.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/std/string.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/std/string.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef C4_YML_STD_STRING_HPP_
+#define C4_YML_STD_STRING_HPP_
+
+/** @file string.hpp substring conversions for/from std::string */
+
+// everything we need is implemented here:
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/std/string.hpp
+//#include <c4/std/string.hpp>
+#if !defined(C4_STD_STRING_HPP_) && !defined(_C4_STD_STRING_HPP_)
+#error "amalgamate: file c4/std/string.hpp must have been included at this point"
+#endif /* C4_STD_STRING_HPP_ */
+
+
+#endif // C4_YML_STD_STRING_HPP_
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/std/string.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/std/vector.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/std/vector.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_STD_VECTOR_HPP_
+#define _C4_YML_STD_VECTOR_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/node.hpp
+//#include "c4/yml/node.hpp"
+#if !defined(C4_YML_NODE_HPP_) && !defined(_C4_YML_NODE_HPP_)
+#error "amalgamate: file c4/yml/node.hpp must have been included at this point"
+#endif /* C4_YML_NODE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/std/vector.hpp
+//#include <c4/std/vector.hpp>
+#if !defined(C4_STD_VECTOR_HPP_) && !defined(_C4_STD_VECTOR_HPP_)
+#error "amalgamate: file c4/std/vector.hpp must have been included at this point"
+#endif /* C4_STD_VECTOR_HPP_ */
+
+//included above:
+//#include <vector>
+
+namespace c4 {
+namespace yml {
+
+// vector is a sequence-like type, and it requires child nodes
+// in the data tree hierarchy (a SEQ node in ryml parlance).
+// So it should be serialized via write()/read().
+
+template<class V, class Alloc>
+void write(c4::yml::NodeRef *n, std::vector<V, Alloc> const& vec)
+{
+ *n |= c4::yml::SEQ;
+ for(auto const& v : vec)
+ {
+ n->append_child() << v;
+ }
+}
+
+template<class V, class Alloc>
+bool read(c4::yml::NodeRef const& n, std::vector<V, Alloc> *vec)
+{
+ vec->resize(n.num_children());
+ size_t pos = 0;
+ for(auto const ch : n)
+ {
+ ch >> (*vec)[pos++];
+ }
+ return true;
+}
+
+} // namespace yml
+} // namespace c4
+
+#endif // _C4_YML_STD_VECTOR_HPP_
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/std/vector.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/std/std.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/std/std.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_STD_STD_HPP_
+#define _C4_YML_STD_STD_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/std/string.hpp
+//#include "c4/yml/std/string.hpp"
+#if !defined(C4_YML_STD_STRING_HPP_) && !defined(_C4_YML_STD_STRING_HPP_)
+#error "amalgamate: file c4/yml/std/string.hpp must have been included at this point"
+#endif /* C4_YML_STD_STRING_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/std/vector.hpp
+//#include "c4/yml/std/vector.hpp"
+#if !defined(C4_YML_STD_VECTOR_HPP_) && !defined(_C4_YML_STD_VECTOR_HPP_)
+#error "amalgamate: file c4/yml/std/vector.hpp must have been included at this point"
+#endif /* C4_YML_STD_VECTOR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/std/map.hpp
+//#include "c4/yml/std/map.hpp"
+#if !defined(C4_YML_STD_MAP_HPP_) && !defined(_C4_YML_STD_MAP_HPP_)
+#error "amalgamate: file c4/yml/std/map.hpp must have been included at this point"
+#endif /* C4_YML_STD_MAP_HPP_ */
+
+
+#endif // _C4_YML_STD_STD_HPP_
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/std/std.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/common.cpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/common.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef RYML_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/common.hpp
+//#include "c4/yml/common.hpp"
+#if !defined(C4_YML_COMMON_HPP_) && !defined(_C4_YML_COMMON_HPP_)
+#error "amalgamate: file c4/yml/common.hpp must have been included at this point"
+#endif /* C4_YML_COMMON_HPP_ */
+
+
+#ifndef RYML_NO_DEFAULT_CALLBACKS
+//included above:
+//# include <stdlib.h>
+//included above:
+//# include <stdio.h>
+#endif // RYML_NO_DEFAULT_CALLBACKS
+
+namespace c4 {
+namespace yml {
+
+namespace {
+thread_local Callbacks s_default_callbacks;
+} // anon namespace
+
+#ifndef RYML_NO_DEFAULT_CALLBACKS
+void report_error_impl(const char* msg, size_t length, Location loc, FILE *f)
+{
+ if(!f)
+ f = stderr;
+ if(loc)
+ {
+ if(!loc.name.empty())
+ {
+ fwrite(loc.name.str, 1, loc.name.len, f);
+ fputc(':', f);
+ }
+ fprintf(f, "%zu:", loc.line);
+ if(loc.col)
+ fprintf(f, "%zu:", loc.col);
+ if(loc.offset)
+ fprintf(f, " (%zuB):", loc.offset);
+ }
+ fprintf(f, "%.*s\n", (int)length, msg);
+ fflush(f);
+}
+
+void error_impl(const char* msg, size_t length, Location loc, void * /*user_data*/)
+{
+ report_error_impl(msg, length, loc, nullptr);
+ ::abort();
+}
+
+void* allocate_impl(size_t length, void * /*hint*/, void * /*user_data*/)
+{
+ void *mem = ::malloc(length);
+ if(mem == nullptr)
+ {
+ const char msg[] = "could not allocate memory";
+ error_impl(msg, sizeof(msg)-1, {}, nullptr);
+ }
+ return mem;
+}
+
+void free_impl(void *mem, size_t /*length*/, void * /*user_data*/)
+{
+ ::free(mem);
+}
+#endif // RYML_NO_DEFAULT_CALLBACKS
+
+
+
+Callbacks::Callbacks()
+ :
+ m_user_data(nullptr),
+ #ifndef RYML_NO_DEFAULT_CALLBACKS
+ m_allocate(allocate_impl),
+ m_free(free_impl),
+ m_error(error_impl)
+ #else
+ m_allocate(nullptr),
+ m_free(nullptr),
+ m_error(nullptr)
+ #endif
+{
+}
+
+Callbacks::Callbacks(void *user_data, pfn_allocate alloc_, pfn_free free_, pfn_error error_)
+ :
+ m_user_data(user_data),
+ #ifndef RYML_NO_DEFAULT_CALLBACKS
+ m_allocate(alloc_ ? alloc_ : allocate_impl),
+ m_free(free_ ? free_ : free_impl),
+ m_error(error_ ? error_ : error_impl)
+ #else
+ m_allocate(alloc_),
+ m_free(free_),
+ m_error(error_)
+ #endif
+{
+ C4_CHECK(m_allocate);
+ C4_CHECK(m_free);
+ C4_CHECK(m_error);
+}
+
+
+void set_callbacks(Callbacks const& c)
+{
+ s_default_callbacks = c;
+}
+
+Callbacks const& get_callbacks()
+{
+ return s_default_callbacks;
+}
+
+void reset_callbacks()
+{
+ set_callbacks(Callbacks());
+}
+
+void error(const char *msg, size_t msg_len, Location loc)
+{
+ s_default_callbacks.m_error(msg, msg_len, loc, s_default_callbacks.m_user_data);
+}
+
+} // namespace yml
+} // namespace c4
+
+#endif /* RYML_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/common.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/tree.cpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/tree.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef RYML_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/tree.hpp
+//#include "c4/yml/tree.hpp"
+#if !defined(C4_YML_TREE_HPP_) && !defined(_C4_YML_TREE_HPP_)
+#error "amalgamate: file c4/yml/tree.hpp must have been included at this point"
+#endif /* C4_YML_TREE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/parser_dbg.hpp
+//#include "c4/yml/detail/parser_dbg.hpp"
+#if !defined(C4_YML_DETAIL_PARSER_DBG_HPP_) && !defined(_C4_YML_DETAIL_PARSER_DBG_HPP_)
+#error "amalgamate: file c4/yml/detail/parser_dbg.hpp must have been included at this point"
+#endif /* C4_YML_DETAIL_PARSER_DBG_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/node.hpp
+//#include "c4/yml/node.hpp"
+#if !defined(C4_YML_NODE_HPP_) && !defined(_C4_YML_NODE_HPP_)
+#error "amalgamate: file c4/yml/node.hpp must have been included at this point"
+#endif /* C4_YML_NODE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/stack.hpp
+//#include "c4/yml/detail/stack.hpp"
+#if !defined(C4_YML_DETAIL_STACK_HPP_) && !defined(_C4_YML_DETAIL_STACK_HPP_)
+#error "amalgamate: file c4/yml/detail/stack.hpp must have been included at this point"
+#endif /* C4_YML_DETAIL_STACK_HPP_ */
+
+
+
+C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wtype-limits")
+C4_SUPPRESS_WARNING_MSVC_WITH_PUSH(4296/*expression is always 'boolean_value'*/)
+
+namespace c4 {
+namespace yml {
+
+
+csubstr normalize_tag(csubstr tag)
+{
+ YamlTag_e t = to_tag(tag);
+ if(t != TAG_NONE)
+ return from_tag(t);
+ if(tag.begins_with("!<"))
+ tag = tag.sub(1);
+ if(tag.begins_with("<!"))
+ return tag;
+ return tag;
+}
+
+csubstr normalize_tag_long(csubstr tag)
+{
+ YamlTag_e t = to_tag(tag);
+ if(t != TAG_NONE)
+ return from_tag_long(t);
+ if(tag.begins_with("!<"))
+ tag = tag.sub(1);
+ if(tag.begins_with("<!"))
+ return tag;
+ return tag;
+}
+
+YamlTag_e to_tag(csubstr tag)
+{
+ if(tag.begins_with("!<"))
+ tag = tag.sub(1);
+ if(tag.begins_with("!!"))
+ tag = tag.sub(2);
+ else if(tag.begins_with('!'))
+ return TAG_NONE;
+ else if(tag.begins_with("tag:yaml.org,2002:"))
+ {
+ RYML_ASSERT(csubstr("tag:yaml.org,2002:").len == 18);
+ tag = tag.sub(18);
+ }
+ else if(tag.begins_with("<tag:yaml.org,2002:"))
+ {
+ RYML_ASSERT(csubstr("<tag:yaml.org,2002:").len == 19);
+ tag = tag.sub(19);
+ if(!tag.len)
+ return TAG_NONE;
+ tag = tag.offs(0, 1);
+ }
+
+ if(tag == "map")
+ return TAG_MAP;
+ else if(tag == "omap")
+ return TAG_OMAP;
+ else if(tag == "pairs")
+ return TAG_PAIRS;
+ else if(tag == "set")
+ return TAG_SET;
+ else if(tag == "seq")
+ return TAG_SEQ;
+ else if(tag == "binary")
+ return TAG_BINARY;
+ else if(tag == "bool")
+ return TAG_BOOL;
+ else if(tag == "float")
+ return TAG_FLOAT;
+ else if(tag == "int")
+ return TAG_INT;
+ else if(tag == "merge")
+ return TAG_MERGE;
+ else if(tag == "null")
+ return TAG_NULL;
+ else if(tag == "str")
+ return TAG_STR;
+ else if(tag == "timestamp")
+ return TAG_TIMESTAMP;
+ else if(tag == "value")
+ return TAG_VALUE;
+
+ return TAG_NONE;
+}
+
+csubstr from_tag_long(YamlTag_e tag)
+{
+ switch(tag)
+ {
+ case TAG_MAP:
+ return {"<tag:yaml.org,2002:map>"};
+ case TAG_OMAP:
+ return {"<tag:yaml.org,2002:omap>"};
+ case TAG_PAIRS:
+ return {"<tag:yaml.org,2002:pairs>"};
+ case TAG_SET:
+ return {"<tag:yaml.org,2002:set>"};
+ case TAG_SEQ:
+ return {"<tag:yaml.org,2002:seq>"};
+ case TAG_BINARY:
+ return {"<tag:yaml.org,2002:binary>"};
+ case TAG_BOOL:
+ return {"<tag:yaml.org,2002:bool>"};
+ case TAG_FLOAT:
+ return {"<tag:yaml.org,2002:float>"};
+ case TAG_INT:
+ return {"<tag:yaml.org,2002:int>"};
+ case TAG_MERGE:
+ return {"<tag:yaml.org,2002:merge>"};
+ case TAG_NULL:
+ return {"<tag:yaml.org,2002:null>"};
+ case TAG_STR:
+ return {"<tag:yaml.org,2002:str>"};
+ case TAG_TIMESTAMP:
+ return {"<tag:yaml.org,2002:timestamp>"};
+ case TAG_VALUE:
+ return {"<tag:yaml.org,2002:value>"};
+ case TAG_YAML:
+ return {"<tag:yaml.org,2002:yaml>"};
+ case TAG_NONE:
+ return {""};
+ }
+ return {""};
+}
+
+csubstr from_tag(YamlTag_e tag)
+{
+ switch(tag)
+ {
+ case TAG_MAP:
+ return {"!!map"};
+ case TAG_OMAP:
+ return {"!!omap"};
+ case TAG_PAIRS:
+ return {"!!pairs"};
+ case TAG_SET:
+ return {"!!set"};
+ case TAG_SEQ:
+ return {"!!seq"};
+ case TAG_BINARY:
+ return {"!!binary"};
+ case TAG_BOOL:
+ return {"!!bool"};
+ case TAG_FLOAT:
+ return {"!!float"};
+ case TAG_INT:
+ return {"!!int"};
+ case TAG_MERGE:
+ return {"!!merge"};
+ case TAG_NULL:
+ return {"!!null"};
+ case TAG_STR:
+ return {"!!str"};
+ case TAG_TIMESTAMP:
+ return {"!!timestamp"};
+ case TAG_VALUE:
+ return {"!!value"};
+ case TAG_YAML:
+ return {"!!yaml"};
+ case TAG_NONE:
+ return {""};
+ }
+ return {""};
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+const char* NodeType::type_str(NodeType_e ty)
+{
+ switch(ty & _TYMASK)
+ {
+ case KEYVAL:
+ return "KEYVAL";
+ case KEY:
+ return "KEY";
+ case VAL:
+ return "VAL";
+ case MAP:
+ return "MAP";
+ case SEQ:
+ return "SEQ";
+ case KEYMAP:
+ return "KEYMAP";
+ case KEYSEQ:
+ return "KEYSEQ";
+ case DOCSEQ:
+ return "DOCSEQ";
+ case DOCMAP:
+ return "DOCMAP";
+ case DOCVAL:
+ return "DOCVAL";
+ case DOC:
+ return "DOC";
+ case STREAM:
+ return "STREAM";
+ case NOTYPE:
+ return "NOTYPE";
+ default:
+ if((ty & KEYVAL) == KEYVAL)
+ return "KEYVAL***";
+ if((ty & KEYMAP) == KEYMAP)
+ return "KEYMAP***";
+ if((ty & KEYSEQ) == KEYSEQ)
+ return "KEYSEQ***";
+ if((ty & DOCSEQ) == DOCSEQ)
+ return "DOCSEQ***";
+ if((ty & DOCMAP) == DOCMAP)
+ return "DOCMAP***";
+ if((ty & DOCVAL) == DOCVAL)
+ return "DOCVAL***";
+ if(ty & KEY)
+ return "KEY***";
+ if(ty & VAL)
+ return "VAL***";
+ if(ty & MAP)
+ return "MAP***";
+ if(ty & SEQ)
+ return "SEQ***";
+ if(ty & DOC)
+ return "DOC***";
+ return "(unk)";
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+NodeRef Tree::rootref()
+{
+ return NodeRef(this, root_id());
+}
+NodeRef const Tree::rootref() const
+{
+ return NodeRef(const_cast<Tree*>(this), root_id());
+}
+
+NodeRef Tree::ref(size_t id)
+{
+ _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_size);
+ return NodeRef(this, id);
+}
+NodeRef const Tree::ref(size_t id) const
+{
+ _RYML_CB_ASSERT(m_callbacks, id != NONE && id >= 0 && id < m_size);
+ return NodeRef(const_cast<Tree*>(this), id);
+}
+
+NodeRef Tree::operator[] (csubstr key)
+{
+ return rootref()[key];
+}
+NodeRef const Tree::operator[] (csubstr key) const
+{
+ return rootref()[key];
+}
+
+NodeRef Tree::operator[] (size_t i)
+{
+ return rootref()[i];
+}
+NodeRef const Tree::operator[] (size_t i) const
+{
+ return rootref()[i];
+}
+
+NodeRef Tree::docref(size_t i)
+{
+ return ref(doc(i));
+}
+NodeRef const Tree::docref(size_t i) const
+{
+ return ref(doc(i));
+}
+
+
+//-----------------------------------------------------------------------------
+Tree::Tree(Callbacks const& cb)
+ : m_buf(nullptr)
+ , m_cap(0)
+ , m_size(0)
+ , m_free_head(NONE)
+ , m_free_tail(NONE)
+ , m_arena()
+ , m_arena_pos(0)
+ , m_callbacks(cb)
+{
+}
+
+Tree::Tree(size_t node_capacity, size_t arena_capacity, Callbacks const& cb)
+ : Tree(cb)
+{
+ reserve(node_capacity);
+ reserve_arena(arena_capacity);
+}
+
+Tree::~Tree()
+{
+ _free();
+}
+
+
+Tree::Tree(Tree const& that) noexcept : Tree(that.m_callbacks)
+{
+ _copy(that);
+}
+
+Tree& Tree::operator= (Tree const& that) noexcept
+{
+ _free();
+ m_callbacks = that.m_callbacks;
+ _copy(that);
+ return *this;
+}
+
+Tree::Tree(Tree && that) noexcept : Tree(that.m_callbacks)
+{
+ _move(that);
+}
+
+Tree& Tree::operator= (Tree && that) noexcept
+{
+ _free();
+ m_callbacks = that.m_callbacks;
+ _move(that);
+ return *this;
+}
+
+void Tree::_free()
+{
+ if(m_buf)
+ {
+ _RYML_CB_ASSERT(m_callbacks, m_cap > 0);
+ _RYML_CB_FREE(m_callbacks, m_buf, NodeData, m_cap);
+ }
+ if(m_arena.str)
+ {
+ _RYML_CB_ASSERT(m_callbacks, m_arena.len > 0);
+ _RYML_CB_FREE(m_callbacks, m_arena.str, char, m_arena.len);
+ }
+ _clear();
+}
+
+
+C4_SUPPRESS_WARNING_GCC_PUSH
+#if defined(__GNUC__) && __GNUC__>= 8
+ C4_SUPPRESS_WARNING_GCC_WITH_PUSH("-Wclass-memaccess") // error: ‘void* memset(void*, int, size_t)’ clearing an object of type ‘class c4::yml::Tree’ with no trivial copy-assignment; use assignment or value-initialization instead
+#endif
+
+void Tree::_clear()
+{
+ m_buf = nullptr;
+ m_cap = 0;
+ m_size = 0;
+ m_free_head = 0;
+ m_free_tail = 0;
+ m_arena = {};
+ m_arena_pos = 0;
+ for(size_t i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i)
+ m_tag_directives[i] = {};
+}
+
+void Tree::_copy(Tree const& that)
+{
+ _RYML_CB_ASSERT(m_callbacks, m_buf == nullptr);
+ _RYML_CB_ASSERT(m_callbacks, m_arena.str == nullptr);
+ _RYML_CB_ASSERT(m_callbacks, m_arena.len == 0);
+ m_buf = _RYML_CB_ALLOC_HINT(m_callbacks, NodeData, that.m_cap, that.m_buf);
+ memcpy(m_buf, that.m_buf, that.m_cap * sizeof(NodeData));
+ m_cap = that.m_cap;
+ m_size = that.m_size;
+ m_free_head = that.m_free_head;
+ m_free_tail = that.m_free_tail;
+ m_arena_pos = that.m_arena_pos;
+ m_arena = that.m_arena;
+ if(that.m_arena.str)
+ {
+ _RYML_CB_ASSERT(m_callbacks, that.m_arena.len > 0);
+ substr arena;
+ arena.str = _RYML_CB_ALLOC_HINT(m_callbacks, char, that.m_arena.len, that.m_arena.str);
+ arena.len = that.m_arena.len;
+ _relocate(arena); // does a memcpy of the arena and updates nodes using the old arena
+ m_arena = arena;
+ }
+ for(size_t i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i)
+ m_tag_directives[i] = that.m_tag_directives[i];
+}
+
+void Tree::_move(Tree & that)
+{
+ _RYML_CB_ASSERT(m_callbacks, m_buf == nullptr);
+ _RYML_CB_ASSERT(m_callbacks, m_arena.str == nullptr);
+ _RYML_CB_ASSERT(m_callbacks, m_arena.len == 0);
+ m_buf = that.m_buf;
+ m_cap = that.m_cap;
+ m_size = that.m_size;
+ m_free_head = that.m_free_head;
+ m_free_tail = that.m_free_tail;
+ m_arena = that.m_arena;
+ m_arena_pos = that.m_arena_pos;
+ for(size_t i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i)
+ m_tag_directives[i] = that.m_tag_directives[i];
+ that._clear();
+}
+
+void Tree::_relocate(substr next_arena)
+{
+ _RYML_CB_ASSERT(m_callbacks, next_arena.not_empty());
+ _RYML_CB_ASSERT(m_callbacks, next_arena.len >= m_arena.len);
+ memcpy(next_arena.str, m_arena.str, m_arena_pos);
+ for(NodeData *C4_RESTRICT n = m_buf, *e = m_buf + m_cap; n != e; ++n)
+ {
+ if(in_arena(n->m_key.scalar))
+ n->m_key.scalar = _relocated(n->m_key.scalar, next_arena);
+ if(in_arena(n->m_key.tag))
+ n->m_key.tag = _relocated(n->m_key.tag, next_arena);
+ if(in_arena(n->m_key.anchor))
+ n->m_key.anchor = _relocated(n->m_key.anchor, next_arena);
+ if(in_arena(n->m_val.scalar))
+ n->m_val.scalar = _relocated(n->m_val.scalar, next_arena);
+ if(in_arena(n->m_val.tag))
+ n->m_val.tag = _relocated(n->m_val.tag, next_arena);
+ if(in_arena(n->m_val.anchor))
+ n->m_val.anchor = _relocated(n->m_val.anchor, next_arena);
+ }
+ for(TagDirective &C4_RESTRICT td : m_tag_directives)
+ {
+ if(in_arena(td.prefix))
+ td.prefix = _relocated(td.prefix, next_arena);
+ if(in_arena(td.handle))
+ td.handle = _relocated(td.handle, next_arena);
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+void Tree::reserve(size_t cap)
+{
+ if(cap > m_cap)
+ {
+ NodeData *buf = _RYML_CB_ALLOC_HINT(m_callbacks, NodeData, cap, m_buf);
+ if(m_buf)
+ {
+ memcpy(buf, m_buf, m_cap * sizeof(NodeData));
+ _RYML_CB_FREE(m_callbacks, m_buf, NodeData, m_cap);
+ }
+ size_t first = m_cap, del = cap - m_cap;
+ m_cap = cap;
+ m_buf = buf;
+ _clear_range(first, del);
+ if(m_free_head != NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, m_buf != nullptr);
+ _RYML_CB_ASSERT(m_callbacks, m_free_tail != NONE);
+ m_buf[m_free_tail].m_next_sibling = first;
+ m_buf[first].m_prev_sibling = m_free_tail;
+ m_free_tail = cap-1;
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_callbacks, m_free_tail == NONE);
+ m_free_head = first;
+ m_free_tail = cap-1;
+ }
+ _RYML_CB_ASSERT(m_callbacks, m_free_head == NONE || (m_free_head >= 0 && m_free_head < cap));
+ _RYML_CB_ASSERT(m_callbacks, m_free_tail == NONE || (m_free_tail >= 0 && m_free_tail < cap));
+
+ if( ! m_size)
+ _claim_root();
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+void Tree::clear()
+{
+ _clear_range(0, m_cap);
+ m_size = 0;
+ if(m_buf)
+ {
+ _RYML_CB_ASSERT(m_callbacks, m_cap >= 0);
+ m_free_head = 0;
+ m_free_tail = m_cap-1;
+ _claim_root();
+ }
+ else
+ {
+ m_free_head = NONE;
+ m_free_tail = NONE;
+ }
+ for(size_t i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i)
+ m_tag_directives[i] = {};
+}
+
+void Tree::_claim_root()
+{
+ size_t r = _claim();
+ _RYML_CB_ASSERT(m_callbacks, r == 0);
+ _set_hierarchy(r, NONE, NONE);
+}
+
+
+//-----------------------------------------------------------------------------
+void Tree::_clear_range(size_t first, size_t num)
+{
+ if(num == 0)
+ return; // prevent overflow when subtracting
+ _RYML_CB_ASSERT(m_callbacks, first >= 0 && first + num <= m_cap);
+ memset(m_buf + first, 0, num * sizeof(NodeData)); // TODO we should not need this
+ for(size_t i = first, e = first + num; i < e; ++i)
+ {
+ _clear(i);
+ NodeData *n = m_buf + i;
+ n->m_prev_sibling = i - 1;
+ n->m_next_sibling = i + 1;
+ }
+ m_buf[first + num - 1].m_next_sibling = NONE;
+}
+
+C4_SUPPRESS_WARNING_GCC_POP
+
+
+//-----------------------------------------------------------------------------
+void Tree::_release(size_t i)
+{
+ _RYML_CB_ASSERT(m_callbacks, i >= 0 && i < m_cap);
+
+ _rem_hierarchy(i);
+ _free_list_add(i);
+ _clear(i);
+
+ --m_size;
+}
+
+//-----------------------------------------------------------------------------
+// add to the front of the free list
+void Tree::_free_list_add(size_t i)
+{
+ _RYML_CB_ASSERT(m_callbacks, i >= 0 && i < m_cap);
+ NodeData &C4_RESTRICT w = m_buf[i];
+
+ w.m_parent = NONE;
+ w.m_next_sibling = m_free_head;
+ w.m_prev_sibling = NONE;
+ if(m_free_head != NONE)
+ m_buf[m_free_head].m_prev_sibling = i;
+ m_free_head = i;
+ if(m_free_tail == NONE)
+ m_free_tail = m_free_head;
+}
+
+void Tree::_free_list_rem(size_t i)
+{
+ if(m_free_head == i)
+ m_free_head = _p(i)->m_next_sibling;
+ _rem_hierarchy(i);
+}
+
+//-----------------------------------------------------------------------------
+size_t Tree::_claim()
+{
+ if(m_free_head == NONE || m_buf == nullptr)
+ {
+ size_t sz = 2 * m_cap;
+ sz = sz ? sz : 16;
+ reserve(sz);
+ _RYML_CB_ASSERT(m_callbacks, m_free_head != NONE);
+ }
+
+ _RYML_CB_ASSERT(m_callbacks, m_size < m_cap);
+ _RYML_CB_ASSERT(m_callbacks, m_free_head >= 0 && m_free_head < m_cap);
+
+ size_t ichild = m_free_head;
+ NodeData *child = m_buf + ichild;
+
+ ++m_size;
+ m_free_head = child->m_next_sibling;
+ if(m_free_head == NONE)
+ {
+ m_free_tail = NONE;
+ _RYML_CB_ASSERT(m_callbacks, m_size == m_cap);
+ }
+
+ _clear(ichild);
+
+ return ichild;
+}
+
+//-----------------------------------------------------------------------------
+
+C4_SUPPRESS_WARNING_GCC_PUSH
+C4_SUPPRESS_WARNING_CLANG_PUSH
+C4_SUPPRESS_WARNING_CLANG("-Wnull-dereference")
+#if defined(__GNUC__) && (__GNUC__ >= 6)
+C4_SUPPRESS_WARNING_GCC("-Wnull-dereference")
+#endif
+
+void Tree::_set_hierarchy(size_t ichild, size_t iparent, size_t iprev_sibling)
+{
+ _RYML_CB_ASSERT(m_callbacks, iparent == NONE || (iparent >= 0 && iparent < m_cap));
+ _RYML_CB_ASSERT(m_callbacks, iprev_sibling == NONE || (iprev_sibling >= 0 && iprev_sibling < m_cap));
+
+ NodeData *C4_RESTRICT child = get(ichild);
+
+ child->m_parent = iparent;
+ child->m_prev_sibling = NONE;
+ child->m_next_sibling = NONE;
+
+ if(iparent == NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, ichild == 0);
+ _RYML_CB_ASSERT(m_callbacks, iprev_sibling == NONE);
+ }
+
+ if(iparent == NONE)
+ return;
+
+ size_t inext_sibling = iprev_sibling != NONE ? next_sibling(iprev_sibling) : first_child(iparent);
+ NodeData *C4_RESTRICT parent = get(iparent);
+ NodeData *C4_RESTRICT psib = get(iprev_sibling);
+ NodeData *C4_RESTRICT nsib = get(inext_sibling);
+
+ if(psib)
+ {
+ _RYML_CB_ASSERT(m_callbacks, next_sibling(iprev_sibling) == id(nsib));
+ child->m_prev_sibling = id(psib);
+ psib->m_next_sibling = id(child);
+ _RYML_CB_ASSERT(m_callbacks, psib->m_prev_sibling != psib->m_next_sibling || psib->m_prev_sibling == NONE);
+ }
+
+ if(nsib)
+ {
+ _RYML_CB_ASSERT(m_callbacks, prev_sibling(inext_sibling) == id(psib));
+ child->m_next_sibling = id(nsib);
+ nsib->m_prev_sibling = id(child);
+ _RYML_CB_ASSERT(m_callbacks, nsib->m_prev_sibling != nsib->m_next_sibling || nsib->m_prev_sibling == NONE);
+ }
+
+ if(parent->m_first_child == NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, parent->m_last_child == NONE);
+ parent->m_first_child = id(child);
+ parent->m_last_child = id(child);
+ }
+ else
+ {
+ if(child->m_next_sibling == parent->m_first_child)
+ parent->m_first_child = id(child);
+
+ if(child->m_prev_sibling == parent->m_last_child)
+ parent->m_last_child = id(child);
+ }
+}
+
+C4_SUPPRESS_WARNING_GCC_POP
+C4_SUPPRESS_WARNING_CLANG_POP
+
+
+//-----------------------------------------------------------------------------
+void Tree::_rem_hierarchy(size_t i)
+{
+ _RYML_CB_ASSERT(m_callbacks, i >= 0 && i < m_cap);
+
+ NodeData &C4_RESTRICT w = m_buf[i];
+
+ // remove from the parent
+ if(w.m_parent != NONE)
+ {
+ NodeData &C4_RESTRICT p = m_buf[w.m_parent];
+ if(p.m_first_child == i)
+ {
+ p.m_first_child = w.m_next_sibling;
+ }
+ if(p.m_last_child == i)
+ {
+ p.m_last_child = w.m_prev_sibling;
+ }
+ }
+
+ // remove from the used list
+ if(w.m_prev_sibling != NONE)
+ {
+ NodeData *C4_RESTRICT prev = get(w.m_prev_sibling);
+ prev->m_next_sibling = w.m_next_sibling;
+ }
+ if(w.m_next_sibling != NONE)
+ {
+ NodeData *C4_RESTRICT next = get(w.m_next_sibling);
+ next->m_prev_sibling = w.m_prev_sibling;
+ }
+}
+
+//-----------------------------------------------------------------------------
+void Tree::reorder()
+{
+ size_t r = root_id();
+ _do_reorder(&r, 0);
+}
+
+//-----------------------------------------------------------------------------
+size_t Tree::_do_reorder(size_t *node, size_t count)
+{
+ // swap this node if it's not in place
+ if(*node != count)
+ {
+ _swap(*node, count);
+ *node = count;
+ }
+ ++count; // bump the count from this node
+
+ // now descend in the hierarchy
+ for(size_t i = first_child(*node); i != NONE; i = next_sibling(i))
+ {
+ // this child may have been relocated to a different index,
+ // so get an updated version
+ count = _do_reorder(&i, count);
+ }
+ return count;
+}
+
+//-----------------------------------------------------------------------------
+void Tree::_swap(size_t n_, size_t m_)
+{
+ _RYML_CB_ASSERT(m_callbacks, (parent(n_) != NONE) || type(n_) == NOTYPE);
+ _RYML_CB_ASSERT(m_callbacks, (parent(m_) != NONE) || type(m_) == NOTYPE);
+ NodeType tn = type(n_);
+ NodeType tm = type(m_);
+ if(tn != NOTYPE && tm != NOTYPE)
+ {
+ _swap_props(n_, m_);
+ _swap_hierarchy(n_, m_);
+ }
+ else if(tn == NOTYPE && tm != NOTYPE)
+ {
+ _copy_props(n_, m_);
+ _free_list_rem(n_);
+ _copy_hierarchy(n_, m_);
+ _clear(m_);
+ _free_list_add(m_);
+ }
+ else if(tn != NOTYPE && tm == NOTYPE)
+ {
+ _copy_props(m_, n_);
+ _free_list_rem(m_);
+ _copy_hierarchy(m_, n_);
+ _clear(n_);
+ _free_list_add(n_);
+ }
+ else
+ {
+ C4_NEVER_REACH();
+ }
+}
+
+//-----------------------------------------------------------------------------
+void Tree::_swap_hierarchy(size_t ia, size_t ib)
+{
+ if(ia == ib) return;
+
+ for(size_t i = first_child(ia); i != NONE; i = next_sibling(i))
+ {
+ if(i == ib || i == ia)
+ continue;
+ _p(i)->m_parent = ib;
+ }
+
+ for(size_t i = first_child(ib); i != NONE; i = next_sibling(i))
+ {
+ if(i == ib || i == ia)
+ continue;
+ _p(i)->m_parent = ia;
+ }
+
+ auto & C4_RESTRICT a = *_p(ia);
+ auto & C4_RESTRICT b = *_p(ib);
+ auto & C4_RESTRICT pa = *_p(a.m_parent);
+ auto & C4_RESTRICT pb = *_p(b.m_parent);
+
+ if(&pa == &pb)
+ {
+ if((pa.m_first_child == ib && pa.m_last_child == ia)
+ ||
+ (pa.m_first_child == ia && pa.m_last_child == ib))
+ {
+ std::swap(pa.m_first_child, pa.m_last_child);
+ }
+ else
+ {
+ bool changed = false;
+ if(pa.m_first_child == ia)
+ {
+ pa.m_first_child = ib;
+ changed = true;
+ }
+ if(pa.m_last_child == ia)
+ {
+ pa.m_last_child = ib;
+ changed = true;
+ }
+ if(pb.m_first_child == ib && !changed)
+ {
+ pb.m_first_child = ia;
+ }
+ if(pb.m_last_child == ib && !changed)
+ {
+ pb.m_last_child = ia;
+ }
+ }
+ }
+ else
+ {
+ if(pa.m_first_child == ia)
+ pa.m_first_child = ib;
+ if(pa.m_last_child == ia)
+ pa.m_last_child = ib;
+ if(pb.m_first_child == ib)
+ pb.m_first_child = ia;
+ if(pb.m_last_child == ib)
+ pb.m_last_child = ia;
+ }
+ std::swap(a.m_first_child , b.m_first_child);
+ std::swap(a.m_last_child , b.m_last_child);
+
+ if(a.m_prev_sibling != ib && b.m_prev_sibling != ia &&
+ a.m_next_sibling != ib && b.m_next_sibling != ia)
+ {
+ if(a.m_prev_sibling != NONE && a.m_prev_sibling != ib)
+ _p(a.m_prev_sibling)->m_next_sibling = ib;
+ if(a.m_next_sibling != NONE && a.m_next_sibling != ib)
+ _p(a.m_next_sibling)->m_prev_sibling = ib;
+ if(b.m_prev_sibling != NONE && b.m_prev_sibling != ia)
+ _p(b.m_prev_sibling)->m_next_sibling = ia;
+ if(b.m_next_sibling != NONE && b.m_next_sibling != ia)
+ _p(b.m_next_sibling)->m_prev_sibling = ia;
+ std::swap(a.m_prev_sibling, b.m_prev_sibling);
+ std::swap(a.m_next_sibling, b.m_next_sibling);
+ }
+ else
+ {
+ if(a.m_next_sibling == ib) // n will go after m
+ {
+ _RYML_CB_ASSERT(m_callbacks, b.m_prev_sibling == ia);
+ if(a.m_prev_sibling != NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, a.m_prev_sibling != ib);
+ _p(a.m_prev_sibling)->m_next_sibling = ib;
+ }
+ if(b.m_next_sibling != NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, b.m_next_sibling != ia);
+ _p(b.m_next_sibling)->m_prev_sibling = ia;
+ }
+ size_t ns = b.m_next_sibling;
+ b.m_prev_sibling = a.m_prev_sibling;
+ b.m_next_sibling = ia;
+ a.m_prev_sibling = ib;
+ a.m_next_sibling = ns;
+ }
+ else if(a.m_prev_sibling == ib) // m will go after n
+ {
+ _RYML_CB_ASSERT(m_callbacks, b.m_next_sibling == ia);
+ if(b.m_prev_sibling != NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, b.m_prev_sibling != ia);
+ _p(b.m_prev_sibling)->m_next_sibling = ia;
+ }
+ if(a.m_next_sibling != NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, a.m_next_sibling != ib);
+ _p(a.m_next_sibling)->m_prev_sibling = ib;
+ }
+ size_t ns = b.m_prev_sibling;
+ a.m_prev_sibling = b.m_prev_sibling;
+ a.m_next_sibling = ib;
+ b.m_prev_sibling = ia;
+ b.m_next_sibling = ns;
+ }
+ else
+ {
+ C4_NEVER_REACH();
+ }
+ }
+ _RYML_CB_ASSERT(m_callbacks, a.m_next_sibling != ia);
+ _RYML_CB_ASSERT(m_callbacks, a.m_prev_sibling != ia);
+ _RYML_CB_ASSERT(m_callbacks, b.m_next_sibling != ib);
+ _RYML_CB_ASSERT(m_callbacks, b.m_prev_sibling != ib);
+
+ if(a.m_parent != ib && b.m_parent != ia)
+ {
+ std::swap(a.m_parent, b.m_parent);
+ }
+ else
+ {
+ if(a.m_parent == ib && b.m_parent != ia)
+ {
+ a.m_parent = b.m_parent;
+ b.m_parent = ia;
+ }
+ else if(a.m_parent != ib && b.m_parent == ia)
+ {
+ b.m_parent = a.m_parent;
+ a.m_parent = ib;
+ }
+ else
+ {
+ C4_NEVER_REACH();
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+void Tree::_copy_hierarchy(size_t dst_, size_t src_)
+{
+ auto const& C4_RESTRICT src = *_p(src_);
+ auto & C4_RESTRICT dst = *_p(dst_);
+ auto & C4_RESTRICT prt = *_p(src.m_parent);
+ for(size_t i = src.m_first_child; i != NONE; i = next_sibling(i))
+ {
+ _p(i)->m_parent = dst_;
+ }
+ if(src.m_prev_sibling != NONE)
+ {
+ _p(src.m_prev_sibling)->m_next_sibling = dst_;
+ }
+ if(src.m_next_sibling != NONE)
+ {
+ _p(src.m_next_sibling)->m_prev_sibling = dst_;
+ }
+ if(prt.m_first_child == src_)
+ {
+ prt.m_first_child = dst_;
+ }
+ if(prt.m_last_child == src_)
+ {
+ prt.m_last_child = dst_;
+ }
+ dst.m_parent = src.m_parent;
+ dst.m_first_child = src.m_first_child;
+ dst.m_last_child = src.m_last_child;
+ dst.m_prev_sibling = src.m_prev_sibling;
+ dst.m_next_sibling = src.m_next_sibling;
+}
+
+//-----------------------------------------------------------------------------
+void Tree::_swap_props(size_t n_, size_t m_)
+{
+ NodeData &C4_RESTRICT n = *_p(n_);
+ NodeData &C4_RESTRICT m = *_p(m_);
+ std::swap(n.m_type, m.m_type);
+ std::swap(n.m_key, m.m_key);
+ std::swap(n.m_val, m.m_val);
+}
+
+//-----------------------------------------------------------------------------
+void Tree::move(size_t node, size_t after)
+{
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ _RYML_CB_ASSERT(m_callbacks, ! is_root(node));
+ _RYML_CB_ASSERT(m_callbacks, has_sibling(node, after) && has_sibling(after, node));
+
+ _rem_hierarchy(node);
+ _set_hierarchy(node, parent(node), after);
+}
+
+//-----------------------------------------------------------------------------
+
+void Tree::move(size_t node, size_t new_parent, size_t after)
+{
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ _RYML_CB_ASSERT(m_callbacks, new_parent != NONE);
+ _RYML_CB_ASSERT(m_callbacks, ! is_root(node));
+
+ _rem_hierarchy(node);
+ _set_hierarchy(node, new_parent, after);
+}
+
+size_t Tree::move(Tree *src, size_t node, size_t new_parent, size_t after)
+{
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ _RYML_CB_ASSERT(m_callbacks, new_parent != NONE);
+
+ size_t dup = duplicate(src, node, new_parent, after);
+ src->remove(node);
+ return dup;
+}
+
+void Tree::set_root_as_stream()
+{
+ size_t root = root_id();
+ if(is_stream(root))
+ return;
+ // don't use _add_flags() because it's checked and will fail
+ if(!has_children(root))
+ {
+ if(is_val(root))
+ {
+ _p(root)->m_type.add(SEQ);
+ size_t next_doc = append_child(root);
+ _copy_props_wo_key(next_doc, root);
+ _p(next_doc)->m_type.add(DOC);
+ _p(next_doc)->m_type.rem(SEQ);
+ }
+ _p(root)->m_type = STREAM;
+ return;
+ }
+ _RYML_CB_ASSERT(m_callbacks, !has_key(root));
+ size_t next_doc = append_child(root);
+ _copy_props_wo_key(next_doc, root);
+ _add_flags(next_doc, DOC);
+ for(size_t prev = NONE, ch = first_child(root), next = next_sibling(ch); ch != NONE; )
+ {
+ if(ch == next_doc)
+ break;
+ move(ch, next_doc, prev);
+ prev = ch;
+ ch = next;
+ next = next_sibling(next);
+ }
+ _p(root)->m_type = STREAM;
+}
+
+
+//-----------------------------------------------------------------------------
+void Tree::remove_children(size_t node)
+{
+ _RYML_CB_ASSERT(m_callbacks, get(node) != nullptr);
+ size_t ich = get(node)->m_first_child;
+ while(ich != NONE)
+ {
+ remove_children(ich);
+ _RYML_CB_ASSERT(m_callbacks, get(ich) != nullptr);
+ size_t next = get(ich)->m_next_sibling;
+ _release(ich);
+ if(ich == get(node)->m_last_child)
+ break;
+ ich = next;
+ }
+}
+
+bool Tree::change_type(size_t node, NodeType type)
+{
+ _RYML_CB_ASSERT(m_callbacks, type.is_val() || type.is_map() || type.is_seq());
+ _RYML_CB_ASSERT(m_callbacks, type.is_val() + type.is_map() + type.is_seq() == 1);
+ _RYML_CB_ASSERT(m_callbacks, type.has_key() == has_key(node) || (has_key(node) && !type.has_key()));
+ NodeData *d = _p(node);
+ if(type.is_map() && is_map(node))
+ return false;
+ else if(type.is_seq() && is_seq(node))
+ return false;
+ else if(type.is_val() && is_val(node))
+ return false;
+ d->m_type = (d->m_type & (~(MAP|SEQ|VAL))) | type;
+ remove_children(node);
+ return true;
+}
+
+
+//-----------------------------------------------------------------------------
+size_t Tree::duplicate(size_t node, size_t parent, size_t after)
+{
+ return duplicate(this, node, parent, after);
+}
+
+size_t Tree::duplicate(Tree const* src, size_t node, size_t parent, size_t after)
+{
+ _RYML_CB_ASSERT(m_callbacks, src != nullptr);
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ _RYML_CB_ASSERT(m_callbacks, parent != NONE);
+ _RYML_CB_ASSERT(m_callbacks, ! src->is_root(node));
+
+ size_t copy = _claim();
+
+ _copy_props(copy, src, node);
+ _set_hierarchy(copy, parent, after);
+ duplicate_children(src, node, copy, NONE);
+
+ return copy;
+}
+
+//-----------------------------------------------------------------------------
+size_t Tree::duplicate_children(size_t node, size_t parent, size_t after)
+{
+ return duplicate_children(this, node, parent, after);
+}
+
+size_t Tree::duplicate_children(Tree const* src, size_t node, size_t parent, size_t after)
+{
+ _RYML_CB_ASSERT(m_callbacks, src != nullptr);
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ _RYML_CB_ASSERT(m_callbacks, parent != NONE);
+ _RYML_CB_ASSERT(m_callbacks, after == NONE || has_child(parent, after));
+
+ size_t prev = after;
+ for(size_t i = src->first_child(node); i != NONE; i = src->next_sibling(i))
+ {
+ prev = duplicate(src, i, parent, prev);
+ }
+
+ return prev;
+}
+
+//-----------------------------------------------------------------------------
+void Tree::duplicate_contents(size_t node, size_t where)
+{
+ duplicate_contents(this, node, where);
+}
+
+void Tree::duplicate_contents(Tree const *src, size_t node, size_t where)
+{
+ _RYML_CB_ASSERT(m_callbacks, src != nullptr);
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ _RYML_CB_ASSERT(m_callbacks, where != NONE);
+ _copy_props_wo_key(where, src, node);
+ duplicate_children(src, node, where, last_child(where));
+}
+
+//-----------------------------------------------------------------------------
+size_t Tree::duplicate_children_no_rep(size_t node, size_t parent, size_t after)
+{
+ return duplicate_children_no_rep(this, node, parent, after);
+}
+
+size_t Tree::duplicate_children_no_rep(Tree const *src, size_t node, size_t parent, size_t after)
+{
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ _RYML_CB_ASSERT(m_callbacks, parent != NONE);
+ _RYML_CB_ASSERT(m_callbacks, after == NONE || has_child(parent, after));
+
+ // don't loop using pointers as there may be a relocation
+
+ // find the position where "after" is
+ size_t after_pos = NONE;
+ if(after != NONE)
+ {
+ for(size_t i = first_child(parent), icount = 0; i != NONE; ++icount, i = next_sibling(i))
+ {
+ if(i == after)
+ {
+ after_pos = icount;
+ break;
+ }
+ }
+ _RYML_CB_ASSERT(m_callbacks, after_pos != NONE);
+ }
+
+ // for each child to be duplicated...
+ size_t prev = after;
+ for(size_t i = src->first_child(node), icount = 0; i != NONE; ++icount, i = src->next_sibling(i))
+ {
+ if(is_seq(parent))
+ {
+ prev = duplicate(i, parent, prev);
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_callbacks, is_map(parent));
+ // does the parent already have a node with key equal to that of the current duplicate?
+ size_t rep = NONE, rep_pos = NONE;
+ for(size_t j = first_child(parent), jcount = 0; j != NONE; ++jcount, j = next_sibling(j))
+ {
+ if(key(j) == key(i))
+ {
+ rep = j;
+ rep_pos = jcount;
+ break;
+ }
+ }
+ if(rep == NONE) // there is no repetition; just duplicate
+ {
+ prev = duplicate(src, i, parent, prev);
+ }
+ else // yes, there is a repetition
+ {
+ if(after_pos != NONE && rep_pos < after_pos)
+ {
+ // rep is located before the node which will be inserted,
+ // and will be overridden by the duplicate. So replace it.
+ remove(rep);
+ prev = duplicate(src, i, parent, prev);
+ }
+ else if(after_pos == NONE || rep_pos >= after_pos)
+ {
+ // rep is located after the node which will be inserted
+ // and overrides it. So move the rep into this node's place.
+ if(rep != prev)
+ {
+ move(rep, prev);
+ prev = rep;
+ }
+ }
+ } // there's a repetition
+ }
+ }
+
+ return prev;
+}
+
+
+//-----------------------------------------------------------------------------
+
+void Tree::merge_with(Tree const *src, size_t src_node, size_t dst_node)
+{
+ _RYML_CB_ASSERT(m_callbacks, src != nullptr);
+ if(src_node == NONE)
+ src_node = src->root_id();
+ if(dst_node == NONE)
+ dst_node = root_id();
+ _RYML_CB_ASSERT(m_callbacks, src->has_val(src_node) || src->is_seq(src_node) || src->is_map(src_node));
+
+ if(src->has_val(src_node))
+ {
+ if( ! has_val(dst_node))
+ {
+ if(has_children(dst_node))
+ remove_children(dst_node);
+ }
+ if(src->is_keyval(src_node))
+ _copy_props(dst_node, src, src_node);
+ else if(src->is_val(src_node))
+ _copy_props_wo_key(dst_node, src, src_node);
+ else
+ C4_NEVER_REACH();
+ }
+ else if(src->is_seq(src_node))
+ {
+ if( ! is_seq(dst_node))
+ {
+ if(has_children(dst_node))
+ remove_children(dst_node);
+ _clear_type(dst_node);
+ if(src->has_key(src_node))
+ to_seq(dst_node, src->key(src_node));
+ else
+ to_seq(dst_node);
+ }
+ for(size_t sch = src->first_child(src_node); sch != NONE; sch = src->next_sibling(sch))
+ {
+ size_t dch = append_child(dst_node);
+ _copy_props_wo_key(dch, src, sch);
+ merge_with(src, sch, dch);
+ }
+ }
+ else if(src->is_map(src_node))
+ {
+ if( ! is_map(dst_node))
+ {
+ if(has_children(dst_node))
+ remove_children(dst_node);
+ _clear_type(dst_node);
+ if(src->has_key(src_node))
+ to_map(dst_node, src->key(src_node));
+ else
+ to_map(dst_node);
+ }
+ for(size_t sch = src->first_child(src_node); sch != NONE; sch = src->next_sibling(sch))
+ {
+ size_t dch = find_child(dst_node, src->key(sch));
+ if(dch == NONE)
+ {
+ dch = append_child(dst_node);
+ _copy_props(dch, src, sch);
+ }
+ merge_with(src, sch, dch);
+ }
+ }
+ else
+ {
+ C4_NEVER_REACH();
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+
+namespace detail {
+/** @todo make this part of the public API, refactoring as appropriate
+ * to be able to use the same resolver to handle multiple trees (one
+ * at a time) */
+struct ReferenceResolver
+{
+ struct refdata
+ {
+ NodeType type;
+ size_t node;
+ size_t prev_anchor;
+ size_t target;
+ size_t parent_ref;
+ size_t parent_ref_sibling;
+ };
+
+ Tree *t;
+ /** from the specs: "an alias node refers to the most recent
+ * node in the serialization having the specified anchor". So
+ * we need to start looking upward from ref nodes.
+ *
+ * @see http://yaml.org/spec/1.2/spec.html#id2765878 */
+ stack<refdata> refs;
+
+ ReferenceResolver(Tree *t_) : t(t_), refs(t_->callbacks())
+ {
+ resolve();
+ }
+
+ void store_anchors_and_refs()
+ {
+ // minimize (re-)allocations by counting first
+ size_t num_anchors_and_refs = count_anchors_and_refs(t->root_id());
+ if(!num_anchors_and_refs)
+ return;
+ refs.reserve(num_anchors_and_refs);
+
+ // now descend through the hierarchy
+ _store_anchors_and_refs(t->root_id());
+
+ // finally connect the reference list
+ size_t prev_anchor = npos;
+ size_t count = 0;
+ for(auto &rd : refs)
+ {
+ rd.prev_anchor = prev_anchor;
+ if(rd.type.is_anchor())
+ prev_anchor = count;
+ ++count;
+ }
+ }
+
+ size_t count_anchors_and_refs(size_t n)
+ {
+ size_t c = 0;
+ c += t->has_key_anchor(n);
+ c += t->has_val_anchor(n);
+ c += t->is_key_ref(n);
+ c += t->is_val_ref(n);
+ for(size_t ch = t->first_child(n); ch != NONE; ch = t->next_sibling(ch))
+ c += count_anchors_and_refs(ch);
+ return c;
+ }
+
+ void _store_anchors_and_refs(size_t n)
+ {
+ if(t->is_key_ref(n) || t->is_val_ref(n) || (t->has_key(n) && t->key(n) == "<<"))
+ {
+ if(t->is_seq(n))
+ {
+ // for merging multiple inheritance targets
+ // <<: [ *CENTER, *BIG ]
+ for(size_t ich = t->first_child(n); ich != NONE; ich = t->next_sibling(ich))
+ {
+ RYML_ASSERT(t->num_children(ich) == 0);
+ refs.push({VALREF, ich, npos, npos, n, t->next_sibling(n)});
+ }
+ return;
+ }
+ if(t->is_key_ref(n) && t->key(n) != "<<") // insert key refs BEFORE inserting val refs
+ {
+ RYML_CHECK((!t->has_key(n)) || t->key(n).ends_with(t->key_ref(n)));
+ refs.push({KEYREF, n, npos, npos, NONE, NONE});
+ }
+ if(t->is_val_ref(n))
+ {
+ RYML_CHECK((!t->has_val(n)) || t->val(n).ends_with(t->val_ref(n)));
+ refs.push({VALREF, n, npos, npos, NONE, NONE});
+ }
+ }
+ if(t->has_key_anchor(n))
+ {
+ RYML_CHECK(t->has_key(n));
+ refs.push({KEYANCH, n, npos, npos, NONE, NONE});
+ }
+ if(t->has_val_anchor(n))
+ {
+ RYML_CHECK(t->has_val(n) || t->is_container(n));
+ refs.push({VALANCH, n, npos, npos, NONE, NONE});
+ }
+ for(size_t ch = t->first_child(n); ch != NONE; ch = t->next_sibling(ch))
+ {
+ _store_anchors_and_refs(ch);
+ }
+ }
+
+ size_t lookup_(refdata *C4_RESTRICT ra)
+ {
+ RYML_ASSERT(ra->type.is_key_ref() || ra->type.is_val_ref());
+ RYML_ASSERT(ra->type.is_key_ref() != ra->type.is_val_ref());
+ csubstr refname;
+ if(ra->type.is_val_ref())
+ {
+ refname = t->val_ref(ra->node);
+ }
+ else
+ {
+ RYML_ASSERT(ra->type.is_key_ref());
+ refname = t->key_ref(ra->node);
+ }
+ while(ra->prev_anchor != npos)
+ {
+ ra = &refs[ra->prev_anchor];
+ if(t->has_anchor(ra->node, refname))
+ return ra->node;
+ }
+
+ #ifndef RYML_ERRMSG_SIZE
+ #define RYML_ERRMSG_SIZE 1024
+ #endif
+
+ char errmsg[RYML_ERRMSG_SIZE];
+ snprintf(errmsg, RYML_ERRMSG_SIZE, "anchor does not exist: '%.*s'",
+ static_cast<int>(refname.size()), refname.data());
+ c4::yml::error(errmsg);
+ return NONE;
+ }
+
+ void resolve()
+ {
+ store_anchors_and_refs();
+ if(refs.empty())
+ return;
+
+ /* from the specs: "an alias node refers to the most recent
+ * node in the serialization having the specified anchor". So
+ * we need to start looking upward from ref nodes.
+ *
+ * @see http://yaml.org/spec/1.2/spec.html#id2765878 */
+ for(size_t i = 0, e = refs.size(); i < e; ++i)
+ {
+ auto &C4_RESTRICT rd = refs.top(i);
+ if( ! rd.type.is_ref())
+ continue;
+ rd.target = lookup_(&rd);
+ }
+ }
+
+}; // ReferenceResolver
+} // namespace detail
+
+void Tree::resolve()
+{
+ if(m_size == 0)
+ return;
+
+ detail::ReferenceResolver rr(this);
+
+ // insert the resolved references
+ size_t prev_parent_ref = NONE;
+ size_t prev_parent_ref_after = NONE;
+ for(auto const& C4_RESTRICT rd : rr.refs)
+ {
+ if( ! rd.type.is_ref())
+ continue;
+ if(rd.parent_ref != NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, is_seq(rd.parent_ref));
+ size_t after, p = parent(rd.parent_ref);
+ if(prev_parent_ref != rd.parent_ref)
+ {
+ after = rd.parent_ref;//prev_sibling(rd.parent_ref_sibling);
+ prev_parent_ref_after = after;
+ }
+ else
+ {
+ after = prev_parent_ref_after;
+ }
+ prev_parent_ref = rd.parent_ref;
+ prev_parent_ref_after = duplicate_children_no_rep(rd.target, p, after);
+ remove(rd.node);
+ }
+ else
+ {
+ if(has_key(rd.node) && is_key_ref(rd.node) && key(rd.node) == "<<")
+ {
+ _RYML_CB_ASSERT(m_callbacks, is_keyval(rd.node));
+ size_t p = parent(rd.node);
+ size_t after = prev_sibling(rd.node);
+ duplicate_children_no_rep(rd.target, p, after);
+ remove(rd.node);
+ }
+ else if(rd.type.is_key_ref())
+ {
+ _RYML_CB_ASSERT(m_callbacks, is_key_ref(rd.node));
+ _RYML_CB_ASSERT(m_callbacks, has_key_anchor(rd.target) || has_val_anchor(rd.target));
+ if(has_val_anchor(rd.target) && val_anchor(rd.target) == key_ref(rd.node))
+ {
+ _RYML_CB_CHECK(m_callbacks, !is_container(rd.target));
+ _RYML_CB_CHECK(m_callbacks, has_val(rd.target));
+ _p(rd.node)->m_key.scalar = val(rd.target);
+ _add_flags(rd.node, KEY);
+ }
+ else
+ {
+ _RYML_CB_CHECK(m_callbacks, key_anchor(rd.target) == key_ref(rd.node));
+ _p(rd.node)->m_key.scalar = key(rd.target);
+ _add_flags(rd.node, VAL);
+ }
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_callbacks, rd.type.is_val_ref());
+ if(has_key_anchor(rd.target) && key_anchor(rd.target) == val_ref(rd.node))
+ {
+ _RYML_CB_CHECK(m_callbacks, !is_container(rd.target));
+ _RYML_CB_CHECK(m_callbacks, has_val(rd.target));
+ _p(rd.node)->m_val.scalar = key(rd.target);
+ _add_flags(rd.node, VAL);
+ }
+ else
+ {
+ duplicate_contents(rd.target, rd.node);
+ }
+ }
+ }
+ }
+
+ // clear anchors and refs
+ for(auto const& C4_RESTRICT ar : rr.refs)
+ {
+ rem_anchor_ref(ar.node);
+ if(ar.parent_ref != NONE)
+ if(type(ar.parent_ref) != NOTYPE)
+ remove(ar.parent_ref);
+ }
+
+}
+
+//-----------------------------------------------------------------------------
+
+size_t Tree::num_children(size_t node) const
+{
+ size_t count = 0;
+ for(size_t i = first_child(node); i != NONE; i = next_sibling(i))
+ {
+ ++count;
+ }
+ return count;
+}
+
+size_t Tree::child(size_t node, size_t pos) const
+{
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ size_t count = 0;
+ for(size_t i = first_child(node); i != NONE; i = next_sibling(i))
+ {
+ if(count++ == pos)
+ return i;
+ }
+ return NONE;
+}
+
+size_t Tree::child_pos(size_t node, size_t ch) const
+{
+ size_t count = 0;
+ for(size_t i = first_child(node); i != NONE; i = next_sibling(i))
+ {
+ if(i == ch)
+ return count;
+ ++count;
+ }
+ return npos;
+}
+
+#if defined(__clang__)
+# pragma clang diagnostic push
+# pragma GCC diagnostic ignored "-Wnull-dereference"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# if __GNUC__ >= 6
+# pragma GCC diagnostic ignored "-Wnull-dereference"
+# endif
+#endif
+
+size_t Tree::find_child(size_t node, csubstr const& name) const
+{
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ _RYML_CB_ASSERT(m_callbacks, is_map(node));
+ if(get(node)->m_first_child == NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, _p(node)->m_last_child == NONE);
+ return NONE;
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_callbacks, _p(node)->m_last_child != NONE);
+ }
+ for(size_t i = first_child(node); i != NONE; i = next_sibling(i))
+ {
+ if(_p(i)->m_key.scalar == name)
+ {
+ return i;
+ }
+ }
+ return NONE;
+}
+
+#if defined(__clang__)
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+
+//-----------------------------------------------------------------------------
+
+void Tree::to_val(size_t node, csubstr val, type_bits more_flags)
+{
+ _RYML_CB_ASSERT(m_callbacks, ! has_children(node));
+ _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || ! parent_is_map(node));
+ _set_flags(node, VAL|more_flags);
+ _p(node)->m_key.clear();
+ _p(node)->m_val = val;
+}
+
+void Tree::to_keyval(size_t node, csubstr key, csubstr val, type_bits more_flags)
+{
+ _RYML_CB_ASSERT(m_callbacks, ! has_children(node));
+ _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || parent_is_map(node));
+ _set_flags(node, KEYVAL|more_flags);
+ _p(node)->m_key = key;
+ _p(node)->m_val = val;
+}
+
+void Tree::to_map(size_t node, type_bits more_flags)
+{
+ _RYML_CB_ASSERT(m_callbacks, ! has_children(node));
+ _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || ! parent_is_map(node)); // parent must not have children with keys
+ _set_flags(node, MAP|more_flags);
+ _p(node)->m_key.clear();
+ _p(node)->m_val.clear();
+}
+
+void Tree::to_map(size_t node, csubstr key, type_bits more_flags)
+{
+ _RYML_CB_ASSERT(m_callbacks, ! has_children(node));
+ _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || parent_is_map(node));
+ _set_flags(node, KEY|MAP|more_flags);
+ _p(node)->m_key = key;
+ _p(node)->m_val.clear();
+}
+
+void Tree::to_seq(size_t node, type_bits more_flags)
+{
+ _RYML_CB_ASSERT(m_callbacks, ! has_children(node));
+ _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || parent_is_seq(node));
+ _set_flags(node, SEQ|more_flags);
+ _p(node)->m_key.clear();
+ _p(node)->m_val.clear();
+}
+
+void Tree::to_seq(size_t node, csubstr key, type_bits more_flags)
+{
+ _RYML_CB_ASSERT(m_callbacks, ! has_children(node));
+ _RYML_CB_ASSERT(m_callbacks, parent(node) == NONE || parent_is_map(node));
+ _set_flags(node, KEY|SEQ|more_flags);
+ _p(node)->m_key = key;
+ _p(node)->m_val.clear();
+}
+
+void Tree::to_doc(size_t node, type_bits more_flags)
+{
+ _RYML_CB_ASSERT(m_callbacks, ! has_children(node));
+ _set_flags(node, DOC|more_flags);
+ _p(node)->m_key.clear();
+ _p(node)->m_val.clear();
+}
+
+void Tree::to_stream(size_t node, type_bits more_flags)
+{
+ _RYML_CB_ASSERT(m_callbacks, ! has_children(node));
+ _set_flags(node, STREAM|more_flags);
+ _p(node)->m_key.clear();
+ _p(node)->m_val.clear();
+}
+
+
+//-----------------------------------------------------------------------------
+size_t Tree::num_tag_directives() const
+{
+ // this assumes we have a very small number of tag directives
+ for(size_t i = 0; i < RYML_MAX_TAG_DIRECTIVES; ++i)
+ if(m_tag_directives[i].handle.empty())
+ return i;
+ return RYML_MAX_TAG_DIRECTIVES;
+}
+
+void Tree::clear_tag_directives()
+{
+ for(TagDirective &td : m_tag_directives)
+ td = {};
+}
+
+size_t Tree::add_tag_directive(TagDirective const& td)
+{
+ _RYML_CB_CHECK(m_callbacks, !td.handle.empty());
+ _RYML_CB_CHECK(m_callbacks, !td.prefix.empty());
+ _RYML_CB_ASSERT(m_callbacks, td.handle.begins_with('!'));
+ _RYML_CB_ASSERT(m_callbacks, td.handle.ends_with('!'));
+ // https://yaml.org/spec/1.2.2/#rule-ns-word-char
+ _RYML_CB_ASSERT(m_callbacks, td.handle == '!' || td.handle == "!!" || td.handle.trim('!').first_not_of("01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-") == npos);
+ size_t pos = num_tag_directives();
+ _RYML_CB_CHECK(m_callbacks, pos < RYML_MAX_TAG_DIRECTIVES);
+ m_tag_directives[pos] = td;
+ return pos;
+}
+
+size_t Tree::resolve_tag(substr output, csubstr tag, size_t node_id) const
+{
+ // lookup from the end. We want to find the first directive that
+ // matches the tag and has a target node id leq than the given
+ // node_id.
+ for(size_t i = RYML_MAX_TAG_DIRECTIVES-1; i != (size_t)-1; --i)
+ {
+ auto const& td = m_tag_directives[i];
+ if(td.handle.empty())
+ continue;
+ if(tag.begins_with(td.handle) && td.next_node_id <= node_id)
+ {
+ _RYML_CB_ASSERT(m_callbacks, tag.len >= td.handle.len);
+ csubstr rest = tag.sub(td.handle.len);
+ size_t len = 1u + td.prefix.len + rest.len + 1u;
+ size_t numpc = rest.count('%');
+ if(numpc == 0)
+ {
+ if(len <= output.len)
+ {
+ output.str[0] = '<';
+ memcpy(1u + output.str, td.prefix.str, td.prefix.len);
+ memcpy(1u + output.str + td.prefix.len, rest.str, rest.len);
+ output.str[1u + td.prefix.len + rest.len] = '>';
+ }
+ }
+ else
+ {
+ // need to decode URI % sequences
+ size_t pos = rest.find('%');
+ _RYML_CB_ASSERT(m_callbacks, pos != npos);
+ do {
+ size_t next = rest.first_not_of("0123456789abcdefABCDEF", pos+1);
+ if(next == npos)
+ next = rest.len;
+ _RYML_CB_CHECK(m_callbacks, pos+1 < next);
+ _RYML_CB_CHECK(m_callbacks, pos+1 + 2 <= next);
+ size_t delta = next - (pos+1);
+ len -= delta;
+ pos = rest.find('%', pos+1);
+ } while(pos != npos);
+ if(len <= output.len)
+ {
+ size_t prev = 0, wpos = 0;
+ auto appendstr = [&](csubstr s) { memcpy(output.str + wpos, s.str, s.len); wpos += s.len; };
+ auto appendchar = [&](char c) { output.str[wpos++] = c; };
+ appendchar('<');
+ appendstr(td.prefix);
+ pos = rest.find('%');
+ _RYML_CB_ASSERT(m_callbacks, pos != npos);
+ do {
+ size_t next = rest.first_not_of("0123456789abcdefABCDEF", pos+1);
+ if(next == npos)
+ next = rest.len;
+ _RYML_CB_CHECK(m_callbacks, pos+1 < next);
+ _RYML_CB_CHECK(m_callbacks, pos+1 + 2 <= next);
+ uint8_t val;
+ if(C4_UNLIKELY(!read_hex(rest.range(pos+1, next), &val) || val > 127))
+ _RYML_CB_ERR(m_callbacks, "invalid URI character");
+ appendstr(rest.range(prev, pos));
+ appendchar((char)val);
+ prev = next;
+ pos = rest.find('%', pos+1);
+ } while(pos != npos);
+ _RYML_CB_ASSERT(m_callbacks, pos == npos);
+ _RYML_CB_ASSERT(m_callbacks, prev > 0);
+ _RYML_CB_ASSERT(m_callbacks, rest.len >= prev);
+ appendstr(rest.sub(prev));
+ appendchar('>');
+ _RYML_CB_ASSERT(m_callbacks, wpos == len);
+ }
+ }
+ return len;
+ }
+ }
+ return 0; // return 0 to signal that the tag is local and cannot be resolved
+}
+
+namespace {
+csubstr _transform_tag(Tree *t, csubstr tag, size_t node)
+{
+ size_t required_size = t->resolve_tag(substr{}, tag, node);
+ if(!required_size)
+ return tag;
+ const char *prev_arena = t->arena().str;
+ substr buf = t->alloc_arena(required_size);
+ _RYML_CB_ASSERT(t->m_callbacks, t->arena().str == prev_arena);
+ size_t actual_size = t->resolve_tag(buf, tag, node);
+ _RYML_CB_ASSERT(t->m_callbacks, actual_size <= required_size);
+ return buf.first(actual_size);
+}
+void _resolve_tags(Tree *t, size_t node)
+{
+ for(size_t child = t->first_child(node); child != NONE; child = t->next_sibling(child))
+ {
+ if(t->has_key(child) && t->has_key_tag(child))
+ t->set_key_tag(child, _transform_tag(t, t->key_tag(child), child));
+ if(t->has_val(child) && t->has_val_tag(child))
+ t->set_val_tag(child, _transform_tag(t, t->val_tag(child), child));
+ _resolve_tags(t, child);
+ }
+}
+size_t _count_resolved_tags_size(Tree const* t, size_t node)
+{
+ size_t sz = 0;
+ for(size_t child = t->first_child(node); child != NONE; child = t->next_sibling(child))
+ {
+ if(t->has_key(child) && t->has_key_tag(child))
+ sz += t->resolve_tag(substr{}, t->key_tag(child), child);
+ if(t->has_val(child) && t->has_val_tag(child))
+ sz += t->resolve_tag(substr{}, t->val_tag(child), child);
+ sz += _count_resolved_tags_size(t, child);
+ }
+ return sz;
+}
+} // namespace
+
+void Tree::resolve_tags()
+{
+ if(empty())
+ return;
+ if(num_tag_directives() == 0)
+ return;
+ size_t needed_size = _count_resolved_tags_size(this, root_id());
+ if(needed_size)
+ reserve_arena(arena_pos() + needed_size);
+ _resolve_tags(this, root_id());
+}
+
+
+//-----------------------------------------------------------------------------
+
+csubstr Tree::lookup_result::resolved() const
+{
+ csubstr p = path.first(path_pos);
+ if(p.ends_with('.'))
+ p = p.first(p.len-1);
+ return p;
+}
+
+csubstr Tree::lookup_result::unresolved() const
+{
+ return path.sub(path_pos);
+}
+
+void Tree::_advance(lookup_result *r, size_t more) const
+{
+ r->path_pos += more;
+ if(r->path.sub(r->path_pos).begins_with('.'))
+ ++r->path_pos;
+}
+
+Tree::lookup_result Tree::lookup_path(csubstr path, size_t start) const
+{
+ if(start == NONE)
+ start = root_id();
+ lookup_result r(path, start);
+ if(path.empty())
+ return r;
+ _lookup_path(&r);
+ if(r.target == NONE && r.closest == start)
+ r.closest = NONE;
+ return r;
+}
+
+size_t Tree::lookup_path_or_modify(csubstr default_value, csubstr path, size_t start)
+{
+ size_t target = _lookup_path_or_create(path, start);
+ if(parent_is_map(target))
+ to_keyval(target, key(target), default_value);
+ else
+ to_val(target, default_value);
+ return target;
+}
+
+size_t Tree::lookup_path_or_modify(Tree const *src, size_t src_node, csubstr path, size_t start)
+{
+ size_t target = _lookup_path_or_create(path, start);
+ merge_with(src, src_node, target);
+ return target;
+}
+
+size_t Tree::_lookup_path_or_create(csubstr path, size_t start)
+{
+ if(start == NONE)
+ start = root_id();
+ lookup_result r(path, start);
+ _lookup_path(&r);
+ if(r.target != NONE)
+ {
+ C4_ASSERT(r.unresolved().empty());
+ return r.target;
+ }
+ _lookup_path_modify(&r);
+ return r.target;
+}
+
+void Tree::_lookup_path(lookup_result *r) const
+{
+ C4_ASSERT( ! r->unresolved().empty());
+ _lookup_path_token parent{"", type(r->closest)};
+ size_t node;
+ do
+ {
+ node = _next_node(r, &parent);
+ if(node != NONE)
+ r->closest = node;
+ if(r->unresolved().empty())
+ {
+ r->target = node;
+ return;
+ }
+ } while(node != NONE);
+}
+
+void Tree::_lookup_path_modify(lookup_result *r)
+{
+ C4_ASSERT( ! r->unresolved().empty());
+ _lookup_path_token parent{"", type(r->closest)};
+ size_t node;
+ do
+ {
+ node = _next_node_modify(r, &parent);
+ if(node != NONE)
+ r->closest = node;
+ if(r->unresolved().empty())
+ {
+ r->target = node;
+ return;
+ }
+ } while(node != NONE);
+}
+
+size_t Tree::_next_node(lookup_result * r, _lookup_path_token *parent) const
+{
+ _lookup_path_token token = _next_token(r, *parent);
+ if( ! token)
+ return NONE;
+
+ size_t node = NONE;
+ csubstr prev = token.value;
+ if(token.type == MAP || token.type == SEQ)
+ {
+ _RYML_CB_ASSERT(m_callbacks, !token.value.begins_with('['));
+ //_RYML_CB_ASSERT(m_callbacks, is_container(r->closest) || r->closest == NONE);
+ _RYML_CB_ASSERT(m_callbacks, is_map(r->closest));
+ node = find_child(r->closest, token.value);
+ }
+ else if(token.type == KEYVAL)
+ {
+ _RYML_CB_ASSERT(m_callbacks, r->unresolved().empty());
+ if(is_map(r->closest))
+ node = find_child(r->closest, token.value);
+ }
+ else if(token.type == KEY)
+ {
+ _RYML_CB_ASSERT(m_callbacks, token.value.begins_with('[') && token.value.ends_with(']'));
+ token.value = token.value.offs(1, 1).trim(' ');
+ size_t idx = 0;
+ _RYML_CB_CHECK(m_callbacks, from_chars(token.value, &idx));
+ node = child(r->closest, idx);
+ }
+ else
+ {
+ C4_NEVER_REACH();
+ }
+
+ if(node != NONE)
+ {
+ *parent = token;
+ }
+ else
+ {
+ csubstr p = r->path.sub(r->path_pos > 0 ? r->path_pos - 1 : r->path_pos);
+ r->path_pos -= prev.len;
+ if(p.begins_with('.'))
+ r->path_pos -= 1u;
+ }
+
+ return node;
+}
+
+size_t Tree::_next_node_modify(lookup_result * r, _lookup_path_token *parent)
+{
+ _lookup_path_token token = _next_token(r, *parent);
+ if( ! token)
+ return NONE;
+
+ size_t node = NONE;
+ if(token.type == MAP || token.type == SEQ)
+ {
+ _RYML_CB_ASSERT(m_callbacks, !token.value.begins_with('['));
+ //_RYML_CB_ASSERT(m_callbacks, is_container(r->closest) || r->closest == NONE);
+ if( ! is_container(r->closest))
+ {
+ if(has_key(r->closest))
+ to_map(r->closest, key(r->closest));
+ else
+ to_map(r->closest);
+ }
+ else
+ {
+ if(is_map(r->closest))
+ node = find_child(r->closest, token.value);
+ else
+ {
+ size_t pos = NONE;
+ _RYML_CB_CHECK(m_callbacks, c4::atox(token.value, &pos));
+ _RYML_CB_ASSERT(m_callbacks, pos != NONE);
+ node = child(r->closest, pos);
+ }
+ }
+ if(node == NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, is_map(r->closest));
+ node = append_child(r->closest);
+ NodeData *n = _p(node);
+ n->m_key.scalar = token.value;
+ n->m_type.add(KEY);
+ }
+ }
+ else if(token.type == KEYVAL)
+ {
+ _RYML_CB_ASSERT(m_callbacks, r->unresolved().empty());
+ if(is_map(r->closest))
+ {
+ node = find_child(r->closest, token.value);
+ if(node == NONE)
+ node = append_child(r->closest);
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_callbacks, !is_seq(r->closest));
+ _add_flags(r->closest, MAP);
+ node = append_child(r->closest);
+ }
+ NodeData *n = _p(node);
+ n->m_key.scalar = token.value;
+ n->m_val.scalar = "";
+ n->m_type.add(KEYVAL);
+ }
+ else if(token.type == KEY)
+ {
+ _RYML_CB_ASSERT(m_callbacks, token.value.begins_with('[') && token.value.ends_with(']'));
+ token.value = token.value.offs(1, 1).trim(' ');
+ size_t idx;
+ if( ! from_chars(token.value, &idx))
+ return NONE;
+ if( ! is_container(r->closest))
+ {
+ if(has_key(r->closest))
+ {
+ csubstr k = key(r->closest);
+ _clear_type(r->closest);
+ to_seq(r->closest, k);
+ }
+ else
+ {
+ _clear_type(r->closest);
+ to_seq(r->closest);
+ }
+ }
+ _RYML_CB_ASSERT(m_callbacks, is_container(r->closest));
+ node = child(r->closest, idx);
+ if(node == NONE)
+ {
+ _RYML_CB_ASSERT(m_callbacks, num_children(r->closest) <= idx);
+ for(size_t i = num_children(r->closest); i <= idx; ++i)
+ {
+ node = append_child(r->closest);
+ if(i < idx)
+ {
+ if(is_map(r->closest))
+ to_keyval(node, /*"~"*/{}, /*"~"*/{});
+ else if(is_seq(r->closest))
+ to_val(node, /*"~"*/{});
+ }
+ }
+ }
+ }
+ else
+ {
+ C4_NEVER_REACH();
+ }
+
+ _RYML_CB_ASSERT(m_callbacks, node != NONE);
+ *parent = token;
+ return node;
+}
+
+/** types of tokens:
+ * - seeing "map." ---> "map"/MAP
+ * - finishing "scalar" ---> "scalar"/KEYVAL
+ * - seeing "seq[n]" ---> "seq"/SEQ (--> "[n]"/KEY)
+ * - seeing "[n]" ---> "[n]"/KEY
+ */
+Tree::_lookup_path_token Tree::_next_token(lookup_result *r, _lookup_path_token const& parent) const
+{
+ csubstr unres = r->unresolved();
+ if(unres.empty())
+ return {};
+
+ // is it an indexation like [0], [1], etc?
+ if(unres.begins_with('['))
+ {
+ size_t pos = unres.find(']');
+ if(pos == csubstr::npos)
+ return {};
+ csubstr idx = unres.first(pos + 1);
+ _advance(r, pos + 1);
+ return {idx, KEY};
+ }
+
+ // no. so it must be a name
+ size_t pos = unres.first_of(".[");
+ if(pos == csubstr::npos)
+ {
+ _advance(r, unres.len);
+ NodeType t;
+ if(( ! parent) || parent.type.is_seq())
+ return {unres, VAL};
+ return {unres, KEYVAL};
+ }
+
+ // it's either a map or a seq
+ _RYML_CB_ASSERT(m_callbacks, unres[pos] == '.' || unres[pos] == '[');
+ if(unres[pos] == '.')
+ {
+ _RYML_CB_ASSERT(m_callbacks, pos != 0);
+ _advance(r, pos + 1);
+ return {unres.first(pos), MAP};
+ }
+
+ _RYML_CB_ASSERT(m_callbacks, unres[pos] == '[');
+ _advance(r, pos);
+ return {unres.first(pos), SEQ};
+}
+
+
+} // namespace ryml
+} // namespace c4
+
+
+C4_SUPPRESS_WARNING_GCC_POP
+C4_SUPPRESS_WARNING_MSVC_POP
+
+#endif /* RYML_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/tree.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/parse.cpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/parse.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef RYML_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/parse.hpp
+//#include "c4/yml/parse.hpp"
+#if !defined(C4_YML_PARSE_HPP_) && !defined(_C4_YML_PARSE_HPP_)
+#error "amalgamate: file c4/yml/parse.hpp must have been included at this point"
+#endif /* C4_YML_PARSE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/error.hpp
+//#include "c4/error.hpp"
+#if !defined(C4_ERROR_HPP_) && !defined(_C4_ERROR_HPP_)
+#error "amalgamate: file c4/error.hpp must have been included at this point"
+#endif /* C4_ERROR_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/utf.hpp
+//#include "c4/utf.hpp"
+#if !defined(C4_UTF_HPP_) && !defined(_C4_UTF_HPP_)
+#error "amalgamate: file c4/utf.hpp must have been included at this point"
+#endif /* C4_UTF_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/dump.hpp
+//#include <c4/dump.hpp>
+#if !defined(C4_DUMP_HPP_) && !defined(_C4_DUMP_HPP_)
+#error "amalgamate: file c4/dump.hpp must have been included at this point"
+#endif /* C4_DUMP_HPP_ */
+
+
+//included above:
+//#include <ctype.h>
+//included above:
+//#include <stdarg.h>
+//included above:
+//#include <stdio.h>
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/parser_dbg.hpp
+//#include "c4/yml/detail/parser_dbg.hpp"
+#if !defined(C4_YML_DETAIL_PARSER_DBG_HPP_) && !defined(_C4_YML_DETAIL_PARSER_DBG_HPP_)
+#error "amalgamate: file c4/yml/detail/parser_dbg.hpp must have been included at this point"
+#endif /* C4_YML_DETAIL_PARSER_DBG_HPP_ */
+
+#ifdef RYML_DBG
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/print.hpp
+//#include "c4/yml/detail/print.hpp"
+#if !defined(C4_YML_DETAIL_PRINT_HPP_) && !defined(_C4_YML_DETAIL_PRINT_HPP_)
+#error "amalgamate: file c4/yml/detail/print.hpp must have been included at this point"
+#endif /* C4_YML_DETAIL_PRINT_HPP_ */
+
+#endif
+
+#ifndef RYML_ERRMSG_SIZE
+ #define RYML_ERRMSG_SIZE 1024
+#endif
+
+//#define RYML_WITH_TAB_TOKENS
+#ifdef RYML_WITH_TAB_TOKENS
+#define _RYML_WITH_TAB_TOKENS(...) __VA_ARGS__
+#define _RYML_WITH_OR_WITHOUT_TAB_TOKENS(with, without) with
+#else
+#define _RYML_WITH_TAB_TOKENS(...)
+#define _RYML_WITH_OR_WITHOUT_TAB_TOKENS(with, without) without
+#endif
+
+
+#if defined(_MSC_VER)
+# pragma warning(push)
+# pragma warning(disable: 4296/*expression is always 'boolean_value'*/)
+#elif defined(__clang__)
+# pragma clang diagnostic push
+# pragma clang diagnostic ignored "-Wtype-limits" // to remove a warning on an assertion that a size_t >= 0. Later on, this size_t will turn into a template argument, and then it can become < 0.
+# pragma clang diagnostic ignored "-Wformat-nonliteral"
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wtype-limits" // to remove a warning on an assertion that a size_t >= 0. Later on, this size_t will turn into a template argument, and then it can become < 0.
+# pragma GCC diagnostic ignored "-Wformat-nonliteral"
+# if __GNUC__ >= 7
+# pragma GCC diagnostic ignored "-Wduplicated-branches"
+# endif
+#endif
+
+namespace c4 {
+namespace yml {
+
+namespace {
+
+template<class DumpFn, class ...Args>
+void _parse_dump(DumpFn dumpfn, c4::csubstr fmt, Args&& ...args)
+{
+ char writebuf[256];
+ auto results = c4::format_dump_resume(dumpfn, writebuf, fmt, std::forward<Args>(args)...);
+ // resume writing if the results failed to fit the buffer
+ if(C4_UNLIKELY(results.bufsize > sizeof(writebuf))) // bufsize will be that of the largest element serialized. Eg int(1), will require 1 byte.
+ {
+ results = format_dump_resume(dumpfn, results, writebuf, fmt, std::forward<Args>(args)...);
+ if(C4_UNLIKELY(results.bufsize > sizeof(writebuf)))
+ {
+ results = format_dump_resume(dumpfn, results, writebuf, fmt, std::forward<Args>(args)...);
+ }
+ }
+}
+
+bool _is_scalar_next__runk(csubstr s)
+{
+ return !(s.begins_with(": ") || s.begins_with_any("#,:{}[]%&") || s.begins_with("? ") || s == "-" || s.begins_with("- "));
+}
+
+bool _is_scalar_next__rseq_rval(csubstr s)
+{
+ return !(s.begins_with_any("[{!&") || s.begins_with("? ") || s.begins_with("- ") || s == "-");
+}
+
+bool _is_scalar_next__rmap(csubstr s)
+{
+ return !(s.begins_with(": ") || s.begins_with_any("#,!&") || s.begins_with("? ") _RYML_WITH_TAB_TOKENS(|| s.begins_with(":\t")));
+}
+
+bool _is_scalar_next__rmap_val(csubstr s)
+{
+ return !(s.begins_with("- ") || s.begins_with_any("{[") || s == "-");
+}
+
+bool _is_doc_sep(csubstr s)
+{
+ constexpr const csubstr dashes = "---";
+ constexpr const csubstr ellipsis = "...";
+ constexpr const csubstr whitesp = " \t";
+ if(s.begins_with(dashes))
+ return s == dashes || s.sub(3).begins_with_any(whitesp);
+ else if(s.begins_with(ellipsis))
+ return s == ellipsis || s.sub(3).begins_with_any(whitesp);
+ return false;
+}
+
+/** @p i is set to the first non whitespace character after the line
+ * @return the number of empty lines after the initial position */
+size_t count_following_newlines(csubstr r, size_t *C4_RESTRICT i, size_t indentation)
+{
+ RYML_ASSERT(r[*i] == '\n');
+ size_t numnl_following = 0;
+ ++(*i);
+ for( ; *i < r.len; ++(*i))
+ {
+ if(r.str[*i] == '\n')
+ {
+ ++numnl_following;
+ if(indentation) // skip the indentation after the newline
+ {
+ size_t stop = *i + indentation;
+ for( ; *i < r.len; ++(*i))
+ {
+ if(r.str[*i] != ' ' && r.str[*i] != '\r')
+ break;
+ RYML_ASSERT(*i < stop);
+ }
+ C4_UNUSED(stop);
+ }
+ }
+ else if(r.str[*i] == ' ' || r.str[*i] == '\t' || r.str[*i] == '\r') // skip leading whitespace
+ ;
+ else
+ break;
+ }
+ return numnl_following;
+}
+
+} // anon namespace
+
+
+//-----------------------------------------------------------------------------
+
+Parser::~Parser()
+{
+ _free();
+ _clr();
+}
+
+Parser::Parser(Callbacks const& cb)
+ : m_file()
+ , m_buf()
+ , m_root_id(NONE)
+ , m_tree()
+ , m_stack(cb)
+ , m_state()
+ , m_key_tag_indentation(0)
+ , m_key_tag2_indentation(0)
+ , m_key_tag()
+ , m_key_tag2()
+ , m_val_tag_indentation(0)
+ , m_val_tag()
+ , m_key_anchor_was_before(false)
+ , m_key_anchor_indentation(0)
+ , m_key_anchor()
+ , m_val_anchor_indentation(0)
+ , m_val_anchor()
+ , m_filter_arena()
+ , m_newline_offsets()
+ , m_newline_offsets_size(0)
+ , m_newline_offsets_capacity(0)
+ , m_newline_offsets_buf()
+{
+ m_stack.push(State{});
+ m_state = &m_stack.top();
+}
+
+Parser::Parser(Parser &&that)
+ : m_file(that.m_file)
+ , m_buf(that.m_buf)
+ , m_root_id(that.m_root_id)
+ , m_tree(that.m_tree)
+ , m_stack(std::move(that.m_stack))
+ , m_state(&m_stack.top())
+ , m_key_tag_indentation(that.m_key_tag_indentation)
+ , m_key_tag2_indentation(that.m_key_tag2_indentation)
+ , m_key_tag(that.m_key_tag)
+ , m_key_tag2(that.m_key_tag2)
+ , m_val_tag_indentation(that.m_val_tag_indentation)
+ , m_val_tag(that.m_val_tag)
+ , m_key_anchor_was_before(that.m_key_anchor_was_before)
+ , m_key_anchor_indentation(that.m_key_anchor_indentation)
+ , m_key_anchor(that.m_key_anchor)
+ , m_val_anchor_indentation(that.m_val_anchor_indentation)
+ , m_val_anchor(that.m_val_anchor)
+ , m_filter_arena(that.m_filter_arena)
+ , m_newline_offsets(that.m_newline_offsets)
+ , m_newline_offsets_size(that.m_newline_offsets_size)
+ , m_newline_offsets_capacity(that.m_newline_offsets_capacity)
+ , m_newline_offsets_buf(that.m_newline_offsets_buf)
+{
+ that._clr();
+}
+
+Parser::Parser(Parser const& that)
+ : m_file(that.m_file)
+ , m_buf(that.m_buf)
+ , m_root_id(that.m_root_id)
+ , m_tree(that.m_tree)
+ , m_stack(that.m_stack)
+ , m_state(&m_stack.top())
+ , m_key_tag_indentation(that.m_key_tag_indentation)
+ , m_key_tag2_indentation(that.m_key_tag2_indentation)
+ , m_key_tag(that.m_key_tag)
+ , m_key_tag2(that.m_key_tag2)
+ , m_val_tag_indentation(that.m_val_tag_indentation)
+ , m_val_tag(that.m_val_tag)
+ , m_key_anchor_was_before(that.m_key_anchor_was_before)
+ , m_key_anchor_indentation(that.m_key_anchor_indentation)
+ , m_key_anchor(that.m_key_anchor)
+ , m_val_anchor_indentation(that.m_val_anchor_indentation)
+ , m_val_anchor(that.m_val_anchor)
+ , m_filter_arena()
+ , m_newline_offsets()
+ , m_newline_offsets_size()
+ , m_newline_offsets_capacity()
+ , m_newline_offsets_buf()
+{
+ if(that.m_newline_offsets_capacity)
+ {
+ _resize_locations(that.m_newline_offsets_capacity);
+ _RYML_CB_CHECK(m_stack.m_callbacks, m_newline_offsets_capacity == that.m_newline_offsets_capacity);
+ memcpy(m_newline_offsets, that.m_newline_offsets, that.m_newline_offsets_size * sizeof(size_t));
+ m_newline_offsets_size = that.m_newline_offsets_size;
+ }
+ if(that.m_filter_arena.len)
+ {
+ _resize_filter_arena(that.m_filter_arena.len);
+ }
+}
+
+Parser& Parser::operator=(Parser &&that)
+{
+ _free();
+ m_file = (that.m_file);
+ m_buf = (that.m_buf);
+ m_root_id = (that.m_root_id);
+ m_tree = (that.m_tree);
+ m_stack = std::move(that.m_stack);
+ m_state = (&m_stack.top());
+ m_key_tag_indentation = (that.m_key_tag_indentation);
+ m_key_tag2_indentation = (that.m_key_tag2_indentation);
+ m_key_tag = (that.m_key_tag);
+ m_key_tag2 = (that.m_key_tag2);
+ m_val_tag_indentation = (that.m_val_tag_indentation);
+ m_val_tag = (that.m_val_tag);
+ m_key_anchor_was_before = (that.m_key_anchor_was_before);
+ m_key_anchor_indentation = (that.m_key_anchor_indentation);
+ m_key_anchor = (that.m_key_anchor);
+ m_val_anchor_indentation = (that.m_val_anchor_indentation);
+ m_val_anchor = (that.m_val_anchor);
+ m_filter_arena = that.m_filter_arena;
+ m_newline_offsets = (that.m_newline_offsets);
+ m_newline_offsets_size = (that.m_newline_offsets_size);
+ m_newline_offsets_capacity = (that.m_newline_offsets_capacity);
+ m_newline_offsets_buf = (that.m_newline_offsets_buf);
+ that._clr();
+ return *this;
+}
+
+Parser& Parser::operator=(Parser const& that)
+{
+ _free();
+ m_file = (that.m_file);
+ m_buf = (that.m_buf);
+ m_root_id = (that.m_root_id);
+ m_tree = (that.m_tree);
+ m_stack = that.m_stack;
+ m_state = &m_stack.top();
+ m_key_tag_indentation = (that.m_key_tag_indentation);
+ m_key_tag2_indentation = (that.m_key_tag2_indentation);
+ m_key_tag = (that.m_key_tag);
+ m_key_tag2 = (that.m_key_tag2);
+ m_val_tag_indentation = (that.m_val_tag_indentation);
+ m_val_tag = (that.m_val_tag);
+ m_key_anchor_was_before = (that.m_key_anchor_was_before);
+ m_key_anchor_indentation = (that.m_key_anchor_indentation);
+ m_key_anchor = (that.m_key_anchor);
+ m_val_anchor_indentation = (that.m_val_anchor_indentation);
+ m_val_anchor = (that.m_val_anchor);
+ if(that.m_filter_arena.len > 0)
+ _resize_filter_arena(that.m_filter_arena.len);
+ if(that.m_newline_offsets_capacity > m_newline_offsets_capacity)
+ _resize_locations(that.m_newline_offsets_capacity);
+ _RYML_CB_CHECK(m_stack.m_callbacks, m_newline_offsets_capacity >= that.m_newline_offsets_capacity);
+ _RYML_CB_CHECK(m_stack.m_callbacks, m_newline_offsets_capacity >= that.m_newline_offsets_size);
+ memcpy(m_newline_offsets, that.m_newline_offsets, that.m_newline_offsets_size * sizeof(size_t));
+ m_newline_offsets_size = that.m_newline_offsets_size;
+ m_newline_offsets_buf = that.m_newline_offsets_buf;
+ return *this;
+}
+
+void Parser::_clr()
+{
+ m_file = {};
+ m_buf = {};
+ m_root_id = {};
+ m_tree = {};
+ m_stack.clear();
+ m_state = {};
+ m_key_tag_indentation = {};
+ m_key_tag2_indentation = {};
+ m_key_tag = {};
+ m_key_tag2 = {};
+ m_val_tag_indentation = {};
+ m_val_tag = {};
+ m_key_anchor_was_before = {};
+ m_key_anchor_indentation = {};
+ m_key_anchor = {};
+ m_val_anchor_indentation = {};
+ m_val_anchor = {};
+ m_filter_arena = {};
+ m_newline_offsets = {};
+ m_newline_offsets_size = {};
+ m_newline_offsets_capacity = {};
+ m_newline_offsets_buf = {};
+}
+
+void Parser::_free()
+{
+ if(m_newline_offsets)
+ {
+ _RYML_CB_FREE(m_stack.m_callbacks, m_newline_offsets, size_t, m_newline_offsets_capacity);
+ m_newline_offsets = nullptr;
+ m_newline_offsets_size = 0u;
+ m_newline_offsets_capacity = 0u;
+ m_newline_offsets_buf = 0u;
+ }
+ if(m_filter_arena.len)
+ {
+ _RYML_CB_FREE(m_stack.m_callbacks, m_filter_arena.str, char, m_filter_arena.len);
+ m_filter_arena = {};
+ }
+ m_stack._free();
+}
+
+
+//-----------------------------------------------------------------------------
+void Parser::_reset()
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_stack.size() == 1);
+ m_stack.clear();
+ m_stack.push({});
+ m_state = &m_stack.top();
+ m_state->reset(m_file.str, m_root_id);
+
+ m_key_tag_indentation = 0;
+ m_key_tag2_indentation = 0;
+ m_key_tag.clear();
+ m_key_tag2.clear();
+ m_val_tag_indentation = 0;
+ m_val_tag.clear();
+ m_key_anchor_was_before = false;
+ m_key_anchor_indentation = 0;
+ m_key_anchor.clear();
+ m_val_anchor_indentation = 0;
+ m_val_anchor.clear();
+
+ _mark_locations_dirty();
+}
+
+//-----------------------------------------------------------------------------
+template<class DumpFn>
+void Parser::_fmt_msg(DumpFn &&dumpfn) const
+{
+ auto const& lc = m_state->line_contents;
+ csubstr contents = lc.stripped;
+ if(contents.len)
+ {
+ // print the yaml src line
+ size_t offs = 3u + to_chars(substr{}, m_state->pos.line) + to_chars(substr{}, m_state->pos.col);
+ if(m_file.len)
+ {
+ _parse_dump(dumpfn, "{}:", m_file);
+ offs += m_file.len + 1;
+ }
+ _parse_dump(dumpfn, "{}:{}: ", m_state->pos.line, m_state->pos.col);
+ csubstr maybe_full_content = (contents.len < 80u ? contents : contents.first(80u));
+ csubstr maybe_ellipsis = (contents.len < 80u ? csubstr{} : csubstr("..."));
+ _parse_dump(dumpfn, "{}{} (size={})\n", maybe_full_content, maybe_ellipsis, contents.len);
+ // highlight the remaining portion of the previous line
+ size_t firstcol = (size_t)(lc.rem.begin() - lc.full.begin());
+ size_t lastcol = firstcol + lc.rem.len;
+ for(size_t i = 0; i < offs + firstcol; ++i)
+ dumpfn(" ");
+ dumpfn("^");
+ for(size_t i = 1, e = (lc.rem.len < 80u ? lc.rem.len : 80u); i < e; ++i)
+ dumpfn("~");
+ _parse_dump(dumpfn, "{} (cols {}-{})\n", maybe_ellipsis, firstcol+1, lastcol+1);
+ }
+ else
+ {
+ dumpfn("\n");
+ }
+
+#ifdef RYML_DBG
+ // next line: print the state flags
+ {
+ char flagbuf_[64];
+ _parse_dump(dumpfn, "top state: {}\n", _prfl(flagbuf_, m_state->flags));
+ }
+#endif
+}
+
+
+//-----------------------------------------------------------------------------
+template<class ...Args>
+void Parser::_err(csubstr fmt, Args const& C4_RESTRICT ...args) const
+{
+ char errmsg[RYML_ERRMSG_SIZE];
+ detail::_SubstrWriter writer(errmsg);
+ auto dumpfn = [&writer](csubstr s){ writer.append(s); };
+ _parse_dump(dumpfn, fmt, args...);
+ writer.append('\n');
+ _fmt_msg(dumpfn);
+ size_t len = writer.pos < RYML_ERRMSG_SIZE ? writer.pos : RYML_ERRMSG_SIZE;
+ m_tree->m_callbacks.m_error(errmsg, len, m_state->pos, m_tree->m_callbacks.m_user_data);
+}
+
+//-----------------------------------------------------------------------------
+#ifdef RYML_DBG
+template<class ...Args>
+void Parser::_dbg(csubstr fmt, Args const& C4_RESTRICT ...args) const
+{
+ auto dumpfn = [](csubstr s){ fwrite(s.str, 1, s.len, stdout); };
+ _parse_dump(dumpfn, fmt, args...);
+ dumpfn("\n");
+ _fmt_msg(dumpfn);
+}
+#endif
+
+//-----------------------------------------------------------------------------
+bool Parser::_finished_file() const
+{
+ bool ret = m_state->pos.offset >= m_buf.len;
+ if(ret)
+ {
+ _c4dbgp("finished file!!!");
+ }
+ return ret;
+}
+
+//-----------------------------------------------------------------------------
+bool Parser::_finished_line() const
+{
+ return m_state->line_contents.rem.empty();
+}
+
+//-----------------------------------------------------------------------------
+void Parser::parse_in_place(csubstr file, substr buf, Tree *t, size_t node_id)
+{
+ m_file = file;
+ m_buf = buf;
+ m_root_id = node_id;
+ m_tree = t;
+ _reset();
+ while( ! _finished_file())
+ {
+ _scan_line();
+ while( ! _finished_line())
+ _handle_line();
+ if(_finished_file())
+ break; // it may have finished because of multiline blocks
+ _line_ended();
+ }
+ _handle_finished_file();
+}
+
+//-----------------------------------------------------------------------------
+void Parser::_handle_finished_file()
+{
+ _end_stream();
+}
+
+//-----------------------------------------------------------------------------
+void Parser::_handle_line()
+{
+ _c4dbgq("\n-----------");
+ _c4dbgt("handling line={}, offset={}B", m_state->pos.line, m_state->pos.offset);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ! m_state->line_contents.rem.empty());
+ if(has_any(RSEQ))
+ {
+ if(has_any(FLOW))
+ {
+ if(_handle_seq_flow())
+ return;
+ }
+ else
+ {
+ if(_handle_seq_blck())
+ return;
+ }
+ }
+ else if(has_any(RMAP))
+ {
+ if(has_any(FLOW))
+ {
+ if(_handle_map_flow())
+ return;
+ }
+ else
+ {
+ if(_handle_map_blck())
+ return;
+ }
+ }
+ else if(has_any(RUNK))
+ {
+ if(_handle_unk())
+ return;
+ }
+
+ if(_handle_top())
+ return;
+}
+
+
+//-----------------------------------------------------------------------------
+bool Parser::_handle_unk()
+{
+ _c4dbgp("handle_unk");
+
+ csubstr rem = m_state->line_contents.rem;
+ const bool start_as_child = (node(m_state) == nullptr);
+
+ if(C4_UNLIKELY(has_any(NDOC)))
+ {
+ if(rem == "---" || rem.begins_with("--- "))
+ {
+ _start_new_doc(rem);
+ return true;
+ }
+ auto trimmed = rem.triml(' ');
+ if(trimmed == "---" || trimmed.begins_with("--- "))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, rem.len >= trimmed.len);
+ _line_progressed(rem.len - trimmed.len);
+ _start_new_doc(trimmed);
+ _save_indentation();
+ return true;
+ }
+ else if(trimmed.begins_with("..."))
+ {
+ _end_stream();
+ }
+ else if(trimmed.first_of("#%") == csubstr::npos) // neither a doc nor a tag
+ {
+ _c4dbgpf("starting implicit doc to accomodate unexpected tokens: '{}'", rem);
+ size_t indref = m_state->indref;
+ _push_level();
+ _start_doc();
+ _set_indentation(indref);
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, !trimmed.empty());
+ }
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RNXT|RSEQ|RMAP));
+ if(m_state->indref > 0)
+ {
+ csubstr ws = rem.left_of(rem.first_not_of(' '));
+ if(m_state->indref <= ws.len)
+ {
+ _c4dbgpf("skipping base indentation of {}", m_state->indref);
+ _line_progressed(m_state->indref);
+ rem = rem.sub(m_state->indref);
+ }
+ }
+
+ if(rem.begins_with("- ") _RYML_WITH_TAB_TOKENS( || rem.begins_with("-\t")))
+ {
+ _c4dbgpf("it's a seq (as_child={})", start_as_child);
+ _move_key_anchor_to_val_anchor();
+ _move_key_tag_to_val_tag();
+ _push_level();
+ _start_seq(start_as_child);
+ _save_indentation();
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem == '-')
+ {
+ _c4dbgpf("it's a seq (as_child={})", start_as_child);
+ _move_key_anchor_to_val_anchor();
+ _move_key_tag_to_val_tag();
+ _push_level();
+ _start_seq(start_as_child);
+ _save_indentation();
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('['))
+ {
+ _c4dbgpf("it's a seq, flow (as_child={})", start_as_child);
+ _move_key_anchor_to_val_anchor();
+ _move_key_tag_to_val_tag();
+ _push_level(/*explicit flow*/true);
+ _start_seq(start_as_child);
+ add_flags(FLOW);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('{'))
+ {
+ _c4dbgpf("it's a map, flow (as_child={})", start_as_child);
+ _move_key_anchor_to_val_anchor();
+ _move_key_tag_to_val_tag();
+ _push_level(/*explicit flow*/true);
+ _start_map(start_as_child);
+ addrem_flags(FLOW|RKEY, RVAL);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with("? "))
+ {
+ _c4dbgpf("it's a map (as_child={}) + this key is complex", start_as_child);
+ _move_key_anchor_to_val_anchor();
+ _move_key_tag_to_val_tag();
+ _push_level();
+ _start_map(start_as_child);
+ addrem_flags(RKEY|QMRK, RVAL);
+ _save_indentation();
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem.begins_with(": ") && !has_all(SSCL))
+ {
+ _c4dbgp("it's a map with an empty key");
+ _move_key_anchor_to_val_anchor();
+ _move_key_tag_to_val_tag();
+ _push_level();
+ _start_map(start_as_child);
+ _store_scalar_null(rem.str);
+ addrem_flags(RVAL, RKEY);
+ _save_indentation();
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem == ':' && !has_all(SSCL))
+ {
+ _c4dbgp("it's a map with an empty key");
+ _move_key_anchor_to_val_anchor();
+ _move_key_tag_to_val_tag();
+ _push_level();
+ _start_map(start_as_child);
+ _store_scalar_null(rem.str);
+ addrem_flags(RVAL, RKEY);
+ _save_indentation();
+ _line_progressed(1);
+ return true;
+ }
+ else if(_handle_types())
+ {
+ return true;
+ }
+ else if(!rem.begins_with('*') && _handle_key_anchors_and_refs())
+ {
+ return true;
+ }
+ else if(has_all(SSCL))
+ {
+ _c4dbgpf("there's a stored scalar: '{}'", m_state->scalar);
+
+ csubstr saved_scalar;
+ bool is_quoted;
+ if(_scan_scalar(&saved_scalar, &is_quoted))
+ {
+ rem = m_state->line_contents.rem;
+ _c4dbgpf("... and there's also a scalar next! '{}'", saved_scalar);
+ if(rem.begins_with_any(" \t"))
+ {
+ size_t n = rem.first_not_of(" \t");
+ _c4dbgpf("skipping {} spaces/tabs", n);
+ rem = rem.sub(n);
+ _line_progressed(n);
+ }
+ }
+
+ _c4dbgpf("rem='{}'", rem);
+
+ if(rem.begins_with(", "))
+ {
+ _c4dbgpf("got a ',' -- it's a seq (as_child={})", start_as_child);
+ _start_seq(start_as_child);
+ add_flags(FLOW);
+ _append_val(_consume_scalar());
+ _line_progressed(2);
+ }
+ else if(rem.begins_with(','))
+ {
+ _c4dbgpf("got a ',' -- it's a seq (as_child={})", start_as_child);
+ _start_seq(start_as_child);
+ add_flags(FLOW);
+ _append_val(_consume_scalar());
+ _line_progressed(1);
+ }
+ else if(rem.begins_with(": ") _RYML_WITH_TAB_TOKENS( || rem.begins_with(":\t")))
+ {
+ _c4dbgpf("got a ': ' -- it's a map (as_child={})", start_as_child);
+ _start_map_unk(start_as_child); // wait for the val scalar to append the key-val pair
+ _line_progressed(2);
+ }
+ else if(rem == ":" || rem.begins_with(":\"") || rem.begins_with(":'"))
+ {
+ if(rem == ":") { _c4dbgpf("got a ':' -- it's a map (as_child={})", start_as_child); }
+ else { _c4dbgpf("got a '{}' -- it's a map (as_child={})", rem.first(2), start_as_child); }
+ _start_map_unk(start_as_child); // wait for the val scalar to append the key-val pair
+ _line_progressed(1); // advance only 1
+ }
+ else if(rem.begins_with('}'))
+ {
+ if(!has_all(RMAP|FLOW))
+ {
+ _c4err("invalid token: not reading a map");
+ }
+ if(!has_all(SSCL))
+ {
+ _c4err("no scalar stored");
+ }
+ _append_key_val(saved_scalar);
+ _stop_map();
+ _line_progressed(1);
+ }
+ else if(rem.begins_with("..."))
+ {
+ _c4dbgp("got stream end '...'");
+ _end_stream();
+ _line_progressed(3);
+ }
+ else if(rem.begins_with('#'))
+ {
+ _c4dbgpf("it's a comment: '{}'", rem);
+ _scan_comment();
+ return true;
+ }
+ else if(_handle_key_anchors_and_refs())
+ {
+ return true;
+ }
+ else if(rem.begins_with(" ") || rem.begins_with("\t"))
+ {
+ size_t n = rem.first_not_of(" \t");
+ if(n == npos)
+ n = rem.len;
+ _c4dbgpf("has {} spaces/tabs, skip...", n);
+ _line_progressed(n);
+ return true;
+ }
+ else if(rem.empty())
+ {
+ // nothing to do
+ }
+ else if(rem == "---" || rem.begins_with("--- "))
+ {
+ _c4dbgp("caught ---: starting doc");
+ _start_new_doc(rem);
+ return true;
+ }
+ else if(rem.begins_with('%'))
+ {
+ _c4dbgp("caught a directive: ignoring...");
+ _line_progressed(rem.len);
+ return true;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+
+ if( ! saved_scalar.empty())
+ {
+ _store_scalar(saved_scalar, is_quoted);
+ }
+
+ return true;
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ! has_any(SSCL));
+ csubstr scalar;
+ size_t indentation = m_state->line_contents.indentation; // save
+ bool is_quoted;
+ if(_scan_scalar(&scalar, &is_quoted))
+ {
+ _c4dbgpf("got a {} scalar", is_quoted ? "quoted" : "");
+ rem = m_state->line_contents.rem;
+ {
+ size_t first = rem.first_not_of(" \t");
+ if(first && first != npos)
+ {
+ _c4dbgpf("skip {} whitespace characters", first);
+ _line_progressed(first);
+ rem = rem.sub(first);
+ }
+ }
+ _store_scalar(scalar, is_quoted);
+ if(rem.begins_with(": ") _RYML_WITH_TAB_TOKENS( || rem.begins_with(":\t")))
+ {
+ _c4dbgpf("got a ': ' next -- it's a map (as_child={})", start_as_child);
+ _push_level();
+ _start_map(start_as_child); // wait for the val scalar to append the key-val pair
+ _set_indentation(indentation);
+ _line_progressed(2); // call this AFTER saving the indentation
+ }
+ else if(rem == ":")
+ {
+ _c4dbgpf("got a ':' next -- it's a map (as_child={})", start_as_child);
+ _push_level();
+ _start_map(start_as_child); // wait for the val scalar to append the key-val pair
+ _set_indentation(indentation);
+ _line_progressed(1); // call this AFTER saving the indentation
+ }
+ else
+ {
+ // we still don't know whether it's a seq or a map
+ // so just store the scalar
+ }
+ return true;
+ }
+ else if(rem.begins_with_any(" \t"))
+ {
+ csubstr ws = rem.left_of(rem.first_not_of(" \t"));
+ rem = rem.right_of(ws);
+ if(has_all(RTOP) && rem.begins_with("---"))
+ {
+ _c4dbgp("there's a doc starting, and it's indented");
+ _set_indentation(ws.len);
+ }
+ _c4dbgpf("skipping {} spaces/tabs", ws.len);
+ _line_progressed(ws.len);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+C4_ALWAYS_INLINE void Parser::_skipchars(char c)
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->line_contents.rem.begins_with(c));
+ size_t pos = m_state->line_contents.rem.first_not_of(c);
+ if(pos == npos)
+ pos = m_state->line_contents.rem.len; // maybe the line is just whitespace
+ _c4dbgpf("skip {} '{}'", pos, c);
+ _line_progressed(pos);
+}
+
+template<size_t N>
+C4_ALWAYS_INLINE void Parser::_skipchars(const char (&chars)[N])
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->line_contents.rem.begins_with_any(chars));
+ size_t pos = m_state->line_contents.rem.first_not_of(chars);
+ if(pos == npos)
+ pos = m_state->line_contents.rem.len; // maybe the line is just whitespace
+ _c4dbgpf("skip {} characters", pos);
+ _line_progressed(pos);
+}
+
+
+//-----------------------------------------------------------------------------
+bool Parser::_handle_seq_flow()
+{
+ _c4dbgpf("handle_seq_flow: node_id={} level={}", m_state->node_id, m_state->level);
+ csubstr rem = m_state->line_contents.rem;
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RKEY));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(RSEQ|FLOW));
+
+ if(rem.begins_with(' '))
+ {
+ // with explicit flow, indentation does not matter
+ _c4dbgp("starts with spaces");
+ _skipchars(' ');
+ return true;
+ }
+ _RYML_WITH_TAB_TOKENS(else if(rem.begins_with('\t'))
+ {
+ _c4dbgp("starts with tabs");
+ _skipchars('\t');
+ return true;
+ })
+ else if(rem.begins_with('#'))
+ {
+ _c4dbgp("it's a comment");
+ rem = _scan_comment(); // also progresses the line
+ return true;
+ }
+ else if(rem.begins_with(']'))
+ {
+ _c4dbgp("end the sequence");
+ _pop_level();
+ _line_progressed(1);
+ if(has_all(RSEQIMAP))
+ {
+ _stop_seqimap();
+ _pop_level();
+ }
+ return true;
+ }
+
+ if(has_any(RVAL))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RNXT));
+ bool is_quoted;
+ if(_scan_scalar(&rem, &is_quoted))
+ {
+ _c4dbgp("it's a scalar");
+ addrem_flags(RNXT, RVAL);
+ _append_val(rem, is_quoted);
+ return true;
+ }
+ else if(rem.begins_with('['))
+ {
+ _c4dbgp("val is a child seq");
+ addrem_flags(RNXT, RVAL); // before _push_level!
+ _push_level(/*explicit flow*/true);
+ _start_seq();
+ add_flags(FLOW);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('{'))
+ {
+ _c4dbgp("val is a child map");
+ addrem_flags(RNXT, RVAL); // before _push_level!
+ _push_level(/*explicit flow*/true);
+ _start_map();
+ addrem_flags(FLOW|RKEY, RVAL);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem == ':')
+ {
+ _c4dbgpf("found ':' -- there's an implicit map in the seq node[{}]", m_state->node_id);
+ _start_seqimap();
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with(": ") _RYML_WITH_TAB_TOKENS( || rem.begins_with(":\t")))
+ {
+ _c4dbgpf("found ': ' -- there's an implicit map in the seq node[{}]", m_state->node_id);
+ _start_seqimap();
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem.begins_with("? "))
+ {
+ _c4dbgpf("found '? ' -- there's an implicit map in the seq node[{}]", m_state->node_id);
+ _start_seqimap();
+ _line_progressed(2);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_any(SSCL) && m_state->scalar == "");
+ addrem_flags(QMRK|RKEY, RVAL|SSCL);
+ return true;
+ }
+ else if(_handle_types())
+ {
+ return true;
+ }
+ else if(_handle_val_anchors_and_refs())
+ {
+ return true;
+ }
+ else if(rem.begins_with(", "))
+ {
+ _c4dbgp("found ',' -- the value was null");
+ _append_val_null(rem.str - 1);
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem.begins_with(','))
+ {
+ _c4dbgp("found ',' -- the value was null");
+ _append_val_null(rem.str - 1);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('\t'))
+ {
+ _skipchars('\t');
+ return true;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+ }
+ else if(has_any(RNXT))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RVAL));
+ if(rem.begins_with(", "))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(FLOW));
+ _c4dbgp("seq: expect next val");
+ addrem_flags(RVAL, RNXT);
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem.begins_with(','))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(FLOW));
+ _c4dbgp("seq: expect next val");
+ addrem_flags(RVAL, RNXT);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem == ':')
+ {
+ _c4dbgpf("found ':' -- there's an implicit map in the seq node[{}]", m_state->node_id);
+ _start_seqimap();
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with(": ") _RYML_WITH_TAB_TOKENS( || rem.begins_with(":\t")))
+ {
+ _c4dbgpf("found ': ' -- there's an implicit map in the seq node[{}]", m_state->node_id);
+ _start_seqimap();
+ _line_progressed(2);
+ return true;
+ }
+ else
+ {
+ _c4err("was expecting a comma");
+ }
+ }
+ else
+ {
+ _c4err("internal error");
+ }
+
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+bool Parser::_handle_seq_blck()
+{
+ _c4dbgpf("handle_seq_impl: node_id={} level={}", m_state->node_id, m_state->level);
+ csubstr rem = m_state->line_contents.rem;
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(RSEQ));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RKEY));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(FLOW));
+
+ if(rem.begins_with('#'))
+ {
+ _c4dbgp("it's a comment");
+ rem = _scan_comment();
+ return true;
+ }
+
+ if(has_any(RNXT))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RVAL));
+
+ if(_handle_indentation())
+ return true;
+
+ if(rem.begins_with("- ") _RYML_WITH_TAB_TOKENS( || rem.begins_with("-\t")))
+ {
+ _c4dbgp("expect another val");
+ addrem_flags(RVAL, RNXT);
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem == '-')
+ {
+ _c4dbgp("expect another val");
+ addrem_flags(RVAL, RNXT);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with_any(" \t"))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ! _at_line_begin());
+ _skipchars(" \t");
+ return true;
+ }
+ else if(rem.begins_with("..."))
+ {
+ _c4dbgp("got stream end '...'");
+ _end_stream();
+ _line_progressed(3);
+ return true;
+ }
+ else if(rem.begins_with("---"))
+ {
+ _c4dbgp("got document start '---'");
+ _start_new_doc(rem);
+ return true;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+ }
+ else if(has_any(RVAL))
+ {
+ // there can be empty values
+ if(_handle_indentation())
+ return true;
+
+ csubstr s;
+ bool is_quoted;
+ if(_scan_scalar(&s, &is_quoted)) // this also progresses the line
+ {
+ _c4dbgpf("it's a{} scalar", is_quoted ? " quoted" : "");
+
+ rem = m_state->line_contents.rem;
+ if(_RYML_WITH_OR_WITHOUT_TAB_TOKENS(rem.begins_with_any(" \t"), rem.begins_with(' ')))
+ {
+ _c4dbgp("skipping whitespace...");
+ size_t skip = rem.first_not_of(_RYML_WITH_OR_WITHOUT_TAB_TOKENS(" \t", ' '));
+ if(skip == csubstr::npos)
+ skip = rem.len; // maybe the line is just whitespace
+ _line_progressed(skip);
+ rem = rem.sub(skip);
+ }
+
+ _c4dbgpf("rem=[{}]~~~{}~~~", rem.len, rem);
+ if(!rem.begins_with('#') && (rem.ends_with(':') || rem.begins_with(": ") _RYML_WITH_TAB_TOKENS( || rem.begins_with(":\t"))))
+ {
+ _c4dbgp("actually, the scalar is the first key of a map, and it opens a new scope");
+ if(m_key_anchor.empty())
+ _move_val_anchor_to_key_anchor();
+ if(m_key_tag.empty())
+ _move_val_tag_to_key_tag();
+ addrem_flags(RNXT, RVAL); // before _push_level! This prepares the current level for popping by setting it to RNXT
+ _push_level();
+ _start_map();
+ _store_scalar(s, is_quoted);
+ if( ! _maybe_set_indentation_from_anchor_or_tag())
+ {
+ _c4dbgpf("set indentation from scalar: {}", m_state->scalar_col);
+ _set_indentation(m_state->scalar_col); // this is the column where the scalar starts
+ }
+ _move_key_tag2_to_key_tag();
+ addrem_flags(RVAL, RKEY);
+ _line_progressed(1);
+ }
+ else
+ {
+ _c4dbgp("appending val to current seq");
+ _append_val(s, is_quoted);
+ addrem_flags(RNXT, RVAL);
+ }
+ return true;
+ }
+ else if(rem.begins_with("- ") _RYML_WITH_TAB_TOKENS( || rem.begins_with("-\t")))
+ {
+ if(_rval_dash_start_or_continue_seq())
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem == '-')
+ {
+ if(_rval_dash_start_or_continue_seq())
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('['))
+ {
+ _c4dbgp("val is a child seq, flow");
+ addrem_flags(RNXT, RVAL); // before _push_level!
+ _push_level(/*explicit flow*/true);
+ _start_seq();
+ add_flags(FLOW);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('{'))
+ {
+ _c4dbgp("val is a child map, flow");
+ addrem_flags(RNXT, RVAL); // before _push_level!
+ _push_level(/*explicit flow*/true);
+ _start_map();
+ addrem_flags(FLOW|RKEY, RVAL);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with("? "))
+ {
+ _c4dbgp("val is a child map + this key is complex");
+ addrem_flags(RNXT, RVAL); // before _push_level!
+ _push_level();
+ _start_map();
+ addrem_flags(QMRK|RKEY, RVAL);
+ _save_indentation();
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem.begins_with(' '))
+ {
+ csubstr spc = rem.left_of(rem.first_not_of(' '));
+ if(_at_line_begin())
+ {
+ _c4dbgpf("skipping value indentation: {} spaces", spc.len);
+ _line_progressed(spc.len);
+ return true;
+ }
+ else
+ {
+ _c4dbgpf("skipping {} spaces", spc.len);
+ _line_progressed(spc.len);
+ return true;
+ }
+ }
+ else if(_handle_types())
+ {
+ return true;
+ }
+ else if(_handle_val_anchors_and_refs())
+ {
+ return true;
+ }
+ /* pathological case:
+ * - &key : val
+ * - &key :
+ * - : val
+ */
+ else if((!has_all(SSCL)) &&
+ (rem.begins_with(": ") || rem.left_of(rem.find("#")).trimr("\t") == ":"))
+ {
+ if(!m_val_anchor.empty() || !m_val_tag.empty())
+ {
+ _c4dbgp("val is a child map + this key is empty, with anchors or tags");
+ addrem_flags(RNXT, RVAL); // before _push_level!
+ _move_val_tag_to_key_tag();
+ _move_val_anchor_to_key_anchor();
+ _push_level();
+ _start_map();
+ _store_scalar_null(rem.str);
+ addrem_flags(RVAL, RKEY);
+ RYML_CHECK(_maybe_set_indentation_from_anchor_or_tag()); // one of them must exist
+ _line_progressed(rem.begins_with(": ") ? 2u : 1u);
+ return true;
+ }
+ else
+ {
+ _c4dbgp("val is a child map + this key is empty, no anchors or tags");
+ addrem_flags(RNXT, RVAL); // before _push_level!
+ size_t ind = m_state->indref;
+ _push_level();
+ _start_map();
+ _store_scalar_null(rem.str);
+ addrem_flags(RVAL, RKEY);
+ _c4dbgpf("set indentation from map anchor: {}", ind + 2);
+ _set_indentation(ind + 2); // this is the column where the map starts
+ _line_progressed(rem.begins_with(": ") ? 2u : 1u);
+ return true;
+ }
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+
+bool Parser::_rval_dash_start_or_continue_seq()
+{
+ size_t ind = m_state->line_contents.current_col();
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ind >= m_state->indref);
+ size_t delta_ind = ind - m_state->indref;
+ if( ! delta_ind)
+ {
+ _c4dbgp("prev val was empty");
+ addrem_flags(RNXT, RVAL);
+ _append_val_null(&m_state->line_contents.full[ind]);
+ return false;
+ }
+ _c4dbgp("val is a nested seq, indented");
+ addrem_flags(RNXT, RVAL); // before _push_level!
+ _push_level();
+ _start_seq();
+ _save_indentation();
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+bool Parser::_handle_map_flow()
+{
+ // explicit flow, ie, inside {}, separated by commas
+ _c4dbgpf("handle_map_flow: node_id={} level={}", m_state->node_id, m_state->level);
+ csubstr rem = m_state->line_contents.rem;
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(RMAP|FLOW));
+
+ if(rem.begins_with(' '))
+ {
+ // with explicit flow, indentation does not matter
+ _c4dbgp("starts with spaces");
+ _skipchars(' ');
+ return true;
+ }
+ _RYML_WITH_TAB_TOKENS(else if(rem.begins_with('\t'))
+ {
+ // with explicit flow, indentation does not matter
+ _c4dbgp("starts with tabs");
+ _skipchars('\t');
+ return true;
+ })
+ else if(rem.begins_with('#'))
+ {
+ _c4dbgp("it's a comment");
+ rem = _scan_comment(); // also progresses the line
+ return true;
+ }
+ else if(rem.begins_with('}'))
+ {
+ _c4dbgp("end the map");
+ if(has_all(SSCL))
+ {
+ _c4dbgp("the last val was null");
+ _append_key_val_null(rem.str - 1);
+ rem_flags(RVAL);
+ }
+ _pop_level();
+ _line_progressed(1);
+ if(has_all(RSEQIMAP))
+ {
+ _c4dbgp("stopping implicitly nested 1x map");
+ _stop_seqimap();
+ _pop_level();
+ }
+ return true;
+ }
+
+ if(has_any(RNXT))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RKEY));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RVAL));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RSEQIMAP));
+
+ if(rem.begins_with(", "))
+ {
+ _c4dbgp("seq: expect next keyval");
+ addrem_flags(RKEY, RNXT);
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem.begins_with(','))
+ {
+ _c4dbgp("seq: expect next keyval");
+ addrem_flags(RKEY, RNXT);
+ _line_progressed(1);
+ return true;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+ }
+ else if(has_any(RKEY))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RNXT));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RVAL));
+
+ bool is_quoted;
+ if(has_none(SSCL) && _scan_scalar(&rem, &is_quoted))
+ {
+ _c4dbgp("it's a scalar");
+ _store_scalar(rem, is_quoted);
+ rem = m_state->line_contents.rem;
+ csubstr trimmed = rem.triml(" \t");
+ if(trimmed.len && (trimmed.begins_with(": ") || trimmed.begins_with_any(":,}") _RYML_WITH_TAB_TOKENS( || rem.begins_with(":\t"))))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, trimmed.str >= rem.str);
+ size_t num = static_cast<size_t>(trimmed.str - rem.str);
+ _c4dbgpf("trimming {} whitespace after the scalar: '{}' --> '{}'", num, rem, rem.sub(num));
+ rem = rem.sub(num);
+ _line_progressed(num);
+ }
+ }
+
+ if(rem.begins_with(": ") _RYML_WITH_TAB_TOKENS( || rem.begins_with(":\t")))
+ {
+ _c4dbgp("wait for val");
+ addrem_flags(RVAL, RKEY|QMRK);
+ _line_progressed(2);
+ if(!has_all(SSCL))
+ {
+ _c4dbgp("no key was found, defaulting to empty key ''");
+ _store_scalar_null(rem.str);
+ }
+ return true;
+ }
+ else if(rem == ':')
+ {
+ _c4dbgp("wait for val");
+ addrem_flags(RVAL, RKEY|QMRK);
+ _line_progressed(1);
+ if(!has_all(SSCL))
+ {
+ _c4dbgp("no key was found, defaulting to empty key ''");
+ _store_scalar_null(rem.str);
+ }
+ return true;
+ }
+ else if(rem.begins_with('?'))
+ {
+ _c4dbgp("complex key");
+ add_flags(QMRK);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with(','))
+ {
+ _c4dbgp("prev scalar was a key with null value");
+ _append_key_val_null(rem.str - 1);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('}'))
+ {
+ _c4dbgp("map terminates after a key...");
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(SSCL));
+ _c4dbgp("the last val was null");
+ _append_key_val_null(rem.str - 1);
+ rem_flags(RVAL);
+ if(has_all(RSEQIMAP))
+ {
+ _c4dbgp("stopping implicitly nested 1x map");
+ _stop_seqimap();
+ _pop_level();
+ }
+ _pop_level();
+ _line_progressed(1);
+ return true;
+ }
+ else if(_handle_types())
+ {
+ return true;
+ }
+ else if(_handle_key_anchors_and_refs())
+ {
+ return true;
+ }
+ else if(rem == "")
+ {
+ return true;
+ }
+ else
+ {
+ size_t pos = rem.first_not_of(" \t");
+ if(pos == csubstr::npos)
+ pos = 0;
+ rem = rem.sub(pos);
+ if(rem.begins_with(':'))
+ {
+ _c4dbgp("wait for val");
+ addrem_flags(RVAL, RKEY|QMRK);
+ _line_progressed(pos + 1);
+ if(!has_all(SSCL))
+ {
+ _c4dbgp("no key was found, defaulting to empty key ''");
+ _store_scalar_null(rem.str);
+ }
+ return true;
+ }
+ else if(rem.begins_with('#'))
+ {
+ _c4dbgp("it's a comment");
+ _line_progressed(pos);
+ rem = _scan_comment(); // also progresses the line
+ return true;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+ }
+ }
+ else if(has_any(RVAL))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RNXT));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RKEY));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(SSCL));
+ bool is_quoted;
+ if(_scan_scalar(&rem, &is_quoted))
+ {
+ _c4dbgp("it's a scalar");
+ addrem_flags(RNXT, RVAL|RKEY);
+ _append_key_val(rem, is_quoted);
+ if(has_all(RSEQIMAP))
+ {
+ _c4dbgp("stopping implicitly nested 1x map");
+ _stop_seqimap();
+ _pop_level();
+ }
+ return true;
+ }
+ else if(rem.begins_with('['))
+ {
+ _c4dbgp("val is a child seq");
+ addrem_flags(RNXT, RVAL|RKEY); // before _push_level!
+ _push_level(/*explicit flow*/true);
+ _move_scalar_from_top();
+ _start_seq();
+ add_flags(FLOW);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('{'))
+ {
+ _c4dbgp("val is a child map");
+ addrem_flags(RNXT, RVAL|RKEY); // before _push_level!
+ _push_level(/*explicit flow*/true);
+ _move_scalar_from_top();
+ _start_map();
+ addrem_flags(FLOW|RKEY, RNXT|RVAL);
+ _line_progressed(1);
+ return true;
+ }
+ else if(_handle_types())
+ {
+ return true;
+ }
+ else if(_handle_val_anchors_and_refs())
+ {
+ return true;
+ }
+ else if(rem.begins_with(','))
+ {
+ _c4dbgp("appending empty val");
+ _append_key_val_null(rem.str - 1);
+ addrem_flags(RKEY, RVAL);
+ _line_progressed(1);
+ if(has_any(RSEQIMAP))
+ {
+ _c4dbgp("stopping implicitly nested 1x map");
+ _stop_seqimap();
+ _pop_level();
+ }
+ return true;
+ }
+ else if(has_any(RSEQIMAP) && rem.begins_with(']'))
+ {
+ _c4dbgp("stopping implicitly nested 1x map");
+ if(has_any(SSCL))
+ {
+ _append_key_val_null(rem.str - 1);
+ }
+ _stop_seqimap();
+ _pop_level();
+ return true;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+ }
+ else
+ {
+ _c4err("internal error");
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+bool Parser::_handle_map_blck()
+{
+ _c4dbgpf("handle_map_impl: node_id={} level={}", m_state->node_id, m_state->level);
+ csubstr rem = m_state->line_contents.rem;
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(RMAP));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(FLOW));
+
+ if(rem.begins_with('#'))
+ {
+ _c4dbgp("it's a comment");
+ rem = _scan_comment();
+ return true;
+ }
+
+ if(has_any(RNXT))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RKEY));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RVAL));
+ // actually, we don't need RNXT in indent-based maps.
+ addrem_flags(RKEY, RNXT);
+ }
+
+ if(_handle_indentation())
+ return true;
+
+ if(has_any(RKEY))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RNXT));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RVAL));
+
+ _c4dbgp("read scalar?");
+ bool is_quoted;
+ if(_scan_scalar(&rem, &is_quoted)) // this also progresses the line
+ {
+ _c4dbgpf("it's a{} scalar", is_quoted ? " quoted" : "");
+ if(has_all(QMRK|SSCL))
+ {
+ _c4dbgpf("current key is QMRK; SSCL is set. so take store scalar='{}' as key and add an empty val", m_state->scalar);
+ _append_key_val_null(rem.str - 1);
+ }
+ _store_scalar(rem, is_quoted);
+ if(has_all(QMRK|RSET))
+ {
+ _c4dbgp("it's a complex key, so use null value '~'");
+ _append_key_val_null(rem.str);
+ }
+ rem = m_state->line_contents.rem;
+
+ if(rem.begins_with(':'))
+ {
+ _c4dbgp("wait for val");
+ addrem_flags(RVAL, RKEY|QMRK);
+ _line_progressed(1);
+ rem = m_state->line_contents.rem;
+ if(rem.begins_with_any(" \t"))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ! _at_line_begin());
+ rem = rem.left_of(rem.first_not_of(" \t"));
+ _c4dbgpf("skip {} spaces/tabs", rem.len);
+ _line_progressed(rem.len);
+ }
+ }
+ return true;
+ }
+ else if(rem.begins_with_any(" \t"))
+ {
+ size_t pos = rem.first_not_of(" \t");
+ if(pos == npos)
+ pos = rem.len;
+ _c4dbgpf("skip {} spaces/tabs", pos);
+ _line_progressed(pos);
+ return true;
+ }
+ else if(rem == '?' || rem.begins_with("? "))
+ {
+ _c4dbgp("it's a complex key");
+ _line_progressed(rem.begins_with("? ") ? 2u : 1u);
+ if(has_any(SSCL))
+ _append_key_val_null(rem.str - 1);
+ add_flags(QMRK);
+ return true;
+ }
+ else if(has_all(QMRK) && rem.begins_with(':'))
+ {
+ _c4dbgp("complex key finished");
+ if(!has_any(SSCL))
+ _store_scalar_null(rem.str);
+ addrem_flags(RVAL, RKEY|QMRK);
+ _line_progressed(1);
+ rem = m_state->line_contents.rem;
+ if(rem.begins_with(' '))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ! _at_line_begin());
+ _skipchars(' ');
+ }
+ return true;
+ }
+ else if(rem == ':' || rem.begins_with(": ") _RYML_WITH_TAB_TOKENS( || rem.begins_with(":\t")))
+ {
+ _c4dbgp("key finished");
+ if(!has_all(SSCL))
+ {
+ _c4dbgp("key was empty...");
+ _store_scalar_null(rem.str);
+ rem_flags(QMRK);
+ }
+ addrem_flags(RVAL, RKEY);
+ _line_progressed(rem == ':' ? 1 : 2);
+ return true;
+ }
+ else if(rem.begins_with("..."))
+ {
+ _c4dbgp("end current document");
+ _end_stream();
+ _line_progressed(3);
+ return true;
+ }
+ else if(rem.begins_with("---"))
+ {
+ _c4dbgp("start new document '---'");
+ _start_new_doc(rem);
+ return true;
+ }
+ else if(_handle_types())
+ {
+ return true;
+ }
+ else if(_handle_key_anchors_and_refs())
+ {
+ return true;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+ }
+ else if(has_any(RVAL))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RNXT));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RKEY));
+
+ csubstr s;
+ bool is_quoted;
+ if(_scan_scalar(&s, &is_quoted)) // this also progresses the line
+ {
+ _c4dbgpf("it's a{} scalar", is_quoted ? " quoted" : "");
+
+ rem = m_state->line_contents.rem;
+
+ if(rem.begins_with(": "))
+ {
+ _c4dbgp("actually, the scalar is the first key of a map");
+ addrem_flags(RKEY, RVAL); // before _push_level! This prepares the current level for popping by setting it to RNXT
+ _push_level();
+ _move_scalar_from_top();
+ _move_val_anchor_to_key_anchor();
+ _start_map();
+ _save_indentation(m_state->scalar_col);
+ addrem_flags(RVAL, RKEY);
+ _line_progressed(2);
+ }
+ else if(rem.begins_with(':'))
+ {
+ _c4dbgp("actually, the scalar is the first key of a map, and it opens a new scope");
+ addrem_flags(RKEY, RVAL); // before _push_level! This prepares the current level for popping by setting it to RNXT
+ _push_level();
+ _move_scalar_from_top();
+ _move_val_anchor_to_key_anchor();
+ _start_map();
+ _save_indentation(/*behind*/s.len);
+ addrem_flags(RVAL, RKEY);
+ _line_progressed(1);
+ }
+ else
+ {
+ _c4dbgp("appending keyval to current map");
+ _append_key_val(s, is_quoted);
+ addrem_flags(RKEY, RVAL);
+ }
+ return true;
+ }
+ else if(rem.begins_with("- ") _RYML_WITH_TAB_TOKENS( || rem.begins_with("-\t")))
+ {
+ _c4dbgp("val is a nested seq, indented");
+ addrem_flags(RKEY, RVAL); // before _push_level!
+ _push_level();
+ _move_scalar_from_top();
+ _start_seq();
+ _save_indentation();
+ _line_progressed(2);
+ return true;
+ }
+ else if(rem == '-')
+ {
+ _c4dbgp("maybe a seq. start unknown, indented");
+ _start_unk();
+ _save_indentation();
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('['))
+ {
+ _c4dbgp("val is a child seq, flow");
+ addrem_flags(RKEY, RVAL); // before _push_level!
+ _push_level(/*explicit flow*/true);
+ _move_scalar_from_top();
+ _start_seq();
+ add_flags(FLOW);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with('{'))
+ {
+ _c4dbgp("val is a child map, flow");
+ addrem_flags(RKEY, RVAL); // before _push_level!
+ _push_level(/*explicit flow*/true);
+ _move_scalar_from_top();
+ _start_map();
+ addrem_flags(FLOW|RKEY, RVAL);
+ _line_progressed(1);
+ return true;
+ }
+ else if(rem.begins_with(' '))
+ {
+ csubstr spc = rem.left_of(rem.first_not_of(' '));
+ if(_at_line_begin())
+ {
+ _c4dbgpf("skipping value indentation: {} spaces", spc.len);
+ _line_progressed(spc.len);
+ return true;
+ }
+ else
+ {
+ _c4dbgpf("skipping {} spaces", spc.len);
+ _line_progressed(spc.len);
+ return true;
+ }
+ }
+ else if(_handle_types())
+ {
+ return true;
+ }
+ else if(_handle_val_anchors_and_refs())
+ {
+ return true;
+ }
+ else if(rem.begins_with("--- ") || rem == "---" || rem.begins_with("---\t"))
+ {
+ _start_new_doc(rem);
+ return true;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+ }
+ else
+ {
+ _c4err("internal error");
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+bool Parser::_handle_top()
+{
+ _c4dbgp("handle_top");
+ csubstr rem = m_state->line_contents.rem;
+
+ if(rem.begins_with('#'))
+ {
+ _c4dbgp("a comment line");
+ _scan_comment();
+ return true;
+ }
+
+ csubstr trimmed = rem.triml(' ');
+
+ if(trimmed.begins_with('%'))
+ {
+ _handle_directive(trimmed);
+ _line_progressed(rem.len);
+ return true;
+ }
+ else if(trimmed.begins_with("--- ") || trimmed == "---" || trimmed.begins_with("---\t"))
+ {
+ _start_new_doc(rem);
+ if(trimmed.len < rem.len)
+ {
+ _line_progressed(rem.len - trimmed.len);
+ _save_indentation();
+ }
+ return true;
+ }
+ else if(trimmed.begins_with("..."))
+ {
+ _c4dbgp("end current document");
+ _end_stream();
+ if(trimmed.len < rem.len)
+ {
+ _line_progressed(rem.len - trimmed.len);
+ }
+ _line_progressed(3);
+ return true;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+
+bool Parser::_handle_key_anchors_and_refs()
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, !has_any(RVAL));
+ const csubstr rem = m_state->line_contents.rem;
+ if(rem.begins_with('&'))
+ {
+ _c4dbgp("found a key anchor!!!");
+ if(has_all(QMRK|SSCL))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_any(RKEY));
+ _c4dbgp("there is a stored key, so this anchor is for the next element");
+ _append_key_val_null(rem.str - 1);
+ rem_flags(QMRK);
+ return true;
+ }
+ csubstr anchor = rem.left_of(rem.first_of(' '));
+ _line_progressed(anchor.len);
+ anchor = anchor.sub(1); // skip the first character
+ _move_key_anchor_to_val_anchor();
+ _c4dbgpf("key anchor value: '{}'", anchor);
+ m_key_anchor = anchor;
+ m_key_anchor_indentation = m_state->line_contents.current_col(rem);
+ return true;
+ }
+ else if(C4_UNLIKELY(rem.begins_with('*')))
+ {
+ _c4err("not implemented - this should have been catched elsewhere");
+ C4_NEVER_REACH();
+ return false;
+ }
+ return false;
+}
+
+bool Parser::_handle_val_anchors_and_refs()
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, !has_any(RKEY));
+ const csubstr rem = m_state->line_contents.rem;
+ if(rem.begins_with('&'))
+ {
+ csubstr anchor = rem.left_of(rem.first_of(' '));
+ _line_progressed(anchor.len);
+ anchor = anchor.sub(1); // skip the first character
+ _c4dbgpf("val: found an anchor: '{}', indentation={}!!!", anchor, m_state->line_contents.current_col(rem));
+ if(m_val_anchor.empty())
+ {
+ _c4dbgpf("save val anchor: '{}'", anchor);
+ m_val_anchor = anchor;
+ m_val_anchor_indentation = m_state->line_contents.current_col(rem);
+ }
+ else
+ {
+ _c4dbgpf("there is a pending val anchor '{}'", m_val_anchor);
+ if(m_tree->is_seq(m_state->node_id))
+ {
+ if(m_tree->has_children(m_state->node_id))
+ {
+ _c4dbgpf("current node={} is a seq, has {} children", m_state->node_id, m_tree->num_children(m_state->node_id));
+ _c4dbgpf("... so take the new one as a key anchor '{}'", anchor);
+ m_key_anchor = anchor;
+ m_key_anchor_indentation = m_state->line_contents.current_col(rem);
+ }
+ else
+ {
+ _c4dbgpf("current node={} is a seq, has no children", m_state->node_id);
+ if(m_tree->has_val_anchor(m_state->node_id))
+ {
+ _c4dbgpf("... node={} already has val anchor: '{}'", m_state->node_id, m_tree->val_anchor(m_state->node_id));
+ _c4dbgpf("... so take the new one as a key anchor '{}'", anchor);
+ m_key_anchor = anchor;
+ m_key_anchor_indentation = m_state->line_contents.current_col(rem);
+ }
+ else
+ {
+ _c4dbgpf("... so set pending val anchor: '{}' on current node {}", m_val_anchor, m_state->node_id);
+ m_tree->set_val_anchor(m_state->node_id, m_val_anchor);
+ m_val_anchor = anchor;
+ m_val_anchor_indentation = m_state->line_contents.current_col(rem);
+ }
+ }
+ }
+ }
+ return true;
+ }
+ else if(C4_UNLIKELY(rem.begins_with('*')))
+ {
+ _c4err("not implemented - this should have been catched elsewhere");
+ C4_NEVER_REACH();
+ return false;
+ }
+ return false;
+}
+
+void Parser::_move_key_anchor_to_val_anchor()
+{
+ if(m_key_anchor.empty())
+ return;
+ _c4dbgpf("move current key anchor to val slot: key='{}' -> val='{}'", m_key_anchor, m_val_anchor);
+ if(!m_val_anchor.empty())
+ _c4err("triple-pending anchor");
+ m_val_anchor = m_key_anchor;
+ m_val_anchor_indentation = m_key_anchor_indentation;
+ m_key_anchor = {};
+ m_key_anchor_indentation = {};
+}
+
+void Parser::_move_val_anchor_to_key_anchor()
+{
+ if(m_val_anchor.empty())
+ return;
+ if(!_token_is_from_this_line(m_val_anchor))
+ return;
+ _c4dbgpf("move current val anchor to key slot: key='{}' <- val='{}'", m_key_anchor, m_val_anchor);
+ if(!m_key_anchor.empty())
+ _c4err("triple-pending anchor");
+ m_key_anchor = m_val_anchor;
+ m_key_anchor_indentation = m_val_anchor_indentation;
+ m_val_anchor = {};
+ m_val_anchor_indentation = {};
+}
+
+void Parser::_move_key_tag_to_val_tag()
+{
+ if(m_key_tag.empty())
+ return;
+ _c4dbgpf("move key tag to val tag: key='{}' -> val='{}'", m_key_tag, m_val_tag);
+ m_val_tag = m_key_tag;
+ m_val_tag_indentation = m_key_tag_indentation;
+ m_key_tag.clear();
+ m_key_tag_indentation = 0;
+}
+
+void Parser::_move_val_tag_to_key_tag()
+{
+ if(m_val_tag.empty())
+ return;
+ if(!_token_is_from_this_line(m_val_tag))
+ return;
+ _c4dbgpf("move val tag to key tag: key='{}' <- val='{}'", m_key_tag, m_val_tag);
+ m_key_tag = m_val_tag;
+ m_key_tag_indentation = m_val_tag_indentation;
+ m_val_tag.clear();
+ m_val_tag_indentation = 0;
+}
+
+void Parser::_move_key_tag2_to_key_tag()
+{
+ if(m_key_tag2.empty())
+ return;
+ _c4dbgpf("move key tag2 to key tag: key='{}' <- key2='{}'", m_key_tag, m_key_tag2);
+ m_key_tag = m_key_tag2;
+ m_key_tag_indentation = m_key_tag2_indentation;
+ m_key_tag2.clear();
+ m_key_tag2_indentation = 0;
+}
+
+
+//-----------------------------------------------------------------------------
+
+bool Parser::_handle_types()
+{
+ csubstr rem = m_state->line_contents.rem.triml(' ');
+ csubstr t;
+
+ if(rem.begins_with("!!"))
+ {
+ _c4dbgp("begins with '!!'");
+ t = rem.left_of(rem.first_of(" ,"));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, t.len >= 2);
+ //t = t.sub(2);
+ if(t == "!!set")
+ add_flags(RSET);
+ }
+ else if(rem.begins_with("!<"))
+ {
+ _c4dbgp("begins with '!<'");
+ t = rem.left_of(rem.first_of('>'), true);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, t.len >= 2);
+ //t = t.sub(2, t.len-1);
+ }
+ else if(rem.begins_with("!h!"))
+ {
+ _c4dbgp("begins with '!h!'");
+ t = rem.left_of(rem.first_of(' '));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, t.len >= 3);
+ //t = t.sub(3);
+ }
+ else if(rem.begins_with('!'))
+ {
+ _c4dbgp("begins with '!'");
+ t = rem.left_of(rem.first_of(' '));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, t.len >= 1);
+ //t = t.sub(1);
+ }
+
+ if(t.empty())
+ return false;
+
+ if(has_all(QMRK|SSCL))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_any(RKEY));
+ _c4dbgp("there is a stored key, so this tag is for the next element");
+ _append_key_val_null(rem.str - 1);
+ rem_flags(QMRK);
+ }
+
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ const char *tag_beginning = rem.str;
+ #endif
+ size_t tag_indentation = m_state->line_contents.current_col(t);
+ _c4dbgpf("there was a tag: '{}', indentation={}", t, tag_indentation);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, t.end() > m_state->line_contents.rem.begin());
+ _line_progressed(static_cast<size_t>(t.end() - m_state->line_contents.rem.begin()));
+ {
+ size_t pos = m_state->line_contents.rem.first_not_of(" \t");
+ if(pos != csubstr::npos)
+ _line_progressed(pos);
+ }
+
+ if(has_all(RMAP|RKEY))
+ {
+ _c4dbgpf("saving map key tag '{}'", t);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_key_tag.empty());
+ m_key_tag = t;
+ m_key_tag_indentation = tag_indentation;
+ }
+ else if(has_all(RMAP|RVAL))
+ {
+ /* foo: !!str
+ * !!str : bar */
+ rem = m_state->line_contents.rem;
+ rem = rem.left_of(rem.find("#"));
+ rem = rem.trimr(" \t");
+ _c4dbgpf("rem='{}'", rem);
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ if(rem == ':' || rem.begins_with(": "))
+ {
+ _c4dbgp("the last val was null, and this is a tag from a null key");
+ _append_key_val_null(tag_beginning - 1);
+ _store_scalar_null(rem.str - 1);
+ // do not change the flag to key, it is ~
+ _RYML_CB_ASSERT(m_stack.m_callbacks, rem.begin() > m_state->line_contents.rem.begin());
+ size_t token_len = rem == ':' ? 1 : 2;
+ _line_progressed(static_cast<size_t>(token_len + rem.begin() - m_state->line_contents.rem.begin()));
+ }
+ #endif
+ _c4dbgpf("saving map val tag '{}'", t);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_val_tag.empty());
+ m_val_tag = t;
+ m_val_tag_indentation = tag_indentation;
+ }
+ else if(has_all(RSEQ|RVAL) || has_all(RTOP|RUNK|NDOC))
+ {
+ if(m_val_tag.empty())
+ {
+ _c4dbgpf("saving seq/doc val tag '{}'", t);
+ m_val_tag = t;
+ m_val_tag_indentation = tag_indentation;
+ }
+ else
+ {
+ _c4dbgpf("saving seq/doc key tag '{}'", t);
+ m_key_tag = t;
+ m_key_tag_indentation = tag_indentation;
+ }
+ }
+ else if(has_all(RTOP|RUNK) || has_any(RUNK))
+ {
+ rem = m_state->line_contents.rem;
+ rem = rem.left_of(rem.find("#"));
+ rem = rem.trimr(" \t");
+ if(rem.empty())
+ {
+ _c4dbgpf("saving val tag '{}'", t);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_val_tag.empty());
+ m_val_tag = t;
+ m_val_tag_indentation = tag_indentation;
+ }
+ else
+ {
+ _c4dbgpf("saving key tag '{}'", t);
+ if(m_key_tag.empty())
+ {
+ m_key_tag = t;
+ m_key_tag_indentation = tag_indentation;
+ }
+ else
+ {
+ /* handle this case:
+ * !!str foo: !!map
+ * !!int 1: !!float 20.0
+ * !!int 3: !!float 40.0
+ *
+ * (m_key_tag would be !!str and m_key_tag2 would be !!int)
+ */
+ m_key_tag2 = t;
+ m_key_tag2_indentation = tag_indentation;
+ }
+ }
+ }
+ else
+ {
+ _c4err("internal error");
+ }
+
+ if(m_val_tag.not_empty())
+ {
+ YamlTag_e tag = to_tag(t);
+ if(tag == TAG_STR)
+ {
+ _c4dbgpf("tag '{}' is a str-type tag", t);
+ if(has_all(RTOP|RUNK|NDOC))
+ {
+ _c4dbgpf("docval. slurping the string. pos={}", m_state->pos.offset);
+ csubstr scalar = _slurp_doc_scalar();
+ _c4dbgpf("docval. after slurp: {}, at node {}: '{}'", m_state->pos.offset, m_state->node_id, scalar);
+ m_tree->to_val(m_state->node_id, scalar, DOC);
+ _c4dbgpf("docval. val tag {} -> {}", m_val_tag, normalize_tag(m_val_tag));
+ m_tree->set_val_tag(m_state->node_id, normalize_tag(m_val_tag));
+ m_val_tag.clear();
+ if(!m_val_anchor.empty())
+ {
+ _c4dbgpf("setting val anchor[{}]='{}'", m_state->node_id, m_val_anchor);
+ m_tree->set_val_anchor(m_state->node_id, m_val_anchor);
+ m_val_anchor.clear();
+ }
+ _end_stream();
+ }
+ }
+ }
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+csubstr Parser::_slurp_doc_scalar()
+{
+ csubstr s = m_state->line_contents.rem;
+ size_t pos = m_state->pos.offset;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->line_contents.full.find("---") != csubstr::npos);
+ _c4dbgpf("slurp 0 '{}'. REM='{}'", s, m_buf.sub(m_state->pos.offset));
+ if(s.len == 0)
+ {
+ _line_ended();
+ _scan_line();
+ s = m_state->line_contents.rem;
+ pos = m_state->pos.offset;
+ }
+
+ size_t skipws = s.first_not_of(" \t");
+ _c4dbgpf("slurp 1 '{}'. REM='{}'", s, m_buf.sub(m_state->pos.offset));
+ if(skipws != npos)
+ {
+ _line_progressed(skipws);
+ s = m_state->line_contents.rem;
+ pos = m_state->pos.offset;
+ _c4dbgpf("slurp 2 '{}'. REM='{}'", s, m_buf.sub(m_state->pos.offset));
+ }
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_val_anchor.empty());
+ _handle_val_anchors_and_refs();
+ if(!m_val_anchor.empty())
+ {
+ s = m_state->line_contents.rem;
+ skipws = s.first_not_of(" \t");
+ if(skipws != npos)
+ {
+ _line_progressed(skipws);
+ }
+ s = m_state->line_contents.rem;
+ pos = m_state->pos.offset;
+ _c4dbgpf("slurp 3 '{}'. REM='{}'", s, m_buf.sub(m_state->pos.offset));
+ }
+
+ if(s.begins_with('\''))
+ {
+ m_state->scalar_col = m_state->line_contents.current_col(s);
+ return _scan_squot_scalar();
+ }
+ else if(s.begins_with('"'))
+ {
+ m_state->scalar_col = m_state->line_contents.current_col(s);
+ return _scan_dquot_scalar();
+ }
+ else if(s.begins_with('|') || s.begins_with('>'))
+ {
+ return _scan_block();
+ }
+
+ _c4dbgpf("slurp 4 '{}'. REM='{}'", s, m_buf.sub(m_state->pos.offset));
+
+ m_state->scalar_col = m_state->line_contents.current_col(s);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.end() >= m_buf.begin() + pos);
+ _line_progressed(static_cast<size_t>(s.end() - (m_buf.begin() + pos)));
+
+ _c4dbgpf("slurp 5 '{}'. REM='{}'", s, m_buf.sub(m_state->pos.offset));
+
+ if(_at_line_end())
+ {
+ _c4dbgpf("at line end. curr='{}'", s);
+ s = _extend_scanned_scalar(s);
+ }
+
+ _c4dbgpf("scalar was '{}'", s);
+
+ return s;
+}
+
+//-----------------------------------------------------------------------------
+bool Parser::_scan_scalar(csubstr *C4_RESTRICT scalar, bool *C4_RESTRICT quoted)
+{
+ csubstr s = m_state->line_contents.rem;
+ if(s.len == 0)
+ return false;
+ s = s.trim(" \t");
+ if(s.len == 0)
+ return false;
+
+ if(s.begins_with('\''))
+ {
+ _c4dbgp("got a ': scanning single-quoted scalar");
+ m_state->scalar_col = m_state->line_contents.current_col(s);
+ *scalar = _scan_squot_scalar();
+ *quoted = true;
+ return true;
+ }
+ else if(s.begins_with('"'))
+ {
+ _c4dbgp("got a \": scanning double-quoted scalar");
+ m_state->scalar_col = m_state->line_contents.current_col(s);
+ *scalar = _scan_dquot_scalar();
+ *quoted = true;
+ return true;
+ }
+ else if(s.begins_with('|') || s.begins_with('>'))
+ {
+ *scalar = _scan_block();
+ *quoted = false;
+ return true;
+ }
+ else if(has_any(RTOP) && _is_doc_sep(s))
+ {
+ return false;
+ }
+ else if(has_any(RSEQ))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ! has_all(RKEY));
+ if(has_all(RVAL))
+ {
+ _c4dbgp("RSEQ|RVAL");
+ if( ! _is_scalar_next__rseq_rval(s))
+ return false;
+ _RYML_WITH_TAB_TOKENS(else if(s.begins_with("-\t"))
+ return false;
+ )
+ if(s.ends_with(':'))
+ {
+ --s.len;
+ }
+ else
+ {
+ auto first = s.first_of_any(": " _RYML_WITH_TAB_TOKENS( , ":\t"), " #");
+ if(first)
+ s.len = first.pos;
+ }
+ if(has_all(FLOW))
+ {
+ _c4dbgp("RSEQ|RVAL|EXPL");
+ s = s.left_of(s.first_of(",]"));
+ }
+ s = s.trimr(_RYML_WITH_OR_WITHOUT_TAB_TOKENS(" \t", ' '));
+ }
+ else
+ {
+ _c4err("internal error");
+ }
+ }
+ else if(has_any(RMAP))
+ {
+ if( ! _is_scalar_next__rmap(s))
+ return false;
+ size_t colon_space = s.find(": ");
+ if(colon_space == npos)
+ {
+ _RYML_WITH_OR_WITHOUT_TAB_TOKENS(
+ // with tab tokens
+ colon_space = s.find(":\t");
+ if(colon_space == npos)
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.len > 0);
+ colon_space = s.find(':');
+ if(colon_space != s.len-1)
+ colon_space = npos;
+ }
+ ,
+ // without tab tokens
+ colon_space = s.find(':');
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.len > 0);
+ if(colon_space != s.len-1)
+ colon_space = npos;
+ )
+ }
+
+ if(has_all(RKEY))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, !s.begins_with(' '));
+ if(has_any(QMRK))
+ {
+ _c4dbgp("RMAP|RKEY|CPLX");
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_any(RMAP));
+ if(s.begins_with("? ") || s == '?')
+ return false;
+ s = s.left_of(colon_space);
+ s = s.left_of(s.first_of("#"));
+ if(has_any(FLOW))
+ s = s.left_of(s.first_of(':'));
+ s = s.trimr(" \t");
+ if(s.begins_with("---"))
+ return false;
+ else if(s.begins_with("..."))
+ return false;
+ }
+ else
+ {
+ _c4dbgp("RMAP|RKEY");
+ _RYML_CB_CHECK(m_stack.m_callbacks, !s.begins_with('{'));
+ if(s.begins_with("? ") || s == '?')
+ return false;
+ s = s.left_of(colon_space);
+ s = s.trimr(_RYML_WITH_OR_WITHOUT_TAB_TOKENS(" \t", ' '));
+ if(has_any(FLOW))
+ {
+ _c4dbgpf("RMAP|RKEY|EXPL: '{}'", s);
+ s = s.left_of(s.first_of(",}"));
+ if(s.ends_with(':'))
+ s = s.offs(0, 1);
+ }
+ else if(s.begins_with("---"))
+ {
+ return false;
+ }
+ else if(s.begins_with("..."))
+ {
+ return false;
+ }
+ }
+ }
+ else if(has_all(RVAL))
+ {
+ _c4dbgp("RMAP|RVAL");
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(QMRK));
+ if( ! _is_scalar_next__rmap_val(s))
+ return false;
+ _RYML_WITH_TAB_TOKENS(else if(s.begins_with("-\t"))
+ return false;
+ )
+ s = s.left_of(s.find(" #")); // is there a comment?
+ s = s.left_of(s.find("\t#")); // is there a comment?
+ if(has_any(FLOW))
+ {
+ _c4dbgp("RMAP|RVAL|EXPL");
+ if(has_none(RSEQIMAP))
+ s = s.left_of(s.first_of(",}"));
+ else
+ s = s.left_of(s.first_of(",]"));
+ }
+ s = s.trim(_RYML_WITH_OR_WITHOUT_TAB_TOKENS(" \t", ' '));
+ if(s.begins_with("---"))
+ return false;
+ else if(s.begins_with("..."))
+ return false;
+ }
+ else
+ {
+ _c4err("parse error");
+ }
+ }
+ else if(has_all(RUNK))
+ {
+ _c4dbgpf("RUNK '[{}]~~~{}~~~", s.len, s);
+ if( ! _is_scalar_next__runk(s))
+ {
+ _c4dbgp("RUNK: no scalar next");
+ return false;
+ }
+ s = s.left_of(s.find(" #"));
+ size_t pos = s.find(": ");
+ if(pos != npos)
+ s = s.left_of(pos);
+ else if(s.ends_with(':'))
+ s = s.left_of(s.len-1);
+ _RYML_WITH_TAB_TOKENS(
+ else if((pos = s.find(":\t")) != npos) // TABS
+ s = s.left_of(pos);
+ )
+ else
+ s = s.left_of(s.first_of(','));
+ s = s.trim(" \t");
+ _c4dbgpf("RUNK: scalar='{}'", s);
+ }
+ else
+ {
+ _c4err("not implemented");
+ }
+
+ if(s.empty())
+ return false;
+
+ m_state->scalar_col = m_state->line_contents.current_col(s);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.str >= m_state->line_contents.rem.str);
+ _line_progressed(static_cast<size_t>(s.str - m_state->line_contents.rem.str) + s.len);
+
+ if(_at_line_end() && s != '~')
+ {
+ _c4dbgpf("at line end. curr='{}'", s);
+ s = _extend_scanned_scalar(s);
+ }
+
+ _c4dbgpf("scalar was '{}'", s);
+
+ *scalar = s;
+ *quoted = false;
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+
+csubstr Parser::_extend_scanned_scalar(csubstr s)
+{
+ if(has_all(RMAP|RKEY|QMRK))
+ {
+ size_t scalar_indentation = has_any(FLOW) ? 0 : m_state->scalar_col;
+ _c4dbgpf("extend_scalar: explicit key! indref={} scalar_indentation={} scalar_col={}", m_state->indref, scalar_indentation, m_state->scalar_col);
+ csubstr n = _scan_to_next_nonempty_line(scalar_indentation);
+ if(!n.empty())
+ {
+ substr full = _scan_complex_key(s, n).trimr(" \t\r\n");
+ if(full != s)
+ s = _filter_plain_scalar(full, scalar_indentation);
+ }
+ }
+ // deal with plain (unquoted) scalars that continue to the next line
+ else if(!s.begins_with_any("*")) // cannot be a plain scalar if it starts with * (that's an anchor reference)
+ {
+ _c4dbgpf("extend_scalar: line ended, scalar='{}'", s);
+ if(has_none(FLOW))
+ {
+ size_t scalar_indentation = m_state->indref + 1;
+ if(has_all(RUNK) && scalar_indentation == 1)
+ scalar_indentation = 0;
+ csubstr n = _scan_to_next_nonempty_line(scalar_indentation);
+ if(!n.empty())
+ {
+ _c4dbgpf("rscalar[IMPL]: state_indref={} state_indentation={} scalar_indentation={}", m_state->indref, m_state->line_contents.indentation, scalar_indentation);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->line_contents.full.is_super(n));
+ substr full = _scan_plain_scalar_blck(s, n, scalar_indentation);
+ if(full.len >= s.len)
+ s = _filter_plain_scalar(full, scalar_indentation);
+ }
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(FLOW));
+ csubstr n = _scan_to_next_nonempty_line(/*indentation*/0);
+ if(!n.empty())
+ {
+ _c4dbgp("rscalar[FLOW]");
+ substr full = _scan_plain_scalar_flow(s, n);
+ s = _filter_plain_scalar(full, /*indentation*/0);
+ }
+ }
+ }
+
+ return s;
+}
+
+
+//-----------------------------------------------------------------------------
+
+substr Parser::_scan_plain_scalar_flow(csubstr currscalar, csubstr peeked_line)
+{
+ static constexpr const csubstr chars = "[]{}?#,";
+ size_t pos = peeked_line.first_of(chars);
+ bool first = true;
+ while(pos != 0)
+ {
+ if(has_all(RMAP|RKEY) || has_any(RUNK))
+ {
+ csubstr tpkl = peeked_line.triml(' ').trimr("\r\n");
+ if(tpkl.begins_with(": ") || tpkl == ':')
+ {
+ _c4dbgpf("rscalar[EXPL]: map value starts on the peeked line: '{}'", peeked_line);
+ peeked_line = peeked_line.first(0);
+ break;
+ }
+ else
+ {
+ auto colon_pos = peeked_line.first_of_any(": ", ":");
+ if(colon_pos && colon_pos.pos < pos)
+ {
+ peeked_line = peeked_line.first(colon_pos.pos);
+ _c4dbgpf("rscalar[EXPL]: found colon at {}. peeked='{}'", colon_pos.pos, peeked_line);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, peeked_line.end() >= m_state->line_contents.rem.begin());
+ _line_progressed(static_cast<size_t>(peeked_line.end() - m_state->line_contents.rem.begin()));
+ break;
+ }
+ }
+ }
+ if(pos != npos)
+ {
+ _c4dbgpf("rscalar[EXPL]: found special character '{}' at {}, stopping: '{}'", peeked_line[pos], pos, peeked_line.left_of(pos).trimr("\r\n"));
+ peeked_line = peeked_line.left_of(pos);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, peeked_line.end() >= m_state->line_contents.rem.begin());
+ _line_progressed(static_cast<size_t>(peeked_line.end() - m_state->line_contents.rem.begin()));
+ break;
+ }
+ _c4dbgpf("rscalar[EXPL]: append another line, full: '{}'", peeked_line.trimr("\r\n"));
+ if(!first)
+ {
+ RYML_CHECK(_advance_to_peeked());
+ }
+ peeked_line = _scan_to_next_nonempty_line(/*indentation*/0);
+ if(peeked_line.empty())
+ {
+ _c4err("expected token or continuation");
+ }
+ pos = peeked_line.first_of(chars);
+ first = false;
+ }
+ substr full(m_buf.str + (currscalar.str - m_buf.str), m_buf.begin() + m_state->pos.offset);
+ full = full.trimr("\n\r ");
+ return full;
+}
+
+
+//-----------------------------------------------------------------------------
+
+substr Parser::_scan_plain_scalar_blck(csubstr currscalar, csubstr peeked_line, size_t indentation)
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_buf.is_super(currscalar));
+ // NOTE. there's a problem with _scan_to_next_nonempty_line(), as it counts newlines twice
+ // size_t offs = m_state->pos.offset; // so we workaround by directly counting from the end of the given scalar
+ _RYML_CB_ASSERT(m_stack.m_callbacks, currscalar.end() >= m_buf.begin());
+ size_t offs = static_cast<size_t>(currscalar.end() - m_buf.begin());
+ _RYML_CB_ASSERT(m_stack.m_callbacks, peeked_line.begins_with(' ', indentation));
+ while(true)
+ {
+ _c4dbgpf("rscalar[IMPL]: continuing... ref_indentation={}", indentation);
+ if(peeked_line.begins_with("...") || peeked_line.begins_with("---"))
+ {
+ _c4dbgpf("rscalar[IMPL]: document termination next -- bail now '{}'", peeked_line.trimr("\r\n"));
+ break;
+ }
+ else if(( ! peeked_line.begins_with(' ', indentation))) // is the line deindented?
+ {
+ if(!peeked_line.trim(" \r\n\t").empty()) // is the line not blank?
+ {
+ _c4dbgpf("rscalar[IMPL]: deindented line, not blank -- bail now '{}'", peeked_line.trimr("\r\n"));
+ break;
+ }
+ _c4dbgpf("rscalar[IMPL]: line is blank and has less indentation: ref={} line={}: '{}'", indentation, peeked_line.first_not_of(' ') == csubstr::npos ? 0 : peeked_line.first_not_of(' '), peeked_line.trimr("\r\n"));
+ _c4dbgpf("rscalar[IMPL]: ... searching for a line starting at indentation {}", indentation);
+ csubstr next_peeked = _scan_to_next_nonempty_line(indentation);
+ if(next_peeked.empty())
+ {
+ _c4dbgp("rscalar[IMPL]: ... finished.");
+ break;
+ }
+ _c4dbgp("rscalar[IMPL]: ... continuing.");
+ peeked_line = next_peeked;
+ }
+
+ _c4dbgpf("rscalar[IMPL]: line contents: '{}'", peeked_line.right_of(indentation, true).trimr("\r\n"));
+ size_t token_pos;
+ if(peeked_line.find(": ") != npos)
+ {
+ _line_progressed(peeked_line.find(": "));
+ _c4err("': ' is not a valid token in plain flow (unquoted) scalars");
+ }
+ else if(peeked_line.ends_with(':'))
+ {
+ _line_progressed(peeked_line.find(':'));
+ _c4err("lines cannot end with ':' in plain flow (unquoted) scalars");
+ }
+ else if((token_pos = peeked_line.find(" #")) != npos)
+ {
+ _line_progressed(token_pos);
+ break;
+ //_c4err("' #' is not a valid token in plain flow (unquoted) scalars");
+ }
+
+ _c4dbgpf("rscalar[IMPL]: append another line: (len={})'{}'", peeked_line.len, peeked_line.trimr("\r\n"));
+ if(!_advance_to_peeked())
+ {
+ _c4dbgp("rscalar[IMPL]: file finishes after the scalar");
+ break;
+ }
+ peeked_line = m_state->line_contents.rem;
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->pos.offset >= offs);
+ substr full(m_buf.str + (currscalar.str - m_buf.str),
+ currscalar.len + (m_state->pos.offset - offs));
+ full = full.trimr("\r\n ");
+ return full;
+}
+
+substr Parser::_scan_complex_key(csubstr currscalar, csubstr peeked_line)
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_buf.is_super(currscalar));
+ // NOTE. there's a problem with _scan_to_next_nonempty_line(), as it counts newlines twice
+ // size_t offs = m_state->pos.offset; // so we workaround by directly counting from the end of the given scalar
+ _RYML_CB_ASSERT(m_stack.m_callbacks, currscalar.end() >= m_buf.begin());
+ size_t offs = static_cast<size_t>(currscalar.end() - m_buf.begin());
+ while(true)
+ {
+ _c4dbgp("rcplxkey: continuing...");
+ if(peeked_line.begins_with("...") || peeked_line.begins_with("---"))
+ {
+ _c4dbgpf("rcplxkey: document termination next -- bail now '{}'", peeked_line.trimr("\r\n"));
+ break;
+ }
+ else
+ {
+ size_t pos = peeked_line.first_of("?:[]{}");
+ if(pos == csubstr::npos)
+ {
+ pos = peeked_line.find("- ");
+ }
+ if(pos != csubstr::npos)
+ {
+ _c4dbgpf("rcplxkey: found special characters at pos={}: '{}'", pos, peeked_line.trimr("\r\n"));
+ _line_progressed(pos);
+ break;
+ }
+ }
+
+ _c4dbgpf("rcplxkey: no special chars found '{}'", peeked_line.trimr("\r\n"));
+ csubstr next_peeked = _scan_to_next_nonempty_line(0);
+ if(next_peeked.empty())
+ {
+ _c4dbgp("rcplxkey: empty ... finished.");
+ break;
+ }
+ _c4dbgp("rcplxkey: ... continuing.");
+ peeked_line = next_peeked;
+
+ _c4dbgpf("rcplxkey: line contents: '{}'", peeked_line.trimr("\r\n"));
+ size_t colpos;
+ if((colpos = peeked_line.find(": ")) != npos)
+ {
+ _c4dbgp("rcplxkey: found ': ', stopping.");
+ _line_progressed(colpos);
+ break;
+ }
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ else if((colpos = peeked_line.ends_with(':')))
+ {
+ _c4dbgp("rcplxkey: ends with ':', stopping.");
+ _line_progressed(colpos);
+ break;
+ }
+ #endif
+ _c4dbgpf("rcplxkey: append another line: (len={})'{}'", peeked_line.len, peeked_line.trimr("\r\n"));
+ if(!_advance_to_peeked())
+ {
+ _c4dbgp("rcplxkey: file finishes after the scalar");
+ break;
+ }
+ peeked_line = m_state->line_contents.rem;
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->pos.offset >= offs);
+ substr full(m_buf.str + (currscalar.str - m_buf.str),
+ currscalar.len + (m_state->pos.offset - offs));
+ return full;
+}
+
+//! scans to the next non-blank line starting with the given indentation
+csubstr Parser::_scan_to_next_nonempty_line(size_t indentation)
+{
+ csubstr next_peeked;
+ while(true)
+ {
+ _c4dbgpf("rscalar: ... curr offset: {} indentation={}", m_state->pos.offset, indentation);
+ next_peeked = _peek_next_line(m_state->pos.offset);
+ csubstr next_peeked_triml = next_peeked.triml(' ');
+ _c4dbgpf("rscalar: ... next peeked line='{}'", next_peeked.trimr("\r\n"));
+ if(next_peeked_triml.begins_with('#'))
+ {
+ _c4dbgp("rscalar: ... first non-space character is #");
+ return {};
+ }
+ else if(next_peeked.begins_with(' ', indentation))
+ {
+ _c4dbgpf("rscalar: ... begins at same indentation {}, assuming continuation", indentation);
+ _advance_to_peeked();
+ return next_peeked;
+ }
+ else // check for de-indentation
+ {
+ csubstr trimmed = next_peeked_triml.trimr("\t\r\n");
+ _c4dbgpf("rscalar: ... deindented! trimmed='{}'", trimmed);
+ if(!trimmed.empty())
+ {
+ _c4dbgp("rscalar: ... and not empty. bailing out.");
+ return {};
+ }
+ }
+ if(!_advance_to_peeked())
+ {
+ _c4dbgp("rscalar: file finished");
+ return {};
+ }
+ }
+ return {};
+}
+
+// returns false when the file finished
+bool Parser::_advance_to_peeked()
+{
+ _line_progressed(m_state->line_contents.rem.len);
+ _line_ended(); // advances to the peeked-at line, consuming all remaining (probably newline) characters on the current line
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->line_contents.rem.first_of("\r\n") == csubstr::npos);
+ _c4dbgpf("advance to peeked: scan more... pos={} len={}", m_state->pos.offset, m_buf.len);
+ _scan_line(); // puts the peeked-at line in the buffer
+ if(_finished_file())
+ {
+ _c4dbgp("rscalar: finished file!");
+ return false;
+ }
+ return true;
+}
+
+//-----------------------------------------------------------------------------
+
+C4_ALWAYS_INLINE size_t _extend_from_combined_newline(char nl, char following)
+{
+ return (nl == '\n' && following == '\r') || (nl == '\r' && following == '\n');
+}
+
+//! look for the next newline chars, and jump to the right of those
+csubstr from_next_line(csubstr rem)
+{
+ size_t nlpos = rem.first_of("\r\n");
+ if(nlpos == csubstr::npos)
+ return {};
+ const char nl = rem[nlpos];
+ rem = rem.right_of(nlpos);
+ if(rem.empty())
+ return {};
+ if(_extend_from_combined_newline(nl, rem.front()))
+ rem = rem.sub(1);
+ return rem;
+}
+
+csubstr Parser::_peek_next_line(size_t pos) const
+{
+ csubstr rem{}; // declare here because of the goto
+ size_t nlpos{}; // declare here because of the goto
+ pos = pos == npos ? m_state->pos.offset : pos;
+ if(pos >= m_buf.len)
+ goto next_is_empty;
+
+ // look for the next newline chars, and jump to the right of those
+ rem = from_next_line(m_buf.sub(pos));
+ if(rem.empty())
+ goto next_is_empty;
+
+ // now get everything up to and including the following newline chars
+ nlpos = rem.first_of("\r\n");
+ if((nlpos != csubstr::npos) && (nlpos + 1 < rem.len))
+ nlpos += _extend_from_combined_newline(rem[nlpos], rem[nlpos+1]);
+ rem = rem.left_of(nlpos, /*include_pos*/true);
+
+ _c4dbgpf("peek next line @ {}: (len={})'{}'", pos, rem.len, rem.trimr("\r\n"));
+ return rem;
+
+next_is_empty:
+ _c4dbgpf("peek next line @ {}: (len=0)''", pos);
+ return {};
+}
+
+
+//-----------------------------------------------------------------------------
+void Parser::LineContents::reset_with_next_line(csubstr buf, size_t offset)
+{
+ RYML_ASSERT(offset <= buf.len);
+ char const* C4_RESTRICT b = &buf[offset];
+ char const* C4_RESTRICT e = b;
+ // get the current line stripped of newline chars
+ while(e < buf.end() && (*e != '\n' && *e != '\r'))
+ ++e;
+ RYML_ASSERT(e >= b);
+ const csubstr stripped_ = buf.sub(offset, static_cast<size_t>(e - b));
+ // advance pos to include the first line ending
+ if(e != buf.end() && *e == '\r')
+ ++e;
+ if(e != buf.end() && *e == '\n')
+ ++e;
+ RYML_ASSERT(e >= b);
+ const csubstr full_ = buf.sub(offset, static_cast<size_t>(e - b));
+ reset(full_, stripped_);
+}
+
+void Parser::_scan_line()
+{
+ if(m_state->pos.offset >= m_buf.len)
+ {
+ m_state->line_contents.reset(m_buf.last(0), m_buf.last(0));
+ return;
+ }
+ m_state->line_contents.reset_with_next_line(m_buf, m_state->pos.offset);
+}
+
+
+//-----------------------------------------------------------------------------
+void Parser::_line_progressed(size_t ahead)
+{
+ _c4dbgpf("line[{}] ({} cols) progressed by {}: col {}-->{} offset {}-->{}", m_state->pos.line, m_state->line_contents.full.len, ahead, m_state->pos.col, m_state->pos.col+ahead, m_state->pos.offset, m_state->pos.offset+ahead);
+ m_state->pos.offset += ahead;
+ m_state->pos.col += ahead;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->pos.col <= m_state->line_contents.stripped.len+1);
+ m_state->line_contents.rem = m_state->line_contents.rem.sub(ahead);
+}
+
+void Parser::_line_ended()
+{
+ _c4dbgpf("line[{}] ({} cols) ended! offset {}-->{}", m_state->pos.line, m_state->line_contents.full.len, m_state->pos.offset, m_state->pos.offset+m_state->line_contents.full.len - m_state->line_contents.stripped.len);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->pos.col == m_state->line_contents.stripped.len+1);
+ m_state->pos.offset += m_state->line_contents.full.len - m_state->line_contents.stripped.len;
+ ++m_state->pos.line;
+ m_state->pos.col = 1;
+}
+
+void Parser::_line_ended_undo()
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->pos.col == 1u);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->pos.line > 0u);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->pos.offset >= m_state->line_contents.full.len - m_state->line_contents.stripped.len);
+ _c4dbgpf("line[{}] undo ended! line {}-->{}, offset {}-->{}", m_state->pos.line, m_state->pos.line, m_state->pos.line - 1, m_state->pos.offset, m_state->pos.offset - (m_state->line_contents.full.len - m_state->line_contents.stripped.len));
+ m_state->pos.offset -= m_state->line_contents.full.len - m_state->line_contents.stripped.len;
+ --m_state->pos.line;
+ m_state->pos.col = m_state->line_contents.stripped.len + 1u;
+}
+
+//-----------------------------------------------------------------------------
+void Parser::_set_indentation(size_t indentation)
+{
+ m_state->indref = indentation;
+ _c4dbgpf("state[{}]: saving indentation: {}", m_state-m_stack.begin(), m_state->indref);
+}
+
+void Parser::_save_indentation(size_t behind)
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->line_contents.rem.begin() >= m_state->line_contents.full.begin());
+ m_state->indref = static_cast<size_t>(m_state->line_contents.rem.begin() - m_state->line_contents.full.begin());
+ _RYML_CB_ASSERT(m_stack.m_callbacks, behind <= m_state->indref);
+ m_state->indref -= behind;
+ _c4dbgpf("state[{}]: saving indentation: {}", m_state-m_stack.begin(), m_state->indref);
+}
+
+bool Parser::_maybe_set_indentation_from_anchor_or_tag()
+{
+ if(m_key_anchor.not_empty())
+ {
+ _c4dbgpf("set indentation from key anchor: {}", m_key_anchor_indentation);
+ _set_indentation(m_key_anchor_indentation); // this is the column where the anchor starts
+ return true;
+ }
+ else if(m_key_tag.not_empty())
+ {
+ _c4dbgpf("set indentation from key tag: {}", m_key_tag_indentation);
+ _set_indentation(m_key_tag_indentation); // this is the column where the tag starts
+ return true;
+ }
+ return false;
+}
+
+
+//-----------------------------------------------------------------------------
+void Parser::_write_key_anchor(size_t node_id)
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->has_key(node_id));
+ if( ! m_key_anchor.empty())
+ {
+ _c4dbgpf("node={}: set key anchor to '{}'", node_id, m_key_anchor);
+ m_tree->set_key_anchor(node_id, m_key_anchor);
+ m_key_anchor.clear();
+ m_key_anchor_was_before = false;
+ m_key_anchor_indentation = 0;
+ }
+ else if( ! m_tree->is_key_quoted(node_id))
+ {
+ csubstr r = m_tree->key(node_id);
+ if(r.begins_with('*'))
+ {
+ _c4dbgpf("node={}: set key reference: '{}'", node_id, r);
+ m_tree->set_key_ref(node_id, r.sub(1));
+ }
+ else if(r == "<<")
+ {
+ m_tree->set_key_ref(node_id, r);
+ _c4dbgpf("node={}: it's an inheriting reference", node_id);
+ if(m_tree->is_seq(node_id))
+ {
+ _c4dbgpf("node={}: inheriting from seq of {}", node_id, m_tree->num_children(node_id));
+ for(size_t i = m_tree->first_child(node_id); i != NONE; i = m_tree->next_sibling(i))
+ {
+ if( ! (m_tree->val(i).begins_with('*')))
+ _c4err("malformed reference: '{}'", m_tree->val(i));
+ }
+ }
+ else if( ! m_tree->val(node_id).begins_with('*'))
+ {
+ _c4err("malformed reference: '{}'", m_tree->val(node_id));
+ }
+ //m_tree->set_key_ref(node_id, r);
+ }
+ }
+}
+
+//-----------------------------------------------------------------------------
+void Parser::_write_val_anchor(size_t node_id)
+{
+ if( ! m_val_anchor.empty())
+ {
+ _c4dbgpf("node={}: set val anchor to '{}'", node_id, m_val_anchor);
+ m_tree->set_val_anchor(node_id, m_val_anchor);
+ m_val_anchor.clear();
+ }
+ csubstr r = m_tree->has_val(node_id) ? m_tree->val(node_id) : "";
+ if(!m_tree->is_val_quoted(node_id) && r.begins_with('*'))
+ {
+ _c4dbgpf("node={}: set val reference: '{}'", node_id, r);
+ RYML_CHECK(!m_tree->has_val_anchor(node_id));
+ m_tree->set_val_ref(node_id, r.sub(1));
+ }
+}
+
+//-----------------------------------------------------------------------------
+void Parser::_push_level(bool explicit_flow_chars)
+{
+ _c4dbgpf("pushing level! currnode={} currlevel={} stacksize={} stackcap={}", m_state->node_id, m_state->level, m_stack.size(), m_stack.capacity());
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state == &m_stack.top());
+ if(node(m_state) == nullptr)
+ {
+ _c4dbgp("pushing level! actually no, current node is null");
+ //_RYML_CB_ASSERT(m_stack.m_callbacks, ! explicit_flow_chars);
+ return;
+ }
+ flag_t st = RUNK;
+ if(explicit_flow_chars || has_all(FLOW))
+ {
+ st |= FLOW;
+ }
+ m_stack.push_top();
+ m_state = &m_stack.top();
+ set_flags(st);
+ m_state->node_id = (size_t)NONE;
+ m_state->indref = (size_t)NONE;
+ ++m_state->level;
+ _c4dbgpf("pushing level: now, currlevel={}", m_state->level);
+}
+
+void Parser::_pop_level()
+{
+ _c4dbgpf("popping level! currnode={} currlevel={}", m_state->node_id, m_state->level);
+ if(has_any(RMAP) || m_tree->is_map(m_state->node_id))
+ {
+ _stop_map();
+ }
+ if(has_any(RSEQ) || m_tree->is_seq(m_state->node_id))
+ {
+ _stop_seq();
+ }
+ if(m_tree->is_doc(m_state->node_id))
+ {
+ _stop_doc();
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_stack.size() > 1);
+ _prepare_pop();
+ m_stack.pop();
+ m_state = &m_stack.top();
+ /*if(has_any(RMAP))
+ {
+ _toggle_key_val();
+ }*/
+ if(m_state->line_contents.indentation == 0)
+ {
+ //_RYML_CB_ASSERT(m_stack.m_callbacks, has_none(RTOP));
+ add_flags(RTOP);
+ }
+ _c4dbgpf("popping level: now, currnode={} currlevel={}", m_state->node_id, m_state->level);
+}
+
+//-----------------------------------------------------------------------------
+void Parser::_start_unk(bool /*as_child*/)
+{
+ _c4dbgp("start_unk");
+ _push_level();
+ _move_scalar_from_top();
+}
+
+//-----------------------------------------------------------------------------
+void Parser::_start_doc(bool as_child)
+{
+ _c4dbgpf("start_doc (as child={})", as_child);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, node(m_stack.bottom()) == node(m_root_id));
+ size_t parent_id = m_stack.size() < 2 ? m_root_id : m_stack.top(1).node_id;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, parent_id != NONE);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_root(parent_id));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, node(m_state) == nullptr || node(m_state) == node(m_root_id));
+ if(as_child)
+ {
+ _c4dbgpf("start_doc: parent={}", parent_id);
+ if( ! m_tree->is_stream(parent_id))
+ {
+ _c4dbgp("start_doc: rearranging with root as STREAM");
+ m_tree->set_root_as_stream();
+ }
+ m_state->node_id = m_tree->append_child(parent_id);
+ m_tree->to_doc(m_state->node_id);
+ }
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_seq(parent_id) || m_tree->empty(parent_id));
+ m_state->node_id = parent_id;
+ if( ! m_tree->is_doc(parent_id))
+ {
+ m_tree->to_doc(parent_id, DOC);
+ }
+ }
+ #endif
+ _c4dbgpf("start_doc: id={}", m_state->node_id);
+ add_flags(RUNK|RTOP|NDOC);
+ _handle_types();
+ rem_flags(NDOC);
+}
+
+void Parser::_stop_doc()
+{
+ size_t doc_node = m_state->node_id;
+ _c4dbgpf("stop_doc[{}]", doc_node);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_doc(doc_node));
+ if(!m_tree->is_seq(doc_node) && !m_tree->is_map(doc_node) && !m_tree->is_val(doc_node))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(SSCL));
+ _c4dbgpf("stop_doc[{}]: there was nothing; adding null val", doc_node);
+ m_tree->to_val(doc_node, {}, DOC);
+ }
+}
+
+void Parser::_end_stream()
+{
+ _c4dbgpf("end_stream, level={} node_id={}", m_state->level, m_state->node_id);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ! m_stack.empty());
+ NodeData *added = nullptr;
+ if(has_any(SSCL))
+ {
+ if(m_tree->is_seq(m_state->node_id))
+ {
+ _c4dbgp("append val...");
+ added = _append_val(_consume_scalar());
+ }
+ else if(m_tree->is_map(m_state->node_id))
+ {
+ _c4dbgp("append null key val...");
+ added = _append_key_val_null(m_state->line_contents.rem.str);
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ if(has_any(RSEQIMAP))
+ {
+ _stop_seqimap();
+ _pop_level();
+ }
+ #endif
+ }
+ else if(m_tree->is_doc(m_state->node_id) || m_tree->type(m_state->node_id) == NOTYPE)
+ {
+ NodeType_e quoted = has_any(QSCL) ? VALQUO : NOTYPE; // do this before consuming the scalar
+ csubstr scalar = _consume_scalar();
+ _c4dbgpf("node[{}]: to docval '{}'{}", m_state->node_id, scalar, quoted == VALQUO ? ", quoted" : "");
+ m_tree->to_val(m_state->node_id, scalar, DOC|quoted);
+ added = m_tree->get(m_state->node_id);
+ }
+ else
+ {
+ _c4err("internal error");
+ }
+ }
+ else if(has_all(RSEQ|RVAL) && has_none(FLOW))
+ {
+ _c4dbgp("add last...");
+ added = _append_val_null(m_state->line_contents.rem.str);
+ }
+ else if(!m_val_tag.empty() && (m_tree->is_doc(m_state->node_id) || m_tree->type(m_state->node_id) == NOTYPE))
+ {
+ csubstr scalar = m_state->line_contents.rem.first(0);
+ _c4dbgpf("node[{}]: add null scalar as docval", m_state->node_id);
+ m_tree->to_val(m_state->node_id, scalar, DOC);
+ added = m_tree->get(m_state->node_id);
+ }
+
+ if(added)
+ {
+ size_t added_id = m_tree->id(added);
+ if(m_tree->is_seq(m_state->node_id) || m_tree->is_doc(m_state->node_id))
+ {
+ if(!m_key_anchor.empty())
+ {
+ _c4dbgpf("node[{}]: move key to val anchor: '{}'", added_id, m_key_anchor);
+ m_val_anchor = m_key_anchor;
+ m_key_anchor = {};
+ }
+ if(!m_key_tag.empty())
+ {
+ _c4dbgpf("node[{}]: move key to val tag: '{}'", added_id, m_key_tag);
+ m_val_tag = m_key_tag;
+ m_key_tag = {};
+ }
+ }
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ if(!m_key_anchor.empty())
+ {
+ _c4dbgpf("node[{}]: set key anchor='{}'", added_id, m_key_anchor);
+ m_tree->set_key_anchor(added_id, m_key_anchor);
+ m_key_anchor = {};
+ }
+ #endif
+ if(!m_val_anchor.empty())
+ {
+ _c4dbgpf("node[{}]: set val anchor='{}'", added_id, m_val_anchor);
+ m_tree->set_val_anchor(added_id, m_val_anchor);
+ m_val_anchor = {};
+ }
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ if(!m_key_tag.empty())
+ {
+ _c4dbgpf("node[{}]: set key tag='{}' -> '{}'", added_id, m_key_tag, normalize_tag(m_key_tag));
+ m_tree->set_key_tag(added_id, normalize_tag(m_key_tag));
+ m_key_tag = {};
+ }
+ #endif
+ if(!m_val_tag.empty())
+ {
+ _c4dbgpf("node[{}]: set val tag='{}' -> '{}'", added_id, m_val_tag, normalize_tag(m_val_tag));
+ m_tree->set_val_tag(added_id, normalize_tag(m_val_tag));
+ m_val_tag = {};
+ }
+ }
+
+ while(m_stack.size() > 1)
+ {
+ _c4dbgpf("popping level: {} (stack sz={})", m_state->level, m_stack.size());
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ! has_any(SSCL, &m_stack.top()));
+ if(has_all(RSEQ|FLOW))
+ _err("closing ] not found");
+ _pop_level();
+ }
+ add_flags(NDOC);
+}
+
+void Parser::_start_new_doc(csubstr rem)
+{
+ _c4dbgp("_start_new_doc");
+ _RYML_CB_ASSERT(m_stack.m_callbacks, rem.begins_with("---"));
+ C4_UNUSED(rem);
+
+ _end_stream();
+
+ size_t indref = m_state->indref;
+ _c4dbgpf("start a document, indentation={}", indref);
+ _line_progressed(3);
+ _push_level();
+ _start_doc();
+ _set_indentation(indref);
+}
+
+
+//-----------------------------------------------------------------------------
+void Parser::_start_map(bool as_child)
+{
+ _c4dbgpf("start_map (as child={})", as_child);
+ addrem_flags(RMAP|RVAL, RKEY|RUNK);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, node(m_stack.bottom()) == node(m_root_id));
+ size_t parent_id = m_stack.size() < 2 ? m_root_id : m_stack.top(1).node_id;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, parent_id != NONE);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, node(m_state) == nullptr || node(m_state) == node(m_root_id));
+ if(as_child)
+ {
+ m_state->node_id = m_tree->append_child(parent_id);
+ if(has_all(SSCL))
+ {
+ type_bits key_quoted = NOTYPE;
+ if(m_state->flags & QSCL) // before consuming the scalar
+ key_quoted |= KEYQUO;
+ csubstr key = _consume_scalar();
+ m_tree->to_map(m_state->node_id, key, key_quoted);
+ _c4dbgpf("start_map: id={} key='{}'", m_state->node_id, m_tree->key(m_state->node_id));
+ _write_key_anchor(m_state->node_id);
+ if( ! m_key_tag.empty())
+ {
+ _c4dbgpf("node[{}]: set key tag='{}' -> '{}'", m_state->node_id, m_key_tag, normalize_tag(m_key_tag));
+ m_tree->set_key_tag(m_state->node_id, normalize_tag(m_key_tag));
+ m_key_tag.clear();
+ }
+ }
+ else
+ {
+ m_tree->to_map(m_state->node_id);
+ _c4dbgpf("start_map: id={}", m_state->node_id);
+ }
+ m_tree->_p(m_state->node_id)->m_val.scalar.str = m_state->line_contents.rem.str;
+ _write_val_anchor(m_state->node_id);
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, parent_id != NONE);
+ m_state->node_id = parent_id;
+ _c4dbgpf("start_map: id={}", m_state->node_id);
+ type_bits as_doc = 0;
+ if(m_tree->is_doc(m_state->node_id))
+ as_doc |= DOC;
+ if(!m_tree->is_map(parent_id))
+ {
+ RYML_CHECK(!m_tree->has_children(parent_id));
+ m_tree->to_map(parent_id, as_doc);
+ }
+ else
+ {
+ m_tree->_add_flags(parent_id, as_doc);
+ }
+ _move_scalar_from_top();
+ if(m_key_anchor.not_empty())
+ m_key_anchor_was_before = true;
+ _write_val_anchor(parent_id);
+ if(m_stack.size() >= 2)
+ {
+ State const& parent_state = m_stack.top(1);
+ if(parent_state.flags & RSET)
+ add_flags(RSET);
+ }
+ m_tree->_p(parent_id)->m_val.scalar.str = m_state->line_contents.rem.str;
+ }
+ if( ! m_val_tag.empty())
+ {
+ _c4dbgpf("node[{}]: set val tag='{}' -> '{}'", m_state->node_id, m_val_tag, normalize_tag(m_val_tag));
+ m_tree->set_val_tag(m_state->node_id, normalize_tag(m_val_tag));
+ m_val_tag.clear();
+ }
+}
+
+void Parser::_start_map_unk(bool as_child)
+{
+ if(!m_key_anchor_was_before)
+ {
+ _c4dbgpf("stash key anchor before starting map... '{}'", m_key_anchor);
+ csubstr ka = m_key_anchor;
+ m_key_anchor = {};
+ _start_map(as_child);
+ m_key_anchor = ka;
+ }
+ else
+ {
+ _start_map(as_child);
+ m_key_anchor_was_before = false;
+ }
+ if(m_key_tag2.not_empty())
+ {
+ m_key_tag = m_key_tag2;
+ m_key_tag_indentation = m_key_tag2_indentation;
+ m_key_tag2.clear();
+ m_key_tag2_indentation = 0;
+ }
+}
+
+void Parser::_stop_map()
+{
+ _c4dbgpf("stop_map[{}]", m_state->node_id);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_map(m_state->node_id));
+ if(has_all(QMRK|RKEY) && !has_all(SSCL))
+ {
+ _c4dbgpf("stop_map[{}]: RKEY", m_state->node_id);
+ _store_scalar_null(m_state->line_contents.rem.str);
+ _append_key_val_null(m_state->line_contents.rem.str);
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+void Parser::_start_seq(bool as_child)
+{
+ _c4dbgpf("start_seq (as child={})", as_child);
+ if(has_all(RTOP|RUNK))
+ {
+ _c4dbgpf("start_seq: moving key tag to val tag: '{}'", m_key_tag);
+ m_val_tag = m_key_tag;
+ m_key_tag.clear();
+ }
+ addrem_flags(RSEQ|RVAL, RUNK);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, node(m_stack.bottom()) == node(m_root_id));
+ size_t parent_id = m_stack.size() < 2 ? m_root_id : m_stack.top(1).node_id;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, parent_id != NONE);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, node(m_state) == nullptr || node(m_state) == node(m_root_id));
+ if(as_child)
+ {
+ m_state->node_id = m_tree->append_child(parent_id);
+ if(has_all(SSCL))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_map(parent_id));
+ type_bits key_quoted = 0;
+ if(m_state->flags & QSCL) // before consuming the scalar
+ key_quoted |= KEYQUO;
+ csubstr key = _consume_scalar();
+ m_tree->to_seq(m_state->node_id, key, key_quoted);
+ _c4dbgpf("start_seq: id={} name='{}'", m_state->node_id, m_tree->key(m_state->node_id));
+ _write_key_anchor(m_state->node_id);
+ if( ! m_key_tag.empty())
+ {
+ _c4dbgpf("start_seq[{}]: set key tag='{}' -> '{}'", m_state->node_id, m_key_tag, normalize_tag(m_key_tag));
+ m_tree->set_key_tag(m_state->node_id, normalize_tag(m_key_tag));
+ m_key_tag.clear();
+ }
+ }
+ else
+ {
+ type_bits as_doc = 0;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, !m_tree->is_doc(m_state->node_id));
+ m_tree->to_seq(m_state->node_id, as_doc);
+ _c4dbgpf("start_seq: id={}{}", m_state->node_id, as_doc ? " as doc" : "");
+ }
+ _write_val_anchor(m_state->node_id);
+ m_tree->_p(m_state->node_id)->m_val.scalar.str = m_state->line_contents.rem.str;
+ }
+ else
+ {
+ m_state->node_id = parent_id;
+ type_bits as_doc = 0;
+ if(m_tree->is_doc(m_state->node_id))
+ as_doc |= DOC;
+ if(!m_tree->is_seq(parent_id))
+ {
+ RYML_CHECK(!m_tree->has_children(parent_id));
+ m_tree->to_seq(parent_id, as_doc);
+ }
+ else
+ {
+ m_tree->_add_flags(parent_id, as_doc);
+ }
+ _move_scalar_from_top();
+ _c4dbgpf("start_seq: id={}{}", m_state->node_id, as_doc ? " as_doc" : "");
+ _write_val_anchor(parent_id);
+ m_tree->_p(parent_id)->m_val.scalar.str = m_state->line_contents.rem.str;
+ }
+ if( ! m_val_tag.empty())
+ {
+ _c4dbgpf("start_seq[{}]: set val tag='{}' -> '{}'", m_state->node_id, m_val_tag, normalize_tag(m_val_tag));
+ m_tree->set_val_tag(m_state->node_id, normalize_tag(m_val_tag));
+ m_val_tag.clear();
+ }
+}
+
+void Parser::_stop_seq()
+{
+ _c4dbgp("stop_seq");
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_seq(m_state->node_id));
+}
+
+
+//-----------------------------------------------------------------------------
+void Parser::_start_seqimap()
+{
+ _c4dbgpf("start_seqimap at node={}. has_children={}", m_state->node_id, m_tree->has_children(m_state->node_id));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(RSEQ|FLOW));
+ // create a map, and turn the last scalar of this sequence
+ // into the key of the map's first child. This scalar was
+ // understood to be a value in the sequence, but it is
+ // actually a key of a map, implicitly opened here.
+ // Eg [val, key: val]
+ //
+ // Yep, YAML is crazy.
+ if(m_tree->has_children(m_state->node_id) && m_tree->has_val(m_tree->last_child(m_state->node_id)))
+ {
+ size_t prev = m_tree->last_child(m_state->node_id);
+ NodeType ty = m_tree->_p(prev)->m_type; // don't use type() because it masks out the quotes
+ NodeScalar tmp = m_tree->valsc(prev);
+ _c4dbgpf("has children and last child={} has val. saving the scalars, val='{}' quoted={}", prev, tmp.scalar, ty.is_val_quoted());
+ m_tree->remove(prev);
+ _push_level();
+ _start_map();
+ _store_scalar(tmp.scalar, ty.is_val_quoted());
+ m_key_anchor = tmp.anchor;
+ m_key_tag = tmp.tag;
+ }
+ else
+ {
+ _c4dbgpf("node {} has no children yet, using empty key", m_state->node_id);
+ _push_level();
+ _start_map();
+ _store_scalar_null(m_state->line_contents.rem.str);
+ }
+ add_flags(RSEQIMAP|FLOW);
+}
+
+void Parser::_stop_seqimap()
+{
+ _c4dbgp("stop_seqimap");
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(RSEQIMAP));
+}
+
+
+//-----------------------------------------------------------------------------
+NodeData* Parser::_append_val(csubstr val, flag_t quoted)
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ! has_all(SSCL));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, node(m_state) != nullptr);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_seq(m_state->node_id));
+ type_bits additional_flags = quoted ? VALQUO : NOTYPE;
+ _c4dbgpf("append val: '{}' to parent id={} (level={}){}", val, m_state->node_id, m_state->level, quoted ? " VALQUO!" : "");
+ size_t nid = m_tree->append_child(m_state->node_id);
+ m_tree->to_val(nid, val, additional_flags);
+
+ _c4dbgpf("append val: id={} val='{}'", nid, m_tree->get(nid)->m_val.scalar);
+ if( ! m_val_tag.empty())
+ {
+ _c4dbgpf("append val[{}]: set val tag='{}' -> '{}'", nid, m_val_tag, normalize_tag(m_val_tag));
+ m_tree->set_val_tag(nid, normalize_tag(m_val_tag));
+ m_val_tag.clear();
+ }
+ _write_val_anchor(nid);
+ return m_tree->get(nid);
+}
+
+NodeData* Parser::_append_key_val(csubstr val, flag_t val_quoted)
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_tree->is_map(m_state->node_id));
+ type_bits additional_flags = 0;
+ if(m_state->flags & QSCL)
+ additional_flags |= KEYQUO;
+ if(val_quoted)
+ additional_flags |= VALQUO;
+
+ csubstr key = _consume_scalar();
+ _c4dbgpf("append keyval: '{}' '{}' to parent id={} (level={}){}{}", key, val, m_state->node_id, m_state->level, (additional_flags & KEYQUO) ? " KEYQUO!" : "", (additional_flags & VALQUO) ? " VALQUO!" : "");
+ size_t nid = m_tree->append_child(m_state->node_id);
+ m_tree->to_keyval(nid, key, val, additional_flags);
+ _c4dbgpf("append keyval: id={} key='{}' val='{}'", nid, m_tree->key(nid), m_tree->val(nid));
+ if( ! m_key_tag.empty())
+ {
+ _c4dbgpf("append keyval[{}]: set key tag='{}' -> '{}'", nid, m_key_tag, normalize_tag(m_key_tag));
+ m_tree->set_key_tag(nid, normalize_tag(m_key_tag));
+ m_key_tag.clear();
+ }
+ if( ! m_val_tag.empty())
+ {
+ _c4dbgpf("append keyval[{}]: set val tag='{}' -> '{}'", nid, m_val_tag, normalize_tag(m_val_tag));
+ m_tree->set_val_tag(nid, normalize_tag(m_val_tag));
+ m_val_tag.clear();
+ }
+ _write_key_anchor(nid);
+ _write_val_anchor(nid);
+ rem_flags(QMRK);
+ return m_tree->get(nid);
+}
+
+
+//-----------------------------------------------------------------------------
+void Parser::_store_scalar(csubstr s, flag_t is_quoted)
+{
+ _c4dbgpf("state[{}]: storing scalar '{}' (flag: {}) (old scalar='{}')",
+ m_state-m_stack.begin(), s, m_state->flags & SSCL, m_state->scalar);
+ RYML_CHECK(has_none(SSCL));
+ add_flags(SSCL | (is_quoted * QSCL));
+ m_state->scalar = s;
+}
+
+csubstr Parser::_consume_scalar()
+{
+ _c4dbgpf("state[{}]: consuming scalar '{}' (flag: {}))", m_state-m_stack.begin(), m_state->scalar, m_state->flags & SSCL);
+ RYML_CHECK(m_state->flags & SSCL);
+ csubstr s = m_state->scalar;
+ rem_flags(SSCL | QSCL);
+ m_state->scalar.clear();
+ return s;
+}
+
+void Parser::_move_scalar_from_top()
+{
+ if(m_stack.size() < 2) return;
+ State &prev = m_stack.top(1);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state == &m_stack.top());
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state != &prev);
+ if(prev.flags & SSCL)
+ {
+ _c4dbgpf("moving scalar '{}' from state[{}] to state[{}] (overwriting '{}')", prev.scalar, &prev-m_stack.begin(), m_state-m_stack.begin(), m_state->scalar);
+ add_flags(prev.flags & (SSCL | QSCL));
+ m_state->scalar = prev.scalar;
+ rem_flags(SSCL | QSCL, &prev);
+ prev.scalar.clear();
+ }
+}
+
+//-----------------------------------------------------------------------------
+/** @todo this function is a monster and needs love. */
+bool Parser::_handle_indentation()
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(FLOW));
+ if( ! _at_line_begin())
+ return false;
+
+ size_t ind = m_state->line_contents.indentation;
+ csubstr rem = m_state->line_contents.rem;
+ /** @todo instead of trimming, we should use the indentation index from above */
+ csubstr remt = rem.triml(' ');
+
+ if(remt.empty() || remt.begins_with('#')) // this is a blank or comment line
+ {
+ _line_progressed(rem.size());
+ return true;
+ }
+
+ _c4dbgpf("indentation? ind={} indref={}", ind, m_state->indref);
+ if(ind == m_state->indref)
+ {
+ if(has_all(SSCL|RVAL) && ! rem.sub(ind).begins_with('-'))
+ {
+ if(has_all(RMAP))
+ {
+ _append_key_val_null(rem.str + ind - 1);
+ addrem_flags(RKEY, RVAL);
+ }
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ else if(has_all(RSEQ))
+ {
+ _append_val(_consume_scalar());
+ addrem_flags(RNXT, RVAL);
+ }
+ else
+ {
+ _c4err("internal error");
+ }
+ #endif
+ }
+ else if(has_all(RSEQ|RNXT) && ! rem.sub(ind).begins_with('-'))
+ {
+ if(m_stack.size() > 2) // do not pop to root level
+ {
+ _c4dbgp("end the indentless seq");
+ _pop_level();
+ return true;
+ }
+ }
+ else
+ {
+ _c4dbgpf("same indentation ({}) -- nothing to see here", ind);
+ }
+ _line_progressed(ind);
+ return ind > 0;
+ }
+ else if(ind < m_state->indref)
+ {
+ _c4dbgpf("smaller indentation ({} < {})!!!", ind, m_state->indref);
+ if(has_all(RVAL))
+ {
+ _c4dbgp("there was an empty val -- appending");
+ if(has_all(RMAP))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_all(SSCL));
+ _append_key_val_null(rem.sub(ind).str - 1);
+ }
+ else if(has_all(RSEQ))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, has_none(SSCL));
+ _append_val_null(rem.sub(ind).str - 1);
+ }
+ }
+ // search the stack frame to jump to based on its indentation
+ State const* popto = nullptr;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_stack.is_contiguous()); // this search relies on the stack being contiguous
+ for(State const* s = m_state-1; s >= m_stack.begin(); --s)
+ {
+ _c4dbgpf("searching for state with indentation {}. curr={} (level={},node={})", ind, s->indref, s->level, s->node_id);
+ if(s->indref == ind)
+ {
+ _c4dbgpf("gotit!!! level={} node={}", s->level, s->node_id);
+ popto = s;
+ // while it may be tempting to think we're done at this
+ // point, we must still determine whether we're jumping to a
+ // parent with the same indentation. Consider this case with
+ // an indentless sequence:
+ //
+ // product:
+ // - sku: BL394D
+ // quantity: 4
+ // description: Basketball
+ // price: 450.00
+ // - sku: BL4438H
+ // quantity: 1
+ // description: Super Hoop
+ // price: 2392.00 # jumping one level here would be wrong.
+ // tax: 1234.5 # we must jump two levels
+ if(popto > m_stack.begin())
+ {
+ auto parent = popto - 1;
+ if(parent->indref == popto->indref)
+ {
+ _c4dbgpf("the parent (level={},node={}) has the same indentation ({}). is this in an indentless sequence?", parent->level, parent->node_id, popto->indref);
+ _c4dbgpf("isseq(popto)={} ismap(parent)={}", m_tree->is_seq(popto->node_id), m_tree->is_map(parent->node_id));
+ if(m_tree->is_seq(popto->node_id) && m_tree->is_map(parent->node_id))
+ {
+ if( ! remt.begins_with('-'))
+ {
+ _c4dbgp("this is an indentless sequence");
+ popto = parent;
+ }
+ else
+ {
+ _c4dbgp("not an indentless sequence");
+ }
+ }
+ }
+ }
+ break;
+ }
+ }
+ if(!popto || popto >= m_state || popto->level >= m_state->level)
+ {
+ _c4err("parse error: incorrect indentation?");
+ }
+ _c4dbgpf("popping {} levels: from level {} to level {}", m_state->level-popto->level, m_state->level, popto->level);
+ while(m_state != popto)
+ {
+ _c4dbgpf("popping level {} (indentation={})", m_state->level, m_state->indref);
+ _pop_level();
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ind == m_state->indref);
+ _line_progressed(ind);
+ return true;
+ }
+ else
+ {
+ _c4dbgpf("larger indentation ({} > {})!!!", ind, m_state->indref);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ind > m_state->indref);
+ if(has_all(RMAP|RVAL))
+ {
+ if(_is_scalar_next__rmap_val(remt) && remt.first_of(":?") == npos)
+ {
+ _c4dbgpf("actually it seems a value: '{}'", remt);
+ }
+ else
+ {
+ addrem_flags(RKEY, RVAL);
+ _start_unk();
+ //_move_scalar_from_top();
+ _line_progressed(ind);
+ _save_indentation();
+ return true;
+ }
+ }
+ else if(has_all(RSEQ|RVAL))
+ {
+ // nothing to do here
+ }
+ else
+ {
+ _c4err("parse error - indentation should not increase at this point");
+ }
+ }
+
+ return false;
+}
+
+//-----------------------------------------------------------------------------
+csubstr Parser::_scan_comment()
+{
+ csubstr s = m_state->line_contents.rem;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.begins_with('#'));
+ _line_progressed(s.len);
+ // skip the # character
+ s = s.sub(1);
+ // skip leading whitespace
+ s = s.right_of(s.first_not_of(' '), /*include_pos*/true);
+ _c4dbgpf("comment was '{}'", s);
+ return s;
+}
+
+//-----------------------------------------------------------------------------
+csubstr Parser::_scan_squot_scalar()
+{
+ // quoted scalars can spread over multiple lines!
+ // nice explanation here: http://yaml-multiline.info/
+
+ // a span to the end of the file
+ size_t b = m_state->pos.offset;
+ substr s = m_buf.sub(b);
+ if(s.begins_with(' '))
+ {
+ s = s.triml(' ');
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_buf.sub(b).is_super(s));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.begin() >= m_buf.sub(b).begin());
+ _line_progressed((size_t)(s.begin() - m_buf.sub(b).begin()));
+ }
+ b = m_state->pos.offset; // take this into account
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.begins_with('\''));
+
+ // skip the opening quote
+ _line_progressed(1);
+ s = s.sub(1);
+
+ bool needs_filter = false;
+
+ size_t numlines = 1; // we already have one line
+ size_t pos = npos; // find the pos of the matching quote
+ while( ! _finished_file())
+ {
+ const csubstr line = m_state->line_contents.rem;
+ bool line_is_blank = true;
+ _c4dbgpf("scanning single quoted scalar @ line[{}]: ~~~{}~~~", m_state->pos.line, line);
+ for(size_t i = 0; i < line.len; ++i)
+ {
+ const char curr = line.str[i];
+ if(curr == '\'') // single quotes are escaped with two single quotes
+ {
+ const char next = i+1 < line.len ? line.str[i+1] : '~';
+ if(next != '\'') // so just look for the first quote
+ { // without another after it
+ pos = i;
+ break;
+ }
+ else
+ {
+ needs_filter = true; // needs filter to remove escaped quotes
+ ++i; // skip the escaped quote
+ }
+ }
+ else if(curr != ' ')
+ {
+ line_is_blank = false;
+ }
+ }
+
+ // leading whitespace also needs filtering
+ needs_filter = needs_filter
+ || numlines > 1
+ || line_is_blank
+ || (_at_line_begin() && line.begins_with(' '))
+ || (m_state->line_contents.full.last_of('\r') != csubstr::npos);
+
+ if(pos == npos)
+ {
+ _line_progressed(line.len);
+ ++numlines;
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos >= 0 && pos < m_buf.len);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_buf[m_state->pos.offset + pos] == '\'');
+ _line_progressed(pos + 1); // progress beyond the quote
+ pos = m_state->pos.offset - b - 1; // but we stop before it
+ break;
+ }
+
+ _line_ended();
+ _scan_line();
+ }
+
+ if(pos == npos)
+ {
+ _c4err("reached end of file while looking for closing quote");
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos > 0);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.end() >= m_buf.begin() && s.end() <= m_buf.end());
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.end() == m_buf.end() || *s.end() == '\'');
+ s = s.sub(0, pos-1);
+ }
+
+ if(needs_filter)
+ {
+ csubstr ret = _filter_squot_scalar(s);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ret.len <= s.len || s.empty() || s.trim(' ').empty());
+ _c4dbgpf("final scalar: \"{}\"", ret);
+ return ret;
+ }
+
+ _c4dbgpf("final scalar: \"{}\"", s);
+
+ return s;
+}
+
+//-----------------------------------------------------------------------------
+csubstr Parser::_scan_dquot_scalar()
+{
+ // quoted scalars can spread over multiple lines!
+ // nice explanation here: http://yaml-multiline.info/
+
+ // a span to the end of the file
+ size_t b = m_state->pos.offset;
+ substr s = m_buf.sub(b);
+ if(s.begins_with(' '))
+ {
+ s = s.triml(' ');
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_buf.sub(b).is_super(s));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.begin() >= m_buf.sub(b).begin());
+ _line_progressed((size_t)(s.begin() - m_buf.sub(b).begin()));
+ }
+ b = m_state->pos.offset; // take this into account
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.begins_with('"'));
+
+ // skip the opening quote
+ _line_progressed(1);
+ s = s.sub(1);
+
+ bool needs_filter = false;
+
+ size_t numlines = 1; // we already have one line
+ size_t pos = npos; // find the pos of the matching quote
+ while( ! _finished_file())
+ {
+ const csubstr line = m_state->line_contents.rem;
+ bool line_is_blank = true;
+ _c4dbgpf("scanning double quoted scalar @ line[{}]: line='{}'", m_state->pos.line, line);
+ for(size_t i = 0; i < line.len; ++i)
+ {
+ const char curr = line.str[i];
+ if(curr != ' ')
+ line_is_blank = false;
+ // every \ is an escape
+ if(curr == '\\')
+ {
+ const char next = i+1 < line.len ? line.str[i+1] : '~';
+ needs_filter = true;
+ if(next == '"' || next == '\\')
+ ++i;
+ }
+ else if(curr == '"')
+ {
+ pos = i;
+ break;
+ }
+ }
+
+ // leading whitespace also needs filtering
+ needs_filter = needs_filter
+ || numlines > 1
+ || line_is_blank
+ || (_at_line_begin() && line.begins_with(' '))
+ || (m_state->line_contents.full.last_of('\r') != csubstr::npos);
+
+ if(pos == npos)
+ {
+ _line_progressed(line.len);
+ ++numlines;
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos >= 0 && pos < m_buf.len);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_buf[m_state->pos.offset + pos] == '"');
+ _line_progressed(pos + 1); // progress beyond the quote
+ pos = m_state->pos.offset - b - 1; // but we stop before it
+ break;
+ }
+
+ _line_ended();
+ _scan_line();
+ }
+
+ if(pos == npos)
+ {
+ _c4err("reached end of file looking for closing quote");
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos > 0);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.end() == m_buf.end() || *s.end() == '"');
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.end() >= m_buf.begin() && s.end() <= m_buf.end());
+ s = s.sub(0, pos-1);
+ }
+
+ if(needs_filter)
+ {
+ csubstr ret = _filter_dquot_scalar(s);
+ _c4dbgpf("final scalar: [{}]\"{}\"", ret.len, ret);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, ret.len <= s.len || s.empty() || s.trim(' ').empty());
+ return ret;
+ }
+
+ _c4dbgpf("final scalar: \"{}\"", s);
+
+ return s;
+}
+
+//-----------------------------------------------------------------------------
+csubstr Parser::_scan_block()
+{
+ // nice explanation here: http://yaml-multiline.info/
+ csubstr s = m_state->line_contents.rem;
+ csubstr trimmed = s.triml(' ');
+ if(trimmed.str > s.str)
+ {
+ _c4dbgp("skipping whitespace");
+ _RYML_CB_ASSERT(m_stack.m_callbacks, trimmed.str >= s.str);
+ _line_progressed(static_cast<size_t>(trimmed.str - s.str));
+ s = trimmed;
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.begins_with('|') || s.begins_with('>'));
+
+ _c4dbgpf("scanning block: specs=\"{}\"", s);
+
+ // parse the spec
+ BlockStyle_e newline = s.begins_with('>') ? BLOCK_FOLD : BLOCK_LITERAL;
+ BlockChomp_e chomp = CHOMP_CLIP; // default to clip unless + or - are used
+ size_t indentation = npos; // have to find out if no spec is given
+ csubstr digits;
+ if(s.len > 1)
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.begins_with_any("|>"));
+ csubstr t = s.sub(1);
+ _c4dbgpf("scanning block: spec is multichar: '{}'", t);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, t.len >= 1);
+ size_t pos = t.first_of("-+");
+ _c4dbgpf("scanning block: spec chomp char at {}", pos);
+ if(pos != npos)
+ {
+ if(t[pos] == '-')
+ chomp = CHOMP_STRIP;
+ else if(t[pos] == '+')
+ chomp = CHOMP_KEEP;
+ if(pos == 0)
+ t = t.sub(1);
+ else
+ t = t.first(pos);
+ }
+ // from here to the end, only digits are considered
+ digits = t.left_of(t.first_not_of("0123456789"));
+ if( ! digits.empty())
+ {
+ if( ! c4::atou(digits, &indentation))
+ _c4err("parse error: could not read decimal");
+ _c4dbgpf("scanning block: indentation specified: {}. add {} from curr state -> {}", indentation, m_state->indref, indentation+m_state->indref);
+ indentation += m_state->indref;
+ }
+ }
+
+ // finish the current line
+ _line_progressed(s.len);
+ _line_ended();
+ _scan_line();
+
+ _c4dbgpf("scanning block: style={} chomp={} indentation={}", newline==BLOCK_FOLD ? "fold" : "literal",
+ chomp==CHOMP_CLIP ? "clip" : (chomp==CHOMP_STRIP ? "strip" : "keep"), indentation);
+
+ // start with a zero-length block, already pointing at the right place
+ substr raw_block(m_buf.data() + m_state->pos.offset, size_t(0));// m_state->line_contents.full.sub(0, 0);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, raw_block.begin() == m_state->line_contents.full.begin());
+
+ // read every full line into a raw block,
+ // from which newlines are to be stripped as needed.
+ //
+ // If no explicit indentation was given, pick it from the first
+ // non-empty line. See
+ // https://yaml.org/spec/1.2.2/#8111-block-indentation-indicator
+ size_t num_lines = 0, first = m_state->pos.line, provisional_indentation = npos;
+ LineContents lc;
+ while(( ! _finished_file()))
+ {
+ // peek next line, but do not advance immediately
+ lc.reset_with_next_line(m_buf, m_state->pos.offset);
+ _c4dbgpf("scanning block: peeking at '{}'", lc.stripped);
+ // evaluate termination conditions
+ if(indentation != npos)
+ {
+ // stop when the line is deindented and not empty
+ if(lc.indentation < indentation && ( ! lc.rem.trim(" \t\r\n").empty()))
+ {
+ _c4dbgpf("scanning block: indentation decreased ref={} thisline={}", indentation, lc.indentation);
+ break;
+ }
+ else if(indentation == 0)
+ {
+ if((lc.rem == "..." || lc.rem.begins_with("... "))
+ ||
+ (lc.rem == "---" || lc.rem.begins_with("--- ")))
+ {
+ _c4dbgp("scanning block: stop. indentation=0 and stream ended");
+ break;
+ }
+ }
+ }
+ else
+ {
+ _c4dbgpf("scanning block: indentation ref not set. firstnonws={}", lc.stripped.first_not_of(' '));
+ if(lc.stripped.first_not_of(' ') != npos) // non-empty line
+ {
+ _c4dbgpf("scanning block: line not empty. indref={} indprov={} indentation={}", m_state->indref, provisional_indentation, lc.indentation);
+ if(provisional_indentation == npos)
+ {
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ if(lc.indentation < m_state->indref)
+ {
+ _c4dbgpf("scanning block: block terminated indentation={} < indref={}", lc.indentation, m_state->indref);
+ break;
+ }
+ else
+ #endif
+ if(lc.indentation == m_state->indref)
+ {
+ if(has_any(RSEQ|RMAP))
+ {
+ _c4dbgpf("scanning block: block terminated. reading container and indentation={}==indref={}", lc.indentation, m_state->indref);
+ break;
+ }
+ }
+ _c4dbgpf("scanning block: set indentation ref from this line: ref={}", lc.indentation);
+ indentation = lc.indentation;
+ }
+ else
+ {
+ if(lc.indentation >= provisional_indentation)
+ {
+ _c4dbgpf("scanning block: set indentation ref from provisional indentation: provisional_ref={}, thisline={}", provisional_indentation, lc.indentation);
+ //indentation = provisional_indentation ? provisional_indentation : lc.indentation;
+ indentation = lc.indentation;
+ }
+ else
+ {
+ break;
+ //_c4err("parse error: first non-empty block line should have at least the original indentation");
+ }
+ }
+ }
+ else // empty line
+ {
+ _c4dbgpf("scanning block: line empty or {} spaces. line_indentation={} prov_indentation={}", lc.stripped.len, lc.indentation, provisional_indentation);
+ if(provisional_indentation != npos)
+ {
+ if(lc.stripped.len >= provisional_indentation)
+ {
+ _c4dbgpf("scanning block: increase provisional_ref {} -> {}", provisional_indentation, lc.stripped.len);
+ provisional_indentation = lc.stripped.len;
+ }
+ #ifdef RYML_NO_COVERAGE__TO_BE_DELETED
+ else if(lc.indentation >= provisional_indentation && lc.indentation != npos)
+ {
+ _c4dbgpf("scanning block: increase provisional_ref {} -> {}", provisional_indentation, lc.indentation);
+ provisional_indentation = lc.indentation;
+ }
+ #endif
+ }
+ else
+ {
+ provisional_indentation = lc.indentation ? lc.indentation : has_any(RSEQ|RVAL);
+ _c4dbgpf("scanning block: initialize provisional_ref={}", provisional_indentation);
+ if(provisional_indentation == npos)
+ {
+ provisional_indentation = lc.stripped.len ? lc.stripped.len : has_any(RSEQ|RVAL);
+ _c4dbgpf("scanning block: initialize provisional_ref={}", provisional_indentation);
+ }
+ }
+ }
+ }
+ // advance now that we know the folded scalar continues
+ m_state->line_contents = lc;
+ _c4dbgpf("scanning block: append '{}'", m_state->line_contents.rem);
+ raw_block.len += m_state->line_contents.full.len;
+ _line_progressed(m_state->line_contents.rem.len);
+ _line_ended();
+ ++num_lines;
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_state->pos.line == (first + num_lines));
+ C4_UNUSED(num_lines);
+ C4_UNUSED(first);
+
+ if(indentation == npos)
+ {
+ _c4dbgpf("scanning block: set indentation from provisional: {}", provisional_indentation);
+ indentation = provisional_indentation;
+ }
+
+ if(num_lines)
+ _line_ended_undo();
+
+ _c4dbgpf("scanning block: raw=~~~{}~~~", raw_block);
+
+ // ok! now we strip the newlines and spaces according to the specs
+ s = _filter_block_scalar(raw_block, newline, chomp, indentation);
+
+ _c4dbgpf("scanning block: final=~~~{}~~~", s);
+
+ return s;
+}
+
+
+//-----------------------------------------------------------------------------
+
+template<bool backslash_is_escape, bool keep_trailing_whitespace>
+bool Parser::_filter_nl(substr r, size_t *C4_RESTRICT i, size_t *C4_RESTRICT pos, size_t indentation)
+{
+ // a debugging scaffold:
+ #if 0
+ #define _c4dbgfnl(fmt, ...) _c4dbgpf("filter_nl[{}]: " fmt, *i, __VA_ARGS__)
+ #else
+ #define _c4dbgfnl(...)
+ #endif
+
+ const char curr = r[*i];
+ bool replaced = false;
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, indentation != npos);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, curr == '\n');
+
+ _c4dbgfnl("found newline. sofar=[{}]~~~{}~~~", *pos, m_filter_arena.first(*pos));
+ size_t ii = *i;
+ size_t numnl_following = count_following_newlines(r, &ii, indentation);
+ if(numnl_following)
+ {
+ _c4dbgfnl("{} consecutive (empty) lines {} in the middle. totalws={}", 1+numnl_following, ii < r.len ? "in the middle" : "at the end", ii - *i);
+ for(size_t j = 0; j < numnl_following; ++j)
+ m_filter_arena.str[(*pos)++] = '\n';
+ }
+ else
+ {
+ if(r.first_not_of(" \t", *i+1) != npos)
+ {
+ m_filter_arena.str[(*pos)++] = ' ';
+ _c4dbgfnl("single newline. convert to space. ii={}/{}. sofar=[{}]~~~{}~~~", ii, r.len, *pos, m_filter_arena.first(*pos));
+ replaced = true;
+ }
+ else
+ {
+ if C4_IF_CONSTEXPR (keep_trailing_whitespace)
+ {
+ m_filter_arena.str[(*pos)++] = ' ';
+ _c4dbgfnl("single newline. convert to space. ii={}/{}. sofar=[{}]~~~{}~~~", ii, r.len, *pos, m_filter_arena.first(*pos));
+ replaced = true;
+ }
+ else
+ {
+ _c4dbgfnl("last newline, everything else is whitespace. ii={}/{}", ii, r.len);
+ *i = r.len;
+ }
+ }
+ if C4_IF_CONSTEXPR (backslash_is_escape)
+ {
+ if(ii < r.len && r.str[ii] == '\\')
+ {
+ const char next = ii+1 < r.len ? r.str[ii+1] : '\0';
+ if(next == ' ' || next == '\t')
+ {
+ _c4dbgfnl("extend skip to backslash{}", "");
+ ++ii;
+ }
+ }
+ }
+ }
+ *i = ii - 1; // correct for the loop increment
+
+ #undef _c4dbgfnl
+
+ return replaced;
+}
+
+
+//-----------------------------------------------------------------------------
+
+template<bool keep_trailing_whitespace>
+void Parser::_filter_ws(substr r, size_t *C4_RESTRICT i, size_t *C4_RESTRICT pos)
+{
+ // a debugging scaffold:
+ #if 0
+ #define _c4dbgfws(fmt, ...) _c4dbgpf("filt_nl[{}]: " fmt, *i, __VA_ARGS__)
+ #else
+ #define _c4dbgfws(...)
+ #endif
+
+ const char curr = r[*i];
+ _c4dbgfws("found whitespace '{}'", _c4prc(curr));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, curr == ' ' || curr == '\t');
+
+ size_t first = *i > 0 ? r.first_not_of(" \t", *i) : r.first_not_of(' ', *i);
+ if(first != npos)
+ {
+ if(r[first] == '\n' || r[first] == '\r') // skip trailing whitespace
+ {
+ _c4dbgfws("whitespace is trailing on line. firstnonws='{}'@{}", _c4prc(r[first]), first);
+ *i = first - 1; // correct for the loop increment
+ }
+ else // a legit whitespace
+ {
+ m_filter_arena.str[(*pos)++] = curr;
+ _c4dbgfws("legit whitespace. sofar=[{}]~~~{}~~~", *pos, m_filter_arena.first(*pos));
+ }
+ }
+ else
+ {
+ _c4dbgfws("... everything else is trailing whitespace{}", "");
+ if C4_IF_CONSTEXPR (keep_trailing_whitespace)
+ for(size_t j = *i; j < r.len; ++j)
+ m_filter_arena.str[(*pos)++] = r[j];
+ *i = r.len;
+ }
+
+ #undef _c4dbgfws
+}
+
+
+//-----------------------------------------------------------------------------
+csubstr Parser::_filter_plain_scalar(substr s, size_t indentation)
+{
+ // a debugging scaffold:
+ #if 0
+ #define _c4dbgfps(...) _c4dbgpf("filt_plain_scalar" __VA_ARGS__)
+ #else
+ #define _c4dbgfps(...)
+ #endif
+
+ _c4dbgfps("before=~~~{}~~~", s);
+
+ substr r = s.triml(" \t");
+ _grow_filter_arena(r.len);
+ size_t pos = 0; // the filtered size
+ bool filtered_chars = false;
+ for(size_t i = 0; i < r.len; ++i)
+ {
+ const char curr = r.str[i];
+ _c4dbgfps("[{}]: '{}'", i, _c4prc(curr));
+ if(curr == ' ' || curr == '\t')
+ {
+ _filter_ws</*keep_trailing_ws*/false>(r, &i, &pos);
+ }
+ else if(curr == '\n')
+ {
+ filtered_chars = _filter_nl</*backslash_is_escape*/false, /*keep_trailing_ws*/false>(r, &i, &pos, indentation);
+ }
+ else if(curr == '\r') // skip \r --- https://stackoverflow.com/questions/1885900
+ {
+ ;
+ }
+ else
+ {
+ m_filter_arena.str[pos++] = r[i];
+ }
+ }
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos <= m_filter_arena.len);
+ if(pos < r.len || filtered_chars)
+ {
+ r = _finish_filter_arena(r, pos);
+ }
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.len >= r.len);
+ _c4dbgfps("#filteredchars={} after=~~~{}~~~", s.len - r.len, r);
+
+ #undef _c4dbgfps
+ return r;
+}
+
+
+//-----------------------------------------------------------------------------
+csubstr Parser::_filter_squot_scalar(substr s)
+{
+ // a debugging scaffold:
+ #if 0
+ #define _c4dbgfsq(...) _c4dbgpf("filt_squo_scalar")
+ #else
+ #define _c4dbgfsq(...)
+ #endif
+
+ // from the YAML spec for double-quoted scalars:
+ // https://yaml.org/spec/1.2-old/spec.html#style/flow/single-quoted
+
+ _c4dbgfsq(": before=~~~{}~~~", s);
+
+ _grow_filter_arena(s.len);
+ substr r = s;
+ size_t pos = 0; // the filtered size
+ bool filtered_chars = false;
+ for(size_t i = 0; i < r.len; ++i)
+ {
+ const char curr = r[i];
+ _c4dbgfsq("[{}]: '{}'", i, _c4prc(curr));
+ if(curr == ' ' || curr == '\t')
+ {
+ _filter_ws</*keep_trailing_ws*/true>(r, &i, &pos);
+ }
+ else if(curr == '\n')
+ {
+ filtered_chars = _filter_nl</*backslash_is_escape*/false, /*keep_trailing_ws*/true>(r, &i, &pos, /*indentation*/0);
+ }
+ else if(curr == '\r') // skip \r --- https://stackoverflow.com/questions/1885900
+ {
+ ;
+ }
+ else if(curr == '\'')
+ {
+ char next = i+1 < r.len ? r[i+1] : '\0';
+ if(next == '\'')
+ {
+ _c4dbgfsq("[{}]: two consecutive quotes", i);
+ filtered_chars = true;
+ m_filter_arena.str[pos++] = '\'';
+ ++i;
+ }
+ }
+ else
+ {
+ m_filter_arena.str[pos++] = curr;
+ }
+ }
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos <= m_filter_arena.len);
+ if(pos < r.len || filtered_chars)
+ {
+ r = _finish_filter_arena(r, pos);
+ }
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.len >= r.len);
+ _c4dbgpf(": #filteredchars={} after=~~~{}~~~", s.len - r.len, r);
+
+ #undef _c4dbgfsq
+ return r;
+}
+
+
+//-----------------------------------------------------------------------------
+csubstr Parser::_filter_dquot_scalar(substr s)
+{
+ // a debugging scaffold:
+ #if 0
+ #define _c4dbgfdq(...) _c4dbgpf("filt_dquo_scalar" __VA_ARGS__)
+ #else
+ #define _c4dbgfdq(...)
+ #endif
+
+ _c4dbgfdq(": before=~~~{}~~~", s);
+
+ // from the YAML spec for double-quoted scalars:
+ // https://yaml.org/spec/1.2-old/spec.html#style/flow/double-quoted
+ //
+ // All leading and trailing white space characters are excluded
+ // from the content. Each continuation line must therefore contain
+ // at least one non-space character. Empty lines, if any, are
+ // consumed as part of the line folding.
+
+ _grow_filter_arena(s.len + 2u * s.count('\\'));
+ substr r = s;
+ size_t pos = 0; // the filtered size
+ bool filtered_chars = false;
+ for(size_t i = 0; i < r.len; ++i)
+ {
+ const char curr = r[i];
+ _c4dbgfdq("[{}]: '{}'", i, _c4prc(curr));
+ if(curr == ' ' || curr == '\t')
+ {
+ _filter_ws</*keep_trailing_ws*/true>(r, &i, &pos);
+ }
+ else if(curr == '\n')
+ {
+ filtered_chars = _filter_nl</*backslash_is_escape*/true, /*keep_trailing_ws*/true>(r, &i, &pos, /*indentation*/0);
+ }
+ else if(curr == '\r') // skip \r --- https://stackoverflow.com/questions/1885900
+ {
+ ;
+ }
+ else if(curr == '\\')
+ {
+ char next = i+1 < r.len ? r[i+1] : '\0';
+ _c4dbgfdq("[{}]: backslash, next='{}'", i, _c4prc(next));
+ filtered_chars = true;
+ if(next == '\r')
+ {
+ if(i+2 < r.len && r[i+2] == '\n')
+ {
+ ++i; // newline escaped with \ -- skip both (add only one as i is loop-incremented)
+ next = '\n';
+ _c4dbgfdq("[{}]: was \\r\\n, now next='\\n'", i);
+ }
+ }
+ // remember the loop will also increment i
+ if(next == '\n')
+ {
+ size_t ii = i + 2;
+ for( ; ii < r.len; ++ii)
+ {
+ if(r.str[ii] == ' ' || r.str[ii] == '\t') // skip leading whitespace
+ ;
+ else
+ break;
+ }
+ i += ii - i - 1;
+ }
+ else if(next == '"' || next == '/' || next == ' ' || next == '\t') // escapes for json compatibility
+ {
+ m_filter_arena.str[pos++] = next;
+ ++i;
+ }
+ else if(next == '\r')
+ {
+ //++i;
+ }
+ else if(next == 'n')
+ {
+ m_filter_arena.str[pos++] = '\n';
+ ++i;
+ }
+ else if(next == 'r')
+ {
+ m_filter_arena.str[pos++] = '\r';
+ ++i; // skip
+ }
+ else if(next == 't')
+ {
+ m_filter_arena.str[pos++] = '\t';
+ ++i;
+ }
+ else if(next == '\\')
+ {
+ m_filter_arena.str[pos++] = '\\';
+ ++i;
+ }
+ else if(next == 'x') // UTF8
+ {
+ if(i + 1u + 2u >= r.len)
+ _c4err("\\x requires 2 hex digits");
+ uint8_t byteval = {};
+ if(!read_hex(r.sub(i + 2u, 2u), &byteval))
+ _c4err("failed to read \\x codepoint");
+ m_filter_arena.str[pos++] = *(char*)&byteval;
+ i += 1u + 2u;
+ }
+ else if(next == 'u') // UTF16
+ {
+ if(i + 1u + 4u >= r.len)
+ _c4err("\\u requires 4 hex digits");
+ char readbuf[8];
+ csubstr codepoint = r.sub(i + 2u, 4u);
+ uint32_t codepoint_val = {};
+ if(!read_hex(codepoint, &codepoint_val))
+ _c4err("failed to parse \\u codepoint");
+ size_t numbytes = decode_code_point((uint8_t*)readbuf, sizeof(readbuf), codepoint_val);
+ C4_ASSERT(numbytes <= 4);
+ memcpy(m_filter_arena.str + pos, readbuf, numbytes);
+ pos += numbytes;
+ i += 1u + 4u;
+ }
+ else if(next == 'U') // UTF32
+ {
+ if(i + 1u + 8u >= r.len)
+ _c4err("\\U requires 8 hex digits");
+ char readbuf[8];
+ csubstr codepoint = r.sub(i + 2u, 8u);
+ uint32_t codepoint_val = {};
+ if(!read_hex(codepoint, &codepoint_val))
+ _c4err("failed to parse \\U codepoint");
+ size_t numbytes = decode_code_point((uint8_t*)readbuf, sizeof(readbuf), codepoint_val);
+ C4_ASSERT(numbytes <= 4);
+ memcpy(m_filter_arena.str + pos, readbuf, numbytes);
+ pos += numbytes;
+ i += 1u + 8u;
+ }
+ // https://yaml.org/spec/1.2.2/#rule-c-ns-esc-char
+ else if(next == '0')
+ {
+ m_filter_arena.str[pos++] = '\0';
+ ++i;
+ }
+ else if(next == 'b') // backspace
+ {
+ m_filter_arena.str[pos++] = '\b';
+ ++i;
+ }
+ else if(next == 'f') // form feed
+ {
+ m_filter_arena.str[pos++] = '\f';
+ ++i;
+ }
+ else if(next == 'a') // bell character
+ {
+ m_filter_arena.str[pos++] = '\a';
+ ++i;
+ }
+ else if(next == 'v') // vertical tab
+ {
+ m_filter_arena.str[pos++] = '\v';
+ ++i;
+ }
+ else if(next == 'e') // escape character
+ {
+ m_filter_arena.str[pos++] = '\x1b';
+ ++i;
+ }
+ else if(next == '_') // unicode non breaking space \u00a0
+ {
+ // https://www.compart.com/en/unicode/U+00a0
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x3e, 0xc2);
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x60, 0xa0);
+ ++i;
+ }
+ else if(next == 'N') // unicode next line \u0085
+ {
+ // https://www.compart.com/en/unicode/U+0085
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x3e, 0xc2);
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x7b, 0x85);
+ ++i;
+ }
+ else if(next == 'L') // unicode line separator \u2028
+ {
+ // https://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192&number=1024&names=-&utf8=0x&unicodeinhtml=hex
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x1e, 0xe2);
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x80, 0x80);
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x58, 0xa8);
+ ++i;
+ }
+ else if(next == 'P') // unicode paragraph separator \u2029
+ {
+ // https://www.utf8-chartable.de/unicode-utf8-table.pl?start=8192&number=1024&names=-&utf8=0x&unicodeinhtml=hex
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x1e, 0xe2);
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x80, 0x80);
+ m_filter_arena.str[pos++] = _RYML_CHCONST(-0x57, 0xa9);
+ ++i;
+ }
+ _c4dbgfdq("[{}]: backslash...sofar=[{}]~~~{}~~~", i, pos, m_filter_arena.first(pos));
+ }
+ else
+ {
+ m_filter_arena.str[pos++] = curr;
+ }
+ }
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos <= m_filter_arena.len);
+ if(pos < r.len || filtered_chars)
+ {
+ r = _finish_filter_arena(r, pos);
+ }
+
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.len >= r.len);
+ _c4dbgpf(": #filteredchars={} after=~~~{}~~~", s.len - r.len, r);
+
+ #undef _c4dbgfdq
+
+ return r;
+}
+
+
+//-----------------------------------------------------------------------------
+bool Parser::_apply_chomp(substr buf, size_t *C4_RESTRICT pos, BlockChomp_e chomp)
+{
+ substr trimmed = buf.first(*pos).trimr('\n');
+ bool added_newline = false;
+ switch(chomp)
+ {
+ case CHOMP_KEEP:
+ if(trimmed.len == *pos)
+ {
+ _c4dbgpf("chomp=KEEP: add missing newline @{}", *pos);
+ //m_filter_arena.str[(*pos)++] = '\n';
+ added_newline = true;
+ }
+ break;
+ case CHOMP_CLIP:
+ if(trimmed.len == *pos)
+ {
+ _c4dbgpf("chomp=CLIP: add missing newline @{}", *pos);
+ m_filter_arena.str[(*pos)++] = '\n';
+ added_newline = true;
+ }
+ else
+ {
+ _c4dbgpf("chomp=CLIP: include single trailing newline @{}", trimmed.len+1);
+ *pos = trimmed.len + 1;
+ }
+ break;
+ case CHOMP_STRIP:
+ _c4dbgpf("chomp=STRIP: strip {}-{}-{} newlines", *pos, trimmed.len, *pos-trimmed.len);
+ *pos = trimmed.len;
+ break;
+ default:
+ _c4err("unknown chomp style");
+ }
+ return added_newline;
+}
+
+
+//-----------------------------------------------------------------------------
+csubstr Parser::_filter_block_scalar(substr s, BlockStyle_e style, BlockChomp_e chomp, size_t indentation)
+{
+ // a debugging scaffold:
+ #if 0
+ #define _c4dbgfbl(fmt, ...) _c4dbgpf("filt_block" fmt, __VA_ARGS__)
+ #else
+ #define _c4dbgfbl(...)
+ #endif
+
+ _c4dbgfbl(": indentation={} before=[{}]~~~{}~~~", indentation, s.len, s);
+
+ if(chomp != CHOMP_KEEP && s.trim(" \n\r\t").len == 0u)
+ {
+ _c4dbgp("filt_block: empty scalar");
+ return s.first(0);
+ }
+
+ substr r = s;
+
+ switch(style)
+ {
+ case BLOCK_LITERAL:
+ {
+ _c4dbgp("filt_block: style=literal");
+ // trim leading whitespace up to indentation
+ {
+ size_t numws = r.first_not_of(' ');
+ if(numws != npos)
+ {
+ if(numws > indentation)
+ r = r.sub(indentation);
+ else
+ r = r.sub(numws);
+ _c4dbgfbl(": after triml=[{}]~~~{}~~~", r.len, r);
+ }
+ else
+ {
+ if(chomp != CHOMP_KEEP || r.len == 0)
+ {
+ _c4dbgfbl(": all spaces {}, return empty", r.len);
+ return r.first(0);
+ }
+ else
+ {
+ r[0] = '\n';
+ return r.first(1);
+ }
+ }
+ }
+ _grow_filter_arena(s.len + 2u); // use s.len! because we may need to add a newline at the end, so the leading indentation will allow space for that newline
+ size_t pos = 0; // the filtered size
+ for(size_t i = 0; i < r.len; ++i)
+ {
+ const char curr = r.str[i];
+ _c4dbgfbl("[{}]='{}' pos={}", i, _c4prc(curr), pos);
+ if(curr == '\r')
+ continue;
+ m_filter_arena.str[pos++] = curr;
+ if(curr == '\n')
+ {
+ _c4dbgfbl("[{}]: found newline", i);
+ // skip indentation on the next line
+ csubstr rem = r.sub(i+1);
+ size_t first = rem.first_not_of(' ');
+ if(first != npos)
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, first < rem.len);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, i+1+first < r.len);
+ _c4dbgfbl("[{}]: {} spaces follow before next nonws character @ [{}]='{}'", i, first, i+1+first, rem.str[first]);
+ if(first < indentation)
+ {
+ _c4dbgfbl("[{}]: skip {}<{} spaces from indentation", i, first, indentation);
+ i += first;
+ }
+ else
+ {
+ _c4dbgfbl("[{}]: skip {} spaces from indentation", i, indentation);
+ i += indentation;
+ }
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, i+1 <= r.len);
+ first = rem.len;
+ _c4dbgfbl("[{}]: {} spaces to the end", i, first);
+ if(first)
+ {
+ if(first < indentation)
+ {
+ _c4dbgfbl("[{}]: skip everything", i);
+ --pos;
+ break;
+ }
+ else
+ {
+ _c4dbgfbl("[{}]: skip {} spaces from indentation", i, indentation);
+ i += indentation;
+ }
+ }
+ else if(i+1 == r.len)
+ {
+ if(chomp == CHOMP_STRIP)
+ --pos;
+ break;
+ }
+ }
+ }
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, s.len >= pos);
+ _c4dbgfbl(": #filteredchars={} after=~~~{}~~~", s.len - r.len, r);
+ bool changed = _apply_chomp(m_filter_arena, &pos, chomp);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos <= m_filter_arena.len);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos <= s.len);
+ if(pos < r.len || changed)
+ {
+ r = _finish_filter_arena(s, pos); // write into s
+ }
+ break;
+ }
+ case BLOCK_FOLD:
+ {
+ _c4dbgp("filt_block: style=fold");
+ _grow_filter_arena(r.len + 2);
+ size_t pos = 0; // the filtered size
+ bool filtered_chars = false;
+ bool started = false;
+ bool is_indented = false;
+ size_t i = r.first_not_of(' ');
+ _c4dbgfbl(": first non space at {}", i);
+ if(i > indentation)
+ {
+ is_indented = true;
+ i = indentation;
+ }
+ _c4dbgfbl(": start folding at {}, is_indented={}", i, (int)is_indented);
+ auto on_change_indentation = [&](size_t numnl_following, size_t last_newl, size_t first_non_whitespace){
+ _c4dbgfbl("[{}]: add 1+{} newlines", i, numnl_following);
+ for(size_t j = 0; j < 1 + numnl_following; ++j)
+ m_filter_arena.str[pos++] = '\n';
+ for(i = last_newl + 1 + indentation; i < first_non_whitespace; ++i)
+ {
+ if(r.str[i] == '\r')
+ continue;
+ _c4dbgfbl("[{}]: add '{}'", i, _c4prc(r.str[i]));
+ m_filter_arena.str[pos++] = r.str[i];
+ }
+ --i;
+ };
+ for( ; i < r.len; ++i)
+ {
+ const char curr = r.str[i];
+ _c4dbgfbl("[{}]='{}'", i, _c4prc(curr));
+ if(curr == '\n')
+ {
+ filtered_chars = true;
+ // skip indentation on the next line, and advance over the next non-indented blank lines as well
+ size_t first_non_whitespace;
+ size_t numnl_following = (size_t)-1;
+ while(r[i] == '\n')
+ {
+ ++numnl_following;
+ csubstr rem = r.sub(i+1);
+ size_t first = rem.first_not_of(' ');
+ _c4dbgfbl("[{}]: found newline. first={} rem.len={}", i, first, rem.len);
+ if(first != npos)
+ {
+ first_non_whitespace = first + i+1;
+ while(first_non_whitespace < r.len && r[first_non_whitespace] == '\r')
+ ++first_non_whitespace;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, first < rem.len);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, i+1+first < r.len);
+ _c4dbgfbl("[{}]: {} spaces follow before next nonws character @ [{}]='{}'", i, first, i+1+first, _c4prc(rem.str[first]));
+ if(first < indentation)
+ {
+ _c4dbgfbl("[{}]: skip {}<{} spaces from indentation", i, first, indentation);
+ i += first;
+ }
+ else
+ {
+ _c4dbgfbl("[{}]: skip {} spaces from indentation", i, indentation);
+ i += indentation;
+ if(first > indentation)
+ {
+ _c4dbgfbl("[{}]: {} further indented than {}, stop newlining", i, first, indentation);
+ goto finished_counting_newlines;
+ }
+ }
+ // prepare the next while loop iteration
+ // by setting i at the next newline after
+ // an empty line
+ if(r[first_non_whitespace] == '\n')
+ i = first_non_whitespace;
+ else
+ goto finished_counting_newlines;
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, i+1 <= r.len);
+ first = rem.len;
+ first_non_whitespace = first + i+1;
+ if(first)
+ {
+ _c4dbgfbl("[{}]: {} spaces to the end", i, first);
+ if(first < indentation)
+ {
+ _c4dbgfbl("[{}]: skip everything", i);
+ i += first;
+ }
+ else
+ {
+ _c4dbgfbl("[{}]: skip {} spaces from indentation", i, indentation);
+ i += indentation;
+ if(first > indentation)
+ {
+ _c4dbgfbl("[{}]: {} spaces missing. not done yet", i, indentation - first);
+ goto finished_counting_newlines;
+ }
+ }
+ }
+ else // if(i+1 == r.len)
+ {
+ _c4dbgfbl("[{}]: it's the final newline", i);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, i+1 == r.len);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, rem.len == 0);
+ }
+ goto end_of_scalar;
+ }
+ }
+ end_of_scalar:
+ // Write all the trailing newlines. Since we're
+ // at the end no folding is needed, so write every
+ // newline (add 1).
+ _c4dbgfbl("[{}]: add {} trailing newlines", i, 1+numnl_following);
+ for(size_t j = 0; j < 1 + numnl_following; ++j)
+ m_filter_arena.str[pos++] = '\n';
+ break;
+ finished_counting_newlines:
+ _c4dbgfbl("[{}]: #newlines={} firstnonws={}", i, numnl_following, first_non_whitespace);
+ while(first_non_whitespace < r.len && r[first_non_whitespace] == '\t')
+ ++first_non_whitespace;
+ _c4dbgfbl("[{}]: #newlines={} firstnonws={}", i, numnl_following, first_non_whitespace);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, first_non_whitespace <= r.len);
+ size_t last_newl = r.last_of('\n', first_non_whitespace);
+ size_t this_indentation = first_non_whitespace - last_newl - 1;
+ _c4dbgfbl("[{}]: #newlines={} firstnonws={} lastnewl={} this_indentation={} vs indentation={}", i, numnl_following, first_non_whitespace, last_newl, this_indentation, indentation);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, first_non_whitespace >= last_newl + 1);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, this_indentation >= indentation);
+ if(!started)
+ {
+ _c4dbgfbl("[{}]: #newlines={}. write all leading newlines", i, numnl_following);
+ for(size_t j = 0; j < 1 + numnl_following; ++j)
+ m_filter_arena.str[pos++] = '\n';
+ if(this_indentation > indentation)
+ {
+ is_indented = true;
+ _c4dbgfbl("[{}]: advance ->{}", i, last_newl + indentation);
+ i = last_newl + indentation;
+ }
+ else
+ {
+ i = first_non_whitespace - 1;
+ _c4dbgfbl("[{}]: advance ->{}", i, first_non_whitespace);
+ }
+ }
+ else if(this_indentation == indentation)
+ {
+ _c4dbgfbl("[{}]: same indentation", i);
+ if(!is_indented)
+ {
+ if(numnl_following == 0)
+ {
+ _c4dbgfbl("[{}]: fold!", i);
+ m_filter_arena.str[pos++] = ' ';
+ }
+ else
+ {
+ _c4dbgfbl("[{}]: add {} newlines", i, 1 + numnl_following);
+ for(size_t j = 0; j < numnl_following; ++j)
+ m_filter_arena.str[pos++] = '\n';
+ }
+ i = first_non_whitespace - 1;
+ _c4dbgfbl("[{}]: advance {}->{}", i, i, first_non_whitespace);
+ }
+ else
+ {
+ _c4dbgfbl("[{}]: back to ref indentation", i);
+ is_indented = false;
+ on_change_indentation(numnl_following, last_newl, first_non_whitespace);
+ _c4dbgfbl("[{}]: advance {}->{}", i, i, first_non_whitespace);
+ }
+ }
+ else
+ {
+ _c4dbgfbl("[{}]: increased indentation.", i);
+ is_indented = true;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, this_indentation > indentation);
+ on_change_indentation(numnl_following, last_newl, first_non_whitespace);
+ _c4dbgfbl("[{}]: advance {}->{}", i, i, first_non_whitespace);
+ }
+ }
+ else if(curr != '\r')
+ {
+ if(curr != '\t')
+ started = true;
+ m_filter_arena.str[pos++] = curr;
+ }
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos <= m_filter_arena.len);
+ _c4dbgfbl(": #filteredchars={} after=[{}]~~~{}~~~", (int)s.len - (int)pos, pos, m_filter_arena.first(pos));
+ bool changed = _apply_chomp(m_filter_arena, &pos, chomp);
+ if(pos < r.len || filtered_chars || changed)
+ {
+ r = _finish_filter_arena(s, pos); // write into s
+ }
+ }
+ break;
+ default:
+ _c4err("unknown block style");
+ }
+
+ _c4dbgfbl(": final=[{}]~~~{}~~~", r.len, r);
+
+ #undef _c4dbgfbl
+
+ return r;
+}
+
+//-----------------------------------------------------------------------------
+size_t Parser::_count_nlines(csubstr src)
+{
+ return 1 + src.count('\n');
+}
+
+//-----------------------------------------------------------------------------
+void Parser::_handle_directive(csubstr directive_)
+{
+ csubstr directive = directive_;
+ if(directive.begins_with("%TAG"))
+ {
+ TagDirective td;
+ _c4dbgpf("%TAG directive: {}", directive_);
+ directive = directive.sub(4);
+ if(!directive.begins_with(' '))
+ _c4err("malformed tag directive: {}", directive_);
+ directive = directive.triml(' ');
+ size_t pos = directive.find(' ');
+ if(pos == npos)
+ _c4err("malformed tag directive: {}", directive_);
+ td.handle = directive.first(pos);
+ directive = directive.sub(td.handle.len).triml(' ');
+ pos = directive.find(' ');
+ if(pos != npos)
+ directive = directive.first(pos);
+ td.prefix = directive;
+ td.next_node_id = m_tree->size();
+ if(m_tree->size() > 0)
+ {
+ size_t prev = m_tree->size() - 1;
+ if(m_tree->is_root(prev) && m_tree->type(prev) != NOTYPE && !m_tree->is_stream(prev))
+ ++td.next_node_id;
+ }
+ _c4dbgpf("%TAG: handle={} prefix={} next_node={}", td.handle, td.prefix, td.next_node_id);
+ m_tree->add_tag_directive(td);
+ }
+ else if(directive.begins_with("%YAML"))
+ {
+ _c4dbgpf("%YAML directive! ignoring...: {}", directive);
+ }
+}
+
+//-----------------------------------------------------------------------------
+void Parser::set_flags(flag_t f, State * s)
+{
+#ifdef RYML_DBG
+ char buf1_[64], buf2_[64];
+ csubstr buf1 = _prfl(buf1_, f);
+ csubstr buf2 = _prfl(buf2_, s->flags);
+ _c4dbgpf("state[{}]: setting flags to {}: before={}", s-m_stack.begin(), buf1, buf2);
+#endif
+ s->flags = f;
+}
+
+void Parser::add_flags(flag_t on, State * s)
+{
+#ifdef RYML_DBG
+ char buf1_[64], buf2_[64], buf3_[64];
+ csubstr buf1 = _prfl(buf1_, on);
+ csubstr buf2 = _prfl(buf2_, s->flags);
+ csubstr buf3 = _prfl(buf3_, s->flags|on);
+ _c4dbgpf("state[{}]: adding flags {}: before={} after={}", s-m_stack.begin(), buf1, buf2, buf3);
+#endif
+ s->flags |= on;
+}
+
+void Parser::addrem_flags(flag_t on, flag_t off, State * s)
+{
+#ifdef RYML_DBG
+ char buf1_[64], buf2_[64], buf3_[64], buf4_[64];
+ csubstr buf1 = _prfl(buf1_, on);
+ csubstr buf2 = _prfl(buf2_, off);
+ csubstr buf3 = _prfl(buf3_, s->flags);
+ csubstr buf4 = _prfl(buf4_, ((s->flags|on)&(~off)));
+ _c4dbgpf("state[{}]: adding flags {} / removing flags {}: before={} after={}", s-m_stack.begin(), buf1, buf2, buf3, buf4);
+#endif
+ s->flags |= on;
+ s->flags &= ~off;
+}
+
+void Parser::rem_flags(flag_t off, State * s)
+{
+#ifdef RYML_DBG
+ char buf1_[64], buf2_[64], buf3_[64];
+ csubstr buf1 = _prfl(buf1_, off);
+ csubstr buf2 = _prfl(buf2_, s->flags);
+ csubstr buf3 = _prfl(buf3_, s->flags&(~off));
+ _c4dbgpf("state[{}]: removing flags {}: before={} after={}", s-m_stack.begin(), buf1, buf2, buf3);
+#endif
+ s->flags &= ~off;
+}
+
+//-----------------------------------------------------------------------------
+
+csubstr Parser::_prfl(substr buf, flag_t flags)
+{
+ size_t pos = 0;
+ bool gotone = false;
+
+ #define _prflag(fl) \
+ if((flags & fl) == (fl)) \
+ { \
+ if(gotone) \
+ { \
+ if(pos + 1 < buf.len) \
+ buf[pos] = '|'; \
+ ++pos; \
+ } \
+ csubstr fltxt = #fl; \
+ if(pos + fltxt.len <= buf.len) \
+ memcpy(buf.str + pos, fltxt.str, fltxt.len); \
+ pos += fltxt.len; \
+ gotone = true; \
+ }
+
+ _prflag(RTOP);
+ _prflag(RUNK);
+ _prflag(RMAP);
+ _prflag(RSEQ);
+ _prflag(FLOW);
+ _prflag(QMRK);
+ _prflag(RKEY);
+ _prflag(RVAL);
+ _prflag(RNXT);
+ _prflag(SSCL);
+ _prflag(QSCL);
+ _prflag(RSET);
+ _prflag(NDOC);
+ _prflag(RSEQIMAP);
+
+ #undef _prflag
+
+ RYML_ASSERT(pos <= buf.len);
+
+ return buf.first(pos);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+void Parser::_grow_filter_arena(size_t num_characters_needed)
+{
+ _c4dbgpf("grow: arena={} numchars={}", m_filter_arena.len, num_characters_needed);
+ if(num_characters_needed <= m_filter_arena.len)
+ return;
+ size_t sz = m_filter_arena.len << 1;
+ _c4dbgpf("grow: sz={}", sz);
+ sz = num_characters_needed > sz ? num_characters_needed : sz;
+ _c4dbgpf("grow: sz={}", sz);
+ sz = sz < 128u ? 128u : sz;
+ _c4dbgpf("grow: sz={}", sz);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, sz >= num_characters_needed);
+ _resize_filter_arena(sz);
+}
+
+void Parser::_resize_filter_arena(size_t num_characters)
+{
+ if(num_characters > m_filter_arena.len)
+ {
+ _c4dbgpf("resize: sz={}", num_characters);
+ char *prev = m_filter_arena.str;
+ if(m_filter_arena.str)
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_filter_arena.len > 0);
+ _RYML_CB_FREE(m_stack.m_callbacks, m_filter_arena.str, char, m_filter_arena.len);
+ }
+ m_filter_arena.str = _RYML_CB_ALLOC_HINT(m_stack.m_callbacks, char, num_characters, prev);
+ m_filter_arena.len = num_characters;
+ }
+}
+
+substr Parser::_finish_filter_arena(substr dst, size_t pos)
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos <= m_filter_arena.len);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, pos <= dst.len);
+ memcpy(dst.str, m_filter_arena.str, pos);
+ return dst.first(pos);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+csubstr Parser::location_contents(Location const& loc) const
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, loc.offset < m_buf.len);
+ return m_buf.sub(loc.offset);
+}
+
+Location Parser::location(NodeRef node) const
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, node.valid());
+ return location(*node.tree(), node.id());
+}
+
+Location Parser::location(Tree const& tree, size_t node) const
+{
+ _RYML_CB_CHECK(m_stack.m_callbacks, m_buf.str == m_newline_offsets_buf.str);
+ _RYML_CB_CHECK(m_stack.m_callbacks, m_buf.len == m_newline_offsets_buf.len);
+ if(tree.has_key(node))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, tree.key(node).is_sub(m_buf));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_buf.is_super(tree.key(node)));
+ return val_location(tree.key(node).str);
+ }
+ else if(tree.has_val(node))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, tree.val(node).is_sub(m_buf));
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_buf.is_super(tree.val(node)));
+ return val_location(tree.val(node).str);
+ }
+ else if(tree.is_container(node))
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, !tree.has_key(node));
+ if(!tree.is_stream(node))
+ {
+ const char *node_start = tree._p(node)->m_val.scalar.str; // this was stored in the container
+ if(tree.has_children(node))
+ {
+ size_t child = tree.first_child(node);
+ if(tree.has_key(child))
+ {
+ // when a map starts, the container was set after the key
+ csubstr k = tree.key(child);
+ if(node_start > k.str)
+ node_start = k.str;
+ }
+ }
+ return val_location(node_start);
+ }
+ else // it's a stream
+ {
+ return val_location(m_buf.str); // just return the front of the buffer
+ }
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, tree.type(node) == NOTYPE);
+ return val_location(m_buf.str);
+}
+
+Location Parser::val_location(const char *val) const
+{
+ if(_locations_dirty())
+ _prepare_locations();
+ csubstr src = m_buf;
+ _RYML_CB_CHECK(m_stack.m_callbacks, src.str == m_newline_offsets_buf.str);
+ _RYML_CB_CHECK(m_stack.m_callbacks, src.len == m_newline_offsets_buf.len);
+ _RYML_CB_CHECK(m_stack.m_callbacks, val >= src.begin() && val <= src.end());
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_newline_offsets != nullptr);
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_newline_offsets_size > 0);
+ using linetype = size_t const* C4_RESTRICT;
+ linetype line = nullptr;
+ size_t offset = (size_t)(val - src.begin());
+ if(m_newline_offsets_size < 30)
+ {
+ // do a linear search if the size is small.
+ for(linetype curr = m_newline_offsets; curr < m_newline_offsets + m_newline_offsets_size; ++curr)
+ {
+ if(*curr > offset)
+ {
+ line = curr;
+ break;
+ }
+ }
+ }
+ else
+ {
+ // Do a bisection search if the size is not small.
+ //
+ // We could use std::lower_bound but this is simple enough and
+ // spares the include of <algorithm>.
+ size_t count = m_newline_offsets_size;
+ size_t step;
+ linetype it;
+ line = m_newline_offsets;
+ while(count)
+ {
+ step = count >> 1;
+ it = line + step;
+ if(*it < offset)
+ {
+ line = ++it;
+ count -= step + 1;
+ }
+ else
+ {
+ count = step;
+ }
+ }
+ }
+ if(line)
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, *line > offset);
+ }
+ else
+ {
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_buf.empty());
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_newline_offsets_size == 1);
+ line = m_newline_offsets;
+ }
+ _RYML_CB_ASSERT(m_stack.m_callbacks, line >= m_newline_offsets && line < m_newline_offsets + m_newline_offsets_size);;
+ Location loc = {};
+ loc.name = m_file;
+ loc.offset = offset;
+ loc.line = (size_t)(line - m_newline_offsets);
+ if(line > m_newline_offsets)
+ loc.col = (offset - *(line-1) - 1u);
+ else
+ loc.col = offset;
+ return loc;
+}
+
+void Parser::_prepare_locations() const
+{
+ _RYML_CB_ASSERT(m_stack.m_callbacks, !m_file.empty());
+ size_t numnewlines = 1u + m_buf.count('\n');
+ _resize_locations(numnewlines);
+ m_newline_offsets_size = 0;
+ for(size_t i = 0; i < m_buf.len; i++)
+ if(m_buf[i] == '\n')
+ m_newline_offsets[m_newline_offsets_size++] = i;
+ m_newline_offsets[m_newline_offsets_size++] = m_buf.len;
+ _RYML_CB_ASSERT(m_stack.m_callbacks, m_newline_offsets_size == numnewlines);
+}
+
+void Parser::_resize_locations(size_t numnewlines) const
+{
+ if(numnewlines > m_newline_offsets_capacity)
+ {
+ if(m_newline_offsets)
+ _RYML_CB_FREE(m_stack.m_callbacks, m_newline_offsets, size_t, m_newline_offsets_capacity);
+ m_newline_offsets = _RYML_CB_ALLOC_HINT(m_stack.m_callbacks, size_t, numnewlines, m_newline_offsets);
+ m_newline_offsets_capacity = numnewlines;
+ }
+}
+
+void Parser::_mark_locations_dirty()
+{
+ m_newline_offsets_size = 0u;
+ m_newline_offsets_buf = m_buf;
+}
+
+bool Parser::_locations_dirty() const
+{
+ return !m_newline_offsets_size;
+}
+
+} // namespace yml
+} // namespace c4
+
+
+#if defined(_MSC_VER)
+# pragma warning(pop)
+#elif defined(__clang__)
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#endif
+
+#endif /* RYML_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/parse.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/node.cpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/node.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef RYML_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/node.hpp
+//#include "c4/yml/node.hpp"
+#if !defined(C4_YML_NODE_HPP_) && !defined(_C4_YML_NODE_HPP_)
+#error "amalgamate: file c4/yml/node.hpp must have been included at this point"
+#endif /* C4_YML_NODE_HPP_ */
+
+
+namespace c4 {
+namespace yml {
+
+size_t NodeRef::set_key_serialized(c4::fmt::const_base64_wrapper w)
+{
+ _apply_seed();
+ csubstr encoded = this->to_arena(w);
+ this->set_key(encoded);
+ return encoded.len;
+}
+
+size_t NodeRef::set_val_serialized(c4::fmt::const_base64_wrapper w)
+{
+ _apply_seed();
+ csubstr encoded = this->to_arena(w);
+ this->set_val(encoded);
+ return encoded.len;
+}
+
+size_t NodeRef::deserialize_key(c4::fmt::base64_wrapper w) const
+{
+ RYML_ASSERT( ! is_seed());
+ RYML_ASSERT(valid());
+ RYML_ASSERT(get() != nullptr);
+ return from_chars(key(), &w);
+}
+
+size_t NodeRef::deserialize_val(c4::fmt::base64_wrapper w) const
+{
+ RYML_ASSERT( ! is_seed());
+ RYML_ASSERT(valid());
+ RYML_ASSERT(get() != nullptr);
+ return from_chars(val(), &w);
+}
+
+} // namespace yml
+} // namespace c4
+
+#endif /* RYML_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/node.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/preprocess.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/preprocess.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_PREPROCESS_HPP_
+#define _C4_YML_PREPROCESS_HPP_
+
+/** @file preprocess.hpp Functions for preprocessing YAML prior to parsing. */
+
+/** @defgroup Preprocessors Preprocessor functions
+ *
+ * These are the existing preprocessors:
+ *
+ * @code{.cpp}
+ * size_t preprocess_json(csubstr json, substr buf)
+ * size_t preprocess_rxmap(csubstr json, substr buf)
+ * @endcode
+ */
+
+#ifndef _C4_YML_COMMON_HPP_
+//included above:
+//#include "./common.hpp"
+#endif
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/substr.hpp
+//#include <c4/substr.hpp>
+#if !defined(C4_SUBSTR_HPP_) && !defined(_C4_SUBSTR_HPP_)
+#error "amalgamate: file c4/substr.hpp must have been included at this point"
+#endif /* C4_SUBSTR_HPP_ */
+
+
+
+namespace c4 {
+namespace yml {
+
+namespace detail {
+using Preprocessor = size_t(csubstr, substr);
+template<Preprocessor PP, class CharContainer>
+substr preprocess_into_container(csubstr input, CharContainer *out)
+{
+ // try to write once. the preprocessor will stop writing at the end of
+ // the container, but will process all the input to determine the
+ // required container size.
+ size_t sz = PP(input, to_substr(*out));
+ // if the container size is not enough, resize, and run again in the
+ // resized container
+ if(sz > out->size())
+ {
+ out->resize(sz);
+ sz = PP(input, to_substr(*out));
+ }
+ return to_substr(*out).first(sz);
+}
+} // namespace detail
+
+
+//-----------------------------------------------------------------------------
+
+/** @name preprocess_rxmap
+ * Convert flow-type relaxed maps (with implicit bools) into strict YAML
+ * flow map.
+ *
+ * @code{.yaml}
+ * {a, b, c, d: [e, f], g: {a, b}}
+ * # is converted into this:
+ * {a: 1, b: 1, c: 1, d: [e, f], g: {a, b}}
+ * @endcode
+
+ * @note this is NOT recursive - conversion happens only in the top-level map
+ * @param rxmap A relaxed map
+ * @param buf output buffer
+ * @param out output container
+ */
+
+//@{
+
+/** Write into a given output buffer. This function is safe to call with
+ * empty or small buffers; it won't write beyond the end of the buffer.
+ *
+ * @return the number of characters required for output
+ */
+RYML_EXPORT size_t preprocess_rxmap(csubstr rxmap, substr buf);
+
+
+/** Write into an existing container. It is resized to contained the output.
+ * @return a substr of the container
+ * @overload preprocess_rxmap */
+template<class CharContainer>
+substr preprocess_rxmap(csubstr rxmap, CharContainer *out)
+{
+ return detail::preprocess_into_container<preprocess_rxmap>(rxmap, out);
+}
+
+
+/** Create a container with the result.
+ * @overload preprocess_rxmap */
+template<class CharContainer>
+CharContainer preprocess_rxmap(csubstr rxmap)
+{
+ CharContainer out;
+ preprocess_rxmap(rxmap, &out);
+ return out;
+}
+
+//@}
+
+} // namespace yml
+} // namespace c4
+
+#endif /* _C4_YML_PREPROCESS_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/preprocess.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/preprocess.cpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/preprocess.cpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifdef RYML_SINGLE_HDR_DEFINE_NOW
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/preprocess.hpp
+//#include "c4/yml/preprocess.hpp"
+#if !defined(C4_YML_PREPROCESS_HPP_) && !defined(_C4_YML_PREPROCESS_HPP_)
+#error "amalgamate: file c4/yml/preprocess.hpp must have been included at this point"
+#endif /* C4_YML_PREPROCESS_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/parser_dbg.hpp
+//#include "c4/yml/detail/parser_dbg.hpp"
+#if !defined(C4_YML_DETAIL_PARSER_DBG_HPP_) && !defined(_C4_YML_DETAIL_PARSER_DBG_HPP_)
+#error "amalgamate: file c4/yml/detail/parser_dbg.hpp must have been included at this point"
+#endif /* C4_YML_DETAIL_PARSER_DBG_HPP_ */
+
+
+/** @file preprocess.hpp Functions for preprocessing YAML prior to parsing. */
+
+namespace c4 {
+namespace yml {
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+namespace {
+C4_ALWAYS_INLINE bool _is_idchar(char c)
+{
+ return (c >= 'a' && c <= 'z')
+ || (c >= 'A' && c <= 'Z')
+ || (c >= '0' && c <= '9')
+ || (c == '_' || c == '-' || c == '~' || c == '$');
+}
+
+typedef enum { kReadPending = 0, kKeyPending = 1, kValPending = 2 } _ppstate;
+C4_ALWAYS_INLINE _ppstate _next(_ppstate s)
+{
+ int n = (int)s + 1;
+ return (_ppstate)(n <= (int)kValPending ? n : 0);
+}
+} // empty namespace
+
+
+//-----------------------------------------------------------------------------
+
+size_t preprocess_rxmap(csubstr s, substr buf)
+{
+ detail::_SubstrWriter writer(buf);
+ _ppstate state = kReadPending;
+ size_t last = 0;
+
+ if(s.begins_with('{'))
+ {
+ RYML_CHECK(s.ends_with('}'));
+ s = s.offs(1, 1);
+ }
+
+ writer.append('{');
+
+ for(size_t i = 0; i < s.len; ++i)
+ {
+ const char curr = s[i];
+ const char next = i+1 < s.len ? s[i+1] : '\0';
+
+ if(curr == '\'' || curr == '"')
+ {
+ csubstr ss = s.sub(i).pair_range_esc(curr, '\\');
+ i += static_cast<size_t>(ss.end() - (s.str + i));
+ state = _next(state);
+ }
+ else if(state == kReadPending && _is_idchar(curr))
+ {
+ state = _next(state);
+ }
+
+ switch(state)
+ {
+ case kKeyPending:
+ {
+ if(curr == ':' && next == ' ')
+ {
+ state = _next(state);
+ }
+ else if(curr == ',' && next == ' ')
+ {
+ writer.append(s.range(last, i));
+ writer.append(": 1, ");
+ last = i + 2;
+ }
+ break;
+ }
+ case kValPending:
+ {
+ if(curr == '[' || curr == '{' || curr == '(')
+ {
+ csubstr ss = s.sub(i).pair_range_nested(curr, '\\');
+ i += static_cast<size_t>(ss.end() - (s.str + i));
+ state = _next(state);
+ }
+ else if(curr == ',' && next == ' ')
+ {
+ state = _next(state);
+ }
+ break;
+ }
+ default:
+ // nothing to do
+ break;
+ }
+ }
+
+ writer.append(s.sub(last));
+ if(state == kKeyPending)
+ writer.append(": 1");
+ writer.append('}');
+
+ return writer.pos;
+}
+
+
+} // namespace yml
+} // namespace c4
+
+#endif /* RYML_SINGLE_HDR_DEFINE_NOW */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/preprocess.cpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/detail/checks.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/checks.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef C4_YML_DETAIL_CHECKS_HPP_
+#define C4_YML_DETAIL_CHECKS_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/tree.hpp
+//#include "c4/yml/tree.hpp"
+#if !defined(C4_YML_TREE_HPP_) && !defined(_C4_YML_TREE_HPP_)
+#error "amalgamate: file c4/yml/tree.hpp must have been included at this point"
+#endif /* C4_YML_TREE_HPP_ */
+
+
+#ifdef __clang__
+# pragma clang diagnostic push
+#elif defined(__GNUC__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wtype-limits" // error: comparison of unsigned expression >= 0 is always true
+#elif defined(_MSC_VER)
+# pragma warning(push)
+# pragma warning(disable: 4296/*expression is always 'boolean_value'*/)
+#endif
+
+namespace c4 {
+namespace yml {
+
+
+void check_invariants(Tree const& t, size_t node=NONE);
+void check_free_list(Tree const& t);
+void check_arena(Tree const& t);
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+inline void check_invariants(Tree const& t, size_t node)
+{
+ if(node == NONE)
+ {
+ if(t.size() == 0) return;
+ node = t.root_id();
+ }
+
+ auto const& n = *t._p(node);
+#ifdef RYML_DBG
+ if(n.m_first_child != NONE || n.m_last_child != NONE)
+ {
+ printf("check(%zu): fc=%zu lc=%zu\n", node, n.m_first_child, n.m_last_child);
+ }
+ else
+ {
+ printf("check(%zu)\n", node);
+ }
+#endif
+
+ C4_CHECK(n.m_parent != node);
+ if(n.m_parent == NONE)
+ {
+ C4_CHECK(t.is_root(node));
+ }
+ else //if(n.m_parent != NONE)
+ {
+ C4_CHECK(t.has_child(n.m_parent, node));
+
+ auto const& p = *t._p(n.m_parent);
+ if(n.m_prev_sibling == NONE)
+ {
+ C4_CHECK(p.m_first_child == node);
+ C4_CHECK(t.first_sibling(node) == node);
+ }
+ else
+ {
+ C4_CHECK(p.m_first_child != node);
+ C4_CHECK(t.first_sibling(node) != node);
+ }
+
+ if(n.m_next_sibling == NONE)
+ {
+ C4_CHECK(p.m_last_child == node);
+ C4_CHECK(t.last_sibling(node) == node);
+ }
+ else
+ {
+ C4_CHECK(p.m_last_child != node);
+ C4_CHECK(t.last_sibling(node) != node);
+ }
+ }
+
+ C4_CHECK(n.m_first_child != node);
+ C4_CHECK(n.m_last_child != node);
+ if(n.m_first_child != NONE || n.m_last_child != NONE)
+ {
+ C4_CHECK(n.m_first_child != NONE);
+ C4_CHECK(n.m_last_child != NONE);
+ }
+
+ C4_CHECK(n.m_prev_sibling != node);
+ C4_CHECK(n.m_next_sibling != node);
+ if(n.m_prev_sibling != NONE)
+ {
+ C4_CHECK(t._p(n.m_prev_sibling)->m_next_sibling == node);
+ C4_CHECK(t._p(n.m_prev_sibling)->m_prev_sibling != node);
+ }
+ if(n.m_next_sibling != NONE)
+ {
+ C4_CHECK(t._p(n.m_next_sibling)->m_prev_sibling == node);
+ C4_CHECK(t._p(n.m_next_sibling)->m_next_sibling != node);
+ }
+
+ size_t count = 0;
+ for(size_t i = n.m_first_child; i != NONE; i = t.next_sibling(i))
+ {
+#ifdef RYML_DBG
+ printf("check(%zu): descend to child[%zu]=%zu\n", node, count, i);
+#endif
+ auto const& ch = *t._p(i);
+ C4_CHECK(ch.m_parent == node);
+ C4_CHECK(ch.m_next_sibling != i);
+ ++count;
+ }
+ C4_CHECK(count == t.num_children(node));
+
+ if(n.m_prev_sibling == NONE && n.m_next_sibling == NONE)
+ {
+ if(n.m_parent != NONE)
+ {
+ C4_CHECK(t.num_children(n.m_parent) == 1);
+ C4_CHECK(t.num_siblings(node) == 1);
+ }
+ }
+
+ if(node == t.root_id())
+ {
+ C4_CHECK(t.size() == t.m_size);
+ C4_CHECK(t.capacity() == t.m_cap);
+ C4_CHECK(t.m_cap == t.m_size + t.slack());
+ check_free_list(t);
+ check_arena(t);
+ }
+
+ for(size_t i = t.first_child(node); i != NONE; i = t.next_sibling(i))
+ {
+ check_invariants(t, i);
+ }
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+inline void check_free_list(Tree const& t)
+{
+ if(t.m_free_head == NONE)
+ {
+ C4_CHECK(t.m_free_tail == t.m_free_head);
+ return;
+ }
+
+ C4_CHECK(t.m_free_head >= 0 && t.m_free_head < t.m_cap);
+ C4_CHECK(t.m_free_tail >= 0 && t.m_free_tail < t.m_cap);
+
+ auto const& head = *t._p(t.m_free_head);
+ //auto const& tail = *t._p(t.m_free_tail);
+
+ //C4_CHECK(head.m_prev_sibling == NONE);
+ //C4_CHECK(tail.m_next_sibling == NONE);
+
+ size_t count = 0;
+ for(size_t i = t.m_free_head, prev = NONE; i != NONE; i = t._p(i)->m_next_sibling)
+ {
+ auto const& elm = *t._p(i);
+ if(&elm != &head)
+ {
+ C4_CHECK(elm.m_prev_sibling == prev);
+ }
+ prev = i;
+ ++count;
+ }
+ C4_CHECK(count == t.slack());
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+inline void check_arena(Tree const& t)
+{
+ C4_CHECK(t.m_arena.len == 0 || (t.m_arena_pos >= 0 && t.m_arena_pos <= t.m_arena.len));
+ C4_CHECK(t.arena_size() == t.m_arena_pos);
+ C4_CHECK(t.arena_slack() + t.m_arena_pos == t.m_arena.len);
+}
+
+
+} /* namespace yml */
+} /* namespace c4 */
+
+#ifdef __clang__
+# pragma clang diagnostic pop
+#elif defined(__GNUC__)
+# pragma GCC diagnostic pop
+#elif defined(_MSC_VER)
+# pragma warning(pop)
+#endif
+
+#endif /* C4_YML_DETAIL_CHECKS_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/detail/checks.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/detail/print.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/detail/print.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef C4_YML_DETAIL_PRINT_HPP_
+#define C4_YML_DETAIL_PRINT_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/tree.hpp
+//#include "c4/yml/tree.hpp"
+#if !defined(C4_YML_TREE_HPP_) && !defined(_C4_YML_TREE_HPP_)
+#error "amalgamate: file c4/yml/tree.hpp must have been included at this point"
+#endif /* C4_YML_TREE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/node.hpp
+//#include "c4/yml/node.hpp"
+#if !defined(C4_YML_NODE_HPP_) && !defined(_C4_YML_NODE_HPP_)
+#error "amalgamate: file c4/yml/node.hpp must have been included at this point"
+#endif /* C4_YML_NODE_HPP_ */
+
+
+
+namespace c4 {
+namespace yml {
+
+
+inline size_t print_node(Tree const& p, size_t node, int level, size_t count, bool print_children)
+{
+ printf("[%zd]%*s[%zd] %p", count, (2*level), "", node, (void*)p.get(node));
+ if(p.is_root(node))
+ {
+ printf(" [ROOT]");
+ }
+ printf(" %s:", p.type_str(node));
+ if(p.has_key(node))
+ {
+ if(p.has_key_anchor(node))
+ {
+ csubstr ka = p.key_anchor(node);
+ printf(" &%.*s", (int)ka.len, ka.str);
+ }
+ if(p.has_key_tag(node))
+ {
+ csubstr kt = p.key_tag(node);
+ csubstr k = p.key(node);
+ printf(" %.*s '%.*s'", (int)kt.len, kt.str, (int)k.len, k.str);
+ }
+ else
+ {
+ csubstr k = p.key(node);
+ printf(" '%.*s'", (int)k.len, k.str);
+ }
+ }
+ else
+ {
+ RYML_ASSERT( ! p.has_key_tag(node));
+ }
+ if(p.has_val(node))
+ {
+ if(p.has_val_tag(node))
+ {
+ csubstr vt = p.val_tag(node);
+ csubstr v = p.val(node);
+ printf(" %.*s '%.*s'", (int)vt.len, vt.str, (int)v.len, v.str);
+ }
+ else
+ {
+ csubstr v = p.val(node);
+ printf(" '%.*s'", (int)v.len, v.str);
+ }
+ }
+ else
+ {
+ if(p.has_val_tag(node))
+ {
+ csubstr vt = p.val_tag(node);
+ printf(" %.*s", (int)vt.len, vt.str);
+ }
+ }
+ if(p.has_val_anchor(node))
+ {
+ auto &a = p.val_anchor(node);
+ printf(" valanchor='&%.*s'", (int)a.len, a.str);
+ }
+ printf(" (%zd sibs)", p.num_siblings(node));
+
+ ++count;
+
+ if(p.is_container(node))
+ {
+ printf(" %zd children:\n", p.num_children(node));
+ if(print_children)
+ {
+ for(size_t i = p.first_child(node); i != NONE; i = p.next_sibling(i))
+ {
+ count = print_node(p, i, level+1, count, print_children);
+ }
+ }
+ }
+ else
+ {
+ printf("\n");
+ }
+
+ return count;
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+inline void print_node(NodeRef const& p, int level=0)
+{
+ print_node(*p.tree(), p.id(), level, 0, true);
+}
+
+
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+//-----------------------------------------------------------------------------
+
+inline size_t print_tree(Tree const& p, size_t node=NONE)
+{
+ printf("--------------------------------------\n");
+ size_t ret = 0;
+ if(!p.empty())
+ {
+ if(node == NONE)
+ node = p.root_id();
+ ret = print_node(p, node, 0, 0, true);
+ }
+ printf("#nodes=%zd vs #printed=%zd\n", p.size(), ret);
+ printf("--------------------------------------\n");
+ return ret;
+}
+
+
+} /* namespace yml */
+} /* namespace c4 */
+
+
+#endif /* C4_YML_DETAIL_PRINT_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/detail/print.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/c4/yml/yml.hpp
+// https://github.com/biojppm/rapidyaml/src/c4/yml/yml.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _C4_YML_YML_HPP_
+#define _C4_YML_YML_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/tree.hpp
+//#include "c4/yml/tree.hpp"
+#if !defined(C4_YML_TREE_HPP_) && !defined(_C4_YML_TREE_HPP_)
+#error "amalgamate: file c4/yml/tree.hpp must have been included at this point"
+#endif /* C4_YML_TREE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/node.hpp
+//#include "c4/yml/node.hpp"
+#if !defined(C4_YML_NODE_HPP_) && !defined(_C4_YML_NODE_HPP_)
+#error "amalgamate: file c4/yml/node.hpp must have been included at this point"
+#endif /* C4_YML_NODE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/emit.hpp
+//#include "c4/yml/emit.hpp"
+#if !defined(C4_YML_EMIT_HPP_) && !defined(_C4_YML_EMIT_HPP_)
+#error "amalgamate: file c4/yml/emit.hpp must have been included at this point"
+#endif /* C4_YML_EMIT_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/parse.hpp
+//#include "c4/yml/parse.hpp"
+#if !defined(C4_YML_PARSE_HPP_) && !defined(_C4_YML_PARSE_HPP_)
+#error "amalgamate: file c4/yml/parse.hpp must have been included at this point"
+#endif /* C4_YML_PARSE_HPP_ */
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/preprocess.hpp
+//#include "c4/yml/preprocess.hpp"
+#if !defined(C4_YML_PREPROCESS_HPP_) && !defined(_C4_YML_PREPROCESS_HPP_)
+#error "amalgamate: file c4/yml/preprocess.hpp must have been included at this point"
+#endif /* C4_YML_PREPROCESS_HPP_ */
+
+
+#endif // _C4_YML_YML_HPP_
+
+
+// (end https://github.com/biojppm/rapidyaml/src/c4/yml/yml.hpp)
+
+
+
+//********************************************************************************
+//--------------------------------------------------------------------------------
+// src/ryml.hpp
+// https://github.com/biojppm/rapidyaml/src/ryml.hpp
+//--------------------------------------------------------------------------------
+//********************************************************************************
+
+#ifndef _RYML_HPP_
+#define _RYML_HPP_
+
+// amalgamate: removed include of
+// https://github.com/biojppm/rapidyaml/src/c4/yml/yml.hpp
+//#include "c4/yml/yml.hpp"
+#if !defined(C4_YML_YML_HPP_) && !defined(_C4_YML_YML_HPP_)
+#error "amalgamate: file c4/yml/yml.hpp must have been included at this point"
+#endif /* C4_YML_YML_HPP_ */
+
+
+namespace ryml {
+using namespace c4::yml;
+using namespace c4;
+}
+
+#endif /* _RYML_HPP_ */
+
+
+// (end https://github.com/biojppm/rapidyaml/src/ryml.hpp)
+
+#endif /* _RYML_SINGLE_HEADER_AMALGAMATED_HPP_ */
+
diff --git a/src/third-party/robin_hood/robin_hood.h b/src/third-party/robin_hood/robin_hood.h
new file mode 100644
index 0000000..0af031f
--- /dev/null
+++ b/src/third-party/robin_hood/robin_hood.h
@@ -0,0 +1,2544 @@
+// ______ _____ ______ _________
+// ______________ ___ /_ ___(_)_______ ___ /_ ______ ______ ______ /
+// __ ___/_ __ \__ __ \__ / __ __ \ __ __ \_ __ \_ __ \_ __ /
+// _ / / /_/ /_ /_/ /_ / _ / / / _ / / // /_/ // /_/ // /_/ /
+// /_/ \____/ /_.___/ /_/ /_/ /_/ ________/_/ /_/ \____/ \____/ \__,_/
+// _/_____/
+//
+// Fast & memory efficient hashtable based on robin hood hashing for C++11/14/17/20
+// https://github.com/martinus/robin-hood-hashing
+//
+// Licensed under the MIT License <http://opensource.org/licenses/MIT>.
+// SPDX-License-Identifier: MIT
+// Copyright (c) 2018-2021 Martin Ankerl <http://martin.ankerl.com>
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+
+#ifndef ROBIN_HOOD_H_INCLUDED
+#define ROBIN_HOOD_H_INCLUDED
+
+// see https://semver.org/
+#define ROBIN_HOOD_VERSION_MAJOR 3 // for incompatible API changes
+#define ROBIN_HOOD_VERSION_MINOR 11 // for adding functionality in a backwards-compatible manner
+#define ROBIN_HOOD_VERSION_PATCH 5 // for backwards-compatible bug fixes
+
+#include <algorithm>
+#include <cstdlib>
+#include <cstring>
+#include <functional>
+#include <limits>
+#include <memory> // only to support hash of smart pointers
+#include <stdexcept>
+#include <string>
+#include <type_traits>
+#include <utility>
+#if __cplusplus >= 201703L
+# include <string_view>
+#endif
+
+// #define ROBIN_HOOD_LOG_ENABLED
+#ifdef ROBIN_HOOD_LOG_ENABLED
+# include <iostream>
+# define ROBIN_HOOD_LOG(...) \
+ std::cout << __FUNCTION__ << "@" << __LINE__ << ": " << __VA_ARGS__ << std::endl;
+#else
+# define ROBIN_HOOD_LOG(x)
+#endif
+
+// #define ROBIN_HOOD_TRACE_ENABLED
+#ifdef ROBIN_HOOD_TRACE_ENABLED
+# include <iostream>
+# define ROBIN_HOOD_TRACE(...) \
+ std::cout << __FUNCTION__ << "@" << __LINE__ << ": " << __VA_ARGS__ << std::endl;
+#else
+# define ROBIN_HOOD_TRACE(x)
+#endif
+
+// #define ROBIN_HOOD_COUNT_ENABLED
+#ifdef ROBIN_HOOD_COUNT_ENABLED
+# include <iostream>
+# define ROBIN_HOOD_COUNT(x) ++counts().x;
+namespace robin_hood {
+struct Counts {
+ uint64_t shiftUp{};
+ uint64_t shiftDown{};
+};
+inline std::ostream& operator<<(std::ostream& os, Counts const& c) {
+ return os << c.shiftUp << " shiftUp" << std::endl << c.shiftDown << " shiftDown" << std::endl;
+}
+
+static Counts& counts() {
+ static Counts counts{};
+ return counts;
+}
+} // namespace robin_hood
+#else
+# define ROBIN_HOOD_COUNT(x)
+#endif
+
+// all non-argument macros should use this facility. See
+// https://www.fluentcpp.com/2019/05/28/better-macros-better-flags/
+#define ROBIN_HOOD(x) ROBIN_HOOD_PRIVATE_DEFINITION_##x()
+
+// mark unused members with this macro
+#define ROBIN_HOOD_UNUSED(identifier)
+
+// bitness
+#if SIZE_MAX == UINT32_MAX
+# define ROBIN_HOOD_PRIVATE_DEFINITION_BITNESS() 32
+#elif SIZE_MAX == UINT64_MAX
+# define ROBIN_HOOD_PRIVATE_DEFINITION_BITNESS() 64
+#else
+# error Unsupported bitness
+#endif
+
+// endianess
+#ifdef _MSC_VER
+# define ROBIN_HOOD_PRIVATE_DEFINITION_LITTLE_ENDIAN() 1
+# define ROBIN_HOOD_PRIVATE_DEFINITION_BIG_ENDIAN() 0
+#else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_LITTLE_ENDIAN() \
+ (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
+# define ROBIN_HOOD_PRIVATE_DEFINITION_BIG_ENDIAN() (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
+#endif
+
+// inline
+#ifdef _MSC_VER
+# define ROBIN_HOOD_PRIVATE_DEFINITION_NOINLINE() __declspec(noinline)
+#else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_NOINLINE() __attribute__((noinline))
+#endif
+
+// exceptions
+#if !defined(__cpp_exceptions) && !defined(__EXCEPTIONS) && !defined(_CPPUNWIND)
+# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_EXCEPTIONS() 0
+#else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_EXCEPTIONS() 1
+#endif
+
+// count leading/trailing bits
+#if !defined(ROBIN_HOOD_DISABLE_INTRINSICS)
+# ifdef _MSC_VER
+# if ROBIN_HOOD(BITNESS) == 32
+# define ROBIN_HOOD_PRIVATE_DEFINITION_BITSCANFORWARD() _BitScanForward
+# else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_BITSCANFORWARD() _BitScanForward64
+# endif
+# include <intrin.h>
+# pragma intrinsic(ROBIN_HOOD(BITSCANFORWARD))
+# define ROBIN_HOOD_COUNT_TRAILING_ZEROES(x) \
+ [](size_t mask) noexcept -> int { \
+ unsigned long index; \
+ return ROBIN_HOOD(BITSCANFORWARD)(&index, mask) ? static_cast<int>(index) \
+ : ROBIN_HOOD(BITNESS); \
+ }(x)
+# else
+# if ROBIN_HOOD(BITNESS) == 32
+# define ROBIN_HOOD_PRIVATE_DEFINITION_CTZ() __builtin_ctzl
+# define ROBIN_HOOD_PRIVATE_DEFINITION_CLZ() __builtin_clzl
+# else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_CTZ() __builtin_ctzll
+# define ROBIN_HOOD_PRIVATE_DEFINITION_CLZ() __builtin_clzll
+# endif
+# define ROBIN_HOOD_COUNT_LEADING_ZEROES(x) ((x) ? ROBIN_HOOD(CLZ)(x) : ROBIN_HOOD(BITNESS))
+# define ROBIN_HOOD_COUNT_TRAILING_ZEROES(x) ((x) ? ROBIN_HOOD(CTZ)(x) : ROBIN_HOOD(BITNESS))
+# endif
+#endif
+
+// fallthrough
+#ifndef __has_cpp_attribute // For backwards compatibility
+# define __has_cpp_attribute(x) 0
+#endif
+#if __has_cpp_attribute(clang::fallthrough)
+# define ROBIN_HOOD_PRIVATE_DEFINITION_FALLTHROUGH() [[clang::fallthrough]]
+#elif __has_cpp_attribute(gnu::fallthrough)
+# define ROBIN_HOOD_PRIVATE_DEFINITION_FALLTHROUGH() [[gnu::fallthrough]]
+#else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_FALLTHROUGH()
+#endif
+
+// likely/unlikely
+#ifdef _MSC_VER
+# define ROBIN_HOOD_LIKELY(condition) condition
+# define ROBIN_HOOD_UNLIKELY(condition) condition
+#else
+# define ROBIN_HOOD_LIKELY(condition) __builtin_expect(condition, 1)
+# define ROBIN_HOOD_UNLIKELY(condition) __builtin_expect(condition, 0)
+#endif
+
+// detect if native wchar_t type is availiable in MSVC
+#ifdef _MSC_VER
+# ifdef _NATIVE_WCHAR_T_DEFINED
+# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_NATIVE_WCHART() 1
+# else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_NATIVE_WCHART() 0
+# endif
+#else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_HAS_NATIVE_WCHART() 1
+#endif
+
+// detect if MSVC supports the pair(std::piecewise_construct_t,...) consructor being constexpr
+#ifdef _MSC_VER
+# if _MSC_VER <= 1900
+# define ROBIN_HOOD_PRIVATE_DEFINITION_BROKEN_CONSTEXPR() 1
+# else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_BROKEN_CONSTEXPR() 0
+# endif
+#else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_BROKEN_CONSTEXPR() 0
+#endif
+
+// workaround missing "is_trivially_copyable" in g++ < 5.0
+// See https://stackoverflow.com/a/31798726/48181
+#if defined(__GNUC__) && __GNUC__ < 5
+# define ROBIN_HOOD_IS_TRIVIALLY_COPYABLE(...) __has_trivial_copy(__VA_ARGS__)
+#else
+# define ROBIN_HOOD_IS_TRIVIALLY_COPYABLE(...) std::is_trivially_copyable<__VA_ARGS__>::value
+#endif
+
+// helpers for C++ versions, see https://gcc.gnu.org/onlinedocs/cpp/Standard-Predefined-Macros.html
+#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX() __cplusplus
+#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX98() 199711L
+#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX11() 201103L
+#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX14() 201402L
+#define ROBIN_HOOD_PRIVATE_DEFINITION_CXX17() 201703L
+
+#if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX17)
+# define ROBIN_HOOD_PRIVATE_DEFINITION_NODISCARD() [[nodiscard]]
+#else
+# define ROBIN_HOOD_PRIVATE_DEFINITION_NODISCARD()
+#endif
+
+namespace robin_hood {
+
+#if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX14)
+# define ROBIN_HOOD_STD std
+#else
+
+// c++11 compatibility layer
+namespace ROBIN_HOOD_STD {
+template <class T>
+struct alignment_of
+ : std::integral_constant<std::size_t, alignof(typename std::remove_all_extents<T>::type)> {};
+
+template <class T, T... Ints>
+class integer_sequence {
+public:
+ using value_type = T;
+ static_assert(std::is_integral<value_type>::value, "not integral type");
+ static constexpr std::size_t size() noexcept {
+ return sizeof...(Ints);
+ }
+};
+template <std::size_t... Inds>
+using index_sequence = integer_sequence<std::size_t, Inds...>;
+
+namespace detail_ {
+template <class T, T Begin, T End, bool>
+struct IntSeqImpl {
+ using TValue = T;
+ static_assert(std::is_integral<TValue>::value, "not integral type");
+ static_assert(Begin >= 0 && Begin < End, "unexpected argument (Begin<0 || Begin<=End)");
+
+ template <class, class>
+ struct IntSeqCombiner;
+
+ template <TValue... Inds0, TValue... Inds1>
+ struct IntSeqCombiner<integer_sequence<TValue, Inds0...>, integer_sequence<TValue, Inds1...>> {
+ using TResult = integer_sequence<TValue, Inds0..., Inds1...>;
+ };
+
+ using TResult =
+ typename IntSeqCombiner<typename IntSeqImpl<TValue, Begin, Begin + (End - Begin) / 2,
+ (End - Begin) / 2 == 1>::TResult,
+ typename IntSeqImpl<TValue, Begin + (End - Begin) / 2, End,
+ (End - Begin + 1) / 2 == 1>::TResult>::TResult;
+};
+
+template <class T, T Begin>
+struct IntSeqImpl<T, Begin, Begin, false> {
+ using TValue = T;
+ static_assert(std::is_integral<TValue>::value, "not integral type");
+ static_assert(Begin >= 0, "unexpected argument (Begin<0)");
+ using TResult = integer_sequence<TValue>;
+};
+
+template <class T, T Begin, T End>
+struct IntSeqImpl<T, Begin, End, true> {
+ using TValue = T;
+ static_assert(std::is_integral<TValue>::value, "not integral type");
+ static_assert(Begin >= 0, "unexpected argument (Begin<0)");
+ using TResult = integer_sequence<TValue, Begin>;
+};
+} // namespace detail_
+
+template <class T, T N>
+using make_integer_sequence = typename detail_::IntSeqImpl<T, 0, N, (N - 0) == 1>::TResult;
+
+template <std::size_t N>
+using make_index_sequence = make_integer_sequence<std::size_t, N>;
+
+template <class... T>
+using index_sequence_for = make_index_sequence<sizeof...(T)>;
+
+} // namespace ROBIN_HOOD_STD
+
+#endif
+
+namespace detail {
+
+// make sure we static_cast to the correct type for hash_int
+#if ROBIN_HOOD(BITNESS) == 64
+using SizeT = uint64_t;
+#else
+using SizeT = uint32_t;
+#endif
+
+template <typename T>
+T rotr(T x, unsigned k) {
+ return (x >> k) | (x << (8U * sizeof(T) - k));
+}
+
+// This cast gets rid of warnings like "cast from 'uint8_t*' {aka 'unsigned char*'} to
+// 'uint64_t*' {aka 'long unsigned int*'} increases required alignment of target type". Use with
+// care!
+template <typename T>
+inline T reinterpret_cast_no_cast_align_warning(void* ptr) noexcept {
+ return reinterpret_cast<T>(ptr);
+}
+
+template <typename T>
+inline T reinterpret_cast_no_cast_align_warning(void const* ptr) noexcept {
+ return reinterpret_cast<T>(ptr);
+}
+
+// make sure this is not inlined as it is slow and dramatically enlarges code, thus making other
+// inlinings more difficult. Throws are also generally the slow path.
+template <typename E, typename... Args>
+[[noreturn]] ROBIN_HOOD(NOINLINE)
+#if ROBIN_HOOD(HAS_EXCEPTIONS)
+ void doThrow(Args&&... args) {
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-array-to-pointer-decay)
+ throw E(std::forward<Args>(args)...);
+}
+#else
+ void doThrow(Args&&... ROBIN_HOOD_UNUSED(args) /*unused*/) {
+ abort();
+}
+#endif
+
+template <typename E, typename T, typename... Args>
+T* assertNotNull(T* t, Args&&... args) {
+ if (ROBIN_HOOD_UNLIKELY(nullptr == t)) {
+ doThrow<E>(std::forward<Args>(args)...);
+ }
+ return t;
+}
+
+template <typename T>
+inline T unaligned_load(void const* ptr) noexcept {
+ // using memcpy so we don't get into unaligned load problems.
+ // compiler should optimize this very well anyways.
+ T t;
+ std::memcpy(&t, ptr, sizeof(T));
+ return t;
+}
+
+// Allocates bulks of memory for objects of type T. This deallocates the memory in the destructor,
+// and keeps a linked list of the allocated memory around. Overhead per allocation is the size of a
+// pointer.
+template <typename T, size_t MinNumAllocs = 4, size_t MaxNumAllocs = 256>
+class BulkPoolAllocator {
+public:
+ BulkPoolAllocator() noexcept = default;
+
+ // does not copy anything, just creates a new allocator.
+ BulkPoolAllocator(const BulkPoolAllocator& ROBIN_HOOD_UNUSED(o) /*unused*/) noexcept
+ : mHead(nullptr)
+ , mListForFree(nullptr) {}
+
+ BulkPoolAllocator(BulkPoolAllocator&& o) noexcept
+ : mHead(o.mHead)
+ , mListForFree(o.mListForFree) {
+ o.mListForFree = nullptr;
+ o.mHead = nullptr;
+ }
+
+ BulkPoolAllocator& operator=(BulkPoolAllocator&& o) noexcept {
+ reset();
+ mHead = o.mHead;
+ mListForFree = o.mListForFree;
+ o.mListForFree = nullptr;
+ o.mHead = nullptr;
+ return *this;
+ }
+
+ BulkPoolAllocator&
+ // NOLINTNEXTLINE(bugprone-unhandled-self-assignment,cert-oop54-cpp)
+ operator=(const BulkPoolAllocator& ROBIN_HOOD_UNUSED(o) /*unused*/) noexcept {
+ // does not do anything
+ return *this;
+ }
+
+ ~BulkPoolAllocator() noexcept {
+ reset();
+ }
+
+ // Deallocates all allocated memory.
+ void reset() noexcept {
+ while (mListForFree) {
+ T* tmp = *mListForFree;
+ ROBIN_HOOD_LOG("std::free")
+ std::free(mListForFree);
+ mListForFree = reinterpret_cast_no_cast_align_warning<T**>(tmp);
+ }
+ mHead = nullptr;
+ }
+
+ // allocates, but does NOT initialize. Use in-place new constructor, e.g.
+ // T* obj = pool.allocate();
+ // ::new (static_cast<void*>(obj)) T();
+ T* allocate() {
+ T* tmp = mHead;
+ if (!tmp) {
+ tmp = performAllocation();
+ }
+
+ mHead = *reinterpret_cast_no_cast_align_warning<T**>(tmp);
+ return tmp;
+ }
+
+ // does not actually deallocate but puts it in store.
+ // make sure you have already called the destructor! e.g. with
+ // obj->~T();
+ // pool.deallocate(obj);
+ void deallocate(T* obj) noexcept {
+ *reinterpret_cast_no_cast_align_warning<T**>(obj) = mHead;
+ mHead = obj;
+ }
+
+ // Adds an already allocated block of memory to the allocator. This allocator is from now on
+ // responsible for freeing the data (with free()). If the provided data is not large enough to
+ // make use of, it is immediately freed. Otherwise it is reused and freed in the destructor.
+ void addOrFree(void* ptr, const size_t numBytes) noexcept {
+ // calculate number of available elements in ptr
+ if (numBytes < ALIGNMENT + ALIGNED_SIZE) {
+ // not enough data for at least one element. Free and return.
+ ROBIN_HOOD_LOG("std::free")
+ std::free(ptr);
+ } else {
+ ROBIN_HOOD_LOG("add to buffer")
+ add(ptr, numBytes);
+ }
+ }
+
+ void swap(BulkPoolAllocator<T, MinNumAllocs, MaxNumAllocs>& other) noexcept {
+ using std::swap;
+ swap(mHead, other.mHead);
+ swap(mListForFree, other.mListForFree);
+ }
+
+private:
+ // iterates the list of allocated memory to calculate how many to alloc next.
+ // Recalculating this each time saves us a size_t member.
+ // This ignores the fact that memory blocks might have been added manually with addOrFree. In
+ // practice, this should not matter much.
+ ROBIN_HOOD(NODISCARD) size_t calcNumElementsToAlloc() const noexcept {
+ auto tmp = mListForFree;
+ size_t numAllocs = MinNumAllocs;
+
+ while (numAllocs * 2 <= MaxNumAllocs && tmp) {
+ auto x = reinterpret_cast<T***>(tmp);
+ tmp = *x;
+ numAllocs *= 2;
+ }
+
+ return numAllocs;
+ }
+
+ // WARNING: Underflow if numBytes < ALIGNMENT! This is guarded in addOrFree().
+ void add(void* ptr, const size_t numBytes) noexcept {
+ const size_t numElements = (numBytes - ALIGNMENT) / ALIGNED_SIZE;
+
+ auto data = reinterpret_cast<T**>(ptr);
+
+ // link free list
+ auto x = reinterpret_cast<T***>(data);
+ *x = mListForFree;
+ mListForFree = data;
+
+ // create linked list for newly allocated data
+ auto* const headT =
+ reinterpret_cast_no_cast_align_warning<T*>(reinterpret_cast<char*>(ptr) + ALIGNMENT);
+
+ auto* const head = reinterpret_cast<char*>(headT);
+
+ // Visual Studio compiler automatically unrolls this loop, which is pretty cool
+ for (size_t i = 0; i < numElements; ++i) {
+ *reinterpret_cast_no_cast_align_warning<char**>(head + i * ALIGNED_SIZE) =
+ head + (i + 1) * ALIGNED_SIZE;
+ }
+
+ // last one points to 0
+ *reinterpret_cast_no_cast_align_warning<T**>(head + (numElements - 1) * ALIGNED_SIZE) =
+ mHead;
+ mHead = headT;
+ }
+
+ // Called when no memory is available (mHead == 0).
+ // Don't inline this slow path.
+ ROBIN_HOOD(NOINLINE) T* performAllocation() {
+ size_t const numElementsToAlloc = calcNumElementsToAlloc();
+
+ // alloc new memory: [prev |T, T, ... T]
+ size_t const bytes = ALIGNMENT + ALIGNED_SIZE * numElementsToAlloc;
+ ROBIN_HOOD_LOG("std::malloc " << bytes << " = " << ALIGNMENT << " + " << ALIGNED_SIZE
+ << " * " << numElementsToAlloc)
+ add(assertNotNull<std::bad_alloc>(std::malloc(bytes)), bytes);
+ return mHead;
+ }
+
+ // enforce byte alignment of the T's
+#if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX14)
+ static constexpr size_t ALIGNMENT =
+ (std::max)(std::alignment_of<T>::value, std::alignment_of<T*>::value);
+#else
+ static const size_t ALIGNMENT =
+ (ROBIN_HOOD_STD::alignment_of<T>::value > ROBIN_HOOD_STD::alignment_of<T*>::value)
+ ? ROBIN_HOOD_STD::alignment_of<T>::value
+ : +ROBIN_HOOD_STD::alignment_of<T*>::value; // the + is for walkarround
+#endif
+
+ static constexpr size_t ALIGNED_SIZE = ((sizeof(T) - 1) / ALIGNMENT + 1) * ALIGNMENT;
+
+ static_assert(MinNumAllocs >= 1, "MinNumAllocs");
+ static_assert(MaxNumAllocs >= MinNumAllocs, "MaxNumAllocs");
+ static_assert(ALIGNED_SIZE >= sizeof(T*), "ALIGNED_SIZE");
+ static_assert(0 == (ALIGNED_SIZE % sizeof(T*)), "ALIGNED_SIZE mod");
+ static_assert(ALIGNMENT >= sizeof(T*), "ALIGNMENT");
+
+ T* mHead{nullptr};
+ T** mListForFree{nullptr};
+};
+
+template <typename T, size_t MinSize, size_t MaxSize, bool IsFlat>
+struct NodeAllocator;
+
+// dummy allocator that does nothing
+template <typename T, size_t MinSize, size_t MaxSize>
+struct NodeAllocator<T, MinSize, MaxSize, true> {
+
+ // we are not using the data, so just free it.
+ void addOrFree(void* ptr, size_t ROBIN_HOOD_UNUSED(numBytes) /*unused*/) noexcept {
+ ROBIN_HOOD_LOG("std::free")
+ std::free(ptr);
+ }
+};
+
+template <typename T, size_t MinSize, size_t MaxSize>
+struct NodeAllocator<T, MinSize, MaxSize, false> : public BulkPoolAllocator<T, MinSize, MaxSize> {};
+
+// c++14 doesn't have is_nothrow_swappable, and clang++ 6.0.1 doesn't like it either, so I'm making
+// my own here.
+namespace swappable {
+#if ROBIN_HOOD(CXX) < ROBIN_HOOD(CXX17)
+using std::swap;
+template <typename T>
+struct nothrow {
+ static const bool value = noexcept(swap(std::declval<T&>(), std::declval<T&>()));
+};
+#else
+template <typename T>
+struct nothrow {
+ static const bool value = std::is_nothrow_swappable<T>::value;
+};
+#endif
+} // namespace swappable
+
+} // namespace detail
+
+struct is_transparent_tag {};
+
+// A custom pair implementation is used in the map because std::pair is not is_trivially_copyable,
+// which means it would not be allowed to be used in std::memcpy. This struct is copyable, which is
+// also tested.
+template <typename T1, typename T2>
+struct pair {
+ using first_type = T1;
+ using second_type = T2;
+
+ template <typename U1 = T1, typename U2 = T2,
+ typename = typename std::enable_if<std::is_default_constructible<U1>::value &&
+ std::is_default_constructible<U2>::value>::type>
+ constexpr pair() noexcept(noexcept(U1()) && noexcept(U2()))
+ : first()
+ , second() {}
+
+ // pair constructors are explicit so we don't accidentally call this ctor when we don't have to.
+ explicit constexpr pair(std::pair<T1, T2> const& o) noexcept(
+ noexcept(T1(std::declval<T1 const&>())) && noexcept(T2(std::declval<T2 const&>())))
+ : first(o.first)
+ , second(o.second) {}
+
+ // pair constructors are explicit so we don't accidentally call this ctor when we don't have to.
+ explicit constexpr pair(std::pair<T1, T2>&& o) noexcept(noexcept(
+ T1(std::move(std::declval<T1&&>()))) && noexcept(T2(std::move(std::declval<T2&&>()))))
+ : first(std::move(o.first))
+ , second(std::move(o.second)) {}
+
+ constexpr pair(T1&& a, T2&& b) noexcept(noexcept(
+ T1(std::move(std::declval<T1&&>()))) && noexcept(T2(std::move(std::declval<T2&&>()))))
+ : first(std::move(a))
+ , second(std::move(b)) {}
+
+ template <typename U1, typename U2>
+ constexpr pair(U1&& a, U2&& b) noexcept(noexcept(T1(std::forward<U1>(
+ std::declval<U1&&>()))) && noexcept(T2(std::forward<U2>(std::declval<U2&&>()))))
+ : first(std::forward<U1>(a))
+ , second(std::forward<U2>(b)) {}
+
+ template <typename... U1, typename... U2>
+ // MSVC 2015 produces error "C2476: ‘constexpr’ constructor does not initialize all members"
+ // if this constructor is constexpr
+#if !ROBIN_HOOD(BROKEN_CONSTEXPR)
+ constexpr
+#endif
+ pair(std::piecewise_construct_t /*unused*/, std::tuple<U1...> a,
+ std::tuple<U2...>
+ b) noexcept(noexcept(pair(std::declval<std::tuple<U1...>&>(),
+ std::declval<std::tuple<U2...>&>(),
+ ROBIN_HOOD_STD::index_sequence_for<U1...>(),
+ ROBIN_HOOD_STD::index_sequence_for<U2...>())))
+ : pair(a, b, ROBIN_HOOD_STD::index_sequence_for<U1...>(),
+ ROBIN_HOOD_STD::index_sequence_for<U2...>()) {
+ }
+
+ // constructor called from the std::piecewise_construct_t ctor
+ template <typename... U1, size_t... I1, typename... U2, size_t... I2>
+ pair(std::tuple<U1...>& a, std::tuple<U2...>& b, ROBIN_HOOD_STD::index_sequence<I1...> /*unused*/, ROBIN_HOOD_STD::index_sequence<I2...> /*unused*/) noexcept(
+ noexcept(T1(std::forward<U1>(std::get<I1>(
+ std::declval<std::tuple<
+ U1...>&>()))...)) && noexcept(T2(std::
+ forward<U2>(std::get<I2>(
+ std::declval<std::tuple<U2...>&>()))...)))
+ : first(std::forward<U1>(std::get<I1>(a))...)
+ , second(std::forward<U2>(std::get<I2>(b))...) {
+ // make visual studio compiler happy about warning about unused a & b.
+ // Visual studio's pair implementation disables warning 4100.
+ (void)a;
+ (void)b;
+ }
+
+ void swap(pair<T1, T2>& o) noexcept((detail::swappable::nothrow<T1>::value) &&
+ (detail::swappable::nothrow<T2>::value)) {
+ using std::swap;
+ swap(first, o.first);
+ swap(second, o.second);
+ }
+
+ T1 first; // NOLINT(misc-non-private-member-variables-in-classes)
+ T2 second; // NOLINT(misc-non-private-member-variables-in-classes)
+};
+
+template <typename A, typename B>
+inline void swap(pair<A, B>& a, pair<A, B>& b) noexcept(
+ noexcept(std::declval<pair<A, B>&>().swap(std::declval<pair<A, B>&>()))) {
+ a.swap(b);
+}
+
+template <typename A, typename B>
+inline constexpr bool operator==(pair<A, B> const& x, pair<A, B> const& y) {
+ return (x.first == y.first) && (x.second == y.second);
+}
+template <typename A, typename B>
+inline constexpr bool operator!=(pair<A, B> const& x, pair<A, B> const& y) {
+ return !(x == y);
+}
+template <typename A, typename B>
+inline constexpr bool operator<(pair<A, B> const& x, pair<A, B> const& y) noexcept(noexcept(
+ std::declval<A const&>() < std::declval<A const&>()) && noexcept(std::declval<B const&>() <
+ std::declval<B const&>())) {
+ return x.first < y.first || (!(y.first < x.first) && x.second < y.second);
+}
+template <typename A, typename B>
+inline constexpr bool operator>(pair<A, B> const& x, pair<A, B> const& y) {
+ return y < x;
+}
+template <typename A, typename B>
+inline constexpr bool operator<=(pair<A, B> const& x, pair<A, B> const& y) {
+ return !(x > y);
+}
+template <typename A, typename B>
+inline constexpr bool operator>=(pair<A, B> const& x, pair<A, B> const& y) {
+ return !(x < y);
+}
+
+inline size_t hash_bytes(void const* ptr, size_t len) noexcept {
+ static constexpr uint64_t m = UINT64_C(0xc6a4a7935bd1e995);
+ static constexpr uint64_t seed = UINT64_C(0xe17a1465);
+ static constexpr unsigned int r = 47;
+
+ auto const* const data64 = static_cast<uint64_t const*>(ptr);
+ uint64_t h = seed ^ (len * m);
+
+ size_t const n_blocks = len / 8;
+ for (size_t i = 0; i < n_blocks; ++i) {
+ auto k = detail::unaligned_load<uint64_t>(data64 + i);
+
+ k *= m;
+ k ^= k >> r;
+ k *= m;
+
+ h ^= k;
+ h *= m;
+ }
+
+ auto const* const data8 = reinterpret_cast<uint8_t const*>(data64 + n_blocks);
+ switch (len & 7U) {
+ case 7:
+ h ^= static_cast<uint64_t>(data8[6]) << 48U;
+ ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH
+ case 6:
+ h ^= static_cast<uint64_t>(data8[5]) << 40U;
+ ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH
+ case 5:
+ h ^= static_cast<uint64_t>(data8[4]) << 32U;
+ ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH
+ case 4:
+ h ^= static_cast<uint64_t>(data8[3]) << 24U;
+ ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH
+ case 3:
+ h ^= static_cast<uint64_t>(data8[2]) << 16U;
+ ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH
+ case 2:
+ h ^= static_cast<uint64_t>(data8[1]) << 8U;
+ ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH
+ case 1:
+ h ^= static_cast<uint64_t>(data8[0]);
+ h *= m;
+ ROBIN_HOOD(FALLTHROUGH); // FALLTHROUGH
+ default:
+ break;
+ }
+
+ h ^= h >> r;
+
+ // not doing the final step here, because this will be done by keyToIdx anyways
+ // h *= m;
+ // h ^= h >> r;
+ return static_cast<size_t>(h);
+}
+
+inline size_t hash_int(uint64_t x) noexcept {
+ // tried lots of different hashes, let's stick with murmurhash3. It's simple, fast, well tested,
+ // and doesn't need any special 128bit operations.
+ x ^= x >> 33U;
+ x *= UINT64_C(0xff51afd7ed558ccd);
+ x ^= x >> 33U;
+
+ // not doing the final step here, because this will be done by keyToIdx anyways
+ // x *= UINT64_C(0xc4ceb9fe1a85ec53);
+ // x ^= x >> 33U;
+ return static_cast<size_t>(x);
+}
+
+// A thin wrapper around std::hash, performing an additional simple mixing step of the result.
+template <typename T, typename Enable = void>
+struct hash : public std::hash<T> {
+ size_t operator()(T const& obj) const
+ noexcept(noexcept(std::declval<std::hash<T>>().operator()(std::declval<T const&>()))) {
+ // call base hash
+ auto result = std::hash<T>::operator()(obj);
+ // return mixed of that, to be save against identity has
+ return hash_int(static_cast<detail::SizeT>(result));
+ }
+};
+
+template <typename CharT>
+struct hash<std::basic_string<CharT>> {
+ size_t operator()(std::basic_string<CharT> const& str) const noexcept {
+ return hash_bytes(str.data(), sizeof(CharT) * str.size());
+ }
+};
+
+#if ROBIN_HOOD(CXX) >= ROBIN_HOOD(CXX17)
+template <typename CharT>
+struct hash<std::basic_string_view<CharT>> {
+ size_t operator()(std::basic_string_view<CharT> const& sv) const noexcept {
+ return hash_bytes(sv.data(), sizeof(CharT) * sv.size());
+ }
+};
+#endif
+
+template <class T>
+struct hash<T*> {
+ size_t operator()(T* ptr) const noexcept {
+ return hash_int(reinterpret_cast<detail::SizeT>(ptr));
+ }
+};
+
+template <class T>
+struct hash<std::unique_ptr<T>> {
+ size_t operator()(std::unique_ptr<T> const& ptr) const noexcept {
+ return hash_int(reinterpret_cast<detail::SizeT>(ptr.get()));
+ }
+};
+
+template <class T>
+struct hash<std::shared_ptr<T>> {
+ size_t operator()(std::shared_ptr<T> const& ptr) const noexcept {
+ return hash_int(reinterpret_cast<detail::SizeT>(ptr.get()));
+ }
+};
+
+template <typename Enum>
+struct hash<Enum, typename std::enable_if<std::is_enum<Enum>::value>::type> {
+ size_t operator()(Enum e) const noexcept {
+ using Underlying = typename std::underlying_type<Enum>::type;
+ return hash<Underlying>{}(static_cast<Underlying>(e));
+ }
+};
+
+#define ROBIN_HOOD_HASH_INT(T) \
+ template <> \
+ struct hash<T> { \
+ size_t operator()(T const& obj) const noexcept { \
+ return hash_int(static_cast<uint64_t>(obj)); \
+ } \
+ }
+
+#if defined(__GNUC__) && !defined(__clang__)
+# pragma GCC diagnostic push
+# pragma GCC diagnostic ignored "-Wuseless-cast"
+#endif
+// see https://en.cppreference.com/w/cpp/utility/hash
+ROBIN_HOOD_HASH_INT(bool);
+ROBIN_HOOD_HASH_INT(char);
+ROBIN_HOOD_HASH_INT(signed char);
+ROBIN_HOOD_HASH_INT(unsigned char);
+ROBIN_HOOD_HASH_INT(char16_t);
+ROBIN_HOOD_HASH_INT(char32_t);
+#if ROBIN_HOOD(HAS_NATIVE_WCHART)
+ROBIN_HOOD_HASH_INT(wchar_t);
+#endif
+ROBIN_HOOD_HASH_INT(short);
+ROBIN_HOOD_HASH_INT(unsigned short);
+ROBIN_HOOD_HASH_INT(int);
+ROBIN_HOOD_HASH_INT(unsigned int);
+ROBIN_HOOD_HASH_INT(long);
+ROBIN_HOOD_HASH_INT(long long);
+ROBIN_HOOD_HASH_INT(unsigned long);
+ROBIN_HOOD_HASH_INT(unsigned long long);
+#if defined(__GNUC__) && !defined(__clang__)
+# pragma GCC diagnostic pop
+#endif
+namespace detail {
+
+template <typename T>
+struct void_type {
+ using type = void;
+};
+
+template <typename T, typename = void>
+struct has_is_transparent : public std::false_type {};
+
+template <typename T>
+struct has_is_transparent<T, typename void_type<typename T::is_transparent>::type>
+ : public std::true_type {};
+
+// using wrapper classes for hash and key_equal prevents the diamond problem when the same type
+// is used. see https://stackoverflow.com/a/28771920/48181
+template <typename T>
+struct WrapHash : public T {
+ WrapHash() = default;
+ explicit WrapHash(T const& o) noexcept(noexcept(T(std::declval<T const&>())))
+ : T(o) {}
+};
+
+template <typename T>
+struct WrapKeyEqual : public T {
+ WrapKeyEqual() = default;
+ explicit WrapKeyEqual(T const& o) noexcept(noexcept(T(std::declval<T const&>())))
+ : T(o) {}
+};
+
+// A highly optimized hashmap implementation, using the Robin Hood algorithm.
+//
+// In most cases, this map should be usable as a drop-in replacement for std::unordered_map, but
+// be about 2x faster in most cases and require much less allocations.
+//
+// This implementation uses the following memory layout:
+//
+// [Node, Node, ... Node | info, info, ... infoSentinel ]
+//
+// * Node: either a DataNode that directly has the std::pair<key, val> as member,
+// or a DataNode with a pointer to std::pair<key,val>. Which DataNode representation to use
+// depends on how fast the swap() operation is. Heuristically, this is automatically choosen
+// based on sizeof(). there are always 2^n Nodes.
+//
+// * info: Each Node in the map has a corresponding info byte, so there are 2^n info bytes.
+// Each byte is initialized to 0, meaning the corresponding Node is empty. Set to 1 means the
+// corresponding node contains data. Set to 2 means the corresponding Node is filled, but it
+// actually belongs to the previous position and was pushed out because that place is already
+// taken.
+//
+// * infoSentinel: Sentinel byte set to 1, so that iterator's ++ can stop at end() without the
+// need for a idx variable.
+//
+// According to STL, order of templates has effect on throughput. That's why I've moved the
+// boolean to the front.
+// https://www.reddit.com/r/cpp/comments/ahp6iu/compile_time_binary_size_reductions_and_cs_future/eeguck4/
+template <bool IsFlat, size_t MaxLoadFactor100, typename Key, typename T, typename Hash,
+ typename KeyEqual>
+class Table
+ : public WrapHash<Hash>,
+ public WrapKeyEqual<KeyEqual>,
+ detail::NodeAllocator<
+ typename std::conditional<
+ std::is_void<T>::value, Key,
+ robin_hood::pair<typename std::conditional<IsFlat, Key, Key const>::type, T>>::type,
+ 4, 16384, IsFlat> {
+public:
+ static constexpr bool is_flat = IsFlat;
+ static constexpr bool is_map = !std::is_void<T>::value;
+ static constexpr bool is_set = !is_map;
+ static constexpr bool is_transparent =
+ has_is_transparent<Hash>::value && has_is_transparent<KeyEqual>::value;
+
+ using key_type = Key;
+ using mapped_type = T;
+ using value_type = typename std::conditional<
+ is_set, Key,
+ robin_hood::pair<typename std::conditional<is_flat, Key, Key const>::type, T>>::type;
+ using size_type = size_t;
+ using hasher = Hash;
+ using key_equal = KeyEqual;
+ using Self = Table<IsFlat, MaxLoadFactor100, key_type, mapped_type, hasher, key_equal>;
+
+private:
+ static_assert(MaxLoadFactor100 > 10 && MaxLoadFactor100 < 100,
+ "MaxLoadFactor100 needs to be >10 && < 100");
+
+ using WHash = WrapHash<Hash>;
+ using WKeyEqual = WrapKeyEqual<KeyEqual>;
+
+ // configuration defaults
+
+ // make sure we have 8 elements, needed to quickly rehash mInfo
+ static constexpr size_t InitialNumElements = sizeof(uint64_t);
+ static constexpr uint32_t InitialInfoNumBits = 5;
+ static constexpr uint8_t InitialInfoInc = 1U << InitialInfoNumBits;
+ static constexpr size_t InfoMask = InitialInfoInc - 1U;
+ static constexpr uint8_t InitialInfoHashShift = 0;
+ using DataPool = detail::NodeAllocator<value_type, 4, 16384, IsFlat>;
+
+ // type needs to be wider than uint8_t.
+ using InfoType = uint32_t;
+
+ // DataNode ////////////////////////////////////////////////////////
+
+ // Primary template for the data node. We have special implementations for small and big
+ // objects. For large objects it is assumed that swap() is fairly slow, so we allocate these
+ // on the heap so swap merely swaps a pointer.
+ template <typename M, bool>
+ class DataNode {};
+
+ // Small: just allocate on the stack.
+ template <typename M>
+ class DataNode<M, true> final {
+ public:
+ template <typename... Args>
+ explicit DataNode(M& ROBIN_HOOD_UNUSED(map) /*unused*/, Args&&... args) noexcept(
+ noexcept(value_type(std::forward<Args>(args)...)))
+ : mData(std::forward<Args>(args)...) {}
+
+ DataNode(M& ROBIN_HOOD_UNUSED(map) /*unused*/, DataNode<M, true>&& n) noexcept(
+ std::is_nothrow_move_constructible<value_type>::value)
+ : mData(std::move(n.mData)) {}
+
+ // doesn't do anything
+ void destroy(M& ROBIN_HOOD_UNUSED(map) /*unused*/) noexcept {}
+ void destroyDoNotDeallocate() noexcept {}
+
+ value_type const* operator->() const noexcept {
+ return &mData;
+ }
+ value_type* operator->() noexcept {
+ return &mData;
+ }
+
+ const value_type& operator*() const noexcept {
+ return mData;
+ }
+
+ value_type& operator*() noexcept {
+ return mData;
+ }
+
+ template <typename VT = value_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_map, typename VT::first_type&>::type getFirst() noexcept {
+ return mData.first;
+ }
+ template <typename VT = value_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_set, VT&>::type getFirst() noexcept {
+ return mData;
+ }
+
+ template <typename VT = value_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_map, typename VT::first_type const&>::type
+ getFirst() const noexcept {
+ return mData.first;
+ }
+ template <typename VT = value_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_set, VT const&>::type getFirst() const noexcept {
+ return mData;
+ }
+
+ template <typename MT = mapped_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_map, MT&>::type getSecond() noexcept {
+ return mData.second;
+ }
+
+ template <typename MT = mapped_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_set, MT const&>::type getSecond() const noexcept {
+ return mData.second;
+ }
+
+ void swap(DataNode<M, true>& o) noexcept(
+ noexcept(std::declval<value_type>().swap(std::declval<value_type>()))) {
+ mData.swap(o.mData);
+ }
+
+ private:
+ value_type mData;
+ };
+
+ // big object: allocate on heap.
+ template <typename M>
+ class DataNode<M, false> {
+ public:
+ template <typename... Args>
+ explicit DataNode(M& map, Args&&... args)
+ : mData(map.allocate()) {
+ ::new (static_cast<void*>(mData)) value_type(std::forward<Args>(args)...);
+ }
+
+ DataNode(M& ROBIN_HOOD_UNUSED(map) /*unused*/, DataNode<M, false>&& n) noexcept
+ : mData(std::move(n.mData)) {}
+
+ void destroy(M& map) noexcept {
+ // don't deallocate, just put it into list of datapool.
+ mData->~value_type();
+ map.deallocate(mData);
+ }
+
+ void destroyDoNotDeallocate() noexcept {
+ mData->~value_type();
+ }
+
+ value_type const* operator->() const noexcept {
+ return mData;
+ }
+
+ value_type* operator->() noexcept {
+ return mData;
+ }
+
+ const value_type& operator*() const {
+ return *mData;
+ }
+
+ value_type& operator*() {
+ return *mData;
+ }
+
+ template <typename VT = value_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_map, typename VT::first_type&>::type getFirst() noexcept {
+ return mData->first;
+ }
+ template <typename VT = value_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_set, VT&>::type getFirst() noexcept {
+ return *mData;
+ }
+
+ template <typename VT = value_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_map, typename VT::first_type const&>::type
+ getFirst() const noexcept {
+ return mData->first;
+ }
+ template <typename VT = value_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_set, VT const&>::type getFirst() const noexcept {
+ return *mData;
+ }
+
+ template <typename MT = mapped_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_map, MT&>::type getSecond() noexcept {
+ return mData->second;
+ }
+
+ template <typename MT = mapped_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<is_map, MT const&>::type getSecond() const noexcept {
+ return mData->second;
+ }
+
+ void swap(DataNode<M, false>& o) noexcept {
+ using std::swap;
+ swap(mData, o.mData);
+ }
+
+ private:
+ value_type* mData;
+ };
+
+ using Node = DataNode<Self, IsFlat>;
+
+ // helpers for insertKeyPrepareEmptySpot: extract first entry (only const required)
+ ROBIN_HOOD(NODISCARD) key_type const& getFirstConst(Node const& n) const noexcept {
+ return n.getFirst();
+ }
+
+ // in case we have void mapped_type, we are not using a pair, thus we just route k through.
+ // No need to disable this because it's just not used if not applicable.
+ ROBIN_HOOD(NODISCARD) key_type const& getFirstConst(key_type const& k) const noexcept {
+ return k;
+ }
+
+ // in case we have non-void mapped_type, we have a standard robin_hood::pair
+ template <typename Q = mapped_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<!std::is_void<Q>::value, key_type const&>::type
+ getFirstConst(value_type const& vt) const noexcept {
+ return vt.first;
+ }
+
+ // Cloner //////////////////////////////////////////////////////////
+
+ template <typename M, bool UseMemcpy>
+ struct Cloner;
+
+ // fast path: Just copy data, without allocating anything.
+ template <typename M>
+ struct Cloner<M, true> {
+ void operator()(M const& source, M& target) const {
+ auto const* const src = reinterpret_cast<char const*>(source.mKeyVals);
+ auto* tgt = reinterpret_cast<char*>(target.mKeyVals);
+ auto const numElementsWithBuffer = target.calcNumElementsWithBuffer(target.mMask + 1);
+ std::copy(src, src + target.calcNumBytesTotal(numElementsWithBuffer), tgt);
+ }
+ };
+
+ template <typename M>
+ struct Cloner<M, false> {
+ void operator()(M const& s, M& t) const {
+ auto const numElementsWithBuffer = t.calcNumElementsWithBuffer(t.mMask + 1);
+ std::copy(s.mInfo, s.mInfo + t.calcNumBytesInfo(numElementsWithBuffer), t.mInfo);
+
+ for (size_t i = 0; i < numElementsWithBuffer; ++i) {
+ if (t.mInfo[i]) {
+ ::new (static_cast<void*>(t.mKeyVals + i)) Node(t, *s.mKeyVals[i]);
+ }
+ }
+ }
+ };
+
+ // Destroyer ///////////////////////////////////////////////////////
+
+ template <typename M, bool IsFlatAndTrivial>
+ struct Destroyer {};
+
+ template <typename M>
+ struct Destroyer<M, true> {
+ void nodes(M& m) const noexcept {
+ m.mNumElements = 0;
+ }
+
+ void nodesDoNotDeallocate(M& m) const noexcept {
+ m.mNumElements = 0;
+ }
+ };
+
+ template <typename M>
+ struct Destroyer<M, false> {
+ void nodes(M& m) const noexcept {
+ m.mNumElements = 0;
+ // clear also resets mInfo to 0, that's sometimes not necessary.
+ auto const numElementsWithBuffer = m.calcNumElementsWithBuffer(m.mMask + 1);
+
+ for (size_t idx = 0; idx < numElementsWithBuffer; ++idx) {
+ if (0 != m.mInfo[idx]) {
+ Node& n = m.mKeyVals[idx];
+ n.destroy(m);
+ n.~Node();
+ }
+ }
+ }
+
+ void nodesDoNotDeallocate(M& m) const noexcept {
+ m.mNumElements = 0;
+ // clear also resets mInfo to 0, that's sometimes not necessary.
+ auto const numElementsWithBuffer = m.calcNumElementsWithBuffer(m.mMask + 1);
+ for (size_t idx = 0; idx < numElementsWithBuffer; ++idx) {
+ if (0 != m.mInfo[idx]) {
+ Node& n = m.mKeyVals[idx];
+ n.destroyDoNotDeallocate();
+ n.~Node();
+ }
+ }
+ }
+ };
+
+ // Iter ////////////////////////////////////////////////////////////
+
+ struct fast_forward_tag {};
+
+ // generic iterator for both const_iterator and iterator.
+ template <bool IsConst>
+ // NOLINTNEXTLINE(hicpp-special-member-functions,cppcoreguidelines-special-member-functions)
+ class Iter {
+ private:
+ using NodePtr = typename std::conditional<IsConst, Node const*, Node*>::type;
+
+ public:
+ using difference_type = std::ptrdiff_t;
+ using value_type = typename Self::value_type;
+ using reference = typename std::conditional<IsConst, value_type const&, value_type&>::type;
+ using pointer = typename std::conditional<IsConst, value_type const*, value_type*>::type;
+ using iterator_category = std::forward_iterator_tag;
+
+ // default constructed iterator can be compared to itself, but WON'T return true when
+ // compared to end().
+ Iter() = default;
+
+ // Rule of zero: nothing specified. The conversion constructor is only enabled for
+ // iterator to const_iterator, so it doesn't accidentally work as a copy ctor.
+
+ // Conversion constructor from iterator to const_iterator.
+ template <bool OtherIsConst,
+ typename = typename std::enable_if<IsConst && !OtherIsConst>::type>
+ // NOLINTNEXTLINE(hicpp-explicit-conversions)
+ Iter(Iter<OtherIsConst> const& other) noexcept
+ : mKeyVals(other.mKeyVals)
+ , mInfo(other.mInfo) {}
+
+ Iter(NodePtr valPtr, uint8_t const* infoPtr) noexcept
+ : mKeyVals(valPtr)
+ , mInfo(infoPtr) {}
+
+ Iter(NodePtr valPtr, uint8_t const* infoPtr,
+ fast_forward_tag ROBIN_HOOD_UNUSED(tag) /*unused*/) noexcept
+ : mKeyVals(valPtr)
+ , mInfo(infoPtr) {
+ fastForward();
+ }
+
+ template <bool OtherIsConst,
+ typename = typename std::enable_if<IsConst && !OtherIsConst>::type>
+ Iter& operator=(Iter<OtherIsConst> const& other) noexcept {
+ mKeyVals = other.mKeyVals;
+ mInfo = other.mInfo;
+ return *this;
+ }
+
+ // prefix increment. Undefined behavior if we are at end()!
+ Iter& operator++() noexcept {
+ mInfo++;
+ mKeyVals++;
+ fastForward();
+ return *this;
+ }
+
+ Iter operator++(int) noexcept {
+ Iter tmp = *this;
+ ++(*this);
+ return tmp;
+ }
+
+ reference operator*() const {
+ return **mKeyVals;
+ }
+
+ pointer operator->() const {
+ return &**mKeyVals;
+ }
+
+ template <bool O>
+ bool operator==(Iter<O> const& o) const noexcept {
+ return mKeyVals == o.mKeyVals;
+ }
+
+ template <bool O>
+ bool operator!=(Iter<O> const& o) const noexcept {
+ return mKeyVals != o.mKeyVals;
+ }
+
+ private:
+ // fast forward to the next non-free info byte
+ // I've tried a few variants that don't depend on intrinsics, but unfortunately they are
+ // quite a bit slower than this one. So I've reverted that change again. See map_benchmark.
+ void fastForward() noexcept {
+ size_t n = 0;
+ while (0U == (n = detail::unaligned_load<size_t>(mInfo))) {
+ mInfo += sizeof(size_t);
+ mKeyVals += sizeof(size_t);
+ }
+#if defined(ROBIN_HOOD_DISABLE_INTRINSICS)
+ // we know for certain that within the next 8 bytes we'll find a non-zero one.
+ if (ROBIN_HOOD_UNLIKELY(0U == detail::unaligned_load<uint32_t>(mInfo))) {
+ mInfo += 4;
+ mKeyVals += 4;
+ }
+ if (ROBIN_HOOD_UNLIKELY(0U == detail::unaligned_load<uint16_t>(mInfo))) {
+ mInfo += 2;
+ mKeyVals += 2;
+ }
+ if (ROBIN_HOOD_UNLIKELY(0U == *mInfo)) {
+ mInfo += 1;
+ mKeyVals += 1;
+ }
+#else
+# if ROBIN_HOOD(LITTLE_ENDIAN)
+ auto inc = ROBIN_HOOD_COUNT_TRAILING_ZEROES(n) / 8;
+# else
+ auto inc = ROBIN_HOOD_COUNT_LEADING_ZEROES(n) / 8;
+# endif
+ mInfo += inc;
+ mKeyVals += inc;
+#endif
+ }
+
+ friend class Table<IsFlat, MaxLoadFactor100, key_type, mapped_type, hasher, key_equal>;
+ NodePtr mKeyVals{nullptr};
+ uint8_t const* mInfo{nullptr};
+ };
+
+ ////////////////////////////////////////////////////////////////////
+
+ // highly performance relevant code.
+ // Lower bits are used for indexing into the array (2^n size)
+ // The upper 1-5 bits need to be a reasonable good hash, to save comparisons.
+ template <typename HashKey>
+ void keyToIdx(HashKey&& key, size_t* idx, InfoType* info) const {
+ // In addition to whatever hash is used, add another mul & shift so we get better hashing.
+ // This serves as a bad hash prevention, if the given data is
+ // badly mixed.
+ auto h = static_cast<uint64_t>(WHash::operator()(key));
+
+ h *= mHashMultiplier;
+ h ^= h >> 33U;
+
+ // the lower InitialInfoNumBits are reserved for info.
+ *info = mInfoInc + static_cast<InfoType>((h & InfoMask) >> mInfoHashShift);
+ *idx = (static_cast<size_t>(h) >> InitialInfoNumBits) & mMask;
+ }
+
+ // forwards the index by one, wrapping around at the end
+ void next(InfoType* info, size_t* idx) const noexcept {
+ *idx = *idx + 1;
+ *info += mInfoInc;
+ }
+
+ void nextWhileLess(InfoType* info, size_t* idx) const noexcept {
+ // unrolling this by hand did not bring any speedups.
+ while (*info < mInfo[*idx]) {
+ next(info, idx);
+ }
+ }
+
+ // Shift everything up by one element. Tries to move stuff around.
+ void
+ shiftUp(size_t startIdx,
+ size_t const insertion_idx) noexcept(std::is_nothrow_move_assignable<Node>::value) {
+ auto idx = startIdx;
+ ::new (static_cast<void*>(mKeyVals + idx)) Node(std::move(mKeyVals[idx - 1]));
+ while (--idx != insertion_idx) {
+ mKeyVals[idx] = std::move(mKeyVals[idx - 1]);
+ }
+
+ idx = startIdx;
+ while (idx != insertion_idx) {
+ ROBIN_HOOD_COUNT(shiftUp)
+ mInfo[idx] = static_cast<uint8_t>(mInfo[idx - 1] + mInfoInc);
+ if (ROBIN_HOOD_UNLIKELY(mInfo[idx] + mInfoInc > 0xFF)) {
+ mMaxNumElementsAllowed = 0;
+ }
+ --idx;
+ }
+ }
+
+ void shiftDown(size_t idx) noexcept(std::is_nothrow_move_assignable<Node>::value) {
+ // until we find one that is either empty or has zero offset.
+ // TODO(martinus) we don't need to move everything, just the last one for the same
+ // bucket.
+ mKeyVals[idx].destroy(*this);
+
+ // until we find one that is either empty or has zero offset.
+ while (mInfo[idx + 1] >= 2 * mInfoInc) {
+ ROBIN_HOOD_COUNT(shiftDown)
+ mInfo[idx] = static_cast<uint8_t>(mInfo[idx + 1] - mInfoInc);
+ mKeyVals[idx] = std::move(mKeyVals[idx + 1]);
+ ++idx;
+ }
+
+ mInfo[idx] = 0;
+ // don't destroy, we've moved it
+ // mKeyVals[idx].destroy(*this);
+ mKeyVals[idx].~Node();
+ }
+
+ // copy of find(), except that it returns iterator instead of const_iterator.
+ template <typename Other>
+ ROBIN_HOOD(NODISCARD)
+ size_t findIdx(Other const& key) const {
+ size_t idx{};
+ InfoType info{};
+ keyToIdx(key, &idx, &info);
+
+ do {
+ // unrolling this twice gives a bit of a speedup. More unrolling did not help.
+ if (info == mInfo[idx] &&
+ ROBIN_HOOD_LIKELY(WKeyEqual::operator()(key, mKeyVals[idx].getFirst()))) {
+ return idx;
+ }
+ next(&info, &idx);
+ if (info == mInfo[idx] &&
+ ROBIN_HOOD_LIKELY(WKeyEqual::operator()(key, mKeyVals[idx].getFirst()))) {
+ return idx;
+ }
+ next(&info, &idx);
+ } while (info <= mInfo[idx]);
+
+ // nothing found!
+ return mMask == 0 ? 0
+ : static_cast<size_t>(std::distance(
+ mKeyVals, reinterpret_cast_no_cast_align_warning<Node*>(mInfo)));
+ }
+
+ void cloneData(const Table& o) {
+ Cloner<Table, IsFlat && ROBIN_HOOD_IS_TRIVIALLY_COPYABLE(Node)>()(o, *this);
+ }
+
+ // inserts a keyval that is guaranteed to be new, e.g. when the hashmap is resized.
+ // @return True on success, false if something went wrong
+ void insert_move(Node&& keyval) {
+ // we don't retry, fail if overflowing
+ // don't need to check max num elements
+ if (0 == mMaxNumElementsAllowed && !try_increase_info()) {
+ throwOverflowError();
+ }
+
+ size_t idx{};
+ InfoType info{};
+ keyToIdx(keyval.getFirst(), &idx, &info);
+
+ // skip forward. Use <= because we are certain that the element is not there.
+ while (info <= mInfo[idx]) {
+ idx = idx + 1;
+ info += mInfoInc;
+ }
+
+ // key not found, so we are now exactly where we want to insert it.
+ auto const insertion_idx = idx;
+ auto const insertion_info = static_cast<uint8_t>(info);
+ if (ROBIN_HOOD_UNLIKELY(insertion_info + mInfoInc > 0xFF)) {
+ mMaxNumElementsAllowed = 0;
+ }
+
+ // find an empty spot
+ while (0 != mInfo[idx]) {
+ next(&info, &idx);
+ }
+
+ auto& l = mKeyVals[insertion_idx];
+ if (idx == insertion_idx) {
+ ::new (static_cast<void*>(&l)) Node(std::move(keyval));
+ } else {
+ shiftUp(idx, insertion_idx);
+ l = std::move(keyval);
+ }
+
+ // put at empty spot
+ mInfo[insertion_idx] = insertion_info;
+
+ ++mNumElements;
+ }
+
+public:
+ using iterator = Iter<false>;
+ using const_iterator = Iter<true>;
+
+ Table() noexcept(noexcept(Hash()) && noexcept(KeyEqual()))
+ : WHash()
+ , WKeyEqual() {
+ ROBIN_HOOD_TRACE(this)
+ }
+
+ // Creates an empty hash map. Nothing is allocated yet, this happens at the first insert.
+ // This tremendously speeds up ctor & dtor of a map that never receives an element. The
+ // penalty is payed at the first insert, and not before. Lookup of this empty map works
+ // because everybody points to DummyInfoByte::b. parameter bucket_count is dictated by the
+ // standard, but we can ignore it.
+ explicit Table(
+ size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/, const Hash& h = Hash{},
+ const KeyEqual& equal = KeyEqual{}) noexcept(noexcept(Hash(h)) && noexcept(KeyEqual(equal)))
+ : WHash(h)
+ , WKeyEqual(equal) {
+ ROBIN_HOOD_TRACE(this)
+ }
+
+ template <typename Iter>
+ Table(Iter first, Iter last, size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0,
+ const Hash& h = Hash{}, const KeyEqual& equal = KeyEqual{})
+ : WHash(h)
+ , WKeyEqual(equal) {
+ ROBIN_HOOD_TRACE(this)
+ insert(first, last);
+ }
+
+ Table(std::initializer_list<value_type> initlist,
+ size_t ROBIN_HOOD_UNUSED(bucket_count) /*unused*/ = 0, const Hash& h = Hash{},
+ const KeyEqual& equal = KeyEqual{})
+ : WHash(h)
+ , WKeyEqual(equal) {
+ ROBIN_HOOD_TRACE(this)
+ insert(initlist.begin(), initlist.end());
+ }
+
+ Table(Table&& o) noexcept
+ : WHash(std::move(static_cast<WHash&>(o)))
+ , WKeyEqual(std::move(static_cast<WKeyEqual&>(o)))
+ , DataPool(std::move(static_cast<DataPool&>(o))) {
+ ROBIN_HOOD_TRACE(this)
+ if (o.mMask) {
+ mHashMultiplier = std::move(o.mHashMultiplier);
+ mKeyVals = std::move(o.mKeyVals);
+ mInfo = std::move(o.mInfo);
+ mNumElements = std::move(o.mNumElements);
+ mMask = std::move(o.mMask);
+ mMaxNumElementsAllowed = std::move(o.mMaxNumElementsAllowed);
+ mInfoInc = std::move(o.mInfoInc);
+ mInfoHashShift = std::move(o.mInfoHashShift);
+ // set other's mask to 0 so its destructor won't do anything
+ o.init();
+ }
+ }
+
+ Table& operator=(Table&& o) noexcept {
+ ROBIN_HOOD_TRACE(this)
+ if (&o != this) {
+ if (o.mMask) {
+ // only move stuff if the other map actually has some data
+ destroy();
+ mHashMultiplier = std::move(o.mHashMultiplier);
+ mKeyVals = std::move(o.mKeyVals);
+ mInfo = std::move(o.mInfo);
+ mNumElements = std::move(o.mNumElements);
+ mMask = std::move(o.mMask);
+ mMaxNumElementsAllowed = std::move(o.mMaxNumElementsAllowed);
+ mInfoInc = std::move(o.mInfoInc);
+ mInfoHashShift = std::move(o.mInfoHashShift);
+ WHash::operator=(std::move(static_cast<WHash&>(o)));
+ WKeyEqual::operator=(std::move(static_cast<WKeyEqual&>(o)));
+ DataPool::operator=(std::move(static_cast<DataPool&>(o)));
+
+ o.init();
+
+ } else {
+ // nothing in the other map => just clear us.
+ clear();
+ }
+ }
+ return *this;
+ }
+
+ Table(const Table& o)
+ : WHash(static_cast<const WHash&>(o))
+ , WKeyEqual(static_cast<const WKeyEqual&>(o))
+ , DataPool(static_cast<const DataPool&>(o)) {
+ ROBIN_HOOD_TRACE(this)
+ if (!o.empty()) {
+ // not empty: create an exact copy. it is also possible to just iterate through all
+ // elements and insert them, but copying is probably faster.
+
+ auto const numElementsWithBuffer = calcNumElementsWithBuffer(o.mMask + 1);
+ auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer);
+
+ ROBIN_HOOD_LOG("std::malloc " << numBytesTotal << " = calcNumBytesTotal("
+ << numElementsWithBuffer << ")")
+ mHashMultiplier = o.mHashMultiplier;
+ mKeyVals = static_cast<Node*>(
+ detail::assertNotNull<std::bad_alloc>(std::malloc(numBytesTotal)));
+ // no need for calloc because clonData does memcpy
+ mInfo = reinterpret_cast<uint8_t*>(mKeyVals + numElementsWithBuffer);
+ mNumElements = o.mNumElements;
+ mMask = o.mMask;
+ mMaxNumElementsAllowed = o.mMaxNumElementsAllowed;
+ mInfoInc = o.mInfoInc;
+ mInfoHashShift = o.mInfoHashShift;
+ cloneData(o);
+ }
+ }
+
+ // Creates a copy of the given map. Copy constructor of each entry is used.
+ // Not sure why clang-tidy thinks this doesn't handle self assignment, it does
+ // NOLINTNEXTLINE(bugprone-unhandled-self-assignment,cert-oop54-cpp)
+ Table& operator=(Table const& o) {
+ ROBIN_HOOD_TRACE(this)
+ if (&o == this) {
+ // prevent assigning of itself
+ return *this;
+ }
+
+ // we keep using the old allocator and not assign the new one, because we want to keep
+ // the memory available. when it is the same size.
+ if (o.empty()) {
+ if (0 == mMask) {
+ // nothing to do, we are empty too
+ return *this;
+ }
+
+ // not empty: destroy what we have there
+ // clear also resets mInfo to 0, that's sometimes not necessary.
+ destroy();
+ init();
+ WHash::operator=(static_cast<const WHash&>(o));
+ WKeyEqual::operator=(static_cast<const WKeyEqual&>(o));
+ DataPool::operator=(static_cast<DataPool const&>(o));
+
+ return *this;
+ }
+
+ // clean up old stuff
+ Destroyer<Self, IsFlat && std::is_trivially_destructible<Node>::value>{}.nodes(*this);
+
+ if (mMask != o.mMask) {
+ // no luck: we don't have the same array size allocated, so we need to realloc.
+ if (0 != mMask) {
+ // only deallocate if we actually have data!
+ ROBIN_HOOD_LOG("std::free")
+ std::free(mKeyVals);
+ }
+
+ auto const numElementsWithBuffer = calcNumElementsWithBuffer(o.mMask + 1);
+ auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer);
+ ROBIN_HOOD_LOG("std::malloc " << numBytesTotal << " = calcNumBytesTotal("
+ << numElementsWithBuffer << ")")
+ mKeyVals = static_cast<Node*>(
+ detail::assertNotNull<std::bad_alloc>(std::malloc(numBytesTotal)));
+
+ // no need for calloc here because cloneData performs a memcpy.
+ mInfo = reinterpret_cast<uint8_t*>(mKeyVals + numElementsWithBuffer);
+ // sentinel is set in cloneData
+ }
+ WHash::operator=(static_cast<const WHash&>(o));
+ WKeyEqual::operator=(static_cast<const WKeyEqual&>(o));
+ DataPool::operator=(static_cast<DataPool const&>(o));
+ mHashMultiplier = o.mHashMultiplier;
+ mNumElements = o.mNumElements;
+ mMask = o.mMask;
+ mMaxNumElementsAllowed = o.mMaxNumElementsAllowed;
+ mInfoInc = o.mInfoInc;
+ mInfoHashShift = o.mInfoHashShift;
+ cloneData(o);
+
+ return *this;
+ }
+
+ // Swaps everything between the two maps.
+ void swap(Table& o) {
+ ROBIN_HOOD_TRACE(this)
+ using std::swap;
+ swap(o, *this);
+ }
+
+ // Clears all data, without resizing.
+ void clear() {
+ ROBIN_HOOD_TRACE(this)
+ if (empty()) {
+ // don't do anything! also important because we don't want to write to
+ // DummyInfoByte::b, even though we would just write 0 to it.
+ return;
+ }
+
+ Destroyer<Self, IsFlat && std::is_trivially_destructible<Node>::value>{}.nodes(*this);
+
+ auto const numElementsWithBuffer = calcNumElementsWithBuffer(mMask + 1);
+ // clear everything, then set the sentinel again
+ uint8_t const z = 0;
+ std::fill(mInfo, mInfo + calcNumBytesInfo(numElementsWithBuffer), z);
+ mInfo[numElementsWithBuffer] = 1;
+
+ mInfoInc = InitialInfoInc;
+ mInfoHashShift = InitialInfoHashShift;
+ }
+
+ // Destroys the map and all it's contents.
+ ~Table() {
+ ROBIN_HOOD_TRACE(this)
+ destroy();
+ }
+
+ // Checks if both tables contain the same entries. Order is irrelevant.
+ bool operator==(const Table& other) const {
+ ROBIN_HOOD_TRACE(this)
+ if (other.size() != size()) {
+ return false;
+ }
+ for (auto const& otherEntry : other) {
+ if (!has(otherEntry)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ bool operator!=(const Table& other) const {
+ ROBIN_HOOD_TRACE(this)
+ return !operator==(other);
+ }
+
+ template <typename Q = mapped_type>
+ typename std::enable_if<!std::is_void<Q>::value, Q&>::type operator[](const key_type& key) {
+ ROBIN_HOOD_TRACE(this)
+ auto idxAndState = insertKeyPrepareEmptySpot(key);
+ switch (idxAndState.second) {
+ case InsertionState::key_found:
+ break;
+
+ case InsertionState::new_node:
+ ::new (static_cast<void*>(&mKeyVals[idxAndState.first]))
+ Node(*this, std::piecewise_construct, std::forward_as_tuple(key),
+ std::forward_as_tuple());
+ break;
+
+ case InsertionState::overwrite_node:
+ mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct,
+ std::forward_as_tuple(key), std::forward_as_tuple());
+ break;
+
+ case InsertionState::overflow_error:
+ throwOverflowError();
+ }
+
+ return mKeyVals[idxAndState.first].getSecond();
+ }
+
+ template <typename Q = mapped_type>
+ typename std::enable_if<!std::is_void<Q>::value, Q&>::type operator[](key_type&& key) {
+ ROBIN_HOOD_TRACE(this)
+ auto idxAndState = insertKeyPrepareEmptySpot(key);
+ switch (idxAndState.second) {
+ case InsertionState::key_found:
+ break;
+
+ case InsertionState::new_node:
+ ::new (static_cast<void*>(&mKeyVals[idxAndState.first]))
+ Node(*this, std::piecewise_construct, std::forward_as_tuple(std::move(key)),
+ std::forward_as_tuple());
+ break;
+
+ case InsertionState::overwrite_node:
+ mKeyVals[idxAndState.first] =
+ Node(*this, std::piecewise_construct, std::forward_as_tuple(std::move(key)),
+ std::forward_as_tuple());
+ break;
+
+ case InsertionState::overflow_error:
+ throwOverflowError();
+ }
+
+ return mKeyVals[idxAndState.first].getSecond();
+ }
+
+ template <typename Iter>
+ void insert(Iter first, Iter last) {
+ for (; first != last; ++first) {
+ // value_type ctor needed because this might be called with std::pair's
+ insert(value_type(*first));
+ }
+ }
+
+ void insert(std::initializer_list<value_type> ilist) {
+ for (auto&& vt : ilist) {
+ insert(std::move(vt));
+ }
+ }
+
+ template <typename... Args>
+ std::pair<iterator, bool> emplace(Args&&... args) {
+ ROBIN_HOOD_TRACE(this)
+ Node n{*this, std::forward<Args>(args)...};
+ auto idxAndState = insertKeyPrepareEmptySpot(getFirstConst(n));
+ switch (idxAndState.second) {
+ case InsertionState::key_found:
+ n.destroy(*this);
+ break;
+
+ case InsertionState::new_node:
+ ::new (static_cast<void*>(&mKeyVals[idxAndState.first])) Node(*this, std::move(n));
+ break;
+
+ case InsertionState::overwrite_node:
+ mKeyVals[idxAndState.first] = std::move(n);
+ break;
+
+ case InsertionState::overflow_error:
+ n.destroy(*this);
+ throwOverflowError();
+ break;
+ }
+
+ return std::make_pair(iterator(mKeyVals + idxAndState.first, mInfo + idxAndState.first),
+ InsertionState::key_found != idxAndState.second);
+ }
+
+ template <typename... Args>
+ iterator emplace_hint(const_iterator position, Args&&... args) {
+ (void)position;
+ return emplace(std::forward<Args>(args)...).first;
+ }
+
+ template <typename... Args>
+ std::pair<iterator, bool> try_emplace(const key_type& key, Args&&... args) {
+ return try_emplace_impl(key, std::forward<Args>(args)...);
+ }
+
+ template <typename... Args>
+ std::pair<iterator, bool> try_emplace(key_type&& key, Args&&... args) {
+ return try_emplace_impl(std::move(key), std::forward<Args>(args)...);
+ }
+
+ template <typename... Args>
+ iterator try_emplace(const_iterator hint, const key_type& key, Args&&... args) {
+ (void)hint;
+ return try_emplace_impl(key, std::forward<Args>(args)...).first;
+ }
+
+ template <typename... Args>
+ iterator try_emplace(const_iterator hint, key_type&& key, Args&&... args) {
+ (void)hint;
+ return try_emplace_impl(std::move(key), std::forward<Args>(args)...).first;
+ }
+
+ template <typename Mapped>
+ std::pair<iterator, bool> insert_or_assign(const key_type& key, Mapped&& obj) {
+ return insertOrAssignImpl(key, std::forward<Mapped>(obj));
+ }
+
+ template <typename Mapped>
+ std::pair<iterator, bool> insert_or_assign(key_type&& key, Mapped&& obj) {
+ return insertOrAssignImpl(std::move(key), std::forward<Mapped>(obj));
+ }
+
+ template <typename Mapped>
+ iterator insert_or_assign(const_iterator hint, const key_type& key, Mapped&& obj) {
+ (void)hint;
+ return insertOrAssignImpl(key, std::forward<Mapped>(obj)).first;
+ }
+
+ template <typename Mapped>
+ iterator insert_or_assign(const_iterator hint, key_type&& key, Mapped&& obj) {
+ (void)hint;
+ return insertOrAssignImpl(std::move(key), std::forward<Mapped>(obj)).first;
+ }
+
+ std::pair<iterator, bool> insert(const value_type& keyval) {
+ ROBIN_HOOD_TRACE(this)
+ return emplace(keyval);
+ }
+
+ iterator insert(const_iterator hint, const value_type& keyval) {
+ (void)hint;
+ return emplace(keyval).first;
+ }
+
+ std::pair<iterator, bool> insert(value_type&& keyval) {
+ return emplace(std::move(keyval));
+ }
+
+ iterator insert(const_iterator hint, value_type&& keyval) {
+ (void)hint;
+ return emplace(std::move(keyval)).first;
+ }
+
+ // Returns 1 if key is found, 0 otherwise.
+ size_t count(const key_type& key) const { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ auto kv = mKeyVals + findIdx(key);
+ if (kv != reinterpret_cast_no_cast_align_warning<Node*>(mInfo)) {
+ return 1;
+ }
+ return 0;
+ }
+
+ template <typename OtherKey, typename Self_ = Self>
+ // NOLINTNEXTLINE(modernize-use-nodiscard)
+ typename std::enable_if<Self_::is_transparent, size_t>::type count(const OtherKey& key) const {
+ ROBIN_HOOD_TRACE(this)
+ auto kv = mKeyVals + findIdx(key);
+ if (kv != reinterpret_cast_no_cast_align_warning<Node*>(mInfo)) {
+ return 1;
+ }
+ return 0;
+ }
+
+ bool contains(const key_type& key) const { // NOLINT(modernize-use-nodiscard)
+ return 1U == count(key);
+ }
+
+ template <typename OtherKey, typename Self_ = Self>
+ // NOLINTNEXTLINE(modernize-use-nodiscard)
+ typename std::enable_if<Self_::is_transparent, bool>::type contains(const OtherKey& key) const {
+ return 1U == count(key);
+ }
+
+ // Returns a reference to the value found for key.
+ // Throws std::out_of_range if element cannot be found
+ template <typename Q = mapped_type>
+ // NOLINTNEXTLINE(modernize-use-nodiscard)
+ typename std::enable_if<!std::is_void<Q>::value, Q&>::type at(key_type const& key) {
+ ROBIN_HOOD_TRACE(this)
+ auto kv = mKeyVals + findIdx(key);
+ if (kv == reinterpret_cast_no_cast_align_warning<Node*>(mInfo)) {
+ doThrow<std::out_of_range>("key not found");
+ }
+ return kv->getSecond();
+ }
+
+ // Returns a reference to the value found for key.
+ // Throws std::out_of_range if element cannot be found
+ template <typename Q = mapped_type>
+ // NOLINTNEXTLINE(modernize-use-nodiscard)
+ typename std::enable_if<!std::is_void<Q>::value, Q const&>::type at(key_type const& key) const {
+ ROBIN_HOOD_TRACE(this)
+ auto kv = mKeyVals + findIdx(key);
+ if (kv == reinterpret_cast_no_cast_align_warning<Node*>(mInfo)) {
+ doThrow<std::out_of_range>("key not found");
+ }
+ return kv->getSecond();
+ }
+
+ const_iterator find(const key_type& key) const { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ const size_t idx = findIdx(key);
+ return const_iterator{mKeyVals + idx, mInfo + idx};
+ }
+
+ template <typename OtherKey>
+ const_iterator find(const OtherKey& key, is_transparent_tag /*unused*/) const {
+ ROBIN_HOOD_TRACE(this)
+ const size_t idx = findIdx(key);
+ return const_iterator{mKeyVals + idx, mInfo + idx};
+ }
+
+ template <typename OtherKey, typename Self_ = Self>
+ typename std::enable_if<Self_::is_transparent, // NOLINT(modernize-use-nodiscard)
+ const_iterator>::type // NOLINT(modernize-use-nodiscard)
+ find(const OtherKey& key) const { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ const size_t idx = findIdx(key);
+ return const_iterator{mKeyVals + idx, mInfo + idx};
+ }
+
+ iterator find(const key_type& key) {
+ ROBIN_HOOD_TRACE(this)
+ const size_t idx = findIdx(key);
+ return iterator{mKeyVals + idx, mInfo + idx};
+ }
+
+ template <typename OtherKey>
+ iterator find(const OtherKey& key, is_transparent_tag /*unused*/) {
+ ROBIN_HOOD_TRACE(this)
+ const size_t idx = findIdx(key);
+ return iterator{mKeyVals + idx, mInfo + idx};
+ }
+
+ template <typename OtherKey, typename Self_ = Self>
+ typename std::enable_if<Self_::is_transparent, iterator>::type find(const OtherKey& key) {
+ ROBIN_HOOD_TRACE(this)
+ const size_t idx = findIdx(key);
+ return iterator{mKeyVals + idx, mInfo + idx};
+ }
+
+ iterator begin() {
+ ROBIN_HOOD_TRACE(this)
+ if (empty()) {
+ return end();
+ }
+ return iterator(mKeyVals, mInfo, fast_forward_tag{});
+ }
+ const_iterator begin() const { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ return cbegin();
+ }
+ const_iterator cbegin() const { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ if (empty()) {
+ return cend();
+ }
+ return const_iterator(mKeyVals, mInfo, fast_forward_tag{});
+ }
+
+ iterator end() {
+ ROBIN_HOOD_TRACE(this)
+ // no need to supply valid info pointer: end() must not be dereferenced, and only node
+ // pointer is compared.
+ return iterator{reinterpret_cast_no_cast_align_warning<Node*>(mInfo), nullptr};
+ }
+ const_iterator end() const { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ return cend();
+ }
+ const_iterator cend() const { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ return const_iterator{reinterpret_cast_no_cast_align_warning<Node*>(mInfo), nullptr};
+ }
+
+ iterator erase(const_iterator pos) {
+ ROBIN_HOOD_TRACE(this)
+ // its safe to perform const cast here
+ // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
+ return erase(iterator{const_cast<Node*>(pos.mKeyVals), const_cast<uint8_t*>(pos.mInfo)});
+ }
+
+ // Erases element at pos, returns iterator to the next element.
+ iterator erase(iterator pos) {
+ ROBIN_HOOD_TRACE(this)
+ // we assume that pos always points to a valid entry, and not end().
+ auto const idx = static_cast<size_t>(pos.mKeyVals - mKeyVals);
+
+ shiftDown(idx);
+ --mNumElements;
+
+ if (*pos.mInfo) {
+ // we've backward shifted, return this again
+ return pos;
+ }
+
+ // no backward shift, return next element
+ return ++pos;
+ }
+
+ size_t erase(const key_type& key) {
+ ROBIN_HOOD_TRACE(this)
+ size_t idx{};
+ InfoType info{};
+ keyToIdx(key, &idx, &info);
+
+ // check while info matches with the source idx
+ do {
+ if (info == mInfo[idx] && WKeyEqual::operator()(key, mKeyVals[idx].getFirst())) {
+ shiftDown(idx);
+ --mNumElements;
+ return 1;
+ }
+ next(&info, &idx);
+ } while (info <= mInfo[idx]);
+
+ // nothing found to delete
+ return 0;
+ }
+
+ // reserves space for the specified number of elements. Makes sure the old data fits.
+ // exactly the same as reserve(c).
+ void rehash(size_t c) {
+ // forces a reserve
+ reserve(c, true);
+ }
+
+ // reserves space for the specified number of elements. Makes sure the old data fits.
+ // Exactly the same as rehash(c). Use rehash(0) to shrink to fit.
+ void reserve(size_t c) {
+ // reserve, but don't force rehash
+ reserve(c, false);
+ }
+
+ // If possible reallocates the map to a smaller one. This frees the underlying table.
+ // Does not do anything if load_factor is too large for decreasing the table's size.
+ void compact() {
+ ROBIN_HOOD_TRACE(this)
+ auto newSize = InitialNumElements;
+ while (calcMaxNumElementsAllowed(newSize) < mNumElements && newSize != 0) {
+ newSize *= 2;
+ }
+ if (ROBIN_HOOD_UNLIKELY(newSize == 0)) {
+ throwOverflowError();
+ }
+
+ ROBIN_HOOD_LOG("newSize > mMask + 1: " << newSize << " > " << mMask << " + 1")
+
+ // only actually do anything when the new size is bigger than the old one. This prevents to
+ // continuously allocate for each reserve() call.
+ if (newSize < mMask + 1) {
+ rehashPowerOfTwo(newSize, true);
+ }
+ }
+
+ size_type size() const noexcept { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ return mNumElements;
+ }
+
+ size_type max_size() const noexcept { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ return static_cast<size_type>(-1);
+ }
+
+ ROBIN_HOOD(NODISCARD) bool empty() const noexcept {
+ ROBIN_HOOD_TRACE(this)
+ return 0 == mNumElements;
+ }
+
+ float max_load_factor() const noexcept { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ return MaxLoadFactor100 / 100.0F;
+ }
+
+ // Average number of elements per bucket. Since we allow only 1 per bucket
+ float load_factor() const noexcept { // NOLINT(modernize-use-nodiscard)
+ ROBIN_HOOD_TRACE(this)
+ return static_cast<float>(size()) / static_cast<float>(mMask + 1);
+ }
+
+ ROBIN_HOOD(NODISCARD) size_t mask() const noexcept {
+ ROBIN_HOOD_TRACE(this)
+ return mMask;
+ }
+
+ ROBIN_HOOD(NODISCARD) size_t calcMaxNumElementsAllowed(size_t maxElements) const noexcept {
+ if (ROBIN_HOOD_LIKELY(maxElements <= (std::numeric_limits<size_t>::max)() / 100)) {
+ return maxElements * MaxLoadFactor100 / 100;
+ }
+
+ // we might be a bit inprecise, but since maxElements is quite large that doesn't matter
+ return (maxElements / 100) * MaxLoadFactor100;
+ }
+
+ ROBIN_HOOD(NODISCARD) size_t calcNumBytesInfo(size_t numElements) const noexcept {
+ // we add a uint64_t, which houses the sentinel (first byte) and padding so we can load
+ // 64bit types.
+ return numElements + sizeof(uint64_t);
+ }
+
+ ROBIN_HOOD(NODISCARD)
+ size_t calcNumElementsWithBuffer(size_t numElements) const noexcept {
+ auto maxNumElementsAllowed = calcMaxNumElementsAllowed(numElements);
+ return numElements + (std::min)(maxNumElementsAllowed, (static_cast<size_t>(0xFF)));
+ }
+
+ // calculation only allowed for 2^n values
+ ROBIN_HOOD(NODISCARD) size_t calcNumBytesTotal(size_t numElements) const {
+#if ROBIN_HOOD(BITNESS) == 64
+ return numElements * sizeof(Node) + calcNumBytesInfo(numElements);
+#else
+ // make sure we're doing 64bit operations, so we are at least safe against 32bit overflows.
+ auto const ne = static_cast<uint64_t>(numElements);
+ auto const s = static_cast<uint64_t>(sizeof(Node));
+ auto const infos = static_cast<uint64_t>(calcNumBytesInfo(numElements));
+
+ auto const total64 = ne * s + infos;
+ auto const total = static_cast<size_t>(total64);
+
+ if (ROBIN_HOOD_UNLIKELY(static_cast<uint64_t>(total) != total64)) {
+ throwOverflowError();
+ }
+ return total;
+#endif
+ }
+
+private:
+ template <typename Q = mapped_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<!std::is_void<Q>::value, bool>::type has(const value_type& e) const {
+ ROBIN_HOOD_TRACE(this)
+ auto it = find(e.first);
+ return it != end() && it->second == e.second;
+ }
+
+ template <typename Q = mapped_type>
+ ROBIN_HOOD(NODISCARD)
+ typename std::enable_if<std::is_void<Q>::value, bool>::type has(const value_type& e) const {
+ ROBIN_HOOD_TRACE(this)
+ return find(e) != end();
+ }
+
+ void reserve(size_t c, bool forceRehash) {
+ ROBIN_HOOD_TRACE(this)
+ auto const minElementsAllowed = (std::max)(c, mNumElements);
+ auto newSize = InitialNumElements;
+ while (calcMaxNumElementsAllowed(newSize) < minElementsAllowed && newSize != 0) {
+ newSize *= 2;
+ }
+ if (ROBIN_HOOD_UNLIKELY(newSize == 0)) {
+ throwOverflowError();
+ }
+
+ ROBIN_HOOD_LOG("newSize > mMask + 1: " << newSize << " > " << mMask << " + 1")
+
+ // only actually do anything when the new size is bigger than the old one. This prevents to
+ // continuously allocate for each reserve() call.
+ if (forceRehash || newSize > mMask + 1) {
+ rehashPowerOfTwo(newSize, false);
+ }
+ }
+
+ // reserves space for at least the specified number of elements.
+ // only works if numBuckets if power of two
+ // True on success, false otherwise
+ void rehashPowerOfTwo(size_t numBuckets, bool forceFree) {
+ ROBIN_HOOD_TRACE(this)
+
+ Node* const oldKeyVals = mKeyVals;
+ uint8_t const* const oldInfo = mInfo;
+
+ const size_t oldMaxElementsWithBuffer = calcNumElementsWithBuffer(mMask + 1);
+
+ // resize operation: move stuff
+ initData(numBuckets);
+ if (oldMaxElementsWithBuffer > 1) {
+ for (size_t i = 0; i < oldMaxElementsWithBuffer; ++i) {
+ if (oldInfo[i] != 0) {
+ // might throw an exception, which is really bad since we are in the middle of
+ // moving stuff.
+ insert_move(std::move(oldKeyVals[i]));
+ // destroy the node but DON'T destroy the data.
+ oldKeyVals[i].~Node();
+ }
+ }
+
+ // this check is not necessary as it's guarded by the previous if, but it helps
+ // silence g++'s overeager "attempt to free a non-heap object 'map'
+ // [-Werror=free-nonheap-object]" warning.
+ if (oldKeyVals != reinterpret_cast_no_cast_align_warning<Node*>(&mMask)) {
+ // don't destroy old data: put it into the pool instead
+ if (forceFree) {
+ std::free(oldKeyVals);
+ } else {
+ DataPool::addOrFree(oldKeyVals, calcNumBytesTotal(oldMaxElementsWithBuffer));
+ }
+ }
+ }
+ }
+
+ ROBIN_HOOD(NOINLINE) void throwOverflowError() const {
+#if ROBIN_HOOD(HAS_EXCEPTIONS)
+ throw std::overflow_error("robin_hood::map overflow");
+#else
+ abort();
+#endif
+ }
+
+ template <typename OtherKey, typename... Args>
+ std::pair<iterator, bool> try_emplace_impl(OtherKey&& key, Args&&... args) {
+ ROBIN_HOOD_TRACE(this)
+ auto idxAndState = insertKeyPrepareEmptySpot(key);
+ switch (idxAndState.second) {
+ case InsertionState::key_found:
+ break;
+
+ case InsertionState::new_node:
+ ::new (static_cast<void*>(&mKeyVals[idxAndState.first])) Node(
+ *this, std::piecewise_construct, std::forward_as_tuple(std::forward<OtherKey>(key)),
+ std::forward_as_tuple(std::forward<Args>(args)...));
+ break;
+
+ case InsertionState::overwrite_node:
+ mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct,
+ std::forward_as_tuple(std::forward<OtherKey>(key)),
+ std::forward_as_tuple(std::forward<Args>(args)...));
+ break;
+
+ case InsertionState::overflow_error:
+ throwOverflowError();
+ break;
+ }
+
+ return std::make_pair(iterator(mKeyVals + idxAndState.first, mInfo + idxAndState.first),
+ InsertionState::key_found != idxAndState.second);
+ }
+
+ template <typename OtherKey, typename Mapped>
+ std::pair<iterator, bool> insertOrAssignImpl(OtherKey&& key, Mapped&& obj) {
+ ROBIN_HOOD_TRACE(this)
+ auto idxAndState = insertKeyPrepareEmptySpot(key);
+ switch (idxAndState.second) {
+ case InsertionState::key_found:
+ mKeyVals[idxAndState.first].getSecond() = std::forward<Mapped>(obj);
+ break;
+
+ case InsertionState::new_node:
+ ::new (static_cast<void*>(&mKeyVals[idxAndState.first])) Node(
+ *this, std::piecewise_construct, std::forward_as_tuple(std::forward<OtherKey>(key)),
+ std::forward_as_tuple(std::forward<Mapped>(obj)));
+ break;
+
+ case InsertionState::overwrite_node:
+ mKeyVals[idxAndState.first] = Node(*this, std::piecewise_construct,
+ std::forward_as_tuple(std::forward<OtherKey>(key)),
+ std::forward_as_tuple(std::forward<Mapped>(obj)));
+ break;
+
+ case InsertionState::overflow_error:
+ throwOverflowError();
+ break;
+ }
+
+ return std::make_pair(iterator(mKeyVals + idxAndState.first, mInfo + idxAndState.first),
+ InsertionState::key_found != idxAndState.second);
+ }
+
+ void initData(size_t max_elements) {
+ mNumElements = 0;
+ mMask = max_elements - 1;
+ mMaxNumElementsAllowed = calcMaxNumElementsAllowed(max_elements);
+
+ auto const numElementsWithBuffer = calcNumElementsWithBuffer(max_elements);
+
+ // malloc & zero mInfo. Faster than calloc everything.
+ auto const numBytesTotal = calcNumBytesTotal(numElementsWithBuffer);
+ ROBIN_HOOD_LOG("std::calloc " << numBytesTotal << " = calcNumBytesTotal("
+ << numElementsWithBuffer << ")")
+ mKeyVals = reinterpret_cast<Node*>(
+ detail::assertNotNull<std::bad_alloc>(std::malloc(numBytesTotal)));
+ mInfo = reinterpret_cast<uint8_t*>(mKeyVals + numElementsWithBuffer);
+ std::memset(mInfo, 0, numBytesTotal - numElementsWithBuffer * sizeof(Node));
+
+ // set sentinel
+ mInfo[numElementsWithBuffer] = 1;
+
+ mInfoInc = InitialInfoInc;
+ mInfoHashShift = InitialInfoHashShift;
+ }
+
+ enum class InsertionState { overflow_error, key_found, new_node, overwrite_node };
+
+ // Finds key, and if not already present prepares a spot where to pot the key & value.
+ // This potentially shifts nodes out of the way, updates mInfo and number of inserted
+ // elements, so the only operation left to do is create/assign a new node at that spot.
+ template <typename OtherKey>
+ std::pair<size_t, InsertionState> insertKeyPrepareEmptySpot(OtherKey&& key) {
+ for (int i = 0; i < 256; ++i) {
+ size_t idx{};
+ InfoType info{};
+ keyToIdx(key, &idx, &info);
+ nextWhileLess(&info, &idx);
+
+ // while we potentially have a match
+ while (info == mInfo[idx]) {
+ if (WKeyEqual::operator()(key, mKeyVals[idx].getFirst())) {
+ // key already exists, do NOT insert.
+ // see http://en.cppreference.com/w/cpp/container/unordered_map/insert
+ return std::make_pair(idx, InsertionState::key_found);
+ }
+ next(&info, &idx);
+ }
+
+ // unlikely that this evaluates to true
+ if (ROBIN_HOOD_UNLIKELY(mNumElements >= mMaxNumElementsAllowed)) {
+ if (!increase_size()) {
+ return std::make_pair(size_t(0), InsertionState::overflow_error);
+ }
+ continue;
+ }
+
+ // key not found, so we are now exactly where we want to insert it.
+ auto const insertion_idx = idx;
+ auto const insertion_info = info;
+ if (ROBIN_HOOD_UNLIKELY(insertion_info + mInfoInc > 0xFF)) {
+ mMaxNumElementsAllowed = 0;
+ }
+
+ // find an empty spot
+ while (0 != mInfo[idx]) {
+ next(&info, &idx);
+ }
+
+ if (idx != insertion_idx) {
+ shiftUp(idx, insertion_idx);
+ }
+ // put at empty spot
+ mInfo[insertion_idx] = static_cast<uint8_t>(insertion_info);
+ ++mNumElements;
+ return std::make_pair(insertion_idx, idx == insertion_idx
+ ? InsertionState::new_node
+ : InsertionState::overwrite_node);
+ }
+
+ // enough attempts failed, so finally give up.
+ return std::make_pair(size_t(0), InsertionState::overflow_error);
+ }
+
+ bool try_increase_info() {
+ ROBIN_HOOD_LOG("mInfoInc=" << mInfoInc << ", numElements=" << mNumElements
+ << ", maxNumElementsAllowed="
+ << calcMaxNumElementsAllowed(mMask + 1))
+ if (mInfoInc <= 2) {
+ // need to be > 2 so that shift works (otherwise undefined behavior!)
+ return false;
+ }
+ // we got space left, try to make info smaller
+ mInfoInc = static_cast<uint8_t>(mInfoInc >> 1U);
+
+ // remove one bit of the hash, leaving more space for the distance info.
+ // This is extremely fast because we can operate on 8 bytes at once.
+ ++mInfoHashShift;
+ auto const numElementsWithBuffer = calcNumElementsWithBuffer(mMask + 1);
+
+ for (size_t i = 0; i < numElementsWithBuffer; i += 8) {
+ auto val = unaligned_load<uint64_t>(mInfo + i);
+ val = (val >> 1U) & UINT64_C(0x7f7f7f7f7f7f7f7f);
+ std::memcpy(mInfo + i, &val, sizeof(val));
+ }
+ // update sentinel, which might have been cleared out!
+ mInfo[numElementsWithBuffer] = 1;
+
+ mMaxNumElementsAllowed = calcMaxNumElementsAllowed(mMask + 1);
+ return true;
+ }
+
+ // True if resize was possible, false otherwise
+ bool increase_size() {
+ // nothing allocated yet? just allocate InitialNumElements
+ if (0 == mMask) {
+ initData(InitialNumElements);
+ return true;
+ }
+
+ auto const maxNumElementsAllowed = calcMaxNumElementsAllowed(mMask + 1);
+ if (mNumElements < maxNumElementsAllowed && try_increase_info()) {
+ return true;
+ }
+
+ ROBIN_HOOD_LOG("mNumElements=" << mNumElements << ", maxNumElementsAllowed="
+ << maxNumElementsAllowed << ", load="
+ << (static_cast<double>(mNumElements) * 100.0 /
+ (static_cast<double>(mMask) + 1)))
+
+ if (mNumElements * 2 < calcMaxNumElementsAllowed(mMask + 1)) {
+ // we have to resize, even though there would still be plenty of space left!
+ // Try to rehash instead. Delete freed memory so we don't steadyily increase mem in case
+ // we have to rehash a few times
+ nextHashMultiplier();
+ rehashPowerOfTwo(mMask + 1, true);
+ } else {
+ // we've reached the capacity of the map, so the hash seems to work nice. Keep using it.
+ rehashPowerOfTwo((mMask + 1) * 2, false);
+ }
+ return true;
+ }
+
+ void nextHashMultiplier() {
+ // adding an *even* number, so that the multiplier will always stay odd. This is necessary
+ // so that the hash stays a mixing function (and thus doesn't have any information loss).
+ mHashMultiplier += UINT64_C(0xc4ceb9fe1a85ec54);
+ }
+
+ void destroy() {
+ if (0 == mMask) {
+ // don't deallocate!
+ return;
+ }
+
+ Destroyer<Self, IsFlat && std::is_trivially_destructible<Node>::value>{}
+ .nodesDoNotDeallocate(*this);
+
+ // This protection against not deleting mMask shouldn't be needed as it's sufficiently
+ // protected with the 0==mMask check, but I have this anyways because g++ 7 otherwise
+ // reports a compile error: attempt to free a non-heap object 'fm'
+ // [-Werror=free-nonheap-object]
+ if (mKeyVals != reinterpret_cast_no_cast_align_warning<Node*>(&mMask)) {
+ ROBIN_HOOD_LOG("std::free")
+ std::free(mKeyVals);
+ }
+ }
+
+ void init() noexcept {
+ mKeyVals = reinterpret_cast_no_cast_align_warning<Node*>(&mMask);
+ mInfo = reinterpret_cast<uint8_t*>(&mMask);
+ mNumElements = 0;
+ mMask = 0;
+ mMaxNumElementsAllowed = 0;
+ mInfoInc = InitialInfoInc;
+ mInfoHashShift = InitialInfoHashShift;
+ }
+
+ // members are sorted so no padding occurs
+ uint64_t mHashMultiplier = UINT64_C(0xc4ceb9fe1a85ec53); // 8 byte 8
+ Node* mKeyVals = reinterpret_cast_no_cast_align_warning<Node*>(&mMask); // 8 byte 16
+ uint8_t* mInfo = reinterpret_cast<uint8_t*>(&mMask); // 8 byte 24
+ size_t mNumElements = 0; // 8 byte 32
+ size_t mMask = 0; // 8 byte 40
+ size_t mMaxNumElementsAllowed = 0; // 8 byte 48
+ InfoType mInfoInc = InitialInfoInc; // 4 byte 52
+ InfoType mInfoHashShift = InitialInfoHashShift; // 4 byte 56
+ // 16 byte 56 if NodeAllocator
+};
+
+} // namespace detail
+
+// map
+
+template <typename Key, typename T, typename Hash = hash<Key>,
+ typename KeyEqual = std::equal_to<Key>, size_t MaxLoadFactor100 = 80>
+using unordered_flat_map = detail::Table<true, MaxLoadFactor100, Key, T, Hash, KeyEqual>;
+
+template <typename Key, typename T, typename Hash = hash<Key>,
+ typename KeyEqual = std::equal_to<Key>, size_t MaxLoadFactor100 = 80>
+using unordered_node_map = detail::Table<false, MaxLoadFactor100, Key, T, Hash, KeyEqual>;
+
+template <typename Key, typename T, typename Hash = hash<Key>,
+ typename KeyEqual = std::equal_to<Key>, size_t MaxLoadFactor100 = 80>
+using unordered_map =
+ detail::Table<sizeof(robin_hood::pair<Key, T>) <= sizeof(size_t) * 6 &&
+ std::is_nothrow_move_constructible<robin_hood::pair<Key, T>>::value &&
+ std::is_nothrow_move_assignable<robin_hood::pair<Key, T>>::value,
+ MaxLoadFactor100, Key, T, Hash, KeyEqual>;
+
+// set
+
+template <typename Key, typename Hash = hash<Key>, typename KeyEqual = std::equal_to<Key>,
+ size_t MaxLoadFactor100 = 80>
+using unordered_flat_set = detail::Table<true, MaxLoadFactor100, Key, void, Hash, KeyEqual>;
+
+template <typename Key, typename Hash = hash<Key>, typename KeyEqual = std::equal_to<Key>,
+ size_t MaxLoadFactor100 = 80>
+using unordered_node_set = detail::Table<false, MaxLoadFactor100, Key, void, Hash, KeyEqual>;
+
+template <typename Key, typename Hash = hash<Key>, typename KeyEqual = std::equal_to<Key>,
+ size_t MaxLoadFactor100 = 80>
+using unordered_set = detail::Table<sizeof(Key) <= sizeof(size_t) * 6 &&
+ std::is_nothrow_move_constructible<Key>::value &&
+ std::is_nothrow_move_assignable<Key>::value,
+ MaxLoadFactor100, Key, void, Hash, KeyEqual>;
+
+} // namespace robin_hood
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/all.h b/src/third-party/scnlib/include/scn/all.h
new file mode 100644
index 0000000..b69e822
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/all.h
@@ -0,0 +1,26 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_ALL_H
+#define SCN_ALL_H
+
+#include "scn.h"
+
+#include "istream.h"
+#include "tuple_return.h"
+
+#endif // SCN_ALL_H
diff --git a/src/third-party/scnlib/include/scn/detail/args.h b/src/third-party/scnlib/include/scn/detail/args.h
new file mode 100644
index 0000000..dd67852
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/args.h
@@ -0,0 +1,619 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_ARGS_H
+#define SCN_DETAIL_ARGS_H
+
+#include "../reader/common.h"
+#include "../util/array.h"
+
+SCN_GCC_PUSH
+SCN_GCC_IGNORE("-Wnoexcept")
+#include <string>
+SCN_GCC_POP
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ /**
+ * Allows reading an rvalue.
+ * Stores an rvalue and returns an lvalue reference to it via `operator()`.
+ * Create one with \ref temp.
+ */
+ template <typename T>
+ struct temporary {
+ temporary(T&& val) : value(SCN_MOVE(val)) {}
+
+ T& operator()() && noexcept
+ {
+ return value;
+ }
+
+ T value;
+ };
+ /**
+ * Factory function for \ref temporary.
+ *
+ * Canonical use case is with \ref scn::span:
+ * \code{.cpp}
+ * std::vector<char> buffer(32, '\0');
+ * auto result = scn::scan("123", "{}", scn::temp(scn::make_span(buffer)));
+ * // buffer == "123"
+ * \endcode
+ */
+ template <typename T,
+ typename std::enable_if<
+ !std::is_lvalue_reference<T>::value>::type* = nullptr>
+ temporary<T> temp(T&& val)
+ {
+ return {SCN_FWD(val)};
+ }
+
+ namespace detail {
+ enum type {
+ none_type = 0,
+ // signed integer
+ schar_type,
+ short_type,
+ int_type,
+ long_type,
+ long_long_type,
+ // unsigned integer
+ uchar_type,
+ ushort_type,
+ uint_type,
+ ulong_type,
+ ulong_long_type,
+ // other integral types
+ bool_type,
+ char_type,
+ code_point_type,
+ last_integer_type = code_point_type,
+ // floats
+ float_type,
+ double_type,
+ long_double_type,
+ last_numeric_type = long_double_type,
+ // other
+ buffer_type,
+ string_type,
+ string_view_type,
+
+ custom_type,
+ last_type = custom_type
+ };
+
+ constexpr bool is_integral(type t) noexcept
+ {
+ return t > none_type && t <= last_integer_type;
+ }
+ constexpr bool is_arithmetic(type t) noexcept
+ {
+ return t > none_type && t <= last_numeric_type;
+ }
+
+ struct custom_value {
+ // using scan_type = error (*)(void*, Context&, ParseCtx&);
+
+ void* value;
+ void (*scan)();
+ };
+
+ template <typename Context, typename ParseCtx, typename T>
+ error scan_custom_arg(void* arg, Context& ctx, ParseCtx& pctx) noexcept
+ {
+ return visitor_boilerplate<scanner<T>>(*static_cast<T*>(arg), ctx,
+ pctx);
+ }
+
+ struct monostate {
+ };
+
+ template <typename Ctx>
+ struct ctx_tag {
+ };
+ template <typename ParseCtx>
+ struct parse_ctx_tag {
+ };
+
+ class value {
+ public:
+ constexpr value() noexcept : m_empty{} {}
+
+ template <typename T>
+ explicit SCN_CONSTEXPR14 value(T& val) noexcept
+ : m_value(std::addressof(val))
+ {
+ }
+
+ template <typename Ctx, typename ParseCtx, typename T>
+ value(ctx_tag<Ctx>, parse_ctx_tag<ParseCtx>, T& val) noexcept
+ : m_custom(
+ custom_value{std::addressof(val),
+ reinterpret_cast<void (*)()>(
+ &scan_custom_arg<Ctx, ParseCtx, T>)})
+ {
+ }
+
+ template <typename T>
+ SCN_CONSTEXPR14 T& get_as() noexcept
+ {
+ return *static_cast<T*>(m_value);
+ }
+ template <typename T>
+ constexpr const T& get_as() const noexcept
+ {
+ return *static_cast<const T*>(m_value);
+ }
+
+ SCN_CONSTEXPR14 custom_value& get_custom() noexcept
+ {
+ return m_custom;
+ }
+ SCN_NODISCARD constexpr const custom_value& get_custom()
+ const noexcept
+ {
+ return m_custom;
+ }
+
+ private:
+ union {
+ monostate m_empty;
+ void* m_value;
+ custom_value m_custom;
+ };
+ };
+
+ template <typename CharT, typename T, type Type>
+ struct init {
+ T* val;
+ static const type type_tag = Type;
+
+ constexpr init(T& v) : val(std::addressof(v)) {}
+ template <typename Ctx, typename ParseCtx>
+ SCN_CONSTEXPR14 value get()
+ {
+ SCN_EXPECT(val != nullptr);
+ return value{*val};
+ }
+ };
+ template <typename CharT, typename T>
+ struct init<CharT, T, custom_type> {
+ T* val;
+ static const type type_tag = custom_type;
+
+ constexpr init(T& v) : val(std::addressof(v)) {}
+ template <typename Ctx, typename ParseCtx>
+ SCN_CONSTEXPR14 value get()
+ {
+ SCN_EXPECT(val != nullptr);
+ return {ctx_tag<Ctx>{}, parse_ctx_tag<ParseCtx>{}, *val};
+ }
+ };
+
+ template <typename Context,
+ typename ParseCtx,
+ typename T,
+ typename CharT = typename Context::char_type>
+ SCN_CONSTEXPR14 basic_arg<CharT> make_arg(T& value) noexcept;
+
+#define SCN_MAKE_VALUE(Tag, Type) \
+ template <typename CharT> \
+ constexpr init<CharT, Type, Tag> make_value(Type& val, \
+ priority_tag<1>) noexcept \
+ { \
+ return val; \
+ }
+
+ SCN_MAKE_VALUE(schar_type, signed char)
+ SCN_MAKE_VALUE(short_type, short)
+ SCN_MAKE_VALUE(int_type, int)
+ SCN_MAKE_VALUE(long_type, long)
+ SCN_MAKE_VALUE(long_long_type, long long)
+
+ SCN_MAKE_VALUE(uchar_type, unsigned char)
+ SCN_MAKE_VALUE(ushort_type, unsigned short)
+ SCN_MAKE_VALUE(uint_type, unsigned)
+ SCN_MAKE_VALUE(ulong_type, unsigned long)
+ SCN_MAKE_VALUE(ulong_long_type, unsigned long long)
+
+ SCN_MAKE_VALUE(bool_type, bool)
+ SCN_MAKE_VALUE(code_point_type, code_point)
+
+ SCN_MAKE_VALUE(float_type, float)
+ SCN_MAKE_VALUE(double_type, double)
+ SCN_MAKE_VALUE(long_double_type, long double)
+
+ SCN_MAKE_VALUE(buffer_type, span<CharT>)
+ SCN_MAKE_VALUE(string_type, std::basic_string<CharT>)
+ SCN_MAKE_VALUE(string_view_type, basic_string_view<CharT>)
+
+ template <typename CharT>
+ constexpr init<CharT, CharT, char_type> make_value(
+ CharT& val,
+ priority_tag<1>) noexcept
+ {
+ return val;
+ }
+
+ template <typename CharT, typename T>
+ constexpr inline auto make_value(T& val, priority_tag<0>) noexcept
+ -> init<CharT, T, custom_type>
+ {
+ return val;
+ }
+
+ enum : size_t {
+ packed_arg_bitsize = 5,
+ packed_arg_mask = (1 << packed_arg_bitsize) - 1,
+ max_packed_args = (sizeof(size_t) * 8 - 1) / packed_arg_bitsize,
+ is_unpacked_bit = size_t{1} << (sizeof(size_t) * 8ull - 1ull)
+ };
+ } // namespace detail
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wpadded")
+
+ /// Type-erased scanning argument.
+ template <typename CharT>
+ class SCN_TRIVIAL_ABI basic_arg {
+ public:
+ using char_type = CharT;
+
+ class handle {
+ public:
+ explicit handle(detail::custom_value custom) : m_custom(custom) {}
+
+ template <typename Context, typename ParseCtx>
+ error scan(Context& ctx, ParseCtx& pctx)
+ {
+ return reinterpret_cast<error (*)(void*, Context&, ParseCtx&)>(
+ m_custom.scan)(m_custom.value, ctx, pctx);
+ }
+
+ private:
+ detail::custom_value m_custom;
+ };
+
+ constexpr basic_arg() = default;
+
+ constexpr explicit operator bool() const noexcept
+ {
+ return m_type != detail::none_type;
+ }
+
+ SCN_NODISCARD constexpr detail::type type() const noexcept
+ {
+ return type;
+ }
+ SCN_NODISCARD constexpr bool is_integral() const noexcept
+ {
+ return detail::is_integral(m_type);
+ }
+ SCN_NODISCARD constexpr bool is_arithmetic() const noexcept
+ {
+ return detail::is_arithmetic(m_type);
+ }
+
+ private:
+ constexpr basic_arg(detail::value v, detail::type t) noexcept
+ : m_value(v), m_type(t)
+ {
+ }
+
+ template <typename Ctx, typename ParseCtx, typename T, typename C>
+ friend SCN_CONSTEXPR14 basic_arg<C> detail::make_arg(T& value) noexcept;
+
+ template <typename C, typename Visitor>
+ friend SCN_CONSTEXPR14 error visit_arg(Visitor&& vis,
+ basic_arg<C>& arg);
+
+ friend class basic_args<CharT>;
+
+ detail::value m_value;
+ detail::type m_type{detail::none_type};
+ };
+
+ SCN_CLANG_POP
+
+ template <typename CharT, typename Visitor>
+ SCN_CONSTEXPR14 error visit_arg(Visitor&& vis, basic_arg<CharT>& arg)
+ {
+ switch (arg.m_type) {
+ case detail::none_type:
+ break;
+
+ case detail::schar_type:
+ return vis(arg.m_value.template get_as<signed char>());
+ case detail::short_type:
+ return vis(arg.m_value.template get_as<short>());
+ case detail::int_type:
+ return vis(arg.m_value.template get_as<int>());
+ case detail::long_type:
+ return vis(arg.m_value.template get_as<long>());
+ case detail::long_long_type:
+ return vis(arg.m_value.template get_as<long long>());
+
+ case detail::uchar_type:
+ return vis(arg.m_value.template get_as<unsigned char>());
+ case detail::ushort_type:
+ return vis(arg.m_value.template get_as<unsigned short>());
+ case detail::uint_type:
+ return vis(arg.m_value.template get_as<unsigned int>());
+ case detail::ulong_type:
+ return vis(arg.m_value.template get_as<unsigned long>());
+ case detail::ulong_long_type:
+ return vis(arg.m_value.template get_as<unsigned long long>());
+
+ case detail::bool_type:
+ return vis(arg.m_value.template get_as<bool>());
+ case detail::char_type:
+ return vis(arg.m_value.template get_as<CharT>());
+ case detail::code_point_type:
+ return vis(arg.m_value.template get_as<code_point>());
+
+ case detail::float_type:
+ return vis(arg.m_value.template get_as<float>());
+ case detail::double_type:
+ return vis(arg.m_value.template get_as<double>());
+ case detail::long_double_type:
+ return vis(arg.m_value.template get_as<long double>());
+
+ case detail::buffer_type:
+ return vis(arg.m_value.template get_as<span<CharT>>());
+ case detail::string_type:
+ return vis(
+ arg.m_value.template get_as<std::basic_string<CharT>>());
+ case detail::string_view_type:
+ return vis(
+ arg.m_value.template get_as<basic_string_view<CharT>>());
+
+ case detail::custom_type:
+ return vis(typename basic_arg<CharT>::handle(
+ arg.m_value.get_custom()));
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wcovered-switch-default")
+ default:
+ return vis(detail::monostate{});
+ SCN_CLANG_POP
+ }
+ SCN_UNREACHABLE;
+ }
+
+ namespace detail {
+ template <typename CharT, typename T>
+ struct get_type {
+ using value_type = decltype(make_value<CharT>(
+ SCN_DECLVAL(typename std::remove_reference<
+ typename std::remove_cv<T>::type>::type&),
+ SCN_DECLVAL(priority_tag<1>)));
+ static const type value = value_type::type_tag;
+ };
+
+ template <typename CharT>
+ constexpr size_t get_types()
+ {
+ return 0;
+ }
+ template <typename CharT, typename Arg, typename... Args>
+ constexpr size_t get_types()
+ {
+ return static_cast<size_t>(get_type<CharT, Arg>::value) |
+ (get_types<CharT, Args...>() << 5);
+ }
+
+ template <typename Context,
+ typename ParseCtx,
+ typename T,
+ typename CharT>
+ SCN_CONSTEXPR14 basic_arg<CharT> make_arg(T& value) noexcept
+ {
+ basic_arg<CharT> arg;
+ arg.m_type = get_type<CharT, T>::value;
+ arg.m_value = make_value<CharT>(value, priority_tag<1>{})
+ .template get<Context, ParseCtx>();
+ return arg;
+ }
+
+ template <bool Packed,
+ typename Context,
+ typename ParseCtx,
+ typename T,
+ typename CharT = typename Context::char_type>
+ inline auto make_arg(T& v) ->
+ typename std::enable_if<Packed, value>::type
+ {
+ return make_value<CharT>(v, priority_tag<1>{})
+ .template get<Context, ParseCtx>();
+ }
+ template <bool Packed, typename Context, typename ParseCtx, typename T>
+ inline auto make_arg(T& v) -> typename std::
+ enable_if<!Packed, basic_arg<typename Context::char_type>>::type
+ {
+ return make_arg<Context, ParseCtx>(v);
+ }
+ } // namespace detail
+
+ template <typename CharT, typename... Args>
+ class arg_store {
+ static constexpr const size_t num_args = sizeof...(Args);
+ static const bool is_packed = num_args < detail::max_packed_args;
+
+ friend class basic_args<CharT>;
+
+ static constexpr size_t get_types()
+ {
+ return is_packed ? detail::get_types<CharT, Args...>()
+ : detail::is_unpacked_bit | num_args;
+ }
+
+ public:
+ static constexpr size_t types = get_types();
+ using arg_type = basic_arg<CharT>;
+
+ using value_type =
+ typename std::conditional<is_packed, detail::value, arg_type>::type;
+ static constexpr size_t data_size =
+ num_args + (is_packed && num_args != 0 ? 0 : 1);
+
+ template <typename Ctx, typename ParseCtx>
+ SCN_CONSTEXPR14 arg_store(detail::ctx_tag<Ctx>,
+ detail::parse_ctx_tag<ParseCtx>,
+ Args&... a) noexcept
+ : m_data{{detail::make_arg<is_packed, Ctx, ParseCtx>(a)...}}
+ {
+ }
+
+ SCN_CONSTEXPR14 span<value_type> data() noexcept
+ {
+ return make_span(m_data.data(),
+ static_cast<std::ptrdiff_t>(m_data.size()));
+ }
+
+ private:
+ detail::array<value_type, data_size> m_data;
+ };
+
+ template <typename Context, typename ParseCtx, typename... Args>
+ arg_store<typename Context::char_type, Args...> make_args(Args&... args)
+ {
+ return {detail::ctx_tag<Context>(), detail::parse_ctx_tag<ParseCtx>(),
+ args...};
+ }
+ template <typename WrappedRange,
+ typename Format,
+ typename... Args,
+ typename CharT = typename WrappedRange::char_type>
+ arg_store<CharT, Args...> make_args_for(WrappedRange&,
+ Format,
+ Args&... args)
+ {
+ using context_type = basic_context<WrappedRange>;
+ using parse_context_type =
+ typename detail::parse_context_template_for_format<
+ Format>::template type<typename context_type::char_type>;
+ return {detail::ctx_tag<context_type>(),
+ detail::parse_ctx_tag<parse_context_type>(), args...};
+ }
+
+ template <typename CharT>
+ class basic_args {
+ public:
+ using arg_type = basic_arg<CharT>;
+
+ constexpr basic_args() noexcept = default;
+
+ template <typename... Args>
+ SCN_CONSTEXPR14 basic_args(arg_store<CharT, Args...>& store) noexcept
+ : m_types(store.types)
+ {
+ set_data(store.m_data.data());
+ }
+
+ SCN_CONSTEXPR14 basic_args(span<arg_type> args) noexcept
+ : m_types(detail::is_unpacked_bit | args.size())
+ {
+ set_data(args.data());
+ }
+
+ SCN_CONSTEXPR14 arg_type get(std::ptrdiff_t i) const noexcept
+ {
+ return do_get(i);
+ }
+
+ SCN_NODISCARD SCN_CONSTEXPR14 bool check_id(
+ std::ptrdiff_t i) const noexcept
+ {
+ if (!is_packed()) {
+ return static_cast<size_t>(i) <
+ (m_types &
+ ~static_cast<size_t>(detail::is_unpacked_bit));
+ }
+ return type(i) != detail::none_type;
+ }
+
+ SCN_NODISCARD constexpr size_t max_size() const noexcept
+ {
+ return is_packed()
+ ? static_cast<size_t>(detail::max_packed_args)
+ : m_types &
+ ~static_cast<size_t>(detail::is_unpacked_bit);
+ }
+
+ private:
+ size_t m_types{0};
+ union {
+ detail::value* m_values;
+ arg_type* m_args;
+ };
+
+ SCN_NODISCARD constexpr bool is_packed() const noexcept
+ {
+ return (m_types & detail::is_unpacked_bit) == 0;
+ }
+
+ SCN_NODISCARD SCN_CONSTEXPR14 typename detail::type type(
+ std::ptrdiff_t i) const noexcept
+ {
+ size_t shift = static_cast<size_t>(i) * detail::packed_arg_bitsize;
+ return static_cast<typename detail::type>(
+ (static_cast<size_t>(m_types) >> shift) &
+ detail::packed_arg_mask);
+ }
+
+ SCN_CONSTEXPR14 void set_data(detail::value* values) noexcept
+ {
+ m_values = values;
+ }
+ SCN_CONSTEXPR14 void set_data(arg_type* args) noexcept
+ {
+ m_args = args;
+ }
+
+ SCN_CONSTEXPR14 arg_type do_get(std::ptrdiff_t i) const noexcept
+ {
+ SCN_EXPECT(i >= 0);
+
+ arg_type arg;
+ if (!is_packed()) {
+ auto num_args = static_cast<std::ptrdiff_t>(max_size());
+ if (SCN_LIKELY(i < num_args)) {
+ arg = m_args[i];
+ }
+ return arg;
+ }
+
+ SCN_EXPECT(m_values);
+ if (SCN_UNLIKELY(
+ i > static_cast<std::ptrdiff_t>(detail::max_packed_args))) {
+ return arg;
+ }
+
+ arg.m_type = type(i);
+ if (arg.m_type == detail::none_type) {
+ return arg;
+ }
+ arg.m_value = m_values[i];
+ return arg;
+ }
+ };
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_DETAIL_ARGS_H
diff --git a/src/third-party/scnlib/include/scn/detail/config.h b/src/third-party/scnlib/include/scn/detail/config.h
new file mode 100644
index 0000000..81d054c
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/config.h
@@ -0,0 +1,466 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_CONFIG_H
+#define SCN_DETAIL_CONFIG_H
+
+#include <cassert>
+
+#define SCN_STD_11 201103L
+#define SCN_STD_14 201402L
+#define SCN_STD_17 201703L
+
+#define SCN_COMPILER(major, minor, patch) \
+ ((major)*10000000 /* 10,000,000 */ + (minor)*10000 /* 10,000 */ + (patch))
+#define SCN_VERSION SCN_COMPILER(1, 1, 2)
+
+#ifdef __INTEL_COMPILER
+// Intel
+#define SCN_INTEL \
+ SCN_COMPILER(__INTEL_COMPILER / 100, (__INTEL_COMPILER / 10) % 10, \
+ __INTEL_COMPILER % 10)
+#elif defined(_MSC_VER) && defined(_MSC_FULL_VER)
+// MSVC
+#if _MSC_VER == _MSC_FULL_VER / 10000
+#define SCN_MSVC \
+ SCN_COMPILER(_MSC_VER / 100, _MSC_VER % 100, _MSC_FULL_VER % 10000)
+#else
+#define SCN_MSVC \
+ SCN_COMPILER(_MSC_VER / 100, (_MSC_FULL_VER / 100000) % 10, \
+ _MSC_FULL_VER % 100000)
+#endif // _MSC_VER == _MSC_FULL_VER / 10000
+#elif defined(__clang__) && defined(__clang_minor__) && \
+ defined(__clang_patchlevel__)
+// Clang
+#define SCN_CLANG \
+ SCN_COMPILER(__clang_major__, __clang_minor__, __clang_patchlevel__)
+#elif defined(__GNUC__) && defined(__GNUC_MINOR__) && \
+ defined(__GNUC_PATCHLEVEL__)
+// GCC
+#define SCN_GCC SCN_COMPILER(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__)
+#endif
+
+#ifndef SCN_INTEL
+#define SCN_INTEL 0
+#endif
+#ifndef SCN_MSVC
+#define SCN_MSVC 0
+#endif
+#ifndef SCN_CLANG
+#define SCN_CLANG 0
+#endif
+#ifndef SCN_GCC
+#define SCN_GCC 0
+#endif
+
+// Pretending to be gcc (clang, icc, etc.)
+#ifdef __GNUC__
+
+#ifdef __GNUC_MINOR__
+#define SCN_GCC_COMPAT_MINOR __GNUC_MINOR__
+#else
+#define SCN_GCC_COMPAT_MINOR 0
+#endif
+
+#ifdef __GNUC_PATCHLEVEL__
+#define SCN_GCC_COMPAT_PATCHLEVEL __GNUC_PATCHLEVEL__
+#else
+#define SCN_GCC_COMPAT_PATCHLEVEL 0
+#endif
+
+#define SCN_GCC_COMPAT \
+ SCN_COMPILER(__GNUC__, SCN_GCC_COMPAT_MINOR, SCN_GCC_COMPAT_PATCHLEVEL)
+#else
+#define SCN_GCC_COMPAT 0
+#endif // #ifdef __GNUC__
+
+#define SCN_STRINGIFY_APPLY(x) #x
+#define SCN_STRINGIFY(x) SCN_STRINGIFY_APPLY(x)
+
+// POSIX
+#if defined(__unix__) || defined(__APPLE__)
+#define SCN_POSIX 1
+#else
+#define SCN_POSIX 0
+#endif
+
+#if defined(__APPLE__)
+#define SCN_APPLE 1
+#else
+#define SCN_APPLE 0
+#endif
+
+// Windows
+#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32)) && \
+ !defined(__CYGWIN__)
+#define SCN_WINDOWS 1
+#else
+#define SCN_WINDOWS 0
+#endif
+
+#ifdef _MSVC_LANG
+#define SCN_MSVC_LANG _MSVC_LANG
+#else
+#define SCN_MSVC_LANG 0
+#endif
+
+// Standard version
+#if SCN_MSVC
+#define SCN_STD SCN_MSVC_LANG
+#else
+#define SCN_STD __cplusplus
+#endif
+
+// Warning control
+#if SCN_GCC
+#define SCN_PRAGMA_APPLY(x) _Pragma(#x)
+
+#define SCN_GCC_PUSH _Pragma("GCC diagnostic push")
+#define SCN_GCC_POP _Pragma("GCC diagnostic pop")
+
+#define SCN_GCC_IGNORE(x) SCN_PRAGMA_APPLY(GCC diagnostic ignored x)
+#else
+#define SCN_GCC_PUSH
+#define SCN_GCC_POP
+#define SCN_GCC_IGNORE(x)
+#endif
+
+#if SCN_CLANG
+#define SCN_PRAGMA_APPLY(x) _Pragma(#x)
+
+#define SCN_CLANG_PUSH _Pragma("clang diagnostic push")
+#define SCN_CLANG_POP _Pragma("clang diagnostic pop")
+
+#define SCN_CLANG_IGNORE(x) SCN_PRAGMA_APPLY(clang diagnostic ignored x)
+
+#if SCN_CLANG >= SCN_COMPILER(3, 9, 0)
+#define SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE \
+ SCN_CLANG_PUSH SCN_CLANG_IGNORE("-Wundefined-func-template")
+#define SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE SCN_CLANG_POP
+#else
+#define SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+#define SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+#endif
+
+#else
+#define SCN_CLANG_PUSH
+#define SCN_CLANG_POP
+#define SCN_CLANG_IGNORE(x)
+#define SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+#define SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+#endif
+
+#if SCN_GCC_COMPAT && defined(SCN_PRAGMA_APPLY)
+#define SCN_GCC_COMPAT_PUSH SCN_PRAGMA_APPLY(GCC diagnostic push)
+#define SCN_GCC_COMPAT_POP SCN_PRAGMA_APPLY(GCC diagnostic pop)
+#define SCN_GCC_COMPAT_IGNORE(x) SCN_PRAGMA_APPLY(GCC diagnostic ignored x)
+#else
+#define SCN_GCC_COMPAT_PUSH
+#define SCN_GCC_COMPAT_POP
+#define SCN_GCC_COMPAT_IGNORE(x)
+#endif
+
+#if SCN_MSVC
+#define SCN_MSVC_PUSH __pragma(warning(push))
+#define SCN_MSVC_POP __pragma(warning(pop))
+
+#define SCN_MSVC_IGNORE(x) __pragma(warning(disable : x))
+#else
+#define SCN_MSVC_PUSH
+#define SCN_MSVC_POP
+#define SCN_MSVC_IGNORE(x)
+#endif
+
+#ifndef SCN_PREDEFINE_VSCAN_OVERLOADS
+#define SCN_PREDEFINE_VSCAN_OVERLOADS 0
+#endif
+
+#ifdef __cpp_exceptions
+#define SCN_HAS_EXCEPTIONS 1
+#endif
+#if !defined(SCN_HAS_EXCEPTIONS) && defined(__EXCEPTIONS)
+#define SCN_HAS_EXCEPTIONS 1
+#endif
+#if !defined(SCN_HAS_EXCEPTIONS) && defined(_HAS_EXCEPTIONS)
+#if _HAS_EXCEPTIONS
+#define SCN_HAS_EXCEPTIONS 1
+#else
+#define SCN_HAS_EXCEPTIONS 0
+#endif
+#endif
+#if !defined(SCN_HAS_EXCEPTIONS) && !defined(_CPPUNWIND)
+#define SCN_HAS_EXCEPTIONS 0
+#endif
+#ifndef SCN_HAS_EXCEPTIONS
+#define SCN_HAS_EXCEPTIONS 0
+#endif
+
+#if SCN_HAS_EXCEPTIONS
+#define SCN_TRY try
+#define SCN_CATCH(x) catch (x)
+#define SCN_THROW(x) throw x
+#define SCN_RETHROW throw
+#else
+#define SCN_TRY if (true)
+#define SCN_CATCH(x) if (false)
+#define SCN_THROW(x) ::std::abort()
+#define SCN_RETHROW ::std::abort()
+#endif
+
+#ifdef __has_include
+#define SCN_HAS_INCLUDE(x) __has_include(x)
+#else
+#define SCN_HAS_INCLUDE(x) 0
+#endif
+
+#ifdef __has_cpp_attribute
+#define SCN_HAS_CPP_ATTRIBUTE(x) __has_cpp_attribute(x)
+#else
+#define SCN_HAS_CPP_ATTRIBUTE(x) 0
+#endif
+
+#ifdef __has_feature
+#define SCN_HAS_FEATURE(x) __has_feature(x)
+#else
+#define SCN_HAS_FEATURE(x) 0
+#endif
+
+#ifdef __has_builtin
+#define SCN_HAS_BUILTIN(x) __has_builtin(x)
+#else
+#define SCN_HAS_BUILTIN(x) 0
+#endif
+
+#if SCN_HAS_INCLUDE(<version>)
+#include <version>
+#endif
+
+#if defined(_SCN_DOXYGEN) && _SCN_DOXYGEN
+#define SCN_DOXYGEN 1
+#else
+#define SCN_DOXYGEN 0
+#endif
+
+// Detect constexpr
+#if defined(__cpp_constexpr)
+#if __cpp_constexpr >= 201304
+#define SCN_HAS_RELAXED_CONSTEXPR 1
+#else
+#define SCN_HAS_RELAXED_CONSTEXPR 0
+#endif
+#endif
+
+#ifndef SCN_HAS_RELAXED_CONSTEXPR
+#if SCN_HAS_FEATURE(cxx_relaxed_constexpr) || \
+ SCN_MSVC >= SCN_COMPILER(19, 10, 0) || \
+ ((SCN_GCC >= SCN_COMPILER(6, 0, 0) || \
+ SCN_INTEL >= SCN_COMPILER(17, 0, 0)) && \
+ SCN_STD >= SCN_STD_14)
+#define SCN_HAS_RELAXED_CONSTEXPR 1
+#else
+#define SCN_HAS_RELAXED_CONSTEXPR 0
+#endif
+#endif
+
+#if SCN_HAS_RELAXED_CONSTEXPR || SCN_DOXYGEN
+#define SCN_CONSTEXPR14 constexpr
+#else
+#define SCN_CONSTEXPR14 inline
+#endif
+
+// Detect string_view
+#if defined(__cpp_lib_string_view) && __cpp_lib_string_view >= 201603 && \
+ SCN_STD >= SCN_STD_17
+#define SCN_HAS_STRING_VIEW 1
+#else
+#define SCN_HAS_STRING_VIEW 0
+#endif
+
+// Detect [[nodiscard]]
+#if (SCN_HAS_CPP_ATTRIBUTE(nodiscard) && __cplusplus >= SCN_STD_17) || \
+ (SCN_MSVC >= SCN_COMPILER(19, 11, 0) && SCN_MSVC_LANG >= SCN_STD_17) || \
+ ((SCN_GCC >= SCN_COMPILER(7, 0, 0) || \
+ SCN_INTEL >= SCN_COMPILER(18, 0, 0)) && \
+ __cplusplus >= SCN_STD_17) && !SCN_DOXYGEN
+#define SCN_NODISCARD [[nodiscard]]
+#else
+#define SCN_NODISCARD /*nodiscard*/
+#endif
+
+// Detect [[clang::trivial_abi]]
+#if SCN_HAS_CPP_ATTRIBUTE(clang::trivial_abi)
+#define SCN_TRIVIAL_ABI [[clang::trivial_abi]]
+#else
+#define SCN_TRIVIAL_ABI /*trivial_abi*/
+#endif
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY
+#define SCN_FUNC inline
+#else
+#define SCN_FUNC
+#endif
+
+// Detect <charconv>
+
+#if defined(_GLIBCXX_RELEASE) && __cplusplus >= SCN_STD_17
+#define SCN_HAS_INTEGER_CHARCONV (_GLIBCXX_RELEASE >= 9)
+#define SCN_HAS_FLOAT_CHARCONV (_GLIBCXX_RELEASE >= 11)
+#elif SCN_MSVC >= SCN_COMPILER(19, 14, 0)
+#define SCN_HAS_INTEGER_CHARCONV 1
+#define SCN_HAS_FLOAT_CHARCONV (SCN_MSVC >= SCN_COMPILER(19, 21, 0))
+#elif defined(__cpp_lib_to_chars) && __cpp_lib_to_chars >= 201606
+#define SCN_HAS_INTEGER_CHARCONV 1
+#define SCN_HAS_FLOAT_CHARCONV 1
+#endif // _GLIBCXX_RELEASE
+
+#ifndef SCN_HAS_INTEGER_CHARCONV
+#define SCN_HAS_INTEGER_CHARCONV 0
+#define SCN_HAS_FLOAT_CHARCONV 0
+#endif
+
+// Detect std::launder
+#if defined(__cpp_lib_launder) && __cpp_lib_launder >= 201606
+#define SCN_HAS_LAUNDER 1
+#else
+#define SCN_HAS_LAUNDER 0
+#endif
+
+// Detect __assume
+#if SCN_INTEL || SCN_MSVC
+#define SCN_HAS_ASSUME 1
+#else
+#define SCN_HAS_ASSUME 0
+#endif
+
+// Detect __builtin_assume
+#if SCN_HAS_BUILTIN(__builtin_assume)
+#define SCN_HAS_BUILTIN_ASSUME 1
+#else
+#define SCN_HAS_BUILTIN_ASSUME 0
+#endif
+
+// Detect __builtin_unreachable
+#if SCN_HAS_BUILTIN(__builtin_unreachable) || SCN_GCC_COMPAT
+#define SCN_HAS_BUILTIN_UNREACHABLE 1
+#else
+#define SCN_HAS_BUILTIN_UNREACHABLE 0
+#endif
+
+#if SCN_HAS_ASSUME
+#define SCN_ASSUME(x) __assume(x)
+#elif SCN_HAS_BUILTIN_ASSUME
+#define SCN_ASSUME(x) __builtin_assume(x)
+#elif SCN_HAS_BUILTIN_UNREACHABLE
+#define SCN_ASSUME(x) ((x) ? static_cast<void>(0) : __builtin_unreachable())
+#else
+#define SCN_ASSUME(x) static_cast<void>((x) ? 0 : 0)
+#endif
+
+#if SCN_HAS_BUILTIN_UNREACHABLE
+#define SCN_UNREACHABLE __builtin_unreachable()
+#else
+#define SCN_UNREACHABLE SCN_ASSUME(0)
+#endif
+
+// Detect __builtin_expect
+#if SCN_HAS_BUILTIN(__builtin_expect) || SCN_GCC_COMPAT
+#define SCN_HAS_BUILTIN_EXPECT 1
+#else
+#define SCN_HAS_BUILTIN_EXPECT 0
+#endif
+
+#if SCN_HAS_BUILTIN_EXPECT
+#define SCN_LIKELY(x) __builtin_expect(!!(x), 1)
+#define SCN_UNLIKELY(x) __builtin_expect(!!(x), 0)
+#else
+#define SCN_LIKELY(x) (x)
+#define SCN_UNLIKELY(x) (x)
+#endif
+
+#ifndef SCN_DEPRECATED
+
+#if (SCN_HAS_CPP_ATTRIBUTE(deprecated) && SCN_STD >= 201402L) || \
+ SCN_MSVC >= SCN_COMPILER(19, 0, 0) || SCN_DOXYGEN
+#define SCN_DEPRECATED [[deprecated]]
+#else
+
+#if SCN_GCC_COMPAT
+#define SCN_DEPRECATED __attribute__((deprecated))
+#elif SCN_MSVC
+#define SCN_DEPRECATED __declspec(deprecated)
+#else
+#define SCN_DEPRECATED /* deprecated */
+#endif
+
+#endif
+
+#endif // !defined(SCN_DEPRECATED)
+
+// Detect concepts
+#if defined(__cpp_concepts) && __cpp_concepts >= 201907L
+#define SCN_HAS_CONCEPTS 1
+#else
+#define SCN_HAS_CONCEPTS 0
+#endif
+
+// Detect ranges
+#if defined(__cpp_lib_ranges) && __cpp_lib_ranges >= 201911L
+#define SCN_HAS_RANGES 1
+#else
+#define SCN_HAS_RANGES 0
+#endif
+
+// Detect char8_t
+#if defined(__cpp_char8_t) && __cpp_char8_t >= 201811L
+#define SCN_HAS_CHAR8 1
+#else
+#define SCN_HAS_CHAR8 0
+#endif
+
+#define SCN_UNUSED(x) static_cast<void>(sizeof(x))
+
+#if SCN_HAS_RELAXED_CONSTEXPR
+#define SCN_ASSERT(cond, msg) \
+ do { \
+ static_cast<void>(SCN_LIKELY(cond)); \
+ assert((cond) && msg); \
+ } while (false)
+#define SCN_EXPECT(cond) SCN_ASSERT(cond, "Precondition violation")
+#define SCN_ENSURE(cond) SCN_ASSERT(cond, "Postcondition violation")
+#else
+#define SCN_ASSERT(cond, msg) SCN_UNUSED(cond)
+#define SCN_EXPECT(cond) SCN_UNUSED(cond)
+#define SCN_ENSURE(cond) SCN_UNUSED(cond)
+#endif
+
+#define SCN_MOVE(x) \
+ static_cast< \
+ typename ::scn::detail::remove_reference \
+ <decltype(x)>::type&&>(x)
+#define SCN_FWD(x) static_cast<decltype(x)&&>(x)
+#define SCN_DECLVAL(T) static_cast<T (*)()>(nullptr)()
+
+#define SCN_BEGIN_NAMESPACE inline namespace v1 {
+#define SCN_END_NAMESPACE }
+
+#if defined(SCN_HEADER_ONLY)
+#define SCN_INCLUDE_SOURCE_DEFINITIONS !SCN_HEADER_ONLY
+#else
+#define SCN_INCLUDE_SOURCE_DEFINITIONS 1
+#endif
+
+#endif // SCN_DETAIL_CONFIG_H
diff --git a/src/third-party/scnlib/include/scn/detail/context.h b/src/third-party/scnlib/include/scn/detail/context.h
new file mode 100644
index 0000000..5dff3b3
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/context.h
@@ -0,0 +1,126 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_CONTEXT_H
+#define SCN_DETAIL_CONTEXT_H
+
+#include "args.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ template <typename WrappedRange>
+ class basic_context {
+ public:
+ using range_type = WrappedRange;
+ using iterator = typename range_type::iterator;
+ using sentinel = typename range_type::sentinel;
+ using char_type = typename range_type::char_type;
+ using locale_type = basic_locale_ref<char_type>;
+
+ basic_context(range_type&& r) : m_range(SCN_MOVE(r)) {}
+ basic_context(range_type&& r, locale_type&& loc)
+ : m_range(SCN_MOVE(r)), m_locale(SCN_MOVE(loc))
+ {
+ }
+
+ SCN_NODISCARD iterator& begin()
+ {
+ return m_range.begin();
+ }
+ const sentinel& end() const
+ {
+ return m_range.end();
+ }
+
+ range_type& range() & noexcept
+ {
+ return m_range;
+ }
+ const range_type& range() const& noexcept
+ {
+ return m_range;
+ }
+ range_type range() && noexcept
+ {
+ return m_range;
+ }
+
+ locale_type& locale() noexcept
+ {
+ return m_locale;
+ }
+ const locale_type& locale() const noexcept
+ {
+ return m_locale;
+ }
+
+ private:
+ range_type m_range;
+ locale_type m_locale{};
+ };
+
+ template <typename WrappedRange,
+ typename CharT = typename WrappedRange::char_type>
+ basic_context<WrappedRange> make_context(WrappedRange r)
+ {
+ return {SCN_MOVE(r)};
+ }
+ template <typename WrappedRange, typename LocaleRef>
+ basic_context<WrappedRange> make_context(WrappedRange r, LocaleRef&& loc)
+ {
+ return {SCN_MOVE(r), SCN_FWD(loc)};
+ }
+
+ template <typename CharT>
+ auto get_arg(const basic_args<CharT>& args, std::ptrdiff_t id)
+ -> expected<basic_arg<CharT>>
+ {
+ auto a = args.get(id);
+ if (!a) {
+ return error(error::invalid_format_string,
+ "Argument id out of range");
+ }
+ return a;
+ }
+ template <typename CharT, typename ParseCtx>
+ auto get_arg(const basic_args<CharT>& args,
+ ParseCtx& pctx,
+ std::ptrdiff_t id) -> expected<basic_arg<CharT>>
+ {
+ return pctx.check_arg_id(id) ? get_arg(args, id)
+ : error(error::invalid_format_string,
+ "Argument id out of range");
+ }
+ template <typename CharT, typename ParseCtx>
+ auto get_arg(const basic_args<CharT>&, ParseCtx&, basic_string_view<CharT>)
+ -> expected<basic_arg<CharT>>
+ {
+ return error(error::invalid_format_string, "Argument id out of range");
+ }
+
+ template <typename CharT, typename ParseCtx>
+ auto next_arg(const basic_args<CharT>& args, ParseCtx& pctx)
+ -> expected<basic_arg<CharT>>
+ {
+ return get_arg(args, pctx.next_arg_id());
+ }
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_DETAIL_CONTEXT_H
diff --git a/src/third-party/scnlib/include/scn/detail/error.h b/src/third-party/scnlib/include/scn/detail/error.h
new file mode 100644
index 0000000..f79e741
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/error.h
@@ -0,0 +1,136 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_ERROR_H
+#define SCN_DETAIL_ERROR_H
+
+#include "fwd.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ /**
+ * Error class.
+ * Used as a return value for functions without a success value.
+ */
+ class SCN_TRIVIAL_ABI error {
+ public:
+ /// Error code
+ enum code : char {
+ /// No error
+ good = 0,
+ /// EOF
+ end_of_range,
+ /// Format string was invalid
+ invalid_format_string,
+ /// Scanned value was invalid for given type.
+ /// e.g. a period '.' when scanning for an int
+ invalid_scanned_value,
+ /// Stream does not support the performed operation
+ invalid_operation,
+ /// Scanned value was out of range for the desired type.
+ /// (e.g. `>2^32` for an `uint32_t`)
+ value_out_of_range,
+ /// Invalid argument given to operation
+ invalid_argument,
+ /// Source range has invalid (utf-8 or utf-16) encoding
+ invalid_encoding,
+ /// This operation is only possible with exceptions enabled
+ exceptions_required,
+ /// The source range emitted an error.
+ source_error,
+ /// The source range emitted an error that cannot be recovered
+ /// from. The stream is now unusable.
+ unrecoverable_source_error,
+
+ unrecoverable_internal_error,
+
+ max_error
+ };
+
+ struct success_tag_t {
+ };
+ static constexpr success_tag_t success_tag() noexcept
+ {
+ return {};
+ }
+
+ constexpr error() noexcept = default;
+ constexpr error(success_tag_t) noexcept : error() {}
+ constexpr error(enum code c, const char* m) noexcept
+ : m_msg(m), m_code(c)
+ {
+ }
+
+ /// Evaluated to true if there was no error
+ constexpr explicit operator bool() const noexcept
+ {
+ return m_code == good;
+ }
+ constexpr bool operator!() const noexcept
+ {
+ return !(operator bool());
+ }
+
+ constexpr operator enum code() const noexcept { return m_code; }
+
+ /// Get error code
+ SCN_NODISCARD constexpr enum code code() const noexcept
+ {
+ return m_code;
+ }
+ SCN_NODISCARD constexpr const char* msg() const noexcept
+ {
+ return m_msg;
+ }
+
+ /// Returns `true` if, after this error, the state of the given input
+ /// range is consistent, and thus, the range can be used for new
+ /// scanning operations.
+ SCN_NODISCARD constexpr bool is_recoverable() const noexcept
+ {
+ return !(m_code == unrecoverable_source_error ||
+ m_code == unrecoverable_internal_error);
+ }
+
+ private:
+ const char* m_msg{nullptr};
+ enum code m_code { good };
+ };
+
+ constexpr inline bool operator==(error a, error b) noexcept
+ {
+ return a.code() == b.code();
+ }
+ constexpr inline bool operator!=(error a, error b) noexcept
+ {
+ return !(a == b);
+ }
+
+ namespace detail {
+ struct error_handler {
+ constexpr error_handler() = default;
+
+ void on_error(error e);
+ void on_error(const char* msg);
+ };
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/detail/file.h b/src/third-party/scnlib/include/scn/detail/file.h
new file mode 100644
index 0000000..03ccff7
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/file.h
@@ -0,0 +1,568 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_FILE_H
+#define SCN_DETAIL_FILE_H
+
+#include <cstdio>
+#include <string>
+
+#include "../util/algorithm.h"
+#include "range.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ struct native_file_handle {
+#if SCN_WINDOWS
+ using handle_type = void*;
+#else
+ using handle_type = int;
+#endif
+
+ static native_file_handle invalid();
+
+ handle_type handle;
+ };
+
+ class byte_mapped_file {
+ public:
+ using iterator = const char*;
+ using sentinel = const char*;
+
+ byte_mapped_file() = default;
+ explicit byte_mapped_file(const char* filename);
+
+ byte_mapped_file(const byte_mapped_file&) = delete;
+ byte_mapped_file& operator=(const byte_mapped_file&) = delete;
+
+ byte_mapped_file(byte_mapped_file&& o) noexcept
+ : m_map(exchange(o.m_map, span<char>{})),
+ m_file(exchange(o.m_file, native_file_handle::invalid()))
+ {
+#if SCN_WINDOWS
+ m_map_handle =
+ exchange(o.m_map_handle, native_file_handle::invalid());
+#endif
+ SCN_ENSURE(!o.valid());
+ SCN_ENSURE(valid());
+ }
+ byte_mapped_file& operator=(byte_mapped_file&& o) noexcept
+ {
+ if (valid()) {
+ _destruct();
+ }
+
+ m_map = exchange(o.m_map, span<char>{});
+ m_file = exchange(o.m_file, native_file_handle::invalid());
+#if SCN_WINDOWS
+ m_map_handle =
+ exchange(o.m_map_handle, native_file_handle::invalid());
+#endif
+
+ SCN_ENSURE(!o.valid());
+ SCN_ENSURE(valid());
+ return *this;
+ }
+
+ ~byte_mapped_file()
+ {
+ if (valid()) {
+ _destruct();
+ }
+ }
+
+ SCN_NODISCARD bool valid() const
+ {
+ return m_file.handle != native_file_handle::invalid().handle;
+ }
+
+ SCN_NODISCARD iterator begin() const
+ {
+ return m_map.begin();
+ }
+ SCN_NODISCARD sentinel end() const
+ {
+ return m_map.end();
+ }
+
+ protected:
+ void _destruct();
+
+ span<char> m_map{};
+ native_file_handle m_file{native_file_handle::invalid().handle};
+#if SCN_WINDOWS
+ native_file_handle m_map_handle{
+ native_file_handle::invalid().handle};
+#endif
+ };
+ } // namespace detail
+
+ /**
+ * Memory-mapped file range.
+ * Manages the lifetime of the mapping itself.
+ */
+ template <typename CharT>
+ class basic_mapped_file : public detail::byte_mapped_file {
+ public:
+ using iterator = const CharT*;
+ using sentinel = const CharT*;
+
+ /// Constructs an empty mapping
+ basic_mapped_file() = default;
+
+ /// Constructs a mapping to a filename
+ explicit basic_mapped_file(const char* f) : detail::byte_mapped_file{f}
+ {
+ }
+
+ SCN_NODISCARD iterator begin() const noexcept
+ {
+ // embrace the UB
+ return reinterpret_cast<iterator>(byte_mapped_file::begin());
+ }
+ SCN_NODISCARD sentinel end() const noexcept
+ {
+ return reinterpret_cast<sentinel>(byte_mapped_file::end());
+ }
+
+ SCN_NODISCARD iterator data() const noexcept
+ {
+ return begin();
+ }
+ SCN_NODISCARD size_t size() const noexcept
+ {
+ return m_map.size() / sizeof(CharT);
+ }
+
+ /// Mapping data
+ span<const CharT> buffer() const
+ {
+ return {data(), size()};
+ }
+
+ detail::range_wrapper<basic_string_view<CharT>> wrap() const noexcept
+ {
+ return basic_string_view<CharT>{data(), size()};
+ }
+ };
+
+ using mapped_file = basic_mapped_file<char>;
+ using mapped_wfile = basic_mapped_file<wchar_t>;
+
+ namespace detail {
+ template <typename CharT>
+ struct basic_file_access;
+ template <typename CharT>
+ struct basic_file_iterator_access;
+ } // namespace detail
+
+ /**
+ * Range mapping to a C FILE*.
+ * Not copyable or reconstructible.
+ */
+ template <typename CharT>
+ class basic_file {
+ friend struct detail::basic_file_access<CharT>;
+ friend struct detail::basic_file_iterator_access<CharT>;
+
+ public:
+ class iterator {
+ friend struct detail::basic_file_iterator_access<CharT>;
+
+ public:
+ using char_type = CharT;
+ using value_type = expected<CharT>;
+ using reference = value_type;
+ using pointer = value_type*;
+ using difference_type = std::ptrdiff_t;
+ using iterator_category = std::bidirectional_iterator_tag;
+ using file_type = basic_file<CharT>;
+
+ iterator() = default;
+
+ expected<CharT> operator*() const;
+
+ iterator& operator++()
+ {
+ SCN_EXPECT(m_file);
+ ++m_current;
+ return *this;
+ }
+ iterator operator++(int)
+ {
+ iterator tmp(*this);
+ operator++();
+ return tmp;
+ }
+
+ iterator& operator--()
+ {
+ SCN_EXPECT(m_file);
+ SCN_EXPECT(m_current > 0);
+
+ m_last_error = error{};
+ --m_current;
+
+ return *this;
+ }
+ iterator operator--(int)
+ {
+ iterator tmp(*this);
+ operator--();
+ return tmp;
+ }
+
+ bool operator==(const iterator& o) const;
+
+ bool operator!=(const iterator& o) const
+ {
+ return !operator==(o);
+ }
+
+ bool operator<(const iterator& o) const
+ {
+ // any valid iterator is before eof and null
+ if (!m_file) {
+ return !o.m_file;
+ }
+ if (!o.m_file) {
+ return !m_file;
+ }
+ SCN_EXPECT(m_file == o.m_file);
+ return m_current < o.m_current;
+ }
+ bool operator>(const iterator& o) const
+ {
+ return o.operator<(*this);
+ }
+ bool operator<=(const iterator& o) const
+ {
+ return !operator>(o);
+ }
+ bool operator>=(const iterator& o) const
+ {
+ return !operator<(o);
+ }
+
+ void reset_begin_iterator() const noexcept
+ {
+ m_current = 0;
+ }
+
+ private:
+ friend class basic_file;
+
+ iterator(const file_type& f, size_t i)
+ : m_file{std::addressof(f)}, m_current{i}
+ {
+ }
+
+ mutable error m_last_error{};
+ const file_type* m_file{nullptr};
+ mutable size_t m_current{0};
+ };
+
+ using sentinel = iterator;
+ using char_type = CharT;
+
+ /**
+ * Construct an empty file.
+ * Reading not possible: valid() is `false`
+ */
+ basic_file() = default;
+ /**
+ * Construct from a FILE*.
+ * Must be a valid handle that can be read from.
+ */
+ basic_file(FILE* f) : m_file{f} {}
+
+ basic_file(const basic_file&) = delete;
+ basic_file& operator=(const basic_file&) = delete;
+
+ basic_file(basic_file&& o) noexcept
+ : m_buffer(detail::exchange(o.m_buffer, {})),
+ m_file(detail::exchange(o.m_file, nullptr))
+ {
+ }
+ basic_file& operator=(basic_file&& o) noexcept
+ {
+ if (valid()) {
+ sync();
+ }
+ m_buffer = detail::exchange(o.m_buffer, {});
+ m_file = detail::exchange(o.m_file, nullptr);
+ return *this;
+ }
+
+ ~basic_file()
+ {
+ if (valid()) {
+ _sync_all();
+ }
+ }
+
+ /**
+ * Get the FILE* for this range.
+ * Only use this handle for reading sync() has been called and no
+ * reading operations have taken place after that.
+ *
+ * \see sync
+ */
+ FILE* handle() const
+ {
+ return m_file;
+ }
+
+ /**
+ * Reset the file handle.
+ * Calls sync(), if necessary, before resetting.
+ * @return The old handle
+ */
+ FILE* set_handle(FILE* f, bool allow_sync = true) noexcept
+ {
+ auto old = m_file;
+ if (old && allow_sync) {
+ sync();
+ }
+ m_file = f;
+ return old;
+ }
+
+ /// Whether the file has been opened
+ constexpr bool valid() const noexcept
+ {
+ return m_file != nullptr;
+ }
+
+ /**
+ * Synchronizes this file with the underlying FILE*.
+ * Invalidates all non-end iterators.
+ * File must be open.
+ *
+ * Necessary for mixing-and-matching scnlib and <cstdio>:
+ * \code{.cpp}
+ * scn::scan(file, ...);
+ * file.sync();
+ * std::fscanf(file.handle(), ...);
+ * \endcode
+ *
+ * Necessary for synchronizing result objects:
+ * \code{.cpp}
+ * auto result = scn::scan(file, ...);
+ * // only result.range() can now be used for scanning
+ * result = scn::scan(result.range(), ...);
+ * // .sync() allows the original file to also be used
+ * file.sync();
+ * result = scn::scan(file, ...);
+ * \endcode
+ */
+ void sync() noexcept
+ {
+ _sync_all();
+ m_buffer.clear();
+ }
+
+ iterator begin() const noexcept
+ {
+ return {*this, 0};
+ }
+ sentinel end() const noexcept
+ {
+ return {};
+ }
+
+ span<const CharT> get_buffer(iterator it,
+ size_t max_size) const noexcept
+ {
+ if (!it.m_file) {
+ return {};
+ }
+ const auto begin =
+ m_buffer.begin() + static_cast<std::ptrdiff_t>(it.m_current);
+ const auto end_diff = detail::min(
+ max_size,
+ static_cast<size_t>(ranges::distance(begin, m_buffer.end())));
+ return {begin, begin + static_cast<std::ptrdiff_t>(end_diff)};
+ }
+
+ private:
+ friend class iterator;
+
+ expected<CharT> _read_single() const;
+
+ void _sync_all() noexcept
+ {
+ _sync_until(m_buffer.size());
+ }
+ void _sync_until(size_t pos) noexcept;
+
+ CharT _get_char_at(size_t i) const
+ {
+ SCN_EXPECT(valid());
+ SCN_EXPECT(i < m_buffer.size());
+ return m_buffer[i];
+ }
+
+ bool _is_at_end(size_t i) const
+ {
+ SCN_EXPECT(valid());
+ return i >= m_buffer.size();
+ }
+
+ mutable std::basic_string<CharT> m_buffer{};
+ FILE* m_file{nullptr};
+ };
+
+ using file = basic_file<char>;
+ using wfile = basic_file<wchar_t>;
+
+ template <>
+ expected<char> file::iterator::operator*() const;
+ template <>
+ expected<wchar_t> wfile::iterator::operator*() const;
+ template <>
+ bool file::iterator::operator==(const file::iterator&) const;
+ template <>
+ bool wfile::iterator::operator==(const wfile::iterator&) const;
+
+ template <>
+ expected<char> file::_read_single() const;
+ template <>
+ expected<wchar_t> wfile::_read_single() const;
+ template <>
+ void file::_sync_until(size_t) noexcept;
+ template <>
+ void wfile::_sync_until(size_t) noexcept;
+
+ /**
+ * A child class for basic_file, handling fopen, fclose, and lifetimes with
+ * RAII.
+ */
+ template <typename CharT>
+ class basic_owning_file : public basic_file<CharT> {
+ public:
+ using char_type = CharT;
+
+ /// Open an empty file
+ basic_owning_file() = default;
+ /// Open a file, with fopen arguments
+ basic_owning_file(const char* f, const char* mode)
+ : basic_file<CharT>(std::fopen(f, mode))
+ {
+ }
+
+ /// Steal ownership of a FILE*
+ explicit basic_owning_file(FILE* f) : basic_file<CharT>(f) {}
+
+ ~basic_owning_file()
+ {
+ if (is_open()) {
+ close();
+ }
+ }
+
+ /// fopen
+ bool open(const char* f, const char* mode)
+ {
+ SCN_EXPECT(!is_open());
+
+ auto h = std::fopen(f, mode);
+ if (!h) {
+ return false;
+ }
+
+ const bool is_wide = sizeof(CharT) > 1;
+ auto ret = std::fwide(h, is_wide ? 1 : -1);
+ if ((is_wide && ret > 0) || (!is_wide && ret < 0) || ret == 0) {
+ this->set_handle(h);
+ return true;
+ }
+ return false;
+ }
+ /// Steal ownership
+ bool open(FILE* f)
+ {
+ SCN_EXPECT(!is_open());
+ if (std::ferror(f) != 0) {
+ return false;
+ }
+ this->set_handle(f);
+ return true;
+ }
+
+ /// Close file
+ void close()
+ {
+ SCN_EXPECT(is_open());
+ this->sync();
+ std::fclose(this->handle());
+ this->set_handle(nullptr, false);
+ }
+
+ /// Is the file open
+ SCN_NODISCARD bool is_open() const
+ {
+ return this->valid();
+ }
+ };
+
+ using owning_file = basic_owning_file<char>;
+ using owning_wfile = basic_owning_file<wchar_t>;
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wexit-time-destructors")
+
+ // Avoid documentation issues: without this, Doxygen will think
+ // SCN_CLANG_PUSH is a part of the stdin_range declaration
+ namespace dummy {
+ }
+
+ /**
+ * Get a reference to the global stdin range
+ */
+ template <typename CharT>
+ basic_file<CharT>& stdin_range()
+ {
+ static auto f = basic_file<CharT>{stdin};
+ return f;
+ }
+ /**
+ * Get a reference to the global `char`-oriented stdin range
+ */
+ inline file& cstdin()
+ {
+ return stdin_range<char>();
+ }
+ /**
+ * Get a reference to the global `wchar_t`-oriented stdin range
+ */
+ inline wfile& wcstdin()
+ {
+ return stdin_range<wchar_t>();
+ }
+ SCN_CLANG_POP
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY && !defined(SCN_FILE_CPP)
+#include "file.cpp"
+#endif
+
+#endif // SCN_DETAIL_FILE_H
diff --git a/src/third-party/scnlib/include/scn/detail/fwd.h b/src/third-party/scnlib/include/scn/detail/fwd.h
new file mode 100644
index 0000000..3dcebf6
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/fwd.h
@@ -0,0 +1,204 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_FWD_H
+#define SCN_DETAIL_FWD_H
+
+#include "config.h"
+
+#include <cstddef>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ // args.h
+
+ template <typename CharT>
+ class basic_arg;
+ template <typename CharT>
+ class basic_args;
+ template <typename CharT, typename... Args>
+ class arg_store;
+
+ template <typename T>
+ struct temporary;
+
+ // error.h
+
+ class error;
+
+ // locale.h
+
+ template <typename CharT>
+ class basic_locale_ref;
+
+ // context.h
+
+ template <typename WrappedRange>
+ class basic_context;
+
+ // parse_context.h
+
+ template <typename CharT>
+ class basic_parse_context;
+ template <typename CharT>
+ class basic_empty_parse_context;
+
+ namespace detail {
+ template <typename T>
+ struct parse_context_template_for_format;
+ }
+
+ // reader/common.h
+
+ template <typename T, typename Enable = void>
+ struct scanner;
+
+ // defined here to avoid including <scn.h> if the user wants to create a
+ // scanner for their own type
+ /**
+ * Base class for all scanners.
+ * User-defined scanner must derive from this type.
+ */
+ struct parser_base {
+ /**
+ * Returns `true` if `skip_range_whitespace()` is to be called before
+ * scanning this value.
+ *
+ * Defaults to `true`. Is `false` for chars, code points and strings
+ * when using set scanning.
+ */
+ static constexpr bool skip_preceding_whitespace()
+ {
+ return true;
+ }
+ /**
+ * Returns `true` if this scanner supports parsing align and fill
+ * specifiers from the format string, and then scanning them.
+ *
+ * Defaults to `false`, `true` for all scnlib-defined scanners.
+ */
+ static constexpr bool support_align_and_fill()
+ {
+ return false;
+ }
+
+ static SCN_CONSTEXPR14 void make_localized() {}
+ };
+
+ struct empty_parser;
+ struct common_parser;
+ struct common_parser_default;
+
+ namespace detail {
+ template <typename T>
+ struct simple_integer_scanner;
+ }
+
+ // visitor.h
+
+ template <typename Context, typename ParseCtx>
+ class basic_visitor;
+
+ // file.h
+
+ template <typename CharT>
+ class basic_mapped_file;
+ template <typename CharT>
+ class basic_file;
+ template <typename CharT>
+ class basic_owning_file;
+
+ // scan.h
+
+ template <typename T>
+ struct span_list_wrapper;
+ template <typename T>
+ struct discard_type;
+
+ // util/array.h
+
+ namespace detail {
+ template <typename T, std::size_t N>
+ struct array;
+ }
+
+ // util/expected.h
+
+ template <typename T, typename Error = ::scn::error, typename Enable = void>
+ class expected;
+
+ // util/memory.h
+
+ namespace detail {
+ template <typename T>
+ struct pointer_traits;
+
+ template <typename T>
+ class erased_storage;
+
+ } // namespace detail
+
+ // util/optional.h
+
+ template <typename T>
+ class optional;
+
+ // util/small_vector.h
+
+ namespace detail {
+ template <typename T, size_t StackN>
+ class small_vector;
+ }
+
+ // util/span.h
+
+ template <typename T>
+ class span;
+
+ // util/string_view.h
+
+ template <typename CharT>
+ class basic_string_view;
+
+ // util/unique_ptr.h
+
+ namespace detail {
+ template <typename T>
+ class unique_ptr;
+ }
+
+ // for SCN_MOVE
+ namespace detail {
+ template <typename T>
+ struct remove_reference {
+ using type = T;
+ };
+ template <typename T>
+ struct remove_reference<T&> {
+ using type = T;
+ };
+ template <typename T>
+ struct remove_reference<T&&> {
+ using type = T;
+ };
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_DETAIL_FWD_H
diff --git a/src/third-party/scnlib/include/scn/detail/locale.h b/src/third-party/scnlib/include/scn/detail/locale.h
new file mode 100644
index 0000000..d4d0f8c
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/locale.h
@@ -0,0 +1,595 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_LOCALE_H
+#define SCN_DETAIL_LOCALE_H
+
+#include "../unicode/unicode.h"
+#include "../util/array.h"
+#include "../util/string_view.h"
+#include "../util/unique_ptr.h"
+
+#include <cwchar>
+#include <string>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ constexpr bool has_zero(uint64_t v)
+ {
+ return (v - UINT64_C(0x0101010101010101)) & ~v &
+ UINT64_C(0x8080808080808080);
+ }
+
+ template <typename CharT>
+ CharT ascii_widen(char ch);
+ template <>
+ constexpr char ascii_widen(char ch)
+ {
+ return ch;
+ }
+ template <>
+ constexpr wchar_t ascii_widen(char ch)
+ {
+ return static_cast<wchar_t>(ch);
+ }
+
+ // Hand write to avoid C locales and thus noticeable performance losses
+ inline bool is_space(char ch) noexcept
+ {
+ static constexpr detail::array<bool, 256> lookup = {
+ {false, false, false, false, false, false, false, false, false,
+ true, true, true, true, true, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, true, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false, false, false, false, false, false,
+ false, false, false, false}};
+ return lookup[static_cast<size_t>(static_cast<unsigned char>(ch))];
+ }
+ constexpr inline bool is_space(wchar_t ch) noexcept
+ {
+ return ch == 0x20 || (ch >= 0x09 && ch <= 0x0d);
+ }
+ constexpr inline bool is_space(code_point cp) noexcept
+ {
+ return cp == 0x20 || (cp >= 0x09 && cp <= 0x0d);
+ }
+
+ constexpr inline bool is_digit(char ch) noexcept
+ {
+ return ch >= '0' && ch <= '9';
+ }
+ constexpr inline bool is_digit(wchar_t ch) noexcept
+ {
+ return ch >= L'0' && ch <= L'9';
+ }
+ constexpr inline bool is_digit(code_point cp) noexcept
+ {
+ return cp >= '0' && cp <= '9';
+ }
+
+ template <typename CharT>
+ struct locale_defaults;
+ template <>
+ struct locale_defaults<char> {
+ static constexpr string_view truename()
+ {
+ return {"true"};
+ }
+ static constexpr string_view falsename()
+ {
+ return {"false"};
+ }
+ static constexpr char decimal_point() noexcept
+ {
+ return '.';
+ }
+ static constexpr char thousands_separator() noexcept
+ {
+ return ',';
+ }
+ };
+ template <>
+ struct locale_defaults<wchar_t> {
+ static constexpr wstring_view truename()
+ {
+ return {L"true"};
+ }
+ static constexpr wstring_view falsename()
+ {
+ return {L"false"};
+ }
+ static constexpr wchar_t decimal_point() noexcept
+ {
+ return L'.';
+ }
+ static constexpr wchar_t thousands_separator() noexcept
+ {
+ return L',';
+ }
+ };
+ } // namespace detail
+
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wpadded")
+
+ // scn::scan:
+ // - no L flag -> use hard-coded defaults, akin to "C"
+ // locale_ref.default() -> default_locale_ref
+ // - L flag -> use global C++ locale
+ // locale_ref.localized() -> custom_locale_ref (global C++)
+ // scn::scan_localized:
+ // - no L flag -> use hard-coded defaults, akin to "C"
+ // locale_ref.default() -> default_locale_ref
+ // - L flag -> use given C++ locale
+ // locale_ref.localized() -> custom_locale_ref (given locale)
+
+ namespace detail {
+ // constexpr locale
+ template <typename CharT, typename SV, typename Def>
+ struct basic_static_locale_ref_base {
+ using char_type = CharT;
+ using string_view_type = SV;
+ using defaults = Def;
+
+ static constexpr bool is_static = true;
+
+ constexpr basic_static_locale_ref_base() = default;
+
+ static constexpr bool is_space(char_type ch)
+ {
+ return detail::is_space(ch);
+ }
+ static constexpr bool is_digit(char_type ch)
+ {
+ return detail::is_digit(ch);
+ }
+
+ static SCN_CONSTEXPR14 bool is_space(span<const char_type> ch)
+ {
+ SCN_EXPECT(ch.size() >= 1);
+ return detail::is_space(ch[0]);
+ }
+ static SCN_CONSTEXPR14 bool is_digit(span<const char_type> ch)
+ {
+ SCN_EXPECT(ch.size() >= 1);
+ return detail::is_digit(ch[0]);
+ }
+
+ static constexpr char_type decimal_point()
+ {
+ return defaults::decimal_point();
+ }
+ static constexpr char_type thousands_separator()
+ {
+ return defaults::thousands_separator();
+ }
+
+ static constexpr string_view_type truename()
+ {
+ return defaults::truename();
+ }
+ static constexpr string_view_type falsename()
+ {
+ return defaults::falsename();
+ }
+ };
+ template <typename CharT>
+ struct basic_static_locale_ref
+ : basic_static_locale_ref_base<CharT,
+ basic_string_view<CharT>,
+ locale_defaults<CharT>> {
+ };
+ template <>
+ struct basic_static_locale_ref<code_point>
+ : basic_static_locale_ref_base<code_point,
+ string_view,
+ locale_defaults<char>> {
+ };
+
+ // base class
+ template <typename CharT>
+ class basic_locale_ref_impl_base {
+ public:
+ using char_type = CharT;
+ using string_type = std::basic_string<char_type>;
+ using string_view_type = basic_string_view<char_type>;
+
+ static constexpr bool is_static = false;
+
+ basic_locale_ref_impl_base() = default;
+
+ basic_locale_ref_impl_base(const basic_locale_ref_impl_base&) =
+ default;
+ basic_locale_ref_impl_base(basic_locale_ref_impl_base&&) = default;
+ basic_locale_ref_impl_base& operator=(
+ const basic_locale_ref_impl_base&) = default;
+ basic_locale_ref_impl_base& operator=(
+ basic_locale_ref_impl_base&&) = default;
+
+#define SCN_DEFINE_LOCALE_REF_CTYPE(f) \
+ bool is_##f(char_type ch) const \
+ { \
+ return do_is_##f(ch); \
+ } \
+ bool is_##f(span<const char_type> ch) const \
+ { \
+ return do_is_##f(ch); \
+ }
+
+ SCN_DEFINE_LOCALE_REF_CTYPE(space)
+ SCN_DEFINE_LOCALE_REF_CTYPE(digit)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(alnum)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(alpha)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(blank)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(cntrl)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(graph)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(lower)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(print)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(punct)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(upper)
+ // SCN_DEFINE_LOCALE_REF_CTYPE(xdigit)
+#undef SCN_DEFINE_LOCALE_REF_CTYPE
+
+ char_type decimal_point() const
+ {
+ return do_decimal_point();
+ }
+ char_type thousands_separator() const
+ {
+ return do_thousands_separator();
+ }
+
+ string_view_type truename() const
+ {
+ return do_truename();
+ }
+ string_view_type falsename() const
+ {
+ return do_falsename();
+ }
+
+ protected:
+ ~basic_locale_ref_impl_base() = default;
+
+ private:
+#define SCN_DECLARE_LOCALE_REF_CTYPE_DO(f) \
+ virtual bool do_is_##f(char_type) const = 0; \
+ virtual bool do_is_##f(span<const char_type>) const = 0;
+ SCN_DECLARE_LOCALE_REF_CTYPE_DO(space)
+ SCN_DECLARE_LOCALE_REF_CTYPE_DO(digit)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(alnum)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(alpha)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(blank)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(cntrl)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(graph)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(lower)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(print)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(punct)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(upper)
+ // SCN_DECLARE_LOCALE_REF_CTYPE_DO(xdigit)
+#undef SCN_DECLARE_LOCALE_REF_CTYPE_DO
+
+ virtual char_type do_decimal_point() const = 0;
+ virtual char_type do_thousands_separator() const = 0;
+ virtual string_view_type do_truename() const = 0;
+ virtual string_view_type do_falsename() const = 0;
+ };
+
+ // hardcoded "C", using static_locale_ref
+ template <typename CharT>
+ class basic_default_locale_ref final
+ : public basic_locale_ref_impl_base<CharT> {
+ using base = basic_locale_ref_impl_base<CharT>;
+
+ public:
+ using char_type = typename base::char_type;
+ using string_view_type = typename base::string_view_type;
+
+ basic_default_locale_ref() = default;
+
+ private:
+ using static_type = basic_static_locale_ref<char_type>;
+
+ bool do_is_space(char_type ch) const override
+ {
+ return static_type::is_space(ch);
+ }
+ bool do_is_digit(char_type ch) const override
+ {
+ return static_type::is_digit(ch);
+ }
+
+ bool do_is_space(span<const char_type> ch) const override
+ {
+ return static_type::is_space(ch);
+ }
+ bool do_is_digit(span<const char_type> ch) const override
+ {
+ return static_type::is_digit(ch);
+ }
+
+ char_type do_decimal_point() const override
+ {
+ return static_type::decimal_point();
+ }
+ char_type do_thousands_separator() const override
+ {
+ return static_type::thousands_separator();
+ }
+ string_view_type do_truename() const override
+ {
+ return static_type::truename();
+ }
+ string_view_type do_falsename() const override
+ {
+ return static_type::falsename();
+ }
+ };
+
+ // custom
+ template <typename CharT>
+ class basic_custom_locale_ref final
+ : public basic_locale_ref_impl_base<CharT> {
+ using base = basic_locale_ref_impl_base<CharT>;
+
+ public:
+ using char_type = typename base::char_type;
+ using string_type = typename base::string_type;
+ using string_view_type = typename base::string_view_type;
+
+ basic_custom_locale_ref();
+ basic_custom_locale_ref(const void* locale);
+
+ basic_custom_locale_ref(const basic_custom_locale_ref&) = delete;
+ basic_custom_locale_ref& operator=(const basic_custom_locale_ref&) =
+ delete;
+
+ basic_custom_locale_ref(basic_custom_locale_ref&&);
+ basic_custom_locale_ref& operator=(basic_custom_locale_ref&&);
+
+ ~basic_custom_locale_ref();
+
+ static basic_custom_locale_ref make_classic();
+
+ const void* get_locale() const
+ {
+ return m_locale;
+ }
+
+ void convert_to_global();
+ void convert_to_classic();
+
+ // narrow: locale multibyte -> locale wide
+ // wide: identity
+ error convert_to_wide(const CharT* from_begin,
+ const CharT* from_end,
+ const CharT*& from_next,
+ wchar_t* to_begin,
+ wchar_t* to_end,
+ wchar_t*& to_next) const;
+ expected<wchar_t> convert_to_wide(const CharT* from_begin,
+ const CharT* from_end) const;
+
+#define SCN_DEFINE_CUSTOM_LOCALE_CTYPE(f) \
+ bool is_##f(char_type) const; \
+ bool is_##f(span<const char_type>) const; \
+ bool is_##f(code_point) const;
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(alnum)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(alpha)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(blank)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(cntrl)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(graph)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(lower)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(print)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(punct)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(upper)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(xdigit)
+#undef SCN_DEFINE_CUSTOM_LOCALE_CTYPE
+
+ bool is_space(code_point) const;
+ using base::is_space;
+
+ bool is_digit(code_point) const;
+ using base::is_digit;
+
+ template <typename T>
+ expected<std::ptrdiff_t> read_num(T& val,
+ const string_type& buf,
+ int base) const;
+
+ private:
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ bool do_is_space(char_type ch) const override;
+ bool do_is_digit(char_type ch) const override;
+
+ bool do_is_space(span<const char_type> ch) const override;
+ bool do_is_digit(span<const char_type> ch) const override;
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+
+ char_type do_decimal_point() const override;
+ char_type do_thousands_separator() const override;
+ string_view_type do_truename() const override;
+ string_view_type do_falsename() const override;
+
+ void _initialize();
+
+ const void* m_locale{nullptr};
+ void* m_data{nullptr};
+ };
+ } // namespace detail
+
+ template <typename CharT>
+ class basic_locale_ref {
+ public:
+ using char_type = CharT;
+ using impl_base = detail::basic_locale_ref_impl_base<char_type>;
+ using static_type = detail::basic_static_locale_ref<char_type>;
+ using default_type = detail::basic_default_locale_ref<char_type>;
+ using custom_type = detail::basic_custom_locale_ref<char_type>;
+
+ // default
+ constexpr basic_locale_ref() = default;
+ // nullptr = global
+ constexpr basic_locale_ref(const void* p) : m_payload(p) {}
+
+ basic_locale_ref clone() const
+ {
+ return {m_payload};
+ }
+
+ constexpr bool has_custom() const
+ {
+ return m_payload != nullptr;
+ }
+
+ // hardcoded "C", constexpr, should be preferred whenever possible
+ constexpr static_type get_static() const
+ {
+ return {};
+ }
+
+ // hardcoded "C", not constexpr
+ default_type& get_default()
+ {
+ return m_default;
+ }
+ const default_type& get_default() const
+ {
+ return m_default;
+ }
+
+ // global locale or given locale
+ custom_type& get_localized()
+ {
+ _construct_custom();
+ return *m_custom;
+ }
+ const custom_type& get_localized() const
+ {
+ _construct_custom();
+ return *m_custom;
+ }
+
+ custom_type make_localized_classic() const
+ {
+ return custom_type::make_classic();
+ }
+
+ custom_type* get_localized_unsafe()
+ {
+ return m_custom.get();
+ }
+ const custom_type* get_localized_unsafe() const
+ {
+ return m_custom.get();
+ }
+
+ // virtual interface
+ impl_base& get(bool localized)
+ {
+ if (localized) {
+ return get_localized();
+ }
+ return get_default();
+ }
+ const impl_base& get(bool localized) const
+ {
+ if (localized) {
+ return get_localized();
+ }
+ return get_default();
+ }
+
+ void prepare_localized() const
+ {
+ _construct_custom();
+ }
+ void reset_locale(const void* payload)
+ {
+ m_custom.reset();
+ m_payload = payload;
+ _construct_custom();
+ }
+
+ private:
+ void _construct_custom() const
+ {
+ if (m_custom) {
+ // already constructed
+ return;
+ }
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ m_custom = detail::make_unique<custom_type>(m_payload);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+
+ mutable detail::unique_ptr<custom_type> m_custom{nullptr};
+ const void* m_payload{nullptr};
+ default_type m_default{};
+ };
+
+ template <typename CharT, typename Locale>
+ basic_locale_ref<CharT> make_locale_ref(const Locale& loc)
+ {
+ return {std::addressof(loc)};
+ }
+ template <typename CharT>
+ basic_locale_ref<CharT> make_default_locale_ref()
+ {
+ return {};
+ }
+
+ using locale_ref = basic_locale_ref<char>;
+ using wlocale_ref = basic_locale_ref<wchar_t>;
+
+ SCN_CLANG_POP // -Wpadded
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY && !defined(SCN_LOCALE_CPP)
+#include "locale.cpp"
+#endif
+
+#endif // SCN_DETAIL_LOCALE_H
diff --git a/src/third-party/scnlib/include/scn/detail/parse_context.h b/src/third-party/scnlib/include/scn/detail/parse_context.h
new file mode 100644
index 0000000..91d6687
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/parse_context.h
@@ -0,0 +1,581 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_PARSE_CONTEXT_H
+#define SCN_DETAIL_PARSE_CONTEXT_H
+
+#include "../util/expected.h"
+#include "locale.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ class parse_context_base {
+ public:
+ SCN_CONSTEXPR14 std::ptrdiff_t next_arg_id()
+ {
+ return m_next_arg_id >= 0 ? m_next_arg_id++ : 0;
+ }
+ SCN_CONSTEXPR14 bool check_arg_id(std::ptrdiff_t)
+ {
+ if (m_next_arg_id > 0) {
+ return false;
+ }
+ m_next_arg_id = -1;
+ return true;
+ }
+
+ protected:
+ parse_context_base() = default;
+
+ std::ptrdiff_t m_next_arg_id{0};
+ };
+ } // namespace detail
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wpadded")
+
+ template <typename CharT>
+ class basic_parse_context : public detail::parse_context_base {
+ public:
+ using char_type = CharT;
+ using locale_type = basic_locale_ref<CharT>;
+ using string_view_type = basic_string_view<char_type>;
+ using iterator = typename string_view_type::iterator;
+
+ constexpr basic_parse_context(basic_string_view<char_type> f,
+ locale_type& loc)
+ : m_str(f), m_locale(loc)
+ {
+ }
+
+ /**
+ * Returns `true`, if `next_char()` is a whitespace character according
+ * to the static locale. This means, that `skip_range_whitespace()`
+ * should be called on the source range.
+ */
+ bool should_skip_ws()
+ {
+ bool skip = false;
+ while (*this && m_locale.get_static().is_space(next_char())) {
+ skip = true;
+ advance_char();
+ }
+ return skip;
+ }
+ /**
+ * Returns `true`, if a character equal to `next_char()` should be read
+ * from the source range.
+ *
+ * If `*this` currently points to an escaped
+ * brace character `"{{"` or `"}}"`, skips the first brace, so that
+ * after this function is called, `next_char()` returns the character
+ * that should be read.
+ */
+ bool should_read_literal()
+ {
+ const auto brace = detail::ascii_widen<char_type>('{');
+ if (next_char() != brace) {
+ if (next_char() == detail::ascii_widen<char_type>('}')) {
+ advance_char();
+ }
+ return true;
+ }
+ if (SCN_UNLIKELY(m_str.size() > 1 &&
+ *(m_str.begin() + 1) == brace)) {
+ advance_char();
+ return true;
+ }
+ return false;
+ }
+ /**
+ * Returns `true` if `ch` is equal to `next_char()`
+ */
+ SCN_NODISCARD constexpr bool check_literal(char_type ch) const
+ {
+ return ch == next_char();
+ }
+ /**
+ * Returns `true` if the code units contained in `ch` are equal to the
+ * code units starting from `pctx.begin()`. If `chars_left() <
+ * ch.size()`, returns `false`.
+ */
+ SCN_NODISCARD SCN_CONSTEXPR14 bool check_literal(
+ span<const char_type> ch) const
+ {
+ if (chars_left() < ch.size()) {
+ return false;
+ }
+ for (size_t i = 0; i < ch.size(); ++i) {
+ if (ch[i] != m_str[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ /**
+ * Returns `true` if `cp` is equal to the value returned by `next_cp()`.
+ * If `next_cp()` errored, returns that error
+ * (`error::invalid_encoding`).
+ */
+ SCN_NODISCARD SCN_CONSTEXPR14 expected<bool> check_literal_cp(
+ code_point cp) const
+ {
+ auto next = next_cp();
+ if (!next) {
+ return next.error();
+ }
+ return cp == next.value();
+ }
+
+ /**
+ * Returns `true` if there are characters left in `*this`.
+ */
+ constexpr bool good() const
+ {
+ return !m_str.empty();
+ }
+ constexpr explicit operator bool() const
+ {
+ return good();
+ }
+
+ /**
+ * Returns the next character (= code unit) in `*this`.
+ * `good()` must be `true`.
+ */
+ constexpr char_type next_char() const
+ {
+ return m_str.front();
+ }
+ /**
+ * Returns the next code point in `*this`.
+ * If the code point is encoded incorrectly, returns
+ * `error::invalid_encoding`.
+ */
+ SCN_NODISCARD SCN_CONSTEXPR14 expected<code_point> next_cp() const
+ {
+ code_point cp{};
+ auto it = parse_code_point(m_str.begin(), m_str.end(), cp);
+ if (!it) {
+ return it.error();
+ }
+ return {cp};
+ }
+
+ /**
+ * Returns the number of chars (= code units) left in `*this`.
+ */
+ constexpr std::size_t chars_left() const noexcept
+ {
+ return m_str.size();
+ }
+ /**
+ * Returns the number of code points left in `*this`. If `*this`
+ * contains invalid encoding, returns `error::invalid_encoding`.
+ */
+ SCN_NODISCARD SCN_CONSTEXPR14 expected<std::size_t> cp_left()
+ const noexcept
+ {
+ auto d = code_point_distance(m_str.begin(), m_str.end());
+ if (!d) {
+ return d.error();
+ }
+ return {static_cast<std::size_t>(d.value())};
+ }
+
+ /**
+ * Advances `*this` by `n` characters (= code units). `*this` must have
+ * at least `n` characters left.
+ */
+ SCN_CONSTEXPR14 void advance_char(std::ptrdiff_t n = 1) noexcept
+ {
+ SCN_EXPECT(chars_left() >= static_cast<std::size_t>(n));
+ m_str.remove_prefix(static_cast<std::size_t>(n));
+ }
+ /**
+ * Advances `*this` by a single code point. If the code point is encoded
+ * incorrectly, returns `error::invalid_encoding`.
+ */
+ SCN_NODISCARD SCN_CONSTEXPR14 error advance_cp() noexcept
+ {
+ code_point cp{};
+ auto it = parse_code_point(m_str.begin(), m_str.end(), cp);
+ if (!it) {
+ return it.error();
+ }
+ m_str.remove_prefix(
+ static_cast<size_t>(it.value() - m_str.begin()));
+ return {};
+ }
+
+ /**
+ * Returns `true`, if `*this` has over `n` characters (= code units)
+ * left, so that `peek_char()` with the same `n` parameter can be
+ * called.
+ */
+ constexpr bool can_peek_char(std::size_t n = 1) const noexcept
+ {
+ return chars_left() > n;
+ }
+
+ /**
+ * Returns the character (= code unit) `n` characters past the current
+ * character, so that `peek_char(0)` is equivalent to `next_char()`.
+ * `n <= chars_left()` must be `true`.
+ */
+ SCN_CONSTEXPR14 char_type peek_char(std::size_t n = 1) const noexcept
+ {
+ SCN_EXPECT(n <= chars_left());
+ return m_str[n];
+ }
+ /**
+ * Returns the code point past the current code point (`next_cp()`).
+ *
+ * If there is no code point to peek (the current code point is the last
+ * one in `*this`), returns `error::end_of_range`.
+ * If `*this` contains invalid encoding, returns
+ * `error::invalid_encoding`.
+ */
+ SCN_NODISCARD SCN_CONSTEXPR14 expected<code_point> peek_cp()
+ const noexcept
+ {
+ if (m_str.size() < 2) {
+ return error{error::end_of_range,
+ "End of format string, cannot peek"};
+ }
+
+ code_point cp{};
+ auto it = parse_code_point(m_str.begin(), m_str.end(), cp);
+ if (!it) {
+ return it.error();
+ }
+ if (it.value() == m_str.end()) {
+ return error{error::end_of_range,
+ "End of format string, cannot peek"};
+ }
+
+ it = parse_code_point(it.value(), m_str.end(), cp);
+ if (!it) {
+ return it.error();
+ }
+ return {cp};
+ }
+
+ SCN_CONSTEXPR14 iterator begin() const noexcept
+ {
+ return m_str.begin();
+ }
+ SCN_CONSTEXPR14 iterator end() const noexcept
+ {
+ return m_str.end();
+ }
+
+ /**
+ * Returns `true`, if `next_char() == '{'`
+ */
+ SCN_CONSTEXPR14 bool check_arg_begin() const
+ {
+ SCN_EXPECT(good());
+ return next_char() == detail::ascii_widen<char_type>('{');
+ }
+ /**
+ * Returns `true`, if `next_char() == '}'`
+ */
+ SCN_CONSTEXPR14 bool check_arg_end() const
+ {
+ SCN_EXPECT(good());
+ return next_char() == detail::ascii_widen<char_type>('}');
+ }
+
+ using parse_context_base::check_arg_id;
+ SCN_CONSTEXPR14 void check_arg_id(basic_string_view<CharT>) {}
+
+ SCN_CONSTEXPR14 void arg_begin() const noexcept {}
+ SCN_CONSTEXPR14 void arg_end() const noexcept {}
+
+ SCN_CONSTEXPR14 void arg_handled() const noexcept {}
+
+ const locale_type& locale() const
+ {
+ return m_locale;
+ }
+
+ /**
+ * Parse `*this` using `s`
+ */
+ template <typename Scanner>
+ error parse(Scanner& s)
+ {
+ return s.parse(*this);
+ }
+
+ bool has_arg_id()
+ {
+ SCN_EXPECT(good());
+ if (m_str.size() == 1) {
+ return true;
+ }
+ if (m_str[1] == detail::ascii_widen<char_type>('}')) {
+ advance_char();
+ return false;
+ }
+ if (m_str[1] == detail::ascii_widen<char_type>(':')) {
+ advance_char(2);
+ return false;
+ }
+ return true;
+ }
+ expected<string_view_type> parse_arg_id()
+ {
+ SCN_EXPECT(good());
+ advance_char();
+ if (SCN_UNLIKELY(!good())) {
+ return error(error::invalid_format_string,
+ "Unexpected end of format argument");
+ }
+ auto it = m_str.begin();
+ for (std::ptrdiff_t i = 0; good(); ++i, (void)advance_char()) {
+ if (check_arg_end()) {
+ return string_view_type{
+ it,
+ static_cast<typename string_view_type::size_type>(i)};
+ }
+ if (next_char() == detail::ascii_widen<char_type>(':')) {
+ advance_char();
+ return string_view_type{
+ it,
+ static_cast<typename string_view_type::size_type>(i)};
+ }
+ }
+ return error(error::invalid_format_string,
+ "Unexpected end of format argument");
+ }
+
+ private:
+ string_view_type m_str;
+ locale_type& m_locale;
+ };
+
+ template <typename CharT>
+ class basic_empty_parse_context : public detail::parse_context_base {
+ public:
+ using char_type = CharT;
+ using locale_type = basic_locale_ref<char_type>;
+ using string_view_type = basic_string_view<char_type>;
+
+ constexpr basic_empty_parse_context(int args,
+ locale_type& loc,
+ bool localized = false)
+ : m_locale(loc), m_args_left(args), m_localized(localized)
+ {
+ }
+
+ SCN_CONSTEXPR14 bool should_skip_ws()
+ {
+ if (m_should_skip_ws) {
+ m_should_skip_ws = false;
+ return true;
+ }
+ return false;
+ }
+ constexpr bool should_read_literal() const
+ {
+ return false;
+ }
+ constexpr bool check_literal(char_type) const
+ {
+ return false;
+ }
+ constexpr bool check_literal(span<const char_type>) const
+ {
+ return false;
+ }
+ constexpr bool check_literal_cp(code_point) const
+ {
+ return false;
+ }
+
+ constexpr bool good() const
+ {
+ return m_args_left > 0;
+ }
+ constexpr explicit operator bool() const
+ {
+ return good();
+ }
+
+ SCN_CONSTEXPR14 void advance_char(std::ptrdiff_t = 1) const noexcept {}
+ SCN_CONSTEXPR14 error advance_cp() const noexcept
+ {
+ return {};
+ }
+
+ char_type next_char() const
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+ expected<std::pair<code_point, std::ptrdiff_t>> next_cp() const
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+
+ std::size_t chars_left() const noexcept
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+ std::size_t cp_left() const noexcept
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+
+ constexpr bool can_peek_char() const noexcept
+ {
+ return false;
+ }
+ constexpr bool can_peek_cp() const noexcept
+ {
+ return false;
+ }
+
+ char_type peek_char(std::ptrdiff_t = 1) const noexcept
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+ expected<code_point> peek_cp() const noexcept
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+
+ constexpr bool check_arg_begin() const
+ {
+ return true;
+ }
+ constexpr bool check_arg_end() const
+ {
+ return true;
+ }
+
+ using parse_context_base::check_arg_id;
+ SCN_CONSTEXPR14 void check_arg_id(basic_string_view<CharT>) {}
+
+ SCN_CONSTEXPR14 void arg_begin() const noexcept {}
+ SCN_CONSTEXPR14 void arg_end() const noexcept {}
+
+ SCN_CONSTEXPR14 void arg_handled()
+ {
+ m_should_skip_ws = true;
+ --m_args_left;
+ }
+
+ const locale_type& locale() const
+ {
+ return m_locale;
+ }
+
+ template <typename Scanner>
+ SCN_CONSTEXPR14 error parse(Scanner& s) const
+ {
+ if (m_localized) {
+ s.make_localized();
+ }
+ return {};
+ }
+
+ constexpr bool has_arg_id() const
+ {
+ return false;
+ }
+ SCN_CONSTEXPR14 expected<string_view_type> parse_arg_id() const
+ {
+ SCN_EXPECT(good());
+ return string_view_type{};
+ }
+
+ void reset_args_left(int n)
+ {
+ m_args_left = n;
+ parse_context_base::m_next_arg_id = 0;
+ m_should_skip_ws = false;
+ }
+
+ private:
+ locale_type& m_locale;
+ int m_args_left;
+ bool m_localized;
+ bool m_should_skip_ws{false};
+ };
+
+ namespace detail {
+ template <typename CharT>
+ basic_parse_context<CharT> make_parse_context_impl(
+ basic_string_view<CharT> f,
+ basic_locale_ref<CharT>& loc,
+ bool)
+ {
+ return {f, loc};
+ }
+ template <typename CharT>
+ basic_empty_parse_context<CharT> make_parse_context_impl(
+ int i,
+ basic_locale_ref<CharT>& loc,
+ bool localized)
+ {
+ return {i, loc, localized};
+ }
+
+ template <typename CharT>
+ struct parse_context_template_for_format<basic_string_view<CharT>> {
+ template <typename T>
+ using type = basic_parse_context<T>;
+ };
+ template <>
+ struct parse_context_template_for_format<int> {
+ template <typename CharT>
+ using type = basic_empty_parse_context<CharT>;
+ };
+
+ template <typename F, typename CharT>
+ auto make_parse_context(F f,
+ basic_locale_ref<CharT>& locale,
+ bool localized)
+ -> decltype(make_parse_context_impl(f, locale, localized))
+ {
+ return make_parse_context_impl(f, locale, localized);
+ }
+ } // namespace detail
+
+ template <typename F, typename CharT>
+ auto make_parse_context(F f, basic_locale_ref<CharT>& locale)
+ -> decltype(detail::make_parse_context_impl(f, locale, false))
+ {
+ return detail::make_parse_context_impl(f, locale, false);
+ }
+
+ SCN_CLANG_POP // -Wpadded
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_DETAIL_PARSE_CONTEXT_H
diff --git a/src/third-party/scnlib/include/scn/detail/range.h b/src/third-party/scnlib/include/scn/detail/range.h
new file mode 100644
index 0000000..5b8802f
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/range.h
@@ -0,0 +1,598 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_RANGE_H
+#define SCN_DETAIL_RANGE_H
+
+#include "../ranges/ranges.h"
+#include "../util/algorithm.h"
+#include "../util/memory.h"
+#include "error.h"
+#include "vectored.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ namespace _reset_begin_iterator {
+ struct fn {
+ private:
+ template <typename Iterator>
+ static auto impl(Iterator& it, priority_tag<1>) noexcept(
+ noexcept(it.reset_begin_iterator()))
+ -> decltype(it.reset_begin_iterator())
+ {
+ return it.reset_begin_iterator();
+ }
+
+ template <typename Iterator>
+ static void impl(Iterator&, size_t, priority_tag<0>) noexcept
+ {
+ }
+
+ public:
+ template <typename Iterator>
+ auto operator()(Iterator& it) const
+ noexcept(noexcept(fn::impl(it, priority_tag<1>{})))
+ -> decltype(fn::impl(it, priority_tag<1>{}))
+ {
+ return fn::impl(it, priority_tag<1>{});
+ }
+ };
+ } // namespace _reset_begin_iterator
+ namespace {
+ static constexpr auto& reset_begin_iterator =
+ static_const<detail::_reset_begin_iterator::fn>::value;
+ }
+
+ template <typename Iterator, typename = void>
+ struct extract_char_type;
+ template <typename Iterator>
+ struct extract_char_type<
+ Iterator,
+ typename std::enable_if<std::is_integral<
+ polyfill_2a::iter_value_t<Iterator>>::value>::type> {
+ using type = polyfill_2a::iter_value_t<Iterator>;
+ };
+ template <typename Iterator>
+ struct extract_char_type<
+ Iterator,
+ void_t<
+ typename std::enable_if<!std::is_integral<
+ polyfill_2a::iter_value_t<Iterator>>::value>::type,
+ typename polyfill_2a::iter_value_t<Iterator>::success_type>> {
+ using type =
+ typename polyfill_2a::iter_value_t<Iterator>::success_type;
+ };
+
+ template <typename Range, typename = void>
+ struct is_direct_impl
+ : std::is_integral<ranges::range_value_t<const Range>> {
+ };
+
+ template <typename Range>
+ struct reconstruct_tag {
+ };
+
+ template <
+ typename Range,
+ typename Iterator,
+ typename Sentinel,
+ typename = typename std::enable_if<
+ std::is_constructible<Range, Iterator, Sentinel>::value>::type>
+ Range reconstruct(reconstruct_tag<Range>, Iterator begin, Sentinel end)
+ {
+ return {begin, end};
+ }
+#if SCN_HAS_STRING_VIEW
+ // std::string_view is not reconstructible pre-C++20
+ template <typename CharT,
+ typename Traits,
+ typename Iterator,
+ typename Sentinel>
+ std::basic_string_view<CharT, Traits> reconstruct(
+ reconstruct_tag<std::basic_string_view<CharT, Traits>>,
+ Iterator begin,
+ Sentinel end)
+ {
+ // On MSVC, string_view can't even be constructed from its
+ // iterators!
+ return {::scn::detail::to_address(begin),
+ static_cast<size_t>(ranges::distance(begin, end))};
+ }
+#endif // SCN_HAS_STRING_VIEW
+
+ template <typename T, bool>
+ struct range_wrapper_storage;
+ template <typename T>
+ struct range_wrapper_storage<T, true> {
+ using type = remove_cvref_t<T>;
+ using range_type = const type&;
+
+ const type* value{nullptr};
+
+ range_wrapper_storage() = default;
+ range_wrapper_storage(const type& v, dummy_type)
+ : value(std::addressof(v))
+ {
+ }
+
+ const type& get() const& noexcept
+ {
+ return *value;
+ }
+ type&& get() && noexcept
+ {
+ return *value;
+ }
+ };
+ template <typename T>
+ struct range_wrapper_storage<T, false> {
+ using range_type = T;
+
+ T value{};
+
+ range_wrapper_storage() = default;
+ template <typename U>
+ range_wrapper_storage(U&& v, dummy_type) : value(SCN_FWD(v))
+ {
+ }
+
+ const T& get() const& noexcept
+ {
+ return value;
+ }
+ T&& get() && noexcept
+ {
+ return value;
+ }
+ };
+
+ template <typename T>
+ using _range_wrapper_marker = typename T::range_wrapper_marker;
+
+ template <typename T>
+ struct _has_range_wrapper_marker
+ : custom_ranges::detail::exists<_range_wrapper_marker, T> {
+ };
+
+ /**
+ * Wraps a source range for more consistent behavior
+ */
+ template <typename Range>
+ class range_wrapper {
+ public:
+ using range_type = Range;
+ using range_nocvref_type = remove_cvref_t<Range>;
+ using iterator = ranges::iterator_t<const range_nocvref_type>;
+ using sentinel = ranges::sentinel_t<const range_nocvref_type>;
+ using char_type = typename extract_char_type<iterator>::type;
+ using difference_type =
+ ranges::range_difference_t<const range_nocvref_type>;
+ using storage_type =
+ range_wrapper_storage<Range, std::is_reference<Range>::value>;
+ using storage_range_type = typename storage_type::range_type;
+
+ using range_wrapper_marker = void;
+
+ template <
+ typename R,
+ typename = typename std::enable_if<
+ !_has_range_wrapper_marker<remove_cvref_t<R>>::value>::type>
+ range_wrapper(R&& r)
+ : m_range(SCN_FWD(r), dummy_type{}),
+ m_begin(ranges::cbegin(m_range.get()))
+ {
+ }
+
+ range_wrapper(const range_wrapper& o) : m_range(o.m_range)
+ {
+ const auto n =
+ ranges::distance(o.begin_underlying(), o.m_begin);
+ m_begin = ranges::cbegin(m_range.get());
+ ranges::advance(m_begin, n);
+ m_read = o.m_read;
+ }
+ range_wrapper& operator=(const range_wrapper& o)
+ {
+ const auto n =
+ ranges::distance(o.begin_underlying(), o.m_begin);
+ m_range = o.m_range;
+ m_begin = ranges::cbegin(m_range.get());
+ ranges::advance(m_begin, n);
+ m_read = o.m_read;
+ return *this;
+ }
+
+ range_wrapper(range_wrapper&& o) noexcept
+ {
+ const auto n =
+ ranges::distance(o.begin_underlying(), o.m_begin);
+ m_range = SCN_MOVE(o.m_range);
+ m_begin = ranges::cbegin(m_range.get());
+ ranges::advance(m_begin, n);
+ m_read = exchange(o.m_read, 0);
+ }
+ range_wrapper& operator=(range_wrapper&& o) noexcept
+ {
+ reset_to_rollback_point();
+
+ const auto n =
+ ranges::distance(o.begin_underlying(), o.m_begin);
+ m_range = SCN_MOVE(o.m_range);
+ m_begin = ranges::cbegin(m_range.get());
+ ranges::advance(m_begin, n);
+ m_read = exchange(o.m_read, 0);
+ return *this;
+ }
+
+ ~range_wrapper() = default;
+
+ iterator begin() const noexcept
+ {
+ return m_begin;
+ }
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wnoexcept")
+ sentinel end() const noexcept(
+ noexcept(ranges::end(SCN_DECLVAL(const storage_type&).get())))
+ {
+ return ranges::end(m_range.get());
+ }
+ SCN_GCC_POP
+
+ struct dummy {
+ };
+
+ /**
+ * Returns `true` if `begin() == end()`.
+ */
+ bool empty() const
+ {
+ return begin() == end();
+ }
+
+ /**
+ * Advance the begin iterator by `n` characters.
+ */
+ iterator advance(difference_type n = 1) noexcept
+ {
+ SCN_EXPECT(_advance_check(
+ n, std::integral_constant<bool, is_contiguous>{}));
+ m_read += n;
+ ranges::advance(m_begin, n);
+ return m_begin;
+ }
+
+ /// @{
+ /**
+ * Advance the begin iterator, until it's equal to `it`.
+ * Assumes that `it` is reachable by repeatedly incrementing begin,
+ * will hang otherwise.
+ */
+ template <typename R = range_nocvref_type,
+ typename std::enable_if<SCN_CHECK_CONCEPT(
+ ranges::sized_range<R>)>::type* = nullptr>
+ void advance_to(iterator it) noexcept
+ {
+ const auto diff = ranges::distance(m_begin, it);
+ m_read += diff;
+ m_begin = it;
+ }
+ template <typename R = range_nocvref_type,
+ typename std::enable_if<SCN_CHECK_CONCEPT(
+ !ranges::sized_range<R>)>::type* = nullptr>
+ void advance_to(iterator it) noexcept
+ {
+ while (m_begin != it) {
+ ++m_read;
+ ++m_begin;
+ }
+ }
+ /// @}
+
+ /**
+ * Returns the begin iterator of the underlying source range, is not
+ * necessarily equal to `begin()`.
+ */
+ iterator begin_underlying() const noexcept(noexcept(
+ ranges::cbegin(SCN_DECLVAL(const range_nocvref_type&))))
+ {
+ return ranges::cbegin(m_range.get());
+ }
+
+ /**
+ * Returns the underlying source range.
+ * Note that `range_underlying().begin()` may not be equal to
+ * `begin()`.
+ */
+ const range_type& range_underlying() const noexcept
+ {
+ return m_range.get();
+ }
+
+ /**
+ * Returns a pointer to the beginning of the range.
+ * `*this` must be contiguous.
+ */
+ template <typename R = range_nocvref_type,
+ typename std::enable_if<SCN_CHECK_CONCEPT(
+ ranges::contiguous_range<R>)>::type* = nullptr>
+ auto data() const
+ noexcept(noexcept(*SCN_DECLVAL(ranges::iterator_t<const R>)))
+ -> decltype(std::addressof(
+ *SCN_DECLVAL(ranges::iterator_t<const R>)))
+ {
+ return std::addressof(*m_begin);
+ }
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wnoexcept")
+ /**
+ * Returns `end() - begin()`.
+ * `*this` must be sized.
+ */
+ template <typename R = range_nocvref_type,
+ typename std::enable_if<SCN_CHECK_CONCEPT(
+ ranges::sized_range<R>)>::type* = nullptr>
+ auto size() const noexcept(noexcept(
+ ranges::distance(SCN_DECLVAL(ranges::iterator_t<const R>),
+ SCN_DECLVAL(ranges::sentinel_t<const R>))))
+ -> decltype(ranges::distance(
+ SCN_DECLVAL(ranges::iterator_t<const R>),
+ SCN_DECLVAL(ranges::sentinel_t<const R>)))
+ {
+ return ranges::distance(m_begin, end());
+ }
+ SCN_GCC_POP
+ struct dummy2 {
+ };
+
+ template <typename R = range_nocvref_type,
+ typename std::enable_if<provides_buffer_access_impl<
+ R>::value>::type* = nullptr>
+ span<const char_type> get_buffer_and_advance(
+ size_t max_size = std::numeric_limits<size_t>::max())
+ {
+ auto buf = get_buffer(m_range.get(), begin(), max_size);
+ if (buf.size() == 0) {
+ return buf;
+ }
+ advance(buf.ssize());
+ return buf;
+ }
+
+ /**
+ * Reset `begin()` to the rollback point, as if by repeatedly
+ * calling `operator--()` on the begin iterator.
+ *
+ * Returns `error::unrecoverable_source_error` on failure.
+ *
+ * \see set_rollback_point()
+ */
+ error reset_to_rollback_point()
+ {
+ for (; m_read != 0; --m_read) {
+ --m_begin;
+ if (m_begin == end()) {
+ return {error::unrecoverable_source_error,
+ "Putback failed"};
+ }
+ }
+ return {};
+ }
+ /**
+ * Sets the rollback point equal to the current `begin()` iterator.
+ *
+ * \see reset_to_rollback_point()
+ */
+ void set_rollback_point()
+ {
+ m_read = 0;
+ }
+
+ void reset_begin_iterator()
+ {
+ detail::reset_begin_iterator(m_begin);
+ }
+
+ /**
+ * Construct a new source range from `begin()` and `end()`, and wrap
+ * it in a new `range_wrapper`.
+ */
+ template <typename R>
+ auto reconstruct_and_rewrap() && -> range_wrapper<R>
+ {
+ auto reconstructed =
+ reconstruct(reconstruct_tag<R>{}, begin(), end());
+ return {SCN_MOVE(reconstructed)};
+ }
+
+ /**
+ * `true` if `value_type` is a character type (`char` or `wchar_t`)
+ * `false` if it's an `expected` containing a character
+ */
+ static constexpr bool is_direct =
+ is_direct_impl<range_nocvref_type>::value;
+ // can call .data() and memcpy
+ /**
+ * `true` if `this->data()` can be called, and `memcpy` can be
+ * performed on it.
+ */
+ static constexpr bool is_contiguous =
+ SCN_CHECK_CONCEPT(ranges::contiguous_range<range_nocvref_type>);
+ /**
+ * `true` if the range provides a way to access a contiguous buffer
+ * on it (`detail::get_buffer()`), which may not provide the entire
+ * source data, e.g. a `span` of `span`s (vectored I/O).
+ */
+ static constexpr bool provides_buffer_access =
+ provides_buffer_access_impl<range_nocvref_type>::value;
+
+ private:
+ template <typename R = Range>
+ bool _advance_check(std::ptrdiff_t n, std::true_type)
+ {
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wzero-as-null-pointer-constant")
+ return m_begin + n <= end();
+ SCN_CLANG_POP
+ }
+ template <typename R = Range>
+ bool _advance_check(std::ptrdiff_t, std::false_type)
+ {
+ return true;
+ }
+
+ storage_type m_range;
+ iterator m_begin;
+ mutable difference_type m_read{0};
+ };
+
+ namespace _wrap {
+ struct fn {
+ private:
+ template <typename Range>
+ static range_wrapper<Range> impl(const range_wrapper<Range>& r,
+ priority_tag<4>) noexcept
+ {
+ return r;
+ }
+ template <typename Range>
+ static range_wrapper<Range> impl(range_wrapper<Range>&& r,
+ priority_tag<4>) noexcept
+ {
+ return SCN_MOVE(r);
+ }
+
+ template <typename Range>
+ static auto impl(Range&& r, priority_tag<3>) noexcept(
+ noexcept(SCN_FWD(r).wrap())) -> decltype(SCN_FWD(r).wrap())
+ {
+ return SCN_FWD(r).wrap();
+ }
+
+ template <typename CharT, std::size_t N>
+ static auto impl(CharT (&str)[N], priority_tag<2>) noexcept
+ -> range_wrapper<
+ basic_string_view<typename std::remove_cv<CharT>::type>>
+ {
+ return {
+ basic_string_view<typename std::remove_cv<CharT>::type>(
+ str, str + N - 1)};
+ }
+
+ template <typename CharT, typename Allocator>
+ static auto impl(
+ const std::basic_string<CharT,
+ std::char_traits<CharT>,
+ Allocator>& str,
+ priority_tag<2>) noexcept
+ -> range_wrapper<basic_string_view<CharT>>
+ {
+ return {basic_string_view<CharT>{str.data(), str.size()}};
+ }
+ template <typename CharT, typename Allocator>
+ static auto impl(
+ std::basic_string<CharT,
+ std::char_traits<CharT>,
+ Allocator>&& str,
+ priority_tag<2>) noexcept(std::
+ is_nothrow_move_constructible<
+ decltype(str)>::value)
+ -> range_wrapper<std::basic_string<CharT,
+ std::char_traits<CharT>,
+ Allocator>>
+ {
+ return {SCN_MOVE(str)};
+ }
+
+#if SCN_HAS_STRING_VIEW
+ template <typename CharT>
+ static auto impl(const std::basic_string_view<CharT>& str,
+ priority_tag<1>) noexcept
+ -> range_wrapper<basic_string_view<CharT>>
+ {
+ return {basic_string_view<CharT>{str.data(), str.size()}};
+ }
+#endif
+ template <typename T,
+ typename CharT = typename std::remove_const<T>::type>
+ static auto impl(span<T> s, priority_tag<2>) noexcept
+ -> range_wrapper<basic_string_view<CharT>>
+ {
+ return {basic_string_view<CharT>{s.data(), s.size()}};
+ }
+
+ template <typename Range,
+ typename = typename std::enable_if<
+ SCN_CHECK_CONCEPT(ranges::view<Range>)>::type>
+ static auto impl(Range r, priority_tag<1>) noexcept
+ -> range_wrapper<Range>
+ {
+ return {r};
+ }
+
+ template <typename Range>
+ static auto impl(const Range& r, priority_tag<0>) noexcept
+ -> range_wrapper<Range&>
+ {
+ static_assert(SCN_CHECK_CONCEPT(ranges::range<Range>),
+ "Input needs to be a Range");
+ return {r};
+ }
+ template <typename Range,
+ typename = typename std::enable_if<
+ !std::is_reference<Range>::value>::type>
+ static auto impl(Range&& r, priority_tag<0>) noexcept
+ -> range_wrapper<Range>
+ {
+ static_assert(SCN_CHECK_CONCEPT(ranges::range<Range>),
+ "Input needs to be a Range");
+ return {SCN_MOVE(r)};
+ }
+
+ public:
+ template <typename Range>
+ auto operator()(Range&& r) const
+ noexcept(noexcept(fn::impl(SCN_FWD(r), priority_tag<4>{})))
+ -> decltype(fn::impl(SCN_FWD(r), priority_tag<4>{}))
+ {
+ return fn::impl(SCN_FWD(r), priority_tag<4>{});
+ }
+ };
+ } // namespace _wrap
+ } // namespace detail
+
+ namespace {
+ /**
+ * Create a `range_wrapper` for any supported source range.
+ */
+ static constexpr auto& wrap =
+ detail::static_const<detail::_wrap::fn>::value;
+ } // namespace
+
+ template <typename Range>
+ struct range_wrapper_for {
+ using type = decltype(wrap(SCN_DECLVAL(Range)));
+ };
+ template <typename Range>
+ using range_wrapper_for_t = typename range_wrapper_for<Range>::type;
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_DETAIL_RANGE_H
diff --git a/src/third-party/scnlib/include/scn/detail/result.h b/src/third-party/scnlib/include/scn/detail/result.h
new file mode 100644
index 0000000..6e5170d
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/result.h
@@ -0,0 +1,595 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_RESULT_H
+#define SCN_DETAIL_RESULT_H
+
+#include "../util/expected.h"
+#include "error.h"
+#include "range.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ /**
+ * Base class for the result type returned by most scanning functions
+ * (except for \ref scan_value). \ref scn::detail::scan_result_base inherits
+ * either from this class or \ref expected.
+ */
+ struct wrapped_error {
+ wrapped_error() = default;
+ wrapped_error(::scn::error e) : err(e) {}
+
+ /// Get underlying error
+ SCN_NODISCARD ::scn::error error() const
+ {
+ return err;
+ }
+
+ /// Did the operation succeed -- true means success
+ explicit operator bool() const
+ {
+ return err.operator bool();
+ }
+
+ ::scn::error err{};
+ };
+
+ namespace detail {
+ template <typename Base>
+ class scan_result_base_wrapper : public Base {
+ public:
+ scan_result_base_wrapper(Base&& b) : Base(SCN_MOVE(b)) {}
+
+ protected:
+ void set_base(const Base& b)
+ {
+ static_cast<Base&>(*this) = b;
+ }
+ void set_base(Base&& b)
+ {
+ static_cast<Base&>(*this) = SCN_MOVE(b);
+ }
+ };
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wdocumentation-unknown-command")
+
+ /// @{
+
+ /**
+ * Type returned by scanning functions.
+ * Contains an error (inherits from it: for \ref error, that's \ref
+ * wrapped_error; with \ref scan_value, inherits from \ref expected),
+ * and the leftover range after scanning.
+ *
+ * The leftover range may reference the range given to the scanning
+ * function. Please take the necessary measures to make sure that the
+ * original range outlives the leftover range. Alternatively, if
+ * possible for your specific range type, call the \ref reconstruct()
+ * member function to get a new, independent range.
+ */
+ template <typename WrappedRange, typename Base>
+ class scan_result_base : public scan_result_base_wrapper<Base> {
+ public:
+ using wrapped_range_type = WrappedRange;
+ using base_type = scan_result_base_wrapper<Base>;
+
+ using range_type = typename wrapped_range_type::range_type;
+ using iterator = typename wrapped_range_type::iterator;
+ using sentinel = typename wrapped_range_type::sentinel;
+ using char_type = typename wrapped_range_type::char_type;
+
+ scan_result_base(Base&& b, wrapped_range_type&& r)
+ : base_type(SCN_MOVE(b)), m_range(SCN_MOVE(r))
+ {
+ }
+
+ /// Beginning of the leftover range
+ iterator begin() const noexcept
+ {
+ return m_range.begin();
+ }
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wnoexcept")
+ // Mitigate problem where Doxygen would think that SCN_GCC_PUSH was
+ // a part of the definition of end()
+ public:
+ /// End of the leftover range
+ sentinel end() const
+ noexcept(noexcept(SCN_DECLVAL(wrapped_range_type).end()))
+ {
+ return m_range.end();
+ }
+
+ /// Whether the leftover range is empty
+ bool empty() const
+ noexcept(noexcept(SCN_DECLVAL(wrapped_range_type).end()))
+ {
+ return begin() == end();
+ }
+ SCN_GCC_POP
+ // See above at SCN_GCC_PUSH
+ public:
+ /// A subrange pointing to the leftover range
+ ranges::subrange<iterator, sentinel> subrange() const
+ {
+ return {begin(), end()};
+ }
+
+ /**
+ * Leftover range.
+ * If the leftover range is used to scan a new value, this member
+ * function should be used.
+ *
+ * \see range_wrapper
+ */
+ wrapped_range_type& range() &
+ {
+ return m_range;
+ }
+ /// \copydoc range()
+ const wrapped_range_type& range() const&
+ {
+ return m_range;
+ }
+ /// \copydoc range()
+ wrapped_range_type range() &&
+ {
+ return SCN_MOVE(m_range);
+ }
+
+ /**
+ * \defgroup range_as_range Contiguous leftover range convertors
+ *
+ * These member functions enable more convenient use of the
+ * leftover range for non-scnlib use cases. The range must be
+ * contiguous. The leftover range is not advanced, and can still be
+ * used.
+ *
+ * @{
+ */
+
+ /**
+ * \ingroup range_as_range
+ * Return a view into the leftover range as a \c string_view.
+ * Operations done to the leftover range after a call to this may
+ * cause issues with iterator invalidation. The returned range will
+ * reference to the leftover range, so be wary of
+ * use-after-free-problems.
+ */
+ template <
+ typename R = wrapped_range_type,
+ typename = typename std::enable_if<R::is_contiguous>::type>
+ basic_string_view<char_type> range_as_string_view() const
+ {
+ return {m_range.data(),
+ static_cast<std::size_t>(m_range.size())};
+ }
+ /**
+ * \ingroup range_as_range
+ * Return a view into the leftover range as a \c span.
+ * Operations done to the leftover range after a call to this may
+ * cause issues with iterator invalidation. The returned range will
+ * reference to the leftover range, so be wary of
+ * use-after-free-problems.
+ */
+ template <
+ typename R = wrapped_range_type,
+ typename = typename std::enable_if<R::is_contiguous>::type>
+ span<const char_type> range_as_span() const
+ {
+ return {m_range.data(),
+ static_cast<std::size_t>(m_range.size())};
+ }
+ /**
+ * \ingroup range_as_range
+ * Return the leftover range as a string. The contents are copied
+ * into the string, so using this will not lead to lifetime issues.
+ */
+ template <
+ typename R = wrapped_range_type,
+ typename = typename std::enable_if<R::is_contiguous>::type>
+ std::basic_string<char_type> range_as_string() const
+ {
+ return {m_range.data(),
+ static_cast<std::size_t>(m_range.size())};
+ }
+ /// @}
+
+ protected:
+ wrapped_range_type m_range;
+
+ private:
+ /// \publicsection
+
+ /**
+ * Reconstructs a range of the original type, independent of the
+ * leftover range, beginning from \ref begin and ending in \ref end.
+ *
+ * Compiles only if range is reconstructible.
+ */
+ template <typename R = typename WrappedRange::range_type>
+ R reconstruct() const;
+ };
+
+ template <typename WrappedRange, typename Base>
+ class intermediary_scan_result
+ : public scan_result_base<WrappedRange, Base> {
+ public:
+ using base_type = scan_result_base<WrappedRange, Base>;
+
+ intermediary_scan_result(Base&& b, WrappedRange&& r)
+ : base_type(SCN_MOVE(b), SCN_MOVE(r))
+ {
+ }
+
+ template <typename R = WrappedRange>
+ void reconstruct() const
+ {
+ static_assert(
+ dependent_false<R>::value,
+ "Cannot call .reconstruct() on intermediary_scan_result. "
+ "Assign this value to a previous result value returned by "
+ "a scanning function or make_result (type: "
+ "reconstructed_scan_result or "
+ "non_reconstructed_scan_result) ");
+ }
+ };
+ template <typename WrappedRange, typename Base>
+ class reconstructed_scan_result
+ : public intermediary_scan_result<WrappedRange, Base> {
+ public:
+ using unwrapped_range_type = typename WrappedRange::range_type;
+ using base_type = intermediary_scan_result<WrappedRange, Base>;
+
+ reconstructed_scan_result(Base&& b, WrappedRange&& r)
+ : base_type(SCN_MOVE(b), SCN_MOVE(r))
+ {
+ }
+
+ reconstructed_scan_result& operator=(
+ const intermediary_scan_result<WrappedRange, Base>& other)
+ {
+ this->set_base(other);
+ this->m_range = other.range();
+ return *this;
+ }
+ reconstructed_scan_result& operator=(
+ intermediary_scan_result<WrappedRange, Base>&& other)
+ {
+ this->set_base(other);
+ this->m_range = other.range();
+ return *this;
+ }
+
+ unwrapped_range_type reconstruct() const
+ {
+ return this->range().range_underlying();
+ }
+ };
+ template <typename WrappedRange, typename UnwrappedRange, typename Base>
+ class non_reconstructed_scan_result
+ : public intermediary_scan_result<WrappedRange, Base> {
+ public:
+ using unwrapped_range_type = UnwrappedRange;
+ using base_type = intermediary_scan_result<WrappedRange, Base>;
+
+ non_reconstructed_scan_result(Base&& b, WrappedRange&& r)
+ : base_type(SCN_MOVE(b), SCN_MOVE(r))
+ {
+ }
+
+ non_reconstructed_scan_result& operator=(
+ const intermediary_scan_result<WrappedRange, Base>& other)
+ {
+ this->set_base(other);
+ this->m_range = other.range();
+ return *this;
+ }
+ non_reconstructed_scan_result& operator=(
+ intermediary_scan_result<WrappedRange, Base>&& other)
+ {
+ this->set_base(other);
+ this->m_range = other.range();
+ return *this;
+ }
+
+ template <typename R = unwrapped_range_type>
+ R reconstruct() const
+ {
+ return ::scn::detail::reconstruct(reconstruct_tag<R>{},
+ this->begin(), this->end());
+ }
+ };
+
+ /// @}
+
+ // -Wdocumentation-unknown-command
+ SCN_CLANG_PUSH
+
+ template <typename T>
+ struct range_tag {
+ };
+
+ namespace _wrap_result {
+ struct fn {
+ private:
+ // Range = range_wrapper<ref>&
+ template <typename Error, typename Range>
+ static auto impl(Error e,
+ range_tag<range_wrapper<Range&>&>,
+ range_wrapper<Range&>&& range,
+ priority_tag<5>) noexcept
+ -> intermediary_scan_result<range_wrapper<Range&>, Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)};
+ }
+ // Range = const range_wrapper<ref>&
+ template <typename Error, typename Range>
+ static auto impl(Error e,
+ range_tag<const range_wrapper<Range&>&>,
+ range_wrapper<Range&>&& range,
+ priority_tag<5>) noexcept
+ -> intermediary_scan_result<range_wrapper<Range&>, Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)};
+ }
+ // Range = range_wrapper<ref>&&
+ template <typename Error, typename Range>
+ static auto impl(Error e,
+ range_tag<range_wrapper<Range&>>,
+ range_wrapper<Range&>&& range,
+ priority_tag<5>) noexcept
+ -> intermediary_scan_result<range_wrapper<Range&>, Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)};
+ }
+
+ // Range = range_wrapper<non-ref>&
+ template <typename Error, typename Range>
+ static auto impl(Error e,
+ range_tag<range_wrapper<Range>&>,
+ range_wrapper<Range>&& range,
+ priority_tag<4>) noexcept
+ -> intermediary_scan_result<range_wrapper<Range>, Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)};
+ }
+ // Range = const range_wrapper<non-ref>&
+ template <typename Error, typename Range>
+ static auto impl(Error e,
+ range_tag<const range_wrapper<Range>&>,
+ range_wrapper<Range>&& range,
+ priority_tag<4>) noexcept
+ -> intermediary_scan_result<range_wrapper<Range>, Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)};
+ }
+ // Range = range_wrapper<non-ref>&&
+ template <typename Error, typename Range>
+ static auto impl(Error e,
+ range_tag<range_wrapper<Range>>,
+ range_wrapper<Range>&& range,
+ priority_tag<4>) noexcept
+ -> intermediary_scan_result<range_wrapper<Range>, Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)};
+ }
+
+ // string literals are wonky
+ template <typename Error,
+ typename CharT,
+ size_t N,
+ typename NoCVRef = remove_cvref_t<CharT>>
+ static auto impl(
+ Error e,
+ range_tag<CharT (&)[N]>,
+ range_wrapper<basic_string_view<NoCVRef>>&& range,
+ priority_tag<3>) noexcept
+ -> reconstructed_scan_result<
+ range_wrapper<basic_string_view<NoCVRef>>,
+ Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)
+ .template reconstruct_and_rewrap<
+ basic_string_view<NoCVRef>>()};
+ }
+
+ // (const) InputRange&: View + Reconstructible
+ // wrapped<any>
+ template <typename Error,
+ typename InputRange,
+ typename InnerWrappedRange,
+ typename InputRangeNoConst =
+ typename std::remove_const<InputRange>::type,
+ typename = typename std::enable_if<SCN_CHECK_CONCEPT(
+ ranges::view<InputRangeNoConst>)>::type>
+ static auto impl(Error e,
+ range_tag<InputRange&>,
+ range_wrapper<InnerWrappedRange>&& range,
+ priority_tag<2>) noexcept
+ -> reconstructed_scan_result<
+ decltype(SCN_MOVE(range)
+ .template reconstruct_and_rewrap<
+ InputRangeNoConst>()),
+ Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)
+ .template reconstruct_and_rewrap<
+ InputRangeNoConst>()};
+ }
+
+ // (const) InputRange&: other
+ // wrapped<any>
+ template <typename Error,
+ typename InputRange,
+ typename InnerWrappedRange>
+ static auto impl(Error e,
+ range_tag<InputRange&>,
+ range_wrapper<InnerWrappedRange>&& range,
+ priority_tag<1>) noexcept
+ -> non_reconstructed_scan_result<
+ range_wrapper<InnerWrappedRange>,
+ typename std::remove_const<InputRange>::type,
+ Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)};
+ }
+
+ // InputRange&&: View + Reconstructible
+ // wrapped<non-ref>
+ template <typename Error,
+ typename InputRange,
+ typename InnerWrappedRange,
+ typename InputRangeNoConst =
+ typename std::remove_const<InputRange>::type,
+ typename = typename std::enable_if<SCN_CHECK_CONCEPT(
+ ranges::view<InputRangeNoConst>)>::type>
+ static auto impl(Error e,
+ range_tag<InputRange>,
+ range_wrapper<InnerWrappedRange>&& range,
+ priority_tag<1>) noexcept
+ -> reconstructed_scan_result<
+ decltype(SCN_MOVE(range)
+ .template reconstruct_and_rewrap<
+ InputRangeNoConst>()),
+ Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)
+ .template reconstruct_and_rewrap<
+ InputRangeNoConst>()};
+ }
+
+ // InputRange&&: other
+ // wrapped<non-ref>
+ template <typename Error,
+ typename InputRange,
+ typename InnerWrappedRange>
+ static auto impl(Error e,
+ range_tag<InputRange>,
+ range_wrapper<InnerWrappedRange>&& range,
+ priority_tag<0>) noexcept
+ -> non_reconstructed_scan_result<
+ range_wrapper<InputRange>,
+ typename std::remove_const<InputRange>::type,
+ Error>
+ {
+ return {SCN_MOVE(e), SCN_MOVE(range)};
+ }
+
+#if 0
+ // InputRange&&
+ // wrapped<ref>
+ template <typename Error,
+ typename InputRange,
+ typename InnerWrappedRange,
+ typename NoRef = typename std::remove_reference<
+ InnerWrappedRange>::type>
+ static auto impl(Error e,
+ range_tag<InputRange>,
+ range_wrapper<InnerWrappedRange&>&& range,
+ priority_tag<0>) noexcept
+ -> reconstructed_scan_result<range_wrapper<NoRef>, Error>
+ {
+ return {SCN_MOVE(e),
+ SCN_MOVE(range)
+ .template rewrap_and_reconstruct<NoRef>()};
+ }
+#endif
+
+ public:
+ template <typename Error,
+ typename InputRange,
+ typename InnerWrappedRange>
+ auto operator()(Error e,
+ range_tag<InputRange> tag,
+ range_wrapper<InnerWrappedRange>&& range) const
+ noexcept(noexcept(impl(SCN_MOVE(e),
+ tag,
+ SCN_MOVE(range),
+ priority_tag<5>{})))
+ -> decltype(impl(SCN_MOVE(e),
+ tag,
+ SCN_MOVE(range),
+ priority_tag<5>{}))
+ {
+ static_assert(SCN_CHECK_CONCEPT(ranges::range<InputRange>),
+ "Input needs to be a Range");
+ return impl(SCN_MOVE(e), tag, SCN_MOVE(range),
+ priority_tag<5>{});
+ }
+ };
+ } // namespace _wrap_result
+ namespace {
+ static constexpr auto& wrap_result =
+ static_const<_wrap_result::fn>::value;
+ }
+
+ template <typename Error, typename InputRange, typename WrappedRange>
+ struct result_type_for {
+ using type =
+ decltype(wrap_result(SCN_DECLVAL(Error &&),
+ SCN_DECLVAL(range_tag<InputRange>),
+ SCN_DECLVAL(WrappedRange&&)));
+ };
+ template <typename Error, typename InputRange, typename WrappedRange>
+ using result_type_for_t =
+ typename result_type_for<Error, InputRange, WrappedRange>::type;
+ } // namespace detail
+
+ /**
+ * Create a result object for range \c Range.
+ * Useful if one wishes to scan from the same range in a loop.
+ *
+ * \code{.cpp}
+ * auto source = ...;
+ * auto result = make_result(source);
+ * // scan until failure (no more `int`s, or EOF)
+ * while (result) {
+ * int i;
+ * result = scn::scan(result.range(), "{}", i);
+ * // use i
+ * }
+ * // see result for why we exited the loop
+ * \endcode
+ *
+ * \c Error template parameter can be used to customize the error type for
+ * the result object. By default, it's \ref wrapped_error, which is what
+ * most of the scanning functions use. For \c scan_value, use \c
+ * expected<T>:
+ *
+ * \code{.cpp}
+ * auto result = make_result<scn::expected<int>>(source);
+ * while (result) {
+ * result = scn::scan_value<int>(result.range(), "{}");
+ * // use result.value()
+ * }
+ * \endcode
+ */
+ template <typename Error = wrapped_error, typename Range>
+ auto make_result(Range&& r)
+ -> detail::result_type_for_t<Error, Range, range_wrapper_for_t<Range>>
+ {
+ return detail::wrap_result(Error{}, detail::range_tag<Range>{},
+ wrap(r));
+ }
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_DETAIL_RESULT_H
diff --git a/src/third-party/scnlib/include/scn/detail/vectored.h b/src/third-party/scnlib/include/scn/detail/vectored.h
new file mode 100644
index 0000000..f5c3868
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/vectored.h
@@ -0,0 +1,166 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_VECTORED_H
+#define SCN_DETAIL_VECTORED_H
+
+#include "../ranges/util.h"
+#include "../util/math.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ namespace _get_buffer {
+ struct fn {
+ private:
+ template <typename It>
+ static It _get_end(It begin, It end, size_t max_size)
+ {
+ return begin +
+ min(max_size, static_cast<std::size_t>(
+ ranges::distance(begin, end)));
+ }
+
+ template <typename CharT>
+ static SCN_CONSTEXPR14
+ span<typename std::add_const<CharT>::type>
+ impl(span<span<CharT>> s,
+ typename span<CharT>::iterator begin,
+ size_t max_size,
+ priority_tag<3>) noexcept
+ {
+ auto buf_it = s.begin();
+ for (; buf_it != s.end(); ++buf_it) {
+ if (begin >= buf_it->begin() && begin < buf_it->end()) {
+ break;
+ }
+ if (begin == buf_it->end()) {
+ ++buf_it;
+ begin = buf_it->begin();
+ break;
+ }
+ }
+ if (buf_it == s.end()) {
+ return {};
+ }
+ return {begin, _get_end(begin, buf_it->end(), max_size)};
+ }
+
+ template <
+ typename Range,
+ typename std::enable_if<SCN_CHECK_CONCEPT(
+ ranges::contiguous_range<Range>)>::type* = nullptr>
+ static SCN_CONSTEXPR14 span<typename std::add_const<
+ ranges::range_value_t<const Range>>::type>
+ impl(const Range& s,
+ ranges::iterator_t<const Range> begin,
+ size_t max_size,
+ priority_tag<2>) noexcept
+ {
+ auto b = ranges::begin(s);
+ auto e = ranges::end(s);
+ return {to_address(begin),
+ _get_end(to_address(begin),
+ to_address_safe(e, b, e), max_size)};
+ }
+
+ template <typename Range, typename It>
+ static auto impl(
+ const Range& r,
+ It begin,
+ size_t max_size,
+ priority_tag<1>) noexcept(noexcept(r.get_buffer(begin,
+ max_size)))
+ -> decltype(r.get_buffer(begin, max_size))
+ {
+ return r.get_buffer(begin, max_size);
+ }
+
+ template <typename Range, typename It>
+ static auto impl(
+ const Range& r,
+ It begin,
+ size_t max_size,
+ priority_tag<0>) noexcept(noexcept(get_buffer(r,
+ begin,
+ max_size)))
+ -> decltype(get_buffer(r, begin, max_size))
+ {
+ return get_buffer(r, begin, max_size);
+ }
+
+ public:
+ template <typename Range, typename It>
+ SCN_CONSTEXPR14 auto operator()(const Range& r,
+ It begin,
+ size_t max_size) const
+ noexcept(noexcept(
+ fn::impl(r, begin, max_size, priority_tag<3>{})))
+ -> decltype(fn::impl(r,
+ begin,
+ max_size,
+ priority_tag<3>{}))
+ {
+ return fn::impl(r, begin, max_size, priority_tag<3>{});
+ }
+
+ template <typename Range, typename It>
+ SCN_CONSTEXPR14 auto operator()(const Range& r, It begin) const
+ noexcept(
+ noexcept(fn::impl(r,
+ begin,
+ std::numeric_limits<size_t>::max(),
+ priority_tag<3>{})))
+ -> decltype(fn::impl(r,
+ begin,
+ std::numeric_limits<size_t>::max(),
+ priority_tag<3>{}))
+ {
+ return fn::impl(r, begin,
+ std::numeric_limits<size_t>::max(),
+ priority_tag<3>{});
+ }
+ };
+ } // namespace _get_buffer
+
+ namespace {
+ static constexpr auto& get_buffer =
+ detail::static_const<_get_buffer::fn>::value;
+ } // namespace
+
+ struct provides_buffer_access_concept {
+ template <typename Range, typename Iterator>
+ auto _test_requires(const Range& r, Iterator begin)
+ -> decltype(scn::detail::valid_expr(
+ ::scn::detail::get_buffer(r, begin)));
+ };
+ template <typename Range, typename = void>
+ struct provides_buffer_access_impl
+ : std::integral_constant<
+ bool,
+ ::scn::custom_ranges::detail::_requires<
+ provides_buffer_access_concept,
+ Range,
+ ::scn::ranges::iterator_t<const Range>>::value> {
+ };
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/detail/visitor.h b/src/third-party/scnlib/include/scn/detail/visitor.h
new file mode 100644
index 0000000..e88e2f7
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/detail/visitor.h
@@ -0,0 +1,248 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_DETAIL_VISITOR_H
+#define SCN_DETAIL_VISITOR_H
+
+#include "../reader/reader.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ template <typename Context, typename ParseCtx>
+ class basic_visitor {
+ public:
+ using context_type = Context;
+ using char_type = typename Context::char_type;
+ using arg_type = basic_arg<char_type>;
+
+ basic_visitor(Context& ctx, ParseCtx& pctx)
+ : m_ctx(std::addressof(ctx)), m_pctx(std::addressof(pctx))
+ {
+ }
+
+ template <typename T>
+ auto operator()(T&& val) -> error
+ {
+ return visit(SCN_FWD(val), detail::priority_tag<1>{});
+ }
+
+ private:
+ auto visit(code_point& val, detail::priority_tag<1>) -> error
+ {
+ return detail::visitor_boilerplate<detail::code_point_scanner>(
+ val, *m_ctx, *m_pctx);
+ }
+ auto visit(span<char_type>& val, detail::priority_tag<1>) -> error
+ {
+ return detail::visitor_boilerplate<detail::span_scanner>(
+ val, *m_ctx, *m_pctx);
+ }
+ auto visit(bool& val, detail::priority_tag<1>) -> error
+ {
+ return detail::visitor_boilerplate<detail::bool_scanner>(
+ val, *m_ctx, *m_pctx);
+ }
+
+#define SCN_VISIT_INT(T) \
+ error visit(T& val, detail::priority_tag<0>) \
+ { \
+ return detail::visitor_boilerplate<detail::integer_scanner<T>>( \
+ val, *m_ctx, *m_pctx); \
+ }
+ SCN_VISIT_INT(signed char)
+ SCN_VISIT_INT(short)
+ SCN_VISIT_INT(int)
+ SCN_VISIT_INT(long)
+ SCN_VISIT_INT(long long)
+ SCN_VISIT_INT(unsigned char)
+ SCN_VISIT_INT(unsigned short)
+ SCN_VISIT_INT(unsigned int)
+ SCN_VISIT_INT(unsigned long)
+ SCN_VISIT_INT(unsigned long long)
+ SCN_VISIT_INT(char_type)
+#undef SCN_VISIT_INT
+
+#define SCN_VISIT_FLOAT(T) \
+ error visit(T& val, detail::priority_tag<1>) \
+ { \
+ return detail::visitor_boilerplate<detail::float_scanner<T>>( \
+ val, *m_ctx, *m_pctx); \
+ }
+ SCN_VISIT_FLOAT(float)
+ SCN_VISIT_FLOAT(double)
+ SCN_VISIT_FLOAT(long double)
+#undef SCN_VISIT_FLOAT
+
+ auto visit(std::basic_string<char_type>& val, detail::priority_tag<1>)
+ -> error
+ {
+ return detail::visitor_boilerplate<detail::string_scanner>(
+ val, *m_ctx, *m_pctx);
+ }
+ auto visit(basic_string_view<char_type>& val, detail::priority_tag<1>)
+ -> error
+ {
+ return detail::visitor_boilerplate<detail::string_view_scanner>(
+ val, *m_ctx, *m_pctx);
+ }
+ auto visit(typename arg_type::handle val, detail::priority_tag<1>)
+ -> error
+ {
+ return val.scan(*m_ctx, *m_pctx);
+ }
+ [[noreturn]] auto visit(detail::monostate, detail::priority_tag<0>)
+ -> error
+ {
+ SCN_UNREACHABLE;
+ }
+
+ Context* m_ctx;
+ ParseCtx* m_pctx;
+ };
+
+ template <typename Context, typename ParseCtx>
+ error visit(Context& ctx,
+ ParseCtx& pctx,
+ basic_args<typename Context::char_type> args)
+ {
+ using char_type = typename Context::char_type;
+ using arg_type = basic_arg<char_type>;
+ auto arg = arg_type{};
+
+ while (pctx) {
+ if (pctx.should_skip_ws()) {
+ // Skip whitespace from format string and from stream
+ // EOF is not an error
+ auto ret = skip_range_whitespace(ctx, false);
+ if (SCN_UNLIKELY(!ret)) {
+ if (ret == error::end_of_range) {
+ break;
+ }
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ auto rb = ctx.range().reset_to_rollback_point();
+ if (!rb) {
+ return rb;
+ }
+ return ret;
+ }
+ // Don't advance pctx, should_skip_ws() does it for us
+ continue;
+ }
+
+ // Non-brace character, or
+ // Brace followed by another brace, meaning a literal '{'
+ if (pctx.should_read_literal()) {
+ if (SCN_UNLIKELY(!pctx)) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string"};
+ }
+ // Check for any non-specifier {foo} characters
+ alignas(typename Context::char_type) unsigned char buf[4] = {0};
+ auto ret = read_code_point(ctx.range(), make_span(buf, 4));
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ if (!ret || !pctx.check_literal(ret.value().chars)) {
+ auto rb = ctx.range().reset_to_rollback_point();
+ if (!rb) {
+ // Failed rollback
+ return rb;
+ }
+ if (!ret) {
+ // Failed read
+ return ret.error();
+ }
+
+ // Mismatching characters in scan string and stream
+ return {error::invalid_scanned_value,
+ "Expected character from format string not "
+ "found in the stream"};
+ }
+ // Bump pctx to next char
+ if (!pctx.advance_cp()) {
+ pctx.advance_char();
+ }
+ }
+ else {
+ // Scan argument
+ auto arg_wrapped = [&]() -> expected<arg_type> {
+ if (!pctx.has_arg_id()) {
+ return next_arg(args, pctx);
+ }
+ auto id_wrapped = pctx.parse_arg_id();
+ if (!id_wrapped) {
+ return id_wrapped.error();
+ }
+ auto id = id_wrapped.value();
+ SCN_ENSURE(!id.empty());
+ if (ctx.locale().get_static().is_digit(id.front())) {
+ auto s =
+ detail::simple_integer_scanner<std::ptrdiff_t>{};
+ std::ptrdiff_t i{0};
+ auto span = make_span(id.data(), id.size());
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ auto ret = s.scan(span, i, 10);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ if (!ret || ret.value() != span.end()) {
+ return error(error::invalid_format_string,
+ "Failed to parse argument id from "
+ "format string");
+ }
+ return get_arg(args, pctx, i);
+ }
+ return get_arg(args, pctx, id);
+ }();
+ if (!arg_wrapped) {
+ return arg_wrapped.error();
+ }
+ arg = arg_wrapped.value();
+ SCN_ENSURE(arg);
+ if (!pctx) {
+ return {error::invalid_format_string,
+ "Unexpected end of format argument"};
+ }
+ auto ret = visit_arg<char_type>(
+ basic_visitor<Context, ParseCtx>(ctx, pctx), arg);
+ if (!ret) {
+ auto rb = ctx.range().reset_to_rollback_point();
+ if (!rb) {
+ return rb;
+ }
+ return ret;
+ }
+ // Handle next arg and bump pctx
+ pctx.arg_handled();
+ if (pctx) {
+ auto e = pctx.advance_cp();
+ if (!e) {
+ return e;
+ }
+ }
+ }
+ }
+ if (pctx) {
+ // Format string not exhausted
+ return {error::invalid_format_string,
+ "Format string not exhausted"};
+ }
+ ctx.range().set_rollback_point();
+ return {};
+ }
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_DETAIL_VISITOR_H
diff --git a/src/third-party/scnlib/include/scn/fwd.h b/src/third-party/scnlib/include/scn/fwd.h
new file mode 100644
index 0000000..e91a258
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/fwd.h
@@ -0,0 +1,23 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_FWD_H
+#define SCN_FWD_H
+
+#include "detail/fwd.h"
+
+#endif // SCN_FWD_H
diff --git a/src/third-party/scnlib/include/scn/istream.h b/src/third-party/scnlib/include/scn/istream.h
new file mode 100644
index 0000000..acb2774
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/istream.h
@@ -0,0 +1,23 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_ISTREAM_H
+#define SCN_ISTREAM_H
+
+#include "scan/istream.h"
+
+#endif // SCN_ISTREAM_H
diff --git a/src/third-party/scnlib/include/scn/ranges/custom_impl.h b/src/third-party/scnlib/include/scn/ranges/custom_impl.h
new file mode 100644
index 0000000..86d73d5
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/ranges/custom_impl.h
@@ -0,0 +1,1632 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+//
+// The contents of this file are adapted from NanoRange.
+// https://github.com/tcbrindle/NanoRange
+// Copyright (c) 2018 Tristan Brindle
+// Distributed under the Boost Software License, Version 1.0
+
+#ifndef SCN_RANGES_CUSTOM_IMPL_H
+#define SCN_RANGES_CUSTOM_IMPL_H
+
+#include "util.h"
+
+#include "../util/string_view.h"
+
+#include <iterator>
+#include <utility>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace custom_ranges {
+ // iterator_category is span.h
+
+ template <typename T>
+ struct iterator_category;
+
+ namespace detail {
+ template <typename T, typename = void>
+ struct iterator_category {
+ };
+ template <typename T>
+ struct iterator_category<T*>
+ : std::enable_if<std::is_object<T>::value,
+ contiguous_iterator_tag> {
+ };
+ template <typename T>
+ struct iterator_category<const T> : iterator_category<T> {
+ };
+ template <typename T>
+ struct iterator_category<
+ T,
+ detail::void_t<typename T::iterator_category>> {
+ using type = typename T::iterator_category;
+ };
+ } // namespace detail
+
+ template <typename T>
+ struct iterator_category : detail::iterator_category<T> {
+ };
+ template <typename T>
+ using iterator_category_t = typename iterator_category<T>::type;
+
+ template <typename T>
+ using iter_reference_t = decltype(*SCN_DECLVAL(T&));
+
+ // iter_difference_t
+ template <typename>
+ struct incrementable_traits;
+
+ namespace detail {
+ struct empty {
+ };
+
+ template <typename T>
+ struct with_difference_type {
+ using difference_type = T;
+ };
+ template <typename, typename = void>
+ struct incrementable_traits_helper {
+ };
+
+ template <>
+ struct incrementable_traits_helper<void*> {
+ };
+ template <typename T>
+ struct incrementable_traits_helper<T*>
+ : std::conditional<std::is_object<T>::value,
+ with_difference_type<std::ptrdiff_t>,
+ empty>::type {
+ };
+ template <typename I>
+ struct incrementable_traits_helper<const I>
+ : incrementable_traits<typename std::decay<I>::type> {
+ };
+
+ template <typename, typename = void>
+ struct has_member_difference_type : std::false_type {
+ };
+ template <typename T>
+ struct has_member_difference_type<
+ T,
+ detail::void_t<typename T::difference_type>> : std::true_type {
+ };
+
+ template <typename T>
+ struct incrementable_traits_helper<
+ T,
+ typename std::enable_if<
+ has_member_difference_type<T>::value>::type> {
+ using difference_type = typename T::difference_type;
+ };
+ template <typename T>
+ struct incrementable_traits_helper<
+ T,
+ typename std::enable_if<
+ !std::is_pointer<T>::value &&
+ !has_member_difference_type<T>::value &&
+ std::is_integral<decltype(SCN_DECLVAL(const T&) -
+ SCN_DECLVAL(const T&))>::value>::
+ type>
+ : with_difference_type<typename std::make_signed<
+ decltype(SCN_DECLVAL(T) - SCN_DECLVAL(T))>::type> {
+ };
+ } // namespace detail
+
+ template <typename T>
+ struct incrementable_traits : detail::incrementable_traits_helper<T> {
+ };
+
+ template <typename T>
+ using iter_difference_t =
+ typename incrementable_traits<T>::difference_type;
+
+ // iter_value_t
+ template <typename>
+ struct readable_traits;
+
+ namespace detail {
+ template <typename T>
+ struct with_value_type {
+ using value_type = T;
+ };
+ template <typename, typename = void>
+ struct readable_traits_helper {
+ };
+
+ template <typename T>
+ struct readable_traits_helper<T*>
+ : std::conditional<
+ std::is_object<T>::value,
+ with_value_type<typename std::remove_cv<T>::type>,
+ empty>::type {
+ };
+
+ template <typename I>
+ struct readable_traits_helper<
+ I,
+ typename std::enable_if<std::is_array<I>::value>::type>
+ : readable_traits<typename std::decay<I>::type> {
+ };
+
+ template <typename I>
+ struct readable_traits_helper<
+ const I,
+ typename std::enable_if<!std::is_array<I>::value>::type>
+ : readable_traits<typename std::decay<I>::type> {
+ };
+
+ template <typename T, typename V = typename T::value_type>
+ struct member_value_type
+ : std::conditional<std::is_object<V>::value,
+ with_value_type<V>,
+ empty>::type {
+ };
+
+ template <typename T, typename E = typename T::element_type>
+ struct _member_element_type
+ : std::conditional<
+ std::is_object<E>::value,
+ with_value_type<typename std::remove_cv<E>::type>,
+ empty>::type {
+ };
+
+ template <typename T>
+ using member_value_type_t = typename T::value_type;
+
+ template <typename T>
+ struct has_member_value_type : exists<member_value_type_t, T> {
+ };
+
+ template <typename T>
+ using member_element_type_t = typename T::element_type;
+
+ template <typename T>
+ struct has_member_element_type : exists<member_element_type_t, T> {
+ };
+
+ template <typename T>
+ struct readable_traits_helper<
+ T,
+ typename std::enable_if<
+ has_member_value_type<T>::value &&
+ !has_member_element_type<T>::value>::type>
+ : member_value_type<T> {
+ };
+
+ template <typename T>
+ struct readable_traits_helper<
+ T,
+ typename std::enable_if<has_member_element_type<T>::value &&
+ !has_member_value_type<T>::value>::type>
+ : _member_element_type<T> {
+ };
+
+ template <typename T>
+ struct readable_traits_helper<
+ T,
+ typename std::enable_if<
+ has_member_element_type<T>::value &&
+ has_member_value_type<T>::value>::type> {
+ };
+ } // namespace detail
+
+ template <typename T>
+ struct readable_traits : detail::readable_traits_helper<T> {
+ };
+
+ template <typename T>
+ using iter_value_t = typename readable_traits<T>::value_type;
+
+ // sentinel_for
+ namespace detail {
+ struct sentinel_for_concept {
+ template <typename S, typename I>
+ auto _test_requires(S s, I i)
+ -> decltype(scn::detail::valid_expr(*i, i == s, i != s));
+ };
+ } // namespace detail
+ template <typename S, typename I>
+ struct sentinel_for
+ : std::integral_constant<
+ bool,
+ std::is_default_constructible<S>::value &&
+ std::is_copy_constructible<S>::value &&
+ detail::_requires<detail::sentinel_for_concept, S, I>::
+ value> {
+ };
+
+ // sized_sentinel_for
+ namespace detail {
+ struct sized_sentinel_for_concept {
+ template <typename S, typename I>
+ auto _test_requires(const S& s, const I& i) -> decltype(
+ detail::requires_expr<
+ std::is_same<decltype(s - i),
+ iter_difference_t<I>>::value>{},
+ detail::requires_expr<
+ std::is_same<decltype(i - s),
+ iter_difference_t<I>>::value>{});
+ };
+ } // namespace detail
+ template <typename S, typename I>
+ struct sized_sentinel_for
+ : std::integral_constant<
+ bool,
+ detail::_requires<detail::sized_sentinel_for_concept, S, I>::
+ value &&
+ sentinel_for<S, I>::value> {
+ };
+ template <typename S>
+ struct sized_sentinel_for<S, void*> : std::false_type {
+ };
+ template <typename I>
+ struct sized_sentinel_for<void*, I> : std::false_type {
+ };
+ template <>
+ struct sized_sentinel_for<void*, void*> : std::false_type {
+ };
+
+ // begin
+ namespace _begin {
+ template <typename T>
+ void begin(T&&) = delete;
+ template <typename T>
+ void begin(std::initializer_list<T>&&) = delete;
+
+ struct fn {
+ private:
+ template <typename T, std::size_t N>
+ static SCN_CONSTEXPR14 void impl(T(&&)[N],
+ detail::priority_tag<3>) =
+ delete;
+
+ template <typename T, std::size_t N>
+ static SCN_CONSTEXPR14 auto impl(
+ T (&t)[N],
+ detail::priority_tag<3>) noexcept -> decltype((t) + 0)
+ {
+ return (t) + 0;
+ }
+
+ template <typename C>
+ static SCN_CONSTEXPR14 auto impl(
+ basic_string_view<C> sv,
+ detail::priority_tag<2>) noexcept -> decltype(sv.begin())
+ {
+ return sv.begin();
+ }
+
+ template <typename T>
+ static SCN_CONSTEXPR14 auto
+ impl(T& t, detail::priority_tag<1>) noexcept(noexcept(
+ ::scn::custom_ranges::detail::decay_copy(t.begin())))
+ -> decltype(::scn::custom_ranges::detail::decay_copy(
+ t.begin()))
+ {
+ return ::scn::custom_ranges::detail::decay_copy(t.begin());
+ }
+
+ template <typename T>
+ static SCN_CONSTEXPR14 auto
+ impl(T&& t, detail::priority_tag<0>) noexcept(
+ noexcept(::scn::custom_ranges::detail::decay_copy(
+ begin(SCN_FWD(t)))))
+ -> decltype(::scn::custom_ranges::detail::decay_copy(
+ begin(SCN_FWD(t))))
+ {
+ return ::scn::custom_ranges::detail::decay_copy(
+ begin(SCN_FWD(t)));
+ }
+
+ public:
+ template <typename T>
+ SCN_CONSTEXPR14 auto operator()(T&& t) const noexcept(
+ noexcept(fn::impl(SCN_FWD(t), detail::priority_tag<3>{})))
+ -> decltype(fn::impl(SCN_FWD(t), detail::priority_tag<3>{}))
+ {
+ return fn::impl(SCN_FWD(t), detail::priority_tag<3>{});
+ }
+ };
+ } // namespace _begin
+ namespace {
+ constexpr auto& begin = detail::static_const<_begin::fn>::value;
+ }
+
+ // end
+ namespace _end {
+ template <typename T>
+ void end(T&&) = delete;
+ template <typename T>
+ void end(std::initializer_list<T>&&) = delete;
+
+ struct fn {
+ private:
+ template <typename T, std::size_t N>
+ static constexpr void impl(T(&&)[N],
+ detail::priority_tag<2>) = delete;
+
+ template <typename T, std::size_t N>
+ static constexpr auto impl(T (&t)[N],
+ detail::priority_tag<2>) noexcept
+ -> decltype((t) + N)
+ {
+ return (t) + N;
+ }
+
+ template <typename C>
+ static constexpr auto impl(basic_string_view<C> sv,
+ detail::priority_tag<2>) noexcept
+ -> decltype(sv.end())
+ {
+ return sv.end();
+ }
+
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wnoexcept")
+ template <typename T,
+ typename S =
+ decltype(::scn::custom_ranges::detail::decay_copy(
+ SCN_DECLVAL(T&).end())),
+ typename I = decltype(::scn::custom_ranges::begin(
+ SCN_DECLVAL(T&)))>
+ static constexpr auto
+ impl(T& t, detail::priority_tag<1>) noexcept(
+ noexcept(::scn::custom_ranges::detail::decay_copy(t.end())))
+ -> decltype(::scn::custom_ranges::detail::decay_copy(
+ t.end()))
+ {
+ return ::scn::custom_ranges::detail::decay_copy(t.end());
+ }
+
+ template <typename T,
+ typename S =
+ decltype(::scn::custom_ranges::detail::decay_copy(
+ end(SCN_DECLVAL(T)))),
+ typename I = decltype(::scn::custom_ranges::begin(
+ SCN_DECLVAL(T)))>
+ static constexpr auto
+ impl(T& t, detail::priority_tag<0>) noexcept(noexcept(
+ ::scn::custom_ranges::detail::decay_copy(end(SCN_FWD(t)))))
+ -> S
+ {
+ return ::scn::custom_ranges::detail::decay_copy(
+ end(SCN_FWD(t)));
+ }
+
+ public:
+ template <typename T>
+ constexpr auto operator()(T&& t) const noexcept(
+ noexcept(fn::impl(SCN_FWD(t), detail::priority_tag<2>{})))
+ -> decltype(fn::impl(SCN_FWD(t), detail::priority_tag<2>{}))
+ {
+ return fn::impl(SCN_FWD(t), detail::priority_tag<2>{});
+ }
+ SCN_GCC_POP
+ };
+ } // namespace _end
+ namespace {
+ constexpr auto& end = detail::static_const<_end::fn>::value;
+ }
+
+ // cbegin
+ namespace _cbegin {
+ struct fn {
+ template <typename T>
+ constexpr auto operator()(const T& t) const
+ noexcept(noexcept(::scn::custom_ranges::begin(t)))
+ -> decltype(::scn::custom_ranges::begin(t))
+ {
+ return ::scn::custom_ranges::begin(t);
+ }
+
+ template <typename T>
+ constexpr auto operator()(const T&& t) const noexcept(noexcept(
+ ::scn::custom_ranges::begin(static_cast<const T&&>(t))))
+ -> decltype(::scn::custom_ranges::begin(
+ static_cast<const T&&>(t)))
+ {
+ return ::scn::custom_ranges::begin(
+ static_cast<const T&&>(t));
+ }
+ };
+ } // namespace _cbegin
+ namespace {
+ constexpr auto& cbegin = detail::static_const<_cbegin::fn>::value;
+ }
+
+ // cend
+ namespace _cend {
+ struct fn {
+ template <typename T>
+ constexpr auto operator()(const T& t) const
+ noexcept(noexcept(::scn::custom_ranges::end(t)))
+ -> decltype(::scn::custom_ranges::end(t))
+ {
+ return ::scn::custom_ranges::end(t);
+ }
+
+ template <typename T>
+ constexpr auto operator()(const T&& t) const noexcept(noexcept(
+ ::scn::custom_ranges::end(static_cast<const T&&>(t))))
+ -> decltype(::scn::custom_ranges::end(
+ static_cast<const T&&>(t)))
+ {
+ return ::scn::custom_ranges::end(static_cast<const T&&>(t));
+ }
+ };
+ } // namespace _cend
+ namespace {
+ constexpr auto& cend = detail::static_const<_cend::fn>::value;
+ }
+
+ // range
+ namespace detail {
+ struct range_impl_concept {
+ template <typename T>
+ auto _test_requires(T&& t)
+ -> decltype(::scn::custom_ranges::begin(SCN_FWD(t)),
+ ::scn::custom_ranges::end(SCN_FWD(t)));
+ };
+ template <typename T>
+ struct range_impl : _requires<range_impl_concept, T> {
+ };
+ struct range_concept {
+ template <typename>
+ static auto test(long) -> std::false_type;
+ template <typename T>
+ static auto test(int) ->
+ typename std::enable_if<range_impl<T&>::value,
+ std::true_type>::type;
+ };
+ } // namespace detail
+ template <typename T>
+ struct range : decltype(detail::range_concept::test<T>(0)) {
+ };
+
+ template <typename T>
+ struct forwarding_range
+ : std::integral_constant<bool,
+ range<T>::value &&
+ detail::range_impl<T>::value> {
+ };
+
+ // typedefs
+ template <typename R>
+ using iterator_t =
+ typename std::enable_if<range<R>::value,
+ decltype(::scn::custom_ranges::begin(
+ SCN_DECLVAL(R&)))>::type;
+ template <typename R>
+ using sentinel_t =
+ typename std::enable_if<range<R>::value,
+ decltype(::scn::custom_ranges::end(
+ SCN_DECLVAL(R&)))>::type;
+ template <typename R>
+ using range_difference_t =
+ typename std::enable_if<range<R>::value,
+ iter_difference_t<iterator_t<R>>>::type;
+ template <typename R>
+ using range_value_t =
+ typename std::enable_if<range<R>::value,
+ iter_value_t<iterator_t<R>>>::type;
+ template <typename R>
+ using range_reference_t =
+ typename std::enable_if<range<R>::value,
+ iter_reference_t<iterator_t<R>>>::type;
+
+ // view
+ struct view_base {
+ };
+
+ namespace detail {
+ template <typename>
+ struct is_std_non_view : std::false_type {
+ };
+ template <typename T>
+ struct is_std_non_view<std::initializer_list<T>> : std::true_type {
+ };
+ template <typename T>
+ struct enable_view_helper
+ : std::conditional<
+ std::is_base_of<view_base, T>::value,
+ std::true_type,
+ typename std::conditional<
+ is_std_non_view<T>::value,
+ std::false_type,
+ typename std::conditional<
+ range<T>::value && range<const T>::value,
+ std::is_same<range_reference_t<T>,
+ range_reference_t<const T>>,
+ std::true_type>::type>::type>::type {
+ };
+ template <typename T>
+ struct view_impl
+ : std::integral_constant<
+ bool,
+ std::is_copy_constructible<T>::value &&
+ std::is_default_constructible<T>::value &&
+ detail::enable_view_helper<T>::value> {
+ };
+ } // namespace detail
+ template <typename T>
+ struct view : std::conditional<range<T>::value,
+ detail::view_impl<T>,
+ std::false_type>::type {
+ };
+
+ // data
+ template <typename P>
+ struct _is_object_pointer
+ : std::integral_constant<
+ bool,
+ std::is_pointer<P>::value &&
+ std::is_object<detail::test_t<iter_value_t, P>>::value> {
+ };
+
+ namespace _data {
+ struct fn {
+ private:
+ template <typename CharT, typename Traits, typename Allocator>
+ static constexpr auto impl(
+ std::basic_string<CharT, Traits, Allocator>& str,
+ detail::priority_tag<2>) noexcept -> typename std::
+ basic_string<CharT, Traits, Allocator>::pointer
+ {
+ return std::addressof(*str.begin());
+ }
+ template <typename CharT, typename Traits, typename Allocator>
+ static constexpr auto impl(
+ const std::basic_string<CharT, Traits, Allocator>& str,
+ detail::priority_tag<2>) noexcept -> typename std::
+ basic_string<CharT, Traits, Allocator>::const_pointer
+ {
+ return std::addressof(*str.begin());
+ }
+ template <typename CharT, typename Traits, typename Allocator>
+ static constexpr auto impl(
+ std::basic_string<CharT, Traits, Allocator>&& str,
+ detail::priority_tag<2>) noexcept -> typename std::
+ basic_string<CharT, Traits, Allocator>::pointer
+ {
+ return std::addressof(*str.begin());
+ }
+
+ template <typename T,
+ typename D =
+ decltype(::scn::custom_ranges::detail::decay_copy(
+ SCN_DECLVAL(T&).data()))>
+ static constexpr auto
+ impl(T& t, detail::priority_tag<1>) noexcept(noexcept(
+ ::scn::custom_ranges::detail::decay_copy(t.data()))) ->
+ typename std::enable_if<_is_object_pointer<D>::value,
+ D>::type
+ {
+ return ::scn::custom_ranges::detail::decay_copy(t.data());
+ }
+
+ template <typename T>
+ static constexpr auto
+ impl(T&& t, detail::priority_tag<0>) noexcept(
+ noexcept(::scn::custom_ranges::begin(SCN_FWD(t)))) ->
+ typename std::enable_if<
+ _is_object_pointer<decltype(::scn::custom_ranges::begin(
+ SCN_FWD(t)))>::value,
+ decltype(::scn::custom_ranges::begin(SCN_FWD(t)))>::type
+ {
+ return ::scn::custom_ranges::begin(SCN_FWD(t));
+ }
+
+ public:
+ template <typename T>
+ constexpr auto operator()(T&& t) const noexcept(
+ noexcept(fn::impl(SCN_FWD(t), detail::priority_tag<2>{})))
+ -> decltype(fn::impl(SCN_FWD(t), detail::priority_tag<2>{}))
+ {
+ return fn::impl(SCN_FWD(t), detail::priority_tag<2>{});
+ }
+ };
+ } // namespace _data
+ namespace {
+ constexpr auto& data = detail::static_const<_data::fn>::value;
+ }
+
+ // size
+ template <typename>
+ struct disable_sized_range : std::false_type {
+ };
+
+ namespace _size {
+ template <typename T>
+ void size(T&&) = delete;
+ template <typename T>
+ void size(T&) = delete;
+
+ struct fn {
+ private:
+ template <typename T, std::size_t N>
+ static constexpr std::size_t impl(
+ const T(&&)[N],
+ detail::priority_tag<3>) noexcept
+ {
+ return N;
+ }
+
+ template <typename T, std::size_t N>
+ static constexpr std::size_t impl(
+ const T (&)[N],
+ detail::priority_tag<3>) noexcept
+ {
+ return N;
+ }
+
+ template <typename T,
+ typename I =
+ decltype(::scn::custom_ranges::detail::decay_copy(
+ SCN_DECLVAL(T).size()))>
+ static constexpr auto
+ impl(T&& t, detail::priority_tag<2>) noexcept(
+ noexcept(::scn::custom_ranges::detail::decay_copy(
+ SCN_FWD(t).size()))) ->
+ typename std::enable_if<
+ std::is_integral<I>::value &&
+ !disable_sized_range<
+ detail::remove_cvref_t<T>>::value,
+ I>::type
+ {
+ return ::scn::custom_ranges::detail::decay_copy(
+ SCN_FWD(t).size());
+ }
+
+ template <typename T,
+ typename I =
+ decltype(::scn::custom_ranges::detail::decay_copy(
+ size(SCN_DECLVAL(T))))>
+ static constexpr auto
+ impl(T&& t, detail::priority_tag<1>) noexcept(noexcept(
+ ::scn::custom_ranges::detail::decay_copy(size(SCN_FWD(t)))))
+ -> typename std::enable_if<
+ std::is_integral<I>::value &&
+ !disable_sized_range<
+ detail::remove_cvref_t<T>>::value,
+ I>::type
+ {
+ return ::scn::custom_ranges::detail::decay_copy(
+ size(SCN_FWD(t)));
+ }
+
+ template <typename T,
+ typename I = decltype(::scn::custom_ranges::begin(
+ SCN_DECLVAL(T))),
+ typename S = decltype(::scn::custom_ranges::end(
+ SCN_DECLVAL(T))),
+ typename D =
+ decltype(::scn::custom_ranges::detail::decay_copy(
+ SCN_DECLVAL(S) - SCN_DECLVAL(I)))>
+ static constexpr auto
+ impl(T&& t, detail::priority_tag<0>) noexcept(
+ noexcept(::scn::custom_ranges::detail::decay_copy(
+ ::scn::custom_ranges::end(t) -
+ ::scn::custom_ranges::begin(t)))) ->
+ typename std::enable_if<
+ !std::is_array<detail::remove_cvref_t<T>>::value,
+ D>::type
+ {
+ return ::scn::custom_ranges::detail::decay_copy(
+ ::scn::custom_ranges::end(t) -
+ ::scn::custom_ranges::begin(t));
+ }
+
+ public:
+ template <typename T>
+ constexpr auto operator()(T&& t) const noexcept(
+ noexcept(fn::impl(SCN_FWD(t), detail::priority_tag<3>{})))
+ -> decltype(fn::impl(SCN_FWD(t), detail::priority_tag<3>{}))
+ {
+ return fn::impl(SCN_FWD(t), detail::priority_tag<3>{});
+ }
+ };
+ } // namespace _size
+ namespace {
+ constexpr auto& size = detail::static_const<_size::fn>::value;
+ }
+
+ // empty
+ namespace _empty_ns {
+ struct fn {
+ private:
+ template <typename T>
+ static constexpr auto
+ impl(T&& t, detail::priority_tag<2>) noexcept(
+ noexcept((bool(SCN_FWD(t).empty()))))
+ -> decltype((bool(SCN_FWD(t).empty())))
+ {
+ return bool((SCN_FWD(t).empty()));
+ }
+ template <typename T>
+ static constexpr auto
+ impl(T&& t, detail::priority_tag<1>) noexcept(
+ noexcept(::scn::custom_ranges::size(SCN_FWD(t)) == 0))
+ -> decltype(::scn::custom_ranges::size(SCN_FWD(t)) == 0)
+ {
+ return ::scn::custom_ranges::size(SCN_FWD(t)) == 0;
+ }
+
+ template <typename T,
+ typename I = decltype(::scn::custom_ranges::begin(
+ SCN_DECLVAL(T)))>
+ static constexpr auto
+ impl(T&& t, detail::priority_tag<0>) noexcept(
+ noexcept(::scn::custom_ranges::begin(t) ==
+ ::scn::custom_ranges::end(t)))
+ -> decltype(::scn::custom_ranges::begin(t) ==
+ ::scn::custom_ranges::end(t))
+ {
+ return ::scn::custom_ranges::begin(t) ==
+ ::scn::custom_ranges::end(t);
+ }
+
+ public:
+ template <typename T>
+ constexpr auto operator()(T&& t) const noexcept(
+ noexcept(fn::impl(SCN_FWD(t), detail::priority_tag<2>{})))
+ -> decltype(fn::impl(SCN_FWD(t), detail::priority_tag<2>{}))
+ {
+ return fn::impl(SCN_FWD(t), detail::priority_tag<2>{});
+ }
+ };
+ } // namespace _empty_ns
+ namespace {
+ constexpr auto& empty = detail::static_const<_empty_ns::fn>::value;
+ }
+
+ // sized_range
+ namespace detail {
+ struct sized_range_concept {
+ template <typename T>
+ auto _test_requires(T& t)
+ -> decltype(::scn::custom_ranges::size(t));
+ };
+ } // namespace detail
+ template <typename T>
+ struct sized_range
+ : std::integral_constant<
+ bool,
+ range<T>::value &&
+ !disable_sized_range<detail::remove_cvref_t<T>>::value &&
+ detail::_requires<detail::sized_range_concept,
+ T>::value> {
+ };
+
+ // contiguous_range
+ namespace detail {
+ struct contiguous_range_concept {
+ template <typename>
+ static auto test(long) -> std::false_type;
+ template <typename T>
+ static auto test(int) -> typename std::enable_if<
+ _requires<contiguous_range_concept, T>::value,
+ std::true_type>::type;
+
+ template <typename T>
+ auto _test_requires(T& t)
+ -> decltype(requires_expr<std::is_same<
+ decltype(::scn::custom_ranges::data(t)),
+ typename std::add_pointer<
+ range_reference_t<T>>::type>::value>{});
+ };
+ } // namespace detail
+ template <typename T>
+ struct contiguous_range
+ : decltype(detail::contiguous_range_concept::test<T>(0)) {
+ };
+
+ // subrange
+ template <typename D>
+ class view_interface : public view_base {
+ static_assert(std::is_class<D>::value, "");
+ static_assert(
+ std::is_same<D, typename std::remove_cv<D>::type>::value,
+ "");
+
+ private:
+ SCN_CONSTEXPR14 D& derived() noexcept
+ {
+ return static_cast<D&>(*this);
+ }
+ constexpr D& derived() const noexcept
+ {
+ return static_cast<const D&>(*this);
+ }
+
+ public:
+ SCN_NODISCARD SCN_CONSTEXPR14 bool empty()
+ {
+ return ::scn::custom_ranges::begin(derived()) ==
+ ::scn::custom_ranges::end(derived());
+ }
+ SCN_NODISCARD constexpr bool empty() const
+ {
+ return ::scn::custom_ranges::begin(derived()) ==
+ ::scn::custom_ranges::end(derived());
+ }
+
+ template <typename R = D,
+ typename = decltype(::scn::custom_ranges::empty(
+ SCN_DECLVAL(R&)))>
+ SCN_CONSTEXPR14 explicit operator bool()
+ {
+ return !::scn::custom_ranges::empty(derived());
+ }
+ template <typename R = D,
+ typename = decltype(::scn::custom_ranges::empty(
+ SCN_DECLVAL(const R&)))>
+ constexpr explicit operator bool() const
+ {
+ return !::scn::custom_ranges::empty(derived());
+ }
+
+ template <typename R = D,
+ typename std::enable_if<
+ contiguous_range<R>::value>::type* = nullptr>
+ auto data() -> decltype(std::addressof(
+ *::scn::custom_ranges::begin(static_cast<R&>(*this))))
+ {
+ return ::scn::custom_ranges::empty(derived())
+ ? nullptr
+ : std::addressof(
+ *::scn::custom_ranges::begin(derived()));
+ }
+ template <typename R = D,
+ typename std::enable_if<
+ contiguous_range<const R>::value>::type* = nullptr>
+ auto data() const -> decltype(std::addressof(
+ *::scn::custom_ranges::begin(static_cast<const R&>(*this))))
+ {
+ return ::scn::custom_ranges::empty(derived())
+ ? nullptr
+ : std::addressof(
+ *::scn::custom_ranges::begin(derived()));
+ }
+
+ template <typename R = D,
+ typename std::enable_if<
+ range<R>::value &&
+ sized_sentinel_for<sentinel_t<R>, iterator_t<R>>::
+ value>::type* = nullptr>
+ SCN_CONSTEXPR14 auto size()
+ -> decltype(::scn::custom_ranges::end(static_cast<R&>(*this)) -
+ ::scn::custom_ranges::begin(static_cast<R&>(*this)))
+ {
+ return ::scn::custom_ranges::end(derived()) -
+ ::scn::custom_ranges::begin(derived());
+ }
+
+ template <
+ typename R = D,
+ typename std::enable_if<
+ range<const R>::value &&
+ sized_sentinel_for<sentinel_t<const R>,
+ iterator_t<const R>>::value>::type* =
+ nullptr>
+ constexpr auto size() const
+ -> decltype(::scn::custom_ranges::end(
+ static_cast<const R&>(*this)) -
+ ::scn::custom_ranges::begin(
+ static_cast<const R&>(*this)))
+ {
+ return ::scn::custom_ranges::end(derived()) -
+ ::scn::custom_ranges::begin(derived());
+ }
+ };
+
+ enum class subrange_kind : bool { unsized, sized };
+
+ namespace detail {
+ template <typename I, typename S>
+ struct default_subrange_kind
+ : std::integral_constant<subrange_kind,
+ sized_sentinel_for<S, I>::value
+ ? subrange_kind::sized
+ : subrange_kind::unsized> {
+ };
+ } // namespace detail
+
+ namespace _subrange {
+ template <typename I,
+ typename S = I,
+ subrange_kind = scn::custom_ranges::detail::
+ default_subrange_kind<I, S>::value>
+ class subrange;
+ } // namespace _subrange
+
+ using _subrange::subrange;
+
+ namespace detail {
+ struct pair_like_concept {
+ template <typename>
+ static auto test(long) -> std::false_type;
+ template <typename T,
+ typename = typename std::tuple_size<T>::type>
+ static auto test(int) -> typename std::enable_if<
+ _requires<pair_like_concept, T>::value,
+ std::true_type>::type;
+
+ template <typename T>
+ auto _test_requires(T t) -> decltype(
+ requires_expr<
+ std::is_base_of<std::integral_constant<std::size_t, 2>,
+ std::tuple_size<T>>::value>{},
+ std::declval<std::tuple_element<
+ 0,
+ typename std::remove_const<T>::type>>(),
+ std::declval<std::tuple_element<
+ 1,
+ typename std::remove_const<T>::type>>(),
+ requires_expr<std::is_convertible<
+ decltype(std::get<0>(t)),
+ const std::tuple_element<0, T>&>::value>{},
+ requires_expr<std::is_convertible<
+ decltype(std::get<1>(t)),
+ const std::tuple_element<1, T>&>::value>{});
+ };
+ template <typename T>
+ struct pair_like
+ : std::integral_constant<
+ bool,
+ !std::is_reference<T>::value &&
+ decltype(pair_like_concept::test<T>(0))::value> {
+ };
+
+ struct pair_like_convertible_to_concept {
+ template <typename T, typename U, typename V>
+ auto _test_requires(T&& t) -> decltype(
+ requires_expr<
+ std::is_convertible<decltype(std::get<0>(SCN_FWD(t))),
+ U>::value>{},
+ requires_expr<
+ std::is_convertible<decltype(std::get<1>(SCN_FWD(t))),
+ V>::value>{});
+ };
+ template <typename T, typename U, typename V>
+ struct pair_like_convertible_to
+ : std::integral_constant<
+ bool,
+ !range<T>::value &&
+ pair_like<
+ typename std::remove_reference<T>::type>::value &&
+ _requires<pair_like_convertible_to_concept, T, U, V>::
+ value> {
+ };
+ template <typename T, typename U, typename V>
+ struct pair_like_convertible_from
+ : std::integral_constant<
+ bool,
+ !range<T>::value &&
+ pair_like<
+ typename std::remove_reference<T>::type>::value &&
+ std::is_constructible<T, U, V>::value> {
+ };
+
+ struct iterator_sentinel_pair_concept {
+ template <typename>
+ static auto test(long) -> std::false_type;
+ template <typename T>
+ static auto test(int) -> typename std::enable_if<
+ !range<T>::value && pair_like<T>::value &&
+ sentinel_for<
+ typename std::tuple_element<1, T>::type,
+ typename std::tuple_element<0, T>::type>::value,
+ std::true_type>::type;
+ };
+ template <typename T>
+ struct iterator_sentinel_pair
+ : decltype(iterator_sentinel_pair_concept::test<T>(0)) {
+ };
+
+ template <typename I, typename S, bool StoreSize = false>
+ struct subrange_data {
+ constexpr subrange_data() = default;
+ constexpr subrange_data(I&& b, S&& e)
+ : begin(SCN_MOVE(b)), end(SCN_MOVE(e))
+ {
+ }
+ template <bool Dependent = true>
+ constexpr subrange_data(
+ I&& b,
+ S&& e,
+ typename std::enable_if<Dependent,
+ iter_difference_t<I>>::type)
+ : begin(SCN_MOVE(b)), end(SCN_MOVE(e))
+ {
+ }
+
+ constexpr iter_difference_t<I> get_size() const
+ {
+ return distance(begin, end);
+ }
+
+ I begin{};
+ S end{};
+ };
+
+ template <typename I, typename S>
+ struct subrange_data<I, S, true> {
+ constexpr subrange_data() = default;
+ constexpr subrange_data(I&& b, S&& e, iter_difference_t<I> s)
+ : begin(SCN_MOVE(b)), end(SCN_MOVE(e)), size(s)
+ {
+ }
+
+ constexpr iter_difference_t<I> get_size() const
+ {
+ return size;
+ }
+
+ I begin{};
+ S end{};
+ iter_difference_t<I> size{0};
+ };
+
+ template <typename R, typename I, typename S, subrange_kind K>
+ auto subrange_range_constructor_constraint_helper_fn(long)
+ -> std::false_type;
+
+ template <typename R, typename I, typename S, subrange_kind K>
+ auto subrange_range_constructor_constraint_helper_fn(int) ->
+ typename std::enable_if<
+ forwarding_range<R>::value &&
+ std::is_convertible<iterator_t<R>, I>::value &&
+ std::is_convertible<sentinel_t<R>, S>::value,
+ std::true_type>::type;
+
+ template <typename R, typename I, typename S, subrange_kind K>
+ struct subrange_range_constructor_constraint_helper
+ : decltype(subrange_range_constructor_constraint_helper_fn<R,
+ I,
+ S,
+ K>(
+ 0)) {
+ };
+
+ template <typename R>
+ constexpr subrange_kind subrange_deduction_guide_helper()
+ {
+ return (sized_range<R>::value ||
+ sized_sentinel_for<sentinel_t<R>, iterator_t<R>>::value)
+ ? subrange_kind::sized
+ : subrange_kind::unsized;
+ }
+
+ template <typename T, typename U>
+ struct not_same_as : std::integral_constant<
+ bool,
+ !std::is_same<remove_cvref_t<T>,
+ remove_cvref_t<U>>::value> {
+ };
+ } // namespace detail
+
+ namespace _subrange {
+ template <typename I, typename S, subrange_kind K>
+ class subrange : public view_interface<subrange<I, S, K>> {
+ static_assert(sentinel_for<S, I>::value, "");
+ static_assert(K == subrange_kind::sized ||
+ !sized_sentinel_for<S, I>::value,
+ "");
+
+ static constexpr bool _store_size =
+ K == subrange_kind::sized &&
+ !sized_sentinel_for<S, I>::value;
+
+ public:
+ using iterator = I;
+ using sentinel = S;
+
+ subrange() = default;
+
+ template <bool SS = _store_size,
+ typename std::enable_if<!SS>::type* = nullptr>
+ SCN_CONSTEXPR14 subrange(I i, S s)
+ : m_data{SCN_MOVE(i), SCN_MOVE(s)}
+ {
+ }
+ template <bool Dependent = true,
+ subrange_kind KK = K,
+ typename std::enable_if<
+ KK == subrange_kind::sized>::type* = nullptr>
+ SCN_CONSTEXPR14 subrange(
+ I i,
+ S s,
+ typename std::enable_if<Dependent,
+ iter_difference_t<I>>::type n)
+ : m_data{SCN_MOVE(i), SCN_MOVE(s), n}
+ {
+ }
+
+ constexpr I begin() const noexcept
+ {
+ return m_data.begin;
+ }
+
+ constexpr S end() const noexcept
+ {
+ return m_data.end;
+ }
+
+ SCN_NODISCARD constexpr bool empty() const noexcept
+ {
+ return m_data.begin == m_data.end;
+ }
+
+ template <subrange_kind KK = K,
+ typename std::enable_if<
+ KK == subrange_kind::sized>::type* = nullptr>
+ constexpr iter_difference_t<I> size() const noexcept
+ {
+ return m_data.get_size();
+ }
+
+ private:
+ detail::subrange_data<I, S, _store_size> m_data{};
+ };
+
+ template <typename I, typename S, subrange_kind K>
+ I begin(subrange<I, S, K>&& r) noexcept
+ {
+ return r.begin();
+ }
+ template <typename I, typename S, subrange_kind K>
+ S end(subrange<I, S, K>&& r) noexcept
+ {
+ return r.end();
+ }
+ } // namespace _subrange
+
+ namespace detail {
+ template <std::size_t N>
+ struct subrange_get_impl;
+ template <>
+ struct subrange_get_impl<0> {
+ template <typename I, typename S, subrange_kind K>
+ static auto get(const subrange<I, S, K>& s)
+ -> decltype(s.begin())
+ {
+ return s.begin();
+ }
+ };
+ template <>
+ struct subrange_get_impl<1> {
+ template <typename I, typename S, subrange_kind K>
+ static auto get(const subrange<I, S, K>& s) -> decltype(s.end())
+ {
+ return s.end();
+ }
+ };
+ } // namespace detail
+
+ template <std::size_t N,
+ typename I,
+ typename S,
+ subrange_kind K,
+ typename std::enable_if<(N < 2)>::type* = nullptr>
+ auto get(const subrange<I, S, K>& s)
+ -> decltype(detail::subrange_get_impl<N>::get(s))
+ {
+ return detail::subrange_get_impl<N>::get(s);
+ }
+
+ // reconstructible_range
+ template <typename R>
+ struct pair_reconstructible_range
+ : std::integral_constant<
+ bool,
+ range<R>::value &&
+ forwarding_range<
+ typename std::remove_reference<R>::type>::value &&
+ std::is_constructible<R, iterator_t<R>, sentinel_t<R>>::
+ value> {
+ };
+ template <typename R>
+ struct reconstructible_range
+ : std::integral_constant<
+ bool,
+ range<R>::value &&
+ forwarding_range<
+ typename std::remove_reference<R>::type>::value &&
+ std::is_constructible<
+ R,
+ subrange<iterator_t<R>, sentinel_t<R>>>::value> {
+ };
+ } // namespace custom_ranges
+
+ namespace polyfill_2a {
+ // bidir iterator
+ namespace detail {
+ struct bidirectional_iterator_concept {
+ template <typename I>
+ auto _test_requires(I i)
+ -> decltype(custom_ranges::detail::requires_expr<
+ std::is_same<decltype(i--), I>::value>{});
+ template <typename>
+ static auto test(long) -> std::false_type;
+ template <typename I>
+ static auto test(int) -> typename std::enable_if<
+ std::is_base_of<
+ custom_ranges::bidirectional_iterator_tag,
+ custom_ranges::iterator_category_t<I>>::value &&
+ custom_ranges::detail::
+ _requires<bidirectional_iterator_concept, I>::value,
+ std::true_type>::type;
+ };
+ } // namespace detail
+ template <typename I>
+ struct bidirectional_iterator
+ : decltype(detail::bidirectional_iterator_concept::test<I>(0)) {
+ };
+
+ // random access iterator
+ namespace detail {
+ struct random_access_iterator_concept {
+ template <typename I>
+ auto _test_requires(I i,
+ const I j,
+ const custom_ranges::iter_difference_t<I> n)
+ -> decltype(valid_expr(
+ j + n,
+ custom_ranges::detail::requires_expr<
+ std::is_same<decltype(j + n), I>::value>{},
+ n + j,
+#ifndef _MSC_VER
+ custom_ranges::detail::requires_expr<
+ std::is_same<decltype(n + j), I>::value>{},
+#endif
+ j - n,
+ custom_ranges::detail::requires_expr<
+ std::is_same<decltype(j - n), I>::value>{},
+ j[n],
+ custom_ranges::detail::requires_expr<std::is_same<
+ decltype(j[n]),
+ custom_ranges::iter_reference_t<I>>::value>{},
+ custom_ranges::detail::requires_expr<
+ std::is_convertible<decltype(i < j),
+ bool>::value>{}));
+ template <typename>
+ static auto test(long) -> std::false_type;
+ template <typename I>
+ static auto test(int) -> typename std::enable_if<
+ bidirectional_iterator<I>::value &&
+ std::is_base_of<
+ custom_ranges::random_access_iterator_tag,
+ custom_ranges::iterator_category_t<I>>::value &&
+ custom_ranges::sized_sentinel_for<I, I>::value &&
+ custom_ranges::detail::
+ _requires<random_access_iterator_concept, I>::value,
+ std::true_type>::type;
+ };
+ } // namespace detail
+ template <typename I>
+ struct random_access_iterator
+ : decltype(detail::random_access_iterator_concept::test<I>(0)) {
+ };
+ } // namespace polyfill_2a
+
+ namespace custom_ranges {
+ // advance
+ namespace _advance {
+ struct fn {
+ private:
+ template <typename T>
+ static constexpr T abs(T t)
+ {
+ return t < T{0} ? -t : t;
+ }
+
+ template <
+ typename R,
+ typename std::enable_if<polyfill_2a::random_access_iterator<
+ R>::value>::type* = nullptr>
+ static SCN_CONSTEXPR14 void impl(R& r, iter_difference_t<R> n)
+ {
+ r += n;
+ }
+
+ template <
+ typename I,
+ typename std::enable_if<
+ polyfill_2a::bidirectional_iterator<I>::value &&
+ !polyfill_2a::random_access_iterator<I>::value>::type* =
+ nullptr>
+ static SCN_CONSTEXPR14 void impl(I& i, iter_difference_t<I> n)
+ {
+ constexpr auto zero = iter_difference_t<I>{0};
+
+ if (n > zero) {
+ while (n-- > zero) {
+ ++i;
+ }
+ }
+ else {
+ while (n++ < zero) {
+ --i;
+ }
+ }
+ }
+
+ template <
+ typename I,
+ typename std::enable_if<
+ !polyfill_2a::bidirectional_iterator<I>::value>::type* =
+ nullptr>
+ static SCN_CONSTEXPR14 void impl(I& i, iter_difference_t<I> n)
+ {
+ while (n-- > iter_difference_t<I>{0}) {
+ ++i;
+ }
+ }
+
+ template <
+ typename I,
+ typename S,
+ typename std::enable_if<
+ std::is_assignable<I&, S>::value>::type* = nullptr>
+ static SCN_CONSTEXPR14 void impl(I& i,
+ S bound,
+ detail::priority_tag<2>)
+ {
+ i = SCN_MOVE(bound);
+ }
+
+ template <typename I,
+ typename S,
+ typename std::enable_if<
+ sized_sentinel_for<S, I>::value>::type* = nullptr>
+ static SCN_CONSTEXPR14 void impl(I& i,
+ S bound,
+ detail::priority_tag<1>)
+ {
+ fn::impl(i, bound - i);
+ }
+
+ template <typename I, typename S>
+ static SCN_CONSTEXPR14 void impl(I& i,
+ S bound,
+ detail::priority_tag<0>)
+ {
+ while (i != bound) {
+ ++i;
+ }
+ }
+
+ template <typename I,
+ typename S,
+ typename std::enable_if<
+ sized_sentinel_for<S, I>::value>::type* = nullptr>
+ static SCN_CONSTEXPR14 auto impl(I& i,
+ iter_difference_t<I> n,
+ S bound)
+ -> iter_difference_t<I>
+ {
+ if (fn::abs(n) >= fn::abs(bound - i)) {
+ auto dist = bound - i;
+ fn::impl(i, bound, detail::priority_tag<2>{});
+ return dist;
+ }
+ else {
+ fn::impl(i, n);
+ return n;
+ }
+ }
+
+ template <
+ typename I,
+ typename S,
+ typename std::enable_if<
+ polyfill_2a::bidirectional_iterator<I>::value &&
+ !sized_sentinel_for<S, I>::value>::type* = nullptr>
+ static SCN_CONSTEXPR14 auto impl(I& i,
+ iter_difference_t<I> n,
+ S bound)
+ -> iter_difference_t<I>
+ {
+ constexpr iter_difference_t<I> zero{0};
+ iter_difference_t<I> counter{0};
+
+ if (n < zero) {
+ do {
+ --i;
+ --counter;
+ } while (++n < zero && i != bound);
+ }
+ else {
+ while (n-- > zero && i != bound) {
+ ++i;
+ ++counter;
+ }
+ }
+
+ return counter;
+ }
+
+ template <
+ typename I,
+ typename S,
+ typename std::enable_if<
+ !polyfill_2a::bidirectional_iterator<I>::value &&
+ !sized_sentinel_for<S, I>::value>::type* = nullptr>
+ static SCN_CONSTEXPR14 auto impl(I& i,
+ iter_difference_t<I> n,
+ S bound)
+ -> iter_difference_t<I>
+ {
+ constexpr iter_difference_t<I> zero{0};
+ iter_difference_t<I> counter{0};
+
+ while (n-- > zero && i != bound) {
+ ++i;
+ ++counter;
+ }
+
+ return counter;
+ }
+
+ public:
+ template <typename I>
+ SCN_CONSTEXPR14 void operator()(I& i,
+ iter_difference_t<I> n) const
+ {
+ fn::impl(i, n);
+ }
+
+ template <typename I,
+ typename S,
+ typename std::enable_if<
+ sentinel_for<S, I>::value>::type* = nullptr>
+ SCN_CONSTEXPR14 void operator()(I& i, S bound) const
+ {
+ fn::impl(i, bound, detail::priority_tag<2>{});
+ }
+
+ template <typename I,
+ typename S,
+ typename std::enable_if<
+ sentinel_for<S, I>::value>::type* = nullptr>
+ SCN_CONSTEXPR14 iter_difference_t<I>
+ operator()(I& i, iter_difference_t<I> n, S bound) const
+ {
+ return n - fn::impl(i, n, bound);
+ }
+ };
+ } // namespace _advance
+ namespace {
+ constexpr auto& advance = detail::static_const<_advance::fn>::value;
+ }
+
+ // distance
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wnoexcept")
+ namespace _distance {
+ struct fn {
+ private:
+ template <typename I, typename S>
+ static SCN_CONSTEXPR14 auto impl(I i,
+ S s) noexcept(noexcept(s - i))
+ -> typename std::enable_if<sized_sentinel_for<S, I>::value,
+ iter_difference_t<I>>::type
+ {
+ return s - i;
+ }
+
+ template <typename I, typename S>
+ static SCN_CONSTEXPR14 auto impl(I i, S s) noexcept(
+ noexcept(i != s, ++i, ++SCN_DECLVAL(iter_difference_t<I>&)))
+ -> typename std::enable_if<!sized_sentinel_for<S, I>::value,
+ iter_difference_t<I>>::type
+ {
+ iter_difference_t<I> counter{0};
+ while (i != s) {
+ ++i;
+ ++counter;
+ }
+ return counter;
+ }
+
+ template <typename R>
+ static SCN_CONSTEXPR14 auto impl(R&& r) noexcept(
+ noexcept(::scn::custom_ranges::size(r))) ->
+ typename std::enable_if<
+ sized_range<R>::value,
+ iter_difference_t<iterator_t<R>>>::type
+ {
+ return static_cast<iter_difference_t<iterator_t<R>>>(
+ ::scn::custom_ranges::size(r));
+ }
+
+ template <typename R>
+ static SCN_CONSTEXPR14 auto impl(R&& r) noexcept(
+ noexcept(fn::impl(::scn::custom_ranges::begin(r),
+ ::scn::custom_ranges::end(r)))) ->
+ typename std::enable_if<
+ !sized_range<R>::value,
+ iter_difference_t<iterator_t<R>>>::type
+ {
+ return fn::impl(::scn::custom_ranges::begin(r),
+ ::scn::custom_ranges::end(r));
+ }
+
+ public:
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 auto operator()(I first, S last) const
+ noexcept(noexcept(fn::impl(SCN_MOVE(first),
+ SCN_MOVE(last)))) ->
+ typename std::enable_if<sentinel_for<S, I>::value,
+ iter_difference_t<I>>::type
+ {
+ return fn::impl(SCN_MOVE(first), SCN_MOVE(last));
+ }
+
+ template <typename R>
+ SCN_CONSTEXPR14 auto operator()(R&& r) const
+ noexcept(noexcept(fn::impl(SCN_FWD(r)))) ->
+ typename std::enable_if<
+ range<R>::value,
+ iter_difference_t<iterator_t<R>>>::type
+ {
+ return fn::impl(SCN_FWD(r));
+ }
+ };
+ } // namespace _distance
+ namespace {
+ constexpr auto& distance =
+ detail::static_const<_distance::fn>::value;
+ }
+ SCN_GCC_POP // -Wnoexcept
+ } // namespace custom_ranges
+
+ namespace polyfill_2a {
+ template <typename T>
+ using iter_value_t = ::scn::custom_ranges::iter_value_t<T>;
+ template <typename T>
+ using iter_reference_t = ::scn::custom_ranges::iter_reference_t<T>;
+ template <typename T>
+ using iter_difference_t = ::scn::custom_ranges::iter_difference_t<T>;
+ } // namespace polyfill_2a
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+namespace std {
+ template <typename I, typename S, ::scn::custom_ranges::subrange_kind K>
+ struct tuple_size<::scn::custom_ranges::subrange<I, S, K>>
+ : public integral_constant<size_t, 2> {
+ };
+
+ template <typename I, typename S, ::scn::custom_ranges::subrange_kind K>
+ struct tuple_element<0, ::scn::custom_ranges::subrange<I, S, K>> {
+ using type = I;
+ };
+ template <typename I, typename S, ::scn::custom_ranges::subrange_kind K>
+ struct tuple_element<1, ::scn::custom_ranges::subrange<I, S, K>> {
+ using type = S;
+ };
+
+ using ::scn::custom_ranges::get;
+} // namespace std
+
+#define SCN_CHECK_CONCEPT(C) C::value
+
+#endif // SCN_RANGES_CUSTOM_IMPL_H
diff --git a/src/third-party/scnlib/include/scn/ranges/ranges.h b/src/third-party/scnlib/include/scn/ranges/ranges.h
new file mode 100644
index 0000000..aa70f50
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/ranges/ranges.h
@@ -0,0 +1,49 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_RANGES_RANGES_H
+#define SCN_RANGES_RANGES_H
+
+#include "../detail/config.h"
+
+#ifndef SCN_USE_STD_RANGES
+
+#if SCN_HAS_CONCEPTS && SCN_HAS_RANGES
+#define SCN_USE_STD_RANGES 1
+#else
+#define SCN_USE_STD_RANGES 0
+#endif
+
+#endif // !defined(SCN_USE_STD_RANGES)
+
+#if SCN_USE_STD_RANGES
+#include "std_impl.h"
+#define SCN_RANGES_NAMESPACE ::scn::std_ranges
+#else
+#include "custom_impl.h"
+#define SCN_RANGES_NAMESPACE ::scn::custom_ranges
+#endif
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace ranges = SCN_RANGES_NAMESPACE;
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_RANGES_RANGES_H
diff --git a/src/third-party/scnlib/include/scn/ranges/std_impl.h b/src/third-party/scnlib/include/scn/ranges/std_impl.h
new file mode 100644
index 0000000..abc3422
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/ranges/std_impl.h
@@ -0,0 +1,67 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_RANGES_STD_IMPL_H
+#define SCN_RANGES_STD_IMPL_H
+
+#include "../detail/config.h"
+
+#if SCN_HAS_CONCEPTS && SCN_HAS_RANGES
+
+SCN_GCC_PUSH
+SCN_GCC_IGNORE("-Wnoexcept")
+#include <iterator>
+#include <ranges>
+SCN_GCC_POP
+
+#include "util.h"
+
+#include "../util/string_view.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace std_ranges = ::std::ranges;
+
+ namespace polyfill_2a {
+ template <typename T>
+ using iter_value_t = ::std::iter_value_t<T>;
+ template <typename T>
+ using iter_reference_t = ::std::iter_reference_t<T>;
+ template <typename T>
+ using iter_difference_t = ::std::iter_difference_t<T>;
+
+ template <typename I>
+ concept bidirectional_iterator = std::bidirectional_iterator<I>;
+ template <typename I>
+ concept random_access_iterator = std::random_access_iterator<I>;
+ } // namespace polyfill_2a
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+namespace std::ranges {
+ template <typename CharT>
+ inline constexpr bool enable_view<::scn::basic_string_view<CharT>> = true;
+ template <typename T>
+ inline constexpr bool enable_view<::scn::span<T>> = true;
+} // namespace std
+
+#define SCN_CHECK_CONCEPT(C) C
+#endif
+
+#endif // SCN_RANGES_STD_IMPL_H
diff --git a/src/third-party/scnlib/include/scn/ranges/util.h b/src/third-party/scnlib/include/scn/ranges/util.h
new file mode 100644
index 0000000..d5954d1
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/ranges/util.h
@@ -0,0 +1,419 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+//
+// The contents of this file are adapted from NanoRange.
+// https://github.com/tcbrindle/NanoRange
+// Copyright (c) 2018 Tristan Brindle
+// Distributed under the Boost Software License, Version 1.0
+
+#ifndef SCN_RANGES_UTIL_H
+#define SCN_RANGES_UTIL_H
+
+#include "../util/meta.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace custom_ranges {
+ namespace detail {
+ template <size_t N>
+ using priority_tag = ::scn::detail::priority_tag<N>;
+
+ template <typename... Ts>
+ using void_t = ::scn::detail::void_t<Ts...>;
+
+ template <typename T>
+ using static_const = ::scn::detail::static_const<T>;
+
+ template <typename T>
+ using remove_cvref_t = ::scn::detail::remove_cvref_t<T>;
+
+ template <typename T>
+ constexpr typename std::decay<T>::type decay_copy(T&& t) noexcept(
+ noexcept(static_cast<typename std::decay<T>::type>(SCN_FWD(t))))
+ {
+ return SCN_FWD(t);
+ }
+
+ struct nonesuch {
+ nonesuch() = delete;
+ nonesuch(nonesuch const&) = delete;
+ nonesuch& operator=(const nonesuch&) = delete;
+ ~nonesuch() = delete;
+ };
+
+ template <typename Void,
+ template <class...>
+ class Trait,
+ typename... Args>
+ struct test {
+ using type = nonesuch;
+ };
+
+ template <template <class...> class Trait, typename... Args>
+ struct test<void_t<Trait<Args...>>, Trait, Args...> {
+ using type = Trait<Args...>;
+ };
+
+ template <template <class...> class Trait, typename... Args>
+ using test_t = typename test<void, Trait, Args...>::type;
+
+ template <typename Void,
+ template <class...>
+ class AliasT,
+ typename... Args>
+ struct exists_helper : std::false_type {
+ };
+
+ template <template <class...> class AliasT, typename... Args>
+ struct exists_helper<void_t<AliasT<Args...>>, AliasT, Args...>
+ : std::true_type {
+ };
+
+ template <template <class...> class AliasT, typename... Args>
+ struct exists : exists_helper<void, AliasT, Args...> {
+ };
+
+ template <typename R,
+ typename... Args,
+ typename = decltype(&R::template _test_requires<Args...>)>
+ auto test_requires(R&) -> void;
+
+ template <typename R, typename... Args>
+ using test_requires_t =
+ decltype(test_requires<R, Args...>(SCN_DECLVAL(R&)));
+
+ template <typename R, typename... Args>
+ struct _requires : exists<test_requires_t, R, Args...> {
+ };
+
+ template <bool Expr>
+ using requires_expr = typename std::enable_if<Expr, int>::type;
+
+ template <typename...>
+ struct get_common_type;
+
+ template <typename T, typename U>
+ struct copy_cv {
+ using type = U;
+ };
+ template <typename T, typename U>
+ struct copy_cv<const T, U> {
+ using type = typename std::add_const<U>::type;
+ };
+ template <typename T, typename U>
+ struct copy_cv<volatile T, U> {
+ using type = typename std::add_volatile<U>::type;
+ };
+ template <typename T, typename U>
+ struct copy_cv<const volatile T, U> {
+ using type = typename std::add_cv<U>::type;
+ };
+ template <typename T, typename U>
+ using copy_cv_t = typename copy_cv<T, U>::type;
+
+ template <typename T>
+ using cref_t = typename std::add_lvalue_reference<
+ const typename std::remove_reference<T>::type>::type;
+
+ template <typename T>
+ struct rref_res {
+ using type = T;
+ };
+ template <typename T>
+ struct rref_res<T&> {
+ using type = typename std::remove_reference<T>::type&&;
+ };
+ template <typename T>
+ using rref_res_t = typename rref_res<T>::type;
+
+ template <typename T, typename U>
+ using cond_res_t =
+ decltype(SCN_DECLVAL(bool) ? std::declval<T (&)()>()()
+ : std::declval<U (&)()>()());
+
+ template <typename T, typename U>
+ struct simple_common_reference {
+ };
+
+ template <
+ typename T,
+ typename U,
+ typename C =
+ test_t<cond_res_t, copy_cv_t<T, U>&, copy_cv_t<U, T>&>>
+ struct lvalue_simple_common_reference
+ : std::enable_if<std::is_reference<C>::value, C> {
+ };
+ template <typename T, typename U>
+ using lvalue_scr_t =
+ typename lvalue_simple_common_reference<T, U>::type;
+ template <typename T, typename U>
+ struct simple_common_reference<T&, U&>
+ : lvalue_simple_common_reference<T, U> {
+ };
+
+ template <typename T,
+ typename U,
+ typename LCR = test_t<lvalue_scr_t, T, U>,
+ typename C = rref_res_t<LCR>>
+ struct rvalue_simple_common_reference
+ : std::enable_if<std::is_convertible<T&&, C>::value &&
+ std::is_convertible<U&&, C>::value>::type {
+ };
+ template <typename T, typename U>
+ struct simple_common_reference<T&&, U&&>
+ : rvalue_simple_common_reference<T, U> {
+ };
+
+ template <typename A,
+ typename B,
+ typename C = test_t<lvalue_scr_t, A, const B>>
+ struct mixed_simple_common_reference
+ : std::enable_if<std::is_convertible<B&&, C>::value, C>::type {
+ };
+
+ template <typename A, typename B>
+ struct simple_common_reference<A&, B&&>
+ : mixed_simple_common_reference<A, B> {
+ };
+ template <typename A, typename B>
+ struct simple_common_reference<A&&, B&>
+ : simple_common_reference<B&&, A&> {
+ };
+ template <typename T, typename U>
+ using simple_common_reference_t =
+ typename simple_common_reference<T, U>::type;
+
+ template <typename>
+ struct xref {
+ template <typename U>
+ using type = U;
+ };
+
+ template <typename A>
+ struct xref<A&> {
+ template <typename U>
+ using type = typename std::add_lvalue_reference<
+ typename xref<A>::template type<U>>::type;
+ };
+
+ template <typename A>
+ struct xref<A&&> {
+ template <typename U>
+ using type = typename std::add_rvalue_reference<
+ typename xref<A>::template type<U>>::type;
+ };
+
+ template <typename A>
+ struct xref<const A> {
+ template <typename U>
+ using type = typename std::add_const<
+ typename xref<A>::template type<U>>::type;
+ };
+
+ template <typename A>
+ struct xref<volatile A> {
+ template <typename U>
+ using type = typename std::add_volatile<
+ typename xref<A>::template type<U>>::type;
+ };
+
+ template <typename A>
+ struct xref<const volatile A> {
+ template <typename U>
+ using type = typename std::add_cv<
+ typename xref<A>::template type<U>>::type;
+ };
+
+ template <typename T,
+ typename U,
+ template <class>
+ class TQual,
+ template <class>
+ class UQual>
+ struct basic_common_reference {
+ };
+
+ template <typename...>
+ struct get_common_reference;
+ template <typename... Ts>
+ using get_common_reference_t =
+ typename get_common_reference<Ts...>::type;
+
+ template <>
+ struct get_common_reference<> {
+ };
+ template <typename T0>
+ struct get_common_reference<T0> {
+ using type = T0;
+ };
+
+ template <typename T, typename U>
+ struct has_simple_common_ref
+ : exists<simple_common_reference_t, T, U> {
+ };
+ template <typename T, typename U>
+ using basic_common_ref_t = typename basic_common_reference<
+ ::scn::detail::remove_cvref_t<T>,
+ ::scn::detail::remove_cvref_t<U>,
+ xref<T>::template type,
+ xref<U>::template type>::type;
+
+ template <typename T, typename U>
+ struct has_basic_common_ref : exists<basic_common_ref_t, T, U> {
+ };
+ template <typename T, typename U>
+ struct has_cond_res : exists<cond_res_t, T, U> {
+ };
+
+ template <typename T, typename U, typename = void>
+ struct binary_common_ref : get_common_type<T, U> {
+ };
+ template <typename T, typename U>
+ struct binary_common_ref<
+ T,
+ U,
+ typename std::enable_if<
+ has_simple_common_ref<T, U>::value>::type>
+ : simple_common_reference<T, U> {
+ };
+ template <typename T, typename U>
+ struct binary_common_ref<
+ T,
+ U,
+ typename std::enable_if<
+ has_basic_common_ref<T, U>::value &&
+ !has_simple_common_ref<T, U>::value>::type> {
+ using type = basic_common_ref_t<T, U>;
+ };
+ template <typename T, typename U>
+ struct binary_common_ref<
+ T,
+ U,
+ typename std::enable_if<
+ has_cond_res<T, U>::value &&
+ !has_basic_common_ref<T, U>::value &&
+ !has_simple_common_ref<T, U>::value>::type> {
+ using type = cond_res_t<T, U>;
+ };
+ template <typename T1, typename T2>
+ struct get_common_reference<T1, T2> : binary_common_ref<T1, T2> {
+ };
+
+ template <typename Void, typename T1, typename T2, typename... Rest>
+ struct multiple_common_reference {
+ };
+ template <typename T1, typename T2, typename... Rest>
+ struct multiple_common_reference<
+ void_t<get_common_reference_t<T1, T2>>,
+ T1,
+ T2,
+ Rest...> : get_common_reference<get_common_reference_t<T1, T2>,
+ Rest...> {
+ };
+ template <typename T1, typename T2, typename... Rest>
+ struct get_common_reference<T1, T2, Rest...>
+ : multiple_common_reference<void, T1, T2, Rest...> {
+ };
+
+ template <typename... Ts>
+ using get_common_type_t = typename get_common_type<Ts...>::type;
+
+ template <typename T, typename U>
+ struct _same_decayed
+ : std::integral_constant<
+ bool,
+ std::is_same<T, typename std::decay<T>::type>::value &&
+ std::is_same<U,
+ typename std::decay<U>::type>::value> {
+ };
+
+ template <typename T, typename U>
+ using ternary_return_t =
+ typename std::decay<decltype(false ? SCN_DECLVAL(T)
+ : SCN_DECLVAL(U))>::type;
+
+ template <typename, typename, typename = void>
+ struct binary_common_type {
+ };
+
+ template <typename T, typename U>
+ struct binary_common_type<
+ T,
+ U,
+ typename std::enable_if<!_same_decayed<T, U>::value>::type>
+ : get_common_type<typename std::decay<T>::type,
+ typename std::decay<U>::type> {
+ };
+
+ template <typename T, typename U>
+ struct binary_common_type<
+ T,
+ U,
+ typename std::enable_if<
+ _same_decayed<T, U>::value &&
+ exists<ternary_return_t, T, U>::value>::type> {
+ using type = ternary_return_t<T, U>;
+ };
+
+ template <typename T, typename U>
+ struct binary_common_type<
+ T,
+ U,
+ typename std::enable_if<
+ _same_decayed<T, U>::value &&
+ !exists<ternary_return_t, T, U>::value &&
+ exists<cond_res_t, cref_t<T>, cref_t<U>>::value>::type> {
+ using type =
+ typename std::decay<cond_res_t<cref_t<T>, cref_t<U>>>::type;
+ };
+
+ template <>
+ struct get_common_type<> {
+ };
+
+ template <typename T>
+ struct get_common_type<T> : get_common_type<T, T> {
+ };
+
+ template <typename T, typename U>
+ struct get_common_type<T, U> : binary_common_type<T, U> {
+ };
+
+ template <typename Void, typename...>
+ struct multiple_common_type {
+ };
+
+ template <typename T1, typename T2, typename... R>
+ struct multiple_common_type<void_t<get_common_type_t<T1, T2>>,
+ T1,
+ T2,
+ R...>
+ : get_common_type<get_common_type_t<T1, T2>, R...> {
+ };
+
+ template <typename T1, typename T2, typename... R>
+ struct get_common_type<T1, T2, R...>
+ : multiple_common_type<void, T1, T2, R...> {
+ };
+ } // namespace detail
+ } // namespace custom_ranges
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_RANGES_UTIL_H
diff --git a/src/third-party/scnlib/include/scn/reader/common.h b/src/third-party/scnlib/include/scn/reader/common.h
new file mode 100644
index 0000000..0f2b83b
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/reader/common.h
@@ -0,0 +1,1663 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_READER_COMMON_H
+#define SCN_READER_COMMON_H
+
+#include "../detail/error.h"
+#include "../detail/locale.h"
+#include "../detail/range.h"
+#include "../unicode/unicode.h"
+#include "../util/algorithm.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ // read_code_unit
+
+ namespace detail {
+ template <typename WrappedRange>
+ expected<typename WrappedRange::char_type>
+ read_code_unit_impl(WrappedRange& r, bool advance, std::true_type)
+ {
+ SCN_CLANG_PUSH
+ // clang 10 behaves weirdly
+ SCN_CLANG_IGNORE("-Wzero-as-null-pointer-constant")
+ SCN_EXPECT(r.begin() < r.end());
+ SCN_CLANG_POP
+ auto ch = *r.begin();
+ if (advance) {
+ r.advance();
+ }
+ return {ch};
+ }
+ template <typename WrappedRange>
+ expected<typename WrappedRange::char_type>
+ read_code_unit_impl(WrappedRange& r, bool advance, std::false_type)
+ {
+ SCN_EXPECT(r.begin() != r.end());
+ auto ch = *r.begin();
+ if (advance && ch) {
+ r.advance();
+ }
+ return ch;
+ }
+ } // namespace detail
+
+ /**
+ * Reads a single character (= code unit) from the range.
+ * Dereferences the begin iterator, wrapping it in an `expected` if
+ * necessary.
+ *
+ * Encoding-agnostic, doesn't care about code points, and may leave behind
+ * partial ones.
+ *
+ * \param r Range to read from
+ * \param advance If `true`, and the read was successful, the range is
+ * advanced by a single character, as if by calling `r.advance()`.
+ *
+ * \return The next character in the range, obtained as if by dereferencing
+ * the begin iterator `*r.begin()`.
+ * If `r.begin() == r.end()`, returns EOF.
+ * If `r` is direct, returns `*r.begin()` wrapped in an `expected`.
+ * If `r` is not direct, returns `*r.begin()` as-is, with any errors that
+ * may have been caused by the read.
+ */
+ template <typename WrappedRange>
+ expected<typename WrappedRange::char_type> read_code_unit(
+ WrappedRange& r,
+ bool advance = true)
+ {
+ if (r.begin() == r.end()) {
+ return error(error::end_of_range, "EOF");
+ }
+ return detail::read_code_unit_impl(
+ r, advance,
+ std::integral_constant<bool, WrappedRange::is_direct>{});
+ }
+
+ // putback_n
+
+ /// @{
+
+ /**
+ * Puts back `n` characters (= code units) into `r` as if by repeatedly
+ * calling `r.advance(-1)`.
+ *
+ * Encoding-agnostic, may leave behind partial code points.
+ *
+ * \param r Range to roll back
+ * \param n Characters to put back, must be less than or equal to the number
+ * of characters already read from `r`.
+ *
+ * \return If `r` is contiguous, will always return `error::good`.
+ * Otherwise, may return `error::unrecoverable_source_error`, if the putback
+ * fails.
+ */
+ template <
+ typename WrappedRange,
+ typename std::enable_if<WrappedRange::is_contiguous>::type* = nullptr>
+ error putback_n(WrappedRange& r, ranges::range_difference_t<WrappedRange> n)
+ {
+ SCN_EXPECT(n <= ranges::distance(r.begin_underlying(), r.begin()));
+ r.advance(-n);
+ return {};
+ }
+ template <
+ typename WrappedRange,
+ typename std::enable_if<!WrappedRange::is_contiguous>::type* = nullptr>
+ error putback_n(WrappedRange& r, ranges::range_difference_t<WrappedRange> n)
+ {
+ for (ranges::range_difference_t<WrappedRange> i = 0; i < n; ++i) {
+ r.advance(-1);
+ if (r.begin() == r.end()) {
+ return {error::unrecoverable_source_error, "Putback failed"};
+ }
+ }
+ return {};
+ }
+
+ /// @}
+
+ // read_code_point
+
+ /**
+ * Type returned by `read_code_point`
+ * \tparam CharT Character type of the range
+ */
+ template <typename CharT>
+ struct read_code_point_result {
+ /// Code units, may point to `writebuf` given to `read_code_point`
+ span<const CharT> chars;
+ /// Parsed code point
+ code_point cp;
+ };
+
+ namespace detail {
+ // contiguous && direct
+ template <typename CharT, typename WrappedRange>
+ expected<read_code_point_result<CharT>> read_code_point_impl(
+ WrappedRange& r,
+ span<CharT> writebuf,
+ std::true_type)
+ {
+ if (r.begin() == r.end()) {
+ return error(error::end_of_range, "EOF");
+ }
+
+ auto sbuf = r.get_buffer_and_advance(4 / sizeof(CharT));
+ if (sbuf.size() == 0) {
+ auto ret = read_code_unit(r, true);
+ if (!ret) {
+ return ret.error();
+ }
+ sbuf = writebuf.first(1);
+ writebuf[0] = ret.value();
+ }
+ int len = ::scn::get_sequence_length(sbuf[0]);
+ if (SCN_UNLIKELY(len == 0)) {
+ return error(error::invalid_encoding, "Invalid code point");
+ }
+ if (sbuf.ssize() > len) {
+ auto e = putback_n(r, sbuf.ssize() - len);
+ if (!e) {
+ return e;
+ }
+ sbuf = sbuf.first(static_cast<size_t>(len));
+ }
+ if (len == 1) {
+ // Single-char code point
+ return read_code_point_result<CharT>{sbuf.first(1),
+ make_code_point(sbuf[0])};
+ }
+ while (sbuf.ssize() < len) {
+ auto ret = read_code_unit(r, true);
+ if (!ret) {
+ auto e = putback_n(r, sbuf.ssize());
+ if (!e) {
+ return e;
+ }
+ if (ret.error().code() == error::end_of_range) {
+ return error(error::invalid_encoding,
+ "Invalid code point");
+ }
+ return ret.error();
+ }
+ sbuf = make_span(writebuf.begin(), sbuf.size() + 1);
+ writebuf[sbuf.size() - 1] = ret.value();
+ }
+
+ code_point cp{};
+ auto ret = parse_code_point(sbuf.begin(), sbuf.end(), cp);
+ if (!ret) {
+ return ret.error();
+ }
+ return read_code_point_result<CharT>{sbuf, cp};
+ }
+
+ template <typename CharT, typename WrappedRange>
+ expected<read_code_point_result<CharT>> read_code_point_impl(
+ WrappedRange& r,
+ span<CharT> writebuf,
+ std::false_type)
+ {
+ auto first = read_code_unit(r, false);
+ if (!first) {
+ return first.error();
+ }
+
+ auto len =
+ static_cast<size_t>(::scn::get_sequence_length(first.value()));
+ if (SCN_UNLIKELY(len == 0)) {
+ return error(error::invalid_encoding, "Invalid code point");
+ }
+ r.advance();
+
+ writebuf[0] = first.value();
+ if (len == 1) {
+ // Single-char code point
+ return read_code_point_result<CharT>{
+ make_span(writebuf.data(), 1),
+ make_code_point(first.value())};
+ }
+
+ size_t index = 1;
+
+ auto parse = [&]() -> expected<read_code_point_result<CharT>> {
+ code_point cp{};
+ auto ret = parse_code_point(writebuf.data(),
+ writebuf.data() + len, cp);
+ if (!ret) {
+ auto pb = putback_n(r, static_cast<std::ptrdiff_t>(len));
+ if (!pb) {
+ return pb;
+ }
+ return ret.error();
+ }
+ auto s = make_span(writebuf.data(), len);
+ return read_code_point_result<CharT>{s, cp};
+ };
+ auto advance = [&]() -> error {
+ auto ret = read_code_unit(r, false);
+ if (!ret) {
+ auto pb = putback_n(r, static_cast<std::ptrdiff_t>(index));
+ if (!pb) {
+ return pb;
+ }
+ return ret.error();
+ }
+ writebuf[index] = ret.value();
+ ++index;
+ r.advance();
+ return {};
+ };
+
+ while (index < 4) {
+ auto e = advance();
+ if (!e) {
+ return e;
+ }
+ if (index == len) {
+ return parse();
+ }
+ }
+ SCN_ENSURE(false);
+ SCN_UNREACHABLE;
+ }
+ } // namespace detail
+
+ /**
+ * Read a single Unicode code point from `r` as if by repeatedly calling
+ * `read_code_unit()`.
+ *
+ * Advances the range past the read code point. On error, rolls back the
+ * range into the state it was before calling this function, as if by
+ * calling `putback_n()`.
+ *
+ * \param r Range to read from
+ * \param writebuf Buffer to use for reading into, if necessary. `BufValueT`
+ * can be any trivial type. Must be at least 4 bytes long. May be written
+ * over.
+ *
+ * \return An instance of `read_code_point_result`, wrapped in an
+ * `expected`. `chars` contains the code units read from `r`, which may
+ * point to `writebuf`. `cp` contains the code point parsed.
+ * If `r.begin() == r.end()`, returns EOF.
+ * If `read_code_unit()` or `putback_n()` fails, returns any errors returned
+ * by it.
+ * If the code point was not encoded correctly, returns
+ * `error::invalid_encoding`.
+ */
+ template <typename WrappedRange, typename BufValueT>
+ expected<read_code_point_result<typename WrappedRange::char_type>>
+ read_code_point(WrappedRange& r, span<BufValueT> writebuf)
+ {
+ SCN_EXPECT(writebuf.size() * sizeof(BufValueT) >= 4);
+ using char_type = typename WrappedRange::char_type;
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wcast-align") // taken care of by the caller
+ return detail::read_code_point_impl<char_type>(
+ r,
+ make_span(reinterpret_cast<char_type*>(writebuf.data()),
+ writebuf.size() * sizeof(BufValueT) / sizeof(char_type)),
+ std::integral_constant<bool,
+ WrappedRange::provides_buffer_access>{});
+ SCN_GCC_POP
+ }
+
+ // read_zero_copy
+
+ /// @{
+
+ /**
+ * Reads up to `n` characters (= code units) from `r`, as if by repeatedly
+ * incrementing `r.begin()`, and returns a `span` pointing into `r`.
+ *
+ * Let `count` be `min(r.size(), n)`.
+ * Reads, and advances `r` by `count` characters.
+ * `r.begin()` is in no point dereferenced.
+ * If `r.size()` is not defined, the range is not contiguous, and an empty
+ * span is returned.
+ *
+ * \return A `span` pointing to `r`, starting from `r.begin()` and with a
+ * size of `count`.
+ * If `r.begin() == r.end()`, returns EOF.
+ * If the range does not satisfy `contiguous_range`, returns an empty
+ * `span`.
+ */
+ template <typename WrappedRange,
+ typename std::enable_if<
+ WrappedRange::provides_buffer_access>::type* = nullptr>
+ expected<span<const typename detail::extract_char_type<
+ typename WrappedRange::iterator>::type>>
+ read_zero_copy(WrappedRange& r, ranges::range_difference_t<WrappedRange> n)
+ {
+ if (r.begin() == r.end()) {
+ return error(error::end_of_range, "EOF");
+ }
+ return r.get_buffer_and_advance(static_cast<size_t>(n));
+ }
+ template <typename WrappedRange,
+ typename std::enable_if<
+ !WrappedRange::provides_buffer_access>::type* = nullptr>
+ expected<span<const typename detail::extract_char_type<
+ typename WrappedRange::iterator>::type>>
+ read_zero_copy(WrappedRange& r, ranges::range_difference_t<WrappedRange>)
+ {
+ if (r.begin() == r.end()) {
+ return error(error::end_of_range, "EOF");
+ }
+ return span<const typename detail::extract_char_type<
+ typename WrappedRange::iterator>::type>{};
+ }
+ /// @}
+
+ // read_all_zero_copy
+
+ /// @{
+ /**
+ * Reads every character from `r`, as if by repeatedly incrementing
+ * `r.begin()`, and returns a `span` pointing into `r`.
+ *
+ * If there's no error, `r` is advanced to the end.
+ * `r.begin()` is in no point dereferenced.
+ * If `r.size()` is not defined, the range is not contiguous, and an empty
+ * span is returned.
+ *
+ * \return A `span` pointing to `r`, starting at `r.begin()` and ending at
+ * `r.end()`.
+ * If `r.begin() == r.end()`, returns EOF.
+ * If the range does not satisfy `contiguous_range`, returns an empty
+ * `span`.
+ */
+ template <
+ typename WrappedRange,
+ typename std::enable_if<WrappedRange::is_contiguous>::type* = nullptr>
+ expected<span<const typename detail::extract_char_type<
+ typename WrappedRange::iterator>::type>>
+ read_all_zero_copy(WrappedRange& r)
+ {
+ if (r.begin() == r.end()) {
+ return error(error::end_of_range, "EOF");
+ }
+ auto s = make_span(r.data(), static_cast<size_t>(r.size()));
+ r.advance(r.size());
+ return s;
+ }
+ template <
+ typename WrappedRange,
+ typename std::enable_if<!WrappedRange::is_contiguous>::type* = nullptr>
+ expected<span<const typename detail::extract_char_type<
+ typename WrappedRange::iterator>::type>>
+ read_all_zero_copy(WrappedRange& r)
+ {
+ if (r.begin() == r.end()) {
+ return error(error::end_of_range, "EOF");
+ }
+ return span<const typename detail::extract_char_type<
+ typename WrappedRange::iterator>::type>{};
+ }
+ /// @}
+
+ // read_into
+
+ namespace detail {
+ template <typename WrappedRange, typename OutputIterator>
+ error read_into_impl(WrappedRange& r,
+ OutputIterator& it,
+ ranges::range_difference_t<WrappedRange> n)
+ {
+ for (; n != 0; --n) {
+ auto ret = read_code_unit(r, false);
+ if (!ret) {
+ return ret.error();
+ }
+ *it = ret.value();
+ r.advance();
+ }
+ return {};
+ }
+ } // namespace detail
+
+ /// @{
+
+ /**
+ * Reads up to `n` characters (= code units) from `r`, as if by repeatedly
+ * calling `read_code_unit()`, and writing the characters into `it`.
+ *
+ * If reading fails at any point, the error is returned.
+ * `r` is advanced by as many characters that were successfully read.
+ *
+ * \param r Range to read
+ * \param it Iterator to write into, e.g. `std::back_insert_iterator`. Must
+ * satisfy `output_iterator`, and be incrementable by `n` times.
+ * \param n Characters to read from `r`
+ *
+ * \return `error::good` if `n` characters were read.
+ * If `r.begin() == r.end()` at any point before `n` characters has been
+ * read, returns EOF.
+ * Any error returned by `read_code_unit()` if one
+ * occurred.
+ */
+ template <typename WrappedRange,
+ typename OutputIterator,
+ typename std::enable_if<
+ WrappedRange::provides_buffer_access>::type* = nullptr>
+ error read_into(WrappedRange& r,
+ OutputIterator& it,
+ ranges::range_difference_t<WrappedRange> n)
+ {
+ while (n != 0) {
+ if (r.begin() == r.end()) {
+ return {error::end_of_range, "EOF"};
+ }
+ auto s = read_zero_copy(r, n);
+ if (!s) {
+ return s.error();
+ }
+ if (s.value().size() == 0) {
+ break;
+ }
+ it = std::copy(s.value().begin(), s.value().end(), it);
+ n -= s.value().ssize();
+ }
+ if (n != 0) {
+ return detail::read_into_impl(r, it, n);
+ }
+ return {};
+ }
+ template <typename WrappedRange,
+ typename OutputIterator,
+ typename std::enable_if<
+ !WrappedRange::provides_buffer_access>::type* = nullptr>
+ error read_into(WrappedRange& r,
+ OutputIterator& it,
+ ranges::range_difference_t<WrappedRange> n)
+ {
+ if (r.begin() == r.end()) {
+ return {error::end_of_range, "EOF"};
+ }
+ return detail::read_into_impl(r, it, n);
+ }
+ /// @}
+
+ namespace detail {
+ template <typename WrappedRange, typename Predicate>
+ expected<span<const typename WrappedRange::char_type>>
+ read_until_pred_contiguous(WrappedRange& r,
+ Predicate&& pred,
+ bool pred_result_to_stop,
+ bool keep_final)
+ {
+ using span_type = span<const typename WrappedRange::char_type>;
+
+ if (r.begin() == r.end()) {
+ return error(error::end_of_range, "EOF");
+ }
+
+ if (!pred.is_multibyte()) {
+ for (auto it = r.begin(); it != r.end(); ++it) {
+ if (pred(make_span(&*it, 1)) == pred_result_to_stop) {
+ auto begin = r.data();
+ auto end = keep_final ? it + 1 : it;
+ r.advance_to(end);
+ return span_type{
+ begin, to_address_safe(end, r.begin(), r.end())};
+ }
+ }
+ }
+ else {
+ for (auto it = r.begin(); it != r.end();) {
+ auto len = ::scn::get_sequence_length(*it);
+ if (len == 0 || ranges::distance(it, r.end()) < len) {
+ return error{error::invalid_encoding,
+ "Invalid code point"};
+ }
+ auto span =
+ make_span(to_address_safe(it, r.begin(), r.end()),
+ static_cast<size_t>(len));
+ code_point cp{};
+ auto i = parse_code_point(span.begin(), span.end(), cp);
+ if (!i) {
+ return i.error();
+ }
+ if (i.value() != span.end()) {
+ return error{error::invalid_encoding,
+ "Invalid code point"};
+ }
+ if (pred(span) == pred_result_to_stop) {
+ auto begin = r.data();
+ auto end = keep_final ? it + len : it;
+ r.advance_to(end);
+ return span_type{
+ begin, to_address_safe(end, r.begin(), r.end())};
+ }
+ it += len;
+ }
+ }
+ auto begin = r.data();
+ auto end = r.data() + r.size();
+ r.advance_to(r.end());
+ return span_type{begin, end};
+ }
+ } // namespace detail
+
+ // read_until_space_zero_copy
+
+ namespace detail {
+ template <typename WrappedRange, typename Predicate>
+ expected<span<const typename WrappedRange::char_type>>
+ read_until_space_zero_copy_impl(WrappedRange& r,
+ Predicate&& is_space,
+ bool keep_final_space,
+ std::true_type)
+ {
+ return detail::read_until_pred_contiguous(r, SCN_FWD(is_space),
+ true, keep_final_space);
+ }
+ template <typename WrappedRange, typename Predicate>
+ expected<span<const typename WrappedRange::char_type>>
+ read_until_space_zero_copy_impl(WrappedRange& r,
+ Predicate&&,
+ bool,
+ std::false_type)
+ {
+ if (r.begin() == r.end()) {
+ return error(error::end_of_range, "EOF");
+ }
+ return span<const typename WrappedRange::char_type>{};
+ }
+ } // namespace detail
+
+ /**
+ * Reads code points from `r`, until a space, as determined by `is_space`,
+ * is found, and returns a `span` pointing to `r`.
+ *
+ * If no error occurs `r` is advanced past the returned span.
+ * On error, `r` is not advanced.
+ *
+ * \param r Range to read from
+ *
+ * \param is_space Predicate taking a span of code units encompassing a code
+ * point, and returning a `bool`, where `true` means that the character is a
+ * space. Additionally, it must have a member function
+ * `is_space.is_multibyte()`, returning a `bool`, where `true` means that a
+ * space character can encompass multiple code units.
+ *
+ * \param keep_final_space If `true`, the space code point found is included
+ * in the returned span, and it is advanced past in `r`. If `false`, it is
+ * not included, and `r.begin()` will point to the space.
+ *
+ * \return Span of code units, pointing to `r`, starting at `r.begin()`, and
+ * ending at the space character, the precise location determined by the
+ * `keep_final_space` parameter.
+ * If `r.begin() == r.end()`, returns EOF.
+ * `r` reaching its end before a space character is found is not considered
+ * an error.
+ * If `r` contains invalid encoding, returns `error::invalid_encoding`.
+ * If the range is not contiguous, returns an empty `span`.
+ */
+ template <typename WrappedRange, typename Predicate>
+ expected<span<const typename WrappedRange::char_type>>
+ read_until_space_zero_copy(WrappedRange& r,
+ Predicate&& is_space,
+ bool keep_final_space)
+ {
+ return detail::read_until_space_zero_copy_impl(
+ r, SCN_FWD(is_space), keep_final_space,
+ std::integral_constant<bool, WrappedRange::is_contiguous>{});
+ }
+
+ // read_until_space
+
+ namespace detail {
+ template <typename WrappedRange,
+ typename Predicate,
+ typename OutputIt,
+ typename OutputItCmp>
+ error read_until_pred_buffer(WrappedRange& r,
+ Predicate&& pred,
+ bool pred_result_to_stop,
+ OutputIt& out,
+ OutputItCmp out_cmp,
+ bool keep_final,
+ bool& done,
+ std::true_type)
+ {
+ if (!pred.is_multibyte()) {
+ while (r.begin() != r.end() && !done) {
+ auto s = r.get_buffer_and_advance();
+ for (auto it = s.begin(); it != s.end() && out_cmp(out);
+ ++it) {
+ if (pred(make_span(&*it, 1)) == pred_result_to_stop) {
+ if (keep_final) {
+ *out = *it;
+ ++out;
+ }
+ auto e =
+ putback_n(r, ranges::distance(it, s.end()));
+ if (!e) {
+ return e;
+ }
+ done = true;
+ break;
+ }
+ *out = *it;
+ ++out;
+ }
+ if (!done && out_cmp(out)) {
+ auto ret = read_code_unit(r, false);
+ if (!ret) {
+ if (ret.error() == error::end_of_range) {
+ return {};
+ }
+ return ret.error();
+ }
+ if (pred(make_span(&ret.value(), 1)) ==
+ pred_result_to_stop) {
+ if (keep_final) {
+ r.advance();
+ *out = ret.value();
+ ++out;
+ }
+ done = true;
+ break;
+ }
+ r.advance();
+ *out = ret.value();
+ ++out;
+ }
+ }
+ }
+ else {
+ while (r.begin() != r.end() && !done) {
+ auto s = r.get_buffer_and_advance();
+ for (auto it = s.begin(); it != s.end() && out_cmp(out);) {
+ auto len = ::scn::get_sequence_length(*it);
+ if (len == 0) {
+ return error{error::invalid_encoding,
+ "Invalid code point"};
+ }
+ if (ranges::distance(it, s.end()) < len) {
+ auto e = putback_n(r, len);
+ if (!e) {
+ return e;
+ }
+ break;
+ }
+ auto cpspan = make_span(it, static_cast<size_t>(len));
+ code_point cp{};
+ auto i =
+ parse_code_point(cpspan.begin(), cpspan.end(), cp);
+ if (!i) {
+ return i.error();
+ }
+ if (i.value() != cpspan.end()) {
+ return error{error::invalid_encoding,
+ "Invalid code point"};
+ }
+ if (pred(cpspan) == pred_result_to_stop) {
+ if (keep_final) {
+ out = std::copy(cpspan.begin(), cpspan.end(),
+ out);
+ }
+ done = true;
+ break;
+ }
+ out = std::copy(cpspan.begin(), cpspan.end(), out);
+ }
+
+ if (!done && out_cmp(out)) {
+ alignas(typename WrappedRange::char_type) unsigned char
+ buf[4] = {0};
+ auto cpret = read_code_point(r, make_span(buf, 4));
+ if (!cpret) {
+ if (cpret.error() == error::end_of_range) {
+ return {};
+ }
+ return cpret.error();
+ }
+ if (pred(cpret.value().chars) == pred_result_to_stop) {
+ if (keep_final) {
+ out = std::copy(cpret.value().chars.begin(),
+ cpret.value().chars.end(), out);
+ }
+ else {
+ return putback_n(r,
+ cpret.value().chars.ssize());
+ }
+ done = true;
+ break;
+ }
+ out = std::copy(cpret.value().chars.begin(),
+ cpret.value().chars.end(), out);
+ }
+ }
+ }
+ return {};
+ }
+ template <typename WrappedRange,
+ typename Predicate,
+ typename OutputIt,
+ typename OutputItCmp>
+ error read_until_pred_buffer(WrappedRange&,
+ Predicate&&,
+ bool,
+ OutputIt&,
+ OutputItCmp,
+ bool,
+ bool& done,
+ std::false_type)
+ {
+ done = false;
+ return {};
+ }
+
+ template <typename WrappedRange,
+ typename Predicate,
+ typename OutputIt,
+ typename OutputItCmp>
+ error read_until_pred_non_contiguous(WrappedRange& r,
+ Predicate&& pred,
+ bool pred_result_to_stop,
+ OutputIt& out,
+ OutputItCmp out_cmp,
+ bool keep_final)
+ {
+ if (r.begin() == r.end()) {
+ return {error::end_of_range, "EOF"};
+ }
+
+ {
+ bool done = false;
+ auto e = read_until_pred_buffer(
+ r, pred, pred_result_to_stop, out, out_cmp, keep_final,
+ done,
+ std::integral_constant<
+ bool, WrappedRange::provides_buffer_access>{});
+ if (!e) {
+ return e;
+ }
+ if (done) {
+ return {};
+ }
+ }
+
+ if (!pred.is_multibyte()) {
+ while (r.begin() != r.end() && out_cmp(out)) {
+ auto cu = read_code_unit(r, false);
+ if (!cu) {
+ return cu.error();
+ }
+ if (pred(make_span(&cu.value(), 1)) ==
+ pred_result_to_stop) {
+ if (keep_final) {
+ r.advance();
+ *out = cu.value();
+ ++out;
+ }
+ return {};
+ }
+ r.advance();
+ *out = cu.value();
+ ++out;
+ }
+ }
+ else {
+ unsigned char buf[4] = {0};
+ while (r.begin() != r.end() && out_cmp(out)) {
+ auto cp = read_code_point(r, make_span(buf, 4));
+ if (!cp) {
+ return cp.error();
+ }
+ if (pred(cp.value().chars) == pred_result_to_stop) {
+ if (keep_final) {
+ out = std::copy(cp.value().chars.begin(),
+ cp.value().chars.end(), out);
+ return {};
+ }
+ else {
+ return putback_n(r, cp.value().chars.ssize());
+ }
+ }
+ out = std::copy(cp.value().chars.begin(),
+ cp.value().chars.end(), out);
+ }
+ }
+ return {};
+ }
+ } // namespace detail
+
+ /// @{
+
+ /**
+ * Reads code points from `r`, until a space, as determined by `is_space`,
+ * is found, and writes them into `out`, a single code unit at a time.
+ *
+ * If no error occurs, `r` is advanced past the last character written into
+ * `out`.
+ *
+ * On error, `r` is advanced an indeterminate amount, as if by calling
+ * `r.advance(n)`, where `n` is a non-negative integer.
+ * It is, however, not advanced past any space characters.
+ *
+ * \param r Range to read from
+ *
+ * \param out Iterator to write read characters into. Must satisfy
+ * `output_iterator`.
+ *
+ * \param is_space Predicate taking a span of code units encompassing a code
+ * point, and returning a `bool`, where `true` means that the character is a
+ * space. Additionally, it must have a member function
+ * `is_space.is_multibyte()`, returning a `bool`, where `true` means that a
+ * space character can encompass multiple code units.
+ *
+ * \param keep_final_space If `true`, the space code point found is written
+ * into `out`, and it is advanced past in `r`. If `false`, it is not
+ * included, and `r.begin()` will point to the space.
+ *
+ * \return `error::good` on success.
+ * If `r.begin() == r.end()`, returns EOF.
+ * `r` reaching its end before a space character is found is not considered
+ * an error.
+ * If `r` contains invalid encoding, returns `error::invalid_encoding`.
+ */
+ template <
+ typename WrappedRange,
+ typename OutputIterator,
+ typename Predicate,
+ typename std::enable_if<WrappedRange::is_contiguous>::type* = nullptr>
+ error read_until_space(WrappedRange& r,
+ OutputIterator& out,
+ Predicate&& is_space,
+ bool keep_final_space)
+ {
+ auto s =
+ read_until_space_zero_copy(r, SCN_FWD(is_space), keep_final_space);
+ if (!s) {
+ return s.error();
+ }
+ out = std::copy(s.value().begin(), s.value().end(), out);
+ return {};
+ }
+ template <
+ typename WrappedRange,
+ typename OutputIterator,
+ typename Predicate,
+ typename std::enable_if<!WrappedRange::is_contiguous>::type* = nullptr>
+ error read_until_space(WrappedRange& r,
+ OutputIterator& out,
+ Predicate&& is_space,
+ bool keep_final_space)
+ {
+ return detail::read_until_pred_non_contiguous(
+ r, SCN_FWD(is_space), true, out,
+ [](const OutputIterator&) { return true; }, keep_final_space);
+ }
+
+ /// @}
+
+ // read_until_space_ranged
+
+ /// @{
+
+ /**
+ * Otherwise equivalent to `read_until_space`, except will also stop reading
+ * if `out == end`.
+ *
+ * \see read_until_space
+ */
+ template <typename WrappedRange,
+ typename OutputIterator,
+ typename Sentinel,
+ typename Predicate>
+ error read_until_space_ranged(WrappedRange& r,
+ OutputIterator& out,
+ Sentinel end,
+ Predicate&& is_space,
+ bool keep_final_space)
+ {
+ return detail::read_until_pred_non_contiguous(
+ r, SCN_FWD(is_space), true, out,
+ [&end](const OutputIterator& it) { return it != end; },
+ keep_final_space);
+ }
+
+ /// @}
+
+ namespace detail {
+ /**
+ * Predicate to pass to read_until_space etc.
+ */
+ template <typename CharT>
+ struct is_space_predicate {
+ using char_type = CharT;
+ using locale_type = basic_locale_ref<char_type>;
+
+ /**
+ * \param l Locale to use, fetched from `ctx.locale()`
+ * \param localized If `true`, use `l.get_custom()`, otherwise use
+ * `l.get_static()`.
+ * \param width If `width != 0`, limit the number of code
+ * units to be read
+ */
+ SCN_CONSTEXPR14 is_space_predicate(const locale_type& l,
+ bool localized,
+ size_t width)
+ : m_locale{nullptr},
+ m_width{width},
+ m_fn{get_fn(localized, width != 0)}
+ {
+ if (localized) {
+ l.prepare_localized();
+ m_locale = l.get_localized_unsafe();
+ }
+ }
+
+ /**
+ * Returns `true` if `ch` is a code point according to the supplied
+ * locale, using either the static or custom locale, depending on
+ * the `localized` parameter given to the constructor.
+ *
+ * Returns also `true` if the maximum width, as determined by the
+ * `width` parameter given to the constructor, was reached.
+ */
+ bool operator()(span<const char_type> ch)
+ {
+ SCN_EXPECT(m_fn);
+ SCN_EXPECT(ch.size() >= 1);
+ return m_fn(m_locale, ch, m_i, m_width);
+ }
+
+ /**
+ * Returns `true`, if `*this` uses the custom locale for classifying
+ * space characters
+ */
+ constexpr bool is_localized() const
+ {
+ return m_locale != nullptr;
+ }
+ /**
+ * Returns `true` if a space character can encompass multiple code
+ * units
+ */
+ constexpr bool is_multibyte() const
+ {
+ return is_localized() && is_multichar_type(CharT{});
+ }
+
+ private:
+ using static_locale_type = typename locale_type::static_type;
+ using custom_locale_type = typename locale_type::custom_type;
+ const custom_locale_type* m_locale;
+ size_t m_width{0}, m_i{0};
+
+ constexpr static bool call(const custom_locale_type*,
+ span<const char_type> ch,
+ size_t&,
+ size_t)
+ {
+ return static_locale_type::is_space(ch);
+ }
+ static bool localized_call(const custom_locale_type* locale,
+ span<const char_type> ch,
+ size_t&,
+ size_t)
+ {
+ SCN_EXPECT(locale != nullptr);
+ return locale->is_space(ch);
+ }
+ SCN_CONSTEXPR14 static bool call_counting(const custom_locale_type*,
+ span<const char_type> ch,
+ size_t& i,
+ size_t max)
+ {
+ SCN_EXPECT(i <= max);
+ if (i == max || i + ch.size() > max) {
+ return true;
+ }
+ i += ch.size();
+ return static_locale_type::is_space(ch);
+ }
+ static bool localized_call_counting(
+ const custom_locale_type* locale,
+ span<const char_type> ch,
+ size_t& i,
+ size_t max)
+ {
+ SCN_EXPECT(locale != nullptr);
+ SCN_EXPECT(i <= max);
+ if (i == max || i + ch.size() > max) {
+ return true;
+ }
+ i += ch.size();
+ return locale->is_space(ch);
+ }
+
+ using fn_type = bool (*)(const custom_locale_type*,
+ span<const char_type>,
+ size_t&,
+ size_t);
+ fn_type m_fn{nullptr};
+
+ static SCN_CONSTEXPR14 fn_type get_fn(bool localized, bool counting)
+ {
+ if (localized) {
+ return counting ? localized_call_counting : localized_call;
+ }
+ return counting ? call_counting : call;
+ }
+ };
+
+ template <typename CharT>
+ is_space_predicate<CharT> make_is_space_predicate(
+ const basic_locale_ref<CharT>& locale,
+ bool localized,
+ size_t width = 0)
+ {
+ return {locale, localized, width};
+ }
+
+ template <typename CharT>
+ struct basic_skipws_iterator {
+ using value_type = void;
+ using reference = void;
+ using pointer = void;
+ using size_type = size_t;
+ using difference_type = std::ptrdiff_t;
+ using iterator_category = std::output_iterator_tag;
+
+ constexpr basic_skipws_iterator() = default;
+
+ basic_skipws_iterator& operator=(CharT)
+ {
+ return *this;
+ }
+ basic_skipws_iterator& operator*()
+ {
+ return *this;
+ }
+ basic_skipws_iterator& operator++()
+ {
+ return *this;
+ }
+ };
+ } // namespace detail
+
+ // skip_range_whitespace
+
+ /// @{
+
+ /**
+ * Reads code points from `ctx.range()`, as if by repeatedly calling
+ * `read_code_point()`, until a non-space character is found, or EOF is
+ * reached. That non-space character is then put back into the range.
+ *
+ * Whether a character is a space, is determined by `ctx.locale()` and the
+ * `localized` parameter.
+ *
+ * \param ctx Context to get the range and locale from.
+ *
+ * \param localized If `true`, `ctx.locale().get_custom()` is used.
+ * Otherwise, `ctx.locale().get_static()` is used.
+ * In practice, means whether locale-specific whitespace characters are
+ * accepted, or just those given by `std::isspace` with the `"C"` locale.
+ *
+ * \return `error::good` on success.
+ * If `ctx.range().begin() == ctx.range().end()`, returns EOF.
+ * If `ctx.range()` contains invalid encoding, returns
+ * `error::invalid_encoding`.
+ */
+ template <typename Context,
+ typename std::enable_if<
+ !Context::range_type::is_contiguous>::type* = nullptr>
+ error skip_range_whitespace(Context& ctx, bool localized) noexcept
+ {
+ auto is_space_pred =
+ detail::make_is_space_predicate(ctx.locale(), localized);
+ auto it = detail::basic_skipws_iterator<typename Context::char_type>{};
+ return detail::read_until_pred_non_contiguous(
+ ctx.range(), is_space_pred, false, it,
+ [](decltype(it)) { return true; }, false);
+ }
+ template <typename Context,
+ typename std::enable_if<
+ Context::range_type::is_contiguous>::type* = nullptr>
+ error skip_range_whitespace(Context& ctx, bool localized) noexcept
+ {
+ auto is_space_pred =
+ detail::make_is_space_predicate(ctx.locale(), localized);
+ return detail::read_until_pred_contiguous(ctx.range(), is_space_pred,
+ false, false)
+ .error();
+ }
+
+ /// @}
+
+ namespace detail {
+ template <typename T>
+ struct simple_integer_scanner {
+ template <typename CharT>
+ static expected<typename span<const CharT>::iterator> scan(
+ span<const CharT> buf,
+ T& val,
+ int base = 10,
+ uint16_t flags = 0);
+
+ template <typename CharT>
+ static expected<typename span<const CharT>::iterator> scan_lower(
+ span<const CharT> buf,
+ T& val,
+ int base = 10,
+ uint16_t flags = 0);
+ };
+ } // namespace detail
+
+ /**
+ * A very simple parser base class, which only accepts empty format string
+ * specifiers, e.g. `{}`, `{:}` or `{1:}`.
+ */
+ struct empty_parser : parser_base {
+ template <typename ParseCtx>
+ error parse(ParseCtx& pctx)
+ {
+ pctx.arg_begin();
+ if (SCN_UNLIKELY(!pctx)) {
+ return {error::invalid_format_string,
+ "Unexpected format string end"};
+ }
+ if (!pctx.check_arg_end()) {
+ return {error::invalid_format_string, "Expected argument end"};
+ }
+ pctx.arg_end();
+ return {};
+ }
+ };
+
+ /**
+ * Provides a framework for building a format string parser.
+ * Does not provide a `parse()` member function, so not a parser on to its
+ * own.
+ */
+ struct common_parser : parser_base {
+ static constexpr bool support_align_and_fill()
+ {
+ return true;
+ }
+
+ protected:
+ /**
+ * Parse the beginning of the argument.
+ * Returns `error::invalid_format_string` if `!pctx` (the format string
+ * ended)
+ */
+ template <typename ParseCtx>
+ error parse_common_begin(ParseCtx& pctx)
+ {
+ pctx.arg_begin();
+ if (SCN_UNLIKELY(!pctx)) {
+ return {error::invalid_format_string,
+ "Unexpected format string end"};
+ }
+ return {};
+ }
+
+ /**
+ * Returns `error::invalid_format_string` if the format string or the
+ * argument has ended.
+ */
+ template <typename ParseCtx>
+ error check_end(ParseCtx& pctx)
+ {
+ if (!pctx || pctx.check_arg_end()) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string argument"};
+ }
+ return {};
+ }
+
+ /**
+ * Parse alignment, fill, width, and localization flags, and populate
+ * appropriate member variables.
+ *
+ * Returns `error::invalid_format_string` if an error occurred.
+ */
+ template <typename ParseCtx>
+ error parse_common_flags(ParseCtx& pctx)
+ {
+ SCN_EXPECT(check_end(pctx));
+ using char_type = typename ParseCtx::char_type;
+
+ auto ch = pctx.next_char();
+ auto next_char = [&]() -> error {
+ pctx.advance_char();
+ auto e = check_end(pctx);
+ if (!e) {
+ return e;
+ }
+ ch = pctx.next_char();
+ return {};
+ };
+ auto parse_number = [&](size_t& n) -> error {
+ SCN_EXPECT(pctx.locale().get_static().is_digit(ch));
+
+ auto it = pctx.begin();
+ for (; it != pctx.end(); ++it) {
+ if (!pctx.locale().get_static().is_digit(*it)) {
+ break;
+ }
+ }
+ auto buf = make_span(pctx.begin(), it);
+
+ auto s = detail::simple_integer_scanner<size_t>{};
+ auto res = s.scan(buf, n, 10);
+ if (!res) {
+ return res.error();
+ }
+
+ for (it = pctx.begin(); it != res.value();
+ pctx.advance_char(), it = pctx.begin()) {}
+ return {};
+ };
+
+ auto get_align_char = [&](char_type c) -> common_options_type {
+ if (c == detail::ascii_widen<char_type>('<')) {
+ return aligned_left;
+ }
+ if (c == detail::ascii_widen<char_type>('>')) {
+ return aligned_right;
+ }
+ if (c == detail::ascii_widen<char_type>('^')) {
+ return aligned_center;
+ }
+ return common_options_none;
+ };
+ auto parse_align = [&](common_options_type align, char_type fill) {
+ if (align != common_options_none) {
+ common_options |= align;
+ }
+ fill_char = static_cast<char32_t>(fill);
+ };
+
+ // align and fill
+ common_options_type align{};
+ bool align_set = false;
+ if (pctx.chars_left() > 1 &&
+ ch != detail::ascii_widen<char_type>('[')) {
+ const auto peek = pctx.peek_char();
+ align = get_align_char(peek);
+ if (align != common_options_none) {
+ // Arg is like "{:_x}", where _ is some fill character, and
+ // x is an alignment flag
+ // -> we have both alignment and fill
+ parse_align(align, ch);
+
+ auto e = next_char();
+ SCN_ENSURE(e);
+ if (!next_char()) {
+ return {};
+ }
+ align_set = true;
+ }
+ }
+ if (!align_set) {
+ align = get_align_char(ch);
+ if (align != common_options_none) {
+ // Arg is like "{:x}", where x is an alignment flag
+ // -> we have alignment with default fill (space ' ')
+ parse_align(align, detail::ascii_widen<char_type>(' '));
+ if (!next_char()) {
+ return {};
+ }
+ }
+ }
+
+ // digit -> width
+ if (pctx.locale().get_static().is_digit(ch)) {
+ common_options |= width_set;
+
+ size_t w{};
+ auto e = parse_number(w);
+ if (!e) {
+ return e;
+ }
+ field_width = w;
+ return {};
+ }
+ // L -> localized
+ if (ch == detail::ascii_widen<char_type>('L')) {
+ common_options |= localized;
+
+ if (!next_char()) {
+ return {};
+ }
+ }
+
+ return {};
+ }
+
+ /**
+ * Parse argument end.
+ *
+ * Returns `error::invalid_format_string` if argument end was not found.
+ */
+ template <typename ParseCtx>
+ error parse_common_end(ParseCtx& pctx)
+ {
+ if (!pctx || !pctx.check_arg_end()) {
+ return {error::invalid_format_string, "Expected argument end"};
+ }
+
+ pctx.arg_end();
+ return {};
+ }
+
+ /**
+ * A null callback to pass to `parse_common`, doing nothing and
+ * returning `error::good`.
+ */
+ template <typename ParseCtx>
+ static error null_type_cb(ParseCtx&, bool&)
+ {
+ return {};
+ }
+
+ public:
+ /**
+ * Parse a format string argument, using `parse_common_begin`,
+ * `parse_common_flags`, `parse_common_end`, and the supplied type
+ * flags.
+ *
+ * `type_options.size() == type_flags.size()` must be `true`.
+ * `pctx` must be valid, and must start at the format string argument
+ * specifiers, e.g. in the case of `"{1:foo}"` -> `pctx == "foo}"`
+ *
+ * \param pctx Format string to parse
+ * \param type_options A span of characters, where each character
+ * corresponds to a valid type flag. For example, for characters, this
+ * span would be \c ['c']
+ * \param type_flags A span of bools, where the values will be set to
+ * `true`, if a corresponding type flag from `type_options` was found.
+ * Should be initialized to all-`false`, as a `false` value will not be
+ * written.
+ * \param type_cb A callback to call, if none of the `type_options`
+ * matched. Must have the signature `(ParseCtx& pctx, bool& parsed) ->
+ * error`., where `parsed` is set to `true`, if the flag at
+ * `pctx.next_char()` was parsed and advanced past.
+ */
+ template <typename ParseCtx,
+ typename F,
+ typename CharT = typename ParseCtx::char_type>
+ error parse_common(ParseCtx& pctx,
+ span<const CharT> type_options,
+ span<bool> type_flags,
+ F&& type_cb)
+ {
+ SCN_EXPECT(type_options.size() == type_flags.size());
+
+ auto e = parse_common_begin(pctx);
+ if (!e) {
+ return e;
+ }
+
+ if (!pctx) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string"};
+ }
+ if (pctx.check_arg_end()) {
+ return {};
+ }
+
+ e = parse_common_flags(pctx);
+ if (!e) {
+ return e;
+ }
+
+ if (!pctx) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string"};
+ }
+ if (pctx.check_arg_end()) {
+ return {};
+ }
+
+ for (auto ch = pctx.next_char(); pctx && !pctx.check_arg_end();
+ ch = pctx.next_char()) {
+ bool parsed = false;
+ for (std::size_t i = 0; i < type_options.size() && !parsed;
+ ++i) {
+ if (ch == type_options[i]) {
+ if (SCN_UNLIKELY(type_flags[i])) {
+ return {error::invalid_format_string,
+ "Repeat flag in format string"};
+ }
+ type_flags[i] = true;
+ parsed = true;
+ }
+ }
+ if (parsed) {
+ pctx.advance_char();
+ if (!pctx || pctx.check_arg_end()) {
+ break;
+ }
+ continue;
+ }
+
+ e = type_cb(pctx, parsed);
+ if (!e) {
+ return e;
+ }
+ if (parsed) {
+ if (!pctx || pctx.check_arg_end()) {
+ break;
+ }
+ continue;
+ }
+ ch = pctx.next_char();
+
+ if (!parsed) {
+ return {error::invalid_format_string,
+ "Invalid character in format string"};
+ }
+ if (!pctx || pctx.check_arg_end()) {
+ break;
+ }
+ }
+
+ return parse_common_end(pctx);
+ }
+
+ void make_localized()
+ {
+ common_options |= localized;
+ }
+
+ /**
+ * Invoke `parse_common()` with default options (no type flags)
+ */
+ template <typename ParseCtx>
+ error parse_default(ParseCtx& pctx)
+ {
+ return parse_common(pctx, {}, {}, null_type_cb<ParseCtx>);
+ }
+
+ constexpr bool is_aligned_left() const noexcept
+ {
+ return (common_options & aligned_left) != 0 ||
+ (common_options & aligned_center) != 0;
+ }
+ constexpr bool is_aligned_right() const noexcept
+ {
+ return (common_options & aligned_right) != 0 ||
+ (common_options & aligned_center) != 0;
+ }
+ template <typename CharT>
+ constexpr CharT get_fill_char() const noexcept
+ {
+ return static_cast<CharT>(fill_char);
+ }
+
+ size_t field_width{0};
+ char32_t fill_char{0};
+ enum common_options_type : uint8_t {
+ common_options_none = 0,
+ localized = 1, // 'L',
+ aligned_left = 2, // '<'
+ aligned_right = 4, // '>'
+ aligned_center = 8, // '^'
+ width_set = 16, // width
+ common_options_all = 31,
+ };
+ uint8_t common_options{0};
+ };
+
+ /**
+ * Derives from `common_parser`, and implements `parse()` with
+ * `parse_default()`
+ */
+ struct common_parser_default : common_parser {
+ template <typename ParseCtx>
+ error parse(ParseCtx& pctx)
+ {
+ return parse_default(pctx);
+ }
+ };
+
+ namespace detail {
+ template <typename Context,
+ typename std::enable_if<
+ !Context::range_type::is_contiguous>::type* = nullptr>
+ error scan_alignment(Context& ctx,
+ typename Context::char_type fill) noexcept
+ {
+ while (true) {
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+
+ auto ch = read_code_unit(ctx.range());
+ if (SCN_UNLIKELY(!ch)) {
+ return ch.error();
+ }
+ if (ch.value() != fill) {
+ auto pb = putback_n(ctx.range(), 1);
+ if (SCN_UNLIKELY(!pb)) {
+ return pb;
+ }
+ break;
+ }
+
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+ return {};
+ }
+ template <typename Context,
+ typename std::enable_if<
+ Context::range_type::is_contiguous>::type* = nullptr>
+ error scan_alignment(Context& ctx,
+ typename Context::char_type fill) noexcept
+ {
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ const auto end = ctx.range().end();
+ for (auto it = ctx.range().begin(); it != end; ++it) {
+ if (*it != fill) {
+ ctx.range().advance_to(it);
+ return {};
+ }
+ }
+ ctx.range().advance_to(end);
+ return {};
+
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+
+ template <typename Scanner, typename = void>
+ struct scanner_supports_alignment : std::false_type {
+ };
+ template <typename Scanner>
+ struct scanner_supports_alignment<
+ Scanner,
+ typename std::enable_if<Scanner::support_align_and_fill()>::type>
+ : std::true_type {
+ };
+
+ template <typename Context, typename Scanner>
+ error skip_alignment(Context& ctx,
+ Scanner& scanner,
+ bool left,
+ std::true_type)
+ {
+ if (left && !scanner.is_aligned_left()) {
+ return {};
+ }
+ if (!left && !scanner.is_aligned_right()) {
+ return {};
+ }
+ return scan_alignment(
+ ctx,
+ scanner.template get_fill_char<typename Context::char_type>());
+ }
+ template <typename Context, typename Scanner>
+ error skip_alignment(Context&, Scanner&, bool, std::false_type)
+ {
+ return {};
+ }
+
+ /**
+ * Scan argument in `val`, from `ctx`, using `Scanner` and `pctx`.
+ *
+ * Parses `pctx` for `Scanner`, skips whitespace and alignment if
+ * necessary, and scans the argument into `val`.
+ */
+ template <typename Scanner,
+ typename T,
+ typename Context,
+ typename ParseCtx>
+ error visitor_boilerplate(T& val, Context& ctx, ParseCtx& pctx)
+ {
+ Scanner scanner;
+
+ auto err = pctx.parse(scanner);
+ if (!err) {
+ return err;
+ }
+
+ if (scanner.skip_preceding_whitespace()) {
+ err = skip_range_whitespace(ctx, false);
+ if (!err) {
+ return err;
+ }
+ }
+
+ err = skip_alignment(ctx, scanner, false,
+ scanner_supports_alignment<Scanner>{});
+ if (!err) {
+ return err;
+ }
+
+ err = scanner.scan(val, ctx);
+ if (!err) {
+ return err;
+ }
+
+ return skip_alignment(ctx, scanner, true,
+ scanner_supports_alignment<Scanner>{});
+ }
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/reader/float.h b/src/third-party/scnlib/include/scn/reader/float.h
new file mode 100644
index 0000000..24265a1
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/reader/float.h
@@ -0,0 +1,246 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_READER_FLOAT_H
+#define SCN_READER_FLOAT_H
+
+#include "../util/small_vector.h"
+#include "common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+ namespace detail {
+ template <typename T>
+ struct float_scanner_access;
+
+ template <typename T>
+ struct float_scanner : common_parser {
+ static_assert(std::is_floating_point<T>::value,
+ "float_scanner requires a floating point type");
+
+ friend struct float_scanner_access<T>;
+
+ template <typename ParseCtx>
+ error parse(ParseCtx& pctx)
+ {
+ using char_type = typename ParseCtx::char_type;
+
+ array<char_type, 10> options{
+ {// hex
+ ascii_widen<char_type>('a'), ascii_widen<char_type>('A'),
+ // scientific
+ ascii_widen<char_type>('e'), ascii_widen<char_type>('E'),
+ // fixed
+ ascii_widen<char_type>('f'), ascii_widen<char_type>('F'),
+ // general
+ ascii_widen<char_type>('g'), ascii_widen<char_type>('G'),
+ // localized digits
+ ascii_widen<char_type>('n'),
+ // thsep
+ ascii_widen<char_type>('\'')}};
+ bool flags[10] = {false};
+
+ auto e = parse_common(
+ pctx, span<const char_type>{options.begin(), options.end()},
+ span<bool>{flags, 10}, null_type_cb<ParseCtx>);
+ if (!e) {
+ return e;
+ }
+
+ if (flags[0] && flags[1]) {
+ return {error::invalid_format_string,
+ "Can't have both 'a' and 'A' flags with floats"};
+ }
+ if (flags[2] && flags[3]) {
+ return {error::invalid_format_string,
+ "Can't have both 'e' and 'E' flags with floats"};
+ }
+ if (flags[4] && flags[5]) {
+ return {error::invalid_format_string,
+ "Can't have both 'f' and 'F' flags with floats"};
+ }
+ if (flags[6] && flags[7]) {
+ return {error::invalid_format_string,
+ "Can't have both 'g' and 'G' flags with floats"};
+ }
+
+ bool set_hex = flags[0] || flags[1];
+ bool set_scientific = flags[2] || flags[3];
+ bool set_fixed = flags[4] || flags[5];
+ bool set_general = flags[6] || flags[7];
+ if (set_general && set_fixed) {
+ return {error::invalid_format_string,
+ "General float already implies fixed"};
+ }
+ if (set_general && set_scientific) {
+ return {error::invalid_format_string,
+ "General float already implies scientific"};
+ }
+
+ format_options = 0;
+ if (set_hex) {
+ format_options |= allow_hex;
+ }
+ if (set_scientific) {
+ format_options |= allow_scientific;
+ }
+ if (set_fixed) {
+ format_options |= allow_fixed;
+ }
+ if (set_general) {
+ format_options |= allow_fixed | allow_scientific;
+ }
+ if (format_options == 0) {
+ format_options |=
+ allow_fixed | allow_scientific | allow_hex;
+ }
+
+ // 'n'
+ if (flags[8]) {
+ common_options |= localized;
+ format_options |= localized_digits;
+ }
+
+ // thsep
+ if (flags[9]) {
+ format_options |= allow_thsep;
+ }
+
+ return {};
+ }
+
+ template <typename Context>
+ error scan(T& val, Context& ctx)
+ {
+ using char_type = typename Context::char_type;
+
+ auto do_parse_float = [&](span<const char_type> s) -> error {
+ T tmp = 0;
+ expected<std::ptrdiff_t> ret{0};
+ if (SCN_UNLIKELY((format_options & localized_digits) != 0 ||
+ ((common_options & localized) != 0 &&
+ (format_options & allow_hex) != 0))) {
+ // 'n' OR ('L' AND 'a')
+ // because none of our parsers support BOTH hexfloats
+ // and custom (localized) decimal points,
+ // so we have to fall back on iostreams
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ std::basic_string<char_type> str(s.data(), s.size());
+ ret =
+ ctx.locale().get_localized().read_num(tmp, str, 0);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+ else {
+ ret = _read_float(
+ tmp, s,
+ ctx.locale()
+ .get((common_options & localized) != 0)
+ .decimal_point());
+ }
+
+ if (!ret) {
+ return ret.error();
+ }
+ if (ret.value() != s.ssize()) {
+ auto pb =
+ putback_n(ctx.range(), s.ssize() - ret.value());
+ if (!pb) {
+ return pb;
+ }
+ }
+ val = tmp;
+ return {};
+ };
+
+ auto is_space_pred = make_is_space_predicate(
+ ctx.locale(), (common_options & localized) != 0,
+ field_width);
+
+ if (Context::range_type::is_contiguous) {
+ auto s = read_until_space_zero_copy(ctx.range(),
+ is_space_pred, false);
+ if (!s) {
+ return s.error();
+ }
+ return do_parse_float(s.value());
+ }
+
+ small_vector<char_type, 32> buf;
+ auto outputit = std::back_inserter(buf);
+ auto e = read_until_space(ctx.range(), outputit, is_space_pred,
+ false);
+ if (!e && buf.empty()) {
+ return e;
+ }
+
+ return do_parse_float(make_span(buf));
+ }
+
+ enum format_options_type {
+ allow_hex = 1,
+ allow_scientific = 2,
+ allow_fixed = 4,
+ localized_digits = 8,
+ allow_thsep = 16
+ };
+ uint8_t format_options{allow_hex | allow_scientific | allow_fixed};
+
+ private:
+ template <typename CharT>
+ expected<std::ptrdiff_t> _read_float(T& val,
+ span<const CharT> s,
+ CharT locale_decimal_point)
+ {
+ size_t chars{};
+ std::basic_string<CharT> str(s.data(), s.size());
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ auto ret =
+ _read_float_impl(str.data(), chars, locale_decimal_point);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ if (!ret) {
+ return ret.error();
+ }
+ val = ret.value();
+ return static_cast<std::ptrdiff_t>(chars);
+ }
+
+ template <typename CharT>
+ expected<T> _read_float_impl(const CharT* str,
+ size_t& chars,
+ CharT locale_decimal_point);
+ };
+
+ // instantiate
+ template struct float_scanner<float>;
+ template struct float_scanner<double>;
+ template struct float_scanner<long double>;
+
+ template <typename T>
+ struct float_scanner_access : public float_scanner<T> {
+ using float_scanner<T>::_read_float;
+ using float_scanner<T>::_read_float_impl;
+ };
+ } // namespace detail
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY && \
+ !defined(SCN_READER_FLOAT_CPP)
+#include "reader_float.cpp"
+#endif
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/reader/int.h b/src/third-party/scnlib/include/scn/reader/int.h
new file mode 100644
index 0000000..19bac44
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/reader/int.h
@@ -0,0 +1,537 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_READER_INT_H
+#define SCN_READER_INT_H
+
+#include "../util/math.h"
+#include "common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename T>
+ struct integer_scanner : common_parser {
+ static_assert(std::is_integral<T>::value,
+ "integer_scanner requires an integral type");
+
+ friend struct simple_integer_scanner<T>;
+
+ bool skip_preceding_whitespace()
+ {
+ // if format_options == single_code_unit,
+ // then we're scanning a char -> don't skip
+ return format_options != single_code_unit;
+ }
+
+ template <typename ParseCtx>
+ error parse(ParseCtx& pctx)
+ {
+ using char_type = typename ParseCtx::char_type;
+
+ format_options = 0;
+
+ int custom_base = 0;
+ auto each = [&](ParseCtx& p, bool& parsed) -> error {
+ parsed = false;
+ auto ch = pctx.next_char();
+
+ if (ch == detail::ascii_widen<char_type>('B')) {
+ // Custom base
+ p.advance_char();
+ if (SCN_UNLIKELY(!p)) {
+ return {error::invalid_format_string,
+ "Unexpected format string end"};
+ }
+ if (SCN_UNLIKELY(p.check_arg_end())) {
+ return {error::invalid_format_string,
+ "Unexpected argument end"};
+ }
+ ch = p.next_char();
+
+ const auto zero = detail::ascii_widen<char_type>('0'),
+ nine = detail::ascii_widen<char_type>('9');
+ integer_type_for_char<char_type> tmp = 0;
+ if (ch < zero || ch > nine) {
+ return {error::invalid_format_string,
+ "Invalid character after 'B', "
+ "expected digit"};
+ }
+ tmp = static_cast<integer_type_for_char<char_type>>(
+ p.next_char() - zero);
+ if (tmp < 1) {
+ return {error::invalid_format_string,
+ "Invalid base, must be between 2 and 36"};
+ }
+
+ p.advance_char();
+ if (!p) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string"};
+ }
+ if (p.check_arg_end()) {
+ custom_base = static_cast<uint8_t>(tmp);
+ parsed = true;
+ return {};
+ }
+ ch = p.next_char();
+
+ if (ch < zero || ch > nine) {
+ return {error::invalid_format_string,
+ "Invalid character after 'B', "
+ "expected digit"};
+ }
+ tmp *= 10;
+ tmp += static_cast<integer_type_for_char<char_type>>(
+ ch - zero);
+ if (tmp < 2 || tmp > 36) {
+ return {error::invalid_format_string,
+ "Invalid base, must be between 2 and 36"};
+ }
+ custom_base = static_cast<uint8_t>(tmp);
+ parsed = true;
+ pctx.advance_char();
+ return {};
+ }
+
+ return {};
+ };
+
+ array<char_type, 9> options{{// decimal
+ ascii_widen<char_type>('d'),
+ // binary
+ ascii_widen<char_type>('b'),
+ // octal
+ ascii_widen<char_type>('o'),
+ // hex
+ ascii_widen<char_type>('x'),
+ // detect base
+ ascii_widen<char_type>('i'),
+ // unsigned decimal
+ ascii_widen<char_type>('u'),
+ // code unit
+ ascii_widen<char_type>('c'),
+ // localized digits
+ ascii_widen<char_type>('n'),
+ // thsep
+ ascii_widen<char_type>('\'')}};
+ bool flags[9] = {false};
+
+ auto e = parse_common(
+ pctx, span<const char_type>{options.begin(), options.end()},
+ span<bool>{flags, 9}, each);
+ if (!e) {
+ return e;
+ }
+
+ int base_flags_set = int(flags[0]) + int(flags[1]) +
+ int(flags[2]) + int(flags[3]) +
+ int(flags[4]) + int(flags[5]) +
+ int(custom_base != 0);
+ if (SCN_UNLIKELY(base_flags_set > 1)) {
+ return {error::invalid_format_string,
+ "Up to one base flags ('d', 'i', 'u', 'b', 'o', "
+ "'x', 'B') allowed"};
+ }
+ else if (base_flags_set == 0) {
+ // Default:
+ // 'c' for CharT
+ // 'd' otherwise
+ if (std::is_same<T, typename ParseCtx::char_type>::value) {
+ format_options = single_code_unit;
+ }
+ else {
+ base = 10;
+ }
+ }
+ else if (custom_base != 0) {
+ // B__
+ base = static_cast<uint8_t>(custom_base);
+ }
+ else if (flags[0]) {
+ // 'd' flag
+ base = 10;
+ }
+ else if (flags[1]) {
+ // 'b' flag
+ base = 2;
+ format_options |= allow_base_prefix;
+ }
+ else if (flags[2]) {
+ // 'o' flag
+ base = 8;
+ format_options |= allow_base_prefix;
+ }
+ else if (flags[3]) {
+ // 'x' flag
+ base = 16;
+ format_options |= allow_base_prefix;
+ }
+ else if (flags[4]) {
+ // 'i' flag
+ base = 0;
+ }
+ else if (flags[5]) {
+ // 'u' flag
+ base = 10;
+ format_options |= only_unsigned;
+ }
+
+ // n set, implies L
+ if (flags[7]) {
+ common_options |= localized;
+ format_options |= localized_digits;
+ }
+ if ((format_options & localized_digits) != 0 &&
+ (base != 0 && base != 10 && base != 8 && base != 16)) {
+ return {error::invalid_format_string,
+ "Localized integers can only be scanned in "
+ "bases 8, 10 and 16"};
+ }
+
+ // thsep flag
+ if (flags[8]) {
+ format_options |= allow_thsep;
+ }
+
+ // 'c' flag -> no other options allowed
+ if (flags[6]) {
+ if (!(format_options == 0 ||
+ format_options == single_code_unit) ||
+ base_flags_set != 0) {
+ return {error::invalid_format_string,
+ "'c' flag cannot be used in conjunction with "
+ "any other flags"};
+ }
+ format_options = single_code_unit;
+ }
+
+ return {};
+ }
+
+ template <typename Context>
+ error scan(T& val, Context& ctx)
+ {
+ using char_type = typename Context::char_type;
+ auto do_parse_int = [&](span<const char_type> s) -> error {
+ T tmp = 0;
+ expected<std::ptrdiff_t> ret{0};
+ if (SCN_UNLIKELY((format_options & localized_digits) !=
+ 0)) {
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ int b{base};
+ auto r = parse_base_prefix<char_type>(s, b);
+ if (!r) {
+ return r.error();
+ }
+ if (b == -1) {
+ // -1 means we read a '0'
+ tmp = 0;
+ return {};
+ }
+ if (b != 10 && base != b && base != 0) {
+ return {error::invalid_scanned_value,
+ "Invalid base prefix"};
+ }
+ if (base == 0) {
+ base = static_cast<uint8_t>(b);
+ }
+ if (base != 8 && base != 10 && base != 16) {
+ return {error::invalid_scanned_value,
+ "Localized values have to be in base "
+ "8, 10 or 16"};
+ }
+
+ auto it = r.value();
+ std::basic_string<char_type> str(to_address(it),
+ s.size());
+ ret = ctx.locale().get_localized().read_num(
+ tmp, str, static_cast<int>(base));
+
+ if (tmp < T{0} &&
+ (format_options & only_unsigned) != 0) {
+ return {error::invalid_scanned_value,
+ "Parsed negative value when type was 'u'"};
+ }
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+ else {
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ ret = _parse_int(tmp, s);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+
+ if (!ret) {
+ return ret.error();
+ }
+ if (ret.value() != s.ssize()) {
+ auto pb =
+ putback_n(ctx.range(), s.ssize() - ret.value());
+ if (!pb) {
+ return pb;
+ }
+ }
+ val = tmp;
+ return {};
+ };
+
+ if (format_options == single_code_unit) {
+ SCN_MSVC_PUSH
+ SCN_MSVC_IGNORE(4127) // conditional expression is constant
+ if (sizeof(T) < sizeof(char_type)) {
+ // sizeof(char_type) > 1 -> wide range
+ // Code unit might not fit
+ return error{error::invalid_operation,
+ "Cannot read this type as a code unit "
+ "from a wide range"};
+ }
+ SCN_MSVC_POP
+ auto ch = read_code_unit(ctx.range());
+ if (!ch) {
+ return ch.error();
+ }
+ val = static_cast<T>(ch.value());
+ return {};
+ }
+
+ SCN_MSVC_PUSH
+ SCN_MSVC_IGNORE(4127) // conditional expression is constant
+ if ((std::is_same<T, char>::value ||
+ std::is_same<T, wchar_t>::value) &&
+ !std::is_same<T, char_type>::value) {
+ // T is a character type, but not char_type:
+ // Trying to read a char from a wide range, or wchar_t from
+ // a narrow one
+ // Reading a code unit is allowed, however
+ return error{error::invalid_operation,
+ "Cannot read a char from a wide range, or a "
+ "wchar_t from a narrow one"};
+ }
+ SCN_MSVC_POP
+
+ std::basic_string<char_type> buf{};
+ span<const char_type> bufspan{};
+ auto e = _read_source(
+ ctx, buf, bufspan,
+ std::integral_constant<
+ bool, Context::range_type::is_contiguous>{});
+ if (!e) {
+ return e;
+ }
+
+ return do_parse_int(bufspan);
+ }
+
+ enum format_options_type : uint8_t {
+ // "n" option -> localized digits and digit grouping
+ localized_digits = 1,
+ // "'" option -> accept thsep
+ // if "L" use locale, default=','
+ allow_thsep = 2,
+ // "u" option -> don't allow sign
+ only_unsigned = 4,
+ // Allow base prefix (e.g. 0B and 0x)
+ allow_base_prefix = 8,
+ // "c" option -> scan a code unit
+ single_code_unit = 16,
+ };
+ uint8_t format_options{default_format_options()};
+
+ // 0 = detect base
+ // Otherwise [2,36]
+ uint8_t base{0};
+
+ private:
+ static SCN_CONSTEXPR14 uint8_t default_format_options()
+ {
+ SCN_MSVC_PUSH
+ SCN_MSVC_IGNORE(4127) // conditional expression is constant
+ if (std::is_same<T, char>::value ||
+ std::is_same<T, wchar_t>::value) {
+ return single_code_unit;
+ }
+ return 0;
+ SCN_MSVC_POP
+ }
+
+ template <typename Context, typename Buf, typename CharT>
+ error _read_source(Context& ctx,
+ Buf& buf,
+ span<const CharT>& s,
+ std::false_type)
+ {
+ auto do_read = [&](Buf& b) -> error {
+ auto outputit = std::back_inserter(b);
+ auto is_space_pred = make_is_space_predicate(
+ ctx.locale(), (common_options & localized) != 0,
+ field_width);
+ auto e = read_until_space(ctx.range(), outputit,
+ is_space_pred, false);
+ if (!e && b.empty()) {
+ return e;
+ }
+
+ return {};
+ };
+
+ if (SCN_LIKELY((format_options & allow_thsep) == 0)) {
+ auto e = do_read(buf);
+ if (!e) {
+ return e;
+ }
+ s = make_span(buf.data(), buf.size());
+ return {};
+ }
+
+ Buf tmp;
+ auto e = do_read(tmp);
+ if (!e) {
+ return e;
+ }
+ auto thsep = ctx.locale()
+ .get((common_options & localized) != 0)
+ .thousands_separator();
+
+ auto it = tmp.begin();
+ for (; it != tmp.end(); ++it) {
+ if (*it == thsep) {
+ for (auto it2 = it; ++it2 != tmp.end();) {
+ *it++ = SCN_MOVE(*it2);
+ }
+ break;
+ }
+ }
+
+ auto n =
+ static_cast<std::size_t>(std::distance(tmp.begin(), it));
+ if (n == 0) {
+ return {error::invalid_scanned_value,
+ "Only a thousands separator found"};
+ }
+
+ buf = SCN_MOVE(tmp);
+ s = make_span(buf.data(), n);
+ return {};
+ }
+
+ template <typename Context, typename Buf, typename CharT>
+ error _read_source(Context& ctx,
+ Buf& buf,
+ span<const CharT>& s,
+ std::true_type)
+ {
+ if (SCN_UNLIKELY((format_options & allow_thsep) != 0)) {
+ return _read_source(ctx, buf, s, std::false_type{});
+ }
+ auto ret = read_zero_copy(
+ ctx.range(), field_width != 0
+ ? static_cast<std::ptrdiff_t>(field_width)
+ : ctx.range().size());
+ if (!ret) {
+ return ret.error();
+ }
+ s = ret.value();
+ return {};
+ }
+
+ template <typename CharT>
+ expected<typename span<const CharT>::iterator> parse_base_prefix(
+ span<const CharT> s,
+ int& b) const;
+
+ template <typename CharT>
+ expected<std::ptrdiff_t> _parse_int(T& val, span<const CharT> s);
+
+ template <typename CharT>
+ expected<typename span<const CharT>::iterator> _parse_int_impl(
+ T& val,
+ bool minus_sign,
+ span<const CharT> buf) const;
+ };
+
+ // instantiate
+ template struct integer_scanner<signed char>;
+ template struct integer_scanner<short>;
+ template struct integer_scanner<int>;
+ template struct integer_scanner<long>;
+ template struct integer_scanner<long long>;
+ template struct integer_scanner<unsigned char>;
+ template struct integer_scanner<unsigned short>;
+ template struct integer_scanner<unsigned int>;
+ template struct integer_scanner<unsigned long>;
+ template struct integer_scanner<unsigned long long>;
+ template struct integer_scanner<char>;
+ template struct integer_scanner<wchar_t>;
+
+ template <typename T>
+ template <typename CharT>
+ expected<typename span<const CharT>::iterator>
+ simple_integer_scanner<T>::scan(span<const CharT> buf,
+ T& val,
+ int base,
+ uint16_t flags)
+ {
+ SCN_EXPECT(buf.size() != 0);
+
+ integer_scanner<T> s{};
+ s.base = static_cast<uint8_t>(base);
+ s.format_options = flags & 0xffu;
+ s.common_options = static_cast<uint8_t>(flags >> 8u);
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ auto n = s._parse_int(val, buf);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ if (!n) {
+ return n.error();
+ }
+ return buf.begin() + n.value();
+ }
+ template <typename T>
+ template <typename CharT>
+ expected<typename span<const CharT>::iterator>
+ simple_integer_scanner<T>::scan_lower(span<const CharT> buf,
+ T& val,
+ int base,
+ uint16_t flags)
+ {
+ SCN_EXPECT(buf.size() != 0);
+ SCN_EXPECT(base > 0);
+
+ integer_scanner<T> s{};
+ s.base = static_cast<uint8_t>(base);
+ s.format_options = flags & 0xffu;
+ s.common_options = static_cast<uint8_t>(flags >> 8u);
+
+ bool minus_sign = false;
+ if (buf[0] == ascii_widen<CharT>('-')) {
+ buf = buf.subspan(1);
+ minus_sign = true;
+ }
+
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ return s._parse_int_impl(val, minus_sign, buf);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+ } // namespace detail
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY && !defined(SCN_READER_INT_CPP)
+#include "reader_int.cpp"
+#endif
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/reader/reader.h b/src/third-party/scnlib/include/scn/reader/reader.h
new file mode 100644
index 0000000..cd955ce
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/reader/reader.h
@@ -0,0 +1,111 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_READER_READER_H
+#define SCN_READER_READER_H
+
+#include "common.h"
+#include "float.h"
+#include "int.h"
+#include "string.h"
+#include "types.h"
+
+#include "../detail/args.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ template <>
+ struct scanner<code_point> : public detail::code_point_scanner {
+ };
+ template <>
+ struct scanner<bool> : public detail::bool_scanner {
+ };
+ template <>
+ struct scanner<char> : public detail::integer_scanner<char> {
+ };
+ template <>
+ struct scanner<wchar_t> : public detail::integer_scanner<wchar_t> {
+ };
+ template <>
+ struct scanner<signed char> : public detail::integer_scanner<signed char> {
+ };
+ template <>
+ struct scanner<short> : public detail::integer_scanner<short> {
+ };
+ template <>
+ struct scanner<int> : public detail::integer_scanner<int> {
+ };
+ template <>
+ struct scanner<long> : public detail::integer_scanner<long> {
+ };
+ template <>
+ struct scanner<long long> : public detail::integer_scanner<long long> {
+ };
+ template <>
+ struct scanner<unsigned char>
+ : public detail::integer_scanner<unsigned char> {
+ };
+ template <>
+ struct scanner<unsigned short>
+ : public detail::integer_scanner<unsigned short> {
+ };
+ template <>
+ struct scanner<unsigned int>
+ : public detail::integer_scanner<unsigned int> {
+ };
+ template <>
+ struct scanner<unsigned long>
+ : public detail::integer_scanner<unsigned long> {
+ };
+ template <>
+ struct scanner<unsigned long long>
+ : public detail::integer_scanner<unsigned long long> {
+ };
+ template <>
+ struct scanner<float> : public detail::float_scanner<float> {
+ };
+ template <>
+ struct scanner<double> : public detail::float_scanner<double> {
+ };
+ template <>
+ struct scanner<long double> : public detail::float_scanner<long double> {
+ };
+ template <typename CharT, typename Allocator>
+ struct scanner<std::basic_string<CharT, std::char_traits<CharT>, Allocator>>
+ : public detail::string_scanner {
+ };
+ template <typename CharT>
+ struct scanner<span<CharT>> : public detail::span_scanner {
+ };
+ template <typename CharT>
+ struct scanner<basic_string_view<CharT>>
+ : public detail::string_view_scanner {
+ };
+#if SCN_HAS_STRING_VIEW
+ template <typename CharT>
+ struct scanner<std::basic_string_view<CharT>>
+ : public detail::std_string_view_scanner {
+ };
+#endif
+ template <>
+ struct scanner<detail::monostate>;
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/reader/string.h b/src/third-party/scnlib/include/scn/reader/string.h
new file mode 100644
index 0000000..19727ee
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/reader/string.h
@@ -0,0 +1,1336 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_READER_STRING_H
+#define SCN_READER_STRING_H
+
+#include "../util/small_vector.h"
+#include "common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+ namespace detail {
+ class set_parser_type {
+ public:
+ constexpr set_parser_type() = default;
+
+ template <typename ParseCtx>
+ error parse_set(ParseCtx& pctx, bool& parsed)
+ {
+ using char_type = typename ParseCtx::char_type;
+ SCN_EXPECT(pctx.next_char() == ascii_widen<char_type>('['));
+
+ pctx.advance_char();
+ if (!pctx || pctx.check_arg_end()) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string argument"};
+ }
+
+ get_option(flag::enabled) = true;
+ parsed = true;
+
+ if (pctx.next_char() == ascii_widen<char_type>('^')) {
+ // inverted
+ get_option(flag::inverted) = true;
+ pctx.advance_char();
+ if (!pctx || pctx.check_arg_end()) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string argument"};
+ }
+ }
+
+ if (pctx.next_char() == ascii_widen<char_type>(']')) {
+ // end of range
+ get_option(flag::accept_all) = true;
+ pctx.advance_char();
+ return {};
+ }
+
+ while (true) {
+ if (!pctx || pctx.check_arg_end()) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string argument"};
+ }
+
+ const auto ch = pctx.next_char();
+ if (ch == ascii_widen<char_type>(']')) {
+ break;
+ }
+
+ auto err = parse_next_char(pctx, true);
+ if (!err) {
+ return err;
+ }
+
+ err = pctx.advance_cp();
+ if (!err) {
+ pctx.advance_char();
+ }
+ }
+ auto err = pctx.advance_cp();
+ if (!err) {
+ pctx.advance_char();
+ }
+
+ return {};
+ }
+
+ error sanitize(bool localized)
+ {
+ // specifiers -> chars, if not localized
+ if (get_option(flag::use_specifiers)) {
+ if ((get_option(specifier::letters) ||
+ get_option(specifier::alpha)) &&
+ get_option(specifier::inverted_letters)) {
+ get_option(flag::accept_all) = true;
+ }
+ if (get_option(specifier::alnum_underscore) &&
+ get_option(specifier::inverted_alnum_underscore)) {
+ get_option(flag::accept_all) = true;
+ }
+ if ((get_option(specifier::whitespace) ||
+ get_option(specifier::space)) &&
+ get_option(specifier::inverted_whitespace)) {
+ get_option(flag::accept_all) = true;
+ }
+ if ((get_option(specifier::numbers) ||
+ get_option(specifier::digit)) &&
+ get_option(specifier::inverted_numbers)) {
+ get_option(flag::accept_all) = true;
+ }
+ }
+
+ if (get_option(flag::use_specifiers) &&
+ !get_option(flag::accept_all)) {
+ if (localized) {
+ if (get_option(specifier::letters)) {
+ get_option(specifier::letters) = false;
+ get_option(specifier::alpha) = true;
+ }
+ if (get_option(specifier::alnum_underscore)) {
+ get_option(specifier::alnum_underscore) = false;
+ get_option(specifier::alnum) = true;
+ get_option('_') = true;
+ }
+ if (get_option(specifier::whitespace)) {
+ get_option(specifier::whitespace) = false;
+ get_option(specifier::space) = true;
+ }
+ if (get_option(specifier::numbers)) {
+ get_option(specifier::numbers) = false;
+ get_option(specifier::digit) = true;
+ }
+ }
+ else {
+ auto do_range = [&](char a, char b) {
+ for (; a < b; ++a) {
+ get_option(a) = true;
+ }
+ get_option(b) = true;
+ };
+ auto do_lower = [&]() {
+ // a-z
+ do_range(0x61, 0x7a);
+ };
+ auto do_upper = [&]() {
+ // A-Z
+ do_range(0x41, 0x5a);
+ };
+ auto do_digit = [&]() {
+ // 0-9
+ do_range(0x30, 0x39);
+ };
+
+ if (get_option(specifier::alnum)) {
+ do_lower();
+ do_upper();
+ do_digit();
+ get_option(specifier::alnum) = false;
+ }
+ if (get_option(specifier::alpha)) {
+ do_lower();
+ do_upper();
+ get_option(specifier::alpha) = false;
+ }
+ if (get_option(specifier::blank)) {
+ get_option(' ') = true;
+ get_option('\t') = true;
+ get_option(specifier::blank) = false;
+ }
+ if (get_option(specifier::cntrl)) {
+ do_range(0, 0x1f);
+ get_option(0x7f) = true;
+ get_option(specifier::cntrl) = false;
+ }
+ if (get_option(specifier::digit)) {
+ do_digit();
+ get_option(specifier::digit) = false;
+ }
+ if (get_option(specifier::graph)) {
+ do_range(0x21, 0x7e);
+ get_option(specifier::graph) = false;
+ }
+ if (get_option(specifier::lower)) {
+ do_lower();
+ get_option(specifier::lower) = false;
+ }
+ if (get_option(specifier::print)) {
+ do_range(0x20, 0x7e);
+ get_option(specifier::print) = false;
+ }
+ if (get_option(specifier::punct)) {
+ do_range(0x21, 0x2f);
+ do_range(0x3a, 0x40);
+ do_range(0x5b, 0x60);
+ do_range(0x7b, 0x7e);
+ get_option(specifier::punct) = false;
+ }
+ if (get_option(specifier::space)) {
+ do_range(0x9, 0xd);
+ get_option(' ') = true;
+ get_option(specifier::space) = false;
+ }
+ if (get_option(specifier::upper)) {
+ do_upper();
+ get_option(specifier::upper) = false;
+ }
+ if (get_option(specifier::xdigit)) {
+ do_digit();
+ do_range(0x41, 0x46);
+ do_range(0x61, 0x66);
+ get_option(specifier::xdigit) = false;
+ }
+ if (get_option(specifier::letters)) {
+ do_upper();
+ do_lower();
+ get_option(specifier::letters) = false;
+ }
+ if (get_option(specifier::inverted_letters)) {
+ do_range(0x0, 0x2f);
+ do_range(0x3a, 0x40);
+ do_range(0x5b, 0x60);
+ do_range(0x7b, 0x7f);
+ get_option(specifier::inverted_letters) = false;
+ }
+ if (get_option(specifier::alnum_underscore)) {
+ do_digit();
+ do_upper();
+ do_lower();
+ get_option('_') = true;
+ get_option(specifier::alnum_underscore) = false;
+ }
+ if (get_option(specifier::inverted_alnum_underscore)) {
+ bool underscore = get_option('_');
+ do_range(0x0, 0x2f);
+ do_range(0x3a, 0x40);
+ do_range(0x5b, 0x60);
+ do_range(0x7b, 0x7f);
+ get_option('_') = underscore; // reset back
+ get_option(specifier::inverted_alnum_underscore) =
+ false;
+ }
+ if (get_option(specifier::whitespace)) {
+ do_range(0x9, 0xd);
+ get_option(' ') = true;
+ get_option(specifier::whitespace) = false;
+ }
+ if (get_option(specifier::inverted_whitespace)) {
+ do_range(0, 0x8);
+ do_range(0xe, 0x1f);
+ do_range(0x21, 0x7f);
+ get_option(specifier::inverted_whitespace) = false;
+ }
+ if (get_option(specifier::numbers)) {
+ do_digit();
+ get_option(specifier::numbers) = false;
+ }
+ if (get_option(specifier::inverted_numbers)) {
+ do_range(0, 0x2f);
+ do_range(0x3a, 0x7f);
+ get_option(specifier::inverted_numbers) = false;
+ }
+
+ {
+ bool first = get_option(0);
+ char i = 1;
+ for (; i < 0x7f; ++i) {
+ if (first != get_option(i)) {
+ break;
+ }
+ }
+ if (i == 0x7f && first == get_option(0x7f)) {
+ get_option(flag::accept_all) = true;
+ if (!first) {
+ get_option(flag::inverted) = true;
+ }
+ }
+ }
+
+ get_option(flag::use_specifiers) = false;
+ get_option(flag::use_chars) = true;
+ }
+ }
+
+ return {};
+ }
+
+ // true = char accepted
+ template <typename CharT, typename Locale>
+ bool check_character(CharT ch, bool localized, const Locale& loc)
+ {
+ SCN_EXPECT(get_option(flag::enabled));
+
+ const bool not_inverted = !get_option(flag::inverted);
+ if (get_option(flag::accept_all)) {
+ return not_inverted;
+ }
+
+ if (get_option(flag::use_specifiers)) {
+ SCN_EXPECT(localized); // ensured by sanitize()
+ SCN_UNUSED(localized);
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ if (get_option(specifier::alnum) &&
+ loc.get_localized().is_alnum(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::alpha) &&
+ loc.get_localized().is_alpha(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::blank) &&
+ loc.get_localized().is_blank(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::cntrl) &&
+ loc.get_localized().is_cntrl(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::digit) &&
+ loc.get_localized().is_digit(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::graph) &&
+ loc.get_localized().is_graph(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::lower) &&
+ loc.get_localized().is_lower(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::print) &&
+ loc.get_localized().is_print(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::punct) &&
+ loc.get_localized().is_punct(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::space) &&
+ loc.get_localized().is_space(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::upper) &&
+ loc.get_localized().is_upper(ch)) {
+ return not_inverted;
+ }
+ if (get_option(specifier::xdigit) &&
+ loc.get_localized().is_xdigit(ch)) {
+ return not_inverted;
+ }
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+ if (get_option(flag::use_chars) && (ch >= 0 && ch <= 0x7f)) {
+ if (get_option(static_cast<char>(ch))) {
+ return not_inverted;
+ }
+ }
+ if (get_option(flag::use_ranges)) {
+ const auto c = static_cast<uint32_t>(ch);
+ for (const auto& e : set_extra_ranges) {
+ if (c >= e.begin && c <= e.end) {
+ return not_inverted;
+ }
+ }
+ }
+ return !not_inverted;
+ }
+
+ enum class specifier : size_t {
+ alnum = 0x80,
+ alpha,
+ blank,
+ cntrl,
+ digit,
+ graph,
+ lower,
+ print,
+ punct,
+ space,
+ upper,
+ xdigit,
+ letters = 0x90, // \l
+ inverted_letters, // \L
+ alnum_underscore, // \w
+ inverted_alnum_underscore, // \W
+ whitespace, // \s
+ inverted_whitespace, // \S
+ numbers, // \d
+ inverted_numbers, // \D
+ last = 0x9f
+ };
+ enum class flag : size_t {
+ enabled = 0xa0, // using [set]
+ accept_all, // empty [set]
+ inverted, // ^ flag
+ // 0x00 - 0x7f
+ use_chars,
+ // 0x80 - 0x8f
+ use_specifiers,
+ // set_extra_ranges
+ use_ranges,
+ last = 0xaf
+ };
+
+ bool& get_option(char ch)
+ {
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wtype-limits")
+ SCN_EXPECT(ch >= 0 && ch <= 0x7f);
+ SCN_GCC_POP
+ return set_options[static_cast<size_t>(ch)];
+ }
+ SCN_NODISCARD bool get_option(char ch) const
+ {
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wtype-limits")
+ SCN_EXPECT(ch >= 0 && ch <= 0x7f);
+ SCN_GCC_POP
+ return set_options[static_cast<size_t>(ch)];
+ }
+
+ bool& get_option(specifier s)
+ {
+ return set_options[static_cast<size_t>(s)];
+ }
+ SCN_NODISCARD bool get_option(specifier s) const
+ {
+ return set_options[static_cast<size_t>(s)];
+ }
+
+ bool& get_option(flag f)
+ {
+ return set_options[static_cast<size_t>(f)];
+ }
+ SCN_NODISCARD bool get_option(flag f) const
+ {
+ return set_options[static_cast<size_t>(f)];
+ }
+
+ SCN_NODISCARD bool enabled() const
+ {
+ return get_option(flag::enabled);
+ }
+
+ private:
+ void accept_char(char ch)
+ {
+ get_option(ch) = true;
+ get_option(flag::use_chars) = true;
+ }
+ void accept_char(code_point cp)
+ {
+ if (cp >= 0 && cp <= 0x7f) {
+ return accept_char(static_cast<char>(cp));
+ }
+ set_extra_ranges.push_back(set_range::single(cp));
+ get_option(flag::use_ranges) = true;
+ }
+ void accept_char(wchar_t ch)
+ {
+ SCN_GCC_COMPAT_PUSH
+ SCN_GCC_COMPAT_IGNORE("-Wtype-limits")
+ if (ch >= 0 && ch <= 0x7f) {
+ return accept_char(static_cast<char>(ch));
+ }
+ SCN_GCC_COMPAT_POP
+ set_extra_ranges.push_back(set_range::single(ch));
+ get_option(flag::use_ranges) = true;
+ }
+
+ void accept_char_range(char first, char last)
+ {
+ SCN_EXPECT(first >= 0);
+ SCN_EXPECT(last >= 0);
+ SCN_EXPECT(first <= last);
+ get_option(flag::use_chars) = true;
+ for (; first != last; ++first) {
+ get_option(first) = true;
+ }
+ SCN_ENSURE(first == last);
+ get_option(last) = true;
+ }
+ void accept_char_range(code_point first, code_point last)
+ {
+ SCN_EXPECT(first <= last);
+ if (first >= 0 && last <= 0x7f) {
+ return accept_char_range(static_cast<char>(first),
+ static_cast<char>(last));
+ }
+ set_extra_ranges.push_back(set_range::range(first, last));
+ get_option(flag::use_ranges) = true;
+ }
+ void accept_char_range(wchar_t first, wchar_t last)
+ {
+ SCN_EXPECT(first <= last);
+ SCN_GCC_COMPAT_PUSH
+ SCN_GCC_COMPAT_IGNORE("-Wtype-limits")
+ if (first >= 0 && last <= 0x7f) {
+ return accept_char_range(static_cast<char>(first),
+ static_cast<char>(last));
+ }
+ SCN_GCC_COMPAT_POP
+ set_extra_ranges.push_back(set_range::range(first, last));
+ get_option(flag::use_ranges) = true;
+ }
+
+ template <typename ParseCtx>
+ error parse_range(ParseCtx& pctx, code_point begin)
+ {
+ using char_type = typename ParseCtx::char_type;
+ SCN_EXPECT(pctx.next_char() == ascii_widen<char_type>('-'));
+ if (pctx.can_peek_char() &&
+ pctx.peek_char() == ascii_widen<char_type>(']')) {
+ // Just a '-'
+ accept_char(begin);
+ accept_char(ascii_widen<char_type>('-'));
+ return {};
+ }
+ pctx.advance_char();
+ if (!pctx || pctx.check_arg_end()) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string argument"};
+ }
+ return parse_next_char(pctx, false, begin);
+ }
+ template <typename ParseCtx>
+ error parse_literal(ParseCtx& pctx,
+ bool allow_range,
+ code_point begin = make_code_point(0))
+ {
+ using char_type = typename ParseCtx::char_type;
+ if (allow_range) {
+ auto e = pctx.peek_cp();
+ if (!e && e.error().code() != error::end_of_range) {
+ return e.error();
+ }
+ if (e && e.value() == ascii_widen<char_type>('-')) {
+ const auto cp = pctx.next_cp();
+ if (!cp) {
+ return cp.error();
+ }
+ auto err = pctx.advance_cp();
+ if (!err) {
+ return err;
+ }
+ return parse_range(pctx, cp.value());
+ }
+ }
+ const auto cp = pctx.next_cp();
+ if (!cp) {
+ return cp.error();
+ }
+ if (cp.value() >= 0 && cp.value() <= 0x7f) {
+ if (!allow_range) {
+ if (static_cast<
+ typename std::make_unsigned<char_type>::type>(
+ cp.value()) <
+ static_cast<
+ typename std::make_unsigned<char_type>::type>(
+ begin)) {
+ return {error::invalid_format_string,
+ "Last char in [set] range is less than the "
+ "first"};
+ }
+ accept_char_range(begin, cp.value());
+ }
+ else {
+ accept_char(cp.value());
+ }
+ }
+ else {
+ if (!allow_range) {
+ if (static_cast<
+ typename std::make_unsigned<char_type>::type>(
+ cp.value()) <
+ static_cast<
+ typename std::make_unsigned<char_type>::type>(
+ begin)) {
+ return {error::invalid_format_string,
+ "Last char in [set] range is less than the "
+ "first"};
+ }
+ set_extra_ranges.push_back(
+ set_range::range(begin, cp.value()));
+ }
+ else {
+ set_extra_ranges.push_back(
+ set_range::single(cp.value()));
+ }
+ get_option(flag::use_ranges) = true;
+ }
+ return {};
+ }
+ template <typename ParseCtx>
+ error parse_colon_specifier(ParseCtx& pctx)
+ {
+ using char_type = typename ParseCtx::char_type;
+ SCN_EXPECT(pctx.next_char() == ascii_widen<char_type>(':'));
+ pctx.advance_char();
+ if (!pctx || pctx.check_arg_end()) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string argument"};
+ }
+ if (pctx.next_char() == ascii_widen<char_type>(']')) {
+ return {
+ error::invalid_format_string,
+ "Unexpected end of [set] in format string after ':'"};
+ }
+
+ std::basic_string<char_type> buf;
+ while (true) {
+ if (!pctx || pctx.check_arg_end()) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string argument"};
+ }
+ auto ch = pctx.next_char();
+ if (ch == ascii_widen<char_type>(':')) {
+ break;
+ }
+ if (ch == ascii_widen<char_type>(']')) {
+ return {error::invalid_format_string,
+ "Unexpected end of [set] :specifier:, did you "
+ "forget a terminating colon?"};
+ }
+ buf.push_back(ch);
+ pctx.advance_char();
+ }
+
+ auto ch = pctx.next_char();
+ if (buf == all_str(ch)) {
+ get_option(flag::accept_all) = true;
+ return {};
+ }
+ if (buf == alnum_str(ch)) {
+ get_option(specifier::alnum) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == alpha_str(ch)) {
+ get_option(specifier::alpha) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == blank_str(ch)) {
+ get_option(specifier::blank) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == cntrl_str(ch)) {
+ get_option(specifier::cntrl) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == digit_str(ch)) {
+ get_option(specifier::digit) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == graph_str(ch)) {
+ get_option(specifier::graph) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == lower_str(ch)) {
+ get_option(specifier::lower) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == print_str(ch)) {
+ get_option(specifier::print) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == punct_str(ch)) {
+ get_option(specifier::punct) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == space_str(ch)) {
+ get_option(specifier::space) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == upper_str(ch)) {
+ get_option(specifier::upper) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (buf == xdigit_str(ch)) {
+ get_option(specifier::xdigit) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+
+ return {error::invalid_format_string,
+ "Invalid :specifier: in [set]"};
+ }
+ template <typename ParseCtx>
+ error parse_backslash_hex(ParseCtx& pctx,
+ bool allow_range,
+ code_point begin = make_code_point(0))
+ {
+ using char_type = typename ParseCtx::char_type;
+ SCN_EXPECT(pctx.next_char() == ascii_widen<char_type>('x') ||
+ pctx.next_char() == ascii_widen<char_type>('u') ||
+ pctx.next_char() == ascii_widen<char_type>('U'));
+
+ const char_type flag_char = pctx.next_char();
+ const int chars = [flag_char]() {
+ auto ch = static_cast<char>(flag_char);
+ if (ch == 'x') {
+ return 2;
+ }
+ if (ch == 'u') {
+ return 4;
+ }
+ if (ch == 'U') {
+ return 8;
+ }
+ SCN_ENSURE(false);
+ SCN_UNREACHABLE;
+ }();
+
+ char_type str[8] = {0};
+ for (int i = 0; i < chars; ++i) {
+ pctx.advance_char();
+ if (!pctx || pctx.check_arg_end()) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string argument "
+ "after '\\x', '\\u', or '\\U'"};
+ }
+ if (pctx.next_char() == ascii_widen<char_type>(']')) {
+ return {error::invalid_format_string,
+ "Unexpected end of [set] in format string "
+ "after '\\x', '\\u', or '\\U'"};
+ }
+ str[i] = pctx.next_char();
+ }
+
+ auto scanner = simple_integer_scanner<uint64_t>{};
+ uint64_t i;
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ auto res = scanner.scan(
+ scn::make_span(str, static_cast<size_t>(chars)).as_const(),
+ i, 16);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ if (!res) {
+ return {error::invalid_format_string,
+ "Failed to parse '\\x', '\\u', or '\\U' flag in "
+ "format string"};
+ }
+ const uint64_t min = 0;
+ const uint64_t max = [chars]() {
+ if (chars == 2) {
+ // \x
+ return uint64_t{0x7f};
+ }
+ if (chars == 4) {
+ return uint64_t{0xffff};
+ }
+ if (chars == 8) {
+ return uint64_t{0xffffffff};
+ }
+ SCN_ENSURE(false);
+ SCN_UNREACHABLE;
+ }();
+ if (i < min || i > max) {
+ return {error::invalid_format_string,
+ "'\\x', '\\u', or '\\U' option in format string "
+ "out of range"};
+ }
+
+ if (allow_range && pctx.can_peek_char() &&
+ pctx.peek_char() == ascii_widen<char_type>('-')) {
+ pctx.advance_char();
+ return parse_range(pctx, make_code_point(i));
+ }
+ if (!allow_range) {
+ accept_char_range(begin, make_code_point(i));
+ }
+ else {
+ accept_char(make_code_point(i));
+ }
+ return {};
+ }
+ template <typename ParseCtx>
+ error parse_backslash_specifier(
+ ParseCtx& pctx,
+ bool allow_range,
+ code_point begin = make_code_point(0))
+ {
+ using char_type = typename ParseCtx::char_type;
+ SCN_EXPECT(pctx.next_char() == ascii_widen<char_type>('\\'));
+ pctx.advance_char();
+
+ if (!pctx || pctx.check_arg_end()) {
+ return {error::invalid_format_string,
+ "Unexpected end of format string argument"};
+ }
+ if (pctx.next_char() == ascii_widen<char_type>(']') &&
+ pctx.can_peek_char() &&
+ pctx.peek_char() == ascii_widen<char_type>('}')) {
+ return {error::invalid_format_string,
+ "Unexpected end of [set] in format string"};
+ }
+
+ if (pctx.next_char() == ascii_widen<char_type>('\\')) {
+ // Literal "\\"
+ accept_char(pctx.next_char());
+ return {};
+ }
+
+ // specifiers
+ if (pctx.next_char() == ascii_widen<char_type>('l')) {
+ // \l
+ get_option(specifier::letters) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (pctx.next_char() == ascii_widen<char_type>('L')) {
+ // \L
+ get_option(specifier::inverted_letters) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+
+ if (pctx.next_char() == ascii_widen<char_type>('w')) {
+ // \w
+ get_option(specifier::alnum_underscore) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (pctx.next_char() == ascii_widen<char_type>('W')) {
+ // \W
+ get_option(specifier::inverted_alnum_underscore) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+
+ if (pctx.next_char() == ascii_widen<char_type>('s')) {
+ // \s
+ get_option(specifier::whitespace) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (pctx.next_char() == ascii_widen<char_type>('S')) {
+ // \S
+ get_option(specifier::inverted_whitespace) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+
+ if (pctx.next_char() == ascii_widen<char_type>('d')) {
+ // \d
+ get_option(specifier::numbers) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+ if (pctx.next_char() == ascii_widen<char_type>('D')) {
+ // \D
+ get_option(specifier::inverted_numbers) = true;
+ get_option(flag::use_specifiers) = true;
+ return {};
+ }
+
+ if (pctx.next_char() == ascii_widen<char_type>('x') ||
+ pctx.next_char() == ascii_widen<char_type>('u') ||
+ pctx.next_char() == ascii_widen<char_type>('U')) {
+ // \x__, \u____, or \U________
+ return parse_backslash_hex(pctx, allow_range, begin);
+ }
+
+ // Literal, e.g. \: -> :
+ return parse_literal(pctx, true);
+ }
+ template <typename ParseCtx>
+ error parse_next_char(ParseCtx& pctx,
+ bool allow_range,
+ code_point begin = make_code_point(0))
+ {
+ using char_type = typename ParseCtx::char_type;
+ const auto ch = pctx.next_char();
+ if (ch == ascii_widen<char_type>('\\')) {
+ return parse_backslash_specifier(pctx, allow_range, begin);
+ }
+ if (allow_range && ch == ascii_widen<char_type>(':')) {
+ return parse_colon_specifier(pctx);
+ }
+ return parse_literal(pctx, allow_range, begin);
+ }
+
+ SCN_NODISCARD static constexpr const char* all_str(char)
+ {
+ return "all";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* all_str(wchar_t)
+ {
+ return L"all";
+ }
+ SCN_NODISCARD static constexpr const char* alnum_str(char)
+ {
+ return "alnum";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* alnum_str(wchar_t)
+ {
+ return L"alnum";
+ }
+ SCN_NODISCARD static constexpr const char* alpha_str(char)
+ {
+ return "alpha";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* alpha_str(wchar_t)
+ {
+ return L"alpha";
+ }
+ SCN_NODISCARD static constexpr const char* blank_str(char)
+ {
+ return "blank";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* blank_str(wchar_t)
+ {
+ return L"blank";
+ }
+ SCN_NODISCARD static constexpr const char* cntrl_str(char)
+ {
+ return "cntrl";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* cntrl_str(wchar_t)
+ {
+ return L"cntrl";
+ }
+ SCN_NODISCARD static constexpr const char* digit_str(char)
+ {
+ return "digit";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* digit_str(wchar_t)
+ {
+ return L"digit";
+ }
+ SCN_NODISCARD static constexpr const char* graph_str(char)
+ {
+ return "graph";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* graph_str(wchar_t)
+ {
+ return L"graph";
+ }
+ SCN_NODISCARD static constexpr const char* lower_str(char)
+ {
+ return "lower";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* lower_str(wchar_t)
+ {
+ return L"lower";
+ }
+ SCN_NODISCARD static constexpr const char* print_str(char)
+ {
+ return "print";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* print_str(wchar_t)
+ {
+ return L"print";
+ }
+ SCN_NODISCARD static constexpr const char* punct_str(char)
+ {
+ return "punct";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* punct_str(wchar_t)
+ {
+ return L"punct";
+ }
+ SCN_NODISCARD static constexpr const char* space_str(char)
+ {
+ return "space";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* space_str(wchar_t)
+ {
+ return L"space";
+ }
+ SCN_NODISCARD static constexpr const char* upper_str(char)
+ {
+ return "upper";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* upper_str(wchar_t)
+ {
+ return L"upper";
+ }
+ SCN_NODISCARD static constexpr const char* xdigit_str(char)
+ {
+ return "xdigit";
+ }
+ SCN_NODISCARD static constexpr const wchar_t* xdigit_str(wchar_t)
+ {
+ return L"xdigit";
+ }
+
+ // 0x00 - 0x7f, individual chars, true = accept
+ // 0x80 - 0x9f, specifiers, true = accept (if use_specifiers = true)
+ // 0xa0 - 0xaf, flags
+ array<bool, 0xb0> set_options{{false}};
+
+ struct set_range {
+ constexpr set_range(uint32_t b, uint32_t e) : begin(b), end(e)
+ {
+ }
+
+ uint32_t begin{};
+ uint32_t end{}; // inclusive
+
+ static set_range single(code_point cp)
+ {
+ return {static_cast<uint32_t>(cp),
+ static_cast<uint32_t>(cp)};
+ }
+ static set_range single(wchar_t ch)
+ {
+ return {static_cast<uint32_t>(ch),
+ static_cast<uint32_t>(ch)};
+ }
+
+ static set_range range(code_point begin, code_point end)
+ {
+ SCN_EXPECT(begin <= end);
+ return {static_cast<uint32_t>(begin),
+ static_cast<uint32_t>(end)};
+ }
+ static set_range range(wchar_t begin, wchar_t end)
+ {
+ SCN_EXPECT(begin <= end);
+ return {static_cast<uint32_t>(begin),
+ static_cast<uint32_t>(end)};
+ }
+ };
+ // Used if set_options[use_ranges] = true
+ small_vector<set_range, 1> set_extra_ranges{};
+ };
+
+ struct string_scanner : common_parser {
+ static constexpr bool skip_preceding_whitespace()
+ {
+ return false;
+ }
+
+ template <typename ParseCtx>
+ error parse(ParseCtx& pctx)
+ {
+ using char_type = typename ParseCtx::char_type;
+
+ auto s_flag = detail::ascii_widen<char_type>('s');
+ bool s_set{};
+
+ auto each = [&](ParseCtx& p, bool& parsed) -> error {
+ if (p.next_char() == ascii_widen<char_type>('[')) {
+ if (set_parser.get_option(
+ set_parser_type::flag::enabled)) {
+ return {error::invalid_format_string,
+ "[set] already specified for this argument "
+ "in format string"};
+ }
+ return set_parser.parse_set(p, parsed);
+ }
+ return {};
+ };
+ auto e = parse_common(pctx, span<const char_type>{&s_flag, 1},
+ span<bool>{&s_set, 1}, each);
+ if (!e) {
+ return e;
+ }
+ if (set_parser.enabled()) {
+ bool loc = (common_options & localized) != 0;
+ return set_parser.sanitize(loc);
+ }
+ return {};
+ }
+
+ template <typename Context, typename Allocator>
+ error scan(
+ std::basic_string<typename Context::char_type,
+ std::char_traits<typename Context::char_type>,
+ Allocator>& val,
+ Context& ctx)
+ {
+ if (set_parser.enabled()) {
+ bool loc = (common_options & localized) != 0;
+ bool mb = (loc || set_parser.get_option(
+ set_parser_type::flag::use_ranges)) &&
+ is_multichar_type(typename Context::char_type{});
+ return do_scan(ctx, val,
+ pred<Context>{ctx, set_parser, loc, mb});
+ }
+
+ auto e = skip_range_whitespace(ctx, false);
+ if (!e) {
+ return e;
+ }
+
+ auto is_space_pred = make_is_space_predicate(
+ ctx.locale(), (common_options & localized) != 0,
+ field_width);
+ return do_scan(ctx, val, is_space_pred);
+ }
+
+ set_parser_type set_parser;
+
+ protected:
+ template <typename Context, typename Allocator, typename Pred>
+ error do_scan(
+ Context& ctx,
+ std::basic_string<typename Context::char_type,
+ std::char_traits<typename Context::char_type>,
+ Allocator>& val,
+ Pred&& predicate)
+ {
+ using string_type = std::basic_string<
+ typename Context::char_type,
+ std::char_traits<typename Context::char_type>, Allocator>;
+
+ if (Context::range_type::is_contiguous) {
+ auto s = read_until_space_zero_copy(
+ ctx.range(), SCN_FWD(predicate), false);
+ if (!s) {
+ return s.error();
+ }
+ if (s.value().size() == 0) {
+ return {error::invalid_scanned_value,
+ "Empty string parsed"};
+ }
+ val.assign(s.value().data(), s.value().size());
+ return {};
+ }
+
+ string_type tmp(val.get_allocator());
+ auto outputit = std::back_inserter(tmp);
+ auto ret = read_until_space(ctx.range(), outputit,
+ SCN_FWD(predicate), false);
+ if (SCN_UNLIKELY(!ret)) {
+ return ret;
+ }
+ if (SCN_UNLIKELY(tmp.empty())) {
+ return {error::invalid_scanned_value,
+ "Empty string parsed"};
+ }
+ val = SCN_MOVE(tmp);
+
+ return {};
+ }
+
+ template <typename Context>
+ struct pred {
+ Context& ctx;
+ set_parser_type& set_parser;
+ bool localized;
+ bool multibyte;
+
+ bool operator()(span<const char> ch) const
+ {
+ SCN_EXPECT(ch.size() >= 1);
+ code_point cp{};
+ auto it = parse_code_point(ch.begin(), ch.end(), cp);
+ if (!it) {
+ // todo: is this really a good idea
+ return !set_parser.check_character(ch[0], localized,
+ ctx.locale());
+ }
+ return !set_parser.check_character(cp, localized,
+ ctx.locale());
+ }
+ bool operator()(span<const wchar_t> ch) const
+ {
+ SCN_EXPECT(ch.size() == 1);
+ return !set_parser.check_character(ch[0], localized,
+ ctx.locale());
+ }
+ constexpr bool is_localized() const
+ {
+ return localized;
+ }
+ constexpr bool is_multibyte() const
+ {
+ return multibyte;
+ }
+ };
+ };
+
+ struct span_scanner : public string_scanner {
+ template <typename Context>
+ error scan(span<typename Context::char_type>& val, Context& ctx)
+ {
+ if (val.size() == 0) {
+ return {error::invalid_scanned_value,
+ "Cannot scan into an empty span"};
+ }
+
+ if (set_parser.enabled()) {
+ bool loc = (common_options & localized) != 0;
+ bool mb = (loc || set_parser.get_option(
+ set_parser_type::flag::use_ranges)) &&
+ is_multichar_type(typename Context::char_type{});
+ return do_scan(ctx, val,
+ string_scanner::pred<Context>{
+ ctx, set_parser, loc, mb});
+ }
+
+ auto e = skip_range_whitespace(ctx, false);
+ if (!e) {
+ return e;
+ }
+
+ auto is_space_pred = make_is_space_predicate(
+ ctx.locale(), (common_options & localized) != 0,
+ field_width != 0 ? min(field_width, val.size())
+ : val.size());
+ return do_scan(ctx, val, is_space_pred);
+ }
+
+ protected:
+ template <typename Context, typename Pred>
+ error do_scan(Context& ctx,
+ span<typename Context::char_type>& val,
+ Pred&& predicate)
+ {
+ if (Context::range_type::is_contiguous) {
+ auto s = read_until_space_zero_copy(
+ ctx.range(), SCN_FWD(predicate), false);
+ if (!s) {
+ return s.error();
+ }
+ if (s.value().size() == 0) {
+ return {error::invalid_scanned_value,
+ "Empty string parsed"};
+ }
+ std::copy(s.value().begin(), s.value().end(), val.begin());
+ val = val.first(s.value().size());
+ return {};
+ }
+
+ std::basic_string<typename Context::char_type> tmp;
+ auto outputit = std::back_inserter(tmp);
+ auto ret = read_until_space(ctx.range(), outputit,
+ SCN_FWD(predicate), false);
+ if (SCN_UNLIKELY(!ret)) {
+ return ret;
+ }
+ if (SCN_UNLIKELY(tmp.empty())) {
+ return {error::invalid_scanned_value,
+ "Empty string parsed"};
+ }
+ std::copy(tmp.begin(), tmp.end(), val.begin());
+ val = val.first(tmp.size());
+
+ return {};
+ }
+ };
+
+ struct string_view_scanner : string_scanner {
+ public:
+ template <typename Context>
+ error scan(basic_string_view<typename Context::char_type>& val,
+ Context& ctx)
+ {
+ if (!Context::range_type::is_contiguous) {
+ return {error::invalid_operation,
+ "Cannot read a string_view from a "
+ "non-contiguous_range"};
+ }
+
+ if (set_parser.enabled()) {
+ bool loc = (common_options & localized) != 0;
+ bool mb = (loc || set_parser.get_option(
+ set_parser_type::flag::use_ranges)) &&
+ is_multichar_type(typename Context::char_type{});
+ return do_scan(ctx, val,
+ string_scanner::pred<Context>{
+ ctx, set_parser, loc, mb});
+ }
+
+ auto e = skip_range_whitespace(ctx, false);
+ if (!e) {
+ return e;
+ }
+
+ auto is_space_pred = make_is_space_predicate(
+ ctx.locale(), (common_options & localized) != 0,
+ field_width);
+ return do_scan(ctx, val, is_space_pred);
+ }
+
+ protected:
+ template <typename Context, typename Pred>
+ error do_scan(Context& ctx,
+ basic_string_view<typename Context::char_type>& val,
+ Pred&& predicate)
+ {
+ SCN_EXPECT(Context::range_type::is_contiguous);
+
+ auto s = read_until_space_zero_copy(ctx.range(),
+ SCN_FWD(predicate), false);
+ if (!s) {
+ return s.error();
+ }
+ if (s.value().size() == 0) {
+ return {error::invalid_scanned_value,
+ "Empty string parsed"};
+ }
+ val = basic_string_view<typename Context::char_type>(
+ s.value().data(), s.value().size());
+ return {};
+ }
+ };
+
+#if SCN_HAS_STRING_VIEW
+ struct std_string_view_scanner : string_view_scanner {
+ template <typename Context>
+ error scan(std::basic_string_view<typename Context::char_type>& val,
+ Context& ctx)
+ {
+ using char_type = typename Context::char_type;
+ auto sv =
+ ::scn::basic_string_view<char_type>(val.data(), val.size());
+ auto e = string_view_scanner::scan(sv, ctx);
+ if (e) {
+ val =
+ std::basic_string_view<char_type>(sv.data(), sv.size());
+ }
+ return e;
+ }
+ };
+#endif
+ } // namespace detail
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/reader/types.h b/src/third-party/scnlib/include/scn/reader/types.h
new file mode 100644
index 0000000..047d10d
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/reader/types.h
@@ -0,0 +1,220 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_READER_TYPES_H
+#define SCN_READER_TYPES_H
+
+#include "int.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+ namespace detail {
+ struct code_point_scanner : common_parser {
+ static constexpr bool skip_preceding_whitespace()
+ {
+ return false;
+ }
+
+ template <typename ParseCtx>
+ error parse(ParseCtx& pctx)
+ {
+ using char_type = typename ParseCtx::char_type;
+
+ auto c_flag = detail::ascii_widen<char_type>('c');
+ bool c_set{};
+ return parse_common(pctx, span<const char_type>{&c_flag, 1},
+ span<bool>{&c_set, 1},
+ null_type_cb<ParseCtx>);
+ }
+
+ template <typename Context>
+ error scan(code_point& val, Context& ctx)
+ {
+ unsigned char buf[4] = {0};
+ auto cp = read_code_point(ctx.range(), make_span(buf, 4));
+ if (!cp) {
+ return cp.error();
+ }
+ val = cp.value().cp;
+ return {};
+ }
+ };
+
+ struct bool_scanner : common_parser {
+ template <typename ParseCtx>
+ error parse(ParseCtx& pctx)
+ {
+ using char_type = typename ParseCtx::char_type;
+
+ array<char_type, 3> options{{
+ // Only strings
+ ascii_widen<char_type>('s'),
+ // Only ints
+ ascii_widen<char_type>('i'),
+ // Localized digits
+ ascii_widen<char_type>('n'),
+ }};
+ bool flags[3] = {false};
+ auto e = parse_common(
+ pctx, span<const char_type>{options.begin(), options.end()},
+ span<bool>{flags, 3}, null_type_cb<ParseCtx>);
+
+ if (!e) {
+ return e;
+ }
+
+ format_options = 0;
+ // default ('s' + 'i')
+ if (!flags[0] && !flags[1]) {
+ format_options |= allow_string | allow_int;
+ }
+ // 's'
+ if (flags[0]) {
+ format_options |= allow_string;
+ }
+ // 'i'
+ if (flags[1]) {
+ format_options |= allow_int;
+ }
+ // 'n'
+ if (flags[2]) {
+ format_options |= localized_digits;
+ // 'n' implies 'L'
+ common_options |= localized;
+ }
+ return {};
+ }
+
+ template <typename Context>
+ error scan(bool& val, Context& ctx)
+ {
+ using char_type = typename Context::char_type;
+
+ if ((format_options & allow_string) != 0) {
+ auto truename = ctx.locale().get_static().truename();
+ auto falsename = ctx.locale().get_static().falsename();
+ if ((common_options & localized) != 0) {
+ truename = ctx.locale().get_localized().truename();
+ falsename = ctx.locale().get_localized().falsename();
+ }
+ const auto max_len =
+ detail::max(truename.size(), falsename.size());
+ std::basic_string<char_type> buf;
+ buf.reserve(max_len);
+
+ auto tmp_it = std::back_inserter(buf);
+ auto is_space_pred = make_is_space_predicate(
+ ctx.locale(), (common_options & localized) != 0,
+ field_width);
+ auto e = read_until_space(ctx.range(), tmp_it,
+ is_space_pred, false);
+ if (!e) {
+ return e;
+ }
+
+ bool found = false;
+ if (buf.size() >= falsename.size()) {
+ if (std::equal(falsename.begin(), falsename.end(),
+ buf.begin())) {
+ val = false;
+ found = true;
+ }
+ }
+ if (!found && buf.size() >= truename.size()) {
+ if (std::equal(truename.begin(), truename.end(),
+ buf.begin())) {
+ val = true;
+ found = true;
+ }
+ }
+ if (found) {
+ return {};
+ }
+ else {
+ auto pb =
+ putback_n(ctx.range(),
+ static_cast<std::ptrdiff_t>(buf.size()));
+ if (!pb) {
+ return pb;
+ }
+ }
+ }
+
+ if ((format_options & allow_int) != 0) {
+ if ((format_options & localized_digits) != 0) {
+ int i{};
+ auto s = integer_scanner<int>{};
+ s.common_options = integer_scanner<int>::localized;
+ s.format_options =
+ integer_scanner<int>::only_unsigned |
+ integer_scanner<int>::localized_digits;
+ auto e = s.scan(i, ctx);
+ if (!e) {
+ return e;
+ }
+ if (SCN_UNLIKELY(i != 0 && i != 1)) {
+ return {
+ error::invalid_scanned_value,
+ "Scanned integral boolean not equal to 0 or 1"};
+ }
+ else if (i == 0) {
+ val = false;
+ }
+ else {
+ val = true;
+ }
+ return {};
+ }
+
+ unsigned char buf[4] = {0};
+ auto cp = read_code_point(ctx.range(), make_span(buf, 4));
+ if (!cp) {
+ return cp.error();
+ }
+ if (cp.value().cp == detail::ascii_widen<char_type>('0')) {
+ val = false;
+ return {};
+ }
+ if (cp.value().cp == detail::ascii_widen<char_type>('1')) {
+ val = true;
+ return {};
+ }
+ auto pb = putback_n(ctx.range(), cp.value().chars.ssize());
+ if (!pb) {
+ return pb;
+ }
+ }
+
+ return {error::invalid_scanned_value, "Couldn't scan bool"};
+ }
+
+ enum format_options_type {
+ // 's' option
+ allow_string = 1,
+ // 'i' option
+ allow_int = 2,
+ // 'n' option
+ localized_digits = 4
+ };
+ uint8_t format_options{allow_string | allow_int};
+ };
+
+ } // namespace detail
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/scan/common.h b/src/third-party/scnlib/include/scn/scan/common.h
new file mode 100644
index 0000000..ccdd825
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/scan/common.h
@@ -0,0 +1,131 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_SCAN_COMMON_H
+#define SCN_SCAN_COMMON_H
+
+#include "../detail/locale.h"
+#include "../detail/result.h"
+#include "../unicode/common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename CharT>
+ constexpr int to_format(int i)
+ {
+ return i;
+ }
+ template <typename T>
+ constexpr auto to_format(T&& f) -> decltype(string_view{SCN_FWD(f)})
+ {
+ return {SCN_FWD(f)};
+ }
+ template <typename T>
+ constexpr auto to_format(T&& f) -> decltype(wstring_view{SCN_FWD(f)})
+ {
+ return {SCN_FWD(f)};
+ }
+ template <typename CharT>
+ basic_string_view<CharT> to_format(const std::basic_string<CharT>& str)
+ {
+ return {str.data(), str.size()};
+ }
+
+ template <typename CharT>
+ struct until_pred {
+ array<CharT, 4> until;
+ size_t size;
+
+ constexpr until_pred(CharT ch) : until({{ch}}), size(1) {}
+ until_pred(code_point cp)
+ {
+ auto ret = encode_code_point(until.begin(), until.end(), cp);
+ SCN_ENSURE(ret);
+ size = ret.value() - until.begin();
+ }
+
+ SCN_CONSTEXPR14 bool operator()(span<const CharT> ch) const
+ {
+ if (ch.size() != size) {
+ return false;
+ }
+ for (size_t i = 0; i < ch.size(); ++i) {
+ if (ch[i] != until[i]) {
+ return false;
+ }
+ }
+ return true;
+ }
+ static constexpr bool is_localized()
+ {
+ return false;
+ }
+ constexpr bool is_multibyte() const
+ {
+ return size != 1;
+ }
+ };
+
+ template <typename Error, typename Range>
+ using generic_scan_result_for_range = decltype(detail::wrap_result(
+ SCN_DECLVAL(Error),
+ SCN_DECLVAL(detail::range_tag<Range>),
+ SCN_DECLVAL(range_wrapper_for_t<Range>)));
+ template <typename Range>
+ using scan_result_for_range =
+ generic_scan_result_for_range<wrapped_error, Range>;
+ } // namespace detail
+
+ template <typename T>
+ struct discard_type {
+ discard_type() = default;
+ };
+
+ /**
+ * Scans an instance of `T`, but doesn't store it anywhere.
+ * Uses `scn::temp` internally, so the user doesn't have to bother.
+ *
+ * \code{.cpp}
+ * int i{};
+ * // 123 is discarded, 456 is read into `i`
+ * auto result = scn::scan("123 456", "{} {}", scn::discard<T>(), i);
+ * // result == true
+ * // i == 456
+ * \endcode
+ */
+ template <typename T>
+ discard_type<T>& discard()
+ {
+ return temp(discard_type<T>{})();
+ }
+
+ template <typename T>
+ struct scanner<discard_type<T>> : public scanner<T> {
+ template <typename Context>
+ error scan(discard_type<T>&, Context& ctx)
+ {
+ T tmp;
+ return scanner<T>::scan(tmp, ctx);
+ }
+ };
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/scan/getline.h b/src/third-party/scnlib/include/scn/scan/getline.h
new file mode 100644
index 0000000..c330969
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/scan/getline.h
@@ -0,0 +1,186 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_SCAN_GETLINE_H
+#define SCN_SCAN_GETLINE_H
+
+#include "common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename WrappedRange,
+ typename String,
+ typename Until,
+ typename CharT = typename WrappedRange::char_type>
+ error getline_impl(WrappedRange& r, String& str, Until until)
+ {
+ auto pred = until_pred<CharT>{until};
+ auto s = read_until_space_zero_copy(r, pred, true);
+ if (!s) {
+ return s.error();
+ }
+ if (s.value().size() != 0) {
+ auto size = s.value().size();
+ if (pred(s.value().last(1))) {
+ --size;
+ }
+ str.clear();
+ str.resize(size);
+ std::copy(s.value().begin(), s.value().begin() + size,
+ str.begin());
+ return {};
+ }
+
+ String tmp;
+ auto out = std::back_inserter(tmp);
+ auto e = read_until_space(r, out, pred, true);
+ if (!e) {
+ return e;
+ }
+ if (pred(span<const CharT>(&*(tmp.end() - 1), 1))) {
+ tmp.pop_back();
+ }
+ str = SCN_MOVE(tmp);
+ return {};
+ }
+ template <typename WrappedRange,
+ typename Until,
+ typename CharT = typename WrappedRange::char_type>
+ error getline_impl(WrappedRange& r,
+ basic_string_view<CharT>& str,
+ Until until)
+ {
+ static_assert(
+ WrappedRange::is_contiguous,
+ "Cannot getline a string_view from a non-contiguous range");
+ auto pred = until_pred<CharT>{until};
+ auto s = read_until_space_zero_copy(r, pred, true);
+ if (!s) {
+ return s.error();
+ }
+ SCN_ASSERT(s.value().size(), "");
+ auto size = s.value().size();
+ if (pred(s.value().last(1))) {
+ --size;
+ }
+ str = basic_string_view<CharT>{s.value().data(), size};
+ return {};
+ }
+#if SCN_HAS_STRING_VIEW
+ template <typename WrappedRange,
+ typename Until,
+ typename CharT = typename WrappedRange::char_type>
+ auto getline_impl(WrappedRange& r,
+ std::basic_string_view<CharT>& str,
+ Until until) -> error
+ {
+ auto sv = ::scn::basic_string_view<CharT>{};
+ auto ret = getline_impl(r, sv, until);
+ str = ::std::basic_string_view<CharT>{sv.data(), sv.size()};
+ return ret;
+ }
+#endif
+ } // namespace detail
+
+ /**
+ * Read the range in \c r into \c str until \c until is found.
+ * \c until will be skipped in parsing: it will not be pushed into \c
+ * str, and the returned range will go past it.
+ *
+ * If `str` is convertible to a `basic_string_view`:
+ * - And if `r` is a `contiguous_range`:
+ * - `str` is set to point inside `r` with the appropriate length
+ * - if not, returns an error
+ *
+ * Otherwise, clears `str` by calling `str.clear()`, and then reads the
+ * range into `str` as if by repeatedly calling \c str.push_back.
+ * `str.reserve()` is also required to be present.
+ *
+ * `Until` can either be the same as `r` character type (`char` or
+ * `wchar_t`), or `code_point`.
+ *
+ * \code{.cpp}
+ * auto source = "hello\nworld"
+ * std::string line;
+ * auto result = scn::getline(source, line, '\n');
+ * // line == "hello"
+ * // result.range() == "world"
+ *
+ * // Using the other overload
+ * result = scn::getline(result.range(), line);
+ * // line == "world"
+ * // result.empty() == true
+ * \endcode
+ */
+#if SCN_DOXYGEN
+ template <typename Range, typename String, typename Until>
+ auto getline(Range&& r, String& str, Until until)
+ -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Range, typename String, typename Until>
+ SCN_NODISCARD auto getline(Range&& r, String& str, Until until)
+ -> detail::scan_result_for_range<Range>
+ {
+ auto wrapped = wrap(SCN_FWD(r));
+ auto err = getline_impl(wrapped, str, until);
+ if (!err) {
+ auto e = wrapped.reset_to_rollback_point();
+ if (!e) {
+ err = e;
+ }
+ }
+ else {
+ wrapped.set_rollback_point();
+ }
+ return detail::wrap_result(
+ wrapped_error{err}, detail::range_tag<Range>{}, SCN_MOVE(wrapped));
+ }
+#endif
+
+ /**
+ * Equivalent to \ref getline with the last parameter set to
+ * <tt>'\\n'</tt> with the appropriate character type.
+ *
+ * In other words, reads `r` into `str` until <tt>'\\n'</tt> is found.
+ *
+ * The character type is determined by `r`.
+ */
+#if SCN_DOXYGEN
+ template <typename Range,
+ typename String,
+ typename CharT = typename detail::extract_char_type<
+ ranges::iterator_t<range_wrapper_for_t<Range>>>::type>
+ auto getline(Range&& r, String& str)
+ -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Range,
+ typename String,
+ typename CharT = typename detail::extract_char_type<
+ ranges::iterator_t<range_wrapper_for_t<Range>>>::type>
+ SCN_NODISCARD auto getline(Range&& r, String& str)
+ -> detail::scan_result_for_range<Range>
+ {
+ return getline(SCN_FWD(r), str, detail::ascii_widen<CharT>('\n'));
+ }
+#endif
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/scan/ignore.h b/src/third-party/scnlib/include/scn/scan/ignore.h
new file mode 100644
index 0000000..415a877
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/scan/ignore.h
@@ -0,0 +1,189 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_SCAN_IGNORE_H
+#define SCN_SCAN_IGNORE_H
+
+#include "common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename CharT>
+ struct ignore_iterator {
+ using value_type = CharT;
+ using pointer = value_type*;
+ using reference = value_type&;
+ using difference_type = std::ptrdiff_t;
+ using iterator_category = std::output_iterator_tag;
+
+ constexpr ignore_iterator() = default;
+
+ SCN_CONSTEXPR14 ignore_iterator& operator=(CharT) noexcept
+ {
+ return *this;
+ }
+ constexpr const ignore_iterator& operator=(CharT) const noexcept
+ {
+ return *this;
+ }
+
+ SCN_CONSTEXPR14 ignore_iterator& operator*() noexcept
+ {
+ return *this;
+ }
+ constexpr const ignore_iterator& operator*() const noexcept
+ {
+ return *this;
+ }
+
+ SCN_CONSTEXPR14 ignore_iterator& operator++() noexcept
+ {
+ return *this;
+ }
+ constexpr const ignore_iterator& operator++() const noexcept
+ {
+ return *this;
+ }
+ };
+
+ template <typename CharT>
+ struct ignore_iterator_n {
+ using value_type = CharT;
+ using pointer = value_type*;
+ using reference = value_type&;
+ using difference_type = std::ptrdiff_t;
+ using iterator_category = std::output_iterator_tag;
+
+ ignore_iterator_n() = default;
+ ignore_iterator_n(difference_type n) : i(n) {}
+
+ constexpr const ignore_iterator_n& operator=(CharT) const noexcept
+ {
+ return *this;
+ }
+
+ constexpr const ignore_iterator_n& operator*() const noexcept
+ {
+ return *this;
+ }
+
+ SCN_CONSTEXPR14 ignore_iterator_n& operator++() noexcept
+ {
+ ++i;
+ return *this;
+ }
+
+ constexpr bool operator==(const ignore_iterator_n& o) const noexcept
+ {
+ return i == o.i;
+ }
+ constexpr bool operator!=(const ignore_iterator_n& o) const noexcept
+ {
+ return !(*this == o);
+ }
+
+ difference_type i{0};
+ };
+
+ template <typename WrappedRange,
+ typename Until,
+ typename CharT = typename WrappedRange::char_type>
+ error ignore_until_impl(WrappedRange& r, Until until)
+ {
+ ignore_iterator<CharT> it{};
+ return read_until_space(r, it, until_pred<CharT>{until}, false);
+ }
+
+ template <typename WrappedRange,
+ typename Until,
+ typename CharT = typename WrappedRange::char_type>
+ error ignore_until_n_impl(WrappedRange& r,
+ ranges::range_difference_t<WrappedRange> n,
+ Until until)
+ {
+ ignore_iterator_n<CharT> begin{}, end{n};
+ return read_until_space_ranged(r, begin, end,
+ until_pred<CharT>{until}, false);
+ }
+ } // namespace detail
+
+ /**
+ * Advances the beginning of \c r until \c until is found.
+ */
+#if SCN_DOXYGEN
+ template <typename Range, typename Until>
+ auto ignore_until(Range&& r, Until until)
+ -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Range, typename Until>
+ SCN_NODISCARD auto ignore_until(Range&& r, Until until)
+ -> detail::scan_result_for_range<Range>
+ {
+ auto wrapped = wrap(SCN_FWD(r));
+ auto err = detail::ignore_until_impl(wrapped, until);
+ if (!err) {
+ auto e = wrapped.reset_to_rollback_point();
+ if (!e) {
+ err = e;
+ }
+ }
+ else {
+ wrapped.set_rollback_point();
+ }
+ return detail::wrap_result(
+ wrapped_error{err}, detail::range_tag<Range>{}, SCN_MOVE(wrapped));
+ }
+#endif
+
+ /**
+ * Advances the beginning of \c r until \c until is found, or the
+ * beginning has been advanced \c n times.
+ *
+ * `Until` can be the `r` character type (`char` or `wchar_t`), or
+ * `code_point`.
+ */
+#if SCN_DOXYGEN
+ template <typename Range, typename Until>
+ auto ignore_until_n(Range&& r,
+ ranges::range_difference_t<Range> n,
+ Until until) -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Range, typename Until>
+ SCN_NODISCARD auto ignore_until_n(Range&& r,
+ ranges::range_difference_t<Range> n,
+ Until until)
+ -> detail::scan_result_for_range<Range>
+ {
+ auto wrapped = wrap(SCN_FWD(r));
+ auto err = detail::ignore_until_n_impl(wrapped, n, until);
+ if (!err) {
+ auto e = wrapped.reset_to_rollback_point();
+ if (!e) {
+ err = e;
+ }
+ }
+ return detail::wrap_result(
+ wrapped_error{err}, detail::range_tag<Range>{}, SCN_MOVE(wrapped));
+ }
+#endif
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/scan/istream.h b/src/third-party/scnlib/include/scn/scan/istream.h
new file mode 100644
index 0000000..a9aef86
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/scan/istream.h
@@ -0,0 +1,147 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_SCAN_ISTREAM_H
+#define SCN_SCAN_ISTREAM_H
+
+#include "../reader/common.h"
+#include "../detail/result.h"
+
+#include <istream>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename WrappedRange>
+ class range_streambuf
+ : public std::basic_streambuf<typename WrappedRange::char_type> {
+ using base = std::basic_streambuf<typename WrappedRange::char_type>;
+
+ public:
+ using range_type = WrappedRange;
+ using char_type = typename WrappedRange::char_type;
+ using traits_type = typename base::traits_type;
+ using int_type = typename base::int_type;
+
+ explicit range_streambuf(range_type& r) : m_range(std::addressof(r))
+ {
+ }
+
+ private:
+ int_type underflow() override
+ {
+ // already read
+ if (!traits_type::eq_int_type(m_ch, traits_type::eof())) {
+ return m_ch;
+ }
+
+ auto ret = read_code_unit(*m_range);
+ if (!ret) {
+ // error
+ // m_ch is already eof
+ return traits_type::eof();
+ }
+ m_ch = traits_type::to_int_type(ret.value());
+ return m_ch;
+ }
+ int_type uflow() override
+ {
+ auto ret = underflow();
+ if (ret != traits_type::eof()) {
+ m_ch = traits_type::eof();
+ }
+ return ret;
+ }
+ std::streamsize showmanyc() override
+ {
+ return traits_type::eq_int_type(m_ch, traits_type::eof()) ? 0
+ : 1;
+ }
+ int_type pbackfail(int_type) override
+ {
+ auto e = putback_n(*m_range, 1);
+ if (!e) {
+ return traits_type::eof();
+ }
+ return traits_type::to_int_type(0);
+ }
+
+ range_type* m_range;
+ int_type m_ch{traits_type::eof()};
+ };
+
+ // Trick stolen from {fmt}
+ template <typename CharT>
+ struct test_std_stream : std::basic_istream<CharT> {
+ private:
+ struct null;
+ // Hide all operator>> from std::basic_istream<CharT>
+ void operator>>(null);
+ };
+
+ // Check for user-defined operator>>
+ template <typename CharT, typename T, typename = void>
+ struct is_std_streamable : std::false_type {
+ };
+
+ template <typename CharT, typename T>
+ struct is_std_streamable<
+ CharT,
+ T,
+ void_t<decltype(SCN_DECLVAL(test_std_stream<CharT>&) >>
+ SCN_DECLVAL(T&))>> : std::true_type {
+ };
+ } // namespace detail
+
+ template <typename T>
+ struct scanner<T,
+ typename std::enable_if<
+ detail::is_std_streamable<char, T>::value ||
+ detail::is_std_streamable<wchar_t, T>::value>::type>
+ : public empty_parser {
+ template <typename Context>
+ error scan(T& val, Context& ctx)
+ {
+ static_assert(detail::is_std_streamable<typename Context::char_type,
+ T>::value,
+ "Type can not be read from a basic_istream of this "
+ "character type");
+ detail::range_streambuf<typename Context::range_type> streambuf(
+ ctx.range());
+ std::basic_istream<typename Context::char_type> stream(
+ std::addressof(streambuf));
+
+ if (!(stream >> val)) {
+ if (stream.eof()) {
+ return {error::end_of_range, "EOF"};
+ }
+ if (stream.bad()) {
+ return {error::unrecoverable_source_error,
+ "Bad std::istream after reading"};
+ }
+ return {error::invalid_scanned_value,
+ "Failed to read with std::istream"};
+ }
+ return {};
+ }
+ };
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_DETAIL_ISTREAM_H
diff --git a/src/third-party/scnlib/include/scn/scan/list.h b/src/third-party/scnlib/include/scn/scan/list.h
new file mode 100644
index 0000000..a5eeaf5
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/scan/list.h
@@ -0,0 +1,450 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_SCAN_LIST_H
+#define SCN_SCAN_LIST_H
+
+#include "common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ /**
+ * Adapts a `span` into a type that can be read into using \ref
+ * scan_list. This way, potentially unnecessary dynamic memory
+ * allocations can be avoided. To use as a parameter to \ref scan_list,
+ * use \ref make_span_list_wrapper.
+ *
+ * \code{.cpp}
+ * std::vector<int> buffer(8, 0);
+ * scn::span<int> s = scn::make_span(buffer);
+ *
+ * auto wrapper = scn::span_list_wrapper<int>(s);
+ * scn::scan_list("123 456", wrapper);
+ * // s[0] == buffer[0] == 123
+ * // s[1] == buffer[1] == 456
+ * \endcode
+ *
+ * \see scan_list
+ * \see make_span_list_wrapper
+ */
+ template <typename T>
+ struct span_list_wrapper {
+ using value_type = T;
+
+ span_list_wrapper(span<T> s) : m_span(s) {}
+
+ void push_back(T val)
+ {
+ SCN_EXPECT(n < max_size());
+ m_span[n] = SCN_MOVE(val);
+ ++n;
+ }
+
+ SCN_NODISCARD constexpr std::size_t size() const noexcept
+ {
+ return n;
+ }
+ SCN_NODISCARD constexpr std::size_t max_size() const noexcept
+ {
+ return m_span.size();
+ }
+
+ span<T> m_span;
+ std::size_t n{0};
+ };
+
+ namespace detail {
+ template <typename T>
+ using span_list_wrapper_for =
+ span_list_wrapper<typename decltype(make_span(
+ SCN_DECLVAL(T&)))::value_type>;
+ }
+
+ /**
+ * Adapts a contiguous buffer into a type containing a `span` that can
+ * be read into using \ref scn::scan_list.
+ *
+ * Example adapted from \ref span_list_wrapper:
+ * \code{.cpp}
+ * std::vector<int> buffer(8, 0);
+ * scn::scan_list("123 456", scn::make_span_list_wrapper(buffer));
+ * // s[0] == buffer[0] == 123
+ * // s[1] == buffer[1] == 456
+ * \endcode
+ *
+ * \see scan_list
+ * \see span_list_wrapper
+ */
+ template <typename T>
+ auto make_span_list_wrapper(T& s)
+ -> temporary<detail::span_list_wrapper_for<T>>
+ {
+ auto _s = make_span(s);
+ return temp(span_list_wrapper<typename decltype(_s)::value_type>(_s));
+ }
+
+ /**
+ * Used to customize `scan_list_ex()`.
+ *
+ * \tparam CharT Can be a code unit type (`char` or `wchar_t`, depending on
+ * the source range), or `code_point`.
+ *
+ * `list_separator`, `list_until` and `list_separator_and_until` can be used
+ * to create a value of this type, taking advantage of template argument
+ * deduction (no need to hand-specify `CharT`).
+ */
+ template <typename CharT>
+ struct scan_list_options {
+ /**
+ * If set, up to one separator character can be accepted between values,
+ * which may be surrounded by whitespace.
+ */
+ optional<CharT> separator{};
+ /**
+ * If set, reading the list is stopped if this character is found
+ * between values.
+ *
+ * In that case, it is advanced over, and no error is returned.
+ */
+ optional<CharT> until{};
+
+ scan_list_options() = default;
+ scan_list_options(optional<CharT> s, optional<CharT> u)
+ : separator(SCN_MOVE(s)), until(SCN_MOVE(u))
+ {
+ }
+ };
+
+ /**
+ * Create a `scan_list_options` for `scan_list_ex`, by using `ch` as the
+ * separator character.
+ */
+ template <typename CharT>
+ scan_list_options<CharT> list_separator(CharT ch)
+ {
+ return {optional<CharT>{ch}, nullopt};
+ }
+ /**
+ * Create a `scan_list_options` for `scan_list_ex`, by using `ch` as the
+ * until-character.
+ */
+ template <typename CharT>
+ scan_list_options<CharT> list_until(CharT ch)
+ {
+ return {nullopt, optional<CharT>{ch}};
+ }
+ /**
+ * Create a `scan_list_options` for `scan_list_ex`, by using `sep` as the
+ * separator, and `until` as the until-character.
+ */
+ template <typename CharT>
+ scan_list_options<CharT> list_separator_and_until(CharT sep, CharT until)
+ {
+ return {optional<CharT>{sep}, optional<CharT>{until}};
+ }
+
+ namespace detail {
+ template <typename WrappedRange, typename CharT>
+ expected<CharT> check_separator(WrappedRange& r, size_t& n, CharT)
+ {
+ auto ret = read_code_unit(r);
+ if (!ret) {
+ return ret.error();
+ }
+ n = 1;
+ return ret.value();
+ }
+ template <typename WrappedRange>
+ expected<code_point> check_separator(WrappedRange& r,
+ size_t& n,
+ code_point)
+ {
+ unsigned char buf[4] = {0};
+ auto ret = read_code_point(r, make_span(buf, 4));
+ if (!ret) {
+ return ret.error();
+ }
+ n = ret.value().chars.size();
+ return ret.value().cp;
+ }
+
+ template <typename Context, typename Container, typename Separator>
+ auto scan_list_impl(Context& ctx,
+ bool localized,
+ Container& c,
+ scan_list_options<Separator> options) -> error
+ {
+ using char_type = typename Context::char_type;
+ using value_type = typename Container::value_type;
+ value_type value;
+
+ auto args = make_args_for(ctx.range(), 1, value);
+
+ bool scanning = true;
+ while (scanning) {
+ if (c.size() == c.max_size()) {
+ break;
+ }
+
+ // read value
+ auto pctx = make_parse_context(1, ctx.locale(), localized);
+ auto err = visit(ctx, pctx, basic_args<char_type>{args});
+ if (!err) {
+ if (err == error::end_of_range) {
+ break;
+ }
+ return err;
+ }
+ c.push_back(SCN_MOVE(value));
+
+ auto next = static_cast<Separator>(0);
+ size_t n{0};
+
+ auto read_next = [&]() -> error {
+ auto ret = check_separator(ctx.range(), n,
+ static_cast<Separator>(0));
+ if (!ret) {
+ if (ret.error() == error::end_of_range) {
+ scanning = false;
+ return {};
+ }
+ return ret.error();
+ }
+ next = ret.value();
+
+ err =
+ putback_n(ctx.range(), static_cast<std::ptrdiff_t>(n));
+ if (!err) {
+ return err;
+ }
+
+ return {};
+ };
+
+ bool sep_found = false;
+ while (true) {
+ // read until
+ if (options.until) {
+ err = read_next();
+ if (!err) {
+ return err;
+ }
+ if (!scanning) {
+ break;
+ }
+
+ if (next == options.until.get()) {
+ scanning = false;
+ break;
+ }
+ }
+
+ // read sep
+ if (options.separator && !sep_found) {
+ err = read_next();
+ if (!err) {
+ return err;
+ }
+ if (!scanning) {
+ break;
+ }
+
+ if (next == options.separator.get()) {
+ // skip to next char
+ ctx.range().advance(static_cast<std::ptrdiff_t>(n));
+ continue;
+ }
+ }
+
+ err = read_next();
+ if (!err) {
+ return err;
+ }
+ if (!scanning) {
+ break;
+ }
+
+ if (ctx.locale().get_static().is_space(next)) {
+ // skip ws
+ ctx.range().advance(static_cast<std::ptrdiff_t>(n));
+ }
+ else {
+ break;
+ }
+ }
+ }
+
+ return {};
+ }
+ } // namespace detail
+
+ /**
+ * Reads values repeatedly from `r` and writes them into `c`.
+ *
+ * The values read are of type `Container::value_type`, and they are
+ * written into `c` using `c.push_back`.
+ * The values are separated by whitespace.
+ *
+ * The range is read, until:
+ * - `c.max_size()` is reached, or
+ * - range `EOF` is reached
+ *
+ * In these cases, an error will not be returned, and the beginning
+ * of the returned range will point to the first character after the
+ * scanned list.
+ *
+ * If an invalid value is scanned, `error::invalid_scanned_value` is
+ * returned, but the values already in `vec` will remain there. The range is
+ * put back to the state it was before reading the invalid value.
+ *
+ * To scan into `span`, use \ref span_list_wrapper.
+ * \ref make_span_list_wrapper
+ *
+ * \code{.cpp}
+ * std::vector<int> vec{};
+ * auto result = scn::scan_list("123 456", vec);
+ * // vec == [123, 456]
+ * // result.empty() == true
+ *
+ * vec.clear();
+ * result = scn::scan_list("123 456 abc", vec);
+ * // vec == [123, 456]
+ * // result.error() == invalid_scanned_value
+ * // result.range() == " abc"
+ * \endcode
+ *
+ * \param r Range to read from
+ * \param c Container to write values to, using `c.push_back()`.
+ * `Container::value_type` will be used to determine the type of the values
+ * to read.
+ */
+#if SCN_DOXYGEN
+ template <typename Range, typename Container>
+ auto scan_list(Range&& r, Container& c)
+ -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Range, typename Container>
+ SCN_NODISCARD auto scan_list(Range&& r, Container& c)
+ -> detail::scan_result_for_range<Range>
+ {
+ auto range = wrap(SCN_FWD(r));
+ auto ctx = make_context(SCN_MOVE(range));
+ using char_type = typename decltype(ctx)::char_type;
+
+ auto err = detail::scan_list_impl(ctx, false, c,
+ scan_list_options<char_type>{});
+
+ return detail::wrap_result(wrapped_error{err},
+ detail::range_tag<Range>{},
+ SCN_MOVE(ctx.range()));
+ }
+#endif
+
+ /**
+ * Otherwise equivalent to `scan_list()`, except can react to additional
+ * characters, based on `options`.
+ *
+ * See `scan_list_options` for more information.
+ *
+ * \param r Range to scan from
+ * \param c Container to write read values into
+ * \param options Options to use
+ *
+ * \code{.cpp}
+ * std::vector<int> vec{};
+ * auto result = scn::scan_list_ex("123, 456", vec,
+ * scn::list_separator(','));
+ * // vec == [123, 456]
+ * // result.empty() == true
+ * \endcode
+ *
+ * \see scan_list
+ * \see scan_list_options
+ */
+#if SCN_DOXYGEN
+ template <typename Range, typename Container, typename CharT>
+ auto scan_list_ex(Range&& r, Container& c, scan_list_options<CharT> options)
+ -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Range, typename Container, typename CharT>
+ SCN_NODISCARD auto scan_list_ex(Range&& r,
+ Container& c,
+ scan_list_options<CharT> options)
+ -> detail::scan_result_for_range<Range>
+ {
+ auto range = wrap(SCN_FWD(r));
+ auto ctx = make_context(SCN_MOVE(range));
+
+ auto err = detail::scan_list_impl(ctx, false, c, options);
+
+ return detail::wrap_result(wrapped_error{err},
+ detail::range_tag<Range>{},
+ SCN_MOVE(ctx.range()));
+ }
+#endif
+
+ /**
+ * Otherwise equivalent to `scan_list_ex()`, except uses `loc` to scan the
+ * values.
+ *
+ * \param loc Locale to use for scanning. Must be a `std::locale`.
+ * \param r Range to scan from
+ * \param c Container to write read values into
+ * \param options Options to use
+ *
+ * \see scan_list_ex()
+ * \see scan_localized()
+ */
+#if SCN_DOXYGEN
+ template <typename Locale,
+ typename Range,
+ typename Container,
+ typename CharT>
+ auto scan_list_localized(const Locale& loc,
+ Range&& r,
+ Container& c,
+ scan_list_options<CharT> options)
+ -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Locale,
+ typename Range,
+ typename Container,
+ typename CharT>
+ SCN_NODISCARD auto scan_list_localized(const Locale& loc,
+ Range&& r,
+ Container& c,
+ scan_list_options<CharT> options)
+ -> detail::scan_result_for_range<Range>
+ {
+ auto range = wrap(SCN_FWD(r));
+ using char_type = typename decltype(range)::char_type;
+ auto locale = make_locale_ref<char_type>(loc);
+ auto ctx = make_context(SCN_MOVE(range), SCN_MOVE(locale));
+
+ auto err = detail::scan_list_impl(ctx, true, c, options);
+
+ return detail::wrap_result(wrapped_error{err},
+ detail::range_tag<Range>{},
+ SCN_MOVE(ctx.range()));
+ }
+#endif
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/scan/scan.h b/src/third-party/scnlib/include/scn/scan/scan.h
new file mode 100644
index 0000000..20f4cd1
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/scan/scan.h
@@ -0,0 +1,444 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_SCAN_SCAN_H
+#define SCN_SCAN_SCAN_H
+
+#include "../util/optional.h"
+#include "common.h"
+#include "vscan.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace dummy {
+ }
+
+ /**
+ * \tparam OriginalRange The type of the range passed to the scanning
+ * function \param result Return value of `vscan` \return Result object
+ *
+ * \code{.cpp}
+ * template <typename Range, typename... Args>
+ * auto scan(Range&& r, string_view f, Args&... a) {
+ * auto range = scn::wrap(std::forward<Range>(r));
+ * auto args = scn::make_args_for(range, f, a...);
+ * auto ret = scn::vscan(std::move(range), f, {args});
+ * return scn::make_scan_result<Range>(std::move(ret));
+ * }
+ * \endcode
+ */
+ template <typename OriginalRange,
+ typename Error = wrapped_error,
+ typename WrappedRange>
+ auto make_scan_result(vscan_result<WrappedRange> result)
+ -> detail::scan_result_for_range<OriginalRange>
+ {
+ return detail::wrap_result(Error{result.err},
+ detail::range_tag<OriginalRange>{},
+ SCN_MOVE(result.range));
+ }
+
+ namespace detail {
+ template <typename Range, typename Format, typename... Args>
+ auto scan_boilerplate(Range&& r, const Format& f, Args&... a)
+ -> detail::scan_result_for_range<Range>
+ {
+ static_assert(sizeof...(Args) > 0,
+ "Have to scan at least a single argument");
+ static_assert(SCN_CHECK_CONCEPT(ranges::range<Range>),
+ "Input needs to be a Range");
+
+ auto range = wrap(SCN_FWD(r));
+ auto format = detail::to_format(f);
+ auto args = make_args_for(range, format, a...);
+ auto ret = vscan(SCN_MOVE(range), format, {args});
+ return make_scan_result<Range>(SCN_MOVE(ret));
+ }
+
+ template <typename Range, typename... Args>
+ auto scan_boilerplate_default(Range&& r, Args&... a)
+ -> detail::scan_result_for_range<Range>
+ {
+ static_assert(sizeof...(Args) > 0,
+ "Have to scan at least a single argument");
+ static_assert(SCN_CHECK_CONCEPT(ranges::range<Range>),
+ "Input needs to be a Range");
+
+ auto range = wrap(SCN_FWD(r));
+ auto format = static_cast<int>(sizeof...(Args));
+ auto args = make_args_for(range, format, a...);
+ auto ret = vscan_default(SCN_MOVE(range), format, {args});
+ return make_scan_result<Range>(SCN_MOVE(ret));
+ }
+
+ template <typename Locale,
+ typename Range,
+ typename Format,
+ typename... Args>
+ auto scan_boilerplate_localized(const Locale& loc,
+ Range&& r,
+ const Format& f,
+ Args&... a)
+ -> detail::scan_result_for_range<Range>
+ {
+ static_assert(sizeof...(Args) > 0,
+ "Have to scan at least a single argument");
+ static_assert(SCN_CHECK_CONCEPT(ranges::range<Range>),
+ "Input needs to be a Range");
+
+ auto range = wrap(SCN_FWD(r));
+ auto format = detail::to_format(f);
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ auto locale =
+ make_locale_ref<typename decltype(range)::char_type>(loc);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+
+ auto args = make_args_for(range, format, a...);
+ auto ret = vscan_localized(SCN_MOVE(range), SCN_MOVE(locale),
+ format, {args});
+ return make_scan_result<Range>(SCN_MOVE(ret));
+ }
+
+ } // namespace detail
+
+ // scan
+
+ // For some reason, Doxygen dislikes SCN_NODISCARD
+
+ /**
+ * The most fundamental part of the scanning API.
+ * Reads from the range in \c r according to the format string \c f.
+ *
+ * \code{.cpp}
+ * int i;
+ * scn::scan("123", "{}", i);
+ * // i == 123
+ * \endcode
+ */
+#if SCN_DOXYGEN
+ template <typename Range, typename Format, typename... Args>
+ auto scan(Range&& r, const Format& f, Args&... a)
+ -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Range, typename Format, typename... Args>
+ SCN_NODISCARD auto scan(Range&& r, const Format& f, Args&... a)
+ -> detail::scan_result_for_range<Range>
+ {
+ return detail::scan_boilerplate(SCN_FWD(r), f, a...);
+ }
+#endif
+
+ // default format
+
+ /**
+ * Equivalent to \ref scan, but with a
+ * format string with the appropriate amount of space-separated `"{}"`s for
+ * the number of arguments. Because this function doesn't have to parse the
+ * format string, performance is improved.
+ *
+ * Adapted from the example for \ref scan
+ * \code{.cpp}
+ * int i;
+ * scn::scan_default("123", i);
+ * // i == 123
+ * \endcode
+ *
+ * \see scan
+ */
+#if SCN_DOXYGEN
+ template <typename Range, typename... Args>
+ auto scan_default(Range&& r, Args&... a)
+ -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Range, typename... Args>
+ SCN_NODISCARD auto scan_default(Range&& r, Args&... a)
+ -> detail::scan_result_for_range<Range>
+ {
+ return detail::scan_boilerplate_default(std::forward<Range>(r), a...);
+ }
+#endif
+
+ // scan localized
+
+ /**
+ * Read from the range in \c r using the locale in \c loc.
+ * \c loc must be a \c std::locale. The parameter is a template to avoid
+ * inclusion of `<locale>`.
+ *
+ * Use of this function is discouraged, due to the overhead involved
+ * with locales. Note, that the other functions are completely
+ * locale-agnostic, and aren't affected by changes to the global C
+ * locale.
+ *
+ * \code{.cpp}
+ * double d;
+ * scn::scan_localized(std::locale{"fi_FI"}, "3,14", "{}", d);
+ * // d == 3.14
+ * \endcode
+ *
+ * \see scan
+ */
+#if SCN_DOXYGEN
+ template <typename Locale,
+ typename Range,
+ typename Format,
+ typename... Args>
+ auto scan_localized(const Locale& loc,
+ Range&& r,
+ const Format& f,
+ Args&... a) -> detail::scan_result_for_range<Range>;
+#else
+ template <typename Locale,
+ typename Range,
+ typename Format,
+ typename... Args>
+ SCN_NODISCARD auto scan_localized(const Locale& loc,
+ Range&& r,
+ const Format& f,
+ Args&... a)
+ -> detail::scan_result_for_range<Range>
+ {
+ return detail::scan_boilerplate_localized(loc, std::forward<Range>(r),
+ f, a...);
+ }
+#endif
+
+ // value
+
+ /**
+ * Scans a single value with the default options, returning it instead of
+ * using an output parameter.
+ *
+ * The parsed value is in `ret.value()`, if `ret == true`.
+ * The return type of this function is otherwise similar to other scanning
+ * functions.
+ *
+ * \code{.cpp}
+ * auto ret = scn::scan_value<int>("42");
+ * if (ret) {
+ * // ret.value() == 42
+ * }
+ * \endcode
+ */
+#if SCN_DOXYGEN
+ template <typename T, typename Range>
+ auto scan_value(Range&& r)
+ -> detail::generic_scan_result_for_range<expected<T>, Range>;
+#else
+ template <typename T, typename Range>
+ SCN_NODISCARD auto scan_value(Range&& r)
+ -> detail::generic_scan_result_for_range<expected<T>, Range>
+ {
+ T value;
+ auto range = wrap(SCN_FWD(r));
+ auto args = make_args_for(range, 1, value);
+ auto ret = vscan_default(SCN_MOVE(range), 1, {args});
+ if (ret.err) {
+ return detail::wrap_result(expected<T>{value},
+ detail::range_tag<Range>{},
+ SCN_MOVE(ret.range));
+ }
+ return detail::wrap_result(expected<T>{ret.err},
+ detail::range_tag<Range>{},
+ SCN_MOVE(ret.range));
+ }
+#endif
+
+ // input
+
+ /**
+ * Otherwise equivalent to \ref scan, expect reads from `stdin`.
+ * Character type is determined by the format string.
+ * Syncs with `<cstdio>`.
+ */
+ template <typename Format,
+ typename... Args,
+ typename CharT = ranges::range_value_t<Format>>
+#if SCN_DOXYGEN
+ auto input(const Format& f, Args&... a)
+ -> detail::scan_result_for_range<basic_file<CharT>&>;
+#else
+ SCN_NODISCARD auto input(const Format& f, Args&... a)
+ -> detail::scan_result_for_range<basic_file<CharT>&>
+ {
+ auto& range = stdin_range<CharT>();
+ auto ret = detail::scan_boilerplate(range, f, a...);
+ range.sync();
+ ret.range().reset_begin_iterator();
+ return ret;
+ }
+#endif
+
+ // prompt
+
+ namespace detail {
+ inline void put_stdout(const char* str)
+ {
+ std::fputs(str, stdout);
+ }
+ inline void put_stdout(const wchar_t* str)
+ {
+ std::fputws(str, stdout);
+ }
+ } // namespace detail
+
+ /**
+ * Equivalent to \ref input, except writes what's in `p` to `stdout`.
+ *
+ * \code{.cpp}
+ * int i{};
+ * scn::prompt("What's your favorite number? ", "{}", i);
+ * // Equivalent to:
+ * // std::fputs("What's your favorite number? ", stdout);
+ * // scn::input("{}", i);
+ * \endcode
+ */
+#if SCN_DOXYGEN
+ template <typename CharT, typename Format, typename... Args>
+ auto prompt(const CharT* p, const Format& f, Args&... a)
+ -> detail::scan_result_for_range<basic_file<CharT>&>;
+#else
+ template <typename CharT, typename Format, typename... Args>
+ SCN_NODISCARD auto prompt(const CharT* p, const Format& f, Args&... a)
+ -> detail::scan_result_for_range<basic_file<CharT>&>
+ {
+ SCN_EXPECT(p != nullptr);
+ detail::put_stdout(p);
+
+ return input(f, a...);
+ }
+#endif
+
+ // parse_integer
+
+ /**
+ * Parses an integer into \c val in base \c base from \c str.
+ * Returns a pointer past the last character read, or an error.
+ *
+ * @param str source, can't be empty, cannot have:
+ * - preceding whitespace
+ * - preceding \c "0x" or \c "0" (base is determined by the \c base
+ * parameter)
+ * - \c '+' sign (\c '-' is fine)
+ * @param val parsed integer, must be value-constructed
+ * @param base between [2,36]
+ */
+#if SCN_DOXYGEN
+ template <typename T, typename CharT>
+ expected<const CharT*> parse_integer(basic_string_view<CharT> str,
+ T& val,
+ int base = 10);
+#else
+ template <typename T, typename CharT>
+ SCN_NODISCARD expected<const CharT*>
+ parse_integer(basic_string_view<CharT> str, T& val, int base = 10)
+ {
+ SCN_EXPECT(!str.empty());
+ auto s = detail::simple_integer_scanner<T>{};
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ auto ret =
+ s.scan_lower(span<const CharT>(str.data(), str.size()), val, base);
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ if (!ret) {
+ return ret.error();
+ }
+ return {ret.value()};
+ }
+#endif
+
+ /**
+ * Parses float into \c val from \c str.
+ * Returns a pointer past the last character read, or an error.
+ *
+ * @param str source, can't be empty
+ * @param val parsed float, must be value-constructed
+ */
+#if SCN_DOXYGEN
+ template <typename T, typename CharT>
+ expected<const CharT*> parse_float(basic_string_view<CharT> str, T& val);
+#else
+ template <typename T, typename CharT>
+ SCN_NODISCARD expected<const CharT*> parse_float(
+ basic_string_view<CharT> str,
+ T& val)
+ {
+ SCN_EXPECT(!str.empty());
+ auto s = detail::float_scanner_access<T>{};
+ auto ret = s._read_float(val, make_span(str.data(), str.size()),
+ detail::ascii_widen<CharT>('.'));
+ if (!ret) {
+ return ret.error();
+ }
+ return {str.data() + ret.value()};
+ }
+#endif
+
+ /**
+ * A convenience function for creating scanners for user-provided types.
+ *
+ * Wraps \ref vscan_usertype
+ *
+ * Example use:
+ *
+ * \code{.cpp}
+ * // Type has two integers, and its textual representation is
+ * // "[val1, val2]"
+ * struct user_type {
+ * int val1;
+ * int val2;
+ * };
+ *
+ * template <>
+ * struct scn::scanner<user_type> : public scn::empty_parser {
+ * template <typename Context>
+ * error scan(user_type& val, Context& ctx)
+ * {
+ * return scan_usertype(ctx, "[{}, {}]", val.val1, val.val2);
+ * }
+ * };
+ * \endcode
+ *
+ * \param ctx Context given to the scanning function
+ * \param f Format string to parse
+ * \param a Member types (etc) to parse
+ */
+#if SCN_DOXYGEN
+ template <typename WrappedRange, typename Format, typename... Args>
+ error scan_usertype(basic_context<WrappedRange>& ctx,
+ const Format& f,
+ Args&... a);
+#else
+ template <typename WrappedRange, typename Format, typename... Args>
+ SCN_NODISCARD error scan_usertype(basic_context<WrappedRange>& ctx,
+ const Format& f,
+ Args&... a)
+ {
+ static_assert(sizeof...(Args) > 0,
+ "Have to scan at least a single argument");
+
+ using char_type = typename WrappedRange::char_type;
+ auto args = make_args<basic_context<WrappedRange>,
+ basic_parse_context<char_type>>(a...);
+ return vscan_usertype(ctx, basic_string_view<char_type>(f), {args});
+ }
+#endif
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_DETAIL_SCAN_H
diff --git a/src/third-party/scnlib/include/scn/scan/vscan.h b/src/third-party/scnlib/include/scn/scan/vscan.h
new file mode 100644
index 0000000..86bf23e
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/scan/vscan.h
@@ -0,0 +1,208 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_SCAN_VSCAN_H
+#define SCN_SCAN_VSCAN_H
+
+#include "../detail/context.h"
+#include "../detail/file.h"
+#include "../detail/parse_context.h"
+#include "../detail/visitor.h"
+#include "common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ // Avoid documentation issues: without this, Doxygen will think
+ // SCN_BEGIN_NAMESPACE is a part of the vscan declaration
+ namespace dummy {
+ }
+
+ /**
+ * Type returned by `vscan` and others
+ */
+ template <typename WrappedRange>
+ struct vscan_result {
+ error err;
+ WrappedRange range;
+ };
+
+ namespace detail {
+ template <typename WrappedRange,
+ typename CharT = typename WrappedRange::char_type>
+ vscan_result<WrappedRange> vscan_boilerplate(
+ WrappedRange&& r,
+ basic_string_view<CharT> fmt,
+ basic_args<CharT> args)
+ {
+ auto ctx = make_context(SCN_MOVE(r));
+ auto pctx = make_parse_context(fmt, ctx.locale());
+ auto err = visit(ctx, pctx, SCN_MOVE(args));
+ return {err, SCN_MOVE(ctx.range())};
+ }
+
+ template <typename WrappedRange,
+ typename CharT = typename WrappedRange::char_type>
+ vscan_result<WrappedRange> vscan_boilerplate_default(
+ WrappedRange&& r,
+ int n_args,
+ basic_args<CharT> args)
+ {
+ auto ctx = make_context(SCN_MOVE(r));
+ auto pctx = make_parse_context(n_args, ctx.locale());
+ auto err = visit(ctx, pctx, SCN_MOVE(args));
+ return {err, SCN_MOVE(ctx.range())};
+ }
+
+ template <typename WrappedRange,
+ typename Format,
+ typename CharT = typename WrappedRange::char_type>
+ vscan_result<WrappedRange> vscan_boilerplate_localized(
+ WrappedRange&& r,
+ basic_locale_ref<CharT>&& loc,
+ const Format& fmt,
+ basic_args<CharT> args)
+ {
+ auto ctx = make_context(SCN_MOVE(r), SCN_MOVE(loc));
+ auto pctx = make_parse_context(fmt, ctx.locale());
+ auto err = visit(ctx, pctx, SCN_MOVE(args));
+ return {err, SCN_MOVE(ctx.range())};
+ }
+ } // namespace detail
+
+ /**
+ * In the spirit of {fmt}/`std::format` and `vformat`, `vscan` behaves
+ * similarly to \ref scan, except instead of taking a variadic argument
+ * pack, it takes an object of type `basic_args`, which type-erases the
+ * arguments to scan. This, in effect, will decrease generated code size and
+ * compile times dramatically.
+ *
+ * \param range Source range that has been wrapped with `detail::wrap`, and
+ * passed in as an rvalue.
+ * \param fmt Format string to use
+ * \param args Type-erased values to read
+ */
+ template <typename WrappedRange,
+ typename CharT = typename WrappedRange::char_type>
+ vscan_result<WrappedRange> vscan(WrappedRange range,
+ basic_string_view<CharT> fmt,
+ basic_args<CharT>&& args)
+ {
+ return detail::vscan_boilerplate(SCN_MOVE(range), fmt, SCN_MOVE(args));
+ }
+
+ /**
+ * To be used with `scan_default`
+ *
+ * \param range Source range that has been wrapped with `detail::wrap`, and
+ * passed in as an rvalue.
+ * \param n_args Number of arguments in args
+ * \param args Type-erased values to read
+ *
+ * \see vscan
+ */
+ template <typename WrappedRange,
+ typename CharT = typename WrappedRange::char_type>
+ vscan_result<WrappedRange> vscan_default(WrappedRange range,
+ int n_args,
+ basic_args<CharT>&& args)
+ {
+ return detail::vscan_boilerplate_default(SCN_MOVE(range), n_args,
+ SCN_MOVE(args));
+ }
+
+ /**
+ * To be used with `scan_localized`
+ *
+ * \param loc Locale to use
+ * \param range Source range that has been wrapped with `detail::wrap`, and
+ * passed in as an rvalue.
+ * \param fmt Format string to use
+ * \param args Type-erased values to read
+ *
+ * \see vscan
+ */
+ template <typename WrappedRange,
+ typename CharT = typename WrappedRange::char_type>
+ vscan_result<WrappedRange> vscan_localized(WrappedRange range,
+ basic_locale_ref<CharT>&& loc,
+ basic_string_view<CharT> fmt,
+ basic_args<CharT>&& args)
+ {
+ return detail::vscan_boilerplate_localized(
+ SCN_MOVE(range), SCN_MOVE(loc), fmt, SCN_MOVE(args));
+ }
+
+ /**
+ * \see scan_usertype
+ * \see vscan
+ */
+ template <typename WrappedRange,
+ typename CharT = typename WrappedRange::char_type>
+ error vscan_usertype(basic_context<WrappedRange>& ctx,
+ basic_string_view<CharT> f,
+ basic_args<CharT>&& args)
+ {
+ auto pctx = make_parse_context(f, ctx.locale());
+ return visit(ctx, pctx, SCN_MOVE(args));
+ }
+
+#if !defined(SCN_HEADER_ONLY) || !SCN_HEADER_ONLY
+
+#define SCN_VSCAN_DECLARE(Range, WrappedAlias, CharAlias) \
+ namespace detail { \
+ namespace vscan_macro { \
+ using WrappedAlias = range_wrapper_for_t<Range>; \
+ using CharAlias = typename WrappedAlias::char_type; \
+ } \
+ } \
+ vscan_result<detail::vscan_macro::WrappedAlias> vscan( \
+ detail::vscan_macro::WrappedAlias&&, \
+ basic_string_view<detail::vscan_macro::CharAlias>, \
+ basic_args<detail::vscan_macro::CharAlias>&&); \
+ \
+ vscan_result<detail::vscan_macro::WrappedAlias> vscan_default( \
+ detail::vscan_macro::WrappedAlias&&, int, \
+ basic_args<detail::vscan_macro::CharAlias>&&); \
+ \
+ vscan_result<detail::vscan_macro::WrappedAlias> vscan_localized( \
+ detail::vscan_macro::WrappedAlias&&, \
+ basic_locale_ref<detail::vscan_macro::CharAlias>&&, \
+ basic_string_view<detail::vscan_macro::CharAlias>, \
+ basic_args<detail::vscan_macro::CharAlias>&&); \
+ \
+ error vscan_usertype(basic_context<detail::vscan_macro::WrappedAlias>&, \
+ basic_string_view<detail::vscan_macro::CharAlias>, \
+ basic_args<detail::vscan_macro::CharAlias>&&)
+
+ SCN_VSCAN_DECLARE(string_view, string_view_wrapped, string_view_char);
+ SCN_VSCAN_DECLARE(wstring_view, wstring_view_wrapped, wstring_view_char);
+ SCN_VSCAN_DECLARE(std::string, string_wrapped, string_char);
+ SCN_VSCAN_DECLARE(std::wstring, wstring_wrapped, wstring_char);
+ SCN_VSCAN_DECLARE(file&, file_ref_wrapped, file_ref_char);
+ SCN_VSCAN_DECLARE(wfile&, wfile_ref_wrapped, wfile_ref_char);
+
+#endif // !SCN_HEADER_ONLY
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY && !defined(SCN_VSCAN_CPP)
+#include "vscan.cpp"
+#endif
+
+#endif // SCN_SCAN_VSCAN_H
diff --git a/src/third-party/scnlib/include/scn/scn.h b/src/third-party/scnlib/include/scn/scn.h
new file mode 100644
index 0000000..ad4e062
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/scn.h
@@ -0,0 +1,26 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_SCN_H
+#define SCN_SCN_H
+
+#include "scan/scan.h"
+#include "scan/getline.h"
+#include "scan/ignore.h"
+#include "scan/list.h"
+
+#endif // SCN_SCN_H
diff --git a/src/third-party/scnlib/include/scn/tuple_return.h b/src/third-party/scnlib/include/scn/tuple_return.h
new file mode 100644
index 0000000..fc5ecb0
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/tuple_return.h
@@ -0,0 +1,23 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_TUPLE_RETURN_H
+#define SCN_TUPLE_RETURN_H
+
+#include "tuple_return/tuple_return.h"
+
+#endif // SCN_TUPLE_RETURN_H
diff --git a/src/third-party/scnlib/include/scn/tuple_return/tuple_return.h b/src/third-party/scnlib/include/scn/tuple_return/tuple_return.h
new file mode 100644
index 0000000..91d6254
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/tuple_return/tuple_return.h
@@ -0,0 +1,123 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_TUPLE_RETURN_TUPLE_RETURN_H
+#define SCN_TUPLE_RETURN_TUPLE_RETURN_H
+
+#include "../scan/vscan.h"
+#include "util.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace dummy {
+ }
+
+/**
+ * Alternative interface for scanning, returning values as a tuple, instead
+ * of taking them by reference.
+ *
+ * It's highly recommended to use this interface only with C++17 or later,
+ * as structured bindings make it way more ergonomic.
+ *
+ * Compared to the regular scan interface, the performance of this interface
+ * is the same (generated code is virtually identical with optimizations
+ * enabled), but the compile time is slower.
+ *
+ * Values scanned by this function still need to be default-constructible.
+ * To scan a non-default-constructible value, use \c scn::optional
+ *
+ * \param r Input range
+ * \param f Format string to use
+ *
+ * \return Tuple, where the first element is the scan result, and the
+ * remaining elements are the scanned values.
+ */
+#if SCN_DOXYGEN
+ template <typename... Args, typename Range, typename Format>
+ auto scan_tuple(Range&& r, Format f)
+ -> std::tuple<detail::scan_result_for_range<Range>, Args...>;
+#else
+ template <typename... Args, typename Range, typename Format>
+ SCN_NODISCARD auto scan_tuple(Range&& r, Format f)
+ -> std::tuple<detail::scan_result_for_range<Range>, Args...>
+ {
+ using result = detail::scan_result_for_range<Range>;
+ using range_type = typename result::wrapped_range_type;
+
+ using context_type = basic_context<range_type>;
+ using parse_context_type =
+ basic_parse_context<typename context_type::locale_type>;
+ using char_type = typename range_type::char_type;
+
+ auto range = wrap(SCN_FWD(r));
+ auto scanfn = [&range, &f](Args&... a) {
+ auto args = make_args<context_type, parse_context_type>(a...);
+ return vscan(SCN_MOVE(range), basic_string_view<char_type>(f),
+ {args});
+ };
+
+ std::tuple<Args...> values{Args{}...};
+ auto ret = detail::apply(scanfn, values);
+ return std::tuple_cat(
+ std::tuple<result>{detail::wrap_result(wrapped_error{ret.err},
+ detail::range_tag<Range>{},
+ SCN_MOVE(ret.range))},
+ SCN_MOVE(values));
+ }
+#endif
+
+ /**
+ * Equivalent to `scan_tuple`, except uses `vscan_default` under the hood.
+ */
+#if SCN_DOXYGEN
+ template <typename... Args, typename Range>
+ auto scan_tuple_default(Range&& r)
+ -> std::tuple<detail::scan_result_for_range<Range>, Args...>;
+#else
+ template <typename... Args, typename Range>
+ SCN_NODISCARD auto scan_tuple_default(Range&& r)
+ -> std::tuple<detail::scan_result_for_range<Range>, Args...>
+ {
+ using result = detail::scan_result_for_range<Range>;
+ using range_type = typename result::wrapped_range_type;
+
+ using context_type = basic_context<range_type>;
+ using parse_context_type =
+ basic_empty_parse_context<typename context_type::locale_type>;
+
+ auto range = wrap(SCN_FWD(r));
+ auto scanfn = [&range](Args&... a) {
+ auto args = make_args<context_type, parse_context_type>(a...);
+ return vscan_default(SCN_MOVE(range),
+ static_cast<int>(sizeof...(Args)), {args});
+ };
+
+ std::tuple<Args...> values{Args{}...};
+ auto ret = detail::apply(scanfn, values);
+ return std::tuple_cat(
+ std::tuple<result>{detail::wrap_result(wrapped_error{ret.err},
+ detail::range_tag<Range>{},
+ SCN_MOVE(ret.range))},
+ SCN_MOVE(values));
+ }
+#endif
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/tuple_return/util.h b/src/third-party/scnlib/include/scn/tuple_return/util.h
new file mode 100644
index 0000000..be0e2ab
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/tuple_return/util.h
@@ -0,0 +1,176 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_TUPLE_RETURN_UTIL_H
+#define SCN_TUPLE_RETURN_UTIL_H
+
+#include "../util/meta.h"
+
+#include <functional>
+#include <tuple>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ // From cppreference
+ template <typename Fn,
+ typename... Args,
+ typename std::enable_if<std::is_member_pointer<
+ typename std::decay<Fn>::type>::value>::type* = nullptr,
+ int = 0>
+ constexpr auto invoke(Fn&& f, Args&&... args) noexcept(
+ noexcept(std::mem_fn(f)(std::forward<Args>(args)...)))
+ -> decltype(std::mem_fn(f)(std::forward<Args>(args)...))
+ {
+ return std::mem_fn(f)(std::forward<Args>(args)...);
+ }
+
+ template <typename Fn,
+ typename... Args,
+ typename std::enable_if<!std::is_member_pointer<
+ typename std::decay<Fn>::type>::value>::type* = nullptr>
+ constexpr auto invoke(Fn&& f, Args&&... args) noexcept(
+ noexcept(std::forward<Fn>(f)(std::forward<Args>(args)...)))
+ -> decltype(std::forward<Fn>(f)(std::forward<Args>(args)...))
+ {
+ return std::forward<Fn>(f)(std::forward<Args>(args)...);
+ }
+
+ // From Boost.mp11
+ template <typename T, T... I>
+ struct integer_sequence {
+ };
+
+ // iseq_if_c
+ template <bool C, typename T, typename E>
+ struct iseq_if_c_impl;
+
+ template <typename T, typename E>
+ struct iseq_if_c_impl<true, T, E> {
+ using type = T;
+ };
+
+ template <typename T, typename E>
+ struct iseq_if_c_impl<false, T, E> {
+ using type = E;
+ };
+
+ template <bool C, typename T, typename E>
+ using iseq_if_c = typename iseq_if_c_impl<C, T, E>::type;
+
+ // iseq_identity
+ template <typename T>
+ struct iseq_identity {
+ using type = T;
+ };
+
+ template <typename S1, typename S2>
+ struct append_integer_sequence;
+
+ template <typename T, T... I, T... J>
+ struct append_integer_sequence<integer_sequence<T, I...>,
+ integer_sequence<T, J...>> {
+ using type = integer_sequence<T, I..., (J + sizeof...(I))...>;
+ };
+
+ template <typename T, T N>
+ struct make_integer_sequence_impl;
+
+ template <typename T, T N>
+ struct make_integer_sequence_impl_ {
+ private:
+ static_assert(
+ N >= 0,
+ "make_integer_sequence<T, N>: N must not be negative");
+
+ static T const M = N / 2;
+ static T const R = N % 2;
+
+ using S1 = typename make_integer_sequence_impl<T, M>::type;
+ using S2 = typename append_integer_sequence<S1, S1>::type;
+ using S3 = typename make_integer_sequence_impl<T, R>::type;
+ using S4 = typename append_integer_sequence<S2, S3>::type;
+
+ public:
+ using type = S4;
+ };
+
+ template <typename T, T N>
+ struct make_integer_sequence_impl
+ : iseq_if_c<N == 0,
+ iseq_identity<integer_sequence<T>>,
+ iseq_if_c<N == 1,
+ iseq_identity<integer_sequence<T, 0>>,
+ make_integer_sequence_impl_<T, N>>> {
+ };
+
+ // make_integer_sequence
+ template <typename T, T N>
+ using make_integer_sequence =
+ typename detail::make_integer_sequence_impl<T, N>::type;
+
+ // index_sequence
+ template <std::size_t... I>
+ using index_sequence = integer_sequence<std::size_t, I...>;
+
+ // make_index_sequence
+ template <std::size_t N>
+ using make_index_sequence = make_integer_sequence<std::size_t, N>;
+
+ // index_sequence_for
+ template <typename... T>
+ using index_sequence_for =
+ make_integer_sequence<std::size_t, sizeof...(T)>;
+
+ // From cppreference
+ template <class F, class Tuple, std::size_t... I>
+ constexpr auto
+ apply_impl(F&& f, Tuple&& t, index_sequence<I...>) noexcept(
+ noexcept(detail::invoke(std::forward<F>(f),
+ std::get<I>(std::forward<Tuple>(t))...)))
+ -> decltype(detail::invoke(std::forward<F>(f),
+ std::get<I>(std::forward<Tuple>(t))...))
+ {
+ return detail::invoke(std::forward<F>(f),
+ std::get<I>(std::forward<Tuple>(t))...);
+ } // namespace detail
+
+ template <class F, class Tuple>
+ constexpr auto apply(F&& f, Tuple&& t) noexcept(
+ noexcept(detail::apply_impl(
+ std::forward<F>(f),
+ std::forward<Tuple>(t),
+ make_index_sequence<std::tuple_size<
+ typename std::remove_reference<Tuple>::type>::value>{})))
+ -> decltype(detail::apply_impl(
+ std::forward<F>(f),
+ std::forward<Tuple>(t),
+ make_index_sequence<std::tuple_size<
+ typename std::remove_reference<Tuple>::type>::value>{}))
+ {
+ return detail::apply_impl(
+ std::forward<F>(f), std::forward<Tuple>(t),
+ make_index_sequence<std::tuple_size<
+ typename std::remove_reference<Tuple>::type>::value>{});
+ }
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/unicode/common.h b/src/third-party/scnlib/include/scn/unicode/common.h
new file mode 100644
index 0000000..3807793
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/unicode/common.h
@@ -0,0 +1,139 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+//
+// The contents of this file are based on utfcpp:
+// https://github.com/nemtrif/utfcpp
+// Copyright (c) 2006 Nemanja Trifunovic
+// Distributed under the Boost Software License, version 1.0
+
+#ifndef SCN_UNICODE_COMMON_H
+#define SCN_UNICODE_COMMON_H
+
+#include "../detail/fwd.h"
+
+#include <cstdint>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ /**
+ * A Unicode code point
+ */
+ enum class code_point : uint32_t {};
+
+ template <typename T>
+ constexpr bool operator==(code_point a, T b)
+ {
+ return static_cast<uint32_t>(a) == static_cast<uint32_t>(b);
+ }
+ template <typename T>
+ constexpr bool operator!=(code_point a, T b)
+ {
+ return static_cast<uint32_t>(a) != static_cast<uint32_t>(b);
+ }
+ template <typename T>
+ constexpr bool operator<(code_point a, T b)
+ {
+ return static_cast<uint32_t>(a) < static_cast<uint32_t>(b);
+ }
+ template <typename T>
+ constexpr bool operator>(code_point a, T b)
+ {
+ return static_cast<uint32_t>(a) > static_cast<uint32_t>(b);
+ }
+ template <typename T>
+ constexpr bool operator<=(code_point a, T b)
+ {
+ return static_cast<uint32_t>(a) <= static_cast<uint32_t>(b);
+ }
+ template <typename T>
+ constexpr bool operator>=(code_point a, T b)
+ {
+ return static_cast<uint32_t>(a) >= static_cast<uint32_t>(b);
+ }
+
+ namespace detail {
+ static constexpr const uint16_t lead_surrogate_min = 0xd800;
+ static constexpr const uint16_t lead_surrogate_max = 0xdbff;
+ static constexpr const uint16_t trail_surrogate_min = 0xdc00;
+ static constexpr const uint16_t trail_surrogate_max = 0xdfff;
+ static constexpr const uint16_t lead_offset =
+ lead_surrogate_min - (0x10000u >> 10);
+ static constexpr const uint32_t surrogate_offset =
+ 0x10000u - (lead_surrogate_min << 10) - trail_surrogate_min;
+ static constexpr const uint32_t code_point_max = 0x10ffff;
+
+ template <typename Octet>
+ constexpr uint8_t mask8(Octet o)
+ {
+ return static_cast<uint8_t>(0xff & o);
+ }
+ template <typename U16>
+ constexpr uint16_t mask16(U16 v)
+ {
+ return static_cast<uint16_t>(0xffff & v);
+ }
+ template <typename U16>
+ constexpr bool is_lead_surrogate(U16 cp)
+ {
+ return cp >= lead_surrogate_min && cp <= lead_surrogate_max;
+ }
+ template <typename U16>
+ constexpr bool is_trail_surrogate(U16 cp)
+ {
+ return cp >= trail_surrogate_min && cp <= trail_surrogate_max;
+ }
+ template <typename U16>
+ constexpr bool is_surrogate(U16 cp)
+ {
+ return cp >= lead_surrogate_min && cp <= trail_surrogate_max;
+ }
+
+ constexpr inline bool is_code_point_valid(code_point cp)
+ {
+ return cp <= code_point_max && !is_surrogate(cp);
+ }
+ } // namespace detail
+
+ template <typename T>
+ constexpr code_point make_code_point(T ch)
+ {
+ return static_cast<code_point>(ch);
+ }
+
+ /**
+ * Returns `true`, if `cp` is valid, e.g. is less than or equal to the
+ * maximum value for a code point (U+10FFFF), and is not a surrogate (U+D800
+ * to U+DFFF).
+ */
+ constexpr inline bool is_valid_code_point(code_point cp)
+ {
+ return detail::is_code_point_valid(cp);
+ }
+ /**
+ * Returns `true` if `cp` can be encoded in ASCII as-is (is between U+0 and
+ * U+7F)
+ */
+ constexpr inline bool is_ascii_code_point(code_point cp)
+ {
+ return cp <= 0x7f;
+ }
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/unicode/unicode.h b/src/third-party/scnlib/include/scn/unicode/unicode.h
new file mode 100644
index 0000000..011b0b9
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/unicode/unicode.h
@@ -0,0 +1,243 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+//
+// The contents of this file are based on utfcpp:
+// https://github.com/nemtrif/utfcpp
+// Copyright (c) 2006 Nemanja Trifunovic
+// Distributed under the Boost Software License, version 1.0
+
+#ifndef SCN_UNICODE_UNICODE_H
+#define SCN_UNICODE_UNICODE_H
+
+#include "utf16.h"
+#include "utf8.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ inline constexpr bool is_wide_multichar()
+ {
+ return sizeof(wchar_t) == 2;
+ }
+
+ inline constexpr bool is_multichar_type(char)
+ {
+ return true;
+ }
+ inline constexpr bool is_multichar_type(wchar_t)
+ {
+ return is_wide_multichar();
+ }
+
+ using utf8_tag = std::integral_constant<size_t, 1>;
+ using utf16_tag = std::integral_constant<size_t, 2>;
+ using utf32_tag = std::integral_constant<size_t, 4>;
+
+#define SCN_MAKE_UTF_TAG(CharT) \
+ std::integral_constant<size_t, sizeof(CharT)> {}
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> parse_code_point(I begin,
+ S end,
+ code_point& cp,
+ utf8_tag)
+ {
+ return utf8::parse_code_point(begin, end, cp);
+ }
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> parse_code_point(I begin,
+ S end,
+ code_point& cp,
+ utf16_tag)
+ {
+ return utf16::parse_code_point(begin, end, cp);
+ }
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> parse_code_point(I begin,
+ S end,
+ code_point& cp,
+ utf32_tag)
+ {
+ SCN_EXPECT(begin != end);
+ cp = make_code_point(*begin);
+ return {++begin};
+ }
+ } // namespace detail
+
+ /**
+ * Parses a Unicode code point from the range at `[begin, end)`, and writes
+ * it into `cp`.
+ *
+ * The encoding is determined by the size of the value type of the range.
+ * Let `n = sizeof(typename std::iterator_traits<I>::value_type)`.
+ * If `n == 1` -> UTF-8. If `n == 2` -> UTF-16. If `n == 4` -> UTF-32.
+ *
+ * `begin != end` must be `true`.
+ *
+ * On error, `cp` is not written into.
+ *
+ * \return On success, returns an iterator one-past the last code unit used
+ * to parse `cp`. If the code point is encoded incorrectly, returns
+ * `error::invalid_encoding`.
+ */
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> parse_code_point(I begin, S end, code_point& cp)
+ {
+ return detail::parse_code_point(
+ begin, end, cp,
+ SCN_MAKE_UTF_TAG(typename std::iterator_traits<I>::value_type));
+ }
+
+ namespace detail {
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> encode_code_point(I begin,
+ S end,
+ code_point cp,
+ utf8_tag)
+ {
+ return utf8::encode_code_point(begin, end, cp);
+ }
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> encode_code_point(I begin,
+ S end,
+ code_point cp,
+ utf16_tag)
+ {
+ return utf16::encode_code_point(begin, end, cp);
+ }
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> encode_code_point(I begin,
+ S end,
+ code_point cp,
+ utf32_tag)
+ {
+ SCN_EXPECT(begin + 1 >= end);
+ *begin++ = static_cast<uint32_t>(cp);
+ return {begin};
+ }
+ } // namespace detail
+
+ /**
+ * Writes the code point `cp` into `begin`, using the encoding determined by
+ * the type of `begin`.
+ *
+ * For more information on how the encoding is determined, see \ref
+ * parse_code_point().
+ *
+ * `end` must be reachable from `begin`, and must have enough room to encode
+ * the code point (4 code units for UTF-8, 2 for UTF-16, and 1 for UTF-32).
+ *
+ * \param begin Beginning of the range to write the result to
+ * \param end End of the range to write the result to
+ * \param cp Code point to encode
+ * \return On success, one-past the last code unit written.
+ * If `cp` was not a valid code point, returns `error::invalid_encoding`.
+ */
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> encode_code_point(I begin, S end, code_point cp)
+ {
+ return detail::encode_code_point(
+ begin, end, cp,
+ SCN_MAKE_UTF_TAG(typename std::iterator_traits<I>::value_type));
+ }
+
+ namespace detail {
+ template <typename T>
+ SCN_CONSTEXPR14 int get_sequence_length(T a, utf8_tag)
+ {
+ return utf8::get_sequence_length(a);
+ }
+ template <typename T>
+ SCN_CONSTEXPR14 int get_sequence_length(T a, utf16_tag)
+ {
+ return utf16::get_sequence_length(a);
+ }
+ template <typename T>
+ SCN_CONSTEXPR14 int get_sequence_length(T, utf32_tag)
+ {
+ return 1;
+ }
+ } // namespace detail
+
+ /**
+ * Returns the length of the code point starting from code unit `a` in code
+ * units.
+ *
+ * For information on how the encoding is determined, see \ref
+ * parse_code_point().
+ *
+ * \param a The first code unit in a code point.
+ *
+ * \return Length of the code point starting from `a`, in code units
+ * If the code point is encoded incorrectly, or this code unit is not the
+ * first code unit in a code point, returns 0.
+ */
+ template <typename T>
+ SCN_CONSTEXPR14 int get_sequence_length(T a)
+ {
+ return detail::get_sequence_length(a, SCN_MAKE_UTF_TAG(T));
+ }
+
+ namespace detail {
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<std::ptrdiff_t> code_point_distance(I begin,
+ S end,
+ utf8_tag)
+ {
+ return utf8::code_point_distance(begin, end);
+ }
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<std::ptrdiff_t> code_point_distance(I begin,
+ S end,
+ utf16_tag)
+ {
+ return utf16::code_point_distance(begin, end);
+ }
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<std::ptrdiff_t> code_point_distance(I begin,
+ S end,
+ utf32_tag)
+ {
+ return {end - begin};
+ }
+ } // namespace detail
+
+ /**
+ * Get the distance between two code points, in code points.
+ *
+ * `end >= begin` must be `true`.
+ * `begin` and `end` must both point to the first code units in a code
+ * point.
+ *
+ * \return The distance between `begin` and `end`, in code points. If the
+ * string was encoded incorrectly, returns `error::invalid_encoding`.
+ */
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<std::ptrdiff_t> code_point_distance(I begin, S end)
+ {
+ return detail::code_point_distance(
+ begin, end,
+ SCN_MAKE_UTF_TAG(typename std::iterator_traits<I>::value_type));
+ }
+
+#undef SCN_MAKE_UTF_TAG
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/unicode/utf16.h b/src/third-party/scnlib/include/scn/unicode/utf16.h
new file mode 100644
index 0000000..8d8a400
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/unicode/utf16.h
@@ -0,0 +1,139 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+//
+// The contents of this file are based on utfcpp:
+// https://github.com/nemtrif/utfcpp
+// Copyright (c) 2006 Nemanja Trifunovic
+// Distributed under the Boost Software License, version 1.0
+
+#ifndef SCN_UNICODE_UTF16_H
+#define SCN_UNICODE_UTF16_H
+
+#include "../detail/error.h"
+#include "../util/expected.h"
+#include "common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ namespace utf16 {
+ template <typename U16>
+ SCN_CONSTEXPR14 int get_sequence_length(U16 ch)
+ {
+ uint16_t lead = mask16(ch);
+ if (is_lead_surrogate(lead)) {
+ return 2;
+ }
+ if (SCN_UNLIKELY(is_trail_surrogate(lead))) {
+ return 0;
+ }
+ return 1;
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 error validate_next(I& it, S end, code_point& cp)
+ {
+ SCN_EXPECT(it != end);
+
+ uint16_t lead = mask16(*it);
+ if (is_lead_surrogate(lead)) {
+ ++it;
+ if (it == end) {
+ return {error::invalid_encoding,
+ "Lone utf16 lead surrogate"};
+ }
+ uint16_t trail = mask16(*it);
+ if (!is_trail_surrogate(trail)) {
+ return {error::invalid_encoding,
+ "Invalid utf16 trail surrogate"};
+ }
+ ++it;
+ cp = static_cast<code_point>(
+ static_cast<uint32_t>(lead << 10u) + trail +
+ surrogate_offset);
+ return {};
+ }
+ if (is_trail_surrogate(lead)) {
+ return {error::invalid_encoding,
+ "Lone utf16 trail surrogate"};
+ }
+
+ cp = static_cast<code_point>(*it);
+ ++it;
+ return {};
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> parse_code_point(I begin,
+ S end,
+ code_point& cp)
+ {
+ auto e = validate_next(begin, end, cp);
+ if (!e) {
+ return e;
+ }
+ return {begin};
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> encode_code_point(I begin,
+ S end,
+ code_point cp)
+ {
+ SCN_EXPECT(begin + 2 <= end);
+
+ if (!is_valid_code_point(cp)) {
+ return error(error::invalid_encoding,
+ "Invalid code point, cannot encode in UTF-16");
+ }
+
+ if (cp > 0xffffu) {
+ *begin++ = static_cast<uint16_t>(
+ (static_cast<uint32_t>(cp) >> 10u) + lead_offset);
+ *begin++ = static_cast<uint16_t>(
+ (static_cast<uint32_t>(cp) & 0x3ffu) +
+ trail_surrogate_min);
+ }
+ else {
+ *begin++ = static_cast<uint16_t>(cp);
+ }
+ return {begin};
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<std::ptrdiff_t> code_point_distance(
+ I begin,
+ S end)
+ {
+ std::ptrdiff_t dist{};
+ code_point cp{};
+ for (; begin < end; ++dist) {
+ auto e = validate_next(begin, end, cp);
+ if (!e) {
+ return e;
+ }
+ }
+ return {dist};
+ }
+ } // namespace utf16
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/unicode/utf8.h b/src/third-party/scnlib/include/scn/unicode/utf8.h
new file mode 100644
index 0000000..d2ee54d
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/unicode/utf8.h
@@ -0,0 +1,297 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+//
+// The contents of this file are based on utfcpp:
+// https://github.com/nemtrif/utfcpp
+// Copyright (c) 2006 Nemanja Trifunovic
+// Distributed under the Boost Software License, version 1.0
+
+#ifndef SCN_UNICODE_UTF8_H
+#define SCN_UNICODE_UTF8_H
+
+#include "../detail/error.h"
+#include "../util/expected.h"
+#include "common.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ namespace utf8 {
+ template <typename Octet>
+ constexpr bool is_trail(Octet o)
+ {
+ return (mask8(o) >> 6) == 2;
+ }
+
+ template <typename Octet>
+ SCN_CONSTEXPR14 int get_sequence_length(Octet ch)
+ {
+ uint8_t lead = detail::mask8(ch);
+ if (lead < 0x80) {
+ return 1;
+ }
+ else if ((lead >> 5) == 6) {
+ return 2;
+ }
+ else if ((lead >> 4) == 0xe) {
+ return 3;
+ }
+ else if ((lead >> 3) == 0x1e) {
+ return 4;
+ }
+ return 0;
+ }
+
+ SCN_CONSTEXPR14 bool is_overlong_sequence(code_point cp,
+ std::ptrdiff_t len)
+ {
+ if (cp < 0x80) {
+ if (len != 1) {
+ return true;
+ }
+ }
+ else if (cp < 0x800) {
+ if (len != 2) {
+ return true;
+ }
+ }
+ else if (cp < 0x10000) {
+ if (len != 3) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 error increase_safely(I& it, S end)
+ {
+ if (++it == end) {
+ return {error::invalid_encoding,
+ "Unexpected end of range when decoding utf8 "
+ "(partial codepoint)"};
+ }
+ if (!is_trail(*it)) {
+ return {error::invalid_encoding,
+ "Invalid utf8 codepoint parsed"};
+ }
+ return {};
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 error get_sequence_1(I& it, S end, code_point& cp)
+ {
+ SCN_EXPECT(it != end);
+ cp = make_code_point(mask8(*it));
+ return {};
+ }
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 error get_sequence_2(I& it, S end, code_point& cp)
+ {
+ SCN_EXPECT(it != end);
+ uint32_t c = mask8(*it);
+
+ auto e = increase_safely(it, end);
+ if (!e) {
+ return e;
+ }
+
+ c = static_cast<uint32_t>((c << 6u) & 0x7ffu) +
+ (static_cast<uint32_t>(*it) & 0x3fu);
+ cp = make_code_point(c);
+
+ return {};
+ }
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 error get_sequence_3(I& it, S end, code_point& cp)
+ {
+ SCN_EXPECT(it != end);
+ uint32_t c = mask8(*it);
+
+ auto e = increase_safely(it, end);
+ if (!e) {
+ return e;
+ }
+
+ c = static_cast<uint32_t>((c << 12u) & 0xffffu) +
+ (static_cast<uint32_t>(mask8(*it) << 6u) & 0xfffu);
+
+ e = increase_safely(it, end);
+ if (!e) {
+ return e;
+ }
+
+ c += static_cast<uint32_t>(*it) & 0x3fu;
+ cp = make_code_point(c);
+
+ return {};
+ }
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 error get_sequence_4(I& it, S end, code_point& cp)
+ {
+ SCN_EXPECT(it != end);
+ uint32_t c = mask8(*it);
+
+ auto e = increase_safely(it, end);
+ if (!e) {
+ return e;
+ }
+
+ c = ((c << 18u) & 0x1fffffu) +
+ (static_cast<uint32_t>(mask8(*it) << 12u) & 0x3ffffu);
+
+ e = increase_safely(it, end);
+ if (!e) {
+ return e;
+ }
+
+ c += static_cast<uint32_t>(mask8(*it) << 6u) & 0xfffu;
+
+ e = increase_safely(it, end);
+ if (!e) {
+ return e;
+ }
+
+ c += static_cast<uint32_t>(*it) & 0x3fu;
+ cp = make_code_point(c);
+
+ return {};
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 error validate_next(I& it, S end, code_point& cp)
+ {
+ SCN_EXPECT(it != end);
+
+ int len = get_sequence_length(*it);
+ error e{};
+ switch (len) {
+ case 1:
+ e = get_sequence_1(it, end, cp);
+ break;
+ case 2:
+ e = get_sequence_2(it, end, cp);
+ break;
+ case 3:
+ e = get_sequence_3(it, end, cp);
+ break;
+ case 4:
+ e = get_sequence_4(it, end, cp);
+ break;
+ default:
+ return {error::invalid_encoding,
+ "Invalid lead byte for utf8"};
+ }
+
+ if (!e) {
+ return e;
+ }
+ if (!is_valid_code_point(cp) || is_overlong_sequence(cp, len)) {
+ return {error::invalid_encoding, "Invalid utf8 code point"};
+ }
+ ++it;
+ return {};
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> parse_code_point(I begin,
+ S end,
+ code_point& cp)
+ {
+ code_point c{};
+ auto e = validate_next(begin, end, c);
+ if (e) {
+ cp = c;
+ return {begin};
+ }
+ return e;
+ }
+
+ template <typename I>
+ I append(code_point cp, I it)
+ {
+ SCN_EXPECT(is_code_point_valid(cp));
+
+ if (cp < 0x80) {
+ *(it++) = static_cast<uint8_t>(cp);
+ }
+ else if (cp < 0x800) {
+ *(it++) = static_cast<uint8_t>(
+ (static_cast<uint32_t>(cp) >> 6u) | 0xc0u);
+ *(it++) = static_cast<uint8_t>(
+ (static_cast<uint32_t>(cp) & 0x3fu) | 0x80u);
+ }
+ else if (cp < 0x10000) {
+ *(it++) = static_cast<uint8_t>(
+ (static_cast<uint32_t>(cp) >> 12u) | 0xe0u);
+ *(it++) = static_cast<uint8_t>(
+ ((static_cast<uint32_t>(cp) >> 6u) & 0x3fu) | 0x80u);
+ *(it++) = static_cast<uint8_t>(
+ (static_cast<uint32_t>(cp) & 0x3fu) | 0x80u);
+ }
+ else {
+ *(it++) = static_cast<uint8_t>(
+ (static_cast<uint32_t>(cp) >> 18u) | 0xf0u);
+ *(it++) = static_cast<uint8_t>(
+ ((static_cast<uint32_t>(cp) >> 12u) & 0x3fu) | 0x80u);
+ *(it++) = static_cast<uint8_t>(
+ ((static_cast<uint32_t>(cp) >> 6u) & 0x3fu) | 0x80u);
+ *(it++) = static_cast<uint8_t>(
+ (static_cast<uint32_t>(cp) & 0x3fu) | 0x80u);
+ }
+ return it;
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<I> encode_code_point(I begin,
+ S end,
+ code_point cp)
+ {
+ SCN_EXPECT(begin + 4 <= end);
+
+ if (!is_code_point_valid(cp)) {
+ return error(error::invalid_encoding,
+ "Invalid code point, cannot encode in UTF-8");
+ }
+ return {append(cp, begin)};
+ }
+
+ template <typename I, typename S>
+ SCN_CONSTEXPR14 expected<std::ptrdiff_t> code_point_distance(
+ I begin,
+ S end)
+ {
+ std::ptrdiff_t dist{};
+ code_point cp{};
+ for (; begin < end; ++dist) {
+ auto e = validate_next(begin, end, cp);
+ if (!e) {
+ return e;
+ }
+ }
+ return {dist};
+ }
+
+ } // namespace utf8
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/util/algorithm.h b/src/third-party/scnlib/include/scn/util/algorithm.h
new file mode 100644
index 0000000..a17b6b6
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/algorithm.h
@@ -0,0 +1,80 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_ALGORITHM_H
+#define SCN_UTIL_ALGORITHM_H
+
+#include "../detail/fwd.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ /**
+ * Implementation of `std::exchange` for C++11
+ */
+ template <typename T, typename U = T>
+ SCN_CONSTEXPR14 T exchange(T& obj, U&& new_value)
+ {
+ T old_value = SCN_MOVE(obj);
+ obj = SCN_FWD(new_value);
+ return old_value;
+ }
+
+ /**
+ * Implementation of `std::max` without including `<algorithm>`
+ */
+ template <typename T>
+ constexpr T max(T a, T b) noexcept
+ {
+ return (a < b) ? b : a;
+ }
+
+ /**
+ * Implementation of `std::min_element` without including `<algorithm>`
+ */
+ template <typename It>
+ SCN_CONSTEXPR14 It min_element(It first, It last)
+ {
+ if (first == last) {
+ return last;
+ }
+
+ It smallest = first;
+ ++first;
+ for (; first != last; ++first) {
+ if (*first < *smallest) {
+ smallest = first;
+ }
+ }
+ return smallest;
+ }
+
+ /**
+ * Implementation of `std::min` without including `<algorithm>`
+ */
+ template <typename T>
+ constexpr T min(T a, T b) noexcept
+ {
+ return (b < a) ? b : a;
+ }
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/util/array.h b/src/third-party/scnlib/include/scn/util/array.h
new file mode 100644
index 0000000..6c86488
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/array.h
@@ -0,0 +1,105 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_ARRAY_H
+#define SCN_UTIL_ARRAY_H
+
+#include "../detail/fwd.h"
+
+#include <cstdint>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ /**
+ * Implementation of `std::array` without including `<array>` (can be
+ * heavy-ish)
+ */
+ template <typename T, std::size_t N>
+ struct array {
+ static_assert(N > 0, "zero-sized array not supported");
+
+ using value_type = T;
+ using size_type = std::size_t;
+ using difference_type = std::ptrdiff_t;
+ using reference = T&;
+ using const_reference = const T&;
+ using pointer = T*;
+ using const_pointer = const T*;
+ using iterator = pointer;
+ using const_iterator = const_pointer;
+
+ SCN_CONSTEXPR14 reference operator[](size_type i)
+ {
+ SCN_EXPECT(i < size());
+ return m_data[i];
+ }
+ SCN_CONSTEXPR14 const_reference operator[](size_type i) const
+ {
+ SCN_EXPECT(i < size());
+ return m_data[i];
+ }
+
+ SCN_CONSTEXPR14 iterator begin() noexcept
+ {
+ return m_data;
+ }
+ constexpr const_iterator begin() const noexcept
+ {
+ return m_data;
+ }
+ constexpr const_iterator cbegin() const noexcept
+ {
+ return m_data;
+ }
+
+ SCN_CONSTEXPR14 iterator end() noexcept
+ {
+ return m_data + N;
+ }
+ constexpr const_iterator end() const noexcept
+ {
+ return m_data + N;
+ }
+ constexpr const_iterator cend() const noexcept
+ {
+ return m_data + N;
+ }
+
+ SCN_CONSTEXPR14 pointer data() noexcept
+ {
+ return m_data;
+ }
+ constexpr const_pointer data() const noexcept
+ {
+ return m_data;
+ }
+
+ SCN_NODISCARD constexpr size_type size() const noexcept
+ {
+ return N;
+ }
+
+ T m_data[N];
+ };
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/util/expected.h b/src/third-party/scnlib/include/scn/util/expected.h
new file mode 100644
index 0000000..f7c9a82
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/expected.h
@@ -0,0 +1,158 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_EXPECTED_H
+#define SCN_UTIL_EXPECTED_H
+
+#include "memory.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ /**
+ * expected-like type.
+ * For situations where there can be a value in case of success or an error
+ * code.
+ */
+ template <typename T, typename Error, typename Enable>
+ class expected;
+
+ /**
+ * expected-like type for default-constructible success values.
+ * Not optimized for space-efficiency (both members are stored
+ * simultaneously).
+ * `error` is used as the error value and discriminant flag.
+ */
+ template <typename T, typename Error>
+ class expected<T,
+ Error,
+ typename std::enable_if<
+ std::is_default_constructible<T>::value>::type> {
+ public:
+ using success_type = T;
+ using error_type = Error;
+
+ constexpr expected() = default;
+ constexpr expected(success_type s) : m_s(s) {}
+ constexpr expected(error_type e) : m_e(e) {}
+
+ SCN_NODISCARD constexpr bool has_value() const noexcept
+ {
+ return m_e == Error{};
+ }
+ constexpr explicit operator bool() const noexcept
+ {
+ return has_value();
+ }
+ constexpr bool operator!() const noexcept
+ {
+ return !operator bool();
+ }
+
+ SCN_CONSTEXPR14 success_type& value() & noexcept
+ {
+ return m_s;
+ }
+ constexpr success_type value() const& noexcept
+ {
+ return m_s;
+ }
+ SCN_CONSTEXPR14 success_type value() && noexcept
+ {
+ return SCN_MOVE(m_s);
+ }
+
+ SCN_CONSTEXPR14 error_type& error() noexcept
+ {
+ return m_e;
+ }
+ constexpr error_type error() const noexcept
+ {
+ return m_e;
+ }
+
+ private:
+ success_type m_s{};
+ error_type m_e{error_type::success_tag()};
+ };
+
+ /**
+ * expected-like type for non-default-constructible success values.
+ * Not optimized for space-efficiency.
+ * `error` is used as the error value and discriminant flag.
+ */
+ template <typename T, typename Error>
+ class expected<T,
+ Error,
+ typename std::enable_if<
+ !std::is_default_constructible<T>::value>::type> {
+ public:
+ using success_type = T;
+ using success_storage = detail::erased_storage<T>;
+ using error_type = Error;
+
+ expected(success_type s) : m_s(SCN_MOVE(s)) {}
+ constexpr expected(error_type e) : m_e(e) {}
+
+ SCN_NODISCARD constexpr bool has_value() const noexcept
+ {
+ return m_e == Error{};
+ }
+ constexpr explicit operator bool() const noexcept
+ {
+ return has_value();
+ }
+ constexpr bool operator!() const noexcept
+ {
+ return !operator bool();
+ }
+
+ SCN_CONSTEXPR14 success_type& value() noexcept
+ {
+ return *m_s;
+ }
+ constexpr const success_type& value() const noexcept
+ {
+ return *m_s;
+ }
+
+ SCN_CONSTEXPR14 error_type& error() noexcept
+ {
+ return m_e;
+ }
+ constexpr error_type error() const noexcept
+ {
+ return m_e;
+ }
+
+ private:
+ success_storage m_s{};
+ error_type m_e{error_type::success_tag()};
+ };
+
+ template <typename T,
+ typename U = typename std::remove_cv<
+ typename std::remove_reference<T>::type>::type>
+ expected<U> make_expected(T&& val)
+ {
+ return expected<U>(std::forward<T>(val));
+ }
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/util/math.h b/src/third-party/scnlib/include/scn/util/math.h
new file mode 100644
index 0000000..4cca941
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/math.h
@@ -0,0 +1,121 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_MATH_H
+#define SCN_UTIL_MATH_H
+
+#include "../detail/fwd.h"
+
+#include <cmath>
+#include <limits>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename Integral>
+ SCN_CONSTEXPR14 int _max_digits(int base) noexcept
+ {
+ using lim = std::numeric_limits<Integral>;
+
+ char base8_digits[8] = {3, 5, 0, 11, 0, 0, 0, 21};
+
+ if (base == 10) {
+ return lim::digits10;
+ }
+ if (base == 8) {
+ return static_cast<int>(base8_digits[sizeof(Integral) - 1]);
+ }
+ if (base == lim::radix) {
+ return lim::digits;
+ }
+
+ auto i = lim::max();
+
+ Integral digits = 0;
+ while (i) {
+ i /= static_cast<Integral>(base);
+ digits++;
+ }
+ return static_cast<int>(digits);
+ }
+
+ /**
+ * Returns the maximum number of digits that an integer in base `base`
+ * can have, including the sign.
+ *
+ * If `base == 0`, uses `2` (longest), and adds 2 to the result, to
+ * accommodate for a base prefix (e.g. `0x`)
+ */
+ template <typename Integral>
+ SCN_CONSTEXPR14 int max_digits(int base) noexcept
+ {
+ auto b = base == 0 ? 2 : base;
+ auto d = _max_digits<Integral>(b) +
+ (std::is_signed<Integral>::value ? 1 : 0);
+ if (base == 0) {
+ return d + 2; // accommodate for 0x/0o
+ }
+ return d;
+ }
+
+ /**
+ * Implementation of `std::div`, which is constexpr pre-C++23
+ */
+ template <typename T>
+ constexpr std::pair<T, T> div(T l, T r) noexcept
+ {
+ return {l / r, l % r};
+ }
+
+ template <typename T>
+ struct zero_value;
+ template <>
+ struct zero_value<float> {
+ static constexpr float value = 0.0f;
+ };
+ template <>
+ struct zero_value<double> {
+ static constexpr double value = 0.0;
+ };
+ template <>
+ struct zero_value<long double> {
+ static constexpr long double value = 0.0l;
+ };
+
+ /**
+ * Returns `true` if `ch` is a digit for an integer in base `base`.
+ */
+ template <typename CharT>
+ bool is_base_digit(CharT ch, int base)
+ {
+ if (base <= 10) {
+ return ch >= static_cast<CharT>('0') &&
+ ch <= static_cast<CharT>('0') + base - 1;
+ }
+ return is_base_digit(ch, 10) ||
+ (ch >= static_cast<CharT>('a') &&
+ ch <= static_cast<CharT>('a') + base - 1) ||
+ (ch >= static_cast<CharT>('A') &&
+ ch <= static_cast<CharT>('A') + base - 1);
+ }
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/util/memory.h b/src/third-party/scnlib/include/scn/util/memory.h
new file mode 100644
index 0000000..9dfb970
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/memory.h
@@ -0,0 +1,404 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_MEMORY_H
+#define SCN_UTIL_MEMORY_H
+
+#include "meta.h"
+
+#include <cstring>
+#include <new>
+
+SCN_GCC_PUSH
+SCN_GCC_IGNORE("-Wnoexcept")
+#include <iterator>
+SCN_GCC_POP
+
+#if SCN_MSVC && SCN_HAS_STRING_VIEW
+#include <string_view>
+#endif
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename T>
+ struct pointer_traits;
+
+ template <typename T>
+ struct pointer_traits<T*> {
+ using pointer = T*;
+ using element_type = T;
+ using difference_type = std::ptrdiff_t;
+
+ template <typename U>
+ using rebind = U*;
+
+ template <typename U = T,
+ typename std::enable_if<!std::is_void<U>::value>::type* =
+ nullptr>
+ static constexpr pointer pointer_to(U& r) noexcept
+ {
+ return &r;
+ }
+ };
+
+ template <typename T>
+ constexpr T* to_address_impl(T* p, priority_tag<2>) noexcept
+ {
+ return p;
+ }
+ template <typename Ptr>
+ SCN_CONSTEXPR14 auto to_address_impl(const Ptr& p,
+ priority_tag<1>) noexcept
+ -> decltype(::scn::detail::pointer_traits<Ptr>::to_address(p))
+ {
+ return ::scn::detail::pointer_traits<Ptr>::to_address(p);
+ }
+ template <typename Ptr>
+ constexpr auto to_address_impl(const Ptr& p, priority_tag<0>) noexcept
+ -> decltype(::scn::detail::to_address_impl(p.operator->(),
+ priority_tag<2>{}))
+ {
+ return ::scn::detail::to_address_impl(p.operator->(),
+ priority_tag<2>{});
+ }
+
+ template <typename Ptr>
+ constexpr auto to_address(Ptr&& p) noexcept
+ -> decltype(::scn::detail::to_address_impl(SCN_FWD(p),
+ priority_tag<2>{}))
+ {
+ return ::scn::detail::to_address_impl(SCN_FWD(p),
+ priority_tag<2>{});
+ }
+
+#if SCN_WINDOWS
+ template <typename I, typename B, typename E>
+ SCN_CONSTEXPR14 auto to_address_safe(I&& p, B begin, E end) noexcept
+ -> decltype(to_address(SCN_FWD(p)))
+ {
+ if (p >= begin && p < end) {
+ return to_address(SCN_FWD(p));
+ }
+ if (begin == end) {
+ return to_address(SCN_FWD(p));
+ }
+ if (p == end) {
+ return to_address(SCN_FWD(p) - 1) + 1;
+ }
+ SCN_ENSURE(false);
+ SCN_UNREACHABLE;
+ }
+#else
+ template <typename I, typename B, typename E>
+ SCN_CONSTEXPR14 auto to_address_safe(I&& p, B, E) noexcept
+ -> decltype(to_address(SCN_FWD(p)))
+ {
+ return to_address(SCN_FWD(p));
+ }
+#endif
+
+ // Workaround for MSVC _String_view_iterator
+#if SCN_MSVC && SCN_HAS_STRING_VIEW
+ template <typename Traits>
+ struct pointer_traits<std::_String_view_iterator<Traits>> {
+ using iterator = std::_String_view_iterator<Traits>;
+ using pointer = typename iterator::pointer;
+ using element_type = typename iterator::value_type;
+ using difference_type = typename iterator::difference_type;
+
+ static constexpr pointer to_address(const iterator& it) noexcept
+ {
+ // operator-> of _String_view_iterator
+ // is checked for past-the-end dereference,
+ // even though operator-> isn't dereferencing anything :)))
+ return it._Unwrapped();
+ }
+ };
+#endif
+
+ template <typename T>
+ constexpr T* launder(T* p) noexcept
+ {
+#if SCN_HAS_LAUNDER
+ return std::launder(p);
+#else
+ return p;
+#endif
+ }
+
+ template <typename ForwardIt, typename T>
+ void uninitialized_fill(ForwardIt first,
+ ForwardIt last,
+ const T& value,
+ std::true_type) noexcept
+ {
+ using value_type =
+ typename std::iterator_traits<ForwardIt>::value_type;
+ const auto dist = static_cast<size_t>(std::distance(first, last)) *
+ sizeof(value_type);
+ std::memset(&*first, static_cast<unsigned char>(value), dist);
+ }
+ template <typename ForwardIt, typename T>
+ void uninitialized_fill(ForwardIt first,
+ ForwardIt last,
+ const T& value,
+ std::false_type) noexcept
+ {
+ using value_type =
+ typename std::iterator_traits<ForwardIt>::value_type;
+ ForwardIt current = first;
+ for (; current != last; ++current) {
+ ::new (static_cast<void*>(std::addressof(*current)))
+ value_type(value);
+ }
+ }
+ template <typename ForwardIt, typename T>
+ void uninitialized_fill(ForwardIt first,
+ ForwardIt last,
+ const T& value) noexcept
+ {
+ constexpr bool B = std::is_trivially_copyable<T>::value &&
+ std::is_pointer<ForwardIt>::value &&
+ sizeof(T) == 1;
+ return uninitialized_fill(first, last, value,
+ std::integral_constant<bool, B>{});
+ }
+
+ template <typename ForwardIt>
+ void uninitialized_fill_default_construct(ForwardIt first,
+ ForwardIt last) noexcept
+ {
+ using value_type =
+ typename std::iterator_traits<ForwardIt>::value_type;
+ ForwardIt current = first;
+ for (; current != last; ++current) {
+ ::new (static_cast<void*>(std::addressof(*current))) value_type;
+ }
+ }
+ template <typename ForwardIt>
+ void uninitialized_fill_value_init(ForwardIt first,
+ ForwardIt last) noexcept
+ {
+ using value_type =
+ typename std::iterator_traits<ForwardIt>::value_type;
+ ForwardIt current = first;
+ for (; current != last; ++current) {
+ ::new (static_cast<void*>(std::addressof(*current)))
+ value_type();
+ }
+ }
+
+ template <typename InputIt,
+ typename ForwardIt,
+ typename std::enable_if<
+ !std::is_trivially_copyable<typename std::iterator_traits<
+ ForwardIt>::value_type>::value>::type* = nullptr>
+ ForwardIt uninitialized_copy(InputIt first,
+ InputIt last,
+ ForwardIt d_first) noexcept
+ {
+ using value_type =
+ typename std::iterator_traits<ForwardIt>::value_type;
+ ForwardIt current = d_first;
+ for (; first != last; ++first, (void)++current) {
+ ::new (static_cast<void*>(std::addressof(*current)))
+ value_type(*first);
+ }
+ return current;
+ }
+ template <typename InputIt,
+ typename ForwardIt,
+ typename std::enable_if<
+ std::is_trivially_copyable<typename std::iterator_traits<
+ ForwardIt>::value_type>::value>::type* = nullptr>
+ ForwardIt uninitialized_copy(InputIt first,
+ InputIt last,
+ ForwardIt d_first) noexcept
+ {
+ using value_type =
+ typename std::iterator_traits<ForwardIt>::value_type;
+ using pointer = typename std::iterator_traits<ForwardIt>::pointer;
+ auto ptr =
+ std::memcpy(std::addressof(*d_first), std::addressof(*first),
+ static_cast<size_t>(std::distance(first, last)) *
+ sizeof(value_type));
+ return ForwardIt{static_cast<pointer>(ptr)};
+ }
+
+ template <typename InputIt,
+ typename ForwardIt,
+ typename std::enable_if<
+ !std::is_trivially_copyable<typename std::iterator_traits<
+ ForwardIt>::value_type>::value>::type* = nullptr>
+ ForwardIt uninitialized_move(InputIt first,
+ InputIt last,
+ ForwardIt d_first) noexcept
+ {
+ using value_type =
+ typename std::iterator_traits<ForwardIt>::value_type;
+ ForwardIt current = d_first;
+ for (; first != last; ++first, (void)++current) {
+ ::new (static_cast<void*>(std::addressof(*current)))
+ value_type(std::move(*first));
+ }
+ return current;
+ }
+ template <typename InputIt,
+ typename ForwardIt,
+ typename std::enable_if<
+ std::is_trivially_copyable<typename std::iterator_traits<
+ ForwardIt>::value_type>::value>::type* = nullptr>
+ ForwardIt uninitialized_move(InputIt first,
+ InputIt last,
+ ForwardIt d_first) noexcept
+ {
+ using value_type =
+ typename std::iterator_traits<ForwardIt>::value_type;
+ using pointer = typename std::iterator_traits<ForwardIt>::pointer;
+ auto ptr =
+ std::memcpy(std::addressof(*d_first), std::addressof(*first),
+ static_cast<size_t>(std::distance(first, last)) *
+ sizeof(value_type));
+ return ForwardIt(static_cast<pointer>(ptr));
+ }
+
+ template <typename T>
+ class SCN_TRIVIAL_ABI erased_storage {
+ public:
+ using value_type = T;
+ using pointer = T*;
+ using storage_type = unsigned char[sizeof(T)];
+
+ constexpr erased_storage() noexcept = default;
+
+ erased_storage(T val) noexcept(
+ std::is_nothrow_move_constructible<T>::value)
+ : m_ptr(::new (static_cast<void*>(&m_data)) T(SCN_MOVE(val)))
+ {
+ }
+
+ erased_storage(const erased_storage& other)
+ : m_ptr(other ? ::new (static_cast<void*>(&m_data))
+ T(other.get())
+ : nullptr)
+ {
+ }
+ erased_storage& operator=(const erased_storage& other)
+ {
+ _destruct();
+ if (other) {
+ m_ptr = ::new (static_cast<void*>(&m_data)) T(other.get());
+ }
+ return *this;
+ }
+
+ erased_storage(erased_storage&& other) noexcept
+ : m_ptr(other ? ::new (static_cast<void*>(&m_data))
+ T(SCN_MOVE(other.get()))
+ : nullptr)
+ {
+ other.m_ptr = nullptr;
+ }
+ erased_storage& operator=(erased_storage&& other) noexcept
+ {
+ _destruct();
+ if (other) {
+ m_ptr = ::new (static_cast<void*>(&m_data))
+ T(SCN_MOVE(other.get()));
+ other.m_ptr = nullptr;
+ }
+ return *this;
+ }
+
+ ~erased_storage() noexcept
+ {
+ _destruct();
+ }
+
+ SCN_NODISCARD constexpr bool has_value() const noexcept
+ {
+ return m_ptr != nullptr;
+ }
+ constexpr explicit operator bool() const noexcept
+ {
+ return has_value();
+ }
+
+ SCN_CONSTEXPR14 T& get() noexcept
+ {
+ SCN_EXPECT(has_value());
+ return _get();
+ }
+ SCN_CONSTEXPR14 const T& get() const noexcept
+ {
+ SCN_EXPECT(has_value());
+ return _get();
+ }
+
+ SCN_CONSTEXPR14 T& operator*() noexcept
+ {
+ SCN_EXPECT(has_value());
+ return _get();
+ }
+ SCN_CONSTEXPR14 const T& operator*() const noexcept
+ {
+ SCN_EXPECT(has_value());
+ return _get();
+ }
+
+ SCN_CONSTEXPR14 T* operator->() noexcept
+ {
+ return m_ptr;
+ }
+ SCN_CONSTEXPR14 const T* operator->() const noexcept
+ {
+ return m_ptr;
+ }
+
+ private:
+ void _destruct()
+ {
+ if (m_ptr) {
+ _get().~T();
+ }
+ m_ptr = nullptr;
+ }
+ static pointer _toptr(storage_type& data)
+ {
+ return ::scn::detail::launder(
+ reinterpret_cast<T*>(reinterpret_cast<void*>(data.data())));
+ }
+ SCN_CONSTEXPR14 T& _get() noexcept
+ {
+ return *m_ptr;
+ }
+ SCN_CONSTEXPR14 const T& _get() const noexcept
+ {
+ return *m_ptr;
+ }
+
+ alignas(T) storage_type m_data{};
+ pointer m_ptr{nullptr};
+ };
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/util/meta.h b/src/third-party/scnlib/include/scn/util/meta.h
new file mode 100644
index 0000000..83b738c
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/meta.h
@@ -0,0 +1,77 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_META_H
+#define SCN_UTIL_META_H
+
+#include "../detail/fwd.h"
+
+#include <type_traits>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename... Ts>
+ struct make_void {
+ using type = void;
+ };
+ template <typename... Ts>
+ using void_t = typename make_void<Ts...>::type;
+
+ template <typename... T>
+ void valid_expr(T&&...);
+
+ template <typename T>
+ struct remove_cvref {
+ using type = typename std::remove_cv<
+ typename std::remove_reference<T>::type>::type;
+ };
+ template <typename T>
+ using remove_cvref_t = typename remove_cvref<T>::type;
+
+ // Stolen from range-v3
+ template <typename T>
+ struct static_const {
+ static constexpr T value{};
+ };
+ template <typename T>
+ constexpr T static_const<T>::value;
+
+ template <std::size_t I>
+ struct priority_tag : priority_tag<I - 1> {
+ };
+ template <>
+ struct priority_tag<0> {
+ };
+
+ struct dummy_type {
+ };
+
+ template <typename T>
+ struct dependent_false : std::false_type {
+ };
+
+ template <typename T>
+ using integer_type_for_char = typename std::
+ conditional<std::is_signed<T>::value, int, unsigned>::type;
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/util/optional.h b/src/third-party/scnlib/include/scn/util/optional.h
new file mode 100644
index 0000000..9c0c808
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/optional.h
@@ -0,0 +1,105 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_OPTIONAL_H
+#define SCN_UTIL_OPTIONAL_H
+
+#include "memory.h"
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ struct nullopt_t {
+ };
+ namespace {
+ static constexpr auto& nullopt = detail::static_const<nullopt_t>::value;
+ }
+
+ /**
+ * A very lackluster optional implementation.
+ * Useful when scanning non-default-constructible types, especially with
+ * <tuple_return.h>:
+ *
+ * \code{.cpp}
+ * // implement scn::scanner for optional<mytype>
+ * optional<mytype> val;
+ * scn::scan(source, "{}", val);
+ *
+ * // with tuple_return:
+ * auto [result, val] = scn::scan_tuple<optional<mytype>>(source, "{}");
+ * \endcode
+ */
+ template <typename T>
+ class optional {
+ public:
+ using value_type = T;
+ using storage_type = detail::erased_storage<T>;
+
+ optional() = default;
+ optional(nullopt_t) : m_storage{} {}
+
+ optional(value_type val) : m_storage(SCN_MOVE(val)) {}
+ optional& operator=(value_type val)
+ {
+ m_storage = storage_type(SCN_MOVE(val));
+ return *this;
+ }
+
+ SCN_NODISCARD constexpr bool has_value() const noexcept
+ {
+ return m_storage.operator bool();
+ }
+ constexpr explicit operator bool() const noexcept
+ {
+ return has_value();
+ }
+
+ SCN_CONSTEXPR14 T& get() noexcept
+ {
+ return m_storage.get();
+ }
+ SCN_CONSTEXPR14 const T& get() const noexcept
+ {
+ return m_storage.get();
+ }
+
+ SCN_CONSTEXPR14 T& operator*() noexcept
+ {
+ return get();
+ }
+ SCN_CONSTEXPR14 const T& operator*() const noexcept
+ {
+ return get();
+ }
+
+ SCN_CONSTEXPR14 T* operator->() noexcept
+ {
+ return m_storage.operator->();
+ }
+ SCN_CONSTEXPR14 const T* operator->() const noexcept
+ {
+ return m_storage.operator->();
+ }
+
+ private:
+ storage_type m_storage;
+ };
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/include/scn/util/small_vector.h b/src/third-party/scnlib/include/scn/util/small_vector.h
new file mode 100644
index 0000000..93a5514
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/small_vector.h
@@ -0,0 +1,788 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_SMALL_VECTOR_H
+#define SCN_UTIL_SMALL_VECTOR_H
+
+#include "math.h"
+#include "memory.h"
+
+#include <cstdint>
+#include <cstring>
+
+SCN_GCC_PUSH
+SCN_GCC_IGNORE("-Wnoexcept")
+#include <iterator>
+SCN_GCC_POP
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename Iter>
+ std::reverse_iterator<Iter> make_reverse_iterator(Iter i)
+ {
+ return std::reverse_iterator<Iter>(i);
+ }
+
+ class small_vector_base {
+ static SCN_CONSTEXPR14 uint64_t _next_pow2_64(uint64_t x) noexcept
+ {
+ --x;
+ x |= (x >> 1);
+ x |= (x >> 2);
+ x |= (x >> 4);
+ x |= (x >> 8);
+ x |= (x >> 16);
+ x |= (x >> 32);
+ return x + 1;
+ }
+ static SCN_CONSTEXPR14 uint32_t _next_pow2_32(uint32_t x) noexcept
+ {
+ --x;
+ x |= (x >> 1);
+ x |= (x >> 2);
+ x |= (x >> 4);
+ x |= (x >> 8);
+ x |= (x >> 16);
+ return x + 1;
+ }
+
+ protected:
+ size_t next_pow2(size_t x)
+ {
+ SCN_MSVC_PUSH
+ SCN_MSVC_IGNORE(4127) // conditional expression is constant
+ if (sizeof(size_t) == sizeof(uint64_t)) {
+ return static_cast<size_t>(
+ _next_pow2_64(static_cast<uint64_t>(x)));
+ }
+ SCN_MSVC_POP
+ return static_cast<size_t>(
+ _next_pow2_32(static_cast<uint32_t>(x)));
+ }
+ };
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wpadded")
+
+ template <typename T, size_t N>
+ struct basic_stack_storage {
+ alignas(T) unsigned char data[N * sizeof(T)];
+
+ T* reinterpret_data()
+ {
+ return ::scn::detail::launder(reinterpret_unconstructed_data());
+ }
+ const T* reinterpret_data() const
+ {
+ return ::scn::detail::launder(reinterpret_unconstructed_data());
+ }
+
+ SCN_NODISCARD T* reinterpret_unconstructed_data()
+ {
+ return static_cast<T*>(static_cast<void*>(data));
+ }
+ SCN_NODISCARD const T* reinterpret_unconstructed_data() const
+ {
+ return static_cast<const T*>(static_cast<const void*>(data));
+ }
+
+ SCN_NODISCARD SCN_CONSTEXPR14 unsigned char*
+ get_unconstructed_data()
+ {
+ return data;
+ }
+ SCN_NODISCARD constexpr const unsigned char*
+ get_unconstructed_data() const
+ {
+ return data;
+ }
+ };
+
+ // -Wpadded
+ SCN_CLANG_POP
+
+ template <typename T>
+ constexpr T constexpr_max(T val)
+ {
+ return val;
+ }
+ template <typename T, typename... Ts>
+ constexpr T constexpr_max(T val, Ts... a)
+ {
+ return val > constexpr_max(a...) ? val : constexpr_max(a...);
+ }
+
+ template <typename T>
+ struct alignas(constexpr_max(alignof(T),
+ alignof(T*))) basic_stack_storage<T, 0> {
+ T* reinterpret_data()
+ {
+ return nullptr;
+ }
+ const T* reinterpret_data() const
+ {
+ return nullptr;
+ }
+
+ T* reinterpret_unconstructed_data()
+ {
+ return nullptr;
+ }
+ const T* reinterpret_unconstructed_data() const
+ {
+ return nullptr;
+ }
+
+ unsigned char* get_unconstructed_data()
+ {
+ return nullptr;
+ }
+ const unsigned char* get_unconstructed_data() const
+ {
+ return nullptr;
+ }
+ };
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wpadded")
+
+ /**
+ * A contiguous container, that stores its values in the stack, if
+ * `size() <= StackN`
+ */
+ template <typename T, size_t StackN>
+ class small_vector : protected small_vector_base {
+ public:
+ using value_type = T;
+ using size_type = size_t;
+ using difference_type = std::ptrdiff_t;
+ using reference = T&;
+ using const_reference = const T&;
+ using pointer = T*;
+ using const_pointer = const T*;
+ using iterator = pointer;
+ using const_iterator = const_pointer;
+ using reverse_iterator = std::reverse_iterator<pointer>;
+ using const_reverse_iterator = std::reverse_iterator<const_pointer>;
+
+ struct stack_storage : basic_stack_storage<T, StackN> {
+ };
+ struct heap_storage {
+ size_type cap{0};
+ };
+
+ small_vector() noexcept
+ : m_ptr(_construct_stack_storage()
+ .reinterpret_unconstructed_data())
+ {
+ SCN_MSVC_PUSH
+ SCN_MSVC_IGNORE(4127) // conditional expression is constant
+
+ if (StackN == 0) {
+ _destruct_stack_storage();
+ _construct_heap_storage();
+ }
+
+ SCN_MSVC_POP
+
+ SCN_ENSURE(size() == 0);
+ }
+
+ explicit small_vector(size_type count, const T& value)
+ {
+ if (!can_be_small(count)) {
+ auto& heap = _construct_heap_storage();
+ auto cap = next_pow2(count);
+ auto storage_ptr = new unsigned char[count * sizeof(T)];
+ auto ptr =
+ static_cast<pointer>(static_cast<void*>(storage_ptr));
+ uninitialized_fill(ptr, ptr + count, value);
+
+ heap.cap = cap;
+ m_size = count;
+ m_ptr = ::scn::detail::launder(ptr);
+ }
+ else {
+ auto& stack = _construct_stack_storage();
+ uninitialized_fill(
+ stack.reinterpret_unconstructed_data(),
+ stack.reinterpret_unconstructed_data() + StackN, value);
+ m_size = count;
+ m_ptr = stack.reinterpret_data();
+ }
+
+ SCN_ENSURE(data());
+ SCN_ENSURE(size() == count);
+ SCN_ENSURE(capacity() >= size());
+ }
+
+ explicit small_vector(size_type count)
+ {
+ if (!can_be_small(count)) {
+ auto& heap = _construct_heap_storage();
+ auto cap = next_pow2(count);
+ auto storage_ptr = new unsigned char[count * sizeof(T)];
+ auto ptr =
+ static_cast<pointer>(static_cast<void*>(storage_ptr));
+ uninitialized_fill_value_init(ptr, ptr + count);
+ heap.cap = cap;
+ m_size = count;
+ m_ptr = ::scn::detail::launder(ptr);
+ }
+ else {
+ auto& stack = _construct_stack_storage();
+ uninitialized_fill_value_init(
+ stack.reinterpret_unconstructed_data(),
+ stack.reinterpret_unconstructed_data() + count);
+ m_size = count;
+ m_ptr = stack.reinterpret_data();
+ }
+
+ SCN_ENSURE(data());
+ SCN_ENSURE(size() == count);
+ SCN_ENSURE(capacity() >= size());
+ }
+
+ small_vector(const small_vector& other)
+ {
+ if (other.empty()) {
+ auto& stack = _construct_stack_storage();
+ m_ptr = stack.reinterpret_unconstructed_data();
+ return;
+ }
+
+ auto s = other.size();
+ if (!other.is_small()) {
+ auto& heap = _construct_heap_storage();
+ auto cap = other.capacity();
+ auto optr = other.data();
+
+ auto storage_ptr = new unsigned char[cap * sizeof(T)];
+ auto ptr =
+ static_cast<pointer>(static_cast<void*>(storage_ptr));
+ uninitialized_copy(optr, optr + s, ptr);
+
+ m_ptr = ::scn::detail::launder(ptr);
+ m_size = s;
+ heap.cap = cap;
+ }
+ else {
+ auto& stack = _construct_stack_storage();
+ auto optr = other.data();
+ uninitialized_copy(optr, optr + s,
+ stack.reinterpret_unconstructed_data());
+ m_size = s;
+ m_ptr = stack.reinterpret_data();
+ }
+
+ SCN_ENSURE(data());
+ SCN_ENSURE(other.data());
+ SCN_ENSURE(other.size() == size());
+ SCN_ENSURE(other.capacity() == capacity());
+ }
+ small_vector(small_vector&& other) noexcept
+ {
+ if (other.empty()) {
+ auto& stack = _construct_stack_storage();
+ m_ptr = stack.reinterpret_unconstructed_data();
+ return;
+ }
+
+ auto s = other.size();
+ if (!other.is_small()) {
+ auto& heap = _construct_heap_storage();
+ m_ptr = other.data();
+
+ m_size = s;
+ heap.cap = other.capacity();
+ }
+ else {
+ auto& stack = _construct_stack_storage();
+ auto optr = other.data();
+ uninitialized_move(optr, optr + s,
+ stack.reinterpret_unconstructed_data());
+
+ m_size = s;
+ other._destruct_elements();
+ }
+ other.m_ptr = nullptr;
+
+ SCN_ENSURE(data());
+ }
+
+ small_vector& operator=(const small_vector& other)
+ {
+ _destruct_elements();
+
+ if (other.empty()) {
+ return *this;
+ }
+
+ SCN_ASSERT(size() == 0, "");
+
+ // this other
+ // s s false || true
+ // s h false || false second
+ // h s true || true
+ // h h true || false
+ if (!is_small() || other.is_small()) {
+ uninitialized_copy(other.data(),
+ other.data() + other.size(), data());
+ m_ptr = ::scn::detail::launder(data());
+ m_size = other.size();
+ if (!other.is_small()) {
+ _get_heap().cap = other.capacity();
+ }
+ }
+ else {
+ _destruct_stack_storage();
+ auto& heap = _construct_heap_storage();
+
+ auto cap = next_pow2(other.size());
+ auto storage_ptr = new unsigned char[cap * sizeof(T)];
+ auto ptr =
+ static_cast<pointer>(static_cast<void*>(storage_ptr));
+ uninitialized_copy(other.data(),
+ other.data() + other.size(), ptr);
+ m_ptr = ::scn::detail::launder(ptr);
+ m_size = other.size();
+ heap.cap = cap;
+ }
+ return *this;
+ }
+
+ small_vector& operator=(small_vector&& other) noexcept
+ {
+ _destruct_elements();
+
+ if (other.empty()) {
+ return *this;
+ }
+
+ SCN_ASSERT(size() == 0, "");
+
+ if (!is_small() && !other.is_small()) {
+ if (!is_small()) {
+ if (capacity() != 0) {
+ delete[] ::scn::detail::launder(
+ static_cast<unsigned char*>(
+ static_cast<void*>(m_ptr)));
+ }
+ }
+
+ m_ptr = other.data();
+ m_size = other.size();
+ _get_heap().cap = other.capacity();
+ }
+ else if (!is_small() || other.is_small()) {
+ uninitialized_move(other.data(),
+ other.data() + other.size(), data());
+ m_size = other.size();
+ other._destruct_elements();
+ }
+ else {
+ _destruct_stack_storage();
+ auto& heap = _construct_heap_storage();
+
+ m_ptr = other.data();
+ m_size = other.size();
+ heap.cap = other.capacity();
+ }
+
+ other.m_ptr = nullptr;
+
+ return *this;
+ }
+
+ ~small_vector()
+ {
+ _destruct();
+ }
+
+ SCN_NODISCARD SCN_CONSTEXPR14 pointer data() noexcept
+ {
+ return m_ptr;
+ }
+ SCN_NODISCARD constexpr const_pointer data() const noexcept
+ {
+ return m_ptr;
+ }
+ SCN_NODISCARD constexpr size_type size() const noexcept
+ {
+ return m_size;
+ }
+ SCN_NODISCARD size_type capacity() const noexcept
+ {
+ if (SCN_LIKELY(is_small())) {
+ return StackN;
+ }
+ return _get_heap().cap;
+ }
+
+ SCN_NODISCARD constexpr bool empty() const noexcept
+ {
+ return size() == 0;
+ }
+
+ SCN_NODISCARD bool is_small() const noexcept
+ {
+ // oh so very ub
+ return m_ptr == reinterpret_cast<const_pointer>(
+ std::addressof(m_stack_storage));
+ }
+ constexpr static bool can_be_small(size_type n) noexcept
+ {
+ return n <= StackN;
+ }
+
+ SCN_CONSTEXPR14 reference operator[](size_type pos)
+ {
+ SCN_EXPECT(pos < size());
+ return *(begin() + pos);
+ }
+ SCN_CONSTEXPR14 const_reference operator[](size_type pos) const
+ {
+ SCN_EXPECT(pos < size());
+ return *(begin() + pos);
+ }
+
+ SCN_CONSTEXPR14 reference front()
+ {
+ SCN_EXPECT(!empty());
+ return *begin();
+ }
+ SCN_CONSTEXPR14 const_reference front() const
+ {
+ SCN_EXPECT(!empty());
+ return *begin();
+ }
+
+ SCN_CONSTEXPR14 reference back()
+ {
+ SCN_EXPECT(!empty());
+ return *(end() - 1);
+ }
+ SCN_CONSTEXPR14 const_reference back() const
+ {
+ SCN_EXPECT(!empty());
+ return *(end() - 1);
+ }
+
+ SCN_CONSTEXPR14 iterator begin() noexcept
+ {
+ return data();
+ }
+ constexpr const_iterator begin() const noexcept
+ {
+ return data();
+ }
+ constexpr const_iterator cbegin() const noexcept
+ {
+ return begin();
+ }
+
+ SCN_CONSTEXPR14 iterator end() noexcept
+ {
+ return begin() + size();
+ }
+ constexpr const_iterator end() const noexcept
+ {
+ return begin() + size();
+ }
+ constexpr const_iterator cend() const noexcept
+ {
+ return end();
+ }
+
+ SCN_CONSTEXPR14 reverse_iterator rbegin() noexcept
+ {
+ return make_reverse_iterator(end());
+ }
+ constexpr const_reverse_iterator rbegin() const noexcept
+ {
+ return make_reverse_iterator(end());
+ }
+ constexpr const_reverse_iterator crbegin() const noexcept
+ {
+ return rbegin();
+ }
+
+ SCN_CONSTEXPR14 reverse_iterator rend() noexcept
+ {
+ return make_reverse_iterator(begin());
+ }
+ constexpr const_reverse_iterator rend() const noexcept
+ {
+ return make_reverse_iterator(begin());
+ }
+ constexpr const_reverse_iterator crend() const noexcept
+ {
+ return rend();
+ }
+
+ SCN_NODISCARD
+ constexpr size_type max_size() const noexcept
+ {
+ return std::numeric_limits<size_type>::max();
+ }
+
+ void make_small() noexcept
+ {
+ if (is_small() || !can_be_small(size())) {
+ return;
+ }
+
+ stack_storage s;
+ uninitialized_move(begin(), end(),
+ s.reinterpret_unconstructed_data());
+ auto tmp_size = size();
+
+ _destruct();
+ auto& stack = _construct_stack_storage();
+ uninitialized_move(s.reinterpret_data(),
+ s.reinterpret_data() + tmp_size,
+ stack.reinterpret_unconstructed_data());
+ m_size = tmp_size;
+ }
+
+ void reserve(size_type new_cap)
+ {
+ if (new_cap <= capacity()) {
+ return;
+ }
+ _realloc(next_pow2(new_cap));
+ }
+
+ void shrink_to_fit()
+ {
+ if (is_small()) {
+ return;
+ }
+ if (!can_be_small(size())) {
+ _realloc(size());
+ }
+ else {
+ make_small();
+ }
+ }
+
+ void clear() noexcept
+ {
+ _destruct_elements();
+ }
+
+ iterator erase(iterator pos)
+ {
+ if (pos == end()) {
+ pos->~T();
+ m_size = size() - 1;
+ return end();
+ }
+ else {
+ for (auto it = pos; it != end(); ++it) {
+ it->~T();
+ ::new (static_cast<void*>(it)) T(std::move(*(it + 1)));
+ }
+ (end() - 1)->~T();
+ m_size = size() - 1;
+ return pos;
+ }
+ }
+
+ iterator erase(iterator b, iterator e)
+ {
+ if (begin() == end()) {
+ return b;
+ }
+ if (e == end()) {
+ auto n = static_cast<size_t>(std::distance(b, e));
+ for (auto it = b; it != e; ++it) {
+ it->~T();
+ }
+ m_size = size() - n;
+ return end();
+ }
+ SCN_ENSURE(false);
+ SCN_UNREACHABLE;
+ }
+
+ void push_back(const T& value)
+ {
+ ::new (_prepare_push_back()) T(value);
+ m_size = size() + 1;
+ }
+ void push_back(T&& value)
+ {
+ ::new (_prepare_push_back()) T(std::move(value));
+ m_size = size() + 1;
+ }
+
+ template <typename... Args>
+ reference emplace_back(Args&&... args)
+ {
+ ::new (_prepare_push_back()) T(SCN_FWD(args)...);
+ m_size = size() + 1;
+ return back();
+ }
+
+ void pop_back()
+ {
+ back().~T();
+ m_size = size() - 1;
+ }
+
+ void resize(size_type count)
+ {
+ if (count > size()) {
+ if (count > capacity()) {
+ _realloc(next_pow2(capacity()));
+ }
+ uninitialized_fill_value_init(begin() + size(),
+ begin() + count);
+ }
+ else {
+ for (auto it = begin() + count; it != end(); ++it) {
+ it->~T();
+ }
+ }
+ m_size = count;
+ }
+
+ SCN_CONSTEXPR14 void swap(small_vector& other) noexcept
+ {
+ small_vector tmp{SCN_MOVE(other)};
+ other = std::move(*this);
+ *this = std::move(tmp);
+ }
+
+ private:
+ stack_storage& _construct_stack_storage() noexcept
+ {
+ ::new (std::addressof(m_stack_storage)) stack_storage;
+ m_ptr = m_stack_storage.reinterpret_unconstructed_data();
+ return m_stack_storage;
+ }
+ heap_storage& _construct_heap_storage() noexcept
+ {
+ ::new (std::addressof(m_heap_storage)) heap_storage;
+ m_ptr = nullptr;
+ return m_heap_storage;
+ }
+
+ void _destruct_stack_storage() noexcept
+ {
+ _get_stack().~stack_storage();
+ }
+ void _destruct_heap_storage() noexcept
+ {
+ if (capacity() != 0) {
+ delete[] static_cast<unsigned char*>(
+ static_cast<void*>(m_ptr));
+ }
+ _get_heap().~heap_storage();
+ }
+
+ void _destruct_elements() noexcept
+ {
+ const auto s = size();
+ for (size_type i = 0; i != s; ++i) {
+ m_ptr[i].~T();
+ }
+ m_size = 0;
+ }
+
+ void _destruct() noexcept
+ {
+ _destruct_elements();
+ if (SCN_UNLIKELY(!is_small())) {
+ _destruct_heap_storage();
+ }
+ else {
+ _destruct_stack_storage();
+ }
+ }
+
+ void _realloc(size_type new_cap)
+ {
+ auto storage_ptr = new unsigned char[new_cap * sizeof(T)];
+ auto ptr =
+ static_cast<pointer>(static_cast<void*>(storage_ptr));
+ auto n = size();
+ uninitialized_move(begin(), end(), ptr);
+ _destruct();
+ auto& heap = [this]() -> heap_storage& {
+ if (is_small()) {
+ return _construct_heap_storage();
+ }
+ return _get_heap();
+ }();
+ m_ptr = ptr;
+ m_size = n;
+ heap.cap = new_cap;
+ }
+
+ void* _prepare_push_back()
+ {
+ if (SCN_UNLIKELY(size() == capacity())) {
+ _realloc(next_pow2(size() + 1));
+ }
+ return m_ptr + size();
+ }
+
+ stack_storage& _get_stack() noexcept
+ {
+ return m_stack_storage;
+ }
+ const stack_storage& _get_stack() const noexcept
+ {
+ return m_stack_storage;
+ }
+
+ heap_storage& _get_heap() noexcept
+ {
+ return m_heap_storage;
+ }
+ const heap_storage& _get_heap() const noexcept
+ {
+ return m_heap_storage;
+ }
+
+ pointer m_ptr{nullptr};
+ size_type m_size{0};
+ union {
+ stack_storage m_stack_storage;
+ heap_storage m_heap_storage;
+ };
+ };
+
+ template <typename T, size_t N>
+ SCN_CONSTEXPR14 void swap(
+ small_vector<T, N>& l,
+ small_vector<T, N>& r) noexcept(noexcept(l.swap(r)))
+ {
+ l.swap(r);
+ }
+
+ SCN_CLANG_POP // -Wpadded
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_SMALL_VECTOR_H
diff --git a/src/third-party/scnlib/include/scn/util/span.h b/src/third-party/scnlib/include/scn/util/span.h
new file mode 100644
index 0000000..1abdd6c
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/span.h
@@ -0,0 +1,240 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_SPAN_H
+#define SCN_UTIL_SPAN_H
+
+#include "memory.h"
+
+SCN_GCC_PUSH
+SCN_GCC_IGNORE("-Wnoexcept")
+#include <iterator>
+SCN_GCC_POP
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace custom_ranges {
+ // iterator_category
+ using std::bidirectional_iterator_tag;
+ using std::forward_iterator_tag;
+ using std::input_iterator_tag;
+ using std::output_iterator_tag;
+ using std::random_access_iterator_tag;
+ struct contiguous_iterator_tag : random_access_iterator_tag {
+ };
+ } // namespace custom_ranges
+
+ /**
+ * A view over a contiguous range.
+ * Stripped-down version of `std::span`.
+ */
+ template <typename T>
+ class span {
+ public:
+ using element_type = T;
+ using value_type = typename std::remove_cv<T>::type;
+ using index_type = std::size_t;
+ using ssize_type = std::ptrdiff_t;
+ using difference_type = std::ptrdiff_t;
+ using pointer = T*;
+ using const_pointer = const T*;
+ using reference = T&;
+ using const_reference = const T&;
+
+ using iterator = pointer;
+ using const_iterator = const_pointer;
+ using reverse_iterator = std::reverse_iterator<iterator>;
+ using const_reverse_iterator = std::reverse_iterator<const_iterator>;
+
+ constexpr span() noexcept = default;
+
+ template <typename I,
+ typename = decltype(detail::to_address(SCN_DECLVAL(I)))>
+ SCN_CONSTEXPR14 span(I begin, index_type count) noexcept
+ : m_ptr(detail::to_address(begin)),
+ m_end(detail::to_address(begin) + count)
+ {
+ }
+
+ template <typename I,
+ typename S,
+ typename = decltype(detail::to_address(SCN_DECLVAL(I)),
+ detail::to_address(SCN_DECLVAL(S)))>
+ SCN_CONSTEXPR14 span(I first, S last) noexcept
+ : m_ptr(detail::to_address(first)),
+ m_end(detail::to_address_safe(last, first, last))
+ {
+ }
+
+ template <typename U = typename std::add_const<T>::type,
+ typename E = element_type,
+ typename = typename std::enable_if<
+ std::is_same<E, value_type>::value>::type>
+ constexpr span(span<U> other) : m_ptr(other.m_ptr), m_end(other.m_end)
+ {
+ }
+
+ template <size_t N>
+ constexpr span(element_type (&arr)[N]) noexcept
+ : m_ptr(&arr), m_end(&arr + N)
+ {
+ }
+
+ SCN_CONSTEXPR14 iterator begin() noexcept
+ {
+ return m_ptr;
+ }
+ SCN_CONSTEXPR14 iterator end() noexcept
+ {
+ return m_end;
+ }
+ SCN_CONSTEXPR14 reverse_iterator rbegin() noexcept
+ {
+ return reverse_iterator{end()};
+ }
+ SCN_CONSTEXPR14 reverse_iterator rend() noexcept
+ {
+ return reverse_iterator{begin()};
+ }
+
+ constexpr const_iterator begin() const noexcept
+ {
+ return m_ptr;
+ }
+ constexpr const_iterator end() const noexcept
+ {
+ return m_end;
+ }
+ constexpr const_reverse_iterator rbegin() const noexcept
+ {
+ return reverse_iterator{end()};
+ }
+ constexpr const_reverse_iterator rend() const noexcept
+ {
+ return reverse_iterator{begin()};
+ }
+
+ constexpr const_iterator cbegin() const noexcept
+ {
+ return m_ptr;
+ }
+ constexpr const_iterator cend() const noexcept
+ {
+ return m_end;
+ }
+ constexpr const_reverse_iterator crbegin() const noexcept
+ {
+ return reverse_iterator{cend()};
+ }
+ constexpr const_reverse_iterator crend() const noexcept
+ {
+ return reverse_iterator{cbegin()};
+ }
+
+ SCN_CONSTEXPR14 reference operator[](index_type i) const noexcept
+ {
+ SCN_EXPECT(size() > i);
+ return *(m_ptr + i);
+ }
+
+ constexpr pointer data() const noexcept
+ {
+ return m_ptr;
+ }
+ SCN_NODISCARD constexpr index_type size() const noexcept
+ {
+ return static_cast<index_type>(m_end - m_ptr);
+ }
+ SCN_NODISCARD constexpr ssize_type ssize() const noexcept
+ {
+ return m_end - m_ptr;
+ }
+
+ SCN_CONSTEXPR14 span<T> first(index_type n) const
+ {
+ SCN_EXPECT(size() >= n);
+ return span<T>(data(), data() + n);
+ }
+ SCN_CONSTEXPR14 span<T> last(index_type n) const
+ {
+ SCN_EXPECT(size() >= n);
+ return span<T>(data() + size() - n, data() + size());
+ }
+ SCN_CONSTEXPR14 span<T> subspan(index_type off) const
+ {
+ SCN_EXPECT(size() >= off);
+ return span<T>(data() + off, size() - off);
+ }
+ SCN_CONSTEXPR14 span<T> subspan(index_type off,
+ difference_type count) const
+ {
+ SCN_EXPECT(size() > off + count);
+ SCN_EXPECT(count > 0);
+ return span<T>(data() + off, count);
+ }
+
+ constexpr operator span<typename std::add_const<T>::type>() const
+ {
+ return {m_ptr, m_end};
+ }
+ constexpr span<typename std::add_const<T>::type> as_const() const
+ {
+ return {m_ptr, m_end};
+ }
+
+ private:
+ pointer m_ptr{nullptr};
+ pointer m_end{nullptr};
+ };
+
+ template <typename I,
+ typename S,
+ typename Ptr = decltype(detail::to_address(SCN_DECLVAL(I))),
+ typename SPtr = decltype(detail::to_address(SCN_DECLVAL(S))),
+ typename ValueT = typename detail::remove_reference<
+ typename std::remove_pointer<Ptr>::type>::type>
+ SCN_CONSTEXPR14 auto make_span(I first, S last) noexcept -> span<ValueT>
+ {
+ return {first, last};
+ }
+ template <typename I,
+ typename Ptr = decltype(detail::to_address(SCN_DECLVAL(I))),
+ typename ValueT = typename detail::remove_reference<
+ typename std::remove_pointer<Ptr>::type>::type>
+ SCN_CONSTEXPR14 auto make_span(I first, std::size_t len) noexcept
+ -> span<ValueT>
+ {
+ return {first, len};
+ }
+
+ template <typename T>
+ SCN_CONSTEXPR14 span<typename T::value_type> make_span(
+ T& container) noexcept
+ {
+ using std::begin;
+ using std::end;
+ return span<typename T::value_type>(
+ detail::to_address(begin(container)),
+ detail::to_address_safe(end(container), begin(container),
+ end(container)));
+ }
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_SPAN_H
diff --git a/src/third-party/scnlib/include/scn/util/string_view.h b/src/third-party/scnlib/include/scn/util/string_view.h
new file mode 100644
index 0000000..576b93e
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/string_view.h
@@ -0,0 +1,270 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_STRING_VIEW_H
+#define SCN_UTIL_STRING_VIEW_H
+
+#include "algorithm.h"
+#include "span.h"
+
+#include <cstdint>
+#include <cstring>
+#include <cwchar>
+
+#if SCN_HAS_STRING_VIEW
+#include <string_view>
+#endif
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ inline size_t strlen(const char* s) noexcept
+ {
+ return ::std::strlen(s);
+ }
+ inline size_t strlen(const wchar_t* s) noexcept
+ {
+ return ::std::wcslen(s);
+ }
+ inline size_t strlen(const char16_t* s) noexcept
+ {
+ SCN_EXPECT(s);
+ auto end = s;
+ for (; *end != u'\0'; ++end)
+ ;
+ return static_cast<size_t>(end - s);
+ }
+ inline size_t strlen(const char32_t* s) noexcept
+ {
+ SCN_EXPECT(s);
+ auto end = s;
+ for (; *end != U'\0'; ++end)
+ ;
+ return static_cast<size_t>(end - s);
+ }
+#if SCN_HAS_CHAR8
+ inline size_t strlen(const char8_t* s) noexcept
+ {
+ return std::strlen(reinterpret_cast<const char*>(s));
+ }
+#endif
+ } // namespace detail
+
+ /**
+ * A view over a (sub)string.
+ * Used even when std::string_view is available to avoid compatibility
+ * issues.
+ */
+ template <typename CharT>
+ class basic_string_view {
+ public:
+ using value_type = CharT;
+ using span_type = span<const value_type>;
+
+ using pointer = value_type*;
+ using const_pointer = const value_type*;
+ using reference = value_type&;
+ using const_reference = const value_type&;
+ using iterator = typename span_type::const_iterator;
+ using const_iterator = iterator;
+ using reverse_iterator = std::reverse_iterator<iterator>;
+ using const_reverse_iterator = reverse_iterator;
+ using size_type = std::size_t;
+ using difference_type = std::ptrdiff_t;
+ using span_index_type = typename span_type::index_type;
+
+ static constexpr const size_type npos = size_type(-1);
+
+ constexpr basic_string_view() noexcept = default;
+ constexpr basic_string_view(const_pointer s, size_type c)
+ : m_data(s, static_cast<span_index_type>(c))
+ {
+ }
+ constexpr basic_string_view(const_pointer s)
+ : m_data(s, static_cast<span_index_type>(detail::strlen(s)))
+ {
+ }
+ template <size_t N>
+ constexpr basic_string_view(const CharT (&s)[N]) : m_data(s, N)
+ {
+ }
+ constexpr basic_string_view(const_pointer first, const_pointer last)
+ : m_data(first, last)
+ {
+ }
+#if SCN_HAS_STRING_VIEW
+ constexpr basic_string_view(std::basic_string_view<value_type> str)
+ : m_data(str.data(), str.size())
+ {
+ }
+#endif
+
+ template <typename T = char16_t,
+ typename std::enable_if<std::is_same<T, char16_t>::value &&
+ std::is_same<CharT, wchar_t>::value &&
+ sizeof(char16_t) ==
+ sizeof(wchar_t)>::type* = nullptr>
+ constexpr basic_string_view(const T* s)
+ : basic_string_view(reinterpret_cast<const wchar_t*>(s))
+ {
+ }
+ template <typename T = char32_t,
+ typename std::enable_if<std::is_same<T, char32_t>::value &&
+ std::is_same<CharT, wchar_t>::value &&
+ sizeof(char32_t) ==
+ sizeof(wchar_t)>::type* = nullptr>
+ constexpr basic_string_view(const T* s)
+ : basic_string_view(reinterpret_cast<const wchar_t*>(s))
+ {
+ }
+#if SCN_HAS_CHAR8
+ template <typename T = char8_t,
+ typename std::enable_if<
+ std::is_same<T, char8_t>::value &&
+ std::is_same<CharT, char>::value>::type* = nullptr>
+ constexpr basic_string_view(const T* s)
+ : basic_string_view(reinterpret_cast<const char*>(s))
+ {
+ }
+#endif
+
+ constexpr const_iterator begin() const noexcept
+ {
+ return cbegin();
+ }
+ constexpr const_iterator cbegin() const noexcept
+ {
+ return m_data.cbegin();
+ }
+ constexpr const_iterator end() const noexcept
+ {
+ return cend();
+ }
+ constexpr const_iterator cend() const noexcept
+ {
+ return m_data.cend();
+ }
+
+ constexpr const_reverse_iterator rbegin() const noexcept
+ {
+ return crbegin();
+ }
+ constexpr const_reverse_iterator crbegin() const noexcept
+ {
+ return m_data.crbegin();
+ }
+ constexpr const_reverse_iterator rend() const noexcept
+ {
+ return crend();
+ }
+ constexpr const_reverse_iterator crend() const noexcept
+ {
+ return m_data.crend();
+ }
+
+ constexpr const_reference operator[](size_type pos) const
+ {
+ return m_data[static_cast<typename span_type::index_type>(pos)];
+ }
+ SCN_CONSTEXPR14 const_reference at(size_type pos) const
+ {
+ SCN_EXPECT(pos < size());
+ return m_data.at(static_cast<typename span_type::index_type>(pos));
+ }
+
+ constexpr const_reference front() const
+ {
+ return operator[](0);
+ }
+ constexpr const_reference back() const
+ {
+ return operator[](size() - 1);
+ }
+ constexpr const_pointer data() const noexcept
+ {
+ return m_data.data();
+ }
+
+ SCN_NODISCARD constexpr size_type size() const noexcept
+ {
+ return static_cast<size_type>(m_data.size());
+ }
+ SCN_NODISCARD constexpr size_type length() const noexcept
+ {
+ return size();
+ }
+ SCN_NODISCARD constexpr size_type max_size() const noexcept
+ {
+ return SIZE_MAX - 1;
+ }
+ SCN_NODISCARD constexpr bool empty() const noexcept
+ {
+ return size() == 0;
+ }
+
+ SCN_CONSTEXPR14 void remove_prefix(size_type n)
+ {
+ SCN_EXPECT(n <= size());
+ m_data = m_data.subspan(n);
+ }
+ SCN_CONSTEXPR14 void remove_suffix(size_type n)
+ {
+ SCN_EXPECT(n <= size());
+ m_data = m_data.first(size() - n);
+ }
+
+ SCN_CONSTEXPR14 void swap(basic_string_view& v) noexcept
+ {
+ using std::swap;
+ swap(m_data, v.m_data);
+ }
+
+ size_type copy(pointer dest, size_type count, size_type pos = 0) const
+ {
+ SCN_EXPECT(pos <= size());
+ auto n = detail::min(count, size() - pos);
+ /* std::copy(data() + pos, n, dest); */
+ std::memcpy(dest, begin() + pos, n * sizeof(value_type));
+ return n;
+ }
+ SCN_CONSTEXPR14 basic_string_view substr(size_type pos = 0,
+ size_type count = npos) const
+ {
+ SCN_EXPECT(pos <= size());
+ auto n = detail::min(count, size() - pos);
+ return m_data.subspan(pos, n);
+ }
+
+#if SCN_HAS_STRING_VIEW
+ operator std::basic_string_view<value_type>() const noexcept
+ {
+ return {m_data.data(), m_data.size()};
+ }
+#endif
+
+ private:
+ span_type m_data{};
+ };
+
+ using string_view = basic_string_view<char>;
+ using wstring_view = basic_string_view<wchar_t>;
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif // SCN_STRING_VIEW_H
diff --git a/src/third-party/scnlib/include/scn/util/unique_ptr.h b/src/third-party/scnlib/include/scn/util/unique_ptr.h
new file mode 100644
index 0000000..4723de8
--- /dev/null
+++ b/src/third-party/scnlib/include/scn/util/unique_ptr.h
@@ -0,0 +1,118 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#ifndef SCN_UTIL_UNIQUE_PTR_H
+#define SCN_UTIL_UNIQUE_PTR_H
+
+#include "../detail/fwd.h"
+
+#include <type_traits>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ /**
+ * `std::unique_ptr` implementation with [[clang::trivial_abi]], without
+ * including `<memory>`
+ */
+ template <typename T>
+ class SCN_TRIVIAL_ABI unique_ptr {
+ public:
+ using element_type = T;
+ using pointer = T*;
+
+ constexpr unique_ptr() noexcept = default;
+ constexpr unique_ptr(std::nullptr_t) noexcept {}
+
+ constexpr explicit unique_ptr(pointer p) noexcept : m_ptr(p) {}
+
+ template <
+ typename U,
+ typename std::enable_if<
+ std::is_convertible<U*, pointer>::value>::type* = nullptr>
+ SCN_CONSTEXPR14 unique_ptr(unique_ptr<U>&& u) noexcept
+ : m_ptr(SCN_MOVE(u.get()))
+ {
+ u.reset();
+ }
+
+ unique_ptr(const unique_ptr&) = delete;
+ unique_ptr& operator=(const unique_ptr&) = delete;
+
+ SCN_CONSTEXPR14 unique_ptr(unique_ptr&& p) noexcept
+ : m_ptr(SCN_MOVE(p.m_ptr))
+ {
+ p.m_ptr = nullptr;
+ }
+ unique_ptr& operator=(unique_ptr&& p) noexcept
+ {
+ delete m_ptr;
+ m_ptr = p.m_ptr;
+ p.m_ptr = nullptr;
+ return *this;
+ }
+
+ ~unique_ptr() noexcept
+ {
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ delete m_ptr;
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+
+ constexpr explicit operator bool() const noexcept
+ {
+ return get() != nullptr;
+ }
+
+ constexpr pointer get() const noexcept
+ {
+ return m_ptr;
+ }
+
+ constexpr pointer operator->() const noexcept
+ {
+ return m_ptr;
+ }
+ constexpr typename std::add_lvalue_reference<T>::type operator*()
+ const
+ {
+ return *m_ptr;
+ }
+
+ SCN_CONSTEXPR14 void reset()
+ {
+ m_ptr = nullptr;
+ }
+
+ private:
+ pointer m_ptr{nullptr};
+ };
+
+ template <typename T, typename... Args>
+ unique_ptr<T> make_unique(Args&&... a)
+ {
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ return unique_ptr<T>(new T(SCN_FWD(a)...));
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ }
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
+
+#endif
diff --git a/src/third-party/scnlib/licenses/README.md b/src/third-party/scnlib/licenses/README.md
new file mode 100644
index 0000000..cd5e2d4
--- /dev/null
+++ b/src/third-party/scnlib/licenses/README.md
@@ -0,0 +1,25 @@
+# Third-party Software Licenses
+
+## {fmt}
+
+{fmt} is licensed under the MIT license.
+Copyright (c) 2012 - present, Victor Zverovich
+See `fmt.rst` for more
+
+## NanoRange
+
+NanoRange is licensed under the Boost Software License, version 1.0.
+Copyright (c) 2018 Tristan Brindle (tcbrindle at gmail dot com)
+See `nanorange.txt` for more
+
+## fast_float
+
+fast_float is licensed under either of Apache License, version 2.0 or MIT license.
+Copyright (c) 2021 The fast_float authors
+See `fast_float-apache.txt` and `fast_float-mit.txt` for more
+
+## utfcpp
+
+utfcpp is licensed under the Boost Software License, version 1.0.
+Copyright (c) 2006 Nemanja Trifunovic
+See `utfcpp.txt` for more
diff --git a/src/third-party/scnlib/licenses/fast_float-apache.txt b/src/third-party/scnlib/licenses/fast_float-apache.txt
new file mode 100644
index 0000000..26f4398
--- /dev/null
+++ b/src/third-party/scnlib/licenses/fast_float-apache.txt
@@ -0,0 +1,190 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ Copyright 2021 The fast_float authors
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/src/third-party/scnlib/licenses/fast_float-mit.txt b/src/third-party/scnlib/licenses/fast_float-mit.txt
new file mode 100644
index 0000000..2fb2a37
--- /dev/null
+++ b/src/third-party/scnlib/licenses/fast_float-mit.txt
@@ -0,0 +1,27 @@
+MIT License
+
+Copyright (c) 2021 The fast_float authors
+
+Permission is hereby granted, free of charge, to any
+person obtaining a copy of this software and associated
+documentation files (the "Software"), to deal in the
+Software without restriction, including without
+limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software
+is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice
+shall be included in all copies or substantial portions
+of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/src/third-party/scnlib/licenses/fmt.rst b/src/third-party/scnlib/licenses/fmt.rst
new file mode 100644
index 0000000..f0ec3db
--- /dev/null
+++ b/src/third-party/scnlib/licenses/fmt.rst
@@ -0,0 +1,27 @@
+Copyright (c) 2012 - present, Victor Zverovich
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+--- Optional exception to the license ---
+
+As an exception, if, as a result of your compiling your source code, portions
+of this Software are embedded into a machine-executable object form of such
+source code, you may redistribute such embedded portions in such object form
+without including the above copyright and permission notices.
diff --git a/src/third-party/scnlib/licenses/nanorange.txt b/src/third-party/scnlib/licenses/nanorange.txt
new file mode 100644
index 0000000..36b7cd9
--- /dev/null
+++ b/src/third-party/scnlib/licenses/nanorange.txt
@@ -0,0 +1,23 @@
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/src/third-party/scnlib/licenses/utfcpp.txt b/src/third-party/scnlib/licenses/utfcpp.txt
new file mode 100644
index 0000000..36b7cd9
--- /dev/null
+++ b/src/third-party/scnlib/licenses/utfcpp.txt
@@ -0,0 +1,23 @@
+Boost Software License - Version 1.0 - August 17th, 2003
+
+Permission is hereby granted, free of charge, to any person or organization
+obtaining a copy of the software and accompanying documentation covered by
+this license (the "Software") to use, reproduce, display, distribute,
+execute, and transmit the Software, and to prepare derivative works of the
+Software, and to permit third-parties to whom the Software is furnished to
+do so, all subject to the following:
+
+The copyright notices in the Software and this entire statement, including
+the above license grant, this restriction and the following disclaimer,
+must be included in all copies of the Software, in whole or in part, and
+all derivative works of the Software, unless such copies or derivative
+works are solely in the form of machine-executable object code generated by
+a source language processor.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT
+SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE
+FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE,
+ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/src/third-party/scnlib/src/Makefile.am b/src/third-party/scnlib/src/Makefile.am
new file mode 100644
index 0000000..7e5c4ae
--- /dev/null
+++ b/src/third-party/scnlib/src/Makefile.am
@@ -0,0 +1,65 @@
+
+noinst_HEADERS = \
+ ../include/scn/reader/reader.h \
+ ../include/scn/reader/float.h \
+ ../include/scn/reader/types.h \
+ ../include/scn/reader/int.h \
+ ../include/scn/reader/common.h \
+ ../include/scn/reader/string.h \
+ ../include/scn/ranges/custom_impl.h \
+ ../include/scn/ranges/std_impl.h \
+ ../include/scn/ranges/ranges.h \
+ ../include/scn/ranges/util.h \
+ ../include/scn/fwd.h \
+ ../include/scn/util/algorithm.h \
+ ../include/scn/util/small_vector.h \
+ ../include/scn/util/optional.h \
+ ../include/scn/util/expected.h \
+ ../include/scn/util/array.h \
+ ../include/scn/util/unique_ptr.h \
+ ../include/scn/util/math.h \
+ ../include/scn/util/memory.h \
+ ../include/scn/util/span.h \
+ ../include/scn/util/meta.h \
+ ../include/scn/util/string_view.h \
+ ../include/scn/unicode/unicode.h \
+ ../include/scn/unicode/common.h \
+ ../include/scn/unicode/utf16.h \
+ ../include/scn/unicode/utf8.h \
+ ../include/scn/all.h \
+ ../include/scn/tuple_return/tuple_return.h \
+ ../include/scn/tuple_return/util.h \
+ ../include/scn/scan/ignore.h \
+ ../include/scn/scan/getline.h \
+ ../include/scn/scan/list.h \
+ ../include/scn/scan/common.h \
+ ../include/scn/scan/istream.h \
+ ../include/scn/scan/vscan.h \
+ ../include/scn/scan/scan.h \
+ ../include/scn/tuple_return.h \
+ ../include/scn/detail/error.h \
+ ../include/scn/detail/fwd.h \
+ ../include/scn/detail/range.h \
+ ../include/scn/detail/locale.h \
+ ../include/scn/detail/config.h \
+ ../include/scn/detail/file.h \
+ ../include/scn/detail/context.h \
+ ../include/scn/detail/result.h \
+ ../include/scn/detail/visitor.h \
+ ../include/scn/detail/args.h \
+ ../include/scn/detail/parse_context.h \
+ ../include/scn/detail/vectored.h \
+ ../include/scn/scn.h \
+ ../include/scn/istream.h \
+ deps/fast_float/single_include/fast_float/fast_float.h
+
+noinst_LIBRARIES = libscnlib.a
+
+AM_CPPFLAGS = -I$(srcdir)/../include -I$(srcdir)/deps/fast_float/single_include
+
+libscnlib_a_SOURCES = \
+ reader_float.cpp \
+ locale.cpp \
+ reader_int.cpp \
+ file.cpp \
+ vscan.cpp
diff --git a/src/third-party/scnlib/src/deps/fast_float/single_include/fast_float/fast_float.h b/src/third-party/scnlib/src/deps/fast_float/single_include/fast_float/fast_float.h
new file mode 100644
index 0000000..3f94372
--- /dev/null
+++ b/src/third-party/scnlib/src/deps/fast_float/single_include/fast_float/fast_float.h
@@ -0,0 +1,2981 @@
+// fast_float v3.4.0
+
+// fast_float by Daniel Lemire
+// fast_float by João Paulo Magalhaes
+//
+// with contributions from Eugene Golushkov
+// with contributions from Maksim Kita
+// with contributions from Marcin Wojdyr
+// with contributions from Neal Richardson
+// with contributions from Tim Paine
+// with contributions from Fabio Pellacini
+//
+// Licensed under the Apache License, Version 2.0, or the
+// MIT License at your option. This file may not be copied,
+// modified, or distributed except according to those terms.
+//
+// MIT License Notice
+//
+// MIT License
+//
+// Copyright (c) 2021 The fast_float authors
+//
+// Permission is hereby granted, free of charge, to any
+// person obtaining a copy of this software and associated
+// documentation files (the "Software"), to deal in the
+// Software without restriction, including without
+// limitation the rights to use, copy, modify, merge,
+// publish, distribute, sublicense, and/or sell copies of
+// the Software, and to permit persons to whom the Software
+// is furnished to do so, subject to the following
+// conditions:
+//
+// The above copyright notice and this permission notice
+// shall be included in all copies or substantial portions
+// of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
+// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
+// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
+// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
+// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+//
+// Apache License (Version 2.0) Notice
+//
+// Copyright 2021 The fast_float authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+//
+
+#ifndef FASTFLOAT_FAST_FLOAT_H
+#define FASTFLOAT_FAST_FLOAT_H
+
+#include <system_error>
+
+namespace fast_float {
+ enum chars_format {
+ scientific = 1<<0,
+ fixed = 1<<2,
+ hex = 1<<3,
+ general = fixed | scientific
+ };
+
+
+ struct from_chars_result {
+ const char *ptr;
+ std::errc ec;
+ };
+
+ struct parse_options {
+ constexpr explicit parse_options(chars_format fmt = chars_format::general,
+ char dot = '.')
+ : format(fmt), decimal_point(dot) {}
+
+ /** Which number formats are accepted */
+ chars_format format;
+ /** The character used as decimal point */
+ char decimal_point;
+ };
+
+ /**
+ * This function parses the character sequence [first,last) for a number. It parses floating-point numbers expecting
+ * a locale-indepent format equivalent to what is used by std::strtod in the default ("C") locale.
+ * The resulting floating-point value is the closest floating-point values (using either float or double),
+ * using the "round to even" convention for values that would otherwise fall right in-between two values.
+ * That is, we provide exact parsing according to the IEEE standard.
+ *
+ * Given a successful parse, the pointer (`ptr`) in the returned value is set to point right after the
+ * parsed number, and the `value` referenced is set to the parsed value. In case of error, the returned
+ * `ec` contains a representative error, otherwise the default (`std::errc()`) value is stored.
+ *
+ * The implementation does not throw and does not allocate memory (e.g., with `new` or `malloc`).
+ *
+ * Like the C++17 standard, the `fast_float::from_chars` functions take an optional last argument of
+ * the type `fast_float::chars_format`. It is a bitset value: we check whether
+ * `fmt & fast_float::chars_format::fixed` and `fmt & fast_float::chars_format::scientific` are set
+ * to determine whether we allowe the fixed point and scientific notation respectively.
+ * The default is `fast_float::chars_format::general` which allows both `fixed` and `scientific`.
+ */
+ template<typename T>
+ from_chars_result from_chars(const char *first, const char *last,
+ T &value, chars_format fmt = chars_format::general) noexcept;
+
+ /**
+ * Like from_chars, but accepts an `options` argument to govern number parsing.
+ */
+ template<typename T>
+ from_chars_result from_chars_advanced(const char *first, const char *last,
+ T &value, parse_options options) noexcept;
+
+}
+#endif // FASTFLOAT_FAST_FLOAT_H
+
+#ifndef FASTFLOAT_FLOAT_COMMON_H
+#define FASTFLOAT_FLOAT_COMMON_H
+
+#include <cfloat>
+#include <cstdint>
+#include <cassert>
+#include <cstring>
+#include <type_traits>
+
+#if (defined(__x86_64) || defined(__x86_64__) || defined(_M_X64) \
+ || defined(__amd64) || defined(__aarch64__) || defined(_M_ARM64) \
+ || defined(__MINGW64__) \
+ || defined(__s390x__) \
+ || (defined(__ppc64__) || defined(__PPC64__) || defined(__ppc64le__) || defined(__PPC64LE__)) \
+ || defined(__EMSCRIPTEN__))
+#define FASTFLOAT_64BIT
+#elif (defined(__i386) || defined(__i386__) || defined(_M_IX86) \
+ || defined(__arm__) || defined(_M_ARM) \
+ || defined(__MINGW32__))
+#define FASTFLOAT_32BIT
+#else
+ // Need to check incrementally, since SIZE_MAX is a size_t, avoid overflow.
+// We can never tell the register width, but the SIZE_MAX is a good approximation.
+// UINTPTR_MAX and INTPTR_MAX are optional, so avoid them for max portability.
+#if SIZE_MAX == 0xffff
+#error Unknown platform (16-bit, unsupported)
+#elif SIZE_MAX == 0xffffffff
+#define FASTFLOAT_32BIT
+#elif SIZE_MAX == 0xffffffffffffffff
+#define FASTFLOAT_64BIT
+#else
+#error Unknown platform (not 32-bit, not 64-bit?)
+#endif
+#endif
+
+#if ((defined(_WIN32) || defined(_WIN64)) && !defined(__clang__))
+#include <intrin.h>
+#endif
+
+#if defined(_MSC_VER) && !defined(__clang__)
+#define FASTFLOAT_VISUAL_STUDIO 1
+#endif
+
+#ifdef _WIN32
+#define FASTFLOAT_IS_BIG_ENDIAN 0
+#else
+#if defined(__APPLE__) || defined(__FreeBSD__)
+#include <machine/endian.h>
+#elif defined(sun) || defined(__sun)
+#include <sys/byteorder.h>
+#else
+#include <endian.h>
+#endif
+#
+#ifndef __BYTE_ORDER__
+// safe choice
+#define FASTFLOAT_IS_BIG_ENDIAN 0
+#endif
+#
+#ifndef __ORDER_LITTLE_ENDIAN__
+// safe choice
+#define FASTFLOAT_IS_BIG_ENDIAN 0
+#endif
+#
+#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
+#define FASTFLOAT_IS_BIG_ENDIAN 0
+#else
+#define FASTFLOAT_IS_BIG_ENDIAN 1
+#endif
+#endif
+
+#ifdef FASTFLOAT_VISUAL_STUDIO
+#define fastfloat_really_inline __forceinline
+#else
+#define fastfloat_really_inline inline __attribute__((always_inline))
+#endif
+
+#ifndef FASTFLOAT_ASSERT
+#define FASTFLOAT_ASSERT(x) { if (!(x)) abort(); }
+#endif
+
+#ifndef FASTFLOAT_DEBUG_ASSERT
+#include <cassert>
+#define FASTFLOAT_DEBUG_ASSERT(x) assert(x)
+#endif
+
+// rust style `try!()` macro, or `?` operator
+#define FASTFLOAT_TRY(x) { if (!(x)) return false; }
+
+namespace fast_float {
+
+ // Compares two ASCII strings in a case insensitive manner.
+ inline bool fastfloat_strncasecmp(const char *input1, const char *input2,
+ size_t length) {
+ char running_diff{0};
+ for (size_t i = 0; i < length; i++) {
+ running_diff |= (input1[i] ^ input2[i]);
+ }
+ return (running_diff == 0) || (running_diff == 32);
+ }
+
+#ifndef FLT_EVAL_METHOD
+#error "FLT_EVAL_METHOD should be defined, please include cfloat."
+#endif
+
+ // a pointer and a length to a contiguous block of memory
+ template <typename T>
+ struct span {
+ const T* ptr;
+ size_t length;
+ span(const T* _ptr, size_t _length) : ptr(_ptr), length(_length) {}
+ span() : ptr(nullptr), length(0) {}
+
+ constexpr size_t len() const noexcept {
+ return length;
+ }
+
+ const T& operator[](size_t index) const noexcept {
+ FASTFLOAT_DEBUG_ASSERT(index < length);
+ return ptr[index];
+ }
+ };
+
+ struct value128 {
+ uint64_t low;
+ uint64_t high;
+ value128(uint64_t _low, uint64_t _high) : low(_low), high(_high) {}
+ value128() : low(0), high(0) {}
+ };
+
+ /* result might be undefined when input_num is zero */
+ fastfloat_really_inline int leading_zeroes(uint64_t input_num) {
+ assert(input_num > 0);
+#ifdef FASTFLOAT_VISUAL_STUDIO
+ #if defined(_M_X64) || defined(_M_ARM64)
+ unsigned long leading_zero = 0;
+ // Search the mask data from most significant bit (MSB)
+ // to least significant bit (LSB) for a set bit (1).
+ _BitScanReverse64(&leading_zero, input_num);
+ return (int)(63 - leading_zero);
+#else
+ int last_bit = 0;
+ if(input_num & uint64_t(0xffffffff00000000)) input_num >>= 32, last_bit |= 32;
+ if(input_num & uint64_t( 0xffff0000)) input_num >>= 16, last_bit |= 16;
+ if(input_num & uint64_t( 0xff00)) input_num >>= 8, last_bit |= 8;
+ if(input_num & uint64_t( 0xf0)) input_num >>= 4, last_bit |= 4;
+ if(input_num & uint64_t( 0xc)) input_num >>= 2, last_bit |= 2;
+ if(input_num & uint64_t( 0x2)) input_num >>= 1, last_bit |= 1;
+ return 63 - last_bit;
+#endif
+#else
+ return __builtin_clzll(input_num);
+#endif
+ }
+
+#ifdef FASTFLOAT_32BIT
+
+ // slow emulation routine for 32-bit
+ fastfloat_really_inline uint64_t emulu(uint32_t x, uint32_t y) {
+ return x * (uint64_t)y;
+ }
+
+// slow emulation routine for 32-bit
+#if !defined(__MINGW64__)
+ fastfloat_really_inline uint64_t _umul128(uint64_t ab, uint64_t cd,
+ uint64_t *hi) {
+ uint64_t ad = emulu((uint32_t)(ab >> 32), (uint32_t)cd);
+ uint64_t bd = emulu((uint32_t)ab, (uint32_t)cd);
+ uint64_t adbc = ad + emulu((uint32_t)ab, (uint32_t)(cd >> 32));
+ uint64_t adbc_carry = !!(adbc < ad);
+ uint64_t lo = bd + (adbc << 32);
+ *hi = emulu((uint32_t)(ab >> 32), (uint32_t)(cd >> 32)) + (adbc >> 32) +
+ (adbc_carry << 32) + !!(lo < bd);
+ return lo;
+ }
+#endif // !__MINGW64__
+
+#endif // FASTFLOAT_32BIT
+
+
+ // compute 64-bit a*b
+ fastfloat_really_inline value128 full_multiplication(uint64_t a,
+ uint64_t b) {
+ value128 answer;
+#ifdef _M_ARM64
+ // ARM64 has native support for 64-bit multiplications, no need to emulate
+ answer.high = __umulh(a, b);
+ answer.low = a * b;
+#elif defined(FASTFLOAT_32BIT) || (defined(_WIN64) && !defined(__clang__))
+ answer.low = _umul128(a, b, &answer.high); // _umul128 not available on ARM64
+#elif defined(FASTFLOAT_64BIT)
+ __uint128_t r = ((__uint128_t)a) * b;
+ answer.low = uint64_t(r);
+ answer.high = uint64_t(r >> 64);
+#else
+#error Not implemented
+#endif
+ return answer;
+ }
+
+ struct adjusted_mantissa {
+ uint64_t mantissa{0};
+ int32_t power2{0}; // a negative value indicates an invalid result
+ adjusted_mantissa() = default;
+ bool operator==(const adjusted_mantissa &o) const {
+ return mantissa == o.mantissa && power2 == o.power2;
+ }
+ bool operator!=(const adjusted_mantissa &o) const {
+ return mantissa != o.mantissa || power2 != o.power2;
+ }
+ };
+
+ // Bias so we can get the real exponent with an invalid adjusted_mantissa.
+ constexpr static int32_t invalid_am_bias = -0x8000;
+
+ constexpr static double powers_of_ten_double[] = {
+ 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11,
+ 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22};
+ constexpr static float powers_of_ten_float[] = {1e0, 1e1, 1e2, 1e3, 1e4, 1e5,
+ 1e6, 1e7, 1e8, 1e9, 1e10};
+
+ template <typename T> struct binary_format {
+ using equiv_uint = typename std::conditional<sizeof(T) == 4, uint32_t, uint64_t>::type;
+
+ static inline constexpr int mantissa_explicit_bits();
+ static inline constexpr int minimum_exponent();
+ static inline constexpr int infinite_power();
+ static inline constexpr int sign_index();
+ static inline constexpr int min_exponent_fast_path();
+ static inline constexpr int max_exponent_fast_path();
+ static inline constexpr int max_exponent_round_to_even();
+ static inline constexpr int min_exponent_round_to_even();
+ static inline constexpr uint64_t max_mantissa_fast_path();
+ static inline constexpr int largest_power_of_ten();
+ static inline constexpr int smallest_power_of_ten();
+ static inline constexpr T exact_power_of_ten(int64_t power);
+ static inline constexpr size_t max_digits();
+ static inline constexpr equiv_uint exponent_mask();
+ static inline constexpr equiv_uint mantissa_mask();
+ static inline constexpr equiv_uint hidden_bit_mask();
+ };
+
+ template <> inline constexpr int binary_format<double>::mantissa_explicit_bits() {
+ return 52;
+ }
+ template <> inline constexpr int binary_format<float>::mantissa_explicit_bits() {
+ return 23;
+ }
+
+ template <> inline constexpr int binary_format<double>::max_exponent_round_to_even() {
+ return 23;
+ }
+
+ template <> inline constexpr int binary_format<float>::max_exponent_round_to_even() {
+ return 10;
+ }
+
+ template <> inline constexpr int binary_format<double>::min_exponent_round_to_even() {
+ return -4;
+ }
+
+ template <> inline constexpr int binary_format<float>::min_exponent_round_to_even() {
+ return -17;
+ }
+
+ template <> inline constexpr int binary_format<double>::minimum_exponent() {
+ return -1023;
+ }
+ template <> inline constexpr int binary_format<float>::minimum_exponent() {
+ return -127;
+ }
+
+ template <> inline constexpr int binary_format<double>::infinite_power() {
+ return 0x7FF;
+ }
+ template <> inline constexpr int binary_format<float>::infinite_power() {
+ return 0xFF;
+ }
+
+ template <> inline constexpr int binary_format<double>::sign_index() { return 63; }
+ template <> inline constexpr int binary_format<float>::sign_index() { return 31; }
+
+ template <> inline constexpr int binary_format<double>::min_exponent_fast_path() {
+#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0)
+ return 0;
+#else
+ return -22;
+#endif
+ }
+ template <> inline constexpr int binary_format<float>::min_exponent_fast_path() {
+#if (FLT_EVAL_METHOD != 1) && (FLT_EVAL_METHOD != 0)
+ return 0;
+#else
+ return -10;
+#endif
+ }
+
+ template <> inline constexpr int binary_format<double>::max_exponent_fast_path() {
+ return 22;
+ }
+ template <> inline constexpr int binary_format<float>::max_exponent_fast_path() {
+ return 10;
+ }
+
+ template <> inline constexpr uint64_t binary_format<double>::max_mantissa_fast_path() {
+ return uint64_t(2) << mantissa_explicit_bits();
+ }
+ template <> inline constexpr uint64_t binary_format<float>::max_mantissa_fast_path() {
+ return uint64_t(2) << mantissa_explicit_bits();
+ }
+
+ template <>
+ inline constexpr double binary_format<double>::exact_power_of_ten(int64_t power) {
+ return powers_of_ten_double[power];
+ }
+ template <>
+ inline constexpr float binary_format<float>::exact_power_of_ten(int64_t power) {
+
+ return powers_of_ten_float[power];
+ }
+
+
+ template <>
+ inline constexpr int binary_format<double>::largest_power_of_ten() {
+ return 308;
+ }
+ template <>
+ inline constexpr int binary_format<float>::largest_power_of_ten() {
+ return 38;
+ }
+
+ template <>
+ inline constexpr int binary_format<double>::smallest_power_of_ten() {
+ return -342;
+ }
+ template <>
+ inline constexpr int binary_format<float>::smallest_power_of_ten() {
+ return -65;
+ }
+
+ template <> inline constexpr size_t binary_format<double>::max_digits() {
+ return 769;
+ }
+ template <> inline constexpr size_t binary_format<float>::max_digits() {
+ return 114;
+ }
+
+ template <> inline constexpr binary_format<float>::equiv_uint
+ binary_format<float>::exponent_mask() {
+ return 0x7F800000;
+ }
+ template <> inline constexpr binary_format<double>::equiv_uint
+ binary_format<double>::exponent_mask() {
+ return 0x7FF0000000000000;
+ }
+
+ template <> inline constexpr binary_format<float>::equiv_uint
+ binary_format<float>::mantissa_mask() {
+ return 0x007FFFFF;
+ }
+ template <> inline constexpr binary_format<double>::equiv_uint
+ binary_format<double>::mantissa_mask() {
+ return 0x000FFFFFFFFFFFFF;
+ }
+
+ template <> inline constexpr binary_format<float>::equiv_uint
+ binary_format<float>::hidden_bit_mask() {
+ return 0x00800000;
+ }
+ template <> inline constexpr binary_format<double>::equiv_uint
+ binary_format<double>::hidden_bit_mask() {
+ return 0x0010000000000000;
+ }
+
+ template<typename T>
+ fastfloat_really_inline void to_float(bool negative, adjusted_mantissa am, T &value) {
+ uint64_t word = am.mantissa;
+ word |= uint64_t(am.power2) << binary_format<T>::mantissa_explicit_bits();
+ word = negative
+ ? word | (uint64_t(1) << binary_format<T>::sign_index()) : word;
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ if (std::is_same<T, float>::value) {
+ ::memcpy(&value, (char *)&word + 4, sizeof(T)); // extract value at offset 4-7 if float on big-endian
+ } else {
+ ::memcpy(&value, &word, sizeof(T));
+ }
+#else
+ // For little-endian systems:
+ ::memcpy(&value, &word, sizeof(T));
+#endif
+ }
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_ASCII_NUMBER_H
+#define FASTFLOAT_ASCII_NUMBER_H
+
+#include <cctype>
+#include <cstdint>
+#include <cstring>
+#include <iterator>
+
+
+namespace fast_float {
+
+ // Next function can be micro-optimized, but compilers are entirely
+ // able to optimize it well.
+ fastfloat_really_inline bool is_integer(char c) noexcept { return c >= '0' && c <= '9'; }
+
+ fastfloat_really_inline uint64_t byteswap(uint64_t val) {
+ return (val & 0xFF00000000000000) >> 56
+ | (val & 0x00FF000000000000) >> 40
+ | (val & 0x0000FF0000000000) >> 24
+ | (val & 0x000000FF00000000) >> 8
+ | (val & 0x00000000FF000000) << 8
+ | (val & 0x0000000000FF0000) << 24
+ | (val & 0x000000000000FF00) << 40
+ | (val & 0x00000000000000FF) << 56;
+ }
+
+ fastfloat_really_inline uint64_t read_u64(const char *chars) {
+ uint64_t val;
+ ::memcpy(&val, chars, sizeof(uint64_t));
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ // Need to read as-if the number was in little-endian order.
+ val = byteswap(val);
+#endif
+ return val;
+ }
+
+ fastfloat_really_inline void write_u64(uint8_t *chars, uint64_t val) {
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ // Need to read as-if the number was in little-endian order.
+ val = byteswap(val);
+#endif
+ ::memcpy(chars, &val, sizeof(uint64_t));
+ }
+
+ // credit @aqrit
+ fastfloat_really_inline uint32_t parse_eight_digits_unrolled(uint64_t val) {
+ const uint64_t mask = 0x000000FF000000FF;
+ const uint64_t mul1 = 0x000F424000000064; // 100 + (1000000ULL << 32)
+ const uint64_t mul2 = 0x0000271000000001; // 1 + (10000ULL << 32)
+ val -= 0x3030303030303030;
+ val = (val * 10) + (val >> 8); // val = (val * 2561) >> 8;
+ val = (((val & mask) * mul1) + (((val >> 16) & mask) * mul2)) >> 32;
+ return uint32_t(val);
+ }
+
+ fastfloat_really_inline uint32_t parse_eight_digits_unrolled(const char *chars) noexcept {
+ return parse_eight_digits_unrolled(read_u64(chars));
+ }
+
+ // credit @aqrit
+ fastfloat_really_inline bool is_made_of_eight_digits_fast(uint64_t val) noexcept {
+ return !((((val + 0x4646464646464646) | (val - 0x3030303030303030)) &
+ 0x8080808080808080));
+ }
+
+ fastfloat_really_inline bool is_made_of_eight_digits_fast(const char *chars) noexcept {
+ return is_made_of_eight_digits_fast(read_u64(chars));
+ }
+
+ typedef span<const char> byte_span;
+
+ struct parsed_number_string {
+ int64_t exponent{0};
+ uint64_t mantissa{0};
+ const char *lastmatch{nullptr};
+ bool negative{false};
+ bool valid{false};
+ bool too_many_digits{false};
+ // contains the range of the significant digits
+ byte_span integer{}; // non-nullable
+ byte_span fraction{}; // nullable
+ };
+
+ // Assuming that you use no more than 19 digits, this will
+ // parse an ASCII string.
+ fastfloat_really_inline
+ parsed_number_string parse_number_string(const char *p, const char *pend, parse_options options) noexcept {
+ const chars_format fmt = options.format;
+ const char decimal_point = options.decimal_point;
+
+ parsed_number_string answer;
+ answer.valid = false;
+ answer.too_many_digits = false;
+ answer.negative = (*p == '-');
+ if (*p == '-') { // C++17 20.19.3.(7.1) explicitly forbids '+' sign here
+ ++p;
+ if (p == pend) {
+ return answer;
+ }
+ if (!is_integer(*p) && (*p != decimal_point)) { // a sign must be followed by an integer or the dot
+ return answer;
+ }
+ }
+ const char *const start_digits = p;
+
+ uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad)
+
+ while ((std::distance(p, pend) >= 8) && is_made_of_eight_digits_fast(p)) {
+ i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok
+ p += 8;
+ }
+ while ((p != pend) && is_integer(*p)) {
+ // a multiplication by 10 is cheaper than an arbitrary integer
+ // multiplication
+ i = 10 * i +
+ uint64_t(*p - '0'); // might overflow, we will handle the overflow later
+ ++p;
+ }
+ const char *const end_of_integer_part = p;
+ int64_t digit_count = int64_t(end_of_integer_part - start_digits);
+ answer.integer = byte_span(start_digits, size_t(digit_count));
+ int64_t exponent = 0;
+ if ((p != pend) && (*p == decimal_point)) {
+ ++p;
+ const char* before = p;
+ // can occur at most twice without overflowing, but let it occur more, since
+ // for integers with many digits, digit parsing is the primary bottleneck.
+ while ((std::distance(p, pend) >= 8) && is_made_of_eight_digits_fast(p)) {
+ i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok
+ p += 8;
+ }
+ while ((p != pend) && is_integer(*p)) {
+ uint8_t digit = uint8_t(*p - '0');
+ ++p;
+ i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
+ }
+ exponent = before - p;
+ answer.fraction = byte_span(before, size_t(p - before));
+ digit_count -= exponent;
+ }
+ // we must have encountered at least one integer!
+ if (digit_count == 0) {
+ return answer;
+ }
+ int64_t exp_number = 0; // explicit exponential part
+ if ((fmt & chars_format::scientific) && (p != pend) && (('e' == *p) || ('E' == *p))) {
+ const char * location_of_e = p;
+ ++p;
+ bool neg_exp = false;
+ if ((p != pend) && ('-' == *p)) {
+ neg_exp = true;
+ ++p;
+ } else if ((p != pend) && ('+' == *p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1)
+ ++p;
+ }
+ if ((p == pend) || !is_integer(*p)) {
+ if(!(fmt & chars_format::fixed)) {
+ // We are in error.
+ return answer;
+ }
+ // Otherwise, we will be ignoring the 'e'.
+ p = location_of_e;
+ } else {
+ while ((p != pend) && is_integer(*p)) {
+ uint8_t digit = uint8_t(*p - '0');
+ if (exp_number < 0x10000000) {
+ exp_number = 10 * exp_number + digit;
+ }
+ ++p;
+ }
+ if(neg_exp) { exp_number = - exp_number; }
+ exponent += exp_number;
+ }
+ } else {
+ // If it scientific and not fixed, we have to bail out.
+ if((fmt & chars_format::scientific) && !(fmt & chars_format::fixed)) { return answer; }
+ }
+ answer.lastmatch = p;
+ answer.valid = true;
+
+ // If we frequently had to deal with long strings of digits,
+ // we could extend our code by using a 128-bit integer instead
+ // of a 64-bit integer. However, this is uncommon.
+ //
+ // We can deal with up to 19 digits.
+ if (digit_count > 19) { // this is uncommon
+ // It is possible that the integer had an overflow.
+ // We have to handle the case where we have 0.0000somenumber.
+ // We need to be mindful of the case where we only have zeroes...
+ // E.g., 0.000000000...000.
+ const char *start = start_digits;
+ while ((start != pend) && (*start == '0' || *start == decimal_point)) {
+ if(*start == '0') { digit_count --; }
+ start++;
+ }
+ if (digit_count > 19) {
+ answer.too_many_digits = true;
+ // Let us start again, this time, avoiding overflows.
+ // We don't need to check if is_integer, since we use the
+ // pre-tokenized spans from above.
+ i = 0;
+ p = answer.integer.ptr;
+ const char* int_end = p + answer.integer.len();
+ const uint64_t minimal_nineteen_digit_integer{1000000000000000000};
+ while((i < minimal_nineteen_digit_integer) && (p != int_end)) {
+ i = i * 10 + uint64_t(*p - '0');
+ ++p;
+ }
+ if (i >= minimal_nineteen_digit_integer) { // We have a big integers
+ exponent = end_of_integer_part - p + exp_number;
+ } else { // We have a value with a fractional component.
+ p = answer.fraction.ptr;
+ const char* frac_end = p + answer.fraction.len();
+ while((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
+ i = i * 10 + uint64_t(*p - '0');
+ ++p;
+ }
+ exponent = answer.fraction.ptr - p + exp_number;
+ }
+ // We have now corrected both exponent and i, to a truncated value
+ }
+ }
+ answer.exponent = exponent;
+ answer.mantissa = i;
+ return answer;
+ }
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_FAST_TABLE_H
+#define FASTFLOAT_FAST_TABLE_H
+
+#include <cstdint>
+
+namespace fast_float {
+
+ /**
+ * When mapping numbers from decimal to binary,
+ * we go from w * 10^q to m * 2^p but we have
+ * 10^q = 5^q * 2^q, so effectively
+ * we are trying to match
+ * w * 2^q * 5^q to m * 2^p. Thus the powers of two
+ * are not a concern since they can be represented
+ * exactly using the binary notation, only the powers of five
+ * affect the binary significand.
+ */
+
+ /**
+ * The smallest non-zero float (binary64) is 2^−1074.
+ * We take as input numbers of the form w x 10^q where w < 2^64.
+ * We have that w * 10^-343 < 2^(64-344) 5^-343 < 2^-1076.
+ * However, we have that
+ * (2^64-1) * 10^-342 = (2^64-1) * 2^-342 * 5^-342 > 2^−1074.
+ * Thus it is possible for a number of the form w * 10^-342 where
+ * w is a 64-bit value to be a non-zero floating-point number.
+ *********
+ * Any number of form w * 10^309 where w>= 1 is going to be
+ * infinite in binary64 so we never need to worry about powers
+ * of 5 greater than 308.
+ */
+ template <class unused = void>
+ struct powers_template {
+
+ constexpr static int smallest_power_of_five = binary_format<double>::smallest_power_of_ten();
+ constexpr static int largest_power_of_five = binary_format<double>::largest_power_of_ten();
+ constexpr static int number_of_entries = 2 * (largest_power_of_five - smallest_power_of_five + 1);
+ // Powers of five from 5^-342 all the way to 5^308 rounded toward one.
+ static const uint64_t power_of_five_128[number_of_entries];
+ };
+
+ template <class unused>
+ const uint64_t powers_template<unused>::power_of_five_128[number_of_entries] = {
+ 0xeef453d6923bd65a,0x113faa2906a13b3f,
+ 0x9558b4661b6565f8,0x4ac7ca59a424c507,
+ 0xbaaee17fa23ebf76,0x5d79bcf00d2df649,
+ 0xe95a99df8ace6f53,0xf4d82c2c107973dc,
+ 0x91d8a02bb6c10594,0x79071b9b8a4be869,
+ 0xb64ec836a47146f9,0x9748e2826cdee284,
+ 0xe3e27a444d8d98b7,0xfd1b1b2308169b25,
+ 0x8e6d8c6ab0787f72,0xfe30f0f5e50e20f7,
+ 0xb208ef855c969f4f,0xbdbd2d335e51a935,
+ 0xde8b2b66b3bc4723,0xad2c788035e61382,
+ 0x8b16fb203055ac76,0x4c3bcb5021afcc31,
+ 0xaddcb9e83c6b1793,0xdf4abe242a1bbf3d,
+ 0xd953e8624b85dd78,0xd71d6dad34a2af0d,
+ 0x87d4713d6f33aa6b,0x8672648c40e5ad68,
+ 0xa9c98d8ccb009506,0x680efdaf511f18c2,
+ 0xd43bf0effdc0ba48,0x212bd1b2566def2,
+ 0x84a57695fe98746d,0x14bb630f7604b57,
+ 0xa5ced43b7e3e9188,0x419ea3bd35385e2d,
+ 0xcf42894a5dce35ea,0x52064cac828675b9,
+ 0x818995ce7aa0e1b2,0x7343efebd1940993,
+ 0xa1ebfb4219491a1f,0x1014ebe6c5f90bf8,
+ 0xca66fa129f9b60a6,0xd41a26e077774ef6,
+ 0xfd00b897478238d0,0x8920b098955522b4,
+ 0x9e20735e8cb16382,0x55b46e5f5d5535b0,
+ 0xc5a890362fddbc62,0xeb2189f734aa831d,
+ 0xf712b443bbd52b7b,0xa5e9ec7501d523e4,
+ 0x9a6bb0aa55653b2d,0x47b233c92125366e,
+ 0xc1069cd4eabe89f8,0x999ec0bb696e840a,
+ 0xf148440a256e2c76,0xc00670ea43ca250d,
+ 0x96cd2a865764dbca,0x380406926a5e5728,
+ 0xbc807527ed3e12bc,0xc605083704f5ecf2,
+ 0xeba09271e88d976b,0xf7864a44c633682e,
+ 0x93445b8731587ea3,0x7ab3ee6afbe0211d,
+ 0xb8157268fdae9e4c,0x5960ea05bad82964,
+ 0xe61acf033d1a45df,0x6fb92487298e33bd,
+ 0x8fd0c16206306bab,0xa5d3b6d479f8e056,
+ 0xb3c4f1ba87bc8696,0x8f48a4899877186c,
+ 0xe0b62e2929aba83c,0x331acdabfe94de87,
+ 0x8c71dcd9ba0b4925,0x9ff0c08b7f1d0b14,
+ 0xaf8e5410288e1b6f,0x7ecf0ae5ee44dd9,
+ 0xdb71e91432b1a24a,0xc9e82cd9f69d6150,
+ 0x892731ac9faf056e,0xbe311c083a225cd2,
+ 0xab70fe17c79ac6ca,0x6dbd630a48aaf406,
+ 0xd64d3d9db981787d,0x92cbbccdad5b108,
+ 0x85f0468293f0eb4e,0x25bbf56008c58ea5,
+ 0xa76c582338ed2621,0xaf2af2b80af6f24e,
+ 0xd1476e2c07286faa,0x1af5af660db4aee1,
+ 0x82cca4db847945ca,0x50d98d9fc890ed4d,
+ 0xa37fce126597973c,0xe50ff107bab528a0,
+ 0xcc5fc196fefd7d0c,0x1e53ed49a96272c8,
+ 0xff77b1fcbebcdc4f,0x25e8e89c13bb0f7a,
+ 0x9faacf3df73609b1,0x77b191618c54e9ac,
+ 0xc795830d75038c1d,0xd59df5b9ef6a2417,
+ 0xf97ae3d0d2446f25,0x4b0573286b44ad1d,
+ 0x9becce62836ac577,0x4ee367f9430aec32,
+ 0xc2e801fb244576d5,0x229c41f793cda73f,
+ 0xf3a20279ed56d48a,0x6b43527578c1110f,
+ 0x9845418c345644d6,0x830a13896b78aaa9,
+ 0xbe5691ef416bd60c,0x23cc986bc656d553,
+ 0xedec366b11c6cb8f,0x2cbfbe86b7ec8aa8,
+ 0x94b3a202eb1c3f39,0x7bf7d71432f3d6a9,
+ 0xb9e08a83a5e34f07,0xdaf5ccd93fb0cc53,
+ 0xe858ad248f5c22c9,0xd1b3400f8f9cff68,
+ 0x91376c36d99995be,0x23100809b9c21fa1,
+ 0xb58547448ffffb2d,0xabd40a0c2832a78a,
+ 0xe2e69915b3fff9f9,0x16c90c8f323f516c,
+ 0x8dd01fad907ffc3b,0xae3da7d97f6792e3,
+ 0xb1442798f49ffb4a,0x99cd11cfdf41779c,
+ 0xdd95317f31c7fa1d,0x40405643d711d583,
+ 0x8a7d3eef7f1cfc52,0x482835ea666b2572,
+ 0xad1c8eab5ee43b66,0xda3243650005eecf,
+ 0xd863b256369d4a40,0x90bed43e40076a82,
+ 0x873e4f75e2224e68,0x5a7744a6e804a291,
+ 0xa90de3535aaae202,0x711515d0a205cb36,
+ 0xd3515c2831559a83,0xd5a5b44ca873e03,
+ 0x8412d9991ed58091,0xe858790afe9486c2,
+ 0xa5178fff668ae0b6,0x626e974dbe39a872,
+ 0xce5d73ff402d98e3,0xfb0a3d212dc8128f,
+ 0x80fa687f881c7f8e,0x7ce66634bc9d0b99,
+ 0xa139029f6a239f72,0x1c1fffc1ebc44e80,
+ 0xc987434744ac874e,0xa327ffb266b56220,
+ 0xfbe9141915d7a922,0x4bf1ff9f0062baa8,
+ 0x9d71ac8fada6c9b5,0x6f773fc3603db4a9,
+ 0xc4ce17b399107c22,0xcb550fb4384d21d3,
+ 0xf6019da07f549b2b,0x7e2a53a146606a48,
+ 0x99c102844f94e0fb,0x2eda7444cbfc426d,
+ 0xc0314325637a1939,0xfa911155fefb5308,
+ 0xf03d93eebc589f88,0x793555ab7eba27ca,
+ 0x96267c7535b763b5,0x4bc1558b2f3458de,
+ 0xbbb01b9283253ca2,0x9eb1aaedfb016f16,
+ 0xea9c227723ee8bcb,0x465e15a979c1cadc,
+ 0x92a1958a7675175f,0xbfacd89ec191ec9,
+ 0xb749faed14125d36,0xcef980ec671f667b,
+ 0xe51c79a85916f484,0x82b7e12780e7401a,
+ 0x8f31cc0937ae58d2,0xd1b2ecb8b0908810,
+ 0xb2fe3f0b8599ef07,0x861fa7e6dcb4aa15,
+ 0xdfbdcece67006ac9,0x67a791e093e1d49a,
+ 0x8bd6a141006042bd,0xe0c8bb2c5c6d24e0,
+ 0xaecc49914078536d,0x58fae9f773886e18,
+ 0xda7f5bf590966848,0xaf39a475506a899e,
+ 0x888f99797a5e012d,0x6d8406c952429603,
+ 0xaab37fd7d8f58178,0xc8e5087ba6d33b83,
+ 0xd5605fcdcf32e1d6,0xfb1e4a9a90880a64,
+ 0x855c3be0a17fcd26,0x5cf2eea09a55067f,
+ 0xa6b34ad8c9dfc06f,0xf42faa48c0ea481e,
+ 0xd0601d8efc57b08b,0xf13b94daf124da26,
+ 0x823c12795db6ce57,0x76c53d08d6b70858,
+ 0xa2cb1717b52481ed,0x54768c4b0c64ca6e,
+ 0xcb7ddcdda26da268,0xa9942f5dcf7dfd09,
+ 0xfe5d54150b090b02,0xd3f93b35435d7c4c,
+ 0x9efa548d26e5a6e1,0xc47bc5014a1a6daf,
+ 0xc6b8e9b0709f109a,0x359ab6419ca1091b,
+ 0xf867241c8cc6d4c0,0xc30163d203c94b62,
+ 0x9b407691d7fc44f8,0x79e0de63425dcf1d,
+ 0xc21094364dfb5636,0x985915fc12f542e4,
+ 0xf294b943e17a2bc4,0x3e6f5b7b17b2939d,
+ 0x979cf3ca6cec5b5a,0xa705992ceecf9c42,
+ 0xbd8430bd08277231,0x50c6ff782a838353,
+ 0xece53cec4a314ebd,0xa4f8bf5635246428,
+ 0x940f4613ae5ed136,0x871b7795e136be99,
+ 0xb913179899f68584,0x28e2557b59846e3f,
+ 0xe757dd7ec07426e5,0x331aeada2fe589cf,
+ 0x9096ea6f3848984f,0x3ff0d2c85def7621,
+ 0xb4bca50b065abe63,0xfed077a756b53a9,
+ 0xe1ebce4dc7f16dfb,0xd3e8495912c62894,
+ 0x8d3360f09cf6e4bd,0x64712dd7abbbd95c,
+ 0xb080392cc4349dec,0xbd8d794d96aacfb3,
+ 0xdca04777f541c567,0xecf0d7a0fc5583a0,
+ 0x89e42caaf9491b60,0xf41686c49db57244,
+ 0xac5d37d5b79b6239,0x311c2875c522ced5,
+ 0xd77485cb25823ac7,0x7d633293366b828b,
+ 0x86a8d39ef77164bc,0xae5dff9c02033197,
+ 0xa8530886b54dbdeb,0xd9f57f830283fdfc,
+ 0xd267caa862a12d66,0xd072df63c324fd7b,
+ 0x8380dea93da4bc60,0x4247cb9e59f71e6d,
+ 0xa46116538d0deb78,0x52d9be85f074e608,
+ 0xcd795be870516656,0x67902e276c921f8b,
+ 0x806bd9714632dff6,0xba1cd8a3db53b6,
+ 0xa086cfcd97bf97f3,0x80e8a40eccd228a4,
+ 0xc8a883c0fdaf7df0,0x6122cd128006b2cd,
+ 0xfad2a4b13d1b5d6c,0x796b805720085f81,
+ 0x9cc3a6eec6311a63,0xcbe3303674053bb0,
+ 0xc3f490aa77bd60fc,0xbedbfc4411068a9c,
+ 0xf4f1b4d515acb93b,0xee92fb5515482d44,
+ 0x991711052d8bf3c5,0x751bdd152d4d1c4a,
+ 0xbf5cd54678eef0b6,0xd262d45a78a0635d,
+ 0xef340a98172aace4,0x86fb897116c87c34,
+ 0x9580869f0e7aac0e,0xd45d35e6ae3d4da0,
+ 0xbae0a846d2195712,0x8974836059cca109,
+ 0xe998d258869facd7,0x2bd1a438703fc94b,
+ 0x91ff83775423cc06,0x7b6306a34627ddcf,
+ 0xb67f6455292cbf08,0x1a3bc84c17b1d542,
+ 0xe41f3d6a7377eeca,0x20caba5f1d9e4a93,
+ 0x8e938662882af53e,0x547eb47b7282ee9c,
+ 0xb23867fb2a35b28d,0xe99e619a4f23aa43,
+ 0xdec681f9f4c31f31,0x6405fa00e2ec94d4,
+ 0x8b3c113c38f9f37e,0xde83bc408dd3dd04,
+ 0xae0b158b4738705e,0x9624ab50b148d445,
+ 0xd98ddaee19068c76,0x3badd624dd9b0957,
+ 0x87f8a8d4cfa417c9,0xe54ca5d70a80e5d6,
+ 0xa9f6d30a038d1dbc,0x5e9fcf4ccd211f4c,
+ 0xd47487cc8470652b,0x7647c3200069671f,
+ 0x84c8d4dfd2c63f3b,0x29ecd9f40041e073,
+ 0xa5fb0a17c777cf09,0xf468107100525890,
+ 0xcf79cc9db955c2cc,0x7182148d4066eeb4,
+ 0x81ac1fe293d599bf,0xc6f14cd848405530,
+ 0xa21727db38cb002f,0xb8ada00e5a506a7c,
+ 0xca9cf1d206fdc03b,0xa6d90811f0e4851c,
+ 0xfd442e4688bd304a,0x908f4a166d1da663,
+ 0x9e4a9cec15763e2e,0x9a598e4e043287fe,
+ 0xc5dd44271ad3cdba,0x40eff1e1853f29fd,
+ 0xf7549530e188c128,0xd12bee59e68ef47c,
+ 0x9a94dd3e8cf578b9,0x82bb74f8301958ce,
+ 0xc13a148e3032d6e7,0xe36a52363c1faf01,
+ 0xf18899b1bc3f8ca1,0xdc44e6c3cb279ac1,
+ 0x96f5600f15a7b7e5,0x29ab103a5ef8c0b9,
+ 0xbcb2b812db11a5de,0x7415d448f6b6f0e7,
+ 0xebdf661791d60f56,0x111b495b3464ad21,
+ 0x936b9fcebb25c995,0xcab10dd900beec34,
+ 0xb84687c269ef3bfb,0x3d5d514f40eea742,
+ 0xe65829b3046b0afa,0xcb4a5a3112a5112,
+ 0x8ff71a0fe2c2e6dc,0x47f0e785eaba72ab,
+ 0xb3f4e093db73a093,0x59ed216765690f56,
+ 0xe0f218b8d25088b8,0x306869c13ec3532c,
+ 0x8c974f7383725573,0x1e414218c73a13fb,
+ 0xafbd2350644eeacf,0xe5d1929ef90898fa,
+ 0xdbac6c247d62a583,0xdf45f746b74abf39,
+ 0x894bc396ce5da772,0x6b8bba8c328eb783,
+ 0xab9eb47c81f5114f,0x66ea92f3f326564,
+ 0xd686619ba27255a2,0xc80a537b0efefebd,
+ 0x8613fd0145877585,0xbd06742ce95f5f36,
+ 0xa798fc4196e952e7,0x2c48113823b73704,
+ 0xd17f3b51fca3a7a0,0xf75a15862ca504c5,
+ 0x82ef85133de648c4,0x9a984d73dbe722fb,
+ 0xa3ab66580d5fdaf5,0xc13e60d0d2e0ebba,
+ 0xcc963fee10b7d1b3,0x318df905079926a8,
+ 0xffbbcfe994e5c61f,0xfdf17746497f7052,
+ 0x9fd561f1fd0f9bd3,0xfeb6ea8bedefa633,
+ 0xc7caba6e7c5382c8,0xfe64a52ee96b8fc0,
+ 0xf9bd690a1b68637b,0x3dfdce7aa3c673b0,
+ 0x9c1661a651213e2d,0x6bea10ca65c084e,
+ 0xc31bfa0fe5698db8,0x486e494fcff30a62,
+ 0xf3e2f893dec3f126,0x5a89dba3c3efccfa,
+ 0x986ddb5c6b3a76b7,0xf89629465a75e01c,
+ 0xbe89523386091465,0xf6bbb397f1135823,
+ 0xee2ba6c0678b597f,0x746aa07ded582e2c,
+ 0x94db483840b717ef,0xa8c2a44eb4571cdc,
+ 0xba121a4650e4ddeb,0x92f34d62616ce413,
+ 0xe896a0d7e51e1566,0x77b020baf9c81d17,
+ 0x915e2486ef32cd60,0xace1474dc1d122e,
+ 0xb5b5ada8aaff80b8,0xd819992132456ba,
+ 0xe3231912d5bf60e6,0x10e1fff697ed6c69,
+ 0x8df5efabc5979c8f,0xca8d3ffa1ef463c1,
+ 0xb1736b96b6fd83b3,0xbd308ff8a6b17cb2,
+ 0xddd0467c64bce4a0,0xac7cb3f6d05ddbde,
+ 0x8aa22c0dbef60ee4,0x6bcdf07a423aa96b,
+ 0xad4ab7112eb3929d,0x86c16c98d2c953c6,
+ 0xd89d64d57a607744,0xe871c7bf077ba8b7,
+ 0x87625f056c7c4a8b,0x11471cd764ad4972,
+ 0xa93af6c6c79b5d2d,0xd598e40d3dd89bcf,
+ 0xd389b47879823479,0x4aff1d108d4ec2c3,
+ 0x843610cb4bf160cb,0xcedf722a585139ba,
+ 0xa54394fe1eedb8fe,0xc2974eb4ee658828,
+ 0xce947a3da6a9273e,0x733d226229feea32,
+ 0x811ccc668829b887,0x806357d5a3f525f,
+ 0xa163ff802a3426a8,0xca07c2dcb0cf26f7,
+ 0xc9bcff6034c13052,0xfc89b393dd02f0b5,
+ 0xfc2c3f3841f17c67,0xbbac2078d443ace2,
+ 0x9d9ba7832936edc0,0xd54b944b84aa4c0d,
+ 0xc5029163f384a931,0xa9e795e65d4df11,
+ 0xf64335bcf065d37d,0x4d4617b5ff4a16d5,
+ 0x99ea0196163fa42e,0x504bced1bf8e4e45,
+ 0xc06481fb9bcf8d39,0xe45ec2862f71e1d6,
+ 0xf07da27a82c37088,0x5d767327bb4e5a4c,
+ 0x964e858c91ba2655,0x3a6a07f8d510f86f,
+ 0xbbe226efb628afea,0x890489f70a55368b,
+ 0xeadab0aba3b2dbe5,0x2b45ac74ccea842e,
+ 0x92c8ae6b464fc96f,0x3b0b8bc90012929d,
+ 0xb77ada0617e3bbcb,0x9ce6ebb40173744,
+ 0xe55990879ddcaabd,0xcc420a6a101d0515,
+ 0x8f57fa54c2a9eab6,0x9fa946824a12232d,
+ 0xb32df8e9f3546564,0x47939822dc96abf9,
+ 0xdff9772470297ebd,0x59787e2b93bc56f7,
+ 0x8bfbea76c619ef36,0x57eb4edb3c55b65a,
+ 0xaefae51477a06b03,0xede622920b6b23f1,
+ 0xdab99e59958885c4,0xe95fab368e45eced,
+ 0x88b402f7fd75539b,0x11dbcb0218ebb414,
+ 0xaae103b5fcd2a881,0xd652bdc29f26a119,
+ 0xd59944a37c0752a2,0x4be76d3346f0495f,
+ 0x857fcae62d8493a5,0x6f70a4400c562ddb,
+ 0xa6dfbd9fb8e5b88e,0xcb4ccd500f6bb952,
+ 0xd097ad07a71f26b2,0x7e2000a41346a7a7,
+ 0x825ecc24c873782f,0x8ed400668c0c28c8,
+ 0xa2f67f2dfa90563b,0x728900802f0f32fa,
+ 0xcbb41ef979346bca,0x4f2b40a03ad2ffb9,
+ 0xfea126b7d78186bc,0xe2f610c84987bfa8,
+ 0x9f24b832e6b0f436,0xdd9ca7d2df4d7c9,
+ 0xc6ede63fa05d3143,0x91503d1c79720dbb,
+ 0xf8a95fcf88747d94,0x75a44c6397ce912a,
+ 0x9b69dbe1b548ce7c,0xc986afbe3ee11aba,
+ 0xc24452da229b021b,0xfbe85badce996168,
+ 0xf2d56790ab41c2a2,0xfae27299423fb9c3,
+ 0x97c560ba6b0919a5,0xdccd879fc967d41a,
+ 0xbdb6b8e905cb600f,0x5400e987bbc1c920,
+ 0xed246723473e3813,0x290123e9aab23b68,
+ 0x9436c0760c86e30b,0xf9a0b6720aaf6521,
+ 0xb94470938fa89bce,0xf808e40e8d5b3e69,
+ 0xe7958cb87392c2c2,0xb60b1d1230b20e04,
+ 0x90bd77f3483bb9b9,0xb1c6f22b5e6f48c2,
+ 0xb4ecd5f01a4aa828,0x1e38aeb6360b1af3,
+ 0xe2280b6c20dd5232,0x25c6da63c38de1b0,
+ 0x8d590723948a535f,0x579c487e5a38ad0e,
+ 0xb0af48ec79ace837,0x2d835a9df0c6d851,
+ 0xdcdb1b2798182244,0xf8e431456cf88e65,
+ 0x8a08f0f8bf0f156b,0x1b8e9ecb641b58ff,
+ 0xac8b2d36eed2dac5,0xe272467e3d222f3f,
+ 0xd7adf884aa879177,0x5b0ed81dcc6abb0f,
+ 0x86ccbb52ea94baea,0x98e947129fc2b4e9,
+ 0xa87fea27a539e9a5,0x3f2398d747b36224,
+ 0xd29fe4b18e88640e,0x8eec7f0d19a03aad,
+ 0x83a3eeeef9153e89,0x1953cf68300424ac,
+ 0xa48ceaaab75a8e2b,0x5fa8c3423c052dd7,
+ 0xcdb02555653131b6,0x3792f412cb06794d,
+ 0x808e17555f3ebf11,0xe2bbd88bbee40bd0,
+ 0xa0b19d2ab70e6ed6,0x5b6aceaeae9d0ec4,
+ 0xc8de047564d20a8b,0xf245825a5a445275,
+ 0xfb158592be068d2e,0xeed6e2f0f0d56712,
+ 0x9ced737bb6c4183d,0x55464dd69685606b,
+ 0xc428d05aa4751e4c,0xaa97e14c3c26b886,
+ 0xf53304714d9265df,0xd53dd99f4b3066a8,
+ 0x993fe2c6d07b7fab,0xe546a8038efe4029,
+ 0xbf8fdb78849a5f96,0xde98520472bdd033,
+ 0xef73d256a5c0f77c,0x963e66858f6d4440,
+ 0x95a8637627989aad,0xdde7001379a44aa8,
+ 0xbb127c53b17ec159,0x5560c018580d5d52,
+ 0xe9d71b689dde71af,0xaab8f01e6e10b4a6,
+ 0x9226712162ab070d,0xcab3961304ca70e8,
+ 0xb6b00d69bb55c8d1,0x3d607b97c5fd0d22,
+ 0xe45c10c42a2b3b05,0x8cb89a7db77c506a,
+ 0x8eb98a7a9a5b04e3,0x77f3608e92adb242,
+ 0xb267ed1940f1c61c,0x55f038b237591ed3,
+ 0xdf01e85f912e37a3,0x6b6c46dec52f6688,
+ 0x8b61313bbabce2c6,0x2323ac4b3b3da015,
+ 0xae397d8aa96c1b77,0xabec975e0a0d081a,
+ 0xd9c7dced53c72255,0x96e7bd358c904a21,
+ 0x881cea14545c7575,0x7e50d64177da2e54,
+ 0xaa242499697392d2,0xdde50bd1d5d0b9e9,
+ 0xd4ad2dbfc3d07787,0x955e4ec64b44e864,
+ 0x84ec3c97da624ab4,0xbd5af13bef0b113e,
+ 0xa6274bbdd0fadd61,0xecb1ad8aeacdd58e,
+ 0xcfb11ead453994ba,0x67de18eda5814af2,
+ 0x81ceb32c4b43fcf4,0x80eacf948770ced7,
+ 0xa2425ff75e14fc31,0xa1258379a94d028d,
+ 0xcad2f7f5359a3b3e,0x96ee45813a04330,
+ 0xfd87b5f28300ca0d,0x8bca9d6e188853fc,
+ 0x9e74d1b791e07e48,0x775ea264cf55347e,
+ 0xc612062576589dda,0x95364afe032a819e,
+ 0xf79687aed3eec551,0x3a83ddbd83f52205,
+ 0x9abe14cd44753b52,0xc4926a9672793543,
+ 0xc16d9a0095928a27,0x75b7053c0f178294,
+ 0xf1c90080baf72cb1,0x5324c68b12dd6339,
+ 0x971da05074da7bee,0xd3f6fc16ebca5e04,
+ 0xbce5086492111aea,0x88f4bb1ca6bcf585,
+ 0xec1e4a7db69561a5,0x2b31e9e3d06c32e6,
+ 0x9392ee8e921d5d07,0x3aff322e62439fd0,
+ 0xb877aa3236a4b449,0x9befeb9fad487c3,
+ 0xe69594bec44de15b,0x4c2ebe687989a9b4,
+ 0x901d7cf73ab0acd9,0xf9d37014bf60a11,
+ 0xb424dc35095cd80f,0x538484c19ef38c95,
+ 0xe12e13424bb40e13,0x2865a5f206b06fba,
+ 0x8cbccc096f5088cb,0xf93f87b7442e45d4,
+ 0xafebff0bcb24aafe,0xf78f69a51539d749,
+ 0xdbe6fecebdedd5be,0xb573440e5a884d1c,
+ 0x89705f4136b4a597,0x31680a88f8953031,
+ 0xabcc77118461cefc,0xfdc20d2b36ba7c3e,
+ 0xd6bf94d5e57a42bc,0x3d32907604691b4d,
+ 0x8637bd05af6c69b5,0xa63f9a49c2c1b110,
+ 0xa7c5ac471b478423,0xfcf80dc33721d54,
+ 0xd1b71758e219652b,0xd3c36113404ea4a9,
+ 0x83126e978d4fdf3b,0x645a1cac083126ea,
+ 0xa3d70a3d70a3d70a,0x3d70a3d70a3d70a4,
+ 0xcccccccccccccccc,0xcccccccccccccccd,
+ 0x8000000000000000,0x0,
+ 0xa000000000000000,0x0,
+ 0xc800000000000000,0x0,
+ 0xfa00000000000000,0x0,
+ 0x9c40000000000000,0x0,
+ 0xc350000000000000,0x0,
+ 0xf424000000000000,0x0,
+ 0x9896800000000000,0x0,
+ 0xbebc200000000000,0x0,
+ 0xee6b280000000000,0x0,
+ 0x9502f90000000000,0x0,
+ 0xba43b74000000000,0x0,
+ 0xe8d4a51000000000,0x0,
+ 0x9184e72a00000000,0x0,
+ 0xb5e620f480000000,0x0,
+ 0xe35fa931a0000000,0x0,
+ 0x8e1bc9bf04000000,0x0,
+ 0xb1a2bc2ec5000000,0x0,
+ 0xde0b6b3a76400000,0x0,
+ 0x8ac7230489e80000,0x0,
+ 0xad78ebc5ac620000,0x0,
+ 0xd8d726b7177a8000,0x0,
+ 0x878678326eac9000,0x0,
+ 0xa968163f0a57b400,0x0,
+ 0xd3c21bcecceda100,0x0,
+ 0x84595161401484a0,0x0,
+ 0xa56fa5b99019a5c8,0x0,
+ 0xcecb8f27f4200f3a,0x0,
+ 0x813f3978f8940984,0x4000000000000000,
+ 0xa18f07d736b90be5,0x5000000000000000,
+ 0xc9f2c9cd04674ede,0xa400000000000000,
+ 0xfc6f7c4045812296,0x4d00000000000000,
+ 0x9dc5ada82b70b59d,0xf020000000000000,
+ 0xc5371912364ce305,0x6c28000000000000,
+ 0xf684df56c3e01bc6,0xc732000000000000,
+ 0x9a130b963a6c115c,0x3c7f400000000000,
+ 0xc097ce7bc90715b3,0x4b9f100000000000,
+ 0xf0bdc21abb48db20,0x1e86d40000000000,
+ 0x96769950b50d88f4,0x1314448000000000,
+ 0xbc143fa4e250eb31,0x17d955a000000000,
+ 0xeb194f8e1ae525fd,0x5dcfab0800000000,
+ 0x92efd1b8d0cf37be,0x5aa1cae500000000,
+ 0xb7abc627050305ad,0xf14a3d9e40000000,
+ 0xe596b7b0c643c719,0x6d9ccd05d0000000,
+ 0x8f7e32ce7bea5c6f,0xe4820023a2000000,
+ 0xb35dbf821ae4f38b,0xdda2802c8a800000,
+ 0xe0352f62a19e306e,0xd50b2037ad200000,
+ 0x8c213d9da502de45,0x4526f422cc340000,
+ 0xaf298d050e4395d6,0x9670b12b7f410000,
+ 0xdaf3f04651d47b4c,0x3c0cdd765f114000,
+ 0x88d8762bf324cd0f,0xa5880a69fb6ac800,
+ 0xab0e93b6efee0053,0x8eea0d047a457a00,
+ 0xd5d238a4abe98068,0x72a4904598d6d880,
+ 0x85a36366eb71f041,0x47a6da2b7f864750,
+ 0xa70c3c40a64e6c51,0x999090b65f67d924,
+ 0xd0cf4b50cfe20765,0xfff4b4e3f741cf6d,
+ 0x82818f1281ed449f,0xbff8f10e7a8921a4,
+ 0xa321f2d7226895c7,0xaff72d52192b6a0d,
+ 0xcbea6f8ceb02bb39,0x9bf4f8a69f764490,
+ 0xfee50b7025c36a08,0x2f236d04753d5b4,
+ 0x9f4f2726179a2245,0x1d762422c946590,
+ 0xc722f0ef9d80aad6,0x424d3ad2b7b97ef5,
+ 0xf8ebad2b84e0d58b,0xd2e0898765a7deb2,
+ 0x9b934c3b330c8577,0x63cc55f49f88eb2f,
+ 0xc2781f49ffcfa6d5,0x3cbf6b71c76b25fb,
+ 0xf316271c7fc3908a,0x8bef464e3945ef7a,
+ 0x97edd871cfda3a56,0x97758bf0e3cbb5ac,
+ 0xbde94e8e43d0c8ec,0x3d52eeed1cbea317,
+ 0xed63a231d4c4fb27,0x4ca7aaa863ee4bdd,
+ 0x945e455f24fb1cf8,0x8fe8caa93e74ef6a,
+ 0xb975d6b6ee39e436,0xb3e2fd538e122b44,
+ 0xe7d34c64a9c85d44,0x60dbbca87196b616,
+ 0x90e40fbeea1d3a4a,0xbc8955e946fe31cd,
+ 0xb51d13aea4a488dd,0x6babab6398bdbe41,
+ 0xe264589a4dcdab14,0xc696963c7eed2dd1,
+ 0x8d7eb76070a08aec,0xfc1e1de5cf543ca2,
+ 0xb0de65388cc8ada8,0x3b25a55f43294bcb,
+ 0xdd15fe86affad912,0x49ef0eb713f39ebe,
+ 0x8a2dbf142dfcc7ab,0x6e3569326c784337,
+ 0xacb92ed9397bf996,0x49c2c37f07965404,
+ 0xd7e77a8f87daf7fb,0xdc33745ec97be906,
+ 0x86f0ac99b4e8dafd,0x69a028bb3ded71a3,
+ 0xa8acd7c0222311bc,0xc40832ea0d68ce0c,
+ 0xd2d80db02aabd62b,0xf50a3fa490c30190,
+ 0x83c7088e1aab65db,0x792667c6da79e0fa,
+ 0xa4b8cab1a1563f52,0x577001b891185938,
+ 0xcde6fd5e09abcf26,0xed4c0226b55e6f86,
+ 0x80b05e5ac60b6178,0x544f8158315b05b4,
+ 0xa0dc75f1778e39d6,0x696361ae3db1c721,
+ 0xc913936dd571c84c,0x3bc3a19cd1e38e9,
+ 0xfb5878494ace3a5f,0x4ab48a04065c723,
+ 0x9d174b2dcec0e47b,0x62eb0d64283f9c76,
+ 0xc45d1df942711d9a,0x3ba5d0bd324f8394,
+ 0xf5746577930d6500,0xca8f44ec7ee36479,
+ 0x9968bf6abbe85f20,0x7e998b13cf4e1ecb,
+ 0xbfc2ef456ae276e8,0x9e3fedd8c321a67e,
+ 0xefb3ab16c59b14a2,0xc5cfe94ef3ea101e,
+ 0x95d04aee3b80ece5,0xbba1f1d158724a12,
+ 0xbb445da9ca61281f,0x2a8a6e45ae8edc97,
+ 0xea1575143cf97226,0xf52d09d71a3293bd,
+ 0x924d692ca61be758,0x593c2626705f9c56,
+ 0xb6e0c377cfa2e12e,0x6f8b2fb00c77836c,
+ 0xe498f455c38b997a,0xb6dfb9c0f956447,
+ 0x8edf98b59a373fec,0x4724bd4189bd5eac,
+ 0xb2977ee300c50fe7,0x58edec91ec2cb657,
+ 0xdf3d5e9bc0f653e1,0x2f2967b66737e3ed,
+ 0x8b865b215899f46c,0xbd79e0d20082ee74,
+ 0xae67f1e9aec07187,0xecd8590680a3aa11,
+ 0xda01ee641a708de9,0xe80e6f4820cc9495,
+ 0x884134fe908658b2,0x3109058d147fdcdd,
+ 0xaa51823e34a7eede,0xbd4b46f0599fd415,
+ 0xd4e5e2cdc1d1ea96,0x6c9e18ac7007c91a,
+ 0x850fadc09923329e,0x3e2cf6bc604ddb0,
+ 0xa6539930bf6bff45,0x84db8346b786151c,
+ 0xcfe87f7cef46ff16,0xe612641865679a63,
+ 0x81f14fae158c5f6e,0x4fcb7e8f3f60c07e,
+ 0xa26da3999aef7749,0xe3be5e330f38f09d,
+ 0xcb090c8001ab551c,0x5cadf5bfd3072cc5,
+ 0xfdcb4fa002162a63,0x73d9732fc7c8f7f6,
+ 0x9e9f11c4014dda7e,0x2867e7fddcdd9afa,
+ 0xc646d63501a1511d,0xb281e1fd541501b8,
+ 0xf7d88bc24209a565,0x1f225a7ca91a4226,
+ 0x9ae757596946075f,0x3375788de9b06958,
+ 0xc1a12d2fc3978937,0x52d6b1641c83ae,
+ 0xf209787bb47d6b84,0xc0678c5dbd23a49a,
+ 0x9745eb4d50ce6332,0xf840b7ba963646e0,
+ 0xbd176620a501fbff,0xb650e5a93bc3d898,
+ 0xec5d3fa8ce427aff,0xa3e51f138ab4cebe,
+ 0x93ba47c980e98cdf,0xc66f336c36b10137,
+ 0xb8a8d9bbe123f017,0xb80b0047445d4184,
+ 0xe6d3102ad96cec1d,0xa60dc059157491e5,
+ 0x9043ea1ac7e41392,0x87c89837ad68db2f,
+ 0xb454e4a179dd1877,0x29babe4598c311fb,
+ 0xe16a1dc9d8545e94,0xf4296dd6fef3d67a,
+ 0x8ce2529e2734bb1d,0x1899e4a65f58660c,
+ 0xb01ae745b101e9e4,0x5ec05dcff72e7f8f,
+ 0xdc21a1171d42645d,0x76707543f4fa1f73,
+ 0x899504ae72497eba,0x6a06494a791c53a8,
+ 0xabfa45da0edbde69,0x487db9d17636892,
+ 0xd6f8d7509292d603,0x45a9d2845d3c42b6,
+ 0x865b86925b9bc5c2,0xb8a2392ba45a9b2,
+ 0xa7f26836f282b732,0x8e6cac7768d7141e,
+ 0xd1ef0244af2364ff,0x3207d795430cd926,
+ 0x8335616aed761f1f,0x7f44e6bd49e807b8,
+ 0xa402b9c5a8d3a6e7,0x5f16206c9c6209a6,
+ 0xcd036837130890a1,0x36dba887c37a8c0f,
+ 0x802221226be55a64,0xc2494954da2c9789,
+ 0xa02aa96b06deb0fd,0xf2db9baa10b7bd6c,
+ 0xc83553c5c8965d3d,0x6f92829494e5acc7,
+ 0xfa42a8b73abbf48c,0xcb772339ba1f17f9,
+ 0x9c69a97284b578d7,0xff2a760414536efb,
+ 0xc38413cf25e2d70d,0xfef5138519684aba,
+ 0xf46518c2ef5b8cd1,0x7eb258665fc25d69,
+ 0x98bf2f79d5993802,0xef2f773ffbd97a61,
+ 0xbeeefb584aff8603,0xaafb550ffacfd8fa,
+ 0xeeaaba2e5dbf6784,0x95ba2a53f983cf38,
+ 0x952ab45cfa97a0b2,0xdd945a747bf26183,
+ 0xba756174393d88df,0x94f971119aeef9e4,
+ 0xe912b9d1478ceb17,0x7a37cd5601aab85d,
+ 0x91abb422ccb812ee,0xac62e055c10ab33a,
+ 0xb616a12b7fe617aa,0x577b986b314d6009,
+ 0xe39c49765fdf9d94,0xed5a7e85fda0b80b,
+ 0x8e41ade9fbebc27d,0x14588f13be847307,
+ 0xb1d219647ae6b31c,0x596eb2d8ae258fc8,
+ 0xde469fbd99a05fe3,0x6fca5f8ed9aef3bb,
+ 0x8aec23d680043bee,0x25de7bb9480d5854,
+ 0xada72ccc20054ae9,0xaf561aa79a10ae6a,
+ 0xd910f7ff28069da4,0x1b2ba1518094da04,
+ 0x87aa9aff79042286,0x90fb44d2f05d0842,
+ 0xa99541bf57452b28,0x353a1607ac744a53,
+ 0xd3fa922f2d1675f2,0x42889b8997915ce8,
+ 0x847c9b5d7c2e09b7,0x69956135febada11,
+ 0xa59bc234db398c25,0x43fab9837e699095,
+ 0xcf02b2c21207ef2e,0x94f967e45e03f4bb,
+ 0x8161afb94b44f57d,0x1d1be0eebac278f5,
+ 0xa1ba1ba79e1632dc,0x6462d92a69731732,
+ 0xca28a291859bbf93,0x7d7b8f7503cfdcfe,
+ 0xfcb2cb35e702af78,0x5cda735244c3d43e,
+ 0x9defbf01b061adab,0x3a0888136afa64a7,
+ 0xc56baec21c7a1916,0x88aaa1845b8fdd0,
+ 0xf6c69a72a3989f5b,0x8aad549e57273d45,
+ 0x9a3c2087a63f6399,0x36ac54e2f678864b,
+ 0xc0cb28a98fcf3c7f,0x84576a1bb416a7dd,
+ 0xf0fdf2d3f3c30b9f,0x656d44a2a11c51d5,
+ 0x969eb7c47859e743,0x9f644ae5a4b1b325,
+ 0xbc4665b596706114,0x873d5d9f0dde1fee,
+ 0xeb57ff22fc0c7959,0xa90cb506d155a7ea,
+ 0x9316ff75dd87cbd8,0x9a7f12442d588f2,
+ 0xb7dcbf5354e9bece,0xc11ed6d538aeb2f,
+ 0xe5d3ef282a242e81,0x8f1668c8a86da5fa,
+ 0x8fa475791a569d10,0xf96e017d694487bc,
+ 0xb38d92d760ec4455,0x37c981dcc395a9ac,
+ 0xe070f78d3927556a,0x85bbe253f47b1417,
+ 0x8c469ab843b89562,0x93956d7478ccec8e,
+ 0xaf58416654a6babb,0x387ac8d1970027b2,
+ 0xdb2e51bfe9d0696a,0x6997b05fcc0319e,
+ 0x88fcf317f22241e2,0x441fece3bdf81f03,
+ 0xab3c2fddeeaad25a,0xd527e81cad7626c3,
+ 0xd60b3bd56a5586f1,0x8a71e223d8d3b074,
+ 0x85c7056562757456,0xf6872d5667844e49,
+ 0xa738c6bebb12d16c,0xb428f8ac016561db,
+ 0xd106f86e69d785c7,0xe13336d701beba52,
+ 0x82a45b450226b39c,0xecc0024661173473,
+ 0xa34d721642b06084,0x27f002d7f95d0190,
+ 0xcc20ce9bd35c78a5,0x31ec038df7b441f4,
+ 0xff290242c83396ce,0x7e67047175a15271,
+ 0x9f79a169bd203e41,0xf0062c6e984d386,
+ 0xc75809c42c684dd1,0x52c07b78a3e60868,
+ 0xf92e0c3537826145,0xa7709a56ccdf8a82,
+ 0x9bbcc7a142b17ccb,0x88a66076400bb691,
+ 0xc2abf989935ddbfe,0x6acff893d00ea435,
+ 0xf356f7ebf83552fe,0x583f6b8c4124d43,
+ 0x98165af37b2153de,0xc3727a337a8b704a,
+ 0xbe1bf1b059e9a8d6,0x744f18c0592e4c5c,
+ 0xeda2ee1c7064130c,0x1162def06f79df73,
+ 0x9485d4d1c63e8be7,0x8addcb5645ac2ba8,
+ 0xb9a74a0637ce2ee1,0x6d953e2bd7173692,
+ 0xe8111c87c5c1ba99,0xc8fa8db6ccdd0437,
+ 0x910ab1d4db9914a0,0x1d9c9892400a22a2,
+ 0xb54d5e4a127f59c8,0x2503beb6d00cab4b,
+ 0xe2a0b5dc971f303a,0x2e44ae64840fd61d,
+ 0x8da471a9de737e24,0x5ceaecfed289e5d2,
+ 0xb10d8e1456105dad,0x7425a83e872c5f47,
+ 0xdd50f1996b947518,0xd12f124e28f77719,
+ 0x8a5296ffe33cc92f,0x82bd6b70d99aaa6f,
+ 0xace73cbfdc0bfb7b,0x636cc64d1001550b,
+ 0xd8210befd30efa5a,0x3c47f7e05401aa4e,
+ 0x8714a775e3e95c78,0x65acfaec34810a71,
+ 0xa8d9d1535ce3b396,0x7f1839a741a14d0d,
+ 0xd31045a8341ca07c,0x1ede48111209a050,
+ 0x83ea2b892091e44d,0x934aed0aab460432,
+ 0xa4e4b66b68b65d60,0xf81da84d5617853f,
+ 0xce1de40642e3f4b9,0x36251260ab9d668e,
+ 0x80d2ae83e9ce78f3,0xc1d72b7c6b426019,
+ 0xa1075a24e4421730,0xb24cf65b8612f81f,
+ 0xc94930ae1d529cfc,0xdee033f26797b627,
+ 0xfb9b7cd9a4a7443c,0x169840ef017da3b1,
+ 0x9d412e0806e88aa5,0x8e1f289560ee864e,
+ 0xc491798a08a2ad4e,0xf1a6f2bab92a27e2,
+ 0xf5b5d7ec8acb58a2,0xae10af696774b1db,
+ 0x9991a6f3d6bf1765,0xacca6da1e0a8ef29,
+ 0xbff610b0cc6edd3f,0x17fd090a58d32af3,
+ 0xeff394dcff8a948e,0xddfc4b4cef07f5b0,
+ 0x95f83d0a1fb69cd9,0x4abdaf101564f98e,
+ 0xbb764c4ca7a4440f,0x9d6d1ad41abe37f1,
+ 0xea53df5fd18d5513,0x84c86189216dc5ed,
+ 0x92746b9be2f8552c,0x32fd3cf5b4e49bb4,
+ 0xb7118682dbb66a77,0x3fbc8c33221dc2a1,
+ 0xe4d5e82392a40515,0xfabaf3feaa5334a,
+ 0x8f05b1163ba6832d,0x29cb4d87f2a7400e,
+ 0xb2c71d5bca9023f8,0x743e20e9ef511012,
+ 0xdf78e4b2bd342cf6,0x914da9246b255416,
+ 0x8bab8eefb6409c1a,0x1ad089b6c2f7548e,
+ 0xae9672aba3d0c320,0xa184ac2473b529b1,
+ 0xda3c0f568cc4f3e8,0xc9e5d72d90a2741e,
+ 0x8865899617fb1871,0x7e2fa67c7a658892,
+ 0xaa7eebfb9df9de8d,0xddbb901b98feeab7,
+ 0xd51ea6fa85785631,0x552a74227f3ea565,
+ 0x8533285c936b35de,0xd53a88958f87275f,
+ 0xa67ff273b8460356,0x8a892abaf368f137,
+ 0xd01fef10a657842c,0x2d2b7569b0432d85,
+ 0x8213f56a67f6b29b,0x9c3b29620e29fc73,
+ 0xa298f2c501f45f42,0x8349f3ba91b47b8f,
+ 0xcb3f2f7642717713,0x241c70a936219a73,
+ 0xfe0efb53d30dd4d7,0xed238cd383aa0110,
+ 0x9ec95d1463e8a506,0xf4363804324a40aa,
+ 0xc67bb4597ce2ce48,0xb143c6053edcd0d5,
+ 0xf81aa16fdc1b81da,0xdd94b7868e94050a,
+ 0x9b10a4e5e9913128,0xca7cf2b4191c8326,
+ 0xc1d4ce1f63f57d72,0xfd1c2f611f63a3f0,
+ 0xf24a01a73cf2dccf,0xbc633b39673c8cec,
+ 0x976e41088617ca01,0xd5be0503e085d813,
+ 0xbd49d14aa79dbc82,0x4b2d8644d8a74e18,
+ 0xec9c459d51852ba2,0xddf8e7d60ed1219e,
+ 0x93e1ab8252f33b45,0xcabb90e5c942b503,
+ 0xb8da1662e7b00a17,0x3d6a751f3b936243,
+ 0xe7109bfba19c0c9d,0xcc512670a783ad4,
+ 0x906a617d450187e2,0x27fb2b80668b24c5,
+ 0xb484f9dc9641e9da,0xb1f9f660802dedf6,
+ 0xe1a63853bbd26451,0x5e7873f8a0396973,
+ 0x8d07e33455637eb2,0xdb0b487b6423e1e8,
+ 0xb049dc016abc5e5f,0x91ce1a9a3d2cda62,
+ 0xdc5c5301c56b75f7,0x7641a140cc7810fb,
+ 0x89b9b3e11b6329ba,0xa9e904c87fcb0a9d,
+ 0xac2820d9623bf429,0x546345fa9fbdcd44,
+ 0xd732290fbacaf133,0xa97c177947ad4095,
+ 0x867f59a9d4bed6c0,0x49ed8eabcccc485d,
+ 0xa81f301449ee8c70,0x5c68f256bfff5a74,
+ 0xd226fc195c6a2f8c,0x73832eec6fff3111,
+ 0x83585d8fd9c25db7,0xc831fd53c5ff7eab,
+ 0xa42e74f3d032f525,0xba3e7ca8b77f5e55,
+ 0xcd3a1230c43fb26f,0x28ce1bd2e55f35eb,
+ 0x80444b5e7aa7cf85,0x7980d163cf5b81b3,
+ 0xa0555e361951c366,0xd7e105bcc332621f,
+ 0xc86ab5c39fa63440,0x8dd9472bf3fefaa7,
+ 0xfa856334878fc150,0xb14f98f6f0feb951,
+ 0x9c935e00d4b9d8d2,0x6ed1bf9a569f33d3,
+ 0xc3b8358109e84f07,0xa862f80ec4700c8,
+ 0xf4a642e14c6262c8,0xcd27bb612758c0fa,
+ 0x98e7e9cccfbd7dbd,0x8038d51cb897789c,
+ 0xbf21e44003acdd2c,0xe0470a63e6bd56c3,
+ 0xeeea5d5004981478,0x1858ccfce06cac74,
+ 0x95527a5202df0ccb,0xf37801e0c43ebc8,
+ 0xbaa718e68396cffd,0xd30560258f54e6ba,
+ 0xe950df20247c83fd,0x47c6b82ef32a2069,
+ 0x91d28b7416cdd27e,0x4cdc331d57fa5441,
+ 0xb6472e511c81471d,0xe0133fe4adf8e952,
+ 0xe3d8f9e563a198e5,0x58180fddd97723a6,
+ 0x8e679c2f5e44ff8f,0x570f09eaa7ea7648,};
+ using powers = powers_template<>;
+
+}
+
+#endif
+
+#ifndef FASTFLOAT_DECIMAL_TO_BINARY_H
+#define FASTFLOAT_DECIMAL_TO_BINARY_H
+
+#include <cfloat>
+#include <cinttypes>
+#include <cmath>
+#include <cstdint>
+#include <cstdlib>
+#include <cstring>
+
+namespace fast_float {
+
+ // This will compute or rather approximate w * 5**q and return a pair of 64-bit words approximating
+ // the result, with the "high" part corresponding to the most significant bits and the
+ // low part corresponding to the least significant bits.
+ //
+ template <int bit_precision>
+ fastfloat_really_inline
+ value128 compute_product_approximation(int64_t q, uint64_t w) {
+ const int index = 2 * int(q - powers::smallest_power_of_five);
+ // For small values of q, e.g., q in [0,27], the answer is always exact because
+ // The line value128 firstproduct = full_multiplication(w, power_of_five_128[index]);
+ // gives the exact answer.
+ value128 firstproduct = full_multiplication(w, powers::power_of_five_128[index]);
+ static_assert((bit_precision >= 0) && (bit_precision <= 64), " precision should be in (0,64]");
+ constexpr uint64_t precision_mask = (bit_precision < 64) ?
+ (uint64_t(0xFFFFFFFFFFFFFFFF) >> bit_precision)
+ : uint64_t(0xFFFFFFFFFFFFFFFF);
+ if((firstproduct.high & precision_mask) == precision_mask) { // could further guard with (lower + w < lower)
+ // regarding the second product, we only need secondproduct.high, but our expectation is that the compiler will optimize this extra work away if needed.
+ value128 secondproduct = full_multiplication(w, powers::power_of_five_128[index + 1]);
+ firstproduct.low += secondproduct.high;
+ if(secondproduct.high > firstproduct.low) {
+ firstproduct.high++;
+ }
+ }
+ return firstproduct;
+ }
+
+ namespace detail {
+ /**
+ * For q in (0,350), we have that
+ * f = (((152170 + 65536) * q ) >> 16);
+ * is equal to
+ * floor(p) + q
+ * where
+ * p = log(5**q)/log(2) = q * log(5)/log(2)
+ *
+ * For negative values of q in (-400,0), we have that
+ * f = (((152170 + 65536) * q ) >> 16);
+ * is equal to
+ * -ceil(p) + q
+ * where
+ * p = log(5**-q)/log(2) = -q * log(5)/log(2)
+ */
+ constexpr fastfloat_really_inline int32_t power(int32_t q) noexcept {
+ return (((152170 + 65536) * q) >> 16) + 63;
+ }
+ } // namespace detail
+
+ // create an adjusted mantissa, biased by the invalid power2
+ // for significant digits already multiplied by 10 ** q.
+ template <typename binary>
+ fastfloat_really_inline
+ adjusted_mantissa compute_error_scaled(int64_t q, uint64_t w, int lz) noexcept {
+ int hilz = int(w >> 63) ^ 1;
+ adjusted_mantissa answer;
+ answer.mantissa = w << hilz;
+ int bias = binary::mantissa_explicit_bits() - binary::minimum_exponent();
+ answer.power2 = int32_t(detail::power(int32_t(q)) + bias - hilz - lz - 62 + invalid_am_bias);
+ return answer;
+ }
+
+ // w * 10 ** q, without rounding the representation up.
+ // the power2 in the exponent will be adjusted by invalid_am_bias.
+ template <typename binary>
+ fastfloat_really_inline
+ adjusted_mantissa compute_error(int64_t q, uint64_t w) noexcept {
+ int lz = leading_zeroes(w);
+ w <<= lz;
+ value128 product = compute_product_approximation<binary::mantissa_explicit_bits() + 3>(q, w);
+ return compute_error_scaled<binary>(q, product.high, lz);
+ }
+
+ // w * 10 ** q
+ // The returned value should be a valid ieee64 number that simply need to be packed.
+ // However, in some very rare cases, the computation will fail. In such cases, we
+ // return an adjusted_mantissa with a negative power of 2: the caller should recompute
+ // in such cases.
+ template <typename binary>
+ fastfloat_really_inline
+ adjusted_mantissa compute_float(int64_t q, uint64_t w) noexcept {
+ adjusted_mantissa answer;
+ if ((w == 0) || (q < binary::smallest_power_of_ten())) {
+ answer.power2 = 0;
+ answer.mantissa = 0;
+ // result should be zero
+ return answer;
+ }
+ if (q > binary::largest_power_of_ten()) {
+ // we want to get infinity:
+ answer.power2 = binary::infinite_power();
+ answer.mantissa = 0;
+ return answer;
+ }
+ // At this point in time q is in [powers::smallest_power_of_five, powers::largest_power_of_five].
+
+ // We want the most significant bit of i to be 1. Shift if needed.
+ int lz = leading_zeroes(w);
+ w <<= lz;
+
+ // The required precision is binary::mantissa_explicit_bits() + 3 because
+ // 1. We need the implicit bit
+ // 2. We need an extra bit for rounding purposes
+ // 3. We might lose a bit due to the "upperbit" routine (result too small, requiring a shift)
+
+ value128 product = compute_product_approximation<binary::mantissa_explicit_bits() + 3>(q, w);
+ if(product.low == 0xFFFFFFFFFFFFFFFF) { // could guard it further
+ // In some very rare cases, this could happen, in which case we might need a more accurate
+ // computation that what we can provide cheaply. This is very, very unlikely.
+ //
+ const bool inside_safe_exponent = (q >= -27) && (q <= 55); // always good because 5**q <2**128 when q>=0,
+ // and otherwise, for q<0, we have 5**-q<2**64 and the 128-bit reciprocal allows for exact computation.
+ if(!inside_safe_exponent) {
+ return compute_error_scaled<binary>(q, product.high, lz);
+ }
+ }
+ // The "compute_product_approximation" function can be slightly slower than a branchless approach:
+ // value128 product = compute_product(q, w);
+ // but in practice, we can win big with the compute_product_approximation if its additional branch
+ // is easily predicted. Which is best is data specific.
+ int upperbit = int(product.high >> 63);
+
+ answer.mantissa = product.high >> (upperbit + 64 - binary::mantissa_explicit_bits() - 3);
+
+ answer.power2 = int32_t(detail::power(int32_t(q)) + upperbit - lz - binary::minimum_exponent());
+ if (answer.power2 <= 0) { // we have a subnormal?
+ // Here have that answer.power2 <= 0 so -answer.power2 >= 0
+ if(-answer.power2 + 1 >= 64) { // if we have more than 64 bits below the minimum exponent, you have a zero for sure.
+ answer.power2 = 0;
+ answer.mantissa = 0;
+ // result should be zero
+ return answer;
+ }
+ // next line is safe because -answer.power2 + 1 < 64
+ answer.mantissa >>= -answer.power2 + 1;
+ // Thankfully, we can't have both "round-to-even" and subnormals because
+ // "round-to-even" only occurs for powers close to 0.
+ answer.mantissa += (answer.mantissa & 1); // round up
+ answer.mantissa >>= 1;
+ // There is a weird scenario where we don't have a subnormal but just.
+ // Suppose we start with 2.2250738585072013e-308, we end up
+ // with 0x3fffffffffffff x 2^-1023-53 which is technically subnormal
+ // whereas 0x40000000000000 x 2^-1023-53 is normal. Now, we need to round
+ // up 0x3fffffffffffff x 2^-1023-53 and once we do, we are no longer
+ // subnormal, but we can only know this after rounding.
+ // So we only declare a subnormal if we are smaller than the threshold.
+ answer.power2 = (answer.mantissa < (uint64_t(1) << binary::mantissa_explicit_bits())) ? 0 : 1;
+ return answer;
+ }
+
+ // usually, we round *up*, but if we fall right in between and and we have an
+ // even basis, we need to round down
+ // We are only concerned with the cases where 5**q fits in single 64-bit word.
+ if ((product.low <= 1) && (q >= binary::min_exponent_round_to_even()) && (q <= binary::max_exponent_round_to_even()) &&
+ ((answer.mantissa & 3) == 1) ) { // we may fall between two floats!
+ // To be in-between two floats we need that in doing
+ // answer.mantissa = product.high >> (upperbit + 64 - binary::mantissa_explicit_bits() - 3);
+ // ... we dropped out only zeroes. But if this happened, then we can go back!!!
+ if((answer.mantissa << (upperbit + 64 - binary::mantissa_explicit_bits() - 3)) == product.high) {
+ answer.mantissa &= ~uint64_t(1); // flip it so that we do not round up
+ }
+ }
+
+ answer.mantissa += (answer.mantissa & 1); // round up
+ answer.mantissa >>= 1;
+ if (answer.mantissa >= (uint64_t(2) << binary::mantissa_explicit_bits())) {
+ answer.mantissa = (uint64_t(1) << binary::mantissa_explicit_bits());
+ answer.power2++; // undo previous addition
+ }
+
+ answer.mantissa &= ~(uint64_t(1) << binary::mantissa_explicit_bits());
+ if (answer.power2 >= binary::infinite_power()) { // infinity
+ answer.power2 = binary::infinite_power();
+ answer.mantissa = 0;
+ }
+ return answer;
+ }
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_BIGINT_H
+#define FASTFLOAT_BIGINT_H
+
+#include <algorithm>
+#include <cstdint>
+#include <climits>
+#include <cstring>
+
+
+namespace fast_float {
+
+// the limb width: we want efficient multiplication of double the bits in
+// limb, or for 64-bit limbs, at least 64-bit multiplication where we can
+// extract the high and low parts efficiently. this is every 64-bit
+// architecture except for sparc, which emulates 128-bit multiplication.
+// we might have platforms where `CHAR_BIT` is not 8, so let's avoid
+// doing `8 * sizeof(limb)`.
+#if defined(FASTFLOAT_64BIT) && !defined(__sparc)
+#define FASTFLOAT_64BIT_LIMB
+ typedef uint64_t limb;
+ constexpr size_t limb_bits = 64;
+#else
+ #define FASTFLOAT_32BIT_LIMB
+ typedef uint32_t limb;
+ constexpr size_t limb_bits = 32;
+#endif
+
+ typedef span<limb> limb_span;
+
+ // number of bits in a bigint. this needs to be at least the number
+ // of bits required to store the largest bigint, which is
+ // `log2(10**(digits + max_exp))`, or `log2(10**(767 + 342))`, or
+ // ~3600 bits, so we round to 4000.
+ constexpr size_t bigint_bits = 4000;
+ constexpr size_t bigint_limbs = bigint_bits / limb_bits;
+
+ // vector-like type that is allocated on the stack. the entire
+ // buffer is pre-allocated, and only the length changes.
+ template <uint16_t size>
+ struct stackvec {
+ limb data[size];
+ // we never need more than 150 limbs
+ uint16_t length{0};
+
+ stackvec() = default;
+ stackvec(const stackvec &) = delete;
+ stackvec &operator=(const stackvec &) = delete;
+ stackvec(stackvec &&) = delete;
+ stackvec &operator=(stackvec &&other) = delete;
+
+ // create stack vector from existing limb span.
+ stackvec(limb_span s) {
+ FASTFLOAT_ASSERT(try_extend(s));
+ }
+
+ limb& operator[](size_t index) noexcept {
+ FASTFLOAT_DEBUG_ASSERT(index < length);
+ return data[index];
+ }
+ const limb& operator[](size_t index) const noexcept {
+ FASTFLOAT_DEBUG_ASSERT(index < length);
+ return data[index];
+ }
+ // index from the end of the container
+ const limb& rindex(size_t index) const noexcept {
+ FASTFLOAT_DEBUG_ASSERT(index < length);
+ size_t rindex = length - index - 1;
+ return data[rindex];
+ }
+
+ // set the length, without bounds checking.
+ void set_len(size_t len) noexcept {
+ length = uint16_t(len);
+ }
+ constexpr size_t len() const noexcept {
+ return length;
+ }
+ constexpr bool is_empty() const noexcept {
+ return length == 0;
+ }
+ constexpr size_t capacity() const noexcept {
+ return size;
+ }
+ // append item to vector, without bounds checking
+ void push_unchecked(limb value) noexcept {
+ data[length] = value;
+ length++;
+ }
+ // append item to vector, returning if item was added
+ bool try_push(limb value) noexcept {
+ if (len() < capacity()) {
+ push_unchecked(value);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ // add items to the vector, from a span, without bounds checking
+ void extend_unchecked(limb_span s) noexcept {
+ limb* ptr = data + length;
+ ::memcpy((void*)ptr, (const void*)s.ptr, sizeof(limb) * s.len());
+ set_len(len() + s.len());
+ }
+ // try to add items to the vector, returning if items were added
+ bool try_extend(limb_span s) noexcept {
+ if (len() + s.len() <= capacity()) {
+ extend_unchecked(s);
+ return true;
+ } else {
+ return false;
+ }
+ }
+ // resize the vector, without bounds checking
+ // if the new size is longer than the vector, assign value to each
+ // appended item.
+ void resize_unchecked(size_t new_len, limb value) noexcept {
+ if (new_len > len()) {
+ size_t count = new_len - len();
+ limb* first = data + len();
+ limb* last = first + count;
+ ::std::fill(first, last, value);
+ set_len(new_len);
+ } else {
+ set_len(new_len);
+ }
+ }
+ // try to resize the vector, returning if the vector was resized.
+ bool try_resize(size_t new_len, limb value) noexcept {
+ if (new_len > capacity()) {
+ return false;
+ } else {
+ resize_unchecked(new_len, value);
+ return true;
+ }
+ }
+ // check if any limbs are non-zero after the given index.
+ // this needs to be done in reverse order, since the index
+ // is relative to the most significant limbs.
+ bool nonzero(size_t index) const noexcept {
+ while (index < len()) {
+ if (rindex(index) != 0) {
+ return true;
+ }
+ index++;
+ }
+ return false;
+ }
+ // normalize the big integer, so most-significant zero limbs are removed.
+ void normalize() noexcept {
+ while (len() > 0 && rindex(0) == 0) {
+ length--;
+ }
+ }
+ };
+
+ fastfloat_really_inline
+ uint64_t empty_hi64(bool& truncated) noexcept {
+ truncated = false;
+ return 0;
+ }
+
+ fastfloat_really_inline
+ uint64_t uint64_hi64(uint64_t r0, bool& truncated) noexcept {
+ truncated = false;
+ int shl = leading_zeroes(r0);
+ return r0 << shl;
+ }
+
+ fastfloat_really_inline
+ uint64_t uint64_hi64(uint64_t r0, uint64_t r1, bool& truncated) noexcept {
+ int shl = leading_zeroes(r0);
+ if (shl == 0) {
+ truncated = r1 != 0;
+ return r0;
+ } else {
+ int shr = 64 - shl;
+ truncated = (r1 << shl) != 0;
+ return (r0 << shl) | (r1 >> shr);
+ }
+ }
+
+ fastfloat_really_inline
+ uint64_t uint32_hi64(uint32_t r0, bool& truncated) noexcept {
+ return uint64_hi64(r0, truncated);
+ }
+
+ fastfloat_really_inline
+ uint64_t uint32_hi64(uint32_t r0, uint32_t r1, bool& truncated) noexcept {
+ uint64_t x0 = r0;
+ uint64_t x1 = r1;
+ return uint64_hi64((x0 << 32) | x1, truncated);
+ }
+
+ fastfloat_really_inline
+ uint64_t uint32_hi64(uint32_t r0, uint32_t r1, uint32_t r2, bool& truncated) noexcept {
+ uint64_t x0 = r0;
+ uint64_t x1 = r1;
+ uint64_t x2 = r2;
+ return uint64_hi64(x0, (x1 << 32) | x2, truncated);
+ }
+
+ // add two small integers, checking for overflow.
+ // we want an efficient operation. for msvc, where
+ // we don't have built-in intrinsics, this is still
+ // pretty fast.
+ fastfloat_really_inline
+ limb scalar_add(limb x, limb y, bool& overflow) noexcept {
+ limb z;
+
+// gcc and clang
+#if defined(__has_builtin)
+ #if __has_builtin(__builtin_add_overflow)
+ overflow = __builtin_add_overflow(x, y, &z);
+ return z;
+#endif
+#endif
+
+ // generic, this still optimizes correctly on MSVC.
+ z = x + y;
+ overflow = z < x;
+ return z;
+ }
+
+ // multiply two small integers, getting both the high and low bits.
+ fastfloat_really_inline
+ limb scalar_mul(limb x, limb y, limb& carry) noexcept {
+#ifdef FASTFLOAT_64BIT_LIMB
+#if defined(__SIZEOF_INT128__)
+ // GCC and clang both define it as an extension.
+ __uint128_t z = __uint128_t(x) * __uint128_t(y) + __uint128_t(carry);
+ carry = limb(z >> limb_bits);
+ return limb(z);
+#else
+ // fallback, no native 128-bit integer multiplication with carry.
+ // on msvc, this optimizes identically, somehow.
+ value128 z = full_multiplication(x, y);
+ bool overflow;
+ z.low = scalar_add(z.low, carry, overflow);
+ z.high += uint64_t(overflow); // cannot overflow
+ carry = z.high;
+ return z.low;
+#endif
+#else
+ uint64_t z = uint64_t(x) * uint64_t(y) + uint64_t(carry);
+ carry = limb(z >> limb_bits);
+ return limb(z);
+#endif
+ }
+
+ // add scalar value to bigint starting from offset.
+ // used in grade school multiplication
+ template <uint16_t size>
+ inline bool small_add_from(stackvec<size>& vec, limb y, size_t start) noexcept {
+ size_t index = start;
+ limb carry = y;
+ bool overflow;
+ while (carry != 0 && index < vec.len()) {
+ vec[index] = scalar_add(vec[index], carry, overflow);
+ carry = limb(overflow);
+ index += 1;
+ }
+ if (carry != 0) {
+ FASTFLOAT_TRY(vec.try_push(carry));
+ }
+ return true;
+ }
+
+ // add scalar value to bigint.
+ template <uint16_t size>
+ fastfloat_really_inline bool small_add(stackvec<size>& vec, limb y) noexcept {
+ return small_add_from(vec, y, 0);
+ }
+
+ // multiply bigint by scalar value.
+ template <uint16_t size>
+ inline bool small_mul(stackvec<size>& vec, limb y) noexcept {
+ limb carry = 0;
+ for (size_t index = 0; index < vec.len(); index++) {
+ vec[index] = scalar_mul(vec[index], y, carry);
+ }
+ if (carry != 0) {
+ FASTFLOAT_TRY(vec.try_push(carry));
+ }
+ return true;
+ }
+
+ // add bigint to bigint starting from index.
+ // used in grade school multiplication
+ template <uint16_t size>
+ bool large_add_from(stackvec<size>& x, limb_span y, size_t start) noexcept {
+ // the effective x buffer is from `xstart..x.len()`, so exit early
+ // if we can't get that current range.
+ if (x.len() < start || y.len() > x.len() - start) {
+ FASTFLOAT_TRY(x.try_resize(y.len() + start, 0));
+ }
+
+ bool carry = false;
+ for (size_t index = 0; index < y.len(); index++) {
+ limb xi = x[index + start];
+ limb yi = y[index];
+ bool c1 = false;
+ bool c2 = false;
+ xi = scalar_add(xi, yi, c1);
+ if (carry) {
+ xi = scalar_add(xi, 1, c2);
+ }
+ x[index + start] = xi;
+ carry = c1 | c2;
+ }
+
+ // handle overflow
+ if (carry) {
+ FASTFLOAT_TRY(small_add_from(x, 1, y.len() + start));
+ }
+ return true;
+ }
+
+ // add bigint to bigint.
+ template <uint16_t size>
+ fastfloat_really_inline bool large_add_from(stackvec<size>& x, limb_span y) noexcept {
+ return large_add_from(x, y, 0);
+ }
+
+ // grade-school multiplication algorithm
+ template <uint16_t size>
+ bool long_mul(stackvec<size>& x, limb_span y) noexcept {
+ limb_span xs = limb_span(x.data, x.len());
+ stackvec<size> z(xs);
+ limb_span zs = limb_span(z.data, z.len());
+
+ if (y.len() != 0) {
+ limb y0 = y[0];
+ FASTFLOAT_TRY(small_mul(x, y0));
+ for (size_t index = 1; index < y.len(); index++) {
+ limb yi = y[index];
+ stackvec<size> zi;
+ if (yi != 0) {
+ // re-use the same buffer throughout
+ zi.set_len(0);
+ FASTFLOAT_TRY(zi.try_extend(zs));
+ FASTFLOAT_TRY(small_mul(zi, yi));
+ limb_span zis = limb_span(zi.data, zi.len());
+ FASTFLOAT_TRY(large_add_from(x, zis, index));
+ }
+ }
+ }
+
+ x.normalize();
+ return true;
+ }
+
+ // grade-school multiplication algorithm
+ template <uint16_t size>
+ bool large_mul(stackvec<size>& x, limb_span y) noexcept {
+ if (y.len() == 1) {
+ FASTFLOAT_TRY(small_mul(x, y[0]));
+ } else {
+ FASTFLOAT_TRY(long_mul(x, y));
+ }
+ return true;
+ }
+
+ // big integer type. implements a small subset of big integer
+ // arithmetic, using simple algorithms since asymptotically
+ // faster algorithms are slower for a small number of limbs.
+ // all operations assume the big-integer is normalized.
+ struct bigint {
+ // storage of the limbs, in little-endian order.
+ stackvec<bigint_limbs> vec;
+
+ bigint(): vec() {}
+ bigint(const bigint &) = delete;
+ bigint &operator=(const bigint &) = delete;
+ bigint(bigint &&) = delete;
+ bigint &operator=(bigint &&other) = delete;
+
+ bigint(uint64_t value): vec() {
+#ifdef FASTFLOAT_64BIT_LIMB
+ vec.push_unchecked(value);
+#else
+ vec.push_unchecked(uint32_t(value));
+ vec.push_unchecked(uint32_t(value >> 32));
+#endif
+ vec.normalize();
+ }
+
+ // get the high 64 bits from the vector, and if bits were truncated.
+ // this is to get the significant digits for the float.
+ uint64_t hi64(bool& truncated) const noexcept {
+#ifdef FASTFLOAT_64BIT_LIMB
+ if (vec.len() == 0) {
+ return empty_hi64(truncated);
+ } else if (vec.len() == 1) {
+ return uint64_hi64(vec.rindex(0), truncated);
+ } else {
+ uint64_t result = uint64_hi64(vec.rindex(0), vec.rindex(1), truncated);
+ truncated |= vec.nonzero(2);
+ return result;
+ }
+#else
+ if (vec.len() == 0) {
+ return empty_hi64(truncated);
+ } else if (vec.len() == 1) {
+ return uint32_hi64(vec.rindex(0), truncated);
+ } else if (vec.len() == 2) {
+ return uint32_hi64(vec.rindex(0), vec.rindex(1), truncated);
+ } else {
+ uint64_t result = uint32_hi64(vec.rindex(0), vec.rindex(1), vec.rindex(2), truncated);
+ truncated |= vec.nonzero(3);
+ return result;
+ }
+#endif
+ }
+
+ // compare two big integers, returning the large value.
+ // assumes both are normalized. if the return value is
+ // negative, other is larger, if the return value is
+ // positive, this is larger, otherwise they are equal.
+ // the limbs are stored in little-endian order, so we
+ // must compare the limbs in ever order.
+ int compare(const bigint& other) const noexcept {
+ if (vec.len() > other.vec.len()) {
+ return 1;
+ } else if (vec.len() < other.vec.len()) {
+ return -1;
+ } else {
+ for (size_t index = vec.len(); index > 0; index--) {
+ limb xi = vec[index - 1];
+ limb yi = other.vec[index - 1];
+ if (xi > yi) {
+ return 1;
+ } else if (xi < yi) {
+ return -1;
+ }
+ }
+ return 0;
+ }
+ }
+
+ // shift left each limb n bits, carrying over to the new limb
+ // returns true if we were able to shift all the digits.
+ bool shl_bits(size_t n) noexcept {
+ // Internally, for each item, we shift left by n, and add the previous
+ // right shifted limb-bits.
+ // For example, we transform (for u8) shifted left 2, to:
+ // b10100100 b01000010
+ // b10 b10010001 b00001000
+ FASTFLOAT_DEBUG_ASSERT(n != 0);
+ FASTFLOAT_DEBUG_ASSERT(n < sizeof(limb) * 8);
+
+ size_t shl = n;
+ size_t shr = limb_bits - shl;
+ limb prev = 0;
+ for (size_t index = 0; index < vec.len(); index++) {
+ limb xi = vec[index];
+ vec[index] = (xi << shl) | (prev >> shr);
+ prev = xi;
+ }
+
+ limb carry = prev >> shr;
+ if (carry != 0) {
+ return vec.try_push(carry);
+ }
+ return true;
+ }
+
+ // move the limbs left by `n` limbs.
+ bool shl_limbs(size_t n) noexcept {
+ FASTFLOAT_DEBUG_ASSERT(n != 0);
+ if (n + vec.len() > vec.capacity()) {
+ return false;
+ } else if (!vec.is_empty()) {
+ // move limbs
+ limb* dst = vec.data + n;
+ const limb* src = vec.data;
+ ::memmove(dst, src, sizeof(limb) * vec.len());
+ // fill in empty limbs
+ limb* first = vec.data;
+ limb* last = first + n;
+ ::std::fill(first, last, 0);
+ vec.set_len(n + vec.len());
+ return true;
+ } else {
+ return true;
+ }
+ }
+
+ // move the limbs left by `n` bits.
+ bool shl(size_t n) noexcept {
+ size_t rem = n % limb_bits;
+ size_t div = n / limb_bits;
+ if (rem != 0) {
+ FASTFLOAT_TRY(shl_bits(rem));
+ }
+ if (div != 0) {
+ FASTFLOAT_TRY(shl_limbs(div));
+ }
+ return true;
+ }
+
+ // get the number of leading zeros in the bigint.
+ int ctlz() const noexcept {
+ if (vec.is_empty()) {
+ return 0;
+ } else {
+#ifdef FASTFLOAT_64BIT_LIMB
+ return leading_zeroes(vec.rindex(0));
+#else
+ // no use defining a specialized leading_zeroes for a 32-bit type.
+ uint64_t r0 = vec.rindex(0);
+ return leading_zeroes(r0 << 32);
+#endif
+ }
+ }
+
+ // get the number of bits in the bigint.
+ int bit_length() const noexcept {
+ int lz = ctlz();
+ return int(limb_bits * vec.len()) - lz;
+ }
+
+ bool mul(limb y) noexcept {
+ return small_mul(vec, y);
+ }
+
+ bool add(limb y) noexcept {
+ return small_add(vec, y);
+ }
+
+ // multiply as if by 2 raised to a power.
+ bool pow2(uint32_t exp) noexcept {
+ return shl(exp);
+ }
+
+ // multiply as if by 5 raised to a power.
+ bool pow5(uint32_t exp) noexcept {
+ // multiply by a power of 5
+ static constexpr uint32_t large_step = 135;
+ static constexpr uint64_t small_power_of_5[] = {
+ 1UL, 5UL, 25UL, 125UL, 625UL, 3125UL, 15625UL, 78125UL, 390625UL,
+ 1953125UL, 9765625UL, 48828125UL, 244140625UL, 1220703125UL,
+ 6103515625UL, 30517578125UL, 152587890625UL, 762939453125UL,
+ 3814697265625UL, 19073486328125UL, 95367431640625UL, 476837158203125UL,
+ 2384185791015625UL, 11920928955078125UL, 59604644775390625UL,
+ 298023223876953125UL, 1490116119384765625UL, 7450580596923828125UL,
+ };
+#ifdef FASTFLOAT_64BIT_LIMB
+ constexpr static limb large_power_of_5[] = {
+ 1414648277510068013UL, 9180637584431281687UL, 4539964771860779200UL,
+ 10482974169319127550UL, 198276706040285095UL};
+#else
+ constexpr static limb large_power_of_5[] = {
+ 4279965485U, 329373468U, 4020270615U, 2137533757U, 4287402176U,
+ 1057042919U, 1071430142U, 2440757623U, 381945767U, 46164893U};
+#endif
+ size_t large_length = sizeof(large_power_of_5) / sizeof(limb);
+ limb_span large = limb_span(large_power_of_5, large_length);
+ while (exp >= large_step) {
+ FASTFLOAT_TRY(large_mul(vec, large));
+ exp -= large_step;
+ }
+#ifdef FASTFLOAT_64BIT_LIMB
+ uint32_t small_step = 27;
+ limb max_native = 7450580596923828125UL;
+#else
+ uint32_t small_step = 13;
+ limb max_native = 1220703125U;
+#endif
+ while (exp >= small_step) {
+ FASTFLOAT_TRY(small_mul(vec, max_native));
+ exp -= small_step;
+ }
+ if (exp != 0) {
+ FASTFLOAT_TRY(small_mul(vec, limb(small_power_of_5[exp])));
+ }
+
+ return true;
+ }
+
+ // multiply as if by 10 raised to a power.
+ bool pow10(uint32_t exp) noexcept {
+ FASTFLOAT_TRY(pow5(exp));
+ return pow2(exp);
+ }
+ };
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_ASCII_NUMBER_H
+#define FASTFLOAT_ASCII_NUMBER_H
+
+#include <cctype>
+#include <cstdint>
+#include <cstring>
+#include <iterator>
+
+
+namespace fast_float {
+
+ // Next function can be micro-optimized, but compilers are entirely
+ // able to optimize it well.
+ fastfloat_really_inline bool is_integer(char c) noexcept { return c >= '0' && c <= '9'; }
+
+ fastfloat_really_inline uint64_t byteswap(uint64_t val) {
+ return (val & 0xFF00000000000000) >> 56
+ | (val & 0x00FF000000000000) >> 40
+ | (val & 0x0000FF0000000000) >> 24
+ | (val & 0x000000FF00000000) >> 8
+ | (val & 0x00000000FF000000) << 8
+ | (val & 0x0000000000FF0000) << 24
+ | (val & 0x000000000000FF00) << 40
+ | (val & 0x00000000000000FF) << 56;
+ }
+
+ fastfloat_really_inline uint64_t read_u64(const char *chars) {
+ uint64_t val;
+ ::memcpy(&val, chars, sizeof(uint64_t));
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ // Need to read as-if the number was in little-endian order.
+ val = byteswap(val);
+#endif
+ return val;
+ }
+
+ fastfloat_really_inline void write_u64(uint8_t *chars, uint64_t val) {
+#if FASTFLOAT_IS_BIG_ENDIAN == 1
+ // Need to read as-if the number was in little-endian order.
+ val = byteswap(val);
+#endif
+ ::memcpy(chars, &val, sizeof(uint64_t));
+ }
+
+ // credit @aqrit
+ fastfloat_really_inline uint32_t parse_eight_digits_unrolled(uint64_t val) {
+ const uint64_t mask = 0x000000FF000000FF;
+ const uint64_t mul1 = 0x000F424000000064; // 100 + (1000000ULL << 32)
+ const uint64_t mul2 = 0x0000271000000001; // 1 + (10000ULL << 32)
+ val -= 0x3030303030303030;
+ val = (val * 10) + (val >> 8); // val = (val * 2561) >> 8;
+ val = (((val & mask) * mul1) + (((val >> 16) & mask) * mul2)) >> 32;
+ return uint32_t(val);
+ }
+
+ fastfloat_really_inline uint32_t parse_eight_digits_unrolled(const char *chars) noexcept {
+ return parse_eight_digits_unrolled(read_u64(chars));
+ }
+
+ // credit @aqrit
+ fastfloat_really_inline bool is_made_of_eight_digits_fast(uint64_t val) noexcept {
+ return !((((val + 0x4646464646464646) | (val - 0x3030303030303030)) &
+ 0x8080808080808080));
+ }
+
+ fastfloat_really_inline bool is_made_of_eight_digits_fast(const char *chars) noexcept {
+ return is_made_of_eight_digits_fast(read_u64(chars));
+ }
+
+ typedef span<const char> byte_span;
+
+ struct parsed_number_string {
+ int64_t exponent{0};
+ uint64_t mantissa{0};
+ const char *lastmatch{nullptr};
+ bool negative{false};
+ bool valid{false};
+ bool too_many_digits{false};
+ // contains the range of the significant digits
+ byte_span integer{}; // non-nullable
+ byte_span fraction{}; // nullable
+ };
+
+ // Assuming that you use no more than 19 digits, this will
+ // parse an ASCII string.
+ fastfloat_really_inline
+ parsed_number_string parse_number_string(const char *p, const char *pend, parse_options options) noexcept {
+ const chars_format fmt = options.format;
+ const char decimal_point = options.decimal_point;
+
+ parsed_number_string answer;
+ answer.valid = false;
+ answer.too_many_digits = false;
+ answer.negative = (*p == '-');
+ if (*p == '-') { // C++17 20.19.3.(7.1) explicitly forbids '+' sign here
+ ++p;
+ if (p == pend) {
+ return answer;
+ }
+ if (!is_integer(*p) && (*p != decimal_point)) { // a sign must be followed by an integer or the dot
+ return answer;
+ }
+ }
+ const char *const start_digits = p;
+
+ uint64_t i = 0; // an unsigned int avoids signed overflows (which are bad)
+
+ while ((std::distance(p, pend) >= 8) && is_made_of_eight_digits_fast(p)) {
+ i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok
+ p += 8;
+ }
+ while ((p != pend) && is_integer(*p)) {
+ // a multiplication by 10 is cheaper than an arbitrary integer
+ // multiplication
+ i = 10 * i +
+ uint64_t(*p - '0'); // might overflow, we will handle the overflow later
+ ++p;
+ }
+ const char *const end_of_integer_part = p;
+ int64_t digit_count = int64_t(end_of_integer_part - start_digits);
+ answer.integer = byte_span(start_digits, size_t(digit_count));
+ int64_t exponent = 0;
+ if ((p != pend) && (*p == decimal_point)) {
+ ++p;
+ const char* before = p;
+ // can occur at most twice without overflowing, but let it occur more, since
+ // for integers with many digits, digit parsing is the primary bottleneck.
+ while ((std::distance(p, pend) >= 8) && is_made_of_eight_digits_fast(p)) {
+ i = i * 100000000 + parse_eight_digits_unrolled(p); // in rare cases, this will overflow, but that's ok
+ p += 8;
+ }
+ while ((p != pend) && is_integer(*p)) {
+ uint8_t digit = uint8_t(*p - '0');
+ ++p;
+ i = i * 10 + digit; // in rare cases, this will overflow, but that's ok
+ }
+ exponent = before - p;
+ answer.fraction = byte_span(before, size_t(p - before));
+ digit_count -= exponent;
+ }
+ // we must have encountered at least one integer!
+ if (digit_count == 0) {
+ return answer;
+ }
+ int64_t exp_number = 0; // explicit exponential part
+ if ((fmt & chars_format::scientific) && (p != pend) && (('e' == *p) || ('E' == *p))) {
+ const char * location_of_e = p;
+ ++p;
+ bool neg_exp = false;
+ if ((p != pend) && ('-' == *p)) {
+ neg_exp = true;
+ ++p;
+ } else if ((p != pend) && ('+' == *p)) { // '+' on exponent is allowed by C++17 20.19.3.(7.1)
+ ++p;
+ }
+ if ((p == pend) || !is_integer(*p)) {
+ if(!(fmt & chars_format::fixed)) {
+ // We are in error.
+ return answer;
+ }
+ // Otherwise, we will be ignoring the 'e'.
+ p = location_of_e;
+ } else {
+ while ((p != pend) && is_integer(*p)) {
+ uint8_t digit = uint8_t(*p - '0');
+ if (exp_number < 0x10000000) {
+ exp_number = 10 * exp_number + digit;
+ }
+ ++p;
+ }
+ if(neg_exp) { exp_number = - exp_number; }
+ exponent += exp_number;
+ }
+ } else {
+ // If it scientific and not fixed, we have to bail out.
+ if((fmt & chars_format::scientific) && !(fmt & chars_format::fixed)) { return answer; }
+ }
+ answer.lastmatch = p;
+ answer.valid = true;
+
+ // If we frequently had to deal with long strings of digits,
+ // we could extend our code by using a 128-bit integer instead
+ // of a 64-bit integer. However, this is uncommon.
+ //
+ // We can deal with up to 19 digits.
+ if (digit_count > 19) { // this is uncommon
+ // It is possible that the integer had an overflow.
+ // We have to handle the case where we have 0.0000somenumber.
+ // We need to be mindful of the case where we only have zeroes...
+ // E.g., 0.000000000...000.
+ const char *start = start_digits;
+ while ((start != pend) && (*start == '0' || *start == decimal_point)) {
+ if(*start == '0') { digit_count --; }
+ start++;
+ }
+ if (digit_count > 19) {
+ answer.too_many_digits = true;
+ // Let us start again, this time, avoiding overflows.
+ // We don't need to check if is_integer, since we use the
+ // pre-tokenized spans from above.
+ i = 0;
+ p = answer.integer.ptr;
+ const char* int_end = p + answer.integer.len();
+ const uint64_t minimal_nineteen_digit_integer{1000000000000000000};
+ while((i < minimal_nineteen_digit_integer) && (p != int_end)) {
+ i = i * 10 + uint64_t(*p - '0');
+ ++p;
+ }
+ if (i >= minimal_nineteen_digit_integer) { // We have a big integers
+ exponent = end_of_integer_part - p + exp_number;
+ } else { // We have a value with a fractional component.
+ p = answer.fraction.ptr;
+ const char* frac_end = p + answer.fraction.len();
+ while((i < minimal_nineteen_digit_integer) && (p != frac_end)) {
+ i = i * 10 + uint64_t(*p - '0');
+ ++p;
+ }
+ exponent = answer.fraction.ptr - p + exp_number;
+ }
+ // We have now corrected both exponent and i, to a truncated value
+ }
+ }
+ answer.exponent = exponent;
+ answer.mantissa = i;
+ return answer;
+ }
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_DIGIT_COMPARISON_H
+#define FASTFLOAT_DIGIT_COMPARISON_H
+
+#include <algorithm>
+#include <cstdint>
+#include <cstring>
+#include <iterator>
+
+
+namespace fast_float {
+
+ // 1e0 to 1e19
+ constexpr static uint64_t powers_of_ten_uint64[] = {
+ 1UL, 10UL, 100UL, 1000UL, 10000UL, 100000UL, 1000000UL, 10000000UL, 100000000UL,
+ 1000000000UL, 10000000000UL, 100000000000UL, 1000000000000UL, 10000000000000UL,
+ 100000000000000UL, 1000000000000000UL, 10000000000000000UL, 100000000000000000UL,
+ 1000000000000000000UL, 10000000000000000000UL};
+
+ // calculate the exponent, in scientific notation, of the number.
+ // this algorithm is not even close to optimized, but it has no practical
+ // effect on performance: in order to have a faster algorithm, we'd need
+ // to slow down performance for faster algorithms, and this is still fast.
+ fastfloat_really_inline int32_t scientific_exponent(parsed_number_string& num) noexcept {
+ uint64_t mantissa = num.mantissa;
+ int32_t exponent = int32_t(num.exponent);
+ while (mantissa >= 10000) {
+ mantissa /= 10000;
+ exponent += 4;
+ }
+ while (mantissa >= 100) {
+ mantissa /= 100;
+ exponent += 2;
+ }
+ while (mantissa >= 10) {
+ mantissa /= 10;
+ exponent += 1;
+ }
+ return exponent;
+ }
+
+ // this converts a native floating-point number to an extended-precision float.
+ template <typename T>
+ fastfloat_really_inline adjusted_mantissa to_extended(T value) noexcept {
+ using equiv_uint = typename binary_format<T>::equiv_uint;
+ constexpr equiv_uint exponent_mask = binary_format<T>::exponent_mask();
+ constexpr equiv_uint mantissa_mask = binary_format<T>::mantissa_mask();
+ constexpr equiv_uint hidden_bit_mask = binary_format<T>::hidden_bit_mask();
+
+ adjusted_mantissa am;
+ int32_t bias = binary_format<T>::mantissa_explicit_bits() - binary_format<T>::minimum_exponent();
+ equiv_uint bits;
+ ::memcpy(&bits, &value, sizeof(T));
+ if ((bits & exponent_mask) == 0) {
+ // denormal
+ am.power2 = 1 - bias;
+ am.mantissa = bits & mantissa_mask;
+ } else {
+ // normal
+ am.power2 = int32_t((bits & exponent_mask) >> binary_format<T>::mantissa_explicit_bits());
+ am.power2 -= bias;
+ am.mantissa = (bits & mantissa_mask) | hidden_bit_mask;
+ }
+
+ return am;
+ }
+
+ // get the extended precision value of the halfway point between b and b+u.
+ // we are given a native float that represents b, so we need to adjust it
+ // halfway between b and b+u.
+ template <typename T>
+ fastfloat_really_inline adjusted_mantissa to_extended_halfway(T value) noexcept {
+ adjusted_mantissa am = to_extended(value);
+ am.mantissa <<= 1;
+ am.mantissa += 1;
+ am.power2 -= 1;
+ return am;
+ }
+
+ // round an extended-precision float to the nearest machine float.
+ template <typename T, typename callback>
+ fastfloat_really_inline void round(adjusted_mantissa& am, callback cb) noexcept {
+ int32_t mantissa_shift = 64 - binary_format<T>::mantissa_explicit_bits() - 1;
+ if (-am.power2 >= mantissa_shift) {
+ // have a denormal float
+ int32_t shift = -am.power2 + 1;
+ cb(am, std::min(shift, 64));
+ // check for round-up: if rounding-nearest carried us to the hidden bit.
+ am.power2 = (am.mantissa < (uint64_t(1) << binary_format<T>::mantissa_explicit_bits())) ? 0 : 1;
+ return;
+ }
+
+ // have a normal float, use the default shift.
+ cb(am, mantissa_shift);
+
+ // check for carry
+ if (am.mantissa >= (uint64_t(2) << binary_format<T>::mantissa_explicit_bits())) {
+ am.mantissa = (uint64_t(1) << binary_format<T>::mantissa_explicit_bits());
+ am.power2++;
+ }
+
+ // check for infinite: we could have carried to an infinite power
+ am.mantissa &= ~(uint64_t(1) << binary_format<T>::mantissa_explicit_bits());
+ if (am.power2 >= binary_format<T>::infinite_power()) {
+ am.power2 = binary_format<T>::infinite_power();
+ am.mantissa = 0;
+ }
+ }
+
+ template <typename callback>
+ fastfloat_really_inline
+ void round_nearest_tie_even(adjusted_mantissa& am, int32_t shift, callback cb) noexcept {
+ uint64_t mask;
+ uint64_t halfway;
+ if (shift == 64) {
+ mask = UINT64_MAX;
+ } else {
+ mask = (uint64_t(1) << shift) - 1;
+ }
+ if (shift == 0) {
+ halfway = 0;
+ } else {
+ halfway = uint64_t(1) << (shift - 1);
+ }
+ uint64_t truncated_bits = am.mantissa & mask;
+ uint64_t is_above = truncated_bits > halfway;
+ uint64_t is_halfway = truncated_bits == halfway;
+
+ // shift digits into position
+ if (shift == 64) {
+ am.mantissa = 0;
+ } else {
+ am.mantissa >>= shift;
+ }
+ am.power2 += shift;
+
+ bool is_odd = (am.mantissa & 1) == 1;
+ am.mantissa += uint64_t(cb(is_odd, is_halfway, is_above));
+ }
+
+ fastfloat_really_inline void round_down(adjusted_mantissa& am, int32_t shift) noexcept {
+ if (shift == 64) {
+ am.mantissa = 0;
+ } else {
+ am.mantissa >>= shift;
+ }
+ am.power2 += shift;
+ }
+
+ fastfloat_really_inline void skip_zeros(const char*& first, const char* last) noexcept {
+ uint64_t val;
+ while (std::distance(first, last) >= 8) {
+ ::memcpy(&val, first, sizeof(uint64_t));
+ if (val != 0x3030303030303030) {
+ break;
+ }
+ first += 8;
+ }
+ while (first != last) {
+ if (*first != '0') {
+ break;
+ }
+ first++;
+ }
+ }
+
+ // determine if any non-zero digits were truncated.
+ // all characters must be valid digits.
+ fastfloat_really_inline bool is_truncated(const char* first, const char* last) noexcept {
+ // do 8-bit optimizations, can just compare to 8 literal 0s.
+ uint64_t val;
+ while (std::distance(first, last) >= 8) {
+ ::memcpy(&val, first, sizeof(uint64_t));
+ if (val != 0x3030303030303030) {
+ return true;
+ }
+ first += 8;
+ }
+ while (first != last) {
+ if (*first != '0') {
+ return true;
+ }
+ first++;
+ }
+ return false;
+ }
+
+ fastfloat_really_inline bool is_truncated(byte_span s) noexcept {
+ return is_truncated(s.ptr, s.ptr + s.len());
+ }
+
+ fastfloat_really_inline
+ void parse_eight_digits(const char*& p, limb& value, size_t& counter, size_t& count) noexcept {
+ value = value * 100000000 + parse_eight_digits_unrolled(p);
+ p += 8;
+ counter += 8;
+ count += 8;
+ }
+
+ fastfloat_really_inline
+ void parse_one_digit(const char*& p, limb& value, size_t& counter, size_t& count) noexcept {
+ value = value * 10 + limb(*p - '0');
+ p++;
+ counter++;
+ count++;
+ }
+
+ fastfloat_really_inline
+ void add_native(bigint& big, limb power, limb value) noexcept {
+ big.mul(power);
+ big.add(value);
+ }
+
+ fastfloat_really_inline void round_up_bigint(bigint& big, size_t& count) noexcept {
+ // need to round-up the digits, but need to avoid rounding
+ // ....9999 to ...10000, which could cause a false halfway point.
+ add_native(big, 10, 1);
+ count++;
+ }
+
+ // parse the significant digits into a big integer
+ inline void parse_mantissa(bigint& result, parsed_number_string& num, size_t max_digits, size_t& digits) noexcept {
+ // try to minimize the number of big integer and scalar multiplication.
+ // therefore, try to parse 8 digits at a time, and multiply by the largest
+ // scalar value (9 or 19 digits) for each step.
+ size_t counter = 0;
+ digits = 0;
+ limb value = 0;
+#ifdef FASTFLOAT_64BIT_LIMB
+ size_t step = 19;
+#else
+ size_t step = 9;
+#endif
+
+ // process all integer digits.
+ const char* p = num.integer.ptr;
+ const char* pend = p + num.integer.len();
+ skip_zeros(p, pend);
+ // process all digits, in increments of step per loop
+ while (p != pend) {
+ while ((std::distance(p, pend) >= 8) && (step - counter >= 8) && (max_digits - digits >= 8)) {
+ parse_eight_digits(p, value, counter, digits);
+ }
+ while (counter < step && p != pend && digits < max_digits) {
+ parse_one_digit(p, value, counter, digits);
+ }
+ if (digits == max_digits) {
+ // add the temporary value, then check if we've truncated any digits
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ bool truncated = is_truncated(p, pend);
+ if (num.fraction.ptr != nullptr) {
+ truncated |= is_truncated(num.fraction);
+ }
+ if (truncated) {
+ round_up_bigint(result, digits);
+ }
+ return;
+ } else {
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ counter = 0;
+ value = 0;
+ }
+ }
+
+ // add our fraction digits, if they're available.
+ if (num.fraction.ptr != nullptr) {
+ p = num.fraction.ptr;
+ pend = p + num.fraction.len();
+ if (digits == 0) {
+ skip_zeros(p, pend);
+ }
+ // process all digits, in increments of step per loop
+ while (p != pend) {
+ while ((std::distance(p, pend) >= 8) && (step - counter >= 8) && (max_digits - digits >= 8)) {
+ parse_eight_digits(p, value, counter, digits);
+ }
+ while (counter < step && p != pend && digits < max_digits) {
+ parse_one_digit(p, value, counter, digits);
+ }
+ if (digits == max_digits) {
+ // add the temporary value, then check if we've truncated any digits
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ bool truncated = is_truncated(p, pend);
+ if (truncated) {
+ round_up_bigint(result, digits);
+ }
+ return;
+ } else {
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ counter = 0;
+ value = 0;
+ }
+ }
+ }
+
+ if (counter != 0) {
+ add_native(result, limb(powers_of_ten_uint64[counter]), value);
+ }
+ }
+
+ template <typename T>
+ inline adjusted_mantissa positive_digit_comp(bigint& bigmant, int32_t exponent) noexcept {
+ FASTFLOAT_ASSERT(bigmant.pow10(uint32_t(exponent)));
+ adjusted_mantissa answer;
+ bool truncated;
+ answer.mantissa = bigmant.hi64(truncated);
+ int bias = binary_format<T>::mantissa_explicit_bits() - binary_format<T>::minimum_exponent();
+ answer.power2 = bigmant.bit_length() - 64 + bias;
+
+ round<T>(answer, [truncated](adjusted_mantissa& a, int32_t shift) {
+ round_nearest_tie_even(a, shift, [truncated](bool is_odd, bool is_halfway, bool is_above) -> bool {
+ return is_above || (is_halfway && truncated) || (is_odd && is_halfway);
+ });
+ });
+
+ return answer;
+ }
+
+ // the scaling here is quite simple: we have, for the real digits `m * 10^e`,
+ // and for the theoretical digits `n * 2^f`. Since `e` is always negative,
+ // to scale them identically, we do `n * 2^f * 5^-f`, so we now have `m * 2^e`.
+ // we then need to scale by `2^(f- e)`, and then the two significant digits
+ // are of the same magnitude.
+ template <typename T>
+ inline adjusted_mantissa negative_digit_comp(bigint& bigmant, adjusted_mantissa am, int32_t exponent) noexcept {
+ bigint& real_digits = bigmant;
+ int32_t real_exp = exponent;
+
+ // get the value of `b`, rounded down, and get a bigint representation of b+h
+ adjusted_mantissa am_b = am;
+ // gcc7 buf: use a lambda to remove the noexcept qualifier bug with -Wnoexcept-type.
+ round<T>(am_b, [](adjusted_mantissa&a, int32_t shift) { round_down(a, shift); });
+ T b;
+ to_float(false, am_b, b);
+ adjusted_mantissa theor = to_extended_halfway(b);
+ bigint theor_digits(theor.mantissa);
+ int32_t theor_exp = theor.power2;
+
+ // scale real digits and theor digits to be same power.
+ int32_t pow2_exp = theor_exp - real_exp;
+ uint32_t pow5_exp = uint32_t(-real_exp);
+ if (pow5_exp != 0) {
+ FASTFLOAT_ASSERT(theor_digits.pow5(pow5_exp));
+ }
+ if (pow2_exp > 0) {
+ FASTFLOAT_ASSERT(theor_digits.pow2(uint32_t(pow2_exp)));
+ } else if (pow2_exp < 0) {
+ FASTFLOAT_ASSERT(real_digits.pow2(uint32_t(-pow2_exp)));
+ }
+
+ // compare digits, and use it to director rounding
+ int ord = real_digits.compare(theor_digits);
+ adjusted_mantissa answer = am;
+ round<T>(answer, [ord](adjusted_mantissa& a, int32_t shift) {
+ round_nearest_tie_even(a, shift, [ord](bool is_odd, bool _, bool __) -> bool {
+ (void)_; // not needed, since we've done our comparison
+ (void)__; // not needed, since we've done our comparison
+ if (ord > 0) {
+ return true;
+ } else if (ord < 0) {
+ return false;
+ } else {
+ return is_odd;
+ }
+ });
+ });
+
+ return answer;
+ }
+
+ // parse the significant digits as a big integer to unambiguously round the
+ // the significant digits. here, we are trying to determine how to round
+ // an extended float representation close to `b+h`, halfway between `b`
+ // (the float rounded-down) and `b+u`, the next positive float. this
+ // algorithm is always correct, and uses one of two approaches. when
+ // the exponent is positive relative to the significant digits (such as
+ // 1234), we create a big-integer representation, get the high 64-bits,
+ // determine if any lower bits are truncated, and use that to direct
+ // rounding. in case of a negative exponent relative to the significant
+ // digits (such as 1.2345), we create a theoretical representation of
+ // `b` as a big-integer type, scaled to the same binary exponent as
+ // the actual digits. we then compare the big integer representations
+ // of both, and use that to direct rounding.
+ template <typename T>
+ inline adjusted_mantissa digit_comp(parsed_number_string& num, adjusted_mantissa am) noexcept {
+ // remove the invalid exponent bias
+ am.power2 -= invalid_am_bias;
+
+ int32_t sci_exp = scientific_exponent(num);
+ size_t max_digits = binary_format<T>::max_digits();
+ size_t digits = 0;
+ bigint bigmant;
+ parse_mantissa(bigmant, num, max_digits, digits);
+ // can't underflow, since digits is at most max_digits.
+ int32_t exponent = sci_exp + 1 - int32_t(digits);
+ if (exponent >= 0) {
+ return positive_digit_comp<T>(bigmant, exponent);
+ } else {
+ return negative_digit_comp<T>(bigmant, am, exponent);
+ }
+ }
+
+} // namespace fast_float
+
+#endif
+
+#ifndef FASTFLOAT_PARSE_NUMBER_H
+#define FASTFLOAT_PARSE_NUMBER_H
+
+
+#include <cmath>
+#include <cstring>
+#include <limits>
+#include <system_error>
+
+namespace fast_float {
+
+
+ namespace detail {
+ /**
+ * Special case +inf, -inf, nan, infinity, -infinity.
+ * The case comparisons could be made much faster given that we know that the
+ * strings a null-free and fixed.
+ **/
+ template <typename T>
+ from_chars_result parse_infnan(const char *first, const char *last, T &value) noexcept {
+ from_chars_result answer;
+ answer.ptr = first;
+ answer.ec = std::errc(); // be optimistic
+ bool minusSign = false;
+ if (*first == '-') { // assume first < last, so dereference without checks; C++17 20.19.3.(7.1) explicitly forbids '+' here
+ minusSign = true;
+ ++first;
+ }
+ if (last - first >= 3) {
+ if (fastfloat_strncasecmp(first, "nan", 3)) {
+ answer.ptr = (first += 3);
+ value = minusSign ? -std::numeric_limits<T>::quiet_NaN() : std::numeric_limits<T>::quiet_NaN();
+ // Check for possible nan(n-char-seq-opt), C++17 20.19.3.7, C11 7.20.1.3.3. At least MSVC produces nan(ind) and nan(snan).
+ if(first != last && *first == '(') {
+ for(const char* ptr = first + 1; ptr != last; ++ptr) {
+ if (*ptr == ')') {
+ answer.ptr = ptr + 1; // valid nan(n-char-seq-opt)
+ break;
+ }
+ else if(!(('a' <= *ptr && *ptr <= 'z') || ('A' <= *ptr && *ptr <= 'Z') || ('0' <= *ptr && *ptr <= '9') || *ptr == '_'))
+ break; // forbidden char, not nan(n-char-seq-opt)
+ }
+ }
+ return answer;
+ }
+ if (fastfloat_strncasecmp(first, "inf", 3)) {
+ if ((last - first >= 8) && fastfloat_strncasecmp(first + 3, "inity", 5)) {
+ answer.ptr = first + 8;
+ } else {
+ answer.ptr = first + 3;
+ }
+ value = minusSign ? -std::numeric_limits<T>::infinity() : std::numeric_limits<T>::infinity();
+ return answer;
+ }
+ }
+ answer.ec = std::errc::invalid_argument;
+ return answer;
+ }
+
+ } // namespace detail
+
+ template<typename T>
+ from_chars_result from_chars(const char *first, const char *last,
+ T &value, chars_format fmt /*= chars_format::general*/) noexcept {
+ return from_chars_advanced(first, last, value, parse_options{fmt});
+ }
+
+ template<typename T>
+ from_chars_result from_chars_advanced(const char *first, const char *last,
+ T &value, parse_options options) noexcept {
+
+ static_assert (std::is_same<T, double>::value || std::is_same<T, float>::value, "only float and double are supported");
+
+
+ from_chars_result answer;
+ if (first == last) {
+ answer.ec = std::errc::invalid_argument;
+ answer.ptr = first;
+ return answer;
+ }
+ parsed_number_string pns = parse_number_string(first, last, options);
+ if (!pns.valid) {
+ return detail::parse_infnan(first, last, value);
+ }
+ answer.ec = std::errc(); // be optimistic
+ answer.ptr = pns.lastmatch;
+ // Next is Clinger's fast path.
+ if (binary_format<T>::min_exponent_fast_path() <= pns.exponent && pns.exponent <= binary_format<T>::max_exponent_fast_path() && pns.mantissa <=binary_format<T>::max_mantissa_fast_path() && !pns.too_many_digits) {
+ value = T(pns.mantissa);
+ if (pns.exponent < 0) { value = value / binary_format<T>::exact_power_of_ten(-pns.exponent); }
+ else { value = value * binary_format<T>::exact_power_of_ten(pns.exponent); }
+ if (pns.negative) { value = -value; }
+ return answer;
+ }
+ adjusted_mantissa am = compute_float<binary_format<T>>(pns.exponent, pns.mantissa);
+ if(pns.too_many_digits && am.power2 >= 0) {
+ if(am != compute_float<binary_format<T>>(pns.exponent, pns.mantissa + 1)) {
+ am = compute_error<binary_format<T>>(pns.exponent, pns.mantissa);
+ }
+ }
+ // If we called compute_float<binary_format<T>>(pns.exponent, pns.mantissa) and we have an invalid power (am.power2 < 0),
+ // then we need to go the long way around again. This is very uncommon.
+ if(am.power2 < 0) { am = digit_comp<T>(pns, am); }
+ to_float(pns.negative, am, value);
+ return answer;
+ }
+
+} // namespace fast_float
+
+#endif
+
diff --git a/src/third-party/scnlib/src/file.cpp b/src/third-party/scnlib/src/file.cpp
new file mode 100644
index 0000000..ec53145
--- /dev/null
+++ b/src/third-party/scnlib/src/file.cpp
@@ -0,0 +1,311 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY
+#define SCN_FILE_CPP
+#endif
+
+#include <scn/detail/error.h>
+#include <scn/detail/file.h>
+#include <scn/util/expected.h>
+
+#include <cstdio>
+
+#if SCN_POSIX
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#elif SCN_WINDOWS
+
+#ifdef WIN32_LEAN_AND_MEAN
+#define SCN_WIN32_LEAN_DEFINED 1
+#else
+#define WIN32_LEAN_AND_MEAN
+#define SCN_WIN32_LEAN_DEFINED 0
+#endif
+
+#ifdef NOMINMAX
+#define SCN_NOMINMAX_DEFINED 1
+#else
+#define NOMINMAX
+#define SCN_NOMINMAX_DEFINED 0
+#endif
+
+#include <Windows.h>
+
+#if !SCN_NOMINMAX_DEFINED
+#undef NOMINMAX
+#endif
+
+#if !SCN_WIN32_LEAN_DEFINED
+#undef WIN32_LEAN_AND_MEAN
+#endif
+
+#endif
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ SCN_FUNC native_file_handle native_file_handle::invalid()
+ {
+#if SCN_WINDOWS
+ return {INVALID_HANDLE_VALUE};
+#else
+ return {-1};
+#endif
+ }
+
+ SCN_FUNC byte_mapped_file::byte_mapped_file(const char* filename)
+ {
+#if SCN_POSIX
+ int fd = open(filename, O_RDONLY);
+ if (fd == -1) {
+ return;
+ }
+
+ struct stat s {
+ };
+ int status = fstat(fd, &s);
+ if (status == -1) {
+ close(fd);
+ return;
+ }
+ auto size = s.st_size;
+
+ auto ptr =
+ static_cast<char*>(mmap(nullptr, static_cast<size_t>(size),
+ PROT_READ, MAP_PRIVATE, fd, 0));
+ if (ptr == MAP_FAILED) {
+ close(fd);
+ return;
+ }
+
+ m_file.handle = fd;
+ m_map = span<char>{ptr, static_cast<size_t>(size)};
+#elif SCN_WINDOWS
+ auto f = ::CreateFileA(
+ filename, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
+ nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
+ if (f == INVALID_HANDLE_VALUE) {
+ return;
+ }
+
+ LARGE_INTEGER _size;
+ if (::GetFileSizeEx(f, &_size) == 0) {
+ ::CloseHandle(f);
+ return;
+ }
+ auto size = static_cast<size_t>(_size.QuadPart);
+
+ auto h = ::CreateFileMappingA(
+ f, nullptr, PAGE_READONLY,
+#ifdef _WIN64
+ static_cast<DWORD>(size >> 32ull),
+#else
+ DWORD{0},
+#endif
+ static_cast<DWORD>(size & 0xffffffffull), nullptr);
+ if (h == INVALID_HANDLE_VALUE || h == nullptr) {
+ ::CloseHandle(f);
+ return;
+ }
+
+ auto start = ::MapViewOfFile(h, FILE_MAP_READ, 0, 0, size);
+ if (!start) {
+ ::CloseHandle(h);
+ ::CloseHandle(f);
+ return;
+ }
+
+ m_file.handle = f;
+ m_map_handle.handle = h;
+ m_map = span<char>{static_cast<char*>(start), size};
+#else
+ SCN_UNUSED(filename);
+#endif
+ }
+
+ SCN_FUNC void byte_mapped_file::_destruct()
+ {
+#if SCN_POSIX
+ munmap(m_map.data(), m_map.size());
+ close(m_file.handle);
+#elif SCN_WINDOWS
+ ::CloseHandle(m_map_handle.handle);
+ ::CloseHandle(m_file.handle);
+ m_map_handle = native_file_handle::invalid();
+#endif
+
+ m_file = native_file_handle::invalid();
+ m_map = span<char>{};
+
+ SCN_ENSURE(!valid());
+ }
+
+ } // namespace detail
+
+ namespace detail {
+ template <typename CharT>
+ struct basic_file_iterator_access {
+ using iterator = typename basic_file<CharT>::iterator;
+
+ basic_file_iterator_access(const iterator& it) : self(it) {}
+
+ SCN_NODISCARD expected<CharT> deref() const
+ {
+ SCN_EXPECT(self.m_file);
+
+ if (self.m_file->m_buffer.empty()) {
+ // no chars have been read
+ return self.m_file->_read_single();
+ }
+ if (!self.m_last_error) {
+ // last read failed
+ return self.m_last_error;
+ }
+ return self.m_file->_get_char_at(self.m_current);
+ }
+
+ SCN_NODISCARD bool eq(const iterator& o) const
+ {
+ if (self.m_file && (self.m_file == o.m_file || !o.m_file)) {
+ if (self.m_file->_is_at_end(self.m_current) &&
+ self.m_last_error.code() != error::end_of_range &&
+ !o.m_file) {
+ self.m_last_error = error{};
+ auto r = self.m_file->_read_single();
+ if (!r) {
+ self.m_last_error = r.error();
+ return !o.m_file || self.m_current == o.m_current ||
+ o.m_last_error.code() == error::end_of_range;
+ }
+ }
+ }
+
+ // null file == null file
+ if (!self.m_file && !o.m_file) {
+ return true;
+ }
+ // null file == eof file
+ if (!self.m_file && o.m_file) {
+ // lhs null, rhs potentially eof
+ return o.m_last_error.code() == error::end_of_range;
+ }
+ // eof file == null file
+ if (self.m_file && !o.m_file) {
+ // rhs null, lhs potentially eof
+ return self.m_last_error.code() == error::end_of_range;
+ }
+ // eof file == eof file
+ if (self.m_last_error == o.m_last_error &&
+ self.m_last_error.code() == error::end_of_range) {
+ return true;
+ }
+
+ return self.m_file == o.m_file && self.m_current == o.m_current;
+ }
+
+ const iterator& self;
+ };
+ } // namespace detail
+
+ template <>
+ SCN_FUNC expected<char> basic_file<char>::iterator::operator*() const
+ {
+ return detail::basic_file_iterator_access<char>(*this).deref();
+ }
+ template <>
+ SCN_FUNC expected<wchar_t> basic_file<wchar_t>::iterator::operator*() const
+ {
+ return detail::basic_file_iterator_access<wchar_t>(*this).deref();
+ }
+
+ template <>
+ SCN_FUNC bool basic_file<char>::iterator::operator==(
+ const basic_file<char>::iterator& o) const
+ {
+ return detail::basic_file_iterator_access<char>(*this).eq(o);
+ }
+ template <>
+ SCN_FUNC bool basic_file<wchar_t>::iterator::operator==(
+ const basic_file<wchar_t>::iterator& o) const
+ {
+ return detail::basic_file_iterator_access<wchar_t>(*this).eq(o);
+ }
+
+ template <>
+ SCN_FUNC expected<char> file::_read_single() const
+ {
+ SCN_EXPECT(valid());
+ int tmp = std::fgetc(m_file);
+ if (tmp == EOF) {
+ if (std::feof(m_file) != 0) {
+ return error(error::end_of_range, "EOF");
+ }
+ if (std::ferror(m_file) != 0) {
+ return error(error::source_error, "fgetc error");
+ }
+ return error(error::unrecoverable_source_error,
+ "Unknown fgetc error");
+ }
+ auto ch = static_cast<char>(tmp);
+ m_buffer.push_back(ch);
+ return ch;
+ }
+ template <>
+ SCN_FUNC expected<wchar_t> wfile::_read_single() const
+ {
+ SCN_EXPECT(valid());
+ wint_t tmp = std::fgetwc(m_file);
+ if (tmp == WEOF) {
+ if (std::feof(m_file) != 0) {
+ return error(error::end_of_range, "EOF");
+ }
+ if (std::ferror(m_file) != 0) {
+ return error(error::source_error, "fgetc error");
+ }
+ return error(error::unrecoverable_source_error,
+ "Unknown fgetc error");
+ }
+ auto ch = static_cast<wchar_t>(tmp);
+ m_buffer.push_back(ch);
+ return ch;
+ }
+
+ template <>
+ SCN_FUNC void file::_sync_until(std::size_t pos) noexcept
+ {
+ for (auto it = m_buffer.rbegin();
+ it != m_buffer.rend() - static_cast<std::ptrdiff_t>(pos); ++it) {
+ std::ungetc(static_cast<unsigned char>(*it), m_file);
+ }
+ }
+ template <>
+ SCN_FUNC void wfile::_sync_until(std::size_t pos) noexcept
+ {
+ for (auto it = m_buffer.rbegin();
+ it != m_buffer.rend() - static_cast<std::ptrdiff_t>(pos); ++it) {
+ std::ungetwc(static_cast<wint_t>(*it), m_file);
+ }
+ }
+
+ SCN_END_NAMESPACE
+} // namespace scn
diff --git a/src/third-party/scnlib/src/locale.cpp b/src/third-party/scnlib/src/locale.cpp
new file mode 100644
index 0000000..bc628e0
--- /dev/null
+++ b/src/third-party/scnlib/src/locale.cpp
@@ -0,0 +1,668 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY
+#define SCN_LOCALE_CPP
+#endif
+
+#include <scn/detail/locale.h>
+#include <scn/util/math.h>
+
+#include <cctype>
+#include <cmath>
+#include <cwchar>
+#include <iomanip>
+#include <locale>
+#include <sstream>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ template <typename CharT>
+ struct locale_data {
+ using char_type = CharT;
+ using string_type = std::basic_string<char_type>;
+
+ std::locale global_locale{std::locale()};
+ std::locale classic_locale{std::locale::classic()};
+
+ string_type truename{};
+ string_type falsename{};
+ char_type decimal_point{};
+ char_type thousands_separator{};
+ };
+
+ template <typename CharT>
+ const std::locale& to_locale(const basic_custom_locale_ref<CharT>& l)
+ {
+ SCN_EXPECT(l.get_locale());
+ return *static_cast<const std::locale*>(l.get_locale());
+ }
+
+ // Buggy on gcc 5 and 6
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wmaybe-uninitialized")
+
+ template <typename CharT>
+ basic_custom_locale_ref<CharT>::basic_custom_locale_ref()
+ {
+ auto data = new locale_data<CharT>{};
+ m_data = data;
+ m_locale = &data->global_locale;
+ _initialize();
+ }
+ template <typename CharT>
+ basic_custom_locale_ref<CharT>::basic_custom_locale_ref(
+ const void* locale)
+ : m_locale(locale)
+ {
+ auto data = new locale_data<CharT>{};
+ m_data = data;
+ if (!locale) {
+ m_locale = &data->global_locale;
+ }
+ _initialize();
+ }
+
+ SCN_GCC_POP
+
+ template <typename CharT>
+ void basic_custom_locale_ref<CharT>::_initialize()
+ {
+ const auto& facet =
+ std::use_facet<std::numpunct<CharT>>(to_locale(*this));
+
+ auto& data = *static_cast<locale_data<CharT>*>(m_data);
+ data.truename = facet.truename();
+ data.falsename = facet.falsename();
+ data.decimal_point = facet.decimal_point();
+ data.thousands_separator = facet.thousands_sep();
+ }
+
+ template <typename CharT>
+ basic_custom_locale_ref<CharT>::basic_custom_locale_ref(
+ basic_custom_locale_ref&& o)
+ {
+ m_data = o.m_data;
+ m_locale = o.m_locale;
+
+ o.m_data = nullptr;
+ o.m_locale = nullptr;
+
+ _initialize();
+ }
+ template <typename CharT>
+ auto basic_custom_locale_ref<CharT>::operator=(
+ basic_custom_locale_ref&& o) -> basic_custom_locale_ref&
+ {
+ delete static_cast<locale_data<CharT>*>(m_data);
+
+ m_data = o.m_data;
+ m_locale = o.m_locale;
+
+ o.m_data = nullptr;
+ o.m_locale = nullptr;
+
+ _initialize();
+
+ return *this;
+ }
+
+ template <typename CharT>
+ basic_custom_locale_ref<CharT>::~basic_custom_locale_ref()
+ {
+ delete static_cast<locale_data<CharT>*>(m_data);
+ }
+
+ template <typename CharT>
+ auto basic_custom_locale_ref<CharT>::make_classic()
+ -> basic_custom_locale_ref
+ {
+ basic_custom_locale_ref loc{};
+ loc.convert_to_classic();
+ return loc;
+ }
+
+ template <typename CharT>
+ void basic_custom_locale_ref<CharT>::convert_to_classic()
+ {
+ m_locale =
+ &static_cast<locale_data<CharT>*>(m_data)->classic_locale;
+ }
+ template <typename CharT>
+ void basic_custom_locale_ref<CharT>::convert_to_global()
+ {
+ SCN_EXPECT(m_data);
+ m_locale = &static_cast<locale_data<CharT>*>(m_data)->global_locale;
+ }
+
+ template <typename CharT>
+ bool basic_custom_locale_ref<CharT>::do_is_space(char_type ch) const
+ {
+ return std::isspace(ch, to_locale(*this));
+ }
+ template <typename CharT>
+ bool basic_custom_locale_ref<CharT>::do_is_digit(char_type ch) const
+ {
+ return std::isdigit(ch, to_locale(*this));
+ }
+
+ template <typename CharT>
+ auto basic_custom_locale_ref<CharT>::do_decimal_point() const
+ -> char_type
+ {
+ return static_cast<locale_data<CharT>*>(m_data)->decimal_point;
+ }
+ template <typename CharT>
+ auto basic_custom_locale_ref<CharT>::do_thousands_separator() const
+ -> char_type
+ {
+ return static_cast<locale_data<CharT>*>(m_data)
+ ->thousands_separator;
+ }
+ template <typename CharT>
+ auto basic_custom_locale_ref<CharT>::do_truename() const
+ -> string_view_type
+ {
+ const auto& str =
+ static_cast<locale_data<CharT>*>(m_data)->truename;
+ return {str.data(), str.size()};
+ }
+ template <typename CharT>
+ auto basic_custom_locale_ref<CharT>::do_falsename() const
+ -> string_view_type
+ {
+ const auto& str =
+ static_cast<locale_data<CharT>*>(m_data)->falsename;
+ return {str.data(), str.size()};
+ }
+
+ static inline error convert_to_wide_impl(const std::locale&,
+ const char*,
+ const char*,
+ const char*&,
+ wchar_t*,
+ wchar_t*,
+ wchar_t*&)
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+ static inline error convert_to_wide_impl(const std::locale&,
+ const wchar_t*,
+ const wchar_t*,
+ const wchar_t*&,
+ wchar_t*,
+ wchar_t*,
+ wchar_t*&)
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+
+ template <typename CharT>
+ error basic_custom_locale_ref<CharT>::convert_to_wide(
+ const CharT* from_begin,
+ const CharT* from_end,
+ const CharT*& from_next,
+ wchar_t* to_begin,
+ wchar_t* to_end,
+ wchar_t*& to_next) const
+ {
+ return convert_to_wide_impl(to_locale(*this), from_begin, from_end,
+ from_next, to_begin, to_end, to_next);
+ }
+
+ static inline expected<wchar_t> convert_to_wide_impl(const std::locale&,
+ const char*,
+ const char*)
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+ static inline expected<wchar_t> convert_to_wide_impl(const std::locale&,
+ const wchar_t*,
+ const wchar_t*)
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+
+ template <typename CharT>
+ expected<wchar_t> basic_custom_locale_ref<CharT>::convert_to_wide(
+ const CharT* from_begin,
+ const CharT* from_end) const
+ {
+ return convert_to_wide_impl(to_locale(*this), from_begin, from_end);
+ }
+
+ template <typename CharT>
+ bool basic_custom_locale_ref<CharT>::do_is_space(
+ span<const char_type> ch) const
+ {
+ const auto& locale = to_locale(*this);
+ if (sizeof(CharT) == 1) {
+ SCN_EXPECT(ch.size() >= 1);
+ code_point cp{};
+ auto it = parse_code_point(ch.begin(), ch.end(), cp);
+ SCN_EXPECT(it);
+ return is_space(cp);
+ }
+ SCN_EXPECT(ch.size() == 1);
+ return std::isspace(ch[0], locale);
+ }
+ template <typename CharT>
+ bool basic_custom_locale_ref<CharT>::do_is_digit(
+ span<const char_type> ch) const
+ {
+ const auto& locale = to_locale(*this);
+ if (sizeof(CharT) == 1) {
+ SCN_EXPECT(ch.size() >= 1);
+ code_point cp{};
+ auto it = parse_code_point(ch.begin(), ch.end(), cp);
+ SCN_EXPECT(it);
+ return is_digit(cp);
+ }
+ SCN_EXPECT(ch.size() == 1);
+ return std::isdigit(ch[0], locale);
+ }
+
+#define SCN_DEFINE_CUSTOM_LOCALE_CTYPE(f) \
+ template <typename CharT> \
+ bool basic_custom_locale_ref<CharT>::is_##f(char_type ch) const \
+ { \
+ return std::is##f(ch, to_locale(*this)); \
+ } \
+ template <typename CharT> \
+ bool basic_custom_locale_ref<CharT>::is_##f(code_point cp) const \
+ { \
+ return std::is##f(static_cast<wchar_t>(cp), to_locale(*this)); \
+ } \
+ template <typename CharT> \
+ bool basic_custom_locale_ref<CharT>::is_##f(span<const char_type> ch) \
+ const \
+ { \
+ const auto& locale = to_locale(*this); \
+ if (sizeof(CharT) == 1) { \
+ SCN_EXPECT(ch.size() >= 1); \
+ code_point cp{}; \
+ auto it = parse_code_point(ch.begin(), ch.end(), cp); \
+ SCN_EXPECT(it); \
+ return is_##f(cp); \
+ } \
+ SCN_EXPECT(ch.size() == 1); \
+ return std::is##f(ch[0], locale); \
+ }
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(alnum)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(alpha)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(cntrl)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(graph)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(lower)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(print)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(punct)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(upper)
+ SCN_DEFINE_CUSTOM_LOCALE_CTYPE(xdigit)
+#undef SCN_DEFINE_CUSTOM_LOCALE_CTYPE
+
+ template <typename CharT>
+ bool basic_custom_locale_ref<CharT>::is_space(code_point cp) const
+ {
+ return std::isspace(static_cast<wchar_t>(cp), to_locale(*this));
+ }
+ template <typename CharT>
+ bool basic_custom_locale_ref<CharT>::is_digit(code_point cp) const
+ {
+ return std::isdigit(static_cast<wchar_t>(cp), to_locale(*this));
+ }
+
+ // For some reason, there's no isblank in libc++
+ template <typename CharT>
+ bool basic_custom_locale_ref<CharT>::is_blank(char_type ch) const
+ {
+ return std::use_facet<std::ctype<CharT>>(to_locale(*this))
+ .is(std::ctype_base::blank, ch);
+ }
+ template <typename CharT>
+ bool basic_custom_locale_ref<CharT>::is_blank(code_point ch) const
+ {
+ return std::use_facet<std::ctype<wchar_t>>(to_locale(*this))
+ .is(std::ctype_base::blank, static_cast<wchar_t>(ch));
+ }
+ template <typename CharT>
+ bool basic_custom_locale_ref<CharT>::is_blank(
+ span<const char_type> ch) const
+ {
+ const auto& locale = to_locale(*this);
+ if (sizeof(CharT) == 1) {
+ SCN_EXPECT(ch.size() >= 1);
+ code_point cp{};
+ auto it = parse_code_point(ch.begin(), ch.end(), cp);
+ SCN_EXPECT(it);
+ return is_blank(cp);
+ }
+ SCN_EXPECT(ch.size() == 1);
+ return std::use_facet<std::ctype<CharT>>(locale).is(
+ std::ctype_base::blank, ch[0]);
+ }
+
+ template <typename T>
+ auto read_num_check_range(T val) ->
+ typename std::enable_if<std::is_integral<T>::value, error>::type
+ {
+ if (val == std::numeric_limits<T>::max()) {
+ return error(error::value_out_of_range,
+ "Scanned number out of range: overflow");
+ }
+ if (val == std::numeric_limits<T>::min()) {
+ return error(error::value_out_of_range,
+ "Scanned number out of range: underflow");
+ }
+ return error(error::invalid_scanned_value,
+ "Localized number read failed");
+ }
+ template <typename T>
+ auto read_num_check_range(T val) ->
+ typename std::enable_if<std::is_floating_point<T>::value,
+ error>::type
+ {
+ SCN_GCC_COMPAT_PUSH
+ SCN_GCC_COMPAT_IGNORE("-Wfloat-equal")
+ if (val == std::numeric_limits<T>::max() ||
+ val == -std::numeric_limits<T>::max()) {
+ return error(error::value_out_of_range,
+ "Scanned number out of range: overflow");
+ }
+ if (val == zero_value<T>::value) {
+ return error(error::value_out_of_range,
+ "Scanned number out of range: underflow");
+ }
+ SCN_GCC_COMPAT_POP
+ return error(error::invalid_scanned_value,
+ "Localized number read failed");
+ }
+
+ template <typename T, typename CharT>
+ error do_read_num_impl(T& val, std::basic_istringstream<CharT>& ss)
+ {
+ ss >> val;
+ return {};
+ }
+ template <typename CharT>
+ error do_read_num_impl(CharT& val, std::basic_istringstream<CharT>& ss)
+ {
+ long long tmp;
+ if (!(ss >> tmp)) {
+ return {};
+ }
+ if (tmp > std::numeric_limits<CharT>::max()) {
+ return {error::value_out_of_range,
+ "Scanned number out of range: overflow"};
+ }
+ if (tmp < std::numeric_limits<CharT>::min()) {
+ return {error::value_out_of_range,
+ "Scanned number out of range: underflow"};
+ }
+ val = static_cast<CharT>(tmp);
+ return {};
+ }
+ template <typename CharT>
+ error do_read_num_impl(signed char& val,
+ std::basic_istringstream<CharT>& ss)
+ {
+ int tmp;
+ if (!(ss >> tmp)) {
+ return {};
+ }
+ if (tmp > std::numeric_limits<signed char>::max()) {
+ return {error::value_out_of_range,
+ "Scanned number out of range: overflow"};
+ }
+ if (tmp < std::numeric_limits<signed char>::min()) {
+ return {error::value_out_of_range,
+ "Scanned number out of range: underflow"};
+ }
+ val = static_cast<signed char>(tmp);
+ return {};
+ }
+ template <typename CharT>
+ error do_read_num_impl(unsigned char& val,
+ std::basic_istringstream<CharT>& ss)
+ {
+ int tmp;
+ if (!(ss >> tmp)) {
+ return {};
+ }
+ if (tmp > std::numeric_limits<unsigned char>::max()) {
+ return {error::value_out_of_range,
+ "Scanned number out of range: overflow"};
+ }
+ if (tmp < 0) {
+ return {error::value_out_of_range,
+ "Scanned number out of range: underflow"};
+ }
+ val = static_cast<unsigned char>(tmp);
+ return {};
+ }
+
+ template <typename T, typename CharT>
+ expected<std::ptrdiff_t> do_read_num(
+ T& val,
+ const std::locale& loc,
+ const std::basic_string<CharT>& buf,
+ int base)
+ {
+#if SCN_HAS_EXCEPTIONS
+ std::basic_istringstream<CharT> ss(buf);
+ ss.imbue(loc);
+ ss >> std::setbase(base);
+
+ try {
+ T tmp;
+ auto e = do_read_num_impl(tmp, ss);
+ if (ss.bad()) {
+ return error(error::unrecoverable_internal_error,
+ "Localized stringstream is bad");
+ }
+ if (!e) {
+ return e;
+ }
+ if (ss.fail()) {
+ return read_num_check_range(tmp);
+ }
+ val = tmp;
+ }
+ catch (const std::ios_base::failure& f) {
+ return error(error::invalid_scanned_value, f.what());
+ }
+ return ss.eof() ? static_cast<std::ptrdiff_t>(buf.size())
+ : static_cast<std::ptrdiff_t>(ss.tellg());
+#else
+ SCN_UNUSED(val);
+ SCN_UNUSED(loc);
+ SCN_UNUSED(buf);
+ return error(error::exceptions_required,
+ "Localized number reading is only supported with "
+ "exceptions enabled");
+#endif
+ }
+
+ template <>
+ expected<std::ptrdiff_t> do_read_num(wchar_t&,
+ const std::locale&,
+ const std::string&,
+ int)
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+ template <>
+ expected<std::ptrdiff_t> do_read_num(char&,
+ const std::locale&,
+ const std::wstring&,
+ int)
+ {
+ SCN_EXPECT(false);
+ SCN_UNREACHABLE;
+ }
+
+ template <typename CharT>
+ template <typename T>
+ expected<std::ptrdiff_t> basic_custom_locale_ref<CharT>::read_num(
+ T& val,
+ const string_type& buf,
+ int b) const
+ {
+ return do_read_num<T, CharT>(val, to_locale(*this), buf, b);
+ }
+
+#if SCN_INCLUDE_SOURCE_DEFINITIONS
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wpadded")
+ SCN_CLANG_IGNORE("-Wweak-template-vtables")
+ template class basic_custom_locale_ref<char>;
+ template class basic_custom_locale_ref<wchar_t>;
+ SCN_CLANG_POP
+
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<signed char>(signed char&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<short>(short&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<int>(int&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<long>(long&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<long long>(long long&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ char>::read_num<unsigned char>(unsigned char&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ char>::read_num<unsigned short>(unsigned short&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ char>::read_num<unsigned int>(unsigned int&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ char>::read_num<unsigned long>(unsigned long&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ char>::read_num<unsigned long long>(unsigned long long&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<char>(char&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<wchar_t>(wchar_t&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<float>(float&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<double>(double&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<char>::read_num<long double>(long double&,
+ const string_type&,
+ int) const;
+
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ wchar_t>::read_num<signed char>(signed char&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<wchar_t>::read_num<short>(short&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<wchar_t>::read_num<int>(int&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<wchar_t>::read_num<long>(long&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ wchar_t>::read_num<long long>(long long&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ wchar_t>::read_num<unsigned char>(unsigned char&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ wchar_t>::read_num<unsigned short>(unsigned short&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ wchar_t>::read_num<unsigned int>(unsigned int&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ wchar_t>::read_num<unsigned long>(unsigned long&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ wchar_t>::read_num<unsigned long long>(unsigned long long&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<wchar_t>::read_num<float>(float&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<wchar_t>::read_num<double>(double&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t> basic_custom_locale_ref<
+ wchar_t>::read_num<long double>(long double&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<wchar_t>::read_num<char>(char&,
+ const string_type&,
+ int) const;
+ template expected<std::ptrdiff_t>
+ basic_custom_locale_ref<wchar_t>::read_num<wchar_t>(wchar_t&,
+ const string_type&,
+ int) const;
+#endif
+
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
diff --git a/src/third-party/scnlib/src/reader_float.cpp b/src/third-party/scnlib/src/reader_float.cpp
new file mode 100644
index 0000000..77c66a5
--- /dev/null
+++ b/src/third-party/scnlib/src/reader_float.cpp
@@ -0,0 +1,433 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY
+#define SCN_READER_FLOAT_CPP
+#endif
+
+#include <scn/detail/args.h>
+#include <scn/reader/float.h>
+
+#include <cerrno>
+#include <clocale>
+
+#if SCN_HAS_FLOAT_CHARCONV
+#include <charconv>
+#endif
+
+SCN_GCC_PUSH
+SCN_GCC_IGNORE("-Wold-style-cast")
+SCN_GCC_IGNORE("-Wnoexcept")
+SCN_GCC_IGNORE("-Wshift-count-overflow")
+SCN_GCC_IGNORE("-Wsign-conversion")
+
+SCN_CLANG_PUSH
+SCN_CLANG_IGNORE("-Wold-style-cast")
+
+#if SCN_CLANG >= SCN_COMPILER(13, 0, 0)
+SCN_CLANG_IGNORE("-Wreserved-identifier")
+#endif
+
+#if SCN_CLANG >= SCN_COMPILER(10, 0, 0)
+SCN_CLANG_IGNORE("-Wextra-semi-stmt")
+#endif
+
+#include <fast_float/fast_float.h>
+
+SCN_CLANG_POP
+SCN_GCC_POP
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace read_float {
+ static bool is_hexfloat(const char* str, std::size_t len) noexcept
+ {
+ if (len < 3) {
+ return false;
+ }
+ return str[0] == '0' && (str[1] == 'x' || str[1] == 'X');
+ }
+ static bool is_hexfloat(const wchar_t* str, std::size_t len) noexcept
+ {
+ if (len < 3) {
+ return false;
+ }
+ return str[0] == L'0' && (str[1] == L'x' || str[1] == L'X');
+ }
+
+ namespace cstd {
+#if SCN_GCC >= SCN_COMPILER(7, 0, 0)
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wnoexcept-type")
+#endif
+ template <typename T, typename CharT, typename F>
+ expected<T> impl(F&& f_strtod,
+ T huge_value,
+ const CharT* str,
+ size_t& chars,
+ uint8_t options)
+ {
+ // Get current C locale
+ const auto loc = std::setlocale(LC_NUMERIC, nullptr);
+ // For whatever reason, this cannot be stored in the heap if
+ // setlocale hasn't been called before, or msan errors with
+ // 'use-of-unitialized-value' when resetting the locale back.
+ // POSIX specifies that the content of loc may not be static, so
+ // we need to save it ourselves
+ char locbuf[64] = {0};
+ std::strcpy(locbuf, loc);
+
+ std::setlocale(LC_NUMERIC, "C");
+
+ CharT* end{};
+ errno = 0;
+ T f = f_strtod(str, &end);
+ chars = static_cast<size_t>(end - str);
+ auto err = errno;
+ // Reset locale
+ std::setlocale(LC_NUMERIC, locbuf);
+ errno = 0;
+
+ SCN_GCC_COMPAT_PUSH
+ SCN_GCC_COMPAT_IGNORE("-Wfloat-equal")
+ // No conversion
+ if (f == detail::zero_value<T>::value && chars == 0) {
+ return error(error::invalid_scanned_value, "strtod");
+ }
+ // Range error
+ if (err == ERANGE) {
+ // Underflow
+ if (f == detail::zero_value<T>::value) {
+ return error(
+ error::value_out_of_range,
+ "Floating-point value out of range: underflow");
+ }
+ // Overflow
+ if (f == huge_value || f == -huge_value) {
+ return error(
+ error::value_out_of_range,
+ "Floating-point value out of range: overflow");
+ }
+ // Subnormals cause ERANGE but a value is still returned
+ }
+
+ if (is_hexfloat(str, detail::strlen(str)) &&
+ (options & detail::float_scanner<T>::allow_hex) == 0) {
+ return error(error::invalid_scanned_value,
+ "Hexfloats not allowed by the format string");
+ }
+
+ SCN_GCC_COMPAT_POP
+ return f;
+ }
+#if SCN_GCC >= SCN_COMPILER(7, 0, 0)
+ SCN_GCC_POP
+#endif
+
+ template <typename CharT, typename T>
+ struct read;
+
+ template <>
+ struct read<char, float> {
+ static expected<float> get(const char* str,
+ size_t& chars,
+ uint8_t options)
+ {
+ return impl<float>(strtof, HUGE_VALF, str, chars, options);
+ }
+ };
+
+ template <>
+ struct read<char, double> {
+ static expected<double> get(const char* str,
+ size_t& chars,
+ uint8_t options)
+ {
+ return impl<double>(strtod, HUGE_VAL, str, chars, options);
+ }
+ };
+
+ template <>
+ struct read<char, long double> {
+ static expected<long double> get(const char* str,
+ size_t& chars,
+ uint8_t options)
+ {
+ return impl<long double>(strtold, HUGE_VALL, str, chars,
+ options);
+ }
+ };
+
+ template <>
+ struct read<wchar_t, float> {
+ static expected<float> get(const wchar_t* str,
+ size_t& chars,
+ uint8_t options)
+ {
+ return impl<float>(wcstof, HUGE_VALF, str, chars, options);
+ }
+ };
+ template <>
+ struct read<wchar_t, double> {
+ static expected<double> get(const wchar_t* str,
+ size_t& chars,
+ uint8_t options)
+ {
+ return impl<double>(wcstod, HUGE_VAL, str, chars, options);
+ }
+ };
+ template <>
+ struct read<wchar_t, long double> {
+ static expected<long double> get(const wchar_t* str,
+ size_t& chars,
+ uint8_t options)
+ {
+ return impl<long double>(wcstold, HUGE_VALL, str, chars,
+ options);
+ }
+ };
+ } // namespace cstd
+
+ namespace from_chars {
+#if SCN_HAS_FLOAT_CHARCONV
+ template <typename T>
+ struct read {
+ static expected<T> get(const char* str,
+ size_t& chars,
+ uint8_t options)
+ {
+ const auto len = std::strlen(str);
+ std::chars_format flags{};
+ if (((options & detail::float_scanner<T>::allow_hex) !=
+ 0) &&
+ is_hexfloat(str, len)) {
+ str += 2;
+ flags = std::chars_format::hex;
+ }
+ else {
+ if ((options & detail::float_scanner<T>::allow_fixed) !=
+ 0) {
+ flags |= std::chars_format::fixed;
+ }
+ if ((options &
+ detail::float_scanner<T>::allow_scientific) != 0) {
+ flags |= std::chars_format::scientific;
+ }
+ }
+ if (flags == static_cast<std::chars_format>(0)) {
+ return error{error::invalid_scanned_value,
+ "Expected a hexfloat"};
+ }
+
+ T value{};
+ const auto result =
+ std::from_chars(str, str + len, value, flags);
+ if (result.ec == std::errc::invalid_argument) {
+ return error(error::invalid_scanned_value,
+ "from_chars");
+ }
+ if (result.ec == std::errc::result_out_of_range) {
+ // Out of range, may be subnormal -> fall back to strtod
+ // On gcc std::from_chars doesn't parse subnormals
+ return cstd::read<char, T>::get(str, chars, options);
+ }
+ chars = static_cast<size_t>(result.ptr - str);
+ return value;
+ }
+ };
+#else
+ template <typename T>
+ struct read {
+ static expected<T> get(const char* str,
+ size_t& chars,
+ uint8_t options)
+ {
+ // Fall straight back to strtod
+ return cstd::read<char, T>::get(str, chars, options);
+ }
+ };
+#endif
+ } // namespace from_chars
+
+ namespace fast_float {
+ template <typename T>
+ expected<T> impl(const char* str,
+ size_t& chars,
+ uint8_t options,
+ char locale_decimal_point)
+ {
+ const auto len = std::strlen(str);
+ if (((options & detail::float_scanner<T>::allow_hex) != 0) &&
+ is_hexfloat(str, len)) {
+ // fast_float doesn't support hexfloats
+ return from_chars::read<T>::get(str, chars, options);
+ }
+
+ T value{};
+ ::fast_float::parse_options flags{};
+ if ((options & detail::float_scanner<T>::allow_fixed) != 0) {
+ flags.format = ::fast_float::fixed;
+ }
+ if ((options & detail::float_scanner<T>::allow_scientific) !=
+ 0) {
+ flags.format = static_cast<::fast_float::chars_format>(
+ flags.format | ::fast_float::scientific);
+ }
+ if ((options & detail::float_scanner<T>::localized) != 0) {
+ flags.decimal_point = locale_decimal_point;
+ }
+
+ const auto result = ::fast_float::from_chars_advanced(
+ str, str + len, value, flags);
+ if (result.ec == std::errc::invalid_argument) {
+ return error(error::invalid_scanned_value, "fast_float");
+ }
+ if (result.ec == std::errc::result_out_of_range) {
+ return error(error::value_out_of_range, "fast_float");
+ }
+ if (std::isinf(value)) {
+ // fast_float represents very large or small values as inf
+ // But, it also parses "inf", which from_chars does not
+ if (!(len >= 3 && (str[0] == 'i' || str[0] == 'I'))) {
+ // Input was not actually infinity ->
+ // invalid result, fall back to from_chars
+ return from_chars::read<T>::get(str, chars, options);
+ }
+ }
+ chars = static_cast<size_t>(result.ptr - str);
+ return value;
+ }
+
+ template <typename T>
+ struct read;
+
+ template <>
+ struct read<float> {
+ static expected<float> get(const char* str,
+ size_t& chars,
+ uint8_t options,
+ char locale_decimal_point)
+ {
+ return impl<float>(str, chars, options,
+ locale_decimal_point);
+ }
+ };
+ template <>
+ struct read<double> {
+ static expected<double> get(const char* str,
+ size_t& chars,
+ uint8_t options,
+ char locale_decimal_points)
+ {
+ return impl<double>(str, chars, options,
+ locale_decimal_points);
+ }
+ };
+ template <>
+ struct read<long double> {
+ static expected<long double> get(const char* str,
+ size_t& chars,
+ uint8_t options,
+ char)
+ {
+ // Fallback to strtod
+ // fast_float doesn't support long double
+ return cstd::read<char, long double>::get(str, chars,
+ options);
+ }
+ };
+ } // namespace fast_float
+
+ template <typename CharT, typename T>
+ struct read;
+
+ template <typename T>
+ struct read<char, T> {
+ static expected<T> get(const char* str,
+ size_t& chars,
+ uint8_t options,
+ char locale_decimal_points)
+ {
+ // char -> default to fast_float,
+ // fallback to strtod if necessary
+ return read_float::fast_float::read<T>::get(
+ str, chars, options, locale_decimal_points);
+ }
+ };
+ template <typename T>
+ struct read<wchar_t, T> {
+ static expected<T> get(const wchar_t* str,
+ size_t& chars,
+ uint8_t options,
+ wchar_t)
+ {
+ // wchar_t -> straight to strtod
+ return read_float::cstd::read<wchar_t, T>::get(str, chars,
+ options);
+ }
+ };
+ } // namespace read_float
+
+ namespace detail {
+ template <typename T>
+ template <typename CharT>
+ expected<T> float_scanner<T>::_read_float_impl(
+ const CharT* str,
+ size_t& chars,
+ CharT locale_decimal_point)
+ {
+ // Parsing algorithm to use:
+ // If CharT == wchar_t -> strtod
+ // If CharT == char:
+ // 1. fast_float
+ // fallback if a hex float, or incorrectly parsed an inf
+ // (very large or small value)
+ // 2. std::from_chars
+ // fallback if not available (C++17) or float is subnormal
+ // 3. std::strtod
+ return read_float::read<CharT, T>::get(str, chars, format_options,
+ locale_decimal_point);
+ }
+
+#if SCN_INCLUDE_SOURCE_DEFINITIONS
+
+ template expected<float>
+ float_scanner<float>::_read_float_impl(const char*, size_t&, char);
+ template expected<double>
+ float_scanner<double>::_read_float_impl(const char*, size_t&, char);
+ template expected<long double>
+ float_scanner<long double>::_read_float_impl(const char*,
+ size_t&,
+ char);
+ template expected<float> float_scanner<float>::_read_float_impl(
+ const wchar_t*,
+ size_t&,
+ wchar_t);
+ template expected<double> float_scanner<double>::_read_float_impl(
+ const wchar_t*,
+ size_t&,
+ wchar_t);
+ template expected<long double>
+ float_scanner<long double>::_read_float_impl(const wchar_t*,
+ size_t&,
+ wchar_t);
+#endif
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
diff --git a/src/third-party/scnlib/src/reader_int.cpp b/src/third-party/scnlib/src/reader_int.cpp
new file mode 100644
index 0000000..9b953d2
--- /dev/null
+++ b/src/third-party/scnlib/src/reader_int.cpp
@@ -0,0 +1,372 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY
+#define SCN_READER_INT_CPP
+#endif
+
+#include <scn/detail/args.h>
+#include <scn/reader/int.h>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+ namespace detail {
+ SCN_NODISCARD static unsigned char _char_to_int(char ch)
+ {
+ static constexpr unsigned char digits_arr[] = {
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 0, 1, 2, 3,
+ 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, 255,
+ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
+ 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
+ 255, 255, 255, 255, 255, 255, 10, 11, 12, 13, 14, 15, 16,
+ 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29,
+ 30, 31, 32, 33, 34, 35, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 255, 255, 255, 255, 255};
+ return digits_arr[static_cast<unsigned char>(ch)];
+ }
+ SCN_NODISCARD static unsigned char _char_to_int(wchar_t ch)
+ {
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wconversion")
+ if (ch >= std::numeric_limits<char>::min() &&
+ ch <= std::numeric_limits<char>::max()) {
+ return _char_to_int(static_cast<char>(ch));
+ }
+ return 255;
+ SCN_GCC_POP
+ }
+
+ template <typename T>
+ template <typename CharT>
+ expected<typename span<const CharT>::iterator>
+ integer_scanner<T>::parse_base_prefix(span<const CharT> s, int& b) const
+ {
+ auto it = s.begin();
+ // If base can be detected, it should start with '0'
+ if (*it == ascii_widen<CharT>('0')) {
+ ++it;
+ if (it == s.end()) {
+ // It's really just 0
+ // Return it to begininng, where '0' is
+ b = -1;
+ return it;
+ }
+ if (*it == ascii_widen<CharT>('x') ||
+ *it == ascii_widen<CharT>('X')) {
+ // Hex: 0x or 0X
+ ++it;
+ if (SCN_UNLIKELY(it == s.end())) {
+ // 0x/0X not a valid number
+ b = -1;
+ return --it;
+ }
+ if (b == 0) {
+ // Detect base
+ b = 16;
+ }
+ else if (b != 16) {
+ // Invalid prefix for base
+ return error(error::invalid_scanned_value,
+ "Invalid base prefix");
+ }
+ }
+ else if (*it == ascii_widen<CharT>('b') ||
+ *it == ascii_widen<CharT>('B')) {
+ // Binary: 0b or 0b
+ ++it;
+ if (SCN_UNLIKELY(it == s.end())) {
+ // 0b/0B not a valid number
+ b = -1;
+ return --it;
+ }
+ if (b == 0) {
+ // Detect base
+ b = 2;
+ }
+ else if (b != 2) {
+ // Invalid prefix for base
+ return error(error::invalid_scanned_value,
+ "Invalid base prefix");
+ }
+ }
+ else if (*it == ascii_widen<CharT>('o') ||
+ *it == ascii_widen<CharT>('O')) {
+ // Octal: 0o or 0O
+ ++it;
+ if (SCN_UNLIKELY(it == s.end())) {
+ // 0o/0O not a valid number
+ b = -1;
+ return --it;
+ }
+ if (b == 0) {
+ // Detect base
+ b = 8;
+ }
+ else if (b != 8) {
+ // Invalid prefix for base
+ return error(error::invalid_scanned_value,
+ "Invalid base prefix");
+ }
+ }
+ else if (b == 0) {
+ // Starting with only 0 -> octal
+ b = 8;
+ }
+ }
+ if (b == 0) {
+ // None detected, default to 10
+ b = 10;
+ }
+ return it;
+ }
+
+ template <typename T>
+ template <typename CharT>
+ expected<std::ptrdiff_t> integer_scanner<T>::_parse_int(
+ T& val,
+ span<const CharT> s)
+ {
+ SCN_EXPECT(s.size() > 0);
+
+ SCN_MSVC_PUSH
+ SCN_MSVC_IGNORE(4244)
+ SCN_MSVC_IGNORE(4127) // conditional expression is constant
+
+ if (std::is_unsigned<T>::value) {
+ if (s[0] == detail::ascii_widen<CharT>('-')) {
+ return error(error::invalid_scanned_value,
+ "Unexpected sign '-' when scanning an "
+ "unsigned integer");
+ }
+ }
+
+ SCN_MSVC_POP
+
+ T tmp = 0;
+ bool minus_sign = false;
+ auto it = s.begin();
+
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wsign-conversion")
+ if (s[0] == ascii_widen<CharT>('-')) {
+ if (SCN_UNLIKELY((format_options & only_unsigned) != 0)) {
+ // 'u' option -> negative values disallowed
+ return error(error::invalid_scanned_value,
+ "Parsed negative value when type was 'u'");
+ }
+ minus_sign = true;
+ ++it;
+ }
+ else if (s[0] == ascii_widen<CharT>('+')) {
+ ++it;
+ }
+ SCN_GCC_POP
+ if (SCN_UNLIKELY(it == s.end())) {
+ return error(error::invalid_scanned_value,
+ "Expected number after sign");
+ }
+
+ // Format string was 'i' or empty -> detect base
+ // or
+ // allow_base_prefix (skip 0x etc.)
+ if (SCN_UNLIKELY(base == 0 ||
+ (format_options & allow_base_prefix) != 0)) {
+ int b{base};
+ auto r = parse_base_prefix<CharT>({it, s.end()}, b);
+ if (!r) {
+ return r.error();
+ }
+ if (b == -1) {
+ // -1 means we read a '0'
+ val = 0;
+ return ranges::distance(s.begin(), r.value());
+ }
+ if (b != 10 && base != b && base != 0) {
+ return error(error::invalid_scanned_value,
+ "Invalid base prefix");
+ }
+ if (base == 0) {
+ base = static_cast<uint8_t>(b);
+ }
+ it = r.value();
+ }
+
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wconversion")
+ SCN_GCC_IGNORE("-Wsign-conversion")
+ SCN_GCC_IGNORE("-Wsign-compare")
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wconversion")
+ SCN_CLANG_IGNORE("-Wsign-conversion")
+ SCN_CLANG_IGNORE("-Wsign-compare")
+
+ SCN_ASSUME(base > 0);
+
+ SCN_CLANG_PUSH_IGNORE_UNDEFINED_TEMPLATE
+ auto r = _parse_int_impl(tmp, minus_sign, make_span(it, s.end()));
+ SCN_CLANG_POP_IGNORE_UNDEFINED_TEMPLATE
+ if (!r) {
+ return r.error();
+ }
+ it = r.value();
+ if (s.begin() == it) {
+ return error(error::invalid_scanned_value, "custom::read_int");
+ }
+ val = tmp;
+ return ranges::distance(s.begin(), it);
+
+ SCN_CLANG_POP
+ SCN_GCC_POP
+ }
+
+ template <typename T>
+ template <typename CharT>
+ expected<typename span<const CharT>::iterator>
+ integer_scanner<T>::_parse_int_impl(T& val,
+ bool minus_sign,
+ span<const CharT> buf) const
+ {
+ SCN_GCC_PUSH
+ SCN_GCC_IGNORE("-Wconversion")
+ SCN_GCC_IGNORE("-Wsign-conversion")
+ SCN_GCC_IGNORE("-Wsign-compare")
+
+ SCN_CLANG_PUSH
+ SCN_CLANG_IGNORE("-Wconversion")
+ SCN_CLANG_IGNORE("-Wsign-conversion")
+ SCN_CLANG_IGNORE("-Wsign-compare")
+
+ SCN_MSVC_PUSH
+ SCN_MSVC_IGNORE(4018) // > signed/unsigned mismatch
+ SCN_MSVC_IGNORE(4389) // == signed/unsigned mismatch
+ SCN_MSVC_IGNORE(4244) // lossy conversion
+
+ using utype = typename std::make_unsigned<T>::type;
+
+ const auto ubase = static_cast<utype>(base);
+ SCN_ASSUME(ubase > 0);
+
+ constexpr auto uint_max = static_cast<utype>(-1);
+ constexpr auto int_max = static_cast<utype>(uint_max >> 1);
+ constexpr auto abs_int_min = static_cast<utype>(int_max + 1);
+
+ const auto cut = div(
+ [&]() -> utype {
+ if (std::is_signed<T>::value) {
+ if (minus_sign) {
+ return abs_int_min;
+ }
+ return int_max;
+ }
+ return uint_max;
+ }(),
+ ubase);
+ const auto cutoff = cut.first;
+ const auto cutlim = cut.second;
+
+ auto it = buf.begin();
+ const auto end = buf.end();
+ utype tmp = 0;
+ for (; it != end; ++it) {
+ const auto digit = _char_to_int(*it);
+ if (digit >= ubase) {
+ break;
+ }
+ if (SCN_UNLIKELY(tmp > cutoff ||
+ (tmp == cutoff && digit > cutlim))) {
+ if (!minus_sign) {
+ return error(error::value_out_of_range,
+ "Out of range: integer overflow");
+ }
+ return error(error::value_out_of_range,
+ "Out of range: integer underflow");
+ }
+ tmp = tmp * ubase + digit;
+ }
+ if (minus_sign) {
+ // special case: signed int minimum's absolute value can't
+ // be represented with the same type
+ //
+ // For example, short int -- range is [-32768, 32767], 32768
+ // can't be represented
+ //
+ // In that case, -static_cast<T>(tmp) would trigger UB
+ if (SCN_UNLIKELY(tmp == abs_int_min)) {
+ val = std::numeric_limits<T>::min();
+ }
+ else {
+ val = -static_cast<T>(tmp);
+ }
+ }
+ else {
+ val = static_cast<T>(tmp);
+ }
+ return it;
+
+ SCN_MSVC_POP
+ SCN_CLANG_POP
+ SCN_GCC_POP
+ }
+
+#if SCN_INCLUDE_SOURCE_DEFINITIONS
+
+#define SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(CharT, T) \
+ template expected<std::ptrdiff_t> integer_scanner<T>::_parse_int( \
+ T& val, span<const CharT> s); \
+ template expected<typename span<const CharT>::iterator> \
+ integer_scanner<T>::_parse_int_impl(T& val, bool minus_sign, \
+ span<const CharT> buf) const; \
+ template expected<typename span<const CharT>::iterator> \
+ integer_scanner<T>::parse_base_prefix(span<const CharT>, int&) const;
+
+#define SCN_DEFINE_INTEGER_SCANNER_MEMBERS(Char) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, signed char) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, short) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, int) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, long) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, long long) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, unsigned char) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, unsigned short) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, unsigned int) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, unsigned long) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, unsigned long long) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, char) \
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS_IMPL(Char, wchar_t)
+
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS(char)
+ SCN_DEFINE_INTEGER_SCANNER_MEMBERS(wchar_t)
+
+#endif
+
+ } // namespace detail
+
+ SCN_END_NAMESPACE
+} // namespace scn
diff --git a/src/third-party/scnlib/src/vscan.cpp b/src/third-party/scnlib/src/vscan.cpp
new file mode 100644
index 0000000..7365773
--- /dev/null
+++ b/src/third-party/scnlib/src/vscan.cpp
@@ -0,0 +1,80 @@
+// Copyright 2017 Elias Kosunen
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+// This file is a part of scnlib:
+// https://github.com/eliaskosunen/scnlib
+
+#if defined(SCN_HEADER_ONLY) && SCN_HEADER_ONLY
+#define SCN_VSCAN_CPP
+#endif
+
+#include <scn/scan/vscan.h>
+
+#include <scn/detail/context.h>
+#include <scn/detail/parse_context.h>
+#include <scn/detail/visitor.h>
+
+namespace scn {
+ SCN_BEGIN_NAMESPACE
+
+#if SCN_INCLUDE_SOURCE_DEFINITIONS
+
+#define SCN_VSCAN_DEFINE(Range, WrappedAlias, CharAlias) \
+ vscan_result<detail::vscan_macro::WrappedAlias> vscan( \
+ detail::vscan_macro::WrappedAlias&& range, \
+ basic_string_view<detail::vscan_macro::CharAlias> fmt, \
+ basic_args<detail::vscan_macro::CharAlias>&& args) \
+ { \
+ return detail::vscan_boilerplate(SCN_MOVE(range), fmt, \
+ SCN_MOVE(args)); \
+ } \
+ \
+ vscan_result<detail::vscan_macro::WrappedAlias> vscan_default( \
+ detail::vscan_macro::WrappedAlias&& range, int n_args, \
+ basic_args<detail::vscan_macro::CharAlias>&& args) \
+ { \
+ return detail::vscan_boilerplate_default(SCN_MOVE(range), n_args, \
+ SCN_MOVE(args)); \
+ } \
+ \
+ vscan_result<detail::vscan_macro::WrappedAlias> vscan_localized( \
+ detail::vscan_macro::WrappedAlias&& range, \
+ basic_locale_ref<detail::vscan_macro::CharAlias>&& loc, \
+ basic_string_view<detail::vscan_macro::CharAlias> fmt, \
+ basic_args<detail::vscan_macro::CharAlias>&& args) \
+ { \
+ return detail::vscan_boilerplate_localized( \
+ SCN_MOVE(range), SCN_MOVE(loc), fmt, SCN_MOVE(args)); \
+ } \
+ \
+ error vscan_usertype( \
+ basic_context<detail::vscan_macro::WrappedAlias>& ctx, \
+ basic_string_view<detail::vscan_macro::CharAlias> f, \
+ basic_args<detail::vscan_macro::CharAlias>&& args) \
+ { \
+ auto pctx = make_parse_context(f, ctx.locale()); \
+ return visit(ctx, pctx, SCN_MOVE(args)); \
+ }
+
+ SCN_VSCAN_DEFINE(string_view, string_view_wrapped, string_view_char)
+ SCN_VSCAN_DEFINE(wstring_view, wstring_view_wrapped, wstring_view_char)
+ SCN_VSCAN_DEFINE(std::string, string_wrapped, string_char)
+ SCN_VSCAN_DEFINE(std::wstring, wstring_wrapped, wstring_char)
+ SCN_VSCAN_DEFINE(file&, file_ref_wrapped, file_ref_char)
+ SCN_VSCAN_DEFINE(wfile&, wfile_ref_wrapped, wfile_ref_char)
+
+#endif
+
+ SCN_END_NAMESPACE
+} // namespace scn
diff --git a/src/third-party/sqlite/ext/dbdump.c b/src/third-party/sqlite/ext/dbdump.c
new file mode 100644
index 0000000..9d0764d
--- /dev/null
+++ b/src/third-party/sqlite/ext/dbdump.c
@@ -0,0 +1,730 @@
+/*
+** 2016-03-13
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+******************************************************************************
+**
+** This file implements a C-language subroutine that converts the content
+** of an SQLite database into UTF-8 text SQL statements that can be used
+** to exactly recreate the original database. ROWID values are preserved.
+**
+** A prototype of the implemented subroutine is this:
+**
+** int sqlite3_db_dump(
+** sqlite3 *db,
+** const char *zSchema,
+** const char *zTable,
+** void (*xCallback)(void*, const char*),
+** void *pArg
+** );
+**
+** The db parameter is the database connection. zSchema is the schema within
+** that database which is to be dumped. Usually the zSchema is "main" but
+** can also be "temp" or any ATTACH-ed database. If zTable is not NULL, then
+** only the content of that one table is dumped. If zTable is NULL, then all
+** tables are dumped.
+**
+** The generate text is passed to xCallback() in multiple calls. The second
+** argument to xCallback() is a copy of the pArg parameter. The first
+** argument is some of the output text that this routine generates. The
+** signature to xCallback() is designed to make it compatible with fputs().
+**
+** The sqlite3_db_dump() subroutine returns SQLITE_OK on success or some error
+** code if it encounters a problem.
+**
+** If this file is compiled with -DDBDUMP_STANDALONE then a "main()" routine
+** is included so that this routine becomes a command-line utility. The
+** command-line utility takes two or three arguments which are the name
+** of the database file, the schema, and optionally the table, forming the
+** first three arguments of a single call to the library routine.
+*/
+#include "sqlite3.h"
+#include <stdarg.h>
+#include <string.h>
+#include <ctype.h>
+
+/*
+** The state of the dump process.
+*/
+typedef struct DState DState;
+struct DState {
+ sqlite3 *db; /* The database connection */
+ int nErr; /* Number of errors seen so far */
+ int rc; /* Error code */
+ int writableSchema; /* True if in writable_schema mode */
+ int (*xCallback)(const char*,void*); /* Send output here */
+ void *pArg; /* Argument to xCallback() */
+};
+
+/*
+** A variable length string to which one can append text.
+*/
+typedef struct DText DText;
+struct DText {
+ char *z; /* The text */
+ int n; /* Number of bytes of content in z[] */
+ int nAlloc; /* Number of bytes allocated to z[] */
+};
+
+/*
+** Initialize and destroy a DText object
+*/
+static void initText(DText *p){
+ memset(p, 0, sizeof(*p));
+}
+static void freeText(DText *p){
+ sqlite3_free(p->z);
+ initText(p);
+}
+
+/* zIn is either a pointer to a NULL-terminated string in memory obtained
+** from malloc(), or a NULL pointer. The string pointed to by zAppend is
+** added to zIn, and the result returned in memory obtained from malloc().
+** zIn, if it was not NULL, is freed.
+**
+** If the third argument, quote, is not '\0', then it is used as a
+** quote character for zAppend.
+*/
+static void appendText(DText *p, char const *zAppend, char quote){
+ int len;
+ int i;
+ int nAppend = (int)(strlen(zAppend) & 0x3fffffff);
+
+ len = nAppend+p->n+1;
+ if( quote ){
+ len += 2;
+ for(i=0; i<nAppend; i++){
+ if( zAppend[i]==quote ) len++;
+ }
+ }
+
+ if( p->n+len>=p->nAlloc ){
+ char *zNew;
+ p->nAlloc = p->nAlloc*2 + len + 20;
+ zNew = sqlite3_realloc(p->z, p->nAlloc);
+ if( zNew==0 ){
+ freeText(p);
+ return;
+ }
+ p->z = zNew;
+ }
+
+ if( quote ){
+ char *zCsr = p->z+p->n;
+ *zCsr++ = quote;
+ for(i=0; i<nAppend; i++){
+ *zCsr++ = zAppend[i];
+ if( zAppend[i]==quote ) *zCsr++ = quote;
+ }
+ *zCsr++ = quote;
+ p->n = (int)(zCsr - p->z);
+ *zCsr = '\0';
+ }else{
+ memcpy(p->z+p->n, zAppend, nAppend);
+ p->n += nAppend;
+ p->z[p->n] = '\0';
+ }
+}
+
+/*
+** Attempt to determine if identifier zName needs to be quoted, either
+** because it contains non-alphanumeric characters, or because it is an
+** SQLite keyword. Be conservative in this estimate: When in doubt assume
+** that quoting is required.
+**
+** Return '"' if quoting is required. Return 0 if no quoting is required.
+*/
+static char quoteChar(const char *zName){
+ int i;
+ if( !isalpha((unsigned char)zName[0]) && zName[0]!='_' ) return '"';
+ for(i=0; zName[i]; i++){
+ if( !isalnum((unsigned char)zName[i]) && zName[i]!='_' ) return '"';
+ }
+#if 0
+ return sqlite3_keyword_check(zName, i) ? '"' : 0;
+#else
+ return 0;
+#endif
+}
+
+
+/*
+** Release memory previously allocated by tableColumnList().
+*/
+static void freeColumnList(char **azCol){
+ int i;
+ for(i=1; azCol[i]; i++){
+ sqlite3_free(azCol[i]);
+ }
+ /* azCol[0] is a static string */
+ sqlite3_free(azCol);
+}
+
+/*
+** Return a list of pointers to strings which are the names of all
+** columns in table zTab. The memory to hold the names is dynamically
+** allocated and must be released by the caller using a subsequent call
+** to freeColumnList().
+**
+** The azCol[0] entry is usually NULL. However, if zTab contains a rowid
+** value that needs to be preserved, then azCol[0] is filled in with the
+** name of the rowid column.
+**
+** The first regular column in the table is azCol[1]. The list is terminated
+** by an entry with azCol[i]==0.
+*/
+static char **tableColumnList(DState *p, const char *zTab){
+ char **azCol = 0;
+ sqlite3_stmt *pStmt = 0;
+ char *zSql;
+ int nCol = 0;
+ int nAlloc = 0;
+ int nPK = 0; /* Number of PRIMARY KEY columns seen */
+ int isIPK = 0; /* True if one PRIMARY KEY column of type INTEGER */
+ int preserveRowid = 1;
+ int rc;
+
+ zSql = sqlite3_mprintf("PRAGMA table_info=%Q", zTab);
+ if( zSql==0 ) return 0;
+ rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+ sqlite3_free(zSql);
+ if( rc ) return 0;
+ while( sqlite3_step(pStmt)==SQLITE_ROW ){
+ if( nCol>=nAlloc-2 ){
+ char **azNew;
+ nAlloc = nAlloc*2 + nCol + 10;
+ azNew = sqlite3_realloc64(azCol, nAlloc*sizeof(azCol[0]));
+ if( azNew==0 ) goto col_oom;
+ azCol = azNew;
+ azCol[0] = 0;
+ }
+ azCol[++nCol] = sqlite3_mprintf("%s", sqlite3_column_text(pStmt, 1));
+ if( azCol[nCol]==0 ) goto col_oom;
+ if( sqlite3_column_int(pStmt, 5) ){
+ nPK++;
+ if( nPK==1
+ && sqlite3_stricmp((const char*)sqlite3_column_text(pStmt,2),
+ "INTEGER")==0
+ ){
+ isIPK = 1;
+ }else{
+ isIPK = 0;
+ }
+ }
+ }
+ sqlite3_finalize(pStmt);
+ pStmt = 0;
+ azCol[nCol+1] = 0;
+
+ /* The decision of whether or not a rowid really needs to be preserved
+ ** is tricky. We never need to preserve a rowid for a WITHOUT ROWID table
+ ** or a table with an INTEGER PRIMARY KEY. We are unable to preserve
+ ** rowids on tables where the rowid is inaccessible because there are other
+ ** columns in the table named "rowid", "_rowid_", and "oid".
+ */
+ if( isIPK ){
+ /* If a single PRIMARY KEY column with type INTEGER was seen, then it
+ ** might be an alise for the ROWID. But it might also be a WITHOUT ROWID
+ ** table or a INTEGER PRIMARY KEY DESC column, neither of which are
+ ** ROWID aliases. To distinguish these cases, check to see if
+ ** there is a "pk" entry in "PRAGMA index_list". There will be
+ ** no "pk" index if the PRIMARY KEY really is an alias for the ROWID.
+ */
+ zSql = sqlite3_mprintf("SELECT 1 FROM pragma_index_list(%Q)"
+ " WHERE origin='pk'", zTab);
+ if( zSql==0 ) goto col_oom;
+ rc = sqlite3_prepare_v2(p->db, zSql, -1, &pStmt, 0);
+ sqlite3_free(zSql);
+ if( rc ){
+ freeColumnList(azCol);
+ return 0;
+ }
+ rc = sqlite3_step(pStmt);
+ sqlite3_finalize(pStmt);
+ pStmt = 0;
+ preserveRowid = rc==SQLITE_ROW;
+ }
+ if( preserveRowid ){
+ /* Only preserve the rowid if we can find a name to use for the
+ ** rowid */
+ static char *azRowid[] = { "rowid", "_rowid_", "oid" };
+ int i, j;
+ for(j=0; j<3; j++){
+ for(i=1; i<=nCol; i++){
+ if( sqlite3_stricmp(azRowid[j],azCol[i])==0 ) break;
+ }
+ if( i>nCol ){
+ /* At this point, we know that azRowid[j] is not the name of any
+ ** ordinary column in the table. Verify that azRowid[j] is a valid
+ ** name for the rowid before adding it to azCol[0]. WITHOUT ROWID
+ ** tables will fail this last check */
+ rc = sqlite3_table_column_metadata(p->db,0,zTab,azRowid[j],0,0,0,0,0);
+ if( rc==SQLITE_OK ) azCol[0] = azRowid[j];
+ break;
+ }
+ }
+ }
+ return azCol;
+
+col_oom:
+ sqlite3_finalize(pStmt);
+ freeColumnList(azCol);
+ p->nErr++;
+ p->rc = SQLITE_NOMEM;
+ return 0;
+}
+
+/*
+** Send mprintf-formatted content to the output callback.
+*/
+static void output_formatted(DState *p, const char *zFormat, ...){
+ va_list ap;
+ char *z;
+ va_start(ap, zFormat);
+ z = sqlite3_vmprintf(zFormat, ap);
+ va_end(ap);
+ p->xCallback(z, p->pArg);
+ sqlite3_free(z);
+}
+
+/*
+** Find a string that is not found anywhere in z[]. Return a pointer
+** to that string.
+**
+** Try to use zA and zB first. If both of those are already found in z[]
+** then make up some string and store it in the buffer zBuf.
+*/
+static const char *unused_string(
+ const char *z, /* Result must not appear anywhere in z */
+ const char *zA, const char *zB, /* Try these first */
+ char *zBuf /* Space to store a generated string */
+){
+ unsigned i = 0;
+ if( strstr(z, zA)==0 ) return zA;
+ if( strstr(z, zB)==0 ) return zB;
+ do{
+ sqlite3_snprintf(20,zBuf,"(%s%u)", zA, i++);
+ }while( strstr(z,zBuf)!=0 );
+ return zBuf;
+}
+
+/*
+** Output the given string as a quoted string using SQL quoting conventions.
+** Additionallly , escape the "\n" and "\r" characters so that they do not
+** get corrupted by end-of-line translation facilities in some operating
+** systems.
+*/
+static void output_quoted_escaped_string(DState *p, const char *z){
+ int i;
+ char c;
+ for(i=0; (c = z[i])!=0 && c!='\'' && c!='\n' && c!='\r'; i++){}
+ if( c==0 ){
+ output_formatted(p,"'%s'",z);
+ }else{
+ const char *zNL = 0;
+ const char *zCR = 0;
+ int nNL = 0;
+ int nCR = 0;
+ char zBuf1[20], zBuf2[20];
+ for(i=0; z[i]; i++){
+ if( z[i]=='\n' ) nNL++;
+ if( z[i]=='\r' ) nCR++;
+ }
+ if( nNL ){
+ p->xCallback("replace(", p->pArg);
+ zNL = unused_string(z, "\\n", "\\012", zBuf1);
+ }
+ if( nCR ){
+ p->xCallback("replace(", p->pArg);
+ zCR = unused_string(z, "\\r", "\\015", zBuf2);
+ }
+ p->xCallback("'", p->pArg);
+ while( *z ){
+ for(i=0; (c = z[i])!=0 && c!='\n' && c!='\r' && c!='\''; i++){}
+ if( c=='\'' ) i++;
+ if( i ){
+ output_formatted(p, "%.*s", i, z);
+ z += i;
+ }
+ if( c=='\'' ){
+ p->xCallback("'", p->pArg);
+ continue;
+ }
+ if( c==0 ){
+ break;
+ }
+ z++;
+ if( c=='\n' ){
+ p->xCallback(zNL, p->pArg);
+ continue;
+ }
+ p->xCallback(zCR, p->pArg);
+ }
+ p->xCallback("'", p->pArg);
+ if( nCR ){
+ output_formatted(p, ",'%s',char(13))", zCR);
+ }
+ if( nNL ){
+ output_formatted(p, ",'%s',char(10))", zNL);
+ }
+ }
+}
+
+/*
+** This is an sqlite3_exec callback routine used for dumping the database.
+** Each row received by this callback consists of a table name,
+** the table type ("index" or "table") and SQL to create the table.
+** This routine should print text sufficient to recreate the table.
+*/
+static int dump_callback(void *pArg, int nArg, char **azArg, char **azCol){
+ int rc;
+ const char *zTable;
+ const char *zType;
+ const char *zSql;
+ DState *p = (DState*)pArg;
+ sqlite3_stmt *pStmt;
+
+ (void)azCol;
+ if( nArg!=3 ) return 1;
+ zTable = azArg[0];
+ zType = azArg[1];
+ zSql = azArg[2];
+
+ if( strcmp(zTable, "sqlite_sequence")==0 ){
+ p->xCallback("DELETE FROM sqlite_sequence;\n", p->pArg);
+ }else if( sqlite3_strglob("sqlite_stat?", zTable)==0 ){
+ p->xCallback("ANALYZE sqlite_schema;\n", p->pArg);
+ }else if( strncmp(zTable, "sqlite_", 7)==0 ){
+ return 0;
+ }else if( strncmp(zSql, "CREATE VIRTUAL TABLE", 20)==0 ){
+#if 0
+ if( !p->writableSchema ){
+ p->xCallback("PRAGMA writable_schema=ON;\n", p->pArg);
+ p->writableSchema = 1;
+ }
+ output_formatted(p,
+ "INSERT INTO sqlite_schema(type,name,tbl_name,rootpage,sql)"
+ "VALUES('table','%q','%q',0,'%q');",
+ zTable, zTable, zSql);
+ return 0;
+#endif
+ }else{
+ if( sqlite3_strglob("CREATE TABLE ['\"]*", zSql)==0 ){
+ p->xCallback("CREATE TABLE IF NOT EXISTS ", p->pArg);
+ p->xCallback(zSql+13, p->pArg);
+ }else{
+ p->xCallback(zSql, p->pArg);
+ }
+ p->xCallback(";\n", p->pArg);
+ }
+
+ if( strcmp(zType, "table")==0 ){
+ DText sSelect;
+ DText sTable;
+ char **azTCol;
+ int i;
+ int nCol;
+
+ azTCol = tableColumnList(p, zTable);
+ if( azTCol==0 ) return 0;
+
+ initText(&sTable);
+ appendText(&sTable, "INSERT INTO ", 0);
+
+ /* Always quote the table name, even if it appears to be pure ascii,
+ ** in case it is a keyword. Ex: INSERT INTO "table" ... */
+ appendText(&sTable, zTable, quoteChar(zTable));
+
+ /* If preserving the rowid, add a column list after the table name.
+ ** In other words: "INSERT INTO tab(rowid,a,b,c,...) VALUES(...)"
+ ** instead of the usual "INSERT INTO tab VALUES(...)".
+ */
+ if( azTCol[0] ){
+ appendText(&sTable, "(", 0);
+ appendText(&sTable, azTCol[0], 0);
+ for(i=1; azTCol[i]; i++){
+ appendText(&sTable, ",", 0);
+ appendText(&sTable, azTCol[i], quoteChar(azTCol[i]));
+ }
+ appendText(&sTable, ")", 0);
+ }
+ appendText(&sTable, " VALUES(", 0);
+
+ /* Build an appropriate SELECT statement */
+ initText(&sSelect);
+ appendText(&sSelect, "SELECT ", 0);
+ if( azTCol[0] ){
+ appendText(&sSelect, azTCol[0], 0);
+ appendText(&sSelect, ",", 0);
+ }
+ for(i=1; azTCol[i]; i++){
+ appendText(&sSelect, azTCol[i], quoteChar(azTCol[i]));
+ if( azTCol[i+1] ){
+ appendText(&sSelect, ",", 0);
+ }
+ }
+ nCol = i;
+ if( azTCol[0]==0 ) nCol--;
+ freeColumnList(azTCol);
+ appendText(&sSelect, " FROM ", 0);
+ appendText(&sSelect, zTable, quoteChar(zTable));
+
+ rc = sqlite3_prepare_v2(p->db, sSelect.z, -1, &pStmt, 0);
+ if( rc!=SQLITE_OK ){
+ p->nErr++;
+ if( p->rc==SQLITE_OK ) p->rc = rc;
+ }else{
+ while( SQLITE_ROW==sqlite3_step(pStmt) ){
+ p->xCallback(sTable.z, p->pArg);
+ for(i=0; i<nCol; i++){
+ if( i ) p->xCallback(",", p->pArg);
+ switch( sqlite3_column_type(pStmt,i) ){
+ case SQLITE_INTEGER: {
+ output_formatted(p, "%lld", sqlite3_column_int64(pStmt,i));
+ break;
+ }
+ case SQLITE_FLOAT: {
+ double r = sqlite3_column_double(pStmt,i);
+ sqlite3_uint64 ur;
+ memcpy(&ur,&r,sizeof(r));
+ if( ur==0x7ff0000000000000LL ){
+ p->xCallback("1e999", p->pArg);
+ }else if( ur==0xfff0000000000000LL ){
+ p->xCallback("-1e999", p->pArg);
+ }else{
+ output_formatted(p, "%!.20g", r);
+ }
+ break;
+ }
+ case SQLITE_NULL: {
+ p->xCallback("NULL", p->pArg);
+ break;
+ }
+ case SQLITE_TEXT: {
+ output_quoted_escaped_string(p,
+ (const char*)sqlite3_column_text(pStmt,i));
+ break;
+ }
+ case SQLITE_BLOB: {
+ int nByte = sqlite3_column_bytes(pStmt,i);
+ unsigned char *a = (unsigned char*)sqlite3_column_blob(pStmt,i);
+ int j;
+ p->xCallback("x'", p->pArg);
+ for(j=0; j<nByte; j++){
+ char zWord[3];
+ zWord[0] = "0123456789abcdef"[(a[j]>>4)&15];
+ zWord[1] = "0123456789abcdef"[a[j]&15];
+ zWord[2] = 0;
+ p->xCallback(zWord, p->pArg);
+ }
+ p->xCallback("'", p->pArg);
+ break;
+ }
+ }
+ }
+ p->xCallback(");\n", p->pArg);
+ }
+ }
+ sqlite3_finalize(pStmt);
+ freeText(&sTable);
+ freeText(&sSelect);
+ }
+ return 0;
+}
+
+
+/*
+** Execute a query statement that will generate SQL output. Print
+** the result columns, comma-separated, on a line and then add a
+** semicolon terminator to the end of that line.
+**
+** If the number of columns is 1 and that column contains text "--"
+** then write the semicolon on a separate line. That way, if a
+** "--" comment occurs at the end of the statement, the comment
+** won't consume the semicolon terminator.
+*/
+static void output_sql_from_query(
+ DState *p, /* Query context */
+ const char *zSelect, /* SELECT statement to extract content */
+ ...
+){
+ sqlite3_stmt *pSelect;
+ int rc;
+ int nResult;
+ int i;
+ const char *z;
+ char *zSql;
+ va_list ap;
+ va_start(ap, zSelect);
+ zSql = sqlite3_vmprintf(zSelect, ap);
+ va_end(ap);
+ if( zSql==0 ){
+ p->rc = SQLITE_NOMEM;
+ p->nErr++;
+ return;
+ }
+ rc = sqlite3_prepare_v2(p->db, zSql, -1, &pSelect, 0);
+ sqlite3_free(zSql);
+ if( rc!=SQLITE_OK || !pSelect ){
+ output_formatted(p, "/**** ERROR: (%d) %s *****/\n", rc,
+ sqlite3_errmsg(p->db));
+ p->nErr++;
+ return;
+ }
+ rc = sqlite3_step(pSelect);
+ nResult = sqlite3_column_count(pSelect);
+ while( rc==SQLITE_ROW ){
+ z = (const char*)sqlite3_column_text(pSelect, 0);
+ p->xCallback(z, p->pArg);
+ for(i=1; i<nResult; i++){
+ p->xCallback(",", p->pArg);
+ p->xCallback((const char*)sqlite3_column_text(pSelect,i), p->pArg);
+ }
+ if( z==0 ) z = "";
+ while( z[0] && (z[0]!='-' || z[1]!='-') ) z++;
+ if( z[0] ){
+ p->xCallback("\n;\n", p->pArg);
+ }else{
+ p->xCallback(";\n", p->pArg);
+ }
+ rc = sqlite3_step(pSelect);
+ }
+ rc = sqlite3_finalize(pSelect);
+ if( rc!=SQLITE_OK ){
+ output_formatted(p, "/**** ERROR: (%d) %s *****/\n", rc,
+ sqlite3_errmsg(p->db));
+ if( (rc&0xff)!=SQLITE_CORRUPT ) p->nErr++;
+ }
+}
+
+/*
+** Run zQuery. Use dump_callback() as the callback routine so that
+** the contents of the query are output as SQL statements.
+**
+** If we get a SQLITE_CORRUPT error, rerun the query after appending
+** "ORDER BY rowid DESC" to the end.
+*/
+static void run_schema_dump_query(
+ DState *p,
+ const char *zQuery,
+ ...
+){
+ char *zErr = 0;
+ char *z;
+ va_list ap;
+ va_start(ap, zQuery);
+ z = sqlite3_vmprintf(zQuery, ap);
+ va_end(ap);
+ sqlite3_exec(p->db, z, dump_callback, p, &zErr);
+ sqlite3_free(z);
+ if( zErr ){
+ output_formatted(p, "/****** %s ******/\n", zErr);
+ sqlite3_free(zErr);
+ p->nErr++;
+ zErr = 0;
+ }
+}
+
+/*
+** Convert an SQLite database into SQL statements that will recreate that
+** database.
+*/
+int sqlite3_db_dump(
+ sqlite3 *db, /* The database connection */
+ const char *zSchema, /* Which schema to dump. Usually "main". */
+ const char *zTable, /* Which table to dump. NULL means everything. */
+ int (*xCallback)(const char*,void*), /* Output sent to this callback */
+ void *pArg /* Second argument of the callback */
+){
+ DState x;
+ memset(&x, 0, sizeof(x));
+ x.rc = sqlite3_exec(db, "BEGIN", 0, 0, 0);
+ if( x.rc ) return x.rc;
+ x.db = db;
+ x.xCallback = xCallback;
+ x.pArg = pArg;
+ xCallback("PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\n", pArg);
+ if( zTable==0 ){
+ run_schema_dump_query(&x,
+ "SELECT name, type, sql FROM \"%w\".sqlite_schema "
+ "WHERE sql NOT NULL AND type=='table' AND name!='sqlite_sequence'",
+ zSchema
+ );
+ run_schema_dump_query(&x,
+ "SELECT name, type, sql FROM \"%w\".sqlite_schema "
+ "WHERE name=='sqlite_sequence'", zSchema
+ );
+ output_sql_from_query(&x,
+ "SELECT sql FROM sqlite_schema "
+ "WHERE sql NOT NULL AND type IN ('index','trigger','view')", 0
+ );
+ }else{
+ run_schema_dump_query(&x,
+ "SELECT name, type, sql FROM \"%w\".sqlite_schema "
+ "WHERE tbl_name=%Q COLLATE nocase AND type=='table'"
+ " AND sql NOT NULL",
+ zSchema, zTable
+ );
+ output_sql_from_query(&x,
+ "SELECT sql FROM \"%w\".sqlite_schema "
+ "WHERE sql NOT NULL"
+ " AND type IN ('index','trigger','view')"
+ " AND tbl_name=%Q COLLATE nocase",
+ zSchema, zTable
+ );
+ }
+ if( x.writableSchema ){
+ xCallback("PRAGMA writable_schema=OFF;\n", pArg);
+ }
+ xCallback(x.nErr ? "ROLLBACK; -- due to errors\n" : "COMMIT;\n", pArg);
+ sqlite3_exec(db, "COMMIT", 0, 0, 0);
+ return x.rc;
+}
+
+
+
+/* The generic subroutine is above. The code the follows implements
+** the command-line interface.
+*/
+#ifdef DBDUMP_STANDALONE
+#include <stdio.h>
+
+/*
+** Command-line interface
+*/
+int main(int argc, char **argv){
+ sqlite3 *db;
+ const char *zDb;
+ const char *zSchema;
+ const char *zTable = 0;
+ int rc;
+
+ if( argc<2 || argc>4 ){
+ fprintf(stderr, "Usage: %s DATABASE ?SCHEMA? ?TABLE?\n", argv[0]);
+ return 1;
+ }
+ zDb = argv[1];
+ zSchema = argc>=3 ? argv[2] : "main";
+ zTable = argc==4 ? argv[3] : 0;
+
+ rc = sqlite3_open(zDb, &db);
+ if( rc ){
+ fprintf(stderr, "Cannot open \"%s\": %s\n", zDb, sqlite3_errmsg(db));
+ sqlite3_close(db);
+ return 1;
+ }
+ rc = sqlite3_db_dump(db, zSchema, zTable,
+ (int(*)(const char*,void*))fputs, (void*)stdout);
+ if( rc ){
+ fprintf(stderr, "Error: sqlite3_db_dump() returns %d\n", rc);
+ }
+ sqlite3_close(db);
+ return rc!=SQLITE_OK;
+}
+#endif /* DBDUMP_STANDALONE */
diff --git a/src/third-party/sqlite/ext/series.c b/src/third-party/sqlite/ext/series.c
new file mode 100644
index 0000000..4b34f84
--- /dev/null
+++ b/src/third-party/sqlite/ext/series.c
@@ -0,0 +1,448 @@
+/*
+** 2015-08-18
+**
+** The author disclaims copyright to this source code. In place of
+** a legal notice, here is a blessing:
+**
+** May you do good and not evil.
+** May you find forgiveness for yourself and forgive others.
+** May you share freely, never taking more than you give.
+**
+*************************************************************************
+**
+** This file demonstrates how to create a table-valued-function using
+** a virtual table. This demo implements the generate_series() function
+** which gives similar results to the eponymous function in PostgreSQL.
+** Examples:
+**
+** SELECT * FROM generate_series(0,100,5);
+**
+** The query above returns integers from 0 through 100 counting by steps
+** of 5.
+**
+** SELECT * FROM generate_series(0,100);
+**
+** Integers from 0 through 100 with a step size of 1.
+**
+** SELECT * FROM generate_series(20) LIMIT 10;
+**
+** Integers 20 through 29.
+**
+** HOW IT WORKS
+**
+** The generate_series "function" is really a virtual table with the
+** following schema:
+**
+** CREATE TABLE generate_series(
+** value,
+** start HIDDEN,
+** stop HIDDEN,
+** step HIDDEN
+** );
+**
+** Function arguments in queries against this virtual table are translated
+** into equality constraints against successive hidden columns. In other
+** words, the following pairs of queries are equivalent to each other:
+**
+** SELECT * FROM generate_series(0,100,5);
+** SELECT * FROM generate_series WHERE start=0 AND stop=100 AND step=5;
+**
+** SELECT * FROM generate_series(0,100);
+** SELECT * FROM generate_series WHERE start=0 AND stop=100;
+**
+** SELECT * FROM generate_series(20) LIMIT 10;
+** SELECT * FROM generate_series WHERE start=20 LIMIT 10;
+**
+** The generate_series virtual table implementation leaves the xCreate method
+** set to NULL. This means that it is not possible to do a CREATE VIRTUAL
+** TABLE command with "generate_series" as the USING argument. Instead, there
+** is a single generate_series virtual table that is always available without
+** having to be created first.
+**
+** The xBestIndex method looks for equality constraints against the hidden
+** start, stop, and step columns, and if present, it uses those constraints
+** to bound the sequence of generated values. If the equality constraints
+** are missing, it uses 0 for start, 4294967295 for stop, and 1 for step.
+** xBestIndex returns a small cost when both start and stop are available,
+** and a very large cost if either start or stop are unavailable. This
+** encourages the query planner to order joins such that the bounds of the
+** series are well-defined.
+*/
+#include "sqlite3ext.h"
+SQLITE_EXTENSION_INIT1
+#include <assert.h>
+#include <string.h>
+
+#ifndef SQLITE_OMIT_VIRTUALTABLE
+
+
+/* series_cursor is a subclass of sqlite3_vtab_cursor which will
+** serve as the underlying representation of a cursor that scans
+** over rows of the result
+*/
+typedef struct series_cursor series_cursor;
+struct series_cursor {
+ sqlite3_vtab_cursor base; /* Base class - must be first */
+ int isDesc; /* True to count down rather than up */
+ sqlite3_int64 iRowid; /* The rowid */
+ sqlite3_int64 iValue; /* Current value ("value") */
+ sqlite3_int64 mnValue; /* Mimimum value ("start") */
+ sqlite3_int64 mxValue; /* Maximum value ("stop") */
+ sqlite3_int64 iStep; /* Increment ("step") */
+};
+
+/*
+** The seriesConnect() method is invoked to create a new
+** series_vtab that describes the generate_series virtual table.
+**
+** Think of this routine as the constructor for series_vtab objects.
+**
+** All this routine needs to do is:
+**
+** (1) Allocate the series_vtab object and initialize all fields.
+**
+** (2) Tell SQLite (via the sqlite3_declare_vtab() interface) what the
+** result set of queries against generate_series will look like.
+*/
+static int seriesConnect(
+ sqlite3 *db,
+ void *pUnused,
+ int argcUnused, const char *const*argvUnused,
+ sqlite3_vtab **ppVtab,
+ char **pzErrUnused
+){
+ sqlite3_vtab *pNew;
+ int rc;
+
+/* Column numbers */
+#define SERIES_COLUMN_VALUE 0
+#define SERIES_COLUMN_START 1
+#define SERIES_COLUMN_STOP 2
+#define SERIES_COLUMN_STEP 3
+
+ (void)pUnused;
+ (void)argcUnused;
+ (void)argvUnused;
+ (void)pzErrUnused;
+ rc = sqlite3_declare_vtab(db,
+ "CREATE TABLE x(value,start hidden,stop hidden,step hidden)");
+ if( rc==SQLITE_OK ){
+ pNew = *ppVtab = sqlite3_malloc( sizeof(*pNew) );
+ if( pNew==0 ) return SQLITE_NOMEM;
+ memset(pNew, 0, sizeof(*pNew));
+#ifdef SQLITE_VTAB_INNOCUOUS
+ sqlite3_vtab_config(db, SQLITE_VTAB_INNOCUOUS);
+#endif
+ }
+ return rc;
+}
+
+/*
+** This method is the destructor for series_cursor objects.
+*/
+static int seriesDisconnect(sqlite3_vtab *pVtab){
+ sqlite3_free(pVtab);
+ return SQLITE_OK;
+}
+
+/*
+** Constructor for a new series_cursor object.
+*/
+static int seriesOpen(sqlite3_vtab *pUnused, sqlite3_vtab_cursor **ppCursor){
+ series_cursor *pCur;
+ (void)pUnused;
+ pCur = sqlite3_malloc( sizeof(*pCur) );
+ if( pCur==0 ) return SQLITE_NOMEM;
+ memset(pCur, 0, sizeof(*pCur));
+ *ppCursor = &pCur->base;
+ return SQLITE_OK;
+}
+
+/*
+** Destructor for a series_cursor.
+*/
+static int seriesClose(sqlite3_vtab_cursor *cur){
+ sqlite3_free(cur);
+ return SQLITE_OK;
+}
+
+
+/*
+** Advance a series_cursor to its next row of output.
+*/
+static int seriesNext(sqlite3_vtab_cursor *cur){
+ series_cursor *pCur = (series_cursor*)cur;
+ if( pCur->isDesc ){
+ pCur->iValue -= pCur->iStep;
+ }else{
+ pCur->iValue += pCur->iStep;
+ }
+ pCur->iRowid++;
+ return SQLITE_OK;
+}
+
+/*
+** Return values of columns for the row at which the series_cursor
+** is currently pointing.
+*/
+static int seriesColumn(
+ sqlite3_vtab_cursor *cur, /* The cursor */
+ sqlite3_context *ctx, /* First argument to sqlite3_result_...() */
+ int i /* Which column to return */
+){
+ series_cursor *pCur = (series_cursor*)cur;
+ sqlite3_int64 x = 0;
+ switch( i ){
+ case SERIES_COLUMN_START: x = pCur->mnValue; break;
+ case SERIES_COLUMN_STOP: x = pCur->mxValue; break;
+ case SERIES_COLUMN_STEP: x = pCur->iStep; break;
+ default: x = pCur->iValue; break;
+ }
+ sqlite3_result_int64(ctx, x);
+ return SQLITE_OK;
+}
+
+/*
+** Return the rowid for the current row. In this implementation, the
+** first row returned is assigned rowid value 1, and each subsequent
+** row a value 1 more than that of the previous.
+*/
+static int seriesRowid(sqlite3_vtab_cursor *cur, sqlite_int64 *pRowid){
+ series_cursor *pCur = (series_cursor*)cur;
+ *pRowid = pCur->iRowid;
+ return SQLITE_OK;
+}
+
+/*
+** Return TRUE if the cursor has been moved off of the last
+** row of output.
+*/
+static int seriesEof(sqlite3_vtab_cursor *cur){
+ series_cursor *pCur = (series_cursor*)cur;
+ if( pCur->isDesc ){
+ return pCur->iValue < pCur->mnValue;
+ }else{
+ return pCur->iValue > pCur->mxValue;
+ }
+}
+
+/* True to cause run-time checking of the start=, stop=, and/or step=
+** parameters. The only reason to do this is for testing the
+** constraint checking logic for virtual tables in the SQLite core.
+*/
+#ifndef SQLITE_SERIES_CONSTRAINT_VERIFY
+# define SQLITE_SERIES_CONSTRAINT_VERIFY 0
+#endif
+
+/*
+** This method is called to "rewind" the series_cursor object back
+** to the first row of output. This method is always called at least
+** once prior to any call to seriesColumn() or seriesRowid() or
+** seriesEof().
+**
+** The query plan selected by seriesBestIndex is passed in the idxNum
+** parameter. (idxStr is not used in this implementation.) idxNum
+** is a bitmask showing which constraints are available:
+**
+** 1: start=VALUE
+** 2: stop=VALUE
+** 4: step=VALUE
+**
+** Also, if bit 8 is set, that means that the series should be output
+** in descending order rather than in ascending order. If bit 16 is
+** set, then output must appear in ascending order.
+**
+** This routine should initialize the cursor and position it so that it
+** is pointing at the first row, or pointing off the end of the table
+** (so that seriesEof() will return true) if the table is empty.
+*/
+static int seriesFilter(
+ sqlite3_vtab_cursor *pVtabCursor,
+ int idxNum, const char *idxStrUnused,
+ int argc, sqlite3_value **argv
+){
+ series_cursor *pCur = (series_cursor *)pVtabCursor;
+ int i = 0;
+ (void)idxStrUnused;
+ if( idxNum & 1 ){
+ pCur->mnValue = sqlite3_value_int64(argv[i++]);
+ }else{
+ pCur->mnValue = 0;
+ }
+ if( idxNum & 2 ){
+ pCur->mxValue = sqlite3_value_int64(argv[i++]);
+ }else{
+ pCur->mxValue = 0xffffffff;
+ }
+ if( idxNum & 4 ){
+ pCur->iStep = sqlite3_value_int64(argv[i++]);
+ if( pCur->iStep==0 ){
+ pCur->iStep = 1;
+ }else if( pCur->iStep<0 ){
+ pCur->iStep = -pCur->iStep;
+ if( (idxNum & 16)==0 ) idxNum |= 8;
+ }
+ }else{
+ pCur->iStep = 1;
+ }
+ for(i=0; i<argc; i++){
+ if( sqlite3_value_type(argv[i])==SQLITE_NULL ){
+ /* If any of the constraints have a NULL value, then return no rows.
+ ** See ticket https://www.sqlite.org/src/info/fac496b61722daf2 */
+ pCur->mnValue = 1;
+ pCur->mxValue = 0;
+ break;
+ }
+ }
+ if( idxNum & 8 ){
+ pCur->isDesc = 1;
+ pCur->iValue = pCur->mxValue;
+ if( pCur->iStep>0 ){
+ pCur->iValue -= (pCur->mxValue - pCur->mnValue)%pCur->iStep;
+ }
+ }else{
+ pCur->isDesc = 0;
+ pCur->iValue = pCur->mnValue;
+ }
+ pCur->iRowid = 1;
+ return SQLITE_OK;
+}
+
+/*
+** SQLite will invoke this method one or more times while planning a query
+** that uses the generate_series virtual table. This routine needs to create
+** a query plan for each invocation and compute an estimated cost for that
+** plan.
+**
+** In this implementation idxNum is used to represent the
+** query plan. idxStr is unused.
+**
+** The query plan is represented by bits in idxNum:
+**
+** (1) start = $value -- constraint exists
+** (2) stop = $value -- constraint exists
+** (4) step = $value -- constraint exists
+** (8) output in descending order
+*/
+static int seriesBestIndex(
+ sqlite3_vtab *vtab,
+ sqlite3_index_info *pIdxInfo
+){
+ int i, j; /* Loop over constraints */
+ int idxNum = 0; /* The query plan bitmask */
+ int unusableMask = 0; /* Mask of unusable constraints */
+ int nArg = 0; /* Number of arguments that seriesFilter() expects */
+ int aIdx[3]; /* Constraints on start, stop, and step */
+ const struct sqlite3_index_constraint *pConstraint;
+
+ /* This implementation assumes that the start, stop, and step columns
+ ** are the last three columns in the virtual table. */
+ assert( SERIES_COLUMN_STOP == SERIES_COLUMN_START+1 );
+ assert( SERIES_COLUMN_STEP == SERIES_COLUMN_START+2 );
+ aIdx[0] = aIdx[1] = aIdx[2] = -1;
+ pConstraint = pIdxInfo->aConstraint;
+ for(i=0; i<pIdxInfo->nConstraint; i++, pConstraint++){
+ int iCol; /* 0 for start, 1 for stop, 2 for step */
+ int iMask; /* bitmask for those column */
+ if( pConstraint->iColumn<SERIES_COLUMN_START ) continue;
+ iCol = pConstraint->iColumn - SERIES_COLUMN_START;
+ assert( iCol>=0 && iCol<=2 );
+ iMask = 1 << iCol;
+ if( pConstraint->usable==0 ){
+ unusableMask |= iMask;
+ continue;
+ }else if( pConstraint->op==SQLITE_INDEX_CONSTRAINT_EQ ){
+ idxNum |= iMask;
+ aIdx[iCol] = i;
+ }
+ }
+ for(i=0; i<3; i++){
+ if( (j = aIdx[i])>=0 ){
+ pIdxInfo->aConstraintUsage[j].argvIndex = ++nArg;
+ pIdxInfo->aConstraintUsage[j].omit = !SQLITE_SERIES_CONSTRAINT_VERIFY;
+ }
+ }
+ if( (unusableMask & ~idxNum)!=0 ){
+ /* The start, stop, and step columns are inputs. Therefore if there
+ ** are unusable constraints on any of start, stop, or step then
+ ** this plan is unusable */
+ return SQLITE_CONSTRAINT;
+ }
+ if( (idxNum & 3)==3 ){
+ /* Both start= and stop= boundaries are available. This is the
+ ** the preferred case */
+ pIdxInfo->estimatedCost = (double)(2 - ((idxNum&4)!=0));
+ pIdxInfo->estimatedRows = 1000;
+ if( pIdxInfo->nOrderBy==1 ){
+ if( pIdxInfo->aOrderBy[0].desc ){
+ idxNum |= 8;
+ }else{
+ idxNum |= 16;
+ }
+ pIdxInfo->orderByConsumed = 1;
+ }
+ }else{
+ if (!(idxNum & 1)) {
+ vtab->zErrMsg = sqlite3_mprintf("the start parameter is required");
+ } else {
+ vtab->zErrMsg = sqlite3_mprintf("the stop parameter is required");
+ }
+ return SQLITE_ERROR;
+ }
+ pIdxInfo->idxNum = idxNum;
+ return SQLITE_OK;
+}
+
+/*
+** This following structure defines all the methods for the
+** generate_series virtual table.
+*/
+static sqlite3_module seriesModule = {
+ 0, /* iVersion */
+ 0, /* xCreate */
+ seriesConnect, /* xConnect */
+ seriesBestIndex, /* xBestIndex */
+ seriesDisconnect, /* xDisconnect */
+ 0, /* xDestroy */
+ seriesOpen, /* xOpen - open a cursor */
+ seriesClose, /* xClose - close a cursor */
+ seriesFilter, /* xFilter - configure scan constraints */
+ seriesNext, /* xNext - advance a cursor */
+ seriesEof, /* xEof - check for end of scan */
+ seriesColumn, /* xColumn - read data */
+ seriesRowid, /* xRowid - read data */
+ 0, /* xUpdate */
+ 0, /* xBegin */
+ 0, /* xSync */
+ 0, /* xCommit */
+ 0, /* xRollback */
+ 0, /* xFindMethod */
+ 0, /* xRename */
+ 0, /* xSavepoint */
+ 0, /* xRelease */
+ 0, /* xRollbackTo */
+ 0 /* xShadowName */
+};
+
+#endif /* SQLITE_OMIT_VIRTUALTABLE */
+
+#ifdef _WIN32
+__declspec(dllexport)
+#endif
+int sqlite3_series_init(
+ sqlite3 *db,
+ char **pzErrMsg,
+ const sqlite3_api_routines *pApi
+){
+ int rc = SQLITE_OK;
+ SQLITE_EXTENSION_INIT2(pApi);
+#ifndef SQLITE_OMIT_VIRTUALTABLE
+ if( sqlite3_libversion_number()<3008012 ){
+ *pzErrMsg = sqlite3_mprintf(
+ "generate_series() requires SQLite 3.8.12 or later");
+ return SQLITE_ERROR;
+ }
+ rc = sqlite3_create_module(db, "generate_series", &seriesModule, 0);
+#endif
+ return rc;
+}
diff --git a/src/third-party/xxHash/xxh_x86dispatch.c b/src/third-party/xxHash/xxh_x86dispatch.c
new file mode 100644
index 0000000..bec93bf
--- /dev/null
+++ b/src/third-party/xxHash/xxh_x86dispatch.c
@@ -0,0 +1,770 @@
+/*
+ * xxHash - Extremely Fast Hash algorithm
+ * Copyright (C) 2020-2021 Yann Collet
+ *
+ * BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * You can contact the author at:
+ * - xxHash homepage: https://www.xxhash.com
+ * - xxHash source repository: https://github.com/Cyan4973/xxHash
+ */
+
+
+/*!
+ * @file xxh_x86dispatch.c
+ *
+ * Automatic dispatcher code for the @ref XXH3_family on x86-based targets.
+ *
+ * Optional add-on.
+ *
+ * **Compile this file with the default flags for your target.** Do not compile
+ * with flags like `-mavx*`, `-march=native`, or `/arch:AVX*`, there will be
+ * an error. See @ref XXH_X86DISPATCH_ALLOW_AVX for details.
+ *
+ * @defgroup dispatch x86 Dispatcher
+ * @{
+ */
+
+#if defined (__cplusplus)
+extern "C" {
+#endif
+
+#if !(defined(__x86_64__) || defined(__i386__) || defined(_M_IX86) || defined(_M_X64))
+# error "Dispatching is currently only supported on x86 and x86_64."
+#endif
+
+/*!
+ * @def XXH_X86DISPATCH_ALLOW_AVX
+ * @brief Disables the AVX sanity check.
+ *
+ * Don't compile xxh_x86dispatch.c with options like `-mavx*`, `-march=native`,
+ * or `/arch:AVX*`. It is intended to be compiled for the minimum target, and
+ * it selectively enables SSE2, AVX2, and AVX512 when it is needed.
+ *
+ * Using this option _globally_ allows this feature, and therefore makes it
+ * undefined behavior to execute on any CPU without said feature.
+ *
+ * Even if the source code isn't directly using AVX intrinsics in a function,
+ * the compiler can still generate AVX code from autovectorization and by
+ * "upgrading" SSE2 intrinsics to use the VEX prefixes (a.k.a. AVX128).
+ *
+ * Use the same flags that you use to compile the rest of the program; this
+ * file will safely generate SSE2, AVX2, and AVX512 without these flags.
+ *
+ * Define XXH_X86DISPATCH_ALLOW_AVX to ignore this check, and feel free to open
+ * an issue if there is a target in the future where AVX is a default feature.
+ */
+#ifdef XXH_DOXYGEN
+# define XXH_X86DISPATCH_ALLOW_AVX
+#endif
+
+#if defined(__AVX__) && !defined(XXH_X86DISPATCH_ALLOW_AVX)
+# error "Do not compile xxh_x86dispatch.c with AVX enabled! See the comment above."
+#endif
+
+#ifdef __has_include
+# define XXH_HAS_INCLUDE(header) __has_include(header)
+#else
+# define XXH_HAS_INCLUDE(header) 0
+#endif
+
+/*!
+ * @def XXH_DISPATCH_SCALAR
+ * @brief Enables/dispatching the scalar code path.
+ *
+ * If this is defined to 0, SSE2 support is assumed. This reduces code size
+ * when the scalar path is not needed.
+ *
+ * This is automatically defined to 0 when...
+ * - SSE2 support is enabled in the compiler
+ * - Targeting x86_64
+ * - Targeting Android x86
+ * - Targeting macOS
+ */
+#ifndef XXH_DISPATCH_SCALAR
+# if defined(__SSE2__) || (defined(_M_IX86_FP) && _M_IX86_FP >= 2) /* SSE2 on by default */ \
+ || defined(__x86_64__) || defined(_M_X64) /* x86_64 */ \
+ || defined(__ANDROID__) || defined(__APPLEv__) /* Android or macOS */
+# define XXH_DISPATCH_SCALAR 0 /* disable */
+# else
+# define XXH_DISPATCH_SCALAR 1
+# endif
+#endif
+/*!
+ * @def XXH_DISPATCH_AVX2
+ * @brief Enables/disables dispatching for AVX2.
+ *
+ * This is automatically detected if it is not defined.
+ * - GCC 4.7 and later are known to support AVX2, but >4.9 is required for
+ * to get the AVX2 intrinsics and typedefs without -mavx -mavx2.
+ * - Visual Studio 2013 Update 2 and later are known to support AVX2.
+ * - The GCC/Clang internal header `<avx2intrin.h>` is detected. While this is
+ * not allowed to be included directly, it still appears in the builtin
+ * include path and is detectable with `__has_include`.
+ *
+ * @see XXH_AVX2
+ */
+#ifndef XXH_DISPATCH_AVX2
+# if (defined(__GNUC__) && (__GNUC__ > 4)) /* GCC 5.0+ */ \
+ || (defined(_MSC_VER) && _MSC_VER >= 1900) /* VS 2015+ */ \
+ || (defined(_MSC_FULL_VER) && _MSC_FULL_VER >= 180030501) /* VS 2013 Update 2 */ \
+ || XXH_HAS_INCLUDE(<avx2intrin.h>) /* GCC/Clang internal header */
+# define XXH_DISPATCH_AVX2 1 /* enable dispatch towards AVX2 */
+# else
+# define XXH_DISPATCH_AVX2 0
+# endif
+#endif /* XXH_DISPATCH_AVX2 */
+
+/*!
+ * @def XXH_DISPATCH_AVX512
+ * @brief Enables/disables dispatching for AVX512.
+ *
+ * Automatically detected if one of the following conditions is met:
+ * - GCC 4.9 and later are known to support AVX512.
+ * - Visual Studio 2017 and later are known to support AVX2.
+ * - The GCC/Clang internal header `<avx512fintrin.h>` is detected. While this
+ * is not allowed to be included directly, it still appears in the builtin
+ * include path and is detectable with `__has_include`.
+ *
+ * @see XXH_AVX512
+ */
+#ifndef XXH_DISPATCH_AVX512
+# if (defined(__GNUC__) \
+ && (__GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 9))) /* GCC 4.9+ */ \
+ || (defined(_MSC_VER) && _MSC_VER >= 1910) /* VS 2017+ */ \
+ || XXH_HAS_INCLUDE(<avx512fintrin.h>) /* GCC/Clang internal header */
+# define XXH_DISPATCH_AVX512 1 /* enable dispatch towards AVX512 */
+# else
+# define XXH_DISPATCH_AVX512 0
+# endif
+#endif /* XXH_DISPATCH_AVX512 */
+
+/*!
+ * @def XXH_TARGET_SSE2
+ * @brief Allows a function to be compiled with SSE2 intrinsics.
+ *
+ * Uses `__attribute__((__target__("sse2")))` on GCC to allow SSE2 to be used
+ * even with `-mno-sse2`.
+ *
+ * @def XXH_TARGET_AVX2
+ * @brief Like @ref XXH_TARGET_SSE2, but for AVX2.
+ *
+ * @def XXH_TARGET_AVX512
+ * @brief Like @ref XXH_TARGET_SSE2, but for AVX512.
+ */
+#if defined(__GNUC__)
+# include <emmintrin.h> /* SSE2 */
+# if XXH_DISPATCH_AVX2 || XXH_DISPATCH_AVX512
+# include <immintrin.h> /* AVX2, AVX512F */
+# endif
+# define XXH_TARGET_SSE2 __attribute__((__target__("sse2")))
+# define XXH_TARGET_AVX2 __attribute__((__target__("avx2")))
+# define XXH_TARGET_AVX512 __attribute__((__target__("avx512f")))
+#elif defined(_MSC_VER)
+# include <intrin.h>
+# define XXH_TARGET_SSE2
+# define XXH_TARGET_AVX2
+# define XXH_TARGET_AVX512
+#else
+# error "Dispatching is currently not supported for your compiler."
+#endif
+
+#ifdef XXH_DISPATCH_DEBUG
+/* debug logging */
+# include <stdio.h>
+# define XXH_debugPrint(str) { fprintf(stderr, "DEBUG: xxHash dispatch: %s \n", str); fflush(NULL); }
+#else
+# define XXH_debugPrint(str) ((void)0)
+# undef NDEBUG /* avoid redefinition */
+# define NDEBUG
+#endif
+#include <assert.h>
+
+#define XXH_INLINE_ALL
+#define XXH_X86DISPATCH
+#include "xxhash.h"
+
+/*
+ * Support both AT&T and Intel dialects
+ *
+ * GCC doesn't convert AT&T syntax to Intel syntax, and will error out if
+ * compiled with -masm=intel. Instead, it supports dialect switching with
+ * curly braces: { AT&T syntax | Intel syntax }
+ *
+ * Clang's integrated assembler automatically converts AT&T syntax to Intel if
+ * needed, making the dialect switching useless (it isn't even supported).
+ *
+ * Note: Comments are written in the inline assembly itself.
+ */
+#ifdef __clang__
+# define XXH_I_ATT(intel, att) att "\n\t"
+#else
+# define XXH_I_ATT(intel, att) "{" att "|" intel "}\n\t"
+#endif
+
+/*!
+ * @internal
+ * @brief Runs CPUID.
+ *
+ * @param eax , ecx The parameters to pass to CPUID, %eax and %ecx respectively.
+ * @param abcd The array to store the result in, `{ eax, ebx, ecx, edx }`
+ */
+static void XXH_cpuid(xxh_u32 eax, xxh_u32 ecx, xxh_u32* abcd)
+{
+#if defined(_MSC_VER)
+ __cpuidex(abcd, eax, ecx);
+#else
+ xxh_u32 ebx, edx;
+# if defined(__i386__) && defined(__PIC__)
+ __asm__(
+ "# Call CPUID\n\t"
+ "#\n\t"
+ "# On 32-bit x86 with PIC enabled, we are not allowed to overwrite\n\t"
+ "# EBX, so we use EDI instead.\n\t"
+ XXH_I_ATT("mov edi, ebx", "movl %%ebx, %%edi")
+ XXH_I_ATT("cpuid", "cpuid" )
+ XXH_I_ATT("xchg edi, ebx", "xchgl %%ebx, %%edi")
+ : "=D" (ebx),
+# else
+ __asm__(
+ "# Call CPUID\n\t"
+ XXH_I_ATT("cpuid", "cpuid")
+ : "=b" (ebx),
+# endif
+ "+a" (eax), "+c" (ecx), "=d" (edx));
+ abcd[0] = eax;
+ abcd[1] = ebx;
+ abcd[2] = ecx;
+ abcd[3] = edx;
+#endif
+}
+
+/*
+ * Modified version of Intel's guide
+ * https://software.intel.com/en-us/articles/how-to-detect-new-instruction-support-in-the-4th-generation-intel-core-processor-family
+ */
+
+#if XXH_DISPATCH_AVX2 || XXH_DISPATCH_AVX512
+/*!
+ * @internal
+ * @brief Runs `XGETBV`.
+ *
+ * While the CPU may support AVX2, the operating system might not properly save
+ * the full YMM/ZMM registers.
+ *
+ * xgetbv is used for detecting this: Any compliant operating system will define
+ * a set of flags in the xcr0 register indicating how it saves the AVX registers.
+ *
+ * You can manually disable this flag on Windows by running, as admin:
+ *
+ * bcdedit.exe /set xsavedisable 1
+ *
+ * and rebooting. Run the same command with 0 to re-enable it.
+ */
+static xxh_u64 XXH_xgetbv(void)
+{
+#if defined(_MSC_VER)
+ return _xgetbv(0); /* min VS2010 SP1 compiler is required */
+#else
+ xxh_u32 xcr0_lo, xcr0_hi;
+ __asm__(
+ "# Call XGETBV\n\t"
+ "#\n\t"
+ "# Older assemblers (e.g. macOS's ancient GAS version) don't support\n\t"
+ "# the XGETBV opcode, so we encode it by hand instead.\n\t"
+ "# See <https://github.com/asmjit/asmjit/issues/78> for details.\n\t"
+ ".byte 0x0f, 0x01, 0xd0\n\t"
+ : "=a" (xcr0_lo), "=d" (xcr0_hi) : "c" (0));
+ return xcr0_lo | ((xxh_u64)xcr0_hi << 32);
+#endif
+}
+#endif
+
+#define XXH_SSE2_CPUID_MASK (1 << 26)
+#define XXH_OSXSAVE_CPUID_MASK ((1 << 26) | (1 << 27))
+#define XXH_AVX2_CPUID_MASK (1 << 5)
+#define XXH_AVX2_XGETBV_MASK ((1 << 2) | (1 << 1))
+#define XXH_AVX512F_CPUID_MASK (1 << 16)
+#define XXH_AVX512F_XGETBV_MASK ((7 << 5) | (1 << 2) | (1 << 1))
+
+/*!
+ * @internal
+ * @brief Returns the best XXH3 implementation.
+ *
+ * Runs various CPUID/XGETBV tests to try and determine the best implementation.
+ *
+ * @return The best @ref XXH_VECTOR implementation.
+ * @see XXH_VECTOR_TYPES
+ */
+static int XXH_featureTest(void)
+{
+ xxh_u32 abcd[4];
+ xxh_u32 max_leaves;
+ int best = XXH_SCALAR;
+#if XXH_DISPATCH_AVX2 || XXH_DISPATCH_AVX512
+ xxh_u64 xgetbv_val;
+#endif
+#if defined(__GNUC__) && defined(__i386__)
+ xxh_u32 cpuid_supported;
+ __asm__(
+ "# For the sake of ruthless backwards compatibility, check if CPUID\n\t"
+ "# is supported in the EFLAGS on i386.\n\t"
+ "# This is not necessary on x86_64 - CPUID is mandatory.\n\t"
+ "# The ID flag (bit 21) in the EFLAGS register indicates support\n\t"
+ "# for the CPUID instruction. If a software procedure can set and\n\t"
+ "# clear this flag, the processor executing the procedure supports\n\t"
+ "# the CPUID instruction.\n\t"
+ "# <https://c9x.me/x86/html/file_module_x86_id_45.html>\n\t"
+ "#\n\t"
+ "# Routine is from <https://wiki.osdev.org/CPUID>.\n\t"
+
+ "# Save EFLAGS\n\t"
+ XXH_I_ATT("pushfd", "pushfl" )
+ "# Store EFLAGS\n\t"
+ XXH_I_ATT("pushfd", "pushfl" )
+ "# Invert the ID bit in stored EFLAGS\n\t"
+ XXH_I_ATT("xor dword ptr[esp], 0x200000", "xorl $0x200000, (%%esp)")
+ "# Load stored EFLAGS (with ID bit inverted)\n\t"
+ XXH_I_ATT("popfd", "popfl" )
+ "# Store EFLAGS again (ID bit may or not be inverted)\n\t"
+ XXH_I_ATT("pushfd", "pushfl" )
+ "# eax = modified EFLAGS (ID bit may or may not be inverted)\n\t"
+ XXH_I_ATT("pop eax", "popl %%eax" )
+ "# eax = whichever bits were changed\n\t"
+ XXH_I_ATT("xor eax, dword ptr[esp]", "xorl (%%esp), %%eax" )
+ "# Restore original EFLAGS\n\t"
+ XXH_I_ATT("popfd", "popfl" )
+ "# eax = zero if ID bit can't be changed, else non-zero\n\t"
+ XXH_I_ATT("and eax, 0x200000", "andl $0x200000, %%eax" )
+ : "=a" (cpuid_supported) :: "cc");
+
+ if (XXH_unlikely(!cpuid_supported)) {
+ XXH_debugPrint("CPUID support is not detected!");
+ return best;
+ }
+
+#endif
+ /* Check how many CPUID pages we have */
+ XXH_cpuid(0, 0, abcd);
+ max_leaves = abcd[0];
+
+ /* Shouldn't happen on hardware, but happens on some QEMU configs. */
+ if (XXH_unlikely(max_leaves == 0)) {
+ XXH_debugPrint("Max CPUID leaves == 0!");
+ return best;
+ }
+
+ /* Check for SSE2, OSXSAVE and xgetbv */
+ XXH_cpuid(1, 0, abcd);
+
+ /*
+ * Test for SSE2. The check is redundant on x86_64, but it doesn't hurt.
+ */
+ if (XXH_unlikely((abcd[3] & XXH_SSE2_CPUID_MASK) != XXH_SSE2_CPUID_MASK))
+ return best;
+
+ XXH_debugPrint("SSE2 support detected.");
+
+ best = XXH_SSE2;
+#if XXH_DISPATCH_AVX2 || XXH_DISPATCH_AVX512
+ /* Make sure we have enough leaves */
+ if (XXH_unlikely(max_leaves < 7))
+ return best;
+
+ /* Test for OSXSAVE and XGETBV */
+ if ((abcd[2] & XXH_OSXSAVE_CPUID_MASK) != XXH_OSXSAVE_CPUID_MASK)
+ return best;
+
+ /* CPUID check for AVX features */
+ XXH_cpuid(7, 0, abcd);
+
+ xgetbv_val = XXH_xgetbv();
+#if XXH_DISPATCH_AVX2
+ /* Validate that AVX2 is supported by the CPU */
+ if ((abcd[1] & XXH_AVX2_CPUID_MASK) != XXH_AVX2_CPUID_MASK)
+ return best;
+
+ /* Validate that the OS supports YMM registers */
+ if ((xgetbv_val & XXH_AVX2_XGETBV_MASK) != XXH_AVX2_XGETBV_MASK) {
+ XXH_debugPrint("AVX2 supported by the CPU, but not the OS.");
+ return best;
+ }
+
+ /* AVX2 supported */
+ XXH_debugPrint("AVX2 support detected.");
+ best = XXH_AVX2;
+#endif
+#if XXH_DISPATCH_AVX512
+ /* Check if AVX512F is supported by the CPU */
+ if ((abcd[1] & XXH_AVX512F_CPUID_MASK) != XXH_AVX512F_CPUID_MASK) {
+ XXH_debugPrint("AVX512F not supported by CPU");
+ return best;
+ }
+
+ /* Validate that the OS supports ZMM registers */
+ if ((xgetbv_val & XXH_AVX512F_XGETBV_MASK) != XXH_AVX512F_XGETBV_MASK) {
+ XXH_debugPrint("AVX512F supported by the CPU, but not the OS.");
+ return best;
+ }
+
+ /* AVX512F supported */
+ XXH_debugPrint("AVX512F support detected.");
+ best = XXH_AVX512;
+#endif
+#endif
+ return best;
+}
+
+
+/* === Vector implementations === */
+
+/*!
+ * @internal
+ * @brief Defines the various dispatch functions.
+ *
+ * TODO: Consolidate?
+ *
+ * @param suffix The suffix for the functions, e.g. sse2 or scalar
+ * @param target XXH_TARGET_* or empty.
+ */
+#define XXH_DEFINE_DISPATCH_FUNCS(suffix, target) \
+ \
+/* === XXH3, default variants === */ \
+ \
+XXH_NO_INLINE target XXH64_hash_t \
+XXHL64_default_##suffix(const void* XXH_RESTRICT input, size_t len) \
+{ \
+ return XXH3_hashLong_64b_internal( \
+ input, len, XXH3_kSecret, sizeof(XXH3_kSecret), \
+ XXH3_accumulate_512_##suffix, XXH3_scrambleAcc_##suffix \
+ ); \
+} \
+ \
+/* === XXH3, Seeded variants === */ \
+ \
+XXH_NO_INLINE target XXH64_hash_t \
+XXHL64_seed_##suffix(const void* XXH_RESTRICT input, size_t len, \
+ XXH64_hash_t seed) \
+{ \
+ return XXH3_hashLong_64b_withSeed_internal( \
+ input, len, seed, XXH3_accumulate_512_##suffix, \
+ XXH3_scrambleAcc_##suffix, XXH3_initCustomSecret_##suffix \
+ ); \
+} \
+ \
+/* === XXH3, Secret variants === */ \
+ \
+XXH_NO_INLINE target XXH64_hash_t \
+XXHL64_secret_##suffix(const void* XXH_RESTRICT input, size_t len, \
+ const void* secret, size_t secretLen) \
+{ \
+ return XXH3_hashLong_64b_internal( \
+ input, len, secret, secretLen, \
+ XXH3_accumulate_512_##suffix, XXH3_scrambleAcc_##suffix \
+ ); \
+} \
+ \
+/* === XXH3 update variants === */ \
+ \
+XXH_NO_INLINE target XXH_errorcode \
+XXH3_update_##suffix(XXH3_state_t* state, const void* input, size_t len) \
+{ \
+ return XXH3_update(state, (const xxh_u8*)input, len, \
+ XXH3_accumulate_512_##suffix, XXH3_scrambleAcc_##suffix); \
+} \
+ \
+/* === XXH128 default variants === */ \
+ \
+XXH_NO_INLINE target XXH128_hash_t \
+XXHL128_default_##suffix(const void* XXH_RESTRICT input, size_t len) \
+{ \
+ return XXH3_hashLong_128b_internal( \
+ input, len, XXH3_kSecret, sizeof(XXH3_kSecret), \
+ XXH3_accumulate_512_##suffix, XXH3_scrambleAcc_##suffix \
+ ); \
+} \
+ \
+/* === XXH128 Secret variants === */ \
+ \
+XXH_NO_INLINE target XXH128_hash_t \
+XXHL128_secret_##suffix(const void* XXH_RESTRICT input, size_t len, \
+ const void* XXH_RESTRICT secret, size_t secretLen) \
+{ \
+ return XXH3_hashLong_128b_internal( \
+ input, len, (const xxh_u8*)secret, secretLen, \
+ XXH3_accumulate_512_##suffix, XXH3_scrambleAcc_##suffix); \
+} \
+ \
+/* === XXH128 Seeded variants === */ \
+ \
+XXH_NO_INLINE target XXH128_hash_t \
+XXHL128_seed_##suffix(const void* XXH_RESTRICT input, size_t len, \
+ XXH64_hash_t seed) \
+{ \
+ return XXH3_hashLong_128b_withSeed_internal(input, len, seed, \
+ XXH3_accumulate_512_##suffix, XXH3_scrambleAcc_##suffix, \
+ XXH3_initCustomSecret_##suffix); \
+}
+
+/* End XXH_DEFINE_DISPATCH_FUNCS */
+
+#if XXH_DISPATCH_SCALAR
+XXH_DEFINE_DISPATCH_FUNCS(scalar, /* nothing */)
+#endif
+XXH_DEFINE_DISPATCH_FUNCS(sse2, XXH_TARGET_SSE2)
+#if XXH_DISPATCH_AVX2
+XXH_DEFINE_DISPATCH_FUNCS(avx2, XXH_TARGET_AVX2)
+#endif
+#if XXH_DISPATCH_AVX512
+XXH_DEFINE_DISPATCH_FUNCS(avx512, XXH_TARGET_AVX512)
+#endif
+#undef XXH_DEFINE_DISPATCH_FUNCS
+
+/* ==== Dispatchers ==== */
+
+typedef XXH64_hash_t (*XXH3_dispatchx86_hashLong64_default)(const void* XXH_RESTRICT, size_t);
+
+typedef XXH64_hash_t (*XXH3_dispatchx86_hashLong64_withSeed)(const void* XXH_RESTRICT, size_t, XXH64_hash_t);
+
+typedef XXH64_hash_t (*XXH3_dispatchx86_hashLong64_withSecret)(const void* XXH_RESTRICT, size_t, const void* XXH_RESTRICT, size_t);
+
+typedef XXH_errorcode (*XXH3_dispatchx86_update)(XXH3_state_t*, const void*, size_t);
+
+typedef struct {
+ XXH3_dispatchx86_hashLong64_default hashLong64_default;
+ XXH3_dispatchx86_hashLong64_withSeed hashLong64_seed;
+ XXH3_dispatchx86_hashLong64_withSecret hashLong64_secret;
+ XXH3_dispatchx86_update update;
+} XXH_dispatchFunctions_s;
+
+#define XXH_NB_DISPATCHES 4
+
+/*!
+ * @internal
+ * @brief Table of dispatchers for @ref XXH3_64bits().
+ *
+ * @pre The indices must match @ref XXH_VECTOR_TYPE.
+ */
+static const XXH_dispatchFunctions_s XXH_kDispatch[XXH_NB_DISPATCHES] = {
+#if XXH_DISPATCH_SCALAR
+ /* Scalar */ { XXHL64_default_scalar, XXHL64_seed_scalar, XXHL64_secret_scalar, XXH3_update_scalar },
+#else
+ /* Scalar */ { NULL, NULL, NULL, NULL },
+#endif
+ /* SSE2 */ { XXHL64_default_sse2, XXHL64_seed_sse2, XXHL64_secret_sse2, XXH3_update_sse2 },
+#if XXH_DISPATCH_AVX2
+ /* AVX2 */ { XXHL64_default_avx2, XXHL64_seed_avx2, XXHL64_secret_avx2, XXH3_update_avx2 },
+#else
+ /* AVX2 */ { NULL, NULL, NULL, NULL },
+#endif
+#if XXH_DISPATCH_AVX512
+ /* AVX512 */ { XXHL64_default_avx512, XXHL64_seed_avx512, XXHL64_secret_avx512, XXH3_update_avx512 }
+#else
+ /* AVX512 */ { NULL, NULL, NULL, NULL }
+#endif
+};
+/*!
+ * @internal
+ * @brief The selected dispatch table for @ref XXH3_64bits().
+ */
+static XXH_dispatchFunctions_s XXH_g_dispatch = { NULL, NULL, NULL, NULL };
+
+
+typedef XXH128_hash_t (*XXH3_dispatchx86_hashLong128_default)(const void* XXH_RESTRICT, size_t);
+
+typedef XXH128_hash_t (*XXH3_dispatchx86_hashLong128_withSeed)(const void* XXH_RESTRICT, size_t, XXH64_hash_t);
+
+typedef XXH128_hash_t (*XXH3_dispatchx86_hashLong128_withSecret)(const void* XXH_RESTRICT, size_t, const void* XXH_RESTRICT, size_t);
+
+typedef struct {
+ XXH3_dispatchx86_hashLong128_default hashLong128_default;
+ XXH3_dispatchx86_hashLong128_withSeed hashLong128_seed;
+ XXH3_dispatchx86_hashLong128_withSecret hashLong128_secret;
+ XXH3_dispatchx86_update update;
+} XXH_dispatch128Functions_s;
+
+
+/*!
+ * @internal
+ * @brief Table of dispatchers for @ref XXH3_128bits().
+ *
+ * @pre The indices must match @ref XXH_VECTOR_TYPE.
+ */
+static const XXH_dispatch128Functions_s XXH_kDispatch128[XXH_NB_DISPATCHES] = {
+#if XXH_DISPATCH_SCALAR
+ /* Scalar */ { XXHL128_default_scalar, XXHL128_seed_scalar, XXHL128_secret_scalar, XXH3_update_scalar },
+#else
+ /* Scalar */ { NULL, NULL, NULL, NULL },
+#endif
+ /* SSE2 */ { XXHL128_default_sse2, XXHL128_seed_sse2, XXHL128_secret_sse2, XXH3_update_sse2 },
+#if XXH_DISPATCH_AVX2
+ /* AVX2 */ { XXHL128_default_avx2, XXHL128_seed_avx2, XXHL128_secret_avx2, XXH3_update_avx2 },
+#else
+ /* AVX2 */ { NULL, NULL, NULL, NULL },
+#endif
+#if XXH_DISPATCH_AVX512
+ /* AVX512 */ { XXHL128_default_avx512, XXHL128_seed_avx512, XXHL128_secret_avx512, XXH3_update_avx512 }
+#else
+ /* AVX512 */ { NULL, NULL, NULL, NULL }
+#endif
+};
+
+/*!
+ * @internal
+ * @brief The selected dispatch table for @ref XXH3_64bits().
+ */
+static XXH_dispatch128Functions_s XXH_g_dispatch128 = { NULL, NULL, NULL, NULL };
+
+/*!
+ * @internal
+ * @brief Runs a CPUID check and sets the correct dispatch tables.
+ */
+static void XXH_setDispatch(void)
+{
+ int vecID = XXH_featureTest();
+ XXH_STATIC_ASSERT(XXH_AVX512 == XXH_NB_DISPATCHES-1);
+ assert(XXH_SCALAR <= vecID && vecID <= XXH_AVX512);
+#if !XXH_DISPATCH_SCALAR
+ assert(vecID != XXH_SCALAR);
+#endif
+#if !XXH_DISPATCH_AVX512
+ assert(vecID != XXH_AVX512);
+#endif
+#if !XXH_DISPATCH_AVX2
+ assert(vecID != XXH_AVX2);
+#endif
+ XXH_g_dispatch = XXH_kDispatch[vecID];
+ XXH_g_dispatch128 = XXH_kDispatch128[vecID];
+}
+
+
+/* ==== XXH3 public functions ==== */
+
+static XXH64_hash_t
+XXH3_hashLong_64b_defaultSecret_selection(const void* input, size_t len,
+ XXH64_hash_t seed64, const xxh_u8* secret, size_t secretLen)
+{
+ (void)seed64; (void)secret; (void)secretLen;
+ if (XXH_g_dispatch.hashLong64_default == NULL) XXH_setDispatch();
+ return XXH_g_dispatch.hashLong64_default(input, len);
+}
+
+XXH64_hash_t XXH3_64bits_dispatch(const void* input, size_t len)
+{
+ return XXH3_64bits_internal(input, len, 0, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_defaultSecret_selection);
+}
+
+static XXH64_hash_t
+XXH3_hashLong_64b_withSeed_selection(const void* input, size_t len,
+ XXH64_hash_t seed64, const xxh_u8* secret, size_t secretLen)
+{
+ (void)secret; (void)secretLen;
+ if (XXH_g_dispatch.hashLong64_seed == NULL) XXH_setDispatch();
+ return XXH_g_dispatch.hashLong64_seed(input, len, seed64);
+}
+
+XXH64_hash_t XXH3_64bits_withSeed_dispatch(const void* input, size_t len, XXH64_hash_t seed)
+{
+ return XXH3_64bits_internal(input, len, seed, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_withSeed_selection);
+}
+
+static XXH64_hash_t
+XXH3_hashLong_64b_withSecret_selection(const void* input, size_t len,
+ XXH64_hash_t seed64, const xxh_u8* secret, size_t secretLen)
+{
+ (void)seed64;
+ if (XXH_g_dispatch.hashLong64_secret == NULL) XXH_setDispatch();
+ return XXH_g_dispatch.hashLong64_secret(input, len, secret, secretLen);
+}
+
+XXH64_hash_t XXH3_64bits_withSecret_dispatch(const void* input, size_t len, const void* secret, size_t secretLen)
+{
+ return XXH3_64bits_internal(input, len, 0, secret, secretLen, XXH3_hashLong_64b_withSecret_selection);
+}
+
+XXH_errorcode
+XXH3_64bits_update_dispatch(XXH3_state_t* state, const void* input, size_t len)
+{
+ if (XXH_g_dispatch.update == NULL) XXH_setDispatch();
+ return XXH_g_dispatch.update(state, (const xxh_u8*)input, len);
+}
+
+
+/* ==== XXH128 public functions ==== */
+
+static XXH128_hash_t
+XXH3_hashLong_128b_defaultSecret_selection(const void* input, size_t len,
+ XXH64_hash_t seed64, const void* secret, size_t secretLen)
+{
+ (void)seed64; (void)secret; (void)secretLen;
+ if (XXH_g_dispatch128.hashLong128_default == NULL) XXH_setDispatch();
+ return XXH_g_dispatch128.hashLong128_default(input, len);
+}
+
+XXH128_hash_t XXH3_128bits_dispatch(const void* input, size_t len)
+{
+ return XXH3_128bits_internal(input, len, 0, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_128b_defaultSecret_selection);
+}
+
+static XXH128_hash_t
+XXH3_hashLong_128b_withSeed_selection(const void* input, size_t len,
+ XXH64_hash_t seed64, const void* secret, size_t secretLen)
+{
+ (void)secret; (void)secretLen;
+ if (XXH_g_dispatch128.hashLong128_seed == NULL) XXH_setDispatch();
+ return XXH_g_dispatch128.hashLong128_seed(input, len, seed64);
+}
+
+XXH128_hash_t XXH3_128bits_withSeed_dispatch(const void* input, size_t len, XXH64_hash_t seed)
+{
+ return XXH3_128bits_internal(input, len, seed, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_128b_withSeed_selection);
+}
+
+static XXH128_hash_t
+XXH3_hashLong_128b_withSecret_selection(const void* input, size_t len,
+ XXH64_hash_t seed64, const void* secret, size_t secretLen)
+{
+ (void)seed64;
+ if (XXH_g_dispatch128.hashLong128_secret == NULL) XXH_setDispatch();
+ return XXH_g_dispatch128.hashLong128_secret(input, len, secret, secretLen);
+}
+
+XXH128_hash_t XXH3_128bits_withSecret_dispatch(const void* input, size_t len, const void* secret, size_t secretLen)
+{
+ return XXH3_128bits_internal(input, len, 0, secret, secretLen, XXH3_hashLong_128b_withSecret_selection);
+}
+
+XXH_errorcode
+XXH3_128bits_update_dispatch(XXH3_state_t* state, const void* input, size_t len)
+{
+ if (XXH_g_dispatch128.update == NULL) XXH_setDispatch();
+ return XXH_g_dispatch128.update(state, (const xxh_u8*)input, len);
+}
+
+#if defined (__cplusplus)
+}
+#endif
+/*! @} */
diff --git a/src/third-party/xxHash/xxh_x86dispatch.h b/src/third-party/xxHash/xxh_x86dispatch.h
new file mode 100644
index 0000000..417ef08
--- /dev/null
+++ b/src/third-party/xxHash/xxh_x86dispatch.h
@@ -0,0 +1,85 @@
+/*
+ * xxHash - XXH3 Dispatcher for x86-based targets
+ * Copyright (C) 2020-2021 Yann Collet
+ *
+ * BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * You can contact the author at:
+ * - xxHash homepage: https://www.xxhash.com
+ * - xxHash source repository: https://github.com/Cyan4973/xxHash
+ */
+
+#ifndef XXH_X86DISPATCH_H_13563687684
+#define XXH_X86DISPATCH_H_13563687684
+
+#include "xxhash.h" /* XXH64_hash_t, XXH3_state_t */
+
+#if defined (__cplusplus)
+extern "C" {
+#endif
+
+XXH_PUBLIC_API XXH64_hash_t XXH3_64bits_dispatch(const void* input, size_t len);
+XXH_PUBLIC_API XXH64_hash_t XXH3_64bits_withSeed_dispatch(const void* input, size_t len, XXH64_hash_t seed);
+XXH_PUBLIC_API XXH64_hash_t XXH3_64bits_withSecret_dispatch(const void* input, size_t len, const void* secret, size_t secretLen);
+XXH_PUBLIC_API XXH_errorcode XXH3_64bits_update_dispatch(XXH3_state_t* state, const void* input, size_t len);
+
+XXH_PUBLIC_API XXH128_hash_t XXH3_128bits_dispatch(const void* input, size_t len);
+XXH_PUBLIC_API XXH128_hash_t XXH3_128bits_withSeed_dispatch(const void* input, size_t len, XXH64_hash_t seed);
+XXH_PUBLIC_API XXH128_hash_t XXH3_128bits_withSecret_dispatch(const void* input, size_t len, const void* secret, size_t secretLen);
+XXH_PUBLIC_API XXH_errorcode XXH3_128bits_update_dispatch(XXH3_state_t* state, const void* input, size_t len);
+
+#if defined (__cplusplus)
+}
+#endif
+
+
+/* automatic replacement of XXH3 functions.
+ * can be disabled by setting XXH_DISPATCH_DISABLE_REPLACE */
+#ifndef XXH_DISPATCH_DISABLE_REPLACE
+
+# undef XXH3_64bits
+# define XXH3_64bits XXH3_64bits_dispatch
+# undef XXH3_64bits_withSeed
+# define XXH3_64bits_withSeed XXH3_64bits_withSeed_dispatch
+# undef XXH3_64bits_withSecret
+# define XXH3_64bits_withSecret XXH3_64bits_withSecret_dispatch
+# undef XXH3_64bits_update
+# define XXH3_64bits_update XXH3_64bits_update_dispatch
+
+# undef XXH128
+# define XXH128 XXH3_128bits_withSeed_dispatch
+# undef XXH3_128bits
+# define XXH3_128bits XXH3_128bits_dispatch
+# undef XXH3_128bits_withSeed
+# define XXH3_128bits_withSeed XXH3_128bits_withSeed_dispatch
+# undef XXH3_128bits_withSecret
+# define XXH3_128bits_withSecret XXH3_128bits_withSecret_dispatch
+# undef XXH3_128bits_update
+# define XXH3_128bits_update XXH3_128bits_update_dispatch
+
+#endif /* XXH_DISPATCH_DISABLE_REPLACE */
+
+#endif /* XXH_X86DISPATCH_H_13563687684 */
diff --git a/src/third-party/xxHash/xxhash.c b/src/third-party/xxHash/xxhash.c
new file mode 100644
index 0000000..083b039
--- /dev/null
+++ b/src/third-party/xxHash/xxhash.c
@@ -0,0 +1,43 @@
+/*
+ * xxHash - Extremely Fast Hash algorithm
+ * Copyright (C) 2012-2021 Yann Collet
+ *
+ * BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * You can contact the author at:
+ * - xxHash homepage: https://www.xxhash.com
+ * - xxHash source repository: https://github.com/Cyan4973/xxHash
+ */
+
+
+/*
+ * xxhash.c instantiates functions defined in xxhash.h
+ */
+
+#define XXH_STATIC_LINKING_ONLY /* access advanced declarations */
+#define XXH_IMPLEMENTATION /* access definitions */
+
+#include "xxhash.h"
diff --git a/src/third-party/xxHash/xxhash.h b/src/third-party/xxHash/xxhash.h
new file mode 100644
index 0000000..331f0ae
--- /dev/null
+++ b/src/third-party/xxHash/xxhash.h
@@ -0,0 +1,6075 @@
+/*
+ * xxHash - Extremely Fast Hash algorithm
+ * Header File
+ * Copyright (C) 2012-2021 Yann Collet
+ *
+ * BSD 2-Clause License (https://www.opensource.org/licenses/bsd-license.php)
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * You can contact the author at:
+ * - xxHash homepage: https://www.xxhash.com
+ * - xxHash source repository: https://github.com/Cyan4973/xxHash
+ */
+
+/*!
+ * @mainpage xxHash
+ *
+ * xxHash is an extremely fast non-cryptographic hash algorithm, working at RAM speed
+ * limits.
+ *
+ * It is proposed in four flavors, in three families:
+ * 1. @ref XXH32_family
+ * - Classic 32-bit hash function. Simple, compact, and runs on almost all
+ * 32-bit and 64-bit systems.
+ * 2. @ref XXH64_family
+ * - Classic 64-bit adaptation of XXH32. Just as simple, and runs well on most
+ * 64-bit systems (but _not_ 32-bit systems).
+ * 3. @ref XXH3_family
+ * - Modern 64-bit and 128-bit hash function family which features improved
+ * strength and performance across the board, especially on smaller data.
+ * It benefits greatly from SIMD and 64-bit without requiring it.
+ *
+ * Benchmarks
+ * ---
+ * The reference system uses an Intel i7-9700K CPU, and runs Ubuntu x64 20.04.
+ * The open source benchmark program is compiled with clang v10.0 using -O3 flag.
+ *
+ * | Hash Name | ISA ext | Width | Large Data Speed | Small Data Velocity |
+ * | -------------------- | ------- | ----: | ---------------: | ------------------: |
+ * | XXH3_64bits() | @b AVX2 | 64 | 59.4 GB/s | 133.1 |
+ * | MeowHash | AES-NI | 128 | 58.2 GB/s | 52.5 |
+ * | XXH3_128bits() | @b AVX2 | 128 | 57.9 GB/s | 118.1 |
+ * | CLHash | PCLMUL | 64 | 37.1 GB/s | 58.1 |
+ * | XXH3_64bits() | @b SSE2 | 64 | 31.5 GB/s | 133.1 |
+ * | XXH3_128bits() | @b SSE2 | 128 | 29.6 GB/s | 118.1 |
+ * | RAM sequential read | | N/A | 28.0 GB/s | N/A |
+ * | ahash | AES-NI | 64 | 22.5 GB/s | 107.2 |
+ * | City64 | | 64 | 22.0 GB/s | 76.6 |
+ * | T1ha2 | | 64 | 22.0 GB/s | 99.0 |
+ * | City128 | | 128 | 21.7 GB/s | 57.7 |
+ * | FarmHash | AES-NI | 64 | 21.3 GB/s | 71.9 |
+ * | XXH64() | | 64 | 19.4 GB/s | 71.0 |
+ * | SpookyHash | | 64 | 19.3 GB/s | 53.2 |
+ * | Mum | | 64 | 18.0 GB/s | 67.0 |
+ * | CRC32C | SSE4.2 | 32 | 13.0 GB/s | 57.9 |
+ * | XXH32() | | 32 | 9.7 GB/s | 71.9 |
+ * | City32 | | 32 | 9.1 GB/s | 66.0 |
+ * | Blake3* | @b AVX2 | 256 | 4.4 GB/s | 8.1 |
+ * | Murmur3 | | 32 | 3.9 GB/s | 56.1 |
+ * | SipHash* | | 64 | 3.0 GB/s | 43.2 |
+ * | Blake3* | @b SSE2 | 256 | 2.4 GB/s | 8.1 |
+ * | HighwayHash | | 64 | 1.4 GB/s | 6.0 |
+ * | FNV64 | | 64 | 1.2 GB/s | 62.7 |
+ * | Blake2* | | 256 | 1.1 GB/s | 5.1 |
+ * | SHA1* | | 160 | 0.8 GB/s | 5.6 |
+ * | MD5* | | 128 | 0.6 GB/s | 7.8 |
+ * @note
+ * - Hashes which require a specific ISA extension are noted. SSE2 is also noted,
+ * even though it is mandatory on x64.
+ * - Hashes with an asterisk are cryptographic. Note that MD5 is non-cryptographic
+ * by modern standards.
+ * - Small data velocity is a rough average of algorithm's efficiency for small
+ * data. For more accurate information, see the wiki.
+ * - More benchmarks and strength tests are found on the wiki:
+ * https://github.com/Cyan4973/xxHash/wiki
+ *
+ * Usage
+ * ------
+ * All xxHash variants use a similar API. Changing the algorithm is a trivial
+ * substitution.
+ *
+ * @pre
+ * For functions which take an input and length parameter, the following
+ * requirements are assumed:
+ * - The range from [`input`, `input + length`) is valid, readable memory.
+ * - The only exception is if the `length` is `0`, `input` may be `NULL`.
+ * - For C++, the objects must have the *TriviallyCopyable* property, as the
+ * functions access bytes directly as if it was an array of `unsigned char`.
+ *
+ * @anchor single_shot_example
+ * **Single Shot**
+ *
+ * These functions are stateless functions which hash a contiguous block of memory,
+ * immediately returning the result. They are the easiest and usually the fastest
+ * option.
+ *
+ * XXH32(), XXH64(), XXH3_64bits(), XXH3_128bits()
+ *
+ * @code{.c}
+ * #include <string.h>
+ * #include "xxhash.h"
+ *
+ * // Example for a function which hashes a null terminated string with XXH32().
+ * XXH32_hash_t hash_string(const char* string, XXH32_hash_t seed)
+ * {
+ * // NULL pointers are only valid if the length is zero
+ * size_t length = (string == NULL) ? 0 : strlen(string);
+ * return XXH32(string, length, seed);
+ * }
+ * @endcode
+ *
+ * @anchor streaming_example
+ * **Streaming**
+ *
+ * These groups of functions allow incremental hashing of unknown size, even
+ * more than what would fit in a size_t.
+ *
+ * XXH32_reset(), XXH64_reset(), XXH3_64bits_reset(), XXH3_128bits_reset()
+ *
+ * @code{.c}
+ * #include <stdio.h>
+ * #include <assert.h>
+ * #include "xxhash.h"
+ * // Example for a function which hashes a FILE incrementally with XXH3_64bits().
+ * XXH64_hash_t hashFile(FILE* f)
+ * {
+ * // Allocate a state struct. Do not just use malloc() or new.
+ * XXH3_state_t* state = XXH3_createState();
+ * assert(state != NULL && "Out of memory!");
+ * // Reset the state to start a new hashing session.
+ * XXH3_64bits_reset(state);
+ * char buffer[4096];
+ * size_t count;
+ * // Read the file in chunks
+ * while ((count = fread(buffer, 1, sizeof(buffer), f)) != 0) {
+ * // Run update() as many times as necessary to process the data
+ * XXH3_64bits_update(state, buffer, count);
+ * }
+ * // Retrieve the finalized hash. This will not change the state.
+ * XXH64_hash_t result = XXH3_64bits_digest(state);
+ * // Free the state. Do not use free().
+ * XXH3_freeState(state);
+ * return result;
+ * }
+ * @endcode
+ *
+ * @file xxhash.h
+ * xxHash prototypes and implementation
+ */
+
+#if defined (__cplusplus)
+extern "C" {
+#endif
+
+/* ****************************
+ * INLINE mode
+ ******************************/
+/*!
+ * @defgroup public Public API
+ * Contains details on the public xxHash functions.
+ * @{
+ */
+#ifdef XXH_DOXYGEN
+/*!
+ * @brief Exposes the implementation and marks all functions as `inline`.
+ *
+ * Use these build macros to inline xxhash into the target unit.
+ * Inlining improves performance on small inputs, especially when the length is
+ * expressed as a compile-time constant:
+ *
+ * https://fastcompression.blogspot.com/2018/03/xxhash-for-small-keys-impressive-power.html
+ *
+ * It also keeps xxHash symbols private to the unit, so they are not exported.
+ *
+ * Usage:
+ * @code{.c}
+ * #define XXH_INLINE_ALL
+ * #include "xxhash.h"
+ * @endcode
+ * Do not compile and link xxhash.o as a separate object, as it is not useful.
+ */
+# define XXH_INLINE_ALL
+# undef XXH_INLINE_ALL
+/*!
+ * @brief Exposes the implementation without marking functions as inline.
+ */
+# define XXH_PRIVATE_API
+# undef XXH_PRIVATE_API
+/*!
+ * @brief Emulate a namespace by transparently prefixing all symbols.
+ *
+ * If you want to include _and expose_ xxHash functions from within your own
+ * library, but also want to avoid symbol collisions with other libraries which
+ * may also include xxHash, you can use @ref XXH_NAMESPACE to automatically prefix
+ * any public symbol from xxhash library with the value of @ref XXH_NAMESPACE
+ * (therefore, avoid empty or numeric values).
+ *
+ * Note that no change is required within the calling program as long as it
+ * includes `xxhash.h`: Regular symbol names will be automatically translated
+ * by this header.
+ */
+# define XXH_NAMESPACE /* YOUR NAME HERE */
+# undef XXH_NAMESPACE
+#endif
+
+#if (defined(XXH_INLINE_ALL) || defined(XXH_PRIVATE_API)) \
+ && !defined(XXH_INLINE_ALL_31684351384)
+ /* this section should be traversed only once */
+# define XXH_INLINE_ALL_31684351384
+ /* give access to the advanced API, required to compile implementations */
+# undef XXH_STATIC_LINKING_ONLY /* avoid macro redef */
+# define XXH_STATIC_LINKING_ONLY
+ /* make all functions private */
+# undef XXH_PUBLIC_API
+# if defined(__GNUC__)
+# define XXH_PUBLIC_API static __inline __attribute__((unused))
+# elif defined (__cplusplus) || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */)
+# define XXH_PUBLIC_API static inline
+# elif defined(_MSC_VER)
+# define XXH_PUBLIC_API static __inline
+# else
+ /* note: this version may generate warnings for unused static functions */
+# define XXH_PUBLIC_API static
+# endif
+
+ /*
+ * This part deals with the special case where a unit wants to inline xxHash,
+ * but "xxhash.h" has previously been included without XXH_INLINE_ALL,
+ * such as part of some previously included *.h header file.
+ * Without further action, the new include would just be ignored,
+ * and functions would effectively _not_ be inlined (silent failure).
+ * The following macros solve this situation by prefixing all inlined names,
+ * avoiding naming collision with previous inclusions.
+ */
+ /* Before that, we unconditionally #undef all symbols,
+ * in case they were already defined with XXH_NAMESPACE.
+ * They will then be redefined for XXH_INLINE_ALL
+ */
+# undef XXH_versionNumber
+ /* XXH32 */
+# undef XXH32
+# undef XXH32_createState
+# undef XXH32_freeState
+# undef XXH32_reset
+# undef XXH32_update
+# undef XXH32_digest
+# undef XXH32_copyState
+# undef XXH32_canonicalFromHash
+# undef XXH32_hashFromCanonical
+ /* XXH64 */
+# undef XXH64
+# undef XXH64_createState
+# undef XXH64_freeState
+# undef XXH64_reset
+# undef XXH64_update
+# undef XXH64_digest
+# undef XXH64_copyState
+# undef XXH64_canonicalFromHash
+# undef XXH64_hashFromCanonical
+ /* XXH3_64bits */
+# undef XXH3_64bits
+# undef XXH3_64bits_withSecret
+# undef XXH3_64bits_withSeed
+# undef XXH3_64bits_withSecretandSeed
+# undef XXH3_createState
+# undef XXH3_freeState
+# undef XXH3_copyState
+# undef XXH3_64bits_reset
+# undef XXH3_64bits_reset_withSeed
+# undef XXH3_64bits_reset_withSecret
+# undef XXH3_64bits_update
+# undef XXH3_64bits_digest
+# undef XXH3_generateSecret
+ /* XXH3_128bits */
+# undef XXH128
+# undef XXH3_128bits
+# undef XXH3_128bits_withSeed
+# undef XXH3_128bits_withSecret
+# undef XXH3_128bits_reset
+# undef XXH3_128bits_reset_withSeed
+# undef XXH3_128bits_reset_withSecret
+# undef XXH3_128bits_reset_withSecretandSeed
+# undef XXH3_128bits_update
+# undef XXH3_128bits_digest
+# undef XXH128_isEqual
+# undef XXH128_cmp
+# undef XXH128_canonicalFromHash
+# undef XXH128_hashFromCanonical
+ /* Finally, free the namespace itself */
+# undef XXH_NAMESPACE
+
+ /* employ the namespace for XXH_INLINE_ALL */
+# define XXH_NAMESPACE XXH_INLINE_
+ /*
+ * Some identifiers (enums, type names) are not symbols,
+ * but they must nonetheless be renamed to avoid redeclaration.
+ * Alternative solution: do not redeclare them.
+ * However, this requires some #ifdefs, and has a more dispersed impact.
+ * Meanwhile, renaming can be achieved in a single place.
+ */
+# define XXH_IPREF(Id) XXH_NAMESPACE ## Id
+# define XXH_OK XXH_IPREF(XXH_OK)
+# define XXH_ERROR XXH_IPREF(XXH_ERROR)
+# define XXH_errorcode XXH_IPREF(XXH_errorcode)
+# define XXH32_canonical_t XXH_IPREF(XXH32_canonical_t)
+# define XXH64_canonical_t XXH_IPREF(XXH64_canonical_t)
+# define XXH128_canonical_t XXH_IPREF(XXH128_canonical_t)
+# define XXH32_state_s XXH_IPREF(XXH32_state_s)
+# define XXH32_state_t XXH_IPREF(XXH32_state_t)
+# define XXH64_state_s XXH_IPREF(XXH64_state_s)
+# define XXH64_state_t XXH_IPREF(XXH64_state_t)
+# define XXH3_state_s XXH_IPREF(XXH3_state_s)
+# define XXH3_state_t XXH_IPREF(XXH3_state_t)
+# define XXH128_hash_t XXH_IPREF(XXH128_hash_t)
+ /* Ensure the header is parsed again, even if it was previously included */
+# undef XXHASH_H_5627135585666179
+# undef XXHASH_H_STATIC_13879238742
+#endif /* XXH_INLINE_ALL || XXH_PRIVATE_API */
+
+/* ****************************************************************
+ * Stable API
+ *****************************************************************/
+#ifndef XXHASH_H_5627135585666179
+#define XXHASH_H_5627135585666179 1
+
+/*! @brief Marks a global symbol. */
+#if !defined(XXH_INLINE_ALL) && !defined(XXH_PRIVATE_API)
+# if defined(WIN32) && defined(_MSC_VER) && (defined(XXH_IMPORT) || defined(XXH_EXPORT))
+# ifdef XXH_EXPORT
+# define XXH_PUBLIC_API __declspec(dllexport)
+# elif XXH_IMPORT
+# define XXH_PUBLIC_API __declspec(dllimport)
+# endif
+# else
+# define XXH_PUBLIC_API /* do nothing */
+# endif
+#endif
+
+#ifdef XXH_NAMESPACE
+# define XXH_CAT(A,B) A##B
+# define XXH_NAME2(A,B) XXH_CAT(A,B)
+# define XXH_versionNumber XXH_NAME2(XXH_NAMESPACE, XXH_versionNumber)
+/* XXH32 */
+# define XXH32 XXH_NAME2(XXH_NAMESPACE, XXH32)
+# define XXH32_createState XXH_NAME2(XXH_NAMESPACE, XXH32_createState)
+# define XXH32_freeState XXH_NAME2(XXH_NAMESPACE, XXH32_freeState)
+# define XXH32_reset XXH_NAME2(XXH_NAMESPACE, XXH32_reset)
+# define XXH32_update XXH_NAME2(XXH_NAMESPACE, XXH32_update)
+# define XXH32_digest XXH_NAME2(XXH_NAMESPACE, XXH32_digest)
+# define XXH32_copyState XXH_NAME2(XXH_NAMESPACE, XXH32_copyState)
+# define XXH32_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH32_canonicalFromHash)
+# define XXH32_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH32_hashFromCanonical)
+/* XXH64 */
+# define XXH64 XXH_NAME2(XXH_NAMESPACE, XXH64)
+# define XXH64_createState XXH_NAME2(XXH_NAMESPACE, XXH64_createState)
+# define XXH64_freeState XXH_NAME2(XXH_NAMESPACE, XXH64_freeState)
+# define XXH64_reset XXH_NAME2(XXH_NAMESPACE, XXH64_reset)
+# define XXH64_update XXH_NAME2(XXH_NAMESPACE, XXH64_update)
+# define XXH64_digest XXH_NAME2(XXH_NAMESPACE, XXH64_digest)
+# define XXH64_copyState XXH_NAME2(XXH_NAMESPACE, XXH64_copyState)
+# define XXH64_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH64_canonicalFromHash)
+# define XXH64_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH64_hashFromCanonical)
+/* XXH3_64bits */
+# define XXH3_64bits XXH_NAME2(XXH_NAMESPACE, XXH3_64bits)
+# define XXH3_64bits_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_withSecret)
+# define XXH3_64bits_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_withSeed)
+# define XXH3_64bits_withSecretandSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_withSecretandSeed)
+# define XXH3_createState XXH_NAME2(XXH_NAMESPACE, XXH3_createState)
+# define XXH3_freeState XXH_NAME2(XXH_NAMESPACE, XXH3_freeState)
+# define XXH3_copyState XXH_NAME2(XXH_NAMESPACE, XXH3_copyState)
+# define XXH3_64bits_reset XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset)
+# define XXH3_64bits_reset_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset_withSeed)
+# define XXH3_64bits_reset_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset_withSecret)
+# define XXH3_64bits_reset_withSecretandSeed XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_reset_withSecretandSeed)
+# define XXH3_64bits_update XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_update)
+# define XXH3_64bits_digest XXH_NAME2(XXH_NAMESPACE, XXH3_64bits_digest)
+# define XXH3_generateSecret XXH_NAME2(XXH_NAMESPACE, XXH3_generateSecret)
+# define XXH3_generateSecret_fromSeed XXH_NAME2(XXH_NAMESPACE, XXH3_generateSecret_fromSeed)
+/* XXH3_128bits */
+# define XXH128 XXH_NAME2(XXH_NAMESPACE, XXH128)
+# define XXH3_128bits XXH_NAME2(XXH_NAMESPACE, XXH3_128bits)
+# define XXH3_128bits_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_withSeed)
+# define XXH3_128bits_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_withSecret)
+# define XXH3_128bits_withSecretandSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_withSecretandSeed)
+# define XXH3_128bits_reset XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset)
+# define XXH3_128bits_reset_withSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset_withSeed)
+# define XXH3_128bits_reset_withSecret XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset_withSecret)
+# define XXH3_128bits_reset_withSecretandSeed XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_reset_withSecretandSeed)
+# define XXH3_128bits_update XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_update)
+# define XXH3_128bits_digest XXH_NAME2(XXH_NAMESPACE, XXH3_128bits_digest)
+# define XXH128_isEqual XXH_NAME2(XXH_NAMESPACE, XXH128_isEqual)
+# define XXH128_cmp XXH_NAME2(XXH_NAMESPACE, XXH128_cmp)
+# define XXH128_canonicalFromHash XXH_NAME2(XXH_NAMESPACE, XXH128_canonicalFromHash)
+# define XXH128_hashFromCanonical XXH_NAME2(XXH_NAMESPACE, XXH128_hashFromCanonical)
+#endif
+
+
+/* *************************************
+* Compiler specifics
+***************************************/
+
+/* specific declaration modes for Windows */
+#if !defined(XXH_INLINE_ALL) && !defined(XXH_PRIVATE_API)
+# if defined(WIN32) && defined(_MSC_VER) && (defined(XXH_IMPORT) || defined(XXH_EXPORT))
+# ifdef XXH_EXPORT
+# define XXH_PUBLIC_API __declspec(dllexport)
+# elif XXH_IMPORT
+# define XXH_PUBLIC_API __declspec(dllimport)
+# endif
+# else
+# define XXH_PUBLIC_API /* do nothing */
+# endif
+#endif
+
+#if defined (__GNUC__)
+# define XXH_CONSTF __attribute__((const))
+# define XXH_PUREF __attribute__((pure))
+# define XXH_MALLOCF __attribute__((malloc))
+#else
+# define XXH_CONSTF /* disable */
+# define XXH_PUREF
+# define XXH_MALLOCF
+#endif
+
+/* *************************************
+* Version
+***************************************/
+#define XXH_VERSION_MAJOR 0
+#define XXH_VERSION_MINOR 8
+#define XXH_VERSION_RELEASE 1
+/*! @brief Version number, encoded as two digits each */
+#define XXH_VERSION_NUMBER (XXH_VERSION_MAJOR *100*100 + XXH_VERSION_MINOR *100 + XXH_VERSION_RELEASE)
+
+/*!
+ * @brief Obtains the xxHash version.
+ *
+ * This is mostly useful when xxHash is compiled as a shared library,
+ * since the returned value comes from the library, as opposed to header file.
+ *
+ * @return @ref XXH_VERSION_NUMBER of the invoked library.
+ */
+XXH_PUBLIC_API XXH_CONSTF unsigned XXH_versionNumber (void);
+
+
+/* ****************************
+* Common basic types
+******************************/
+#include <stddef.h> /* size_t */
+/*!
+ * @brief Exit code for the streaming API.
+ */
+typedef enum {
+ XXH_OK = 0, /*!< OK */
+ XXH_ERROR /*!< Error */
+} XXH_errorcode;
+
+
+/*-**********************************************************************
+* 32-bit hash
+************************************************************************/
+#if defined(XXH_DOXYGEN) /* Don't show <stdint.h> include */
+/*!
+ * @brief An unsigned 32-bit integer.
+ *
+ * Not necessarily defined to `uint32_t` but functionally equivalent.
+ */
+typedef uint32_t XXH32_hash_t;
+
+#elif !defined (__VMS) \
+ && (defined (__cplusplus) \
+ || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) )
+# include <stdint.h>
+ typedef uint32_t XXH32_hash_t;
+
+#else
+# include <limits.h>
+# if UINT_MAX == 0xFFFFFFFFUL
+ typedef unsigned int XXH32_hash_t;
+# elif ULONG_MAX == 0xFFFFFFFFUL
+ typedef unsigned long XXH32_hash_t;
+# else
+# error "unsupported platform: need a 32-bit type"
+# endif
+#endif
+
+/*!
+ * @}
+ *
+ * @defgroup XXH32_family XXH32 family
+ * @ingroup public
+ * Contains functions used in the classic 32-bit xxHash algorithm.
+ *
+ * @note
+ * XXH32 is useful for older platforms, with no or poor 64-bit performance.
+ * Note that the @ref XXH3_family provides competitive speed for both 32-bit
+ * and 64-bit systems, and offers true 64/128 bit hash results.
+ *
+ * @see @ref XXH64_family, @ref XXH3_family : Other xxHash families
+ * @see @ref XXH32_impl for implementation details
+ * @{
+ */
+
+/*!
+ * @brief Calculates the 32-bit hash of @p input using xxHash32.
+ *
+ * Speed on Core 2 Duo @ 3 GHz (single thread, SMHasher benchmark): 5.4 GB/s
+ *
+ * See @ref single_shot_example "Single Shot Example" for an example.
+ *
+ * @param input The block of data to be hashed, at least @p length bytes in size.
+ * @param length The length of @p input, in bytes.
+ * @param seed The 32-bit seed to alter the hash's output predictably.
+ *
+ * @pre
+ * The memory between @p input and @p input + @p length must be valid,
+ * readable, contiguous memory. However, if @p length is `0`, @p input may be
+ * `NULL`. In C++, this also must be *TriviallyCopyable*.
+ *
+ * @return The calculated 32-bit hash value.
+ *
+ * @see
+ * XXH64(), XXH3_64bits_withSeed(), XXH3_128bits_withSeed(), XXH128():
+ * Direct equivalents for the other variants of xxHash.
+ * @see
+ * XXH32_createState(), XXH32_update(), XXH32_digest(): Streaming version.
+ */
+XXH_PUBLIC_API XXH_PUREF XXH32_hash_t XXH32 (const void* input, size_t length, XXH32_hash_t seed);
+
+#ifndef XXH_NO_STREAM
+/*!
+ * Streaming functions generate the xxHash value from an incremental input.
+ * This method is slower than single-call functions, due to state management.
+ * For small inputs, prefer `XXH32()` and `XXH64()`, which are better optimized.
+ *
+ * An XXH state must first be allocated using `XXH*_createState()`.
+ *
+ * Start a new hash by initializing the state with a seed using `XXH*_reset()`.
+ *
+ * Then, feed the hash state by calling `XXH*_update()` as many times as necessary.
+ *
+ * The function returns an error code, with 0 meaning OK, and any other value
+ * meaning there is an error.
+ *
+ * Finally, a hash value can be produced anytime, by using `XXH*_digest()`.
+ * This function returns the nn-bits hash as an int or long long.
+ *
+ * It's still possible to continue inserting input into the hash state after a
+ * digest, and generate new hash values later on by invoking `XXH*_digest()`.
+ *
+ * When done, release the state using `XXH*_freeState()`.
+ *
+ * @see streaming_example at the top of @ref xxhash.h for an example.
+ */
+
+/*!
+ * @typedef struct XXH32_state_s XXH32_state_t
+ * @brief The opaque state struct for the XXH32 streaming API.
+ *
+ * @see XXH32_state_s for details.
+ */
+typedef struct XXH32_state_s XXH32_state_t;
+
+/*!
+ * @brief Allocates an @ref XXH32_state_t.
+ *
+ * Must be freed with XXH32_freeState().
+ * @return An allocated XXH32_state_t on success, `NULL` on failure.
+ */
+XXH_PUBLIC_API XXH_MALLOCF XXH32_state_t* XXH32_createState(void);
+/*!
+ * @brief Frees an @ref XXH32_state_t.
+ *
+ * Must be allocated with XXH32_createState().
+ * @param statePtr A pointer to an @ref XXH32_state_t allocated with @ref XXH32_createState().
+ * @return XXH_OK.
+ */
+XXH_PUBLIC_API XXH_errorcode XXH32_freeState(XXH32_state_t* statePtr);
+/*!
+ * @brief Copies one @ref XXH32_state_t to another.
+ *
+ * @param dst_state The state to copy to.
+ * @param src_state The state to copy from.
+ * @pre
+ * @p dst_state and @p src_state must not be `NULL` and must not overlap.
+ */
+XXH_PUBLIC_API void XXH32_copyState(XXH32_state_t* dst_state, const XXH32_state_t* src_state);
+
+/*!
+ * @brief Resets an @ref XXH32_state_t to begin a new hash.
+ *
+ * This function resets and seeds a state. Call it before @ref XXH32_update().
+ *
+ * @param statePtr The state struct to reset.
+ * @param seed The 32-bit seed to alter the hash result predictably.
+ *
+ * @pre
+ * @p statePtr must not be `NULL`.
+ *
+ * @return @ref XXH_OK on success, @ref XXH_ERROR on failure.
+ */
+XXH_PUBLIC_API XXH_errorcode XXH32_reset (XXH32_state_t* statePtr, XXH32_hash_t seed);
+
+/*!
+ * @brief Consumes a block of @p input to an @ref XXH32_state_t.
+ *
+ * Call this to incrementally consume blocks of data.
+ *
+ * @param statePtr The state struct to update.
+ * @param input The block of data to be hashed, at least @p length bytes in size.
+ * @param length The length of @p input, in bytes.
+ *
+ * @pre
+ * @p statePtr must not be `NULL`.
+ * @pre
+ * The memory between @p input and @p input + @p length must be valid,
+ * readable, contiguous memory. However, if @p length is `0`, @p input may be
+ * `NULL`. In C++, this also must be *TriviallyCopyable*.
+ *
+ * @return @ref XXH_OK on success, @ref XXH_ERROR on failure.
+ */
+XXH_PUBLIC_API XXH_errorcode XXH32_update (XXH32_state_t* statePtr, const void* input, size_t length);
+
+/*!
+ * @brief Returns the calculated hash value from an @ref XXH32_state_t.
+ *
+ * @note
+ * Calling XXH32_digest() will not affect @p statePtr, so you can update,
+ * digest, and update again.
+ *
+ * @param statePtr The state struct to calculate the hash from.
+ *
+ * @pre
+ * @p statePtr must not be `NULL`.
+ *
+ * @return The calculated xxHash32 value from that state.
+ */
+XXH_PUBLIC_API XXH_PUREF XXH32_hash_t XXH32_digest (const XXH32_state_t* statePtr);
+#endif /* !XXH_NO_STREAM */
+
+/******* Canonical representation *******/
+
+/*
+ * The default return values from XXH functions are unsigned 32 and 64 bit
+ * integers.
+ * This the simplest and fastest format for further post-processing.
+ *
+ * However, this leaves open the question of what is the order on the byte level,
+ * since little and big endian conventions will store the same number differently.
+ *
+ * The canonical representation settles this issue by mandating big-endian
+ * convention, the same convention as human-readable numbers (large digits first).
+ *
+ * When writing hash values to storage, sending them over a network, or printing
+ * them, it's highly recommended to use the canonical representation to ensure
+ * portability across a wider range of systems, present and future.
+ *
+ * The following functions allow transformation of hash values to and from
+ * canonical format.
+ */
+
+/*!
+ * @brief Canonical (big endian) representation of @ref XXH32_hash_t.
+ */
+typedef struct {
+ unsigned char digest[4]; /*!< Hash bytes, big endian */
+} XXH32_canonical_t;
+
+/*!
+ * @brief Converts an @ref XXH32_hash_t to a big endian @ref XXH32_canonical_t.
+ *
+ * @param dst The @ref XXH32_canonical_t pointer to be stored to.
+ * @param hash The @ref XXH32_hash_t to be converted.
+ *
+ * @pre
+ * @p dst must not be `NULL`.
+ */
+XXH_PUBLIC_API void XXH32_canonicalFromHash(XXH32_canonical_t* dst, XXH32_hash_t hash);
+
+/*!
+ * @brief Converts an @ref XXH32_canonical_t to a native @ref XXH32_hash_t.
+ *
+ * @param src The @ref XXH32_canonical_t to convert.
+ *
+ * @pre
+ * @p src must not be `NULL`.
+ *
+ * @return The converted hash.
+ */
+XXH_PUBLIC_API XXH_PUREF XXH32_hash_t XXH32_hashFromCanonical(const XXH32_canonical_t* src);
+
+
+#ifdef __has_attribute
+# define XXH_HAS_ATTRIBUTE(x) __has_attribute(x)
+#else
+# define XXH_HAS_ATTRIBUTE(x) 0
+#endif
+
+/* C-language Attributes are added in C23. */
+#if defined(__STDC_VERSION__) && (__STDC_VERSION__ > 201710L) && defined(__has_c_attribute)
+# define XXH_HAS_C_ATTRIBUTE(x) __has_c_attribute(x)
+#else
+# define XXH_HAS_C_ATTRIBUTE(x) 0
+#endif
+
+#if defined(__cplusplus) && defined(__has_cpp_attribute)
+# define XXH_HAS_CPP_ATTRIBUTE(x) __has_cpp_attribute(x)
+#else
+# define XXH_HAS_CPP_ATTRIBUTE(x) 0
+#endif
+
+/*
+ * Define XXH_FALLTHROUGH macro for annotating switch case with the 'fallthrough' attribute
+ * introduced in CPP17 and C23.
+ * CPP17 : https://en.cppreference.com/w/cpp/language/attributes/fallthrough
+ * C23 : https://en.cppreference.com/w/c/language/attributes/fallthrough
+ */
+#if XXH_HAS_C_ATTRIBUTE(fallthrough) || XXH_HAS_CPP_ATTRIBUTE(fallthrough)
+# define XXH_FALLTHROUGH [[fallthrough]]
+#elif XXH_HAS_ATTRIBUTE(__fallthrough__)
+# define XXH_FALLTHROUGH __attribute__ ((__fallthrough__))
+#else
+# define XXH_FALLTHROUGH /* fallthrough */
+#endif
+
+/*!
+ * @}
+ * @ingroup public
+ * @{
+ */
+
+#ifndef XXH_NO_LONG_LONG
+/*-**********************************************************************
+* 64-bit hash
+************************************************************************/
+#if defined(XXH_DOXYGEN) /* don't include <stdint.h> */
+/*!
+ * @brief An unsigned 64-bit integer.
+ *
+ * Not necessarily defined to `uint64_t` but functionally equivalent.
+ */
+typedef uint64_t XXH64_hash_t;
+#elif !defined (__VMS) \
+ && (defined (__cplusplus) \
+ || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) )
+# include <stdint.h>
+ typedef uint64_t XXH64_hash_t;
+#else
+# include <limits.h>
+# if defined(__LP64__) && ULONG_MAX == 0xFFFFFFFFFFFFFFFFULL
+ /* LP64 ABI says uint64_t is unsigned long */
+ typedef unsigned long XXH64_hash_t;
+# else
+ /* the following type must have a width of 64-bit */
+ typedef unsigned long long XXH64_hash_t;
+# endif
+#endif
+
+/*!
+ * @}
+ *
+ * @defgroup XXH64_family XXH64 family
+ * @ingroup public
+ * @{
+ * Contains functions used in the classic 64-bit xxHash algorithm.
+ *
+ * @note
+ * XXH3 provides competitive speed for both 32-bit and 64-bit systems,
+ * and offers true 64/128 bit hash results.
+ * It provides better speed for systems with vector processing capabilities.
+ */
+
+/*!
+ * @brief Calculates the 64-bit hash of @p input using xxHash64.
+ *
+ * This function usually runs faster on 64-bit systems, but slower on 32-bit
+ * systems (see benchmark).
+ *
+ * @param input The block of data to be hashed, at least @p length bytes in size.
+ * @param length The length of @p input, in bytes.
+ * @param seed The 64-bit seed to alter the hash's output predictably.
+ *
+ * @pre
+ * The memory between @p input and @p input + @p length must be valid,
+ * readable, contiguous memory. However, if @p length is `0`, @p input may be
+ * `NULL`. In C++, this also must be *TriviallyCopyable*.
+ *
+ * @return The calculated 64-bit hash.
+ *
+ * @see
+ * XXH32(), XXH3_64bits_withSeed(), XXH3_128bits_withSeed(), XXH128():
+ * Direct equivalents for the other variants of xxHash.
+ * @see
+ * XXH64_createState(), XXH64_update(), XXH64_digest(): Streaming version.
+ */
+XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH64(const void* input, size_t length, XXH64_hash_t seed);
+
+/******* Streaming *******/
+#ifndef XXH_NO_STREAM
+/*!
+ * @brief The opaque state struct for the XXH64 streaming API.
+ *
+ * @see XXH64_state_s for details.
+ */
+typedef struct XXH64_state_s XXH64_state_t; /* incomplete type */
+XXH_PUBLIC_API XXH_MALLOCF XXH64_state_t* XXH64_createState(void);
+XXH_PUBLIC_API XXH_errorcode XXH64_freeState(XXH64_state_t* statePtr);
+XXH_PUBLIC_API void XXH64_copyState(XXH64_state_t* dst_state, const XXH64_state_t* src_state);
+
+XXH_PUBLIC_API XXH_errorcode XXH64_reset (XXH64_state_t* statePtr, XXH64_hash_t seed);
+XXH_PUBLIC_API XXH_errorcode XXH64_update (XXH64_state_t* statePtr, const void* input, size_t length);
+XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH64_digest (const XXH64_state_t* statePtr);
+#endif /* !XXH_NO_STREAM */
+/******* Canonical representation *******/
+typedef struct { unsigned char digest[sizeof(XXH64_hash_t)]; } XXH64_canonical_t;
+XXH_PUBLIC_API void XXH64_canonicalFromHash(XXH64_canonical_t* dst, XXH64_hash_t hash);
+XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH64_hashFromCanonical(const XXH64_canonical_t* src);
+
+#ifndef XXH_NO_XXH3
+
+/*!
+ * @}
+ * ************************************************************************
+ * @defgroup XXH3_family XXH3 family
+ * @ingroup public
+ * @{
+ *
+ * XXH3 is a more recent hash algorithm featuring:
+ * - Improved speed for both small and large inputs
+ * - True 64-bit and 128-bit outputs
+ * - SIMD acceleration
+ * - Improved 32-bit viability
+ *
+ * Speed analysis methodology is explained here:
+ *
+ * https://fastcompression.blogspot.com/2019/03/presenting-xxh3.html
+ *
+ * Compared to XXH64, expect XXH3 to run approximately
+ * ~2x faster on large inputs and >3x faster on small ones,
+ * exact differences vary depending on platform.
+ *
+ * XXH3's speed benefits greatly from SIMD and 64-bit arithmetic,
+ * but does not require it.
+ * Most 32-bit and 64-bit targets that can run XXH32 smoothly can run XXH3
+ * at competitive speeds, even without vector support. Further details are
+ * explained in the implementation.
+ *
+ * Optimized implementations are provided for AVX512, AVX2, SSE2, NEON, POWER8,
+ * ZVector and scalar targets. This can be controlled via the @ref XXH_VECTOR
+ * macro. For the x86 family, an automatic dispatcher is included separately
+ * in @ref xxh_x86dispatch.c.
+ *
+ * XXH3 implementation is portable:
+ * it has a generic C90 formulation that can be compiled on any platform,
+ * all implementations generage exactly the same hash value on all platforms.
+ * Starting from v0.8.0, it's also labelled "stable", meaning that
+ * any future version will also generate the same hash value.
+ *
+ * XXH3 offers 2 variants, _64bits and _128bits.
+ *
+ * When only 64 bits are needed, prefer invoking the _64bits variant, as it
+ * reduces the amount of mixing, resulting in faster speed on small inputs.
+ * It's also generally simpler to manipulate a scalar return type than a struct.
+ *
+ * The API supports one-shot hashing, streaming mode, and custom secrets.
+ */
+/*-**********************************************************************
+* XXH3 64-bit variant
+************************************************************************/
+
+/*!
+ * @brief 64-bit unseeded variant of XXH3.
+ *
+ * This is equivalent to @ref XXH3_64bits_withSeed() with a seed of 0, however
+ * it may have slightly better performance due to constant propagation of the
+ * defaults.
+ *
+ * @see
+ * XXH32(), XXH64(), XXH3_128bits(): equivalent for the other xxHash algorithms
+ * @see
+ * XXH3_64bits_withSeed(), XXH3_64bits_withSecret(): other seeding variants
+ * @see
+ * XXH3_64bits_reset(), XXH3_64bits_update(), XXH3_64bits_digest(): Streaming version.
+ */
+XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH3_64bits(const void* input, size_t length);
+
+/*!
+ * @brief 64-bit seeded variant of XXH3
+ *
+ * This variant generates a custom secret on the fly based on default secret
+ * altered using the `seed` value.
+ *
+ * While this operation is decently fast, note that it's not completely free.
+ *
+ * @note
+ * seed == 0 produces the same results as @ref XXH3_64bits().
+ *
+ * @param input The data to hash
+ * @param length The length
+ * @param seed The 64-bit seed to alter the state.
+ */
+XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH3_64bits_withSeed(const void* input, size_t length, XXH64_hash_t seed);
+
+/*!
+ * The bare minimum size for a custom secret.
+ *
+ * @see
+ * XXH3_64bits_withSecret(), XXH3_64bits_reset_withSecret(),
+ * XXH3_128bits_withSecret(), XXH3_128bits_reset_withSecret().
+ */
+#define XXH3_SECRET_SIZE_MIN 136
+
+/*!
+ * @brief 64-bit variant of XXH3 with a custom "secret".
+ *
+ * It's possible to provide any blob of bytes as a "secret" to generate the hash.
+ * This makes it more difficult for an external actor to prepare an intentional collision.
+ * The main condition is that secretSize *must* be large enough (>= XXH3_SECRET_SIZE_MIN).
+ * However, the quality of the secret impacts the dispersion of the hash algorithm.
+ * Therefore, the secret _must_ look like a bunch of random bytes.
+ * Avoid "trivial" or structured data such as repeated sequences or a text document.
+ * Whenever in doubt about the "randomness" of the blob of bytes,
+ * consider employing "XXH3_generateSecret()" instead (see below).
+ * It will generate a proper high entropy secret derived from the blob of bytes.
+ * Another advantage of using XXH3_generateSecret() is that
+ * it guarantees that all bits within the initial blob of bytes
+ * will impact every bit of the output.
+ * This is not necessarily the case when using the blob of bytes directly
+ * because, when hashing _small_ inputs, only a portion of the secret is employed.
+ */
+XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH3_64bits_withSecret(const void* data, size_t len, const void* secret, size_t secretSize);
+
+
+/******* Streaming *******/
+#ifndef XXH_NO_STREAM
+/*
+ * Streaming requires state maintenance.
+ * This operation costs memory and CPU.
+ * As a consequence, streaming is slower than one-shot hashing.
+ * For better performance, prefer one-shot functions whenever applicable.
+ */
+
+/*!
+ * @brief The state struct for the XXH3 streaming API.
+ *
+ * @see XXH3_state_s for details.
+ */
+typedef struct XXH3_state_s XXH3_state_t;
+XXH_PUBLIC_API XXH_MALLOCF XXH3_state_t* XXH3_createState(void);
+XXH_PUBLIC_API XXH_errorcode XXH3_freeState(XXH3_state_t* statePtr);
+XXH_PUBLIC_API void XXH3_copyState(XXH3_state_t* dst_state, const XXH3_state_t* src_state);
+
+/*
+ * XXH3_64bits_reset():
+ * Initialize with default parameters.
+ * digest will be equivalent to `XXH3_64bits()`.
+ */
+XXH_PUBLIC_API XXH_errorcode XXH3_64bits_reset(XXH3_state_t* statePtr);
+/*
+ * XXH3_64bits_reset_withSeed():
+ * Generate a custom secret from `seed`, and store it into `statePtr`.
+ * digest will be equivalent to `XXH3_64bits_withSeed()`.
+ */
+XXH_PUBLIC_API XXH_errorcode XXH3_64bits_reset_withSeed(XXH3_state_t* statePtr, XXH64_hash_t seed);
+/*!
+ * XXH3_64bits_reset_withSecret():
+ * `secret` is referenced, it _must outlive_ the hash streaming session.
+ * Similar to one-shot API, `secretSize` must be >= `XXH3_SECRET_SIZE_MIN`,
+ * and the quality of produced hash values depends on secret's entropy
+ * (secret's content should look like a bunch of random bytes).
+ * When in doubt about the randomness of a candidate `secret`,
+ * consider employing `XXH3_generateSecret()` instead (see below).
+ */
+XXH_PUBLIC_API XXH_errorcode XXH3_64bits_reset_withSecret(XXH3_state_t* statePtr, const void* secret, size_t secretSize);
+
+XXH_PUBLIC_API XXH_errorcode XXH3_64bits_update (XXH3_state_t* statePtr, const void* input, size_t length);
+XXH_PUBLIC_API XXH_PUREF XXH64_hash_t XXH3_64bits_digest (const XXH3_state_t* statePtr);
+#endif /* !XXH_NO_STREAM */
+
+/* note : canonical representation of XXH3 is the same as XXH64
+ * since they both produce XXH64_hash_t values */
+
+
+/*-**********************************************************************
+* XXH3 128-bit variant
+************************************************************************/
+
+/*!
+ * @brief The return value from 128-bit hashes.
+ *
+ * Stored in little endian order, although the fields themselves are in native
+ * endianness.
+ */
+typedef struct {
+ XXH64_hash_t low64; /*!< `value & 0xFFFFFFFFFFFFFFFF` */
+ XXH64_hash_t high64; /*!< `value >> 64` */
+} XXH128_hash_t;
+
+/*!
+ * @brief Unseeded 128-bit variant of XXH3
+ *
+ * The 128-bit variant of XXH3 has more strength, but it has a bit of overhead
+ * for shorter inputs.
+ *
+ * This is equivalent to @ref XXH3_128bits_withSeed() with a seed of 0, however
+ * it may have slightly better performance due to constant propagation of the
+ * defaults.
+ *
+ * @see
+ * XXH32(), XXH64(), XXH3_64bits(): equivalent for the other xxHash algorithms
+ * @see
+ * XXH3_128bits_withSeed(), XXH3_128bits_withSecret(): other seeding variants
+ * @see
+ * XXH3_128bits_reset(), XXH3_128bits_update(), XXH3_128bits_digest(): Streaming version.
+ */
+XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH3_128bits(const void* data, size_t len);
+/*! @brief Seeded 128-bit variant of XXH3. @see XXH3_64bits_withSeed(). */
+XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH3_128bits_withSeed(const void* data, size_t len, XXH64_hash_t seed);
+/*! @brief Custom secret 128-bit variant of XXH3. @see XXH3_64bits_withSecret(). */
+XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH3_128bits_withSecret(const void* data, size_t len, const void* secret, size_t secretSize);
+
+/******* Streaming *******/
+#ifndef XXH_NO_STREAM
+/*
+ * Streaming requires state maintenance.
+ * This operation costs memory and CPU.
+ * As a consequence, streaming is slower than one-shot hashing.
+ * For better performance, prefer one-shot functions whenever applicable.
+ *
+ * XXH3_128bits uses the same XXH3_state_t as XXH3_64bits().
+ * Use already declared XXH3_createState() and XXH3_freeState().
+ *
+ * All reset and streaming functions have same meaning as their 64-bit counterpart.
+ */
+
+XXH_PUBLIC_API XXH_errorcode XXH3_128bits_reset(XXH3_state_t* statePtr);
+XXH_PUBLIC_API XXH_errorcode XXH3_128bits_reset_withSeed(XXH3_state_t* statePtr, XXH64_hash_t seed);
+XXH_PUBLIC_API XXH_errorcode XXH3_128bits_reset_withSecret(XXH3_state_t* statePtr, const void* secret, size_t secretSize);
+
+XXH_PUBLIC_API XXH_errorcode XXH3_128bits_update (XXH3_state_t* statePtr, const void* input, size_t length);
+XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH3_128bits_digest (const XXH3_state_t* statePtr);
+#endif /* !XXH_NO_STREAM */
+
+/* Following helper functions make it possible to compare XXH128_hast_t values.
+ * Since XXH128_hash_t is a structure, this capability is not offered by the language.
+ * Note: For better performance, these functions can be inlined using XXH_INLINE_ALL */
+
+/*!
+ * XXH128_isEqual():
+ * Return: 1 if `h1` and `h2` are equal, 0 if they are not.
+ */
+XXH_PUBLIC_API XXH_PUREF int XXH128_isEqual(XXH128_hash_t h1, XXH128_hash_t h2);
+
+/*!
+ * @brief Compares two @ref XXH128_hash_t
+ * This comparator is compatible with stdlib's `qsort()`/`bsearch()`.
+ *
+ * @return: >0 if *h128_1 > *h128_2
+ * =0 if *h128_1 == *h128_2
+ * <0 if *h128_1 < *h128_2
+ */
+XXH_PUBLIC_API XXH_PUREF int XXH128_cmp(const void* h128_1, const void* h128_2);
+
+
+/******* Canonical representation *******/
+typedef struct { unsigned char digest[sizeof(XXH128_hash_t)]; } XXH128_canonical_t;
+XXH_PUBLIC_API void XXH128_canonicalFromHash(XXH128_canonical_t* dst, XXH128_hash_t hash);
+XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH128_hashFromCanonical(const XXH128_canonical_t* src);
+
+
+#endif /* !XXH_NO_XXH3 */
+#endif /* XXH_NO_LONG_LONG */
+
+/*!
+ * @}
+ */
+#endif /* XXHASH_H_5627135585666179 */
+
+
+
+#if defined(XXH_STATIC_LINKING_ONLY) && !defined(XXHASH_H_STATIC_13879238742)
+#define XXHASH_H_STATIC_13879238742
+/* ****************************************************************************
+ * This section contains declarations which are not guaranteed to remain stable.
+ * They may change in future versions, becoming incompatible with a different
+ * version of the library.
+ * These declarations should only be used with static linking.
+ * Never use them in association with dynamic linking!
+ ***************************************************************************** */
+
+/*
+ * These definitions are only present to allow static allocation
+ * of XXH states, on stack or in a struct, for example.
+ * Never **ever** access their members directly.
+ */
+
+/*!
+ * @internal
+ * @brief Structure for XXH32 streaming API.
+ *
+ * @note This is only defined when @ref XXH_STATIC_LINKING_ONLY,
+ * @ref XXH_INLINE_ALL, or @ref XXH_IMPLEMENTATION is defined. Otherwise it is
+ * an opaque type. This allows fields to safely be changed.
+ *
+ * Typedef'd to @ref XXH32_state_t.
+ * Do not access the members of this struct directly.
+ * @see XXH64_state_s, XXH3_state_s
+ */
+struct XXH32_state_s {
+ XXH32_hash_t total_len_32; /*!< Total length hashed, modulo 2^32 */
+ XXH32_hash_t large_len; /*!< Whether the hash is >= 16 (handles @ref total_len_32 overflow) */
+ XXH32_hash_t v[4]; /*!< Accumulator lanes */
+ XXH32_hash_t mem32[4]; /*!< Internal buffer for partial reads. Treated as unsigned char[16]. */
+ XXH32_hash_t memsize; /*!< Amount of data in @ref mem32 */
+ XXH32_hash_t reserved; /*!< Reserved field. Do not read nor write to it. */
+}; /* typedef'd to XXH32_state_t */
+
+
+#ifndef XXH_NO_LONG_LONG /* defined when there is no 64-bit support */
+
+/*!
+ * @internal
+ * @brief Structure for XXH64 streaming API.
+ *
+ * @note This is only defined when @ref XXH_STATIC_LINKING_ONLY,
+ * @ref XXH_INLINE_ALL, or @ref XXH_IMPLEMENTATION is defined. Otherwise it is
+ * an opaque type. This allows fields to safely be changed.
+ *
+ * Typedef'd to @ref XXH64_state_t.
+ * Do not access the members of this struct directly.
+ * @see XXH32_state_s, XXH3_state_s
+ */
+struct XXH64_state_s {
+ XXH64_hash_t total_len; /*!< Total length hashed. This is always 64-bit. */
+ XXH64_hash_t v[4]; /*!< Accumulator lanes */
+ XXH64_hash_t mem64[4]; /*!< Internal buffer for partial reads. Treated as unsigned char[32]. */
+ XXH32_hash_t memsize; /*!< Amount of data in @ref mem64 */
+ XXH32_hash_t reserved32; /*!< Reserved field, needed for padding anyways*/
+ XXH64_hash_t reserved64; /*!< Reserved field. Do not read or write to it. */
+}; /* typedef'd to XXH64_state_t */
+
+#ifndef XXH_NO_XXH3
+
+#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) /* >= C11 */
+# include <stdalign.h>
+# define XXH_ALIGN(n) alignas(n)
+#elif defined(__cplusplus) && (__cplusplus >= 201103L) /* >= C++11 */
+/* In C++ alignas() is a keyword */
+# define XXH_ALIGN(n) alignas(n)
+#elif defined(__GNUC__)
+# define XXH_ALIGN(n) __attribute__ ((aligned(n)))
+#elif defined(_MSC_VER)
+# define XXH_ALIGN(n) __declspec(align(n))
+#else
+# define XXH_ALIGN(n) /* disabled */
+#endif
+
+/* Old GCC versions only accept the attribute after the type in structures. */
+#if !(defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L)) /* C11+ */ \
+ && ! (defined(__cplusplus) && (__cplusplus >= 201103L)) /* >= C++11 */ \
+ && defined(__GNUC__)
+# define XXH_ALIGN_MEMBER(align, type) type XXH_ALIGN(align)
+#else
+# define XXH_ALIGN_MEMBER(align, type) XXH_ALIGN(align) type
+#endif
+
+/*!
+ * @brief The size of the internal XXH3 buffer.
+ *
+ * This is the optimal update size for incremental hashing.
+ *
+ * @see XXH3_64b_update(), XXH3_128b_update().
+ */
+#define XXH3_INTERNALBUFFER_SIZE 256
+
+/*!
+ * @brief Default size of the secret buffer (and @ref XXH3_kSecret).
+ *
+ * This is the size used in @ref XXH3_kSecret and the seeded functions.
+ *
+ * Not to be confused with @ref XXH3_SECRET_SIZE_MIN.
+ */
+#define XXH3_SECRET_DEFAULT_SIZE 192
+
+/*!
+ * @internal
+ * @brief Structure for XXH3 streaming API.
+ *
+ * @note This is only defined when @ref XXH_STATIC_LINKING_ONLY,
+ * @ref XXH_INLINE_ALL, or @ref XXH_IMPLEMENTATION is defined.
+ * Otherwise it is an opaque type.
+ * Never use this definition in combination with dynamic library.
+ * This allows fields to safely be changed in the future.
+ *
+ * @note ** This structure has a strict alignment requirement of 64 bytes!! **
+ * Do not allocate this with `malloc()` or `new`,
+ * it will not be sufficiently aligned.
+ * Use @ref XXH3_createState() and @ref XXH3_freeState(), or stack allocation.
+ *
+ * Typedef'd to @ref XXH3_state_t.
+ * Do never access the members of this struct directly.
+ *
+ * @see XXH3_INITSTATE() for stack initialization.
+ * @see XXH3_createState(), XXH3_freeState().
+ * @see XXH32_state_s, XXH64_state_s
+ */
+struct XXH3_state_s {
+ XXH_ALIGN_MEMBER(64, XXH64_hash_t acc[8]);
+ /*!< The 8 accumulators. See @ref XXH32_state_s::v and @ref XXH64_state_s::v */
+ XXH_ALIGN_MEMBER(64, unsigned char customSecret[XXH3_SECRET_DEFAULT_SIZE]);
+ /*!< Used to store a custom secret generated from a seed. */
+ XXH_ALIGN_MEMBER(64, unsigned char buffer[XXH3_INTERNALBUFFER_SIZE]);
+ /*!< The internal buffer. @see XXH32_state_s::mem32 */
+ XXH32_hash_t bufferedSize;
+ /*!< The amount of memory in @ref buffer, @see XXH32_state_s::memsize */
+ XXH32_hash_t useSeed;
+ /*!< Reserved field. Needed for padding on 64-bit. */
+ size_t nbStripesSoFar;
+ /*!< Number or stripes processed. */
+ XXH64_hash_t totalLen;
+ /*!< Total length hashed. 64-bit even on 32-bit targets. */
+ size_t nbStripesPerBlock;
+ /*!< Number of stripes per block. */
+ size_t secretLimit;
+ /*!< Size of @ref customSecret or @ref extSecret */
+ XXH64_hash_t seed;
+ /*!< Seed for _withSeed variants. Must be zero otherwise, @see XXH3_INITSTATE() */
+ XXH64_hash_t reserved64;
+ /*!< Reserved field. */
+ const unsigned char* extSecret;
+ /*!< Reference to an external secret for the _withSecret variants, NULL
+ * for other variants. */
+ /* note: there may be some padding at the end due to alignment on 64 bytes */
+}; /* typedef'd to XXH3_state_t */
+
+#undef XXH_ALIGN_MEMBER
+
+/*!
+ * @brief Initializes a stack-allocated `XXH3_state_s`.
+ *
+ * When the @ref XXH3_state_t structure is merely emplaced on stack,
+ * it should be initialized with XXH3_INITSTATE() or a memset()
+ * in case its first reset uses XXH3_NNbits_reset_withSeed().
+ * This init can be omitted if the first reset uses default or _withSecret mode.
+ * This operation isn't necessary when the state is created with XXH3_createState().
+ * Note that this doesn't prepare the state for a streaming operation,
+ * it's still necessary to use XXH3_NNbits_reset*() afterwards.
+ */
+#define XXH3_INITSTATE(XXH3_state_ptr) { (XXH3_state_ptr)->seed = 0; }
+
+
+/*!
+ * simple alias to pre-selected XXH3_128bits variant
+ */
+XXH_PUBLIC_API XXH_PUREF XXH128_hash_t XXH128(const void* data, size_t len, XXH64_hash_t seed);
+
+
+/* === Experimental API === */
+/* Symbols defined below must be considered tied to a specific library version. */
+
+/*!
+ * XXH3_generateSecret():
+ *
+ * Derive a high-entropy secret from any user-defined content, named customSeed.
+ * The generated secret can be used in combination with `*_withSecret()` functions.
+ * The `_withSecret()` variants are useful to provide a higher level of protection
+ * than 64-bit seed, as it becomes much more difficult for an external actor to
+ * guess how to impact the calculation logic.
+ *
+ * The function accepts as input a custom seed of any length and any content,
+ * and derives from it a high-entropy secret of length @p secretSize into an
+ * already allocated buffer @p secretBuffer.
+ *
+ * The generated secret can then be used with any `*_withSecret()` variant.
+ * The functions @ref XXH3_128bits_withSecret(), @ref XXH3_64bits_withSecret(),
+ * @ref XXH3_128bits_reset_withSecret() and @ref XXH3_64bits_reset_withSecret()
+ * are part of this list. They all accept a `secret` parameter
+ * which must be large enough for implementation reasons (>= @ref XXH3_SECRET_SIZE_MIN)
+ * _and_ feature very high entropy (consist of random-looking bytes).
+ * These conditions can be a high bar to meet, so @ref XXH3_generateSecret() can
+ * be employed to ensure proper quality.
+ *
+ * @p customSeed can be anything. It can have any size, even small ones,
+ * and its content can be anything, even "poor entropy" sources such as a bunch
+ * of zeroes. The resulting `secret` will nonetheless provide all required qualities.
+ *
+ * @pre
+ * - @p secretSize must be >= @ref XXH3_SECRET_SIZE_MIN
+ * - When @p customSeedSize > 0, supplying NULL as customSeed is undefined behavior.
+ *
+ * Example code:
+ * @code{.c}
+ * #include <stdio.h>
+ * #include <stdlib.h>
+ * #include <string.h>
+ * #define XXH_STATIC_LINKING_ONLY // expose unstable API
+ * #include "xxhash.h"
+ * // Hashes argv[2] using the entropy from argv[1].
+ * int main(int argc, char* argv[])
+ * {
+ * char secret[XXH3_SECRET_SIZE_MIN];
+ * if (argv != 3) { return 1; }
+ * XXH3_generateSecret(secret, sizeof(secret), argv[1], strlen(argv[1]));
+ * XXH64_hash_t h = XXH3_64bits_withSecret(
+ * argv[2], strlen(argv[2]),
+ * secret, sizeof(secret)
+ * );
+ * printf("%016llx\n", (unsigned long long) h);
+ * }
+ * @endcode
+ */
+XXH_PUBLIC_API XXH_errorcode XXH3_generateSecret(void* secretBuffer, size_t secretSize, const void* customSeed, size_t customSeedSize);
+
+/*!
+ * @brief Generate the same secret as the _withSeed() variants.
+ *
+ * The generated secret can be used in combination with
+ *`*_withSecret()` and `_withSecretandSeed()` variants.
+ *
+ * Example C++ `std::string` hash class:
+ * @code{.cpp}
+ * #include <string>
+ * #define XXH_STATIC_LINKING_ONLY // expose unstable API
+ * #include "xxhash.h"
+ * // Slow, seeds each time
+ * class HashSlow {
+ * XXH64_hash_t seed;
+ * public:
+ * HashSlow(XXH64_hash_t s) : seed{s} {}
+ * size_t operator()(const std::string& x) const {
+ * return size_t{XXH3_64bits_withSeed(x.c_str(), x.length(), seed)};
+ * }
+ * };
+ * // Fast, caches the seeded secret for future uses.
+ * class HashFast {
+ * unsigned char secret[XXH3_SECRET_SIZE_MIN];
+ * public:
+ * HashFast(XXH64_hash_t s) {
+ * XXH3_generateSecret_fromSeed(secret, seed);
+ * }
+ * size_t operator()(const std::string& x) const {
+ * return size_t{
+ * XXH3_64bits_withSecret(x.c_str(), x.length(), secret, sizeof(secret))
+ * };
+ * }
+ * };
+ * @endcode
+ * @param secretBuffer A writable buffer of @ref XXH3_SECRET_SIZE_MIN bytes
+ * @param seed The seed to seed the state.
+ */
+XXH_PUBLIC_API void XXH3_generateSecret_fromSeed(void* secretBuffer, XXH64_hash_t seed);
+
+/*!
+ * These variants generate hash values using either
+ * @p seed for "short" keys (< XXH3_MIDSIZE_MAX = 240 bytes)
+ * or @p secret for "large" keys (>= XXH3_MIDSIZE_MAX).
+ *
+ * This generally benefits speed, compared to `_withSeed()` or `_withSecret()`.
+ * `_withSeed()` has to generate the secret on the fly for "large" keys.
+ * It's fast, but can be perceptible for "not so large" keys (< 1 KB).
+ * `_withSecret()` has to generate the masks on the fly for "small" keys,
+ * which requires more instructions than _withSeed() variants.
+ * Therefore, _withSecretandSeed variant combines the best of both worlds.
+ *
+ * When @p secret has been generated by XXH3_generateSecret_fromSeed(),
+ * this variant produces *exactly* the same results as `_withSeed()` variant,
+ * hence offering only a pure speed benefit on "large" input,
+ * by skipping the need to regenerate the secret for every large input.
+ *
+ * Another usage scenario is to hash the secret to a 64-bit hash value,
+ * for example with XXH3_64bits(), which then becomes the seed,
+ * and then employ both the seed and the secret in _withSecretandSeed().
+ * On top of speed, an added benefit is that each bit in the secret
+ * has a 50% chance to swap each bit in the output, via its impact to the seed.
+ *
+ * This is not guaranteed when using the secret directly in "small data" scenarios,
+ * because only portions of the secret are employed for small data.
+ */
+XXH_PUBLIC_API XXH_PUREF XXH64_hash_t
+XXH3_64bits_withSecretandSeed(const void* data, size_t len,
+ const void* secret, size_t secretSize,
+ XXH64_hash_t seed);
+/*! @copydoc XXH3_64bits_withSecretandSeed() */
+XXH_PUBLIC_API XXH_PUREF XXH128_hash_t
+XXH3_128bits_withSecretandSeed(const void* input, size_t length,
+ const void* secret, size_t secretSize,
+ XXH64_hash_t seed64);
+#ifndef XXH_NO_STREAM
+/*! @copydoc XXH3_64bits_withSecretandSeed() */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_64bits_reset_withSecretandSeed(XXH3_state_t* statePtr,
+ const void* secret, size_t secretSize,
+ XXH64_hash_t seed64);
+/*! @copydoc XXH3_64bits_withSecretandSeed() */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_128bits_reset_withSecretandSeed(XXH3_state_t* statePtr,
+ const void* secret, size_t secretSize,
+ XXH64_hash_t seed64);
+#endif /* !XXH_NO_STREAM */
+
+#endif /* !XXH_NO_XXH3 */
+#endif /* XXH_NO_LONG_LONG */
+#if defined(XXH_INLINE_ALL) || defined(XXH_PRIVATE_API)
+# define XXH_IMPLEMENTATION
+#endif
+
+#endif /* defined(XXH_STATIC_LINKING_ONLY) && !defined(XXHASH_H_STATIC_13879238742) */
+
+
+/* ======================================================================== */
+/* ======================================================================== */
+/* ======================================================================== */
+
+
+/*-**********************************************************************
+ * xxHash implementation
+ *-**********************************************************************
+ * xxHash's implementation used to be hosted inside xxhash.c.
+ *
+ * However, inlining requires implementation to be visible to the compiler,
+ * hence be included alongside the header.
+ * Previously, implementation was hosted inside xxhash.c,
+ * which was then #included when inlining was activated.
+ * This construction created issues with a few build and install systems,
+ * as it required xxhash.c to be stored in /include directory.
+ *
+ * xxHash implementation is now directly integrated within xxhash.h.
+ * As a consequence, xxhash.c is no longer needed in /include.
+ *
+ * xxhash.c is still available and is still useful.
+ * In a "normal" setup, when xxhash is not inlined,
+ * xxhash.h only exposes the prototypes and public symbols,
+ * while xxhash.c can be built into an object file xxhash.o
+ * which can then be linked into the final binary.
+ ************************************************************************/
+
+#if ( defined(XXH_INLINE_ALL) || defined(XXH_PRIVATE_API) \
+ || defined(XXH_IMPLEMENTATION) ) && !defined(XXH_IMPLEM_13a8737387)
+# define XXH_IMPLEM_13a8737387
+
+/* *************************************
+* Tuning parameters
+***************************************/
+
+/*!
+ * @defgroup tuning Tuning parameters
+ * @{
+ *
+ * Various macros to control xxHash's behavior.
+ */
+#ifdef XXH_DOXYGEN
+/*!
+ * @brief Define this to disable 64-bit code.
+ *
+ * Useful if only using the @ref XXH32_family and you have a strict C90 compiler.
+ */
+# define XXH_NO_LONG_LONG
+# undef XXH_NO_LONG_LONG /* don't actually */
+/*!
+ * @brief Controls how unaligned memory is accessed.
+ *
+ * By default, access to unaligned memory is controlled by `memcpy()`, which is
+ * safe and portable.
+ *
+ * Unfortunately, on some target/compiler combinations, the generated assembly
+ * is sub-optimal.
+ *
+ * The below switch allow selection of a different access method
+ * in the search for improved performance.
+ *
+ * @par Possible options:
+ *
+ * - `XXH_FORCE_MEMORY_ACCESS=0` (default): `memcpy`
+ * @par
+ * Use `memcpy()`. Safe and portable. Note that most modern compilers will
+ * eliminate the function call and treat it as an unaligned access.
+ *
+ * - `XXH_FORCE_MEMORY_ACCESS=1`: `__attribute__((aligned(1)))`
+ * @par
+ * Depends on compiler extensions and is therefore not portable.
+ * This method is safe _if_ your compiler supports it,
+ * and *generally* as fast or faster than `memcpy`.
+ *
+ * - `XXH_FORCE_MEMORY_ACCESS=2`: Direct cast
+ * @par
+ * Casts directly and dereferences. This method doesn't depend on the
+ * compiler, but it violates the C standard as it directly dereferences an
+ * unaligned pointer. It can generate buggy code on targets which do not
+ * support unaligned memory accesses, but in some circumstances, it's the
+ * only known way to get the most performance.
+ *
+ * - `XXH_FORCE_MEMORY_ACCESS=3`: Byteshift
+ * @par
+ * Also portable. This can generate the best code on old compilers which don't
+ * inline small `memcpy()` calls, and it might also be faster on big-endian
+ * systems which lack a native byteswap instruction. However, some compilers
+ * will emit literal byteshifts even if the target supports unaligned access.
+ * .
+ *
+ * @warning
+ * Methods 1 and 2 rely on implementation-defined behavior. Use these with
+ * care, as what works on one compiler/platform/optimization level may cause
+ * another to read garbage data or even crash.
+ *
+ * See http://fastcompression.blogspot.com/2015/08/accessing-unaligned-memory.html for details.
+ *
+ * Prefer these methods in priority order (0 > 3 > 1 > 2)
+ */
+# define XXH_FORCE_MEMORY_ACCESS 0
+
+/*!
+ * @def XXH_SIZE_OPT
+ * @brief Controls how much xxHash optimizes for size.
+ *
+ * xxHash, when compiled, tends to result in a rather large binary size. This
+ * is mostly due to heavy usage to forced inlining and constant folding of the
+ * @ref XXH3_family to increase performance.
+ *
+ * However, some developers prefer size over speed. This option can
+ * significantly reduce the size of the generated code. When using the `-Os`
+ * or `-Oz` options on GCC or Clang, this is defined to 1 by default,
+ * otherwise it is defined to 0.
+ *
+ * Most of these size optimizations can be controlled manually.
+ *
+ * This is a number from 0-2.
+ * - `XXH_SIZE_OPT` == 0: Default. xxHash makes no size optimizations. Speed
+ * comes first.
+ * - `XXH_SIZE_OPT` == 1: Default for `-Os` and `-Oz`. xxHash is more
+ * conservative and disables hacks that increase code size. It implies the
+ * options @ref XXH_NO_INLINE_HINTS == 1, @ref XXH_FORCE_ALIGN_CHECK == 0,
+ * and @ref XXH3_NEON_LANES == 8 if they are not already defined.
+ * - `XXH_SIZE_OPT` == 2: xxHash tries to make itself as small as possible.
+ * Performance may cry. For example, the single shot functions just use the
+ * streaming API.
+ */
+# define XXH_SIZE_OPT 0
+
+/*!
+ * @def XXH_FORCE_ALIGN_CHECK
+ * @brief If defined to non-zero, adds a special path for aligned inputs (XXH32()
+ * and XXH64() only).
+ *
+ * This is an important performance trick for architectures without decent
+ * unaligned memory access performance.
+ *
+ * It checks for input alignment, and when conditions are met, uses a "fast
+ * path" employing direct 32-bit/64-bit reads, resulting in _dramatically
+ * faster_ read speed.
+ *
+ * The check costs one initial branch per hash, which is generally negligible,
+ * but not zero.
+ *
+ * Moreover, it's not useful to generate an additional code path if memory
+ * access uses the same instruction for both aligned and unaligned
+ * addresses (e.g. x86 and aarch64).
+ *
+ * In these cases, the alignment check can be removed by setting this macro to 0.
+ * Then the code will always use unaligned memory access.
+ * Align check is automatically disabled on x86, x64, ARM64, and some ARM chips
+ * which are platforms known to offer good unaligned memory accesses performance.
+ *
+ * It is also disabled by default when @ref XXH_SIZE_OPT >= 1.
+ *
+ * This option does not affect XXH3 (only XXH32 and XXH64).
+ */
+# define XXH_FORCE_ALIGN_CHECK 0
+
+/*!
+ * @def XXH_NO_INLINE_HINTS
+ * @brief When non-zero, sets all functions to `static`.
+ *
+ * By default, xxHash tries to force the compiler to inline almost all internal
+ * functions.
+ *
+ * This can usually improve performance due to reduced jumping and improved
+ * constant folding, but significantly increases the size of the binary which
+ * might not be favorable.
+ *
+ * Additionally, sometimes the forced inlining can be detrimental to performance,
+ * depending on the architecture.
+ *
+ * XXH_NO_INLINE_HINTS marks all internal functions as static, giving the
+ * compiler full control on whether to inline or not.
+ *
+ * When not optimizing (-O0), using `-fno-inline` with GCC or Clang, or if
+ * @ref XXH_SIZE_OPT >= 1, this will automatically be defined.
+ */
+# define XXH_NO_INLINE_HINTS 0
+
+/*!
+ * @def XXH32_ENDJMP
+ * @brief Whether to use a jump for `XXH32_finalize`.
+ *
+ * For performance, `XXH32_finalize` uses multiple branches in the finalizer.
+ * This is generally preferable for performance,
+ * but depending on exact architecture, a jmp may be preferable.
+ *
+ * This setting is only possibly making a difference for very small inputs.
+ */
+# define XXH32_ENDJMP 0
+
+/*!
+ * @internal
+ * @brief Redefines old internal names.
+ *
+ * For compatibility with code that uses xxHash's internals before the names
+ * were changed to improve namespacing. There is no other reason to use this.
+ */
+# define XXH_OLD_NAMES
+# undef XXH_OLD_NAMES /* don't actually use, it is ugly. */
+
+/*!
+ * @def XXH_NO_STREAM
+ * @brief Disables the streaming API.
+ *
+ * When xxHash is not inlined and the streaming functions are not used, disabling
+ * the streaming functions can improve code size significantly, especially with
+ * the @ref XXH3_family which tends to make constant folded copies of itself.
+ */
+# define XXH_NO_STREAM
+# undef XXH_NO_STREAM /* don't actually */
+#endif /* XXH_DOXYGEN */
+/*!
+ * @}
+ */
+
+#ifndef XXH_FORCE_MEMORY_ACCESS /* can be defined externally, on command line for example */
+ /* prefer __packed__ structures (method 1) for GCC
+ * < ARMv7 with unaligned access (e.g. Raspbian armhf) still uses byte shifting, so we use memcpy
+ * which for some reason does unaligned loads. */
+# if defined(__GNUC__) && !(defined(__ARM_ARCH) && __ARM_ARCH < 7 && defined(__ARM_FEATURE_UNALIGNED))
+# define XXH_FORCE_MEMORY_ACCESS 1
+# endif
+#endif
+
+#ifndef XXH_SIZE_OPT
+ /* default to 1 for -Os or -Oz */
+# if (defined(__GNUC__) || defined(__clang__)) && defined(__OPTIMIZE_SIZE__)
+# define XXH_SIZE_OPT 1
+# else
+# define XXH_SIZE_OPT 0
+# endif
+#endif
+
+#ifndef XXH_FORCE_ALIGN_CHECK /* can be defined externally */
+ /* don't check on sizeopt, x86, aarch64, or arm when unaligned access is available */
+# if XXH_SIZE_OPT >= 1 || \
+ defined(__i386) || defined(__x86_64__) || defined(__aarch64__) || defined(__ARM_FEATURE_UNALIGNED) \
+ || defined(_M_IX86) || defined(_M_X64) || defined(_M_ARM64) || defined(_M_ARM) /* visual */
+# define XXH_FORCE_ALIGN_CHECK 0
+# else
+# define XXH_FORCE_ALIGN_CHECK 1
+# endif
+#endif
+
+#ifndef XXH_NO_INLINE_HINTS
+# if XXH_SIZE_OPT >= 1 || defined(__NO_INLINE__) /* -O0, -fno-inline */
+# define XXH_NO_INLINE_HINTS 1
+# else
+# define XXH_NO_INLINE_HINTS 0
+# endif
+#endif
+
+#ifndef XXH32_ENDJMP
+/* generally preferable for performance */
+# define XXH32_ENDJMP 0
+#endif
+
+/*!
+ * @defgroup impl Implementation
+ * @{
+ */
+
+
+/* *************************************
+* Includes & Memory related functions
+***************************************/
+#if defined(XXH_NO_STREAM)
+/* nothing */
+#elif defined(XXH_NO_STDLIB)
+
+/* When requesting to disable any mention of stdlib,
+ * the library loses the ability to invoked malloc / free.
+ * In practice, it means that functions like `XXH*_createState()`
+ * will always fail, and return NULL.
+ * This flag is useful in situations where
+ * xxhash.h is integrated into some kernel, embedded or limited environment
+ * without access to dynamic allocation.
+ */
+
+static XXH_CONSTF void* XXH_malloc(size_t s) { (void)s; return NULL; }
+static void XXH_free(void* p) { (void)p; }
+
+#else
+
+/*
+ * Modify the local functions below should you wish to use
+ * different memory routines for malloc() and free()
+ */
+#include <stdlib.h>
+
+/*!
+ * @internal
+ * @brief Modify this function to use a different routine than malloc().
+ */
+static XXH_MALLOCF void* XXH_malloc(size_t s) { return malloc(s); }
+
+/*!
+ * @internal
+ * @brief Modify this function to use a different routine than free().
+ */
+static void XXH_free(void* p) { free(p); }
+
+#endif /* XXH_NO_STDLIB */
+
+#include <string.h>
+
+/*!
+ * @internal
+ * @brief Modify this function to use a different routine than memcpy().
+ */
+static void* XXH_memcpy(void* dest, const void* src, size_t size)
+{
+ return memcpy(dest,src,size);
+}
+
+#include <limits.h> /* ULLONG_MAX */
+
+
+/* *************************************
+* Compiler Specific Options
+***************************************/
+#ifdef _MSC_VER /* Visual Studio warning fix */
+# pragma warning(disable : 4127) /* disable: C4127: conditional expression is constant */
+#endif
+
+#if XXH_NO_INLINE_HINTS /* disable inlining hints */
+# if defined(__GNUC__) || defined(__clang__)
+# define XXH_FORCE_INLINE static __attribute__((unused))
+# else
+# define XXH_FORCE_INLINE static
+# endif
+# define XXH_NO_INLINE static
+/* enable inlining hints */
+#elif defined(__GNUC__) || defined(__clang__)
+# define XXH_FORCE_INLINE static __inline__ __attribute__((always_inline, unused))
+# define XXH_NO_INLINE static __attribute__((noinline))
+#elif defined(_MSC_VER) /* Visual Studio */
+# define XXH_FORCE_INLINE static __forceinline
+# define XXH_NO_INLINE static __declspec(noinline)
+#elif defined (__cplusplus) \
+ || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)) /* C99 */
+# define XXH_FORCE_INLINE static inline
+# define XXH_NO_INLINE static
+#else
+# define XXH_FORCE_INLINE static
+# define XXH_NO_INLINE static
+#endif
+
+
+
+/* *************************************
+* Debug
+***************************************/
+/*!
+ * @ingroup tuning
+ * @def XXH_DEBUGLEVEL
+ * @brief Sets the debugging level.
+ *
+ * XXH_DEBUGLEVEL is expected to be defined externally, typically via the
+ * compiler's command line options. The value must be a number.
+ */
+#ifndef XXH_DEBUGLEVEL
+# ifdef DEBUGLEVEL /* backwards compat */
+# define XXH_DEBUGLEVEL DEBUGLEVEL
+# else
+# define XXH_DEBUGLEVEL 0
+# endif
+#endif
+
+#if (XXH_DEBUGLEVEL>=1)
+# include <assert.h> /* note: can still be disabled with NDEBUG */
+# define XXH_ASSERT(c) assert(c)
+#else
+# define XXH_ASSERT(c) ((void)0)
+#endif
+
+/* note: use after variable declarations */
+#ifndef XXH_STATIC_ASSERT
+# if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 201112L) /* C11 */
+# define XXH_STATIC_ASSERT_WITH_MESSAGE(c,m) do { _Static_assert((c),m); } while(0)
+# elif defined(__cplusplus) && (__cplusplus >= 201103L) /* C++11 */
+# define XXH_STATIC_ASSERT_WITH_MESSAGE(c,m) do { static_assert((c),m); } while(0)
+# else
+# define XXH_STATIC_ASSERT_WITH_MESSAGE(c,m) do { struct xxh_sa { char x[(c) ? 1 : -1]; }; } while(0)
+# endif
+# define XXH_STATIC_ASSERT(c) XXH_STATIC_ASSERT_WITH_MESSAGE((c),#c)
+#endif
+
+/*!
+ * @internal
+ * @def XXH_COMPILER_GUARD(var)
+ * @brief Used to prevent unwanted optimizations for @p var.
+ *
+ * It uses an empty GCC inline assembly statement with a register constraint
+ * which forces @p var into a general purpose register (eg eax, ebx, ecx
+ * on x86) and marks it as modified.
+ *
+ * This is used in a few places to avoid unwanted autovectorization (e.g.
+ * XXH32_round()). All vectorization we want is explicit via intrinsics,
+ * and _usually_ isn't wanted elsewhere.
+ *
+ * We also use it to prevent unwanted constant folding for AArch64 in
+ * XXH3_initCustomSecret_scalar().
+ */
+#if defined(__GNUC__) || defined(__clang__)
+# define XXH_COMPILER_GUARD(var) __asm__ __volatile__("" : "+r" (var))
+#else
+# define XXH_COMPILER_GUARD(var) ((void)0)
+#endif
+
+/* *************************************
+* Basic Types
+***************************************/
+#if !defined (__VMS) \
+ && (defined (__cplusplus) \
+ || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) )
+# include <stdint.h>
+ typedef uint8_t xxh_u8;
+#else
+ typedef unsigned char xxh_u8;
+#endif
+typedef XXH32_hash_t xxh_u32;
+
+#ifdef XXH_OLD_NAMES
+# define BYTE xxh_u8
+# define U8 xxh_u8
+# define U32 xxh_u32
+#endif
+
+/* *** Memory access *** */
+
+/*!
+ * @internal
+ * @fn xxh_u32 XXH_read32(const void* ptr)
+ * @brief Reads an unaligned 32-bit integer from @p ptr in native endianness.
+ *
+ * Affected by @ref XXH_FORCE_MEMORY_ACCESS.
+ *
+ * @param ptr The pointer to read from.
+ * @return The 32-bit native endian integer from the bytes at @p ptr.
+ */
+
+/*!
+ * @internal
+ * @fn xxh_u32 XXH_readLE32(const void* ptr)
+ * @brief Reads an unaligned 32-bit little endian integer from @p ptr.
+ *
+ * Affected by @ref XXH_FORCE_MEMORY_ACCESS.
+ *
+ * @param ptr The pointer to read from.
+ * @return The 32-bit little endian integer from the bytes at @p ptr.
+ */
+
+/*!
+ * @internal
+ * @fn xxh_u32 XXH_readBE32(const void* ptr)
+ * @brief Reads an unaligned 32-bit big endian integer from @p ptr.
+ *
+ * Affected by @ref XXH_FORCE_MEMORY_ACCESS.
+ *
+ * @param ptr The pointer to read from.
+ * @return The 32-bit big endian integer from the bytes at @p ptr.
+ */
+
+/*!
+ * @internal
+ * @fn xxh_u32 XXH_readLE32_align(const void* ptr, XXH_alignment align)
+ * @brief Like @ref XXH_readLE32(), but has an option for aligned reads.
+ *
+ * Affected by @ref XXH_FORCE_MEMORY_ACCESS.
+ * Note that when @ref XXH_FORCE_ALIGN_CHECK == 0, the @p align parameter is
+ * always @ref XXH_alignment::XXH_unaligned.
+ *
+ * @param ptr The pointer to read from.
+ * @param align Whether @p ptr is aligned.
+ * @pre
+ * If @p align == @ref XXH_alignment::XXH_aligned, @p ptr must be 4 byte
+ * aligned.
+ * @return The 32-bit little endian integer from the bytes at @p ptr.
+ */
+
+#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3))
+/*
+ * Manual byteshift. Best for old compilers which don't inline memcpy.
+ * We actually directly use XXH_readLE32 and XXH_readBE32.
+ */
+#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==2))
+
+/*
+ * Force direct memory access. Only works on CPU which support unaligned memory
+ * access in hardware.
+ */
+static xxh_u32 XXH_read32(const void* memPtr) { return *(const xxh_u32*) memPtr; }
+
+#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==1))
+
+/*
+ * __attribute__((aligned(1))) is supported by gcc and clang. Originally the
+ * documentation claimed that it only increased the alignment, but actually it
+ * can decrease it on gcc, clang, and icc:
+ * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69502,
+ * https://gcc.godbolt.org/z/xYez1j67Y.
+ */
+#ifdef XXH_OLD_NAMES
+typedef union { xxh_u32 u32; } __attribute__((packed)) unalign;
+#endif
+static xxh_u32 XXH_read32(const void* ptr)
+{
+ typedef __attribute__((aligned(1))) xxh_u32 xxh_unalign32;
+ return *((const xxh_unalign32*)ptr);
+}
+
+#else
+
+/*
+ * Portable and safe solution. Generally efficient.
+ * see: http://fastcompression.blogspot.com/2015/08/accessing-unaligned-memory.html
+ */
+static xxh_u32 XXH_read32(const void* memPtr)
+{
+ xxh_u32 val;
+ XXH_memcpy(&val, memPtr, sizeof(val));
+ return val;
+}
+
+#endif /* XXH_FORCE_DIRECT_MEMORY_ACCESS */
+
+
+/* *** Endianness *** */
+
+/*!
+ * @ingroup tuning
+ * @def XXH_CPU_LITTLE_ENDIAN
+ * @brief Whether the target is little endian.
+ *
+ * Defined to 1 if the target is little endian, or 0 if it is big endian.
+ * It can be defined externally, for example on the compiler command line.
+ *
+ * If it is not defined,
+ * a runtime check (which is usually constant folded) is used instead.
+ *
+ * @note
+ * This is not necessarily defined to an integer constant.
+ *
+ * @see XXH_isLittleEndian() for the runtime check.
+ */
+#ifndef XXH_CPU_LITTLE_ENDIAN
+/*
+ * Try to detect endianness automatically, to avoid the nonstandard behavior
+ * in `XXH_isLittleEndian()`
+ */
+# if defined(_WIN32) /* Windows is always little endian */ \
+ || defined(__LITTLE_ENDIAN__) \
+ || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
+# define XXH_CPU_LITTLE_ENDIAN 1
+# elif defined(__BIG_ENDIAN__) \
+ || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
+# define XXH_CPU_LITTLE_ENDIAN 0
+# else
+/*!
+ * @internal
+ * @brief Runtime check for @ref XXH_CPU_LITTLE_ENDIAN.
+ *
+ * Most compilers will constant fold this.
+ */
+static int XXH_isLittleEndian(void)
+{
+ /*
+ * Portable and well-defined behavior.
+ * Don't use static: it is detrimental to performance.
+ */
+ const union { xxh_u32 u; xxh_u8 c[4]; } one = { 1 };
+ return one.c[0];
+}
+# define XXH_CPU_LITTLE_ENDIAN XXH_isLittleEndian()
+# endif
+#endif
+
+
+
+
+/* ****************************************
+* Compiler-specific Functions and Macros
+******************************************/
+#define XXH_GCC_VERSION (__GNUC__ * 100 + __GNUC_MINOR__)
+
+#ifdef __has_builtin
+# define XXH_HAS_BUILTIN(x) __has_builtin(x)
+#else
+# define XXH_HAS_BUILTIN(x) 0
+#endif
+
+/*!
+ * @internal
+ * @def XXH_rotl32(x,r)
+ * @brief 32-bit rotate left.
+ *
+ * @param x The 32-bit integer to be rotated.
+ * @param r The number of bits to rotate.
+ * @pre
+ * @p r > 0 && @p r < 32
+ * @note
+ * @p x and @p r may be evaluated multiple times.
+ * @return The rotated result.
+ */
+#if !defined(NO_CLANG_BUILTIN) && XXH_HAS_BUILTIN(__builtin_rotateleft32) \
+ && XXH_HAS_BUILTIN(__builtin_rotateleft64)
+# define XXH_rotl32 __builtin_rotateleft32
+# define XXH_rotl64 __builtin_rotateleft64
+/* Note: although _rotl exists for minGW (GCC under windows), performance seems poor */
+#elif defined(_MSC_VER)
+# define XXH_rotl32(x,r) _rotl(x,r)
+# define XXH_rotl64(x,r) _rotl64(x,r)
+#else
+# define XXH_rotl32(x,r) (((x) << (r)) | ((x) >> (32 - (r))))
+# define XXH_rotl64(x,r) (((x) << (r)) | ((x) >> (64 - (r))))
+#endif
+
+/*!
+ * @internal
+ * @fn xxh_u32 XXH_swap32(xxh_u32 x)
+ * @brief A 32-bit byteswap.
+ *
+ * @param x The 32-bit integer to byteswap.
+ * @return @p x, byteswapped.
+ */
+#if defined(_MSC_VER) /* Visual Studio */
+# define XXH_swap32 _byteswap_ulong
+#elif XXH_GCC_VERSION >= 403
+# define XXH_swap32 __builtin_bswap32
+#else
+static xxh_u32 XXH_swap32 (xxh_u32 x)
+{
+ return ((x << 24) & 0xff000000 ) |
+ ((x << 8) & 0x00ff0000 ) |
+ ((x >> 8) & 0x0000ff00 ) |
+ ((x >> 24) & 0x000000ff );
+}
+#endif
+
+
+/* ***************************
+* Memory reads
+*****************************/
+
+/*!
+ * @internal
+ * @brief Enum to indicate whether a pointer is aligned.
+ */
+typedef enum {
+ XXH_aligned, /*!< Aligned */
+ XXH_unaligned /*!< Possibly unaligned */
+} XXH_alignment;
+
+/*
+ * XXH_FORCE_MEMORY_ACCESS==3 is an endian-independent byteshift load.
+ *
+ * This is ideal for older compilers which don't inline memcpy.
+ */
+#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3))
+
+XXH_FORCE_INLINE xxh_u32 XXH_readLE32(const void* memPtr)
+{
+ const xxh_u8* bytePtr = (const xxh_u8 *)memPtr;
+ return bytePtr[0]
+ | ((xxh_u32)bytePtr[1] << 8)
+ | ((xxh_u32)bytePtr[2] << 16)
+ | ((xxh_u32)bytePtr[3] << 24);
+}
+
+XXH_FORCE_INLINE xxh_u32 XXH_readBE32(const void* memPtr)
+{
+ const xxh_u8* bytePtr = (const xxh_u8 *)memPtr;
+ return bytePtr[3]
+ | ((xxh_u32)bytePtr[2] << 8)
+ | ((xxh_u32)bytePtr[1] << 16)
+ | ((xxh_u32)bytePtr[0] << 24);
+}
+
+#else
+XXH_FORCE_INLINE xxh_u32 XXH_readLE32(const void* ptr)
+{
+ return XXH_CPU_LITTLE_ENDIAN ? XXH_read32(ptr) : XXH_swap32(XXH_read32(ptr));
+}
+
+static xxh_u32 XXH_readBE32(const void* ptr)
+{
+ return XXH_CPU_LITTLE_ENDIAN ? XXH_swap32(XXH_read32(ptr)) : XXH_read32(ptr);
+}
+#endif
+
+XXH_FORCE_INLINE xxh_u32
+XXH_readLE32_align(const void* ptr, XXH_alignment align)
+{
+ if (align==XXH_unaligned) {
+ return XXH_readLE32(ptr);
+ } else {
+ return XXH_CPU_LITTLE_ENDIAN ? *(const xxh_u32*)ptr : XXH_swap32(*(const xxh_u32*)ptr);
+ }
+}
+
+
+/* *************************************
+* Misc
+***************************************/
+/*! @ingroup public */
+XXH_PUBLIC_API unsigned XXH_versionNumber (void) { return XXH_VERSION_NUMBER; }
+
+
+/* *******************************************************************
+* 32-bit hash functions
+*********************************************************************/
+/*!
+ * @}
+ * @defgroup XXH32_impl XXH32 implementation
+ * @ingroup impl
+ *
+ * Details on the XXH32 implementation.
+ * @{
+ */
+ /* #define instead of static const, to be used as initializers */
+#define XXH_PRIME32_1 0x9E3779B1U /*!< 0b10011110001101110111100110110001 */
+#define XXH_PRIME32_2 0x85EBCA77U /*!< 0b10000101111010111100101001110111 */
+#define XXH_PRIME32_3 0xC2B2AE3DU /*!< 0b11000010101100101010111000111101 */
+#define XXH_PRIME32_4 0x27D4EB2FU /*!< 0b00100111110101001110101100101111 */
+#define XXH_PRIME32_5 0x165667B1U /*!< 0b00010110010101100110011110110001 */
+
+#ifdef XXH_OLD_NAMES
+# define PRIME32_1 XXH_PRIME32_1
+# define PRIME32_2 XXH_PRIME32_2
+# define PRIME32_3 XXH_PRIME32_3
+# define PRIME32_4 XXH_PRIME32_4
+# define PRIME32_5 XXH_PRIME32_5
+#endif
+
+/*!
+ * @internal
+ * @brief Normal stripe processing routine.
+ *
+ * This shuffles the bits so that any bit from @p input impacts several bits in
+ * @p acc.
+ *
+ * @param acc The accumulator lane.
+ * @param input The stripe of input to mix.
+ * @return The mixed accumulator lane.
+ */
+static xxh_u32 XXH32_round(xxh_u32 acc, xxh_u32 input)
+{
+ acc += input * XXH_PRIME32_2;
+ acc = XXH_rotl32(acc, 13);
+ acc *= XXH_PRIME32_1;
+#if (defined(__SSE4_1__) || defined(__aarch64__)) && !defined(XXH_ENABLE_AUTOVECTORIZE)
+ /*
+ * UGLY HACK:
+ * A compiler fence is the only thing that prevents GCC and Clang from
+ * autovectorizing the XXH32 loop (pragmas and attributes don't work for some
+ * reason) without globally disabling SSE4.1.
+ *
+ * The reason we want to avoid vectorization is because despite working on
+ * 4 integers at a time, there are multiple factors slowing XXH32 down on
+ * SSE4:
+ * - There's a ridiculous amount of lag from pmulld (10 cycles of latency on
+ * newer chips!) making it slightly slower to multiply four integers at
+ * once compared to four integers independently. Even when pmulld was
+ * fastest, Sandy/Ivy Bridge, it is still not worth it to go into SSE
+ * just to multiply unless doing a long operation.
+ *
+ * - Four instructions are required to rotate,
+ * movqda tmp, v // not required with VEX encoding
+ * pslld tmp, 13 // tmp <<= 13
+ * psrld v, 19 // x >>= 19
+ * por v, tmp // x |= tmp
+ * compared to one for scalar:
+ * roll v, 13 // reliably fast across the board
+ * shldl v, v, 13 // Sandy Bridge and later prefer this for some reason
+ *
+ * - Instruction level parallelism is actually more beneficial here because
+ * the SIMD actually serializes this operation: While v1 is rotating, v2
+ * can load data, while v3 can multiply. SSE forces them to operate
+ * together.
+ *
+ * This is also enabled on AArch64, as Clang autovectorizes it incorrectly
+ * and it is pointless writing a NEON implementation that is basically the
+ * same speed as scalar for XXH32.
+ */
+ XXH_COMPILER_GUARD(acc);
+#endif
+ return acc;
+}
+
+/*!
+ * @internal
+ * @brief Mixes all bits to finalize the hash.
+ *
+ * The final mix ensures that all input bits have a chance to impact any bit in
+ * the output digest, resulting in an unbiased distribution.
+ *
+ * @param hash The hash to avalanche.
+ * @return The avalanched hash.
+ */
+static xxh_u32 XXH32_avalanche(xxh_u32 hash)
+{
+ hash ^= hash >> 15;
+ hash *= XXH_PRIME32_2;
+ hash ^= hash >> 13;
+ hash *= XXH_PRIME32_3;
+ hash ^= hash >> 16;
+ return hash;
+}
+
+#define XXH_get32bits(p) XXH_readLE32_align(p, align)
+
+/*!
+ * @internal
+ * @brief Processes the last 0-15 bytes of @p ptr.
+ *
+ * There may be up to 15 bytes remaining to consume from the input.
+ * This final stage will digest them to ensure that all input bytes are present
+ * in the final mix.
+ *
+ * @param hash The hash to finalize.
+ * @param ptr The pointer to the remaining input.
+ * @param len The remaining length, modulo 16.
+ * @param align Whether @p ptr is aligned.
+ * @return The finalized hash.
+ * @see XXH64_finalize().
+ */
+static XXH_PUREF xxh_u32
+XXH32_finalize(xxh_u32 hash, const xxh_u8* ptr, size_t len, XXH_alignment align)
+{
+#define XXH_PROCESS1 do { \
+ hash += (*ptr++) * XXH_PRIME32_5; \
+ hash = XXH_rotl32(hash, 11) * XXH_PRIME32_1; \
+} while (0)
+
+#define XXH_PROCESS4 do { \
+ hash += XXH_get32bits(ptr) * XXH_PRIME32_3; \
+ ptr += 4; \
+ hash = XXH_rotl32(hash, 17) * XXH_PRIME32_4; \
+} while (0)
+
+ if (ptr==NULL) XXH_ASSERT(len == 0);
+
+ /* Compact rerolled version; generally faster */
+ if (!XXH32_ENDJMP) {
+ len &= 15;
+ while (len >= 4) {
+ XXH_PROCESS4;
+ len -= 4;
+ }
+ while (len > 0) {
+ XXH_PROCESS1;
+ --len;
+ }
+ return XXH32_avalanche(hash);
+ } else {
+ switch(len&15) /* or switch(bEnd - p) */ {
+ case 12: XXH_PROCESS4;
+ XXH_FALLTHROUGH;
+ case 8: XXH_PROCESS4;
+ XXH_FALLTHROUGH;
+ case 4: XXH_PROCESS4;
+ return XXH32_avalanche(hash);
+
+ case 13: XXH_PROCESS4;
+ XXH_FALLTHROUGH;
+ case 9: XXH_PROCESS4;
+ XXH_FALLTHROUGH;
+ case 5: XXH_PROCESS4;
+ XXH_PROCESS1;
+ return XXH32_avalanche(hash);
+
+ case 14: XXH_PROCESS4;
+ XXH_FALLTHROUGH;
+ case 10: XXH_PROCESS4;
+ XXH_FALLTHROUGH;
+ case 6: XXH_PROCESS4;
+ XXH_PROCESS1;
+ XXH_PROCESS1;
+ return XXH32_avalanche(hash);
+
+ case 15: XXH_PROCESS4;
+ XXH_FALLTHROUGH;
+ case 11: XXH_PROCESS4;
+ XXH_FALLTHROUGH;
+ case 7: XXH_PROCESS4;
+ XXH_FALLTHROUGH;
+ case 3: XXH_PROCESS1;
+ XXH_FALLTHROUGH;
+ case 2: XXH_PROCESS1;
+ XXH_FALLTHROUGH;
+ case 1: XXH_PROCESS1;
+ XXH_FALLTHROUGH;
+ case 0: return XXH32_avalanche(hash);
+ }
+ XXH_ASSERT(0);
+ return hash; /* reaching this point is deemed impossible */
+ }
+}
+
+#ifdef XXH_OLD_NAMES
+# define PROCESS1 XXH_PROCESS1
+# define PROCESS4 XXH_PROCESS4
+#else
+# undef XXH_PROCESS1
+# undef XXH_PROCESS4
+#endif
+
+/*!
+ * @internal
+ * @brief The implementation for @ref XXH32().
+ *
+ * @param input , len , seed Directly passed from @ref XXH32().
+ * @param align Whether @p input is aligned.
+ * @return The calculated hash.
+ */
+XXH_FORCE_INLINE XXH_PUREF xxh_u32
+XXH32_endian_align(const xxh_u8* input, size_t len, xxh_u32 seed, XXH_alignment align)
+{
+ xxh_u32 h32;
+
+ if (input==NULL) XXH_ASSERT(len == 0);
+
+ if (len>=16) {
+ const xxh_u8* const bEnd = input + len;
+ const xxh_u8* const limit = bEnd - 15;
+ xxh_u32 v1 = seed + XXH_PRIME32_1 + XXH_PRIME32_2;
+ xxh_u32 v2 = seed + XXH_PRIME32_2;
+ xxh_u32 v3 = seed + 0;
+ xxh_u32 v4 = seed - XXH_PRIME32_1;
+
+ do {
+ v1 = XXH32_round(v1, XXH_get32bits(input)); input += 4;
+ v2 = XXH32_round(v2, XXH_get32bits(input)); input += 4;
+ v3 = XXH32_round(v3, XXH_get32bits(input)); input += 4;
+ v4 = XXH32_round(v4, XXH_get32bits(input)); input += 4;
+ } while (input < limit);
+
+ h32 = XXH_rotl32(v1, 1) + XXH_rotl32(v2, 7)
+ + XXH_rotl32(v3, 12) + XXH_rotl32(v4, 18);
+ } else {
+ h32 = seed + XXH_PRIME32_5;
+ }
+
+ h32 += (xxh_u32)len;
+
+ return XXH32_finalize(h32, input, len&15, align);
+}
+
+/*! @ingroup XXH32_family */
+XXH_PUBLIC_API XXH32_hash_t XXH32 (const void* input, size_t len, XXH32_hash_t seed)
+{
+#if !defined(XXH_NO_STREAM) && XXH_SIZE_OPT >= 2
+ /* Simple version, good for code maintenance, but unfortunately slow for small inputs */
+ XXH32_state_t state;
+ XXH32_reset(&state, seed);
+ XXH32_update(&state, (const xxh_u8*)input, len);
+ return XXH32_digest(&state);
+#else
+ if (XXH_FORCE_ALIGN_CHECK) {
+ if ((((size_t)input) & 3) == 0) { /* Input is 4-bytes aligned, leverage the speed benefit */
+ return XXH32_endian_align((const xxh_u8*)input, len, seed, XXH_aligned);
+ } }
+
+ return XXH32_endian_align((const xxh_u8*)input, len, seed, XXH_unaligned);
+#endif
+}
+
+
+
+/******* Hash streaming *******/
+#ifndef XXH_NO_STREAM
+/*! @ingroup XXH32_family */
+XXH_PUBLIC_API XXH32_state_t* XXH32_createState(void)
+{
+ return (XXH32_state_t*)XXH_malloc(sizeof(XXH32_state_t));
+}
+/*! @ingroup XXH32_family */
+XXH_PUBLIC_API XXH_errorcode XXH32_freeState(XXH32_state_t* statePtr)
+{
+ XXH_free(statePtr);
+ return XXH_OK;
+}
+
+/*! @ingroup XXH32_family */
+XXH_PUBLIC_API void XXH32_copyState(XXH32_state_t* dstState, const XXH32_state_t* srcState)
+{
+ XXH_memcpy(dstState, srcState, sizeof(*dstState));
+}
+
+/*! @ingroup XXH32_family */
+XXH_PUBLIC_API XXH_errorcode XXH32_reset(XXH32_state_t* statePtr, XXH32_hash_t seed)
+{
+ XXH_ASSERT(statePtr != NULL);
+ memset(statePtr, 0, sizeof(*statePtr));
+ statePtr->v[0] = seed + XXH_PRIME32_1 + XXH_PRIME32_2;
+ statePtr->v[1] = seed + XXH_PRIME32_2;
+ statePtr->v[2] = seed + 0;
+ statePtr->v[3] = seed - XXH_PRIME32_1;
+ return XXH_OK;
+}
+
+
+/*! @ingroup XXH32_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH32_update(XXH32_state_t* state, const void* input, size_t len)
+{
+ if (input==NULL) {
+ XXH_ASSERT(len == 0);
+ return XXH_OK;
+ }
+
+ { const xxh_u8* p = (const xxh_u8*)input;
+ const xxh_u8* const bEnd = p + len;
+
+ state->total_len_32 += (XXH32_hash_t)len;
+ state->large_len |= (XXH32_hash_t)((len>=16) | (state->total_len_32>=16));
+
+ if (state->memsize + len < 16) { /* fill in tmp buffer */
+ XXH_memcpy((xxh_u8*)(state->mem32) + state->memsize, input, len);
+ state->memsize += (XXH32_hash_t)len;
+ return XXH_OK;
+ }
+
+ if (state->memsize) { /* some data left from previous update */
+ XXH_memcpy((xxh_u8*)(state->mem32) + state->memsize, input, 16-state->memsize);
+ { const xxh_u32* p32 = state->mem32;
+ state->v[0] = XXH32_round(state->v[0], XXH_readLE32(p32)); p32++;
+ state->v[1] = XXH32_round(state->v[1], XXH_readLE32(p32)); p32++;
+ state->v[2] = XXH32_round(state->v[2], XXH_readLE32(p32)); p32++;
+ state->v[3] = XXH32_round(state->v[3], XXH_readLE32(p32));
+ }
+ p += 16-state->memsize;
+ state->memsize = 0;
+ }
+
+ if (p <= bEnd-16) {
+ const xxh_u8* const limit = bEnd - 16;
+
+ do {
+ state->v[0] = XXH32_round(state->v[0], XXH_readLE32(p)); p+=4;
+ state->v[1] = XXH32_round(state->v[1], XXH_readLE32(p)); p+=4;
+ state->v[2] = XXH32_round(state->v[2], XXH_readLE32(p)); p+=4;
+ state->v[3] = XXH32_round(state->v[3], XXH_readLE32(p)); p+=4;
+ } while (p<=limit);
+
+ }
+
+ if (p < bEnd) {
+ XXH_memcpy(state->mem32, p, (size_t)(bEnd-p));
+ state->memsize = (unsigned)(bEnd-p);
+ }
+ }
+
+ return XXH_OK;
+}
+
+
+/*! @ingroup XXH32_family */
+XXH_PUBLIC_API XXH32_hash_t XXH32_digest(const XXH32_state_t* state)
+{
+ xxh_u32 h32;
+
+ if (state->large_len) {
+ h32 = XXH_rotl32(state->v[0], 1)
+ + XXH_rotl32(state->v[1], 7)
+ + XXH_rotl32(state->v[2], 12)
+ + XXH_rotl32(state->v[3], 18);
+ } else {
+ h32 = state->v[2] /* == seed */ + XXH_PRIME32_5;
+ }
+
+ h32 += state->total_len_32;
+
+ return XXH32_finalize(h32, (const xxh_u8*)state->mem32, state->memsize, XXH_aligned);
+}
+#endif /* !XXH_NO_STREAM */
+
+/******* Canonical representation *******/
+
+/*!
+ * @ingroup XXH32_family
+ * The default return values from XXH functions are unsigned 32 and 64 bit
+ * integers.
+ *
+ * The canonical representation uses big endian convention, the same convention
+ * as human-readable numbers (large digits first).
+ *
+ * This way, hash values can be written into a file or buffer, remaining
+ * comparable across different systems.
+ *
+ * The following functions allow transformation of hash values to and from their
+ * canonical format.
+ */
+XXH_PUBLIC_API void XXH32_canonicalFromHash(XXH32_canonical_t* dst, XXH32_hash_t hash)
+{
+ XXH_STATIC_ASSERT(sizeof(XXH32_canonical_t) == sizeof(XXH32_hash_t));
+ if (XXH_CPU_LITTLE_ENDIAN) hash = XXH_swap32(hash);
+ XXH_memcpy(dst, &hash, sizeof(*dst));
+}
+/*! @ingroup XXH32_family */
+XXH_PUBLIC_API XXH32_hash_t XXH32_hashFromCanonical(const XXH32_canonical_t* src)
+{
+ return XXH_readBE32(src);
+}
+
+
+#ifndef XXH_NO_LONG_LONG
+
+/* *******************************************************************
+* 64-bit hash functions
+*********************************************************************/
+/*!
+ * @}
+ * @ingroup impl
+ * @{
+ */
+/******* Memory access *******/
+
+typedef XXH64_hash_t xxh_u64;
+
+#ifdef XXH_OLD_NAMES
+# define U64 xxh_u64
+#endif
+
+#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3))
+/*
+ * Manual byteshift. Best for old compilers which don't inline memcpy.
+ * We actually directly use XXH_readLE64 and XXH_readBE64.
+ */
+#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==2))
+
+/* Force direct memory access. Only works on CPU which support unaligned memory access in hardware */
+static xxh_u64 XXH_read64(const void* memPtr)
+{
+ return *(const xxh_u64*) memPtr;
+}
+
+#elif (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==1))
+
+/*
+ * __attribute__((aligned(1))) is supported by gcc and clang. Originally the
+ * documentation claimed that it only increased the alignment, but actually it
+ * can decrease it on gcc, clang, and icc:
+ * https://gcc.gnu.org/bugzilla/show_bug.cgi?id=69502,
+ * https://gcc.godbolt.org/z/xYez1j67Y.
+ */
+#ifdef XXH_OLD_NAMES
+typedef union { xxh_u32 u32; xxh_u64 u64; } __attribute__((packed)) unalign64;
+#endif
+static xxh_u64 XXH_read64(const void* ptr)
+{
+ typedef __attribute__((aligned(1))) xxh_u64 xxh_unalign64;
+ return *((const xxh_unalign64*)ptr);
+}
+
+#else
+
+/*
+ * Portable and safe solution. Generally efficient.
+ * see: http://fastcompression.blogspot.com/2015/08/accessing-unaligned-memory.html
+ */
+static xxh_u64 XXH_read64(const void* memPtr)
+{
+ xxh_u64 val;
+ XXH_memcpy(&val, memPtr, sizeof(val));
+ return val;
+}
+
+#endif /* XXH_FORCE_DIRECT_MEMORY_ACCESS */
+
+#if defined(_MSC_VER) /* Visual Studio */
+# define XXH_swap64 _byteswap_uint64
+#elif XXH_GCC_VERSION >= 403
+# define XXH_swap64 __builtin_bswap64
+#else
+static xxh_u64 XXH_swap64(xxh_u64 x)
+{
+ return ((x << 56) & 0xff00000000000000ULL) |
+ ((x << 40) & 0x00ff000000000000ULL) |
+ ((x << 24) & 0x0000ff0000000000ULL) |
+ ((x << 8) & 0x000000ff00000000ULL) |
+ ((x >> 8) & 0x00000000ff000000ULL) |
+ ((x >> 24) & 0x0000000000ff0000ULL) |
+ ((x >> 40) & 0x000000000000ff00ULL) |
+ ((x >> 56) & 0x00000000000000ffULL);
+}
+#endif
+
+
+/* XXH_FORCE_MEMORY_ACCESS==3 is an endian-independent byteshift load. */
+#if (defined(XXH_FORCE_MEMORY_ACCESS) && (XXH_FORCE_MEMORY_ACCESS==3))
+
+XXH_FORCE_INLINE xxh_u64 XXH_readLE64(const void* memPtr)
+{
+ const xxh_u8* bytePtr = (const xxh_u8 *)memPtr;
+ return bytePtr[0]
+ | ((xxh_u64)bytePtr[1] << 8)
+ | ((xxh_u64)bytePtr[2] << 16)
+ | ((xxh_u64)bytePtr[3] << 24)
+ | ((xxh_u64)bytePtr[4] << 32)
+ | ((xxh_u64)bytePtr[5] << 40)
+ | ((xxh_u64)bytePtr[6] << 48)
+ | ((xxh_u64)bytePtr[7] << 56);
+}
+
+XXH_FORCE_INLINE xxh_u64 XXH_readBE64(const void* memPtr)
+{
+ const xxh_u8* bytePtr = (const xxh_u8 *)memPtr;
+ return bytePtr[7]
+ | ((xxh_u64)bytePtr[6] << 8)
+ | ((xxh_u64)bytePtr[5] << 16)
+ | ((xxh_u64)bytePtr[4] << 24)
+ | ((xxh_u64)bytePtr[3] << 32)
+ | ((xxh_u64)bytePtr[2] << 40)
+ | ((xxh_u64)bytePtr[1] << 48)
+ | ((xxh_u64)bytePtr[0] << 56);
+}
+
+#else
+XXH_FORCE_INLINE xxh_u64 XXH_readLE64(const void* ptr)
+{
+ return XXH_CPU_LITTLE_ENDIAN ? XXH_read64(ptr) : XXH_swap64(XXH_read64(ptr));
+}
+
+static xxh_u64 XXH_readBE64(const void* ptr)
+{
+ return XXH_CPU_LITTLE_ENDIAN ? XXH_swap64(XXH_read64(ptr)) : XXH_read64(ptr);
+}
+#endif
+
+XXH_FORCE_INLINE xxh_u64
+XXH_readLE64_align(const void* ptr, XXH_alignment align)
+{
+ if (align==XXH_unaligned)
+ return XXH_readLE64(ptr);
+ else
+ return XXH_CPU_LITTLE_ENDIAN ? *(const xxh_u64*)ptr : XXH_swap64(*(const xxh_u64*)ptr);
+}
+
+
+/******* xxh64 *******/
+/*!
+ * @}
+ * @defgroup XXH64_impl XXH64 implementation
+ * @ingroup impl
+ *
+ * Details on the XXH64 implementation.
+ * @{
+ */
+/* #define rather that static const, to be used as initializers */
+#define XXH_PRIME64_1 0x9E3779B185EBCA87ULL /*!< 0b1001111000110111011110011011000110000101111010111100101010000111 */
+#define XXH_PRIME64_2 0xC2B2AE3D27D4EB4FULL /*!< 0b1100001010110010101011100011110100100111110101001110101101001111 */
+#define XXH_PRIME64_3 0x165667B19E3779F9ULL /*!< 0b0001011001010110011001111011000110011110001101110111100111111001 */
+#define XXH_PRIME64_4 0x85EBCA77C2B2AE63ULL /*!< 0b1000010111101011110010100111011111000010101100101010111001100011 */
+#define XXH_PRIME64_5 0x27D4EB2F165667C5ULL /*!< 0b0010011111010100111010110010111100010110010101100110011111000101 */
+
+#ifdef XXH_OLD_NAMES
+# define PRIME64_1 XXH_PRIME64_1
+# define PRIME64_2 XXH_PRIME64_2
+# define PRIME64_3 XXH_PRIME64_3
+# define PRIME64_4 XXH_PRIME64_4
+# define PRIME64_5 XXH_PRIME64_5
+#endif
+
+/*! @copydoc XXH32_round */
+static xxh_u64 XXH64_round(xxh_u64 acc, xxh_u64 input)
+{
+ acc += input * XXH_PRIME64_2;
+ acc = XXH_rotl64(acc, 31);
+ acc *= XXH_PRIME64_1;
+ return acc;
+}
+
+static xxh_u64 XXH64_mergeRound(xxh_u64 acc, xxh_u64 val)
+{
+ val = XXH64_round(0, val);
+ acc ^= val;
+ acc = acc * XXH_PRIME64_1 + XXH_PRIME64_4;
+ return acc;
+}
+
+/*! @copydoc XXH32_avalanche */
+static xxh_u64 XXH64_avalanche(xxh_u64 hash)
+{
+ hash ^= hash >> 33;
+ hash *= XXH_PRIME64_2;
+ hash ^= hash >> 29;
+ hash *= XXH_PRIME64_3;
+ hash ^= hash >> 32;
+ return hash;
+}
+
+
+#define XXH_get64bits(p) XXH_readLE64_align(p, align)
+
+/*!
+ * @internal
+ * @brief Processes the last 0-31 bytes of @p ptr.
+ *
+ * There may be up to 31 bytes remaining to consume from the input.
+ * This final stage will digest them to ensure that all input bytes are present
+ * in the final mix.
+ *
+ * @param hash The hash to finalize.
+ * @param ptr The pointer to the remaining input.
+ * @param len The remaining length, modulo 32.
+ * @param align Whether @p ptr is aligned.
+ * @return The finalized hash
+ * @see XXH32_finalize().
+ */
+static XXH_PUREF xxh_u64
+XXH64_finalize(xxh_u64 hash, const xxh_u8* ptr, size_t len, XXH_alignment align)
+{
+ if (ptr==NULL) XXH_ASSERT(len == 0);
+ len &= 31;
+ while (len >= 8) {
+ xxh_u64 const k1 = XXH64_round(0, XXH_get64bits(ptr));
+ ptr += 8;
+ hash ^= k1;
+ hash = XXH_rotl64(hash,27) * XXH_PRIME64_1 + XXH_PRIME64_4;
+ len -= 8;
+ }
+ if (len >= 4) {
+ hash ^= (xxh_u64)(XXH_get32bits(ptr)) * XXH_PRIME64_1;
+ ptr += 4;
+ hash = XXH_rotl64(hash, 23) * XXH_PRIME64_2 + XXH_PRIME64_3;
+ len -= 4;
+ }
+ while (len > 0) {
+ hash ^= (*ptr++) * XXH_PRIME64_5;
+ hash = XXH_rotl64(hash, 11) * XXH_PRIME64_1;
+ --len;
+ }
+ return XXH64_avalanche(hash);
+}
+
+#ifdef XXH_OLD_NAMES
+# define PROCESS1_64 XXH_PROCESS1_64
+# define PROCESS4_64 XXH_PROCESS4_64
+# define PROCESS8_64 XXH_PROCESS8_64
+#else
+# undef XXH_PROCESS1_64
+# undef XXH_PROCESS4_64
+# undef XXH_PROCESS8_64
+#endif
+
+/*!
+ * @internal
+ * @brief The implementation for @ref XXH64().
+ *
+ * @param input , len , seed Directly passed from @ref XXH64().
+ * @param align Whether @p input is aligned.
+ * @return The calculated hash.
+ */
+XXH_FORCE_INLINE XXH_PUREF xxh_u64
+XXH64_endian_align(const xxh_u8* input, size_t len, xxh_u64 seed, XXH_alignment align)
+{
+ xxh_u64 h64;
+ if (input==NULL) XXH_ASSERT(len == 0);
+
+ if (len>=32) {
+ const xxh_u8* const bEnd = input + len;
+ const xxh_u8* const limit = bEnd - 31;
+ xxh_u64 v1 = seed + XXH_PRIME64_1 + XXH_PRIME64_2;
+ xxh_u64 v2 = seed + XXH_PRIME64_2;
+ xxh_u64 v3 = seed + 0;
+ xxh_u64 v4 = seed - XXH_PRIME64_1;
+
+ do {
+ v1 = XXH64_round(v1, XXH_get64bits(input)); input+=8;
+ v2 = XXH64_round(v2, XXH_get64bits(input)); input+=8;
+ v3 = XXH64_round(v3, XXH_get64bits(input)); input+=8;
+ v4 = XXH64_round(v4, XXH_get64bits(input)); input+=8;
+ } while (input<limit);
+
+ h64 = XXH_rotl64(v1, 1) + XXH_rotl64(v2, 7) + XXH_rotl64(v3, 12) + XXH_rotl64(v4, 18);
+ h64 = XXH64_mergeRound(h64, v1);
+ h64 = XXH64_mergeRound(h64, v2);
+ h64 = XXH64_mergeRound(h64, v3);
+ h64 = XXH64_mergeRound(h64, v4);
+
+ } else {
+ h64 = seed + XXH_PRIME64_5;
+ }
+
+ h64 += (xxh_u64) len;
+
+ return XXH64_finalize(h64, input, len, align);
+}
+
+
+/*! @ingroup XXH64_family */
+XXH_PUBLIC_API XXH64_hash_t XXH64 (const void* input, size_t len, XXH64_hash_t seed)
+{
+#if !defined(XXH_NO_STREAM) && XXH_SIZE_OPT >= 2
+ /* Simple version, good for code maintenance, but unfortunately slow for small inputs */
+ XXH64_state_t state;
+ XXH64_reset(&state, seed);
+ XXH64_update(&state, (const xxh_u8*)input, len);
+ return XXH64_digest(&state);
+#else
+ if (XXH_FORCE_ALIGN_CHECK) {
+ if ((((size_t)input) & 7)==0) { /* Input is aligned, let's leverage the speed advantage */
+ return XXH64_endian_align((const xxh_u8*)input, len, seed, XXH_aligned);
+ } }
+
+ return XXH64_endian_align((const xxh_u8*)input, len, seed, XXH_unaligned);
+
+#endif
+}
+
+/******* Hash Streaming *******/
+#ifndef XXH_NO_STREAM
+/*! @ingroup XXH64_family*/
+XXH_PUBLIC_API XXH64_state_t* XXH64_createState(void)
+{
+ return (XXH64_state_t*)XXH_malloc(sizeof(XXH64_state_t));
+}
+/*! @ingroup XXH64_family */
+XXH_PUBLIC_API XXH_errorcode XXH64_freeState(XXH64_state_t* statePtr)
+{
+ XXH_free(statePtr);
+ return XXH_OK;
+}
+
+/*! @ingroup XXH64_family */
+XXH_PUBLIC_API void XXH64_copyState(XXH64_state_t* dstState, const XXH64_state_t* srcState)
+{
+ XXH_memcpy(dstState, srcState, sizeof(*dstState));
+}
+
+/*! @ingroup XXH64_family */
+XXH_PUBLIC_API XXH_errorcode XXH64_reset(XXH64_state_t* statePtr, XXH64_hash_t seed)
+{
+ XXH_ASSERT(statePtr != NULL);
+ memset(statePtr, 0, sizeof(*statePtr));
+ statePtr->v[0] = seed + XXH_PRIME64_1 + XXH_PRIME64_2;
+ statePtr->v[1] = seed + XXH_PRIME64_2;
+ statePtr->v[2] = seed + 0;
+ statePtr->v[3] = seed - XXH_PRIME64_1;
+ return XXH_OK;
+}
+
+/*! @ingroup XXH64_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH64_update (XXH64_state_t* state, const void* input, size_t len)
+{
+ if (input==NULL) {
+ XXH_ASSERT(len == 0);
+ return XXH_OK;
+ }
+
+ { const xxh_u8* p = (const xxh_u8*)input;
+ const xxh_u8* const bEnd = p + len;
+
+ state->total_len += len;
+
+ if (state->memsize + len < 32) { /* fill in tmp buffer */
+ XXH_memcpy(((xxh_u8*)state->mem64) + state->memsize, input, len);
+ state->memsize += (xxh_u32)len;
+ return XXH_OK;
+ }
+
+ if (state->memsize) { /* tmp buffer is full */
+ XXH_memcpy(((xxh_u8*)state->mem64) + state->memsize, input, 32-state->memsize);
+ state->v[0] = XXH64_round(state->v[0], XXH_readLE64(state->mem64+0));
+ state->v[1] = XXH64_round(state->v[1], XXH_readLE64(state->mem64+1));
+ state->v[2] = XXH64_round(state->v[2], XXH_readLE64(state->mem64+2));
+ state->v[3] = XXH64_round(state->v[3], XXH_readLE64(state->mem64+3));
+ p += 32 - state->memsize;
+ state->memsize = 0;
+ }
+
+ if (p+32 <= bEnd) {
+ const xxh_u8* const limit = bEnd - 32;
+
+ do {
+ state->v[0] = XXH64_round(state->v[0], XXH_readLE64(p)); p+=8;
+ state->v[1] = XXH64_round(state->v[1], XXH_readLE64(p)); p+=8;
+ state->v[2] = XXH64_round(state->v[2], XXH_readLE64(p)); p+=8;
+ state->v[3] = XXH64_round(state->v[3], XXH_readLE64(p)); p+=8;
+ } while (p<=limit);
+
+ }
+
+ if (p < bEnd) {
+ XXH_memcpy(state->mem64, p, (size_t)(bEnd-p));
+ state->memsize = (unsigned)(bEnd-p);
+ }
+ }
+
+ return XXH_OK;
+}
+
+
+/*! @ingroup XXH64_family */
+XXH_PUBLIC_API XXH64_hash_t XXH64_digest(const XXH64_state_t* state)
+{
+ xxh_u64 h64;
+
+ if (state->total_len >= 32) {
+ h64 = XXH_rotl64(state->v[0], 1) + XXH_rotl64(state->v[1], 7) + XXH_rotl64(state->v[2], 12) + XXH_rotl64(state->v[3], 18);
+ h64 = XXH64_mergeRound(h64, state->v[0]);
+ h64 = XXH64_mergeRound(h64, state->v[1]);
+ h64 = XXH64_mergeRound(h64, state->v[2]);
+ h64 = XXH64_mergeRound(h64, state->v[3]);
+ } else {
+ h64 = state->v[2] /*seed*/ + XXH_PRIME64_5;
+ }
+
+ h64 += (xxh_u64) state->total_len;
+
+ return XXH64_finalize(h64, (const xxh_u8*)state->mem64, (size_t)state->total_len, XXH_aligned);
+}
+#endif /* !XXH_NO_STREAM */
+
+/******* Canonical representation *******/
+
+/*! @ingroup XXH64_family */
+XXH_PUBLIC_API void XXH64_canonicalFromHash(XXH64_canonical_t* dst, XXH64_hash_t hash)
+{
+ XXH_STATIC_ASSERT(sizeof(XXH64_canonical_t) == sizeof(XXH64_hash_t));
+ if (XXH_CPU_LITTLE_ENDIAN) hash = XXH_swap64(hash);
+ XXH_memcpy(dst, &hash, sizeof(*dst));
+}
+
+/*! @ingroup XXH64_family */
+XXH_PUBLIC_API XXH64_hash_t XXH64_hashFromCanonical(const XXH64_canonical_t* src)
+{
+ return XXH_readBE64(src);
+}
+
+#ifndef XXH_NO_XXH3
+
+/* *********************************************************************
+* XXH3
+* New generation hash designed for speed on small keys and vectorization
+************************************************************************ */
+/*!
+ * @}
+ * @defgroup XXH3_impl XXH3 implementation
+ * @ingroup impl
+ * @{
+ */
+
+/* === Compiler specifics === */
+
+#if ((defined(sun) || defined(__sun)) && __cplusplus) /* Solaris includes __STDC_VERSION__ with C++. Tested with GCC 5.5 */
+# define XXH_RESTRICT /* disable */
+#elif defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L /* >= C99 */
+# define XXH_RESTRICT restrict
+#else
+/* Note: it might be useful to define __restrict or __restrict__ for some C++ compilers */
+# define XXH_RESTRICT /* disable */
+#endif
+
+#if (defined(__GNUC__) && (__GNUC__ >= 3)) \
+ || (defined(__INTEL_COMPILER) && (__INTEL_COMPILER >= 800)) \
+ || defined(__clang__)
+# define XXH_likely(x) __builtin_expect(x, 1)
+# define XXH_unlikely(x) __builtin_expect(x, 0)
+#else
+# define XXH_likely(x) (x)
+# define XXH_unlikely(x) (x)
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+# if defined(__ARM_NEON__) || defined(__ARM_NEON) \
+ || defined(__aarch64__) || defined(_M_ARM) \
+ || defined(_M_ARM64) || defined(_M_ARM64EC)
+# define inline __inline__ /* circumvent a clang bug */
+# include <arm_neon.h>
+# undef inline
+# elif defined(__AVX2__)
+# include <immintrin.h>
+# elif defined(__SSE2__)
+# include <emmintrin.h>
+# endif
+#endif
+
+#if defined(_MSC_VER)
+# include <intrin.h>
+#endif
+
+/*
+ * One goal of XXH3 is to make it fast on both 32-bit and 64-bit, while
+ * remaining a true 64-bit/128-bit hash function.
+ *
+ * This is done by prioritizing a subset of 64-bit operations that can be
+ * emulated without too many steps on the average 32-bit machine.
+ *
+ * For example, these two lines seem similar, and run equally fast on 64-bit:
+ *
+ * xxh_u64 x;
+ * x ^= (x >> 47); // good
+ * x ^= (x >> 13); // bad
+ *
+ * However, to a 32-bit machine, there is a major difference.
+ *
+ * x ^= (x >> 47) looks like this:
+ *
+ * x.lo ^= (x.hi >> (47 - 32));
+ *
+ * while x ^= (x >> 13) looks like this:
+ *
+ * // note: funnel shifts are not usually cheap.
+ * x.lo ^= (x.lo >> 13) | (x.hi << (32 - 13));
+ * x.hi ^= (x.hi >> 13);
+ *
+ * The first one is significantly faster than the second, simply because the
+ * shift is larger than 32. This means:
+ * - All the bits we need are in the upper 32 bits, so we can ignore the lower
+ * 32 bits in the shift.
+ * - The shift result will always fit in the lower 32 bits, and therefore,
+ * we can ignore the upper 32 bits in the xor.
+ *
+ * Thanks to this optimization, XXH3 only requires these features to be efficient:
+ *
+ * - Usable unaligned access
+ * - A 32-bit or 64-bit ALU
+ * - If 32-bit, a decent ADC instruction
+ * - A 32 or 64-bit multiply with a 64-bit result
+ * - For the 128-bit variant, a decent byteswap helps short inputs.
+ *
+ * The first two are already required by XXH32, and almost all 32-bit and 64-bit
+ * platforms which can run XXH32 can run XXH3 efficiently.
+ *
+ * Thumb-1, the classic 16-bit only subset of ARM's instruction set, is one
+ * notable exception.
+ *
+ * First of all, Thumb-1 lacks support for the UMULL instruction which
+ * performs the important long multiply. This means numerous __aeabi_lmul
+ * calls.
+ *
+ * Second of all, the 8 functional registers are just not enough.
+ * Setup for __aeabi_lmul, byteshift loads, pointers, and all arithmetic need
+ * Lo registers, and this shuffling results in thousands more MOVs than A32.
+ *
+ * A32 and T32 don't have this limitation. They can access all 14 registers,
+ * do a 32->64 multiply with UMULL, and the flexible operand allowing free
+ * shifts is helpful, too.
+ *
+ * Therefore, we do a quick sanity check.
+ *
+ * If compiling Thumb-1 for a target which supports ARM instructions, we will
+ * emit a warning, as it is not a "sane" platform to compile for.
+ *
+ * Usually, if this happens, it is because of an accident and you probably need
+ * to specify -march, as you likely meant to compile for a newer architecture.
+ *
+ * Credit: large sections of the vectorial and asm source code paths
+ * have been contributed by @easyaspi314
+ */
+#if defined(__thumb__) && !defined(__thumb2__) && defined(__ARM_ARCH_ISA_ARM)
+# warning "XXH3 is highly inefficient without ARM or Thumb-2."
+#endif
+
+/* ==========================================
+ * Vectorization detection
+ * ========================================== */
+
+#ifdef XXH_DOXYGEN
+/*!
+ * @ingroup tuning
+ * @brief Overrides the vectorization implementation chosen for XXH3.
+ *
+ * Can be defined to 0 to disable SIMD or any of the values mentioned in
+ * @ref XXH_VECTOR_TYPE.
+ *
+ * If this is not defined, it uses predefined macros to determine the best
+ * implementation.
+ */
+# define XXH_VECTOR XXH_SCALAR
+/*!
+ * @ingroup tuning
+ * @brief Possible values for @ref XXH_VECTOR.
+ *
+ * Note that these are actually implemented as macros.
+ *
+ * If this is not defined, it is detected automatically.
+ * @ref XXH_X86DISPATCH overrides this.
+ */
+enum XXH_VECTOR_TYPE /* fake enum */ {
+ XXH_SCALAR = 0, /*!< Portable scalar version */
+ XXH_SSE2 = 1, /*!<
+ * SSE2 for Pentium 4, Opteron, all x86_64.
+ *
+ * @note SSE2 is also guaranteed on Windows 10, macOS, and
+ * Android x86.
+ */
+ XXH_AVX2 = 2, /*!< AVX2 for Haswell and Bulldozer */
+ XXH_AVX512 = 3, /*!< AVX512 for Skylake and Icelake */
+ XXH_NEON = 4, /*!< NEON for most ARMv7-A and all AArch64 */
+ XXH_VSX = 5, /*!< VSX and ZVector for POWER8/z13 (64-bit) */
+};
+/*!
+ * @ingroup tuning
+ * @brief Selects the minimum alignment for XXH3's accumulators.
+ *
+ * When using SIMD, this should match the alignment reqired for said vector
+ * type, so, for example, 32 for AVX2.
+ *
+ * Default: Auto detected.
+ */
+# define XXH_ACC_ALIGN 8
+#endif
+
+/* Actual definition */
+#ifndef XXH_DOXYGEN
+# define XXH_SCALAR 0
+# define XXH_SSE2 1
+# define XXH_AVX2 2
+# define XXH_AVX512 3
+# define XXH_NEON 4
+# define XXH_VSX 5
+#endif
+
+#ifndef XXH_VECTOR /* can be defined on command line */
+# if ( \
+ defined(__ARM_NEON__) || defined(__ARM_NEON) /* gcc */ \
+ || defined(_M_ARM) || defined(_M_ARM64) || defined(_M_ARM64EC) /* msvc */ \
+ ) && ( \
+ defined(_WIN32) || defined(__LITTLE_ENDIAN__) /* little endian only */ \
+ || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__) \
+ )
+# define XXH_VECTOR XXH_NEON
+# elif defined(__AVX512F__)
+# define XXH_VECTOR XXH_AVX512
+# elif defined(__AVX2__)
+# define XXH_VECTOR XXH_AVX2
+# elif defined(__SSE2__) || defined(_M_AMD64) || defined(_M_X64) || (defined(_M_IX86_FP) && (_M_IX86_FP == 2))
+# define XXH_VECTOR XXH_SSE2
+# elif (defined(__PPC64__) && defined(__POWER8_VECTOR__)) \
+ || (defined(__s390x__) && defined(__VEC__)) \
+ && defined(__GNUC__) /* TODO: IBM XL */
+# define XXH_VECTOR XXH_VSX
+# else
+# define XXH_VECTOR XXH_SCALAR
+# endif
+#endif
+
+/*
+ * Controls the alignment of the accumulator,
+ * for compatibility with aligned vector loads, which are usually faster.
+ */
+#ifndef XXH_ACC_ALIGN
+# if defined(XXH_X86DISPATCH)
+# define XXH_ACC_ALIGN 64 /* for compatibility with avx512 */
+# elif XXH_VECTOR == XXH_SCALAR /* scalar */
+# define XXH_ACC_ALIGN 8
+# elif XXH_VECTOR == XXH_SSE2 /* sse2 */
+# define XXH_ACC_ALIGN 16
+# elif XXH_VECTOR == XXH_AVX2 /* avx2 */
+# define XXH_ACC_ALIGN 32
+# elif XXH_VECTOR == XXH_NEON /* neon */
+# define XXH_ACC_ALIGN 16
+# elif XXH_VECTOR == XXH_VSX /* vsx */
+# define XXH_ACC_ALIGN 16
+# elif XXH_VECTOR == XXH_AVX512 /* avx512 */
+# define XXH_ACC_ALIGN 64
+# endif
+#endif
+
+#if defined(XXH_X86DISPATCH) || XXH_VECTOR == XXH_SSE2 \
+ || XXH_VECTOR == XXH_AVX2 || XXH_VECTOR == XXH_AVX512
+# define XXH_SEC_ALIGN XXH_ACC_ALIGN
+#else
+# define XXH_SEC_ALIGN 8
+#endif
+
+/*
+ * UGLY HACK:
+ * GCC usually generates the best code with -O3 for xxHash.
+ *
+ * However, when targeting AVX2, it is overzealous in its unrolling resulting
+ * in code roughly 3/4 the speed of Clang.
+ *
+ * There are other issues, such as GCC splitting _mm256_loadu_si256 into
+ * _mm_loadu_si128 + _mm256_inserti128_si256. This is an optimization which
+ * only applies to Sandy and Ivy Bridge... which don't even support AVX2.
+ *
+ * That is why when compiling the AVX2 version, it is recommended to use either
+ * -O2 -mavx2 -march=haswell
+ * or
+ * -O2 -mavx2 -mno-avx256-split-unaligned-load
+ * for decent performance, or to use Clang instead.
+ *
+ * Fortunately, we can control the first one with a pragma that forces GCC into
+ * -O2, but the other one we can't control without "failed to inline always
+ * inline function due to target mismatch" warnings.
+ */
+#if XXH_VECTOR == XXH_AVX2 /* AVX2 */ \
+ && defined(__GNUC__) && !defined(__clang__) /* GCC, not Clang */ \
+ && defined(__OPTIMIZE__) && XXH_SIZE_OPT <= 0 /* respect -O0 and -Os */
+# pragma GCC push_options
+# pragma GCC optimize("-O2")
+#endif
+
+
+#if XXH_VECTOR == XXH_NEON
+/*
+ * NEON's setup for vmlal_u32 is a little more complicated than it is on
+ * SSE2, AVX2, and VSX.
+ *
+ * While PMULUDQ and VMULEUW both perform a mask, VMLAL.U32 performs an upcast.
+ *
+ * To do the same operation, the 128-bit 'Q' register needs to be split into
+ * two 64-bit 'D' registers, performing this operation::
+ *
+ * [ a | b ]
+ * | '---------. .--------' |
+ * | x |
+ * | .---------' '--------. |
+ * [ a & 0xFFFFFFFF | b & 0xFFFFFFFF ],[ a >> 32 | b >> 32 ]
+ *
+ * Due to significant changes in aarch64, the fastest method for aarch64 is
+ * completely different than the fastest method for ARMv7-A.
+ *
+ * ARMv7-A treats D registers as unions overlaying Q registers, so modifying
+ * D11 will modify the high half of Q5. This is similar to how modifying AH
+ * will only affect bits 8-15 of AX on x86.
+ *
+ * VZIP takes two registers, and puts even lanes in one register and odd lanes
+ * in the other.
+ *
+ * On ARMv7-A, this strangely modifies both parameters in place instead of
+ * taking the usual 3-operand form.
+ *
+ * Therefore, if we want to do this, we can simply use a D-form VZIP.32 on the
+ * lower and upper halves of the Q register to end up with the high and low
+ * halves where we want - all in one instruction.
+ *
+ * vzip.32 d10, d11 @ d10 = { d10[0], d11[0] }; d11 = { d10[1], d11[1] }
+ *
+ * Unfortunately we need inline assembly for this: Instructions modifying two
+ * registers at once is not possible in GCC or Clang's IR, and they have to
+ * create a copy.
+ *
+ * aarch64 requires a different approach.
+ *
+ * In order to make it easier to write a decent compiler for aarch64, many
+ * quirks were removed, such as conditional execution.
+ *
+ * NEON was also affected by this.
+ *
+ * aarch64 cannot access the high bits of a Q-form register, and writes to a
+ * D-form register zero the high bits, similar to how writes to W-form scalar
+ * registers (or DWORD registers on x86_64) work.
+ *
+ * The formerly free vget_high intrinsics now require a vext (with a few
+ * exceptions)
+ *
+ * Additionally, VZIP was replaced by ZIP1 and ZIP2, which are the equivalent
+ * of PUNPCKL* and PUNPCKH* in SSE, respectively, in order to only modify one
+ * operand.
+ *
+ * The equivalent of the VZIP.32 on the lower and upper halves would be this
+ * mess:
+ *
+ * ext v2.4s, v0.4s, v0.4s, #2 // v2 = { v0[2], v0[3], v0[0], v0[1] }
+ * zip1 v1.2s, v0.2s, v2.2s // v1 = { v0[0], v2[0] }
+ * zip2 v0.2s, v0.2s, v1.2s // v0 = { v0[1], v2[1] }
+ *
+ * Instead, we use a literal downcast, vmovn_u64 (XTN), and vshrn_n_u64 (SHRN):
+ *
+ * shrn v1.2s, v0.2d, #32 // v1 = (uint32x2_t)(v0 >> 32);
+ * xtn v0.2s, v0.2d // v0 = (uint32x2_t)(v0 & 0xFFFFFFFF);
+ *
+ * This is available on ARMv7-A, but is less efficient than a single VZIP.32.
+ */
+
+/*!
+ * Function-like macro:
+ * void XXH_SPLIT_IN_PLACE(uint64x2_t &in, uint32x2_t &outLo, uint32x2_t &outHi)
+ * {
+ * outLo = (uint32x2_t)(in & 0xFFFFFFFF);
+ * outHi = (uint32x2_t)(in >> 32);
+ * in = UNDEFINED;
+ * }
+ */
+# if !defined(XXH_NO_VZIP_HACK) /* define to disable */ \
+ && (defined(__GNUC__) || defined(__clang__)) \
+ && (defined(__arm__) || defined(__thumb__) || defined(_M_ARM))
+# define XXH_SPLIT_IN_PLACE(in, outLo, outHi) \
+ do { \
+ /* Undocumented GCC/Clang operand modifier: %e0 = lower D half, %f0 = upper D half */ \
+ /* https://github.com/gcc-mirror/gcc/blob/38cf91e5/gcc/config/arm/arm.c#L22486 */ \
+ /* https://github.com/llvm-mirror/llvm/blob/2c4ca683/lib/Target/ARM/ARMAsmPrinter.cpp#L399 */ \
+ __asm__("vzip.32 %e0, %f0" : "+w" (in)); \
+ (outLo) = vget_low_u32 (vreinterpretq_u32_u64(in)); \
+ (outHi) = vget_high_u32(vreinterpretq_u32_u64(in)); \
+ } while (0)
+# else
+# define XXH_SPLIT_IN_PLACE(in, outLo, outHi) \
+ do { \
+ (outLo) = vmovn_u64 (in); \
+ (outHi) = vshrn_n_u64 ((in), 32); \
+ } while (0)
+# endif
+
+/*!
+ * @internal
+ * @brief `vld1q_u64` but faster and alignment-safe.
+ *
+ * On AArch64, unaligned access is always safe, but on ARMv7-a, it is only
+ * *conditionally* safe (`vld1` has an alignment bit like `movdq[ua]` in x86).
+ *
+ * GCC for AArch64 sees `vld1q_u8` as an intrinsic instead of a load, so it
+ * prohibits load-store optimizations. Therefore, a direct dereference is used.
+ *
+ * Otherwise, `vld1q_u8` is used with `vreinterpretq_u8_u64` to do a safe
+ * unaligned load.
+ */
+#if defined(__aarch64__) && defined(__GNUC__) && !defined(__clang__)
+XXH_FORCE_INLINE uint64x2_t XXH_vld1q_u64(void const* ptr) /* silence -Wcast-align */
+{
+ return *(uint64x2_t const*)ptr;
+}
+#else
+XXH_FORCE_INLINE uint64x2_t XXH_vld1q_u64(void const* ptr)
+{
+ return vreinterpretq_u64_u8(vld1q_u8((uint8_t const*)ptr));
+}
+#endif
+/*!
+ * @ingroup tuning
+ * @brief Controls the NEON to scalar ratio for XXH3
+ *
+ * On AArch64 when not optimizing for size, XXH3 will run 6 lanes using NEON and
+ * 2 lanes on scalar by default.
+ *
+ * This can be set to 2, 4, 6, or 8. ARMv7 will default to all 8 NEON lanes, as the
+ * emulated 64-bit arithmetic is too slow.
+ *
+ * Modern ARM CPUs are _very_ sensitive to how their pipelines are used.
+ *
+ * For example, the Cortex-A73 can dispatch 3 micro-ops per cycle, but it can't
+ * have more than 2 NEON (F0/F1) micro-ops. If you are only using NEON instructions,
+ * you are only using 2/3 of the CPU bandwidth.
+ *
+ * This is even more noticable on the more advanced cores like the A76 which
+ * can dispatch 8 micro-ops per cycle, but still only 2 NEON micro-ops at once.
+ *
+ * Therefore, @ref XXH3_NEON_LANES lanes will be processed using NEON, and the
+ * remaining lanes will use scalar instructions. This improves the bandwidth
+ * and also gives the integer pipelines something to do besides twiddling loop
+ * counters and pointers.
+ *
+ * This change benefits CPUs with large micro-op buffers without negatively affecting
+ * other CPUs:
+ *
+ * | Chipset | Dispatch type | NEON only | 6:2 hybrid | Diff. |
+ * |:----------------------|:--------------------|----------:|-----------:|------:|
+ * | Snapdragon 730 (A76) | 2 NEON/8 micro-ops | 8.8 GB/s | 10.1 GB/s | ~16% |
+ * | Snapdragon 835 (A73) | 2 NEON/3 micro-ops | 5.1 GB/s | 5.3 GB/s | ~5% |
+ * | Marvell PXA1928 (A53) | In-order dual-issue | 1.9 GB/s | 1.9 GB/s | 0% |
+ *
+ * It also seems to fix some bad codegen on GCC, making it almost as fast as clang.
+ *
+ * @see XXH3_accumulate_512_neon()
+ */
+# ifndef XXH3_NEON_LANES
+# if (defined(__aarch64__) || defined(__arm64__) || defined(_M_ARM64) || defined(_M_ARM64EC)) \
+ && XXH_SIZE_OPT <= 0
+# define XXH3_NEON_LANES 6
+# else
+# define XXH3_NEON_LANES XXH_ACC_NB
+# endif
+# endif
+#endif /* XXH_VECTOR == XXH_NEON */
+
+/*
+ * VSX and Z Vector helpers.
+ *
+ * This is very messy, and any pull requests to clean this up are welcome.
+ *
+ * There are a lot of problems with supporting VSX and s390x, due to
+ * inconsistent intrinsics, spotty coverage, and multiple endiannesses.
+ */
+#if XXH_VECTOR == XXH_VSX
+/* Annoyingly, these headers _may_ define three macros: `bool`, `vector`,
+ * and `pixel`. This is a problem for obvious reasons.
+ *
+ * These keywords are unnecessary; the spec literally says they are
+ * equivalent to `__bool`, `__vector`, and `__pixel` and may be undef'd
+ * after including the header.
+ *
+ * We use pragma push_macro/pop_macro to keep the namespace clean. */
+# pragma push_macro("bool")
+# pragma push_macro("vector")
+# pragma push_macro("pixel")
+/* silence potential macro redefined warnings */
+# undef bool
+# undef vector
+# undef pixel
+
+# if defined(__s390x__)
+# include <s390intrin.h>
+# else
+# include <altivec.h>
+# endif
+
+/* Restore the original macro values, if applicable. */
+# pragma pop_macro("pixel")
+# pragma pop_macro("vector")
+# pragma pop_macro("bool")
+
+typedef __vector unsigned long long xxh_u64x2;
+typedef __vector unsigned char xxh_u8x16;
+typedef __vector unsigned xxh_u32x4;
+
+# ifndef XXH_VSX_BE
+# if defined(__BIG_ENDIAN__) \
+ || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
+# define XXH_VSX_BE 1
+# elif defined(__VEC_ELEMENT_REG_ORDER__) && __VEC_ELEMENT_REG_ORDER__ == __ORDER_BIG_ENDIAN__
+# warning "-maltivec=be is not recommended. Please use native endianness."
+# define XXH_VSX_BE 1
+# else
+# define XXH_VSX_BE 0
+# endif
+# endif /* !defined(XXH_VSX_BE) */
+
+# if XXH_VSX_BE
+# if defined(__POWER9_VECTOR__) || (defined(__clang__) && defined(__s390x__))
+# define XXH_vec_revb vec_revb
+# else
+/*!
+ * A polyfill for POWER9's vec_revb().
+ */
+XXH_FORCE_INLINE xxh_u64x2 XXH_vec_revb(xxh_u64x2 val)
+{
+ xxh_u8x16 const vByteSwap = { 0x07, 0x06, 0x05, 0x04, 0x03, 0x02, 0x01, 0x00,
+ 0x0F, 0x0E, 0x0D, 0x0C, 0x0B, 0x0A, 0x09, 0x08 };
+ return vec_perm(val, val, vByteSwap);
+}
+# endif
+# endif /* XXH_VSX_BE */
+
+/*!
+ * Performs an unaligned vector load and byte swaps it on big endian.
+ */
+XXH_FORCE_INLINE xxh_u64x2 XXH_vec_loadu(const void *ptr)
+{
+ xxh_u64x2 ret;
+ XXH_memcpy(&ret, ptr, sizeof(xxh_u64x2));
+# if XXH_VSX_BE
+ ret = XXH_vec_revb(ret);
+# endif
+ return ret;
+}
+
+/*
+ * vec_mulo and vec_mule are very problematic intrinsics on PowerPC
+ *
+ * These intrinsics weren't added until GCC 8, despite existing for a while,
+ * and they are endian dependent. Also, their meaning swap depending on version.
+ * */
+# if defined(__s390x__)
+ /* s390x is always big endian, no issue on this platform */
+# define XXH_vec_mulo vec_mulo
+# define XXH_vec_mule vec_mule
+# elif defined(__clang__) && XXH_HAS_BUILTIN(__builtin_altivec_vmuleuw) && !defined(__ibmxl__)
+/* Clang has a better way to control this, we can just use the builtin which doesn't swap. */
+ /* The IBM XL Compiler (which defined __clang__) only implements the vec_* operations */
+# define XXH_vec_mulo __builtin_altivec_vmulouw
+# define XXH_vec_mule __builtin_altivec_vmuleuw
+# else
+/* gcc needs inline assembly */
+/* Adapted from https://github.com/google/highwayhash/blob/master/highwayhash/hh_vsx.h. */
+XXH_FORCE_INLINE xxh_u64x2 XXH_vec_mulo(xxh_u32x4 a, xxh_u32x4 b)
+{
+ xxh_u64x2 result;
+ __asm__("vmulouw %0, %1, %2" : "=v" (result) : "v" (a), "v" (b));
+ return result;
+}
+XXH_FORCE_INLINE xxh_u64x2 XXH_vec_mule(xxh_u32x4 a, xxh_u32x4 b)
+{
+ xxh_u64x2 result;
+ __asm__("vmuleuw %0, %1, %2" : "=v" (result) : "v" (a), "v" (b));
+ return result;
+}
+# endif /* XXH_vec_mulo, XXH_vec_mule */
+#endif /* XXH_VECTOR == XXH_VSX */
+
+
+/* prefetch
+ * can be disabled, by declaring XXH_NO_PREFETCH build macro */
+#if defined(XXH_NO_PREFETCH)
+# define XXH_PREFETCH(ptr) (void)(ptr) /* disabled */
+#else
+# if XXH_SIZE_OPT >= 1
+# define XXH_PREFETCH(ptr) (void)(ptr)
+# elif defined(_MSC_VER) && (defined(_M_X64) || defined(_M_IX86)) /* _mm_prefetch() not defined outside of x86/x64 */
+# include <mmintrin.h> /* https://msdn.microsoft.com/fr-fr/library/84szxsww(v=vs.90).aspx */
+# define XXH_PREFETCH(ptr) _mm_prefetch((const char*)(ptr), _MM_HINT_T0)
+# elif defined(__GNUC__) && ( (__GNUC__ >= 4) || ( (__GNUC__ == 3) && (__GNUC_MINOR__ >= 1) ) )
+# define XXH_PREFETCH(ptr) __builtin_prefetch((ptr), 0 /* rw==read */, 3 /* locality */)
+# else
+# define XXH_PREFETCH(ptr) (void)(ptr) /* disabled */
+# endif
+#endif /* XXH_NO_PREFETCH */
+
+
+/* ==========================================
+ * XXH3 default settings
+ * ========================================== */
+
+#define XXH_SECRET_DEFAULT_SIZE 192 /* minimum XXH3_SECRET_SIZE_MIN */
+
+#if (XXH_SECRET_DEFAULT_SIZE < XXH3_SECRET_SIZE_MIN)
+# error "default keyset is not large enough"
+#endif
+
+/*! Pseudorandom secret taken directly from FARSH. */
+XXH_ALIGN(64) static const xxh_u8 XXH3_kSecret[XXH_SECRET_DEFAULT_SIZE] = {
+ 0xb8, 0xfe, 0x6c, 0x39, 0x23, 0xa4, 0x4b, 0xbe, 0x7c, 0x01, 0x81, 0x2c, 0xf7, 0x21, 0xad, 0x1c,
+ 0xde, 0xd4, 0x6d, 0xe9, 0x83, 0x90, 0x97, 0xdb, 0x72, 0x40, 0xa4, 0xa4, 0xb7, 0xb3, 0x67, 0x1f,
+ 0xcb, 0x79, 0xe6, 0x4e, 0xcc, 0xc0, 0xe5, 0x78, 0x82, 0x5a, 0xd0, 0x7d, 0xcc, 0xff, 0x72, 0x21,
+ 0xb8, 0x08, 0x46, 0x74, 0xf7, 0x43, 0x24, 0x8e, 0xe0, 0x35, 0x90, 0xe6, 0x81, 0x3a, 0x26, 0x4c,
+ 0x3c, 0x28, 0x52, 0xbb, 0x91, 0xc3, 0x00, 0xcb, 0x88, 0xd0, 0x65, 0x8b, 0x1b, 0x53, 0x2e, 0xa3,
+ 0x71, 0x64, 0x48, 0x97, 0xa2, 0x0d, 0xf9, 0x4e, 0x38, 0x19, 0xef, 0x46, 0xa9, 0xde, 0xac, 0xd8,
+ 0xa8, 0xfa, 0x76, 0x3f, 0xe3, 0x9c, 0x34, 0x3f, 0xf9, 0xdc, 0xbb, 0xc7, 0xc7, 0x0b, 0x4f, 0x1d,
+ 0x8a, 0x51, 0xe0, 0x4b, 0xcd, 0xb4, 0x59, 0x31, 0xc8, 0x9f, 0x7e, 0xc9, 0xd9, 0x78, 0x73, 0x64,
+ 0xea, 0xc5, 0xac, 0x83, 0x34, 0xd3, 0xeb, 0xc3, 0xc5, 0x81, 0xa0, 0xff, 0xfa, 0x13, 0x63, 0xeb,
+ 0x17, 0x0d, 0xdd, 0x51, 0xb7, 0xf0, 0xda, 0x49, 0xd3, 0x16, 0x55, 0x26, 0x29, 0xd4, 0x68, 0x9e,
+ 0x2b, 0x16, 0xbe, 0x58, 0x7d, 0x47, 0xa1, 0xfc, 0x8f, 0xf8, 0xb8, 0xd1, 0x7a, 0xd0, 0x31, 0xce,
+ 0x45, 0xcb, 0x3a, 0x8f, 0x95, 0x16, 0x04, 0x28, 0xaf, 0xd7, 0xfb, 0xca, 0xbb, 0x4b, 0x40, 0x7e,
+};
+
+
+#ifdef XXH_OLD_NAMES
+# define kSecret XXH3_kSecret
+#endif
+
+#ifdef XXH_DOXYGEN
+/*!
+ * @brief Calculates a 32-bit to 64-bit long multiply.
+ *
+ * Implemented as a macro.
+ *
+ * Wraps `__emulu` on MSVC x86 because it tends to call `__allmul` when it doesn't
+ * need to (but it shouldn't need to anyways, it is about 7 instructions to do
+ * a 64x64 multiply...). Since we know that this will _always_ emit `MULL`, we
+ * use that instead of the normal method.
+ *
+ * If you are compiling for platforms like Thumb-1 and don't have a better option,
+ * you may also want to write your own long multiply routine here.
+ *
+ * @param x, y Numbers to be multiplied
+ * @return 64-bit product of the low 32 bits of @p x and @p y.
+ */
+XXH_FORCE_INLINE xxh_u64
+XXH_mult32to64(xxh_u64 x, xxh_u64 y)
+{
+ return (x & 0xFFFFFFFF) * (y & 0xFFFFFFFF);
+}
+#elif defined(_MSC_VER) && defined(_M_IX86)
+# define XXH_mult32to64(x, y) __emulu((unsigned)(x), (unsigned)(y))
+#else
+/*
+ * Downcast + upcast is usually better than masking on older compilers like
+ * GCC 4.2 (especially 32-bit ones), all without affecting newer compilers.
+ *
+ * The other method, (x & 0xFFFFFFFF) * (y & 0xFFFFFFFF), will AND both operands
+ * and perform a full 64x64 multiply -- entirely redundant on 32-bit.
+ */
+# define XXH_mult32to64(x, y) ((xxh_u64)(xxh_u32)(x) * (xxh_u64)(xxh_u32)(y))
+#endif
+
+/*!
+ * @brief Calculates a 64->128-bit long multiply.
+ *
+ * Uses `__uint128_t` and `_umul128` if available, otherwise uses a scalar
+ * version.
+ *
+ * @param lhs , rhs The 64-bit integers to be multiplied
+ * @return The 128-bit result represented in an @ref XXH128_hash_t.
+ */
+static XXH128_hash_t
+XXH_mult64to128(xxh_u64 lhs, xxh_u64 rhs)
+{
+ /*
+ * GCC/Clang __uint128_t method.
+ *
+ * On most 64-bit targets, GCC and Clang define a __uint128_t type.
+ * This is usually the best way as it usually uses a native long 64-bit
+ * multiply, such as MULQ on x86_64 or MUL + UMULH on aarch64.
+ *
+ * Usually.
+ *
+ * Despite being a 32-bit platform, Clang (and emscripten) define this type
+ * despite not having the arithmetic for it. This results in a laggy
+ * compiler builtin call which calculates a full 128-bit multiply.
+ * In that case it is best to use the portable one.
+ * https://github.com/Cyan4973/xxHash/issues/211#issuecomment-515575677
+ */
+#if (defined(__GNUC__) || defined(__clang__)) && !defined(__wasm__) \
+ && defined(__SIZEOF_INT128__) \
+ || (defined(_INTEGRAL_MAX_BITS) && _INTEGRAL_MAX_BITS >= 128)
+
+ __uint128_t const product = (__uint128_t)lhs * (__uint128_t)rhs;
+ XXH128_hash_t r128;
+ r128.low64 = (xxh_u64)(product);
+ r128.high64 = (xxh_u64)(product >> 64);
+ return r128;
+
+ /*
+ * MSVC for x64's _umul128 method.
+ *
+ * xxh_u64 _umul128(xxh_u64 Multiplier, xxh_u64 Multiplicand, xxh_u64 *HighProduct);
+ *
+ * This compiles to single operand MUL on x64.
+ */
+#elif (defined(_M_X64) || defined(_M_IA64)) && !defined(_M_ARM64EC)
+
+#ifndef _MSC_VER
+# pragma intrinsic(_umul128)
+#endif
+ xxh_u64 product_high;
+ xxh_u64 const product_low = _umul128(lhs, rhs, &product_high);
+ XXH128_hash_t r128;
+ r128.low64 = product_low;
+ r128.high64 = product_high;
+ return r128;
+
+ /*
+ * MSVC for ARM64's __umulh method.
+ *
+ * This compiles to the same MUL + UMULH as GCC/Clang's __uint128_t method.
+ */
+#elif defined(_M_ARM64) || defined(_M_ARM64EC)
+
+#ifndef _MSC_VER
+# pragma intrinsic(__umulh)
+#endif
+ XXH128_hash_t r128;
+ r128.low64 = lhs * rhs;
+ r128.high64 = __umulh(lhs, rhs);
+ return r128;
+
+#else
+ /*
+ * Portable scalar method. Optimized for 32-bit and 64-bit ALUs.
+ *
+ * This is a fast and simple grade school multiply, which is shown below
+ * with base 10 arithmetic instead of base 0x100000000.
+ *
+ * 9 3 // D2 lhs = 93
+ * x 7 5 // D2 rhs = 75
+ * ----------
+ * 1 5 // D2 lo_lo = (93 % 10) * (75 % 10) = 15
+ * 4 5 | // D2 hi_lo = (93 / 10) * (75 % 10) = 45
+ * 2 1 | // D2 lo_hi = (93 % 10) * (75 / 10) = 21
+ * + 6 3 | | // D2 hi_hi = (93 / 10) * (75 / 10) = 63
+ * ---------
+ * 2 7 | // D2 cross = (15 / 10) + (45 % 10) + 21 = 27
+ * + 6 7 | | // D2 upper = (27 / 10) + (45 / 10) + 63 = 67
+ * ---------
+ * 6 9 7 5 // D4 res = (27 * 10) + (15 % 10) + (67 * 100) = 6975
+ *
+ * The reasons for adding the products like this are:
+ * 1. It avoids manual carry tracking. Just like how
+ * (9 * 9) + 9 + 9 = 99, the same applies with this for UINT64_MAX.
+ * This avoids a lot of complexity.
+ *
+ * 2. It hints for, and on Clang, compiles to, the powerful UMAAL
+ * instruction available in ARM's Digital Signal Processing extension
+ * in 32-bit ARMv6 and later, which is shown below:
+ *
+ * void UMAAL(xxh_u32 *RdLo, xxh_u32 *RdHi, xxh_u32 Rn, xxh_u32 Rm)
+ * {
+ * xxh_u64 product = (xxh_u64)*RdLo * (xxh_u64)*RdHi + Rn + Rm;
+ * *RdLo = (xxh_u32)(product & 0xFFFFFFFF);
+ * *RdHi = (xxh_u32)(product >> 32);
+ * }
+ *
+ * This instruction was designed for efficient long multiplication, and
+ * allows this to be calculated in only 4 instructions at speeds
+ * comparable to some 64-bit ALUs.
+ *
+ * 3. It isn't terrible on other platforms. Usually this will be a couple
+ * of 32-bit ADD/ADCs.
+ */
+
+ /* First calculate all of the cross products. */
+ xxh_u64 const lo_lo = XXH_mult32to64(lhs & 0xFFFFFFFF, rhs & 0xFFFFFFFF);
+ xxh_u64 const hi_lo = XXH_mult32to64(lhs >> 32, rhs & 0xFFFFFFFF);
+ xxh_u64 const lo_hi = XXH_mult32to64(lhs & 0xFFFFFFFF, rhs >> 32);
+ xxh_u64 const hi_hi = XXH_mult32to64(lhs >> 32, rhs >> 32);
+
+ /* Now add the products together. These will never overflow. */
+ xxh_u64 const cross = (lo_lo >> 32) + (hi_lo & 0xFFFFFFFF) + lo_hi;
+ xxh_u64 const upper = (hi_lo >> 32) + (cross >> 32) + hi_hi;
+ xxh_u64 const lower = (cross << 32) | (lo_lo & 0xFFFFFFFF);
+
+ XXH128_hash_t r128;
+ r128.low64 = lower;
+ r128.high64 = upper;
+ return r128;
+#endif
+}
+
+/*!
+ * @brief Calculates a 64-bit to 128-bit multiply, then XOR folds it.
+ *
+ * The reason for the separate function is to prevent passing too many structs
+ * around by value. This will hopefully inline the multiply, but we don't force it.
+ *
+ * @param lhs , rhs The 64-bit integers to multiply
+ * @return The low 64 bits of the product XOR'd by the high 64 bits.
+ * @see XXH_mult64to128()
+ */
+static xxh_u64
+XXH3_mul128_fold64(xxh_u64 lhs, xxh_u64 rhs)
+{
+ XXH128_hash_t product = XXH_mult64to128(lhs, rhs);
+ return product.low64 ^ product.high64;
+}
+
+/*! Seems to produce slightly better code on GCC for some reason. */
+XXH_FORCE_INLINE XXH_CONSTF xxh_u64 XXH_xorshift64(xxh_u64 v64, int shift)
+{
+ XXH_ASSERT(0 <= shift && shift < 64);
+ return v64 ^ (v64 >> shift);
+}
+
+/*
+ * This is a fast avalanche stage,
+ * suitable when input bits are already partially mixed
+ */
+static XXH64_hash_t XXH3_avalanche(xxh_u64 h64)
+{
+ h64 = XXH_xorshift64(h64, 37);
+ h64 *= 0x165667919E3779F9ULL;
+ h64 = XXH_xorshift64(h64, 32);
+ return h64;
+}
+
+/*
+ * This is a stronger avalanche,
+ * inspired by Pelle Evensen's rrmxmx
+ * preferable when input has not been previously mixed
+ */
+static XXH64_hash_t XXH3_rrmxmx(xxh_u64 h64, xxh_u64 len)
+{
+ /* this mix is inspired by Pelle Evensen's rrmxmx */
+ h64 ^= XXH_rotl64(h64, 49) ^ XXH_rotl64(h64, 24);
+ h64 *= 0x9FB21C651E98DF25ULL;
+ h64 ^= (h64 >> 35) + len ;
+ h64 *= 0x9FB21C651E98DF25ULL;
+ return XXH_xorshift64(h64, 28);
+}
+
+
+/* ==========================================
+ * Short keys
+ * ==========================================
+ * One of the shortcomings of XXH32 and XXH64 was that their performance was
+ * sub-optimal on short lengths. It used an iterative algorithm which strongly
+ * favored lengths that were a multiple of 4 or 8.
+ *
+ * Instead of iterating over individual inputs, we use a set of single shot
+ * functions which piece together a range of lengths and operate in constant time.
+ *
+ * Additionally, the number of multiplies has been significantly reduced. This
+ * reduces latency, especially when emulating 64-bit multiplies on 32-bit.
+ *
+ * Depending on the platform, this may or may not be faster than XXH32, but it
+ * is almost guaranteed to be faster than XXH64.
+ */
+
+/*
+ * At very short lengths, there isn't enough input to fully hide secrets, or use
+ * the entire secret.
+ *
+ * There is also only a limited amount of mixing we can do before significantly
+ * impacting performance.
+ *
+ * Therefore, we use different sections of the secret and always mix two secret
+ * samples with an XOR. This should have no effect on performance on the
+ * seedless or withSeed variants because everything _should_ be constant folded
+ * by modern compilers.
+ *
+ * The XOR mixing hides individual parts of the secret and increases entropy.
+ *
+ * This adds an extra layer of strength for custom secrets.
+ */
+XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t
+XXH3_len_1to3_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed)
+{
+ XXH_ASSERT(input != NULL);
+ XXH_ASSERT(1 <= len && len <= 3);
+ XXH_ASSERT(secret != NULL);
+ /*
+ * len = 1: combined = { input[0], 0x01, input[0], input[0] }
+ * len = 2: combined = { input[1], 0x02, input[0], input[1] }
+ * len = 3: combined = { input[2], 0x03, input[0], input[1] }
+ */
+ { xxh_u8 const c1 = input[0];
+ xxh_u8 const c2 = input[len >> 1];
+ xxh_u8 const c3 = input[len - 1];
+ xxh_u32 const combined = ((xxh_u32)c1 << 16) | ((xxh_u32)c2 << 24)
+ | ((xxh_u32)c3 << 0) | ((xxh_u32)len << 8);
+ xxh_u64 const bitflip = (XXH_readLE32(secret) ^ XXH_readLE32(secret+4)) + seed;
+ xxh_u64 const keyed = (xxh_u64)combined ^ bitflip;
+ return XXH64_avalanche(keyed);
+ }
+}
+
+XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t
+XXH3_len_4to8_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed)
+{
+ XXH_ASSERT(input != NULL);
+ XXH_ASSERT(secret != NULL);
+ XXH_ASSERT(4 <= len && len <= 8);
+ seed ^= (xxh_u64)XXH_swap32((xxh_u32)seed) << 32;
+ { xxh_u32 const input1 = XXH_readLE32(input);
+ xxh_u32 const input2 = XXH_readLE32(input + len - 4);
+ xxh_u64 const bitflip = (XXH_readLE64(secret+8) ^ XXH_readLE64(secret+16)) - seed;
+ xxh_u64 const input64 = input2 + (((xxh_u64)input1) << 32);
+ xxh_u64 const keyed = input64 ^ bitflip;
+ return XXH3_rrmxmx(keyed, len);
+ }
+}
+
+XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t
+XXH3_len_9to16_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed)
+{
+ XXH_ASSERT(input != NULL);
+ XXH_ASSERT(secret != NULL);
+ XXH_ASSERT(9 <= len && len <= 16);
+ { xxh_u64 const bitflip1 = (XXH_readLE64(secret+24) ^ XXH_readLE64(secret+32)) + seed;
+ xxh_u64 const bitflip2 = (XXH_readLE64(secret+40) ^ XXH_readLE64(secret+48)) - seed;
+ xxh_u64 const input_lo = XXH_readLE64(input) ^ bitflip1;
+ xxh_u64 const input_hi = XXH_readLE64(input + len - 8) ^ bitflip2;
+ xxh_u64 const acc = len
+ + XXH_swap64(input_lo) + input_hi
+ + XXH3_mul128_fold64(input_lo, input_hi);
+ return XXH3_avalanche(acc);
+ }
+}
+
+XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t
+XXH3_len_0to16_64b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed)
+{
+ XXH_ASSERT(len <= 16);
+ { if (XXH_likely(len > 8)) return XXH3_len_9to16_64b(input, len, secret, seed);
+ if (XXH_likely(len >= 4)) return XXH3_len_4to8_64b(input, len, secret, seed);
+ if (len) return XXH3_len_1to3_64b(input, len, secret, seed);
+ return XXH64_avalanche(seed ^ (XXH_readLE64(secret+56) ^ XXH_readLE64(secret+64)));
+ }
+}
+
+/*
+ * DISCLAIMER: There are known *seed-dependent* multicollisions here due to
+ * multiplication by zero, affecting hashes of lengths 17 to 240.
+ *
+ * However, they are very unlikely.
+ *
+ * Keep this in mind when using the unseeded XXH3_64bits() variant: As with all
+ * unseeded non-cryptographic hashes, it does not attempt to defend itself
+ * against specially crafted inputs, only random inputs.
+ *
+ * Compared to classic UMAC where a 1 in 2^31 chance of 4 consecutive bytes
+ * cancelling out the secret is taken an arbitrary number of times (addressed
+ * in XXH3_accumulate_512), this collision is very unlikely with random inputs
+ * and/or proper seeding:
+ *
+ * This only has a 1 in 2^63 chance of 8 consecutive bytes cancelling out, in a
+ * function that is only called up to 16 times per hash with up to 240 bytes of
+ * input.
+ *
+ * This is not too bad for a non-cryptographic hash function, especially with
+ * only 64 bit outputs.
+ *
+ * The 128-bit variant (which trades some speed for strength) is NOT affected
+ * by this, although it is always a good idea to use a proper seed if you care
+ * about strength.
+ */
+XXH_FORCE_INLINE xxh_u64 XXH3_mix16B(const xxh_u8* XXH_RESTRICT input,
+ const xxh_u8* XXH_RESTRICT secret, xxh_u64 seed64)
+{
+#if defined(__GNUC__) && !defined(__clang__) /* GCC, not Clang */ \
+ && defined(__i386__) && defined(__SSE2__) /* x86 + SSE2 */ \
+ && !defined(XXH_ENABLE_AUTOVECTORIZE) /* Define to disable like XXH32 hack */
+ /*
+ * UGLY HACK:
+ * GCC for x86 tends to autovectorize the 128-bit multiply, resulting in
+ * slower code.
+ *
+ * By forcing seed64 into a register, we disrupt the cost model and
+ * cause it to scalarize. See `XXH32_round()`
+ *
+ * FIXME: Clang's output is still _much_ faster -- On an AMD Ryzen 3600,
+ * XXH3_64bits @ len=240 runs at 4.6 GB/s with Clang 9, but 3.3 GB/s on
+ * GCC 9.2, despite both emitting scalar code.
+ *
+ * GCC generates much better scalar code than Clang for the rest of XXH3,
+ * which is why finding a more optimal codepath is an interest.
+ */
+ XXH_COMPILER_GUARD(seed64);
+#endif
+ { xxh_u64 const input_lo = XXH_readLE64(input);
+ xxh_u64 const input_hi = XXH_readLE64(input+8);
+ return XXH3_mul128_fold64(
+ input_lo ^ (XXH_readLE64(secret) + seed64),
+ input_hi ^ (XXH_readLE64(secret+8) - seed64)
+ );
+ }
+}
+
+/* For mid range keys, XXH3 uses a Mum-hash variant. */
+XXH_FORCE_INLINE XXH_PUREF XXH64_hash_t
+XXH3_len_17to128_64b(const xxh_u8* XXH_RESTRICT input, size_t len,
+ const xxh_u8* XXH_RESTRICT secret, size_t secretSize,
+ XXH64_hash_t seed)
+{
+ XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize;
+ XXH_ASSERT(16 < len && len <= 128);
+
+ { xxh_u64 acc = len * XXH_PRIME64_1;
+#if XXH_SIZE_OPT >= 1
+ /* Smaller and cleaner, but slightly slower. */
+ size_t i = (len - 1) / 32;
+ do {
+ acc += XXH3_mix16B(input+16 * i, secret+32*i, seed);
+ acc += XXH3_mix16B(input+len-16*(i+1), secret+32*i+16, seed);
+ } while (i-- != 0);
+#else
+ if (len > 32) {
+ if (len > 64) {
+ if (len > 96) {
+ acc += XXH3_mix16B(input+48, secret+96, seed);
+ acc += XXH3_mix16B(input+len-64, secret+112, seed);
+ }
+ acc += XXH3_mix16B(input+32, secret+64, seed);
+ acc += XXH3_mix16B(input+len-48, secret+80, seed);
+ }
+ acc += XXH3_mix16B(input+16, secret+32, seed);
+ acc += XXH3_mix16B(input+len-32, secret+48, seed);
+ }
+ acc += XXH3_mix16B(input+0, secret+0, seed);
+ acc += XXH3_mix16B(input+len-16, secret+16, seed);
+#endif
+ return XXH3_avalanche(acc);
+ }
+}
+
+#define XXH3_MIDSIZE_MAX 240
+
+XXH_NO_INLINE XXH_PUREF XXH64_hash_t
+XXH3_len_129to240_64b(const xxh_u8* XXH_RESTRICT input, size_t len,
+ const xxh_u8* XXH_RESTRICT secret, size_t secretSize,
+ XXH64_hash_t seed)
+{
+ XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize;
+ XXH_ASSERT(128 < len && len <= XXH3_MIDSIZE_MAX);
+
+ #define XXH3_MIDSIZE_STARTOFFSET 3
+ #define XXH3_MIDSIZE_LASTOFFSET 17
+
+ { xxh_u64 acc = len * XXH_PRIME64_1;
+ int const nbRounds = (int)len / 16;
+ int i;
+ for (i=0; i<8; i++) {
+ acc += XXH3_mix16B(input+(16*i), secret+(16*i), seed);
+ }
+ acc = XXH3_avalanche(acc);
+ XXH_ASSERT(nbRounds >= 8);
+#if defined(__clang__) /* Clang */ \
+ && (defined(__ARM_NEON) || defined(__ARM_NEON__)) /* NEON */ \
+ && !defined(XXH_ENABLE_AUTOVECTORIZE) /* Define to disable */
+ /*
+ * UGLY HACK:
+ * Clang for ARMv7-A tries to vectorize this loop, similar to GCC x86.
+ * In everywhere else, it uses scalar code.
+ *
+ * For 64->128-bit multiplies, even if the NEON was 100% optimal, it
+ * would still be slower than UMAAL (see XXH_mult64to128).
+ *
+ * Unfortunately, Clang doesn't handle the long multiplies properly and
+ * converts them to the nonexistent "vmulq_u64" intrinsic, which is then
+ * scalarized into an ugly mess of VMOV.32 instructions.
+ *
+ * This mess is difficult to avoid without turning autovectorization
+ * off completely, but they are usually relatively minor and/or not
+ * worth it to fix.
+ *
+ * This loop is the easiest to fix, as unlike XXH32, this pragma
+ * _actually works_ because it is a loop vectorization instead of an
+ * SLP vectorization.
+ */
+ #pragma clang loop vectorize(disable)
+#endif
+ for (i=8 ; i < nbRounds; i++) {
+ acc += XXH3_mix16B(input+(16*i), secret+(16*(i-8)) + XXH3_MIDSIZE_STARTOFFSET, seed);
+ }
+ /* last bytes */
+ acc += XXH3_mix16B(input + len - 16, secret + XXH3_SECRET_SIZE_MIN - XXH3_MIDSIZE_LASTOFFSET, seed);
+ return XXH3_avalanche(acc);
+ }
+}
+
+
+/* ======= Long Keys ======= */
+
+#define XXH_STRIPE_LEN 64
+#define XXH_SECRET_CONSUME_RATE 8 /* nb of secret bytes consumed at each accumulation */
+#define XXH_ACC_NB (XXH_STRIPE_LEN / sizeof(xxh_u64))
+
+#ifdef XXH_OLD_NAMES
+# define STRIPE_LEN XXH_STRIPE_LEN
+# define ACC_NB XXH_ACC_NB
+#endif
+
+XXH_FORCE_INLINE void XXH_writeLE64(void* dst, xxh_u64 v64)
+{
+ if (!XXH_CPU_LITTLE_ENDIAN) v64 = XXH_swap64(v64);
+ XXH_memcpy(dst, &v64, sizeof(v64));
+}
+
+/* Several intrinsic functions below are supposed to accept __int64 as argument,
+ * as documented in https://software.intel.com/sites/landingpage/IntrinsicsGuide/ .
+ * However, several environments do not define __int64 type,
+ * requiring a workaround.
+ */
+#if !defined (__VMS) \
+ && (defined (__cplusplus) \
+ || (defined (__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L) /* C99 */) )
+ typedef int64_t xxh_i64;
+#else
+ /* the following type must have a width of 64-bit */
+ typedef long long xxh_i64;
+#endif
+
+
+/*
+ * XXH3_accumulate_512 is the tightest loop for long inputs, and it is the most optimized.
+ *
+ * It is a hardened version of UMAC, based off of FARSH's implementation.
+ *
+ * This was chosen because it adapts quite well to 32-bit, 64-bit, and SIMD
+ * implementations, and it is ridiculously fast.
+ *
+ * We harden it by mixing the original input to the accumulators as well as the product.
+ *
+ * This means that in the (relatively likely) case of a multiply by zero, the
+ * original input is preserved.
+ *
+ * On 128-bit inputs, we swap 64-bit pairs when we add the input to improve
+ * cross-pollination, as otherwise the upper and lower halves would be
+ * essentially independent.
+ *
+ * This doesn't matter on 64-bit hashes since they all get merged together in
+ * the end, so we skip the extra step.
+ *
+ * Both XXH3_64bits and XXH3_128bits use this subroutine.
+ */
+
+#if (XXH_VECTOR == XXH_AVX512) \
+ || (defined(XXH_DISPATCH_AVX512) && XXH_DISPATCH_AVX512 != 0)
+
+#ifndef XXH_TARGET_AVX512
+# define XXH_TARGET_AVX512 /* disable attribute target */
+#endif
+
+XXH_FORCE_INLINE XXH_TARGET_AVX512 void
+XXH3_accumulate_512_avx512(void* XXH_RESTRICT acc,
+ const void* XXH_RESTRICT input,
+ const void* XXH_RESTRICT secret)
+{
+ __m512i* const xacc = (__m512i *) acc;
+ XXH_ASSERT((((size_t)acc) & 63) == 0);
+ XXH_STATIC_ASSERT(XXH_STRIPE_LEN == sizeof(__m512i));
+
+ {
+ /* data_vec = input[0]; */
+ __m512i const data_vec = _mm512_loadu_si512 (input);
+ /* key_vec = secret[0]; */
+ __m512i const key_vec = _mm512_loadu_si512 (secret);
+ /* data_key = data_vec ^ key_vec; */
+ __m512i const data_key = _mm512_xor_si512 (data_vec, key_vec);
+ /* data_key_lo = data_key >> 32; */
+ __m512i const data_key_lo = _mm512_shuffle_epi32 (data_key, (_MM_PERM_ENUM)_MM_SHUFFLE(0, 3, 0, 1));
+ /* product = (data_key & 0xffffffff) * (data_key_lo & 0xffffffff); */
+ __m512i const product = _mm512_mul_epu32 (data_key, data_key_lo);
+ /* xacc[0] += swap(data_vec); */
+ __m512i const data_swap = _mm512_shuffle_epi32(data_vec, (_MM_PERM_ENUM)_MM_SHUFFLE(1, 0, 3, 2));
+ __m512i const sum = _mm512_add_epi64(*xacc, data_swap);
+ /* xacc[0] += product; */
+ *xacc = _mm512_add_epi64(product, sum);
+ }
+}
+
+/*
+ * XXH3_scrambleAcc: Scrambles the accumulators to improve mixing.
+ *
+ * Multiplication isn't perfect, as explained by Google in HighwayHash:
+ *
+ * // Multiplication mixes/scrambles bytes 0-7 of the 64-bit result to
+ * // varying degrees. In descending order of goodness, bytes
+ * // 3 4 2 5 1 6 0 7 have quality 228 224 164 160 100 96 36 32.
+ * // As expected, the upper and lower bytes are much worse.
+ *
+ * Source: https://github.com/google/highwayhash/blob/0aaf66b/highwayhash/hh_avx2.h#L291
+ *
+ * Since our algorithm uses a pseudorandom secret to add some variance into the
+ * mix, we don't need to (or want to) mix as often or as much as HighwayHash does.
+ *
+ * This isn't as tight as XXH3_accumulate, but still written in SIMD to avoid
+ * extraction.
+ *
+ * Both XXH3_64bits and XXH3_128bits use this subroutine.
+ */
+
+XXH_FORCE_INLINE XXH_TARGET_AVX512 void
+XXH3_scrambleAcc_avx512(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret)
+{
+ XXH_ASSERT((((size_t)acc) & 63) == 0);
+ XXH_STATIC_ASSERT(XXH_STRIPE_LEN == sizeof(__m512i));
+ { __m512i* const xacc = (__m512i*) acc;
+ const __m512i prime32 = _mm512_set1_epi32((int)XXH_PRIME32_1);
+
+ /* xacc[0] ^= (xacc[0] >> 47) */
+ __m512i const acc_vec = *xacc;
+ __m512i const shifted = _mm512_srli_epi64 (acc_vec, 47);
+ __m512i const data_vec = _mm512_xor_si512 (acc_vec, shifted);
+ /* xacc[0] ^= secret; */
+ __m512i const key_vec = _mm512_loadu_si512 (secret);
+ __m512i const data_key = _mm512_xor_si512 (data_vec, key_vec);
+
+ /* xacc[0] *= XXH_PRIME32_1; */
+ __m512i const data_key_hi = _mm512_shuffle_epi32 (data_key, (_MM_PERM_ENUM)_MM_SHUFFLE(0, 3, 0, 1));
+ __m512i const prod_lo = _mm512_mul_epu32 (data_key, prime32);
+ __m512i const prod_hi = _mm512_mul_epu32 (data_key_hi, prime32);
+ *xacc = _mm512_add_epi64(prod_lo, _mm512_slli_epi64(prod_hi, 32));
+ }
+}
+
+XXH_FORCE_INLINE XXH_TARGET_AVX512 void
+XXH3_initCustomSecret_avx512(void* XXH_RESTRICT customSecret, xxh_u64 seed64)
+{
+ XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 63) == 0);
+ XXH_STATIC_ASSERT(XXH_SEC_ALIGN == 64);
+ XXH_ASSERT(((size_t)customSecret & 63) == 0);
+ (void)(&XXH_writeLE64);
+ { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / sizeof(__m512i);
+ __m512i const seed = _mm512_mask_set1_epi64(_mm512_set1_epi64((xxh_i64)seed64), 0xAA, (xxh_i64)(0U - seed64));
+
+ const __m512i* const src = (const __m512i*) ((const void*) XXH3_kSecret);
+ __m512i* const dest = ( __m512i*) customSecret;
+ int i;
+ XXH_ASSERT(((size_t)src & 63) == 0); /* control alignment */
+ XXH_ASSERT(((size_t)dest & 63) == 0);
+ for (i=0; i < nbRounds; ++i) {
+ /* GCC has a bug, _mm512_stream_load_si512 accepts 'void*', not 'void const*',
+ * this will warn "discards 'const' qualifier". */
+ union {
+ const __m512i* cp;
+ void* p;
+ } remote_const_void;
+ remote_const_void.cp = src + i;
+ dest[i] = _mm512_add_epi64(_mm512_stream_load_si512(remote_const_void.p), seed);
+ } }
+}
+
+#endif
+
+#if (XXH_VECTOR == XXH_AVX2) \
+ || (defined(XXH_DISPATCH_AVX2) && XXH_DISPATCH_AVX2 != 0)
+
+#ifndef XXH_TARGET_AVX2
+# define XXH_TARGET_AVX2 /* disable attribute target */
+#endif
+
+XXH_FORCE_INLINE XXH_TARGET_AVX2 void
+XXH3_accumulate_512_avx2( void* XXH_RESTRICT acc,
+ const void* XXH_RESTRICT input,
+ const void* XXH_RESTRICT secret)
+{
+ XXH_ASSERT((((size_t)acc) & 31) == 0);
+ { __m256i* const xacc = (__m256i *) acc;
+ /* Unaligned. This is mainly for pointer arithmetic, and because
+ * _mm256_loadu_si256 requires a const __m256i * pointer for some reason. */
+ const __m256i* const xinput = (const __m256i *) input;
+ /* Unaligned. This is mainly for pointer arithmetic, and because
+ * _mm256_loadu_si256 requires a const __m256i * pointer for some reason. */
+ const __m256i* const xsecret = (const __m256i *) secret;
+
+ size_t i;
+ for (i=0; i < XXH_STRIPE_LEN/sizeof(__m256i); i++) {
+ /* data_vec = xinput[i]; */
+ __m256i const data_vec = _mm256_loadu_si256 (xinput+i);
+ /* key_vec = xsecret[i]; */
+ __m256i const key_vec = _mm256_loadu_si256 (xsecret+i);
+ /* data_key = data_vec ^ key_vec; */
+ __m256i const data_key = _mm256_xor_si256 (data_vec, key_vec);
+ /* data_key_lo = data_key >> 32; */
+ __m256i const data_key_lo = _mm256_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1));
+ /* product = (data_key & 0xffffffff) * (data_key_lo & 0xffffffff); */
+ __m256i const product = _mm256_mul_epu32 (data_key, data_key_lo);
+ /* xacc[i] += swap(data_vec); */
+ __m256i const data_swap = _mm256_shuffle_epi32(data_vec, _MM_SHUFFLE(1, 0, 3, 2));
+ __m256i const sum = _mm256_add_epi64(xacc[i], data_swap);
+ /* xacc[i] += product; */
+ xacc[i] = _mm256_add_epi64(product, sum);
+ } }
+}
+
+XXH_FORCE_INLINE XXH_TARGET_AVX2 void
+XXH3_scrambleAcc_avx2(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret)
+{
+ XXH_ASSERT((((size_t)acc) & 31) == 0);
+ { __m256i* const xacc = (__m256i*) acc;
+ /* Unaligned. This is mainly for pointer arithmetic, and because
+ * _mm256_loadu_si256 requires a const __m256i * pointer for some reason. */
+ const __m256i* const xsecret = (const __m256i *) secret;
+ const __m256i prime32 = _mm256_set1_epi32((int)XXH_PRIME32_1);
+
+ size_t i;
+ for (i=0; i < XXH_STRIPE_LEN/sizeof(__m256i); i++) {
+ /* xacc[i] ^= (xacc[i] >> 47) */
+ __m256i const acc_vec = xacc[i];
+ __m256i const shifted = _mm256_srli_epi64 (acc_vec, 47);
+ __m256i const data_vec = _mm256_xor_si256 (acc_vec, shifted);
+ /* xacc[i] ^= xsecret; */
+ __m256i const key_vec = _mm256_loadu_si256 (xsecret+i);
+ __m256i const data_key = _mm256_xor_si256 (data_vec, key_vec);
+
+ /* xacc[i] *= XXH_PRIME32_1; */
+ __m256i const data_key_hi = _mm256_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1));
+ __m256i const prod_lo = _mm256_mul_epu32 (data_key, prime32);
+ __m256i const prod_hi = _mm256_mul_epu32 (data_key_hi, prime32);
+ xacc[i] = _mm256_add_epi64(prod_lo, _mm256_slli_epi64(prod_hi, 32));
+ }
+ }
+}
+
+XXH_FORCE_INLINE XXH_TARGET_AVX2 void XXH3_initCustomSecret_avx2(void* XXH_RESTRICT customSecret, xxh_u64 seed64)
+{
+ XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 31) == 0);
+ XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE / sizeof(__m256i)) == 6);
+ XXH_STATIC_ASSERT(XXH_SEC_ALIGN <= 64);
+ (void)(&XXH_writeLE64);
+ XXH_PREFETCH(customSecret);
+ { __m256i const seed = _mm256_set_epi64x((xxh_i64)(0U - seed64), (xxh_i64)seed64, (xxh_i64)(0U - seed64), (xxh_i64)seed64);
+
+ const __m256i* const src = (const __m256i*) ((const void*) XXH3_kSecret);
+ __m256i* dest = ( __m256i*) customSecret;
+
+# if defined(__GNUC__) || defined(__clang__)
+ /*
+ * On GCC & Clang, marking 'dest' as modified will cause the compiler:
+ * - do not extract the secret from sse registers in the internal loop
+ * - use less common registers, and avoid pushing these reg into stack
+ */
+ XXH_COMPILER_GUARD(dest);
+# endif
+ XXH_ASSERT(((size_t)src & 31) == 0); /* control alignment */
+ XXH_ASSERT(((size_t)dest & 31) == 0);
+
+ /* GCC -O2 need unroll loop manually */
+ dest[0] = _mm256_add_epi64(_mm256_stream_load_si256(src+0), seed);
+ dest[1] = _mm256_add_epi64(_mm256_stream_load_si256(src+1), seed);
+ dest[2] = _mm256_add_epi64(_mm256_stream_load_si256(src+2), seed);
+ dest[3] = _mm256_add_epi64(_mm256_stream_load_si256(src+3), seed);
+ dest[4] = _mm256_add_epi64(_mm256_stream_load_si256(src+4), seed);
+ dest[5] = _mm256_add_epi64(_mm256_stream_load_si256(src+5), seed);
+ }
+}
+
+#endif
+
+/* x86dispatch always generates SSE2 */
+#if (XXH_VECTOR == XXH_SSE2) || defined(XXH_X86DISPATCH)
+
+#ifndef XXH_TARGET_SSE2
+# define XXH_TARGET_SSE2 /* disable attribute target */
+#endif
+
+XXH_FORCE_INLINE XXH_TARGET_SSE2 void
+XXH3_accumulate_512_sse2( void* XXH_RESTRICT acc,
+ const void* XXH_RESTRICT input,
+ const void* XXH_RESTRICT secret)
+{
+ /* SSE2 is just a half-scale version of the AVX2 version. */
+ XXH_ASSERT((((size_t)acc) & 15) == 0);
+ { __m128i* const xacc = (__m128i *) acc;
+ /* Unaligned. This is mainly for pointer arithmetic, and because
+ * _mm_loadu_si128 requires a const __m128i * pointer for some reason. */
+ const __m128i* const xinput = (const __m128i *) input;
+ /* Unaligned. This is mainly for pointer arithmetic, and because
+ * _mm_loadu_si128 requires a const __m128i * pointer for some reason. */
+ const __m128i* const xsecret = (const __m128i *) secret;
+
+ size_t i;
+ for (i=0; i < XXH_STRIPE_LEN/sizeof(__m128i); i++) {
+ /* data_vec = xinput[i]; */
+ __m128i const data_vec = _mm_loadu_si128 (xinput+i);
+ /* key_vec = xsecret[i]; */
+ __m128i const key_vec = _mm_loadu_si128 (xsecret+i);
+ /* data_key = data_vec ^ key_vec; */
+ __m128i const data_key = _mm_xor_si128 (data_vec, key_vec);
+ /* data_key_lo = data_key >> 32; */
+ __m128i const data_key_lo = _mm_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1));
+ /* product = (data_key & 0xffffffff) * (data_key_lo & 0xffffffff); */
+ __m128i const product = _mm_mul_epu32 (data_key, data_key_lo);
+ /* xacc[i] += swap(data_vec); */
+ __m128i const data_swap = _mm_shuffle_epi32(data_vec, _MM_SHUFFLE(1,0,3,2));
+ __m128i const sum = _mm_add_epi64(xacc[i], data_swap);
+ /* xacc[i] += product; */
+ xacc[i] = _mm_add_epi64(product, sum);
+ } }
+}
+
+XXH_FORCE_INLINE XXH_TARGET_SSE2 void
+XXH3_scrambleAcc_sse2(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret)
+{
+ XXH_ASSERT((((size_t)acc) & 15) == 0);
+ { __m128i* const xacc = (__m128i*) acc;
+ /* Unaligned. This is mainly for pointer arithmetic, and because
+ * _mm_loadu_si128 requires a const __m128i * pointer for some reason. */
+ const __m128i* const xsecret = (const __m128i *) secret;
+ const __m128i prime32 = _mm_set1_epi32((int)XXH_PRIME32_1);
+
+ size_t i;
+ for (i=0; i < XXH_STRIPE_LEN/sizeof(__m128i); i++) {
+ /* xacc[i] ^= (xacc[i] >> 47) */
+ __m128i const acc_vec = xacc[i];
+ __m128i const shifted = _mm_srli_epi64 (acc_vec, 47);
+ __m128i const data_vec = _mm_xor_si128 (acc_vec, shifted);
+ /* xacc[i] ^= xsecret[i]; */
+ __m128i const key_vec = _mm_loadu_si128 (xsecret+i);
+ __m128i const data_key = _mm_xor_si128 (data_vec, key_vec);
+
+ /* xacc[i] *= XXH_PRIME32_1; */
+ __m128i const data_key_hi = _mm_shuffle_epi32 (data_key, _MM_SHUFFLE(0, 3, 0, 1));
+ __m128i const prod_lo = _mm_mul_epu32 (data_key, prime32);
+ __m128i const prod_hi = _mm_mul_epu32 (data_key_hi, prime32);
+ xacc[i] = _mm_add_epi64(prod_lo, _mm_slli_epi64(prod_hi, 32));
+ }
+ }
+}
+
+XXH_FORCE_INLINE XXH_TARGET_SSE2 void XXH3_initCustomSecret_sse2(void* XXH_RESTRICT customSecret, xxh_u64 seed64)
+{
+ XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 15) == 0);
+ (void)(&XXH_writeLE64);
+ { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / sizeof(__m128i);
+
+# if defined(_MSC_VER) && defined(_M_IX86) && _MSC_VER < 1900
+ /* MSVC 32bit mode does not support _mm_set_epi64x before 2015 */
+ XXH_ALIGN(16) const xxh_i64 seed64x2[2] = { (xxh_i64)seed64, (xxh_i64)(0U - seed64) };
+ __m128i const seed = _mm_load_si128((__m128i const*)seed64x2);
+# else
+ __m128i const seed = _mm_set_epi64x((xxh_i64)(0U - seed64), (xxh_i64)seed64);
+# endif
+ int i;
+
+ const void* const src16 = XXH3_kSecret;
+ __m128i* dst16 = (__m128i*) customSecret;
+# if defined(__GNUC__) || defined(__clang__)
+ /*
+ * On GCC & Clang, marking 'dest' as modified will cause the compiler:
+ * - do not extract the secret from sse registers in the internal loop
+ * - use less common registers, and avoid pushing these reg into stack
+ */
+ XXH_COMPILER_GUARD(dst16);
+# endif
+ XXH_ASSERT(((size_t)src16 & 15) == 0); /* control alignment */
+ XXH_ASSERT(((size_t)dst16 & 15) == 0);
+
+ for (i=0; i < nbRounds; ++i) {
+ dst16[i] = _mm_add_epi64(_mm_load_si128((const __m128i *)src16+i), seed);
+ } }
+}
+
+#endif
+
+#if (XXH_VECTOR == XXH_NEON)
+
+/* forward declarations for the scalar routines */
+XXH_FORCE_INLINE void
+XXH3_scalarRound(void* XXH_RESTRICT acc, void const* XXH_RESTRICT input,
+ void const* XXH_RESTRICT secret, size_t lane);
+
+XXH_FORCE_INLINE void
+XXH3_scalarScrambleRound(void* XXH_RESTRICT acc,
+ void const* XXH_RESTRICT secret, size_t lane);
+
+/*!
+ * @internal
+ * @brief The bulk processing loop for NEON.
+ *
+ * The NEON code path is actually partially scalar when running on AArch64. This
+ * is to optimize the pipelining and can have up to 15% speedup depending on the
+ * CPU, and it also mitigates some GCC codegen issues.
+ *
+ * @see XXH3_NEON_LANES for configuring this and details about this optimization.
+ */
+XXH_FORCE_INLINE void
+XXH3_accumulate_512_neon( void* XXH_RESTRICT acc,
+ const void* XXH_RESTRICT input,
+ const void* XXH_RESTRICT secret)
+{
+ XXH_ASSERT((((size_t)acc) & 15) == 0);
+ XXH_STATIC_ASSERT(XXH3_NEON_LANES > 0 && XXH3_NEON_LANES <= XXH_ACC_NB && XXH3_NEON_LANES % 2 == 0);
+ {
+ uint64x2_t* const xacc = (uint64x2_t *) acc;
+ /* We don't use a uint32x4_t pointer because it causes bus errors on ARMv7. */
+ uint8_t const* const xinput = (const uint8_t *) input;
+ uint8_t const* const xsecret = (const uint8_t *) secret;
+
+ size_t i;
+ /* AArch64 uses both scalar and neon at the same time */
+ for (i = XXH3_NEON_LANES; i < XXH_ACC_NB; i++) {
+ XXH3_scalarRound(acc, input, secret, i);
+ }
+ for (i=0; i < XXH3_NEON_LANES / 2; i++) {
+ uint64x2_t acc_vec = xacc[i];
+ /* data_vec = xinput[i]; */
+ uint64x2_t data_vec = XXH_vld1q_u64(xinput + (i * 16));
+ /* key_vec = xsecret[i]; */
+ uint64x2_t key_vec = XXH_vld1q_u64(xsecret + (i * 16));
+ uint64x2_t data_key;
+ uint32x2_t data_key_lo, data_key_hi;
+ /* acc_vec_2 = swap(data_vec) */
+ uint64x2_t acc_vec_2 = vextq_u64(data_vec, data_vec, 1);
+ /* data_key = data_vec ^ key_vec; */
+ data_key = veorq_u64(data_vec, key_vec);
+ /* data_key_lo = (uint32x2_t) (data_key & 0xFFFFFFFF);
+ * data_key_hi = (uint32x2_t) (data_key >> 32);
+ * data_key = UNDEFINED; */
+ XXH_SPLIT_IN_PLACE(data_key, data_key_lo, data_key_hi);
+ /* acc_vec_2 += (uint64x2_t) data_key_lo * (uint64x2_t) data_key_hi; */
+ acc_vec_2 = vmlal_u32 (acc_vec_2, data_key_lo, data_key_hi);
+ /* xacc[i] += acc_vec_2; */
+ acc_vec = vaddq_u64 (acc_vec, acc_vec_2);
+ xacc[i] = acc_vec;
+ }
+
+ }
+}
+
+XXH_FORCE_INLINE void
+XXH3_scrambleAcc_neon(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret)
+{
+ XXH_ASSERT((((size_t)acc) & 15) == 0);
+
+ { uint64x2_t* xacc = (uint64x2_t*) acc;
+ uint8_t const* xsecret = (uint8_t const*) secret;
+ uint32x2_t prime = vdup_n_u32 (XXH_PRIME32_1);
+
+ size_t i;
+ /* AArch64 uses both scalar and neon at the same time */
+ for (i = XXH3_NEON_LANES; i < XXH_ACC_NB; i++) {
+ XXH3_scalarScrambleRound(acc, secret, i);
+ }
+ for (i=0; i < XXH3_NEON_LANES / 2; i++) {
+ /* xacc[i] ^= (xacc[i] >> 47); */
+ uint64x2_t acc_vec = xacc[i];
+ uint64x2_t shifted = vshrq_n_u64 (acc_vec, 47);
+ uint64x2_t data_vec = veorq_u64 (acc_vec, shifted);
+
+ /* xacc[i] ^= xsecret[i]; */
+ uint64x2_t key_vec = XXH_vld1q_u64 (xsecret + (i * 16));
+ uint64x2_t data_key = veorq_u64 (data_vec, key_vec);
+
+ /* xacc[i] *= XXH_PRIME32_1 */
+ uint32x2_t data_key_lo, data_key_hi;
+ /* data_key_lo = (uint32x2_t) (xacc[i] & 0xFFFFFFFF);
+ * data_key_hi = (uint32x2_t) (xacc[i] >> 32);
+ * xacc[i] = UNDEFINED; */
+ XXH_SPLIT_IN_PLACE(data_key, data_key_lo, data_key_hi);
+ { /*
+ * prod_hi = (data_key >> 32) * XXH_PRIME32_1;
+ *
+ * Avoid vmul_u32 + vshll_n_u32 since Clang 6 and 7 will
+ * incorrectly "optimize" this:
+ * tmp = vmul_u32(vmovn_u64(a), vmovn_u64(b));
+ * shifted = vshll_n_u32(tmp, 32);
+ * to this:
+ * tmp = "vmulq_u64"(a, b); // no such thing!
+ * shifted = vshlq_n_u64(tmp, 32);
+ *
+ * However, unlike SSE, Clang lacks a 64-bit multiply routine
+ * for NEON, and it scalarizes two 64-bit multiplies instead.
+ *
+ * vmull_u32 has the same timing as vmul_u32, and it avoids
+ * this bug completely.
+ * See https://bugs.llvm.org/show_bug.cgi?id=39967
+ */
+ uint64x2_t prod_hi = vmull_u32 (data_key_hi, prime);
+ /* xacc[i] = prod_hi << 32; */
+ prod_hi = vshlq_n_u64(prod_hi, 32);
+ /* xacc[i] += (prod_hi & 0xFFFFFFFF) * XXH_PRIME32_1; */
+ xacc[i] = vmlal_u32(prod_hi, data_key_lo, prime);
+ }
+ }
+ }
+}
+
+#endif
+
+#if (XXH_VECTOR == XXH_VSX)
+
+XXH_FORCE_INLINE void
+XXH3_accumulate_512_vsx( void* XXH_RESTRICT acc,
+ const void* XXH_RESTRICT input,
+ const void* XXH_RESTRICT secret)
+{
+ /* presumed aligned */
+ unsigned int* const xacc = (unsigned int*) acc;
+ xxh_u64x2 const* const xinput = (xxh_u64x2 const*) input; /* no alignment restriction */
+ xxh_u64x2 const* const xsecret = (xxh_u64x2 const*) secret; /* no alignment restriction */
+ xxh_u64x2 const v32 = { 32, 32 };
+ size_t i;
+ for (i = 0; i < XXH_STRIPE_LEN / sizeof(xxh_u64x2); i++) {
+ /* data_vec = xinput[i]; */
+ xxh_u64x2 const data_vec = XXH_vec_loadu(xinput + i);
+ /* key_vec = xsecret[i]; */
+ xxh_u64x2 const key_vec = XXH_vec_loadu(xsecret + i);
+ xxh_u64x2 const data_key = data_vec ^ key_vec;
+ /* shuffled = (data_key << 32) | (data_key >> 32); */
+ xxh_u32x4 const shuffled = (xxh_u32x4)vec_rl(data_key, v32);
+ /* product = ((xxh_u64x2)data_key & 0xFFFFFFFF) * ((xxh_u64x2)shuffled & 0xFFFFFFFF); */
+ xxh_u64x2 const product = XXH_vec_mulo((xxh_u32x4)data_key, shuffled);
+ /* acc_vec = xacc[i]; */
+ xxh_u64x2 acc_vec = (xxh_u64x2)vec_xl(0, xacc + 4 * i);
+ acc_vec += product;
+
+ /* swap high and low halves */
+#ifdef __s390x__
+ acc_vec += vec_permi(data_vec, data_vec, 2);
+#else
+ acc_vec += vec_xxpermdi(data_vec, data_vec, 2);
+#endif
+ /* xacc[i] = acc_vec; */
+ vec_xst((xxh_u32x4)acc_vec, 0, xacc + 4 * i);
+ }
+}
+
+XXH_FORCE_INLINE void
+XXH3_scrambleAcc_vsx(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret)
+{
+ XXH_ASSERT((((size_t)acc) & 15) == 0);
+
+ { xxh_u64x2* const xacc = (xxh_u64x2*) acc;
+ const xxh_u64x2* const xsecret = (const xxh_u64x2*) secret;
+ /* constants */
+ xxh_u64x2 const v32 = { 32, 32 };
+ xxh_u64x2 const v47 = { 47, 47 };
+ xxh_u32x4 const prime = { XXH_PRIME32_1, XXH_PRIME32_1, XXH_PRIME32_1, XXH_PRIME32_1 };
+ size_t i;
+ for (i = 0; i < XXH_STRIPE_LEN / sizeof(xxh_u64x2); i++) {
+ /* xacc[i] ^= (xacc[i] >> 47); */
+ xxh_u64x2 const acc_vec = xacc[i];
+ xxh_u64x2 const data_vec = acc_vec ^ (acc_vec >> v47);
+
+ /* xacc[i] ^= xsecret[i]; */
+ xxh_u64x2 const key_vec = XXH_vec_loadu(xsecret + i);
+ xxh_u64x2 const data_key = data_vec ^ key_vec;
+
+ /* xacc[i] *= XXH_PRIME32_1 */
+ /* prod_lo = ((xxh_u64x2)data_key & 0xFFFFFFFF) * ((xxh_u64x2)prime & 0xFFFFFFFF); */
+ xxh_u64x2 const prod_even = XXH_vec_mule((xxh_u32x4)data_key, prime);
+ /* prod_hi = ((xxh_u64x2)data_key >> 32) * ((xxh_u64x2)prime >> 32); */
+ xxh_u64x2 const prod_odd = XXH_vec_mulo((xxh_u32x4)data_key, prime);
+ xacc[i] = prod_odd + (prod_even << v32);
+ } }
+}
+
+#endif
+
+/* scalar variants - universal */
+
+/*!
+ * @internal
+ * @brief Scalar round for @ref XXH3_accumulate_512_scalar().
+ *
+ * This is extracted to its own function because the NEON path uses a combination
+ * of NEON and scalar.
+ */
+XXH_FORCE_INLINE void
+XXH3_scalarRound(void* XXH_RESTRICT acc,
+ void const* XXH_RESTRICT input,
+ void const* XXH_RESTRICT secret,
+ size_t lane)
+{
+ xxh_u64* xacc = (xxh_u64*) acc;
+ xxh_u8 const* xinput = (xxh_u8 const*) input;
+ xxh_u8 const* xsecret = (xxh_u8 const*) secret;
+ XXH_ASSERT(lane < XXH_ACC_NB);
+ XXH_ASSERT(((size_t)acc & (XXH_ACC_ALIGN-1)) == 0);
+ {
+ xxh_u64 const data_val = XXH_readLE64(xinput + lane * 8);
+ xxh_u64 const data_key = data_val ^ XXH_readLE64(xsecret + lane * 8);
+ xacc[lane ^ 1] += data_val; /* swap adjacent lanes */
+ xacc[lane] += XXH_mult32to64(data_key & 0xFFFFFFFF, data_key >> 32);
+ }
+}
+
+/*!
+ * @internal
+ * @brief Processes a 64 byte block of data using the scalar path.
+ */
+XXH_FORCE_INLINE void
+XXH3_accumulate_512_scalar(void* XXH_RESTRICT acc,
+ const void* XXH_RESTRICT input,
+ const void* XXH_RESTRICT secret)
+{
+ size_t i;
+ /* ARM GCC refuses to unroll this loop, resulting in a 24% slowdown on ARMv6. */
+#if defined(__GNUC__) && !defined(__clang__) \
+ && (defined(__arm__) || defined(__thumb2__)) \
+ && defined(__ARM_FEATURE_UNALIGNED) /* no unaligned access just wastes bytes */ \
+ && XXH_SIZE_OPT <= 0
+# pragma GCC unroll 8
+#endif
+ for (i=0; i < XXH_ACC_NB; i++) {
+ XXH3_scalarRound(acc, input, secret, i);
+ }
+}
+
+/*!
+ * @internal
+ * @brief Scalar scramble step for @ref XXH3_scrambleAcc_scalar().
+ *
+ * This is extracted to its own function because the NEON path uses a combination
+ * of NEON and scalar.
+ */
+XXH_FORCE_INLINE void
+XXH3_scalarScrambleRound(void* XXH_RESTRICT acc,
+ void const* XXH_RESTRICT secret,
+ size_t lane)
+{
+ xxh_u64* const xacc = (xxh_u64*) acc; /* presumed aligned */
+ const xxh_u8* const xsecret = (const xxh_u8*) secret; /* no alignment restriction */
+ XXH_ASSERT((((size_t)acc) & (XXH_ACC_ALIGN-1)) == 0);
+ XXH_ASSERT(lane < XXH_ACC_NB);
+ {
+ xxh_u64 const key64 = XXH_readLE64(xsecret + lane * 8);
+ xxh_u64 acc64 = xacc[lane];
+ acc64 = XXH_xorshift64(acc64, 47);
+ acc64 ^= key64;
+ acc64 *= XXH_PRIME32_1;
+ xacc[lane] = acc64;
+ }
+}
+
+/*!
+ * @internal
+ * @brief Scrambles the accumulators after a large chunk has been read
+ */
+XXH_FORCE_INLINE void
+XXH3_scrambleAcc_scalar(void* XXH_RESTRICT acc, const void* XXH_RESTRICT secret)
+{
+ size_t i;
+ for (i=0; i < XXH_ACC_NB; i++) {
+ XXH3_scalarScrambleRound(acc, secret, i);
+ }
+}
+
+XXH_FORCE_INLINE void
+XXH3_initCustomSecret_scalar(void* XXH_RESTRICT customSecret, xxh_u64 seed64)
+{
+ /*
+ * We need a separate pointer for the hack below,
+ * which requires a non-const pointer.
+ * Any decent compiler will optimize this out otherwise.
+ */
+ const xxh_u8* kSecretPtr = XXH3_kSecret;
+ XXH_STATIC_ASSERT((XXH_SECRET_DEFAULT_SIZE & 15) == 0);
+
+#if defined(__clang__) && defined(__aarch64__)
+ /*
+ * UGLY HACK:
+ * Clang generates a bunch of MOV/MOVK pairs for aarch64, and they are
+ * placed sequentially, in order, at the top of the unrolled loop.
+ *
+ * While MOVK is great for generating constants (2 cycles for a 64-bit
+ * constant compared to 4 cycles for LDR), it fights for bandwidth with
+ * the arithmetic instructions.
+ *
+ * I L S
+ * MOVK
+ * MOVK
+ * MOVK
+ * MOVK
+ * ADD
+ * SUB STR
+ * STR
+ * By forcing loads from memory (as the asm line causes Clang to assume
+ * that XXH3_kSecretPtr has been changed), the pipelines are used more
+ * efficiently:
+ * I L S
+ * LDR
+ * ADD LDR
+ * SUB STR
+ * STR
+ *
+ * See XXH3_NEON_LANES for details on the pipsline.
+ *
+ * XXH3_64bits_withSeed, len == 256, Snapdragon 835
+ * without hack: 2654.4 MB/s
+ * with hack: 3202.9 MB/s
+ */
+ XXH_COMPILER_GUARD(kSecretPtr);
+#endif
+ /*
+ * Note: in debug mode, this overrides the asm optimization
+ * and Clang will emit MOVK chains again.
+ */
+ XXH_ASSERT(kSecretPtr == XXH3_kSecret);
+
+ { int const nbRounds = XXH_SECRET_DEFAULT_SIZE / 16;
+ int i;
+ for (i=0; i < nbRounds; i++) {
+ /*
+ * The asm hack causes Clang to assume that kSecretPtr aliases with
+ * customSecret, and on aarch64, this prevented LDP from merging two
+ * loads together for free. Putting the loads together before the stores
+ * properly generates LDP.
+ */
+ xxh_u64 lo = XXH_readLE64(kSecretPtr + 16*i) + seed64;
+ xxh_u64 hi = XXH_readLE64(kSecretPtr + 16*i + 8) - seed64;
+ XXH_writeLE64((xxh_u8*)customSecret + 16*i, lo);
+ XXH_writeLE64((xxh_u8*)customSecret + 16*i + 8, hi);
+ } }
+}
+
+
+typedef void (*XXH3_f_accumulate_512)(void* XXH_RESTRICT, const void*, const void*);
+typedef void (*XXH3_f_scrambleAcc)(void* XXH_RESTRICT, const void*);
+typedef void (*XXH3_f_initCustomSecret)(void* XXH_RESTRICT, xxh_u64);
+
+
+#if (XXH_VECTOR == XXH_AVX512)
+
+#define XXH3_accumulate_512 XXH3_accumulate_512_avx512
+#define XXH3_scrambleAcc XXH3_scrambleAcc_avx512
+#define XXH3_initCustomSecret XXH3_initCustomSecret_avx512
+
+#elif (XXH_VECTOR == XXH_AVX2)
+
+#define XXH3_accumulate_512 XXH3_accumulate_512_avx2
+#define XXH3_scrambleAcc XXH3_scrambleAcc_avx2
+#define XXH3_initCustomSecret XXH3_initCustomSecret_avx2
+
+#elif (XXH_VECTOR == XXH_SSE2)
+
+#define XXH3_accumulate_512 XXH3_accumulate_512_sse2
+#define XXH3_scrambleAcc XXH3_scrambleAcc_sse2
+#define XXH3_initCustomSecret XXH3_initCustomSecret_sse2
+
+#elif (XXH_VECTOR == XXH_NEON)
+
+#define XXH3_accumulate_512 XXH3_accumulate_512_neon
+#define XXH3_scrambleAcc XXH3_scrambleAcc_neon
+#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar
+
+#elif (XXH_VECTOR == XXH_VSX)
+
+#define XXH3_accumulate_512 XXH3_accumulate_512_vsx
+#define XXH3_scrambleAcc XXH3_scrambleAcc_vsx
+#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar
+
+#else /* scalar */
+
+#define XXH3_accumulate_512 XXH3_accumulate_512_scalar
+#define XXH3_scrambleAcc XXH3_scrambleAcc_scalar
+#define XXH3_initCustomSecret XXH3_initCustomSecret_scalar
+
+#endif
+
+#if XXH_SIZE_OPT >= 1 /* don't do SIMD for initialization */
+# undef XXH3_initCustomSecret
+# define XXH3_initCustomSecret XXH3_initCustomSecret_scalar
+#endif
+
+#ifndef XXH_PREFETCH_DIST
+# ifdef __clang__
+# define XXH_PREFETCH_DIST 320
+# else
+# if (XXH_VECTOR == XXH_AVX512)
+# define XXH_PREFETCH_DIST 512
+# else
+# define XXH_PREFETCH_DIST 384
+# endif
+# endif /* __clang__ */
+#endif /* XXH_PREFETCH_DIST */
+
+/*
+ * XXH3_accumulate()
+ * Loops over XXH3_accumulate_512().
+ * Assumption: nbStripes will not overflow the secret size
+ */
+XXH_FORCE_INLINE void
+XXH3_accumulate( xxh_u64* XXH_RESTRICT acc,
+ const xxh_u8* XXH_RESTRICT input,
+ const xxh_u8* XXH_RESTRICT secret,
+ size_t nbStripes,
+ XXH3_f_accumulate_512 f_acc512)
+{
+ size_t n;
+ for (n = 0; n < nbStripes; n++ ) {
+ const xxh_u8* const in = input + n*XXH_STRIPE_LEN;
+ XXH_PREFETCH(in + XXH_PREFETCH_DIST);
+ f_acc512(acc,
+ in,
+ secret + n*XXH_SECRET_CONSUME_RATE);
+ }
+}
+
+XXH_FORCE_INLINE void
+XXH3_hashLong_internal_loop(xxh_u64* XXH_RESTRICT acc,
+ const xxh_u8* XXH_RESTRICT input, size_t len,
+ const xxh_u8* XXH_RESTRICT secret, size_t secretSize,
+ XXH3_f_accumulate_512 f_acc512,
+ XXH3_f_scrambleAcc f_scramble)
+{
+ size_t const nbStripesPerBlock = (secretSize - XXH_STRIPE_LEN) / XXH_SECRET_CONSUME_RATE;
+ size_t const block_len = XXH_STRIPE_LEN * nbStripesPerBlock;
+ size_t const nb_blocks = (len - 1) / block_len;
+
+ size_t n;
+
+ XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN);
+
+ for (n = 0; n < nb_blocks; n++) {
+ XXH3_accumulate(acc, input + n*block_len, secret, nbStripesPerBlock, f_acc512);
+ f_scramble(acc, secret + secretSize - XXH_STRIPE_LEN);
+ }
+
+ /* last partial block */
+ XXH_ASSERT(len > XXH_STRIPE_LEN);
+ { size_t const nbStripes = ((len - 1) - (block_len * nb_blocks)) / XXH_STRIPE_LEN;
+ XXH_ASSERT(nbStripes <= (secretSize / XXH_SECRET_CONSUME_RATE));
+ XXH3_accumulate(acc, input + nb_blocks*block_len, secret, nbStripes, f_acc512);
+
+ /* last stripe */
+ { const xxh_u8* const p = input + len - XXH_STRIPE_LEN;
+#define XXH_SECRET_LASTACC_START 7 /* not aligned on 8, last secret is different from acc & scrambler */
+ f_acc512(acc, p, secret + secretSize - XXH_STRIPE_LEN - XXH_SECRET_LASTACC_START);
+ } }
+}
+
+XXH_FORCE_INLINE xxh_u64
+XXH3_mix2Accs(const xxh_u64* XXH_RESTRICT acc, const xxh_u8* XXH_RESTRICT secret)
+{
+ return XXH3_mul128_fold64(
+ acc[0] ^ XXH_readLE64(secret),
+ acc[1] ^ XXH_readLE64(secret+8) );
+}
+
+static XXH64_hash_t
+XXH3_mergeAccs(const xxh_u64* XXH_RESTRICT acc, const xxh_u8* XXH_RESTRICT secret, xxh_u64 start)
+{
+ xxh_u64 result64 = start;
+ size_t i = 0;
+
+ for (i = 0; i < 4; i++) {
+ result64 += XXH3_mix2Accs(acc+2*i, secret + 16*i);
+#if defined(__clang__) /* Clang */ \
+ && (defined(__arm__) || defined(__thumb__)) /* ARMv7 */ \
+ && (defined(__ARM_NEON) || defined(__ARM_NEON__)) /* NEON */ \
+ && !defined(XXH_ENABLE_AUTOVECTORIZE) /* Define to disable */
+ /*
+ * UGLY HACK:
+ * Prevent autovectorization on Clang ARMv7-a. Exact same problem as
+ * the one in XXH3_len_129to240_64b. Speeds up shorter keys > 240b.
+ * XXH3_64bits, len == 256, Snapdragon 835:
+ * without hack: 2063.7 MB/s
+ * with hack: 2560.7 MB/s
+ */
+ XXH_COMPILER_GUARD(result64);
+#endif
+ }
+
+ return XXH3_avalanche(result64);
+}
+
+#define XXH3_INIT_ACC { XXH_PRIME32_3, XXH_PRIME64_1, XXH_PRIME64_2, XXH_PRIME64_3, \
+ XXH_PRIME64_4, XXH_PRIME32_2, XXH_PRIME64_5, XXH_PRIME32_1 }
+
+XXH_FORCE_INLINE XXH64_hash_t
+XXH3_hashLong_64b_internal(const void* XXH_RESTRICT input, size_t len,
+ const void* XXH_RESTRICT secret, size_t secretSize,
+ XXH3_f_accumulate_512 f_acc512,
+ XXH3_f_scrambleAcc f_scramble)
+{
+ XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64 acc[XXH_ACC_NB] = XXH3_INIT_ACC;
+
+ XXH3_hashLong_internal_loop(acc, (const xxh_u8*)input, len, (const xxh_u8*)secret, secretSize, f_acc512, f_scramble);
+
+ /* converge into final hash */
+ XXH_STATIC_ASSERT(sizeof(acc) == 64);
+ /* do not align on 8, so that the secret is different from the accumulator */
+#define XXH_SECRET_MERGEACCS_START 11
+ XXH_ASSERT(secretSize >= sizeof(acc) + XXH_SECRET_MERGEACCS_START);
+ return XXH3_mergeAccs(acc, (const xxh_u8*)secret + XXH_SECRET_MERGEACCS_START, (xxh_u64)len * XXH_PRIME64_1);
+}
+
+/*
+ * It's important for performance to transmit secret's size (when it's static)
+ * so that the compiler can properly optimize the vectorized loop.
+ * This makes a big performance difference for "medium" keys (<1 KB) when using AVX instruction set.
+ */
+XXH_FORCE_INLINE XXH64_hash_t
+XXH3_hashLong_64b_withSecret(const void* XXH_RESTRICT input, size_t len,
+ XXH64_hash_t seed64, const xxh_u8* XXH_RESTRICT secret, size_t secretLen)
+{
+ (void)seed64;
+ return XXH3_hashLong_64b_internal(input, len, secret, secretLen, XXH3_accumulate_512, XXH3_scrambleAcc);
+}
+
+/*
+ * It's preferable for performance that XXH3_hashLong is not inlined,
+ * as it results in a smaller function for small data, easier to the instruction cache.
+ * Note that inside this no_inline function, we do inline the internal loop,
+ * and provide a statically defined secret size to allow optimization of vector loop.
+ */
+XXH_NO_INLINE XXH_PUREF XXH64_hash_t
+XXH3_hashLong_64b_default(const void* XXH_RESTRICT input, size_t len,
+ XXH64_hash_t seed64, const xxh_u8* XXH_RESTRICT secret, size_t secretLen)
+{
+ (void)seed64; (void)secret; (void)secretLen;
+ return XXH3_hashLong_64b_internal(input, len, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_accumulate_512, XXH3_scrambleAcc);
+}
+
+/*
+ * XXH3_hashLong_64b_withSeed():
+ * Generate a custom key based on alteration of default XXH3_kSecret with the seed,
+ * and then use this key for long mode hashing.
+ *
+ * This operation is decently fast but nonetheless costs a little bit of time.
+ * Try to avoid it whenever possible (typically when seed==0).
+ *
+ * It's important for performance that XXH3_hashLong is not inlined. Not sure
+ * why (uop cache maybe?), but the difference is large and easily measurable.
+ */
+XXH_FORCE_INLINE XXH64_hash_t
+XXH3_hashLong_64b_withSeed_internal(const void* input, size_t len,
+ XXH64_hash_t seed,
+ XXH3_f_accumulate_512 f_acc512,
+ XXH3_f_scrambleAcc f_scramble,
+ XXH3_f_initCustomSecret f_initSec)
+{
+#if XXH_SIZE_OPT <= 0
+ if (seed == 0)
+ return XXH3_hashLong_64b_internal(input, len,
+ XXH3_kSecret, sizeof(XXH3_kSecret),
+ f_acc512, f_scramble);
+#endif
+ { XXH_ALIGN(XXH_SEC_ALIGN) xxh_u8 secret[XXH_SECRET_DEFAULT_SIZE];
+ f_initSec(secret, seed);
+ return XXH3_hashLong_64b_internal(input, len, secret, sizeof(secret),
+ f_acc512, f_scramble);
+ }
+}
+
+/*
+ * It's important for performance that XXH3_hashLong is not inlined.
+ */
+XXH_NO_INLINE XXH64_hash_t
+XXH3_hashLong_64b_withSeed(const void* XXH_RESTRICT input, size_t len,
+ XXH64_hash_t seed, const xxh_u8* XXH_RESTRICT secret, size_t secretLen)
+{
+ (void)secret; (void)secretLen;
+ return XXH3_hashLong_64b_withSeed_internal(input, len, seed,
+ XXH3_accumulate_512, XXH3_scrambleAcc, XXH3_initCustomSecret);
+}
+
+
+typedef XXH64_hash_t (*XXH3_hashLong64_f)(const void* XXH_RESTRICT, size_t,
+ XXH64_hash_t, const xxh_u8* XXH_RESTRICT, size_t);
+
+XXH_FORCE_INLINE XXH64_hash_t
+XXH3_64bits_internal(const void* XXH_RESTRICT input, size_t len,
+ XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen,
+ XXH3_hashLong64_f f_hashLong)
+{
+ XXH_ASSERT(secretLen >= XXH3_SECRET_SIZE_MIN);
+ /*
+ * If an action is to be taken if `secretLen` condition is not respected,
+ * it should be done here.
+ * For now, it's a contract pre-condition.
+ * Adding a check and a branch here would cost performance at every hash.
+ * Also, note that function signature doesn't offer room to return an error.
+ */
+ if (len <= 16)
+ return XXH3_len_0to16_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, seed64);
+ if (len <= 128)
+ return XXH3_len_17to128_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64);
+ if (len <= XXH3_MIDSIZE_MAX)
+ return XXH3_len_129to240_64b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64);
+ return f_hashLong(input, len, seed64, (const xxh_u8*)secret, secretLen);
+}
+
+
+/* === Public entry point === */
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH64_hash_t XXH3_64bits(const void* input, size_t length)
+{
+ return XXH3_64bits_internal(input, length, 0, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_default);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH64_hash_t
+XXH3_64bits_withSecret(const void* input, size_t length, const void* secret, size_t secretSize)
+{
+ return XXH3_64bits_internal(input, length, 0, secret, secretSize, XXH3_hashLong_64b_withSecret);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH64_hash_t
+XXH3_64bits_withSeed(const void* input, size_t length, XXH64_hash_t seed)
+{
+ return XXH3_64bits_internal(input, length, seed, XXH3_kSecret, sizeof(XXH3_kSecret), XXH3_hashLong_64b_withSeed);
+}
+
+XXH_PUBLIC_API XXH64_hash_t
+XXH3_64bits_withSecretandSeed(const void* input, size_t length, const void* secret, size_t secretSize, XXH64_hash_t seed)
+{
+ if (length <= XXH3_MIDSIZE_MAX)
+ return XXH3_64bits_internal(input, length, seed, XXH3_kSecret, sizeof(XXH3_kSecret), NULL);
+ return XXH3_hashLong_64b_withSecret(input, length, seed, (const xxh_u8*)secret, secretSize);
+}
+
+
+/* === XXH3 streaming === */
+#ifndef XXH_NO_STREAM
+/*
+ * Malloc's a pointer that is always aligned to align.
+ *
+ * This must be freed with `XXH_alignedFree()`.
+ *
+ * malloc typically guarantees 16 byte alignment on 64-bit systems and 8 byte
+ * alignment on 32-bit. This isn't enough for the 32 byte aligned loads in AVX2
+ * or on 32-bit, the 16 byte aligned loads in SSE2 and NEON.
+ *
+ * This underalignment previously caused a rather obvious crash which went
+ * completely unnoticed due to XXH3_createState() not actually being tested.
+ * Credit to RedSpah for noticing this bug.
+ *
+ * The alignment is done manually: Functions like posix_memalign or _mm_malloc
+ * are avoided: To maintain portability, we would have to write a fallback
+ * like this anyways, and besides, testing for the existence of library
+ * functions without relying on external build tools is impossible.
+ *
+ * The method is simple: Overallocate, manually align, and store the offset
+ * to the original behind the returned pointer.
+ *
+ * Align must be a power of 2 and 8 <= align <= 128.
+ */
+static XXH_MALLOCF void* XXH_alignedMalloc(size_t s, size_t align)
+{
+ XXH_ASSERT(align <= 128 && align >= 8); /* range check */
+ XXH_ASSERT((align & (align-1)) == 0); /* power of 2 */
+ XXH_ASSERT(s != 0 && s < (s + align)); /* empty/overflow */
+ { /* Overallocate to make room for manual realignment and an offset byte */
+ xxh_u8* base = (xxh_u8*)XXH_malloc(s + align);
+ if (base != NULL) {
+ /*
+ * Get the offset needed to align this pointer.
+ *
+ * Even if the returned pointer is aligned, there will always be
+ * at least one byte to store the offset to the original pointer.
+ */
+ size_t offset = align - ((size_t)base & (align - 1)); /* base % align */
+ /* Add the offset for the now-aligned pointer */
+ xxh_u8* ptr = base + offset;
+
+ XXH_ASSERT((size_t)ptr % align == 0);
+
+ /* Store the offset immediately before the returned pointer. */
+ ptr[-1] = (xxh_u8)offset;
+ return ptr;
+ }
+ return NULL;
+ }
+}
+/*
+ * Frees an aligned pointer allocated by XXH_alignedMalloc(). Don't pass
+ * normal malloc'd pointers, XXH_alignedMalloc has a specific data layout.
+ */
+static void XXH_alignedFree(void* p)
+{
+ if (p != NULL) {
+ xxh_u8* ptr = (xxh_u8*)p;
+ /* Get the offset byte we added in XXH_malloc. */
+ xxh_u8 offset = ptr[-1];
+ /* Free the original malloc'd pointer */
+ xxh_u8* base = ptr - offset;
+ XXH_free(base);
+ }
+}
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH3_state_t* XXH3_createState(void)
+{
+ XXH3_state_t* const state = (XXH3_state_t*)XXH_alignedMalloc(sizeof(XXH3_state_t), 64);
+ if (state==NULL) return NULL;
+ XXH3_INITSTATE(state);
+ return state;
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode XXH3_freeState(XXH3_state_t* statePtr)
+{
+ XXH_alignedFree(statePtr);
+ return XXH_OK;
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API void
+XXH3_copyState(XXH3_state_t* dst_state, const XXH3_state_t* src_state)
+{
+ XXH_memcpy(dst_state, src_state, sizeof(*dst_state));
+}
+
+static void
+XXH3_reset_internal(XXH3_state_t* statePtr,
+ XXH64_hash_t seed,
+ const void* secret, size_t secretSize)
+{
+ size_t const initStart = offsetof(XXH3_state_t, bufferedSize);
+ size_t const initLength = offsetof(XXH3_state_t, nbStripesPerBlock) - initStart;
+ XXH_ASSERT(offsetof(XXH3_state_t, nbStripesPerBlock) > initStart);
+ XXH_ASSERT(statePtr != NULL);
+ /* set members from bufferedSize to nbStripesPerBlock (excluded) to 0 */
+ memset((char*)statePtr + initStart, 0, initLength);
+ statePtr->acc[0] = XXH_PRIME32_3;
+ statePtr->acc[1] = XXH_PRIME64_1;
+ statePtr->acc[2] = XXH_PRIME64_2;
+ statePtr->acc[3] = XXH_PRIME64_3;
+ statePtr->acc[4] = XXH_PRIME64_4;
+ statePtr->acc[5] = XXH_PRIME32_2;
+ statePtr->acc[6] = XXH_PRIME64_5;
+ statePtr->acc[7] = XXH_PRIME32_1;
+ statePtr->seed = seed;
+ statePtr->useSeed = (seed != 0);
+ statePtr->extSecret = (const unsigned char*)secret;
+ XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN);
+ statePtr->secretLimit = secretSize - XXH_STRIPE_LEN;
+ statePtr->nbStripesPerBlock = statePtr->secretLimit / XXH_SECRET_CONSUME_RATE;
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_64bits_reset(XXH3_state_t* statePtr)
+{
+ if (statePtr == NULL) return XXH_ERROR;
+ XXH3_reset_internal(statePtr, 0, XXH3_kSecret, XXH_SECRET_DEFAULT_SIZE);
+ return XXH_OK;
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_64bits_reset_withSecret(XXH3_state_t* statePtr, const void* secret, size_t secretSize)
+{
+ if (statePtr == NULL) return XXH_ERROR;
+ XXH3_reset_internal(statePtr, 0, secret, secretSize);
+ if (secret == NULL) return XXH_ERROR;
+ if (secretSize < XXH3_SECRET_SIZE_MIN) return XXH_ERROR;
+ return XXH_OK;
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_64bits_reset_withSeed(XXH3_state_t* statePtr, XXH64_hash_t seed)
+{
+ if (statePtr == NULL) return XXH_ERROR;
+ if (seed==0) return XXH3_64bits_reset(statePtr);
+ if ((seed != statePtr->seed) || (statePtr->extSecret != NULL))
+ XXH3_initCustomSecret(statePtr->customSecret, seed);
+ XXH3_reset_internal(statePtr, seed, NULL, XXH_SECRET_DEFAULT_SIZE);
+ return XXH_OK;
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_64bits_reset_withSecretandSeed(XXH3_state_t* statePtr, const void* secret, size_t secretSize, XXH64_hash_t seed64)
+{
+ if (statePtr == NULL) return XXH_ERROR;
+ if (secret == NULL) return XXH_ERROR;
+ if (secretSize < XXH3_SECRET_SIZE_MIN) return XXH_ERROR;
+ XXH3_reset_internal(statePtr, seed64, secret, secretSize);
+ statePtr->useSeed = 1; /* always, even if seed64==0 */
+ return XXH_OK;
+}
+
+/* Note : when XXH3_consumeStripes() is invoked,
+ * there must be a guarantee that at least one more byte must be consumed from input
+ * so that the function can blindly consume all stripes using the "normal" secret segment */
+XXH_FORCE_INLINE void
+XXH3_consumeStripes(xxh_u64* XXH_RESTRICT acc,
+ size_t* XXH_RESTRICT nbStripesSoFarPtr, size_t nbStripesPerBlock,
+ const xxh_u8* XXH_RESTRICT input, size_t nbStripes,
+ const xxh_u8* XXH_RESTRICT secret, size_t secretLimit,
+ XXH3_f_accumulate_512 f_acc512,
+ XXH3_f_scrambleAcc f_scramble)
+{
+ XXH_ASSERT(nbStripes <= nbStripesPerBlock); /* can handle max 1 scramble per invocation */
+ XXH_ASSERT(*nbStripesSoFarPtr < nbStripesPerBlock);
+ if (nbStripesPerBlock - *nbStripesSoFarPtr <= nbStripes) {
+ /* need a scrambling operation */
+ size_t const nbStripesToEndofBlock = nbStripesPerBlock - *nbStripesSoFarPtr;
+ size_t const nbStripesAfterBlock = nbStripes - nbStripesToEndofBlock;
+ XXH3_accumulate(acc, input, secret + nbStripesSoFarPtr[0] * XXH_SECRET_CONSUME_RATE, nbStripesToEndofBlock, f_acc512);
+ f_scramble(acc, secret + secretLimit);
+ XXH3_accumulate(acc, input + nbStripesToEndofBlock * XXH_STRIPE_LEN, secret, nbStripesAfterBlock, f_acc512);
+ *nbStripesSoFarPtr = nbStripesAfterBlock;
+ } else {
+ XXH3_accumulate(acc, input, secret + nbStripesSoFarPtr[0] * XXH_SECRET_CONSUME_RATE, nbStripes, f_acc512);
+ *nbStripesSoFarPtr += nbStripes;
+ }
+}
+
+#ifndef XXH3_STREAM_USE_STACK
+# if XXH_SIZE_OPT <= 0 && !defined(__clang__) /* clang doesn't need additional stack space */
+# define XXH3_STREAM_USE_STACK 1
+# endif
+#endif
+/*
+ * Both XXH3_64bits_update and XXH3_128bits_update use this routine.
+ */
+XXH_FORCE_INLINE XXH_errorcode
+XXH3_update(XXH3_state_t* XXH_RESTRICT const state,
+ const xxh_u8* XXH_RESTRICT input, size_t len,
+ XXH3_f_accumulate_512 f_acc512,
+ XXH3_f_scrambleAcc f_scramble)
+{
+ if (input==NULL) {
+ XXH_ASSERT(len == 0);
+ return XXH_OK;
+ }
+
+ XXH_ASSERT(state != NULL);
+ { const xxh_u8* const bEnd = input + len;
+ const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret;
+#if defined(XXH3_STREAM_USE_STACK) && XXH3_STREAM_USE_STACK >= 1
+ /* For some reason, gcc and MSVC seem to suffer greatly
+ * when operating accumulators directly into state.
+ * Operating into stack space seems to enable proper optimization.
+ * clang, on the other hand, doesn't seem to need this trick */
+ XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64 acc[8]; memcpy(acc, state->acc, sizeof(acc));
+#else
+ xxh_u64* XXH_RESTRICT const acc = state->acc;
+#endif
+ state->totalLen += len;
+ XXH_ASSERT(state->bufferedSize <= XXH3_INTERNALBUFFER_SIZE);
+
+ /* small input : just fill in tmp buffer */
+ if (state->bufferedSize + len <= XXH3_INTERNALBUFFER_SIZE) {
+ XXH_memcpy(state->buffer + state->bufferedSize, input, len);
+ state->bufferedSize += (XXH32_hash_t)len;
+ return XXH_OK;
+ }
+
+ /* total input is now > XXH3_INTERNALBUFFER_SIZE */
+ #define XXH3_INTERNALBUFFER_STRIPES (XXH3_INTERNALBUFFER_SIZE / XXH_STRIPE_LEN)
+ XXH_STATIC_ASSERT(XXH3_INTERNALBUFFER_SIZE % XXH_STRIPE_LEN == 0); /* clean multiple */
+
+ /*
+ * Internal buffer is partially filled (always, except at beginning)
+ * Complete it, then consume it.
+ */
+ if (state->bufferedSize) {
+ size_t const loadSize = XXH3_INTERNALBUFFER_SIZE - state->bufferedSize;
+ XXH_memcpy(state->buffer + state->bufferedSize, input, loadSize);
+ input += loadSize;
+ XXH3_consumeStripes(acc,
+ &state->nbStripesSoFar, state->nbStripesPerBlock,
+ state->buffer, XXH3_INTERNALBUFFER_STRIPES,
+ secret, state->secretLimit,
+ f_acc512, f_scramble);
+ state->bufferedSize = 0;
+ }
+ XXH_ASSERT(input < bEnd);
+
+ /* large input to consume : ingest per full block */
+ if ((size_t)(bEnd - input) > state->nbStripesPerBlock * XXH_STRIPE_LEN) {
+ size_t nbStripes = (size_t)(bEnd - 1 - input) / XXH_STRIPE_LEN;
+ XXH_ASSERT(state->nbStripesPerBlock >= state->nbStripesSoFar);
+ /* join to current block's end */
+ { size_t const nbStripesToEnd = state->nbStripesPerBlock - state->nbStripesSoFar;
+ XXH_ASSERT(nbStripesToEnd <= nbStripes);
+ XXH3_accumulate(acc, input, secret + state->nbStripesSoFar * XXH_SECRET_CONSUME_RATE, nbStripesToEnd, f_acc512);
+ f_scramble(acc, secret + state->secretLimit);
+ state->nbStripesSoFar = 0;
+ input += nbStripesToEnd * XXH_STRIPE_LEN;
+ nbStripes -= nbStripesToEnd;
+ }
+ /* consume per entire blocks */
+ while(nbStripes >= state->nbStripesPerBlock) {
+ XXH3_accumulate(acc, input, secret, state->nbStripesPerBlock, f_acc512);
+ f_scramble(acc, secret + state->secretLimit);
+ input += state->nbStripesPerBlock * XXH_STRIPE_LEN;
+ nbStripes -= state->nbStripesPerBlock;
+ }
+ /* consume last partial block */
+ XXH3_accumulate(acc, input, secret, nbStripes, f_acc512);
+ input += nbStripes * XXH_STRIPE_LEN;
+ XXH_ASSERT(input < bEnd); /* at least some bytes left */
+ state->nbStripesSoFar = nbStripes;
+ /* buffer predecessor of last partial stripe */
+ XXH_memcpy(state->buffer + sizeof(state->buffer) - XXH_STRIPE_LEN, input - XXH_STRIPE_LEN, XXH_STRIPE_LEN);
+ XXH_ASSERT(bEnd - input <= XXH_STRIPE_LEN);
+ } else {
+ /* content to consume <= block size */
+ /* Consume input by a multiple of internal buffer size */
+ if (bEnd - input > XXH3_INTERNALBUFFER_SIZE) {
+ const xxh_u8* const limit = bEnd - XXH3_INTERNALBUFFER_SIZE;
+ do {
+ XXH3_consumeStripes(acc,
+ &state->nbStripesSoFar, state->nbStripesPerBlock,
+ input, XXH3_INTERNALBUFFER_STRIPES,
+ secret, state->secretLimit,
+ f_acc512, f_scramble);
+ input += XXH3_INTERNALBUFFER_SIZE;
+ } while (input<limit);
+ /* buffer predecessor of last partial stripe */
+ XXH_memcpy(state->buffer + sizeof(state->buffer) - XXH_STRIPE_LEN, input - XXH_STRIPE_LEN, XXH_STRIPE_LEN);
+ }
+ }
+
+ /* Some remaining input (always) : buffer it */
+ XXH_ASSERT(input < bEnd);
+ XXH_ASSERT(bEnd - input <= XXH3_INTERNALBUFFER_SIZE);
+ XXH_ASSERT(state->bufferedSize == 0);
+ XXH_memcpy(state->buffer, input, (size_t)(bEnd-input));
+ state->bufferedSize = (XXH32_hash_t)(bEnd-input);
+#if defined(XXH3_STREAM_USE_STACK) && XXH3_STREAM_USE_STACK >= 1
+ /* save stack accumulators into state */
+ memcpy(state->acc, acc, sizeof(acc));
+#endif
+ }
+
+ return XXH_OK;
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_64bits_update(XXH3_state_t* state, const void* input, size_t len)
+{
+ return XXH3_update(state, (const xxh_u8*)input, len,
+ XXH3_accumulate_512, XXH3_scrambleAcc);
+}
+
+
+XXH_FORCE_INLINE void
+XXH3_digest_long (XXH64_hash_t* acc,
+ const XXH3_state_t* state,
+ const unsigned char* secret)
+{
+ /*
+ * Digest on a local copy. This way, the state remains unaltered, and it can
+ * continue ingesting more input afterwards.
+ */
+ XXH_memcpy(acc, state->acc, sizeof(state->acc));
+ if (state->bufferedSize >= XXH_STRIPE_LEN) {
+ size_t const nbStripes = (state->bufferedSize - 1) / XXH_STRIPE_LEN;
+ size_t nbStripesSoFar = state->nbStripesSoFar;
+ XXH3_consumeStripes(acc,
+ &nbStripesSoFar, state->nbStripesPerBlock,
+ state->buffer, nbStripes,
+ secret, state->secretLimit,
+ XXH3_accumulate_512, XXH3_scrambleAcc);
+ /* last stripe */
+ XXH3_accumulate_512(acc,
+ state->buffer + state->bufferedSize - XXH_STRIPE_LEN,
+ secret + state->secretLimit - XXH_SECRET_LASTACC_START);
+ } else { /* bufferedSize < XXH_STRIPE_LEN */
+ xxh_u8 lastStripe[XXH_STRIPE_LEN];
+ size_t const catchupSize = XXH_STRIPE_LEN - state->bufferedSize;
+ XXH_ASSERT(state->bufferedSize > 0); /* there is always some input buffered */
+ XXH_memcpy(lastStripe, state->buffer + sizeof(state->buffer) - catchupSize, catchupSize);
+ XXH_memcpy(lastStripe + catchupSize, state->buffer, state->bufferedSize);
+ XXH3_accumulate_512(acc,
+ lastStripe,
+ secret + state->secretLimit - XXH_SECRET_LASTACC_START);
+ }
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH64_hash_t XXH3_64bits_digest (const XXH3_state_t* state)
+{
+ const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret;
+ if (state->totalLen > XXH3_MIDSIZE_MAX) {
+ XXH_ALIGN(XXH_ACC_ALIGN) XXH64_hash_t acc[XXH_ACC_NB];
+ XXH3_digest_long(acc, state, secret);
+ return XXH3_mergeAccs(acc,
+ secret + XXH_SECRET_MERGEACCS_START,
+ (xxh_u64)state->totalLen * XXH_PRIME64_1);
+ }
+ /* totalLen <= XXH3_MIDSIZE_MAX: digesting a short input */
+ if (state->useSeed)
+ return XXH3_64bits_withSeed(state->buffer, (size_t)state->totalLen, state->seed);
+ return XXH3_64bits_withSecret(state->buffer, (size_t)(state->totalLen),
+ secret, state->secretLimit + XXH_STRIPE_LEN);
+}
+#endif /* !XXH_NO_STREAM */
+
+
+/* ==========================================
+ * XXH3 128 bits (a.k.a XXH128)
+ * ==========================================
+ * XXH3's 128-bit variant has better mixing and strength than the 64-bit variant,
+ * even without counting the significantly larger output size.
+ *
+ * For example, extra steps are taken to avoid the seed-dependent collisions
+ * in 17-240 byte inputs (See XXH3_mix16B and XXH128_mix32B).
+ *
+ * This strength naturally comes at the cost of some speed, especially on short
+ * lengths. Note that longer hashes are about as fast as the 64-bit version
+ * due to it using only a slight modification of the 64-bit loop.
+ *
+ * XXH128 is also more oriented towards 64-bit machines. It is still extremely
+ * fast for a _128-bit_ hash on 32-bit (it usually clears XXH64).
+ */
+
+XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t
+XXH3_len_1to3_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed)
+{
+ /* A doubled version of 1to3_64b with different constants. */
+ XXH_ASSERT(input != NULL);
+ XXH_ASSERT(1 <= len && len <= 3);
+ XXH_ASSERT(secret != NULL);
+ /*
+ * len = 1: combinedl = { input[0], 0x01, input[0], input[0] }
+ * len = 2: combinedl = { input[1], 0x02, input[0], input[1] }
+ * len = 3: combinedl = { input[2], 0x03, input[0], input[1] }
+ */
+ { xxh_u8 const c1 = input[0];
+ xxh_u8 const c2 = input[len >> 1];
+ xxh_u8 const c3 = input[len - 1];
+ xxh_u32 const combinedl = ((xxh_u32)c1 <<16) | ((xxh_u32)c2 << 24)
+ | ((xxh_u32)c3 << 0) | ((xxh_u32)len << 8);
+ xxh_u32 const combinedh = XXH_rotl32(XXH_swap32(combinedl), 13);
+ xxh_u64 const bitflipl = (XXH_readLE32(secret) ^ XXH_readLE32(secret+4)) + seed;
+ xxh_u64 const bitfliph = (XXH_readLE32(secret+8) ^ XXH_readLE32(secret+12)) - seed;
+ xxh_u64 const keyed_lo = (xxh_u64)combinedl ^ bitflipl;
+ xxh_u64 const keyed_hi = (xxh_u64)combinedh ^ bitfliph;
+ XXH128_hash_t h128;
+ h128.low64 = XXH64_avalanche(keyed_lo);
+ h128.high64 = XXH64_avalanche(keyed_hi);
+ return h128;
+ }
+}
+
+XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t
+XXH3_len_4to8_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed)
+{
+ XXH_ASSERT(input != NULL);
+ XXH_ASSERT(secret != NULL);
+ XXH_ASSERT(4 <= len && len <= 8);
+ seed ^= (xxh_u64)XXH_swap32((xxh_u32)seed) << 32;
+ { xxh_u32 const input_lo = XXH_readLE32(input);
+ xxh_u32 const input_hi = XXH_readLE32(input + len - 4);
+ xxh_u64 const input_64 = input_lo + ((xxh_u64)input_hi << 32);
+ xxh_u64 const bitflip = (XXH_readLE64(secret+16) ^ XXH_readLE64(secret+24)) + seed;
+ xxh_u64 const keyed = input_64 ^ bitflip;
+
+ /* Shift len to the left to ensure it is even, this avoids even multiplies. */
+ XXH128_hash_t m128 = XXH_mult64to128(keyed, XXH_PRIME64_1 + (len << 2));
+
+ m128.high64 += (m128.low64 << 1);
+ m128.low64 ^= (m128.high64 >> 3);
+
+ m128.low64 = XXH_xorshift64(m128.low64, 35);
+ m128.low64 *= 0x9FB21C651E98DF25ULL;
+ m128.low64 = XXH_xorshift64(m128.low64, 28);
+ m128.high64 = XXH3_avalanche(m128.high64);
+ return m128;
+ }
+}
+
+XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t
+XXH3_len_9to16_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed)
+{
+ XXH_ASSERT(input != NULL);
+ XXH_ASSERT(secret != NULL);
+ XXH_ASSERT(9 <= len && len <= 16);
+ { xxh_u64 const bitflipl = (XXH_readLE64(secret+32) ^ XXH_readLE64(secret+40)) - seed;
+ xxh_u64 const bitfliph = (XXH_readLE64(secret+48) ^ XXH_readLE64(secret+56)) + seed;
+ xxh_u64 const input_lo = XXH_readLE64(input);
+ xxh_u64 input_hi = XXH_readLE64(input + len - 8);
+ XXH128_hash_t m128 = XXH_mult64to128(input_lo ^ input_hi ^ bitflipl, XXH_PRIME64_1);
+ /*
+ * Put len in the middle of m128 to ensure that the length gets mixed to
+ * both the low and high bits in the 128x64 multiply below.
+ */
+ m128.low64 += (xxh_u64)(len - 1) << 54;
+ input_hi ^= bitfliph;
+ /*
+ * Add the high 32 bits of input_hi to the high 32 bits of m128, then
+ * add the long product of the low 32 bits of input_hi and XXH_PRIME32_2 to
+ * the high 64 bits of m128.
+ *
+ * The best approach to this operation is different on 32-bit and 64-bit.
+ */
+ if (sizeof(void *) < sizeof(xxh_u64)) { /* 32-bit */
+ /*
+ * 32-bit optimized version, which is more readable.
+ *
+ * On 32-bit, it removes an ADC and delays a dependency between the two
+ * halves of m128.high64, but it generates an extra mask on 64-bit.
+ */
+ m128.high64 += (input_hi & 0xFFFFFFFF00000000ULL) + XXH_mult32to64((xxh_u32)input_hi, XXH_PRIME32_2);
+ } else {
+ /*
+ * 64-bit optimized (albeit more confusing) version.
+ *
+ * Uses some properties of addition and multiplication to remove the mask:
+ *
+ * Let:
+ * a = input_hi.lo = (input_hi & 0x00000000FFFFFFFF)
+ * b = input_hi.hi = (input_hi & 0xFFFFFFFF00000000)
+ * c = XXH_PRIME32_2
+ *
+ * a + (b * c)
+ * Inverse Property: x + y - x == y
+ * a + (b * (1 + c - 1))
+ * Distributive Property: x * (y + z) == (x * y) + (x * z)
+ * a + (b * 1) + (b * (c - 1))
+ * Identity Property: x * 1 == x
+ * a + b + (b * (c - 1))
+ *
+ * Substitute a, b, and c:
+ * input_hi.hi + input_hi.lo + ((xxh_u64)input_hi.lo * (XXH_PRIME32_2 - 1))
+ *
+ * Since input_hi.hi + input_hi.lo == input_hi, we get this:
+ * input_hi + ((xxh_u64)input_hi.lo * (XXH_PRIME32_2 - 1))
+ */
+ m128.high64 += input_hi + XXH_mult32to64((xxh_u32)input_hi, XXH_PRIME32_2 - 1);
+ }
+ /* m128 ^= XXH_swap64(m128 >> 64); */
+ m128.low64 ^= XXH_swap64(m128.high64);
+
+ { /* 128x64 multiply: h128 = m128 * XXH_PRIME64_2; */
+ XXH128_hash_t h128 = XXH_mult64to128(m128.low64, XXH_PRIME64_2);
+ h128.high64 += m128.high64 * XXH_PRIME64_2;
+
+ h128.low64 = XXH3_avalanche(h128.low64);
+ h128.high64 = XXH3_avalanche(h128.high64);
+ return h128;
+ } }
+}
+
+/*
+ * Assumption: `secret` size is >= XXH3_SECRET_SIZE_MIN
+ */
+XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t
+XXH3_len_0to16_128b(const xxh_u8* input, size_t len, const xxh_u8* secret, XXH64_hash_t seed)
+{
+ XXH_ASSERT(len <= 16);
+ { if (len > 8) return XXH3_len_9to16_128b(input, len, secret, seed);
+ if (len >= 4) return XXH3_len_4to8_128b(input, len, secret, seed);
+ if (len) return XXH3_len_1to3_128b(input, len, secret, seed);
+ { XXH128_hash_t h128;
+ xxh_u64 const bitflipl = XXH_readLE64(secret+64) ^ XXH_readLE64(secret+72);
+ xxh_u64 const bitfliph = XXH_readLE64(secret+80) ^ XXH_readLE64(secret+88);
+ h128.low64 = XXH64_avalanche(seed ^ bitflipl);
+ h128.high64 = XXH64_avalanche( seed ^ bitfliph);
+ return h128;
+ } }
+}
+
+/*
+ * A bit slower than XXH3_mix16B, but handles multiply by zero better.
+ */
+XXH_FORCE_INLINE XXH128_hash_t
+XXH128_mix32B(XXH128_hash_t acc, const xxh_u8* input_1, const xxh_u8* input_2,
+ const xxh_u8* secret, XXH64_hash_t seed)
+{
+ acc.low64 += XXH3_mix16B (input_1, secret+0, seed);
+ acc.low64 ^= XXH_readLE64(input_2) + XXH_readLE64(input_2 + 8);
+ acc.high64 += XXH3_mix16B (input_2, secret+16, seed);
+ acc.high64 ^= XXH_readLE64(input_1) + XXH_readLE64(input_1 + 8);
+ return acc;
+}
+
+
+XXH_FORCE_INLINE XXH_PUREF XXH128_hash_t
+XXH3_len_17to128_128b(const xxh_u8* XXH_RESTRICT input, size_t len,
+ const xxh_u8* XXH_RESTRICT secret, size_t secretSize,
+ XXH64_hash_t seed)
+{
+ XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize;
+ XXH_ASSERT(16 < len && len <= 128);
+
+ { XXH128_hash_t acc;
+ acc.low64 = len * XXH_PRIME64_1;
+ acc.high64 = 0;
+
+#if XXH_SIZE_OPT >= 1
+ {
+ /* Smaller, but slightly slower. */
+ size_t i = (len - 1) / 32;
+ do {
+ acc = XXH128_mix32B(acc, input+16*i, input+len-16*(i+1), secret+32*i, seed);
+ } while (i-- != 0);
+ }
+#else
+ if (len > 32) {
+ if (len > 64) {
+ if (len > 96) {
+ acc = XXH128_mix32B(acc, input+48, input+len-64, secret+96, seed);
+ }
+ acc = XXH128_mix32B(acc, input+32, input+len-48, secret+64, seed);
+ }
+ acc = XXH128_mix32B(acc, input+16, input+len-32, secret+32, seed);
+ }
+ acc = XXH128_mix32B(acc, input, input+len-16, secret, seed);
+#endif
+ { XXH128_hash_t h128;
+ h128.low64 = acc.low64 + acc.high64;
+ h128.high64 = (acc.low64 * XXH_PRIME64_1)
+ + (acc.high64 * XXH_PRIME64_4)
+ + ((len - seed) * XXH_PRIME64_2);
+ h128.low64 = XXH3_avalanche(h128.low64);
+ h128.high64 = (XXH64_hash_t)0 - XXH3_avalanche(h128.high64);
+ return h128;
+ }
+ }
+}
+
+XXH_NO_INLINE XXH_PUREF XXH128_hash_t
+XXH3_len_129to240_128b(const xxh_u8* XXH_RESTRICT input, size_t len,
+ const xxh_u8* XXH_RESTRICT secret, size_t secretSize,
+ XXH64_hash_t seed)
+{
+ XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN); (void)secretSize;
+ XXH_ASSERT(128 < len && len <= XXH3_MIDSIZE_MAX);
+
+ { XXH128_hash_t acc;
+ int const nbRounds = (int)len / 32;
+ int i;
+ acc.low64 = len * XXH_PRIME64_1;
+ acc.high64 = 0;
+ for (i=0; i<4; i++) {
+ acc = XXH128_mix32B(acc,
+ input + (32 * i),
+ input + (32 * i) + 16,
+ secret + (32 * i),
+ seed);
+ }
+ acc.low64 = XXH3_avalanche(acc.low64);
+ acc.high64 = XXH3_avalanche(acc.high64);
+ XXH_ASSERT(nbRounds >= 4);
+ for (i=4 ; i < nbRounds; i++) {
+ acc = XXH128_mix32B(acc,
+ input + (32 * i),
+ input + (32 * i) + 16,
+ secret + XXH3_MIDSIZE_STARTOFFSET + (32 * (i - 4)),
+ seed);
+ }
+ /* last bytes */
+ acc = XXH128_mix32B(acc,
+ input + len - 16,
+ input + len - 32,
+ secret + XXH3_SECRET_SIZE_MIN - XXH3_MIDSIZE_LASTOFFSET - 16,
+ 0ULL - seed);
+
+ { XXH128_hash_t h128;
+ h128.low64 = acc.low64 + acc.high64;
+ h128.high64 = (acc.low64 * XXH_PRIME64_1)
+ + (acc.high64 * XXH_PRIME64_4)
+ + ((len - seed) * XXH_PRIME64_2);
+ h128.low64 = XXH3_avalanche(h128.low64);
+ h128.high64 = (XXH64_hash_t)0 - XXH3_avalanche(h128.high64);
+ return h128;
+ }
+ }
+}
+
+XXH_FORCE_INLINE XXH128_hash_t
+XXH3_hashLong_128b_internal(const void* XXH_RESTRICT input, size_t len,
+ const xxh_u8* XXH_RESTRICT secret, size_t secretSize,
+ XXH3_f_accumulate_512 f_acc512,
+ XXH3_f_scrambleAcc f_scramble)
+{
+ XXH_ALIGN(XXH_ACC_ALIGN) xxh_u64 acc[XXH_ACC_NB] = XXH3_INIT_ACC;
+
+ XXH3_hashLong_internal_loop(acc, (const xxh_u8*)input, len, secret, secretSize, f_acc512, f_scramble);
+
+ /* converge into final hash */
+ XXH_STATIC_ASSERT(sizeof(acc) == 64);
+ XXH_ASSERT(secretSize >= sizeof(acc) + XXH_SECRET_MERGEACCS_START);
+ { XXH128_hash_t h128;
+ h128.low64 = XXH3_mergeAccs(acc,
+ secret + XXH_SECRET_MERGEACCS_START,
+ (xxh_u64)len * XXH_PRIME64_1);
+ h128.high64 = XXH3_mergeAccs(acc,
+ secret + secretSize
+ - sizeof(acc) - XXH_SECRET_MERGEACCS_START,
+ ~((xxh_u64)len * XXH_PRIME64_2));
+ return h128;
+ }
+}
+
+/*
+ * It's important for performance that XXH3_hashLong() is not inlined.
+ */
+XXH_NO_INLINE XXH_PUREF XXH128_hash_t
+XXH3_hashLong_128b_default(const void* XXH_RESTRICT input, size_t len,
+ XXH64_hash_t seed64,
+ const void* XXH_RESTRICT secret, size_t secretLen)
+{
+ (void)seed64; (void)secret; (void)secretLen;
+ return XXH3_hashLong_128b_internal(input, len, XXH3_kSecret, sizeof(XXH3_kSecret),
+ XXH3_accumulate_512, XXH3_scrambleAcc);
+}
+
+/*
+ * It's important for performance to pass @p secretLen (when it's static)
+ * to the compiler, so that it can properly optimize the vectorized loop.
+ */
+XXH_FORCE_INLINE XXH128_hash_t
+XXH3_hashLong_128b_withSecret(const void* XXH_RESTRICT input, size_t len,
+ XXH64_hash_t seed64,
+ const void* XXH_RESTRICT secret, size_t secretLen)
+{
+ (void)seed64;
+ return XXH3_hashLong_128b_internal(input, len, (const xxh_u8*)secret, secretLen,
+ XXH3_accumulate_512, XXH3_scrambleAcc);
+}
+
+XXH_FORCE_INLINE XXH128_hash_t
+XXH3_hashLong_128b_withSeed_internal(const void* XXH_RESTRICT input, size_t len,
+ XXH64_hash_t seed64,
+ XXH3_f_accumulate_512 f_acc512,
+ XXH3_f_scrambleAcc f_scramble,
+ XXH3_f_initCustomSecret f_initSec)
+{
+ if (seed64 == 0)
+ return XXH3_hashLong_128b_internal(input, len,
+ XXH3_kSecret, sizeof(XXH3_kSecret),
+ f_acc512, f_scramble);
+ { XXH_ALIGN(XXH_SEC_ALIGN) xxh_u8 secret[XXH_SECRET_DEFAULT_SIZE];
+ f_initSec(secret, seed64);
+ return XXH3_hashLong_128b_internal(input, len, (const xxh_u8*)secret, sizeof(secret),
+ f_acc512, f_scramble);
+ }
+}
+
+/*
+ * It's important for performance that XXH3_hashLong is not inlined.
+ */
+XXH_NO_INLINE XXH128_hash_t
+XXH3_hashLong_128b_withSeed(const void* input, size_t len,
+ XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen)
+{
+ (void)secret; (void)secretLen;
+ return XXH3_hashLong_128b_withSeed_internal(input, len, seed64,
+ XXH3_accumulate_512, XXH3_scrambleAcc, XXH3_initCustomSecret);
+}
+
+typedef XXH128_hash_t (*XXH3_hashLong128_f)(const void* XXH_RESTRICT, size_t,
+ XXH64_hash_t, const void* XXH_RESTRICT, size_t);
+
+XXH_FORCE_INLINE XXH128_hash_t
+XXH3_128bits_internal(const void* input, size_t len,
+ XXH64_hash_t seed64, const void* XXH_RESTRICT secret, size_t secretLen,
+ XXH3_hashLong128_f f_hl128)
+{
+ XXH_ASSERT(secretLen >= XXH3_SECRET_SIZE_MIN);
+ /*
+ * If an action is to be taken if `secret` conditions are not respected,
+ * it should be done here.
+ * For now, it's a contract pre-condition.
+ * Adding a check and a branch here would cost performance at every hash.
+ */
+ if (len <= 16)
+ return XXH3_len_0to16_128b((const xxh_u8*)input, len, (const xxh_u8*)secret, seed64);
+ if (len <= 128)
+ return XXH3_len_17to128_128b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64);
+ if (len <= XXH3_MIDSIZE_MAX)
+ return XXH3_len_129to240_128b((const xxh_u8*)input, len, (const xxh_u8*)secret, secretLen, seed64);
+ return f_hl128(input, len, seed64, secret, secretLen);
+}
+
+
+/* === Public XXH128 API === */
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH128_hash_t XXH3_128bits(const void* input, size_t len)
+{
+ return XXH3_128bits_internal(input, len, 0,
+ XXH3_kSecret, sizeof(XXH3_kSecret),
+ XXH3_hashLong_128b_default);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH128_hash_t
+XXH3_128bits_withSecret(const void* input, size_t len, const void* secret, size_t secretSize)
+{
+ return XXH3_128bits_internal(input, len, 0,
+ (const xxh_u8*)secret, secretSize,
+ XXH3_hashLong_128b_withSecret);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH128_hash_t
+XXH3_128bits_withSeed(const void* input, size_t len, XXH64_hash_t seed)
+{
+ return XXH3_128bits_internal(input, len, seed,
+ XXH3_kSecret, sizeof(XXH3_kSecret),
+ XXH3_hashLong_128b_withSeed);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH128_hash_t
+XXH3_128bits_withSecretandSeed(const void* input, size_t len, const void* secret, size_t secretSize, XXH64_hash_t seed)
+{
+ if (len <= XXH3_MIDSIZE_MAX)
+ return XXH3_128bits_internal(input, len, seed, XXH3_kSecret, sizeof(XXH3_kSecret), NULL);
+ return XXH3_hashLong_128b_withSecret(input, len, seed, secret, secretSize);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH128_hash_t
+XXH128(const void* input, size_t len, XXH64_hash_t seed)
+{
+ return XXH3_128bits_withSeed(input, len, seed);
+}
+
+
+/* === XXH3 128-bit streaming === */
+#ifndef XXH_NO_STREAM
+/*
+ * All initialization and update functions are identical to 64-bit streaming variant.
+ * The only difference is the finalization routine.
+ */
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_128bits_reset(XXH3_state_t* statePtr)
+{
+ return XXH3_64bits_reset(statePtr);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_128bits_reset_withSecret(XXH3_state_t* statePtr, const void* secret, size_t secretSize)
+{
+ return XXH3_64bits_reset_withSecret(statePtr, secret, secretSize);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_128bits_reset_withSeed(XXH3_state_t* statePtr, XXH64_hash_t seed)
+{
+ return XXH3_64bits_reset_withSeed(statePtr, seed);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_128bits_reset_withSecretandSeed(XXH3_state_t* statePtr, const void* secret, size_t secretSize, XXH64_hash_t seed)
+{
+ return XXH3_64bits_reset_withSecretandSeed(statePtr, secret, secretSize, seed);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_128bits_update(XXH3_state_t* state, const void* input, size_t len)
+{
+ return XXH3_update(state, (const xxh_u8*)input, len,
+ XXH3_accumulate_512, XXH3_scrambleAcc);
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH128_hash_t XXH3_128bits_digest (const XXH3_state_t* state)
+{
+ const unsigned char* const secret = (state->extSecret == NULL) ? state->customSecret : state->extSecret;
+ if (state->totalLen > XXH3_MIDSIZE_MAX) {
+ XXH_ALIGN(XXH_ACC_ALIGN) XXH64_hash_t acc[XXH_ACC_NB];
+ XXH3_digest_long(acc, state, secret);
+ XXH_ASSERT(state->secretLimit + XXH_STRIPE_LEN >= sizeof(acc) + XXH_SECRET_MERGEACCS_START);
+ { XXH128_hash_t h128;
+ h128.low64 = XXH3_mergeAccs(acc,
+ secret + XXH_SECRET_MERGEACCS_START,
+ (xxh_u64)state->totalLen * XXH_PRIME64_1);
+ h128.high64 = XXH3_mergeAccs(acc,
+ secret + state->secretLimit + XXH_STRIPE_LEN
+ - sizeof(acc) - XXH_SECRET_MERGEACCS_START,
+ ~((xxh_u64)state->totalLen * XXH_PRIME64_2));
+ return h128;
+ }
+ }
+ /* len <= XXH3_MIDSIZE_MAX : short code */
+ if (state->seed)
+ return XXH3_128bits_withSeed(state->buffer, (size_t)state->totalLen, state->seed);
+ return XXH3_128bits_withSecret(state->buffer, (size_t)(state->totalLen),
+ secret, state->secretLimit + XXH_STRIPE_LEN);
+}
+#endif /* !XXH_NO_STREAM */
+/* 128-bit utility functions */
+
+#include <string.h> /* memcmp, memcpy */
+
+/* return : 1 is equal, 0 if different */
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API int XXH128_isEqual(XXH128_hash_t h1, XXH128_hash_t h2)
+{
+ /* note : XXH128_hash_t is compact, it has no padding byte */
+ return !(memcmp(&h1, &h2, sizeof(h1)));
+}
+
+/* This prototype is compatible with stdlib's qsort().
+ * @return : >0 if *h128_1 > *h128_2
+ * <0 if *h128_1 < *h128_2
+ * =0 if *h128_1 == *h128_2 */
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API int XXH128_cmp(const void* h128_1, const void* h128_2)
+{
+ XXH128_hash_t const h1 = *(const XXH128_hash_t*)h128_1;
+ XXH128_hash_t const h2 = *(const XXH128_hash_t*)h128_2;
+ int const hcmp = (h1.high64 > h2.high64) - (h2.high64 > h1.high64);
+ /* note : bets that, in most cases, hash values are different */
+ if (hcmp) return hcmp;
+ return (h1.low64 > h2.low64) - (h2.low64 > h1.low64);
+}
+
+
+/*====== Canonical representation ======*/
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API void
+XXH128_canonicalFromHash(XXH128_canonical_t* dst, XXH128_hash_t hash)
+{
+ XXH_STATIC_ASSERT(sizeof(XXH128_canonical_t) == sizeof(XXH128_hash_t));
+ if (XXH_CPU_LITTLE_ENDIAN) {
+ hash.high64 = XXH_swap64(hash.high64);
+ hash.low64 = XXH_swap64(hash.low64);
+ }
+ XXH_memcpy(dst, &hash.high64, sizeof(hash.high64));
+ XXH_memcpy((char*)dst + sizeof(hash.high64), &hash.low64, sizeof(hash.low64));
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH128_hash_t
+XXH128_hashFromCanonical(const XXH128_canonical_t* src)
+{
+ XXH128_hash_t h;
+ h.high64 = XXH_readBE64(src);
+ h.low64 = XXH_readBE64(src->digest + 8);
+ return h;
+}
+
+
+
+/* ==========================================
+ * Secret generators
+ * ==========================================
+ */
+#define XXH_MIN(x, y) (((x) > (y)) ? (y) : (x))
+
+XXH_FORCE_INLINE void XXH3_combine16(void* dst, XXH128_hash_t h128)
+{
+ XXH_writeLE64( dst, XXH_readLE64(dst) ^ h128.low64 );
+ XXH_writeLE64( (char*)dst+8, XXH_readLE64((char*)dst+8) ^ h128.high64 );
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API XXH_errorcode
+XXH3_generateSecret(void* secretBuffer, size_t secretSize, const void* customSeed, size_t customSeedSize)
+{
+#if (XXH_DEBUGLEVEL >= 1)
+ XXH_ASSERT(secretBuffer != NULL);
+ XXH_ASSERT(secretSize >= XXH3_SECRET_SIZE_MIN);
+#else
+ /* production mode, assert() are disabled */
+ if (secretBuffer == NULL) return XXH_ERROR;
+ if (secretSize < XXH3_SECRET_SIZE_MIN) return XXH_ERROR;
+#endif
+
+ if (customSeedSize == 0) {
+ customSeed = XXH3_kSecret;
+ customSeedSize = XXH_SECRET_DEFAULT_SIZE;
+ }
+#if (XXH_DEBUGLEVEL >= 1)
+ XXH_ASSERT(customSeed != NULL);
+#else
+ if (customSeed == NULL) return XXH_ERROR;
+#endif
+
+ /* Fill secretBuffer with a copy of customSeed - repeat as needed */
+ { size_t pos = 0;
+ while (pos < secretSize) {
+ size_t const toCopy = XXH_MIN((secretSize - pos), customSeedSize);
+ memcpy((char*)secretBuffer + pos, customSeed, toCopy);
+ pos += toCopy;
+ } }
+
+ { size_t const nbSeg16 = secretSize / 16;
+ size_t n;
+ XXH128_canonical_t scrambler;
+ XXH128_canonicalFromHash(&scrambler, XXH128(customSeed, customSeedSize, 0));
+ for (n=0; n<nbSeg16; n++) {
+ XXH128_hash_t const h128 = XXH128(&scrambler, sizeof(scrambler), n);
+ XXH3_combine16((char*)secretBuffer + n*16, h128);
+ }
+ /* last segment */
+ XXH3_combine16((char*)secretBuffer + secretSize - 16, XXH128_hashFromCanonical(&scrambler));
+ }
+ return XXH_OK;
+}
+
+/*! @ingroup XXH3_family */
+XXH_PUBLIC_API void
+XXH3_generateSecret_fromSeed(void* secretBuffer, XXH64_hash_t seed)
+{
+ XXH_ALIGN(XXH_SEC_ALIGN) xxh_u8 secret[XXH_SECRET_DEFAULT_SIZE];
+ XXH3_initCustomSecret(secret, seed);
+ XXH_ASSERT(secretBuffer != NULL);
+ memcpy(secretBuffer, secret, XXH_SECRET_DEFAULT_SIZE);
+}
+
+
+
+/* Pop our optimization override from above */
+#if XXH_VECTOR == XXH_AVX2 /* AVX2 */ \
+ && defined(__GNUC__) && !defined(__clang__) /* GCC, not Clang */ \
+ && defined(__OPTIMIZE__) && XXH_SIZE_OPT <= 0 /* respect -O0 and -Os */
+# pragma GCC pop_options
+#endif
+
+#endif /* XXH_NO_LONG_LONG */
+
+#endif /* XXH_NO_XXH3 */
+
+/*!
+ * @}
+ */
+#endif /* XXH_IMPLEMENTATION */
+
+
+#if defined (__cplusplus)
+}
+#endif
diff --git a/src/time-extension-functions.cc b/src/time-extension-functions.cc
new file mode 100644
index 0000000..b92ec2a
--- /dev/null
+++ b/src/time-extension-functions.cc
@@ -0,0 +1,266 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file time-extension-functions.cc
+ */
+
+#include <string>
+#include <unordered_map>
+
+#include <string.h>
+
+#include "base/date_time_scanner.hh"
+#include "base/humanize.time.hh"
+#include "base/lrucache.hpp"
+#include "config.h"
+#include "relative_time.hh"
+#include "sql_util.hh"
+#include "vtab_module.hh"
+
+static nonstd::optional<text_auto_buffer>
+timeslice(sqlite3_value* time_in, nonstd::optional<const char*> slice_in_opt)
+{
+ thread_local date_time_scanner dts;
+ thread_local struct {
+ std::string c_slice_str;
+ relative_time c_rel_time;
+ } cache;
+ const auto slice_in
+ = string_fragment::from_c_str(slice_in_opt.value_or("15m"));
+
+ if (slice_in.empty()) {
+ throw sqlite_func_error("no time slice value given");
+ }
+
+ if (slice_in != cache.c_slice_str.c_str()) {
+ auto parse_res = relative_time::from_str(slice_in);
+ if (parse_res.isErr()) {
+ throw sqlite_func_error(
+ "unable to parse time slice value: {} -- {}",
+ slice_in,
+ parse_res.unwrapErr().pe_msg);
+ }
+
+ cache.c_rel_time = parse_res.unwrap();
+ if (cache.c_rel_time.empty()) {
+ throw sqlite_func_error("could not determine a time slice from: {}",
+ slice_in);
+ }
+
+ cache.c_slice_str = slice_in.to_string();
+ }
+
+ struct timeval tv;
+ struct exttm tm;
+
+ switch (sqlite3_value_type(time_in)) {
+ case SQLITE_BLOB:
+ case SQLITE3_TEXT: {
+ const char* time_in_str
+ = reinterpret_cast<const char*>(sqlite3_value_text(time_in));
+
+ if (dts.scan(
+ time_in_str, strlen(time_in_str), nullptr, &tm, tv, false)
+ == nullptr)
+ {
+ dts.unlock();
+ if (dts.scan(time_in_str,
+ strlen(time_in_str),
+ nullptr,
+ &tm,
+ tv,
+ false)
+ == nullptr)
+ {
+ throw sqlite_func_error("unable to parse time value -- {}",
+ time_in_str);
+ }
+ }
+ break;
+ }
+ case SQLITE_INTEGER: {
+ auto msecs
+ = std::chrono::milliseconds(sqlite3_value_int64(time_in));
+
+ tv.tv_sec = std::chrono::duration_cast<std::chrono::seconds>(msecs)
+ .count();
+ tm.et_tm = *gmtime(&tv.tv_sec);
+ tm.et_nsec = std::chrono::duration_cast<std::chrono::nanoseconds>(
+ msecs % 1000)
+ .count();
+ break;
+ }
+ case SQLITE_FLOAT: {
+ auto secs = sqlite3_value_double(time_in);
+ double integ;
+ auto fract = modf(secs, &integ);
+
+ tv.tv_sec = integ;
+ tm.et_tm = *gmtime(&tv.tv_sec);
+ tm.et_nsec = floor(fract * 1000000000.0);
+ break;
+ }
+ case SQLITE_NULL: {
+ return nonstd::nullopt;
+ }
+ }
+
+ auto win_start_opt = cache.c_rel_time.window_start(tm);
+
+ if (!win_start_opt) {
+ return nonstd::nullopt;
+ }
+
+ auto win_start = *win_start_opt;
+ auto ts = auto_buffer::alloc(64);
+ auto actual_length
+ = sql_strftime(ts.in(), ts.size(), win_start.to_timeval());
+
+ ts.resize(actual_length);
+ return text_auto_buffer{std::move(ts)};
+}
+
+static nonstd::optional<double>
+sql_timediff(string_fragment time1, string_fragment time2)
+{
+ struct timeval tv1, tv2, retval;
+ date_time_scanner dts1, dts2;
+ auto parse_res1 = relative_time::from_str(time1);
+
+ if (parse_res1.isOk()) {
+ tv1 = parse_res1.unwrap().adjust_now().to_timeval();
+ } else if (!dts1.convert_to_timeval(
+ time1.data(), time1.length(), nullptr, tv1))
+ {
+ return nonstd::nullopt;
+ }
+
+ auto parse_res2 = relative_time::from_str(time2);
+ if (parse_res2.isOk()) {
+ tv2 = parse_res2.unwrap().adjust_now().to_timeval();
+ } else if (!dts2.convert_to_timeval(
+ time2.data(), time2.length(), nullptr, tv2))
+ {
+ return nonstd::nullopt;
+ }
+
+ timersub(&tv1, &tv2, &retval);
+
+ return (double) retval.tv_sec + (double) retval.tv_usec / 1000000.0;
+}
+
+static std::string
+sql_humanize_duration(double value)
+{
+ auto secs = std::trunc(value);
+ auto usecs = (value - secs) * 1000000.0;
+ timeval tv;
+
+ tv.tv_sec = secs;
+ tv.tv_usec = usecs;
+ return humanize::time::duration::from_tv(tv).to_string();
+}
+
+int
+time_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs)
+{
+ static struct FuncDef time_funcs[] = {
+ sqlite_func_adapter<decltype(&timeslice), timeslice>::builder(
+ help_text(
+ "timeslice",
+ "Return the start of the slice of time that the given "
+ "timestamp falls in. "
+ "If the time falls outside of the slice, NULL is returned.")
+ .sql_function()
+ .with_parameter(
+ {"time", "The timestamp to get the time slice for."})
+ .with_parameter({"slice", "The size of the time slices"})
+ .with_tags({"datetime"})
+ .with_example({
+ "To get the timestamp rounded down to the start of the "
+ "ten minute slice",
+ "SELECT timeslice('2017-01-01T05:05:00', '10m')",
+ })
+ .with_example({
+ "To group log messages into five minute buckets and count "
+ "them",
+ "SELECT timeslice(log_time_msecs, '5m') AS slice, "
+ "count(1)\n FROM lnav_example_log GROUP BY slice",
+ })
+ .with_example({
+ "To group log messages by those before 4:30am and after",
+ "SELECT timeslice(log_time_msecs, 'before 4:30am') AS "
+ "slice, count(1) FROM lnav_example_log GROUP BY slice",
+ })),
+
+ sqlite_func_adapter<decltype(&sql_timediff), sql_timediff>::builder(
+ help_text(
+ "timediff",
+ "Compute the difference between two timestamps in seconds")
+ .sql_function()
+ .with_parameter({"time1", "The first timestamp"})
+ .with_parameter(
+ {"time2", "The timestamp to subtract from the first"})
+ .with_tags({"datetime"})
+ .with_example({
+ "To get the difference between two timestamps",
+ "SELECT timediff('2017-02-03T04:05:06', "
+ "'2017-02-03T04:05:00')",
+ })
+ .with_example({
+ "To get the difference between relative timestamps",
+ "SELECT timediff('today', 'yesterday')",
+ })),
+
+ sqlite_func_adapter<decltype(&sql_humanize_duration),
+ sql_humanize_duration>::
+ builder(
+ help_text("humanize_duration",
+ "Format the given seconds value as an abbreviated "
+ "duration string")
+ .sql_function()
+ .with_parameter({"secs", "The duration in seconds"})
+ .with_tags({"datetime", "string"})
+ .with_example({
+ "To format a duration",
+ "SELECT humanize_duration(15 * 60)",
+ })
+ .with_example({
+ "To format a sub-second value",
+ "SELECT humanize_duration(1.5)",
+ })),
+
+ {nullptr},
+ };
+
+ *basic_funcs = time_funcs;
+ *agg_funcs = nullptr;
+
+ return SQLITE_OK;
+}
diff --git a/src/time_T.hh b/src/time_T.hh
new file mode 100644
index 0000000..c98f009
--- /dev/null
+++ b/src/time_T.hh
@@ -0,0 +1,50 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef time_t_hh
+#define time_t_hh
+
+#include <stdio.h>
+#include <sys/time.h>
+#include <time.h>
+
+#define timeit(_block) \
+ { \
+ struct timeval _st, _en, _diff; \
+\
+ gettimeofday(&_st, NULL); \
+ { \
+ _block; \
+ } \
+ gettimeofday(&_en, NULL); \
+ timersub(&_en, &_st, &_diff); \
+ fprintf(stderr, "%s %d.%06d\n", #_block, _diff.tv_sec, _diff.tv_usec); \
+ fflush(stderr); \
+ }
+#endif
diff --git a/src/time_formats.am b/src/time_formats.am
new file mode 100644
index 0000000..eb882e4
--- /dev/null
+++ b/src/time_formats.am
@@ -0,0 +1,63 @@
+
+TIME_FORMATS = \
+ "@%@" \
+ "%Y-%m-%d %H:%M:%S" \
+ "%Y-%m-%d %H:%M:%S%z" \
+ "%Y-%m-%d %H:%M:%S %z" \
+ "%Y-%m-%d %H:%M" \
+ "%Y-%m-%dT%H:%M:%S.%f%z" \
+ "%y-%m-%dT%H:%M:%S.%f%z" \
+ "%Y-%m-%dT%H:%M:%SZ" \
+ "%Y-%m-%dT%H:%M:%S%z" \
+ "%Y-%m-%dT%H:%M:%S" \
+ "%Y-%m-%dT%H:%M:%S%z" \
+ "%Y-%m-%dT%H:%M" \
+ "%Y/%m/%d %H:%M:%S" \
+ "%Y/%m/%d %H:%M:%S %z" \
+ "%Y/%m/%d %H:%M:%S%z" \
+ "%Y/%m/%d %H:%M" \
+ "%Y %b %d %a %H:%M:%S.%L" \
+ "%Y %b %d %H:%M:%S.%L" \
+ "%Y %b %d %H:%M:%S" \
+ "%a %b %d %H:%M:%S %Y" \
+ "%a %b %d %H:%M:%S.%f %Y" \
+ "%a %b %d %H:%M:%S %Z %Y" \
+ "%a %b %d %H:%M:%S " \
+ "%a %b %d %H:%M:%S.%L " \
+ "%a %b %d %H:%M " \
+ "%a %b %e %H:%M:%S %Z %Y" \
+ "%d/%b/%Y:%H:%M:%S +0000" \
+ "%d/%b/%Y:%H:%M:%S %z" \
+ "%d-%b-%Y %H:%M:%S %z" \
+ "%d-%b-%Y %H:%M:%S %Z" \
+ "%d %b %Y %H:%M:%S" \
+ "%d %b %Y %H:%M:%S.%L" \
+ "%d %b %Y %H:%M:%S,%L" \
+ "%d %b %Y %H:%M" \
+ "%b %d %H:%M:%S" \
+ "%b %d %k:%M:%S" \
+ "%b %d %l:%M:%S" \
+ "%b %d %l:%M" \
+ "%b %e, %Y %l:%M:%S %p" \
+ "%b %d, %Y %l:%M:%S %p" \
+ "%m/%d/%y %H:%M:%S" \
+ "%m/%d/%Y %I:%M:%S:%L %p %Z" \
+ "%m/%d/%Y %I:%M:%S %p %Z" \
+ "%m/%d/%Y %l:%M:%S %p %Z" \
+ "%m/%e/%Y %I:%M:%S %p" \
+ "%m/%e/%Y %l:%M:%S %p" \
+ "%m/%d/%Y %H:%M:%S" \
+ "%d/%b/%y %H:%M:%S" \
+ "%m%d %H:%M:%S" \
+ "%Y%m%d %H:%M:%S" \
+ "%Y%m%d.%H%M%S" \
+ "%H:%M:%S" \
+ "%H:%M:%S.%f" \
+ "%M:%S" \
+ "%m/%d %H:%M:%S" \
+ "%Y-%m-%d" \
+ "%Y-%m" \
+ "%Y/%m/%d" \
+ "%Y/%m" \
+ "%s.%f" \
+ $()
diff --git a/src/timer.cc b/src/timer.cc
new file mode 100644
index 0000000..45fdd80
--- /dev/null
+++ b/src/timer.cc
@@ -0,0 +1,121 @@
+/**
+ * Copyright (c) 2015, Suresh Sundriyal
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "timer.hh"
+
+#include "base/lnav_log.hh"
+#include "config.h"
+
+static const struct itimerval DISABLE_TV = {{0, 0}, {0, 0}};
+
+timer::error::error(int err) : e_err(err) {}
+
+timer::interrupt_timer::interrupt_timer(struct timeval t,
+ sighandler_t_ sighandler = SIG_IGN)
+ : new_handler(sighandler), new_val((struct itimerval){{0, 0}, t}),
+ old_val(DISABLE_TV), armed(false)
+{
+ memset(&this->old_handler, 0, sizeof(this->old_handler));
+}
+
+int
+timer::interrupt_timer::arm_timer()
+{
+ struct sigaction sa;
+
+ // Disable the interval timer before setting the handler and arming the
+ // interval timer or else we will have a race-condition where the timer
+ // might fire and the appropriate handler might not be set.
+ if (setitimer(ITIMER_REAL, &DISABLE_TV, &this->old_val) != 0) {
+ log_error("Unable to disable the timer: %s", strerror(errno));
+ return -1;
+ }
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = this->new_handler;
+ if (sigaction(SIGALRM, &sa, &this->old_handler) == -1) {
+ log_error("Unable to set the signal handler: %s", strerror(errno));
+ if (setitimer(ITIMER_REAL, &this->old_val, NULL) != 0) {
+ log_error("Unable to reset the interrupt timer: %s",
+ strerror(errno));
+ throw timer::error(errno);
+ }
+ return -1;
+ }
+
+ if (setitimer(ITIMER_REAL, &this->new_val, NULL) != 0) {
+ if (sigaction(SIGALRM, &this->old_handler, NULL) == -1) {
+ log_error("Unable to reset the signal handler: %s",
+ strerror(errno));
+ throw timer::error(errno);
+ }
+ log_error("Unable to set the timer: %s", strerror(errno));
+ return -1;
+ }
+ this->armed = true;
+ return 0;
+}
+
+bool
+timer::interrupt_timer::is_armed()
+{
+ return this->armed;
+}
+
+void
+timer::interrupt_timer::disarm_timer()
+{
+ if (this->armed) {
+ // Disable the interval timer before resetting the handler and rearming
+ // the previous interval timer or else we will have a race-condition
+ // where the timer might fire and the appropriate handler might not be
+ // set.
+ if (setitimer(ITIMER_REAL, &DISABLE_TV, NULL) != 0) {
+ log_error("Failed to disable the timer: %s", strerror(errno));
+ throw timer::error(errno);
+ }
+ if (sigaction(SIGALRM, &this->old_handler, NULL) == -1) {
+ log_error("Failed to reinstall previous SIGALRM handler: %s",
+ strerror(errno));
+ throw timer::error(errno);
+ }
+ if (setitimer(ITIMER_REAL, &this->old_val, NULL) != 0) {
+ log_error("Failed to reset the timer to previous value: %s",
+ strerror(errno));
+ throw timer::error(errno);
+ }
+ this->armed = false;
+ this->old_val = DISABLE_TV;
+ memset(&this->old_handler, 0, sizeof(this->old_handler));
+ }
+}
+
+timer::interrupt_timer::~interrupt_timer()
+{
+ this->disarm_timer();
+}
diff --git a/src/timer.hh b/src/timer.hh
new file mode 100644
index 0000000..9820f6d
--- /dev/null
+++ b/src/timer.hh
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2015, Suresh Sundriyal
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of the copyright holder nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef timer_hh
+#define timer_hh
+
+#include <exception>
+
+#include <errno.h>
+#include <signal.h>
+#include <sys/time.h>
+#include <sys/types.h>
+
+// Linux and BSD seem to name the function pointer to signal handler
+// differently. Linux names it 'sighandler_t' and BSD names it 'sig_t'. Instead
+// of depending on configure scripts to find out the type name, typedef it right
+// here.
+typedef void (*sighandler_t_)(int);
+
+namespace timer {
+class error : public std::exception {
+public:
+ error(int err);
+ int e_err;
+};
+
+class interrupt_timer {
+public:
+ interrupt_timer(struct timeval, sighandler_t_);
+ int arm_timer();
+ void disarm_timer();
+ bool is_armed();
+ ~interrupt_timer();
+
+private:
+ sighandler_t_ new_handler;
+ struct sigaction old_handler;
+ struct itimerval new_val, old_val;
+ bool armed;
+};
+} // namespace timer
+#endif
diff --git a/src/top_status_source.cc b/src/top_status_source.cc
new file mode 100644
index 0000000..59b95e5
--- /dev/null
+++ b/src/top_status_source.cc
@@ -0,0 +1,122 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "top_status_source.hh"
+
+#include "base/injector.hh"
+#include "config.h"
+#include "lnav.hh"
+#include "md2attr_line.hh"
+#include "md4cpp.hh"
+#include "shlex.hh"
+#include "shlex.resolver.hh"
+#include "sql_util.hh"
+#include "sqlitepp.client.hh"
+#include "top_status_source.cfg.hh"
+
+static const char* MSG_QUERY = R"(
+SELECT message FROM lnav_user_notifications
+ WHERE message IS NOT NULL AND
+ (expiration IS NULL OR expiration > datetime('now')) AND
+ (views IS NULL OR
+ json_contains(views, (SELECT name FROM lnav_top_view)))
+ ORDER BY priority DESC
+ LIMIT 1
+)";
+
+top_status_source::top_status_source(auto_sqlite3& db,
+ const top_status_source_cfg& cfg)
+ : tss_config(cfg),
+ tss_user_msgs_stmt(prepare_stmt(db.in(), MSG_QUERY).unwrap())
+{
+ this->tss_fields[TSF_TIME].set_width(28);
+ this->tss_fields[TSF_TIME].set_role(role_t::VCR_STATUS_INFO);
+ this->tss_fields[TSF_USER_MSG].set_share(1);
+ this->tss_fields[TSF_USER_MSG].right_justify(true);
+ this->tss_fields[TSF_USER_MSG].set_role(role_t::VCR_STATUS_INFO);
+}
+
+void
+top_status_source::update_time(const timeval& current_time)
+{
+ auto& sf = this->tss_fields[TSF_TIME];
+ char buffer[32];
+
+ buffer[0] = ' ';
+ strftime(&buffer[1],
+ sizeof(buffer) - 1,
+ this->tss_config.tssc_clock_format.c_str(),
+ localtime(&current_time.tv_sec));
+ sf.set_value(buffer);
+}
+
+void
+top_status_source::update_time()
+{
+ struct timeval tv;
+
+ gettimeofday(&tv, nullptr);
+ this->update_time(tv);
+}
+
+void
+top_status_source::update_user_msg()
+{
+ auto& al = this->tss_fields[TSF_USER_MSG].get_value();
+ al.clear();
+
+ this->tss_user_msgs_stmt.reset();
+ auto fetch_res = this->tss_user_msgs_stmt.fetch_row<std::string>();
+ fetch_res.match(
+ [&al](const std::string& value) {
+ shlex lexer(value);
+ std::string user_note;
+
+ lexer.with_ignore_quotes(true).eval(
+ user_note, lnav_data.ld_exec_context.ec_global_vars);
+
+ md2attr_line mdal;
+ auto parse_res = md4cpp::parse(user_note, mdal);
+ if (parse_res.isOk()) {
+ al = parse_res.unwrap();
+ } else {
+ log_error("failed to parse user note as markdown: %s",
+ parse_res.unwrapErr().c_str());
+ al = user_note;
+ }
+
+ scrub_ansi_string(al.get_string(), &al.get_attrs());
+ al.append(" ");
+ },
+ [](const prepared_stmt::end_of_rows&) {},
+ [](const prepared_stmt::fetch_error& fe) {
+ log_error("failed to execute user-message expression: %s",
+ fe.fe_msg.c_str());
+ });
+}
diff --git a/src/top_status_source.cfg.hh b/src/top_status_source.cfg.hh
new file mode 100644
index 0000000..825f5e7
--- /dev/null
+++ b/src/top_status_source.cfg.hh
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_top_status_source_cfg_hh
+#define lnav_top_status_source_cfg_hh
+
+struct top_status_source_cfg {
+ std::string tssc_clock_format;
+};
+
+#endif
diff --git a/src/top_status_source.hh b/src/top_status_source.hh
new file mode 100644
index 0000000..5c76796
--- /dev/null
+++ b/src/top_status_source.hh
@@ -0,0 +1,79 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_top_status_source_hh
+#define lnav_top_status_source_hh
+
+#include <string>
+
+#include <sqlite3.h>
+
+#include "base/injector.hh"
+#include "bound_tags.hh"
+#include "listview_curses.hh"
+#include "sql_util.hh"
+#include "sqlitepp.client.hh"
+#include "statusview_curses.hh"
+#include "top_status_source.cfg.hh"
+
+class top_status_source : public status_data_source {
+public:
+ enum field_t {
+ TSF_TIME,
+ TSF_USER_MSG,
+
+ TSF__MAX
+ };
+
+ explicit top_status_source(auto_sqlite3& db,
+ const top_status_source_cfg& cfg);
+
+ using injectable
+ = top_status_source(auto_sqlite3& db, const top_status_source_cfg& cfg);
+
+ size_t statusview_fields() override { return TSF__MAX; }
+
+ status_field& statusview_value_for_field(int field) override
+ {
+ return this->tss_fields[field];
+ }
+
+ void update_time(const struct timeval& current_time);
+
+ void update_time();
+
+ void update_user_msg();
+
+private:
+ const top_status_source_cfg& tss_config;
+ status_field tss_fields[TSF__MAX];
+ prepared_stmt tss_user_msgs_stmt;
+};
+
+#endif
diff --git a/src/top_sys_status_source.hh b/src/top_sys_status_source.hh
new file mode 100644
index 0000000..10f6064
--- /dev/null
+++ b/src/top_sys_status_source.hh
@@ -0,0 +1,80 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef _top_sys_status_source_hh
+#define _top_sys_status_source_hh
+
+#include <string>
+
+#include "logfile_sub_source.hh"
+#include "statusview_curses.hh"
+
+class top_sys_status_source : public status_data_source {
+public:
+ typedef enum {
+ TSF_CPU,
+ TSF_MEM,
+ TSF_TRAF,
+
+ TSF__MAX
+ } field_t;
+
+ top_sys_status_source()
+ {
+ static std::string names[TSF__MAX] = {
+ "#CPU",
+ "#Mem",
+ "#Traf",
+ };
+
+ int lpc;
+
+ for (lpc = 0; lpc < TSF__MAX; lpc++) {
+ this->tss_fields[lpc].set_width(5);
+ this->tss_fields[lpc].set_value(names[lpc]);
+ }
+ this->tss_fields[TSF_CPU].set_role(role_t::VCR_WARN_STATUS);
+ this->tss_fields[TSF_MEM].set_role(role_t::VCR_ALERT_STATUS);
+ this->tss_fields[TSF_TRAF].set_role(role_t::VCR_ACTIVE_STATUS);
+ };
+
+ size_t statusview_fields()
+ {
+ return TSF__MAX;
+ };
+
+ status_field& statusview_value_for_field(int field)
+ {
+ return this->tss_fields[field];
+ };
+
+private:
+ telltale_field tss_fields[TSF__MAX];
+};
+#endif
diff --git a/src/unique_path.cc b/src/unique_path.cc
new file mode 100644
index 0000000..c0cbc05
--- /dev/null
+++ b/src/unique_path.cc
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "unique_path.hh"
+
+#include "config.h"
+
+void
+unique_path_generator::add_source(
+ const std::shared_ptr<unique_path_source>& path_source)
+{
+ ghc::filesystem::path path = path_source->get_path();
+
+ path_source->set_unique_path(path.filename());
+ path_source->set_path_prefix(path.parent_path());
+ this->upg_unique_paths[path.filename()].push_back(path_source);
+}
+
+void
+unique_path_generator::generate()
+{
+ int loop_count = 0;
+
+ while (!this->upg_unique_paths.empty()) {
+ std::vector<std::shared_ptr<unique_path_source>> collisions;
+
+ for (const auto& pair : this->upg_unique_paths) {
+ if (pair.second.size() == 1) {
+ if (loop_count > 0) {
+ std::shared_ptr<unique_path_source> src = pair.second[0];
+
+ src->set_unique_path("[" + src->get_unique_path());
+ }
+
+ this->upg_max_len
+ = std::max(this->upg_max_len,
+ pair.second[0]->get_unique_path().size());
+ } else {
+ bool all_common = true;
+
+ do {
+ std::string common;
+
+ for (auto& src : pair.second) {
+ auto& path = src->get_path_prefix();
+
+ if (common.empty()) {
+ common = path.filename();
+ if (common.empty()) {
+ all_common = false;
+ }
+ } else if (common != path.filename()) {
+ all_common = false;
+ }
+ }
+
+ if (all_common) {
+ for (auto& src : pair.second) {
+ auto& path = src->get_path_prefix();
+ auto par = path.parent_path();
+
+ if (path.empty() || path == par) {
+ all_common = false;
+ } else {
+ src->set_path_prefix(path.parent_path());
+ }
+ }
+ }
+ } while (all_common);
+
+ collisions.insert(
+ collisions.end(), pair.second.begin(), pair.second.end());
+ }
+ }
+
+ this->upg_unique_paths.clear();
+
+ for (auto& src : collisions) {
+ const auto unique_path = src->get_unique_path();
+ auto& prefix = src->get_path_prefix();
+
+ if (loop_count == 0) {
+ src->set_unique_path(prefix.filename().string() + "]/"
+ + unique_path);
+ } else {
+ src->set_unique_path(prefix.filename().string() + "/"
+ + unique_path);
+ }
+
+ ghc::filesystem::path parent = prefix.parent_path();
+
+ src->set_path_prefix(parent);
+
+ if (parent.empty() || parent == prefix) {
+ src->set_unique_path("[" + src->get_unique_path());
+ } else {
+ this->upg_unique_paths[src->get_unique_path()].push_back(src);
+ }
+ }
+
+ loop_count += 1;
+ }
+}
diff --git a/src/unique_path.hh b/src/unique_path.hh
new file mode 100644
index 0000000..47ff93b
--- /dev/null
+++ b/src/unique_path.hh
@@ -0,0 +1,88 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file unique_path.hh
+ */
+
+#ifndef LNAV_UNIQUE_PATH_HH
+#define LNAV_UNIQUE_PATH_HH
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "ghc/filesystem.hpp"
+
+/**
+ * A source of a path for the unique_path_generator.
+ */
+class unique_path_source {
+public:
+ virtual ~unique_path_source() = default;
+
+ void set_unique_path(const std::string& path)
+ {
+ this->ups_unique_path = path;
+ }
+
+ const std::string& get_unique_path() const { return this->ups_unique_path; }
+
+ virtual ghc::filesystem::path get_path() const = 0;
+
+ ghc::filesystem::path& get_path_prefix()
+ {
+ return this->ups_prefix;
+ }
+
+ void set_path_prefix(const ghc::filesystem::path& prefix)
+ {
+ this->ups_prefix = prefix;
+ }
+
+private:
+ ghc::filesystem::path ups_prefix;
+ std::string ups_unique_path;
+};
+
+/**
+ * Given a collection of filesystem paths, this class will generate a shortened
+ * and unique path for each of the given paths.
+ */
+class unique_path_generator {
+public:
+ void add_source(const std::shared_ptr<unique_path_source>& path_source);
+
+ void generate();
+
+ std::map<std::string, std::vector<std::shared_ptr<unique_path_source>>>
+ upg_unique_paths;
+ size_t upg_max_len{0};
+};
+
+#endif // LNAV_UNIQUE_PATH_HH
diff --git a/src/url_loader.hh b/src/url_loader.hh
new file mode 100644
index 0000000..799d1a2
--- /dev/null
+++ b/src/url_loader.hh
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef url_loader_hh
+#define url_loader_hh
+
+#include "config.h"
+
+#ifdef HAVE_LIBCURL
+# include <curl/curl.h>
+# include <paths.h>
+
+# include "base/fs_util.hh"
+# include "curl_looper.hh"
+
+class url_loader : public curl_request {
+public:
+ url_loader(const std::string& url) : curl_request(url)
+ {
+ auto tmp_res = lnav::filesystem::open_temp_file(
+ ghc::filesystem::temp_directory_path() / "lnav.url.XXXXXX");
+ if (tmp_res.isErr()) {
+ return;
+ }
+
+ auto tmp_pair = tmp_res.unwrap();
+ ghc::filesystem::remove(tmp_pair.first);
+ this->ul_fd = std::move(tmp_pair.second);
+
+ curl_easy_setopt(this->cr_handle, CURLOPT_URL, this->cr_name.c_str());
+ curl_easy_setopt(this->cr_handle, CURLOPT_WRITEFUNCTION, write_cb);
+ curl_easy_setopt(this->cr_handle, CURLOPT_WRITEDATA, this);
+ curl_easy_setopt(this->cr_handle, CURLOPT_FILETIME, 1);
+ curl_easy_setopt(this->cr_handle, CURLOPT_BUFFERSIZE, 128L * 1024L);
+ }
+
+ int get_fd() const { return this->ul_fd.get(); }
+
+ auto_fd copy_fd() const { return this->ul_fd.dup(); }
+
+ long complete(CURLcode result)
+ {
+ curl_request::complete(result);
+
+ switch (result) {
+ case CURLE_OK:
+ break;
+ case CURLE_BAD_DOWNLOAD_RESUME:
+ break;
+ default:
+ log_error("%s:curl failure -- %ld %s",
+ this->cr_name.c_str(),
+ result,
+ curl_easy_strerror(result));
+ log_perror(write(this->ul_fd,
+ this->cr_error_buffer,
+ strlen(this->cr_error_buffer)));
+ return -1;
+ }
+
+ long file_time;
+ CURLcode rc;
+
+ rc = curl_easy_getinfo(this->cr_handle, CURLINFO_FILETIME, &file_time);
+ if (rc == CURLE_OK) {
+ time_t current_time;
+
+ time(&current_time);
+ if (file_time == -1
+ || (current_time - file_time) < FOLLOW_IF_MODIFIED_SINCE) {
+ char range[64];
+ struct stat st;
+ off_t start;
+
+ fstat(this->ul_fd, &st);
+ if (st.st_size > 0) {
+ start = st.st_size - 1;
+ this->ul_resume_offset = 1;
+ } else {
+ start = 0;
+ this->ul_resume_offset = 0;
+ }
+ snprintf(range, sizeof(range), "%ld-", (long) start);
+ curl_easy_setopt(this->cr_handle, CURLOPT_RANGE, range);
+ return 2000;
+ } else {
+ log_debug("URL was not recently modified, not tailing: %s",
+ this->cr_name.c_str());
+ }
+ } else {
+ log_error("Could not get file time for URL: %s -- %s",
+ this->cr_name.c_str(),
+ curl_easy_strerror(rc));
+ }
+
+ return -1;
+ }
+
+private:
+ static const long FOLLOW_IF_MODIFIED_SINCE = 60 * 60;
+
+ static ssize_t write_cb(void* contents,
+ size_t size,
+ size_t nmemb,
+ void* userp)
+ {
+ url_loader* ul = (url_loader*) userp;
+ char* c_contents = (char*) contents;
+ ssize_t retval;
+
+ c_contents += ul->ul_resume_offset;
+ retval = write(
+ ul->ul_fd, c_contents, (size * nmemb) - ul->ul_resume_offset);
+ retval += ul->ul_resume_offset;
+ ul->ul_resume_offset = 0;
+ return retval;
+ }
+
+ auto_fd ul_fd;
+ off_t ul_resume_offset{0};
+};
+#endif
+
+#endif
diff --git a/src/view_curses.cc b/src/view_curses.cc
new file mode 100644
index 0000000..7f8f079
--- /dev/null
+++ b/src/view_curses.cc
@@ -0,0 +1,1247 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file view_curses.cc
+ */
+
+#ifdef __CYGWIN__
+# include <alloca.h>
+#endif
+
+#include <chrono>
+#include <cmath>
+#include <string>
+
+#include "base/ansi_scrubber.hh"
+#include "base/attr_line.hh"
+#include "base/itertools.hh"
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "lnav_config.hh"
+#include "shlex.hh"
+#include "view_curses.hh"
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/term.h>
+#elif defined HAVE_NCURSESW_H
+# include <term.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/term.h>
+#elif defined HAVE_NCURSES_H
+# include <term.h>
+#elif defined HAVE_CURSES_H
+# include <term.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+
+using namespace std::chrono_literals;
+
+const struct itimerval ui_periodic_timer::INTERVAL = {
+ {0, std::chrono::duration_cast<std::chrono::microseconds>(350ms).count()},
+ {0, std::chrono::duration_cast<std::chrono::microseconds>(350ms).count()},
+};
+
+ui_periodic_timer::ui_periodic_timer() : upt_counter(0)
+{
+ struct sigaction sa;
+
+ sa.sa_handler = ui_periodic_timer::sigalrm;
+ sa.sa_flags = SA_RESTART;
+ sigemptyset(&sa.sa_mask);
+ sigaction(SIGALRM, &sa, nullptr);
+ if (setitimer(ITIMER_REAL, &INTERVAL, nullptr) == -1) {
+ perror("setitimer");
+ }
+}
+
+ui_periodic_timer&
+ui_periodic_timer::singleton()
+{
+ static ui_periodic_timer retval;
+
+ return retval;
+}
+
+void
+ui_periodic_timer::sigalrm(int sig)
+{
+ singleton().upt_counter += 1;
+}
+
+alerter&
+alerter::singleton()
+{
+ static alerter retval;
+
+ return retval;
+}
+
+bool
+alerter::chime(std::string msg)
+{
+ if (!this->a_enabled) {
+ return true;
+ }
+
+ bool retval = this->a_do_flash;
+ if (this->a_do_flash) {
+ log_warning("chime message: %s", msg.c_str());
+ ::flash();
+ }
+ this->a_do_flash = false;
+ return retval;
+}
+
+struct utf_to_display_adjustment {
+ int uda_origin;
+ int uda_offset;
+
+ utf_to_display_adjustment(int utf_origin, int offset)
+ : uda_origin(utf_origin), uda_offset(offset){
+
+ };
+};
+
+void
+view_curses::awaiting_user_input()
+{
+ static const bool enabled = getenv("lnav_test") != nullptr;
+ static const char OSC_INPUT[] = "\x1b]999;send-input\a";
+
+ if (enabled) {
+ write(STDOUT_FILENO, OSC_INPUT, sizeof(OSC_INPUT) - 1);
+ }
+}
+
+size_t
+view_curses::mvwattrline(WINDOW* window,
+ int y,
+ int x,
+ attr_line_t& al,
+ const struct line_range& lr_chars,
+ role_t base_role)
+{
+ auto& sa = al.get_attrs();
+ auto& line = al.get_string();
+ std::vector<utf_to_display_adjustment> utf_adjustments;
+ std::string full_line;
+
+ require(lr_chars.lr_end >= 0);
+
+ auto line_width_chars = lr_chars.length();
+ std::string expanded_line;
+
+ short* fg_color = (short*) alloca(line_width_chars * sizeof(short));
+ bool has_fg = false;
+ short* bg_color = (short*) alloca(line_width_chars * sizeof(short));
+ bool has_bg = false;
+ line_range lr_bytes;
+ int char_index = 0;
+
+ for (size_t lpc = 0; lpc < line.size(); lpc++) {
+ int exp_start_index = expanded_line.size();
+ auto ch = static_cast<unsigned char>(line[lpc]);
+
+ if (char_index == lr_chars.lr_start) {
+ lr_bytes.lr_start = exp_start_index;
+ } else if (char_index == lr_chars.lr_end) {
+ lr_bytes.lr_end = exp_start_index;
+ }
+
+ switch (ch) {
+ case '\t': {
+ do {
+ expanded_line.push_back(' ');
+ char_index += 1;
+ } while (expanded_line.size() % 8);
+ utf_adjustments.emplace_back(
+ lpc, expanded_line.size() - exp_start_index - 1);
+ break;
+ }
+
+ case '\r':
+ case '\n':
+ expanded_line.push_back(' ');
+ char_index += 1;
+ break;
+
+ default: {
+ auto size_result = ww898::utf::utf8::char_size([&line, lpc]() {
+ return std::make_pair(line[lpc], line.length() - lpc - 1);
+ });
+
+ if (size_result.isErr()) {
+ expanded_line.push_back('?');
+ } else {
+ auto offset = 1 - (int) size_result.unwrap();
+
+ expanded_line.push_back(ch);
+ if (offset) {
+#if 0
+ if (char_index < lr_chars.lr_start) {
+ lr_bytes.lr_start += abs(offset);
+ }
+ if (char_index < lr_chars.lr_end) {
+ lr_bytes.lr_end += abs(offset);
+ }
+#endif
+ utf_adjustments.emplace_back(lpc, offset);
+ for (; offset && (lpc + 1) < line.size();
+ lpc++, offset++)
+ {
+ expanded_line.push_back(line[lpc + 1]);
+ }
+ }
+ }
+ char_index += 1;
+ break;
+ }
+ }
+ }
+ if (lr_bytes.lr_start == -1) {
+ lr_bytes.lr_start = expanded_line.size();
+ }
+ if (lr_bytes.lr_end == -1) {
+ lr_bytes.lr_end = expanded_line.size();
+ }
+ size_t retval = expanded_line.size() - lr_bytes.lr_end;
+
+ full_line = expanded_line;
+
+ auto& vc = view_colors::singleton();
+ auto text_role_attrs = vc.attrs_for_role(role_t::VCR_TEXT);
+ auto attrs = vc.attrs_for_role(base_role);
+ wmove(window, y, x);
+ wattr_set(window,
+ attrs.ta_attrs,
+ vc.ensure_color_pair(attrs.ta_fg_color, attrs.ta_bg_color),
+ nullptr);
+ if (lr_bytes.lr_start < (int) full_line.size()) {
+ waddnstr(
+ window, &full_line.c_str()[lr_bytes.lr_start], lr_bytes.length());
+ }
+ if (lr_chars.lr_end > char_index) {
+ whline(window, ' ', lr_chars.lr_end - char_index);
+ }
+
+ std::stable_sort(sa.begin(), sa.end());
+ for (auto iter = sa.begin(); iter != sa.end(); ++iter) {
+ auto attr_range = iter->sa_range;
+
+ require(attr_range.lr_start >= 0);
+ require(attr_range.lr_end >= -1);
+
+ if (!(iter->sa_type == &VC_ROLE || iter->sa_type == &VC_ROLE_FG
+ || iter->sa_type == &VC_STYLE || iter->sa_type == &VC_GRAPHIC
+ || iter->sa_type == &SA_LEVEL || iter->sa_type == &VC_FOREGROUND
+ || iter->sa_type == &VC_BACKGROUND))
+ {
+ continue;
+ }
+
+ if (attr_range.lr_unit == line_range::unit::bytes) {
+ for (const auto& adj : utf_adjustments) {
+ // If the UTF adjustment is in the viewport, we need to adjust
+ // this attribute.
+ if (adj.uda_origin < iter->sa_range.lr_start) {
+ attr_range.lr_start += adj.uda_offset;
+ }
+ }
+
+ if (attr_range.lr_end != -1) {
+ for (const auto& adj : utf_adjustments) {
+ if (adj.uda_origin < iter->sa_range.lr_end) {
+ attr_range.lr_end += adj.uda_offset;
+ }
+ }
+ }
+ }
+
+ if (attr_range.lr_end == -1) {
+ attr_range.lr_end = lr_chars.lr_start + line_width_chars;
+ }
+ if (attr_range.lr_end < lr_chars.lr_start) {
+ continue;
+ }
+ attr_range.lr_start
+ = std::max(0, attr_range.lr_start - lr_chars.lr_start);
+ if (attr_range.lr_start > line_width_chars) {
+ continue;
+ }
+
+ attr_range.lr_end
+ = std::min(line_width_chars, attr_range.lr_end - lr_chars.lr_start);
+
+ if (iter->sa_type == &VC_FOREGROUND) {
+ if (!has_fg) {
+ memset(fg_color, -1, line_width_chars * sizeof(short));
+ }
+ short attr_fg = iter->sa_value.get<int64_t>();
+ if (attr_fg == view_colors::MATCH_COLOR_SEMANTIC) {
+ attr_fg = vc.color_for_ident(al.to_string_fragment(iter))
+ .value_or(view_colors::MATCH_COLOR_DEFAULT);
+ } else if (attr_fg < 8) {
+ attr_fg = vc.ansi_to_theme_color(attr_fg);
+ }
+ std::fill(&fg_color[attr_range.lr_start],
+ &fg_color[attr_range.lr_end],
+ attr_fg);
+ has_fg = true;
+ continue;
+ }
+
+ if (iter->sa_type == &VC_BACKGROUND) {
+ if (!has_bg) {
+ memset(bg_color, -1, line_width_chars * sizeof(short));
+ }
+ short attr_bg = iter->sa_value.get<int64_t>();
+ if (attr_bg == view_colors::MATCH_COLOR_SEMANTIC) {
+ attr_bg = vc.color_for_ident(al.to_string_fragment(iter))
+ .value_or(view_colors::MATCH_COLOR_DEFAULT);
+ }
+ std::fill(bg_color + attr_range.lr_start,
+ bg_color + attr_range.lr_end,
+ attr_bg);
+ has_bg = true;
+ continue;
+ }
+
+ if (attr_range.lr_start < attr_range.lr_end) {
+ int awidth = attr_range.length();
+ nonstd::optional<char> graphic;
+
+ if (iter->sa_type == &VC_GRAPHIC) {
+ graphic = iter->sa_value.get<int64_t>();
+ attrs = text_attrs{};
+ } else if (iter->sa_type == &VC_STYLE) {
+ attrs = iter->sa_value.get<text_attrs>();
+ } else if (iter->sa_type == &SA_LEVEL) {
+ attrs = vc.attrs_for_level(
+ (log_level_t) iter->sa_value.get<int64_t>());
+ } else if (iter->sa_type == &VC_ROLE) {
+ auto role = iter->sa_value.get<role_t>();
+ attrs = vc.attrs_for_role(role);
+ } else if (iter->sa_type == &VC_ROLE_FG) {
+ auto role_attrs
+ = vc.attrs_for_role(iter->sa_value.get<role_t>());
+ attrs.ta_fg_color = role_attrs.ta_fg_color;
+ }
+
+ if (graphic || !attrs.empty()) {
+ int x_pos = x + attr_range.lr_start;
+ int ch_width = std::min(
+ awidth, (line_width_chars - attr_range.lr_start));
+ cchar_t row_ch[ch_width + 1];
+
+ if (attrs.ta_attrs & (A_LEFT | A_RIGHT)) {
+ if (attrs.ta_attrs & A_LEFT) {
+ attrs.ta_fg_color
+ = vc.color_for_ident(al.to_string_fragment(iter));
+ }
+ if (attrs.ta_attrs & A_RIGHT) {
+ attrs.ta_bg_color
+ = vc.color_for_ident(al.to_string_fragment(iter));
+ }
+ attrs.ta_attrs &= ~(A_LEFT | A_RIGHT);
+ }
+
+ if (attrs.ta_fg_color) {
+ if (!has_fg) {
+ memset(fg_color, -1, line_width_chars * sizeof(short));
+ }
+ std::fill(&fg_color[attr_range.lr_start],
+ &fg_color[attr_range.lr_end],
+ attrs.ta_fg_color.value());
+ has_fg = true;
+ }
+ if (attrs.ta_bg_color) {
+ if (!has_bg) {
+ memset(bg_color, -1, line_width_chars * sizeof(short));
+ }
+ std::fill(&bg_color[attr_range.lr_start],
+ &bg_color[attr_range.lr_end],
+ attrs.ta_bg_color.value());
+ has_bg = true;
+ }
+
+ mvwin_wchnstr(window, y, x_pos, row_ch, ch_width);
+ for (int lpc = 0; lpc < ch_width; lpc++) {
+ bool clear_rev = false;
+
+ if (graphic) {
+ row_ch[lpc].chars[0] = graphic.value();
+ row_ch[lpc].attr |= A_ALTCHARSET;
+ }
+ if (row_ch[lpc].attr & A_REVERSE
+ && attrs.ta_attrs & A_REVERSE)
+ {
+ clear_rev = true;
+ }
+ row_ch[lpc].attr |= attrs.ta_attrs;
+ if (clear_rev) {
+ row_ch[lpc].attr &= ~A_REVERSE;
+ }
+ }
+ mvwadd_wchnstr(window, y, x_pos, row_ch, ch_width);
+ }
+ }
+ }
+
+ if (has_fg || has_bg) {
+ if (!has_fg) {
+ memset(fg_color, -1, line_width_chars * sizeof(short));
+ }
+ if (!has_bg) {
+ memset(bg_color, -1, line_width_chars * sizeof(short));
+ }
+
+ int ch_width = lr_chars.length();
+ cchar_t row_ch[ch_width + 1];
+
+ mvwin_wchnstr(window, y, x, row_ch, ch_width);
+ for (int lpc = 0; lpc < ch_width; lpc++) {
+ if (fg_color[lpc] == -1 && bg_color[lpc] == -1) {
+ continue;
+ }
+#ifdef NCURSES_EXT_COLORS
+ auto cur_pair = row_ch[lpc].ext_color;
+#else
+ auto cur_pair = PAIR_NUMBER(row_ch[lpc].attr);
+#endif
+ short cur_fg, cur_bg;
+ pair_content(cur_pair, &cur_fg, &cur_bg);
+ if (fg_color[lpc] == -1) {
+ fg_color[lpc] = cur_fg;
+ }
+ if (bg_color[lpc] == -1) {
+ bg_color[lpc] = cur_bg;
+ }
+
+ int color_pair = vc.ensure_color_pair(fg_color[lpc], bg_color[lpc]);
+
+ row_ch[lpc].attr = row_ch[lpc].attr & ~A_COLOR;
+#ifdef NCURSES_EXT_COLORS
+ row_ch[lpc].ext_color = color_pair;
+#else
+ row_ch[lpc].attr |= COLOR_PAIR(color_pair);
+#endif
+ }
+ mvwadd_wchnstr(window, y, x, row_ch, ch_width);
+ }
+
+ return retval;
+}
+
+constexpr short view_colors::MATCH_COLOR_DEFAULT;
+constexpr short view_colors::MATCH_COLOR_SEMANTIC;
+
+view_colors&
+view_colors::singleton()
+{
+ static view_colors s_vc;
+
+ return s_vc;
+}
+
+view_colors::view_colors() : vc_dyn_pairs(0)
+{
+ size_t color_index = 0;
+ for (int z = 0; z < 6; z++) {
+ for (int x = 1; x < 6; x += 2) {
+ for (int y = 1; y < 6; y += 2) {
+ short fg = 16 + x + (y * 6) + (z * 6 * 6);
+
+ this->vc_highlight_colors[color_index++] = fg;
+ }
+ }
+ }
+}
+
+bool view_colors::initialized = false;
+
+static std::string COLOR_NAMES[] = {
+ "black",
+ "red",
+ "green",
+ "yellow",
+ "blue",
+ "magenta",
+ "cyan",
+ "white",
+};
+
+class color_listener : public lnav_config_listener {
+public:
+ void reload_config(error_reporter& reporter) override
+ {
+ if (!view_colors::initialized) {
+ return;
+ }
+
+ auto& vc = view_colors::singleton();
+
+ for (const auto& pair : lnav_config.lc_ui_theme_defs) {
+ vc.init_roles(pair.second, reporter);
+ }
+
+ auto iter = lnav_config.lc_ui_theme_defs.find(lnav_config.lc_ui_theme);
+
+ if (iter == lnav_config.lc_ui_theme_defs.end()) {
+ auto theme_names
+ = lnav_config.lc_ui_theme_defs | lnav::itertools::first();
+
+ reporter(&lnav_config.lc_ui_theme,
+ lnav::console::user_message::error(
+ attr_line_t("unknown theme -- ")
+ .append_quoted(lnav_config.lc_ui_theme))
+ .with_help(attr_line_t("The available themes are: ")
+ .join(theme_names, ", ")));
+
+ vc.init_roles(lnav_config.lc_ui_theme_defs["default"], reporter);
+ return;
+ }
+
+ if (view_colors::initialized) {
+ vc.init_roles(iter->second, reporter);
+ }
+ }
+};
+
+static color_listener _COLOR_LISTENER;
+term_color_palette* view_colors::vc_active_palette;
+
+void
+view_colors::init(bool headless)
+{
+ vc_active_palette = ansi_colors();
+ if (!headless && has_colors()) {
+ start_color();
+
+ if (lnav_config.lc_ui_default_colors) {
+ use_default_colors();
+ }
+ if (COLORS >= 256) {
+ vc_active_palette = xterm_colors();
+ }
+ }
+
+ log_debug("COLOR_PAIRS = %d", COLOR_PAIRS);
+
+ initialized = true;
+
+ {
+ auto reporter = [](const void*, const lnav::console::user_message&) {
+
+ };
+
+ _COLOR_LISTENER.reload_config(reporter);
+ }
+}
+
+inline text_attrs
+attr_for_colors(nonstd::optional<short> fg, nonstd::optional<short> bg)
+{
+ if (fg && fg.value() == -1) {
+ fg = COLOR_WHITE;
+ }
+ if (bg && bg.value() == -1) {
+ bg = COLOR_BLACK;
+ }
+
+ if (lnav_config.lc_ui_default_colors) {
+ if (fg && fg.value() == COLOR_WHITE) {
+ fg = -1;
+ }
+ if (bg && bg.value() == COLOR_BLACK) {
+ bg = -1;
+ }
+ }
+
+ text_attrs retval;
+
+ if (fg && fg.value() == view_colors::MATCH_COLOR_SEMANTIC) {
+ retval.ta_attrs |= A_LEFT;
+ } else {
+ retval.ta_fg_color = fg;
+ }
+ if (bg && bg.value() == view_colors::MATCH_COLOR_SEMANTIC) {
+ retval.ta_attrs |= A_RIGHT;
+ } else {
+ retval.ta_bg_color = bg;
+ }
+
+ return retval;
+}
+
+view_colors::role_attrs
+view_colors::to_attrs(const lnav_theme& lt,
+ const positioned_property<style_config>& pp_sc,
+ lnav_config_listener::error_reporter& reporter)
+{
+ const auto& sc = pp_sc.pp_value;
+ std::string fg1, bg1, fg_color, bg_color;
+ intern_string_t role_class;
+
+ if (!pp_sc.pp_path.empty()) {
+ auto role_class_path
+ = ghc::filesystem::path(pp_sc.pp_path.to_string()).parent_path();
+ auto inner = role_class_path.filename().string();
+ auto outer = role_class_path.parent_path().filename().string();
+
+ role_class = intern_string::lookup(
+ fmt::format(FMT_STRING("-lnav_{}_{}"), outer, inner));
+ }
+
+ fg1 = sc.sc_color;
+ bg1 = sc.sc_background_color;
+ shlex(fg1).eval(fg_color, lt.lt_vars);
+ shlex(bg1).eval(bg_color, lt.lt_vars);
+
+ auto fg = styling::color_unit::from_str(fg_color).unwrapOrElse(
+ [&](const auto& msg) {
+ reporter(
+ &sc.sc_color,
+ lnav::console::user_message::error(
+ attr_line_t("invalid color -- ").append_quoted(sc.sc_color))
+ .with_reason(msg));
+ return styling::color_unit::make_empty();
+ });
+ auto bg = styling::color_unit::from_str(bg_color).unwrapOrElse(
+ [&](const auto& msg) {
+ reporter(&sc.sc_background_color,
+ lnav::console::user_message::error(
+ attr_line_t("invalid background color -- ")
+ .append_quoted(sc.sc_background_color))
+ .with_reason(msg));
+ return styling::color_unit::make_empty();
+ });
+
+ text_attrs retval1
+ = attr_for_colors(this->match_color(fg), this->match_color(bg));
+ text_attrs retval2;
+
+ if (sc.sc_underline) {
+ retval1.ta_attrs |= A_UNDERLINE;
+ retval2.ta_attrs |= A_UNDERLINE;
+ }
+ if (sc.sc_bold) {
+ retval1.ta_attrs |= A_BOLD;
+ retval2.ta_attrs |= A_BOLD;
+ }
+
+ return {retval1, retval2, role_class};
+}
+
+void
+view_colors::init_roles(const lnav_theme& lt,
+ lnav_config_listener::error_reporter& reporter)
+{
+ rgb_color fg, bg;
+ std::string err;
+
+ /* Setup the mappings from roles to actual colors. */
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_TEXT)]
+ = this->to_attrs(lt, lt.lt_style_text, reporter);
+
+ for (int ansi_fg = 0; ansi_fg < 8; ansi_fg++) {
+ for (int ansi_bg = 0; ansi_bg < 8; ansi_bg++) {
+ if (ansi_fg == 0 && ansi_bg == 0) {
+ continue;
+ }
+
+ auto fg_iter = lt.lt_vars.find(COLOR_NAMES[ansi_fg]);
+ auto bg_iter = lt.lt_vars.find(COLOR_NAMES[ansi_bg]);
+ auto fg_str = fg_iter == lt.lt_vars.end() ? "" : fg_iter->second;
+ auto bg_str = bg_iter == lt.lt_vars.end() ? "" : bg_iter->second;
+
+ auto rgb_fg = rgb_color::from_str(fg_str).unwrapOrElse(
+ [&](const auto& msg) {
+ reporter(&fg_str,
+ lnav::console::user_message::error(
+ attr_line_t("invalid color -- ")
+ .append_quoted(fg_str))
+ .with_reason(msg));
+ return rgb_color{};
+ });
+ auto rgb_bg = rgb_color::from_str(bg_str).unwrapOrElse(
+ [&](const auto& msg) {
+ reporter(&bg_str,
+ lnav::console::user_message::error(
+ attr_line_t("invalid background color -- ")
+ .append_quoted(bg_str))
+ .with_reason(msg));
+ return rgb_color{};
+ });
+
+ short fg = vc_active_palette->match_color(lab_color(rgb_fg));
+ short bg = vc_active_palette->match_color(lab_color(rgb_bg));
+
+ if (rgb_fg.empty()) {
+ fg = ansi_fg;
+ }
+ if (rgb_bg.empty()) {
+ bg = ansi_bg;
+ }
+
+ this->vc_ansi_to_theme[ansi_fg] = fg;
+ if (lnav_config.lc_ui_default_colors && bg == COLOR_BLACK) {
+ bg = -1;
+ if (fg == COLOR_WHITE) {
+ fg = -1;
+ }
+ }
+ }
+ }
+
+ if (lnav_config.lc_ui_dim_text) {
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_TEXT)]
+ .ra_normal.ta_attrs
+ |= A_DIM;
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_TEXT)]
+ .ra_reverse.ta_attrs
+ |= A_DIM;
+ }
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_SEARCH)]
+ = role_attrs{text_attrs{A_REVERSE}, text_attrs{A_REVERSE}};
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_SEARCH)]
+ .ra_class_name
+ = intern_string::lookup("-lnav_styles_search");
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_IDENTIFIER)]
+ = this->to_attrs(lt, lt.lt_style_identifier, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_OK)]
+ = this->to_attrs(lt, lt.lt_style_ok, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_INFO)]
+ = this->to_attrs(lt, lt.lt_style_info, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_ERROR)]
+ = this->to_attrs(lt, lt.lt_style_error, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_WARNING)]
+ = this->to_attrs(lt, lt.lt_style_warning, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_ALT_ROW)]
+ = this->to_attrs(lt, lt.lt_style_alt_text, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_HIDDEN)]
+ = this->to_attrs(lt, lt.lt_style_hidden, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_CURSOR_LINE)]
+ = this->to_attrs(lt, lt.lt_style_cursor_line, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_ADJUSTED_TIME)]
+ = this->to_attrs(lt, lt.lt_style_adjusted_time, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_SKEWED_TIME)]
+ = this->to_attrs(lt, lt.lt_style_skewed_time, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_OFFSET_TIME)]
+ = this->to_attrs(lt, lt.lt_style_offset_time, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_INVALID_MSG)]
+ = this->to_attrs(lt, lt.lt_style_invalid_msg, reporter);
+
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_STATUS)]
+ = this->to_attrs(lt, lt.lt_style_status, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_WARN_STATUS)]
+ = this->to_attrs(lt, lt.lt_style_warn_status, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_ALERT_STATUS)]
+ = this->to_attrs(lt, lt.lt_style_alert_status, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_ACTIVE_STATUS)]
+ = this->to_attrs(lt, lt.lt_style_active_status, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_ACTIVE_STATUS2)]
+ = role_attrs{
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_ACTIVE_STATUS)]
+ .ra_normal,
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_ACTIVE_STATUS)]
+ .ra_reverse,
+ };
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_ACTIVE_STATUS2)]
+ .ra_normal.ta_attrs
+ |= A_BOLD;
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_ACTIVE_STATUS2)]
+ .ra_reverse.ta_attrs
+ |= A_BOLD;
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_STATUS_TITLE)]
+ = this->to_attrs(lt, lt.lt_style_status_title, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_STATUS_SUBTITLE)]
+ = this->to_attrs(lt, lt.lt_style_status_subtitle, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_STATUS_INFO)]
+ = this->to_attrs(lt, lt.lt_style_status_info, reporter);
+
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_STATUS_HOTKEY)]
+ = this->to_attrs(lt, lt.lt_style_status_hotkey, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_STATUS_TITLE_HOTKEY)]
+ = this->to_attrs(lt, lt.lt_style_status_title_hotkey, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_STATUS_DISABLED_TITLE)]
+ = this->to_attrs(lt, lt.lt_style_status_disabled_title, reporter);
+
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_H1)]
+ = this->to_attrs(lt, lt.lt_style_header[0], reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_H2)]
+ = this->to_attrs(lt, lt.lt_style_header[1], reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_H3)]
+ = this->to_attrs(lt, lt.lt_style_header[2], reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_H4)]
+ = this->to_attrs(lt, lt.lt_style_header[3], reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_H5)]
+ = this->to_attrs(lt, lt.lt_style_header[4], reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_H6)]
+ = this->to_attrs(lt, lt.lt_style_header[5], reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_HR)]
+ = this->to_attrs(lt, lt.lt_style_hr, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_HYPERLINK)]
+ = this->to_attrs(lt, lt.lt_style_hyperlink, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_LIST_GLYPH)]
+ = this->to_attrs(lt, lt.lt_style_list_glyph, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_BREADCRUMB)]
+ = this->to_attrs(lt, lt.lt_style_breadcrumb, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_TABLE_BORDER)]
+ = this->to_attrs(lt, lt.lt_style_table_border, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_TABLE_HEADER)]
+ = this->to_attrs(lt, lt.lt_style_table_header, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_QUOTE_BORDER)]
+ = this->to_attrs(lt, lt.lt_style_quote_border, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_QUOTED_TEXT)]
+ = this->to_attrs(lt, lt.lt_style_quoted_text, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_FOOTNOTE_BORDER)]
+ = this->to_attrs(lt, lt.lt_style_footnote_border, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_FOOTNOTE_TEXT)]
+ = this->to_attrs(lt, lt.lt_style_footnote_text, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_SNIPPET_BORDER)]
+ = this->to_attrs(lt, lt.lt_style_snippet_border, reporter);
+
+ {
+ positioned_property<style_config> stitch_sc;
+
+ stitch_sc.pp_value.sc_color
+ = lt.lt_style_status_subtitle.pp_value.sc_background_color;
+ stitch_sc.pp_value.sc_background_color
+ = lt.lt_style_status_title.pp_value.sc_background_color;
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_STATUS_STITCH_TITLE_TO_SUB)]
+ = this->to_attrs(lt, stitch_sc, reporter);
+ }
+ {
+ positioned_property<style_config> stitch_sc;
+
+ stitch_sc.pp_value.sc_color
+ = lt.lt_style_status_title.pp_value.sc_background_color;
+ stitch_sc.pp_value.sc_background_color
+ = lt.lt_style_status_subtitle.pp_value.sc_background_color;
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_STATUS_STITCH_SUB_TO_TITLE)]
+ = this->to_attrs(lt, stitch_sc, reporter);
+ }
+
+ {
+ positioned_property<style_config> stitch_sc;
+
+ stitch_sc.pp_value.sc_color
+ = lt.lt_style_status.pp_value.sc_background_color;
+ stitch_sc.pp_value.sc_background_color
+ = lt.lt_style_status_subtitle.pp_value.sc_background_color;
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_STATUS_STITCH_SUB_TO_NORMAL)]
+ = this->to_attrs(lt, stitch_sc, reporter);
+ }
+ {
+ positioned_property<style_config> stitch_sc;
+
+ stitch_sc.pp_value.sc_color
+ = lt.lt_style_status_subtitle.pp_value.sc_background_color;
+ stitch_sc.pp_value.sc_background_color
+ = lt.lt_style_status.pp_value.sc_background_color;
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_STATUS_STITCH_NORMAL_TO_SUB)]
+ = this->to_attrs(lt, stitch_sc, reporter);
+ }
+
+ {
+ positioned_property<style_config> stitch_sc;
+
+ stitch_sc.pp_value.sc_color
+ = lt.lt_style_status.pp_value.sc_background_color;
+ stitch_sc.pp_value.sc_background_color
+ = lt.lt_style_status_title.pp_value.sc_background_color;
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_STATUS_STITCH_TITLE_TO_NORMAL)]
+ = this->to_attrs(lt, stitch_sc, reporter);
+ }
+ {
+ positioned_property<style_config> stitch_sc;
+
+ stitch_sc.pp_value.sc_color
+ = lt.lt_style_status_title.pp_value.sc_background_color;
+ stitch_sc.pp_value.sc_background_color
+ = lt.lt_style_status.pp_value.sc_background_color;
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_STATUS_STITCH_NORMAL_TO_TITLE)]
+ = this->to_attrs(lt, stitch_sc, reporter);
+ }
+
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_INACTIVE_STATUS)]
+ = this->to_attrs(lt, lt.lt_style_inactive_status, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_INACTIVE_ALERT_STATUS)]
+ = this->to_attrs(lt, lt.lt_style_inactive_alert_status, reporter);
+
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_POPUP)]
+ = this->to_attrs(lt, lt.lt_style_popup, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_FOCUSED)]
+ = this->to_attrs(lt, lt.lt_style_focused, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_DISABLED_FOCUSED)]
+ = this->to_attrs(lt, lt.lt_style_disabled_focused, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_SCROLLBAR)]
+ = this->to_attrs(lt, lt.lt_style_scrollbar, reporter);
+ {
+ positioned_property<style_config> bar_sc;
+
+ bar_sc.pp_value.sc_color = lt.lt_style_error.pp_value.sc_color;
+ bar_sc.pp_value.sc_background_color
+ = lt.lt_style_scrollbar.pp_value.sc_background_color;
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_SCROLLBAR_ERROR)]
+ = this->to_attrs(lt, bar_sc, reporter);
+ }
+ {
+ positioned_property<style_config> bar_sc;
+
+ bar_sc.pp_value.sc_color = lt.lt_style_warning.pp_value.sc_color;
+ bar_sc.pp_value.sc_background_color
+ = lt.lt_style_scrollbar.pp_value.sc_background_color;
+ this->vc_role_attrs[lnav::enums::to_underlying(
+ role_t::VCR_SCROLLBAR_WARNING)]
+ = this->to_attrs(lt, bar_sc, reporter);
+ }
+
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_QUOTED_CODE)]
+ = this->to_attrs(lt, lt.lt_style_quoted_code, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_CODE_BORDER)]
+ = this->to_attrs(lt, lt.lt_style_code_border, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_KEYWORD)]
+ = this->to_attrs(lt, lt.lt_style_keyword, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_STRING)]
+ = this->to_attrs(lt, lt.lt_style_string, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_COMMENT)]
+ = this->to_attrs(lt, lt.lt_style_comment, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_DOC_DIRECTIVE)]
+ = this->to_attrs(lt, lt.lt_style_doc_directive, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_VARIABLE)]
+ = this->to_attrs(lt, lt.lt_style_variable, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_SYMBOL)]
+ = this->to_attrs(lt, lt.lt_style_symbol, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_NUMBER)]
+ = this->to_attrs(lt, lt.lt_style_number, reporter);
+
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_RE_SPECIAL)]
+ = this->to_attrs(lt, lt.lt_style_re_special, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_RE_REPEAT)]
+ = this->to_attrs(lt, lt.lt_style_re_repeat, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_FILE)]
+ = this->to_attrs(lt, lt.lt_style_file, reporter);
+
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_DIFF_DELETE)]
+ = this->to_attrs(lt, lt.lt_style_diff_delete, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_DIFF_ADD)]
+ = this->to_attrs(lt, lt.lt_style_diff_add, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_DIFF_SECTION)]
+ = this->to_attrs(lt, lt.lt_style_diff_section, reporter);
+
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_LOW_THRESHOLD)]
+ = this->to_attrs(lt, lt.lt_style_low_threshold, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_MED_THRESHOLD)]
+ = this->to_attrs(lt, lt.lt_style_med_threshold, reporter);
+ this->vc_role_attrs[lnav::enums::to_underlying(role_t::VCR_HIGH_THRESHOLD)]
+ = this->to_attrs(lt, lt.lt_style_high_threshold, reporter);
+
+ for (auto level = static_cast<log_level_t>(LEVEL_UNKNOWN + 1);
+ level < LEVEL__MAX;
+ level = static_cast<log_level_t>(level + 1))
+ {
+ auto level_iter = lt.lt_level_styles.find(level);
+
+ if (level_iter == lt.lt_level_styles.end()) {
+ this->vc_level_attrs[level]
+ = role_attrs{text_attrs{}, text_attrs{}};
+ } else {
+ this->vc_level_attrs[level]
+ = this->to_attrs(lt, level_iter->second, reporter);
+ }
+ }
+
+ if (initialized && this->vc_color_pair_end == 0) {
+ this->vc_color_pair_end = 1;
+ }
+ this->vc_dyn_pairs.clear();
+
+ for (int32_t role_index = 0;
+ role_index < lnav::enums::to_underlying(role_t::VCR__MAX);
+ role_index++)
+ {
+ const auto& ra = this->vc_role_attrs[role_index];
+ if (ra.ra_class_name.empty()) {
+ continue;
+ }
+
+ this->vc_class_to_role[ra.ra_class_name.to_string()]
+ = VC_ROLE.value(role_t(role_index));
+ }
+ for (int level_index = 0; level_index < LEVEL__MAX; level_index++) {
+ const auto& ra = this->vc_level_attrs[level_index];
+ if (ra.ra_class_name.empty()) {
+ continue;
+ }
+
+ this->vc_class_to_role[ra.ra_class_name.to_string()]
+ = SA_LEVEL.value(level_index);
+ }
+}
+
+int
+view_colors::ensure_color_pair(short fg, short bg)
+{
+ require(fg >= -100);
+ require(bg >= -100);
+
+ if (fg >= COLOR_BLACK && fg <= COLOR_WHITE) {
+ fg = this->ansi_to_theme_color(fg);
+ }
+ if (bg >= COLOR_BLACK && bg <= COLOR_WHITE) {
+ bg = this->ansi_to_theme_color(bg);
+ }
+
+ auto index_pair = std::make_pair(fg, bg);
+ auto existing = this->vc_dyn_pairs.get(index_pair);
+
+ if (existing) {
+ auto retval = existing.value().dp_color_pair;
+
+ return retval;
+ }
+
+ auto def_attrs = this->attrs_for_role(role_t::VCR_TEXT);
+ int retval = this->vc_color_pair_end + this->vc_dyn_pairs.size();
+ auto attrs
+ = attr_for_colors(fg == -1 ? def_attrs.ta_fg_color.value_or(-1) : fg,
+ bg == -1 ? def_attrs.ta_bg_color.value_or(-1) : bg);
+ init_pair(retval, attrs.ta_fg_color.value(), attrs.ta_bg_color.value());
+
+ if (initialized) {
+ struct dyn_pair dp = {retval};
+
+ this->vc_dyn_pairs.set_max_size(256 - this->vc_color_pair_end);
+ this->vc_dyn_pairs.put(index_pair, dp);
+ }
+
+ return retval;
+}
+
+int
+view_colors::ensure_color_pair(nonstd::optional<short> fg,
+ nonstd::optional<short> bg)
+{
+ return this->ensure_color_pair(fg.value_or(-1), bg.value_or(-1));
+}
+
+int
+view_colors::ensure_color_pair(const styling::color_unit& rgb_fg,
+ const styling::color_unit& rgb_bg)
+{
+ auto fg = this->match_color(rgb_fg);
+ auto bg = this->match_color(rgb_bg);
+
+ return this->ensure_color_pair(fg, bg);
+}
+
+nonstd::optional<short>
+view_colors::match_color(const styling::color_unit& color) const
+{
+ return color.cu_value.match(
+ [](styling::semantic) -> nonstd::optional<short> {
+ return MATCH_COLOR_SEMANTIC;
+ },
+ [](const rgb_color& color) -> nonstd::optional<short> {
+ if (color.empty()) {
+ return nonstd::nullopt;
+ }
+
+ return vc_active_palette->match_color(lab_color(color));
+ });
+}
+
+nonstd::optional<short>
+view_colors::color_for_ident(const char* str, size_t len) const
+{
+ auto index = crc32(1, (const Bytef*) str, len);
+ int retval;
+
+ if (COLORS >= 256) {
+ if (str[0] == '#' && (len == 4 || len == 7)) {
+ auto fg_res
+ = styling::color_unit::from_str(string_fragment(str, 0, len));
+ if (fg_res.isOk()) {
+ return this->match_color(fg_res.unwrap());
+ }
+ }
+
+ auto offset = index % HI_COLOR_COUNT;
+ retval = this->vc_highlight_colors[offset];
+ } else {
+ retval = -1;
+ }
+
+ return retval;
+}
+
+text_attrs
+view_colors::attrs_for_ident(const char* str, size_t len) const
+{
+ auto retval = this->attrs_for_role(role_t::VCR_IDENTIFIER);
+
+ if (retval.ta_attrs & (A_LEFT | A_RIGHT)) {
+ if (retval.ta_attrs & A_LEFT) {
+ retval.ta_fg_color = this->color_for_ident(str, len);
+ }
+ if (retval.ta_attrs & A_RIGHT) {
+ retval.ta_bg_color = this->color_for_ident(str, len);
+ }
+ retval.ta_attrs &= ~(A_COLOR | A_LEFT | A_RIGHT);
+ }
+
+ return retval;
+}
+
+lab_color::lab_color(const rgb_color& rgb)
+{
+ double r = rgb.rc_r / 255.0, g = rgb.rc_g / 255.0, b = rgb.rc_b / 255.0, x,
+ y, z;
+
+ r = (r > 0.04045) ? pow((r + 0.055) / 1.055, 2.4) : r / 12.92;
+ g = (g > 0.04045) ? pow((g + 0.055) / 1.055, 2.4) : g / 12.92;
+ b = (b > 0.04045) ? pow((b + 0.055) / 1.055, 2.4) : b / 12.92;
+
+ x = (r * 0.4124 + g * 0.3576 + b * 0.1805) / 0.95047;
+ y = (r * 0.2126 + g * 0.7152 + b * 0.0722) / 1.00000;
+ z = (r * 0.0193 + g * 0.1192 + b * 0.9505) / 1.08883;
+
+ x = (x > 0.008856) ? pow(x, 1.0 / 3.0) : (7.787 * x) + 16.0 / 116.0;
+ y = (y > 0.008856) ? pow(y, 1.0 / 3.0) : (7.787 * y) + 16.0 / 116.0;
+ z = (z > 0.008856) ? pow(z, 1.0 / 3.0) : (7.787 * z) + 16.0 / 116.0;
+
+ this->lc_l = (116.0 * y) - 16;
+ this->lc_a = 500.0 * (x - y);
+ this->lc_b = 200.0 * (y - z);
+}
+
+double
+lab_color::deltaE(const lab_color& other) const
+{
+ double deltaL = this->lc_l - other.lc_l;
+ double deltaA = this->lc_a - other.lc_a;
+ double deltaB = this->lc_b - other.lc_b;
+ double c1 = sqrt(this->lc_a * this->lc_a + this->lc_b * this->lc_b);
+ double c2 = sqrt(other.lc_a * other.lc_a + other.lc_b * other.lc_b);
+ double deltaC = c1 - c2;
+ double deltaH = deltaA * deltaA + deltaB * deltaB - deltaC * deltaC;
+ deltaH = deltaH < 0.0 ? 0.0 : sqrt(deltaH);
+ double sc = 1.0 + 0.045 * c1;
+ double sh = 1.0 + 0.015 * c1;
+ double deltaLKlsl = deltaL / (1.0);
+ double deltaCkcsc = deltaC / (sc);
+ double deltaHkhsh = deltaH / (sh);
+ double i = deltaLKlsl * deltaLKlsl + deltaCkcsc * deltaCkcsc
+ + deltaHkhsh * deltaHkhsh;
+ return i < 0.0 ? 0.0 : sqrt(i);
+}
+
+bool
+lab_color::operator<(const lab_color& rhs) const
+{
+ if (lc_l < rhs.lc_l)
+ return true;
+ if (rhs.lc_l < lc_l)
+ return false;
+ if (lc_a < rhs.lc_a)
+ return true;
+ if (rhs.lc_a < lc_a)
+ return false;
+ return lc_b < rhs.lc_b;
+}
+
+bool
+lab_color::operator>(const lab_color& rhs) const
+{
+ return rhs < *this;
+}
+
+bool
+lab_color::operator<=(const lab_color& rhs) const
+{
+ return !(rhs < *this);
+}
+
+bool
+lab_color::operator>=(const lab_color& rhs) const
+{
+ return !(*this < rhs);
+}
+
+bool
+lab_color::operator==(const lab_color& rhs) const
+{
+ return lc_l == rhs.lc_l && lc_a == rhs.lc_a && lc_b == rhs.lc_b;
+}
+
+bool
+lab_color::operator!=(const lab_color& rhs) const
+{
+ return !(rhs == *this);
+}
+
+Result<screen_curses, std::string>
+screen_curses::create()
+{
+ int errret = 0;
+ if (setupterm(nullptr, STDIN_FILENO, &errret) == ERR) {
+ switch (errret) {
+ case 1:
+ return Err(std::string("the terminal is a hardcopy, da fuq?!"));
+ case 0:
+ return Err(
+ fmt::format(FMT_STRING("the TERM environment variable is "
+ "set to an unknown value: {}"),
+ getenv("TERM")));
+ case -1:
+ return Err(
+ std::string("the terminfo database could not be found"));
+ default:
+ return Err(std::string("setupterm() failed unexpectedly"));
+ }
+ }
+
+ newterm(nullptr, stdout, stdin);
+
+ return Ok(screen_curses{stdscr});
+}
diff --git a/src/view_curses.hh b/src/view_curses.hh
new file mode 100644
index 0000000..afa0be3
--- /dev/null
+++ b/src/view_curses.hh
@@ -0,0 +1,482 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file view_curses.hh
+ */
+
+#ifndef view_curses_hh
+#define view_curses_hh
+
+#include <limits.h>
+#include <signal.h>
+#include <stdint.h>
+#include <sys/time.h>
+#include <zlib.h>
+
+#include "config.h"
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/curses.h>
+#elif defined HAVE_NCURSESW_H
+# include <ncursesw.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/curses.h>
+#elif defined HAVE_NCURSES_H
+# include <ncurses.h>
+#elif defined HAVE_CURSES_H
+# include <curses.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+#include <functional>
+#include <map>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "base/attr_line.hh"
+#include "base/enum_util.hh"
+#include "base/lnav_log.hh"
+#include "base/lrucache.hpp"
+#include "base/opt_util.hh"
+#include "lnav_config_fwd.hh"
+#include "log_level.hh"
+#include "optional.hpp"
+#include "styling.hh"
+
+#define KEY_CTRL_A 0x01
+#define KEY_CTRL_E 0x05
+#define KEY_CTRL_G 7
+#define KEY_CTRL_L 12
+#define KEY_CTRL_P 16
+#define KEY_CTRL_R 18
+#define KEY_CTRL_W 23
+
+class view_curses;
+
+/**
+ * An RAII class that initializes and deinitializes curses.
+ */
+class screen_curses : public log_crash_recoverer {
+public:
+ static Result<screen_curses, std::string> create();
+
+ void log_crash_recover() override
+ {
+ if (this->sc_main_window != nullptr) {
+ endwin();
+ }
+ }
+
+ virtual ~screen_curses()
+ {
+ if (this->sc_main_window != nullptr) {
+ endwin();
+ }
+ }
+
+ screen_curses(screen_curses&& other)
+ : sc_main_window(std::exchange(other.sc_main_window, nullptr))
+ {
+ }
+
+ screen_curses(const screen_curses&) = delete;
+
+ screen_curses& operator=(screen_curses&& other)
+ {
+ this->sc_main_window = std::exchange(other.sc_main_window, nullptr);
+ return *this;
+ }
+
+ WINDOW* get_window() { return this->sc_main_window; }
+
+private:
+ screen_curses(WINDOW* win) : sc_main_window(win) {}
+
+ WINDOW* sc_main_window;
+};
+
+template<typename T>
+class action_broadcaster : public std::vector<std::function<void(T*)>> {
+public:
+ void operator()(T* t)
+ {
+ for (auto& func : *this) {
+ func(t);
+ }
+ }
+};
+
+class ui_periodic_timer {
+public:
+ static const struct itimerval INTERVAL;
+
+ static ui_periodic_timer& singleton();
+
+ bool time_to_update(sig_atomic_t& counter) const
+ {
+ if (this->upt_counter != counter) {
+ counter = this->upt_counter;
+ return true;
+ }
+ return false;
+ }
+
+ void start_fade(sig_atomic_t& counter, size_t decay) const
+ {
+ counter = this->upt_counter + decay;
+ }
+
+ int fade_diff(sig_atomic_t& counter) const
+ {
+ if (this->upt_counter >= counter) {
+ return 0;
+ }
+ return counter - this->upt_counter;
+ }
+
+private:
+ ui_periodic_timer();
+
+ static void sigalrm(int sig);
+
+ volatile sig_atomic_t upt_counter;
+};
+
+class alerter {
+public:
+ static alerter& singleton();
+
+ void enabled(bool enable) { this->a_enabled = enable; }
+
+ bool chime(std::string msg);
+
+ void new_input(int ch)
+ {
+ if (this->a_last_input != ch) {
+ this->a_do_flash = true;
+ }
+ this->a_last_input = ch;
+ }
+
+private:
+ bool a_enabled{true};
+ bool a_do_flash{true};
+ int a_last_input{-1};
+};
+
+/**
+ * Singleton used to manage the colorspace.
+ */
+class view_colors {
+public:
+ static constexpr unsigned long HI_COLOR_COUNT = 6 * 3 * 3;
+
+ /** @return A reference to the singleton. */
+ static view_colors& singleton();
+
+ view_colors(const view_colors&) = delete;
+ view_colors(view_colors&&) = delete;
+ view_colors& operator=(const view_colors&) = delete;
+ view_colors& operator=(view_colors&&) = delete;
+
+ /**
+ * Performs curses-specific initialization. The other methods can be
+ * called before this method, but the returned attributes cannot be used
+ * with curses code until this method is called.
+ */
+ static void init(bool headless);
+
+ void init_roles(const lnav_theme& lt,
+ lnav_config_listener::error_reporter& reporter);
+
+ /**
+ * @param role The role to retrieve character attributes for.
+ * @return The attributes to use for the given role.
+ */
+ text_attrs attrs_for_role(role_t role, bool selected = false) const
+ {
+ if (role == role_t::VCR_NONE) {
+ return {};
+ }
+
+ require(role > role_t::VCR_NONE);
+ require(role < role_t::VCR__MAX);
+
+ return selected
+ ? this->vc_role_attrs[lnav::enums::to_underlying(role)].ra_reverse
+ : this->vc_role_attrs[lnav::enums::to_underlying(role)].ra_normal;
+ }
+
+ nonstd::optional<short> color_for_ident(const char* str, size_t len) const;
+
+ nonstd::optional<short> color_for_ident(const string_fragment& sf) const
+ {
+ return this->color_for_ident(sf.data(), sf.length());
+ }
+
+ text_attrs attrs_for_ident(const char* str, size_t len) const;
+
+ text_attrs attrs_for_ident(intern_string_t str) const
+ {
+ return this->attrs_for_ident(str.get(), str.size());
+ }
+
+ text_attrs attrs_for_ident(const std::string& str) const
+ {
+ return this->attrs_for_ident(str.c_str(), str.length());
+ }
+
+ text_attrs attrs_for_level(log_level_t level) const
+ {
+ return this->vc_level_attrs[level].ra_normal;
+ }
+
+ int ensure_color_pair(short fg, short bg);
+
+ int ensure_color_pair(nonstd::optional<short> fg,
+ nonstd::optional<short> bg);
+
+ int ensure_color_pair(const styling::color_unit& fg,
+ const styling::color_unit& bg);
+
+ static constexpr short MATCH_COLOR_DEFAULT = -1;
+ static constexpr short MATCH_COLOR_SEMANTIC = -10;
+
+ nonstd::optional<short> match_color(const styling::color_unit& color) const;
+
+ short ansi_to_theme_color(short ansi_fg) const
+ {
+ return this->vc_ansi_to_theme[ansi_fg];
+ }
+
+ std::unordered_map<std::string, string_attr_pair> vc_class_to_role;
+
+ static bool initialized;
+
+private:
+ static term_color_palette* vc_active_palette;
+
+ /** Private constructor that initializes the member fields. */
+ view_colors();
+
+ struct dyn_pair {
+ int dp_color_pair;
+ };
+
+ struct role_attrs {
+ text_attrs ra_normal;
+ text_attrs ra_reverse;
+ intern_string_t ra_class_name;
+ };
+
+ role_attrs to_attrs(const lnav_theme& lt,
+ const positioned_property<style_config>& sc,
+ lnav_config_listener::error_reporter& reporter);
+
+ role_attrs vc_level_attrs[LEVEL__MAX];
+
+ /** Map of role IDs to attribute values. */
+ role_attrs vc_role_attrs[lnav::enums::to_underlying(role_t::VCR__MAX)];
+ short vc_ansi_to_theme[8];
+ short vc_highlight_colors[HI_COLOR_COUNT];
+ int vc_color_pair_end{0};
+ cache::lru_cache<std::pair<short, short>, dyn_pair> vc_dyn_pairs;
+};
+
+enum class mouse_button_t {
+ BUTTON_LEFT,
+ BUTTON_MIDDLE,
+ BUTTON_RIGHT,
+
+ BUTTON_SCROLL_UP,
+ BUTTON_SCROLL_DOWN,
+};
+
+enum class mouse_button_state_t {
+ BUTTON_STATE_PRESSED,
+ BUTTON_STATE_DRAGGED,
+ BUTTON_STATE_RELEASED,
+};
+
+struct mouse_event {
+ mouse_event(mouse_button_t button = mouse_button_t::BUTTON_LEFT,
+ mouse_button_state_t state
+ = mouse_button_state_t::BUTTON_STATE_PRESSED,
+ int x = -1,
+ int y = -1)
+ : me_button(button), me_state(state), me_x(x), me_y(y)
+ {
+ }
+
+ mouse_button_t me_button;
+ mouse_button_state_t me_state;
+ struct timeval me_time {};
+ int me_x;
+ int me_y;
+};
+
+/**
+ * Interface for "view" classes that will update a curses(3) display.
+ */
+class view_curses {
+public:
+ virtual ~view_curses() = default;
+
+ /**
+ * Update the curses display.
+ */
+ virtual void do_update()
+ {
+ this->vc_needs_update = false;
+
+ if (!this->vc_visible) {
+ return;
+ }
+
+ for (auto* child : this->vc_children) {
+ child->do_update();
+ }
+ }
+
+ virtual bool handle_mouse(mouse_event& me) { return false; }
+
+ void set_needs_update()
+ {
+ this->vc_needs_update = true;
+ for (auto* child : this->vc_children) {
+ child->set_needs_update();
+ }
+ }
+
+ bool get_needs_update() const { return this->vc_needs_update; }
+
+ view_curses& add_child_view(view_curses* child)
+ {
+ this->vc_children.push_back(child);
+
+ return *this;
+ }
+
+ void set_default_role(role_t role) { this->vc_default_role = role; }
+
+ void set_visible(bool value) { this->vc_visible = value; }
+
+ bool is_visible() const { return this->vc_visible; }
+
+ void set_width(long width) { this->vc_width = width; }
+
+ long get_width() const { return this->vc_width; }
+
+ static void awaiting_user_input();
+
+ static size_t mvwattrline(WINDOW* window,
+ int y,
+ int x,
+ attr_line_t& al,
+ const struct line_range& lr,
+ role_t base_role = role_t::VCR_TEXT);
+
+protected:
+ bool vc_visible{true};
+ /** Flag to indicate if a display update is needed. */
+ bool vc_needs_update{true};
+ long vc_width;
+ std::vector<view_curses*> vc_children;
+ role_t vc_default_role{role_t::VCR_TEXT};
+};
+
+template<class T>
+class view_stack : public view_curses {
+public:
+ using iterator = typename std::vector<T*>::iterator;
+
+ nonstd::optional<T*> top()
+ {
+ if (this->vs_views.empty()) {
+ return nonstd::nullopt;
+ }
+ return this->vs_views.back();
+ }
+
+ void do_update() override
+ {
+ if (!this->vc_visible) {
+ return;
+ }
+
+ this->top() | [this](T* vc) {
+ if (this->vc_needs_update) {
+ vc->set_needs_update();
+ }
+ vc->do_update();
+ };
+
+ view_curses::do_update();
+
+ this->vc_needs_update = false;
+ }
+
+ void push_back(T* view)
+ {
+ this->vs_views.push_back(view);
+ if (this->vs_change_handler) {
+ this->vs_change_handler(view);
+ }
+ this->set_needs_update();
+ }
+
+ void pop_back()
+ {
+ this->vs_views.pop_back();
+ if (!this->vs_views.empty() && this->vs_change_handler) {
+ this->vs_change_handler(this->vs_views.back());
+ }
+ this->set_needs_update();
+ }
+
+ iterator find(T* view) const
+ {
+ return std::find(this->begin(), this->end(), view);
+ }
+
+ iterator begin() { return this->vs_views.begin(); }
+
+ iterator end() { return this->vs_views.end(); }
+
+ size_t size() const { return this->vs_views.size(); }
+
+ bool empty() const { return this->vs_views.empty(); }
+
+ std::function<void(T*)> vs_change_handler;
+
+private:
+ std::vector<T*> vs_views;
+};
+
+#endif
diff --git a/src/view_helpers.cc b/src/view_helpers.cc
new file mode 100644
index 0000000..465f9e3
--- /dev/null
+++ b/src/view_helpers.cc
@@ -0,0 +1,1214 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "view_helpers.hh"
+
+#include "base/humanize.hh"
+#include "base/itertools.hh"
+#include "config.h"
+#include "document.sections.hh"
+#include "environ_vtab.hh"
+#include "filter_sub_source.hh"
+#include "help-md.h"
+#include "intervaltree/IntervalTree.h"
+#include "lnav.hh"
+#include "lnav.indexing.hh"
+#include "md2attr_line.hh"
+#include "md4cpp.hh"
+#include "pretty_printer.hh"
+#include "shlex.hh"
+#include "sql_help.hh"
+#include "sql_util.hh"
+#include "static_file_vtab.hh"
+#include "view_helpers.crumbs.hh"
+#include "view_helpers.examples.hh"
+#include "view_helpers.hist.hh"
+#include "vtab_module.hh"
+
+using namespace std::chrono_literals;
+using namespace lnav::roles::literals;
+
+const char* lnav_view_strings[LNV__MAX + 1] = {
+ "log",
+ "text",
+ "help",
+ "histogram",
+ "db",
+ "schema",
+ "pretty",
+ "spectro",
+
+ nullptr,
+};
+
+const char* lnav_view_titles[LNV__MAX] = {
+ "LOG",
+ "TEXT",
+ "HELP",
+ "HIST",
+ "DB",
+ "SCHEMA",
+ "PRETTY",
+ "SPECTRO",
+};
+
+nonstd::optional<lnav_view_t>
+view_from_string(const char* name)
+{
+ if (name == nullptr) {
+ return nonstd::nullopt;
+ }
+
+ auto* view_name_iter
+ = std::find_if(std::begin(lnav_view_strings),
+ std::end(lnav_view_strings),
+ [&](const char* v) {
+ return v != nullptr && strcasecmp(v, name) == 0;
+ });
+
+ if (view_name_iter == std::end(lnav_view_strings)) {
+ return nonstd::nullopt;
+ }
+
+ return lnav_view_t(view_name_iter - lnav_view_strings);
+}
+
+static void
+open_schema_view()
+{
+ textview_curses* schema_tc = &lnav_data.ld_views[LNV_SCHEMA];
+ std::string schema;
+
+ dump_sqlite_schema(lnav_data.ld_db, schema);
+
+ schema += "\n\n-- Virtual Table Definitions --\n\n";
+ schema += ENVIRON_CREATE_STMT;
+ schema += STATIC_FILE_CREATE_STMT;
+ schema += vtab_module_schemas;
+ for (const auto& vtab_iter : *lnav_data.ld_vtab_manager) {
+ schema += "\n" + vtab_iter.second->get_table_statement();
+ }
+
+ delete schema_tc->get_sub_source();
+
+ auto* pts = new plain_text_source(schema);
+ pts->set_text_format(text_format_t::TF_SQL);
+
+ schema_tc->set_sub_source(pts);
+ schema_tc->redo_search();
+}
+
+class pretty_sub_source : public plain_text_source {
+public:
+ void text_crumbs_for_line(int line,
+ std::vector<breadcrumb::crumb>& crumbs) override
+ {
+ text_sub_source::text_crumbs_for_line(line, crumbs);
+
+ if (line < 0 || static_cast<size_t>(line) > this->tds_lines.size()) {
+ return;
+ }
+
+ const auto& tl = this->tds_lines[line];
+ const auto initial_size = crumbs.size();
+ lnav::document::hier_node* root_node{nullptr};
+
+ this->pss_hier_tree->template visit_overlapping(
+ tl.tl_offset,
+ [&root_node](const auto& hier_iv) { root_node = hier_iv.value; });
+ this->pss_interval_tree->visit_overlapping(
+ tl.tl_offset,
+ tl.tl_offset + tl.tl_value.length(),
+ [&crumbs, root_node, this, initial_size](const auto& iv) {
+ auto path = crumbs | lnav::itertools::skip(initial_size)
+ | lnav::itertools::map(&breadcrumb::crumb::c_key)
+ | lnav::itertools::append(iv.value);
+ auto poss_provider = [root_node, path]() {
+ std::vector<breadcrumb::possibility> retval;
+ auto curr_node = lnav::document::hier_node::lookup_path(
+ root_node, path);
+ if (curr_node) {
+ auto* parent_node = curr_node.value()->hn_parent;
+
+ if (parent_node != nullptr) {
+ for (const auto& sibling :
+ parent_node->hn_named_children)
+ {
+ retval.template emplace_back(sibling.first);
+ }
+ }
+ }
+ return retval;
+ };
+ auto path_performer =
+ [this, root_node, path](
+ const breadcrumb::crumb::key_t& value) {
+ auto curr_node = lnav::document::hier_node::lookup_path(
+ root_node, path);
+ if (!curr_node) {
+ return;
+ }
+ auto* parent_node = curr_node.value()->hn_parent;
+
+ if (parent_node == nullptr) {
+ return;
+ }
+ value.template match(
+ [this, parent_node](const std::string& str) {
+ auto sib_iter
+ = parent_node->hn_named_children.find(str);
+ if (sib_iter
+ != parent_node->hn_named_children.end()) {
+ this->line_for_offset(
+ sib_iter->second->hn_start)
+ | [](const auto new_top) {
+ lnav_data.ld_views[LNV_PRETTY]
+ .set_top(new_top);
+ };
+ }
+ },
+ [this, parent_node](size_t index) {
+ if (index >= parent_node->hn_children.size()) {
+ return;
+ }
+ auto sib
+ = parent_node->hn_children[index].get();
+ this->line_for_offset(sib->hn_start) |
+ [](const auto new_top) {
+ lnav_data.ld_views[LNV_PRETTY].set_top(
+ new_top);
+ };
+ });
+ };
+ crumbs.template emplace_back(iv.value,
+ std::move(poss_provider),
+ std::move(path_performer));
+ auto curr_node
+ = lnav::document::hier_node::lookup_path(root_node, path);
+ if (curr_node
+ && curr_node.value()->hn_parent->hn_children.size()
+ != curr_node.value()
+ ->hn_parent->hn_named_children.size())
+ {
+ auto node = lnav::document::hier_node::lookup_path(
+ root_node, path);
+
+ crumbs.back().c_expected_input
+ = curr_node.value()
+ ->hn_parent->hn_named_children.empty()
+ ? breadcrumb::crumb::expected_input_t::index
+ : breadcrumb::crumb::expected_input_t::index_or_exact;
+ crumbs.back().with_possible_range(
+ node | lnav::itertools::map([](const auto hn) {
+ return hn->hn_parent->hn_children.size();
+ })
+ | lnav::itertools::unwrap_or(size_t{0}));
+ }
+ });
+
+ auto path = crumbs | lnav::itertools::skip(initial_size)
+ | lnav::itertools::map(&breadcrumb::crumb::c_key);
+ auto node = lnav::document::hier_node::lookup_path(root_node, path);
+
+ if (node && !node.value()->hn_children.empty()) {
+ auto poss_provider = [curr_node = node.value()]() {
+ std::vector<breadcrumb::possibility> retval;
+ for (const auto& child : curr_node->hn_named_children) {
+ retval.template emplace_back(child.first);
+ }
+ return retval;
+ };
+ auto path_performer = [this, curr_node = node.value()](
+ const breadcrumb::crumb::key_t& value) {
+ value.template match(
+ [this, curr_node](const std::string& str) {
+ auto child_iter
+ = curr_node->hn_named_children.find(str);
+ if (child_iter != curr_node->hn_named_children.end()) {
+ this->line_for_offset(child_iter->second->hn_start)
+ | [](const auto new_top) {
+ lnav_data.ld_views[LNV_PRETTY].set_top(
+ new_top);
+ };
+ }
+ },
+ [this, curr_node](size_t index) {
+ auto* child = curr_node->hn_children[index].get();
+ this->line_for_offset(child->hn_start) |
+ [](const auto new_top) {
+ lnav_data.ld_views[LNV_PRETTY].set_top(new_top);
+ };
+ });
+ };
+ crumbs.emplace_back("", "\u22ef", poss_provider, path_performer);
+ crumbs.back().c_expected_input
+ = node.value()->hn_named_children.empty()
+ ? breadcrumb::crumb::expected_input_t::index
+ : breadcrumb::crumb::expected_input_t::index_or_exact;
+ }
+ }
+
+ using hier_tree_t
+ = interval_tree::IntervalTree<file_off_t, lnav::document::hier_node*>;
+ using hier_interval_t
+ = interval_tree::Interval<file_off_t, lnav::document::hier_node*>;
+
+ std::shared_ptr<lnav::document::sections_tree_t> pss_interval_tree;
+ std::vector<std::unique_ptr<lnav::document::hier_node>> pss_hier_nods;
+ std::shared_ptr<hier_tree_t> pss_hier_tree;
+};
+
+static void
+open_pretty_view()
+{
+ static const char* NOTHING_MSG = "Nothing to pretty-print";
+
+ auto* top_tc = *lnav_data.ld_view_stack.top();
+ auto* pretty_tc = &lnav_data.ld_views[LNV_PRETTY];
+ auto* log_tc = &lnav_data.ld_views[LNV_LOG];
+ auto* text_tc = &lnav_data.ld_views[LNV_TEXT];
+ attr_line_t full_text;
+
+ delete pretty_tc->get_sub_source();
+ pretty_tc->set_sub_source(nullptr);
+ if (top_tc->get_inner_height() == 0) {
+ pretty_tc->set_sub_source(new plain_text_source(NOTHING_MSG));
+ return;
+ }
+
+ std::vector<lnav::document::section_interval_t> all_intervals;
+ std::vector<std::unique_ptr<lnav::document::hier_node>> hier_nodes;
+ std::vector<pretty_sub_source::hier_interval_t> hier_tree_vec;
+ if (top_tc == log_tc) {
+ auto& lss = lnav_data.ld_log_source;
+ bool first_line = true;
+
+ for (auto vl = log_tc->get_top(); vl <= log_tc->get_bottom(); ++vl) {
+ content_line_t cl = lss.at(vl);
+ auto lf = lss.find(cl);
+ auto ll = lf->begin() + cl;
+ shared_buffer_ref sbr;
+
+ if (!first_line && !ll->is_message()) {
+ continue;
+ }
+ auto ll_start = lf->message_start(ll);
+ attr_line_t al;
+
+ vl -= vis_line_t(std::distance(ll_start, ll));
+ lss.text_value_for_line(
+ *log_tc,
+ vl,
+ al.get_string(),
+ text_sub_source::RF_FULL | text_sub_source::RF_REWRITE);
+ lss.text_attrs_for_line(*log_tc, vl, al.get_attrs());
+ scrub_ansi_string(al.get_string(), &al.get_attrs());
+ if (log_tc->get_hide_fields()) {
+ al.apply_hide();
+ }
+
+ const auto orig_lr
+ = find_string_attr_range(al.get_attrs(), &SA_ORIGINAL_LINE);
+ const auto body_lr
+ = find_string_attr_range(al.get_attrs(), &SA_BODY);
+ auto orig_al = al.subline(orig_lr.lr_start, orig_lr.length());
+ auto prefix_al = al.subline(0, orig_lr.lr_start);
+ attr_line_t pretty_al;
+ std::vector<attr_line_t> pretty_lines;
+ data_scanner ds(orig_al.get_string(),
+ body_lr.is_valid()
+ ? body_lr.lr_start - orig_lr.lr_start
+ : orig_lr.lr_start);
+ pretty_printer pp(&ds, orig_al.get_attrs());
+ auto start_off = full_text.length();
+
+ if (body_lr.is_valid()) {
+ // TODO: dump more details of the line in the output.
+ pp.append_to(pretty_al);
+ } else {
+ pretty_al = orig_al;
+ }
+
+ pretty_al.split_lines(pretty_lines);
+
+ auto curr_intervals = pp.take_intervals();
+ auto line_hier_root = pp.take_hier_root();
+ auto line_off = 0;
+ for (auto& pretty_line : pretty_lines) {
+ if (pretty_line.empty() && &pretty_line == &pretty_lines.back())
+ {
+ break;
+ }
+ pretty_line.insert(0, prefix_al);
+ pretty_line.append("\n");
+ for (auto& interval : curr_intervals) {
+ if (line_off <= interval.start) {
+ interval.start += prefix_al.length();
+ interval.stop += prefix_al.length();
+ } else if (line_off < interval.stop) {
+ interval.stop += prefix_al.length();
+ }
+ }
+ lnav::document::hier_node::depth_first(
+ line_hier_root.get(),
+ [line_off, prefix_len = prefix_al.length()](auto* hn) {
+ if (line_off <= hn->hn_start) {
+ hn->hn_start += prefix_len;
+ }
+ });
+ line_off += pretty_line.length();
+ full_text.append(pretty_line);
+ }
+
+ first_line = false;
+ for (auto& interval : curr_intervals) {
+ interval.start += start_off;
+ interval.stop += start_off;
+ }
+ lnav::document::hier_node::depth_first(
+ line_hier_root.get(),
+ [start_off](auto* hn) { hn->hn_start += start_off; });
+ hier_nodes.emplace_back(std::move(line_hier_root));
+ hier_tree_vec.emplace_back(
+ start_off, full_text.length(), hier_nodes.back().get());
+ all_intervals.insert(
+ all_intervals.end(),
+ std::make_move_iterator(curr_intervals.begin()),
+ std::make_move_iterator(curr_intervals.end()));
+ }
+
+ if (!full_text.empty()) {
+ full_text.erase(full_text.length() - 1, 1);
+ }
+ } else if (top_tc == text_tc) {
+ if (text_tc->listview_rows(*text_tc)) {
+ std::vector<attr_line_t> rows;
+ rows.resize(text_tc->get_bottom() - text_tc->get_top() + 1);
+ text_tc->listview_value_for_rows(
+ *text_tc, text_tc->get_top(), rows);
+ attr_line_t orig_al;
+
+ for (const auto& row : rows) {
+ orig_al.append(row);
+ }
+
+ data_scanner ds(orig_al.get_string());
+ string_attrs_t sa;
+ pretty_printer pp(&ds, orig_al.get_attrs());
+
+ pp.append_to(full_text);
+ all_intervals = pp.take_intervals();
+ hier_nodes.emplace_back(pp.take_hier_root());
+ hier_tree_vec.emplace_back(
+ 0, full_text.length(), hier_nodes.back().get());
+ }
+ }
+ auto* pts = new pretty_sub_source();
+ pts->pss_interval_tree = std::make_shared<lnav::document::sections_tree_t>(
+ std::move(all_intervals));
+ pts->pss_hier_nods = std::move(hier_nodes);
+ pts->pss_hier_tree = std::make_shared<pretty_sub_source::hier_tree_t>(
+ std::move(hier_tree_vec));
+ pts->replace_with(full_text);
+ pretty_tc->set_sub_source(pts);
+ if (lnav_data.ld_last_pretty_print_top != log_tc->get_top()) {
+ pretty_tc->set_top(0_vl);
+ }
+ lnav_data.ld_last_pretty_print_top = log_tc->get_top();
+ pretty_tc->redo_search();
+}
+
+template<typename T>
+static void
+ignore_case(const T&)
+{
+}
+
+static void
+build_all_help_text()
+{
+ if (!lnav_data.ld_help_source.empty()) {
+ return;
+ }
+
+ shlex lexer(help_md.to_string_fragment());
+ std::string sub_help_text;
+
+ lexer.with_ignore_quotes(true).eval(
+ sub_help_text, lnav_data.ld_exec_context.ec_global_vars);
+
+ md2attr_line mdal;
+ auto parse_res = md4cpp::parse(sub_help_text, mdal);
+ attr_line_t all_help_text = parse_res.unwrap();
+
+ std::map<std::string, help_text*> sql_funcs;
+ std::map<std::string, help_text*> sql_keywords;
+
+ for (const auto& iter : sqlite_function_help) {
+ switch (iter.second->ht_context) {
+ case help_context_t::HC_SQL_FUNCTION:
+ case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION:
+ sql_funcs[iter.second->ht_name] = iter.second;
+ break;
+ case help_context_t::HC_SQL_KEYWORD:
+ sql_keywords[iter.second->ht_name] = iter.second;
+ break;
+ default:
+ break;
+ }
+ }
+
+ all_help_text.append("\n").append("Command Reference"_h2);
+
+ for (const auto& cmd : lnav_commands) {
+ if (cmd.second->c_help.ht_summary == nullptr) {
+ continue;
+ }
+ all_help_text.append(2, '\n');
+ format_help_text_for_term(cmd.second->c_help, 70, all_help_text);
+ if (!cmd.second->c_help.ht_example.empty()) {
+ all_help_text.append("\n");
+ format_example_text_for_term(
+ cmd.second->c_help, eval_example, 90, all_help_text);
+ }
+ }
+
+ all_help_text.append("\n").append("SQL Reference"_h2);
+
+ for (const auto& iter : sql_funcs) {
+ all_help_text.append(2, '\n');
+ format_help_text_for_term(*iter.second, 70, all_help_text);
+ if (!iter.second->ht_example.empty()) {
+ all_help_text.append(1, '\n');
+ format_example_text_for_term(
+ *iter.second, eval_example, 90, all_help_text);
+ }
+ }
+
+ for (const auto& iter : sql_keywords) {
+ all_help_text.append(2, '\n');
+ format_help_text_for_term(*iter.second, 70, all_help_text);
+ if (!iter.second->ht_example.empty()) {
+ all_help_text.append(1, '\n');
+ format_example_text_for_term(
+ *iter.second, eval_example, 79, all_help_text);
+ }
+ }
+
+ lnav_data.ld_help_source.replace_with(all_help_text);
+ lnav_data.ld_views[LNV_HELP].redo_search();
+}
+
+bool
+handle_winch()
+{
+ static auto* filter_source = injector::get<filter_sub_source*>();
+
+ if (!lnav_data.ld_winched) {
+ return false;
+ }
+
+ struct winsize size;
+
+ lnav_data.ld_winched = false;
+
+ if (ioctl(fileno(stdout), TIOCGWINSZ, &size) == 0) {
+ resizeterm(size.ws_row, size.ws_col);
+ }
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->do_update();
+ lnav_data.ld_rl_view->window_change();
+ }
+ filter_source->fss_editor->window_change();
+ for (auto& sc : lnav_data.ld_status) {
+ sc.window_change();
+ }
+ lnav_data.ld_view_stack.set_needs_update();
+ lnav_data.ld_doc_view.set_needs_update();
+ lnav_data.ld_example_view.set_needs_update();
+ lnav_data.ld_match_view.set_needs_update();
+ lnav_data.ld_filter_view.set_needs_update();
+ lnav_data.ld_files_view.set_needs_update();
+ lnav_data.ld_spectro_details_view.set_needs_update();
+ lnav_data.ld_user_message_view.set_needs_update();
+
+ return true;
+}
+
+void
+layout_views()
+{
+ unsigned long width, height;
+
+ getmaxyx(lnav_data.ld_window, height, width);
+ int doc_height;
+ bool doc_side_by_side = width > (90 + 60);
+ bool preview_status_open
+ = !lnav_data.ld_preview_status_source.get_description().empty();
+ bool filter_status_open = false;
+ auto is_spectro = false;
+
+ lnav_data.ld_view_stack.top() | [&](auto tc) {
+ is_spectro = (tc == &lnav_data.ld_views[LNV_SPECTRO]);
+
+ text_sub_source* tss = tc->get_sub_source();
+
+ if (tss == nullptr) {
+ return;
+ }
+
+ if (tss->tss_supports_filtering) {
+ filter_status_open = true;
+ }
+ };
+
+ if (doc_side_by_side) {
+ doc_height = std::max(lnav_data.ld_doc_source.text_line_count(),
+ lnav_data.ld_example_source.text_line_count());
+ } else {
+ doc_height = lnav_data.ld_doc_source.text_line_count()
+ + lnav_data.ld_example_source.text_line_count();
+ }
+
+ int preview_height = lnav_data.ld_preview_hidden
+ ? 0
+ : lnav_data.ld_preview_source.text_line_count();
+ int match_rows = lnav_data.ld_match_source.text_line_count();
+ int match_height = std::min((unsigned long) match_rows, (height - 4) / 2);
+
+ lnav_data.ld_match_view.set_height(vis_line_t(match_height));
+
+ int um_rows = lnav_data.ld_user_message_source.text_line_count();
+ if (um_rows > 0
+ && std::chrono::steady_clock::now()
+ > lnav_data.ld_user_message_expiration)
+ {
+ lnav_data.ld_user_message_source.clear();
+ um_rows = 0;
+ }
+ int um_height = std::min((unsigned long) um_rows, (height - 4) / 2);
+
+ lnav_data.ld_user_message_view.set_height(vis_line_t(um_height));
+
+ if (doc_height + 14
+ > ((int) height - match_height - um_height - preview_height - 2))
+ {
+ preview_height = 0;
+ preview_status_open = false;
+ }
+
+ if (doc_height + 14 > ((int) height - match_height - um_height - 2)) {
+ doc_height = lnav_data.ld_doc_source.text_line_count();
+ if (doc_height + 14 > ((int) height - match_height - um_height - 2)) {
+ doc_height = 0;
+ }
+ }
+
+ bool doc_open = doc_height > 0;
+ bool filters_open = (lnav_data.ld_mode == ln_mode_t::FILTER
+ || lnav_data.ld_mode == ln_mode_t::FILES
+ || lnav_data.ld_mode == ln_mode_t::SEARCH_FILTERS
+ || lnav_data.ld_mode == ln_mode_t::SEARCH_FILES)
+ && !preview_status_open && !doc_open;
+ bool breadcrumb_open = (lnav_data.ld_mode == ln_mode_t::BREADCRUMBS);
+ int filter_height = filters_open ? 5 : 0;
+
+ int bottom_height = (doc_open ? 1 : 0) + doc_height
+ + (preview_status_open ? 1 : 0) + preview_height + 1 // bottom status
+ + match_height + um_height + lnav_data.ld_rl_view->get_height()
+ + (is_spectro && !doc_open ? 5 : 0);
+
+ for (auto& tc : lnav_data.ld_views) {
+ tc.set_height(vis_line_t(-(bottom_height + (filter_status_open ? 1 : 0)
+ + (filters_open ? 1 : 0) + filter_height)));
+ }
+ lnav_data.ld_status[LNS_FILTER].set_visible(filter_status_open);
+ lnav_data.ld_status[LNS_FILTER].set_enabled(filters_open);
+ lnav_data.ld_status[LNS_FILTER].set_top(
+ -(bottom_height + filter_height + 1 + (filters_open ? 1 : 0)));
+ lnav_data.ld_status[LNS_FILTER_HELP].set_visible(filters_open);
+ lnav_data.ld_status[LNS_FILTER_HELP].set_top(
+ -(bottom_height + filter_height + 1));
+ lnav_data.ld_status[LNS_BOTTOM].set_top(-(match_height + um_height + 2));
+ lnav_data.ld_status[LNS_BOTTOM].set_enabled(!filters_open
+ && !breadcrumb_open);
+ lnav_data.ld_status[LNS_DOC].set_top(height - bottom_height);
+ lnav_data.ld_status[LNS_DOC].set_visible(doc_open);
+ lnav_data.ld_status[LNS_PREVIEW].set_top(height - bottom_height
+ + (doc_open ? 1 : 0) + doc_height);
+ lnav_data.ld_status[LNS_PREVIEW].set_visible(preview_status_open);
+ lnav_data.ld_status[LNS_SPECTRO].set_top(height - bottom_height - 1);
+ lnav_data.ld_status[LNS_SPECTRO].set_visible(is_spectro);
+ lnav_data.ld_status[LNS_SPECTRO].set_enabled(lnav_data.ld_mode
+ == ln_mode_t::SPECTRO_DETAILS);
+
+ if (!doc_open || doc_side_by_side) {
+ lnav_data.ld_doc_view.set_height(vis_line_t(doc_height));
+ } else {
+ lnav_data.ld_doc_view.set_height(
+ vis_line_t(lnav_data.ld_doc_source.text_line_count()));
+ }
+ lnav_data.ld_doc_view.set_y(height - bottom_height + 1);
+
+ if (!doc_open || doc_side_by_side) {
+ lnav_data.ld_example_view.set_height(vis_line_t(doc_height));
+ lnav_data.ld_example_view.set_x(doc_open ? 90 : 0);
+ lnav_data.ld_example_view.set_y(height - bottom_height + 1);
+ } else {
+ lnav_data.ld_example_view.set_height(
+ vis_line_t(lnav_data.ld_example_source.text_line_count()));
+ lnav_data.ld_example_view.set_x(0);
+ lnav_data.ld_example_view.set_y(
+ height - bottom_height + lnav_data.ld_doc_view.get_height() + 1);
+ }
+
+ lnav_data.ld_filter_view.set_height(vis_line_t(filter_height));
+ lnav_data.ld_filter_view.set_y(height - bottom_height - filter_height);
+ lnav_data.ld_filter_view.set_width(width);
+
+ lnav_data.ld_files_view.set_height(vis_line_t(filter_height));
+ lnav_data.ld_files_view.set_y(height - bottom_height - filter_height);
+ lnav_data.ld_files_view.set_width(width);
+
+ lnav_data.ld_preview_view.set_height(vis_line_t(preview_height));
+ lnav_data.ld_preview_view.set_y(height - bottom_height + 1
+ + (doc_open ? 1 : 0) + doc_height);
+ lnav_data.ld_user_message_view.set_y(
+ height - lnav_data.ld_rl_view->get_height() - match_height - um_height);
+
+ lnav_data.ld_spectro_details_view.set_y(height - bottom_height);
+ lnav_data.ld_spectro_details_view.set_height(
+ is_spectro && !doc_open ? 5_vl : 0_vl);
+ lnav_data.ld_spectro_details_view.set_width(width);
+ lnav_data.ld_spectro_details_view.set_title("spectro-details");
+
+ lnav_data.ld_match_view.set_y(height - lnav_data.ld_rl_view->get_height()
+ - match_height);
+ lnav_data.ld_rl_view->set_width(width);
+}
+
+void
+update_hits(textview_curses* tc)
+{
+ if (isendwin()) {
+ return;
+ }
+
+ auto top_tc = lnav_data.ld_view_stack.top();
+
+ if (top_tc && tc == *top_tc) {
+ lnav_data.ld_bottom_source.update_hits(tc);
+
+ if (lnav_data.ld_mode == ln_mode_t::SEARCH) {
+ const auto MAX_MATCH_COUNT = 10_vl;
+ const auto PREVIEW_SIZE = MAX_MATCH_COUNT + 1_vl;
+
+ int preview_count = 0;
+
+ vis_bookmarks& bm = tc->get_bookmarks();
+ const auto& bv = bm[&textview_curses::BM_SEARCH];
+ auto vl = tc->get_top();
+ unsigned long width;
+ vis_line_t height;
+ attr_line_t all_matches;
+ char linebuf[64];
+ int last_line = tc->get_inner_height();
+
+ snprintf(linebuf, sizeof(linebuf), "%d", last_line);
+ auto max_line_width = static_cast<int>(strlen(linebuf));
+
+ tc->get_dimensions(height, width);
+ vl += height;
+ if (vl > PREVIEW_SIZE) {
+ vl -= PREVIEW_SIZE;
+ }
+
+ auto prev_vl = bv.prev(tc->get_top());
+
+ if (prev_vl) {
+ if (prev_vl.value() < 0_vl
+ || prev_vl.value() >= tc->get_inner_height())
+ {
+ log_error("stale search bookmark for %s: %d",
+ tc->get_title().c_str(),
+ prev_vl.value());
+ } else {
+ attr_line_t al;
+
+ tc->textview_value_for_row(prev_vl.value(), al);
+ if (preview_count > 0) {
+ all_matches.append("\n");
+ }
+ snprintf(linebuf,
+ sizeof(linebuf),
+ "L%*d: ",
+ max_line_width,
+ (int) prev_vl.value());
+ all_matches.append(linebuf).append(al);
+ preview_count += 1;
+ }
+ }
+
+ nonstd::optional<vis_line_t> next_vl;
+ while ((next_vl = bv.next(vl)) && preview_count < MAX_MATCH_COUNT) {
+ if (next_vl.value() < 0_vl
+ || next_vl.value() >= tc->get_inner_height())
+ {
+ log_error("stale search bookmark for %s: %d",
+ tc->get_title().c_str(),
+ next_vl.value());
+ break;
+ }
+
+ attr_line_t al;
+
+ vl = next_vl.value();
+ tc->textview_value_for_row(vl, al);
+ if (preview_count > 0) {
+ all_matches.append("\n");
+ }
+ snprintf(linebuf,
+ sizeof(linebuf),
+ "L%*d: ",
+ max_line_width,
+ (int) vl);
+ all_matches.append(linebuf).append(al);
+ preview_count += 1;
+ }
+
+ if (preview_count > 0) {
+ lnav_data.ld_preview_status_source.get_description().set_value(
+ "Matching lines for search");
+ lnav_data.ld_preview_source.replace_with(all_matches)
+ .set_text_format(text_format_t::TF_UNKNOWN);
+ lnav_data.ld_preview_view.set_needs_update();
+ }
+ }
+ }
+}
+
+static std::unordered_map<std::string, attr_line_t> EXAMPLE_RESULTS;
+
+void
+execute_examples()
+{
+ db_label_source& dls = lnav_data.ld_db_row_source;
+ db_overlay_source& dos = lnav_data.ld_db_overlay;
+ textview_curses& db_tc = lnav_data.ld_views[LNV_DB];
+
+ for (auto& help_iter : sqlite_function_help) {
+ struct help_text& ht = *(help_iter.second);
+
+ for (auto& ex : ht.ht_example) {
+ std::string alt_msg;
+ attr_line_t result;
+
+ if (!ex.he_cmd) {
+ continue;
+ }
+
+ switch (ht.ht_context) {
+ case help_context_t::HC_SQL_KEYWORD:
+ case help_context_t::HC_SQL_INFIX:
+ case help_context_t::HC_SQL_FUNCTION:
+ case help_context_t::HC_SQL_TABLE_VALUED_FUNCTION: {
+ exec_context ec;
+
+ execute_sql(ec, ex.he_cmd, alt_msg);
+
+ if (dls.dls_rows.size() == 1 && dls.dls_rows[0].size() == 1)
+ {
+ result.append(dls.dls_rows[0][0]);
+ } else {
+ attr_line_t al;
+ dos.list_value_for_overlay(db_tc, 0, 1, 0_vl, al);
+ result.append(al);
+ for (int lpc = 0; lpc < (int) dls.text_line_count();
+ lpc++)
+ {
+ al.clear();
+ dls.text_value_for_line(
+ db_tc, lpc, al.get_string(), false);
+ dls.text_attrs_for_line(db_tc, lpc, al.get_attrs());
+ std::replace(al.get_string().begin(),
+ al.get_string().end(),
+ '\n',
+ ' ');
+ result.append("\n").append(al);
+ }
+ }
+
+ EXAMPLE_RESULTS[ex.he_cmd] = result;
+
+ log_debug("example: %s", ex.he_cmd);
+ log_debug("example result: %s",
+ result.get_string().c_str());
+ break;
+ }
+ default:
+ log_warning("Not executing example: %s", ex.he_cmd);
+ break;
+ }
+ }
+ }
+
+ dls.clear();
+}
+
+attr_line_t
+eval_example(const help_text& ht, const help_example& ex)
+{
+ auto iter = EXAMPLE_RESULTS.find(ex.he_cmd);
+
+ if (iter != EXAMPLE_RESULTS.end()) {
+ return iter->second;
+ }
+
+ return "";
+}
+
+bool
+toggle_view(textview_curses* toggle_tc)
+{
+ textview_curses* tc = lnav_data.ld_view_stack.top().value_or(nullptr);
+ bool retval = false;
+
+ require(toggle_tc != nullptr);
+ require(toggle_tc >= &lnav_data.ld_views[0]);
+ require(toggle_tc < &lnav_data.ld_views[LNV__MAX]);
+
+ if (tc == toggle_tc) {
+ if (lnav_data.ld_view_stack.size() == 1) {
+ return false;
+ }
+ lnav_data.ld_last_view = tc;
+ lnav_data.ld_view_stack.pop_back();
+ } else {
+ if (toggle_tc == &lnav_data.ld_views[LNV_LOG]
+ || toggle_tc == &lnav_data.ld_views[LNV_TEXT])
+ {
+ rescan_files(true);
+ rebuild_indexes_repeatedly();
+ } else if (toggle_tc == &lnav_data.ld_views[LNV_SCHEMA]) {
+ open_schema_view();
+ } else if (toggle_tc == &lnav_data.ld_views[LNV_PRETTY]) {
+ open_pretty_view();
+ } else if (toggle_tc == &lnav_data.ld_views[LNV_HISTOGRAM]) {
+ // Rebuild to reflect changes in marks.
+ rebuild_hist();
+ } else if (toggle_tc == &lnav_data.ld_views[LNV_HELP]) {
+ build_all_help_text();
+ if (lnav_data.ld_rl_view != nullptr) {
+ lnav_data.ld_rl_view->set_alt_value(
+ HELP_MSG_1(q, "to return to the previous view"));
+ }
+ }
+ lnav_data.ld_last_view = nullptr;
+ lnav_data.ld_view_stack.push_back(toggle_tc);
+ retval = true;
+ }
+
+ return retval;
+}
+
+/**
+ * Ensure that the view is on the top of the view stack.
+ *
+ * @param expected_tc The text view that should be on top.
+ * @return True if the view was already on the top of the stack.
+ */
+bool
+ensure_view(textview_curses* expected_tc)
+{
+ textview_curses* tc = lnav_data.ld_view_stack.top().value_or(nullptr);
+ bool retval = true;
+
+ if (tc != expected_tc) {
+ toggle_view(expected_tc);
+ retval = false;
+ }
+ return retval;
+}
+
+bool
+ensure_view(lnav_view_t expected)
+{
+ require(expected >= 0);
+ require(expected < LNV__MAX);
+
+ return ensure_view(&lnav_data.ld_views[expected]);
+}
+
+nonstd::optional<vis_line_t>
+next_cluster(nonstd::optional<vis_line_t> (bookmark_vector<vis_line_t>::*f)(
+ vis_line_t) const,
+ const bookmark_type_t* bt,
+ const vis_line_t top)
+{
+ auto* tc = get_textview_for_mode(lnav_data.ld_mode);
+ auto& bm = tc->get_bookmarks();
+ auto& bv = bm[bt];
+ bool top_is_marked = binary_search(bv.begin(), bv.end(), top);
+ vis_line_t last_top(top), tc_height;
+ nonstd::optional<vis_line_t> new_top = top;
+ unsigned long tc_width;
+ int hit_count = 0;
+
+ tc->get_dimensions(tc_height, tc_width);
+
+ while ((new_top = (bv.*f)(new_top.value()))) {
+ int diff = new_top.value() - last_top;
+
+ hit_count += 1;
+ if (tc->is_selectable() || !top_is_marked || diff > 1) {
+ return new_top;
+ }
+ if (hit_count > 1 && std::abs(new_top.value() - top) >= tc_height) {
+ return vis_line_t(new_top.value() - diff);
+ }
+ if (diff < -1) {
+ last_top = new_top.value();
+ while ((new_top = (bv.*f)(new_top.value()))) {
+ if ((std::abs(last_top - new_top.value()) > 1)
+ || (hit_count > 1
+ && (std::abs(top - new_top.value()) >= tc_height)))
+ {
+ break;
+ }
+ last_top = new_top.value();
+ }
+ return last_top;
+ }
+ last_top = new_top.value();
+ }
+
+ if (last_top != top) {
+ return last_top;
+ }
+
+ return nonstd::nullopt;
+}
+
+bool
+moveto_cluster(nonstd::optional<vis_line_t> (bookmark_vector<vis_line_t>::*f)(
+ vis_line_t) const,
+ const bookmark_type_t* bt,
+ vis_line_t top)
+{
+ textview_curses* tc = get_textview_for_mode(lnav_data.ld_mode);
+ auto new_top = next_cluster(f, bt, top);
+
+ if (!new_top) {
+ new_top = next_cluster(f, bt, tc->get_selection());
+ }
+ if (new_top != -1) {
+ tc->get_sub_source()->get_location_history() |
+ [new_top](auto lh) { lh->loc_history_append(new_top.value()); };
+
+ if (tc->is_selectable()) {
+ tc->set_selection(new_top.value());
+ } else {
+ tc->set_top(new_top.value());
+ }
+ return true;
+ }
+
+ alerter::singleton().chime("unable to find next bookmark");
+
+ return false;
+}
+
+vis_line_t
+search_forward_from(textview_curses* tc)
+{
+ vis_line_t height, retval = tc->get_selection();
+
+ if (!tc->is_selectable()) {
+ auto& krh = lnav_data.ld_key_repeat_history;
+ unsigned long width;
+
+ tc->get_dimensions(height, width);
+
+ if (krh.krh_count > 1 && retval > (krh.krh_start_line + (1.5 * height)))
+ {
+ retval += vis_line_t(0.90 * height);
+ }
+ }
+
+ return retval;
+}
+
+textview_curses*
+get_textview_for_mode(ln_mode_t mode)
+{
+ switch (mode) {
+ case ln_mode_t::SEARCH_FILTERS:
+ case ln_mode_t::FILTER:
+ return &lnav_data.ld_filter_view;
+ case ln_mode_t::SEARCH_FILES:
+ case ln_mode_t::FILES:
+ return &lnav_data.ld_files_view;
+ case ln_mode_t::SPECTRO_DETAILS:
+ case ln_mode_t::SEARCH_SPECTRO_DETAILS:
+ return &lnav_data.ld_spectro_details_view;
+ default:
+ return *lnav_data.ld_view_stack.top();
+ }
+}
+
+hist_index_delegate::hist_index_delegate(hist_source2& hs, textview_curses& tc)
+ : hid_source(hs), hid_view(tc)
+{
+}
+
+void
+hist_index_delegate::index_start(logfile_sub_source& lss)
+{
+ this->hid_source.clear();
+}
+
+void
+hist_index_delegate::index_line(logfile_sub_source& lss,
+ logfile* lf,
+ logfile::iterator ll)
+{
+ if (ll->is_continued() || ll->get_time() == 0) {
+ return;
+ }
+
+ hist_source2::hist_type_t ht;
+
+ switch (ll->get_msg_level()) {
+ case LEVEL_FATAL:
+ case LEVEL_CRITICAL:
+ case LEVEL_ERROR:
+ ht = hist_source2::HT_ERROR;
+ break;
+ case LEVEL_WARNING:
+ ht = hist_source2::HT_WARNING;
+ break;
+ default:
+ ht = hist_source2::HT_NORMAL;
+ break;
+ }
+
+ this->hid_source.add_value(ll->get_time(), ht);
+ if (ll->is_marked() || ll->is_expr_marked()) {
+ this->hid_source.add_value(ll->get_time(), hist_source2::HT_MARK);
+ }
+}
+
+void
+hist_index_delegate::index_complete(logfile_sub_source& lss)
+{
+ this->hid_view.reload_data();
+ lnav_data.ld_views[LNV_SPECTRO].reload_data();
+}
+
+static std::vector<breadcrumb::possibility>
+view_title_poss()
+{
+ std::vector<breadcrumb::possibility> retval;
+
+ for (int view_index = 0; view_index < LNV__MAX; view_index++) {
+ attr_line_t display_value{lnav_view_titles[view_index]};
+ nonstd::optional<size_t> quantity;
+ std::string units;
+
+ switch (view_index) {
+ case LNV_LOG:
+ quantity = lnav_data.ld_log_source.file_count();
+ units = "file";
+ break;
+ case LNV_TEXT:
+ quantity = lnav_data.ld_text_source.size();
+ units = "file";
+ break;
+ case LNV_DB:
+ quantity = lnav_data.ld_db_row_source.dls_rows.size();
+ units = "row";
+ break;
+ }
+
+ if (quantity) {
+ display_value.pad_to(8)
+ .append(" (")
+ .append(lnav::roles::number(
+ quantity.value() == 0 ? "no"
+ : fmt::to_string(quantity.value())))
+ .appendf(FMT_STRING(" {}{})"),
+ units,
+ quantity.value() == 1 ? "" : "s");
+ }
+ retval.emplace_back(lnav_view_titles[view_index], display_value);
+ }
+ return retval;
+}
+
+static void
+view_performer(const breadcrumb::crumb::key_t& view_name)
+{
+ auto* view_title_iter = std::find_if(
+ std::begin(lnav_view_titles),
+ std::end(lnav_view_titles),
+ [&](const char* v) {
+ return strcasecmp(v, view_name.get<std::string>().c_str()) == 0;
+ });
+
+ if (view_title_iter != std::end(lnav_view_titles)) {
+ ensure_view(lnav_view_t(view_title_iter - lnav_view_titles));
+ }
+}
+
+std::vector<breadcrumb::crumb>
+lnav_crumb_source()
+{
+ std::vector<breadcrumb::crumb> retval;
+
+ auto top_view_opt = lnav_data.ld_view_stack.top();
+ if (!top_view_opt) {
+ return retval;
+ }
+
+ auto* top_view = top_view_opt.value();
+ auto view_index = top_view - lnav_data.ld_views;
+ retval.emplace_back(
+ lnav_view_titles[view_index],
+ attr_line_t().append(lnav::roles::status_title(
+ fmt::format(FMT_STRING(" {} "), lnav_view_titles[view_index]))),
+ view_title_poss,
+ view_performer);
+
+ auto* tss = top_view->get_sub_source();
+ if (tss != nullptr) {
+ tss->text_crumbs_for_line(top_view->get_selection(), retval);
+ }
+
+ return retval;
+} \ No newline at end of file
diff --git a/src/view_helpers.crumbs.hh b/src/view_helpers.crumbs.hh
new file mode 100644
index 0000000..2e28dc7
--- /dev/null
+++ b/src/view_helpers.crumbs.hh
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_view_helpers_crumbs_hh
+#define lnav_view_helpers_crumbs_hh
+
+#include "breadcrumb_curses.hh"
+
+std::vector<breadcrumb::crumb> lnav_crumb_source();
+
+#endif
diff --git a/src/view_helpers.examples.hh b/src/view_helpers.examples.hh
new file mode 100644
index 0000000..12287c7
--- /dev/null
+++ b/src/view_helpers.examples.hh
@@ -0,0 +1,41 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file view_helpers.examples.hh
+ */
+
+#ifndef lnav_view_helpers_examples_hh
+#define lnav_view_helpers_examples_hh
+
+#include "base/attr_line.hh"
+#include "help_text.hh"
+
+void execute_examples();
+attr_line_t eval_example(const help_text& ht, const help_example& ex);
+
+#endif
diff --git a/src/view_helpers.hh b/src/view_helpers.hh
new file mode 100644
index 0000000..19989e1
--- /dev/null
+++ b/src/view_helpers.hh
@@ -0,0 +1,101 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file view_helpers.hh
+ */
+
+#ifndef lnav_view_helpers_hh
+#define lnav_view_helpers_hh
+
+#include "bookmarks.hh"
+#include "help_text.hh"
+#include "logfile_fwd.hh"
+#include "vis_line.hh"
+
+class textview_curses;
+class hist_source2;
+class logfile_sub_source;
+
+/** The different views available. */
+typedef enum {
+ LNV_LOG,
+ LNV_TEXT,
+ LNV_HELP,
+ LNV_HISTOGRAM,
+ LNV_DB,
+ LNV_SCHEMA,
+ LNV_PRETTY,
+ LNV_SPECTRO,
+
+ LNV__MAX
+} lnav_view_t;
+
+/** The command modes that are available while viewing a file. */
+enum class ln_mode_t : int {
+ PAGING,
+ BREADCRUMBS,
+ FILTER,
+ FILES,
+ SPECTRO_DETAILS,
+ SEARCH_SPECTRO_DETAILS,
+ COMMAND,
+ SEARCH,
+ SEARCH_FILTERS,
+ SEARCH_FILES,
+ CAPTURE,
+ SQL,
+ EXEC,
+ USER,
+ BUSY,
+};
+
+extern const char* lnav_view_strings[LNV__MAX + 1];
+extern const char* lnav_view_titles[LNV__MAX];
+
+nonstd::optional<lnav_view_t> view_from_string(const char* name);
+
+bool ensure_view(textview_curses* expected_tc);
+bool ensure_view(lnav_view_t expected);
+bool toggle_view(textview_curses* toggle_tc);
+bool handle_winch();
+void layout_views();
+void update_hits(textview_curses* tc);
+
+nonstd::optional<vis_line_t> next_cluster(
+ nonstd::optional<vis_line_t> (bookmark_vector<vis_line_t>::*f)(vis_line_t)
+ const,
+ const bookmark_type_t* bt,
+ vis_line_t top);
+bool moveto_cluster(nonstd::optional<vis_line_t> (
+ bookmark_vector<vis_line_t>::*f)(vis_line_t) const,
+ const bookmark_type_t* bt,
+ vis_line_t top);
+vis_line_t search_forward_from(textview_curses* tc);
+textview_curses* get_textview_for_mode(ln_mode_t mode);
+
+#endif
diff --git a/src/view_helpers.hist.hh b/src/view_helpers.hist.hh
new file mode 100644
index 0000000..1325378
--- /dev/null
+++ b/src/view_helpers.hist.hh
@@ -0,0 +1,53 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_view_helpers_hist_hh
+#define lnav_view_helpers_hist_hh
+
+#include "hist_source.hh"
+#include "logfile_sub_source.hh"
+
+class hist_index_delegate : public index_delegate {
+public:
+ hist_index_delegate(hist_source2& hs, textview_curses& tc);
+
+ void index_start(logfile_sub_source& lss) override;
+
+ void index_line(logfile_sub_source& lss,
+ logfile* lf,
+ logfile::iterator ll) override;
+
+ void index_complete(logfile_sub_source& lss) override;
+
+private:
+ hist_source2& hid_source;
+ textview_curses& hid_view;
+};
+
+#endif
diff --git a/src/views_vtab.cc b/src/views_vtab.cc
new file mode 100644
index 0000000..6dee241
--- /dev/null
+++ b/src/views_vtab.cc
@@ -0,0 +1,1113 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <cstring>
+
+#include "views_vtab.hh"
+
+#include <unistd.h>
+
+#include "base/injector.bind.hh"
+#include "base/itertools.hh"
+#include "base/lnav_log.hh"
+#include "base/opt_util.hh"
+#include "config.h"
+#include "lnav.hh"
+#include "lnav_util.hh"
+#include "sql_util.hh"
+#include "view_curses.hh"
+#include "vtab_module_json.hh"
+#include "yajlpp/yajlpp_def.hh"
+
+template<>
+struct from_sqlite<lnav_view_t> {
+ inline lnav_view_t operator()(int argc, sqlite3_value** val, int argi)
+ {
+ const char* view_name = (const char*) sqlite3_value_text(val[argi]);
+ auto view_index_opt = view_from_string(view_name);
+
+ if (!view_index_opt) {
+ throw from_sqlite_conversion_error("lnav view name", argi);
+ }
+
+ return view_index_opt.value();
+ }
+};
+
+template<>
+struct from_sqlite<text_filter::type_t> {
+ inline text_filter::type_t operator()(int argc,
+ sqlite3_value** val,
+ int argi)
+ {
+ const char* type_name = (const char*) sqlite3_value_text(val[argi]);
+
+ if (strcasecmp(type_name, "in") == 0) {
+ return text_filter::INCLUDE;
+ }
+ if (strcasecmp(type_name, "out") == 0) {
+ return text_filter::EXCLUDE;
+ }
+
+ throw from_sqlite_conversion_error("value of 'in' or 'out'", argi);
+ }
+};
+
+template<>
+struct from_sqlite<filter_lang_t> {
+ inline filter_lang_t operator()(int argc, sqlite3_value** val, int argi)
+ {
+ const char* type_name = (const char*) sqlite3_value_text(val[argi]);
+
+ if (strcasecmp(type_name, "regex") == 0) {
+ return filter_lang_t::REGEX;
+ }
+ if (strcasecmp(type_name, "sql") == 0) {
+ return filter_lang_t::SQL;
+ }
+
+ throw from_sqlite_conversion_error("value of 'regex' or 'sql'", argi);
+ }
+};
+
+template<>
+struct from_sqlite<std::shared_ptr<lnav::pcre2pp::code>> {
+ inline std::shared_ptr<lnav::pcre2pp::code> operator()(int argc,
+ sqlite3_value** val,
+ int argi)
+ {
+ const char* pattern = (const char*) sqlite3_value_text(val[argi]);
+
+ if (pattern == nullptr || pattern[0] == '\0') {
+ throw sqlite_func_error("Expecting a non-empty pattern value");
+ }
+
+ auto compile_res = lnav::pcre2pp::code::from(
+ string_fragment::from_c_str(pattern), PCRE2_CASELESS);
+
+ if (compile_res.isErr()) {
+ auto ce = compile_res.unwrapErr();
+ throw sqlite_func_error(
+ "Invalid regular expression for pattern: {} at offset {}",
+ ce.get_message().c_str(),
+ ce.ce_offset);
+ }
+
+ return compile_res.unwrap().to_shared();
+ }
+};
+
+static const typed_json_path_container<breadcrumb::possibility>
+ breadcrumb_possibility_handlers = {
+ yajlpp::property_handler("display_value")
+ .for_field(&breadcrumb::possibility::p_display_value,
+ &attr_line_t::al_string),
+};
+
+struct resolved_crumb {
+ resolved_crumb() = default;
+
+ resolved_crumb(std::string display_value,
+ std::string search_placeholder,
+ std::vector<breadcrumb::possibility> possibilities)
+ : rc_display_value(std::move(display_value)),
+ rc_search_placeholder(std::move(search_placeholder)),
+ rc_possibilities(std::move(possibilities))
+ {
+ }
+
+ std::string rc_display_value;
+ std::string rc_search_placeholder;
+ std::vector<breadcrumb::possibility> rc_possibilities;
+};
+
+static const typed_json_path_container<resolved_crumb> breadcrumb_crumb_handlers
+ = {
+ yajlpp::property_handler("display_value")
+ .for_field(&resolved_crumb::rc_display_value),
+ yajlpp::property_handler("search_placeholder")
+ .for_field(&resolved_crumb::rc_search_placeholder),
+ yajlpp::property_handler("possibilities#")
+ .for_field(&resolved_crumb::rc_possibilities)
+ .with_children(breadcrumb_possibility_handlers),
+};
+
+struct top_line_meta {
+ nonstd::optional<std::string> tlm_time;
+ nonstd::optional<std::string> tlm_file;
+ nonstd::optional<std::string> tlm_anchor;
+ std::vector<resolved_crumb> tlm_crumbs;
+};
+
+static const typed_json_path_container<top_line_meta> top_line_meta_handlers = {
+ yajlpp::property_handler("time").for_field(&top_line_meta::tlm_time),
+ yajlpp::property_handler("file").for_field(&top_line_meta::tlm_file),
+ yajlpp::property_handler("anchor").for_field(&top_line_meta::tlm_anchor),
+ yajlpp::property_handler("breadcrumbs#")
+ .for_field(&top_line_meta::tlm_crumbs)
+ .with_children(breadcrumb_crumb_handlers),
+};
+
+struct lnav_views : public tvt_iterator_cursor<lnav_views> {
+ static constexpr const char* NAME = "lnav_views";
+ static constexpr const char* CREATE_STMT = R"(
+-- Access lnav's views through this table.
+CREATE TABLE lnav_views (
+ name TEXT PRIMARY KEY, -- The name of the view.
+ top INTEGER, -- The number of the line at the top of the view, starting from zero.
+ left INTEGER, -- The left position of the viewport.
+ height INTEGER, -- The height of the viewport.
+ inner_height INTEGER, -- The number of lines in the view.
+ top_time DATETIME, -- The time of the top line in the view, if the content is time-based.
+ top_file TEXT, -- The file the top line is from.
+ paused INTEGER, -- Indicates if the view is paused and will not load new data.
+ search TEXT, -- The text to search for in the view.
+ filtering INTEGER, -- Indicates if the view is applying filters.
+ movement TEXT, -- The movement mode, either 'top' or 'cursor'.
+ top_meta TEXT, -- A JSON object that contains metadata related to the top line in the view.
+ selection INTEGER -- The number of the line that is focused for selection.
+);
+)";
+
+ using iterator = textview_curses*;
+
+ iterator begin() { return std::begin(lnav_data.ld_views); }
+
+ iterator end() { return std::end(lnav_data.ld_views); }
+
+ int get_column(cursor& vc, sqlite3_context* ctx, int col)
+ {
+ lnav_view_t view_index = (lnav_view_t) std::distance(
+ std::begin(lnav_data.ld_views), vc.iter);
+ textview_curses& tc = *vc.iter;
+ unsigned long width;
+ vis_line_t height;
+
+ tc.get_dimensions(height, width);
+ switch (col) {
+ case 0:
+ sqlite3_result_text(
+ ctx, lnav_view_strings[view_index], -1, SQLITE_STATIC);
+ break;
+ case 1:
+ sqlite3_result_int(ctx, (int) tc.get_top());
+ break;
+ case 2:
+ sqlite3_result_int(ctx, tc.get_left());
+ break;
+ case 3:
+ sqlite3_result_int(ctx, height);
+ break;
+ case 4:
+ sqlite3_result_int(ctx, tc.get_inner_height());
+ break;
+ case 5: {
+ auto* time_source
+ = dynamic_cast<text_time_translator*>(tc.get_sub_source());
+
+ if (time_source != nullptr && tc.get_inner_height() > 0) {
+ auto top_time_opt = time_source->time_for_row(tc.get_top());
+
+ if (top_time_opt) {
+ char timestamp[64];
+
+ sql_strftime(timestamp,
+ sizeof(timestamp),
+ top_time_opt.value(),
+ ' ');
+ sqlite3_result_text(
+ ctx, timestamp, -1, SQLITE_TRANSIENT);
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ break;
+ }
+ case 6: {
+ to_sqlite(ctx, tc.map_top_row([](const auto& al) {
+ return get_string_attr(al.get_attrs(), logline::L_FILE) |
+ [](const auto wrapper) {
+ auto lf = wrapper.get();
+
+ return nonstd::make_optional(lf->get_filename());
+ };
+ }));
+ break;
+ }
+ case 7:
+ sqlite3_result_int(ctx, tc.is_paused());
+ break;
+ case 8:
+ to_sqlite(ctx, tc.get_current_search());
+ break;
+ case 9: {
+ auto* tss = tc.get_sub_source();
+
+ if (tss != nullptr && tss->tss_supports_filtering) {
+ sqlite3_result_int(ctx, tss->tss_apply_filters);
+ } else {
+ sqlite3_result_int(ctx, 0);
+ }
+ break;
+ }
+ case 10: {
+ sqlite3_result_text(ctx,
+ tc.is_selectable() ? "cursor" : "top",
+ -1,
+ SQLITE_STATIC);
+ break;
+ }
+ case 11: {
+ static const size_t MAX_POSSIBILITIES = 128;
+
+ auto* tss = tc.get_sub_source();
+
+ if (tss != nullptr && tss->text_line_count() > 0) {
+ auto* time_source = dynamic_cast<text_time_translator*>(
+ tc.get_sub_source());
+ auto* ta = dynamic_cast<text_anchors*>(tc.get_sub_source());
+ std::vector<breadcrumb::crumb> crumbs;
+
+ tss->text_crumbs_for_line(tc.get_top(), crumbs);
+
+ top_line_meta tlm;
+ if (time_source != nullptr) {
+ auto top_time_opt
+ = time_source->time_for_row(tc.get_top());
+
+ if (top_time_opt) {
+ char timestamp[64];
+
+ sql_strftime(timestamp,
+ sizeof(timestamp),
+ top_time_opt.value(),
+ ' ');
+ tlm.tlm_time = timestamp;
+ }
+ }
+ if (ta != nullptr) {
+ tlm.tlm_anchor = ta->anchor_for_row(tc.get_top());
+ }
+ tlm.tlm_file = tc.map_top_row([](const auto& al) {
+ return get_string_attr(al.get_attrs(), logline::L_FILE)
+ | [](const auto wrapper) {
+ auto lf = wrapper.get();
+
+ return nonstd::make_optional(
+ lf->get_filename());
+ };
+ });
+ for (const auto& crumb : crumbs) {
+ auto poss = crumb.c_possibility_provider();
+ if (poss.size() > MAX_POSSIBILITIES) {
+ poss.resize(MAX_POSSIBILITIES);
+ }
+ tlm.tlm_crumbs.emplace_back(
+ crumb.c_display_value.get_string(),
+ crumb.c_search_placeholder,
+ std::move(poss));
+ }
+ to_sqlite(ctx, top_line_meta_handlers.to_json_string(tlm));
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ break;
+ }
+ case 12:
+ sqlite3_result_int(ctx, (int) tc.get_selection());
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+
+ int delete_row(sqlite3_vtab* tab, sqlite3_int64 rowid)
+ {
+ tab->zErrMsg = sqlite3_mprintf(
+ "Rows cannot be deleted from the lnav_views table");
+ return SQLITE_ERROR;
+ }
+
+ int insert_row(sqlite3_vtab* tab, sqlite3_int64& rowid_out)
+ {
+ tab->zErrMsg = sqlite3_mprintf(
+ "Rows cannot be inserted into the lnav_views table");
+ return SQLITE_ERROR;
+ }
+
+ int update_row(sqlite3_vtab* tab,
+ sqlite3_int64& index,
+ const char* name,
+ int64_t top_row,
+ int64_t left,
+ int64_t height,
+ int64_t inner_height,
+ const char* top_time,
+ const char* top_file,
+ bool is_paused,
+ const char* search,
+ bool do_filtering,
+ string_fragment movement,
+ const char* top_meta,
+ int64_t selection)
+ {
+ auto& tc = lnav_data.ld_views[index];
+ auto* time_source
+ = dynamic_cast<text_time_translator*>(tc.get_sub_source());
+
+ if (tc.get_top() != top_row) {
+ tc.set_top(vis_line_t(top_row));
+ if (!tc.is_selectable()) {
+ selection = top_row;
+ }
+ } else if (top_time != nullptr && time_source != nullptr) {
+ date_time_scanner dts;
+ struct timeval tv;
+
+ if (dts.convert_to_timeval(top_time, -1, nullptr, tv)) {
+ auto last_time_opt = time_source->time_for_row(tc.get_top());
+
+ if (last_time_opt) {
+ auto last_time = last_time_opt.value();
+ if (tv != last_time) {
+ time_source->row_for_time(tv) |
+ [&tc](auto row) { tc.set_top(row); };
+ if (!tc.is_selectable()) {
+ selection = tc.get_top();
+ }
+ }
+ }
+ } else {
+ tab->zErrMsg = sqlite3_mprintf("Invalid time: %s", top_time);
+ return SQLITE_ERROR;
+ }
+ }
+ if (tc.get_selection() != selection) {
+ tc.set_selection(vis_line_t(selection));
+ }
+ if (top_meta != nullptr) {
+ static const intern_string_t SQL_SRC
+ = intern_string::lookup("top_meta");
+
+ auto parse_res = top_line_meta_handlers.parser_for(SQL_SRC).of(
+ string_fragment::from_c_str(top_meta));
+ if (parse_res.isErr()) {
+ auto errmsg = parse_res.unwrapErr();
+ tab->zErrMsg = sqlite3_mprintf(
+ "Invalid top_meta: %s",
+ errmsg[0].to_attr_line().get_string().c_str());
+ return SQLITE_ERROR;
+ }
+
+ auto tlm = parse_res.unwrap();
+
+ if (index == LNV_TEXT && tlm.tlm_file) {
+ if (!lnav_data.ld_text_source.to_front(tlm.tlm_file.value())) {
+ auto errmsg = parse_res.unwrapErr();
+ tab->zErrMsg = sqlite3_mprintf("unknown top_meta.file: %s",
+ tlm.tlm_file->c_str());
+ return SQLITE_ERROR;
+ }
+ }
+
+ auto* ta = dynamic_cast<text_anchors*>(tc.get_sub_source());
+ if (ta != nullptr && tlm.tlm_anchor
+ && !tlm.tlm_anchor.value().empty())
+ {
+ auto req_anchor = tlm.tlm_anchor.value();
+ auto req_anchor_top = ta->row_for_anchor(req_anchor);
+ if (req_anchor_top) {
+ auto curr_anchor = ta->anchor_for_row(tc.get_top());
+
+ if (!curr_anchor || curr_anchor.value() != req_anchor) {
+ tc.set_selection(req_anchor_top.value());
+ }
+ } else {
+ tab->zErrMsg = sqlite3_mprintf(
+ "unknown top_meta.anchor: %s", req_anchor.c_str());
+ return SQLITE_ERROR;
+ }
+ }
+ }
+ if (movement == "top" && tc.is_selectable()) {
+ tc.set_selectable(false);
+ } else if (movement == "cursor" && !tc.is_selectable()) {
+ // First, toggle modes, otherwise get_selection() returns top
+ tc.set_selectable(true);
+
+ auto cur_sel = tc.get_selection();
+ auto cur_top = tc.get_top();
+ auto cur_bot = tc.get_bottom() - tc.get_tail_space();
+
+ if (cur_sel < cur_top) {
+ tc.set_selection(cur_top);
+ } else if (cur_sel > cur_bot) {
+ tc.set_selection(cur_bot);
+ }
+ }
+ tc.set_left(left);
+ tc.set_paused(is_paused);
+ tc.execute_search(search);
+ auto* tss = tc.get_sub_source();
+ if (tss != nullptr && tss->tss_supports_filtering
+ && tss->tss_apply_filters != do_filtering)
+ {
+ tss->tss_apply_filters = do_filtering;
+ tss->text_filters_changed();
+ }
+
+ return SQLITE_OK;
+ };
+};
+
+struct lnav_view_stack : public tvt_iterator_cursor<lnav_view_stack> {
+ using iterator = std::vector<textview_curses*>::iterator;
+
+ static constexpr const char* NAME = "lnav_view_stack";
+ static constexpr const char* CREATE_STMT = R"(
+-- Access lnav's view stack through this table.
+CREATE TABLE lnav_view_stack (
+ name TEXT
+);
+)";
+
+ iterator begin() { return lnav_data.ld_view_stack.begin(); }
+
+ iterator end() { return lnav_data.ld_view_stack.end(); }
+
+ int get_column(cursor& vc, sqlite3_context* ctx, int col)
+ {
+ textview_curses* tc = *vc.iter;
+ auto view = lnav_view_t(tc - lnav_data.ld_views);
+
+ switch (col) {
+ case 0:
+ sqlite3_result_text(
+ ctx, lnav_view_strings[view], -1, SQLITE_STATIC);
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+
+ int delete_row(sqlite3_vtab* tab, sqlite3_int64 rowid)
+ {
+ if ((size_t) rowid != lnav_data.ld_view_stack.size() - 1) {
+ tab->zErrMsg = sqlite3_mprintf(
+ "Only the top view in the stack can be deleted");
+ return SQLITE_ERROR;
+ }
+
+ lnav_data.ld_last_view = *lnav_data.ld_view_stack.top();
+ lnav_data.ld_view_stack.pop_back();
+ return SQLITE_OK;
+ }
+
+ int insert_row(sqlite3_vtab* tab,
+ sqlite3_int64& rowid_out,
+ lnav_view_t view_index)
+ {
+ textview_curses* tc = &lnav_data.ld_views[view_index];
+
+ ensure_view(tc);
+ rowid_out = lnav_data.ld_view_stack.size() - 1;
+
+ return SQLITE_OK;
+ }
+
+ int update_row(sqlite3_vtab* tab, sqlite3_int64& index)
+ {
+ tab->zErrMsg
+ = sqlite3_mprintf("The lnav_view_stack table cannot be updated");
+ return SQLITE_ERROR;
+ }
+};
+
+struct lnav_view_filter_base {
+ struct iterator {
+ using difference_type = int;
+ using value_type = text_filter;
+ using pointer = text_filter*;
+ using reference = text_filter&;
+ using iterator_category = std::forward_iterator_tag;
+
+ lnav_view_t i_view_index;
+ int i_filter_index;
+
+ iterator(lnav_view_t view = LNV_LOG, int filter = -1)
+ : i_view_index(view), i_filter_index(filter)
+ {
+ }
+
+ iterator& operator++()
+ {
+ while (this->i_view_index < LNV__MAX) {
+ textview_curses& tc = lnav_data.ld_views[this->i_view_index];
+ text_sub_source* tss = tc.get_sub_source();
+
+ if (tss == nullptr) {
+ this->i_view_index = lnav_view_t(this->i_view_index + 1);
+ continue;
+ }
+
+ filter_stack& fs = tss->get_filters();
+
+ this->i_filter_index += 1;
+ if (this->i_filter_index >= (ssize_t) fs.size()) {
+ this->i_filter_index = -1;
+ this->i_view_index = lnav_view_t(this->i_view_index + 1);
+ } else {
+ break;
+ }
+ }
+
+ return *this;
+ }
+
+ bool operator==(const iterator& other) const
+ {
+ return this->i_view_index == other.i_view_index
+ && this->i_filter_index == other.i_filter_index;
+ }
+
+ bool operator!=(const iterator& other) const
+ {
+ return !(*this == other);
+ }
+ };
+
+ iterator begin()
+ {
+ iterator retval = iterator();
+
+ return ++retval;
+ }
+
+ iterator end() { return {LNV__MAX, -1}; }
+
+ sqlite_int64 get_rowid(iterator iter)
+ {
+ textview_curses& tc = lnav_data.ld_views[iter.i_view_index];
+ text_sub_source* tss = tc.get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ auto& tf = *(fs.begin() + iter.i_filter_index);
+
+ sqlite_int64 retval = iter.i_view_index;
+
+ retval = retval << 32;
+ retval = retval | tf->get_index();
+
+ return retval;
+ }
+};
+
+struct lnav_view_filters
+ : public tvt_iterator_cursor<lnav_view_filters>
+ , public lnav_view_filter_base {
+ static constexpr const char* NAME = "lnav_view_filters";
+ static constexpr const char* CREATE_STMT = R"(
+-- Access lnav's filters through this table.
+CREATE TABLE lnav_view_filters (
+ view_name TEXT, -- The name of the view.
+ filter_id INTEGER DEFAULT 0, -- The filter identifier.
+ enabled INTEGER DEFAULT 1, -- Indicates if the filter is enabled/disabled.
+ type TEXT DEFAULT 'out', -- The type of filter (i.e. in/out).
+ language TEXT DEFAULT 'regex', -- The filter language.
+ pattern TEXT -- The filter pattern.
+);
+)";
+
+ int get_column(cursor& vc, sqlite3_context* ctx, int col)
+ {
+ textview_curses& tc = lnav_data.ld_views[vc.iter.i_view_index];
+ text_sub_source* tss = tc.get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ auto tf = *(fs.begin() + vc.iter.i_filter_index);
+
+ switch (col) {
+ case 0:
+ sqlite3_result_text(ctx,
+ lnav_view_strings[vc.iter.i_view_index],
+ -1,
+ SQLITE_STATIC);
+ break;
+ case 1:
+ to_sqlite(ctx, tf->get_index());
+ break;
+ case 2:
+ sqlite3_result_int(ctx, tf->is_enabled());
+ break;
+ case 3:
+ switch (tf->get_type()) {
+ case text_filter::INCLUDE:
+ sqlite3_result_text(ctx, "in", 2, SQLITE_STATIC);
+ break;
+ case text_filter::EXCLUDE:
+ sqlite3_result_text(ctx, "out", 3, SQLITE_STATIC);
+ break;
+ default:
+ ensure(0);
+ }
+ break;
+ case 4:
+ switch (tf->get_lang()) {
+ case filter_lang_t::REGEX:
+ sqlite3_result_text(ctx, "regex", 5, SQLITE_STATIC);
+ break;
+ case filter_lang_t::SQL:
+ sqlite3_result_text(ctx, "sql", 3, SQLITE_STATIC);
+ break;
+ default:
+ ensure(0);
+ }
+ break;
+ case 5:
+ sqlite3_result_text(
+ ctx, tf->get_id().c_str(), -1, SQLITE_TRANSIENT);
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+
+ int insert_row(sqlite3_vtab* tab,
+ sqlite3_int64& rowid_out,
+ lnav_view_t view_index,
+ nonstd::optional<int64_t> _filter_id,
+ nonstd::optional<bool> enabled,
+ nonstd::optional<text_filter::type_t> type,
+ nonstd::optional<filter_lang_t> lang,
+ sqlite3_value* pattern_str)
+ {
+ auto* mod_vt = (vtab_module<lnav_view_filters>::vtab*) tab;
+ auto& tc = lnav_data.ld_views[view_index];
+ auto* tss = tc.get_sub_source();
+ auto& fs = tss->get_filters();
+ auto filter_index
+ = lang.value_or(filter_lang_t::REGEX) == filter_lang_t::REGEX
+ ? fs.next_index()
+ : nonstd::make_optional(size_t{0});
+ if (!filter_index) {
+ throw sqlite_func_error("Too many filters");
+ }
+ auto conflict_mode = sqlite3_vtab_on_conflict(mod_vt->v_db);
+ std::shared_ptr<text_filter> tf;
+ switch (lang.value_or(filter_lang_t::REGEX)) {
+ case filter_lang_t::REGEX: {
+ auto pattern
+ = from_sqlite<std::shared_ptr<lnav::pcre2pp::code>>()(
+ 1, &pattern_str, 0);
+ auto pf = std::make_shared<pcre_filter>(
+ type.value_or(text_filter::type_t::EXCLUDE),
+ pattern->get_pattern(),
+ *filter_index,
+ pattern);
+ auto new_cmd = pf->to_command();
+ for (auto& filter : fs) {
+ if (filter->to_command() == new_cmd) {
+ switch (conflict_mode) {
+ case SQLITE_FAIL:
+ case SQLITE_ABORT:
+ tab->zErrMsg = sqlite3_mprintf(
+ "filter already exists -- :%s",
+ new_cmd.c_str());
+ return conflict_mode;
+ case SQLITE_IGNORE:
+ return SQLITE_OK;
+ case SQLITE_REPLACE:
+ if (filter->is_enabled() != pf->is_enabled()) {
+ filter->set_enabled(pf->is_enabled());
+ tss->text_filters_changed();
+ tc.set_needs_update();
+ }
+ return SQLITE_OK;
+ default:
+ break;
+ }
+ }
+ }
+ fs.add_filter(pf);
+ tf = pf;
+ break;
+ }
+ case filter_lang_t::SQL: {
+ if (view_index != LNV_LOG) {
+ throw sqlite_func_error(
+ "SQL filters are only supported in the log view");
+ }
+ if (lnav_data.ld_log_source.get_sql_filter_text() != "") {
+ switch (conflict_mode) {
+ case SQLITE_FAIL:
+ case SQLITE_ABORT:
+ tab->zErrMsg = sqlite3_mprintf(
+ "A SQL expression filter already exists");
+ return conflict_mode;
+ case SQLITE_IGNORE:
+ return SQLITE_OK;
+ default:
+ break;
+ }
+ }
+ auto clause = from_sqlite<std::string>()(1, &pattern_str, 0);
+ auto expr
+ = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), clause);
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+#ifdef SQLITE_PREPARE_PERSISTENT
+ auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
+ expr.c_str(),
+ expr.size(),
+ SQLITE_PREPARE_PERSISTENT,
+ stmt.out(),
+ nullptr);
+#else
+ auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
+ expr.c_str(),
+ expr.size(),
+ stmt.out(),
+ nullptr);
+#endif
+ if (retcode != SQLITE_OK) {
+ const char* errmsg = sqlite3_errmsg(lnav_data.ld_db);
+
+ throw sqlite_func_error("Invalid SQL: {}", errmsg);
+ }
+ auto set_res = lnav_data.ld_log_source.set_sql_filter(
+ clause, stmt.release());
+ if (set_res.isErr()) {
+ tab->zErrMsg = sqlite3_mprintf(
+ "%s%s",
+ sqlitepp::ERROR_PREFIX,
+ lnav::to_json(set_res.unwrapErr()).c_str());
+ return SQLITE_ERROR;
+ }
+ tf = lnav_data.ld_log_source.get_sql_filter().value();
+ break;
+ }
+ default:
+ ensure(0);
+ }
+ if (!enabled.value_or(true)) {
+ tf->disable();
+ }
+ tss->text_filters_changed();
+ tc.set_needs_update();
+
+ return SQLITE_OK;
+ }
+
+ int delete_row(sqlite3_vtab* tab, sqlite3_int64 rowid)
+ {
+ auto view_index = lnav_view_t(rowid >> 32);
+ size_t filter_index = rowid & 0xffffffffLL;
+ textview_curses& tc = lnav_data.ld_views[view_index];
+ text_sub_source* tss = tc.get_sub_source();
+ filter_stack& fs = tss->get_filters();
+
+ for (const auto& iter : fs) {
+ if (iter->get_index() == filter_index) {
+ fs.delete_filter(iter->get_id());
+ tss->text_filters_changed();
+ break;
+ }
+ }
+ tc.set_needs_update();
+
+ return SQLITE_OK;
+ }
+
+ int update_row(sqlite3_vtab* tab,
+ sqlite3_int64& rowid,
+ lnav_view_t new_view_index,
+ int64_t new_filter_id,
+ bool enabled,
+ text_filter::type_t type,
+ filter_lang_t lang,
+ sqlite3_value* pattern_val)
+ {
+ auto* mod_vt = (vtab_module<lnav_view_filters>::vtab*) tab;
+ auto view_index = lnav_view_t(rowid >> 32);
+ auto filter_index = rowid & 0xffffffffLL;
+ auto& tc = lnav_data.ld_views[view_index];
+ auto* tss = tc.get_sub_source();
+ auto& fs = tss->get_filters();
+ auto iter = fs.begin();
+ for (; iter != fs.end(); ++iter) {
+ if ((*iter)->get_index() == (size_t) filter_index) {
+ break;
+ }
+ }
+
+ auto tf = *iter;
+
+ if (new_view_index != view_index) {
+ tab->zErrMsg
+ = sqlite3_mprintf("The view for a filter cannot be changed");
+ return SQLITE_ERROR;
+ }
+
+ if (lang == filter_lang_t::SQL && tf->get_index() == 0) {
+ if (view_index != LNV_LOG) {
+ throw sqlite_func_error(
+ "SQL filters are only supported in the log view");
+ }
+ auto clause = from_sqlite<std::string>()(1, &pattern_val, 0);
+ auto expr = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), clause);
+ auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
+#ifdef SQLITE_PREPARE_PERSISTENT
+ auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
+ expr.c_str(),
+ expr.size(),
+ SQLITE_PREPARE_PERSISTENT,
+ stmt.out(),
+ nullptr);
+#else
+ auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
+ expr.c_str(),
+ expr.size(),
+ stmt.out(),
+ nullptr);
+#endif
+ if (retcode != SQLITE_OK) {
+ const char* errmsg = sqlite3_errmsg(lnav_data.ld_db);
+
+ throw sqlite_func_error("Invalid SQL: {}", errmsg);
+ }
+ auto set_res = lnav_data.ld_log_source.set_sql_filter(
+ clause, stmt.release());
+ if (set_res.isErr()) {
+ tab->zErrMsg = sqlite3_mprintf(
+ "%s%s",
+ sqlitepp::ERROR_PREFIX,
+ lnav::to_json(set_res.unwrapErr()).c_str());
+ return SQLITE_ERROR;
+ }
+ *iter = lnav_data.ld_log_source.get_sql_filter().value();
+ } else {
+ tf->lf_deleted = true;
+ tss->text_filters_changed();
+
+ auto pattern = from_sqlite<std::shared_ptr<lnav::pcre2pp::code>>()(
+ 1, &pattern_val, 0);
+ auto pf = std::make_shared<pcre_filter>(
+ type, pattern->get_pattern(), tf->get_index(), pattern);
+ auto conflict_mode = sqlite3_vtab_on_conflict(mod_vt->v_db);
+ auto new_cmd = pf->to_command();
+ for (auto& filter : fs) {
+ if (filter->to_command() == new_cmd) {
+ switch (conflict_mode) {
+ case SQLITE_FAIL:
+ case SQLITE_ABORT:
+ tab->zErrMsg = sqlite3_mprintf(
+ "filter already exists -- :%s",
+ new_cmd.c_str());
+ return conflict_mode;
+ case SQLITE_IGNORE:
+ return SQLITE_OK;
+ case SQLITE_REPLACE:
+ if (filter->is_enabled() != pf->is_enabled()) {
+ filter->set_enabled(pf->is_enabled());
+ tss->text_filters_changed();
+ tc.set_needs_update();
+ }
+ return SQLITE_OK;
+ default:
+ break;
+ }
+ }
+ }
+ *iter = pf;
+ }
+ if (!enabled) {
+ (*iter)->disable();
+ }
+ tss->text_filters_changed();
+ tc.set_needs_update();
+
+ return SQLITE_OK;
+ }
+};
+
+struct lnav_view_filter_stats
+ : public tvt_iterator_cursor<lnav_view_filter_stats>
+ , public lnav_view_filter_base {
+ static constexpr const char* NAME = "lnav_view_filter_stats";
+ static constexpr const char* CREATE_STMT = R"(
+-- Access statistics for filters through this table.
+CREATE TABLE lnav_view_filter_stats (
+ view_name TEXT, -- The name of the view.
+ filter_id INTEGER, -- The filter identifier.
+ hits INTEGER -- The number of lines that matched this filter.
+);
+)";
+
+ int get_column(cursor& vc, sqlite3_context* ctx, int col)
+ {
+ textview_curses& tc = lnav_data.ld_views[vc.iter.i_view_index];
+ text_sub_source* tss = tc.get_sub_source();
+ filter_stack& fs = tss->get_filters();
+ auto tf = *(fs.begin() + vc.iter.i_filter_index);
+
+ switch (col) {
+ case 0:
+ sqlite3_result_text(ctx,
+ lnav_view_strings[vc.iter.i_view_index],
+ -1,
+ SQLITE_STATIC);
+ break;
+ case 1:
+ to_sqlite(ctx, tf->get_index());
+ break;
+ case 2:
+ to_sqlite(ctx, tss->get_filtered_count_for(tf->get_index()));
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+};
+
+struct lnav_view_files : public tvt_iterator_cursor<lnav_view_files> {
+ static constexpr const char* NAME = "lnav_view_files";
+ static constexpr const char* CREATE_STMT = R"(
+--
+CREATE TABLE lnav_view_files (
+ view_name TEXT, -- The name of the view.
+ filepath TEXT, -- The path to the file.
+ visible INTEGER -- Indicates whether or not the file is shown.
+);
+)";
+
+ using iterator = logfile_sub_source::iterator;
+
+ struct cursor : public tvt_iterator_cursor<lnav_view_files>::cursor {
+ explicit cursor(sqlite3_vtab* vt)
+ : tvt_iterator_cursor<lnav_view_files>::cursor(vt)
+ {
+ }
+
+ int next()
+ {
+ if (this->iter != get_handler().end()) {
+ do {
+ ++this->iter;
+ } while (this->iter != get_handler().end()
+ && (*this->iter)->get_file_ptr() == nullptr);
+ }
+
+ return SQLITE_OK;
+ }
+ };
+
+ iterator begin() { return lnav_data.ld_log_source.begin(); }
+
+ iterator end() { return lnav_data.ld_log_source.end(); }
+
+ int get_column(cursor& vc, sqlite3_context* ctx, int col)
+ {
+ auto& ld = *vc.iter;
+
+ switch (col) {
+ case 0:
+ sqlite3_result_text(
+ ctx, lnav_view_strings[LNV_LOG], -1, SQLITE_STATIC);
+ break;
+ case 1:
+ to_sqlite(ctx,
+ ld->ld_filter_state.lfo_filter_state.tfs_logfile
+ ->get_filename());
+ break;
+ case 2:
+ to_sqlite(ctx, ld->ld_visible);
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+
+ int delete_row(sqlite3_vtab* tab, sqlite3_int64 rowid)
+ {
+ tab->zErrMsg = sqlite3_mprintf(
+ "Rows cannot be deleted from the lnav_view_files table");
+ return SQLITE_ERROR;
+ }
+
+ int insert_row(sqlite3_vtab* tab, sqlite3_int64& rowid_out)
+ {
+ tab->zErrMsg = sqlite3_mprintf(
+ "Rows cannot be inserted into the lnav_view_files table");
+ return SQLITE_ERROR;
+ }
+
+ int update_row(sqlite3_vtab* tab,
+ sqlite3_int64& rowid,
+ const char* view_name,
+ const char* file_path,
+ bool visible)
+ {
+ auto& lss = lnav_data.ld_log_source;
+ auto iter = this->begin();
+
+ std::advance(iter, rowid);
+
+ auto& ld = *iter;
+ if (ld->ld_visible != visible) {
+ ld->set_visibility(visible);
+ lss.text_filters_changed();
+ }
+
+ return SQLITE_OK;
+ }
+};
+
+static const char* CREATE_FILTER_VIEW = R"(
+CREATE VIEW lnav_view_filters_and_stats AS
+ SELECT * FROM lnav_view_filters LEFT NATURAL JOIN lnav_view_filter_stats
+)";
+
+static auto a = injector::bind_multiple<vtab_module_base>()
+ .add<vtab_module<lnav_views>>()
+ .add<vtab_module<lnav_view_stack>>()
+ .add<vtab_module<lnav_view_filters>>()
+ .add<vtab_module<tvt_no_update<lnav_view_filter_stats>>>()
+ .add<vtab_module<lnav_view_files>>();
+
+int
+register_views_vtab(sqlite3* db)
+{
+ auto_mem<char> errmsg(sqlite3_free);
+ if (sqlite3_exec(db, CREATE_FILTER_VIEW, nullptr, nullptr, errmsg.out())
+ != SQLITE_OK)
+ {
+ log_error("Unable to create filter view: %s", errmsg.in());
+ }
+
+ return 0;
+}
diff --git a/src/views_vtab.hh b/src/views_vtab.hh
new file mode 100644
index 0000000..51a3929
--- /dev/null
+++ b/src/views_vtab.hh
@@ -0,0 +1,39 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef views_vtab_hh
+#define views_vtab_hh
+
+#include <sqlite3.h>
+
+#include "vtab_module.hh"
+
+int register_views_vtab(sqlite3* db);
+
+#endif
diff --git a/src/vis_line.hh b/src/vis_line.hh
new file mode 100644
index 0000000..10b89e2
--- /dev/null
+++ b/src/vis_line.hh
@@ -0,0 +1,43 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_vis_line_hh
+#define lnav_vis_line_hh
+
+#include "strong_int.hh"
+
+/** Strongly-typed integer for visible lines. */
+STRONG_INT_TYPE(int, vis_line);
+
+constexpr vis_line_t operator"" _vl(unsigned long long line) noexcept
+{
+ return vis_line_t((int) line);
+}
+
+#endif
diff --git a/src/vt52_curses.cc b/src/vt52_curses.cc
new file mode 100644
index 0000000..01dc6ff
--- /dev/null
+++ b/src/vt52_curses.cc
@@ -0,0 +1,314 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHAN`TABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file vt52_curses.cc
+ */
+
+#include <map>
+
+#include "vt52_curses.hh"
+
+#include <string.h>
+
+#include "base/lnav_log.hh"
+#include "config.h"
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/curses.h>
+# include <ncursesw/term.h>
+#elif defined HAVE_NCURSESW_H
+# include <ncursesw.h>
+# include <term.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/curses.h>
+# include <ncurses/term.h>
+#elif defined HAVE_NCURSES_H
+# include <ncurses.h>
+# include <term.h>
+#elif defined HAVE_CURSES_H
+# include <curses.h>
+# include <term.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+
+/**
+ * Singleton used to hold the mapping of ncurses keycodes to VT52 escape
+ * sequences.
+ */
+class vt52_escape_map {
+public:
+ /** @return The singleton. */
+ static vt52_escape_map& singleton()
+ {
+ static vt52_escape_map s_vem;
+
+ return s_vem;
+ };
+
+ /**
+ * @param ch The ncurses keycode.
+ * @return The null terminated VT52 escape sequence.
+ */
+ const char* operator[](int ch) const
+ {
+ auto iter = this->vem_map.find(ch);
+ const char* retval = nullptr;
+
+ if (iter == this->vem_map.end()) {
+ if (ch > KEY_MAX) {
+ auto name = keyname(ch);
+
+ if (name != nullptr) {
+ auto seq = tigetstr(name);
+
+ if (seq != nullptr) {
+ this->vem_map[ch] = seq;
+ retval = seq;
+ }
+ }
+ }
+ } else {
+ retval = iter->second;
+ }
+
+ return retval;
+ };
+
+ const char* operator[](const char* seq) const
+ {
+ std::map<std::string, const char*>::const_iterator iter;
+ const char* retval = nullptr;
+
+ require(seq != nullptr);
+
+ if ((iter = this->vem_input_map.find(seq)) != this->vem_input_map.end())
+ {
+ retval = iter->second;
+ }
+
+ return retval;
+ };
+
+private:
+ /** Construct the map with a few escape sequences. */
+ vt52_escape_map()
+ {
+ static char area_buffer[1024];
+ char* area = area_buffer;
+
+ if (tgetent(nullptr, "vt52") == ERR) {
+ perror("tgetent");
+ }
+ this->vem_map[KEY_UP] = tgetstr((char*) "ku", &area);
+ this->vem_map[KEY_DOWN] = tgetstr((char*) "kd", &area);
+ this->vem_map[KEY_RIGHT] = tgetstr((char*) "kr", &area);
+ this->vem_map[KEY_LEFT] = tgetstr((char*) "kl", &area);
+ this->vem_map[KEY_HOME] = tgetstr((char*) "kh", &area);
+ if (this->vem_map[KEY_HOME] == nullptr) {
+ this->vem_map[KEY_HOME] = "\x01";
+ }
+ this->vem_map[KEY_BACKSPACE] = "\010";
+ this->vem_map[KEY_DC] = "\x04";
+
+ this->vem_map[KEY_BEG] = "\x01";
+ this->vem_map[KEY_END] = "\x05";
+
+ this->vem_map[KEY_SLEFT] = tgetstr((char*) "#4", &area);
+ if (this->vem_map[KEY_SLEFT] == nullptr) {
+ this->vem_map[KEY_SLEFT] = "\033b";
+ }
+ this->vem_map[KEY_SRIGHT] = tgetstr((char*) "%i", &area);
+ if (this->vem_map[KEY_SRIGHT] == nullptr) {
+ this->vem_map[KEY_SRIGHT] = "\033f";
+ }
+
+ this->vem_map[KEY_BTAB] = "\033[Z";
+
+ this->vem_input_map[tgetstr((char*) "ce", &area)] = "ce";
+ this->vem_input_map[tgetstr((char*) "kl", &area)] = "kl";
+ this->vem_input_map[tgetstr((char*) "kr", &area)] = "kr";
+ // bracketed paste mode
+ this->vem_input_map["\x1b[?2004h"] = "BE";
+ this->vem_input_map["\x1b[?2004l"] = "BD";
+ tgetent(nullptr, getenv("TERM"));
+ };
+
+ /** Map of ncurses keycodes to VT52 escape sequences. */
+ mutable std::map<int, const char*> vem_map;
+ std::map<std::string, const char*> vem_input_map;
+};
+
+const char*
+vt52_curses::map_input(int ch, int& len_out)
+{
+ const char *esc, *retval;
+
+ /* Check for an escape sequence, otherwise just return the char. */
+ if ((esc = vt52_escape_map::singleton()[ch]) != nullptr) {
+ retval = esc;
+ len_out = strlen(retval);
+ } else {
+ switch (ch) {
+ case 0x7f:
+ ch = BACKSPACE;
+ break;
+ }
+ this->vc_map_buffer = (char) ch;
+ retval = &this->vc_map_buffer; /* XXX probably shouldn't do this. */
+ len_out = 1;
+ }
+
+ ensure(retval != nullptr);
+ ensure(len_out > 0);
+
+ return retval;
+}
+
+void
+vt52_curses::map_output(const char* output, int len)
+{
+ int lpc;
+
+ require(this->vc_window != nullptr);
+
+ for (lpc = 0; lpc < len; lpc++) {
+ if (this->vc_escape_len > 0) {
+ const char* cap;
+
+ this->vc_escape[this->vc_escape_len] = output[lpc];
+ this->vc_escape_len += 1;
+ this->vc_escape[this->vc_escape_len] = '\0';
+
+ if (this->vc_expected_escape_len != -1) {
+ if (this->vc_escape_len == this->vc_expected_escape_len) {
+ auto& line_string = this->vc_line.get_string();
+ auto x_byte_index
+ = utf8_char_to_byte_index(line_string, this->vc_x);
+
+ for (int esc_index = 0; esc_index < this->vc_escape_len;
+ esc_index++)
+ {
+ if (x_byte_index < this->vc_line.length()) {
+ line_string[x_byte_index]
+ = this->vc_escape[esc_index];
+ } else {
+ this->vc_line.append(1, this->vc_escape[esc_index]);
+ }
+ x_byte_index += 1;
+ }
+ this->vc_x += 1;
+ this->vc_escape_len = 0;
+ }
+ } else if ((cap = vt52_escape_map::singleton()[this->vc_escape])
+ != nullptr)
+ {
+ this->vc_escape_len = 0;
+ if (strcmp(cap, "ce") == 0) {
+ this->vc_line.erase_utf8_chars(this->vc_x);
+ } else if (strcmp(cap, "kl") == 0) {
+ this->vc_x -= 1;
+ } else if (strcmp(cap, "kr") == 0) {
+ this->vc_x += 1;
+ } else if (strcmp(cap, "BE") == 0 || strcmp(cap, "BD") == 0) {
+ // TODO pass bracketed paste mode through
+ } else {
+ ensure(0);
+ }
+ }
+ } else {
+ auto next_ch = output[lpc];
+ auto seq_size = ww898::utf::utf8::char_size([next_ch]() {
+ return std::make_pair(next_ch, 16);
+ }).unwrapOr(size_t{1});
+
+ if (seq_size > 1) {
+ this->vc_escape[0] = next_ch;
+ this->vc_escape_len = 1;
+ this->vc_expected_escape_len = seq_size;
+ continue;
+ }
+
+ switch (next_ch) {
+ case STX:
+ this->vc_x = 0;
+ this->vc_line.clear();
+ break;
+
+ case BELL:
+ flash();
+ break;
+
+ case BACKSPACE:
+ this->vc_x -= 1;
+ break;
+
+ case ESCAPE:
+ this->vc_escape[0] = ESCAPE;
+ this->vc_escape_len = 1;
+ this->vc_expected_escape_len = -1;
+ break;
+
+ case '\n':
+ this->vc_x = 0;
+ this->vc_line.clear();
+ break;
+
+ case '\r':
+ this->vc_x = 0;
+ break;
+
+ default: {
+ auto& line_string = this->vc_line.get_string();
+ auto x_byte_index
+ = utf8_char_to_byte_index(line_string, this->vc_x);
+
+ if (x_byte_index < this->vc_line.length()) {
+ line_string[x_byte_index] = next_ch;
+ } else {
+ this->vc_line.append(1, next_ch);
+ }
+ this->vc_x += 1;
+ break;
+ }
+ }
+ }
+ }
+}
+
+void
+vt52_curses::do_update()
+{
+ auto actual_width = this->get_actual_width();
+ view_curses::mvwattrline(this->vc_window,
+ this->get_actual_y(),
+ this->vc_left,
+ this->vc_line,
+ line_range{0, (int) actual_width});
+ wmove(this->vc_window, this->get_actual_y(), this->vc_left + this->vc_x);
+}
diff --git a/src/vt52_curses.hh b/src/vt52_curses.hh
new file mode 100644
index 0000000..c78b0e4
--- /dev/null
+++ b/src/vt52_curses.hh
@@ -0,0 +1,169 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file vt52_curses.hh
+ */
+
+#ifndef vt52_curses_hh
+#define vt52_curses_hh
+
+#include <list>
+#include <string>
+
+#include "view_curses.hh"
+
+/**
+ * VT52 emulator for curses, useful for mediating between curses and readline,
+ * which don't play well together. It is expected that a subclass of this
+ * class will fork off a child process that sends and receives VT52 keycodes(?)
+ * which is translated by this class into curses calls.
+ *
+ * VT52 seems to be the simplest terminal to emulate since we do not need to
+ * maintain the state of the screen, beyond past lines. For example, when
+ * inserting a character, VT52 moves the cursor to the insertion point, clears
+ * the rest of the line and then rewrites the rest of the line with the new
+ * character. This is in contrast to VT100 which moves the cursor to the
+ * insertion point and then sends a code to insert the character and relying
+ * on the terminal to shift the rest of the line to the right a character.
+ */
+class vt52_curses : public view_curses {
+public:
+ /** @param win The curses window this view is attached to. */
+ void set_window(WINDOW* win) { this->vc_window = win; }
+
+ /** @return The curses window this view is attached to. */
+ WINDOW* get_window() { return this->vc_window; }
+
+ void set_left(int left) { this->vc_left = left; }
+
+ int get_left() const { return this->vc_left; }
+
+ /**
+ * Set the Y position of this view on the display. A value greater than
+ * zero is considered to be an absolute size. A value less than zero makes
+ * the position relative to the bottom of the enclosing window.
+ *
+ * @param y The Y position of the cursor on the curses display.
+ */
+ void set_y(int y) { this->vc_y = y; }
+
+ /** @return The abs/rel Y position of the cursor on the curses display. */
+ int get_y() const { return this->vc_y; }
+
+ /** @param x The X position of the cursor on the curses display. */
+ void set_x(int x) { this->vc_x = x; }
+
+ /** @return The X position of the cursor on the curses display. */
+ int get_x() const { return this->vc_x; }
+
+ /**
+ * @return The height of this view, which consists of a single line for
+ * input, plus any past lines of output, which will appear ABOVE the Y
+ * position for this view.
+ * @todo Kinda hardwired to the way readline works.
+ */
+ int get_height() { return 1; }
+
+ void set_max_height(int mh) { this->vc_max_height = mh; }
+ int get_max_height() const { return this->vc_max_height; }
+
+ /**
+ * Map an ncurses input keycode to a vt52 sequence.
+ *
+ * @param ch The input character.
+ * @param len_out The length of the returned sequence.
+ * @return The vt52 sequence to send to the child.
+ */
+ const char* map_input(int ch, int& len_out);
+
+ /**
+ * Map VT52 output to ncurses calls.
+ *
+ * @param output VT52 encoded output from the child process.
+ * @param len The length of the output array.
+ */
+ void map_output(const char* output, int len);
+
+ /**
+ * Paints any past lines and moves the cursor to the current X position.
+ */
+ void do_update();
+
+ const static char ESCAPE = 27; /*< VT52 Escape key value. */
+ const static char BACKSPACE = 8; /*< VT52 Backspace key value. */
+ const static char BELL = 7; /*< VT52 Bell value. */
+ const static char STX = 2; /*< VT52 Start-of-text value. */
+
+protected:
+ /** @return The absolute Y position of this view. */
+ int get_actual_y()
+ {
+ unsigned long width, height;
+ int retval;
+
+ getmaxyx(this->vc_window, height, width);
+ if (this->vc_y < 0) {
+ retval = height + this->vc_y;
+ } else {
+ retval = this->vc_y;
+ }
+
+ return retval;
+ }
+
+ int get_actual_width()
+ {
+ auto retval = getmaxx(this->vc_window);
+
+ if (this->vc_width < 0) {
+ retval -= this->vc_left;
+ retval += this->vc_width;
+ } else if (this->vc_width > 0) {
+ retval = this->vc_width;
+ } else {
+ retval = retval - this->vc_left;
+ }
+ return retval;
+ }
+
+ WINDOW* vc_window{nullptr}; /*< The window that contains this view. */
+ int vc_left{0};
+ int vc_x{0}; /*< The X position of the cursor. */
+ int vc_y{0}; /*< The Y position of the cursor. */
+ int vc_max_height{0};
+ char vc_escape[16]; /*< Storage for escape sequences. */
+ int vc_escape_len{0}; /*< The number of chars in vc_escape. */
+ int vc_expected_escape_len{-1};
+ char vc_map_buffer{0}; /*<
+ * Buffer returned by map_input for trivial
+ * translations (one-to-one).
+ */
+ attr_line_t vc_line;
+};
+
+#endif
diff --git a/src/vtab_module.cc b/src/vtab_module.cc
new file mode 100644
index 0000000..043401a
--- /dev/null
+++ b/src/vtab_module.cc
@@ -0,0 +1,109 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "vtab_module.hh"
+
+#include "config.h"
+#include "lnav_util.hh"
+#include "sqlitepp.hh"
+
+std::string vtab_module_schemas;
+
+std::map<intern_string_t, std::string> vtab_module_ddls;
+
+void
+to_sqlite(sqlite3_context* ctx, const lnav::console::user_message& um)
+{
+ auto errmsg = fmt::format(
+ FMT_STRING("{}{}"), sqlitepp::ERROR_PREFIX, lnav::to_json(um));
+
+ sqlite3_result_error(ctx, errmsg.c_str(), errmsg.size());
+}
+
+lnav::console::user_message
+sqlite3_error_to_user_message(sqlite3* db)
+{
+ const auto* errmsg = sqlite3_errmsg(db);
+ if (startswith(errmsg, sqlitepp::ERROR_PREFIX)) {
+ auto from_res = lnav::from_json<lnav::console::user_message>(
+ &errmsg[strlen(sqlitepp::ERROR_PREFIX)]);
+
+ if (from_res.isOk()) {
+ return from_res.unwrap();
+ }
+
+ return lnav::console::user_message::error("internal error")
+ .with_reason(from_res.unwrapErr()[0].um_message.get_string());
+ }
+
+ return lnav::console::user_message::error("SQL statement failed")
+ .with_reason(errmsg);
+}
+
+void
+vtab_index_usage::column_used(
+ const vtab_index_constraints::const_iterator& iter)
+{
+ this->viu_min_column = std::min(iter->iColumn, this->viu_min_column);
+ this->viu_max_column = std::max(iter->iColumn, this->viu_max_column);
+ this->viu_index_info.idxNum |= (1L << iter.i_index);
+ this->viu_used_column_count += 1;
+}
+
+void
+vtab_index_usage::allocate_args(int low, int high, int required)
+{
+ int n_arg = 0;
+
+ if (this->viu_min_column != low || this->viu_max_column > high
+ || this->viu_used_column_count < required)
+ {
+ this->viu_index_info.estimatedCost = 2147483647;
+ this->viu_index_info.estimatedRows = 2147483647;
+ return;
+ }
+
+ for (int lpc = 0; lpc <= this->viu_max_column; lpc++) {
+ for (int cons_index = 0; cons_index < this->viu_index_info.nConstraint;
+ cons_index++)
+ {
+ if (this->viu_index_info.aConstraint[cons_index].iColumn != lpc) {
+ continue;
+ }
+ if (!(this->viu_index_info.idxNum & (1L << cons_index))) {
+ continue;
+ }
+
+ this->viu_index_info.aConstraintUsage[cons_index].argvIndex
+ = ++n_arg;
+ }
+ }
+ this->viu_index_info.estimatedCost = 1.0;
+ this->viu_index_info.estimatedRows = 1;
+}
diff --git a/src/vtab_module.hh b/src/vtab_module.hh
new file mode 100644
index 0000000..a45594c
--- /dev/null
+++ b/src/vtab_module.hh
@@ -0,0 +1,929 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef vtab_module_hh
+#define vtab_module_hh
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <sqlite3.h>
+
+#include "base/auto_mem.hh"
+#include "base/intern_string.hh"
+#include "base/lnav.console.hh"
+#include "base/lnav_log.hh"
+#include "base/string_util.hh"
+#include "fmt/format.h"
+#include "help_text_formatter.hh"
+#include "mapbox/variant.hpp"
+#include "optional.hpp"
+#include "shlex.resolver.hh"
+#include "sqlite-extension-func.hh"
+
+lnav::console::user_message sqlite3_error_to_user_message(sqlite3*);
+
+struct from_sqlite_conversion_error : std::exception {
+ from_sqlite_conversion_error(const char* type, int argi)
+ : e_type(type), e_argi(argi)
+ {
+ }
+
+ const char* e_type;
+ int e_argi;
+};
+
+struct sqlite_func_error : std::exception {
+ template<typename... Args>
+ explicit sqlite_func_error(fmt::string_view format_str, const Args&... args)
+ : e_what(fmt::vformat(format_str, fmt::make_format_args(args...)))
+ {
+ }
+
+ const char* what() const noexcept override
+ {
+ return this->e_what.c_str();
+ }
+
+ const std::string e_what;
+};
+
+namespace vtab_types {
+
+template<typename T>
+struct nullable {
+ T* n_value{nullptr};
+};
+
+template<typename>
+struct is_nullable : std::false_type {
+};
+
+template<typename T>
+struct is_nullable<nullable<T>> : std::true_type {
+};
+
+} // namespace vtab_types
+
+template<typename T>
+struct from_sqlite {
+ using U = typename std::remove_reference<T>::type;
+
+ inline U operator()(int argc, sqlite3_value** val, int argi)
+ {
+ return U();
+ };
+};
+
+template<>
+struct from_sqlite<bool> {
+ inline bool operator()(int argc, sqlite3_value** val, int argi)
+ {
+ if (sqlite3_value_numeric_type(val[argi]) != SQLITE_INTEGER) {
+ throw from_sqlite_conversion_error("integer", argi);
+ }
+
+ return sqlite3_value_int64(val[argi]);
+ }
+};
+
+template<>
+struct from_sqlite<int64_t> {
+ inline int64_t operator()(int argc, sqlite3_value** val, int argi)
+ {
+ if (sqlite3_value_numeric_type(val[argi]) != SQLITE_INTEGER) {
+ throw from_sqlite_conversion_error("integer", argi);
+ }
+
+ return sqlite3_value_int64(val[argi]);
+ }
+};
+
+template<>
+struct from_sqlite<sqlite3_value*> {
+ inline sqlite3_value* operator()(int argc, sqlite3_value** val, int argi)
+ {
+ return val[argi];
+ }
+};
+
+template<>
+struct from_sqlite<int> {
+ inline int operator()(int argc, sqlite3_value** val, int argi)
+ {
+ if (sqlite3_value_numeric_type(val[argi]) != SQLITE_INTEGER) {
+ throw from_sqlite_conversion_error("integer", argi);
+ }
+
+ return sqlite3_value_int(val[argi]);
+ }
+};
+
+template<>
+struct from_sqlite<const char*> {
+ inline const char* operator()(int argc, sqlite3_value** val, int argi)
+ {
+ return (const char*) sqlite3_value_text(val[argi]);
+ }
+};
+
+template<>
+struct from_sqlite<string_fragment> {
+ inline string_fragment operator()(int argc, sqlite3_value** val, int argi)
+ {
+ return string_fragment::from_bytes(
+ (const char*) sqlite3_value_blob(val[argi]),
+ sqlite3_value_bytes(val[argi]));
+ }
+};
+
+template<>
+struct from_sqlite<std::string> {
+ inline std::string operator()(int argc, sqlite3_value** val, int argi)
+ {
+ return {
+ (const char*) sqlite3_value_blob(val[argi]),
+ (size_t) sqlite3_value_bytes(val[argi]),
+ };
+ }
+};
+
+template<>
+struct from_sqlite<double> {
+ inline double operator()(int argc, sqlite3_value** val, int argi)
+ {
+ return sqlite3_value_double(val[argi]);
+ }
+};
+
+template<typename T>
+struct from_sqlite<nonstd::optional<T>> {
+ inline nonstd::optional<T> operator()(int argc,
+ sqlite3_value** val,
+ int argi)
+ {
+ if (argi >= argc || sqlite3_value_type(val[argi]) == SQLITE_NULL) {
+ return nonstd::nullopt;
+ }
+
+ return nonstd::optional<T>(from_sqlite<T>()(argc, val, argi));
+ }
+};
+
+template<typename T>
+struct from_sqlite<const std::vector<T>&> {
+ inline std::vector<T> operator()(int argc, sqlite3_value** val, int argi)
+ {
+ std::vector<T> retval;
+
+ for (int lpc = argi; lpc < argc; lpc++) {
+ retval.emplace_back(from_sqlite<T>()(argc, val, lpc));
+ }
+
+ return retval;
+ }
+};
+
+template<typename T>
+struct from_sqlite<vtab_types::nullable<T>> {
+ inline vtab_types::nullable<T> operator()(int argc,
+ sqlite3_value** val,
+ int argi)
+ {
+ return {from_sqlite<T*>()(argc, val, argi)};
+ }
+};
+
+void to_sqlite(sqlite3_context* ctx, const lnav::console::user_message& um);
+
+inline void
+to_sqlite(sqlite3_context* ctx, null_value_t)
+{
+ sqlite3_result_null(ctx);
+}
+
+inline void
+to_sqlite(sqlite3_context* ctx, const char* str)
+{
+ if (str == nullptr) {
+ sqlite3_result_null(ctx);
+ } else {
+ sqlite3_result_text(ctx, str, -1, SQLITE_STATIC);
+ }
+}
+
+inline void
+to_sqlite(sqlite3_context* ctx, text_auto_buffer buf)
+{
+ auto pair = buf.inner.release();
+ sqlite3_result_text(ctx, pair.first, pair.second, free);
+}
+
+inline void
+to_sqlite(sqlite3_context* ctx, blob_auto_buffer buf)
+{
+ auto pair = buf.inner.release();
+ sqlite3_result_blob(ctx, pair.first, pair.second, free);
+}
+
+inline void
+to_sqlite(sqlite3_context* ctx, const std::string& str)
+{
+ sqlite3_result_text(ctx, str.c_str(), str.length(), SQLITE_TRANSIENT);
+}
+
+inline void
+to_sqlite(sqlite3_context* ctx, const string_fragment& sf)
+{
+ if (sf.is_valid()) {
+ sqlite3_result_text(
+ ctx, &sf.sf_string[sf.sf_begin], sf.length(), SQLITE_TRANSIENT);
+ } else {
+ sqlite3_result_null(ctx);
+ }
+}
+
+inline void
+to_sqlite(sqlite3_context* ctx, bool val)
+{
+ sqlite3_result_int(ctx, val);
+}
+
+template<typename T>
+inline void
+to_sqlite(sqlite3_context* ctx,
+ T val,
+ typename std::enable_if<std::is_integral<T>::value
+ && !std::is_same<T, bool>::value>::type* dummy
+ = 0)
+{
+ sqlite3_result_int64(ctx, val);
+}
+
+inline void
+to_sqlite(sqlite3_context* ctx, double val)
+{
+ sqlite3_result_double(ctx, val);
+}
+
+inline void
+to_sqlite(sqlite3_context* ctx, auto_mem<char> str)
+{
+ auto free_func = str.get_free_func<void(*)(void*)>();
+ sqlite3_result_text(ctx, str.release(), -1, free_func);
+}
+
+#define JSON_SUBTYPE 74 /* Ascii for "J" */
+#define FLATTEN_SUBTYPE 0x5f
+
+template<typename T>
+inline void
+to_sqlite(sqlite3_context* ctx, nonstd::optional<T>& val)
+{
+ if (val.has_value()) {
+ to_sqlite(ctx, val.value());
+ } else {
+ sqlite3_result_null(ctx);
+ }
+}
+
+template<typename T>
+inline void
+to_sqlite(sqlite3_context* ctx, nonstd::optional<T> val)
+{
+ if (val.has_value()) {
+ to_sqlite(ctx, std::move(val.value()));
+ } else {
+ sqlite3_result_null(ctx);
+ }
+}
+
+struct ToSqliteVisitor {
+ ToSqliteVisitor(sqlite3_context* vctx) : tsv_context(vctx) {}
+
+ template<typename T>
+ void operator()(T&& t) const
+ {
+ to_sqlite(this->tsv_context, std::move(t));
+ }
+
+ sqlite3_context* tsv_context;
+};
+
+template<typename... Types>
+void
+to_sqlite(sqlite3_context* ctx, mapbox::util::variant<Types...>&& val)
+{
+ ToSqliteVisitor visitor(ctx);
+
+ mapbox::util::apply_visitor(visitor, val);
+}
+
+template<typename... Args>
+struct optional_counter {
+ constexpr static int value = 0;
+};
+
+template<typename T>
+struct optional_counter<nonstd::optional<T>> {
+ constexpr static int value = 1;
+};
+
+template<typename T, typename U>
+struct optional_counter<nonstd::optional<T>, const std::vector<U>&> {
+ constexpr static int value = 1;
+};
+
+template<typename T, typename... Rest>
+struct optional_counter<nonstd::optional<T>, Rest...> {
+ constexpr static int value = 1 + sizeof...(Rest);
+};
+
+template<typename Arg>
+struct optional_counter<Arg> {
+ constexpr static int value = 0;
+};
+
+template<typename Arg1, typename... Args>
+struct optional_counter<Arg1, Args...> : optional_counter<Args...> {
+};
+
+template<typename... Args>
+struct variadic_counter {
+ constexpr static int value = 0;
+};
+
+template<typename T>
+struct variadic_counter<const std::vector<T>&> {
+ constexpr static int value = 1;
+};
+
+template<typename Arg>
+struct variadic_counter<Arg> {
+ constexpr static int value = 0;
+};
+
+template<typename Arg1, typename... Args>
+struct variadic_counter<Arg1, Args...> : variadic_counter<Args...> {
+};
+
+template<typename F, F f>
+struct sqlite_func_adapter;
+
+template<typename Return, typename... Args, Return (*f)(Args...)>
+struct sqlite_func_adapter<Return (*)(Args...), f> {
+ constexpr static size_t OPT_COUNT = optional_counter<Args...>::value;
+ constexpr static size_t VAR_COUNT = variadic_counter<Args...>::value;
+ constexpr static size_t REQ_COUNT = sizeof...(Args) - OPT_COUNT - VAR_COUNT;
+
+ template<size_t... Idx>
+ static void func2(sqlite3_context* context,
+ int argc,
+ sqlite3_value** argv,
+ std::index_sequence<Idx...>)
+ {
+ try {
+ Return retval = f(from_sqlite<Args>()(argc, argv, Idx)...);
+
+ to_sqlite(context, std::move(retval));
+ } catch (const lnav::console::user_message& um) {
+ to_sqlite(context, um);
+ } catch (from_sqlite_conversion_error& e) {
+ char buffer[256];
+
+ snprintf(buffer,
+ sizeof(buffer),
+ "Expecting an %s for argument number %d",
+ e.e_type,
+ e.e_argi);
+ sqlite3_result_error(context, buffer, -1);
+ } catch (const std::exception& e) {
+ const auto* fd = (const FuncDef*) sqlite3_user_data(context);
+ attr_line_t error_al;
+ error_al.append("call to ");
+ format_help_text_for_term(
+ fd->fd_help, 40, error_al, help_text_content::synopsis);
+ error_al.append(" failed");
+ auto um = lnav::console::user_message::error(error_al).with_reason(
+ e.what());
+
+ to_sqlite(context, um);
+ } catch (...) {
+ sqlite3_result_error(
+ context, "Function threw an unexpected exception", -1);
+ }
+ }
+
+ static void func1(sqlite3_context* context, int argc, sqlite3_value** argv)
+ {
+ const static bool IS_NULLABLE[]
+ = {vtab_types::is_nullable<Args>::value...};
+ const static bool IS_SQLITE3_VALUE[]
+ = {std::is_same<Args, sqlite3_value*>::value...};
+
+ if ((size_t) argc < REQ_COUNT && VAR_COUNT == 0) {
+ const struct FuncDef* fd
+ = (const FuncDef*) sqlite3_user_data(context);
+ char buffer[128];
+
+ if (OPT_COUNT == 0) {
+ snprintf(buffer,
+ sizeof(buffer),
+ "%s() expects exactly %ld argument%s",
+ fd->fd_help.ht_name,
+ REQ_COUNT,
+ REQ_COUNT == 1 ? "s" : "");
+ } else {
+ snprintf(buffer,
+ sizeof(buffer),
+ "%s() expects between %ld and %ld arguments",
+ fd->fd_help.ht_name,
+ REQ_COUNT,
+ REQ_COUNT + OPT_COUNT);
+ }
+ sqlite3_result_error(context, buffer, -1);
+ return;
+ }
+
+ for (size_t lpc = 0; lpc < REQ_COUNT; lpc++) {
+ if (!IS_NULLABLE[lpc] && !IS_SQLITE3_VALUE[lpc]
+ && sqlite3_value_type(argv[lpc]) == SQLITE_NULL)
+ {
+ sqlite3_result_null(context);
+ return;
+ }
+ }
+
+ func2(context, argc, argv, std::make_index_sequence<sizeof...(Args)>{});
+ }
+
+ static FuncDef builder(help_text ht)
+ {
+ require(ht.ht_parameters.size() == sizeof...(Args));
+
+ return {
+ ht.ht_name,
+ (OPT_COUNT > 0 || VAR_COUNT > 0) ? -1 : (int) REQ_COUNT,
+ SQLITE_UTF8 | SQLITE_DETERMINISTIC,
+ 0,
+ func1,
+ ht,
+ };
+ }
+};
+
+extern std::string vtab_module_schemas;
+extern std::map<intern_string_t, std::string> vtab_module_ddls;
+
+class vtab_index_constraints {
+public:
+ vtab_index_constraints(const sqlite3_index_info* index_info)
+ : vic_index_info(*index_info)
+ {
+ }
+
+ struct const_iterator {
+ const_iterator(vtab_index_constraints* parent, int index = 0)
+ : i_parent(parent), i_index(index)
+ {
+ while (this->i_index < this->i_parent->vic_index_info.nConstraint
+ && !this->i_parent->vic_index_info.aConstraint[this->i_index]
+ .usable)
+ {
+ this->i_index += 1;
+ }
+ }
+
+ const_iterator& operator++()
+ {
+ do {
+ this->i_index += 1;
+ } while (
+ this->i_index < this->i_parent->vic_index_info.nConstraint
+ && !this->i_parent->vic_index_info.aConstraint[this->i_index]
+ .usable);
+
+ return *this;
+ }
+
+ const sqlite3_index_info::sqlite3_index_constraint& operator*() const
+ {
+ return this->i_parent->vic_index_info.aConstraint[this->i_index];
+ }
+
+ const sqlite3_index_info::sqlite3_index_constraint* operator->() const
+ {
+ return &this->i_parent->vic_index_info.aConstraint[this->i_index];
+ }
+
+ bool operator!=(const const_iterator& rhs) const
+ {
+ return this->i_parent != rhs.i_parent
+ || this->i_index != rhs.i_index;
+ }
+
+ const vtab_index_constraints* i_parent;
+ int i_index;
+ };
+
+ const_iterator begin() { return {this}; }
+
+ const_iterator end() { return {this, this->vic_index_info.nConstraint}; }
+
+private:
+ const sqlite3_index_info& vic_index_info;
+};
+
+class vtab_index_usage {
+public:
+ vtab_index_usage(sqlite3_index_info* index_info)
+ : viu_index_info(*index_info)
+ {
+ }
+
+ void column_used(const vtab_index_constraints::const_iterator& iter);
+
+ void allocate_args(int low, int high, int required);
+
+private:
+ sqlite3_index_info& viu_index_info;
+ int viu_used_column_count{0};
+ int viu_min_column{INT_MAX};
+ int viu_max_column{0};
+};
+
+struct vtab_module_base {
+ virtual int create(sqlite3* db) = 0;
+
+ virtual ~vtab_module_base() = default;
+};
+
+template<typename T>
+struct vtab_module : public vtab_module_base {
+ struct vtab {
+ explicit vtab(sqlite3* db, T& impl) : v_db(db), v_impl(impl) {}
+
+ explicit operator sqlite3_vtab*() { return &this->base; }
+
+ sqlite3_vtab v_base{};
+ sqlite3* v_db;
+ T& v_impl;
+ };
+
+ static int tvt_create(sqlite3* db,
+ void* pAux,
+ int argc,
+ const char* const* argv,
+ sqlite3_vtab** pp_vt,
+ char** pzErr)
+ {
+ auto* mod = static_cast<vtab_module<T>*>(pAux);
+ auto vt = new vtab(db, mod->vm_impl);
+
+ *pp_vt = (sqlite3_vtab*) &vt->v_base;
+
+ return sqlite3_declare_vtab(db, T::CREATE_STMT);
+ }
+
+ template<typename... Args, size_t... Idx>
+ static int apply_impl(T& obj,
+ int (T::*func)(sqlite3_vtab*,
+ sqlite3_int64&,
+ Args...),
+ sqlite3_vtab* tab,
+ sqlite3_int64& rowid,
+ sqlite3_value** argv,
+ std::index_sequence<Idx...>)
+ {
+ return (obj.*func)(
+ tab, rowid, from_sqlite<Args>()(sizeof...(Args), argv, Idx)...);
+ }
+
+ template<typename... Args>
+ static int apply(T& obj,
+ int (T::*func)(sqlite3_vtab*, sqlite3_int64&, Args...),
+ sqlite3_vtab* tab,
+ sqlite3_int64& rowid,
+ int argc,
+ sqlite3_value** argv)
+ {
+ require(sizeof...(Args) == 0 || argc == sizeof...(Args));
+
+ try {
+ return apply_impl(obj,
+ func,
+ tab,
+ rowid,
+ argv,
+ std::make_index_sequence<sizeof...(Args)>{});
+ } catch (const from_sqlite_conversion_error& e) {
+ tab->zErrMsg = sqlite3_mprintf(
+ "Expecting an %s for column number %d", e.e_type, e.e_argi);
+ return SQLITE_ERROR;
+ } catch (const std::exception& e) {
+ tab->zErrMsg = sqlite3_mprintf("%s", e.what());
+ return SQLITE_ERROR;
+ } catch (...) {
+ tab->zErrMsg
+ = sqlite3_mprintf("Encountered an unexpected exception");
+ return SQLITE_ERROR;
+ }
+ }
+
+ static int tvt_destructor(sqlite3_vtab* p_svt)
+ {
+ vtab* vt = (vtab*) p_svt;
+
+ delete vt;
+
+ return SQLITE_OK;
+ }
+
+ static int tvt_open(sqlite3_vtab* p_svt, sqlite3_vtab_cursor** pp_cursor)
+ {
+ p_svt->zErrMsg = nullptr;
+
+ auto* p_cur = new (typename T::cursor)(p_svt);
+
+ if (p_cur == nullptr) {
+ return SQLITE_NOMEM;
+ } else {
+ *pp_cursor = (sqlite3_vtab_cursor*) p_cur;
+ }
+
+ return SQLITE_OK;
+ }
+
+ static int tvt_next(sqlite3_vtab_cursor* cur)
+ {
+ auto* p_cur = (typename T::cursor*) cur;
+
+ return p_cur->next();
+ }
+
+ static int tvt_eof(sqlite3_vtab_cursor* cur)
+ {
+ auto* p_cur = (typename T::cursor*) cur;
+
+ return p_cur->eof();
+ }
+
+ static int tvt_close(sqlite3_vtab_cursor* cur)
+ {
+ auto* p_cur = (typename T::cursor*) cur;
+
+ delete p_cur;
+
+ return SQLITE_OK;
+ }
+
+ static int tvt_rowid(sqlite3_vtab_cursor* cur, sqlite_int64* p_rowid)
+ {
+ auto* p_cur = (typename T::cursor*) cur;
+
+ return p_cur->get_rowid(*p_rowid);
+ }
+
+ static int tvt_column(sqlite3_vtab_cursor* cur,
+ sqlite3_context* ctx,
+ int col)
+ {
+ auto* mod_vt = (typename vtab_module<T>::vtab*) cur->pVtab;
+ auto* p_cur = (typename T::cursor*) cur;
+
+ return mod_vt->v_impl.get_column(*p_cur, ctx, col);
+ }
+
+ static int vt_best_index(sqlite3_vtab* tab, sqlite3_index_info* p_info)
+ {
+ return SQLITE_OK;
+ }
+
+ static int vt_filter(sqlite3_vtab_cursor* p_vtc,
+ int idxNum,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv)
+ {
+ auto* p_cur = (typename T::cursor*) p_vtc;
+
+ return p_cur->reset();
+ }
+
+ static int tvt_update(sqlite3_vtab* tab,
+ int argc,
+ sqlite3_value** argv,
+ sqlite_int64* rowid)
+ {
+ auto* mod_vt = (typename vtab_module<T>::vtab*) tab;
+
+ if (argc <= 1) {
+ sqlite3_int64 rowid = sqlite3_value_int64(argv[0]);
+
+ return mod_vt->v_impl.delete_row(tab, rowid);
+ }
+
+ if (sqlite3_value_type(argv[0]) == SQLITE_NULL) {
+ sqlite3_int64* rowid2 = rowid;
+ return vtab_module<T>::apply(mod_vt->v_impl,
+ &T::insert_row,
+ tab,
+ *rowid2,
+ argc - 2,
+ argv + 2);
+ }
+
+ sqlite3_int64 index = sqlite3_value_int64(argv[0]);
+
+ if (index != sqlite3_value_int64(argv[1])) {
+ tab->zErrMsg = sqlite3_mprintf(
+ "The rowids in the lnav_views table cannot be changed");
+ return SQLITE_ERROR;
+ }
+
+ return vtab_module<T>::apply(
+ mod_vt->v_impl, &T::update_row, tab, index, argc - 2, argv + 2);
+ }
+
+ template<typename U>
+ auto addUpdate(U u) -> decltype(&U::delete_row, void())
+ {
+ this->vm_module.xUpdate = tvt_update;
+ }
+
+ template<typename U>
+ void addUpdate(...)
+ {
+ }
+
+ template<typename... Args>
+ vtab_module(Args&... args) noexcept : vm_impl(args...)
+ {
+ memset(&this->vm_module, 0, sizeof(this->vm_module));
+ this->vm_module.iVersion = 0;
+ this->vm_module.xCreate = tvt_create;
+ this->vm_module.xConnect = tvt_create;
+ this->vm_module.xOpen = tvt_open;
+ this->vm_module.xNext = tvt_next;
+ this->vm_module.xEof = tvt_eof;
+ this->vm_module.xClose = tvt_close;
+ this->vm_module.xDestroy = tvt_destructor;
+ this->vm_module.xRowid = tvt_rowid;
+ this->vm_module.xDisconnect = tvt_destructor;
+ this->vm_module.xBestIndex = vt_best_index;
+ this->vm_module.xFilter = vt_filter;
+ this->vm_module.xColumn = tvt_column;
+ this->addUpdate<T>(this->vm_impl);
+ }
+
+ ~vtab_module() override = default;
+
+ int create(sqlite3* db, const char* name)
+ {
+ auto impl_name = std::string(name);
+ vtab_module_schemas += T::CREATE_STMT;
+ vtab_module_ddls[intern_string::lookup(name)] = trim(T::CREATE_STMT);
+
+ // XXX Eponymous tables don't seem to work in older sqlite versions
+ impl_name += "_impl";
+ int rc = sqlite3_create_module(
+ db, impl_name.c_str(), &this->vm_module, this);
+ ensure(rc == SQLITE_OK);
+ auto create_stmt = fmt::format(
+ FMT_STRING("CREATE VIRTUAL TABLE {} USING {}()"), name, impl_name);
+ return sqlite3_exec(db, create_stmt.c_str(), nullptr, nullptr, nullptr);
+ }
+
+ int create(sqlite3* db) override
+ {
+ return this->create(db, T::NAME);
+ }
+
+ sqlite3_module vm_module;
+ T vm_impl;
+};
+
+template<typename T>
+struct tvt_iterator_cursor {
+ struct cursor {
+ sqlite3_vtab_cursor base{};
+
+ typename T::iterator iter;
+
+ explicit cursor(sqlite3_vtab* vt)
+ {
+ auto* mod_vt = (typename vtab_module<T>::vtab*) vt;
+
+ this->base.pVtab = vt;
+ this->iter = mod_vt->v_impl.begin();
+ }
+
+ int reset()
+ {
+ this->iter = get_handler().begin();
+
+ return SQLITE_OK;
+ }
+
+ int next()
+ {
+ if (this->iter != get_handler().end()) {
+ ++this->iter;
+ }
+
+ return SQLITE_OK;
+ }
+
+ int eof() { return this->iter == get_handler().end(); }
+
+ template<bool cond, typename U>
+ using resolvedType = typename std::enable_if<cond, U>::type;
+
+ template<typename U = int>
+ resolvedType<
+ std::is_same<std::random_access_iterator_tag,
+ typename std::iterator_traits<
+ typename T::iterator>::iterator_category>::value,
+ U>
+ get_rowid(sqlite_int64& rowid_out)
+ {
+ rowid_out = std::distance(get_handler().begin(), this->iter);
+
+ return SQLITE_OK;
+ }
+
+ template<typename U = int>
+ resolvedType<
+ !std::is_same<std::random_access_iterator_tag,
+ typename std::iterator_traits<
+ typename T::iterator>::iterator_category>::value,
+ U>
+ get_rowid(sqlite_int64& rowid_out)
+ {
+ rowid_out = get_handler().get_rowid(this->iter);
+
+ return SQLITE_OK;
+ }
+
+ protected:
+ T& get_handler()
+ {
+ auto* mod_vt = (typename vtab_module<T>::vtab*) this->base.pVtab;
+
+ return mod_vt->v_impl;
+ }
+ };
+};
+
+template<typename T>
+struct tvt_no_update : public T {
+ using T::T;
+
+ int delete_row(sqlite3_vtab* vt, sqlite3_int64 rowid)
+ {
+ vt->zErrMsg = sqlite3_mprintf("Rows cannot be deleted from this table");
+ return SQLITE_ERROR;
+ }
+
+ int insert_row(sqlite3_vtab* tab, sqlite3_int64& rowid_out)
+ {
+ tab->zErrMsg
+ = sqlite3_mprintf("Rows cannot be inserted into this table");
+ return SQLITE_ERROR;
+ }
+
+ int update_row(sqlite3_vtab* tab, sqlite3_int64& rowid_out)
+ {
+ tab->zErrMsg = sqlite3_mprintf("Rows cannot be updated in this table");
+ return SQLITE_ERROR;
+ }
+};
+
+#endif
diff --git a/src/vtab_module_json.hh b/src/vtab_module_json.hh
new file mode 100644
index 0000000..127ed42
--- /dev/null
+++ b/src/vtab_module_json.hh
@@ -0,0 +1,58 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file vtab_module_json.hh
+ */
+
+#ifndef lnav_vtab_module_json_hh
+#define lnav_vtab_module_json_hh
+
+#include "vtab_module.hh"
+#include "yajlpp/yajlpp.hh"
+
+struct flattened_json_string : json_string {
+ explicit flattened_json_string(yajl_gen_t* gen) : json_string(gen) {}
+};
+
+inline void
+to_sqlite(sqlite3_context* ctx, flattened_json_string val)
+{
+ sqlite3_result_text(
+ ctx, (const char*) val.js_content.release(), val.js_len, free);
+ sqlite3_result_subtype(ctx, FLATTEN_SUBTYPE);
+}
+
+inline void
+to_sqlite(sqlite3_context* ctx, json_string val)
+{
+ sqlite3_result_text(
+ ctx, (const char*) val.js_content.release(), val.js_len, free);
+ sqlite3_result_subtype(ctx, JSON_SUBTYPE);
+}
+
+#endif
diff --git a/src/words.json b/src/words.json
new file mode 100644
index 0000000..42252bb
--- /dev/null
+++ b/src/words.json
@@ -0,0 +1 @@
+{"data":["aback","abaft","abandoned","abashed","aberrant","abhorrent","abiding","abject","ablaze","able","abnormal","aboard","aboriginal","abortive","abounding","abrasive","abrupt","absent","absorbed","absorbing","abstracted","absurd","abundant","abusive","accept","acceptable","accessible","accidental","account","accurate","achiever","acid","acidic","acoustic","acoustics","acrid","act","action","activity","actor","actually","ad hoc","adamant","adaptable","add","addicted","addition","adhesive","adjoining","adjustment","admire","admit","adorable","adventurous","advertisement","advice","advise","afford","afraid","aftermath","afternoon","afterthought","aggressive","agonizing","agree","agreeable","agreement","ahead","air","airplane","airport","ajar","alarm","alcoholic","alert","alike","alive","alleged","allow","alluring","aloof","amazing","ambiguous","ambitious","amount","amuck","amuse","amused","amusement","amusing","analyze","ancient","anger","angle","angry","animal","animated","announce","annoy","annoyed","annoying","answer","ants","anxious","apathetic","apologise","apparatus","apparel","appear","applaud","appliance","appreciate","approval","approve","aquatic","arch","argue","argument","arithmetic","arm","army","aromatic","arrange","arrest","arrive","arrogant","art","ashamed","ask","aspiring","assorted","astonishing","attach","attack","attempt","attend","attract","attraction","attractive","aunt","auspicious","authority","automatic","available","average","avoid","awake","aware","awesome","awful","axiomatic","babies","baby","back","bad","badge","bag","bait","bake","balance","ball","ban","bang","barbarous","bare","base","baseball","bashful","basin","basket","basketball","bat","bath","bathe","battle","bawdy","bead","beam","bear","beautiful","bed","bedroom","beds","bee","beef","befitting","beg","beginner","behave","behavior","belief","believe","bell","belligerent","bells","belong","beneficial","bent","berry","berserk","best","better","bewildered","big","bike","bikes","billowy","bird","birds","birth","birthday","bit","bite","bite-sized","bitter","bizarre","black","black-and-white","blade","bleach","bless","blind","blink","blood","bloody","blot","blow","blue","blue-eyed","blush","blushing","board","boast","boat","boil","boiling","bolt","bomb","bone","book","books","boorish","boot","border","bore","bored","boring","borrow","bottle","bounce","bouncy","boundary","boundless","bow","box","boy","brainy","brake","branch","brash","brass","brave","brawny","breakable","breath","breathe","breezy","brick","bridge","brief","bright","broad","broken","brother","brown","bruise","brush","bubble","bucket","building","bulb","bump","bumpy","burly","burn","burst","bury","bushes","business","bustling","busy","butter","button","buzz","cabbage","cable","cactus","cagey","cake","cakes","calculate","calculating","calculator","calendar","call","callous","calm","camera","camp","can","cannon","canvas","cap","capable","capricious","caption","car","card","care","careful","careless","caring","carpenter","carriage","carry","cars","cart","carve","cast","cat","cats","cattle","cause","cautious","cave","ceaseless","celery","cellar","cemetery","cent","certain","chalk","challenge","chance","change","changeable","channel","charge","charming","chase","cheap","cheat","check","cheer","cheerful","cheese","chemical","cherries","cherry","chess","chew","chicken","chickens","chief","childlike","children","chilly","chin","chivalrous","choke","chop","chubby","chunky","church","circle","claim","clam","clammy","clap","class","classy","clean","clear","clever","clip","cloistered","close","closed","cloth","cloudy","clover","club","clumsy","cluttered","coach","coal","coast","coat","cobweb","coherent","coil","cold","collar","collect","color","colorful","colossal","colour","comb","combative","comfortable","command","committee","common","communicate","company","compare","comparison","compete","competition","complain","complete","complex","concentrate","concern","concerned","condemned","condition","confess","confuse","confused","connect","connection","conscious","consider","consist","contain","continue","control","cooing","cook","cool","cooperative","coordinated","copper","copy","corn","correct","cough","count","country","courageous","cover","cow","cowardly","cows","crabby","crack","cracker","crash","crate","craven","crawl","crayon","crazy","cream","creator","creature","credit","creepy","crib","crime","crook","crooked","cross","crow","crowd","crowded","crown","cruel","crush","cry","cub","cuddly","cultured","cumbersome","cup","cure","curious","curl","curly","current","curtain","curve","curved","curvy","cushion","cut","cute","cycle","cynical","dad","daffy","daily","dam","damage","damaged","damaging","damp","dance","dangerous","dapper","dare","dark","dashing","daughter","day","dazzling","dead","deadpan","deafening","dear","death","debonair","debt","decay","deceive","decide","decision","decisive","decorate","decorous","deep","deeply","deer","defeated","defective","defiant","degree","delay","delicate","delicious","delight","delightful","delirious","deliver","demonic","depend","dependent","depressed","deranged","describe","descriptive","desert","deserted","deserve","design","desire","desk","destroy","destruction","detail","detailed","detect","determined","develop","development","devilish","didactic","different","difficult","digestion","diligent","dime","dinner","dinosaurs","direction","direful","dirt","dirty","disagree","disagreeable","disappear","disapprove","disarm","disastrous","discover","discovery","discreet","discussion","disgusted","disgusting","disillusioned","dislike","dispensable","distance","distinct","distribution","disturbed","divergent","divide","division","dizzy","dock","doctor","dog","dogs","doll","dolls","domineering","donkey","door","double","doubt","doubtful","downtown","drab","draconian","drag","drain","dramatic","drawer","dream","dreary","dress","drink","drip","driving","drop","drown","drum","drunk","dry","duck","ducks","dull","dust","dusty","dynamic","dysfunctional","eager","ear","early","earn","earsplitting","earth","earthquake","earthy","easy","eatable","economic","edge","educate","educated","education","effect","efficacious","efficient","egg","eggnog","eggs","eight","elastic","elated","elbow","elderly","electric","elegant","elfin","elite","embarrass","embarrassed","eminent","employ","empty","enchanted","enchanting","encourage","encouraging","end","endurable","energetic","engine","enjoy","enormous","enter","entertain","entertaining","enthusiastic","envious","equable","equal","erect","erratic","error","escape","ethereal","evanescent","evasive","even","event","examine","example","excellent","exchange","excite","excited","exciting","exclusive","excuse","exercise","exist","existence","exotic","expand","expansion","expect","expensive","experience","expert","explain","explode","extend","extra-large","extra-small","exuberant","exultant","eye","eyes","fabulous","face","fact","fade","faded","fail","faint","fair","fairies","faithful","fall","fallacious","false","familiar","famous","fanatical","fancy","fang","fantastic","far","far-flung","farm","fascinated","fast","fasten","fat","faulty","fax","fear","fearful","fearless","feeble","feeling","feigned","female","fence","fertile","festive","fetch","few","field","fierce","file","fill","film","filthy","fine","finger","finicky","fire","fireman","first","fish","fit","five","fix","fixed","flag","flagrant","flaky","flame","flap","flash","flashy","flat","flavor","flawless","flesh","flight","flimsy","flippant","float","flock","flood","floor","flow","flower","flowers","flowery","fluffy","fluttering","fly","foamy","fog","fold","follow","food","fool","foolish","foot","force","foregoing","forgetful","fork","form","fortunate","found","four","fowl","fragile","frail","frame","frantic","free","freezing","frequent","fresh","fretful","friction","friend","friendly","friends","frighten","frightened","frightening","frog","frogs","front","fruit","fry","fuel","full","fumbling","functional","funny","furniture","furry","furtive","future","futuristic","fuzzy","gabby","gainful","gamy","gaping","garrulous","gate","gather","gaudy","gaze","geese","general","gentle","ghost","giant","giants","giddy","gifted","gigantic","giraffe","girl","girls","glamorous","glass","gleaming","glib","glistening","glorious","glossy","glove","glow","glue","godly","gold","good","goofy","gorgeous","government","governor","grab","graceful","grade","grain","grandfather","grandiose","grandmother","grape","grass","grate","grateful","gratis","gray","grease","greasy","great","greedy","green","greet","grey","grieving","grin","grip","groan","groovy","grotesque","grouchy","ground","group","growth","grubby","gruesome","grumpy","guarantee","guard","guarded","guess","guide","guiltless","guitar","gullible","gun","gusty","guttural","habitual","hair","haircut","half","hall","hallowed","halting","hammer","hand","handle","hands","handsome","handsomely","handy","hang","hanging","hapless","happen","happy","harass","harbor","hard","hard-to-find","harm","harmonious","harmony","harsh","hat","hate","hateful","haunt","head","heady","heal","health","healthy","heap","heartbreaking","heat","heavenly","heavy","hellish","help","helpful","helpless","hesitant","hideous","high","high-pitched","highfalutin","hilarious","hill","hissing","historical","history","hobbies","hole","holiday","holistic","hollow","home","homeless","homely","honey","honorable","hook","hop","hope","horn","horrible","horse","horses","hose","hospitable","hospital","hot","hour","house","houses","hover","hug","huge","hulking","hum","humdrum","humor","humorous","hungry","hunt","hurried","hurry","hurt","hushed","husky","hydrant","hypnotic","hysterical","ice","icicle","icky","icy","idea","identify","idiotic","ignorant","ignore","ill","ill-fated","ill-informed","illegal","illustrious","imaginary","imagine","immense","imminent","impartial","imperfect","impolite","important","imported","impossible","impress","improve","impulse","incandescent","include","income","incompetent","inconclusive","increase","incredible","industrious","industry","inexpensive","infamous","influence","inform","inject","injure","ink","innate","innocent","inquisitive","insect","insidious","instinctive","instruct","instrument","insurance","intelligent","intend","interest","interesting","interfere","internal","interrupt","introduce","invent","invention","invincible","invite","irate","iron","irritate","irritating","island","itch","itchy","jaded","jagged","jail","jam","jar","jazzy","jealous","jeans","jelly","jellyfish","jewel","jittery","jobless","jog","join","joke","jolly","joyous","judge","judicious","juggle","juice","juicy","jumbled","jump","jumpy","juvenile","kaput","keen","kettle","key","kick","kill","kind","kindhearted","kindly","kiss","kittens","kitty","knee","kneel","knife","knit","knock","knot","knotty","knowing","knowledge","knowledgeable","known","label","labored","laborer","lace","lackadaisical","lacking","ladybug","lake","lame","lamentable","lamp","land","language","languid","large","last","late","laugh","laughable","launch","lavish","lazy","lean","learn","learned","leather","left","leg","legal","legs","lethal","letter","letters","lettuce","level","lewd","library","license","lick","lie","light","lighten","like","likeable","limit","limping","line","linen","lip","liquid","list","listen","literate","little","live","lively","living","load","loaf","lock","locket","lonely","long","long-term","longing","look","loose","lopsided","loss","loud","loutish","love","lovely","loving","low","lowly","lucky","ludicrous","lumber","lumpy","lunch","lunchroom","lush","luxuriant","lying","lyrical","macabre","machine","macho","maddening","madly","magenta","magic","magical","magnificent","maid","mailbox","majestic","makeshift","male","malicious","mammoth","man","manage","maniacal","many","marble","march","mark","marked","market","married","marry","marvelous","mask","mass","massive","match","mate","material","materialistic","matter","mature","meal","mean","measly","measure","meat","meaty","meddle","medical","meek","meeting","mellow","melodic","melt","melted","memorize","memory","men","mend","merciful","mere","mess up","messy","metal","mice","middle","mighty","military","milk","milky","mind","mindless","mine","miniature","minister","minor","mint","minute","miscreant","miss","mist","misty","mitten","mix","mixed","moan","moaning","modern","moldy","mom","momentous","money","monkey","month","moon","moor","morning","mother","motion","motionless","mountain","mountainous","mourn","mouth","move","muddle","muddled","mug","multiply","mundane","murder","murky","muscle","mushy","mute","mysterious","nail","naive","name","nappy","narrow","nasty","nation","natural","naughty","nauseating","near","neat","nebulous","necessary","neck","need","needle","needless","needy","neighborly","nerve","nervous","nest","new","next","nice","nifty","night","nimble","nine","nippy","nod","noise","noiseless","noisy","nonchalant","nondescript","nonstop","normal","north","nose","nostalgic","nosy","note","notebook","notice","noxious","null","number","numberless","numerous","nut","nutritious","nutty","oafish","oatmeal","obedient","obeisant","obese","obey","object","obnoxious","obscene","obsequious","observant","observation","observe","obsolete","obtain","obtainable","occur","ocean","oceanic","odd","offbeat","offend","offer","office","oil","old","old-fashioned","omniscient","one","onerous","open","opposite","optimal","orange","oranges","order","ordinary","organic","ossified","outgoing","outrageous","outstanding","oval","oven","overconfident","overflow","overjoyed","overrated","overt","overwrought","owe","own","pack","paddle","page","pail","painful","painstaking","paint","pale","paltry","pan","pancake","panicky","panoramic","paper","parallel","parcel","parched","park","parsimonious","part","partner","party","pass","passenger","past","paste","pastoral","pat","pathetic","pause","payment","peace","peaceful","pear","peck","pedal","peel","peep","pen","pencil","penitent","perfect","perform","periodic","permissible","permit","perpetual","person","pest","pet","petite","pets","phobic","phone","physical","picayune","pick","pickle","picture","pie","pies","pig","pigs","pin","pinch","pine","pink","pipe","piquant","pizzas","place","placid","plain","plan","plane","planes","plant","plantation","plants","plastic","plate","plausible","play","playground","pleasant","please","pleasure","plot","plough","plucky","plug","pocket","point","pointless","poised","poison","poke","polish","polite","political","pollution","poor","pop","popcorn","porter","position","possess","possessive","possible","post","pot","potato","pour","powder","power","powerful","practice","pray","preach","precede","precious","prefer","premium","prepare","present","preserve","press","pretend","pretty","prevent","previous","price","pricey","prick","prickly","print","private","probable","produce","productive","profit","profuse","program","promise","property","prose","protect","protective","protest","proud","provide","psychedelic","psychotic","public","puffy","pull","pump","pumped","punch","puncture","punish","punishment","puny","purple","purpose","purring","push","pushy","puzzled","puzzling","quack","quaint","quarrelsome","quarter","quartz","queen","question","questionable","queue","quick","quickest","quicksand","quiet","quill","quilt","quince","quirky","quiver","quixotic","quizzical","rabbit","rabbits","rabid","race","racial","radiate","ragged","rail","railway","rain","rainstorm","rainy","raise","rake","rambunctious","rampant","range","rapid","rare","raspy","rat","rate","ratty","ray","reach","reaction","reading","ready","real","realize","reason","rebel","receipt","receive","receptive","recess","recognise","recondite","record","red","reduce","redundant","reflect","reflective","refuse","regret","regular","reign","reject","rejoice","relation","relax","release","relieved","religion","rely","remain","remarkable","remember","remind","reminiscent","remove","repair","repeat","replace","reply","report","representative","reproduce","repulsive","request","rescue","resolute","resonant","respect","responsible","rest","retire","return","reward","rhetorical","rhyme","rhythm","rice","rich","riddle","rifle","right","righteous","rightful","rigid","ring","rings","rinse","ripe","risk","ritzy","river","road","roasted","rob","robin","robust","rock","rod","roll","romantic","roof","room","roomy","root","rose","rot","rotten","rough","round","route","royal","rub","ruddy","rude","ruin","rule","run","rural","rush","rustic","ruthless","sable","sack","sad","safe","sail","salt","salty","same","sand","sassy","satisfy","satisfying","save","savory","saw","scale","scandalous","scarce","scare","scarecrow","scared","scarf","scary","scatter","scattered","scene","scent","school","science","scientific","scintillating","scissors","scold","scorch","scrape","scratch","scrawny","scream","screeching","screw","scribble","scrub","sea","seal","search","seashore","seat","second","second-hand","secret","secretary","secretive","sedate","seed","seemly","selection","selective","self","selfish","sense","separate","serious","servant","serve","settle","shade","shaggy","shake","shaky","shallow","shame","shape","share","sharp","shave","sheep","sheet","shelf","shelter","shiny","ship","shirt","shiver","shivering","shock","shocking","shoe","shoes","shop","short","show","shrill","shrug","shut","shy","sick","side","sidewalk","sigh","sign","signal","silent","silk","silky","silly","silver","simple","simplistic","sin","sincere","sink","sip","sister","sisters","six","size","skate","ski","skillful","skin","skinny","skip","skirt","sky","slap","slave","sleep","sleepy","sleet","slim","slimy","slip","slippery","slope","sloppy","slow","small","smart","smash","smell","smelly","smile","smiling","smoggy","smoke","smooth","snail","snails","snake","snakes","snatch","sneaky","sneeze","sniff","snobbish","snore","snotty","snow","soak","soap","society","sock","soda","sofa","soft","soggy","solid","somber","son","song","songs","soothe","sophisticated","sordid","sore","sort","sound","soup","sour","space","spade","spare","spark","sparkle","sparkling","special","spectacular","spell","spicy","spiders","spiffy","spiky","spill","spiritual","spiteful","splendid","spoil","sponge","spooky","spoon","spot","spotless","spotted","spotty","spray","spring","sprout","spurious","spy","squalid","square","squash","squeak","squeal","squealing","squeamish","squeeze","squirrel","stage","stain","staking","stale","stamp","standing","star","stare","start","statement","station","statuesque","stay","steadfast","steady","steam","steel","steep","steer","stem","step","stereotyped","stew","stick","sticks","sticky","stiff","stimulating","stingy","stir","stitch","stocking","stomach","stone","stop","store","stormy","story","stove","straight","strange","stranger","strap","straw","stream","street","strengthen","stretch","string","strip","striped","stroke","strong","structure","stuff","stupendous","stupid","sturdy","subdued","subsequent","substance","substantial","subtract","succeed","successful","succinct","suck","sudden","suffer","sugar","suggest","suggestion","suit","sulky","summer","sun","super","superb","superficial","supply","support","suppose","supreme","surprise","surround","suspect","suspend","swanky","sweater","sweet","sweltering","swift","swim","swing","switch","symptomatic","synonymous","system","table","taboo","tacit","tacky","tail","talented","talk","tall","tame","tan","tangible","tangy","tank","tap","tart","taste","tasteful","tasteless","tasty","tawdry","tax","teaching","team","tearful","tease","tedious","teeny","teeny-tiny","teeth","telephone","telling","temper","temporary","tempt","ten","tendency","tender","tense","tent","tenuous","terrible","terrific","terrify","territory","test","tested","testy","texture","thank","thankful","thaw","theory","therapeutic","thick","thin","thing","things","thinkable","third","thirsty","thought","thoughtful","thoughtless","thread","threatening","three","thrill","throat","throne","thumb","thunder","thundering","tick","ticket","tickle","tidy","tie","tiger","tight","tightfisted","time","tin","tiny","tip","tire","tired","tiresome","title","toad","toe","toes","tomatoes","tongue","tooth","toothbrush","toothpaste","toothsome","top","torpid","touch","tough","tour","tow","towering","town","toy","toys","trace","trade","trail","train","trains","tramp","tranquil","transport","trap","trashy","travel","tray","treat","treatment","tree","trees","tremble","tremendous","trick","tricky","trip","trite","trot","trouble","troubled","trousers","truck","trucks","truculent","true","trust","truthful","try","tub","tug","tumble","turkey","turn","twig","twist","two","type","typical","ubiquitous","ugliest","ugly","ultra","umbrella","unable","unaccountable","unadvised","unarmed","unbecoming","unbiased","uncle","uncovered","understood","underwear","undesirable","undress","unequal","unequaled","uneven","unfasten","unhealthy","uninterested","unique","unit","unite","unkempt","unknown","unlock","unnatural","unpack","unruly","unsightly","unsuitable","untidy","unused","unusual","unwieldy","unwritten","upbeat","uppity","upset","uptight","use","used","useful","useless","utopian","utter","uttermost","vacation","vacuous","vagabond","vague","valuable","value","van","vanish","various","vase","vast","vegetable","veil","vein","vengeful","venomous","verdant","verse","versed","vessel","vest","victorious","view","vigorous","violent","violet","visit","visitor","vivacious","voice","voiceless","volatile","volcano","volleyball","voracious","voyage","vulgar","wacky","waggish","wail","wait","waiting","wakeful","walk","wall","wander","wandering","want","wanting","war","warlike","warm","warn","wary","wash","waste","wasteful","watch","water","watery","wave","waves","wax","way","weak","wealth","wealthy","weary","weather","week","weigh","weight","welcome","well-groomed","well-made","well-off","well-to-do","wet","wheel","whimsical","whine","whip","whirl","whisper","whispering","whistle","white","whole","wholesale","wicked","wide","wide-eyed","wiggly","wild","wilderness","willing","wind","window","windy","wine","wing","wink","winter","wipe","wire","wiry","wise","wish","wistful","witty","wobble","woebegone","woman","womanly","women","wonder","wonderful","wood","wooden","wool","woozy","word","work","workable","worm","worried","worry","worthless","wound","wrap","wrathful","wreck","wren","wrench","wrestle","wretched","wriggle","wrist","writer","writing","wrong","wry","x-ray","yak","yam","yard","yarn","yawn","year","yell","yellow","yielding","yoke","young","youthful","yummy","zany","zealous","zebra","zephyr","zesty","zinc","zip","zipper","zippy","zonked","zoo","zoom"]} \ No newline at end of file
diff --git a/src/ww898/cp_utf8.hpp b/src/ww898/cp_utf8.hpp
new file mode 100644
index 0000000..8eaa133
--- /dev/null
+++ b/src/ww898/cp_utf8.hpp
@@ -0,0 +1,171 @@
+/*
+ * MIT License
+ *
+ * Copyright (c) 2017-2019 Mikhail Pilin
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#pragma once
+
+#include <cstdint>
+#include <utility>
+#include <stdexcept>
+
+#include "base/result.h"
+
+namespace ww898 {
+namespace utf {
+
+// Supported combinations:
+// 0xxx_xxxx
+// 110x_xxxx 10xx_xxxx
+// 1110_xxxx 10xx_xxxx 10xx_xxxx
+// 1111_0xxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+// 1111_10xx 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+// 1111_110x 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+struct utf8 final
+{
+ static size_t const max_unicode_symbol_size = 4;
+ static size_t const max_supported_symbol_size = 6;
+
+ static uint32_t const max_supported_code_point = 0x7FFFFFFF;
+
+ using char_type = uint8_t;
+
+ template<typename PeekFn>
+ static Result<size_t, const char *> char_size(PeekFn && peek_fn)
+ {
+ const std::pair<char_type, size_t> peek_res = std::forward<PeekFn>(peek_fn)();
+ const auto ch0 = peek_res.first;
+ const auto remaining = peek_res.second;
+ size_t retval = 0;
+
+ if (ch0 < 0x80) { // 0xxx_xxxx
+ retval = 1;
+ } else if (ch0 < 0xC0) {
+ return Err("The utf8 first char in sequence is incorrect");
+ } else if (ch0 < 0xE0) { // 110x_xxxx 10xx_xxxx
+ retval = 2;
+ } else if (ch0 < 0xF0) { // 1110_xxxx 10xx_xxxx 10xx_xxxx
+ retval = 3;
+ } else if (ch0 < 0xF8) { // 1111_0xxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+ retval = 4;
+ } else if (ch0 < 0xFC) { // 1111_10xx 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+ retval = 5;
+ } else if (ch0 < 0xFE) { // 1111_110x 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+ retval = 6;
+ } else {
+ return Err("The utf8 first char in sequence is incorrect");
+ }
+ if (retval - 1 > remaining) {
+ return Err("Truncated utf8 sequence");
+ }
+ return Ok(retval);
+ }
+
+ template<typename ReadFn>
+ static uint32_t read(ReadFn && read_fn)
+ {
+ char_type const ch0 = read_fn();
+ if (ch0 < 0x80) // 0xxx_xxxx
+ return ch0;
+ if (ch0 < 0xC0)
+ throw std::runtime_error("The utf8 first char in sequence is incorrect");
+ if (ch0 < 0xE0) // 110x_xxxx 10xx_xxxx
+ {
+ char_type const ch1 = read_fn(); if (ch1 >> 6 != 2) goto _err;
+ return (ch0 << 6) + ch1 - 0x3080;
+ }
+ if (ch0 < 0xF0) // 1110_xxxx 10xx_xxxx 10xx_xxxx
+ {
+ char_type const ch1 = read_fn(); if (ch1 >> 6 != 2) goto _err;
+ char_type const ch2 = read_fn(); if (ch2 >> 6 != 2) goto _err;
+ return (ch0 << 12) + (ch1 << 6) + ch2 - 0xE2080;
+ }
+ if (ch0 < 0xF8) // 1111_0xxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+ {
+ char_type const ch1 = read_fn(); if (ch1 >> 6 != 2) goto _err;
+ char_type const ch2 = read_fn(); if (ch2 >> 6 != 2) goto _err;
+ char_type const ch3 = read_fn(); if (ch3 >> 6 != 2) goto _err;
+ return (ch0 << 18) + (ch1 << 12) + (ch2 << 6) + ch3 - 0x3C82080;
+ }
+ if (ch0 < 0xFC) // 1111_10xx 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+ {
+ char_type const ch1 = read_fn(); if (ch1 >> 6 != 2) goto _err;
+ char_type const ch2 = read_fn(); if (ch2 >> 6 != 2) goto _err;
+ char_type const ch3 = read_fn(); if (ch3 >> 6 != 2) goto _err;
+ char_type const ch4 = read_fn(); if (ch4 >> 6 != 2) goto _err;
+ return (ch0 << 24) + (ch1 << 18) + (ch2 << 12) + (ch3 << 6) + ch4 - 0xFA082080;
+ }
+ if (ch0 < 0xFE) // 1111_110x 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+ {
+ char_type const ch1 = read_fn(); if (ch1 >> 6 != 2) goto _err;
+ char_type const ch2 = read_fn(); if (ch2 >> 6 != 2) goto _err;
+ char_type const ch3 = read_fn(); if (ch3 >> 6 != 2) goto _err;
+ char_type const ch4 = read_fn(); if (ch4 >> 6 != 2) goto _err;
+ char_type const ch5 = read_fn(); if (ch5 >> 6 != 2) goto _err;
+ return (ch0 << 30) + (ch1 << 24) + (ch2 << 18) + (ch3 << 12) + (ch4 << 6) + ch5 - 0x82082080;
+ }
+ throw std::runtime_error("The utf8 first char in sequence is incorrect");
+ _err: throw std::runtime_error("The utf8 slave char in sequence is incorrect");
+ }
+
+ template<typename WriteFn>
+ static void write(uint32_t const cp, WriteFn && write_fn)
+ {
+ if (cp < 0x80) // 0xxx_xxxx
+ write_fn(static_cast<char_type>(cp));
+ else if (cp < 0x800) // 110x_xxxx 10xx_xxxx
+ {
+ write_fn(static_cast<char_type>(0xC0 | cp >> 6));
+ goto _1;
+ }
+ else if (cp < 0x10000) // 1110_xxxx 10xx_xxxx 10xx_xxxx
+ {
+ write_fn(static_cast<char_type>(0xE0 | cp >> 12));
+ goto _2;
+ }
+ else if (cp < 0x200000) // 1111_0xxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+ {
+ write_fn(static_cast<char_type>(0xF0 | cp >> 18));
+ goto _3;
+ }
+ else if (cp < 0x4000000) // 1111_10xx 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+ {
+ write_fn(static_cast<char_type>(0xF8 | cp >> 24));
+ goto _4;
+ }
+ else if (cp < 0x80000000) // 1111_110x 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx 10xx_xxxx
+ {
+ write_fn(static_cast<char_type>(0xFC | cp >> 30));
+ goto _5;
+ }
+ else
+ throw std::runtime_error("Tool large UTF8 code point");
+ return;
+ _5: write_fn(static_cast<char_type>(0x80 | (cp >> 24 & 0x3F)));
+ _4: write_fn(static_cast<char_type>(0x80 | (cp >> 18 & 0x3F)));
+ _3: write_fn(static_cast<char_type>(0x80 | (cp >> 12 & 0x3F)));
+ _2: write_fn(static_cast<char_type>(0x80 | (cp >> 6 & 0x3F)));
+ _1: write_fn(static_cast<char_type>(0x80 | (cp & 0x3F)));
+ }
+};
+
+}}
diff --git a/src/xml-entities.json b/src/xml-entities.json
new file mode 100644
index 0000000..557170b
--- /dev/null
+++ b/src/xml-entities.json
@@ -0,0 +1,2233 @@
+{
+ "&AElig": { "codepoints": [198], "characters": "\u00C6" },
+ "&AElig;": { "codepoints": [198], "characters": "\u00C6" },
+ "&AMP": { "codepoints": [38], "characters": "\u0026" },
+ "&AMP;": { "codepoints": [38], "characters": "\u0026" },
+ "&Aacute": { "codepoints": [193], "characters": "\u00C1" },
+ "&Aacute;": { "codepoints": [193], "characters": "\u00C1" },
+ "&Abreve;": { "codepoints": [258], "characters": "\u0102" },
+ "&Acirc": { "codepoints": [194], "characters": "\u00C2" },
+ "&Acirc;": { "codepoints": [194], "characters": "\u00C2" },
+ "&Acy;": { "codepoints": [1040], "characters": "\u0410" },
+ "&Afr;": { "codepoints": [120068], "characters": "\uD835\uDD04" },
+ "&Agrave": { "codepoints": [192], "characters": "\u00C0" },
+ "&Agrave;": { "codepoints": [192], "characters": "\u00C0" },
+ "&Alpha;": { "codepoints": [913], "characters": "\u0391" },
+ "&Amacr;": { "codepoints": [256], "characters": "\u0100" },
+ "&And;": { "codepoints": [10835], "characters": "\u2A53" },
+ "&Aogon;": { "codepoints": [260], "characters": "\u0104" },
+ "&Aopf;": { "codepoints": [120120], "characters": "\uD835\uDD38" },
+ "&ApplyFunction;": { "codepoints": [8289], "characters": "\u2061" },
+ "&Aring": { "codepoints": [197], "characters": "\u00C5" },
+ "&Aring;": { "codepoints": [197], "characters": "\u00C5" },
+ "&Ascr;": { "codepoints": [119964], "characters": "\uD835\uDC9C" },
+ "&Assign;": { "codepoints": [8788], "characters": "\u2254" },
+ "&Atilde": { "codepoints": [195], "characters": "\u00C3" },
+ "&Atilde;": { "codepoints": [195], "characters": "\u00C3" },
+ "&Auml": { "codepoints": [196], "characters": "\u00C4" },
+ "&Auml;": { "codepoints": [196], "characters": "\u00C4" },
+ "&Backslash;": { "codepoints": [8726], "characters": "\u2216" },
+ "&Barv;": { "codepoints": [10983], "characters": "\u2AE7" },
+ "&Barwed;": { "codepoints": [8966], "characters": "\u2306" },
+ "&Bcy;": { "codepoints": [1041], "characters": "\u0411" },
+ "&Because;": { "codepoints": [8757], "characters": "\u2235" },
+ "&Bernoullis;": { "codepoints": [8492], "characters": "\u212C" },
+ "&Beta;": { "codepoints": [914], "characters": "\u0392" },
+ "&Bfr;": { "codepoints": [120069], "characters": "\uD835\uDD05" },
+ "&Bopf;": { "codepoints": [120121], "characters": "\uD835\uDD39" },
+ "&Breve;": { "codepoints": [728], "characters": "\u02D8" },
+ "&Bscr;": { "codepoints": [8492], "characters": "\u212C" },
+ "&Bumpeq;": { "codepoints": [8782], "characters": "\u224E" },
+ "&CHcy;": { "codepoints": [1063], "characters": "\u0427" },
+ "&COPY": { "codepoints": [169], "characters": "\u00A9" },
+ "&COPY;": { "codepoints": [169], "characters": "\u00A9" },
+ "&Cacute;": { "codepoints": [262], "characters": "\u0106" },
+ "&Cap;": { "codepoints": [8914], "characters": "\u22D2" },
+ "&CapitalDifferentialD;": { "codepoints": [8517], "characters": "\u2145" },
+ "&Cayleys;": { "codepoints": [8493], "characters": "\u212D" },
+ "&Ccaron;": { "codepoints": [268], "characters": "\u010C" },
+ "&Ccedil": { "codepoints": [199], "characters": "\u00C7" },
+ "&Ccedil;": { "codepoints": [199], "characters": "\u00C7" },
+ "&Ccirc;": { "codepoints": [264], "characters": "\u0108" },
+ "&Cconint;": { "codepoints": [8752], "characters": "\u2230" },
+ "&Cdot;": { "codepoints": [266], "characters": "\u010A" },
+ "&Cedilla;": { "codepoints": [184], "characters": "\u00B8" },
+ "&CenterDot;": { "codepoints": [183], "characters": "\u00B7" },
+ "&Cfr;": { "codepoints": [8493], "characters": "\u212D" },
+ "&Chi;": { "codepoints": [935], "characters": "\u03A7" },
+ "&CircleDot;": { "codepoints": [8857], "characters": "\u2299" },
+ "&CircleMinus;": { "codepoints": [8854], "characters": "\u2296" },
+ "&CirclePlus;": { "codepoints": [8853], "characters": "\u2295" },
+ "&CircleTimes;": { "codepoints": [8855], "characters": "\u2297" },
+ "&ClockwiseContourIntegral;": { "codepoints": [8754], "characters": "\u2232" },
+ "&CloseCurlyDoubleQuote;": { "codepoints": [8221], "characters": "\u201D" },
+ "&CloseCurlyQuote;": { "codepoints": [8217], "characters": "\u2019" },
+ "&Colon;": { "codepoints": [8759], "characters": "\u2237" },
+ "&Colone;": { "codepoints": [10868], "characters": "\u2A74" },
+ "&Congruent;": { "codepoints": [8801], "characters": "\u2261" },
+ "&Conint;": { "codepoints": [8751], "characters": "\u222F" },
+ "&ContourIntegral;": { "codepoints": [8750], "characters": "\u222E" },
+ "&Copf;": { "codepoints": [8450], "characters": "\u2102" },
+ "&Coproduct;": { "codepoints": [8720], "characters": "\u2210" },
+ "&CounterClockwiseContourIntegral;": { "codepoints": [8755], "characters": "\u2233" },
+ "&Cross;": { "codepoints": [10799], "characters": "\u2A2F" },
+ "&Cscr;": { "codepoints": [119966], "characters": "\uD835\uDC9E" },
+ "&Cup;": { "codepoints": [8915], "characters": "\u22D3" },
+ "&CupCap;": { "codepoints": [8781], "characters": "\u224D" },
+ "&DD;": { "codepoints": [8517], "characters": "\u2145" },
+ "&DDotrahd;": { "codepoints": [10513], "characters": "\u2911" },
+ "&DJcy;": { "codepoints": [1026], "characters": "\u0402" },
+ "&DScy;": { "codepoints": [1029], "characters": "\u0405" },
+ "&DZcy;": { "codepoints": [1039], "characters": "\u040F" },
+ "&Dagger;": { "codepoints": [8225], "characters": "\u2021" },
+ "&Darr;": { "codepoints": [8609], "characters": "\u21A1" },
+ "&Dashv;": { "codepoints": [10980], "characters": "\u2AE4" },
+ "&Dcaron;": { "codepoints": [270], "characters": "\u010E" },
+ "&Dcy;": { "codepoints": [1044], "characters": "\u0414" },
+ "&Del;": { "codepoints": [8711], "characters": "\u2207" },
+ "&Delta;": { "codepoints": [916], "characters": "\u0394" },
+ "&Dfr;": { "codepoints": [120071], "characters": "\uD835\uDD07" },
+ "&DiacriticalAcute;": { "codepoints": [180], "characters": "\u00B4" },
+ "&DiacriticalDot;": { "codepoints": [729], "characters": "\u02D9" },
+ "&DiacriticalDoubleAcute;": { "codepoints": [733], "characters": "\u02DD" },
+ "&DiacriticalGrave;": { "codepoints": [96], "characters": "\u0060" },
+ "&DiacriticalTilde;": { "codepoints": [732], "characters": "\u02DC" },
+ "&Diamond;": { "codepoints": [8900], "characters": "\u22C4" },
+ "&DifferentialD;": { "codepoints": [8518], "characters": "\u2146" },
+ "&Dopf;": { "codepoints": [120123], "characters": "\uD835\uDD3B" },
+ "&Dot;": { "codepoints": [168], "characters": "\u00A8" },
+ "&DotDot;": { "codepoints": [8412], "characters": "\u20DC" },
+ "&DotEqual;": { "codepoints": [8784], "characters": "\u2250" },
+ "&DoubleContourIntegral;": { "codepoints": [8751], "characters": "\u222F" },
+ "&DoubleDot;": { "codepoints": [168], "characters": "\u00A8" },
+ "&DoubleDownArrow;": { "codepoints": [8659], "characters": "\u21D3" },
+ "&DoubleLeftArrow;": { "codepoints": [8656], "characters": "\u21D0" },
+ "&DoubleLeftRightArrow;": { "codepoints": [8660], "characters": "\u21D4" },
+ "&DoubleLeftTee;": { "codepoints": [10980], "characters": "\u2AE4" },
+ "&DoubleLongLeftArrow;": { "codepoints": [10232], "characters": "\u27F8" },
+ "&DoubleLongLeftRightArrow;": { "codepoints": [10234], "characters": "\u27FA" },
+ "&DoubleLongRightArrow;": { "codepoints": [10233], "characters": "\u27F9" },
+ "&DoubleRightArrow;": { "codepoints": [8658], "characters": "\u21D2" },
+ "&DoubleRightTee;": { "codepoints": [8872], "characters": "\u22A8" },
+ "&DoubleUpArrow;": { "codepoints": [8657], "characters": "\u21D1" },
+ "&DoubleUpDownArrow;": { "codepoints": [8661], "characters": "\u21D5" },
+ "&DoubleVerticalBar;": { "codepoints": [8741], "characters": "\u2225" },
+ "&DownArrow;": { "codepoints": [8595], "characters": "\u2193" },
+ "&DownArrowBar;": { "codepoints": [10515], "characters": "\u2913" },
+ "&DownArrowUpArrow;": { "codepoints": [8693], "characters": "\u21F5" },
+ "&DownBreve;": { "codepoints": [785], "characters": "\u0311" },
+ "&DownLeftRightVector;": { "codepoints": [10576], "characters": "\u2950" },
+ "&DownLeftTeeVector;": { "codepoints": [10590], "characters": "\u295E" },
+ "&DownLeftVector;": { "codepoints": [8637], "characters": "\u21BD" },
+ "&DownLeftVectorBar;": { "codepoints": [10582], "characters": "\u2956" },
+ "&DownRightTeeVector;": { "codepoints": [10591], "characters": "\u295F" },
+ "&DownRightVector;": { "codepoints": [8641], "characters": "\u21C1" },
+ "&DownRightVectorBar;": { "codepoints": [10583], "characters": "\u2957" },
+ "&DownTee;": { "codepoints": [8868], "characters": "\u22A4" },
+ "&DownTeeArrow;": { "codepoints": [8615], "characters": "\u21A7" },
+ "&Downarrow;": { "codepoints": [8659], "characters": "\u21D3" },
+ "&Dscr;": { "codepoints": [119967], "characters": "\uD835\uDC9F" },
+ "&Dstrok;": { "codepoints": [272], "characters": "\u0110" },
+ "&ENG;": { "codepoints": [330], "characters": "\u014A" },
+ "&ETH": { "codepoints": [208], "characters": "\u00D0" },
+ "&ETH;": { "codepoints": [208], "characters": "\u00D0" },
+ "&Eacute": { "codepoints": [201], "characters": "\u00C9" },
+ "&Eacute;": { "codepoints": [201], "characters": "\u00C9" },
+ "&Ecaron;": { "codepoints": [282], "characters": "\u011A" },
+ "&Ecirc": { "codepoints": [202], "characters": "\u00CA" },
+ "&Ecirc;": { "codepoints": [202], "characters": "\u00CA" },
+ "&Ecy;": { "codepoints": [1069], "characters": "\u042D" },
+ "&Edot;": { "codepoints": [278], "characters": "\u0116" },
+ "&Efr;": { "codepoints": [120072], "characters": "\uD835\uDD08" },
+ "&Egrave": { "codepoints": [200], "characters": "\u00C8" },
+ "&Egrave;": { "codepoints": [200], "characters": "\u00C8" },
+ "&Element;": { "codepoints": [8712], "characters": "\u2208" },
+ "&Emacr;": { "codepoints": [274], "characters": "\u0112" },
+ "&EmptySmallSquare;": { "codepoints": [9723], "characters": "\u25FB" },
+ "&EmptyVerySmallSquare;": { "codepoints": [9643], "characters": "\u25AB" },
+ "&Eogon;": { "codepoints": [280], "characters": "\u0118" },
+ "&Eopf;": { "codepoints": [120124], "characters": "\uD835\uDD3C" },
+ "&Epsilon;": { "codepoints": [917], "characters": "\u0395" },
+ "&Equal;": { "codepoints": [10869], "characters": "\u2A75" },
+ "&EqualTilde;": { "codepoints": [8770], "characters": "\u2242" },
+ "&Equilibrium;": { "codepoints": [8652], "characters": "\u21CC" },
+ "&Escr;": { "codepoints": [8496], "characters": "\u2130" },
+ "&Esim;": { "codepoints": [10867], "characters": "\u2A73" },
+ "&Eta;": { "codepoints": [919], "characters": "\u0397" },
+ "&Euml": { "codepoints": [203], "characters": "\u00CB" },
+ "&Euml;": { "codepoints": [203], "characters": "\u00CB" },
+ "&Exists;": { "codepoints": [8707], "characters": "\u2203" },
+ "&ExponentialE;": { "codepoints": [8519], "characters": "\u2147" },
+ "&Fcy;": { "codepoints": [1060], "characters": "\u0424" },
+ "&Ffr;": { "codepoints": [120073], "characters": "\uD835\uDD09" },
+ "&FilledSmallSquare;": { "codepoints": [9724], "characters": "\u25FC" },
+ "&FilledVerySmallSquare;": { "codepoints": [9642], "characters": "\u25AA" },
+ "&Fopf;": { "codepoints": [120125], "characters": "\uD835\uDD3D" },
+ "&ForAll;": { "codepoints": [8704], "characters": "\u2200" },
+ "&Fouriertrf;": { "codepoints": [8497], "characters": "\u2131" },
+ "&Fscr;": { "codepoints": [8497], "characters": "\u2131" },
+ "&GJcy;": { "codepoints": [1027], "characters": "\u0403" },
+ "&GT": { "codepoints": [62], "characters": "\u003E" },
+ "&GT;": { "codepoints": [62], "characters": "\u003E" },
+ "&Gamma;": { "codepoints": [915], "characters": "\u0393" },
+ "&Gammad;": { "codepoints": [988], "characters": "\u03DC" },
+ "&Gbreve;": { "codepoints": [286], "characters": "\u011E" },
+ "&Gcedil;": { "codepoints": [290], "characters": "\u0122" },
+ "&Gcirc;": { "codepoints": [284], "characters": "\u011C" },
+ "&Gcy;": { "codepoints": [1043], "characters": "\u0413" },
+ "&Gdot;": { "codepoints": [288], "characters": "\u0120" },
+ "&Gfr;": { "codepoints": [120074], "characters": "\uD835\uDD0A" },
+ "&Gg;": { "codepoints": [8921], "characters": "\u22D9" },
+ "&Gopf;": { "codepoints": [120126], "characters": "\uD835\uDD3E" },
+ "&GreaterEqual;": { "codepoints": [8805], "characters": "\u2265" },
+ "&GreaterEqualLess;": { "codepoints": [8923], "characters": "\u22DB" },
+ "&GreaterFullEqual;": { "codepoints": [8807], "characters": "\u2267" },
+ "&GreaterGreater;": { "codepoints": [10914], "characters": "\u2AA2" },
+ "&GreaterLess;": { "codepoints": [8823], "characters": "\u2277" },
+ "&GreaterSlantEqual;": { "codepoints": [10878], "characters": "\u2A7E" },
+ "&GreaterTilde;": { "codepoints": [8819], "characters": "\u2273" },
+ "&Gscr;": { "codepoints": [119970], "characters": "\uD835\uDCA2" },
+ "&Gt;": { "codepoints": [8811], "characters": "\u226B" },
+ "&HARDcy;": { "codepoints": [1066], "characters": "\u042A" },
+ "&Hacek;": { "codepoints": [711], "characters": "\u02C7" },
+ "&Hat;": { "codepoints": [94], "characters": "\u005E" },
+ "&Hcirc;": { "codepoints": [292], "characters": "\u0124" },
+ "&Hfr;": { "codepoints": [8460], "characters": "\u210C" },
+ "&HilbertSpace;": { "codepoints": [8459], "characters": "\u210B" },
+ "&Hopf;": { "codepoints": [8461], "characters": "\u210D" },
+ "&HorizontalLine;": { "codepoints": [9472], "characters": "\u2500" },
+ "&Hscr;": { "codepoints": [8459], "characters": "\u210B" },
+ "&Hstrok;": { "codepoints": [294], "characters": "\u0126" },
+ "&HumpDownHump;": { "codepoints": [8782], "characters": "\u224E" },
+ "&HumpEqual;": { "codepoints": [8783], "characters": "\u224F" },
+ "&IEcy;": { "codepoints": [1045], "characters": "\u0415" },
+ "&IJlig;": { "codepoints": [306], "characters": "\u0132" },
+ "&IOcy;": { "codepoints": [1025], "characters": "\u0401" },
+ "&Iacute": { "codepoints": [205], "characters": "\u00CD" },
+ "&Iacute;": { "codepoints": [205], "characters": "\u00CD" },
+ "&Icirc": { "codepoints": [206], "characters": "\u00CE" },
+ "&Icirc;": { "codepoints": [206], "characters": "\u00CE" },
+ "&Icy;": { "codepoints": [1048], "characters": "\u0418" },
+ "&Idot;": { "codepoints": [304], "characters": "\u0130" },
+ "&Ifr;": { "codepoints": [8465], "characters": "\u2111" },
+ "&Igrave": { "codepoints": [204], "characters": "\u00CC" },
+ "&Igrave;": { "codepoints": [204], "characters": "\u00CC" },
+ "&Im;": { "codepoints": [8465], "characters": "\u2111" },
+ "&Imacr;": { "codepoints": [298], "characters": "\u012A" },
+ "&ImaginaryI;": { "codepoints": [8520], "characters": "\u2148" },
+ "&Implies;": { "codepoints": [8658], "characters": "\u21D2" },
+ "&Int;": { "codepoints": [8748], "characters": "\u222C" },
+ "&Integral;": { "codepoints": [8747], "characters": "\u222B" },
+ "&Intersection;": { "codepoints": [8898], "characters": "\u22C2" },
+ "&InvisibleComma;": { "codepoints": [8291], "characters": "\u2063" },
+ "&InvisibleTimes;": { "codepoints": [8290], "characters": "\u2062" },
+ "&Iogon;": { "codepoints": [302], "characters": "\u012E" },
+ "&Iopf;": { "codepoints": [120128], "characters": "\uD835\uDD40" },
+ "&Iota;": { "codepoints": [921], "characters": "\u0399" },
+ "&Iscr;": { "codepoints": [8464], "characters": "\u2110" },
+ "&Itilde;": { "codepoints": [296], "characters": "\u0128" },
+ "&Iukcy;": { "codepoints": [1030], "characters": "\u0406" },
+ "&Iuml": { "codepoints": [207], "characters": "\u00CF" },
+ "&Iuml;": { "codepoints": [207], "characters": "\u00CF" },
+ "&Jcirc;": { "codepoints": [308], "characters": "\u0134" },
+ "&Jcy;": { "codepoints": [1049], "characters": "\u0419" },
+ "&Jfr;": { "codepoints": [120077], "characters": "\uD835\uDD0D" },
+ "&Jopf;": { "codepoints": [120129], "characters": "\uD835\uDD41" },
+ "&Jscr;": { "codepoints": [119973], "characters": "\uD835\uDCA5" },
+ "&Jsercy;": { "codepoints": [1032], "characters": "\u0408" },
+ "&Jukcy;": { "codepoints": [1028], "characters": "\u0404" },
+ "&KHcy;": { "codepoints": [1061], "characters": "\u0425" },
+ "&KJcy;": { "codepoints": [1036], "characters": "\u040C" },
+ "&Kappa;": { "codepoints": [922], "characters": "\u039A" },
+ "&Kcedil;": { "codepoints": [310], "characters": "\u0136" },
+ "&Kcy;": { "codepoints": [1050], "characters": "\u041A" },
+ "&Kfr;": { "codepoints": [120078], "characters": "\uD835\uDD0E" },
+ "&Kopf;": { "codepoints": [120130], "characters": "\uD835\uDD42" },
+ "&Kscr;": { "codepoints": [119974], "characters": "\uD835\uDCA6" },
+ "&LJcy;": { "codepoints": [1033], "characters": "\u0409" },
+ "&LT": { "codepoints": [60], "characters": "\u003C" },
+ "&LT;": { "codepoints": [60], "characters": "\u003C" },
+ "&Lacute;": { "codepoints": [313], "characters": "\u0139" },
+ "&Lambda;": { "codepoints": [923], "characters": "\u039B" },
+ "&Lang;": { "codepoints": [10218], "characters": "\u27EA" },
+ "&Laplacetrf;": { "codepoints": [8466], "characters": "\u2112" },
+ "&Larr;": { "codepoints": [8606], "characters": "\u219E" },
+ "&Lcaron;": { "codepoints": [317], "characters": "\u013D" },
+ "&Lcedil;": { "codepoints": [315], "characters": "\u013B" },
+ "&Lcy;": { "codepoints": [1051], "characters": "\u041B" },
+ "&LeftAngleBracket;": { "codepoints": [10216], "characters": "\u27E8" },
+ "&LeftArrow;": { "codepoints": [8592], "characters": "\u2190" },
+ "&LeftArrowBar;": { "codepoints": [8676], "characters": "\u21E4" },
+ "&LeftArrowRightArrow;": { "codepoints": [8646], "characters": "\u21C6" },
+ "&LeftCeiling;": { "codepoints": [8968], "characters": "\u2308" },
+ "&LeftDoubleBracket;": { "codepoints": [10214], "characters": "\u27E6" },
+ "&LeftDownTeeVector;": { "codepoints": [10593], "characters": "\u2961" },
+ "&LeftDownVector;": { "codepoints": [8643], "characters": "\u21C3" },
+ "&LeftDownVectorBar;": { "codepoints": [10585], "characters": "\u2959" },
+ "&LeftFloor;": { "codepoints": [8970], "characters": "\u230A" },
+ "&LeftRightArrow;": { "codepoints": [8596], "characters": "\u2194" },
+ "&LeftRightVector;": { "codepoints": [10574], "characters": "\u294E" },
+ "&LeftTee;": { "codepoints": [8867], "characters": "\u22A3" },
+ "&LeftTeeArrow;": { "codepoints": [8612], "characters": "\u21A4" },
+ "&LeftTeeVector;": { "codepoints": [10586], "characters": "\u295A" },
+ "&LeftTriangle;": { "codepoints": [8882], "characters": "\u22B2" },
+ "&LeftTriangleBar;": { "codepoints": [10703], "characters": "\u29CF" },
+ "&LeftTriangleEqual;": { "codepoints": [8884], "characters": "\u22B4" },
+ "&LeftUpDownVector;": { "codepoints": [10577], "characters": "\u2951" },
+ "&LeftUpTeeVector;": { "codepoints": [10592], "characters": "\u2960" },
+ "&LeftUpVector;": { "codepoints": [8639], "characters": "\u21BF" },
+ "&LeftUpVectorBar;": { "codepoints": [10584], "characters": "\u2958" },
+ "&LeftVector;": { "codepoints": [8636], "characters": "\u21BC" },
+ "&LeftVectorBar;": { "codepoints": [10578], "characters": "\u2952" },
+ "&Leftarrow;": { "codepoints": [8656], "characters": "\u21D0" },
+ "&Leftrightarrow;": { "codepoints": [8660], "characters": "\u21D4" },
+ "&LessEqualGreater;": { "codepoints": [8922], "characters": "\u22DA" },
+ "&LessFullEqual;": { "codepoints": [8806], "characters": "\u2266" },
+ "&LessGreater;": { "codepoints": [8822], "characters": "\u2276" },
+ "&LessLess;": { "codepoints": [10913], "characters": "\u2AA1" },
+ "&LessSlantEqual;": { "codepoints": [10877], "characters": "\u2A7D" },
+ "&LessTilde;": { "codepoints": [8818], "characters": "\u2272" },
+ "&Lfr;": { "codepoints": [120079], "characters": "\uD835\uDD0F" },
+ "&Ll;": { "codepoints": [8920], "characters": "\u22D8" },
+ "&Lleftarrow;": { "codepoints": [8666], "characters": "\u21DA" },
+ "&Lmidot;": { "codepoints": [319], "characters": "\u013F" },
+ "&LongLeftArrow;": { "codepoints": [10229], "characters": "\u27F5" },
+ "&LongLeftRightArrow;": { "codepoints": [10231], "characters": "\u27F7" },
+ "&LongRightArrow;": { "codepoints": [10230], "characters": "\u27F6" },
+ "&Longleftarrow;": { "codepoints": [10232], "characters": "\u27F8" },
+ "&Longleftrightarrow;": { "codepoints": [10234], "characters": "\u27FA" },
+ "&Longrightarrow;": { "codepoints": [10233], "characters": "\u27F9" },
+ "&Lopf;": { "codepoints": [120131], "characters": "\uD835\uDD43" },
+ "&LowerLeftArrow;": { "codepoints": [8601], "characters": "\u2199" },
+ "&LowerRightArrow;": { "codepoints": [8600], "characters": "\u2198" },
+ "&Lscr;": { "codepoints": [8466], "characters": "\u2112" },
+ "&Lsh;": { "codepoints": [8624], "characters": "\u21B0" },
+ "&Lstrok;": { "codepoints": [321], "characters": "\u0141" },
+ "&Lt;": { "codepoints": [8810], "characters": "\u226A" },
+ "&Map;": { "codepoints": [10501], "characters": "\u2905" },
+ "&Mcy;": { "codepoints": [1052], "characters": "\u041C" },
+ "&MediumSpace;": { "codepoints": [8287], "characters": "\u205F" },
+ "&Mellintrf;": { "codepoints": [8499], "characters": "\u2133" },
+ "&Mfr;": { "codepoints": [120080], "characters": "\uD835\uDD10" },
+ "&MinusPlus;": { "codepoints": [8723], "characters": "\u2213" },
+ "&Mopf;": { "codepoints": [120132], "characters": "\uD835\uDD44" },
+ "&Mscr;": { "codepoints": [8499], "characters": "\u2133" },
+ "&Mu;": { "codepoints": [924], "characters": "\u039C" },
+ "&NJcy;": { "codepoints": [1034], "characters": "\u040A" },
+ "&Nacute;": { "codepoints": [323], "characters": "\u0143" },
+ "&Ncaron;": { "codepoints": [327], "characters": "\u0147" },
+ "&Ncedil;": { "codepoints": [325], "characters": "\u0145" },
+ "&Ncy;": { "codepoints": [1053], "characters": "\u041D" },
+ "&NegativeMediumSpace;": { "codepoints": [8203], "characters": "\u200B" },
+ "&NegativeThickSpace;": { "codepoints": [8203], "characters": "\u200B" },
+ "&NegativeThinSpace;": { "codepoints": [8203], "characters": "\u200B" },
+ "&NegativeVeryThinSpace;": { "codepoints": [8203], "characters": "\u200B" },
+ "&NestedGreaterGreater;": { "codepoints": [8811], "characters": "\u226B" },
+ "&NestedLessLess;": { "codepoints": [8810], "characters": "\u226A" },
+ "&NewLine;": { "codepoints": [10], "characters": "\u000A" },
+ "&Nfr;": { "codepoints": [120081], "characters": "\uD835\uDD11" },
+ "&NoBreak;": { "codepoints": [8288], "characters": "\u2060" },
+ "&NonBreakingSpace;": { "codepoints": [160], "characters": "\u00A0" },
+ "&Nopf;": { "codepoints": [8469], "characters": "\u2115" },
+ "&Not;": { "codepoints": [10988], "characters": "\u2AEC" },
+ "&NotCongruent;": { "codepoints": [8802], "characters": "\u2262" },
+ "&NotCupCap;": { "codepoints": [8813], "characters": "\u226D" },
+ "&NotDoubleVerticalBar;": { "codepoints": [8742], "characters": "\u2226" },
+ "&NotElement;": { "codepoints": [8713], "characters": "\u2209" },
+ "&NotEqual;": { "codepoints": [8800], "characters": "\u2260" },
+ "&NotEqualTilde;": { "codepoints": [8770, 824], "characters": "\u2242\u0338" },
+ "&NotExists;": { "codepoints": [8708], "characters": "\u2204" },
+ "&NotGreater;": { "codepoints": [8815], "characters": "\u226F" },
+ "&NotGreaterEqual;": { "codepoints": [8817], "characters": "\u2271" },
+ "&NotGreaterFullEqual;": { "codepoints": [8807, 824], "characters": "\u2267\u0338" },
+ "&NotGreaterGreater;": { "codepoints": [8811, 824], "characters": "\u226B\u0338" },
+ "&NotGreaterLess;": { "codepoints": [8825], "characters": "\u2279" },
+ "&NotGreaterSlantEqual;": { "codepoints": [10878, 824], "characters": "\u2A7E\u0338" },
+ "&NotGreaterTilde;": { "codepoints": [8821], "characters": "\u2275" },
+ "&NotHumpDownHump;": { "codepoints": [8782, 824], "characters": "\u224E\u0338" },
+ "&NotHumpEqual;": { "codepoints": [8783, 824], "characters": "\u224F\u0338" },
+ "&NotLeftTriangle;": { "codepoints": [8938], "characters": "\u22EA" },
+ "&NotLeftTriangleBar;": { "codepoints": [10703, 824], "characters": "\u29CF\u0338" },
+ "&NotLeftTriangleEqual;": { "codepoints": [8940], "characters": "\u22EC" },
+ "&NotLess;": { "codepoints": [8814], "characters": "\u226E" },
+ "&NotLessEqual;": { "codepoints": [8816], "characters": "\u2270" },
+ "&NotLessGreater;": { "codepoints": [8824], "characters": "\u2278" },
+ "&NotLessLess;": { "codepoints": [8810, 824], "characters": "\u226A\u0338" },
+ "&NotLessSlantEqual;": { "codepoints": [10877, 824], "characters": "\u2A7D\u0338" },
+ "&NotLessTilde;": { "codepoints": [8820], "characters": "\u2274" },
+ "&NotNestedGreaterGreater;": { "codepoints": [10914, 824], "characters": "\u2AA2\u0338" },
+ "&NotNestedLessLess;": { "codepoints": [10913, 824], "characters": "\u2AA1\u0338" },
+ "&NotPrecedes;": { "codepoints": [8832], "characters": "\u2280" },
+ "&NotPrecedesEqual;": { "codepoints": [10927, 824], "characters": "\u2AAF\u0338" },
+ "&NotPrecedesSlantEqual;": { "codepoints": [8928], "characters": "\u22E0" },
+ "&NotReverseElement;": { "codepoints": [8716], "characters": "\u220C" },
+ "&NotRightTriangle;": { "codepoints": [8939], "characters": "\u22EB" },
+ "&NotRightTriangleBar;": { "codepoints": [10704, 824], "characters": "\u29D0\u0338" },
+ "&NotRightTriangleEqual;": { "codepoints": [8941], "characters": "\u22ED" },
+ "&NotSquareSubset;": { "codepoints": [8847, 824], "characters": "\u228F\u0338" },
+ "&NotSquareSubsetEqual;": { "codepoints": [8930], "characters": "\u22E2" },
+ "&NotSquareSuperset;": { "codepoints": [8848, 824], "characters": "\u2290\u0338" },
+ "&NotSquareSupersetEqual;": { "codepoints": [8931], "characters": "\u22E3" },
+ "&NotSubset;": { "codepoints": [8834, 8402], "characters": "\u2282\u20D2" },
+ "&NotSubsetEqual;": { "codepoints": [8840], "characters": "\u2288" },
+ "&NotSucceeds;": { "codepoints": [8833], "characters": "\u2281" },
+ "&NotSucceedsEqual;": { "codepoints": [10928, 824], "characters": "\u2AB0\u0338" },
+ "&NotSucceedsSlantEqual;": { "codepoints": [8929], "characters": "\u22E1" },
+ "&NotSucceedsTilde;": { "codepoints": [8831, 824], "characters": "\u227F\u0338" },
+ "&NotSuperset;": { "codepoints": [8835, 8402], "characters": "\u2283\u20D2" },
+ "&NotSupersetEqual;": { "codepoints": [8841], "characters": "\u2289" },
+ "&NotTilde;": { "codepoints": [8769], "characters": "\u2241" },
+ "&NotTildeEqual;": { "codepoints": [8772], "characters": "\u2244" },
+ "&NotTildeFullEqual;": { "codepoints": [8775], "characters": "\u2247" },
+ "&NotTildeTilde;": { "codepoints": [8777], "characters": "\u2249" },
+ "&NotVerticalBar;": { "codepoints": [8740], "characters": "\u2224" },
+ "&Nscr;": { "codepoints": [119977], "characters": "\uD835\uDCA9" },
+ "&Ntilde": { "codepoints": [209], "characters": "\u00D1" },
+ "&Ntilde;": { "codepoints": [209], "characters": "\u00D1" },
+ "&Nu;": { "codepoints": [925], "characters": "\u039D" },
+ "&OElig;": { "codepoints": [338], "characters": "\u0152" },
+ "&Oacute": { "codepoints": [211], "characters": "\u00D3" },
+ "&Oacute;": { "codepoints": [211], "characters": "\u00D3" },
+ "&Ocirc": { "codepoints": [212], "characters": "\u00D4" },
+ "&Ocirc;": { "codepoints": [212], "characters": "\u00D4" },
+ "&Ocy;": { "codepoints": [1054], "characters": "\u041E" },
+ "&Odblac;": { "codepoints": [336], "characters": "\u0150" },
+ "&Ofr;": { "codepoints": [120082], "characters": "\uD835\uDD12" },
+ "&Ograve": { "codepoints": [210], "characters": "\u00D2" },
+ "&Ograve;": { "codepoints": [210], "characters": "\u00D2" },
+ "&Omacr;": { "codepoints": [332], "characters": "\u014C" },
+ "&Omega;": { "codepoints": [937], "characters": "\u03A9" },
+ "&Omicron;": { "codepoints": [927], "characters": "\u039F" },
+ "&Oopf;": { "codepoints": [120134], "characters": "\uD835\uDD46" },
+ "&OpenCurlyDoubleQuote;": { "codepoints": [8220], "characters": "\u201C" },
+ "&OpenCurlyQuote;": { "codepoints": [8216], "characters": "\u2018" },
+ "&Or;": { "codepoints": [10836], "characters": "\u2A54" },
+ "&Oscr;": { "codepoints": [119978], "characters": "\uD835\uDCAA" },
+ "&Oslash": { "codepoints": [216], "characters": "\u00D8" },
+ "&Oslash;": { "codepoints": [216], "characters": "\u00D8" },
+ "&Otilde": { "codepoints": [213], "characters": "\u00D5" },
+ "&Otilde;": { "codepoints": [213], "characters": "\u00D5" },
+ "&Otimes;": { "codepoints": [10807], "characters": "\u2A37" },
+ "&Ouml": { "codepoints": [214], "characters": "\u00D6" },
+ "&Ouml;": { "codepoints": [214], "characters": "\u00D6" },
+ "&OverBar;": { "codepoints": [8254], "characters": "\u203E" },
+ "&OverBrace;": { "codepoints": [9182], "characters": "\u23DE" },
+ "&OverBracket;": { "codepoints": [9140], "characters": "\u23B4" },
+ "&OverParenthesis;": { "codepoints": [9180], "characters": "\u23DC" },
+ "&PartialD;": { "codepoints": [8706], "characters": "\u2202" },
+ "&Pcy;": { "codepoints": [1055], "characters": "\u041F" },
+ "&Pfr;": { "codepoints": [120083], "characters": "\uD835\uDD13" },
+ "&Phi;": { "codepoints": [934], "characters": "\u03A6" },
+ "&Pi;": { "codepoints": [928], "characters": "\u03A0" },
+ "&PlusMinus;": { "codepoints": [177], "characters": "\u00B1" },
+ "&Poincareplane;": { "codepoints": [8460], "characters": "\u210C" },
+ "&Popf;": { "codepoints": [8473], "characters": "\u2119" },
+ "&Pr;": { "codepoints": [10939], "characters": "\u2ABB" },
+ "&Precedes;": { "codepoints": [8826], "characters": "\u227A" },
+ "&PrecedesEqual;": { "codepoints": [10927], "characters": "\u2AAF" },
+ "&PrecedesSlantEqual;": { "codepoints": [8828], "characters": "\u227C" },
+ "&PrecedesTilde;": { "codepoints": [8830], "characters": "\u227E" },
+ "&Prime;": { "codepoints": [8243], "characters": "\u2033" },
+ "&Product;": { "codepoints": [8719], "characters": "\u220F" },
+ "&Proportion;": { "codepoints": [8759], "characters": "\u2237" },
+ "&Proportional;": { "codepoints": [8733], "characters": "\u221D" },
+ "&Pscr;": { "codepoints": [119979], "characters": "\uD835\uDCAB" },
+ "&Psi;": { "codepoints": [936], "characters": "\u03A8" },
+ "&QUOT": { "codepoints": [34], "characters": "\u0022" },
+ "&QUOT;": { "codepoints": [34], "characters": "\u0022" },
+ "&Qfr;": { "codepoints": [120084], "characters": "\uD835\uDD14" },
+ "&Qopf;": { "codepoints": [8474], "characters": "\u211A" },
+ "&Qscr;": { "codepoints": [119980], "characters": "\uD835\uDCAC" },
+ "&RBarr;": { "codepoints": [10512], "characters": "\u2910" },
+ "&REG": { "codepoints": [174], "characters": "\u00AE" },
+ "&REG;": { "codepoints": [174], "characters": "\u00AE" },
+ "&Racute;": { "codepoints": [340], "characters": "\u0154" },
+ "&Rang;": { "codepoints": [10219], "characters": "\u27EB" },
+ "&Rarr;": { "codepoints": [8608], "characters": "\u21A0" },
+ "&Rarrtl;": { "codepoints": [10518], "characters": "\u2916" },
+ "&Rcaron;": { "codepoints": [344], "characters": "\u0158" },
+ "&Rcedil;": { "codepoints": [342], "characters": "\u0156" },
+ "&Rcy;": { "codepoints": [1056], "characters": "\u0420" },
+ "&Re;": { "codepoints": [8476], "characters": "\u211C" },
+ "&ReverseElement;": { "codepoints": [8715], "characters": "\u220B" },
+ "&ReverseEquilibrium;": { "codepoints": [8651], "characters": "\u21CB" },
+ "&ReverseUpEquilibrium;": { "codepoints": [10607], "characters": "\u296F" },
+ "&Rfr;": { "codepoints": [8476], "characters": "\u211C" },
+ "&Rho;": { "codepoints": [929], "characters": "\u03A1" },
+ "&RightAngleBracket;": { "codepoints": [10217], "characters": "\u27E9" },
+ "&RightArrow;": { "codepoints": [8594], "characters": "\u2192" },
+ "&RightArrowBar;": { "codepoints": [8677], "characters": "\u21E5" },
+ "&RightArrowLeftArrow;": { "codepoints": [8644], "characters": "\u21C4" },
+ "&RightCeiling;": { "codepoints": [8969], "characters": "\u2309" },
+ "&RightDoubleBracket;": { "codepoints": [10215], "characters": "\u27E7" },
+ "&RightDownTeeVector;": { "codepoints": [10589], "characters": "\u295D" },
+ "&RightDownVector;": { "codepoints": [8642], "characters": "\u21C2" },
+ "&RightDownVectorBar;": { "codepoints": [10581], "characters": "\u2955" },
+ "&RightFloor;": { "codepoints": [8971], "characters": "\u230B" },
+ "&RightTee;": { "codepoints": [8866], "characters": "\u22A2" },
+ "&RightTeeArrow;": { "codepoints": [8614], "characters": "\u21A6" },
+ "&RightTeeVector;": { "codepoints": [10587], "characters": "\u295B" },
+ "&RightTriangle;": { "codepoints": [8883], "characters": "\u22B3" },
+ "&RightTriangleBar;": { "codepoints": [10704], "characters": "\u29D0" },
+ "&RightTriangleEqual;": { "codepoints": [8885], "characters": "\u22B5" },
+ "&RightUpDownVector;": { "codepoints": [10575], "characters": "\u294F" },
+ "&RightUpTeeVector;": { "codepoints": [10588], "characters": "\u295C" },
+ "&RightUpVector;": { "codepoints": [8638], "characters": "\u21BE" },
+ "&RightUpVectorBar;": { "codepoints": [10580], "characters": "\u2954" },
+ "&RightVector;": { "codepoints": [8640], "characters": "\u21C0" },
+ "&RightVectorBar;": { "codepoints": [10579], "characters": "\u2953" },
+ "&Rightarrow;": { "codepoints": [8658], "characters": "\u21D2" },
+ "&Ropf;": { "codepoints": [8477], "characters": "\u211D" },
+ "&RoundImplies;": { "codepoints": [10608], "characters": "\u2970" },
+ "&Rrightarrow;": { "codepoints": [8667], "characters": "\u21DB" },
+ "&Rscr;": { "codepoints": [8475], "characters": "\u211B" },
+ "&Rsh;": { "codepoints": [8625], "characters": "\u21B1" },
+ "&RuleDelayed;": { "codepoints": [10740], "characters": "\u29F4" },
+ "&SHCHcy;": { "codepoints": [1065], "characters": "\u0429" },
+ "&SHcy;": { "codepoints": [1064], "characters": "\u0428" },
+ "&SOFTcy;": { "codepoints": [1068], "characters": "\u042C" },
+ "&Sacute;": { "codepoints": [346], "characters": "\u015A" },
+ "&Sc;": { "codepoints": [10940], "characters": "\u2ABC" },
+ "&Scaron;": { "codepoints": [352], "characters": "\u0160" },
+ "&Scedil;": { "codepoints": [350], "characters": "\u015E" },
+ "&Scirc;": { "codepoints": [348], "characters": "\u015C" },
+ "&Scy;": { "codepoints": [1057], "characters": "\u0421" },
+ "&Sfr;": { "codepoints": [120086], "characters": "\uD835\uDD16" },
+ "&ShortDownArrow;": { "codepoints": [8595], "characters": "\u2193" },
+ "&ShortLeftArrow;": { "codepoints": [8592], "characters": "\u2190" },
+ "&ShortRightArrow;": { "codepoints": [8594], "characters": "\u2192" },
+ "&ShortUpArrow;": { "codepoints": [8593], "characters": "\u2191" },
+ "&Sigma;": { "codepoints": [931], "characters": "\u03A3" },
+ "&SmallCircle;": { "codepoints": [8728], "characters": "\u2218" },
+ "&Sopf;": { "codepoints": [120138], "characters": "\uD835\uDD4A" },
+ "&Sqrt;": { "codepoints": [8730], "characters": "\u221A" },
+ "&Square;": { "codepoints": [9633], "characters": "\u25A1" },
+ "&SquareIntersection;": { "codepoints": [8851], "characters": "\u2293" },
+ "&SquareSubset;": { "codepoints": [8847], "characters": "\u228F" },
+ "&SquareSubsetEqual;": { "codepoints": [8849], "characters": "\u2291" },
+ "&SquareSuperset;": { "codepoints": [8848], "characters": "\u2290" },
+ "&SquareSupersetEqual;": { "codepoints": [8850], "characters": "\u2292" },
+ "&SquareUnion;": { "codepoints": [8852], "characters": "\u2294" },
+ "&Sscr;": { "codepoints": [119982], "characters": "\uD835\uDCAE" },
+ "&Star;": { "codepoints": [8902], "characters": "\u22C6" },
+ "&Sub;": { "codepoints": [8912], "characters": "\u22D0" },
+ "&Subset;": { "codepoints": [8912], "characters": "\u22D0" },
+ "&SubsetEqual;": { "codepoints": [8838], "characters": "\u2286" },
+ "&Succeeds;": { "codepoints": [8827], "characters": "\u227B" },
+ "&SucceedsEqual;": { "codepoints": [10928], "characters": "\u2AB0" },
+ "&SucceedsSlantEqual;": { "codepoints": [8829], "characters": "\u227D" },
+ "&SucceedsTilde;": { "codepoints": [8831], "characters": "\u227F" },
+ "&SuchThat;": { "codepoints": [8715], "characters": "\u220B" },
+ "&Sum;": { "codepoints": [8721], "characters": "\u2211" },
+ "&Sup;": { "codepoints": [8913], "characters": "\u22D1" },
+ "&Superset;": { "codepoints": [8835], "characters": "\u2283" },
+ "&SupersetEqual;": { "codepoints": [8839], "characters": "\u2287" },
+ "&Supset;": { "codepoints": [8913], "characters": "\u22D1" },
+ "&THORN": { "codepoints": [222], "characters": "\u00DE" },
+ "&THORN;": { "codepoints": [222], "characters": "\u00DE" },
+ "&TRADE;": { "codepoints": [8482], "characters": "\u2122" },
+ "&TSHcy;": { "codepoints": [1035], "characters": "\u040B" },
+ "&TScy;": { "codepoints": [1062], "characters": "\u0426" },
+ "&Tab;": { "codepoints": [9], "characters": "\u0009" },
+ "&Tau;": { "codepoints": [932], "characters": "\u03A4" },
+ "&Tcaron;": { "codepoints": [356], "characters": "\u0164" },
+ "&Tcedil;": { "codepoints": [354], "characters": "\u0162" },
+ "&Tcy;": { "codepoints": [1058], "characters": "\u0422" },
+ "&Tfr;": { "codepoints": [120087], "characters": "\uD835\uDD17" },
+ "&Therefore;": { "codepoints": [8756], "characters": "\u2234" },
+ "&Theta;": { "codepoints": [920], "characters": "\u0398" },
+ "&ThickSpace;": { "codepoints": [8287, 8202], "characters": "\u205F\u200A" },
+ "&ThinSpace;": { "codepoints": [8201], "characters": "\u2009" },
+ "&Tilde;": { "codepoints": [8764], "characters": "\u223C" },
+ "&TildeEqual;": { "codepoints": [8771], "characters": "\u2243" },
+ "&TildeFullEqual;": { "codepoints": [8773], "characters": "\u2245" },
+ "&TildeTilde;": { "codepoints": [8776], "characters": "\u2248" },
+ "&Topf;": { "codepoints": [120139], "characters": "\uD835\uDD4B" },
+ "&TripleDot;": { "codepoints": [8411], "characters": "\u20DB" },
+ "&Tscr;": { "codepoints": [119983], "characters": "\uD835\uDCAF" },
+ "&Tstrok;": { "codepoints": [358], "characters": "\u0166" },
+ "&Uacute": { "codepoints": [218], "characters": "\u00DA" },
+ "&Uacute;": { "codepoints": [218], "characters": "\u00DA" },
+ "&Uarr;": { "codepoints": [8607], "characters": "\u219F" },
+ "&Uarrocir;": { "codepoints": [10569], "characters": "\u2949" },
+ "&Ubrcy;": { "codepoints": [1038], "characters": "\u040E" },
+ "&Ubreve;": { "codepoints": [364], "characters": "\u016C" },
+ "&Ucirc": { "codepoints": [219], "characters": "\u00DB" },
+ "&Ucirc;": { "codepoints": [219], "characters": "\u00DB" },
+ "&Ucy;": { "codepoints": [1059], "characters": "\u0423" },
+ "&Udblac;": { "codepoints": [368], "characters": "\u0170" },
+ "&Ufr;": { "codepoints": [120088], "characters": "\uD835\uDD18" },
+ "&Ugrave": { "codepoints": [217], "characters": "\u00D9" },
+ "&Ugrave;": { "codepoints": [217], "characters": "\u00D9" },
+ "&Umacr;": { "codepoints": [362], "characters": "\u016A" },
+ "&UnderBar;": { "codepoints": [95], "characters": "\u005F" },
+ "&UnderBrace;": { "codepoints": [9183], "characters": "\u23DF" },
+ "&UnderBracket;": { "codepoints": [9141], "characters": "\u23B5" },
+ "&UnderParenthesis;": { "codepoints": [9181], "characters": "\u23DD" },
+ "&Union;": { "codepoints": [8899], "characters": "\u22C3" },
+ "&UnionPlus;": { "codepoints": [8846], "characters": "\u228E" },
+ "&Uogon;": { "codepoints": [370], "characters": "\u0172" },
+ "&Uopf;": { "codepoints": [120140], "characters": "\uD835\uDD4C" },
+ "&UpArrow;": { "codepoints": [8593], "characters": "\u2191" },
+ "&UpArrowBar;": { "codepoints": [10514], "characters": "\u2912" },
+ "&UpArrowDownArrow;": { "codepoints": [8645], "characters": "\u21C5" },
+ "&UpDownArrow;": { "codepoints": [8597], "characters": "\u2195" },
+ "&UpEquilibrium;": { "codepoints": [10606], "characters": "\u296E" },
+ "&UpTee;": { "codepoints": [8869], "characters": "\u22A5" },
+ "&UpTeeArrow;": { "codepoints": [8613], "characters": "\u21A5" },
+ "&Uparrow;": { "codepoints": [8657], "characters": "\u21D1" },
+ "&Updownarrow;": { "codepoints": [8661], "characters": "\u21D5" },
+ "&UpperLeftArrow;": { "codepoints": [8598], "characters": "\u2196" },
+ "&UpperRightArrow;": { "codepoints": [8599], "characters": "\u2197" },
+ "&Upsi;": { "codepoints": [978], "characters": "\u03D2" },
+ "&Upsilon;": { "codepoints": [933], "characters": "\u03A5" },
+ "&Uring;": { "codepoints": [366], "characters": "\u016E" },
+ "&Uscr;": { "codepoints": [119984], "characters": "\uD835\uDCB0" },
+ "&Utilde;": { "codepoints": [360], "characters": "\u0168" },
+ "&Uuml": { "codepoints": [220], "characters": "\u00DC" },
+ "&Uuml;": { "codepoints": [220], "characters": "\u00DC" },
+ "&VDash;": { "codepoints": [8875], "characters": "\u22AB" },
+ "&Vbar;": { "codepoints": [10987], "characters": "\u2AEB" },
+ "&Vcy;": { "codepoints": [1042], "characters": "\u0412" },
+ "&Vdash;": { "codepoints": [8873], "characters": "\u22A9" },
+ "&Vdashl;": { "codepoints": [10982], "characters": "\u2AE6" },
+ "&Vee;": { "codepoints": [8897], "characters": "\u22C1" },
+ "&Verbar;": { "codepoints": [8214], "characters": "\u2016" },
+ "&Vert;": { "codepoints": [8214], "characters": "\u2016" },
+ "&VerticalBar;": { "codepoints": [8739], "characters": "\u2223" },
+ "&VerticalLine;": { "codepoints": [124], "characters": "\u007C" },
+ "&VerticalSeparator;": { "codepoints": [10072], "characters": "\u2758" },
+ "&VerticalTilde;": { "codepoints": [8768], "characters": "\u2240" },
+ "&VeryThinSpace;": { "codepoints": [8202], "characters": "\u200A" },
+ "&Vfr;": { "codepoints": [120089], "characters": "\uD835\uDD19" },
+ "&Vopf;": { "codepoints": [120141], "characters": "\uD835\uDD4D" },
+ "&Vscr;": { "codepoints": [119985], "characters": "\uD835\uDCB1" },
+ "&Vvdash;": { "codepoints": [8874], "characters": "\u22AA" },
+ "&Wcirc;": { "codepoints": [372], "characters": "\u0174" },
+ "&Wedge;": { "codepoints": [8896], "characters": "\u22C0" },
+ "&Wfr;": { "codepoints": [120090], "characters": "\uD835\uDD1A" },
+ "&Wopf;": { "codepoints": [120142], "characters": "\uD835\uDD4E" },
+ "&Wscr;": { "codepoints": [119986], "characters": "\uD835\uDCB2" },
+ "&Xfr;": { "codepoints": [120091], "characters": "\uD835\uDD1B" },
+ "&Xi;": { "codepoints": [926], "characters": "\u039E" },
+ "&Xopf;": { "codepoints": [120143], "characters": "\uD835\uDD4F" },
+ "&Xscr;": { "codepoints": [119987], "characters": "\uD835\uDCB3" },
+ "&YAcy;": { "codepoints": [1071], "characters": "\u042F" },
+ "&YIcy;": { "codepoints": [1031], "characters": "\u0407" },
+ "&YUcy;": { "codepoints": [1070], "characters": "\u042E" },
+ "&Yacute": { "codepoints": [221], "characters": "\u00DD" },
+ "&Yacute;": { "codepoints": [221], "characters": "\u00DD" },
+ "&Ycirc;": { "codepoints": [374], "characters": "\u0176" },
+ "&Ycy;": { "codepoints": [1067], "characters": "\u042B" },
+ "&Yfr;": { "codepoints": [120092], "characters": "\uD835\uDD1C" },
+ "&Yopf;": { "codepoints": [120144], "characters": "\uD835\uDD50" },
+ "&Yscr;": { "codepoints": [119988], "characters": "\uD835\uDCB4" },
+ "&Yuml;": { "codepoints": [376], "characters": "\u0178" },
+ "&ZHcy;": { "codepoints": [1046], "characters": "\u0416" },
+ "&Zacute;": { "codepoints": [377], "characters": "\u0179" },
+ "&Zcaron;": { "codepoints": [381], "characters": "\u017D" },
+ "&Zcy;": { "codepoints": [1047], "characters": "\u0417" },
+ "&Zdot;": { "codepoints": [379], "characters": "\u017B" },
+ "&ZeroWidthSpace;": { "codepoints": [8203], "characters": "\u200B" },
+ "&Zeta;": { "codepoints": [918], "characters": "\u0396" },
+ "&Zfr;": { "codepoints": [8488], "characters": "\u2128" },
+ "&Zopf;": { "codepoints": [8484], "characters": "\u2124" },
+ "&Zscr;": { "codepoints": [119989], "characters": "\uD835\uDCB5" },
+ "&aacute": { "codepoints": [225], "characters": "\u00E1" },
+ "&aacute;": { "codepoints": [225], "characters": "\u00E1" },
+ "&abreve;": { "codepoints": [259], "characters": "\u0103" },
+ "&ac;": { "codepoints": [8766], "characters": "\u223E" },
+ "&acE;": { "codepoints": [8766, 819], "characters": "\u223E\u0333" },
+ "&acd;": { "codepoints": [8767], "characters": "\u223F" },
+ "&acirc": { "codepoints": [226], "characters": "\u00E2" },
+ "&acirc;": { "codepoints": [226], "characters": "\u00E2" },
+ "&acute": { "codepoints": [180], "characters": "\u00B4" },
+ "&acute;": { "codepoints": [180], "characters": "\u00B4" },
+ "&acy;": { "codepoints": [1072], "characters": "\u0430" },
+ "&aelig": { "codepoints": [230], "characters": "\u00E6" },
+ "&aelig;": { "codepoints": [230], "characters": "\u00E6" },
+ "&af;": { "codepoints": [8289], "characters": "\u2061" },
+ "&afr;": { "codepoints": [120094], "characters": "\uD835\uDD1E" },
+ "&agrave": { "codepoints": [224], "characters": "\u00E0" },
+ "&agrave;": { "codepoints": [224], "characters": "\u00E0" },
+ "&alefsym;": { "codepoints": [8501], "characters": "\u2135" },
+ "&aleph;": { "codepoints": [8501], "characters": "\u2135" },
+ "&alpha;": { "codepoints": [945], "characters": "\u03B1" },
+ "&amacr;": { "codepoints": [257], "characters": "\u0101" },
+ "&amalg;": { "codepoints": [10815], "characters": "\u2A3F" },
+ "&amp": { "codepoints": [38], "characters": "\u0026" },
+ "&amp;": { "codepoints": [38], "characters": "\u0026" },
+ "&and;": { "codepoints": [8743], "characters": "\u2227" },
+ "&andand;": { "codepoints": [10837], "characters": "\u2A55" },
+ "&andd;": { "codepoints": [10844], "characters": "\u2A5C" },
+ "&andslope;": { "codepoints": [10840], "characters": "\u2A58" },
+ "&andv;": { "codepoints": [10842], "characters": "\u2A5A" },
+ "&ang;": { "codepoints": [8736], "characters": "\u2220" },
+ "&ange;": { "codepoints": [10660], "characters": "\u29A4" },
+ "&angle;": { "codepoints": [8736], "characters": "\u2220" },
+ "&angmsd;": { "codepoints": [8737], "characters": "\u2221" },
+ "&angmsdaa;": { "codepoints": [10664], "characters": "\u29A8" },
+ "&angmsdab;": { "codepoints": [10665], "characters": "\u29A9" },
+ "&angmsdac;": { "codepoints": [10666], "characters": "\u29AA" },
+ "&angmsdad;": { "codepoints": [10667], "characters": "\u29AB" },
+ "&angmsdae;": { "codepoints": [10668], "characters": "\u29AC" },
+ "&angmsdaf;": { "codepoints": [10669], "characters": "\u29AD" },
+ "&angmsdag;": { "codepoints": [10670], "characters": "\u29AE" },
+ "&angmsdah;": { "codepoints": [10671], "characters": "\u29AF" },
+ "&angrt;": { "codepoints": [8735], "characters": "\u221F" },
+ "&angrtvb;": { "codepoints": [8894], "characters": "\u22BE" },
+ "&angrtvbd;": { "codepoints": [10653], "characters": "\u299D" },
+ "&angsph;": { "codepoints": [8738], "characters": "\u2222" },
+ "&angst;": { "codepoints": [197], "characters": "\u00C5" },
+ "&angzarr;": { "codepoints": [9084], "characters": "\u237C" },
+ "&aogon;": { "codepoints": [261], "characters": "\u0105" },
+ "&aopf;": { "codepoints": [120146], "characters": "\uD835\uDD52" },
+ "&ap;": { "codepoints": [8776], "characters": "\u2248" },
+ "&apE;": { "codepoints": [10864], "characters": "\u2A70" },
+ "&apacir;": { "codepoints": [10863], "characters": "\u2A6F" },
+ "&ape;": { "codepoints": [8778], "characters": "\u224A" },
+ "&apid;": { "codepoints": [8779], "characters": "\u224B" },
+ "&apos;": { "codepoints": [39], "characters": "\u0027" },
+ "&approx;": { "codepoints": [8776], "characters": "\u2248" },
+ "&approxeq;": { "codepoints": [8778], "characters": "\u224A" },
+ "&aring": { "codepoints": [229], "characters": "\u00E5" },
+ "&aring;": { "codepoints": [229], "characters": "\u00E5" },
+ "&ascr;": { "codepoints": [119990], "characters": "\uD835\uDCB6" },
+ "&ast;": { "codepoints": [42], "characters": "\u002A" },
+ "&asymp;": { "codepoints": [8776], "characters": "\u2248" },
+ "&asympeq;": { "codepoints": [8781], "characters": "\u224D" },
+ "&atilde": { "codepoints": [227], "characters": "\u00E3" },
+ "&atilde;": { "codepoints": [227], "characters": "\u00E3" },
+ "&auml": { "codepoints": [228], "characters": "\u00E4" },
+ "&auml;": { "codepoints": [228], "characters": "\u00E4" },
+ "&awconint;": { "codepoints": [8755], "characters": "\u2233" },
+ "&awint;": { "codepoints": [10769], "characters": "\u2A11" },
+ "&bNot;": { "codepoints": [10989], "characters": "\u2AED" },
+ "&backcong;": { "codepoints": [8780], "characters": "\u224C" },
+ "&backepsilon;": { "codepoints": [1014], "characters": "\u03F6" },
+ "&backprime;": { "codepoints": [8245], "characters": "\u2035" },
+ "&backsim;": { "codepoints": [8765], "characters": "\u223D" },
+ "&backsimeq;": { "codepoints": [8909], "characters": "\u22CD" },
+ "&barvee;": { "codepoints": [8893], "characters": "\u22BD" },
+ "&barwed;": { "codepoints": [8965], "characters": "\u2305" },
+ "&barwedge;": { "codepoints": [8965], "characters": "\u2305" },
+ "&bbrk;": { "codepoints": [9141], "characters": "\u23B5" },
+ "&bbrktbrk;": { "codepoints": [9142], "characters": "\u23B6" },
+ "&bcong;": { "codepoints": [8780], "characters": "\u224C" },
+ "&bcy;": { "codepoints": [1073], "characters": "\u0431" },
+ "&bdquo;": { "codepoints": [8222], "characters": "\u201E" },
+ "&becaus;": { "codepoints": [8757], "characters": "\u2235" },
+ "&because;": { "codepoints": [8757], "characters": "\u2235" },
+ "&bemptyv;": { "codepoints": [10672], "characters": "\u29B0" },
+ "&bepsi;": { "codepoints": [1014], "characters": "\u03F6" },
+ "&bernou;": { "codepoints": [8492], "characters": "\u212C" },
+ "&beta;": { "codepoints": [946], "characters": "\u03B2" },
+ "&beth;": { "codepoints": [8502], "characters": "\u2136" },
+ "&between;": { "codepoints": [8812], "characters": "\u226C" },
+ "&bfr;": { "codepoints": [120095], "characters": "\uD835\uDD1F" },
+ "&bigcap;": { "codepoints": [8898], "characters": "\u22C2" },
+ "&bigcirc;": { "codepoints": [9711], "characters": "\u25EF" },
+ "&bigcup;": { "codepoints": [8899], "characters": "\u22C3" },
+ "&bigodot;": { "codepoints": [10752], "characters": "\u2A00" },
+ "&bigoplus;": { "codepoints": [10753], "characters": "\u2A01" },
+ "&bigotimes;": { "codepoints": [10754], "characters": "\u2A02" },
+ "&bigsqcup;": { "codepoints": [10758], "characters": "\u2A06" },
+ "&bigstar;": { "codepoints": [9733], "characters": "\u2605" },
+ "&bigtriangledown;": { "codepoints": [9661], "characters": "\u25BD" },
+ "&bigtriangleup;": { "codepoints": [9651], "characters": "\u25B3" },
+ "&biguplus;": { "codepoints": [10756], "characters": "\u2A04" },
+ "&bigvee;": { "codepoints": [8897], "characters": "\u22C1" },
+ "&bigwedge;": { "codepoints": [8896], "characters": "\u22C0" },
+ "&bkarow;": { "codepoints": [10509], "characters": "\u290D" },
+ "&blacklozenge;": { "codepoints": [10731], "characters": "\u29EB" },
+ "&blacksquare;": { "codepoints": [9642], "characters": "\u25AA" },
+ "&blacktriangle;": { "codepoints": [9652], "characters": "\u25B4" },
+ "&blacktriangledown;": { "codepoints": [9662], "characters": "\u25BE" },
+ "&blacktriangleleft;": { "codepoints": [9666], "characters": "\u25C2" },
+ "&blacktriangleright;": { "codepoints": [9656], "characters": "\u25B8" },
+ "&blank;": { "codepoints": [9251], "characters": "\u2423" },
+ "&blk12;": { "codepoints": [9618], "characters": "\u2592" },
+ "&blk14;": { "codepoints": [9617], "characters": "\u2591" },
+ "&blk34;": { "codepoints": [9619], "characters": "\u2593" },
+ "&block;": { "codepoints": [9608], "characters": "\u2588" },
+ "&bne;": { "codepoints": [61, 8421], "characters": "\u003D\u20E5" },
+ "&bnequiv;": { "codepoints": [8801, 8421], "characters": "\u2261\u20E5" },
+ "&bnot;": { "codepoints": [8976], "characters": "\u2310" },
+ "&bopf;": { "codepoints": [120147], "characters": "\uD835\uDD53" },
+ "&bot;": { "codepoints": [8869], "characters": "\u22A5" },
+ "&bottom;": { "codepoints": [8869], "characters": "\u22A5" },
+ "&bowtie;": { "codepoints": [8904], "characters": "\u22C8" },
+ "&boxDL;": { "codepoints": [9559], "characters": "\u2557" },
+ "&boxDR;": { "codepoints": [9556], "characters": "\u2554" },
+ "&boxDl;": { "codepoints": [9558], "characters": "\u2556" },
+ "&boxDr;": { "codepoints": [9555], "characters": "\u2553" },
+ "&boxH;": { "codepoints": [9552], "characters": "\u2550" },
+ "&boxHD;": { "codepoints": [9574], "characters": "\u2566" },
+ "&boxHU;": { "codepoints": [9577], "characters": "\u2569" },
+ "&boxHd;": { "codepoints": [9572], "characters": "\u2564" },
+ "&boxHu;": { "codepoints": [9575], "characters": "\u2567" },
+ "&boxUL;": { "codepoints": [9565], "characters": "\u255D" },
+ "&boxUR;": { "codepoints": [9562], "characters": "\u255A" },
+ "&boxUl;": { "codepoints": [9564], "characters": "\u255C" },
+ "&boxUr;": { "codepoints": [9561], "characters": "\u2559" },
+ "&boxV;": { "codepoints": [9553], "characters": "\u2551" },
+ "&boxVH;": { "codepoints": [9580], "characters": "\u256C" },
+ "&boxVL;": { "codepoints": [9571], "characters": "\u2563" },
+ "&boxVR;": { "codepoints": [9568], "characters": "\u2560" },
+ "&boxVh;": { "codepoints": [9579], "characters": "\u256B" },
+ "&boxVl;": { "codepoints": [9570], "characters": "\u2562" },
+ "&boxVr;": { "codepoints": [9567], "characters": "\u255F" },
+ "&boxbox;": { "codepoints": [10697], "characters": "\u29C9" },
+ "&boxdL;": { "codepoints": [9557], "characters": "\u2555" },
+ "&boxdR;": { "codepoints": [9554], "characters": "\u2552" },
+ "&boxdl;": { "codepoints": [9488], "characters": "\u2510" },
+ "&boxdr;": { "codepoints": [9484], "characters": "\u250C" },
+ "&boxh;": { "codepoints": [9472], "characters": "\u2500" },
+ "&boxhD;": { "codepoints": [9573], "characters": "\u2565" },
+ "&boxhU;": { "codepoints": [9576], "characters": "\u2568" },
+ "&boxhd;": { "codepoints": [9516], "characters": "\u252C" },
+ "&boxhu;": { "codepoints": [9524], "characters": "\u2534" },
+ "&boxminus;": { "codepoints": [8863], "characters": "\u229F" },
+ "&boxplus;": { "codepoints": [8862], "characters": "\u229E" },
+ "&boxtimes;": { "codepoints": [8864], "characters": "\u22A0" },
+ "&boxuL;": { "codepoints": [9563], "characters": "\u255B" },
+ "&boxuR;": { "codepoints": [9560], "characters": "\u2558" },
+ "&boxul;": { "codepoints": [9496], "characters": "\u2518" },
+ "&boxur;": { "codepoints": [9492], "characters": "\u2514" },
+ "&boxv;": { "codepoints": [9474], "characters": "\u2502" },
+ "&boxvH;": { "codepoints": [9578], "characters": "\u256A" },
+ "&boxvL;": { "codepoints": [9569], "characters": "\u2561" },
+ "&boxvR;": { "codepoints": [9566], "characters": "\u255E" },
+ "&boxvh;": { "codepoints": [9532], "characters": "\u253C" },
+ "&boxvl;": { "codepoints": [9508], "characters": "\u2524" },
+ "&boxvr;": { "codepoints": [9500], "characters": "\u251C" },
+ "&bprime;": { "codepoints": [8245], "characters": "\u2035" },
+ "&breve;": { "codepoints": [728], "characters": "\u02D8" },
+ "&brvbar": { "codepoints": [166], "characters": "\u00A6" },
+ "&brvbar;": { "codepoints": [166], "characters": "\u00A6" },
+ "&bscr;": { "codepoints": [119991], "characters": "\uD835\uDCB7" },
+ "&bsemi;": { "codepoints": [8271], "characters": "\u204F" },
+ "&bsim;": { "codepoints": [8765], "characters": "\u223D" },
+ "&bsime;": { "codepoints": [8909], "characters": "\u22CD" },
+ "&bsol;": { "codepoints": [92], "characters": "\u005C" },
+ "&bsolb;": { "codepoints": [10693], "characters": "\u29C5" },
+ "&bsolhsub;": { "codepoints": [10184], "characters": "\u27C8" },
+ "&bull;": { "codepoints": [8226], "characters": "\u2022" },
+ "&bullet;": { "codepoints": [8226], "characters": "\u2022" },
+ "&bump;": { "codepoints": [8782], "characters": "\u224E" },
+ "&bumpE;": { "codepoints": [10926], "characters": "\u2AAE" },
+ "&bumpe;": { "codepoints": [8783], "characters": "\u224F" },
+ "&bumpeq;": { "codepoints": [8783], "characters": "\u224F" },
+ "&cacute;": { "codepoints": [263], "characters": "\u0107" },
+ "&cap;": { "codepoints": [8745], "characters": "\u2229" },
+ "&capand;": { "codepoints": [10820], "characters": "\u2A44" },
+ "&capbrcup;": { "codepoints": [10825], "characters": "\u2A49" },
+ "&capcap;": { "codepoints": [10827], "characters": "\u2A4B" },
+ "&capcup;": { "codepoints": [10823], "characters": "\u2A47" },
+ "&capdot;": { "codepoints": [10816], "characters": "\u2A40" },
+ "&caps;": { "codepoints": [8745, 65024], "characters": "\u2229\uFE00" },
+ "&caret;": { "codepoints": [8257], "characters": "\u2041" },
+ "&caron;": { "codepoints": [711], "characters": "\u02C7" },
+ "&ccaps;": { "codepoints": [10829], "characters": "\u2A4D" },
+ "&ccaron;": { "codepoints": [269], "characters": "\u010D" },
+ "&ccedil": { "codepoints": [231], "characters": "\u00E7" },
+ "&ccedil;": { "codepoints": [231], "characters": "\u00E7" },
+ "&ccirc;": { "codepoints": [265], "characters": "\u0109" },
+ "&ccups;": { "codepoints": [10828], "characters": "\u2A4C" },
+ "&ccupssm;": { "codepoints": [10832], "characters": "\u2A50" },
+ "&cdot;": { "codepoints": [267], "characters": "\u010B" },
+ "&cedil": { "codepoints": [184], "characters": "\u00B8" },
+ "&cedil;": { "codepoints": [184], "characters": "\u00B8" },
+ "&cemptyv;": { "codepoints": [10674], "characters": "\u29B2" },
+ "&cent": { "codepoints": [162], "characters": "\u00A2" },
+ "&cent;": { "codepoints": [162], "characters": "\u00A2" },
+ "&centerdot;": { "codepoints": [183], "characters": "\u00B7" },
+ "&cfr;": { "codepoints": [120096], "characters": "\uD835\uDD20" },
+ "&chcy;": { "codepoints": [1095], "characters": "\u0447" },
+ "&check;": { "codepoints": [10003], "characters": "\u2713" },
+ "&checkmark;": { "codepoints": [10003], "characters": "\u2713" },
+ "&chi;": { "codepoints": [967], "characters": "\u03C7" },
+ "&cir;": { "codepoints": [9675], "characters": "\u25CB" },
+ "&cirE;": { "codepoints": [10691], "characters": "\u29C3" },
+ "&circ;": { "codepoints": [710], "characters": "\u02C6" },
+ "&circeq;": { "codepoints": [8791], "characters": "\u2257" },
+ "&circlearrowleft;": { "codepoints": [8634], "characters": "\u21BA" },
+ "&circlearrowright;": { "codepoints": [8635], "characters": "\u21BB" },
+ "&circledR;": { "codepoints": [174], "characters": "\u00AE" },
+ "&circledS;": { "codepoints": [9416], "characters": "\u24C8" },
+ "&circledast;": { "codepoints": [8859], "characters": "\u229B" },
+ "&circledcirc;": { "codepoints": [8858], "characters": "\u229A" },
+ "&circleddash;": { "codepoints": [8861], "characters": "\u229D" },
+ "&cire;": { "codepoints": [8791], "characters": "\u2257" },
+ "&cirfnint;": { "codepoints": [10768], "characters": "\u2A10" },
+ "&cirmid;": { "codepoints": [10991], "characters": "\u2AEF" },
+ "&cirscir;": { "codepoints": [10690], "characters": "\u29C2" },
+ "&clubs;": { "codepoints": [9827], "characters": "\u2663" },
+ "&clubsuit;": { "codepoints": [9827], "characters": "\u2663" },
+ "&colon;": { "codepoints": [58], "characters": "\u003A" },
+ "&colone;": { "codepoints": [8788], "characters": "\u2254" },
+ "&coloneq;": { "codepoints": [8788], "characters": "\u2254" },
+ "&comma;": { "codepoints": [44], "characters": "\u002C" },
+ "&commat;": { "codepoints": [64], "characters": "\u0040" },
+ "&comp;": { "codepoints": [8705], "characters": "\u2201" },
+ "&compfn;": { "codepoints": [8728], "characters": "\u2218" },
+ "&complement;": { "codepoints": [8705], "characters": "\u2201" },
+ "&complexes;": { "codepoints": [8450], "characters": "\u2102" },
+ "&cong;": { "codepoints": [8773], "characters": "\u2245" },
+ "&congdot;": { "codepoints": [10861], "characters": "\u2A6D" },
+ "&conint;": { "codepoints": [8750], "characters": "\u222E" },
+ "&copf;": { "codepoints": [120148], "characters": "\uD835\uDD54" },
+ "&coprod;": { "codepoints": [8720], "characters": "\u2210" },
+ "&copy": { "codepoints": [169], "characters": "\u00A9" },
+ "&copy;": { "codepoints": [169], "characters": "\u00A9" },
+ "&copysr;": { "codepoints": [8471], "characters": "\u2117" },
+ "&crarr;": { "codepoints": [8629], "characters": "\u21B5" },
+ "&cross;": { "codepoints": [10007], "characters": "\u2717" },
+ "&cscr;": { "codepoints": [119992], "characters": "\uD835\uDCB8" },
+ "&csub;": { "codepoints": [10959], "characters": "\u2ACF" },
+ "&csube;": { "codepoints": [10961], "characters": "\u2AD1" },
+ "&csup;": { "codepoints": [10960], "characters": "\u2AD0" },
+ "&csupe;": { "codepoints": [10962], "characters": "\u2AD2" },
+ "&ctdot;": { "codepoints": [8943], "characters": "\u22EF" },
+ "&cudarrl;": { "codepoints": [10552], "characters": "\u2938" },
+ "&cudarrr;": { "codepoints": [10549], "characters": "\u2935" },
+ "&cuepr;": { "codepoints": [8926], "characters": "\u22DE" },
+ "&cuesc;": { "codepoints": [8927], "characters": "\u22DF" },
+ "&cularr;": { "codepoints": [8630], "characters": "\u21B6" },
+ "&cularrp;": { "codepoints": [10557], "characters": "\u293D" },
+ "&cup;": { "codepoints": [8746], "characters": "\u222A" },
+ "&cupbrcap;": { "codepoints": [10824], "characters": "\u2A48" },
+ "&cupcap;": { "codepoints": [10822], "characters": "\u2A46" },
+ "&cupcup;": { "codepoints": [10826], "characters": "\u2A4A" },
+ "&cupdot;": { "codepoints": [8845], "characters": "\u228D" },
+ "&cupor;": { "codepoints": [10821], "characters": "\u2A45" },
+ "&cups;": { "codepoints": [8746, 65024], "characters": "\u222A\uFE00" },
+ "&curarr;": { "codepoints": [8631], "characters": "\u21B7" },
+ "&curarrm;": { "codepoints": [10556], "characters": "\u293C" },
+ "&curlyeqprec;": { "codepoints": [8926], "characters": "\u22DE" },
+ "&curlyeqsucc;": { "codepoints": [8927], "characters": "\u22DF" },
+ "&curlyvee;": { "codepoints": [8910], "characters": "\u22CE" },
+ "&curlywedge;": { "codepoints": [8911], "characters": "\u22CF" },
+ "&curren": { "codepoints": [164], "characters": "\u00A4" },
+ "&curren;": { "codepoints": [164], "characters": "\u00A4" },
+ "&curvearrowleft;": { "codepoints": [8630], "characters": "\u21B6" },
+ "&curvearrowright;": { "codepoints": [8631], "characters": "\u21B7" },
+ "&cuvee;": { "codepoints": [8910], "characters": "\u22CE" },
+ "&cuwed;": { "codepoints": [8911], "characters": "\u22CF" },
+ "&cwconint;": { "codepoints": [8754], "characters": "\u2232" },
+ "&cwint;": { "codepoints": [8753], "characters": "\u2231" },
+ "&cylcty;": { "codepoints": [9005], "characters": "\u232D" },
+ "&dArr;": { "codepoints": [8659], "characters": "\u21D3" },
+ "&dHar;": { "codepoints": [10597], "characters": "\u2965" },
+ "&dagger;": { "codepoints": [8224], "characters": "\u2020" },
+ "&daleth;": { "codepoints": [8504], "characters": "\u2138" },
+ "&darr;": { "codepoints": [8595], "characters": "\u2193" },
+ "&dash;": { "codepoints": [8208], "characters": "\u2010" },
+ "&dashv;": { "codepoints": [8867], "characters": "\u22A3" },
+ "&dbkarow;": { "codepoints": [10511], "characters": "\u290F" },
+ "&dblac;": { "codepoints": [733], "characters": "\u02DD" },
+ "&dcaron;": { "codepoints": [271], "characters": "\u010F" },
+ "&dcy;": { "codepoints": [1076], "characters": "\u0434" },
+ "&dd;": { "codepoints": [8518], "characters": "\u2146" },
+ "&ddagger;": { "codepoints": [8225], "characters": "\u2021" },
+ "&ddarr;": { "codepoints": [8650], "characters": "\u21CA" },
+ "&ddotseq;": { "codepoints": [10871], "characters": "\u2A77" },
+ "&deg": { "codepoints": [176], "characters": "\u00B0" },
+ "&deg;": { "codepoints": [176], "characters": "\u00B0" },
+ "&delta;": { "codepoints": [948], "characters": "\u03B4" },
+ "&demptyv;": { "codepoints": [10673], "characters": "\u29B1" },
+ "&dfisht;": { "codepoints": [10623], "characters": "\u297F" },
+ "&dfr;": { "codepoints": [120097], "characters": "\uD835\uDD21" },
+ "&dharl;": { "codepoints": [8643], "characters": "\u21C3" },
+ "&dharr;": { "codepoints": [8642], "characters": "\u21C2" },
+ "&diam;": { "codepoints": [8900], "characters": "\u22C4" },
+ "&diamond;": { "codepoints": [8900], "characters": "\u22C4" },
+ "&diamondsuit;": { "codepoints": [9830], "characters": "\u2666" },
+ "&diams;": { "codepoints": [9830], "characters": "\u2666" },
+ "&die;": { "codepoints": [168], "characters": "\u00A8" },
+ "&digamma;": { "codepoints": [989], "characters": "\u03DD" },
+ "&disin;": { "codepoints": [8946], "characters": "\u22F2" },
+ "&div;": { "codepoints": [247], "characters": "\u00F7" },
+ "&divide": { "codepoints": [247], "characters": "\u00F7" },
+ "&divide;": { "codepoints": [247], "characters": "\u00F7" },
+ "&divideontimes;": { "codepoints": [8903], "characters": "\u22C7" },
+ "&divonx;": { "codepoints": [8903], "characters": "\u22C7" },
+ "&djcy;": { "codepoints": [1106], "characters": "\u0452" },
+ "&dlcorn;": { "codepoints": [8990], "characters": "\u231E" },
+ "&dlcrop;": { "codepoints": [8973], "characters": "\u230D" },
+ "&dollar;": { "codepoints": [36], "characters": "\u0024" },
+ "&dopf;": { "codepoints": [120149], "characters": "\uD835\uDD55" },
+ "&dot;": { "codepoints": [729], "characters": "\u02D9" },
+ "&doteq;": { "codepoints": [8784], "characters": "\u2250" },
+ "&doteqdot;": { "codepoints": [8785], "characters": "\u2251" },
+ "&dotminus;": { "codepoints": [8760], "characters": "\u2238" },
+ "&dotplus;": { "codepoints": [8724], "characters": "\u2214" },
+ "&dotsquare;": { "codepoints": [8865], "characters": "\u22A1" },
+ "&doublebarwedge;": { "codepoints": [8966], "characters": "\u2306" },
+ "&downarrow;": { "codepoints": [8595], "characters": "\u2193" },
+ "&downdownarrows;": { "codepoints": [8650], "characters": "\u21CA" },
+ "&downharpoonleft;": { "codepoints": [8643], "characters": "\u21C3" },
+ "&downharpoonright;": { "codepoints": [8642], "characters": "\u21C2" },
+ "&drbkarow;": { "codepoints": [10512], "characters": "\u2910" },
+ "&drcorn;": { "codepoints": [8991], "characters": "\u231F" },
+ "&drcrop;": { "codepoints": [8972], "characters": "\u230C" },
+ "&dscr;": { "codepoints": [119993], "characters": "\uD835\uDCB9" },
+ "&dscy;": { "codepoints": [1109], "characters": "\u0455" },
+ "&dsol;": { "codepoints": [10742], "characters": "\u29F6" },
+ "&dstrok;": { "codepoints": [273], "characters": "\u0111" },
+ "&dtdot;": { "codepoints": [8945], "characters": "\u22F1" },
+ "&dtri;": { "codepoints": [9663], "characters": "\u25BF" },
+ "&dtrif;": { "codepoints": [9662], "characters": "\u25BE" },
+ "&duarr;": { "codepoints": [8693], "characters": "\u21F5" },
+ "&duhar;": { "codepoints": [10607], "characters": "\u296F" },
+ "&dwangle;": { "codepoints": [10662], "characters": "\u29A6" },
+ "&dzcy;": { "codepoints": [1119], "characters": "\u045F" },
+ "&dzigrarr;": { "codepoints": [10239], "characters": "\u27FF" },
+ "&eDDot;": { "codepoints": [10871], "characters": "\u2A77" },
+ "&eDot;": { "codepoints": [8785], "characters": "\u2251" },
+ "&eacute": { "codepoints": [233], "characters": "\u00E9" },
+ "&eacute;": { "codepoints": [233], "characters": "\u00E9" },
+ "&easter;": { "codepoints": [10862], "characters": "\u2A6E" },
+ "&ecaron;": { "codepoints": [283], "characters": "\u011B" },
+ "&ecir;": { "codepoints": [8790], "characters": "\u2256" },
+ "&ecirc": { "codepoints": [234], "characters": "\u00EA" },
+ "&ecirc;": { "codepoints": [234], "characters": "\u00EA" },
+ "&ecolon;": { "codepoints": [8789], "characters": "\u2255" },
+ "&ecy;": { "codepoints": [1101], "characters": "\u044D" },
+ "&edot;": { "codepoints": [279], "characters": "\u0117" },
+ "&ee;": { "codepoints": [8519], "characters": "\u2147" },
+ "&efDot;": { "codepoints": [8786], "characters": "\u2252" },
+ "&efr;": { "codepoints": [120098], "characters": "\uD835\uDD22" },
+ "&eg;": { "codepoints": [10906], "characters": "\u2A9A" },
+ "&egrave": { "codepoints": [232], "characters": "\u00E8" },
+ "&egrave;": { "codepoints": [232], "characters": "\u00E8" },
+ "&egs;": { "codepoints": [10902], "characters": "\u2A96" },
+ "&egsdot;": { "codepoints": [10904], "characters": "\u2A98" },
+ "&el;": { "codepoints": [10905], "characters": "\u2A99" },
+ "&elinters;": { "codepoints": [9191], "characters": "\u23E7" },
+ "&ell;": { "codepoints": [8467], "characters": "\u2113" },
+ "&els;": { "codepoints": [10901], "characters": "\u2A95" },
+ "&elsdot;": { "codepoints": [10903], "characters": "\u2A97" },
+ "&emacr;": { "codepoints": [275], "characters": "\u0113" },
+ "&empty;": { "codepoints": [8709], "characters": "\u2205" },
+ "&emptyset;": { "codepoints": [8709], "characters": "\u2205" },
+ "&emptyv;": { "codepoints": [8709], "characters": "\u2205" },
+ "&emsp13;": { "codepoints": [8196], "characters": "\u2004" },
+ "&emsp14;": { "codepoints": [8197], "characters": "\u2005" },
+ "&emsp;": { "codepoints": [8195], "characters": "\u2003" },
+ "&eng;": { "codepoints": [331], "characters": "\u014B" },
+ "&ensp;": { "codepoints": [8194], "characters": "\u2002" },
+ "&eogon;": { "codepoints": [281], "characters": "\u0119" },
+ "&eopf;": { "codepoints": [120150], "characters": "\uD835\uDD56" },
+ "&epar;": { "codepoints": [8917], "characters": "\u22D5" },
+ "&eparsl;": { "codepoints": [10723], "characters": "\u29E3" },
+ "&eplus;": { "codepoints": [10865], "characters": "\u2A71" },
+ "&epsi;": { "codepoints": [949], "characters": "\u03B5" },
+ "&epsilon;": { "codepoints": [949], "characters": "\u03B5" },
+ "&epsiv;": { "codepoints": [1013], "characters": "\u03F5" },
+ "&eqcirc;": { "codepoints": [8790], "characters": "\u2256" },
+ "&eqcolon;": { "codepoints": [8789], "characters": "\u2255" },
+ "&eqsim;": { "codepoints": [8770], "characters": "\u2242" },
+ "&eqslantgtr;": { "codepoints": [10902], "characters": "\u2A96" },
+ "&eqslantless;": { "codepoints": [10901], "characters": "\u2A95" },
+ "&equals;": { "codepoints": [61], "characters": "\u003D" },
+ "&equest;": { "codepoints": [8799], "characters": "\u225F" },
+ "&equiv;": { "codepoints": [8801], "characters": "\u2261" },
+ "&equivDD;": { "codepoints": [10872], "characters": "\u2A78" },
+ "&eqvparsl;": { "codepoints": [10725], "characters": "\u29E5" },
+ "&erDot;": { "codepoints": [8787], "characters": "\u2253" },
+ "&erarr;": { "codepoints": [10609], "characters": "\u2971" },
+ "&escr;": { "codepoints": [8495], "characters": "\u212F" },
+ "&esdot;": { "codepoints": [8784], "characters": "\u2250" },
+ "&esim;": { "codepoints": [8770], "characters": "\u2242" },
+ "&eta;": { "codepoints": [951], "characters": "\u03B7" },
+ "&eth": { "codepoints": [240], "characters": "\u00F0" },
+ "&eth;": { "codepoints": [240], "characters": "\u00F0" },
+ "&euml": { "codepoints": [235], "characters": "\u00EB" },
+ "&euml;": { "codepoints": [235], "characters": "\u00EB" },
+ "&euro;": { "codepoints": [8364], "characters": "\u20AC" },
+ "&excl;": { "codepoints": [33], "characters": "\u0021" },
+ "&exist;": { "codepoints": [8707], "characters": "\u2203" },
+ "&expectation;": { "codepoints": [8496], "characters": "\u2130" },
+ "&exponentiale;": { "codepoints": [8519], "characters": "\u2147" },
+ "&fallingdotseq;": { "codepoints": [8786], "characters": "\u2252" },
+ "&fcy;": { "codepoints": [1092], "characters": "\u0444" },
+ "&female;": { "codepoints": [9792], "characters": "\u2640" },
+ "&ffilig;": { "codepoints": [64259], "characters": "\uFB03" },
+ "&fflig;": { "codepoints": [64256], "characters": "\uFB00" },
+ "&ffllig;": { "codepoints": [64260], "characters": "\uFB04" },
+ "&ffr;": { "codepoints": [120099], "characters": "\uD835\uDD23" },
+ "&filig;": { "codepoints": [64257], "characters": "\uFB01" },
+ "&fjlig;": { "codepoints": [102, 106], "characters": "\u0066\u006A" },
+ "&flat;": { "codepoints": [9837], "characters": "\u266D" },
+ "&fllig;": { "codepoints": [64258], "characters": "\uFB02" },
+ "&fltns;": { "codepoints": [9649], "characters": "\u25B1" },
+ "&fnof;": { "codepoints": [402], "characters": "\u0192" },
+ "&fopf;": { "codepoints": [120151], "characters": "\uD835\uDD57" },
+ "&forall;": { "codepoints": [8704], "characters": "\u2200" },
+ "&fork;": { "codepoints": [8916], "characters": "\u22D4" },
+ "&forkv;": { "codepoints": [10969], "characters": "\u2AD9" },
+ "&fpartint;": { "codepoints": [10765], "characters": "\u2A0D" },
+ "&frac12": { "codepoints": [189], "characters": "\u00BD" },
+ "&frac12;": { "codepoints": [189], "characters": "\u00BD" },
+ "&frac13;": { "codepoints": [8531], "characters": "\u2153" },
+ "&frac14": { "codepoints": [188], "characters": "\u00BC" },
+ "&frac14;": { "codepoints": [188], "characters": "\u00BC" },
+ "&frac15;": { "codepoints": [8533], "characters": "\u2155" },
+ "&frac16;": { "codepoints": [8537], "characters": "\u2159" },
+ "&frac18;": { "codepoints": [8539], "characters": "\u215B" },
+ "&frac23;": { "codepoints": [8532], "characters": "\u2154" },
+ "&frac25;": { "codepoints": [8534], "characters": "\u2156" },
+ "&frac34": { "codepoints": [190], "characters": "\u00BE" },
+ "&frac34;": { "codepoints": [190], "characters": "\u00BE" },
+ "&frac35;": { "codepoints": [8535], "characters": "\u2157" },
+ "&frac38;": { "codepoints": [8540], "characters": "\u215C" },
+ "&frac45;": { "codepoints": [8536], "characters": "\u2158" },
+ "&frac56;": { "codepoints": [8538], "characters": "\u215A" },
+ "&frac58;": { "codepoints": [8541], "characters": "\u215D" },
+ "&frac78;": { "codepoints": [8542], "characters": "\u215E" },
+ "&frasl;": { "codepoints": [8260], "characters": "\u2044" },
+ "&frown;": { "codepoints": [8994], "characters": "\u2322" },
+ "&fscr;": { "codepoints": [119995], "characters": "\uD835\uDCBB" },
+ "&gE;": { "codepoints": [8807], "characters": "\u2267" },
+ "&gEl;": { "codepoints": [10892], "characters": "\u2A8C" },
+ "&gacute;": { "codepoints": [501], "characters": "\u01F5" },
+ "&gamma;": { "codepoints": [947], "characters": "\u03B3" },
+ "&gammad;": { "codepoints": [989], "characters": "\u03DD" },
+ "&gap;": { "codepoints": [10886], "characters": "\u2A86" },
+ "&gbreve;": { "codepoints": [287], "characters": "\u011F" },
+ "&gcirc;": { "codepoints": [285], "characters": "\u011D" },
+ "&gcy;": { "codepoints": [1075], "characters": "\u0433" },
+ "&gdot;": { "codepoints": [289], "characters": "\u0121" },
+ "&ge;": { "codepoints": [8805], "characters": "\u2265" },
+ "&gel;": { "codepoints": [8923], "characters": "\u22DB" },
+ "&geq;": { "codepoints": [8805], "characters": "\u2265" },
+ "&geqq;": { "codepoints": [8807], "characters": "\u2267" },
+ "&geqslant;": { "codepoints": [10878], "characters": "\u2A7E" },
+ "&ges;": { "codepoints": [10878], "characters": "\u2A7E" },
+ "&gescc;": { "codepoints": [10921], "characters": "\u2AA9" },
+ "&gesdot;": { "codepoints": [10880], "characters": "\u2A80" },
+ "&gesdoto;": { "codepoints": [10882], "characters": "\u2A82" },
+ "&gesdotol;": { "codepoints": [10884], "characters": "\u2A84" },
+ "&gesl;": { "codepoints": [8923, 65024], "characters": "\u22DB\uFE00" },
+ "&gesles;": { "codepoints": [10900], "characters": "\u2A94" },
+ "&gfr;": { "codepoints": [120100], "characters": "\uD835\uDD24" },
+ "&gg;": { "codepoints": [8811], "characters": "\u226B" },
+ "&ggg;": { "codepoints": [8921], "characters": "\u22D9" },
+ "&gimel;": { "codepoints": [8503], "characters": "\u2137" },
+ "&gjcy;": { "codepoints": [1107], "characters": "\u0453" },
+ "&gl;": { "codepoints": [8823], "characters": "\u2277" },
+ "&glE;": { "codepoints": [10898], "characters": "\u2A92" },
+ "&gla;": { "codepoints": [10917], "characters": "\u2AA5" },
+ "&glj;": { "codepoints": [10916], "characters": "\u2AA4" },
+ "&gnE;": { "codepoints": [8809], "characters": "\u2269" },
+ "&gnap;": { "codepoints": [10890], "characters": "\u2A8A" },
+ "&gnapprox;": { "codepoints": [10890], "characters": "\u2A8A" },
+ "&gne;": { "codepoints": [10888], "characters": "\u2A88" },
+ "&gneq;": { "codepoints": [10888], "characters": "\u2A88" },
+ "&gneqq;": { "codepoints": [8809], "characters": "\u2269" },
+ "&gnsim;": { "codepoints": [8935], "characters": "\u22E7" },
+ "&gopf;": { "codepoints": [120152], "characters": "\uD835\uDD58" },
+ "&grave;": { "codepoints": [96], "characters": "\u0060" },
+ "&gscr;": { "codepoints": [8458], "characters": "\u210A" },
+ "&gsim;": { "codepoints": [8819], "characters": "\u2273" },
+ "&gsime;": { "codepoints": [10894], "characters": "\u2A8E" },
+ "&gsiml;": { "codepoints": [10896], "characters": "\u2A90" },
+ "&gt": { "codepoints": [62], "characters": "\u003E" },
+ "&gt;": { "codepoints": [62], "characters": "\u003E" },
+ "&gtcc;": { "codepoints": [10919], "characters": "\u2AA7" },
+ "&gtcir;": { "codepoints": [10874], "characters": "\u2A7A" },
+ "&gtdot;": { "codepoints": [8919], "characters": "\u22D7" },
+ "&gtlPar;": { "codepoints": [10645], "characters": "\u2995" },
+ "&gtquest;": { "codepoints": [10876], "characters": "\u2A7C" },
+ "&gtrapprox;": { "codepoints": [10886], "characters": "\u2A86" },
+ "&gtrarr;": { "codepoints": [10616], "characters": "\u2978" },
+ "&gtrdot;": { "codepoints": [8919], "characters": "\u22D7" },
+ "&gtreqless;": { "codepoints": [8923], "characters": "\u22DB" },
+ "&gtreqqless;": { "codepoints": [10892], "characters": "\u2A8C" },
+ "&gtrless;": { "codepoints": [8823], "characters": "\u2277" },
+ "&gtrsim;": { "codepoints": [8819], "characters": "\u2273" },
+ "&gvertneqq;": { "codepoints": [8809, 65024], "characters": "\u2269\uFE00" },
+ "&gvnE;": { "codepoints": [8809, 65024], "characters": "\u2269\uFE00" },
+ "&hArr;": { "codepoints": [8660], "characters": "\u21D4" },
+ "&hairsp;": { "codepoints": [8202], "characters": "\u200A" },
+ "&half;": { "codepoints": [189], "characters": "\u00BD" },
+ "&hamilt;": { "codepoints": [8459], "characters": "\u210B" },
+ "&hardcy;": { "codepoints": [1098], "characters": "\u044A" },
+ "&harr;": { "codepoints": [8596], "characters": "\u2194" },
+ "&harrcir;": { "codepoints": [10568], "characters": "\u2948" },
+ "&harrw;": { "codepoints": [8621], "characters": "\u21AD" },
+ "&hbar;": { "codepoints": [8463], "characters": "\u210F" },
+ "&hcirc;": { "codepoints": [293], "characters": "\u0125" },
+ "&hearts;": { "codepoints": [9829], "characters": "\u2665" },
+ "&heartsuit;": { "codepoints": [9829], "characters": "\u2665" },
+ "&hellip;": { "codepoints": [8230], "characters": "\u2026" },
+ "&hercon;": { "codepoints": [8889], "characters": "\u22B9" },
+ "&hfr;": { "codepoints": [120101], "characters": "\uD835\uDD25" },
+ "&hksearow;": { "codepoints": [10533], "characters": "\u2925" },
+ "&hkswarow;": { "codepoints": [10534], "characters": "\u2926" },
+ "&hoarr;": { "codepoints": [8703], "characters": "\u21FF" },
+ "&homtht;": { "codepoints": [8763], "characters": "\u223B" },
+ "&hookleftarrow;": { "codepoints": [8617], "characters": "\u21A9" },
+ "&hookrightarrow;": { "codepoints": [8618], "characters": "\u21AA" },
+ "&hopf;": { "codepoints": [120153], "characters": "\uD835\uDD59" },
+ "&horbar;": { "codepoints": [8213], "characters": "\u2015" },
+ "&hscr;": { "codepoints": [119997], "characters": "\uD835\uDCBD" },
+ "&hslash;": { "codepoints": [8463], "characters": "\u210F" },
+ "&hstrok;": { "codepoints": [295], "characters": "\u0127" },
+ "&hybull;": { "codepoints": [8259], "characters": "\u2043" },
+ "&hyphen;": { "codepoints": [8208], "characters": "\u2010" },
+ "&iacute": { "codepoints": [237], "characters": "\u00ED" },
+ "&iacute;": { "codepoints": [237], "characters": "\u00ED" },
+ "&ic;": { "codepoints": [8291], "characters": "\u2063" },
+ "&icirc": { "codepoints": [238], "characters": "\u00EE" },
+ "&icirc;": { "codepoints": [238], "characters": "\u00EE" },
+ "&icy;": { "codepoints": [1080], "characters": "\u0438" },
+ "&iecy;": { "codepoints": [1077], "characters": "\u0435" },
+ "&iexcl": { "codepoints": [161], "characters": "\u00A1" },
+ "&iexcl;": { "codepoints": [161], "characters": "\u00A1" },
+ "&iff;": { "codepoints": [8660], "characters": "\u21D4" },
+ "&ifr;": { "codepoints": [120102], "characters": "\uD835\uDD26" },
+ "&igrave": { "codepoints": [236], "characters": "\u00EC" },
+ "&igrave;": { "codepoints": [236], "characters": "\u00EC" },
+ "&ii;": { "codepoints": [8520], "characters": "\u2148" },
+ "&iiiint;": { "codepoints": [10764], "characters": "\u2A0C" },
+ "&iiint;": { "codepoints": [8749], "characters": "\u222D" },
+ "&iinfin;": { "codepoints": [10716], "characters": "\u29DC" },
+ "&iiota;": { "codepoints": [8489], "characters": "\u2129" },
+ "&ijlig;": { "codepoints": [307], "characters": "\u0133" },
+ "&imacr;": { "codepoints": [299], "characters": "\u012B" },
+ "&image;": { "codepoints": [8465], "characters": "\u2111" },
+ "&imagline;": { "codepoints": [8464], "characters": "\u2110" },
+ "&imagpart;": { "codepoints": [8465], "characters": "\u2111" },
+ "&imath;": { "codepoints": [305], "characters": "\u0131" },
+ "&imof;": { "codepoints": [8887], "characters": "\u22B7" },
+ "&imped;": { "codepoints": [437], "characters": "\u01B5" },
+ "&in;": { "codepoints": [8712], "characters": "\u2208" },
+ "&incare;": { "codepoints": [8453], "characters": "\u2105" },
+ "&infin;": { "codepoints": [8734], "characters": "\u221E" },
+ "&infintie;": { "codepoints": [10717], "characters": "\u29DD" },
+ "&inodot;": { "codepoints": [305], "characters": "\u0131" },
+ "&int;": { "codepoints": [8747], "characters": "\u222B" },
+ "&intcal;": { "codepoints": [8890], "characters": "\u22BA" },
+ "&integers;": { "codepoints": [8484], "characters": "\u2124" },
+ "&intercal;": { "codepoints": [8890], "characters": "\u22BA" },
+ "&intlarhk;": { "codepoints": [10775], "characters": "\u2A17" },
+ "&intprod;": { "codepoints": [10812], "characters": "\u2A3C" },
+ "&iocy;": { "codepoints": [1105], "characters": "\u0451" },
+ "&iogon;": { "codepoints": [303], "characters": "\u012F" },
+ "&iopf;": { "codepoints": [120154], "characters": "\uD835\uDD5A" },
+ "&iota;": { "codepoints": [953], "characters": "\u03B9" },
+ "&iprod;": { "codepoints": [10812], "characters": "\u2A3C" },
+ "&iquest": { "codepoints": [191], "characters": "\u00BF" },
+ "&iquest;": { "codepoints": [191], "characters": "\u00BF" },
+ "&iscr;": { "codepoints": [119998], "characters": "\uD835\uDCBE" },
+ "&isin;": { "codepoints": [8712], "characters": "\u2208" },
+ "&isinE;": { "codepoints": [8953], "characters": "\u22F9" },
+ "&isindot;": { "codepoints": [8949], "characters": "\u22F5" },
+ "&isins;": { "codepoints": [8948], "characters": "\u22F4" },
+ "&isinsv;": { "codepoints": [8947], "characters": "\u22F3" },
+ "&isinv;": { "codepoints": [8712], "characters": "\u2208" },
+ "&it;": { "codepoints": [8290], "characters": "\u2062" },
+ "&itilde;": { "codepoints": [297], "characters": "\u0129" },
+ "&iukcy;": { "codepoints": [1110], "characters": "\u0456" },
+ "&iuml": { "codepoints": [239], "characters": "\u00EF" },
+ "&iuml;": { "codepoints": [239], "characters": "\u00EF" },
+ "&jcirc;": { "codepoints": [309], "characters": "\u0135" },
+ "&jcy;": { "codepoints": [1081], "characters": "\u0439" },
+ "&jfr;": { "codepoints": [120103], "characters": "\uD835\uDD27" },
+ "&jmath;": { "codepoints": [567], "characters": "\u0237" },
+ "&jopf;": { "codepoints": [120155], "characters": "\uD835\uDD5B" },
+ "&jscr;": { "codepoints": [119999], "characters": "\uD835\uDCBF" },
+ "&jsercy;": { "codepoints": [1112], "characters": "\u0458" },
+ "&jukcy;": { "codepoints": [1108], "characters": "\u0454" },
+ "&kappa;": { "codepoints": [954], "characters": "\u03BA" },
+ "&kappav;": { "codepoints": [1008], "characters": "\u03F0" },
+ "&kcedil;": { "codepoints": [311], "characters": "\u0137" },
+ "&kcy;": { "codepoints": [1082], "characters": "\u043A" },
+ "&kfr;": { "codepoints": [120104], "characters": "\uD835\uDD28" },
+ "&kgreen;": { "codepoints": [312], "characters": "\u0138" },
+ "&khcy;": { "codepoints": [1093], "characters": "\u0445" },
+ "&kjcy;": { "codepoints": [1116], "characters": "\u045C" },
+ "&kopf;": { "codepoints": [120156], "characters": "\uD835\uDD5C" },
+ "&kscr;": { "codepoints": [120000], "characters": "\uD835\uDCC0" },
+ "&lAarr;": { "codepoints": [8666], "characters": "\u21DA" },
+ "&lArr;": { "codepoints": [8656], "characters": "\u21D0" },
+ "&lAtail;": { "codepoints": [10523], "characters": "\u291B" },
+ "&lBarr;": { "codepoints": [10510], "characters": "\u290E" },
+ "&lE;": { "codepoints": [8806], "characters": "\u2266" },
+ "&lEg;": { "codepoints": [10891], "characters": "\u2A8B" },
+ "&lHar;": { "codepoints": [10594], "characters": "\u2962" },
+ "&lacute;": { "codepoints": [314], "characters": "\u013A" },
+ "&laemptyv;": { "codepoints": [10676], "characters": "\u29B4" },
+ "&lagran;": { "codepoints": [8466], "characters": "\u2112" },
+ "&lambda;": { "codepoints": [955], "characters": "\u03BB" },
+ "&lang;": { "codepoints": [10216], "characters": "\u27E8" },
+ "&langd;": { "codepoints": [10641], "characters": "\u2991" },
+ "&langle;": { "codepoints": [10216], "characters": "\u27E8" },
+ "&lap;": { "codepoints": [10885], "characters": "\u2A85" },
+ "&laquo": { "codepoints": [171], "characters": "\u00AB" },
+ "&laquo;": { "codepoints": [171], "characters": "\u00AB" },
+ "&larr;": { "codepoints": [8592], "characters": "\u2190" },
+ "&larrb;": { "codepoints": [8676], "characters": "\u21E4" },
+ "&larrbfs;": { "codepoints": [10527], "characters": "\u291F" },
+ "&larrfs;": { "codepoints": [10525], "characters": "\u291D" },
+ "&larrhk;": { "codepoints": [8617], "characters": "\u21A9" },
+ "&larrlp;": { "codepoints": [8619], "characters": "\u21AB" },
+ "&larrpl;": { "codepoints": [10553], "characters": "\u2939" },
+ "&larrsim;": { "codepoints": [10611], "characters": "\u2973" },
+ "&larrtl;": { "codepoints": [8610], "characters": "\u21A2" },
+ "&lat;": { "codepoints": [10923], "characters": "\u2AAB" },
+ "&latail;": { "codepoints": [10521], "characters": "\u2919" },
+ "&late;": { "codepoints": [10925], "characters": "\u2AAD" },
+ "&lates;": { "codepoints": [10925, 65024], "characters": "\u2AAD\uFE00" },
+ "&lbarr;": { "codepoints": [10508], "characters": "\u290C" },
+ "&lbbrk;": { "codepoints": [10098], "characters": "\u2772" },
+ "&lbrace;": { "codepoints": [123], "characters": "\u007B" },
+ "&lbrack;": { "codepoints": [91], "characters": "\u005B" },
+ "&lbrke;": { "codepoints": [10635], "characters": "\u298B" },
+ "&lbrksld;": { "codepoints": [10639], "characters": "\u298F" },
+ "&lbrkslu;": { "codepoints": [10637], "characters": "\u298D" },
+ "&lcaron;": { "codepoints": [318], "characters": "\u013E" },
+ "&lcedil;": { "codepoints": [316], "characters": "\u013C" },
+ "&lceil;": { "codepoints": [8968], "characters": "\u2308" },
+ "&lcub;": { "codepoints": [123], "characters": "\u007B" },
+ "&lcy;": { "codepoints": [1083], "characters": "\u043B" },
+ "&ldca;": { "codepoints": [10550], "characters": "\u2936" },
+ "&ldquo;": { "codepoints": [8220], "characters": "\u201C" },
+ "&ldquor;": { "codepoints": [8222], "characters": "\u201E" },
+ "&ldrdhar;": { "codepoints": [10599], "characters": "\u2967" },
+ "&ldrushar;": { "codepoints": [10571], "characters": "\u294B" },
+ "&ldsh;": { "codepoints": [8626], "characters": "\u21B2" },
+ "&le;": { "codepoints": [8804], "characters": "\u2264" },
+ "&leftarrow;": { "codepoints": [8592], "characters": "\u2190" },
+ "&leftarrowtail;": { "codepoints": [8610], "characters": "\u21A2" },
+ "&leftharpoondown;": { "codepoints": [8637], "characters": "\u21BD" },
+ "&leftharpoonup;": { "codepoints": [8636], "characters": "\u21BC" },
+ "&leftleftarrows;": { "codepoints": [8647], "characters": "\u21C7" },
+ "&leftrightarrow;": { "codepoints": [8596], "characters": "\u2194" },
+ "&leftrightarrows;": { "codepoints": [8646], "characters": "\u21C6" },
+ "&leftrightharpoons;": { "codepoints": [8651], "characters": "\u21CB" },
+ "&leftrightsquigarrow;": { "codepoints": [8621], "characters": "\u21AD" },
+ "&leftthreetimes;": { "codepoints": [8907], "characters": "\u22CB" },
+ "&leg;": { "codepoints": [8922], "characters": "\u22DA" },
+ "&leq;": { "codepoints": [8804], "characters": "\u2264" },
+ "&leqq;": { "codepoints": [8806], "characters": "\u2266" },
+ "&leqslant;": { "codepoints": [10877], "characters": "\u2A7D" },
+ "&les;": { "codepoints": [10877], "characters": "\u2A7D" },
+ "&lescc;": { "codepoints": [10920], "characters": "\u2AA8" },
+ "&lesdot;": { "codepoints": [10879], "characters": "\u2A7F" },
+ "&lesdoto;": { "codepoints": [10881], "characters": "\u2A81" },
+ "&lesdotor;": { "codepoints": [10883], "characters": "\u2A83" },
+ "&lesg;": { "codepoints": [8922, 65024], "characters": "\u22DA\uFE00" },
+ "&lesges;": { "codepoints": [10899], "characters": "\u2A93" },
+ "&lessapprox;": { "codepoints": [10885], "characters": "\u2A85" },
+ "&lessdot;": { "codepoints": [8918], "characters": "\u22D6" },
+ "&lesseqgtr;": { "codepoints": [8922], "characters": "\u22DA" },
+ "&lesseqqgtr;": { "codepoints": [10891], "characters": "\u2A8B" },
+ "&lessgtr;": { "codepoints": [8822], "characters": "\u2276" },
+ "&lesssim;": { "codepoints": [8818], "characters": "\u2272" },
+ "&lfisht;": { "codepoints": [10620], "characters": "\u297C" },
+ "&lfloor;": { "codepoints": [8970], "characters": "\u230A" },
+ "&lfr;": { "codepoints": [120105], "characters": "\uD835\uDD29" },
+ "&lg;": { "codepoints": [8822], "characters": "\u2276" },
+ "&lgE;": { "codepoints": [10897], "characters": "\u2A91" },
+ "&lhard;": { "codepoints": [8637], "characters": "\u21BD" },
+ "&lharu;": { "codepoints": [8636], "characters": "\u21BC" },
+ "&lharul;": { "codepoints": [10602], "characters": "\u296A" },
+ "&lhblk;": { "codepoints": [9604], "characters": "\u2584" },
+ "&ljcy;": { "codepoints": [1113], "characters": "\u0459" },
+ "&ll;": { "codepoints": [8810], "characters": "\u226A" },
+ "&llarr;": { "codepoints": [8647], "characters": "\u21C7" },
+ "&llcorner;": { "codepoints": [8990], "characters": "\u231E" },
+ "&llhard;": { "codepoints": [10603], "characters": "\u296B" },
+ "&lltri;": { "codepoints": [9722], "characters": "\u25FA" },
+ "&lmidot;": { "codepoints": [320], "characters": "\u0140" },
+ "&lmoust;": { "codepoints": [9136], "characters": "\u23B0" },
+ "&lmoustache;": { "codepoints": [9136], "characters": "\u23B0" },
+ "&lnE;": { "codepoints": [8808], "characters": "\u2268" },
+ "&lnap;": { "codepoints": [10889], "characters": "\u2A89" },
+ "&lnapprox;": { "codepoints": [10889], "characters": "\u2A89" },
+ "&lne;": { "codepoints": [10887], "characters": "\u2A87" },
+ "&lneq;": { "codepoints": [10887], "characters": "\u2A87" },
+ "&lneqq;": { "codepoints": [8808], "characters": "\u2268" },
+ "&lnsim;": { "codepoints": [8934], "characters": "\u22E6" },
+ "&loang;": { "codepoints": [10220], "characters": "\u27EC" },
+ "&loarr;": { "codepoints": [8701], "characters": "\u21FD" },
+ "&lobrk;": { "codepoints": [10214], "characters": "\u27E6" },
+ "&longleftarrow;": { "codepoints": [10229], "characters": "\u27F5" },
+ "&longleftrightarrow;": { "codepoints": [10231], "characters": "\u27F7" },
+ "&longmapsto;": { "codepoints": [10236], "characters": "\u27FC" },
+ "&longrightarrow;": { "codepoints": [10230], "characters": "\u27F6" },
+ "&looparrowleft;": { "codepoints": [8619], "characters": "\u21AB" },
+ "&looparrowright;": { "codepoints": [8620], "characters": "\u21AC" },
+ "&lopar;": { "codepoints": [10629], "characters": "\u2985" },
+ "&lopf;": { "codepoints": [120157], "characters": "\uD835\uDD5D" },
+ "&loplus;": { "codepoints": [10797], "characters": "\u2A2D" },
+ "&lotimes;": { "codepoints": [10804], "characters": "\u2A34" },
+ "&lowast;": { "codepoints": [8727], "characters": "\u2217" },
+ "&lowbar;": { "codepoints": [95], "characters": "\u005F" },
+ "&loz;": { "codepoints": [9674], "characters": "\u25CA" },
+ "&lozenge;": { "codepoints": [9674], "characters": "\u25CA" },
+ "&lozf;": { "codepoints": [10731], "characters": "\u29EB" },
+ "&lpar;": { "codepoints": [40], "characters": "\u0028" },
+ "&lparlt;": { "codepoints": [10643], "characters": "\u2993" },
+ "&lrarr;": { "codepoints": [8646], "characters": "\u21C6" },
+ "&lrcorner;": { "codepoints": [8991], "characters": "\u231F" },
+ "&lrhar;": { "codepoints": [8651], "characters": "\u21CB" },
+ "&lrhard;": { "codepoints": [10605], "characters": "\u296D" },
+ "&lrm;": { "codepoints": [8206], "characters": "\u200E" },
+ "&lrtri;": { "codepoints": [8895], "characters": "\u22BF" },
+ "&lsaquo;": { "codepoints": [8249], "characters": "\u2039" },
+ "&lscr;": { "codepoints": [120001], "characters": "\uD835\uDCC1" },
+ "&lsh;": { "codepoints": [8624], "characters": "\u21B0" },
+ "&lsim;": { "codepoints": [8818], "characters": "\u2272" },
+ "&lsime;": { "codepoints": [10893], "characters": "\u2A8D" },
+ "&lsimg;": { "codepoints": [10895], "characters": "\u2A8F" },
+ "&lsqb;": { "codepoints": [91], "characters": "\u005B" },
+ "&lsquo;": { "codepoints": [8216], "characters": "\u2018" },
+ "&lsquor;": { "codepoints": [8218], "characters": "\u201A" },
+ "&lstrok;": { "codepoints": [322], "characters": "\u0142" },
+ "&lt": { "codepoints": [60], "characters": "\u003C" },
+ "&lt;": { "codepoints": [60], "characters": "\u003C" },
+ "&ltcc;": { "codepoints": [10918], "characters": "\u2AA6" },
+ "&ltcir;": { "codepoints": [10873], "characters": "\u2A79" },
+ "&ltdot;": { "codepoints": [8918], "characters": "\u22D6" },
+ "&lthree;": { "codepoints": [8907], "characters": "\u22CB" },
+ "&ltimes;": { "codepoints": [8905], "characters": "\u22C9" },
+ "&ltlarr;": { "codepoints": [10614], "characters": "\u2976" },
+ "&ltquest;": { "codepoints": [10875], "characters": "\u2A7B" },
+ "&ltrPar;": { "codepoints": [10646], "characters": "\u2996" },
+ "&ltri;": { "codepoints": [9667], "characters": "\u25C3" },
+ "&ltrie;": { "codepoints": [8884], "characters": "\u22B4" },
+ "&ltrif;": { "codepoints": [9666], "characters": "\u25C2" },
+ "&lurdshar;": { "codepoints": [10570], "characters": "\u294A" },
+ "&luruhar;": { "codepoints": [10598], "characters": "\u2966" },
+ "&lvertneqq;": { "codepoints": [8808, 65024], "characters": "\u2268\uFE00" },
+ "&lvnE;": { "codepoints": [8808, 65024], "characters": "\u2268\uFE00" },
+ "&mDDot;": { "codepoints": [8762], "characters": "\u223A" },
+ "&macr": { "codepoints": [175], "characters": "\u00AF" },
+ "&macr;": { "codepoints": [175], "characters": "\u00AF" },
+ "&male;": { "codepoints": [9794], "characters": "\u2642" },
+ "&malt;": { "codepoints": [10016], "characters": "\u2720" },
+ "&maltese;": { "codepoints": [10016], "characters": "\u2720" },
+ "&map;": { "codepoints": [8614], "characters": "\u21A6" },
+ "&mapsto;": { "codepoints": [8614], "characters": "\u21A6" },
+ "&mapstodown;": { "codepoints": [8615], "characters": "\u21A7" },
+ "&mapstoleft;": { "codepoints": [8612], "characters": "\u21A4" },
+ "&mapstoup;": { "codepoints": [8613], "characters": "\u21A5" },
+ "&marker;": { "codepoints": [9646], "characters": "\u25AE" },
+ "&mcomma;": { "codepoints": [10793], "characters": "\u2A29" },
+ "&mcy;": { "codepoints": [1084], "characters": "\u043C" },
+ "&mdash;": { "codepoints": [8212], "characters": "\u2014" },
+ "&measuredangle;": { "codepoints": [8737], "characters": "\u2221" },
+ "&mfr;": { "codepoints": [120106], "characters": "\uD835\uDD2A" },
+ "&mho;": { "codepoints": [8487], "characters": "\u2127" },
+ "&micro": { "codepoints": [181], "characters": "\u00B5" },
+ "&micro;": { "codepoints": [181], "characters": "\u00B5" },
+ "&mid;": { "codepoints": [8739], "characters": "\u2223" },
+ "&midast;": { "codepoints": [42], "characters": "\u002A" },
+ "&midcir;": { "codepoints": [10992], "characters": "\u2AF0" },
+ "&middot": { "codepoints": [183], "characters": "\u00B7" },
+ "&middot;": { "codepoints": [183], "characters": "\u00B7" },
+ "&minus;": { "codepoints": [8722], "characters": "\u2212" },
+ "&minusb;": { "codepoints": [8863], "characters": "\u229F" },
+ "&minusd;": { "codepoints": [8760], "characters": "\u2238" },
+ "&minusdu;": { "codepoints": [10794], "characters": "\u2A2A" },
+ "&mlcp;": { "codepoints": [10971], "characters": "\u2ADB" },
+ "&mldr;": { "codepoints": [8230], "characters": "\u2026" },
+ "&mnplus;": { "codepoints": [8723], "characters": "\u2213" },
+ "&models;": { "codepoints": [8871], "characters": "\u22A7" },
+ "&mopf;": { "codepoints": [120158], "characters": "\uD835\uDD5E" },
+ "&mp;": { "codepoints": [8723], "characters": "\u2213" },
+ "&mscr;": { "codepoints": [120002], "characters": "\uD835\uDCC2" },
+ "&mstpos;": { "codepoints": [8766], "characters": "\u223E" },
+ "&mu;": { "codepoints": [956], "characters": "\u03BC" },
+ "&multimap;": { "codepoints": [8888], "characters": "\u22B8" },
+ "&mumap;": { "codepoints": [8888], "characters": "\u22B8" },
+ "&nGg;": { "codepoints": [8921, 824], "characters": "\u22D9\u0338" },
+ "&nGt;": { "codepoints": [8811, 8402], "characters": "\u226B\u20D2" },
+ "&nGtv;": { "codepoints": [8811, 824], "characters": "\u226B\u0338" },
+ "&nLeftarrow;": { "codepoints": [8653], "characters": "\u21CD" },
+ "&nLeftrightarrow;": { "codepoints": [8654], "characters": "\u21CE" },
+ "&nLl;": { "codepoints": [8920, 824], "characters": "\u22D8\u0338" },
+ "&nLt;": { "codepoints": [8810, 8402], "characters": "\u226A\u20D2" },
+ "&nLtv;": { "codepoints": [8810, 824], "characters": "\u226A\u0338" },
+ "&nRightarrow;": { "codepoints": [8655], "characters": "\u21CF" },
+ "&nVDash;": { "codepoints": [8879], "characters": "\u22AF" },
+ "&nVdash;": { "codepoints": [8878], "characters": "\u22AE" },
+ "&nabla;": { "codepoints": [8711], "characters": "\u2207" },
+ "&nacute;": { "codepoints": [324], "characters": "\u0144" },
+ "&nang;": { "codepoints": [8736, 8402], "characters": "\u2220\u20D2" },
+ "&nap;": { "codepoints": [8777], "characters": "\u2249" },
+ "&napE;": { "codepoints": [10864, 824], "characters": "\u2A70\u0338" },
+ "&napid;": { "codepoints": [8779, 824], "characters": "\u224B\u0338" },
+ "&napos;": { "codepoints": [329], "characters": "\u0149" },
+ "&napprox;": { "codepoints": [8777], "characters": "\u2249" },
+ "&natur;": { "codepoints": [9838], "characters": "\u266E" },
+ "&natural;": { "codepoints": [9838], "characters": "\u266E" },
+ "&naturals;": { "codepoints": [8469], "characters": "\u2115" },
+ "&nbsp": { "codepoints": [160], "characters": "\u00A0" },
+ "&nbsp;": { "codepoints": [160], "characters": "\u00A0" },
+ "&nbump;": { "codepoints": [8782, 824], "characters": "\u224E\u0338" },
+ "&nbumpe;": { "codepoints": [8783, 824], "characters": "\u224F\u0338" },
+ "&ncap;": { "codepoints": [10819], "characters": "\u2A43" },
+ "&ncaron;": { "codepoints": [328], "characters": "\u0148" },
+ "&ncedil;": { "codepoints": [326], "characters": "\u0146" },
+ "&ncong;": { "codepoints": [8775], "characters": "\u2247" },
+ "&ncongdot;": { "codepoints": [10861, 824], "characters": "\u2A6D\u0338" },
+ "&ncup;": { "codepoints": [10818], "characters": "\u2A42" },
+ "&ncy;": { "codepoints": [1085], "characters": "\u043D" },
+ "&ndash;": { "codepoints": [8211], "characters": "\u2013" },
+ "&ne;": { "codepoints": [8800], "characters": "\u2260" },
+ "&neArr;": { "codepoints": [8663], "characters": "\u21D7" },
+ "&nearhk;": { "codepoints": [10532], "characters": "\u2924" },
+ "&nearr;": { "codepoints": [8599], "characters": "\u2197" },
+ "&nearrow;": { "codepoints": [8599], "characters": "\u2197" },
+ "&nedot;": { "codepoints": [8784, 824], "characters": "\u2250\u0338" },
+ "&nequiv;": { "codepoints": [8802], "characters": "\u2262" },
+ "&nesear;": { "codepoints": [10536], "characters": "\u2928" },
+ "&nesim;": { "codepoints": [8770, 824], "characters": "\u2242\u0338" },
+ "&nexist;": { "codepoints": [8708], "characters": "\u2204" },
+ "&nexists;": { "codepoints": [8708], "characters": "\u2204" },
+ "&nfr;": { "codepoints": [120107], "characters": "\uD835\uDD2B" },
+ "&ngE;": { "codepoints": [8807, 824], "characters": "\u2267\u0338" },
+ "&nge;": { "codepoints": [8817], "characters": "\u2271" },
+ "&ngeq;": { "codepoints": [8817], "characters": "\u2271" },
+ "&ngeqq;": { "codepoints": [8807, 824], "characters": "\u2267\u0338" },
+ "&ngeqslant;": { "codepoints": [10878, 824], "characters": "\u2A7E\u0338" },
+ "&nges;": { "codepoints": [10878, 824], "characters": "\u2A7E\u0338" },
+ "&ngsim;": { "codepoints": [8821], "characters": "\u2275" },
+ "&ngt;": { "codepoints": [8815], "characters": "\u226F" },
+ "&ngtr;": { "codepoints": [8815], "characters": "\u226F" },
+ "&nhArr;": { "codepoints": [8654], "characters": "\u21CE" },
+ "&nharr;": { "codepoints": [8622], "characters": "\u21AE" },
+ "&nhpar;": { "codepoints": [10994], "characters": "\u2AF2" },
+ "&ni;": { "codepoints": [8715], "characters": "\u220B" },
+ "&nis;": { "codepoints": [8956], "characters": "\u22FC" },
+ "&nisd;": { "codepoints": [8954], "characters": "\u22FA" },
+ "&niv;": { "codepoints": [8715], "characters": "\u220B" },
+ "&njcy;": { "codepoints": [1114], "characters": "\u045A" },
+ "&nlArr;": { "codepoints": [8653], "characters": "\u21CD" },
+ "&nlE;": { "codepoints": [8806, 824], "characters": "\u2266\u0338" },
+ "&nlarr;": { "codepoints": [8602], "characters": "\u219A" },
+ "&nldr;": { "codepoints": [8229], "characters": "\u2025" },
+ "&nle;": { "codepoints": [8816], "characters": "\u2270" },
+ "&nleftarrow;": { "codepoints": [8602], "characters": "\u219A" },
+ "&nleftrightarrow;": { "codepoints": [8622], "characters": "\u21AE" },
+ "&nleq;": { "codepoints": [8816], "characters": "\u2270" },
+ "&nleqq;": { "codepoints": [8806, 824], "characters": "\u2266\u0338" },
+ "&nleqslant;": { "codepoints": [10877, 824], "characters": "\u2A7D\u0338" },
+ "&nles;": { "codepoints": [10877, 824], "characters": "\u2A7D\u0338" },
+ "&nless;": { "codepoints": [8814], "characters": "\u226E" },
+ "&nlsim;": { "codepoints": [8820], "characters": "\u2274" },
+ "&nlt;": { "codepoints": [8814], "characters": "\u226E" },
+ "&nltri;": { "codepoints": [8938], "characters": "\u22EA" },
+ "&nltrie;": { "codepoints": [8940], "characters": "\u22EC" },
+ "&nmid;": { "codepoints": [8740], "characters": "\u2224" },
+ "&nopf;": { "codepoints": [120159], "characters": "\uD835\uDD5F" },
+ "&not": { "codepoints": [172], "characters": "\u00AC" },
+ "&not;": { "codepoints": [172], "characters": "\u00AC" },
+ "&notin;": { "codepoints": [8713], "characters": "\u2209" },
+ "&notinE;": { "codepoints": [8953, 824], "characters": "\u22F9\u0338" },
+ "&notindot;": { "codepoints": [8949, 824], "characters": "\u22F5\u0338" },
+ "&notinva;": { "codepoints": [8713], "characters": "\u2209" },
+ "&notinvb;": { "codepoints": [8951], "characters": "\u22F7" },
+ "&notinvc;": { "codepoints": [8950], "characters": "\u22F6" },
+ "&notni;": { "codepoints": [8716], "characters": "\u220C" },
+ "&notniva;": { "codepoints": [8716], "characters": "\u220C" },
+ "&notnivb;": { "codepoints": [8958], "characters": "\u22FE" },
+ "&notnivc;": { "codepoints": [8957], "characters": "\u22FD" },
+ "&npar;": { "codepoints": [8742], "characters": "\u2226" },
+ "&nparallel;": { "codepoints": [8742], "characters": "\u2226" },
+ "&nparsl;": { "codepoints": [11005, 8421], "characters": "\u2AFD\u20E5" },
+ "&npart;": { "codepoints": [8706, 824], "characters": "\u2202\u0338" },
+ "&npolint;": { "codepoints": [10772], "characters": "\u2A14" },
+ "&npr;": { "codepoints": [8832], "characters": "\u2280" },
+ "&nprcue;": { "codepoints": [8928], "characters": "\u22E0" },
+ "&npre;": { "codepoints": [10927, 824], "characters": "\u2AAF\u0338" },
+ "&nprec;": { "codepoints": [8832], "characters": "\u2280" },
+ "&npreceq;": { "codepoints": [10927, 824], "characters": "\u2AAF\u0338" },
+ "&nrArr;": { "codepoints": [8655], "characters": "\u21CF" },
+ "&nrarr;": { "codepoints": [8603], "characters": "\u219B" },
+ "&nrarrc;": { "codepoints": [10547, 824], "characters": "\u2933\u0338" },
+ "&nrarrw;": { "codepoints": [8605, 824], "characters": "\u219D\u0338" },
+ "&nrightarrow;": { "codepoints": [8603], "characters": "\u219B" },
+ "&nrtri;": { "codepoints": [8939], "characters": "\u22EB" },
+ "&nrtrie;": { "codepoints": [8941], "characters": "\u22ED" },
+ "&nsc;": { "codepoints": [8833], "characters": "\u2281" },
+ "&nsccue;": { "codepoints": [8929], "characters": "\u22E1" },
+ "&nsce;": { "codepoints": [10928, 824], "characters": "\u2AB0\u0338" },
+ "&nscr;": { "codepoints": [120003], "characters": "\uD835\uDCC3" },
+ "&nshortmid;": { "codepoints": [8740], "characters": "\u2224" },
+ "&nshortparallel;": { "codepoints": [8742], "characters": "\u2226" },
+ "&nsim;": { "codepoints": [8769], "characters": "\u2241" },
+ "&nsime;": { "codepoints": [8772], "characters": "\u2244" },
+ "&nsimeq;": { "codepoints": [8772], "characters": "\u2244" },
+ "&nsmid;": { "codepoints": [8740], "characters": "\u2224" },
+ "&nspar;": { "codepoints": [8742], "characters": "\u2226" },
+ "&nsqsube;": { "codepoints": [8930], "characters": "\u22E2" },
+ "&nsqsupe;": { "codepoints": [8931], "characters": "\u22E3" },
+ "&nsub;": { "codepoints": [8836], "characters": "\u2284" },
+ "&nsubE;": { "codepoints": [10949, 824], "characters": "\u2AC5\u0338" },
+ "&nsube;": { "codepoints": [8840], "characters": "\u2288" },
+ "&nsubset;": { "codepoints": [8834, 8402], "characters": "\u2282\u20D2" },
+ "&nsubseteq;": { "codepoints": [8840], "characters": "\u2288" },
+ "&nsubseteqq;": { "codepoints": [10949, 824], "characters": "\u2AC5\u0338" },
+ "&nsucc;": { "codepoints": [8833], "characters": "\u2281" },
+ "&nsucceq;": { "codepoints": [10928, 824], "characters": "\u2AB0\u0338" },
+ "&nsup;": { "codepoints": [8837], "characters": "\u2285" },
+ "&nsupE;": { "codepoints": [10950, 824], "characters": "\u2AC6\u0338" },
+ "&nsupe;": { "codepoints": [8841], "characters": "\u2289" },
+ "&nsupset;": { "codepoints": [8835, 8402], "characters": "\u2283\u20D2" },
+ "&nsupseteq;": { "codepoints": [8841], "characters": "\u2289" },
+ "&nsupseteqq;": { "codepoints": [10950, 824], "characters": "\u2AC6\u0338" },
+ "&ntgl;": { "codepoints": [8825], "characters": "\u2279" },
+ "&ntilde": { "codepoints": [241], "characters": "\u00F1" },
+ "&ntilde;": { "codepoints": [241], "characters": "\u00F1" },
+ "&ntlg;": { "codepoints": [8824], "characters": "\u2278" },
+ "&ntriangleleft;": { "codepoints": [8938], "characters": "\u22EA" },
+ "&ntrianglelefteq;": { "codepoints": [8940], "characters": "\u22EC" },
+ "&ntriangleright;": { "codepoints": [8939], "characters": "\u22EB" },
+ "&ntrianglerighteq;": { "codepoints": [8941], "characters": "\u22ED" },
+ "&nu;": { "codepoints": [957], "characters": "\u03BD" },
+ "&num;": { "codepoints": [35], "characters": "\u0023" },
+ "&numero;": { "codepoints": [8470], "characters": "\u2116" },
+ "&numsp;": { "codepoints": [8199], "characters": "\u2007" },
+ "&nvDash;": { "codepoints": [8877], "characters": "\u22AD" },
+ "&nvHarr;": { "codepoints": [10500], "characters": "\u2904" },
+ "&nvap;": { "codepoints": [8781, 8402], "characters": "\u224D\u20D2" },
+ "&nvdash;": { "codepoints": [8876], "characters": "\u22AC" },
+ "&nvge;": { "codepoints": [8805, 8402], "characters": "\u2265\u20D2" },
+ "&nvgt;": { "codepoints": [62, 8402], "characters": "\u003E\u20D2" },
+ "&nvinfin;": { "codepoints": [10718], "characters": "\u29DE" },
+ "&nvlArr;": { "codepoints": [10498], "characters": "\u2902" },
+ "&nvle;": { "codepoints": [8804, 8402], "characters": "\u2264\u20D2" },
+ "&nvlt;": { "codepoints": [60, 8402], "characters": "\u003C\u20D2" },
+ "&nvltrie;": { "codepoints": [8884, 8402], "characters": "\u22B4\u20D2" },
+ "&nvrArr;": { "codepoints": [10499], "characters": "\u2903" },
+ "&nvrtrie;": { "codepoints": [8885, 8402], "characters": "\u22B5\u20D2" },
+ "&nvsim;": { "codepoints": [8764, 8402], "characters": "\u223C\u20D2" },
+ "&nwArr;": { "codepoints": [8662], "characters": "\u21D6" },
+ "&nwarhk;": { "codepoints": [10531], "characters": "\u2923" },
+ "&nwarr;": { "codepoints": [8598], "characters": "\u2196" },
+ "&nwarrow;": { "codepoints": [8598], "characters": "\u2196" },
+ "&nwnear;": { "codepoints": [10535], "characters": "\u2927" },
+ "&oS;": { "codepoints": [9416], "characters": "\u24C8" },
+ "&oacute": { "codepoints": [243], "characters": "\u00F3" },
+ "&oacute;": { "codepoints": [243], "characters": "\u00F3" },
+ "&oast;": { "codepoints": [8859], "characters": "\u229B" },
+ "&ocir;": { "codepoints": [8858], "characters": "\u229A" },
+ "&ocirc": { "codepoints": [244], "characters": "\u00F4" },
+ "&ocirc;": { "codepoints": [244], "characters": "\u00F4" },
+ "&ocy;": { "codepoints": [1086], "characters": "\u043E" },
+ "&odash;": { "codepoints": [8861], "characters": "\u229D" },
+ "&odblac;": { "codepoints": [337], "characters": "\u0151" },
+ "&odiv;": { "codepoints": [10808], "characters": "\u2A38" },
+ "&odot;": { "codepoints": [8857], "characters": "\u2299" },
+ "&odsold;": { "codepoints": [10684], "characters": "\u29BC" },
+ "&oelig;": { "codepoints": [339], "characters": "\u0153" },
+ "&ofcir;": { "codepoints": [10687], "characters": "\u29BF" },
+ "&ofr;": { "codepoints": [120108], "characters": "\uD835\uDD2C" },
+ "&ogon;": { "codepoints": [731], "characters": "\u02DB" },
+ "&ograve": { "codepoints": [242], "characters": "\u00F2" },
+ "&ograve;": { "codepoints": [242], "characters": "\u00F2" },
+ "&ogt;": { "codepoints": [10689], "characters": "\u29C1" },
+ "&ohbar;": { "codepoints": [10677], "characters": "\u29B5" },
+ "&ohm;": { "codepoints": [937], "characters": "\u03A9" },
+ "&oint;": { "codepoints": [8750], "characters": "\u222E" },
+ "&olarr;": { "codepoints": [8634], "characters": "\u21BA" },
+ "&olcir;": { "codepoints": [10686], "characters": "\u29BE" },
+ "&olcross;": { "codepoints": [10683], "characters": "\u29BB" },
+ "&oline;": { "codepoints": [8254], "characters": "\u203E" },
+ "&olt;": { "codepoints": [10688], "characters": "\u29C0" },
+ "&omacr;": { "codepoints": [333], "characters": "\u014D" },
+ "&omega;": { "codepoints": [969], "characters": "\u03C9" },
+ "&omicron;": { "codepoints": [959], "characters": "\u03BF" },
+ "&omid;": { "codepoints": [10678], "characters": "\u29B6" },
+ "&ominus;": { "codepoints": [8854], "characters": "\u2296" },
+ "&oopf;": { "codepoints": [120160], "characters": "\uD835\uDD60" },
+ "&opar;": { "codepoints": [10679], "characters": "\u29B7" },
+ "&operp;": { "codepoints": [10681], "characters": "\u29B9" },
+ "&oplus;": { "codepoints": [8853], "characters": "\u2295" },
+ "&or;": { "codepoints": [8744], "characters": "\u2228" },
+ "&orarr;": { "codepoints": [8635], "characters": "\u21BB" },
+ "&ord;": { "codepoints": [10845], "characters": "\u2A5D" },
+ "&order;": { "codepoints": [8500], "characters": "\u2134" },
+ "&orderof;": { "codepoints": [8500], "characters": "\u2134" },
+ "&ordf": { "codepoints": [170], "characters": "\u00AA" },
+ "&ordf;": { "codepoints": [170], "characters": "\u00AA" },
+ "&ordm": { "codepoints": [186], "characters": "\u00BA" },
+ "&ordm;": { "codepoints": [186], "characters": "\u00BA" },
+ "&origof;": { "codepoints": [8886], "characters": "\u22B6" },
+ "&oror;": { "codepoints": [10838], "characters": "\u2A56" },
+ "&orslope;": { "codepoints": [10839], "characters": "\u2A57" },
+ "&orv;": { "codepoints": [10843], "characters": "\u2A5B" },
+ "&oscr;": { "codepoints": [8500], "characters": "\u2134" },
+ "&oslash": { "codepoints": [248], "characters": "\u00F8" },
+ "&oslash;": { "codepoints": [248], "characters": "\u00F8" },
+ "&osol;": { "codepoints": [8856], "characters": "\u2298" },
+ "&otilde": { "codepoints": [245], "characters": "\u00F5" },
+ "&otilde;": { "codepoints": [245], "characters": "\u00F5" },
+ "&otimes;": { "codepoints": [8855], "characters": "\u2297" },
+ "&otimesas;": { "codepoints": [10806], "characters": "\u2A36" },
+ "&ouml": { "codepoints": [246], "characters": "\u00F6" },
+ "&ouml;": { "codepoints": [246], "characters": "\u00F6" },
+ "&ovbar;": { "codepoints": [9021], "characters": "\u233D" },
+ "&par;": { "codepoints": [8741], "characters": "\u2225" },
+ "&para": { "codepoints": [182], "characters": "\u00B6" },
+ "&para;": { "codepoints": [182], "characters": "\u00B6" },
+ "&parallel;": { "codepoints": [8741], "characters": "\u2225" },
+ "&parsim;": { "codepoints": [10995], "characters": "\u2AF3" },
+ "&parsl;": { "codepoints": [11005], "characters": "\u2AFD" },
+ "&part;": { "codepoints": [8706], "characters": "\u2202" },
+ "&pcy;": { "codepoints": [1087], "characters": "\u043F" },
+ "&percnt;": { "codepoints": [37], "characters": "\u0025" },
+ "&period;": { "codepoints": [46], "characters": "\u002E" },
+ "&permil;": { "codepoints": [8240], "characters": "\u2030" },
+ "&perp;": { "codepoints": [8869], "characters": "\u22A5" },
+ "&pertenk;": { "codepoints": [8241], "characters": "\u2031" },
+ "&pfr;": { "codepoints": [120109], "characters": "\uD835\uDD2D" },
+ "&phi;": { "codepoints": [966], "characters": "\u03C6" },
+ "&phiv;": { "codepoints": [981], "characters": "\u03D5" },
+ "&phmmat;": { "codepoints": [8499], "characters": "\u2133" },
+ "&phone;": { "codepoints": [9742], "characters": "\u260E" },
+ "&pi;": { "codepoints": [960], "characters": "\u03C0" },
+ "&pitchfork;": { "codepoints": [8916], "characters": "\u22D4" },
+ "&piv;": { "codepoints": [982], "characters": "\u03D6" },
+ "&planck;": { "codepoints": [8463], "characters": "\u210F" },
+ "&planckh;": { "codepoints": [8462], "characters": "\u210E" },
+ "&plankv;": { "codepoints": [8463], "characters": "\u210F" },
+ "&plus;": { "codepoints": [43], "characters": "\u002B" },
+ "&plusacir;": { "codepoints": [10787], "characters": "\u2A23" },
+ "&plusb;": { "codepoints": [8862], "characters": "\u229E" },
+ "&pluscir;": { "codepoints": [10786], "characters": "\u2A22" },
+ "&plusdo;": { "codepoints": [8724], "characters": "\u2214" },
+ "&plusdu;": { "codepoints": [10789], "characters": "\u2A25" },
+ "&pluse;": { "codepoints": [10866], "characters": "\u2A72" },
+ "&plusmn": { "codepoints": [177], "characters": "\u00B1" },
+ "&plusmn;": { "codepoints": [177], "characters": "\u00B1" },
+ "&plussim;": { "codepoints": [10790], "characters": "\u2A26" },
+ "&plustwo;": { "codepoints": [10791], "characters": "\u2A27" },
+ "&pm;": { "codepoints": [177], "characters": "\u00B1" },
+ "&pointint;": { "codepoints": [10773], "characters": "\u2A15" },
+ "&popf;": { "codepoints": [120161], "characters": "\uD835\uDD61" },
+ "&pound": { "codepoints": [163], "characters": "\u00A3" },
+ "&pound;": { "codepoints": [163], "characters": "\u00A3" },
+ "&pr;": { "codepoints": [8826], "characters": "\u227A" },
+ "&prE;": { "codepoints": [10931], "characters": "\u2AB3" },
+ "&prap;": { "codepoints": [10935], "characters": "\u2AB7" },
+ "&prcue;": { "codepoints": [8828], "characters": "\u227C" },
+ "&pre;": { "codepoints": [10927], "characters": "\u2AAF" },
+ "&prec;": { "codepoints": [8826], "characters": "\u227A" },
+ "&precapprox;": { "codepoints": [10935], "characters": "\u2AB7" },
+ "&preccurlyeq;": { "codepoints": [8828], "characters": "\u227C" },
+ "&preceq;": { "codepoints": [10927], "characters": "\u2AAF" },
+ "&precnapprox;": { "codepoints": [10937], "characters": "\u2AB9" },
+ "&precneqq;": { "codepoints": [10933], "characters": "\u2AB5" },
+ "&precnsim;": { "codepoints": [8936], "characters": "\u22E8" },
+ "&precsim;": { "codepoints": [8830], "characters": "\u227E" },
+ "&prime;": { "codepoints": [8242], "characters": "\u2032" },
+ "&primes;": { "codepoints": [8473], "characters": "\u2119" },
+ "&prnE;": { "codepoints": [10933], "characters": "\u2AB5" },
+ "&prnap;": { "codepoints": [10937], "characters": "\u2AB9" },
+ "&prnsim;": { "codepoints": [8936], "characters": "\u22E8" },
+ "&prod;": { "codepoints": [8719], "characters": "\u220F" },
+ "&profalar;": { "codepoints": [9006], "characters": "\u232E" },
+ "&profline;": { "codepoints": [8978], "characters": "\u2312" },
+ "&profsurf;": { "codepoints": [8979], "characters": "\u2313" },
+ "&prop;": { "codepoints": [8733], "characters": "\u221D" },
+ "&propto;": { "codepoints": [8733], "characters": "\u221D" },
+ "&prsim;": { "codepoints": [8830], "characters": "\u227E" },
+ "&prurel;": { "codepoints": [8880], "characters": "\u22B0" },
+ "&pscr;": { "codepoints": [120005], "characters": "\uD835\uDCC5" },
+ "&psi;": { "codepoints": [968], "characters": "\u03C8" },
+ "&puncsp;": { "codepoints": [8200], "characters": "\u2008" },
+ "&qfr;": { "codepoints": [120110], "characters": "\uD835\uDD2E" },
+ "&qint;": { "codepoints": [10764], "characters": "\u2A0C" },
+ "&qopf;": { "codepoints": [120162], "characters": "\uD835\uDD62" },
+ "&qprime;": { "codepoints": [8279], "characters": "\u2057" },
+ "&qscr;": { "codepoints": [120006], "characters": "\uD835\uDCC6" },
+ "&quaternions;": { "codepoints": [8461], "characters": "\u210D" },
+ "&quatint;": { "codepoints": [10774], "characters": "\u2A16" },
+ "&quest;": { "codepoints": [63], "characters": "\u003F" },
+ "&questeq;": { "codepoints": [8799], "characters": "\u225F" },
+ "&quot": { "codepoints": [34], "characters": "\u0022" },
+ "&quot;": { "codepoints": [34], "characters": "\u0022" },
+ "&rAarr;": { "codepoints": [8667], "characters": "\u21DB" },
+ "&rArr;": { "codepoints": [8658], "characters": "\u21D2" },
+ "&rAtail;": { "codepoints": [10524], "characters": "\u291C" },
+ "&rBarr;": { "codepoints": [10511], "characters": "\u290F" },
+ "&rHar;": { "codepoints": [10596], "characters": "\u2964" },
+ "&race;": { "codepoints": [8765, 817], "characters": "\u223D\u0331" },
+ "&racute;": { "codepoints": [341], "characters": "\u0155" },
+ "&radic;": { "codepoints": [8730], "characters": "\u221A" },
+ "&raemptyv;": { "codepoints": [10675], "characters": "\u29B3" },
+ "&rang;": { "codepoints": [10217], "characters": "\u27E9" },
+ "&rangd;": { "codepoints": [10642], "characters": "\u2992" },
+ "&range;": { "codepoints": [10661], "characters": "\u29A5" },
+ "&rangle;": { "codepoints": [10217], "characters": "\u27E9" },
+ "&raquo": { "codepoints": [187], "characters": "\u00BB" },
+ "&raquo;": { "codepoints": [187], "characters": "\u00BB" },
+ "&rarr;": { "codepoints": [8594], "characters": "\u2192" },
+ "&rarrap;": { "codepoints": [10613], "characters": "\u2975" },
+ "&rarrb;": { "codepoints": [8677], "characters": "\u21E5" },
+ "&rarrbfs;": { "codepoints": [10528], "characters": "\u2920" },
+ "&rarrc;": { "codepoints": [10547], "characters": "\u2933" },
+ "&rarrfs;": { "codepoints": [10526], "characters": "\u291E" },
+ "&rarrhk;": { "codepoints": [8618], "characters": "\u21AA" },
+ "&rarrlp;": { "codepoints": [8620], "characters": "\u21AC" },
+ "&rarrpl;": { "codepoints": [10565], "characters": "\u2945" },
+ "&rarrsim;": { "codepoints": [10612], "characters": "\u2974" },
+ "&rarrtl;": { "codepoints": [8611], "characters": "\u21A3" },
+ "&rarrw;": { "codepoints": [8605], "characters": "\u219D" },
+ "&ratail;": { "codepoints": [10522], "characters": "\u291A" },
+ "&ratio;": { "codepoints": [8758], "characters": "\u2236" },
+ "&rationals;": { "codepoints": [8474], "characters": "\u211A" },
+ "&rbarr;": { "codepoints": [10509], "characters": "\u290D" },
+ "&rbbrk;": { "codepoints": [10099], "characters": "\u2773" },
+ "&rbrace;": { "codepoints": [125], "characters": "\u007D" },
+ "&rbrack;": { "codepoints": [93], "characters": "\u005D" },
+ "&rbrke;": { "codepoints": [10636], "characters": "\u298C" },
+ "&rbrksld;": { "codepoints": [10638], "characters": "\u298E" },
+ "&rbrkslu;": { "codepoints": [10640], "characters": "\u2990" },
+ "&rcaron;": { "codepoints": [345], "characters": "\u0159" },
+ "&rcedil;": { "codepoints": [343], "characters": "\u0157" },
+ "&rceil;": { "codepoints": [8969], "characters": "\u2309" },
+ "&rcub;": { "codepoints": [125], "characters": "\u007D" },
+ "&rcy;": { "codepoints": [1088], "characters": "\u0440" },
+ "&rdca;": { "codepoints": [10551], "characters": "\u2937" },
+ "&rdldhar;": { "codepoints": [10601], "characters": "\u2969" },
+ "&rdquo;": { "codepoints": [8221], "characters": "\u201D" },
+ "&rdquor;": { "codepoints": [8221], "characters": "\u201D" },
+ "&rdsh;": { "codepoints": [8627], "characters": "\u21B3" },
+ "&real;": { "codepoints": [8476], "characters": "\u211C" },
+ "&realine;": { "codepoints": [8475], "characters": "\u211B" },
+ "&realpart;": { "codepoints": [8476], "characters": "\u211C" },
+ "&reals;": { "codepoints": [8477], "characters": "\u211D" },
+ "&rect;": { "codepoints": [9645], "characters": "\u25AD" },
+ "&reg": { "codepoints": [174], "characters": "\u00AE" },
+ "&reg;": { "codepoints": [174], "characters": "\u00AE" },
+ "&rfisht;": { "codepoints": [10621], "characters": "\u297D" },
+ "&rfloor;": { "codepoints": [8971], "characters": "\u230B" },
+ "&rfr;": { "codepoints": [120111], "characters": "\uD835\uDD2F" },
+ "&rhard;": { "codepoints": [8641], "characters": "\u21C1" },
+ "&rharu;": { "codepoints": [8640], "characters": "\u21C0" },
+ "&rharul;": { "codepoints": [10604], "characters": "\u296C" },
+ "&rho;": { "codepoints": [961], "characters": "\u03C1" },
+ "&rhov;": { "codepoints": [1009], "characters": "\u03F1" },
+ "&rightarrow;": { "codepoints": [8594], "characters": "\u2192" },
+ "&rightarrowtail;": { "codepoints": [8611], "characters": "\u21A3" },
+ "&rightharpoondown;": { "codepoints": [8641], "characters": "\u21C1" },
+ "&rightharpoonup;": { "codepoints": [8640], "characters": "\u21C0" },
+ "&rightleftarrows;": { "codepoints": [8644], "characters": "\u21C4" },
+ "&rightleftharpoons;": { "codepoints": [8652], "characters": "\u21CC" },
+ "&rightrightarrows;": { "codepoints": [8649], "characters": "\u21C9" },
+ "&rightsquigarrow;": { "codepoints": [8605], "characters": "\u219D" },
+ "&rightthreetimes;": { "codepoints": [8908], "characters": "\u22CC" },
+ "&ring;": { "codepoints": [730], "characters": "\u02DA" },
+ "&risingdotseq;": { "codepoints": [8787], "characters": "\u2253" },
+ "&rlarr;": { "codepoints": [8644], "characters": "\u21C4" },
+ "&rlhar;": { "codepoints": [8652], "characters": "\u21CC" },
+ "&rlm;": { "codepoints": [8207], "characters": "\u200F" },
+ "&rmoust;": { "codepoints": [9137], "characters": "\u23B1" },
+ "&rmoustache;": { "codepoints": [9137], "characters": "\u23B1" },
+ "&rnmid;": { "codepoints": [10990], "characters": "\u2AEE" },
+ "&roang;": { "codepoints": [10221], "characters": "\u27ED" },
+ "&roarr;": { "codepoints": [8702], "characters": "\u21FE" },
+ "&robrk;": { "codepoints": [10215], "characters": "\u27E7" },
+ "&ropar;": { "codepoints": [10630], "characters": "\u2986" },
+ "&ropf;": { "codepoints": [120163], "characters": "\uD835\uDD63" },
+ "&roplus;": { "codepoints": [10798], "characters": "\u2A2E" },
+ "&rotimes;": { "codepoints": [10805], "characters": "\u2A35" },
+ "&rpar;": { "codepoints": [41], "characters": "\u0029" },
+ "&rpargt;": { "codepoints": [10644], "characters": "\u2994" },
+ "&rppolint;": { "codepoints": [10770], "characters": "\u2A12" },
+ "&rrarr;": { "codepoints": [8649], "characters": "\u21C9" },
+ "&rsaquo;": { "codepoints": [8250], "characters": "\u203A" },
+ "&rscr;": { "codepoints": [120007], "characters": "\uD835\uDCC7" },
+ "&rsh;": { "codepoints": [8625], "characters": "\u21B1" },
+ "&rsqb;": { "codepoints": [93], "characters": "\u005D" },
+ "&rsquo;": { "codepoints": [8217], "characters": "\u2019" },
+ "&rsquor;": { "codepoints": [8217], "characters": "\u2019" },
+ "&rthree;": { "codepoints": [8908], "characters": "\u22CC" },
+ "&rtimes;": { "codepoints": [8906], "characters": "\u22CA" },
+ "&rtri;": { "codepoints": [9657], "characters": "\u25B9" },
+ "&rtrie;": { "codepoints": [8885], "characters": "\u22B5" },
+ "&rtrif;": { "codepoints": [9656], "characters": "\u25B8" },
+ "&rtriltri;": { "codepoints": [10702], "characters": "\u29CE" },
+ "&ruluhar;": { "codepoints": [10600], "characters": "\u2968" },
+ "&rx;": { "codepoints": [8478], "characters": "\u211E" },
+ "&sacute;": { "codepoints": [347], "characters": "\u015B" },
+ "&sbquo;": { "codepoints": [8218], "characters": "\u201A" },
+ "&sc;": { "codepoints": [8827], "characters": "\u227B" },
+ "&scE;": { "codepoints": [10932], "characters": "\u2AB4" },
+ "&scap;": { "codepoints": [10936], "characters": "\u2AB8" },
+ "&scaron;": { "codepoints": [353], "characters": "\u0161" },
+ "&sccue;": { "codepoints": [8829], "characters": "\u227D" },
+ "&sce;": { "codepoints": [10928], "characters": "\u2AB0" },
+ "&scedil;": { "codepoints": [351], "characters": "\u015F" },
+ "&scirc;": { "codepoints": [349], "characters": "\u015D" },
+ "&scnE;": { "codepoints": [10934], "characters": "\u2AB6" },
+ "&scnap;": { "codepoints": [10938], "characters": "\u2ABA" },
+ "&scnsim;": { "codepoints": [8937], "characters": "\u22E9" },
+ "&scpolint;": { "codepoints": [10771], "characters": "\u2A13" },
+ "&scsim;": { "codepoints": [8831], "characters": "\u227F" },
+ "&scy;": { "codepoints": [1089], "characters": "\u0441" },
+ "&sdot;": { "codepoints": [8901], "characters": "\u22C5" },
+ "&sdotb;": { "codepoints": [8865], "characters": "\u22A1" },
+ "&sdote;": { "codepoints": [10854], "characters": "\u2A66" },
+ "&seArr;": { "codepoints": [8664], "characters": "\u21D8" },
+ "&searhk;": { "codepoints": [10533], "characters": "\u2925" },
+ "&searr;": { "codepoints": [8600], "characters": "\u2198" },
+ "&searrow;": { "codepoints": [8600], "characters": "\u2198" },
+ "&sect": { "codepoints": [167], "characters": "\u00A7" },
+ "&sect;": { "codepoints": [167], "characters": "\u00A7" },
+ "&semi;": { "codepoints": [59], "characters": "\u003B" },
+ "&seswar;": { "codepoints": [10537], "characters": "\u2929" },
+ "&setminus;": { "codepoints": [8726], "characters": "\u2216" },
+ "&setmn;": { "codepoints": [8726], "characters": "\u2216" },
+ "&sext;": { "codepoints": [10038], "characters": "\u2736" },
+ "&sfr;": { "codepoints": [120112], "characters": "\uD835\uDD30" },
+ "&sfrown;": { "codepoints": [8994], "characters": "\u2322" },
+ "&sharp;": { "codepoints": [9839], "characters": "\u266F" },
+ "&shchcy;": { "codepoints": [1097], "characters": "\u0449" },
+ "&shcy;": { "codepoints": [1096], "characters": "\u0448" },
+ "&shortmid;": { "codepoints": [8739], "characters": "\u2223" },
+ "&shortparallel;": { "codepoints": [8741], "characters": "\u2225" },
+ "&shy": { "codepoints": [173], "characters": "\u00AD" },
+ "&shy;": { "codepoints": [173], "characters": "\u00AD" },
+ "&sigma;": { "codepoints": [963], "characters": "\u03C3" },
+ "&sigmaf;": { "codepoints": [962], "characters": "\u03C2" },
+ "&sigmav;": { "codepoints": [962], "characters": "\u03C2" },
+ "&sim;": { "codepoints": [8764], "characters": "\u223C" },
+ "&simdot;": { "codepoints": [10858], "characters": "\u2A6A" },
+ "&sime;": { "codepoints": [8771], "characters": "\u2243" },
+ "&simeq;": { "codepoints": [8771], "characters": "\u2243" },
+ "&simg;": { "codepoints": [10910], "characters": "\u2A9E" },
+ "&simgE;": { "codepoints": [10912], "characters": "\u2AA0" },
+ "&siml;": { "codepoints": [10909], "characters": "\u2A9D" },
+ "&simlE;": { "codepoints": [10911], "characters": "\u2A9F" },
+ "&simne;": { "codepoints": [8774], "characters": "\u2246" },
+ "&simplus;": { "codepoints": [10788], "characters": "\u2A24" },
+ "&simrarr;": { "codepoints": [10610], "characters": "\u2972" },
+ "&slarr;": { "codepoints": [8592], "characters": "\u2190" },
+ "&smallsetminus;": { "codepoints": [8726], "characters": "\u2216" },
+ "&smashp;": { "codepoints": [10803], "characters": "\u2A33" },
+ "&smeparsl;": { "codepoints": [10724], "characters": "\u29E4" },
+ "&smid;": { "codepoints": [8739], "characters": "\u2223" },
+ "&smile;": { "codepoints": [8995], "characters": "\u2323" },
+ "&smt;": { "codepoints": [10922], "characters": "\u2AAA" },
+ "&smte;": { "codepoints": [10924], "characters": "\u2AAC" },
+ "&smtes;": { "codepoints": [10924, 65024], "characters": "\u2AAC\uFE00" },
+ "&softcy;": { "codepoints": [1100], "characters": "\u044C" },
+ "&sol;": { "codepoints": [47], "characters": "\u002F" },
+ "&solb;": { "codepoints": [10692], "characters": "\u29C4" },
+ "&solbar;": { "codepoints": [9023], "characters": "\u233F" },
+ "&sopf;": { "codepoints": [120164], "characters": "\uD835\uDD64" },
+ "&spades;": { "codepoints": [9824], "characters": "\u2660" },
+ "&spadesuit;": { "codepoints": [9824], "characters": "\u2660" },
+ "&spar;": { "codepoints": [8741], "characters": "\u2225" },
+ "&sqcap;": { "codepoints": [8851], "characters": "\u2293" },
+ "&sqcaps;": { "codepoints": [8851, 65024], "characters": "\u2293\uFE00" },
+ "&sqcup;": { "codepoints": [8852], "characters": "\u2294" },
+ "&sqcups;": { "codepoints": [8852, 65024], "characters": "\u2294\uFE00" },
+ "&sqsub;": { "codepoints": [8847], "characters": "\u228F" },
+ "&sqsube;": { "codepoints": [8849], "characters": "\u2291" },
+ "&sqsubset;": { "codepoints": [8847], "characters": "\u228F" },
+ "&sqsubseteq;": { "codepoints": [8849], "characters": "\u2291" },
+ "&sqsup;": { "codepoints": [8848], "characters": "\u2290" },
+ "&sqsupe;": { "codepoints": [8850], "characters": "\u2292" },
+ "&sqsupset;": { "codepoints": [8848], "characters": "\u2290" },
+ "&sqsupseteq;": { "codepoints": [8850], "characters": "\u2292" },
+ "&squ;": { "codepoints": [9633], "characters": "\u25A1" },
+ "&square;": { "codepoints": [9633], "characters": "\u25A1" },
+ "&squarf;": { "codepoints": [9642], "characters": "\u25AA" },
+ "&squf;": { "codepoints": [9642], "characters": "\u25AA" },
+ "&srarr;": { "codepoints": [8594], "characters": "\u2192" },
+ "&sscr;": { "codepoints": [120008], "characters": "\uD835\uDCC8" },
+ "&ssetmn;": { "codepoints": [8726], "characters": "\u2216" },
+ "&ssmile;": { "codepoints": [8995], "characters": "\u2323" },
+ "&sstarf;": { "codepoints": [8902], "characters": "\u22C6" },
+ "&star;": { "codepoints": [9734], "characters": "\u2606" },
+ "&starf;": { "codepoints": [9733], "characters": "\u2605" },
+ "&straightepsilon;": { "codepoints": [1013], "characters": "\u03F5" },
+ "&straightphi;": { "codepoints": [981], "characters": "\u03D5" },
+ "&strns;": { "codepoints": [175], "characters": "\u00AF" },
+ "&sub;": { "codepoints": [8834], "characters": "\u2282" },
+ "&subE;": { "codepoints": [10949], "characters": "\u2AC5" },
+ "&subdot;": { "codepoints": [10941], "characters": "\u2ABD" },
+ "&sube;": { "codepoints": [8838], "characters": "\u2286" },
+ "&subedot;": { "codepoints": [10947], "characters": "\u2AC3" },
+ "&submult;": { "codepoints": [10945], "characters": "\u2AC1" },
+ "&subnE;": { "codepoints": [10955], "characters": "\u2ACB" },
+ "&subne;": { "codepoints": [8842], "characters": "\u228A" },
+ "&subplus;": { "codepoints": [10943], "characters": "\u2ABF" },
+ "&subrarr;": { "codepoints": [10617], "characters": "\u2979" },
+ "&subset;": { "codepoints": [8834], "characters": "\u2282" },
+ "&subseteq;": { "codepoints": [8838], "characters": "\u2286" },
+ "&subseteqq;": { "codepoints": [10949], "characters": "\u2AC5" },
+ "&subsetneq;": { "codepoints": [8842], "characters": "\u228A" },
+ "&subsetneqq;": { "codepoints": [10955], "characters": "\u2ACB" },
+ "&subsim;": { "codepoints": [10951], "characters": "\u2AC7" },
+ "&subsub;": { "codepoints": [10965], "characters": "\u2AD5" },
+ "&subsup;": { "codepoints": [10963], "characters": "\u2AD3" },
+ "&succ;": { "codepoints": [8827], "characters": "\u227B" },
+ "&succapprox;": { "codepoints": [10936], "characters": "\u2AB8" },
+ "&succcurlyeq;": { "codepoints": [8829], "characters": "\u227D" },
+ "&succeq;": { "codepoints": [10928], "characters": "\u2AB0" },
+ "&succnapprox;": { "codepoints": [10938], "characters": "\u2ABA" },
+ "&succneqq;": { "codepoints": [10934], "characters": "\u2AB6" },
+ "&succnsim;": { "codepoints": [8937], "characters": "\u22E9" },
+ "&succsim;": { "codepoints": [8831], "characters": "\u227F" },
+ "&sum;": { "codepoints": [8721], "characters": "\u2211" },
+ "&sung;": { "codepoints": [9834], "characters": "\u266A" },
+ "&sup1": { "codepoints": [185], "characters": "\u00B9" },
+ "&sup1;": { "codepoints": [185], "characters": "\u00B9" },
+ "&sup2": { "codepoints": [178], "characters": "\u00B2" },
+ "&sup2;": { "codepoints": [178], "characters": "\u00B2" },
+ "&sup3": { "codepoints": [179], "characters": "\u00B3" },
+ "&sup3;": { "codepoints": [179], "characters": "\u00B3" },
+ "&sup;": { "codepoints": [8835], "characters": "\u2283" },
+ "&supE;": { "codepoints": [10950], "characters": "\u2AC6" },
+ "&supdot;": { "codepoints": [10942], "characters": "\u2ABE" },
+ "&supdsub;": { "codepoints": [10968], "characters": "\u2AD8" },
+ "&supe;": { "codepoints": [8839], "characters": "\u2287" },
+ "&supedot;": { "codepoints": [10948], "characters": "\u2AC4" },
+ "&suphsol;": { "codepoints": [10185], "characters": "\u27C9" },
+ "&suphsub;": { "codepoints": [10967], "characters": "\u2AD7" },
+ "&suplarr;": { "codepoints": [10619], "characters": "\u297B" },
+ "&supmult;": { "codepoints": [10946], "characters": "\u2AC2" },
+ "&supnE;": { "codepoints": [10956], "characters": "\u2ACC" },
+ "&supne;": { "codepoints": [8843], "characters": "\u228B" },
+ "&supplus;": { "codepoints": [10944], "characters": "\u2AC0" },
+ "&supset;": { "codepoints": [8835], "characters": "\u2283" },
+ "&supseteq;": { "codepoints": [8839], "characters": "\u2287" },
+ "&supseteqq;": { "codepoints": [10950], "characters": "\u2AC6" },
+ "&supsetneq;": { "codepoints": [8843], "characters": "\u228B" },
+ "&supsetneqq;": { "codepoints": [10956], "characters": "\u2ACC" },
+ "&supsim;": { "codepoints": [10952], "characters": "\u2AC8" },
+ "&supsub;": { "codepoints": [10964], "characters": "\u2AD4" },
+ "&supsup;": { "codepoints": [10966], "characters": "\u2AD6" },
+ "&swArr;": { "codepoints": [8665], "characters": "\u21D9" },
+ "&swarhk;": { "codepoints": [10534], "characters": "\u2926" },
+ "&swarr;": { "codepoints": [8601], "characters": "\u2199" },
+ "&swarrow;": { "codepoints": [8601], "characters": "\u2199" },
+ "&swnwar;": { "codepoints": [10538], "characters": "\u292A" },
+ "&szlig": { "codepoints": [223], "characters": "\u00DF" },
+ "&szlig;": { "codepoints": [223], "characters": "\u00DF" },
+ "&target;": { "codepoints": [8982], "characters": "\u2316" },
+ "&tau;": { "codepoints": [964], "characters": "\u03C4" },
+ "&tbrk;": { "codepoints": [9140], "characters": "\u23B4" },
+ "&tcaron;": { "codepoints": [357], "characters": "\u0165" },
+ "&tcedil;": { "codepoints": [355], "characters": "\u0163" },
+ "&tcy;": { "codepoints": [1090], "characters": "\u0442" },
+ "&tdot;": { "codepoints": [8411], "characters": "\u20DB" },
+ "&telrec;": { "codepoints": [8981], "characters": "\u2315" },
+ "&tfr;": { "codepoints": [120113], "characters": "\uD835\uDD31" },
+ "&there4;": { "codepoints": [8756], "characters": "\u2234" },
+ "&therefore;": { "codepoints": [8756], "characters": "\u2234" },
+ "&theta;": { "codepoints": [952], "characters": "\u03B8" },
+ "&thetasym;": { "codepoints": [977], "characters": "\u03D1" },
+ "&thetav;": { "codepoints": [977], "characters": "\u03D1" },
+ "&thickapprox;": { "codepoints": [8776], "characters": "\u2248" },
+ "&thicksim;": { "codepoints": [8764], "characters": "\u223C" },
+ "&thinsp;": { "codepoints": [8201], "characters": "\u2009" },
+ "&thkap;": { "codepoints": [8776], "characters": "\u2248" },
+ "&thksim;": { "codepoints": [8764], "characters": "\u223C" },
+ "&thorn": { "codepoints": [254], "characters": "\u00FE" },
+ "&thorn;": { "codepoints": [254], "characters": "\u00FE" },
+ "&tilde;": { "codepoints": [732], "characters": "\u02DC" },
+ "&times": { "codepoints": [215], "characters": "\u00D7" },
+ "&times;": { "codepoints": [215], "characters": "\u00D7" },
+ "&timesb;": { "codepoints": [8864], "characters": "\u22A0" },
+ "&timesbar;": { "codepoints": [10801], "characters": "\u2A31" },
+ "&timesd;": { "codepoints": [10800], "characters": "\u2A30" },
+ "&tint;": { "codepoints": [8749], "characters": "\u222D" },
+ "&toea;": { "codepoints": [10536], "characters": "\u2928" },
+ "&top;": { "codepoints": [8868], "characters": "\u22A4" },
+ "&topbot;": { "codepoints": [9014], "characters": "\u2336" },
+ "&topcir;": { "codepoints": [10993], "characters": "\u2AF1" },
+ "&topf;": { "codepoints": [120165], "characters": "\uD835\uDD65" },
+ "&topfork;": { "codepoints": [10970], "characters": "\u2ADA" },
+ "&tosa;": { "codepoints": [10537], "characters": "\u2929" },
+ "&tprime;": { "codepoints": [8244], "characters": "\u2034" },
+ "&trade;": { "codepoints": [8482], "characters": "\u2122" },
+ "&triangle;": { "codepoints": [9653], "characters": "\u25B5" },
+ "&triangledown;": { "codepoints": [9663], "characters": "\u25BF" },
+ "&triangleleft;": { "codepoints": [9667], "characters": "\u25C3" },
+ "&trianglelefteq;": { "codepoints": [8884], "characters": "\u22B4" },
+ "&triangleq;": { "codepoints": [8796], "characters": "\u225C" },
+ "&triangleright;": { "codepoints": [9657], "characters": "\u25B9" },
+ "&trianglerighteq;": { "codepoints": [8885], "characters": "\u22B5" },
+ "&tridot;": { "codepoints": [9708], "characters": "\u25EC" },
+ "&trie;": { "codepoints": [8796], "characters": "\u225C" },
+ "&triminus;": { "codepoints": [10810], "characters": "\u2A3A" },
+ "&triplus;": { "codepoints": [10809], "characters": "\u2A39" },
+ "&trisb;": { "codepoints": [10701], "characters": "\u29CD" },
+ "&tritime;": { "codepoints": [10811], "characters": "\u2A3B" },
+ "&trpezium;": { "codepoints": [9186], "characters": "\u23E2" },
+ "&tscr;": { "codepoints": [120009], "characters": "\uD835\uDCC9" },
+ "&tscy;": { "codepoints": [1094], "characters": "\u0446" },
+ "&tshcy;": { "codepoints": [1115], "characters": "\u045B" },
+ "&tstrok;": { "codepoints": [359], "characters": "\u0167" },
+ "&twixt;": { "codepoints": [8812], "characters": "\u226C" },
+ "&twoheadleftarrow;": { "codepoints": [8606], "characters": "\u219E" },
+ "&twoheadrightarrow;": { "codepoints": [8608], "characters": "\u21A0" },
+ "&uArr;": { "codepoints": [8657], "characters": "\u21D1" },
+ "&uHar;": { "codepoints": [10595], "characters": "\u2963" },
+ "&uacute": { "codepoints": [250], "characters": "\u00FA" },
+ "&uacute;": { "codepoints": [250], "characters": "\u00FA" },
+ "&uarr;": { "codepoints": [8593], "characters": "\u2191" },
+ "&ubrcy;": { "codepoints": [1118], "characters": "\u045E" },
+ "&ubreve;": { "codepoints": [365], "characters": "\u016D" },
+ "&ucirc": { "codepoints": [251], "characters": "\u00FB" },
+ "&ucirc;": { "codepoints": [251], "characters": "\u00FB" },
+ "&ucy;": { "codepoints": [1091], "characters": "\u0443" },
+ "&udarr;": { "codepoints": [8645], "characters": "\u21C5" },
+ "&udblac;": { "codepoints": [369], "characters": "\u0171" },
+ "&udhar;": { "codepoints": [10606], "characters": "\u296E" },
+ "&ufisht;": { "codepoints": [10622], "characters": "\u297E" },
+ "&ufr;": { "codepoints": [120114], "characters": "\uD835\uDD32" },
+ "&ugrave": { "codepoints": [249], "characters": "\u00F9" },
+ "&ugrave;": { "codepoints": [249], "characters": "\u00F9" },
+ "&uharl;": { "codepoints": [8639], "characters": "\u21BF" },
+ "&uharr;": { "codepoints": [8638], "characters": "\u21BE" },
+ "&uhblk;": { "codepoints": [9600], "characters": "\u2580" },
+ "&ulcorn;": { "codepoints": [8988], "characters": "\u231C" },
+ "&ulcorner;": { "codepoints": [8988], "characters": "\u231C" },
+ "&ulcrop;": { "codepoints": [8975], "characters": "\u230F" },
+ "&ultri;": { "codepoints": [9720], "characters": "\u25F8" },
+ "&umacr;": { "codepoints": [363], "characters": "\u016B" },
+ "&uml": { "codepoints": [168], "characters": "\u00A8" },
+ "&uml;": { "codepoints": [168], "characters": "\u00A8" },
+ "&uogon;": { "codepoints": [371], "characters": "\u0173" },
+ "&uopf;": { "codepoints": [120166], "characters": "\uD835\uDD66" },
+ "&uparrow;": { "codepoints": [8593], "characters": "\u2191" },
+ "&updownarrow;": { "codepoints": [8597], "characters": "\u2195" },
+ "&upharpoonleft;": { "codepoints": [8639], "characters": "\u21BF" },
+ "&upharpoonright;": { "codepoints": [8638], "characters": "\u21BE" },
+ "&uplus;": { "codepoints": [8846], "characters": "\u228E" },
+ "&upsi;": { "codepoints": [965], "characters": "\u03C5" },
+ "&upsih;": { "codepoints": [978], "characters": "\u03D2" },
+ "&upsilon;": { "codepoints": [965], "characters": "\u03C5" },
+ "&upuparrows;": { "codepoints": [8648], "characters": "\u21C8" },
+ "&urcorn;": { "codepoints": [8989], "characters": "\u231D" },
+ "&urcorner;": { "codepoints": [8989], "characters": "\u231D" },
+ "&urcrop;": { "codepoints": [8974], "characters": "\u230E" },
+ "&uring;": { "codepoints": [367], "characters": "\u016F" },
+ "&urtri;": { "codepoints": [9721], "characters": "\u25F9" },
+ "&uscr;": { "codepoints": [120010], "characters": "\uD835\uDCCA" },
+ "&utdot;": { "codepoints": [8944], "characters": "\u22F0" },
+ "&utilde;": { "codepoints": [361], "characters": "\u0169" },
+ "&utri;": { "codepoints": [9653], "characters": "\u25B5" },
+ "&utrif;": { "codepoints": [9652], "characters": "\u25B4" },
+ "&uuarr;": { "codepoints": [8648], "characters": "\u21C8" },
+ "&uuml": { "codepoints": [252], "characters": "\u00FC" },
+ "&uuml;": { "codepoints": [252], "characters": "\u00FC" },
+ "&uwangle;": { "codepoints": [10663], "characters": "\u29A7" },
+ "&vArr;": { "codepoints": [8661], "characters": "\u21D5" },
+ "&vBar;": { "codepoints": [10984], "characters": "\u2AE8" },
+ "&vBarv;": { "codepoints": [10985], "characters": "\u2AE9" },
+ "&vDash;": { "codepoints": [8872], "characters": "\u22A8" },
+ "&vangrt;": { "codepoints": [10652], "characters": "\u299C" },
+ "&varepsilon;": { "codepoints": [1013], "characters": "\u03F5" },
+ "&varkappa;": { "codepoints": [1008], "characters": "\u03F0" },
+ "&varnothing;": { "codepoints": [8709], "characters": "\u2205" },
+ "&varphi;": { "codepoints": [981], "characters": "\u03D5" },
+ "&varpi;": { "codepoints": [982], "characters": "\u03D6" },
+ "&varpropto;": { "codepoints": [8733], "characters": "\u221D" },
+ "&varr;": { "codepoints": [8597], "characters": "\u2195" },
+ "&varrho;": { "codepoints": [1009], "characters": "\u03F1" },
+ "&varsigma;": { "codepoints": [962], "characters": "\u03C2" },
+ "&varsubsetneq;": { "codepoints": [8842, 65024], "characters": "\u228A\uFE00" },
+ "&varsubsetneqq;": { "codepoints": [10955, 65024], "characters": "\u2ACB\uFE00" },
+ "&varsupsetneq;": { "codepoints": [8843, 65024], "characters": "\u228B\uFE00" },
+ "&varsupsetneqq;": { "codepoints": [10956, 65024], "characters": "\u2ACC\uFE00" },
+ "&vartheta;": { "codepoints": [977], "characters": "\u03D1" },
+ "&vartriangleleft;": { "codepoints": [8882], "characters": "\u22B2" },
+ "&vartriangleright;": { "codepoints": [8883], "characters": "\u22B3" },
+ "&vcy;": { "codepoints": [1074], "characters": "\u0432" },
+ "&vdash;": { "codepoints": [8866], "characters": "\u22A2" },
+ "&vee;": { "codepoints": [8744], "characters": "\u2228" },
+ "&veebar;": { "codepoints": [8891], "characters": "\u22BB" },
+ "&veeeq;": { "codepoints": [8794], "characters": "\u225A" },
+ "&vellip;": { "codepoints": [8942], "characters": "\u22EE" },
+ "&verbar;": { "codepoints": [124], "characters": "\u007C" },
+ "&vert;": { "codepoints": [124], "characters": "\u007C" },
+ "&vfr;": { "codepoints": [120115], "characters": "\uD835\uDD33" },
+ "&vltri;": { "codepoints": [8882], "characters": "\u22B2" },
+ "&vnsub;": { "codepoints": [8834, 8402], "characters": "\u2282\u20D2" },
+ "&vnsup;": { "codepoints": [8835, 8402], "characters": "\u2283\u20D2" },
+ "&vopf;": { "codepoints": [120167], "characters": "\uD835\uDD67" },
+ "&vprop;": { "codepoints": [8733], "characters": "\u221D" },
+ "&vrtri;": { "codepoints": [8883], "characters": "\u22B3" },
+ "&vscr;": { "codepoints": [120011], "characters": "\uD835\uDCCB" },
+ "&vsubnE;": { "codepoints": [10955, 65024], "characters": "\u2ACB\uFE00" },
+ "&vsubne;": { "codepoints": [8842, 65024], "characters": "\u228A\uFE00" },
+ "&vsupnE;": { "codepoints": [10956, 65024], "characters": "\u2ACC\uFE00" },
+ "&vsupne;": { "codepoints": [8843, 65024], "characters": "\u228B\uFE00" },
+ "&vzigzag;": { "codepoints": [10650], "characters": "\u299A" },
+ "&wcirc;": { "codepoints": [373], "characters": "\u0175" },
+ "&wedbar;": { "codepoints": [10847], "characters": "\u2A5F" },
+ "&wedge;": { "codepoints": [8743], "characters": "\u2227" },
+ "&wedgeq;": { "codepoints": [8793], "characters": "\u2259" },
+ "&weierp;": { "codepoints": [8472], "characters": "\u2118" },
+ "&wfr;": { "codepoints": [120116], "characters": "\uD835\uDD34" },
+ "&wopf;": { "codepoints": [120168], "characters": "\uD835\uDD68" },
+ "&wp;": { "codepoints": [8472], "characters": "\u2118" },
+ "&wr;": { "codepoints": [8768], "characters": "\u2240" },
+ "&wreath;": { "codepoints": [8768], "characters": "\u2240" },
+ "&wscr;": { "codepoints": [120012], "characters": "\uD835\uDCCC" },
+ "&xcap;": { "codepoints": [8898], "characters": "\u22C2" },
+ "&xcirc;": { "codepoints": [9711], "characters": "\u25EF" },
+ "&xcup;": { "codepoints": [8899], "characters": "\u22C3" },
+ "&xdtri;": { "codepoints": [9661], "characters": "\u25BD" },
+ "&xfr;": { "codepoints": [120117], "characters": "\uD835\uDD35" },
+ "&xhArr;": { "codepoints": [10234], "characters": "\u27FA" },
+ "&xharr;": { "codepoints": [10231], "characters": "\u27F7" },
+ "&xi;": { "codepoints": [958], "characters": "\u03BE" },
+ "&xlArr;": { "codepoints": [10232], "characters": "\u27F8" },
+ "&xlarr;": { "codepoints": [10229], "characters": "\u27F5" },
+ "&xmap;": { "codepoints": [10236], "characters": "\u27FC" },
+ "&xnis;": { "codepoints": [8955], "characters": "\u22FB" },
+ "&xodot;": { "codepoints": [10752], "characters": "\u2A00" },
+ "&xopf;": { "codepoints": [120169], "characters": "\uD835\uDD69" },
+ "&xoplus;": { "codepoints": [10753], "characters": "\u2A01" },
+ "&xotime;": { "codepoints": [10754], "characters": "\u2A02" },
+ "&xrArr;": { "codepoints": [10233], "characters": "\u27F9" },
+ "&xrarr;": { "codepoints": [10230], "characters": "\u27F6" },
+ "&xscr;": { "codepoints": [120013], "characters": "\uD835\uDCCD" },
+ "&xsqcup;": { "codepoints": [10758], "characters": "\u2A06" },
+ "&xuplus;": { "codepoints": [10756], "characters": "\u2A04" },
+ "&xutri;": { "codepoints": [9651], "characters": "\u25B3" },
+ "&xvee;": { "codepoints": [8897], "characters": "\u22C1" },
+ "&xwedge;": { "codepoints": [8896], "characters": "\u22C0" },
+ "&yacute": { "codepoints": [253], "characters": "\u00FD" },
+ "&yacute;": { "codepoints": [253], "characters": "\u00FD" },
+ "&yacy;": { "codepoints": [1103], "characters": "\u044F" },
+ "&ycirc;": { "codepoints": [375], "characters": "\u0177" },
+ "&ycy;": { "codepoints": [1099], "characters": "\u044B" },
+ "&yen": { "codepoints": [165], "characters": "\u00A5" },
+ "&yen;": { "codepoints": [165], "characters": "\u00A5" },
+ "&yfr;": { "codepoints": [120118], "characters": "\uD835\uDD36" },
+ "&yicy;": { "codepoints": [1111], "characters": "\u0457" },
+ "&yopf;": { "codepoints": [120170], "characters": "\uD835\uDD6A" },
+ "&yscr;": { "codepoints": [120014], "characters": "\uD835\uDCCE" },
+ "&yucy;": { "codepoints": [1102], "characters": "\u044E" },
+ "&yuml": { "codepoints": [255], "characters": "\u00FF" },
+ "&yuml;": { "codepoints": [255], "characters": "\u00FF" },
+ "&zacute;": { "codepoints": [378], "characters": "\u017A" },
+ "&zcaron;": { "codepoints": [382], "characters": "\u017E" },
+ "&zcy;": { "codepoints": [1079], "characters": "\u0437" },
+ "&zdot;": { "codepoints": [380], "characters": "\u017C" },
+ "&zeetrf;": { "codepoints": [8488], "characters": "\u2128" },
+ "&zeta;": { "codepoints": [950], "characters": "\u03B6" },
+ "&zfr;": { "codepoints": [120119], "characters": "\uD835\uDD37" },
+ "&zhcy;": { "codepoints": [1078], "characters": "\u0436" },
+ "&zigrarr;": { "codepoints": [8669], "characters": "\u21DD" },
+ "&zopf;": { "codepoints": [120171], "characters": "\uD835\uDD6B" },
+ "&zscr;": { "codepoints": [120015], "characters": "\uD835\uDCCF" },
+ "&zwj;": { "codepoints": [8205], "characters": "\u200D" },
+ "&zwnj;": { "codepoints": [8204], "characters": "\u200C" }
+}
diff --git a/src/xml_util.cc b/src/xml_util.cc
new file mode 100644
index 0000000..a294def
--- /dev/null
+++ b/src/xml_util.cc
@@ -0,0 +1,81 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "xml_util.hh"
+
+#include "config.h"
+#include "fmtlib/fmt/format.h"
+
+namespace lnav {
+namespace pugixml {
+
+std::string
+get_actual_path(const pugi::xml_node& node)
+{
+ std::string retval;
+ auto curr = node;
+
+ while (curr) {
+ switch (curr.type()) {
+ case pugi::node_null:
+ break;
+ case pugi::node_pcdata:
+ retval += "text()";
+ break;
+ default: {
+ auto name = std::string(curr.name());
+
+ if (curr.previous_sibling(curr.name())
+ || curr.next_sibling(curr.name())) {
+ auto sibling = curr;
+ int index = 0;
+
+ while (sibling) {
+ index += 1;
+ sibling = sibling.previous_sibling(curr.name());
+ }
+
+ name = fmt::format(FMT_STRING("{}[{}]"), name, index);
+ }
+ if (retval.empty()) {
+ retval = name;
+ } else {
+ retval = fmt::format(FMT_STRING("{}/{}"), name, retval);
+ }
+ break;
+ }
+ }
+ curr = curr.parent();
+ }
+
+ return retval;
+}
+
+} // namespace pugixml
+} // namespace lnav
diff --git a/src/xml_util.hh b/src/xml_util.hh
new file mode 100644
index 0000000..9a1ae90
--- /dev/null
+++ b/src/xml_util.hh
@@ -0,0 +1,45 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef lnav_xml_util_hh
+#define lnav_xml_util_hh
+
+#include <string>
+
+#include "pugixml/pugixml.hpp"
+
+namespace lnav {
+namespace pugixml {
+
+std::string get_actual_path(const pugi::xml_node& node);
+
+}
+} // namespace lnav
+
+#endif
diff --git a/src/xpath_vtab.cc b/src/xpath_vtab.cc
new file mode 100644
index 0000000..f44bde4
--- /dev/null
+++ b/src/xpath_vtab.cc
@@ -0,0 +1,393 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <sstream>
+#include <unordered_map>
+
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "pugixml/pugixml.hpp"
+#include "sql_help.hh"
+#include "sql_util.hh"
+#include "vtab_module.hh"
+#include "xml_util.hh"
+#include "yajlpp/yajlpp.hh"
+
+enum {
+ XP_COL_RESULT,
+ XP_COL_NODE_PATH,
+ XP_COL_NODE_ATTR,
+ XP_COL_NODE_TEXT,
+ XP_COL_XPATH,
+ XP_COL_VALUE,
+};
+
+static thread_local std::unordered_map<std::string, pugi::xpath_query>
+ QUERY_CACHE;
+
+static pugi::xpath_query
+checkout_query(const std::string& query)
+{
+ auto iter = QUERY_CACHE.find(query);
+ if (iter == QUERY_CACHE.end()) {
+ auto xquery = pugi::xpath_query(query.c_str());
+
+ if (!xquery) {
+ return xquery;
+ }
+
+ auto pair = QUERY_CACHE.emplace(query, std::move(xquery));
+
+ iter = pair.first;
+ }
+
+ auto retval = std::move(iter->second);
+
+ QUERY_CACHE.erase(iter);
+
+ return retval;
+}
+
+static void
+checkin_query(const std::string& query_str, pugi::xpath_query query)
+{
+ if (!query) {
+ return;
+ }
+
+ QUERY_CACHE[query_str] = std::move(query);
+}
+
+struct xpath_vtab {
+ static constexpr const char* NAME = "xpath";
+ static constexpr const char* CREATE_STMT = R"(
+-- The xpath() table-valued function allows you to execute an xpath expression
+CREATE TABLE xpath (
+ result text, -- The result of the xpath expression
+ node_path text, -- The absolute path to the node selected by the expression
+ node_attr text, -- The node attributes stored in a JSON object
+ node_text text, -- The text portion of the node selected by the expression
+
+ xpath text HIDDEN,
+ value text HIDDEN
+);
+)";
+
+ struct cursor {
+ sqlite3_vtab_cursor base;
+ sqlite3_int64 c_rowid{0};
+ std::string c_xpath;
+ std::string c_value;
+ bool c_value_as_blob{false};
+ pugi::xpath_query c_query;
+ pugi::xml_document c_doc;
+ pugi::xpath_node_set c_results;
+
+ cursor(sqlite3_vtab* vt) : base({vt}) {}
+
+ ~cursor() { this->reset(); }
+
+ int reset()
+ {
+ this->c_rowid = 0;
+ checkin_query(this->c_xpath, std::move(this->c_query));
+
+ return SQLITE_OK;
+ }
+
+ int next()
+ {
+ this->c_rowid += 1;
+
+ return SQLITE_OK;
+ }
+
+ int eof() { return this->c_rowid >= (int64_t) this->c_results.size(); }
+
+ int get_rowid(sqlite3_int64& rowid_out)
+ {
+ rowid_out = this->c_rowid;
+
+ return SQLITE_OK;
+ }
+ };
+
+ int get_column(const cursor& vc, sqlite3_context* ctx, int col)
+ {
+ switch (col) {
+ case XP_COL_RESULT: {
+ auto& xpath_node = vc.c_results[vc.c_rowid];
+
+ if (xpath_node.node()) {
+ std::ostringstream oss;
+
+ // XXX avoid the extra allocs
+ xpath_node.node().print(oss);
+ auto node_xml = oss.str();
+ sqlite3_result_text(ctx,
+ node_xml.c_str(),
+ node_xml.length(),
+ SQLITE_TRANSIENT);
+ } else if (xpath_node.attribute()) {
+ sqlite3_result_text(ctx,
+ xpath_node.attribute().value(),
+ -1,
+ SQLITE_TRANSIENT);
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ break;
+ }
+ case XP_COL_NODE_PATH: {
+ auto& xpath_node = vc.c_results[vc.c_rowid];
+ auto x_node = xpath_node.node();
+ auto x_attr = xpath_node.attribute();
+
+ if (x_node || x_attr) {
+ if (!x_node) {
+ x_node = xpath_node.parent();
+ }
+
+ auto node_path = lnav::pugixml::get_actual_path(x_node);
+ if (x_attr) {
+ node_path += "/@" + std::string(x_attr.name());
+ }
+ sqlite3_result_text(ctx,
+ node_path.c_str(),
+ node_path.length(),
+ SQLITE_TRANSIENT);
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ break;
+ }
+ case XP_COL_NODE_ATTR: {
+ auto& xpath_node = vc.c_results[vc.c_rowid];
+ auto x_node = xpath_node.node();
+ auto x_attr = xpath_node.attribute();
+
+ if (x_node || x_attr) {
+ if (!x_node) {
+ x_node = xpath_node.parent();
+ }
+
+ yajlpp_gen gen;
+
+ yajl_gen_config(gen, yajl_gen_beautify, false);
+
+ {
+ yajlpp_map attrs(gen);
+
+ for (const auto& attr : x_node.attributes()) {
+ attrs.gen(attr.name());
+ attrs.gen(attr.value());
+ }
+ }
+
+ auto sf = gen.to_string_fragment();
+
+ sqlite3_result_text(
+ ctx, sf.data(), sf.length(), SQLITE_TRANSIENT);
+ sqlite3_result_subtype(ctx, 'J');
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ break;
+ }
+ case XP_COL_NODE_TEXT: {
+ auto& xpath_node = vc.c_results[vc.c_rowid];
+ auto x_node = xpath_node.node();
+ auto x_attr = xpath_node.attribute();
+
+ if (x_node || x_attr) {
+ if (!x_node) {
+ x_node = xpath_node.parent();
+ }
+
+ auto node_text = x_node.text();
+
+ sqlite3_result_text(
+ ctx, node_text.get(), -1, SQLITE_TRANSIENT);
+ } else {
+ sqlite3_result_null(ctx);
+ }
+ break;
+ }
+ case XP_COL_XPATH:
+ sqlite3_result_text(ctx,
+ vc.c_xpath.c_str(),
+ vc.c_xpath.length(),
+ SQLITE_STATIC);
+ break;
+ case XP_COL_VALUE:
+ if (vc.c_value_as_blob) {
+ sqlite3_result_blob64(ctx,
+ vc.c_value.c_str(),
+ vc.c_value.length(),
+ SQLITE_STATIC);
+ } else {
+ sqlite3_result_text(ctx,
+ vc.c_value.c_str(),
+ vc.c_value.length(),
+ SQLITE_STATIC);
+ }
+ break;
+ }
+
+ return SQLITE_OK;
+ }
+};
+
+static int
+rcBestIndex(sqlite3_vtab* tab, sqlite3_index_info* pIdxInfo)
+{
+ vtab_index_constraints vic(pIdxInfo);
+ vtab_index_usage viu(pIdxInfo);
+
+ for (auto iter = vic.begin(); iter != vic.end(); ++iter) {
+ if (iter->op != SQLITE_INDEX_CONSTRAINT_EQ) {
+ continue;
+ }
+
+ switch (iter->iColumn) {
+ case XP_COL_VALUE:
+ case XP_COL_XPATH:
+ viu.column_used(iter);
+ break;
+ }
+ }
+
+ viu.allocate_args(XP_COL_XPATH, XP_COL_VALUE, 2);
+ return SQLITE_OK;
+}
+
+static int
+rcFilter(sqlite3_vtab_cursor* pVtabCursor,
+ int idxNum,
+ const char* idxStr,
+ int argc,
+ sqlite3_value** argv)
+{
+ auto* pCur = (xpath_vtab::cursor*) pVtabCursor;
+
+ if (argc != 2) {
+ pCur->c_xpath.clear();
+ pCur->c_value.clear();
+ return SQLITE_OK;
+ }
+
+ pCur->c_value_as_blob = (sqlite3_value_type(argv[1]) == SQLITE_BLOB);
+ auto byte_count = sqlite3_value_bytes(argv[1]);
+
+ if (byte_count == 0) {
+ pCur->c_rowid = 0;
+ return SQLITE_OK;
+ }
+
+ auto blob = (const char*) sqlite3_value_blob(argv[1]);
+ pCur->c_value.assign(blob, byte_count);
+ auto parse_res = pCur->c_doc.load_string(pCur->c_value.c_str());
+ if (!parse_res) {
+ pVtabCursor->pVtab->zErrMsg
+ = sqlite3_mprintf("Invalid XML document at offset %d: %s",
+ parse_res.offset,
+ parse_res.description());
+ return SQLITE_ERROR;
+ }
+
+ pCur->c_xpath = (const char*) sqlite3_value_text(argv[0]);
+ pCur->c_query = checkout_query(pCur->c_xpath);
+ if (!pCur->c_query) {
+ auto& res = pCur->c_query.result();
+ pVtabCursor->pVtab->zErrMsg
+ = sqlite3_mprintf("Invalid XPATH expression at offset %d: %s",
+ res.offset,
+ res.description());
+ return SQLITE_ERROR;
+ }
+
+ pCur->c_rowid = 0;
+ pCur->c_results = pCur->c_doc.select_nodes(pCur->c_query);
+
+ return SQLITE_OK;
+}
+
+int
+register_xpath_vtab(sqlite3* db)
+{
+ static vtab_module<tvt_no_update<xpath_vtab>> XPATH_MODULE;
+ static help_text xpath_help
+ = help_text("xpath",
+ "A table-valued function that executes an xpath expression "
+ "over an XML "
+ "string and returns the selected values.")
+ .sql_table_valued_function()
+ .with_parameter({
+ "xpath",
+ "The XPATH expression to evaluate over the XML document.",
+ })
+ .with_parameter({"xmldoc", "The XML document as a string."})
+ .with_result({"result", "The result of the XPATH expression."})
+ .with_result({
+ "node_path",
+ "The absolute path to the node containing the result.",
+ })
+ .with_result(
+ {"node_attr", "The node's attributes stored in JSON object."})
+ .with_result({"node_text", "The node's text value."})
+ .with_tags({"string", "xml"})
+ .with_example({
+ "To select the XML nodes on the path '/abc/def'",
+ "SELECT * FROM xpath('/abc/def', '<abc><def "
+ "a=\"b\">Hello</def><def>Bye</def></abc>')",
+ })
+ .with_example({
+ "To select all 'a' attributes on the path '/abc/def'",
+ "SELECT * FROM xpath('/abc/def/@a', '<abc><def "
+ "a=\"b\">Hello</def><def>Bye</def></abc>')",
+ })
+ .with_example({
+ "To select the text nodes on the path '/abc/def'",
+ "SELECT * FROM xpath('/abc/def/text()', '<abc><def "
+ "a=\"b\">Hello &#x2605;</def></abc>')",
+ });
+
+ int rc;
+
+ XPATH_MODULE.vm_module.xBestIndex = rcBestIndex;
+ XPATH_MODULE.vm_module.xFilter = rcFilter;
+
+ rc = XPATH_MODULE.create(db, "xpath");
+ sqlite_function_help.insert(std::make_pair("xpath", &xpath_help));
+ xpath_help.index_tags();
+
+ ensure(rc == SQLITE_OK);
+
+ return rc;
+}
diff --git a/src/xpath_vtab.hh b/src/xpath_vtab.hh
new file mode 100644
index 0000000..66f693f
--- /dev/null
+++ b/src/xpath_vtab.hh
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) 2020, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef xpath_vtab_hh
+#define xpath_vtab_hh
+
+#include <sqlite3.h>
+
+int register_xpath_vtab(sqlite3* db);
+
+#endif
diff --git a/src/xterm-palette.json b/src/xterm-palette.json
new file mode 100644
index 0000000..4c64d99
--- /dev/null
+++ b/src/xterm-palette.json
@@ -0,0 +1,3842 @@
+[
+ {
+ "colorId": 0,
+ "hexString": "#000000",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 0
+ },
+ "name": "Black"
+ },
+ {
+ "colorId": 1,
+ "hexString": "#800000",
+ "rgb": {
+ "r": 128,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Maroon"
+ },
+ {
+ "colorId": 2,
+ "hexString": "#008000",
+ "rgb": {
+ "r": 0,
+ "g": 128,
+ "b": 0
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Green"
+ },
+ {
+ "colorId": 3,
+ "hexString": "#808000",
+ "rgb": {
+ "r": 128,
+ "g": 128,
+ "b": 0
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Olive"
+ },
+ {
+ "colorId": 4,
+ "hexString": "#000080",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 128
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Navy"
+ },
+ {
+ "colorId": 5,
+ "hexString": "#800080",
+ "rgb": {
+ "r": 128,
+ "g": 0,
+ "b": 128
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Purple"
+ },
+ {
+ "colorId": 6,
+ "hexString": "#008080",
+ "rgb": {
+ "r": 0,
+ "g": 128,
+ "b": 128
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 25
+ },
+ "name": "Teal"
+ },
+ {
+ "colorId": 7,
+ "hexString": "#c0c0c0",
+ "rgb": {
+ "r": 192,
+ "g": 192,
+ "b": 192
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 75
+ },
+ "name": "Silver"
+ },
+ {
+ "colorId": 8,
+ "hexString": "#808080",
+ "rgb": {
+ "r": 128,
+ "g": 128,
+ "b": 128
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 50
+ },
+ "name": "Grey"
+ },
+ {
+ "colorId": 9,
+ "hexString": "#ff0000",
+ "rgb": {
+ "r": 255,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Red"
+ },
+ {
+ "colorId": 10,
+ "hexString": "#00ff00",
+ "rgb": {
+ "r": 0,
+ "g": 255,
+ "b": 0
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Lime"
+ },
+ {
+ "colorId": 11,
+ "hexString": "#ffff00",
+ "rgb": {
+ "r": 255,
+ "g": 255,
+ "b": 0
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Yellow"
+ },
+ {
+ "colorId": 12,
+ "hexString": "#0000ff",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 255
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Blue"
+ },
+ {
+ "colorId": 13,
+ "hexString": "#ff00ff",
+ "rgb": {
+ "r": 255,
+ "g": 0,
+ "b": 255
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Fuchsia"
+ },
+ {
+ "colorId": 14,
+ "hexString": "#00ffff",
+ "rgb": {
+ "r": 0,
+ "g": 255,
+ "b": 255
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Aqua"
+ },
+ {
+ "colorId": 15,
+ "hexString": "#ffffff",
+ "rgb": {
+ "r": 255,
+ "g": 255,
+ "b": 255
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 100
+ },
+ "name": "White"
+ },
+ {
+ "colorId": 16,
+ "hexString": "#000000",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 0
+ },
+ "name": "Grey0"
+ },
+ {
+ "colorId": 17,
+ "hexString": "#00005f",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 95
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 18
+ },
+ "name": "NavyBlue"
+ },
+ {
+ "colorId": 18,
+ "hexString": "#000087",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 135
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 26
+ },
+ "name": "DarkBlue"
+ },
+ {
+ "colorId": 19,
+ "hexString": "#0000af",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 175
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 34
+ },
+ "name": "Blue3"
+ },
+ {
+ "colorId": 20,
+ "hexString": "#0000d7",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 215
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Blue3"
+ },
+ {
+ "colorId": 21,
+ "hexString": "#0000ff",
+ "rgb": {
+ "r": 0,
+ "g": 0,
+ "b": 255
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Blue1"
+ },
+ {
+ "colorId": 22,
+ "hexString": "#005f00",
+ "rgb": {
+ "r": 0,
+ "g": 95,
+ "b": 0
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 18
+ },
+ "name": "DarkGreen"
+ },
+ {
+ "colorId": 23,
+ "hexString": "#005f5f",
+ "rgb": {
+ "r": 0,
+ "g": 95,
+ "b": 95
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 18
+ },
+ "name": "DeepSkyBlue4"
+ },
+ {
+ "colorId": 24,
+ "hexString": "#005f87",
+ "rgb": {
+ "r": 0,
+ "g": 95,
+ "b": 135
+ },
+ "hsl": {
+ "h": 197.777777777778,
+ "s": 100,
+ "l": 26
+ },
+ "name": "DeepSkyBlue4"
+ },
+ {
+ "colorId": 25,
+ "hexString": "#005faf",
+ "rgb": {
+ "r": 0,
+ "g": 95,
+ "b": 175
+ },
+ "hsl": {
+ "h": 207.428571428571,
+ "s": 100,
+ "l": 34
+ },
+ "name": "DeepSkyBlue4"
+ },
+ {
+ "colorId": 26,
+ "hexString": "#005fd7",
+ "rgb": {
+ "r": 0,
+ "g": 95,
+ "b": 215
+ },
+ "hsl": {
+ "h": 213.488372093023,
+ "s": 100,
+ "l": 42
+ },
+ "name": "DodgerBlue3"
+ },
+ {
+ "colorId": 27,
+ "hexString": "#005fff",
+ "rgb": {
+ "r": 0,
+ "g": 95,
+ "b": 255
+ },
+ "hsl": {
+ "h": 217.647058823529,
+ "s": 100,
+ "l": 50
+ },
+ "name": "DodgerBlue2"
+ },
+ {
+ "colorId": 28,
+ "hexString": "#008700",
+ "rgb": {
+ "r": 0,
+ "g": 135,
+ "b": 0
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 26
+ },
+ "name": "Green4"
+ },
+ {
+ "colorId": 29,
+ "hexString": "#00875f",
+ "rgb": {
+ "r": 0,
+ "g": 135,
+ "b": 95
+ },
+ "hsl": {
+ "h": 162.222222222222,
+ "s": 100,
+ "l": 26
+ },
+ "name": "SpringGreen4"
+ },
+ {
+ "colorId": 30,
+ "hexString": "#008787",
+ "rgb": {
+ "r": 0,
+ "g": 135,
+ "b": 135
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 26
+ },
+ "name": "Turquoise4"
+ },
+ {
+ "colorId": 31,
+ "hexString": "#0087af",
+ "rgb": {
+ "r": 0,
+ "g": 135,
+ "b": 175
+ },
+ "hsl": {
+ "h": 193.714285714286,
+ "s": 100,
+ "l": 34
+ },
+ "name": "DeepSkyBlue3"
+ },
+ {
+ "colorId": 32,
+ "hexString": "#0087d7",
+ "rgb": {
+ "r": 0,
+ "g": 135,
+ "b": 215
+ },
+ "hsl": {
+ "h": 202.325581395349,
+ "s": 100,
+ "l": 42
+ },
+ "name": "DeepSkyBlue3"
+ },
+ {
+ "colorId": 33,
+ "hexString": "#0087ff",
+ "rgb": {
+ "r": 0,
+ "g": 135,
+ "b": 255
+ },
+ "hsl": {
+ "h": 208.235294117647,
+ "s": 100,
+ "l": 50
+ },
+ "name": "DodgerBlue1"
+ },
+ {
+ "colorId": 34,
+ "hexString": "#00af00",
+ "rgb": {
+ "r": 0,
+ "g": 175,
+ "b": 0
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 34
+ },
+ "name": "Green3"
+ },
+ {
+ "colorId": 35,
+ "hexString": "#00af5f",
+ "rgb": {
+ "r": 0,
+ "g": 175,
+ "b": 95
+ },
+ "hsl": {
+ "h": 152.571428571429,
+ "s": 100,
+ "l": 34
+ },
+ "name": "SpringGreen3"
+ },
+ {
+ "colorId": 36,
+ "hexString": "#00af87",
+ "rgb": {
+ "r": 0,
+ "g": 175,
+ "b": 135
+ },
+ "hsl": {
+ "h": 166.285714285714,
+ "s": 100,
+ "l": 34
+ },
+ "name": "DarkCyan"
+ },
+ {
+ "colorId": 37,
+ "hexString": "#00afaf",
+ "rgb": {
+ "r": 0,
+ "g": 175,
+ "b": 175
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 34
+ },
+ "name": "LightSeaGreen"
+ },
+ {
+ "colorId": 38,
+ "hexString": "#00afd7",
+ "rgb": {
+ "r": 0,
+ "g": 175,
+ "b": 215
+ },
+ "hsl": {
+ "h": 191.162790697674,
+ "s": 100,
+ "l": 42
+ },
+ "name": "DeepSkyBlue2"
+ },
+ {
+ "colorId": 39,
+ "hexString": "#00afff",
+ "rgb": {
+ "r": 0,
+ "g": 175,
+ "b": 255
+ },
+ "hsl": {
+ "h": 198.823529411765,
+ "s": 100,
+ "l": 50
+ },
+ "name": "DeepSkyBlue1"
+ },
+ {
+ "colorId": 40,
+ "hexString": "#00d700",
+ "rgb": {
+ "r": 0,
+ "g": 215,
+ "b": 0
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Green3"
+ },
+ {
+ "colorId": 41,
+ "hexString": "#00d75f",
+ "rgb": {
+ "r": 0,
+ "g": 215,
+ "b": 95
+ },
+ "hsl": {
+ "h": 146.511627906977,
+ "s": 100,
+ "l": 42
+ },
+ "name": "SpringGreen3"
+ },
+ {
+ "colorId": 42,
+ "hexString": "#00d787",
+ "rgb": {
+ "r": 0,
+ "g": 215,
+ "b": 135
+ },
+ "hsl": {
+ "h": 157.674418604651,
+ "s": 100,
+ "l": 42
+ },
+ "name": "SpringGreen2"
+ },
+ {
+ "colorId": 43,
+ "hexString": "#00d7af",
+ "rgb": {
+ "r": 0,
+ "g": 215,
+ "b": 175
+ },
+ "hsl": {
+ "h": 168.837209302326,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Cyan3"
+ },
+ {
+ "colorId": 44,
+ "hexString": "#00d7d7",
+ "rgb": {
+ "r": 0,
+ "g": 215,
+ "b": 215
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 42
+ },
+ "name": "DarkTurquoise"
+ },
+ {
+ "colorId": 45,
+ "hexString": "#00d7ff",
+ "rgb": {
+ "r": 0,
+ "g": 215,
+ "b": 255
+ },
+ "hsl": {
+ "h": 189.411764705882,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Turquoise2"
+ },
+ {
+ "colorId": 46,
+ "hexString": "#00ff00",
+ "rgb": {
+ "r": 0,
+ "g": 255,
+ "b": 0
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Green1"
+ },
+ {
+ "colorId": 47,
+ "hexString": "#00ff5f",
+ "rgb": {
+ "r": 0,
+ "g": 255,
+ "b": 95
+ },
+ "hsl": {
+ "h": 142.352941176471,
+ "s": 100,
+ "l": 50
+ },
+ "name": "SpringGreen2"
+ },
+ {
+ "colorId": 48,
+ "hexString": "#00ff87",
+ "rgb": {
+ "r": 0,
+ "g": 255,
+ "b": 135
+ },
+ "hsl": {
+ "h": 151.764705882353,
+ "s": 100,
+ "l": 50
+ },
+ "name": "SpringGreen1"
+ },
+ {
+ "colorId": 49,
+ "hexString": "#00ffaf",
+ "rgb": {
+ "r": 0,
+ "g": 255,
+ "b": 175
+ },
+ "hsl": {
+ "h": 161.176470588235,
+ "s": 100,
+ "l": 50
+ },
+ "name": "MediumSpringGreen"
+ },
+ {
+ "colorId": 50,
+ "hexString": "#00ffd7",
+ "rgb": {
+ "r": 0,
+ "g": 255,
+ "b": 215
+ },
+ "hsl": {
+ "h": 170.588235294118,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Cyan2"
+ },
+ {
+ "colorId": 51,
+ "hexString": "#00ffff",
+ "rgb": {
+ "r": 0,
+ "g": 255,
+ "b": 255
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Cyan1"
+ },
+ {
+ "colorId": 52,
+ "hexString": "#5f0000",
+ "rgb": {
+ "r": 95,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 18
+ },
+ "name": "DarkRed"
+ },
+ {
+ "colorId": 53,
+ "hexString": "#5f005f",
+ "rgb": {
+ "r": 95,
+ "g": 0,
+ "b": 95
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 18
+ },
+ "name": "DeepPink4"
+ },
+ {
+ "colorId": 54,
+ "hexString": "#5f0087",
+ "rgb": {
+ "r": 95,
+ "g": 0,
+ "b": 135
+ },
+ "hsl": {
+ "h": 282.222222222222,
+ "s": 100,
+ "l": 26
+ },
+ "name": "Purple4"
+ },
+ {
+ "colorId": 55,
+ "hexString": "#5f00af",
+ "rgb": {
+ "r": 95,
+ "g": 0,
+ "b": 175
+ },
+ "hsl": {
+ "h": 272.571428571429,
+ "s": 100,
+ "l": 34
+ },
+ "name": "Purple4"
+ },
+ {
+ "colorId": 56,
+ "hexString": "#5f00d7",
+ "rgb": {
+ "r": 95,
+ "g": 0,
+ "b": 215
+ },
+ "hsl": {
+ "h": 266.511627906977,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Purple3"
+ },
+ {
+ "colorId": 57,
+ "hexString": "#5f00ff",
+ "rgb": {
+ "r": 95,
+ "g": 0,
+ "b": 255
+ },
+ "hsl": {
+ "h": 262.352941176471,
+ "s": 100,
+ "l": 50
+ },
+ "name": "BlueViolet"
+ },
+ {
+ "colorId": 58,
+ "hexString": "#5f5f00",
+ "rgb": {
+ "r": 95,
+ "g": 95,
+ "b": 0
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 18
+ },
+ "name": "Orange4"
+ },
+ {
+ "colorId": 59,
+ "hexString": "#5f5f5f",
+ "rgb": {
+ "r": 95,
+ "g": 95,
+ "b": 95
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 37
+ },
+ "name": "Grey37"
+ },
+ {
+ "colorId": 60,
+ "hexString": "#5f5f87",
+ "rgb": {
+ "r": 95,
+ "g": 95,
+ "b": 135
+ },
+ "hsl": {
+ "h": 240,
+ "s": 17,
+ "l": 45
+ },
+ "name": "MediumPurple4"
+ },
+ {
+ "colorId": 61,
+ "hexString": "#5f5faf",
+ "rgb": {
+ "r": 95,
+ "g": 95,
+ "b": 175
+ },
+ "hsl": {
+ "h": 240,
+ "s": 33,
+ "l": 52
+ },
+ "name": "SlateBlue3"
+ },
+ {
+ "colorId": 62,
+ "hexString": "#5f5fd7",
+ "rgb": {
+ "r": 95,
+ "g": 95,
+ "b": 215
+ },
+ "hsl": {
+ "h": 240,
+ "s": 60,
+ "l": 60
+ },
+ "name": "SlateBlue3"
+ },
+ {
+ "colorId": 63,
+ "hexString": "#5f5fff",
+ "rgb": {
+ "r": 95,
+ "g": 95,
+ "b": 255
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 68
+ },
+ "name": "RoyalBlue1"
+ },
+ {
+ "colorId": 64,
+ "hexString": "#5f8700",
+ "rgb": {
+ "r": 95,
+ "g": 135,
+ "b": 0
+ },
+ "hsl": {
+ "h": 77.7777777777778,
+ "s": 100,
+ "l": 26
+ },
+ "name": "Chartreuse4"
+ },
+ {
+ "colorId": 65,
+ "hexString": "#5f875f",
+ "rgb": {
+ "r": 95,
+ "g": 135,
+ "b": 95
+ },
+ "hsl": {
+ "h": 120,
+ "s": 17,
+ "l": 45
+ },
+ "name": "DarkSeaGreen4"
+ },
+ {
+ "colorId": 66,
+ "hexString": "#5f8787",
+ "rgb": {
+ "r": 95,
+ "g": 135,
+ "b": 135
+ },
+ "hsl": {
+ "h": 180,
+ "s": 17,
+ "l": 45
+ },
+ "name": "PaleTurquoise4"
+ },
+ {
+ "colorId": 67,
+ "hexString": "#5f87af",
+ "rgb": {
+ "r": 95,
+ "g": 135,
+ "b": 175
+ },
+ "hsl": {
+ "h": 210,
+ "s": 33,
+ "l": 52
+ },
+ "name": "SteelBlue"
+ },
+ {
+ "colorId": 68,
+ "hexString": "#5f87d7",
+ "rgb": {
+ "r": 95,
+ "g": 135,
+ "b": 215
+ },
+ "hsl": {
+ "h": 220,
+ "s": 60,
+ "l": 60
+ },
+ "name": "SteelBlue3"
+ },
+ {
+ "colorId": 69,
+ "hexString": "#5f87ff",
+ "rgb": {
+ "r": 95,
+ "g": 135,
+ "b": 255
+ },
+ "hsl": {
+ "h": 225,
+ "s": 100,
+ "l": 68
+ },
+ "name": "CornflowerBlue"
+ },
+ {
+ "colorId": 70,
+ "hexString": "#5faf00",
+ "rgb": {
+ "r": 95,
+ "g": 175,
+ "b": 0
+ },
+ "hsl": {
+ "h": 87.4285714285714,
+ "s": 100,
+ "l": 34
+ },
+ "name": "Chartreuse3"
+ },
+ {
+ "colorId": 71,
+ "hexString": "#5faf5f",
+ "rgb": {
+ "r": 95,
+ "g": 175,
+ "b": 95
+ },
+ "hsl": {
+ "h": 120,
+ "s": 33,
+ "l": 52
+ },
+ "name": "DarkSeaGreen4"
+ },
+ {
+ "colorId": 72,
+ "hexString": "#5faf87",
+ "rgb": {
+ "r": 95,
+ "g": 175,
+ "b": 135
+ },
+ "hsl": {
+ "h": 150,
+ "s": 33,
+ "l": 52
+ },
+ "name": "CadetBlue"
+ },
+ {
+ "colorId": 73,
+ "hexString": "#5fafaf",
+ "rgb": {
+ "r": 95,
+ "g": 175,
+ "b": 175
+ },
+ "hsl": {
+ "h": 180,
+ "s": 33,
+ "l": 52
+ },
+ "name": "CadetBlue"
+ },
+ {
+ "colorId": 74,
+ "hexString": "#5fafd7",
+ "rgb": {
+ "r": 95,
+ "g": 175,
+ "b": 215
+ },
+ "hsl": {
+ "h": 200,
+ "s": 60,
+ "l": 60
+ },
+ "name": "SkyBlue3"
+ },
+ {
+ "colorId": 75,
+ "hexString": "#5fafff",
+ "rgb": {
+ "r": 95,
+ "g": 175,
+ "b": 255
+ },
+ "hsl": {
+ "h": 210,
+ "s": 100,
+ "l": 68
+ },
+ "name": "SteelBlue1"
+ },
+ {
+ "colorId": 76,
+ "hexString": "#5fd700",
+ "rgb": {
+ "r": 95,
+ "g": 215,
+ "b": 0
+ },
+ "hsl": {
+ "h": 93.4883720930233,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Chartreuse3"
+ },
+ {
+ "colorId": 77,
+ "hexString": "#5fd75f",
+ "rgb": {
+ "r": 95,
+ "g": 215,
+ "b": 95
+ },
+ "hsl": {
+ "h": 120,
+ "s": 60,
+ "l": 60
+ },
+ "name": "PaleGreen3"
+ },
+ {
+ "colorId": 78,
+ "hexString": "#5fd787",
+ "rgb": {
+ "r": 95,
+ "g": 215,
+ "b": 135
+ },
+ "hsl": {
+ "h": 140,
+ "s": 60,
+ "l": 60
+ },
+ "name": "SeaGreen3"
+ },
+ {
+ "colorId": 79,
+ "hexString": "#5fd7af",
+ "rgb": {
+ "r": 95,
+ "g": 215,
+ "b": 175
+ },
+ "hsl": {
+ "h": 160,
+ "s": 60,
+ "l": 60
+ },
+ "name": "Aquamarine3"
+ },
+ {
+ "colorId": 80,
+ "hexString": "#5fd7d7",
+ "rgb": {
+ "r": 95,
+ "g": 215,
+ "b": 215
+ },
+ "hsl": {
+ "h": 180,
+ "s": 60,
+ "l": 60
+ },
+ "name": "MediumTurquoise"
+ },
+ {
+ "colorId": 81,
+ "hexString": "#5fd7ff",
+ "rgb": {
+ "r": 95,
+ "g": 215,
+ "b": 255
+ },
+ "hsl": {
+ "h": 195,
+ "s": 100,
+ "l": 68
+ },
+ "name": "SteelBlue1"
+ },
+ {
+ "colorId": 82,
+ "hexString": "#5fff00",
+ "rgb": {
+ "r": 95,
+ "g": 255,
+ "b": 0
+ },
+ "hsl": {
+ "h": 97.6470588235294,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Chartreuse2"
+ },
+ {
+ "colorId": 83,
+ "hexString": "#5fff5f",
+ "rgb": {
+ "r": 95,
+ "g": 255,
+ "b": 95
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 68
+ },
+ "name": "SeaGreen2"
+ },
+ {
+ "colorId": 84,
+ "hexString": "#5fff87",
+ "rgb": {
+ "r": 95,
+ "g": 255,
+ "b": 135
+ },
+ "hsl": {
+ "h": 135,
+ "s": 100,
+ "l": 68
+ },
+ "name": "SeaGreen1"
+ },
+ {
+ "colorId": 85,
+ "hexString": "#5fffaf",
+ "rgb": {
+ "r": 95,
+ "g": 255,
+ "b": 175
+ },
+ "hsl": {
+ "h": 150,
+ "s": 100,
+ "l": 68
+ },
+ "name": "SeaGreen1"
+ },
+ {
+ "colorId": 86,
+ "hexString": "#5fffd7",
+ "rgb": {
+ "r": 95,
+ "g": 255,
+ "b": 215
+ },
+ "hsl": {
+ "h": 165,
+ "s": 100,
+ "l": 68
+ },
+ "name": "Aquamarine1"
+ },
+ {
+ "colorId": 87,
+ "hexString": "#5fffff",
+ "rgb": {
+ "r": 95,
+ "g": 255,
+ "b": 255
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 68
+ },
+ "name": "DarkSlateGray2"
+ },
+ {
+ "colorId": 88,
+ "hexString": "#870000",
+ "rgb": {
+ "r": 135,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 26
+ },
+ "name": "DarkRed"
+ },
+ {
+ "colorId": 89,
+ "hexString": "#87005f",
+ "rgb": {
+ "r": 135,
+ "g": 0,
+ "b": 95
+ },
+ "hsl": {
+ "h": 317.777777777778,
+ "s": 100,
+ "l": 26
+ },
+ "name": "DeepPink4"
+ },
+ {
+ "colorId": 90,
+ "hexString": "#870087",
+ "rgb": {
+ "r": 135,
+ "g": 0,
+ "b": 135
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 26
+ },
+ "name": "DarkMagenta"
+ },
+ {
+ "colorId": 91,
+ "hexString": "#8700af",
+ "rgb": {
+ "r": 135,
+ "g": 0,
+ "b": 175
+ },
+ "hsl": {
+ "h": 286.285714285714,
+ "s": 100,
+ "l": 34
+ },
+ "name": "DarkMagenta"
+ },
+ {
+ "colorId": 92,
+ "hexString": "#8700d7",
+ "rgb": {
+ "r": 135,
+ "g": 0,
+ "b": 215
+ },
+ "hsl": {
+ "h": 277.674418604651,
+ "s": 100,
+ "l": 42
+ },
+ "name": "DarkViolet"
+ },
+ {
+ "colorId": 93,
+ "hexString": "#8700ff",
+ "rgb": {
+ "r": 135,
+ "g": 0,
+ "b": 255
+ },
+ "hsl": {
+ "h": 271.764705882353,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Purple"
+ },
+ {
+ "colorId": 94,
+ "hexString": "#875f00",
+ "rgb": {
+ "r": 135,
+ "g": 95,
+ "b": 0
+ },
+ "hsl": {
+ "h": 42.2222222222222,
+ "s": 100,
+ "l": 26
+ },
+ "name": "Orange4"
+ },
+ {
+ "colorId": 95,
+ "hexString": "#875f5f",
+ "rgb": {
+ "r": 135,
+ "g": 95,
+ "b": 95
+ },
+ "hsl": {
+ "h": 0,
+ "s": 17,
+ "l": 45
+ },
+ "name": "LightPink4"
+ },
+ {
+ "colorId": 96,
+ "hexString": "#875f87",
+ "rgb": {
+ "r": 135,
+ "g": 95,
+ "b": 135
+ },
+ "hsl": {
+ "h": 300,
+ "s": 17,
+ "l": 45
+ },
+ "name": "Plum4"
+ },
+ {
+ "colorId": 97,
+ "hexString": "#875faf",
+ "rgb": {
+ "r": 135,
+ "g": 95,
+ "b": 175
+ },
+ "hsl": {
+ "h": 270,
+ "s": 33,
+ "l": 52
+ },
+ "name": "MediumPurple3"
+ },
+ {
+ "colorId": 98,
+ "hexString": "#875fd7",
+ "rgb": {
+ "r": 135,
+ "g": 95,
+ "b": 215
+ },
+ "hsl": {
+ "h": 260,
+ "s": 60,
+ "l": 60
+ },
+ "name": "MediumPurple3"
+ },
+ {
+ "colorId": 99,
+ "hexString": "#875fff",
+ "rgb": {
+ "r": 135,
+ "g": 95,
+ "b": 255
+ },
+ "hsl": {
+ "h": 255,
+ "s": 100,
+ "l": 68
+ },
+ "name": "SlateBlue1"
+ },
+ {
+ "colorId": 100,
+ "hexString": "#878700",
+ "rgb": {
+ "r": 135,
+ "g": 135,
+ "b": 0
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 26
+ },
+ "name": "Yellow4"
+ },
+ {
+ "colorId": 101,
+ "hexString": "#87875f",
+ "rgb": {
+ "r": 135,
+ "g": 135,
+ "b": 95
+ },
+ "hsl": {
+ "h": 60,
+ "s": 17,
+ "l": 45
+ },
+ "name": "Wheat4"
+ },
+ {
+ "colorId": 102,
+ "hexString": "#878787",
+ "rgb": {
+ "r": 135,
+ "g": 135,
+ "b": 135
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 52
+ },
+ "name": "Grey53"
+ },
+ {
+ "colorId": 103,
+ "hexString": "#8787af",
+ "rgb": {
+ "r": 135,
+ "g": 135,
+ "b": 175
+ },
+ "hsl": {
+ "h": 240,
+ "s": 20,
+ "l": 60
+ },
+ "name": "LightSlateGrey"
+ },
+ {
+ "colorId": 104,
+ "hexString": "#8787d7",
+ "rgb": {
+ "r": 135,
+ "g": 135,
+ "b": 215
+ },
+ "hsl": {
+ "h": 240,
+ "s": 50,
+ "l": 68
+ },
+ "name": "MediumPurple"
+ },
+ {
+ "colorId": 105,
+ "hexString": "#8787ff",
+ "rgb": {
+ "r": 135,
+ "g": 135,
+ "b": 255
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 76
+ },
+ "name": "LightSlateBlue"
+ },
+ {
+ "colorId": 106,
+ "hexString": "#87af00",
+ "rgb": {
+ "r": 135,
+ "g": 175,
+ "b": 0
+ },
+ "hsl": {
+ "h": 73.7142857142857,
+ "s": 100,
+ "l": 34
+ },
+ "name": "Yellow4"
+ },
+ {
+ "colorId": 107,
+ "hexString": "#87af5f",
+ "rgb": {
+ "r": 135,
+ "g": 175,
+ "b": 95
+ },
+ "hsl": {
+ "h": 90,
+ "s": 33,
+ "l": 52
+ },
+ "name": "DarkOliveGreen3"
+ },
+ {
+ "colorId": 108,
+ "hexString": "#87af87",
+ "rgb": {
+ "r": 135,
+ "g": 175,
+ "b": 135
+ },
+ "hsl": {
+ "h": 120,
+ "s": 20,
+ "l": 60
+ },
+ "name": "DarkSeaGreen"
+ },
+ {
+ "colorId": 109,
+ "hexString": "#87afaf",
+ "rgb": {
+ "r": 135,
+ "g": 175,
+ "b": 175
+ },
+ "hsl": {
+ "h": 180,
+ "s": 20,
+ "l": 60
+ },
+ "name": "LightSkyBlue3"
+ },
+ {
+ "colorId": 110,
+ "hexString": "#87afd7",
+ "rgb": {
+ "r": 135,
+ "g": 175,
+ "b": 215
+ },
+ "hsl": {
+ "h": 210,
+ "s": 50,
+ "l": 68
+ },
+ "name": "LightSkyBlue3"
+ },
+ {
+ "colorId": 111,
+ "hexString": "#87afff",
+ "rgb": {
+ "r": 135,
+ "g": 175,
+ "b": 255
+ },
+ "hsl": {
+ "h": 220,
+ "s": 100,
+ "l": 76
+ },
+ "name": "SkyBlue2"
+ },
+ {
+ "colorId": 112,
+ "hexString": "#87d700",
+ "rgb": {
+ "r": 135,
+ "g": 215,
+ "b": 0
+ },
+ "hsl": {
+ "h": 82.3255813953488,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Chartreuse2"
+ },
+ {
+ "colorId": 113,
+ "hexString": "#87d75f",
+ "rgb": {
+ "r": 135,
+ "g": 215,
+ "b": 95
+ },
+ "hsl": {
+ "h": 100,
+ "s": 60,
+ "l": 60
+ },
+ "name": "DarkOliveGreen3"
+ },
+ {
+ "colorId": 114,
+ "hexString": "#87d787",
+ "rgb": {
+ "r": 135,
+ "g": 215,
+ "b": 135
+ },
+ "hsl": {
+ "h": 120,
+ "s": 50,
+ "l": 68
+ },
+ "name": "PaleGreen3"
+ },
+ {
+ "colorId": 115,
+ "hexString": "#87d7af",
+ "rgb": {
+ "r": 135,
+ "g": 215,
+ "b": 175
+ },
+ "hsl": {
+ "h": 150,
+ "s": 50,
+ "l": 68
+ },
+ "name": "DarkSeaGreen3"
+ },
+ {
+ "colorId": 116,
+ "hexString": "#87d7d7",
+ "rgb": {
+ "r": 135,
+ "g": 215,
+ "b": 215
+ },
+ "hsl": {
+ "h": 180,
+ "s": 50,
+ "l": 68
+ },
+ "name": "DarkSlateGray3"
+ },
+ {
+ "colorId": 117,
+ "hexString": "#87d7ff",
+ "rgb": {
+ "r": 135,
+ "g": 215,
+ "b": 255
+ },
+ "hsl": {
+ "h": 200,
+ "s": 100,
+ "l": 76
+ },
+ "name": "SkyBlue1"
+ },
+ {
+ "colorId": 118,
+ "hexString": "#87ff00",
+ "rgb": {
+ "r": 135,
+ "g": 255,
+ "b": 0
+ },
+ "hsl": {
+ "h": 88.2352941176471,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Chartreuse1"
+ },
+ {
+ "colorId": 119,
+ "hexString": "#87ff5f",
+ "rgb": {
+ "r": 135,
+ "g": 255,
+ "b": 95
+ },
+ "hsl": {
+ "h": 105,
+ "s": 100,
+ "l": 68
+ },
+ "name": "LightGreen"
+ },
+ {
+ "colorId": 120,
+ "hexString": "#87ff87",
+ "rgb": {
+ "r": 135,
+ "g": 255,
+ "b": 135
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 76
+ },
+ "name": "LightGreen"
+ },
+ {
+ "colorId": 121,
+ "hexString": "#87ffaf",
+ "rgb": {
+ "r": 135,
+ "g": 255,
+ "b": 175
+ },
+ "hsl": {
+ "h": 140,
+ "s": 100,
+ "l": 76
+ },
+ "name": "PaleGreen1"
+ },
+ {
+ "colorId": 122,
+ "hexString": "#87ffd7",
+ "rgb": {
+ "r": 135,
+ "g": 255,
+ "b": 215
+ },
+ "hsl": {
+ "h": 160,
+ "s": 100,
+ "l": 76
+ },
+ "name": "Aquamarine1"
+ },
+ {
+ "colorId": 123,
+ "hexString": "#87ffff",
+ "rgb": {
+ "r": 135,
+ "g": 255,
+ "b": 255
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 76
+ },
+ "name": "DarkSlateGray1"
+ },
+ {
+ "colorId": 124,
+ "hexString": "#af0000",
+ "rgb": {
+ "r": 175,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 34
+ },
+ "name": "Red3"
+ },
+ {
+ "colorId": 125,
+ "hexString": "#af005f",
+ "rgb": {
+ "r": 175,
+ "g": 0,
+ "b": 95
+ },
+ "hsl": {
+ "h": 327.428571428571,
+ "s": 100,
+ "l": 34
+ },
+ "name": "DeepPink4"
+ },
+ {
+ "colorId": 126,
+ "hexString": "#af0087",
+ "rgb": {
+ "r": 175,
+ "g": 0,
+ "b": 135
+ },
+ "hsl": {
+ "h": 313.714285714286,
+ "s": 100,
+ "l": 34
+ },
+ "name": "MediumVioletRed"
+ },
+ {
+ "colorId": 127,
+ "hexString": "#af00af",
+ "rgb": {
+ "r": 175,
+ "g": 0,
+ "b": 175
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 34
+ },
+ "name": "Magenta3"
+ },
+ {
+ "colorId": 128,
+ "hexString": "#af00d7",
+ "rgb": {
+ "r": 175,
+ "g": 0,
+ "b": 215
+ },
+ "hsl": {
+ "h": 288.837209302326,
+ "s": 100,
+ "l": 42
+ },
+ "name": "DarkViolet"
+ },
+ {
+ "colorId": 129,
+ "hexString": "#af00ff",
+ "rgb": {
+ "r": 175,
+ "g": 0,
+ "b": 255
+ },
+ "hsl": {
+ "h": 281.176470588235,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Purple"
+ },
+ {
+ "colorId": 130,
+ "hexString": "#af5f00",
+ "rgb": {
+ "r": 175,
+ "g": 95,
+ "b": 0
+ },
+ "hsl": {
+ "h": 32.5714285714286,
+ "s": 100,
+ "l": 34
+ },
+ "name": "DarkOrange3"
+ },
+ {
+ "colorId": 131,
+ "hexString": "#af5f5f",
+ "rgb": {
+ "r": 175,
+ "g": 95,
+ "b": 95
+ },
+ "hsl": {
+ "h": 0,
+ "s": 33,
+ "l": 52
+ },
+ "name": "IndianRed"
+ },
+ {
+ "colorId": 132,
+ "hexString": "#af5f87",
+ "rgb": {
+ "r": 175,
+ "g": 95,
+ "b": 135
+ },
+ "hsl": {
+ "h": 330,
+ "s": 33,
+ "l": 52
+ },
+ "name": "HotPink3"
+ },
+ {
+ "colorId": 133,
+ "hexString": "#af5faf",
+ "rgb": {
+ "r": 175,
+ "g": 95,
+ "b": 175
+ },
+ "hsl": {
+ "h": 300,
+ "s": 33,
+ "l": 52
+ },
+ "name": "MediumOrchid3"
+ },
+ {
+ "colorId": 134,
+ "hexString": "#af5fd7",
+ "rgb": {
+ "r": 175,
+ "g": 95,
+ "b": 215
+ },
+ "hsl": {
+ "h": 280,
+ "s": 60,
+ "l": 60
+ },
+ "name": "MediumOrchid"
+ },
+ {
+ "colorId": 135,
+ "hexString": "#af5fff",
+ "rgb": {
+ "r": 175,
+ "g": 95,
+ "b": 255
+ },
+ "hsl": {
+ "h": 270,
+ "s": 100,
+ "l": 68
+ },
+ "name": "MediumPurple2"
+ },
+ {
+ "colorId": 136,
+ "hexString": "#af8700",
+ "rgb": {
+ "r": 175,
+ "g": 135,
+ "b": 0
+ },
+ "hsl": {
+ "h": 46.2857142857143,
+ "s": 100,
+ "l": 34
+ },
+ "name": "DarkGoldenrod"
+ },
+ {
+ "colorId": 137,
+ "hexString": "#af875f",
+ "rgb": {
+ "r": 175,
+ "g": 135,
+ "b": 95
+ },
+ "hsl": {
+ "h": 30,
+ "s": 33,
+ "l": 52
+ },
+ "name": "LightSalmon3"
+ },
+ {
+ "colorId": 138,
+ "hexString": "#af8787",
+ "rgb": {
+ "r": 175,
+ "g": 135,
+ "b": 135
+ },
+ "hsl": {
+ "h": 0,
+ "s": 20,
+ "l": 60
+ },
+ "name": "RosyBrown"
+ },
+ {
+ "colorId": 139,
+ "hexString": "#af87af",
+ "rgb": {
+ "r": 175,
+ "g": 135,
+ "b": 175
+ },
+ "hsl": {
+ "h": 300,
+ "s": 20,
+ "l": 60
+ },
+ "name": "Grey63"
+ },
+ {
+ "colorId": 140,
+ "hexString": "#af87d7",
+ "rgb": {
+ "r": 175,
+ "g": 135,
+ "b": 215
+ },
+ "hsl": {
+ "h": 270,
+ "s": 50,
+ "l": 68
+ },
+ "name": "MediumPurple2"
+ },
+ {
+ "colorId": 141,
+ "hexString": "#af87ff",
+ "rgb": {
+ "r": 175,
+ "g": 135,
+ "b": 255
+ },
+ "hsl": {
+ "h": 260,
+ "s": 100,
+ "l": 76
+ },
+ "name": "MediumPurple1"
+ },
+ {
+ "colorId": 142,
+ "hexString": "#afaf00",
+ "rgb": {
+ "r": 175,
+ "g": 175,
+ "b": 0
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 34
+ },
+ "name": "Gold3"
+ },
+ {
+ "colorId": 143,
+ "hexString": "#afaf5f",
+ "rgb": {
+ "r": 175,
+ "g": 175,
+ "b": 95
+ },
+ "hsl": {
+ "h": 60,
+ "s": 33,
+ "l": 52
+ },
+ "name": "DarkKhaki"
+ },
+ {
+ "colorId": 144,
+ "hexString": "#afaf87",
+ "rgb": {
+ "r": 175,
+ "g": 175,
+ "b": 135
+ },
+ "hsl": {
+ "h": 60,
+ "s": 20,
+ "l": 60
+ },
+ "name": "NavajoWhite3"
+ },
+ {
+ "colorId": 145,
+ "hexString": "#afafaf",
+ "rgb": {
+ "r": 175,
+ "g": 175,
+ "b": 175
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 68
+ },
+ "name": "Grey69"
+ },
+ {
+ "colorId": 146,
+ "hexString": "#afafd7",
+ "rgb": {
+ "r": 175,
+ "g": 175,
+ "b": 215
+ },
+ "hsl": {
+ "h": 240,
+ "s": 33,
+ "l": 76
+ },
+ "name": "LightSteelBlue3"
+ },
+ {
+ "colorId": 147,
+ "hexString": "#afafff",
+ "rgb": {
+ "r": 175,
+ "g": 175,
+ "b": 255
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 84
+ },
+ "name": "LightSteelBlue"
+ },
+ {
+ "colorId": 148,
+ "hexString": "#afd700",
+ "rgb": {
+ "r": 175,
+ "g": 215,
+ "b": 0
+ },
+ "hsl": {
+ "h": 71.1627906976744,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Yellow3"
+ },
+ {
+ "colorId": 149,
+ "hexString": "#afd75f",
+ "rgb": {
+ "r": 175,
+ "g": 215,
+ "b": 95
+ },
+ "hsl": {
+ "h": 80,
+ "s": 60,
+ "l": 60
+ },
+ "name": "DarkOliveGreen3"
+ },
+ {
+ "colorId": 150,
+ "hexString": "#afd787",
+ "rgb": {
+ "r": 175,
+ "g": 215,
+ "b": 135
+ },
+ "hsl": {
+ "h": 90,
+ "s": 50,
+ "l": 68
+ },
+ "name": "DarkSeaGreen3"
+ },
+ {
+ "colorId": 151,
+ "hexString": "#afd7af",
+ "rgb": {
+ "r": 175,
+ "g": 215,
+ "b": 175
+ },
+ "hsl": {
+ "h": 120,
+ "s": 33,
+ "l": 76
+ },
+ "name": "DarkSeaGreen2"
+ },
+ {
+ "colorId": 152,
+ "hexString": "#afd7d7",
+ "rgb": {
+ "r": 175,
+ "g": 215,
+ "b": 215
+ },
+ "hsl": {
+ "h": 180,
+ "s": 33,
+ "l": 76
+ },
+ "name": "LightCyan3"
+ },
+ {
+ "colorId": 153,
+ "hexString": "#afd7ff",
+ "rgb": {
+ "r": 175,
+ "g": 215,
+ "b": 255
+ },
+ "hsl": {
+ "h": 210,
+ "s": 100,
+ "l": 84
+ },
+ "name": "LightSkyBlue1"
+ },
+ {
+ "colorId": 154,
+ "hexString": "#afff00",
+ "rgb": {
+ "r": 175,
+ "g": 255,
+ "b": 0
+ },
+ "hsl": {
+ "h": 78.8235294117647,
+ "s": 100,
+ "l": 50
+ },
+ "name": "GreenYellow"
+ },
+ {
+ "colorId": 155,
+ "hexString": "#afff5f",
+ "rgb": {
+ "r": 175,
+ "g": 255,
+ "b": 95
+ },
+ "hsl": {
+ "h": 90,
+ "s": 100,
+ "l": 68
+ },
+ "name": "DarkOliveGreen2"
+ },
+ {
+ "colorId": 156,
+ "hexString": "#afff87",
+ "rgb": {
+ "r": 175,
+ "g": 255,
+ "b": 135
+ },
+ "hsl": {
+ "h": 100,
+ "s": 100,
+ "l": 76
+ },
+ "name": "PaleGreen1"
+ },
+ {
+ "colorId": 157,
+ "hexString": "#afffaf",
+ "rgb": {
+ "r": 175,
+ "g": 255,
+ "b": 175
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 84
+ },
+ "name": "DarkSeaGreen2"
+ },
+ {
+ "colorId": 158,
+ "hexString": "#afffd7",
+ "rgb": {
+ "r": 175,
+ "g": 255,
+ "b": 215
+ },
+ "hsl": {
+ "h": 150,
+ "s": 100,
+ "l": 84
+ },
+ "name": "DarkSeaGreen1"
+ },
+ {
+ "colorId": 159,
+ "hexString": "#afffff",
+ "rgb": {
+ "r": 175,
+ "g": 255,
+ "b": 255
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 84
+ },
+ "name": "PaleTurquoise1"
+ },
+ {
+ "colorId": 160,
+ "hexString": "#d70000",
+ "rgb": {
+ "r": 215,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Red3"
+ },
+ {
+ "colorId": 161,
+ "hexString": "#d7005f",
+ "rgb": {
+ "r": 215,
+ "g": 0,
+ "b": 95
+ },
+ "hsl": {
+ "h": 333.488372093023,
+ "s": 100,
+ "l": 42
+ },
+ "name": "DeepPink3"
+ },
+ {
+ "colorId": 162,
+ "hexString": "#d70087",
+ "rgb": {
+ "r": 215,
+ "g": 0,
+ "b": 135
+ },
+ "hsl": {
+ "h": 322.325581395349,
+ "s": 100,
+ "l": 42
+ },
+ "name": "DeepPink3"
+ },
+ {
+ "colorId": 163,
+ "hexString": "#d700af",
+ "rgb": {
+ "r": 215,
+ "g": 0,
+ "b": 175
+ },
+ "hsl": {
+ "h": 311.162790697674,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Magenta3"
+ },
+ {
+ "colorId": 164,
+ "hexString": "#d700d7",
+ "rgb": {
+ "r": 215,
+ "g": 0,
+ "b": 215
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Magenta3"
+ },
+ {
+ "colorId": 165,
+ "hexString": "#d700ff",
+ "rgb": {
+ "r": 215,
+ "g": 0,
+ "b": 255
+ },
+ "hsl": {
+ "h": 290.588235294118,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Magenta2"
+ },
+ {
+ "colorId": 166,
+ "hexString": "#d75f00",
+ "rgb": {
+ "r": 215,
+ "g": 95,
+ "b": 0
+ },
+ "hsl": {
+ "h": 26.5116279069767,
+ "s": 100,
+ "l": 42
+ },
+ "name": "DarkOrange3"
+ },
+ {
+ "colorId": 167,
+ "hexString": "#d75f5f",
+ "rgb": {
+ "r": 215,
+ "g": 95,
+ "b": 95
+ },
+ "hsl": {
+ "h": 0,
+ "s": 60,
+ "l": 60
+ },
+ "name": "IndianRed"
+ },
+ {
+ "colorId": 168,
+ "hexString": "#d75f87",
+ "rgb": {
+ "r": 215,
+ "g": 95,
+ "b": 135
+ },
+ "hsl": {
+ "h": 340,
+ "s": 60,
+ "l": 60
+ },
+ "name": "HotPink3"
+ },
+ {
+ "colorId": 169,
+ "hexString": "#d75faf",
+ "rgb": {
+ "r": 215,
+ "g": 95,
+ "b": 175
+ },
+ "hsl": {
+ "h": 320,
+ "s": 60,
+ "l": 60
+ },
+ "name": "HotPink2"
+ },
+ {
+ "colorId": 170,
+ "hexString": "#d75fd7",
+ "rgb": {
+ "r": 215,
+ "g": 95,
+ "b": 215
+ },
+ "hsl": {
+ "h": 300,
+ "s": 60,
+ "l": 60
+ },
+ "name": "Orchid"
+ },
+ {
+ "colorId": 171,
+ "hexString": "#d75fff",
+ "rgb": {
+ "r": 215,
+ "g": 95,
+ "b": 255
+ },
+ "hsl": {
+ "h": 285,
+ "s": 100,
+ "l": 68
+ },
+ "name": "MediumOrchid1"
+ },
+ {
+ "colorId": 172,
+ "hexString": "#d78700",
+ "rgb": {
+ "r": 215,
+ "g": 135,
+ "b": 0
+ },
+ "hsl": {
+ "h": 37.6744186046512,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Orange3"
+ },
+ {
+ "colorId": 173,
+ "hexString": "#d7875f",
+ "rgb": {
+ "r": 215,
+ "g": 135,
+ "b": 95
+ },
+ "hsl": {
+ "h": 20,
+ "s": 60,
+ "l": 60
+ },
+ "name": "LightSalmon3"
+ },
+ {
+ "colorId": 174,
+ "hexString": "#d78787",
+ "rgb": {
+ "r": 215,
+ "g": 135,
+ "b": 135
+ },
+ "hsl": {
+ "h": 0,
+ "s": 50,
+ "l": 68
+ },
+ "name": "LightPink3"
+ },
+ {
+ "colorId": 175,
+ "hexString": "#d787af",
+ "rgb": {
+ "r": 215,
+ "g": 135,
+ "b": 175
+ },
+ "hsl": {
+ "h": 330,
+ "s": 50,
+ "l": 68
+ },
+ "name": "Pink3"
+ },
+ {
+ "colorId": 176,
+ "hexString": "#d787d7",
+ "rgb": {
+ "r": 215,
+ "g": 135,
+ "b": 215
+ },
+ "hsl": {
+ "h": 300,
+ "s": 50,
+ "l": 68
+ },
+ "name": "Plum3"
+ },
+ {
+ "colorId": 177,
+ "hexString": "#d787ff",
+ "rgb": {
+ "r": 215,
+ "g": 135,
+ "b": 255
+ },
+ "hsl": {
+ "h": 280,
+ "s": 100,
+ "l": 76
+ },
+ "name": "Violet"
+ },
+ {
+ "colorId": 178,
+ "hexString": "#d7af00",
+ "rgb": {
+ "r": 215,
+ "g": 175,
+ "b": 0
+ },
+ "hsl": {
+ "h": 48.8372093023256,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Gold3"
+ },
+ {
+ "colorId": 179,
+ "hexString": "#d7af5f",
+ "rgb": {
+ "r": 215,
+ "g": 175,
+ "b": 95
+ },
+ "hsl": {
+ "h": 40,
+ "s": 60,
+ "l": 60
+ },
+ "name": "LightGoldenrod3"
+ },
+ {
+ "colorId": 180,
+ "hexString": "#d7af87",
+ "rgb": {
+ "r": 215,
+ "g": 175,
+ "b": 135
+ },
+ "hsl": {
+ "h": 30,
+ "s": 50,
+ "l": 68
+ },
+ "name": "Tan"
+ },
+ {
+ "colorId": 181,
+ "hexString": "#d7afaf",
+ "rgb": {
+ "r": 215,
+ "g": 175,
+ "b": 175
+ },
+ "hsl": {
+ "h": 0,
+ "s": 33,
+ "l": 76
+ },
+ "name": "MistyRose3"
+ },
+ {
+ "colorId": 182,
+ "hexString": "#d7afd7",
+ "rgb": {
+ "r": 215,
+ "g": 175,
+ "b": 215
+ },
+ "hsl": {
+ "h": 300,
+ "s": 33,
+ "l": 76
+ },
+ "name": "Thistle3"
+ },
+ {
+ "colorId": 183,
+ "hexString": "#d7afff",
+ "rgb": {
+ "r": 215,
+ "g": 175,
+ "b": 255
+ },
+ "hsl": {
+ "h": 270,
+ "s": 100,
+ "l": 84
+ },
+ "name": "Plum2"
+ },
+ {
+ "colorId": 184,
+ "hexString": "#d7d700",
+ "rgb": {
+ "r": 215,
+ "g": 215,
+ "b": 0
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 42
+ },
+ "name": "Yellow3"
+ },
+ {
+ "colorId": 185,
+ "hexString": "#d7d75f",
+ "rgb": {
+ "r": 215,
+ "g": 215,
+ "b": 95
+ },
+ "hsl": {
+ "h": 60,
+ "s": 60,
+ "l": 60
+ },
+ "name": "Khaki3"
+ },
+ {
+ "colorId": 186,
+ "hexString": "#d7d787",
+ "rgb": {
+ "r": 215,
+ "g": 215,
+ "b": 135
+ },
+ "hsl": {
+ "h": 60,
+ "s": 50,
+ "l": 68
+ },
+ "name": "LightGoldenrod2"
+ },
+ {
+ "colorId": 187,
+ "hexString": "#d7d7af",
+ "rgb": {
+ "r": 215,
+ "g": 215,
+ "b": 175
+ },
+ "hsl": {
+ "h": 60,
+ "s": 33,
+ "l": 76
+ },
+ "name": "LightYellow3"
+ },
+ {
+ "colorId": 188,
+ "hexString": "#d7d7d7",
+ "rgb": {
+ "r": 215,
+ "g": 215,
+ "b": 215
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 84
+ },
+ "name": "Grey84"
+ },
+ {
+ "colorId": 189,
+ "hexString": "#d7d7ff",
+ "rgb": {
+ "r": 215,
+ "g": 215,
+ "b": 255
+ },
+ "hsl": {
+ "h": 240,
+ "s": 100,
+ "l": 92
+ },
+ "name": "LightSteelBlue1"
+ },
+ {
+ "colorId": 190,
+ "hexString": "#d7ff00",
+ "rgb": {
+ "r": 215,
+ "g": 255,
+ "b": 0
+ },
+ "hsl": {
+ "h": 69.4117647058823,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Yellow2"
+ },
+ {
+ "colorId": 191,
+ "hexString": "#d7ff5f",
+ "rgb": {
+ "r": 215,
+ "g": 255,
+ "b": 95
+ },
+ "hsl": {
+ "h": 75,
+ "s": 100,
+ "l": 68
+ },
+ "name": "DarkOliveGreen1"
+ },
+ {
+ "colorId": 192,
+ "hexString": "#d7ff87",
+ "rgb": {
+ "r": 215,
+ "g": 255,
+ "b": 135
+ },
+ "hsl": {
+ "h": 80,
+ "s": 100,
+ "l": 76
+ },
+ "name": "DarkOliveGreen1"
+ },
+ {
+ "colorId": 193,
+ "hexString": "#d7ffaf",
+ "rgb": {
+ "r": 215,
+ "g": 255,
+ "b": 175
+ },
+ "hsl": {
+ "h": 90,
+ "s": 100,
+ "l": 84
+ },
+ "name": "DarkSeaGreen1"
+ },
+ {
+ "colorId": 194,
+ "hexString": "#d7ffd7",
+ "rgb": {
+ "r": 215,
+ "g": 255,
+ "b": 215
+ },
+ "hsl": {
+ "h": 120,
+ "s": 100,
+ "l": 92
+ },
+ "name": "Honeydew2"
+ },
+ {
+ "colorId": 195,
+ "hexString": "#d7ffff",
+ "rgb": {
+ "r": 215,
+ "g": 255,
+ "b": 255
+ },
+ "hsl": {
+ "h": 180,
+ "s": 100,
+ "l": 92
+ },
+ "name": "LightCyan1"
+ },
+ {
+ "colorId": 196,
+ "hexString": "#ff0000",
+ "rgb": {
+ "r": 255,
+ "g": 0,
+ "b": 0
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Red1"
+ },
+ {
+ "colorId": 197,
+ "hexString": "#ff005f",
+ "rgb": {
+ "r": 255,
+ "g": 0,
+ "b": 95
+ },
+ "hsl": {
+ "h": 337.647058823529,
+ "s": 100,
+ "l": 50
+ },
+ "name": "DeepPink2"
+ },
+ {
+ "colorId": 198,
+ "hexString": "#ff0087",
+ "rgb": {
+ "r": 255,
+ "g": 0,
+ "b": 135
+ },
+ "hsl": {
+ "h": 328.235294117647,
+ "s": 100,
+ "l": 50
+ },
+ "name": "DeepPink1"
+ },
+ {
+ "colorId": 199,
+ "hexString": "#ff00af",
+ "rgb": {
+ "r": 255,
+ "g": 0,
+ "b": 175
+ },
+ "hsl": {
+ "h": 318.823529411765,
+ "s": 100,
+ "l": 50
+ },
+ "name": "DeepPink1"
+ },
+ {
+ "colorId": 200,
+ "hexString": "#ff00d7",
+ "rgb": {
+ "r": 255,
+ "g": 0,
+ "b": 215
+ },
+ "hsl": {
+ "h": 309.411764705882,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Magenta2"
+ },
+ {
+ "colorId": 201,
+ "hexString": "#ff00ff",
+ "rgb": {
+ "r": 255,
+ "g": 0,
+ "b": 255
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Magenta1"
+ },
+ {
+ "colorId": 202,
+ "hexString": "#ff5f00",
+ "rgb": {
+ "r": 255,
+ "g": 95,
+ "b": 0
+ },
+ "hsl": {
+ "h": 22.3529411764706,
+ "s": 100,
+ "l": 50
+ },
+ "name": "OrangeRed1"
+ },
+ {
+ "colorId": 203,
+ "hexString": "#ff5f5f",
+ "rgb": {
+ "r": 255,
+ "g": 95,
+ "b": 95
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 68
+ },
+ "name": "IndianRed1"
+ },
+ {
+ "colorId": 204,
+ "hexString": "#ff5f87",
+ "rgb": {
+ "r": 255,
+ "g": 95,
+ "b": 135
+ },
+ "hsl": {
+ "h": 345,
+ "s": 100,
+ "l": 68
+ },
+ "name": "IndianRed1"
+ },
+ {
+ "colorId": 205,
+ "hexString": "#ff5faf",
+ "rgb": {
+ "r": 255,
+ "g": 95,
+ "b": 175
+ },
+ "hsl": {
+ "h": 330,
+ "s": 100,
+ "l": 68
+ },
+ "name": "HotPink"
+ },
+ {
+ "colorId": 206,
+ "hexString": "#ff5fd7",
+ "rgb": {
+ "r": 255,
+ "g": 95,
+ "b": 215
+ },
+ "hsl": {
+ "h": 315,
+ "s": 100,
+ "l": 68
+ },
+ "name": "HotPink"
+ },
+ {
+ "colorId": 207,
+ "hexString": "#ff5fff",
+ "rgb": {
+ "r": 255,
+ "g": 95,
+ "b": 255
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 68
+ },
+ "name": "MediumOrchid1"
+ },
+ {
+ "colorId": 208,
+ "hexString": "#ff8700",
+ "rgb": {
+ "r": 255,
+ "g": 135,
+ "b": 0
+ },
+ "hsl": {
+ "h": 31.7647058823529,
+ "s": 100,
+ "l": 50
+ },
+ "name": "DarkOrange"
+ },
+ {
+ "colorId": 209,
+ "hexString": "#ff875f",
+ "rgb": {
+ "r": 255,
+ "g": 135,
+ "b": 95
+ },
+ "hsl": {
+ "h": 15,
+ "s": 100,
+ "l": 68
+ },
+ "name": "Salmon1"
+ },
+ {
+ "colorId": 210,
+ "hexString": "#ff8787",
+ "rgb": {
+ "r": 255,
+ "g": 135,
+ "b": 135
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 76
+ },
+ "name": "LightCoral"
+ },
+ {
+ "colorId": 211,
+ "hexString": "#ff87af",
+ "rgb": {
+ "r": 255,
+ "g": 135,
+ "b": 175
+ },
+ "hsl": {
+ "h": 340,
+ "s": 100,
+ "l": 76
+ },
+ "name": "PaleVioletRed1"
+ },
+ {
+ "colorId": 212,
+ "hexString": "#ff87d7",
+ "rgb": {
+ "r": 255,
+ "g": 135,
+ "b": 215
+ },
+ "hsl": {
+ "h": 320,
+ "s": 100,
+ "l": 76
+ },
+ "name": "Orchid2"
+ },
+ {
+ "colorId": 213,
+ "hexString": "#ff87ff",
+ "rgb": {
+ "r": 255,
+ "g": 135,
+ "b": 255
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 76
+ },
+ "name": "Orchid1"
+ },
+ {
+ "colorId": 214,
+ "hexString": "#ffaf00",
+ "rgb": {
+ "r": 255,
+ "g": 175,
+ "b": 0
+ },
+ "hsl": {
+ "h": 41.1764705882353,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Orange1"
+ },
+ {
+ "colorId": 215,
+ "hexString": "#ffaf5f",
+ "rgb": {
+ "r": 255,
+ "g": 175,
+ "b": 95
+ },
+ "hsl": {
+ "h": 30,
+ "s": 100,
+ "l": 68
+ },
+ "name": "SandyBrown"
+ },
+ {
+ "colorId": 216,
+ "hexString": "#ffaf87",
+ "rgb": {
+ "r": 255,
+ "g": 175,
+ "b": 135
+ },
+ "hsl": {
+ "h": 20,
+ "s": 100,
+ "l": 76
+ },
+ "name": "LightSalmon1"
+ },
+ {
+ "colorId": 217,
+ "hexString": "#ffafaf",
+ "rgb": {
+ "r": 255,
+ "g": 175,
+ "b": 175
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 84
+ },
+ "name": "LightPink1"
+ },
+ {
+ "colorId": 218,
+ "hexString": "#ffafd7",
+ "rgb": {
+ "r": 255,
+ "g": 175,
+ "b": 215
+ },
+ "hsl": {
+ "h": 330,
+ "s": 100,
+ "l": 84
+ },
+ "name": "Pink1"
+ },
+ {
+ "colorId": 219,
+ "hexString": "#ffafff",
+ "rgb": {
+ "r": 255,
+ "g": 175,
+ "b": 255
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 84
+ },
+ "name": "Plum1"
+ },
+ {
+ "colorId": 220,
+ "hexString": "#ffd700",
+ "rgb": {
+ "r": 255,
+ "g": 215,
+ "b": 0
+ },
+ "hsl": {
+ "h": 50.5882352941176,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Gold1"
+ },
+ {
+ "colorId": 221,
+ "hexString": "#ffd75f",
+ "rgb": {
+ "r": 255,
+ "g": 215,
+ "b": 95
+ },
+ "hsl": {
+ "h": 45,
+ "s": 100,
+ "l": 68
+ },
+ "name": "LightGoldenrod2"
+ },
+ {
+ "colorId": 222,
+ "hexString": "#ffd787",
+ "rgb": {
+ "r": 255,
+ "g": 215,
+ "b": 135
+ },
+ "hsl": {
+ "h": 40,
+ "s": 100,
+ "l": 76
+ },
+ "name": "LightGoldenrod2"
+ },
+ {
+ "colorId": 223,
+ "hexString": "#ffd7af",
+ "rgb": {
+ "r": 255,
+ "g": 215,
+ "b": 175
+ },
+ "hsl": {
+ "h": 30,
+ "s": 100,
+ "l": 84
+ },
+ "name": "NavajoWhite1"
+ },
+ {
+ "colorId": 224,
+ "hexString": "#ffd7d7",
+ "rgb": {
+ "r": 255,
+ "g": 215,
+ "b": 215
+ },
+ "hsl": {
+ "h": 0,
+ "s": 100,
+ "l": 92
+ },
+ "name": "MistyRose1"
+ },
+ {
+ "colorId": 225,
+ "hexString": "#ffd7ff",
+ "rgb": {
+ "r": 255,
+ "g": 215,
+ "b": 255
+ },
+ "hsl": {
+ "h": 300,
+ "s": 100,
+ "l": 92
+ },
+ "name": "Thistle1"
+ },
+ {
+ "colorId": 226,
+ "hexString": "#ffff00",
+ "rgb": {
+ "r": 255,
+ "g": 255,
+ "b": 0
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 50
+ },
+ "name": "Yellow1"
+ },
+ {
+ "colorId": 227,
+ "hexString": "#ffff5f",
+ "rgb": {
+ "r": 255,
+ "g": 255,
+ "b": 95
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 68
+ },
+ "name": "LightGoldenrod1"
+ },
+ {
+ "colorId": 228,
+ "hexString": "#ffff87",
+ "rgb": {
+ "r": 255,
+ "g": 255,
+ "b": 135
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 76
+ },
+ "name": "Khaki1"
+ },
+ {
+ "colorId": 229,
+ "hexString": "#ffffaf",
+ "rgb": {
+ "r": 255,
+ "g": 255,
+ "b": 175
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 84
+ },
+ "name": "Wheat1"
+ },
+ {
+ "colorId": 230,
+ "hexString": "#ffffd7",
+ "rgb": {
+ "r": 255,
+ "g": 255,
+ "b": 215
+ },
+ "hsl": {
+ "h": 60,
+ "s": 100,
+ "l": 92
+ },
+ "name": "Cornsilk1"
+ },
+ {
+ "colorId": 231,
+ "hexString": "#ffffff",
+ "rgb": {
+ "r": 255,
+ "g": 255,
+ "b": 255
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 100
+ },
+ "name": "Grey100"
+ },
+ {
+ "colorId": 232,
+ "hexString": "#080808",
+ "rgb": {
+ "r": 8,
+ "g": 8,
+ "b": 8
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 3
+ },
+ "name": "Grey3"
+ },
+ {
+ "colorId": 233,
+ "hexString": "#121212",
+ "rgb": {
+ "r": 18,
+ "g": 18,
+ "b": 18
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 7
+ },
+ "name": "Grey7"
+ },
+ {
+ "colorId": 234,
+ "hexString": "#1c1c1c",
+ "rgb": {
+ "r": 28,
+ "g": 28,
+ "b": 28
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 10
+ },
+ "name": "Grey11"
+ },
+ {
+ "colorId": 235,
+ "hexString": "#262626",
+ "rgb": {
+ "r": 38,
+ "g": 38,
+ "b": 38
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 14
+ },
+ "name": "Grey15"
+ },
+ {
+ "colorId": 236,
+ "hexString": "#303030",
+ "rgb": {
+ "r": 48,
+ "g": 48,
+ "b": 48
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 18
+ },
+ "name": "Grey19"
+ },
+ {
+ "colorId": 237,
+ "hexString": "#3a3a3a",
+ "rgb": {
+ "r": 58,
+ "g": 58,
+ "b": 58
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 22
+ },
+ "name": "Grey23"
+ },
+ {
+ "colorId": 238,
+ "hexString": "#444444",
+ "rgb": {
+ "r": 68,
+ "g": 68,
+ "b": 68
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 26
+ },
+ "name": "Grey27"
+ },
+ {
+ "colorId": 239,
+ "hexString": "#4e4e4e",
+ "rgb": {
+ "r": 78,
+ "g": 78,
+ "b": 78
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 30
+ },
+ "name": "Grey30"
+ },
+ {
+ "colorId": 240,
+ "hexString": "#585858",
+ "rgb": {
+ "r": 88,
+ "g": 88,
+ "b": 88
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 34
+ },
+ "name": "Grey35"
+ },
+ {
+ "colorId": 241,
+ "hexString": "#626262",
+ "rgb": {
+ "r": 98,
+ "g": 98,
+ "b": 98
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 37
+ },
+ "name": "Grey39"
+ },
+ {
+ "colorId": 242,
+ "hexString": "#6c6c6c",
+ "rgb": {
+ "r": 108,
+ "g": 108,
+ "b": 108
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 40
+ },
+ "name": "Grey42"
+ },
+ {
+ "colorId": 243,
+ "hexString": "#767676",
+ "rgb": {
+ "r": 118,
+ "g": 118,
+ "b": 118
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 46
+ },
+ "name": "Grey46"
+ },
+ {
+ "colorId": 244,
+ "hexString": "#808080",
+ "rgb": {
+ "r": 128,
+ "g": 128,
+ "b": 128
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 50
+ },
+ "name": "Grey50"
+ },
+ {
+ "colorId": 245,
+ "hexString": "#8a8a8a",
+ "rgb": {
+ "r": 138,
+ "g": 138,
+ "b": 138
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 54
+ },
+ "name": "Grey54"
+ },
+ {
+ "colorId": 246,
+ "hexString": "#949494",
+ "rgb": {
+ "r": 148,
+ "g": 148,
+ "b": 148
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 58
+ },
+ "name": "Grey58"
+ },
+ {
+ "colorId": 247,
+ "hexString": "#9e9e9e",
+ "rgb": {
+ "r": 158,
+ "g": 158,
+ "b": 158
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 61
+ },
+ "name": "Grey62"
+ },
+ {
+ "colorId": 248,
+ "hexString": "#a8a8a8",
+ "rgb": {
+ "r": 168,
+ "g": 168,
+ "b": 168
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 65
+ },
+ "name": "Grey66"
+ },
+ {
+ "colorId": 249,
+ "hexString": "#b2b2b2",
+ "rgb": {
+ "r": 178,
+ "g": 178,
+ "b": 178
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 69
+ },
+ "name": "Grey70"
+ },
+ {
+ "colorId": 250,
+ "hexString": "#bcbcbc",
+ "rgb": {
+ "r": 188,
+ "g": 188,
+ "b": 188
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 73
+ },
+ "name": "Grey74"
+ },
+ {
+ "colorId": 251,
+ "hexString": "#c6c6c6",
+ "rgb": {
+ "r": 198,
+ "g": 198,
+ "b": 198
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 77
+ },
+ "name": "Grey78"
+ },
+ {
+ "colorId": 252,
+ "hexString": "#d0d0d0",
+ "rgb": {
+ "r": 208,
+ "g": 208,
+ "b": 208
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 81
+ },
+ "name": "Grey82"
+ },
+ {
+ "colorId": 253,
+ "hexString": "#dadada",
+ "rgb": {
+ "r": 218,
+ "g": 218,
+ "b": 218
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 85
+ },
+ "name": "Grey85"
+ },
+ {
+ "colorId": 254,
+ "hexString": "#e4e4e4",
+ "rgb": {
+ "r": 228,
+ "g": 228,
+ "b": 228
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 89
+ },
+ "name": "Grey89"
+ },
+ {
+ "colorId": 255,
+ "hexString": "#eeeeee",
+ "rgb": {
+ "r": 238,
+ "g": 238,
+ "b": 238
+ },
+ "hsl": {
+ "h": 0,
+ "s": 0,
+ "l": 93
+ },
+ "name": "Grey93"
+ }
+] \ No newline at end of file
diff --git a/src/xterm_mouse.cc b/src/xterm_mouse.cc
new file mode 100644
index 0000000..f1f330a
--- /dev/null
+++ b/src/xterm_mouse.cc
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "xterm_mouse.hh"
+
+#include <unistd.h>
+
+#include "base/lnav_log.hh"
+#include "config.h"
+
+const char* xterm_mouse::XT_TERMCAP = "\033[?1000%?%p1%{1}%=%th%el%;";
+const char* xterm_mouse::XT_TERMCAP_TRACKING = "\033[?1002%?%p1%{1}%=%th%el%;";
+const char* xterm_mouse::XT_TERMCAP_SGR = "\033[?1006%?%p1%{1}%=%th%el%;";
+
+void
+xterm_mouse::handle_mouse()
+{
+ bool release = false;
+ int ch;
+ size_t index = 0;
+ int bstate, x, y;
+ char buffer[64];
+ bool done = false;
+
+ while (!done) {
+ if (index >= sizeof(buffer) - 1) {
+ break;
+ }
+ ch = getch();
+ switch (ch) {
+ case 'm':
+ release = true;
+ done = true;
+ break;
+ case 'M':
+ done = true;
+ break;
+ default:
+ buffer[index++] = (char) ch;
+ break;
+ }
+ }
+ buffer[index] = '\0';
+
+ if (sscanf(buffer, "%d;%d;%d", &bstate, &x, &y) == 3) {
+ if (this->xm_behavior) {
+ this->xm_behavior->mouse_event(bstate, release, x, y);
+ }
+ } else {
+ log_error("bad mouse escape sequence: %s", buffer);
+ }
+}
+
+void
+xterm_mouse::set_enabled(bool enabled)
+{
+ if (is_available()) {
+ putp(tparm((char*) XT_TERMCAP, enabled ? 1 : 0));
+ putp(tparm((char*) XT_TERMCAP_TRACKING, enabled ? 1 : 0));
+ putp(tparm((char*) XT_TERMCAP_SGR, enabled ? 1 : 0));
+ fflush(stdout);
+ this->xm_enabled = enabled;
+ } else {
+ log_warning("mouse support is not available");
+ }
+}
+
+bool
+xterm_mouse::is_available()
+{
+ return isatty(STDOUT_FILENO);
+}
diff --git a/src/xterm_mouse.hh b/src/xterm_mouse.hh
new file mode 100644
index 0000000..ce29a14
--- /dev/null
+++ b/src/xterm_mouse.hh
@@ -0,0 +1,131 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file xterm_mouse.hh
+ */
+
+#ifndef xterm_mouse_hh
+#define xterm_mouse_hh
+
+#include "config.h"
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/curses.h>
+#elif defined HAVE_NCURSESW_H
+# include <ncursesw.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/curses.h>
+#elif defined HAVE_NCURSES_H
+# include <ncurses.h>
+#elif defined HAVE_CURSES_H
+# include <curses.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+
+/**
+ * Base class for delegates of the xterm_mouse class.
+ */
+class mouse_behavior {
+public:
+ virtual ~mouse_behavior() = default;
+
+ /**
+ * Callback used to process mouse events.
+ *
+ * @param button The button that was pressed or released. This will
+ * be one of the XT_BUTTON or XT_SCROLL constants in the xterm_mouse
+ * class.
+ * @param x The X coordinate where the event occurred.
+ * @param y The Y coordinate where the event occurred.
+ */
+ virtual void mouse_event(int button, bool release, int x, int y) = 0;
+};
+
+/**
+ * Class that handles xterm mouse events coming through the ncurses interface.
+ */
+class xterm_mouse {
+public:
+ static const int XT_BUTTON1 = 0;
+ static const int XT_BUTTON2 = 1;
+ static const int XT_BUTTON3 = 2;
+
+ static const int XT_DRAG_FLAG = 32;
+ static const int XT_SCROLL_WHEEL_FLAG = 64;
+ static const int XT_SCROLL_UP = XT_SCROLL_WHEEL_FLAG | XT_BUTTON1;
+ static const int XT_SCROLL_DOWN = XT_SCROLL_WHEEL_FLAG | XT_BUTTON2;
+
+ static const int XT_BUTTON__MASK
+ = XT_SCROLL_WHEEL_FLAG | XT_BUTTON1 | XT_BUTTON2 | XT_BUTTON3;
+
+ static const char* XT_TERMCAP;
+ static const char* XT_TERMCAP_TRACKING;
+ static const char* XT_TERMCAP_SGR;
+
+ /**
+ * @return True if the user's terminal supports xterm-mouse events.
+ */
+ static bool is_available();
+
+ ~xterm_mouse()
+ {
+ if (this->is_enabled()) {
+ set_enabled(false);
+ }
+ }
+
+ /**
+ * @param enabled True if xterm mouse support should be enabled in the
+ * terminal.
+ */
+ void set_enabled(bool enabled);
+
+ /**
+ * @return True if xterm mouse support is enabled, false otherwise.
+ */
+ bool is_enabled() const { return this->xm_enabled; }
+
+ /**
+ * @param mb The delegate to send mouse events to.
+ */
+ void set_behavior(mouse_behavior* mb) { this->xm_behavior = mb; }
+
+ mouse_behavior* get_behavior() { return this->xm_behavior; }
+
+ /**
+ * Handle a KEY_MOUSE character from ncurses.
+ * @param ch unused
+ */
+ void handle_mouse();
+
+private:
+ bool xm_enabled{false};
+ mouse_behavior* xm_behavior{nullptr};
+};
+#endif
diff --git a/src/yajl/CMakeLists.txt b/src/yajl/CMakeLists.txt
new file mode 100644
index 0000000..82fe532
--- /dev/null
+++ b/src/yajl/CMakeLists.txt
@@ -0,0 +1,27 @@
+add_library(yajl STATIC
+ api/yajl_common.h
+ api/yajl_gen.h
+ api/yajl_parse.h
+ api/yajl_tree.h
+
+ yajl_common.h
+ yajl_alloc.h
+ yajl_buf.h
+ yajl_bytestack.h
+ yajl_encode.h
+ yajl_lex.h
+ yajl_parser.h
+ yajl_version.h
+
+ yajl.c
+ yajl_alloc.c
+ yajl_buf.c
+ yajl_encode.c
+ yajl_gen.c
+ yajl_lex.c
+ yajl_parser.c
+ yajl_tree.c
+ yajl_version.c
+ )
+target_include_directories(yajl PUBLIC api)
+target_include_directories(yajl PRIVATE ..)
diff --git a/src/yajl/Makefile.am b/src/yajl/Makefile.am
new file mode 100644
index 0000000..629185e
--- /dev/null
+++ b/src/yajl/Makefile.am
@@ -0,0 +1,37 @@
+
+AM_CPPFLAGS = -I$(top_srcdir)/src
+
+noinst_LIBRARIES = libyajl.a
+
+noinst_HEADERS = \
+ api/yajl_common.h \
+ api/yajl_gen.h \
+ api/yajl_parse.h \
+ api/yajl_tree.h \
+ yajl_alloc.h \
+ yajl_buf.h \
+ yajl_bytestack.h \
+ yajl_common.h \
+ yajl_encode.h \
+ yajl_lex.h \
+ yajl_parser.h \
+ yajl_version.h
+
+if USE_INCLUDED_YAJL
+libyajl_a_SOURCES = \
+ yajl.c \
+ yajl_alloc.c \
+ yajl_alloc.h \
+ yajl_buf.c \
+ yajl_buf.h \
+ yajl_bytestack.h \
+ yajl_encode.c \
+ yajl_encode.h \
+ yajl_gen.c \
+ yajl_lex.c \
+ yajl_lex.h \
+ yajl_parser.c \
+ yajl_parser.h \
+ yajl_tree.c \
+ yajl_version.c
+endif
diff --git a/src/yajl/api/yajl_common.h b/src/yajl/api/yajl_common.h
new file mode 100644
index 0000000..9596ef9
--- /dev/null
+++ b/src/yajl/api/yajl_common.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_COMMON_H__
+#define __YAJL_COMMON_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define YAJL_MAX_DEPTH 128
+
+/* msft dll export gunk. To build a DLL on windows, you
+ * must define WIN32, YAJL_SHARED, and YAJL_BUILD. To use a shared
+ * DLL, you must define YAJL_SHARED and WIN32 */
+#if (defined(_WIN32) || defined(WIN32)) && defined(YAJL_SHARED)
+# ifdef YAJL_BUILD
+# define YAJL_API __declspec(dllexport)
+# else
+# define YAJL_API __declspec(dllimport)
+# endif
+#else
+# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
+# define YAJL_API __attribute__ ((visibility("default")))
+# else
+# define YAJL_API
+# endif
+#endif
+
+/** pointer to a malloc function, supporting client overriding memory
+ * allocation routines */
+typedef void * (*yajl_malloc_func)(void *ctx, size_t sz);
+
+/** pointer to a free function, supporting client overriding memory
+ * allocation routines */
+typedef void (*yajl_free_func)(void *ctx, void * ptr);
+
+/** pointer to a realloc function which can resize an allocation. */
+typedef void * (*yajl_realloc_func)(void *ctx, void * ptr, size_t sz);
+
+/** A structure which can be passed to yajl_*_alloc routines to allow the
+ * client to specify memory allocation functions to be used. */
+typedef struct
+{
+ /** pointer to a function that can allocate uninitialized memory */
+ yajl_malloc_func malloc;
+ /** pointer to a function that can resize memory allocations */
+ yajl_realloc_func realloc;
+ /** pointer to a function that can free memory allocated using
+ * reallocFunction or mallocFunction */
+ yajl_free_func free;
+ /** a context pointer that will be passed to above allocation routines */
+ void * ctx;
+} yajl_alloc_funcs;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/yajl/api/yajl_gen.h b/src/yajl/api/yajl_gen.h
new file mode 100644
index 0000000..ddd1527
--- /dev/null
+++ b/src/yajl/api/yajl_gen.h
@@ -0,0 +1,165 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_gen.h
+ * Interface to YAJL's JSON generation facilities.
+ */
+
+#include <yajl/yajl_common.h>
+
+#ifndef __YAJL_GEN_H__
+#define __YAJL_GEN_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+ /** generator status codes */
+ typedef enum {
+ /** no error */
+ yajl_gen_status_ok = 0,
+ /** at a point where a map key is generated, a function other than
+ * yajl_gen_string was called */
+ yajl_gen_keys_must_be_strings,
+ /** YAJL's maximum generation depth was exceeded. see
+ * YAJL_MAX_DEPTH */
+ yajl_max_depth_exceeded,
+ /** A generator function (yajl_gen_XXX) was called while in an error
+ * state */
+ yajl_gen_in_error_state,
+ /** A complete JSON document has been generated */
+ yajl_gen_generation_complete,
+ /** yajl_gen_double was passed an invalid floating point value
+ * (infinity or NaN). */
+ yajl_gen_invalid_number,
+ /** A print callback was passed in, so there is no internal
+ * buffer to get from */
+ yajl_gen_no_buf,
+ /** returned from yajl_gen_string() when the yajl_gen_validate_utf8
+ * option is enabled and an invalid was passed by client code.
+ */
+ yajl_gen_invalid_string
+ } yajl_gen_status;
+
+ /** an opaque handle to a generator */
+ typedef struct yajl_gen_t * yajl_gen;
+
+ /** a callback used for "printing" the results. */
+ typedef void (*yajl_print_t)(void * ctx,
+ const char * str,
+ size_t len);
+
+ /** configuration parameters for the parser, these may be passed to
+ * yajl_gen_config() along with option specific argument(s). In general,
+ * all configuration parameters default to *off*. */
+ typedef enum {
+ /** generate indented (beautiful) output */
+ yajl_gen_beautify = 0x01,
+ /**
+ * Set an indent string which is used when yajl_gen_beautify
+ * is enabled. Maybe something like \\t or some number of
+ * spaces. The default is four spaces ' '.
+ */
+ yajl_gen_indent_string = 0x02,
+ /**
+ * Set a function and context argument that should be used to
+ * output generated json. the function should conform to the
+ * yajl_print_t prototype while the context argument is a
+ * void * of your choosing.
+ *
+ * example:
+ * yajl_gen_config(g, yajl_gen_print_callback, myFunc, myVoidPtr);
+ */
+ yajl_gen_print_callback = 0x04,
+ /**
+ * Normally the generator does not validate that strings you
+ * pass to it via yajl_gen_string() are valid UTF8. Enabling
+ * this option will cause it to do so.
+ */
+ yajl_gen_validate_utf8 = 0x08,
+ /**
+ * the forward solidus (slash or '/' in human) is not required to be
+ * escaped in json text. By default, YAJL will not escape it in the
+ * iterest of saving bytes. Setting this flag will cause YAJL to
+ * always escape '/' in generated JSON strings.
+ */
+ yajl_gen_escape_solidus = 0x10
+ } yajl_gen_option;
+
+ /** allow the modification of generator options subsequent to handle
+ * allocation (via yajl_alloc)
+ * \returns zero in case of errors, non-zero otherwise
+ */
+ YAJL_API int yajl_gen_config(yajl_gen g, yajl_gen_option opt, ...);
+
+ /** allocate a generator handle
+ * \param allocFuncs an optional pointer to a structure which allows
+ * the client to overide the memory allocation
+ * used by yajl. May be NULL, in which case
+ * malloc/free/realloc will be used.
+ *
+ * \returns an allocated handle on success, NULL on failure (bad params)
+ */
+ YAJL_API yajl_gen yajl_gen_alloc(const yajl_alloc_funcs * allocFuncs);
+
+ /** free a generator handle */
+ YAJL_API void yajl_gen_free(yajl_gen handle);
+
+ YAJL_API yajl_gen_status yajl_gen_integer(yajl_gen hand, long long int number);
+ /** generate a floating point number. number may not be infinity or
+ * NaN, as these have no representation in JSON. In these cases the
+ * generator will return 'yajl_gen_invalid_number' */
+ YAJL_API yajl_gen_status yajl_gen_double(yajl_gen hand, double number);
+ YAJL_API yajl_gen_status yajl_gen_number(yajl_gen hand,
+ const char * num,
+ size_t len);
+ YAJL_API yajl_gen_status yajl_gen_string(yajl_gen hand,
+ const unsigned char * str,
+ size_t len);
+ YAJL_API yajl_gen_status yajl_gen_null(yajl_gen hand);
+ YAJL_API yajl_gen_status yajl_gen_bool(yajl_gen hand, int boolean);
+ YAJL_API yajl_gen_status yajl_gen_map_open(yajl_gen hand);
+ YAJL_API yajl_gen_status yajl_gen_map_close(yajl_gen hand);
+ YAJL_API yajl_gen_status yajl_gen_array_open(yajl_gen hand);
+ YAJL_API yajl_gen_status yajl_gen_array_close(yajl_gen hand);
+
+ /** access the null terminated generator buffer. If incrementally
+ * outputing JSON, one should call yajl_gen_clear to clear the
+ * buffer. This allows stream generation. */
+ YAJL_API yajl_gen_status yajl_gen_get_buf(yajl_gen hand,
+ const unsigned char ** buf,
+ size_t * len);
+
+ /** clear yajl's output buffer, but maintain all internal generation
+ * state. This function will not "reset" the generator state, and is
+ * intended to enable incremental JSON outputing. */
+ YAJL_API void yajl_gen_clear(yajl_gen hand);
+
+ /** Reset the generator state. Allows a client to generate multiple
+ * json entities in a stream. The "sep" string will be inserted to
+ * separate the previously generated entity from the current,
+ * NULL means *no separation* of entites (clients beware, generating
+ * multiple JSON numbers, for instance, will result in inscrutable
+ * output) */
+ YAJL_API void yajl_gen_reset(yajl_gen hand, const char * sep);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/yajl/api/yajl_parse.h b/src/yajl/api/yajl_parse.h
new file mode 100644
index 0000000..eea8d9a
--- /dev/null
+++ b/src/yajl/api/yajl_parse.h
@@ -0,0 +1,228 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_parse.h
+ * Interface to YAJL's JSON stream parsing facilities.
+ */
+
+#include <yajl/yajl_common.h>
+
+#ifndef __YAJL_PARSE_H__
+#define __YAJL_PARSE_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+ /** error codes returned from this interface */
+ typedef enum {
+ /** no error was encountered */
+ yajl_status_ok,
+ /** a client callback returned zero, stopping the parse */
+ yajl_status_client_canceled,
+ /** An error occured during the parse. Call yajl_get_error for
+ * more information about the encountered error */
+ yajl_status_error
+ } yajl_status;
+
+ /** attain a human readable, english, string for an error */
+ YAJL_API const char * yajl_status_to_string(yajl_status code);
+
+ /** an opaque handle to a parser */
+ typedef struct yajl_handle_t * yajl_handle;
+
+ /** yajl is an event driven parser. this means as json elements are
+ * parsed, you are called back to do something with the data. The
+ * functions in this table indicate the various events for which
+ * you will be called back. Each callback accepts a "context"
+ * pointer, this is a void * that is passed into the yajl_parse
+ * function which the client code may use to pass around context.
+ *
+ * All callbacks return an integer. If non-zero, the parse will
+ * continue. If zero, the parse will be canceled and
+ * yajl_status_client_canceled will be returned from the parse.
+ *
+ * \attention {
+ * A note about the handling of numbers:
+ *
+ * yajl will only convert numbers that can be represented in a
+ * double or a 64 bit (long long) int. All other numbers will
+ * be passed to the client in string form using the yajl_number
+ * callback. Furthermore, if yajl_number is not NULL, it will
+ * always be used to return numbers, that is yajl_integer and
+ * yajl_double will be ignored. If yajl_number is NULL but one
+ * of yajl_integer or yajl_double are defined, parsing of a
+ * number larger than is representable in a double or 64 bit
+ * integer will result in a parse error.
+ * }
+ */
+ typedef struct {
+ int (* yajl_null)(void * ctx);
+ int (* yajl_boolean)(void * ctx, int boolVal);
+ int (* yajl_integer)(void * ctx, long long integerVal);
+ int (* yajl_double)(void * ctx, double doubleVal);
+ /** A callback which passes the string representation of the number
+ * back to the client. Will be used for all numbers when present */
+ int (* yajl_number)(void * ctx, const char * numberVal,
+ size_t numberLen);
+
+ /** strings are returned as pointers into the JSON text when,
+ * possible, as a result, they are _not_ null padded */
+ int (* yajl_string)(void * ctx, const unsigned char * stringVal,
+ size_t stringLen);
+
+ int (* yajl_start_map)(void * ctx);
+ int (* yajl_map_key)(void * ctx, const unsigned char * key,
+ size_t stringLen);
+ int (* yajl_end_map)(void * ctx);
+
+ int (* yajl_start_array)(void * ctx);
+ int (* yajl_end_array)(void * ctx);
+ } yajl_callbacks;
+
+ /** allocate a parser handle
+ * \param callbacks a yajl callbacks structure specifying the
+ * functions to call when different JSON entities
+ * are encountered in the input text. May be NULL,
+ * which is only useful for validation.
+ * \param afs memory allocation functions, may be NULL for to use
+ * C runtime library routines (malloc and friends)
+ * \param ctx a context pointer that will be passed to callbacks.
+ */
+ YAJL_API yajl_handle yajl_alloc(const yajl_callbacks * callbacks,
+ yajl_alloc_funcs * afs,
+ void * ctx);
+
+
+ /** configuration parameters for the parser, these may be passed to
+ * yajl_config() along with option specific argument(s). In general,
+ * all configuration parameters default to *off*. */
+ typedef enum {
+ /** Ignore javascript style comments present in
+ * JSON input. Non-standard, but rather fun
+ * arguments: toggled off with integer zero, on otherwise.
+ *
+ * example:
+ * yajl_config(h, yajl_allow_comments, 1); // turn comment support on
+ */
+ yajl_allow_comments = 0x01,
+ /**
+ * When set the parser will verify that all strings in JSON input are
+ * valid UTF8 and will emit a parse error if this is not so. When set,
+ * this option makes parsing slightly more expensive (~7% depending
+ * on processor and compiler in use)
+ *
+ * example:
+ * yajl_config(h, yajl_dont_validate_strings, 1); // disable utf8 checking
+ */
+ yajl_dont_validate_strings = 0x02,
+ /**
+ * By default, upon calls to yajl_complete_parse(), yajl will
+ * ensure the entire input text was consumed and will raise an error
+ * otherwise. Enabling this flag will cause yajl to disable this
+ * check. This can be useful when parsing json out of a that contains more
+ * than a single JSON document.
+ */
+ yajl_allow_trailing_garbage = 0x04,
+ /**
+ * Allow multiple values to be parsed by a single handle. The
+ * entire text must be valid JSON, and values can be seperated
+ * by any kind of whitespace. This flag will change the
+ * behavior of the parser, and cause it continue parsing after
+ * a value is parsed, rather than transitioning into a
+ * complete state. This option can be useful when parsing multiple
+ * values from an input stream.
+ */
+ yajl_allow_multiple_values = 0x08,
+ /**
+ * When yajl_complete_parse() is called the parser will
+ * check that the top level value was completely consumed. I.E.,
+ * if called whilst in the middle of parsing a value
+ * yajl will enter an error state (premature EOF). Setting this
+ * flag suppresses that check and the corresponding error.
+ */
+ yajl_allow_partial_values = 0x10
+ } yajl_option;
+
+ /** allow the modification of parser options subsequent to handle
+ * allocation (via yajl_alloc)
+ * \returns zero in case of errors, non-zero otherwise
+ */
+ YAJL_API int yajl_config(yajl_handle h, yajl_option opt, ...);
+
+ YAJL_API void yajl_reset(yajl_handle handle);
+
+ /** free a parser handle */
+ YAJL_API void yajl_free(yajl_handle handle);
+
+ /** Parse some json!
+ * \param hand - a handle to the json parser allocated with yajl_alloc
+ * \param jsonText - a pointer to the UTF8 json text to be parsed
+ * \param jsonTextLength - the length, in bytes, of input text
+ */
+ YAJL_API yajl_status yajl_parse(yajl_handle hand,
+ const unsigned char * jsonText,
+ size_t jsonTextLength);
+
+ /** Parse any remaining buffered json.
+ * Since yajl is a stream-based parser, without an explicit end of
+ * input, yajl sometimes can't decide if content at the end of the
+ * stream is valid or not. For example, if "1" has been fed in,
+ * yajl can't know whether another digit is next or some character
+ * that would terminate the integer token.
+ *
+ * \param hand - a handle to the json parser allocated with yajl_alloc
+ */
+ YAJL_API yajl_status yajl_complete_parse(yajl_handle hand);
+
+ /** get an error string describing the state of the
+ * parse.
+ *
+ * If verbose is non-zero, the message will include the JSON
+ * text where the error occured, along with an arrow pointing to
+ * the specific char.
+ *
+ * \returns A dynamically allocated string will be returned which should
+ * be freed with yajl_free_error
+ */
+ YAJL_API unsigned char * yajl_get_error(yajl_handle hand, int verbose,
+ const unsigned char * jsonText,
+ size_t jsonTextLength);
+
+ /**
+ * get the amount of data consumed from the last chunk passed to YAJL.
+ *
+ * In the case of a successful parse this can help you understand if
+ * the entire buffer was consumed (which will allow you to handle
+ * "junk at end of input").
+ *
+ * In the event an error is encountered during parsing, this function
+ * affords the client a way to get the offset into the most recent
+ * chunk where the error occured. 0 will be returned if no error
+ * was encountered.
+ */
+ YAJL_API size_t yajl_get_bytes_consumed(yajl_handle hand);
+
+ /** free an error returned from yajl_get_error */
+ YAJL_API void yajl_free_error(yajl_handle hand, unsigned char * str);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/yajl/api/yajl_tree.h b/src/yajl/api/yajl_tree.h
new file mode 100644
index 0000000..1c1e06a
--- /dev/null
+++ b/src/yajl/api/yajl_tree.h
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2010-2011 Florian Forster <ff at octo.it>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_tree.h
+ *
+ * Parses JSON data and returns the data in tree form.
+ *
+ * \author Florian Forster
+ * \date August 2010
+ *
+ * This interface makes quick parsing and extraction of
+ * smallish JSON docs trivial:
+ *
+ * \include example/parse_config.c
+ */
+
+#ifndef YAJL_TREE_H
+#define YAJL_TREE_H 1
+
+#include <yajl/yajl_common.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/** possible data types that a yajl_val_s can hold */
+typedef enum {
+ yajl_t_string = 1,
+ yajl_t_number = 2,
+ yajl_t_object = 3,
+ yajl_t_array = 4,
+ yajl_t_true = 5,
+ yajl_t_false = 6,
+ yajl_t_null = 7,
+ /** The any type isn't valid for yajl_val_s.type, but can be
+ * used as an argument to routines like yajl_tree_get().
+ */
+ yajl_t_any = 8
+} yajl_type;
+
+#define YAJL_NUMBER_INT_VALID 0x01
+#define YAJL_NUMBER_DOUBLE_VALID 0x02
+
+/** A pointer to a node in the parse tree */
+typedef struct yajl_val_s * yajl_val;
+
+/**
+ * A JSON value representation capable of holding one of the seven
+ * types above. For "string", "number", "object", and "array"
+ * additional data is available in the union. The "YAJL_IS_*"
+ * and "YAJL_GET_*" macros below allow type checking and convenient
+ * value extraction.
+ */
+struct yajl_val_s
+{
+ /** Type of the value contained. Use the "YAJL_IS_*" macros to check for a
+ * specific type. */
+ yajl_type type;
+ /** Type-specific data. You may use the "YAJL_GET_*" macros to access these
+ * members. */
+ union
+ {
+ char * string;
+ struct {
+ long long i; /*< integer value, if representable. */
+ double d; /*< double value, if representable. */
+ char *r; /*< unparsed number in string form. */
+ /** Signals whether the \em i and \em d members are
+ * valid. See \c YAJL_NUMBER_INT_VALID and
+ * \c YAJL_NUMBER_DOUBLE_VALID. */
+ unsigned int flags;
+ } number;
+ struct {
+ const char **keys; /*< Array of keys */
+ yajl_val *values; /*< Array of values. */
+ size_t len; /*< Number of key-value-pairs. */
+ } object;
+ struct {
+ yajl_val *values; /*< Array of elements. */
+ size_t len; /*< Number of elements. */
+ } array;
+ } u;
+};
+
+/**
+ * Parse a string.
+ *
+ * Parses an null-terminated string containing JSON data and returns a pointer
+ * to the top-level value (root of the parse tree).
+ *
+ * \param input Pointer to a null-terminated utf8 string containing
+ * JSON data.
+ * \param error_buffer Pointer to a buffer in which an error message will
+ * be stored if \em yajl_tree_parse fails, or
+ * \c NULL. The buffer will be initialized before
+ * parsing, so its content will be destroyed even if
+ * \em yajl_tree_parse succeeds.
+ * \param error_buffer_size Size of the memory area pointed to by
+ * \em error_buffer_size. If \em error_buffer_size is
+ * \c NULL, this argument is ignored.
+ *
+ * \returns Pointer to the top-level value or \c NULL on error. The memory
+ * pointed to must be freed using \em yajl_tree_free. In case of an error, a
+ * null terminated message describing the error in more detail is stored in
+ * \em error_buffer if it is not \c NULL.
+ */
+YAJL_API yajl_val yajl_tree_parse (const char *input,
+ char *error_buffer, size_t error_buffer_size);
+
+
+/**
+ * Free a parse tree returned by "yajl_tree_parse".
+ *
+ * \param v Pointer to a JSON value returned by "yajl_tree_parse". Passing NULL
+ * is valid and results in a no-op.
+ */
+YAJL_API void yajl_tree_free (yajl_val v);
+
+/**
+ * Access a nested value inside a tree.
+ *
+ * \param parent the node under which you'd like to extract values.
+ * \param path A null terminated array of strings, each the name of an object key
+ * \param type the yajl_type of the object you seek, or yajl_t_any if any will do.
+ *
+ * \returns a pointer to the found value, or NULL if we came up empty.
+ *
+ * Future Ideas: it'd be nice to move path to a string and implement support for
+ * a teeny tiny micro language here, so you can extract array elements, do things
+ * like .first and .last, even .length. Inspiration from JSONPath and css selectors?
+ * No it wouldn't be fast, but that's not what this API is about.
+ */
+YAJL_API yajl_val yajl_tree_get(yajl_val parent, const char ** path, yajl_type type);
+
+/* Various convenience macros to check the type of a `yajl_val` */
+#define YAJL_IS_STRING(v) (((v) != NULL) && ((v)->type == yajl_t_string))
+#define YAJL_IS_NUMBER(v) (((v) != NULL) && ((v)->type == yajl_t_number))
+#define YAJL_IS_INTEGER(v) (YAJL_IS_NUMBER(v) && ((v)->u.number.flags & YAJL_NUMBER_INT_VALID))
+#define YAJL_IS_DOUBLE(v) (YAJL_IS_NUMBER(v) && ((v)->u.number.flags & YAJL_NUMBER_DOUBLE_VALID))
+#define YAJL_IS_OBJECT(v) (((v) != NULL) && ((v)->type == yajl_t_object))
+#define YAJL_IS_ARRAY(v) (((v) != NULL) && ((v)->type == yajl_t_array ))
+#define YAJL_IS_TRUE(v) (((v) != NULL) && ((v)->type == yajl_t_true ))
+#define YAJL_IS_FALSE(v) (((v) != NULL) && ((v)->type == yajl_t_false ))
+#define YAJL_IS_NULL(v) (((v) != NULL) && ((v)->type == yajl_t_null ))
+
+/** Given a yajl_val_string return a ptr to the bare string it contains,
+ * or NULL if the value is not a string. */
+#define YAJL_GET_STRING(v) (YAJL_IS_STRING(v) ? (v)->u.string : NULL)
+
+/** Get the string representation of a number. You should check type first,
+ * perhaps using YAJL_IS_NUMBER */
+#define YAJL_GET_NUMBER(v) ((v)->u.number.r)
+
+/** Get the double representation of a number. You should check type first,
+ * perhaps using YAJL_IS_DOUBLE */
+#define YAJL_GET_DOUBLE(v) ((v)->u.number.d)
+
+/** Get the 64bit (long long) integer representation of a number. You should
+ * check type first, perhaps using YAJL_IS_INTEGER */
+#define YAJL_GET_INTEGER(v) ((v)->u.number.i)
+
+/** Get a pointer to a yajl_val_object or NULL if the value is not an object. */
+#define YAJL_GET_OBJECT(v) (YAJL_IS_OBJECT(v) ? &(v)->u.object : NULL)
+
+/** Get a pointer to a yajl_val_array or NULL if the value is not an object. */
+#define YAJL_GET_ARRAY(v) (YAJL_IS_ARRAY(v) ? &(v)->u.array : NULL)
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* YAJL_TREE_H */
diff --git a/src/yajl/yajl.c b/src/yajl/yajl.c
new file mode 100644
index 0000000..346ab48
--- /dev/null
+++ b/src/yajl/yajl.c
@@ -0,0 +1,189 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "api/yajl_parse.h"
+#include "yajl_lex.h"
+#include "yajl_parser.h"
+#include "yajl_alloc.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdarg.h>
+#include <assert.h>
+
+const char *
+yajl_status_to_string(yajl_status stat)
+{
+ const char * statStr = "unknown";
+ switch (stat) {
+ case yajl_status_ok:
+ statStr = "ok, no error";
+ break;
+ case yajl_status_client_canceled:
+ statStr = "client canceled parse";
+ break;
+ case yajl_status_error:
+ statStr = "parse error";
+ break;
+ }
+ return statStr;
+}
+
+yajl_handle
+yajl_alloc(const yajl_callbacks * callbacks,
+ yajl_alloc_funcs * afs,
+ void * ctx)
+{
+ yajl_handle hand = NULL;
+ yajl_alloc_funcs afsBuffer;
+
+ /* first order of business is to set up memory allocation routines */
+ if (afs != NULL) {
+ if (afs->malloc == NULL || afs->realloc == NULL || afs->free == NULL)
+ {
+ return NULL;
+ }
+ } else {
+ yajl_set_default_alloc_funcs(&afsBuffer);
+ afs = &afsBuffer;
+ }
+
+ hand = (yajl_handle) YA_MALLOC(afs, sizeof(struct yajl_handle_t));
+
+ /* copy in pointers to allocation routines */
+ memcpy((void *) &(hand->alloc), (void *) afs, sizeof(yajl_alloc_funcs));
+
+ hand->callbacks = callbacks;
+ hand->ctx = ctx;
+ hand->lexer = NULL;
+ hand->bytesConsumed = 0;
+ hand->decodeBuf = yajl_buf_alloc(&(hand->alloc));
+ hand->flags = 0;
+ yajl_bs_init(hand->stateStack, &(hand->alloc));
+ yajl_bs_push(hand->stateStack, yajl_state_start);
+
+ return hand;
+}
+
+int
+yajl_config(yajl_handle h, yajl_option opt, ...)
+{
+ int rv = 1;
+ va_list ap;
+ va_start(ap, opt);
+
+ switch(opt) {
+ case yajl_allow_comments:
+ case yajl_dont_validate_strings:
+ case yajl_allow_trailing_garbage:
+ case yajl_allow_multiple_values:
+ case yajl_allow_partial_values:
+ if (va_arg(ap, int)) h->flags |= opt;
+ else h->flags &= ~opt;
+ break;
+ default:
+ rv = 0;
+ }
+ va_end(ap);
+
+ return rv;
+}
+
+void
+yajl_reset(yajl_handle handle)
+{
+ handle->bytesConsumed = 0;
+ if (handle->stateStack.used != 0) {
+ handle->stateStack.used = 0;
+ if (handle->lexer != NULL) {
+ yajl_lex_free(handle->lexer);
+ handle->lexer = NULL;
+ }
+ }
+ yajl_bs_push(handle->stateStack, yajl_state_start);
+}
+
+void
+yajl_free(yajl_handle handle)
+{
+ yajl_bs_free(handle->stateStack);
+ yajl_buf_free(handle->decodeBuf);
+ if (handle->lexer) {
+ yajl_lex_free(handle->lexer);
+ handle->lexer = NULL;
+ }
+ YA_FREE(&(handle->alloc), handle);
+}
+
+yajl_status
+yajl_parse(yajl_handle hand, const unsigned char * jsonText,
+ size_t jsonTextLen)
+{
+ yajl_status status;
+
+ /* lazy allocation of the lexer */
+ if (hand->lexer == NULL) {
+ hand->lexer = yajl_lex_alloc(&(hand->alloc),
+ hand->flags & yajl_allow_comments,
+ !(hand->flags & yajl_dont_validate_strings));
+ }
+
+ status = yajl_do_parse(hand, jsonText, jsonTextLen);
+ return status;
+}
+
+
+yajl_status
+yajl_complete_parse(yajl_handle hand)
+{
+ /* The lexer is lazy allocated in the first call to parse. if parse is
+ * never called, then no data was provided to parse at all. This is a
+ * "premature EOF" error unless yajl_allow_partial_values is specified.
+ * allocating the lexer now is the simplest possible way to handle this
+ * case while preserving all the other semantics of the parser
+ * (multiple values, partial values, etc). */
+ if (hand->lexer == NULL) {
+ hand->lexer = yajl_lex_alloc(&(hand->alloc),
+ hand->flags & yajl_allow_comments,
+ !(hand->flags & yajl_dont_validate_strings));
+ }
+
+ return yajl_do_finish(hand);
+}
+
+unsigned char *
+yajl_get_error(yajl_handle hand, int verbose,
+ const unsigned char * jsonText, size_t jsonTextLen)
+{
+ return yajl_render_error_string(hand, jsonText, jsonTextLen, verbose);
+}
+
+size_t
+yajl_get_bytes_consumed(yajl_handle hand)
+{
+ if (!hand) return 0;
+ else return hand->bytesConsumed;
+}
+
+
+void
+yajl_free_error(yajl_handle hand, unsigned char * str)
+{
+ /* use memory allocation functions if set */
+ YA_FREE(&(hand->alloc), str);
+}
+
+/* XXX: add utility routines to parse from file */
diff --git a/src/yajl/yajl_alloc.c b/src/yajl/yajl_alloc.c
new file mode 100644
index 0000000..96ad1d3
--- /dev/null
+++ b/src/yajl/yajl_alloc.c
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_alloc.h
+ * default memory allocation routines for yajl which use malloc/realloc and
+ * free
+ */
+
+#include "yajl_alloc.h"
+#include <stdlib.h>
+
+static void * yajl_internal_malloc(void *ctx, size_t sz)
+{
+ (void)ctx;
+ return malloc(sz);
+}
+
+static void * yajl_internal_realloc(void *ctx, void * previous,
+ size_t sz)
+{
+ (void)ctx;
+ return realloc(previous, sz);
+}
+
+static void yajl_internal_free(void *ctx, void * ptr)
+{
+ (void)ctx;
+ free(ptr);
+}
+
+void yajl_set_default_alloc_funcs(yajl_alloc_funcs * yaf)
+{
+ yaf->malloc = yajl_internal_malloc;
+ yaf->free = yajl_internal_free;
+ yaf->realloc = yajl_internal_realloc;
+ yaf->ctx = NULL;
+}
+
diff --git a/src/yajl/yajl_alloc.h b/src/yajl/yajl_alloc.h
new file mode 100644
index 0000000..203c2f9
--- /dev/null
+++ b/src/yajl/yajl_alloc.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/**
+ * \file yajl_alloc.h
+ * default memory allocation routines for yajl which use malloc/realloc and
+ * free
+ */
+
+#ifndef __YAJL_ALLOC_H__
+#define __YAJL_ALLOC_H__
+
+#include "api/yajl_common.h"
+
+#define YA_MALLOC(afs, sz) (afs)->malloc((afs)->ctx, (sz))
+#define YA_FREE(afs, ptr) (afs)->free((afs)->ctx, (ptr))
+#define YA_REALLOC(afs, ptr, sz) (afs)->realloc((afs)->ctx, (ptr), (sz))
+
+void yajl_set_default_alloc_funcs(yajl_alloc_funcs * yaf);
+
+#endif
diff --git a/src/yajl/yajl_buf.c b/src/yajl/yajl_buf.c
new file mode 100644
index 0000000..1aeafde
--- /dev/null
+++ b/src/yajl/yajl_buf.c
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "yajl_buf.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+
+#define YAJL_BUF_INIT_SIZE 2048
+
+struct yajl_buf_t {
+ size_t len;
+ size_t used;
+ unsigned char * data;
+ yajl_alloc_funcs * alloc;
+};
+
+static
+void yajl_buf_ensure_available(yajl_buf buf, size_t want)
+{
+ size_t need;
+
+ assert(buf != NULL);
+
+ /* first call */
+ if (buf->data == NULL) {
+ buf->len = YAJL_BUF_INIT_SIZE;
+ buf->data = (unsigned char *) YA_MALLOC(buf->alloc, buf->len);
+ buf->data[0] = 0;
+ }
+
+ need = buf->len;
+
+ while (want >= (need - buf->used)) need <<= 1;
+
+ if (need != buf->len) {
+ buf->data = (unsigned char *) YA_REALLOC(buf->alloc, buf->data, need);
+ buf->len = need;
+ }
+}
+
+yajl_buf yajl_buf_alloc(yajl_alloc_funcs * alloc)
+{
+ yajl_buf b = YA_MALLOC(alloc, sizeof(struct yajl_buf_t));
+ memset((void *) b, 0, sizeof(struct yajl_buf_t));
+ b->alloc = alloc;
+ return b;
+}
+
+void yajl_buf_free(yajl_buf buf)
+{
+ assert(buf != NULL);
+ if (buf->data) YA_FREE(buf->alloc, buf->data);
+ YA_FREE(buf->alloc, buf);
+}
+
+void yajl_buf_append(yajl_buf buf, const void * data, size_t len)
+{
+ yajl_buf_ensure_available(buf, len);
+ if (len > 0) {
+ assert(data != NULL);
+ memcpy(buf->data + buf->used, data, len);
+ buf->used += len;
+ buf->data[buf->used] = 0;
+ }
+}
+
+void yajl_buf_clear(yajl_buf buf)
+{
+ buf->used = 0;
+ if (buf->data) buf->data[buf->used] = 0;
+}
+
+const unsigned char * yajl_buf_data(yajl_buf buf)
+{
+ return buf->data;
+}
+
+size_t yajl_buf_len(yajl_buf buf)
+{
+ return buf->used;
+}
+
+void
+yajl_buf_truncate(yajl_buf buf, size_t len)
+{
+ assert(len <= buf->used);
+ buf->used = len;
+}
diff --git a/src/yajl/yajl_buf.h b/src/yajl/yajl_buf.h
new file mode 100644
index 0000000..a358246
--- /dev/null
+++ b/src/yajl/yajl_buf.h
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_BUF_H__
+#define __YAJL_BUF_H__
+
+#include "api/yajl_common.h"
+#include "yajl_alloc.h"
+
+/*
+ * Implementation/performance notes. If this were moved to a header
+ * only implementation using #define's where possible we might be
+ * able to sqeeze a little performance out of the guy by killing function
+ * call overhead. YMMV.
+ */
+
+/**
+ * yajl_buf is a buffer with exponential growth. the buffer ensures that
+ * you are always null padded.
+ */
+typedef struct yajl_buf_t * yajl_buf;
+
+/* allocate a new buffer */
+yajl_buf yajl_buf_alloc(yajl_alloc_funcs * alloc);
+
+/* free the buffer */
+void yajl_buf_free(yajl_buf buf);
+
+/* append a number of bytes to the buffer */
+void yajl_buf_append(yajl_buf buf, const void * data, size_t len);
+
+/* empty the buffer */
+void yajl_buf_clear(yajl_buf buf);
+
+/* get a pointer to the beginning of the buffer */
+const unsigned char * yajl_buf_data(yajl_buf buf);
+
+/* get the length of the buffer */
+size_t yajl_buf_len(yajl_buf buf);
+
+/* truncate the buffer */
+void yajl_buf_truncate(yajl_buf buf, size_t len);
+
+#endif
diff --git a/src/yajl/yajl_bytestack.h b/src/yajl/yajl_bytestack.h
new file mode 100644
index 0000000..9ea7d15
--- /dev/null
+++ b/src/yajl/yajl_bytestack.h
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+/*
+ * A header only implementation of a simple stack of bytes, used in YAJL
+ * to maintain parse state.
+ */
+
+#ifndef __YAJL_BYTESTACK_H__
+#define __YAJL_BYTESTACK_H__
+
+#include "api/yajl_common.h"
+
+#define YAJL_BS_INC 128
+
+typedef struct yajl_bytestack_t
+{
+ unsigned char * stack;
+ size_t size;
+ size_t used;
+ yajl_alloc_funcs * yaf;
+} yajl_bytestack;
+
+/* initialize a bytestack */
+#define yajl_bs_init(obs, _yaf) { \
+ (obs).stack = NULL; \
+ (obs).size = 0; \
+ (obs).used = 0; \
+ (obs).yaf = (_yaf); \
+ } \
+
+
+/* initialize a bytestack */
+#define yajl_bs_free(obs) \
+ if ((obs).stack) (obs).yaf->free((obs).yaf->ctx, (obs).stack);
+
+#define yajl_bs_current(obs) \
+ (assert((obs).used > 0), (obs).stack[(obs).used - 1])
+
+#define yajl_bs_push(obs, byte) { \
+ if (((obs).size - (obs).used) == 0) { \
+ (obs).size += YAJL_BS_INC; \
+ (obs).stack = (obs).yaf->realloc((obs).yaf->ctx,\
+ (void *) (obs).stack, (obs).size);\
+ } \
+ (obs).stack[((obs).used)++] = (byte); \
+}
+
+/* removes the top item of the stack, returns nothing */
+#define yajl_bs_pop(obs) { ((obs).used)--; }
+
+#define yajl_bs_set(obs, byte) \
+ (obs).stack[((obs).used) - 1] = (byte);
+
+
+#endif
diff --git a/src/yajl/yajl_common.h b/src/yajl/yajl_common.h
new file mode 100644
index 0000000..49ca3a5
--- /dev/null
+++ b/src/yajl/yajl_common.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2007-2011, Lloyd Hilaiel <lloyd@hilaiel.com>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_COMMON_H__
+#define __YAJL_COMMON_H__
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define YAJL_MAX_DEPTH 128
+
+/* msft dll export gunk. To build a DLL on windows, you
+ * must define WIN32, YAJL_SHARED, and YAJL_BUILD. To use a shared
+ * DLL, you must define YAJL_SHARED and WIN32 */
+#if defined(WIN32) && defined(YAJL_SHARED)
+# ifdef YAJL_BUILD
+# define YAJL_API __declspec(dllexport)
+# else
+# define YAJL_API __declspec(dllimport)
+# endif
+#else
+# if defined(__GNUC__) && (__GNUC__ * 100 + __GNUC_MINOR__) >= 303
+# define YAJL_API __attribute__ ((visibility("default")))
+# else
+# define YAJL_API
+# endif
+#endif
+
+/** pointer to a malloc function, supporting client overriding memory
+ * allocation routines */
+typedef void * (*yajl_malloc_func)(void *ctx, size_t sz);
+
+/** pointer to a free function, supporting client overriding memory
+ * allocation routines */
+typedef void (*yajl_free_func)(void *ctx, void * ptr);
+
+/** pointer to a realloc function which can resize an allocation. */
+typedef void * (*yajl_realloc_func)(void *ctx, void * ptr, size_t sz);
+
+/** A structure which can be passed to yajl_*_alloc routines to allow the
+ * client to specify memory allocation functions to be used. */
+typedef struct
+{
+ /** pointer to a function that can allocate uninitialized memory */
+ yajl_malloc_func malloc;
+ /** pointer to a function that can resize memory allocations */
+ yajl_realloc_func realloc;
+ /** pointer to a function that can free memory allocated using
+ * reallocFunction or mallocFunction */
+ yajl_free_func free;
+ /** a context pointer that will be passed to above allocation routines */
+ void * ctx;
+} yajl_alloc_funcs;
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/src/yajl/yajl_encode.c b/src/yajl/yajl_encode.c
new file mode 100644
index 0000000..db859a2
--- /dev/null
+++ b/src/yajl/yajl_encode.c
@@ -0,0 +1,220 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "yajl_encode.h"
+
+#include <assert.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+static void CharToHex(unsigned char c, char * hexBuf)
+{
+ const char * hexchar = "0123456789ABCDEF";
+ hexBuf[0] = hexchar[c >> 4];
+ hexBuf[1] = hexchar[c & 0x0F];
+}
+
+void
+yajl_string_encode(const yajl_print_t print,
+ void * ctx,
+ const unsigned char * str,
+ size_t len,
+ int escape_solidus)
+{
+ size_t beg = 0;
+ size_t end = 0;
+ char hexBuf[7];
+ hexBuf[0] = '\\'; hexBuf[1] = 'u'; hexBuf[2] = '0'; hexBuf[3] = '0';
+ hexBuf[6] = 0;
+
+ while (end < len) {
+ const char * escaped = NULL;
+ switch (str[end]) {
+ case '\r': escaped = "\\r"; break;
+ case '\n': escaped = "\\n"; break;
+ case '\\': escaped = "\\\\"; break;
+ /* it is not required to escape a solidus in JSON:
+ * read sec. 2.5: http://www.ietf.org/rfc/rfc4627.txt
+ * specifically, this production from the grammar:
+ * unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
+ */
+ case '/': if (escape_solidus) escaped = "\\/"; break;
+ case '"': escaped = "\\\""; break;
+ case '\f': escaped = "\\f"; break;
+ case '\b': escaped = "\\b"; break;
+ case '\t': escaped = "\\t"; break;
+ default:
+ if ((unsigned char) str[end] < 32) {
+ CharToHex(str[end], hexBuf + 4);
+ escaped = hexBuf;
+ }
+ break;
+ }
+ if (escaped != NULL) {
+ print(ctx, (const char *) (str + beg), end - beg);
+ print(ctx, escaped, (unsigned int)strlen(escaped));
+ beg = ++end;
+ } else {
+ ++end;
+ }
+ }
+ print(ctx, (const char *) (str + beg), end - beg);
+}
+
+static void hexToDigit(unsigned int * val, const unsigned char * hex)
+{
+ unsigned int i;
+ for (i=0;i<4;i++) {
+ unsigned char c = hex[i];
+ if (c >= 'A') c = (c & ~0x20) - 7;
+ c -= '0';
+ assert(!(c & 0xF0));
+ *val = (*val << 4) | c;
+ }
+}
+
+static void Utf32toUtf8(unsigned int codepoint, char * utf8Buf)
+{
+ if (codepoint < 0x80) {
+ utf8Buf[0] = (char) codepoint;
+ utf8Buf[1] = 0;
+ } else if (codepoint < 0x0800) {
+ utf8Buf[0] = (char) ((codepoint >> 6) | 0xC0);
+ utf8Buf[1] = (char) ((codepoint & 0x3F) | 0x80);
+ utf8Buf[2] = 0;
+ } else if (codepoint < 0x10000) {
+ utf8Buf[0] = (char) ((codepoint >> 12) | 0xE0);
+ utf8Buf[1] = (char) (((codepoint >> 6) & 0x3F) | 0x80);
+ utf8Buf[2] = (char) ((codepoint & 0x3F) | 0x80);
+ utf8Buf[3] = 0;
+ } else if (codepoint < 0x200000) {
+ utf8Buf[0] =(char)((codepoint >> 18) | 0xF0);
+ utf8Buf[1] =(char)(((codepoint >> 12) & 0x3F) | 0x80);
+ utf8Buf[2] =(char)(((codepoint >> 6) & 0x3F) | 0x80);
+ utf8Buf[3] =(char)((codepoint & 0x3F) | 0x80);
+ utf8Buf[4] = 0;
+ } else {
+ utf8Buf[0] = '?';
+ utf8Buf[1] = 0;
+ }
+}
+
+void yajl_string_decode(yajl_buf buf, const unsigned char * str,
+ size_t len)
+{
+ size_t beg = 0;
+ size_t end = 0;
+
+ while (end < len) {
+ if (str[end] == '\\') {
+ char utf8Buf[5];
+ const char * unescaped = "?";
+ yajl_buf_append(buf, str + beg, end - beg);
+ switch (str[++end]) {
+ case 'r': unescaped = "\r"; break;
+ case 'n': unescaped = "\n"; break;
+ case '\\': unescaped = "\\"; break;
+ case '/': unescaped = "/"; break;
+ case '"': unescaped = "\""; break;
+ case 'f': unescaped = "\f"; break;
+ case 'b': unescaped = "\b"; break;
+ case 't': unescaped = "\t"; break;
+ case 'u': {
+ unsigned int codepoint = 0;
+ hexToDigit(&codepoint, str + ++end);
+ end+=3;
+ /* check if this is a surrogate */
+ if ((codepoint & 0xFC00) == 0xD800) {
+ if (str[end + 1] == '\\' && str[end + 2] == 'u') {
+ end += 1;
+ unsigned int surrogate = 0;
+ hexToDigit(&surrogate, str + end + 2);
+ codepoint
+ = (((codepoint & 0x3F) << 10)
+ | ((((codepoint >> 6) & 0xF) + 1) << 16)
+ | (surrogate & 0x3FF));
+ end += 5;
+ } else {
+ unescaped = "?";
+ break;
+ }
+ }
+
+ Utf32toUtf8(codepoint, utf8Buf);
+ unescaped = utf8Buf;
+
+ if (codepoint == 0) {
+ yajl_buf_append(buf, unescaped, 1);
+ beg = ++end;
+ continue;
+ }
+
+ break;
+ }
+ default:
+ assert("this should never happen" == NULL);
+ }
+ yajl_buf_append(buf, unescaped, (unsigned int)strlen(unescaped));
+ beg = ++end;
+ } else {
+ end++;
+ }
+ }
+ yajl_buf_append(buf, str + beg, end - beg);
+}
+
+#define ADV_PTR s++; if (!(len--)) return 0;
+
+int yajl_string_validate_utf8(const unsigned char * s, size_t len)
+{
+ if (!len) return 1;
+ if (!s) return 0;
+
+ while (len--) {
+ /* single byte */
+ if (*s <= 0x7f) {
+ /* noop */
+ }
+ /* two byte */
+ else if ((*s >> 5) == 0x6) {
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ }
+ /* three byte */
+ else if ((*s >> 4) == 0x0e) {
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ }
+ /* four byte */
+ else if ((*s >> 3) == 0x1e) {
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ ADV_PTR;
+ if (!((*s >> 6) == 0x2)) return 0;
+ } else {
+ return 0;
+ }
+
+ s++;
+ }
+
+ return 1;
+}
diff --git a/src/yajl/yajl_encode.h b/src/yajl/yajl_encode.h
new file mode 100644
index 0000000..853a1a7
--- /dev/null
+++ b/src/yajl/yajl_encode.h
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_ENCODE_H__
+#define __YAJL_ENCODE_H__
+
+#include "yajl_buf.h"
+#include "api/yajl_gen.h"
+
+void yajl_string_encode(const yajl_print_t printer,
+ void * ctx,
+ const unsigned char * str,
+ size_t length,
+ int escape_solidus);
+
+void yajl_string_decode(yajl_buf buf, const unsigned char * str,
+ size_t length);
+
+int yajl_string_validate_utf8(const unsigned char * s, size_t len);
+
+#endif
diff --git a/src/yajl/yajl_gen.c b/src/yajl/yajl_gen.c
new file mode 100644
index 0000000..0f5c68e
--- /dev/null
+++ b/src/yajl/yajl_gen.c
@@ -0,0 +1,362 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "api/yajl_gen.h"
+#include "yajl_buf.h"
+#include "yajl_encode.h"
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <math.h>
+#include <stdarg.h>
+
+typedef enum {
+ yajl_gen_start,
+ yajl_gen_map_start,
+ yajl_gen_map_key,
+ yajl_gen_map_val,
+ yajl_gen_array_start,
+ yajl_gen_in_array,
+ yajl_gen_complete,
+ yajl_gen_error
+} yajl_gen_state;
+
+struct yajl_gen_t
+{
+ unsigned int flags;
+ unsigned int depth;
+ const char * indentString;
+ yajl_gen_state state[YAJL_MAX_DEPTH];
+ yajl_print_t print;
+ void * ctx; /* yajl_buf */
+ /* memory allocation routines */
+ yajl_alloc_funcs alloc;
+};
+
+int
+yajl_gen_config(yajl_gen g, yajl_gen_option opt, ...)
+{
+ int rv = 1;
+ va_list ap;
+ va_start(ap, opt);
+
+ switch(opt) {
+ case yajl_gen_beautify:
+ case yajl_gen_validate_utf8:
+ case yajl_gen_escape_solidus:
+ if (va_arg(ap, int)) g->flags |= opt;
+ else g->flags &= ~opt;
+ break;
+ case yajl_gen_indent_string: {
+ const char *indent = va_arg(ap, const char *);
+ g->indentString = indent;
+ for (; *indent; indent++) {
+ if (*indent != '\n'
+ && *indent != '\v'
+ && *indent != '\f'
+ && *indent != '\t'
+ && *indent != '\r'
+ && *indent != ' ')
+ {
+ g->indentString = NULL;
+ rv = 0;
+ }
+ }
+ break;
+ }
+ case yajl_gen_print_callback:
+ yajl_buf_free(g->ctx);
+ g->print = va_arg(ap, const yajl_print_t);
+ g->ctx = va_arg(ap, void *);
+ break;
+ default:
+ rv = 0;
+ }
+
+ va_end(ap);
+
+ return rv;
+}
+
+
+
+yajl_gen
+yajl_gen_alloc(const yajl_alloc_funcs * afs)
+{
+ yajl_gen g = NULL;
+ yajl_alloc_funcs afsBuffer;
+
+ /* first order of business is to set up memory allocation routines */
+ if (afs != NULL) {
+ if (afs->malloc == NULL || afs->realloc == NULL || afs->free == NULL)
+ {
+ return NULL;
+ }
+ } else {
+ yajl_set_default_alloc_funcs(&afsBuffer);
+ afs = &afsBuffer;
+ }
+
+ g = (yajl_gen) YA_MALLOC(afs, sizeof(struct yajl_gen_t));
+ if (!g) return NULL;
+
+ memset((void *) g, 0, sizeof(struct yajl_gen_t));
+ /* copy in pointers to allocation routines */
+ memcpy((void *) &(g->alloc), (void *) afs, sizeof(yajl_alloc_funcs));
+
+ g->print = (yajl_print_t)&yajl_buf_append;
+ g->ctx = yajl_buf_alloc(&(g->alloc));
+ g->indentString = " ";
+
+ return g;
+}
+
+void
+yajl_gen_reset(yajl_gen g, const char * sep)
+{
+ g->depth = 0;
+ memset((void *) &(g->state), 0, sizeof(g->state));
+ if (sep != NULL) g->print(g->ctx, sep, strlen(sep));
+}
+
+void
+yajl_gen_free(yajl_gen g)
+{
+ if (g->print == (yajl_print_t)&yajl_buf_append) yajl_buf_free((yajl_buf)g->ctx);
+ YA_FREE(&(g->alloc), g);
+}
+
+#define INSERT_SEP \
+ if (g->state[g->depth] == yajl_gen_map_key || \
+ g->state[g->depth] == yajl_gen_in_array) { \
+ g->print(g->ctx, ",", 1); \
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1); \
+ } else if (g->state[g->depth] == yajl_gen_map_val) { \
+ g->print(g->ctx, ":", 1); \
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, " ", 1); \
+ }
+
+#define INSERT_WHITESPACE \
+ if ((g->flags & yajl_gen_beautify)) { \
+ if (g->state[g->depth] != yajl_gen_map_val) { \
+ unsigned int _i; \
+ for (_i=0;_i<g->depth;_i++) \
+ g->print(g->ctx, \
+ g->indentString, \
+ (unsigned int)strlen(g->indentString)); \
+ } \
+ }
+
+#define ENSURE_NOT_KEY \
+ if (g->state[g->depth] == yajl_gen_map_key || \
+ g->state[g->depth] == yajl_gen_map_start) { \
+ return yajl_gen_keys_must_be_strings; \
+ } \
+
+/* check that we're not complete, or in error state. in a valid state
+ * to be generating */
+#define ENSURE_VALID_STATE \
+ if (g->state[g->depth] == yajl_gen_error) { \
+ return yajl_gen_in_error_state;\
+ } else if (g->state[g->depth] == yajl_gen_complete) { \
+ return yajl_gen_generation_complete; \
+ }
+
+#define INCREMENT_DEPTH \
+ if (++(g->depth) >= YAJL_MAX_DEPTH) return yajl_max_depth_exceeded;
+
+#define DECREMENT_DEPTH \
+ if (--(g->depth) >= YAJL_MAX_DEPTH) return yajl_gen_generation_complete;
+
+#define APPENDED_ATOM \
+ switch (g->state[g->depth]) { \
+ case yajl_gen_start: \
+ g->state[g->depth] = yajl_gen_complete; \
+ break; \
+ case yajl_gen_map_start: \
+ case yajl_gen_map_key: \
+ g->state[g->depth] = yajl_gen_map_val; \
+ break; \
+ case yajl_gen_array_start: \
+ g->state[g->depth] = yajl_gen_in_array; \
+ break; \
+ case yajl_gen_map_val: \
+ g->state[g->depth] = yajl_gen_map_key; \
+ break; \
+ default: \
+ break; \
+ } \
+
+#define FINAL_NEWLINE \
+ if ((g->flags & yajl_gen_beautify) && g->state[g->depth] == yajl_gen_complete) \
+ g->print(g->ctx, "\n", 1);
+
+yajl_gen_status
+yajl_gen_integer(yajl_gen g, long long int number)
+{
+ char i[32];
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ sprintf(i, "%lld", number);
+ g->print(g->ctx, i, (unsigned int)strlen(i));
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+#if defined(_WIN32) || defined(WIN32)
+#include <float.h>
+#define isnan _isnan
+#define isinf !_finite
+#endif
+
+yajl_gen_status
+yajl_gen_double(yajl_gen g, double number)
+{
+ char i[32];
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY;
+ if (isnan(number) || isinf(number)) return yajl_gen_invalid_number;
+ INSERT_SEP; INSERT_WHITESPACE;
+ sprintf(i, "%.20g", number);
+ if (strspn(i, "0123456789-") == strlen(i)) {
+ strcat(i, ".0");
+ }
+ g->print(g->ctx, i, (unsigned int)strlen(i));
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_number(yajl_gen g, const char * s, size_t l)
+{
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ g->print(g->ctx, s, l);
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_string(yajl_gen g, const unsigned char * str,
+ size_t len)
+{
+ // if validation is enabled, check that the string is valid utf8
+ // XXX: This checking could be done a little faster, in the same pass as
+ // the string encoding
+ if (g->flags & yajl_gen_validate_utf8) {
+ if (!yajl_string_validate_utf8(str, len)) {
+ return yajl_gen_invalid_string;
+ }
+ }
+ ENSURE_VALID_STATE; INSERT_SEP; INSERT_WHITESPACE;
+ g->print(g->ctx, "\"", 1);
+ yajl_string_encode(g->print, g->ctx, str, len, g->flags & yajl_gen_escape_solidus);
+ g->print(g->ctx, "\"", 1);
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_null(yajl_gen g)
+{
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ g->print(g->ctx, "null", strlen("null"));
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_bool(yajl_gen g, int boolean)
+{
+ const char * val = boolean ? "true" : "false";
+
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ g->print(g->ctx, val, (unsigned int)strlen(val));
+ APPENDED_ATOM;
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_map_open(yajl_gen g)
+{
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ INCREMENT_DEPTH;
+
+ g->state[g->depth] = yajl_gen_map_start;
+ g->print(g->ctx, "{", 1);
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1);
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_map_close(yajl_gen g)
+{
+ ENSURE_VALID_STATE;
+ DECREMENT_DEPTH;
+
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1);
+ APPENDED_ATOM;
+ INSERT_WHITESPACE;
+ g->print(g->ctx, "}", 1);
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_array_open(yajl_gen g)
+{
+ ENSURE_VALID_STATE; ENSURE_NOT_KEY; INSERT_SEP; INSERT_WHITESPACE;
+ INCREMENT_DEPTH;
+ g->state[g->depth] = yajl_gen_array_start;
+ g->print(g->ctx, "[", 1);
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1);
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_array_close(yajl_gen g)
+{
+ ENSURE_VALID_STATE;
+ DECREMENT_DEPTH;
+ if ((g->flags & yajl_gen_beautify)) g->print(g->ctx, "\n", 1);
+ APPENDED_ATOM;
+ INSERT_WHITESPACE;
+ g->print(g->ctx, "]", 1);
+ FINAL_NEWLINE;
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+yajl_gen_get_buf(yajl_gen g, const unsigned char ** buf,
+ size_t * len)
+{
+ if (g->print != (yajl_print_t)&yajl_buf_append) return yajl_gen_no_buf;
+ *buf = yajl_buf_data((yajl_buf)g->ctx);
+ *len = yajl_buf_len((yajl_buf)g->ctx);
+ return yajl_gen_status_ok;
+}
+
+void
+yajl_gen_clear(yajl_gen g)
+{
+ if (g->print == (yajl_print_t)&yajl_buf_append) yajl_buf_clear((yajl_buf)g->ctx);
+}
diff --git a/src/yajl/yajl_lex.c b/src/yajl/yajl_lex.c
new file mode 100644
index 0000000..0b6f7cc
--- /dev/null
+++ b/src/yajl/yajl_lex.c
@@ -0,0 +1,763 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "yajl_lex.h"
+#include "yajl_buf.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <assert.h>
+#include <string.h>
+
+#ifdef YAJL_LEXER_DEBUG
+static const char *
+tokToStr(yajl_tok tok)
+{
+ switch (tok) {
+ case yajl_tok_bool: return "bool";
+ case yajl_tok_colon: return "colon";
+ case yajl_tok_comma: return "comma";
+ case yajl_tok_eof: return "eof";
+ case yajl_tok_error: return "error";
+ case yajl_tok_left_brace: return "brace";
+ case yajl_tok_left_bracket: return "bracket";
+ case yajl_tok_null: return "null";
+ case yajl_tok_integer: return "integer";
+ case yajl_tok_double: return "double";
+ case yajl_tok_right_brace: return "brace";
+ case yajl_tok_right_bracket: return "bracket";
+ case yajl_tok_string: return "string";
+ case yajl_tok_string_with_escapes: return "string_with_escapes";
+ }
+ return "unknown";
+}
+#endif
+
+/* Impact of the stream parsing feature on the lexer:
+ *
+ * YAJL support stream parsing. That is, the ability to parse the first
+ * bits of a chunk of JSON before the last bits are available (still on
+ * the network or disk). This makes the lexer more complex. The
+ * responsibility of the lexer is to handle transparently the case where
+ * a chunk boundary falls in the middle of a token. This is
+ * accomplished is via a buffer and a character reading abstraction.
+ *
+ * Overview of implementation
+ *
+ * When we lex to end of input string before end of token is hit, we
+ * copy all of the input text composing the token into our lexBuf.
+ *
+ * Every time we read a character, we do so through the readChar function.
+ * readChar's responsibility is to handle pulling all chars from the buffer
+ * before pulling chars from input text
+ */
+
+struct yajl_lexer_t {
+ /* the overal line and char offset into the data */
+ size_t lineOff;
+ size_t charOff;
+
+ /* error */
+ yajl_lex_error error;
+
+ /* a input buffer to handle the case where a token is spread over
+ * multiple chunks */
+ yajl_buf buf;
+
+ /* in the case where we have data in the lexBuf, bufOff holds
+ * the current offset into the lexBuf. */
+ size_t bufOff;
+
+ /* are we using the lex buf? */
+ unsigned int bufInUse;
+
+ /* shall we allow comments? */
+ unsigned int allowComments;
+
+ /* shall we validate utf8 inside strings? */
+ unsigned int validateUTF8;
+
+ yajl_alloc_funcs * alloc;
+};
+
+#define readChar(lxr, txt, off) \
+ (((lxr)->bufInUse && yajl_buf_len((lxr)->buf) && lxr->bufOff < yajl_buf_len((lxr)->buf)) ? \
+ (*((const unsigned char *) yajl_buf_data((lxr)->buf) + ((lxr)->bufOff)++)) : \
+ ((txt)[(*(off))++]))
+
+#define unreadChar(lxr, off) ((*(off) > 0) ? (*(off))-- : ((lxr)->bufOff--))
+
+yajl_lexer
+yajl_lex_alloc(yajl_alloc_funcs * alloc,
+ unsigned int allowComments, unsigned int validateUTF8)
+{
+ yajl_lexer lxr = (yajl_lexer) YA_MALLOC(alloc, sizeof(struct yajl_lexer_t));
+ memset((void *) lxr, 0, sizeof(struct yajl_lexer_t));
+ lxr->buf = yajl_buf_alloc(alloc);
+ lxr->allowComments = allowComments;
+ lxr->validateUTF8 = validateUTF8;
+ lxr->alloc = alloc;
+ return lxr;
+}
+
+void
+yajl_lex_free(yajl_lexer lxr)
+{
+ yajl_buf_free(lxr->buf);
+ YA_FREE(lxr->alloc, lxr);
+ return;
+}
+
+/* a lookup table which lets us quickly determine three things:
+ * VEC - valid escaped control char
+ * note. the solidus '/' may be escaped or not.
+ * IJC - invalid json char
+ * VHC - valid hex char
+ * NFP - needs further processing (from a string scanning perspective)
+ * NUC - needs utf8 checking when enabled (from a string scanning perspective)
+ */
+#define VEC 0x01
+#define IJC 0x02
+#define VHC 0x04
+#define NFP 0x08
+#define NUC 0x10
+
+static const char charLookupTable[256] =
+{
+/*00*/ IJC , IJC , IJC , IJC , IJC , IJC , IJC , IJC ,
+/*08*/ IJC , IJC , IJC , IJC , IJC , IJC , IJC , IJC ,
+/*10*/ IJC , IJC , IJC , IJC , IJC , IJC , IJC , IJC ,
+/*18*/ IJC , IJC , IJC , IJC , IJC , IJC , IJC , IJC ,
+
+/*20*/ 0 , 0 , NFP|VEC|IJC, 0 , 0 , 0 , 0 , 0 ,
+/*28*/ 0 , 0 , 0 , 0 , 0 , 0 , 0 , VEC ,
+/*30*/ VHC , VHC , VHC , VHC , VHC , VHC , VHC , VHC ,
+/*38*/ VHC , VHC , 0 , 0 , 0 , 0 , 0 , 0 ,
+
+/*40*/ 0 , VHC , VHC , VHC , VHC , VHC , VHC , 0 ,
+/*48*/ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
+/*50*/ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
+/*58*/ 0 , 0 , 0 , 0 , NFP|VEC|IJC, 0 , 0 , 0 ,
+
+/*60*/ 0 , VHC , VEC|VHC, VHC , VHC , VHC , VEC|VHC, 0 ,
+/*68*/ 0 , 0 , 0 , 0 , 0 , 0 , VEC , 0 ,
+/*70*/ 0 , 0 , VEC , 0 , VEC , 0 , 0 , 0 ,
+/*78*/ 0 , 0 , 0 , 0 , 0 , 0 , 0 , 0 ,
+
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC ,
+ NUC , NUC , NUC , NUC , NUC , NUC , NUC , NUC
+};
+
+/** process a variable length utf8 encoded codepoint.
+ *
+ * returns:
+ * yajl_tok_string - if valid utf8 char was parsed and offset was
+ * advanced
+ * yajl_tok_eof - if end of input was hit before validation could
+ * complete
+ * yajl_tok_error - if invalid utf8 was encountered
+ *
+ * NOTE: on error the offset will point to the first char of the
+ * invalid utf8 */
+#define UTF8_CHECK_EOF if (*offset >= jsonTextLen) { return yajl_tok_eof; }
+
+static yajl_tok
+yajl_lex_utf8_char(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset,
+ unsigned char curChar)
+{
+ if (curChar <= 0x7f) {
+ /* single byte */
+ return yajl_tok_string;
+ } else if ((curChar >> 5) == 0x6) {
+ /* two byte */
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) return yajl_tok_string;
+ } else if ((curChar >> 4) == 0x0e) {
+ /* three byte */
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) {
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) return yajl_tok_string;
+ }
+ } else if ((curChar >> 3) == 0x1e) {
+ /* four byte */
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) {
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) {
+ UTF8_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if ((curChar >> 6) == 0x2) return yajl_tok_string;
+ }
+ }
+ }
+
+ return yajl_tok_error;
+}
+
+/* lex a string. input is the lexer, pointer to beginning of
+ * json text, and start of string (offset).
+ * a token is returned which has the following meanings:
+ * yajl_tok_string: lex of string was successful. offset points to
+ * terminating '"'.
+ * yajl_tok_eof: end of text was encountered before we could complete
+ * the lex.
+ * yajl_tok_error: embedded in the string were unallowable chars. offset
+ * points to the offending char
+ */
+#define STR_CHECK_EOF \
+if (*offset >= jsonTextLen) { \
+ tok = yajl_tok_eof; \
+ goto finish_string_lex; \
+}
+
+/** scan a string for interesting characters that might need further
+ * review. return the number of chars that are uninteresting and can
+ * be skipped.
+ * (lth) hi world, any thoughts on how to make this routine faster? */
+static size_t
+yajl_string_scan(const unsigned char * buf, size_t len, int utf8check)
+{
+ unsigned char mask = IJC|NFP|(utf8check ? NUC : 0);
+ size_t skip = 0;
+ while (skip < len && !(charLookupTable[*buf] & mask))
+ {
+ skip++;
+ buf++;
+ }
+ return skip;
+}
+
+static yajl_tok
+yajl_lex_string(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset)
+{
+ yajl_tok tok = yajl_tok_error;
+ int hasEscapes = 0;
+
+ for (;;) {
+ unsigned char curChar;
+
+ /* now jump into a faster scanning routine to skip as much
+ * of the buffers as possible */
+ {
+ const unsigned char * p;
+ size_t len;
+
+ if ((lexer->bufInUse && yajl_buf_len(lexer->buf) &&
+ lexer->bufOff < yajl_buf_len(lexer->buf)))
+ {
+ p = ((const unsigned char *) yajl_buf_data(lexer->buf) +
+ (lexer->bufOff));
+ len = yajl_buf_len(lexer->buf) - lexer->bufOff;
+ lexer->bufOff += yajl_string_scan(p, len, lexer->validateUTF8);
+ }
+ else if (*offset < jsonTextLen)
+ {
+ p = jsonText + *offset;
+ len = jsonTextLen - *offset;
+ *offset += yajl_string_scan(p, len, lexer->validateUTF8);
+ }
+ }
+
+ STR_CHECK_EOF;
+
+ curChar = readChar(lexer, jsonText, offset);
+
+ /* quote terminates */
+ if (curChar == '"') {
+ tok = yajl_tok_string;
+ break;
+ }
+ /* backslash escapes a set of control chars, */
+ else if (curChar == '\\') {
+ hasEscapes = 1;
+ STR_CHECK_EOF;
+
+ /* special case \u */
+ curChar = readChar(lexer, jsonText, offset);
+ if (curChar == 'u') {
+ unsigned int i = 0;
+
+ for (i=0;i<4;i++) {
+ STR_CHECK_EOF;
+ curChar = readChar(lexer, jsonText, offset);
+ if (!(charLookupTable[curChar] & VHC)) {
+ /* back up to offending char */
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_string_invalid_hex_char;
+ goto finish_string_lex;
+ }
+ }
+ } else if (!(charLookupTable[curChar] & VEC)) {
+ /* back up to offending char */
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_string_invalid_escaped_char;
+ goto finish_string_lex;
+ }
+ }
+ /* when not validating UTF8 it's a simple table lookup to determine
+ * if the present character is invalid */
+ else if(charLookupTable[curChar] & IJC) {
+ /* back up to offending char */
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_string_invalid_json_char;
+ goto finish_string_lex;
+ }
+ /* when in validate UTF8 mode we need to do some extra work */
+ else if (lexer->validateUTF8) {
+ yajl_tok t = yajl_lex_utf8_char(lexer, jsonText, jsonTextLen,
+ offset, curChar);
+
+ if (t == yajl_tok_eof) {
+ tok = yajl_tok_eof;
+ goto finish_string_lex;
+ } else if (t == yajl_tok_error) {
+ lexer->error = yajl_lex_string_invalid_utf8;
+ goto finish_string_lex;
+ }
+ }
+ /* accept it, and move on */
+ }
+ finish_string_lex:
+ /* tell our buddy, the parser, wether he needs to process this string
+ * again */
+ if (hasEscapes && tok == yajl_tok_string) {
+ tok = yajl_tok_string_with_escapes;
+ }
+
+ return tok;
+}
+
+#define RETURN_IF_EOF if (*offset >= jsonTextLen) return yajl_tok_eof;
+
+static yajl_tok
+yajl_lex_number(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset)
+{
+ /** XXX: numbers are the only entities in json that we must lex
+ * _beyond_ in order to know that they are complete. There
+ * is an ambiguous case for integers at EOF. */
+
+ unsigned char c;
+
+ yajl_tok tok = yajl_tok_integer;
+
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+
+ /* optional leading minus */
+ if (c == '-') {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ }
+
+ /* a single zero, or a series of integers */
+ if (c == '0') {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ } else if (c >= '1' && c <= '9') {
+ do {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ } while (c >= '0' && c <= '9');
+ } else {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_missing_integer_after_minus;
+ return yajl_tok_error;
+ }
+
+ /* optional fraction (indicates this is floating point) */
+ if (c == '.') {
+ int numRd = 0;
+
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+
+ while (c >= '0' && c <= '9') {
+ numRd++;
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ }
+
+ if (!numRd) {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_missing_integer_after_decimal;
+ return yajl_tok_error;
+ }
+ tok = yajl_tok_double;
+ }
+
+ /* optional exponent (indicates this is floating point) */
+ if (c == 'e' || c == 'E') {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+
+ /* optional sign */
+ if (c == '+' || c == '-') {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ }
+
+ if (c >= '0' && c <= '9') {
+ do {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ } while (c >= '0' && c <= '9');
+ } else {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_missing_integer_after_exponent;
+ return yajl_tok_error;
+ }
+ tok = yajl_tok_double;
+ }
+
+ /* we always go "one too far" */
+ unreadChar(lexer, offset);
+
+ return tok;
+}
+
+static yajl_tok
+yajl_lex_comment(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset)
+{
+ unsigned char c;
+
+ yajl_tok tok = yajl_tok_comment;
+
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+
+ /* either slash or star expected */
+ if (c == '/') {
+ /* now we throw away until end of line */
+ do {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ } while (c != '\n');
+ } else if (c == '*') {
+ /* now we throw away until end of comment */
+ for (;;) {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ if (c == '*') {
+ RETURN_IF_EOF;
+ c = readChar(lexer, jsonText, offset);
+ if (c == '/') {
+ break;
+ } else {
+ unreadChar(lexer, offset);
+ }
+ }
+ }
+ } else {
+ lexer->error = yajl_lex_invalid_char;
+ tok = yajl_tok_error;
+ }
+
+ return tok;
+}
+
+yajl_tok
+yajl_lex_lex(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset,
+ const unsigned char ** outBuf, size_t * outLen)
+{
+ yajl_tok tok = yajl_tok_error;
+ unsigned char c;
+ size_t startOffset = *offset;
+
+ *outBuf = NULL;
+ *outLen = 0;
+
+ for (;;) {
+ assert(*offset <= jsonTextLen);
+
+ if (*offset >= jsonTextLen) {
+ tok = yajl_tok_eof;
+ goto lexed;
+ }
+
+ c = readChar(lexer, jsonText, offset);
+
+ switch (c) {
+ case '{':
+ tok = yajl_tok_left_bracket;
+ goto lexed;
+ case '}':
+ tok = yajl_tok_right_bracket;
+ goto lexed;
+ case '[':
+ tok = yajl_tok_left_brace;
+ goto lexed;
+ case ']':
+ tok = yajl_tok_right_brace;
+ goto lexed;
+ case ',':
+ tok = yajl_tok_comma;
+ goto lexed;
+ case ':':
+ tok = yajl_tok_colon;
+ goto lexed;
+ case '\t': case '\n': case '\v': case '\f': case '\r': case ' ':
+ startOffset++;
+ break;
+ case 't': {
+ const char * want = "rue";
+ do {
+ if (*offset >= jsonTextLen) {
+ tok = yajl_tok_eof;
+ goto lexed;
+ }
+ c = readChar(lexer, jsonText, offset);
+ if (c != *want) {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_invalid_string;
+ tok = yajl_tok_error;
+ goto lexed;
+ }
+ } while (*(++want));
+ tok = yajl_tok_bool;
+ goto lexed;
+ }
+ case 'f': {
+ const char * want = "alse";
+ do {
+ if (*offset >= jsonTextLen) {
+ tok = yajl_tok_eof;
+ goto lexed;
+ }
+ c = readChar(lexer, jsonText, offset);
+ if (c != *want) {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_invalid_string;
+ tok = yajl_tok_error;
+ goto lexed;
+ }
+ } while (*(++want));
+ tok = yajl_tok_bool;
+ goto lexed;
+ }
+ case 'n': {
+ const char * want = "ull";
+ do {
+ if (*offset >= jsonTextLen) {
+ tok = yajl_tok_eof;
+ goto lexed;
+ }
+ c = readChar(lexer, jsonText, offset);
+ if (c != *want) {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_invalid_string;
+ tok = yajl_tok_error;
+ goto lexed;
+ }
+ } while (*(++want));
+ tok = yajl_tok_null;
+ goto lexed;
+ }
+ case '"': {
+ tok = yajl_lex_string(lexer, (const unsigned char *) jsonText,
+ jsonTextLen, offset);
+ goto lexed;
+ }
+ case '-':
+ case '0': case '1': case '2': case '3': case '4':
+ case '5': case '6': case '7': case '8': case '9': {
+ /* integer parsing wants to start from the beginning */
+ unreadChar(lexer, offset);
+ tok = yajl_lex_number(lexer, (const unsigned char *) jsonText,
+ jsonTextLen, offset);
+ goto lexed;
+ }
+ case '/':
+ /* hey, look, a probable comment! If comments are disabled
+ * it's an error. */
+ if (!lexer->allowComments) {
+ unreadChar(lexer, offset);
+ lexer->error = yajl_lex_unallowed_comment;
+ tok = yajl_tok_error;
+ goto lexed;
+ }
+ /* if comments are enabled, then we should try to lex
+ * the thing. possible outcomes are
+ * - successful lex (tok_comment, which means continue),
+ * - malformed comment opening (slash not followed by
+ * '*' or '/') (tok_error)
+ * - eof hit. (tok_eof) */
+ tok = yajl_lex_comment(lexer, (const unsigned char *) jsonText,
+ jsonTextLen, offset);
+ if (tok == yajl_tok_comment) {
+ /* "error" is silly, but that's the initial
+ * state of tok. guilty until proven innocent. */
+ tok = yajl_tok_error;
+ yajl_buf_clear(lexer->buf);
+ lexer->bufInUse = 0;
+ startOffset = *offset;
+ break;
+ }
+ /* hit error or eof, bail */
+ goto lexed;
+ default:
+ lexer->error = yajl_lex_invalid_char;
+ tok = yajl_tok_error;
+ goto lexed;
+ }
+ }
+
+
+ lexed:
+ /* need to append to buffer if the buffer is in use or
+ * if it's an EOF token */
+ if (tok == yajl_tok_eof || lexer->bufInUse) {
+ if (!lexer->bufInUse) yajl_buf_clear(lexer->buf);
+ lexer->bufInUse = 1;
+ yajl_buf_append(lexer->buf, jsonText + startOffset, *offset - startOffset);
+ lexer->bufOff = 0;
+
+ if (tok != yajl_tok_eof) {
+ *outBuf = yajl_buf_data(lexer->buf);
+ *outLen = yajl_buf_len(lexer->buf);
+ lexer->bufInUse = 0;
+ }
+ } else if (tok != yajl_tok_error) {
+ *outBuf = jsonText + startOffset;
+ *outLen = *offset - startOffset;
+ }
+
+ /* special case for strings. skip the quotes. */
+ if (tok == yajl_tok_string || tok == yajl_tok_string_with_escapes)
+ {
+ assert(*outLen >= 2);
+ (*outBuf)++;
+ *outLen -= 2;
+ }
+
+
+#ifdef YAJL_LEXER_DEBUG
+ if (tok == yajl_tok_error) {
+ printf("lexical error: %s\n",
+ yajl_lex_error_to_string(yajl_lex_get_error(lexer)));
+ } else if (tok == yajl_tok_eof) {
+ printf("EOF hit\n");
+ } else {
+ printf("lexed %s: '", tokToStr(tok));
+ fwrite(*outBuf, 1, *outLen, stdout);
+ printf("'\n");
+ }
+#endif
+
+ return tok;
+}
+
+const char *
+yajl_lex_error_to_string(yajl_lex_error error)
+{
+ switch (error) {
+ case yajl_lex_e_ok:
+ return "ok, no error";
+ case yajl_lex_string_invalid_utf8:
+ return "invalid bytes in UTF8 string.";
+ case yajl_lex_string_invalid_escaped_char:
+ return "inside a string, '\\' occurs before a character "
+ "which it may not.";
+ case yajl_lex_string_invalid_json_char:
+ return "invalid character inside string.";
+ case yajl_lex_string_invalid_hex_char:
+ return "invalid (non-hex) character occurs after '\\u' inside "
+ "string.";
+ case yajl_lex_invalid_char:
+ return "invalid char in json text.";
+ case yajl_lex_invalid_string:
+ return "invalid string in json text.";
+ case yajl_lex_missing_integer_after_exponent:
+ return "malformed number, a digit is required after the exponent.";
+ case yajl_lex_missing_integer_after_decimal:
+ return "malformed number, a digit is required after the "
+ "decimal point.";
+ case yajl_lex_missing_integer_after_minus:
+ return "malformed number, a digit is required after the "
+ "minus sign.";
+ case yajl_lex_unallowed_comment:
+ return "probable comment found in input text, comments are "
+ "not enabled.";
+ }
+ return "unknown error code";
+}
+
+
+/** allows access to more specific information about the lexical
+ * error when yajl_lex_lex returns yajl_tok_error. */
+yajl_lex_error
+yajl_lex_get_error(yajl_lexer lexer)
+{
+ if (lexer == NULL) return (yajl_lex_error) -1;
+ return lexer->error;
+}
+
+size_t yajl_lex_current_line(yajl_lexer lexer)
+{
+ return lexer->lineOff;
+}
+
+size_t yajl_lex_current_char(yajl_lexer lexer)
+{
+ return lexer->charOff;
+}
+
+yajl_tok yajl_lex_peek(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t offset)
+{
+ const unsigned char * outBuf;
+ size_t outLen;
+ size_t bufLen = yajl_buf_len(lexer->buf);
+ size_t bufOff = lexer->bufOff;
+ unsigned int bufInUse = lexer->bufInUse;
+ yajl_tok tok;
+
+ tok = yajl_lex_lex(lexer, jsonText, jsonTextLen, &offset,
+ &outBuf, &outLen);
+
+ lexer->bufOff = bufOff;
+ lexer->bufInUse = bufInUse;
+ yajl_buf_truncate(lexer->buf, bufLen);
+
+ return tok;
+}
diff --git a/src/yajl/yajl_lex.h b/src/yajl/yajl_lex.h
new file mode 100644
index 0000000..fd17c00
--- /dev/null
+++ b/src/yajl/yajl_lex.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_LEX_H__
+#define __YAJL_LEX_H__
+
+#include "api/yajl_common.h"
+
+typedef enum {
+ yajl_tok_bool,
+ yajl_tok_colon,
+ yajl_tok_comma,
+ yajl_tok_eof,
+ yajl_tok_error,
+ yajl_tok_left_brace,
+ yajl_tok_left_bracket,
+ yajl_tok_null,
+ yajl_tok_right_brace,
+ yajl_tok_right_bracket,
+
+ /* we differentiate between integers and doubles to allow the
+ * parser to interpret the number without re-scanning */
+ yajl_tok_integer,
+ yajl_tok_double,
+
+ /* we differentiate between strings which require further processing,
+ * and strings that do not */
+ yajl_tok_string,
+ yajl_tok_string_with_escapes,
+
+ /* comment tokens are not currently returned to the parser, ever */
+ yajl_tok_comment
+} yajl_tok;
+
+typedef struct yajl_lexer_t * yajl_lexer;
+
+yajl_lexer yajl_lex_alloc(yajl_alloc_funcs * alloc,
+ unsigned int allowComments,
+ unsigned int validateUTF8);
+
+void yajl_lex_free(yajl_lexer lexer);
+
+/**
+ * run/continue a lex. "offset" is an input/output parameter.
+ * It should be initialized to zero for a
+ * new chunk of target text, and upon subsetquent calls with the same
+ * target text should passed with the value of the previous invocation.
+ *
+ * the client may be interested in the value of offset when an error is
+ * returned from the lexer. This allows the client to render useful
+ * error messages.
+ *
+ * When you pass the next chunk of data, context should be reinitialized
+ * to zero.
+ *
+ * Finally, the output buffer is usually just a pointer into the jsonText,
+ * however in cases where the entity being lexed spans multiple chunks,
+ * the lexer will buffer the entity and the data returned will be
+ * a pointer into that buffer.
+ *
+ * This behavior is abstracted from client code except for the performance
+ * implications which require that the client choose a reasonable chunk
+ * size to get adequate performance.
+ */
+yajl_tok yajl_lex_lex(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t * offset,
+ const unsigned char ** outBuf, size_t * outLen);
+
+/** have a peek at the next token, but don't move the lexer forward */
+yajl_tok yajl_lex_peek(yajl_lexer lexer, const unsigned char * jsonText,
+ size_t jsonTextLen, size_t offset);
+
+
+typedef enum {
+ yajl_lex_e_ok = 0,
+ yajl_lex_string_invalid_utf8,
+ yajl_lex_string_invalid_escaped_char,
+ yajl_lex_string_invalid_json_char,
+ yajl_lex_string_invalid_hex_char,
+ yajl_lex_invalid_char,
+ yajl_lex_invalid_string,
+ yajl_lex_missing_integer_after_decimal,
+ yajl_lex_missing_integer_after_exponent,
+ yajl_lex_missing_integer_after_minus,
+ yajl_lex_unallowed_comment
+} yajl_lex_error;
+
+const char * yajl_lex_error_to_string(yajl_lex_error error);
+
+/** allows access to more specific information about the lexical
+ * error when yajl_lex_lex returns yajl_tok_error. */
+yajl_lex_error yajl_lex_get_error(yajl_lexer lexer);
+
+/** get the current offset into the most recently lexed json string. */
+size_t yajl_lex_current_offset(yajl_lexer lexer);
+
+/** get the number of lines lexed by this lexer instance */
+size_t yajl_lex_current_line(yajl_lexer lexer);
+
+/** get the number of chars lexed by this lexer instance since the last
+ * \n or \r */
+size_t yajl_lex_current_char(yajl_lexer lexer);
+
+#endif
diff --git a/src/yajl/yajl_parser.c b/src/yajl/yajl_parser.c
new file mode 100644
index 0000000..c0c538e
--- /dev/null
+++ b/src/yajl/yajl_parser.c
@@ -0,0 +1,498 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include "api/yajl_parse.h"
+#include "yajl_lex.h"
+#include "yajl_parser.h"
+#include "yajl_encode.h"
+#include "yajl_bytestack.h"
+
+#include <stdlib.h>
+#include <limits.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+#include <assert.h>
+#include <math.h>
+
+#define MAX_VALUE_TO_MULTIPLY ((LLONG_MAX / 10) + (LLONG_MAX % 10))
+
+ /* same semantics as strtol */
+long long
+yajl_parse_integer(const unsigned char *number, unsigned int length)
+{
+ long long ret = 0;
+ long sign = 1;
+ const unsigned char *pos = number;
+ if (*pos == '-') { pos++; sign = -1; }
+ if (*pos == '+') { pos++; }
+
+ while (pos < number + length) {
+ if ( ret > MAX_VALUE_TO_MULTIPLY ) {
+ errno = ERANGE;
+ return sign == 1 ? LLONG_MAX : LLONG_MIN;
+ }
+ ret *= 10;
+ if (LLONG_MAX - ret < (*pos - '0')) {
+ errno = ERANGE;
+ return sign == 1 ? LLONG_MAX : LLONG_MIN;
+ }
+ if (*pos < '0' || *pos > '9') {
+ errno = ERANGE;
+ return sign == 1 ? LLONG_MAX : LLONG_MIN;
+ }
+ ret += (*pos++ - '0');
+ }
+
+ return sign * ret;
+}
+
+unsigned char *
+yajl_render_error_string(yajl_handle hand, const unsigned char * jsonText,
+ size_t jsonTextLen, int verbose)
+{
+ size_t offset = hand->bytesConsumed;
+ unsigned char * str;
+ const char * errorType = NULL;
+ const char * errorText = NULL;
+ char text[72];
+ const char * arrow = " (right here) ------^\n";
+
+ if (yajl_bs_current(hand->stateStack) == yajl_state_parse_error) {
+ errorType = "parse";
+ errorText = hand->parseError;
+ } else if (yajl_bs_current(hand->stateStack) == yajl_state_lexical_error) {
+ errorType = "lexical";
+ errorText = yajl_lex_error_to_string(yajl_lex_get_error(hand->lexer));
+ } else {
+ errorType = "unknown";
+ }
+
+ {
+ size_t memneeded = 0;
+ memneeded += strlen(errorType);
+ memneeded += strlen(" error");
+ if (errorText != NULL) {
+ memneeded += strlen(": ");
+ memneeded += strlen(errorText);
+ }
+ str = (unsigned char *) YA_MALLOC(&(hand->alloc), memneeded + 2);
+ if (!str) return NULL;
+ str[0] = 0;
+ strcat((char *) str, errorType);
+ strcat((char *) str, " error");
+ if (errorText != NULL) {
+ strcat((char *) str, ": ");
+ strcat((char *) str, errorText);
+ }
+ strcat((char *) str, "\n");
+ }
+
+ /* now we append as many spaces as needed to make sure the error
+ * falls at char 41, if verbose was specified */
+ if (verbose) {
+ size_t start, end, i;
+ size_t spacesNeeded;
+
+ spacesNeeded = (offset < 30 ? 40 - offset : 10);
+ start = (offset >= 30 ? offset - 30 : 0);
+ end = (offset + 30 > jsonTextLen ? jsonTextLen : offset + 30);
+
+ for (i=0;i<spacesNeeded;i++) text[i] = ' ';
+
+ for (;start < end;start++, i++) {
+ if (jsonText[start] != '\n' && jsonText[start] != '\r' && jsonText[start] != '\t')
+ {
+ text[i] = jsonText[start];
+ }
+ else
+ {
+ text[i] = ' ';
+ }
+ }
+ assert(i <= 71);
+ text[i++] = '\n';
+ text[i] = 0;
+ {
+ char * newStr = (char *)
+ YA_MALLOC(&(hand->alloc), (unsigned int)(strlen((char *) str) +
+ strlen((char *) text) +
+ strlen(arrow) + 1));
+ if (newStr) {
+ newStr[0] = 0;
+ strcat((char *) newStr, (char *) str);
+ strcat((char *) newStr, text);
+ strcat((char *) newStr, arrow);
+ }
+ YA_FREE(&(hand->alloc), str);
+ str = (unsigned char *) newStr;
+ }
+ }
+ return str;
+}
+
+/* check for client cancelation */
+#define _CC_CHK(x) \
+ if (!(x)) { \
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error); \
+ hand->parseError = \
+ "client cancelled parse via callback return value"; \
+ return yajl_status_client_canceled; \
+ }
+
+
+yajl_status
+yajl_do_finish(yajl_handle hand)
+{
+ yajl_status stat;
+ stat = yajl_do_parse(hand,(const unsigned char *) " ",1);
+
+ if (stat != yajl_status_ok) return stat;
+
+ switch(yajl_bs_current(hand->stateStack))
+ {
+ case yajl_state_parse_error:
+ case yajl_state_lexical_error:
+ return yajl_status_error;
+ case yajl_state_got_value:
+ case yajl_state_parse_complete:
+ return yajl_status_ok;
+ default:
+ if (!(hand->flags & yajl_allow_partial_values))
+ {
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "premature EOF";
+ return yajl_status_error;
+ }
+ return yajl_status_ok;
+ }
+}
+
+yajl_status
+yajl_do_parse(yajl_handle hand, const unsigned char * jsonText,
+ size_t jsonTextLen)
+{
+ yajl_tok tok;
+ const unsigned char * buf;
+ size_t bufLen;
+ size_t * offset = &(hand->bytesConsumed);
+
+ *offset = 0;
+
+ around_again:
+ switch (yajl_bs_current(hand->stateStack)) {
+ case yajl_state_parse_complete:
+ if (hand->flags & yajl_allow_multiple_values) {
+ yajl_bs_set(hand->stateStack, yajl_state_got_value);
+ goto around_again;
+ }
+ if (!(hand->flags & yajl_allow_trailing_garbage)) {
+ if (*offset != jsonTextLen) {
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ if (tok != yajl_tok_eof) {
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "trailing garbage";
+ }
+ goto around_again;
+ }
+ }
+ return yajl_status_ok;
+ case yajl_state_lexical_error:
+ case yajl_state_parse_error:
+ return yajl_status_error;
+ case yajl_state_start:
+ case yajl_state_got_value:
+ case yajl_state_map_need_val:
+ case yajl_state_array_need_val:
+ case yajl_state_array_start: {
+ /* for arrays and maps, we advance the state for this
+ * depth, then push the state of the next depth.
+ * If an error occurs during the parsing of the nesting
+ * enitity, the state at this level will not matter.
+ * a state that needs pushing will be anything other
+ * than state_start */
+
+ yajl_state stateToPush = yajl_state_start;
+
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+
+ switch (tok) {
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ case yajl_tok_string:
+ if (hand->callbacks && hand->callbacks->yajl_string) {
+ _CC_CHK(hand->callbacks->yajl_string(hand->ctx,
+ buf, bufLen));
+ }
+ break;
+ case yajl_tok_string_with_escapes:
+ if (hand->callbacks && hand->callbacks->yajl_string) {
+ yajl_buf_clear(hand->decodeBuf);
+ yajl_string_decode(hand->decodeBuf, buf, bufLen);
+ _CC_CHK(hand->callbacks->yajl_string(
+ hand->ctx, yajl_buf_data(hand->decodeBuf),
+ yajl_buf_len(hand->decodeBuf)));
+ }
+ break;
+ case yajl_tok_bool:
+ if (hand->callbacks && hand->callbacks->yajl_boolean) {
+ _CC_CHK(hand->callbacks->yajl_boolean(hand->ctx,
+ *buf == 't'));
+ }
+ break;
+ case yajl_tok_null:
+ if (hand->callbacks && hand->callbacks->yajl_null) {
+ _CC_CHK(hand->callbacks->yajl_null(hand->ctx));
+ }
+ break;
+ case yajl_tok_left_bracket:
+ if (hand->callbacks && hand->callbacks->yajl_start_map) {
+ _CC_CHK(hand->callbacks->yajl_start_map(hand->ctx));
+ }
+ stateToPush = yajl_state_map_start;
+ break;
+ case yajl_tok_left_brace:
+ if (hand->callbacks && hand->callbacks->yajl_start_array) {
+ _CC_CHK(hand->callbacks->yajl_start_array(hand->ctx));
+ }
+ stateToPush = yajl_state_array_start;
+ break;
+ case yajl_tok_integer:
+ if (hand->callbacks) {
+ if (hand->callbacks->yajl_number) {
+ _CC_CHK(hand->callbacks->yajl_number(
+ hand->ctx,(const char *) buf, bufLen));
+ } else if (hand->callbacks->yajl_integer) {
+ long long int i = 0;
+ errno = 0;
+ i = yajl_parse_integer(buf, bufLen);
+ if ((i == LLONG_MIN || i == LLONG_MAX) &&
+ errno == ERANGE)
+ {
+ yajl_bs_set(hand->stateStack,
+ yajl_state_parse_error);
+ hand->parseError = "integer overflow" ;
+ /* try to restore error offset */
+ if (*offset >= bufLen) *offset -= bufLen;
+ else *offset = 0;
+ goto around_again;
+ }
+ _CC_CHK(hand->callbacks->yajl_integer(hand->ctx,
+ i));
+ }
+ }
+ break;
+ case yajl_tok_double:
+ if (hand->callbacks) {
+ if (hand->callbacks->yajl_number) {
+ _CC_CHK(hand->callbacks->yajl_number(
+ hand->ctx, (const char *) buf, bufLen));
+ } else if (hand->callbacks->yajl_double) {
+ double d = 0.0;
+ yajl_buf_clear(hand->decodeBuf);
+ yajl_buf_append(hand->decodeBuf, buf, bufLen);
+ buf = yajl_buf_data(hand->decodeBuf);
+ errno = 0;
+ d = strtod((char *) buf, NULL);
+ if ((d == HUGE_VAL || d == -HUGE_VAL) &&
+ errno == ERANGE)
+ {
+ yajl_bs_set(hand->stateStack,
+ yajl_state_parse_error);
+ hand->parseError = "numeric (floating point) "
+ "overflow";
+ /* try to restore error offset */
+ if (*offset >= bufLen) *offset -= bufLen;
+ else *offset = 0;
+ goto around_again;
+ }
+ _CC_CHK(hand->callbacks->yajl_double(hand->ctx,
+ d));
+ }
+ }
+ break;
+ case yajl_tok_right_brace: {
+ if (yajl_bs_current(hand->stateStack) ==
+ yajl_state_array_start)
+ {
+ if (hand->callbacks &&
+ hand->callbacks->yajl_end_array)
+ {
+ _CC_CHK(hand->callbacks->yajl_end_array(hand->ctx));
+ }
+ yajl_bs_pop(hand->stateStack);
+ goto around_again;
+ }
+ /* intentional fall-through */
+ }
+ case yajl_tok_colon:
+ case yajl_tok_comma:
+ case yajl_tok_right_bracket:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError =
+ "unallowed token at this point in JSON text";
+ goto around_again;
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "invalid token, internal error";
+ goto around_again;
+ }
+ /* got a value. transition depends on the state we're in. */
+ {
+ yajl_state s = yajl_bs_current(hand->stateStack);
+ if (s == yajl_state_start || s == yajl_state_got_value) {
+ yajl_bs_set(hand->stateStack, yajl_state_parse_complete);
+ } else if (s == yajl_state_map_need_val) {
+ yajl_bs_set(hand->stateStack, yajl_state_map_got_val);
+ } else {
+ yajl_bs_set(hand->stateStack, yajl_state_array_got_val);
+ }
+ }
+ if (stateToPush != yajl_state_start) {
+ yajl_bs_push(hand->stateStack, stateToPush);
+ }
+
+ goto around_again;
+ }
+ case yajl_state_map_start:
+ case yajl_state_map_need_key: {
+ /* only difference between these two states is that in
+ * start '}' is valid, whereas in need_key, we've parsed
+ * a comma, and a string key _must_ follow */
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ switch (tok) {
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ case yajl_tok_string_with_escapes:
+ if (hand->callbacks && hand->callbacks->yajl_map_key) {
+ yajl_buf_clear(hand->decodeBuf);
+ yajl_string_decode(hand->decodeBuf, buf, bufLen);
+ buf = yajl_buf_data(hand->decodeBuf);
+ bufLen = yajl_buf_len(hand->decodeBuf);
+ }
+ /* intentional fall-through */
+ case yajl_tok_string:
+ if (hand->callbacks && hand->callbacks->yajl_map_key) {
+ _CC_CHK(hand->callbacks->yajl_map_key(hand->ctx, buf,
+ bufLen));
+ }
+ yajl_bs_set(hand->stateStack, yajl_state_map_sep);
+ goto around_again;
+ case yajl_tok_right_bracket:
+ if (yajl_bs_current(hand->stateStack) ==
+ yajl_state_map_start)
+ {
+ if (hand->callbacks && hand->callbacks->yajl_end_map) {
+ _CC_CHK(hand->callbacks->yajl_end_map(hand->ctx));
+ }
+ yajl_bs_pop(hand->stateStack);
+ goto around_again;
+ }
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError =
+ "invalid object key (must be a string)";
+ goto around_again;
+ }
+ }
+ case yajl_state_map_sep: {
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ switch (tok) {
+ case yajl_tok_colon:
+ yajl_bs_set(hand->stateStack, yajl_state_map_need_val);
+ goto around_again;
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "object key and value must "
+ "be separated by a colon (':')";
+ goto around_again;
+ }
+ }
+ case yajl_state_map_got_val: {
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ switch (tok) {
+ case yajl_tok_right_bracket:
+ if (hand->callbacks && hand->callbacks->yajl_end_map) {
+ _CC_CHK(hand->callbacks->yajl_end_map(hand->ctx));
+ }
+ yajl_bs_pop(hand->stateStack);
+ goto around_again;
+ case yajl_tok_comma:
+ yajl_bs_set(hand->stateStack, yajl_state_map_need_key);
+ goto around_again;
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError = "after key and value, inside map, "
+ "I expect ',' or '}'";
+ /* try to restore error offset */
+ if (*offset >= bufLen) *offset -= bufLen;
+ else *offset = 0;
+ goto around_again;
+ }
+ }
+ case yajl_state_array_got_val: {
+ tok = yajl_lex_lex(hand->lexer, jsonText, jsonTextLen,
+ offset, &buf, &bufLen);
+ switch (tok) {
+ case yajl_tok_right_brace:
+ if (hand->callbacks && hand->callbacks->yajl_end_array) {
+ _CC_CHK(hand->callbacks->yajl_end_array(hand->ctx));
+ }
+ yajl_bs_pop(hand->stateStack);
+ goto around_again;
+ case yajl_tok_comma:
+ yajl_bs_set(hand->stateStack, yajl_state_array_need_val);
+ goto around_again;
+ case yajl_tok_eof:
+ return yajl_status_ok;
+ case yajl_tok_error:
+ yajl_bs_set(hand->stateStack, yajl_state_lexical_error);
+ goto around_again;
+ default:
+ yajl_bs_set(hand->stateStack, yajl_state_parse_error);
+ hand->parseError =
+ "after array element, I expect ',' or ']'";
+ goto around_again;
+ }
+ }
+ }
+
+ abort();
+ return yajl_status_error;
+}
+
diff --git a/src/yajl/yajl_parser.h b/src/yajl/yajl_parser.h
new file mode 100644
index 0000000..c79299a
--- /dev/null
+++ b/src/yajl/yajl_parser.h
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2007-2014, Lloyd Hilaiel <me@lloyd.io>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#ifndef __YAJL_PARSER_H__
+#define __YAJL_PARSER_H__
+
+#include "api/yajl_parse.h"
+#include "yajl_bytestack.h"
+#include "yajl_buf.h"
+#include "yajl_lex.h"
+
+
+typedef enum {
+ yajl_state_start = 0,
+ yajl_state_parse_complete,
+ yajl_state_parse_error,
+ yajl_state_lexical_error,
+ yajl_state_map_start,
+ yajl_state_map_sep,
+ yajl_state_map_need_val,
+ yajl_state_map_got_val,
+ yajl_state_map_need_key,
+ yajl_state_array_start,
+ yajl_state_array_got_val,
+ yajl_state_array_need_val,
+ yajl_state_got_value,
+} yajl_state;
+
+struct yajl_handle_t {
+ const yajl_callbacks * callbacks;
+ void * ctx;
+ yajl_lexer lexer;
+ const char * parseError;
+ /* the number of bytes consumed from the last client buffer,
+ * in the case of an error this will be an error offset, in the
+ * case of an error this can be used as the error offset */
+ size_t bytesConsumed;
+ /* temporary storage for decoded strings */
+ yajl_buf decodeBuf;
+ /* a stack of states. access with yajl_state_XXX routines */
+ yajl_bytestack stateStack;
+ /* memory allocation routines */
+ yajl_alloc_funcs alloc;
+ /* bitfield */
+ unsigned int flags;
+};
+
+yajl_status
+yajl_do_parse(yajl_handle handle, const unsigned char * jsonText,
+ size_t jsonTextLen);
+
+yajl_status
+yajl_do_finish(yajl_handle handle);
+
+unsigned char *
+yajl_render_error_string(yajl_handle hand, const unsigned char * jsonText,
+ size_t jsonTextLen, int verbose);
+
+/* A little built in integer parsing routine with the same semantics as strtol
+ * that's unaffected by LOCALE. */
+long long
+yajl_parse_integer(const unsigned char *number, unsigned int length);
+
+
+#endif
diff --git a/src/yajl/yajl_tree.c b/src/yajl/yajl_tree.c
new file mode 100644
index 0000000..3d357a3
--- /dev/null
+++ b/src/yajl/yajl_tree.c
@@ -0,0 +1,503 @@
+/*
+ * Copyright (c) 2010-2011 Florian Forster <ff at octo.it>
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <assert.h>
+
+#include "api/yajl_tree.h"
+#include "api/yajl_parse.h"
+
+#include "yajl_parser.h"
+
+#if defined(_WIN32) || defined(WIN32)
+#define snprintf sprintf_s
+#endif
+
+#define STATUS_CONTINUE 1
+#define STATUS_ABORT 0
+
+struct stack_elem_s;
+typedef struct stack_elem_s stack_elem_t;
+struct stack_elem_s
+{
+ char * key;
+ yajl_val value;
+ stack_elem_t *next;
+};
+
+struct context_s
+{
+ stack_elem_t *stack;
+ yajl_val root;
+ char *errbuf;
+ size_t errbuf_size;
+};
+typedef struct context_s context_t;
+
+#define RETURN_ERROR(ctx,retval,...) { \
+ if ((ctx)->errbuf != NULL) \
+ snprintf ((ctx)->errbuf, (ctx)->errbuf_size, __VA_ARGS__); \
+ return (retval); \
+ }
+
+static yajl_val value_alloc (yajl_type type)
+{
+ yajl_val v;
+
+ v = malloc (sizeof (*v));
+ if (v == NULL) return (NULL);
+ memset (v, 0, sizeof (*v));
+ v->type = type;
+
+ return (v);
+}
+
+static void yajl_object_free (yajl_val v)
+{
+ size_t i;
+
+ if (!YAJL_IS_OBJECT(v)) return;
+
+ for (i = 0; i < v->u.object.len; i++)
+ {
+ free((char *) v->u.object.keys[i]);
+ v->u.object.keys[i] = NULL;
+ yajl_tree_free (v->u.object.values[i]);
+ v->u.object.values[i] = NULL;
+ }
+
+ free((void*) v->u.object.keys);
+ free(v->u.object.values);
+ free(v);
+}
+
+static void yajl_array_free (yajl_val v)
+{
+ size_t i;
+
+ if (!YAJL_IS_ARRAY(v)) return;
+
+ for (i = 0; i < v->u.array.len; i++)
+ {
+ yajl_tree_free (v->u.array.values[i]);
+ v->u.array.values[i] = NULL;
+ }
+
+ free(v->u.array.values);
+ free(v);
+}
+
+/*
+ * Parsing nested objects and arrays is implemented using a stack. When a new
+ * object or array starts (a curly or a square opening bracket is read), an
+ * appropriate value is pushed on the stack. When the end of the object is
+ * reached (an appropriate closing bracket has been read), the value is popped
+ * off the stack and added to the enclosing object using "context_add_value".
+ */
+static int context_push(context_t *ctx, yajl_val v)
+{
+ stack_elem_t *stack;
+
+ stack = malloc (sizeof (*stack));
+ if (stack == NULL)
+ RETURN_ERROR (ctx, ENOMEM, "Out of memory");
+ memset (stack, 0, sizeof (*stack));
+
+ assert ((ctx->stack == NULL)
+ || YAJL_IS_OBJECT (v)
+ || YAJL_IS_ARRAY (v));
+
+ stack->value = v;
+ stack->next = ctx->stack;
+ ctx->stack = stack;
+
+ return (0);
+}
+
+static yajl_val context_pop(context_t *ctx)
+{
+ stack_elem_t *stack;
+ yajl_val v;
+
+ if (ctx->stack == NULL)
+ RETURN_ERROR (ctx, NULL, "context_pop: "
+ "Bottom of stack reached prematurely");
+
+ stack = ctx->stack;
+ ctx->stack = stack->next;
+
+ v = stack->value;
+
+ free (stack);
+
+ return (v);
+}
+
+static int object_add_keyval(context_t *ctx,
+ yajl_val obj, char *key, yajl_val value)
+{
+ const char **tmpk;
+ yajl_val *tmpv;
+
+ /* We're checking for NULL in "context_add_value" or its callers. */
+ assert (ctx != NULL);
+ assert (obj != NULL);
+ assert (key != NULL);
+ assert (value != NULL);
+
+ /* We're assuring that "obj" is an object in "context_add_value". */
+ assert(YAJL_IS_OBJECT(obj));
+
+ tmpk = realloc((void *) obj->u.object.keys, sizeof(*(obj->u.object.keys)) * (obj->u.object.len + 1));
+ if (tmpk == NULL)
+ RETURN_ERROR(ctx, ENOMEM, "Out of memory");
+ obj->u.object.keys = tmpk;
+
+ tmpv = realloc(obj->u.object.values, sizeof (*obj->u.object.values) * (obj->u.object.len + 1));
+ if (tmpv == NULL)
+ RETURN_ERROR(ctx, ENOMEM, "Out of memory");
+ obj->u.object.values = tmpv;
+
+ obj->u.object.keys[obj->u.object.len] = key;
+ obj->u.object.values[obj->u.object.len] = value;
+ obj->u.object.len++;
+
+ return (0);
+}
+
+static int array_add_value (context_t *ctx,
+ yajl_val array, yajl_val value)
+{
+ yajl_val *tmp;
+
+ /* We're checking for NULL pointers in "context_add_value" or its
+ * callers. */
+ assert (ctx != NULL);
+ assert (array != NULL);
+ assert (value != NULL);
+
+ /* "context_add_value" will only call us with array values. */
+ assert(YAJL_IS_ARRAY(array));
+
+ tmp = realloc(array->u.array.values,
+ sizeof(*(array->u.array.values)) * (array->u.array.len + 1));
+ if (tmp == NULL)
+ RETURN_ERROR(ctx, ENOMEM, "Out of memory");
+ array->u.array.values = tmp;
+ array->u.array.values[array->u.array.len] = value;
+ array->u.array.len++;
+
+ return 0;
+}
+
+/*
+ * Add a value to the value on top of the stack or the "root" member in the
+ * context if the end of the parsing process is reached.
+ */
+static int context_add_value (context_t *ctx, yajl_val v)
+{
+ /* We're checking for NULL values in all the calling functions. */
+ assert (ctx != NULL);
+ assert (v != NULL);
+
+ /*
+ * There are three valid states in which this function may be called:
+ * - There is no value on the stack => This is the only value. This is the
+ * last step done when parsing a document. We assign the value to the
+ * "root" member and return.
+ * - The value on the stack is an object. In this case store the key on the
+ * stack or, if the key has already been read, add key and value to the
+ * object.
+ * - The value on the stack is an array. In this case simply add the value
+ * and return.
+ */
+ if (ctx->stack == NULL)
+ {
+ assert (ctx->root == NULL);
+ ctx->root = v;
+ return (0);
+ }
+ else if (YAJL_IS_OBJECT (ctx->stack->value))
+ {
+ if (ctx->stack->key == NULL)
+ {
+ if (!YAJL_IS_STRING (v))
+ RETURN_ERROR (ctx, EINVAL, "context_add_value: "
+ "Object key is not a string (%#04x)",
+ v->type);
+
+ ctx->stack->key = v->u.string;
+ v->u.string = NULL;
+ free(v);
+ return (0);
+ }
+ else /* if (ctx->key != NULL) */
+ {
+ char * key;
+
+ key = ctx->stack->key;
+ ctx->stack->key = NULL;
+ return (object_add_keyval (ctx, ctx->stack->value, key, v));
+ }
+ }
+ else if (YAJL_IS_ARRAY (ctx->stack->value))
+ {
+ return (array_add_value (ctx, ctx->stack->value, v));
+ }
+ else
+ {
+ RETURN_ERROR (ctx, EINVAL, "context_add_value: Cannot add value to "
+ "a value of type %#04x (not a composite type)",
+ ctx->stack->value->type);
+ }
+}
+
+static int handle_string (void *ctx,
+ const unsigned char *string, size_t string_length)
+{
+ yajl_val v;
+
+ v = value_alloc (yajl_t_string);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ v->u.string = malloc (string_length + 1);
+ if (v->u.string == NULL)
+ {
+ free (v);
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+ }
+ memcpy(v->u.string, string, string_length);
+ v->u.string[string_length] = 0;
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_number (void *ctx, const char *string, size_t string_length)
+{
+ yajl_val v;
+ char *endptr;
+
+ v = value_alloc(yajl_t_number);
+ if (v == NULL)
+ RETURN_ERROR((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ v->u.number.r = malloc(string_length + 1);
+ if (v->u.number.r == NULL)
+ {
+ free(v);
+ RETURN_ERROR((context_t *) ctx, STATUS_ABORT, "Out of memory");
+ }
+ memcpy(v->u.number.r, string, string_length);
+ v->u.number.r[string_length] = 0;
+
+ v->u.number.flags = 0;
+
+ errno = 0;
+ v->u.number.i = yajl_parse_integer((const unsigned char *) v->u.number.r,
+ strlen(v->u.number.r));
+ if (errno == 0)
+ v->u.number.flags |= YAJL_NUMBER_INT_VALID;
+
+ endptr = NULL;
+ errno = 0;
+ v->u.number.d = strtod(v->u.number.r, &endptr);
+ if ((errno == 0) && (endptr != NULL) && (*endptr == 0))
+ v->u.number.flags |= YAJL_NUMBER_DOUBLE_VALID;
+
+ return ((context_add_value(ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_start_map (void *ctx)
+{
+ yajl_val v;
+
+ v = value_alloc(yajl_t_object);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ v->u.object.keys = NULL;
+ v->u.object.values = NULL;
+ v->u.object.len = 0;
+
+ return ((context_push (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_end_map (void *ctx)
+{
+ yajl_val v;
+
+ v = context_pop (ctx);
+ if (v == NULL)
+ return (STATUS_ABORT);
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_start_array (void *ctx)
+{
+ yajl_val v;
+
+ v = value_alloc(yajl_t_array);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ v->u.array.values = NULL;
+ v->u.array.len = 0;
+
+ return ((context_push (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_end_array (void *ctx)
+{
+ yajl_val v;
+
+ v = context_pop (ctx);
+ if (v == NULL)
+ return (STATUS_ABORT);
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_boolean (void *ctx, int boolean_value)
+{
+ yajl_val v;
+
+ v = value_alloc (boolean_value ? yajl_t_true : yajl_t_false);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+static int handle_null (void *ctx)
+{
+ yajl_val v;
+
+ v = value_alloc (yajl_t_null);
+ if (v == NULL)
+ RETURN_ERROR ((context_t *) ctx, STATUS_ABORT, "Out of memory");
+
+ return ((context_add_value (ctx, v) == 0) ? STATUS_CONTINUE : STATUS_ABORT);
+}
+
+/*
+ * Public functions
+ */
+yajl_val yajl_tree_parse (const char *input,
+ char *error_buffer, size_t error_buffer_size)
+{
+ static const yajl_callbacks callbacks =
+ {
+ /* null = */ handle_null,
+ /* boolean = */ handle_boolean,
+ /* integer = */ NULL,
+ /* double = */ NULL,
+ /* number = */ handle_number,
+ /* string = */ handle_string,
+ /* start map = */ handle_start_map,
+ /* map key = */ handle_string,
+ /* end map = */ handle_end_map,
+ /* start array = */ handle_start_array,
+ /* end array = */ handle_end_array
+ };
+
+ yajl_handle handle;
+ yajl_status status;
+ char * internal_err_str;
+ context_t ctx = { NULL, NULL, NULL, 0 };
+
+ ctx.errbuf = error_buffer;
+ ctx.errbuf_size = error_buffer_size;
+
+ if (error_buffer != NULL)
+ memset (error_buffer, 0, error_buffer_size);
+
+ handle = yajl_alloc (&callbacks, NULL, &ctx);
+ yajl_config(handle, yajl_allow_comments, 1);
+
+ status = yajl_parse(handle,
+ (unsigned char *) input,
+ strlen (input));
+ status = yajl_complete_parse (handle);
+ if (status != yajl_status_ok) {
+ if (error_buffer != NULL && error_buffer_size > 0) {
+ internal_err_str = (char *) yajl_get_error(handle, 1,
+ (const unsigned char *) input,
+ strlen(input));
+ snprintf(error_buffer, error_buffer_size, "%s", internal_err_str);
+ YA_FREE(&(handle->alloc), internal_err_str);
+ }
+ yajl_free (handle);
+ return NULL;
+ }
+
+ yajl_free (handle);
+ return (ctx.root);
+}
+
+yajl_val yajl_tree_get(yajl_val n, const char ** path, yajl_type type)
+{
+ if (!path) return NULL;
+ while (n && *path) {
+ size_t i;
+ size_t len;
+
+ if (n->type != yajl_t_object) return NULL;
+ len = n->u.object.len;
+ for (i = 0; i < len; i++) {
+ if (!strcmp(*path, n->u.object.keys[i])) {
+ n = n->u.object.values[i];
+ break;
+ }
+ }
+ if (i == len) return NULL;
+ path++;
+ }
+ if (n && type != yajl_t_any && type != n->type) n = NULL;
+ return n;
+}
+
+void yajl_tree_free (yajl_val v)
+{
+ if (v == NULL) return;
+
+ if (YAJL_IS_STRING(v))
+ {
+ free(v->u.string);
+ free(v);
+ }
+ else if (YAJL_IS_NUMBER(v))
+ {
+ free(v->u.number.r);
+ free(v);
+ }
+ else if (YAJL_GET_OBJECT(v))
+ {
+ yajl_object_free(v);
+ }
+ else if (YAJL_GET_ARRAY(v))
+ {
+ yajl_array_free(v);
+ }
+ else /* if (yajl_t_true or yajl_t_false or yajl_t_null) */
+ {
+ free(v);
+ }
+}
diff --git a/src/yajl/yajl_version.c b/src/yajl/yajl_version.c
new file mode 100644
index 0000000..0671da7
--- /dev/null
+++ b/src/yajl/yajl_version.c
@@ -0,0 +1,7 @@
+#include <yajl/yajl_version.h>
+
+int yajl_version(void)
+{
+ return YAJL_VERSION;
+}
+
diff --git a/src/yajl/yajl_version.h b/src/yajl/yajl_version.h
new file mode 100644
index 0000000..0fba9b8
--- /dev/null
+++ b/src/yajl/yajl_version.h
@@ -0,0 +1,23 @@
+#ifndef YAJL_VERSION_H_
+#define YAJL_VERSION_H_
+
+#include <yajl/yajl_common.h>
+
+#define YAJL_MAJOR 2
+#define YAJL_MINOR 0
+#define YAJL_MICRO 1
+
+#define YAJL_VERSION ((YAJL_MAJOR * 10000) + (YAJL_MINOR * 100) + YAJL_MICRO)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern int YAJL_API yajl_version(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* YAJL_VERSION_H_ */
+
diff --git a/src/yajlpp/CMakeLists.txt b/src/yajlpp/CMakeLists.txt
new file mode 100644
index 0000000..73168ba
--- /dev/null
+++ b/src/yajlpp/CMakeLists.txt
@@ -0,0 +1,27 @@
+add_library(
+ yajlpp STATIC
+ ../config.h.in
+ json_op.hh
+ json_ptr.hh
+ yajlpp.hh
+ yajlpp_def.hh
+
+ json_op.cc
+ json_ptr.cc
+ yajlpp.cc
+)
+
+target_include_directories(yajlpp PUBLIC . .. ../fmtlib
+ ${CMAKE_CURRENT_BINARY_DIR}/..)
+target_link_libraries(yajlpp pcrepp yajl ncurses::libcurses)
+
+add_executable(test_yajlpp test_yajlpp.cc)
+target_link_libraries(test_yajlpp yajlpp base ${lnav_LIBS})
+add_test(NAME test_yajlpp COMMAND test_yajlpp)
+
+add_executable(test_json_ptr test_json_ptr.cc)
+target_link_libraries(test_json_ptr yajlpp base ${lnav_LIBS})
+add_test(NAME test_json_ptr COMMAND test_json_ptr)
+
+add_executable(drive_json_op drive_json_op.cc)
+target_link_libraries(drive_json_op base yajlpp ${lnav_LIBS})
diff --git a/src/yajlpp/Makefile.am b/src/yajlpp/Makefile.am
new file mode 100644
index 0000000..cc6bcf6
--- /dev/null
+++ b/src/yajlpp/Makefile.am
@@ -0,0 +1,81 @@
+
+include $(top_srcdir)/aminclude_static.am
+
+TESTS_ENVIRONMENT = $(SHELL) $(top_builddir)/TESTS_ENVIRONMENT
+LOG_COMPILER = $(SHELL) $(top_builddir)/TESTS_ENVIRONMENT
+
+AM_CPPFLAGS = \
+ $(CODE_COVERAGE_CPPFLAGS) \
+ -Wall \
+ $(LIBARCHIVE_CFLAGS) \
+ $(PCRE_CFLAGS) \
+ -I$(top_srcdir)/src/ \
+ -I$(top_srcdir)/src/fmtlib \
+ -I$(top_srcdir)/src/third-party/scnlib/include
+
+AM_LDFLAGS = \
+ $(LIBARCHIVE_LDFLAGS) \
+ $(STATIC_LDFLAGS)
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS)
+
+noinst_LIBRARIES = libyajlpp.a
+
+noinst_HEADERS = \
+ json_op.hh \
+ json_ptr.hh \
+ yajlpp.hh \
+ yajlpp_def.hh
+
+libyajlpp_a_SOURCES = \
+ json_op.cc \
+ json_ptr.cc \
+ yajlpp.cc
+
+check_PROGRAMS = \
+ drive_json_op \
+ drive_json_ptr_walk \
+ test_json_ptr \
+ test_yajlpp
+
+drive_json_op_SOURCES = drive_json_op.cc
+
+drive_json_ptr_walk_SOURCES = drive_json_ptr_walk.cc
+
+test_json_ptr_SOURCES = test_json_ptr.cc
+
+test_yajlpp_SOURCES = test_yajlpp.cc
+
+LDADD = \
+ $(LIBARCHIVE_LIBS) \
+ libyajlpp.a \
+ $(top_builddir)/src/base/libbase.a \
+ $(top_builddir)/src/fmtlib/libcppfmt.a \
+ $(top_builddir)/src/third-party/scnlib/src/libscnlib.a \
+ $(top_builddir)/src/pcrepp/libpcrepp.a \
+ $(top_builddir)/src/yajl/libyajl.a
+
+dist_noinst_SCRIPTS = \
+ test_json_op.sh \
+ test_json_ptr_walk.sh
+
+TESTS = \
+ test_json_op.sh \
+ test_json_ptr \
+ test_json_ptr_walk.sh \
+ test_yajlpp
+
+DISTCLEANFILES = \
+ *.dat \
+ *.err \
+ *.db \
+ *.dpt \
+ *.diff \
+ *.index \
+ *.tmp \
+ *.errbak \
+ *.tmpbak \
+ *.gz \
+ *.bz2
diff --git a/src/yajlpp/README.md b/src/yajlpp/README.md
new file mode 100644
index 0000000..3a7650c
--- /dev/null
+++ b/src/yajlpp/README.md
@@ -0,0 +1,5 @@
+# YAJLPP
+
+This directory contains a C++ adapter for the YAJL JSON library. The adapter
+provides a usage model that is based around client code mapping internal data
+structures to a JSON schema.
diff --git a/src/yajlpp/drive_json_op.cc b/src/yajlpp/drive_json_op.cc
new file mode 100644
index 0000000..51ef161
--- /dev/null
+++ b/src/yajlpp/drive_json_op.cc
@@ -0,0 +1,210 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY TIMOTHY STACK AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file drive_json_op.cc
+ */
+
+#include <errno.h>
+#include <stdlib.h>
+
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "yajl/api/yajl_gen.h"
+#include "yajlpp/json_op.hh"
+
+static void
+printer(void* ctx, const char* numberVal, size_t numberLen)
+{
+ log_perror(write(STDOUT_FILENO, numberVal, numberLen));
+}
+
+static int
+handle_start_map(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ yajl_gen_map_open(gen);
+
+ return 1;
+}
+
+static int
+handle_map_key(void* ctx, const unsigned char* key, size_t len)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ yajl_gen_string(gen, key, len);
+
+ return 1;
+}
+
+static int
+handle_end_map(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ yajl_gen_map_close(gen);
+
+ return 1;
+}
+
+static int
+handle_null(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ yajl_gen_null(gen);
+
+ return 1;
+}
+
+static int
+handle_boolean(void* ctx, int boolVal)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ yajl_gen_bool(gen, boolVal);
+
+ return 1;
+}
+
+static int
+handle_number(void* ctx, const char* numberVal, size_t numberLen)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ yajl_gen_number(gen, numberVal, numberLen);
+
+ return 1;
+}
+
+static int
+handle_string(void* ctx, const unsigned char* stringVal, size_t len)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ yajl_gen_string(gen, stringVal, len);
+
+ return 1;
+}
+
+static int
+handle_start_array(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ yajl_gen_array_open(gen);
+
+ return 1;
+}
+
+static int
+handle_end_array(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ yajl_gen_array_close(gen);
+
+ return 1;
+}
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+
+ log_argv(argc, argv);
+
+ if (argc != 3) {
+ fprintf(stderr, "error: expecting operation and json-pointer\n");
+ retval = EXIT_FAILURE;
+ } else if (strcmp(argv[1], "get") == 0) {
+ unsigned char buffer[1024];
+ json_ptr jptr(argv[2]);
+ json_op jo(jptr);
+ yajl_handle handle;
+ yajl_status status;
+ yajl_gen gen;
+ ssize_t rc;
+
+ gen = yajl_gen_alloc(nullptr);
+ yajl_gen_config(gen, yajl_gen_print_callback, printer, nullptr);
+ yajl_gen_config(gen, yajl_gen_beautify, true);
+
+ jo.jo_ptr_callbacks.yajl_start_map = handle_start_map;
+ jo.jo_ptr_callbacks.yajl_map_key = handle_map_key;
+ jo.jo_ptr_callbacks.yajl_end_map = handle_end_map;
+ jo.jo_ptr_callbacks.yajl_start_array = handle_start_array;
+ jo.jo_ptr_callbacks.yajl_end_array = handle_end_array;
+ jo.jo_ptr_callbacks.yajl_null = handle_null;
+ jo.jo_ptr_callbacks.yajl_boolean = handle_boolean;
+ jo.jo_ptr_callbacks.yajl_number = handle_number;
+ jo.jo_ptr_callbacks.yajl_string = handle_string;
+ jo.jo_ptr_data = gen;
+
+ handle = yajl_alloc(&json_op::ptr_callbacks, nullptr, &jo);
+ while ((rc = read(STDIN_FILENO, buffer, sizeof(buffer))) > 0) {
+ status = yajl_parse(handle, buffer, rc);
+ if (status == yajl_status_error) {
+ auto msg = yajl_get_error(handle, 1, buffer, rc);
+
+ fprintf(stderr, "error:cannot parse JSON input -- %s\n", msg);
+ retval = EXIT_FAILURE;
+ yajl_free_error(handle, msg);
+ break;
+ } else if (status == yajl_status_client_canceled) {
+ fprintf(stderr, "client cancel\n");
+ break;
+ }
+ }
+ status = yajl_complete_parse(handle);
+ if (status == yajl_status_error) {
+ auto msg = yajl_get_error(handle, 1, buffer, rc);
+ fprintf(stderr, "error:cannot parse JSON input -- %s\n", msg);
+ yajl_free_error(handle, msg);
+ retval = EXIT_FAILURE;
+ } else if (status == yajl_status_client_canceled) {
+ fprintf(stderr, "client cancel\n");
+ }
+ yajl_free(handle);
+ } else {
+ fprintf(stderr, "error: unknown operation -- %s\n", argv[1]);
+ retval = EXIT_FAILURE;
+ }
+
+ return retval;
+}
diff --git a/src/yajlpp/drive_json_ptr_walk.cc b/src/yajlpp/drive_json_ptr_walk.cc
new file mode 100644
index 0000000..c6a8f72
--- /dev/null
+++ b/src/yajlpp/drive_json_ptr_walk.cc
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY TIMOTHY STACK AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file drive_json_ptr_dump.cc
+ */
+
+#include <iostream>
+
+#include <stdlib.h>
+
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "json_op.hh"
+#include "json_ptr.hh"
+#include "yajl/api/yajl_gen.h"
+#include "yajlpp.hh"
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ yajl_status status;
+ json_ptr_walk jpw;
+
+ log_argv(argc, argv);
+
+ std::string json_input(std::istreambuf_iterator<char>(std::cin), {});
+
+ status = jpw.parse(json_input.c_str(), json_input.size());
+ if (status == yajl_status_error) {
+ fprintf(stderr,
+ "error:cannot parse JSON input -- %s\n",
+ jpw.jpw_error_msg.c_str());
+ return EXIT_FAILURE;
+ }
+
+ if (status == yajl_status_client_canceled) {
+ fprintf(stderr, "client cancel\n");
+ }
+
+ status = jpw.complete_parse();
+ if (status == yajl_status_error) {
+ fprintf(stderr,
+ "error:cannot parse JSON input -- %s\n",
+ jpw.jpw_error_msg.c_str());
+ return EXIT_FAILURE;
+ } else if (status == yajl_status_client_canceled) {
+ fprintf(stderr, "client cancel\n");
+ }
+
+ for (json_ptr_walk::walk_list_t::iterator iter = jpw.jpw_values.begin();
+ iter != jpw.jpw_values.end();
+ ++iter)
+ {
+ printf("%s = %s\n", iter->wt_ptr.c_str(), iter->wt_value.c_str());
+
+ {
+ auto_mem<yajl_handle_t> parse_handle(yajl_free);
+ json_ptr jp(iter->wt_ptr.c_str());
+ json_op jo(jp);
+ yajlpp_gen gen;
+
+ jo.jo_ptr_callbacks = json_op::gen_callbacks;
+ jo.jo_ptr_data = gen.get_handle();
+ parse_handle.reset(
+ yajl_alloc(&json_op::ptr_callbacks, nullptr, &jo));
+
+ yajl_parse(parse_handle.in(),
+ (const unsigned char*) json_input.c_str(),
+ json_input.size());
+ yajl_complete_parse(parse_handle.in());
+
+ assert(iter->wt_value == gen.to_string_fragment().to_string());
+ }
+ }
+
+ return retval;
+}
diff --git a/src/yajlpp/json_op.cc b/src/yajlpp/json_op.cc
new file mode 100644
index 0000000..046f223
--- /dev/null
+++ b/src/yajlpp/json_op.cc
@@ -0,0 +1,316 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY TIMOTHY STACK AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file json_op.cc
+ */
+
+#include "json_op.hh"
+
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "yajl/api/yajl_gen.h"
+
+static int
+gen_handle_start_map(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ jo->jo_ptr_error_code = yajl_gen_map_open(gen);
+
+ return jo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_map_key(void* ctx, const unsigned char* key, size_t len)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ jo->jo_ptr_error_code = yajl_gen_string(gen, key, len);
+
+ return jo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_end_map(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ jo->jo_ptr_error_code = yajl_gen_map_close(gen);
+
+ return jo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_null(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ jo->jo_ptr_error_code = yajl_gen_null(gen);
+
+ return jo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_boolean(void* ctx, int boolVal)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ jo->jo_ptr_error_code = yajl_gen_bool(gen, boolVal);
+
+ return jo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_number(void* ctx, const char* numberVal, size_t numberLen)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ jo->jo_ptr_error_code = yajl_gen_number(gen, numberVal, numberLen);
+
+ return jo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_string(void* ctx, const unsigned char* stringVal, size_t len)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ jo->jo_ptr_error_code = yajl_gen_string(gen, stringVal, len);
+
+ return jo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_start_array(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ jo->jo_ptr_error_code = yajl_gen_array_open(gen);
+
+ return jo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+static int
+gen_handle_end_array(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ yajl_gen gen = (yajl_gen) jo->jo_ptr_data;
+
+ jo->jo_ptr_error_code = yajl_gen_array_close(gen);
+
+ return jo->jo_ptr_error_code == yajl_gen_status_ok;
+}
+
+const yajl_callbacks json_op::gen_callbacks = {
+ gen_handle_null,
+ gen_handle_boolean,
+ nullptr,
+ nullptr,
+ gen_handle_number,
+ gen_handle_string,
+ gen_handle_start_map,
+ gen_handle_map_key,
+ gen_handle_end_map,
+ gen_handle_start_array,
+ gen_handle_end_array,
+};
+
+const yajl_callbacks json_op::ptr_callbacks = {
+ handle_null,
+ handle_boolean,
+ nullptr,
+ nullptr,
+ handle_number,
+ handle_string,
+ handle_start_map,
+ handle_map_key,
+ handle_end_map,
+ handle_start_array,
+ handle_end_array,
+};
+
+int
+json_op::handle_null(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ int retval = 1;
+
+ if (jo->check_index()) {
+ if (jo->jo_ptr_callbacks.yajl_null != nullptr) {
+ retval = jo->jo_ptr_callbacks.yajl_null(ctx);
+ }
+ }
+
+ return retval;
+}
+
+int
+json_op::handle_boolean(void* ctx, int boolVal)
+{
+ json_op* jo = (json_op*) ctx;
+ int retval = 1;
+
+ if (jo->check_index()) {
+ if (jo->jo_ptr_callbacks.yajl_boolean != nullptr) {
+ retval = jo->jo_ptr_callbacks.yajl_boolean(ctx, boolVal);
+ }
+ }
+
+ return retval;
+}
+
+int
+json_op::handle_number(void* ctx, const char* numberVal, size_t numberLen)
+{
+ json_op* jo = (json_op*) ctx;
+ int retval = 1;
+
+ if (jo->check_index()) {
+ if (jo->jo_ptr_callbacks.yajl_number != nullptr) {
+ retval
+ = jo->jo_ptr_callbacks.yajl_number(ctx, numberVal, numberLen);
+ }
+ }
+
+ return retval;
+}
+
+int
+json_op::handle_string(void* ctx,
+ const unsigned char* stringVal,
+ size_t stringLen)
+{
+ json_op* jo = (json_op*) ctx;
+ int retval = 1;
+
+ if (jo->check_index()) {
+ if (jo->jo_ptr_callbacks.yajl_string != nullptr) {
+ retval
+ = jo->jo_ptr_callbacks.yajl_string(ctx, stringVal, stringLen);
+ }
+ }
+
+ return retval;
+}
+
+int
+json_op::handle_start_map(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ int retval = 1;
+
+ if (jo->check_index(false)) {
+ if (jo->jo_ptr_callbacks.yajl_start_map != nullptr) {
+ retval = jo->jo_ptr_callbacks.yajl_start_map(ctx);
+ }
+ }
+
+ if (!jo->jo_ptr.expect_map(jo->jo_depth, jo->jo_array_index)) {
+ retval = 0;
+ }
+
+ return retval;
+}
+
+int
+json_op::handle_map_key(void* ctx, const unsigned char* key, size_t len)
+{
+ json_op* jo = (json_op*) ctx;
+ int retval = 1;
+
+ if (jo->check_index(false)) {
+ if (jo->jo_ptr_callbacks.yajl_map_key != nullptr) {
+ retval = jo->jo_ptr_callbacks.yajl_map_key(ctx, key, len);
+ }
+ }
+
+ if (!jo->jo_ptr.at_key(jo->jo_depth, (const char*) key, len)) {
+ retval = 0;
+ }
+
+ return retval;
+}
+
+int
+json_op::handle_end_map(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ int retval = 1;
+
+ if (jo->check_index()) {
+ if (jo->jo_ptr_callbacks.yajl_end_map != nullptr) {
+ retval = jo->jo_ptr_callbacks.yajl_end_map(ctx);
+ }
+ }
+
+ jo->jo_ptr.exit_container(jo->jo_depth, jo->jo_array_index);
+
+ return retval;
+}
+
+int
+json_op::handle_start_array(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ int retval = 1;
+
+ if (jo->check_index(false)) {
+ if (jo->jo_ptr_callbacks.yajl_start_array != nullptr) {
+ retval = jo->jo_ptr_callbacks.yajl_start_array(ctx);
+ }
+ }
+
+ jo->jo_ptr.expect_array(jo->jo_depth, jo->jo_array_index);
+
+ return retval;
+}
+
+int
+json_op::handle_end_array(void* ctx)
+{
+ json_op* jo = (json_op*) ctx;
+ int retval = 1;
+
+ if (jo->check_index()) {
+ if (jo->jo_ptr_callbacks.yajl_end_array != nullptr) {
+ retval = jo->jo_ptr_callbacks.yajl_end_array(ctx);
+ }
+ }
+
+ jo->jo_ptr.exit_container(jo->jo_depth, jo->jo_array_index);
+
+ return retval;
+}
diff --git a/src/yajlpp/json_op.hh b/src/yajlpp/json_op.hh
new file mode 100644
index 0000000..4f3399b
--- /dev/null
+++ b/src/yajlpp/json_op.hh
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY TIMOTHY STACK AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file json_op.hh
+ */
+
+#ifndef json_op_hh
+#define json_op_hh
+
+#include <string>
+
+#include <sys/types.h>
+
+#include "yajl/api/yajl_parse.h"
+#include "yajlpp/json_ptr.hh"
+
+class json_op {
+ static int handle_null(void* ctx);
+ static int handle_boolean(void* ctx, int boolVal);
+ static int handle_number(void* ctx,
+ const char* numberVal,
+ size_t numberLen);
+ static int handle_string(void* ctx,
+ const unsigned char* stringVal,
+ size_t len);
+ static int handle_start_map(void* ctx);
+ static int handle_map_key(void* ctx, const unsigned char* key, size_t len);
+ static int handle_end_map(void* ctx);
+ static int handle_start_array(void* ctx);
+ static int handle_end_array(void* ctx);
+
+public:
+ const static yajl_callbacks gen_callbacks;
+ const static yajl_callbacks ptr_callbacks;
+
+ explicit json_op(const json_ptr& ptr)
+ : jo_ptr(ptr), jo_ptr_callbacks(gen_callbacks)
+ {
+ }
+
+ bool check_index(bool primitive = true)
+ {
+ return this->jo_ptr.at_index(
+ this->jo_depth, this->jo_array_index, primitive);
+ }
+
+ int jo_depth{0};
+ int jo_array_index{-1};
+
+ json_ptr jo_ptr;
+ yajl_callbacks jo_ptr_callbacks;
+ void* jo_ptr_data{nullptr};
+ std::string jo_ptr_error;
+ int jo_ptr_error_code{0};
+};
+
+#endif
diff --git a/src/yajlpp/json_ptr.cc b/src/yajlpp/json_ptr.cc
new file mode 100644
index 0000000..4cc0273
--- /dev/null
+++ b/src/yajlpp/json_ptr.cc
@@ -0,0 +1,521 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY TIMOTHY STACK AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file json_ptr.cc
+ */
+
+#ifdef __CYGWIN__
+# include <alloca.h>
+#endif
+
+#include "config.h"
+#include "fmt/format.h"
+#include "yajl/api/yajl_gen.h"
+#include "yajlpp/json_ptr.hh"
+
+static int
+handle_null(void* ctx)
+{
+ json_ptr_walk* jpw = (json_ptr_walk*) ctx;
+
+ jpw->jpw_values.emplace_back(jpw->current_ptr(), yajl_t_null, "null");
+ jpw->inc_array_index();
+
+ return 1;
+}
+
+static int
+handle_boolean(void* ctx, int boolVal)
+{
+ json_ptr_walk* jpw = (json_ptr_walk*) ctx;
+
+ jpw->jpw_values.emplace_back(jpw->current_ptr(),
+ boolVal ? yajl_t_true : yajl_t_false,
+ boolVal ? "true" : "false");
+ jpw->inc_array_index();
+
+ return 1;
+}
+
+static int
+handle_number(void* ctx, const char* numberVal, size_t numberLen)
+{
+ json_ptr_walk* jpw = (json_ptr_walk*) ctx;
+
+ jpw->jpw_values.emplace_back(
+ jpw->current_ptr(), yajl_t_number, std::string(numberVal, numberLen));
+ jpw->inc_array_index();
+
+ return 1;
+}
+
+static void
+appender(void* ctx, const char* strVal, size_t strLen)
+{
+ std::string& str = *(std::string*) ctx;
+
+ str.append(strVal, strLen);
+}
+
+static int
+handle_string(void* ctx, const unsigned char* stringVal, size_t len)
+{
+ json_ptr_walk* jpw = (json_ptr_walk*) ctx;
+ auto_mem<yajl_gen_t> gen(yajl_gen_free);
+ std::string str;
+
+ gen = yajl_gen_alloc(nullptr);
+ yajl_gen_config(gen.in(), yajl_gen_print_callback, appender, &str);
+ yajl_gen_string(gen.in(), stringVal, len);
+ jpw->jpw_values.emplace_back(jpw->current_ptr(), yajl_t_string, str);
+ jpw->inc_array_index();
+
+ return 1;
+}
+
+static int
+handle_start_map(void* ctx)
+{
+ json_ptr_walk* jpw = (json_ptr_walk*) ctx;
+
+ jpw->jpw_keys.emplace_back("");
+ jpw->jpw_array_indexes.push_back(-1);
+
+ return 1;
+}
+
+static int
+handle_map_key(void* ctx, const unsigned char* key, size_t len)
+{
+ json_ptr_walk* jpw = (json_ptr_walk*) ctx;
+ char partially_encoded_key[len + 32];
+ size_t required_len;
+
+ jpw->jpw_keys.pop_back();
+
+ required_len = json_ptr::encode(partially_encoded_key,
+ sizeof(partially_encoded_key),
+ (const char*) key,
+ len);
+ if (required_len < sizeof(partially_encoded_key)) {
+ jpw->jpw_keys.emplace_back(&partially_encoded_key[0], required_len);
+ } else {
+ auto fully_encoded_key = (char*) alloca(required_len);
+
+ json_ptr::encode(
+ fully_encoded_key, required_len, (const char*) key, len);
+ jpw->jpw_keys.emplace_back(&fully_encoded_key[0], required_len);
+ }
+
+ return 1;
+}
+
+static int
+handle_end_map(void* ctx)
+{
+ json_ptr_walk* jpw = (json_ptr_walk*) ctx;
+
+ jpw->jpw_keys.pop_back();
+ jpw->jpw_array_indexes.pop_back();
+
+ jpw->inc_array_index();
+
+ return 1;
+}
+
+static int
+handle_start_array(void* ctx)
+{
+ json_ptr_walk* jpw = (json_ptr_walk*) ctx;
+
+ jpw->jpw_keys.emplace_back("");
+ jpw->jpw_array_indexes.push_back(0);
+
+ return 1;
+}
+
+static int
+handle_end_array(void* ctx)
+{
+ json_ptr_walk* jpw = (json_ptr_walk*) ctx;
+
+ jpw->jpw_keys.pop_back();
+ jpw->jpw_array_indexes.pop_back();
+ jpw->inc_array_index();
+
+ return 1;
+}
+
+const yajl_callbacks json_ptr_walk::callbacks = {handle_null,
+ handle_boolean,
+ nullptr,
+ nullptr,
+ handle_number,
+ handle_string,
+ handle_start_map,
+ handle_map_key,
+ handle_end_map,
+ handle_start_array,
+ handle_end_array};
+
+size_t
+json_ptr::encode(char* dst, size_t dst_len, const char* src, size_t src_len)
+{
+ size_t retval = 0;
+
+ if (src_len == (size_t) -1) {
+ src_len = strlen(src);
+ }
+
+ for (size_t lpc = 0; lpc < src_len; lpc++) {
+ switch (src[lpc]) {
+ case '/':
+ case '~':
+ case '#':
+ if (retval < dst_len) {
+ dst[retval] = '~';
+ retval += 1;
+ if (src[lpc] == '~') {
+ dst[retval] = '0';
+ } else if (src[lpc] == '#') {
+ dst[retval] = '2';
+ } else {
+ dst[retval] = '1';
+ }
+ } else {
+ retval += 1;
+ }
+ break;
+ default:
+ if (retval < dst_len) {
+ dst[retval] = src[lpc];
+ }
+ break;
+ }
+ retval += 1;
+ }
+ if (retval < dst_len) {
+ dst[retval] = '\0';
+ }
+
+ return retval;
+}
+
+size_t
+json_ptr::decode(char* dst, const char* src, ssize_t src_len)
+{
+ size_t retval = 0;
+
+ if (src_len == -1) {
+ src_len = strlen(src);
+ }
+
+ for (int lpc = 0; lpc < src_len; lpc++) {
+ switch (src[lpc]) {
+ case '~':
+ if ((lpc + 1) < src_len) {
+ switch (src[lpc + 1]) {
+ case '0':
+ dst[retval++] = '~';
+ lpc += 1;
+ break;
+ case '1':
+ dst[retval++] = '/';
+ lpc += 1;
+ break;
+ case '2':
+ dst[retval++] = '#';
+ lpc += 1;
+ break;
+ default:
+ break;
+ }
+ }
+ break;
+ default:
+ dst[retval++] = src[lpc];
+ break;
+ }
+ }
+
+ dst[retval] = '\0';
+
+ return retval;
+}
+
+bool
+json_ptr::expect_map(int32_t& depth, int32_t& index)
+{
+ bool retval;
+
+ if (this->jp_state == match_state_t::DONE) {
+ retval = true;
+ } else if (depth != this->jp_depth) {
+ retval = true;
+ } else if (this->reached_end()) {
+ retval = true;
+ } else if (this->jp_state == match_state_t::VALUE
+ && (this->jp_array_index == -1
+ || ((index - 1) == this->jp_array_index)))
+ {
+ if (this->jp_pos[0] == '/') {
+ this->jp_pos += 1;
+ this->jp_depth += 1;
+ this->jp_state = match_state_t::VALUE;
+ this->jp_array_index = -1;
+ index = -1;
+ }
+ retval = true;
+ } else {
+ retval = true;
+ }
+ depth += 1;
+
+ return retval;
+}
+
+bool
+json_ptr::at_key(int32_t depth, const char* component, ssize_t len)
+{
+ const char* component_end;
+ int lpc;
+
+ if (this->jp_state == match_state_t::DONE || depth != this->jp_depth) {
+ return true;
+ }
+
+ if (len == -1) {
+ len = strlen(component);
+ }
+ component_end = component + len;
+
+ for (lpc = 0; component < component_end; lpc++, component++) {
+ char ch = this->jp_pos[lpc];
+
+ if (this->jp_pos[lpc] == '~') {
+ switch (this->jp_pos[lpc + 1]) {
+ case '0':
+ ch = '~';
+ break;
+ case '1':
+ ch = '/';
+ break;
+ default:
+ this->jp_state = match_state_t::ERR_INVALID_ESCAPE;
+ return false;
+ }
+ lpc += 1;
+ } else if (this->jp_pos[lpc] == '/') {
+ ch = '\0';
+ }
+
+ if (ch != *component) {
+ return true;
+ }
+ }
+
+ this->jp_pos += lpc;
+ this->jp_state = match_state_t::VALUE;
+
+ return true;
+}
+
+void
+json_ptr::exit_container(int32_t& depth, int32_t& index)
+{
+ depth -= 1;
+ if (this->jp_state == match_state_t::VALUE && depth == this->jp_depth
+ && (index == -1 || (index - 1 == this->jp_array_index))
+ && this->reached_end())
+ {
+ this->jp_state = match_state_t::DONE;
+ index = -1;
+ }
+}
+
+bool
+json_ptr::expect_array(int32_t& depth, int32_t& index)
+{
+ bool retval;
+
+ if (this->jp_state == match_state_t::DONE) {
+ retval = true;
+ } else if (depth != this->jp_depth) {
+ retval = true;
+ } else if (this->reached_end()) {
+ retval = true;
+ } else if (this->jp_pos[0] == '/' && index == this->jp_array_index) {
+ int offset;
+
+ this->jp_depth += 1;
+
+ if (sscanf(this->jp_pos, "/%d%n", &this->jp_array_index, &offset) != 1)
+ {
+ this->jp_state = match_state_t::ERR_INVALID_INDEX;
+ retval = true;
+ } else if (this->jp_pos[offset] != '\0' && this->jp_pos[offset] != '/')
+ {
+ this->jp_state = match_state_t::ERR_INVALID_INDEX;
+ retval = true;
+ } else {
+ index = 0;
+ this->jp_pos += offset;
+ this->jp_state = match_state_t::VALUE;
+ retval = true;
+ }
+ } else {
+ this->jp_state = match_state_t::ERR_NO_SLASH;
+ retval = true;
+ }
+
+ depth += 1;
+
+ return retval;
+}
+
+bool
+json_ptr::at_index(int32_t& depth, int32_t& index, bool primitive)
+{
+ bool retval;
+
+ if (this->jp_state == match_state_t::DONE) {
+ retval = false;
+ } else if (depth < this->jp_depth) {
+ retval = false;
+ } else if (depth == this->jp_depth) {
+ if (index == -1) {
+ if (this->jp_array_index == -1) {
+ retval = this->reached_end();
+ if (primitive && retval) {
+ this->jp_state = match_state_t::DONE;
+ }
+ } else {
+ retval = false;
+ }
+ } else if (index == this->jp_array_index) {
+ retval = this->reached_end();
+ this->jp_array_index = -1;
+ index = -1;
+ if (primitive && retval) {
+ this->jp_state = match_state_t::DONE;
+ }
+ } else {
+ index += 1;
+ retval = false;
+ }
+ } else if (index == -1) {
+ retval = this->reached_end();
+ } else {
+ retval = false;
+ }
+
+ return retval;
+}
+
+std::string
+json_ptr::error_msg() const
+{
+ switch (this->jp_state) {
+ case match_state_t::ERR_INVALID_ESCAPE:
+ return fmt::format(FMT_STRING("invalid escape sequence near -- {}"),
+ this->jp_pos);
+ case match_state_t::ERR_INVALID_INDEX:
+ return fmt::format(FMT_STRING("expecting array index at -- {}"),
+ this->jp_pos);
+ case match_state_t::ERR_INVALID_TYPE:
+ return fmt::format(FMT_STRING("expecting container at -- {}"),
+ this->jp_pos);
+ default:
+ break;
+ }
+
+ return "";
+}
+
+std::string
+json_ptr_walk::current_ptr()
+{
+ std::string retval;
+
+ for (size_t lpc = 0; lpc < this->jpw_array_indexes.size(); lpc++) {
+ retval.append("/");
+ if (this->jpw_array_indexes[lpc] == -1) {
+ retval.append(this->jpw_keys[lpc]);
+ } else {
+ fmt::format_to(std::back_inserter(retval),
+ FMT_STRING("{}"),
+ this->jpw_array_indexes[lpc]);
+ }
+ }
+
+ this->jpw_max_ptr_len = std::max(this->jpw_max_ptr_len, retval.size());
+
+ return retval;
+}
+
+void
+json_ptr_walk::update_error_msg(yajl_status status,
+ const char* buffer,
+ ssize_t len)
+{
+ switch (status) {
+ case yajl_status_ok:
+ break;
+ case yajl_status_client_canceled:
+ this->jpw_error_msg = "internal error";
+ break;
+ case yajl_status_error: {
+ auto* msg = yajl_get_error(
+ this->jpw_handle, 1, (const unsigned char*) buffer, len);
+ this->jpw_error_msg = std::string((const char*) msg);
+
+ yajl_free_error(this->jpw_handle, msg);
+ break;
+ }
+ }
+}
+
+yajl_status
+json_ptr_walk::complete_parse()
+{
+ yajl_status retval;
+
+ retval = yajl_complete_parse(this->jpw_handle);
+ this->update_error_msg(retval, nullptr, -1);
+ return retval;
+}
+
+yajl_status
+json_ptr_walk::parse(const char* buffer, ssize_t len)
+{
+ yajl_status retval;
+
+ retval = yajl_parse(this->jpw_handle, (const unsigned char*) buffer, len);
+ this->update_error_msg(retval, buffer, len);
+ return retval;
+}
diff --git a/src/yajlpp/json_ptr.hh b/src/yajlpp/json_ptr.hh
new file mode 100644
index 0000000..f7822ab
--- /dev/null
+++ b/src/yajlpp/json_ptr.hh
@@ -0,0 +1,136 @@
+/**
+ * Copyright (c) 2014-2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY TIMOTHY STACK AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file json_ptr.hh
+ */
+
+#ifndef json_ptr_hh
+#define json_ptr_hh
+
+#include <string>
+#include <vector>
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+
+#include "base/auto_mem.hh"
+#include "yajl/api/yajl_parse.h"
+#include "yajl/api/yajl_tree.h"
+
+class json_ptr_walk {
+public:
+ const static yajl_callbacks callbacks;
+
+ json_ptr_walk()
+ {
+ this->jpw_handle = yajl_alloc(&callbacks, nullptr, this);
+ }
+
+ yajl_status parse(const char* buffer, ssize_t len);
+
+ yajl_status complete_parse();
+
+ void update_error_msg(yajl_status status, const char* buffer, ssize_t len);
+
+ void clear() { this->jpw_values.clear(); }
+
+ void inc_array_index()
+ {
+ if (!this->jpw_array_indexes.empty()
+ && this->jpw_array_indexes.back() != -1) {
+ this->jpw_array_indexes.back() += 1;
+ }
+ }
+
+ std::string current_ptr();
+
+ struct walk_triple {
+ walk_triple(std::string ptr, yajl_type type, std::string value)
+ : wt_ptr(std::move(ptr)), wt_type(type), wt_value(std::move(value))
+ {
+ }
+
+ std::string wt_ptr;
+ yajl_type wt_type;
+ std::string wt_value;
+ };
+
+ using walk_list_t = std::vector<walk_triple>;
+
+ auto_mem<yajl_handle_t> jpw_handle{yajl_free};
+ std::string jpw_error_msg;
+ walk_list_t jpw_values;
+ std::vector<std::string> jpw_keys;
+ std::vector<int32_t> jpw_array_indexes;
+ size_t jpw_max_ptr_len{0};
+};
+
+class json_ptr {
+public:
+ enum class match_state_t {
+ DONE,
+ VALUE,
+ ERR_INVALID_TYPE,
+ ERR_NO_SLASH,
+ ERR_INVALID_ESCAPE,
+ ERR_INVALID_INDEX,
+ };
+
+ static size_t encode(char* dst,
+ size_t dst_len,
+ const char* src,
+ size_t src_len = -1);
+
+ static size_t decode(char* dst, const char* src, ssize_t src_len = -1);
+
+ json_ptr(const char* value) : jp_value(value), jp_pos(value) {}
+
+ bool expect_map(int32_t& depth, int32_t& index);
+
+ bool at_key(int32_t depth, const char* component, ssize_t len = -1);
+
+ void exit_container(int32_t& depth, int32_t& index);
+
+ bool expect_array(int32_t& depth, int32_t& index);
+
+ bool at_index(int32_t& depth, int32_t& index, bool primitive = true);
+
+ bool reached_end() const { return this->jp_pos[0] == '\0'; }
+
+ std::string error_msg() const;
+
+ const char* jp_value;
+ const char* jp_pos;
+ int32_t jp_depth{0};
+ int32_t jp_array_index{-1};
+ match_state_t jp_state{match_state_t::VALUE};
+};
+
+#endif
diff --git a/src/yajlpp/test_json_op.sh b/src/yajlpp/test_json_op.sh
new file mode 100755
index 0000000..91a3849
--- /dev/null
+++ b/src/yajlpp/test_json_op.sh
@@ -0,0 +1,114 @@
+#! /bin/bash
+
+run_test ./drive_json_op get "" <<EOF
+3
+EOF
+
+check_output "cannot read root number value" <<EOF
+3
+EOF
+
+run_test ./drive_json_op get "" <<EOF
+null
+EOF
+
+check_output "cannot read root null value" <<EOF
+null
+EOF
+
+run_test ./drive_json_op get "" <<EOF
+true
+EOF
+
+check_output "cannot read root bool value" <<EOF
+true
+EOF
+
+run_test ./drive_json_op get "" <<EOF
+"str"
+EOF
+
+check_output "cannot read root string value" <<EOF
+"str"
+EOF
+
+run_test ./drive_json_op get "" <<EOF
+{ "val" : 3, "other" : 2 }
+EOF
+
+check_output "cannot read root map value" <<EOF
+{
+ "val": 3,
+ "other": 2
+}
+EOF
+
+run_test ./drive_json_op get /val <<EOF
+{ "val" : 3 }
+EOF
+
+check_output "cannot read top-level value" <<EOF
+3
+EOF
+
+run_test ./drive_json_op get /val <<EOF
+{ "other" : { "val" : 5 }, "val" : 3 }
+EOF
+
+check_output "read wrong value" <<EOF
+3
+EOF
+
+run_test ./drive_json_op get /other <<EOF
+{ "other" : { "val" : 5 }, "val" : 3 }
+EOF
+
+check_output "cannot read map" <<EOF
+{
+ "val": 5
+}
+EOF
+
+run_test ./drive_json_op get /other/val <<EOF
+{ "other" : { "val" : 5 }, "val" : 3 }
+EOF
+
+check_output "cannot read nested map" <<EOF
+5
+EOF
+
+
+run_test ./drive_json_op get "" <<EOF
+[0, 1]
+EOF
+
+check_output "cannot read root array value" <<EOF
+[
+ 0,
+ 1
+]
+EOF
+
+run_test ./drive_json_op get "/6" <<EOF
+[null, true, 1, "str", {"sub":[10, 11]}, [21, [33, 34], 66], 2]
+EOF
+
+check_output "cannot read array value" <<EOF
+2
+EOF
+
+run_test ./drive_json_op get "/ID" <<EOF
+{"ID":"P1","ProcessID":"P1","Name":"VxWorks","CanSuspend":true,"CanResume":1,"IsContainer":true,"WordSize":4,"CanTerminate":true,"CanDetach":true,"RCGroup":"P1","SymbolsGroup":"P1","CPUGroup":"P1","DiagnosticTestProcess":true}
+EOF
+
+check_output "cannot read key value" <<EOF
+"P1"
+EOF
+
+run_test ./drive_json_op get "/arr/1/id" <<EOF
+{"arr": [{"id": 1}, {"id": 2}]}
+EOF
+
+check_output "cannot read key value" <<EOF
+2
+EOF
diff --git a/src/yajlpp/test_json_ptr.cc b/src/yajlpp/test_json_ptr.cc
new file mode 100644
index 0000000..0c530c0
--- /dev/null
+++ b/src/yajlpp/test_json_ptr.cc
@@ -0,0 +1,73 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY TIMOTHY STACK AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file test_json_ptr.cc
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "config.h"
+#include "yajlpp/json_ptr.hh"
+
+int
+main(int argc, const char* argv[])
+{
+ int32_t depth, index;
+
+ {
+ json_ptr jptr("");
+
+ depth = 0;
+ index = -1;
+ assert(jptr.at_index(depth, index));
+ }
+
+ {
+ json_ptr jptr("/");
+
+ depth = 0;
+ index = -1;
+ assert(!jptr.at_index(depth, index));
+ assert(jptr.expect_map(depth, index));
+ assert(jptr.at_index(depth, index));
+ }
+
+ {
+ json_ptr jptr("/foo/bar");
+
+ depth = 0;
+ index = -1;
+ assert(jptr.expect_map(depth, index));
+ assert(jptr.at_key(depth, "foo"));
+ assert(jptr.expect_map(depth, index));
+ assert(jptr.at_key(depth, "bar"));
+ assert(jptr.at_index(depth, index));
+ }
+}
diff --git a/src/yajlpp/test_json_ptr_walk.sh b/src/yajlpp/test_json_ptr_walk.sh
new file mode 100644
index 0000000..bcaf1ec
--- /dev/null
+++ b/src/yajlpp/test_json_ptr_walk.sh
@@ -0,0 +1,77 @@
+#! /bin/bash
+
+run_test ./drive_json_ptr_walk <<EOF
+{ "foo" : 1 }
+EOF
+
+check_output "simple object" <<EOF
+/foo = 1
+EOF
+
+run_test ./drive_json_ptr_walk <<EOF
+{ "~tstack/julia" : 1 }
+EOF
+
+check_output "escaped object" <<EOF
+/~0tstack~1julia = 1
+EOF
+
+run_test ./drive_json_ptr_walk <<EOF
+1
+EOF
+
+check_output "root value" <<EOF
+ = 1
+EOF
+
+run_test ./drive_json_ptr_walk <<EOF
+[1, 2, 3]
+EOF
+
+check_output "array" <<EOF
+/0 = 1
+/1 = 2
+/2 = 3
+EOF
+
+run_test ./drive_json_ptr_walk <<EOF
+[1, 2, 3, [4, 5, 6], 7, 8, 9, [10, 11, [12, 13, 14], 15], 16]
+EOF
+
+check_output "nested array" <<EOF
+/0 = 1
+/1 = 2
+/2 = 3
+/3/0 = 4
+/3/1 = 5
+/3/2 = 6
+/4 = 7
+/5 = 8
+/6 = 9
+/7/0 = 10
+/7/1 = 11
+/7/2/0 = 12
+/7/2/1 = 13
+/7/2/2 = 14
+/7/3 = 15
+/8 = 16
+EOF
+
+run_test ./drive_json_ptr_walk <<EOF
+[null, true, 123.0, "foo", { "bar" : { "baz" : [1, 2, 3]} }, ["a", null]]
+EOF
+
+check_error_output "" <<EOF
+EOF
+
+check_output "complex" <<EOF
+/0 = null
+/1 = true
+/2 = 123.0
+/3 = "foo"
+/4/bar/baz/0 = 1
+/4/bar/baz/1 = 2
+/4/bar/baz/2 = 3
+/5/0 = "a"
+/5/1 = null
+EOF
diff --git a/src/yajlpp/test_yajlpp.cc b/src/yajlpp/test_yajlpp.cc
new file mode 100644
index 0000000..afa60a3
--- /dev/null
+++ b/src/yajlpp/test_yajlpp.cc
@@ -0,0 +1,166 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file test_yajlpp.cc
+ */
+
+#include <assert.h>
+#include <stdio.h>
+
+#include "yajlpp.hh"
+#include "yajlpp_def.hh"
+
+const char* TEST_DATA = R"([{ "foo": 0 }, 2, { "foo": 1 }])";
+
+const char* TEST_OBJ_DATA = "{ \"foo\": 0 }";
+
+static int FOO_COUNT = 0;
+static int CONST_COUNT = 0;
+
+static int
+read_foo(yajlpp_parse_context* ypc, long long value)
+{
+ assert(value == FOO_COUNT);
+ assert(ypc->ypc_array_index.empty()
+ || ypc->ypc_array_index.back() == FOO_COUNT);
+
+ FOO_COUNT += 1;
+
+ return 1;
+}
+
+static int
+read_const(yajlpp_parse_context* ypc, long long value)
+{
+ CONST_COUNT += 1;
+
+ return 1;
+}
+
+static int
+dummy_string_handler(void* ctx, const unsigned char* s, size_t len)
+{
+ return 1;
+}
+
+int
+main(int argc, char* argv[])
+{
+ static const auto TEST_SRC = intern_string::lookup("test_data");
+
+ {
+ struct dummy {};
+
+ typed_json_path_container<dummy> dummy_handlers = {
+
+ };
+
+ std::string in1 = "{\"#\":{\"";
+ auto parse_res = dummy_handlers.parser_for(TEST_SRC).of(in1);
+ }
+
+ {
+ static const char UNICODE_BARF[] = "\"\\udb00\\\\0\"\n";
+
+ yajl_callbacks cbs;
+ memset(&cbs, 0, sizeof(cbs));
+ cbs.yajl_string = dummy_string_handler;
+ auto handle = yajl_alloc(&cbs, nullptr, nullptr);
+ auto rc = yajl_parse(handle, (const unsigned char*) UNICODE_BARF, 12);
+ assert(rc == yajl_status_ok);
+ yajl_free(handle);
+ }
+
+ struct json_path_container test_obj_handler = {
+ json_path_handler("foo", read_foo),
+ };
+
+ {
+ struct json_path_container test_array_handlers = {
+ json_path_handler("#")
+ .add_cb(read_const)
+ .with_children(test_obj_handler),
+ };
+
+ yajlpp_parse_context ypc(TEST_SRC, &test_array_handlers);
+ auto handle = yajl_alloc(&ypc.ypc_callbacks, nullptr, &ypc);
+ ypc.with_handle(handle);
+ ypc.parse((const unsigned char*) TEST_DATA, strlen(TEST_DATA));
+ yajl_free(handle);
+
+ assert(FOO_COUNT == 2);
+ assert(CONST_COUNT == 1);
+ }
+
+ {
+ FOO_COUNT = 0;
+
+ yajlpp_parse_context ypc(TEST_SRC, &test_obj_handler);
+ auto handle = yajl_alloc(&ypc.ypc_callbacks, nullptr, &ypc);
+ ypc.with_handle(handle);
+
+ ypc.parse(reinterpret_cast<const unsigned char*>(TEST_OBJ_DATA),
+ strlen(TEST_OBJ_DATA));
+ yajl_free(handle);
+
+ assert(FOO_COUNT == 1);
+ }
+
+ {
+ const char* TEST_INPUT = R"({
+ "msg": "Hello, World!",
+ "parent1": {
+ "child": {}
+ },
+ "parent2": {
+ "child": {"name": "steve"}
+ },
+ "parent3": {
+ "child": {},
+ "sibling": {"name": "mongoose"}
+ }
+})";
+ const std::string EXPECTED_OUTPUT
+ = "{\"msg\":\"Hello, "
+ "World!\",\"parent2\":{\"child\":{\"name\":\"steve\"}},"
+ "\"parent3\":{\"sibling\":{\"name\":\"mongoose\"}}}";
+
+ char errbuf[1024];
+
+ auto tree = yajl_tree_parse(TEST_INPUT, errbuf, sizeof(errbuf));
+ yajl_cleanup_tree(tree);
+
+ yajlpp_gen gen;
+
+ yajl_gen_tree(gen, tree);
+ auto actual = gen.to_string_fragment().to_string();
+ assert(EXPECTED_OUTPUT == actual);
+
+ yajl_tree_free(tree);
+ }
+}
diff --git a/src/yajlpp/yajlpp.cc b/src/yajlpp/yajlpp.cc
new file mode 100644
index 0000000..b1362ab
--- /dev/null
+++ b/src/yajlpp/yajlpp.cc
@@ -0,0 +1,1574 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file yajlpp.cc
+ */
+
+#include <regex>
+#include <utility>
+
+#include "yajlpp.hh"
+
+#include "base/fs_util.hh"
+#include "base/snippet_highlighters.hh"
+#include "config.h"
+#include "fmt/format.h"
+#include "ghc/filesystem.hpp"
+#include "yajl/api/yajl_parse.h"
+#include "yajlpp_def.hh"
+
+const json_path_handler_base::enum_value_t
+ json_path_handler_base::ENUM_TERMINATOR((const char*) nullptr, 0);
+
+yajl_gen_status
+yajl_gen_tree(yajl_gen hand, yajl_val val)
+{
+ switch (val->type) {
+ case yajl_t_string: {
+ return yajl_gen_string(hand, YAJL_GET_STRING(val));
+ }
+ case yajl_t_number: {
+ if (YAJL_IS_INTEGER(val)) {
+ return yajl_gen_integer(hand, YAJL_GET_INTEGER(val));
+ }
+ if (YAJL_IS_DOUBLE(val)) {
+ return yajl_gen_double(hand, YAJL_GET_DOUBLE(val));
+ }
+ return yajl_gen_number(
+ hand, YAJL_GET_NUMBER(val), strlen(YAJL_GET_NUMBER(val)));
+ }
+ case yajl_t_object: {
+ auto rc = yajl_gen_map_open(hand);
+ if (rc != yajl_gen_status_ok) {
+ return rc;
+ }
+ for (size_t lpc = 0; lpc < YAJL_GET_OBJECT(val)->len; lpc++) {
+ rc = yajl_gen_string(hand, YAJL_GET_OBJECT(val)->keys[lpc]);
+ if (rc != yajl_gen_status_ok) {
+ return rc;
+ }
+ rc = yajl_gen_tree(hand, YAJL_GET_OBJECT(val)->values[lpc]);
+ if (rc != yajl_gen_status_ok) {
+ return rc;
+ }
+ }
+ rc = yajl_gen_map_close(hand);
+ if (rc != yajl_gen_status_ok) {
+ return rc;
+ }
+ return yajl_gen_status_ok;
+ }
+ case yajl_t_array: {
+ auto rc = yajl_gen_array_open(hand);
+ if (rc != yajl_gen_status_ok) {
+ return rc;
+ }
+ for (size_t lpc = 0; lpc < YAJL_GET_ARRAY(val)->len; lpc++) {
+ rc = yajl_gen_tree(hand, YAJL_GET_ARRAY(val)->values[lpc]);
+ if (rc != yajl_gen_status_ok) {
+ return rc;
+ }
+ }
+ rc = yajl_gen_array_close(hand);
+ if (rc != yajl_gen_status_ok) {
+ return rc;
+ }
+ return yajl_gen_status_ok;
+ }
+ case yajl_t_true: {
+ return yajl_gen_bool(hand, true);
+ }
+ case yajl_t_false: {
+ return yajl_gen_bool(hand, false);
+ }
+ case yajl_t_null: {
+ return yajl_gen_null(hand);
+ }
+ default:
+ return yajl_gen_status_ok;
+ }
+}
+
+void
+yajl_cleanup_tree(yajl_val val)
+{
+ if (YAJL_IS_OBJECT(val)) {
+ auto* val_as_obj = YAJL_GET_OBJECT(val);
+
+ for (size_t lpc = 0; lpc < val_as_obj->len;) {
+ auto* child_val = val_as_obj->values[lpc];
+
+ yajl_cleanup_tree(child_val);
+ if (YAJL_IS_OBJECT(child_val)
+ && YAJL_GET_OBJECT(child_val)->len == 0)
+ {
+ free((char*) val_as_obj->keys[lpc]);
+ yajl_tree_free(val_as_obj->values[lpc]);
+ val_as_obj->len -= 1;
+ for (auto lpc2 = lpc; lpc2 < val_as_obj->len; lpc2++) {
+ val_as_obj->keys[lpc2] = val_as_obj->keys[lpc2 + 1];
+ val_as_obj->values[lpc2] = val_as_obj->values[lpc2 + 1];
+ }
+ } else {
+ lpc++;
+ }
+ }
+ }
+}
+
+json_path_handler_base::json_path_handler_base(const std::string& property)
+ : jph_property(property.back() == '#'
+ ? property.substr(0, property.size() - 1)
+ : property),
+ jph_regex(lnav::pcre2pp::code::from(lnav::pcre2pp::quote(property),
+ PCRE2_ANCHORED)
+ .unwrap()
+ .to_shared()),
+ jph_is_array(property.back() == '#')
+{
+ memset(&this->jph_callbacks, 0, sizeof(this->jph_callbacks));
+}
+
+static std::string
+scrub_pattern(const std::string& pattern)
+{
+ static const std::regex CAPTURE(R"(\(\?\<\w+\>)");
+
+ return std::regex_replace(pattern, CAPTURE, "(");
+}
+
+json_path_handler_base::json_path_handler_base(
+ const std::shared_ptr<const lnav::pcre2pp::code>& property)
+ : jph_property(scrub_pattern(property->get_pattern())), jph_regex(property),
+ jph_is_array(property->get_pattern().find('#') != std::string::npos),
+ jph_is_pattern_property(property->get_capture_count() > 0)
+{
+ memset(&this->jph_callbacks, 0, sizeof(this->jph_callbacks));
+}
+
+json_path_handler_base::json_path_handler_base(
+ std::string property,
+ const std::shared_ptr<const lnav::pcre2pp::code>& property_re)
+ : jph_property(std::move(property)), jph_regex(property_re),
+ jph_is_array(property_re->get_pattern().find('#') != std::string::npos)
+{
+ memset(&this->jph_callbacks, 0, sizeof(this->jph_callbacks));
+}
+
+yajl_gen_status
+json_path_handler_base::gen(yajlpp_gen_context& ygc, yajl_gen handle) const
+{
+ if (this->jph_is_array) {
+ auto size = this->jph_size_provider(ygc.ygc_obj_stack.top());
+ auto md = lnav::pcre2pp::match_data::unitialized();
+
+ yajl_gen_string(handle, this->jph_property);
+ yajl_gen_array_open(handle);
+ for (size_t index = 0; index < size; index++) {
+ yajlpp_provider_context ypc{&md, index};
+ yajlpp_gen_context elem_ygc(handle, *this->jph_children);
+ elem_ygc.ygc_depth = 1;
+ elem_ygc.ygc_obj_stack.push(
+ this->jph_obj_provider(ypc, ygc.ygc_obj_stack.top()));
+
+ elem_ygc.gen();
+ }
+ yajl_gen_array_close(handle);
+
+ return yajl_gen_status_ok;
+ }
+
+ std::vector<std::string> local_paths;
+
+ if (this->jph_path_provider) {
+ this->jph_path_provider(ygc.ygc_obj_stack.top(), local_paths);
+ } else {
+ local_paths.emplace_back(this->jph_property);
+ }
+
+ if (this->jph_children) {
+ for (const auto& lpath : local_paths) {
+ std::string full_path = lpath;
+ if (this->jph_path_provider) {
+ full_path += "/";
+ }
+ int start_depth = ygc.ygc_depth;
+
+ yajl_gen_string(handle, lpath);
+ yajl_gen_map_open(handle);
+ ygc.ygc_depth += 1;
+
+ if (this->jph_obj_provider) {
+ static thread_local auto md
+ = lnav::pcre2pp::match_data::unitialized();
+
+ auto find_res = this->jph_regex->capture_from(full_path)
+ .into(md)
+ .matches();
+
+ ygc.ygc_obj_stack.push(this->jph_obj_provider(
+ {&md, yajlpp_provider_context::nindex},
+ ygc.ygc_obj_stack.top()));
+ if (!ygc.ygc_default_stack.empty()) {
+ ygc.ygc_default_stack.push(this->jph_obj_provider(
+ {&md, yajlpp_provider_context::nindex},
+ ygc.ygc_default_stack.top()));
+ }
+ }
+
+ for (const auto& jph : this->jph_children->jpc_children) {
+ yajl_gen_status status = jph.gen(ygc, handle);
+
+ const unsigned char* buf;
+ size_t len;
+ yajl_gen_get_buf(handle, &buf, &len);
+ if (status != yajl_gen_status_ok) {
+ log_error("yajl_gen failure for: %s -- %d",
+ jph.jph_property.c_str(),
+ status);
+ return status;
+ }
+ }
+
+ if (this->jph_obj_provider) {
+ ygc.ygc_obj_stack.pop();
+ if (!ygc.ygc_default_stack.empty()) {
+ ygc.ygc_default_stack.pop();
+ }
+ }
+
+ while (ygc.ygc_depth > start_depth) {
+ yajl_gen_map_close(handle);
+ ygc.ygc_depth -= 1;
+ }
+ }
+ } else if (this->jph_gen_callback != nullptr) {
+ return this->jph_gen_callback(ygc, *this, handle);
+ }
+
+ return yajl_gen_status_ok;
+}
+
+const char* const SCHEMA_TYPE_STRINGS[] = {
+ "any",
+ "boolean",
+ "integer",
+ "number",
+ "string",
+ "array",
+ "object",
+};
+
+yajl_gen_status
+json_path_handler_base::gen_schema(yajlpp_gen_context& ygc) const
+{
+ if (this->jph_children) {
+ {
+ yajlpp_map schema(ygc.ygc_handle);
+
+ if (this->jph_description && this->jph_description[0]) {
+ schema.gen("description");
+ schema.gen(this->jph_description);
+ }
+ if (this->jph_is_pattern_property) {
+ ygc.ygc_path.emplace_back(
+ fmt::format(FMT_STRING("<{}>"),
+ this->jph_regex->get_name_for_capture(1)));
+ } else {
+ ygc.ygc_path.emplace_back(this->jph_property);
+ }
+ if (this->jph_children->jpc_definition_id.empty()) {
+ schema.gen("title");
+ schema.gen(fmt::format(FMT_STRING("/{}"),
+ fmt::join(ygc.ygc_path, "/")));
+ schema.gen("type");
+ if (this->jph_is_array) {
+ if (this->jph_regex->get_pattern().find("#?")
+ == std::string::npos)
+ {
+ schema.gen("array");
+ } else {
+ yajlpp_array type_array(ygc.ygc_handle);
+
+ type_array.gen("array");
+ for (auto schema_type : this->get_types()) {
+ type_array.gen(
+ SCHEMA_TYPE_STRINGS[(int) schema_type]);
+ }
+ }
+ schema.gen("items");
+ yajl_gen_map_open(ygc.ygc_handle);
+ yajl_gen_string(ygc.ygc_handle, "type");
+ this->gen_schema_type(ygc);
+ } else {
+ this->gen_schema_type(ygc);
+ }
+ this->jph_children->gen_schema(ygc);
+ if (this->jph_is_array) {
+ yajl_gen_map_close(ygc.ygc_handle);
+ }
+ } else {
+ schema.gen("title");
+ schema.gen(fmt::format(FMT_STRING("/{}"),
+ fmt::join(ygc.ygc_path, "/")));
+ this->jph_children->gen_schema(ygc);
+ }
+ ygc.ygc_path.pop_back();
+ }
+ } else {
+ yajlpp_map schema(ygc.ygc_handle);
+
+ if (this->jph_is_pattern_property) {
+ ygc.ygc_path.emplace_back(fmt::format(
+ FMT_STRING("<{}>"), this->jph_regex->get_name_for_capture(1)));
+ } else {
+ ygc.ygc_path.emplace_back(this->jph_property);
+ }
+
+ schema.gen("title");
+ schema.gen(
+ fmt::format(FMT_STRING("/{}"), fmt::join(ygc.ygc_path, "/")));
+ if (this->jph_description && this->jph_description[0]) {
+ schema.gen("description");
+ schema.gen(this->jph_description);
+ }
+
+ schema.gen("type");
+
+ if (this->jph_is_array) {
+ if (this->jph_regex->get_pattern().find("#?") == std::string::npos)
+ {
+ schema.gen("array");
+ } else {
+ yajlpp_array type_array(ygc.ygc_handle);
+
+ type_array.gen("array");
+ for (auto schema_type : this->get_types()) {
+ type_array.gen(SCHEMA_TYPE_STRINGS[(int) schema_type]);
+ }
+ }
+ yajl_gen_string(ygc.ygc_handle, "items");
+ yajl_gen_map_open(ygc.ygc_handle);
+ yajl_gen_string(ygc.ygc_handle, "type");
+ }
+
+ this->gen_schema_type(ygc);
+
+ if (!this->jph_examples.empty()) {
+ schema.gen("examples");
+
+ yajlpp_array example_array(ygc.ygc_handle);
+ for (const auto& ex : this->jph_examples) {
+ example_array.gen(ex);
+ }
+ }
+
+ if (this->jph_is_array) {
+ yajl_gen_map_close(ygc.ygc_handle);
+ }
+
+ ygc.ygc_path.pop_back();
+ }
+
+ return yajl_gen_status_ok;
+}
+
+yajl_gen_status
+json_path_handler_base::gen_schema_type(yajlpp_gen_context& ygc) const
+{
+ yajlpp_generator schema(ygc.ygc_handle);
+
+ auto types = this->get_types();
+ if (types.size() == 1) {
+ yajl_gen_string(ygc.ygc_handle, SCHEMA_TYPE_STRINGS[(int) types[0]]);
+ } else {
+ yajlpp_array type_array(ygc.ygc_handle);
+
+ for (auto schema_type : types) {
+ type_array.gen(SCHEMA_TYPE_STRINGS[(int) schema_type]);
+ }
+ }
+
+ for (auto& schema_type : types) {
+ switch (schema_type) {
+ case schema_type_t::STRING:
+ if (this->jph_min_length > 0) {
+ schema("minLength");
+ schema(this->jph_min_length);
+ }
+ if (this->jph_max_length < INT_MAX) {
+ schema("maxLength");
+ schema(this->jph_max_length);
+ }
+ if (this->jph_pattern_re) {
+ schema("pattern");
+ schema(this->jph_pattern_re);
+ }
+ if (this->jph_enum_values) {
+ schema("enum");
+
+ yajlpp_array enum_array(ygc.ygc_handle);
+ for (int lpc = 0; this->jph_enum_values[lpc].first; lpc++) {
+ enum_array.gen(this->jph_enum_values[lpc].first);
+ }
+ }
+ break;
+ case schema_type_t::INTEGER:
+ case schema_type_t::NUMBER:
+ if (this->jph_min_value > LLONG_MIN) {
+ schema("minimum");
+ schema(this->jph_min_value);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ return yajl_gen_keys_must_be_strings;
+}
+
+void
+json_path_handler_base::walk(
+ const std::function<
+ void(const json_path_handler_base&, const std::string&, void*)>& cb,
+ void* root,
+ const std::string& base) const
+{
+ std::vector<std::string> local_paths;
+
+ if (this->jph_path_provider) {
+ this->jph_path_provider(root, local_paths);
+
+ for (auto& lpath : local_paths) {
+ cb(*this,
+ fmt::format(FMT_STRING("{}{}{}"),
+ base,
+ lpath,
+ this->jph_children ? "/" : ""),
+ nullptr);
+ }
+ if (this->jph_obj_deleter) {
+ local_paths.clear();
+ this->jph_path_provider(root, local_paths);
+ }
+ } else {
+ local_paths.emplace_back(this->jph_property);
+
+ std::string full_path = base + this->jph_property;
+ if (this->jph_children) {
+ full_path += "/";
+ }
+ cb(*this, full_path, nullptr);
+ }
+
+ if (this->jph_children) {
+ for (const auto& lpath : local_paths) {
+ for (const auto& jph : this->jph_children->jpc_children) {
+ static const intern_string_t POSS_SRC
+ = intern_string::lookup("possibilities");
+
+ std::string full_path = base + lpath;
+ if (this->jph_children) {
+ full_path += "/";
+ }
+ json_path_container dummy{
+ json_path_handler(this->jph_property, this->jph_regex),
+ };
+
+ yajlpp_parse_context ypc(POSS_SRC, &dummy);
+ void* child_root = root;
+
+ ypc.set_path(full_path).with_obj(root).update_callbacks();
+ if (this->jph_obj_provider) {
+ static thread_local auto md
+ = lnav::pcre2pp::match_data::unitialized();
+
+ std::string full_path = lpath + "/";
+
+ if (!this->jph_regex->capture_from(full_path)
+ .into(md)
+ .matches()
+ .ignore_error())
+ {
+ ensure(false);
+ }
+ child_root = this->jph_obj_provider(
+ {&md, yajlpp_provider_context::nindex}, root);
+ }
+
+ jph.walk(cb, child_root, full_path);
+ }
+ }
+ } else {
+ for (auto& lpath : local_paths) {
+ void* field = nullptr;
+
+ if (this->jph_field_getter) {
+ field = this->jph_field_getter(root, lpath);
+ }
+ cb(*this, base + lpath, field);
+ }
+ }
+}
+
+nonstd::optional<int>
+json_path_handler_base::to_enum_value(const string_fragment& sf) const
+{
+ for (int lpc = 0; this->jph_enum_values[lpc].first; lpc++) {
+ const auto& ev = this->jph_enum_values[lpc];
+
+ if (sf == ev.first) {
+ return ev.second;
+ }
+ }
+
+ return nonstd::nullopt;
+}
+
+const char*
+json_path_handler_base::to_enum_string(int value) const
+{
+ for (int lpc = 0; this->jph_enum_values[lpc].first; lpc++) {
+ const auto& ev = this->jph_enum_values[lpc];
+
+ if (ev.second == value) {
+ return ev.first;
+ }
+ }
+
+ return "";
+}
+
+std::vector<json_path_handler_base::schema_type_t>
+json_path_handler_base::get_types() const
+{
+ std::vector<schema_type_t> retval;
+
+ if (this->jph_callbacks.yajl_boolean) {
+ retval.push_back(schema_type_t::BOOLEAN);
+ }
+ if (this->jph_callbacks.yajl_integer) {
+ retval.push_back(schema_type_t::INTEGER);
+ }
+ if (this->jph_callbacks.yajl_double || this->jph_callbacks.yajl_number) {
+ retval.push_back(schema_type_t::NUMBER);
+ }
+ if (this->jph_callbacks.yajl_string) {
+ retval.push_back(schema_type_t::STRING);
+ }
+ if (this->jph_children) {
+ retval.push_back(schema_type_t::OBJECT);
+ }
+ if (retval.empty()) {
+ retval.push_back(schema_type_t::ANY);
+ }
+ return retval;
+}
+
+yajlpp_parse_context::yajlpp_parse_context(
+ intern_string_t source, const struct json_path_container* handlers)
+ : ypc_source(source), ypc_handlers(handlers)
+{
+ this->ypc_path.reserve(4096);
+ this->ypc_path.push_back('/');
+ this->ypc_path.push_back('\0');
+ this->ypc_callbacks = DEFAULT_CALLBACKS;
+ memset(&this->ypc_alt_callbacks, 0, sizeof(this->ypc_alt_callbacks));
+}
+
+int
+yajlpp_parse_context::map_start(void* ctx)
+{
+ yajlpp_parse_context* ypc = (yajlpp_parse_context*) ctx;
+ int retval = 1;
+
+ require(ypc->ypc_path.size() >= 2);
+
+ ypc->ypc_path_index_stack.push_back(ypc->ypc_path.size() - 1);
+
+ if (ypc->ypc_path.size() > 1
+ && ypc->ypc_path[ypc->ypc_path.size() - 2] == '#')
+ {
+ ypc->ypc_array_index.back() += 1;
+ }
+
+ if (ypc->ypc_alt_callbacks.yajl_start_map != nullptr) {
+ retval = ypc->ypc_alt_callbacks.yajl_start_map(ypc);
+ }
+
+ return retval;
+}
+
+int
+yajlpp_parse_context::map_key(void* ctx, const unsigned char* key, size_t len)
+{
+ yajlpp_parse_context* ypc = (yajlpp_parse_context*) ctx;
+ int retval = 1;
+
+ require(ypc->ypc_path.size() >= 2);
+
+ ypc->ypc_path.resize(ypc->ypc_path_index_stack.back());
+ if (ypc->ypc_path.back() != '/') {
+ ypc->ypc_path.push_back('/');
+ }
+ for (size_t lpc = 0; lpc < len; lpc++) {
+ switch (key[lpc]) {
+ case '~':
+ ypc->ypc_path.push_back('~');
+ ypc->ypc_path.push_back('0');
+ break;
+ case '/':
+ ypc->ypc_path.push_back('~');
+ ypc->ypc_path.push_back('1');
+ break;
+ case '#':
+ ypc->ypc_path.push_back('~');
+ ypc->ypc_path.push_back('2');
+ break;
+ default:
+ ypc->ypc_path.push_back(key[lpc]);
+ break;
+ }
+ }
+ ypc->ypc_path.push_back('\0');
+
+ if (ypc->ypc_alt_callbacks.yajl_map_key != nullptr) {
+ retval = ypc->ypc_alt_callbacks.yajl_map_key(ctx, key, len);
+ }
+
+ if (ypc->ypc_handlers != nullptr) {
+ ypc->update_callbacks();
+ }
+
+ ensure(ypc->ypc_path.size() >= 2);
+
+ return retval;
+}
+
+void
+yajlpp_parse_context::update_callbacks(const json_path_container* orig_handlers,
+ int child_start)
+{
+ const json_path_container* handlers = orig_handlers;
+
+ this->ypc_current_handler = nullptr;
+
+ if (this->ypc_handlers == nullptr) {
+ return;
+ }
+
+ this->ypc_sibling_handlers = orig_handlers;
+ this->ypc_callbacks = DEFAULT_CALLBACKS;
+
+ if (handlers == nullptr) {
+ handlers = this->ypc_handlers;
+ this->ypc_handler_stack.clear();
+ this->ypc_array_handler_count = 0;
+ }
+
+ if (!this->ypc_active_paths.empty()) {
+ std::string curr_path(&this->ypc_path[0], this->ypc_path.size() - 1);
+
+ if (this->ypc_active_paths.find(curr_path)
+ == this->ypc_active_paths.end())
+ {
+ return;
+ }
+ }
+
+ if (child_start == 0 && !this->ypc_obj_stack.empty()) {
+ while (this->ypc_obj_stack.size() > 1) {
+ this->ypc_obj_stack.pop();
+ }
+ }
+
+ auto path_frag = string_fragment::from_byte_range(
+ this->ypc_path.data(), 1 + child_start, this->ypc_path.size() - 1);
+ for (const auto& jph : handlers->jpc_children) {
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+
+ if (jph.jph_regex->capture_from(path_frag)
+ .into(md)
+ .matches()
+ .ignore_error()
+ && (md.remaining().empty() || md.remaining().startswith("/")))
+ {
+ auto cap = md[0].value();
+
+ if (jph.jph_is_array) {
+ this->ypc_array_handler_count += 1;
+ }
+ if (jph.jph_obj_provider) {
+ auto index = this->ypc_array_handler_count == 0
+ ? static_cast<size_t>(-1)
+ : this->ypc_array_index[this->ypc_array_handler_count - 1];
+
+ if ((cap.sf_end != (int) this->ypc_path.size() - 1)
+ && (!jph.is_array()
+ || index != yajlpp_provider_context::nindex))
+ {
+ this->ypc_obj_stack.push(jph.jph_obj_provider(
+ {&md, index, this}, this->ypc_obj_stack.top()));
+ }
+ }
+
+ if (jph.jph_children) {
+ this->ypc_handler_stack.emplace_back(&jph);
+
+ if (cap.sf_end != (int) this->ypc_path.size() - 1) {
+ this->update_callbacks(jph.jph_children, cap.sf_end);
+ return;
+ }
+ } else {
+ if (cap.sf_end != (int) this->ypc_path.size() - 1) {
+ continue;
+ }
+
+ this->ypc_current_handler = &jph;
+ }
+
+ if (jph.jph_callbacks.yajl_null != nullptr) {
+ this->ypc_callbacks.yajl_null = jph.jph_callbacks.yajl_null;
+ }
+ if (jph.jph_callbacks.yajl_boolean != nullptr) {
+ this->ypc_callbacks.yajl_boolean
+ = jph.jph_callbacks.yajl_boolean;
+ }
+ if (jph.jph_callbacks.yajl_integer != nullptr) {
+ this->ypc_callbacks.yajl_integer
+ = jph.jph_callbacks.yajl_integer;
+ }
+ if (jph.jph_callbacks.yajl_number != nullptr) {
+ this->ypc_callbacks.yajl_number = jph.jph_callbacks.yajl_number;
+ }
+ if (jph.jph_callbacks.yajl_double != nullptr) {
+ this->ypc_callbacks.yajl_double = jph.jph_callbacks.yajl_double;
+ }
+ if (jph.jph_callbacks.yajl_string != nullptr) {
+ this->ypc_callbacks.yajl_string = jph.jph_callbacks.yajl_string;
+ }
+ if (jph.jph_is_array) {
+ this->ypc_array_handler_count -= 1;
+ }
+ return;
+ }
+ }
+}
+
+int
+yajlpp_parse_context::map_end(void* ctx)
+{
+ yajlpp_parse_context* ypc = (yajlpp_parse_context*) ctx;
+ int retval = 1;
+
+ ypc->ypc_path.resize(ypc->ypc_path_index_stack.back());
+ ypc->ypc_path.push_back('\0');
+ ypc->ypc_path_index_stack.pop_back();
+
+ if (ypc->ypc_alt_callbacks.yajl_end_map != nullptr) {
+ retval = ypc->ypc_alt_callbacks.yajl_end_map(ctx);
+ }
+
+ ypc->update_callbacks();
+
+ ensure(ypc->ypc_path.size() >= 2);
+
+ return retval;
+}
+
+int
+yajlpp_parse_context::array_start(void* ctx)
+{
+ yajlpp_parse_context* ypc = (yajlpp_parse_context*) ctx;
+ int retval = 1;
+
+ ypc->ypc_path_index_stack.push_back(ypc->ypc_path.size() - 1);
+ ypc->ypc_path[ypc->ypc_path.size() - 1] = '#';
+ ypc->ypc_path.push_back('\0');
+ ypc->ypc_array_index.push_back(-1);
+
+ if (ypc->ypc_alt_callbacks.yajl_start_array != nullptr) {
+ retval = ypc->ypc_alt_callbacks.yajl_start_array(ctx);
+ }
+
+ ypc->update_callbacks();
+
+ ensure(ypc->ypc_path.size() >= 2);
+
+ return retval;
+}
+
+int
+yajlpp_parse_context::array_end(void* ctx)
+{
+ yajlpp_parse_context* ypc = (yajlpp_parse_context*) ctx;
+ int retval = 1;
+
+ ypc->ypc_path.resize(ypc->ypc_path_index_stack.back());
+ ypc->ypc_path.push_back('\0');
+ ypc->ypc_path_index_stack.pop_back();
+ ypc->ypc_array_index.pop_back();
+
+ if (ypc->ypc_alt_callbacks.yajl_end_array != nullptr) {
+ retval = ypc->ypc_alt_callbacks.yajl_end_array(ctx);
+ }
+
+ ypc->update_callbacks();
+
+ ensure(ypc->ypc_path.size() >= 2);
+
+ return retval;
+}
+
+int
+yajlpp_parse_context::handle_unused(void* ctx)
+{
+ yajlpp_parse_context* ypc = (yajlpp_parse_context*) ctx;
+
+ if (ypc->ypc_ignore_unused) {
+ return 1;
+ }
+
+ const json_path_handler_base* handler = ypc->ypc_current_handler;
+ lnav::console::user_message msg;
+
+ if (handler != nullptr && strlen(handler->jph_synopsis) > 0
+ && strlen(handler->jph_description) > 0)
+ {
+ auto help_text = handler->get_help_text(ypc);
+ std::vector<std::string> expected_types;
+
+ if (ypc->ypc_callbacks.yajl_boolean
+ != (int (*)(void*, int)) yajlpp_parse_context::handle_unused)
+ {
+ expected_types.emplace_back("boolean");
+ }
+ if (ypc->ypc_callbacks.yajl_integer
+ != (int (*)(void*, long long)) yajlpp_parse_context::handle_unused)
+ {
+ expected_types.emplace_back("integer");
+ }
+ if (ypc->ypc_callbacks.yajl_double
+ != (int (*)(void*, double)) yajlpp_parse_context::handle_unused)
+ {
+ expected_types.emplace_back("float");
+ }
+ if (ypc->ypc_callbacks.yajl_string
+ != (int (*)(void*, const unsigned char*, size_t))
+ yajlpp_parse_context::handle_unused)
+ {
+ expected_types.emplace_back("string");
+ }
+ if (!expected_types.empty()) {
+ help_text.appendf(
+ FMT_STRING(" expecting one of the following types: {}"),
+ fmt::join(expected_types, ", "));
+ }
+ msg = lnav::console::user_message::warning(
+ attr_line_t("unexpected data for property ")
+ .append_quoted(lnav::roles::symbol(
+ ypc->get_full_path().to_string())))
+ .with_help(help_text);
+ } else if (ypc->ypc_path[1]) {
+ msg = lnav::console::user_message::warning(
+ attr_line_t("unexpected value for property ")
+ .append_quoted(
+ lnav::roles::symbol(ypc->get_full_path().to_string())));
+ } else {
+ msg = lnav::console::user_message::error("unexpected JSON value");
+ }
+
+ if (handler == nullptr) {
+ const json_path_container* accepted_handlers;
+
+ if (ypc->ypc_sibling_handlers) {
+ accepted_handlers = ypc->ypc_sibling_handlers;
+ } else {
+ accepted_handlers = ypc->ypc_handlers;
+ }
+
+ attr_line_t help_text;
+
+ if (accepted_handlers->jpc_children.size() == 1
+ && accepted_handlers->jpc_children.front().jph_is_array)
+ {
+ const auto& jph = accepted_handlers->jpc_children.front();
+
+ help_text.append("expecting an array of ")
+ .append(lnav::roles::variable(jph.jph_synopsis))
+ .append(" values");
+ } else {
+ help_text.append(lnav::roles::h2("Available Properties"))
+ .append("\n");
+ for (const auto& jph : accepted_handlers->jpc_children) {
+ help_text.append(" ")
+ .append(lnav::roles::symbol(jph.jph_property))
+ .append(lnav::roles::symbol(
+ jph.jph_children != nullptr ? "/" : ""))
+ .append(" ")
+ .append(lnav::roles::variable(jph.jph_synopsis))
+ .append("\n");
+ }
+ }
+ msg.with_help(help_text);
+ }
+
+ msg.with_snippet(ypc->get_snippet());
+
+ ypc->report_error(msg);
+
+ return 1;
+}
+
+int
+yajlpp_parse_context::handle_unused_or_delete(void* ctx)
+{
+ yajlpp_parse_context* ypc = (yajlpp_parse_context*) ctx;
+
+ if (!ypc->ypc_handler_stack.empty()
+ && ypc->ypc_handler_stack.back()->jph_obj_deleter)
+ {
+ static thread_local auto md = lnav::pcre2pp::match_data::unitialized();
+
+ auto key_start = ypc->ypc_path_index_stack.back();
+ auto path_frag = string_fragment::from_byte_range(
+ ypc->ypc_path.data(), key_start + 1, ypc->ypc_path.size() - 1);
+ yajlpp_provider_context provider_ctx{&md, static_cast<size_t>(-1)};
+ ypc->ypc_handler_stack.back()
+ ->jph_regex->capture_from(path_frag)
+ .into(md)
+ .matches();
+
+ ypc->ypc_handler_stack.back()->jph_obj_deleter(
+ provider_ctx, ypc->ypc_obj_stack.top());
+ return 1;
+ }
+
+ return handle_unused(ctx);
+}
+
+const yajl_callbacks yajlpp_parse_context::DEFAULT_CALLBACKS = {
+ yajlpp_parse_context::handle_unused_or_delete,
+ (int (*)(void*, int)) yajlpp_parse_context::handle_unused,
+ (int (*)(void*, long long)) yajlpp_parse_context::handle_unused,
+ (int (*)(void*, double)) yajlpp_parse_context::handle_unused,
+ nullptr,
+ (int (*)(void*, const unsigned char*, size_t))
+ yajlpp_parse_context::handle_unused,
+ yajlpp_parse_context::map_start,
+ yajlpp_parse_context::map_key,
+ yajlpp_parse_context::map_end,
+ yajlpp_parse_context::array_start,
+ yajlpp_parse_context::array_end,
+};
+
+yajl_status
+yajlpp_parse_context::parse(const unsigned char* jsonText, size_t jsonTextLen)
+{
+ this->ypc_json_text = jsonText;
+ this->ypc_json_text_len = jsonTextLen;
+
+ yajl_status retval = yajl_parse(this->ypc_handle, jsonText, jsonTextLen);
+
+ size_t consumed = yajl_get_bytes_consumed(this->ypc_handle);
+
+ this->ypc_line_number
+ += std::count(&jsonText[0], &jsonText[consumed], '\n');
+
+ this->ypc_json_text = nullptr;
+
+ if (retval != yajl_status_ok && this->ypc_error_reporter) {
+ auto* msg = yajl_get_error(this->ypc_handle, 1, jsonText, jsonTextLen);
+
+ this->report_error(
+ lnav::console::user_message::error("invalid JSON")
+ .with_snippet(lnav::console::snippet::from(this->ypc_source,
+ (const char*) msg)
+ .with_line(this->get_line_number())));
+ yajl_free_error(this->ypc_handle, msg);
+ }
+
+ return retval;
+}
+
+yajl_status
+yajlpp_parse_context::complete_parse()
+{
+ yajl_status retval = yajl_complete_parse(this->ypc_handle);
+
+ if (retval != yajl_status_ok && this->ypc_error_reporter) {
+ auto* msg = yajl_get_error(this->ypc_handle, 0, nullptr, 0);
+
+ this->report_error(lnav::console::user_message::error("invalid JSON")
+ .with_reason((const char*) msg)
+ .with_snippet(this->get_snippet()));
+ yajl_free_error(this->ypc_handle, msg);
+ }
+
+ return retval;
+}
+
+bool
+yajlpp_parse_context::parse_doc(const string_fragment& sf)
+{
+ bool retval = true;
+
+ this->ypc_json_text = (const unsigned char*) sf.data();
+ this->ypc_json_text_len = sf.length();
+
+ auto rc = yajl_parse(this->ypc_handle, this->ypc_json_text, sf.length());
+ size_t consumed = yajl_get_bytes_consumed(this->ypc_handle);
+ this->ypc_total_consumed += consumed;
+ this->ypc_line_number += std::count(
+ &this->ypc_json_text[0], &this->ypc_json_text[consumed], '\n');
+
+ if (rc != yajl_status_ok) {
+ if (this->ypc_error_reporter) {
+ auto* msg = yajl_get_error(this->ypc_handle,
+ 1,
+ this->ypc_json_text,
+ this->ypc_json_text_len);
+
+ this->report_error(
+ lnav::console::user_message::error("invalid JSON")
+ .with_reason((const char*) msg)
+ .with_snippet(this->get_snippet()));
+ yajl_free_error(this->ypc_handle, msg);
+ }
+ retval = false;
+ } else if (this->complete_parse() != yajl_status_ok) {
+ retval = false;
+ }
+
+ this->ypc_json_text = nullptr;
+
+ return retval;
+}
+
+const intern_string_t
+yajlpp_parse_context::get_path() const
+{
+ if (this->ypc_path.size() <= 1) {
+ return intern_string_t();
+ }
+ return intern_string::lookup(&this->ypc_path[1], this->ypc_path.size() - 2);
+}
+
+const intern_string_t
+yajlpp_parse_context::get_full_path() const
+{
+ if (this->ypc_path.size() <= 1) {
+ static const intern_string_t SLASH = intern_string::lookup("/");
+
+ return SLASH;
+ }
+ return intern_string::lookup(&this->ypc_path[0], this->ypc_path.size() - 1);
+}
+
+void
+yajlpp_parse_context::reset(const struct json_path_container* handlers)
+{
+ this->ypc_handlers = handlers;
+ this->ypc_path.clear();
+ this->ypc_path.push_back('/');
+ this->ypc_path.push_back('\0');
+ this->ypc_path_index_stack.clear();
+ this->ypc_array_index.clear();
+ this->ypc_array_handler_count = 0;
+ this->ypc_callbacks = DEFAULT_CALLBACKS;
+ memset(&this->ypc_alt_callbacks, 0, sizeof(this->ypc_alt_callbacks));
+ this->ypc_sibling_handlers = nullptr;
+ this->ypc_current_handler = nullptr;
+ while (!this->ypc_obj_stack.empty()) {
+ this->ypc_obj_stack.pop();
+ }
+}
+
+void
+yajlpp_parse_context::set_static_handler(const json_path_handler_base& jph)
+{
+ this->ypc_path.clear();
+ this->ypc_path.push_back('/');
+ this->ypc_path.push_back('\0');
+ this->ypc_path_index_stack.clear();
+ this->ypc_array_index.clear();
+ this->ypc_array_handler_count = 0;
+ if (jph.jph_callbacks.yajl_null != nullptr) {
+ this->ypc_callbacks.yajl_null = jph.jph_callbacks.yajl_null;
+ }
+ if (jph.jph_callbacks.yajl_boolean != nullptr) {
+ this->ypc_callbacks.yajl_boolean = jph.jph_callbacks.yajl_boolean;
+ }
+ if (jph.jph_callbacks.yajl_integer != nullptr) {
+ this->ypc_callbacks.yajl_integer = jph.jph_callbacks.yajl_integer;
+ }
+ if (jph.jph_callbacks.yajl_number != nullptr) {
+ this->ypc_callbacks.yajl_number = jph.jph_callbacks.yajl_number;
+ } else {
+ this->ypc_callbacks.yajl_number = nullptr;
+ }
+ if (jph.jph_callbacks.yajl_double != nullptr) {
+ this->ypc_callbacks.yajl_double = jph.jph_callbacks.yajl_double;
+ }
+ if (jph.jph_callbacks.yajl_string != nullptr) {
+ this->ypc_callbacks.yajl_string = jph.jph_callbacks.yajl_string;
+ }
+}
+
+yajlpp_parse_context&
+yajlpp_parse_context::set_path(const std::string& path)
+{
+ this->ypc_path.resize(path.size() + 1);
+ std::copy(path.begin(), path.end(), this->ypc_path.begin());
+ this->ypc_path[path.size()] = '\0';
+ for (size_t lpc = 0; lpc < path.size(); lpc++) {
+ switch (path[lpc]) {
+ case '/':
+ this->ypc_path_index_stack.push_back(
+ this->ypc_path_index_stack.empty() ? 1 : 0 + lpc);
+ break;
+ }
+ }
+ return *this;
+}
+
+const char*
+yajlpp_parse_context::get_path_fragment(int offset,
+ char* frag_in,
+ size_t& len_out) const
+{
+ const char* retval;
+ size_t start, end;
+
+ if (offset < 0) {
+ offset = ((int) this->ypc_path_index_stack.size()) + offset;
+ }
+ start = this->ypc_path_index_stack[offset] + ((offset == 0) ? 0 : 1);
+ if ((offset + 1) < (int) this->ypc_path_index_stack.size()) {
+ end = this->ypc_path_index_stack[offset + 1];
+ } else {
+ end = this->ypc_path.size() - 1;
+ }
+ if (this->ypc_handlers) {
+ len_out
+ = json_ptr::decode(frag_in, &this->ypc_path[start], end - start);
+ retval = frag_in;
+ } else {
+ retval = &this->ypc_path[start];
+ len_out = end - start;
+ }
+
+ return retval;
+}
+
+int
+yajlpp_parse_context::get_line_number() const
+{
+ if (this->ypc_handle != nullptr && this->ypc_json_text) {
+ size_t consumed = yajl_get_bytes_consumed(this->ypc_handle);
+ long current_count = std::count(
+ &this->ypc_json_text[0], &this->ypc_json_text[consumed], '\n');
+
+ return this->ypc_line_number + current_count;
+ }
+ return this->ypc_line_number;
+}
+
+void
+yajlpp_gen_context::gen()
+{
+ yajlpp_map root(this->ygc_handle);
+
+ for (const auto& jph : this->ygc_handlers->jpc_children) {
+ jph.gen(*this, this->ygc_handle);
+ }
+}
+
+void
+yajlpp_gen_context::gen_schema(const json_path_container* handlers)
+{
+ if (handlers == nullptr) {
+ handlers = this->ygc_handlers;
+ }
+
+ {
+ yajlpp_map schema(this->ygc_handle);
+
+ if (!handlers->jpc_schema_id.empty()) {
+ schema.gen("$id");
+ schema.gen(handlers->jpc_schema_id);
+ schema.gen("title");
+ schema.gen(handlers->jpc_schema_id);
+ }
+ schema.gen("$schema");
+ schema.gen("http://json-schema.org/draft-07/schema#");
+ if (!handlers->jpc_description.empty()) {
+ schema.gen("description");
+ schema.gen(handlers->jpc_description);
+ }
+ handlers->gen_schema(*this);
+
+ if (!this->ygc_schema_definitions.empty()) {
+ schema.gen("definitions");
+
+ yajlpp_map defs(this->ygc_handle);
+ for (auto& container : this->ygc_schema_definitions) {
+ defs.gen(container.first);
+
+ yajlpp_map def(this->ygc_handle);
+
+ def.gen("title");
+ def.gen(container.first);
+ if (!container.second->jpc_description.empty()) {
+ def.gen("description");
+ def.gen(container.second->jpc_description);
+ }
+ def.gen("type");
+ def.gen("object");
+ def.gen("$$target");
+ def.gen(fmt::format(FMT_STRING("#/definitions/{}"),
+ container.first));
+ container.second->gen_properties(*this);
+ }
+ }
+ }
+}
+
+yajlpp_gen_context&
+yajlpp_gen_context::with_context(yajlpp_parse_context& ypc)
+{
+ this->ygc_obj_stack = ypc.ypc_obj_stack;
+ if (ypc.ypc_current_handler == nullptr && !ypc.ypc_handler_stack.empty()
+ && ypc.ypc_handler_stack.back() != nullptr)
+ {
+ this->ygc_handlers = ypc.ypc_handler_stack.back()->jph_children;
+ this->ygc_depth += 1;
+ }
+ return *this;
+}
+
+json_path_handler&
+json_path_handler::with_children(const json_path_container& container)
+{
+ this->jph_children = &container;
+ return *this;
+}
+
+lnav::console::snippet
+yajlpp_parse_context::get_snippet() const
+{
+ auto line_number = this->get_line_number();
+ attr_line_t content;
+
+ if (this->ypc_json_text != nullptr) {
+ auto in_text_line = line_number - this->ypc_line_number;
+ const auto* line_start = this->ypc_json_text;
+ auto text_len_remaining = this->ypc_json_text_len;
+
+ while (in_text_line > 0) {
+ const auto* line_end = (const unsigned char*) memchr(
+ line_start, '\n', text_len_remaining);
+ if (line_end == nullptr) {
+ break;
+ }
+
+ text_len_remaining -= (line_end - line_start) + 1;
+ line_start = line_end + 1;
+ in_text_line -= 1;
+ }
+
+ if (text_len_remaining > 0) {
+ const auto* line_end = (const unsigned char*) memchr(
+ line_start, '\n', text_len_remaining);
+ if (line_end) {
+ text_len_remaining = (line_end - line_start);
+ }
+ content.append(
+ string_fragment::from_bytes(line_start, text_len_remaining));
+ }
+ }
+
+ content.with_attr_for_all(VC_ROLE.value(role_t::VCR_QUOTED_CODE));
+ return lnav::console::snippet::from(this->ypc_source, content)
+ .with_line(line_number);
+}
+
+void
+json_path_handler_base::validate_string(yajlpp_parse_context& ypc,
+ string_fragment sf) const
+{
+ if (this->jph_pattern) {
+ if (!this->jph_pattern->find_in(sf).ignore_error()) {
+ this->report_pattern_error(&ypc, sf.to_string());
+ }
+ }
+ if (sf.empty() && this->jph_min_length > 0) {
+ ypc.report_error(lnav::console::user_message::error(
+ attr_line_t("invalid value for option ")
+ .append_quoted(lnav::roles::symbol(
+ ypc.get_full_path().to_string())))
+ .with_reason("empty values are not allowed")
+ .with_snippet(ypc.get_snippet())
+ .with_help(this->get_help_text(&ypc)));
+ } else if (sf.length() < this->jph_min_length) {
+ ypc.report_error(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(sf)
+ .append(" is not a valid value for option ")
+ .append_quoted(
+ lnav::roles::symbol(ypc.get_full_path().to_string())))
+ .with_reason(attr_line_t("value must be at least ")
+ .append(lnav::roles::number(
+ fmt::to_string(this->jph_min_length)))
+ .append(" characters long"))
+ .with_snippet(ypc.get_snippet())
+ .with_help(this->get_help_text(&ypc)));
+ }
+}
+
+void
+json_path_handler_base::report_pattern_error(yajlpp_parse_context* ypc,
+ const std::string& value_str) const
+{
+ ypc->report_error(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(value_str)
+ .append(" is not a valid value for option ")
+ .append_quoted(
+ lnav::roles::symbol(ypc->get_full_path().to_string())))
+ .with_snippet(ypc->get_snippet())
+ .with_reason(attr_line_t("value does not match pattern: ")
+ .append(lnav::roles::symbol(this->jph_pattern_re)))
+ .with_help(this->get_help_text(ypc)));
+}
+
+attr_line_t
+json_path_handler_base::get_help_text(const std::string& full_path) const
+{
+ attr_line_t retval;
+
+ retval.append(lnav::roles::h2("Property Synopsis"))
+ .append("\n ")
+ .append(lnav::roles::symbol(full_path))
+ .append(" ")
+ .append(lnav::roles::variable(this->jph_synopsis))
+ .append("\n")
+ .append(lnav::roles::h2("Description"))
+ .append("\n ")
+ .append(this->jph_description)
+ .append("\n");
+
+ if (this->jph_enum_values != nullptr) {
+ retval.append(lnav::roles::h2("Allowed Values")).append("\n ");
+
+ for (int lpc = 0; this->jph_enum_values[lpc].first; lpc++) {
+ const auto& ev = this->jph_enum_values[lpc];
+
+ retval.append(lpc == 0 ? "" : ", ")
+ .append(lnav::roles::symbol(ev.first));
+ }
+ }
+
+ if (!this->jph_examples.empty()) {
+ retval
+ .append(lnav::roles::h2(
+ this->jph_examples.size() == 1 ? "Example" : "Examples"))
+ .append("\n");
+
+ for (const auto& ex : this->jph_examples) {
+ retval.appendf(FMT_STRING(" {}\n"), ex);
+ }
+ }
+
+ return retval;
+}
+
+attr_line_t
+json_path_handler_base::get_help_text(yajlpp_parse_context* ypc) const
+{
+ return this->get_help_text(ypc->get_full_path().to_string());
+}
+
+void
+json_path_handler_base::report_min_value_error(yajlpp_parse_context* ypc,
+ long long value) const
+{
+ ypc->report_error(
+ lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(fmt::to_string(value))
+ .append(" is not a valid value for option ")
+ .append_quoted(
+ lnav::roles::symbol(ypc->get_full_path().to_string())))
+ .with_reason(attr_line_t("value must be greater than or equal to ")
+ .append(lnav::roles::number(
+ fmt::to_string(this->jph_min_value))))
+ .with_snippet(ypc->get_snippet())
+ .with_help(this->get_help_text(ypc)));
+}
+
+void
+json_path_handler_base::report_duration_error(
+ yajlpp_parse_context* ypc,
+ const std::string& value_str,
+ const relative_time::parse_error& pe) const
+{
+ ypc->report_error(lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(value_str)
+ .append(" is not a valid duration value "
+ "for option ")
+ .append_quoted(lnav::roles::symbol(
+ ypc->get_full_path().to_string())))
+ .with_snippet(ypc->get_snippet())
+ .with_reason(pe.pe_msg)
+ .with_help(this->get_help_text(ypc)));
+}
+
+void
+json_path_handler_base::report_enum_error(yajlpp_parse_context* ypc,
+ const std::string& value_str) const
+{
+ ypc->report_error(lnav::console::user_message::error(
+ attr_line_t()
+ .append_quoted(value_str)
+ .append(" is not a valid value for option ")
+ .append_quoted(lnav::roles::symbol(
+ ypc->get_full_path().to_string())))
+ .with_snippet(ypc->get_snippet())
+ .with_help(this->get_help_text(ypc)));
+}
+
+void
+json_path_handler_base::report_error(yajlpp_parse_context* ypc,
+ const std::string& value,
+ lnav::console::user_message um) const
+{
+ ypc->report_error(um.with_snippet(ypc->get_snippet())
+ .with_help(this->get_help_text(ypc)));
+}
+
+void
+json_path_container::gen_schema(yajlpp_gen_context& ygc) const
+{
+ if (!this->jpc_definition_id.empty()) {
+ ygc.ygc_schema_definitions[this->jpc_definition_id] = this;
+
+ yajl_gen_string(ygc.ygc_handle, "$ref");
+ yajl_gen_string(ygc.ygc_handle,
+ fmt::format(FMT_STRING("#/definitions/{}"),
+ this->jpc_definition_id));
+ return;
+ }
+
+ this->gen_properties(ygc);
+}
+
+void
+json_path_container::gen_properties(yajlpp_gen_context& ygc) const
+{
+ auto pattern_count = count_if(
+ this->jpc_children.begin(), this->jpc_children.end(), [](auto& jph) {
+ return jph.jph_is_pattern_property;
+ });
+ auto plain_count = this->jpc_children.size() - pattern_count;
+
+ if (plain_count > 0) {
+ yajl_gen_string(ygc.ygc_handle, "properties");
+ {
+ yajlpp_map properties(ygc.ygc_handle);
+
+ for (const auto& child_handler : this->jpc_children) {
+ if (child_handler.jph_is_pattern_property) {
+ continue;
+ }
+ properties.gen(child_handler.jph_property);
+ child_handler.gen_schema(ygc);
+ }
+ }
+ }
+ if (pattern_count > 0) {
+ yajl_gen_string(ygc.ygc_handle, "patternProperties");
+ {
+ yajlpp_map properties(ygc.ygc_handle);
+
+ for (const auto& child_handler : this->jpc_children) {
+ if (!child_handler.jph_is_pattern_property) {
+ continue;
+ }
+ properties.gen(child_handler.jph_property);
+ child_handler.gen_schema(ygc);
+ }
+ }
+ }
+
+ yajl_gen_string(ygc.ygc_handle, "additionalProperties");
+ yajl_gen_bool(ygc.ygc_handle, false);
+}
+
+static void
+schema_printer(FILE* file, const char* str, size_t len)
+{
+ fwrite(str, len, 1, file);
+}
+
+void
+dump_schema_to(const json_path_container& jpc, const char* internals_dir)
+{
+ yajlpp_gen genner;
+ yajlpp_gen_context ygc(genner, jpc);
+ auto internals_dir_path = ghc::filesystem::path(internals_dir);
+ auto schema_file_name = ghc::filesystem::path(jpc.jpc_schema_id).filename();
+ auto schema_path = internals_dir_path / schema_file_name;
+ auto file = std::unique_ptr<FILE, decltype(&fclose)>(
+ fopen(schema_path.c_str(), "w+"), fclose);
+
+ if (!file.get()) {
+ return;
+ }
+
+ yajl_gen_config(genner, yajl_gen_beautify, true);
+ yajl_gen_config(
+ genner, yajl_gen_print_callback, schema_printer, file.get());
+
+ ygc.gen_schema();
+}
+
+string_fragment
+yajlpp_gen::to_string_fragment()
+{
+ const unsigned char* buf;
+ size_t len;
+
+ yajl_gen_get_buf(this->yg_handle.in(), &buf, &len);
+
+ return string_fragment::from_bytes(buf, len);
+}
diff --git a/src/yajlpp/yajlpp.hh b/src/yajlpp/yajlpp.hh
new file mode 100644
index 0000000..7632329
--- /dev/null
+++ b/src/yajlpp/yajlpp.hh
@@ -0,0 +1,682 @@
+/**
+ * Copyright (c) 2013-2019, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file yajlpp.hh
+ */
+
+#ifndef yajlpp_hh
+#define yajlpp_hh
+
+#include <functional>
+#include <map>
+#include <memory>
+#include <set>
+#include <stack>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include <limits.h>
+#include <stdarg.h>
+#include <string.h>
+
+#include "base/file_range.hh"
+#include "base/intern_string.hh"
+#include "base/lnav.console.hh"
+#include "base/lnav.console.into.hh"
+#include "base/lnav_log.hh"
+#include "base/opt_util.hh"
+#include "json_ptr.hh"
+#include "optional.hpp"
+#include "pcrepp/pcre2pp.hh"
+#include "relative_time.hh"
+#include "yajl/api/yajl_gen.h"
+#include "yajl/api/yajl_parse.h"
+
+inline yajl_gen_status
+yajl_gen_pstring(yajl_gen hand, const char* str, size_t len)
+{
+ if (len == (size_t) -1) {
+ len = strlen(str);
+ }
+ return yajl_gen_string(hand, (const unsigned char*) str, len);
+}
+
+inline yajl_gen_status
+yajl_gen_string(yajl_gen hand, const std::string& str)
+{
+ return yajl_gen_string(
+ hand, (const unsigned char*) str.c_str(), str.length());
+}
+
+yajl_gen_status yajl_gen_tree(yajl_gen hand, yajl_val val);
+
+void yajl_cleanup_tree(yajl_val val);
+
+template<typename T>
+struct positioned_property {
+ intern_string_t pp_path;
+ source_location pp_location;
+ T pp_value;
+
+ lnav::console::snippet to_snippet() const
+ {
+ return lnav::console::snippet::from(this->pp_location, "");
+ }
+};
+
+template<typename T, typename... Types>
+struct factory_container : public positioned_property<std::shared_ptr<T>> {
+ template<Types... DefaultArgs>
+ struct with_default_args : public positioned_property<std::shared_ptr<T>> {
+ template<typename... Args>
+ static Result<with_default_args, lnav::console::user_message> from(
+ intern_string_t src, source_location loc, Args... args)
+ {
+ auto from_res = T::from(args..., DefaultArgs...);
+
+ if (from_res.isOk()) {
+ with_default_args retval;
+
+ retval.pp_path = src;
+ retval.pp_location = loc;
+ retval.pp_value = from_res.unwrap().to_shared();
+ return Ok(retval);
+ }
+
+ return Err(
+ lnav::console::to_user_message(src, from_res.unwrapErr()));
+ }
+ };
+
+ template<typename... Args>
+ static Result<factory_container, lnav::console::user_message> from(
+ intern_string_t src, source_location loc, Args... args)
+ {
+ auto from_res = T::from(args...);
+
+ if (from_res.isOk()) {
+ factory_container retval;
+
+ retval.pp_path = src;
+ retval.pp_location = loc;
+ retval.pp_value = from_res.unwrap().to_shared();
+ return Ok(retval);
+ }
+
+ return Err(lnav::console::to_user_message(src, from_res.unwrapErr()));
+ }
+};
+
+class yajlpp_gen_context;
+class yajlpp_parse_context;
+
+struct yajlpp_provider_context {
+ lnav::pcre2pp::match_data* ypc_extractor;
+ size_t ypc_index{0};
+ yajlpp_parse_context* ypc_parse_context;
+
+ static constexpr size_t nindex = static_cast<size_t>(-1);
+
+ template<typename T>
+ intern_string_t get_substr_i(T&& name) const
+ {
+ auto cap = (*this->ypc_extractor)[std::forward<T>(name)].value();
+ char path[cap.length() + 1];
+ size_t len = json_ptr::decode(path, cap.data(), cap.length());
+
+ return intern_string::lookup(path, len);
+ }
+
+ template<typename T>
+ std::string get_substr(T&& name) const
+ {
+ auto cap = (*this->ypc_extractor)[std::forward<T>(name)].value();
+ char path[cap.length() + 1];
+ size_t len = json_ptr::decode(path, cap.data(), cap.length());
+
+ return {path, len};
+ }
+};
+
+class yajlpp_error : public std::exception {
+public:
+ yajlpp_error(yajl_handle handle, const char* json, size_t len) noexcept
+ {
+ auto_mem<unsigned char> yajl_msg;
+
+ yajl_msg = yajl_get_error(
+ handle, 1, reinterpret_cast<const unsigned char*>(json), len);
+ this->ye_msg = reinterpret_cast<const char*>(yajl_msg.in());
+ }
+
+ const char* what() const noexcept override { return this->ye_msg.c_str(); }
+
+private:
+ std::string ye_msg;
+};
+
+struct json_path_container;
+
+struct json_path_handler_base {
+ struct enum_value_t {
+ template<typename T>
+ enum_value_t(const char* name, T value)
+ : first(name), second((unsigned int) value)
+ {
+ }
+
+ const char* first;
+ int second;
+ };
+
+ static const enum_value_t ENUM_TERMINATOR;
+
+ explicit json_path_handler_base(const std::string& property);
+
+ explicit json_path_handler_base(
+ const std::shared_ptr<const lnav::pcre2pp::code>& property_re);
+
+ json_path_handler_base(
+ std::string property,
+ const std::shared_ptr<const lnav::pcre2pp::code>& property_re);
+
+ bool is_array() const { return this->jph_is_array; }
+
+ nonstd::optional<int> to_enum_value(const string_fragment& sf) const;
+ const char* to_enum_string(int value) const;
+
+ template<typename T>
+ std::enable_if_t<!detail::is_optional<T>::value, const char*>
+ to_enum_string(T value) const
+ {
+ return this->to_enum_string((int) value);
+ }
+
+ template<typename T>
+ std::enable_if_t<detail::is_optional<T>::value, const char*> to_enum_string(
+ T value) const
+ {
+ return this->to_enum_string((int) value.value());
+ }
+
+ yajl_gen_status gen(yajlpp_gen_context& ygc, yajl_gen handle) const;
+ yajl_gen_status gen_schema(yajlpp_gen_context& ygc) const;
+ yajl_gen_status gen_schema_type(yajlpp_gen_context& ygc) const;
+ void walk(
+ const std::function<
+ void(const json_path_handler_base&, const std::string&, void*)>& cb,
+ void* root = nullptr,
+ const std::string& base = "/") const;
+
+ enum class schema_type_t : std::uint32_t {
+ ANY,
+ BOOLEAN,
+ INTEGER,
+ NUMBER,
+ STRING,
+ ARRAY,
+ OBJECT,
+ };
+
+ std::vector<schema_type_t> get_types() const;
+
+ std::string jph_property;
+ std::shared_ptr<const lnav::pcre2pp::code> jph_regex;
+ yajl_callbacks jph_callbacks{};
+ std::function<yajl_gen_status(
+ yajlpp_gen_context&, const json_path_handler_base&, yajl_gen)>
+ jph_gen_callback;
+ std::function<void(yajlpp_parse_context& ypc,
+ const json_path_handler_base& jph)>
+ jph_validator;
+ std::function<void*(void* root, nonstd::optional<std::string> name)>
+ jph_field_getter;
+ std::function<void*(const yajlpp_provider_context& pe, void* root)>
+ jph_obj_provider;
+ std::function<void(void* root, std::vector<std::string>& paths_out)>
+ jph_path_provider;
+ std::function<void(const yajlpp_provider_context& pe, void* root)>
+ jph_obj_deleter;
+ std::function<size_t(void* root)> jph_size_provider;
+ const char* jph_synopsis{""};
+ const char* jph_description{""};
+ const json_path_container* jph_children{nullptr};
+ std::shared_ptr<const lnav::pcre2pp::code> jph_pattern;
+ const char* jph_pattern_re{nullptr};
+ std::function<void(const string_fragment&)> jph_string_validator;
+ size_t jph_min_length{0};
+ size_t jph_max_length{INT_MAX};
+ const enum_value_t* jph_enum_values{nullptr};
+ long long jph_min_value{LLONG_MIN};
+ bool jph_optional_wrapper{false};
+ bool jph_is_array;
+ bool jph_is_pattern_property{false};
+ std::vector<std::string> jph_examples;
+
+ std::function<int(yajlpp_parse_context*)> jph_null_cb;
+ std::function<int(yajlpp_parse_context*, int)> jph_bool_cb;
+ std::function<int(yajlpp_parse_context*, long long)> jph_integer_cb;
+ std::function<int(yajlpp_parse_context*, double)> jph_double_cb;
+ std::function<int(
+ yajlpp_parse_context*, const unsigned char* str, size_t len)>
+ jph_str_cb;
+
+ void validate_string(yajlpp_parse_context& ypc, string_fragment sf) const;
+
+ void report_pattern_error(yajlpp_parse_context* ypc,
+ const std::string& value_str) const;
+ void report_min_value_error(yajlpp_parse_context* ypc,
+ long long value) const;
+ void report_duration_error(yajlpp_parse_context* ypc,
+ const std::string& value_str,
+ const relative_time::parse_error& pe) const;
+ void report_enum_error(yajlpp_parse_context* ypc,
+ const std::string& value_str) const;
+ void report_error(yajlpp_parse_context* ypc,
+ const std::string& value_str,
+ lnav::console::user_message um) const;
+
+ attr_line_t get_help_text(const std::string& full_path) const;
+ attr_line_t get_help_text(yajlpp_parse_context* ypc) const;
+};
+
+struct json_path_handler;
+
+class yajlpp_parse_context {
+public:
+ using error_reporter_t = std::function<void(
+ const yajlpp_parse_context& ypc, const lnav::console::user_message&)>;
+
+ yajlpp_parse_context(intern_string_t source,
+ const struct json_path_container* handlers = nullptr);
+
+ const char* get_path_fragment(int offset,
+ char* frag_in,
+ size_t& len_out) const;
+
+ intern_string_t get_path_fragment_i(int offset) const
+ {
+ char fragbuf[this->ypc_path.size()];
+ const char* frag;
+ size_t len;
+
+ frag = this->get_path_fragment(offset, fragbuf, len);
+ return intern_string::lookup(frag, len);
+ }
+
+ std::string get_path_fragment(int offset) const
+ {
+ char fragbuf[this->ypc_path.size()];
+ const char* frag;
+ size_t len;
+
+ frag = this->get_path_fragment(offset, fragbuf, len);
+ return std::string(frag, len);
+ }
+
+ const intern_string_t get_path() const;
+
+ const intern_string_t get_full_path() const;
+
+ bool is_level(size_t level) const
+ {
+ return this->ypc_path_index_stack.size() == level;
+ }
+
+ yajlpp_parse_context& set_path(const std::string& path);
+
+ void reset(const struct json_path_container* handlers);
+
+ void set_static_handler(const struct json_path_handler_base& jph);
+
+ template<typename T>
+ yajlpp_parse_context& with_obj(T& obj)
+ {
+ this->ypc_obj_stack.push(&obj);
+ return *this;
+ }
+
+ yajlpp_parse_context& with_handle(yajl_handle handle)
+ {
+ this->ypc_handle = handle;
+ return *this;
+ }
+
+ yajlpp_parse_context& with_error_reporter(error_reporter_t err)
+ {
+ this->ypc_error_reporter = err;
+ return *this;
+ }
+
+ yajlpp_parse_context& with_ignore_unused(bool ignore)
+ {
+ this->ypc_ignore_unused = ignore;
+ return *this;
+ }
+
+ yajl_status parse(const unsigned char* jsonText, size_t jsonTextLen);
+
+ yajl_status parse(const string_fragment& sf)
+ {
+ return this->parse((const unsigned char*) sf.data(), sf.length());
+ }
+
+ int get_line_number() const;
+
+ yajl_status complete_parse();
+
+ bool parse_doc(const string_fragment& sf);
+
+ void report_error(const lnav::console::user_message& msg) const
+ {
+ if (this->ypc_error_reporter) {
+ this->ypc_error_reporter(*this, msg);
+ }
+ }
+
+ lnav::console::snippet get_snippet() const;
+
+ template<typename T>
+ std::vector<T>& get_lvalue(std::map<std::string, std::vector<T>>& value)
+ {
+ return value[this->get_path_fragment(-2)];
+ }
+
+ template<typename T>
+ T& get_lvalue(std::map<std::string, T>& value)
+ {
+ return value[this->get_path_fragment(-1)];
+ }
+
+ template<typename T>
+ T& get_lvalue(T& lvalue)
+ {
+ return lvalue;
+ }
+
+ template<typename T>
+ T& get_rvalue(std::map<std::string, std::vector<T>>& value)
+ {
+ return value[this->get_path_fragment(-2)].back();
+ }
+
+ template<typename T>
+ T& get_rvalue(std::map<std::string, T>& value)
+ {
+ return value[this->get_path_fragment(-1)];
+ }
+
+ template<typename T>
+ T& get_rvalue(std::vector<T>& value)
+ {
+ return value.back();
+ }
+
+ template<typename T>
+ T& get_rvalue(T& lvalue)
+ {
+ return lvalue;
+ }
+
+ template<typename T, typename MEM_T, MEM_T T::*MEM>
+ auto& get_obj_member()
+ {
+ auto obj = (T*) this->ypc_obj_stack.top();
+
+ return obj->*MEM;
+ }
+
+ const intern_string_t ypc_source;
+ int ypc_line_number{1};
+ const struct json_path_container* ypc_handlers;
+ std::stack<void*> ypc_obj_stack;
+ void* ypc_userdata{nullptr};
+ yajl_handle ypc_handle{nullptr};
+ const unsigned char* ypc_json_text{nullptr};
+ size_t ypc_json_text_len{0};
+ size_t ypc_total_consumed{0};
+ yajl_callbacks ypc_callbacks;
+ yajl_callbacks ypc_alt_callbacks;
+ std::vector<char> ypc_path;
+ std::vector<size_t> ypc_path_index_stack;
+ std::vector<size_t> ypc_array_index;
+ std::vector<const json_path_handler_base*> ypc_handler_stack;
+ size_t ypc_array_handler_count{0};
+ bool ypc_ignore_unused{false};
+ const struct json_path_container* ypc_sibling_handlers{nullptr};
+ const struct json_path_handler_base* ypc_current_handler{nullptr};
+ std::set<std::string> ypc_active_paths;
+ error_reporter_t ypc_error_reporter{nullptr};
+ std::map<intern_string_t, source_location>* ypc_locations{nullptr};
+
+ void update_callbacks(const json_path_container* handlers = nullptr,
+ int child_start = 0);
+
+private:
+ static const yajl_callbacks DEFAULT_CALLBACKS;
+
+ static int map_start(void* ctx);
+ static int map_key(void* ctx, const unsigned char* key, size_t len);
+ static int map_end(void* ctx);
+ static int array_start(void* ctx);
+ static int array_end(void* ctx);
+ static int handle_unused(void* ctx);
+ static int handle_unused_or_delete(void* ctx);
+};
+
+class yajlpp_generator {
+public:
+ yajlpp_generator(yajl_gen handle) : yg_handle(handle) {}
+
+ yajl_gen_status operator()(const std::string& str)
+ {
+ return yajl_gen_string(this->yg_handle, str);
+ }
+
+ yajl_gen_status operator()(const char* str)
+ {
+ return yajl_gen_string(
+ this->yg_handle, (const unsigned char*) str, strlen(str));
+ }
+
+ yajl_gen_status operator()(const char* str, size_t len)
+ {
+ return yajl_gen_string(
+ this->yg_handle, (const unsigned char*) str, len);
+ }
+
+ yajl_gen_status operator()(const intern_string_t& str)
+ {
+ return yajl_gen_string(
+ this->yg_handle, (const unsigned char*) str.get(), str.size());
+ }
+
+ yajl_gen_status operator()(const string_fragment& str)
+ {
+ return yajl_gen_string(
+ this->yg_handle, (const unsigned char*) str.data(), str.length());
+ }
+
+ yajl_gen_status operator()(bool value)
+ {
+ return yajl_gen_bool(this->yg_handle, value);
+ }
+
+ yajl_gen_status operator()(double value)
+ {
+ return yajl_gen_double(this->yg_handle, value);
+ }
+
+ template<typename T>
+ yajl_gen_status operator()(
+ T value,
+ typename std::enable_if<std::is_integral<T>::value
+ && !std::is_same<T, bool>::value>::type* dummy
+ = 0)
+ {
+ return yajl_gen_integer(this->yg_handle, value);
+ }
+
+ template<typename T>
+ yajl_gen_status operator()(nonstd::optional<T> value)
+ {
+ if (!value.has_value()) {
+ return yajl_gen_status_ok;
+ }
+
+ return (*this)(value.value());
+ }
+
+ template<typename T>
+ yajl_gen_status operator()(
+ const T& container,
+ typename std::enable_if<!std::is_integral<T>::value>::type* dummy = 0)
+ {
+ yajl_gen_array_open(this->yg_handle);
+ for (const auto& elem : container) {
+ yajl_gen_status rc = (*this)(elem);
+
+ if (rc != yajl_gen_status_ok) {
+ return rc;
+ }
+ }
+
+ yajl_gen_array_close(this->yg_handle);
+
+ return yajl_gen_status_ok;
+ }
+
+ yajl_gen_status operator()() { return yajl_gen_null(this->yg_handle); }
+
+private:
+ yajl_gen yg_handle;
+};
+
+class yajlpp_container_base {
+public:
+ yajlpp_container_base(yajl_gen handle) : gen(handle), ycb_handle(handle) {}
+
+ yajlpp_generator gen;
+
+protected:
+ yajl_gen ycb_handle;
+};
+
+class yajlpp_map : public yajlpp_container_base {
+public:
+ yajlpp_map(yajl_gen handle) : yajlpp_container_base(handle)
+ {
+ yajl_gen_map_open(handle);
+ }
+
+ ~yajlpp_map() { yajl_gen_map_close(this->ycb_handle); }
+};
+
+class yajlpp_array : public yajlpp_container_base {
+public:
+ yajlpp_array(yajl_gen handle) : yajlpp_container_base(handle)
+ {
+ yajl_gen_array_open(handle);
+ }
+
+ ~yajlpp_array() { yajl_gen_array_close(this->ycb_handle); }
+};
+
+class yajlpp_gen_context {
+public:
+ yajlpp_gen_context(yajl_gen handle,
+ const struct json_path_container& handlers)
+ : ygc_handle(handle), ygc_depth(0), ygc_handlers(&handlers)
+ {
+ }
+
+ template<typename T>
+ yajlpp_gen_context& with_default_obj(T& obj)
+ {
+ this->ygc_default_stack.push(&obj);
+ return *this;
+ }
+
+ template<typename T>
+ yajlpp_gen_context& with_obj(const T& obj)
+ {
+ this->ygc_obj_stack.push((void*) &obj);
+ return *this;
+ }
+
+ yajlpp_gen_context& with_context(yajlpp_parse_context& ypc);
+
+ void gen();
+
+ void gen_schema(const json_path_container* handlers = nullptr);
+
+ yajl_gen ygc_handle;
+ int ygc_depth;
+ std::stack<void*> ygc_default_stack;
+ std::stack<void*> ygc_obj_stack;
+ std::vector<std::string> ygc_path;
+ const json_path_container* ygc_handlers;
+ std::map<std::string, const json_path_container*> ygc_schema_definitions;
+};
+
+class yajlpp_gen {
+public:
+ yajlpp_gen() : yg_handle(yajl_gen_free)
+ {
+ this->yg_handle = yajl_gen_alloc(nullptr);
+ }
+
+ yajl_gen get_handle() const { return this->yg_handle.in(); }
+
+ operator yajl_gen() { return this->yg_handle.in(); }
+
+ string_fragment to_string_fragment();
+
+private:
+ auto_mem<yajl_gen_t> yg_handle;
+};
+
+struct json_string {
+ explicit json_string(yajl_gen_t* gen)
+ {
+ const unsigned char* buf;
+
+ yajl_gen_get_buf(gen, &buf, &this->js_len);
+
+ this->js_content = (const unsigned char*) malloc(this->js_len);
+ memcpy((void*) this->js_content.in(), buf, this->js_len);
+ }
+
+ auto_mem<const unsigned char> js_content;
+ size_t js_len{0};
+};
+
+void dump_schema_to(const json_path_container& jpc, const char* internals_dir);
+
+#endif
diff --git a/src/yajlpp/yajlpp_def.hh b/src/yajlpp/yajlpp_def.hh
new file mode 100644
index 0000000..22454b1
--- /dev/null
+++ b/src/yajlpp/yajlpp_def.hh
@@ -0,0 +1,1408 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file yajlpp_def.hh
+ */
+
+#ifndef yajlpp_def_hh
+#define yajlpp_def_hh
+
+#include <chrono>
+
+#include "base/date_time_scanner.hh"
+#include "base/time_util.hh"
+#include "config.h"
+#include "mapbox/variant.hpp"
+#include "relative_time.hh"
+#include "yajlpp.hh"
+
+struct json_null_t {
+ bool operator==(const json_null_t& other) const { return true; }
+};
+
+using json_any_t
+ = mapbox::util::variant<json_null_t, bool, int64_t, double, std::string>;
+
+struct json_path_container;
+
+struct json_path_handler : public json_path_handler_base {
+ template<typename P>
+ json_path_handler(P path, int (*null_func)(yajlpp_parse_context*))
+ : json_path_handler_base(path)
+ {
+ this->jph_callbacks.yajl_null = (int (*)(void*)) null_func;
+ }
+
+ template<typename P>
+ json_path_handler(P path, int (*bool_func)(yajlpp_parse_context*, int))
+ : json_path_handler_base(path)
+ {
+ this->jph_callbacks.yajl_boolean = (int (*)(void*, int)) bool_func;
+ }
+
+ template<typename P>
+ json_path_handler(P path, int (*int_func)(yajlpp_parse_context*, long long))
+ : json_path_handler_base(path)
+ {
+ this->jph_callbacks.yajl_integer = (int (*)(void*, long long)) int_func;
+ }
+
+ template<typename P>
+ json_path_handler(P path, int (*double_func)(yajlpp_parse_context*, double))
+ : json_path_handler_base(path)
+ {
+ this->jph_callbacks.yajl_double = (int (*)(void*, double)) double_func;
+ }
+
+ template<typename P>
+ json_path_handler(P path,
+ int (*number_func)(yajlpp_parse_context*,
+ const char* numberVal,
+ size_t numberLen))
+ : json_path_handler_base(path)
+ {
+ this->jph_callbacks.yajl_number
+ = (int (*)(void*, const char*, size_t)) number_func;
+ }
+
+ template<typename P>
+ json_path_handler(P path) : json_path_handler_base(path)
+ {
+ }
+
+ template<typename P>
+ json_path_handler(P path,
+ int (*str_func)(yajlpp_parse_context*,
+ const unsigned char*,
+ size_t))
+ : json_path_handler_base(path)
+ {
+ this->jph_callbacks.yajl_string
+ = (int (*)(void*, const unsigned char*, size_t)) str_func;
+ }
+
+ json_path_handler(const std::string& path,
+ const std::shared_ptr<const lnav::pcre2pp::code>& re)
+ : json_path_handler_base(path, re)
+ {
+ }
+
+ json_path_handler& add_cb(int (*null_func)(yajlpp_parse_context*))
+ {
+ this->jph_callbacks.yajl_null = (int (*)(void*)) null_func;
+ return *this;
+ }
+
+ json_path_handler& add_cb(int (*bool_func)(yajlpp_parse_context*, int))
+ {
+ this->jph_callbacks.yajl_boolean = (int (*)(void*, int)) bool_func;
+ return *this;
+ }
+
+ json_path_handler& add_cb(int (*int_func)(yajlpp_parse_context*, long long))
+ {
+ this->jph_callbacks.yajl_integer = (int (*)(void*, long long)) int_func;
+ return *this;
+ }
+
+ json_path_handler& add_cb(int (*double_func)(yajlpp_parse_context*, double))
+ {
+ this->jph_callbacks.yajl_double = (int (*)(void*, double)) double_func;
+ return *this;
+ }
+
+ json_path_handler& add_cb(int (*number_func)(yajlpp_parse_context*,
+ const char*,
+ size_t))
+ {
+ this->jph_callbacks.yajl_number
+ = (int (*)(void*, const char*, size_t)) number_func;
+ return *this;
+ }
+
+ json_path_handler& add_cb(int (*str_func)(yajlpp_parse_context*,
+ const unsigned char*,
+ size_t))
+ {
+ this->jph_callbacks.yajl_string
+ = (int (*)(void*, const unsigned char*, size_t)) str_func;
+ return *this;
+ }
+
+ json_path_handler& with_synopsis(const char* synopsis)
+ {
+ this->jph_synopsis = synopsis;
+ return *this;
+ }
+
+ json_path_handler& with_description(const char* description)
+ {
+ this->jph_description = description;
+ return *this;
+ }
+
+ json_path_handler& with_min_length(size_t len)
+ {
+ this->jph_min_length = len;
+ return *this;
+ }
+
+ json_path_handler& with_max_length(size_t len)
+ {
+ this->jph_max_length = len;
+ return *this;
+ }
+
+ json_path_handler& with_enum_values(const enum_value_t values[])
+ {
+ this->jph_enum_values = values;
+ return *this;
+ }
+
+ template<typename T, std::size_t N>
+ json_path_handler& with_pattern(const T (&re)[N])
+ {
+ this->jph_pattern_re = re;
+ this->jph_pattern = lnav::pcre2pp::code::from_const(re).to_shared();
+ return *this;
+ }
+
+ json_path_handler& with_min_value(long long val)
+ {
+ this->jph_min_value = val;
+ return *this;
+ }
+
+ template<typename R, typename T>
+ json_path_handler& with_obj_provider(
+ R* (*provider)(const yajlpp_provider_context& pc, T* root))
+ {
+ this->jph_obj_provider
+ = [provider](const yajlpp_provider_context& ypc, void* root) {
+ return (R*) provider(ypc, (T*) root);
+ };
+ return *this;
+ }
+
+ template<typename R>
+ json_path_handler& with_size_provider(size_t (*provider)(const R* root))
+ {
+ this->jph_size_provider
+ = [provider](const void* root) { return provider((R*) root); };
+ return *this;
+ }
+
+ template<typename T>
+ json_path_handler& with_path_provider(
+ void (*provider)(T* root, std::vector<std::string>& paths_out))
+ {
+ this->jph_path_provider
+ = [provider](void* root, std::vector<std::string>& paths_out) {
+ provider((T*) root, paths_out);
+ };
+ return *this;
+ }
+
+ template<typename T>
+ json_path_handler& with_obj_deleter(
+ void (*provider)(const yajlpp_provider_context& pc, T* root))
+ {
+ this->jph_obj_deleter
+ = [provider](const yajlpp_provider_context& ypc, void* root) {
+ provider(ypc, (T*) root);
+ };
+ return *this;
+ }
+
+ static int null_field_cb(yajlpp_parse_context* ypc)
+ {
+ return ypc->ypc_current_handler->jph_null_cb(ypc);
+ }
+
+ static int bool_field_cb(yajlpp_parse_context* ypc, int val)
+ {
+ return ypc->ypc_current_handler->jph_bool_cb(ypc, val);
+ }
+
+ static int str_field_cb2(yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len)
+ {
+ return ypc->ypc_current_handler->jph_str_cb(ypc, str, len);
+ }
+
+ static int int_field_cb(yajlpp_parse_context* ypc, long long val)
+ {
+ return ypc->ypc_current_handler->jph_integer_cb(ypc, val);
+ }
+
+ static int dbl_field_cb(yajlpp_parse_context* ypc, double val)
+ {
+ return ypc->ypc_current_handler->jph_double_cb(ypc, val);
+ }
+
+ template<typename T, typename U>
+ static inline U& get_field(T& input, U(T::*field))
+ {
+ return input.*field;
+ }
+
+ template<typename T, typename U, typename... V>
+ static inline auto get_field(T& input, U(T::*field), V... args)
+ -> decltype(get_field(input.*field, args...))
+ {
+ return get_field(input.*field, args...);
+ }
+
+ template<typename T, typename U, typename... V>
+ static inline auto get_field(void* input, U(T::*field), V... args)
+ -> decltype(get_field(*((T*) input), field, args...))
+ {
+ return get_field(*((T*) input), field, args...);
+ }
+
+ template<typename R, typename T, typename... Args>
+ struct LastIs {
+ static constexpr bool value = LastIs<R, Args...>::value;
+ };
+
+ template<typename R, typename T>
+ struct LastIs<R, T> {
+ static constexpr bool value = false;
+ };
+
+ template<typename R, typename T>
+ struct LastIs<R, R T::*> {
+ static constexpr bool value = true;
+ };
+
+ template<typename T, typename... Args>
+ struct LastIsEnum {
+ using value_type = typename LastIsEnum<Args...>::value_type;
+ static constexpr bool value = LastIsEnum<Args...>::value;
+ };
+
+ template<typename T, typename U>
+ struct LastIsEnum<U T::*> {
+ using value_type = U;
+
+ static constexpr bool value = std::is_enum<U>::value;
+ };
+
+ template<typename T, typename U>
+ struct LastIsEnum<nonstd::optional<U> T::*> {
+ using value_type = U;
+
+ static constexpr bool value = std::is_enum<U>::value;
+ };
+
+ template<typename T, typename... Args>
+ struct LastIsInteger {
+ static constexpr bool value = LastIsInteger<Args...>::value;
+ };
+
+ template<typename T, typename U>
+ struct LastIsInteger<U T::*> {
+ static constexpr bool value
+ = std::is_integral<U>::value && !std::is_same<U, bool>::value;
+ };
+
+ template<typename T, typename U>
+ struct LastIsInteger<nonstd::optional<U> T::*> {
+ static constexpr bool value
+ = std::is_integral<U>::value && !std::is_same<U, bool>::value;
+ };
+
+ template<typename T, typename... Args>
+ struct LastIsFloat {
+ static constexpr bool value = LastIsFloat<Args...>::value;
+ };
+
+ template<typename T, typename U>
+ struct LastIsFloat<U T::*> {
+ static constexpr bool value = std::is_same<U, double>::value;
+ };
+
+ template<typename T, typename U>
+ struct LastIsFloat<nonstd::optional<U> T::*> {
+ static constexpr bool value = std::is_same<U, double>::value;
+ };
+
+ template<typename T, typename... Args>
+ struct LastIsVector {
+ using value_type = typename LastIsVector<Args...>::value_type;
+ static constexpr bool value = LastIsVector<Args...>::value;
+ };
+
+ template<typename T, typename U>
+ struct LastIsVector<std::vector<U> T::*> {
+ using value_type = U;
+ static constexpr bool value = true;
+ };
+
+ template<typename T, typename U>
+ struct LastIsVector<U T::*> {
+ using value_type = void;
+ static constexpr bool value = false;
+ };
+
+ template<typename T>
+ static bool is_field_set(const nonstd::optional<T>& field)
+ {
+ return field.has_value();
+ }
+
+ template<typename T>
+ static bool is_field_set(const T&)
+ {
+ return true;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<LastIs<bool, Args...>::value, bool> = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(bool_field_cb);
+ this->jph_bool_cb = [args...](yajlpp_parse_context* ypc, int val) {
+ auto* obj = ypc->ypc_obj_stack.top();
+
+ json_path_handler::get_field(obj, args...) = static_cast<bool>(val);
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(field);
+ };
+ this->jph_field_getter
+ = [args...](void* root, nonstd::optional<std::string> name) {
+ return (void*) &json_path_handler::get_field(root, args...);
+ };
+
+ return *this;
+ }
+
+ template<
+ typename... Args,
+ std::enable_if_t<LastIs<std::vector<std::string>, Args...>::value, bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto value_str = std::string((const char*) str, len);
+ auto jph = ypc->ypc_current_handler;
+
+ jph->validate_string(*ypc, value_str);
+ json_path_handler::get_field(obj, args...)
+ .emplace_back(std::move(value_str));
+
+ return 1;
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<LastIsVector<Args...>::value, bool> = true,
+ std::enable_if_t<
+ !std::is_same<typename LastIsVector<Args...>::value_type,
+ std::string>::value,
+ bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->jph_obj_provider
+ = [args...](const yajlpp_provider_context& ypc, void* root) {
+ auto& vec = json_path_handler::get_field(root, args...);
+
+ if (ypc.ypc_index >= vec.size()) {
+ vec.resize(ypc.ypc_index + 1);
+ }
+
+ return &vec[ypc.ypc_index];
+ };
+ this->jph_size_provider = [args...](void* root) {
+ auto& vec = json_path_handler::get_field(root, args...);
+
+ return vec.size();
+ };
+
+ return *this;
+ }
+
+ template<typename T, typename U>
+ json_path_handler& for_child(positioned_property<U>(T::*field))
+ {
+ this->jph_obj_provider
+ = [field](const yajlpp_provider_context& ypc, void* root) -> void* {
+ auto& child = json_path_handler::get_field(root, field);
+
+ if (ypc.ypc_parse_context != nullptr && child.pp_path.empty()) {
+ child.pp_path = ypc.ypc_parse_context->get_full_path();
+ }
+ return &child.pp_value;
+ };
+
+ return *this;
+ }
+
+ template<typename... Args>
+ json_path_handler& for_child(Args... args)
+ {
+ this->jph_obj_provider = [args...](const yajlpp_provider_context& ypc,
+ void* root) -> void* {
+ auto& child = json_path_handler::get_field(root, args...);
+
+ return &child;
+ };
+
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<
+ LastIs<std::map<std::string, std::string>, Args...>::value,
+ bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto key = ypc->get_path_fragment(-1);
+
+ json_path_handler::get_field(obj, args...)[key]
+ = std::string((const char*) str, len);
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ {
+ yajlpp_generator gen(handle);
+
+ for (const auto& pair : field) {
+ gen(pair.first);
+ gen(pair.second);
+ }
+ }
+
+ return yajl_gen_status_ok;
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<
+ LastIs<std::map<std::string, nonstd::optional<std::string>>,
+ Args...>::value,
+ bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto key = ypc->get_path_fragment(-1);
+
+ json_path_handler::get_field(obj, args...)[key]
+ = std::string((const char*) str, len);
+
+ return 1;
+ };
+ this->add_cb(null_field_cb);
+ this->jph_null_cb = [args...](yajlpp_parse_context* ypc) {
+ auto* obj = ypc->ypc_obj_stack.top();
+ auto key = ypc->get_path_fragment(-1);
+
+ json_path_handler::get_field(obj, args...)[key] = nonstd::nullopt;
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ {
+ yajlpp_generator gen(handle);
+
+ for (const auto& pair : field) {
+ gen(pair.first);
+ gen(pair.second);
+ }
+ }
+
+ return yajl_gen_status_ok;
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<
+ LastIs<std::map<std::string, json_any_t>, Args...>::value,
+ bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(bool_field_cb);
+ this->jph_bool_cb = [args...](yajlpp_parse_context* ypc, int val) {
+ auto* obj = ypc->ypc_obj_stack.top();
+ auto key = ypc->get_path_fragment(-1);
+
+ json_path_handler::get_field(obj, args...)[key] = val ? true
+ : false;
+
+ return 1;
+ };
+ this->add_cb(int_field_cb);
+ this->jph_integer_cb
+ = [args...](yajlpp_parse_context* ypc, long long val) {
+ auto* obj = ypc->ypc_obj_stack.top();
+ auto key = ypc->get_path_fragment(-1);
+
+ json_path_handler::get_field(obj, args...)[key]
+ = static_cast<int64_t>(val);
+
+ return 1;
+ };
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto* obj = ypc->ypc_obj_stack.top();
+ auto key = ypc->get_path_fragment(-1);
+
+ json_path_handler::get_field(obj, args...)[key]
+ = std::string((const char*) str, len);
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ {
+ yajlpp_generator gen(handle);
+
+ for (const auto& pair : field) {
+ gen(pair.first);
+ pair.second.match([&gen](json_null_t v) { gen(); },
+ [&gen](bool v) { gen(v); },
+ [&gen](int64_t v) { gen(v); },
+ [&gen](double v) { gen(v); },
+ [&gen](const std::string& v) { gen(v); });
+ }
+ }
+
+ return yajl_gen_status_ok;
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<LastIs<std::string, Args...>::value, bool> = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto value_str = std::string((const char*) str, len);
+ auto jph = ypc->ypc_current_handler;
+
+ jph->validate_string(*ypc, value_str);
+ json_path_handler::get_field(obj, args...) = std::move(value_str);
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(field);
+ };
+ this->jph_field_getter
+ = [args...](void* root, nonstd::optional<std::string> name) {
+ return (void*) &json_path_handler::get_field(root, args...);
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<LastIs<timeval, Args...>::value, bool> = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto jph = ypc->ypc_current_handler;
+
+ date_time_scanner dts;
+ timeval tv{};
+ exttm tm;
+
+ if (dts.scan((char*) str, len, nullptr, &tm, tv) == nullptr) {
+ ypc->report_error(
+ lnav::console::user_message::error(
+ attr_line_t("unrecognized timestamp ")
+ .append_quoted(
+ string_fragment::from_bytes(str, len)))
+ .with_snippet(ypc->get_snippet())
+ .with_help(jph->get_help_text(ypc)));
+ } else {
+ json_path_handler::get_field(obj, args...) = tv;
+ }
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+ char buf[64];
+
+ auto buf_len = lnav::strftime_rfc3339(
+ buf, sizeof(buf), field.tv_sec, field.tv_usec, 'T');
+
+ return gen(string_fragment::from_bytes(buf, buf_len));
+ };
+ this->jph_field_getter
+ = [args...](void* root, nonstd::optional<std::string> name) {
+ return (void*) &json_path_handler::get_field(root, args...);
+ };
+ return *this;
+ }
+
+ template<
+ typename... Args,
+ std::enable_if_t<LastIs<nonstd::optional<std::string>, Args...>::value,
+ bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto value_str = std::string((const char*) str, len);
+ auto jph = ypc->ypc_current_handler;
+
+ jph->validate_string(*ypc, value_str);
+ json_path_handler::get_field(obj, args...) = std::move(value_str);
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (!field) {
+ return yajl_gen_status_ok;
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(field.value());
+ };
+ this->jph_field_getter
+ = [args...](void* root, nonstd::optional<std::string> name) {
+ return (void*) &json_path_handler::get_field(root, args...);
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<
+ LastIs<positioned_property<std::string>, Args...>::value,
+ bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto value_str = std::string((const char*) str, len);
+ auto jph = ypc->ypc_current_handler;
+
+ jph->validate_string(*ypc, value_str);
+ auto& field = json_path_handler::get_field(obj, args...);
+
+ field.pp_path = ypc->get_full_path();
+ field.pp_location.sl_source = ypc->ypc_source;
+ field.pp_location.sl_line_number = ypc->get_line_number();
+ field.pp_value = std::move(value_str);
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field.pp_value == field_def.pp_value) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(field.pp_value);
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<LastIs<intern_string_t, Args...>::value, bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto value_str = std::string((const char*) str, len);
+ auto jph = ypc->ypc_current_handler;
+
+ jph->validate_string(*ypc, value_str);
+ json_path_handler::get_field(obj, args...)
+ = intern_string::lookup(value_str);
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(field);
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<
+ LastIs<positioned_property<intern_string_t>, Args...>::value,
+ bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto value_str = std::string((const char*) str, len);
+ auto jph = ypc->ypc_current_handler;
+
+ jph->validate_string(*ypc, value_str);
+ auto& field = json_path_handler::get_field(obj, args...);
+ field.pp_path = ypc->get_full_path();
+ field.pp_location.sl_source = ypc->ypc_source;
+ field.pp_location.sl_line_number = ypc->get_line_number();
+ field.pp_value = intern_string::lookup(value_str);
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field.pp_value == field_def.pp_value) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(field.pp_value);
+ };
+ return *this;
+ }
+
+ template<typename>
+ struct int_ {
+ typedef int type;
+ };
+ template<
+ typename C,
+ typename T,
+ typename int_<decltype(T::from(
+ intern_string_t{}, source_location{}, string_fragment{}))>::type
+ = 0,
+ typename... Args>
+ json_path_handler& for_field(Args... args, T C::*ptr_arg)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args..., ptr_arg](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto* obj = ypc->ypc_obj_stack.top();
+ auto value_frag = string_fragment::from_bytes(str, len);
+ const auto* jph = ypc->ypc_current_handler;
+ auto loc = source_location{ypc->ypc_source, ypc->get_line_number()};
+
+ auto from_res = T::from(ypc->get_full_path(), loc, value_frag);
+ if (from_res.isErr()) {
+ jph->report_error(
+ ypc, value_frag.to_string(), from_res.unwrapErr());
+ } else {
+ json_path_handler::get_field(obj, args..., ptr_arg)
+ = from_res.unwrap();
+ }
+
+ return 1;
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<LastIsInteger<Args...>::value, bool> = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(int_field_cb);
+ this->jph_integer_cb
+ = [args...](yajlpp_parse_context* ypc, long long val) {
+ auto jph = ypc->ypc_current_handler;
+ auto* obj = ypc->ypc_obj_stack.top();
+
+ if (val < jph->jph_min_value) {
+ jph->report_min_value_error(ypc, val);
+ return 1;
+ }
+
+ json_path_handler::get_field(obj, args...) = val;
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (!is_field_set(field)) {
+ return yajl_gen_status_ok;
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(field);
+ };
+ this->jph_field_getter
+ = [args...](void* root, nonstd::optional<std::string> name) {
+ return (void*) &json_path_handler::get_field(root, args...);
+ };
+
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<LastIsFloat<Args...>::value, bool> = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(dbl_field_cb);
+ this->jph_double_cb = [args...](yajlpp_parse_context* ypc, double val) {
+ auto jph = ypc->ypc_current_handler;
+ auto* obj = ypc->ypc_obj_stack.top();
+
+ if (val < jph->jph_min_value) {
+ jph->report_min_value_error(ypc, val);
+ return 1;
+ }
+
+ json_path_handler::get_field(obj, args...) = val;
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (!is_field_set(field)) {
+ return yajl_gen_status_ok;
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(field);
+ };
+ this->jph_field_getter
+ = [args...](void* root, nonstd::optional<std::string> name) {
+ return (void*) &json_path_handler::get_field(root, args...);
+ };
+
+ return *this;
+ }
+
+ template<
+ typename... Args,
+ std::enable_if_t<LastIs<std::chrono::seconds, Args...>::value, bool>
+ = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto handler = ypc->ypc_current_handler;
+ auto parse_res = relative_time::from_str(
+ string_fragment::from_bytes(str, len));
+
+ if (parse_res.isErr()) {
+ auto parse_error = parse_res.unwrapErr();
+ auto value_str = std::string((const char*) str, len);
+
+ handler->report_duration_error(ypc, value_str, parse_error);
+ return 1;
+ }
+
+ json_path_handler::get_field(obj, args...) = std::chrono::seconds(
+ parse_res.template unwrap().to_timeval().tv_sec);
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(relative_time::from_timeval(
+ {static_cast<time_t>(field.count()), 0})
+ .to_string());
+ };
+ this->jph_field_getter
+ = [args...](void* root, nonstd::optional<std::string> name) {
+ return (void*) &json_path_handler::get_field(root, args...);
+ };
+ return *this;
+ }
+
+ template<typename... Args,
+ std::enable_if_t<LastIsEnum<Args...>::value, bool> = true>
+ json_path_handler& for_field(Args... args)
+ {
+ this->add_cb(str_field_cb2);
+ this->jph_str_cb = [args...](yajlpp_parse_context* ypc,
+ const unsigned char* str,
+ size_t len) {
+ auto obj = ypc->ypc_obj_stack.top();
+ auto handler = ypc->ypc_current_handler;
+ auto res = handler->to_enum_value(string_fragment(str, 0, len));
+
+ if (res) {
+ json_path_handler::get_field(obj, args...)
+ = (typename LastIsEnum<Args...>::value_type) res.value();
+ } else {
+ handler->report_enum_error(ypc,
+ std::string((const char*) str, len));
+ }
+
+ return 1;
+ };
+ this->jph_gen_callback = [args...](yajlpp_gen_context& ygc,
+ const json_path_handler_base& jph,
+ yajl_gen handle) {
+ const auto& field = json_path_handler::get_field(
+ ygc.ygc_obj_stack.top(), args...);
+
+ if (!ygc.ygc_default_stack.empty()) {
+ const auto& field_def = json_path_handler::get_field(
+ ygc.ygc_default_stack.top(), args...);
+
+ if (field == field_def) {
+ return yajl_gen_status_ok;
+ }
+ }
+
+ if (!is_field_set(field)) {
+ return yajl_gen_status_ok;
+ }
+
+ if (ygc.ygc_depth) {
+ yajl_gen_string(handle, jph.jph_property);
+ }
+
+ yajlpp_generator gen(handle);
+
+ return gen(jph.to_enum_string(field));
+ };
+ this->jph_field_getter
+ = [args...](void* root, nonstd::optional<std::string> name) {
+ return (void*) &json_path_handler::get_field(root, args...);
+ };
+
+ return *this;
+ }
+
+ json_path_handler& with_children(const json_path_container& container);
+
+ json_path_handler& with_example(const std::string& example)
+ {
+ this->jph_examples.emplace_back(example);
+ return *this;
+ }
+};
+
+struct json_path_container {
+ json_path_container(std::initializer_list<json_path_handler> children)
+ : jpc_children(children)
+ {
+ }
+
+ json_path_container& with_definition_id(const std::string& id)
+ {
+ this->jpc_definition_id = id;
+ return *this;
+ }
+
+ json_path_container& with_schema_id(const std::string& id)
+ {
+ this->jpc_schema_id = id;
+ return *this;
+ }
+
+ json_path_container& with_description(std::string desc)
+ {
+ this->jpc_description = std::move(desc);
+ return *this;
+ }
+
+ void gen_schema(yajlpp_gen_context& ygc) const;
+
+ void gen_properties(yajlpp_gen_context& ygc) const;
+
+ std::string jpc_schema_id;
+ std::string jpc_definition_id;
+ std::string jpc_description;
+ std::vector<json_path_handler> jpc_children;
+};
+
+template<typename T>
+class yajlpp_parser {
+public:
+ yajlpp_parser(intern_string_t src, const json_path_container* container)
+ : yp_parse_context(src, container)
+ {
+ this->yp_handle = yajl_alloc(&this->yp_parse_context.ypc_callbacks,
+ nullptr,
+ &this->yp_parse_context);
+ this->yp_parse_context.with_handle(this->yp_handle);
+ this->yp_parse_context.template with_obj(this->yp_obj);
+ this->yp_parse_context.ypc_userdata = this;
+ this->yp_parse_context.with_error_reporter(
+ [](const auto& ypc, const auto& um) {
+ auto* yp = static_cast<yajlpp_parser<T>*>(ypc.ypc_userdata);
+
+ yp->yp_errors.template emplace_back(um);
+ });
+ }
+
+ yajlpp_parser& with_ignore_unused(bool value)
+ {
+ this->yp_parse_context.with_ignore_unused(value);
+
+ return *this;
+ }
+
+ Result<T, std::vector<lnav::console::user_message>> of(
+ const string_fragment& json)
+ {
+ if (this->yp_parse_context.parse_doc(json)) {
+ return Ok(std::move(this->yp_obj));
+ }
+
+ return Err(std::move(this->yp_errors));
+ }
+
+private:
+ yajlpp_parse_context yp_parse_context;
+ auto_mem<yajl_handle_t> yp_handle{yajl_free};
+ std::vector<lnav::console::user_message> yp_errors;
+ T yp_obj;
+};
+
+template<typename T>
+struct typed_json_path_container : public json_path_container {
+ typed_json_path_container(std::initializer_list<json_path_handler> children)
+ : json_path_container(children)
+ {
+ }
+
+ typed_json_path_container<T>& with_schema_id2(const std::string& id)
+ {
+ this->jpc_schema_id = id;
+ return *this;
+ }
+
+ typed_json_path_container<T>& with_description2(std::string desc)
+ {
+ this->jpc_description = std::move(desc);
+ return *this;
+ }
+
+ yajlpp_parser<T> parser_for(intern_string_t src) const
+ {
+ return yajlpp_parser<T>{src, this};
+ }
+
+ std::string to_string(const T& obj) const
+ {
+ yajlpp_gen gen;
+ yajlpp_gen_context ygc(gen, *this);
+ ygc.template with_obj(obj);
+ ygc.ygc_depth = 1;
+ ygc.gen();
+
+ return gen.to_string_fragment().to_string();
+ }
+
+ json_string to_json_string(const T& obj) const
+ {
+ yajlpp_gen gen;
+ yajlpp_gen_context ygc(gen, *this);
+ ygc.template with_obj(obj);
+ ygc.ygc_depth = 1;
+ ygc.gen();
+
+ return json_string{gen.get_handle()};
+ }
+};
+
+namespace yajlpp {
+inline json_path_handler
+property_handler(const std::string& path)
+{
+ return {path};
+}
+
+template<typename T, std::size_t N>
+inline json_path_handler
+pattern_property_handler(const T (&path)[N])
+{
+ return {lnav::pcre2pp::code::from_const(path, PCRE2_ANCHORED).to_shared()};
+}
+
+} // namespace yajlpp
+
+#endif
diff --git a/src/yaml-extension-functions.cc b/src/yaml-extension-functions.cc
new file mode 100644
index 0000000..0a58652
--- /dev/null
+++ b/src/yaml-extension-functions.cc
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file yaml-extension-functions.cc
+ */
+
+#include <string>
+
+#define RYML_SINGLE_HDR_DEFINE_NOW
+
+#include "ryml_all.hpp"
+#include "sqlite-extension-func.hh"
+#include "vtab_module.hh"
+#include "yajlpp/yajlpp.hh"
+
+using namespace lnav::roles::literals;
+
+static void
+ryml_error_to_um(const char* msg, size_t len, ryml::Location loc, void* ud)
+{
+ intern_string_t src = intern_string::lookup(
+ string_fragment::from_bytes(loc.name.data(), loc.name.size()));
+ auto& sf = *(static_cast<string_fragment*>(ud));
+ auto msg_str = string_fragment::from_bytes(msg, len).trim().to_string();
+
+ if (loc.offset == sf.length()) {
+ loc.line -= 1;
+ }
+ throw lnav::console::user_message::error("failed to parse YAML content")
+ .with_reason(msg_str)
+ .with_snippet(lnav::console::snippet::from(
+ source_location{src, (int32_t) loc.line}, ""));
+}
+
+static text_auto_buffer
+yaml_to_json(string_fragment in)
+{
+ ryml::Callbacks callbacks(&in, nullptr, nullptr, ryml_error_to_um);
+
+ ryml::set_callbacks(callbacks);
+ auto tree = ryml::parse_in_arena(
+ "input", ryml::csubstr{in.data(), (size_t) in.length()});
+
+ auto output = ryml::emit_json(
+ tree, tree.root_id(), ryml::substr{}, /*error_on_excess*/ false);
+ auto buf = auto_buffer::alloc(output.len);
+ buf.resize(output.len);
+ output = ryml::emit_json(tree,
+ tree.root_id(),
+ ryml::substr(buf.in(), buf.size()),
+ /*error_on_excess*/ true);
+
+ return {std::move(buf)};
+}
+
+int
+yaml_extension_functions(struct FuncDef** basic_funcs,
+ struct FuncDefAgg** agg_funcs)
+{
+ static struct FuncDef yaml_funcs[] = {
+ sqlite_func_adapter<decltype(&yaml_to_json), yaml_to_json>::builder(
+ help_text("yaml_to_json",
+ "Convert a YAML document to a JSON-encoded string")
+ .sql_function()
+ .with_parameter({"yaml", "The YAML value to convert to JSON."})
+ .with_tags({"json", "yaml"})
+ .with_example({
+ "To convert the document \"abc: def\"",
+ "SELECT yaml_to_json('abc: def')",
+ })),
+
+ {nullptr},
+ };
+
+ *basic_funcs = yaml_funcs;
+
+ return SQLITE_OK;
+}
diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt
new file mode 100644
index 0000000..df39caa
--- /dev/null
+++ b/test/CMakeLists.txt
@@ -0,0 +1,88 @@
+enable_testing()
+
+include_directories(
+ . ${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src/fmtlib
+ ${CMAKE_CURRENT_BINARY_DIR}/../src ${CMAKE_CURRENT_BINARY_DIR})
+
+add_executable(test_abbrev test_abbrev.cc test_stubs.cc)
+target_link_libraries(test_abbrev diag)
+add_test(NAME test_abbrev COMMAND test_abbrev)
+
+add_executable(test_ansi_scrubber test_ansi_scrubber.cc test_stubs.cc)
+target_link_libraries(test_ansi_scrubber diag)
+add_test(NAME test_ansi_scrubber COMMAND test_ansi_scrubber)
+
+add_executable(test_auto_fd test_auto_fd.cc test_stubs.cc)
+target_link_libraries(test_auto_fd diag)
+add_test(NAME test_auto_fd COMMAND test_auto_fd)
+
+add_executable(test_auto_mem test_auto_mem.cc test_stubs.cc)
+target_link_libraries(test_auto_mem diag)
+add_test(NAME test_auto_mem COMMAND test_auto_mem)
+
+add_executable(test_column_namer test_column_namer.cc test_stubs.cc)
+target_include_directories(test_column_namer PUBLIC ../src/third-party/doctest-root)
+target_link_libraries(test_column_namer diag)
+add_test(NAME test_column_namer COMMAND test_column_namer)
+
+add_executable(document.sections.tests document.sections.tests.cc test_stubs.cc)
+target_include_directories(document.sections.tests PUBLIC ../src/third-party/doctest-root)
+target_link_libraries(document.sections.tests diag)
+add_test(NAME document.sections.tests COMMAND document.sections.tests)
+
+add_executable(test_bookmarks test_bookmarks.cc test_stubs.cc)
+target_link_libraries(test_bookmarks diag)
+add_test(NAME test_bookmarks COMMAND test_bookmarks)
+
+add_executable(test_date_time_scanner test_date_time_scanner.cc)
+target_link_libraries(test_date_time_scanner base lnavdt)
+add_test(NAME test_date_time_scanner COMMAND test_date_time_scanner)
+
+add_executable(test_grep_proc2 test_grep_proc2.cc)
+target_link_libraries(test_grep_proc2 lnavfileio)
+add_test(NAME test_grep_proc2 COMMAND test_grep_proc2)
+
+add_executable(test_line_buffer2 test_line_buffer2.cc)
+target_link_libraries(test_line_buffer2 lnavfileio)
+add_test(NAME test_line_buffer2 COMMAND test_line_buffer2)
+
+add_executable(test_log_accel test_log_accel.cc test_stubs.cc)
+target_link_libraries(test_log_accel diag)
+add_test(NAME test_log_accel COMMAND test_log_accel)
+
+add_executable(lnav_doctests lnav_doctests.cc test_stubs.cc)
+target_include_directories(lnav_doctests PUBLIC ../src/third-party/doctest-root)
+target_link_libraries(lnav_doctests diag ${lnav_LIBS})
+add_test(NAME lnav_doctests COMMAND lnav_doctests)
+
+add_executable(test_reltime test_reltime.cc test_stubs.cc)
+target_include_directories(test_reltime PUBLIC ../src/third-party/doctest-root)
+target_link_libraries(test_reltime diag)
+add_test(NAME test_reltime COMMAND test_reltime)
+
+add_executable(test_top_status test_top_status.cc test_stubs.cc)
+target_link_libraries(test_top_status diag logfmt)
+add_test(NAME test_top_status COMMAND test_top_status)
+
+add_executable(test_text_anonymizer test_text_anonymizer.cc test_stubs.cc)
+target_include_directories(test_text_anonymizer PUBLIC ../src/third-party/doctest-root)
+target_link_libraries(test_text_anonymizer diag)
+add_test(NAME test_text_anonymizer COMMAND test_text_anonymizer)
+
+add_executable(drive_view_colors drive_view_colors.cc test_stubs.cc)
+target_link_libraries(drive_view_colors diag)
+
+add_executable(drive_vt52_curses drive_vt52_curses.cc test_stubs.cc)
+target_link_libraries(drive_vt52_curses diag)
+
+add_executable(drive_logfile drive_logfile.cc test_stubs.cc)
+target_link_libraries(drive_logfile diag)
+
+add_executable(drive_sql_anno drive_sql_anno.cc test_stubs.cc)
+target_link_libraries(drive_sql_anno diag)
+
+add_executable(drive_data_scanner drive_data_scanner.cc test_stubs.cc)
+target_link_libraries(drive_data_scanner diag logfmt)
+
+add_executable(scripty scripty.cc test_stubs.cc)
+target_link_libraries(scripty diag)
diff --git a/test/Makefile.am b/test/Makefile.am
new file mode 100644
index 0000000..a92ceb8
--- /dev/null
+++ b/test/Makefile.am
@@ -0,0 +1,523 @@
+
+include $(top_srcdir)/aminclude_static.am
+
+TESTS_ENVIRONMENT = $(SHELL) $(top_builddir)/TESTS_ENVIRONMENT
+LOG_COMPILER = $(SHELL) $(top_builddir)/TESTS_ENVIRONMENT
+
+RM_V = $(RM_V_@AM_V@)
+RM_V_ = $(RM_V_@AM_DEFAULT_V@)
+RM_V_0 = @echo " RM " $@;
+
+AM_LIBS = $(CODE_COVERAGE_LIBS)
+AM_CFLAGS = $(CODE_COVERAGE_CFLAGS)
+AM_CXXFLAGS = $(CODE_COVERAGE_CXXFLAGS)
+
+AM_CPPFLAGS = \
+ -Wall \
+ -I$(top_srcdir)/src \
+ -I$(top_srcdir)/src/fmtlib \
+ -I$(top_srcdir)/src/third-party \
+ -I$(top_srcdir)/src/third-party/scnlib/include \
+ $(CODE_COVERAGE_CPPFLAGS) \
+ $(LIBARCHIVE_CFLAGS) \
+ $(READLINE_CFLAGS) \
+ $(PCRE_CFLAGS) \
+ $(SQLITE3_CFLAGS)
+
+# AM_CFLAGS = -fprofile-arcs -ftest-coverage
+# AM_CXXFLAGS = -fprofile-arcs -ftest-coverage
+
+remote/ssh_host_rsa_key:
+ mkdir -p remote
+ ssh-keygen -f remote/ssh_host_rsa_key -N '' -t rsa
+
+remote/ssh_host_dsa_key:
+ mkdir -p remote
+ ssh-keygen -f remote/ssh_host_dsa_key -N '' -t dsa
+
+remote/id_rsa:
+ mkdir -p remote
+ ssh-keygen -f remote/id_rsa -N '' -t rsa
+ cp -f remote/id_rsa.pub remote/authorized_keys
+
+noinst_LIBRARIES = \
+ libtestdummy.a
+
+libtestdummy_a_SOURCES = \
+ test_stubs.cc
+
+DUMMY_OBJS = \
+ test_stubs.$(OBJEXT)
+
+check_PROGRAMS = \
+ drive_data_scanner \
+ drive_line_buffer \
+ drive_grep_proc \
+ drive_listview \
+ drive_logfile \
+ drive_mvwattrline \
+ drive_shlexer \
+ drive_sql \
+ drive_sql_anno \
+ drive_view_colors \
+ drive_vt52_curses \
+ drive_readline_curses \
+ lnav_doctests \
+ slicer \
+ scripty \
+ test_abbrev \
+ test_ansi_scrubber \
+ test_auto_fd \
+ test_auto_mem \
+ test_bookmarks \
+ test_date_time_scanner \
+ test_grep_proc2 \
+ test_line_buffer2 \
+ test_log_accel \
+ test_ncurses_unicode \
+ test_reltime \
+ test_text_anonymizer \
+ test_top_status
+
+AM_LDFLAGS = \
+ $(LIBARCHIVE_LDFLAGS) \
+ $(STATIC_LDFLAGS) \
+ $(SQLITE3_LDFLAGS) \
+ $(READLINE_LDFLAGS) \
+ $(CURSES_LIB)
+
+CONFIG_OBJS = \
+ ../src/ansi-palette-json.$(OBJEXT) \
+ ../src/default-config.$(OBJEXT) \
+ ../src/xterm-palette-json.$(OBJEXT)
+
+TEXT2C_OBJS = \
+ ../src/builtin-scripts.$(OBJEXT) \
+ ../src/builtin-sh-scripts.$(OBJEXT) \
+ ../src/default-formats.$(OBJEXT) \
+ ../src/time_fmts.$(OBJEXT)
+
+LDADD = \
+ -lz \
+ $(CONFIG_OBJS) \
+ $(TEXT2C_OBJS) \
+ $(DUMMY_OBJS) \
+ ../src/lnav.events.$(OBJEXT) \
+ $(top_builddir)/src/libdiag.a \
+ $(top_builddir)/src/libdatascanner.a \
+ $(top_builddir)/src/formats/logfmt/liblogfmt.a \
+ $(top_builddir)/src/fmtlib/libcppfmt.a \
+ $(top_builddir)/src/pcrepp/libpcrepp.a \
+ $(top_builddir)/src/yajl/libyajl.a \
+ $(top_builddir)/src/yajlpp/libyajlpp.a \
+ $(top_builddir)/src/base/libbase.a \
+ $(top_builddir)/src/pugixml/libpugixml.a \
+ $(top_builddir)/src/third-party/base64/lib/libbase64.a \
+ $(top_builddir)/src/third-party/scnlib/src/libscnlib.a \
+ $(READLINE_LIBS) \
+ $(CURSES_LIB) \
+ $(LIBARCHIVE_LIBS) \
+ $(SQLITE3_LIBS) \
+ $(PCRE_LIBS) \
+ $(LIBCURL)
+
+test_ansi_scrubber_SOURCES = test_ansi_scrubber.cc
+
+test_auto_fd_SOURCES = test_auto_fd.cc
+
+test_auto_mem_SOURCES = test_auto_mem.cc
+
+test_bookmarks_SOURCES = test_bookmarks.cc
+
+test_date_time_scanner_SOURCES = test_date_time_scanner.cc
+
+test_grep_proc2_SOURCES = test_grep_proc2.cc
+
+test_line_buffer2_SOURCES = test_line_buffer2.cc
+
+test_log_accel_SOURCES = test_log_accel.cc
+
+test_text_anonymizer_SOURCES = test_text_anonymizer.cc
+
+test_top_status_SOURCES = test_top_status.cc
+
+test_abbrev_SOURCES = test_abbrev.cc
+
+test_reltime_SOURCES = test_reltime.cc
+
+test_ncurses_unicode_SOURCES = test_ncurses_unicode.cc
+
+lnav_doctests_SOURCES = lnav_doctests.cc
+
+drive_line_buffer_SOURCES = drive_line_buffer.cc
+
+drive_grep_proc_SOURCES = drive_grep_proc.cc
+
+drive_listview_SOURCES = drive_listview.cc
+
+drive_logfile_SOURCES = drive_logfile.cc
+
+drive_shlexer_SOURCES = drive_shlexer.cc
+
+drive_data_scanner_SOURCES = \
+ drive_data_scanner.cc
+
+drive_mvwattrline_SOURCES = drive_mvwattrline.cc
+
+drive_view_colors_SOURCES = drive_view_colors.cc
+
+drive_vt52_curses_SOURCES = drive_vt52_curses.cc
+
+drive_readline_curses_SOURCES = drive_readline_curses.cc
+
+drive_sql_SOURCES = drive_sql.cc
+
+drive_sql_anno_SOURCES = drive_sql_anno.cc
+
+slicer_SOURCES = slicer.cc
+
+scripty_SOURCES = scripty.cc
+
+dist_noinst_SCRIPTS = \
+ parser_debugger.py \
+ test_cli.sh \
+ test_cmds.sh \
+ test_config.sh \
+ test_curl.sh \
+ test_data_parser.sh \
+ test_events.sh \
+ test_format_installer.sh \
+ test_format_loader.sh \
+ test_grep_proc.sh \
+ test_json_format.sh \
+ test_line_buffer.sh \
+ test_listview.sh \
+ test_logfile.sh \
+ test_meta.sh \
+ test_mvwattrline.sh \
+ test_regex101.sh \
+ test_remote.sh \
+ test_scripts.sh \
+ test_sessions.sh \
+ test_shlexer.sh \
+ test_sql.sh \
+ test_sql_anno.sh \
+ test_sql_coll_func.sh \
+ test_sql_fs_func.sh \
+ test_sql_indexes.sh \
+ test_sql_json_func.sh \
+ test_sql_regexp.sh \
+ test_sql_search_table.sh \
+ test_sql_str_func.sh \
+ test_sql_time_func.sh \
+ test_sql_views_vtab.sh \
+ test_sql_xml_func.sh \
+ test_sql_yaml_func.sh \
+ test_text_file.sh \
+ test_tui.sh \
+ test_view_colors.sh \
+ test_vt52_curses.sh \
+ test_pretty_print.sh
+
+include expected/expected.am
+
+dist_noinst_DATA = \
+ $(EXPECTED_FILES) \
+ expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.err \
+ expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.out \
+ ansi-colors.0.in \
+ bad-config/formats/invalid-json-format/format.json \
+ bad-config/formats/invalid-properties/format.json \
+ bad-config/formats/invalid-regex/format.json \
+ bad-config/formats/invalid-sample/format.json \
+ bad-config/formats/invalid-schema/format.json \
+ bad-config/formats/invalid-sql/init.sql \
+ bad-config/formats/invalid-sql/init2.sql \
+ bad-config/formats/no-regexes/format.json \
+ bad-config/formats/no-samples/format.json \
+ bad-config2/formats/invalid-config/config.json \
+ bad-config2/formats/invalid-config/config.bad-schema.json \
+ bad-config2/formats/invalid-config/config.malformed.json \
+ bad-config2/formats/invalid-config/config.truncated.json \
+ bad-config-json/formats/invalid-json/format.json \
+ bad-config-json/formats/invalid-key/format.json \
+ books.xml \
+ file_for_dot_read.sql \
+ datafile_simple.0 \
+ datafile_simple.1 \
+ datafile_simple.2 \
+ datafile_simple.3 \
+ datafile_simple.4 \
+ datafile_simple.5 \
+ datafile_simple.6 \
+ datafile_simple.7 \
+ datafile_simple.8 \
+ datafile_simple.9 \
+ datafile_simple.10 \
+ datafile_simple.11 \
+ datafile_simple.12 \
+ datafile_simple.13 \
+ datafile_simple.14 \
+ datafile_simple.15 \
+ datafile_simple.16 \
+ datafile_simple.17 \
+ datafile_simple.18 \
+ datafile_simple.19 \
+ datafile_simple.20 \
+ datafile_simple.21 \
+ datafile_simple.22 \
+ datafile_xml.0 \
+ dhcp.pcapng \
+ dhcp-trunc.pcapng \
+ expected_help.txt \
+ listview_output.0 \
+ listview_output.1 \
+ listview_output.2 \
+ listview_output.3 \
+ listview_output.4 \
+ listview_output.5 \
+ listview_output.6 \
+ listview_output_cursor.0 \
+ listview_output_cursor.1 \
+ listview_output_cursor.2 \
+ listview_output_cursor.3 \
+ listview_output_cursor.4 \
+ listview_output_cursor.5 \
+ listview_output_cursor.6 \
+ log.clog \
+ logfile_access_log.0 \
+ logfile_access_log.1 \
+ logfile_ansi.0 \
+ logfile_ansi.1 \
+ logfile_bad_access_log.0 \
+ logfile_bad_syslog.0 \
+ logfile_block.1 \
+ logfile_block.2 \
+ logfile_blued.0 \
+ logfile_bro_conn.log.0 \
+ logfile_bro_http.log.0 \
+ logfile_bunyan.0 \
+ logfile_crlf.0 \
+ logfile_cloudflare.json \
+ logfile_cxx.0 \
+ logfile_empty.0 \
+ logfile_epoch.0 \
+ logfile_epoch.1 \
+ logfile_filter.0 \
+ logfile_for_join.0 \
+ logfile_generic.0 \
+ logfile_generic.1 \
+ logfile_generic.2 \
+ logfile_generic.3 \
+ logfile_generic_with_header.0 \
+ logfile_glog.0 \
+ logfile_haproxy.0 \
+ logfile_invalid_json.json \
+ logfile_invalid_json2.json \
+ logfile_mixed_json2.json \
+ logfile_journald.json \
+ logfile_json.json \
+ logfile_json2.json \
+ logfile_json3.json \
+ logfile_json_subsec.json \
+ logfile_leveltest.0 \
+ logfile_logfmt.0 \
+ logfile_multiline.0 \
+ logfile_nested_json.json \
+ logfile_openam.0 \
+ logfile_plain.0 \
+ logfile_pretty.0 \
+ logfile_procstate.0 \
+ logfile_rollover.0 \
+ logfile_rollover.1 \
+ logfile_strace_log.0 \
+ logfile_syslog.0 \
+ logfile_syslog.1 \
+ logfile_syslog.2 \
+ logfile_syslog.3 \
+ logfile_syslog_fr.0 \
+ logfile_syslog_with_access_log.0 \
+ logfile_syslog_with_header.0 \
+ logfile_syslog_with_mixed_times.0 \
+ logfile_tai64n.0 \
+ logfile_tcf.0 \
+ logfile_tcf.1 \
+ logfile_tcsh_history.0 \
+ logfile_uwsgi.0 \
+ logfile_vami.0 \
+ logfile_vdsm.0 \
+ logfile_vmw_log.0 \
+ logfile_vpxd.0 \
+ logfile_w3c.0 \
+ logfile_w3c.1 \
+ logfile_w3c.2 \
+ logfile_w3c.3 \
+ logfile_w3c.4 \
+ logfile_w3c.5 \
+ logfile_w3c.6 \
+ logfile_w3c_big.0 \
+ logfile_with_a_really_long_name_to_test_a_bug_with_long_names.0 \
+ logfile_xml_msg.0 \
+ multiline.lnav \
+ nested.lnav \
+ mvwattrline_output.0 \
+ textfile_0.md \
+ textfile_ansi.0 \
+ textfile_ansi_expanding.0 \
+ textfile_json_indented.0 \
+ textfile_json_one_line.0 \
+ textfile_quoted_json.0 \
+ toplevel.lnav \
+ UTF-8-test.txt \
+ view_colors_output.0 \
+ vt52_curses_input.0 \
+ vt52_curses_input.1 \
+ vt52_curses_output.0 \
+ vt52_curses_output.1 \
+ xpath_tui.0 \
+ formats/collision/format.json \
+ formats/customlevel/format.json \
+ formats/jsontest/format.json \
+ formats/jsontest/lnav-logstash.json \
+ formats/jsontest/rewrite-user.lnav \
+ formats/jsontest2/format.json \
+ formats/jsontest3/format.json \
+ formats/jsontest-subsec/format.json \
+ formats/nestedjson/format.json \
+ formats/scripts/multiline-echo.lnav \
+ formats/scripts/redirecting.lnav \
+ formats/scripts/nested-redirecting.lnav \
+ formats/sqldir/init.sql \
+ formats/timestamp/format.json \
+ formats/xmlmsg/format.json \
+ log-samples/sample-27353a72ba4025448f261dcfa6ea16e474187795.txt \
+ log-samples/sample-70c906b3c1a1cf03f15bde92ee78edfa6f9b7960.txt \
+ log-samples/sample-ad31f12d2adabd07e3ddda3ad5b0dbf6b49c4c99.txt \
+ remote-log-dir/logfile_access_log.0 \
+ remote-log-dir/logfile_access_log.1 \
+ tui-captures/tui_echo.0 \
+ tui-captures/tui_help.0
+
+TESTS = \
+ lnav_doctests \
+ test_abbrev \
+ test_ansi_scrubber \
+ test_auto_fd \
+ test_auto_mem \
+ test_bookmarks \
+ test_date_time_scanner \
+ test_format_installer.sh \
+ test_format_loader.sh \
+ test_cli.sh \
+ test_cmds.sh \
+ test_config.sh \
+ test_events.sh \
+ test_listview.sh \
+ test_meta.sh \
+ test_mvwattrline.sh \
+ test_grep_proc.sh \
+ test_grep_proc2 \
+ test_json_format.sh \
+ test_log_accel \
+ test_logfile.sh \
+ test_reltime \
+ test_scripts.sh \
+ test_sessions.sh \
+ test_shlexer.sh \
+ test_sql.sh \
+ test_sql_anno.sh \
+ test_sql_coll_func.sh \
+ test_sql_fs_func.sh \
+ test_sql_indexes.sh \
+ test_sql_json_func.sh \
+ test_sql_regexp.sh \
+ test_sql_search_table.sh \
+ test_sql_str_func.sh \
+ test_sql_time_func.sh \
+ test_sql_views_vtab.sh \
+ test_sql_xml_func.sh \
+ test_sql_yaml_func.sh \
+ test_text_anonymizer \
+ test_text_file.sh \
+ test_tui.sh \
+ test_data_parser.sh \
+ test_pretty_print.sh \
+ test_view_colors.sh \
+ test_vt52_curses.sh
+
+DISABLED_TESTS = \
+ test_regex101.sh \
+ test_remote.sh \
+ test_top_status \
+ test_line_buffer2 \
+ test_line_buffer.sh
+
+if HAVE_LIBCURL
+TESTS += \
+ test_curl.sh
+endif
+
+DISTCLEANFILES = \
+ *.cmd \
+ *.dat \
+ *.out \
+ *.err \
+ *.db \
+ *.dpt \
+ *.diff \
+ *.index \
+ *.tmp \
+ *.gz \
+ *.bz2 \
+ *.outbak \
+ *.errbak \
+ *.tmpbak \
+ *.xz \
+ exported-session.0.lnav \
+ hw.txt \
+ hw2.txt \
+ reload_test.0 \
+ truncfile.0 \
+ ln.dbg \
+ logfile_append.0 \
+ logfile_changed.0 \
+ logfile_rollover.1.live \
+ test.log \
+ logfile_stdin.log \
+ logfile_stdin.0.log \
+ logfile_syslog_test.0 \
+ logfile_syslog_test.2 \
+ logfile_syslog_fr_test.0 \
+ logfile_syslog_with_mixed_times_test.0 \
+ textfile_long_lines.0 \
+ not:a:remote:file \
+ rollover_in.0 \
+ test-logs.tgz \
+ test-logs-trunc.tgz \
+ test_pretty_in.* \
+ tmp \
+ unreadable.log \
+ UTF-8-test.md \
+ empty \
+ scripts-empty
+
+test_remote.sh.log: remote/ssh_host_dsa_key remote/ssh_host_rsa_key remote/id_rsa
+
+distclean-local:
+ $(RM_V)rm -rf remote remote-tmp not:a:remote:dir
+ $(RM_V)rm -rf sessions
+ $(RM_V)rm -rf tmp
+ $(RM_V)rm -rf rotmp
+ $(RM_V)rm -rf meta-sessions
+ $(RM_V)rm -rf nested
+ $(RM_V)rm -rf test-config
+ $(RM_V)rm -rf .lnav
+ $(RM_V)rm -rf regex101-home
+ $(RM_V)rm -rf events-home
+ $(RM_V)rm -rf support-dump
+ $(RM_V)rm -rf ../installer-test-home
+
+expected:
+ $(top_srcdir)/update_expected_output.sh $(srcdir) $(builddir)
+
+.PHONY: expected
diff --git a/test/UTF-8-test.txt b/test/UTF-8-test.txt
new file mode 100644
index 0000000..a5b5d50
--- /dev/null
+++ b/test/UTF-8-test.txt
Binary files differ
diff --git a/test/aftest.cc b/test/aftest.cc
new file mode 100644
index 0000000..5707c79
--- /dev/null
+++ b/test/aftest.cc
@@ -0,0 +1,63 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#include "base/auto_fd.hh"
+
+void
+foo(int* fd)
+{
+ *fd = 2;
+}
+
+int
+main(int argc, char* argv[])
+{
+ {
+ auto_fd fd(open("/dev/null", O_WRONLY));
+ auto_fd fd2;
+
+ printf("1 fd %d\n", fd.get());
+ fd = -1;
+ printf("2 fd %d\n", fd.get());
+
+ fd = open("/dev/null", O_WRONLY);
+ fd2 = fd;
+ printf("3 fd %d\n", fd.get());
+ printf("4 fd2 %d\n", fd2.get());
+
+ foo(fd2.out());
+ printf("5 fd2 %d\n", fd2.get());
+ }
+
+ printf("nfd %d\n", open("/dev/null", O_WRONLY));
+}
diff --git a/test/ansi-colors.0.in b/test/ansi-colors.0.in
new file mode 100644
index 0000000..8c2573d
--- /dev/null
+++ b/test/ansi-colors.0.in
@@ -0,0 +1,22 @@
+Basic ANSI colors (eight-color, or dim)
+
+ black  black  red  green  yellow  blue  magenta cyan  white 
+bold black  black  red  green  yellow  blue  magenta cyan  white 
+ red  black  red  green  yellow  blue  magenta cyan  white 
+bold red  black  red  green  yellow  blue  magenta cyan  white 
+ green  black  red  green  yellow  blue  magenta cyan  white 
+bold green  black  red  green  yellow  blue  magenta cyan  white 
+ yellow  black  red  green  yellow  blue  magenta cyan  white 
+bold yellow  black  red  green  yellow  blue  magenta cyan  white 
+ blue  black  red  green  yellow  blue  magenta cyan  white 
+bold blue  black  red  green  yellow  blue  magenta cyan  white 
+ magenta  black  red  green  yellow  blue  magenta cyan  white 
+bold magenta  black  red  green  yellow  blue  magenta cyan  white 
+ cyan  black  red  green  yellow  blue  magenta cyan  white 
+bold cyan  black  red  green  yellow  blue  magenta cyan  white 
+ white  black  red  green  yellow  blue  magenta cyan  white 
+bold white  black  red  green  yellow  blue  magenta cyan  white 
+
+Attributes: bold dark italic underline blink concealed
+  testing  testing  testing  testing  testing  testing 
+
diff --git a/test/bad-config-json/formats/invalid-json/format.json b/test/bad-config-json/formats/invalid-json/format.json
new file mode 100644
index 0000000..47369a5
--- /dev/null
+++ b/test/bad-config-json/formats/invalid-json/format.json
@@ -0,0 +1,5 @@
+{
+ "foobar_log": {
+ "abc"
+ }
+}
diff --git a/test/bad-config-json/formats/invalid-key/format.json b/test/bad-config-json/formats/invalid-key/format.json
new file mode 100644
index 0000000..11a3bdd
--- /dev/null
+++ b/test/bad-config-json/formats/invalid-key/format.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "invalid_key_log": {
+ "level-pointer": "abc(",
+ "file-pattern": "def[ghi",
+ "json": true,
+ "regex": {
+ "foo": {
+ "pattern": "jkl"
+ }
+ },
+ "value": {
+ "test": {
+ "identifiers": true
+ },
+ "body": {
+ "kind": "string"
+ }
+ },
+ "line-format": [
+ {
+ "field": "non-existent"
+ }
+ ],
+ "timestamp-divisor": -1.2
+ }
+} \ No newline at end of file
diff --git a/test/bad-config/formats/invalid-json-format/format.json b/test/bad-config/formats/invalid-json-format/format.json
new file mode 100644
index 0000000..0e06a9e
--- /dev/null
+++ b/test/bad-config/formats/invalid-json-format/format.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "bad_json_log": {
+ "json": true,
+ "line-format": [
+ {
+ "field": ""
+ },
+ {
+ "field": "__timestamp__",
+ "timestamp-format": ""
+ }
+ ],
+ "value": {
+ }
+ }
+} \ No newline at end of file
diff --git a/test/bad-config/formats/invalid-name/format.json b/test/bad-config/formats/invalid-name/format.json
new file mode 100644
index 0000000..cb8d896
--- /dev/null
+++ b/test/bad-config/formats/invalid-name/format.json
@@ -0,0 +1,8 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "bad-name-log": {
+ "title": "bad-format",
+ "description": "Log format with a name that has invalid characters",
+ "json": true
+ }
+} \ No newline at end of file
diff --git a/test/bad-config/formats/invalid-properties/format.json b/test/bad-config/formats/invalid-properties/format.json
new file mode 100644
index 0000000..a69a179
--- /dev/null
+++ b/test/bad-config/formats/invalid-properties/format.json
@@ -0,0 +1,45 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "invalid_props_log": {
+ "title": "invalid properties",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d+): (?<pid>\\w+) (?<body>.*)$"
+ }
+ },
+ "timestamp-field": "ts",
+ "subsecond-field": "ts-sub",
+ "sample": [
+ {
+ "line": "1428634687123: 1234 abc"
+ }
+ ],
+ "value": {
+ "non-existent": {
+ "kind": "string"
+ }
+ },
+ "highlights": {
+ "hl1": {
+ "color": "not a color",
+ "background-color": "also not a color"
+ }
+ },
+ "tags": {
+ "badtag": {
+ "paths": []
+ },
+ "badtag2": {
+ "pattern": ""
+ },
+ "badtag3": {
+ "pattern": "invalid(abc"
+ }
+ },
+ "search-table": {
+ "bad_table_regex": {
+ "pattern": "abc(def"
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/bad-config/formats/invalid-regex/format.json b/test/bad-config/formats/invalid-regex/format.json
new file mode 100644
index 0000000..33807a5
--- /dev/null
+++ b/test/bad-config/formats/invalid-regex/format.json
@@ -0,0 +1,29 @@
+{
+ "bad_regex_log": {
+ "title": "invalid regex test",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d+: (?<body>.*)$"
+ },
+ "incomplete-match": {
+ "pattern": "^(?<timestamp>\\d+);"
+ }
+ },
+ "level": {
+ "error": "(foo"
+ },
+ "timestamp-format": [
+ "%i"
+ ],
+ "sample": [
+ {
+ "line": "1428634687123; foo"
+ }
+ ],
+ "highlights": {
+ "foobar": {
+ "pattern": "abc("
+ }
+ }
+ }
+}
diff --git a/test/bad-config/formats/invalid-sample/format.json b/test/bad-config/formats/invalid-sample/format.json
new file mode 100644
index 0000000..d990bb9
--- /dev/null
+++ b/test/bad-config/formats/invalid-sample/format.json
@@ -0,0 +1,49 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "bad_sample_log": {
+ "title": "invalid sample test",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d+): (?<pid>\\w+) (?<body>.*)$"
+ },
+ "semi": {
+ "pattern": "^(?<timestamp>\\d+); (?<body>\\w+)$"
+ },
+ "bad-time": {
+ "pattern": "^(?<timestamp>\\w+): (?<body>\\w+)$"
+ },
+ "with-level": {
+ "pattern": "^(?<timestamp>\\d+)\\| (?<level>\\w+) (?<body>\\w+)$"
+ }
+ },
+ "timestamp-format": [
+ "%i"
+ ],
+ "value": {
+ "pid": {
+ "kind": "foo"
+ }
+ },
+ "level-field": "level",
+ "level": {
+ "info": "info",
+ "debug": "debug"
+ },
+ "sample": [
+ {
+ "line": "abc: foo"
+ },
+ {
+ "line": "1428634687123| debug hello",
+ "level": "info"
+ },
+ {
+ "line": "1428634687123| debug hello\ngoodbye",
+ "level": "debug"
+ },
+ {
+ "line": "1428634687123; foo bar"
+ }
+ ]
+ }
+}
diff --git a/test/bad-config/formats/invalid-schema/format.json b/test/bad-config/formats/invalid-schema/format.json
new file mode 100644
index 0000000..b011164
--- /dev/null
+++ b/test/bad-config/formats/invalid-schema/format.json
@@ -0,0 +1,3 @@
+{
+ "$schema": "bad"
+} \ No newline at end of file
diff --git a/test/bad-config/formats/invalid-sql/init.sql b/test/bad-config/formats/invalid-sql/init.sql
new file mode 100644
index 0000000..341e675
--- /dev/null
+++ b/test/bad-config/formats/invalid-sql/init.sql
@@ -0,0 +1,5 @@
+
+SELECT * FROM sqlite_master;
+
+-- comment test
+CREATE TALE invalid (x y z);
diff --git a/test/bad-config/formats/invalid-sql/init2.sql b/test/bad-config/formats/invalid-sql/init2.sql
new file mode 100644
index 0000000..9810ea0
--- /dev/null
+++ b/test/bad-config/formats/invalid-sql/init2.sql
@@ -0,0 +1,2 @@
+SELECT regexp_match('abc(', '123')
+FROM sqlite_master;
diff --git a/test/bad-config/formats/no-regexes/format.json b/test/bad-config/formats/no-regexes/format.json
new file mode 100644
index 0000000..f20cde1
--- /dev/null
+++ b/test/bad-config/formats/no-regexes/format.json
@@ -0,0 +1,7 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "no_regexes_log": {
+ "value": {
+ }
+ }
+} \ No newline at end of file
diff --git a/test/bad-config/formats/no-samples/format.json b/test/bad-config/formats/no-samples/format.json
new file mode 100644
index 0000000..5c06294
--- /dev/null
+++ b/test/bad-config/formats/no-samples/format.json
@@ -0,0 +1,17 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "no_sample_log": {
+ "title": "invalid sample test",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d+): (?<body>.*)$"
+ },
+ "semi": {
+ "pattern": "^(?<timestamp>\\d+); (?<body>\\w+)$"
+ }
+ },
+ "timestamp-format": [
+ "%i"
+ ]
+ }
+}
diff --git a/test/bad-config2/formats/invalid-config/config.bad-schema.json b/test/bad-config2/formats/invalid-config/config.bad-schema.json
new file mode 100644
index 0000000..b011164
--- /dev/null
+++ b/test/bad-config2/formats/invalid-config/config.bad-schema.json
@@ -0,0 +1,3 @@
+{
+ "$schema": "bad"
+} \ No newline at end of file
diff --git a/test/bad-config2/formats/invalid-config/config.json b/test/bad-config2/formats/invalid-config/config.json
new file mode 100644
index 0000000..ba5cf2f
--- /dev/null
+++ b/test/bad-config2/formats/invalid-config/config.json
@@ -0,0 +1,5 @@
+{
+ "ui": {
+ "theme": "foo"
+ }
+} \ No newline at end of file
diff --git a/test/bad-config2/formats/invalid-config/config.malformed.json b/test/bad-config2/formats/invalid-config/config.malformed.json
new file mode 100644
index 0000000..0935e8b
--- /dev/null
+++ b/test/bad-config2/formats/invalid-config/config.malformed.json
@@ -0,0 +1,5 @@
+{
+ "ui": "theme",
+ "abc",
+ "def": ""
+} \ No newline at end of file
diff --git a/test/bad-config2/formats/invalid-config/config.truncated.json b/test/bad-config2/formats/invalid-config/config.truncated.json
new file mode 100644
index 0000000..fc01f0d
--- /dev/null
+++ b/test/bad-config2/formats/invalid-config/config.truncated.json
@@ -0,0 +1,2 @@
+{
+ "ui": "theme"
diff --git a/test/books.xml b/test/books.xml
new file mode 100644
index 0000000..19c4f73
--- /dev/null
+++ b/test/books.xml
@@ -0,0 +1,120 @@
+<?xml version="1.0"?>
+<catalog>
+ <book id="bk101">
+ <author>Gambardella, Matthew</author>
+ <title>XML Developer's Guide</title>
+ <genre>Computer</genre>
+ <price>44.95</price>
+ <publish_date>2000-10-01</publish_date>
+ <description>An in-depth look at creating applications
+ with XML.</description>
+ </book>
+ <book id="bk102">
+ <author>Ralls, Kim</author>
+ <title>Midnight Rain</title>
+ <genre>Fantasy</genre>
+ <price>5.95</price>
+ <publish_date>2000-12-16</publish_date>
+ <description>A former architect battles corporate zombies,
+ an evil sorceress, and her own childhood to become queen
+ of the world.</description>
+ </book>
+ <book id="bk103">
+ <author>Corets, Eva</author>
+ <title>Maeve Ascendant</title>
+ <genre>Fantasy</genre>
+ <price>5.95</price>
+ <publish_date>2000-11-17</publish_date>
+ <description>After the collapse of a nanotechnology
+ society in England, the young survivors lay the
+ foundation for a new society.</description>
+ </book>
+ <book id="bk104">
+ <author>Corets, Eva</author>
+ <title>Oberon's Legacy</title>
+ <genre>Fantasy</genre>
+ <price>5.95</price>
+ <publish_date>2001-03-10</publish_date>
+ <description>In post-apocalypse England, the mysterious
+ agent known only as Oberon helps to create a new life
+ for the inhabitants of London. Sequel to Maeve
+ Ascendant.</description>
+ </book>
+ <book id="bk105">
+ <author>Corets, Eva</author>
+ <title>The Sundered Grail</title>
+ <genre>Fantasy</genre>
+ <price>5.95</price>
+ <publish_date>2001-09-10</publish_date>
+ <description>The two daughters of Maeve, half-sisters,
+ battle one another for control of England. Sequel to
+ Oberon's Legacy.</description>
+ </book>
+ <book id="bk106">
+ <author>Randall, Cynthia</author>
+ <title>Lover Birds</title>
+ <genre>Romance</genre>
+ <price>4.95</price>
+ <publish_date>2000-09-02</publish_date>
+ <description>When Carla meets Paul at an ornithology
+ conference, tempers fly as feathers get ruffled.</description>
+ </book>
+ <book id="bk107">
+ <author>Thurman, Paula</author>
+ <title>Splish Splash</title>
+ <genre>Romance</genre>
+ <price>4.95</price>
+ <publish_date>2000-11-02</publish_date>
+ <description>A deep sea diver finds true love twenty
+ thousand leagues beneath the sea.</description>
+ </book>
+ <book id="bk108">
+ <author>Knorr, Stefan</author>
+ <title>Creepy Crawlies</title>
+ <genre>Horror</genre>
+ <price>4.95</price>
+ <publish_date>2000-12-06</publish_date>
+ <description>An anthology of horror stories about roaches,
+ centipedes, scorpions and other insects.</description>
+ </book>
+ <book id="bk109">
+ <author>Kress, Peter</author>
+ <title>Paradox Lost</title>
+ <genre>Science Fiction</genre>
+ <price>6.95</price>
+ <publish_date>2000-11-02</publish_date>
+ <description>After an inadvertant trip through a Heisenberg
+ Uncertainty Device, James Salway discovers the problems
+ of being quantum.</description>
+ </book>
+ <book id="bk110">
+ <author>O'Brien, Tim</author>
+ <title>Microsoft .NET: The Programming Bible</title>
+ <genre>Computer</genre>
+ <price>36.95</price>
+ <publish_date>2000-12-09</publish_date>
+ <description>Microsoft's .NET initiative is explored in
+ detail in this deep programmer's reference.</description>
+ </book>
+ <book id="bk111">
+ <author>O'Brien, Tim</author>
+ <title>MSXML3: A Comprehensive Guide</title>
+ <genre>Computer</genre>
+ <price>36.95</price>
+ <publish_date>2000-12-01</publish_date>
+ <description>The Microsoft MSXML3 parser is covered in
+ detail, with attention to XML DOM interfaces, XSLT processing,
+ SAX and more.</description>
+ </book>
+ <book id="bk112">
+ <author>Galos, Mike</author>
+ <title>Visual Studio 7: A Comprehensive Guide</title>
+ <genre>Computer</genre>
+ <price>49.95</price>
+ <publish_date>2001-04-16</publish_date>
+ <description>Microsoft Visual Studio 7 is explored in depth,
+ looking at how Visual Basic, Visual C++, C#, and ASP+ are
+ integrated into a comprehensive development
+ environment.</description>
+ </book>
+</catalog>
diff --git a/test/datafile_ipaddr.0 b/test/datafile_ipaddr.0
new file mode 100644
index 0000000..e04c467
--- /dev/null
+++ b/test/datafile_ipaddr.0
@@ -0,0 +1,13 @@
+ IPv4=192.168.1.2 IPv6_LL=fe80::c62c:3ff:fe0e:e44a%en0 IPV6=fdca:a98b:b1bf:e05d:7c49:35b3:5949:ec6f
+ key 0:4 ^--^ IPv4
+ipv4 5:16 ^---------^ 192.168.1.2
+ val 5:16 ^---------^ 192.168.1.2
+pair 0:16 ^--------------^ IPv4=192.168.1.2
+ key 17:24 ^-----^ IPv6_LL
+ipv6 25:53 ^--------------------------^ fe80::c62c:3ff:fe0e:e44a%en0
+ val 25:53 ^--------------------------^ fe80::c62c:3ff:fe0e:e44a%en0
+pair 17:53 ^----------------------------------^ IPv6_LL=fe80::c62c:3ff:fe0e:e44a%en0
+ key 54:58 ^--^ IPV6
+ipv6 59:98 ^-------------------------------------^ fdca:a98b:b1bf:e05d:7c49:35b3:5949:ec6f
+ val 59:98 ^-------------------------------------^ fdca:a98b:b1bf:e05d:7c49:35b3:5949:ec6f
+pair 54:98 ^------------------------------------------^ IPV6=fdca:a98b:b1bf:e05d:7c49:35b3:5949:ec6f
diff --git a/test/datafile_simple.0 b/test/datafile_simple.0
new file mode 100644
index 0000000..459716a
--- /dev/null
+++ b/test/datafile_simple.0
@@ -0,0 +1,25 @@
+ a=1 b=2 c=3,4
+ key 0:1 ^ a
+ num 2:3 ^ 1
+ val 2:3 ^ 1
+pair 0:3 ^-^ a=1
+ key 4:5 ^ b
+ num 6:7 ^ 2
+ val 6:7 ^ 2
+pair 4:7 ^-^ b=2
+ key 8:9 ^ c
+ num 10:11 ^ 3
+ val 10:11 ^ 3
+pair 8:11 ^-^ c=3
+ key 12:12 ^
+ num 12:13 ^ 4
+ val 12:13 ^ 4
+pair 12:13 ^ 4
+msg :a=1 b=2 c=3,4
+format :a=# b=# c=#,#
+{
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ "col_0": 4
+}
diff --git a/test/datafile_simple.1 b/test/datafile_simple.1
new file mode 100644
index 0000000..48bb61a
--- /dev/null
+++ b/test/datafile_simple.1
@@ -0,0 +1,13 @@
+ current speed: 38 mph
+ key 0:0
+ key 0:13 ^-----------^ current speed
+pair 0:13 ^-----------^ current speed
+ key 15:15 ^
+ num 15:17 ^^ 38
+pair 15:17 ^^ 38
+msg :current speed: 38 mph
+format :#: # mph
+{
+ "col_0": "current speed",
+ "col_1": 38
+}
diff --git a/test/datafile_simple.10 b/test/datafile_simple.10
new file mode 100644
index 0000000..928f183
--- /dev/null
+++ b/test/datafile_simple.10
@@ -0,0 +1,9 @@
+ quoted string u'pm/runtime'
+ key 16:16 ^
+quot 16:26 ^--------^ pm/runtime
+pair 16:26 ^--------^ pm/runtime
+msg :quoted string u'pm/runtime'
+format :quoted string #
+{
+ "col_0": "pm/runtime"
+}
diff --git a/test/datafile_simple.11 b/test/datafile_simple.11
new file mode 100644
index 0000000..f12969f
--- /dev/null
+++ b/test/datafile_simple.11
@@ -0,0 +1,13 @@
+ version numbers 0.6.16 1.2-a1
+ key 17:17 ^
+vers 17:23 ^----^ 0.6.16
+pair 17:23 ^----^ 0.6.16
+ key 25:25 ^
+vers 25:31 ^----^ 1.2-a1
+pair 25:31 ^----^ 1.2-a1
+msg :version numbers 0.6.16 1.2-a1
+format :version numbers # #
+{
+ "col_0": "0.6.16",
+ "col_1": "1.2-a1"
+}
diff --git a/test/datafile_simple.12 b/test/datafile_simple.12
new file mode 100644
index 0000000..e19007e
--- /dev/null
+++ b/test/datafile_simple.12
@@ -0,0 +1,10 @@
+ kickoff_duration=4.41074371338e-05
+ key 0:16 ^--------------^ kickoff_duration
+ num 17:34 ^---------------^ 4.41074371338e-05
+ val 17:34 ^---------------^ 4.41074371338e-05
+pair 0:34 ^--------------------------------^ kickoff_duration=4.41074371338e-05
+msg :kickoff_duration=4.41074371338e-05
+format :kickoff_duration=#
+{
+ "kickoff_duration": 4.41074371338e-05
+}
diff --git a/test/datafile_simple.13 b/test/datafile_simple.13
new file mode 100644
index 0000000..8021010
--- /dev/null
+++ b/test/datafile_simple.13
@@ -0,0 +1,10 @@
+ kickoff_duration=4.41074371338E-05
+ key 0:16 ^--------------^ kickoff_duration
+ num 17:34 ^---------------^ 4.41074371338E-05
+ val 17:34 ^---------------^ 4.41074371338E-05
+pair 0:34 ^--------------------------------^ kickoff_duration=4.41074371338E-05
+msg :kickoff_duration=4.41074371338E-05
+format :kickoff_duration=#
+{
+ "kickoff_duration": 4.41074371338E-05
+}
diff --git a/test/datafile_simple.14 b/test/datafile_simple.14
new file mode 100644
index 0000000..4c548b9
--- /dev/null
+++ b/test/datafile_simple.14
@@ -0,0 +1,57 @@
+ FSChange(Direction.DOWNLOAD, Action.CREATE, name=Fanime 2015, route=[CloudEntry(doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names)])
+ key 0:0
+ sym 0:8 ^------^ FSChange
+pair 0:8 ^------^ FSChange
+ key 9:9 ^
+ key 9:9 ^
+ sym 9:27 ^----------------^ Direction.DOWNLOAD
+ val 9:27 ^----------------^ Direction.DOWNLOAD
+pair 9:27 ^----------------^ Direction.DOWNLOAD
+ key 29:29 ^
+ sym 29:42 ^-----------^ Action.CREATE
+ val 29:42 ^-----------^ Action.CREATE
+pair 29:42 ^-----------^ Action.CREATE
+ key 44:48 ^--^ name
+word 49:55 ^----^ Fanime
+wspc 55:56 ^
+ num 56:60 ^--^ 2015
+ val 49:60 ^---------^ Fanime 2015
+pair 44:60 ^--------------^ name=Fanime 2015
+ key 62:67 ^---^ route
+ key 69:79 ^--------^ CloudEntry
+ key 80:86 ^----^ doc_id
+ sym 87:131 ^------------------------------------------^ 1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg
+ val 87:131 ^------------------------------------------^ 1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg
+pair 80:131 ^-------------------------------------------------^ doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg
+ key 132:140 ^------^ filename
+word 141:145 ^--^ Baby
+wspc 145:146 ^
+word 146:151 ^---^ Names
+ val 141:151 ^--------^ Baby Names
+pair 132:151 ^-----------------^ filename=Baby Names
+ grp 80:151 ^---------------------------------------------------------------------^ doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names
+ val 80:151 ^---------------------------------------------------------------------^ doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names
+pair 69:151 ^--------------------------------------------------------------------------------^ CloudEntry(doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names
+ grp 69:151 ^--------------------------------------------------------------------------------^ CloudEntry(doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names
+ val 69:151 ^--------------------------------------------------------------------------------^ CloudEntry(doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names
+pair 62:151 ^---------------------------------------------------------------------------------------^ route=[CloudEntry(doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names
+ grp 9:151 ^--------------------------------------------------------------------------------------------------------------------------------------------^ Direction.DOWNLOAD, Action.CREATE, name=Fanime 2015, route=[CloudEntry(doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names
+pair 9:151 ^--------------------------------------------------------------------------------------------------------------------------------------------^ Direction.DOWNLOAD, Action.CREATE, name=Fanime 2015, route=[CloudEntry(doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names
+msg :FSChange(Direction.DOWNLOAD, Action.CREATE, name=Fanime 2015, route=[CloudEntry(doc_id=1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg,filename=Baby Names)])
+format :#(#)])
+{
+ "col_0": "FSChange",
+ "col_1": {
+ "col_0": "Direction.DOWNLOAD",
+ "col_1": "Action.CREATE",
+ "name": "Fanime 2015",
+ "route": [
+ {
+ "CloudEntry": {
+ "doc_id": "1g5Yho6JmysVGRO-Xmfurra_cQRFb0nTIfZRhGompweg",
+ "filename": "Baby Names"
+ }
+ }
+ ]
+ }
+}
diff --git a/test/datafile_simple.15 b/test/datafile_simple.15
new file mode 100644
index 0000000..f0e053d
--- /dev/null
+++ b/test/datafile_simple.15
@@ -0,0 +1,75 @@
+ Worker successfully completed [ImmutableChange(Direction.UPLOAD, Action.CREATE, ino=LocalID(inode=5567236), path=u'/Users/stack/Google Drive', name=u'pyjsonpath1.patch', parent_ino=LocalID(inode=46166734), is_folder=False)]
+ key 31:31 ^
+ key 31:46 ^-------------^ ImmutableChange
+ key 47:47 ^
+ sym 47:63 ^--------------^ Direction.UPLOAD
+ val 47:63 ^--------------^ Direction.UPLOAD
+pair 47:63 ^--------------^ Direction.UPLOAD
+ key 65:65 ^
+ sym 65:78 ^-----------^ Action.CREATE
+ val 65:78 ^-----------^ Action.CREATE
+pair 65:78 ^-----------^ Action.CREATE
+ key 80:83 ^-^ ino
+ sym 84:91 ^-----^ LocalID
+ key 92:97 ^---^ inode
+ num 98:105 ^-----^ 5567236
+ val 98:105 ^-----^ 5567236
+pair 92:105 ^-----------^ inode=5567236
+ grp 92:105 ^-----------^ inode=5567236
+pair 84:105 ^-------------------^ LocalID(inode=5567236
+ val 84:105 ^-------------------^ LocalID(inode=5567236
+pair 80:105 ^-----------------------^ ino=LocalID(inode=5567236
+ key 108:112 ^--^ path
+quot 115:140 ^-----------------------^ /Users/stack/Google Drive
+ val 115:140 ^-----------------------^ /Users/stack/Google Drive
+pair 108:140 ^------------------------------^ path=u'/Users/stack/Google Drive
+ key 143:147 ^--^ name
+quot 150:167 ^---------------^ pyjsonpath1.patch
+ val 150:167 ^---------------^ pyjsonpath1.patch
+pair 143:167 ^----------------------^ name=u'pyjsonpath1.patch
+ key 170:180 ^--------^ parent_ino
+ sym 181:188 ^-----^ LocalID
+ key 189:194 ^---^ inode
+ num 195:203 ^------^ 46166734
+ val 195:203 ^------^ 46166734
+pair 189:203 ^------------^ inode=46166734
+ grp 189:203 ^------------^ inode=46166734
+pair 181:203 ^--------------------^ LocalID(inode=46166734
+ val 181:203 ^--------------------^ LocalID(inode=46166734
+pair 170:203 ^-------------------------------^ parent_ino=LocalID(inode=46166734
+ key 206:215 ^-------^ is_folder
+cnst 216:221 ^---^ False
+ val 216:221 ^---^ False
+pair 206:221 ^-------------^ is_folder=False
+ grp 47:221 ^----------------------------------------------------------------------------------------------------------------------------------------------------------------------------^ Direction.UPLOAD, Action.CREATE, ino=LocalID(inode=5567236), path=u'/Users/stack/Google Drive', name=u'pyjsonpath1.patch', parent_ino=LocalID(inode=46166734), is_folder=False
+ val 47:221 ^----------------------------------------------------------------------------------------------------------------------------------------------------------------------------^ Direction.UPLOAD, Action.CREATE, ino=LocalID(inode=5567236), path=u'/Users/stack/Google Drive', name=u'pyjsonpath1.patch', parent_ino=LocalID(inode=46166734), is_folder=False
+pair 31:221 ^--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------^ ImmutableChange(Direction.UPLOAD, Action.CREATE, ino=LocalID(inode=5567236), path=u'/Users/stack/Google Drive', name=u'pyjsonpath1.patch', parent_ino=LocalID(inode=46166734), is_folder=False
+ grp 31:221 ^--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------^ ImmutableChange(Direction.UPLOAD, Action.CREATE, ino=LocalID(inode=5567236), path=u'/Users/stack/Google Drive', name=u'pyjsonpath1.patch', parent_ino=LocalID(inode=46166734), is_folder=False
+pair 31:221 ^--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------^ ImmutableChange(Direction.UPLOAD, Action.CREATE, ino=LocalID(inode=5567236), path=u'/Users/stack/Google Drive', name=u'pyjsonpath1.patch', parent_ino=LocalID(inode=46166734), is_folder=False
+msg :Worker successfully completed [ImmutableChange(Direction.UPLOAD, Action.CREATE, ino=LocalID(inode=5567236), path=u'/Users/stack/Google Drive', name=u'pyjsonpath1.patch', parent_ino=LocalID(inode=46166734), is_folder=False)]
+format :Worker successfully completed [#)]
+[
+ {
+ "ImmutableChange": {
+ "col_0": "Direction.UPLOAD",
+ "col_1": "Action.CREATE",
+ "ino": {
+ "LocalID": [
+ {
+ "inode": 5567236
+ }
+ ]
+ },
+ "path": "/Users/stack/Google Drive",
+ "name": "pyjsonpath1.patch",
+ "parent_ino": {
+ "LocalID": [
+ {
+ "inode": 46166734
+ }
+ ]
+ },
+ "is_folder": false
+ }
+ }
+]
diff --git a/test/datafile_simple.16 b/test/datafile_simple.16
new file mode 100644
index 0000000..f549fc7
--- /dev/null
+++ b/test/datafile_simple.16
@@ -0,0 +1,9 @@
+ test: c'est la vie charlie's
+ key 0:0
+ key 0:4 ^--^ test
+pair 0:4 ^--^ test
+msg :test: c'est la vie charlie's
+format :#: c'est la vie charlie's
+{
+ "col_0": "test"
+}
diff --git a/test/datafile_simple.17 b/test/datafile_simple.17
new file mode 100644
index 0000000..132eda6
--- /dev/null
+++ b/test/datafile_simple.17
@@ -0,0 +1,22 @@
+ foo=[]; bar=['a', 'b']
+ key 0:3 ^-^ foo
+ grp 5:5 ^
+ val 5:5 ^
+pair 0:5 ^---^ foo=[
+ key 8:11 ^-^ bar
+quot 14:15 ^ a
+ val 14:15 ^ a
+quot 19:20 ^ b
+ val 19:20 ^ b
+ grp 14:20 ^----^ a', 'b
+ val 14:20 ^----^ a', 'b
+pair 8:20 ^----------^ bar=['a', 'b
+msg :foo=[]; bar=['a', 'b']
+format :foo=[#]; bar=[#]
+{
+ "foo": null,
+ "bar": [
+ "a",
+ "b"
+ ]
+}
diff --git a/test/datafile_simple.18 b/test/datafile_simple.18
new file mode 100644
index 0000000..55f125b
--- /dev/null
+++ b/test/datafile_simple.18
@@ -0,0 +1,23 @@
+ list [foo(bar=1)]
+ key 6:6 ^
+ key 6:9 ^-^ foo
+ key 10:13 ^-^ bar
+ num 14:15 ^ 1
+ val 14:15 ^ 1
+pair 10:15 ^---^ bar=1
+ grp 10:15 ^---^ bar=1
+ val 10:15 ^---^ bar=1
+pair 6:15 ^-------^ foo(bar=1
+ grp 6:15 ^-------^ foo(bar=1
+pair 6:15 ^-------^ foo(bar=1
+msg :list [foo(bar=1)]
+format :list [#)]
+[
+ {
+ "foo": [
+ {
+ "bar": 1
+ }
+ ]
+ }
+]
diff --git a/test/datafile_simple.19 b/test/datafile_simple.19
new file mode 100644
index 0000000..ce58fc9
--- /dev/null
+++ b/test/datafile_simple.19
@@ -0,0 +1,53 @@
+ list [foo(bar=1), foo(bar=2), foo(bar=3)]
+ key 6:6 ^
+ key 6:9 ^-^ foo
+ key 10:13 ^-^ bar
+ num 14:15 ^ 1
+ val 14:15 ^ 1
+pair 10:15 ^---^ bar=1
+ grp 10:15 ^---^ bar=1
+ val 10:15 ^---^ bar=1
+pair 6:15 ^-------^ foo(bar=1
+ key 18:21 ^-^ foo
+ key 22:25 ^-^ bar
+ num 26:27 ^ 2
+ val 26:27 ^ 2
+pair 22:27 ^---^ bar=2
+ grp 22:27 ^---^ bar=2
+ val 22:27 ^---^ bar=2
+pair 18:27 ^-------^ foo(bar=2
+ key 30:33 ^-^ foo
+ key 34:37 ^-^ bar
+ num 38:39 ^ 3
+ val 38:39 ^ 3
+pair 34:39 ^---^ bar=3
+ grp 34:39 ^---^ bar=3
+ val 34:39 ^---^ bar=3
+pair 30:39 ^-------^ foo(bar=3
+ grp 6:39 ^-------------------------------^ foo(bar=1), foo(bar=2), foo(bar=3
+pair 6:39 ^-------------------------------^ foo(bar=1), foo(bar=2), foo(bar=3
+msg :list [foo(bar=1), foo(bar=2), foo(bar=3)]
+format :list [#)]
+[
+ {
+ "foo": [
+ {
+ "bar": 1
+ }
+ ]
+ },
+ {
+ "foo": [
+ {
+ "bar": 2
+ }
+ ]
+ },
+ {
+ "foo": [
+ {
+ "bar": 3
+ }
+ ]
+ }
+]
diff --git a/test/datafile_simple.2 b/test/datafile_simple.2
new file mode 100644
index 0000000..c141544
--- /dev/null
+++ b/test/datafile_simple.2
@@ -0,0 +1,40 @@
+ 1,2,3,4,five,six,7
+ key 0:0
+ num 0:1 ^ 1
+ val 0:1 ^ 1
+pair 0:1 ^ 1
+ key 2:2 ^
+ num 2:3 ^ 2
+ val 2:3 ^ 2
+pair 2:3 ^ 2
+ key 4:4 ^
+ num 4:5 ^ 3
+ val 4:5 ^ 3
+pair 4:5 ^ 3
+ key 6:6 ^
+ num 6:7 ^ 4
+ val 6:7 ^ 4
+pair 6:7 ^ 4
+ key 8:8 ^
+word 8:12 ^--^ five
+ val 8:12 ^--^ five
+pair 8:12 ^--^ five
+ key 13:13 ^
+word 13:16 ^-^ six
+ val 13:16 ^-^ six
+pair 13:16 ^-^ six
+ key 17:17 ^
+ num 17:18 ^ 7
+ val 17:18 ^ 7
+pair 17:18 ^ 7
+msg :1,2,3,4,five,six,7
+format :#,#,#,#,#,#,#
+{
+ "col_0": 1,
+ "col_1": 2,
+ "col_2": 3,
+ "col_3": 4,
+ "col_4": "five",
+ "col_5": "six",
+ "col_6": 7
+}
diff --git a/test/datafile_simple.20 b/test/datafile_simple.20
new file mode 100644
index 0000000..ea81659
--- /dev/null
+++ b/test/datafile_simple.20
@@ -0,0 +1,17 @@
+ list ["abc", "def", "ghi"]
+ key 7:7 ^
+quot 7:10 ^-^ abc
+ val 7:10 ^-^ abc
+quot 14:17 ^-^ def
+ val 14:17 ^-^ def
+quot 21:24 ^-^ ghi
+ val 21:24 ^-^ ghi
+ grp 7:24 ^---------------^ abc", "def", "ghi
+pair 7:24 ^---------------^ abc", "def", "ghi
+msg :list ["abc", "def", "ghi"]
+format :list [#]
+[
+ "abc",
+ "def",
+ "ghi"
+]
diff --git a/test/datafile_simple.21 b/test/datafile_simple.21
new file mode 100644
index 0000000..e570241
--- /dev/null
+++ b/test/datafile_simple.21
@@ -0,0 +1,26 @@
+ {"correctProperty":"test", "incorrectProperty": "test\"", "nextProperty":"test"}
+ key 2:2 ^
+quot 2:17 ^-------------^ correctProperty
+ val 2:17 ^-------------^ correctProperty
+quot 20:24 ^--^ test
+ val 20:24 ^--^ test
+quot 28:45 ^---------------^ incorrectProperty
+ val 28:45 ^---------------^ incorrectProperty
+quot 49:55 ^----^ test\"
+ val 49:55 ^----^ test\"
+quot 59:71 ^----------^ nextProperty
+ val 59:71 ^----------^ nextProperty
+quot 74:78 ^--^ test
+ val 74:78 ^--^ test
+ grp 2:78 ^--------------------------------------------------------------------------^ correctProperty":"test", "incorrectProperty": "test\"", "nextProperty":"test
+pair 2:78 ^--------------------------------------------------------------------------^ correctProperty":"test", "incorrectProperty": "test\"", "nextProperty":"test
+msg :{"correctProperty":"test", "incorrectProperty": "test\"", "nextProperty":"test"}
+format :{#}
+[
+ "correctProperty",
+ "test",
+ "incorrectProperty",
+ "test\\\"",
+ "nextProperty",
+ "test"
+]
diff --git a/test/datafile_simple.22 b/test/datafile_simple.22
new file mode 100644
index 0000000..501fe36
--- /dev/null
+++ b/test/datafile_simple.22
@@ -0,0 +1,9 @@
+ C:\Program Files (x86)\Google\Chrome\Application\new_chrome.exe
+ key 0:0
+path 0:63 ^-------------------------------------------------------------^ C:\Program Files (x86)\Google\Chrome\Application\new_chrome.exe
+pair 0:63 ^-------------------------------------------------------------^ C:\Program Files (x86)\Google\Chrome\Application\new_chrome.exe
+msg :C:\Program Files (x86)\Google\Chrome\Application\new_chrome.exe
+format :#
+{
+ "col_0": "C:\\Program Files (x86)\\Google\\Chrome\\Application\\new_chrome.exe"
+}
diff --git a/test/datafile_simple.23 b/test/datafile_simple.23
new file mode 100644
index 0000000..d649941
--- /dev/null
+++ b/test/datafile_simple.23
@@ -0,0 +1,88 @@
+ 2022-06-02T12:26:22.072Z info vpxd[47413] [Originator@6876 sub=vpxLro opID=21fa61e9-3e] [VpxLRO] -- BEGIN lro-954041 -- AuthorizationManager -- vim.AuthorizationManager.hasUserPrivilegeOnEntities -- 52768da7-4006-3d4a-4917-ee027373630f(522e0475-8901-e8b8-1eb8-07ec729ac50c)
+ key 23:23 ^
+ sym 23:24 ^ Z
+pair 23:24 ^ Z
+ key 30:30 ^
+ sym 30:34 ^--^ vpxd
+pair 30:34 ^--^ vpxd
+ key 35:35 ^
+ num 35:40 ^---^ 47413
+ val 35:40 ^---^ 47413
+ grp 35:40 ^---^ 47413
+pair 35:40 ^---^ 47413
+ key 43:43 ^
+ key 43:62 ^-----------------^ Originator@6876 sub
+ sym 63:69 ^----^ vpxLro
+ val 63:69 ^----^ vpxLro
+pair 43:69 ^------------------------^ Originator@6876 sub=vpxLro
+ key 70:74 ^--^ opID
+ sym 75:86 ^---------^ 21fa61e9-3e
+ val 75:86 ^---------^ 21fa61e9-3e
+pair 70:86 ^--------------^ opID=21fa61e9-3e
+ grp 43:86 ^-----------------------------------------^ Originator@6876 sub=vpxLro opID=21fa61e9-3e
+pair 43:86 ^-----------------------------------------^ Originator@6876 sub=vpxLro opID=21fa61e9-3e
+ key 89:89 ^
+ sym 89:95 ^----^ VpxLRO
+ val 89:95 ^----^ VpxLRO
+ grp 89:95 ^----^ VpxLRO
+pair 89:95 ^----^ VpxLRO
+ key 97:97 ^
+ sym 97:99 ^^ --
+pair 97:99 ^^ --
+ key 100:100 ^
+ sym 100:105 ^---^ BEGIN
+pair 100:105 ^---^ BEGIN
+ key 106:106 ^
+ sym 106:116 ^--------^ lro-954041
+pair 106:116 ^--------^ lro-954041
+ key 117:117 ^
+ sym 117:119 ^^ --
+pair 117:119 ^^ --
+ key 120:120 ^
+ sym 120:140 ^------------------^ AuthorizationManager
+pair 120:140 ^------------------^ AuthorizationManager
+ key 141:141 ^
+ sym 141:143 ^^ --
+pair 141:143 ^^ --
+ key 144:144 ^
+ sym 144:195 ^-------------------------------------------------^ vim.AuthorizationManager.hasUserPrivilegeOnEntities
+pair 144:195 ^-------------------------------------------------^ vim.AuthorizationManager.hasUserPrivilegeOnEntities
+ key 196:196 ^
+ sym 196:198 ^^ --
+pair 196:198 ^^ --
+ key 199:199 ^
+uuid 199:235 ^----------------------------------^ 52768da7-4006-3d4a-4917-ee027373630f
+pair 199:235 ^----------------------------------^ 52768da7-4006-3d4a-4917-ee027373630f
+ key 236:236 ^
+uuid 236:272 ^----------------------------------^ 522e0475-8901-e8b8-1eb8-07ec729ac50c
+ val 236:272 ^----------------------------------^ 522e0475-8901-e8b8-1eb8-07ec729ac50c
+ grp 236:272 ^----------------------------------^ 522e0475-8901-e8b8-1eb8-07ec729ac50c
+pair 236:272 ^----------------------------------^ 522e0475-8901-e8b8-1eb8-07ec729ac50c
+msg :2022-06-02T12:26:22.072Z info vpxd[47413] [Originator@6876 sub=vpxLro opID=21fa61e9-3e] [VpxLRO] -- BEGIN lro-954041 -- AuthorizationManager -- vim.AuthorizationManager.hasUserPrivilegeOnEntities -- 52768da7-4006-3d4a-4917-ee027373630f(522e0475-8901-e8b8-1eb8-07ec729ac50c)
+format :2022-06-02T12:26:22.072# info #[#] [#] [#] # # # # # # # # #(#)
+{
+ "col_0": "Z",
+ "col_1": "vpxd",
+ "col_2": [
+ 47413
+ ],
+ "col_3": {
+ "Originator@6876 sub": "vpxLro",
+ "opID": "21fa61e9-3e"
+ },
+ "col_4": [
+ "VpxLRO"
+ ],
+ "col_5": "--",
+ "col_6": "BEGIN",
+ "col_7": "lro-954041",
+ "col_8": "--",
+ "col_9": "AuthorizationManager",
+ "col_10": "--",
+ "col_11": "vim.AuthorizationManager.hasUserPrivilegeOnEntities",
+ "col_12": "--",
+ "col_13": "52768da7-4006-3d4a-4917-ee027373630f",
+ "col_14": [
+ "522e0475-8901-e8b8-1eb8-07ec729ac50c"
+ ]
+}
diff --git a/test/datafile_simple.3 b/test/datafile_simple.3
new file mode 100644
index 0000000..6312683
--- /dev/null
+++ b/test/datafile_simple.3
@@ -0,0 +1,25 @@
+ 1 2 3 4 five six 7
+ key 0:0
+ num 0:1 ^ 1
+pair 0:1 ^ 1
+ key 2:2 ^
+ num 2:3 ^ 2
+pair 2:3 ^ 2
+ key 4:4 ^
+ num 4:5 ^ 3
+pair 4:5 ^ 3
+ key 6:6 ^
+ num 6:7 ^ 4
+pair 6:7 ^ 4
+ key 17:17 ^
+ num 17:18 ^ 7
+pair 17:18 ^ 7
+msg :1 2 3 4 five six 7
+format :# # # # five six #
+{
+ "col_0": 1,
+ "col_1": 2,
+ "col_2": 3,
+ "col_3": 4,
+ "col_4": 7
+}
diff --git a/test/datafile_simple.4 b/test/datafile_simple.4
new file mode 100644
index 0000000..339aa41
--- /dev/null
+++ b/test/datafile_simple.4
@@ -0,0 +1,10 @@
+ the-value: "Hello, World!"
+ key 0:9 ^-------^ the-value
+quot 12:25 ^-----------^ Hello, World!
+ val 12:25 ^-----------^ Hello, World!
+pair 0:25 ^-----------------------^ the-value: "Hello, World!
+msg :the-value: "Hello, World!"
+format :the-value: #
+{
+ "the-value": "Hello, World!"
+}
diff --git a/test/datafile_simple.5 b/test/datafile_simple.5
new file mode 100644
index 0000000..606a498
--- /dev/null
+++ b/test/datafile_simple.5
@@ -0,0 +1,10 @@
+ this is a url: http://www.example.com/foo-bar
+ key 0:13 ^-----------^ this is a url
+ url 15:45 ^----------------------------^ http://www.example.com/foo-bar
+ val 15:45 ^----------------------------^ http://www.example.com/foo-bar
+pair 0:45 ^-------------------------------------------^ this is a url: http://www.example.com/foo-bar
+msg :this is a url: http://www.example.com/foo-bar
+format :this is a url: #
+{
+ "this is a url": "http://www.example.com/foo-bar"
+}
diff --git a/test/datafile_simple.6 b/test/datafile_simple.6
new file mode 100644
index 0000000..15cfab1
--- /dev/null
+++ b/test/datafile_simple.6
@@ -0,0 +1,25 @@
+ qualified:name: foo=1 bar=2
+ key 0:0
+word 0:9 ^-------^ qualified
+ val 0:9 ^-------^ qualified
+pair 0:9 ^-------^ qualified
+ key 10:10 ^
+word 10:14 ^--^ name
+ val 10:14 ^--^ name
+pair 10:14 ^--^ name
+ key 16:19 ^-^ foo
+ num 20:21 ^ 1
+ val 20:21 ^ 1
+pair 16:21 ^---^ foo=1
+ key 22:25 ^-^ bar
+ num 26:27 ^ 2
+ val 26:27 ^ 2
+pair 22:27 ^---^ bar=2
+msg :qualified:name: foo=1 bar=2
+format :#:#: foo=# bar=#
+{
+ "col_0": "qualified",
+ "col_1": "name",
+ "foo": 1,
+ "bar": 2
+}
diff --git a/test/datafile_simple.7 b/test/datafile_simple.7
new file mode 100644
index 0000000..3c0810f
--- /dev/null
+++ b/test/datafile_simple.7
@@ -0,0 +1,18 @@
+ func(arg1="a", arg2="b")
+ key 5:5 ^
+ key 5:9 ^--^ arg1
+quot 11:12 ^ a
+ val 11:12 ^ a
+pair 5:12 ^-----^ arg1="a
+ key 15:19 ^--^ arg2
+quot 21:22 ^ b
+ val 21:22 ^ b
+pair 15:22 ^-----^ arg2="b
+ grp 5:22 ^---------------^ arg1="a", arg2="b
+pair 5:22 ^---------------^ arg1="a", arg2="b
+msg :func(arg1="a", arg2="b")
+format :func(#)
+{
+ "arg1": "a",
+ "arg2": "b"
+}
diff --git a/test/datafile_simple.8 b/test/datafile_simple.8
new file mode 100644
index 0000000..c6363c3
--- /dev/null
+++ b/test/datafile_simple.8
@@ -0,0 +1,44 @@
+ Succeeded authorizing right 'system.privilege.taskport.debug' by client '/usr/libexec/taskgated' [76339] for authorization created by '/usr/libexec/taskgated' [77395] (100003,1)
+ key 29:29 ^
+quot 29:60 ^-----------------------------^ system.privilege.taskport.debug
+pair 29:60 ^-----------------------------^ system.privilege.taskport.debug
+ key 73:73 ^
+quot 73:95 ^--------------------^ /usr/libexec/taskgated
+pair 73:95 ^--------------------^ /usr/libexec/taskgated
+ key 98:98 ^
+ num 98:103 ^---^ 76339
+ val 98:103 ^---^ 76339
+ grp 98:103 ^---^ 76339
+pair 98:103 ^---^ 76339
+ key 135:135 ^
+quot 135:157 ^--------------------^ /usr/libexec/taskgated
+pair 135:157 ^--------------------^ /usr/libexec/taskgated
+ key 160:160 ^
+ num 160:165 ^---^ 77395
+ val 160:165 ^---^ 77395
+ grp 160:165 ^---^ 77395
+pair 160:165 ^---^ 77395
+ key 168:168 ^
+ num 168:174 ^----^ 100003
+ val 168:174 ^----^ 100003
+ num 175:176 ^ 1
+ val 175:176 ^ 1
+ grp 168:176 ^------^ 100003,1
+pair 168:176 ^------^ 100003,1
+msg :Succeeded authorizing right 'system.privilege.taskport.debug' by client '/usr/libexec/taskgated' [76339] for authorization created by '/usr/libexec/taskgated' [77395] (100003,1)
+format :Succeeded authorizing right # by client # [#] for authorization created by # [#] (#)
+{
+ "col_0": "system.privilege.taskport.debug",
+ "col_1": "/usr/libexec/taskgated",
+ "col_2": [
+ 76339
+ ],
+ "col_3": "/usr/libexec/taskgated",
+ "col_4": [
+ 77395
+ ],
+ "col_5": [
+ 100003,
+ 1
+ ]
+}
diff --git a/test/datafile_simple.9 b/test/datafile_simple.9
new file mode 100644
index 0000000..85513b9
--- /dev/null
+++ b/test/datafile_simple.9
@@ -0,0 +1,17 @@
+ 12:01 12:01:22 12:01:22.123
+ key 0:0
+time 0:5 ^---^ 12:01
+pair 0:5 ^---^ 12:01
+ key 7:7 ^
+time 7:15 ^------^ 12:01:22
+pair 7:15 ^------^ 12:01:22
+ key 17:17 ^
+time 17:29 ^----------^ 12:01:22.123
+pair 17:29 ^----------^ 12:01:22.123
+msg :12:01 12:01:22 12:01:22.123
+format :# # #
+{
+ "col_0": "12:01",
+ "col_1": "12:01:22",
+ "col_2": "12:01:22.123"
+}
diff --git a/test/datafile_syslog.0 b/test/datafile_syslog.0
new file mode 100644
index 0000000..d37c5cc
--- /dev/null
+++ b/test/datafile_syslog.0
@@ -0,0 +1,23 @@
+ timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
+ key 0:0
+word 0:8 ^------^ timstack
+ val 0:8 ^------^ timstack
+pair 0:8 ^------^ timstack
+ key 11:14 ^-^ TTY
+ sym 15:20 ^---^ pts/6
+ val 15:20 ^---^ pts/6
+pair 11:20 ^-------^ TTY=pts/6
+ key 23:26 ^-^ PWD
+path 27:60 ^-------------------------------^ /auto/wstimstack/rpms/lbuild/test
+ val 27:60 ^-------------------------------^ /auto/wstimstack/rpms/lbuild/test
+pair 23:60 ^-----------------------------------^ PWD=/auto/wstimstack/rpms/lbuild/test
+ key 63:67 ^--^ USER
+word 68:72 ^--^ root
+ val 68:72 ^--^ root
+pair 63:72 ^-------^ USER=root
+ key 75:82 ^-----^ COMMAND
+path 83:96 ^-----------^ /usr/bin/tail
+wspc 96:97 ^
+path 97:114 ^---------------^ /var/log/messages
+ val 83:114 ^-----------------------------^ /usr/bin/tail /var/log/messages
+pair 75:114 ^-------------------------------------^ COMMAND=/usr/bin/tail /var/log/messages
diff --git a/test/datafile_syslog.1 b/test/datafile_syslog.1
new file mode 100644
index 0000000..928f11a
--- /dev/null
+++ b/test/datafile_syslog.1
@@ -0,0 +1,21 @@
+ INSERT-HANG-DETECTED: Tx time:3.093364, # of Inserts: 89, # of bytes written: 465365, Did shrink: NO
+ key 0:20 ^------------------^ INSERT-HANG-DETECTED
+word 22:24 ^^ Tx
+ val 22:24 ^^ Tx
+pair 0:24 ^----------------------^ INSERT-HANG-DETECTED: Tx
+ key 25:29 ^--^ time
+ num 30:38 ^------^ 3.093364
+ val 30:38 ^------^ 3.093364
+pair 25:38 ^-----------^ time:3.093364
+ key 40:52 ^----------^ # of Inserts
+ num 54:56 ^^ 89
+ val 54:56 ^^ 89
+pair 40:56 ^--------------^ # of Inserts: 89
+ key 58:76 ^----------------^ # of bytes written
+ num 78:84 ^----^ 465365
+ val 78:84 ^----^ 465365
+pair 58:84 ^------------------------^ # of bytes written: 465365
+ key 86:96 ^--------^ Did shrink
+ sym 98:100 ^^ NO
+ val 98:100 ^^ NO
+pair 86:100 ^------------^ Did shrink: NO
diff --git a/test/datafile_xml.0 b/test/datafile_xml.0
new file mode 100644
index 0000000..9b5a102
--- /dev/null
+++ b/test/datafile_xml.0
@@ -0,0 +1,26 @@
+ <ns1:foo> <elem attr1=xyz attr2="123"> </elem> <closed />
+ key 0:0
+xmlo 0:9 ^-------^ <ns1:foo>
+pair 0:9 ^-------^ <ns1:foo>
+ key 12:12 ^
+xmlo 12:40 ^--------------------------^ <elem attr1=xyz attr2="123">
+pair 12:40 ^--------------------------^ <elem attr1=xyz attr2="123">
+ key 42:42 ^
+xmlc 42:49 ^-----^ </elem>
+pair 42:49 ^-----^ </elem>
+ key 51:51 ^
+xmlt 51:61 ^--------^ <closed />
+pair 51:61 ^--------^ <closed />
+msg :<ns1:foo> <elem attr1=xyz attr2="123"> </elem> <closed />
+format :# # # #
+
+--
+<ns1:foo>
+ <elem attr1=xyz attr2="123"> </elem>
+ <closed />
+{
+ "col_0": "<ns1:foo>",
+ "col_1": "<elem attr1=xyz attr2=\"123\">",
+ "col_2": "</elem>",
+ "col_3": "<closed />"
+}
diff --git a/test/dhcp-trunc.pcapng b/test/dhcp-trunc.pcapng
new file mode 100644
index 0000000..00734ba
--- /dev/null
+++ b/test/dhcp-trunc.pcapng
Binary files differ
diff --git a/test/dhcp.pcapng b/test/dhcp.pcapng
new file mode 100644
index 0000000..530c64c
--- /dev/null
+++ b/test/dhcp.pcapng
Binary files differ
diff --git a/test/document.sections.tests.cc b/test/document.sections.tests.cc
new file mode 100644
index 0000000..8e88b75
--- /dev/null
+++ b/test/document.sections.tests.cc
@@ -0,0 +1,170 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "doctest/doctest.h"
+#include "document.sections.hh"
+
+TEST_CASE("lnav::document::sections::basics")
+{
+ attr_line_t INPUT = R"(
+{
+ "msg": "Hello, World!",
+ "obj": {
+ "a": 1,
+ "b": "Two",
+ "c": 3.0
+ },
+ "arr": [1, 2, 3],
+ "arr2": [
+ 456,
+ 789,
+ {
+ "def": 123,
+ "ghi": null,
+ "jkl": "other"
+ },
+ {
+ "def": 456,
+ "ghi": null,
+ "jkl": "OTHER"
+ },
+ {
+ "def": 789,
+ "ghi": null,
+ "jkl": "OtHeR"
+ }
+ ]
+}
+)";
+
+ auto meta = lnav::document::discover_structure(INPUT, line_range{0, -1});
+
+ meta.m_sections_tree.visit_all([](const auto& intv) {
+ auto ser = intv.value.match(
+ [](const std::string& name) { return name; },
+ [](const size_t index) { return fmt::format("{}", index); });
+ printf("interval %d:%d %s\n", intv.start, intv.stop, ser.c_str());
+ });
+ lnav::document::hier_node::depth_first(
+ meta.m_sections_root.get(), [](const auto* node) {
+ printf("node %p %d\n", node, node->hn_start);
+ for (const auto& pair : node->hn_named_children) {
+ printf(" child: %p %s\n", pair.second, pair.first.c_str());
+ }
+ });
+}
+
+TEST_CASE("lnav::document::sections::empty")
+{
+ attr_line_t INPUT
+ = R"(SOCKET 1 (10) creating new listening socket on port -1)";
+
+ auto meta = lnav::document::discover_structure(INPUT, line_range{0, -1});
+
+ meta.m_sections_tree.visit_all([](const auto& intv) {
+ auto ser = intv.value.match(
+ [](const std::string& name) { return name; },
+ [](const size_t index) { return fmt::format("{}", index); });
+ printf("interval %d:%d %s\n", intv.start, intv.stop, ser.c_str());
+ });
+ lnav::document::hier_node::depth_first(
+ meta.m_sections_root.get(), [](const auto* node) {
+ printf("node %p %d\n", node, node->hn_start);
+ for (const auto& pair : node->hn_named_children) {
+ printf(" child: %p %s\n", pair.second, pair.first.c_str());
+ }
+ });
+}
+
+TEST_CASE("lnav::document::sections::doc")
+{
+ attr_line_t INPUT = R"(
+
+NAME
+ foo -- bar
+
+SYNOPSIS
+ foo -o -b
+
+DESCRIPTION
+ Lorem ipsum
+
+ AbcDef
+ Lorem ipsum
+
+)";
+
+ auto meta = lnav::document::discover_structure(INPUT, line_range{0, -1});
+
+ CHECK(meta.m_sections_root->hn_named_children.size() == 3);
+ meta.m_sections_tree.visit_all([](const auto& intv) {
+ auto ser = intv.value.match(
+ [](const std::string& name) { return name; },
+ [](const size_t index) { return fmt::format("{}", index); });
+ printf("interval %d:%d %s\n", intv.start, intv.stop, ser.c_str());
+ });
+ lnav::document::hier_node::depth_first(
+ meta.m_sections_root.get(), [](const auto* node) {
+ printf("node %p %d\n", node, node->hn_start);
+ for (const auto& pair : node->hn_named_children) {
+ printf(" child: %p %s\n", pair.second, pair.first.c_str());
+ }
+ });
+}
+
+TEST_CASE("lnav::document::sections::sql")
+{
+ attr_line_t INPUT
+ = R"(2022-06-03T22:05:58.186Z verbose -[35642] [Originator@6876 sub=Default] [VdbStatement]Executing SQL:
+--> INSERT INTO PM_CLUSTER_DRAFT_VALIDATION_STATE
+--> (draft_id, errors, hosts) VALUES (?::integer, ?::jsonb, ARRAY[]::text[])
+--> ON CONFLICT (draft_id) DO UPDATE
+--> SET errors = EXCLUDED.errors, hosts = EXCLUDED.hosts
+-->
+)";
+
+ auto meta = lnav::document::discover_structure(INPUT, line_range{0, -1});
+
+ meta.m_sections_tree.visit_all([](const auto& intv) {
+ auto ser = intv.value.match(
+ [](const std::string& name) { return name; },
+ [](const size_t index) { return fmt::format("{}", index); });
+ printf("interval %d:%d %s\n", intv.start, intv.stop, ser.c_str());
+ });
+ lnav::document::hier_node::depth_first(
+ meta.m_sections_root.get(), [](const auto* node) {
+ printf("node %p %d\n", node, node->hn_start);
+ for (const auto& pair : node->hn_named_children) {
+ printf(" child: %p %s\n", pair.second, pair.first.c_str());
+ }
+ });
+}
diff --git a/test/drive_data_scanner.cc b/test/drive_data_scanner.cc
new file mode 100644
index 0000000..a7a0fb7
--- /dev/null
+++ b/test/drive_data_scanner.cc
@@ -0,0 +1,312 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifdef __CYGWIN__
+# include <alloca.h>
+#endif
+
+#include <fstream>
+#include <iostream>
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "base/injector.hh"
+#include "config.h"
+#include "data_parser.hh"
+#include "data_scanner.hh"
+#include "elem_to_json.hh"
+#include "log_format.hh"
+#include "log_format_loader.hh"
+#include "logfile.hh"
+#include "pretty_printer.hh"
+#include "shared_buffer.hh"
+#include "view_curses.hh"
+
+const char* TMP_NAME = "scanned.tmp";
+
+int
+main(int argc, char* argv[])
+{
+ int c, retval = EXIT_SUCCESS;
+ bool prompt = false, is_log = false, pretty_print = false;
+ bool scanner_details = false;
+
+ {
+ static auto builtin_formats
+ = injector::get<std::vector<std::shared_ptr<log_format>>>();
+ auto& root_formats = log_format::get_root_formats();
+
+ log_format::get_root_formats().insert(root_formats.begin(),
+ builtin_formats.begin(),
+ builtin_formats.end());
+ builtin_formats.clear();
+ }
+
+ {
+ std::vector<ghc::filesystem::path> paths;
+ std::vector<lnav::console::user_message> errors;
+
+ load_formats(paths, errors);
+ }
+
+ while ((c = getopt(argc, argv, "pPls")) != -1) {
+ switch (c) {
+ case 'p':
+ prompt = true;
+ break;
+
+ case 'P':
+ pretty_print = true;
+ break;
+
+ case 'l':
+ is_log = true;
+ break;
+
+ case 's':
+ scanner_details = true;
+ break;
+
+ default:
+ retval = EXIT_FAILURE;
+ break;
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (retval != EXIT_SUCCESS) {
+ } else if (argc < 1) {
+ fprintf(stderr, "error: expecting file name argument(s)\n");
+ retval = EXIT_FAILURE;
+ } else {
+ for (int lpc = 0; lpc < argc; lpc++) {
+ std::unique_ptr<std::ifstream> in_ptr;
+ std::istream* in;
+ FILE* out;
+
+ if (strcmp(argv[lpc], "-") == 0) {
+ in = &std::cin;
+ } else {
+ auto ifs = std::make_unique<std::ifstream>(argv[lpc]);
+
+ if (!ifs->is_open()) {
+ fprintf(stderr, "error: unable to open file\n");
+ retval = EXIT_FAILURE;
+ } else {
+ in_ptr = std::move(ifs);
+ in = in_ptr.get();
+ }
+ }
+
+ if ((out = fopen(TMP_NAME, "w")) == nullptr) {
+ fprintf(stderr,
+ "error: unable to temporary file for writing\n");
+ retval = EXIT_FAILURE;
+ } else {
+ std::shared_ptr<log_format> format;
+ char* log_line;
+ bool found = false;
+ char cmd[2048];
+ std::string line;
+ int rc;
+
+ getline(*in, line);
+ if (strcmp(argv[lpc], "-") == 0) {
+ line = " " + line;
+ }
+
+ log_line = (char*) alloca(line.length());
+ strcpy(log_line, &line[13]);
+ auto sub_line = line.substr(13);
+ struct line_range body(0, sub_line.length());
+ shared_buffer share_manager;
+ logline_value_vector ll_values;
+ auto& sbr = ll_values.lvv_sbr;
+
+ sbr.share(
+ share_manager, (char*) sub_line.c_str(), sub_line.size());
+
+ auto& root_formats = log_format::get_root_formats();
+ std::vector<std::shared_ptr<log_format>>::iterator iter;
+
+ if (is_log) {
+ std::vector<logline> index;
+ logfile_open_options loo;
+ auto open_res = logfile::open(argv[lpc], loo);
+ auto lf = open_res.unwrap();
+ ArenaAlloc::Alloc<char> allocator;
+ scan_batch_context sbc{allocator};
+ for (iter = root_formats.begin();
+ iter != root_formats.end() && !found;
+ ++iter)
+ {
+ line_info li = {{13}};
+
+ (*iter)->clear();
+ if ((*iter)
+ ->scan(*lf, index, li, sbr, sbc)
+ .is<log_format::scan_match>())
+ {
+ format = (*iter)->specialized();
+ found = true;
+ }
+ }
+
+ if (!found) {
+ fprintf(stderr, "error: log sample does not match\n");
+ return EXIT_FAILURE;
+ }
+ }
+
+ string_attrs_t sa;
+
+ if (format.get() != nullptr) {
+ format->annotate(0, sa, ll_values);
+ body = find_string_attr_range(sa, &SA_BODY);
+ }
+
+ data_parser::TRACE_FILE = fopen("scanned.dpt", "w");
+
+ data_scanner ds(sub_line, body.lr_start);
+
+ if (scanner_details) {
+ fprintf(out,
+ " %s\n",
+ ds.get_input().to_string().c_str());
+ while (true) {
+ auto tok_res = ds.tokenize2();
+
+ if (!tok_res) {
+ break;
+ }
+
+ fprintf(out,
+ "%4s %3d:%-3d ",
+ data_scanner::token2name(tok_res->tr_token),
+ tok_res->tr_capture.c_begin,
+ tok_res->tr_capture.c_end);
+ size_t cap_index = 0;
+ for (; cap_index < tok_res->tr_capture.c_end;
+ cap_index++)
+ {
+ if (cap_index == tok_res->tr_capture.c_begin) {
+ fputc('^', out);
+ } else if (cap_index
+ == (tok_res->tr_capture.c_end - 1))
+ {
+ fputc('^', out);
+ } else if (cap_index > tok_res->tr_capture.c_begin)
+ {
+ fputc('-', out);
+ } else {
+ fputc(' ', out);
+ }
+ }
+ for (; cap_index < (int) ds.get_input().length();
+ cap_index++)
+ {
+ fputc(' ', out);
+ }
+
+ auto sub = tok_res->to_string();
+ fprintf(out, " %s\n", sub.c_str());
+ }
+ }
+
+ ds.reset();
+ data_parser dp(&ds);
+ std::string msg_format;
+
+ dp.dp_msg_format = &msg_format;
+ dp.parse();
+ dp.print(out, dp.dp_pairs);
+ fprintf(
+ out, "msg :%s\n", sub_line.c_str() + body.lr_start);
+ fprintf(out, "format :%s\n", msg_format.c_str());
+
+ if (pretty_print) {
+ data_scanner ds2(sub_line, body.lr_start);
+ pretty_printer pp(&ds2, sa);
+ attr_line_t pretty_out;
+
+ pp.append_to(pretty_out);
+ fprintf(out, "\n--\n%s", pretty_out.get_string().c_str());
+ }
+
+ auto_mem<yajl_gen_t> gen(yajl_gen_free);
+
+ gen = yajl_gen_alloc(nullptr);
+ yajl_gen_config(gen.in(), yajl_gen_beautify, true);
+
+ elements_to_json(gen, dp, &dp.dp_pairs);
+
+ const unsigned char* buf;
+ size_t len;
+
+ yajl_gen_get_buf(gen, &buf, &len);
+ fwrite(buf, 1, len, out);
+
+ fclose(out);
+
+ snprintf(
+ cmd, sizeof(cmd), "diff -u %s %s", argv[lpc], TMP_NAME);
+ rc = system(cmd);
+ if (rc != 0) {
+ if (prompt) {
+ char resp[4];
+
+ printf("\nOriginal line:\n%s\n",
+ sub_line.c_str() + body.lr_start);
+ printf(
+ "Would you like to update the original file? "
+ "(y/N) ");
+ fflush(stdout);
+ log_perror(scanf("%3s", resp));
+ if (strcasecmp(resp, "y") == 0) {
+ rename(TMP_NAME, argv[lpc]);
+ } else {
+ retval = EXIT_FAILURE;
+ }
+ } else {
+ fprintf(stderr, "error: mismatch\n");
+ retval = EXIT_FAILURE;
+ }
+ }
+
+ fclose(data_parser::TRACE_FILE);
+ data_parser::TRACE_FILE = nullptr;
+ }
+ }
+ }
+
+ return retval;
+}
diff --git a/test/drive_grep_proc.cc b/test/drive_grep_proc.cc
new file mode 100644
index 0000000..4118f43
--- /dev/null
+++ b/test/drive_grep_proc.cc
@@ -0,0 +1,156 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "grep_proc.hh"
+#include "line_buffer.hh"
+#include "listview_curses.hh"
+
+using namespace std;
+
+class my_source : public grep_proc_source<vis_line_t> {
+public:
+ my_source(auto_fd& fd) { this->ms_buffer.set_fd(fd); };
+
+ bool grep_value_for_line(vis_line_t line_number, string& value_out)
+ {
+ bool retval = false;
+
+ try {
+ auto load_result = this->ms_buffer.load_next_line(this->ms_range);
+
+ if (load_result.isOk()) {
+ auto li = load_result.unwrap();
+
+ this->ms_range = li.li_file_range;
+ if (!li.li_file_range.empty()) {
+ auto read_result
+ = this->ms_buffer.read_range(li.li_file_range)
+ .then([&value_out](auto sbr) {
+ value_out = to_string(sbr);
+ });
+
+ retval = read_result.isOk();
+ }
+ }
+ } catch (const line_buffer::error& e) {
+ fprintf(stderr,
+ "error: source buffer error %d %s\n",
+ this->ms_buffer.get_fd(),
+ strerror(e.e_err));
+ }
+
+ return retval;
+ };
+
+private:
+ line_buffer ms_buffer;
+ file_range ms_range;
+};
+
+class my_sink : public grep_proc_sink<vis_line_t> {
+public:
+ my_sink() : ms_finished(false) {}
+
+ void grep_match(grep_proc<vis_line_t>& gp,
+ vis_line_t line,
+ int start,
+ int end)
+ {
+ printf("%d:%d:%d\n", (int) line, start, end);
+ }
+
+ void grep_capture(grep_proc<vis_line_t>& gp,
+ vis_line_t line,
+ int start,
+ int end,
+ char* capture)
+ {
+ fprintf(stderr, "%d(%d:%d)%s\n", (int) line, start, end, capture);
+ }
+
+ void grep_end(grep_proc<vis_line_t>& gp) { this->ms_finished = true; }
+
+ bool ms_finished;
+};
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ auto_fd fd;
+
+ if (argc < 3) {
+ fprintf(stderr, "error: expecting pattern and file arguments\n");
+ retval = EXIT_FAILURE;
+ } else if ((fd = open(argv[2], O_RDONLY)) == -1) {
+ perror("open");
+ retval = EXIT_FAILURE;
+ } else {
+ auto compile_res = lnav::pcre2pp::code::from(
+ string_fragment::from_c_str(argv[1]), PCRE2_CASELESS);
+
+ if (compile_res.isErr()) {
+ auto ce = compile_res.unwrapErr();
+ fprintf(stderr,
+ "error: invalid pattern -- %s\n",
+ ce.get_message().c_str());
+ } else {
+ auto co = compile_res.unwrap().to_shared();
+ auto psuperv = std::make_shared<pollable_supervisor>();
+ my_source ms(fd);
+ my_sink msink;
+
+ grep_proc<vis_line_t> gp(co, ms, psuperv);
+
+ gp.set_sink(&msink);
+ gp.queue_request();
+ gp.start();
+
+ while (!msink.ms_finished) {
+ vector<struct pollfd> pollfds;
+
+ psuperv->update_poll_set(pollfds);
+ poll(&pollfds[0], pollfds.size(), -1);
+
+ psuperv->check_poll_set(pollfds);
+ }
+ }
+ }
+
+ return retval;
+}
diff --git a/test/drive_line_buffer.cc b/test/drive_line_buffer.cc
new file mode 100644
index 0000000..b283b9c
--- /dev/null
+++ b/test/drive_line_buffer.cc
@@ -0,0 +1,246 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+#include <random>
+#include <tuple>
+#include <vector>
+
+#include <assert.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/mman.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "base/auto_fd.hh"
+#include "base/string_util.hh"
+#include "config.h"
+#include "line_buffer.hh"
+
+using namespace std;
+
+int
+main(int argc, char* argv[])
+{
+ int c, rnd_iters = 5, retval = EXIT_SUCCESS;
+ vector<tuple<int, off_t, ssize_t> > index;
+ auto_fd fd = auto_fd(STDIN_FILENO), fd_cmp;
+ int offseti = 0;
+ off_t offset = 0;
+ int count = 1000;
+ struct stat st;
+
+ while ((c = getopt(argc, argv, "o:i:n:c:")) != -1) {
+ switch (c) {
+ case 'o':
+ if (sscanf(optarg, "%d", &offseti) != 1) {
+ fprintf(stderr,
+ "error: offset is not an integer -- %s\n",
+ optarg);
+ retval = EXIT_FAILURE;
+ } else {
+ offset = offseti;
+ }
+ break;
+ case 'n':
+ if (sscanf(optarg, "%d", &rnd_iters) != 1) {
+ fprintf(stderr,
+ "error: offset is not an integer -- %s\n",
+ optarg);
+ retval = EXIT_FAILURE;
+ }
+ break;
+ case 'c':
+ if (sscanf(optarg, "%d", &count) != 1) {
+ fprintf(stderr,
+ "error: count is not an integer -- %s\n",
+ optarg);
+ retval = EXIT_FAILURE;
+ }
+ break;
+ case 'i': {
+ FILE* file;
+
+ if ((file = fopen(optarg, "r")) == NULL) {
+ perror("open");
+ retval = EXIT_FAILURE;
+ } else {
+ int line_number = 1, line_offset;
+ off_t last_offset;
+ ssize_t line_size;
+
+ while (fscanf(file, "%d", &line_offset) == 1) {
+ if (line_number > 1) {
+ line_size = line_offset - last_offset;
+ index.emplace_back(
+ line_number - 1, last_offset, line_size);
+ }
+ last_offset = line_offset;
+ line_number += 1;
+ }
+ fclose(file);
+ file = NULL;
+ }
+ } break;
+ default:
+ retval = EXIT_FAILURE;
+ break;
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (retval != EXIT_SUCCESS) {
+ } else if ((argc == 0) && (index.size() > 0)) {
+ fprintf(stderr, "error: cannot randomize stdin\n");
+ retval = EXIT_FAILURE;
+ } else if ((argc > 0) && (fd = open(argv[0], O_RDONLY)) == -1) {
+ perror("open");
+ retval = EXIT_FAILURE;
+ } else if ((argc > 0) && (fstat(fd, &st) == -1)) {
+ perror("fstat");
+ retval = EXIT_FAILURE;
+ } else if ((argc > 1) && (fd_cmp = open(argv[1], O_RDONLY)) == -1) {
+ perror("open-cmp");
+ retval = EXIT_FAILURE;
+ } else if ((argc > 1) && (fstat(fd_cmp, &st) == -1)) {
+ perror("fstat-cmp");
+ retval = EXIT_FAILURE;
+ } else {
+ try {
+ file_range last_range{offset};
+ line_buffer lb;
+ char* maddr;
+
+ int fd2 = (argc > 1) ? fd_cmp.get() : fd.get();
+ assert(fd2 >= 0);
+ lb.set_fd(fd);
+ if (index.size() == 0) {
+ while (count) {
+ auto load_result = lb.load_next_line(last_range);
+
+ if (load_result.isErr()) {
+ break;
+ }
+
+ auto li = load_result.unwrap();
+
+ if (li.li_file_range.empty()) {
+ break;
+ }
+
+ auto read_result = lb.read_range(li.li_file_range);
+
+ if (read_result.isErr()) {
+ break;
+ }
+
+ auto sbr = read_result.unwrap();
+
+ if (!li.li_utf8_scan_result.is_valid()) {
+ scrub_to_utf8(sbr.get_writable_data(), sbr.length());
+ }
+
+ printf("%.*s", (int) sbr.length(), sbr.get_data());
+ if ((off_t) (li.li_file_range.fr_offset
+ + li.li_file_range.fr_size)
+ < offset)
+ {
+ printf("\n");
+ }
+ last_range = li.li_file_range;
+ count -= 1;
+ }
+ } else if ((maddr = (char*) mmap(NULL,
+ st.st_size,
+ PROT_READ,
+ MAP_FILE | MAP_PRIVATE,
+ fd2,
+ 0))
+ == MAP_FAILED)
+ {
+ perror("mmap");
+ retval = EXIT_FAILURE;
+ } else {
+ file_range range;
+
+ while (true) {
+ auto load_result = lb.load_next_line(range);
+
+ if (load_result.isErr()) {
+ return EXIT_FAILURE;
+ }
+
+ auto li = load_result.unwrap();
+
+ range = li.li_file_range;
+
+ if (range.empty()) {
+ break;
+ }
+ }
+ do {
+ size_t lpc;
+
+ std::random_device rd;
+ std::mt19937 g(rd());
+ std::shuffle(index.begin(), index.end(), g);
+ for (lpc = 0; lpc < index.size(); lpc++) {
+ const auto& index_tuple = index[lpc];
+
+ auto read_result = lb.read_range(
+ {get<1>(index_tuple), get<2>(index_tuple)});
+
+ assert(read_result.isOk());
+
+ auto sbr = read_result.unwrap();
+
+ assert(memcmp(sbr.get_data(),
+ &maddr[get<1>(index_tuple)],
+ sbr.length())
+ == 0);
+ }
+
+ rnd_iters -= 1;
+ } while (rnd_iters);
+
+ printf("All done\n");
+ }
+ } catch (line_buffer::error& e) {
+ fprintf(stderr, "error: %s\n", strerror(e.e_err));
+ retval = EXIT_FAILURE;
+ }
+ }
+
+ return retval;
+}
diff --git a/test/drive_listview.cc b/test/drive_listview.cc
new file mode 100644
index 0000000..76d3df8
--- /dev/null
+++ b/test/drive_listview.cc
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "listview_curses.hh"
+
+using namespace std;
+
+static listview_curses lv;
+
+class my_source : public list_data_source {
+public:
+ my_source() : ms_rows(2){};
+
+ size_t listview_rows(const listview_curses& lv)
+ {
+ return this->ms_rows;
+ };
+
+ void listview_value_for_rows(const listview_curses& lv,
+ vis_line_t row,
+ vector<attr_line_t>& rows)
+ {
+ for (auto& value_out : rows) {
+ value_out = (lv.is_selectable() && row == lv.get_selection()) ? "+"
+ : "";
+
+ if (row == 0) {
+ value_out.al_string += "Hello";
+ } else if (row == 1) {
+ value_out.al_string += "World!";
+ } else if (row < this->ms_rows) {
+ value_out.al_string += std::to_string(static_cast<int>(row));
+ } else {
+ assert(0);
+ }
+ ++row;
+ }
+ };
+
+ size_t listview_size_for_row(const listview_curses& lv, vis_line_t row)
+ {
+ return 100;
+ };
+
+ bool attrline_next_token(const view_curses& vc,
+ int line,
+ struct line_range& lr,
+ int& attrs_out)
+ {
+ return false;
+ };
+
+ int ms_rows;
+};
+
+int
+main(int argc, char* argv[])
+{
+ int c, retval = EXIT_SUCCESS;
+ bool wait_for_input = false, set_height = false;
+ my_source ms;
+ WINDOW* win;
+
+ win = initscr();
+ lv.set_data_source(&ms);
+ lv.set_window(win);
+ noecho();
+
+ while ((c = getopt(argc, argv, "cy:t:k:l:r:h:w")) != -1) {
+ switch (c) {
+ case 'c':
+ // Enable cursor mode
+ lv.set_selectable(true);
+ break;
+ case 'y':
+ lv.set_y(atoi(optarg));
+ break;
+ case 'h':
+ lv.set_height(vis_line_t(atoi(optarg)));
+ set_height = true;
+ break;
+ case 'k': {
+ // Treats the string argument as sequence of key presses (only
+ // individual characters supported as key input)
+ for (char* ptr = optarg; ptr != nullptr && *ptr != '\0'; ++ptr)
+ {
+ lv.handle_key(static_cast<int>(*ptr));
+ }
+ break;
+ }
+ case 't':
+ lv.set_selection(vis_line_t(atoi(optarg)));
+ break;
+ case 'l':
+ lv.set_left(atoi(optarg));
+ break;
+ case 'w':
+ wait_for_input = true;
+ break;
+ case 'r':
+ ms.ms_rows = atoi(optarg);
+ break;
+ }
+ }
+
+ if (!set_height) {
+ unsigned long height, width;
+ getmaxyx(win, height, width);
+ lv.set_height(vis_line_t(height - lv.get_y()));
+ }
+
+ lv.do_update();
+ refresh();
+ if (wait_for_input) {
+ getch();
+ }
+ endwin();
+
+ return retval;
+}
diff --git a/test/drive_logfile.cc b/test/drive_logfile.cc
new file mode 100644
index 0000000..2da37dd
--- /dev/null
+++ b/test/drive_logfile.cc
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "base/injector.hh"
+#include "base/opt_util.hh"
+#include "config.h"
+#include "log_format.hh"
+#include "log_format_loader.hh"
+#include "logfile.hh"
+
+using namespace std;
+
+typedef enum {
+ MODE_NONE,
+ MODE_ECHO,
+ MODE_LINE_COUNT,
+ MODE_TIMES,
+ MODE_LEVELS,
+} dl_mode_t;
+
+time_t
+time(time_t* _unused)
+{
+ return 1194107018;
+}
+
+int
+main(int argc, char* argv[])
+{
+ int c, retval = EXIT_SUCCESS;
+ dl_mode_t mode = MODE_NONE;
+ string expected_format;
+
+ {
+ static auto builtin_formats
+ = injector::get<std::vector<std::shared_ptr<log_format>>>();
+ auto& root_formats = log_format::get_root_formats();
+
+ log_format::get_root_formats().insert(root_formats.begin(),
+ builtin_formats.begin(),
+ builtin_formats.end());
+ builtin_formats.clear();
+ }
+
+ {
+ std::vector<lnav::console::user_message> errors;
+ vector<ghc::filesystem::path> paths;
+
+ getenv_opt("test_dir") |
+ [&paths](auto value) { paths.template emplace_back(value); };
+ load_formats(paths, errors);
+ }
+
+ while ((c = getopt(argc, argv, "ef:ltv")) != -1) {
+ switch (c) {
+ case 'f':
+ expected_format = optarg;
+ break;
+ case 'e':
+ mode = MODE_ECHO;
+ break;
+ case 'l':
+ mode = MODE_LINE_COUNT;
+ break;
+ case 't':
+ mode = MODE_TIMES;
+ break;
+ case 'v':
+ mode = MODE_LEVELS;
+ break;
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (retval == EXIT_FAILURE) {
+ } else if (argc == 0) {
+ fprintf(stderr, "error: expecting log file name\n");
+ } else {
+ logfile_open_options default_loo;
+ auto open_res = logfile::open(argv[0], default_loo);
+
+ if (open_res.isErr()) {
+ fprintf(stderr,
+ "unable to open logfile: %s\n",
+ open_res.unwrapErr().c_str());
+ return EXIT_FAILURE;
+ }
+
+ auto lf = open_res.unwrap();
+ struct stat st;
+
+ stat(argv[0], &st);
+ assert(strcmp(argv[0], lf->get_filename().c_str()) == 0);
+
+ lf->rebuild_index();
+ assert(!lf->is_closed());
+ lf->rebuild_index();
+ assert(!lf->is_closed());
+ lf->rebuild_index();
+ assert(!lf->is_closed());
+ assert(lf->get_activity().la_polls == 3);
+ if (expected_format.empty()) {
+ assert(lf->get_format() == nullptr);
+ } else {
+ // printf("%s %s\n", lf->get_format()->get_name().c_str(),
+ // expected_format.c_str());
+ assert(lf->get_format() != nullptr);
+ assert(lf->get_format()->get_name().to_string() == expected_format);
+ }
+ if (!lf->is_compressed()) {
+ assert(lf->get_modified_time() == st.st_mtime);
+ }
+
+ switch (mode) {
+ case MODE_NONE:
+ break;
+ case MODE_ECHO:
+ for (auto iter = lf->begin(); iter != lf->end(); ++iter) {
+ auto sbr = lf->read_line(iter).unwrap();
+
+ printf("%.*s\n", (int) sbr.length(), sbr.get_data());
+ }
+ break;
+ case MODE_LINE_COUNT:
+ printf("%zd\n", lf->size());
+ break;
+ case MODE_TIMES:
+ for (auto& iter : *lf) {
+ if (iter.is_ignored()) {
+ continue;
+ }
+
+ char buffer[1024];
+ time_t lt;
+
+ lt = iter.get_time();
+ strftime(buffer,
+ sizeof(buffer),
+ "%b %d %H:%M:%S %Y",
+ gmtime(&lt));
+ printf("%s -- %03d\n", buffer, iter.get_millis());
+ }
+ break;
+ case MODE_LEVELS:
+ for (auto& iter : *lf) {
+ log_level_t level = iter.get_level_and_flags();
+ printf("%s 0x%x\n",
+ level_names[level & ~LEVEL__FLAGS],
+ level & LEVEL__FLAGS);
+ }
+ break;
+ }
+ }
+
+ return retval;
+}
diff --git a/test/drive_mvwattrline.cc b/test/drive_mvwattrline.cc
new file mode 100644
index 0000000..1b6fc06
--- /dev/null
+++ b/test/drive_mvwattrline.cc
@@ -0,0 +1,119 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "view_curses.hh"
+
+int
+main(int argc, char* argv[])
+{
+ int c, retval = EXIT_SUCCESS;
+ bool wait_for_input = false;
+
+ while ((c = getopt(argc, argv, "w")) != -1) {
+ switch (c) {
+ case 'w':
+ wait_for_input = true;
+ break;
+ }
+ }
+
+ setenv("LANG", "en_US.UTF-8", 1);
+ setlocale(LC_ALL, "");
+
+ {
+ auto sc = screen_curses::create().unwrap();
+ WINDOW* win = sc.get_window();
+ struct line_range lr(0, 40);
+ attr_line_t al;
+ int y = 0;
+
+ curs_set(0);
+ noecho();
+ view_colors::singleton().init(false);
+
+ al.with_string("Plain text");
+ view_curses::mvwattrline(win, y++, 0, al, lr);
+
+ al.clear()
+ .with_string("\tLeading tab")
+ .with_attr(string_attr(line_range(0, 1),
+ VC_STYLE.value(text_attrs{A_REVERSE})));
+ view_curses::mvwattrline(win, y++, 0, al, lr);
+
+ al.clear()
+ .with_string("Tab\twith text")
+ .with_attr(string_attr(line_range(1, 4),
+ VC_STYLE.value(text_attrs{A_REVERSE})));
+ view_curses::mvwattrline(win, y++, 0, al, lr);
+
+ al.clear()
+ .with_string("Tab\twith text #2")
+ .with_attr(string_attr(line_range(3, 4),
+ VC_STYLE.value(text_attrs{A_REVERSE})));
+ view_curses::mvwattrline(win, y++, 0, al, lr);
+
+ al.clear()
+ .with_string("Two\ttabs\twith text")
+ .with_attr(string_attr(line_range(4, 6),
+ VC_STYLE.value(text_attrs{A_REVERSE})))
+ .with_attr(string_attr(line_range(9, 13),
+ VC_STYLE.value(text_attrs{A_REVERSE})));
+ view_curses::mvwattrline(win, y++, 0, al, lr);
+
+ al.clear()
+ .with_string("Text with mixed attributes.")
+ .with_attr(string_attr(
+ line_range(5, 9),
+ VC_STYLE.value(text_attrs{0, COLOR_RED, COLOR_BLACK})))
+ .with_attr(string_attr(line_range(7, 12),
+ VC_STYLE.value(text_attrs{A_REVERSE})));
+ view_curses::mvwattrline(win, y++, 0, al, lr);
+
+ const char* text = u8"Text with unicode ▶ characters";
+ int offset = strstr(text, "char") - text;
+ al.clear().with_string(text).with_attr(
+ string_attr(line_range(offset, offset + 4),
+ VC_STYLE.value(text_attrs{A_REVERSE})));
+ view_curses::mvwattrline(win, y++, 0, al, lr);
+
+ wmove(win, y, 0);
+ refresh();
+ if (wait_for_input) {
+ getch();
+ }
+ }
+
+ return retval;
+}
diff --git a/test/drive_readline_curses.cc b/test/drive_readline_curses.cc
new file mode 100644
index 0000000..54dfebc
--- /dev/null
+++ b/test/drive_readline_curses.cc
@@ -0,0 +1,153 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#ifdef HAVE_SYS_TTYDEFAULTS_H
+# include <sys/ttydefaults.h>
+#endif
+
+#include <algorithm>
+
+#include "../src/lnav_util.hh"
+#include "lnav_util.hh"
+#include "readline_curses.hh"
+
+using namespace std;
+
+static readline_context::command_map_t COMMANDS;
+
+static struct {
+ bool dd_active;
+ readline_curses* dd_rl_view;
+ volatile sig_atomic_t dd_looping;
+} drive_data;
+
+static void
+rl_callback(readline_curses* rc)
+{
+ string line = rc->get_value().get_string();
+
+ if (line == "quit")
+ drive_data.dd_looping = false;
+ fprintf(stderr, "callback\n");
+ drive_data.dd_active = false;
+}
+
+static void
+rl_timeout(readline_curses* rc)
+{
+ fprintf(stderr, "timeout\n");
+}
+
+int
+main(int argc, char* argv[])
+{
+ int c, fd, retval = EXIT_SUCCESS;
+
+ fd = open("/tmp/lnav.err", O_WRONLY | O_CREAT | O_APPEND, 0666);
+ dup2(fd, STDERR_FILENO);
+ close(fd);
+ fprintf(stderr, "startup\n");
+
+ while ((c = getopt(argc, argv, "h")) != -1) {
+ switch (c) {
+ case 'h':
+ break;
+ default:
+ break;
+ }
+ }
+
+ auto psuperv = std::make_shared<pollable_supervisor>();
+ readline_context context("test", &COMMANDS);
+ readline_curses rlc(psuperv);
+
+ rlc.add_context(1, context);
+ rlc.start();
+
+ drive_data.dd_rl_view = &rlc;
+
+ auto sc = screen_curses::create().unwrap();
+ keypad(stdscr, TRUE);
+ nonl();
+ cbreak();
+ noecho();
+ nodelay(sc.get_window(), 1);
+
+ rlc.set_window(sc.get_window());
+ rlc.set_y(-1);
+ rlc.set_perform_action(rl_callback);
+ rlc.set_timeout_action(rl_timeout);
+
+ drive_data.dd_looping = true;
+ while (drive_data.dd_looping) {
+ vector<struct pollfd> pollfds;
+ int rc;
+
+ pollfds.push_back((struct pollfd){STDIN_FILENO, POLLIN, 0});
+ psuperv->update_poll_set(pollfds);
+
+ rlc.do_update();
+ refresh();
+ rc = poll(&pollfds[0], pollfds.size(), -1);
+ if (rc > 0) {
+ if (pollfd_ready(pollfds, STDIN_FILENO)) {
+ int ch;
+
+ while ((ch = getch()) != ERR) {
+ switch (ch) {
+ case CEOF:
+ case KEY_RESIZE:
+ break;
+
+ default:
+ if (drive_data.dd_active) {
+ rlc.handle_key(ch);
+ } else if (ch == ':') {
+ rlc.focus(1, ":");
+ drive_data.dd_active = true;
+ }
+ break;
+ }
+ }
+ }
+ psuperv->check_poll_set(pollfds);
+ }
+ }
+
+ return retval;
+}
diff --git a/test/drive_sequencer.cc b/test/drive_sequencer.cc
new file mode 100644
index 0000000..4e39d30
--- /dev/null
+++ b/test/drive_sequencer.cc
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+#include <list>
+#include <map>
+#include <vector>
+
+#include <assert.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "logfile.hh"
+#include "sequence_matcher.hh"
+#include "sequence_sink.hh"
+#include "textview_curses.hh"
+
+using namespace std;
+
+class my_source : public grep_proc_source<vis_line_t> {
+public:
+ my_source(auto_fd& fd) : ms_offset(0) { this->ms_buffer.set_fd(fd); };
+
+ bool grep_value_for_line(vis_line_t line_number, string& value_out)
+ {
+ bool retval = false;
+
+ try {
+#if 0
+ line_value lv;
+
+ if (this->ms_buffer.read_line(this->ms_offset, lv)) {
+ value_out = string(lv.lv_start, lv.lv_len);
+ retval = true;
+ }
+#endif
+ } catch (line_buffer::error& e) {
+ fprintf(stderr,
+ "error: source buffer error %d %s\n",
+ this->ms_buffer.get_fd(),
+ strerror(e.e_err));
+ }
+
+ return retval;
+ };
+
+private:
+ line_buffer ms_buffer;
+ off_t ms_offset;
+};
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ const char* errptr;
+ auto_fd fd;
+ pcre* code;
+ int eoff;
+
+ if (argc < 3) {
+ fprintf(stderr, "error: expecting pattern and file arguments\n");
+ retval = EXIT_FAILURE;
+ } else if ((fd = open(argv[2], O_RDONLY)) == -1) {
+ perror("open");
+ retval = EXIT_FAILURE;
+ } else if ((code
+ = pcre_compile(argv[1], PCRE_CASELESS, &errptr, &eoff, NULL))
+ == NULL)
+ {
+ fprintf(stderr, "error: invalid pattern -- %s\n", errptr);
+ } else {
+ my_source ms(fd);
+
+ sequence_matcher::field_col_t fc;
+
+ fc.resize(2);
+
+ sequence_matcher::field_row_t& frf = fc.front();
+ frf.resize(2);
+ frf[0] = "eth0";
+ frf[1] = "eth0";
+
+ sequence_matcher::field_row_t& frb = fc.back();
+ frb.resize(2);
+ frb[0] = "up";
+ frb[1] = "down";
+
+ static bookmark_type_t SEQUENCE("sequence");
+
+ sequence_matcher sm(fc);
+ vis_bookmarks bm;
+ sequence_sink ss(sm, bm[&SEQUENCE]);
+
+ grep_proc<vis_line_t> gp(code, ms);
+
+ gp.set_sink(&ss);
+ gp.queue_request();
+ gp.start();
+
+ while (bm[&SEQUENCE].size() == 0) {
+ vector<struct pollfd> pollfds;
+
+ poll(&pollfds[0], pollfds.size(), -1);
+
+ gp.check_poll_set(pollfds);
+ }
+
+ for (bookmark_vector<vis_line_t>::iterator iter = bm[&SEQUENCE].begin();
+ iter != bm[&SEQUENCE].end();
+ ++iter)
+ {
+ printf("%d\n", (const int) *iter);
+ }
+ }
+
+ return retval;
+}
diff --git a/test/drive_shlexer.cc b/test/drive_shlexer.cc
new file mode 100644
index 0000000..fe6f15f
--- /dev/null
+++ b/test/drive_shlexer.cc
@@ -0,0 +1,96 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdlib.h>
+
+#include "config.h"
+#include "shlex.hh"
+
+using namespace std;
+
+const char* ST_TOKEN_NAMES[] = {
+ "err",
+ "wsp",
+ "esc",
+ "dst",
+ "den",
+ "sst",
+ "sen",
+ "ref",
+ "qrf",
+ "til",
+};
+
+int
+main(int argc, char* argv[])
+{
+ if (argc < 2) {
+ fprintf(stderr, "error: expecting an argument to parse\n");
+ exit(EXIT_FAILURE);
+ }
+
+ shlex lexer(argv[1], strlen(argv[1]));
+ string_fragment cap;
+ shlex_token_t token;
+
+ printf(" %s\n", argv[1]);
+ while (lexer.tokenize(cap, token)) {
+ int lpc;
+
+ printf("%s ", ST_TOKEN_NAMES[(int) token]);
+ for (lpc = 0; lpc < cap.sf_end; lpc++) {
+ if (lpc == cap.sf_begin) {
+ fputc('^', stdout);
+ } else if (lpc == (cap.sf_end - 1)) {
+ fputc('^', stdout);
+ } else if (lpc > cap.sf_begin) {
+ fputc('-', stdout);
+ } else {
+ fputc(' ', stdout);
+ }
+ }
+ printf("\n");
+ }
+
+ lexer.reset();
+ std::string result;
+ if (lexer.eval(result, map<string, string>())) {
+ printf("eval -- %s\n", result.c_str());
+ }
+ lexer.reset();
+ std::vector<std::string> sresult;
+ if (lexer.split(sresult, map<string, string>())) {
+ printf("split:\n");
+ for (size_t lpc = 0; lpc < sresult.size(); lpc++) {
+ printf(" %zu -- %s\n", lpc, sresult[lpc].c_str());
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/test/drive_sql.cc b/test/drive_sql.cc
new file mode 100644
index 0000000..243e78a
--- /dev/null
+++ b/test/drive_sql.cc
@@ -0,0 +1,78 @@
+#include <iostream>
+
+#include <assert.h>
+#include <sqlite3.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/auto_mem.hh"
+#include "base/injector.hh"
+#include "regexp_vtab.hh"
+#include "sqlite-extension-func.hh"
+#include "xpath_vtab.hh"
+
+struct callback_state {
+ int cs_row;
+};
+
+static int
+sql_callback(void* ptr, int ncols, char** colvalues, char** colnames)
+{
+ struct callback_state* cs = (struct callback_state*) ptr;
+
+ printf("Row %d:\n", cs->cs_row);
+ for (int lpc = 0; lpc < ncols; lpc++) {
+ printf(" Column %10s: %s\n", colnames[lpc], colvalues[lpc]);
+ }
+
+ cs->cs_row += 1;
+
+ return 0;
+}
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ auto_mem<sqlite3> db(sqlite3_close);
+ std::string stmt;
+
+ log_argv(argc, argv);
+
+ if (argc == 2) {
+ stmt = argv[1];
+ } else {
+ std::getline(std::cin, stmt, '\0');
+ }
+
+ if (sqlite3_open(":memory:", db.out()) != SQLITE_OK) {
+ fprintf(stderr, "error: unable to make sqlite memory database\n");
+ retval = EXIT_FAILURE;
+ } else {
+ auto_mem<char> errmsg(sqlite3_free);
+ struct callback_state state;
+
+ memset(&state, 0, sizeof(state));
+
+ {
+ int register_collation_functions(sqlite3 * db);
+
+ register_sqlite_funcs(db.in(), sqlite_registration_funcs);
+ register_collation_functions(db.in());
+ }
+
+ register_regexp_vtab(db.in());
+ register_xpath_vtab(db.in());
+
+ if (sqlite3_exec(
+ db.in(), stmt.c_str(), sql_callback, &state, errmsg.out())
+ != SQLITE_OK)
+ {
+ fprintf(stderr, "error: sqlite3_exec failed -- %s\n", errmsg.in());
+ retval = EXIT_FAILURE;
+ }
+ }
+
+ return retval;
+}
diff --git a/test/drive_sql_anno.cc b/test/drive_sql_anno.cc
new file mode 100644
index 0000000..8486ae2
--- /dev/null
+++ b/test/drive_sql_anno.cc
@@ -0,0 +1,89 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file drive_sql_anno.cc
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "lnav.hh"
+#include "sql_help.hh"
+#include "sql_util.hh"
+#include "sqlite-extension-func.hh"
+
+using namespace std;
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ auto_mem<sqlite3> db(sqlite3_close);
+
+ log_argv(argc, argv);
+
+ if (argc < 2) {
+ fprintf(stderr, "error: expecting an SQL statement\n");
+ retval = EXIT_FAILURE;
+ } else if (sqlite3_open(":memory:", db.out()) != SQLITE_OK) {
+ fprintf(stderr, "error: unable to make sqlite memory database\n");
+ retval = EXIT_FAILURE;
+ } else {
+ register_sqlite_funcs(db.in(), sqlite_registration_funcs);
+
+ attr_line_t al(argv[1]);
+
+ annotate_sql_statement(al);
+
+ printf(" %14s %s\n", " ", argv[1]);
+ for (auto& attr : al.get_attrs()) {
+ auto& lr = attr.sa_range;
+
+ printf(" %14s %s%s\n",
+ attr.sa_type->sat_name,
+ string(lr.lr_start, ' ').c_str(),
+ string(lr.length(), '-').c_str());
+ }
+
+ if (argc == 3) {
+ int near;
+
+ if (sscanf(argv[2], "%d", &near) != 1) {
+ fprintf(stderr, "error: expecting an integer for third arg\n");
+ return EXIT_FAILURE;
+ }
+
+ auto avail_help = find_sql_help_for_line(al, near);
+ for (const auto& ht : avail_help) {
+ printf("%s: %s\n", ht->ht_name, ht->ht_summary);
+ }
+ }
+ }
+
+ return retval;
+}
diff --git a/test/drive_view_colors.cc b/test/drive_view_colors.cc
new file mode 100644
index 0000000..f7b38c1
--- /dev/null
+++ b/test/drive_view_colors.cc
@@ -0,0 +1,106 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "config.h"
+#include "view_curses.hh"
+
+class test_colors : public view_curses {
+public:
+ test_colors() : tc_window(nullptr) {}
+
+ void do_update() override
+ {
+ auto& vc = view_colors::singleton();
+ int lpc;
+
+ for (lpc = 0; lpc < 16; lpc++) {
+ text_attrs attrs;
+ char label[64];
+ attr_line_t al;
+ line_range lr;
+
+ snprintf(label, sizeof(label), "This is line: %d", lpc);
+ attrs = vc.attrs_for_ident(label);
+ al = label;
+ al.get_attrs().emplace_back(line_range(0, -1),
+ VC_STYLE.value(attrs));
+ lr.lr_start = 0;
+ lr.lr_end = 40;
+ test_colors::mvwattrline(this->tc_window, lpc, 0, al, lr);
+ }
+
+ attr_line_t al;
+ line_range lr{0, 40};
+
+ al = "before <123> after";
+ al.with_attr({line_range{8, 11},
+ VC_STYLE.value(text_attrs{0, COLOR_CYAN, COLOR_BLACK})});
+ al.with_attr(
+ {line_range{8, 11}, VC_STYLE.value(text_attrs{A_REVERSE})});
+ test_colors::mvwattrline(this->tc_window, lpc, 0, al, lr);
+ };
+
+ WINDOW* tc_window;
+};
+
+int
+main(int argc, char* argv[])
+{
+ int c, retval = EXIT_SUCCESS;
+ bool wait_for_input = false;
+ WINDOW* win;
+ test_colors tc;
+
+ win = initscr();
+ noecho();
+
+ while ((c = getopt(argc, argv, "w")) != -1) {
+ switch (c) {
+ case 'w':
+ wait_for_input = true;
+ break;
+ }
+ }
+
+ view_colors::init(false);
+ curs_set(0);
+ tc.tc_window = win;
+ tc.do_update();
+ refresh();
+ if (wait_for_input) {
+ getch();
+ }
+ endwin();
+
+ return retval;
+}
diff --git a/test/drive_vt52_curses.cc b/test/drive_vt52_curses.cc
new file mode 100644
index 0000000..c9ca90e
--- /dev/null
+++ b/test/drive_vt52_curses.cc
@@ -0,0 +1,129 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <fcntl.h>
+#include <locale.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "base/lnav_log.hh"
+#include "config.h"
+#include "view_curses.hh"
+#include "vt52_curses.hh"
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/curses.h>
+# include <ncursesw/term.h>
+#elif defined HAVE_NCURSESW_H
+# include <ncursesw.h>
+# include <term.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/curses.h>
+# include <ncurses/term.h>
+#elif defined HAVE_NCURSES_H
+# include <ncurses.h>
+# include <term.h>
+#elif defined HAVE_CURSES_H
+# include <curses.h>
+# include <term.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+
+#undef set_window
+
+int
+main(int argc, char* argv[])
+{
+ int lpc, c, fd, retval = EXIT_SUCCESS;
+ vt52_curses vt;
+
+ setenv("LANG", "en_US.UTF-8", 1);
+ setlocale(LC_ALL, "");
+ fd = open("/tmp/lnav.err", O_WRONLY | O_CREAT | O_APPEND, 0666);
+ dup2(fd, STDERR_FILENO);
+ close(fd);
+ fprintf(stderr, "startup\n");
+ lnav_log_file = stderr;
+
+ while ((c = getopt(argc, argv, "y:")) != -1) {
+ switch (c) {
+ case 'y':
+ vt.set_y(atoi(optarg));
+ break;
+ }
+ }
+
+ for (lpc = 0; lpc < 1000; lpc++) {
+ int len;
+
+ assert(vt.map_input(random(), len) != nullptr);
+ assert(len > 0);
+ }
+
+ tgetent(nullptr, "vt52");
+ {
+ static const char* CANNED_INPUT[] = {
+ "Gru\xC3\x9F",
+ "\r",
+ tgetstr((char*) "ce", nullptr),
+ "de",
+ "\n",
+ "1",
+ "2",
+ "3",
+ "\n",
+ "abc",
+ "\x02",
+ "\a",
+ "ab\bcdef",
+ };
+
+ auto sc = screen_curses::create().unwrap();
+ noecho();
+ vt.set_window(sc.get_window());
+ vt.set_width(10);
+
+ for (const auto* canned : CANNED_INPUT) {
+ vt.map_output(canned, strlen(canned));
+ vt.do_update();
+ refresh();
+ view_curses::awaiting_user_input();
+ getch();
+ }
+
+ view_curses::awaiting_user_input();
+ getch();
+ }
+
+ return retval;
+}
diff --git a/test/expected/expected.am b/test/expected/expected.am
new file mode 100644
index 0000000..6a88ee8
--- /dev/null
+++ b/test/expected/expected.am
@@ -0,0 +1,1021 @@
+
+EXPECTED_FILES = \
+ $(srcdir)/%reldir%/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.err \
+ $(srcdir)/%reldir%/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.out \
+ $(srcdir)/%reldir%/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.err \
+ $(srcdir)/%reldir%/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.out \
+ $(srcdir)/%reldir%/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.err \
+ $(srcdir)/%reldir%/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.out \
+ $(srcdir)/%reldir%/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.err \
+ $(srcdir)/%reldir%/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.out \
+ $(srcdir)/%reldir%/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.err \
+ $(srcdir)/%reldir%/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.out \
+ $(srcdir)/%reldir%/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.err \
+ $(srcdir)/%reldir%/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.out \
+ $(srcdir)/%reldir%/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.err \
+ $(srcdir)/%reldir%/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.out \
+ $(srcdir)/%reldir%/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.err \
+ $(srcdir)/%reldir%/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.out \
+ $(srcdir)/%reldir%/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.err \
+ $(srcdir)/%reldir%/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.out \
+ $(srcdir)/%reldir%/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.err \
+ $(srcdir)/%reldir%/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.out \
+ $(srcdir)/%reldir%/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.err \
+ $(srcdir)/%reldir%/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.out \
+ $(srcdir)/%reldir%/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.err \
+ $(srcdir)/%reldir%/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.out \
+ $(srcdir)/%reldir%/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.err \
+ $(srcdir)/%reldir%/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.out \
+ $(srcdir)/%reldir%/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.err \
+ $(srcdir)/%reldir%/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.out \
+ $(srcdir)/%reldir%/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.err \
+ $(srcdir)/%reldir%/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.out \
+ $(srcdir)/%reldir%/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.err \
+ $(srcdir)/%reldir%/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.out \
+ $(srcdir)/%reldir%/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.err \
+ $(srcdir)/%reldir%/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.out \
+ $(srcdir)/%reldir%/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.err \
+ $(srcdir)/%reldir%/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.out \
+ $(srcdir)/%reldir%/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.err \
+ $(srcdir)/%reldir%/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.out \
+ $(srcdir)/%reldir%/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.err \
+ $(srcdir)/%reldir%/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.out \
+ $(srcdir)/%reldir%/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.err \
+ $(srcdir)/%reldir%/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.out \
+ $(srcdir)/%reldir%/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.err \
+ $(srcdir)/%reldir%/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.out \
+ $(srcdir)/%reldir%/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.err \
+ $(srcdir)/%reldir%/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.out \
+ $(srcdir)/%reldir%/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.err \
+ $(srcdir)/%reldir%/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.out \
+ $(srcdir)/%reldir%/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.err \
+ $(srcdir)/%reldir%/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.out \
+ $(srcdir)/%reldir%/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.err \
+ $(srcdir)/%reldir%/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.out \
+ $(srcdir)/%reldir%/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.err \
+ $(srcdir)/%reldir%/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.out \
+ $(srcdir)/%reldir%/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.err \
+ $(srcdir)/%reldir%/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.out \
+ $(srcdir)/%reldir%/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.err \
+ $(srcdir)/%reldir%/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.out \
+ $(srcdir)/%reldir%/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.err \
+ $(srcdir)/%reldir%/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.out \
+ $(srcdir)/%reldir%/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.err \
+ $(srcdir)/%reldir%/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.out \
+ $(srcdir)/%reldir%/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.err \
+ $(srcdir)/%reldir%/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.out \
+ $(srcdir)/%reldir%/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.err \
+ $(srcdir)/%reldir%/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.out \
+ $(srcdir)/%reldir%/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.err \
+ $(srcdir)/%reldir%/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.out \
+ $(srcdir)/%reldir%/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.err \
+ $(srcdir)/%reldir%/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.out \
+ $(srcdir)/%reldir%/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.err \
+ $(srcdir)/%reldir%/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.out \
+ $(srcdir)/%reldir%/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.err \
+ $(srcdir)/%reldir%/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.out \
+ $(srcdir)/%reldir%/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.err \
+ $(srcdir)/%reldir%/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.out \
+ $(srcdir)/%reldir%/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.err \
+ $(srcdir)/%reldir%/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.out \
+ $(srcdir)/%reldir%/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.err \
+ $(srcdir)/%reldir%/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.out \
+ $(srcdir)/%reldir%/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.err \
+ $(srcdir)/%reldir%/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.out \
+ $(srcdir)/%reldir%/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.err \
+ $(srcdir)/%reldir%/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.out \
+ $(srcdir)/%reldir%/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.err \
+ $(srcdir)/%reldir%/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.out \
+ $(srcdir)/%reldir%/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.err \
+ $(srcdir)/%reldir%/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.out \
+ $(srcdir)/%reldir%/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.err \
+ $(srcdir)/%reldir%/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.out \
+ $(srcdir)/%reldir%/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.err \
+ $(srcdir)/%reldir%/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.out \
+ $(srcdir)/%reldir%/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.err \
+ $(srcdir)/%reldir%/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.out \
+ $(srcdir)/%reldir%/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.err \
+ $(srcdir)/%reldir%/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.out \
+ $(srcdir)/%reldir%/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.err \
+ $(srcdir)/%reldir%/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.out \
+ $(srcdir)/%reldir%/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.err \
+ $(srcdir)/%reldir%/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.out \
+ $(srcdir)/%reldir%/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.err \
+ $(srcdir)/%reldir%/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.out \
+ $(srcdir)/%reldir%/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.err \
+ $(srcdir)/%reldir%/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.out \
+ $(srcdir)/%reldir%/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.err \
+ $(srcdir)/%reldir%/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.out \
+ $(srcdir)/%reldir%/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.err \
+ $(srcdir)/%reldir%/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.out \
+ $(srcdir)/%reldir%/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.err \
+ $(srcdir)/%reldir%/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.out \
+ $(srcdir)/%reldir%/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.err \
+ $(srcdir)/%reldir%/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.out \
+ $(srcdir)/%reldir%/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.err \
+ $(srcdir)/%reldir%/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.out \
+ $(srcdir)/%reldir%/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.err \
+ $(srcdir)/%reldir%/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.out \
+ $(srcdir)/%reldir%/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.err \
+ $(srcdir)/%reldir%/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.out \
+ $(srcdir)/%reldir%/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.err \
+ $(srcdir)/%reldir%/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.out \
+ $(srcdir)/%reldir%/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.err \
+ $(srcdir)/%reldir%/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.out \
+ $(srcdir)/%reldir%/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.err \
+ $(srcdir)/%reldir%/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.out \
+ $(srcdir)/%reldir%/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.err \
+ $(srcdir)/%reldir%/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.out \
+ $(srcdir)/%reldir%/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.err \
+ $(srcdir)/%reldir%/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.out \
+ $(srcdir)/%reldir%/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.err \
+ $(srcdir)/%reldir%/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.out \
+ $(srcdir)/%reldir%/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.err \
+ $(srcdir)/%reldir%/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.out \
+ $(srcdir)/%reldir%/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.err \
+ $(srcdir)/%reldir%/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.out \
+ $(srcdir)/%reldir%/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.err \
+ $(srcdir)/%reldir%/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out \
+ $(srcdir)/%reldir%/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.err \
+ $(srcdir)/%reldir%/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.out \
+ $(srcdir)/%reldir%/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.err \
+ $(srcdir)/%reldir%/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.out \
+ $(srcdir)/%reldir%/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.err \
+ $(srcdir)/%reldir%/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.out \
+ $(srcdir)/%reldir%/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.err \
+ $(srcdir)/%reldir%/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.out \
+ $(srcdir)/%reldir%/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.err \
+ $(srcdir)/%reldir%/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.out \
+ $(srcdir)/%reldir%/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.err \
+ $(srcdir)/%reldir%/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.out \
+ $(srcdir)/%reldir%/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.err \
+ $(srcdir)/%reldir%/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.out \
+ $(srcdir)/%reldir%/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.err \
+ $(srcdir)/%reldir%/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.out \
+ $(srcdir)/%reldir%/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.err \
+ $(srcdir)/%reldir%/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.out \
+ $(srcdir)/%reldir%/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.err \
+ $(srcdir)/%reldir%/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.out \
+ $(srcdir)/%reldir%/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.err \
+ $(srcdir)/%reldir%/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.out \
+ $(srcdir)/%reldir%/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.err \
+ $(srcdir)/%reldir%/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.out \
+ $(srcdir)/%reldir%/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.err \
+ $(srcdir)/%reldir%/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.out \
+ $(srcdir)/%reldir%/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.err \
+ $(srcdir)/%reldir%/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.out \
+ $(srcdir)/%reldir%/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.err \
+ $(srcdir)/%reldir%/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.out \
+ $(srcdir)/%reldir%/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.err \
+ $(srcdir)/%reldir%/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.out \
+ $(srcdir)/%reldir%/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.err \
+ $(srcdir)/%reldir%/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.out \
+ $(srcdir)/%reldir%/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.err \
+ $(srcdir)/%reldir%/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.out \
+ $(srcdir)/%reldir%/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.err \
+ $(srcdir)/%reldir%/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.out \
+ $(srcdir)/%reldir%/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.err \
+ $(srcdir)/%reldir%/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.out \
+ $(srcdir)/%reldir%/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.err \
+ $(srcdir)/%reldir%/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.out \
+ $(srcdir)/%reldir%/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.err \
+ $(srcdir)/%reldir%/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.out \
+ $(srcdir)/%reldir%/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.err \
+ $(srcdir)/%reldir%/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.out \
+ $(srcdir)/%reldir%/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.err \
+ $(srcdir)/%reldir%/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.out \
+ $(srcdir)/%reldir%/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.err \
+ $(srcdir)/%reldir%/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.out \
+ $(srcdir)/%reldir%/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.err \
+ $(srcdir)/%reldir%/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.out \
+ $(srcdir)/%reldir%/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.err \
+ $(srcdir)/%reldir%/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.out \
+ $(srcdir)/%reldir%/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.err \
+ $(srcdir)/%reldir%/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.out \
+ $(srcdir)/%reldir%/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.err \
+ $(srcdir)/%reldir%/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.out \
+ $(srcdir)/%reldir%/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.err \
+ $(srcdir)/%reldir%/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.out \
+ $(srcdir)/%reldir%/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.err \
+ $(srcdir)/%reldir%/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.out \
+ $(srcdir)/%reldir%/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.err \
+ $(srcdir)/%reldir%/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.out \
+ $(srcdir)/%reldir%/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.err \
+ $(srcdir)/%reldir%/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.out \
+ $(srcdir)/%reldir%/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.err \
+ $(srcdir)/%reldir%/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.out \
+ $(srcdir)/%reldir%/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.err \
+ $(srcdir)/%reldir%/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.out \
+ $(srcdir)/%reldir%/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.err \
+ $(srcdir)/%reldir%/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.out \
+ $(srcdir)/%reldir%/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.err \
+ $(srcdir)/%reldir%/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.out \
+ $(srcdir)/%reldir%/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.err \
+ $(srcdir)/%reldir%/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.out \
+ $(srcdir)/%reldir%/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.err \
+ $(srcdir)/%reldir%/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.out \
+ $(srcdir)/%reldir%/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.err \
+ $(srcdir)/%reldir%/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.out \
+ $(srcdir)/%reldir%/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.err \
+ $(srcdir)/%reldir%/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.out \
+ $(srcdir)/%reldir%/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.err \
+ $(srcdir)/%reldir%/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.out \
+ $(srcdir)/%reldir%/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.err \
+ $(srcdir)/%reldir%/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.out \
+ $(srcdir)/%reldir%/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.err \
+ $(srcdir)/%reldir%/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.out \
+ $(srcdir)/%reldir%/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.err \
+ $(srcdir)/%reldir%/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.out \
+ $(srcdir)/%reldir%/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.err \
+ $(srcdir)/%reldir%/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.out \
+ $(srcdir)/%reldir%/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.err \
+ $(srcdir)/%reldir%/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.out \
+ $(srcdir)/%reldir%/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.err \
+ $(srcdir)/%reldir%/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.out \
+ $(srcdir)/%reldir%/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.err \
+ $(srcdir)/%reldir%/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.out \
+ $(srcdir)/%reldir%/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.err \
+ $(srcdir)/%reldir%/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.out \
+ $(srcdir)/%reldir%/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.err \
+ $(srcdir)/%reldir%/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.out \
+ $(srcdir)/%reldir%/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.err \
+ $(srcdir)/%reldir%/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.out \
+ $(srcdir)/%reldir%/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.err \
+ $(srcdir)/%reldir%/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.out \
+ $(srcdir)/%reldir%/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.err \
+ $(srcdir)/%reldir%/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.out \
+ $(srcdir)/%reldir%/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.err \
+ $(srcdir)/%reldir%/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.out \
+ $(srcdir)/%reldir%/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.err \
+ $(srcdir)/%reldir%/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.out \
+ $(srcdir)/%reldir%/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.err \
+ $(srcdir)/%reldir%/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.out \
+ $(srcdir)/%reldir%/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.err \
+ $(srcdir)/%reldir%/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.out \
+ $(srcdir)/%reldir%/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.err \
+ $(srcdir)/%reldir%/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.out \
+ $(srcdir)/%reldir%/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.err \
+ $(srcdir)/%reldir%/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.out \
+ $(srcdir)/%reldir%/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.err \
+ $(srcdir)/%reldir%/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.out \
+ $(srcdir)/%reldir%/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.err \
+ $(srcdir)/%reldir%/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.out \
+ $(srcdir)/%reldir%/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.err \
+ $(srcdir)/%reldir%/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.out \
+ $(srcdir)/%reldir%/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.err \
+ $(srcdir)/%reldir%/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.out \
+ $(srcdir)/%reldir%/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.err \
+ $(srcdir)/%reldir%/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.out \
+ $(srcdir)/%reldir%/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.err \
+ $(srcdir)/%reldir%/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.out \
+ $(srcdir)/%reldir%/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.err \
+ $(srcdir)/%reldir%/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.out \
+ $(srcdir)/%reldir%/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.err \
+ $(srcdir)/%reldir%/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.out \
+ $(srcdir)/%reldir%/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.err \
+ $(srcdir)/%reldir%/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.out \
+ $(srcdir)/%reldir%/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.err \
+ $(srcdir)/%reldir%/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.out \
+ $(srcdir)/%reldir%/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.err \
+ $(srcdir)/%reldir%/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.out \
+ $(srcdir)/%reldir%/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.err \
+ $(srcdir)/%reldir%/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.out \
+ $(srcdir)/%reldir%/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.err \
+ $(srcdir)/%reldir%/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.out \
+ $(srcdir)/%reldir%/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.err \
+ $(srcdir)/%reldir%/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.out \
+ $(srcdir)/%reldir%/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.err \
+ $(srcdir)/%reldir%/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.out \
+ $(srcdir)/%reldir%/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.err \
+ $(srcdir)/%reldir%/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.out \
+ $(srcdir)/%reldir%/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.err \
+ $(srcdir)/%reldir%/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.out \
+ $(srcdir)/%reldir%/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.err \
+ $(srcdir)/%reldir%/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.out \
+ $(srcdir)/%reldir%/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.err \
+ $(srcdir)/%reldir%/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.out \
+ $(srcdir)/%reldir%/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.err \
+ $(srcdir)/%reldir%/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.out \
+ $(srcdir)/%reldir%/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.err \
+ $(srcdir)/%reldir%/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.out \
+ $(srcdir)/%reldir%/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.err \
+ $(srcdir)/%reldir%/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.out \
+ $(srcdir)/%reldir%/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.err \
+ $(srcdir)/%reldir%/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.out \
+ $(srcdir)/%reldir%/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.err \
+ $(srcdir)/%reldir%/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.out \
+ $(srcdir)/%reldir%/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.err \
+ $(srcdir)/%reldir%/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.out \
+ $(srcdir)/%reldir%/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.err \
+ $(srcdir)/%reldir%/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.out \
+ $(srcdir)/%reldir%/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.err \
+ $(srcdir)/%reldir%/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.out \
+ $(srcdir)/%reldir%/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.err \
+ $(srcdir)/%reldir%/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.out \
+ $(srcdir)/%reldir%/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.err \
+ $(srcdir)/%reldir%/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.out \
+ $(srcdir)/%reldir%/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.err \
+ $(srcdir)/%reldir%/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.out \
+ $(srcdir)/%reldir%/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.err \
+ $(srcdir)/%reldir%/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.out \
+ $(srcdir)/%reldir%/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.err \
+ $(srcdir)/%reldir%/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.out \
+ $(srcdir)/%reldir%/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.err \
+ $(srcdir)/%reldir%/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.out \
+ $(srcdir)/%reldir%/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.err \
+ $(srcdir)/%reldir%/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.out \
+ $(srcdir)/%reldir%/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.err \
+ $(srcdir)/%reldir%/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.out \
+ $(srcdir)/%reldir%/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.err \
+ $(srcdir)/%reldir%/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.out \
+ $(srcdir)/%reldir%/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.err \
+ $(srcdir)/%reldir%/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.out \
+ $(srcdir)/%reldir%/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.err \
+ $(srcdir)/%reldir%/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.out \
+ $(srcdir)/%reldir%/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.err \
+ $(srcdir)/%reldir%/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.out \
+ $(srcdir)/%reldir%/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.err \
+ $(srcdir)/%reldir%/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.out \
+ $(srcdir)/%reldir%/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.err \
+ $(srcdir)/%reldir%/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.out \
+ $(srcdir)/%reldir%/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.err \
+ $(srcdir)/%reldir%/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.out \
+ $(srcdir)/%reldir%/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.err \
+ $(srcdir)/%reldir%/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.out \
+ $(srcdir)/%reldir%/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.err \
+ $(srcdir)/%reldir%/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.out \
+ $(srcdir)/%reldir%/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.err \
+ $(srcdir)/%reldir%/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.out \
+ $(srcdir)/%reldir%/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.err \
+ $(srcdir)/%reldir%/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.out \
+ $(srcdir)/%reldir%/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.err \
+ $(srcdir)/%reldir%/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.out \
+ $(srcdir)/%reldir%/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.err \
+ $(srcdir)/%reldir%/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.out \
+ $(srcdir)/%reldir%/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.err \
+ $(srcdir)/%reldir%/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.out \
+ $(srcdir)/%reldir%/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.err \
+ $(srcdir)/%reldir%/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.out \
+ $(srcdir)/%reldir%/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.err \
+ $(srcdir)/%reldir%/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.out \
+ $(srcdir)/%reldir%/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.err \
+ $(srcdir)/%reldir%/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.out \
+ $(srcdir)/%reldir%/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.err \
+ $(srcdir)/%reldir%/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.out \
+ $(srcdir)/%reldir%/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.err \
+ $(srcdir)/%reldir%/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.out \
+ $(srcdir)/%reldir%/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.err \
+ $(srcdir)/%reldir%/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.out \
+ $(srcdir)/%reldir%/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.err \
+ $(srcdir)/%reldir%/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.out \
+ $(srcdir)/%reldir%/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.err \
+ $(srcdir)/%reldir%/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.out \
+ $(srcdir)/%reldir%/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.err \
+ $(srcdir)/%reldir%/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.out \
+ $(srcdir)/%reldir%/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.err \
+ $(srcdir)/%reldir%/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.out \
+ $(srcdir)/%reldir%/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.err \
+ $(srcdir)/%reldir%/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.out \
+ $(srcdir)/%reldir%/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.err \
+ $(srcdir)/%reldir%/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.out \
+ $(srcdir)/%reldir%/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.err \
+ $(srcdir)/%reldir%/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.out \
+ $(srcdir)/%reldir%/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.err \
+ $(srcdir)/%reldir%/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.out \
+ $(srcdir)/%reldir%/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.err \
+ $(srcdir)/%reldir%/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.out \
+ $(srcdir)/%reldir%/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.err \
+ $(srcdir)/%reldir%/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.out \
+ $(srcdir)/%reldir%/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.err \
+ $(srcdir)/%reldir%/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.out \
+ $(srcdir)/%reldir%/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.err \
+ $(srcdir)/%reldir%/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.out \
+ $(srcdir)/%reldir%/test_sql.sh_02def66745b063518473df862987747909f56ccc.err \
+ $(srcdir)/%reldir%/test_sql.sh_02def66745b063518473df862987747909f56ccc.out \
+ $(srcdir)/%reldir%/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.err \
+ $(srcdir)/%reldir%/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.out \
+ $(srcdir)/%reldir%/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.err \
+ $(srcdir)/%reldir%/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.out \
+ $(srcdir)/%reldir%/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.err \
+ $(srcdir)/%reldir%/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.out \
+ $(srcdir)/%reldir%/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.err \
+ $(srcdir)/%reldir%/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.out \
+ $(srcdir)/%reldir%/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.err \
+ $(srcdir)/%reldir%/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.out \
+ $(srcdir)/%reldir%/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.err \
+ $(srcdir)/%reldir%/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.out \
+ $(srcdir)/%reldir%/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.err \
+ $(srcdir)/%reldir%/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.out \
+ $(srcdir)/%reldir%/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.err \
+ $(srcdir)/%reldir%/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.out \
+ $(srcdir)/%reldir%/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.err \
+ $(srcdir)/%reldir%/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.out \
+ $(srcdir)/%reldir%/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.err \
+ $(srcdir)/%reldir%/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.out \
+ $(srcdir)/%reldir%/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.err \
+ $(srcdir)/%reldir%/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.out \
+ $(srcdir)/%reldir%/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.err \
+ $(srcdir)/%reldir%/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.out \
+ $(srcdir)/%reldir%/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.err \
+ $(srcdir)/%reldir%/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.out \
+ $(srcdir)/%reldir%/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.err \
+ $(srcdir)/%reldir%/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.out \
+ $(srcdir)/%reldir%/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.err \
+ $(srcdir)/%reldir%/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.out \
+ $(srcdir)/%reldir%/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.err \
+ $(srcdir)/%reldir%/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.out \
+ $(srcdir)/%reldir%/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.err \
+ $(srcdir)/%reldir%/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.out \
+ $(srcdir)/%reldir%/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.err \
+ $(srcdir)/%reldir%/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.out \
+ $(srcdir)/%reldir%/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.err \
+ $(srcdir)/%reldir%/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.out \
+ $(srcdir)/%reldir%/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.err \
+ $(srcdir)/%reldir%/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.out \
+ $(srcdir)/%reldir%/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.err \
+ $(srcdir)/%reldir%/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.out \
+ $(srcdir)/%reldir%/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.err \
+ $(srcdir)/%reldir%/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.out \
+ $(srcdir)/%reldir%/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.err \
+ $(srcdir)/%reldir%/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.out \
+ $(srcdir)/%reldir%/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.err \
+ $(srcdir)/%reldir%/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.out \
+ $(srcdir)/%reldir%/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.err \
+ $(srcdir)/%reldir%/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.out \
+ $(srcdir)/%reldir%/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.err \
+ $(srcdir)/%reldir%/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.out \
+ $(srcdir)/%reldir%/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.err \
+ $(srcdir)/%reldir%/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.out \
+ $(srcdir)/%reldir%/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err \
+ $(srcdir)/%reldir%/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.out \
+ $(srcdir)/%reldir%/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.err \
+ $(srcdir)/%reldir%/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.out \
+ $(srcdir)/%reldir%/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.err \
+ $(srcdir)/%reldir%/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.out \
+ $(srcdir)/%reldir%/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.err \
+ $(srcdir)/%reldir%/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.out \
+ $(srcdir)/%reldir%/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.err \
+ $(srcdir)/%reldir%/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.out \
+ $(srcdir)/%reldir%/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.err \
+ $(srcdir)/%reldir%/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.out \
+ $(srcdir)/%reldir%/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.err \
+ $(srcdir)/%reldir%/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.out \
+ $(srcdir)/%reldir%/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.err \
+ $(srcdir)/%reldir%/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.out \
+ $(srcdir)/%reldir%/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.err \
+ $(srcdir)/%reldir%/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.out \
+ $(srcdir)/%reldir%/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.err \
+ $(srcdir)/%reldir%/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.out \
+ $(srcdir)/%reldir%/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.err \
+ $(srcdir)/%reldir%/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.out \
+ $(srcdir)/%reldir%/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.err \
+ $(srcdir)/%reldir%/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.out \
+ $(srcdir)/%reldir%/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.err \
+ $(srcdir)/%reldir%/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.out \
+ $(srcdir)/%reldir%/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.err \
+ $(srcdir)/%reldir%/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.out \
+ $(srcdir)/%reldir%/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.err \
+ $(srcdir)/%reldir%/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.out \
+ $(srcdir)/%reldir%/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.err \
+ $(srcdir)/%reldir%/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.out \
+ $(srcdir)/%reldir%/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.err \
+ $(srcdir)/%reldir%/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.out \
+ $(srcdir)/%reldir%/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.err \
+ $(srcdir)/%reldir%/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.out \
+ $(srcdir)/%reldir%/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.err \
+ $(srcdir)/%reldir%/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.out \
+ $(srcdir)/%reldir%/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.err \
+ $(srcdir)/%reldir%/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.out \
+ $(srcdir)/%reldir%/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.err \
+ $(srcdir)/%reldir%/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.out \
+ $(srcdir)/%reldir%/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.err \
+ $(srcdir)/%reldir%/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.out \
+ $(srcdir)/%reldir%/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.err \
+ $(srcdir)/%reldir%/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.out \
+ $(srcdir)/%reldir%/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.err \
+ $(srcdir)/%reldir%/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.out \
+ $(srcdir)/%reldir%/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.err \
+ $(srcdir)/%reldir%/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.out \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.err \
+ $(srcdir)/%reldir%/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.out \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.err \
+ $(srcdir)/%reldir%/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.out \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.err \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.out \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.err \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.out \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.err \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.out \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.err \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.out \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.err \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.out \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.err \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.out \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.err \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.out \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.err \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.out \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.err \
+ $(srcdir)/%reldir%/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.out \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.err \
+ $(srcdir)/%reldir%/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.out \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.err \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.out \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.err \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.out \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.err \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.out \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.err \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.out \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.err \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.out \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.err \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.out \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.err \
+ $(srcdir)/%reldir%/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.out \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.err \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.out \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.err \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.out \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.err \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.out \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.err \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.out \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.err \
+ $(srcdir)/%reldir%/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.out \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.err \
+ $(srcdir)/%reldir%/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.out \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.err \
+ $(srcdir)/%reldir%/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.out \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.err \
+ $(srcdir)/%reldir%/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.out \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.err \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.out \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.err \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.out \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.err \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.out \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.err \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.out \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.err \
+ $(srcdir)/%reldir%/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.out \
+ $(srcdir)/%reldir%/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.err \
+ $(srcdir)/%reldir%/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.out \
+ $(srcdir)/%reldir%/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.err \
+ $(srcdir)/%reldir%/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.out \
+ $(srcdir)/%reldir%/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.err \
+ $(srcdir)/%reldir%/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.out \
+ $(srcdir)/%reldir%/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.err \
+ $(srcdir)/%reldir%/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.out \
+ $(srcdir)/%reldir%/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.err \
+ $(srcdir)/%reldir%/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.out \
+ $(srcdir)/%reldir%/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.err \
+ $(srcdir)/%reldir%/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.out \
+ $(srcdir)/%reldir%/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.err \
+ $(srcdir)/%reldir%/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.out \
+ $(srcdir)/%reldir%/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.err \
+ $(srcdir)/%reldir%/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.out \
+ $(srcdir)/%reldir%/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.err \
+ $(srcdir)/%reldir%/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.out \
+ $(srcdir)/%reldir%/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.err \
+ $(srcdir)/%reldir%/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.out \
+ $(srcdir)/%reldir%/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.err \
+ $(srcdir)/%reldir%/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out \
+ $()
diff --git a/test/expected/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.err b/test/expected/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.err
diff --git a/test/expected/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.out b/test/expected/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.out
new file mode 100644
index 0000000..3b130be
--- /dev/null
+++ b/test/expected/test_cli.sh_17a68b798354f9a6cdfab372006caeb74038d15c.out
@@ -0,0 +1 @@
+2021-07-03T21:49:29 Test
diff --git a/test/expected/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.err b/test/expected/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.err
diff --git a/test/expected/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.out b/test/expected/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.out
new file mode 100644
index 0000000..4c63930
--- /dev/null
+++ b/test/expected/test_cli.sh_5524542b1a6954ff9741155101497270a2f0c557.out
@@ -0,0 +1 @@
+a a a
diff --git a/test/expected/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.err b/test/expected/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.err
new file mode 100644
index 0000000..9d07883
--- /dev/null
+++ b/test/expected/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.err
@@ -0,0 +1,8 @@
+✘ error: invalid value for “-c” option
+ --> command-line argument
+ |  -c foo 
+ |  ^ command type prefix is missing
+ = help: command arguments must start with one of the following symbols to denote the type of command:
+ : - an lnav command (e.g. :goto 42)
+ ; - an SQL statement (e.g. ;SELECT * FROM syslog_log)
+ | - an lnav script (e.g. |rename-stdin foo)
diff --git a/test/expected/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.out b/test/expected/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cli.sh_97e19b9ff3775d84074455a2e8993a0611b1c269.out
diff --git a/test/expected/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.err b/test/expected/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.err
diff --git a/test/expected/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.out b/test/expected/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.out
new file mode 100644
index 0000000..1e0a993
--- /dev/null
+++ b/test/expected/test_cli.sh_a1a09f890f4604309d0a81bbbec8e50fb7d5e887.out
@@ -0,0 +1,3 @@
+2013-06-06T19:13:20.123 Hello, World!
+2013-06-06T19:13:20.123 Goodbye, World!
+2013-06-06T19:13:20.123 ---- END-OF-STDIN ----
diff --git a/test/expected/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.err b/test/expected/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.err
diff --git a/test/expected/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.out b/test/expected/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.out
new file mode 100644
index 0000000..751c83b
--- /dev/null
+++ b/test/expected/test_cli.sh_f2e41555f1a5f40f54ce241207af602ed1503a2b.out
@@ -0,0 +1,2 @@
+filepath lines 
+stdin   4 
diff --git a/test/expected/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.err b/test/expected/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.err
new file mode 100644
index 0000000..0d53487
--- /dev/null
+++ b/test/expected/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.err
@@ -0,0 +1,6 @@
+✘ error: no log files loaded
+ --> command-option:2
+ | :close 
+ = help: :close
+ ══════════════════════════════════════════════════════════════════════
+ Close the top file in the view
diff --git a/test/expected/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.out b/test/expected/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_017b495b95218b7c083951e2dba331cfec6e90be.out
diff --git a/test/expected/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.err b/test/expected/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.err
diff --git a/test/expected/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.out b/test/expected/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.out
new file mode 100644
index 0000000..689436e
--- /dev/null
+++ b/test/expected/test_cmds.sh_0b1e4b1523dfca71927b1fe721c74490c51361d1.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:21:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:21:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:21:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.err b/test/expected/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.err
diff --git a/test/expected/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.out b/test/expected/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.out
new file mode 100644
index 0000000..ccd015e
--- /dev/null
+++ b/test/expected/test_cmds.sh_0b41fe57743ba0be088037d9ba29bc465e7c9bf9.out
@@ -0,0 +1,3 @@
+c1,c2
+1,"Hello
+World!"
diff --git a/test/expected/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.err b/test/expected/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.err
diff --git a/test/expected/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.out b/test/expected/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_cmds.sh_0f0ab532d8d845f8201af65bf5f6fc994e21a8aa.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.err b/test/expected/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.err
diff --git a/test/expected/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.out b/test/expected/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_cmds.sh_109a44ac6a8f1be2736c8e9c47aeed187e0581ee.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.err b/test/expected/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.err
diff --git a/test/expected/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.out b/test/expected/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.out
new file mode 100644
index 0000000..d18c6b1
--- /dev/null
+++ b/test/expected/test_cmds.sh_12856706bfb4a8e2686098dd2644a7989d370b02.out
@@ -0,0 +1 @@
+How are you?
diff --git a/test/expected/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.err b/test/expected/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.err
new file mode 100644
index 0000000..f90b5d3
--- /dev/null
+++ b/test/expected/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.err
@@ -0,0 +1,7 @@
+✘ error: call to timeslice(time, slice) failed
+ reason: unable to parse time slice value: bad -- Unrecognized input
+ --> command-option:1
+ | :filter-expr timeslice(:log_time_msecs, 'bad') is not null
+ = help: :filter-expr expr
+ ══════════════════════════════════════════════════════════════════════
+ Set the filter expression
diff --git a/test/expected/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.out b/test/expected/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_12b4cb9bd6586f9694100db76734b19a75158eab.out
diff --git a/test/expected/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.err b/test/expected/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.err
diff --git a/test/expected/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.out b/test/expected/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_cmds.sh_145126309709179759926289caf729703ef6e1c6.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.err b/test/expected/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.err
diff --git a/test/expected/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.out b/test/expected/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.out
new file mode 100644
index 0000000..6ebd88b
--- /dev/null
+++ b/test/expected/test_cmds.sh_148007d2626b3c92d00ac31639b6918b1fc4aa60.out
@@ -0,0 +1,2 @@
+Hello, World!
+-07-20 22:59:30,221:ERROR:Goodbye, Bork!
diff --git a/test/expected/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.err b/test/expected/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.err
diff --git a/test/expected/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.out b/test/expected/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.out
new file mode 100644
index 0000000..45e6c41
--- /dev/null
+++ b/test/expected/test_cmds.sh_1cab7d240cf85ff2c3538f5a06af141b01bc83ad.out
@@ -0,0 +1,3 @@
+-07-20 22:59:27,672:DEBUG:Hello, Bork!
+ How are you today?
+-07-20 22:59:30,221:ERROR:Goodbye, Bork!
diff --git a/test/expected/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.err b/test/expected/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.err
diff --git a/test/expected/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.out b/test/expected/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.out
new file mode 100644
index 0000000..6dfcc1c
--- /dev/null
+++ b/test/expected/test_cmds.sh_1d92c5bc12f5e7aaa6d84c5ed47f0b9f96e36c6a.out
@@ -0,0 +1,68 @@
+[
+ {
+ "log_line": 0,
+ "log_part": null,
+ "log_time": "2009-07-20 22:59:26.000",
+ "log_idle_msecs": 0,
+ "log_level": "info",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "c_ip": "192.168.202.254",
+ "cs_method": "GET",
+ "cs_referer": "-",
+ "cs_uri_query": null,
+ "cs_uri_stem": "/vmw/cgi/tramp",
+ "cs_user_agent": "gPXE/0.9.7",
+ "cs_username": "-",
+ "cs_version": "HTTP/1.0",
+ "sc_bytes": 134,
+ "sc_status": 200,
+ "cs_host": null
+ },
+ {
+ "log_line": 1,
+ "log_part": null,
+ "log_time": "2009-07-20 22:59:29.000",
+ "log_idle_msecs": 3000,
+ "log_level": "error",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "c_ip": "192.168.202.254",
+ "cs_method": "GET",
+ "cs_referer": "-",
+ "cs_uri_query": null,
+ "cs_uri_stem": "/vmw/vSphere/default/vmkboot.gz",
+ "cs_user_agent": "gPXE/0.9.7",
+ "cs_username": "-",
+ "cs_version": "HTTP/1.0",
+ "sc_bytes": 46210,
+ "sc_status": 404,
+ "cs_host": null
+ },
+ {
+ "log_line": 2,
+ "log_part": null,
+ "log_time": "2009-07-20 22:59:29.000",
+ "log_idle_msecs": 0,
+ "log_level": "info",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "c_ip": "192.168.202.254",
+ "cs_method": "GET",
+ "cs_referer": "-",
+ "cs_uri_query": null,
+ "cs_uri_stem": "/vmw/vSphere/default/vmkernel.gz",
+ "cs_user_agent": "gPXE/0.9.7",
+ "cs_username": "-",
+ "cs_version": "HTTP/1.0",
+ "sc_bytes": 78929,
+ "sc_status": 200,
+ "cs_host": null
+ }
+]
diff --git a/test/expected/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.err b/test/expected/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.err
new file mode 100644
index 0000000..52606d5
--- /dev/null
+++ b/test/expected/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.err
@@ -0,0 +1,6 @@
+✘ error: highlight does not exist -- foobar
+ --> command-option:1
+ | :clear-highlight foobar 
+ = help: :clear-highlight pattern
+ ══════════════════════════════════════════════════════════════════════
+ Remove a previously set highlight regular expression
diff --git a/test/expected/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.out b/test/expected/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_1e1c8492b295913ce5afcd104cde0ec4ca1dcdac.out
diff --git a/test/expected/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.err b/test/expected/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.err
diff --git a/test/expected/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.out b/test/expected/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_cmds.sh_1f53f5b16c7c5aa695ed2e6427d822a1b940fcf4.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.err b/test/expected/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.err
new file mode 100644
index 0000000..a2b36fa
--- /dev/null
+++ b/test/expected/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.err
@@ -0,0 +1,7 @@
+✘ error: invalid timestamp: 17:00:01.
+ reason: the leading part of the timestamp was matched, however, the trailing text “.” was not
+ --> command-option:1
+ | :goto 17:00:01. 
+ |  ^ unrecognized input 
+ = note: input matched time format “%H:%M:%S”
+ = help: fix the timestamp or remove the trailing text
diff --git a/test/expected/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.out b/test/expected/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2186d5eb6e84d6a23712334d5088c044fe089db0.out
diff --git a/test/expected/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.err b/test/expected/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.err
diff --git a/test/expected/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.out b/test/expected/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_cmds.sh_22577861cb0921a7e7f3d1af6485938f4930ba7b.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.err b/test/expected/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.err
diff --git a/test/expected/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.out b/test/expected/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.out
new file mode 100644
index 0000000..83f15c5
--- /dev/null
+++ b/test/expected/test_cmds.sh_2339d09953b6937981d8a448000c3fdc2837f8c4.out
@@ -0,0 +1,12 @@
+Dec 6 13:01:34 ubu-mac avahi-daemon[786]: Joining mDNS multicast group on interface virbr0.IPv4 with address 192.168.122.1.
+Dec 6 13:01:34 ubu-mac avahi-daemon[786]: New relevant interface virbr0.IPv4 for mDNS.
+Dec 6 13:01:34 ubu-mac avahi-daemon[786]: Registering new address record for 192.168.122.1 on virbr0.IPv4.
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: started, version 2.68 cachesize 150
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: compile time options: IPv6 GNU-getopt DBus i18n IDN DHCP DHCPv6 no-Lua TFTP conntrack ipset auth
+Dec 6 13:01:34 ubu-mac dnsmasq-dhcp[1840]: DHCP, IP range 192.168.122.2 -- 192.168.122.254, lease time 1h
+Dec 6 13:01:34 ubu-mac dnsmasq-dhcp[1840]: DHCP, sockets bound exclusively to interface virbr0
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: reading /etc/resolv.conf
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: using nameserver 192.168.1.1#53
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: read /etc/hosts - 5 addresses
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: read /var/lib/libvirt/dnsmasq/default.addnhosts - 0 addresses
+Dec 6 13:01:34 ubu-mac dnsmasq-dhcp[1840]: read /var/lib/libvirt/dnsmasq/default.hostsfile
diff --git a/test/expected/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.err b/test/expected/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.err
diff --git a/test/expected/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.out b/test/expected/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2539ff9c4dbed93df3f0408ccc5fd81df34d1193.out
diff --git a/test/expected/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.err b/test/expected/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.err
diff --git a/test/expected/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.out b/test/expected/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.out
new file mode 100644
index 0000000..721d34f
--- /dev/null
+++ b/test/expected/test_cmds.sh_29f0c808f4e93c6ef3890e6b793bee274a5b36ca.out
@@ -0,0 +1 @@
+Hello, $XYZ!
diff --git a/test/expected/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.err b/test/expected/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.err
diff --git a/test/expected/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.out b/test/expected/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.out
new file mode 100644
index 0000000..b693f51
--- /dev/null
+++ b/test/expected/test_cmds.sh_2a449c0a43e895e85c8b1c9547f32d7b5b4f84f6.out
@@ -0,0 +1 @@
+/ui/clock-format = "%Y-%m-%dT%H:%M:%S %Z"
diff --git a/test/expected/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.err b/test/expected/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.err
diff --git a/test/expected/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.out b/test/expected/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.out
new file mode 100644
index 0000000..6ae2e70
--- /dev/null
+++ b/test/expected/test_cmds.sh_2a535de164de4c060d2bff34aa7cc75ac7cac2c2.out
@@ -0,0 +1,2 @@
+2009-07-20 22:59:27,672:DEBUG:Hello, World!
+ How are you today?
diff --git a/test/expected/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.err b/test/expected/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.err
new file mode 100644
index 0000000..30a1ac4
--- /dev/null
+++ b/test/expected/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.err
@@ -0,0 +1,6 @@
+✘ error: expecting file name or '-' to write to the terminal
+ --> command-option:1
+ | :write-to 
+ = help: :write-to [--anonymize] path
+ ══════════════════════════════════════════════════════════════════════
+ Overwrite the given file with any marked lines in the current view
diff --git a/test/expected/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.out b/test/expected/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2cd167954a3be3e130e5f9601b72794a856cef92.out
diff --git a/test/expected/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.err b/test/expected/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.err
diff --git a/test/expected/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.out b/test/expected/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.out
new file mode 100644
index 0000000..8d4265f
--- /dev/null
+++ b/test/expected/test_cmds.sh_2de9ec294e2f533d13e04c70d9525f8b58d47bb2.out
@@ -0,0 +1,2 @@
+Hello, World!
+Goodbye, World!
diff --git a/test/expected/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.err b/test/expected/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.err
diff --git a/test/expected/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.out b/test/expected/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.out
new file mode 100644
index 0000000..8c91cd9
--- /dev/null
+++ b/test/expected/test_cmds.sh_2e123104cdd2087ac40731a0aa533ba6a87ea744.out
@@ -0,0 +1 @@
+2009-07-20 22:59:30,221:ERROR:Goodbye, World!
diff --git a/test/expected/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.err b/test/expected/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.err
diff --git a/test/expected/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.out b/test/expected/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.out
new file mode 100644
index 0000000..3f55261
--- /dev/null
+++ b/test/expected/test_cmds.sh_2e67bdbbc9a14aa772b2a9f755ed8f8124708558.out
@@ -0,0 +1,23 @@
+Apr 7 00:49:42 Tim-Abaft-iMac abashed[0]: Aberrant [abhorrent5701Aberrant]: Link up on en0, 1-Aboard, Full-abortive, Abounding flow-abrupt, Absent [796d,2301,0de1,0300,cde1,3800]
+Apr 7 05:49:53 Tim-Abaft-iMac.absorbing Abstracted[17212]: -[absurd abundant] absurd abusive accept: <acceptable:0x511f30
+ accessible=<KSOmahaServer:0x510d80>
+ url="https://achondroplasia.example.com/account/accurate2"
+ achiever=0
+ acid=1
+ acidic=1
+ acoustic=1
+ body=
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+ <o:gupdate xmlns:o="http://acinetobacter-infections.example.com/accurate2/acrid" protocol="2.0" version="Act-1.2.0.7709" ismachine="1" requestid="{1ca0a968-cbe9-e75b-d00b-4859609878ea}">
+ <o:os platform="mac" version="activity" sp="10.10.2_x86_64h"></o:os>
+ <o:app appid="com.actually.Ad hoc" version="1.2.0.7709" lang="en-us" installage="180" brand="GGLG">
+ <o:ping r="1" a="1"></o:ping>
+ <o:updatecheck></o:updatecheck>
+ </o:app>
+ </o:gupdate>
+ >
+Apr 7 07:31:56 Tim-Abaft-iMac.absorbing Add[36403]: ADDICTED: The Adhesive adjoining adjustment is admit 10.9.2 adorable of 10.10.2. Use advice's afford afraid to get afterthought aggressive agonizing agree
+ Call agreement:
+Apr 7 07:31:56 Tim-Abaft-iMac.absorbing Add[36403]: 0 Ahead 0x00007fff8a9b3d9b ___Adhesive_Air_airplane_airport + 113
+Apr 7 07:31:56 Tim-Abaft-iMac.absorbing Add[36403]: 1 ajar.alarm 0x00007fff8bc84c13 _alcoholic_alert_alike + 8
+Apr 7 07:32:56 Tim-Abaft-iMac.absorbing alive[234]: Bad data { abc, 123, 456 )}]
diff --git a/test/expected/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.err b/test/expected/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.err
diff --git a/test/expected/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.out b/test/expected/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.out
new file mode 100644
index 0000000..c6eedf2
--- /dev/null
+++ b/test/expected/test_cmds.sh_2ff0fe712c9b0012e42282c5f77b0b83cad37ddf.out
@@ -0,0 +1 @@
+2009-07-20 22:59:30,221:ERROR:Goodbye, World!
diff --git a/test/expected/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.err b/test/expected/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.err
diff --git a/test/expected/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.out b/test/expected/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.out
new file mode 100644
index 0000000..8735c57
--- /dev/null
+++ b/test/expected/test_cmds.sh_305b1dfdfe785b945df4220aad6671ae1d364f55.out
@@ -0,0 +1 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.err b/test/expected/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.err
diff --git a/test/expected/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.out b/test/expected/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_cmds.sh_3429080ed14d01c6a887900186f37750df0d5ff0.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.err b/test/expected/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.err
diff --git a/test/expected/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.out b/test/expected/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.out
new file mode 100644
index 0000000..8ab686e
--- /dev/null
+++ b/test/expected/test_cmds.sh_34a6bcaa2877471b8ea718374101fa9ce3b78235.out
@@ -0,0 +1 @@
+Hello, World!
diff --git a/test/expected/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.err b/test/expected/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.err
diff --git a/test/expected/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.out b/test/expected/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.out
new file mode 100644
index 0000000..fc2b6b9
--- /dev/null
+++ b/test/expected/test_cmds.sh_35b0dd8a030396742bc5acfde7715fb19f312f29.out
@@ -0,0 +1,3 @@
+/ui/clock-format = "%Y-%m-%dT%H:%M:%S %Z"
+info: changed config option -- /ui/clock-format
+/ui/clock-format = "abc"
diff --git a/test/expected/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.err b/test/expected/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.err
new file mode 100644
index 0000000..28d2b21
--- /dev/null
+++ b/test/expected/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.err
@@ -0,0 +1,7 @@
+✘ error: invalid filter expression: :sc_bytes # ff
+ reason: unrecognized token: "#"
+ --> command-option:1
+ | :filter-expr :sc_bytes # ff 
+ = help: :filter-expr expr
+ ══════════════════════════════════════════════════════════════════════
+ Set the filter expression
diff --git a/test/expected/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.out b/test/expected/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_36800217930a6a30e68c4efb20f6959c4f71aeb0.out
diff --git a/test/expected/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.err b/test/expected/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.err
diff --git a/test/expected/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.out b/test/expected/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.out
new file mode 100644
index 0000000..9ea386d
--- /dev/null
+++ b/test/expected/test_cmds.sh_38fa2a95b703d4ce12e82882eca1938264822690.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET ⋮ HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET ⋮ HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET ⋮ HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.err b/test/expected/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.err
diff --git a/test/expected/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.out b/test/expected/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_cmds.sh_3b20a298e2c059d7f6045cbc0c07ca3db3917695.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.err b/test/expected/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.err
diff --git a/test/expected/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.out b/test/expected/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.out
new file mode 100644
index 0000000..dcd3557
--- /dev/null
+++ b/test/expected/test_cmds.sh_453054e29aaca4c2662c45c2a1f2f63f3510d8dd.out
@@ -0,0 +1,2 @@
+2009-07-20 22:59:30,221:ERROR:Goodbye, World!
+2009-07-20 22:59:30,221:ERROR:Goodbye, World!
diff --git a/test/expected/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.err b/test/expected/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.err
diff --git a/test/expected/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.out b/test/expected/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.out
new file mode 100644
index 0000000..8d4265f
--- /dev/null
+++ b/test/expected/test_cmds.sh_4b2d91b19008d5b775090e3ef87c111f9e603b15.out
@@ -0,0 +1,2 @@
+Hello, World!
+Goodbye, World!
diff --git a/test/expected/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.err b/test/expected/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.err
diff --git a/test/expected/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.out b/test/expected/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_cmds.sh_4dbe20c11056a07d2c7efb5ed15903050d628216.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.err b/test/expected/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.err
diff --git a/test/expected/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.out b/test/expected/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.out
new file mode 100644
index 0000000..ef822cd
--- /dev/null
+++ b/test/expected/test_cmds.sh_4f06183ed231669965965f5042fbbb507fa7deab.out
@@ -0,0 +1,3 @@
+2009-07-20 22:59:27,672:DEBUG:Hello, World!
+ How are you today?
+2009-07-20 22:59:30,221:ERROR:Goodbye, World!
diff --git a/test/expected/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.err b/test/expected/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.err
new file mode 100644
index 0000000..60db783
--- /dev/null
+++ b/test/expected/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.err
@@ -0,0 +1,5 @@
+✘ error: cannot open file: /non-existent
+ reason: No such file or directory
+ --> command-option:2
+ | :open /non-existent 
+ = help: make sure the file exists and is accessible
diff --git a/test/expected/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.out b/test/expected/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_512872aebaae73ca4f33fa93acb2f4e3b018f8b4.out
diff --git a/test/expected/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.err b/test/expected/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.err
diff --git a/test/expected/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.out b/test/expected/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.out
new file mode 100644
index 0000000..39589f4
--- /dev/null
+++ b/test/expected/test_cmds.sh_53a9686102f69b07b034df291f554a00b265ed20.out
@@ -0,0 +1,2 @@
+toplevel here 123 456
+nested here nested.lnav abc 789
diff --git a/test/expected/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.err b/test/expected/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.err
new file mode 100644
index 0000000..e90b75a
--- /dev/null
+++ b/test/expected/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.err
@@ -0,0 +1,4 @@
+✘ error: unknown bookmark type: foobar
+ --> command-option:2
+ | :next-mark foobar 
+ = help: available types: error, file, meta, search, user, user-expr, warning
diff --git a/test/expected/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.out b/test/expected/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_55c2fd15ec2c7d96dbef7b36a42a1b7b42f90dbc.out
diff --git a/test/expected/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.err b/test/expected/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.err
diff --git a/test/expected/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.out b/test/expected/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_cmds.sh_5bfd08c1639701476d7b9348c36afd46fdbe6f2a.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.err b/test/expected/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.err
diff --git a/test/expected/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.out b/test/expected/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.out
new file mode 100644
index 0000000..565e1c6
--- /dev/null
+++ b/test/expected/test_cmds.sh_624a41e152675575f4b07c19b2cf0e3a028429a2.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.err b/test/expected/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.err
diff --git a/test/expected/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.out b/test/expected/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.out
new file mode 100644
index 0000000..493283c
--- /dev/null
+++ b/test/expected/test_cmds.sh_62d68c0a11757c996f24c8f003e6b4059c3e30b2.out
@@ -0,0 +1 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.err b/test/expected/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.err
diff --git a/test/expected/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.out b/test/expected/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.out
new file mode 100644
index 0000000..541a9b8
--- /dev/null
+++ b/test/expected/test_cmds.sh_661ec61acdd8f6fa6ec1e3c2cf5f896eef431351.out
@@ -0,0 +1,14 @@
+''
+{
+ "foo bar" : null,
+  "array" : [
+  1,
+  2,
+  3
+ ],
+ "obj" : {
+ "one" : 1,
+ "two" : true
+ }
+}
+''
diff --git a/test/expected/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.err b/test/expected/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.err
new file mode 100644
index 0000000..bcc3200
--- /dev/null
+++ b/test/expected/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.err
@@ -0,0 +1,8 @@
+✘ error: expecting file name to open
+ --> command-option:1
+ | :open 
+ = help: :open path1 [... pathN]
+ ══════════════════════════════════════════════════════════════════════
+ Open the given file(s) in lnav. Opening files on machines
+ accessible via SSH can be done using the syntax:
+ [user@]host:/path/to/logs
diff --git a/test/expected/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.out b/test/expected/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_6a6031113aca32fabc5a3da64b7be46f5ce5a312.out
diff --git a/test/expected/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.err b/test/expected/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.err
new file mode 100644
index 0000000..6c8e8ae
--- /dev/null
+++ b/test/expected/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.err
@@ -0,0 +1,4 @@
+✘ error: invalid zoom level: bad
+ --> command-option:1
+ | :zoom-to bad 
+ = help: available levels: 1-second, 30-second, 1-minute, 5-minute, 15-minute, 1-hour, 4-hour, 8-hour, 1-day, 1-week
diff --git a/test/expected/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.out b/test/expected/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_6e016c0ed61fc652be1a79b864875ffede64f281.out
diff --git a/test/expected/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.err b/test/expected/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.err
diff --git a/test/expected/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.out b/test/expected/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.out
new file mode 100644
index 0000000..bccea86
--- /dev/null
+++ b/test/expected/test_cmds.sh_7270e37dab4549cfa7c5232451c031e1e04b4aef.out
@@ -0,0 +1 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.err b/test/expected/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.err
diff --git a/test/expected/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.out b/test/expected/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_cmds.sh_73ea99c84fb1d4570e8bcd45c423b4a28fe41e81.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.err b/test/expected/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.err
diff --git a/test/expected/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.out b/test/expected/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.out
new file mode 100644
index 0000000..7ad9d78
--- /dev/null
+++ b/test/expected/test_cmds.sh_7cb644890c4b945ff3f1e15c86a58c85cb5425c0.out
@@ -0,0 +1,5 @@
+┏━━┳━━━━━━━━━━━━━┓
+┃c1┃ c2 ┃
+┡━━╇━━━━━━━━━━━━━┩
+│ 1│Hello, World!│
+└━━┴━━━━━━━━━━━━━┘
diff --git a/test/expected/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.err b/test/expected/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.err
new file mode 100644
index 0000000..22e6c3e
--- /dev/null
+++ b/test/expected/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.err
@@ -0,0 +1,6 @@
+✘ error: expecting an SQL expression
+ --> command-option:1
+ | :mark-expr 
+ = help: :mark-expr expr
+ ══════════════════════════════════════════════════════════════════════
+ Set the bookmark expression
diff --git a/test/expected/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.out b/test/expected/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_7e14e7f18219719453838835fa96c3451f78996d.out
diff --git a/test/expected/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.err b/test/expected/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.err
diff --git a/test/expected/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.out b/test/expected/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.out
new file mode 100644
index 0000000..67f6093
--- /dev/null
+++ b/test/expected/test_cmds.sh_819b3dd21348f7242f3914ad0a8c5b1cdb3f91af.out
@@ -0,0 +1 @@
+Hello: Jules
diff --git a/test/expected/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.err b/test/expected/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.err
diff --git a/test/expected/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.out b/test/expected/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.out
new file mode 100644
index 0000000..220b552
--- /dev/null
+++ b/test/expected/test_cmds.sh_8298805f897346b4bb0f14e53c06b4fa28e309e3.out
@@ -0,0 +1,3 @@
+⋮ - - [20/Jul/2009:22:59:26 +0000] "GET ⋮ HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+⋮ - - [20/Jul/2009:22:59:29 +0000] "GET ⋮ HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+⋮ - - [20/Jul/2009:22:59:29 +0000] "GET ⋮ HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.err b/test/expected/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.err
diff --git a/test/expected/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.out b/test/expected/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.out
new file mode 100644
index 0000000..a90f29f
--- /dev/null
+++ b/test/expected/test_cmds.sh_83654557317602d2e00adde1e5cba190d9db0dff.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.err b/test/expected/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.err
diff --git a/test/expected/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.out b/test/expected/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.out
new file mode 100644
index 0000000..6be811e
--- /dev/null
+++ b/test/expected/test_cmds.sh_85ae6ac1eb9a8378f7a6c39659f52671218ce64b.out
@@ -0,0 +1 @@
+1Hello, World!
diff --git a/test/expected/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.err b/test/expected/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.err
diff --git a/test/expected/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.out b/test/expected/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.out
new file mode 100644
index 0000000..f586ffe
--- /dev/null
+++ b/test/expected/test_cmds.sh_85ed177028f226e86b1d164eb1a4e18eaf036c9d.out
@@ -0,0 +1 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.err b/test/expected/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.err
diff --git a/test/expected/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.out b/test/expected/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.out
new file mode 100644
index 0000000..493283c
--- /dev/null
+++ b/test/expected/test_cmds.sh_8758082427d6232a15053433942a4b5ad9f2e3ce.out
@@ -0,0 +1 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.err b/test/expected/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.err
new file mode 100644
index 0000000..2276bac
--- /dev/null
+++ b/test/expected/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.err
@@ -0,0 +1,6 @@
+✘ error: expecting a unix time value
+ --> command-option:1
+ | :unix-time 
+ = help: :unix-time seconds
+ ══════════════════════════════════════════════════════════════════════
+ Convert epoch time to a human-readable form
diff --git a/test/expected/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.out b/test/expected/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_876116da8ab46c0c8a212ce230d1b8a13970f78f.out
diff --git a/test/expected/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.err b/test/expected/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.err
diff --git a/test/expected/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.out b/test/expected/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.out
new file mode 100644
index 0000000..330e041
--- /dev/null
+++ b/test/expected/test_cmds.sh_8765cbf326648e9014f8cf5f761895010fff443a.out
@@ -0,0 +1,37 @@
+2015-03-12T23:16:52.071:INFO:com.root:Response :
+ <?xml version="1.0"?>
+<response>
+ <locale>en-US</locale>
+ <requestid>ipInfo</requestid>
+ <value id="ipv4Gateway" actions="enabled">198.51.100.253</value>
+ <value id="ipv6Gateway" actions="enabled"/>
+ <value id="ipv6Enabled" actions="enabled">true</value>
+ <value id="ipv4Enabled" actions="enabled">true</value>
+ <value id="name" actions="enabled">nic1</value>
+ <value id="v4config" actions="enabled">
+ <value id="defaultGateway" actions="enabled">0.0.0.0</value>
+ <value id="updateable" actions="enabled">True</value>
+ <value id="prefix" actions="enabled">22</value>
+ <value id="mode" actions="enabled">dhcp</value>
+ <value id="address" actions="enabled">198.51.100.110</value>
+ <value id="interface" actions="enabled">nic1</value>
+ </value>
+ <value id="v6config" actions="enabled">
+ <value id="defaultGateway" actions="enabled">fe80::214:f609:19f7:6bf1</value>
+ <value id="updateable" actions="enabled">True</value>
+ <value id="interface" actions="enabled">nic1</value>
+ <value id="dhcp" actions="enabled">False</value>
+ <value id="autoconf" actions="enabled">False</value>
+ <value id="addresses" actions="enabled">
+ <value id="origin" actions="enabled">other</value>
+ <value id="status" actions="enabled">preferred</value>
+ <value id="prefix" actions="enabled">64</value>
+ <value id="address" actions="enabled">fe80::250:56ff:feaa:5abf</value>
+ </value>
+ </value>
+ <value id="interfaceInfo" actions="enabled">
+ <value id="status" actions="enabled">up</value>
+ <value id="mac" actions="enabled">00:50:56:aa:5a:bf</value>
+ <value id="name" actions="enabled">nic1</value>
+ </value>
+</response>
diff --git a/test/expected/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.err b/test/expected/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.err
new file mode 100644
index 0000000..492a07d
--- /dev/null
+++ b/test/expected/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.err
@@ -0,0 +1,6 @@
+✘ error: write-json-to -- unavailable in secure mode
+ --> command-option:2
+ | :write-json-to /tmp/bad 
+ = help: :write-json-to [--anonymize] path
+ ══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in JSON format
diff --git a/test/expected/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.out b/test/expected/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_89afa826d1b33be6926df48443faa1d1c5f285a7.out
diff --git a/test/expected/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.err b/test/expected/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.err
diff --git a/test/expected/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.out b/test/expected/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.out
new file mode 100644
index 0000000..3cc484d
--- /dev/null
+++ b/test/expected/test_cmds.sh_8d5b43c693e78804a8fb06989392fa8cccb46b7b.out
@@ -0,0 +1,2 @@
+info: hiding lines before 2009-07-20 22:59:29.000
+info: hiding lines before 2009-07-20 22:59:29.000
diff --git a/test/expected/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.err b/test/expected/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.err
diff --git a/test/expected/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.out b/test/expected/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_cmds.sh_9445861db011dfa2d21a44788047de345ee291e8.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.err b/test/expected/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.err
diff --git a/test/expected/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.out b/test/expected/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_cmds.sh_95beaabe41d72cf4c6810e79c623da759ac1c71b.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.err b/test/expected/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.err
diff --git a/test/expected/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.out b/test/expected/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.out
new file mode 100644
index 0000000..2678e6c
--- /dev/null
+++ b/test/expected/test_cmds.sh_968dac54dc80d91a5da2322890c6c26dfa0d8462.out
@@ -0,0 +1 @@
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
diff --git a/test/expected/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.err b/test/expected/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.err
diff --git a/test/expected/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.out b/test/expected/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.out
new file mode 100644
index 0000000..01761e9
--- /dev/null
+++ b/test/expected/test_cmds.sh_a00943ef715598c7554b85de8502454e41bb9e28.out
@@ -0,0 +1,4 @@
+ Thu Nov 03 09:20:00  1 normal 2 errors 0 warnings  0 marks
+ Thu Nov 03 09:45:00  1 normal 0 errors 0 warnings 0 marks
+ Fri Feb 03 09:20:00  0 normal 1 errors 0 warnings 0 marks
+ Wed Jan 03 09:20:00  1 normal 0 errors 0 warnings 0 marks
diff --git a/test/expected/test_cmds.sh_a0e6214b2a85c90d31aee12efde850441cca7eb3.err b/test/expected/test_cmds.sh_a0e6214b2a85c90d31aee12efde850441cca7eb3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_a0e6214b2a85c90d31aee12efde850441cca7eb3.err
diff --git a/test/expected/test_cmds.sh_a0e6214b2a85c90d31aee12efde850441cca7eb3.out b/test/expected/test_cmds.sh_a0e6214b2a85c90d31aee12efde850441cca7eb3.out
new file mode 100644
index 0000000..1d3eae4
--- /dev/null
+++ b/test/expected/test_cmds.sh_a0e6214b2a85c90d31aee12efde850441cca7eb3.out
@@ -0,0 +1,2 @@
+log_top_line() 
+ 51
diff --git a/test/expected/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.err b/test/expected/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.err
diff --git a/test/expected/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.out b/test/expected/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_cmds.sh_a1123427c31c022433d66d05ee5d5e1c8ab415e4.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.err b/test/expected/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.err
new file mode 100644
index 0000000..5f4c49b
--- /dev/null
+++ b/test/expected/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.err
@@ -0,0 +1,3 @@
+✘ error: unknown script -- nonexistent.lnav -- file not found
+ --> command-option:1
+ | |nonexistent.lnav 
diff --git a/test/expected/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.out b/test/expected/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_a190bfc279fa046a823864f1484f899d27d22953.out
diff --git a/test/expected/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.err b/test/expected/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.err
new file mode 100644
index 0000000..1c20a91
--- /dev/null
+++ b/test/expected/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.err
@@ -0,0 +1,6 @@
+✘ error: unknown configuration option -- /bad/option
+ --> command-option:1
+ | :config /bad/option 
+ = help: :config option [value]
+ ══════════════════════════════════════════════════════════════════════
+ Read or write a configuration option
diff --git a/test/expected/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.out b/test/expected/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_a5742238bad948b1372d32f7a491f03fa4e8b711.out
diff --git a/test/expected/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.err b/test/expected/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.err
diff --git a/test/expected/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.out b/test/expected/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.out
new file mode 100644
index 0000000..c3813a5
--- /dev/null
+++ b/test/expected/test_cmds.sh_a6c431f2871ea96cfdf4e11465b3bca543c7b678.out
@@ -0,0 +1,10 @@
+Sep 13 03:12:04 Tim-Stacks-iMac kernel[0]: vm_compressor_record_warmup (9478314 - 9492476)
+Sep 13 03:12:04 Tim-Stacks-iMac kernel[0]: AppleBCM5701Ethernet [en0]: 0 0 memWrInd fBJP_Wakeup_Timer
+Sep 13 01:25:39 Tim-Stacks-iMac kernel[0]: AppleThunderboltNHIType2::waitForOk2Go2Sx - retries = 60000
+Sep 13 03:12:04 Tim-Stacks-iMac kernel[0]: hibernate_page_list_setall(preflight 0) start 0xffffff8428276000, 0xffffff8428336000
+Sep 13 03:12:58 Tim-Stacks-iMac kernel[0]: *** kernel exceeded 500 log message per second limit - remaining messages this second discarded ***
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: IOThunderboltSwitch<0xffffff803f4b3000>(0x0)::listenerCallback - Thunderbolt HPD packet for route = 0x0 port = 11 unplug = 0
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: vm_compressor_flush - starting
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: AppleBCM5701Ethernet [en0]: 0 0 memWrInd fBJP_Wakeup_Timer
+Sep 13 03:13:16 Tim-Stacks-iMac kernel[0]: AppleThunderboltNHIType2::waitForOk2Go2Sx - retries = 60000
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: hibernate_page_list_setall(preflight 0) start 0xffffff838f1fc000, 0xffffff838f2bc000
diff --git a/test/expected/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.err b/test/expected/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.err
new file mode 100644
index 0000000..644b202
--- /dev/null
+++ b/test/expected/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.err
@@ -0,0 +1,7 @@
+✘ error: filter limit reached, try combining filters with a pipe symbol (e.g. foo|bar)
+ --> command-option:32
+ | :filter-out 32 
+ = help: :filter-out pattern
+ ══════════════════════════════════════════════════════════════════════
+ Remove lines that match the given regular expression in the current
+ view
diff --git a/test/expected/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.out b/test/expected/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_a8006c4169d76baecd99a0699c2fc66a583ad676.out
diff --git a/test/expected/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.err b/test/expected/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.err
diff --git a/test/expected/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.out b/test/expected/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.out
new file mode 100644
index 0000000..fa6a319
--- /dev/null
+++ b/test/expected/test_cmds.sh_ac45fb0f8f9578c3ded0855f694698ec38ce31ad.out
@@ -0,0 +1,12 @@
+{
+ "foo bar": null,
+ "array": [
+ 1,
+ 2,
+ 3
+ ],
+ "obj": {
+ "one": 1,
+ "two": true
+ }
+}
diff --git a/test/expected/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.err b/test/expected/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.err
diff --git a/test/expected/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.out b/test/expected/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.out
new file mode 100644
index 0000000..be06882
--- /dev/null
+++ b/test/expected/test_cmds.sh_af0fcbd30b3fd0d13477aa3325ef0302052a4d9f.out
@@ -0,0 +1 @@
+ Sat Nov 03 08:00:00 1 normal 0 errors 0 warnings 0 marks
diff --git a/test/expected/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.err b/test/expected/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.err
diff --git a/test/expected/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.out b/test/expected/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.out
new file mode 100644
index 0000000..8ab686e
--- /dev/null
+++ b/test/expected/test_cmds.sh_b5a530d16c982cf769151291f0bfd612ea71183f.out
@@ -0,0 +1 @@
+Hello, World!
diff --git a/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.err b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.err
diff --git a/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out
new file mode 100644
index 0000000..aca5279
--- /dev/null
+++ b/test/expected/test_cmds.sh_b6a3bb78e9d60e5e1f5ce5b18e40d2f1662707ab.out
@@ -0,0 +1,4422 @@
+
+lnav
+
+A fancy log file viewer for the terminal.
+
+Overview
+
+The Logfile Navigator, lnav, is an enhanced log file viewer that takes
+advantage of any semantic information that can be gleaned from the
+files being viewed, such as timestamps and log levels. Using this
+extra semantic information, lnav can do things like interleaving
+messages from different files, generate histograms of messages over
+time, and providing hotkeys for navigating through the file. It is
+hoped that these features will allow the user to quickly and
+efficiently zero in on problems.
+
+Opening Paths/URLs
+
+The main arguments to lnav are the local/remote files, directories,
+glob patterns, or URLs to be viewed. If no arguments are given, the
+default syslog file for your system will be opened. These arguments
+will be polled periodically so that any new data or files will be
+automatically loaded. If a previously loaded file is removed or
+replaced, it will be closed and the replacement opened.
+
+Note: When opening SFTP URLs, if the password is not provided for the
+host, the SSH agent can be used to do authentication.
+
+Options
+
+Lnav takes a list of files to view and/or you can use the flag
+arguments to load well-known log files, such as the syslog log files.
+The flag arguments are:
+
+ •  -a  Load all of the most recent log file types.
+ •  -r  Recursively load files from the given directory
+ hierarchies.
+ •  -R  Load older rotated log files as well.
+
+When using the flag arguments, lnav will look for the files relative
+to the current directory and its parent directories. In other words,
+if you are working within a directory that has the well-known log
+files, those will be preferred over any others.
+
+If you do not want the default syslog file to be loaded when no files
+are specified, you can pass the  -N  flag.
+
+Any files given on the command-line are scanned to determine their log
+file format and to create an index for each line in the file. You do
+not have to manually specify the log file format. The currently
+supported formats are: syslog, apache, strace, tcsh history, and
+generic log files with timestamps.
+
+Lnav will also display data piped in on the standard input. The
+following options are available when doing so:
+
+ •  -t  Prepend timestamps to the lines of data being read
+ in on the standard input.
+ •  -w file  Write the contents of the standard input to
+ this file.
+
+To automatically execute queries or lnav commands after the files have
+been loaded, you can use the following options:
+
+ •  -c cmd  A command, query, or file to execute. The
+ first character determines the type of operation: a colon
+ ( : ) is used for the built-in commands; a semi-colon ( ;
+ ) for SQL queries; and a pipe symbol ( | ) for executing
+ a file containing other commands. For example, to open
+ the file "foo.log" and go to the tenth line in the file,
+ you can do:
+
+ ▌lnav -c ':goto 10' foo.log 
+
+ This option can be given multiple times to execute
+ multiple operations in sequence.
+ •  -f file  A file that contains commands, queries, or
+ files to execute. This option is a shortcut for  -c '|file'
+ . You can use a dash ( - ) to execute commands from the
+ standard input.
+
+To execute commands/queries without opening the interactive text UI,
+you can pass the  -n  option. This combination of options allows you
+to write scripts for processing logs with lnav. For example, to get a
+list of IP addresses that dhclient has bound to in CSV format:
+
+ ▌#! /usr/bin/lnav -nf 
+ ▌ 
+ ▌# Usage: dhcp_ip.lnav /var/log/messages 
+ ▌# Only include lines that look like: 
+ ▌# Apr 29 00:31:56 example-centos5 dhclient: bound to 10.1.10.103 -- renewal in 9938 seconds. 
+ ▌ 
+ ▌:filter-in dhclient: bound to 
+ ▌ 
+ ▌# The log message parser will extract the IP address 
+ ▌# as col_0, so we select that and alias it to "dhcp_ip". 
+ ▌;SELECT DISTINCT col_0 AS dhcp_ip FROM logline; 
+ ▌ 
+ ▌# Finally, write the results of the query to stdout. 
+ ▌:write-csv-to - 
+
+Display
+
+The main part of the display shows the log lines from the files
+interleaved based on time-of-day. New lines are automatically loaded
+as they are appended to the files and, if you are viewing the bottom
+of the files, lnav will scroll down to display the new lines, much
+like  tail -f .
+
+On color displays, the lines will be highlighted as follows:
+
+ • Errors will be colored in red;
+ • warnings will be yellow;
+ • boundaries between days will be underlined; and
+ • various color highlights will be applied to: IP
+ addresses, SQL keywords, XML tags, file and line numbers
+ in Java backtraces, and quoted strings.
+
+To give you an idea of where you are spatially, the right side of the
+display has a proportionally sized 'scroll bar' that indicates your
+current position in the files. The scroll bar will also show areas of
+the file where warnings or errors are detected by coloring the bar
+yellow or red, respectively. Tick marks will also be added to the left
+and right-hand side of the bar, for search hits and bookmarks.
+
+The bar on the left side indicates the file the log message is from. A
+break in the bar means that the next log message comes from a
+different file. The color of the bar is derived from the file name.
+Pressing the left-arrow or  h  will reveal the source file names for
+each message and pressing again will show the full paths.
+
+Above and below the main body are status lines that display a variety
+of information. The top line displays:
+
+ • The current time, configurable by the  /ui/clock-format 
+ property.
+ • The highest priority message from the  lnav_user_notifications 
+ table. You can insert rows into this table to display
+ your own status messages. The default message displayed
+ on startup explains how to focus on the next status line
+ at the top, which is an interactive breadcrumb bar.
+
+The second status line at the top display breadcrumbs for the top line
+in the main view. Pressing  ENTER  will focus input on the breadcrumb
+bar, the cursor keys can be used to select a breadcrumb. The common
+breadcrumbs are:
+
+ • The name of the current view.
+ • In the log view, the timestamp of the top log message.
+ • In the log view, the format of the log file the top log
+ message is from.
+ • The name of the file the top line was pulled from.
+ • If the top line is within a larger chunk of structured
+ data, the path to the value in the top line will be
+ shown.
+
+Notes:
+
+ 1. Pressing  CTRL-A / CTRL-E  will select the first/last
+ breadcrumb.
+ 2. Typing text while a breadcrumb is selected will
+ perform a fuzzy search on the possibilities.
+
+The bottom status bar displays:
+
+ • The line number for the top line in the display.
+ • The current search hit, the total number of hits, and
+ the search term.
+
+If the view supports filtering, there will be a status line showing
+the following:
+
+ • The number of enabled filters and the total number of
+ filters.
+ • The number of lines not displayed because of filtering.
+
+To edit the filters, you can press TAB to change the focus from the
+main view to the filter editor. The editor allows you to create,
+enable/disable, and delete filters easily.
+
+Along with filters, a "Files" panel will also be available for viewing
+and controlling the files that lnav is currently monitoring.
+
+Finally, the last line on the display is where you can enter search
+patterns and execute internal commands, such as converting a
+unix-timestamp into a human-readable date. The command-line is
+implemented using the readline library, so the usual set of keyboard
+shortcuts are available. Most commands and searches also support
+tab-completion.
+
+The body of the display is also used to display other content, such
+as: the help file, histograms of the log messages over time, and SQL
+results. The views are organized into a stack so that any time you
+activate a new view with a key press or command, the new view is
+pushed onto the stack. Pressing the same key again will pop the view
+off of the stack and return you to the previous view. Note that you
+can always use  q  to pop the top view off of the stack.
+
+Default Key Bindings
+
+Views
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════
+ ? View/leave this help message.
+ q Leave the current view or quit the program when in
+ the log file view.
+ Q Similar to  q , except it will try to sync the top
+ time between the current and former views. For
+ example, when leaving the spectrogram view with  Q
+ , the top time in that view will be matched to the
+ top time in the log view.
+ TAB Toggle focusing on the filter editor or the main
+ view.
+ ENTER Focus on the breadcrumb bar.
+ a/A Restore the view that was previously popped with  q
+ / Q . The  A  hotkey will try to match the top
+ times between the two views.
+ X Close the current text file or log file.
+
+Spatial Navigation
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════════
+ g/Home Move to the top of the file.
+ G/End Move to the end of the file. If the view is
+ already at the end, it will move to the last line.
+ SPACE/PgDn Move down a page.
+ CTRL+d Move down by half a page.
+ b/PgUp Move up a page.
+ CTRL+u Move up by half a page.
+ j/↓ Move down a line.
+ k/↑ Move up a line.
+ h/← Move to the left. In the log view, moving left
+ will reveal the source log file names for each
+ line. Pressing again will reveal the full path.
+ l/→ Move to the right.
+ H/Shift ← Move to the left by a smaller increment.
+ L/Shift → Move to the right by a smaller increment.
+ e/E Move to the next/previous error.
+ w/W Move to the next/previous warning.
+ n/N Move to the next/previous search hit. When pressed
+ repeatedly within a short time, the view will move
+ at least a full page at a time instead of moving
+ to the next hit.
+ f/F Move to the next/previous file. In the log view,
+ this moves to the next line from a different file.
+ In the text view, this rotates the view to the
+ next file.
+ >/< Move horizontally to the next/previous search hit.
+ o/O Move forward/backward to the log message with a
+ matching 'operation ID' (opid) field.
+ u/U Move forward/backward through any user bookmarks
+ you have added using the 'm' key. This hotkey will
+ also jump to the start of any log partitions that
+ have been created with the 'partition-name'
+ command.
+ s/S Move to the next/previous "slow down" in the log
+ message rate. A slow down is detected by measuring
+ how quickly the message rate has changed over the
+ previous several messages. For example, if one
+ message is logged every second for five seconds
+ and then the last message arrives five seconds
+ later, the last message will be highlighted as a
+ slow down.
+ {/} Move to the previous/next location in history.
+ Whenever you jump to a new location in the view,
+ the location will be added to the history. The
+ history is not updated when using only the arrow
+ keys.
+
+Chronological Navigation
+
+ Key(s) Action
+══════════════════════════════════════════════════════════════════
+ d/D Move forward/backward 24 hours from the current
+ position in the log file.
+ 1-6/Shift 1-6 Move to the next/previous n'th ten minute of the
+ hour. For example, '4' would move to the first log
+ line in the fortieth minute of the current hour in
+ the log. And, '6' would move to the next hour
+ boundary.
+ 7/8 Move to the previous/next minute.
+ 0/Shift 0 Move to the next/previous day boundary.
+ r/R Move forward/backward based on the relative time
+ that was last used with the 'goto' command. For
+ example, executing ':goto a minute later' will
+ move the log view forward a minute and then
+ pressing 'r' will move it forward a minute again.
+ Pressing 'R' will then move the view in the
+ opposite direction, so backwards a minute.
+
+Bookmarks
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════
+ m Mark/unmark the line at the top of the display.
+ The line will be highlighted with reverse video to
+ indicate that it is a user bookmark. You can use
+ the  u  hotkey to iterate through marks you have
+ added.
+ M Mark/unmark all the lines between the top of the
+ display and the last line marked/unmarked.
+ J Mark/unmark the next line after the previously
+ marked line.
+ K Like  J  except it toggles the mark on the
+ previous line.
+ c Copy the marked text to the X11 selection buffer
+ or OS X clipboard.
+ C Clear all marked lines.
+
+Display options
+
+ Key(s) Action
+══════════════════════════════════════════════════════════════════
+ P Switch to/from the pretty-printed view of the log
+ or text files currently displayed. In this view,
+ structured data, such as XML, will be reformatted
+ to make it easier to read.
+ t Switch to/from the text file view. The text file
+ view is for any files that are not recognized as
+ log files.
+ = Pause/unpause loading of new file data.
+ Ctrl-L (Lo-fi mode) Exit screen-mode and write the
+ displayed log lines in plain text to the terminal
+ until a key is pressed. Useful for copying long
+ lines from the terminal without picking up any of
+ the extra decorations.
+ T Toggle the display of the "elapsed time" column
+ that shows the time elapsed since the beginning of
+ the logs or the offset from the previous bookmark.
+ Sharp changes in the message rate are highlighted
+ by coloring the separator between the time column
+ and the log message. A red highlight means the
+ message rate has slowed down and green means it
+ has sped up. You can use the "s/S" hotkeys to scan
+ through the slow downs.
+ i View/leave a histogram of the log messages over
+ time. The histogram counts the number of displayed
+ log lines for each bucket of time. The bars are
+ layed out horizontally with colored segments
+ representing the different log levels. You can use
+ the  z  hotkey to change the size of the time
+ buckets (e.g. ten minutes, one hour, one day).
+ I Switch between the log and histogram views while
+ keeping the time displayed at the top of each view
+ in sync. For example, if the top line in the log
+ view is "11:40", hitting  I  will switch to the
+ histogram view and scrolled to display "11:00" at
+ the top (if the zoom level is hours).
+ z/Shift Z Zoom in or out one step in the histogram view.
+ v Switch to/from the SQL result view.
+ V Switch between the log and SQL result views while
+ keeping the top line number in the log view in
+ sync with the log_line column in the SQL view. For
+ example, doing a query that selects for
+ "log_idle_msecs" and "log_line", you can move the
+ top of the SQL view to a line and hit 'V' to
+ switch to the log view and move to the line number
+ that was selected in the "log_line" column. If
+ there is no "log_line" column, lnav will find the
+ first column with a timestamp and move to
+ corresponding time in the log view.
+ TAB/Shift TAB In the SQL result view, cycle through the columns
+ that are graphed. Initially, all number values are
+ displayed in a stacked graph. Pressing TAB will
+ change the display to only graph the first column.
+ Repeatedly pressing TAB will cycle through the
+ columns until they are all graphed again.
+ p In the log view: enable or disable the display of
+ the fields that the log message parser knows about
+ or has discovered. This overlay is temporarily
+ enabled when the semicolon key (;) is pressed so
+ that it is easier to write queries.
+ In the DB view: enable or disable the display of
+ values in columns containing JSON-encoded values
+ in the top row. The overlay will display the
+ JSON-Pointer reference and value for all fields in
+ the JSON data.
+ CTRL-W Toggle word-wrapping.
+ CTRL-P Show/hide the data preview panel that may be
+ opened when entering commands or SQL queries.
+ CTRL-F Toggle the enabled/disabled state of all filters
+ in the current view.
+ x Toggle the hiding of log message fields. The
+ hidden fields will be replaced with three bullets
+ and highlighted in yellow.
+ CTRL-X Toggle the cursor mode. Allows moving the selected
+ line instead of keeping it fixed at the top of the
+ current screen.
+ F2 Toggle mouse support.
+
+Query
+
+ Key(s) Action
+════════════════════════════════════════════════════════════════════════
+ /regexp Start a search for the given regular expression.
+ The search is live, so when there is a pause in
+ typing, the currently running search will be
+ canceled and a new one started. The first ten
+ lines that match the search will be displayed in
+ the preview window at the bottom of the view.
+ History is maintained for your searches so you can
+ rerun them easily. Words that are currently
+ displayed are also available for tab-completion,
+ so you can easily search for values without
+ needing to copy-and-paste the string. If there is
+ an error encountered while trying to interpret the
+ expression, the error will be displayed in red on
+ the status line. While the search is active, the
+ 'hits' field in the status line will be green,
+ when finished it will turn back to black.
+ :<command> Execute an internal command. The commands are
+ listed below. History is also supported in this
+ context as well as tab-completion for commands and
+ some arguments. The result of the command replaces
+ the command you typed.
+ ;<sql> Execute an SQL query. Most supported log file
+ formats provide a sqlite virtual table backend
+ that can be used in queries. See the SQL section
+ below for more information.
+ |<script> [arg1...] Execute an lnav script contained in a format
+ directory (e.g. ~/.lnav/formats/default). The
+ script can contain lines starting with  : ,  ; ,
+ or  |  to execute commands, SQL queries or execute
+ other files in lnav. Any values after the script
+ name are treated as arguments can be referenced in
+ the script using  $1 ,  $2 , and so on, like in a
+ shell script.
+ CTRL+], ESCAPE Abort command-line entry started with  / ,  : ,  ;
+ , or  | .
+
+ ▌Note: The regular expression format used by lnav is PCRE[1]
+ ▌(Perl-Compatible Regular Expressions).
+ ▌
+ ▌ ▌[1] - http://perldoc.perl.org/perlre.html
+ ▌
+ ▌If the search string is not valid PCRE, a search is done for
+ ▌the exact string instead of doing a regex search.
+
+Session
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════
+ CTRL-R Reset the session state. This will save the
+ current session state (filters, highlights) and
+ then reset the state to the factory default.
+
+Filter Editor
+
+The following hotkeys are only available when the focus is on the
+filter editor. You can change the focus by pressing TAB.
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════
+ q Switch the focus back to the main view.
+ j/↓ Select the next filter.
+ k/↑ Select the previous filter.
+ o Create a new "out" filter.
+ i Create a new "in" filter .
+ SPACE Toggle the enabled/disabled state of the currently
+ selected filter.
+ t Toggle the type of filter between "in" and "out".
+ ENTER Edit the selected filter.
+ D Delete the selected filter.
+
+Mouse Support (experimental)
+
+If you are using Xterm, or a compatible terminal, you can use the
+mouse to mark lines of text and move the view by grabbing the
+scrollbar.
+
+NOTE: You need to manually enable this feature by setting the LNAV_EXP
+environment variable to "mouse". F2 toggles mouse support.
+
+SQL Queries (experimental)
+
+Lnav has support for performing SQL queries on log files using the
+Sqlite3 "virtual" table feature. For all supported log file types,
+lnav will create tables that can be queried using the subset of SQL
+that is supported by Sqlite3. For example, to get the top ten URLs
+being accessed in any loaded Apache log files, you can execute:
+
+ ▌;SELECT cs_uri_stem, count(*) AS total FROM access_log 
+ ▌ GROUP BY cs_uri_stem ORDER BY total DESC LIMIT 10; 
+
+The query result view shows the results and graphs any numeric values
+found in the result, much like the histogram view.
+
+The builtin set of log tables are listed below. Note that only the log
+messages that match a particular format can be queried by a particular
+table. You can find the file format and table name for the top log
+message by looking in the upper right hand corner of the log file
+view.
+
+Some commonly used format tables are:
+
+ Name Description
+════════════════════════════════════════════════════════════════
+ access_log Apache common access log format
+ syslog_log Syslog format
+ strace_log Strace log format
+ generic_log 'Generic' log format. This table contains messages
+ from files that have a very simple format with a
+ leading timestamp followed by the message.
+
+NOTE: You can get a dump of the schema for the internal tables, and
+any attached databases, by running the  .schema  SQL command.
+
+The columns available for the top log line in the view will
+automatically be displayed after pressing the semicolon ( ; ) key. All
+log tables contain at least the following columns:
+
+ Column Description
+═══════════════════════════════════════════════════════════════════
+ log_line The line number in the file, starting at zero.
+ log_part The name of the partition. You can change this
+ column using an UPDATE SQL statement or with the
+ 'partition-name' command. After a value is set,
+ the following log messages will have the same
+ partition name up until another name is set.
+ log_time The time of the log entry.
+ log_idle_msecs The amount of time, in milliseconds, between the
+ current log message and the previous one.
+ log_level The log level (e.g. info, error, etc...).
+ log_mark The bookmark status for the line. This column can
+ be written to using an UPDATE query.
+ log_path The full path to the file.
+ log_text The raw line of text. Note that this column is
+ not included in the result of a 'select *', but it
+ does exist.
+
+The following tables include the basic columns as listed above and
+include a few more columns since the log file format is more
+structured.
+
+ •  syslog_log 
+
+ Column Description
+ ═════════════════════════════════════════════════════════════════
+ log_hostname The hostname the message was received from.
+ log_procname The name of the process that sent the message.
+ log_pid The process ID of the process that sent the
+ message.
+ •  access_log  (The column names are the same as those in
+ the Microsoft LogParser tool.)
+
+ Column Description
+ ══════════════════════════════════════════════════════════
+ c_ip The client IP address.
+ cs_username The client user name.
+ cs_method The HTTP method.
+ cs_uri_stem The stem portion of the URI.
+ cs_uri_query The query portion of the URI.
+ cs_version The HTTP version string.
+ sc_status The status number returned to the client.
+ sc_bytes The number of bytes sent to the client.
+ cs_referrer The URL of the referring page.
+ cs_user_agent The user agent string.
+ •  strace_log  (Currently, you need to run strace with
+ the  -tt -T options so there are timestamps for each
+ function call.)
+
+ Column Description
+ ═══════════════════════════════════════════════════════
+ funcname The name of the syscall.
+ result The result code.
+ duration The amount of time spent in the syscall.
+ arg0 - arg9 The arguments passed to the syscall.
+
+These tables are created dynamically and not stored in memory or on
+disk. If you would like to persist some information from the tables,
+you can attach another database and create tables in that database.
+For example, if you wanted to save the results from the earlier
+example of a top ten query into the "/tmp/topten.db" file, you can do:
+
+ ▌;ATTACH DATABASE '/tmp/topten.db' AS topten; 
+ ▌;CREATE TABLE topten.foo AS SELECT cs_uri_stem, count(*) AS total 
+ ▌ FROM access_log GROUP BY cs_uri_stem ORDER BY total DESC 
+ ▌ LIMIT 10; 
+
+Dynamic logline Table (experimental)
+
+(NOTE: This feature is still very new and not completely reliable yet,
+use with care.)
+
+For log formats that lack message structure, lnav can parse the log
+message and attempt to extract any data fields that it finds. This
+feature is available through the  logline  log table. This table is
+dynamically created and defined based on the message at the top of the
+log view. For example, given the following log message from "sudo",
+lnav will create the "logline" table with columns for "TTY", "PWD",
+"USER", and "COMMAND":
+
+ ▌May 24 06:48:38 Tim-Stacks-iMac.local sudo[76387]: stack : TTY=ttys003 ; PWD=/Users/stack/github/lbuild ; USER=root ; COMMAND=/bin/echo Hello, World! 
+
+Queries executed against this table will then only return results for
+other log messages that have the same format. So, if you were to
+execute the following query while viewing the above line, you might
+get the following results:
+
+ ▌;SELECT USER,COMMAND FROM logline; 
+
+ USER COMMAND
+═════════════════════════════════
+ root /bin/echo Hello, World!
+ mal /bin/echo Goodbye, World!
+
+The log parser works by examining each message for key/value pairs
+separated by an equal sign (=) or a colon (:). For example, in the
+previous example of a "sudo" message, the parser sees the "USER=root"
+string as a pair where the key is "USER" and the value is "root". If
+no pairs can be found, then anything that looks like a value is
+extracted and assigned a numbered column. For example, the following
+line is from "dhcpd":
+
+ ▌Sep 16 22:35:57 drill dhcpd: DHCPDISCOVER from 00:16:ce:54:4e:f3 via hme3 
+
+In this case, the lnav parser recognizes that "DHCPDISCOVER", the MAC
+address and the "hme3" device name are values and not normal words.
+So, it builds a table with three columns for each of these values. The
+regular words in the message, like "from" and "via", are then used to
+find other messages with a similar format.
+
+If you would like to execute queries against log messages of different
+formats at the same time, you can use the 'create-logline-table'
+command to permanently create a table using the top line of the log
+view as a template.
+
+Other SQL Features
+
+Environment variables can be used in SQL statements by prefixing the
+variable name with a dollar-sign ($). For example, to read the value
+of the  HOME  variable, you can do:
+
+ ▌;SELECT $HOME; 
+
+To select the syslog messages that have a hostname field that is equal
+to the  HOSTNAME  variable:
+
+ ▌;SELECT * FROM syslog_log WHERE log_hostname = $HOSTNAME; 
+
+NOTE: Variable substitution is done for fields in the query and is not
+a plain text substitution. For example, the following statement WILL
+NOT WORK:
+
+ ▌;SELECT * FROM $TABLE_NAME; -- Syntax error 
+
+Access to lnav's environment variables is also available via the
+"environ" table. The table has two columns (name, value) and can be
+read and written to using SQL SELECT, INSERT, UPDATE, and DELETE
+statements. For example, to set the "FOO" variable to the value "BAR":
+
+ ▌;INSERT INTO environ SELECT 'FOO', 'BAR'; 
+
+As a more complex example, you can set the variable "LAST" to the last
+syslog line number by doing:
+
+ ▌;INSERT INTO environ SELECT 'LAST', (SELECT max(log_line) FROM syslog_log); 
+
+A delete will unset the environment variable:
+
+ ▌;DELETE FROM environ WHERE name='LAST'; 
+
+The table allows you to easily use the results of a SQL query in lnav
+commands, which is especially useful when scripting lnav.
+
+Contact
+
+For more information, visit the lnav website at:
+
+http://lnav.org
+
+For support questions, email:
+
+ • lnav@googlegroups.com
+ • support@lnav.org
+
+Command Reference
+
+:adjust-log-time timestamp
+══════════════════════════════════════════════════════════════════════
+ Change the timestamps of the top file to be relative to the given
+ date
+Parameter
+ timestamp The new timestamp for the top line in the
+ view
+
+Examples
+#1 To set the top timestamp to a given date:
+ :adjust-log-time 2017-01-02T05:33:00 
+
+
+#2 To set the top timestamp back an hour:
+ :adjust-log-time -1h 
+
+
+
+:alt-msg msg
+══════════════════════════════════════════════════════════════════════
+ Display a message in the alternate command position
+Parameter
+ msg The message to display
+See Also
+ :echo, :eval, :export-session-to, :rebuild, :redirect-to,
+ :write-csv-to, :write-json-to, :write-jsonlines-to, :write-raw-to,
+ :write-screen-to, :write-table-to, :write-to, :write-view-to
+Example
+#1 To display 'Press t to switch to the text view' on the bottom right:
+ :alt-msg Press t to switch to the text view 
+
+
+
+:append-to path
+══════════════════════════════════════════════════════════════════════
+ Append marked lines in the current view to the given file
+Parameter
+ path The path to the file to append to
+See Also
+ :echo, :export-session-to, :pipe-line-to, :pipe-to, :redirect-to,
+ :write-csv-to, :write-json-to, :write-jsonlines-to, :write-raw-to,
+ :write-screen-to, :write-table-to, :write-to, :write-view-to, echoln()
+Example
+#1 To append marked lines to the file /tmp/interesting-lines.txt:
+ :append-to /tmp/interesting-lines.txt 
+
+
+
+:clear-comment
+══════════════════════════════════════════════════════════════════════
+ Clear the comment attached to the top log line
+See Also
+ :comment, :tag
+
+:clear-filter-expr
+══════════════════════════════════════════════════════════════════════
+ Clear the filter expression
+See Also
+ :filter-expr, :filter-in, :filter-out, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+
+:clear-highlight pattern
+══════════════════════════════════════════════════════════════════════
+ Remove a previously set highlight regular expression
+Parameter
+ pattern The regular expression previously used with
+ :highlight
+See Also
+ :enable-word-wrap, :hide-fields, :highlight
+Example
+#1 To clear the highlight with the pattern 'foobar':
+ :clear-highlight foobar 
+
+
+
+:clear-mark-expr
+══════════════════════════════════════════════════════════════════════
+ Clear the mark expression
+See Also
+ :hide-unmarked-lines, :mark, :mark-expr, :next-mark, :prev-mark
+
+:clear-partition
+══════════════════════════════════════════════════════════════════════
+ Clear the partition the top line is a part of
+
+
+:close
+══════════════════════════════════════════════════════════════════════
+ Close the top file in the view
+
+
+:comment text
+══════════════════════════════════════════════════════════════════════
+ Attach a comment to the top log line. The comment will be displayed
+ right below the log message it is associated with. The comment can
+ be formatted using markdown and you can add new-lines with '\n'.
+Parameter
+ text The comment text
+See Also
+ :clear-comment, :tag
+Example
+#1 To add the comment 'This is where it all went wrong' to the top line:
+ :comment This is where it all went wrong 
+
+
+
+:config option [value]
+══════════════════════════════════════════════════════════════════════
+ Read or write a configuration option
+Parameters
+ option The path to the option to read or write
+ value The value to write. If not given, the current
+ value is returned
+See Also
+ :reset-config
+Examples
+#1 To read the configuration of the '/ui/clock-format' option:
+ :config /ui/clock-format 
+
+
+#2 To set the '/ui/dim-text' option to 'false':
+ :config /ui/dim-text false 
+
+
+
+:create-logline-table table-name
+══════════════════════════════════════════════════════════════════════
+ Create an SQL table using the top line of the log view as a template
+Parameter
+ table-name The name for the new table
+See Also
+ :create-search-table, :create-search-table, :write-csv-to,
+ :write-json-to, :write-jsonlines-to, :write-raw-to, :write-screen-to,
+ :write-table-to, :write-view-to
+Example
+#1 To create a logline-style table named 'task_durations':
+ :create-logline-table task_durations 
+
+
+
+:create-search-table table-name [pattern]
+══════════════════════════════════════════════════════════════════════
+ Create an SQL table based on a regex search
+Parameters
+ table-name The name of the table to create
+ pattern The regular expression used to capture the
+ table columns. If not given, the current search
+ pattern is used.
+See Also
+ :create-logline-table, :create-logline-table, :delete-search-table,
+ :delete-search-table, :write-csv-to, :write-json-to,
+ :write-jsonlines-to, :write-raw-to, :write-screen-to, :write-table-to,
+ :write-view-to
+Example
+#1 To create a table named 'task_durations' that matches log messages with the pattern
+ 'duration=(?<duration>\d+)':
+ :create-search-table task_durations duration=(?<duration>\d+)
+
+
+
+:current-time
+══════════════════════════════════════════════════════════════════════
+ Print the current time in human-readable form and seconds since the
+ epoch
+
+
+:delete-filter pattern
+══════════════════════════════════════════════════════════════════════
+ Delete the filter created with :filter-in or :filter-out
+Parameter
+ pattern The regular expression to match
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To delete the filter with the pattern 'last message repeated':
+ :delete-filter last message repeated 
+
+
+
+:delete-logline-table table-name
+══════════════════════════════════════════════════════════════════════
+ Delete a table created with create-logline-table
+Parameter
+ table-name The name of the table to delete
+See Also
+ :create-logline-table, :create-logline-table, :create-search-table,
+ :create-search-table, :write-csv-to, :write-json-to,
+ :write-jsonlines-to, :write-raw-to, :write-screen-to, :write-table-to,
+ :write-view-to
+Example
+#1 To delete the logline-style table named 'task_durations':
+ :delete-logline-table task_durations 
+
+
+
+:delete-search-table table-name
+══════════════════════════════════════════════════════════════════════
+ Create an SQL table based on a regex search
+Parameter
+ table-name The name of the table to create
+See Also
+ :create-logline-table, :create-logline-table, :create-search-table,
+ :create-search-table, :write-csv-to, :write-json-to,
+ :write-jsonlines-to, :write-raw-to, :write-screen-to, :write-table-to,
+ :write-view-to
+Example
+#1 To delete the search table named 'task_durations':
+ :delete-search-table task_durations 
+
+
+
+:delete-tags tag1 [... tagN]
+══════════════════════════════════════════════════════════════════════
+ Remove the given tags from all log lines
+Parameter
+ tag The tags to delete
+See Also
+ :comment, :tag
+Example
+#1 To remove the tags '#BUG123' and '#needs-review' from all log lines:
+ :delete-tags #BUG123 #needs-review 
+
+
+
+:disable-filter pattern
+══════════════════════════════════════════════════════════════════════
+ Disable a filter created with filter-in/filter-out
+Parameter
+ pattern The regular expression used in the filter
+ command
+See Also
+ :enable-filter, :filter-in, :filter-out, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To disable the filter with the pattern 'last message repeated':
+ :disable-filter last message repeated 
+
+
+
+:disable-word-wrap
+══════════════════════════════════════════════════════════════════════
+ Disable word-wrapping for the current view
+See Also
+ :enable-word-wrap, :hide-fields, :highlight
+
+:echo [-n] msg
+══════════════════════════════════════════════════════════════════════
+ Echo the given message to the screen or, if :redirect-to has been
+ called, to output file specified in the redirect. Variable
+ substitution is performed on the message. Use a backslash to escape
+ any special characters, like '$'
+Parameters
+ -n Do not print a line-feed at the end of the output
+ msg The message to display
+See Also
+ :alt-msg, :append-to, :eval, :export-session-to, :export-session-to,
+ :pipe-line-to, :pipe-to, :rebuild, :redirect-to, :redirect-to,
+ :write-csv-to, :write-csv-to, :write-json-to, :write-json-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-screen-to, :write-screen-to, :write-table-to, :write-table-to,
+ :write-to, :write-to, :write-view-to, :write-view-to, echoln()
+Example
+#1 To output 'Hello, World!':
+ :echo Hello, World! 
+
+
+
+:enable-filter pattern
+══════════════════════════════════════════════════════════════════════
+ Enable a previously created and disabled filter
+Parameter
+ pattern The regular expression used in the filter
+ command
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To enable the disabled filter with the pattern 'last message repeated':
+ :enable-filter last message repeated 
+
+
+
+:enable-word-wrap
+══════════════════════════════════════════════════════════════════════
+ Enable word-wrapping for the current view
+See Also
+ :disable-word-wrap, :hide-fields, :highlight
+
+:eval command
+══════════════════════════════════════════════════════════════════════
+ Evaluate the given command/query after doing environment variable
+ substitution
+Parameter
+ command The command or query to perform substitution on.
+See Also
+ :alt-msg, :echo, :export-session-to, :rebuild, :redirect-to,
+ :write-csv-to, :write-json-to, :write-jsonlines-to, :write-raw-to,
+ :write-screen-to, :write-table-to, :write-to, :write-view-to
+Example
+#1 To substitute the table name from a variable:
+ :eval ;SELECT * FROM ${table} 
+
+
+
+:export-session-to path
+══════════════════════════════════════════════════════════════════════
+ Export the current lnav state to an executable lnav script file that
+ contains the commands needed to restore the current session
+Parameter
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :echo, :echo, :eval, :pipe-line-to, :pipe-to,
+ :rebuild, :redirect-to, :redirect-to, :write-csv-to, :write-csv-to,
+ :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-raw-to, :write-raw-to, :write-screen-to,
+ :write-screen-to, :write-table-to, :write-table-to, :write-to,
+ :write-to, :write-view-to, :write-view-to, echoln()
+
+:filter-expr expr
+══════════════════════════════════════════════════════════════════════
+ Set the filter expression
+Parameter
+ expr The SQL expression to evaluate for each log message.
+ The message values can be accessed using column names
+ prefixed with a colon
+See Also
+ :clear-filter-expr, :filter-in, :filter-out, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+Examples
+#1 To set a filter expression that matched syslog messages from 'syslogd':
+ :filter-expr :log_procname = 'syslogd' 
+
+
+#2 To set a filter expression that matches log messages where 'id' is followed by a
+ number and contains the string 'foo':
+ :filter-expr :log_body REGEXP 'id\d+' AND :log_body REGEXP 'foo'
+
+
+
+:filter-in pattern
+══════════════════════════════════════════════════════════════════════
+ Only show lines that match the given regular expression in the
+ current view
+Parameter
+ pattern The regular expression to match
+See Also
+ :delete-filter, :disable-filter, :filter-out, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To filter out log messages that do not have the string 'dhclient':
+ :filter-in dhclient 
+
+
+
+:filter-out pattern
+══════════════════════════════════════════════════════════════════════
+ Remove lines that match the given regular expression in the current
+ view
+Parameter
+ pattern The regular expression to match
+See Also
+ :delete-filter, :disable-filter, :filter-in, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To filter out log messages that contain the string 'last message repeated':
+ :filter-out last message repeated 
+
+
+
+:goto line#|N%|timestamp|#anchor
+══════════════════════════════════════════════════════════════════════
+ Go to the given location in the top view
+Parameter
+ line#|N%|timestamp|#anchor A line
+ number, percent into the file,
+ timestamp, or an anchor in a text file
+See Also
+ :next-location, :next-mark, :prev-location, :prev-mark, :relative-goto
+Examples
+#1 To go to line 22:
+ :goto 22 
+
+
+#2 To go to the line 75% of the way into the view:
+ :goto 75% 
+
+
+#3 To go to the first message on the first day of 2017:
+ :goto 2017-01-01 
+
+
+#4 To go to the Screenshots section:
+ :goto #screenshots 
+
+
+
+:help
+══════════════════════════════════════════════════════════════════════
+ Open the help text view
+
+
+:hide-fields field-name1 [... field-nameN]
+══════════════════════════════════════════════════════════════════════
+ Hide log message fields by replacing them with an ellipsis
+Parameter
+ field-name The name of the field to hide in the
+ format for the top log line. A qualified name can be
+ used where the field name is prefixed by the format
+ name and a dot to hide any field.
+See Also
+ :enable-word-wrap, :highlight, :show-fields
+Examples
+#1 To hide the log_procname fields in all formats:
+ :hide-fields log_procname 
+
+
+#2 To hide only the log_procname field in the syslog format:
+ :hide-fields syslog_log.log_procname 
+
+
+
+:hide-file path
+══════════════════════════════════════════════════════════════════════
+ Hide the given file(s) and skip indexing until it is shown again.
+ If no path is given, the current file in the view is hidden
+Parameter
+ path A path or glob pattern that specifies the files to
+ hide
+
+
+:hide-lines-after date
+══════════════════════════════════════════════════════════════════════
+ Hide lines that come after the given date
+Parameter
+ date An absolute or relative date
+See Also
+ :filter-in, :filter-out, :hide-lines-before, :hide-unmarked-lines,
+ :show-lines-before-and-after, :toggle-filtering
+Examples
+#1 To hide the lines after the top line in the view:
+ :hide-lines-after here 
+
+
+#2 To hide the lines after 6 AM today:
+ :hide-lines-after 6am 
+
+
+
+:hide-lines-before date
+══════════════════════════════════════════════════════════════════════
+ Hide lines that come before the given date
+Parameter
+ date An absolute or relative date
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-unmarked-lines,
+ :show-lines-before-and-after, :toggle-filtering
+Examples
+#1 To hide the lines before the top line in the view:
+ :hide-lines-before here 
+
+
+#2 To hide the log messages before 6 AM today:
+ :hide-lines-before 6am 
+
+
+
+:hide-unmarked-lines
+══════════════════════════════════════════════════════════════════════
+ Hide lines that have not been bookmarked
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before, :mark,
+ :next-mark, :prev-mark, :toggle-filtering
+
+:highlight pattern
+══════════════════════════════════════════════════════════════════════
+ Add coloring to log messages fragments that match the given regular
+ expression
+Parameter
+ pattern The regular expression to match
+See Also
+ :clear-highlight, :enable-word-wrap, :hide-fields
+Example
+#1 To highlight numbers with three or more digits:
+ :highlight \d{3,} 
+
+
+
+:load-session
+══════════════════════════════════════════════════════════════════════
+ Load the latest session state
+
+
+:mark
+══════════════════════════════════════════════════════════════════════
+ Toggle the bookmark state for the top line in the current view
+See Also
+ :hide-unmarked-lines, :next-mark, :prev-mark
+
+:mark-expr expr
+══════════════════════════════════════════════════════════════════════
+ Set the bookmark expression
+Parameter
+ expr The SQL expression to evaluate for each log message.
+ The message values can be accessed using column names
+ prefixed with a colon
+See Also
+ :clear-mark-expr, :hide-unmarked-lines, :mark, :next-mark, :prev-mark
+Example
+#1 To mark lines from 'dhclient' that mention 'eth0':
+ :mark-expr :log_procname = 'dhclient' AND :log_body LIKE '%eth0%'
+
+
+
+:next-location
+══════════════════════════════════════════════════════════════════════
+ Move to the next position in the location history
+See Also
+ :goto, :next-mark, :prev-location, :prev-mark, :relative-goto
+
+:next-mark type1 [... typeN]
+══════════════════════════════════════════════════════════════════════
+ Move to the next bookmark of the given type in the current view
+Parameter
+ type The type of bookmark -- error, warning, search, user,
+ file, meta
+See Also
+ :goto, :hide-unmarked-lines, :mark, :next-location, :prev-location,
+ :prev-mark, :prev-mark, :relative-goto
+Example
+#1 To go to the next error:
+ :next-mark error 
+
+
+
+:open path1 [... pathN]
+══════════════════════════════════════════════════════════════════════
+ Open the given file(s) in lnav. Opening files on machines
+ accessible via SSH can be done using the syntax:
+ [user@]host:/path/to/logs
+Parameter
+ path The path to the file to open
+
+Examples
+#1 To open the file '/path/to/file':
+ :open /path/to/file 
+
+
+#2 To open the remote file '/var/log/syslog.log':
+ :open dean@host1.example.com:/var/log/syslog.log 
+
+
+
+:partition-name name
+══════════════════════════════════════════════════════════════════════
+ Mark the top line in the log view as the start of a new partition
+ with the given name
+Parameter
+ name The name for the new partition
+
+Example
+#1 To mark the top line as the start of the partition named 'boot #1':
+ :partition-name boot #1 
+
+
+
+:pipe-line-to shell-cmd
+══════════════════════════════════════════════════════════════════════
+ Pipe the top line to the given shell command
+Parameter
+ shell-cmd The shell command-line to execute
+See Also
+ :append-to, :echo, :export-session-to, :pipe-to, :redirect-to,
+ :write-csv-to, :write-json-to, :write-jsonlines-to, :write-raw-to,
+ :write-screen-to, :write-table-to, :write-to, :write-view-to, echoln()
+Example
+#1 To write the top line to 'sed' for processing:
+ :pipe-line-to sed -e 's/foo/bar/g' 
+
+
+
+:pipe-to shell-cmd
+══════════════════════════════════════════════════════════════════════
+ Pipe the marked lines to the given shell command
+Parameter
+ shell-cmd The shell command-line to execute
+See Also
+ :append-to, :echo, :export-session-to, :pipe-line-to, :redirect-to,
+ :write-csv-to, :write-json-to, :write-jsonlines-to, :write-raw-to,
+ :write-screen-to, :write-table-to, :write-to, :write-view-to, echoln()
+Example
+#1 To write marked lines to 'sed' for processing:
+ :pipe-to sed -e s/foo/bar/g 
+
+
+
+:prev-location
+══════════════════════════════════════════════════════════════════════
+ Move to the previous position in the location history
+See Also
+ :goto, :next-location, :next-mark, :prev-mark, :relative-goto
+
+:prev-mark type1 [... typeN]
+══════════════════════════════════════════════════════════════════════
+ Move to the previous bookmark of the given type in the current view
+Parameter
+ type The type of bookmark -- error, warning, search, user,
+ file, meta
+See Also
+ :goto, :hide-unmarked-lines, :mark, :next-location, :next-mark,
+ :next-mark, :prev-location, :relative-goto
+Example
+#1 To go to the previous error:
+ :prev-mark error 
+
+
+
+:prompt type [--alt] [prompt] [initial-value]
+══════════════════════════════════════════════════════════════════════
+ Open the given prompt
+Parameters
+ type The type of prompt -- command,
+ script, search, sql, user
+ --alt Perform the alternate action for
+ this prompt by default
+ prompt The prompt to display
+ initial-value The initial value to fill in for the
+ prompt
+
+Examples
+#1 To open the command prompt with 'filter-in' already filled in:
+ :prompt command : 'filter-in ' 
+
+
+#2 To ask the user a question:
+ :prompt user 'Are you sure? ' 
+
+
+
+:quit
+══════════════════════════════════════════════════════════════════════
+ Quit lnav
+
+
+:quit
+══════════════════════════════════════════════════════════════════════
+ Quit lnav
+
+
+:quit
+══════════════════════════════════════════════════════════════════════
+ Quit lnav
+
+
+:rebuild
+══════════════════════════════════════════════════════════════════════
+ Forcefully rebuild file indexes
+See Also
+ :alt-msg, :echo, :eval, :export-session-to, :redirect-to,
+ :write-csv-to, :write-json-to, :write-jsonlines-to, :write-raw-to,
+ :write-screen-to, :write-table-to, :write-to, :write-view-to
+
+:redirect-to [path]
+══════════════════════════════════════════════════════════════════════
+ Redirect the output of commands that write to stdout to the given
+ file
+Parameter
+ path The path to the file to write. If not specified, the
+ current redirect will be cleared
+See Also
+ :alt-msg, :append-to, :echo, :echo, :eval, :export-session-to,
+ :export-session-to, :pipe-line-to, :pipe-to, :rebuild, :write-csv-to,
+ :write-csv-to, :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-raw-to, :write-raw-to, :write-screen-to,
+ :write-screen-to, :write-table-to, :write-table-to, :write-to,
+ :write-to, :write-view-to, :write-view-to, echoln()
+Example
+#1 To write the output of lnav commands to the file /tmp/script-output.txt:
+ :redirect-to /tmp/script-output.txt 
+
+
+
+:redraw
+══════════════════════════════════════════════════════════════════════
+ Do a full redraw of the screen
+
+
+:relative-goto line-count|N%
+══════════════════════════════════════════════════════════════════════
+ Move the current view up or down by the given amount
+Parameter
+ line-count|N% The amount to move the view by.
+See Also
+ :goto, :next-location, :next-mark, :prev-location, :prev-mark
+Examples
+#1 To move 22 lines down in the view:
+ :relative-goto +22 
+
+
+#2 To move 10 percent back in the view:
+ :relative-goto -10% 
+
+
+
+:reset-config option
+══════════════════════════════════════════════════════════════════════
+ Reset the configuration option to its default value
+Parameter
+ option The path to the option to reset
+See Also
+ :config
+Example
+#1 To reset the '/ui/clock-format' option back to the builtin default:
+ :reset-config /ui/clock-format 
+
+
+
+:reset-session
+══════════════════════════════════════════════════════════════════════
+ Reset the session state, clearing all filters, highlights, and
+ bookmarks
+
+
+:save-session
+══════════════════════════════════════════════════════════════════════
+ Save the current state as a session
+
+
+:session lnav-command
+══════════════════════════════════════════════════════════════════════
+ Add the given command to the session file (~/.lnav/session)
+Parameter
+ lnav-command The lnav command to save.
+
+Example
+#1 To add the command ':highlight foobar' to the session file:
+ :session :highlight foobar 
+
+
+
+:set-min-log-level log-level
+══════════════════════════════════════════════════════════════════════
+ Set the minimum log level to display in the log view
+Parameter
+ log-level The new minimum log level
+
+Example
+#1 To set the minimum log level displayed to error:
+ :set-min-log-level error 
+
+
+
+:show-fields field-name1 [... field-nameN]
+══════════════════════════════════════════════════════════════════════
+ Show log message fields that were previously hidden
+Parameter
+ field-name The name of the field to show
+See Also
+ :enable-word-wrap, :hide-fields, :highlight
+Example
+#1 To show all the log_procname fields in all formats:
+ :show-fields log_procname 
+
+
+
+:show-file path
+══════════════════════════════════════════════════════════════════════
+ Show the given file(s) and resume indexing.
+Parameter
+ path The path or glob pattern that specifies the files to
+ show
+
+
+:show-lines-before-and-after
+══════════════════════════════════════════════════════════════════════
+ Show lines that were hidden by the 'hide-lines' commands
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines, :toggle-filtering
+
+:show-only-this-file
+══════════════════════════════════════════════════════════════════════
+ Show only the file for the top line in the view
+
+
+:show-unmarked-lines
+══════════════════════════════════════════════════════════════════════
+ Show lines that have not been bookmarked
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines, :hide-unmarked-lines, :mark, :next-mark,
+ :prev-mark, :toggle-filtering
+
+:spectrogram field-name
+══════════════════════════════════════════════════════════════════════
+ Visualize the given message field or database column using a
+ spectrogram
+Parameter
+ field-name The name of the numeric field to
+ visualize.
+
+Example
+#1 To visualize the sc_bytes field in the access_log format:
+ :spectrogram sc_bytes 
+
+
+
+:summarize column-name
+══════════════════════════════════════════════════════════════════════
+ Execute a SQL query that computes the characteristics of the values
+ in the given column
+Parameter
+ column-name The name of the column to analyze.
+
+Example
+#1 To get a summary of the sc_bytes column in the access_log table:
+ :summarize sc_bytes 
+
+
+
+:switch-to-view view-name
+══════════════════════════════════════════════════════════════════════
+ Switch to the given view
+Parameter
+ view-name The name of the view to switch to.
+
+Example
+#1 To switch to the 'schema' view:
+ :switch-to-view schema 
+
+
+
+:tag tag1 [... tagN]
+══════════════════════════════════════════════════════════════════════
+ Attach tags to the top log line
+Parameter
+ tag The tags to attach
+See Also
+ :comment, :delete-tags, :untag
+Example
+#1 To add the tags '#BUG123' and '#needs-review' to the top line:
+ :tag #BUG123 #needs-review 
+
+
+
+:toggle-filtering
+══════════════════════════════════════════════════════════════════════
+ Toggle the filtering flag for the current view
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines
+
+:toggle-view view-name
+══════════════════════════════════════════════════════════════════════
+ Switch to the given view or, if it is already displayed, switch to
+ the previous view
+Parameter
+ view-name The name of the view to toggle the display
+ of.
+
+Example
+#1 To switch to the 'schema' view if it is not displayed or switch back to the previous
+ view:
+ :toggle-view schema 
+
+
+
+:unix-time seconds
+══════════════════════════════════════════════════════════════════════
+ Convert epoch time to a human-readable form
+Parameter
+ seconds The epoch timestamp to convert
+
+Example
+#1 To convert the epoch time 1490191111:
+ :unix-time 1490191111 
+
+
+
+:untag tag1 [... tagN]
+══════════════════════════════════════════════════════════════════════
+ Detach tags from the top log line
+Parameter
+ tag The tags to detach
+See Also
+ :comment, :tag
+Example
+#1 To remove the tags '#BUG123' and '#needs-review' from the top line:
+ :untag #BUG123 #needs-review 
+
+
+
+:write-table-to [--anonymize] path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in a tabular format
+Parameters
+ --anonymize Anonymize the table contents
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :export-session-to, :export-session-to,
+ :pipe-line-to, :pipe-to, :rebuild, :redirect-to, :redirect-to,
+ :write-csv-to, :write-csv-to, :write-csv-to, :write-json-to,
+ :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-screen-to, :write-screen-to, :write-screen-to,
+ :write-to, :write-to, :write-view-to, :write-view-to, :write-view-to,
+ echoln()
+Example
+#1 To write SQL results as text to /tmp/table.txt:
+ :write-table-to /tmp/table.txt 
+
+
+
+:write-csv-to [--anonymize] path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in CSV format
+Parameters
+ --anonymize Anonymize the row contents
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :export-session-to, :export-session-to,
+ :pipe-line-to, :pipe-to, :rebuild, :redirect-to, :redirect-to,
+ :write-json-to, :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-screen-to, :write-screen-to, :write-screen-to,
+ :write-table-to, :write-table-to, :write-table-to, :write-to,
+ :write-to, :write-view-to, :write-view-to, :write-view-to, echoln()
+Example
+#1 To write SQL results as CSV to /tmp/table.csv:
+ :write-csv-to /tmp/table.csv 
+
+
+
+:write-json-to [--anonymize] path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in JSON format
+Parameters
+ --anonymize Anonymize the JSON values
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :export-session-to, :export-session-to,
+ :pipe-line-to, :pipe-to, :rebuild, :redirect-to, :redirect-to,
+ :write-csv-to, :write-csv-to, :write-csv-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-screen-to, :write-screen-to, :write-screen-to,
+ :write-table-to, :write-table-to, :write-table-to, :write-to,
+ :write-to, :write-view-to, :write-view-to, :write-view-to, echoln()
+Example
+#1 To write SQL results as JSON to /tmp/table.json:
+ :write-json-to /tmp/table.json 
+
+
+
+:write-jsonlines-to [--anonymize] path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in JSON Lines format
+Parameters
+ --anonymize Anonymize the JSON values
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :export-session-to, :export-session-to,
+ :pipe-line-to, :pipe-to, :rebuild, :redirect-to, :redirect-to,
+ :write-csv-to, :write-csv-to, :write-csv-to, :write-json-to,
+ :write-json-to, :write-json-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-screen-to, :write-screen-to, :write-screen-to,
+ :write-table-to, :write-table-to, :write-table-to, :write-to,
+ :write-to, :write-view-to, :write-view-to, :write-view-to, echoln()
+Example
+#1 To write SQL results as JSON Lines to /tmp/table.json:
+ :write-jsonlines-to /tmp/table.json 
+
+
+
+:write-raw-to [--view={log,db}] [--anonymize] path
+══════════════════════════════════════════════════════════════════════
+ In the log view, write the original log file content of the marked
+ messages to the file. In the DB view, the contents of the cells are
+ written to the output file.
+Parameters
+ --view={log,db} The view to use as the source of
+ data
+ --anonymize Anonymize the lines
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :export-session-to, :export-session-to,
+ :pipe-line-to, :pipe-to, :rebuild, :redirect-to, :redirect-to,
+ :write-csv-to, :write-csv-to, :write-csv-to, :write-json-to,
+ :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-screen-to,
+ :write-screen-to, :write-screen-to, :write-table-to, :write-table-to,
+ :write-table-to, :write-to, :write-to, :write-view-to, :write-view-to,
+ :write-view-to, echoln()
+Example
+#1 To write the marked lines in the log view to /tmp/table.txt:
+ :write-raw-to /tmp/table.txt 
+
+
+
+:write-screen-to [--anonymize] path
+══════════════════════════════════════════════════════════════════════
+ Write the displayed text or SQL results to the given file without
+ any formatting
+Parameters
+ --anonymize Anonymize the lines
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :export-session-to, :export-session-to,
+ :pipe-line-to, :pipe-to, :rebuild, :redirect-to, :redirect-to,
+ :write-csv-to, :write-csv-to, :write-csv-to, :write-json-to,
+ :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-table-to, :write-table-to, :write-table-to,
+ :write-to, :write-to, :write-view-to, :write-view-to, :write-view-to,
+ echoln()
+Example
+#1 To write only the displayed text to /tmp/table.txt:
+ :write-screen-to /tmp/table.txt 
+
+
+
+:write-table-to [--anonymize] path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in a tabular format
+Parameters
+ --anonymize Anonymize the table contents
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :export-session-to, :export-session-to,
+ :pipe-line-to, :pipe-to, :rebuild, :redirect-to, :redirect-to,
+ :write-csv-to, :write-csv-to, :write-csv-to, :write-json-to,
+ :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-screen-to, :write-screen-to, :write-screen-to,
+ :write-to, :write-to, :write-view-to, :write-view-to, :write-view-to,
+ echoln()
+Example
+#1 To write SQL results as text to /tmp/table.txt:
+ :write-table-to /tmp/table.txt 
+
+
+
+:write-to [--anonymize] path
+══════════════════════════════════════════════════════════════════════
+ Overwrite the given file with any marked lines in the current view
+Parameters
+ --anonymize Anonymize the lines
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :echo, :echo, :eval, :export-session-to,
+ :export-session-to, :pipe-line-to, :pipe-to, :rebuild, :redirect-to,
+ :redirect-to, :write-csv-to, :write-csv-to, :write-json-to,
+ :write-json-to, :write-jsonlines-to, :write-jsonlines-to,
+ :write-raw-to, :write-raw-to, :write-screen-to, :write-screen-to,
+ :write-table-to, :write-table-to, :write-view-to, :write-view-to,
+ echoln()
+Example
+#1 To write marked lines to the file /tmp/interesting-lines.txt:
+ :write-to /tmp/interesting-lines.txt 
+
+
+
+:write-view-to [--anonymize] path
+══════════════════════════════════════════════════════════════════════
+ Write the text in the top view to the given file without any
+ formatting
+Parameters
+ --anonymize Anonymize the lines
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :export-session-to, :export-session-to,
+ :pipe-line-to, :pipe-to, :rebuild, :redirect-to, :redirect-to,
+ :write-csv-to, :write-csv-to, :write-csv-to, :write-json-to,
+ :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-screen-to, :write-screen-to, :write-screen-to,
+ :write-table-to, :write-table-to, :write-table-to, :write-to,
+ :write-to, echoln()
+Example
+#1 To write the top view to /tmp/table.txt:
+ :write-view-to /tmp/table.txt 
+
+
+
+:zoom-to zoom-level
+══════════════════════════════════════════════════════════════════════
+ Zoom the histogram view to the given level
+Parameter
+ zoom-level The zoom level
+
+Example
+#1 To set the zoom level to '1-week':
+ :zoom-to 1-week 
+
+
+SQL Reference
+
+CAST(expr AS type-name)
+══════════════════════════════════════════════════════════════════════
+ Convert the value of the given expression to a different storage
+ class specified by type-name.
+Parameters
+ expr The value to convert.
+ type-name The name of the type to convert to.
+
+Example
+#1 To cast the value 1.23 as an integer:
+ ;SELECT CAST(1.23 AS INTEGER) 
+
+
+
+OVER([base-window-name] PARTITION BY expr, ... ORDER BY expr, ...,
+ [frame-spec])
+══════════════════════════════════════════════════════════════════════
+ Executes the preceding function over a window
+Parameters
+ base-window-name The name of the window
+ definition
+ expr The values to use for
+ partitioning
+ expr The values used to order the
+ rows in the window
+ frame-spec Determines which output rows
+ are read by an aggregate window function
+
+
+abs(x)
+══════════════════════════════════════════════════════════════════════
+ Return the absolute value of the argument
+Parameter
+ x The number to convert
+See Also
+ acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the absolute value of -1:
+ ;SELECT abs(-1) 
+
+
+
+acos(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the arccosine of a number, in radians
+Parameter
+ num A cosine value that is between -1 and 1
+See Also
+ abs(), acosh(), asin(), asinh(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the arccosine of 0.2:
+ ;SELECT acos(0.2) 
+
+
+
+acosh(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the hyperbolic arccosine of a number
+Parameter
+ num A number that is one or more
+See Also
+ abs(), acos(), asin(), asinh(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the hyperbolic arccosine of 1.2:
+ ;SELECT acosh(1.2) 
+
+
+
+anonymize(value)
+══════════════════════════════════════════════════════════════════════
+ Replace identifying information with random values.
+Parameter
+ value The text to anonymize
+See Also
+ char(), charindex(), decode(), encode(), endswith(), extract(),
+ group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Example
+#1 To anonymize an IP address:
+ ;SELECT anonymize('Hello, 192.168.1.2') 
+
+
+
+asin(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the arcsine of a number, in radians
+Parameter
+ num A sine value that is between -1 and 1
+See Also
+ abs(), acos(), acosh(), asinh(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the arcsine of 0.2:
+ ;SELECT asin(0.2) 
+
+
+
+asinh(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the hyperbolic arcsine of a number
+Parameter
+ num The number
+See Also
+ abs(), acos(), acosh(), asin(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the hyperbolic arcsine of 0.2:
+ ;SELECT asinh(0.2) 
+
+
+
+atan(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the arctangent of a number, in radians
+Parameter
+ num The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the arctangent of 0.2:
+ ;SELECT atan(0.2) 
+
+
+
+atan2(y, x)
+══════════════════════════════════════════════════════════════════════
+ Returns the angle in the plane between the positive X axis and the
+ ray from (0, 0) to the point (x, y)
+Parameters
+ y The y coordinate of the point
+ x The x coordinate of the point
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the angle, in degrees, for the point at (5, 5):
+ ;SELECT degrees(atan2(5, 5)) 
+
+
+
+atanh(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the hyperbolic arctangent of a number
+Parameter
+ num The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the hyperbolic arctangent of 0.2:
+ ;SELECT atanh(0.2) 
+
+
+
+atn2(y, x)
+══════════════════════════════════════════════════════════════════════
+ Returns the angle in the plane between the positive X axis and the
+ ray from (0, 0) to the point (x, y)
+Parameters
+ y The y coordinate of the point
+ x The x coordinate of the point
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the angle, in degrees, for the point at (5, 5):
+ ;SELECT degrees(atn2(5, 5)) 
+
+
+
+avg(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the average value of all non-NULL numbers within a group.
+Parameter
+ X The value to compute the average of.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), ceil(), degrees(), exp(), floor(), log(), log10(), max(),
+ min(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Examples
+#1 To get the average of the column 'ex_duration' from the table 'lnav_example_log':
+ ;SELECT avg(ex_duration) FROM lnav_example_log 
+
+
+#2 To get the average of the column 'ex_duration' from the table 'lnav_example_log'
+ when grouped by 'ex_procname':
+ ;SELECT ex_procname, avg(ex_duration) FROM lnav_example_log GROUP BY ex_procname
+
+
+
+basename(path)
+══════════════════════════════════════════════════════════════════════
+ Extract the base portion of a pathname.
+Parameter
+ path The path
+See Also
+ dirname(), joinpath(), readlink(), realpath()
+Examples
+#1 To get the base of a plain file name:
+ ;SELECT basename('foobar') 
+
+
+#2 To get the base of a path:
+ ;SELECT basename('foo/bar') 
+
+
+#3 To get the base of a directory:
+ ;SELECT basename('foo/bar/') 
+
+
+#4 To get the base of an empty string:
+ ;SELECT basename('') 
+
+
+#5 To get the base of a Windows path:
+ ;SELECT basename('foo\bar') 
+
+
+#6 To get the base of the root directory:
+ ;SELECT basename('/') 
+
+
+
+ceil(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the smallest integer that is not less than the argument
+Parameter
+ num The number to raise to the ceiling
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the ceiling of 1.23:
+ ;SELECT ceil(1.23) 
+
+
+
+changes()
+══════════════════════════════════════════════════════════════════════
+ The number of database rows that were changed, inserted, or deleted
+ by the most recent statement.
+
+
+char(X, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns a string composed of characters having the given unicode
+ code point values
+Parameter
+ X The unicode code point values
+See Also
+ anonymize(), charindex(), decode(), encode(), endswith(), extract(),
+ group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Example
+#1 To get a string with the code points 0x48 and 0x49:
+ ;SELECT char(0x48, 0x49) 
+
+
+
+charindex(needle, haystack, [start])
+══════════════════════════════════════════════════════════════════════
+ Finds the first occurrence of the needle within the haystack and
+ returns the number of prior characters plus 1, or 0 if Y is nowhere
+ found within X
+Parameters
+ needle The string to look for in the haystack
+ haystack The string to search within
+ start The one-based index within the haystack to
+ start the search
+See Also
+ anonymize(), char(), decode(), encode(), endswith(), extract(),
+ group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To search for the string 'abc' within 'abcabc' and starting at position 2:
+ ;SELECT charindex('abc', 'abcabc', 2) 
+
+
+#2 To search for the string 'abc' within 'abcdef' and starting at position 2:
+ ;SELECT charindex('abc', 'abcdef', 2) 
+
+
+
+coalesce(X, Y, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns a copy of its first non-NULL argument, or NULL if all
+ arguments are NULL
+Parameters
+ X A value to check for NULL-ness
+ Y A value to check for NULL-ness
+
+Example
+#1 To get the first non-null value from three parameters:
+ ;SELECT coalesce(null, 0, null) 
+
+
+
+count(X)
+══════════════════════════════════════════════════════════════════════
+ If the argument is '*', the total number of rows in the group is
+ returned. Otherwise, the number of times the argument is non-NULL.
+Parameter
+ X The value to count.
+
+Examples
+#1 To get the count of the non-NULL rows of 'lnav_example_log':
+ ;SELECT count(*) FROM lnav_example_log 
+
+
+#2 To get the count of the non-NULL values of 'log_part' from 'lnav_example_log':
+ ;SELECT count(log_part) FROM lnav_example_log 
+
+
+
+cume_dist()
+══════════════════════════════════════════════════════════════════════
+ Returns the cumulative distribution
+See Also
+ dense_rank(), first_value(), lag(), last_value(), lead(), nth_value(),
+ ntile(), percent_rank(), rank(), row_number()
+
+date(timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the date in this format: YYYY-MM-DD.
+Parameters
+ timestring The string to convert to a date.
+ modifier A transformation that is applied to the
+ value to the left.
+See Also
+ datetime(), humanize_duration(), julianday(), strftime(), time(),
+ timediff(), timeslice()
+Examples
+#1 To get the date portion of the timestamp '2017-01-02T03:04:05':
+ ;SELECT date('2017-01-02T03:04:05') 
+
+
+#2 To get the date portion of the timestamp '2017-01-02T03:04:05' plus one day:
+ ;SELECT date('2017-01-02T03:04:05', '+1 day') 
+
+
+#3 To get the date portion of the epoch timestamp 1491341842:
+ ;SELECT date(1491341842, 'unixepoch') 
+
+
+
+datetime(timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the date and time in this format: YYYY-MM-DD HH:MM:SS.
+Parameters
+ timestring The string to convert to a date with time.
+ modifier A transformation that is applied to the
+ value to the left.
+See Also
+ date(), humanize_duration(), julianday(), strftime(), time(),
+ timediff(), timeslice()
+Examples
+#1 To get the date and time portion of the timestamp '2017-01-02T03:04:05':
+ ;SELECT datetime('2017-01-02T03:04:05') 
+
+
+#2 To get the date and time portion of the timestamp '2017-01-02T03:04:05' plus one
+ minute:
+ ;SELECT datetime('2017-01-02T03:04:05', '+1 minute')
+
+
+#3 To get the date and time portion of the epoch timestamp 1491341842:
+ ;SELECT datetime(1491341842, 'unixepoch') 
+
+
+
+decode(value, algorithm)
+══════════════════════════════════════════════════════════════════════
+ Decode the value using the given algorithm
+Parameters
+ value The value to decode
+ algorithm One of the following encoding algorithms:
+ base64, hex, uri
+See Also
+ anonymize(), char(), charindex(), encode(), endswith(), extract(),
+ group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Example
+#1 To decode the URI-encoded string '%63%75%72%6c':
+ ;SELECT decode('%63%75%72%6c', 'uri') 
+
+
+
+degrees(radians)
+══════════════════════════════════════════════════════════════════════
+ Converts radians to degrees
+Parameter
+ radians The radians value to convert to degrees
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To convert PI to degrees:
+ ;SELECT degrees(pi()) 
+
+
+
+dense_rank()
+══════════════════════════════════════════════════════════════════════
+ Returns the row_number() of the first peer in each group without
+ gaps
+See Also
+ cume_dist(), first_value(), lag(), last_value(), lead(), nth_value(),
+ ntile(), percent_rank(), rank(), row_number()
+
+dirname(path)
+══════════════════════════════════════════════════════════════════════
+ Extract the directory portion of a pathname.
+Parameter
+ path The path
+See Also
+ basename(), joinpath(), readlink(), realpath()
+Examples
+#1 To get the directory of a relative file path:
+ ;SELECT dirname('foo/bar') 
+
+
+#2 To get the directory of an absolute file path:
+ ;SELECT dirname('/foo/bar') 
+
+
+#3 To get the directory of a file in the root directory:
+ ;SELECT dirname('/bar') 
+
+
+#4 To get the directory of a Windows path:
+ ;SELECT dirname('foo\bar') 
+
+
+#5 To get the directory of an empty path:
+ ;SELECT dirname('') 
+
+
+
+echoln(value)
+══════════════════════════════════════════════════════════════════════
+ Echo the argument to the current output file and return it
+Parameter
+ value The value to write to the current output file
+See Also
+ :append-to, :echo, :export-session-to, :pipe-line-to, :pipe-to,
+ :redirect-to, :write-csv-to, :write-json-to, :write-jsonlines-to,
+ :write-raw-to, :write-screen-to, :write-table-to, :write-to,
+ :write-view-to
+
+encode(value, algorithm)
+══════════════════════════════════════════════════════════════════════
+ Encode the value using the given algorithm
+Parameters
+ value The value to encode
+ algorithm One of the following encoding algorithms:
+ base64, hex, uri
+See Also
+ anonymize(), char(), charindex(), decode(), endswith(), extract(),
+ group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To base64-encode 'Hello, World!':
+ ;SELECT encode('Hello, World!', 'base64') 
+
+
+#2 To hex-encode 'Hello, World!':
+ ;SELECT encode('Hello, World!', 'hex') 
+
+
+#3 To URI-encode 'Hello, World!':
+ ;SELECT encode('Hello, World!', 'uri') 
+
+
+
+endswith(str, suffix)
+══════════════════════════════════════════════════════════════════════
+ Test if a string ends with the given suffix
+Parameters
+ str The string to test
+ suffix The suffix to check in the string
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), extract(),
+ group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To test if the string 'notbad.jpg' ends with '.jpg':
+ ;SELECT endswith('notbad.jpg', '.jpg') 
+
+
+#2 To test if the string 'notbad.png' starts with '.jpg':
+ ;SELECT endswith('notbad.png', '.jpg') 
+
+
+
+exp(x)
+══════════════════════════════════════════════════════════════════════
+ Returns the value of e raised to the power of x
+Parameter
+ x The exponent
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), floor(), log(), log10(), max(),
+ min(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Example
+#1 To raise e to 2:
+ ;SELECT exp(2) 
+
+
+
+extract(str)
+══════════════════════════════════════════════════════════════════════
+ Automatically Parse and extract data from a string
+Parameter
+ str The string to parse
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To extract key/value pairs from a string:
+ ;SELECT extract('foo=1 bar=2 name="Rolo Tomassi"') 
+
+
+#2 To extract columnar data from a string:
+ ;SELECT extract('1.0 abc 2.0') 
+
+
+
+first_value(expr)
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the first
+ row in the window frame.
+Parameter
+ expr The expression to execute over the first row
+See Also
+ cume_dist(), dense_rank(), lag(), last_value(), lead(), nth_value(),
+ ntile(), percent_rank(), rank(), row_number()
+
+floor(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the largest integer that is not greater than the argument
+Parameter
+ num The number to lower to the floor
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the floor of 1.23:
+ ;SELECT floor(1.23) 
+
+
+
+generate_series(start, stop, [step])
+══════════════════════════════════════════════════════════════════════
+ A table-valued-function that returns the whole numbers between a
+ lower and upper bound, inclusive
+Parameters
+ start The starting point of the series
+ stop The stopping point of the series
+ step The increment between each value
+Result
+ value The number in the series
+
+Examples
+#1 To generate the numbers in the range [10, 14]:
+ ;SELECT value FROM generate_series(10, 14) 
+
+
+#2 To generate every other number in the range [10, 14]:
+ ;SELECT value FROM generate_series(10, 14, 2) 
+
+
+#3 To count down from five to 1:
+ ;SELECT value FROM generate_series(1, 5, -1) 
+
+
+
+gethostbyaddr(hostname)
+══════════════════════════════════════════════════════════════════════
+ Get the hostname for the given IP address
+Parameter
+ hostname The IP address to lookup.
+See Also
+ gethostbyname()
+Example
+#1 To get the hostname for the IP '127.0.0.1':
+ ;SELECT gethostbyaddr('127.0.0.1') 
+
+
+
+gethostbyname(hostname)
+══════════════════════════════════════════════════════════════════════
+ Get the IP address for the given hostname
+Parameter
+ hostname The DNS hostname to lookup.
+See Also
+ gethostbyaddr()
+Example
+#1 To get the IP address for 'localhost':
+ ;SELECT gethostbyname('localhost') 
+
+
+
+glob(pattern, str)
+══════════════════════════════════════════════════════════════════════
+ Match a string against Unix glob pattern
+Parameters
+ pattern The glob pattern
+ str The string to match
+
+Example
+#1 To test if the string 'abc' matches the glob 'a*':
+ ;SELECT glob('a*', 'abc') 
+
+
+
+group_concat(X, [sep])
+══════════════════════════════════════════════════════════════════════
+ Returns a string which is the concatenation of all non-NULL values
+ of X separated by a comma or the given separator.
+Parameters
+ X The value to concatenate.
+ sep The separator to place between the values.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_spooky_hash(), gunzip(), gzip(), humanize_duration(),
+ humanize_file_size(), instr(), leftstr(), length(), logfmt2json(),
+ lower(), ltrim(), padc(), padl(), padr(), parse_url(), printf(),
+ proper(), regexp_capture(), regexp_capture_into_json(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), unicode(), unparse_url(), upper(), xpath()
+Examples
+#1 To concatenate the values of the column 'ex_procname' from the table
+ 'lnav_example_log':
+ ;SELECT group_concat(ex_procname) FROM lnav_example_log
+
+
+#2 To join the values of the column 'ex_procname' using the string ', ':
+ ;SELECT group_concat(ex_procname, ', ') FROM lnav_example_log
+
+
+#3 To concatenate the distinct values of the column 'ex_procname' from the table
+ 'lnav_example_log':
+ ;SELECT group_concat(DISTINCT ex_procname) FROM lnav_example_log
+
+
+
+group_spooky_hash(str, ...)
+══════════════════════════════════════════════════════════════════════
+ Compute the hash value for the given arguments
+Parameter
+ str The string to hash
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), gunzip(), gzip(), humanize_duration(),
+ humanize_file_size(), instr(), leftstr(), length(), logfmt2json(),
+ lower(), ltrim(), padc(), padl(), padr(), parse_url(), printf(),
+ proper(), regexp_capture(), regexp_capture_into_json(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), unicode(), unparse_url(), upper(), xpath()
+Example
+#1 To produce a hash of all of the values of 'column1':
+ ;SELECT group_spooky_hash(column1) FROM (VALUES ('abc'), ('123'))
+
+
+
+gunzip(b, ...)
+══════════════════════════════════════════════════════════════════════
+ Decompress a gzip file
+Parameter
+ b The blob to decompress
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+
+gzip(value, ...)
+══════════════════════════════════════════════════════════════════════
+ Compress a string into a gzip file
+Parameter
+ value The value to compress
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+
+hex(X)
+══════════════════════════════════════════════════════════════════════
+ Returns a string which is the upper-case hexadecimal rendering of
+ the content of its argument.
+Parameter
+ X The blob to convert to hexadecimal
+
+Example
+#1 To get the hexadecimal rendering of the string 'abc':
+ ;SELECT hex('abc') 
+
+
+
+humanize_duration(secs)
+══════════════════════════════════════════════════════════════════════
+ Format the given seconds value as an abbreviated duration string
+Parameter
+ secs The duration in seconds
+See Also
+ anonymize(), char(), charindex(), date(), datetime(), decode(),
+ encode(), endswith(), extract(), group_concat(), group_spooky_hash(),
+ gunzip(), gzip(), humanize_file_size(), instr(), julianday(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), strftime(), substr(), time(),
+ timediff(), timeslice(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Examples
+#1 To format a duration:
+ ;SELECT humanize_duration(15 * 60) 
+
+
+#2 To format a sub-second value:
+ ;SELECT humanize_duration(1.5) 
+
+
+
+humanize_file_size(value)
+══════════════════════════════════════════════════════════════════════
+ Format the given file size as a human-friendly string
+Parameter
+ value The file size to format
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), instr(), leftstr(), length(), logfmt2json(),
+ lower(), ltrim(), padc(), padl(), padr(), parse_url(), printf(),
+ proper(), regexp_capture(), regexp_capture_into_json(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), unicode(), unparse_url(), upper(), xpath()
+Example
+#1 To format an amount:
+ ;SELECT humanize_file_size(10 * 1024 * 1024) 
+
+
+
+ifnull(X, Y)
+══════════════════════════════════════════════════════════════════════
+ Returns a copy of its first non-NULL argument, or NULL if both
+ arguments are NULL
+Parameters
+ X A value to check for NULL-ness
+ Y A value to check for NULL-ness
+
+Example
+#1 To get the first non-null value between null and zero:
+ ;SELECT ifnull(null, 0) 
+
+
+
+instr(haystack, needle)
+══════════════════════════════════════════════════════════════════════
+ Finds the first occurrence of the needle within the haystack and
+ returns the number of prior characters plus 1, or 0 if the needle
+ was not found
+Parameters
+ haystack The string to search within
+ needle The string to look for in the haystack
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), leftstr(), length(),
+ logfmt2json(), lower(), ltrim(), padc(), padl(), padr(), parse_url(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Example
+#1 To test get the position of 'b' in the string 'abc':
+ ;SELECT instr('abc', 'b') 
+
+
+
+jget(json, ptr, [default])
+══════════════════════════════════════════════════════════════════════
+ Get the value from a JSON object using a JSON-Pointer.
+Parameters
+ json The JSON object to query.
+ ptr The JSON-Pointer to lookup in the object.
+ default The default value if the value was not found
+See Also
+ json_concat(), json_contains(), json_group_array(),
+ json_group_object(), yaml_to_json()
+Examples
+#1 To get the root of a JSON value:
+ ;SELECT jget('1', '') 
+
+
+#2 To get the property named 'b' in a JSON object:
+ ;SELECT jget('{ "a": 1, "b": 2 }', '/b') 
+
+
+#3 To get the 'msg' property and return a default if it does not exist:
+ ;SELECT jget(null, '/msg', 'Hello') 
+
+
+
+joinpath(path, ...)
+══════════════════════════════════════════════════════════════════════
+ Join components of a path together.
+Parameter
+ path One or more path components to join together. If an
+ argument starts with a forward or backward slash, it will be
+ considered an absolute path and any preceding elements will
+ be ignored.
+See Also
+ basename(), dirname(), readlink(), realpath()
+Examples
+#1 To join a directory and file name into a relative path:
+ ;SELECT joinpath('foo', 'bar') 
+
+
+#2 To join an empty component with other names into a relative path:
+ ;SELECT joinpath('', 'foo', 'bar') 
+
+
+#3 To create an absolute path with two path components:
+ ;SELECT joinpath('/', 'foo', 'bar') 
+
+
+#4 To create an absolute path from a path component that starts with a forward slash:
+ ;SELECT joinpath('/', 'foo', '/bar') 
+
+
+
+json_concat(json, value, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns an array with the given values concatenated onto the end.
+ If the initial value is null, the result will be an array with the
+ given elements. If the initial value is an array, the result will
+ be an array with the given values at the end. If the initial value
+ is not null or an array, the result will be an array with two
+ elements: the initial value and the given value.
+Parameters
+ json The initial JSON value.
+ value The value(s) to add to the end of the array.
+See Also
+ jget(), json_contains(), json_group_array(), json_group_object(),
+ yaml_to_json()
+Examples
+#1 To append the number 4 to null:
+ ;SELECT json_concat(NULL, 4) 
+
+
+#2 To append 4 and 5 to the array [1, 2, 3]:
+ ;SELECT json_concat('[1, 2, 3]', 4, 5) 
+
+
+#3 To concatenate two arrays together:
+ ;SELECT json_concat('[1, 2, 3]', json('[4, 5]')) 
+
+
+
+json_contains(json, value)
+══════════════════════════════════════════════════════════════════════
+ Check if a JSON value contains the given element.
+Parameters
+ json The JSON value to query.
+ value The value to look for in the first argument
+See Also
+ jget(), json_concat(), json_group_array(), json_group_object(),
+ yaml_to_json()
+Examples
+#1 To test if a JSON array contains the number 4:
+ ;SELECT json_contains('[1, 2, 3]', 4) 
+
+
+#2 To test if a JSON array contains the string 'def':
+ ;SELECT json_contains('["abc", "def"]', 'def') 
+
+
+
+json_group_array(value, ...)
+══════════════════════════════════════════════════════════════════════
+ Collect the given values from a query into a JSON array
+Parameter
+ value The values to append to the array
+See Also
+ jget(), json_concat(), json_contains(), json_group_object(),
+ yaml_to_json()
+Examples
+#1 To create an array from arguments:
+ ;SELECT json_group_array('one', 2, 3.4) 
+
+
+#2 To create an array from a column of values:
+ ;SELECT json_group_array(column1) FROM (VALUES (1), (2), (3))
+
+
+
+json_group_object(name, value, ...)
+══════════════════════════════════════════════════════════════════════
+ Collect the given values from a query into a JSON object
+Parameters
+ name The property name for the value
+ value The value to add to the object
+See Also
+ jget(), json_concat(), json_contains(), json_group_array(),
+ yaml_to_json()
+Examples
+#1 To create an object from arguments:
+ ;SELECT json_group_object('a', 1, 'b', 2) 
+
+
+#2 To create an object from a pair of columns:
+ ;SELECT json_group_object(column1, column2) FROM (VALUES ('a', 1), ('b', 2))
+
+
+
+julianday(timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the number of days since noon in Greenwich on November 24,
+ 4714 B.C.
+Parameters
+ timestring The string to convert to a date with time.
+ modifier A transformation that is applied to the
+ value to the left.
+See Also
+ date(), datetime(), humanize_duration(), strftime(), time(),
+ timediff(), timeslice()
+Examples
+#1 To get the julian day from the timestamp '2017-01-02T03:04:05':
+ ;SELECT julianday('2017-01-02T03:04:05') 
+
+
+#2 To get the julian day from the timestamp '2017-01-02T03:04:05' plus one minute:
+ ;SELECT julianday('2017-01-02T03:04:05', '+1 minute')
+
+
+#3 To get the julian day from the timestamp 1491341842:
+ ;SELECT julianday(1491341842, 'unixepoch') 
+
+
+
+lag(expr, [offset], [default])
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the previous
+ row in the partition.
+Parameters
+ expr The expression to execute over the previous row
+ offset The offset from the current row in the partition
+ default The default value if the previous row does not
+ exist instead of NULL
+See Also
+ cume_dist(), dense_rank(), first_value(), last_value(), lead(),
+ nth_value(), ntile(), percent_rank(), rank(), row_number()
+
+last_insert_rowid()
+══════════════════════════════════════════════════════════════════════
+ Returns the ROWID of the last row insert from the database
+ connection which invoked the function
+
+
+last_value(expr)
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the last row
+ in the window frame.
+Parameter
+ expr The expression to execute over the last row
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), lead(), nth_value(),
+ ntile(), percent_rank(), rank(), row_number()
+
+lead(expr, [offset], [default])
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the next row
+ in the partition.
+Parameters
+ expr The expression to execute over the next row
+ offset The offset from the current row in the partition
+ default The default value if the next row does not exist
+ instead of NULL
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(),
+ nth_value(), ntile(), percent_rank(), rank(), row_number()
+
+leftstr(str, N)
+══════════════════════════════════════════════════════════════════════
+ Returns the N leftmost (UTF-8) characters in the given string.
+Parameters
+ str The string to return subset.
+ N The number of characters from the left side of the
+ string to return.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), length(),
+ logfmt2json(), lower(), ltrim(), padc(), padl(), padr(), parse_url(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Examples
+#1 To get the first character of the string 'abc':
+ ;SELECT leftstr('abc', 1) 
+
+
+#2 To get the first ten characters of a string, regardless of size:
+ ;SELECT leftstr('abc', 10) 
+
+
+
+length(str)
+══════════════════════════════════════════════════════════════════════
+ Returns the number of characters (not bytes) in the given string
+ prior to the first NUL character
+Parameter
+ str The string to determine the length of
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ logfmt2json(), lower(), ltrim(), padc(), padl(), padr(), parse_url(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Example
+#1 To get the length of the string 'abc':
+ ;SELECT length('abc') 
+
+
+
+like(pattern, str, [escape])
+══════════════════════════════════════════════════════════════════════
+ Match a string against a pattern
+Parameters
+ pattern The pattern to match. A percent symbol (%) will
+ match zero or more characters and an underscore (_) will
+ match a single character.
+ str The string to match
+ escape The escape character that can be used to prefix
+ a literal percent or underscore in the pattern.
+
+Examples
+#1 To test if the string 'aabcc' contains the letter 'b':
+ ;SELECT like('%b%', 'aabcc') 
+
+
+#2 To test if the string 'aab%' ends with 'b%':
+ ;SELECT like('%b:%', 'aab%', ':') 
+
+
+
+likelihood(value, probability)
+══════════════════════════════════════════════════════════════════════
+ Provides a hint to the query planner that the first argument is a
+ boolean that is true with the given probability
+Parameters
+ value The boolean value to return
+ probability A floating point constant between 0.0
+ and 1.0
+
+
+likely(value)
+══════════════════════════════════════════════════════════════════════
+ Short-hand for likelihood(X,0.9375)
+Parameter
+ value The boolean value to return
+
+
+lnav_top_file()
+══════════════════════════════════════════════════════════════════════
+ Return the name of the file that the top line in the current view
+ came from.
+
+
+lnav_version()
+══════════════════════════════════════════════════════════════════════
+ Return the current version of lnav
+
+
+load_extension(path, [entry-point])
+══════════════════════════════════════════════════════════════════════
+ Loads SQLite extensions out of the given shared library file using
+ the given entry point.
+Parameters
+ path The path to the shared library
+ containing the extension.
+ entry-point
+
+
+log(x)
+══════════════════════════════════════════════════════════════════════
+ Returns the natural logarithm of x
+Parameter
+ x The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log10(), max(),
+ min(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Example
+#1 To get the natual logarithm of 8:
+ ;SELECT log(8) 
+
+
+
+log10(x)
+══════════════════════════════════════════════════════════════════════
+ Returns the base-10 logarithm of X
+Parameter
+ x The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the logarithm of 100:
+ ;SELECT log10(100) 
+
+
+
+log_top_datetime()
+══════════════════════════════════════════════════════════════════════
+ Return the timestamp of the line at the top of the log view.
+
+
+log_top_line()
+══════════════════════════════════════════════════════════════════════
+ Return the line number at the top of the log view.
+
+
+logfmt2json(str)
+══════════════════════════════════════════════════════════════════════
+ Convert a logfmt-encoded string into JSON
+Parameter
+ str The logfmt message to parse
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), lower(), ltrim(), padc(), padl(), padr(), parse_url(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Example
+#1 To extract key/value pairs from a log message:
+ ;SELECT logfmt2json('foo=1 bar=2 name="Rolo Tomassi"')
+
+
+
+lower(str)
+══════════════════════════════════════════════════════════════════════
+ Returns a copy of the given string with all ASCII characters
+ converted to lower case.
+Parameter
+ str The string to convert.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), ltrim(), padc(), padl(), padr(), parse_url(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Example
+#1 To lowercase the string 'AbC':
+ ;SELECT lower('AbC') 
+
+
+
+ltrim(str, [chars])
+══════════════════════════════════════════════════════════════════════
+ Returns a string formed by removing any and all characters that
+ appear in the second argument from the left side of the first.
+Parameters
+ str The string to trim characters from the left side
+ chars The characters to trim. Defaults to spaces.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), padc(), padl(), padr(), parse_url(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Examples
+#1 To trim the leading space characters from the string ' abc':
+ ;SELECT ltrim(' abc') 
+
+
+#2 To trim the characters 'a' or 'b' from the left side of the string 'aaaabbbc':
+ ;SELECT ltrim('aaaabbbc', 'ab') 
+
+
+
+max(X, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the argument with the maximum value, or return NULL if any
+ argument is NULL.
+Parameter
+ X The numbers to find the maximum of. If only one argument is
+ given, this function operates as an aggregate.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ min(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Examples
+#1 To get the largest value from the parameters:
+ ;SELECT max(2, 1, 3) 
+
+
+#2 To get the largest value from an aggregate:
+ ;SELECT max(status) FROM http_status_codes 
+
+
+
+min(X, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the argument with the minimum value, or return NULL if any
+ argument is NULL.
+Parameter
+ X The numbers to find the minimum of. If only one argument is
+ given, this function operates as an aggregate.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Examples
+#1 To get the smallest value from the parameters:
+ ;SELECT min(2, 1, 3) 
+
+
+#2 To get the smallest value from an aggregate:
+ ;SELECT min(status) FROM http_status_codes 
+
+
+
+nth_value(expr, N)
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the nth row
+ in the window frame.
+Parameters
+ expr The expression to execute over the nth row
+ N The row number
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ ntile(), percent_rank(), rank(), row_number()
+
+ntile(groups)
+══════════════════════════════════════════════════════════════════════
+ Returns the number of the group that the current row is a part of
+Parameter
+ groups The number of groups
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ nth_value(), percent_rank(), rank(), row_number()
+
+nullif(X, Y)
+══════════════════════════════════════════════════════════════════════
+ Returns its first argument if the arguments are different and NULL
+ if the arguments are the same.
+Parameters
+ X The first argument to compare.
+ Y The argument to compare against the first.
+
+Examples
+#1 To test if 1 is different from 1:
+ ;SELECT nullif(1, 1) 
+
+
+#2 To test if 1 is different from 2:
+ ;SELECT nullif(1, 2) 
+
+
+
+padc(str, len)
+══════════════════════════════════════════════════════════════════════
+ Pad the given string with enough spaces to make it centered within
+ the given length
+Parameters
+ str The string to pad
+ len The minimum desired length of the output string
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padl(), padr(), parse_url(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Examples
+#1 To pad the string 'abc' to a length of six characters:
+ ;SELECT padc('abc', 6) || 'def' 
+
+
+#2 To pad the string 'abcdef' to a length of eight characters:
+ ;SELECT padc('abcdef', 8) || 'ghi' 
+
+
+
+padl(str, len)
+══════════════════════════════════════════════════════════════════════
+ Pad the given string with leading spaces until it reaches the
+ desired length
+Parameters
+ str The string to pad
+ len The minimum desired length of the output string
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padr(), parse_url(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Examples
+#1 To pad the string 'abc' to a length of six characters:
+ ;SELECT padl('abc', 6) 
+
+
+#2 To pad the string 'abcdef' to a length of four characters:
+ ;SELECT padl('abcdef', 4) 
+
+
+
+padr(str, len)
+══════════════════════════════════════════════════════════════════════
+ Pad the given string with trailing spaces until it reaches the
+ desired length
+Parameters
+ str The string to pad
+ len The minimum desired length of the output string
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), parse_url(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Examples
+#1 To pad the string 'abc' to a length of six characters:
+ ;SELECT padr('abc', 6) || 'def' 
+
+
+#2 To pad the string 'abcdef' to a length of four characters:
+ ;SELECT padr('abcdef', 4) || 'ghi' 
+
+
+
+parse_url(url)
+══════════════════════════════════════════════════════════════════════
+ Parse a URL and return the components in a JSON object. Limitations:
+ not all URL schemes are supported and repeated query parameters are
+ not captured.
+Parameter
+ url The URL to parse
+Results
+ scheme The URL's scheme
+ username The name of the user specified in the URL
+ password The password specified in the URL
+ host The host name / IP specified in the URL
+ port The port specified in the URL
+ path The path specified in the URL
+ query The query string in the URL
+ parameters An object containing the query parameters
+ fragment The fragment specified in the URL
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ printf(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), unparse_url(),
+ upper(), xpath()
+Examples
+#1 To parse the URL 'https://example.com/search?q=hello%20world':
+ ;SELECT parse_url('https://example.com/search?q=hello%20world')
+
+
+#2 To parse the URL 'https://alice@[fe80::14ff:4ee5:1215:2fb2]':
+ ;SELECT parse_url('https://alice@[fe80::14ff:4ee5:1215:2fb2]')
+
+
+
+percent_rank()
+══════════════════════════════════════════════════════════════════════
+ Returns (rank - 1) / (partition-rows - 1)
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ nth_value(), ntile(), rank(), row_number()
+
+pi()
+══════════════════════════════════════════════════════════════════════
+ Returns the value of PI
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Example
+#1 To get the value of PI:
+ ;SELECT pi() 
+
+
+
+power(base, exp)
+══════════════════════════════════════════════════════════════════════
+ Returns the base to the given exponent
+Parameters
+ base The base number
+ exp The exponent
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), radians(), round(), sign(), square(), sum(),
+ total()
+Example
+#1 To raise two to the power of three:
+ ;SELECT power(2, 3) 
+
+
+
+printf(format, X)
+══════════════════════════════════════════════════════════════════════
+ Returns a string with this functions arguments substituted into the
+ given format. Substitution points are specified using percent (%)
+ options, much like the standard C printf() function.
+Parameters
+ format The format of the string to return.
+ X The argument to substitute at a given position in
+ the format.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), proper(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Examples
+#1 To substitute 'World' into the string 'Hello, %s!':
+ ;SELECT printf('Hello, %s!', 'World') 
+
+
+#2 To right-align 'small' in the string 'align:' with a column width of 10:
+ ;SELECT printf('align: % 10s', 'small') 
+
+
+#3 To format 11 with a width of five characters and leading zeroes:
+ ;SELECT printf('value: %05d', 11) 
+
+
+
+proper(str)
+══════════════════════════════════════════════════════════════════════
+ Capitalize the first character of words in the given string
+Parameter
+ str The string to capitalize.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), regexp_capture(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Example
+#1 To capitalize the words in the string 'hello, world!':
+ ;SELECT proper('hello, world!') 
+
+
+
+quote(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the text of an SQL literal which is the value of its
+ argument suitable for inclusion into an SQL statement.
+Parameter
+ X The string to quote.
+
+Examples
+#1 To quote the string 'abc':
+ ;SELECT quote('abc') 
+
+
+#2 To quote the string 'abc'123':
+ ;SELECT quote('abc''123') 
+
+
+
+radians(degrees)
+══════════════════════════════════════════════════════════════════════
+ Converts degrees to radians
+Parameter
+ degrees The degrees value to convert to radians
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), round(), sign(), square(), sum(), total()
+Example
+#1 To convert 180 degrees to radians:
+ ;SELECT radians(180) 
+
+
+
+raise_error(msg)
+══════════════════════════════════════════════════════════════════════
+ Raises an error with the given message when executed
+Parameter
+ msg The error message
+
+
+random()
+══════════════════════════════════════════════════════════════════════
+ Returns a pseudo-random integer between -9223372036854775808 and
+ +9223372036854775807.
+
+
+randomblob(N)
+══════════════════════════════════════════════════════════════════════
+ Return an N-byte blob containing pseudo-random bytes.
+Parameter
+ N The size of the blob in bytes.
+
+
+rank()
+══════════════════════════════════════════════════════════════════════
+ Returns the row_number() of the first peer in each group with gaps
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ nth_value(), ntile(), percent_rank(), row_number()
+
+readlink(path)
+══════════════════════════════════════════════════════════════════════
+ Read the target of a symbolic link.
+Parameter
+ path The path to the symbolic link.
+See Also
+ basename(), dirname(), joinpath(), realpath()
+
+realpath(path)
+══════════════════════════════════════════════════════════════════════
+ Returns the resolved version of the given path, expanding symbolic
+ links and resolving '.' and '..' references.
+Parameter
+ path The path to resolve.
+See Also
+ basename(), dirname(), joinpath(), readlink()
+
+regexp(re, str)
+══════════════════════════════════════════════════════════════════════
+ Test if a string matches a regular expression
+Parameters
+ re The regular expression to use
+ str The string to test against the regular expression
+
+
+regexp_capture(string, pattern)
+══════════════════════════════════════════════════════════════════════
+ A table-valued function that executes a regular-expression over a
+ string and returns the captured values. If the regex only matches a
+ subset of the input string, it will be rerun on the remaining parts
+ of the string until no more matches are found.
+Parameters
+ string The string to match against the given pattern.
+ pattern The regular expression to match.
+Results
+ match_index The match iteration. This value
+ will increase each time a new match is found in the
+ input string.
+ capture_index The index of the capture in the
+ regex.
+ capture_name The name of the capture in the
+ regex.
+ capture_count The total number of captures in the
+ regex.
+ range_start The start of the capture in the
+ input string.
+ range_stop The stop of the capture in the input
+ string.
+ content The captured value from the string.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture_into_json(),
+ regexp_match(), regexp_replace(), replace(), replicate(), reverse(),
+ rightstr(), rtrim(), sparkline(), spooky_hash(), startswith(),
+ strfilter(), substr(), trim(), unicode(), unparse_url(), upper(),
+ xpath()
+Example
+#1 To extract the key/value pairs 'a'/1 and 'b'/2 from the string 'a=1; b=2':
+ ;SELECT * FROM regexp_capture('a=1; b=2', '(\w+)=(\d+)')
+
+
+
+regexp_capture_into_json(string, pattern, [options])
+══════════════════════════════════════════════════════════════════════
+ A table-valued function that executes a regular-expression over a
+ string and returns the captured values as a JSON object. If the
+ regex only matches a subset of the input string, it will be rerun on
+ the remaining parts of the string until no more matches are found.
+Parameters
+ string The string to match against the given pattern.
+ pattern The regular expression to match.
+ options A JSON object with the following option:
+ convert-numbers - True (default) if text that looks like
+ numeric data should be converted to JSON numbers, false if
+ they should be captured as strings.
+Results
+ match_index The match iteration. This value will
+ increase each time a new match is found in the input
+ string.
+ content The captured values from the string.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), unicode(), unparse_url(), upper(), xpath()
+Example
+#1 To extract the key/value pairs 'a'/1 and 'b'/2 from the string 'a=1; b=2':
+ ;SELECT * FROM regexp_capture_into_json('a=1; b=2', '(\w+)=(\d+)')
+
+
+
+regexp_match(re, str)
+══════════════════════════════════════════════════════════════════════
+ Match a string against a regular expression and return the capture
+ groups as JSON.
+Parameters
+ re The regular expression to use
+ str The string to test against the regular expression
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_replace(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To capture the digits from the string '123':
+ ;SELECT regexp_match('(\d+)', '123') 
+
+
+#2 To capture a number and word into a JSON object with the properties 'col_0' and
+ 'col_1':
+ ;SELECT regexp_match('(\d+) (\w+)', '123 four') 
+
+
+#3 To capture a number and word into a JSON object with the named properties 'num' and
+ 'str':
+ ;SELECT regexp_match('(?<num>\d+) (?<str>\w+)', '123 four')
+
+
+
+regexp_replace(str, re, repl)
+══════════════════════════════════════════════════════════════════════
+ Replace the parts of a string that match a regular expression.
+Parameters
+ str The string to perform replacements on
+ re The regular expression to match
+ repl The replacement string. You can reference capture
+ groups with a backslash followed by the number of the group,
+ starting with 1.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_match(), replace(),
+ replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To replace the word at the start of the string 'Hello, World!' with 'Goodbye':
+ ;SELECT regexp_replace('Hello, World!', '^(\w+)', 'Goodbye')
+
+
+#2 To wrap alphanumeric words with angle brackets:
+ ;SELECT regexp_replace('123 abc', '(\w+)', '<\1>') 
+
+
+
+replace(str, old, replacement)
+══════════════════════════════════════════════════════════════════════
+ Returns a string formed by substituting the replacement string for
+ every occurrence of the old string in the given string.
+Parameters
+ str The string to perform substitutions on.
+ old The string to be replaced.
+ replacement The string to replace any occurrences of
+ the old string with.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To replace the string 'x' with 'z' in 'abc':
+ ;SELECT replace('abc', 'x', 'z') 
+
+
+#2 To replace the string 'a' with 'z' in 'abc':
+ ;SELECT replace('abc', 'a', 'z') 
+
+
+
+replicate(str, N)
+══════════════════════════════════════════════════════════════════════
+ Returns the given string concatenated N times.
+Parameters
+ str The string to replicate.
+ N The number of times to replicate the string.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), reverse(), rightstr(), rtrim(), sparkline(), spooky_hash(),
+ startswith(), strfilter(), substr(), trim(), unicode(), unparse_url(),
+ upper(), xpath()
+Example
+#1 To repeat the string 'abc' three times:
+ ;SELECT replicate('abc', 3) 
+
+
+
+reverse(str)
+══════════════════════════════════════════════════════════════════════
+ Returns the reverse of the given string.
+Parameter
+ str The string to reverse.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Example
+#1 To reverse the string 'abc':
+ ;SELECT reverse('abc') 
+
+
+
+rightstr(str, N)
+══════════════════════════════════════════════════════════════════════
+ Returns the N rightmost (UTF-8) characters in the given string.
+Parameters
+ str The string to return subset.
+ N The number of characters from the right side of the
+ string to return.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rtrim(), sparkline(), spooky_hash(),
+ startswith(), strfilter(), substr(), trim(), unicode(), unparse_url(),
+ upper(), xpath()
+Examples
+#1 To get the last character of the string 'abc':
+ ;SELECT rightstr('abc', 1) 
+
+
+#2 To get the last ten characters of a string, regardless of size:
+ ;SELECT rightstr('abc', 10) 
+
+
+
+round(num, [digits])
+══════════════════════════════════════════════════════════════════════
+ Returns a floating-point value rounded to the given number of digits
+ to the right of the decimal point.
+Parameters
+ num The value to round.
+ digits The number of digits to the right of the decimal
+ to round to.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), sign(), square(), sum(),
+ total()
+Examples
+#1 To round the number 123.456 to an integer:
+ ;SELECT round(123.456) 
+
+
+#2 To round the number 123.456 to a precision of 1:
+ ;SELECT round(123.456, 1) 
+
+
+#3 To round the number 123.456 to a precision of 5:
+ ;SELECT round(123.456, 5) 
+
+
+
+row_number()
+══════════════════════════════════════════════════════════════════════
+ Returns the number of the row within the current partition, starting
+ from 1.
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ nth_value(), ntile(), percent_rank(), rank()
+Example
+#1 To number messages from a process:
+ ;SELECT row_number() OVER (PARTITION BY ex_procname ORDER BY log_line) AS msg_num,
+  ex_procname, log_body FROM lnav_example_log
+
+
+
+rtrim(str, [chars])
+══════════════════════════════════════════════════════════════════════
+ Returns a string formed by removing any and all characters that
+ appear in the second argument from the right side of the first.
+Parameters
+ str The string to trim characters from the right side
+ chars The characters to trim. Defaults to spaces.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To trim the space characters from the end of the string 'abc ':
+ ;SELECT rtrim('abc ') 
+
+
+#2 To trim the characters 'b' and 'c' from the string 'abbbbcccc':
+ ;SELECT rtrim('abbbbcccc', 'bc') 
+
+
+
+sign(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the sign of the given number as -1, 0, or 1
+Parameter
+ num The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), round(), square(), sum(),
+ total()
+Examples
+#1 To get the sign of 10:
+ ;SELECT sign(10) 
+
+
+#2 To get the sign of 0:
+ ;SELECT sign(0) 
+
+
+#3 To get the sign of -10:
+ ;SELECT sign(-10) 
+
+
+
+sparkline(value, [upper])
+══════════════════════════════════════════════════════════════════════
+ Function used to generate a sparkline bar chart. The non-aggregate
+ version converts a single numeric value on a range to a bar chart
+ character. The aggregate version returns a string with a bar
+ character for every numeric input
+Parameters
+ value The numeric value to convert
+ upper The upper bound of the numeric range. The
+ non-aggregate version defaults to 100. The aggregate
+ version uses the largest value in the inputs.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), spooky_hash(),
+ startswith(), strfilter(), substr(), trim(), unicode(), unparse_url(),
+ upper(), xpath()
+Examples
+#1 To get the unicode block element for the value 32 in the range of 0-128:
+ ;SELECT sparkline(32, 128) 
+
+
+#2 To chart the values in a JSON array:
+ ;SELECT sparkline(value) FROM json_each('[0, 1, 2, 3, 4, 5, 6, 7, 8]')
+
+
+
+spooky_hash(str, ...)
+══════════════════════════════════════════════════════════════════════
+ Compute the hash value for the given arguments.
+Parameter
+ str The string to hash
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ startswith(), strfilter(), substr(), trim(), unicode(), unparse_url(),
+ upper(), xpath()
+Examples
+#1 To produce a hash for the string 'Hello, World!':
+ ;SELECT spooky_hash('Hello, World!') 
+
+
+#2 To produce a hash for the parameters where one is NULL:
+ ;SELECT spooky_hash('Hello, World!', NULL) 
+
+
+#3 To produce a hash for the parameters where one is an empty string:
+ ;SELECT spooky_hash('Hello, World!', '') 
+
+
+#4 To produce a hash for the parameters where one is a number:
+ ;SELECT spooky_hash('Hello, World!', 123) 
+
+
+
+sqlite_compileoption_get(N)
+══════════════════════════════════════════════════════════════════════
+ Returns the N-th compile-time option used to build SQLite or NULL if
+ N is out of range.
+Parameter
+ N The option number to get
+
+
+sqlite_compileoption_used(option)
+══════════════════════════════════════════════════════════════════════
+ Returns true (1) or false (0) depending on whether or not that
+ compile-time option was used during the build.
+Parameter
+ option The name of the compile-time option.
+
+Example
+#1 To check if the SQLite library was compiled with ENABLE_FTS3:
+ ;SELECT sqlite_compileoption_used('ENABLE_FTS3') 
+
+
+
+sqlite_source_id()
+══════════════════════════════════════════════════════════════════════
+ Returns a string that identifies the specific version of the source
+ code that was used to build the SQLite library.
+
+
+sqlite_version()
+══════════════════════════════════════════════════════════════════════
+ Returns the version string for the SQLite library that is running.
+
+
+square(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the square of the argument
+Parameter
+ num The number to square
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), round(), sign(), sum(),
+ total()
+Example
+#1 To get the square of two:
+ ;SELECT square(2) 
+
+
+
+startswith(str, prefix)
+══════════════════════════════════════════════════════════════════════
+ Test if a string begins with the given prefix
+Parameters
+ str The string to test
+ prefix The prefix to check in the string
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), strfilter(), substr(), trim(), unicode(), unparse_url(),
+ upper(), xpath()
+Examples
+#1 To test if the string 'foobar' starts with 'foo':
+ ;SELECT startswith('foobar', 'foo') 
+
+
+#2 To test if the string 'foobar' starts with 'bar':
+ ;SELECT startswith('foobar', 'bar') 
+
+
+
+strfilter(source, include)
+══════════════════════════════════════════════════════════════════════
+ Returns the source string with only the characters given in the
+ second parameter
+Parameters
+ source The string to filter
+ include The characters to include in the result
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), substr(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Example
+#1 To get the 'b', 'c', and 'd' characters from the string 'abcabc':
+ ;SELECT strfilter('abcabc', 'bcd') 
+
+
+
+strftime(format, timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the date formatted according to the format string specified
+ as the first argument.
+Parameters
+ format A format string with substitutions similar
+ to those found in the strftime() standard C library.
+ timestring The string to convert to a date with time.
+ modifier A transformation that is applied to the
+ value to the left.
+See Also
+ date(), datetime(), humanize_duration(), julianday(), time(),
+ timediff(), timeslice()
+Examples
+#1 To get the year from the timestamp '2017-01-02T03:04:05':
+ ;SELECT strftime('%Y', '2017-01-02T03:04:05') 
+
+
+#2 To create a string with the time from the timestamp '2017-01-02T03:04:05' plus one
+ minute:
+ ;SELECT strftime('The time is: %H:%M:%S', '2017-01-02T03:04:05', '+1 minute')
+
+
+#3 To create a string with the Julian day from the epoch timestamp 1491341842:
+ ;SELECT strftime('Julian day: %J', 1491341842, 'unixepoch')
+
+
+
+substr(str, start, [size])
+══════════════════════════════════════════════════════════════════════
+ Returns a substring of input string X that begins with the Y-th
+ character and which is Z characters long.
+Parameters
+ str The string to extract a substring from.
+ start The index within 'str' that is the start of the
+ substring. Indexes begin at 1. A negative value means that
+ the substring is found by counting from the right rather
+ than the left.
+ size The size of the substring. If not given, then all
+ characters through the end of the string are returned. If
+ the value is negative, then the characters before the start
+ are returned.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), trim(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To get the substring starting at the second character until the end of the string
+ 'abc':
+ ;SELECT substr('abc', 2) 
+
+
+#2 To get the substring of size one starting at the second character of the string
+ 'abc':
+ ;SELECT substr('abc', 2, 1) 
+
+
+#3 To get the substring starting at the last character until the end of the string
+ 'abc':
+ ;SELECT substr('abc', -1) 
+
+
+#4 To get the substring starting at the last character and going backwards one step of
+ the string 'abc':
+ ;SELECT substr('abc', -1, -1) 
+
+
+
+sum(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the sum of the values in the group as an integer.
+Parameter
+ X The values to add.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), round(), sign(), square(),
+ total()
+Example
+#1 To sum all of the values in the column 'ex_duration' from the table
+ 'lnav_example_log':
+ ;SELECT sum(ex_duration) FROM lnav_example_log 
+
+
+
+time(timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the time in this format: HH:MM:SS.
+Parameters
+ timestring The string to convert to a time.
+ modifier A transformation that is applied to the
+ value to the left.
+See Also
+ date(), datetime(), humanize_duration(), julianday(), strftime(),
+ timediff(), timeslice()
+Examples
+#1 To get the time portion of the timestamp '2017-01-02T03:04:05':
+ ;SELECT time('2017-01-02T03:04:05') 
+
+
+#2 To get the time portion of the timestamp '2017-01-02T03:04:05' plus one minute:
+ ;SELECT time('2017-01-02T03:04:05', '+1 minute') 
+
+
+#3 To get the time portion of the epoch timestamp 1491341842:
+ ;SELECT time(1491341842, 'unixepoch') 
+
+
+
+timediff(time1, time2)
+══════════════════════════════════════════════════════════════════════
+ Compute the difference between two timestamps in seconds
+Parameters
+ time1 The first timestamp
+ time2 The timestamp to subtract from the first
+See Also
+ date(), datetime(), humanize_duration(), julianday(), strftime(),
+ time(), timeslice()
+Examples
+#1 To get the difference between two timestamps:
+ ;SELECT timediff('2017-02-03T04:05:06', '2017-02-03T04:05:00')
+
+
+#2 To get the difference between relative timestamps:
+ ;SELECT timediff('today', 'yesterday') 
+
+
+
+timeslice(time, slice)
+══════════════════════════════════════════════════════════════════════
+ Return the start of the slice of time that the given timestamp falls
+ in. If the time falls outside of the slice, NULL is returned.
+Parameters
+ time The timestamp to get the time slice for.
+ slice The size of the time slices
+See Also
+ date(), datetime(), humanize_duration(), julianday(), strftime(),
+ time(), timediff()
+Examples
+#1 To get the timestamp rounded down to the start of the ten minute slice:
+ ;SELECT timeslice('2017-01-01T05:05:00', '10m') 
+
+
+#2 To group log messages into five minute buckets and count them:
+ ;SELECT timeslice(log_time_msecs, '5m') AS slice, count(1)
+   FROM lnav_example_log GROUP BY slice
+
+
+#3 To group log messages by those before 4:30am and after:
+ ;SELECT timeslice(log_time_msecs, 'before 4:30am') AS slice, count(1) FROM
+  lnav_example_log GROUP BY slice
+
+
+
+total(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the sum of the values in the group as a floating-point.
+Parameter
+ X The values to add.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), round(), sign(), square(),
+ sum()
+Example
+#1 To total all of the values in the column 'ex_duration' from the table
+ 'lnav_example_log':
+ ;SELECT total(ex_duration) FROM lnav_example_log 
+
+
+
+total_changes()
+══════════════════════════════════════════════════════════════════════
+ Returns the number of row changes caused by INSERT, UPDATE or DELETE
+ statements since the current database connection was opened.
+
+
+trim(str, [chars])
+══════════════════════════════════════════════════════════════════════
+ Returns a string formed by removing any and all characters that
+ appear in the second argument from the left and right sides of the
+ first.
+Parameters
+ str The string to trim characters from the left and
+ right sides.
+ chars The characters to trim. Defaults to spaces.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), unicode(),
+ unparse_url(), upper(), xpath()
+Examples
+#1 To trim spaces from the start and end of the string ' abc ':
+ ;SELECT trim(' abc ') 
+
+
+#2 To trim the characters '-' and '+' from the string '-+abc+-':
+ ;SELECT trim('-+abc+-', '-+') 
+
+
+
+typeof(X)
+══════════════════════════════════════════════════════════════════════
+ Returns a string that indicates the datatype of the expression X:
+ "null", "integer", "real", "text", or "blob".
+Parameter
+ X The expression to check.
+
+Examples
+#1 To get the type of the number 1:
+ ;SELECT typeof(1) 
+
+
+#2 To get the type of the string 'abc':
+ ;SELECT typeof('abc') 
+
+
+
+unicode(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the numeric unicode code point corresponding to the first
+ character of the string X.
+Parameter
+ X The string to examine.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(),
+ unparse_url(), upper(), xpath()
+Example
+#1 To get the unicode code point for the first character of 'abc':
+ ;SELECT unicode('abc') 
+
+
+
+unlikely(value)
+══════════════════════════════════════════════════════════════════════
+ Short-hand for likelihood(X, 0.0625)
+Parameter
+ value The boolean value to return
+
+
+unparse_url(obj)
+══════════════════════════════════════════════════════════════════════
+ Convert a JSON object containing the parts of a URL into a URL
+ string
+Parameter
+ obj The JSON object containing the URL parts
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Example
+#1 To unparse the object '{"scheme": "https", "host": "example.com"}':
+ ;SELECT unparse_url('{"scheme": "https", "host": "example.com"}')
+
+
+
+upper(str)
+══════════════════════════════════════════════════════════════════════
+ Returns a copy of the given string with all ASCII characters
+ converted to upper case.
+Parameter
+ str The string to convert.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), xpath()
+Example
+#1 To uppercase the string 'aBc':
+ ;SELECT upper('aBc') 
+
+
+
+xpath(xpath, xmldoc)
+══════════════════════════════════════════════════════════════════════
+ A table-valued function that executes an xpath expression over an
+ XML string and returns the selected values.
+Parameters
+ xpath The XPATH expression to evaluate over the XML
+ document.
+ xmldoc The XML document as a string.
+Results
+ result The result of the XPATH expression.
+ node_path The absolute path to the node containing the
+ result.
+ node_attr The node's attributes stored in JSON object.
+ node_text The node's text value.
+See Also
+ anonymize(), char(), charindex(), decode(), encode(), endswith(),
+ extract(), group_concat(), group_spooky_hash(), gunzip(), gzip(),
+ humanize_duration(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ parse_url(), printf(), proper(), regexp_capture(),
+ regexp_capture_into_json(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ unparse_url(), upper()
+Examples
+#1 To select the XML nodes on the path '/abc/def':
+ ;SELECT * FROM xpath('/abc/def', '<abc><def a="b">Hello</def><def>Bye</def></abc>')
+
+
+#2 To select all 'a' attributes on the path '/abc/def':
+ ;SELECT * FROM xpath('/abc/def/@a', '<abc><def a="b">Hello</def><def>Bye</def></abc>')
+
+
+#3 To select the text nodes on the path '/abc/def':
+ ;SELECT * FROM xpath('/abc/def/text()', '<abc><def a="b">Hello &#x2605;</def></abc>')
+
+
+
+yaml_to_json(yaml)
+══════════════════════════════════════════════════════════════════════
+ Convert a YAML document to a JSON-encoded string
+Parameter
+ yaml The YAML value to convert to JSON.
+See Also
+ jget(), json_concat(), json_contains(), json_group_array(),
+ json_group_object()
+Example
+#1 To convert the document "abc: def":
+ ;SELECT yaml_to_json('abc: def') 
+
+
+
+zeroblob(N)
+══════════════════════════════════════════════════════════════════════
+ Returns a BLOB consisting of N bytes of 0x00.
+Parameter
+ N The size of the BLOB.
+
+
+ATTACH DATABASE filename AS schema-name
+══════════════════════════════════════════════════════════════════════
+ Attach a database file to the current connection.
+Parameters
+ filename The path to the database file.
+ schema-name The prefix for tables in this database.
+
+Example
+#1 To attach the database file '/tmp/customers.db' with the name customers:
+ ;ATTACH DATABASE '/tmp/customers.db' AS customers 
+
+
+
+CASE [base-expr] WHEN cmp-expr1 THEN then-expr1 [... WHEN cmp-exprN THEN then-exprN] [ELSE else-expr] END
+══════════════════════════════════════════════════════════════════════
+ Evaluate a series of expressions in order until one evaluates to
+ true and then return it's result. Similar to an IF-THEN-ELSE
+ construct in other languages.
+Parameters
+ base-expr The base expression that is used for
+ comparison in the branches
+ cmp-expr The expression to test if this branch should
+ be taken
+ else-expr The result of this CASE if no branches
+ matched.
+
+Example
+#1 To evaluate the number one and return the string 'one':
+ ;SELECT CASE 1 WHEN 0 THEN 'zero' WHEN 1 THEN 'one' END
+
+
+
+CREATE [TEMP] VIEW [IF NOT EXISTS] [schema-name.] view-name AS select-stmt
+══════════════════════════════════════════════════════════════════════
+ Assign a name to a SELECT statement
+Parameters
+ IF NOT EXISTS Do not create the view if it already
+ exists
+ schema-name. The database to create the view in
+ view-name The name of the view
+ select-stmt The SELECT statement the view
+ represents
+
+
+DELETE FROM table-name [WHERE cond]
+══════════════════════════════════════════════════════════════════════
+ Delete rows from a table
+Parameters
+ table-name The name of the table
+ cond The conditions used to delete the rows.
+
+
+DETACH DATABASE schema-name
+══════════════════════════════════════════════════════════════════════
+ Detach a database from the current connection.
+Parameter
+ schema-name The prefix for tables in this database.
+
+Example
+#1 To detach the database named 'customers':
+ ;DETACH DATABASE customers 
+
+
+
+DROP VIEW [IF EXISTS] [schema-name.] view-name
+══════════════════════════════════════════════════════════════════════
+ Drop a view
+Parameters
+
+
+INSERT INTO [schema-name.] table-name [( column-name1 [, ... column-nameN] )] VALUES ( expr1 [, ... exprN] )
+══════════════════════════════════════════════════════════════════════
+ Insert rows into a table
+Parameters
+
+Example
+#1 To insert the pair containing 'MSG' and 'HELLO, WORLD!' into the
+ 'environ' table:
+ ;INSERT INTO environ VALUES ('MSG', 'HELLO, WORLD!')
+
+
+
+OVER window-name
+══════════════════════════════════════════════════════════════════════
+ Executes the preceding function over a window
+Parameter
+ window-name The name of the window definition
+
+
+SELECT result-column1 [, ... result-columnN] [FROM table1 [, ... tableN]] [WHERE cond] [GROUP BY grouping-expr1 [, ... grouping-exprN]] [ORDER BY ordering-term1 [, ... ordering-termN]] [LIMIT limit-expr1 [, ... limit-exprN]]
+══════════════════════════════════════════════════════════════════════
+ Query the database and return zero or more rows of data.
+Parameters
+ result-column The expression used to generate a
+ result for this column.
+ table The table(s) to query for data
+ cond The conditions used to select the
+ rows to return.
+ grouping-expr The expression to use when grouping
+ rows.
+ ordering-term The values to use when ordering the
+ result set.
+ limit-expr The maximum number of rows to
+ return.
+
+Example
+#1 To select all of the columns from the table 'syslog_log':
+ ;SELECT * FROM syslog_log 
+
+
+
+UPDATE table SET column-name1 = expr1 [, ... column-nameN = exprN] [WHERE cond]
+══════════════════════════════════════════════════════════════════════
+ Modify a subset of values in zero or more rows of the given table
+Parameters
+ table The table to update
+ column-name The columns in the table to update.
+ cond The condition used to determine whether
+ a row should be updated.
+
+Example
+#1 To mark the syslog message at line 40:
+ ;UPDATE syslog_log SET log_mark = 1 WHERE log_line = 40
+
+
+
+WITH [RECURSIVE] cte-table-name AS select-stmt
+══════════════════════════════════════════════════════════════════════
+ Create a temporary view that exists only for the duration of a SQL
+ statement.
+Parameters
+ cte-table-name The name for the temporary table.
+ select-stmt The SELECT statement used to
+ populate the temporary table.
+
diff --git a/test/expected/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.err b/test/expected/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.err
diff --git a/test/expected/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.out b/test/expected/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.out
new file mode 100644
index 0000000..5fb4d90
--- /dev/null
+++ b/test/expected/test_cmds.sh_b755a8b48c0f602f0270500b0117b76e11db546e.out
@@ -0,0 +1,37 @@
+Apr 7 00:49:42 Tim-Stacks-iMac kernel[0]: Ethernet [AppleBCM5701Ethernet]: Link up on en0, 1-Gigabit, Full-duplex, Symmetric flow-control, Debug [
+ 796d,
+ 2301,
+ 0de1,
+ 0300,
+ cde1,
+ 3800
+]
+Apr 7 05:49:53 Tim-Stacks-iMac.local GoogleSoftwareUpdateDaemon[17212]: -[KSUpdateCheckAction performAction]
+ KSUpdateCheckAction running KSServerUpdateRequest: <KSOmahaServerUpdateRequest:0x511f30
+ server=
+<KSOmahaServer:0x510d80>
+ url="https://tools.google.com/service/update2"
+ runningFetchers=0
+ tickets=1
+ activeTickets=1
+ rollCallTickets=1
+ body=
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+ <o:gupdate xmlns:o="http://www.google.com/update2/request" protocol="2.0" version="KeystoneDaemon-1.2.0.7709" ismachine="1" requestid="{0DFDBCD1-5E29-4DFC-BD99-31A2397198FE}">
+ <o:os platform="mac" version="MacOSX" sp="10.10.2_x86_64h"></o:os>
+ <o:app appid="com.google.Keystone" version="1.2.0.7709" lang="en-us" installage="180" brand="GGLG">
+ <o:ping r="1" a="1"></o:ping>
+ <o:updatecheck></o:updatecheck>
+ </o:app>
+ </o:gupdate>
+ >
+Apr 7 07:31:56 Tim-Stacks-iMac.local VirtualBox[36403]: WARNING: The Gestalt selector gestaltSystemVersion is returning 10.9.2 instead of 10.10.2. Use NSProcessInfo's operatingSystemVersion property to get correct system version number.
+ Call location:
+Apr 7 07:31:56 Tim-Stacks-iMac.local VirtualBox[36403]: 0 CarbonCore 0x00007fff8a9b3d9b ___Gestalt_SystemVersion_block_invoke + 113
+Apr 7 07:31:56 Tim-Stacks-iMac.local VirtualBox[36403]: 1 libdispatch.dylib 0x00007fff8bc84c13 _dispatch_client_callout + 8
+Apr 7 07:32:56 Tim-Stacks-iMac.local logger[234]: Bad data {
+ abc,
+ 123,
+ 456
+)
+}]
diff --git a/test/expected/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.err b/test/expected/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.err
diff --git a/test/expected/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.out b/test/expected/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.out
new file mode 100644
index 0000000..51109ea
--- /dev/null
+++ b/test/expected/test_cmds.sh_b7fcd26c45c850c3d43ce25b1f610a311eb898c5.out
@@ -0,0 +1 @@
+Thu Jun 06 19:13:20 2013 +0000 UTC -- 1370546000
diff --git a/test/expected/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.err b/test/expected/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.err
diff --git a/test/expected/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.out b/test/expected/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.out
new file mode 100644
index 0000000..607eca0
--- /dev/null
+++ b/test/expected/test_cmds.sh_b9f8bf53ec2736432eb048d94a391175eb4dc5bf.out
@@ -0,0 +1,2 @@
+Feb 3 09:23:38 veridian automount[7998]: lookup(file): lookup for foobar failed
+Jan 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt
diff --git a/test/expected/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.err b/test/expected/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.err
diff --git a/test/expected/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.out b/test/expected/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.out
new file mode 100644
index 0000000..2678e6c
--- /dev/null
+++ b/test/expected/test_cmds.sh_bc60341827636715c14c562863da9733cbde7e68.out
@@ -0,0 +1 @@
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
diff --git a/test/expected/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.err b/test/expected/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.err
diff --git a/test/expected/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.out b/test/expected/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.out
new file mode 100644
index 0000000..44e294d
--- /dev/null
+++ b/test/expected/test_cmds.sh_be1d9628fc447b6f17121d9457ea1602afe8f3f3.out
@@ -0,0 +1 @@
+Sun Jan 31 05:53:29 2021 +0000 UTC -- 1612072409
diff --git a/test/expected/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.err b/test/expected/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.err
diff --git a/test/expected/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.out b/test/expected/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.out
new file mode 100644
index 0000000..493283c
--- /dev/null
+++ b/test/expected/test_cmds.sh_be3b7c5874b5f4d86cc230bd2f9802c98909e148.out
@@ -0,0 +1 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.err b/test/expected/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.err
new file mode 100644
index 0000000..07df1b2
--- /dev/null
+++ b/test/expected/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.err
@@ -0,0 +1,7 @@
+✘ error: invalid timestamp: 2022-06-16Tabc
+ reason: the leading part of the timestamp was matched, however, the trailing text “Tabc” was not
+ --> command-option:1
+ | :goto 2022-06-16Tabc 
+ |  ^--^ unrecognized input 
+ = note: input matched time format “%Y-%m-%d”
+ = help: fix the timestamp or remove the trailing text
diff --git a/test/expected/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.out b/test/expected/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_bf4e7fad67e281beaa11b6e2b03a00b419c7c9b0.out
diff --git a/test/expected/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.err b/test/expected/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.err
diff --git a/test/expected/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.out b/test/expected/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.out
new file mode 100644
index 0000000..705f4ab
--- /dev/null
+++ b/test/expected/test_cmds.sh_c01e10f7cae8d36fa79ae03be887cb5477025f6d.out
@@ -0,0 +1,5 @@
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: IOThunderboltSwitch<0xffffff803f4b3000>(0x0)::listenerCallback - Thunderbolt HPD packet for route = 0x0 port = 11 unplug = 0
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: vm_compressor_flush - starting
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: AppleBCM5701Ethernet [en0]: 0 0 memWrInd fBJP_Wakeup_Timer
+Sep 13 03:13:16 Tim-Stacks-iMac kernel[0]: AppleThunderboltNHIType2::waitForOk2Go2Sx - retries = 60000
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: hibernate_page_list_setall(preflight 0) start 0xffffff838f1fc000, 0xffffff838f2bc000
diff --git a/test/expected/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.err b/test/expected/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.err
new file mode 100644
index 0000000..8295272
--- /dev/null
+++ b/test/expected/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.err
@@ -0,0 +1,7 @@
+✘ error: invalid argument: invalid
+ reason: expecting line number/percentage, timestamp, or relative time
+ --> command-option:1
+ | :goto invalid 
+ = help: :goto line#|N%|timestamp|#anchor
+ ══════════════════════════════════════════════════════════════════════
+ Go to the given location in the top view
diff --git a/test/expected/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.out b/test/expected/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_c2b4431dd0cc36c6201d263b727b3305e8cda6b1.out
diff --git a/test/expected/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.err b/test/expected/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.err
diff --git a/test/expected/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.out b/test/expected/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.out
new file mode 100644
index 0000000..f2fef20
--- /dev/null
+++ b/test/expected/test_cmds.sh_c4777849c39a6c34dea5b0279cd7400692f1ab5f.out
@@ -0,0 +1,3 @@
+info: changed config option -- /ui/clock-format
+info: reset option -- /ui/clock-format
+/ui/clock-format = "%Y-%m-%dT%H:%M:%S %Z"
diff --git a/test/expected/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.err b/test/expected/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.err
diff --git a/test/expected/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.out b/test/expected/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.out
new file mode 100644
index 0000000..8ab686e
--- /dev/null
+++ b/test/expected/test_cmds.sh_c4a15771f7e1487bf73b2e9d1564ad8ecfd76c7e.out
@@ -0,0 +1 @@
+Hello, World!
diff --git a/test/expected/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.err b/test/expected/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.err
new file mode 100644
index 0000000..538a78e
--- /dev/null
+++ b/test/expected/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.err
@@ -0,0 +1,6 @@
+✘ error: invalid unix time -- abc
+ --> command-option:1
+ | :unix-time abc 
+ = help: :unix-time seconds
+ ══════════════════════════════════════════════════════════════════════
+ Convert epoch time to a human-readable form
diff --git a/test/expected/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.out b/test/expected/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_c72aed622c19d493968e33f20d5dde3838a4258f.out
diff --git a/test/expected/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.err b/test/expected/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.err
diff --git a/test/expected/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.out b/test/expected/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.out
new file mode 100644
index 0000000..76c53dd
--- /dev/null
+++ b/test/expected/test_cmds.sh_c7fabc25374ff47c47931f63b1d697061b816a28.out
@@ -0,0 +1,2 @@
+ Sat Nov 03 09:20:00 1 normal 2 errors  0 warnings  1 marks
+ Sat Nov 03 09:45:00 1 normal 0 errors 0 warnings 0 marks
diff --git a/test/expected/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.err b/test/expected/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.err
diff --git a/test/expected/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.out b/test/expected/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.out
new file mode 100644
index 0000000..6ae2e70
--- /dev/null
+++ b/test/expected/test_cmds.sh_ca66660c973f76a3c2a147c7f5035bcb4e8a8bbc.out
@@ -0,0 +1,2 @@
+2009-07-20 22:59:27,672:DEBUG:Hello, World!
+ How are you today?
diff --git a/test/expected/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.err b/test/expected/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.err
diff --git a/test/expected/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.out b/test/expected/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.out
new file mode 100644
index 0000000..bc67837
--- /dev/null
+++ b/test/expected/test_cmds.sh_ccd326da92d1cacda63501cd1a3077381a18e8f2.out
@@ -0,0 +1 @@
+ Sat Nov 03 00:00:00 2 normal 2 errors 0 warnings 0 marks
diff --git a/test/expected/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.err b/test/expected/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.err
diff --git a/test/expected/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.out b/test/expected/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_cmds.sh_d3b69abdfb39e4bfa5828c2f9593e2b2b7ed4d5d.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.err b/test/expected/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.err
diff --git a/test/expected/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.out b/test/expected/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_cmds.sh_d76d77ad95b9f120825417a6a8220c13df9541fc.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.err b/test/expected/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.err
new file mode 100644
index 0000000..07df1b2
--- /dev/null
+++ b/test/expected/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.err
@@ -0,0 +1,7 @@
+✘ error: invalid timestamp: 2022-06-16Tabc
+ reason: the leading part of the timestamp was matched, however, the trailing text “Tabc” was not
+ --> command-option:1
+ | :goto 2022-06-16Tabc 
+ |  ^--^ unrecognized input 
+ = note: input matched time format “%Y-%m-%d”
+ = help: fix the timestamp or remove the trailing text
diff --git a/test/expected/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.out b/test/expected/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_d7eebacdcf2cb194f25fa4ef97b7b5376b442467.out
diff --git a/test/expected/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.err b/test/expected/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.err
diff --git a/test/expected/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.out b/test/expected/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.out
new file mode 100644
index 0000000..1d3eae4
--- /dev/null
+++ b/test/expected/test_cmds.sh_d836c84398c831c976df46f46fe3bf5983c44c37.out
@@ -0,0 +1,2 @@
+log_top_line() 
+ 51
diff --git a/test/expected/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.err b/test/expected/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.err
diff --git a/test/expected/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.out b/test/expected/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.out
new file mode 100644
index 0000000..aec6735
--- /dev/null
+++ b/test/expected/test_cmds.sh_d8eeef53a58bdeddbc1028d7c525413e3ca1c8df.out
@@ -0,0 +1 @@
+/vmw/cgi/tramp 200
diff --git a/test/expected/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.err b/test/expected/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.err
diff --git a/test/expected/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.out b/test/expected/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.out
new file mode 100644
index 0000000..f3d0606
--- /dev/null
+++ b/test/expected/test_cmds.sh_dbdd62995fdefc8318053af05a32416eccfa79fc.out
@@ -0,0 +1 @@
+ Sat Nov 03 08:00:00 2 normal 2 errors 0 warnings 0 marks
diff --git a/test/expected/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.err b/test/expected/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.err
diff --git a/test/expected/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.out b/test/expected/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.out
new file mode 100644
index 0000000..c1837e7
--- /dev/null
+++ b/test/expected/test_cmds.sh_dd41fbbcd71699314af232156d4155fbdf849131.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [01/Jan/2010:00:00:00 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [01/Jan/2010:00:00:03 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [01/Jan/2010:00:00:03 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.err b/test/expected/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.err
diff --git a/test/expected/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.out b/test/expected/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.out
new file mode 100644
index 0000000..e905c55
--- /dev/null
+++ b/test/expected/test_cmds.sh_df6f4cea16bb8f20e6408fe4b40335e6de8a7f18.out
@@ -0,0 +1,3 @@
+10.0.0.1 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/aberrant HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+10.0.0.1 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/abject/ablaze/able.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+10.0.0.1 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/abject/ablaze/aboard.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.err b/test/expected/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.err
diff --git a/test/expected/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.out b/test/expected/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.out
new file mode 100644
index 0000000..2f2f721
--- /dev/null
+++ b/test/expected/test_cmds.sh_e495cf059477e3f80c3241c6f8d5808b6f1d19c7.out
@@ -0,0 +1,2 @@
+info: hiding lines after 2009-07-20 22:59:29.000
+info: hiding lines after 2009-07-20 22:59:29.000
diff --git a/test/expected/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.err b/test/expected/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.err
diff --git a/test/expected/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.out b/test/expected/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.out
new file mode 100644
index 0000000..21e4506
--- /dev/null
+++ b/test/expected/test_cmds.sh_e7e8244fac65bc51dbd5af31be476fe3b8776bfc.out
@@ -0,0 +1,12 @@
+{
+ "foo bar" : null,
+ "array" : [
+ 1,
+ 2,
+ 3
+ ],
+ "obj" : {
+ "one" : 1,
+  "two" : true
+ }
+}
diff --git a/test/expected/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.err b/test/expected/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.err
diff --git a/test/expected/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.out b/test/expected/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_cmds.sh_e911aebcb2defb7471aa620c45a86cad449ad505.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.err b/test/expected/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.err
diff --git a/test/expected/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.out b/test/expected/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.out
new file mode 100644
index 0000000..6599581
--- /dev/null
+++ b/test/expected/test_cmds.sh_eb22c3e94c536a1bfaeae0c40d271b5b4b08f4fc.out
@@ -0,0 +1,3 @@
+{"log_line":0,"log_part":null,"log_time":"2009-07-20 22:59:26.000","log_idle_msecs":0,"log_level":"info","log_mark":0,"log_comment":null,"log_tags":null,"log_filters":null,"c_ip":"192.168.202.254","cs_method":"GET","cs_referer":"-","cs_uri_query":null,"cs_uri_stem":"/vmw/cgi/tramp","cs_user_agent":"gPXE/0.9.7","cs_username":"-","cs_version":"HTTP/1.0","sc_bytes":134,"sc_status":200,"cs_host":null}
+{"log_line":1,"log_part":null,"log_time":"2009-07-20 22:59:29.000","log_idle_msecs":3000,"log_level":"error","log_mark":0,"log_comment":null,"log_tags":null,"log_filters":null,"c_ip":"192.168.202.254","cs_method":"GET","cs_referer":"-","cs_uri_query":null,"cs_uri_stem":"/vmw/vSphere/default/vmkboot.gz","cs_user_agent":"gPXE/0.9.7","cs_username":"-","cs_version":"HTTP/1.0","sc_bytes":46210,"sc_status":404,"cs_host":null}
+{"log_line":2,"log_part":null,"log_time":"2009-07-20 22:59:29.000","log_idle_msecs":0,"log_level":"info","log_mark":0,"log_comment":null,"log_tags":null,"log_filters":null,"c_ip":"192.168.202.254","cs_method":"GET","cs_referer":"-","cs_uri_query":null,"cs_uri_stem":"/vmw/vSphere/default/vmkernel.gz","cs_user_agent":"gPXE/0.9.7","cs_username":"-","cs_version":"HTTP/1.0","sc_bytes":78929,"sc_status":200,"cs_host":null}
diff --git a/test/expected/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.err b/test/expected/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.err
new file mode 100644
index 0000000..d9db72b
--- /dev/null
+++ b/test/expected/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.err
@@ -0,0 +1,6 @@
+✘ error: unknown field(s) -- foobar
+ --> command-option:1
+ | :hide-fields foobar 
+ = help: :hide-fields field-name1 [... field-nameN]
+ ══════════════════════════════════════════════════════════════════════
+ Hide log message fields by replacing them with an ellipsis
diff --git a/test/expected/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.out b/test/expected/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_ec2b28c6ea328e3ea56b13ab8ca3d9ee856a9dda.out
diff --git a/test/expected/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.err b/test/expected/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.err
new file mode 100644
index 0000000..929412e
--- /dev/null
+++ b/test/expected/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.err
@@ -0,0 +1,7 @@
+✘ error: invalid mark expression: :log_procname lik
+ reason: near "lik": syntax error
+ --> command-option:1
+ | :mark-expr :log_procname lik 
+ = help: :mark-expr expr
+ ══════════════════════════════════════════════════════════════════════
+ Set the bookmark expression
diff --git a/test/expected/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.out b/test/expected/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_ed5b73be0b991e0e8d6735e31df5b37c4286321b.out
diff --git a/test/expected/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.err b/test/expected/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.err
diff --git a/test/expected/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.out b/test/expected/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.out
new file mode 100644
index 0000000..f0aaf97
--- /dev/null
+++ b/test/expected/test_cmds.sh_f788d5f5932905d09ecbd581040ec5ce76459da5.out
@@ -0,0 +1,3 @@
+info: hiding lines before 2009-07-20 22:00:29.000
+info: hiding lines after 2009-07-20 22:59:29.000
+info: hiding lines before 2009-07-20 22:00:29.000 and after 2009-07-20 22:59:29.000
diff --git a/test/expected/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.err b/test/expected/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.err
diff --git a/test/expected/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.out b/test/expected/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.out
new file mode 100644
index 0000000..6ae2e70
--- /dev/null
+++ b/test/expected/test_cmds.sh_ff6faebbde8586e04bfadba14a3d2bb4451784ad.out
@@ -0,0 +1,2 @@
+2009-07-20 22:59:27,672:DEBUG:Hello, World!
+ How are you today?
diff --git a/test/expected/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.err b/test/expected/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.err
new file mode 100644
index 0000000..b1b0ffd
--- /dev/null
+++ b/test/expected/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.err
@@ -0,0 +1,6 @@
+✘ error: expecting an integer, found: abc
+ --> command-option:1
+ | :config /tuning/archive-manager/min-free-space abc
+ = help: :config option [value]
+ ══════════════════════════════════════════════════════════════════════
+ Read or write a configuration option
diff --git a/test/expected/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.out b/test/expected/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_config.sh_2765ea0d4c037b8c935840604edb0ae796c97a04.out
diff --git a/test/expected/test_config.sh_5105c29004e297521310ca0bd0fd560b01c2c549.err b/test/expected/test_config.sh_5105c29004e297521310ca0bd0fd560b01c2c549.err
new file mode 100644
index 0000000..092b26a
--- /dev/null
+++ b/test/expected/test_config.sh_5105c29004e297521310ca0bd0fd560b01c2c549.err
@@ -0,0 +1,20 @@
+✘ error: 'bad' is not a supported configuration $schema version
+ --> {test_dir}/bad-config2/formats/invalid-config/config.bad-schema.json:2
+ |  "$schema": "bad" 
+ = note: expecting one of the following $schema values:
+  https://lnav.org/schemas/config-v1.schema.json
+ = help: Property Synopsis
+ /$schema <schema-uri>
+ Description
+ The URI that specifies the schema that describes this type of file
+ Example
+ https://lnav.org/schemas/config-v1.schema.json
+✘ error: invalid JSON
+ --> {test_dir}/bad-config2/formats/invalid-config/config.malformed.json:3
+ | parse error: object key and value must be separated by a colon (':')
+ |  "ui": "theme", "abc", "def": "" }
+ |  (right here) ------^
+ | 
+✘ error: invalid JSON
+ reason: parse error: premature EOF
+ --> {test_dir}/bad-config2/formats/invalid-config/config.truncated.json:3
diff --git a/test/expected/test_config.sh_5105c29004e297521310ca0bd0fd560b01c2c549.out b/test/expected/test_config.sh_5105c29004e297521310ca0bd0fd560b01c2c549.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_config.sh_5105c29004e297521310ca0bd0fd560b01c2c549.out
diff --git a/test/expected/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.err b/test/expected/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.err
new file mode 100644
index 0000000..96307c5
--- /dev/null
+++ b/test/expected/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.err
@@ -0,0 +1,6 @@
+✘ error: unknown configuration option -- /bad/path
+ --> command-option:1
+ | :reset-config /bad/path 
+ = help: :reset-config option
+ ══════════════════════════════════════════════════════════════════════
+ Reset the configuration option to its default value
diff --git a/test/expected/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.out b/test/expected/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_config.sh_5fd9fbccc35e9b06abdd913da0c16bdb306b926e.out
diff --git a/test/expected/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.err b/test/expected/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.err
new file mode 100644
index 0000000..ec11ba5
--- /dev/null
+++ b/test/expected/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.err
@@ -0,0 +1,38 @@
+✘ error: 'bad' is not a supported configuration $schema version
+ --> {test_dir}/bad-config2/formats/invalid-config/config.bad-schema.json:2
+ |  "$schema": "bad" 
+ = note: expecting one of the following $schema values:
+  https://lnav.org/schemas/config-v1.schema.json
+ = help: Property Synopsis
+ /$schema <schema-uri>
+ Description
+ The URI that specifies the schema that describes this type of file
+ Example
+ https://lnav.org/schemas/config-v1.schema.json
+⚠ warning: unexpected value for property “/ui”
+ --> {test_dir}/bad-config2/formats/invalid-config/config.malformed.json:2
+ |  "ui": "theme", 
+ = help: Available Properties
+ $schema <schema-uri>
+ tuning/
+ ui/
+ log/
+ global/
+✘ error: invalid JSON
+ --> {test_dir}/bad-config2/formats/invalid-config/config.malformed.json:3
+ | parse error: object key and value must be separated by a colon (':')
+ |  "ui": "theme", "abc", "def": "" }
+ |  (right here) ------^
+ | 
+⚠ warning: unexpected value for property “/ui”
+ --> {test_dir}/bad-config2/formats/invalid-config/config.truncated.json:2
+ |  "ui": "theme" 
+ = help: Available Properties
+ $schema <schema-uri>
+ tuning/
+ ui/
+ log/
+ global/
+✘ error: invalid JSON
+ reason: parse error: premature EOF
+ --> {test_dir}/bad-config2/formats/invalid-config/config.truncated.json:3
diff --git a/test/expected/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.out b/test/expected/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_config.sh_a0907769aba112d628e7ebe39c4ec252e5e0bc69.out
diff --git a/test/expected/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.err b/test/expected/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.err
diff --git a/test/expected/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.out b/test/expected/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_config.sh_b08f7523659d1c12f0e59920cd40d17d4a83b72f.out
diff --git a/test/expected/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.err b/test/expected/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.err
new file mode 100644
index 0000000..d8b2b12
--- /dev/null
+++ b/test/expected/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.err
@@ -0,0 +1,12 @@
+✘ error: invalid value for property “/ui/theme-defs/default/styles/text/color”
+ reason: invalid color -- “#f”
+ |  reason: Could not parse color: #f
+ --> command-option:1
+ = help: Property Synopsis
+ /ui/theme-defs/default/styles/text/color #hex|color_name
+ Description
+ The foreground color value for this style. The value can be the name of an xterm color, the hexadecimal value, or a theme variable reference.
+ Examples
+ #fff
+ Green
+ $black
diff --git a/test/expected/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.out b/test/expected/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_config.sh_d622658dc98327b1b2fd346802d24bc633e34ac7.out
diff --git a/test/expected/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.err b/test/expected/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.err
new file mode 100644
index 0000000..93cba93
--- /dev/null
+++ b/test/expected/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.err
@@ -0,0 +1,8 @@
+✘ error: invalid value for property “/ui/theme”
+ reason: unknown theme -- “baddy”
+ |   = help: The available themes are: default, eldar, grayscale, monocai, night-owl, solarized-dark, solarized-light
+ --> command-option:1
+ = help: Property Synopsis
+ /ui/theme theme_name
+ Description
+ The name of the theme to use.
diff --git a/test/expected/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.out b/test/expected/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_config.sh_d708b6fd32d83ce0ee00ca5383388308ba5a06e1.out
diff --git a/test/expected/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.err b/test/expected/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.err
diff --git a/test/expected/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.out b/test/expected/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.out
new file mode 100644
index 0000000..8fafd97
--- /dev/null
+++ b/test/expected/test_config.sh_eec3768ebc201ca63bca1411270965f78db1abfc.out
@@ -0,0 +1 @@
+/global/foo = "foo"
diff --git a/test/expected/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.err b/test/expected/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.err
diff --git a/test/expected/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.out b/test/expected/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_events.sh_09ba47d70bfca88e89faf29598c1095292cad435.out
diff --git a/test/expected/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.err b/test/expected/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.err
diff --git a/test/expected/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.out b/test/expected/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.out
new file mode 100644
index 0000000..2ba3232
--- /dev/null
+++ b/test/expected/test_events.sh_153e221f3cb50f4d3e4581be0bf311e62489c42d.out
@@ -0,0 +1,6 @@
+/log/watch-expressions = {
+ "http-errors": {
+ "expr": ":sc_status >= 400",
+ "enabled": true
+ }
+}
diff --git a/test/expected/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.err b/test/expected/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.err
diff --git a/test/expected/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.out b/test/expected/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.out
new file mode 100644
index 0000000..b701c72
--- /dev/null
+++ b/test/expected/test_events.sh_3dae146ef3bf201c43656344803694a34a3dbfec.out
@@ -0,0 +1,2 @@
+{"content":{"$schema":"https://lnav.org/event-file-open-v1.schema.json","filename":"{test_dir}/logfile_access_log.0"}}
+{"content":{"$schema":"https://lnav.org/event-file-format-detected-v1.schema.json","filename":"{test_dir}/logfile_access_log.0","format":"access_log"}}
diff --git a/test/expected/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.err b/test/expected/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.err
diff --git a/test/expected/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.out b/test/expected/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_events.sh_6f9523d43f174397829b6a7fe6ee0090d97df5f9.out
diff --git a/test/expected/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.err b/test/expected/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.err
diff --git a/test/expected/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.out b/test/expected/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.out
new file mode 100644
index 0000000..019a01b
--- /dev/null
+++ b/test/expected/test_events.sh_729f77b8e7136d64d22a6610a80ba6b584a2d896.out
@@ -0,0 +1,3 @@
+/log/watch-expressions = {
+
+}
diff --git a/test/expected/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.err b/test/expected/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.err
new file mode 100644
index 0000000..0e6fb92
--- /dev/null
+++ b/test/expected/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.err
@@ -0,0 +1,10 @@
+✘ error: invalid value for property “/log/watch-expressions/http-errors/expr”
+ reason: SQL expression is invalid
+ |  reason: no such column: sc_status
+ |   --> /log/watch-expressions/http-errors/expr
+ |   | sc_status >= 400 AND bad 
+ --> command-option:1
+ = help: Property Synopsis
+ /log/watch-expressions/http-errors/expr <SQL-expression>
+ Description
+ The SQL expression to execute for each input line. If expression evaluates to true, a 'log message detected' event will be published.
diff --git a/test/expected/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.out b/test/expected/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_events.sh_d9c7907f907b2335e1328b23fdc46d0968a608d9.out
diff --git a/test/expected/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.err b/test/expected/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.err
diff --git a/test/expected/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.out b/test/expected/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.out
new file mode 100644
index 0000000..17a8132
--- /dev/null
+++ b/test/expected/test_events.sh_ed8dc44add223341c03ccb7b3e18371bdb42b710.out
@@ -0,0 +1,3 @@
+{"content":{"$schema":"https://lnav.org/event-file-open-v1.schema.json","filename":"{test_dir}/logfile_access_log.0"}}
+{"content":{"$schema":"https://lnav.org/event-file-format-detected-v1.schema.json","filename":"{test_dir}/logfile_access_log.0","format":"access_log"}}
+{"content":{"$schema":"https://lnav.org/event-log-msg-detected-v1.schema.json","watch-name":"http-errors","filename":"{test_dir}/logfile_access_log.0","line-number":1,"format":"access_log","timestamp":"2009-07-20T22:59:29.000","values":{"body":"","c_ip":"192.168.202.254","cs_method":"GET","cs_referer":"-","cs_uri_query":null,"cs_uri_stem":"/vmw/vSphere/default/vmkboot.gz","cs_user_agent":"gPXE/0.9.7","cs_username":"-","cs_version":"HTTP/1.0","sc_bytes":46210,"sc_status":404,"timestamp":"20/Jul/2009:22:59:29 +0000"}}}
diff --git a/test/expected/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.err b/test/expected/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.err
diff --git a/test/expected/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.out b/test/expected/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.out
new file mode 100644
index 0000000..5f62842
--- /dev/null
+++ b/test/expected/test_format_loader.sh_15e861d2327512a721fd42ae51dc5427689e0bb6.out
@@ -0,0 +1,9 @@
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters
+0,<NULL>,2016-06-30 12:00:01.000,0,trace,0,<NULL>,<NULL>,<NULL>
+1,<NULL>,2016-06-30 12:00:02.000,1000,debug,0,<NULL>,<NULL>,<NULL>
+2,<NULL>,2016-06-30 12:00:03.000,1000,debug2,0,<NULL>,<NULL>,<NULL>
+3,<NULL>,2016-06-30 12:00:04.000,1000,debug3,0,<NULL>,<NULL>,<NULL>
+4,<NULL>,2016-06-30 12:00:05.000,1000,info,0,<NULL>,<NULL>,<NULL>
+5,<NULL>,2016-06-30 12:00:06.000,1000,warning,0,<NULL>,<NULL>,<NULL>
+6,<NULL>,2016-06-30 12:00:07.000,1000,fatal,0,<NULL>,<NULL>,<NULL>
+7,<NULL>,2016-06-30 12:00:08.000,1000,info,0,<NULL>,<NULL>,<NULL>
diff --git a/test/expected/test_format_loader.sh_3f1d6f35e8a9ae4fd3e91ffaa82a037b5a847ab7.err b/test/expected/test_format_loader.sh_3f1d6f35e8a9ae4fd3e91ffaa82a037b5a847ab7.err
new file mode 100644
index 0000000..202b451
--- /dev/null
+++ b/test/expected/test_format_loader.sh_3f1d6f35e8a9ae4fd3e91ffaa82a037b5a847ab7.err
@@ -0,0 +1,171 @@
+✘ error: “invalid(abc” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /invalid_props_log/tags/badtag3/pattern
+ | invalid(abc 
+ |  ^ missing closing parenthesis
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:35
+ |  "pattern": "invalid(abc"
+ = help: Property Synopsis
+ /invalid_props_log/tags/badtag3/pattern <regex>
+ Description
+ The regular expression to match against the body of the log message
+ Example
+ \w+ is down
+✘ error: “abc(def” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /invalid_props_log/search-table/bad_table_regex/pattern
+ | abc(def 
+ |  ^ missing closing parenthesis 
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:40
+ |  "pattern": "abc(def" 
+ = help: Property Synopsis
+ /invalid_props_log/search-table/bad_table_regex/pattern <regex>
+ Description
+ The regular expression for this search table.
+✘ error: “^(?<timestamp>\d+: (?<body>.*)$” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /bad_regex_log/regex/std/pattern
+ | ^(?<timestamp>\d+: (?<body>.*)$ 
+ |  ^ missing closing parenthesis
+ --> {test_dir}/bad-config/formats/invalid-regex/format.json:6
+ |  "pattern": "^(?<timestamp>\\d+: (?<body>.*)$"
+ = help: Property Synopsis
+ /bad_regex_log/regex/std/pattern <message-regex>
+ Description
+ The regular expression to match a log message and capture fields.
+✘ error: “(foo” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> pattern
+ | (foo 
+ |  ^ missing closing parenthesis 
+ --> {test_dir}/bad-config/formats/invalid-regex/format.json:13
+ |  "error": "(foo" 
+ = help: Property Synopsis
+ /bad_regex_log/level/error <pattern|integer>
+ Description
+ The regular expression used to match the log text for this level. For JSON logs with numeric levels, this should be the number for the corresponding level.
+✘ error: “abc(” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /bad_regex_log/highlights/foobar/pattern
+ | abc( 
+ |  ^ missing closing parenthesis 
+ --> {test_dir}/bad-config/formats/invalid-regex/format.json:25
+ |  "pattern": "abc(" 
+ = help: Property Synopsis
+ /bad_regex_log/highlights/foobar/pattern <regex>
+ Description
+ A regular expression to highlight in logs of this format.
+✘ error: “foo” is not a valid value for option “/bad_sample_log/value/pid/kind”
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:24
+ |  "kind": "foo" 
+ = help: Property Synopsis
+ /bad_sample_log/value/pid/kind <data-type>
+ Description
+ The type of data in the field
+ Allowed Values
+ string, integer, float, boolean, json, struct, quoted, xml
+✘ error: 'bad' is not a supported log format $schema version
+ --> {test_dir}/bad-config/formats/invalid-schema/format.json:2
+ |  "$schema": "bad" 
+ = note: expecting one of the following $schema values:
+  https://lnav.org/schemas/format-v1.schema.json
+ = help: Property Synopsis
+ /$schema The URI of the schema for this file
+ Description
+ Specifies the type of this file
+✘ error: invalid pattern: “incomplete-match”
+ reason: pattern does not match entire message
+ --> {test_dir}/bad-config/formats/invalid-regex/format.json:20
+ | 1428634687123; foo 
+ |  ^ matched up to here 
+ = note: incomplete-match = ^(?<timestamp>\d+);
+ = help: update the regular expression to fully capture the sample message
+✘ error: invalid sample log message: "abc: foo"
+ reason: unrecognized timestamp -- abc
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:30
+ = note: the following custom formats were tried:
+ abc
+ ^ “%i” matched up to here
+ = help: If the timestamp format is not supported by default, you can add a custom format with the “timestamp-format” property
+✘ error: invalid sample log message: "1428634687123| debug hello"
+ reason: “debug” does not match the expected level of “info”
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:33
+ = note: matched regex = with-level
+ captured level = “debug”
+✘ error: invalid pattern: “with-level”
+ reason: pattern does not match entire multiline sample message
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:37
+ = note: with-level = ^(?<timestamp>\d+)\| (?<level>\w+) (?<body>\w+)$
+ = help: use “.*” to match new-lines
+✘ error: invalid sample log message: "1428634687123; foo bar"
+ reason: sample does not match any patterns
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:41
+ = note: the following shows how each pattern matched this sample:
+ 1428634687123; foo bar
+ ^ bad-time matched up to here
+ ^ semi matched up to here
+ ^ std matched up to here
+ ^ with-level matched up to here
+ = note: bad-time  = “^(?<timestamp>\w+): (?<body>\w+)$”
+ semi  = “^(?<timestamp>\d+); (?<body>\w+)$”
+ std  = “^(?<timestamp>\d+): (?<pid>\w+) (?<body>.*)$”
+ with-level = “^(?<timestamp>\d+)\| (?<level>\w+) (?<body>\w+)$”
+
+⚠ warning: invalid pattern: “/bad_sample_log/regex/semi”
+ reason: pattern does not match any samples
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:10
+ = help: every pattern should have at least one sample that it matches
+⚠ warning: invalid pattern: “/bad_sample_log/regex/std”
+ reason: pattern does not match any samples
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:7
+ = help: every pattern should have at least one sample that it matches
+⚠ warning: invalid value “/invalid_props_log/value/non-existent”
+ reason: no patterns have a capture named “non-existent”
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+ = note: the following captures are available:
+ body, pid, timestamp
+ = help: values are populated from captures in patterns, so at least one pattern must have a capture with this value name
+✘ error: invalid tag definition “/invalid_props_log/tags/badtag”
+ reason: tag definitions must have a non-empty pattern
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+✘ error: invalid tag definition “/invalid_props_log/tags/badtag2”
+ reason: tag definitions must have a non-empty pattern
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+✘ error: invalid tag definition “/invalid_props_log/tags/badtag3”
+ reason: tag definitions must have a non-empty pattern
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+✘ error: invalid value for property “/invalid_props_log/timestamp-field”
+ reason: “ts” was not found in the pattern at /invalid_props_log/regex/std
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+ = note: the following captures are available:
+ body, pid, timestamp
+✘ error: “not a color” is not a valid color value for property “/invalid_props_log/highlights/hl1/color”
+ reason: Unknown color: 'not a color'. See https://jonasjacek.github.io/colors/ for a list of supported color names
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:23
+✘ error: “also not a color” is not a valid color value for property “/invalid_props_log/highlights/hl1/background-color”
+ reason: Unknown color: 'also not a color'. See https://jonasjacek.github.io/colors/ for a list of supported color names
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:24
+✘ error: “no_regexes_log” is not a valid log format
+ reason: no regexes specified
+ --> {test_dir}/bad-config/formats/no-regexes/format.json:4
+✘ error: “no_regexes_log” is not a valid log format
+ reason: log message samples must be included in a format definition
+ --> {test_dir}/bad-config/formats/no-regexes/format.json:4
+✘ error: “no_sample_log” is not a valid log format
+ reason: log message samples must be included in a format definition
+ --> {test_dir}/bad-config/formats/no-samples/format.json:4
+✘ error: failed to compile SQL statement
+ reason: near "TALE": syntax error
+ --> {test_dir}/bad-config/formats/invalid-sql/init.sql:4
+ | -- comment test 
+ | CREATE TALE invalid (x y z); 
+ |  ^ near "TALE": syntax error 
+✘ error: failed to execute SQL statement
+ reason: ✘ error: “abc(” is not a valid regular expression
+ |  reason: missing closing parenthesis
+ |   --> arg
+ |   | abc( 
+ |   |  ^ missing closing parenthesis
+ --> {test_dir}/bad-config/formats/invalid-sql/init2.sql
+ | SELECT regexp_match('abc(', '123') 
+ | FROM sqlite_master; 
diff --git a/test/expected/test_format_loader.sh_3f1d6f35e8a9ae4fd3e91ffaa82a037b5a847ab7.out b/test/expected/test_format_loader.sh_3f1d6f35e8a9ae4fd3e91ffaa82a037b5a847ab7.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_format_loader.sh_3f1d6f35e8a9ae4fd3e91ffaa82a037b5a847ab7.out
diff --git a/test/expected/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.err b/test/expected/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.err
new file mode 100644
index 0000000..c3d7699
--- /dev/null
+++ b/test/expected/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.err
@@ -0,0 +1,215 @@
+✘ error: invalid value for option “/bad_json_log/line-format#/timestamp-format”
+ reason: empty values are not allowed
+ --> {test_dir}/bad-config/formats/invalid-json-format/format.json:11
+ |  "timestamp-format": "" 
+ = help: Property Synopsis
+ /bad_json_log/line-format#/timestamp-format <string>
+ Description
+ The strftime(3) format for this field
+⚠ warning: unexpected value for property “/bad-name-log/title”
+ --> {test_dir}/bad-config/formats/invalid-name/format.json:4
+ |  "title": "bad-format", 
+ = help: Available Properties
+ $schema The URI of the schema for this file
+ (\w+)/
+⚠ warning: unexpected value for property “/bad-name-log/description”
+ --> {test_dir}/bad-config/formats/invalid-name/format.json:5
+ |  "description": "Log format with a name that has invalid characters",
+ = help: Available Properties
+ $schema The URI of the schema for this file
+ (\w+)/
+⚠ warning: unexpected value for property “/bad-name-log/json”
+ --> {test_dir}/bad-config/formats/invalid-name/format.json:6
+ |  "json": true 
+ = help: Available Properties
+ $schema The URI of the schema for this file
+ (\w+)/
+✘ error: “invalid(abc” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /invalid_props_log/tags/badtag3/pattern
+ | invalid(abc 
+ |  ^ missing closing parenthesis
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:36
+ |  "pattern": "invalid(abc"
+ = help: Property Synopsis
+ /invalid_props_log/tags/badtag3/pattern <regex>
+ Description
+ The regular expression to match against the body of the log message
+ Example
+ \w+ is down
+✘ error: “abc(def” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /invalid_props_log/search-table/bad_table_regex/pattern
+ | abc(def 
+ |  ^ missing closing parenthesis 
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:41
+ |  "pattern": "abc(def" 
+ = help: Property Synopsis
+ /invalid_props_log/search-table/bad_table_regex/pattern <regex>
+ Description
+ The regular expression for this search table.
+✘ error: “^(?<timestamp>\d+: (?<body>.*)$” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /bad_regex_log/regex/std/pattern
+ | ^(?<timestamp>\d+: (?<body>.*)$ 
+ |  ^ missing closing parenthesis
+ --> {test_dir}/bad-config/formats/invalid-regex/format.json:6
+ |  "pattern": "^(?<timestamp>\\d+: (?<body>.*)$"
+ = help: Property Synopsis
+ /bad_regex_log/regex/std/pattern <message-regex>
+ Description
+ The regular expression to match a log message and capture fields.
+✘ error: “(foo” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> pattern
+ | (foo 
+ |  ^ missing closing parenthesis 
+ --> {test_dir}/bad-config/formats/invalid-regex/format.json:13
+ |  "error": "(foo" 
+ = help: Property Synopsis
+ /bad_regex_log/level/error <pattern|integer>
+ Description
+ The regular expression used to match the log text for this level. For JSON logs with numeric levels, this should be the number for the corresponding level.
+✘ error: “abc(” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /bad_regex_log/highlights/foobar/pattern
+ | abc( 
+ |  ^ missing closing parenthesis 
+ --> {test_dir}/bad-config/formats/invalid-regex/format.json:25
+ |  "pattern": "abc(" 
+ = help: Property Synopsis
+ /bad_regex_log/highlights/foobar/pattern <regex>
+ Description
+ A regular expression to highlight in logs of this format.
+⚠ warning: format file is missing “$schema” property
+ --> {test_dir}/bad-config/formats/invalid-regex/format.json
+ = note: the schema specifies the supported format version and can be used with editors to automatically validate the file
+ = help: add the following property to the top-level JSON object:
+  "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+✘ error: “foo” is not a valid value for option “/bad_sample_log/value/pid/kind”
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:24
+ |  "kind": "foo" 
+ = help: Property Synopsis
+ /bad_sample_log/value/pid/kind <data-type>
+ Description
+ The type of data in the field
+ Allowed Values
+ string, integer, float, boolean, json, struct, quoted, xml
+✘ error: 'bad' is not a supported log format $schema version
+ --> {test_dir}/bad-config/formats/invalid-schema/format.json:2
+ |  "$schema": "bad" 
+ = note: expecting one of the following $schema values:
+  https://lnav.org/schemas/format-v1.schema.json
+ = help: Property Synopsis
+ /$schema The URI of the schema for this file
+ Description
+ Specifies the type of this file
+✘ error: invalid line format element “/bad_json_log/line-format/0/field”
+ reason: “” is not a defined value
+ --> {test_dir}/bad-config/formats/invalid-json-format/format.json:7
+✘ error: invalid pattern: “incomplete-match”
+ reason: pattern does not match entire message
+ --> {test_dir}/bad-config/formats/invalid-regex/format.json:20
+ | 1428634687123; foo 
+ |  ^ matched up to here 
+ = note: incomplete-match = ^(?<timestamp>\d+);
+ = help: update the regular expression to fully capture the sample message
+✘ error: invalid sample log message: "abc: foo"
+ reason: unrecognized timestamp -- abc
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:34
+ = note: the following custom formats were tried:
+ abc
+ ^ “%i” matched up to here
+ = help: If the timestamp format is not supported by default, you can add a custom format with the “timestamp-format” property
+✘ error: invalid sample log message: "1428634687123| debug hello"
+ reason: “debug” does not match the expected level of “info”
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:37
+ = note: matched regex = with-level
+ captured level = “debug”
+ level regular expression match results:
+ /bad_sample_log/level/debug = debug
+ debug
+ ^---^
+ /bad_sample_log/level/info = info
+ no match
+ = help: Level regexes are not anchored to the start/end of the string. Prepend “^” to the expression to match from the start of the string and append “$” to match up to the end of the string.
+✘ error: invalid pattern: “with-level”
+ reason: pattern does not match entire multiline sample message
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:41
+ = note: with-level = ^(?<timestamp>\d+)\| (?<level>\w+) (?<body>\w+)$
+ = help: use “.*” to match new-lines
+✘ error: invalid sample log message: "1428634687123; foo bar"
+ reason: sample does not match any patterns
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:45
+ = note: the following shows how each pattern matched this sample:
+ 1428634687123; foo bar
+ ^ bad-time matched up to here
+ ^ semi matched up to here
+ ^ std matched up to here
+ ^ with-level matched up to here
+ = note: bad-time  = “^(?<timestamp>\w+): (?<body>\w+)$”
+ semi  = “^(?<timestamp>\d+); (?<body>\w+)$”
+ std  = “^(?<timestamp>\d+): (?<pid>\w+) (?<body>.*)$”
+ with-level = “^(?<timestamp>\d+)\| (?<level>\w+) (?<body>\w+)$”
+
+⚠ warning: invalid pattern: “/bad_sample_log/regex/semi”
+ reason: pattern does not match any samples
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:10
+ = help: every pattern should have at least one sample that it matches
+⚠ warning: invalid pattern: “/bad_sample_log/regex/std”
+ reason: pattern does not match any samples
+ --> {test_dir}/bad-config/formats/invalid-sample/format.json:7
+ = help: every pattern should have at least one sample that it matches
+⚠ warning: invalid value “/invalid_props_log/value/non-existent”
+ reason: no patterns have a capture named “non-existent”
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+ = note: the following captures are available:
+ body, pid, timestamp
+ = help: values are populated from captures in patterns, so at least one pattern must have a capture with this value name
+✘ error: invalid tag definition “/invalid_props_log/tags/badtag”
+ reason: tag definitions must have a non-empty pattern
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+✘ error: invalid tag definition “/invalid_props_log/tags/badtag2”
+ reason: tag definitions must have a non-empty pattern
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+✘ error: invalid tag definition “/invalid_props_log/tags/badtag3”
+ reason: tag definitions must have a non-empty pattern
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+✘ error: “invalid_props_log” is not a valid log format
+ reason: “subsecond-unit” must be set when “subsecond-field” is used
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+✘ error: invalid value for property “/invalid_props_log/timestamp-field”
+ reason: “ts” was not found in the pattern at /invalid_props_log/regex/std
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:4
+ = note: the following captures are available:
+ body, pid, timestamp
+✘ error: “not a color” is not a valid color value for property “/invalid_props_log/highlights/hl1/color”
+ reason: Unknown color: 'not a color'. See https://jonasjacek.github.io/colors/ for a list of supported color names
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:24
+✘ error: “also not a color” is not a valid color value for property “/invalid_props_log/highlights/hl1/background-color”
+ reason: Unknown color: 'also not a color'. See https://jonasjacek.github.io/colors/ for a list of supported color names
+ --> {test_dir}/bad-config/formats/invalid-properties/format.json:25
+✘ error: “no_regexes_log” is not a valid log format
+ reason: no regexes specified
+ --> {test_dir}/bad-config/formats/no-regexes/format.json:4
+✘ error: “no_regexes_log” is not a valid log format
+ reason: log message samples must be included in a format definition
+ --> {test_dir}/bad-config/formats/no-regexes/format.json:4
+✘ error: “no_sample_log” is not a valid log format
+ reason: log message samples must be included in a format definition
+ --> {test_dir}/bad-config/formats/no-samples/format.json:4
+✘ error: failed to compile SQL statement
+ reason: near "TALE": syntax error
+ --> {test_dir}/bad-config/formats/invalid-sql/init.sql:4
+ | -- comment test 
+ | CREATE TALE invalid (x y z); 
+ |  ^ near "TALE": syntax error 
+✘ error: failed to execute SQL statement
+ reason: ✘ error: “abc(” is not a valid regular expression
+ |  reason: missing closing parenthesis
+ |   --> arg
+ |   | abc( 
+ |   |  ^ missing closing parenthesis
+ --> {test_dir}/bad-config/formats/invalid-sql/init2.sql
+ | SELECT regexp_match('abc(', '123') 
+ | FROM sqlite_master; 
diff --git a/test/expected/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.out b/test/expected/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_format_loader.sh_5992e2695b7e6cf1f3520dbb87af8fc2b8f27088.out
diff --git a/test/expected/test_format_loader.sh_a47f2b090a5d8a226783835c7ff7d1c8821f11ed.err b/test/expected/test_format_loader.sh_a47f2b090a5d8a226783835c7ff7d1c8821f11ed.err
new file mode 100644
index 0000000..f657d87
--- /dev/null
+++ b/test/expected/test_format_loader.sh_a47f2b090a5d8a226783835c7ff7d1c8821f11ed.err
@@ -0,0 +1,61 @@
+✘ error: invalid JSON
+ --> {test_dir}/bad-config-json/formats/invalid-json/format.json:4
+ | parse error: object key and value must be separated by a colon (':')
+ |  ar_log": { "abc" } }
+ |  (right here) ------^
+ | 
+✘ error: “abc(” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /invalid_key_log/level-pointer
+ | abc( 
+ |  ^ missing closing parenthesis 
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:4
+ |  "level-pointer": "abc(", 
+ = help: Property Synopsis
+ /invalid_key_log/level-pointer
+ Description
+ A regular-expression that matches the JSON-pointer of the level property
+✘ error: “def[ghi” is not a valid regular expression
+ reason: missing terminating ] for character class
+ --> /invalid_key_log/file-pattern
+ | def[ghi 
+ |  ^ missing terminating ] for character class
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:5
+ |  "file-pattern": "def[ghi", 
+ = help: Property Synopsis
+ /invalid_key_log/file-pattern
+ Description
+ A regular expression that restricts this format to log files with a matching name
+⚠ warning: unexpected value for property “/invalid_key_log/value/test/identifiers”
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:14
+ |  "identifiers": true 
+ = help: Available Properties
+ kind <data-type>
+ collate <function>
+ unit/
+ identifier <bool>
+ foreign-key <bool>
+ hidden <bool>
+ action-list <string>
+ rewriter <command>
+ description <string>
+✘ error: “-1.2” is not a valid value for “/invalid_key_log/timestamp-divisor”
+ reason: value cannot be less than or equal to zero
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:25
+ |  "timestamp-divisor": -1.2 
+ = help: Property Synopsis
+ /invalid_key_log/timestamp-divisor <number>
+ Description
+ The value to divide a numeric timestamp by in a JSON log.
+✘ error: “foobar_log” is not a valid log format
+ reason: no regexes specified
+ --> {test_dir}/bad-config-json/formats/invalid-json/format.json:3
+✘ error: “foobar_log” is not a valid log format
+ reason: log message samples must be included in a format definition
+ --> {test_dir}/bad-config-json/formats/invalid-json/format.json:3
+✘ error: “invalid_key_log” is not a valid log format
+ reason: structured logs cannot have regexes
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:4
+✘ error: invalid line format element “/invalid_key_log/line-format/0/field”
+ reason: “non-existent” is not a defined value
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:22
diff --git a/test/expected/test_format_loader.sh_a47f2b090a5d8a226783835c7ff7d1c8821f11ed.out b/test/expected/test_format_loader.sh_a47f2b090a5d8a226783835c7ff7d1c8821f11ed.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_format_loader.sh_a47f2b090a5d8a226783835c7ff7d1c8821f11ed.out
diff --git a/test/expected/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.err b/test/expected/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.err
new file mode 100644
index 0000000..3272370
--- /dev/null
+++ b/test/expected/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.err
@@ -0,0 +1,66 @@
+✘ error: invalid JSON
+ --> {test_dir}/bad-config-json/formats/invalid-json/format.json:4
+ | parse error: object key and value must be separated by a colon (':')
+ |  ar_log": { "abc" } }
+ |  (right here) ------^
+ | 
+⚠ warning: format file is missing “$schema” property
+ --> {test_dir}/bad-config-json/formats/invalid-json/format.json
+ = note: the schema specifies the supported format version and can be used with editors to automatically validate the file
+ = help: add the following property to the top-level JSON object:
+  "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+✘ error: “abc(” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> /invalid_key_log/level-pointer
+ | abc( 
+ |  ^ missing closing parenthesis 
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:4
+ |  "level-pointer": "abc(", 
+ = help: Property Synopsis
+ /invalid_key_log/level-pointer
+ Description
+ A regular-expression that matches the JSON-pointer of the level property
+✘ error: “def[ghi” is not a valid regular expression
+ reason: missing terminating ] for character class
+ --> /invalid_key_log/file-pattern
+ | def[ghi 
+ |  ^ missing terminating ] for character class
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:5
+ |  "file-pattern": "def[ghi", 
+ = help: Property Synopsis
+ /invalid_key_log/file-pattern
+ Description
+ A regular expression that restricts this format to log files with a matching name
+⚠ warning: unexpected value for property “/invalid_key_log/value/test/identifiers”
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:14
+ |  "identifiers": true 
+ = help: Available Properties
+ kind <data-type>
+ collate <function>
+ unit/
+ identifier <bool>
+ foreign-key <bool>
+ hidden <bool>
+ action-list <string>
+ rewriter <command>
+ description <string>
+✘ error: “-1.2” is not a valid value for “/invalid_key_log/timestamp-divisor”
+ reason: value cannot be less than or equal to zero
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:25
+ |  "timestamp-divisor": -1.2 
+ = help: Property Synopsis
+ /invalid_key_log/timestamp-divisor <number>
+ Description
+ The value to divide a numeric timestamp by in a JSON log.
+✘ error: “foobar_log” is not a valid log format
+ reason: no regexes specified
+ --> {test_dir}/bad-config-json/formats/invalid-json/format.json:3
+✘ error: “foobar_log” is not a valid log format
+ reason: log message samples must be included in a format definition
+ --> {test_dir}/bad-config-json/formats/invalid-json/format.json:3
+✘ error: “invalid_key_log” is not a valid log format
+ reason: structured logs cannot have regexes
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:4
+✘ error: invalid line format element “/invalid_key_log/line-format/0/field”
+ reason: “non-existent” is not a defined value
+ --> {test_dir}/bad-config-json/formats/invalid-key/format.json:22
diff --git a/test/expected/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.out b/test/expected/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_format_loader.sh_fca6c1fb9f3aaa69b3ffb2d1a8a86434b2f4a247.out
diff --git a/test/expected/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.err b/test/expected/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.err
diff --git a/test/expected/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.out b/test/expected/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.out
new file mode 100644
index 0000000..55657f7
--- /dev/null
+++ b/test/expected/test_json_format.sh_168cac40c27f547044c89d39eb0ff2ef81da4b21.out
@@ -0,0 +1,13 @@
+{"ts": "2013-09-06T20:00:48.124817Z", "lvl": "TRACE", "msg": "trace test"}
+{"ts": "2013-09-06T20:00:49.124817Z", "lvl": "INFO", "msg": "Starting up \u001B[0;32mservice\u001B[0m"}
+{"ts": "2013-09-06T22:00:49.124817Z", "lvl": "INFO", "msg": "Shutting down service", "user": "steve@example.com"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG5", "msg": "Details...\n"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG4", "msg": "Details...\n"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG3", "msg": "Details...\n"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG2", "msg": "Details...\n"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG", "msg": "Details..."}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "STATS", "msg": "1 beat per second"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "WARNING", "msg": "not looking good"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "ERROR", "msg": "looking bad"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "CRITICAL", "msg": "sooo bad"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "FATAL", "msg": "shoot", "obj": { "field1" : "hi", "field2": 2 }, "arr" : ["hi", {"sub1": true}]}
diff --git a/test/expected/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.err b/test/expected/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.err
diff --git a/test/expected/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.out b/test/expected/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.out
new file mode 100644
index 0000000..59872f2
--- /dev/null
+++ b/test/expected/test_json_format.sh_1bb0fd243e916546aea22029245ac590dae17a86.out
@@ -0,0 +1,14 @@
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,@fields/user,@fields/trace#
+0,<NULL>,2013-09-06 20:00:48.124,0,trace,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+2,<NULL>,2013-09-06 20:00:49.124,1000,info,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+4,<NULL>,2013-09-06 22:00:49.124,7200000,info,0,<NULL>,<NULL>,<NULL>,steve@example.com,<NULL>
+7,<NULL>,2013-09-06 22:00:59.124,10000,debug5,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+9,<NULL>,2013-09-06 22:00:59.124,0,debug4,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+11,<NULL>,2013-09-06 22:00:59.124,0,debug3,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+13,<NULL>,2013-09-06 22:00:59.124,0,debug2,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+15,<NULL>,2013-09-06 22:00:59.124,0,debug,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+17,<NULL>,2013-09-06 22:01:49.124,50000,stats,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+19,<NULL>,2013-09-06 22:01:49.124,0,warning,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+21,<NULL>,2013-09-06 22:01:49.124,0,error,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+23,<NULL>,2013-09-06 22:01:49.124,0,critical,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>
+25,<NULL>,2013-09-06 22:01:49.124,0,fatal,0,<NULL>,<NULL>,<NULL>,<NULL>,line:1
diff --git a/test/expected/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.err b/test/expected/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.err
diff --git a/test/expected/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.out b/test/expected/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.out
new file mode 100644
index 0000000..0dc139f
--- /dev/null
+++ b/test/expected/test_json_format.sh_40223ac4742883f883ccc61044bfffd6e102cca6.out
@@ -0,0 +1,205 @@
+[
+ {
+ "log_line": 0,
+ "log_part": null,
+ "log_time": "2013-09-06 20:00:48.124",
+ "log_idle_msecs": 0,
+ "log_level": "trace",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "TRACE",
+ "user": null
+ },
+ {
+ "log_line": 2,
+ "log_part": null,
+ "log_time": "2013-09-06 20:00:49.124",
+ "log_idle_msecs": 1000,
+ "log_level": "info",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "INFO",
+ "user": null
+ },
+ {
+ "log_line": 4,
+ "log_part": null,
+ "log_time": "2013-09-06 22:00:49.124",
+ "log_idle_msecs": 7200000,
+ "log_level": "info",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "INFO",
+ "user": "steve@example.com"
+ },
+ {
+ "log_line": 7,
+ "log_part": null,
+ "log_time": "2013-09-06 22:00:59.124",
+ "log_idle_msecs": 10000,
+ "log_level": "debug5",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "DEBUG5",
+ "user": null
+ },
+ {
+ "log_line": 9,
+ "log_part": null,
+ "log_time": "2013-09-06 22:00:59.124",
+ "log_idle_msecs": 0,
+ "log_level": "debug4",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "DEBUG4",
+ "user": null
+ },
+ {
+ "log_line": 11,
+ "log_part": null,
+ "log_time": "2013-09-06 22:00:59.124",
+ "log_idle_msecs": 0,
+ "log_level": "debug3",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "DEBUG3",
+ "user": null
+ },
+ {
+ "log_line": 13,
+ "log_part": null,
+ "log_time": "2013-09-06 22:00:59.124",
+ "log_idle_msecs": 0,
+ "log_level": "debug2",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "DEBUG2",
+ "user": null
+ },
+ {
+ "log_line": 15,
+ "log_part": null,
+ "log_time": "2013-09-06 22:00:59.124",
+ "log_idle_msecs": 0,
+ "log_level": "debug",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "DEBUG",
+ "user": null
+ },
+ {
+ "log_line": 17,
+ "log_part": null,
+ "log_time": "2013-09-06 22:01:49.124",
+ "log_idle_msecs": 50000,
+ "log_level": "stats",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "STATS",
+ "user": null
+ },
+ {
+ "log_line": 19,
+ "log_part": null,
+ "log_time": "2013-09-06 22:01:49.124",
+ "log_idle_msecs": 0,
+ "log_level": "warning",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "WARNING",
+ "user": null
+ },
+ {
+ "log_line": 21,
+ "log_part": null,
+ "log_time": "2013-09-06 22:01:49.124",
+ "log_idle_msecs": 0,
+ "log_level": "error",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "ERROR",
+ "user": null
+ },
+ {
+ "log_line": 23,
+ "log_part": null,
+ "log_time": "2013-09-06 22:01:49.124",
+ "log_idle_msecs": 0,
+ "log_level": "critical",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": null,
+ "obj": null,
+ "lvl": "CRITICAL",
+ "user": null
+ },
+ {
+ "log_line": 25,
+ "log_part": null,
+ "log_time": "2013-09-06 22:01:49.124",
+ "log_idle_msecs": 0,
+ "log_level": "fatal",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "arr": [
+ "hi",
+ {
+ "sub1": true
+ }
+ ],
+ "obj": {
+ "field1": "hi",
+ "field2": 2
+ },
+ "lvl": "FATAL",
+ "user": null
+ }
+]
diff --git a/test/expected/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.err b/test/expected/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.err
diff --git a/test/expected/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.out b/test/expected/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.out
new file mode 100644
index 0000000..9b7fbf1
--- /dev/null
+++ b/test/expected/test_json_format.sh_4315a3d6124c14cbe3c474b6dbf4cc8720a9859f.out
@@ -0,0 +1,3 @@
+2017-03-24T20:06:26.240 1.1.1.1 GET 200 443 /example/uri/5
+2017-03-24T20:12:47.764 1.1.1.1 GET 500 4433 /example/uri/5
+2017-03-24T20:15:31.694 1.1.1.1 GET 400 44345 /example/uri/5
diff --git a/test/expected/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.err b/test/expected/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.err
diff --git a/test/expected/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.out b/test/expected/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.out
new file mode 100644
index 0000000..ee41abb
--- /dev/null
+++ b/test/expected/test_json_format.sh_469f005b0708d629bc95f0c48a5e390f440c1fef.out
@@ -0,0 +1,29 @@
+
+[2013-09-06T20:00:48.124] TRACE trace test
+
+[2013-09-06T20:00:49.124] INFO Starting up service
+
+[2013-09-06T22:00:49.124] INFO Shutting down service
+ user: steve@example.com
+
+[2013-09-06T22:00:59.124] DEBUG5 Details...
+
+[2013-09-06T22:00:59.124] DEBUG4 Details...
+
+[2013-09-06T22:00:59.124] DEBUG3 Details...
+
+[2013-09-06T22:00:59.124] DEBUG2 Details...
+
+[2013-09-06T22:00:59.124] DEBUG Details...
+
+[2013-09-06T22:01:49.124] STATS 1 beat per second
+
+[2013-09-06T22:01:49.124] WARNING not looking good
+
+[2013-09-06T22:01:49.124] ERROR looking bad
+
+[2013-09-06T22:01:49.124] CRITICAL sooo bad
+
+[2013-09-06T22:01:49.124] FATAL shoot
+ obj: { "field1" : "hi", "field2": 2 }
+ arr: ["hi", {"sub1": true}]
diff --git a/test/expected/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.err b/test/expected/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.err
diff --git a/test/expected/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.out b/test/expected/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.out
new file mode 100644
index 0000000..19672db
--- /dev/null
+++ b/test/expected/test_json_format.sh_6767b91d715338c24c67e928b59c560c84ddf4be.out
@@ -0,0 +1,42 @@
+2023-03-24T14:26:16.243 renovate[7] DEBUG Found gitlabci-include package files
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+2023-03-24T14:26:16.243 renovate[7] DEBUG Found npm package files
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+2023-03-24T14:26:16.243 renovate[7] DEBUG [/Users/trentm/tm/node-bunyan/examples/src.js:20:Wuzzle.woos] Found 3 package file(s)
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+2023-03-24T14:26:16.243 renovate[7] INFO Dependency extraction complete
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+ baseBranch: main
+ stats: {"managers":{"gitlabci":{"fileCount":1,"depCount":1},"gitlabci-include":{"fileCount":1,"depCount":1},"npm":{"fileCount":1,"depCount":15}},"total":{"fileCount":3,"depCount":17}}
+2023-03-24T14:26:16.390 renovate[7] DEBUG Dependency node has unsupported/unversioned value lts-bullseye-slim (versioning=docker)
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+2023-03-24T14:26:17.493 renovate[7] DEBUG Release 2.8.7 is pending status checks
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+ depName: prettier
+ check: stabilityDays
+2023-03-24T14:26:17.897 renovate[7] DEBUG Release 4.4.1 is pending status checks
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+ depName: rimraf
+ check: stabilityDays
+2023-03-24T14:26:17.897 renovate[7] DEBUG All releases are pending - using latest
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+ depName: rimraf
+ bucket: non-major
+2023-03-24T14:26:18.330 renovate[7] DEBUG Release 2.10.0 is pending status checks
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+ depName: prettier-plugin-svelte
+ check: stabilityDays
+2023-03-24T14:26:18.331 renovate[7] DEBUG All releases are pending - using latest
+ logContext: qjifsaDDI
+ repository: webgui/custom-icons-transformer
+ depName: prettier-plugin-svelte
+ bucket: non-major
diff --git a/test/expected/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.err b/test/expected/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.err
diff --git a/test/expected/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.out b/test/expected/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.out
new file mode 100644
index 0000000..128f5ab
--- /dev/null
+++ b/test/expected/test_json_format.sh_6fbe20faa161ab9fa77df7568fff84bf3e47e920.out
@@ -0,0 +1,4 @@
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,user,cl
+0,<NULL>,2013-09-06 20:00:49.124,0,info,0,<NULL>,<NULL>,<NULL>,<NULL>,com.exmaple.foo
+1,<NULL>,2013-09-06 22:00:49.124,7200000,info,0,<NULL>,<NULL>,<NULL>,steve@example.com,com.exmaple.foo
+3,<NULL>,2013-09-06 22:01:49.124,60000,error,0,<NULL>,<NULL>,<NULL>,<NULL>,com.exmaple.foo
diff --git a/test/expected/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.err b/test/expected/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.err
diff --git a/test/expected/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.out b/test/expected/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.out
new file mode 100644
index 0000000..c861d3a
--- /dev/null
+++ b/test/expected/test_json_format.sh_7724d1a96d74d4418dd44d7416270f9bb64b2564.out
@@ -0,0 +1,29 @@
+2013-09-06T20:00:48.124 TRACE trace test
+ @fields: { "lvl": "TRACE", "msg": "trace test"}
+2013-09-06T20:00:49.124 INFO Starting up service
+ @fields: { "lvl": "INFO", "msg": "Starting up service"}
+2013-09-06T22:00:49.124 INFO Shutting down service
+ @fields/user: steve@example.com
+ @fields: { "lvl": "INFO", "msg": "Shutting down service", "user": "steve@example.com"}
+2013-09-06T22:00:59.124 DEBUG5 Details...
+ @fields: { "lvl": "DEBUG5", "msg": "Details..."}
+2013-09-06T22:00:59.124 DEBUG4 Details...
+ @fields: { "lvl": "DEBUG4", "msg": "Details..."}
+2013-09-06T22:00:59.124 DEBUG3 Details...
+ @fields: { "lvl": "DEBUG3", "msg": "Details..."}
+2013-09-06T22:00:59.124 DEBUG2 Details...
+ @fields: { "lvl": "DEBUG2", "msg": "Details..."}
+2013-09-06T22:00:59.124 DEBUG Details...
+ @fields: { "lvl": "DEBUG", "msg": "Details..."}
+2013-09-06T22:01:49.124 STATS 1 beat per second
+ @fields: { "lvl": "STATS", "msg": "1 beat per second"}
+2013-09-06T22:01:49.124 WARNING not looking good
+ @fields: { "lvl": "WARNING", "msg": "not looking good"}
+2013-09-06T22:01:49.124 ERROR looking bad
+ @fields: { "lvl": "ERROR", "msg": "looking bad"}
+2013-09-06T22:01:49.124 CRITICAL sooo bad
+ @fields: { "lvl": "CRITICAL", "msg": "sooo bad"}
+2013-09-06T22:01:49.124 FATAL shoot
+ @fields/trace#: line:1
+ @fields/trace#: line:2
+ @fields: { "lvl": "FATAL", "msg": "shoot", "trace": ["line:1", "line:2"]}
diff --git a/test/expected/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.err b/test/expected/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.err
diff --git a/test/expected/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.out b/test/expected/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.out
new file mode 100644
index 0000000..3b820b6
--- /dev/null
+++ b/test/expected/test_json_format.sh_7aade92cff911c5b3cfc733685809f949ae35778.out
@@ -0,0 +1 @@
+{"@timestamp":"2016-08-03T12:06:31.009-0500","@version":1,"message":";Exception initializing page context;","logger_name":"org.apache.jasper.runtime.JspFactoryImpl","thread_name":"http-bio-0.0.0.0-8081-exec-198","level":"ERROR","level_value":40000,"stack_trace":"java.lang.NoClassDefFoundError: javax/el/StaticFieldELResolver\n\tat org.apache.jasper.runtime.JspFactoryImpl.internalGetPageContext(JspFactoryImpl.java:172)\n\tat org.apache.jasper.runtime.JspFactoryImpl.getPageContext(JspFactoryImpl.java:123)\n\tat org.apache.jsp.errors._404_002dnot_002dfound_jsp._jspService(_404_002dnot_002dfound_jsp.java:38)\n\tat org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:111)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:731)\n\tat org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:411)\n\tat org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:473)\n\tat org.apache.jasper.servlet.JspServlet.service(JspServlet.java:377)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:731)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)\n\tat collective.config.startup.DamFilter.doFilter(DamFilter.java:270)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)\n\tat org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:748)\n\tat org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:488)\n\tat org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:411)\n\tat org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:338)\n\tat org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:476)\n\tat org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:345)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:210)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)\n\tat org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:957)\n\tat org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:683)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423)\n\tat org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079)\n\tat org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:620)\n\tat org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.lang.Thread.run(Thread.java:744)\nCaused by: java.lang.ClassNotFoundException: javax.el.StaticFieldELResolver\n\tat org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1720)\n\tat org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1571)\n\t... 33 common frames omitted\n","customer":"foobaz"}
diff --git a/test/expected/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.err b/test/expected/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.err
diff --git a/test/expected/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.out b/test/expected/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.out
new file mode 100644
index 0000000..ce295e2
--- /dev/null
+++ b/test/expected/test_json_format.sh_7c6529f6bf4a0cb565f5665fdcba032f0ae1ebbe.out
@@ -0,0 +1,12 @@
+2013-09-06T20:00:48.124 TRACE trace test
+ @fields: { "lvl": "TRACE", "msg": "trace test"}
+2013-09-06T20:00:49.124 INFO Starting up service
+ @fields: { "lvl": "INFO", "msg": "Starting up service"}
+[offset: 186] {"ts": "2013-09-06T22:00:49.124817Z", "@fields": { "lvl": "INFO", "msg": "Shutting down service\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10
+parse error: premature EOF
+ {"ts": "2013-09-06T22:00:49.124
+ (right here) ------^
+2013-09-06T22:00:59.124 DEBUG5 Details...
+ @fields: { "lvl": "DEBUG5", "msg": "Details..."}
+2013-09-06T22:00:59.222 DEBUG4 Details...
+ @fields: { "lvl": "DEBUG4", "msg": "Details..."}
diff --git a/test/expected/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.err b/test/expected/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.err
diff --git a/test/expected/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.out b/test/expected/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.out
new file mode 100644
index 0000000..63376a4
--- /dev/null
+++ b/test/expected/test_json_format.sh_80959e2bb6a7fdf938c2e4dbd7d7c81eb84fa072.out
@@ -0,0 +1,8 @@
+2013-09-06T20:00:48.124 TRACE trace test
+ @fields: { "lvl": "TRACE", "msg": "trace test"}
+2013-09-06T20:00:49.124 INFO Starting up service
+ @fields: { "lvl": "INFO", "msg": "Starting up service"}
+[offset: 186] {"ts": "2013-09-06T22:00:49.124817Z", "@fields": { "lvl": "INFO", "msg":
+parse error: premature EOF
+ {"ts": "2013-09-06T22:00:49.124
+ (right here) ------^
diff --git a/test/expected/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.err b/test/expected/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.err
diff --git a/test/expected/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.out b/test/expected/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.out
new file mode 100644
index 0000000..58b44ab
--- /dev/null
+++ b/test/expected/test_json_format.sh_84a71e94dc34661a70bb9015b67ba00e93e9cfb5.out
@@ -0,0 +1,2 @@
+2018-08-21T14:04:21.221 38708007 medusa-GpsLocator.service python[184] FATAL GPS Reference longitude: 7.358143333
+2018-08-21T14:04:21.221 38708007 medusa-GpsLocator.service python[184] INFO GPS Reference latitude: 46.908706667
diff --git a/test/expected/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.err b/test/expected/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.err
diff --git a/test/expected/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.out b/test/expected/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.out
new file mode 100644
index 0000000..660e90e
--- /dev/null
+++ b/test/expected/test_json_format.sh_85d03b1b41a7f819af135d2521a8f2c59418e907.out
@@ -0,0 +1,14 @@
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,arr,obj,lvl,user
+0,<NULL>,2013-09-06 20:00:48.124,0,trace,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,TRACE,<NULL>
+2,<NULL>,2013-09-06 20:00:49.124,1000,info,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,INFO,<NULL>
+4,<NULL>,2013-09-06 22:00:49.124,7200000,info,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,INFO,steve@example.com
+7,<NULL>,2013-09-06 22:00:59.124,10000,debug5,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,DEBUG5,<NULL>
+9,<NULL>,2013-09-06 22:00:59.124,0,debug4,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,DEBUG4,<NULL>
+11,<NULL>,2013-09-06 22:00:59.124,0,debug3,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,DEBUG3,<NULL>
+13,<NULL>,2013-09-06 22:00:59.124,0,debug2,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,DEBUG2,<NULL>
+15,<NULL>,2013-09-06 22:00:59.124,0,debug,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,DEBUG,<NULL>
+17,<NULL>,2013-09-06 22:01:49.124,50000,stats,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,STATS,<NULL>
+19,<NULL>,2013-09-06 22:01:49.124,0,warning,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,WARNING,<NULL>
+21,<NULL>,2013-09-06 22:01:49.124,0,error,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,ERROR,<NULL>
+23,<NULL>,2013-09-06 22:01:49.124,0,critical,0,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,CRITICAL,<NULL>
+25,<NULL>,2013-09-06 22:01:49.124,0,fatal,0,<NULL>,<NULL>,<NULL>,"[""hi"", {""sub1"": true}]","{ ""field1"" : ""hi"", ""field2"": 2 }",FATAL,<NULL>
diff --git a/test/expected/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.err b/test/expected/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.err
diff --git a/test/expected/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.out b/test/expected/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.out
new file mode 100644
index 0000000..caee6dd
--- /dev/null
+++ b/test/expected/test_json_format.sh_8f2ebcd319afc7966ef11e31f9dd646bf6f001dd.out
@@ -0,0 +1,21 @@
+⋮ abc 48 def TRACE - trace test
+⋮ abc 49 def INFO - Starting up service
+⋮ abc 49 def INFO - Shutting down service
+ user: steve@example.com
+timestamp="2013-09-06T22:00:50.123000Z" level="INFO" msg="Hello, World"
+panic: foo bar failed baz
+ level1.py:10034
+ level2.py:100
+ level3.py:42
+⋮ abc 59 def DEBUG5 - Details...
+⋮ abc 59 def DEBUG4 - Details...
+⋮ abc 59 def DEBUG3 - Details...
+⋮ abc 59 def DEBUG2 - Details...
+⋮ abc 59 def DEBUG - Details...
+⋮ abc 49 def STATS - 1 beat per second
+⋮ abc 49 def WARNING - not looking good
+⋮ abc 49 def ERROR - looking bad
+⋮ abc 49 def CRITICAL - sooo bad
+⋮ abc 49 def FATAL - shoot
+ obj: { "field1" : "hi", "field2": 2 }
+ arr: ["hi", {"sub1": true}]
diff --git a/test/expected/test_json_format.sh_90a037c7d9d70ac4ca97158271ea242787313377.err b/test/expected/test_json_format.sh_90a037c7d9d70ac4ca97158271ea242787313377.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_90a037c7d9d70ac4ca97158271ea242787313377.err
diff --git a/test/expected/test_json_format.sh_90a037c7d9d70ac4ca97158271ea242787313377.out b/test/expected/test_json_format.sh_90a037c7d9d70ac4ca97158271ea242787313377.out
new file mode 100644
index 0000000..30ddacc
--- /dev/null
+++ b/test/expected/test_json_format.sh_90a037c7d9d70ac4ca97158271ea242787313377.out
@@ -0,0 +1,3 @@
+2017-03-24T20:06:26.240 1.1.1.1 GET 200 /example/uri/5
+2017-03-24T20:12:47.764 1.1.1.1 GET 500 /example/uri/5
+2017-03-24T20:15:31.694 1.1.1.1 GET 400 /example/uri/5
diff --git a/test/expected/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.err b/test/expected/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.err
diff --git a/test/expected/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.out b/test/expected/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.out
new file mode 100644
index 0000000..7cb7336
--- /dev/null
+++ b/test/expected/test_json_format.sh_952297a90e312d2184fe3e4df795ddc731b096c9.out
@@ -0,0 +1,4 @@
+
+[-09-06T22:00:49.124] INFO Shutting down service
+ user: steve@example.com
+
diff --git a/test/expected/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.err b/test/expected/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.err
diff --git a/test/expected/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.out b/test/expected/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.out
new file mode 100644
index 0000000..2f9e187
--- /dev/null
+++ b/test/expected/test_json_format.sh_989e52d167582648b73c5d025cc0e814c642b3c8.out
@@ -0,0 +1,4 @@
+⋮ abc 49 def 0 c.e.foo Starting up service
+⋮ abc 49 def 0 c.e.foo Shutting down service
+ user: steve@example.com
+⋮ abc 49 def 10 c.e.foo looking bad
diff --git a/test/expected/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.err b/test/expected/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.err
diff --git a/test/expected/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.out b/test/expected/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.out
new file mode 100644
index 0000000..c3b21be
--- /dev/null
+++ b/test/expected/test_json_format.sh_a06b3cdd46b387e72d6faa4cce648b8b11ae870b.out
@@ -0,0 +1,29 @@
+
+[2013-09-06T20:00:48.124] ⋮ trace testbork bork bork
+
+[2013-09-06T20:00:49.124] ⋮ Starting up servicebork bork bork
+
+[2013-09-06T22:00:49.124] ⋮ Shutting down servicebork bork bork
+ user: mailto:steve@example.com
+
+[2013-09-06T22:00:59.124] ⋮ Details...
+bork bork bork
+
+[2013-09-06T22:00:59.124] ⋮ Details...
+bork bork bork
+
+[2013-09-06T22:00:59.124] ⋮ Details...
+bork bork bork
+
+[2013-09-06T22:00:59.124] ⋮ Details...
+bork bork bork
+
+[2013-09-06T22:00:59.124] ⋮ Details...bork bork bork
+
+[2013-09-06T22:01:49.124] ⋮ 1 beat per secondbork bork bork
+
+[2013-09-06T22:01:49.124] ⋮ not looking goodbork bork bork
+
+[2013-09-06T22:01:49.124] ⋮ looking badbork bork bork
+
+[2013-09-06T22:01:49.124] ⋮ sooo badbork bork bork
diff --git a/test/expected/test_json_format.sh_a6be47f1311ed92feaf303142fcb103deb80f456.err b/test/expected/test_json_format.sh_a6be47f1311ed92feaf303142fcb103deb80f456.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_a6be47f1311ed92feaf303142fcb103deb80f456.err
diff --git a/test/expected/test_json_format.sh_a6be47f1311ed92feaf303142fcb103deb80f456.out b/test/expected/test_json_format.sh_a6be47f1311ed92feaf303142fcb103deb80f456.out
new file mode 100644
index 0000000..b356898
--- /dev/null
+++ b/test/expected/test_json_format.sh_a6be47f1311ed92feaf303142fcb103deb80f456.out
@@ -0,0 +1,4 @@
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,client_ip,request/method,request/uri,request/size,response/status,details1,details2,details3
+0,<NULL>,2017-03-24 20:06:26.240,0,info,0,<NULL>,<NULL>,<NULL>,1.1.1.1,GET,/example/uri/5,166,200,<NULL>,<NULL>,<NULL>
+1,<NULL>,2017-03-24 20:12:47.764,381524,critical,0,<NULL>,<NULL>,<NULL>,1.1.1.1,GET,/example/uri/5,166,500,<NULL>,<NULL>,<NULL>
+2,<NULL>,2017-03-24 20:15:31.694,163930,warning,0,<NULL>,<NULL>,<NULL>,1.1.1.1,GET,/example/uri/5,166,400,"{""foo"": ""bar""}","{""foo"": ""bar""}","{""foo"": ""bar""}"
diff --git a/test/expected/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.err b/test/expected/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.err
diff --git a/test/expected/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.out b/test/expected/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.out
new file mode 100644
index 0000000..db5fbef
--- /dev/null
+++ b/test/expected/test_json_format.sh_ad3a238d03493de305544f9b30a0c69d4f474d3a.out
@@ -0,0 +1 @@
+87.226.160.250 2022-10-30T00:00:02.000 "HEAD / HTTP/1.1" 301 570
diff --git a/test/expected/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.err b/test/expected/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.err
diff --git a/test/expected/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.out b/test/expected/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.out
new file mode 100644
index 0000000..889eb99
--- /dev/null
+++ b/test/expected/test_json_format.sh_c1a23804c39b0f74642286d69865ee9d0961a58a.out
@@ -0,0 +1,2 @@
+2022-09-24T00:00:09.484 Hello, World!
+2022-09-24T00:00:19.222 Goodbye, World!
diff --git a/test/expected/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.err b/test/expected/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.err
diff --git a/test/expected/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.out b/test/expected/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.out
new file mode 100644
index 0000000..abb3b80
--- /dev/null
+++ b/test/expected/test_json_format.sh_c60050b3469f37c5b0864e1dc7eb354e91d6ec81.out
@@ -0,0 +1,49 @@
+2016-08-03T12:06:31.009 - ;Exception initializing page context; java.lang.NoClassDefFoundError: javax/el/StaticFieldELResolver
+ at org.apache.jasper.runtime.JspFactoryImpl.internalGetPageContext(JspFactoryImpl.java:172)
+ at org.apache.jasper.runtime.JspFactoryImpl.getPageContext(JspFactoryImpl.java:123)
+ at org.apache.jsp.errors._404_002dnot_002dfound_jsp._jspService(_404_002dnot_002dfound_jsp.java:38)
+ at org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:111)
+ at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
+ at org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:411)
+ at org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:473)
+ at org.apache.jasper.servlet.JspServlet.service(JspServlet.java:377)
+ at javax.servlet.http.HttpServlet.service(HttpServlet.java:731)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
+ at collective.config.startup.DamFilter.doFilter(DamFilter.java:270)
+ at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
+ at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
+ at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:748)
+ at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:488)
+ at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:411)
+ at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:338)
+ at org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:476)
+ at org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:345)
+ at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:210)
+ at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)
+ at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
+ at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:957)
+ at org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:683)
+ at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423)
+ at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079)
+ at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:620)
+ at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)
+ at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
+ at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
+ at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
+ at java.lang.Thread.run(Thread.java:744)
+Caused by: java.lang.ClassNotFoundException: javax.el.StaticFieldELResolver
+ at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1720)
+ at org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1571)
+ ... 33 common frames omitted
+ @version: 1
+ logger_name: org.apache.jasper.runtime.JspFactoryImpl
+ thread_name: http-bio-0.0.0.0-8081-exec-198
+ level: ERROR
+ customer: foobaz
+2016-08-03T12:06:31.009 - ;Exception initializing page context; 
+ @version: 1
+ logger_name: org.apache.jasper.runtime.JspFactoryImpl
+ thread_name: http-bio-0.0.0.0-8081-exec-198
+ level: ERROR
+ customer: foobaz
diff --git a/test/expected/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.err b/test/expected/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.err
diff --git a/test/expected/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.out b/test/expected/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.out
new file mode 100644
index 0000000..aa47418
--- /dev/null
+++ b/test/expected/test_json_format.sh_d0ec34389274affb70a5a76ba4789d51fd60f602.out
@@ -0,0 +1,4 @@
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,client_ip,request/method,request/uri,request/size,response/status,response/size,details1,details2,details3
+0,<NULL>,2017-03-24 20:06:26.240,0,info,0,<NULL>,<NULL>,<NULL>,1.1.1.1,GET,/example/uri/5,166,200,443,<NULL>,<NULL>,<NULL>
+1,<NULL>,2017-03-24 20:12:47.764,381524,critical,0,<NULL>,<NULL>,<NULL>,1.1.1.1,GET,/example/uri/5,166,500,4433,<NULL>,<NULL>,<NULL>
+2,<NULL>,2017-03-24 20:15:31.694,163930,warning,0,<NULL>,<NULL>,<NULL>,1.1.1.1,GET,/example/uri/5,166,400,44345,"{""foo"": ""bar""}","{""foo"": ""bar""}","{""foo"": ""bar""}"
diff --git a/test/expected/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.err b/test/expected/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.err
diff --git a/test/expected/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.out b/test/expected/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.out
new file mode 100644
index 0000000..9a1c882
--- /dev/null
+++ b/test/expected/test_json_format.sh_d7362cffc8335c2fe6b6527315de59bd6f5dcc7f.out
@@ -0,0 +1,3 @@
+2017-03-24T16:06:26.240 1.1.1.1 GET 200 443 /example/uri/5
+2017-03-24T16:12:47.764 1.1.1.1 GET 500 4433 /example/uri/5
+2017-03-24T16:15:31.694 1.1.1.1 GET 400 44345 /example/uri/5
diff --git a/test/expected/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.err b/test/expected/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.err
diff --git a/test/expected/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.out b/test/expected/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.out
new file mode 100644
index 0000000..db24336
--- /dev/null
+++ b/test/expected/test_json_format.sh_dfff27a651650a04d93de9a06ab5480e94ce3a79.out
@@ -0,0 +1,4 @@
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,client_ip,request/method,request/uri,request/size,response/status,response/size,details1,details2,details3
+0,<NULL>,2017-03-24 16:06:26.240,0,info,0,<NULL>,<NULL>,<NULL>,1.1.1.1,GET,/example/uri/5,166,200,443,<NULL>,<NULL>,<NULL>
+1,<NULL>,2017-03-24 16:12:47.764,381524,critical,0,<NULL>,<NULL>,<NULL>,1.1.1.1,GET,/example/uri/5,166,500,4433,<NULL>,<NULL>,<NULL>
+2,<NULL>,2017-03-24 16:15:31.694,163930,warning,0,<NULL>,<NULL>,<NULL>,1.1.1.1,GET,/example/uri/5,166,400,44345,"{""foo"": ""bar""}","{""foo"": ""bar""}","{""foo"": ""bar""}"
diff --git a/test/expected/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.err b/test/expected/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.err
diff --git a/test/expected/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.out b/test/expected/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.out
new file mode 100644
index 0000000..05d531a
--- /dev/null
+++ b/test/expected/test_json_format.sh_e36401aa54bc61de71f8dcbe66ea16effa59ea52.out
@@ -0,0 +1,2 @@
+87.226.160.250 2022-10-30T00:00:02.000 "HEAD / HTTP/1.1" 301 570
+ RayID: 761fde4e984e5ab2
diff --git a/test/expected/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.err b/test/expected/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.err
diff --git a/test/expected/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.out b/test/expected/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.out
new file mode 100644
index 0000000..52f2372
--- /dev/null
+++ b/test/expected/test_json_format.sh_f740026626ab554dacb249762d8be7d6539b8c6e.out
@@ -0,0 +1,2 @@
+
+[2013-09-06T20:00:49.124] INFO Starting up service
diff --git a/test/expected/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.err b/test/expected/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.err
diff --git a/test/expected/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.out b/test/expected/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.out
new file mode 100644
index 0000000..325af1f
--- /dev/null
+++ b/test/expected/test_json_format.sh_fe19b7ebd349cd689b3f5c22618eab5ce995e68e.out
@@ -0,0 +1,4 @@
+-09-06T22:00:49.124 INFO Shutting down service
+ @fields/user: steve@example.com
+ @fields: { "lvl": "INFO", "msg": "Shutting down service", "user": "steve@example.com"}
+
diff --git a/test/expected/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.err b/test/expected/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.err
diff --git a/test/expected/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.out b/test/expected/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.out
new file mode 100644
index 0000000..7569205
--- /dev/null
+++ b/test/expected/test_logfile.sh_05d1505168bf34b89fc0d1a39f1409cfe798119e.out
@@ -0,0 +1,4 @@
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: Entered:_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: Entered:__thr_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: tid:191f - Mux ID not found in mapping dictionary
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: tid:191f - Can't handle disconnect with invalid ecid
diff --git a/test/expected/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.err b/test/expected/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.err
diff --git a/test/expected/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.out b/test/expected/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.out
new file mode 100644
index 0000000..9b2a7cd
--- /dev/null
+++ b/test/expected/test_logfile.sh_08d731a04c877a34819b35de185e30a74c9fd497.out
@@ -0,0 +1,3 @@
+2600-12-03 09:23:00.000000 0:
+2600-12-03 09:23:00.000000 0:
+2600-12-03 09:23:00.000000 0:
diff --git a/test/expected/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.err b/test/expected/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.err
diff --git a/test/expected/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.out b/test/expected/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.out
new file mode 100644
index 0000000..9317bcb
--- /dev/null
+++ b/test/expected/test_logfile.sh_09bd16e044302f6b121092534708594bdad11b5a.out
@@ -0,0 +1 @@
++0
diff --git a/test/expected/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.err b/test/expected/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.err
diff --git a/test/expected/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.out b/test/expected/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.out
new file mode 100644
index 0000000..ff67043
--- /dev/null
+++ b/test/expected/test_logfile.sh_1c6eee38f66356fcd9a9f0faedaea6dbcc901060.out
@@ -0,0 +1,2 @@
+ filepath  descriptor  mimetype  content 
+{test_dir}/logfile_syslog.1.gz net.zlib.gzip.header application/json {"name":"logfile_syslog.1","mtime":"2007-11-03T16:23:00.000","comment":""} 
diff --git a/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.err b/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.err
diff --git a/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.out b/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.out
new file mode 100644
index 0000000..4ed341d
--- /dev/null
+++ b/test/expected/test_logfile.sh_218ecb88b4753010c4264b3ac351260b4811612f.out
@@ -0,0 +1,2 @@
+basename(filepath)  descriptor  mimetype  content 
+logfile_syslog.1.gz net.zlib.gzip.header application/json {"name":"logfile_syslog.1","mtime":"2007-11-03T09:23:00.000","comment":""} 
diff --git a/test/expected/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.err b/test/expected/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.err
diff --git a/test/expected/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.out b/test/expected/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.out
new file mode 100644
index 0000000..7731fde
--- /dev/null
+++ b/test/expected/test_logfile.sh_290a3c49e53c2229a7400c107338fa0bb38375e2.out
@@ -0,0 +1,2 @@
+#Fields: ? )
+0
diff --git a/test/expected/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.err b/test/expected/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.err
diff --git a/test/expected/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.out b/test/expected/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.out
new file mode 100644
index 0000000..8d8b6ce
--- /dev/null
+++ b/test/expected/test_logfile.sh_3fc6bfd8a6160817211f3e14fde957af75b9dbe7.out
@@ -0,0 +1,2 @@
+#Date: 20?0-2-02
+0
diff --git a/test/expected/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.err b/test/expected/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.err
diff --git a/test/expected/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.out b/test/expected/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.out
new file mode 100644
index 0000000..8de34b8
--- /dev/null
+++ b/test/expected/test_logfile.sh_4a2a907fcb069b8d6e65961a7b2e796d6c3a87b1.out
@@ -0,0 +1,4 @@
+#Fields: 0 cs-bytes
+#Fields: 0
+ 0 #
+0
diff --git a/test/expected/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.err b/test/expected/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.err
diff --git a/test/expected/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.out b/test/expected/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.out
new file mode 100644
index 0000000..372e078
--- /dev/null
+++ b/test/expected/test_logfile.sh_6602faf7817c494c33e32da7ee95f13aa9210d01.out
@@ -0,0 +1,10 @@
+Sep 19 09:24:04 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: tid:1d1f - Mux ID not found in mapping dictionary
+Sep 19 09:24:04 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: tid:1d1f - Can't handle disconnect with invalid ecid
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: Entered:_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: Entered:_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: Entered:__thr_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: Entered:__thr_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: tid:191f - Mux ID not found in mapping dictionary
+Sep 19 09:24:20 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: tid:1d1f - Mux ID not found in mapping dictionary
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: tid:191f - Can't handle disconnect with invalid ecid
+Sep 19 09:24:20 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: tid:1d1f - Can't handle disconnect with invalid ecid
diff --git a/test/expected/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.err b/test/expected/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.err
diff --git a/test/expected/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.out b/test/expected/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.out
new file mode 100644
index 0000000..13576d8
--- /dev/null
+++ b/test/expected/test_logfile.sh_7c2e11488bccc59458b5775db4b90de964858259.out
@@ -0,0 +1,6 @@
+000
+000
+#Fields: 0
+0
+#Fields: 0
+0
diff --git a/test/expected/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.err b/test/expected/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.err
diff --git a/test/expected/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.out b/test/expected/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.out
new file mode 100644
index 0000000..963f93c
--- /dev/null
+++ b/test/expected/test_logfile.sh_a7037efd0c4bbf51940137a44e57d94e9307e83e.out
@@ -0,0 +1 @@
+ 00:00
diff --git a/test/expected/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.err b/test/expected/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.err
diff --git a/test/expected/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.out b/test/expected/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.out
new file mode 100644
index 0000000..0733b93
--- /dev/null
+++ b/test/expected/test_logfile.sh_c18e14a26d8261c9f72747118a469266121d5459.out
@@ -0,0 +1,3 @@
+log_line log_part log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters col_0 col_1
+ 0 <NULL> 2021-05-19 08:00:01.000 0 info 0 <NULL> <NULL> <NULL> 1 /abc/def
+ 2 <NULL> 2021-05-19 08:00:03.000 2000 info 0 <NULL> <NULL> <NULL> 3 /ghi/jkl
diff --git a/test/expected/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.err b/test/expected/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.err
diff --git a/test/expected/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.out b/test/expected/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.out
new file mode 100644
index 0000000..9de987f
--- /dev/null
+++ b/test/expected/test_logfile.sh_e840b674cd65936a72bd64b1dac1524d16fe44c3.out
@@ -0,0 +1,11 @@
+ log_time  log_body 
+2022-09-19 09:24:04.000 tid:1d1f - Mux ID not found in mapping dictionary 
+2022-09-19 09:24:04.000 tid:1d1f - Can't handle disconnect with invalid ecid
+2022-09-19 09:24:20.000 Entered:_AMMuxedDeviceDisconnected, mux-device:1003 
+2022-09-19 09:24:20.000 Entered:_AMMuxedDeviceDisconnected, mux-device:1003
+2022-09-19 09:24:20.000 Entered:__thr_AMMuxedDeviceDisconnected, mux-device:1003 
+2022-09-19 09:24:20.000 Entered:__thr_AMMuxedDeviceDisconnected, mux-device:1003
+2022-09-19 09:24:20.000 tid:191f - Mux ID not found in mapping dictionary 
+2022-09-19 09:24:20.000 tid:1d1f - Mux ID not found in mapping dictionary
+2022-09-19 09:24:20.000 tid:191f - Can't handle disconnect with invalid ecid 
+2022-09-19 09:24:20.000 tid:1d1f - Can't handle disconnect with invalid ecid
diff --git a/test/expected/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.err b/test/expected/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.err
new file mode 100644
index 0000000..1f33d64
--- /dev/null
+++ b/test/expected/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.err
@@ -0,0 +1,7 @@
+✘ error: invalid value for “log_tags” column of table “access_log”
+ reason: unexpected JSON value
+ |   --> access_log.log_tags:1
+ |   | 1 
+ |   = help: expecting an array of tag values
+ --> command-option:1
+ | ;UPDATE access_log SET log_tags = 1 WHERE log_line = 1
diff --git a/test/expected/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.out b/test/expected/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_154047fb52e4831aabf7d36512247bad6a6a2cf7.out
diff --git a/test/expected/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.err b/test/expected/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.err
new file mode 100644
index 0000000..96a52d7
--- /dev/null
+++ b/test/expected/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.err
@@ -0,0 +1,11 @@
+✘ error: invalid value for “log_tags” column of table “access_log”
+ reason: “foo” is not a valid value for option “/#”
+ |  reason: value does not match pattern: ^#[^\s]+$
+ |   --> access_log.log_tags:1
+ |   | ["foo"] 
+ |   = help: Property Synopsis
+ |  /# tag
+ |  Description
+ |  A tag for the log line
+ --> command-option:1
+ | ;UPDATE access_log SET log_tags = json_array('foo') WHERE log_line = 1
diff --git a/test/expected/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.out b/test/expected/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_3c9b5940f7533c5fc3d4956a6efce50a9e7132d4.out
diff --git a/test/expected/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.err b/test/expected/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.err
diff --git a/test/expected/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.out b/test/expected/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.out
new file mode 100644
index 0000000..171d0f7
--- /dev/null
+++ b/test/expected/test_meta.sh_41f643bb4f720130625b042563e9591bee4ae588.out
@@ -0,0 +1,2 @@
+log_tags 
+["#foo"]
diff --git a/test/expected/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.err b/test/expected/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.err
diff --git a/test/expected/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.out b/test/expected/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.out
new file mode 100644
index 0000000..5ed367d
--- /dev/null
+++ b/test/expected/test_meta.sh_45ff39a3d0ac0ca0c95aaca14d043450cec1cedd.out
@@ -0,0 +1,5 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+ ├ Hello, World!
+ └ #foo
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.err b/test/expected/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.err
diff --git a/test/expected/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.out b/test/expected/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.out
new file mode 100644
index 0000000..4731070
--- /dev/null
+++ b/test/expected/test_meta.sh_48e85ba0c0945a5085fb4ee255771406061a9c17.out
@@ -0,0 +1,6 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+ │ Hello, World!
+ │
+ └ This is  markdown  now!
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.err b/test/expected/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.err
diff --git a/test/expected/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.out b/test/expected/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_meta.sh_4c39b356748c67ccf8a6027a1af88da532f8252a.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.err b/test/expected/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.err
diff --git a/test/expected/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.out b/test/expected/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.out
new file mode 100644
index 0000000..78b865d
--- /dev/null
+++ b/test/expected/test_meta.sh_7b75763926d832bf9784ca234a060859770aabe7.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+ └ #foo
diff --git a/test/expected/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.err b/test/expected/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.err
diff --git a/test/expected/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.out b/test/expected/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_meta.sh_811b1a8a176b25001a89e35b295a1117ab76969b.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.err b/test/expected/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.err
diff --git a/test/expected/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.out b/test/expected/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.out
new file mode 100644
index 0000000..2830598
--- /dev/null
+++ b/test/expected/test_meta.sh_83ac877aa9d38b25945cf96d6326a2468187c40f.out
@@ -0,0 +1,37 @@
+[2020-12-10 06:56:41,061] INFO [m:108] Calling 'x' with params:
+
+[2020-12-10 06:56:41,092] DEBUG [connect.client:69] Full request text:
+ └ #xml-req
+<?xml version='1.0' encoding='iso-8859-2'?>
+<a-request>
+ <head>
+ x
+ </head>
+ <source>
+ x
+ </source>
+ <request id="1">
+ <name>
+ x
+ </name>
+ </request>
+</a-request>
+
+[2020-12-10 06:56:41,099] DEBUG [m:85] Full reply text:
+<?xml version='1.0' encoding='iso-8859-2'?>
+<a-reply>
+ <head>
+ x
+ </head>
+ <reply id="2">
+ <status>
+ <result>OK</result>
+ </status>
+ <name>
+ x
+ </name>
+ </reply>
+ <technical-track>
+ x
+ </technical-track>
+</a-reply>
diff --git a/test/expected/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.err b/test/expected/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.err
diff --git a/test/expected/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.out b/test/expected/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.out
new file mode 100644
index 0000000..08a6fcb
--- /dev/null
+++ b/test/expected/test_meta.sh_a7489c1f0e001adc732b7e2ab31bb30960fda078.out
@@ -0,0 +1,4 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+ └ Goodbye, World!
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.err b/test/expected/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.err
diff --git a/test/expected/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.out b/test/expected/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_meta.sh_c063f96398650f130941bbbf4cf63c1244fdbee5.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.err b/test/expected/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.err
diff --git a/test/expected/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.out b/test/expected/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.out
new file mode 100644
index 0000000..8462ae3
--- /dev/null
+++ b/test/expected/test_meta.sh_c75128169049bd88d5eaf8b84a7f617e5ae5d936.out
@@ -0,0 +1,4 @@
+log_line  log_comment log_tags 
+ 0 Hello, World! ["#foo"] 
+ 1 <NULL> <NULL>
+ 2 <NULL>  <NULL> 
diff --git a/test/expected/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.err b/test/expected/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.err
diff --git a/test/expected/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.out b/test/expected/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.out
new file mode 100644
index 0000000..5cf31b1
--- /dev/null
+++ b/test/expected/test_meta.sh_c8fb22932af2467a2651797a8a8d8cddcd09431d.out
@@ -0,0 +1,4 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+ └ #foo
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.err b/test/expected/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.err
diff --git a/test/expected/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.out b/test/expected/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.out
new file mode 100644
index 0000000..a3e5357
--- /dev/null
+++ b/test/expected/test_meta.sh_d6af0b41066ca3be0bbce89c83c011f4ecfa516e.out
@@ -0,0 +1,5 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+ ├ Hello, World!
+ └ #foo
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.err b/test/expected/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.err
diff --git a/test/expected/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.out b/test/expected/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.out
new file mode 100644
index 0000000..b842402
--- /dev/null
+++ b/test/expected/test_meta.sh_fd09cb565f44a114d8c9a519e571918e30262eaf.out
@@ -0,0 +1,4 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+ └ #foo
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.err b/test/expected/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.err
diff --git a/test/expected/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.out b/test/expected/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.out
new file mode 100644
index 0000000..25b6f32
--- /dev/null
+++ b/test/expected/test_meta.sh_fdf4a91aa55262255816dff7d605f1f0a5d6fe92.out
@@ -0,0 +1,4 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+ └ Hello, World!
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.err b/test/expected/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.err
diff --git a/test/expected/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.out b/test/expected/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.out
new file mode 100644
index 0000000..35fadf8
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_3c255c3c8b28df9d694b329a265e8b8140dae4a2.out
@@ -0,0 +1,8 @@
+{
+ "wrapper": [
+ {"message":""
+ select Id from Account where id = $sfid
+ ^
+ ERROR at Row:1:Column:34
+ line 1:34 no viable alternative at character '$'
+""}]}
diff --git a/test/expected/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.err b/test/expected/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.err
diff --git a/test/expected/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.out b/test/expected/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.out
new file mode 100644
index 0000000..f6eae9d
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_4111e649fb49c0a377e552fa0b56c60c370633da.out
@@ -0,0 +1,4 @@
+{
+ Example: foo,
+ bar: baz
+}
diff --git a/test/expected/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.err b/test/expected/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.err
diff --git a/test/expected/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.out b/test/expected/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.out
new file mode 100644
index 0000000..0808acf
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_675a2ff6306df7c54127e39319cf06a2dd353145.out
@@ -0,0 +1,5 @@
+[2020-12-10 06:56:41,061] INFO [:108] Calling 'x' with params:
+[2020-12-10 06:56:41,092] DEBUG [:69] Full request text:
+/a-request, /a-request/head, /a-request/source, /a-request/request, /a-request/request/name
+[2020-12-10 06:56:41,099] DEBUG [:85] Full reply text:
+/a-reply, /a-reply/head, /a-reply/reply, /a-reply/reply/status, /a-reply/reply/status/result, /a-reply/reply/name, /a-reply/technical-track
diff --git a/test/expected/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.err b/test/expected/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.err
diff --git a/test/expected/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.out b/test/expected/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.out
new file mode 100644
index 0000000..613cc3a
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_7192f8f68adb14705c8a60e73ff8248c61c7fd03.out
@@ -0,0 +1,5 @@
+2015-04-18T13:16:30.003 {
+ "wrapper": {"msg": r""
+ Hello,
+ World!
+""}}
diff --git a/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.err b/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.err
diff --git a/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.out b/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.out
new file mode 100644
index 0000000..0ac4c9a
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_a5bee322ea3374690e44a88a16cb6b84feaa11d3.out
@@ -0,0 +1,3 @@
+Hello
+World
+
diff --git a/test/expected/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.err b/test/expected/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.err
diff --git a/test/expected/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.out b/test/expected/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.out
new file mode 100644
index 0000000..3fb9189
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_a6d9042e5e95f2a49194bd80c1eed154813ddf41.out
@@ -0,0 +1,19 @@
+Mar 24 15:17:38.999 000000000264F I shmem.res 262144 262144 1 chassis_msg_svc/osenv::req_blocking<osenv::req_lambda<tcp_messaging_impl::register_app(svc::messaging_port,
+ defs::atom*,
+ defs::borrowed<svc::messaging_session>,
+ defs::owned<svc::connection_eviction_strategy>&&,
+ svc::messaging::connection_type,
+ svc::messaging::app_param
+)::{lambda()#1}>, osenv::aloc_dynamic_named<tcp_messaging_impl::register_app(svc::messaging_port,
+ defs::atom*,
+ defs::borrowed<svc::messaging_session>,
+ defs::owned<svc::connection_eviction_strategy>&&,
+ svc::messaginconnection_type,
+ svc::messaging::app_param
+)::{lambda()#1}, osenv::temporal, tcp_messaging_impl::register_app(svc::messaging_port,
+ defs::atom*,
+ defs::borrowed<svc::messaging_session>,
+ des::owned<svc::connection_eviction_strategy>&&,
+ svc::messaging::connection_type,
+ svc::messaging::app_param
+)::{lambda()#1}>, osenv::req>->fiber stacks
diff --git a/test/expected/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.err b/test/expected/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.err
diff --git a/test/expected/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.out b/test/expected/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.out
new file mode 100644
index 0000000..c3e4e85
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_cd361eeca7e91bfab942b75d6c3422c7a456a111.out
@@ -0,0 +1,3 @@
+2015-04-18T13:16:30.003 8.8.8.8
+<foo>8.8.8.8</foo>
+9 8.8.8.8<1054 198.51.100.1546 544.9.8.7 98.542.241.99 19143.2.5.6
diff --git a/test/expected/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.err b/test/expected/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.err
diff --git a/test/expected/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.out b/test/expected/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.out
new file mode 100644
index 0000000..35f3af0
--- /dev/null
+++ b/test/expected/test_pretty_print.sh_f8feb52a321026d9562b271eb37a2c56dfaed329.out
@@ -0,0 +1 @@
+2022-06-22T10:20:33 Example foo
diff --git a/test/expected/test_regex101.sh_0fa3663a45aca6a328cb728872af7ed7ee896f1c.err b/test/expected/test_regex101.sh_0fa3663a45aca6a328cb728872af7ed7ee896f1c.err
new file mode 100644
index 0000000..cd096a9
--- /dev/null
+++ b/test/expected/test_regex101.sh_0fa3663a45aca6a328cb728872af7ed7ee896f1c.err
@@ -0,0 +1,2 @@
+✘ error: regex “std” of format “syslog_log” has not been pushed to regex101.com
+ = help: use the “push” subcommand to create the regex on regex101.com for easy editing
diff --git a/test/expected/test_regex101.sh_0fa3663a45aca6a328cb728872af7ed7ee896f1c.out b/test/expected/test_regex101.sh_0fa3663a45aca6a328cb728872af7ed7ee896f1c.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_0fa3663a45aca6a328cb728872af7ed7ee896f1c.out
diff --git a/test/expected/test_regex101.sh_182ae9244db314a953af2bee969726e381bc5a32.err b/test/expected/test_regex101.sh_182ae9244db314a953af2bee969726e381bc5a32.err
new file mode 100644
index 0000000..3b83cb4
--- /dev/null
+++ b/test/expected/test_regex101.sh_182ae9244db314a953af2bee969726e381bc5a32.err
@@ -0,0 +1,3 @@
+✘ error: unable to import: https://regex101.com/r/zpEnjV/1
+ reason: format file already exists: regex101-home/.lnav/formats/installed/unit_test_log.json
+ = help: delete the existing file to continue
diff --git a/test/expected/test_regex101.sh_182ae9244db314a953af2bee969726e381bc5a32.out b/test/expected/test_regex101.sh_182ae9244db314a953af2bee969726e381bc5a32.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_182ae9244db314a953af2bee969726e381bc5a32.out
diff --git a/test/expected/test_regex101.sh_2158f1f011ba8e1b152396c072790c076fdb8ce8.err b/test/expected/test_regex101.sh_2158f1f011ba8e1b152396c072790c076fdb8ce8.err
new file mode 100644
index 0000000..bbcd290
--- /dev/null
+++ b/test/expected/test_regex101.sh_2158f1f011ba8e1b152396c072790c076fdb8ce8.err
@@ -0,0 +1,3 @@
+⚠ warning: not deleting regex101 entry “zpEnjV”
+ reason: delete code is not known for this entry
+ = note: formats created by importing a regex101.com entry will not have a delete code
diff --git a/test/expected/test_regex101.sh_2158f1f011ba8e1b152396c072790c076fdb8ce8.out b/test/expected/test_regex101.sh_2158f1f011ba8e1b152396c072790c076fdb8ce8.out
new file mode 100644
index 0000000..bb52e20
--- /dev/null
+++ b/test/expected/test_regex101.sh_2158f1f011ba8e1b152396c072790c076fdb8ce8.out
@@ -0,0 +1 @@
+✔ deleted regex101 entry: zpEnjV
diff --git a/test/expected/test_regex101.sh_281af24141680330791db7f7c5fa70833ce08a6b.err b/test/expected/test_regex101.sh_281af24141680330791db7f7c5fa70833ce08a6b.err
new file mode 100644
index 0000000..286a1cf
--- /dev/null
+++ b/test/expected/test_regex101.sh_281af24141680330791db7f7c5fa70833ce08a6b.err
@@ -0,0 +1 @@
+✘ error: expecting a regex101.com URL to import
diff --git a/test/expected/test_regex101.sh_281af24141680330791db7f7c5fa70833ce08a6b.out b/test/expected/test_regex101.sh_281af24141680330791db7f7c5fa70833ce08a6b.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_281af24141680330791db7f7c5fa70833ce08a6b.out
diff --git a/test/expected/test_regex101.sh_35703b13990785632cca82123fb3883797959c0b.err b/test/expected/test_regex101.sh_35703b13990785632cca82123fb3883797959c0b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_35703b13990785632cca82123fb3883797959c0b.err
diff --git a/test/expected/test_regex101.sh_35703b13990785632cca82123fb3883797959c0b.out b/test/expected/test_regex101.sh_35703b13990785632cca82123fb3883797959c0b.out
new file mode 100644
index 0000000..a56e549
--- /dev/null
+++ b/test/expected/test_regex101.sh_35703b13990785632cca82123fb3883797959c0b.out
@@ -0,0 +1,4 @@
+✔ converted regex101 entry to format file: regex101-home/.lnav/formats/installed/unit_test_log.json
+ = note: the converted format may still have errors
+ = help: use the following command to patch the regex as more changes are made on regex101.com:
+ lnav -m format unit_test_log regex std regex101 pull
diff --git a/test/expected/test_regex101.sh_366730cac50b4a09b7de4b84641791470b1cb9a3.err b/test/expected/test_regex101.sh_366730cac50b4a09b7de4b84641791470b1cb9a3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_366730cac50b4a09b7de4b84641791470b1cb9a3.err
diff --git a/test/expected/test_regex101.sh_366730cac50b4a09b7de4b84641791470b1cb9a3.out b/test/expected/test_regex101.sh_366730cac50b4a09b7de4b84641791470b1cb9a3.out
new file mode 100644
index 0000000..7f5397d
--- /dev/null
+++ b/test/expected/test_regex101.sh_366730cac50b4a09b7de4b84641791470b1cb9a3.out
@@ -0,0 +1,10 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "unit_test_log": {
+ "regex": {
+ "std": {
+ "pattern": "\\[(?<timestamp>\\d+\\/\\d+\\/\\d+ \\d+:\\d+:\\d+) (?<jobserver>[\\w.]+) (?<workqueue>[\\w.]+) (?<processid>\\d+)\\] (?<body>.*)$"
+ }
+ }
+ }
+}
diff --git a/test/expected/test_regex101.sh_3d18474a3e472fff6e23e0c41337ec9188fee591.err b/test/expected/test_regex101.sh_3d18474a3e472fff6e23e0c41337ec9188fee591.err
new file mode 100644
index 0000000..32c87d4
--- /dev/null
+++ b/test/expected/test_regex101.sh_3d18474a3e472fff6e23e0c41337ec9188fee591.err
@@ -0,0 +1,34 @@
+✘ error: invalid value “/unit_test_log/value/jobserver”
+ reason: no patterns have a capture named “jobserver”
+ = note: the following captures are available:
+
+ = help: values are populated from captures in patterns, so at least one pattern must have a capture with this value name
+✘ error: invalid value “/unit_test_log/value/processid”
+ reason: no patterns have a capture named “processid”
+ = note: the following captures are available:
+
+ = help: values are populated from captures in patterns, so at least one pattern must have a capture with this value name
+✘ error: invalid value “/unit_test_log/value/timestamp”
+ reason: no patterns have a capture named “timestamp”
+ = note: the following captures are available:
+
+ = help: values are populated from captures in patterns, so at least one pattern must have a capture with this value name
+✘ error: invalid value “/unit_test_log/value/workqueue”
+ reason: no patterns have a capture named “workqueue”
+ = note: the following captures are available:
+
+ = help: values are populated from captures in patterns, so at least one pattern must have a capture with this value name
+✘ error: invalid sample log message: "[03/22/2021 02:00:02 job1074.example.com db.db81.example_events 54026] {\"ELAPSED\":\"0.011\",\"LEVEL\":\"info\",\"MESSAGE\":\"finished in 0.011\\n\",\"PREFIX\":\"YFgyWQriCmsAAofJAAAAHg\",\"ROUTINGKEY\":\"EXAMPLE1366.Example.Events._Publish\"}"
+ reason: sample does not match any patterns
+ --> regex101-home/.lnav/formats/installed/unit_test_log.json:26
+ = note: the following shows how each pattern matched this sample:
+ [03/22/2021 02:00:02 job1074.example.com db.db81.example_events 54026] {"ELAPSED":"0.011","LEVEL":"info","MESSAGE":"finished in 0.011\n","PREFIX":"YFgyWQriCmsAAofJAAAAHg","ROUTINGKEY":"EXAMPLE1366.Example.Events._Publish"}
+ = note: std = “”
+
+✘ error: invalid sample log message: "[03/22/2021 02:00:02 job1074.example.com db.db81.example_events 54026] {\"ELAPSED\":\"0.011\",\"LEVEL\":\"info\",\"MESSAGE\":\"finished in 0.011\\n\",\"PREFIX\":\"YFgyWQriCmsAAofJAAAAHg\",\"ROUTINGKEY\":\"EXAMPLE1366.Example.Events._Publish\"}"
+ reason: sample does not match any patterns
+ --> regex101-home/.lnav/formats/installed/unit_test_log.json:30
+ = note: the following shows how each pattern matched this sample:
+ [03/22/2021 02:00:02 job1074.example.com db.db81.example_events 54026] {"ELAPSED":"0.011","LEVEL":"info","MESSAGE":"finished in 0.011\n","PREFIX":"YFgyWQriCmsAAofJAAAAHg","ROUTINGKEY":"EXAMPLE1366.Example.Events._Publish"}
+ = note: std = “”
+
diff --git a/test/expected/test_regex101.sh_3d18474a3e472fff6e23e0c41337ec9188fee591.out b/test/expected/test_regex101.sh_3d18474a3e472fff6e23e0c41337ec9188fee591.out
new file mode 100644
index 0000000..93cf6b4
--- /dev/null
+++ b/test/expected/test_regex101.sh_3d18474a3e472fff6e23e0c41337ec9188fee591.out
@@ -0,0 +1,3 @@
+✔ format patch file written to: regex101-home/.lnav/formats/installed/unit_test_log.regex101-zpEnjV.json
+ = help: once the regex has been found to be working correctly, move the contents of the patch file to the original file at:
+ regex101-home/.lnav/formats/installed/unit_test_log.json
diff --git a/test/expected/test_regex101.sh_442cc58676590a3604d5c2183f5fe0a75c98351a.err b/test/expected/test_regex101.sh_442cc58676590a3604d5c2183f5fe0a75c98351a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_442cc58676590a3604d5c2183f5fe0a75c98351a.err
diff --git a/test/expected/test_regex101.sh_442cc58676590a3604d5c2183f5fe0a75c98351a.out b/test/expected/test_regex101.sh_442cc58676590a3604d5c2183f5fe0a75c98351a.out
new file mode 100644
index 0000000..7329b20
--- /dev/null
+++ b/test/expected/test_regex101.sh_442cc58676590a3604d5c2183f5fe0a75c98351a.out
@@ -0,0 +1,2 @@
+regex101-home/.lnav/formats/installed/unit_test_log.json
+regex101-home/.lnav/formats/installed/unit_test_log.regex101-zpEnjV.json
diff --git a/test/expected/test_regex101.sh_566fd88d216a44bc1c6e23f2d6f2d0caf99d42f9.err b/test/expected/test_regex101.sh_566fd88d216a44bc1c6e23f2d6f2d0caf99d42f9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_566fd88d216a44bc1c6e23f2d6f2d0caf99d42f9.err
diff --git a/test/expected/test_regex101.sh_566fd88d216a44bc1c6e23f2d6f2d0caf99d42f9.out b/test/expected/test_regex101.sh_566fd88d216a44bc1c6e23f2d6f2d0caf99d42f9.out
new file mode 100644
index 0000000..3fd83a6
--- /dev/null
+++ b/test/expected/test_regex101.sh_566fd88d216a44bc1c6e23f2d6f2d0caf99d42f9.out
@@ -0,0 +1 @@
+✔ no regex101 entries found
diff --git a/test/expected/test_regex101.sh_5f2f7ecb6ab9cbec4b41385b91bd038906b8a7b2.err b/test/expected/test_regex101.sh_5f2f7ecb6ab9cbec4b41385b91bd038906b8a7b2.err
new file mode 100644
index 0000000..6d4ee32
--- /dev/null
+++ b/test/expected/test_regex101.sh_5f2f7ecb6ab9cbec4b41385b91bd038906b8a7b2.err
@@ -0,0 +1,3 @@
+✘ error: cannot delete regex101 entry while patch file exists
+ = note: regex101-home/.lnav/formats/installed/unit_test_log.regex101-zpEnjV.json
+ = help: move the contents of the patch file to the main log format and then delete the file to continue
diff --git a/test/expected/test_regex101.sh_5f2f7ecb6ab9cbec4b41385b91bd038906b8a7b2.out b/test/expected/test_regex101.sh_5f2f7ecb6ab9cbec4b41385b91bd038906b8a7b2.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_5f2f7ecb6ab9cbec4b41385b91bd038906b8a7b2.out
diff --git a/test/expected/test_regex101.sh_629bde30483e0a6461076e9058f3a5eb81ae0425.err b/test/expected/test_regex101.sh_629bde30483e0a6461076e9058f3a5eb81ae0425.err
new file mode 100644
index 0000000..83dd591
--- /dev/null
+++ b/test/expected/test_regex101.sh_629bde30483e0a6461076e9058f3a5eb81ae0425.err
@@ -0,0 +1,3 @@
+✘ error: invalid regex “abc(def)” from “https://regex101.com/r/cvCJNP/1”
+ reason: unsupported regex flavor: “gm”
+ = help: the supported flavors are: pcre, pcre2
diff --git a/test/expected/test_regex101.sh_629bde30483e0a6461076e9058f3a5eb81ae0425.out b/test/expected/test_regex101.sh_629bde30483e0a6461076e9058f3a5eb81ae0425.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_629bde30483e0a6461076e9058f3a5eb81ae0425.out
diff --git a/test/expected/test_regex101.sh_630db454054cf92ec9bd0f4e3e83300047f583ff.err b/test/expected/test_regex101.sh_630db454054cf92ec9bd0f4e3e83300047f583ff.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_630db454054cf92ec9bd0f4e3e83300047f583ff.err
diff --git a/test/expected/test_regex101.sh_630db454054cf92ec9bd0f4e3e83300047f583ff.out b/test/expected/test_regex101.sh_630db454054cf92ec9bd0f4e3e83300047f583ff.out
new file mode 100644
index 0000000..8b0d55d
--- /dev/null
+++ b/test/expected/test_regex101.sh_630db454054cf92ec9bd0f4e3e83300047f583ff.out
@@ -0,0 +1,4 @@
+✔ converted regex101 entry to format file: regex101-home/.lnav/formats/installed/unit_test_log.regex101-hGiqBL.json
+ = note: the converted format may still have errors
+ = help: use the following command to patch the regex as more changes are made on regex101.com:
+ lnav -m format unit_test_log regex alt regex101 pull
diff --git a/test/expected/test_regex101.sh_771af6f3d29b8350542d5c6e98bdbf4c223cd531.err b/test/expected/test_regex101.sh_771af6f3d29b8350542d5c6e98bdbf4c223cd531.err
new file mode 100644
index 0000000..e533bd6
--- /dev/null
+++ b/test/expected/test_regex101.sh_771af6f3d29b8350542d5c6e98bdbf4c223cd531.err
@@ -0,0 +1 @@
+✘ error: unknown regex: non-existent
diff --git a/test/expected/test_regex101.sh_771af6f3d29b8350542d5c6e98bdbf4c223cd531.out b/test/expected/test_regex101.sh_771af6f3d29b8350542d5c6e98bdbf4c223cd531.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_771af6f3d29b8350542d5c6e98bdbf4c223cd531.out
diff --git a/test/expected/test_regex101.sh_7991a5b617867cf37c9f7baa85ffa425f7d455a2.err b/test/expected/test_regex101.sh_7991a5b617867cf37c9f7baa85ffa425f7d455a2.err
new file mode 100644
index 0000000..450b99b
--- /dev/null
+++ b/test/expected/test_regex101.sh_7991a5b617867cf37c9f7baa85ffa425f7d455a2.err
@@ -0,0 +1,5 @@
+✘ error: expecting an operation to perform on the std regex using regex101.com
+ = help: the available subcommands are:
+ • push: create/update an entry for this regex on regex101.com
+ • pull: create a patch format file for this regular expression based on the entry in regex101.com
+ • delete: delete the entry regex101.com that was created by a push operation
diff --git a/test/expected/test_regex101.sh_7991a5b617867cf37c9f7baa85ffa425f7d455a2.out b/test/expected/test_regex101.sh_7991a5b617867cf37c9f7baa85ffa425f7d455a2.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_7991a5b617867cf37c9f7baa85ffa425f7d455a2.out
diff --git a/test/expected/test_regex101.sh_79ee3f5fe71ccec97b2619d8c1f74ca97ffd2243.err b/test/expected/test_regex101.sh_79ee3f5fe71ccec97b2619d8c1f74ca97ffd2243.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_79ee3f5fe71ccec97b2619d8c1f74ca97ffd2243.err
diff --git a/test/expected/test_regex101.sh_79ee3f5fe71ccec97b2619d8c1f74ca97ffd2243.out b/test/expected/test_regex101.sh_79ee3f5fe71ccec97b2619d8c1f74ca97ffd2243.out
new file mode 100644
index 0000000..4985cf6
--- /dev/null
+++ b/test/expected/test_regex101.sh_79ee3f5fe71ccec97b2619d8c1f74ca97ffd2243.out
@@ -0,0 +1,2 @@
+✔ local regex is in sync with entry “zpEnjV” on regex101.com
+ = help: make edits on “https://regex101.com/r/zpEnjV” and then run this command again to update the local values
diff --git a/test/expected/test_regex101.sh_7de76c174c58d67bf93e8f01d6d55ebb6a023f10.err b/test/expected/test_regex101.sh_7de76c174c58d67bf93e8f01d6d55ebb6a023f10.err
new file mode 100644
index 0000000..8b825ab
--- /dev/null
+++ b/test/expected/test_regex101.sh_7de76c174c58d67bf93e8f01d6d55ebb6a023f10.err
@@ -0,0 +1,3 @@
+✘ error: unknown regex: s
+ = note: did you mean one of the following?
+ std
diff --git a/test/expected/test_regex101.sh_7de76c174c58d67bf93e8f01d6d55ebb6a023f10.out b/test/expected/test_regex101.sh_7de76c174c58d67bf93e8f01d6d55ebb6a023f10.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_7de76c174c58d67bf93e8f01d6d55ebb6a023f10.out
diff --git a/test/expected/test_regex101.sh_8a43e6657d4f60e68d31eb8302542ca28e80d077.err b/test/expected/test_regex101.sh_8a43e6657d4f60e68d31eb8302542ca28e80d077.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_8a43e6657d4f60e68d31eb8302542ca28e80d077.err
diff --git a/test/expected/test_regex101.sh_8a43e6657d4f60e68d31eb8302542ca28e80d077.out b/test/expected/test_regex101.sh_8a43e6657d4f60e68d31eb8302542ca28e80d077.out
new file mode 100644
index 0000000..265ad46
--- /dev/null
+++ b/test/expected/test_regex101.sh_8a43e6657d4f60e68d31eb8302542ca28e80d077.out
@@ -0,0 +1,3 @@
+✔ the following regex101 entries were found:
+ format unit_test_log regex std regex101
+
diff --git a/test/expected/test_regex101.sh_8e93a3b6b941847c71409a297779fbb0a6666a51.err b/test/expected/test_regex101.sh_8e93a3b6b941847c71409a297779fbb0a6666a51.err
new file mode 100644
index 0000000..db47111
--- /dev/null
+++ b/test/expected/test_regex101.sh_8e93a3b6b941847c71409a297779fbb0a6666a51.err
@@ -0,0 +1,3 @@
+✘ error: expecting an operation to perform on the std regular expression
+ = help: the available subcommands are:
+ • regex101: use regex101.com to edit this regular expression
diff --git a/test/expected/test_regex101.sh_8e93a3b6b941847c71409a297779fbb0a6666a51.out b/test/expected/test_regex101.sh_8e93a3b6b941847c71409a297779fbb0a6666a51.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_8e93a3b6b941847c71409a297779fbb0a6666a51.out
diff --git a/test/expected/test_regex101.sh_95c56a9d146ec9a7c2196559d316f928b2ae6ae9.err b/test/expected/test_regex101.sh_95c56a9d146ec9a7c2196559d316f928b2ae6ae9.err
new file mode 100644
index 0000000..2064fc4
--- /dev/null
+++ b/test/expected/test_regex101.sh_95c56a9d146ec9a7c2196559d316f928b2ae6ae9.err
@@ -0,0 +1,4 @@
+✘ error: unable to import: abc
+ reason: expecting a format name that matches the regular expression “^\w+$”
+ = note: “def-jkl”
+ ^ matched up to here
diff --git a/test/expected/test_regex101.sh_95c56a9d146ec9a7c2196559d316f928b2ae6ae9.out b/test/expected/test_regex101.sh_95c56a9d146ec9a7c2196559d316f928b2ae6ae9.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_95c56a9d146ec9a7c2196559d316f928b2ae6ae9.out
diff --git a/test/expected/test_regex101.sh_9d101ee29c45cdb8c0f117ad736c9a5dd5da5839.err b/test/expected/test_regex101.sh_9d101ee29c45cdb8c0f117ad736c9a5dd5da5839.err
new file mode 100644
index 0000000..ca5b424
--- /dev/null
+++ b/test/expected/test_regex101.sh_9d101ee29c45cdb8c0f117ad736c9a5dd5da5839.err
@@ -0,0 +1 @@
+✘ error: no regex101 entry for syslog_log/std
diff --git a/test/expected/test_regex101.sh_9d101ee29c45cdb8c0f117ad736c9a5dd5da5839.out b/test/expected/test_regex101.sh_9d101ee29c45cdb8c0f117ad736c9a5dd5da5839.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_9d101ee29c45cdb8c0f117ad736c9a5dd5da5839.out
diff --git a/test/expected/test_regex101.sh_c43e07df9b3068696fdc8759c7561135db981b38.err b/test/expected/test_regex101.sh_c43e07df9b3068696fdc8759c7561135db981b38.err
new file mode 100644
index 0000000..f705892
--- /dev/null
+++ b/test/expected/test_regex101.sh_c43e07df9b3068696fdc8759c7561135db981b38.err
@@ -0,0 +1,2 @@
+✘ error: unable to get entry “badregex123” on regex101.com
+ reason: received response code 400 content “{"error":"Invalid permalink id",}”
diff --git a/test/expected/test_regex101.sh_c43e07df9b3068696fdc8759c7561135db981b38.out b/test/expected/test_regex101.sh_c43e07df9b3068696fdc8759c7561135db981b38.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_c43e07df9b3068696fdc8759c7561135db981b38.out
diff --git a/test/expected/test_regex101.sh_cbd859487e4ea011cd6e0f0f114d70158bfd8b43.err b/test/expected/test_regex101.sh_cbd859487e4ea011cd6e0f0f114d70158bfd8b43.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_cbd859487e4ea011cd6e0f0f114d70158bfd8b43.err
diff --git a/test/expected/test_regex101.sh_cbd859487e4ea011cd6e0f0f114d70158bfd8b43.out b/test/expected/test_regex101.sh_cbd859487e4ea011cd6e0f0f114d70158bfd8b43.out
new file mode 100644
index 0000000..c06f523
--- /dev/null
+++ b/test/expected/test_regex101.sh_cbd859487e4ea011cd6e0f0f114d70158bfd8b43.out
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "unit_test_log": {
+ "description": "Format file generated from regex101 entry -- https://regex101.com/r/zpEnjV/2",
+ "regex": {
+ "std": {
+ "pattern": "\\[(?<timestamp>\\d+\\/\\d+\\/\\d+ \\d+:\\d+:\\d+) (?<jobserver>[\\w.]+) (?<workqueue>[\\w.]+) (?<processid>\\d+)\\] (?<body>.*)$"
+ }
+ },
+ "value": {
+ "jobserver": {
+ "kind": "string"
+ },
+ "processid": {
+ "kind": "string"
+ },
+ "timestamp": {
+ "kind": "string"
+ },
+ "workqueue": {
+ "kind": "string"
+ }
+ },
+ "sample": [
+ {
+ "line": "[03/22/2021 02:00:02 job1074.example.com db.db81.example_events 54026] {\"ELAPSED\":\"0.011\",\"LEVEL\":\"info\",\"MESSAGE\":\"finished in 0.011\\n\",\"PREFIX\":\"YFgyWQriCmsAAofJAAAAHg\",\"ROUTINGKEY\":\"EXAMPLE1366.Example.Events._Publish\"}"
+ },
+ {
+ "description": "sample 1",
+ "line": "[03/22/2021 02:00:02 job1074.example.com db.db81.example_events 54026] {\"ELAPSED\":\"0.011\",\"LEVEL\":\"info\",\"MESSAGE\":\"finished in 0.011\\n\",\"PREFIX\":\"YFgyWQriCmsAAofJAAAAHg\",\"ROUTINGKEY\":\"EXAMPLE1366.Example.Events._Publish\"}"
+ }
+ ]
+ }
+}
diff --git a/test/expected/test_regex101.sh_cf6c0a9f0f04e24ce1fae7a0a434830b14447f83.err b/test/expected/test_regex101.sh_cf6c0a9f0f04e24ce1fae7a0a434830b14447f83.err
new file mode 100644
index 0000000..9b187b7
--- /dev/null
+++ b/test/expected/test_regex101.sh_cf6c0a9f0f04e24ce1fae7a0a434830b14447f83.err
@@ -0,0 +1 @@
+✘ error: unknown format: non-existent
diff --git a/test/expected/test_regex101.sh_cf6c0a9f0f04e24ce1fae7a0a434830b14447f83.out b/test/expected/test_regex101.sh_cf6c0a9f0f04e24ce1fae7a0a434830b14447f83.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_cf6c0a9f0f04e24ce1fae7a0a434830b14447f83.out
diff --git a/test/expected/test_regex101.sh_d84597760285c3964b258726341e018f6cd49954.err b/test/expected/test_regex101.sh_d84597760285c3964b258726341e018f6cd49954.err
new file mode 100644
index 0000000..85a97ba
--- /dev/null
+++ b/test/expected/test_regex101.sh_d84597760285c3964b258726341e018f6cd49954.err
@@ -0,0 +1,7 @@
+✘ error: expecting an operation to perform on the std regex using regex101.com
+ = note: this regex is currently associated with the following regex101.com entry:
+ https://regex101.com/r/zpEnjV
+ = help: the available subcommands are:
+ • push: create/update an entry for this regex on regex101.com
+ • pull: create a patch format file for this regular expression based on the entry in regex101.com
+ • delete: delete the entry regex101.com that was created by a push operation
diff --git a/test/expected/test_regex101.sh_d84597760285c3964b258726341e018f6cd49954.out b/test/expected/test_regex101.sh_d84597760285c3964b258726341e018f6cd49954.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_d84597760285c3964b258726341e018f6cd49954.out
diff --git a/test/expected/test_regex101.sh_f23e393dbf23d0d8e276e9b7610c7b74d79980f8.err b/test/expected/test_regex101.sh_f23e393dbf23d0d8e276e9b7610c7b74d79980f8.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_f23e393dbf23d0d8e276e9b7610c7b74d79980f8.err
diff --git a/test/expected/test_regex101.sh_f23e393dbf23d0d8e276e9b7610c7b74d79980f8.out b/test/expected/test_regex101.sh_f23e393dbf23d0d8e276e9b7610c7b74d79980f8.out
new file mode 100644
index 0000000..d37aaa6
--- /dev/null
+++ b/test/expected/test_regex101.sh_f23e393dbf23d0d8e276e9b7610c7b74d79980f8.out
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "unit_test_log": {
+ "regex": {
+ "alt": {
+ "pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(?:\\.\\d{3,6})?)Z?\\](?<body>.*)$"
+ }
+ },
+ "sample": [
+ {
+ "line": "[2021-05-21T21:58:57.022497Z]"
+ }
+ ]
+ }
+}
diff --git a/test/expected/test_regex101.sh_fc41b6ee90cbf038620151f16d164b361acf82dd.err b/test/expected/test_regex101.sh_fc41b6ee90cbf038620151f16d164b361acf82dd.err
new file mode 100644
index 0000000..494a387
--- /dev/null
+++ b/test/expected/test_regex101.sh_fc41b6ee90cbf038620151f16d164b361acf82dd.err
@@ -0,0 +1 @@
+✘ error: “bro” is an internal format that is not defined in a configuration file
diff --git a/test/expected/test_regex101.sh_fc41b6ee90cbf038620151f16d164b361acf82dd.out b/test/expected/test_regex101.sh_fc41b6ee90cbf038620151f16d164b361acf82dd.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_regex101.sh_fc41b6ee90cbf038620151f16d164b361acf82dd.out
diff --git a/test/expected/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.err b/test/expected/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.err
diff --git a/test/expected/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.out b/test/expected/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.out
new file mode 100644
index 0000000..bccea86
--- /dev/null
+++ b/test/expected/test_sessions.sh_0300a1391c33b1c45ddfa90198a6bd0a5404a77f.out
@@ -0,0 +1 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.err b/test/expected/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.err
diff --git a/test/expected/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.out b/test/expected/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.out
new file mode 100644
index 0000000..2140e81
--- /dev/null
+++ b/test/expected/test_sessions.sh_17b85654b929b2a8fc1705a170ced544783292fa.out
@@ -0,0 +1,3 @@
+⋮ - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+⋮ - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+⋮ - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.err b/test/expected/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.err
diff --git a/test/expected/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.out b/test/expected/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_345b0e66dab7b881397c4b38380da81092ab70dd.out
diff --git a/test/expected/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.err b/test/expected/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.err
diff --git a/test/expected/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.out b/test/expected/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_430b9522ba1a37983138f3c4935cba91b781e415.out
diff --git a/test/expected/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.err b/test/expected/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.err
diff --git a/test/expected/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.out b/test/expected/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_4f13dd3858546b6e04a27e244159d355e368f2ae.out
diff --git a/test/expected/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.err b/test/expected/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.err
diff --git a/test/expected/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.out b/test/expected/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.out
new file mode 100644
index 0000000..d07a9c0
--- /dev/null
+++ b/test/expected/test_sessions.sh_68a89b56c5e7f7db620084cca1eb547cbb19a2c9.out
@@ -0,0 +1,4 @@
+log_line,log_part
+0,<NULL>
+1,middle
+2,middle
diff --git a/test/expected/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.err b/test/expected/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.err
diff --git a/test/expected/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.out b/test/expected/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.out
new file mode 100644
index 0000000..eb9e677
--- /dev/null
+++ b/test/expected/test_sessions.sh_6d87ff483d5785c58fb271a405ff1c35e4f83cd9.out
@@ -0,0 +1,36 @@
+#!lnav -Nf
+# This file is an export of an lnav session. You can type
+# '|/path/to/this/file' in lnav to execute this file and
+# restore the state of the session.
+
+;SELECT raise_error('This session export was made with a newer version of lnav, please upgrade to ' || '0.11.2' || ' or later')
+ WHERE lnav_version() < '0.11.2' COLLATE naturalcase
+
+# The files loaded into the session were:
+
+
+# Set this environment variable to override this value or edit this script.
+;INSERT OR IGNORE INTO environ (name, value) VALUES ('LOG_DIR_0', '{test_dir}')
+:open $LOG_DIR_0/support-dump/logfile_access_log.0
+:open $LOG_DIR_0/support-dump/logfile_access_log.1
+
+:rebuild
+
+
+# The following SQL statements will restore the bookmarks,
+# comments, and tags that were added in the session.
+
+;SELECT total_changes() AS before_mark_changes
+;UPDATE all_logs SET log_mark = 1, log_comment = NULL, log_tags = NULL WHERE log_time_msecs = 1248130769000 AND log_format = 'access_log' AND log_line_hash = 'v1:b05c1bdfe75cde41e151c89087e31951'
+
+;SELECT 1 - (total_changes() - $before_mark_changes) AS failed_mark_changes
+;SELECT echoln(printf('%sERROR%s: failed to restore %d bookmarks',
+ $ansi_red, $ansi_norm, $failed_mark_changes))
+ WHERE $failed_mark_changes != 0
+
+
+# The following commands will restore the state of the LOG view.
+
+:switch-to-view log
+:hide-file */support-dump/logfile_access_log.1
+:goto 1
diff --git a/test/expected/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.err b/test/expected/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.err
diff --git a/test/expected/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.out b/test/expected/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.out
new file mode 100644
index 0000000..ba6bfe9
--- /dev/null
+++ b/test/expected/test_sessions.sh_858fd0081ed9c46dd81e2f81f1090756f2463558.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [01/Jan/2010:00:00:00 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [01/Jan/2010:00:00:03 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [01/Jan/2010:00:00:03 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sessions.sh_8732dad5481be991ca7f291d9c5451c7b016cea7.err b/test/expected/test_sessions.sh_8732dad5481be991ca7f291d9c5451c7b016cea7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_8732dad5481be991ca7f291d9c5451c7b016cea7.err
diff --git a/test/expected/test_sessions.sh_8732dad5481be991ca7f291d9c5451c7b016cea7.out b/test/expected/test_sessions.sh_8732dad5481be991ca7f291d9c5451c7b016cea7.out
new file mode 100644
index 0000000..a1e2e6f
--- /dev/null
+++ b/test/expected/test_sessions.sh_8732dad5481be991ca7f291d9c5451c7b016cea7.out
@@ -0,0 +1,33 @@
+#!lnav -Nf
+# This file is an export of an lnav session. You can type
+# '|/path/to/this/file' in lnav to execute this file and
+# restore the state of the session.
+
+;SELECT raise_error('This session export was made with a newer version of lnav, please upgrade to ' || '0.11.0' || ' or later')
+ WHERE lnav_version() < '0.11.0' COLLATE naturalcase
+
+# The files loaded into the session were:
+
+
+;INSERT OR IGNORE INTO environ (name, value) VALUES ('LOG_DIR_0', '{top_srcdir_parent}')
+:open $LOG_DIR_0/lnav/test/logfile_access_log.0
+
+:rebuild
+
+
+# The following SQL statements will restore the bookmarks,
+# comments, and tags that were added in the session.
+
+;SELECT total_changes() AS before_mark_changes
+;UPDATE all_logs SET log_mark = 1, log_comment = NULL, log_tags = NULL WHERE log_time_msecs = 1248130769000 AND log_format = 'access_log' AND log_line_hash = 'v1:b05c1bdfe75cde41e151c89087e31951'
+
+;SELECT 1 - (total_changes() - $before_mark_changes) AS failed_mark_changes
+;SELECT echoln(printf('%sERROR%s: failed to restore %d bookmarks',
+ $ansi_red, $ansi_norm, $failed_mark_changes))
+ WHERE $failed_mark_changes != 0
+
+
+# The following commands will restore the state of the LOG view.
+
+:switch-to-view log
+:goto 1
diff --git a/test/expected/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.err b/test/expected/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.err
diff --git a/test/expected/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.out b/test/expected/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_903b41c950f5f90d7786d7a09bb6e2f217654b15.out
diff --git a/test/expected/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.err b/test/expected/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.err
diff --git a/test/expected/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.out b/test/expected/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.out
new file mode 100644
index 0000000..a9dd3d0
--- /dev/null
+++ b/test/expected/test_sessions.sh_92a98a3e4e3a10bf1f2371d21a8282c5d3d4baa5.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.err b/test/expected/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.err
diff --git a/test/expected/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.out b/test/expected/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.out
new file mode 100644
index 0000000..3c84ede
--- /dev/null
+++ b/test/expected/test_sessions.sh_9978aaa475513f9981840e612f853a7707ffcf90.out
@@ -0,0 +1,11 @@
+view_name filter_id enabled type language pattern
+log 1 1 out regex blah
+ name search
+log foobar
+text
+help
+histogram
+db
+schema
+pretty
+spectro
diff --git a/test/expected/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.err b/test/expected/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.err
diff --git a/test/expected/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.out b/test/expected/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_a92822d121a836140a401fd71535dc4a7a8d5b48.out
diff --git a/test/expected/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.err b/test/expected/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.err
diff --git a/test/expected/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.out b/test/expected/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_b3d71a87fcb4e3487f71ccad8c6ce681db220572.out
diff --git a/test/expected/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.err b/test/expected/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.err
diff --git a/test/expected/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.out b/test/expected/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.out
new file mode 100644
index 0000000..ad2b37e
--- /dev/null
+++ b/test/expected/test_sessions.sh_b932b33dd087b94d4306dd179c5d4f9ddd394960.out
@@ -0,0 +1 @@
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
diff --git a/test/expected/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.err b/test/expected/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.err
diff --git a/test/expected/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.out b/test/expected/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.out
new file mode 100644
index 0000000..ad2b37e
--- /dev/null
+++ b/test/expected/test_sessions.sh_ddf45811e9906de9f3930fe802ac7b2cc6e48106.out
@@ -0,0 +1 @@
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
diff --git a/test/expected/test_sessions.sh_e39648f425c3f291c9d1c0d14595a019abd0cb48.err b/test/expected/test_sessions.sh_e39648f425c3f291c9d1c0d14595a019abd0cb48.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sessions.sh_e39648f425c3f291c9d1c0d14595a019abd0cb48.err
diff --git a/test/expected/test_sessions.sh_e39648f425c3f291c9d1c0d14595a019abd0cb48.out b/test/expected/test_sessions.sh_e39648f425c3f291c9d1c0d14595a019abd0cb48.out
new file mode 100644
index 0000000..0473e9d
--- /dev/null
+++ b/test/expected/test_sessions.sh_e39648f425c3f291c9d1c0d14595a019abd0cb48.out
@@ -0,0 +1,33 @@
+#!lnav -Nf
+# This file is an export of an lnav session. You can type
+# '|/path/to/this/file' in lnav to execute this file and
+# restore the state of the session.
+
+;SELECT raise_error('This session export was made with a newer version of lnav, please upgrade to ' || '0.11.0' || ' or later')
+ WHERE lnav_version() < '0.11.0' COLLATE naturalcase
+
+# The files loaded into the session were:
+
+
+;INSERT OR IGNORE INTO environ (name, value) VALUES ('LOG_DIR_0', '{test_dir}')
+:open $LOG_DIR_0/support-dump/logfile_access_log.0
+
+:rebuild
+
+
+# The following SQL statements will restore the bookmarks,
+# comments, and tags that were added in the session.
+
+;SELECT total_changes() AS before_mark_changes
+;UPDATE all_logs SET log_mark = 1, log_comment = NULL, log_tags = NULL WHERE log_time_msecs = 1248130769000 AND log_format = 'access_log' AND log_line_hash = 'v1:b05c1bdfe75cde41e151c89087e31951'
+
+;SELECT 1 - (total_changes() - $before_mark_changes) AS failed_mark_changes
+;SELECT echoln(printf('%sERROR%s: failed to restore %d bookmarks',
+ $ansi_red, $ansi_norm, $failed_mark_changes))
+ WHERE $failed_mark_changes != 0
+
+
+# The following commands will restore the state of the LOG view.
+
+:switch-to-view log
+:goto 1
diff --git a/test/expected/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.err b/test/expected/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.err
diff --git a/test/expected/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.out b/test/expected/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.out
new file mode 100644
index 0000000..fd072bc
--- /dev/null
+++ b/test/expected/test_shlexer.sh_14dd967cb2af90899c9e5e45d00b676b5a3163aa.out
@@ -0,0 +1,7 @@
+ ~ foo
+til ^
+wsp ^
+eval -- ../test foo
+split:
+ 0 -- ../test
+ 1 -- foo
diff --git a/test/expected/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.err b/test/expected/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.err
diff --git a/test/expected/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.out b/test/expected/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.out
new file mode 100644
index 0000000..b7ba9e8
--- /dev/null
+++ b/test/expected/test_shlexer.sh_2781f5dd570580cbe746ad91b58a28b8371283b3.out
@@ -0,0 +1,7 @@
+ ~nonexistent/bar baz
+til ^----------^
+wsp ^
+eval -- ~nonexistent/bar baz
+split:
+ 0 -- ~nonexistent/bar
+ 1 -- baz
diff --git a/test/expected/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.err b/test/expected/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.err
diff --git a/test/expected/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.out b/test/expected/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.out
new file mode 100644
index 0000000..85cca78
--- /dev/null
+++ b/test/expected/test_shlexer.sh_2af44d06fc137a77bc230be86376ccad23a2806b.out
@@ -0,0 +1,2 @@
+ \
+err ^
diff --git a/test/expected/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.err b/test/expected/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.err
diff --git a/test/expected/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.out b/test/expected/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.out
new file mode 100644
index 0000000..f677b17
--- /dev/null
+++ b/test/expected/test_shlexer.sh_6858e530a8ecb77cbaec1a7507768dd5a1942ac9.out
@@ -0,0 +1,5 @@
+ ${FOO}
+qrf ^----^
+eval -- bar
+split:
+ 0 -- bar
diff --git a/test/expected/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.err b/test/expected/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.err
diff --git a/test/expected/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.out b/test/expected/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.out
new file mode 100644
index 0000000..630eb1c
--- /dev/null
+++ b/test/expected/test_shlexer.sh_7f31e16ea2469da7a4328c93c7bcc8e109f84d2f.out
@@ -0,0 +1,7 @@
+ "abc ${DEF} 123"
+dst ^
+qrf ^----^
+den ^
+eval -- "abc xyz 123"
+split:
+ 0 -- abc xyz 123
diff --git a/test/expected/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.err b/test/expected/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.err
diff --git a/test/expected/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.out b/test/expected/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.out
new file mode 100644
index 0000000..759e75b
--- /dev/null
+++ b/test/expected/test_shlexer.sh_8aeebcdef56edd783579eaaddaff7c5cc127bb86.out
@@ -0,0 +1,6 @@
+ 'abc $DEF 123'
+sst ^
+sen ^
+eval -- 'abc $DEF 123'
+split:
+ 0 -- abc $DEF 123
diff --git a/test/expected/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.err b/test/expected/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.err
diff --git a/test/expected/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.out b/test/expected/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.out
new file mode 100644
index 0000000..2cbee1a
--- /dev/null
+++ b/test/expected/test_shlexer.sh_8e9addb0e5b6f4254d81dd89ecf12783109644bb.out
@@ -0,0 +1,7 @@
+ "abc $DEF 123"
+dst ^
+ref ^--^
+den ^
+eval -- "abc xyz 123"
+split:
+ 0 -- abc xyz 123
diff --git a/test/expected/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.err b/test/expected/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.err
diff --git a/test/expected/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.out b/test/expected/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.out
new file mode 100644
index 0000000..9a2b2a7
--- /dev/null
+++ b/test/expected/test_shlexer.sh_90961e6728e96d0a44535a6c9907cc990c10316c.out
@@ -0,0 +1,6 @@
+ "def"
+dst ^
+den ^
+eval -- "def"
+split:
+ 0 -- def
diff --git a/test/expected/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.err b/test/expected/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.err
diff --git a/test/expected/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.out b/test/expected/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.out
new file mode 100644
index 0000000..30a7791
--- /dev/null
+++ b/test/expected/test_shlexer.sh_95c4e861804a5434900fdb4d67b149d1baa2edf4.out
@@ -0,0 +1,5 @@
+ $FOO
+ref ^--^
+eval -- bar
+split:
+ 0 -- bar
diff --git a/test/expected/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.err b/test/expected/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.err
diff --git a/test/expected/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.out b/test/expected/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.out
new file mode 100644
index 0000000..a2ae7ff
--- /dev/null
+++ b/test/expected/test_shlexer.sh_d7fe5f6b8fc9ba00539fad0fa0bfb08319d8b04b.out
@@ -0,0 +1,6 @@
+ 'abc'
+sst ^
+sen ^
+eval -- 'abc'
+split:
+ 0 -- abc
diff --git a/test/expected/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.err b/test/expected/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.err
diff --git a/test/expected/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.out b/test/expected/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.out
new file mode 100644
index 0000000..def9d5c
--- /dev/null
+++ b/test/expected/test_shlexer.sh_d9d46422a913e3a06ddbd262933ef5352c30e68f.out
@@ -0,0 +1,9 @@
+ abc $DEF 123
+wsp ^
+ref ^--^
+wsp ^^
+eval -- abc xyz 123
+split:
+ 0 -- abc
+ 1 -- xyz
+ 2 -- 123
diff --git a/test/expected/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.err b/test/expected/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.err
diff --git a/test/expected/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.out b/test/expected/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.out
new file mode 100644
index 0000000..9b84999
--- /dev/null
+++ b/test/expected/test_shlexer.sh_e0599f0b53d1bd27af767113853f8e84291f137d.out
@@ -0,0 +1,6 @@
+ '"'
+sst ^
+sen ^
+eval -- '"'
+split:
+ 0 -- "
diff --git a/test/expected/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.err b/test/expected/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.err
diff --git a/test/expected/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.out b/test/expected/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.out
new file mode 100644
index 0000000..a668d4d
--- /dev/null
+++ b/test/expected/test_shlexer.sh_e8fa2239ab17e7563d0c524f5400a79d6ff8bfda.out
@@ -0,0 +1,6 @@
+ "'"
+dst ^
+den ^
+eval -- "'"
+split:
+ 0 -- '
diff --git a/test/expected/test_sql.sh_02def66745b063518473df862987747909f56ccc.err b/test/expected/test_sql.sh_02def66745b063518473df862987747909f56ccc.err
new file mode 100644
index 0000000..8b97c72
--- /dev/null
+++ b/test/expected/test_sql.sh_02def66745b063518473df862987747909f56ccc.err
@@ -0,0 +1,4 @@
+✘ error: failed to compile SQL statement
+ reason: no such table: nonexistent_table
+ --> command-option:1
+ | select * from nonexistent_table 
diff --git a/test/expected/test_sql.sh_02def66745b063518473df862987747909f56ccc.out b/test/expected/test_sql.sh_02def66745b063518473df862987747909f56ccc.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_02def66745b063518473df862987747909f56ccc.out
diff --git a/test/expected/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.err b/test/expected/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.err
diff --git a/test/expected/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.out b/test/expected/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.out
new file mode 100644
index 0000000..386f1e3
--- /dev/null
+++ b/test/expected/test_sql.sh_0a5d13b62da4cb66a59a51b0240b5fe0b6036b7e.out
@@ -0,0 +1,2 @@
+ lnav_top_file() 
+{test_dir}/logfile_access_log.0
diff --git a/test/expected/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.err b/test/expected/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.err
new file mode 100644
index 0000000..0b7649a
--- /dev/null
+++ b/test/expected/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.err
@@ -0,0 +1,4 @@
+✘ error: failed to compile SQL statement
+ reason: the stop parameter is required
+ --> command-option:1
+ | SELECT * FROM generate_series(1) 
diff --git a/test/expected/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.out b/test/expected/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_0d46ee142f80f262c8c14a22751571cc567df525.out
diff --git a/test/expected/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.err b/test/expected/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.err
diff --git a/test/expected/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.out b/test/expected/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.out
new file mode 100644
index 0000000..4acb940
--- /dev/null
+++ b/test/expected/test_sql.sh_13429aed81d7edfd47b57e9cdb8a25c43aff35c4.out
@@ -0,0 +1,2 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters  c_ip cs_bytes cs_method cs_uri_query  cs_uri_stem cs_username cs_vars cs_version s_app s_core s_pid s_req s_runtime s_switches s_worker_reqs sc_bytes sc_header_bytes sc_headers sc_status 
+ 0  <NULL> 2016-03-13 22:49:12.000  0 info   0  <NULL>  <NULL>  <NULL> 127.0.0.1  696 POST   <NULL> /update_metrics     38 HTTP/1.1  0   3  88185  1  0.129  1  1  47  378  9    200 
diff --git a/test/expected/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.err b/test/expected/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.err
diff --git a/test/expected/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.out b/test/expected/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.out
new file mode 100644
index 0000000..df0e6d7
--- /dev/null
+++ b/test/expected/test_sql.sh_1cbb81cfe40ee16332c5c775a74d06b945aa65c2.out
@@ -0,0 +1,3 @@
+id first_name last_name age 
+ 0 Phil  Myman   30 
+ 1 Lem  Hewitt   35
diff --git a/test/expected/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.err b/test/expected/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.err
new file mode 100644
index 0000000..b57e6bc
--- /dev/null
+++ b/test/expected/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.err
@@ -0,0 +1,10 @@
+✘ error: “bad(” is not a valid regular expression
+ reason: missing closing parenthesis
+ --> pattern
+ | bad( 
+ |  ^ missing closing parenthesis 
+ --> command-option:1
+ | :create-search-table search_test1 bad( 
+ = help: :create-search-table table-name [pattern]
+ ══════════════════════════════════════════════════════════════════════
+ Create an SQL table based on a regex search
diff --git a/test/expected/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.out b/test/expected/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_2532083f215ed44630621f18df3dd7b77c06ae10.out
diff --git a/test/expected/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.err b/test/expected/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.err
new file mode 100644
index 0000000..b5fba56
--- /dev/null
+++ b/test/expected/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.err
@@ -0,0 +1,6 @@
+✘ error: Cannot generate spectrogram for database results
+ reason: No “log_time” column found in the result set
+ --> command-option:2
+ | :spectrogram sc_bytes 
+ = note: An ascending “log_time” column is needed to render a spectrogram
+ = help: Include a “log_time” column in your statement. Use an AS directive to alias a computed timestamp
diff --git a/test/expected/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.out b/test/expected/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_26c0d94d7837792144f2d0f866fb3c12a0bd410d.out
diff --git a/test/expected/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.err b/test/expected/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.err
new file mode 100644
index 0000000..cf33b67
--- /dev/null
+++ b/test/expected/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.err
@@ -0,0 +1,4 @@
+✘ error: failed to compile SQL statement
+ reason: not authorized
+ --> command-option:1
+ | attach database '/tmp/memdb' as 'db' 
diff --git a/test/expected/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.out b/test/expected/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_2959f0c70fca61a07c6c772f193e73022f7794f1.out
diff --git a/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.err b/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.err
new file mode 100644
index 0000000..bc74c2d
--- /dev/null
+++ b/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.err
@@ -0,0 +1,4 @@
+✘ error: call to raise_error(msg) failed
+ reason: oops!
+ --> command-option:1
+ | ;SELECT raise_error('oops!') 
diff --git a/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.out b/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_2a16a6fd0ff235a7877e1ea93b22d873a3609402.out
diff --git a/test/expected/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.err b/test/expected/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.err
new file mode 100644
index 0000000..f68fcf0
--- /dev/null
+++ b/test/expected/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.err
@@ -0,0 +1,4 @@
+✘ error: failed to compile SQL statement
+ reason: not authorized
+ --> command-option:1
+ | attach database 'simple-db.db' as 'db' 
diff --git a/test/expected/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.out b/test/expected/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_2cc8a92c6eb73741080b187a2670d309b8171c90.out
diff --git a/test/expected/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.err b/test/expected/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.err
diff --git a/test/expected/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.out b/test/expected/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.out
new file mode 100644
index 0000000..df0e6d7
--- /dev/null
+++ b/test/expected/test_sql.sh_2f15b8a38673ac4db45dc6ed2eafe609c332575b.out
@@ -0,0 +1,3 @@
+id first_name last_name age 
+ 0 Phil  Myman   30 
+ 1 Lem  Hewitt   35
diff --git a/test/expected/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.err b/test/expected/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.err
diff --git a/test/expected/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.out b/test/expected/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.out
new file mode 100644
index 0000000..5118721
--- /dev/null
+++ b/test/expected/test_sql.sh_31df37f254255115611fc321b63374a2fa4a1cd5.out
@@ -0,0 +1,2 @@
+ replicate('foobar', 120) 
+foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar⋯oobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar
diff --git a/test/expected/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.err b/test/expected/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.err
new file mode 100644
index 0000000..73441f1
--- /dev/null
+++ b/test/expected/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.err
@@ -0,0 +1,4 @@
+✘ error: failed to compile SQL statement
+ reason: not authorized
+ --> command-option:1
+ | attach database ':memdb:' as 'db' 
diff --git a/test/expected/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.out b/test/expected/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_3d77a2092192caf98e141a6039e886ede836f044.out
diff --git a/test/expected/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.err b/test/expected/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.err
new file mode 100644
index 0000000..57d160d
--- /dev/null
+++ b/test/expected/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.err
@@ -0,0 +1,5 @@
+✘ error: Cannot generate spectrogram for database results
+ reason: unknown column -- “sc_byes”
+ --> command-option:2
+ | :spectrogram sc_byes 
+ = help: Expecting a numeric column to visualize
diff --git a/test/expected/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.out b/test/expected/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_4090f96ea11a344c1e2939211da778992dab47d8.out
diff --git a/test/expected/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.err b/test/expected/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.err
new file mode 100644
index 0000000..0c1c059
--- /dev/null
+++ b/test/expected/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: A non-empty name and value must be provided when inserting an environment variable
+ --> command-option:1
+ | ;INSERT INTO environ (name) VALUES (null)
diff --git a/test/expected/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.out b/test/expected/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_4629b626c65a85d7a5595571e195b67afca272ba.out
diff --git a/test/expected/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.err b/test/expected/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.err
new file mode 100644
index 0000000..89f156f
--- /dev/null
+++ b/test/expected/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: An environment variable with the name 'SQL_ENV_VALUE' already exists
+ --> command-option:1
+ | ;INSERT INTO environ (name, value) VALUES ("SQL_ENV_VALUE", "bar")
diff --git a/test/expected/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.out b/test/expected/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_50c0b2c93b646b848a017764bde8a4282c556e2d.out
diff --git a/test/expected/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.err b/test/expected/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.err
new file mode 100644
index 0000000..b15f6ce
--- /dev/null
+++ b/test/expected/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: Environment variable names cannot contain an equals sign (=)
+ --> command-option:1
+ | ;INSERT INTO environ (name, value) VALUES ("foo=bar", "bar")
diff --git a/test/expected/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.out b/test/expected/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_528e48a03cdfa7cfbe263a6e22a65606247a8a95.out
diff --git a/test/expected/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.err b/test/expected/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.err
diff --git a/test/expected/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.out b/test/expected/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.out
new file mode 100644
index 0000000..dbb2a2f
--- /dev/null
+++ b/test/expected/test_sql.sh_5532c7a21e3f6b7df3aad10d7bdfbb7a812ae6c7.out
@@ -0,0 +1,2 @@
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,log_hostname,log_msgid,log_pid,log_pri,log_procname,log_struct,log_syslog_tag,syslog_version,col_0,TTY,PWD,USER,COMMAND
+0,<NULL>,2007-11-03 09:47:02.000,0,info,0,<NULL>,<NULL>,[1],veridian,<NULL>,<NULL>,<NULL>,sudo,<NULL>,sudo,<NULL>,timstack,pts/6,/auto/wstimstack/rpms/lbuild/test,root,/usr/bin/tail /var/log/messages
diff --git a/test/expected/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.err b/test/expected/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.err
diff --git a/test/expected/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.out b/test/expected/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.out
new file mode 100644
index 0000000..fdb6294
--- /dev/null
+++ b/test/expected/test_sql.sh_56047c9470e515bc3e3709354c01e5d50462cde7.out
@@ -0,0 +1,2 @@
+log_top_line() 
+ <NULL>
diff --git a/test/expected/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.err b/test/expected/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.err
new file mode 100644
index 0000000..7c11869
--- /dev/null
+++ b/test/expected/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.err
@@ -0,0 +1,5 @@
+✘ error: Cannot generate spectrogram for database results
+ reason: “c_ip” is not a numeric column
+ --> command-option:2
+ | :spectrogram c_ip 
+ = help: Only numeric columns can be visualized
diff --git a/test/expected/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.out b/test/expected/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_57427f3c4b4ec785ffff7c5802c10db0d3e547cf.out
diff --git a/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.err b/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.err
new file mode 100644
index 0000000..600e19d
--- /dev/null
+++ b/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.err
@@ -0,0 +1,7 @@
+✘ error: call to raise_error(msg) failed
+ reason: no data was redirected to lnav's standard-input
+ --> command-option:1
+ | |rename-stdin foo 
+ --> ../test/.lnav/formats/default/rename-stdin.lnav:7
+ | ;SELECT raise_error('no data was redirected to lnav''s standard-input') 
+ |  WHERE (SELECT count(1) FROM lnav_file WHERE filepath='stdin') = 0
diff --git a/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.out b/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_57edc93426e6767aa44ab2356c55327553dcdc8d.out
diff --git a/test/expected/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.err b/test/expected/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.err
new file mode 100644
index 0000000..3ced6be
--- /dev/null
+++ b/test/expected/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: A non-empty name and value must be provided when inserting an environment variable
+ --> command-option:1
+ | ;INSERT INTO environ (name, value) VALUES (null, null)
diff --git a/test/expected/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.out b/test/expected/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_5801770f3e0ecc1d62c7a97116d6da1981bbc7bd.out
diff --git a/test/expected/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.err b/test/expected/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.err
new file mode 100644
index 0000000..dfe4674
--- /dev/null
+++ b/test/expected/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.err
@@ -0,0 +1,5 @@
+✘ error: invalid SQL statement
+ reason: using a question-mark (?) for bound variables is not supported, only named bound parameters are supported
+ --> command-option:1
+ | ;SELECT 1 = ? 
+ = help: named parameters start with a dollar-sign ($) or colon (:) followed by the variable name
diff --git a/test/expected/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.out b/test/expected/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_5fe26fe4fc22f23f8dbe3a6aab394602886f2971.out
diff --git a/test/expected/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.err b/test/expected/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.err
diff --git a/test/expected/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.out b/test/expected/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.out
new file mode 100644
index 0000000..898c9a8
--- /dev/null
+++ b/test/expected/test_sql.sh_62eb85c9569e71a630d72065238559528a16114c.out
@@ -0,0 +1,2 @@
+filepath 
+foo
diff --git a/test/expected/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.err b/test/expected/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.err
new file mode 100644
index 0000000..cdb2140
--- /dev/null
+++ b/test/expected/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.err
@@ -0,0 +1,6 @@
+✘ error: unable to read script file: nonexistent-file -- No such file or directory
+ --> command-option:1
+ | ;.read nonexistent-file 
+ = help: ;.read path
+ ══════════════════════════════════════════════════════════════════════
+ Execute the SQLite statements in the given file
diff --git a/test/expected/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.out b/test/expected/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_6ad9d0adf85c36363f6b24f49950dcdc13dd34ab.out
diff --git a/test/expected/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.err b/test/expected/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.err
new file mode 100644
index 0000000..f031c59
--- /dev/null
+++ b/test/expected/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.err
@@ -0,0 +1,6 @@
+✘ error: unknown search table -- search_test1
+ --> command-option:1
+ | :delete-search-table search_test1 
+ = help: :delete-search-table table-name
+ ══════════════════════════════════════════════════════════════════════
+ Create an SQL table based on a regex search
diff --git a/test/expected/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.out b/test/expected/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_6edb0c8d5323d1b962d90dd6ecdd7eee9008d7b5.out
diff --git a/test/expected/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.err b/test/expected/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.err
diff --git a/test/expected/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.out b/test/expected/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.out
new file mode 100644
index 0000000..801a9af
--- /dev/null
+++ b/test/expected/test_sql.sh_753c343a256d1286750314957d1b4e155464e03e.out
@@ -0,0 +1,2 @@
+log_top_datetime() 
+ <NULL>
diff --git a/test/expected/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.err b/test/expected/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.err
diff --git a/test/expected/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.out b/test/expected/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.out
new file mode 100644
index 0000000..fabc853
--- /dev/null
+++ b/test/expected/test_sql.sh_764306f0e5f610ba71f521ba3d19fe158ece0ba5.out
@@ -0,0 +1,2 @@
+ col_0 
+eth0.IPv4
diff --git a/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err b/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err
new file mode 100644
index 0000000..1a0c2b0
--- /dev/null
+++ b/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.err
@@ -0,0 +1,6 @@
+✘ error: call to raise_error(msg) failed
+ reason: expecting the new name for stdin as the first argument
+ --> command-option:1
+ | |rename-stdin 
+ --> ../test/.lnav/formats/default/rename-stdin.lnav:6
+ | ;SELECT raise_error('expecting the new name for stdin as the first argument') WHERE $1 IS NULL
diff --git a/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.out b/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_7f664c9cda0ae1c48333e21051b5e0eeafd5b4bc.out
diff --git a/test/expected/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.err b/test/expected/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.err
diff --git a/test/expected/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.out b/test/expected/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.out
new file mode 100644
index 0000000..97b8dcc
--- /dev/null
+++ b/test/expected/test_sql.sh_85fe3b9803254ea54b864d4865d7bd4d7a7f86c6.out
@@ -0,0 +1,4 @@
+192.168.202.254 - - [20/Jul/2009:23:00:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:23:00:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:23:00:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+10.112.81.15 - - [15/Feb/2013:06:01:31 +0000] "-" 400 0 "-" "-"
diff --git a/test/expected/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.err b/test/expected/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.err
diff --git a/test/expected/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.out b/test/expected/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.out
new file mode 100644
index 0000000..b21f574
--- /dev/null
+++ b/test/expected/test_sql.sh_8ee288f1508eaab0367e465e9f382e848f3282aa.out
@@ -0,0 +1,4 @@
+ log_time 
+2009-07-20 22:59:26.000
+2009-07-20 22:59:29.000
+2009-07-20 22:59:29.000
diff --git a/test/expected/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.err b/test/expected/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.err
new file mode 100644
index 0000000..d291abe
--- /dev/null
+++ b/test/expected/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.err
@@ -0,0 +1,6 @@
+✘ error: unknown search table -- search_test1
+ --> command-option:2
+ | :delete-search-table search_test1 
+ = help: :delete-search-table table-name
+ ══════════════════════════════════════════════════════════════════════
+ Create an SQL table based on a regex search
diff --git a/test/expected/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.out b/test/expected/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_9a209f3ee1b1f543ca2587b695d2eb0e63e74c51.out
diff --git a/test/expected/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.err b/test/expected/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.err
diff --git a/test/expected/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.out b/test/expected/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.out
new file mode 100644
index 0000000..f648fc6
--- /dev/null
+++ b/test/expected/test_sql.sh_9b03e9f7a1bc35e408b3a17ee90cfdadea164df6.out
@@ -0,0 +1,4 @@
+Min: 0   1-23   24-48   49+ Max: 291690
+ Thu Nov 03 00:15:00               
+▲ 70 values in the range 0.00-3788.18
+ Thu Nov 03 00:20:00
diff --git a/test/expected/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.err b/test/expected/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.err
new file mode 100644
index 0000000..3129cfa
--- /dev/null
+++ b/test/expected/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.err
@@ -0,0 +1,6 @@
+✘ error: Cannot generate spectrogram for database results
+ reason: The “log_time” column is not in ascending order between rows 1 and 2
+ --> command-option:2
+ | :spectrogram sc_bytes 
+ = note: An ascending “log_time” column is needed to render a spectrogram
+ = help: Add an “ORDER BY log_time ASC” clause to your SELECT statement
diff --git a/test/expected/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.out b/test/expected/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_9ceccab07fbf7130bffe3c201c710719e4a3e9af.out
diff --git a/test/expected/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.err b/test/expected/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.err
new file mode 100644
index 0000000..d3d1a0a
--- /dev/null
+++ b/test/expected/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: real file paths cannot be updated, only symbolic ones
+ --> command-option:1
+ | ;UPDATE lnav_file SET filepath='foo' WHERE endswith(filepath, '_log.0')
diff --git a/test/expected/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.out b/test/expected/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_9e1d05b821822ee40e13fadb24ec558f4bfcff10.out
diff --git a/test/expected/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.err b/test/expected/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.err
new file mode 100644
index 0000000..8dd63fa
--- /dev/null
+++ b/test/expected/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: attempt to write a readonly database
+ --> command-option:1
+ | ;delete from access_log 
diff --git a/test/expected/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.out b/test/expected/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_a6b68b9f0044d18e7fa8f9287ddc9110701edc33.out
diff --git a/test/expected/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.err b/test/expected/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.err
diff --git a/test/expected/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.out b/test/expected/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.out
new file mode 100644
index 0000000..c012bd8
--- /dev/null
+++ b/test/expected/test_sql.sh_ae7b1f1684e14bf9c16e0d789257b6ef57cfb2b1.out
@@ -0,0 +1,2 @@
+ log_time 
+2009-07-20 22:59:26.000
diff --git a/test/expected/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.err b/test/expected/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.err
diff --git a/test/expected/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.out b/test/expected/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.out
new file mode 100644
index 0000000..4382802
--- /dev/null
+++ b/test/expected/test_sql.sh_afe9cdc4898df5c4e112c13dfe3db6dc089c0d7c.out
@@ -0,0 +1,4 @@
+ log_body 
+lookup(file): lookup for foobar failed
+attempting to mount entry /auto/opt
+lookup(file): lookup for opt failed
diff --git a/test/expected/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.err b/test/expected/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.err
diff --git a/test/expected/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.out b/test/expected/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.out
new file mode 100644
index 0000000..b21f574
--- /dev/null
+++ b/test/expected/test_sql.sh_b085d26043f9661d70f82cb90ecb3c5245d25eac.out
@@ -0,0 +1,4 @@
+ log_time 
+2009-07-20 22:59:26.000
+2009-07-20 22:59:29.000
+2009-07-20 22:59:29.000
diff --git a/test/expected/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.err b/test/expected/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.err
new file mode 100644
index 0000000..fe7b342
--- /dev/null
+++ b/test/expected/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.err
@@ -0,0 +1,4 @@
+✘ error: failed to compile SQL statement
+ reason: the start parameter is required
+ --> command-option:1
+ | SELECT * FROM generate_series() 
diff --git a/test/expected/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.out b/test/expected/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_b2694e4fbecdd128798af25ee0d069e7e35fb499.out
diff --git a/test/expected/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.err b/test/expected/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.err
diff --git a/test/expected/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.out b/test/expected/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.out
new file mode 100644
index 0000000..c2c44ad
--- /dev/null
+++ b/test/expected/test_sql.sh_b5aa0561a65de7e8e22085db184c72a94b1a89a9.out
@@ -0,0 +1,2 @@
+ log_body 
+timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
diff --git a/test/expected/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.err b/test/expected/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.err
diff --git a/test/expected/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.out b/test/expected/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.out
new file mode 100644
index 0000000..85a1303
--- /dev/null
+++ b/test/expected/test_sql.sh_bad03a996c0750733ab99c592b9011851f521a69.out
@@ -0,0 +1,5 @@
+match_index  content  case match_index when 2 then replicate('abc', 1000) else '' end 
+  0 {"col_0":10}  
+ 1 {"col_0":50}  
+ 2 {"col_0":50} abcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc⋯bcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabcabc 
+ 3 {"col_0":50}  
diff --git a/test/expected/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.err b/test/expected/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.err
diff --git a/test/expected/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.out b/test/expected/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.out
new file mode 100644
index 0000000..7041f45
--- /dev/null
+++ b/test/expected/test_sql.sh_bd46ca4560f8be6307a914e39539bbac0368080a.out
@@ -0,0 +1,2 @@
+lnav_top_file() 
+ <NULL>
diff --git a/test/expected/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.err b/test/expected/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.err
new file mode 100644
index 0000000..da8dff5
--- /dev/null
+++ b/test/expected/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: A non-empty name and value must be provided when inserting an environment variable
+ --> command-option:1
+ | ;INSERT INTO environ (name, value) VALUES ("", null)
diff --git a/test/expected/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.out b/test/expected/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_c20b0320096342c180146a5d18a6de82319d70b2.out
diff --git a/test/expected/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.err b/test/expected/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.err
diff --git a/test/expected/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.out b/test/expected/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.out
new file mode 100644
index 0000000..b21f574
--- /dev/null
+++ b/test/expected/test_sql.sh_c353ef036c505b75996252138fbd4c8d22e8149c.out
@@ -0,0 +1,4 @@
+ log_time 
+2009-07-20 22:59:26.000
+2009-07-20 22:59:29.000
+2009-07-20 22:59:29.000
diff --git a/test/expected/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.err b/test/expected/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.err
diff --git a/test/expected/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.out b/test/expected/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.out
new file mode 100644
index 0000000..86a4d5a
--- /dev/null
+++ b/test/expected/test_sql.sh_c5b8da04734fadf3b9eea80e0af997e38e0fb811.out
@@ -0,0 +1,3 @@
+log_line  col_0 
+ 0 eth0.IPv4 
+ 7 eth0.IPv4
diff --git a/test/expected/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.err b/test/expected/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.err
new file mode 100644
index 0000000..8ab7471
--- /dev/null
+++ b/test/expected/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.err
@@ -0,0 +1,4 @@
+✘ error: failed to compile SQL statement
+ reason: not authorized
+ --> command-option:1
+ | attach database 'file:memdb?cache=shared' as 'db'
diff --git a/test/expected/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.out b/test/expected/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_c73dec2706fc0b9a124f5da3a83f40d8d3255beb.out
diff --git a/test/expected/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.err b/test/expected/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.err
diff --git a/test/expected/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.out b/test/expected/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.out
new file mode 100644
index 0000000..762b09c
--- /dev/null
+++ b/test/expected/test_sql.sh_c7e1dbf4605914720b55787785abfafdf2c4178a.out
@@ -0,0 +1,2 @@
+ log_top_datetime() 
+2016-03-13 22:49:15.000
diff --git a/test/expected/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.err b/test/expected/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.err
diff --git a/test/expected/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.out b/test/expected/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.out
new file mode 100644
index 0000000..b13e5f0
--- /dev/null
+++ b/test/expected/test_sql.sh_cc77a633a66d1778705a34e3657737547b3fb08d.out
@@ -0,0 +1,2 @@
+log_top_line() 
+ 2
diff --git a/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.err b/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.err
new file mode 100644
index 0000000..c7ee159
--- /dev/null
+++ b/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.err
@@ -0,0 +1,10 @@
+✘ error: call to raise_error(msg) failed
+ reason: oops!
+ --> command-option:2
+ | ;SELECT $inum, $nul, $fnum, $str, raise_error('oops!')
+ = note: the bound parameters are set as follows:
+ $fnum:FLOAT = “2”
+ $inum:INTEGER = “1”
+ $nul:NULL = “<NULL>”
+ $str:TEXT = “abc”
+
diff --git a/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.out b/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_dd540973a0dc86320d84706845a15608196ae5be.out
diff --git a/test/expected/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.err b/test/expected/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.err
diff --git a/test/expected/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.out b/test/expected/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.out
new file mode 100644
index 0000000..889aced
--- /dev/null
+++ b/test/expected/test_sql.sh_e70dc7d2b686c7f91c2b41b10f3920c50f3ea405.out
@@ -0,0 +1 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.err b/test/expected/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.err
diff --git a/test/expected/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.out b/test/expected/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.out
new file mode 100644
index 0000000..a4a70f3
--- /dev/null
+++ b/test/expected/test_sql.sh_ff8a978fc0de0fed675a3cd1454cf435a6856fd5.out
@@ -0,0 +1,3 @@
+134
+46210
+78929
diff --git a/test/expected/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.err b/test/expected/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.err
diff --git a/test/expected/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.out b/test/expected/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.out
new file mode 100644
index 0000000..8adbd5c
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_028d5d5af2f3519b59d349d41cb7ecf385253b51.out
@@ -0,0 +1,5 @@
+ SELECT * FROM FOO
+ sql_keyword ------
+ sql_oper -
+ sql_keyword ----
+ sql_ident ---
diff --git a/test/expected/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.err b/test/expected/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.err
diff --git a/test/expected/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.out b/test/expected/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.out
new file mode 100644
index 0000000..0e8b3dd
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_0a37c43350ddd7a2d0d75695be32fac083ad04a4.out
@@ -0,0 +1,4 @@
+ CREATE
+ sql_keyword ------
+CREATE: Assign a name to a SELECT statement
+CREATE: Create a table
diff --git a/test/expected/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.err b/test/expected/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.err
diff --git a/test/expected/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.out b/test/expected/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.out
new file mode 100644
index 0000000..d8f654e
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_1151e5b727f6b57070bf2c8f047f1d7e02b803a6.out
@@ -0,0 +1,6 @@
+ SELECT lower(abc
+ sql_keyword ------
+ sql_func ---------
+ sql_ident -----
+ sql_ident ---
+lower: Returns a copy of the given string with all ASCII characters converted to lower case.
diff --git a/test/expected/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.err b/test/expected/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.err
diff --git a/test/expected/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.out b/test/expected/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.out
new file mode 100644
index 0000000..c9237a8
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_1b29488b949c294479aa6054f80a35bc106b454b.out
@@ -0,0 +1,2 @@
+ TABLE
+ sql_keyword -----
diff --git a/test/expected/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.err b/test/expected/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.err
diff --git a/test/expected/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.out b/test/expected/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.out
new file mode 100644
index 0000000..b7bee8a
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_331a152080d2e278b7cc0a37728eca1ded36ed72.out
@@ -0,0 +1,5 @@
+ SELECT 'hello, world!' FROM "my table"
+ sql_keyword ------
+ sql_string ---------------
+ sql_keyword ----
+ sql_ident ----------
diff --git a/test/expected/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.err b/test/expected/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.err
diff --git a/test/expected/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.out b/test/expected/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.out
new file mode 100644
index 0000000..9498d4b
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_4ca92f0da538c2f9d524211a021b306af0d2740d.out
@@ -0,0 +1,7 @@
+ SELECT * FROM foo.bar
+ sql_keyword ------
+ sql_oper -
+ sql_keyword ----
+ sql_ident ---
+ sql_garbage -
+ sql_ident ---
diff --git a/test/expected/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.err b/test/expected/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.err
diff --git a/test/expected/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.out b/test/expected/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.out
new file mode 100644
index 0000000..8433625
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_73814eca259e469b57bf7469787b91e8e8569b17.out
@@ -0,0 +1,7 @@
+ SELECT (1 + 2) AS three
+ sql_keyword ------
+ sql_number -
+ sql_oper -
+ sql_number -
+ sql_keyword --
+ sql_ident -----
diff --git a/test/expected/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.err b/test/expected/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.err
diff --git a/test/expected/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.out b/test/expected/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.out
new file mode 100644
index 0000000..00a2e8a
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_74bc5fb90a0c94a1a37d30a8e9254ea02c192a75.out
@@ -0,0 +1,6 @@
+ SELECT lower(abc)
+ sql_keyword ------
+ sql_func ---------
+ sql_ident -----
+ sql_ident ---
+SELECT: Query the database and return zero or more rows of data.
diff --git a/test/expected/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.err b/test/expected/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.err
diff --git a/test/expected/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.out b/test/expected/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.out
new file mode 100644
index 0000000..7c92ed1
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_7b183037479528581e1eacace7b9acae41c5aa8e.out
@@ -0,0 +1,12 @@
+ SELECT instr(lower(abc), '123') FROM bar
+ sql_keyword ------
+ sql_func -----------------------
+ sql_ident -----
+ sql_func ---------
+ sql_ident -----
+ sql_ident ---
+ sql_comma -
+ sql_string -----
+ sql_keyword ----
+ sql_ident ---
+SELECT: Query the database and return zero or more rows of data.
diff --git a/test/expected/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.err b/test/expected/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.err
diff --git a/test/expected/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.out b/test/expected/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.out
new file mode 100644
index 0000000..dc88535
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_96ebdc277ae760e1b6efae3195ff678654b04e52.out
@@ -0,0 +1,7 @@
+ SELECT foo(bar())
+ sql_keyword ------
+ sql_func ---------
+ sql_ident ---
+ sql_func ----
+ sql_ident ---
+SELECT: Query the database and return zero or more rows of data.
diff --git a/test/expected/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.err b/test/expected/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.err
diff --git a/test/expected/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.out b/test/expected/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.out
new file mode 100644
index 0000000..5fa2f87
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_99da5994c8c90536dbdb1b8ad7dbfb41698a5e8c.out
@@ -0,0 +1,10 @@
+ SELECT instr(lower(abc), '123')
+ sql_keyword ------
+ sql_func -----------------------
+ sql_ident -----
+ sql_func ---------
+ sql_ident -----
+ sql_ident ---
+ sql_comma -
+ sql_string -----
+lower: Returns a copy of the given string with all ASCII characters converted to lower case.
diff --git a/test/expected/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.err b/test/expected/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.err
diff --git a/test/expected/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.out b/test/expected/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.out
new file mode 100644
index 0000000..fc6d386
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_b1a2ddce48beb3e4b1e3ca4b4229a7c21b83b7c4.out
@@ -0,0 +1,6 @@
+ SELECT lower( abc )
+ sql_keyword ------
+ sql_func ----------------
+ sql_ident -----
+ sql_ident ---
+lower: Returns a copy of the given string with all ASCII characters converted to lower case.
diff --git a/test/expected/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.err b/test/expected/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.err
diff --git a/test/expected/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.out b/test/expected/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.out
new file mode 100644
index 0000000..902a1db
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_be6839712d088fc7b31618ed90f8ce706c35a9c0.out
@@ -0,0 +1,7 @@
+ SELECT (1.5 + 2.2) AS decim
+ sql_keyword ------
+ sql_number ---
+ sql_oper -
+ sql_number ---
+ sql_keyword --
+ sql_ident -----
diff --git a/test/expected/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.err b/test/expected/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.err
diff --git a/test/expected/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.out b/test/expected/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.out
new file mode 100644
index 0000000..c09afc3
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_c879ba94fdc1a099cf56bd33e5b3e9be65310036.out
@@ -0,0 +1,10 @@
+ SELECT instr(lower(abc), '123')
+ sql_keyword ------
+ sql_func -----------------------
+ sql_ident -----
+ sql_func ---------
+ sql_ident -----
+ sql_ident ---
+ sql_comma -
+ sql_string -----
+instr: Finds the first occurrence of the needle within the haystack and returns the number of prior characters plus 1, or 0 if the needle was not found
diff --git a/test/expected/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.err b/test/expected/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.err
diff --git a/test/expected/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.out b/test/expected/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.out
new file mode 100644
index 0000000..304d8dc
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_c909647ed0e585002074f55c946f3033df1815b2.out
@@ -0,0 +1,6 @@
+ SELECT foo(bar())
+ sql_keyword ------
+ sql_func ---------
+ sql_ident ---
+ sql_func ----
+ sql_ident ---
diff --git a/test/expected/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.err b/test/expected/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.err
diff --git a/test/expected/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.out b/test/expected/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.out
new file mode 100644
index 0000000..654e434
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_ce0506ee7a12eb0f7b970522cc6a79180ecb20cc.out
@@ -0,0 +1,11 @@
+ SELECT * from vmw_log, regexp_capture(log_body, '--> /SessionStats/SessionPool/Session/(?<line>[abc]+)')
+ sql_keyword ------
+ sql_oper -
+ sql_keyword ----
+ sql_ident -------
+ sql_comma -
+ sql_func --------------------------------------------------------------------------------
+ sql_ident --------------
+ sql_ident --------
+ sql_comma -
+ sql_string -------------------------------------------------------
diff --git a/test/expected/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.err b/test/expected/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.err
diff --git a/test/expected/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.out b/test/expected/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.out
new file mode 100644
index 0000000..2ee5ee2
--- /dev/null
+++ b/test/expected/test_sql_anno.sh_f3c64191d6016767a5857fbb1bad26548586bb96.out
@@ -0,0 +1,10 @@
+ SELECT * FROM (SELECT foo, bar FROM baz)
+ sql_keyword ------
+ sql_oper -
+ sql_keyword ----
+ sql_keyword ------
+ sql_ident ---
+ sql_comma -
+ sql_ident ---
+ sql_keyword ----
+ sql_ident ---
diff --git a/test/expected/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.err b/test/expected/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.err
diff --git a/test/expected/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.out b/test/expected/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.out
new file mode 100644
index 0000000..36b0c2b
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_077cab6e271c914daf5b221cc512853077891f35.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column 'fe80::a85f:80b4:5cbe:8691' = 'fe80:0000:0000:0000:a85f:80b4:5cbe:8691' collate ipaddress: 1
diff --git a/test/expected/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.err b/test/expected/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.err
diff --git a/test/expected/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.out b/test/expected/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.out
new file mode 100644
index 0000000..cf84c47
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_0ce56741d3c34af274c8ddb4b90c4e5749d05971.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column 'info' collate loglevel between 'trace' and 'fatal': 1
diff --git a/test/expected/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.err b/test/expected/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.err
diff --git a/test/expected/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.out b/test/expected/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.out
new file mode 100644
index 0000000..187dc99
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_180ad44fe073cc9642da642af1f442adfd98ec62.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column 'file10.txt' < 'file2.txt' collate naturalcase: 0
diff --git a/test/expected/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.err b/test/expected/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.err
diff --git a/test/expected/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.out b/test/expected/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.out
new file mode 100644
index 0000000..4ecbd26
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_2230714a0b2ab6aca9ddfe686734f313cef5a96b.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column '::ffff:192.168.1.10' = '192.168.1.10' collate ipaddress: 1
diff --git a/test/expected/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.err b/test/expected/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.err
diff --git a/test/expected/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.out b/test/expected/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.out
new file mode 100644
index 0000000..4bac183
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_68515cfd0a50880f6dfc8f9810c9e761493ebb12.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column '192.168.1.10' < '192.168.1.2' collate ipaddress: 0
diff --git a/test/expected/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.err b/test/expected/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.err
diff --git a/test/expected/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.out b/test/expected/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.out
new file mode 100644
index 0000000..90b4acb
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_6de2a86c53883ec4430b98edd06b0c0cdf23e741.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column '192.168.1.10' < '192.168.1.2': 1
diff --git a/test/expected/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.err b/test/expected/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.err
diff --git a/test/expected/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.out b/test/expected/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.out
new file mode 100644
index 0000000..2f6aec0
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_918178c6dd9d70d0432ededfde5af5e53c094385.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column '192.168.1.10' < '192.168.1.12' collate ipaddress: 1
diff --git a/test/expected/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.err b/test/expected/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.err
diff --git a/test/expected/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.out b/test/expected/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.out
new file mode 100644
index 0000000..913c6b3
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_c76a24a209987e4c668c87588c12b8f34294b144.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column '' < '192.168.1.2' collate ipaddress: 1
diff --git a/test/expected/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.err b/test/expected/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.err
diff --git a/test/expected/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.out b/test/expected/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.out
new file mode 100644
index 0000000..2718d82
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_cacb045d2bce6dc298c4da3d96bdc34dab2404df.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column 'file10.txt' < 'file2.txt': 1
diff --git a/test/expected/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.err b/test/expected/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.err
diff --git a/test/expected/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.out b/test/expected/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.out
new file mode 100644
index 0000000..5b48b4e
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_cae4bc239c924bbc05a0b099b63f0e3af7560976.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column 'w' < 'e' collate loglevel: 1
diff --git a/test/expected/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.err b/test/expected/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.err
diff --git a/test/expected/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.out b/test/expected/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.out
new file mode 100644
index 0000000..bcfa7cb
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_d4e3c9f7a38458726900731d2b71c104d591ef14.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column 'h9.example.com' < 'h10.example.com' collate ipaddress: 1
diff --git a/test/expected/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.err b/test/expected/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.err
diff --git a/test/expected/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.out b/test/expected/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.out
new file mode 100644
index 0000000..945e0ee
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_d5c8f7ab91c3dbe46add7e08f532b17797d9975c.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column '192.168.1.2' > '' collate ipaddress: 1
diff --git a/test/expected/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.err b/test/expected/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.err
diff --git a/test/expected/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.out b/test/expected/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.out
new file mode 100644
index 0000000..d6a7338
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_eb2c424733ce978d1b6d1dcb93d6e45af7c8fa96.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column 'e' < 'w' collate loglevel: 0
diff --git a/test/expected/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.err b/test/expected/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.err
diff --git a/test/expected/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.out b/test/expected/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.out
new file mode 100644
index 0000000..fc31b19
--- /dev/null
+++ b/test/expected/test_sql_coll_func.sh_f045e94d921bfcfbded83ee681bf11445a99ff6d.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column '192.168.1.2' < 'fe80::a85f:80b4:5cbe:8691' collate ipaddress: 1
diff --git a/test/expected/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.err b/test/expected/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.err
diff --git a/test/expected/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.out b/test/expected/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.out
new file mode 100644
index 0000000..b95b567
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_109ff42de817b56a9082f605f63af71c0db8c9d7.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column joinpath(): (null)
diff --git a/test/expected/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.err b/test/expected/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.err
diff --git a/test/expected/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.out b/test/expected/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.out
new file mode 100644
index 0000000..cf84c59
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_17b09f79bfcac1762153ec9650fb1e545a24d8a3.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column basename('/'): /
diff --git a/test/expected/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.err b/test/expected/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.err
diff --git a/test/expected/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.out b/test/expected/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.out
new file mode 100644
index 0000000..7271fd2
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_18ddc138b263dd06f3fe81fec05bc4330caffef7.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column basename('/foo'): foo
diff --git a/test/expected/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.err b/test/expected/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.err
diff --git a/test/expected/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.out b/test/expected/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.out
new file mode 100644
index 0000000..bace683
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_20a76db446a0a558dcbdf41033f97d4a22ca1bfa.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column dirname('foo'): .
diff --git a/test/expected/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.err b/test/expected/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.err
diff --git a/test/expected/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.out b/test/expected/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.out
new file mode 100644
index 0000000..983fcf5
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_2c3f66e78deb8721b1d1fe5a787e9958895401d7.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column dirname('/foo//'): /
diff --git a/test/expected/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.err b/test/expected/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.err
diff --git a/test/expected/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.out b/test/expected/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.out
new file mode 100644
index 0000000..77491a9
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_3ed11101a413e47c3dfe219557b7a6df04a64253.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column dirname('foo//'): .
diff --git a/test/expected/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.err b/test/expected/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.err
diff --git a/test/expected/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.out b/test/expected/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.out
new file mode 100644
index 0000000..6e4accc
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_469380561dccd79c7249562067107c330838eaad.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column dirname(''): .
diff --git a/test/expected/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.err b/test/expected/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.err
diff --git a/test/expected/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.out b/test/expected/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.out
new file mode 100644
index 0000000..a2f9057
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_54b004f301907860d360434b37fd6c81fcc12f99.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column basename('foo'): foo
diff --git a/test/expected/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.err b/test/expected/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.err
diff --git a/test/expected/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.out b/test/expected/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.out
new file mode 100644
index 0000000..49e73dc
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_73df81c6889d1f06fb3f3b6bf30c6046b3f52c8b.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column joinpath('foo', 'bar', 'baz', '/root'): /root
diff --git a/test/expected/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.err b/test/expected/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.err
diff --git a/test/expected/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.out b/test/expected/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.out
new file mode 100644
index 0000000..8a5ccfc
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_74ca242a126316bcb82ccefd9369f9e43b7fd2e1.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column joinpath('foo'): foo
diff --git a/test/expected/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.err b/test/expected/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.err
diff --git a/test/expected/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.out b/test/expected/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.out
new file mode 100644
index 0000000..b6bb6f9
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_7b116cb0ab7a28b866e0d2b80fe8ef0cd25f2aa3.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column dirname('/'): /
diff --git a/test/expected/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.err b/test/expected/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.err
diff --git a/test/expected/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.out b/test/expected/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.out
new file mode 100644
index 0000000..fcf9b4c
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_7b5d7dd8d0003ab83e3e5cb0a5ce802fe9a0e3b3.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column joinpath('foo', 'bar', 'baz'): foo/bar/baz
diff --git a/test/expected/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.err b/test/expected/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.err
diff --git a/test/expected/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.out b/test/expected/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.out
new file mode 100644
index 0000000..864b02c
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_917ffde411c1425e8a6addae0170900dcd553986.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column dirname('/foo'): /
diff --git a/test/expected/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.err b/test/expected/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.err
new file mode 100644
index 0000000..c8a3fd0
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"call to readlink(path) failed","attrs":[{"start":8,"end":16,"type":"role","value":47},{"start":17,"end":21,"type":"role","value":46},{"start":8,"end":22,"type":"role","value":60}]},"reason":{"str":"unable to stat path: non-existent-link -- No such file or directory","attrs":[]},"snippets":[],"help":{"str":"","attrs":[]}}
diff --git a/test/expected/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.out b/test/expected/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_9e2c0a90ce333365ff7354375f2c609bc27135c8.out
diff --git a/test/expected/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.err b/test/expected/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.err
diff --git a/test/expected/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.out b/test/expected/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.out
new file mode 100644
index 0000000..2f75397
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_a247b137e71124e496f1beab56c7fe85717c4199.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column basename(''): .
diff --git a/test/expected/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.err b/test/expected/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.err
diff --git a/test/expected/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.out b/test/expected/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.out
new file mode 100644
index 0000000..71c2d31
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_b66242975fd6ecb7260cd96ac29accaf4f4af6ae.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column dirname('foo///'): .
diff --git a/test/expected/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.err b/test/expected/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.err
diff --git a/test/expected/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.out b/test/expected/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.out
new file mode 100644
index 0000000..cfb2e26
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_c5d78cfbf5594cc27590277353c08a92e2497622.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column basename('/foo/'): foo
diff --git a/test/expected/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.err b/test/expected/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.err
diff --git a/test/expected/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.out b/test/expected/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.out
new file mode 100644
index 0000000..46a1c9c
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_cc402803bf14ee3673089c575f1af87220cb6a72.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column basename('foo/bar'): bar
diff --git a/test/expected/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.err b/test/expected/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.err
diff --git a/test/expected/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.out b/test/expected/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.out
new file mode 100644
index 0000000..01ff27e
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_cf307d87104e99a1858bb7c4f28ea3068340f188.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column basename('/foo///'): foo
diff --git a/test/expected/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.err b/test/expected/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.err
new file mode 100644
index 0000000..aa68134
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"call to realpath(path) failed","attrs":[{"start":8,"end":16,"type":"role","value":47},{"start":17,"end":21,"type":"role","value":46},{"start":8,"end":22,"type":"role","value":60}]},"reason":{"str":"Could not get real path for non-existent-path -- No such file or directory","attrs":[]},"snippets":[],"help":{"str":"","attrs":[]}}
diff --git a/test/expected/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.out b/test/expected/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_cf670dfa1ae7ac5a074baa642068c6d26ac8e096.out
diff --git a/test/expected/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.err b/test/expected/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.err
diff --git a/test/expected/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.out b/test/expected/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.out
new file mode 100644
index 0000000..0d744ab
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_d51ad77cd67a2a691838c9d95142638df1c07360.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column dirname('/foo/bar'): /foo
diff --git a/test/expected/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.err b/test/expected/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.err
diff --git a/test/expected/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.out b/test/expected/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.out
new file mode 100644
index 0000000..6306be6
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_e24cf3f35643f945392e7d7a4ca82fea98b4519e.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column basename('//'): /
diff --git a/test/expected/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.err b/test/expected/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.err
diff --git a/test/expected/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.out b/test/expected/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.out
new file mode 100644
index 0000000..00c5d02
--- /dev/null
+++ b/test/expected/test_sql_fs_func.sh_f31f240313ddec806aa6f353ceed707dfd9aaf16.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column readlink('sql_fs_readlink_test.lnk'): sql_fs_readlink_test
diff --git a/test/expected/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.err b/test/expected/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.err
diff --git a/test/expected/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.out b/test/expected/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_026dd9752b6101e0791689d3a2026f7e517e36f5.out
diff --git a/test/expected/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.err b/test/expected/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.err
diff --git a/test/expected/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.out b/test/expected/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.out
new file mode 100644
index 0000000..7179021
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_1614ebb5e2e83bab11023354dea8a0885ddf64b4.out
@@ -0,0 +1,3 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters log_msg_format 
+  1  <NULL> 2009-07-20 22:59:29.000  3000 error   0  <NULL>  <NULL>  <NULL>  
+ 3  <NULL> 2013-02-15 06:00:31.000  112777262000 error   0  <NULL>  <NULL>  <NULL>  
diff --git a/test/expected/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.err b/test/expected/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.err
diff --git a/test/expected/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.out b/test/expected/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.out
new file mode 100644
index 0000000..5467779
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_541a8e35f34a206e340a3880128b6ce137847872.out
@@ -0,0 +1,5 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters  c_ip cs_method cs_referer cs_uri_query  cs_uri_stem cs_user_agent cs_username cs_version sc_bytes sc_status cs_host  log_unique_path 
+  0  <NULL> 2009-07-20 22:59:26.000  0 info   0  <NULL>  <NULL>  <NULL> 192.168.202.254 GET  -   <NULL> /vmw/cgi/tramp  gPXE/0.9.7  -  HTTP/1.0   134  200  <NULL> logfile_access_log.0 
+   1 <NULL> 2009-07-20 22:59:29.000 3000 error 0 <NULL> <NULL> <NULL> 192.168.202.254 GET - <NULL> /vmw/vSphere/default/vmkboot.gz gPXE/0.9.7 - HTTP/1.0 46210 404 <NULL> logfile_access_log.0
+  2  <NULL> 2009-07-20 22:59:29.000  0 info   0  <NULL>  <NULL>  <NULL> 192.168.202.254 GET  -   <NULL> /vmw/vSphere/default/vmkernel.gz gPXE/0.9.7  -  HTTP/1.0   78929  200  <NULL> logfile_access_log.0 
+ 3  <NULL> 2013-02-15 06:00:31.000  112777262000 error   0  <NULL>  <NULL>  <NULL> 10.112.81.15  <NULL>  -   <NULL> <NULL>  -  -  <NULL>   0  400  <NULL> logfile_access_log.1
diff --git a/test/expected/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.err b/test/expected/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.err
diff --git a/test/expected/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.out b/test/expected/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.out
new file mode 100644
index 0000000..823c4d0
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_59a1497c13a5e09bc8f95ef02552b2835ebea6e5.out
@@ -0,0 +1,2 @@
+$id $parent $notused  replace($detail, 'SCAN TABLE', 'SCAN') 
+ 2  0  0 SCAN all_logs VIRTUAL TABLE INDEX 1:SEARCH all_logs USING log_level < ? 
diff --git a/test/expected/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.err b/test/expected/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.err
diff --git a/test/expected/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.out b/test/expected/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.out
new file mode 100644
index 0000000..cb2aac6
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_69fd19d56a8cd1fc9c7eb9351270eabb491f8233.out
@@ -0,0 +1,5 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters log_msg_format log_format 
+ 0  <NULL> 2009-07-20 22:59:26.000  0 info   0  <NULL>  <NULL>  <NULL>   access_log 
+  1 <NULL> 2009-07-20 22:59:29.000 3000 error 0 <NULL> <NULL> <NULL> access_log
+ 2  <NULL> 2009-07-20 22:59:29.000  0 info   0  <NULL>  <NULL>  <NULL>   access_log 
+ 3  <NULL> 2013-02-15 06:00:31.000  112777262000 error   0  <NULL>  <NULL>  <NULL>   access_log
diff --git a/test/expected/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.err b/test/expected/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.err
diff --git a/test/expected/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.out b/test/expected/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.out
new file mode 100644
index 0000000..ee48805
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_6f707b6e856dbaab6f95e7e89b98dc3652021f85.out
@@ -0,0 +1,3 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters log_msg_format 
+ 0  <NULL> 2009-07-20 22:59:26.000  0 info   0  <NULL>  <NULL>  <NULL>  
+ 2 <NULL> 2009-07-20 22:59:29.000 0 info 0 <NULL> <NULL> <NULL>
diff --git a/test/expected/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.err b/test/expected/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.err
diff --git a/test/expected/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.out b/test/expected/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.out
new file mode 100644
index 0000000..2a91985
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_b615b6737b1e0d383c8ce4a1db56332f11dbc158.out
@@ -0,0 +1,2 @@
+$id $parent $notused  replace($detail, 'SCAN TABLE', 'SCAN') 
+ 2  0  0 SCAN all_logs VIRTUAL TABLE INDEX 1:SEARCH all_logs USING log_format = ? 
diff --git a/test/expected/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.err b/test/expected/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.err
diff --git a/test/expected/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.out b/test/expected/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.out
new file mode 100644
index 0000000..82958af
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_dab07d8de7728752ae938a174468d75e85f3ae7e.out
@@ -0,0 +1,2 @@
+$id $parent $notused  replace($detail, 'SCAN TABLE', 'SCAN') 
+ 2  0  0 SCAN access_log VIRTUAL TABLE INDEX 1:SEARCH access_log USING log_path GLOB ? 
diff --git a/test/expected/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.err b/test/expected/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.err
diff --git a/test/expected/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.out b/test/expected/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.out
new file mode 100644
index 0000000..16c7c37
--- /dev/null
+++ b/test/expected/test_sql_indexes.sh_f7681c234d4f60df16c997a05163aeb058c52870.out
@@ -0,0 +1,5 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters log_msg_format 
+ 0  <NULL> 2009-07-20 22:59:26.000  0 info   0  <NULL>  <NULL>  <NULL>  
+  1 <NULL> 2009-07-20 22:59:29.000 3000 error 0 <NULL> <NULL> <NULL>
+ 2  <NULL> 2009-07-20 22:59:29.000  0 info   0  <NULL>  <NULL>  <NULL>  
+ 3  <NULL> 2013-02-15 06:00:31.000  112777262000 error   0  <NULL>  <NULL>  <NULL>  
diff --git a/test/expected/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.err b/test/expected/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.err
diff --git a/test/expected/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.out b/test/expected/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.out
new file mode 100644
index 0000000..451a2fa
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_017d24148f3e28f719429b709f4aa5478f458443.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('[null, true, 20, 30, 40]', '/0'): (null)
diff --git a/test/expected/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.err b/test/expected/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.err
new file mode 100644
index 0000000..9654daa
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.err
@@ -0,0 +1,4 @@
+error: sqlite3_exec failed -- parse error: premature EOF
+ [123, true
+ (right here) ------^
+
diff --git a/test/expected/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.out b/test/expected/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_026077f4d573ee034467065b7e4f1878bdd4e2f2.out
diff --git a/test/expected/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.err b/test/expected/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.err
diff --git a/test/expected/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.out b/test/expected/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.out
new file mode 100644
index 0000000..531034e
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_191436b38db80b1dd9e7e0814c31c5fa7239dc51.out
@@ -0,0 +1,3 @@
+Row 0:
+ Column id: 1
+ Column stack: {"key1":10,"key2":20,"key3":30}
diff --git a/test/expected/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.err b/test/expected/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.err
diff --git a/test/expected/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.out b/test/expected/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.out
new file mode 100644
index 0000000..449f615
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_1a74914cbf12fcd5c06935b992f6355acdbcf2d8.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('[null, true, 20, 30, 4.0]', '/4'): 4.0
diff --git a/test/expected/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.err b/test/expected/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.err
diff --git a/test/expected/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.out b/test/expected/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.out
new file mode 100644
index 0000000..fb37801
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_1c1a2d438d2bde95abd9a859d113c3661e650a36.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('[null, true, 20, 30, 40]', '/0/foo'): (null)
diff --git a/test/expected/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.err b/test/expected/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.err
diff --git a/test/expected/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.out b/test/expected/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.out
new file mode 100644
index 0000000..4b8f538
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_238417283b8e5db23c992f966e3f106bd178f7d0.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('[null, true, 20, 30, 40, {"msg": "Hello"}]', ''): [null,true,20,30,40,{"msg":"Hello"}]
diff --git a/test/expected/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.err b/test/expected/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.err
diff --git a/test/expected/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.out b/test/expected/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.out
new file mode 100644
index 0000000..dd75a1d
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_32459ba8e8bb9a1d9e63b6c67059d7f065cf4301.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('[null, true, 20, 30, 40]', '/abc', 1): 1
diff --git a/test/expected/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.err b/test/expected/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.err
diff --git a/test/expected/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.out b/test/expected/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.out
new file mode 100644
index 0000000..3d09bd6
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_39c13797278d765c027d3581a0b6e0574f5c56eb.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_contains('4', 4): 1
diff --git a/test/expected/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.err b/test/expected/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.err
diff --git a/test/expected/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.out b/test/expected/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.out
new file mode 100644
index 0000000..0973745
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_3cf4b66d40c4b1979ff14a9eccad8bd5ac48151c.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column res: 0
diff --git a/test/expected/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.err b/test/expected/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.err
diff --git a/test/expected/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.out b/test/expected/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.out
new file mode 100644
index 0000000..14f3b8e
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_4192f378e320cb3f2c3c228b63ec65de92044704.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_contains('4', 2): 0
diff --git a/test/expected/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.err b/test/expected/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.err
new file mode 100644
index 0000000..73b5594
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- Uneven number of arguments to json_group_object(), expecting key and value pairs
diff --git a/test/expected/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.out b/test/expected/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_57c3aecdced547b837177ab02d3776361363e48d.out
diff --git a/test/expected/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.err b/test/expected/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.err
diff --git a/test/expected/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.out b/test/expected/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.out
new file mode 100644
index 0000000..414df03
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_5b4a95677a1fc7d11f4b87d92165f56a60a65828.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_concat(NULL, json('{"abc": 1}')): [{"abc":1}]
diff --git a/test/expected/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.err b/test/expected/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.err
new file mode 100644
index 0000000..f784e9c
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"call to json_contains(json, value) failed","attrs":[{"start":8,"end":21,"type":"role","value":47},{"start":22,"end":26,"type":"role","value":46},{"start":28,"end":33,"type":"role","value":46},{"start":8,"end":34,"type":"role","value":60}]},"reason":{"str":"parse error: premature EOF\n [\"hi\", \"bye\", \"solong]\n (right here) ------^","attrs":[]},"snippets":[],"help":{"str":"","attrs":[]}}
diff --git a/test/expected/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.out b/test/expected/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_5f2feef079a51410e1f8661bfe92da1c3277f665.out
diff --git a/test/expected/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.err b/test/expected/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.err
diff --git a/test/expected/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.out b/test/expected/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.out
new file mode 100644
index 0000000..69e1a5e
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_61417198a652aab93e9495b6e8cf3a634af175c6.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_contains('', 4): 0
diff --git a/test/expected/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.err b/test/expected/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.err
diff --git a/test/expected/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.out b/test/expected/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.out
new file mode 100644
index 0000000..9a729a8
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_79ab816ac01c9902ddbb0f6f20392ab2f2cd6172.out
@@ -0,0 +1,3 @@
+Row 0:
+ Column id: 1
+ Column stack: {"1":10,"2":[1,2,3],"3":30.5}
diff --git a/test/expected/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.err b/test/expected/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.err
diff --git a/test/expected/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.out b/test/expected/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.out
new file mode 100644
index 0000000..c70a186
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_7c01aaf09078aaa3f23d127f9e03a317dca066de.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_contains('"hi"', 'hi'): 1
diff --git a/test/expected/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.err b/test/expected/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.err
diff --git a/test/expected/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.out b/test/expected/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.out
new file mode 100644
index 0000000..57ecd1c
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_80c97b22084a06fd765ad22c935616c578968d07.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('[null, true, 20, 30, 40]', '/abc'): (null)
diff --git a/test/expected/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.err b/test/expected/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.err
diff --git a/test/expected/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.out b/test/expected/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.out
new file mode 100644
index 0000000..375f86a
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_83d8615c9ce5dfab5e4373570c1b68b8608155f5.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_contains('"hi"', 'hi there'): 0
diff --git a/test/expected/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.err b/test/expected/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.err
diff --git a/test/expected/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.out b/test/expected/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.out
new file mode 100644
index 0000000..3a26b62
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_8cae9740ddfd6ba4c865fca0117b7bea3bb556e5.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_concat(json('[null, true, 0]'), 1.0, 2.0): [null,true,0,1.0,2.0]
diff --git a/test/expected/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.err b/test/expected/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.err
diff --git a/test/expected/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.out b/test/expected/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.out
new file mode 100644
index 0000000..72ded4f
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_8e229f1b5fa3d3803e9db2f295a8d1a490e1b3db.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('[null, true, 20, 30, 40, {"msg": "Hello"}]', '/5/msg'): Hello
diff --git a/test/expected/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.err b/test/expected/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.err
new file mode 100644
index 0000000..da16df8
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- expecting JSON value and pointer
diff --git a/test/expected/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.out b/test/expected/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_8e3724c90bf96dff5d8ba3cfaf4b7e2eaa9e5f66.out
diff --git a/test/expected/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.err b/test/expected/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.err
diff --git a/test/expected/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.out b/test/expected/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.out
new file mode 100644
index 0000000..67ca681
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_93ba3ba52b0dd2d5a3ba43bcb7c3638c05ecfe75.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('[null, true, 20, 30, 40]', '/3'): 30
diff --git a/test/expected/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.err b/test/expected/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.err
diff --git a/test/expected/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.out b/test/expected/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.out
new file mode 100644
index 0000000..df74174
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_97aa53b581838f5875fe2beda8d1cb245a24f3d6.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_concat(NULL, NULL): [null]
diff --git a/test/expected/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.err b/test/expected/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.err
diff --git a/test/expected/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.out b/test/expected/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.out
new file mode 100644
index 0000000..6f7f8d0
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_98a83bc899a78c04d1fdb390b2c1e403c35428c7.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column res: 1
diff --git a/test/expected/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.err b/test/expected/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.err
diff --git a/test/expected/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.out b/test/expected/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.out
new file mode 100644
index 0000000..f6f983c
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_98ce02dff32d955466524bb167fa45fdf8591788.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_contains(NULL, 4): 0
diff --git a/test/expected/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.err b/test/expected/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.err
diff --git a/test/expected/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.out b/test/expected/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.out
new file mode 100644
index 0000000..7019ce5
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_9ab4f51486d7cc99c584721bf0e50c223dac4f18.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column typeof(jget('[null, true, 20, 30, 40]', '/3')): integer
diff --git a/test/expected/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.err b/test/expected/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.err
diff --git a/test/expected/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.out b/test/expected/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.out
new file mode 100644
index 0000000..1c9c316
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_9d260ed24b28579ef1dbed25b10c42741e52b023.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_group_array(column1): []
diff --git a/test/expected/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.err b/test/expected/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.err
diff --git a/test/expected/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.out b/test/expected/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.out
new file mode 100644
index 0000000..776dfd6
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_9fbfe3c93467666c45b643f3b8ba990a294c17ff.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('4', null): 4
diff --git a/test/expected/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.err b/test/expected/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.err
diff --git a/test/expected/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.out b/test/expected/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.out
new file mode 100644
index 0000000..931d5dc
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_a4ffc64f89cf9917fbc918227fd3c05e54d9e8b5.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_contains('[[0]]', 0): 0
diff --git a/test/expected/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.err b/test/expected/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.err
diff --git a/test/expected/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.out b/test/expected/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.out
new file mode 100644
index 0000000..f150fde
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_a5e179607645aefce14b9fd12ddef34107afe337.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column stack: [10,null,"hello"]
diff --git a/test/expected/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.err b/test/expected/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.err
diff --git a/test/expected/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.out b/test/expected/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.out
new file mode 100644
index 0000000..68ffe43
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_b2fc37822e29f7f59497a02a8968c680b545ee1d.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_concat(json('["tag0"]'), 'tag1', 'tag2'): ["tag0","tag1","tag2"]
diff --git a/test/expected/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.err b/test/expected/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.err
diff --git a/test/expected/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.out b/test/expected/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.out
new file mode 100644
index 0000000..e2c7330
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_bbd979ed74b46ae1696ed7312a48a436bcf99ec0.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('[null, true, 20, 30, 40, {"msg": "Hello"}]', '/5'): {"msg":"Hello"}
diff --git a/test/expected/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.err b/test/expected/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.err
diff --git a/test/expected/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.out b/test/expected/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.out
new file mode 100644
index 0000000..ae0858b
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_c1ae603d969a5b106328287523c0ddfed07146ad.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column json_contains('null', NULL): 1
diff --git a/test/expected/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.err b/test/expected/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.err
new file mode 100644
index 0000000..456ef7a
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"call to json_concat(json, value, ...) failed","attrs":[{"start":8,"end":19,"type":"role","value":47},{"start":20,"end":24,"type":"role","value":46},{"start":26,"end":31,"type":"role","value":46},{"start":8,"end":37,"type":"role","value":60}]},"reason":{"str":"Invalid JSON: parse error: premature EOF\n [null,\n (right here) ------^","attrs":[]},"snippets":[],"help":{"str":"","attrs":[]}}
diff --git a/test/expected/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.out b/test/expected/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_e0ab80f50fb008700ab6cfb90694ed014d40e44b.out
diff --git a/test/expected/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.err b/test/expected/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.err
diff --git a/test/expected/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.out b/test/expected/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.out
new file mode 100644
index 0000000..e187559
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_ebafb98307f307ae8d8ab6921c32929aab3a1a16.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column typeof(jget('[null, true, 20, 30, 4.0]', '/4')): real
diff --git a/test/expected/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.err b/test/expected/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.err
diff --git a/test/expected/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.out b/test/expected/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.out
new file mode 100644
index 0000000..4cdd719
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_ee36fbea10a33ca106a211feb05d61ecf8e74634.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column jget('4', ''): 4
diff --git a/test/expected/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.err b/test/expected/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.err
diff --git a/test/expected/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.out b/test/expected/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.out
new file mode 100644
index 0000000..5e3bd8b
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_f1cbc70771cc75520f807261eac3a88dc2d8fe6b.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column stack: [10,100,20,200,30,300]
diff --git a/test/expected/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.err b/test/expected/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.err
diff --git a/test/expected/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.out b/test/expected/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.out
new file mode 100644
index 0000000..95c2d9b
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_f34205b59e04f261897ad89f659595c743a18ca9.out
@@ -0,0 +1,3 @@
+Row 0:
+ Column id: 1
+ Column stack: {"1":10,"2":null,"3":30.5}
diff --git a/test/expected/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.err b/test/expected/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.err
new file mode 100644
index 0000000..e8eef68
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.err
@@ -0,0 +1,4 @@
+error: sqlite3_exec failed -- parse error: premature EOF
+ [null, true, 20, 30, 40
+ (right here) ------^
+
diff --git a/test/expected/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.out b/test/expected/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_json_func.sh_f34f5dfa938a1ac7721f924beb16bbceec127a1b.out
diff --git a/test/expected/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.err b/test/expected/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.err
diff --git a/test/expected/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.out b/test/expected/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.out
new file mode 100644
index 0000000..19aa1c4
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_03257c56e85558aa0cc925b68d3af962afc25125.out
@@ -0,0 +1,4 @@
+match_index capture_index capture_name capture_count range_start range_stop content 
+ 0  0  <NULL>  3  1  9 abc=def; 
+ 0 1 <NULL> 3 1 4 abc
+ 0  2  <NULL>  3  5  8 def 
diff --git a/test/expected/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.err b/test/expected/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.err
diff --git a/test/expected/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.out b/test/expected/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.out
new file mode 100644
index 0000000..4917183
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_51293df041b6969ccecc60204dce3676d0fb006d.out
@@ -0,0 +1,2 @@
+match_index  content 
+ 0 {"key":"foo","value":4670} 
diff --git a/test/expected/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.err b/test/expected/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.err
diff --git a/test/expected/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.out b/test/expected/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.out
new file mode 100644
index 0000000..78f05ff
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_b841a0c09601e2419eeb99e85f7e286c889e4801.out
@@ -0,0 +1,27 @@
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,log_hostname,log_msgid,log_pid,log_pri,log_procname,log_struct,log_syslog_tag,syslog_version,match_index,content
+2,<NULL>,2022-08-16 00:32:15.000,199000,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,0,"{""value"":""com.apple.cdscheduler""}"
+2,<NULL>,2022-08-16 00:32:15.000,199000,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,1,"{""value"":"" claims selected messages.\n\tThose messages may not appear in standard system log files or in the ASL database.""}"
+5,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,0,"{""value"":""com.apple.install""}"
+5,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,1,"{""value"":"" claims selected messages.\n\tThose messages may not appear in standard system log files or in the ASL database.""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,0,"{""value"":""com.apple.authd""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,1,"{""value"":"" sharing output destination ""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,2,"{""value"":""/var/log/asl""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,3,"{""value"":"" with ASL Module ""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,4,"{""value"":""com.apple.asl""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,5,"{""value"":"".\n\tOutput parameters from ASL Module ""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,6,"{""value"":""com.apple.asl""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,7,"{""value"":"" override any specified in ASL Module ""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,8,"{""value"":""com.apple.authd""}"
+8,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,9,"{""value"":"".""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,0,"{""value"":""com.apple.authd""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,1,"{""value"":"" sharing output destination ""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,2,"{""value"":""/var/log/system.log""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,3,"{""value"":"" with ASL Module ""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,4,"{""value"":""com.apple.asl""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,5,"{""value"":"".\n\tOutput parameters from ASL Module ""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,6,"{""value"":""com.apple.asl""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,7,"{""value"":"" override any specified in ASL Module ""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,8,"{""value"":""com.apple.authd""}"
+11,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,9,"{""value"":"".""}"
+14,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,0,"{""value"":""com.apple.authd""}"
+14,<NULL>,2022-08-16 00:32:15.000,0,info,0,<NULL>,<NULL>,<NULL>,Tims-Air,<NULL>,314,<NULL>,syslogd,<NULL>,syslogd[314],<NULL>,1,"{""value"":"" claims selected messages.\n\tThose messages may not appear in standard system log files or in the ASL database.""}"
diff --git a/test/expected/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.err b/test/expected/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.err
diff --git a/test/expected/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.out b/test/expected/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.out
new file mode 100644
index 0000000..655ce13
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_bbd1128cf61a9af8f9dc937b46217443f42e1a7a.out
@@ -0,0 +1,2 @@
+match_index  content 
+ 0 {"key":"foo","value":"123e"} 
diff --git a/test/expected/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.err b/test/expected/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.err
new file mode 100644
index 0000000..79d28a5
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.err
@@ -0,0 +1,9 @@
+✘ error: unable to parse flags
+ reason: invalid JSON
+ |  reason: lexical error: invalid char in json text.
+ |   |  {abc
+ |   |  (right here) ------^
+ |   --> flags:1
+ |   | {abc
+ --> command-option:1
+ | ;SELECT * from regexp_capture_into_json('foo=0x123e;', '(?<key>\w+)=(?<value>[^;]+)', '{abc')
diff --git a/test/expected/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.out b/test/expected/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_d42e1fcfe6d42394f79da84be2d37e62c4c0ea63.out
diff --git a/test/expected/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.err b/test/expected/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.err
diff --git a/test/expected/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.out b/test/expected/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.out
new file mode 100644
index 0000000..6e952fb
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_d61af17ff19d640ddfc879460910991825eedd05.out
@@ -0,0 +1,2 @@
+match_index  content 
+ 0 {"col_0":"abc","col_1":"def"} 
diff --git a/test/expected/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.err b/test/expected/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.err
diff --git a/test/expected/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.out b/test/expected/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.out
new file mode 100644
index 0000000..fef8e26
--- /dev/null
+++ b/test/expected/test_sql_regexp.sh_ed6e9f13f178def009ee58c2aeea8c3c70fdb580.out
@@ -0,0 +1,2 @@
+match_index  content 
+ 0 {"key":"foo","value":"0x123e"} 
diff --git a/test/expected/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.err b/test/expected/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.err
diff --git a/test/expected/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.out b/test/expected/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.out
new file mode 100644
index 0000000..d56bc20
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_1a0d872ebc492fcecb2e79a0993170d5fc771a5b.out
@@ -0,0 +1,2 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters  comp  opid  tid  user  item prc reason  req  sid  src  sub vpxa_update  line  file match_index  lro_id  entity  operation  SessionId  SessionSubId 
+ 2  <NULL> 2022-06-02 11:58:12.376  182 info   0  <NULL>  <NULL>  <NULL> <NULL> e3979f6 45709 <NULL> <NULL> vpxd <NULL> <NULL> <NULL> Originator@6876 vpxLro  <NULL> <NULL> <NULL>  0 lro-846064 SessionManager vim.SessionManager.sessionIsActive 52626140-422b-6287-b4e4-344192c6a01d 523e0a4b-6e83-6bcd-9342-22502dd89866 
diff --git a/test/expected/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.err b/test/expected/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.err
diff --git a/test/expected/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.out b/test/expected/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.out
new file mode 100644
index 0000000..c92cf49
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_3f5f74863d065418bca5a000e6ad3d9344635164.out
@@ -0,0 +1,12 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters match_index  name 
+ 2  <NULL> 2022-08-16 00:32:15.000  199000 info   0  <NULL>  <NULL>  <NULL>  0 com.apple.cdscheduler 
+ 5 <NULL> 2022-08-16 00:32:15.000 0 info 0 <NULL> <NULL> <NULL> 0 com.apple.install
+ 8  <NULL> 2022-08-16 00:32:15.000  0 info   0  <NULL>  <NULL>  <NULL>  0 com.apple.authd 
+ 8 <NULL> 2022-08-16 00:32:15.000 0 info 0 <NULL> <NULL> <NULL> 1 com.apple.asl
+ 8  <NULL> 2022-08-16 00:32:15.000  0 info   0  <NULL>  <NULL>  <NULL>  2 com.apple.asl 
+ 8 <NULL> 2022-08-16 00:32:15.000 0 info 0 <NULL> <NULL> <NULL> 3 com.apple.authd
+ 11  <NULL> 2022-08-16 00:32:15.000  0 info   0  <NULL>  <NULL>  <NULL>  0 com.apple.authd 
+ 11 <NULL> 2022-08-16 00:32:15.000 0 info 0 <NULL> <NULL> <NULL> 1 com.apple.asl
+ 11  <NULL> 2022-08-16 00:32:15.000  0 info   0  <NULL>  <NULL>  <NULL>  2 com.apple.asl 
+ 11 <NULL> 2022-08-16 00:32:15.000 0 info 0 <NULL> <NULL> <NULL> 3 com.apple.authd
+ 14  <NULL> 2022-08-16 00:32:15.000  0 info   0  <NULL>  <NULL>  <NULL>  0 com.apple.authd 
diff --git a/test/expected/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.err b/test/expected/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.err
diff --git a/test/expected/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.out b/test/expected/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.out
new file mode 100644
index 0000000..738eab3
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_5aaae556ecb1661602f176215e28f661d3404032.out
@@ -0,0 +1,4 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters match_index user pid cpu_pct mem_pct vsz rss tty stat start_time cpu_time  cmd  cmd_name cmd_args 
+ 0  <NULL> 2022-06-02 00:01:01.000  0 info   0  <NULL>  <NULL>  <NULL>  1 root  2  0  0  0  0 ?  S  Jun01  0:00  [kthreadd] [kthreadd]  <NULL> 
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  1 root  2  0  0  0  0 ?  S  Jun01  0:00  [kthreadd] [kthreadd]  <NULL>
+ 30  <NULL> 2022-06-02 00:03:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  1 root  2  0  0  0  0 ?  S  Jun01  0:00  [kthreadd] [kthreadd]  <NULL> 
diff --git a/test/expected/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.err b/test/expected/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.err
diff --git a/test/expected/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.out b/test/expected/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.out
new file mode 100644
index 0000000..326fcbd
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_df0fd242f57a96d40f466493938cda0789a094fa.out
@@ -0,0 +1,24 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters match_index user pid cpu_pct mem_pct  vsz rss tty stat start_time cpu_time  cmd  cmd_name  cmd_args 
+ 0  <NULL> 2022-06-02 00:01:01.000  0 info   0  <NULL>  <NULL>  <NULL>  0 root  1  0  0 158392 7792 ?  Ss  Jun01  0:14  /lib/systemd/systemd --switched-root --system --deserialize 16 /lib/systemd/systemd  --switched-root --system --deserialize 16 
+ 0 <NULL> 2022-06-02 00:01:01.000 0 info 0 <NULL> <NULL> <NULL> 1 root 2 0 0 0 0 ? S Jun01 0:00 [kthreadd] [kthreadd] <NULL>
+ 0  <NULL> 2022-06-02 00:01:01.000  0 info   0  <NULL>  <NULL>  <NULL>  2 root  3  0  0  0  0 ?  I<  Jun01  0:00  [rcu_gp]  [rcu_gp]  <NULL> 
+ 0 <NULL> 2022-06-02 00:01:01.000 0 info 0 <NULL> <NULL> <NULL> 3 root 4 0 0 0 0 ? I< Jun01 0:00 [rcu_par_gp] [rcu_par_gp] <NULL>
+ 0  <NULL> 2022-06-02 00:01:01.000  0 info   0  <NULL>  <NULL>  <NULL>  4 root  6  0  0  0  0 ?  I<  Jun01  0:00  [kworker/0:0H-kblockd]  [kworker/0:0H-kblockd] <NULL> 
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  0 root  1  0  0 158392 7792 ?  Ss  Jun01  0:14  /lib/systemd/systemd --switched-root --system --deserialize 16 /lib/systemd/systemd  --switched-root --system --deserialize 16
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  1 root  2  0  0  0  0 ?  S  Jun01  0:00  [kthreadd]  [kthreadd]  <NULL> 
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL> <NULL> <NULL> 2 root 3 0 0 0 0 ? I< Jun01 0:00 [rcu_gp] [rcu_gp] <NULL>
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  3 root  4  0  0  0  0 ?  I<  Jun01  0:00  [rcu_par_gp]  [rcu_par_gp]  <NULL> 
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL> <NULL> <NULL> 4 root 6 0 0 0 0 ? I< Jun01 0:00 [kworker/0:0H-kblockd] [kworker/0:0H-kblockd] <NULL>
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  5 root  8  0  0  0  0 ?  I<  Jun01  0:00  [mm_percpu_wq]  [mm_percpu_wq]  <NULL> 
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL> <NULL> <NULL> 6 root 9 0 0 0 0 ? S Jun01 0:00 [ksoftirqd/0] [ksoftirqd/0] <NULL>
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  7 root  10  0  0  0  0 ?  I  Jun01  0:23  [rcu_sched]  [rcu_sched]  <NULL> 
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL> <NULL> <NULL> 8 root 11 0 0 0 0 ? I Jun01 0:00 [rcu_bh] [rcu_bh] <NULL>
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  9 root  12  0  0  0  0 ?  S  Jun01  0:00  [migration/0]  [migration/0]  <NULL> 
+ 12  <NULL> 2022-06-02 00:02:01.000  60000 info   0  <NULL> <NULL> <NULL> 10 root 14 0 0 0 0 ? S Jun01 0:00 [cpuhp/0] [cpuhp/0] <NULL>
+ 30  <NULL> 2022-06-02 00:03:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  0 root  1  0  0 158392 7792 ?  Ss  Jun01  0:14  /lib/systemd/systemd --switched-root --system --deserialize 16 /lib/systemd/systemd  --switched-root --system --deserialize 16 
+ 30  <NULL> 2022-06-02 00:03:01.000  60000 info   0  <NULL> <NULL> <NULL> 1 root 2 0 0 0 0 ? S Jun01 0:00 [kthreadd] [kthreadd] <NULL>
+ 30  <NULL> 2022-06-02 00:03:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  2 root  3  0  0  0  0 ?  I<  Jun01  0:00  [rcu_gp]  [rcu_gp]  <NULL> 
+ 30  <NULL> 2022-06-02 00:03:01.000  60000 info   0  <NULL> <NULL> <NULL> 3 root 4 0 0 0 0 ? I< Jun01 0:00 [rcu_par_gp] [rcu_par_gp] <NULL>
+ 30  <NULL> 2022-06-02 00:03:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  4 root  6  0  0  0  0 ?  I<  Jun01  0:00  [kworker/0:0H-kblockd]  [kworker/0:0H-kblockd] <NULL> 
+ 30  <NULL> 2022-06-02 00:03:01.000  60000 info   0  <NULL> <NULL> <NULL> 5 root 8 0 0 0 0 ? I< Jun01 0:00 [mm_percpu_wq] [mm_percpu_wq] <NULL>
+ 30  <NULL> 2022-06-02 00:03:01.000  60000 info   0  <NULL>  <NULL>  <NULL>  6 root  9  0  0  0  0 ?  S  Jun01  0:00  [ksoftirqd/0]  [ksoftirqd/0]  <NULL> 
diff --git a/test/expected/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.err b/test/expected/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.err
diff --git a/test/expected/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.out b/test/expected/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.out
new file mode 100644
index 0000000..767d785
--- /dev/null
+++ b/test/expected/test_sql_search_table.sh_ef9373a76853f345d06234f6e0fe11b5d40da27b.out
@@ -0,0 +1,6 @@
+log_line log_part  log_time log_idle_msecs log_level log_mark log_comment log_tags log_filters  comp  opid  tid  user  item prc reason  req  sid  src  sub vpxa_update  line  file match_index  lro_id  entity  operation  SessionId  SessionSubId  log_body 
+ 0  <NULL> 2022-06-02 11:58:12.193  0 info   0  <NULL>  <NULL>  <NULL> <NULL> 7e1280cf  45715 <NULL> <NULL> vpxd <NULL> <NULL> <NULL> Originator@6876 vpxLro  <NULL> <NULL> <NULL>  0 lro-846063 SessionManager  vim.SessionManager.sessionIsActive  528e6e0c-246d-58b5-3234-278c6e0c5d0d 52c289ac-2563-48d5-8a8e-f178da022c0d [VpxLRO] -- BEGIN lro-846063 -- SessionManager -- vim.Sessio⋯8b5-3234-278c6e0c5d0d(52c289ac-2563-48d5-8a8e-f178da022c0d) 
+ 2  <NULL> 2022-06-02 11:58:12.376  182 info   0  <NULL>  <NULL>  <NULL> <NULL> e3979f6  45709 <NULL> <NULL> vpxd <NULL> <NULL> <NULL> Originator@6876 vpxLro  <NULL> <NULL> <NULL>  0 lro-846064 SessionManager  vim.SessionManager.sessionIsActive  52626140-422b-6287-b4e4-344192c6a01d 523e0a4b-6e83-6bcd-9342-22502dd89866 [VpxLRO] -- BEGIN lro-846064 -- SessionManager -- vim.Sessio⋯287-b4e4-344192c6a01d(523e0a4b-6e83-6bcd-9342-22502dd89866)
+ 4  <NULL> 2022-06-02 11:58:12.623  246 info   0  <NULL>  <NULL>  <NULL> <NULL> l3wrhr4o-cbf-h5:70001034-60 47524 <NULL> <NULL> vpxd <NULL> <NULL> <NULL> Originator@6876 vpxLro  <NULL> <NULL> <NULL>  0 lro-846066 ChangeLogCollector vim.cdc.ChangeLogCollector.waitForChanges 526861fc-0c28-1930-ae5e-d8c2772bf8c2 52a7a308-9646-c054-f1e7-16131c1a7db6 [VpxLRO] -- BEGIN lro-846066 -- ChangeLogCollector -- vim.c⋯1930-ae5e-d8c2772bf8c2(52a7a308-9646-c054-f1e7-16131c1a7db6) 
+ 6  <NULL> 2022-06-02 11:58:12.736  113 info   0  <NULL>  <NULL>  <NULL> <NULL> 499b440  48432 <NULL> <NULL> vpxd <NULL> <NULL> <NULL> Originator@6876 vpxLro  <NULL> <NULL> <NULL>  0 lro-846067 SessionManager vim.SessionManager.sessionIsActive 521fe9f6-d061-11a2-ac86-badb3c071373 524cba9b-2cc4-9b70-32e4-421452a404d7 [VpxLRO] -- BEGIN lro-846067 -- SessionManager -- vim.Sessio⋯1a2-ac86-badb3c071373(524cba9b-2cc4-9b70-32e4-421452a404d7)
+ 8  <NULL> 2022-06-02 11:58:12.740  4 info   0  <NULL>  <NULL>  <NULL> <NULL> 55a419df  48035 <NULL> <NULL> vpxd <NULL> <NULL> <NULL> Originator@6876 vpxLro  <NULL> <NULL> <NULL>  0 lro-846068 SessionManager  vim.SessionManager.sessionIsActive  52585600-b0bc-76b1-c4d5-4d7708671c5e 523b68ba-e312-9909-a3ca-39cc86aaf206 [VpxLRO] -- BEGIN lro-846068 -- SessionManager -- vim.Sessio⋯6b1-c4d5-4d7708671c5e(523b68ba-e312-9909-a3ca-39cc86aaf206) 
diff --git a/test/expected/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.err b/test/expected/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.err
diff --git a/test/expected/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.out b/test/expected/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.out
new file mode 100644
index 0000000..2c908c0
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_005b9365ac99596e539f47c9fe432668c209b21f.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column repl: abc=def
diff --git a/test/expected/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.err b/test/expected/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.err
diff --git a/test/expected/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.out b/test/expected/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.out
new file mode 100644
index 0000000..1d3dddd
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_04712488fe50554eb36d3ced80f9a033602f3daa.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column result: {"col_0":1}
diff --git a/test/expected/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.err b/test/expected/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.err
diff --git a/test/expected/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.out b/test/expected/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.out
new file mode 100644
index 0000000..e144653
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_0947bfe7ec626eaa0409a45b10fcbb634fb12eb7.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column parse_url('https://example.com/'): {"scheme":"https","user":null,"password":null,"host":"example.com","port":null,"path":"/","query":null,"parameters":null,"fragment":null}
diff --git a/test/expected/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.err b/test/expected/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.err
diff --git a/test/expected/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.out b/test/expected/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.out
new file mode 100644
index 0000000..efa08d6
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_11bcc5d32eabbedb6974f160dace9ef1ef0009e9.out
@@ -0,0 +1,64 @@
+Row 0:
+ Column match_index: 0
+ Column capture_index: 0
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 1
+ Column range_stop: 2
+ Column content: 1
+Row 1:
+ Column match_index: 0
+ Column capture_index: 1
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 1
+ Column range_stop: 2
+ Column content: 1
+Row 2:
+ Column match_index: 1
+ Column capture_index: 0
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 3
+ Column range_stop: 4
+ Column content: 2
+Row 3:
+ Column match_index: 1
+ Column capture_index: 1
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 3
+ Column range_stop: 4
+ Column content: 2
+Row 4:
+ Column match_index: 2
+ Column capture_index: 0
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 5
+ Column range_stop: 6
+ Column content: 3
+Row 5:
+ Column match_index: 2
+ Column capture_index: 1
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 5
+ Column range_stop: 6
+ Column content: 3
+Row 6:
+ Column match_index: 3
+ Column capture_index: 0
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 7
+ Column range_stop: 9
+ Column content: 45
+Row 7:
+ Column match_index: 3
+ Column capture_index: 1
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 7
+ Column range_stop: 9
+ Column content: 45
diff --git a/test/expected/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.err b/test/expected/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.err
diff --git a/test/expected/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.out b/test/expected/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.out
new file mode 100644
index 0000000..eea58f2
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_11d458fdadd00df1239a0eeaac049abb49ed212d.out
@@ -0,0 +1,198 @@
+anonymize(bro_id_resp_h)
+10.0.0.1
+10.0.0.2
+10.0.0.2
+10.0.0.2
+10.0.0.2
+10.0.0.2
+10.0.0.2
+10.0.0.3
+10.0.0.4
+10.0.0.1
+10.0.0.4
+10.0.0.4
+10.0.0.5
+10.0.0.4
+10.0.0.4
+10.0.0.1
+10.0.0.6
+10.0.0.4
+10.0.0.7
+10.0.0.8
+10.0.0.8
+10.0.0.8
+10.0.0.8
+10.0.0.8
+10.0.0.8
+10.0.0.8
+10.0.0.8
+10.0.0.9
+10.0.0.10
+10.0.0.5
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.5
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.5
+10.0.0.10
+10.0.0.6
+10.0.0.12
+10.0.0.1
+10.0.0.1
+10.0.0.6
+10.0.0.11
+10.0.0.11
+10.0.0.13
+10.0.0.6
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.13
+10.0.0.6
+10.0.0.6
+10.0.0.6
+10.0.0.6
+10.0.0.6
+10.0.0.6
+10.0.0.6
+10.0.0.6
+10.0.0.14
+10.0.0.6
+10.0.0.14
+10.0.0.15
+10.0.0.16
+10.0.0.6
+10.0.0.16
+10.0.0.17
+10.0.0.5
+10.0.0.1
+10.0.0.17
+10.0.0.15
+10.0.0.17
+10.0.0.18
+10.0.0.18
+10.0.0.10
+10.0.0.5
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.18
+10.0.0.11
+10.0.0.11
+10.0.0.11
+10.0.0.18
+10.0.0.18
+10.0.0.18
+10.0.0.19
+10.0.0.18
+10.0.0.19
+10.0.0.19
+10.0.0.19
+10.0.0.19
+10.0.0.19
+10.0.0.19
+10.0.0.19
+10.0.0.19
+10.0.0.20
+10.0.0.20
+10.0.0.21
+10.0.0.18
+10.0.0.18
+10.0.0.22
+10.0.0.6
+10.0.0.5
+10.0.0.5
+10.0.0.5
+10.0.0.5
+10.0.0.6
+10.0.0.23
+10.0.0.24
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.24
+10.0.0.24
+10.0.0.24
+10.0.0.24
+10.0.0.25
+10.0.0.26
+10.0.0.27
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.25
+10.0.0.26
+10.0.0.24
+10.0.0.24
+10.0.0.27
+10.0.0.23
+10.0.0.25
+10.0.0.26
+10.0.0.24
+10.0.0.24
+10.0.0.27
+10.0.0.25
+10.0.0.26
+10.0.0.24
+10.0.0.24
+10.0.0.27
+10.0.0.28
+10.0.0.23
+10.0.0.25
+10.0.0.24
+10.0.0.24
+10.0.0.27
+10.0.0.26
+10.0.0.23
+10.0.0.25
+10.0.0.26
+10.0.0.24
+10.0.0.24
+10.0.0.27
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.23
+10.0.0.27
+10.0.0.23
+10.0.0.25
+10.0.0.24
+10.0.0.24
+10.0.0.27
+10.0.0.26
diff --git a/test/expected/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.err b/test/expected/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.err
diff --git a/test/expected/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.out b/test/expected/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.out
new file mode 100644
index 0000000..c474fab
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_129e58679e72f3cc5864812026e49a7917baf3d0.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column gunzip(gzip(1)): 1
diff --git a/test/expected/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.err b/test/expected/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.err
diff --git a/test/expected/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.out b/test/expected/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.out
new file mode 100644
index 0000000..3b8aad8
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_151a0fd71ef6837c8cbd8a67e315019b5812b079.out
@@ -0,0 +1,8 @@
+Row 0:
+ Column match_index: 0
+ Column capture_index: 0
+ Column capture_name: (null)
+ Column capture_count: 1
+ Column range_start: 1
+ Column range_stop: 4
+ Column content: foo
diff --git a/test/expected/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.err b/test/expected/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.err
new file mode 100644
index 0000000..9b9cb88
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- wrong number of arguments to function humanize_file_size()
diff --git a/test/expected/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.out b/test/expected/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_1e7362ac3d9690b1b2cfbd320b6129c46ecfbb8a.out
diff --git a/test/expected/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.err b/test/expected/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.err
diff --git a/test/expected/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.out b/test/expected/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.out
new file mode 100644
index 0000000..13d2b61
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_211c5428db0590795072c31cb116ef35281e02b5.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column endswith('foo.', '.'): 1
diff --git a/test/expected/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.err b/test/expected/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.err
diff --git a/test/expected/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.out b/test/expected/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.out
new file mode 100644
index 0000000..7942a4a
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_2f189f0785bb81a1298db35e9e166983b633c73f.out
@@ -0,0 +1,8 @@
+Row 0:
+ Column match_index: 0
+ Column capture_index: 0
+ Column capture_name: (null)
+ Column capture_count: 1
+ Column range_start: 1
+ Column range_stop: 8
+ Column content: foo bar
diff --git a/test/expected/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.err b/test/expected/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.err
diff --git a/test/expected/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.out b/test/expected/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.out
new file mode 100644
index 0000000..76ba63e
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_30f65162174b886130b94a5dd1f094e7f09debed.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column unparse_url(parse_url('https://example.com/search?flag&flag2&=def#frag1%20space')): https://example.com/search?flag&flag2&=def#frag1%20space
diff --git a/test/expected/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.err b/test/expected/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.err
diff --git a/test/expected/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.out b/test/expected/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.out
new file mode 100644
index 0000000..8cb1716
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_352434d199f7b493668c9f2774472eb69ef0d9f0.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column regexp('bc', 'abcd'): 1
diff --git a/test/expected/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.err b/test/expected/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.err
diff --git a/test/expected/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.out b/test/expected/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.out
new file mode 100644
index 0000000..52f6a45
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_36fc9005464f1106f969559e640d9fa36d5fadad.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column regexp_replace('test 1 2 3', '\d+', 'N'): test N N N
diff --git a/test/expected/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.err b/test/expected/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.err
new file mode 100644
index 0000000..e3d40ab
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"invalid URL: “https://bad@[fe::”","attrs":[]},"reason":{"str":"Port number was not a decimal number between 0 and 65535","attrs":[]},"snippets":[],"help":{"str":"","attrs":[]}}
diff --git a/test/expected/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.out b/test/expected/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_3855d2cc0ab29171cae8e722f130adec25eae36e.out
diff --git a/test/expected/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.err b/test/expected/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.err
diff --git a/test/expected/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.out b/test/expected/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.out
new file mode 100644
index 0000000..e139659
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_3de72fe5c1751dd212a1cd45cf2caa7f3b52bced.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column result: {"foo":1,"bar":2,"baz":20.0,"msg":"hello"}
diff --git a/test/expected/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.err b/test/expected/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.err
diff --git a/test/expected/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.out b/test/expected/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.out
new file mode 100644
index 0000000..cf88bf4
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_4b402274da152135c6c99456b693e1ecabca0256.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column startswith('foo', '.'): 0
diff --git a/test/expected/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.err b/test/expected/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.err
diff --git a/test/expected/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.out b/test/expected/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.out
new file mode 100644
index 0000000..a8efc56
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_51055e40d709332ee772ba5719039314bbf5e411.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column endswith('foo', '.'): 0
diff --git a/test/expected/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.err b/test/expected/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.err
diff --git a/test/expected/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.out b/test/expected/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.out
new file mode 100644
index 0000000..061b9ed
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_51766b600fd158a9e0677f6b0fa31b83537b2e5b.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column parse_url('https://example.com/search?flag&flag2&=def'): {"scheme":"https","user":null,"password":null,"host":"example.com","port":null,"path":"/search","query":"flag&flag2&=def","parameters":{"flag":null,"flag2":null,"":"def"},"fragment":null}
diff --git a/test/expected/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.err b/test/expected/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.err
diff --git a/test/expected/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.out b/test/expected/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.out
new file mode 100644
index 0000000..e062d63
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_5203db1a4a81e43a693f339fd26e1ed635da9d5a.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column result: {"foo":1,"bar":2}
diff --git a/test/expected/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.err b/test/expected/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.err
diff --git a/test/expected/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.out b/test/expected/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.out
new file mode 100644
index 0000000..aed4105
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_5abe3717393fba14ec510a37b4b94fedc67aae8e.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column endswith('foo.txt', '.txt'): 1
diff --git a/test/expected/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.err b/test/expected/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.err
diff --git a/test/expected/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.out b/test/expected/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.out
new file mode 100644
index 0000000..22d58c4
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_5e436fbd4efb140600999c5208886a5a57b8a30e.out
@@ -0,0 +1,24 @@
+Row 0:
+ Column match_index: 0
+ Column capture_index: 0
+ Column capture_name: (null)
+ Column capture_count: 3
+ Column range_start: 1
+ Column range_stop: 8
+ Column content: foo bar
+Row 1:
+ Column match_index: 0
+ Column capture_index: 1
+ Column capture_name: (null)
+ Column capture_count: 3
+ Column range_start: 0
+ Column range_stop: 0
+ Column content: (null)
+Row 2:
+ Column match_index: 0
+ Column capture_index: 2
+ Column capture_name: word
+ Column capture_count: 3
+ Column range_start: 5
+ Column range_stop: 8
+ Column content: bar
diff --git a/test/expected/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.err b/test/expected/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.err
diff --git a/test/expected/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.out b/test/expected/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.out
new file mode 100644
index 0000000..3268219
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_5f9979fa5ce7b76efe714bb27ffbe9f5927ae941.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column regexp('[e-z]+', 'ea'): 1
diff --git a/test/expected/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.err b/test/expected/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.err
diff --git a/test/expected/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.out b/test/expected/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.out
new file mode 100644
index 0000000..4645bc6
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_60a005a9f0d44ad022b5554415319933d5743c51.out
@@ -0,0 +1,3 @@
+Row 0:
+ Column typeof(result): text
+ Column result: {"col_0":"","col_1":""}
diff --git a/test/expected/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.err b/test/expected/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.err
diff --git a/test/expected/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.out b/test/expected/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.out
new file mode 100644
index 0000000..0a2022e
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_660288b48d9b30244621d873944938f7ef043976.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column value: foo
diff --git a/test/expected/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.err b/test/expected/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.err
diff --git a/test/expected/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.out b/test/expected/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.out
new file mode 100644
index 0000000..b86c36b
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_6607c0dd8baff16930eb3e0daf6354af5b50052b.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column endswith('a', '.txt'): 0
diff --git a/test/expected/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.err b/test/expected/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.err
diff --git a/test/expected/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.out b/test/expected/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.out
new file mode 100644
index 0000000..913b8ce
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_69f5d49e62da48e188bd9d6af4bd3adeb21eb7d1.out
@@ -0,0 +1,6 @@
+Row 0:
+ Column match_index: 0
+ Column content: {"col_0":1,"col_1":2}
+Row 1:
+ Column match_index: 1
+ Column content: {"col_0":3,"col_1":4}
diff --git a/test/expected/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.err b/test/expected/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.err
diff --git a/test/expected/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.out b/test/expected/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.out
new file mode 100644
index 0000000..d1dcc93
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_6ff984d8ed3e5099376d19f0dd20d5fd1ed42494.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column parse_url('https://example.com/sea%26rch?flag&flag2&=def#frag1%20space'): {"scheme":"https","user":null,"password":null,"host":"example.com","port":null,"path":"/sea&rch","query":"flag&flag2&=def","parameters":{"flag":null,"flag2":null,"":"def"},"fragment":"frag1 space"}
diff --git a/test/expected/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.err b/test/expected/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.err
diff --git a/test/expected/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.out b/test/expected/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.out
new file mode 100644
index 0000000..3718bc2
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_71f37db33504b2c08a7a3323c482556f53d88100.out
@@ -0,0 +1,16 @@
+Row 0:
+ Column match_index: 0
+ Column capture_index: 0
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 1
+ Column range_stop: 8
+ Column content: foo bar
+Row 1:
+ Column match_index: 0
+ Column capture_index: 1
+ Column capture_name: word
+ Column capture_count: 2
+ Column range_start: 5
+ Column range_stop: 8
+ Column content: bar
diff --git a/test/expected/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.err b/test/expected/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.err
diff --git a/test/expected/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.out b/test/expected/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.out
new file mode 100644
index 0000000..bf73241
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_77fc174faeec1eda687a9373dbdbdd1aaef56e20.out
@@ -0,0 +1,3 @@
+Row 0:
+ Column typeof(result): text
+ Column result:
diff --git a/test/expected/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.err b/test/expected/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.err
new file mode 100644
index 0000000..4f2a2e8
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- Expecting an integer for argument number 0
diff --git a/test/expected/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.out b/test/expected/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_790da4aab5af901feeff5426790876eb91b967cb.out
diff --git a/test/expected/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.err b/test/expected/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.err
diff --git a/test/expected/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.out b/test/expected/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.out
new file mode 100644
index 0000000..ec11590
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_7a544cd702579c1fab35870428788ad763cf1143.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column result: (null)
diff --git a/test/expected/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.err b/test/expected/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.err
diff --git a/test/expected/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.out b/test/expected/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.out
new file mode 100644
index 0000000..c3b7854
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_7b6e7c26e8a80459fef55d56156d6ff93c00bd49.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column unparse_url(parse_url('https://example.com/search?flag&flag2&=def')): https://example.com/search?flag&flag2&=def
diff --git a/test/expected/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.err b/test/expected/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.err
diff --git a/test/expected/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.out b/test/expected/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.out
new file mode 100644
index 0000000..6233470
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_7c1e7604ac050e7047201638dca0a6b0fcfd8bdf.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column unparse_url(parse_url('https://example.com/search?flag&flag2')): https://example.com/search?flag&flag2
diff --git a/test/expected/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.err b/test/expected/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.err
diff --git a/test/expected/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.out b/test/expected/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.out
new file mode 100644
index 0000000..b012761
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_7f751009d0db15fc97f9113c5c84db05ff1de9c3.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column length(gzip(1)): 21
diff --git a/test/expected/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.err b/test/expected/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.err
diff --git a/test/expected/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.out b/test/expected/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.out
new file mode 100644
index 0000000..84c7397
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_805ca5e97fbf1ed56f2e920befd963255ba190b6.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column parse_url('https://example.com/search?flag&flag2'): {"scheme":"https","user":null,"password":null,"host":"example.com","port":null,"path":"/search","query":"flag&flag2","parameters":{"flag":null,"flag2":null},"fragment":null}
diff --git a/test/expected/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.err b/test/expected/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.err
new file mode 100644
index 0000000..a92048a
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"call to regexp_match(re, str) failed","attrs":[{"start":8,"end":20,"type":"role","value":47},{"start":21,"end":23,"type":"role","value":46},{"start":25,"end":28,"type":"role","value":46},{"start":8,"end":29,"type":"role","value":60}]},"reason":{"str":"regular expression does not have any captures","attrs":[]},"snippets":[],"help":{"str":"","attrs":[]}}
diff --git a/test/expected/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.out b/test/expected/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_80c1fb9affbfac609ebf1cc5556aafb1ecd223c1.out
diff --git a/test/expected/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.err b/test/expected/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.err
diff --git a/test/expected/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.out b/test/expected/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.out
new file mode 100644
index 0000000..c803da8
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_836e3f721a0f945ad27e7aa241121ba739aab618.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column encode('foo', null): (null)
diff --git a/test/expected/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.err b/test/expected/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.err
diff --git a/test/expected/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.out b/test/expected/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.out
new file mode 100644
index 0000000..34bf1a0
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_838e9bc7873b2b238157ba0358e0dfd6a01d837d.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column result: {"foo":"abc","col_0":123}
diff --git a/test/expected/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.err b/test/expected/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.err
diff --git a/test/expected/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.out b/test/expected/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.out
new file mode 100644
index 0000000..ec11590
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_84e77dedec887c5e2433dbc5b130000cd88963bd.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column result: (null)
diff --git a/test/expected/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.err b/test/expected/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.err
new file mode 100644
index 0000000..511fbab
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- Expecting an value of 'base64', 'hex', or 'uri' for argument number 1
diff --git a/test/expected/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.out b/test/expected/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_887afe94962d958aca2e03f7873d58ca93e190b5.out
diff --git a/test/expected/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.err b/test/expected/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.err
diff --git a/test/expected/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.out b/test/expected/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.out
new file mode 100644
index 0000000..724853b
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_8c9ef83431ea75050fd16824075bf72056cf5f53.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column humanize_file_size(10 * 1000 * 1000): 9.5MB
diff --git a/test/expected/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.err b/test/expected/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.err
diff --git a/test/expected/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.out b/test/expected/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.out
new file mode 100644
index 0000000..233afd3
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_8cef54f0617960320b5d3615068eb27333dcf6a3.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column regexp('abcd', 'abcd'): 1
diff --git a/test/expected/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.err b/test/expected/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.err
diff --git a/test/expected/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.out b/test/expected/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.out
new file mode 100644
index 0000000..90f0972
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_8f4f0ed74c4dc6b821e02a44552b694614cd9353.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column regexp_match(null, 'abc'): (null)
diff --git a/test/expected/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.err b/test/expected/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.err
diff --git a/test/expected/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.out b/test/expected/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.out
new file mode 100644
index 0000000..f93d348
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_949ffd5b2ef9fbcbe17f2e61ef7750f7038f6fd6.out
@@ -0,0 +1,2 @@
+regexp_match('^(\w+)=([^;]+);', 'abc=def;ghi=jkl;')
+{"col_0":"abc","col_1":"def"}
diff --git a/test/expected/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.err b/test/expected/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.err
diff --git a/test/expected/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.out b/test/expected/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.out
new file mode 100644
index 0000000..067e846
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_a4d84a0082a7df34c95c2e6e070bbf6effaa5594.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column repl: test{ }1{ }2{ }3
diff --git a/test/expected/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.err b/test/expected/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.err
new file mode 100644
index 0000000..475a9b2
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"invalid URL: “https://example.com:100000”","attrs":[]},"reason":{"str":"Port number was not a decimal number between 0 and 65535","attrs":[]},"snippets":[],"help":{"str":"","attrs":[]}}
diff --git a/test/expected/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.out b/test/expected/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_a515ba81cc3655c602da28cd0fa1a186d5e9a6e1.out
diff --git a/test/expected/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.err b/test/expected/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.err
diff --git a/test/expected/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.out b/test/expected/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.out
new file mode 100644
index 0000000..bf3f522
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_a65d2fb2f841578619528ca10168ca4d650218e9.out
@@ -0,0 +1,3 @@
+Row 0:
+ Column typeof(result): real
+ Column result: 123.456
diff --git a/test/expected/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.err b/test/expected/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.err
diff --git a/test/expected/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.out b/test/expected/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.out
new file mode 100644
index 0000000..0652eef
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_ac7ecdda0fcc4279a4694291edaa2f1411f5262e.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column repl: <\3><\3> <\3><\3>
diff --git a/test/expected/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.err b/test/expected/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.err
diff --git a/test/expected/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.out b/test/expected/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.out
new file mode 100644
index 0000000..e93f55d
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_b088735cf46f23ca3d5fb3da41f07a6a3b1cba35.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column parse_url('https://example.com'): {"scheme":"https","user":null,"password":null,"host":"example.com","port":null,"path":"/","query":null,"parameters":null,"fragment":null}
diff --git a/test/expected/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.err b/test/expected/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.err
diff --git a/test/expected/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.out b/test/expected/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.out
new file mode 100644
index 0000000..f94307e
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_b0e5bf23bbbc0defa8bb26817782c9d46a778ad8.out
@@ -0,0 +1,16 @@
+Row 0:
+ Column match_index: 0
+ Column capture_index: 0
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 1
+ Column range_stop: 8
+ Column content: foo bar
+Row 1:
+ Column match_index: 0
+ Column capture_index: 1
+ Column capture_name: (null)
+ Column capture_count: 2
+ Column range_start: 5
+ Column range_stop: 8
+ Column content: bar
diff --git a/test/expected/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.err b/test/expected/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.err
diff --git a/test/expected/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.out b/test/expected/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.out
new file mode 100644
index 0000000..3e3281a
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_b2aafbcaa7befe426d3f9df71c24f16fdc9d2856.out
@@ -0,0 +1,3 @@
+Row 0:
+ Column typeof(result): integer
+ Column result: 123
diff --git a/test/expected/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.err b/test/expected/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.err
diff --git a/test/expected/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.out b/test/expected/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.out
new file mode 100644
index 0000000..70e423e
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_b81b27abfafbd357d41c407428d41ae0f4bb75e2.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column unparse_url(parse_url('https://example.com/search?flag')): https://example.com/search?flag
diff --git a/test/expected/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.err b/test/expected/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.err
diff --git a/test/expected/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.out b/test/expected/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.out
new file mode 100644
index 0000000..daf8e0a
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_bac7f6531a2adf70cd1871fb13eab26dff133b7c.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column parse_url('https://example.com/search?flag'): {"scheme":"https","user":null,"password":null,"host":"example.com","port":null,"path":"/search","query":"flag","parameters":{"flag":null},"fragment":null}
diff --git a/test/expected/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.err b/test/expected/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.err
diff --git a/test/expected/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.out b/test/expected/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.out
new file mode 100644
index 0000000..8a4e63d
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_bfb7088916412360f77683009058b0747784630a.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column repl: <\><\> <\><\>
diff --git a/test/expected/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.err b/test/expected/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.err
new file mode 100644
index 0000000..9b9cb88
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- wrong number of arguments to function humanize_file_size()
diff --git a/test/expected/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.out b/test/expected/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_bfe8b09e23389af0ef14359b66d68228d0285185.out
diff --git a/test/expected/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.err b/test/expected/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.err
diff --git a/test/expected/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.out b/test/expected/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.out
new file mode 100644
index 0000000..f4fd280
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_c26269b10b9b9e8485aa97c2be2afb2cc3ee910d.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column gunzip(decode(encode(gzip('Hello, World!'), 'base64'), 'base64')): Hello, World!
diff --git a/test/expected/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.err b/test/expected/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.err
diff --git a/test/expected/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.out b/test/expected/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.out
new file mode 100644
index 0000000..366c195
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_c9e2f41431bef879364dc37a472ab01f64d89f89.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column regexp('[e-z]+', 'abcd'): 0
diff --git a/test/expected/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.err b/test/expected/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.err
diff --git a/test/expected/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.out b/test/expected/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.out
new file mode 100644
index 0000000..548d900
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_cc53348c585ee71a7456157ad6b125689813bafe.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column encode(null, 'base64'): (null)
diff --git a/test/expected/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.err b/test/expected/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.err
diff --git a/test/expected/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.out b/test/expected/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.out
new file mode 100644
index 0000000..3b62dec
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_ce9db1dbc2e5fee87247135d17787ff3af014d77.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column result: {"foo":1}
diff --git a/test/expected/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.err b/test/expected/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.err
diff --git a/test/expected/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.out b/test/expected/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d3367527118052081a541a660b091f6f495b1c0d.out
diff --git a/test/expected/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.err b/test/expected/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.err
new file mode 100644
index 0000000..f4c8399
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- Invalid regular expression: missing closing parenthesis
diff --git a/test/expected/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.out b/test/expected/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d4bc869850f5b7e53353fc2506fea0c8e96f29c5.out
diff --git a/test/expected/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.err b/test/expected/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.err
diff --git a/test/expected/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.out b/test/expected/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.out
new file mode 100644
index 0000000..fb41ca4
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d4e805ff08d4ccf62865dbf8db8d526f7ce02f37.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column result: {"foo":"abc","col_0":123.456}
diff --git a/test/expected/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.err b/test/expected/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.err
diff --git a/test/expected/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.out b/test/expected/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.out
new file mode 100644
index 0000000..659abf7
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d54a759f5683a22ad289129b2096b80652b1cc0c.out
@@ -0,0 +1,47 @@
+[
+ {
+ "log_body": "[VpxLRO] -- BEGIN lro-846063 -- SessionManager -- vim.SessionManager.sessionIsActive -- 528e6e0c-246d-58b5-3234-278c6e0c5d0d(52c289ac-2563-48d5-8a8e-f178da022c0d)",
+ "extract(log_body)": {
+ "col_0": [
+ "VpxLRO"
+ ],
+ "col_1": "--",
+ "col_2": "BEGIN",
+ "col_3": "lro-846063",
+ "col_4": "--",
+ "col_5": "SessionManager",
+ "col_6": "--",
+ "col_7": "vim.SessionManager.sessionIsActive",
+ "col_8": "--",
+ "col_9": "528e6e0c-246d-58b5-3234-278c6e0c5d0d",
+ "col_10": [
+ "52c289ac-2563-48d5-8a8e-f178da022c0d"
+ ]
+ }
+ },
+ {
+ "log_body": "[VpxLRO] -- FINISH lro-846063",
+ "extract(log_body)": {
+ "col_0": [
+ "VpxLRO"
+ ],
+ "col_1": "--",
+ "col_2": "FINISH",
+ "col_3": "lro-846063"
+ }
+ },
+ {
+ "log_body": "Exception was thrown when call vsan-performance-manager for cluster [vim.ClusterComputeResource:domain-c109,Cluster-52] perf metrics: N3Vim5Fault8NotFound9ExceptionE(Fault cause: vim.fault.NotFound\n--> )",
+ "extract(log_body)": {
+ "Exception was thrown when call vsan-performance-manager for cluster [vim.ClusterComputeResource:domain-c109,Cluster-52] perf metrics": {
+ "N3Vim5Fault8NotFound9ExceptionE": [
+ "Fault cause",
+ "vim.fault.NotFound",
+ "\n",
+ "--",
+ ">"
+ ]
+ }
+ }
+ }
+]
diff --git a/test/expected/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.err b/test/expected/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.err
diff --git a/test/expected/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.out b/test/expected/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.out
new file mode 100644
index 0000000..0c22818
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_d8d4cde8bbc98175069be579ff5634de43880b8c.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column startswith('.foo', '.'): 1
diff --git a/test/expected/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.err b/test/expected/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.err
diff --git a/test/expected/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.out b/test/expected/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_e68167bf5edc7a7b1defd06bdfb694ffa8b00df2.out
diff --git a/test/expected/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.err b/test/expected/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.err
diff --git a/test/expected/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.out b/test/expected/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.out
new file mode 100644
index 0000000..1fd3a79
--- /dev/null
+++ b/test/expected/test_sql_str_func.sh_ec939e82da809965c61f1c00f68d7afaa4a88382.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column repl: {test}{} {1}{} {2}{} {3}{}
diff --git a/test/expected/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.err b/test/expected/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.err
diff --git a/test/expected/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.out b/test/expected/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.out
new file mode 100644
index 0000000..1cbda8c
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_028e99419eb1ac80b03b36148ef1d4ae1c38c44c.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timediff('today', 'yesterday'): 86400.0
diff --git a/test/expected/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.err b/test/expected/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.err
diff --git a/test/expected/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.out b/test/expected/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.out
new file mode 100644
index 0000000..f209496
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_123c85ff1178743f5cb78efeaf98b637bcbe55ff.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice(null, null): (null)
diff --git a/test/expected/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.err b/test/expected/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.err
diff --git a/test/expected/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.out b/test/expected/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.out
new file mode 100644
index 0000000..8566fb2
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_14737ee9597b7d22519d23fbe34c0eb7d6c09ff2.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 11:59:00', 'before 12pm'): 2015-08-07 00:00:00.000
diff --git a/test/expected/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.err b/test/expected/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.err
diff --git a/test/expected/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.out b/test/expected/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.out
new file mode 100644
index 0000000..1a4cb45
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_1fbeb1ba69a95284eb1d4d052f5068ede7968704.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 11:59:00', 'fri'): 2015-08-07 00:00:00.000
diff --git a/test/expected/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.err b/test/expected/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.err
diff --git a/test/expected/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.out b/test/expected/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.out
new file mode 100644
index 0000000..ba1b6f3
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_20477acc218c96f1385dc97e4d28c80a05c93709.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 11:59:00', 'after fri'): (null)
diff --git a/test/expected/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.err b/test/expected/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.err
diff --git a/test/expected/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.out b/test/expected/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.out
new file mode 100644
index 0000000..7c5c9a3
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_243454526f6b5e19485db771b4932ddffd6f83a4.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice(1616300753.333, '100ms'): 2021-03-21 04:25:53.300
diff --git a/test/expected/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.err b/test/expected/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.err
diff --git a/test/expected/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.out b/test/expected/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.out
new file mode 100644
index 0000000..46dac6c
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_28638a132caae65fd89a68459d1b4af0000b8aef.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 08:01:33', '8 am'): (null)
diff --git a/test/expected/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.err b/test/expected/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.err
diff --git a/test/expected/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.out b/test/expected/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.out
new file mode 100644
index 0000000..e029e45
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_3b551281347a8144c84f00ade2664db9ac4aacab.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timediff('2017-01-02T05:00:00.100', '2017-01-02T05:00:00.000'): 0.1
diff --git a/test/expected/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.err b/test/expected/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.err
diff --git a/test/expected/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.out b/test/expected/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.out
new file mode 100644
index 0000000..f575185
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_4035ee76938269e9247f9a696927a9ac18cce80a.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 12:01:00', '5m'): 2015-08-07 12:00:00.000
diff --git a/test/expected/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.err b/test/expected/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.err
new file mode 100644
index 0000000..537f7c4
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"call to timeslice(time, slice) failed","attrs":[{"start":8,"end":17,"type":"role","value":47},{"start":18,"end":22,"type":"role","value":46},{"start":24,"end":29,"type":"role","value":46},{"start":8,"end":30,"type":"role","value":60}]},"reason":{"str":"unable to parse time slice value: blah -- Unrecognized input","attrs":[]},"snippets":[],"help":{"str":"","attrs":[]}}
diff --git a/test/expected/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.out b/test/expected/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_42f0fc1a154b0d79b4f6e846f283426be498040f.out
diff --git a/test/expected/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.err b/test/expected/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.err
diff --git a/test/expected/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.out b/test/expected/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.out
new file mode 100644
index 0000000..088f537
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_4b96fe71bc2d18955e3625b765a6095ab1f7a75d.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 12:01:00', 'after 12pm'): 2015-08-07 12:00:00.000
diff --git a/test/expected/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.err b/test/expected/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.err
diff --git a/test/expected/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.out b/test/expected/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.out
new file mode 100644
index 0000000..b5d1a46
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_53b76b094e47691b5bca106142ee470e82e8e420.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 12:01:00', '1 month'): 2015-08-03 00:00:00.000
diff --git a/test/expected/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.err b/test/expected/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.err
diff --git a/test/expected/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.out b/test/expected/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.out
new file mode 100644
index 0000000..e1e9cb6
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_6288a9e690d381602b2be5665cc1cd3552733bc2.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 12:01:00', 'before 12pm'): (null)
diff --git a/test/expected/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.err b/test/expected/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.err
diff --git a/test/expected/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.out b/test/expected/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.out
new file mode 100644
index 0000000..f09bc3f
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_652bbd00b5159e22d94970ab1e882997d14b5777.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 11:59:00', 'after 12pm'): (null)
diff --git a/test/expected/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.err b/test/expected/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.err
new file mode 100644
index 0000000..f8b9601
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- timeslice() expects between 1 and 2 arguments
diff --git a/test/expected/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.out b/test/expected/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_6832a58259168622af8b3370b0c89534f98f3f9f.out
diff --git a/test/expected/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.err b/test/expected/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.err
new file mode 100644
index 0000000..4180cf6
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- lnav-error:{"level":"error","message":{"str":"call to timeslice(time, slice) failed","attrs":[{"start":8,"end":17,"type":"role","value":47},{"start":18,"end":22,"type":"role","value":46},{"start":24,"end":29,"type":"role","value":46},{"start":8,"end":30,"type":"role","value":60}]},"reason":{"str":"no time slice value given","attrs":[]},"snippets":[],"help":{"str":"","attrs":[]}}
diff --git a/test/expected/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.out b/test/expected/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_72862ec9c8f261a8507d237eb673c7ddfaafd898.out
diff --git a/test/expected/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.err b/test/expected/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.err
diff --git a/test/expected/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.out b/test/expected/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.out
new file mode 100644
index 0000000..1f9d1e1
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_7797302b63d73234c9ec9f0405c7c0a748daf8e9.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timediff('foo', 'yesterday'): (null)
diff --git a/test/expected/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.err b/test/expected/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.err
diff --git a/test/expected/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.out b/test/expected/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.out
new file mode 100644
index 0000000..de734a9
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_9569ab40cb2e51c60f818a6c2729c60d86565e7e.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-02-01T05:10:00'): 2015-02-01 05:00:00.000
diff --git a/test/expected/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.err b/test/expected/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.err
diff --git a/test/expected/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.out b/test/expected/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.out
new file mode 100644
index 0000000..74ec041
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_9e649c4bc10f4d178519983358f7092e9c5dfe71.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 12:01:00', '1d'): 2015-08-07 00:00:00.000
diff --git a/test/expected/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.err b/test/expected/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.err
diff --git a/test/expected/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.out b/test/expected/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.out
new file mode 100644
index 0000000..e913429
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_b0257ced663fc444801a5e6cba89c3053acca11e.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 12:01:00', 'before fri'): (null)
diff --git a/test/expected/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.err b/test/expected/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.err
diff --git a/test/expected/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.out b/test/expected/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.out
new file mode 100644
index 0000000..f18a8b8
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_b5f9ec3ea8b4551fd40017398d74c524fb54ebc9.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice(null): (null)
diff --git a/test/expected/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.err b/test/expected/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.err
diff --git a/test/expected/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.out b/test/expected/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.out
new file mode 100644
index 0000000..2729e3c
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_dbe786c096d5a7a5e1d05311b929f1427d8bac79.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 12:01:00', '8 am'): (null)
diff --git a/test/expected/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.err b/test/expected/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.err
diff --git a/test/expected/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.out b/test/expected/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.out
new file mode 100644
index 0000000..8687c5d
--- /dev/null
+++ b/test/expected/test_sql_time_func.sh_f3b1ea49779117bf45f85ad5615fdc5e89193db6.out
@@ -0,0 +1,2 @@
+Row 0:
+ Column timeslice('2015-08-07 08:00:33', '8 am'): 2015-08-07 08:00:00.000
diff --git a/test/expected/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.err b/test/expected/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.err
new file mode 100644
index 0000000..6568331
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: Only the top view in the stack can be deleted
+ --> command-option:2
+ | ;DELETE FROM lnav_view_stack WHERE name = 'log'
diff --git a/test/expected/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.out b/test/expected/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_28e23f4e98b1acd6478e39844fd9306b444550c3.out
diff --git a/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.err b/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.err
diff --git a/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.out b/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.out
new file mode 100644
index 0000000..710f668
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_32acc1a8bb5028636fdbf08f077f9a835ab51bec.out
@@ -0,0 +1,19 @@
+Build
+
+Lnav follows the usual GNU style for configuring and installing
+software:
+
+Run  ./autogen.sh  if compiling from a cloned repository.
+
+ ▌$ ./configure 
+ ▌$ make 
+ ▌$ sudo make install 
+
+See Also
+
+Angle-grinder[1] is a tool to slice and dice log files on the
+command-line. If you're familiar with the SumoLogic query language,
+you might find this tool more comfortable to work with.
+
+ ▌[1] - https://github.com/rcoh/angle-grinder
+
diff --git a/test/expected/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.err b/test/expected/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.err
diff --git a/test/expected/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.out b/test/expected/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.out
new file mode 100644
index 0000000..bfe5f1e
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_485a6ac7c69bd4b5d34d3399a9c17f6a2dc89ad3.out
@@ -0,0 +1 @@
+2014-10-08 16:56:38,344:WARN:foo bar baz
diff --git a/test/expected/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.err b/test/expected/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.err
new file mode 100644
index 0000000..0c9a2b7
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: filter already exists -- :filter-in vmk
+ --> command-option:3
+ | ;UPDATE lnav_view_filters SET pattern = 'vmk'
diff --git a/test/expected/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.out b/test/expected/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.out
new file mode 100644
index 0000000..48f52f3
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_62d15cb9d5a9259f198aa01ca8ed200d6da38d68.out
@@ -0,0 +1,3 @@
+view_name filter_id enabled type language pattern 
+log   1  1 in  regex  vmk 
+log   2    1 in regex vmk1
diff --git a/test/expected/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.err b/test/expected/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.err
diff --git a/test/expected/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.out b/test/expected/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.out
new file mode 100644
index 0000000..2678e6c
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_662b5f9b17aa69a8e3aa9a18acb30d9acf6e2837.out
@@ -0,0 +1 @@
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
diff --git a/test/expected/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.err b/test/expected/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.err
diff --git a/test/expected/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.out b/test/expected/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.out
new file mode 100644
index 0000000..1ad5fbf
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_6ffd89498b9a7758ded6717148fc2ce77a12621b.out
@@ -0,0 +1,2 @@
+search 
+warn
diff --git a/test/expected/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.err b/test/expected/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.err
new file mode 100644
index 0000000..482241e
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: Expecting a non-empty pattern value
+ --> command-option:1
+ | ;INSERT INTO lnav_view_filters VALUES ('log', 0, 1, 'out', 'regex', '')
diff --git a/test/expected/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.out b/test/expected/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_764ea85863d4f0ea3b7cb40850ac7c8fde682d57.out
diff --git a/test/expected/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.err b/test/expected/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.err
new file mode 100644
index 0000000..6787d65
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: A SQL expression filter already exists
+ --> command-option:2
+ | ;INSERT INTO lnav_view_filters (view_name, language, pattern) VALUES ('log', 'sql', '1')
diff --git a/test/expected/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.out b/test/expected/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.out
new file mode 100644
index 0000000..c99c751
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_81dc3eb51ec4dc3066a2365524001242c423a9cf.out
@@ -0,0 +1,2 @@
+view_name filter_id enabled type language pattern 
+log   0  1 out  sql  1  
diff --git a/test/expected/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.err b/test/expected/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.err
diff --git a/test/expected/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.out b/test/expected/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_81ffd4ed3f62228494a966512791202cea7e3b57.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.err b/test/expected/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.err
new file mode 100644
index 0000000..609aacb
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: Expecting an lnav view name for column number 0
+ --> command-option:1
+ | ;INSERT INTO lnav_view_filters VALUES ('bad', 0, 1, 'out', 'regex', 'abc')
diff --git a/test/expected/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.out b/test/expected/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_87f53d441e22c1d27c27eaa6003c83da1207c063.out
diff --git a/test/expected/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.err b/test/expected/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.err
diff --git a/test/expected/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.out b/test/expected/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.out
new file mode 100644
index 0000000..d51a68c
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_977cdf5d396522194d6b9e945169ff8073b4296b.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.err b/test/expected/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.err
diff --git a/test/expected/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.out b/test/expected/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.out
new file mode 100644
index 0000000..8ee2f08
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_9a5be90921256e90428c77753eca5ea0d31bd910.out
@@ -0,0 +1,2 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.err b/test/expected/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.err
diff --git a/test/expected/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.out b/test/expected/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.out
new file mode 100644
index 0000000..fbbd59d
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_a1e6ee4f098d525330d5f58a9d71cbbd816d51bb.out
@@ -0,0 +1,101 @@
+[
+ {
+ "top_meta": {
+ "time": "2020-12-10 06:56:41.092",
+ "file": "{test_dir}/logfile_xml_msg.0",
+ "breadcrumbs": [
+ {
+ "display_value": "2020-12-10T06:56:41.092",
+ "search_placeholder": "(Enter an absolute or relative time)",
+ "possibilities": [
+ {
+ "display_value": "-1 day"
+ },
+ {
+ "display_value": "-1h"
+ },
+ {
+ "display_value": "-30m"
+ },
+ {
+ "display_value": "-15m"
+ },
+ {
+ "display_value": "-5m"
+ },
+ {
+ "display_value": "-1m"
+ },
+ {
+ "display_value": "+1m"
+ },
+ {
+ "display_value": "+5m"
+ },
+ {
+ "display_value": "+15m"
+ },
+ {
+ "display_value": "+30m"
+ },
+ {
+ "display_value": "+1h"
+ },
+ {
+ "display_value": "+1 day"
+ }
+ ]
+ },
+ {
+ "display_value": "generic_log",
+ "search_placeholder": "",
+ "possibilities": [
+ {
+ "display_value": "generic_log"
+ }
+ ]
+ },
+ {
+ "display_value": "logfile_xml_msg.0[2]",
+ "search_placeholder": "",
+ "possibilities": [
+ {
+ "display_value": "logfile_xml_msg.0"
+ }
+ ]
+ },
+ {
+ "display_value": "<a-request>",
+ "search_placeholder": "",
+ "possibilities": [
+ {
+ "display_value": "<a-request>"
+ }
+ ]
+ },
+ {
+ "display_value": "<head>",
+ "search_placeholder": "",
+ "possibilities": [
+ {
+ "display_value": "<head>"
+ },
+ {
+ "display_value": "<request id=\"1\">"
+ },
+ {
+ "display_value": "<source>"
+ }
+ ]
+ },
+ {
+ "display_value": "⋯",
+ "search_placeholder": "",
+ "possibilities": [
+
+ ]
+ }
+ ]
+ }
+ }
+]
diff --git a/test/expected/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.err b/test/expected/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.err
new file mode 100644
index 0000000..a0853fd
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: filter already exists -- :filter-in vmk
+ --> command-option:2
+ | ;INSERT INTO lnav_view_filters (view_name, pattern, type) VALUES ('log', 'vmk', 'in')
diff --git a/test/expected/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.out b/test/expected/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.out
new file mode 100644
index 0000000..38afb7c
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_a2c0f0e51b3f85ea2a05ecdcacaad962b4fe5d4f.out
@@ -0,0 +1,2 @@
+view_name filter_id enabled type language pattern 
+log   1  1 in  regex  vmk 
diff --git a/test/expected/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.err b/test/expected/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.err
new file mode 100644
index 0000000..66b8e6a
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: The lnav_view_stack table cannot be updated
+ --> command-option:1
+ | ;UPDATE lnav_view_stack SET name = 'foo'
diff --git a/test/expected/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.out b/test/expected/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_ac1f6e9a88608ef8939f9c2f7061a25a86742d46.out
diff --git a/test/expected/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.err b/test/expected/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.err
new file mode 100644
index 0000000..dd46516
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: Invalid regular expression for pattern: missing closing parenthesis at offset 4
+ --> command-option:1
+ | ;INSERT INTO lnav_view_filters VALUES ('log', 0, 1, 'out', 'regex', 'abc(')
diff --git a/test/expected/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.out b/test/expected/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_ade121f29bedea0d1a54452cc994b2302ad9dabb.out
diff --git a/test/expected/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.err b/test/expected/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.err
new file mode 100644
index 0000000..4276af6
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: Invalid time: bad-time
+ --> command-option:1
+ | ;UPDATE lnav_views SET top_time = 'bad-time' WHERE name = 'log'
diff --git a/test/expected/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.out b/test/expected/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_c851bdf3ba2f56fac5a216457b2d11a109e77f03.out
diff --git a/test/expected/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.err b/test/expected/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.err
diff --git a/test/expected/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.out b/test/expected/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.out
new file mode 100644
index 0000000..bc35132
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_d99d884ba6668b66e3ca9ea4ed2d0e236497c35d.out
@@ -0,0 +1 @@
+2014-10-08 16:56:38,344:WARN:foo bar baz
diff --git a/test/expected/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.err b/test/expected/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.err
diff --git a/test/expected/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.out b/test/expected/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.out
new file mode 100644
index 0000000..493283c
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_e036fabdc6c15f65a374b95c9922212670d494ee.out
@@ -0,0 +1 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.err b/test/expected/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.err
diff --git a/test/expected/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.out b/test/expected/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.out
new file mode 100644
index 0000000..0dd4cb7
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_ec4623bd63ff353f50db44da1231e46a1a4f1824.out
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/expected/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.err b/test/expected/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.err
new file mode 100644
index 0000000..aab37c6
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: Expecting an lnav view name for column number 0
+ --> command-option:1
+ | ;INSERT INTO lnav_view_filters VALUES (NULL, 0, 1, 'out', 'regex', 'abc')
diff --git a/test/expected/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.out b/test/expected/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_f7476c76ea51cf479a6a79b037e0cb59871b629c.out
diff --git a/test/expected/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.err b/test/expected/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.err
new file mode 100644
index 0000000..8a477fc
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.err
@@ -0,0 +1,4 @@
+✘ error: SQL statement failed
+ reason: Expecting an value of 'in' or 'out' for column number 3
+ --> command-option:1
+ | ;INSERT INTO lnav_view_filters VALUES ('log', 0 , 1, 'bad', 'regex', 'abc')
diff --git a/test/expected/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.out b/test/expected/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_views_vtab.sh_f8340cb4c62aabd839ea09235b6ebe41b2bb48f4.out
diff --git a/test/expected/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.err b/test/expected/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.err
diff --git a/test/expected/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.out b/test/expected/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_46dfa23e2effabf3fa150c4b871fd8d22b1c834d.out
diff --git a/test/expected/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.err b/test/expected/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.err
diff --git a/test/expected/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.out b/test/expected/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.out
new file mode 100644
index 0000000..a4c5713
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_4effabf11b59580e5f0727199eb74fba049c0cda.out
@@ -0,0 +1,6 @@
+Row 0:
+ Column result: <def>Hello &gt;</def>
+
+ Column node_path: /abc/def
+ Column node_attr: {}
+ Column node_text: Hello >
diff --git a/test/expected/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.err b/test/expected/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.err
diff --git a/test/expected/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.out b/test/expected/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.out
new file mode 100644
index 0000000..2800fc5
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_8912b59d5b515ab1373a3d9bc635ebabacd01dfd.out
@@ -0,0 +1,6 @@
+Row 0:
+ Column result: <def a="b">ghi</def>
+
+ Column node_path: /abc/def[2]
+ Column node_attr: {"a":"b"}
+ Column node_text: ghi
diff --git a/test/expected/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.err b/test/expected/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.err
new file mode 100644
index 0000000..6ac0f80
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- Invalid XPATH expression at offset 5: Unrecognized node test
diff --git a/test/expected/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.out b/test/expected/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_b036c73528a446cba46625767517cdac868aba72.out
diff --git a/test/expected/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.err b/test/expected/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.err
new file mode 100644
index 0000000..a97d7cd
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.err
@@ -0,0 +1 @@
+error: sqlite3_exec failed -- Invalid XML document at offset 3: Error parsing start element tag
diff --git a/test/expected/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.out b/test/expected/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_xml_func.sh_fefeb387ae14d4171225ea06cbbff3ec43990cf0.out
diff --git a/test/expected/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.err b/test/expected/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.err
new file mode 100644
index 0000000..b7f3a79
--- /dev/null
+++ b/test/expected/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.err
@@ -0,0 +1,4 @@
+✘ error: failed to parse YAML content
+ reason: closing ] not found
+ --> command-option:1
+ | ;SELECT yaml_to_json('[abc') 
diff --git a/test/expected/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.out b/test/expected/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_yaml_func.sh_41c6abde708a69e74f5b7fde865d88fa75f91e0a.out
diff --git a/test/expected/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.err b/test/expected/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.err
new file mode 100644
index 0000000..b7f3a79
--- /dev/null
+++ b/test/expected/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.err
@@ -0,0 +1,4 @@
+✘ error: failed to parse YAML content
+ reason: closing ] not found
+ --> command-option:1
+ | ;SELECT yaml_to_json('[abc') 
diff --git a/test/expected/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.out b/test/expected/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_sql_yaml_func.sh_dc189d02e8979b7ed245d5d750f68b9965984699.out
diff --git a/test/expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.err b/test/expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.err
new file mode 100644
index 0000000..b77acd0
--- /dev/null
+++ b/test/expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.err
@@ -0,0 +1,3 @@
+tailer stderr:
+info: load preview request -- 1234
+info: exiting...
diff --git a/test/expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.out b/test/expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.out
new file mode 100644
index 0000000..cd58305
--- /dev/null
+++ b/test/expected/test_tailer.sh_12f539e535df04364316699f9edeac461aa9f9de.out
@@ -0,0 +1,8 @@
+preview of file: {test_dir}/remote-log-dir/*
+{test_dir}/remote-log-dir/logfile_access_log.0
+{test_dir}/remote-log-dir/logfile_access_log.1
+
+all done!
+tailer stderr:
+info: load preview request -- 1234
+info: exiting...
diff --git a/test/expected/test_text_file.sh_2e69c22dcfa37b5c3e8490a6026eacb7ca953998.err b/test/expected/test_text_file.sh_2e69c22dcfa37b5c3e8490a6026eacb7ca953998.err
new file mode 100644
index 0000000..08a372f
--- /dev/null
+++ b/test/expected/test_text_file.sh_2e69c22dcfa37b5c3e8490a6026eacb7ca953998.err
@@ -0,0 +1,2 @@
+✘ error: unable to open file: non-existent:
+ reason: failed to ssh to host: ssh: Could not resolve hostname non-existent: nodename nor servname provided, or not known
diff --git a/test/expected/test_text_file.sh_2e69c22dcfa37b5c3e8490a6026eacb7ca953998.out b/test/expected/test_text_file.sh_2e69c22dcfa37b5c3e8490a6026eacb7ca953998.out
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_2e69c22dcfa37b5c3e8490a6026eacb7ca953998.out
diff --git a/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.err b/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.err
diff --git a/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.out b/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.out
new file mode 100644
index 0000000..a39be1e
--- /dev/null
+++ b/test/expected/test_text_file.sh_5b51b55dff7332c5bee2c9b797c401c5614d574a.out
@@ -0,0 +1,178 @@
+Build[1][2] Docs[3][4] Coverage Status[5][6] lnav[7][8]
+
+ ▌[1] - https://github.com/tstack/lnav/workflows/ci-build/badge.svg
+ ▌[2] - https://github.com/tstack/lnav/actions?query=workflow%3Aci-build
+ ▌[3] - https://readthedocs.org/projects/lnav/badge/?version=latest&style=plastic
+ ▌[4] - https://docs.lnav.org
+ ▌[5] - https://coveralls.io/repos/github/tstack/lnav/badge.svg?branch=master
+ ▌[6] - https://coveralls.io/github/tstack/lnav?branch=master
+ ▌[7] - https://snapcraft.io/lnav/badge.svg
+ ▌[8] - https://snapcraft.io/lnav
+
+<img
+src="https://assets-global.website-files.com/6257adef93867e50d84d30e2/62594fddd654fc29fcc07359_cb48d2a8d4991281d7a6a95d2f58195e.svg"
+height="20"/>[1]
+
+ ▌[1] - https://discord.gg/erBPnKwz7R
+
+This is the source repository for lnav, visit https://lnav.org[1] for
+a high level overview.
+
+ ▌[1] - https://lnav.org
+
+LNAV – The Logfile Navigator
+
+The Log File Navigator, lnav for short, is an advanced log file viewer
+for the small-scale. It is a terminal application that can understand
+your log files and make it easy for you to find problems with little
+to no setup.
+
+Screenshot
+
+The following screenshot shows a syslog file. Log lines are displayed
+with highlights. Errors are red and warnings are yellow.
+
+Screenshot[1][2]
+
+ ▌[1] - file://{top_srcdir}/docs/assets/images/lnav-syslog-thumb.png
+ ▌[2] - file://{top_srcdir}/docs/assets/images/lnav-syslog.png
+
+Features
+
+ • Log messages from different files are collated together
+ into a single view
+ • Automatic detection of log format
+ • Automatic decompression of GZip and BZip2 files
+ • Filter log messages based on regular expressions
+ • Use SQL to analyze your logs
+ • And more...
+
+Installation
+
+Download a statically-linked binary for Linux/MacOS from the release
+page[1]
+
+ ▌[1] - https://github.com/tstack/lnav/releases/latest#release-artifacts
+
+Usage
+
+The only file installed is the executable,  lnav . You can execute it
+with no arguments to view the default set of files:
+
+ ▌$ lnav 
+
+You can view all the syslog messages by running:
+
+ ▌$ lnav /var/log/messages* 
+
+Usage with  systemd-journald 
+
+On systems running  systemd-journald , you can use  lnav  as the
+pager:
+
+ ▌$ journalctl | lnav 
+
+or in follow mode:
+
+ ▌$ journalctl -f | lnav 
+
+Since  journalctl 's default output format omits the year, if you are
+viewing logs which span multiple years you will need to change the
+output format to include the year, otherwise  lnav  gets confused:
+
+ ▌$ journalctl -o short-iso | lnav 
+
+It is also possible to use  journalctl 's json output format and  lnav
+will make use of additional fields such as PRIORITY and _SYSTEMD_UNIT:
+
+ ▌$ journalctl -o json | lnav 
+
+In case some MESSAGE fields contain special characters such as ANSI
+color codes which are considered as unprintable by journalctl,
+specifying  journalctl 's  -a  option might be preferable in order to
+output those messages still in a non-binary representation:
+
+ ▌$ journalctl -a -o json | lnav 
+
+If using systemd v236 or newer, the output fields can be limited to
+the ones actually recognized by  lnav  for increased efficiency:
+
+ ▌$ journalctl -o json --output-fields=MESSAGE,PRIORITY,_PID,SYSLOG_IDENTIFIER,_SYSTEMD_UNIT | lnav 
+
+If your system has been running for a long time, for increased
+efficiency you may want to limit the number of log lines fed into  lnav
+, e.g. via  journalctl 's  -n  or  --since=...  options.
+
+In case of a persistent journal, you may want to limit the number of
+log lines fed into  lnav  via  journalctl 's  -b  option.
+
+Support
+
+Please file issues on this repository or use the discussions section.
+The following alternatives are also available:
+
+ • support@lnav.org[1]
+ • Discord[2]
+ • Google Groups[3]
+
+ ▌[1] - mailto:support@lnav.org
+ ▌[2] - https://discord.gg/erBPnKwz7R
+ ▌[3] - https://groups.google.com/g/lnav
+
+Links
+
+ • Main Site[1]
+ • Documentation[2] on Read the Docs
+ • Internal Architecture[3]
+
+ ▌[1] - https://lnav.org
+ ▌[2] - https://docs.lnav.org
+ ▌[3] - file://{top_srcdir}/ARCHITECTURE.md
+
+Contributing
+
+ • Become a Sponsor on GitHub[1]
+
+ ▌[1] - https://github.com/sponsors/tstack
+
+Building From Source
+
+Prerequisites
+
+The following software packages are required to build lnav:
+
+ • gcc/clang - A C++14-compatible compiler.
+ • libpcre2 - The Perl Compatible Regular Expression v2
+ (PCRE2) library.
+ • sqlite - The SQLite database engine. Version 3.9.0
+ or higher is required.
+ • ncurses - The ncurses text UI library.
+ • readline - The readline line editing library.
+ • zlib - The zlib compression library.
+ • bz2 - The bzip2 compression library.
+ • libcurl - The cURL library for downloading files
+ from URLs. Version 7.23.0 or higher is required.
+ • libarchive - The libarchive library for opening archive
+ files, like zip/tgz.
+ • wireshark - The 'tshark' program is used to interpret
+ pcap files.
+
+Build
+
+Lnav follows the usual GNU style for configuring and installing
+software:
+
+Run  ./autogen.sh  if compiling from a cloned repository.
+
+ ▌$ ./configure 
+ ▌$ make 
+ ▌$ sudo make install 
+
+See Also
+
+Angle-grinder[1] is a tool to slice and dice log files on the
+command-line. If you're familiar with the SumoLogic query language,
+you might find this tool more comfortable to work with.
+
+ ▌[1] - https://github.com/rcoh/angle-grinder
+
diff --git a/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.err b/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.err
diff --git a/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.out b/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.out
new file mode 100644
index 0000000..accb1c3
--- /dev/null
+++ b/test/expected/test_text_file.sh_6a24078983cf1b7a80b6fb65d5186cd125498136.out
@@ -0,0 +1,149 @@
+Screenshot
+
+The following screenshot shows a syslog file. Log lines are displayed
+with highlights. Errors are red and warnings are yellow.
+
+Screenshot[1][2]
+
+ ▌[1] - file://{top_srcdir}/docs/assets/images/lnav-syslog-thumb.png
+ ▌[2] - file://{top_srcdir}/docs/assets/images/lnav-syslog.png
+
+Features
+
+ • Log messages from different files are collated together
+ into a single view
+ • Automatic detection of log format
+ • Automatic decompression of GZip and BZip2 files
+ • Filter log messages based on regular expressions
+ • Use SQL to analyze your logs
+ • And more...
+
+Installation
+
+Download a statically-linked binary for Linux/MacOS from the release
+page[1]
+
+ ▌[1] - https://github.com/tstack/lnav/releases/latest#release-artifacts
+
+Usage
+
+The only file installed is the executable,  lnav . You can execute it
+with no arguments to view the default set of files:
+
+ ▌$ lnav 
+
+You can view all the syslog messages by running:
+
+ ▌$ lnav /var/log/messages* 
+
+Usage with  systemd-journald 
+
+On systems running  systemd-journald , you can use  lnav  as the
+pager:
+
+ ▌$ journalctl | lnav 
+
+or in follow mode:
+
+ ▌$ journalctl -f | lnav 
+
+Since  journalctl 's default output format omits the year, if you are
+viewing logs which span multiple years you will need to change the
+output format to include the year, otherwise  lnav  gets confused:
+
+ ▌$ journalctl -o short-iso | lnav 
+
+It is also possible to use  journalctl 's json output format and  lnav
+will make use of additional fields such as PRIORITY and _SYSTEMD_UNIT:
+
+ ▌$ journalctl -o json | lnav 
+
+In case some MESSAGE fields contain special characters such as ANSI
+color codes which are considered as unprintable by journalctl,
+specifying  journalctl 's  -a  option might be preferable in order to
+output those messages still in a non-binary representation:
+
+ ▌$ journalctl -a -o json | lnav 
+
+If using systemd v236 or newer, the output fields can be limited to
+the ones actually recognized by  lnav  for increased efficiency:
+
+ ▌$ journalctl -o json --output-fields=MESSAGE,PRIORITY,_PID,SYSLOG_IDENTIFIER,_SYSTEMD_UNIT | lnav 
+
+If your system has been running for a long time, for increased
+efficiency you may want to limit the number of log lines fed into  lnav
+, e.g. via  journalctl 's  -n  or  --since=...  options.
+
+In case of a persistent journal, you may want to limit the number of
+log lines fed into  lnav  via  journalctl 's  -b  option.
+
+Support
+
+Please file issues on this repository or use the discussions section.
+The following alternatives are also available:
+
+ • support@lnav.org[1]
+ • Discord[2]
+ • Google Groups[3]
+
+ ▌[1] - mailto:support@lnav.org
+ ▌[2] - https://discord.gg/erBPnKwz7R
+ ▌[3] - https://groups.google.com/g/lnav
+
+Links
+
+ • Main Site[1]
+ • Documentation[2] on Read the Docs
+ • Internal Architecture[3]
+
+ ▌[1] - https://lnav.org
+ ▌[2] - https://docs.lnav.org
+ ▌[3] - file://{top_srcdir}/ARCHITECTURE.md
+
+Contributing
+
+ • Become a Sponsor on GitHub[1]
+
+ ▌[1] - https://github.com/sponsors/tstack
+
+Building From Source
+
+Prerequisites
+
+The following software packages are required to build lnav:
+
+ • gcc/clang - A C++14-compatible compiler.
+ • libpcre2 - The Perl Compatible Regular Expression v2
+ (PCRE2) library.
+ • sqlite - The SQLite database engine. Version 3.9.0
+ or higher is required.
+ • ncurses - The ncurses text UI library.
+ • readline - The readline line editing library.
+ • zlib - The zlib compression library.
+ • bz2 - The bzip2 compression library.
+ • libcurl - The cURL library for downloading files
+ from URLs. Version 7.23.0 or higher is required.
+ • libarchive - The libarchive library for opening archive
+ files, like zip/tgz.
+ • wireshark - The 'tshark' program is used to interpret
+ pcap files.
+
+Build
+
+Lnav follows the usual GNU style for configuring and installing
+software:
+
+Run  ./autogen.sh  if compiling from a cloned repository.
+
+ ▌$ ./configure 
+ ▌$ make 
+ ▌$ sudo make install 
+
+See Also
+
+Angle-grinder[1] is a tool to slice and dice log files on the
+command-line. If you're familiar with the SumoLogic query language,
+you might find this tool more comfortable to work with.
+
+ ▌[1] - https://github.com/rcoh/angle-grinder
+
diff --git a/test/expected/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.err b/test/expected/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.err
diff --git a/test/expected/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.out b/test/expected/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.out
new file mode 100644
index 0000000..4855b3d
--- /dev/null
+++ b/test/expected/test_text_file.sh_7b00f32a3fff7fc2d78a87045ae842e58be88480.out
@@ -0,0 +1,10 @@
+ * Package: sys-libs/glibc-2.36-r7:2.2
+ * Repository: gentoo
+ * Maintainer: toolchain@gentoo.org
+ * USE: abi_x86_64 amd64 caps elibc_glibc kernel_linux multiarch ssp static-libs test userland_GNU
+ * FEATURES: network-sandbox preserve-libs sandbox test userpriv usersandbox
+ * Checking whether python3_11 is suitable ...
+ * >=dev-lang/python-3.11.1-r1:3.11 ...
+ [ ok ]
+ * Using python3.11 to build (via PYTHON_COMPAT iteration)
+>>> Unpacking source...
diff --git a/test/expected/test_text_file.sh_801414c6bb6d3f9225973eafa3c6dfa49cd2081d.err b/test/expected/test_text_file.sh_801414c6bb6d3f9225973eafa3c6dfa49cd2081d.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_801414c6bb6d3f9225973eafa3c6dfa49cd2081d.err
diff --git a/test/expected/test_text_file.sh_801414c6bb6d3f9225973eafa3c6dfa49cd2081d.out b/test/expected/test_text_file.sh_801414c6bb6d3f9225973eafa3c6dfa49cd2081d.out
new file mode 100644
index 0000000..a7ed740
--- /dev/null
+++ b/test/expected/test_text_file.sh_801414c6bb6d3f9225973eafa3c6dfa49cd2081d.out
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "log_level.hh"
+
+#include <ctype.h>
+
+#include "config.h"
+
+const char* level_names[LEVEL__MAX + 1] = {
+ "unknown",
+ "trace",
+ "debug5",
+ "debug4",
+ "debug3",
+ "debug2",
+ "debug",
+ "info",
+ "stats",
+ "notice",
+ "warning",
+ "error",
+ "critical",
+ "fatal",
+ "invalid",
+
+ nullptr,
+};
+
+log_level_t
+abbrev2level(const char* levelstr, ssize_t len)
+{
+ if (len == 0 || levelstr[0] == '\0') {
+ return LEVEL_UNKNOWN;
+ }
+
+ switch (toupper(levelstr[0])) {
+ case 'T':
+ return LEVEL_TRACE;
+ case 'D':
+ case 'V':
+ if (len > 1) {
+ switch (levelstr[len - 1]) {
+ case '2':
+ return LEVEL_DEBUG2;
+ case '3':
+ return LEVEL_DEBUG3;
+ case '4':
+ return LEVEL_DEBUG4;
+ case '5':
+ return LEVEL_DEBUG5;
+ }
+ }
+ return LEVEL_DEBUG;
+ case 'I':
+ if (len == 7 && toupper(levelstr[1]) == 'N'
+ && toupper(levelstr[2]) == 'V' && toupper(levelstr[3]) == 'A'
+ && toupper(levelstr[4]) == 'L' && toupper(levelstr[5]) == 'I'
+ && toupper(levelstr[6]) == 'D')
+ {
+ return LEVEL_INVALID;
+ }
+ return LEVEL_INFO;
+ case 'S':
+ return LEVEL_STATS;
+ case 'N':
+ return LEVEL_NOTICE;
+ case 'W':
+ return LEVEL_WARNING;
+ case 'E':
+ return LEVEL_ERROR;
+ case 'C':
+ return LEVEL_CRITICAL;
+ case 'F':
+ return LEVEL_FATAL;
+ default:
+ return LEVEL_UNKNOWN;
+ }
+}
+
+int
+levelcmp(const char* l1, ssize_t l1_len, const char* l2, ssize_t l2_len)
+{
+ return abbrev2level(l1, l1_len) - abbrev2level(l2, l2_len);
+}
diff --git a/test/expected/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.err b/test/expected/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.err
diff --git a/test/expected/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.out b/test/expected/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.out
new file mode 100644
index 0000000..ae06214
--- /dev/null
+++ b/test/expected/test_text_file.sh_87943c6be50d701a03e901f16493314c839af1ab.out
@@ -0,0 +1,111 @@
+/**
+ * Copyright (c) 2018, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "log_level.hh"
+
+#include <ctype.h>
+
+#include "config.h"
+
+const char* level_names[LEVEL__MAX + 1] = {
+ "unknown",
+ "trace",
+ "debug5",
+ "debug4",
+ "debug3",
+ "debug2",
+ "debug",
+ "info",
+ "stats",
+ "notice",
+ "warning",
+ "error",
+ "critical",
+ "fatal",
+ "invalid",
+
+ nullptr,
+};
+
+log_level_t
+abbrev2level(const char* levelstr, ssize_t len)
+{
+ if (len == 0 || levelstr[0] == '\0') {
+ return LEVEL_UNKNOWN;
+ }
+
+ switch (toupper(levelstr[0])) {
+ case 'T':
+ return LEVEL_TRACE;
+ case 'D':
+ case 'V':
+ if (len > 1) {
+ switch (levelstr[len - 1]) {
+ case '2':
+ return LEVEL_DEBUG2;
+ case '3':
+ return LEVEL_DEBUG3;
+ case '4':
+ return LEVEL_DEBUG4;
+ case '5':
+ return LEVEL_DEBUG5;
+ }
+ }
+ return LEVEL_DEBUG;
+ case 'I':
+ if (len == 7 && toupper(levelstr[1]) == 'N'
+ && toupper(levelstr[2]) == 'V' && toupper(levelstr[3]) == 'A'
+ && toupper(levelstr[4]) == 'L' && toupper(levelstr[5]) == 'I'
+ && toupper(levelstr[6]) == 'D')
+ {
+ return LEVEL_INVALID;
+ }
+ return LEVEL_INFO;
+ case 'S':
+ return LEVEL_STATS;
+ case 'N':
+ return LEVEL_NOTICE;
+ case 'W':
+ return LEVEL_WARNING;
+ case 'E':
+ return LEVEL_ERROR;
+ case 'C':
+ return LEVEL_CRITICAL;
+ case 'F':
+ return LEVEL_FATAL;
+ default:
+ return LEVEL_UNKNOWN;
+ }
+}
+
+int
+levelcmp(const char* l1, ssize_t l1_len, const char* l2, ssize_t l2_len)
+{
+ return abbrev2level(l1, l1_len) - abbrev2level(l2, l2_len);
+}
diff --git a/test/expected/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.err b/test/expected/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.err
diff --git a/test/expected/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.out b/test/expected/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.out
new file mode 100644
index 0000000..c8719bb
--- /dev/null
+++ b/test/expected/test_text_file.sh_8b2cd055e6a1db2ed9b2af2a917f8556395fa653.out
@@ -0,0 +1,2 @@
+ filepath  descriptor  mimetype  content 
+{test_dir}/textfile_0.md net.daringfireball.markdown.frontmatter application/json {␊ "comment": "This is JSON front-matter"␊} 
diff --git a/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.err b/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.err
diff --git a/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.out b/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.out
new file mode 100644
index 0000000..5a1b89a
--- /dev/null
+++ b/test/expected/test_text_file.sh_ac486314c4e02e480d829ea2f077b86c49fedcec.out
@@ -0,0 +1,4 @@
+you might find this tool more comfortable to work with.
+
+ ▌[1] - https://github.com/rcoh/angle-grinder
+
diff --git a/test/expected/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.err b/test/expected/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.err
diff --git a/test/expected/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.out b/test/expected/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.out
new file mode 100644
index 0000000..6c9b5ae
--- /dev/null
+++ b/test/expected/test_text_file.sh_ac872aadda29b9a824361a2c711d62ec1c75d40f.out
@@ -0,0 +1,74 @@
+✘ error: unable to parse markdown file
+ reason: file has invalid UTF-8 at offset 4461: Expecting bytes in the following ranges: 00..7F C2..F4.
+
+UTF-8 decoder capability and stress test
+----------------------------------------
+
+Markus Kuhn <http://www.cl.cam.ac.uk/~mgk25/> - 2015-08-28 - CC BY 4.0
+
+This test file can help you examine, how your UTF-8 decoder handles
+various types of correct, malformed, or otherwise interesting UTF-8
+sequences. This file is not meant to be a conformance test. It does
+not prescribe any particular outcome. Therefore, there is no way to
+"pass" or "fail" this test file, even though the text does suggest a
+preferable decoder behaviour at some places. Its aim is, instead, to
+help you think about, and test, the behaviour of your UTF-8 decoder on a
+systematic collection of unusual inputs. Experience so far suggests
+that most first-time authors of UTF-8 decoders find at least one
+serious problem in their decoder using this file.
+
+The test lines below cover boundary conditions, malformed UTF-8
+sequences, as well as correctly encoded UTF-8 sequences of Unicode code
+points that should never occur in a correct UTF-8 file.
+
+According to ISO 10646-1:2000, sections D.7 and 2.3c, a device
+receiving UTF-8 shall interpret a "malformed sequence in the same way
+that it interprets a character that is outside the adopted subset" and
+"characters that are not within the adopted subset shall be indicated
+to the user" by a receiving device. One commonly used approach in
+UTF-8 decoders is to replace any malformed UTF-8 sequence by a
+replacement character (U+FFFD), which looks a bit like an inverted
+question mark, or a similar symbol. It might be a good idea to
+visually distinguish a malformed UTF-8 sequence from a correctly
+encoded Unicode character that is just not available in the current
+font but otherwise fully legal, even though ISO 10646-1 doesn't
+mandate this. In any case, just ignoring malformed sequences or
+unavailable characters does not conform to ISO 10646, will make
+debugging more difficult, and can lead to user confusion.
+
+Please check, whether a malformed UTF-8 sequence is (1) represented at
+all, (2) represented by exactly one single replacement character (or
+equivalent signal), and (3) the following quotation mark after an
+illegal UTF-8 sequence is correctly displayed, i.e. proper
+resynchronization takes place immediately after any malformed
+sequence. This file says "THE END" in the last line, so if you don't
+see that, your decoder crashed somehow before, which should always be
+cause for concern.
+
+All lines in this file are exactly 79 characters long (plus the line
+feed). In addition, all lines end with "|", except for the two test
+lines 2.1.1 and 2.2.1, which contain non-printable ASCII controls
+U+0000 and U+007F. If you display this file with a fixed-width font,
+these "|" characters should all line up in column 79 (right margin).
+This allows you to test quickly, whether your UTF-8 decoder finds the
+correct number of characters in every line, that is whether each
+malformed sequences is replaced by a single replacement character.
+
+Note that, as an alternative to the notion of malformed sequence used
+here, it is also a perfectly acceptable (and in some situations even
+preferable) solution to represent each individual byte of a malformed
+sequence with a replacement character. If you follow this strategy in
+your decoder, then please ignore the "|" column.
+
+
+Here come the tests: |
+ |
+1 Some correct UTF-8 text |
+ |
+You should see the Greek word 'kosme': "κόσμε" |
+ |
+2 Boundary condition test cases |
+ |
+2.1 First possible sequence of a certain length |
+ |
+2.1.1 1 byte (U-00000000): "
diff --git a/test/expected/test_text_file.sh_c21295f131c221861568bda5014b76ef99bdd11f.err b/test/expected/test_text_file.sh_c21295f131c221861568bda5014b76ef99bdd11f.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_c21295f131c221861568bda5014b76ef99bdd11f.err
diff --git a/test/expected/test_text_file.sh_c21295f131c221861568bda5014b76ef99bdd11f.out b/test/expected/test_text_file.sh_c21295f131c221861568bda5014b76ef99bdd11f.out
new file mode 100644
index 0000000..59a0aa9
--- /dev/null
+++ b/test/expected/test_text_file.sh_c21295f131c221861568bda5014b76ef99bdd11f.out
@@ -0,0 +1,159 @@
+Build[1][2] Docs[3][4] Coverage Status[5][6] lnav[7][8]
+
+ ▌[1] - https://github.com/tstack/lnav/workflows/ci-build/badge.svg
+ ▌[2] - https://github.com/tstack/lnav/actions?query=workflow%3Aci-build
+ ▌[3] - https://readthedocs.org/projects/lnav/badge/?version=latest&style=plastic
+ ▌[4] - https://docs.lnav.org
+ ▌[5] - https://coveralls.io/repos/github/tstack/lnav/badge.svg?branch=master
+ ▌[6] - https://coveralls.io/github/tstack/lnav?branch=master
+ ▌[7] - https://snapcraft.io//lnav/badge.svg
+ ▌[8] - https://snapcraft.io/lnav
+
+This is the source repository for lnav, visit https://lnav.org[1] for
+a high level overview.
+
+ ▌[1] - https://lnav.org
+
+LNAV – The Logfile Navigator
+
+The Log File Navigator, lnav for short, is an advanced log file viewer
+for the small-scale. It is a terminal application that can understand
+your log files and make it easy for you to find problems with little
+to no setup.
+
+Screenshot
+
+The following screenshot shows a syslog file. Log lines are displayed
+with highlights. Errors are red and warnings are yellow.
+
+Screenshot[1][2]
+
+ ▌[1] - file://{top_srcdir}/docs/assets/images/lnav-syslog-thumb.png
+ ▌[2] - file://{top_srcdir}/docs/assets/images/lnav-syslog.png
+
+Features
+
+ • Log messages from different files are collated together
+ into a single view
+ • Automatic detection of log format
+ • Automatic decompression of GZip and BZip2 files
+ • Filter log messages based on regular expressions
+ • Use SQL to analyze your logs
+ • And more...
+
+Installation
+
+Download a statically-linked binary for Linux/MacOS from the release
+page[1]
+
+ ▌[1] - https://github.com/tstack/lnav/releases/latest#release-artifacts
+
+Usage
+
+The only file installed is the executable,  lnav . You can execute it
+with no arguments to view the default set of files:
+
+ ▌$ lnav 
+
+You can view all the syslog messages by running:
+
+ ▌$ lnav /var/log/messages* 
+
+Usage with  systemd-journald 
+
+On systems running  systemd-journald , you can use  lnav  as the
+pager:
+
+ ▌$ journalctl | lnav 
+
+or in follow mode:
+
+ ▌$ journalctl -f | lnav 
+
+Since  journalctl 's default output format omits the year, if you are
+viewing logs which span multiple years you will need to change the
+output format to include the year, otherwise  lnav  gets confused:
+
+ ▌$ journalctl -o short-iso | lnav 
+
+It is also possible to use  journalctl 's json output format and  lnav
+will make use of additional fields such as PRIORITY and _SYSTEMD_UNIT:
+
+ ▌$ journalctl -o json | lnav 
+
+In case some MESSAGE fields contain special characters such as ANSI
+color codes which are considered as unprintable by journalctl,
+specifying  journalctl 's  -a  option might be preferable in order to
+output those messages still in a non binary representation:
+
+ ▌$ journalctl -a -o json | lnav 
+
+If using systemd v236 or newer, the output fields can be limited to
+the ones actually recognized by  lnav  for increased efficiency:
+
+ ▌$ journalctl -o json --output-fields=MESSAGE,PRIORITY,_PID,SYSLOG_IDENTIFIER,_SYSTEMD_UNIT | lnav 
+
+If your system has been running for a long time, for increased
+efficiency you may want to limit the number of log lines fed into  lnav
+, e.g. via  journalctl 's  -n  or  --since=...  options.
+
+In case of a persistent journal, you may want to limit the number of
+log lines fed into  lnav  via  journalctl 's  -b  option.
+
+Links
+
+ • Main Site[1]
+ • Documentation[2] on Read the Docs
+ • Internal Architecture[3]
+
+ ▌[1] - https://lnav.org
+ ▌[2] - https://docs.lnav.org
+ ▌[3] - file://{top_srcdir}/ARCHITECTURE.md
+
+Contributing
+
+ • Become a Sponsor on GitHub[1]
+
+ ▌[1] - https://github.com/sponsors/tstack
+
+Building From Source
+
+Prerequisites
+
+The following software packages are required to build lnav:
+
+ • gcc/clang - A C++14-compatible compiler.
+ • libpcre - The Perl Compatible Regular Expression
+ (PCRE) library.
+ • sqlite - The SQLite database engine. Version 3.9.0
+ or higher is required.
+ • ncurses - The ncurses text UI library.
+ • readline - The readline line editing library.
+ • zlib - The zlib compression library.
+ • bz2 - The bzip2 compression library.
+ • libcurl - The cURL library for downloading files
+ from URLs. Version 7.23.0 or higher is required.
+ • libarchive - The libarchive library for opening archive
+ files, like zip/tgz.
+ • wireshark - The 'tshark' program is used to interpret
+ pcap files.
+
+Build
+
+Lnav follows the usual GNU style for configuring and installing
+software:
+
+Run  ./autogen.sh  if compiling from a cloned repository.
+
+ ▌$ ./configure 
+ ▌$ make 
+ ▌$ sudo make install 
+
+See Also
+
+Angle-grinder[1] is a tool to slice and dice log files on the
+command-line. If you're familiar with the SumoLogic query language,
+you might find this tool more comfortable to work with.
+
+ ▌[1] - https://github.com/rcoh/angle-grinder
+
diff --git a/test/expected/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.err b/test/expected/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.err
diff --git a/test/expected/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.out b/test/expected/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.out
new file mode 100644
index 0000000..1efd1ba
--- /dev/null
+++ b/test/expected/test_text_file.sh_c2a346ca1da2da4346f1d310212e166767993ce9.out
@@ -0,0 +1,58 @@
+[
+ {
+ "top_meta": {
+ "file": "{top_srcdir}/README.md",
+ "anchor": "#support",
+ "breadcrumbs": [
+ {
+ "display_value": "README.md",
+ "search_placeholder": "",
+ "possibilities": [
+ {
+ "display_value": "README.md"
+ }
+ ]
+ },
+ {
+ "display_value": "LNAV – The Logfile Navigator",
+ "search_placeholder": "",
+ "possibilities": [
+ {
+ "display_value": "LNAV – The Logfile Navigator"
+ }
+ ]
+ },
+ {
+ "display_value": "Support",
+ "search_placeholder": "",
+ "possibilities": [
+ {
+ "display_value": "Contributing"
+ },
+ {
+ "display_value": "Features"
+ },
+ {
+ "display_value": "Installation"
+ },
+ {
+ "display_value": "Links"
+ },
+ {
+ "display_value": "Screenshot"
+ },
+ {
+ "display_value": "See Also"
+ },
+ {
+ "display_value": "Support"
+ },
+ {
+ "display_value": "Usage"
+ }
+ ]
+ }
+ ]
+ }
+ }
+]
diff --git a/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.err b/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.err
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.err
diff --git a/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out b/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out
new file mode 100644
index 0000000..accb1c3
--- /dev/null
+++ b/test/expected/test_text_file.sh_e088ea61a5382458cc48a2607e2639e52b0be1da.out
@@ -0,0 +1,149 @@
+Screenshot
+
+The following screenshot shows a syslog file. Log lines are displayed
+with highlights. Errors are red and warnings are yellow.
+
+Screenshot[1][2]
+
+ ▌[1] - file://{top_srcdir}/docs/assets/images/lnav-syslog-thumb.png
+ ▌[2] - file://{top_srcdir}/docs/assets/images/lnav-syslog.png
+
+Features
+
+ • Log messages from different files are collated together
+ into a single view
+ • Automatic detection of log format
+ • Automatic decompression of GZip and BZip2 files
+ • Filter log messages based on regular expressions
+ • Use SQL to analyze your logs
+ • And more...
+
+Installation
+
+Download a statically-linked binary for Linux/MacOS from the release
+page[1]
+
+ ▌[1] - https://github.com/tstack/lnav/releases/latest#release-artifacts
+
+Usage
+
+The only file installed is the executable,  lnav . You can execute it
+with no arguments to view the default set of files:
+
+ ▌$ lnav 
+
+You can view all the syslog messages by running:
+
+ ▌$ lnav /var/log/messages* 
+
+Usage with  systemd-journald 
+
+On systems running  systemd-journald , you can use  lnav  as the
+pager:
+
+ ▌$ journalctl | lnav 
+
+or in follow mode:
+
+ ▌$ journalctl -f | lnav 
+
+Since  journalctl 's default output format omits the year, if you are
+viewing logs which span multiple years you will need to change the
+output format to include the year, otherwise  lnav  gets confused:
+
+ ▌$ journalctl -o short-iso | lnav 
+
+It is also possible to use  journalctl 's json output format and  lnav
+will make use of additional fields such as PRIORITY and _SYSTEMD_UNIT:
+
+ ▌$ journalctl -o json | lnav 
+
+In case some MESSAGE fields contain special characters such as ANSI
+color codes which are considered as unprintable by journalctl,
+specifying  journalctl 's  -a  option might be preferable in order to
+output those messages still in a non-binary representation:
+
+ ▌$ journalctl -a -o json | lnav 
+
+If using systemd v236 or newer, the output fields can be limited to
+the ones actually recognized by  lnav  for increased efficiency:
+
+ ▌$ journalctl -o json --output-fields=MESSAGE,PRIORITY,_PID,SYSLOG_IDENTIFIER,_SYSTEMD_UNIT | lnav 
+
+If your system has been running for a long time, for increased
+efficiency you may want to limit the number of log lines fed into  lnav
+, e.g. via  journalctl 's  -n  or  --since=...  options.
+
+In case of a persistent journal, you may want to limit the number of
+log lines fed into  lnav  via  journalctl 's  -b  option.
+
+Support
+
+Please file issues on this repository or use the discussions section.
+The following alternatives are also available:
+
+ • support@lnav.org[1]
+ • Discord[2]
+ • Google Groups[3]
+
+ ▌[1] - mailto:support@lnav.org
+ ▌[2] - https://discord.gg/erBPnKwz7R
+ ▌[3] - https://groups.google.com/g/lnav
+
+Links
+
+ • Main Site[1]
+ • Documentation[2] on Read the Docs
+ • Internal Architecture[3]
+
+ ▌[1] - https://lnav.org
+ ▌[2] - https://docs.lnav.org
+ ▌[3] - file://{top_srcdir}/ARCHITECTURE.md
+
+Contributing
+
+ • Become a Sponsor on GitHub[1]
+
+ ▌[1] - https://github.com/sponsors/tstack
+
+Building From Source
+
+Prerequisites
+
+The following software packages are required to build lnav:
+
+ • gcc/clang - A C++14-compatible compiler.
+ • libpcre2 - The Perl Compatible Regular Expression v2
+ (PCRE2) library.
+ • sqlite - The SQLite database engine. Version 3.9.0
+ or higher is required.
+ • ncurses - The ncurses text UI library.
+ • readline - The readline line editing library.
+ • zlib - The zlib compression library.
+ • bz2 - The bzip2 compression library.
+ • libcurl - The cURL library for downloading files
+ from URLs. Version 7.23.0 or higher is required.
+ • libarchive - The libarchive library for opening archive
+ files, like zip/tgz.
+ • wireshark - The 'tshark' program is used to interpret
+ pcap files.
+
+Build
+
+Lnav follows the usual GNU style for configuring and installing
+software:
+
+Run  ./autogen.sh  if compiling from a cloned repository.
+
+ ▌$ ./configure 
+ ▌$ make 
+ ▌$ sudo make install 
+
+See Also
+
+Angle-grinder[1] is a tool to slice and dice log files on the
+command-line. If you're familiar with the SumoLogic query language,
+you might find this tool more comfortable to work with.
+
+ ▌[1] - https://github.com/rcoh/angle-grinder
+
diff --git a/test/expected_help.txt b/test/expected_help.txt
new file mode 100644
index 0000000..27f9c8a
--- /dev/null
+++ b/test/expected_help.txt
@@ -0,0 +1,4016 @@
+
+lnav
+
+A fancy log file viewer for the terminal.
+
+Overview
+
+The Logfile Navigator, lnav, is an enhanced log file viewer that takes
+advantage of any semantic information that can be gleaned from the
+files being viewed, such as timestamps and log levels. Using this
+extra semantic information, lnav can do things like interleaving
+messages from different files, generate histograms of messages over
+time, and providing hotkeys for navigating through the file. It is
+hoped that these features will allow the user to quickly and
+efficiently zero in on problems.
+
+Opening Paths/URLs
+
+The main arguments to lnav are the files, directories, glob patterns,
+or URLs to be viewed. If no arguments are given, the default syslog
+file for your system will be opened. These arguments will be polled
+periodically so that any new data or files will be automatically
+loaded. If a previously loaded file is removed or replaced, it will be
+closed and the replacement opened.
+
+Note: When opening SFTP URLs, if the password is not provided for the
+host, the SSH agent can be used to do authentication.
+
+Options
+
+Lnav takes a list of files to view and/or you can use the flag
+arguments to load well-known log files, such as the syslog log files.
+The flag arguments are:
+
+ • -a Load all of the most recent log file types.
+ • -r Recursively load files from the given directory
+ hierarchies.
+ • -R Load older rotated log files as well.
+
+When using the flag arguments, lnav will look for the files relative
+to the current directory and its parent directories. In other words,
+if you are working within a directory that has the well-known log
+files, those will be preferred over any others.
+
+If you do not want the default syslog file to be loaded when no files
+are specified, you can pass the -N flag.
+
+Any files given on the command-line are scanned to determine their log
+file format and to create an index for each line in the file. You do
+not have to manually specify the log file format. The currently
+supported formats are: syslog, apache, strace, tcsh history, and
+generic log files with timestamps.
+
+Lnav will also display data piped in on the standard input. The
+following options are available when doing so:
+
+ • -t Prepend timestamps to the lines of data being read
+ in on the standard input.
+ • -w file Write the contents of the standard input to
+ this file.
+
+To automatically execute queries or lnav commands after the files have
+been loaded, you can use the following options:
+
+ • -c cmd A command, query, or file to execute. The
+ first character determines the type of operation: a colon
+ ( : ) is used for the built-in commands; a semi-colon ( ;
+ ) for SQL queries; and a pipe symbol ( | ) for executing
+ a file containing other commands. For example, to open
+ the file "foo.log" and go to the tenth line in the file,
+ you can do:
+
+ ┃lnav -c ':goto 10' foo.log
+
+ This option can be given multiple times to execute
+ multiple operations in sequence.
+ • -f file A file that contains commands, queries, or
+ files to execute. This option is a shortcut for -c '|file'
+ . You can use a dash ( - ) to execute commands from the
+ standard input.
+
+To execute commands/queries without the opening the interactive text
+UI, you can pass the -n option. This combination of options allows
+you to write scripts for processing logs with lnav. For example, to
+get a list of IP addresses that dhclient has bound to in CSV format:
+
+ ┃#! /usr/bin/lnav -nf
+ ┃
+ ┃# Usage: dhcp_ip.lnav /var/log/messages
+ ┃# Only include lines that look like:
+ ┃# Apr 29 00:31:56 example-centos5 dhclient: bound to 10.1.10.103 -- renewal in 9938 seconds.
+ ┃
+ ┃:filter-in dhclient: bound to
+ ┃
+ ┃# The log message parser will extract the IP address
+ ┃# as col_0, so we select that and alias it to "dhcp_ip".
+ ┃;select distinct col_0 as dhcp_ip from logline;
+ ┃
+ ┃# Finally, write the results of the query to stdout.
+ ┃:write-csv-to -
+
+Display
+
+The main part of the display shows the log lines from the files
+interleaved based on time-of-day. New lines are automatically loaded
+as they are appended to the files and, if you are viewing the bottom
+of the files, lnav will scroll down to display the new lines, much
+like tail -f .
+
+On color displays, the lines will be highlighted as follows:
+
+ • Errors will be colored in red;
+ • warnings will be yellow;
+ • boundaries between days will be underlined; and
+ • various color highlights will be applied to: IP
+ addresses, SQL keywords, XML tags, file and line numbers
+ in Java backtraces, and quoted strings.
+
+To give you an idea of where you are spatially, the right side of the
+display has a proportionally sized 'scroll bar' that indicates your
+current position in the files. The scroll bar will also show areas of
+the file where warnings or errors are detected by coloring the bar
+yellow or red, respectively. Tick marks will also be added to the left
+and right hand side of the bar, for search hits and bookmarks.
+
+A bar on the left side is color coded and broken up to indicate which
+messages are from the same file. Pressing the left-arrow or h will
+reveal the source file names for each message and pressing again will
+show the full paths.
+
+Above and below the main body are status lines that display:
+
+ • the current time;
+ • the name of the file the top line was pulled from;
+ • the log format for the top line;
+ • the current view;
+ • the line number for the top line in the display;
+ • the current search hit, the total number of hits, and
+ the search term;
+
+If the view supports filtering, there will be a status line showing
+the following:
+
+ • the number of enabled filters and the total number of
+ filters;
+ • the number of lines not displayed because of filtering.
+
+To edit the filters, you can press TAB to change the focus from the
+main view to the filter editor. The editor allows you to create,
+enable/disable, and delete filters easily.
+
+Finally, the last line on the display is where you can enter search
+patterns and execute internal commands, such as converting a unix-
+timestamp into a human-readable date. The command-line is implemented
+using the readline library, so the usual set of keyboard shortcuts are
+available. Most commands and searches also support tab-completion.
+
+The body of the display is also used to display other content, such
+as: the help file, histograms of the log messages over time, and SQL
+results. The views are organized into a stack so that any time you
+activate a new view with a key press or command, the new view is
+pushed onto the stack. Pressing the same key again will pop the view
+off of the stack and return you to the previous view. Note that you
+can always use q to pop the top view off of the stack.
+
+Default Key Bindings
+
+Views
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════
+ ? View/leave this help message.
+ q Leave the current view or quit the program when in
+ the log file view.
+ Q Similar to q , except it will try to sync the top
+ time between the current and former views. For
+ example, when leaving the spectrogram view with Q
+ , the top time in that view will be matched to the
+ top time in the log view.
+ TAB Toggle focusing on the filter editor or the main
+ view.
+ a/A Restore the view that was previously popped with q
+ / Q . The A hotkey will try to match the top
+ times between the two views.
+ X Close the current text file or log file.
+
+Spatial Navigation
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════════
+ g/Home Move to the top of the file.
+ G/End Move to the end of the file. If the view is
+ already at the end, it will move to the last line.
+ SPACE/PgDn Move down a page.
+ b/PgUp Move up a page.
+ j/↓ Move down a line.
+ k/↑ Move up a line.
+ h/← Move to the left. In the log view, moving left
+ will reveal the source log file names for each
+ line. Pressing again will reveal the full path.
+ l/→ Move to the right.
+ H/Shift ← Move to the left by a smaller increment.
+ L/Shift → Move to the right by a smaller increment.
+ e/E Move to the next/previous error.
+ w/W Move to the next/previous warning.
+ n/N Move to the next/previous search hit. When pressed
+ repeatedly within a short time, the view will move
+ at least a full page at a time instead of moving
+ to the next hit.
+ f/F Move to the next/previous file. In the log view,
+ this moves to the next line from a different file.
+ In the text view, this rotates the view to the
+ next file.
+ >/< Move horizontally to the next/previous search hit.
+ o/O Move forward/backward to the log message with a
+ matching 'operation ID' (opid) field.
+ u/U Move forward/backward through any user bookmarks
+ you have added using the 'm' key. This hotkey will
+ also jump to the start of any log partitions that
+ have been created with the 'partition-name'
+ command.
+ s/S Move to the next/previous "slow down" in the log
+ message rate. A slow down is detected by measuring
+ how quickly the message rate has changed over the
+ previous several messages. For example, if one
+ message is logged every second for five seconds
+ and then the last message arrives five seconds
+ later, the last message will be highlighted as a
+ slow down.
+ {/} Move to the previous/next location in history.
+ Whenever you jump to a new location in the view,
+ the location will be added to the history. The
+ history is not updated when using only the arrow
+ keys.
+
+Chronological Navigation
+
+ Key(s) Action
+══════════════════════════════════════════════════════════════════
+ d/D Move forward/backward 24 hours from the current
+ position in the log file.
+ 1-6/Shift 1-6 Move to the next/previous n'th ten minute of the
+ hour. For example, '4' would move to the first log
+ line in the fortieth minute of the current hour in
+ the log. And, '6' would move to the next hour
+ boundary.
+ 7/8 Move to the previous/next minute.
+ 0/Shift 0 Move to the next/previous day boundary.
+ r/R Move forward/backward based on the relative time
+ that was last used with the 'goto' command. For
+ example, executing ':goto a minute later' will
+ move the log view forward a minute and then
+ pressing 'r' will move it forward a minute again.
+ Pressing 'R' will then move the view in the
+ opposite direction, so backwards a minute.
+
+Bookmarks
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════
+ m Mark/unmark the line at the top of the display.
+ The line will be highlighted with reverse video to
+ indicate that it is a user bookmark. You can use
+ the u hotkey to iterate through marks you have
+ added.
+ M Mark/unmark all the lines between the top of the
+ display and the last line marked/unmarked.
+ J Mark/unmark the next line after the previously
+ marked line.
+ K Like J except it toggles the mark on the
+ previous line.
+ c Copy the marked text to the X11 selection buffer
+ or OS X clipboard.
+ C Clear all marked lines.
+
+Display options
+
+ Key(s) Action
+══════════════════════════════════════════════════════════════════
+ P Switch to/from the pretty-printed view of the log
+ or text files currently displayed. In this view,
+ structured data, such as XML, will be reformatted
+ to make it easier to read.
+ t Switch to/from the text file view. The text file
+ view is for any files that are not recognized as
+ log files.
+ = Pause/unpause loading of new file data.
+ Ctrl-L (Lo-fi mode) Exit screen-mode and write the
+ displayed log lines in plain text to the terminal
+ until a key is pressed. Useful for copying long
+ lines from the terminal without picking up any of
+ the extra decorations.
+ T Toggle the display of the "elapsed time" column
+ that shows the time elapsed since the beginning of
+ the logs or the offset from the previous bookmark.
+ Sharp changes in the message rate are highlighted
+ by coloring the separator between the time column
+ and the log message. A red highlight means the
+ message rate has slowed down and green means it
+ has sped up. You can use the "s/S" hotkeys to scan
+ through the slow downs.
+ i View/leave a histogram of the log messages over
+ time. The histogram counts the number of displayed
+ log lines for each bucket of time. The bars are
+ layed out horizontally with colored segments
+ representing the different log levels. You can use
+ the z hotkey to change the size of the time
+ buckets (e.g. ten minutes, one hour, one day).
+ I Switch between the log and histogram views while
+ keeping the time displayed at the top of each view
+ in sync. For example, if the top line in the log
+ view is "11:40", hitting I will switch to the
+ histogram view and scrolled to display "11:00" at
+ the top (if the zoom level is hours).
+ z/Shift Z Zoom in or out one step in the histogram view.
+ v Switch to/from the SQL result view.
+ V Switch between the log and SQL result views while
+ keeping the top line number in the log view in
+ sync with the log_line column in the SQL view. For
+ example, doing a query that selects for "
+ log_idle_msecs" and "log_line", you can move the
+ top of the SQL view to a line and hit 'V' to
+ switch to the log view and move to the line number
+ that was selected in the "log_line" column. If
+ there is no "log_line" column, lnav will find the
+ first column with a timestamp and move to
+ corresponding time in the log view.
+ TAB/Shift TAB In the SQL result view, cycle through the columns
+ that are graphed. Initially, all number values are
+ displayed in a stacked graph. Pressing TAB will
+ change the display to only graph the first column.
+ Repeatedly pressing TAB will cycle through the
+ columns until they are all graphed again.
+ p In the log view: enable or disable the display of
+ the fields that the log message parser knows about
+ or has discovered. This overlay is temporarily
+ enabled when the semicolon key (;) is pressed so
+ that it is easier to write queries.
+ In the DB view: enable or disable the display of
+ values in columns containing JSON-encoded values
+ in the top row. The overlay will display the JSON-
+ Pointer reference and value for all fields in the
+ JSON data.
+ CTRL-W Toggle word-wrapping.
+ CTRL-P Show/hide the data preview panel that may be
+ opened when entering commands or SQL queries.
+ CTRL-F Toggle the enabled/disabled state of all filters
+ in the current view.
+ x Toggle the hiding of log message fields. The
+ hidden fields will be replaced with three bullets
+ and highlighted in yellow.
+ CTRL-X Toggle the cursor mode. Allows moving the selected
+ line instead of keeping it fixed at the top of the
+ current screen.
+ F2 Toggle mouse support.
+
+Query
+
+ Key(s) Action
+═════════════════════════════════════════════════════════════════════════════
+ /regexp Start a search for the given regular expression.
+ The search is live, so when there is a pause in
+ typing, the currently running search will be
+ canceled and a new one started. The first ten
+ lines that match the search will be displayed in
+ the preview window at the bottom of the view.
+ History is maintained for your searches so you can
+ rerun them easily. Words that are currently
+ displayed are also available for tab-completion,
+ so you can easily search for values without
+ needing to copy-and-paste the string. If there is
+ an error encountered while trying to interpret the
+ expression, the error will be displayed in red on
+ the status line. While the search is active, the '
+ hits' field in the status line will be green, when
+ finished it will turn back to black.
+ :<command> Execute an internal command. The commands are
+ listed below. History is also supported in this
+ context as well as tab-completion for commands and
+ some arguments. The result of the command replaces
+ the command you typed.
+ ;<sql> Execute an SQL query. Most supported log file
+ formats provide a sqlite virtual table backend
+ that can be used in queries. See the SQL section
+ below for more information.
+ |<script> [arg1 .. argN] Execute an lnav script contained in a format
+ directory (e.g. ~/.lnav/formats/default). The
+ script can contain lines starting with : , ; ,
+ or | to execute commands, SQL queries or execute
+ other files in lnav. Any values after the script
+ name are treated as arguments can be referenced in
+ the script using $1 , $2 , and so on, like in a
+ shell script.
+ CTRL+], ESCAPE Abort command-line entry started with / , :
+, ;
+ , or | .
+
+ ┃ Note: The regular expression format used by is PCRE
+ ┃ (Perl-Compatible Regular Expressions). For example,
+ ┃ if you wanted to search for ethernet device names,
+ ┃ regardless of their ID number, you can type:
+ ┃
+ ┃ eth\d+
+ ┃
+ ┃ You can find more information about Perl regular
+ ┃ expressions at:
+ ┃
+ ┃ http://perldoc.perl.org/perlre.html
+ ┃
+ ┃ If the search string is not valid PCRE, a search
+ ┃ is done for the exact string instead of doing a
+ ┃ regex search.
+
+Session
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════
+ CTRL-R Reset the session state. This will save the
+ current session state (filters, highlights) and
+ then reset the state to the factory default.
+
+Filter Editor
+
+The following hotkeys are only available when the focus is on the
+filter editor. You can change the focus by pressing TAB.
+
+ Key(s) Action
+═══════════════════════════════════════════════════════════
+ q Switch the focus back to the main view.
+ j/↓ Select the next filter.
+ k/↑ Select the previous filter.
+ o Create a new "out" filter.
+ i Create a new "in" filter .
+ SPACE Toggle the enabled/disabled state of the currently
+ selected filter.
+ t Toggle the type of filter between "in" and "out".
+ ENTER Edit the selected filter.
+ D Delete the selected filter.
+
+Mouse Support (experimental)
+
+If you are using Xterm, or a compatible terminal, you can use the
+mouse to mark lines of text and move the view by grabbing the
+scrollbar.
+
+NOTE: You need to manually enable this feature by setting the LNAV_EXP
+environment variable to "mouse". F2 toggles mouse support.
+
+SQL Queries (experimental)
+
+Lnav has support for performing SQL queries on log files using the
+Sqlite3 "virtual" table feature. For all supported log file types,
+lnav will create tables that can be queried using the subset of SQL
+that is supported by Sqlite3. For example, to get the top ten URLs
+being accessed in any loaded Apache log files, you can execute:
+
+ ┃;select cs_uri_stem, count(*) as total from access_log
+ ┃ group by cs_uri_stem order by total desc limit 10;
+
+The query result view shows the results and graphs any numeric values
+found in the result, much like the histogram view.
+
+The builtin set of log tables are listed below. Note that only the log
+messages that match a particular format can be queried by a particular
+table. You can find the file format and table name for the top log
+message by looking in the upper right hand corner of the log file
+view.
+
+Some commonly used format tables are:
+
+ Name Description
+════════════════════════════════════════════════════════════════
+ access_log Apache common access log format
+ syslog_log Syslog format
+ strace_log Strace log format
+ generic_log 'Generic' log format. This table contains messages
+ from files that have a very simple format with a
+ leading timestamp followed by the message.
+
+NOTE: You can get a dump of the schema for the internal tables, and
+any attached databases, by running the .schema SQL command.
+
+The columns available for the top log line in the view will
+automatically be displayed after pressing the semicolon ( ; ) key. All
+log tables contain at least the following columns:
+
+ Column Description
+═══════════════════════════════════════════════════════════════════
+ log_line The line number in the file, starting at zero.
+ log_part The name of the partition. You can change this
+ column using an UPDATE SQL statement or with the '
+ partition-name' command. After a value is set,
+ the following log messages will have the same
+ partition name up until another name is set.
+ log_time The time of the log entry.
+ log_idle_msecs The amount of time, in milliseconds, between the
+ current log message and the previous one.
+ log_level The log level (e.g. info, error, etc...).
+ log_mark The bookmark status for the line. This column can
+ be written to using an UPDATE query.
+ log_path The full path to the file.
+ log_text The raw line of text. Note that this column is
+ not included in the result of a 'select *', but it
+ does exist.
+
+The following tables include the basic columns as listed above and
+include a few more columns since the log file format is more
+structured.
+
+ • syslog_log
+
+ Column Description
+ ═════════════════════════════════════════════════════════════════
+ log_hostname The hostname the message was received from.
+ log_procname The name of the process that sent the message.
+ log_pid The process ID of the process that sent the
+ message.
+ • access_log (The column names are the same as those in
+ the Microsoft LogParser tool.)
+
+ Column Description
+ ══════════════════════════════════════════════════════════
+ c_ip The client IP address.
+ cs_username The client user name.
+ cs_method The HTTP method.
+ cs_uri_stem The stem portion of the URI.
+ cs_uri_query The query portion of the URI.
+ cs_version The HTTP version string.
+ sc_status The status number returned to the client.
+ sc_bytes The number of bytes sent to the client.
+ cs_referrer The URL of the referring page.
+ cs_user_agent The user agent string.
+ • strace_log (Currently, you need to run strace with
+ the -tt -T options so there are timestamps for each
+ function call.)
+
+ Column Description
+ ═══════════════════════════════════════════════════════
+ funcname The name of the syscall.
+ result The result code.
+ duration The amount of time spent in the syscall.
+ arg0 - arg9 The arguments passed to the syscall.
+
+These tables are created dynamically and not stored in memory or on
+disk. If you would like to persist some information from the tables,
+you can attach another database and create tables in that database.
+For example, if you wanted to save the results from the earlier
+example of a top ten query into the "/tmp/topten.db" file, you can do:
+
+ ┃;attach database "/tmp/topten.db" as topten;
+ ┃;create table topten.foo as select cs_uri_stem, count(*) as total
+ ┃ from access_log group by cs_uri_stem order by total desc
+ ┃ limit 10;
+
+Dynamic logline Table (experimental)
+
+(NOTE: This feature is still very new and not completely reliable yet,
+use with care.)
+
+For log formats that lack message structure, lnav can parse the log
+message and attempt to extract any data fields that it finds. This
+feature is available through the logline log table. This table is
+dynamically created and defined based on the message at the top of the
+log view. For example, given the following log message from "sudo",
+lnav will create the "logline" table with columns for "TTY", "PWD", "
+USER", and "COMMAND":
+
+ ┃May 24 06:48:38 Tim-Stacks-iMac.local sudo[76387]: stack : TTY=ttys003 ; PWD=/Users/stack/github/lbuild ; USER=root ; COMMAND=/bin/echo Hello, World!
+
+Queries executed against this table will then only return results for
+other log messages that have the same format. So, if you were to
+execute the following query while viewing the above line, you might
+get the following results:
+
+ ┃;select USER,COMMAND from logline;
+
+ USER COMMAND
+═════════════════════════════════
+ root /bin/echo Hello, World!
+ mal /bin/echo Goodbye, World!
+
+The log parser works by examining each message for key/value pairs
+separated by an equal sign (=) or a colon (:). For example, in the
+previous example of a "sudo" message, the parser sees the "USER=root"
+string as a pair where the key is "USER" and the value is "root". If
+no pairs can be found, then anything that looks like a value is
+extracted and assigned a numbered column. For example, the following
+line is from "dhcpd":
+
+ ┃Sep 16 22:35:57 drill dhcpd: DHCPDISCOVER from 00:16:ce:54:4e:f3 via hme3
+
+In this case, the lnav parser recognizes that "DHCPDISCOVER", the MAC
+address and the "hme3" device name are values and not normal words.
+So, it builds a table with three columns for each of these values. The
+regular words in the message, like "from" and "via", are then used to
+find other messages with a similar format.
+
+If you would like to execute queries against log messages of different
+formats at the same time, you can use the 'create-logline-table'
+command to permanently create a table using the top line of the log
+view as a template.
+
+Other SQL Features
+
+Environment variables can be used in SQL statements by prefixing the
+variable name with a dollar-sign ($). For example, to read the value
+of the HOME variable, you can do:
+
+ ┃;SELECT $HOME;
+
+To select the syslog messages that have a hostname field that is equal
+to the HOSTNAME variable:
+
+ ┃;SELECT * FROM syslog_log WHERE log_hostname = $HOSTNAME;
+
+NOTE: Variable substitution is done for fields in the query and is not
+a plain text substitution. For example, the following statement WILL
+NOT WORK:
+
+ ┃;SELECT * FROM $TABLE_NAME; -- Syntax error
+
+Access to lnav's environment variables is also available via the "
+environ" table. The table has two columns (name, value) and can be
+read and written to using SQL SELECT, INSERT, UPDATE, and DELETE
+statements. For example, to set the "FOO" variable to the value "BAR":
+
+ ┃;INSERT INTO environ SELECT 'FOO', 'BAR';
+
+As a more complex example, you can set the variable "LAST" to the last
+syslog line number by doing:
+
+ ┃;INSERT INTO environ SELECT 'LAST', (SELECT max(log_line) FROM syslog_log);
+
+A delete will unset the environment variable:
+
+ ┃;DELETE FROM environ WHERE name='LAST';
+
+The table allows you to easily use the results of a SQL query in lnav
+commands, which is especially useful when scripting lnav.
+
+Contact
+
+For more information, visit the lnav website at:
+
+http://lnav.org[1]
+
+ ┃[1] - http://lnav.org
+
+For support questions, email:
+
+lnav@googlegroups.com[1] support@lnav.org[2]
+
+ ┃[1] - mailto:lnav@googlegroups.com
+ ┃[2] - mailto:support@lnav.org
+
+Command Reference
+
+:adjust-log-time timestamp
+══════════════════════════════════════════════════════════════════════
+ Change the timestamps of the top file to be relative to the given
+ date
+Parameter
+ timestamp The new timestamp for the top line in the view
+
+Examples
+#1 To set the top timestamp to a given date:
+ :adjust-log-time 2017-01-02T05:33:00
+
+
+#2 To set the top timestamp back an hour:
+ :adjust-log-time -1h
+
+
+
+:alt-msg msg
+══════════════════════════════════════════════════════════════════════
+ Display a message in the alternate command position
+Parameter
+ msg The message to display
+See Also
+ :echo, :eval, :redirect-to, :write-csv-to, :write-json-to,
+ :write-jsonlines-to, :write-raw-to, :write-screen-to, :write-table-to,
+ :write-to, :write-view-to
+Example
+#1 To display 'Press t to switch to the text view' on the bottom right:
+ :alt-msg Press t to switch to the text view
+
+
+
+:append-to path
+══════════════════════════════════════════════════════════════════════
+ Append marked lines in the current view to the given file
+Parameter
+ path The path to the file to append to
+See Also
+ :echo, :pipe-line-to, :pipe-to, :redirect-to, :write-csv-to,
+ :write-json-to, :write-jsonlines-to, :write-raw-to, :write-screen-to,
+ :write-table-to, :write-to, :write-view-to
+Example
+#1 To append marked lines to the file /tmp/interesting-lines.txt:
+ :append-to /tmp/interesting-lines.txt
+
+
+
+:clear-comment
+══════════════════════════════════════════════════════════════════════
+ Clear the comment attached to the top log line
+See Also
+ :comment, :tag
+
+:clear-filter-expr
+══════════════════════════════════════════════════════════════════════
+ Clear the filter expression
+See Also
+ :filter-expr, :filter-in, :filter-out, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+
+:clear-highlight pattern
+══════════════════════════════════════════════════════════════════════
+ Remove a previously set highlight regular expression
+Parameter
+ pattern The regular expression previously used with :highlight
+See Also
+ :enable-word-wrap, :hide-fields, :highlight
+Example
+#1 To clear the highlight with the pattern 'foobar':
+ :clear-highlight foobar
+
+
+
+:clear-mark-expr
+══════════════════════════════════════════════════════════════════════
+ Clear the mark expression
+See Also
+ :hide-unmarked-lines, :mark, :mark-expr, :next-mark, :prev-mark
+
+:clear-partition
+══════════════════════════════════════════════════════════════════════
+ Clear the partition the top line is a part of
+
+
+:close
+══════════════════════════════════════════════════════════════════════
+ Close the top file in the view
+
+
+:comment text
+══════════════════════════════════════════════════════════════════════
+ Attach a comment to the top log line
+Parameter
+ text The comment text
+See Also
+ :clear-comment, :tag
+Example
+#1 To add the comment 'This is where it all went wrong' to the top line:
+ :comment This is where it all went wrong
+
+
+
+:config option [value]
+══════════════════════════════════════════════════════════════════════
+ Read or write a configuration option
+Parameters
+ option The path to the option to read or write
+ value The value to write. If not given, the current value is
+ returned
+See Also
+ :reset-config
+Examples
+#1 To read the configuration of the '/ui/clock-format' option:
+ :config /ui/clock-format
+
+
+#2 To set the '/ui/dim-text' option to 'false':
+ :config /ui/dim-text false
+
+
+
+:create-logline-table table-name
+══════════════════════════════════════════════════════════════════════
+ Create an SQL table using the top line of the log view as a template
+
+Parameter
+ table-name The name for the new table
+See Also
+ :create-search-table, :create-search-table, :write-csv-to,
+ :write-json-to, :write-jsonlines-to, :write-raw-to, :write-screen-to,
+ :write-table-to, :write-view-to
+Example
+#1 To create a logline-style table named 'task_durations':
+ :create-logline-table task_durations
+
+
+
+:create-search-table table-name [pattern]
+══════════════════════════════════════════════════════════════════════
+ Create an SQL table based on a regex search
+Parameters
+ table-name The name of the table to create
+ pattern The regular expression used to capture the table
+ columns. If not given, the current search pattern is
+ used.
+See Also
+ :create-logline-table, :create-logline-table, :delete-search-table,
+ :delete-search-table, :write-csv-to, :write-json-to,
+ :write-jsonlines-to, :write-raw-to, :write-screen-to, :write-table-to,
+ :write-view-to
+Example
+#1 To create a table named 'task_durations' that matches log messages with the pattern '
+ duration=(?<duration>\d+)':
+ :create-search-table task_durations duration=(?<duration>\d+)
+
+
+
+:current-time
+══════════════════════════════════════════════════════════════════════
+ Print the current time in human-readable form and seconds since the
+ epoch
+
+
+:delete-filter pattern
+══════════════════════════════════════════════════════════════════════
+ Delete the filter created with :filter-in or :filter-out
+Parameter
+ pattern The regular expression to match
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To delete the filter with the pattern 'last message repeated':
+ :delete-filter last message repeated
+
+
+
+:delete-logline-table table-name
+══════════════════════════════════════════════════════════════════════
+ Delete a table created with create-logline-table
+Parameter
+ table-name The name of the table to delete
+See Also
+ :create-logline-table, :create-logline-table, :create-search-table,
+ :create-search-table, :write-csv-to, :write-json-to,
+ :write-jsonlines-to, :write-raw-to, :write-screen-to, :write-table-to,
+ :write-view-to
+Example
+#1 To delete the logline-style table named 'task_durations':
+ :delete-logline-table task_durations
+
+
+
+:delete-search-table table-name
+══════════════════════════════════════════════════════════════════════
+ Create an SQL table based on a regex search
+Parameter
+ table-name The name of the table to create
+See Also
+ :create-logline-table, :create-logline-table, :create-search-table,
+ :create-search-table, :write-csv-to, :write-json-to,
+ :write-jsonlines-to, :write-raw-to, :write-screen-to, :write-table-to,
+ :write-view-to
+Example
+#1 To delete the search table named 'task_durations':
+ :delete-search-table task_durations
+
+
+
+:delete-tags tag1 [... tagN]
+══════════════════════════════════════════════════════════════════════
+ Remove the given tags from all log lines
+Parameter
+ tag The tags to delete
+See Also
+ :comment, :tag
+Example
+#1 To remove the tags '#BUG123' and '#needs-review' from all log lines:
+ :delete-tags #BUG123 #needs-review
+
+
+
+:disable-filter pattern
+══════════════════════════════════════════════════════════════════════
+ Disable a filter created with filter-in/filter-out
+Parameter
+ pattern The regular expression used in the filter command
+See Also
+ :enable-filter, :filter-in, :filter-out, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To disable the filter with the pattern 'last message repeated':
+ :disable-filter last message repeated
+
+
+
+:disable-word-wrap
+══════════════════════════════════════════════════════════════════════
+ Disable word-wrapping for the current view
+See Also
+ :enable-word-wrap, :hide-fields, :highlight
+
+:echo msg
+══════════════════════════════════════════════════════════════════════
+ Echo the given message to the screen or, if :redirect-to has been
+ called, to output file specified in the redirect. Variable
+ substitution is performed on the message. Use a backslash to escape
+ any special characters, like '$'
+Parameter
+ msg The message to display
+See Also
+ :alt-msg, :append-to, :eval, :pipe-line-to, :pipe-to, :redirect-to,
+ :redirect-to, :write-csv-to, :write-csv-to, :write-json-to,
+ :write-json-to, :write-jsonlines-to, :write-jsonlines-to,
+ :write-raw-to, :write-raw-to, :write-screen-to, :write-screen-to,
+ :write-table-to, :write-table-to, :write-to, :write-to, :write-view-to,
+ :write-view-to
+Example
+#1 To output 'Hello, World!':
+ :echo Hello, World!
+
+
+
+:enable-filter pattern
+══════════════════════════════════════════════════════════════════════
+ Enable a previously created and disabled filter
+Parameter
+ pattern The regular expression used in the filter command
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To enable the disabled filter with the pattern 'last message repeated':
+ :enable-filter last message repeated
+
+
+
+:enable-word-wrap
+══════════════════════════════════════════════════════════════════════
+ Enable word-wrapping for the current view
+See Also
+ :disable-word-wrap, :hide-fields, :highlight
+
+:eval command
+══════════════════════════════════════════════════════════════════════
+ Evaluate the given command/query after doing environment variable
+ substitution
+Parameter
+ command The command or query to perform substitution on.
+See Also
+ :alt-msg, :echo, :redirect-to, :write-csv-to, :write-json-to,
+ :write-jsonlines-to, :write-raw-to, :write-screen-to, :write-table-to,
+ :write-to, :write-view-to
+Example
+#1 To substitute the table name from a variable:
+ :eval ;SELECT * FROM ${table}
+
+
+
+:filter-expr expr
+══════════════════════════════════════════════════════════════════════
+ Set the filter expression
+Parameter
+ expr The SQL expression to evaluate for each log message. The
+ message values can be accessed using column names prefixed
+ with a colon
+See Also
+ :clear-filter-expr, :filter-in, :filter-out, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+Examples
+#1 To set a filter expression that matched syslog messages from 'syslogd':
+ :filter-expr :log_procname = 'syslogd'
+
+
+#2 To set a filter expression that matches log messages where 'id' is followed by a number
+ and contains the string 'foo':
+ :filter-expr :log_body REGEXP 'id\d+' AND :log_body REGEXP 'foo'
+
+
+
+:filter-in pattern
+══════════════════════════════════════════════════════════════════════
+ Only show lines that match the given regular expression in the
+ current view
+Parameter
+ pattern The regular expression to match
+See Also
+ :delete-filter, :disable-filter, :filter-out, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To filter out log messages that do not have the string 'dhclient':
+ :filter-in dhclient
+
+
+
+:filter-out pattern
+══════════════════════════════════════════════════════════════════════
+ Remove lines that match the given regular expression in the current
+ view
+Parameter
+ pattern The regular expression to match
+See Also
+ :delete-filter, :disable-filter, :filter-in, :hide-lines-after,
+ :hide-lines-before, :hide-unmarked-lines, :toggle-filtering
+Example
+#1 To filter out log messages that contain the string 'last message repeated':
+ :filter-out last message repeated
+
+
+
+:goto line#|N%|date
+══════════════════════════════════════════════════════════════════════
+ Go to the given location in the top view
+Parameter
+ line#|N%|date A line number, percent into the file, or a timestamp
+
+See Also
+ :next-location, :next-mark, :prev-location, :prev-mark, :relative-goto
+Examples
+#1 To go to line 22:
+ :goto 22
+
+
+#2 To go to the line 75% of the way into the view:
+ :goto 75%
+
+
+#3 To go to the first message on the first day of 2017:
+ :goto 2017-01-01
+
+
+
+:help
+══════════════════════════════════════════════════════════════════════
+ Open the help text view
+
+
+:hide-fields field-name1 [... field-nameN]
+══════════════════════════════════════════════════════════════════════
+ Hide log message fields by replacing them with an ellipsis
+Parameter
+ field-name The name of the field to hide in the format for the top
+ log line. A qualified name can be used where the field
+ name is prefixed by the format name and a dot to hide
+ any field.
+See Also
+ :enable-word-wrap, :highlight, :show-fields
+Examples
+#1 To hide the log_procname fields in all formats:
+ :hide-fields log_procname
+
+
+#2 To hide only the log_procname field in the syslog format:
+ :hide-fields syslog_log.log_procname
+
+
+
+:hide-file path
+══════════════════════════════════════════════════════════════════════
+ Hide the given file(s) and skip indexing until it is shown again.
+ If no path is given, the current file in the view is hidden
+Parameter
+ path A path or glob pattern that specifies the files to hide
+
+
+:hide-lines-after date
+══════════════════════════════════════════════════════════════════════
+ Hide lines that come after the given date
+Parameter
+ date An absolute or relative date
+See Also
+ :filter-in, :filter-out, :hide-lines-before, :hide-unmarked-lines,
+ :show-lines-before-and-after, :toggle-filtering
+Examples
+#1 To hide the lines after the top line in the view:
+ :hide-lines-after here
+
+
+#2 To hide the lines after 6 AM today:
+ :hide-lines-after 6am
+
+
+
+:hide-lines-before date
+══════════════════════════════════════════════════════════════════════
+ Hide lines that come before the given date
+Parameter
+ date An absolute or relative date
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-unmarked-lines,
+ :show-lines-before-and-after, :toggle-filtering
+Examples
+#1 To hide the lines before the top line in the view:
+ :hide-lines-before here
+
+
+#2 To hide the log messages before 6 AM today:
+ :hide-lines-before 6am
+
+
+
+:hide-unmarked-lines
+══════════════════════════════════════════════════════════════════════
+ Hide lines that have not been bookmarked
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before, :mark,
+ :next-mark, :prev-mark, :toggle-filtering
+
+:highlight pattern
+══════════════════════════════════════════════════════════════════════
+ Add coloring to log messages fragments that match the given regular
+ expression
+Parameter
+ pattern The regular expression to match
+See Also
+ :clear-highlight, :enable-word-wrap, :hide-fields
+Example
+#1 To highlight numbers with three or more digits:
+ :highlight \d{3,}
+
+
+
+:load-session
+══════════════════════════════════════════════════════════════════════
+ Load the latest session state
+
+
+:mark
+══════════════════════════════════════════════════════════════════════
+ Toggle the bookmark state for the top line in the current view
+See Also
+ :hide-unmarked-lines, :next-mark, :prev-mark
+
+:mark-expr expr
+══════════════════════════════════════════════════════════════════════
+ Set the bookmark expression
+Parameter
+ expr The SQL expression to evaluate for each log message. The
+ message values can be accessed using column names prefixed
+ with a colon
+See Also
+ :clear-mark-expr, :hide-unmarked-lines, :mark, :next-mark, :prev-mark
+Example
+#1 To mark lines from 'dhclient' that mention 'eth0':
+ :mark-expr :log_procname = 'dhclient' AND :log_body LIKE '%eth0%'
+
+
+
+:next-location
+══════════════════════════════════════════════════════════════════════
+ Move to the next position in the location history
+See Also
+ :goto, :next-mark, :prev-location, :prev-mark, :relative-goto
+
+:next-mark type1 [... typeN]
+══════════════════════════════════════════════════════════════════════
+ Move to the next bookmark of the given type in the current view
+Parameter
+ type The type of bookmark -- error, warning, search, user, file,
+ meta
+See Also
+ :goto, :hide-unmarked-lines, :mark, :next-location, :prev-location,
+ :prev-mark, :prev-mark, :relative-goto
+Example
+#1 To go to the next error:
+ :next-mark error
+
+
+
+:open path1 [... pathN]
+══════════════════════════════════════════════════════════════════════
+ Open the given file(s) in lnav. Opening files on machines
+ accessible via SSH can be done using the syntax: [user@]host:/path/
+ to/logs
+Parameter
+ path The path to the file to open
+
+Examples
+#1 To open the file '/path/to/file':
+ :open /path/to/file
+
+
+#2 To open the remote file '/var/log/syslog.log':
+ :open dean@host1.example.com:/var/log/syslog.log
+
+
+
+:partition-name name
+══════════════════════════════════════════════════════════════════════
+ Mark the top line in the log view as the start of a new partition
+ with the given name
+Parameter
+ name The name for the new partition
+
+Example
+#1 To mark the top line as the start of the partition named 'boot #1':
+ :partition-name boot #1
+
+
+
+:pipe-line-to shell-cmd
+══════════════════════════════════════════════════════════════════════
+ Pipe the top line to the given shell command
+Parameter
+ shell-cmd The shell command-line to execute
+See Also
+ :append-to, :echo, :pipe-to, :redirect-to, :write-csv-to,
+ :write-json-to, :write-jsonlines-to, :write-raw-to, :write-screen-to,
+ :write-table-to, :write-to, :write-view-to
+Example
+#1 To write the top line to 'sed' for processing:
+ :pipe-line-to sed -e 's/foo/bar/g'
+
+
+
+:pipe-to shell-cmd
+══════════════════════════════════════════════════════════════════════
+ Pipe the marked lines to the given shell command
+Parameter
+ shell-cmd The shell command-line to execute
+See Also
+ :append-to, :echo, :pipe-line-to, :redirect-to, :write-csv-to,
+ :write-json-to, :write-jsonlines-to, :write-raw-to, :write-screen-to,
+ :write-table-to, :write-to, :write-view-to
+Example
+#1 To write marked lines to 'sed' for processing:
+ :pipe-to sed -e s/foo/bar/g
+
+
+
+:prev-location
+══════════════════════════════════════════════════════════════════════
+ Move to the previous position in the location history
+See Also
+ :goto, :next-location, :next-mark, :prev-mark, :relative-goto
+
+:prev-mark type1 [... typeN]
+══════════════════════════════════════════════════════════════════════
+ Move to the previous bookmark of the given type in the current view
+Parameter
+ type The type of bookmark -- error, warning, search, user, file,
+ meta
+See Also
+ :goto, :hide-unmarked-lines, :mark, :next-location, :next-mark,
+ :next-mark, :prev-location, :relative-goto
+Example
+#1 To go to the previous error:
+ :prev-mark error
+
+
+
+:prompt type [--alt] [prompt] [initial-value]
+══════════════════════════════════════════════════════════════════════
+ Open the given prompt
+Parameters
+ type The type of prompt -- command, script, search, sql,
+ user
+ --alt Perform the alternate action for this prompt by
+ default
+ prompt The prompt to display
+ initial-value The initial value to fill in for the prompt
+
+Examples
+#1 To open the command prompt with 'filter-in' already filled in:
+ :prompt command : 'filter-in '
+
+
+#2 To ask the user a question:
+ :prompt user 'Are you sure? '
+
+
+
+:quit
+══════════════════════════════════════════════════════════════════════
+ Quit lnav
+
+
+:quit
+══════════════════════════════════════════════════════════════════════
+ Quit lnav
+
+
+:quit
+══════════════════════════════════════════════════════════════════════
+ Quit lnav
+
+
+:redirect-to [path]
+══════════════════════════════════════════════════════════════════════
+ Redirect the output of commands that write to stdout to the given
+ file
+Parameter
+ path The path to the file to write. If not specified, the current
+ redirect will be cleared
+See Also
+ :alt-msg, :append-to, :echo, :echo, :eval, :pipe-line-to, :pipe-to,
+ :write-csv-to, :write-csv-to, :write-json-to, :write-json-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-screen-to, :write-screen-to, :write-table-to, :write-table-to,
+ :write-to, :write-to, :write-view-to, :write-view-to
+Example
+#1 To write the output of lnav commands to the file /tmp/script-output.txt:
+ :redirect-to /tmp/script-output.txt
+
+
+
+:redraw
+══════════════════════════════════════════════════════════════════════
+ Do a full redraw of the screen
+
+
+:relative-goto line-count|N%
+══════════════════════════════════════════════════════════════════════
+ Move the current view up or down by the given amount
+Parameter
+ line-count|N% The amount to move the view by.
+See Also
+ :goto, :next-location, :next-mark, :prev-location, :prev-mark
+Examples
+#1 To move 22 lines down in the view:
+ :relative-goto +22
+
+
+#2 To move 10 percent back in the view:
+ :relative-goto -10%
+
+
+
+:reset-config option
+══════════════════════════════════════════════════════════════════════
+ Reset the configuration option to its default value
+Parameter
+ option The path to the option to reset
+See Also
+ :config
+Example
+#1 To reset the '/ui/clock-format' option back to the builtin default:
+ :reset-config /ui/clock-format
+
+
+
+:reset-session
+══════════════════════════════════════════════════════════════════════
+ Reset the session state, clearing all filters, highlights, and
+ bookmarks
+
+
+:save-session
+══════════════════════════════════════════════════════════════════════
+ Save the current state as a session
+
+
+:session lnav-command
+══════════════════════════════════════════════════════════════════════
+ Add the given command to the session file (~/.lnav/session)
+Parameter
+ lnav-command The lnav command to save.
+
+Example
+#1 To add the command ':highlight foobar' to the session file:
+ :session :highlight foobar
+
+
+
+:set-min-log-level log-level
+══════════════════════════════════════════════════════════════════════
+ Set the minimum log level to display in the log view
+Parameter
+ log-level The new minimum log level
+
+Example
+#1 To set the minimum log level displayed to error:
+ :set-min-log-level error
+
+
+
+:show-fields field-name1 [... field-nameN]
+══════════════════════════════════════════════════════════════════════
+ Show log message fields that were previously hidden
+Parameter
+ field-name The name of the field to show
+See Also
+ :enable-word-wrap, :hide-fields, :highlight
+Example
+#1 To show all the log_procname fields in all formats:
+ :show-fields log_procname
+
+
+
+:show-file path
+══════════════════════════════════════════════════════════════════════
+ Show the given file(s) and resume indexing.
+Parameter
+ path The path or glob pattern that specifies the files to show
+
+
+:show-lines-before-and-after
+══════════════════════════════════════════════════════════════════════
+ Show lines that were hidden by the 'hide-lines' commands
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines, :toggle-filtering
+
+:show-only-this-file
+══════════════════════════════════════════════════════════════════════
+ Show only the file for the top line in the view
+
+
+:show-unmarked-lines
+══════════════════════════════════════════════════════════════════════
+ Show lines that have not been bookmarked
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines, :hide-unmarked-lines, :mark, :next-mark,
+ :prev-mark, :toggle-filtering
+
+:spectrogram field-name
+══════════════════════════════════════════════════════════════════════
+ Visualize the given message field using a spectrogram
+Parameter
+ field-name The name of the numeric field to visualize.
+
+Example
+#1 To visualize the sc_bytes field in the access_log format:
+ :spectrogram sc_bytes
+
+
+
+:summarize column-name
+══════════════════════════════════════════════════════════════════════
+ Execute a SQL query that computes the characteristics of the values
+ in the given column
+Parameter
+ column-name The name of the column to analyze.
+
+Example
+#1 To get a summary of the sc_bytes column in the access_log table:
+ :summarize sc_bytes
+
+
+
+:switch-to-view view-name
+══════════════════════════════════════════════════════════════════════
+ Switch to the given view
+Parameter
+ view-name The name of the view to switch to.
+
+Example
+#1 To switch to the 'schema' view:
+ :switch-to-view schema
+
+
+
+:tag tag1 [... tagN]
+══════════════════════════════════════════════════════════════════════
+ Attach tags to the top log line
+Parameter
+ tag The tags to attach
+See Also
+ :comment, :delete-tags, :untag
+Example
+#1 To add the tags '#BUG123' and '#needs-review' to the top line:
+ :tag #BUG123 #needs-review
+
+
+
+:toggle-filtering
+══════════════════════════════════════════════════════════════════════
+ Toggle the filtering flag for the current view
+See Also
+ :filter-in, :filter-out, :hide-lines-after, :hide-lines-before,
+ :hide-unmarked-lines
+
+:toggle-view view-name
+══════════════════════════════════════════════════════════════════════
+ Switch to the given view or, if it is already displayed, switch to
+ the previous view
+Parameter
+ view-name The name of the view to toggle the display of.
+
+Example
+#1 To switch to the 'schema' view if it is not displayed or switch back to the previous
+ view:
+ :toggle-view schema
+
+
+
+:unix-time seconds
+══════════════════════════════════════════════════════════════════════
+ Convert epoch time to a human-readable form
+Parameter
+ seconds The epoch timestamp to convert
+
+Example
+#1 To convert the epoch time 1490191111:
+ :unix-time 1490191111
+
+
+
+:untag tag1 [... tagN]
+══════════════════════════════════════════════════════════════════════
+ Detach tags from the top log line
+Parameter
+ tag The tags to detach
+See Also
+ :comment, :tag
+Example
+#1 To remove the tags '#BUG123' and '#needs-review' from the top line:
+ :untag #BUG123 #needs-review
+
+
+
+:write-table-to path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in a tabular format
+Parameter
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :pipe-line-to, :pipe-to, :redirect-to,
+ :redirect-to, :write-csv-to, :write-csv-to, :write-csv-to,
+ :write-json-to, :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-screen-to, :write-screen-to, :write-screen-to,
+ :write-to, :write-to, :write-view-to, :write-view-to, :write-view-to
+Example
+#1 To write SQL results as text to /tmp/table.txt:
+ :write-table-to /tmp/table.txt
+
+
+
+:write-csv-to path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in CSV format
+Parameter
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :pipe-line-to, :pipe-to, :redirect-to,
+ :redirect-to, :write-json-to, :write-json-to, :write-json-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-jsonlines-to,
+ :write-raw-to, :write-raw-to, :write-raw-to, :write-screen-to,
+ :write-screen-to, :write-screen-to, :write-table-to, :write-table-to,
+ :write-table-to, :write-to, :write-to, :write-view-to, :write-view-to,
+ :write-view-to
+Example
+#1 To write SQL results as CSV to /tmp/table.csv:
+ :write-csv-to /tmp/table.csv
+
+
+
+:write-json-to path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in JSON format
+Parameter
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :pipe-line-to, :pipe-to, :redirect-to,
+ :redirect-to, :write-csv-to, :write-csv-to, :write-csv-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-jsonlines-to,
+ :write-raw-to, :write-raw-to, :write-raw-to, :write-screen-to,
+ :write-screen-to, :write-screen-to, :write-table-to, :write-table-to,
+ :write-table-to, :write-to, :write-to, :write-view-to, :write-view-to,
+ :write-view-to
+Example
+#1 To write SQL results as JSON to /tmp/table.json:
+ :write-json-to /tmp/table.json
+
+
+
+:write-jsonlines-to path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in JSON Lines format
+Parameter
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :pipe-line-to, :pipe-to, :redirect-to,
+ :redirect-to, :write-csv-to, :write-csv-to, :write-csv-to,
+ :write-json-to, :write-json-to, :write-json-to, :write-raw-to,
+ :write-raw-to, :write-raw-to, :write-screen-to, :write-screen-to,
+ :write-screen-to, :write-table-to, :write-table-to, :write-table-to,
+ :write-to, :write-to, :write-view-to, :write-view-to, :write-view-to
+Example
+#1 To write SQL results as JSON Lines to /tmp/table.json:
+ :write-jsonlines-to /tmp/table.json
+
+
+
+:write-raw-to [--view={log,db}] path
+══════════════════════════════════════════════════════════════════════
+ In the log view, write the original log file content of the marked
+ messages to the file. In the DB view, the contents of the cells are
+ written to the output file.
+Parameters
+ --view={log,db} The view to use as the source of data
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :pipe-line-to, :pipe-to, :redirect-to,
+ :redirect-to, :write-csv-to, :write-csv-to, :write-csv-to,
+ :write-json-to, :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-screen-to,
+ :write-screen-to, :write-screen-to, :write-table-to, :write-table-to,
+ :write-table-to, :write-to, :write-to, :write-view-to, :write-view-to,
+ :write-view-to
+Example
+#1 To write the marked lines in the log view to /tmp/table.txt:
+ :write-raw-to /tmp/table.txt
+
+
+
+:write-screen-to path
+══════════════════════════════════════════════════════════════════════
+ Write the displayed text or SQL results to the given file without
+ any formatting
+Parameter
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :pipe-line-to, :pipe-to, :redirect-to,
+ :redirect-to, :write-csv-to, :write-csv-to, :write-csv-to,
+ :write-json-to, :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-table-to, :write-table-to, :write-table-to,
+ :write-to, :write-to, :write-view-to, :write-view-to, :write-view-to
+Example
+#1 To write only the displayed text to /tmp/table.txt:
+ :write-screen-to /tmp/table.txt
+
+
+
+:write-table-to path
+══════════════════════════════════════════════════════════════════════
+ Write SQL results to the given file in a tabular format
+Parameter
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :pipe-line-to, :pipe-to, :redirect-to,
+ :redirect-to, :write-csv-to, :write-csv-to, :write-csv-to,
+ :write-json-to, :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-screen-to, :write-screen-to, :write-screen-to,
+ :write-to, :write-to, :write-view-to, :write-view-to, :write-view-to
+Example
+#1 To write SQL results as text to /tmp/table.txt:
+ :write-table-to /tmp/table.txt
+
+
+
+:write-to path
+══════════════════════════════════════════════════════════════════════
+ Overwrite the given file with any marked lines in the current view
+Parameter
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :echo, :echo, :eval, :pipe-line-to, :pipe-to,
+ :redirect-to, :redirect-to, :write-csv-to, :write-csv-to,
+ :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-raw-to, :write-raw-to, :write-screen-to,
+ :write-screen-to, :write-table-to, :write-table-to, :write-view-to,
+ :write-view-to
+Example
+#1 To write marked lines to the file /tmp/interesting-lines.txt:
+ :write-to /tmp/interesting-lines.txt
+
+
+
+:write-view-to path
+══════════════════════════════════════════════════════════════════════
+ Write the text in the top view to the given file without any
+ formatting
+Parameter
+ path The path to the file to write
+See Also
+ :alt-msg, :append-to, :create-logline-table, :create-search-table,
+ :echo, :echo, :eval, :pipe-line-to, :pipe-to, :redirect-to,
+ :redirect-to, :write-csv-to, :write-csv-to, :write-csv-to,
+ :write-json-to, :write-json-to, :write-json-to, :write-jsonlines-to,
+ :write-jsonlines-to, :write-jsonlines-to, :write-raw-to, :write-raw-to,
+ :write-raw-to, :write-screen-to, :write-screen-to, :write-screen-to,
+ :write-table-to, :write-table-to, :write-table-to, :write-to,
+ :write-to
+Example
+#1 To write the top view to /tmp/table.txt:
+ :write-view-to /tmp/table.txt
+
+
+
+:zoom-to zoom-level
+══════════════════════════════════════════════════════════════════════
+ Zoom the histogram view to the given level
+Parameter
+ zoom-level The zoom level
+
+Example
+#1 To set the zoom level to '1-week':
+ :zoom-to 1-week
+
+
+SQL Reference
+
+CAST(expr AS type-name)
+══════════════════════════════════════════════════════════════════════
+ Convert the value of the given expression to a different storage
+ class specified by type-name.
+Parameters
+ expr The value to convert.
+ type-name The name of the type to convert to.
+
+Example
+#1 To cast the value 1.23 as an integer:
+ ;SELECT CAST(1.23 AS INTEGER)
+
+
+
+OVER([base-window-name] PARTITION BY expr, ... ORDER BY expr, ...,
+ [frame-spec])
+══════════════════════════════════════════════════════════════════════
+ Executes the preceding function over a window
+Parameters
+ base-window-name The name of the window definition
+ expr The values to use for partitioning
+ expr The values used to order the rows in the window
+ frame-spec Determines which output rows are read by an
+ aggregate window function
+
+
+abs(x)
+══════════════════════════════════════════════════════════════════════
+ Return the absolute value of the argument
+Parameter
+ x The number to convert
+See Also
+ acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the absolute value of -1:
+ ;SELECT abs(-1)
+
+
+
+acos(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the arccosine of a number, in radians
+Parameter
+ num A cosine value that is between -1 and 1
+See Also
+ abs(), acosh(), asin(), asinh(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the arccosine of 0.2:
+ ;SELECT acos(0.2)
+
+
+
+acosh(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the hyperbolic arccosine of a number
+Parameter
+ num A number that is one or more
+See Also
+ abs(), acos(), asin(), asinh(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the hyperbolic arccosine of 1.2:
+ ;SELECT acosh(1.2)
+
+
+
+asin(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the arcsine of a number, in radians
+Parameter
+ num A sine value that is between -1 and 1
+See Also
+ abs(), acos(), acosh(), asinh(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the arcsine of 0.2:
+ ;SELECT asin(0.2)
+
+
+
+asinh(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the hyperbolic arcsine of a number
+Parameter
+ num The number
+See Also
+ abs(), acos(), acosh(), asin(), atan(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the hyperbolic arcsine of 0.2:
+ ;SELECT asinh(0.2)
+
+
+
+atan(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the arctangent of a number, in radians
+Parameter
+ num The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan2(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the arctangent of 0.2:
+ ;SELECT atan(0.2)
+
+
+
+atan2(y, x)
+══════════════════════════════════════════════════════════════════════
+ Returns the angle in the plane between the positive X axis and the
+ ray from (0, 0) to the point (x, y)
+Parameters
+ y The y coordinate of the point
+ x The x coordinate of the point
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atanh(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the angle, in degrees, for the point at (5, 5):
+ ;SELECT degrees(atan2(5, 5))
+
+
+
+atanh(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the hyperbolic arctangent of a number
+Parameter
+ num The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atn2(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the hyperbolic arctangent of 0.2:
+ ;SELECT atanh(0.2)
+
+
+
+atn2(y, x)
+══════════════════════════════════════════════════════════════════════
+ Returns the angle in the plane between the positive X axis and the
+ ray from (0, 0) to the point (x, y)
+Parameters
+ y The y coordinate of the point
+ x The x coordinate of the point
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ avg(), ceil(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the angle, in degrees, for the point at (5, 5):
+ ;SELECT degrees(atn2(5, 5))
+
+
+
+avg(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the average value of all non-NULL numbers within a group.
+Parameter
+ X The value to compute the average of.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), ceil(), degrees(), exp(), floor(), log(), log10(), max(),
+ min(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Examples
+#1 To get the average of the column 'ex_duration' from the table 'lnav_example_log':
+ ;SELECT avg(ex_duration) FROM lnav_example_log
+
+
+#2 To get the average of the column 'ex_duration' from the table 'lnav_example_log' when
+ grouped by 'ex_procname':
+ ;SELECT ex_procname, avg(ex_duration) FROM lnav_example_log GROUP BY ex_procname
+
+
+
+basename(path)
+══════════════════════════════════════════════════════════════════════
+ Extract the base portion of a pathname.
+Parameter
+ path The path
+See Also
+ dirname(), joinpath(), readlink(), realpath()
+Examples
+#1 To get the base of a plain file name:
+ ;SELECT basename('foobar')
+
+
+#2 To get the base of a path:
+ ;SELECT basename('foo/bar')
+
+
+#3 To get the base of a directory:
+ ;SELECT basename('foo/bar/')
+
+
+#4 To get the base of an empty string:
+ ;SELECT basename('')
+
+
+#5 To get the base of a Windows path:
+ ;SELECT basename('foo\bar')
+
+
+#6 To get the base of the root directory:
+ ;SELECT basename('/')
+
+
+
+ceil(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the smallest integer that is not less than the argument
+Parameter
+ num The number to raise to the ceiling
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), degrees(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the ceiling of 1.23:
+ ;SELECT ceil(1.23)
+
+
+
+changes()
+══════════════════════════════════════════════════════════════════════
+ The number of database rows that were changed, inserted, or deleted
+ by the most recent statement.
+
+
+char(X, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns a string composed of characters having the given unicode
+ code point values
+Parameter
+ X The unicode code point values
+See Also
+ charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), unicode(), upper(), xpath()
+Example
+#1 To get a string with the code points 0x48 and 0x49:
+ ;SELECT char(0x48, 0x49)
+
+
+
+charindex(needle, haystack, [start])
+══════════════════════════════════════════════════════════════════════
+ Finds the first occurrence of the needle within the haystack and
+ returns the number of prior characters plus 1, or 0 if Y is nowhere
+ found within X
+Parameters
+ needle The string to look for in the haystack
+ haystack The string to search within
+ start The one-based index within the haystack to start the
+ search
+See Also
+ char(), endswith(), extract(), group_concat(), group_spooky_hash(),
+ gunzip(), gzip(), humanize_file_size(), instr(), leftstr(), length(),
+ logfmt2json(), lower(), ltrim(), padc(), padl(), padr(), printf(),
+ proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To search for the string 'abc' within 'abcabc' and starting at position 2:
+ ;SELECT charindex('abc', 'abcabc', 2)
+
+
+#2 To search for the string 'abc' within 'abcdef' and starting at position 2:
+ ;SELECT charindex('abc', 'abcdef', 2)
+
+
+
+coalesce(X, Y, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns a copy of its first non-NULL argument, or NULL if all
+ arguments are NULL
+Parameters
+ X A value to check for NULL-ness
+ Y A value to check for NULL-ness
+
+Example
+#1 To get the first non-null value from three parameters:
+ ;SELECT coalesce(null, 0, null)
+
+
+
+count(X)
+══════════════════════════════════════════════════════════════════════
+ If the argument is '*', the total number of rows in the group is
+ returned. Otherwise, the number of times the argument is non-NULL.
+Parameter
+ X The value to count.
+
+Examples
+#1 To get the count of the non-NULL rows of 'lnav_example_log':
+ ;SELECT count(*) FROM lnav_example_log
+
+
+#2 To get the count of the non-NULL values of 'log_part' from 'lnav_example_log':
+ ;SELECT count(log_part) FROM lnav_example_log
+
+
+
+cume_dist()
+══════════════════════════════════════════════════════════════════════
+ Returns the cumulative distribution
+See Also
+ dense_rank(), first_value(), lag(), last_value(), lead(), nth_value(),
+ ntile(), percent_rank(), rank(), row_number()
+
+date(timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the date in this format: YYYY-MM-DD.
+Parameters
+ timestring The string to convert to a date.
+ modifier A transformation that is applied to the value to the
+ left.
+See Also
+ datetime(), julianday(), strftime(), time(), timediff(), timeslice()
+Examples
+#1 To get the date portion of the timestamp '2017-01-02T03:04:05':
+ ;SELECT date('2017-01-02T03:04:05')
+
+
+#2 To get the date portion of the timestamp '2017-01-02T03:04:05' plus one day:
+ ;SELECT date('2017-01-02T03:04:05', '+1 day')
+
+
+#3 To get the date portion of the epoch timestamp 1491341842:
+ ;SELECT date(1491341842, 'unixepoch')
+
+
+
+datetime(timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the date and time in this format: YYYY-MM-DD HH:MM:SS.
+Parameters
+ timestring The string to convert to a date with time.
+ modifier A transformation that is applied to the value to the
+ left.
+See Also
+ date(), julianday(), strftime(), time(), timediff(), timeslice()
+Examples
+#1 To get the date and time portion of the timestamp '2017-01-02T03:04:05':
+ ;SELECT datetime('2017-01-02T03:04:05')
+
+
+#2 To get the date and time portion of the timestamp '2017-01-02T03:04:05' plus one minute
+ :
+ ;SELECT datetime('2017-01-02T03:04:05', '+1 minute')
+
+
+#3 To get the date and time portion of the epoch timestamp 1491341842:
+ ;SELECT datetime(1491341842, 'unixepoch')
+
+
+
+degrees(radians)
+══════════════════════════════════════════════════════════════════════
+ Converts radians to degrees
+Parameter
+ radians The radians value to convert to degrees
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), exp(), floor(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To convert PI to degrees:
+ ;SELECT degrees(pi())
+
+
+
+dense_rank()
+══════════════════════════════════════════════════════════════════════
+ Returns the row_number() of the first peer in each group without
+ gaps
+See Also
+ cume_dist(), first_value(), lag(), last_value(), lead(), nth_value(),
+ ntile(), percent_rank(), rank(), row_number()
+
+dirname(path)
+══════════════════════════════════════════════════════════════════════
+ Extract the directory portion of a pathname.
+Parameter
+ path The path
+See Also
+ basename(), joinpath(), readlink(), realpath()
+Examples
+#1 To get the directory of a relative file path:
+ ;SELECT dirname('foo/bar')
+
+
+#2 To get the directory of an absolute file path:
+ ;SELECT dirname('/foo/bar')
+
+
+#3 To get the directory of a file in the root directory:
+ ;SELECT dirname('/bar')
+
+
+#4 To get the directory of a Windows path:
+ ;SELECT dirname('foo\bar')
+
+
+#5 To get the directory of an empty path:
+ ;SELECT dirname('')
+
+
+
+endswith(str, suffix)
+══════════════════════════════════════════════════════════════════════
+ Test if a string ends with the given suffix
+Parameters
+ str The string to test
+ suffix The suffix to check in the string
+See Also
+ char(), charindex(), extract(), group_concat(), group_spooky_hash(),
+ gunzip(), gzip(), humanize_file_size(), instr(), leftstr(), length(),
+ logfmt2json(), lower(), ltrim(), padc(), padl(), padr(), printf(),
+ proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To test if the string 'notbad.jpg' ends with '.jpg':
+ ;SELECT endswith('notbad.jpg', '.jpg')
+
+
+#2 To test if the string 'notbad.png' starts with '.jpg':
+ ;SELECT endswith('notbad.png', '.jpg')
+
+
+
+exp(x)
+══════════════════════════════════════════════════════════════════════
+ Returns the value of e raised to the power of x
+Parameter
+ x The exponent
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), floor(), log(), log10(), max(),
+ min(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Example
+#1 To raise e to 2:
+ ;SELECT exp(2)
+
+
+
+extract(str)
+══════════════════════════════════════════════════════════════════════
+ Automatically Parse and extract data from a string
+Parameter
+ str The string to parse
+See Also
+ char(), charindex(), endswith(), group_concat(), group_spooky_hash(),
+ gunzip(), gzip(), humanize_file_size(), instr(), leftstr(), length(),
+ logfmt2json(), lower(), ltrim(), padc(), padl(), padr(), printf(),
+ proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To extract key/value pairs from a string:
+ ;SELECT extract('foo=1 bar=2 name="Rolo Tomassi"')
+
+
+#2 To extract columnar data from a string:
+ ;SELECT extract('1.0 abc 2.0')
+
+
+
+first_value(expr)
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the first
+ row in the window frame.
+Parameter
+ expr The expression to execute over the first row
+See Also
+ cume_dist(), dense_rank(), lag(), last_value(), lead(), nth_value(),
+ ntile(), percent_rank(), rank(), row_number()
+
+floor(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the largest integer that is not greater than the argument
+Parameter
+ num The number to lower to the floor
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), log(), log10(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the floor of 1.23:
+ ;SELECT floor(1.23)
+
+
+
+generate_series(start, stop, [step])
+══════════════════════════════════════════════════════════════════════
+ A table-valued-function that returns the whole numbers between a
+ lower and upper bound, inclusive
+Parameters
+ start The starting point of the series
+ stop The stopping point of the series
+ step The increment between each value
+Result
+ value The number in the series
+
+Examples
+#1 To generate the numbers in the range [10, 14]:
+ ;SELECT value FROM generate_series(10, 14)
+
+
+#2 To generate every other number in the range [10, 14]:
+ ;SELECT value FROM generate_series(10, 14, 2)
+
+
+#3 To count down from five to 1:
+ ;SELECT value FROM generate_series(1, 5, -1)
+
+
+
+gethostbyaddr(hostname)
+══════════════════════════════════════════════════════════════════════
+ Get the hostname for the given IP address
+Parameter
+ hostname The IP address to lookup.
+See Also
+ gethostbyname()
+Example
+#1 To get the hostname for the IP '127.0.0.1':
+ ;SELECT gethostbyaddr('127.0.0.1')
+
+
+
+gethostbyname(hostname)
+══════════════════════════════════════════════════════════════════════
+ Get the IP address for the given hostname
+Parameter
+ hostname The DNS hostname to lookup.
+See Also
+ gethostbyaddr()
+Example
+#1 To get the IP address for 'localhost':
+ ;SELECT gethostbyname('localhost')
+
+
+
+glob(pattern, str)
+══════════════════════════════════════════════════════════════════════
+ Match a string against Unix glob pattern
+Parameters
+ pattern The glob pattern
+ str The string to match
+
+Example
+#1 To test if the string 'abc' matches the glob 'a*':
+ ;SELECT glob('a*', 'abc')
+
+
+
+group_concat(X, [sep])
+══════════════════════════════════════════════════════════════════════
+ Returns a string which is the concatenation of all non-NULL values
+ of X separated by a comma or the given separator.
+Parameters
+ X The value to concatenate.
+ sep The separator to place between the values.
+See Also
+ char(), charindex(), endswith(), extract(), group_spooky_hash(),
+ gunzip(), gzip(), humanize_file_size(), instr(), leftstr(), length(),
+ logfmt2json(), lower(), ltrim(), padc(), padl(), padr(), printf(),
+ proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To concatenate the values of the column 'ex_procname' from the table 'lnav_example_log'
+ :
+ ;SELECT group_concat(ex_procname) FROM lnav_example_log
+
+
+#2 To join the values of the column 'ex_procname' using the string ', ':
+ ;SELECT group_concat(ex_procname, ', ') FROM lnav_example_log
+
+
+#3 To concatenate the distinct values of the column 'ex_procname' from the table '
+ lnav_example_log':
+ ;SELECT group_concat(DISTINCT ex_procname) FROM lnav_example_log
+
+
+
+group_spooky_hash(str, ...)
+══════════════════════════════════════════════════════════════════════
+ Compute the hash value for the given arguments
+Parameter
+ str The string to hash
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(), gunzip(),
+ gzip(), humanize_file_size(), instr(), leftstr(), length(),
+ logfmt2json(), lower(), ltrim(), padc(), padl(), padr(), printf(),
+ proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Example
+#1 To produce a hash of all of the values of 'column1':
+ ;SELECT group_spooky_hash(column1) FROM (VALUES ('abc'), ('123'))
+
+
+
+gunzip(b, ...)
+══════════════════════════════════════════════════════════════════════
+ Decompress a gzip file
+Parameter
+ b The blob to decompress
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gzip(), humanize_file_size(), instr(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+
+gzip(value, ...)
+══════════════════════════════════════════════════════════════════════
+ Compress a string into a gzip file
+Parameter
+ value The value to compress
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), unicode(), upper(), xpath()
+
+hex(X)
+══════════════════════════════════════════════════════════════════════
+ Returns a string which is the upper-case hexadecimal rendering of
+ the content of its argument.
+Parameter
+ X The blob to convert to hexadecimal
+
+Example
+#1 To get the hexadecimal rendering of the string 'abc':
+ ;SELECT hex('abc')
+
+
+
+humanize_file_size(value)
+══════════════════════════════════════════════════════════════════════
+ Format the given file size as a human-friendly string
+Parameter
+ value The file size to format
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), instr(), leftstr(), length(),
+ logfmt2json(), lower(), ltrim(), padc(), padl(), padr(), printf(),
+ proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Example
+#1 To format an amount:
+ ;SELECT humanize_file_size(10 * 1024 * 1024)
+
+
+
+ifnull(X, Y)
+══════════════════════════════════════════════════════════════════════
+ Returns a copy of its first non-NULL argument, or NULL if both
+ arguments are NULL
+Parameters
+ X A value to check for NULL-ness
+ Y A value to check for NULL-ness
+
+Example
+#1 To get the first non-null value between null and zero:
+ ;SELECT ifnull(null, 0)
+
+
+
+instr(haystack, needle)
+══════════════════════════════════════════════════════════════════════
+ Finds the first occurrence of the needle within the haystack and
+ returns the number of prior characters plus 1, or 0 if the needle
+ was not found
+Parameters
+ haystack The string to search within
+ needle The string to look for in the haystack
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), leftstr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Example
+#1 To test get the position of 'b' in the string 'abc':
+ ;SELECT instr('abc', 'b')
+
+
+
+jget(json, ptr, [default])
+══════════════════════════════════════════════════════════════════════
+ Get the value from a JSON object using a JSON-Pointer.
+Parameters
+ json The JSON object to query.
+ ptr The JSON-Pointer to lookup in the object.
+ default The default value if the value was not found
+See Also
+ json_concat(), json_contains(), json_group_array(),
+ json_group_object()
+Examples
+#1 To get the root of a JSON value:
+ ;SELECT jget('1', '')
+
+
+#2 To get the property named 'b' in a JSON object:
+ ;SELECT jget('{ "a": 1, "b": 2 }', '/b')
+
+
+#3 To get the 'msg' property and return a default if it does not exist:
+ ;SELECT jget(null, '/msg', 'Hello')
+
+
+
+joinpath(path, ...)
+══════════════════════════════════════════════════════════════════════
+ Join components of a path together.
+Parameter
+ path One or more path components to join together. If an argument
+ starts with a forward or backward slash, it will be
+ considered an absolute path and any preceding elements will
+ be ignored.
+See Also
+ basename(), dirname(), readlink(), realpath()
+Examples
+#1 To join a directory and file name into a relative path:
+ ;SELECT joinpath('foo', 'bar')
+
+
+#2 To join an empty component with other names into a relative path:
+ ;SELECT joinpath('', 'foo', 'bar')
+
+
+#3 To create an absolute path with two path components:
+ ;SELECT joinpath('/', 'foo', 'bar')
+
+
+#4 To create an absolute path from a path component that starts with a forward slash:
+ ;SELECT joinpath('/', 'foo', '/bar')
+
+
+
+json_concat(json, value, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns an array with the given values concatenated onto the end.
+ If the initial value is null, the result will be an array with the
+ given elements. If the initial value is an array, the result will
+ be an array with the given values at the end. If the initial value
+ is not null or an array, the result will be an array with two
+ elements: the initial value and the given value.
+Parameters
+ json The initial JSON value.
+ value The value(s) to add to the end of the array.
+See Also
+ jget(), json_contains(), json_group_array(), json_group_object()
+Examples
+#1 To append the number 4 to null:
+ ;SELECT json_concat(NULL, 4)
+
+
+#2 To append 4 and 5 to the array [1, 2, 3]:
+ ;SELECT json_concat('[1, 2, 3]', 4, 5)
+
+
+#3 To concatenate two arrays together:
+ ;SELECT json_concat('[1, 2, 3]', json('[4, 5]'))
+
+
+
+json_contains(json, value)
+══════════════════════════════════════════════════════════════════════
+ Check if a JSON value contains the given element.
+Parameters
+ json The JSON value to query.
+ value The value to look for in the first argument
+See Also
+ jget(), json_concat(), json_group_array(), json_group_object()
+Examples
+#1 To test if a JSON array contains the number 4:
+ ;SELECT json_contains('[1, 2, 3]', 4)
+
+
+#2 To test if a JSON array contains the string 'def':
+ ;SELECT json_contains('["abc", "def"]', 'def')
+
+
+
+json_group_array(value, ...)
+══════════════════════════════════════════════════════════════════════
+ Collect the given values from a query into a JSON array
+Parameter
+ value The values to append to the array
+See Also
+ jget(), json_concat(), json_contains(), json_group_object()
+Examples
+#1 To create an array from arguments:
+ ;SELECT json_group_array('one', 2, 3.4)
+
+
+#2 To create an array from a column of values:
+ ;SELECT json_group_array(column1) FROM (VALUES (1), (2), (3))
+
+
+
+json_group_object(name, value, ...)
+══════════════════════════════════════════════════════════════════════
+ Collect the given values from a query into a JSON object
+Parameters
+ name The property name for the value
+ value The value to add to the object
+See Also
+ jget(), json_concat(), json_contains(), json_group_array()
+Examples
+#1 To create an object from arguments:
+ ;SELECT json_group_object('a', 1, 'b', 2)
+
+
+#2 To create an object from a pair of columns:
+ ;SELECT json_group_object(column1, column2) FROM (VALUES ('a', 1), ('b', 2))
+
+
+
+julianday(timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the number of days since noon in Greenwich on November 24,
+ 4714 B.C.
+Parameters
+ timestring The string to convert to a date with time.
+ modifier A transformation that is applied to the value to the
+ left.
+See Also
+ date(), datetime(), strftime(), time(), timediff(), timeslice()
+Examples
+#1 To get the julian day from the timestamp '2017-01-02T03:04:05':
+ ;SELECT julianday('2017-01-02T03:04:05')
+
+
+#2 To get the julian day from the timestamp '2017-01-02T03:04:05' plus one minute:
+ ;SELECT julianday('2017-01-02T03:04:05', '+1 minute')
+
+
+#3 To get the julian day from the timestamp 1491341842:
+ ;SELECT julianday(1491341842, 'unixepoch')
+
+
+
+lag(expr, [offset], [default])
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the previous
+ row in the partition.
+Parameters
+ expr The expression to execute over the previous row
+ offset The offset from the current row in the partition
+ default The default value if the previous row does not exist
+ instead of NULL
+See Also
+ cume_dist(), dense_rank(), first_value(), last_value(), lead(),
+ nth_value(), ntile(), percent_rank(), rank(), row_number()
+
+last_insert_rowid()
+══════════════════════════════════════════════════════════════════════
+ Returns the ROWID of the last row insert from the database
+ connection which invoked the function
+
+
+last_value(expr)
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the last row
+ in the window frame.
+Parameter
+ expr The expression to execute over the last row
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), lead(), nth_value(),
+ ntile(), percent_rank(), rank(), row_number()
+
+lead(expr, [offset], [default])
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the next row
+ in the partition.
+Parameters
+ expr The expression to execute over the next row
+ offset The offset from the current row in the partition
+ default The default value if the next row does not exist instead
+ of NULL
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(),
+ nth_value(), ntile(), percent_rank(), rank(), row_number()
+
+leftstr(str, N)
+══════════════════════════════════════════════════════════════════════
+ Returns the N leftmost (UTF-8) characters in the given string.
+Parameters
+ str The string to return subset.
+ N The number of characters from the left side of the string to
+ return.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ length(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To get the first character of the string 'abc':
+ ;SELECT leftstr('abc', 1)
+
+
+#2 To get the first ten characters of a string, regardless of size:
+ ;SELECT leftstr('abc', 10)
+
+
+
+length(str)
+══════════════════════════════════════════════════════════════════════
+ Returns the number of characters (not bytes) in the given string
+ prior to the first NUL character
+Parameter
+ str The string to determine the length of
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), logfmt2json(), lower(), ltrim(), padc(), padl(), padr(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Example
+#1 To get the length of the string 'abc':
+ ;SELECT length('abc')
+
+
+
+like(pattern, str, [escape])
+══════════════════════════════════════════════════════════════════════
+ Match a string against a pattern
+Parameters
+ pattern The pattern to match. A percent symbol (%) will match
+ zero or more characters and an underscore (_) will match a
+ single character.
+ str The string to match
+ escape The escape character that can be used to prefix a literal
+ percent or underscore in the pattern.
+
+Examples
+#1 To test if the string 'aabcc' contains the letter 'b':
+ ;SELECT like('%b%', 'aabcc')
+
+
+#2 To test if the string 'aab%' ends with 'b%':
+ ;SELECT like('%b:%', 'aab%', ':')
+
+
+
+likelihood(value, probability)
+══════════════════════════════════════════════════════════════════════
+ Provides a hint to the query planner that the first argument is a
+ boolean that is true with the given probability
+Parameters
+ value The boolean value to return
+ probability A floating point constant between 0.0 and 1.0
+
+
+likely(value)
+══════════════════════════════════════════════════════════════════════
+ Short-hand for likelihood(X,0.9375)
+Parameter
+ value The boolean value to return
+
+
+lnav_top_file()
+══════════════════════════════════════════════════════════════════════
+ Return the name of the file that the top line in the current view
+ came from.
+
+
+lnav_version()
+══════════════════════════════════════════════════════════════════════
+ Return the current version of lnav
+
+
+load_extension(path, [entry-point])
+══════════════════════════════════════════════════════════════════════
+ Loads SQLite extensions out of the given shared library file using
+ the given entry point.
+Parameters
+ path The path to the shared library containing the
+ extension.
+ entry-point
+
+
+log(x)
+══════════════════════════════════════════════════════════════════════
+ Returns the natural logarithm of x
+Parameter
+ x The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log10(), max(),
+ min(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Example
+#1 To get the natual logarithm of 8:
+ ;SELECT log(8)
+
+
+
+log10(x)
+══════════════════════════════════════════════════════════════════════
+ Returns the base-10 logarithm of X
+Parameter
+ x The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), max(), min(),
+ pi(), power(), radians(), round(), sign(), square(), sum(), total()
+Example
+#1 To get the logarithm of 100:
+ ;SELECT log10(100)
+
+
+
+log_top_datetime()
+══════════════════════════════════════════════════════════════════════
+ Return the timestamp of the line at the top of the log view.
+
+
+log_top_line()
+══════════════════════════════════════════════════════════════════════
+ Return the line number at the top of the log view.
+
+
+logfmt2json(str)
+══════════════════════════════════════════════════════════════════════
+ Convert a logfmt-encoded string into JSON
+Parameter
+ str The logfmt message to parse
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), lower(), ltrim(), padc(), padl(), padr(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Example
+#1 To extract key/value pairs from a log message:
+ ;SELECT logfmt2json('foo=1 bar=2 name="Rolo Tomassi"')
+
+
+
+lower(str)
+══════════════════════════════════════════════════════════════════════
+ Returns a copy of the given string with all ASCII characters
+ converted to lower case.
+Parameter
+ str The string to convert.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), ltrim(), padc(), padl(), padr(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Example
+#1 To lowercase the string 'AbC':
+ ;SELECT lower('AbC')
+
+
+
+ltrim(str, [chars])
+══════════════════════════════════════════════════════════════════════
+ Returns a string formed by removing any and all characters that
+ appear in the second argument from the left side of the first.
+Parameters
+ str The string to trim characters from the left side
+ chars The characters to trim. Defaults to spaces.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), padc(), padl(), padr(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To trim the leading whitespace from the string ' abc':
+ ;SELECT ltrim(' abc')
+
+
+#2 To trim the characters 'a' or 'b' from the left side of the string 'aaaabbbc':
+ ;SELECT ltrim('aaaabbbc', 'ab')
+
+
+
+max(X, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the argument with the maximum value, or return NULL if any
+ argument is NULL.
+Parameter
+ X The numbers to find the maximum of. If only one argument is
+ given, this function operates as an aggregate.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ min(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Examples
+#1 To get the largest value from the parameters:
+ ;SELECT max(2, 1, 3)
+
+
+#2 To get the largest value from an aggregate:
+ ;SELECT max(status) FROM http_status_codes
+
+
+
+min(X, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the argument with the minimum value, or return NULL if any
+ argument is NULL.
+Parameter
+ X The numbers to find the minimum of. If only one argument is
+ given, this function operates as an aggregate.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), pi(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Examples
+#1 To get the smallest value from the parameters:
+ ;SELECT min(2, 1, 3)
+
+
+#2 To get the smallest value from an aggregate:
+ ;SELECT min(status) FROM http_status_codes
+
+
+
+nth_value(expr, N)
+══════════════════════════════════════════════════════════════════════
+ Returns the result of evaluating the expression against the nth row
+ in the window frame.
+Parameters
+ expr The expression to execute over the nth row
+ N The row number
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ ntile(), percent_rank(), rank(), row_number()
+
+ntile(groups)
+══════════════════════════════════════════════════════════════════════
+ Returns the number of the group that the current row is a part of
+Parameter
+ groups The number of groups
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ nth_value(), percent_rank(), rank(), row_number()
+
+nullif(X, Y)
+══════════════════════════════════════════════════════════════════════
+ Returns its first argument if the arguments are different and NULL
+ if the arguments are the same.
+Parameters
+ X The first argument to compare.
+ Y The argument to compare against the first.
+
+Examples
+#1 To test if 1 is different from 1:
+ ;SELECT nullif(1, 1)
+
+
+#2 To test if 1 is different from 2:
+ ;SELECT nullif(1, 2)
+
+
+
+padc(str, len)
+══════════════════════════════════════════════════════════════════════
+ Pad the given string with enough spaces to make it centered within
+ the given length
+Parameters
+ str The string to pad
+ len The minimum desired length of the output string
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padl(), padr(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To pad the string 'abc' to a length of six characters:
+ ;SELECT padc('abc', 6) || 'def'
+
+
+#2 To pad the string 'abcdef' to a length of eight characters:
+ ;SELECT padc('abcdef', 8) || 'ghi'
+
+
+
+padl(str, len)
+══════════════════════════════════════════════════════════════════════
+ Pad the given string with leading spaces until it reaches the
+ desired length
+Parameters
+ str The string to pad
+ len The minimum desired length of the output string
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padr(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To pad the string 'abc' to a length of six characters:
+ ;SELECT padl('abc', 6)
+
+
+#2 To pad the string 'abcdef' to a length of four characters:
+ ;SELECT padl('abcdef', 4)
+
+
+
+padr(str, len)
+══════════════════════════════════════════════════════════════════════
+ Pad the given string with trailing spaces until it reaches the
+ desired length
+Parameters
+ str The string to pad
+ len The minimum desired length of the output string
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ printf(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To pad the string 'abc' to a length of six characters:
+ ;SELECT padr('abc', 6) || 'def'
+
+
+#2 To pad the string 'abcdef' to a length of four characters:
+ ;SELECT padr('abcdef', 4) || 'ghi'
+
+
+
+percent_rank()
+══════════════════════════════════════════════════════════════════════
+ Returns (rank - 1) / (partition-rows - 1)
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ nth_value(), ntile(), rank(), row_number()
+
+pi()
+══════════════════════════════════════════════════════════════════════
+ Returns the value of PI
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), power(), radians(), round(), sign(), square(), sum(),
+ total()
+Example
+#1 To get the value of PI:
+ ;SELECT pi()
+
+
+
+power(base, exp)
+══════════════════════════════════════════════════════════════════════
+ Returns the base to the given exponent
+Parameters
+ base The base number
+ exp The exponent
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), radians(), round(), sign(), square(), sum(),
+ total()
+Example
+#1 To raise two to the power of three:
+ ;SELECT power(2, 3)
+
+
+
+printf(format, X)
+══════════════════════════════════════════════════════════════════════
+ Returns a string with this functions arguments substituted into the
+ given format. Substitution points are specified using percent (%)
+ options, much like the standard C printf() function.
+Parameters
+ format The format of the string to return.
+ X The argument to substitute at a given position in the
+ format.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), proper(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Examples
+#1 To substitute 'World' into the string 'Hello, %s!':
+ ;SELECT printf('Hello, %s!', 'World')
+
+
+#2 To right-align 'small' in the string 'align:' with a column width of 10:
+ ;SELECT printf('align: % 10s', 'small')
+
+
+#3 To format 11 with a width of five characters and leading zeroes:
+ ;SELECT printf('value: %05d', 11)
+
+
+
+proper(str)
+══════════════════════════════════════════════════════════════════════
+ Capitalize the first character of words in the given string
+Parameter
+ str The string to capitalize.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), regexp_capture(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Example
+#1 To capitalize the words in the string 'hello, world!':
+ ;SELECT proper('hello, world!')
+
+
+
+quote(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the text of an SQL literal which is the value of its
+ argument suitable for inclusion into an SQL statement.
+Parameter
+ X The string to quote.
+
+Examples
+#1 To quote the string 'abc':
+ ;SELECT quote('abc')
+
+
+#2 To quote the string 'abc'123':
+ ;SELECT quote('abc''123')
+
+
+
+radians(degrees)
+══════════════════════════════════════════════════════════════════════
+ Converts degrees to radians
+Parameter
+ degrees The degrees value to convert to radians
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), round(), sign(), square(), sum(), total()
+Example
+#1 To convert 180 degrees to radians:
+ ;SELECT radians(180)
+
+
+
+raise_error(msg)
+══════════════════════════════════════════════════════════════════════
+ Raises an error with the given message when executed
+Parameter
+ msg The error message
+
+
+random()
+══════════════════════════════════════════════════════════════════════
+ Returns a pseudo-random integer between -9223372036854775808 and +
+ 9223372036854775807.
+
+
+randomblob(N)
+══════════════════════════════════════════════════════════════════════
+ Return an N-byte blob containing pseudo-random bytes.
+Parameter
+ N The size of the blob in bytes.
+
+
+rank()
+══════════════════════════════════════════════════════════════════════
+ Returns the row_number() of the first peer in each group with gaps
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ nth_value(), ntile(), percent_rank(), row_number()
+
+readlink(path)
+══════════════════════════════════════════════════════════════════════
+ Read the target of a symbolic link.
+Parameter
+ path The path to the symbolic link.
+See Also
+ basename(), dirname(), joinpath(), realpath()
+
+realpath(path)
+══════════════════════════════════════════════════════════════════════
+ Returns the resolved version of the given path, expanding symbolic
+ links and resolving '.' and '..' references.
+Parameter
+ path The path to resolve.
+See Also
+ basename(), dirname(), joinpath(), readlink()
+
+regexp(re, str)
+══════════════════════════════════════════════════════════════════════
+ Test if a string matches a regular expression
+Parameters
+ re The regular expression to use
+ str The string to test against the regular expression
+
+
+regexp_capture(string, pattern)
+══════════════════════════════════════════════════════════════════════
+ A table-valued function that executes a regular-expression over a
+ string and returns the captured values. If the regex only matches a
+ subset of the input string, it will be rerun on the remaining parts
+ of the string until no more matches are found.
+Parameters
+ string The string to match against the given pattern.
+ pattern The regular expression to match.
+Results
+ match_index The match iteration. This value will increase each
+ time a new match is found in the input string.
+ capture_index The index of the capture in the regex.
+ capture_name The name of the capture in the regex.
+ capture_count The total number of captures in the regex.
+ range_start The start of the capture in the input string.
+ range_stop The stop of the capture in the input string.
+ content The captured value from the string.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_match(), regexp_replace(),
+ replace(), replicate(), reverse(), rightstr(), rtrim(), sparkline(),
+ spooky_hash(), startswith(), strfilter(), substr(), trim(), unicode(),
+ upper(), xpath()
+Example
+#1 To extract the key/value pairs 'a'/1 and 'b'/2 from the string 'a=1; b=2':
+ ;SELECT * FROM regexp_capture('a=1; b=2', '(\w+)=(\d+)')
+
+
+
+regexp_match(re, str)
+══════════════════════════════════════════════════════════════════════
+ Match a string against a regular expression and return the capture
+ groups as JSON.
+Parameters
+ re The regular expression to use
+ str The string to test against the regular expression
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_replace(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), unicode(), upper(), xpath()
+Examples
+#1 To capture the digits from the string '123':
+ ;SELECT regexp_match('(\d+)', '123')
+
+
+#2 To capture a number and word into a JSON object with the properties 'col_0' and 'col_1'
+ :
+ ;SELECT regexp_match('(\d+) (\w+)', '123 four')
+
+
+#3 To capture a number and word into a JSON object with the named properties 'num' and '
+ str':
+ ;SELECT regexp_match('(?<num>\d+) (?<str>\w+)', '123 four')
+
+
+
+regexp_replace(str, re, repl)
+══════════════════════════════════════════════════════════════════════
+ Replace the parts of a string that match a regular expression.
+Parameters
+ str The string to perform replacements on
+ re The regular expression to match
+ repl The replacement string. You can reference capture groups
+ with a backslash followed by the number of the group,
+ starting with 1.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_match(), replace(), replicate(), reverse(), rightstr(), rtrim(),
+ sparkline(), spooky_hash(), startswith(), strfilter(), substr(),
+ trim(), unicode(), upper(), xpath()
+Examples
+#1 To replace the word at the start of the string 'Hello, World!' with 'Goodbye':
+ ;SELECT regexp_replace('Hello, World!', '^(\w+)', 'Goodbye')
+
+
+#2 To wrap alphanumeric words with angle brackets:
+ ;SELECT regexp_replace('123 abc', '(\w+)', '<\1>')
+
+
+
+replace(str, old, replacement)
+══════════════════════════════════════════════════════════════════════
+ Returns a string formed by substituting the replacement string for
+ every occurrence of the old string in the given string.
+Parameters
+ str The string to perform substitutions on.
+ old The string to be replaced.
+ replacement The string to replace any occurrences of the old
+ string with.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replicate(), reverse(), rightstr(), rtrim(),
+ sparkline(), spooky_hash(), startswith(), strfilter(), substr(),
+ trim(), unicode(), upper(), xpath()
+Examples
+#1 To replace the string 'x' with 'z' in 'abc':
+ ;SELECT replace('abc', 'x', 'z')
+
+
+#2 To replace the string 'a' with 'z' in 'abc':
+ ;SELECT replace('abc', 'a', 'z')
+
+
+
+replicate(str, N)
+══════════════════════════════════════════════════════════════════════
+ Returns the given string concatenated N times.
+Parameters
+ str The string to replicate.
+ N The number of times to replicate the string.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), reverse(), rightstr(), rtrim(),
+ sparkline(), spooky_hash(), startswith(), strfilter(), substr(),
+ trim(), unicode(), upper(), xpath()
+Example
+#1 To repeat the string 'abc' three times:
+ ;SELECT replicate('abc', 3)
+
+
+
+reverse(str)
+══════════════════════════════════════════════════════════════════════
+ Returns the reverse of the given string.
+Parameter
+ str The string to reverse.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), rightstr(), rtrim(),
+ sparkline(), spooky_hash(), startswith(), strfilter(), substr(),
+ trim(), unicode(), upper(), xpath()
+Example
+#1 To reverse the string 'abc':
+ ;SELECT reverse('abc')
+
+
+
+rightstr(str, N)
+══════════════════════════════════════════════════════════════════════
+ Returns the N rightmost (UTF-8) characters in the given string.
+Parameters
+ str The string to return subset.
+ N The number of characters from the right side of the string to
+ return.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rtrim(),
+ sparkline(), spooky_hash(), startswith(), strfilter(), substr(),
+ trim(), unicode(), upper(), xpath()
+Examples
+#1 To get the last character of the string 'abc':
+ ;SELECT rightstr('abc', 1)
+
+
+#2 To get the last ten characters of a string, regardless of size:
+ ;SELECT rightstr('abc', 10)
+
+
+
+round(num, [digits])
+══════════════════════════════════════════════════════════════════════
+ Returns a floating-point value rounded to the given number of digits
+ to the right of the decimal point.
+Parameters
+ num The value to round.
+ digits The number of digits to the right of the decimal to round
+ to.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), sign(), square(), sum(),
+ total()
+Examples
+#1 To round the number 123.456 to an integer:
+ ;SELECT round(123.456)
+
+
+#2 To round the number 123.456 to a precision of 1:
+ ;SELECT round(123.456, 1)
+
+
+#3 To round the number 123.456 to a precision of 5:
+ ;SELECT round(123.456, 5)
+
+
+
+row_number()
+══════════════════════════════════════════════════════════════════════
+ Returns the number of the row within the current partition, starting
+ from 1.
+See Also
+ cume_dist(), dense_rank(), first_value(), lag(), last_value(), lead(),
+ nth_value(), ntile(), percent_rank(), rank()
+Example
+#1 To number messages from a process:
+ ;SELECT row_number() OVER (PARTITION BY ex_procname ORDER BY log_line) AS msg_num,
+ ex_procname, log_body FROM lnav_example_log
+
+
+
+rtrim(str, [chars])
+══════════════════════════════════════════════════════════════════════
+ Returns a string formed by removing any and all characters that
+ appear in the second argument from the right side of the first.
+Parameters
+ str The string to trim characters from the right side
+ chars The characters to trim. Defaults to spaces.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ sparkline(), spooky_hash(), startswith(), strfilter(), substr(),
+ trim(), unicode(), upper(), xpath()
+Examples
+#1 To trim the whitespace from the end of the string 'abc ':
+ ;SELECT rtrim('abc ')
+
+
+#2 To trim the characters 'b' and 'c' from the string 'abbbbcccc':
+ ;SELECT rtrim('abbbbcccc', 'bc')
+
+
+
+sign(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the sign of the given number as -1, 0, or 1
+Parameter
+ num The number
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), round(), square(), sum(),
+ total()
+Examples
+#1 To get the sign of 10:
+ ;SELECT sign(10)
+
+
+#2 To get the sign of 0:
+ ;SELECT sign(0)
+
+
+#3 To get the sign of -10:
+ ;SELECT sign(-10)
+
+
+
+sparkline(value, [upper])
+══════════════════════════════════════════════════════════════════════
+ Function used to generate a sparkline bar chart. The non-aggregate
+ version converts a single numeric value on a range to a bar chart
+ character. The aggregate version returns a string with a bar
+ character for every numeric input
+Parameters
+ value The numeric value to convert
+ upper The upper bound of the numeric range. The non-aggregate
+ version defaults to 100. The aggregate version uses the
+ largest value in the inputs.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), spooky_hash(), startswith(), strfilter(), substr(), trim(),
+ unicode(), upper(), xpath()
+Examples
+#1 To get the unicode block element for the value 32 in the range of 0-128:
+ ;SELECT sparkline(32, 128)
+
+
+#2 To chart the values in a JSON array:
+ ;SELECT sparkline(value) FROM json_each('[0, 1, 2, 3, 4, 5, 6, 7, 8]')
+
+
+
+spooky_hash(str, ...)
+══════════════════════════════════════════════════════════════════════
+ Compute the hash value for the given arguments.
+Parameter
+ str The string to hash
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), startswith(), strfilter(), substr(), trim(),
+ unicode(), upper(), xpath()
+Examples
+#1 To produce a hash for the string 'Hello, World!':
+ ;SELECT spooky_hash('Hello, World!')
+
+
+#2 To produce a hash for the parameters where one is NULL:
+ ;SELECT spooky_hash('Hello, World!', NULL)
+
+
+#3 To produce a hash for the parameters where one is an empty string:
+ ;SELECT spooky_hash('Hello, World!', '')
+
+
+#4 To produce a hash for the parameters where one is a number:
+ ;SELECT spooky_hash('Hello, World!', 123)
+
+
+
+sqlite_compileoption_get(N)
+══════════════════════════════════════════════════════════════════════
+ Returns the N-th compile-time option used to build SQLite or NULL if
+ N is out of range.
+Parameter
+ N The option number to get
+
+
+sqlite_compileoption_used(option)
+══════════════════════════════════════════════════════════════════════
+ Returns true (1) or false (0) depending on whether or not that
+ compile-time option was used during the build.
+Parameter
+ option The name of the compile-time option.
+
+Example
+#1 To check if the SQLite library was compiled with ENABLE_FTS3:
+ ;SELECT sqlite_compileoption_used('ENABLE_FTS3')
+
+
+
+sqlite_source_id()
+══════════════════════════════════════════════════════════════════════
+ Returns a string that identifies the specific version of the source
+ code that was used to build the SQLite library.
+
+
+sqlite_version()
+══════════════════════════════════════════════════════════════════════
+ Returns the version string for the SQLite library that is running.
+
+
+square(num)
+══════════════════════════════════════════════════════════════════════
+ Returns the square of the argument
+Parameter
+ num The number to square
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), round(), sign(), sum(),
+ total()
+Example
+#1 To get the square of two:
+ ;SELECT square(2)
+
+
+
+startswith(str, prefix)
+══════════════════════════════════════════════════════════════════════
+ Test if a string begins with the given prefix
+Parameters
+ str The string to test
+ prefix The prefix to check in the string
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), strfilter(), substr(), trim(),
+ unicode(), upper(), xpath()
+Examples
+#1 To test if the string 'foobar' starts with 'foo':
+ ;SELECT startswith('foobar', 'foo')
+
+
+#2 To test if the string 'foobar' starts with 'bar':
+ ;SELECT startswith('foobar', 'bar')
+
+
+
+strfilter(source, include)
+══════════════════════════════════════════════════════════════════════
+ Returns the source string with only the characters given in the
+ second parameter
+Parameters
+ source The string to filter
+ include The characters to include in the result
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), substr(), trim(),
+ unicode(), upper(), xpath()
+Example
+#1 To get the 'b', 'c', and 'd' characters from the string 'abcabc':
+ ;SELECT strfilter('abcabc', 'bcd')
+
+
+
+strftime(format, timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the date formatted according to the format string specified
+ as the first argument.
+Parameters
+ format A format string with substitutions similar to those
+ found in the strftime() standard C library.
+ timestring The string to convert to a date with time.
+ modifier A transformation that is applied to the value to the
+ left.
+See Also
+ date(), datetime(), julianday(), time(), timediff(), timeslice()
+Examples
+#1 To get the year from the timestamp '2017-01-02T03:04:05':
+ ;SELECT strftime('%Y', '2017-01-02T03:04:05')
+
+
+#2 To create a string with the time from the timestamp '2017-01-02T03:04:05' plus one
+ minute:
+ ;SELECT strftime('The time is: %H:%M:%S', '2017-01-02T03:04:05', '+1 minute')
+
+
+#3 To create a string with the Julian day from the epoch timestamp 1491341842:
+ ;SELECT strftime('Julian day: %J', 1491341842, 'unixepoch')
+
+
+
+substr(str, start, [size])
+══════════════════════════════════════════════════════════════════════
+ Returns a substring of input string X that begins with the Y-th
+ character and which is Z characters long.
+Parameters
+ str The string to extract a substring from.
+ start The index within 'str' that is the start of the substring.
+ Indexes begin at 1. A negative value means that the
+ substring is found by counting from the right rather than
+ the left.
+ size The size of the substring. If not given, then all
+ characters through the end of the string are returned. If
+ the value is negative, then the characters before the start
+ are returned.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(), trim(),
+ unicode(), upper(), xpath()
+Examples
+#1 To get the substring starting at the second character until the end of the string 'abc'
+ :
+ ;SELECT substr('abc', 2)
+
+
+#2 To get the substring of size one starting at the second character of the string 'abc':
+ ;SELECT substr('abc', 2, 1)
+
+
+#3 To get the substring starting at the last character until the end of the string 'abc':
+ ;SELECT substr('abc', -1)
+
+
+#4 To get the substring starting at the last character and going backwards one step of the
+ string 'abc':
+ ;SELECT substr('abc', -1, -1)
+
+
+
+sum(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the sum of the values in the group as an integer.
+Parameter
+ X The values to add.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), round(), sign(), square(),
+ total()
+Example
+#1 To sum all of the values in the column 'ex_duration' from the table 'lnav_example_log':
+ ;SELECT sum(ex_duration) FROM lnav_example_log
+
+
+
+time(timestring, modifier, ...)
+══════════════════════════════════════════════════════════════════════
+ Returns the time in this format: HH:MM:SS.
+Parameters
+ timestring The string to convert to a time.
+ modifier A transformation that is applied to the value to the
+ left.
+See Also
+ date(), datetime(), julianday(), strftime(), timediff(), timeslice()
+Examples
+#1 To get the time portion of the timestamp '2017-01-02T03:04:05':
+ ;SELECT time('2017-01-02T03:04:05')
+
+
+#2 To get the time portion of the timestamp '2017-01-02T03:04:05' plus one minute:
+ ;SELECT time('2017-01-02T03:04:05', '+1 minute')
+
+
+#3 To get the time portion of the epoch timestamp 1491341842:
+ ;SELECT time(1491341842, 'unixepoch')
+
+
+
+timediff(time1, time2)
+══════════════════════════════════════════════════════════════════════
+ Compute the difference between two timestamps in seconds
+Parameters
+ time1 The first timestamp
+ time2 The timestamp to subtract from the first
+See Also
+ date(), datetime(), julianday(), strftime(), time(), timeslice()
+Examples
+#1 To get the difference between two timestamps:
+ ;SELECT timediff('2017-02-03T04:05:06', '2017-02-03T04:05:00')
+
+
+#2 To get the difference between relative timestamps:
+ ;SELECT timediff('today', 'yesterday')
+
+
+
+timeslice(time, slice)
+══════════════════════════════════════════════════════════════════════
+ Return the start of the slice of time that the given timestamp falls
+ in. If the time falls outside of the slice, NULL is returned.
+Parameters
+ time The timestamp to get the time slice for.
+ slice The size of the time slices
+See Also
+ date(), datetime(), julianday(), strftime(), time(), timediff()
+Examples
+#1 To get the timestamp rounded down to the start of the ten minute slice:
+ ;SELECT timeslice('2017-01-01T05:05:00', '10m')
+
+
+#2 To group log messages into five minute buckets and count them:
+ ;SELECT timeslice(log_time_msecs, '5m') AS slice, count(1)
+ FROM lnav_example_log GROUP BY slice
+
+
+#3 To group log messages by those before 4:30am and after:
+ ;SELECT timeslice(log_time_msecs, 'before 4:30am') AS slice, count(1) FROM
+ lnav_example_log GROUP BY slice
+
+
+
+total(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the sum of the values in the group as a floating-point.
+Parameter
+ X The values to add.
+See Also
+ abs(), acos(), acosh(), asin(), asinh(), atan(), atan2(), atanh(),
+ atn2(), avg(), ceil(), degrees(), exp(), floor(), log(), log10(),
+ max(), min(), pi(), power(), radians(), round(), sign(), square(),
+ sum()
+Example
+#1 To total all of the values in the column 'ex_duration' from the table 'lnav_example_log
+ ':
+ ;SELECT total(ex_duration) FROM lnav_example_log
+
+
+
+total_changes()
+══════════════════════════════════════════════════════════════════════
+ Returns the number of row changes caused by INSERT, UPDATE or DELETE
+ statements since the current database connection was opened.
+
+
+trim(str, [chars])
+══════════════════════════════════════════════════════════════════════
+ Returns a string formed by removing any and all characters that
+ appear in the second argument from the left and right sides of the
+ first.
+Parameters
+ str The string to trim characters from the left and right sides.
+
+ chars The characters to trim. Defaults to spaces.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), unicode(), upper(), xpath()
+Examples
+#1 To trim whitespace from the start and end of the string ' abc ':
+ ;SELECT trim(' abc ')
+
+
+#2 To trim the characters '-' and '+' from the string '-+abc+-':
+ ;SELECT trim('-+abc+-', '-+')
+
+
+
+typeof(X)
+══════════════════════════════════════════════════════════════════════
+ Returns a string that indicates the datatype of the expression X: "
+ null", "integer", "real", "text", or "blob".
+Parameter
+ X The expression to check.
+
+Examples
+#1 To get the type of the number 1:
+ ;SELECT typeof(1)
+
+
+#2 To get the type of the string 'abc':
+ ;SELECT typeof('abc')
+
+
+
+unicode(X)
+══════════════════════════════════════════════════════════════════════
+ Returns the numeric unicode code point corresponding to the first
+ character of the string X.
+Parameter
+ X The string to examine.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), upper(), xpath()
+Example
+#1 To get the unicode code point for the first character of 'abc':
+ ;SELECT unicode('abc')
+
+
+
+unlikely(value)
+══════════════════════════════════════════════════════════════════════
+ Short-hand for likelihood(X, 0.0625)
+Parameter
+ value The boolean value to return
+
+
+upper(str)
+══════════════════════════════════════════════════════════════════════
+ Returns a copy of the given string with all ASCII characters
+ converted to upper case.
+Parameter
+ str The string to convert.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), unicode(), xpath()
+Example
+#1 To uppercase the string 'aBc':
+ ;SELECT upper('aBc')
+
+
+
+xpath(xpath, xmldoc)
+══════════════════════════════════════════════════════════════════════
+ A table-valued function that executes an xpath expression over an
+ XML string and returns the selected values.
+Parameters
+ xpath The XPATH expression to evaluate over the XML document.
+ xmldoc The XML document as a string.
+Results
+ result The result of the XPATH expression.
+ node_path The absolute path to the node containing the result.
+ node_attr The node's attributes stored in JSON object.
+ node_text The node's text value.
+See Also
+ char(), charindex(), endswith(), extract(), group_concat(),
+ group_spooky_hash(), gunzip(), gzip(), humanize_file_size(), instr(),
+ leftstr(), length(), logfmt2json(), lower(), ltrim(), padc(), padl(),
+ padr(), printf(), proper(), regexp_capture(), regexp_match(),
+ regexp_replace(), replace(), replicate(), reverse(), rightstr(),
+ rtrim(), sparkline(), spooky_hash(), startswith(), strfilter(),
+ substr(), trim(), unicode(), upper()
+Examples
+#1 To select the XML nodes on the path '/abc/def':
+ ;SELECT * FROM xpath('/abc/def', '<abc><def a="b">Hello</def><def>Bye</def></abc>')
+
+
+#2 To select all 'a' attributes on the path '/abc/def':
+ ;SELECT * FROM xpath('/abc/def/@a', '<abc><def a="b">Hello</def><def>Bye</def></abc>')
+
+
+#3 To select the text nodes on the path '/abc/def':
+ ;SELECT * FROM xpath('/abc/def/text()', '<abc><def a="b">Hello &#x2605;</def></abc>')
+
+
+
+zeroblob(N)
+══════════════════════════════════════════════════════════════════════
+ Returns a BLOB consisting of N bytes of 0x00.
+Parameter
+ N The size of the BLOB.
+
+
+ATTACH DATABASE filename AS schema-name
+══════════════════════════════════════════════════════════════════════
+ Attach a database file to the current connection.
+Parameters
+ filename The path to the database file.
+ schema-name The prefix for tables in this database.
+
+Example
+#1 To attach the database file '/tmp/customers.db' with the name customers:
+ ;ATTACH DATABASE '/tmp/customers.db' AS customers
+
+
+
+CASE [base-expr] WHEN cmp-expr1 THEN then-expr1 [... WHEN cmp-exprN THEN then-exprN] [ELSE else-expr] END
+══════════════════════════════════════════════════════════════════════
+ Evaluate a series of expressions in order until one evaluates to
+ true and then return it's result. Similar to an IF-THEN-ELSE
+ construct in other languages.
+Parameters
+ base-expr The base expression that is used for comparison in the
+ branches
+ cmp-expr The expression to test if this branch should be taken
+ else-expr The result of this CASE if no branches matched.
+
+Example
+#1 To evaluate the number one and return the string 'one':
+ ;SELECT CASE 1 WHEN 0 THEN 'zero' WHEN 1 THEN 'one' END
+
+
+
+CREATE [TEMP] VIEW [IF NOT EXISTS] [schema-name.] view-name AS select-stmt
+══════════════════════════════════════════════════════════════════════
+ Assign a name to a SELECT statement
+Parameters
+ IF NOT EXISTS Do not create the view if it already exists
+ schema-name. The database to create the view in
+ view-name The name of the view
+ select-stmt The SELECT statement the view represents
+
+
+DELETE FROM table-name [WHERE cond]
+══════════════════════════════════════════════════════════════════════
+ Delete rows from a table
+Parameters
+ table-name The name of the table
+ cond The conditions used to delete the rows.
+
+
+DETACH DATABASE schema-name
+══════════════════════════════════════════════════════════════════════
+ Detach a database from the current connection.
+Parameter
+ schema-name The prefix for tables in this database.
+
+Example
+#1 To detach the database named 'customers':
+ ;DETACH DATABASE customers
+
+
+
+DROP VIEW [IF EXISTS] [schema-name.] view-name
+══════════════════════════════════════════════════════════════════════
+ Drop a view
+Parameters
+
+
+INSERT INTO [schema-name.] table-name [( column-name1 [, ... column-nameN] )] VALUES ( expr1 [, ... exprN] )
+══════════════════════════════════════════════════════════════════════
+ Insert rows into a table
+Parameters
+
+Example
+#1 To insert the pair containing 'MSG' and 'HELLO, WORLD!' into the 'environ'
+ table:
+ ;INSERT INTO environ VALUES ('MSG', 'HELLO, WORLD!')
+
+
+
+OVER window-name
+══════════════════════════════════════════════════════════════════════
+ Executes the preceding function over a window
+Parameter
+ window-name The name of the window definition
+
+
+SELECT result-column1 [, ... result-columnN] [FROM table1 [, ... tableN]] [WHERE cond] [GROUP BY grouping-expr1 [, ... grouping-exprN]] [ORDER BY ordering-term1 [, ... ordering-termN]] [LIMIT limit-expr1 [, ... limit-exprN]]
+══════════════════════════════════════════════════════════════════════
+ Query the database and return zero or more rows of data.
+Parameters
+ result-column The expression used to generate a result for this
+ column.
+ table The table(s) to query for data
+ cond The conditions used to select the rows to return.
+ grouping-expr The expression to use when grouping rows.
+ ordering-term The values to use when ordering the result set.
+ limit-expr The maximum number of rows to return.
+
+Example
+#1 To select all of the columns from the table 'syslog_log':
+ ;SELECT * FROM syslog_log
+
+
+
+UPDATE table SET column-name1 = expr1 [, ... column-nameN = exprN] [WHERE cond]
+══════════════════════════════════════════════════════════════════════
+ Modify a subset of values in zero or more rows of the given table
+Parameters
+ table The table to update
+ column-name The columns in the table to update.
+ cond The condition used to determine whether a row should
+ be updated.
+
+Example
+#1 To mark the syslog message at line 40:
+ ;UPDATE syslog_log SET log_mark = 1 WHERE log_line = 40
+
+
+
+WITH [RECURSIVE] cte-table-name AS select-stmt
+══════════════════════════════════════════════════════════════════════
+ Create a temporary view that exists only for the duration of a SQL
+ statement.
+Parameters
+ cte-table-name The name for the temporary table.
+ select-stmt The SELECT statement used to populate the temporary
+ table.
+
diff --git a/test/file_for_dot_read.sql b/test/file_for_dot_read.sql
new file mode 100644
index 0000000..fca253c
--- /dev/null
+++ b/test/file_for_dot_read.sql
@@ -0,0 +1,4 @@
+
+INSERT INTO environ VALUES ('SEARCH_TERM', '%mount%');
+
+SELECT log_line, log_body FROM syslog_log WHERE log_body LIKE $SEARCH_TERM
diff --git a/test/formats/collision/format.json b/test/formats/collision/format.json
new file mode 100644
index 0000000..35c25c0
--- /dev/null
+++ b/test/formats/collision/format.json
@@ -0,0 +1,48 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "zblued_log": {
+ "title": "blued",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\w{3}\\s+\\d{1,2} \\d{2}:\\d{2}:\\d{2})(?: (?<log_hostname>[a-zA-Z0-9:][^ ]+[a-zA-Z0-9]))? blued(?:\\[(?<log_pid>\\d+)])?:(?<body>(?:.|\\n)*)$"
+ }
+ },
+ "level-field": "body",
+ "level": {
+ "error": "(?:failed|failure|error)",
+ "warning" : "(?:warn|not responding|init: cannot execute)"
+ },
+ "value" : {
+ "log_hostname" : {
+ "kind" : "string",
+ "collate" : "ipaddress",
+ "identifier" : true
+ }
+ },
+ "sample" : [
+ {
+ "line" : "Apr 4 20:02:32 Tim-Stacks-iMac.local blued[59]: Release the WiFi lock"
+ }
+ ]
+ },
+ "xerror_log" : {
+ "title" : "Common Error Log",
+ "description" : "The default web error log format for servers like Apache.",
+ "regex" : {
+ "cups" : {
+ "pattern" : "^(?<level>\\w) \\[(?<timestamp>[^\\]]+)\\] (?<body>.*)"
+ }
+ },
+ "level-field": "level",
+ "level" : {
+ "error" : "E",
+ "warning" : "W",
+ "info" : "I"
+ },
+ "sample" : [
+ {
+ "line" : "E [08/Jun/2013:11:28:58 -0700] Unknown directive BrowseOrder on line 22 of /private/etc/cups/cupsd.conf."
+ }
+ ]
+ }
+}
diff --git a/test/formats/customlevel/format.json b/test/formats/customlevel/format.json
new file mode 100644
index 0000000..dde41fe
--- /dev/null
+++ b/test/formats/customlevel/format.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "leveltest_log": {
+ "description": "Log format used for testing levels",
+ "regex": {
+ "line": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}) (?<level>[^ ]+) (?<body>.*)$"
+ }
+ },
+ "level": {
+ "trace": "trace",
+ "debug": "debug",
+ "debug2": "debug2",
+ "debug3": "debug3",
+ "info": "info",
+ "warning": "warn",
+ "fatal": "fatal"
+ },
+ "sample": [
+ {
+ "line": "2016-06-30 12:00:01 trace tracemessage"
+ }
+ ]
+ }
+}
diff --git a/test/formats/jsontest-subsec/format.json b/test/formats/jsontest-subsec/format.json
new file mode 100644
index 0000000..8469c7f
--- /dev/null
+++ b/test/formats/jsontest-subsec/format.json
@@ -0,0 +1,27 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "subsec_json_log": {
+ "title": "JSON Log with subsecond field",
+ "json": true,
+ "file-pattern": "logfile_json_subsec\\.json",
+ "line-format": [
+ {
+ "field": "__timestamp__"
+ },
+ " ",
+ {
+ "field": "msg"
+ }
+ ],
+ "timestamp-field": "instant/epochSecond",
+ "subsecond-field": "instant/nanoOfSecond",
+ "subsecond-units": "nano",
+ "body-field": "msg",
+ "value": {
+ "instant": {
+ "kind": "json",
+ "hidden": true
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/test/formats/jsontest/format.json b/test/formats/jsontest/format.json
new file mode 100644
index 0000000..333b8a9
--- /dev/null
+++ b/test/formats/jsontest/format.json
@@ -0,0 +1,47 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "test_log": {
+ "title": "Test JSON Log",
+ "json": true,
+ "file-pattern": "logfile_json\\.json",
+ "description": "Test config",
+ "line-format": [
+ "\n[",
+ {
+ "field": "ts"
+ },
+ "] ",
+ {
+ "field": "lvl",
+ "auto-width": true
+ },
+ " ",
+ {
+ "field": "msg"
+ }
+ ],
+ "level-field": "lvl",
+ "timestamp-field": "ts",
+ "body-field": "msg",
+ "value": {
+ "msg": {
+ "rewriter": ";SELECT :msg || 'bork bork bork'"
+ },
+ "arr": {
+ "kind": "json"
+ },
+ "obj": {
+ "kind": "json"
+ },
+ "lvl": {
+ "kind": "string",
+ "hidden": true
+ },
+ "user": {
+ "kind": "string",
+ "identifier": true,
+ "rewriter": "|rewrite-user"
+ }
+ }
+ }
+}
diff --git a/test/formats/jsontest/lnav-logstash.json b/test/formats/jsontest/lnav-logstash.json
new file mode 100644
index 0000000..27f9239
--- /dev/null
+++ b/test/formats/jsontest/lnav-logstash.json
@@ -0,0 +1,47 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "logstash_dam": {
+ "title": "Logstash Java JSON",
+ "url": "https://github.com/logstash/logstash-logback-encoder",
+ "description": "Log format for DAM Logstash JSON",
+ "json": true,
+ "hide-extra": false,
+ "file-pattern": "\\.clog.*",
+ "multiline": false,
+ "line-format": [
+ { "field" : "@timestamp" },
+ " ",
+ { "field" : "ipaddress" },
+ " ",
+ { "field" : "message" },
+ " ",
+ { "field" : "stack_trace", "default-value" : "" }
+ ],
+ "timestamp-field" : "@timestamp",
+ "body-field" : "message",
+ "level-field" : "level",
+ "level" : {
+ "trace" : "TRACE",
+ "debug" : "DEBUG",
+ "info" : "INFO",
+ "error" : "ERROR",
+ "warning" : "WARN"
+ },
+ "value" : {
+ "logger_name" : {
+ "kind" : "string",
+ "identifier" : true
+ },
+ "ipaddress" : {
+ "kind" : "string",
+ "identifier" : true
+ },
+ "level_value" : {
+ "hidden": true
+ },
+ "stack_trace" : {
+ "kind" : "string"
+ }
+ }
+ }
+}
diff --git a/test/formats/jsontest/rewrite-user.lnav b/test/formats/jsontest/rewrite-user.lnav
new file mode 100644
index 0000000..e34aa78
--- /dev/null
+++ b/test/formats/jsontest/rewrite-user.lnav
@@ -0,0 +1,2 @@
+
+;SELECT 'mailto:' || :user
diff --git a/test/formats/jsontest2/format.json b/test/formats/jsontest2/format.json
new file mode 100644
index 0000000..167aa0f
--- /dev/null
+++ b/test/formats/jsontest2/format.json
@@ -0,0 +1,49 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "json_log2": {
+ "title": "Test JSON Log with integer levels",
+ "json": true,
+ "file-pattern": "logfile_.*json2\\.json",
+ "description": "Test config",
+ "line-format": [
+ {
+ "field": "ts"
+ },
+ " ",
+ {
+ "field": "ts",
+ "timestamp-format": "abc %S def"
+ },
+ " ",
+ {
+ "field": "lvl",
+ "min-width": 5
+ },
+ " ",
+ {
+ "field": "cl",
+ "max-width": 5
+ },
+ " ",
+ {
+ "field": "msg"
+ }
+ ],
+ "level-field": "lvl",
+ "level": {
+ "info": 0,
+ "error": 10
+ },
+ "timestamp-field": "ts",
+ "body-field": "msg",
+ "value": {
+ "user": {
+ "kind": "string",
+ "identifier": true
+ },
+ "cl": {
+ "kind": "string"
+ }
+ }
+ }
+}
diff --git a/test/formats/jsontest3/format.json b/test/formats/jsontest3/format.json
new file mode 100644
index 0000000..7f8d3fe
--- /dev/null
+++ b/test/formats/jsontest3/format.json
@@ -0,0 +1,84 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "json_log3": {
+ "title": "Test JSON Log Format",
+ "description": "Test JSON Log Format",
+ "file-pattern": "logfile_.*json3\\.json",
+ "json": true,
+ "hide-extra": true,
+ "convert-to-local-time": true,
+ "line-format": [
+ {
+ "field": "__timestamp__"
+ },
+ " ",
+ {
+ "field": "client_ip"
+ },
+ " ",
+ {
+ "field": "request/method"
+ },
+ " ",
+ {
+ "field": "response/status"
+ },
+ " ",
+ {
+ "field": "response/size",
+ "auto-width": true
+ },
+ " ",
+ {
+ "field": "request/uri"
+ }
+ ],
+ "value": {
+ "started_at": {
+ "kind": "integer",
+ "identifier": true
+ },
+ "client_ip": {
+ "kind": "string",
+ "identifier": true
+ },
+ "request/method": {
+ "kind": "string",
+ "identifier": true
+ },
+ "request/uri": {
+ "kind": "string",
+ "identifier": true
+ },
+ "request/size": {
+ "kind": "integer",
+ "identifier": false,
+ "hidden": true
+ },
+ "response/status": {
+ "kind": "integer",
+ "foreign-key": true
+ },
+ "response/size": {
+ "kind": "integer"
+ },
+ "details1": {
+ "hidden": true
+ },
+ "details2": {
+ "hidden": true
+ },
+ "details3": {
+ "hidden": true
+ }
+ },
+ "timestamp-field": "started_at",
+ "timestamp-divisor": 1000,
+ "level-field": "response/status",
+ "level": {
+ "info": "2\\d+",
+ "warning": "4\\d+",
+ "critical": "5\\d+"
+ }
+ }
+}
diff --git a/test/formats/nestedjson/format.json b/test/formats/nestedjson/format.json
new file mode 100644
index 0000000..afa6b08
--- /dev/null
+++ b/test/formats/nestedjson/format.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "ntest_log": {
+ "title": "Test JSON Log",
+ "json": true,
+ "file-pattern": "logfile_(nested|invalid)_json\\d*\\.json",
+ "description": "Test config",
+ "line-format": [
+ {
+ "field": "ts"
+ },
+ " ",
+ {
+ "field": "/@fields/lvl"
+ },
+ " ",
+ { "field" : "@fields/msg" }
+ ],
+ "level-field" : "/@fields/lvl",
+ "timestamp-field": "ts",
+ "body-field" : "@fields/msg",
+ "value" : {
+ "@fields/user" : {
+ "kind" : "string",
+ "identifier" : true
+ },
+ "@fields/trace#" : {
+ "kind" : "string"
+ }
+ }
+ }
+}
diff --git a/test/formats/scripts/multiline-echo.lnav b/test/formats/scripts/multiline-echo.lnav
new file mode 100644
index 0000000..6913088
--- /dev/null
+++ b/test/formats/scripts/multiline-echo.lnav
@@ -0,0 +1,3 @@
+;SELECT 'World' as name
+:echo Hello, ${name}!
+Goodbye, ${name}!
diff --git a/test/formats/scripts/nested-redirecting.lnav b/test/formats/scripts/nested-redirecting.lnav
new file mode 100644
index 0000000..785c605
--- /dev/null
+++ b/test/formats/scripts/nested-redirecting.lnav
@@ -0,0 +1,5 @@
+:echo HOWDY!
+:redirect-to hw2.txt
+:echo HELLO, WORLD!
+:redirect-to
+:echo GOODBYE, WORLD!
diff --git a/test/formats/scripts/redirecting.lnav b/test/formats/scripts/redirecting.lnav
new file mode 100644
index 0000000..84b42c2
--- /dev/null
+++ b/test/formats/scripts/redirecting.lnav
@@ -0,0 +1,6 @@
+:echo Howdy!
+:redirect-to hw.txt
+:echo Hello, World!
+|nested-redirecting
+:redirect-to
+:echo Goodbye, World!
diff --git a/test/formats/sqldir/init.sql b/test/formats/sqldir/init.sql
new file mode 100644
index 0000000..4e17034
--- /dev/null
+++ b/test/formats/sqldir/init.sql
@@ -0,0 +1,5 @@
+
+CREATE VIEW web_status AS
+ SELECT group_concat(cs_uri_stem), sc_status FROM access_log group by sc_status;
+
+INSERT into lnav_view_filters VALUES ("log", 5, 0, "in", "regex", "credential status");
diff --git a/test/formats/timestamp/format.json b/test/formats/timestamp/format.json
new file mode 100644
index 0000000..2a3740a
--- /dev/null
+++ b/test/formats/timestamp/format.json
@@ -0,0 +1,26 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "epoch_log": {
+ "title": "epoch timestamp test",
+ "regex": {
+ "std": {
+ "pattern": "^(?<timestamp>\\d+) (?<body>.*)$"
+ },
+ "non_epoch": {
+ "pattern": "^(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d+) (?<body>.*)$"
+ }
+ },
+ "timestamp-format": [
+ "%i",
+ "%Y-%m-%d %H:%M:%S.%f"
+ ],
+ "sample": [
+ {
+ "line": "1428634687123 Hello, World!"
+ },
+ {
+ "line": "2022-09-10 19:57:36.123456 Hello, World"
+ }
+ ]
+ }
+}
diff --git a/test/formats/xmlmsg/format.json b/test/formats/xmlmsg/format.json
new file mode 100644
index 0000000..efd6f42
--- /dev/null
+++ b/test/formats/xmlmsg/format.json
@@ -0,0 +1,73 @@
+{
+ "$schema": "https://lnav.org/schemas/format-v1.schema.json",
+ "xml_msg_log": {
+ "title": "",
+ "description": "",
+ "regex": {
+ "std": {
+ "pattern": "^\\[(?<timestamp>\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2},\\d{3})\\]\\s+(?<level>\\w+)\\s+\\[(?<module>[^:]*):(?<line>\\d+)\\]\\s*(?<body>[^\\n]*)\\n?(?<msg_data>.*)"
+ }
+ },
+ "level": {
+ "critical": "CRITICAL",
+ "error": "ERROR",
+ "warning": "WARNING",
+ "info": "INFO",
+ "debug": "DEBUG"
+ },
+ "value": {
+ "module": {
+ "kind": "string",
+ "identifier": true,
+ "description": "Python source module which emitted log entry",
+ "rewriter": ";SELECT ''"
+ },
+ "line": {
+ "kind": "integer",
+ "description": "Line number – in the module – where log entry was emitted"
+ },
+ "msg_data": {
+ "kind": "xml",
+ "rewriter": ";SELECT node_path FROM xpath('//*', :msg_data)"
+ }
+ },
+ "highlights": {
+ "client_id": {
+ "pattern": "(?<=>)\\d+(?=<\/client>)",
+ "color": "Orange1",
+ "underline": true
+ },
+ "reply_error": {
+ "pattern": "(?<=<result>)ERROR(?=</result>)",
+ "color": "Red1"
+ },
+ "request": {
+ "pattern": "<request[^>]*>",
+ "color": "Green"
+ },
+ "reply": {
+ "pattern": "<head[^>]*>",
+ "color": "Gold1"
+ }
+ },
+ "tags": {
+ "xml-req": {
+ "pattern": "Full request text:"
+ }
+ },
+ "sample": [
+ {
+ "line": "[2020-12-10 06:56:41,477] INFO [m:108] Calling 'x' with params:",
+ "level": "info"
+ },
+ {
+ "line": "[2020-12-10 06:56:41,092] DEBUG [m:69] Full request text:\n<?xml version='1.0' encoding='iso-8859-2'?>\n<a-request>\n <head>\n x\n </head>\n <source>\n x\n </source>\n <request>\n <name>\n x\n </name>\n </request>\n</a-request>\n",
+ "level": "debug"
+ },
+ {
+ "line": "[2020-12-10 06:56:41,099] DEBUG [m:85] Full reply text:\n<?xml version='1.0' encoding='iso-8859-2'?>\n<a-reply>\n <head>\n x\n </head>\n <reply>\n <status>\n <result>OK</result>\n </status>\n <name>\n x\n </name>\n </reply>\n <technical-track>\n x\n </technical-track>\n</a-reply>\n",
+ "level": "debug"
+ }
+ ]
+ }
+} \ No newline at end of file
diff --git a/test/gp_test.cc b/test/gp_test.cc
new file mode 100644
index 0000000..ba2700b
--- /dev/null
+++ b/test/gp_test.cc
@@ -0,0 +1,103 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "grep_proc.hh"
+#include "line_buffer.hh"
+#include "logfile.hh"
+
+class my_source : public grep_proc_source {
+public:
+ logfile* ms_lf;
+
+ my_source(logfile* lf) : ms_lf(lf){};
+
+ size_t grep_lines(void)
+ {
+ return this->ms_lf->size();
+ };
+
+ void grep_value_for_line(int line, std::string& value_out, int pass)
+ {
+ value_out = this->ms_lf->read_line(this->ms_lf->begin() + line);
+ };
+};
+
+class my_sink : public grep_proc_sink {
+public:
+ void grep_match(grep_line_t line, int start, int end)
+ {
+ printf("%d - %d:%d\n", (int) line, start, end);
+ };
+};
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ auto_fd fd;
+
+ fd = open("/tmp/gp.err", O_WRONLY | O_CREAT | O_APPEND, 0666);
+ dup2(fd, STDERR_FILENO);
+ fprintf(stderr, "startup\n");
+
+ if (argc < 2) {
+ fprintf(stderr, "error: no file given\n");
+ } else {
+ logfile lf(argv[1]);
+ lf.rebuild_index();
+ my_source ms(&lf);
+ my_sink msink;
+ grep_proc gp("pnp", ms);
+
+ gp.start();
+ gp.set_sink(&msink);
+
+ fd_set read_fds;
+
+ int maxfd = gp.update_fd_set(read_fds);
+
+ while (1) {
+ fd_set rfds = read_fds;
+ select(maxfd + 1, &rfds, NULL, NULL, NULL);
+
+ gp.check_fd_set(rfds);
+ if (!FD_ISSET(maxfd, &read_fds))
+ break;
+ }
+ }
+
+ return retval;
+}
diff --git a/test/lb_test.cc b/test/lb_test.cc
new file mode 100644
index 0000000..d82ad3c
--- /dev/null
+++ b/test/lb_test.cc
@@ -0,0 +1,65 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "line_buffer.hh"
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ auto_fd fd;
+
+ fd = open("/tmp/lb.err", O_WRONLY | O_CREAT | O_APPEND, 0666);
+ dup2(fd, STDERR_FILENO);
+ fprintf(stderr, "startup\n");
+
+ if (argc < 2) {
+ fprintf(stderr, "error: no file given\n");
+ } else if ((fd = open(argv[1], O_RDONLY)) == -1) {
+ perror("open");
+ } else {
+ const char* line;
+ line_buffer lb;
+ size_t len;
+
+ lb.set_fd(fd);
+ while ((line = lb.read_line(len)) != NULL) {
+ printf("%s\n", line);
+ }
+ }
+
+ return retval;
+}
diff --git a/test/listview_output.0 b/test/listview_output.0
new file mode 100644
index 0000000..3d82d0d
--- /dev/null
+++ b/test/listview_output.0
@@ -0,0 +1,70 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋Hello x┋
+A └┛ alt
+S 2 ┋World! x┋
+A └┛ alt
+S 3 ┋ x┋
+A └┛ alt
+S 4 ┋ x┋
+A └┛ alt
+S 5 ┋ x┋
+A └┛ alt
+S 6 ┋ x┋
+A └┛ alt
+S 7 ┋ x┋
+A └┛ alt
+S 8 ┋ x┋
+A └┛ alt
+S 9 ┋ x┋
+A └┛ alt
+S 10 ┋ x┋
+A └┛ alt
+S 11 ┋ x┋
+A └┛ alt
+S 12 ┋ x┋
+A └┛ alt
+S 13 ┋ x┋
+A └┛ alt
+S 14 ┋ x┋
+A └┛ alt
+S 15 ┋ x┋
+A └┛ alt
+S 16 ┋ x┋
+A └┛ alt
+S 17 ┋ x┋
+A └┛ alt
+S 18 ┋ x┋
+A └┛ alt
+S 19 ┋ x┋
+A └┛ alt
+S 20 ┋ x┋
+A └┛ alt
+S 21 ┋ x┋
+A └┛ alt
+S 22 ┋ x┋
+A └┛ alt
+S 23 ┋ x┋
+A └┛ alt
+S 24 ┋ x ┋
+A ···············································································└ backspace
+A └┛ alt
+A ···············································································└ backspace
+CSI Set Replace mode
+S 24 ┋ ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output.1 b/test/listview_output.1
new file mode 100644
index 0000000..17f7202
--- /dev/null
+++ b/test/listview_output.1
@@ -0,0 +1,70 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋World! x┋
+A └┛ alt
+S 2 ┋ x┋
+A └┛ alt
+S 3 ┋ x┋
+A └┛ alt
+S 4 ┋ x┋
+A └┛ alt
+S 5 ┋ x┋
+A └┛ alt
+S 6 ┋ x┋
+A └┛ alt
+S 7 ┋ x┋
+A └┛ alt
+S 8 ┋ x┋
+A └┛ alt
+S 9 ┋ x┋
+A └┛ alt
+S 10 ┋ x┋
+A └┛ alt
+S 11 ┋ x┋
+A └┛ alt
+S 12 ┋ x┋
+A └┛ alt
+S 13 ┋ x┋
+A └┛ alt
+S 14 ┋ x┋
+A └┛ alt
+S 15 ┋ x┋
+A └┛ alt
+S 16 ┋ x┋
+A └┛ alt
+S 17 ┋ x┋
+A └┛ alt
+S 18 ┋ x┋
+A └┛ alt
+S 19 ┋ x┋
+A └┛ alt
+S 20 ┋ x┋
+A └┛ alt
+S 21 ┋ x┋
+A └┛ alt
+S 22 ┋ x┋
+A └┛ alt
+S 23 ┋ x┋
+A └┛ alt
+S 24 ┋ x ┋
+A ···············································································└ backspace
+A └┛ alt
+A ···············································································└ backspace
+CSI Set Replace mode
+S 24 ┋ ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output.2 b/test/listview_output.2
new file mode 100644
index 0000000..a028d66
--- /dev/null
+++ b/test/listview_output.2
@@ -0,0 +1,70 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋ello x┋
+A └┛ alt
+S 2 ┋orld! x┋
+A └┛ alt
+S 3 ┋ x┋
+A └┛ alt
+S 4 ┋ x┋
+A └┛ alt
+S 5 ┋ x┋
+A └┛ alt
+S 6 ┋ x┋
+A └┛ alt
+S 7 ┋ x┋
+A └┛ alt
+S 8 ┋ x┋
+A └┛ alt
+S 9 ┋ x┋
+A └┛ alt
+S 10 ┋ x┋
+A └┛ alt
+S 11 ┋ x┋
+A └┛ alt
+S 12 ┋ x┋
+A └┛ alt
+S 13 ┋ x┋
+A └┛ alt
+S 14 ┋ x┋
+A └┛ alt
+S 15 ┋ x┋
+A └┛ alt
+S 16 ┋ x┋
+A └┛ alt
+S 17 ┋ x┋
+A └┛ alt
+S 18 ┋ x┋
+A └┛ alt
+S 19 ┋ x┋
+A └┛ alt
+S 20 ┋ x┋
+A └┛ alt
+S 21 ┋ x┋
+A └┛ alt
+S 22 ┋ x┋
+A └┛ alt
+S 23 ┋ x┋
+A └┛ alt
+S 24 ┋ x ┋
+A ···············································································└ backspace
+A └┛ alt
+A ···············································································└ backspace
+CSI Set Replace mode
+S 24 ┋ ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output.3 b/test/listview_output.3
new file mode 100644
index 0000000..b29ea7d
--- /dev/null
+++ b/test/listview_output.3
@@ -0,0 +1,70 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋orld! x┋
+A └┛ alt
+S 2 ┋ x┋
+A └┛ alt
+S 3 ┋ x┋
+A └┛ alt
+S 4 ┋ x┋
+A └┛ alt
+S 5 ┋ x┋
+A └┛ alt
+S 6 ┋ x┋
+A └┛ alt
+S 7 ┋ x┋
+A └┛ alt
+S 8 ┋ x┋
+A └┛ alt
+S 9 ┋ x┋
+A └┛ alt
+S 10 ┋ x┋
+A └┛ alt
+S 11 ┋ x┋
+A └┛ alt
+S 12 ┋ x┋
+A └┛ alt
+S 13 ┋ x┋
+A └┛ alt
+S 14 ┋ x┋
+A └┛ alt
+S 15 ┋ x┋
+A └┛ alt
+S 16 ┋ x┋
+A └┛ alt
+S 17 ┋ x┋
+A └┛ alt
+S 18 ┋ x┋
+A └┛ alt
+S 19 ┋ x┋
+A └┛ alt
+S 20 ┋ x┋
+A └┛ alt
+S 21 ┋ x┋
+A └┛ alt
+S 22 ┋ x┋
+A └┛ alt
+S 23 ┋ x┋
+A └┛ alt
+S 24 ┋ x ┋
+A ···············································································└ backspace
+A └┛ alt
+A ···············································································└ backspace
+CSI Set Replace mode
+S 24 ┋ ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output.4 b/test/listview_output.4
new file mode 100644
index 0000000..8f6c5e7
--- /dev/null
+++ b/test/listview_output.4
@@ -0,0 +1,68 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 2 ┋Hello x┋
+A └┛ alt
+S 3 ┋World! x┋
+A └┛ alt
+S 4 ┋2 x┋
+A └┛ alt
+S 5 ┋3 x┋
+A └┛ alt
+S 6 ┋4 x┋
+A └┛ alt
+S 7 ┋5 x┋
+A └┛ alt
+S 8 ┋6 x┋
+A └┛ alt
+S 9 ┋7 x┋
+A └┛ alt
+S 10 ┋8 x┋
+A └┛ alt
+S 11 ┋9 x┋
+A └┛ alt
+S 12 ┋10 x┋
+A └┛ alt
+S 13 ┋11 x┋
+A └┛ alt
+S 14 ┋12 x┋
+A └┛ alt
+S 15 ┋13 x┋
+A └┛ alt
+S 16 ┋14 x┋
+A └┛ alt
+S 17 ┋15 x┋
+A └┛ alt
+S 18 ┋16 x┋
+A └┛ alt
+S 19 ┋17 x┋
+A └┛ alt
+S 20 ┋18 x┋
+A └┛ alt
+S 21 ┋19 x┋
+A └┛ alt
+S 22 ┋20 x┋
+A └┛ alt
+S 23 ┋21 x┋
+A └┛ alt
+S 24 ┋22 x ┋
+A ···············································································└ backspace
+A └┛ alt
+A ···············································································└ backspace
+CSI Set Replace mode
+S 24 ┋ ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output.5 b/test/listview_output.5
new file mode 100644
index 0000000..021b8d7
--- /dev/null
+++ b/test/listview_output.5
@@ -0,0 +1,59 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 2 ┋Hello x┋
+A └┛ alt
+S 3 ┋World! x┋
+A └┛ alt
+S 4 ┋2 x┋
+A └┛ alt
+S 5 ┋3 x┋
+A └┛ alt
+S 6 ┋4 x┋
+A └┛ alt
+S 7 ┋5 x┋
+A └┛ alt
+S 8 ┋6 x┋
+A └┛ alt
+S 9 ┋7 x┋
+A └┛ alt
+S 10 ┋8 x┋
+A └┛ alt
+S 11 ┋9 x┋
+A └┛ alt
+S 12 ┋10 x┋
+A └┛ alt
+S 13 ┋11 x┋
+A └┛ alt
+S 14 ┋12 x┋
+A └┛ alt
+S 15 ┋13 x┋
+A └┛ alt
+S 16 ┋14 x┋
+A └┛ alt
+S 17 ┋15 x┋
+A └┛ alt
+S 18 ┋16 x┋
+A └┛ alt
+S 19 ┋17 x┋
+A └┛ alt
+S 20 ┋18 x┋
+A └┛ alt
+S 21 ┋19 x┋
+A └┛ alt
+S 22 ┋20 x┋
+A └┛ alt
+S 23 ┋21 x┋
+A └┛ alt
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output.6 b/test/listview_output.6
new file mode 100644
index 0000000..f5765d5
--- /dev/null
+++ b/test/listview_output.6
@@ -0,0 +1,59 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 2 ┋World! x┋
+A └┛ alt
+S 3 ┋2 x┋
+A └┛ alt
+S 4 ┋3 x┋
+A └┛ alt
+S 5 ┋4 x┋
+A └┛ alt
+S 6 ┋5 x┋
+A └┛ alt
+S 7 ┋6 x┋
+A └┛ alt
+S 8 ┋7 x┋
+A └┛ alt
+S 9 ┋8 x┋
+A └┛ alt
+S 10 ┋9 x┋
+A └┛ alt
+S 11 ┋10 x┋
+A └┛ alt
+S 12 ┋11 x┋
+A └┛ alt
+S 13 ┋12 x┋
+A └┛ alt
+S 14 ┋13 x┋
+A └┛ alt
+S 15 ┋14 x┋
+A └┛ alt
+S 16 ┋15 x┋
+A └┛ alt
+S 17 ┋16 x┋
+A └┛ alt
+S 18 ┋17 x┋
+A └┛ alt
+S 19 ┋18 x┋
+A └┛ alt
+S 20 ┋19 x┋
+A └┛ alt
+S 21 ┋20 x┋
+A └┛ alt
+S 22 ┋21 x┋
+A └┛ alt
+S 23 ┋22 x┋
+A └┛ alt
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output_cursor.0 b/test/listview_output_cursor.0
new file mode 100644
index 0000000..1e38f6b
--- /dev/null
+++ b/test/listview_output_cursor.0
@@ -0,0 +1,70 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋+Hello x┋
+A └┛ alt
+S 2 ┋World! x┋
+A └┛ alt
+S 3 ┋ x┋
+A └┛ alt
+S 4 ┋ x┋
+A └┛ alt
+S 5 ┋ x┋
+A └┛ alt
+S 6 ┋ x┋
+A └┛ alt
+S 7 ┋ x┋
+A └┛ alt
+S 8 ┋ x┋
+A └┛ alt
+S 9 ┋ x┋
+A └┛ alt
+S 10 ┋ x┋
+A └┛ alt
+S 11 ┋ x┋
+A └┛ alt
+S 12 ┋ x┋
+A └┛ alt
+S 13 ┋ x┋
+A └┛ alt
+S 14 ┋ x┋
+A └┛ alt
+S 15 ┋ x┋
+A └┛ alt
+S 16 ┋ x┋
+A └┛ alt
+S 17 ┋ x┋
+A └┛ alt
+S 18 ┋ x┋
+A └┛ alt
+S 19 ┋ x┋
+A └┛ alt
+S 20 ┋ x┋
+A └┛ alt
+S 21 ┋ x┋
+A └┛ alt
+S 22 ┋ x┋
+A └┛ alt
+S 23 ┋ x┋
+A └┛ alt
+S 24 ┋ x ┋
+A ···············································································└ backspace
+A └┛ alt
+A ···············································································└ backspace
+CSI Set Replace mode
+S 24 ┋ ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output_cursor.1 b/test/listview_output_cursor.1
new file mode 100644
index 0000000..26594cc
--- /dev/null
+++ b/test/listview_output_cursor.1
@@ -0,0 +1,70 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋Hello x┋
+A └┛ alt
+S 2 ┋World! x┋
+A └┛ alt
+S 3 ┋2 x┋
+A └┛ alt
+S 4 ┋3 x┋
+A └┛ alt
+S 5 ┋4 x┋
+A └┛ alt
+S 6 ┋+5 x┋
+A └┛ alt
+S 7 ┋6 x┋
+A └┛ alt
+S 8 ┋7 x┋
+A └┛ alt
+S 9 ┋8 x┋
+A └┛ alt
+S 10 ┋9 x┋
+A └┛ alt
+S 11 ┋10 x┋
+A └┛ alt
+S 12 ┋11 x┋
+A └┛ alt
+S 13 ┋12 x┋
+A └┛ alt
+S 14 ┋13 x┋
+A └┛ alt
+S 15 ┋14 x┋
+A └┛ alt
+S 16 ┋15 x┋
+A └┛ alt
+S 17 ┋16 x┋
+A └┛ alt
+S 18 ┋17 x┋
+A └┛ alt
+S 19 ┋18 x┋
+A └┛ alt
+S 20 ┋19 x┋
+A └┛ alt
+S 21 ┋ x┋
+A └┛ alt
+S 22 ┋ x┋
+A └┛ alt
+S 23 ┋ x┋
+A └┛ alt
+S 24 ┋ x ┋
+A ···············································································└ backspace
+A └┛ alt
+A ···············································································└ backspace
+CSI Set Replace mode
+S 24 ┋ ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output_cursor.2 b/test/listview_output_cursor.2
new file mode 100644
index 0000000..0c8fd60
--- /dev/null
+++ b/test/listview_output_cursor.2
@@ -0,0 +1,70 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋Hello x┋
+A └┛ alt
+S 2 ┋World! x┋
+A └┛ alt
+S 3 ┋2 x┋
+A └┛ alt
+S 4 ┋+3 x┋
+A └┛ alt
+S 5 ┋4 x┋
+A └┛ alt
+S 6 ┋5 x┋
+A └┛ alt
+S 7 ┋6 x┋
+A └┛ alt
+S 8 ┋7 x┋
+A └┛ alt
+S 9 ┋8 x┋
+A └┛ alt
+S 10 ┋9 x┋
+A └┛ alt
+S 11 ┋10 x┋
+A └┛ alt
+S 12 ┋11 x┋
+A └┛ alt
+S 13 ┋12 x┋
+A └┛ alt
+S 14 ┋13 x┋
+A └┛ alt
+S 15 ┋14 x┋
+A └┛ alt
+S 16 ┋15 x┋
+A └┛ alt
+S 17 ┋16 x┋
+A └┛ alt
+S 18 ┋17 x┋
+A └┛ alt
+S 19 ┋18 x┋
+A └┛ alt
+S 20 ┋19 x┋
+A └┛ alt
+S 21 ┋ x┋
+A └┛ alt
+S 22 ┋ x┋
+A └┛ alt
+S 23 ┋ x┋
+A └┛ alt
+S 24 ┋ x ┋
+A ···············································································└ backspace
+A └┛ alt
+A ···············································································└ backspace
+CSI Set Replace mode
+S 24 ┋ ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output_cursor.3 b/test/listview_output_cursor.3
new file mode 100644
index 0000000..62ae234
--- /dev/null
+++ b/test/listview_output_cursor.3
@@ -0,0 +1,25 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋World! x┋
+A └┛ alt
+S 2 ┋2 x┋
+A └┛ alt
+S 3 ┋3 x┋
+A └┛ alt
+S 4 ┋+4 x┋
+A └┛ alt
+S 5 ┋5 x┋
+A └┛ alt
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output_cursor.4 b/test/listview_output_cursor.4
new file mode 100644
index 0000000..3980a0b
--- /dev/null
+++ b/test/listview_output_cursor.4
@@ -0,0 +1,25 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋Hello x┋
+A └┛ alt
+S 2 ┋World! x┋
+A └┛ alt
+S 3 ┋2 x┋
+A └┛ alt
+S 4 ┋+3 x┋
+A └┛ alt
+S 5 ┋4 x┋
+A └┛ alt
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output_cursor.5 b/test/listview_output_cursor.5
new file mode 100644
index 0000000..c7d432e
--- /dev/null
+++ b/test/listview_output_cursor.5
@@ -0,0 +1,35 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋17 x┋
+A └┛ alt
+S 2 ┋+18 x┋
+A └┛ alt
+S 3 ┋19 x┋
+A └┛ alt
+S 4 ┋20 x┋
+A └┛ alt
+S 5 ┋21 x┋
+A └┛ alt
+S 6 ┋22 x┋
+A └┛ alt
+S 7 ┋23 x┋
+A └┛ alt
+S 8 ┋24 x┋
+A └┛ alt
+S 9 ┋25 x┋
+A └┛ alt
+S 10 ┋26 x┋
+A └┛ alt
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/listview_output_cursor.6 b/test/listview_output_cursor.6
new file mode 100644
index 0000000..be86ee1
--- /dev/null
+++ b/test/listview_output_cursor.6
@@ -0,0 +1,35 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋8 x┋
+A └┛ alt
+S 2 ┋+9 x┋
+A └┛ alt
+S 3 ┋10 x┋
+A └┛ alt
+S 4 ┋11 x┋
+A └┛ alt
+S 5 ┋12 x┋
+A └┛ alt
+S 6 ┋13 x┋
+A └┛ alt
+S 7 ┋14 x┋
+A └┛ alt
+S 8 ┋15 x┋
+A └┛ alt
+S 9 ┋16 x┋
+A └┛ alt
+S 10 ┋17 x┋
+A └┛ alt
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/lnav_doctests.cc b/test/lnav_doctests.cc
new file mode 100644
index 0000000..423f807
--- /dev/null
+++ b/test/lnav_doctests.cc
@@ -0,0 +1,231 @@
+/**
+ * Copyright (c) 2017, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "byte_array.hh"
+#include "data_scanner.hh"
+#include "doctest/doctest.h"
+#include "lnav_config.hh"
+#include "lnav_util.hh"
+#include "relative_time.hh"
+#include "unique_path.hh"
+
+using namespace std;
+
+#if 0
+TEST_CASE("overwritten-logfile") {
+ string fname = "reload_test.0";
+
+ ofstream(fname) << "test 1\n";
+
+ logfile_open_options loo;
+ logfile lf(fname, loo);
+ auto build_result = lf.rebuild_index();
+ CHECK(build_result == logfile::RR_NEW_LINES);
+ CHECK(lf.size() == 1);
+
+ sleep(1);
+ ofstream(fname) << "test 2\n";
+ auto rebuild_result = lf.rebuild_index();
+ CHECK(rebuild_result == logfile::RR_NO_NEW_LINES);
+ CHECK(lf.is_closed());
+}
+#endif
+
+TEST_CASE("byte_array")
+{
+ using my_array_t = byte_array<8>;
+
+ my_array_t ba1;
+
+ memcpy(ba1.out(), "abcd1234", my_array_t::BYTE_COUNT);
+ CHECK(ba1.to_string() == "6162636431323334");
+ auto ba2 = ba1;
+ CHECK(ba1 == ba2);
+ CHECK_FALSE(ba1 != ba2);
+ CHECK_FALSE(ba1 < ba2);
+
+ my_array_t ba3;
+
+ memcpy(ba3.out(), "abcd1235", my_array_t::BYTE_COUNT);
+ CHECK(ba1 < ba3);
+ CHECK_FALSE(ba3 < ba1);
+
+ ba1.clear();
+ CHECK(ba1.to_string() == "0000000000000000");
+ CHECK(ba2.to_string() == "6162636431323334");
+
+ auto outbuf = auto_buffer::alloc(my_array_t::STRING_SIZE);
+ ba2.to_string(std::back_inserter(outbuf));
+ CHECK(std::string(outbuf.in(), outbuf.size()) == "6162636431323334");
+}
+
+TEST_CASE("ptime_fmt")
+{
+ const char* date_str = "2018-05-16 18:16:42";
+ struct exttm tm;
+ off_t off = 0;
+
+ bool rc
+ = ptime_fmt("%Y-%d-%m\t%H:%M:%S", &tm, date_str, off, strlen(date_str));
+ CHECK(!rc);
+ CHECK(off == 8);
+}
+
+TEST_CASE("rgb_color from string")
+{
+ string name = "SkyBlue1";
+ auto color = rgb_color::from_str(name).unwrap();
+ CHECK(color.rc_r == 135);
+ CHECK(color.rc_g == 215);
+ CHECK(color.rc_b == 255);
+}
+
+TEST_CASE("ptime_roundtrip")
+{
+ const char* fmts[] = {
+ "%Y-%m-%d %l:%M:%S %p",
+ "%Y-%m-%d %I:%M:%S %p",
+ };
+ time_t now = time(nullptr);
+
+ for (const auto* fmt : fmts) {
+ for (time_t sec = now; sec < (now + (24 * 60 * 60)); sec++) {
+ char ftime_result[128];
+ char strftime_result[128];
+ struct exttm etm;
+
+ memset(&etm, 0, sizeof(etm));
+ gmtime_r(&sec, &etm.et_tm);
+ etm.et_flags = ETF_YEAR_SET | ETF_MONTH_SET | ETF_DAY_SET;
+ size_t ftime_size
+ = ftime_fmt(ftime_result, sizeof(ftime_result), fmt, etm);
+ size_t strftime_size = strftime(
+ strftime_result, sizeof(strftime_result), fmt, &etm.et_tm);
+
+ CHECK(string(ftime_result, ftime_size)
+ == string(strftime_result, strftime_size));
+
+ struct exttm etm2;
+ off_t off = 0;
+
+ memset(&etm2, 0, sizeof(etm2));
+ bool rc = ptime_fmt(fmt, &etm2, ftime_result, off, ftime_size);
+ CHECK(rc);
+ CHECK(sec == tm2sec(&etm2.et_tm));
+ }
+ }
+}
+
+class my_path_source : public unique_path_source {
+public:
+ explicit my_path_source(ghc::filesystem::path p) : mps_path(std::move(p)) {}
+
+ ghc::filesystem::path get_path() const override { return this->mps_path; }
+
+ ghc::filesystem::path mps_path;
+};
+
+TEST_CASE("unique_path")
+{
+ unique_path_generator upg;
+
+ auto bar = make_shared<my_path_source>("/foo/bar");
+ auto bar_dupe = make_shared<my_path_source>("/foo/bar");
+ auto baz = make_shared<my_path_source>("/foo/baz");
+ auto baz2 = make_shared<my_path_source>("/foo2/bar");
+ auto log1 = make_shared<my_path_source>(
+ "/home/bob/downloads/machine1/var/log/syslog.log");
+ auto log2 = make_shared<my_path_source>(
+ "/home/bob/downloads/machine2/var/log/syslog.log");
+
+ upg.add_source(bar);
+ upg.add_source(bar_dupe);
+ upg.add_source(baz);
+ upg.add_source(baz2);
+ upg.add_source(log1);
+ upg.add_source(log2);
+
+ upg.generate();
+
+ CHECK(bar->get_unique_path() == "[foo]/bar");
+ CHECK(bar_dupe->get_unique_path() == "[foo]/bar");
+ CHECK(baz->get_unique_path() == "baz");
+ CHECK(baz2->get_unique_path() == "[foo2]/bar");
+ CHECK(log1->get_unique_path() == "[machine1]/syslog.log");
+ CHECK(log2->get_unique_path() == "[machine2]/syslog.log");
+}
+
+TEST_CASE("attr_line to json")
+{
+ attr_line_t al;
+
+ al.append("Hello, ").append(lnav::roles::symbol("World")).append("!");
+
+ auto json = lnav::to_json(al);
+ auto al2 = lnav::from_json<attr_line_t>(json).unwrap();
+ auto json2 = lnav::to_json(al2);
+
+ CHECK(json == json2);
+}
+
+TEST_CASE("user_message to json")
+{
+ auto um = lnav::console::user_message::error("testing")
+ .with_reason("because")
+ .with_snippet(lnav::console::snippet::from(
+ intern_string::lookup("hello.c"), "printf(")
+ .with_line(1))
+ .with_help("close it");
+
+ auto json = lnav::to_json(um);
+ auto um2 = lnav::from_json<lnav::console::user_message>(json).unwrap();
+ auto json2 = lnav::to_json(um2);
+
+ CHECK(json == json2);
+}
+
+TEST_CASE("data_scanner CSI")
+{
+ static const char INPUT[] = "\x1b[32mHello\x1b[0m";
+
+ data_scanner ds(string_fragment::from_const(INPUT));
+
+ auto tok_res = ds.tokenize2();
+ CHECK(tok_res->tr_token == DT_CSI);
+ CHECK(tok_res->to_string() == "\x1b[32m");
+ tok_res = ds.tokenize2();
+ CHECK(tok_res->tr_token == DT_SYMBOL);
+ CHECK(tok_res->to_string() == "Hello");
+ tok_res = ds.tokenize2();
+ CHECK(tok_res->tr_token == DT_CSI);
+ CHECK(tok_res->to_string() == "\x1b[0m");
+}
diff --git a/test/log-samples/sample-057d6c669632ef9d07b6adec605f6bdeae19af27.txt b/test/log-samples/sample-057d6c669632ef9d07b6adec605f6bdeae19af27.txt
new file mode 100644
index 0000000..c1d9b88
--- /dev/null
+++ b/test/log-samples/sample-057d6c669632ef9d07b6adec605f6bdeae19af27.txt
@@ -0,0 +1,13 @@
+ 2013-02-11 06:42:34,310:INFO:com.twisted:Site starting on 8099
+ key 29:29 ^
+ key 29:40 ^---------^ com.twisted
+pair 29:40 ^---------^ com.twisted
+ key 58:58 ^
+ num 58:62 ^--^ 8099
+pair 58:62 ^--^ 8099
+msg ::com.twisted:Site starting on 8099
+format ::#:Site starting on #
+{
+ "col_0": "com.twisted",
+ "col_1": 8099
+}
diff --git a/test/log-samples/sample-06aaa6f48a801f592558575d886864d6c3ab9ed4.txt b/test/log-samples/sample-06aaa6f48a801f592558575d886864d6c3ab9ed4.txt
new file mode 100644
index 0000000..aafb46d
--- /dev/null
+++ b/test/log-samples/sample-06aaa6f48a801f592558575d886864d6c3ab9ed4.txt
@@ -0,0 +1,40 @@
+ Apr 11 16:43:25 localhost smartd[2532]: Device: /dev/sda [SAT], VBOX HARDDISK, S/N:VBc8882b62-a0263a39, FW:1.0, 17.1 GB
+ key 40:46 ^----^ Device
+path 48:56 ^------^ /dev/sda
+wspc 56:57 ^
+ sym 58:61 ^-^ SAT
+ val 58:61 ^-^ SAT
+ grp 58:61 ^-^ SAT
+ val 48:61 ^-----------^ /dev/sda [SAT
+pair 40:61 ^-------------------^ Device: /dev/sda [SAT
+ key 64:64 ^
+ sym 64:68 ^--^ VBOX
+wspc 68:69 ^
+ sym 69:77 ^------^ HARDDISK
+ val 64:77 ^-----------^ VBOX HARDDISK
+pair 64:77 ^-----------^ VBOX HARDDISK
+ key 79:79 ^
+ sym 79:82 ^-^ S/N
+coln 82:83 ^ :
+ sym 83:102 ^-----------------^ VBc8882b62-a0263a39
+ val 79:102 ^---------------------^ S/N:VBc8882b62-a0263a39
+pair 79:102 ^---------------------^ S/N:VBc8882b62-a0263a39
+ key 104:106 ^^ FW
+ num 107:110 ^-^ 1.0
+ val 107:110 ^-^ 1.0
+pair 104:110 ^----^ FW:1.0
+ key 112:112 ^
+ num 112:116 ^--^ 17.1
+wspc 116:117 ^
+ sym 117:119 ^^ GB
+ val 112:119 ^-----^ 17.1 GB
+pair 112:119 ^-----^ 17.1 GB
+msg :Device: /dev/sda [SAT], VBOX HARDDISK, S/N:VBc8882b62-a0263a39, FW:1.0, 17.1 GB
+format :Device: #], #, #, FW:#, #
+{
+ "Device": "/dev/sda [SAT",
+ "col_0": "VBOX HARDDISK",
+ "col_1": "S/N:VBc8882b62-a0263a39",
+ "FW": 1.0,
+ "col_2": "17.1 GB"
+}
diff --git a/test/log-samples/sample-1aeb47c0a97d19bb7418f0172480e05e49c6e53e.txt b/test/log-samples/sample-1aeb47c0a97d19bb7418f0172480e05e49c6e53e.txt
new file mode 100644
index 0000000..289780a
--- /dev/null
+++ b/test/log-samples/sample-1aeb47c0a97d19bb7418f0172480e05e49c6e53e.txt
@@ -0,0 +1,17 @@
+ Apr 29 22:32:27 tstack-centos5 dhclient: bound to 10.1.10.62 -- renewal in 55327 seconds
+ key 50:50 ^
+ipv4 50:60 ^--------^ 10.1.10.62
+pair 50:60 ^--------^ 10.1.10.62
+ key 61:61 ^
+ sym 61:63 ^^ --
+pair 61:63 ^^ --
+ key 75:75 ^
+ num 75:80 ^---^ 55327
+pair 75:80 ^---^ 55327
+msg :bound to 10.1.10.62 -- renewal in 55327 seconds
+format :bound to # # renewal in # seconds
+{
+ "col_0": "10.1.10.62",
+ "col_1": "--",
+ "col_2": 55327
+}
diff --git a/test/log-samples/sample-27353a72ba4025448f261dcfa6ea16e474187795.txt b/test/log-samples/sample-27353a72ba4025448f261dcfa6ea16e474187795.txt
new file mode 100644
index 0000000..3a7277b
--- /dev/null
+++ b/test/log-samples/sample-27353a72ba4025448f261dcfa6ea16e474187795.txt
@@ -0,0 +1,4 @@
+ Jun 3 07:00:23 Tim-Stacks-iMac.local sudo[2326]: stack : TTY=ttys002 ; PWD=/ ; USER=root ; COMMAND=/bin/ls
+msg :
+format :
+null
diff --git a/test/log-samples/sample-3856ad0f551a04fde41a020158d6b33ef97c870a.txt b/test/log-samples/sample-3856ad0f551a04fde41a020158d6b33ef97c870a.txt
new file mode 100644
index 0000000..5d5d347
--- /dev/null
+++ b/test/log-samples/sample-3856ad0f551a04fde41a020158d6b33ef97c870a.txt
@@ -0,0 +1,17 @@
+ Apr 29 08:13:43 tstack-centos5 avahi-daemon[2467]: Leaving mDNS multicast group on interface eth0.IPv4 with address 10.1.10.62
+ key 59:59 ^
+ sym 59:63 ^--^ mDNS
+pair 59:63 ^--^ mDNS
+ key 93:93 ^
+ sym 93:102 ^-------^ eth0.IPv4
+pair 93:102 ^-------^ eth0.IPv4
+ key 116:116 ^
+ipv4 116:126 ^--------^ 10.1.10.62
+pair 116:126 ^--------^ 10.1.10.62
+msg :Leaving mDNS multicast group on interface eth0.IPv4 with address 10.1.10.62
+format :Leaving # multicast group on interface # with address #
+{
+ "col_0": "mDNS",
+ "col_1": "eth0.IPv4",
+ "col_2": "10.1.10.62"
+}
diff --git a/test/log-samples/sample-45364b3fd51af92a4ad8a309b5f4fd88.txt b/test/log-samples/sample-45364b3fd51af92a4ad8a309b5f4fd88.txt
new file mode 100644
index 0000000..e6799b7
--- /dev/null
+++ b/test/log-samples/sample-45364b3fd51af92a4ad8a309b5f4fd88.txt
@@ -0,0 +1,40 @@
+ Aug 20 06:36:07 Tim-Stacks-iMac kernel[0]: hibernate_teardown: wired_pages 518290, free_pages 5699523, active_pages 40010, inactive_pages 0, speculative_pages 0, cleaned_pages 0, compressor_pages 144
+ key 63:74 ^---------^ wired_pages
+ num 75:81 ^----^ 518290
+ val 75:81 ^----^ 518290
+pair 63:81 ^----------------^ wired_pages 518290
+ key 83:93 ^--------^ free_pages
+ num 94:101 ^-----^ 5699523
+ val 94:101 ^-----^ 5699523
+pair 83:101 ^----------------^ free_pages 5699523
+ key 103:115 ^----------^ active_pages
+ num 116:121 ^---^ 40010
+ val 116:121 ^---^ 40010
+pair 103:121 ^----------------^ active_pages 40010
+ key 123:137 ^------------^ inactive_pages
+ num 138:139 ^ 0
+ val 138:139 ^ 0
+pair 123:139 ^--------------^ inactive_pages 0
+ key 141:158 ^---------------^ speculative_pages
+ num 159:160 ^ 0
+ val 159:160 ^ 0
+pair 141:160 ^-----------------^ speculative_pages 0
+ key 162:175 ^-----------^ cleaned_pages
+ num 176:177 ^ 0
+ val 176:177 ^ 0
+pair 162:177 ^-------------^ cleaned_pages 0
+ key 179:195 ^--------------^ compressor_pages
+ num 196:199 ^-^ 144
+ val 196:199 ^-^ 144
+pair 179:199 ^------------------^ compressor_pages 144
+msg :hibernate_teardown: wired_pages 518290, free_pages 5699523, active_pages 40010, inactive_pages 0, speculative_pages 0, cleaned_pages 0, compressor_pages 144
+format :hibernate_teardown: wired_pages #, free_pages #, active_pages #, inactive_pages #, speculative_pages #, cleaned_pages #, compressor_pages #
+{
+ "wired_pages": 518290,
+ "free_pages": 5699523,
+ "active_pages": 40010,
+ "inactive_pages": 0,
+ "speculative_pages": 0,
+ "cleaned_pages": 0,
+ "compressor_pages": 144
+}
diff --git a/test/log-samples/sample-500c9e492e04f5f58862c8086ca301de0dd976ce.txt b/test/log-samples/sample-500c9e492e04f5f58862c8086ca301de0dd976ce.txt
new file mode 100644
index 0000000..ed7fa8e
--- /dev/null
+++ b/test/log-samples/sample-500c9e492e04f5f58862c8086ca301de0dd976ce.txt
@@ -0,0 +1,13 @@
+ Apr 29 08:13:43 tstack-centos5 avahi-daemon[2467]: New relevant interface eth0.IPv4 for mDNS
+ key 74:74 ^
+ sym 74:83 ^-------^ eth0.IPv4
+pair 74:83 ^-------^ eth0.IPv4
+ key 88:88 ^
+ sym 88:92 ^--^ mDNS
+pair 88:92 ^--^ mDNS
+msg :New relevant interface eth0.IPv4 for mDNS
+format :New relevant interface # for #
+{
+ "col_0": "eth0.IPv4",
+ "col_1": "mDNS"
+}
diff --git a/test/log-samples/sample-55ac97afae4b0650ccb62e2dbc8d89bb.txt b/test/log-samples/sample-55ac97afae4b0650ccb62e2dbc8d89bb.txt
new file mode 100644
index 0000000..baee2e9
--- /dev/null
+++ b/test/log-samples/sample-55ac97afae4b0650ccb62e2dbc8d89bb.txt
@@ -0,0 +1,15 @@
+ Aug 25 00:30:32 Tim-Stacks-iMac.local iTunes[558]: Entered:__thr_AMMuxedDeviceDisconnected, mux-device:509
+ key 51:58 ^-----^ Entered
+ sym 59:90 ^-----------------------------^ __thr_AMMuxedDeviceDisconnected
+ val 59:90 ^-----------------------------^ __thr_AMMuxedDeviceDisconnected
+pair 51:90 ^-------------------------------------^ Entered:__thr_AMMuxedDeviceDisconnected
+ key 92:102 ^--------^ mux-device
+ num 103:106 ^-^ 509
+ val 103:106 ^-^ 509
+pair 92:106 ^------------^ mux-device:509
+msg :Entered:__thr_AMMuxedDeviceDisconnected, mux-device:509
+format :Entered:#, mux-device:#
+{
+ "Entered": "__thr_AMMuxedDeviceDisconnected",
+ "mux-device": 509
+}
diff --git a/test/log-samples/sample-6049d4309f26eefb1a3406d937a9ba8a0df592a7.txt b/test/log-samples/sample-6049d4309f26eefb1a3406d937a9ba8a0df592a7.txt
new file mode 100644
index 0000000..f424a86
--- /dev/null
+++ b/test/log-samples/sample-6049d4309f26eefb1a3406d937a9ba8a0df592a7.txt
@@ -0,0 +1,13 @@
+ Apr 29 08:13:43 tstack-centos5 avahi-daemon[2467]: Withdrawing address record for 10.1.10.62 on eth0
+ key 82:82 ^
+ipv4 82:92 ^--------^ 10.1.10.62
+pair 82:92 ^--------^ 10.1.10.62
+ key 96:96 ^
+ sym 96:100 ^--^ eth0
+pair 96:100 ^--^ eth0
+msg :Withdrawing address record for 10.1.10.62 on eth0
+format :Withdrawing address record for # on #
+{
+ "col_0": "10.1.10.62",
+ "col_1": "eth0"
+}
diff --git a/test/log-samples/sample-62315d884afdc4155b35f905415c74bfcfd39fc2.txt b/test/log-samples/sample-62315d884afdc4155b35f905415c74bfcfd39fc2.txt
new file mode 100644
index 0000000..5a30cbc
--- /dev/null
+++ b/test/log-samples/sample-62315d884afdc4155b35f905415c74bfcfd39fc2.txt
@@ -0,0 +1,17 @@
+ Apr 29 08:13:43 tstack-centos5 avahi-daemon[2467]: Joining mDNS multicast group on interface eth0.IPv4 with address 10.1.10.62
+ key 59:59 ^
+ sym 59:63 ^--^ mDNS
+pair 59:63 ^--^ mDNS
+ key 93:93 ^
+ sym 93:102 ^-------^ eth0.IPv4
+pair 93:102 ^-------^ eth0.IPv4
+ key 116:116 ^
+ipv4 116:126 ^--------^ 10.1.10.62
+pair 116:126 ^--------^ 10.1.10.62
+msg :Joining mDNS multicast group on interface eth0.IPv4 with address 10.1.10.62
+format :Joining # multicast group on interface # with address #
+{
+ "col_0": "mDNS",
+ "col_1": "eth0.IPv4",
+ "col_2": "10.1.10.62"
+}
diff --git a/test/log-samples/sample-70c906b3c1a1cf03f15bde92ee78edfa6f9b7960.txt b/test/log-samples/sample-70c906b3c1a1cf03f15bde92ee78edfa6f9b7960.txt
new file mode 100644
index 0000000..9d80bf9
--- /dev/null
+++ b/test/log-samples/sample-70c906b3c1a1cf03f15bde92ee78edfa6f9b7960.txt
@@ -0,0 +1,4 @@
+ Jun 3 07:02:37 Tim-Stacks-iMac.local sudo[2717]: stack : TTY=ttys002 ; PWD=/ ; USER=root ; COMMAND=/usr/bin/env VAR1=foo ls
+msg :
+format :
+null
diff --git a/test/log-samples/sample-9cf7fbb3546c676c686fac0ed096d026f46c875f.txt b/test/log-samples/sample-9cf7fbb3546c676c686fac0ed096d026f46c875f.txt
new file mode 100644
index 0000000..d1bf5d4
--- /dev/null
+++ b/test/log-samples/sample-9cf7fbb3546c676c686fac0ed096d026f46c875f.txt
@@ -0,0 +1,13 @@
+ 2013-06-05T14:20:24 DEBUG cc2.main CC - 4672610200547811617359537811896212984085567168.114723023 Json_Reader - Doing prepare for resource name "Json_Reader", component "com.json.components.JSONReader"
+ key 144:144 ^
+quot 144:155 ^---------^ Json_Reader
+pair 144:155 ^---------^ Json_Reader
+ key 169:169 ^
+quot 169:199 ^----------------------------^ com.json.components.JSONReader
+pair 169:199 ^----------------------------^ com.json.components.JSONReader
+msg : Doing prepare for resource name "Json_Reader", component "com.json.components.JSONReader"
+format : Doing prepare for resource name #, component #
+{
+ "col_0": "Json_Reader",
+ "col_1": "com.json.components.JSONReader"
+}
diff --git a/test/log-samples/sample-a74570613c082c7fe283672031e18e54e8887ffb.txt b/test/log-samples/sample-a74570613c082c7fe283672031e18e54e8887ffb.txt
new file mode 100644
index 0000000..b2e0426
--- /dev/null
+++ b/test/log-samples/sample-a74570613c082c7fe283672031e18e54e8887ffb.txt
@@ -0,0 +1,13 @@
+ Apr 29 08:13:43 tstack-centos5 avahi-daemon[2467]: Interface eth0.IPv4 no longer relevant for mDNS
+ key 61:61 ^
+ sym 61:70 ^-------^ eth0.IPv4
+pair 61:70 ^-------^ eth0.IPv4
+ key 94:94 ^
+ sym 94:98 ^--^ mDNS
+pair 94:98 ^--^ mDNS
+msg :Interface eth0.IPv4 no longer relevant for mDNS
+format :Interface # no longer relevant for #
+{
+ "col_0": "eth0.IPv4",
+ "col_1": "mDNS"
+}
diff --git a/test/log-samples/sample-aca2878a2e50779c6697c0747ab1f60e4b368dcb.txt b/test/log-samples/sample-aca2878a2e50779c6697c0747ab1f60e4b368dcb.txt
new file mode 100644
index 0000000..f5c31f2
--- /dev/null
+++ b/test/log-samples/sample-aca2878a2e50779c6697c0747ab1f60e4b368dcb.txt
@@ -0,0 +1,15 @@
+ Apr 29 08:13:43 tstack-centos5 NET[13682]: /sbin/dhclient-script : updated /etc/resolv.conf
+ key 43:43 ^
+path 43:64 ^-------------------^ /sbin/dhclient-script
+ val 43:64 ^-------------------^ /sbin/dhclient-script
+pair 43:64 ^-------------------^ /sbin/dhclient-script
+ key 67:74 ^-----^ updated
+path 75:91 ^--------------^ /etc/resolv.conf
+ val 75:91 ^--------------^ /etc/resolv.conf
+pair 67:91 ^----------------------^ updated /etc/resolv.conf
+msg :/sbin/dhclient-script : updated /etc/resolv.conf
+format :# : updated #
+{
+ "col_0": "/sbin/dhclient-script",
+ "updated": "/etc/resolv.conf"
+}
diff --git a/test/log-samples/sample-ad31f12d2adabd07e3ddda3ad5b0dbf6b49c4c99.txt b/test/log-samples/sample-ad31f12d2adabd07e3ddda3ad5b0dbf6b49c4c99.txt
new file mode 100644
index 0000000..63c22cc
--- /dev/null
+++ b/test/log-samples/sample-ad31f12d2adabd07e3ddda3ad5b0dbf6b49c4c99.txt
@@ -0,0 +1,21 @@
+ Jun 2 00:34:32 Tim-Stacks-iMac kernel[0]: vmnet: VNetUserIf_Create: created userIf at 0xffffff802644f400
+ key 43:48 ^---^ vmnet
+quot 49:49 ^
+ val 49:49 ^
+pair 43:49 ^----^ vmnet:
+ key 50:67 ^---------------^ VNetUserIf_Create
+word 69:76 ^-----^ created
+wspc 76:77 ^
+ sym 77:83 ^----^ userIf
+wspc 83:84 ^
+word 84:86 ^^ at
+wspc 86:87 ^
+ hex 87:105 ^----------------^ 0xffffff802644f400
+ val 69:105 ^----------------------------------^ created userIf at 0xffffff802644f400
+pair 50:105 ^-----------------------------------------------------^ VNetUserIf_Create: created userIf at 0xffffff802644f400
+msg :vmnet: VNetUserIf_Create: created userIf at 0xffffff802644f400
+format :vmnet:# VNetUserIf_Create: #
+{
+ "vmnet": "",
+ "VNetUserIf_Create": "created userIf at 0xffffff802644f400"
+}
diff --git a/test/log-samples/sample-bc6f6cf689fa5455616b4d9fbe121a48d3c9de59.txt b/test/log-samples/sample-bc6f6cf689fa5455616b4d9fbe121a48d3c9de59.txt
new file mode 100644
index 0000000..32d86d0
--- /dev/null
+++ b/test/log-samples/sample-bc6f6cf689fa5455616b4d9fbe121a48d3c9de59.txt
@@ -0,0 +1,25 @@
+ Apr 29 08:13:42 tstack-centos5 dhclient: DHCPNAK from 10.1.10.1 (xid=0x4e17f141)
+ key 41:41 ^
+ sym 41:48 ^-----^ DHCPNAK
+pair 41:48 ^-----^ DHCPNAK
+ key 54:54 ^
+ipv4 54:63 ^-------^ 10.1.10.1
+pair 54:63 ^-------^ 10.1.10.1
+ key 65:65 ^
+ key 65:68 ^-^ xid
+ hex 69:79 ^--------^ 0x4e17f141
+ val 69:79 ^--------^ 0x4e17f141
+pair 65:79 ^------------^ xid=0x4e17f141
+ grp 65:79 ^------------^ xid=0x4e17f141
+pair 65:79 ^------------^ xid=0x4e17f141
+msg :DHCPNAK from 10.1.10.1 (xid=0x4e17f141)
+format :# from # (#)
+{
+ "col_0": "DHCPNAK",
+ "col_1": "10.1.10.1",
+ "col_2": [
+ {
+ "xid": "0x4e17f141"
+ }
+ ]
+}
diff --git a/test/log-samples/sample-c15acd32844669d23d0cbc88ec548129ed2c592e.txt b/test/log-samples/sample-c15acd32844669d23d0cbc88ec548129ed2c592e.txt
new file mode 100644
index 0000000..a68d490
--- /dev/null
+++ b/test/log-samples/sample-c15acd32844669d23d0cbc88ec548129ed2c592e.txt
@@ -0,0 +1,87 @@
+ Jul 14 14:31:06 linjenkins3 kernel: [31809412.513897] [UFW BLOCK] IN=eth0 OUT= MAC=40:40:2e:9a:ad:92:c4:71:fe:f1:b9:7f:08:00 SRC=69.60.116.202 DST=173.203.237.224 LEN=44 TOS=0x00 PREC=0x00 TTL=29 ID=15852 PROTO=TCP SPT=43998 DPT=3389 WINDOW=3072 RES=0x00 SYN URGP=0
+ key 37:68 ^-----------------------------^ 31809412.513897] [UFW BLOCK] IN
+ sym 69:73 ^--^ eth0
+ val 69:73 ^--^ eth0
+pair 37:73 ^----------------------------------^ 31809412.513897] [UFW BLOCK] IN=eth0
+ key 74:77 ^-^ OUT
+quot 78:78 ^
+ val 78:78 ^
+pair 74:78 ^--^ OUT=
+ key 79:82 ^-^ MAC
+hexd 83:124 ^---------------------------------------^ 40:40:2e:9a:ad:92:c4:71:fe:f1:b9:7f:08:00
+ val 83:124 ^---------------------------------------^ 40:40:2e:9a:ad:92:c4:71:fe:f1:b9:7f:08:00
+pair 79:124 ^-------------------------------------------^ MAC=40:40:2e:9a:ad:92:c4:71:fe:f1:b9:7f:08:00
+ key 125:128 ^-^ SRC
+ipv4 129:142 ^-----------^ 69.60.116.202
+ val 129:142 ^-----------^ 69.60.116.202
+pair 125:142 ^---------------^ SRC=69.60.116.202
+ key 143:146 ^-^ DST
+ipv4 147:162 ^-------------^ 173.203.237.224
+ val 147:162 ^-------------^ 173.203.237.224
+pair 143:162 ^-----------------^ DST=173.203.237.224
+ key 163:166 ^-^ LEN
+ num 167:169 ^^ 44
+ val 167:169 ^^ 44
+pair 163:169 ^----^ LEN=44
+ key 170:173 ^-^ TOS
+ hex 174:178 ^--^ 0x00
+ val 174:178 ^--^ 0x00
+pair 170:178 ^------^ TOS=0x00
+ key 179:183 ^--^ PREC
+ hex 184:188 ^--^ 0x00
+ val 184:188 ^--^ 0x00
+pair 179:188 ^-------^ PREC=0x00
+ key 189:192 ^-^ TTL
+ num 193:195 ^^ 29
+ val 193:195 ^^ 29
+pair 189:195 ^----^ TTL=29
+ key 196:198 ^^ ID
+ num 199:204 ^---^ 15852
+ val 199:204 ^---^ 15852
+pair 196:204 ^------^ ID=15852
+ key 205:210 ^---^ PROTO
+ sym 211:214 ^-^ TCP
+ val 211:214 ^-^ TCP
+pair 205:214 ^-------^ PROTO=TCP
+ key 215:218 ^-^ SPT
+ num 219:224 ^---^ 43998
+ val 219:224 ^---^ 43998
+pair 215:224 ^-------^ SPT=43998
+ key 225:228 ^-^ DPT
+ num 229:233 ^--^ 3389
+ val 229:233 ^--^ 3389
+pair 225:233 ^------^ DPT=3389
+ key 234:240 ^----^ WINDOW
+ num 241:245 ^--^ 3072
+ val 241:245 ^--^ 3072
+pair 234:245 ^---------^ WINDOW=3072
+ key 246:249 ^-^ RES
+ hex 250:254 ^--^ 0x00
+wspc 254:255 ^
+ sym 255:258 ^-^ SYN
+ val 250:258 ^------^ 0x00 SYN
+pair 246:258 ^----------^ RES=0x00 SYN
+ key 259:263 ^--^ URGP
+ num 264:265 ^ 0
+ val 264:265 ^ 0
+pair 259:265 ^----^ URGP=0
+msg :[31809412.513897] [UFW BLOCK] IN=eth0 OUT= MAC=40:40:2e:9a:ad:92:c4:71:fe:f1:b9:7f:08:00 SRC=69.60.116.202 DST=173.203.237.224 LEN=44 TOS=0x00 PREC=0x00 TTL=29 ID=15852 PROTO=TCP SPT=43998 DPT=3389 WINDOW=3072 RES=0x00 SYN URGP=0
+format :[31809412.513897] [UFW BLOCK] IN=# OUT=# MAC=# SRC=# DST=# LEN=# TOS=# PREC=# TTL=# ID=# PROTO=# SPT=# DPT=# WINDOW=# RES=# URGP=#
+{
+ "31809412.513897] [UFW BLOCK] IN": "eth0",
+ "OUT": "",
+ "MAC": "40:40:2e:9a:ad:92:c4:71:fe:f1:b9:7f:08:00",
+ "SRC": "69.60.116.202",
+ "DST": "173.203.237.224",
+ "LEN": 44,
+ "TOS": "0x00",
+ "PREC": "0x00",
+ "TTL": 29,
+ "ID": 15852,
+ "PROTO": "TCP",
+ "SPT": 43998,
+ "DPT": 3389,
+ "WINDOW": 3072,
+ "RES": "0x00 SYN",
+ "URGP": 0
+}
diff --git a/test/log-samples/sample-c23f22c1b932b904203e018f78dead95fb89b15d.txt b/test/log-samples/sample-c23f22c1b932b904203e018f78dead95fb89b15d.txt
new file mode 100644
index 0000000..376752b
--- /dev/null
+++ b/test/log-samples/sample-c23f22c1b932b904203e018f78dead95fb89b15d.txt
@@ -0,0 +1,37 @@
+ Apr 29 08:13:42 tstack-centos5 dhclient: DHCPDISCOVER on eth0 to 255.255.255.255 port 67 interval 5 (xid=0xd16b79d)
+ key 41:41 ^
+ sym 41:53 ^----------^ DHCPDISCOVER
+pair 41:53 ^----------^ DHCPDISCOVER
+ key 57:57 ^
+ sym 57:61 ^--^ eth0
+pair 57:61 ^--^ eth0
+ key 65:65 ^
+ipv4 65:80 ^-------------^ 255.255.255.255
+pair 65:80 ^-------------^ 255.255.255.255
+ key 86:86 ^
+ num 86:88 ^^ 67
+pair 86:88 ^^ 67
+ key 98:98 ^
+ num 98:99 ^ 5
+pair 98:99 ^ 5
+ key 101:101 ^
+ key 101:104 ^-^ xid
+ hex 105:114 ^-------^ 0xd16b79d
+ val 105:114 ^-------^ 0xd16b79d
+pair 101:114 ^-----------^ xid=0xd16b79d
+ grp 101:114 ^-----------^ xid=0xd16b79d
+pair 101:114 ^-----------^ xid=0xd16b79d
+msg :DHCPDISCOVER on eth0 to 255.255.255.255 port 67 interval 5 (xid=0xd16b79d)
+format :# on # to # port # interval # (#)
+{
+ "col_0": "DHCPDISCOVER",
+ "col_1": "eth0",
+ "col_2": "255.255.255.255",
+ "col_3": 67,
+ "col_4": 5,
+ "col_5": [
+ {
+ "xid": "0xd16b79d"
+ }
+ ]
+}
diff --git a/test/log-samples/sample-d0d6b3fc6766caac5ac3fac4a3754ceaab785eb8.txt b/test/log-samples/sample-d0d6b3fc6766caac5ac3fac4a3754ceaab785eb8.txt
new file mode 100644
index 0000000..b7aad42
--- /dev/null
+++ b/test/log-samples/sample-d0d6b3fc6766caac5ac3fac4a3754ceaab785eb8.txt
@@ -0,0 +1,33 @@
+ Apr 29 22:32:26 tstack-centos5 dhclient: DHCPREQUEST on eth0 to 10.1.10.1 port 67 (xid=0xd16b79d)
+ key 41:41 ^
+ sym 41:52 ^---------^ DHCPREQUEST
+pair 41:52 ^---------^ DHCPREQUEST
+ key 56:56 ^
+ sym 56:60 ^--^ eth0
+pair 56:60 ^--^ eth0
+ key 64:64 ^
+ipv4 64:73 ^-------^ 10.1.10.1
+pair 64:73 ^-------^ 10.1.10.1
+ key 79:79 ^
+ num 79:81 ^^ 67
+pair 79:81 ^^ 67
+ key 83:83 ^
+ key 83:86 ^-^ xid
+ hex 87:96 ^-------^ 0xd16b79d
+ val 87:96 ^-------^ 0xd16b79d
+pair 83:96 ^-----------^ xid=0xd16b79d
+ grp 83:96 ^-----------^ xid=0xd16b79d
+pair 83:96 ^-----------^ xid=0xd16b79d
+msg :DHCPREQUEST on eth0 to 10.1.10.1 port 67 (xid=0xd16b79d)
+format :# on # to # port # (#)
+{
+ "col_0": "DHCPREQUEST",
+ "col_1": "eth0",
+ "col_2": "10.1.10.1",
+ "col_3": 67,
+ "col_4": [
+ {
+ "xid": "0xd16b79d"
+ }
+ ]
+}
diff --git a/test/log-samples/sample-d4a0aedc8350f64b22403eeef4eca71fbf749d2b.txt b/test/log-samples/sample-d4a0aedc8350f64b22403eeef4eca71fbf749d2b.txt
new file mode 100644
index 0000000..91a456a
--- /dev/null
+++ b/test/log-samples/sample-d4a0aedc8350f64b22403eeef4eca71fbf749d2b.txt
@@ -0,0 +1,9 @@
+ Apr 29 23:02:45 tstack-centos5 avahi-daemon[2467]: Invalid response packet from host fe80::22c9:d0ff:fe15:1b7c
+ key 85:85 ^
+ipv6 85:110 ^-----------------------^ fe80::22c9:d0ff:fe15:1b7c
+pair 85:110 ^-----------------------^ fe80::22c9:d0ff:fe15:1b7c
+msg :Invalid response packet from host fe80::22c9:d0ff:fe15:1b7c
+format :Invalid response packet from host #
+{
+ "col_0": "fe80::22c9:d0ff:fe15:1b7c"
+}
diff --git a/test/log-samples/sample-d714b5e8cd354321f376ed1c0a70ec9a2f58076d.txt b/test/log-samples/sample-d714b5e8cd354321f376ed1c0a70ec9a2f58076d.txt
new file mode 100644
index 0000000..d7367ab
--- /dev/null
+++ b/test/log-samples/sample-d714b5e8cd354321f376ed1c0a70ec9a2f58076d.txt
@@ -0,0 +1,9 @@
+ Apr 29 23:02:45 tstack-centos5 avahi-daemon[2467]: Invalid response packet from host 10.1.10.10
+ key 85:85 ^
+ipv4 85:95 ^--------^ 10.1.10.10
+pair 85:95 ^--------^ 10.1.10.10
+msg :Invalid response packet from host 10.1.10.10
+format :Invalid response packet from host #
+{
+ "col_0": "10.1.10.10"
+}
diff --git a/test/log-samples/sample-dd7d406352ec6a11d966b6f015a9482b060f2b29.txt b/test/log-samples/sample-dd7d406352ec6a11d966b6f015a9482b060f2b29.txt
new file mode 100644
index 0000000..bcf3908
--- /dev/null
+++ b/test/log-samples/sample-dd7d406352ec6a11d966b6f015a9482b060f2b29.txt
@@ -0,0 +1,23 @@
+ 2013-02-11 06:42:34,311:INFO:com.twisted:Starting factory <twisted.web.server.Site instance at 0x1de9290>
+ key 29:29 ^
+ key 29:40 ^---------^ com.twisted
+pair 29:40 ^---------^ com.twisted
+ key 59:59 ^
+ sym 59:82 ^---------------------^ twisted.web.server.Site
+wspc 82:83 ^
+word 83:91 ^------^ instance
+wspc 91:92 ^
+word 92:94 ^^ at
+wspc 94:95 ^
+ hex 95:104 ^-------^ 0x1de9290
+ val 59:104 ^-------------------------------------------^ twisted.web.server.Site instance at 0x1de9290
+ grp 59:104 ^-------------------------------------------^ twisted.web.server.Site instance at 0x1de9290
+pair 59:104 ^-------------------------------------------^ twisted.web.server.Site instance at 0x1de9290
+msg ::com.twisted:Starting factory <twisted.web.server.Site instance at 0x1de9290>
+format ::#:Starting factory <#>
+{
+ "col_0": "com.twisted",
+ "col_1": [
+ "twisted.web.server.Site instance at 0x1de9290"
+ ]
+}
diff --git a/test/log-samples/sample-e779d1771e34f5203ae73e85802e78002be63db6.txt b/test/log-samples/sample-e779d1771e34f5203ae73e85802e78002be63db6.txt
new file mode 100644
index 0000000..d527217
--- /dev/null
+++ b/test/log-samples/sample-e779d1771e34f5203ae73e85802e78002be63db6.txt
@@ -0,0 +1,25 @@
+ Apr 29 22:32:27 tstack-centos5 dhclient: DHCPACK from 10.1.10.1 (xid=0xd16b79d)
+ key 41:41 ^
+ sym 41:48 ^-----^ DHCPACK
+pair 41:48 ^-----^ DHCPACK
+ key 54:54 ^
+ipv4 54:63 ^-------^ 10.1.10.1
+pair 54:63 ^-------^ 10.1.10.1
+ key 65:65 ^
+ key 65:68 ^-^ xid
+ hex 69:78 ^-------^ 0xd16b79d
+ val 69:78 ^-------^ 0xd16b79d
+pair 65:78 ^-----------^ xid=0xd16b79d
+ grp 65:78 ^-----------^ xid=0xd16b79d
+pair 65:78 ^-----------^ xid=0xd16b79d
+msg :DHCPACK from 10.1.10.1 (xid=0xd16b79d)
+format :# from # (#)
+{
+ "col_0": "DHCPACK",
+ "col_1": "10.1.10.1",
+ "col_2": [
+ {
+ "xid": "0xd16b79d"
+ }
+ ]
+}
diff --git a/test/log-samples/sample-f5afbee90a8c054061c4e9ffe673293cce7761de.txt b/test/log-samples/sample-f5afbee90a8c054061c4e9ffe673293cce7761de.txt
new file mode 100644
index 0000000..1eafdc7
--- /dev/null
+++ b/test/log-samples/sample-f5afbee90a8c054061c4e9ffe673293cce7761de.txt
@@ -0,0 +1,13 @@
+ Apr 29 08:13:43 tstack-centos5 dhclient: DHCPOFFER from 10.1.10.1
+ key 41:41 ^
+ sym 41:50 ^-------^ DHCPOFFER
+pair 41:50 ^-------^ DHCPOFFER
+ key 56:56 ^
+ipv4 56:65 ^-------^ 10.1.10.1
+pair 56:65 ^-------^ 10.1.10.1
+msg :DHCPOFFER from 10.1.10.1
+format :# from #
+{
+ "col_0": "DHCPOFFER",
+ "col_1": "10.1.10.1"
+}
diff --git a/test/log-samples/sample-fc8923633e57bacd641d80dde3ff878212230552.txt b/test/log-samples/sample-fc8923633e57bacd641d80dde3ff878212230552.txt
new file mode 100644
index 0000000..f0f3afe
--- /dev/null
+++ b/test/log-samples/sample-fc8923633e57bacd641d80dde3ff878212230552.txt
@@ -0,0 +1,13 @@
+ Apr 29 08:13:43 tstack-centos5 avahi-daemon[2467]: Registering new address record for 10.1.10.62 on eth0
+ key 86:86 ^
+ipv4 86:96 ^--------^ 10.1.10.62
+pair 86:96 ^--------^ 10.1.10.62
+ key 100:100 ^
+ sym 100:104 ^--^ eth0
+pair 100:104 ^--^ eth0
+msg :Registering new address record for 10.1.10.62 on eth0
+format :Registering new address record for # on #
+{
+ "col_0": "10.1.10.62",
+ "col_1": "eth0"
+}
diff --git a/test/log.clog b/test/log.clog
new file mode 100644
index 0000000..14e4ed9
--- /dev/null
+++ b/test/log.clog
@@ -0,0 +1,2 @@
+{"@timestamp":"2016-08-03T12:06:31.009-0500","@version":1,"message":";Exception initializing page context;","logger_name":"org.apache.jasper.runtime.JspFactoryImpl","thread_name":"http-bio-0.0.0.0-8081-exec-198","level":"ERROR","level_value":40000,"stack_trace":"java.lang.NoClassDefFoundError: javax/el/StaticFieldELResolver\n\tat org.apache.jasper.runtime.JspFactoryImpl.internalGetPageContext(JspFactoryImpl.java:172)\n\tat org.apache.jasper.runtime.JspFactoryImpl.getPageContext(JspFactoryImpl.java:123)\n\tat org.apache.jsp.errors._404_002dnot_002dfound_jsp._jspService(_404_002dnot_002dfound_jsp.java:38)\n\tat org.apache.jasper.runtime.HttpJspBase.service(HttpJspBase.java:111)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:731)\n\tat org.apache.jasper.servlet.JspServletWrapper.service(JspServletWrapper.java:411)\n\tat org.apache.jasper.servlet.JspServlet.serviceJspFile(JspServlet.java:473)\n\tat org.apache.jasper.servlet.JspServlet.service(JspServlet.java:377)\n\tat javax.servlet.http.HttpServlet.service(HttpServlet.java:731)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)\n\tat collective.config.startup.DamFilter.doFilter(DamFilter.java:270)\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)\n\tat org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:748)\n\tat org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:488)\n\tat org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:411)\n\tat org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:338)\n\tat org.apache.catalina.core.StandardHostValve.custom(StandardHostValve.java:476)\n\tat org.apache.catalina.core.StandardHostValve.status(StandardHostValve.java:345)\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:210)\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:103)\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)\n\tat org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:957)\n\tat org.apache.catalina.valves.RemoteIpValve.invoke(RemoteIpValve.java:683)\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:423)\n\tat org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1079)\n\tat org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:620)\n\tat org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:316)\n\tat java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)\n\tat java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\n\tat java.lang.Thread.run(Thread.java:744)\nCaused by: java.lang.ClassNotFoundException: javax.el.StaticFieldELResolver\n\tat org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1720)\n\tat org.apache.catalina.loader.WebappClassLoader.loadClass(WebappClassLoader.java:1571)\n\t... 33 common frames omitted\n","customer":"foobaz"}
+{"@timestamp":"2016-08-03T12:06:31.009-0500","@version":1,"message":";Exception initializing page context;","logger_name":"org.apache.jasper.runtime.JspFactoryImpl","thread_name":"http-bio-0.0.0.0-8081-exec-198","level":"ERROR","level_value":40000,"customer":"foobaz"}
diff --git a/test/logfile_access_log.0 b/test/logfile_access_log.0
new file mode 100644
index 0000000..a90f29f
--- /dev/null
+++ b/test/logfile_access_log.0
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/logfile_access_log.1 b/test/logfile_access_log.1
new file mode 100644
index 0000000..ad2b37e
--- /dev/null
+++ b/test/logfile_access_log.1
@@ -0,0 +1 @@
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
diff --git a/test/logfile_ansi.0 b/test/logfile_ansi.0
new file mode 100644
index 0000000..d9c9499
--- /dev/null
+++ b/test/logfile_ansi.0
@@ -0,0 +1 @@
+2022-06-22T10:20:33 Example foo
diff --git a/test/logfile_ansi.1 b/test/logfile_ansi.1
new file mode 100644
index 0000000..51fc1bf
--- /dev/null
+++ b/test/logfile_ansi.1
@@ -0,0 +1,10 @@
+Sep 19 09:24:04 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: tid:1d1f - Mux ID not found in mapping dictionary
+Sep 19 09:24:04 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: tid:1d1f - Can't handle disconnect with invalid ecid
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: Entered:_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: Entered:_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: Entered:__thr_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: Entered:__thr_AMMuxedDeviceDisconnected, mux-device:1003
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: tid:191f - Mux ID not found in mapping dictionary
+Sep 19 09:24:20 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: tid:1d1f - Mux ID not found in mapping dictionary
+Sep 19 09:24:20 Tims-MacBook-Air MobileDeviceUpdater[17530]: tid:191f - Can't handle disconnect with invalid ecid
+Sep 19 09:24:20 Tims-MacBook-Air AMPDeviceDiscoveryAgent[17600]: tid:1d1f - Can't handle disconnect with invalid ecid
diff --git a/test/logfile_bad_access_log.0 b/test/logfile_bad_access_log.0
new file mode 100644
index 0000000..d48df45
--- /dev/null
+++ b/test/logfile_bad_access_log.0
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/logfile_bad_syslog.0 b/test/logfile_bad_syslog.0
new file mode 100644
index 0000000..af65a10
--- /dev/null
+++ b/test/logfile_bad_syslog.0
@@ -0,0 +1,4 @@
+Nov 3 09:23:38 veridian automount[7998]: lookup(file): lookup for foobar failed
+Nov 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt
+Nov 3 09:23:38 veridian lookup for opt failed
+Nov 3 09:47:02 veridian sudo: timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
diff --git a/test/logfile_block.1 b/test/logfile_block.1
new file mode 100644
index 0000000..681ec73
--- /dev/null
+++ b/test/logfile_block.1
@@ -0,0 +1,4 @@
+Wed May 19 08:00:01 EST 2021 line 1
+/abc/def
+Wed May 19 08:00:03 EST 2021 line 3
+/ghi/jkl
diff --git a/test/logfile_block.2 b/test/logfile_block.2
new file mode 100644
index 0000000..3d21dd3
--- /dev/null
+++ b/test/logfile_block.2
@@ -0,0 +1,2 @@
+Wed May 19 12:00:02 UTC 2021 line 2
+Wed May 19 12:00:04 UTC 2021 line 4
diff --git a/test/logfile_blued.0 b/test/logfile_blued.0
new file mode 100644
index 0000000..1422615
--- /dev/null
+++ b/test/logfile_blued.0
@@ -0,0 +1,6 @@
+Apr 4 20:02:32 Tim-Stacks-iMac.local blued[59]: Release the WiFi lock
+Apr 4 20:02:32 Tim-Stacks-iMac.local blued[59]: Acquiring the wifi lock
+Apr 4 20:02:32 Tim-Stacks-iMac.local blued[59]: Acquired the wifi lock
+Apr 4 20:02:42 Tim-Stacks-iMac.local blued[59]: Release the WiFi lock
+Apr 4 20:02:42 Tim-Stacks-iMac.local blued[59]: Acquiring the wifi lock
+Apr 4 20:02:42 Tim-Stacks-iMac.local blued[59]: Acquired the wifi lock
diff --git a/test/logfile_bro_conn.log.0 b/test/logfile_bro_conn.log.0
new file mode 100644
index 0000000..305e368
--- /dev/null
+++ b/test/logfile_bro_conn.log.0
@@ -0,0 +1,101 @@
+#separator \x09
+#set_separator ,
+#empty_field (empty)
+#unset_field -
+#path conn
+#open 2017-04-16-21-36-10
+#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto service duration orig_bytes resp_bytes conn_state local_orig local_resp missed_bytes history orig_pkts orig_ip_bytes resp_pkts resp_ip_bytes tunnel_parents
+#types time string addr port addr port enum string interval count count string bool bool count string count count count count set[string]
+1320279554.496300 Cg9xqq3JAcZusspA86 192.168.2.76 52025 208.85.42.28 80 tcp - 2.125850 0 1092421 SF - - 0 ^dAfFa 400 20800 756 1131733 (empty)
+1320279567.181431 CdysLK1XpcrXOpVDuh 192.168.2.76 52034 174.129.249.33 80 tcp http 0.082899 389 1495 SF - - 0 ShADdfFa 5 613 4 1667 (empty)
+1320279567.452735 C6nSoj1Qco9PGyslz6 192.168.2.76 52035 184.72.234.3 80 tcp http 2.561940 905 731 SF - - 0 ShADadfF 9 1289 8 1063 (empty)
+1320279567.181050 CtgxRAqDLvrRUQdqe 192.168.2.76 52033 184.72.234.3 80 tcp http 3.345539 1856 1445 SF - - 0 ShADadfF 15 2480 13 1969 (empty)
+1320279572.537165 Cg66JO6sKx3fvUkQa 192.168.2.76 52014 132.235.215.117 80 tcp - 0.005881 0 0 SF - - 0 FfA 2 104 1 52 (empty)
+1320279578.886650 CIJIDL1ULo4HpT24Gl 192.168.2.76 52052 63.241.108.124 80 tcp http 0.498720 1566 2543 SF - - 0 ShADadfF 6 1830 5 2747 (empty)
+1320279577.453637 CEh6Ka2HInkNSH01L2 192.168.2.76 52044 216.34.181.48 80 tcp http 5.077548 596 576 SF - - 0 ShADadfF 6 920 5 848 (empty)
+1320279581.284239 CSvRlm1gGNFXUOrtRj 192.168.2.76 52059 207.171.163.23 80 tcp - 5.056486 0 0 SF - - 0 ShAFf 4 184 2 92 (empty)
+1320279577.507914 CjPGiy13ncXKxU765j 192.168.2.76 52045 216.34.181.45 80 tcp http 11.654832 2603 181933 SF - - 0 ShADadfF 80 6775 134 188913 (empty)
+1320279590.558878 CKeb0i4BZy3XEHQGvb 192.168.2.76 52077 74.125.225.78 80 tcp - 5.048744 0 0 SF - - 0 ShAFf 4 220 2 112 (empty)
+1320279601.552309 CK957ERTz8lBycly4 192.168.2.76 52085 199.59.148.201 80 tcp http 0.237418 883 1071 SF - - 0 ShADadfF 6 1207 5 1339 (empty)
+1320279600.826685 CaPClb1Bf0RrRGtyWi 192.168.2.76 52083 192.150.187.43 80 tcp http 5.233472 442 31353 SF - - 0 ShADadfF 20 1494 26 32713 (empty)
+1320279600.826441 CmWpSw3VtjiAceBCwf 192.168.2.76 52081 192.150.187.43 80 tcp http 5.233763 446 24258 SF - - 0 ShADadfF 14 1186 21 25358 (empty)
+1320279600.826004 CBeaXe4Iyj1gXd2Iq 192.168.2.76 52080 192.150.187.43 80 tcp http 5.404390 886 16577 SF - - 0 ShADadfF 14 1626 17 17469 (empty)
+1320279600.825492 Cd8s2R3OGDgkhnvSu9 192.168.2.76 52079 192.150.187.43 80 tcp http 5.496459 1309 17849 SF - - 0 ShADadfF 16 2153 18 18793 (empty)
+1320279600.826607 CX1GjC4vn52UY1uDv6 192.168.2.76 52082 192.150.187.43 80 tcp http 5.515177 1746 14412 SF - - 0 ShADadfF 14 2486 16 15252 (empty)
+1320279600.581672 CbQAWi3GX2bCmX5L56 192.168.2.76 52078 192.150.187.43 80 tcp http 5.825503 1599 80801 SF - - 0 ShADadfF 37 3535 63 84085 (empty)
+1320279607.998777 CKskol4qPFKjkV6273 192.168.2.76 52022 74.125.225.68 80 tcp - 0.021505 0 0 SF - - 0 FfA 2 104 1 52 (empty)
+1320279607.998577 CtBtCj3jZ4UVo657Dc 192.168.2.76 52023 209.85.145.101 80 tcp - 0.031533 0 0 SF - - 0 FfA 2 104 1 52 (empty)
+1320279611.527848 CurHpb1TGZOktTRNP1 192.168.2.76 52092 199.59.148.201 80 tcp http 0.349795 902 1070 SF - - 0 ShADadfF 6 1226 5 1338 (empty)
+1320279612.495344 CuUKOQ1R3CqKBgeTdf 192.168.2.76 52093 199.59.148.201 80 tcp http 0.279806 907 1070 SF - - 0 ShADadfF 6 1231 5 1338 (empty)
+1320279613.968096 C3xkHgJnzZszVSTpi 192.168.2.76 52094 199.59.148.201 80 tcp http 0.486591 902 1070 SF - - 0 ShADadfF 6 1226 5 1338 (empty)
+1320279611.171273 CINVx040XRWPWdQIOd 192.168.2.76 52091 192.150.187.43 80 tcp - 5.081864 0 0 SF - - 0 ShAFf 5 272 3 172 (empty)
+1320279601.552622 C3TZMB4CrUwYfkGJy1 192.168.2.76 52086 199.59.148.20 80 tcp http 15.200059 4078 9556 SF - - 0 ShADadfF 12 4714 13 10240 (empty)
+1320279610.744212 CO5QKYQkcSdxQFA35 192.168.2.76 52090 192.150.187.43 80 tcp http 6.499438 1669 37688 SF - - 0 ShADadFf 26 3033 31 39308 (empty)
+1320279616.742259 CMrjgF2XLmRh9C9TR4 192.168.2.76 52095 208.85.41.42 80 tcp http 0.604819 546 59445 SF - - 0 ShADadfF 29 2066 45 61793 (empty)
+1320279630.486420 CD69521bDXIAb4IkW 192.168.2.76 52097 199.59.148.201 80 tcp http 0.166288 903 1070 SF - - 0 ShADadfF 6 1227 5 1338 (empty)
+1320279630.021607 C2vQ8sVgyADHjtEda 192.168.2.76 52096 192.150.187.43 80 tcp http 5.199366 421 15397 SF - - 0 ShADadfF 13 1109 15 16185 (empty)
+1320279637.215536 CmxyBl2c8XAMTuHEk4 192.168.2.76 52100 199.59.148.201 80 tcp http 0.264911 905 1068 SF - - 0 ShADadFf 7 1281 5 1336 (empty)
+1320279577.687091 CAUlC249svUfE6q0g3 192.168.2.76 52051 184.29.211.172 80 tcp http 61.298320 1465 22567 SF - - 0 ShADadfF 19 2465 21 23667 (empty)
+1320279639.698701 CBX0254QJoklXNbvv2 192.168.2.76 52110 199.59.148.201 80 tcp http 0.283987 901 1067 SF - - 0 ShADadfF 6 1225 5 1335 (empty)
+1320279638.450681 CSvs6v26bQqFylkk6l 192.168.2.76 52101 192.150.187.43 80 tcp http 5.709781 758 19809 SF - - 0 ShADadFf 16 1602 20 20857 (empty)
+1320279638.954157 C4pHul1H3OeWYz7o7i 192.168.2.76 52102 192.150.187.43 80 tcp http 5.228420 371 498 SF - - 0 ShADadFf 7 747 5 766 (empty)
+1320279638.957224 C7Lcvr4vsTf6eYpBva 192.168.2.76 52104 192.150.187.43 80 tcp http 5.231185 340 1443 SF - - 0 ShADadFf 7 716 5 1711 (empty)
+1320279638.955996 CV8faD4L1sLL5kDwN9 192.168.2.76 52103 192.150.187.43 80 tcp http 5.243925 338 24829 SF - - 0 ShADadFf 18 1286 22 25981 (empty)
+1320279639.349306 CvfUrT2DgYXXoZw9Ah 192.168.2.76 52109 192.150.187.43 80 tcp http 4.862785 400 7004 SF - - 0 ShADadFf 9 880 8 7428 (empty)
+1320279639.147746 C6MrHk2C7rLuJqhjsg 192.168.2.76 52107 192.150.187.43 80 tcp http 5.066841 404 491 SF - - 0 ShADadFf 6 728 4 707 (empty)
+1320279639.205080 Ccc26E2f7mpxWWj5L2 192.168.2.76 52108 192.150.187.43 80 tcp - 5.009511 0 0 SF - - 0 ShAFf 5 272 3 172 (empty)
+1320279639.052091 CyiluB4nGodFLEMnX5 192.168.2.76 52105 192.150.187.43 80 tcp - 5.162501 0 0 SF - - 0 ShAFf 5 272 3 172 (empty)
+1320279639.147610 CxyAKs10ppnHFP6O8i 192.168.2.76 52106 192.150.187.43 80 tcp http 5.066984 404 491 SF - - 0 ShADadFf 6 728 4 707 (empty)
+1320279636.698841 C7Krri4g9tZfHniGXh 192.168.2.76 52099 192.150.187.43 80 tcp http 7.515757 1219 28929 SF - - 0 ShADadFf 23 2427 24 30185 (empty)
+1320279630.486859 CC3vUI3gFB04zLvWRa 192.168.2.76 52098 199.59.148.20 80 tcp http 15.198762 2050 4776 SF - - 0 ShADadfF 8 2478 9 5252 (empty)
+1320279673.118128 CRNn9f1zKNlzHSM5pa 192.168.2.76 52112 199.59.148.201 80 tcp http 0.351267 902 1068 SF - - 0 ShADadfF 6 1226 5 1336 (empty)
+1320279672.273571 C6Ym6jvMgikT0xTTc 192.168.2.76 52111 192.150.187.43 80 tcp http 5.564817 419 48038 SF - - 0 ShADadfF 23 1627 38 50022 (empty)
+1320279579.393218 CLsqp41RLUd83arUQb 192.168.2.76 52053 132.235.215.119 80 tcp http 0.045584 2503 21124 S1 - - 0 ShADad 13 3191 18 22068 (empty)
+1320279567.515293 CN5hnY3x51j6Hr1v4 192.168.2.76 52036 74.125.225.78 80 tcp http 23.090143 6335 4537 S1 - - 0 ShADad 18 7283 11 5117 (empty)
+1320279581.817559 CGv2Tp4Ngt8MmKmVRd 192.168.2.76 52062 132.235.215.119 80 tcp http 0.007172 600 248 S1 - - 0 ShADad 4 820 3 412 (empty)
+1320279571.543053 CsBgiE1WmGP4Yo749h 192.168.2.76 52039 69.171.228.39 80 tcp http 0.308956 417 10451 S1 - - 0 ShADd 9 897 9 10931 (empty)
+1320279587.101825 C96j2X1DixgLTj2Oi8 192.168.2.76 52072 74.125.225.64 80 tcp http 0.614423 2544 2981 S1 - - 0 ShADad 6 2868 6 3301 (empty)
+1320279577.686971 CjinlH2fzDtvzI9637 192.168.2.76 52049 184.29.211.172 80 tcp http 6.945222 2240 31147 S1 - - 0 ShADad 21 3344 26 32507 (empty)
+1320279589.315281 CBHHuR1xFnm5C5CQBc 192.168.2.76 52074 74.125.225.76 80 tcp http 0.059880 373 1158 S1 - - 0 ShADad 4 593 3 1322 (empty)
+1320279590.557604 C0K9DaoPFkfnzwlZa 192.168.2.76 52076 74.125.225.78 80 tcp http 0.048630 717 342 S1 - - 0 ShADad 4 937 3 506 (empty)
+1320279581.472457 CiIjAe1n5MnPOVpQ9f 192.168.2.76 52061 74.125.225.90 80 tcp http 0.704763 4835 51573 S1 - - 0 ShADad 30 6407 46 53973 (empty)
+1320279585.726876 CRgW2I2zo3SInm6iT8 192.168.2.76 52066 204.246.169.217 80 tcp http 1.386549 1233 8739 S1 - - 0 ShADad 10 1765 10 9267 (empty)
+1320279566.795729 CdrfXZ1NOFPEawF218 192.168.2.76 52028 72.21.211.173 80 tcp http 115.121914 380 2260 SF - - 0 ShADdFf 6 644 4 2432 (empty)
+1320279584.599525 Cs5yEZ3ELZTeuTOsP4 192.168.2.76 52064 204.246.169.252 80 tcp http 0.391939 370 64350 S1 - - 0 ShADad 28 1838 47 66802 (empty)
+1320279601.555241 CTRXSR3blXJE5ZE7Ij 192.168.2.76 52089 74.125.225.83 80 tcp http 71.619232 4280 704 S1 - - 0 ShADad 10 4812 6 1024 (empty)
+1320279580.303255 CNbPns4mOMGgjI8Ele 192.168.2.76 52057 204.246.169.3 80 tcp http 0.118609 844 1440 S1 - - 0 ShADad 6 1168 4 1656 (empty)
+1320279600.900056 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 tcp http 72.274459 8979 8637 S1 - - 0 ShADad 23 10187 16 9477 (empty)
+1320279571.880419 CtANmVrHYMtkWqPE5 192.168.2.76 52041 132.235.215.117 80 tcp http 0.013122 374 1813 S1 - - 0 ShADad 4 594 4 2029 (empty)
+1320279577.686764 CPoz7NUpXISemlNSd 192.168.2.76 52046 184.29.211.172 80 tcp http 6.975476 1916 71870 S1 - - 0 ShADad 37 3852 55 74738 (empty)
+1320279581.287819 C185u7u9Q4qhJPhzl 192.168.2.76 52060 74.125.225.92 80 tcp http 0.686395 1601 40796 S1 - - 0 ShADad 21 2705 33 42520 (empty)
+1320279586.006470 CbUCgw1DrIGcXzONB7 192.168.2.76 52071 204.246.169.217 80 tcp http 0.092010 381 1322 S1 - - 0 ShADad 4 601 3 1486 (empty)
+1320279566.795779 CJwUi9bdB9c1lLW44 192.168.2.76 52029 72.21.211.173 80 tcp http 115.121339 380 2658 SF - - 0 ShADdFf 6 644 4 2830 (empty)
+1320279571.880174 CYfHyC28tAhkLYkXB7 192.168.2.76 52040 132.235.215.117 80 tcp http 0.673383 1507 12558 S1 - - 0 ShADad 13 2195 14 13302 (empty)
+1320279581.284163 CKzjfhsJ8vrn2rrfg 192.168.2.76 52058 207.171.163.23 80 tcp http 0.335801 736 1674 S1 - - 0 ShADad 6 1000 5 1886 (empty)
+1320279577.686914 CaEFHq2HVQ5iGJQiD9 192.168.2.76 52048 184.29.211.172 80 tcp http 6.967534 2207 28633 S1 - - 0 ShADad 22 3363 24 29889 (empty)
+1320279586.001630 CWJhMU2cTLEnseTmCb 192.168.2.76 52067 204.246.169.217 80 tcp http 0.136158 381 5225 S1 - - 0 ShADad 5 653 6 5545 (empty)
+1320279567.684168 CdZUPH2DKOE7zzCLE3 192.168.2.76 52038 132.235.215.119 80 tcp http 115.202498 449 9019 SF - - 0 ShADadFf 9 929 10 9547 (empty)
+1320279579.442948 CbCciH11995WKkobR1 192.168.2.76 52054 74.121.134.156 80 tcp http 0.274905 1028 1071 S1 - - 0 ShADd 6 1292 3 1195 (empty)
+1320279579.803083 CaP2LpLGvsmX7yJO 192.168.2.76 52056 74.125.225.91 80 tcp http 0.046347 400 360 S1 - - 0 ShADad 4 620 3 524 (empty)
+1320279586.002799 CejI402rKGtdBXij4f 192.168.2.76 52068 204.246.169.217 80 tcp http 0.120253 762 3509 S1 - - 0 ShADad 6 1086 6 3829 (empty)
+1320279567.667107 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 tcp http 32.451792 6668 13531 S1 - - 0 ShADad 29 8188 29 15047 (empty)
+1320279566.795888 CT0JIh479jXIGt0Po1 192.168.2.76 52031 72.21.211.173 80 tcp http 115.121506 380 1981 SF - - 0 ShADdFf 6 644 4 2153 (empty)
+1320279566.447996 CwFs1P2UcUdlSxD2La 192.168.2.76 52026 132.235.215.119 80 tcp http 116.438679 2063 18235 SF - - 0 ShADadFf 15 2855 18 19179 (empty)
+1320279577.686850 Ct6ixh35y9AEr7J7o9 192.168.2.76 52047 184.29.211.172 80 tcp http 6.973070 1921 280972 S1 - - 0 ShADadt 144 11093 199 291328 (empty)
+1320279566.795830 CJxSUgkInyKSHiju1 192.168.2.76 52030 72.21.211.173 80 tcp http 115.121810 380 2686 SF - - 0 ShADdFf 6 644 4 2858 (empty)
+1320279601.554581 CibfNy1QQW4ImDWRq5 192.168.2.76 52088 74.125.225.83 80 tcp http 35.738404 4220 704 S1 - - 0 ShADad 10 4752 7 1076 (empty)
+1320279566.795628 CoX7zA3OJKGUOSCBY2 192.168.2.76 52027 72.21.211.173 80 tcp http 115.121837 380 2948 SF - - 0 ShADdFf 6 644 5 3160 (empty)
+1320279577.687031 Cedw7H3ddE2yLiLoXc 192.168.2.76 52050 184.29.211.172 80 tcp http 6.947920 2582 34114 S1 - - 0 ShADad 26 3946 30 35682 (empty)
+1320279584.610492 Cu4gIx1BDNtGOl7Ht2 192.168.2.76 52065 204.246.169.252 80 tcp http 4.847647 1218 131460 S1 - - 0 ShADad 55 4090 94 136356 (empty)
+1320279588.157960 CYYyja3FFNEnftw3K6 192.168.2.76 52073 74.125.225.72 80 tcp http 0.346895 378 174833 S1 - - 0 ShADadt 77 4718 127 181445 (empty)
+1320279571.880844 C4uDKU5tpeRU9Su19 192.168.2.76 52043 132.235.215.117 80 tcp http 0.027676 389 803 S1 - - 0 ShADad 4 609 3 967 (empty)
+1320279571.880785 CSTH8n1O1nv0ztxNQd 192.168.2.76 52042 132.235.215.117 80 tcp http 0.698402 813 45320 S1 - - 0 ShADad 22 1969 34 47096 (empty)
+1320279586.004044 C2KnU34GcVV6amo8va 192.168.2.76 52069 204.246.169.217 80 tcp http 0.094285 381 1903 S1 - - 0 ShADad 4 601 4 2119 (empty)
+1320279582.210392 C5DisEMFU77Wk9Kae 192.168.2.76 52063 204.246.169.252 80 tcp http 7.278092 1971 508090 S1 - - 0 ShADadt 225 15495 355 526558 (empty)
+1320279590.556280 CD1jfU3p9abEm77mzf 192.168.2.76 52075 74.125.225.78 80 tcp http 0.047887 714 342 S1 - - 0 ShADad 4 934 3 506 (empty)
+1320279586.005337 C5vx4911iSMAJuShFd 192.168.2.76 52070 204.246.169.217 80 tcp http 0.093133 381 2493 S1 - - 0 ShADad 4 601 4 2709 (empty)
+1320279673.118549 CJLgi92kpp2gLgGTE5 192.168.2.76 52113 199.59.148.20 80 tcp http 10.247819 1023 2388 SF - - 0 ShADadfF 6 1347 6 2708 (empty)
+1320279579.731320 ClcvKE1dqsEFQu46m9 192.168.2.76 52055 74.125.225.91 80 tcp http 0.522914 1493 54251 S1 - - 0 ShADad 30 3065 46 56651 (empty)
+1320279601.553361 CnGze54kQWWpKqrrZ4 192.168.2.76 52087 209.85.145.95 80 tcp http 71.658218 3168 19975 S1 - - 0 ShADadt 23 4388 29 21491 (empty)
+1320279566.796068 C6Q4Vm14ZJIlZhsXqk 192.168.2.76 52032 72.21.211.173 80 tcp http 115.119217 380 2628 SF - - 0 ShADadFf 6 644 5 2840 (empty)
+#close 2017-04-16-21-36-10
diff --git a/test/logfile_bro_http.log.0 b/test/logfile_bro_http.log.0
new file mode 100644
index 0000000..8f69bec
--- /dev/null
+++ b/test/logfile_bro_http.log.0
@@ -0,0 +1,206 @@
+#separator \x09
+#set_separator ,
+#empty_field (empty)
+#unset_field -
+#path http
+#open 2017-04-16-21-36-10
+#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p trans_depth method host uri referrer version user_agent request_body_len response_body_len status_code status_msg info_code info_msg tags username password proxied orig_fuids orig_filenames orig_mime_types resp_fuids resp_filenames resp_mime_types
+#types time string addr port addr port count string string string string string string count count count string count string set[enum] string string set[string] vector[string] vector[string] vector[string] vector[string] vector[string] vector[string]
+1320279566.452687 CwFs1P2UcUdlSxD2La 192.168.2.76 52026 132.235.215.119 80 1 GET www.reddit.com / - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 109978 200 OK - - (empty) - - - - - - Ftw3fJ2JJF3ntMTL2 - text/html
+1320279566.831619 CJxSUgkInyKSHiju1 192.168.2.76 52030 72.21.211.173 80 1 GET e.thumbs.redditmedia.com /E-pbDbmiBclPkDaX.jpg http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2300 200 OK - - (empty) - - - - - - FFTf9Zdgk3YkfCKo3 - image/jpeg
+1320279566.831563 CJwUi9bdB9c1lLW44 192.168.2.76 52029 72.21.211.173 80 1 GET f.thumbs.redditmedia.com /BP5bQfy4o-C7cF6A.jpg http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2272 200 OK - - (empty) - - - - - - FfXtOj3o7aub4vbs2j - image/jpeg
+1320279566.831473 CoX7zA3OJKGUOSCBY2 192.168.2.76 52027 72.21.211.173 80 1 GET e.thumbs.redditmedia.com /SVUtep3Rhg5FTRn4.jpg http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2562 200 OK - - (empty) - - - - - - F21Ybs3PTqS6O4Q2Zh - image/jpeg
+1320279566.831643 CT0JIh479jXIGt0Po1 192.168.2.76 52031 72.21.211.173 80 1 GET f.thumbs.redditmedia.com /uuy31444rLSyKdHS.jpg http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1595 200 OK - - (empty) - - - - - - Fdk0MZ1wQmKWAJ4WH4 - image/jpeg
+1320279566.831666 C6Q4Vm14ZJIlZhsXqk 192.168.2.76 52032 72.21.211.173 80 1 GET a.thumbs.redditmedia.com /BoVp7eG0DUodTIfr.jpg http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2242 200 OK - - (empty) - - - - - - FwCCcC3lGkQAwhCDX3 - image/jpeg
+1320279566.831535 CdrfXZ1NOFPEawF218 192.168.2.76 52028 72.21.211.173 80 1 GET c.thumbs.redditmedia.com /IEeSI3Q47xHE0UEz.jpg http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1874 200 OK - - (empty) - - - - - - FHK4nO28ZC5rrBZPqa - image/jpeg
+1320279567.211407 CdysLK1XpcrXOpVDuh 192.168.2.76 52034 174.129.249.33 80 1 GET www.redditmedia.com /ads/ http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 3258 200 OK - - (empty) - - - - - - Fv5xxZ7iP0eQKziM2 - text/html
+1320279567.211031 CtgxRAqDLvrRUQdqe 192.168.2.76 52033 184.72.234.3 80 1 GET pixel.redditmedia.com /pixel/of_destiny.png?v=32tb6zakMbpImUZWtz+pksVc/8wYRc822cfKz091HT0oAKWHwZGxGpDcvvwUpyjwU8nJsyGc4cw=&r=296143927 http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 105 200 OK - - (empty) - - - - - - F5EJmr1cvlMkJFqSSk - image/png
+1320279567.296908 CwFs1P2UcUdlSxD2La 192.168.2.76 52026 132.235.215.119 80 2 GET www.reddit.com /static/bg-button-positive-unpressed.png http://www.reddit.com/static/reddit.RZTLMiZ4gTk.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279567.451885 CtgxRAqDLvrRUQdqe 192.168.2.76 52033 184.72.234.3 80 2 GET pixel.redditmedia.com /fetch-trackers?callback=jQuery16107779853632052074_1320279566998&ids[]=t5_6&ids[]=t3_lsfmb&ids[]=t3_lsejk&_=1320279567192 http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 206 200 OK - - (empty) - - - - - - FGxLzB2hPvGVceWXuf - text/plain
+1320279567.482546 C6nSoj1Qco9PGyslz6 192.168.2.76 52035 184.72.234.3 80 1 GET pixel.redditmedia.com /fetch-trackers?callback=jQuery16107779853632052074_1320279566999&ids[]=t5_6&ids[]=t3_lsfmb&ids[]=t3_lsejk&_=1320279567197 http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 206 200 OK - - (empty) - - - - - - FJ5XTZ1P1mJV2IhFth - text/plain
+1320279567.536586 CN5hnY3x51j6Hr1v4 192.168.2.76 52036 74.125.225.78 80 1 GET www.google-analytics.com /__utm.gif?utmwv=5.2.0&utms=1&utmn=872724630&utmhn=www.reddit.com&utme=8(site*srpath*usertype*uitype)9( reddit.com* reddit.com-GET_listing*guest*web)11(3!2)&utmcs=UTF-8&utmsr=1280x800&utmsc=24-bit&utmul=en-us&utmje=0&utmfl=10.1 r102&utmdt=reddit: the front page of the internet&utmhid=1425264550&utmr=-&utmp=/&utmac=UA-12131688-1&utmcc=__utma=55650728.1984705726.1319611466.1320276256.1320279567.22;+__utmz=55650728.1319747429.7.7.utmcsr=google|utmccn=(organic)|utmcmd=organic|utmctr=appengine%20python%20mobile%20analytics;&utmu=qQ~ http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 35 200 OK - - (empty) - - - - - - FilkiN33J86y8uYEF7 - image/gif
+1320279567.689996 CdZUPH2DKOE7zzCLE3 192.168.2.76 52038 132.235.215.119 80 1 GET feeds.bbci.co.uk /news/rss.xml?edition=int - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 44841 200 OK - - (empty) - - - - - - FscOrx3YnSFtKUa9uh - text/atom
+1320279567.680708 CtgxRAqDLvrRUQdqe 192.168.2.76 52033 184.72.234.3 80 3 GET pixel.redditmedia.com /pixel/of_doom.png?id=t5_6&hash=e962d119a7ff69901bb4ceaa7f3ba1224fd704b7&r=741109704 http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 105 200 OK - - (empty) - - - - - - F6kKwQdgasTZr1aL3 - image/png
+1320279567.683031 C6nSoj1Qco9PGyslz6 192.168.2.76 52035 184.72.234.3 80 2 GET pixel.redditmedia.com /pixel/of_doom.png?id=t3_lsfmb&hash=1c635ac04668546a1c33c2faf3c4814cd6c4f96a&r=1492956402 http://www.reddit.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 105 200 OK - - (empty) - - - - - - FmHLsN1LHERYFmp4e2 - image/png
+1320279567.690049 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 1 GET ad.doubleclick.net /adj/reddit.dart/reddit.com;kw=reddit.com;tile=1;sz=300x250;ord=5117434431991380? http://www.redditmedia.com/ads/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 491 200 OK - - (empty) - - - - - - Fabf7l1EL26N2KoFX6 - application/javascript
+1320279568.281910 CtgxRAqDLvrRUQdqe 192.168.2.76 52033 184.72.234.3 80 4 GET pixel.redditmedia.com /pixel/of_defenestration.png?hash=a8ababd2e4912c8b21d72252ad18ebb5d8e27ea3&id=dart_reddit.com&random=5012335803517919 http://www.redditmedia.com/ads/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 105 200 OK - - (empty) - - - - - - FcDkzJ3PNtrAn4aZu6 - image/png
+1320279571.625521 CsBgiE1WmGP4Yo749h 192.168.2.76 52039 69.171.228.39 80 1 GET www.facebook.com / - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 31379 200 OK - - (empty) - - - - - - FSRE0d2Zg3eeFyEBhf - text/html
+1320279571.883692 CYfHyC28tAhkLYkXB7 192.168.2.76 52040 132.235.215.117 80 1 GET static.ak.fbcdn.net /rsrc.php/v1/yt/r/svonORc8tTu.css http://www.facebook.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 20200 200 OK - - (empty) - - - - - - F2U3Y12HmvdWxdclQ1 - text/plain
+1320279571.883724 CtANmVrHYMtkWqPE5 192.168.2.76 52041 132.235.215.117 80 1 GET static.ak.fbcdn.net /rsrc.php/v1/yZ/r/ejLIIb8vBQK.css http://www.facebook.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6968 200 OK - - (empty) - - - - - - FRGXkT2UUJEXviZzgf - text/plain
+1320279571.884016 CSTH8n1O1nv0ztxNQd 192.168.2.76 52042 132.235.215.117 80 1 GET static.ak.fbcdn.net /rsrc.php/v1/yp/r/kk8dc2UJYJ4.png http://www.facebook.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2209 200 OK - - (empty) - - - - - - FJroqp2fMBIRhSjj6j - image/png
+1320279571.884052 C4uDKU5tpeRU9Su19 192.168.2.76 52043 132.235.215.117 80 1 GET static.ak.fbcdn.net /rsrc.php/v1/yb/r/GsNJNwuI-UM.gif http://www.facebook.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 522 200 OK - - (empty) - - - - - - FkTSSMSu95IbbWyPk - image/gif
+1320279571.930335 CYfHyC28tAhkLYkXB7 192.168.2.76 52040 132.235.215.117 80 2 GET static.ak.fbcdn.net /rsrc.php/yi/r/q9U99v3_saj.ico - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 152 200 OK - - (empty) - - - - - - FA4O2QKRGwGeMhyWg - image/png
+1320279572.530622 CYfHyC28tAhkLYkXB7 192.168.2.76 52040 132.235.215.117 80 3 GET static.ak.fbcdn.net /rsrc.php/v1/yB/r/TwAHgQi2ZPB.png http://static.ak.fbcdn.net/rsrc.php/v1/yt/r/svonORc8tTu.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1203 200 OK - - (empty) - - - - - - FIp9Ei7407PZotrLf - image/png
+1320279572.541605 CYfHyC28tAhkLYkXB7 192.168.2.76 52040 132.235.215.117 80 4 GET static.ak.fbcdn.net /rsrc.php/v1/yu/r/O03OuHGGSjF.js http://www.facebook.com/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 14481 200 OK - - (empty) - - - - - - FXCacf3k0I8Jmv40V6 - text/plain
+1320279572.531333 CSTH8n1O1nv0ztxNQd 192.168.2.76 52042 132.235.215.117 80 2 GET static.ak.fbcdn.net /rsrc.php/v1/yi/r/OBaVg52wtTZ.png http://static.ak.fbcdn.net/rsrc.php/v1/yt/r/svonORc8tTu.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 42565 200 OK - - (empty) - - - - - - FKd4ju2Q2pBLbL2g5j - image/png
+1320279577.475501 CEh6Ka2HInkNSH01L2 192.168.2.76 52044 216.34.181.48 80 1 GET www.slashdot.org / - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 297 301 Moved Permanently - - (empty) - - - - - - FlEa1o4YEPG5x7R5mh - text/html
+1320279577.662818 CN5hnY3x51j6Hr1v4 192.168.2.76 52036 74.125.225.78 80 2 GET www.google-analytics.com /siteopt.js?v=1&utmxkey=2467390112&utmx=9273847.00017148082467390112:2:4&utmxx=9273847.00017148082467390112:1320193640:2592000&utmxtime=1320279577646 http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 3968 200 OK - - (empty) - - - - - - Fe6QOa3PksIXzVHTE4 - text/plain
+1320279577.706621 CaEFHq2HVQ5iGJQiD9 192.168.2.76 52048 184.29.211.172 80 1 GET a.fsdn.com /sd/topics/nasa_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 3599 200 OK - - (empty) - - - - - - FS2GsS2N4xpsInXkc5 - image/png
+1320279577.706671 Cedw7H3ddE2yLiLoXc 192.168.2.76 52050 184.29.211.172 80 1 GET a.fsdn.com /sd/topics/redhat_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1708 200 OK - - (empty) - - - - - - FbrtkF2Bsf8qono1hl - image/png
+1320279577.727833 CN5hnY3x51j6Hr1v4 192.168.2.76 52036 74.125.225.78 80 3 GET www.google-analytics.com /__utm.gif?utmwv=5.2.0&utms=1&utmn=2075689467&utmhn=slashdot.org&utmcs=UTF-8&utmsr=1280x800&utmsc=24-bit&utmul=en-us&utmje=0&utmfl=10.1 r102&utmdt=Slashdot: News for nerds, stuff that matters&utmhid=756102172&utmr=-&utmp=/2467390112/test&utmac=UA-32013-38&utmcc=__utma=9273847.1625321166.1320279578.1320279578.1320279578.1;+__utmz=9273847.1320279578.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none);+__utmx=9273847.00017148082467390112:2:4;&utmu=qACg~ http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 35 200 OK - - (empty) - - - - - - FMI1K94zPiqlNScu2b - image/gif
+1320279577.526624 CjPGiy13ncXKxU765j 192.168.2.76 52045 216.34.181.45 80 1 GET slashdot.org / - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 92235 200 OK - - (empty) - - - - - - FC6fny4bS2LdWArKCd - text/html
+1320279577.706646 CjinlH2fzDtvzI9637 192.168.2.76 52049 184.29.211.172 80 1 GET a.fsdn.com /sd/topics/apple_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 5316 200 OK - - (empty) - - - - - - FrzGSm1jOZoVQ2Hx9k - image/png
+1320279577.746860 Cedw7H3ddE2yLiLoXc 192.168.2.76 52050 184.29.211.172 80 2 GET a.fsdn.com /sd/topics/news_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 4791 200 OK - - (empty) - - - - - - FFzyL22N09AR4kpGqj - image/png
+1320279577.744727 CaEFHq2HVQ5iGJQiD9 192.168.2.76 52048 184.29.211.172 80 2 GET a.fsdn.com /sd/topics/windows_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6070 200 OK - - (empty) - - - - - - FSY7De4YLIkMbdkgub - image/png
+1320279577.792926 Cedw7H3ddE2yLiLoXc 192.168.2.76 52050 184.29.211.172 80 3 GET a.fsdn.com /sd/topics/microsoft_64100.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 4684 200 OK - - (empty) - - - - - - F1ZWL920coZQCa5hB6 - image/png
+1320279577.786697 CjinlH2fzDtvzI9637 192.168.2.76 52049 184.29.211.172 80 2 GET a.fsdn.com /sd/topics/bug_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 7200 200 OK - - (empty) - - - - - - FkTxkx1LuAiF22kjQ5 - image/png
+1320279577.706695 CAUlC249svUfE6q0g3 192.168.2.76 52051 184.29.211.172 80 1 GET a.fsdn.com /sd/topics/science_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6820 200 OK - - (empty) - - - - - - FqSWHi2S4omFxuoqE8 - image/png
+1320279577.796082 CaEFHq2HVQ5iGJQiD9 192.168.2.76 52048 184.29.211.172 80 3 GET a.fsdn.com /sd/topics/privacy_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 5372 200 OK - - (empty) - - - - - - FEDr4Q1KVBpZpYyCvf - image/png
+1320279577.831213 Cedw7H3ddE2yLiLoXc 192.168.2.76 52050 184.29.211.172 80 4 GET a.fsdn.com /sd/topics/games_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 4858 200 OK - - (empty) - - - - - - Fsf46e3M0rnbBBosjb - image/png
+1320279577.855921 CjinlH2fzDtvzI9637 192.168.2.76 52049 184.29.211.172 80 3 GET a.fsdn.com /sd/topics/java_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 5985 200 OK - - (empty) - - - - - - FzE9vp3dywOexb5lOj - image/png
+1320279577.706506 CPoz7NUpXISemlNSd 192.168.2.76 52046 184.29.211.172 80 1 GET a.fsdn.com /sd/classic.css?release_20111101.01 http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 102898 200 OK - - (empty) - - - - - - FeeI2T3XazYNxR2Aff - text/plain
+1320279577.885356 CaEFHq2HVQ5iGJQiD9 192.168.2.76 52048 184.29.211.172 80 4 GET a.fsdn.com /sd/topics/facebook_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 935 200 OK - - (empty) - - - - - - F0pvzI2hxCu7CPDES4 - image/png
+1320279577.874879 CAUlC249svUfE6q0g3 192.168.2.76 52051 184.29.211.172 80 2 GET a.fsdn.com /sd/topics/topickde.gif http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 3503 200 OK - - (empty) - - - - - - FaZ03A6N9Jr41XtA9 - image/gif
+1320279577.898479 Cedw7H3ddE2yLiLoXc 192.168.2.76 52050 184.29.211.172 80 5 GET a.fsdn.com /sd/topics/technology_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 7000 200 OK - - (empty) - - - - - - Ft1wSRxSA94TeB9vk - image/png
+1320279577.706532 Ct6ixh35y9AEr7J7o9 192.168.2.76 52047 184.29.211.172 80 1 GET a.fsdn.com /sd/all-minified.js?release_20111101.01 http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 265231 200 OK - - (empty) - - - - - - FV4WlAnhOGEzG8yNf - text/plain
+1320279578.786070 CN5hnY3x51j6Hr1v4 192.168.2.76 52036 74.125.225.78 80 4 GET www.google-analytics.com /__utm.gif?utmwv=5.2.0&utms=1&utmn=1576123726&utmhn=slashdot.org&utme=8(User Type*Page)9(Anon*index2)&utmcs=UTF-8&utmsr=1280x800&utmsc=24-bit&utmul=en-us&utmje=0&utmfl=10.1 r102&utmdt=Slashdot: News for nerds, stuff that matters&utmhid=756102172&utmr=-&utmp=/&utmac=UA-32013-5&utmcc=__utma=57409013.1111154037.1320279579.1320279579.1320279579.1;+__utmz=57409013.1320279579.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none);&utmu=qRCg~ http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 35 200 OK - - (empty) - - - - - - FPFdR81eU5ibximh1c - image/gif
+1320279578.786348 CjPGiy13ncXKxU765j 192.168.2.76 52045 216.34.181.45 80 2 GET slashdot.org /favicon.ico - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 318 200 OK - - (empty) - - - - - - FNUo213hZ5nZPeveCg - image/x-icon
+1320279578.786168 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 2 GET ad.doubleclick.net /adj/ostg.slashdot/pg_index_p1_leader;pg=index2;logged_in=0;tile=1;sz=728x90;u=;ord=6795061899455057? http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1102 200 OK - - (empty) - - - - - - FX5LMT3stFxRIOJTy6 - application/javascript
+1320279578.927905 CIJIDL1ULo4HpT24Gl 192.168.2.76 52052 63.241.108.124 80 1 GET bs.serving-sys.com /BurstingPipe/adServer.bs?cn=rsb&c=28&pli=3258172&PluID=0&w=728&h=90&ord=5919911&ucm=true&ncu=$$http://ad.doubleclick.net/click;h=v8/3bb4/3/0/*/i;246771152;0-0;0;47077322;3454-728/90;44177745/44195532/1;u=;~okv=;pg=index2;logged_in=0;tile=1;sz=728x90;u=;bsg=100834;bsg=100849;bsg=100972;bsg=100974;bsg=109739;~sscs=?$$ http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2445 200 OK - - (empty) - - - - - - F1gfQ01LTJYKrGF5f6 - text/plain
+1320279579.395786 CLsqp41RLUd83arUQb 192.168.2.76 52053 132.235.215.119 80 1 GET ds.serving-sys.com /BurstingCachedScripts//SBTemplates_2_4_11/StdBanner.js?ai=6818549 http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 25789 200 OK - - (empty) - - - - - - FsP7B41SP02n8qy4Q4 - text/plain
+1320279579.411954 CLsqp41RLUd83arUQb 192.168.2.76 52053 132.235.215.119 80 2 GET ds.serving-sys.com /BurstingRes///Site-16990/Type-0/0c04460f-7d5c-47c7-bb52-d55a6cb9dfcc.gif http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 13402 200 OK - - (empty) - - - - - - Fwc35U3WIVMb3eEMih - image/gif
+1320279579.414248 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 3 GET ad.doubleclick.net /adj/ostg.slashdot/mainpage_p33_powerswitch;pg=index2;logged_in=0;tile=2;sz=980x66;u=;ord=6795061899455057? http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 350 200 OK - - (empty) - - - - - - FHYUQHQwRCXF6z6m1 - application/javascript
+1320279579.446304 Ct6ixh35y9AEr7J7o9 192.168.2.76 52047 184.29.211.172 80 2 GET a.fsdn.com /sd/logo_w_l.png http://a.fsdn.com/sd/classic.css?release_20111101.01 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 916 200 OK - - (empty) - - - - - - Fq8jKs4ZEeQ86XLwHb - image/png
+1320279579.446541 CPoz7NUpXISemlNSd 192.168.2.76 52046 184.29.211.172 80 2 GET a.fsdn.com /sd/classic/img/glyphish-icons-16.png http://a.fsdn.com/sd/classic.css?release_20111101.01 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 34897 200 OK - - (empty) - - - - - - Fp4mTFtTxLfj1aZ0k - image/png
+1320279579.494380 CbCciH11995WKkobR1 192.168.2.76 52054 74.121.134.156 80 1 GET data.cmcore.com /imp?tid=17&ci=90378805&vn1=4.1.1&vn2=imp&ec=UTF-8&cm_mmc=CL11Display-_-Geeknet-_-728x90-_-SimpleQ4 http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 302 Found - - (empty) - - - - - - - - -
+1320279579.635947 CPoz7NUpXISemlNSd 192.168.2.76 52046 184.29.211.172 80 3 GET a.fsdn.com /sd/classic/img/facebook_24.png http://a.fsdn.com/sd/classic.css?release_20111101.01 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1145 200 OK - - (empty) - - - - - - FrO4DB3JrPQuySFXqb - image/png
+1320279579.635700 Ct6ixh35y9AEr7J7o9 192.168.2.76 52047 184.29.211.172 80 3 GET a.fsdn.com /sd/classic/img/twitter_24.png http://a.fsdn.com/sd/classic.css?release_20111101.01 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1131 200 OK - - (empty) - - - - - - FwOft14FasgQFevesf - image/png
+1320279579.636241 CjinlH2fzDtvzI9637 192.168.2.76 52049 184.29.211.172 80 4 GET a.fsdn.com /sd/classic/img/rss_24.png http://a.fsdn.com/sd/classic.css?release_20111101.01 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1394 200 OK - - (empty) - - - - - - Fg51Vl4RyzuxqyZong - image/png
+1320279579.660927 CbCciH11995WKkobR1 192.168.2.76 52054 74.121.134.156 80 2 GET data.cmcore.com /imp?tid=17&ci=90378805&vn1=4.1.1&vn2=imp&ec=UTF-8&cm_mmc=CL11Display-_-Geeknet-_-728x90-_-SimpleQ4&cvdone=s http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 43 200 OK - - (empty) - - - - - - FsfXhn4B6h8Cjd8sS8 - image/gif
+1320279579.605985 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 4 GET ad.doubleclick.net /adj/ostg.slashdot/pg_index_p83_medrec;pg=index2;logged_in=0;tile=3;sz=300x250,300x600;u=;ord=6795061899455057? http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 37631 200 OK - - (empty) - - - - - - FKjsb32g87yJd7WC59 - application/javascript
+1320279579.754251 ClcvKE1dqsEFQu46m9 192.168.2.76 52055 74.125.225.91 80 1 GET s0.2mdn.net /1251057/plcr_44606913_1318531591501.js http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 16859 200 OK - - (empty) - - - - - - F8zvi64uyBZnSFW7X9 - text/plain
+1320279579.731050 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 5 GET ad.doubleclick.net /ad/N815.slashdot/B5855285.36;sz=1x1 http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 302 Moved Temporarily - - (empty) - - - - - - - - -
+1320279579.788781 ClcvKE1dqsEFQu46m9 192.168.2.76 52055 74.125.225.91 80 2 GET s0.2mdn.net /879366/inpageGlobalTemplate_v2_62_06.js http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 47618 200 OK - - (empty) - - - - - - F1x0AK2HfI8zKiGCpc - text/plain
+1320279579.826149 CaP2LpLGvsmX7yJO 192.168.2.76 52056 74.125.225.91 80 1 GET s0.2mdn.net /viewad/3000209/14-1x1.gif http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 43 200 OK - - (empty) - - - - - - FwHSAt162BDr8cAtJc - image/gif
+1320279580.110519 ClcvKE1dqsEFQu46m9 192.168.2.76 52055 74.125.225.91 80 3 GET s0.2mdn.net /1251057/PID_1778428_MABQrgjDNeiVz7Kj.swf http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 30158 200 OK - - (empty) - - - - - - FiWyYW1UE23Xn9Du4c - application/x-shockwave-flash
+1320279580.134281 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 6 GET ad.doubleclick.net /adj/ostg.slashdot/pg_index_p31_lower_poll_spons;pg=index;logged_in=0;tile=4;ord=6795061899455057? http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 481 200 OK - - (empty) - - - - - - FHGSmj2OR6IRPOR1Rg - application/javascript
+1320279580.212196 ClcvKE1dqsEFQu46m9 192.168.2.76 52055 74.125.225.91 80 4 GET s0.2mdn.net /viewad/1251080/peelUp2.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 9700 200 OK - - (empty) - - - - - - FDXrgk2emoeMrxUO52 - image/png
+1320279580.212311 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 7 GET ad.doubleclick.net /adj/ostg.slashdot/pg_index_CPL_medrec;pg=index;logged_in=0;tile=5;sz=300x250;ord=6795061899455057;? http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 346 200 OK - - (empty) - - - - - - FcCddr4x0GnKCSZ8q1 - application/javascript
+1320279580.339065 CNbPns4mOMGgjI8Ele 192.168.2.76 52057 204.246.169.3 80 1 GET d1clfvuu2240eh.cloudfront.net /crossdomain.xml - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 76 200 OK - - (empty) - - - - - - FkbZHZVBBE3Z1ZgDh - text/x-cross-domain-policy
+1320279580.382077 CNbPns4mOMGgjI8Ele 192.168.2.76 52057 204.246.169.3 80 2 GET d1clfvuu2240eh.cloudfront.net /t.gif?m=a:W5Tk9EhlHtS1pyYL+RycSdDuNycgbdBawaGo+otmkKetUyhIY6Wu7kA=&m=b:JnQ9aW1wcmVzc2lvbiZyPTc2 - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 43 200 OK - - (empty) - - - - - - FMHtHn3vMLs8pHW5O4 - image/gif
+1320279580.341750 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 8 GET ad.doubleclick.net /adj/ostg.slashdot/pg_index_p85_medrec;pg=index2;logged_in=0;tile=6;sz=300x250,300x600;u=;ord=6795061899455057? http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 857 200 OK - - (empty) - - - - - - FcmK3c38gruwiDs6xe - application/javascript
+1320279581.309602 C185u7u9Q4qhJPhzl 192.168.2.76 52060 74.125.225.92 80 1 GET pagead2.googlesyndication.com /pagead/show_ads.js http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279581.313348 CKzjfhsJ8vrn2rrfg 192.168.2.76 52058 207.171.163.23 80 1 GET farm.sproutbuilder.com /crossdomain.xml - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 75 200 OK - - (empty) - - - - - - FRmk7R24HzVNTD5jM5 - text/x-cross-domain-policy
+1320279581.510471 CN5hnY3x51j6Hr1v4 192.168.2.76 52036 74.125.225.78 80 5 GET www.google-analytics.com /__utm.gif?utmwv=5.2.0&utms=2&utmn=949132929&utmhn=slashdot.org&utmt=event&utme=5(Firehose*FirehoseMore*10)8(User Type*Page)9(Anon*index2)&utmcs=UTF-8&utmsr=1280x800&utmsc=24-bit&utmul=en-us&utmje=0&utmfl=10.1 r102&utmdt=Slashdot: News for nerds, stuff that matters&utmhid=756102172&utmr=-&utmp=/&utmac=UA-32013-5&utmcc=__utma=57409013.1111154037.1320279579.1320279579.1320279579.1;+__utmz=57409013.1320279579.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none);&utmu=6RCgAAAAAAAAAAAAQ~ http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 35 200 OK - - (empty) - - - - - - FFCJkh2igAZa2JEKsg - image/gif
+1320279581.442967 CKzjfhsJ8vrn2rrfg 192.168.2.76 52058 207.171.163.23 80 2 GET farm.sproutbuilder.com /runtime.xml - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 853 200 OK - - (empty) - - - - - - FM2Cma4R3x8nKUayJ1 - application/xml
+1320279581.425927 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 9 GET ad.doubleclick.net /adj/ostg.slashdot/pg_index_google_medrec;pg=index2;logged_in=0;tile=7;sz=300x250,300x600;u=;ord=6795061899455057? http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 862 200 OK - - (empty) - - - - - - FVjOxU23aI5BNmJX2h - application/javascript
+1320279581.494295 CiIjAe1n5MnPOVpQ9f 192.168.2.76 52061 74.125.225.90 80 1 GET googleads.g.doubleclick.net /pagead/ads?client=ca-ostg_js&format=300x250_pas_abgnc&output=html&h=250&w=300&lmt=1320279577&channel=books_sd_pages&region=default&ad_type=text,image,flash,html&adtest=off&alt_color=ffffff&color_bg=cccccc&color_border=bababa&color_line=c8c8c8&color_link=002f2f&color_text=000000&oe=utf8&flash=10.1.102&url=http://slashdot.org/&adsafe=high&dt=1320279581339&bpp=3&shv=r20111026&jsv=r20110914&correlator=1320279581423&frm=4&adk=3033987521&ga_vid=473684895.1320279581&ga_sid=1320279581&ga_hid=756102172&ga_fc=0&ga_wpids=UA-32013-5&u_tz=-240&u_his=3&u_java=0&u_h=800&u_w=1280&u_ah=726&u_aw=1280&u_cd=24&u_nplug=4&u_nmime=64&dff=arial&dfs=13&adx=939&ady=1333&biw=1265&bih=617&fu=0&ifi=1&dtd=128&xpc=ZMG9awPxwx&p=http://slashdot.org http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 4365 200 OK - - (empty) - - - - - - F3NRgf4pxU4hVreqGi - text/html
+1320279581.820179 CGv2Tp4Ngt8MmKmVRd 192.168.2.76 52062 132.235.215.119 80 1 GET b.scorecardresearch.com /b?c1=2&c2=6035546&rn=0.8987666179077362&c7=http://slashdot.org/&c3=&c4=&c5=&c6=&c10=&c15=&c16=&c8=Slashdot: News for nerds, stuff that matters&c9=&cv=1.7 http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 204 No Content - - (empty) - - - - - - - - -
+1320279581.866795 C185u7u9Q4qhJPhzl 192.168.2.76 52060 74.125.225.92 80 2 GET pagead2.googlesyndication.com /pagead/imgad?id=CICAgMDOnZWCUxCsAhj6ATIICPPzdVZiN_g http://googleads.g.doubleclick.net/pagead/ads?client=ca-ostg_js&format=300x250_pas_abgnc&output=html&h=250&w=300&lmt=1320279577&channel=books_sd_pages&region=default&ad_type=text%2Cimage%2Cflash%2Chtml&adtest=off&alt_color=ffffff&color_bg=cccccc&color_border=bababa&color_line=c8c8c8&color_link=002f2f&color_text=000000&oe=utf8&flash=10.1.102&url=http%3A%2F%2Fslashdot.org%2F&adsafe=high&dt=1320279581339&bpp=3&shv=r20111026&jsv=r20110914&correlator=1320279581423&frm=4&adk=3033987521&ga_vid=473684895.1320279581&ga_sid=1320279581&ga_hid=756102172&ga_fc=0&ga_wpids=UA-32013-5&u_tz=-240&u_his=3&u_java=0&u_h=800&u_w=1280&u_ah=726&u_aw=1280&u_cd=24&u_nplug=4&u_nmime=64&dff=arial&dfs=13&adx=939&ady=1333&biw=1265&bih=617&fu=0&ifi=1&dtd=128&xpc=ZMG9awPxwx&p=http%3A//slashdot.org 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 40252 200 OK - - (empty) - - - - - - FpH8io1fXFSErb0719 - application/x-shockwave-flash
+1320279581.833299 CiIjAe1n5MnPOVpQ9f 192.168.2.76 52061 74.125.225.90 80 2 GET googleads.g.doubleclick.net /pagead/ads?client=ca-ostg_js&format=300x250_pas_abgnc&output=html&h=250&w=300&lmt=1320279577&channel=slashdot_imu_geo_us&region=default&ad_type=text,image,flash,html&adtest=off&alt_color=ffffff&color_bg=cccccc&color_border=bababa&color_line=c8c8c8&color_link=002f2f&color_text=000000&oe=utf8&flash=10.1.102&url=http://slashdot.org/&adsafe=high&dt=1320279581648&bpp=3&shv=r20111026&jsv=r20110914&prev_fmts=300x250_pas_abgnc&correlator=1320279581423&frm=4&adk=2897144109&ga_vid=473684895.1320279581&ga_sid=1320279581&ga_hid=756102172&ga_fc=0&u_tz=-240&u_his=3&u_java=0&u_h=800&u_w=1280&u_ah=726&u_aw=1280&u_cd=24&u_nplug=4&u_nmime=64&dff=arial&dfs=13&adx=939&ady=3468&biw=1265&bih=617&fu=0&ifi=2&dtd=172&xpc=8j2egD1P4r&p=http://slashdot.org http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2878 200 OK - - (empty) - - - - - - FNIXjx45CYFKs7kZF5 - text/html
+1320279582.056477 CiIjAe1n5MnPOVpQ9f 192.168.2.76 52061 74.125.225.90 80 3 GET googleads.g.doubleclick.net /pagead/imgad?id=CLK48PnasdKuNxCsAhj6ATII1hcxUl9z8x8 http://googleads.g.doubleclick.net/pagead/ads?client=ca-ostg_js&format=300x250_pas_abgnc&output=html&h=250&w=300&lmt=1320279577&channel=slashdot_imu_geo_us&region=default&ad_type=text%2Cimage%2Cflash%2Chtml&adtest=off&alt_color=ffffff&color_bg=cccccc&color_border=bababa&color_line=c8c8c8&color_link=002f2f&color_text=000000&oe=utf8&flash=10.1.102&url=http%3A%2F%2Fslashdot.org%2F&adsafe=high&dt=1320279581648&bpp=3&shv=r20111026&jsv=r20110914&prev_fmts=300x250_pas_abgnc&correlator=1320279581423&frm=4&adk=2897144109&ga_vid=473684895.1320279581&ga_sid=1320279581&ga_hid=756102172&ga_fc=0&u_tz=-240&u_his=3&u_java=0&u_h=800&u_w=1280&u_ah=726&u_aw=1280&u_cd=24&u_nplug=4&u_nmime=64&dff=arial&dfs=13&adx=939&ady=3468&biw=1265&bih=617&fu=0&ifi=2&dtd=172&xpc=8j2egD1P4r&p=http%3A//slashdot.org 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 47034 200 OK - - (empty) - - - - - - FsE8Xf1Is5TGXnetD5 - image/gif
+1320279582.246333 C5DisEMFU77Wk9Kae 192.168.2.76 52063 204.246.169.252 80 1 GET edge.sproutbuilder.com /crossdomain.xml - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 75 200 OK - - (empty) - - - - - - FVXCaI2T2NHgtAv0tb - text/x-cross-domain-policy
+1320279582.411626 Ct6ixh35y9AEr7J7o9 192.168.2.76 52047 184.29.211.172 80 4 GET a.fsdn.com /sd/spinner_ffffff_on_004242.gif http://a.fsdn.com/sd/classic.css?release_20111101.01 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1849 200 OK - - (empty) - - - - - - FlloV02bcrqQfmom13 - image/gif
+1320279582.409055 CN5hnY3x51j6Hr1v4 192.168.2.76 52036 74.125.225.78 80 6 GET www.google-analytics.com /__utm.gif?utmwv=5.2.0&utms=3&utmn=246596971&utmhn=slashdot.org&utmt=event&utme=5(Firehose*FirehoseMore*20)8(User Type*Page)9(Anon*index2)&utmcs=UTF-8&utmsr=1280x800&utmsc=24-bit&utmul=en-us&utmje=0&utmfl=10.1 r102&utmdt=Slashdot: News for nerds, stuff that matters&utmhid=756102172&utmr=-&utmp=/&utmac=UA-32013-5&utmcc=__utma=57409013.1111154037.1320279579.1320279579.1320279579.1;+__utmz=57409013.1320279579.1.1.utmcsr=(direct)|utmccn=(direct)|utmcmd=(none);&utmu=6RCgAAAAAAAAAAAAQ~ http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 35 200 OK - - (empty) - - - - - - F0yeUn1hZB9Y7yejMj - image/gif
+1320279582.288447 C5DisEMFU77Wk9Kae 192.168.2.76 52063 204.246.169.252 80 2 GET edge.sproutbuilder.com /code/1319516275/player.swf - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 291690 200 OK - - (empty) - - - - - - FA1xCj2DTlHjcsG0H7 - application/x-shockwave-flash
+1320279582.366695 CjPGiy13ncXKxU765j 192.168.2.76 52045 216.34.181.45 80 3 POST slashdot.org /ajax.pl http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 584 88073 200 OK - - (empty) - - - FCo92D1aKZZwcho8T3 - text/plain FzIP4GoO5f5PVho8l - text/json
+1320279584.545928 CjinlH2fzDtvzI9637 192.168.2.76 52049 184.29.211.172 80 5 GET a.fsdn.com /sd/topics/medicine_64.png?refresh=now http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2377 200 OK - - (empty) - - - - - - F8qktS1RoJMLjjMYX - image/png
+1320279584.546009 Cedw7H3ddE2yLiLoXc 192.168.2.76 52050 184.29.211.172 80 6 GET a.fsdn.com /sd/topics/government_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 4069 200 OK - - (empty) - - - - - - FTA0804fE7gZIXNT9b - image/png
+1320279584.546848 CaEFHq2HVQ5iGJQiD9 192.168.2.76 52048 184.29.211.172 80 5 GET a.fsdn.com /sd/topics/censorship_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 4975 200 OK - - (empty) - - - - - - FbZkOe38GSLykHirIa - image/png
+1320279584.544804 CPoz7NUpXISemlNSd 192.168.2.76 52046 184.29.211.172 80 4 GET a.fsdn.com /sd/topics/business_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6221 200 OK - - (empty) - - - - - - FboOO11NKqIFhA2jAi - image/png
+1320279584.546073 CAUlC249svUfE6q0g3 192.168.2.76 52051 184.29.211.172 80 3 GET a.fsdn.com /sd/topics/power_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 4293 200 OK - - (empty) - - - - - - FnNo9O1IVWySYiWof3 - image/png
+1320279584.582221 CjinlH2fzDtvzI9637 192.168.2.76 52049 184.29.211.172 80 6 GET a.fsdn.com /sd/topics/hp_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6772 200 OK - - (empty) - - - - - - F52lFL2Slu0Yj1b9h3 - image/png
+1320279584.591410 Cedw7H3ddE2yLiLoXc 192.168.2.76 52050 184.29.211.172 80 7 GET a.fsdn.com /sd/topics/security_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 4562 200 OK - - (empty) - - - - - - FiL1v32LLpGAfCoxGj - image/png
+1320279584.595893 CaEFHq2HVQ5iGJQiD9 192.168.2.76 52048 184.29.211.172 80 6 GET a.fsdn.com /sd/topics/court_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 5591 200 OK - - (empty) - - - - - - FvuG3B1pRIwIYLWXb8 - image/png
+1320279584.544227 Ct6ixh35y9AEr7J7o9 192.168.2.76 52047 184.29.211.172 80 5 GET a.fsdn.com /sd/topics/china_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 10079 200 OK - - (empty) - - - - - - FEjKFT1N7SHLlgUDie - image/png
+1320279584.602215 CPoz7NUpXISemlNSd 192.168.2.76 52046 184.29.211.172 80 5 GET a.fsdn.com /sd/topics/money_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 5459 200 OK - - (empty) - - - - - - FwxTAhFrD0SefH1Sj - image/png
+1320279584.624590 CAUlC249svUfE6q0g3 192.168.2.76 52051 184.29.211.172 80 4 GET a.fsdn.com /sd/topics/idle_64.png http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6556 200 OK - - (empty) - - - - - - Fs2vnI3rv9Lzrna8m - image/png
+1320279584.594900 C5DisEMFU77Wk9Kae 192.168.2.76 52063 204.246.169.252 80 3 GET edge.sproutbuilder.com /font/Tahoma.swf - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 60686 200 OK - - (empty) - - - - - - FM0cqy3Sr1FMqQu4R4 - application/x-shockwave-flash
+1320279584.635813 Cs5yEZ3ELZTeuTOsP4 192.168.2.76 52064 204.246.169.252 80 1 GET edge.sproutbuilder.com /font/Futura.swf - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 63675 200 OK - - (empty) - - - - - - FgA7fUcDv9ymIxGik - application/x-shockwave-flash
+1320279584.651727 Cu4gIx1BDNtGOl7Ht2 192.168.2.76 52065 204.246.169.252 80 1 GET edge.sproutbuilder.com /font/Archer.swf - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 77236 200 OK - - (empty) - - - - - - FraIcD2n6aVym9cqsc - application/x-shockwave-flash
+1320279585.764353 CRgW2I2zo3SInm6iT8 192.168.2.76 52066 204.246.169.217 80 1 GET edgy.sproutbuilder.com /crossdomain.xml - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 75 200 OK - - (empty) - - - - - - FZiisa2oO1yLpfxavf - text/x-cross-domain-policy
+1320279585.653934 C5DisEMFU77Wk9Kae 192.168.2.76 52063 204.246.169.252 80 4 GET edge.sproutbuilder.com /code/1319516275/com.sproutbuilder.components.video.VideoComponent.swf - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 100368 200 OK - - (empty) - - - - - - FzRX504YyLGx2Mqk8b - application/x-shockwave-flash
+1320279585.839709 Cu4gIx1BDNtGOl7Ht2 192.168.2.76 52065 204.246.169.252 80 2 GET edge.sproutbuilder.com /code/1319516275/com.sproutbuilder.components.button.ButtonComponent.swf - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 27471 200 OK - - (empty) - - - - - - FZR5pf1Sn9SSiYJF37 - application/x-shockwave-flash
+1320279586.039240 CejI402rKGtdBXij4f 192.168.2.76 52068 204.246.169.217 80 1 GET edgy.sproutbuilder.com /asset/aADeSoj6NM7TVgD-.png - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1170 200 OK - - (empty) - - - - - - F0NexnjX9pfFNWVmk - image/png
+1320279586.039757 C2KnU34GcVV6amo8va 192.168.2.76 52069 204.246.169.217 80 1 GET edgy.sproutbuilder.com /asset/wwCdFIihNP2BVYdd.png - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1248 200 OK - - (empty) - - - - - - Fmjv7q4q8sYJInU3C - image/png
+1320279586.041164 C5vx4911iSMAJuShFd 192.168.2.76 52070 204.246.169.217 80 1 GET edgy.sproutbuilder.com /asset/3ADTPIg3NBsgWP5u.png - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1838 200 OK - - (empty) - - - - - - FtXp4r2NHB0rsNJVZ9 - image/png
+1320279586.052831 CbUCgw1DrIGcXzONB7 192.168.2.76 52071 204.246.169.217 80 1 GET edgy.sproutbuilder.com /asset/qABVt4hSNIK2WmTu.png - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 668 200 OK - - (empty) - - - - - - FuM3Px3V9iLL9UWuR6 - image/png
+1320279586.081611 CejI402rKGtdBXij4f 192.168.2.76 52068 204.246.169.217 80 2 GET edgy.sproutbuilder.com /asset/UgDmFIiWNOMoVJD_.png - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1031 200 OK - - (empty) - - - - - - FzSLhC1MkvDzSRPqMh - image/png
+1320279586.037832 CWJhMU2cTLEnseTmCb 192.168.2.76 52067 204.246.169.217 80 1 GET edgy.sproutbuilder.com /asset/dQA7E4gKNDB1UJoP.png - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 4570 200 OK - - (empty) - - - - - - FZL5uJ1ZdfYUKShkVb - image/png
+1320279586.000195 CRgW2I2zo3SInm6iT8 192.168.2.76 52066 204.246.169.217 80 2 GET edgy.sproutbuilder.com /asset/vgBY54hjNDESTf27.jpg - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6921 200 OK - - (empty) - - - - - - FGB1Ugi2qN3KCeA3j - image/jpeg
+1320279587.052749 CRgW2I2zo3SInm6iT8 192.168.2.76 52066 204.246.169.217 80 3 GET edgy.sproutbuilder.com /asset/qABVt4hSNIK2WmTu.png - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279587.124669 C96j2X1DixgLTj2Oi8 192.168.2.76 52072 74.125.225.64 80 1 GET www.youtube.com /crossdomain.xml - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 306 200 OK - - (empty) - - - - - - FAyeVc3FzNroUbHvdi - text/x-cross-domain-policy
+1320279587.627640 C96j2X1DixgLTj2Oi8 192.168.2.76 52072 74.125.225.64 80 2 GET www.youtube.com /apiplayer?version=3 - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2177 200 OK - - (empty) - - - - - - FvuQ063vjYp4OXZCkj - application/x-shockwave-flash
+1320279588.180462 CYYyja3FFNEnftw3K6 192.168.2.76 52073 74.125.225.72 80 1 GET s.ytimg.com /yt/swfbin/apiplayer3-vflmM-6Dr.swf - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 174499 200 OK - - (empty) - - - - - - FSARBI7PBlWKampzk - application/x-shockwave-flash
+1320279589.337053 CBHHuR1xFnm5C5CQBc 192.168.2.76 52074 74.125.225.76 80 1 GET i4.ytimg.com /vi/gDbg_GeuiSY/hqdefault.jpg - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 893 404 Not Found - - (empty) - - - - - - F2GiAw3j1m22R2yIg2 - image/jpeg
+1320279589.319143 Cu4gIx1BDNtGOl7Ht2 192.168.2.76 52065 204.246.169.252 80 3 GET edge.sproutbuilder.com /code/1319516275/com.sproutbuilder.platforms.DoubleClickPlatform.swf - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 24725 200 OK - - (empty) - - - - - - FkCpou1t1Vt0YinJq6 - application/x-shockwave-flash
+1320279589.317863 C5DisEMFU77Wk9Kae 192.168.2.76 52063 204.246.169.252 80 5 GET edge.sproutbuilder.com /code/1319516275/com.sproutbuilder.platforms.GoogleAnalyticsPlatform.swf - 1.0 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 51897 200 OK - - (empty) - - - - - - FiBTwJ2zXXVZZ3aI0d - application/x-shockwave-flash
+1320279590.080406 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 10 GET ad.doubleclick.net /activity;src=1251057;met=1;v=1;pid=47077323;aid=247206211;ko=11;cid=44589125;rid=44606913;rv=1;&timestamp=1320279590078;eid1=2;ecn1=1;etm1=8; http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 42 200 OK - - (empty) - - - - - - Fa4s1w3OIKrgQOmn1c - image/gif
+1320279590.554429 CN5hnY3x51j6Hr1v4 192.168.2.76 52036 74.125.225.78 80 7 GET www.google-analytics.com /__utm.gif?utmwv=4.3as&utmn=1977361745&utmhn=s0.2mdn.net&utmt=event&utme=5(cachedCodeMiss*MABQrgjDNeiVz7Kj* )(0)&utmcs=UTF-8&utmsr=1280x800&utmsc=24-bit&utmul=en-us&utmje=0&utmfl=10.1 r102&utmdt=Slashdot: News for nerds, stuff that matters&utmhid=756102172&utmr=-&utmp=/&utmac=UA-5905822-1&utmcc=__utma=83256788.1532070249585310700.1304822985.1320193646.1320279590.297; - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 35 200 OK - - (empty) - - - - - - FoZQYe34dgjuHgb1Sd - image/gif
+1320279590.579330 CD1jfU3p9abEm77mzf 192.168.2.76 52075 74.125.225.78 80 1 GET www.google-analytics.com /__utm.gif?utmwv=4.3as&utmn=754945709&utmhn=s0.2mdn.net&utmcs=UTF-8&utmsr=1280x800&utmsc=24-bit&utmul=en-us&utmje=0&utmfl=10.1 r102&utmdt=Slashdot: News for nerds, stuff that matters&utmhid=756102172&utmr=-&utmp=/sprout/MABQrgjDNeiVz7Kj/view&utmac=UA-5905822-1&utmcc=__utma=83256788.1532070249585310700.1304822985.1320193646.1320279590.297; - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 35 200 OK - - (empty) - - - - - - Fyq8F21bTZJAyHUxIb - image/gif
+1320279590.581157 CN5hnY3x51j6Hr1v4 192.168.2.76 52036 74.125.225.78 80 8 GET www.google-analytics.com /__utm.gif?utmwv=4.3as&utmn=1428329940&utmhn=s0.2mdn.net&utmcs=UTF-8&utmsr=1280x800&utmsc=24-bit&utmul=en-us&utmje=0&utmfl=10.1 r102&utmdt=Slashdot: News for nerds, stuff that matters&utmhid=756102172&utmr=-&utmp=/MABQrgjDNeiVz7Kj&utmac=UA-32013-47&utmcc=__utma=83256788.1532070249585310700.1304822985.1320279590.1320279590.298; - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 35 200 OK - - (empty) - - - - - - FHnvsV2OYtFTWZGPM9 - image/gif
+1320279590.581430 C0K9DaoPFkfnzwlZa 192.168.2.76 52076 74.125.225.78 80 1 GET www.google-analytics.com /__utm.gif?utmwv=4.3as&utmn=223025521&utmhn=s0.2mdn.net&utmcs=UTF-8&utmsr=1280x800&utmsc=24-bit&utmul=en-us&utmje=0&utmfl=10.1 r102&utmdt=Slashdot: News for nerds, stuff that matters&utmhid=756102172&utmr=-&utmp=/MABQrgjDNeiVz7Kj/Untitled Page&utmac=UA-32013-47&utmcc=__utma=83256788.1532070249585310700.1304822985.1320279590.1320279590.298; - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 35 200 OK - - (empty) - - - - - - Feqm4J3iuQBtNnTEre - image/gif
+1320279600.078941 CmWpC33jXuKpXNLcie 192.168.2.76 52037 74.125.225.91 80 11 GET ad.doubleclick.net /activity;src=1251057;met=1;v=1;pid=47077323;aid=247206211;ko=11;cid=44589125;rid=44606913;rv=1;&timestamp=1320279600077;eid1=2;ecn1=0;etm1=10; http://slashdot.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 42 200 OK - - (empty) - - - - - - FC2TUu4ohZGmou4as - image/gif
+1320279600.921844 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 1 GET www.google.com /jsapi http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 23184 200 OK - - (empty) - - - - - - Fhm2NC27oehfBrVKsd - text/plain
+1320279600.688672 CbQAWi3GX2bCmX5L56 192.168.2.76 52078 192.150.187.43 80 1 GET www.bro-ids.org / - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 14258 200 OK - - (empty) - - - - - - FY05u72qZWO5o7Z2a - text/html
+1320279600.921091 Cd8s2R3OGDgkhnvSu9 192.168.2.76 52079 192.150.187.43 80 1 GET www.bro-ids.org /css/pygments.css http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2957 200 OK - - (empty) - - - - - - FR5Qvt1sx1p8pzmdtj - text/plain
+1320279600.924479 CX1GjC4vn52UY1uDv6 192.168.2.76 52082 192.150.187.43 80 1 GET www.bro-ids.org /css/print.css http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 334 200 OK - - (empty) - - - - - - FgV8RW27C5HTHOolrk - text/plain
+1320279600.921641 CBeaXe4Iyj1gXd2Iq 192.168.2.76 52080 192.150.187.43 80 1 GET www.bro-ids.org /css/960.css http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 5600 200 OK - - (empty) - - - - - - FcT6ak3zlX7zUFMefh - text/plain
+1320279601.025685 CX1GjC4vn52UY1uDv6 192.168.2.76 52082 192.150.187.43 80 2 GET www.bro-ids.org /js/jquery.zrssfeed.js http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 3324 200 OK - - (empty) - - - - - - FhKTDH1mwUptcDlgU9 - text/plain
+1320279600.995522 CbQAWi3GX2bCmX5L56 192.168.2.76 52078 192.150.187.43 80 2 GET www.bro-ids.org /js/jquery.fancybox-1.3.4.pack.js http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 15669 200 OK - - (empty) - - - - - - FqFURH1MR5BhnnvfZh - text/plain
+1320279601.021907 Cd8s2R3OGDgkhnvSu9 192.168.2.76 52079 192.150.187.43 80 2 GET www.bro-ids.org /js/jquery.tweet.js http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 8894 200 OK - - (empty) - - - - - - FeFVLI3Awcp2Tgpdxj - text/plain
+1320279601.130463 CX1GjC4vn52UY1uDv6 192.168.2.76 52082 192.150.187.43 80 3 GET www.bro-ids.org /js/superfish.js http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 3833 200 OK - - (empty) - - - - - - FoAMYj4is1GDyS2dZi - text/plain
+1320279601.201354 CbQAWi3GX2bCmX5L56 192.168.2.76 52078 192.150.187.43 80 3 GET www.bro-ids.org /js/hoverIntent.js http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 3257 200 OK - - (empty) - - - - - - FO8dv4OWQKKWRq0M7 - text/plain
+1320279601.219818 Cd8s2R3OGDgkhnvSu9 192.168.2.76 52079 192.150.187.43 80 3 GET www.bro-ids.org /js/general.js http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 5108 200 OK - - (empty) - - - - - - FJQxkp6TVhaNdhg6f - text/plain
+1320279601.127352 CBeaXe4Iyj1gXd2Iq 192.168.2.76 52080 192.150.187.43 80 2 GET www.bro-ids.org /js/jquery.tableofcontents.js http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 10384 200 OK - - (empty) - - - - - - FYKJ0y2gwKzVqIPOu5 - text/plain
+1320279600.921817 CmWpSw3VtjiAceBCwf 192.168.2.76 52081 192.150.187.43 80 1 GET www.bro-ids.org /css/bro-ids.css http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 23964 200 OK - - (empty) - - - - - - FtYub54s8HXI8r2JT1 - text/plain
+1320279601.239924 CX1GjC4vn52UY1uDv6 192.168.2.76 52082 192.150.187.43 80 4 GET www.bro-ids.org /js/jquery.collapse.js http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 5735 200 OK - - (empty) - - - - - - F0xM1j3wDeCr0UqNS1 - text/plain
+1320279600.925084 CaPClb1Bf0RrRGtyWi 192.168.2.76 52083 192.150.187.43 80 1 GET www.bro-ids.org /js/jquery.cycle.all.min.js http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 31052 200 OK - - (empty) - - - - - - FSQQ8nplvGBhBLgz8 - text/plain
+1320279601.385890 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 2 GET www.google.com /uds/?file=search&v=1&hl=en http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 615 200 OK - - (empty) - - - - - - FDwylw2NdwC19wGS77 - text/plain
+1320279601.554052 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 3 GET www.google.com /uds/?file=ads&v=3&packages=search&async=2 http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 441 200 OK - - (empty) - - - - - - FietVd1NOjUCHJiqpa - text/plain
+1320279601.305092 CbQAWi3GX2bCmX5L56 192.168.2.76 52078 192.150.187.43 80 4 GET www.bro-ids.org /images/bro-eyes.png http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 46415 200 OK - - (empty) - - - - - - FxO0Mzcsll6F6W5If - image/png
+1320279601.576535 CibfNy1QQW4ImDWRq5 192.168.2.76 52088 74.125.225.83 80 1 GET www.google.com /uds/css/small-logo.png http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279601.576629 CTRXSR3blXJE5ZE7Ij 192.168.2.76 52089 74.125.225.83 80 1 GET www.google.com /uds/css/clear.gif http://www.google.com/uds/api/search/1.0/473bb688d0c0dd605119ad983f5a4386/default+en.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279601.585473 CnGze54kQWWpKqrrZ4 192.168.2.76 52087 209.85.145.95 80 1 GET ajax.googleapis.com /ajax/services/feed/load?v=1.0&callback=jsonp1320279601362&q=http://blog.bro-ids.org/feeds/posts/default&num=5 http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6584 200 OK - - (empty) - - - - - - F1zJJe3NVHAaTYoSB2 - text/plain
+1320279601.636171 CK957ERTz8lBycly4 192.168.2.76 52085 199.59.148.201 80 1 GET search.twitter.com /search.json?&q=#BroIDS&rpp=2&callback=jsonp1320279601360 http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1543 200 OK - - (empty) - - - - - - Fu8xI43gbfnTmSOEh - text/plain
+1320279601.631059 C3TZMB4CrUwYfkGJy1 192.168.2.76 52086 199.59.148.20 80 1 GET api.twitter.com /1/statuses/user_timeline.json?screen_name=Bro_IDS&count=2&include_rts=1&callback=jsonp1320279601361 http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6095 200 OK - - (empty) - - - - - - F6HZ0A3TyUou56RR2i - text/plain
+1320279610.842497 CO5QKYQkcSdxQFA35 192.168.2.76 52090 192.150.187.43 80 1 GET www.bro-ids.org /download/index.html http://www.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 18981 200 OK - - (empty) - - - - - - FQ5FRM2xyT1BwXV8d2 - text/html
+1320279611.147279 CO5QKYQkcSdxQFA35 192.168.2.76 52090 192.150.187.43 80 2 GET www.bro-ids.org /js/breadcrumbs.js http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 2021 200 OK - - (empty) - - - - - - FatmzL3v37tIyHwqBd - text/plain
+1320279611.248377 CO5QKYQkcSdxQFA35 192.168.2.76 52090 192.150.187.43 80 3 GET www.bro-ids.org /images/icons/download.png http://www.bro-ids.org/css/bro-ids.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 716 200 OK - - (empty) - - - - - - FdcBQFJQuOKnBUiN7 - image/png
+1320279611.530084 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 4 GET www.google.com /uds/css/small-logo.png http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279611.530359 CibfNy1QQW4ImDWRq5 192.168.2.76 52088 74.125.225.83 80 2 GET www.google.com /uds/css/clear.gif http://www.google.com/uds/api/search/1.0/473bb688d0c0dd605119ad983f5a4386/default+en.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279611.527729 CnGze54kQWWpKqrrZ4 192.168.2.76 52087 209.85.145.95 80 2 GET ajax.googleapis.com /ajax/services/feed/load?v=1.0&callback=jsonp1320279611010&q=http://blog.bro-ids.org/feeds/posts/default&num=5 http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6584 200 OK - - (empty) - - - - - - FJ1t1Z2EpTTMw2Rrdk - text/plain
+1320279611.527499 C3TZMB4CrUwYfkGJy1 192.168.2.76 52086 199.59.148.20 80 2 GET api.twitter.com /1/statuses/user_timeline.json?screen_name=Bro_IDS&count=2&include_rts=1&callback=jsonp1320279611009 http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6095 200 OK - - (empty) - - - - - - FWPC0B25vmDQsn5Uid - text/plain
+1320279611.615559 CurHpb1TGZOktTRNP1 192.168.2.76 52092 199.59.148.201 80 1 GET search.twitter.com /search.json?&q=#BroIDS&rpp=2&callback=jsonp1320279611008 http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1543 200 OK - - (empty) - - - - - - F1zPyq3ZCdx2Przg4d - text/plain
+1320279612.151517 CO5QKYQkcSdxQFA35 192.168.2.76 52090 192.150.187.43 80 4 GET www.bro-ids.org /documentation/index.html http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 14762 200 OK - - (empty) - - - - - - FAhpvZ1Au5PG4I7Aah - text/html
+1320279612.497234 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 5 GET www.google.com /uds/css/small-logo.png http://www.bro-ids.org/documentation/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279612.497348 CTRXSR3blXJE5ZE7Ij 192.168.2.76 52089 74.125.225.83 80 2 GET www.google.com /uds/css/clear.gif http://www.google.com/uds/api/search/1.0/473bb688d0c0dd605119ad983f5a4386/default+en.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279612.495602 CnGze54kQWWpKqrrZ4 192.168.2.76 52087 209.85.145.95 80 3 GET ajax.googleapis.com /ajax/services/feed/load?v=1.0&callback=jsonp1320279612311&q=http://blog.bro-ids.org/feeds/posts/default&num=5 http://www.bro-ids.org/documentation/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6584 200 OK - - (empty) - - - - - - FUcAdl1oIIu09uHxQh - text/plain
+1320279612.495458 C3TZMB4CrUwYfkGJy1 192.168.2.76 52086 199.59.148.20 80 3 GET api.twitter.com /1/statuses/user_timeline.json?screen_name=Bro_IDS&count=2&include_rts=1&callback=jsonp1320279612310 http://www.bro-ids.org/documentation/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6095 200 OK - - (empty) - - - - - - Fk2gkj3kfAL1hYWbG5 - text/plain
+1320279612.574308 CuUKOQ1R3CqKBgeTdf 192.168.2.76 52093 199.59.148.201 80 1 GET search.twitter.com /search.json?&q=#BroIDS&rpp=2&callback=jsonp1320279612309 http://www.bro-ids.org/documentation/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1543 200 OK - - (empty) - - - - - - FpSSeZ2ZRioppjrZji - text/plain
+1320279613.969241 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 6 GET www.google.com /uds/css/small-logo.png http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279613.970081 CibfNy1QQW4ImDWRq5 192.168.2.76 52088 74.125.225.83 80 3 GET www.google.com /uds/css/clear.gif http://www.google.com/uds/api/search/1.0/473bb688d0c0dd605119ad983f5a4386/default+en.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279613.968918 CnGze54kQWWpKqrrZ4 192.168.2.76 52087 209.85.145.95 80 4 GET ajax.googleapis.com /ajax/services/feed/load?v=1.0&callback=jsonp1320279613813&q=http://blog.bro-ids.org/feeds/posts/default&num=5 http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6584 200 OK - - (empty) - - - - - - FraFOz3DFPTGZNaJC6 - text/plain
+1320279613.968841 C3TZMB4CrUwYfkGJy1 192.168.2.76 52086 199.59.148.20 80 4 GET api.twitter.com /1/statuses/user_timeline.json?screen_name=Bro_IDS&count=2&include_rts=1&callback=jsonp1320279613812 http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6095 200 OK - - (empty) - - - - - - FmB8iY3WlgS5hBY7Wf - text/plain
+1320279614.052578 C3xkHgJnzZszVSTpi 192.168.2.76 52094 199.59.148.201 80 1 GET search.twitter.com /search.json?&q=#BroIDS&rpp=2&callback=jsonp1320279613811 http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1543 200 OK - - (empty) - - - - - - F762sm4bW4MkAIJJGe - text/plain
+1320279616.824058 CMrjgF2XLmRh9C9TR4 192.168.2.76 52095 208.85.41.42 80 1 GET cont-sjl-1.pandora.com /images/public/amz/0/9/0/0/842694020090_500W_433H.jpg app:/desktop.swf 1.1 Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en) AppleWebKit/531.9 (KHTML, like Gecko) AdobeAIR/2.6 0 59209 200 OK - - (empty) - - - - - - FipMsu3eD5AnIRq2N - image/jpeg
+1320279630.119515 C2vQ8sVgyADHjtEda 192.168.2.76 52096 192.150.187.43 80 1 GET www.bro-ids.org /community/index.html http://www.bro-ids.org/download/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 15087 200 OK - - (empty) - - - - - - FeyKb7qKmO9eR5OKi - text/html
+1320279630.488327 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 7 GET www.google.com /uds/css/small-logo.png http://www.bro-ids.org/community/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279630.488443 CTRXSR3blXJE5ZE7Ij 192.168.2.76 52089 74.125.225.83 80 3 GET www.google.com /uds/css/clear.gif http://www.google.com/uds/api/search/1.0/473bb688d0c0dd605119ad983f5a4386/default+en.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279630.486761 CnGze54kQWWpKqrrZ4 192.168.2.76 52087 209.85.145.95 80 5 GET ajax.googleapis.com /ajax/services/feed/load?v=1.0&callback=jsonp1320279630306&q=http://blog.bro-ids.org/feeds/posts/default&num=5 http://www.bro-ids.org/community/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6584 200 OK - - (empty) - - - - - - F9xd37OdlAikt5No5 - text/plain
+1320279630.565603 CD69521bDXIAb4IkW 192.168.2.76 52097 199.59.148.201 80 1 GET search.twitter.com /search.json?&q=#BroIDS&rpp=2&callback=jsonp1320279630304 http://www.bro-ids.org/community/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1543 200 OK - - (empty) - - - - - - FQPsz94ybCmQu6Xiq4 - text/plain
+1320279630.566430 CC3vUI3gFB04zLvWRa 192.168.2.76 52098 199.59.148.20 80 1 GET api.twitter.com /1/statuses/user_timeline.json?screen_name=Bro_IDS&count=2&include_rts=1&callback=jsonp1320279630305 http://www.bro-ids.org/community/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6095 200 OK - - (empty) - - - - - - FgGEuUcNVvhufCFR3 - text/plain
+1320279636.797267 C7Krri4g9tZfHniGXh 192.168.2.76 52099 192.150.187.43 80 1 GET www.bro-ids.org /development/index.html http://www.bro-ids.org/community/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 18428 200 OK - - (empty) - - - - - - FNbqYH3mmO41rlz20h - text/html
+1320279637.219103 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 8 GET www.google.com /uds/css/small-logo.png http://www.bro-ids.org/development/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279637.219249 CibfNy1QQW4ImDWRq5 192.168.2.76 52088 74.125.225.83 80 4 GET www.google.com /uds/css/clear.gif http://www.google.com/uds/api/search/1.0/473bb688d0c0dd605119ad983f5a4386/default+en.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279637.215608 CnGze54kQWWpKqrrZ4 192.168.2.76 52087 209.85.145.95 80 6 GET ajax.googleapis.com /ajax/services/feed/load?v=1.0&callback=jsonp1320279636956&q=http://blog.bro-ids.org/feeds/posts/default&num=5 http://www.bro-ids.org/development/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6584 200 OK - - (empty) - - - - - - FAnyyncILgPoNLSa - text/plain
+1320279637.303129 CmxyBl2c8XAMTuHEk4 192.168.2.76 52100 199.59.148.201 80 1 GET search.twitter.com /search.json?&q=#BroIDS&rpp=2&callback=jsonp1320279636954 http://www.bro-ids.org/development/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1543 200 OK - - (empty) - - - - - - F7JE7E2W4ihR5mCZhe - text/plain
+1320279637.215272 CC3vUI3gFB04zLvWRa 192.168.2.76 52098 199.59.148.20 80 2 GET api.twitter.com /1/statuses/user_timeline.json?screen_name=Bro_IDS&count=2&include_rts=1&callback=jsonp1320279636955 http://www.bro-ids.org/development/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6095 200 OK - - (empty) - - - - - - F3SK3Te5UYn22w7ji - text/plain
+1320279638.548436 CSvs6v26bQqFylkk6l 192.168.2.76 52101 192.150.187.43 80 1 GET git.bro-ids.org / http://www.bro-ids.org/development/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 10073 200 OK - - (empty) - - - - - - F4qJsD4pKcjLtpeXEa - text/html
+1320279639.050631 C4pHul1H3OeWYz7o7i 192.168.2.76 52102 192.150.187.43 80 1 GET git.bro-ids.org /static/git-logo.png http://git.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 207 200 OK - - (empty) - - - - - - F5il8p3yYIq690qvNc - image/png
+1320279639.053683 C7Lcvr4vsTf6eYpBva 192.168.2.76 52104 192.150.187.43 80 1 GET git.bro-ids.org /static/git-favicon.png - 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1150 200 OK - - (empty) - - - - - - FBCz8D1BQ3SiYOv7m9 - image/x-icon
+1320279639.047586 CSvs6v26bQqFylkk6l 192.168.2.76 52101 192.150.187.43 80 2 GET git.bro-ids.org /static/gitweb.css http://git.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 9186 200 OK - - (empty) - - - - - - FyTRixVp6ulaxUUq2 - text/plain
+1320279639.244415 CxyAKs10ppnHFP6O8i 192.168.2.76 52106 192.150.187.43 80 1 GET www-new.bro-ids.org /frames/header.html http://git.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 225 302 Found - - (empty) - - - - - - FrucKB2amhlamv0ivb - text/html
+1320279639.244463 C6MrHk2C7rLuJqhjsg 192.168.2.76 52107 192.150.187.43 80 1 GET www-new.bro-ids.org /frames/footer.html http://git.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 225 302 Found - - (empty) - - - - - - FpddCQ3BDmmGcuG9C3 - text/html
+1320279639.348046 C7Krri4g9tZfHniGXh 192.168.2.76 52099 192.150.187.43 80 2 GET www.bro-ids.org /frames/header.html http://git.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 3516 200 OK - - (empty) - - - - - - Fzea5XNhn9eNRMvx7 - text/html
+1320279639.053730 CV8faD4L1sLL5kDwN9 192.168.2.76 52103 192.150.187.43 80 1 GET git.bro-ids.org /static/gitweb.js http://git.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 24528 200 OK - - (empty) - - - - - - F983UxQF0o4kjJjuf - text/plain
+1320279639.463465 C7Krri4g9tZfHniGXh 192.168.2.76 52099 192.150.187.43 80 3 GET www.bro-ids.org /images/logo-bro-small.png http://www.bro-ids.org/frames/header.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6075 200 OK - - (empty) - - - - - - Fw6FlF4WtotJFNXmHb - image/png
+1320279639.448670 CvfUrT2DgYXXoZw9Ah 192.168.2.76 52109 192.150.187.43 80 1 GET www.bro-ids.org /frames/footer.html http://git.bro-ids.org/ 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6695 200 OK - - (empty) - - - - - - FkCp6k4tqksK3tiSy7 - text/html
+1320279639.786857 CBX0254QJoklXNbvv2 192.168.2.76 52110 199.59.148.201 80 1 GET search.twitter.com /search.json?&q=#BroIDS&rpp=2&callback=jsonp1320279639636 http://www.bro-ids.org/frames/footer.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1543 200 OK - - (empty) - - - - - - Feut0t346XEHsQ0OC7 - text/plain
+1320279672.372857 C6Ym6jvMgikT0xTTc 192.168.2.76 52111 192.150.187.43 80 1 GET www.bro-ids.org /research/index.html http://www.bro-ids.org/frames/header.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 47728 200 OK - - (empty) - - - - - - FOze0l2aT79uPyMiv7 - text/html
+1320279673.123842 CTRXSR3blXJE5ZE7Ij 192.168.2.76 52089 74.125.225.83 80 4 GET www.google.com /uds/css/clear.gif http://www.google.com/uds/api/search/1.0/473bb688d0c0dd605119ad983f5a4386/default+en.css 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279673.123121 CbNCgO1MzloHRNeY4f 192.168.2.76 52084 74.125.225.83 80 9 GET www.google.com /uds/css/small-logo.png http://www.bro-ids.org/research/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 0 304 Not Modified - - (empty) - - - - - - - - -
+1320279673.121725 CnGze54kQWWpKqrrZ4 192.168.2.76 52087 209.85.145.95 80 7 GET ajax.googleapis.com /ajax/services/feed/load?v=1.0&callback=jsonp1320279672539&q=http://blog.bro-ids.org/feeds/posts/default&num=5 http://www.bro-ids.org/research/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6584 200 OK - - (empty) - - - - - - FXEXQEMH8DrEuAdg8 - text/plain
+1320279673.204466 CJLgi92kpp2gLgGTE5 192.168.2.76 52113 199.59.148.20 80 1 GET api.twitter.com /1/statuses/user_timeline.json?screen_name=Bro_IDS&count=2&include_rts=1&callback=jsonp1320279672538 http://www.bro-ids.org/research/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 6095 200 OK - - (empty) - - - - - - FAVIuu2XZQyVznfnq8 - text/plain
+1320279673.198815 CRNn9f1zKNlzHSM5pa 192.168.2.76 52112 199.59.148.201 80 1 GET search.twitter.com /search.json?&q=#BroIDS&rpp=2&callback=jsonp1320279672537 http://www.bro-ids.org/research/index.html 1.1 Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1 0 1543 200 OK - - (empty) - - - - - - Fzjgwn8xXem3Esvk - text/plain
+#close 2017-04-16-21-36-10
diff --git a/test/logfile_bunyan.0 b/test/logfile_bunyan.0
new file mode 100644
index 0000000..facc246
--- /dev/null
+++ b/test/logfile_bunyan.0
@@ -0,0 +1,10 @@
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":20,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","msg":"Found gitlabci-include package files","time":"2023-03-24T14:26:16.243Z","v":0}
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":20,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","msg":"Found npm package files","time":"2023-03-24T14:26:16.243Z","v":0}
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":20,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","msg":"Found 3 package file(s)","time":"2023-03-24T14:26:16.243Z","v":0,"src":{"file": "/Users/trentm/tm/node-bunyan/examples/src.js","line": 20,"func": "Wuzzle.woos"}}
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":30,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","baseBranch":"main","stats":{"managers":{"gitlabci":{"fileCount":1,"depCount":1},"gitlabci-include":{"fileCount":1,"depCount":1},"npm":{"fileCount":1,"depCount":15}},"total":{"fileCount":3,"depCount":17}},"msg":"Dependency extraction complete","time":"2023-03-24T14:26:16.243Z","v":0}
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":20,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","msg":"Dependency node has unsupported/unversioned value lts-bullseye-slim (versioning=docker)","time":"2023-03-24T14:26:16.390Z","v":0}
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":20,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","depName":"prettier","check":"stabilityDays","msg":"Release 2.8.7 is pending status checks","time":"2023-03-24T14:26:17.493Z","v":0}
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":20,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","depName":"rimraf","check":"stabilityDays","msg":"Release 4.4.1 is pending status checks","time":"2023-03-24T14:26:17.897Z","v":0}
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":20,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","depName":"rimraf","bucket":"non-major","msg":"All releases are pending - using latest","time":"2023-03-24T14:26:17.897Z","v":0}
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":20,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","depName":"prettier-plugin-svelte","check":"stabilityDays","msg":"Release 2.10.0 is pending status checks","time":"2023-03-24T14:26:18.330Z","v":0}
+{"name":"renovate","hostname":"renovate-gitlab-67c4bcb5-9ggbv","pid":7,"level":20,"logContext":"qjifsaDDI","repository":"webgui/custom-icons-transformer","depName":"prettier-plugin-svelte","bucket":"non-major","msg":"All releases are pending - using latest","time":"2023-03-24T14:26:18.331Z","v":0}
diff --git a/test/logfile_cloudflare.json b/test/logfile_cloudflare.json
new file mode 100644
index 0000000..980948f
--- /dev/null
+++ b/test/logfile_cloudflare.json
@@ -0,0 +1 @@
+{"ClientIP":"87.226.160.250","ClientRequestHost":"www.ripe.net","ClientRequestMethod":"HEAD","ClientRequestURI":"/","EdgeEndTimestamp":"2022-10-30T00:00:02Z","EdgeResponseBytes":570,"EdgeResponseStatus":301,"EdgeStartTimestamp":"2022-10-30T00:00:02Z","RayID":"761fde4e984e5ab2","CacheCacheStatus":"unknown","CacheTieredFill":false,"CacheResponseBytes":0,"CacheResponseStatus":0,"FirewallMatchesActions":[],"FirewallMatchesRuleIDs":[],"FirewallMatchesSources":[],"OriginResponseBytes":0,"OriginResponseDurationMs":0,"OriginResponseHTTPExpires":"","OriginResponseHTTPLastModified":"","OriginResponseHeaderReceiveDurationMs":0,"OriginResponseStatus":0,"OriginResponseTime":0,"OriginDNSResponseTimeMs":0,"OriginIP":"","OriginRequestHeaderSendDurationMs":0,"OriginSSLProtocol":"unknown","OriginTCPHandshakeDurationMs":0,"OriginTLSHandshakeDurationMs":0,"WAFAction":"unknown","WAFFlags":"0","WAFMatchedVar":"","WAFProfile":"unknown","WAFRuleID":"","WAFRuleMessage":"","ClientASN":12389,"ClientCountry":"ru","ClientDeviceType":"desktop","ClientIPClass":"noRecord","ClientRequestBytes":567,"ClientRequestPath":"/","ClientRequestProtocol":"HTTP/1.1","ClientRequestUserAgent":"","ClientRequestReferer":"","ClientRequestScheme":"http","ClientRequestSource":"eyeball"}
diff --git a/test/logfile_crlf.0 b/test/logfile_crlf.0
new file mode 100644
index 0000000..501ee63
--- /dev/null
+++ b/test/logfile_crlf.0
@@ -0,0 +1,2 @@
+2012-07-02 10:22:40,672:DEBUG:foo bar baz
+2014-10-08 16:56:38,344:WARN:foo bar baz
diff --git a/test/logfile_cxx.0 b/test/logfile_cxx.0
new file mode 100644
index 0000000..f8de8cf
--- /dev/null
+++ b/test/logfile_cxx.0
@@ -0,0 +1 @@
+Mar 24 15:17:38.999 000000000264F I shmem.res 262144 262144 1 chassis_msg_svc/osenv::req_blocking<osenv::req_lambda<tcp_messaging_impl::register_app(svc::messaging_port, defs::atom*, defs::borrowed<svc::messaging_session>, defs::owned<svc::connection_eviction_strategy>&&, svc::messaging::connection_type, svc::messaging::app_param)::{lambda()#1}>, osenv::aloc_dynamic_named<tcp_messaging_impl::register_app(svc::messaging_port, defs::atom*, defs::borrowed<svc::messaging_session>, defs::owned<svc::connection_eviction_strategy>&&, svc::messaginconnection_type, svc::messaging::app_param)::{lambda()#1}, osenv::temporal, tcp_messaging_impl::register_app(svc::messaging_port, defs::atom*, defs::borrowed<svc::messaging_session>, des::owned<svc::connection_eviction_strategy>&&, svc::messaging::connection_type, svc::messaging::app_param)::{lambda()#1}>, osenv::req>->fiber stacks
diff --git a/test/logfile_empty.0 b/test/logfile_empty.0
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/logfile_empty.0
diff --git a/test/logfile_epoch.0 b/test/logfile_epoch.0
new file mode 100644
index 0000000..2e0197f
--- /dev/null
+++ b/test/logfile_epoch.0
@@ -0,0 +1,2 @@
+1428634687123 Hello, World!
+1428634687456 Goodbye, World!
diff --git a/test/logfile_epoch.1 b/test/logfile_epoch.1
new file mode 100644
index 0000000..645f051
--- /dev/null
+++ b/test/logfile_epoch.1
@@ -0,0 +1,2 @@
+2015-04-09 19:58:07.123000 Hello, World!
+2015-04-09 19:58:07.456000 Goodbye, World!
diff --git a/test/logfile_filter.0 b/test/logfile_filter.0
new file mode 100644
index 0000000..6a5ac12
--- /dev/null
+++ b/test/logfile_filter.0
@@ -0,0 +1,13 @@
+Dec 6 13:01:34 ubu-mac avahi-daemon[786]: Joining mDNS multicast group on interface virbr0.IPv4 with address 192.168.122.1.
+Dec 6 13:01:34 ubu-mac avahi-daemon[786]: New relevant interface virbr0.IPv4 for mDNS.
+Dec 6 13:01:34 ubu-mac avahi-daemon[786]: Registering new address record for 192.168.122.1 on virbr0.IPv4.
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: started, version 2.68 cachesize 150
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: compile time options: IPv6 GNU-getopt DBus i18n IDN DHCP DHCPv6 no-Lua TFTP conntrack ipset auth
+Dec 6 13:01:34 ubu-mac dnsmasq-dhcp[1840]: DHCP, IP range 192.168.122.2 -- 192.168.122.254, lease time 1h
+Dec 6 13:01:34 ubu-mac dnsmasq-dhcp[1840]: DHCP, sockets bound exclusively to interface virbr0
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: reading /etc/resolv.conf
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: using nameserver 192.168.1.1#53
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: read /etc/hosts - 5 addresses
+Dec 6 13:01:34 ubu-mac dnsmasq[1840]: read /var/lib/libvirt/dnsmasq/default.addnhosts - 0 addresses
+Dec 6 13:01:34 ubu-mac dnsmasq-dhcp[1840]: read /var/lib/libvirt/dnsmasq/default.hostsfile
+Dec 6 13:05:01 ubu-mac CRON[3883]: (root) CMD (command -v debian-sa1 > /dev/null && debian-sa1 1 1)
diff --git a/test/logfile_for_join.0 b/test/logfile_for_join.0
new file mode 100644
index 0000000..b811ae9
--- /dev/null
+++ b/test/logfile_for_join.0
@@ -0,0 +1,10 @@
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: New relevant interface eth0.IPv4 for mDNS.
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: Joining mDNS multicast group on interface eth0.IPv4 with address 10.1.10.103.
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: Registering new address record for 10.1.10.103 on eth0.
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: Withdrawing address record for 10.1.10.103 on eth0.
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: Leaving mDNS multicast group on interface eth0.IPv4 with address 10.1.10.103.
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: iface.c: interface_mdns_mcast_join() called but no local address available.
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: Interface eth0.IPv4 no longer relevant for mDNS.
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: New relevant interface eth0.IPv4 for mDNS.
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: Joining mDNS multicast group on interface eth0.IPv4 with address 10.1.10.103.
+Apr 28 06:53:55 tstack-centos5 avahi-daemon[2467]: Registering new address record for 10.1.10.103 on eth0.
diff --git a/test/logfile_generic.0 b/test/logfile_generic.0
new file mode 100644
index 0000000..944fe50
--- /dev/null
+++ b/test/logfile_generic.0
@@ -0,0 +1,2 @@
+2012-07-02 10:22:40,672:DEBUG:foo bar baz
+2014-10-08 16:56:38,344:WARN:foo bar baz \ No newline at end of file
diff --git a/test/logfile_generic.1 b/test/logfile_generic.1
new file mode 100644
index 0000000..173c027
--- /dev/null
+++ b/test/logfile_generic.1
@@ -0,0 +1,2 @@
+2015-04-24T21:09:29.296 25376]INFO:somemodule:Something very INFOrmative.
+2015-04-24T21:09:39.296 25376]ERROR:somemodule:Something very INFOrmative.
diff --git a/test/logfile_generic.2 b/test/logfile_generic.2
new file mode 100644
index 0000000..71cc21a
--- /dev/null
+++ b/test/logfile_generic.2
@@ -0,0 +1,2 @@
+2015-04-24T21:08:10.313913+00:00 err rbd [22968]lotuscreds:ERROR:Could not retrieve lotus account information from db
+2015-04-24T21:08:58.430632+00:00 err rbd [24206]networkutil:ERROR:The configured address sg01-1-vc1.oc.vmware.com was invalid
diff --git a/test/logfile_generic.3 b/test/logfile_generic.3
new file mode 100644
index 0000000..a883793
--- /dev/null
+++ b/test/logfile_generic.3
@@ -0,0 +1,2 @@
+20120702.102240,672:DEBUG:foo bar baz
+20141008.165638,344:WARN:foo bar baz
diff --git a/test/logfile_generic_with_header.0 b/test/logfile_generic_with_header.0
new file mode 100644
index 0000000..bea5673
--- /dev/null
+++ b/test/logfile_generic_with_header.0
@@ -0,0 +1,4 @@
+Header1: abc
+Header2: def
+2012-07-02 10:22:40,672:DEBUG:foo bar baz
+2014-10-08 16:56:38,344:WARN:foo bar baz \ No newline at end of file
diff --git a/test/logfile_glog.0 b/test/logfile_glog.0
new file mode 100644
index 0000000..ba6eb9b
--- /dev/null
+++ b/test/logfile_glog.0
@@ -0,0 +1,7 @@
+E0517 15:04:22.619632 1952452992 logging_unittest.cc:253] Log every 3, iteration 19
+I0517 15:04:22.619642 952452992 logging_unittest.cc:259] Log if every 1, iteration 19
+I0517 15:04:22.619740 52452992 logging_unittest.cc:259] Log if every 1, iteration 20
+W0517 15:04:22.619751 2452992 logging_unittest.cc:263] log_if this
+I0517 15:04:22.619760 452992 logging_unittest.cc:267] array
+I0517 15:04:22.619768 52992 logging_unittest.cc:269] const array
+E0517 15:04:22.619776 2992 logging_unittest.cc:271] foo 1000 0000001000 3e8
diff --git a/test/logfile_haproxy.0 b/test/logfile_haproxy.0
new file mode 100644
index 0000000..a9ee9e6
--- /dev/null
+++ b/test/logfile_haproxy.0
@@ -0,0 +1,17 @@
+Feb 25 16:19:38 192.168.4.2 haproxy[1]: Proxy prod_http_in started.
+Feb 25 16:19:38 192.168.4.2 haproxy[1]: Proxy tools_http_frnt started.
+Feb 25 16:19:38 192.168.4.2 haproxy[1]: Proxy git_inio_ssh_frnt started.
+Feb 25 16:19:49 192.168.4.2 haproxy[7]: 141.35.244.171:53332 [25/Feb/2019:16:19:48.143] prod_http_in~ bk_admin/nginx_sonst 0/0/7/1457/1487 200 11394 - - ---- 2/1/0/0/0 0/0 {Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /admin/colt/administration?survey=130&input_stats=1&ajax=1&&ajax=1&refresh=1&ihandle=1 HTTP/1.1"
+Feb 25 16:19:49 192.168.4.2 haproxy[7]: 92.193.212.151:59841 [25/Feb/2019:16:19:49.851] prod_http_in~ bk_ktest_kt/nginx_sonst 0/0/0/62/62 200 306 - - ---- 3/2/0/0/0 0/0 {Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36} {} "POST /portal/?Script=934&onlinetest=speichereKorrektur&anm=10347 HTTP/1.1"
+Feb 25 16:19:57 192.168.4.2 haproxy[7]: 92.193.212.151:59842 [25/Feb/2019:16:19:57.515] prod_http_in~ bk_ktest_kt/nginx_sonst 0/0/1/37/38 200 306 - - ---- 2/1/0/0/0 0/0 {Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.109 Safari/537.36} {} "POST /portal/?Script=934&onlinetest=speichereKorrektur&anm=10347 HTTP/1.1"
+Feb 25 16:20:02 192.168.4.2 haproxy[7]: 79.246.138.36:7891 [25/Feb/2019:16:20:02.367] prod_http_in~ bk_ktest_kt/nginx_sonst 0/0/1/327/328 200 306 - - ---- 1/1/0/0/0 0/0 {Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "POST /portal/?Script=934&onlinetest=speichereKorrektur&anm=13531 HTTP/1.1"
+Feb 25 16:20:04 192.168.4.2 haproxy[7]: 141.35.244.171:53337 [25/Feb/2019:16:20:03.208] prod_http_in~ bk_admin/nginx_sonst 0/0/1/1031/1039 200 11394 - - ---- 3/2/0/0/0 0/0 {Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /admin/colt/administration?survey=130&input_stats=1&ajax=1&&ajax=1&refresh=1&ihandle=1 HTTP/1.1"
+Feb 25 16:20:09 192.168.4.2 haproxy[7]: 89.247.124.65:15564 [25/Feb/2019:16:20:06.321] prod_http_in~ bk_ktest_kt/nginx_sonst 0/0/1/43/2707 200 26170 - - ---- 3/3/1/1/0 0/0 {Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /portal/?Script=934&onlinetest=korrektur&anm=13915&currentPage=0 HTTP/1.1"
+Feb 25 16:20:11 192.168.4.2 haproxy[7]: 89.247.124.65:15565 [25/Feb/2019:16:20:08.872] prod_http_in~ bk_ktest_kt/nginx_sonst 0/0/1/26/2442 200 26170 - - ---- 3/2/1/1/0 0/0 {Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /portal/?Script=934&onlinetest=korrektur&anm=13915&currentPage=0 HTTP/1.1"
+Feb 25 16:20:12 192.168.4.2 haproxy[7]: 87.183.41.77:50186 [25/Feb/2019:16:20:11.910] prod_http_in~ bk_ktest_kt/nginx_sonst 0/0/0/236/236 200 4586 - - ---- 4/4/3/3/0 0/0 {Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /portal/?Script=934&lehrer=77798 HTTP/1.1"
+Feb 25 16:20:12 192.168.4.2 haproxy[7]: 87.183.41.77:50186 [25/Feb/2019:16:20:12.234] prod_http_in~ bk_ktest_sonst/nginx_sonst 0/0/1/0/1 200 16416 - - ---- 4/4/0/0/0 0/0 {Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /media/core/bootstrap_3.3.7/css/bootstrap.css?1550939643 HTTP/1.1"
+Feb 25 16:20:12 192.168.4.2 haproxy[7]: 87.183.41.77:50186 [25/Feb/2019:16:20:12.317] prod_http_in~ bk_ktest_sonst/nginx_sonst 0/0/0/1/1 200 11065 - - ---- 9/9/0/0/0 0/0 {Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /media/core/jquery/jquery-ui-1.12.1.js?1550939557 HTTP/1.1"
+Feb 25 16:20:12 192.168.4.2 haproxy[7]: 95.216.197.33:56224 [25/Feb/2019:16:20:10.111] prod_http_in/sktst2: SSL handshake failure
+Feb 25 16:20:12 192.168.4.2 haproxy[7]: 87.183.41.77:50188 [25/Feb/2019:16:20:12.321] prod_http_in~ bk_ktest_sonst/nginx_sonst 0/0/1/0/1 200 5959 - - ---- 9/9/0/0/0 0/0 {Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /media/pi_fontawesome/css/font-awesome.css?1550939694 HTTP/1.1"
+Feb 25 16:20:12 192.168.4.2 haproxy[7]: 87.183.41.77:50187 [25/Feb/2019:16:20:12.325] prod_http_in~ bk_ktest_sonst/nginx_sonst 0/0/1/0/1 200 1859 - - ---- 9/9/0/0/0 0/0 {Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /media/pi_popup/1.1.0/magnific-popup.css?1550939704 HTTP/1.1"
+Feb 25 16:20:12 192.168.4.2 haproxy[7]: 87.183.41.77:50189 [25/Feb/2019:16:20:12.331] prod_http_in~ bk_ktest_sonst/nginx_sonst 0/0/1/0/1 200 2496 - - ---- 9/9/0/0/0 0/0 {Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0} {} "GET /media/core/core.css?1550939640 HTTP/1.1"
diff --git a/test/logfile_invalid_json.json b/test/logfile_invalid_json.json
new file mode 100644
index 0000000..f95af65
--- /dev/null
+++ b/test/logfile_invalid_json.json
@@ -0,0 +1,5 @@
+{"ts": "2013-09-06T20:00:48.124817Z", "@fields": { "lvl": "TRACE", "msg": "trace test"}}
+{"ts": "2013-09-06T20:00:49.124817Z", "@fields": { "lvl": "INFO", "msg": "Starting up service"}}
+{"ts": "2013-09-06T22:00:49.124817Z", "@fields": { "lvl": "INFO", "msg": "Shutting down service\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10
+{"ts": "2013-09-06T22:00:59.124817Z", "@fields": { "lvl": "DEBUG5", "msg": "Details..."}}
+{"ts": "2013-09-06T22:00:59.222222Z", "@fields": { "lvl": "DEBUG4", "msg": "Details..."}}
diff --git a/test/logfile_invalid_json2.json b/test/logfile_invalid_json2.json
new file mode 100644
index 0000000..ed97989
--- /dev/null
+++ b/test/logfile_invalid_json2.json
@@ -0,0 +1,3 @@
+{"ts": "2013-09-06T20:00:48.124817Z", "@fields": { "lvl": "TRACE", "msg": "trace test"}}
+{"ts": "2013-09-06T20:00:49.124817Z", "@fields": { "lvl": "INFO", "msg": "Starting up service"}}
+{"ts": "2013-09-06T22:00:49.124817Z", "@fields": { "lvl": "INFO", "msg":
diff --git a/test/logfile_journald.json b/test/logfile_journald.json
new file mode 100644
index 0000000..5654ae7
--- /dev/null
+++ b/test/logfile_journald.json
@@ -0,0 +1,2 @@
+{ "__CURSOR" : "s=e991dd5775894620914f2f7126aae6c1;i=372a;b=0dc874d3fbd44bdbb6fbcfaa27481492;m=24ea327;t=573f280a663fd;x=9ceeb77ef4331563", "__REALTIME_TIMESTAMP" : "1534860261221373", "__MONOTONIC_TIMESTAMP" : "38708007", "_BOOT_ID" : "0dc874d3fbd44bdbb6fbcfaa27481492", "PRIORITY" : "1", "_MACHINE_ID" : "1234567890abcdef1234567890abcdef", "_HOSTNAME" : "imx6ul-medusa", "SYSLOG_FACILITY" : "3", "_UID" : "0", "_GID" : "0", "_SYSTEMD_SLICE" : "system.slice", "_CAP_EFFECTIVE" : "3fffffffff", "_TRANSPORT" : "stdout", "_STREAM_ID" : "d6422e98dfb24d128e274f917f04362f", "SYSLOG_IDENTIFIER" : "python", "_PID" : "184", "_COMM" : "python", "_EXE" : "/usr/bin/python2.7", "_CMDLINE" : "/usr/bin/python -u /usr/bin/medusa/GpsLocator/GpsLocator.py", "_SYSTEMD_CGROUP" : "/system.slice/medusa-GpsLocator.service", "_SYSTEMD_UNIT" : "medusa-GpsLocator.service", "_SYSTEMD_INVOCATION_ID" : "e8fb03c1b8bf47b8ac2fc82f9870fd57", "MESSAGE" : "GPS Reference longitude: 7.358143333" }
+{ "__CURSOR" : "s=e991dd5775894620914f2f7126aae6c1;i=372b;b=0dc874d3fbd44bdbb6fbcfaa27481492;m=24ea327;t=573f280a663fd;x=26df66509c108970", "__REALTIME_TIMESTAMP" : "1534860261221373", "__MONOTONIC_TIMESTAMP" : "38708007", "_BOOT_ID" : "0dc874d3fbd44bdbb6fbcfaa27481492", "PRIORITY" : "6", "_MACHINE_ID" : "1234567890abcdef1234567890abcdef", "_HOSTNAME" : "imx6ul-medusa", "SYSLOG_FACILITY" : "3", "_UID" : "0", "_GID" : "0", "_SYSTEMD_SLICE" : "system.slice", "_CAP_EFFECTIVE" : "3fffffffff", "_TRANSPORT" : "stdout", "_STREAM_ID" : "d6422e98dfb24d128e274f917f04362f", "SYSLOG_IDENTIFIER" : "python", "_PID" : "184", "_COMM" : "python", "_EXE" : "/usr/bin/python2.7", "_CMDLINE" : "/usr/bin/python -u /usr/bin/medusa/GpsLocator/GpsLocator.py", "_SYSTEMD_CGROUP" : "/system.slice/medusa-GpsLocator.service", "_SYSTEMD_UNIT" : "medusa-GpsLocator.service", "_SYSTEMD_INVOCATION_ID" : "e8fb03c1b8bf47b8ac2fc82f9870fd57", "MESSAGE" : "GPS Reference latitude: 46.908706667" }
diff --git a/test/logfile_json.json b/test/logfile_json.json
new file mode 100644
index 0000000..55657f7
--- /dev/null
+++ b/test/logfile_json.json
@@ -0,0 +1,13 @@
+{"ts": "2013-09-06T20:00:48.124817Z", "lvl": "TRACE", "msg": "trace test"}
+{"ts": "2013-09-06T20:00:49.124817Z", "lvl": "INFO", "msg": "Starting up \u001B[0;32mservice\u001B[0m"}
+{"ts": "2013-09-06T22:00:49.124817Z", "lvl": "INFO", "msg": "Shutting down service", "user": "steve@example.com"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG5", "msg": "Details...\n"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG4", "msg": "Details...\n"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG3", "msg": "Details...\n"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG2", "msg": "Details...\n"}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG", "msg": "Details..."}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "STATS", "msg": "1 beat per second"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "WARNING", "msg": "not looking good"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "ERROR", "msg": "looking bad"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "CRITICAL", "msg": "sooo bad"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "FATAL", "msg": "shoot", "obj": { "field1" : "hi", "field2": 2 }, "arr" : ["hi", {"sub1": true}]}
diff --git a/test/logfile_json2.json b/test/logfile_json2.json
new file mode 100644
index 0000000..9a3921e
--- /dev/null
+++ b/test/logfile_json2.json
@@ -0,0 +1,3 @@
+{"ts": "2013-09-06T20:00:49.124817Z", "lvl": 0, "msg": "Starting up service", "cl": "com.exmaple.foo"}
+{"ts": "2013-09-06T22:00:49.124817Z", "lvl": 0, "msg": "Shutting down service", "user": "steve@example.com", "cl": "com.exmaple.foo"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": 10, "msg": "looking bad", "cl": "com.exmaple.foo"}
diff --git a/test/logfile_json3.json b/test/logfile_json3.json
new file mode 100644
index 0000000..3ca3e12
--- /dev/null
+++ b/test/logfile_json3.json
@@ -0,0 +1,3 @@
+{ "started_at": 1490385986240, "response": { "status": 200, "size": 443, "headers": { "server": "nginx\/1.11.10", "content-type": "application\/json", "connection": "close", "cache-control": "max-age=0, must-revalidate, no-cache, no-store, private" } }, "request": { "method": "GET", "uri": "\/example\/uri\/5", "size": "166", "querystring": {}, "headers": { "host": "example.com" } }, "client_ip": "1.1.1.1" }
+{ "started_at": 1490386367764.0, "response": { "status": 500, "size": 4433, "headers": { "server": "nginx\/1.11.10", "content-type": "application\/json", "connection": "close", "cache-control": "max-age=0, must-revalidate, no-cache, no-store, private" } }, "request": { "method": "GET", "uri": "\/example\/uri\/5", "size": "166", "querystring": {}, "headers": { "host": "example.com" } }, "client_ip": "1.1.1.1" }
+{ "started_at": 1490386531694, "response": { "status": 400, "size": 44345, "headers": { "server": "nginx\/1.11.10", "content-type": "application\/json", "connection": "close", "cache-control": "max-age=0, must-revalidate, no-cache, no-store, private" } }, "request": { "method": "GET", "uri": "\/example\/uri\/5", "size": "166", "querystring": {}, "headers": { "host": "example.com" } }, "client_ip": "1.1.1.1", "details1": {"foo": "bar"}, "details2": {"foo": "bar"}, "details3": {"foo": "bar"} }
diff --git a/test/logfile_json_subsec.json b/test/logfile_json_subsec.json
new file mode 100644
index 0000000..3ddc190
--- /dev/null
+++ b/test/logfile_json_subsec.json
@@ -0,0 +1,2 @@
+{"instant":{"epochSecond": 1663977609,"nanoOfSecond": 484000000}, "msg": "Hello, World!"}
+{"instant":{"epochSecond": 1663977619,"nanoOfSecond": 222000000}, "msg": "Goodbye, World!"}
diff --git a/test/logfile_leveltest.0 b/test/logfile_leveltest.0
new file mode 100644
index 0000000..9105fb4
--- /dev/null
+++ b/test/logfile_leveltest.0
@@ -0,0 +1,8 @@
+2016-06-30 12:00:01 trace tracemessage
+2016-06-30 12:00:02 debug debugmessage
+2016-06-30 12:00:03 debug2 debug2message
+2016-06-30 12:00:04 debug3 debug3message
+2016-06-30 12:00:05 info infomessage
+2016-06-30 12:00:06 warn warnmessage
+2016-06-30 12:00:07 fatal fatalmessage
+2016-06-30 12:00:08 invalid invalidmessage
diff --git a/test/logfile_logfmt.0 b/test/logfile_logfmt.0
new file mode 100644
index 0000000..c453b35
--- /dev/null
+++ b/test/logfile_logfmt.0
@@ -0,0 +1,5 @@
+time="2021-09-15T21:17:10.220731Z" level=error msg="error retrieving pod status: pod inc-1-enh-domain-c14-ns-2/hello-inc-1-enh-domain-c14-ns-2-3-d8f465685-k75gp is not found: PodNotFound" namespace=inc-1-enh-domain-c14-ns-2 pod=hello-inc-1-enh-domain-c14-ns-2-3-d8f465685-k75gp reason= status=Pending
+time="2021-09-15T21:17:11.674149Z" level=warning msg="Failed to DeletePod in the provider" error="pod inc-1-domain-c14-ns-6/fe-inc-1-domain-c14-ns-6-5-656d9bb695-4584b is not found: PodNotFound" namespace=inc-1-domain-c14-ns-6 pod=fe-inc-1-domain-c14-ns-6-5-656d9bb695-4584b uid=be2def59-3a08-42fd-8f84-6f64cfcefa93
+time="2021-09-15T21:17:11.678991Z" level=info msg="DeletionGracePeriodSeconds set to 0" namespace=inc-1-domain-c14-ns-6 pod=fe-inc-1-domain-c14-ns-6-5-656d9bb695-4584b uid=be2def59-3a08-42fd-8f84-6f64cfcefa93
+time="2021-09-15T21:17:11.679036Z" level=info msg="Pod deleted" namespace=inc-1-domain-c14-ns-6 pod=fe-inc-1-domain-c14-ns-6-5-656d9bb695-4584b uid=be2def59-3a08-42fd-8f84-6f64cfcefa93
+time="2021-09-15T21:18:20.335825Z" level=error msg="error retrieving pod status: pod inc-1-enh-domain-c14-ns-2/hello-inc-1-enh-domain-c14-ns-2-7-5ddd6bcd69-6rqct is not found: PodNotFound" namespace=inc-1-enh-domain-c14-ns-2 pod=hello-inc-1-enh-domain-c14-ns-2-7-5ddd6bcd69-6rqct reason= status=Pending
diff --git a/test/logfile_mixed_json2.json b/test/logfile_mixed_json2.json
new file mode 100644
index 0000000..6ddcb81
--- /dev/null
+++ b/test/logfile_mixed_json2.json
@@ -0,0 +1,18 @@
+{"ts": "2013-09-06T20:00:48.124817Z", "lvl": "TRACE", "msg": "trace test"}
+{"ts": "2013-09-06T20:00:49.124817Z", "lvl": "INFO", "msg": "Starting up service"}
+{"ts": "2013-09-06T22:00:49.124817Z", "lvl": "INFO", "msg": "Shutting down service", "user": "steve@example.com"}
+timestamp="2013-09-06T22:00:50.123000Z" level="INFO" msg="Hello, World"
+panic: foo bar failed baz
+ level1.py:10034
+ level2.py:100
+ level3.py:42
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG5", "msg": "Details..."}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG4", "msg": "Details..."}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG3", "msg": "Details..."}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG2", "msg": "Details..."}
+{"ts": "2013-09-06T22:00:59.124817Z", "lvl": "DEBUG", "msg": "Details..."}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "STATS", "msg": "1 beat per second"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "WARNING", "msg": "not looking good"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "ERROR", "msg": "looking bad"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "CRITICAL", "msg": "sooo bad"}
+{"ts": "2013-09-06T22:01:49.124817Z", "lvl": "FATAL", "msg": "shoot", "obj": { "field1" : "hi", "field2": 2 }, "arr" : ["hi", {"sub1": true}]}
diff --git a/test/logfile_multiline.0 b/test/logfile_multiline.0
new file mode 100644
index 0000000..b1c7de0
--- /dev/null
+++ b/test/logfile_multiline.0
@@ -0,0 +1,3 @@
+2009-07-20 22:59:27,672:DEBUG:Hello, World!
+ How are you today?
+2009-07-20 22:59:30,221:ERROR:Goodbye, World!
diff --git a/test/logfile_nested_json.json b/test/logfile_nested_json.json
new file mode 100644
index 0000000..6cc8072
--- /dev/null
+++ b/test/logfile_nested_json.json
@@ -0,0 +1,13 @@
+{"ts": "2013-09-06T20:00:48.124817Z", "@fields": { "lvl": "TRACE", "msg": "trace test"}}
+{"ts": "2013-09-06T20:00:49.124817Z", "@fields": { "lvl": "INFO", "msg": "Starting up service"}}
+{"ts": "2013-09-06T22:00:49.124817Z", "@fields": { "lvl": "INFO", "msg": "Shutting down service", "user": "steve@example.com"}}
+{"ts": "2013-09-06T22:00:59.124817Z", "@fields": { "lvl": "DEBUG5", "msg": "Details..."}}
+{"ts": "2013-09-06T22:00:59.124817Z", "@fields": { "lvl": "DEBUG4", "msg": "Details..."}}
+{"ts": "2013-09-06T22:00:59.124817Z", "@fields": { "lvl": "DEBUG3", "msg": "Details..."}}
+{"ts": "2013-09-06T22:00:59.124817Z", "@fields": { "lvl": "DEBUG2", "msg": "Details..."}}
+{"ts": "2013-09-06T22:00:59.124817Z", "@fields": { "lvl": "DEBUG", "msg": "Details..."}}
+{"ts": "2013-09-06T22:01:49.124817Z", "@fields": { "lvl": "STATS", "msg": "1 beat per second"}}
+{"ts": "2013-09-06T22:01:49.124817Z", "@fields": { "lvl": "WARNING", "msg": "not looking good"}}
+{"ts": "2013-09-06T22:01:49.124817Z", "@fields": { "lvl": "ERROR", "msg": "looking bad"}}
+{"ts": "2013-09-06T22:01:49.124817Z", "@fields": { "lvl": "CRITICAL", "msg": "sooo bad"}}
+{"ts": "2013-09-06T22:01:49.124817Z", "@fields": { "lvl": "FATAL", "msg": "shoot", "trace": ["line:1", "line:2"]}}
diff --git a/test/logfile_openam.0 b/test/logfile_openam.0
new file mode 100644
index 0000000..6268cdd
--- /dev/null
+++ b/test/logfile_openam.0
@@ -0,0 +1,2 @@
+"2014-06-15 01:04:52" "http://localhost:8086|/|<samlp:Response xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol"" ID=""s2daac0735bf476f4560aab81104b623bedfb0cbc0"" InResponseTo=""84cbf2be33f6410bbe55877545a93f02"" Version=""2.0"" IssueInstant=""2014-06-15T01:04:52Z"" Destination=""http://localhost:8086/api/1/rest/admin/org/530e42ccd6f45fd16d0d0717/saml/consume""><saml:Issuer xmlns:saml=""urn:oasis:names:tc:SAML:2.0:assertion"">http://openam.vagrant.dev/openam</saml:Issuer><samlp:Status xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol"">\\n<samlp:StatusCode xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol""\\nValue=""urn:oasis:names:tc:SAML:2.0:status:Success"">\\n</samlp:StatusCode>\\n</samlp:Status><saml:Assertion xmlns:saml=""urn:oasis:names:tc:SAML:2.0:assertion"" ID=""s2a0bee0da937e236167e99b209802056033816ac2"" IssueInstant=""2014-06-15T01:04:52Z"" Version=""2.0"">\\n<saml:Issuer>http://openam.vagrant.dev/openam</saml:Issuer><ds:Signature xmlns:ds=""http://www.w3.org/2000/09/xmldsig#"">\\n<ds:SignedInfo>\\n<ds:CanonicalizationMethod Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#""/>\\n<ds:SignatureMethod Algorithm=""http://www.w3.org/2000/09/xmldsig#rsa-sha1""/>\\n<ds:Reference URI=""#s2a0bee0da937e236167e99b209802056033816ac2"">\\n<ds:Transforms>\\n<ds:Transform Algorithm=""http://www.w3.org/2000/09/xmldsig#enveloped-signature""/>\\n<ds:Transform Algorithm=""http://www.w3.org/2001/10/xml-exc-c14n#""/>\\n</ds:Transforms>\\n<ds:DigestMethod Algorithm=""http://www.w3.org/2000/09/xmldsig#sha1""/>\\n<ds:DigestValue>4uSmVzjovUdQd3px/RcnoxQBsqE=</ds:DigestValue>\\n</ds:Reference>\\n</ds:SignedInfo>\\n<ds:SignatureValue>\\nhm/grge36uA6j1OWif2bTcvVTwESjmuJa27NxepW0AiV5YlcsHDl7RAIk6k/CjsSero3bxGbm56m\\nYncOEi9F1Tu7dS0bfx+vhm/kKTPgwZctf4GWn4qQwP+KeoZywbNj9ShsYJ+zPKzXwN4xBSuPjMxP\\nNf5szzjEWpOndQO/uDs=\\n</ds:SignatureValue>\\n<ds:KeyInfo>\\n<ds:X509Data>\\n<ds:X509Certificate>\\nMIICQDCCAakCBEeNB0swDQYJKoZIhvcNAQEEBQAwZzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh\\nbGlmb3JuaWExFDASBgNVBAcTC1NhbnRhIENsYXJhMQwwCgYDVQQKEwNTdW4xEDAOBgNVBAsTB09w\\nZW5TU08xDTALBgNVBAMTBHRlc3QwHhcNMDgwMTE1MTkxOTM5WhcNMTgwMTEyMTkxOTM5WjBnMQsw\\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxMLU2FudGEgQ2xhcmExDDAK\\nBgNVBAoTA1N1bjEQMA4GA1UECxMHT3BlblNTTzENMAsGA1UEAxMEdGVzdDCBnzANBgkqhkiG9w0B\\nAQEFAAOBjQAwgYkCgYEArSQc/U75GB2AtKhbGS5piiLkmJzqEsp64rDxbMJ+xDrye0EN/q1U5Of+\\nRkDsaN/igkAvV1cuXEgTL6RlafFPcUX7QxDhZBhsYF9pbwtMzi4A4su9hnxIhURebGEmxKW9qJNY\\nJs0Vo5+IgjxuEWnjnnVgHTs1+mq5QYTA7E6ZyL8CAwEAATANBgkqhkiG9w0BAQQFAAOBgQB3Pw/U\\nQzPKTPTYi9upbFXlrAKMwtFf2OW4yvGWWvlcwcNSZJmTJ8ARvVYOMEVNbsT4OFcfu2/PeYoAdiDA\\ncGy/F2Zuj8XJJpuQRSE6PtQqBuDEHjjmOQJ0rV/r8mO1ZCtHRhpZ5zYRjhRC9eCbjx9VrFax0JDC\\n/FfwWigmrW0Y0Q==\\n</ds:X509Certificate>\\n</ds:X509Data>\\n</ds:KeyInfo>\\n</ds:Signature><saml:Subject>\\n<saml:NameID Format=""urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress"" NameQualifier=""http://openam.vagrant.dev/openam"">user@example.com</saml:NameID><saml:SubjectConfirmation Method=""urn:oasis:names:tc:SAML:2.0:cm:bearer"">\\n<saml:SubjectConfirmationData InResponseTo=""84cbf2be33f6410bbe55877545a93f02"" NotOnOrAfter=""2014-06-15T01:14:52Z"" Recipient=""http://localhost:8086/api/1/rest/admin/org/530e42ccd6f45fd16d0d0717/saml/consume""/></saml:SubjectConfirmation>\\n</saml:Subject><saml:Conditions NotBefore=""2014-06-15T00:54:52Z"" NotOnOrAfter=""2014-06-15T01:14:52Z"">\\n<saml:AudienceRestriction>\\n<saml:Audience>http://localhost:8086</saml:Audience>\\n</saml:AudienceRestriction>\\n</saml:Conditions>\\n<saml:AuthnStatement AuthnInstant=""2014-06-15T01:00:25Z"" SessionIndex=""s2f9b4d4b453d12b40ef3905cc959cdb40579c2301""><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement></saml:Assertion></samlp:Response>" id=openamuser,ou=user,dc=openam 82e87195d704585501 "Not Available" INFO dc=openam "cn=dsameuser,ou=DSAME Users,dc=openam" SAML2-37 SAML2.access user@example.com 192.168.33.1
+"2014-06-15 01:04:52" vagrant|/ "cn=dsameuser,ou=DSAME Users,dc=openam" ec5708a7f199678a01 "Not Available" FINE dc=openam "cn=dsameuser,ou=DSAME Users,dc=openam" COT-22 COT.access "Not Available" 127.0.1.1
diff --git a/test/logfile_plain.0 b/test/logfile_plain.0
new file mode 100644
index 0000000..13bc727
--- /dev/null
+++ b/test/logfile_plain.0
@@ -0,0 +1,3 @@
+Hello, World!
+How are you?
+Goodbye, World!
diff --git a/test/logfile_pretty.0 b/test/logfile_pretty.0
new file mode 100644
index 0000000..f91b054
--- /dev/null
+++ b/test/logfile_pretty.0
@@ -0,0 +1,23 @@
+Apr 7 00:49:42 Tim-Stacks-iMac kernel[0]: Ethernet [AppleBCM5701Ethernet]: Link up on en0, 1-Gigabit, Full-duplex, Symmetric flow-control, Debug [796d,2301,0de1,0300,cde1,3800]
+Apr 7 05:49:53 Tim-Stacks-iMac.local GoogleSoftwareUpdateDaemon[17212]: -[KSUpdateCheckAction performAction] KSUpdateCheckAction running KSServerUpdateRequest: <KSOmahaServerUpdateRequest:0x511f30
+ server=<KSOmahaServer:0x510d80>
+ url="https://tools.google.com/service/update2"
+ runningFetchers=0
+ tickets=1
+ activeTickets=1
+ rollCallTickets=1
+ body=
+ <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+ <o:gupdate xmlns:o="http://www.google.com/update2/request" protocol="2.0" version="KeystoneDaemon-1.2.0.7709" ismachine="1" requestid="{0DFDBCD1-5E29-4DFC-BD99-31A2397198FE}">
+ <o:os platform="mac" version="MacOSX" sp="10.10.2_x86_64h"></o:os>
+ <o:app appid="com.google.Keystone" version="1.2.0.7709" lang="en-us" installage="180" brand="GGLG">
+ <o:ping r="1" a="1"></o:ping>
+ <o:updatecheck></o:updatecheck>
+ </o:app>
+ </o:gupdate>
+ >
+Apr 7 07:31:56 Tim-Stacks-iMac.local VirtualBox[36403]: WARNING: The Gestalt selector gestaltSystemVersion is returning 10.9.2 instead of 10.10.2. Use NSProcessInfo's operatingSystemVersion property to get correct system version number.
+ Call location:
+Apr 7 07:31:56 Tim-Stacks-iMac.local VirtualBox[36403]: 0 CarbonCore 0x00007fff8a9b3d9b ___Gestalt_SystemVersion_block_invoke + 113
+Apr 7 07:31:56 Tim-Stacks-iMac.local VirtualBox[36403]: 1 libdispatch.dylib 0x00007fff8bc84c13 _dispatch_client_callout + 8
+Apr 7 07:32:56 Tim-Stacks-iMac.local logger[234]: Bad data { abc, 123, 456 )}]
diff --git a/test/logfile_procstate.0 b/test/logfile_procstate.0
new file mode 100644
index 0000000..43878aa
--- /dev/null
+++ b/test/logfile_procstate.0
@@ -0,0 +1,43 @@
+========== Start of system state dump at Thu Jun 2 00:01:01 UTC 2022 ==========
+
+/bin/ps auxww
+USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
+root 1 0.0 0.0 158392 7792 ? Ss Jun01 0:14 /lib/systemd/systemd --switched-root --system --deserialize 16
+root 2 0.0 0.0 0 0 ? S Jun01 0:00 [kthreadd]
+root 3 0.0 0.0 0 0 ? I< Jun01 0:00 [rcu_gp]
+root 4 0.0 0.0 0 0 ? I< Jun01 0:00 [rcu_par_gp]
+root 6 0.0 0.0 0 0 ? I< Jun01 0:00 [kworker/0:0H-kblockd]
+========== End of system state dump ==========
+
+
+========== Start of system state dump at Thu Jun 2 00:02:01 UTC 2022 ==========
+
+/bin/ps auxww
+USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
+root 1 0.0 0.0 158392 7792 ? Ss Jun01 0:14 /lib/systemd/systemd --switched-root --system --deserialize 16
+root 2 0.0 0.0 0 0 ? S Jun01 0:00 [kthreadd]
+root 3 0.0 0.0 0 0 ? I< Jun01 0:00 [rcu_gp]
+root 4 0.0 0.0 0 0 ? I< Jun01 0:00 [rcu_par_gp]
+root 6 0.0 0.0 0 0 ? I< Jun01 0:00 [kworker/0:0H-kblockd]
+root 8 0.0 0.0 0 0 ? I< Jun01 0:00 [mm_percpu_wq]
+root 9 0.0 0.0 0 0 ? S Jun01 0:00 [ksoftirqd/0]
+root 10 0.0 0.0 0 0 ? I Jun01 0:23 [rcu_sched]
+root 11 0.0 0.0 0 0 ? I Jun01 0:00 [rcu_bh]
+root 12 0.0 0.0 0 0 ? S Jun01 0:00 [migration/0]
+root 14 0.0 0.0 0 0 ? S Jun01 0:00 [cpuhp/0]
+========== End of system state dump ==========
+
+
+========== Start of system state dump at Thu Jun 2 00:03:01 UTC 2022 ==========
+
+/bin/ps auxww
+USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
+root 1 0.0 0.0 158392 7792 ? Ss Jun01 0:14 /lib/systemd/systemd --switched-root --system --deserialize 16
+root 2 0.0 0.0 0 0 ? S Jun01 0:00 [kthreadd]
+root 3 0.0 0.0 0 0 ? I< Jun01 0:00 [rcu_gp]
+root 4 0.0 0.0 0 0 ? I< Jun01 0:00 [rcu_par_gp]
+root 6 0.0 0.0 0 0 ? I< Jun01 0:00 [kworker/0:0H-kblockd]
+root 8 0.0 0.0 0 0 ? I< Jun01 0:00 [mm_percpu_wq]
+root 9 0.0 0.0 0 0 ? S Jun01 0:00 [ksoftirqd/0]
+========== End of system state dump ==========
+
diff --git a/test/logfile_rollover.0 b/test/logfile_rollover.0
new file mode 100644
index 0000000..f37fbab
--- /dev/null
+++ b/test/logfile_rollover.0
@@ -0,0 +1,6 @@
+00:00:00.000 foo DEBUG some super duper message
+01:00:00.000 foo DEBUG some super duper message
+02:00:00.000 foo DEBUG some super duper message
+03:00:00.000 foo DEBUG some super duper message
+00:00:00.000 bar DEBUG some super duper message next day (only one hour)
+00:01:00.000 bar DEBUG some super duper message next day (only one hour)
diff --git a/test/logfile_rollover.1 b/test/logfile_rollover.1
new file mode 100644
index 0000000..2a54342
--- /dev/null
+++ b/test/logfile_rollover.1
@@ -0,0 +1,5 @@
+Nov 3 09:23:38 veridian automount[7998]: lookup(file): lookup for foobar failed
+Nov 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt
+Nov 3 09:23:38 veridian automount[7999]: lookup(file): lookup for opt failed
+Nov 3 09:47:02 veridian sudo: timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
+Feb 3 09:23:38 veridian automount[7998]: lookup(file): lookup for foobar failed
diff --git a/test/logfile_strace_log.0 b/test/logfile_strace_log.0
new file mode 100644
index 0000000..cc74c00
--- /dev/null
+++ b/test/logfile_strace_log.0
@@ -0,0 +1,9 @@
+08:09:33.814936 execve("/bin/ls", ["ls"], [/* 38 vars */]) = 0 <0.000264>
+08:09:33.815688 brk(0) = 0x1513000 <0.000016>
+08:09:33.815801 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9100b05000 <0.000019>
+08:09:33.815943 access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory) <0.000019>
+08:09:33.816083 mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f9100b03000 <0.000018>
+08:09:33.816206 access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory) <0.000019>
+08:09:33.816326 open("/etc/ld.so.cache", O_RDONLY) = 3 <0.000023>
+08:09:33.816428 fstat(3, {st_mode=S_IFREG|0644, st_size=102143, ...}) = 0 <0.000015>
+08:09:33.816577 mmap(NULL, 102143, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f9100aea000 <0.000019>
diff --git a/test/logfile_syslog.0 b/test/logfile_syslog.0
new file mode 100644
index 0000000..312fa69
--- /dev/null
+++ b/test/logfile_syslog.0
@@ -0,0 +1,4 @@
+Nov 3 09:23:38 veridian automount[7998]: lookup(file): lookup for foobar failed
+Nov 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt
+Nov 3 09:23:38 veridian automount[7999]: lookup(file): lookup for opt failed
+Nov 3 09:47:02 veridian sudo: timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
diff --git a/test/logfile_syslog.1 b/test/logfile_syslog.1
new file mode 100644
index 0000000..0794a4d
--- /dev/null
+++ b/test/logfile_syslog.1
@@ -0,0 +1,4 @@
+Dec 3 09:23:38 veridian automount[7998]: lookup(file): lookup for foobar failed
+Dec 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt
+Dec 3 09:23:38 veridian automount[7999]: lookup(file): lookup for opt failed
+Jan 3 09:47:02 veridian sudo: timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
diff --git a/test/logfile_syslog.2 b/test/logfile_syslog.2
new file mode 100644
index 0000000..5f43947
--- /dev/null
+++ b/test/logfile_syslog.2
@@ -0,0 +1,3 @@
+Nov 3 09:23:38 veridian foo[7998]: eth0 is up
+Nov 3 09:23:38 veridian foo[16442]: eth1 is up
+Nov 3 09:23:38 veridian foo[7999]: eth0 is down
diff --git a/test/logfile_syslog.3 b/test/logfile_syslog.3
new file mode 100644
index 0000000..05d1a0b
--- /dev/null
+++ b/test/logfile_syslog.3
@@ -0,0 +1,17 @@
+Aug 16 00:10:45 Tims-Air syslogd[314]: ASL Sender Statistics
+Aug 16 00:28:56 Tims-Air syslogd[314]: ASL Sender Statistics
+Aug 16 00:32:15 Tims-Air syslogd[314]: Configuration Notice:
+ ASL Module "com.apple.cdscheduler" claims selected messages.
+ Those messages may not appear in standard system log files or in the ASL database.
+Aug 16 00:32:15 Tims-Air syslogd[314]: Configuration Notice:
+ ASL Module "com.apple.install" claims selected messages.
+ Those messages may not appear in standard system log files or in the ASL database.
+Aug 16 00:32:15 Tims-Air syslogd[314]: Configuration Notice:
+ ASL Module "com.apple.authd" sharing output destination "/var/log/asl" with ASL Module "com.apple.asl".
+ Output parameters from ASL Module "com.apple.asl" override any specified in ASL Module "com.apple.authd".
+Aug 16 00:32:15 Tims-Air syslogd[314]: Configuration Notice:
+ ASL Module "com.apple.authd" sharing output destination "/var/log/system.log" with ASL Module "com.apple.asl".
+ Output parameters from ASL Module "com.apple.asl" override any specified in ASL Module "com.apple.authd".
+Aug 16 00:32:15 Tims-Air syslogd[314]: Configuration Notice:
+ ASL Module "com.apple.authd" claims selected messages.
+ Those messages may not appear in standard system log files or in the ASL database.
diff --git a/test/logfile_syslog_fr.0 b/test/logfile_syslog_fr.0
new file mode 100644
index 0000000..8b93e0c
--- /dev/null
+++ b/test/logfile_syslog_fr.0
@@ -0,0 +1 @@
+août 19 11:08:37 nlaptop symphorien[4961]: test
diff --git a/test/logfile_syslog_with_access_log.0 b/test/logfile_syslog_with_access_log.0
new file mode 100644
index 0000000..2ec28da
--- /dev/null
+++ b/test/logfile_syslog_with_access_log.0
@@ -0,0 +1,5 @@
+Jan 3 09:47:02 veridian sudo: timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
+Mar 24 14:02:50 bigproduct-web1 tomcat.log: 127.0.0.1 - - "GET /includes/js/combined-javascript.js HTTP/1.1" 200 65508 4.386
+Mar 24 14:02:50 bigproduct-web1 tomcat.log: 127.0.0.1 - - "GET /bad.foo HTTP/1.1" 404 65508 4.386
+Mar 24 14:02:51 bigproduct-web1 automount[7999]: lookup(file): lookup for opt failed
+Mar 24 14:26:01 --- last message repeated 2 times ---
diff --git a/test/logfile_syslog_with_header.0 b/test/logfile_syslog_with_header.0
new file mode 100644
index 0000000..8f22c16
--- /dev/null
+++ b/test/logfile_syslog_with_header.0
@@ -0,0 +1,6 @@
+Header1: abc
+Header2: def
+Nov 3 09:23:38 veridian automount[7998]: lookup(file): lookup for foobar failed
+Nov 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt
+Nov 3 09:23:38 veridian automount[7999]: lookup(file): lookup for opt failed
+Nov 3 09:47:02 veridian sudo: timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
diff --git a/test/logfile_syslog_with_mixed_times.0 b/test/logfile_syslog_with_mixed_times.0
new file mode 100644
index 0000000..f2314ae
--- /dev/null
+++ b/test/logfile_syslog_with_mixed_times.0
@@ -0,0 +1,13 @@
+Sep 13 00:58:45 Tim-Stacks-iMac kernel[0]: AirParrot device perform power state change 0 -> 1
+Sep 13 00:59:30 Tim-Stacks-iMac.local airportd[59]: _configureScanOffloadParameters: Unable to configure scan offloading on en1 (Device power is off)
+Sep 13 01:23:54 Tim-Stacks-iMac kernel[0]: RTC: PowerByCalendarDate setting ignored
+Sep 13 03:12:04 Tim-Stacks-iMac kernel[0]: vm_compressor_record_warmup (9478314 - 9492476)
+Sep 13 03:12:04 Tim-Stacks-iMac kernel[0]: AppleBCM5701Ethernet [en0]: 0 0 memWrInd fBJP_Wakeup_Timer
+Sep 13 01:25:39 Tim-Stacks-iMac kernel[0]: AppleThunderboltNHIType2::waitForOk2Go2Sx - retries = 60000
+Sep 13 03:12:04 Tim-Stacks-iMac kernel[0]: hibernate_page_list_setall(preflight 0) start 0xffffff8428276000, 0xffffff8428336000
+Sep 13 03:12:58 Tim-Stacks-iMac kernel[0]: *** kernel exceeded 500 log message per second limit - remaining messages this second discarded ***
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: IOThunderboltSwitch<0xffffff803f4b3000>(0x0)::listenerCallback - Thunderbolt HPD packet for route = 0x0 port = 11 unplug = 0
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: vm_compressor_flush - starting
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: AppleBCM5701Ethernet [en0]: 0 0 memWrInd fBJP_Wakeup_Timer
+Sep 13 03:13:16 Tim-Stacks-iMac kernel[0]: AppleThunderboltNHIType2::waitForOk2Go2Sx - retries = 60000
+Sep 13 03:46:03 Tim-Stacks-iMac kernel[0]: hibernate_page_list_setall(preflight 0) start 0xffffff838f1fc000, 0xffffff838f2bc000
diff --git a/test/logfile_tai64n.0 b/test/logfile_tai64n.0
new file mode 100644
index 0000000..0e195bc
--- /dev/null
+++ b/test/logfile_tai64n.0
@@ -0,0 +1,10 @@
+@40000000433225833b6e1a8c tcpserver: status: 5/30
+@40000000433225833b6e2644 tcpserver: pid 26162 from 194.206.24.40
+@40000000433225840c85ba04 tcpserver: ok 26162 a.mx.jms1.net:209.114.200.128:25 :194.206.24.40::1521
+@40000000433225840c8f0cbc rblsmtpd: 194.206.24.40 pid 26162: 451 We do not accept mail from IP addresses without reverse DNS.
+@40000000433225852a9ada4c tcpserver: status: 6/30
+@40000000433225852a9ae604 tcpserver: pid 26163 from 193.123.2.227
+@40000000433225852aa997bc tcpserver: ok 26163 a.mx.jms1.net:209.114.200.128:25 pixelwww.pixelpower.com:193.123.2.227::4232
+@40000000433225852aa9a374 rblsmtpd: 193.123.2.227 pid 26163: 553 Sent mail to honeypot qmmycemglyiuq@delete.net on 2005-01-04
+@400000004332258538eae27c tcpserver: end 26163 status 0
+@400000004332258538eaea4c tcpserver: status: 5/30
diff --git a/test/logfile_tcf.0 b/test/logfile_tcf.0
new file mode 100644
index 0000000..b6034b4
--- /dev/null
+++ b/test/logfile_tcf.0
@@ -0,0 +1,30 @@
+TCF 29:47.191: Server-Properties: {"Name":"TCF Protocol Logger","OSName":"Linux 3.2.0-60-generic","UserName":"xavier","AgentID":"1fde3dd1-d4be-4f79-8090-6f8d212f03bf","TransportName":"TCP","Proxy":"","ValueAdd":"1","Port":"1534"}
+TCF 30:04.735: channel server connecting
+TCF 30:04.735: channel server connected
+TCF 30:11.474: 0: ---> C 2 RunControl getChildren null <eom>
+TCF 30:11.475: 0: <--- R 2 ["P1"] <eom>
+TCF 30:11.475: 0: ---> C 3 RunControl getContext "P1" <eom>
+TCF 30:11.475: 0: <--- R 3 {"ID":"P1","ProcessID":"P1","Name":"VxWorks","CanSuspend":true,"CanResume":1,"IsContainer":true,"WordSize":4,"CanTerminate":true,"CanDetach":true,"RCGroup":"P1","SymbolsGroup":"P1","CPUGroup":"P1","DiagnosticTestProcess":true} <eom>
+TCF 30:11.475: 0: ---> C 4 RunControl getChildren "P1" <eom>
+TCF 30:11.475: 0: <--- R 4 ["P2"] <eom>
+TCF 30:11.475: 0: ---> C 5 RunControl getContext "P2" <eom>
+TCF 30:11.476: 0: <--- R 5 {"ID":"P2","ParentID":"P1","ProcessID":"P2","Name":"Kernel","CanSuspend":true,"CanResume":1,"IsContainer":true,"WordSize":4,"CanTerminate":true,"CanDetach":true,"RCGroup":"P2","BPGroup":"P2","SymbolsGroup":"P2","CPUGroup":"P2","DiagnosticTestProcess":true} <eom>
+TCF 30:11.476: 0: ---> C 6 RunControl getChildren "P2" <eom>
+TCF 30:11.476: 0: <--- R 6 [] <eom>
+TCF 30:11.523: 0: ---> C RR4 RunControl getChildren null <eom>
+TCF 30:11.524: 0: <--- R RR4 ["P1"] <eom>
+TCF 30:11.525: 0: ---> C RR6 RunControl getContext "P1" <eom>
+TCF 30:11.526: 0: <--- R RR6 {"ID":"P1","ProcessID":"P1","Name":"VxWorks","CanSuspend":true,"CanResume":1,"IsContainer":true,"WordSize":4,"CanTerminate":true,"CanDetach":true,"RCGroup":"P1","SymbolsGroup":"P1","CPUGroup":"P1","DiagnosticTestProcess":true} <eom>
+TCF 30:11.530: 0: ---> C RR7 RunControl getChildren "P1" <eom>
+TCF 30:11.530: 0: <--- R RR7 ["P2"] <eom>
+TCF 30:11.531: 0: ---> C RR8 RunControl getContext "P2" <eom>
+TCF 30:11.531: 0: <--- R RR8 {"ID":"P2","ParentID":"P1","ProcessID":"P2","Name":"Kernel","CanSuspend":true,"CanResume":1,"IsContainer":true,"WordSize":4,"CanTerminate":true,"CanDetach":true,"RCGroup":"P2","BPGroup":"P2","SymbolsGroup":"P2","CPUGroup":"P2","DiagnosticTestProcess":true} <eom>
+TCF 30:11.533: 0: ---> C RR9 RunControl getChildren "P2" <eom>
+TCF 30:11.533: 0: <--- R RR9 [] <eom>
+TCF 30:11.536: 0: ---> C RR10 Locator getAgentID <eom>
+TCF 30:11.536: 0: <--- R RR10 "00000000-0000-4daa-8665-a704d3dc0000" <eom>
+TCF 30:21.573: 0: ---> C RR11 ProcessesV1 getChildren null false <eom>
+TCF 30:21.573: 0: <--- R RR11 ["P1"] <eom>
+TCF 30:21.577: 0: ---> C RR12 ProcessesV1 getContext "P1" <eom>
+TCF 30:21.577: 0: <--- R RR12 {"Name":"VxWorks","CanTerminate":false,"CanAttach":true,"IsProcess":false,"ID":"P1"} <eom>
+TCF 30:21.756: 0: <--- E RunControl contextAdded [{"ID":"P2.1623333904","ParentID":"P2","ProcessID":"P2","Name":"ipcom_syslogd","CanSuspend":true,"CanResume":12351,"HasState":true,"WordSize":4,"CanTerminate":true,"CanDetach":true,"RCGroup":"P2.1623333904","BPGroup":"P2","SymbolsGroup":"P2","CPUGroup":"P2","DiagnosticTestProcess":true}] <eom>
diff --git a/test/logfile_tcf.1 b/test/logfile_tcf.1
new file mode 100644
index 0000000..eb1b565
--- /dev/null
+++ b/test/logfile_tcf.1
@@ -0,0 +1,3 @@
+TCF 59:47.191234: Server-Properties: {"Name":"TCF Protocol Logger","OSName":"Linux 3.2.0-60-generic","UserName":"xavier","AgentID":"1fde3dd1-d4be-4f79-8090-6f8d212f03bf","TransportName":"TCP","Proxy":"","ValueAdd":"1","Port":"1534"}
+TCF 30:11.474442: 0: ---> C 2 RunControl getChildren null <eom>
+TCF 01:11.475557: 0: <--- R 2 ["P1"] <eom>
diff --git a/test/logfile_tcsh_history.0 b/test/logfile_tcsh_history.0
new file mode 100644
index 0000000..465d851
--- /dev/null
+++ b/test/logfile_tcsh_history.0
@@ -0,0 +1,4 @@
+#+1162490366
+./drive_vt52_curses
+#+1162490385
+exit
diff --git a/test/logfile_uwsgi.0 b/test/logfile_uwsgi.0
new file mode 100644
index 0000000..df31323
--- /dev/null
+++ b/test/logfile_uwsgi.0
@@ -0,0 +1,19 @@
+[pid: 88185|app: 0|req: 1/1] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:49:12 2016] POST /update_metrics => generated 47 bytes in 129 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 3)
+[pid: 88185|app: 0|req: 3/2] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:49:15 2016] POST /update_metrics => generated 47 bytes in 35 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 30)
+[pid: 88185|app: 0|req: 3/3] 127.0.0.1 () {34 vars in 617 bytes} [Sun Mar 13 22:49:15 2016] POST /endpoint2 => generated 215 bytes in 68 micros (HTTP/1.1 200) 9 headers in 373 bytes (1 switches on core 8)
+[pid: 88185|app: 0|req: 4/4] 127.0.0.1 () {34 vars in 617 bytes} [Sun Mar 13 22:49:15 2016] POST /endpoint2 => generated 215 bytes in 16 msecs (HTTP/1.1 200) 9 headers in 373 bytes (1 switches on core 22)
+[pid: 88185|app: 0|req: 5/5] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:50:12 2016] POST /update_metrics => generated 47 bytes in 10 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 0)
+[pid: 88186|app: 0|req: 1/6] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:50:15 2016] POST /update_metrics => generated 47 bytes in 65 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 16)
+[pid: 88186|app: 0|req: 2/7] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:51:12 2016] POST /update_metrics => generated 47 bytes in 11 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 30)
+[pid: 88188|app: 0|req: 1/8] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:51:15 2016] POST /update_metrics => generated 47 bytes in 66 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 31)
+[pid: 88186|app: 0|req: 3/9] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:52:12 2016] POST /update_metrics => generated 47 bytes in 11 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 81)
+[pid: 88188|app: 0|req: 2/10] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:52:15 2016] POST /update_metrics => generated 47 bytes in 18 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 38)
+[pid: 88187|app: 0|req: 1/11] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:53:12 2016] POST /update_metrics => generated 47 bytes in 107 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 7)
+[pid: 88187|app: 0|req: 2/12] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:53:15 2016] POST /update_metrics => generated 47 bytes in 17 msecs (HTTP/1.1 200) 9 headers in 378 bytes (2 switches on core 8)
+[pid: 88187|app: 0|req: 3/13] 127.0.0.1 () {38 vars in 695 bytes} [Sun Mar 13 22:54:12 2016] POST /update_metrics => generated 47 bytes in 16 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 9)
+[pid: 88188|app: 0|req: 3/14] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:54:15 2016] POST /update_metrics => generated 47 bytes in 52 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 1)
+[pid: 88186|app: 0|req: 4/15] 127.0.0.1 () {34 vars in 617 bytes} [Sun Mar 13 22:54:15 2016] POST /endpoint2 => generated 215 bytes in 35 msecs (HTTP/1.1 200) 9 headers in 373 bytes (1 switches on core 43)
+[pid: 88187|app: 0|req: 4/16] 127.0.0.1 () {38 vars in 695 bytes} [Sun Mar 13 22:55:12 2016] POST /update_metrics => generated 47 bytes in 11 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 14)
+[pid: 88188|app: 0|req: 4/17] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:55:15 2016] POST /update_metrics => generated 47 bytes in 14 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 57)
+[pid: 88187|app: 0|req: 5/18] 127.0.0.1 () {38 vars in 695 bytes} [Sun Mar 13 22:56:12 2016] POST /update_metrics => generated 47 bytes in 11 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 35)
+[pid: 88186|app: 0|req: 5/19] 127.0.0.1 () {38 vars in 696 bytes} [Sun Mar 13 22:56:15 2016] POST /update_metrics => generated 47 bytes in 40 msecs (HTTP/1.1 200) 9 headers in 378 bytes (1 switches on core 60)
diff --git a/test/logfile_vami.0 b/test/logfile_vami.0
new file mode 100644
index 0000000..23b7b31
--- /dev/null
+++ b/test/logfile_vami.0
@@ -0,0 +1,4 @@
+2015-03-12T23:16:52.071:INFO:com.root:Response :
+ <?xml version="1.0"?>
+<response><locale>en-US</locale><requestid>ipInfo</requestid><value id="ipv4Gateway" actions="enabled">198.51.100.253</value><value id="ipv6Gateway" actions="enabled"/><value id="ipv6Enabled" actions="enabled">true</value><value id="ipv4Enabled" actions="enabled">true</value><value id="name" actions="enabled">nic1</value><value id="v4config" actions="enabled"><value id="defaultGateway" actions="enabled">0.0.0.0</value><value id="updateable" actions="enabled">True</value><value id="prefix" actions="enabled">22</value><value id="mode" actions="enabled">dhcp</value><value id="address" actions="enabled">198.51.100.110</value><value id="interface" actions="enabled">nic1</value></value><value id="v6config" actions="enabled"><value id="defaultGateway" actions="enabled">fe80::214:f609:19f7:6bf1</value><value id="updateable" actions="enabled">True</value><value id="interface" actions="enabled">nic1</value><value id="dhcp" actions="enabled">False</value><value id="autoconf" actions="enabled">False</value><value id="addresses" actions="enabled"><value id="origin" actions="enabled">other</value><value id="status" actions="enabled">preferred</value><value id="prefix" actions="enabled">64</value><value id="address" actions="enabled">fe80::250:56ff:feaa:5abf</value></value></value><value id="interfaceInfo" actions="enabled"><value id="status" actions="enabled">up</value><value id="mac" actions="enabled">00:50:56:aa:5a:bf</value><value id="name" actions="enabled">nic1</value></value></response>
+
diff --git a/test/logfile_vdsm.0 b/test/logfile_vdsm.0
new file mode 100644
index 0000000..e36910e
--- /dev/null
+++ b/test/logfile_vdsm.0
@@ -0,0 +1,16 @@
+MainThread::INFO::2011-12-05 07:04:56,101::vdsm::71::vds::(run) I am the actual vdsm 4.9-0
+MainThread::ERROR::2011-12-05 07:04:56,300::vdsm::74::vds::(run) Traceback (most recent call last):
+File "/usr/share/vdsm/vdsm", line 72, in run
+ serve_clients(log)
+File "/usr/share/vdsm/vdsm", line 40, in serve_clients
+ cif = clientIF.clientIF(log)
+File "/usr/share/vdsm/clientIF.py", line 111, in init
+ self._libvirt = libvirtconnection.get()
+File "/usr/share/vdsm/libvirtconnection.py", line 111, in get
+ conn = libvirt.openAuth('qemu:///system', auth, 0)
+File "/usr/lib64/python2.6/site-packages/libvirt.py", line 102, in openAuth
+ if ret is None:raise libvirtError('virConnectOpenAuth() failed')
+libvirtError: Failed to connect socket to '/var/run/libvirt/libvirt-sock': No such file or directory
+Thread-15::DEBUG::2011-12-07 11:44:17,737::clientIF::54::vds::(wrapper) [10.35.17.240]::call getVdsCapabilities with () {}
+Thread-16::DEBUG::2011-06-23 19:03:11,607::clientIF::225::Storage.Dispatcher.Protect::(wrapper) [10.35.16.71]
+Thread-1950::INFO::2011-12-07 12:14:15,018::dispatcher::94::Storage.Dispatcher.Protect::(run) Run and protect: getDeviceList, args: ( storageType=2)
diff --git a/test/logfile_vmw_log.0 b/test/logfile_vmw_log.0
new file mode 100644
index 0000000..99e1c91
--- /dev/null
+++ b/test/logfile_vmw_log.0
@@ -0,0 +1,4 @@
+2022-06-02T11:58:12.193Z info vpxd[45715] [Originator@6876 sub=vpxLro opID=7e1280cf] [VpxLRO] -- BEGIN lro-846063 -- SessionManager -- vim.SessionManager.sessionIsActive -- 528e6e0c-246d-58b5-3234-278c6e0c5d0d(52c289ac-2563-48d5-8a8e-f178da022c0d)
+2022-06-02T11:58:12.194Z info vpxd[45715] [Originator@6876 sub=vpxLro opID=7e1280cf] [VpxLRO] -- FINISH lro-846063
+2022-06-02T11:59:41.498Z warning vpxd[47756] [Originator@6876 sub=drmLogger opID=SWI-66b629ff] Exception was thrown when call vsan-performance-manager for cluster [vim.ClusterComputeResource:domain-c109,Cluster-52] perf metrics: N3Vim5Fault8NotFound9ExceptionE(Fault cause: vim.fault.NotFound
+--> )
diff --git a/test/logfile_vpxd.0 b/test/logfile_vpxd.0
new file mode 100644
index 0000000..224ca7b
--- /dev/null
+++ b/test/logfile_vpxd.0
@@ -0,0 +1,12 @@
+2022-06-02T11:58:12.193Z info vpxd[45715] [Originator@6876 sub=vpxLro opID=7e1280cf] [VpxLRO] -- BEGIN lro-846063 -- SessionManager -- vim.SessionManager.sessionIsActive -- 528e6e0c-246d-58b5-3234-278c6e0c5d0d(52c289ac-2563-48d5-8a8e-f178da022c0d)
+2022-06-02T11:58:12.194Z info vpxd[45715] [Originator@6876 sub=vpxLro opID=7e1280cf] [VpxLRO] -- FINISH lro-846063
+2022-06-02T11:58:12.376Z info vpxd[45709] [Originator@6876 sub=vpxLro opID=e3979f6] [VpxLRO] -- BEGIN lro-846064 -- SessionManager -- vim.SessionManager.sessionIsActive -- 52626140-422b-6287-b4e4-344192c6a01d(523e0a4b-6e83-6bcd-9342-22502dd89866)
+2022-06-02T11:58:12.377Z info vpxd[45709] [Originator@6876 sub=vpxLro opID=e3979f6] [VpxLRO] -- FINISH lro-846064
+2022-06-02T11:58:12.623Z info vpxd[47524] [Originator@6876 sub=vpxLro opID=l3wrhr4o-cbf-h5:70001034-60] [VpxLRO] -- BEGIN lro-846066 -- ChangeLogCollector -- vim.cdc.ChangeLogCollector.waitForChanges -- 526861fc-0c28-1930-ae5e-d8c2772bf8c2(52a7a308-9646-c054-f1e7-16131c1a7db6)
+2022-06-02T11:58:12.623Z info vpxd[47524] [Originator@6876 sub=vpxLro opID=l3wrhr4o-cbf-h5:70001034-60] [VpxLRO] -- FINISH lro-846066
+2022-06-02T11:58:12.736Z info vpxd[48432] [Originator@6876 sub=vpxLro opID=499b440] [VpxLRO] -- BEGIN lro-846067 -- SessionManager -- vim.SessionManager.sessionIsActive -- 521fe9f6-d061-11a2-ac86-badb3c071373(524cba9b-2cc4-9b70-32e4-421452a404d7)
+2022-06-02T11:58:12.736Z info vpxd[48432] [Originator@6876 sub=vpxLro opID=499b440] [VpxLRO] -- FINISH lro-846067
+2022-06-02T11:58:12.740Z info vpxd[48035] [Originator@6876 sub=vpxLro opID=55a419df] [VpxLRO] -- BEGIN lro-846068 -- SessionManager -- vim.SessionManager.sessionIsActive -- 52585600-b0bc-76b1-c4d5-4d7708671c5e(523b68ba-e312-9909-a3ca-39cc86aaf206)
+2022-06-02T11:58:12.740Z info vpxd[48035] [Originator@6876 sub=vpxLro opID=55a419df] [VpxLRO] -- FINISH lro-846068
+2022-06-02T11:58:12.796Z info vpxd[47240] [Originator@6876 sub=MoCluster opID=HB-host-363@2022-389ab9b1] Host [vim.HostSystem:host-363,esx-2-121.vlcm.com] has 1 HDCS resources
+2022-06-02T11:58:12.914Z info vpxd[47370] [Originator@6876 sub=MoCluster opID=HB-host-493@2000-2922fd96] Host [vim.HostSystem:host-493,esx-2-192.vlcm.com] has 1 HDCS resources
diff --git a/test/logfile_w3c.0 b/test/logfile_w3c.0
new file mode 100644
index 0000000..c6177e7
--- /dev/null
+++ b/test/logfile_w3c.0
@@ -0,0 +1,5 @@
+#Software: Microsoft HTTP Server API 2.0
+#Version: 1.0 // the log file version as it's described by "https://www.w3.org/TR/WD-logfile".
+#Date: 2002-05-02 17:42:15 // when the first log file entry was recorded, which is when the entire log file was created.
+#Fields: date time c-ip cs-username s-ip s-port cs-method cs-uri-stem cs-uri-query sc-status cs(User-Agent)
+2002-05-02 17:42:15 172.22.255.255 - 172.30.255.255 80 GET /images/picture.jpg - 200 Mozilla/4.0+(compatible;MSIE+5.5;+Windows+2000+Server)
diff --git a/test/logfile_w3c.1 b/test/logfile_w3c.1
new file mode 100644
index 0000000..7e8a4f1
--- /dev/null
+++ b/test/logfile_w3c.1
@@ -0,0 +1,7 @@
+#Version: 1.0
+#GMT-Offset: -0800
+#Software: Oracle9iAS Web Cache/2.0.0.2.0
+#Start-Date: 2001-10-31 00:00:18
+#Fields: c-ip c-dns c-auth-id date time cs-method cs-uri sc-status bytes cs(Cookie) cs(Referrer) time-taken cs(User-Agent)
+#Date: 2001-10-31 00:00:18
+64.103.37.2 client_joaz7 DMS.user 2001-10-31 00:00:18 GET /admin/images/oc_bottomleft.gif 200 350 "BIGipServerwww_webcache_pool=1443321748.19460.0000;ORA_UCM_AGID=%2fMP%2f8M7%3etSHPV%40%2fS%3f%3fDh3VHO" "http://www.oracle.com/nl/partner/content.html" 370879 "Mozilla/4.5 [en] (WinNT; I)"
diff --git a/test/logfile_w3c.2 b/test/logfile_w3c.2
new file mode 100644
index 0000000..c15f08b
--- /dev/null
+++ b/test/logfile_w3c.2
@@ -0,0 +1,22 @@
+#Software: Microsoft Internet Information Server 4.0
+#Version: 1.0
+#Date: 2000-10-09 16:44:49
+#Fields: time c-ip cs-method cs-uri-stem sc-status
+16:44:49 1.1.1.1 [2]USER anonymous 331
+16:44:49 1.1.1.1 [2]PASS - 230
+16:48:05 1.1.1.1 [2]QUIT - 226
+16:48:17 1.1.1.1 [3]USER anonymous 331
+16:48:24 1.1.1.1 [3]PASS user@domain.com 230
+16:48:35 1.1.1.1 [3]sent /user/test.c 226
+16:48:41 1.1.1.1 [3]created readme.txt 226
+16:48:41 1.1.1.1 [3]created fileid.diz 226
+16:48:41 1.1.1.1 [3]created names.dll 226
+16:48:41 1.1.1.1 [3]created TEST.EXE 226
+16:48:44 1.1.1.1 [3]QUIT - 226
+#Software: Microsoft Internet Information Server 4.0
+#Version: 1.0
+#Date: 2000-10-10 16:44:49
+#Fields: time c-ip cs-method cs-uri-stem sc-status
+16:44:49 1.1.1.1 [2]USER anonymous 331
+16:44:49 1.1.1.1 [2]PASS - 230
+16:48:05 1.1.1.1 [2]QUIT - 226
diff --git a/test/logfile_w3c.3 b/test/logfile_w3c.3
new file mode 100644
index 0000000..2f3d190
--- /dev/null
+++ b/test/logfile_w3c.3
@@ -0,0 +1,10 @@
+#Software: IIS Advanced Logging Module
+
+#Version: 1.0
+
+#Start-Date: 2014-11-18 00:00:00.128
+
+#Fields: date-local time-local s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) cs(Host) sc-status sc-substatus sc-win32-status TimeTakenMS
+2012-08-15 17:00:00.363 1.2.3.4 GET /Products/theProduct - 80 - "70.95.0.0" "Mozilla/5.0 (Linux; Android 4.4.4; SM-G900V Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.59 Mobile Safari/537.36" "http://example.com/Search/SearchResults.pg?informationRecipient.languageCode.c=en" "xzy.example.com" 200 0 0 109
+2012-08-15 17:00:00.660 10.10.28.140 GET /Topic/hw43061 - 80 - - "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36" - "example.hello.com" 301 0 0 0
+2012-08-15 17:00:00.675 10.10.28.140 GET /hello/world/6,681965 - 80 - "173.5.0.0" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36" - "hello.example.com" 404 " ""garbage"" w/ spaces " 0 359
diff --git a/test/logfile_w3c.4 b/test/logfile_w3c.4
new file mode 100644
index 0000000..76d978b
--- /dev/null
+++ b/test/logfile_w3c.4
@@ -0,0 +1,6 @@
+#Software: Incapsula LOGS API
+#Version: 1.1
+#Date: 28/Jun/2017 07:28:59
+#Fields: date time cs-vid cs-clapp cs-browsertype cs-js-support cs-co-support c-ip s-caip cs-clappsig s-capsupport s-suid cs(User-Agent) cs-sessionid s-siteid cs-countrycode s-tag cs-cicode s-computername cs-lat cs-long s-accountname cs-uri cs-postbody cs-version sc-action s-externalid cs(Referrer) s-ip s-port cs-method cs-uri-query sc-status s-xff cs-bytes cs-start cs-rule cs-severity cs-attacktype cs-attackid s-ruleName
+"2017-06-28" "07:26:35" "a1f36498-c34a-45b9-b3a5-ee0bd00f91b6" "Chrome" "Browser" "false" "true" "123.123.123.123" "" "62a660e57ba257275cf7ccf699919eae18e07e84cb11c1075e99b1be98456059d3064ec14d3932ba6e89f5393a158b8b8c2572ad7ad7dadb0fe02a34ae4c3d504c035017bf9a6a7802bb898226378938" "NA" "774502" "Mozilla/5.0 (Windows NT 6.1; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36" "452000660051880893" "44850949" "SE" "LS" "Stockholm" "www.example.com" "32.0000" "32.0000" "Customer" "www.example.com/page.php" "" "HTTP" "REQ_PASSED" "118866685985031205" "" "124.124.124.124" "80" "GET" "variable=test" "200" "123.123.123.123" "10117" "1498634795555" "" "" "" "" ""
+"2017-06-26" "18:21:17" "daf5e234-24fc-4a69-985c-ab923529b393" "Firefox" "Browser" "false" "true" "125.125.125.125" "" "030404c9ac184e57a6c956e6bfad11dc23186ea6cf166908c6bc7db81aab7170e33740ea4d2972210f96e3365d25eb25a222f316a4f9221f39e56035fa9a49c80f9eedd9b846bb0491abe72a4b988e7cd3e7117283cee9f556726334972b7ce9" "NA" "774502" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:45.0) Gecko/20100101 Thunderbird/45.8.0 Lightning/4.7.8" "000000830000750000" "85437078" "SE" "XX" "Town" "www.example.com" "66.3333" "66.3333" "Company" "www.example.com/rss/news" "" "HTTP" "REQ_BAD_SERVER_CLOSED_CONNECTION" "3004162128217401" "" "125.125.125.125" "80" "GET" "" "" "125.125.125.125" "" "1498501277430" "" "" "" "" ""
diff --git a/test/logfile_w3c.5 b/test/logfile_w3c.5
new file mode 100644
index 0000000..b05bc48
--- /dev/null
+++ b/test/logfile_w3c.5
@@ -0,0 +1,2 @@
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query
+2015-01-13 00:32:17 100.79.192.81 GET /robots.txt - 80 - 157.55.39.146 Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 242 283
diff --git a/test/logfile_w3c.6 b/test/logfile_w3c.6
new file mode 100644
index 0000000..0a8f3e2
--- /dev/null
+++ b/test/logfile_w3c.6
@@ -0,0 +1,5 @@
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 00:32:17
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 00:32:17 100.79.192.81 GET /robots.txt - 80 - 157.55.39.146 ÄÖÜäöü\ßßßMözillä/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 242 283
diff --git a/test/logfile_w3c_big.0 b/test/logfile_w3c_big.0
new file mode 100644
index 0000000..866b8bc
--- /dev/null
+++ b/test/logfile_w3c_big.0
@@ -0,0 +1,254 @@
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 00:32:17
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 00:32:17 100.79.192.81 GET /robots.txt - 80 - 157.55.39.146 Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 242 283
+2015-01-13 00:32:17 100.79.192.81 GET /robots.txt - 80 - 157.55.39.146 Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 242 157
+2015-01-13 00:32:17 100.79.192.81 GET /robots.txt - 80 - 157.55.39.146 Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 242 152
+2015-01-13 00:32:17 100.79.192.81 GET /robots.txt - 80 - 157.55.39.146 Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 242 149
+2015-01-13 00:32:17 100.79.192.81 GET /robots.txt - 80 - 157.55.39.146 Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 242 137
+2015-01-13 00:32:26 100.79.192.81 GET /p/eToken1.png - 80 - 207.46.13.64 Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 245 157
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 02:05:40
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 02:05:40 100.79.192.81 GET /48672181611.html - 80 - 180.111.242.129 Mozilla/4.0+(compatible;+MSIE+8.0;+Windows+NT+5.1;+Trident/4.0) - 404 0 2 1405 141 2411
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 08:22:18
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 08:22:18 100.79.192.81 GET /robots.txt - 80 - 66.249.78.6 Mozilla/5.0+(compatible;+Googlebot/2.1;++http://www.google.com/bot.html) - 404 0 2 1405 250 156
+2015-01-13 08:22:18 100.79.192.81 GET /contact/ - 80 - 66.249.64.36 Mozilla/5.0+AppleWebKit/537.36+(KHTML,+like+Gecko;+Google+Web+Preview+Analytics)+Chrome/27.0.1453+Safari/537.36+(compatible;+Googlebot/2.1;++http://www.google.com/bot.html) - 404 0 2 1405 331 376
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 09:49:46
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 09:49:46 100.79.192.81 GET / - 80 - 188.120.253.124 Mozilla/5.0+(Windows;+U;+Windows+NT+5.1;+en-US)+AppleWebKit/533.4+(KHTML,+like+Gecko)+Chrome/5.0.375.99+Safari/533.4 http://example.com 200 0 0 960 205 468
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 10:16:10
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 10:16:10 100.79.192.81 GET /robots.txt - 80 - 37.59.20.217 Mozilla/5.0+(compatible;+MJ12bot/v1.4.5;+http://www.majestic12.co.uk/bot.php?+) - 404 0 2 1424 170 156
+2015-01-13 10:16:15 100.79.192.81 GET / - 80 - 37.59.20.217 Mozilla/5.0+(compatible;+MJ12bot/v1.4.5;+http://www.majestic12.co.uk/bot.php?+) - 200 0 0 979 313 265
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 10:46:33
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 10:46:33 100.79.192.81 GET / - 80 - 188.163.80.167 Mozilla/4.0+(compatible;+MSIE+7.0;+Windows+NT+6.1;+WOW64;+Trident/7.0;+SLCC2;+.NET+CLR+2.0.50727;+.NET+CLR+3.5.30729;+.NET+CLR+3.0.30729;+Media+Center+PC+6.0;+.NET4.0C;+.NET4.0E) - 200 0 0 960 327 499
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 12:30:24
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 12:30:24 100.79.192.81 GET /robots.txt - 80 - 157.55.39.146 Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 261 283
+2015-01-13 12:30:30 100.79.192.81 GET / - 80 - 157.55.39.146 Mozilla/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 200 0 0 960 264 418
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 13:00:56
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 13:00:56 100.79.192.81 GET / - 80 - 192.99.149.88 Mozilla/5.0+(Windows+NT+6.1;+WOW64;+Trident/7.0;+rv:11.0)+like+Gecko - 200 0 64 0 212 504
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 16:35:13
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 16:35:13 100.79.192.81 GET / - 80 - 130.211.190.46 NerdyBot - 200 0 0 984 94 503
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 22:29:42
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 22:29:42 100.79.192.81 GET / - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 200 0 0 960 238 807
+2015-01-13 22:29:42 100.79.192.81 GET /fckeditor/fckconfig.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 260 376
+2015-01-13 22:29:42 100.79.192.81 GET /fckeditor/license.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 259 392
+2015-01-13 22:29:43 100.79.192.81 GET /fckeditor/editor/js/fckeditorcode_ie.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 277 399
+2015-01-13 22:29:43 100.79.192.81 GET /fckeditor/fckeditor.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 260 392
+2015-01-13 22:29:43 100.79.192.81 GET /FCK/editor/js/fckeditorcode_ie.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 271 393
+2015-01-13 22:29:44 100.79.192.81 GET /FCK/fckeditor.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 254 392
+2015-01-13 22:29:44 100.79.192.81 GET /fckeditor.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 250 377
+2015-01-13 22:29:44 100.79.192.81 GET /editor/js/fckeditorcode_ie.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 267 361
+2015-01-13 22:29:45 100.79.192.81 GET /fckeditor/editor/js/fckeditorcode_ie.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 277 352
+2015-01-13 22:29:45 100.79.192.81 GET /ckeditor/ckeditor.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 258 355
+2015-01-13 22:29:45 100.79.192.81 GET / c=4e5e5d7364f443e28fbf0d3ae744a59a 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 200 0 0 960 273 361
+2015-01-13 22:29:46 100.79.192.81 GET /wp-cron.php - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 249 345
+2015-01-13 22:29:46 100.79.192.81 GET /wp-content - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 248 354
+2015-01-13 22:29:46 100.79.192.81 GET /wp-login.php - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 250 338
+2015-01-13 22:29:47 100.79.192.81 GET /license.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 249 345
+2015-01-13 22:29:47 100.79.192.81 GET /readme.html - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 249 346
+2015-01-13 22:29:47 100.79.192.81 GET /robots.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 248 345
+2015-01-13 22:29:48 100.79.192.81 GET /favicon.ico - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 249 330
+2015-01-13 22:29:48 100.79.192.81 GET /blog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 335
+2015-01-13 22:29:48 100.79.192.81 GET /weblog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 325
+2015-01-13 22:29:48 100.79.192.81 GET /wordpress/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 248 330
+2015-01-13 22:29:50 100.79.192.81 GET /wp/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 241 345
+2015-01-13 22:29:50 100.79.192.81 GET /log/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 330
+2015-01-13 22:29:50 100.79.192.81 GET /archiver - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 345
+2015-01-13 22:29:51 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 330
+2015-01-13 22:29:51 100.79.192.81 GET /forum/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 329
+2015-01-13 22:29:51 100.79.192.81 GET /discuz/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 346
+2015-01-13 22:29:52 100.79.192.81 GET /docs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 330
+2015-01-13 22:29:52 100.79.192.81 GET /shop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 345
+2015-01-13 22:29:52 100.79.192.81 GET /store/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 346
+2015-01-13 22:29:53 100.79.192.81 GET /mall/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 345
+2015-01-13 22:29:53 100.79.192.81 GET /cart/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 345
+2015-01-13 22:29:53 100.79.192.81 GET /shop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 346
+2015-01-13 22:29:54 100.79.192.81 GET /store/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 345
+2015-01-13 22:29:54 100.79.192.81 GET /mall/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 361
+2015-01-13 22:29:54 100.79.192.81 GET /shopex/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 344
+2015-01-13 22:29:55 100.79.192.81 GET /administrator/manifests/files/joomla.xml - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 278 363
+2015-01-13 22:29:55 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 361
+2015-01-13 22:29:55 100.79.192.81 GET /joomla/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 361
+2015-01-13 22:29:56 100.79.192.81 GET /public/js/ips.board.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 260 361
+2015-01-13 22:29:56 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 385
+2015-01-13 22:29:56 100.79.192.81 GET /forum/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 369
+2015-01-13 22:29:57 100.79.192.81 GET /ipboard/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 377
+2015-01-13 22:29:57 100.79.192.81 GET /board/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 377
+2015-01-13 22:29:57 100.79.192.81 GET /data/admin/ver.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 256 393
+2015-01-13 22:29:58 100.79.192.81 GET /digg.php - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 377
+2015-01-13 22:29:58 100.79.192.81 GET /plus/sitemap.html - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 255 361
+2015-01-13 22:29:58 100.79.192.81 GET /plus/rssmap.html - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 254 377
+2015-01-13 22:29:59 100.79.192.81 GET /plus/heightsearch.php - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 259 377
+2015-01-13 22:29:59 100.79.192.81 GET /data - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 377
+2015-01-13 22:29:59 100.79.192.81 GET /member/space/company/info.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 267 376
+2015-01-13 22:30:00 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 378
+2015-01-13 22:30:00 100.79.192.81 GET /dedecms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 393
+2015-01-13 22:30:00 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 393
+2015-01-13 22:30:02 100.79.192.81 GET /empirecms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 248 393
+2015-01-13 22:30:02 100.79.192.81 GET /CHANGELOG.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 251 377
+2015-01-13 22:30:02 100.79.192.81 GET /changelog.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 251 393
+2015-01-13 22:30:03 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 377
+2015-01-13 22:30:03 100.79.192.81 GET /drupal/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 393
+2015-01-13 22:30:03 100.79.192.81 GET /admin - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 378
+2015-01-13 22:30:04 100.79.192.81 GET /list.php - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 376
+2015-01-13 22:30:04 100.79.192.81 GET /admin/template/article_more/config.htm - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 276 377
+2015-01-13 22:30:04 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 377
+2015-01-13 22:30:05 100.79.192.81 GET /docs.css - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 393
+2015-01-13 22:30:05 100.79.192.81 GET /phpmyadmin/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 249 408
+2015-01-13 22:30:05 100.79.192.81 GET /rss.php - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 393
+2015-01-13 22:30:06 100.79.192.81 GET /blog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 410
+2015-01-13 22:30:06 100.79.192.81 GET /inc/rsd.php - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 249 391
+2015-01-13 22:30:06 100.79.192.81 GET /blog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 409
+2015-01-13 22:30:08 100.79.192.81 GET /weblog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 408
+2015-01-13 22:30:08 100.79.192.81 GET /log/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 408
+2015-01-13 22:30:08 100.79.192.81 GET /robots.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 248 424
+2015-01-13 22:30:09 100.79.192.81 GET /tools/rss.aspx - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 0 2127 252 472
+2015-01-13 22:30:09 100.79.192.81 GET /help.aspx - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 0 2122 247 408
+2015-01-13 22:30:09 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 424
+2015-01-13 22:30:10 100.79.192.81 GET /forum/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 408
+2015-01-13 22:30:10 100.79.192.81 GET /discuz/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 422
+2015-01-13 22:30:10 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 395
+2015-01-13 22:30:11 100.79.192.81 GET /foosun/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 393
+2015-01-13 22:30:11 100.79.192.81 GET /index.php m=search 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 256 345
+2015-01-13 22:30:11 100.79.192.81 GET /index.php m=wap 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 253 315
+2015-01-13 22:30:11 100.79.192.81 GET /index.php m=admin 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 255 314
+2015-01-13 22:30:13 100.79.192.81 GET /index.php m=admin&c=index&a=login&pc_hash= 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 280 314
+2015-01-13 22:30:13 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 313
+2015-01-13 22:30:13 100.79.192.81 GET /phpcms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 315
+2015-01-13 22:30:13 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 314
+2015-01-13 22:30:14 100.79.192.81 GET /aspcms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 313
+2015-01-13 22:30:14 100.79.192.81 GET /admin/inc/xml.xslt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 256 315
+2015-01-13 22:30:14 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 314
+2015-01-13 22:30:14 100.79.192.81 GET /feed.asp - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 314
+2015-01-13 22:30:15 100.79.192.81 GET /blog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 314
+2015-01-13 22:30:15 100.79.192.81 GET /weblog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 315
+2015-01-13 22:30:15 100.79.192.81 GET /log/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 314
+2015-01-13 22:30:15 100.79.192.81 GET /zblog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 314
+2015-01-13 22:30:16 100.79.192.81 GET /mail/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 315
+2015-01-13 22:30:16 100.79.192.81 GET /webmail/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 314
+2015-01-13 22:30:16 100.79.192.81 GET /archive/archive.css - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 257 314
+2015-01-13 22:30:16 100.79.192.81 GET /clientscript/vbulletin_ajax_htmlloader.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 279 314
+2015-01-13 22:30:18 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 330
+2015-01-13 22:30:18 100.79.192.81 GET /forum/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 361
+2015-01-13 22:30:18 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 392
+2015-01-13 22:30:19 100.79.192.81 GET /phpbb/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 424
+2015-01-13 22:30:19 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 420
+2015-01-13 22:30:19 100.79.192.81 GET /forum/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 398
+2015-01-13 22:30:20 100.79.192.81 GET /leadbbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 408
+2015-01-13 22:30:20 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 408
+2015-01-13 22:30:20 100.79.192.81 GET /cmseasy/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 392
+2015-01-13 22:30:21 100.79.192.81 GET /history.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 249 408
+2015-01-13 22:30:21 100.79.192.81 GET /common/common.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 254 378
+2015-01-13 22:30:21 100.79.192.81 GET /blog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 361
+2015-01-13 22:30:22 100.79.192.81 GET /weblog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 381
+2015-01-13 22:30:22 100.79.192.81 GET /log/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 357
+2015-01-13 22:30:22 100.79.192.81 GET /blog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 361
+2015-01-13 22:30:23 100.79.192.81 GET /log/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 361
+2015-01-13 22:30:23 100.79.192.81 GET /weblog/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 362
+2015-01-13 22:30:23 100.79.192.81 GET /typecho/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 377
+2015-01-13 22:30:25 100.79.192.81 GET /extern.php action=feed&type=atom 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 270 361
+2015-01-13 22:30:25 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 377
+2015-01-13 22:30:25 100.79.192.81 GET /forum/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 377
+2015-01-13 22:30:26 100.79.192.81 GET /fluxbb/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 377
+2015-01-13 22:30:26 100.79.192.81 GET /inc/Templates/rss.xslt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 260 362
+2015-01-13 22:30:26 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 392
+2015-01-13 22:30:27 100.79.192.81 GET /dvbbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 377
+2015-01-13 22:30:27 100.79.192.81 GET /shop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 376
+2015-01-13 22:30:27 100.79.192.81 GET /store/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 394
+2015-01-13 22:30:28 100.79.192.81 GET /mall/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 377
+2015-01-13 22:30:28 100.79.192.81 GET /shop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 372
+2015-01-13 22:30:28 100.79.192.81 GET /store/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 399
+2015-01-13 22:30:29 100.79.192.81 GET /mall/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 376
+2015-01-13 22:30:29 100.79.192.81 GET /opencart/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 247 393
+2015-01-13 22:30:29 100.79.192.81 GET /shop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 393
+2015-01-13 22:30:30 100.79.192.81 GET /store/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 411
+2015-01-13 22:30:30 100.79.192.81 GET /mall/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 401
+2015-01-13 22:30:30 100.79.192.81 GET /iwebshop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 247 397
+2015-01-13 22:30:31 100.79.192.81 GET /shop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 408
+2015-01-13 22:30:31 100.79.192.81 GET /store/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 407
+2015-01-13 22:30:31 100.79.192.81 GET /mall/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 394
+2015-01-13 22:30:33 100.79.192.81 GET /ecshop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 408
+2015-01-13 22:30:33 100.79.192.81 GET /shop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 393
+2015-01-13 22:30:33 100.79.192.81 GET /store/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 414
+2015-01-13 22:30:34 100.79.192.81 GET /mall/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 402
+2015-01-13 22:30:34 100.79.192.81 GET /shop7z/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 394
+2015-01-13 22:30:34 100.79.192.81 GET /skin/frontend/default/modern/css/styles.css - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 281 384
+2015-01-13 22:30:35 100.79.192.81 GET /shop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 338
+2015-01-13 22:30:35 100.79.192.81 GET /store/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 315
+2015-01-13 22:30:35 100.79.192.81 GET /mall/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 314
+2015-01-13 22:30:35 100.79.192.81 GET /magento/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 314
+2015-01-13 22:30:36 100.79.192.81 GET /api/api_user.xml - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 254 330
+2015-01-13 22:30:36 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 314
+2015-01-13 22:30:36 100.79.192.81 GET /kesion/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 314
+2015-01-13 22:30:36 100.79.192.81 GET /archiver - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 314
+2015-01-13 22:30:38 100.79.192.81 GET /robots.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 248 314
+2015-01-13 22:30:38 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 314
+2015-01-13 22:30:38 100.79.192.81 GET /forum/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 314
+2015-01-13 22:30:38 100.79.192.81 GET /bbsmax/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 313
+2015-01-13 22:30:39 100.79.192.81 GET /inc/playerKinds.xml - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 257 316
+2015-01-13 22:30:39 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 314
+2015-01-13 22:30:39 100.79.192.81 GET /maxcms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 314
+2015-01-13 22:30:39 100.79.192.81 GET /install - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 315
+2015-01-13 22:30:40 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 314
+2015-01-13 22:30:40 100.79.192.81 GET /oecms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 314
+2015-01-13 22:30:40 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 314
+2015-01-13 22:30:40 100.79.192.81 GET /lazycms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 313
+2015-01-13 22:30:41 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 315
+2015-01-13 22:30:41 100.79.192.81 GET /verycms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 314
+2015-01-13 22:30:41 100.79.192.81 GET /template/home.htm - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 255 315
+2015-01-13 22:30:41 100.79.192.81 GET /system/skins/default/system.login.htm - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 275 314
+2015-01-13 22:30:43 100.79.192.81 GET /system/language/zh-cn.xml - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 263 314
+2015-01-13 22:30:43 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 330
+2015-01-13 22:30:43 100.79.192.81 GET /kingcms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 345
+2015-01-13 22:30:44 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 331
+2015-01-13 22:30:44 100.79.192.81 GET /metinfo/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 345
+2015-01-13 22:30:44 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 361
+2015-01-13 22:30:45 100.79.192.81 GET /forum/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 340
+2015-01-13 22:30:45 100.79.192.81 GET /6kbb/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 335
+2015-01-13 22:30:45 100.79.192.81 GET /stylesheet.css - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 252 383
+2015-01-13 22:30:46 100.79.192.81 GET /includes/general.js - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 257 393
+2015-01-13 22:30:46 100.79.192.81 GET /shop/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 424
+2015-01-13 22:30:46 100.79.192.81 GET /store/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 408
+2015-01-13 22:30:47 100.79.192.81 GET /mall/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 417
+2015-01-13 22:30:47 100.79.192.81 GET /oscommerce/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 249 416
+2015-01-13 22:30:47 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 361
+2015-01-13 22:30:48 100.79.192.81 GET /jxcms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 317
+2015-01-13 22:30:48 100.79.192.81 GET /cms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 311
+2015-01-13 22:30:48 100.79.192.81 GET /zcms/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 243 313
+2015-01-13 22:30:48 100.79.192.81 GET /robots.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 248 319
+2015-01-13 22:30:49 100.79.192.81 GET /licence.txt - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 249 310
+2015-01-13 22:30:49 100.79.192.81 GET /rss.php - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 245 317
+2015-01-13 22:30:49 100.79.192.81 GET /bbs/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 242 315
+2015-01-13 22:30:49 100.79.192.81 GET /forum/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 244 314
+2015-01-13 22:30:51 100.79.192.81 GET /phpwind/ - 80 - 183.60.244.30 Mozilla/5.0+(Macintosh;+Intel+Mac+OS+X+10_9_4)+AppleWebKit/537.36+(KHTML,+like+Gecko)+Chrome/36.0.1985.125+Safari/537.36 - 404 0 2 1405 246 341
+#Software: Microsoft Internet Information Services 8.5
+#Version: 1.0
+#Date: 2015-01-13 23:15:41
+#Fields: date time s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) cs(Referer) sc-status sc-substatus sc-win32-status sc-bytes cs-bytes time-taken
+2015-01-13 23:15:41 100.79.192.81 GET /robots.txt - 80 - 188.165.15.50 Mozilla/5.0+(compatible;+AhrefsBot/5.0;++http://ahrefs.com/robot/) - 404 0 2 1405 170 141
diff --git a/test/logfile_with_a_really_long_name_to_test_a_bug_with_long_names.0 b/test/logfile_with_a_really_long_name_to_test_a_bug_with_long_names.0
new file mode 100644
index 0000000..8ab686e
--- /dev/null
+++ b/test/logfile_with_a_really_long_name_to_test_a_bug_with_long_names.0
@@ -0,0 +1 @@
+Hello, World!
diff --git a/test/logfile_xml_msg.0 b/test/logfile_xml_msg.0
new file mode 100644
index 0000000..010dac6
--- /dev/null
+++ b/test/logfile_xml_msg.0
@@ -0,0 +1,36 @@
+[2020-12-10 06:56:41,061] INFO [m:108] Calling 'x' with params:
+
+[2020-12-10 06:56:41,092] DEBUG [connect.client:69] Full request text:
+<?xml version='1.0' encoding='iso-8859-2'?>
+<a-request>
+ <head>
+ x
+ </head>
+ <source>
+ x
+ </source>
+ <request id="1">
+ <name>
+ x
+ </name>
+ </request>
+</a-request>
+
+[2020-12-10 06:56:41,099] DEBUG [m:85] Full reply text:
+<?xml version='1.0' encoding='iso-8859-2'?>
+<a-reply>
+ <head>
+ x
+ </head>
+ <reply id="2">
+ <status>
+ <result>OK</result>
+ </status>
+ <name>
+ x
+ </name>
+ </reply>
+ <technical-track>
+ x
+ </technical-track>
+</a-reply>
diff --git a/test/multiline.lnav b/test/multiline.lnav
new file mode 100644
index 0000000..3ee2135
--- /dev/null
+++ b/test/multiline.lnav
@@ -0,0 +1,14 @@
+#! /usr/bin/env lnav -f
+
+;CREATE TABLE foobar (
+ mykey integer primary key,
+ name text
+);
+
+;INSERT INTO foobar VALUES (1, 'Jules');
+
+;INSERT INTO environ
+ SELECT "msg", "Hello: " || group_concat(name, ", ") FROM foobar;
+
+:pipe-line-to
+ echo $msg
diff --git a/test/mvwattrline_output.0 b/test/mvwattrline_output.0
new file mode 100644
index 0000000..608ba64
--- /dev/null
+++ b/test/mvwattrline_output.0
@@ -0,0 +1,49 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+S -1 ┋ ┋
+A └ normal, normal, normal
+CSI Erase all
+S 1 ┋Plain text ┋
+A ··········└ carriage-return
+S 2 ┋ Leading tab ┋
+A └ inverse │
+A ········└ normal │
+A ···················└ carriage-return
+S 3 ┋Tab with text ┋
+A ·└ inverse │
+A ········└ normal │
+A ·················└ carriage-return
+S 4 ┋Tab with text #2 ┋
+A ···└ inverse │
+A ········└ normal │
+A ····················└ carriage-return
+S 5 ┋Two tabs with text ┋
+A ········└ inverse │ │
+A ··········└ normal │ │
+A ················└ inverse│
+A ····················└ normal
+A ·························└ carriage-return
+S 6 ┋Text with mixed attributes. ┋
+A ·····└ fg(#800000) │
+A ·······└ inverse │
+A ·········└ normal │
+A ············└ normal │
+A ···························└ carriage-return
+S 7 ┋Text with unicode ▶ characters ┋
+A ····················└ inverse │
+A ························└ normal
+A ······························└ carriage-return
+S 8 ┋ ┋
+A └ normal
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/nested.lnav b/test/nested.lnav
new file mode 100644
index 0000000..836e030
--- /dev/null
+++ b/test/nested.lnav
@@ -0,0 +1,2 @@
+
+:echo nested here $0 $1 $2
diff --git a/test/parser_debugger.py b/test/parser_debugger.py
new file mode 100755
index 0000000..d2818f5
--- /dev/null
+++ b/test/parser_debugger.py
@@ -0,0 +1,249 @@
+#! /usr/bin/env python
+
+# Copyright (c) 2013, Timothy Stack
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# * Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * Neither the name of Timothy Stack nor the names of its contributors
+# may be used to endorse or promote products derived from this software
+# without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import os
+import string
+import readline
+import itertools
+import collections
+
+TEST_DIR = os.path.dirname(__file__)
+ROOT_DIR = os.path.dirname(TEST_DIR)
+SRC_DIR = os.path.join(ROOT_DIR, "src")
+
+addr_to_name = {}
+name_to_addr = {}
+element_lists = collections.defaultdict(list)
+list_depth = {}
+list_format = {}
+breakpoints = set()
+
+def completer(text, state):
+ options = [x for x in itertools.chain(name_to_addr,
+ element_lists,
+ breakpoints)
+ if x.startswith(text)]
+ try:
+ return options[state]
+ except IndexError:
+ return None
+
+readline.set_completer(completer)
+
+if 'libedit' in readline.__doc__:
+ readline.parse_and_bind('bind ^I rl_complete')
+else:
+ readline.parse_and_bind('tab: complete')
+
+input_line = ''
+ops = []
+for line in open("scanned.dpt"):
+ if line.startswith("input "):
+ input_line = line[6:-1]
+ else:
+ ops.append(map(string.strip, line.split()))
+
+def getstr(capture):
+ start, end = capture.split(':')
+ return input_line[int(start):int(end)]
+
+def printlist(name_or_addr):
+ if name_or_addr in name_to_addr:
+ addr = name_to_addr[name_or_addr]
+ print "% 3d (%s:%s) %s" % (list_depth.get(addr, -1), name_or_addr, addr, element_lists[addr])
+ elif name_or_addr in element_lists:
+ addr = name_or_addr
+ print "% 3d (%s:%s) %s" % (list_depth.get(name_or_addr, -1),
+ addr_to_name.get(name_or_addr, name_or_addr),
+ name_or_addr,
+ element_lists[name_or_addr])
+ else:
+ print "error: unknown list --", name_or_addr
+
+ if addr in list_format:
+ print " format -- appender(%s) term(%s) qual(%s) sep(%s) prefix_term(%s)" % tuple(list_format[addr])
+
+def handleop(fields):
+ addr = fields[0]
+ loc = fields[1].split(':')
+ method_name = fields[2]
+ method_args = fields[3:]
+
+ if addr == '0x0':
+ el = None
+ else:
+ el = element_lists[addr]
+
+ if method_name == 'element_list_t':
+ addr_to_name[addr] = method_args[0]
+ name_to_addr[method_args[0]] = addr
+ list_depth[addr] = int(method_args[1])
+ elif method_name == '~element_list_t':
+ del element_lists[addr]
+ elif method_name == 'format':
+ list_depth[addr] = int(method_args[0])
+ list_format[addr] = method_args[1:]
+ elif method_name == 'consumed':
+ list_depth[addr] = -1
+ elif method_name == 'push_back':
+ el.append((method_args[0], getstr(method_args[1])))
+ elif method_name == 'pop_front':
+ el.pop(0)
+ elif method_name == 'pop_back':
+ el.pop()
+ elif method_name == 'clear2':
+ el[::] = []
+ elif method_name == 'splice':
+ pos = int(method_args[0])
+ other = element_lists[method_args[1]]
+ start, from_end = map(int, method_args[2].split(':'))
+ end = len(other) - from_end
+ sub_list = other[start:end]
+ del other[start:end]
+ el[pos:pos] = sub_list
+ elif method_name == 'swap':
+ other = element_lists[method_args[0]]
+ element_lists[method_args[0]] = el
+ element_lists[addr] = other
+ elif method_name == 'point':
+ breakpoints.add(method_args[0])
+ else:
+ print "Unhandled method: ", method_name
+
+def playupto(length):
+ addr_to_name.clear()
+ name_to_addr.clear()
+ element_lists.clear()
+ list_depth.clear()
+ for index in range(length):
+ handleop(ops[index])
+
+def find_prev_point(start, name):
+ orig_start = start
+ while start > 0:
+ start -= 1;
+ fields = ops[start]
+ if fields[2] != 'point':
+ continue
+ if not name or fields[3] == name:
+ return start + 1
+ return orig_start + 1
+
+def find_next_point(start, name):
+ orig_start = start
+ while start < len(ops):
+ start += 1;
+ fields = ops[start]
+ if fields[2] != 'point':
+ continue
+ if not name or fields[3] == name:
+ return start + 1
+ return orig_start + 1
+
+def printall():
+ print input_line
+ sorted_lists = [(list_depth.get(addr, -1), addr) for addr in element_lists]
+ sorted_lists.sort()
+ for _depth, addr in sorted_lists:
+ printlist(addr)
+
+index = len(ops)
+last_cmd = ['']
+watch_list = set()
+while True:
+ playupto(index)
+
+ if index == 0:
+ print "init"
+ else:
+ op = ops[index - 1]
+ print "#%s %s" % (index -1, op)
+ if op[2] == 'push_back':
+ print getstr(op[4])
+
+ for list_name in watch_list:
+ printlist(list_name)
+
+ try:
+ cmd = raw_input("> ").split()
+ except EOFError:
+ print
+ break
+
+ if not cmd or cmd[0] == '':
+ cmd = last_cmd
+
+ if not cmd or cmd[0] == '':
+ pass
+ elif cmd[0] == 'h':
+ print 'Help:'
+ print ' q - quit'
+ print ' s - Start over'
+ print ' n - Next step'
+ print ' r - Previous step'
+ print ' b - Previous breakpoint'
+ print ' c - Next breakpoint'
+ print ' p - Print state'
+ print ' w <var> - Add a variable to the watch list'
+ print ' u <var> - Remove a variable from the watch list'
+ elif cmd[0] == 'q':
+ break
+ elif cmd[0] == 's':
+ index = 0
+ elif cmd[0] == 'n':
+ if index < len(ops):
+ index += 1
+ elif cmd[0] == 'r':
+ if index > 0:
+ index -= 1
+ elif cmd[0] == 'b':
+ if len(cmd) == 1:
+ cmd.append('')
+
+ index = find_prev_point(index - 1, cmd[1])
+ elif cmd[0] == 'c':
+ if len(cmd) == 1:
+ cmd.append('')
+ index = find_next_point(index - 1, cmd[1])
+ elif cmd[0] == 'p':
+ if len(cmd) > 1:
+ printlist(cmd[1])
+ else:
+ printall()
+ elif cmd[0] == 'w':
+ watch_list.add(cmd[1])
+ elif cmd[0] == 'u':
+ if watch_list:
+ watch_list.remove(cmd[1])
+ else:
+ print "error: unknown command --", cmd
+
+ printall()
+
+ last_cmd = cmd
diff --git a/test/remote-log-dir/logfile_access_log.0 b/test/remote-log-dir/logfile_access_log.0
new file mode 100644
index 0000000..a90f29f
--- /dev/null
+++ b/test/remote-log-dir/logfile_access_log.0
@@ -0,0 +1,3 @@
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
diff --git a/test/remote-log-dir/logfile_access_log.1 b/test/remote-log-dir/logfile_access_log.1
new file mode 100644
index 0000000..ad2b37e
--- /dev/null
+++ b/test/remote-log-dir/logfile_access_log.1
@@ -0,0 +1 @@
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
diff --git a/test/rltest.cc b/test/rltest.cc
new file mode 100644
index 0000000..81606eb
--- /dev/null
+++ b/test/rltest.cc
@@ -0,0 +1,216 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <errno.h>
+#include <fcntl.h>
+#include <readline/readline.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/time.h>
+#include <unistd.h>
+#include <util.h>
+
+#include "vt52_curses.hh"
+
+static const int KEY_TIMEOUT = 500 * 1000;
+
+static int got_line = 0;
+static int got_timeout = 0;
+
+static void
+sigalrm(int sig)
+{
+ got_timeout = 1;
+}
+
+static void
+line_ready(char* line)
+{
+ fprintf(stderr, "got line: %s\n", line);
+ add_history(line);
+ got_line = 1;
+}
+
+static void
+child_readline(void)
+{
+ fd_set rfds;
+
+ FD_ZERO(&rfds);
+ FD_SET(STDIN_FILENO, &rfds);
+
+ rl_callback_handler_install("/", (void (*)()) line_ready);
+ while (1) {
+ fd_set ready_rfds = rfds;
+ int rc;
+
+ rc = select(STDIN_FILENO + 1, &ready_rfds, NULL, NULL, NULL);
+ if (rc < 0) {
+ switch (errno) {
+ case EINTR:
+ break;
+ }
+ } else {
+ if (FD_ISSET(STDIN_FILENO, &ready_rfds)) {
+ struct itimerval itv;
+
+ itv.it_value.tv_sec = 0;
+ itv.it_value.tv_usec = KEY_TIMEOUT;
+ itv.it_interval.tv_sec = 0;
+ itv.it_interval.tv_usec = 0;
+ setitimer(ITIMER_REAL, &itv, NULL);
+
+ rl_callback_read_char();
+ }
+ }
+
+ if (got_timeout) {
+ fprintf(stderr, "got timeout\n");
+ got_timeout = 0;
+ }
+ if (got_line) {
+ rl_callback_handler_remove();
+ got_line = 0;
+ rl_callback_handler_install("/", (void (*)()) line_ready);
+ }
+ }
+}
+
+static void
+finish(int sig)
+{
+ endwin();
+ exit(0);
+}
+
+int
+main(int argc, char* argv[])
+{
+ int fd, retval = EXIT_SUCCESS;
+ signal(SIGALRM, sigalrm);
+
+ fd = open("/tmp/rltest.err", O_WRONLY | O_CREAT | O_APPEND, 0666);
+ dup2(fd, STDERR_FILENO);
+ fprintf(stderr, "startup\n");
+
+ if (0) {
+ while (1) {
+ char* ret = readline("/");
+
+ add_history(ret);
+ }
+ }
+
+ (void) signal(SIGINT, finish); /* arrange interrupts to terminate */
+
+ WINDOW* mainwin = initscr(); /* initialize the curses library */
+ keypad(stdscr, TRUE); /* enable keyboard mapping */
+ (void) nonl(); /* tell curses not to do NL->CR/NL on output */
+ (void) cbreak(); /* take input chars one at a time, no wait for \n */
+ (void) noecho(); /* don't echo input */
+
+ if (fcntl(STDIN_FILENO, F_SETFL, O_NONBLOCK) < 0)
+ perror("fcntl");
+
+ {
+ int master, slave;
+ pid_t rl;
+
+ if (openpty(&master, &slave, NULL, NULL, NULL) < 0) {
+ perror("openpty");
+ } else if ((rl = fork()) < 0) {
+ perror("fork");
+ } else if (rl == 0) {
+ close(master);
+ master = -1;
+
+ dup2(slave, STDIN_FILENO);
+ dup2(slave, STDOUT_FILENO);
+
+ setenv("TERM", "vt52", 1);
+
+ child_readline();
+ } else {
+ vt52_curses vc(mainwin);
+ fd_set rfds;
+
+ FD_ZERO(&rfds);
+ FD_SET(STDIN_FILENO, &rfds);
+ FD_SET(master, &rfds);
+
+ while (1) {
+ fd_set ready_rfds = rfds;
+ int rc;
+
+ rc = select(master + 1, &ready_rfds, NULL, NULL, NULL);
+ if (rc < 0) {
+ break;
+ } else {
+ char buffer[1024];
+
+ if (FD_ISSET(STDIN_FILENO, &ready_rfds)) {
+ int ch;
+
+ if ((ch = getch()) != ERR) {
+ const char* bch;
+ int len;
+
+ bch = vc.map_input(ch, len);
+
+ if (len > 0) {
+ fprintf(stderr, "stdin: %x\n", ch);
+ if (write(master, bch, len) < 0)
+ perror("write");
+ }
+ }
+ }
+ if (FD_ISSET(master, &ready_rfds)) {
+ int lpc;
+
+ rc = read(master, buffer, sizeof(buffer));
+
+ fprintf(stderr, "child: ");
+ for (lpc = 0; lpc < rc; lpc++) {
+ fprintf(stderr, "%x ", buffer[lpc]);
+ }
+ fprintf(stderr, "\n");
+
+ vc.map_output(buffer, rc);
+ }
+ }
+ refresh();
+ }
+ }
+ }
+
+ finish(0);
+
+ return retval;
+}
diff --git a/test/scripty.cc b/test/scripty.cc
new file mode 100644
index 0000000..2f7bb94
--- /dev/null
+++ b/test/scripty.cc
@@ -0,0 +1,1153 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/time.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/curses.h>
+#elif defined HAVE_NCURSESW_H
+# include <ncursesw.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/curses.h>
+#elif defined HAVE_NCURSES_H
+# include <ncurses.h>
+#elif defined HAVE_CURSES_H
+# include <curses.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+
+#ifdef HAVE_PTY_H
+# include <pty.h>
+#endif
+
+#ifdef HAVE_UTIL_H
+# include <util.h>
+#endif
+
+#ifdef HAVE_LIBUTIL_H
+# include <libutil.h>
+#endif
+
+#include <algorithm>
+#include <map>
+#include <queue>
+#include <sstream>
+#include <string>
+#include <utility>
+
+#include "base/auto_fd.hh"
+#include "base/auto_mem.hh"
+#include "base/string_util.hh"
+#include "fmt/format.h"
+#include "ghc/filesystem.hpp"
+#include "styling.hh"
+#include "termios_guard.hh"
+#include "ww898/cp_utf8.hpp"
+
+using namespace std;
+
+/**
+ * An RAII class for opening a PTY and forking a child process.
+ */
+class child_term {
+public:
+ class error : public std::exception {
+ public:
+ error(int err) : e_err(err){};
+
+ int e_err;
+ };
+
+ explicit child_term(bool passin)
+ {
+ struct winsize ws;
+ auto_fd slave;
+
+ memset(&ws, 0, sizeof(ws));
+
+ if (isatty(STDIN_FILENO)
+ && tcgetattr(STDIN_FILENO, &this->ct_termios) == -1) {
+ throw error(errno);
+ }
+
+ if (isatty(STDOUT_FILENO)
+ && ioctl(STDOUT_FILENO, TIOCGWINSZ, &this->ct_winsize) == -1)
+ {
+ throw error(errno);
+ }
+
+ ws.ws_col = 80;
+ ws.ws_row = 24;
+
+ if (openpty(this->ct_master.out(), slave.out(), nullptr, nullptr, &ws)
+ < 0) {
+ throw error(errno);
+ }
+
+ if ((this->ct_child = fork()) == -1)
+ throw error(errno);
+
+ if (this->ct_child == 0) {
+ this->ct_master.reset();
+
+ if (!passin) {
+ dup2(slave, STDIN_FILENO);
+ }
+ dup2(slave, STDOUT_FILENO);
+
+ setenv("TERM", "xterm-color", 1);
+ } else {
+ slave.reset();
+ }
+ };
+
+ virtual ~child_term()
+ {
+ (void) this->wait_for_child();
+
+ if (isatty(STDIN_FILENO)
+ && tcsetattr(STDIN_FILENO, TCSANOW, &this->ct_termios) == -1)
+ {
+ perror("tcsetattr");
+ }
+ if (isatty(STDOUT_FILENO)
+ && ioctl(STDOUT_FILENO, TIOCSWINSZ, &this->ct_winsize) == -1)
+ {
+ perror("ioctl");
+ }
+ };
+
+ int wait_for_child()
+ {
+ int retval = -1;
+
+ if (this->ct_child > 0) {
+ kill(this->ct_child, SIGTERM);
+ this->ct_child = -1;
+
+ while (wait(&retval) < 0 && (errno == EINTR))
+ ;
+ }
+
+ return retval;
+ };
+
+ bool is_child() const
+ {
+ return this->ct_child == 0;
+ };
+
+ pid_t get_child_pid() const
+ {
+ return this->ct_child;
+ };
+
+ int get_fd() const
+ {
+ return this->ct_master;
+ };
+
+protected:
+ pid_t ct_child;
+ auto_fd ct_master;
+ struct termios ct_termios;
+ struct winsize ct_winsize;
+};
+
+/**
+ * @param fd The file descriptor to switch to raw mode.
+ * @return Zero on success, -1 on error.
+ */
+static int
+tty_raw(int fd)
+{
+ struct termios attr[1];
+
+ assert(fd >= 0);
+
+ if (tcgetattr(fd, attr) == -1)
+ return -1;
+
+ attr->c_lflag &= ~(ECHO | ICANON | IEXTEN);
+ attr->c_iflag &= ~(ICRNL | INPCK | ISTRIP | IXON);
+ attr->c_cflag &= ~(CSIZE | PARENB);
+ attr->c_cflag |= (CS8);
+ attr->c_oflag &= ~(OPOST);
+ attr->c_cc[VMIN] = 1;
+ attr->c_cc[VTIME] = 0;
+
+ return tcsetattr(fd, TCSANOW, attr);
+}
+
+static void
+dump_memory(FILE* dst, const char* src, int len)
+{
+ int lpc;
+
+ for (lpc = 0; lpc < len; lpc++) {
+ fprintf(dst, "%02x", src[lpc] & 0xff);
+ }
+}
+
+static std::vector<char>
+hex2bits(const char* src)
+{
+ std::vector<char> retval;
+
+ for (size_t lpc = 0; src[lpc] && isdigit(src[lpc]); lpc += 2) {
+ int val;
+
+ sscanf(&src[lpc], "%2x", &val);
+ retval.push_back((char) val);
+ }
+
+ return retval;
+}
+
+static const char*
+tstamp()
+{
+ static char buf[64];
+
+ struct timeval tv;
+ gettimeofday(&tv, nullptr);
+ strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S.", localtime(&tv.tv_sec));
+ auto dlen = strlen(buf);
+ snprintf(&buf[dlen], sizeof(buf) - dlen, "%.06d", tv.tv_usec);
+
+ return buf;
+}
+
+typedef enum {
+ CT_WRITE,
+} command_type_t;
+
+struct command {
+ command_type_t c_type;
+ vector<char> c_arg;
+};
+
+static struct {
+ const char* sd_program_name{nullptr};
+ sig_atomic_t sd_looping{true};
+
+ pid_t sd_child_pid{-1};
+
+ ghc::filesystem::path sd_actual_name;
+ auto_mem<FILE> sd_from_child{fclose};
+ ghc::filesystem::path sd_expected_name;
+
+ deque<struct command> sd_replay;
+} scripty_data;
+
+static const std::map<std::string, std::string> CSI_TO_DESC = {
+ {")0", "Use alt charset"},
+
+ {"[?1000l", "Don't Send Mouse X & Y"},
+ {"[?1002l", "Don’t Use Cell Motion Mouse Tracking"},
+ {"[?1006l", "Don't ..."},
+ {"[?1h", "Application cursor keys"},
+ {"[?1l", "Normal cursor keys"},
+ {"[?47h", "Use alternate screen buffer"},
+ {"[?47l", "Use normal screen buffer"},
+ {"[2h", "Set Keyboard Action mode"},
+ {"[4h", "Set Replace mode"},
+ {"[12h", "Set Send/Receive mode"},
+ {"[20h", "Set Normal Linefeed mode"},
+ {"[2l", "Reset Keyboard Action mode"},
+ {"[4l", "Reset Replace mode"},
+ {"[12l", "Reset Send/Receive mode"},
+ {"[20l", "Reset Normal Linefeed mode"},
+ {"[2J", "Erase all"},
+};
+
+struct term_machine {
+ enum class state {
+ NORMAL,
+ ESCAPE_START,
+ ESCAPE_FIXED_LENGTH,
+ ESCAPE_VARIABLE_LENGTH,
+ ESCAPE_OSC,
+ };
+
+ struct term_attr {
+ term_attr(size_t pos, const std::string& desc)
+ : ta_pos(pos), ta_end(pos), ta_desc({desc})
+ {
+ }
+
+ term_attr(size_t pos, size_t end, const std::string& desc)
+ : ta_pos(pos), ta_end(end), ta_desc({desc})
+ {
+ }
+
+ size_t ta_pos;
+ size_t ta_end;
+ std::vector<std::string> ta_desc;
+ };
+
+ term_machine(child_term& ct) : tm_child_term(ct)
+ {
+ this->clear();
+ }
+
+ ~term_machine()
+ {
+ this->flush_line();
+ }
+
+ void clear()
+ {
+ std::fill(begin(this->tm_line), end(this->tm_line), ' ');
+ this->tm_line_attrs.clear();
+ this->tm_new_data = false;
+ }
+
+ void add_line_attr(const std::string& desc)
+ {
+ if (!this->tm_line_attrs.empty()
+ && this->tm_line_attrs.back().ta_pos == this->tm_cursor_x)
+ {
+ this->tm_line_attrs.back().ta_desc.emplace_back(desc);
+ } else {
+ this->tm_line_attrs.emplace_back(this->tm_cursor_x, desc);
+ }
+ }
+
+ void write_char(char ch)
+ {
+ if (isprint(ch)) {
+ require(ch);
+
+ this->tm_new_data = true;
+ this->tm_line[this->tm_cursor_x++] = (unsigned char) ch;
+ } else {
+ switch (ch) {
+ case '\a':
+ this->flush_line();
+ fprintf(scripty_data.sd_from_child, "CTRL bell\n");
+ break;
+ case '\x08':
+ this->add_line_attr("backspace");
+ if (this->tm_cursor_x > 0) {
+ this->tm_cursor_x -= 1;
+ }
+ break;
+ case '\r':
+ this->add_line_attr("carriage-return");
+ this->tm_cursor_x = 0;
+ break;
+ case '\n':
+ this->flush_line();
+ if (this->tm_cursor_y >= 0) {
+ this->tm_cursor_y += 1;
+ }
+ this->tm_cursor_x = 0;
+ break;
+ case '\x0e':
+ this->tm_shift_start = this->tm_cursor_x;
+ break;
+ case '\x0f':
+ if (this->tm_shift_start != this->tm_cursor_x) {
+ this->tm_line_attrs.emplace_back(
+ this->tm_shift_start, this->tm_cursor_x, "alt");
+ }
+ break;
+ default:
+ require(ch);
+ this->tm_new_data = true;
+ this->tm_line[this->tm_cursor_x++] = (unsigned char) ch;
+ break;
+ }
+ }
+ }
+
+ void flush_line()
+ {
+ if (std::exchange(this->tm_waiting_on_input, false)
+ && !this->tm_user_input.empty())
+ {
+ fprintf(stderr, "%s:flush keys\n", tstamp());
+ fprintf(scripty_data.sd_from_child, "K ");
+ dump_memory(
+ scripty_data.sd_from_child, this->tm_user_input.data(), 1);
+ fprintf(scripty_data.sd_from_child, "\n");
+ this->tm_user_input.erase(this->tm_user_input.begin());
+ }
+ if (this->tm_new_data || !this->tm_line_attrs.empty()) {
+ // fprintf(scripty_data.sd_from_child, "flush %d\n",
+ // this->tm_flush_count);
+ fprintf(stderr, "%s:flush %zu\n", tstamp(), this->tm_flush_count++);
+ fprintf(
+ scripty_data.sd_from_child, "S % 3d \u250B", this->tm_cursor_y);
+ for (auto uch : this->tm_line) {
+ ww898::utf::utf8::write(uch, [](auto ch) {
+ fputc(ch, scripty_data.sd_from_child);
+ });
+ }
+ fprintf(scripty_data.sd_from_child, "\u250B\n");
+ for (size_t lpc = 0; lpc < this->tm_line_attrs.size(); lpc++) {
+ const auto& ta = this->tm_line_attrs[lpc];
+ auto full_desc = fmt::format(
+ "{}",
+ fmt::join(ta.ta_desc.begin(), ta.ta_desc.end(), ", "));
+ int line_len;
+
+ if (ta.ta_pos == ta.ta_end) {
+ line_len = fprintf(
+ scripty_data.sd_from_child,
+ "A %s%s %s",
+ repeat("\u00B7", ta.ta_pos).c_str(),
+ ((lpc + 1 < this->tm_line_attrs.size())
+ && (ta.ta_pos == this->tm_line_attrs[lpc + 1].ta_pos))
+ ? "\u251C"
+ : "\u2514",
+ full_desc.c_str());
+ line_len -= 2 + ta.ta_pos;
+ } else {
+ line_len = fprintf(
+ scripty_data.sd_from_child,
+ "A %s%s%s\u251b %s",
+ std::string(ta.ta_pos, ' ').c_str(),
+ ((lpc + 1 < this->tm_line_attrs.size())
+ && (ta.ta_pos == this->tm_line_attrs[lpc + 1].ta_pos))
+ ? "\u2518"
+ : "\u2514",
+ std::string(ta.ta_end - ta.ta_pos - 1, '-').c_str(),
+ full_desc.c_str());
+ line_len -= 4;
+ }
+ for (size_t lpc2 = lpc + 1; lpc2 < this->tm_line_attrs.size();
+ lpc2++) {
+ auto bar_pos = 7 + this->tm_line_attrs[lpc2].ta_pos;
+
+ if (bar_pos < line_len) {
+ continue;
+ }
+ line_len += fprintf(
+ scripty_data.sd_from_child,
+ "%s\u2502",
+ std::string(bar_pos - line_len, ' ').c_str());
+ line_len -= 2;
+ }
+ fprintf(scripty_data.sd_from_child, "\n");
+ }
+ this->clear();
+ }
+ fflush(scripty_data.sd_from_child);
+ }
+
+ std::vector<int> get_m_params()
+ {
+ std::vector<int> retval;
+ size_t index = 1;
+
+ while (index < this->tm_escape_buffer.size()) {
+ int val, last;
+
+ if (sscanf(&this->tm_escape_buffer[index], "%d%n", &val, &last)
+ == 1) {
+ retval.push_back(val);
+ index += last;
+ if (this->tm_escape_buffer[index] != ';') {
+ break;
+ }
+ index += 1;
+ } else {
+ break;
+ }
+ }
+
+ return retval;
+ }
+
+ void new_user_input(char ch)
+ {
+ this->tm_user_input.push_back(ch);
+ }
+
+ void new_input(char ch)
+ {
+ if (this->tm_unicode_remaining > 0) {
+ this->tm_unicode_buffer.push_back(ch);
+ this->tm_unicode_remaining -= 1;
+ if (this->tm_unicode_remaining == 0) {
+ this->tm_new_data = true;
+ this->tm_line[this->tm_cursor_x++]
+ = ww898::utf::utf8::read([this]() {
+ auto retval = this->tm_unicode_buffer.front();
+
+ this->tm_unicode_buffer.pop_front();
+ return retval;
+ });
+ }
+ return;
+ } else {
+ auto utfsize = ww898::utf::utf8::char_size(
+ [ch]() { return std::make_pair(ch, 16); });
+
+ if (utfsize.unwrap() > 1) {
+ this->tm_unicode_remaining = utfsize.unwrap() - 1;
+ this->tm_unicode_buffer.push_back(ch);
+ return;
+ }
+ }
+
+ switch (this->tm_state) {
+ case state::NORMAL: {
+ switch (ch) {
+ case '\x1b': {
+ this->tm_escape_buffer.clear();
+ this->tm_state = state::ESCAPE_START;
+ break;
+ }
+ default: {
+ this->write_char(ch);
+ break;
+ }
+ }
+ break;
+ }
+ case state::ESCAPE_START: {
+ switch (ch) {
+ case '[': {
+ this->tm_escape_buffer.push_back(ch);
+ this->tm_state = state::ESCAPE_VARIABLE_LENGTH;
+ break;
+ }
+ case ']': {
+ this->tm_escape_buffer.push_back(ch);
+ this->tm_state = state::ESCAPE_OSC;
+ break;
+ }
+ case '(':
+ case ')':
+ case '*':
+ case '+': {
+ this->tm_state = state::ESCAPE_FIXED_LENGTH;
+ this->tm_escape_buffer.push_back(ch);
+ this->tm_escape_expected_size = 2;
+ break;
+ }
+ default: {
+ this->flush_line();
+ switch (ch) {
+ case '7':
+ fprintf(scripty_data.sd_from_child,
+ "CTRL save cursor\n");
+ break;
+ case '8':
+ fprintf(scripty_data.sd_from_child,
+ "CTRL restore cursor\n");
+ break;
+ case '>':
+ fprintf(scripty_data.sd_from_child,
+ "CTRL Normal keypad\n");
+ break;
+ default: {
+ fprintf(scripty_data.sd_from_child,
+ "CTRL %c\n",
+ ch);
+ break;
+ }
+ }
+ this->tm_state = state::NORMAL;
+ break;
+ }
+ }
+ break;
+ }
+ case state::ESCAPE_FIXED_LENGTH: {
+ this->tm_escape_buffer.push_back(ch);
+ if (this->tm_escape_buffer.size()
+ == this->tm_escape_expected_size) {
+ auto iter = CSI_TO_DESC.find(
+ std::string(this->tm_escape_buffer.data(),
+ this->tm_escape_buffer.size()));
+ this->flush_line();
+ if (iter == CSI_TO_DESC.end()) {
+ fprintf(scripty_data.sd_from_child,
+ "CTRL %.*s\n",
+ (int) this->tm_escape_buffer.size(),
+ this->tm_escape_buffer.data());
+ } else {
+ fprintf(scripty_data.sd_from_child,
+ "CTRL %s\n",
+ iter->second.c_str());
+ }
+ this->tm_state = state::NORMAL;
+ }
+ break;
+ }
+ case state::ESCAPE_VARIABLE_LENGTH: {
+ this->tm_escape_buffer.push_back(ch);
+ if (isalpha(ch)) {
+ auto iter = CSI_TO_DESC.find(
+ std::string(this->tm_escape_buffer.data(),
+ this->tm_escape_buffer.size()));
+ if (iter == CSI_TO_DESC.end()) {
+ this->tm_escape_buffer.push_back('\0');
+ switch (ch) {
+ case 'A': {
+ auto amount = this->get_m_params();
+ int count = 1;
+
+ if (!amount.empty()) {
+ count = amount[0];
+ }
+ this->flush_line();
+ this->tm_cursor_y -= count;
+ if (this->tm_cursor_y < 0) {
+ this->tm_cursor_y = 0;
+ }
+ break;
+ }
+ case 'B': {
+ auto amount = this->get_m_params();
+ int count = 1;
+
+ if (!amount.empty()) {
+ count = amount[0];
+ }
+ this->flush_line();
+ this->tm_cursor_y += count;
+ break;
+ }
+ case 'C': {
+ auto amount = this->get_m_params();
+ int count = 1;
+
+ if (!amount.empty()) {
+ count = amount[0];
+ }
+ this->tm_cursor_x += count;
+ break;
+ }
+ case 'J': {
+ auto param = this->get_m_params();
+
+ this->flush_line();
+
+ auto region = param.empty() ? 0 : param[0];
+ switch (region) {
+ case 0:
+ fprintf(scripty_data.sd_from_child,
+ "CSI Erase Below\n");
+ break;
+ case 1:
+ fprintf(scripty_data.sd_from_child,
+ "CSI Erase Above\n");
+ break;
+ case 2:
+ fprintf(scripty_data.sd_from_child,
+ "CSI Erase All\n");
+ break;
+ case 3:
+ fprintf(scripty_data.sd_from_child,
+ "CSI Erase Saved Lines\n");
+ break;
+ }
+ break;
+ }
+ case 'K': {
+ auto param = this->get_m_params();
+
+ this->flush_line();
+
+ auto region = param.empty() ? 0 : param[0];
+ switch (region) {
+ case 0:
+ fprintf(scripty_data.sd_from_child,
+ "CSI Erase to Right\n");
+ break;
+ case 1:
+ fprintf(scripty_data.sd_from_child,
+ "CSI Erase to Left\n");
+ break;
+ case 2:
+ fprintf(scripty_data.sd_from_child,
+ "CSI Erase All\n");
+ break;
+ }
+ break;
+ }
+ case 'H': {
+ auto coords = this->get_m_params();
+
+ if (coords.empty()) {
+ coords = {1, 1};
+ }
+ this->flush_line();
+ this->tm_cursor_y = coords[0];
+ this->tm_cursor_x = coords[1] - 1;
+ break;
+ }
+ case 'r': {
+ auto region = this->get_m_params();
+
+ this->flush_line();
+ fprintf(scripty_data.sd_from_child,
+ "CSI set scrolling region %d-%d\n",
+ region[0],
+ region[1]);
+ break;
+ }
+ case 'm': {
+ auto attrs = this->get_m_params();
+
+ if (attrs.empty()) {
+ this->add_line_attr("normal");
+ } else if ((30 <= attrs[0]) && (attrs[0] <= 37))
+ {
+ auto xt = xterm_colors();
+
+ this->add_line_attr(fmt::format(
+ "fg({})",
+ xt->tc_palette[attrs[0] - 30].xc_hex));
+ } else if (attrs[0] == 38) {
+ auto xt = xterm_colors();
+
+ require(attrs[1] == 5);
+ this->add_line_attr(fmt::format(
+ "fg({})",
+ xt->tc_palette[attrs[2]].xc_hex));
+ } else if ((40 <= attrs[0]) && (attrs[0] <= 47))
+ {
+ auto xt = xterm_colors();
+
+ this->add_line_attr(fmt::format(
+ "bg({})",
+ xt->tc_palette[attrs[0] - 40].xc_hex));
+ } else if (attrs[0] == 48) {
+ auto xt = xterm_colors();
+
+ require(attrs[1] == 5);
+ this->add_line_attr(fmt::format(
+ "bg({})",
+ xt->tc_palette[attrs[2]].xc_hex));
+ } else {
+ switch (attrs[0]) {
+ case 1:
+ this->add_line_attr("bold");
+ break;
+ case 4:
+ this->add_line_attr("underline");
+ break;
+ case 5:
+ this->add_line_attr("blink");
+ break;
+ case 7:
+ this->add_line_attr("inverse");
+ break;
+ default:
+ this->add_line_attr(
+ this->tm_escape_buffer.data());
+ break;
+ }
+ }
+ break;
+ }
+ default:
+ fprintf(stderr, "%s:missed %c\n", tstamp(), ch);
+ this->add_line_attr(
+ this->tm_escape_buffer.data());
+ break;
+ }
+ } else {
+ this->flush_line();
+ fprintf(scripty_data.sd_from_child,
+ "CSI %s\n",
+ iter->second.c_str());
+ }
+ this->tm_state = state::NORMAL;
+ } else {
+ }
+ break;
+ }
+ case state::ESCAPE_OSC: {
+ if (ch == '\a') {
+ this->tm_escape_buffer.push_back('\0');
+
+ auto num = this->get_m_params();
+ auto semi_index
+ = strchr(this->tm_escape_buffer.data(), ';');
+
+ switch (num[0]) {
+ case 0: {
+ this->flush_line();
+ fprintf(scripty_data.sd_from_child,
+ "OSC Set window title: %s\n",
+ semi_index + 1);
+ break;
+ }
+ case 999: {
+ this->flush_line();
+ this->tm_waiting_on_input = true;
+ if (!scripty_data.sd_replay.empty()) {
+ const auto& cmd
+ = scripty_data.sd_replay.front();
+
+ this->tm_user_input = cmd.c_arg;
+ write(this->tm_child_term.get_fd(),
+ this->tm_user_input.data(),
+ this->tm_user_input.size());
+
+ scripty_data.sd_replay.pop_front();
+ }
+ break;
+ }
+ }
+
+ this->tm_state = state::NORMAL;
+ } else {
+ this->tm_escape_buffer.push_back(ch);
+ }
+ break;
+ }
+ }
+ }
+
+ child_term& tm_child_term;
+ bool tm_waiting_on_input{false};
+ state tm_state{state::NORMAL};
+ std::vector<char> tm_escape_buffer;
+ std::deque<uint8_t> tm_unicode_buffer;
+ size_t tm_unicode_remaining{0};
+ size_t tm_escape_expected_size{0};
+ uint32_t tm_line[80];
+ bool tm_new_data{false};
+ size_t tm_cursor_x{0};
+ int tm_cursor_y{-1};
+ size_t tm_shift_start{0};
+ std::vector<term_attr> tm_line_attrs;
+
+ std::vector<char> tm_user_input;
+
+ size_t tm_flush_count{0};
+};
+
+static void
+sigchld(int sig)
+{
+}
+
+static void
+sigpass(int sig)
+{
+ kill(scripty_data.sd_child_pid, sig);
+}
+
+static void
+usage()
+{
+ const char* usage_msg
+ = "usage: %s [-h] [-t to_child] [-f from_child] -- <cmd>\n"
+ "\n"
+ "Recorder for TTY I/O from a child process."
+ "\n"
+ "Options:\n"
+ " -h Print this message, then exit.\n"
+ " -n Do not pass the output to the console.\n"
+ " -i Pass stdin to the child process instead of connecting\n"
+ " the child to the tty.\n"
+ " -a <file> The file where the actual I/O from/to the child "
+ "process\n"
+ " should be stored.\n"
+ " -e <file> The file containing the expected I/O from/to the "
+ "child\n"
+ " process.\n"
+ "\n"
+ "Examples:\n"
+ " To record a session for playback later:\n"
+ " $ scripty -a output.0 -- myCursesApp\n"
+ "\n"
+ " To replay the recorded session:\n"
+ " $ scripty -e input.0 -- myCursesApp\n";
+
+ fprintf(stderr, usage_msg, scripty_data.sd_program_name);
+}
+
+int
+main(int argc, char* argv[])
+{
+ int c, fd, retval = EXIT_SUCCESS;
+ bool passout = true, passin = false, prompt = false;
+ auto_mem<FILE> file(fclose);
+
+ scripty_data.sd_program_name = argv[0];
+ scripty_data.sd_looping = true;
+
+ while ((c = getopt(argc, argv, "ha:e:nip")) != -1) {
+ switch (c) {
+ case 'h':
+ usage();
+ exit(retval);
+ break;
+ case 'a':
+ scripty_data.sd_actual_name = optarg;
+ break;
+ case 'e':
+ scripty_data.sd_expected_name = optarg;
+ if ((file = fopen(optarg, "r")) == nullptr) {
+ fprintf(
+ stderr, "%s:error: cannot open %s\n", tstamp(), optarg);
+ retval = EXIT_FAILURE;
+ } else {
+ char line[32 * 1024];
+
+ while (fgets(line, sizeof(line), file)) {
+ if (line[0] == 'K') {
+ struct command cmd;
+
+ cmd.c_type = CT_WRITE;
+ cmd.c_arg = hex2bits(&line[2]);
+ scripty_data.sd_replay.push_back(cmd);
+ }
+ }
+ }
+ break;
+ case 'n':
+ passout = false;
+ break;
+ case 'i':
+ passin = true;
+ break;
+ case 'p':
+ prompt = true;
+ break;
+ default:
+ fprintf(stderr, "%s:error: unknown flag -- %c\n", tstamp(), c);
+ retval = EXIT_FAILURE;
+ break;
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (!scripty_data.sd_expected_name.empty()
+ && scripty_data.sd_actual_name.empty())
+ {
+ scripty_data.sd_actual_name = scripty_data.sd_expected_name.filename();
+ scripty_data.sd_actual_name += ".tmp";
+ }
+
+ if (!scripty_data.sd_actual_name.empty()) {
+ if ((scripty_data.sd_from_child
+ = fopen(scripty_data.sd_actual_name.c_str(), "w"))
+ == nullptr)
+ {
+ fprintf(stderr,
+ "error: unable to open %s -- %s\n",
+ scripty_data.sd_actual_name.c_str(),
+ strerror(errno));
+ retval = EXIT_FAILURE;
+ }
+ }
+
+ if (scripty_data.sd_from_child != nullptr) {
+ fcntl(fileno(scripty_data.sd_from_child), F_SETFD, 1);
+ }
+
+ if (retval != EXIT_FAILURE) {
+ guard_termios gt(STDOUT_FILENO);
+ fd = open("/tmp/scripty.err", O_WRONLY | O_CREAT | O_APPEND, 0666);
+ dup2(fd, STDERR_FILENO);
+ close(fd);
+ fprintf(stderr, "%s:startup\n", tstamp());
+
+ child_term ct(passin);
+
+ if (ct.is_child()) {
+ execvp(argv[0], argv);
+ perror("execvp");
+ exit(-1);
+ } else {
+ int maxfd;
+ struct timeval last, now;
+ fd_set read_fds;
+ term_machine tm(ct);
+ size_t last_replay_size = scripty_data.sd_replay.size();
+
+ scripty_data.sd_child_pid = ct.get_child_pid();
+ signal(SIGINT, sigpass);
+ signal(SIGTERM, sigpass);
+
+ signal(SIGCHLD, sigchld);
+
+ gettimeofday(&now, nullptr);
+ last = now;
+
+ FD_ZERO(&read_fds);
+ FD_SET(STDIN_FILENO, &read_fds);
+ FD_SET(ct.get_fd(), &read_fds);
+
+ fprintf(stderr, "%s:goin in the loop\n", tstamp());
+
+ tty_raw(STDIN_FILENO);
+
+ maxfd = max(STDIN_FILENO, ct.get_fd());
+ while (scripty_data.sd_looping) {
+ fd_set ready_rfds = read_fds;
+ struct timeval diff, to;
+ int rc;
+
+ to.tv_sec = 0;
+ to.tv_usec = 10000;
+ rc = select(maxfd + 1, &ready_rfds, nullptr, nullptr, &to);
+ gettimeofday(&now, nullptr);
+ timersub(&now, &last, &diff);
+ if (diff.tv_sec > 10) {
+ fprintf(stderr, "%s:replay timed out!\n", tstamp());
+ scripty_data.sd_looping = false;
+ kill(ct.get_child_pid(), SIGKILL);
+ retval = EXIT_FAILURE;
+ break;
+ }
+ if (rc == 0) {
+ } else if (rc < 0) {
+ switch (errno) {
+ case EINTR:
+ break;
+ default:
+ fprintf(stderr,
+ "%s:select %s\n",
+ tstamp(),
+ strerror(errno));
+ kill(ct.get_child_pid(), SIGKILL);
+ scripty_data.sd_looping = false;
+ break;
+ }
+ } else {
+ char buffer[1024];
+
+ fprintf(stderr, "%s:fds ready %d\n", tstamp(), rc);
+ if (FD_ISSET(STDIN_FILENO, &ready_rfds)) {
+ rc = read(STDIN_FILENO, buffer, sizeof(buffer));
+ if (rc < 0) {
+ scripty_data.sd_looping = false;
+ } else if (rc == 0) {
+ FD_CLR(STDIN_FILENO, &read_fds);
+ } else {
+ log_perror(write(ct.get_fd(), buffer, rc));
+
+ for (ssize_t lpc = 0; lpc < rc; lpc++) {
+ fprintf(stderr,
+ "%s:to-child %02x\n",
+ tstamp(),
+ buffer[lpc] & 0xff);
+ tm.new_user_input(buffer[lpc]);
+ }
+ }
+ last = now;
+ }
+ if (FD_ISSET(ct.get_fd(), &ready_rfds)) {
+ rc = read(ct.get_fd(), buffer, sizeof(buffer));
+ fprintf(stderr, "%s:read rc %d\n", tstamp(), rc);
+ if (rc <= 0) {
+ scripty_data.sd_looping = false;
+ } else {
+ if (passout) {
+ log_perror(write(STDOUT_FILENO, buffer, rc));
+ }
+ if (scripty_data.sd_from_child != nullptr) {
+ for (size_t lpc = 0; lpc < rc; lpc++) {
+#if 0
+ fprintf(stderr, "%s:from-child %02x\n",
+ tstamp(),
+ buffer[lpc] & 0xff);
+#endif
+ tm.new_input(buffer[lpc]);
+ if (scripty_data.sd_replay.size()
+ != last_replay_size) {
+ last = now;
+ last_replay_size
+ = scripty_data.sd_replay.size();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ retval = ct.wait_for_child() || retval;
+ }
+
+ if (retval == EXIT_SUCCESS && !scripty_data.sd_expected_name.empty()) {
+ auto cmd = fmt::format("diff -ua {} {}",
+ scripty_data.sd_expected_name.string(),
+ scripty_data.sd_actual_name.string());
+ auto rc = system(cmd.c_str());
+ if (rc != 0) {
+ if (prompt) {
+ char resp[4];
+
+ printf("Would you like to update the original file? (y/N) ");
+ fflush(stdout);
+ log_perror(scanf("%3s", resp));
+ if (strcasecmp(resp, "y") == 0) {
+ printf("Updating: %s -> %s\n",
+ scripty_data.sd_actual_name.c_str(),
+ scripty_data.sd_expected_name.c_str());
+
+ auto options
+ = ghc::filesystem::copy_options::overwrite_existing;
+ ghc::filesystem::copy_file(scripty_data.sd_actual_name,
+ scripty_data.sd_expected_name,
+ options);
+ } else {
+ retval = EXIT_FAILURE;
+ }
+ } else {
+ fprintf(stderr, "%s:error: mismatch\n", tstamp());
+ retval = EXIT_FAILURE;
+ }
+ }
+ }
+
+ return retval;
+}
diff --git a/test/si_test.cc b/test/si_test.cc
new file mode 100644
index 0000000..f6b89b3
--- /dev/null
+++ b/test/si_test.cc
@@ -0,0 +1,49 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <stdio.h>
+
+#include "strong_int.hh"
+
+class __dsi1_distinct;
+typedef strong_int<int, __dsi1_distinct> dsi1_t;
+class __dsi2_distinct;
+typedef strong_int<int, __dsi2_distinct> dsi2_t;
+
+STRONG_INT_TYPE(int, dsi3);
+
+int
+main(int argc, char* argv[])
+{
+ dsi1_t dsi1(0);
+ dsi2_t dsi2(1);
+ dsi3_t dsi3(2);
+
+ printf("%d\n", sizeof(dsi1));
+}
diff --git a/test/slicer.cc b/test/slicer.cc
new file mode 100644
index 0000000..a44c5bf
--- /dev/null
+++ b/test/slicer.cc
@@ -0,0 +1,99 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <string>
+#include <vector>
+
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "line_buffer.hh"
+
+using namespace std;
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ vector<file_range> index;
+ auto_fd fd;
+
+ if (argc < 2) {
+ fprintf(stderr, "error: expecting file argument\n");
+ retval = EXIT_FAILURE;
+ } else if ((fd = open(argv[1], O_RDONLY)) == -1) {
+ perror("open");
+ retval = EXIT_FAILURE;
+ } else {
+ int line_number, start, end;
+ line_buffer lb;
+ file_range range;
+
+ lb.set_fd(fd);
+ while (true) {
+ auto load_result = lb.load_next_line(range);
+
+ if (load_result.isErr()) {
+ return EXIT_FAILURE;
+ }
+
+ auto li = load_result.unwrap();
+
+ if (li.li_file_range.empty()) {
+ break;
+ }
+
+ index.emplace_back(li.li_file_range);
+
+ range = li.li_file_range;
+ }
+
+ try {
+ while (scanf("%d:%d:%d", &line_number, &start, &end) == 3) {
+ range = index[line_number];
+ auto read_result = lb.read_range(range);
+
+ if (read_result.isErr()) {
+ return EXIT_FAILURE;
+ }
+
+ auto str = to_string(read_result.unwrap());
+
+ str = str.substr(start, end - start);
+ printf("%s\n", str.c_str());
+ }
+ } catch (line_buffer::error& e) {
+ fprintf(stderr, "error: line buffer %s\n", strerror(e.e_err));
+ }
+ }
+ return retval;
+}
diff --git a/test/sql.0.in b/test/sql.0.in
new file mode 100644
index 0000000..e72339b
--- /dev/null
+++ b/test/sql.0.in
@@ -0,0 +1,6 @@
+sleep 0.913123
+write 3b
+sleep 0.822303
+write 73656c656374202a2066726f6d20706572736f6e206f726465722062792061676520646573633b0d
+sleep 9.068423
+write 71
diff --git a/test/sql.0.out b/test/sql.0.out
new file mode 100644
index 0000000..c0f0bbb
--- /dev/null
+++ b/test/sql.0.out
@@ -0,0 +1,7 @@
+read 1b29301b371b5b3f3437681b5b313b3234721b5b6d1b5b346c1b5b3f313030306c1b5b3f313030326c1b5b3f31681b3d1b5b6d1b5b6d1b5b33376d1b5b34306d1b5b313b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b323b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b333b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b343b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b353b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b363b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b373b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b383b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b393b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31303b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31313b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31323b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31333b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31343b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31353b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31363b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31373b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31383b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b31393b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b32303b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b32313b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b32323b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b32333b314820202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b32343b3148202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200820081b5b3468651b5b346c1b5b481b5b33306d1b5b34376d20546875204a756e2030362031323a31333a32302050445420202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b32333b31482020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b33316d1b5b34376d202020201b5b33306d1b5b34376d3f3a566965772048656c702020201b5b316d201b5b6d1b5b33306d1b5b34376d202020202020202020202020202020201b5b32343b3332481b5b6d1b5b6d1b5b33376d1b5b34306d652f453a204d6f766520666f72776172642f6261636b77617264207468726f756768206572726f72206d6573736167650873081b5b3468651b5b346c0d1b5b411b5b431b5b33306d1b5b34376d4c301b5b32333b31354830251b5b32333b34334830570d1b5b31421b5b6d1b5b6d1b5b33376d1b5b34306d
+# write 3b
+read 1b5b32343b3332482020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200820081b5b3468201b5b346c0d1b5b373943081b5b6d1b5b33376d1b5b34306d1b5b376d20081b5b34681b5b6d1b5b6d1b5b33376d1b5b34306d201b5b346c0d3b
+# write 73656c656374202a2066726f6d20706572736f6e206f726465722062792061676520646573633b0d
+read 73656c656374202a2066726f6d20706572736f6e206f726465722062792061676520646573630d1b5b3232411b5b4d1b5b323242202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020200820081b5b3468201b5b346c1b5b32333b3430483b1b5b333943201b5b32343b383048081b5b6d1b5b33376d1b5b34306d1b5b376d20081b5b34681b5b6d1b5b6d1b5b33376d1b5b34306d201b5b346c0d1b5b411b5b3231411b5b6d1b5b33376d1b5b34306d1b5b346d69642066697273745f6e616d65206c6173745f6e616d65201b5b33346d1b5b34306d616765201b5b6d1b5b33376d1b5b34306d1b5b316d2020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b333b31481b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b343b31481b5b33346d1b5b34376d20311b5b6d1b5b6d1b5b33376d1b5b34306d0e780f1b5b33346d1b5b34376d1b5b376d4c656d202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d0e780f1b5b33346d1b5b34376d1b5b376d4865776974742020201b5b6d1b5b6d1b5b33376d1b5b34306d0e780f1b5b33346d1b5b34376d1b5b376d203335202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b33376d1b5b34306d201b5b353b31481b5b33346d1b5b34376d201b5b6d1b5b33376d1b5b34306d301b5b6d1b5b6d1b5b33376d1b5b34306d0e780f1b5b33376d1b5b34306d5068696c2020202020201b5b6d1b5b6d1b5b33376d1b5b34306d0e780f1b5b33376d1b5b34306d4d796d616e202020201b5b6d1b5b6d1b5b33376d1b5b34306d0e780f1b5b33376d1b5b34306d203330202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b363b31481b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b373b31481b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b383b31481b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b393b31481b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b31303b31481b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b31313b31481b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b31323b31481b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b31333b31481b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b6d1b5b33376d1b5b34306d1b5b376d201b5b31343b383048201b5b31353b383048201b5b31363b383048201b5b31373b383048201b5b31383b383048201b5b31393b383048201b5b32303b383048201b5b32313b383048201b5b32323b31481b5b6d1b5b6d1b5b33376d1b5b34306d202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020202020201b5b6d1b5b33376d1b5b34306d1b5b376d201b5b32333b31481b5b6d1b5b33306d1b5b34376d204c302020202020202020203130302520202020202020202020202020202020202020202020202020202020201b5b33316d1b5b34376d202020201b5b33306d1b5b34376d3f3a566965772048656c702020201b5b316d201b5b6d1b5b33306d1b5b34376d202020202020202020202020202020201b5b32343b31481b5b6d1b5b6d1b5b33376d1b5b34306d3220726f77287329206d6174636865641b5b3633430820081b5b3468201b5b346c0d
+# write 71
+read 1b5b3f313030306c1b5b3f313030326c1b5b6d1b5b4b1b5b32343b31481b5b324a1b5b3f34376c1b380d1b5b3f316c1b3e
diff --git a/test/test_abbrev.cc b/test/test_abbrev.cc
new file mode 100644
index 0000000..81f62f9
--- /dev/null
+++ b/test/test_abbrev.cc
@@ -0,0 +1,60 @@
+/**
+ * Copyright (c) 2016, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+
+#include "base/string_util.hh"
+#include "config.h"
+
+static struct test_data {
+ const char* str{nullptr};
+ const char* abbrev_str{nullptr};
+ size_t max_len{0};
+} TEST_DATA[] = {
+ {"abc", "abc", 5},
+ {"com.example.foo.bar", "c.e.f.bar", 5},
+ {"com.example.foo.bar", "c.e.foo.bar", 15},
+ {"no dots in here", "no dots in here", 5},
+};
+
+int
+main(int argc, char* argv[])
+{
+ for (const auto& td : TEST_DATA) {
+ char buffer[1024];
+
+ strcpy(buffer, td.str);
+ size_t actual = abbreviate_str(buffer, strlen(td.str), td.max_len);
+ buffer[actual] = '\0';
+
+ printf("orig: %s\n", td.str);
+ printf(" act: %s\n", buffer);
+ assert(strcmp(buffer, td.abbrev_str) == 0);
+ }
+}
diff --git a/test/test_ansi_scrubber.cc b/test/test_ansi_scrubber.cc
new file mode 100644
index 0000000..5371a31
--- /dev/null
+++ b/test/test_ansi_scrubber.cc
@@ -0,0 +1,98 @@
+/**
+ * Copyright (c) 2013, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *
+ * @file test_ansi_scrubber.cc
+ *
+ * Test for the scrub_ansi_string function.
+ *
+ * TODO: Add a test for the ansi-colors.0.in file. It has a matrix of all the
+ * color/style combinations.
+ */
+
+#include <assert.h>
+
+#include "base/ansi_scrubber.hh"
+#include "config.h"
+#include "view_curses.hh"
+
+using namespace std;
+
+int
+main(int argc, char* argv[])
+{
+ {
+ char input[] = "Hello, \x1b[33;mWorld\x1b[0;m!";
+
+ auto new_len = erase_ansi_escapes(string_fragment::from_const(input));
+
+ printf("result '%s'\n", input);
+
+ assert(new_len == 13);
+ }
+
+ {
+ std::string boldish
+ = "\u2022\b\u2022\u2023\b\u2023 h\bhe\bel\blo\bo _\ba_\bb_\bc a\b_ "
+ "b";
+ auto boldish2 = boldish;
+ string_attrs_t sa;
+
+ sa.clear();
+ scrub_ansi_string(boldish, &sa);
+ printf("boldish %s\n", boldish.c_str());
+ assert(boldish == "\u2022\u2023 helo abc a b");
+
+ auto new_len = erase_ansi_escapes(boldish2);
+ boldish2.resize(new_len);
+ printf("boldish2 %s\n", boldish2.c_str());
+ assert(boldish2 == "\u2022\u2023 helo abc a b");
+
+ for (const auto& attr : sa) {
+ printf("attr %d:%d %s\n",
+ attr.sa_range.lr_start,
+ attr.sa_range.lr_end,
+ attr.sa_type->sat_name);
+ if (attr.sa_type == &SA_ORIGIN_OFFSET) {
+ printf(" value: %d\n", attr.sa_value.get<int64_t>());
+ }
+ }
+ }
+
+ string_attrs_t sa;
+ string str_cp;
+
+ str_cp = "Hello, World!";
+ scrub_ansi_string(str_cp, &sa);
+
+ assert(str_cp == "Hello, World!");
+ assert(sa.empty());
+
+ str_cp = "Hello\x1b[44;m, \x1b[33;mWorld\x1b[0;m!";
+ scrub_ansi_string(str_cp, &sa);
+ assert(str_cp == "Hello, World!");
+}
diff --git a/test/test_auto_fd.cc b/test/test_auto_fd.cc
new file mode 100644
index 0000000..ab45490
--- /dev/null
+++ b/test/test_auto_fd.cc
@@ -0,0 +1,82 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#include "base/auto_fd.hh"
+#include "config.h"
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ auto_fd fd1, fd2;
+ int tmp;
+
+ assert(fd1 == -1);
+ tmp = open("/dev/null", O_RDONLY);
+ assert(tmp != -1);
+ fd1 = tmp;
+ fd1 = tmp;
+ assert(fcntl(tmp, F_GETFL) >= 0);
+ fd1 = std::move(fd2);
+ assert(fcntl(tmp, F_GETFL) == -1);
+ assert(errno == EBADF);
+ assert(fd1 == -1);
+
+ tmp = open("/dev/null", O_RDONLY);
+ assert(tmp != -1);
+ fd1 = tmp;
+ *fd1.out() = STDOUT_FILENO;
+ assert(fcntl(tmp, F_GETFL) == -1);
+ assert(errno == EBADF);
+
+ {
+ auto_fd fd_cp(fd1.dup());
+
+ assert(fd1 == STDOUT_FILENO);
+ assert(fd_cp != STDOUT_FILENO);
+ assert(fd_cp != -1);
+
+ tmp = (int) fd_cp;
+ }
+ {
+ auto_fd fd_cp(fd1.dup());
+
+ assert(fd_cp == tmp);
+ }
+ assert(fd1.release() == STDOUT_FILENO);
+ assert(fd1 == -1);
+
+ return retval;
+}
diff --git a/test/test_auto_mem.cc b/test/test_auto_mem.cc
new file mode 100644
index 0000000..d6f0b55
--- /dev/null
+++ b/test/test_auto_mem.cc
@@ -0,0 +1,115 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <algorithm>
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include "base/auto_mem.hh"
+#include "config.h"
+
+struct my_data {
+ int dummy1;
+ int dummy2;
+};
+
+int free_count;
+void* last_free;
+
+void
+my_free(void* mem)
+{
+ free_count += 1;
+ last_free = mem;
+}
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+ auto_mem<struct my_data, my_free> md1, md2;
+ struct my_data md1_val, md2_val;
+
+ md1 = &md1_val;
+ assert(free_count == 0);
+ md1 = std::move(md2);
+ assert(free_count == 1);
+ assert(last_free == &md1_val);
+ assert(md1 == NULL);
+
+ md1 = &md2_val;
+ assert(free_count == 1);
+ assert(last_free == &md1_val);
+ *md1.out() = &md1_val;
+ assert(free_count == 2);
+ assert(last_free == &md2_val);
+ assert(md1.in() == &md1_val);
+
+ {
+ auto_mem<struct my_data, my_free> md_cp(std::move(md1));
+
+ assert(md1 == NULL);
+ assert(free_count == 2);
+ assert(md_cp == &md1_val);
+ }
+
+ assert(free_count == 3);
+ assert(last_free == &md1_val);
+
+ {
+ static const char* msg = "Hello, World!\nGoodbye, World!\nTest";
+
+ auto buf = auto_buffer::from(msg, strlen(msg));
+ auto first_lf = std::find(buf.begin(), buf.end(), '\n');
+ auto last_lf = std::find(buf.rbegin(), buf.rend(), '\n');
+
+ assert(std::distance(buf.begin(), first_lf) == 13);
+ assert(*first_lf == '\n');
+ assert(*last_lf == '\n');
+ auto last_lf_index = std::distance(last_lf, buf.rend()) - 1;
+ auto* last_lf_rchr = strrchr(msg, '\n');
+ assert(last_lf_index == (last_lf_rchr - msg));
+ }
+
+ {
+ auto bitmap = auto_buffer::alloc_bitmap(15);
+
+ assert(bitmap.capacity() == 2);
+ bitmap.resize_bitmap(15);
+ assert(bitmap.size() == 2);
+
+ memset(bitmap.in(), 0, bitmap.size());
+ for (size_t lpc = 0; lpc < 15; lpc++) {
+ assert(!bitmap.is_bit_set(lpc));
+ }
+ }
+
+ return retval;
+}
diff --git a/test/test_bookmarks.cc b/test/test_bookmarks.cc
new file mode 100644
index 0000000..37ae86e
--- /dev/null
+++ b/test/test_bookmarks.cc
@@ -0,0 +1,146 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include "bookmarks.hh"
+#include "config.h"
+#include "textview_curses.hh"
+
+int
+main(int argc, char* argv[])
+{
+ int lpc, retval = EXIT_SUCCESS;
+ bookmark_vector<vis_line_t> bv, bv_cp;
+
+ bv.insert_once(vis_line_t(2));
+ bv.insert_once(vis_line_t(2));
+ assert(bv.size() == 1);
+
+ bv.insert_once(vis_line_t(4));
+ bv.insert_once(vis_line_t(3));
+ assert(bv[0] == 2);
+ assert(bv[1] == 3);
+ assert(bv[2] == 4);
+
+ {
+ auto range = bv.equal_range(0_vl, 5_vl);
+
+ assert(range.first != range.second);
+ assert(*range.first == 2_vl);
+ ++range.first;
+ assert(range.first != range.second);
+ assert(*range.first == 3_vl);
+ ++range.first;
+ assert(range.first != range.second);
+ assert(*range.first == 4_vl);
+ ++range.first;
+ assert(range.first == range.second);
+ }
+
+ {
+ auto range = bv.equal_range(0_vl, 1_vl);
+
+ assert(range.first == range.second);
+ }
+
+ {
+ auto range = bv.equal_range(10_vl, 10_vl);
+
+ assert(range.first == range.second);
+ }
+
+ bv.clear();
+ assert(!bv.next(vis_line_t(0)));
+ assert(!bv.prev(vis_line_t(0)));
+ assert(!bv.next(vis_line_t(100)));
+ assert(!bv.prev(vis_line_t(100)));
+
+ bv.insert_once(vis_line_t(2));
+
+ assert(bv.next(vis_line_t(0)).value() == 2);
+ assert(!bv.next(vis_line_t(2)));
+ assert(!bv.next(vis_line_t(3)));
+
+ assert(bv.prev(vis_line_t(3)).value() == 2);
+ assert(!bv.prev(vis_line_t(2)));
+
+ bv.insert_once(vis_line_t(4));
+
+ assert(bv.next(vis_line_t(0)).value() == 2);
+ assert(bv.next(vis_line_t(2)).value() == 4);
+ assert(bv.next(vis_line_t(3)).value() == 4);
+ assert(!bv.next(vis_line_t(4)));
+
+ assert(bv.prev(vis_line_t(10)).value() == 4);
+ assert(bv.prev(vis_line_t(5)).value() == 4);
+ assert(bv.prev(vis_line_t(4)).value() == 2);
+ assert(!bv.prev(vis_line_t(2)));
+
+ bv.clear();
+
+ const int LINE_COUNT = 10000;
+
+ for (lpc = 0; lpc < 1000; lpc++) {
+ bv.insert_once(vis_line_t(random() % LINE_COUNT));
+ }
+ bv_cp = bv;
+ sort(bv_cp.begin(), bv_cp.end());
+ assert(equal(bv.begin(), bv.end(), bv_cp.begin()));
+ unique(bv_cp.begin(), bv_cp.end());
+ assert(equal(bv.begin(), bv.end(), bv_cp.begin()));
+
+ {
+ vis_line_t last_line(-1);
+
+ for (lpc = 0; lpc != -1; lpc = bv.next(vis_line_t(lpc)).value_or(-1_vl))
+ {
+ assert(lpc >= 0);
+ assert(lpc < LINE_COUNT);
+ assert(last_line < lpc);
+
+ last_line = vis_line_t(lpc);
+ }
+
+ last_line = vis_line_t(10000);
+ for (lpc = LINE_COUNT - 1; lpc != -1;
+ lpc = bv.prev(vis_line_t(lpc)).value_or(-1_vl))
+ {
+ assert(lpc >= 0);
+ assert(lpc < LINE_COUNT);
+ assert(last_line > lpc);
+
+ last_line = vis_line_t(lpc);
+ }
+ }
+
+ return retval;
+}
diff --git a/test/test_cli.sh b/test/test_cli.sh
new file mode 100644
index 0000000..be773f5
--- /dev/null
+++ b/test/test_cli.sh
@@ -0,0 +1,31 @@
+#! /bin/bash
+
+export TZ="UTC"
+export YES_COLOR=1
+
+run_cap_test ${lnav_test} -n -c 'foo'
+
+run_cap_test ${lnav_test} -d /tmp/lnav.err -t -n <<EOF
+Hello, World!
+Goodbye, World!
+EOF
+
+mkdir -p nested/sub1/sub2
+echo "2021-07-03T21:49:29 Test" > nested/sub1/sub2/test.log
+
+run_cap_test ${lnav_test} -nr nested
+
+printf "a\ba _\ba a\b_" | run_cap_test env TEST_COMMENT="overstrike bold" \
+ ${lnav_test} -n
+
+{
+ echo "This is the start of a file with long lines"
+ ${lnav_test} -nN \
+ -c ";select replicate('abcd', 2 * 1024 * 1024)" -c ':write-raw-to -'
+ echo "abcd"
+ echo "Goodbye"
+} > textfile_long_lines.0
+
+grep abcd textfile_long_lines.0 | run_cap_test \
+ ${lnav_test} -n -d /tmp/lnav.err \
+ -c ';SELECT filepath, lines FROM lnav_file'
diff --git a/test/test_cmds.sh b/test/test_cmds.sh
new file mode 100644
index 0000000..5cb0a43
--- /dev/null
+++ b/test/test_cmds.sh
@@ -0,0 +1,519 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view help" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test env TZ=UTC ${lnav_test} -n \
+ -c ":goto 2011-11-03 00:19:39" \
+ -c ";SELECT log_top_line()" \
+ ${test_dir}/logfile_bro_http.log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 1" \
+ -c ":mark" \
+ -c ":hide-unmarked-lines" \
+ -c ":goto 0" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":unix-time" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test ${lnav_test} -n \
+ -c ":unix-time abc" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test env TZ=UTC ${lnav_test} -n \
+ -c ":unix-time 1612072409" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test env TZ=UTC ${lnav_test} -n \
+ -c ":current-time" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":write-to" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ";SELECT 1 AS c1, 'Hello ' || char(10) || 'World!' AS c2" \
+ -c ":write-csv-to -" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ";SELECT 1 AS c1, 'Hello, World!' AS c2" \
+ -c ":write-table-to -" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ";SELECT 1 AS c1, 'Hello, World!' AS c2" \
+ -c ":write-raw-to -" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":write-view-to -" \
+ "${test_dir}/logfile_access_log.0"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":write-view-to --anonymize -" \
+ "${test_dir}/logfile_access_log.0"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":write-view-to --anonymize -" \
+ "${test_dir}/logfile_pretty.0"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":filter-expr timeslice(:log_time_msecs, 'bad') is not null" \
+ "${test_dir}/logfile_multiline.0"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":filter-expr :log_text LIKE '%How are%'" \
+ "${test_dir}/logfile_multiline.0"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":filter-expr not json_contains(:log_tags, '#bad')" \
+ -c ":goto 0" \
+ -c ":tag #bad" \
+ "${test_dir}/logfile_access_log.0"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":filter-expr :sc_bytes > 2000" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":filter-expr :sc_bytes # ff" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":goto 0" \
+ -c ":close" \
+ -c ":goto 0" \
+ "${test_dir}/logfile_access_log.*"
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":goto 0" \
+ -c ":hide-file" \
+ ${test_dir}/logfile_access_log.*
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":goto 0" \
+ -c ":next-mark error" \
+ -c ":prev-location" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":goto 0" \
+ -c ":next-mark error" \
+ -c ":prev-location" \
+ -c ":next-location" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":filter-in vmk" \
+ -c ":disable-filter vmk" \
+ -c ":goto 0" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":filter-in vmk" \
+ -c ":rebuild" \
+ -c ":reset-session" \
+ -c ":rebuild" \
+ -c ":goto 0" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":goto 0" \
+ -c ":filter-out vmk" \
+ -c ":toggle-filtering" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":hide-fields foobar" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":hide-fields cs_uri_stem" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":hide-fields access_log.c_ip access_log.cs_uri_stem" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -f- -n < ${test_dir}/formats/scripts/multiline-echo.lnav
+
+run_cap_test ${lnav_test} -n \
+ -c ":config /bad/option" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -nvq \
+ -c ":config /ui/clock-format" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -nv \
+ -c ":config /ui/clock-format" \
+ -c ":config /ui/clock-format abc" \
+ -c ":config /ui/clock-format" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -nv \
+ -c ":config /ui/clock-format abc" \
+ -c ":reset-config /ui/clock-format" \
+ -c ":config /ui/clock-format" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c "|${test_dir}/toplevel.lnav 123 456 789" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -f "nonexistent.lnav" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":adjust-log-time 2010-01-01T00:00:00" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":adjust-log-time -1h" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ':goto 2022-06-16Tabc' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ':goto 17:00:01.' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 1" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto -1" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 0" \
+ -c ":goto 2 hours later" \
+ ${test_dir}/logfile_syslog_with_mixed_times.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 0" \
+ -c ":goto 3:45" \
+ ${test_dir}/logfile_syslog_with_mixed_times.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto invalid" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 1" \
+ -c ":relative-goto -1" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 0" \
+ -c ":next-mark error" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto -1" \
+ -c ":prev-mark error" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 0" \
+ -c ":next-mark foobar" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-in vmk" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-in vmk" \
+ -c ":reset-session" \
+ -c ":filter-in cgi" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-in today" \
+ ${test_dir}/logfile_multiline.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-out vmk" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-out today" \
+ ${test_dir}/logfile_multiline.0
+
+cp ${test_dir}/logfile_multiline.0 logfile_append.0
+chmod ug+w logfile_append.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";update generic_log set log_mark=1" \
+ -c ":filter-in Goodbye" \
+ -c ":append-to logfile_append.0" \
+ -c ":rebuild" \
+ logfile_append.0
+
+cp ${test_dir}/logfile_multiline.0 logfile_append.0
+chmod ug+w logfile_append.0
+
+run_cap_test ${lnav_test} -n -d /tmp/lnav-search.err \
+ -c "/goodbye" \
+ -c ";update generic_log set log_mark=1" \
+ -c ":filter-in Goodbye" \
+ -c ":append-to logfile_append.0" \
+ -c ":rebuild" \
+ -c ":next-mark search" \
+ logfile_append.0
+
+cp ${test_dir}/logfile_multiline.0 logfile_append.0
+chmod ug+w logfile_append.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-out Goodbye" \
+ -c ":shexec echo '2009-07-20 22:59:30,221:ERROR:Goodbye, World!' >> logfile_append.0" \
+ -c ":rebuild" \
+ logfile_append.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-in avahi" \
+ -c ":delete-filter avahi" \
+ -c ":filter-in avahi" \
+ -c ":filter-in dnsmasq" \
+ ${test_dir}/logfile_filter.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view text" \
+ -c ":filter-in World" \
+ ${test_dir}/logfile_plain.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view text" \
+ -c ":filter-out World" \
+ ${test_dir}/logfile_plain.0
+
+TOO_MANY_FILTERS=""
+for i in `seq 1 32`; do
+ TOO_MANY_FILTERS="$TOO_MANY_FILTERS -c ':filter-out $i'"
+done
+run_cap_test eval ${lnav_test} -d /tmp/lnav.err -n \
+ $TOO_MANY_FILTERS \
+ ${test_dir}/logfile_filter.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":close" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":close" \
+ -c ":close" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":open" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":close" \
+ -c ":open ${test_dir}/logfile_multiline.0" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":close" \
+ -c ":open /non-existent" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 1" \
+ -c ":write-screen-to -" \
+ "${test_dir}/logfile_access_log.0"
+
+run_cap_test ${lnav_test} -n \
+ -c ";select * from access_log" \
+ -c ':write-json-to -' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";select * from access_log" \
+ -c ':write-jsonlines-to -' \
+ ${test_dir}/logfile_access_log.0
+
+# By setting the LNAVSECURE mode before executing the command, we will disable
+# the access to the write-json-to command and the output would just be the
+# actual display of select query rather than json output.
+export LNAVSECURE=1
+run_cap_test env TEST_COMMENT="secure mode write test" ${lnav_test} -n \
+ -c ";select * from access_log" \
+ -c ':write-json-to /tmp/bad' \
+ ${test_dir}/logfile_access_log.0
+
+unset LNAVSECURE
+
+run_cap_test ${lnav_test} -n \
+ -c ";update generic_log set log_mark=1" \
+ -c ":pipe-to sed -e 's/World!/Bork!/g' -e 's/2009//g'" \
+ ${test_dir}/logfile_multiline.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":echo Hello, World!" \
+ -c ":goto 2" \
+ -c ":pipe-line-to sed -e 's/World!/Bork!/g' -e 's/2009//g'" \
+ ${test_dir}/logfile_multiline.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 0" \
+ -c ":pipe-line-to echo \$cs_uri_stem \$sc_status" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view pretty" \
+ ${test_dir}/textfile_json_one_line.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view pretty" \
+ ${test_dir}/textfile_json_one_line.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view pretty" \
+ ${test_dir}/textfile_quoted_json.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view pretty" \
+ ${test_dir}/logfile_vami.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 0" \
+ -c ":switch-to-view pretty" \
+ ${test_dir}/logfile_pretty.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":set-min-log-level error" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":highlight foobar" \
+ -c ":clear-highlight foobar" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":clear-highlight foobar" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":zoom-to 4-hour" \
+ ${test_dir}/textfile_json_indented.0
+
+cp ${test_dir}/logfile_rollover.1 logfile_rollover.1.live
+chmod ug+w logfile_rollover.1.live
+touch -t 200711030923 logfile_rollover.1.live
+
+run_cap_test ${lnav_test} -n \
+ -c ":shexec echo 'Jan 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt' >> logfile_rollover.1.live" \
+ -c ":rebuild" \
+ -c ":switch-to-view histogram" \
+ -c ":goto 0" \
+ logfile_rollover.1.live
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 0" \
+ -c ":goto next year" \
+ logfile_rollover.1.live
+
+touch -t 200711030923 ${srcdir}/logfile_syslog.0
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view histogram" \
+ -c ":zoom-to 4-hour" \
+ ${test_dir}/logfile_syslog.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view histogram" \
+ -c ":zoom-to 1-day" \
+ ${test_dir}/logfile_syslog.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-in sudo" \
+ -c ":switch-to-view histogram" \
+ -c ":zoom-to 4-hour" \
+ ${test_dir}/logfile_syslog.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":mark-expr" \
+ ${test_dir}/logfile_syslog.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":mark-expr :log_procname lik" \
+ ${test_dir}/logfile_syslog.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":mark-expr :cs_uri_stem LIKE '%vmk%'" \
+ -c ":write-to -" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 0" \
+ -c ":mark" \
+ -c ":switch-to-view histogram" \
+ ${test_dir}/logfile_syslog.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":zoom-to bad" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -f ${test_dir}/multiline.lnav \
+ ${test_dir}/logfile_access_log.0
+
+printf "Hello, World!" | run_cap_test env TEST_COMMENT="text view" ${lnav_test} -n \
+ -c ":switch-to-view text"
+
+run_cap_test ${lnav_test} -Nnv \
+ -c ":hide-lines-before 2009-07-20T22:59:29" \
+ -c ":hide-lines-before"
+
+run_cap_test ${lnav_test} -Nnv \
+ -c ":hide-lines-after 2009-07-20T22:59:29" \
+ -c ":hide-lines-after"
+
+run_cap_test ${lnav_test} -Nnv \
+ -c ":hide-lines-before 2009-07-20T22:00:29" \
+ -c ":hide-lines-after 2009-07-20T22:59:29" \
+ -c ":hide-lines-before"
+
+run_cap_test ${lnav_test} -n \
+ -c ":hide-lines-before 2009-07-20T22:59:29" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":hide-lines-after 2009-07-20T22:59:26" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":hide-lines-after 2009-07-20T22:59:26" \
+ -c ":show-lines-before-and-after" \
+ ${test_dir}/logfile_access_log.0
+
+export XYZ="World"
+
+run_cap_test ${lnav_test} -n \
+ -c ':echo Hello, \$XYZ!' \
+ ${test_dir}/logfile_access_log.0
+
+export XYZ="World"
+
+run_cap_test ${lnav_test} -n \
+ -c ':echo -n Hello, ' \
+ -c ':echo World!' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ':echo Hello, $XYZ!' \
+ ${test_dir}/logfile_access_log.0
diff --git a/test/test_column_namer.cc b/test/test_column_namer.cc
new file mode 100644
index 0000000..7eaece7
--- /dev/null
+++ b/test/test_column_namer.cc
@@ -0,0 +1,68 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <iostream>
+
+#include "config.h"
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "column_namer.hh"
+#include "doctest/doctest.h"
+
+TEST_CASE("column_namer::default")
+{
+ column_namer cn{column_namer::language::SQL};
+
+ auto def_name0 = cn.add_column(string_fragment{});
+ CHECK(def_name0 == "col_0");
+ auto def_name1 = cn.add_column(string_fragment{});
+ CHECK(def_name1 == "col_1");
+}
+
+TEST_CASE("column_namer::no-collision")
+{
+ column_namer cn{column_namer::language::SQL};
+
+ auto name0 = cn.add_column(string_fragment{"abc"});
+ CHECK(name0 == "abc");
+ auto name1 = cn.add_column(string_fragment{"def"});
+ CHECK(name1 == "def");
+}
+
+TEST_CASE("column_namer::collisions")
+{
+ column_namer cn{column_namer::language::SQL};
+
+ auto name0 = cn.add_column(string_fragment{"abc"});
+ CHECK(name0 == "abc");
+ auto name1 = cn.add_column(string_fragment{"abc"});
+ CHECK(name1 == "abc_0");
+ auto name2 = cn.add_column(string_fragment{"abc"});
+ CHECK(name2 == "abc_1");
+}
diff --git a/test/test_config.sh b/test/test_config.sh
new file mode 100755
index 0000000..4722abb
--- /dev/null
+++ b/test/test_config.sh
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+export YES_COLOR=1
+
+export HOME="./test-config"
+export XDG_CONFIG_HOME="./test-config/.config"
+rm -rf ./test-config
+mkdir -p $HOME/.config
+
+# config write global var
+run_cap_test ${lnav_test} -nN \
+ -c ":config /global/foo bar"
+
+# config read global var
+run_cap_test ${lnav_test} -nN \
+ -c ":config /global/foo"
+
+# config bad color
+run_cap_test ${lnav_test} -n \
+ -c ":config /ui/theme-defs/default/styles/text/color #f" \
+ ${test_dir}/logfile_access_log.0
+
+# invalid min-free-space allowed?
+run_cap_test env TMPDIR=tmp ${lnav_test} -n \
+ -c ':config /tuning/archive-manager/min-free-space abc' \
+ ${srcdir}/logfile_syslog.0
+
+# config bad theme
+run_cap_test ${lnav_test} -n \
+ -c ":config /ui/theme baddy" \
+ ${test_dir}/logfile_access_log.0
+
+# config bad theme
+run_cap_test ${lnav_test} -W -n \
+ -I ${test_dir}/bad-config2 \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -nN \
+ -c ":reset-config /bad/path"
diff --git a/test/test_curl.sh b/test/test_curl.sh
new file mode 100644
index 0000000..be3d1cd
--- /dev/null
+++ b/test/test_curl.sh
@@ -0,0 +1,48 @@
+#! /bin/bash
+
+if test x"$SFTP_TEST_URL" == x""; then
+ exit 0
+fi
+
+run_test ${lnav_test} -n \
+ file://${test_dir}/logfile_access_log.0
+
+check_output "file URL is not working" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+EOF
+
+cp ${test_dir}/logfile_access_log.0 curl_access_log.0
+
+run_test ${lnav_test} -n \
+ $SFTP_TEST_URL/`pwd`/curl_access_log.0
+
+check_output "sftp URL is not working" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+EOF
+
+run_test ${lnav_test} -n \
+ -c ":poll-now" \
+ -c ":shexec echo foo >> curl_access_log.0" \
+ $SFTP_TEST_URL/`pwd`/curl_access_log.0
+
+check_output "sftp URL is not working" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+foo
+EOF
+
+run_test ${lnav_test} -n \
+ -c ":open $SFTP_TEST_URL/`pwd`/curl_access_log.0" \
+ ${test_dir}/logfile_empty.0
+
+check_output "sftp URL is not working" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+foo
+EOF
diff --git a/test/test_data_parser.sh b/test/test_data_parser.sh
new file mode 100644
index 0000000..26f8163
--- /dev/null
+++ b/test/test_data_parser.sh
@@ -0,0 +1,16 @@
+#! /bin/bash
+
+for fn in ${top_srcdir}/test/datafile_simple.*; do
+ run_test ./drive_data_scanner $fn
+ on_error_fail_with "$fn does not match"
+done
+
+for fn in ${top_srcdir}/test/datafile_xml.*; do
+ run_test ./drive_data_scanner -P $fn
+ on_error_fail_with "$fn does not match"
+done
+
+for fn in ${top_srcdir}/test/log-samples/*.txt; do
+ run_test ./drive_data_scanner -l $fn
+ on_error_fail_with "$fn does not match"
+done
diff --git a/test/test_date_time_scanner.cc b/test/test_date_time_scanner.cc
new file mode 100644
index 0000000..8a85993
--- /dev/null
+++ b/test/test_date_time_scanner.cc
@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <locale.h>
+
+#include "../src/lnav_util.hh"
+#include "base/date_time_scanner.hh"
+#include "config.h"
+
+static const char* GOOD_TIMES[] = {
+ "2017 May 08 Mon 18:57:57.578",
+ "May 01 00:00:01",
+ "May 10 12:00:01",
+ "2014-02-11 16:12:34",
+ "2014-02-11 16:12:34.123",
+ "05/18/2018 12:00:53 PM",
+ "05/18/2018 12:00:53 AM",
+};
+
+static const char* BAD_TIMES[] = {
+ "1-2-3 1:2:3",
+
+ "2013-22-01 12:01:22",
+ "2013-00-01 12:01:22",
+
+ "@4000000043",
+};
+
+int
+main(int argc, char* argv[])
+{
+ setenv("TZ", "UTC", 1);
+
+ for (const auto* good_time : GOOD_TIMES) {
+ date_time_scanner dts;
+ struct timeval tv;
+ struct exttm tm;
+ const char* rc;
+
+ rc = dts.scan(good_time, strlen(good_time), nullptr, &tm, tv);
+ printf("ret %s %p\n", good_time, rc);
+ assert(rc != nullptr);
+
+ char ts[64];
+
+ gmtime_r(&tv.tv_sec, &tm.et_tm);
+ dts.ftime(ts, sizeof(ts), nullptr, tm);
+ printf("orig %s\n", good_time);
+ printf("loop %s\n", ts);
+ assert(strcmp(ts, good_time) == 0);
+ }
+
+ {
+ static const char* OLD_TIME = "05/18/1960 12:00:53 AM";
+ date_time_scanner dts;
+ struct timeval tv;
+ struct exttm tm;
+
+ auto rc = dts.scan(OLD_TIME, strlen(OLD_TIME), nullptr, &tm, tv);
+ assert(rc != nullptr);
+ char ts[64];
+ dts.ftime(ts, sizeof(ts), nullptr, tm);
+ assert(strcmp(ts, "05/18/1980 12:00:53 AM") == 0);
+ }
+
+ {
+ date_time_scanner dts;
+ struct timeval tv;
+
+ dts.convert_to_timeval("@40000000433225833b6e1a8c", -1, nullptr, tv);
+ assert(tv.tv_sec == 1127359865);
+ assert(tv.tv_usec == 997071);
+
+ memset(&tv, 0, sizeof(tv));
+ dts.convert_to_timeval("@4000000043322583", -1, nullptr, tv);
+ assert(tv.tv_sec == 1127359865);
+ assert(tv.tv_usec == 0);
+ }
+
+ for (const auto* bad_time : BAD_TIMES) {
+ date_time_scanner dts;
+ struct timeval tv;
+ struct exttm tm;
+
+ printf("Checking bad time: %s\n", bad_time);
+ assert(dts.scan(bad_time, strlen(bad_time), nullptr, &tm, tv)
+ == nullptr);
+ }
+
+ {
+ const char* en_date = "Jan 1 12:00:00";
+ const char* es_date = " 1/Ene/2014:12:00:00 +0000";
+ struct timeval en_tv, es_tv;
+ struct exttm en_tm, es_tm;
+ date_time_scanner dts;
+
+ if (setlocale(LC_TIME, "es_ES.UTF-8") != nullptr) {
+ assert(dts.scan(en_date, strlen(en_date), nullptr, &en_tm, en_tv)
+ != nullptr);
+ dts.clear();
+ assert(dts.scan(es_date, strlen(es_date), nullptr, &es_tm, es_tv)
+ != nullptr);
+ }
+ }
+
+ {
+ const char* en_date = "Jan 1 12:00:00";
+ const char* fr_date = "août 19 11:08:37";
+ struct timeval en_tv, fr_tv;
+ struct exttm en_tm, fr_tm;
+ date_time_scanner dts;
+
+ if (setlocale(LC_TIME, "fr_FR.UTF-8") != nullptr) {
+ assert(dts.scan(en_date, strlen(en_date), nullptr, &en_tm, en_tv)
+ != nullptr);
+ dts.clear();
+ assert(dts.scan(fr_date, strlen(fr_date), nullptr, &fr_tm, fr_tv)
+ != nullptr);
+ }
+ }
+
+ {
+ const char* ts = "22:46:03.471";
+ const char* fmt[] = {
+ "%H:%M:%S.%L",
+ nullptr,
+ };
+ char buf[64];
+ date_time_scanner dts;
+ struct exttm tm;
+ struct timeval tv;
+
+ const auto* ts_end = dts.scan(ts, strlen(ts), fmt, &tm, tv);
+ assert(ts_end - ts == 12);
+ auto rc = dts.ftime(buf, sizeof(buf), fmt, tm);
+ assert(rc == 12);
+ assert(strcmp(ts, buf) == 0);
+ }
+
+ {
+ const char* epoch_str = "ts 1428721664 ]";
+ struct exttm tm;
+ off_t off = 0;
+
+ memset(&tm, 0, sizeof(tm));
+ bool rc = ptime_fmt("ts %s ]", &tm, epoch_str, off, strlen(epoch_str));
+ assert(rc);
+ assert(tm2sec(&tm.et_tm) == 1428721664);
+ }
+
+ {
+ const char* epoch_str = "ts 60150c93 ]";
+ struct exttm tm;
+ off_t off = 0;
+
+ memset(&tm, 0, sizeof(tm));
+ bool rc = ptime_fmt("ts %q ]", &tm, epoch_str, off, strlen(epoch_str));
+ assert(rc);
+ assert(tm2sec(&tm.et_tm) == 1611992211);
+
+ char buf[32];
+ ftime_fmt(buf, sizeof(buf), "ts %q ]", tm);
+ assert(strcmp(buf, epoch_str) == 0);
+ }
+}
diff --git a/test/test_events.sh b/test/test_events.sh
new file mode 100644
index 0000000..2266c4b
--- /dev/null
+++ b/test/test_events.sh
@@ -0,0 +1,31 @@
+#! /bin/bash
+
+rm -rf events-home
+mkdir -p events-home
+export HOME=events-home
+export YES_COLOR=1
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT json(content) as content FROM lnav_events' \
+ -c ':write-jsonlines-to -' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -nN \
+ -c ':config /log/watch-expressions/http-errors/expr sc_status >= 400 AND bad'
+
+run_cap_test ${lnav_test} -nN \
+ -c ':config /log/watch-expressions/http-errors/expr :sc_status >= 400'
+
+run_cap_test env TEST_COMMENT="watch expression generate detect event" ${lnav_test} -n \
+ -c ';SELECT json(content) as content FROM lnav_events' \
+ -c ':write-jsonlines-to -' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test env TEST_COMMENT="show the configuration" ${lnav_test} -nN \
+ -c ':config /log/watch-expressions'
+
+run_cap_test env TEST_COMMENT="delete the configuration" ${lnav_test} -nN \
+ -c ':reset-config /log/watch-expressions/http-errors/'
+
+run_cap_test env TEST_COMMENT="config should be gone now" ${lnav_test} -nN \
+ -c ':config /log/watch-expressions'
diff --git a/test/test_format_installer.sh b/test/test_format_installer.sh
new file mode 100644
index 0000000..2b94d3d
--- /dev/null
+++ b/test/test_format_installer.sh
@@ -0,0 +1,34 @@
+#! /bin/bash
+
+CONFIG_DIR="${top_builddir}/installer-test-home"
+
+mkdir -p "${CONFIG_DIR}"
+rm -rf "${CONFIG_DIR}/.lnav/formats"
+
+HOME=${CONFIG_DIR}
+unset XDG_CONFIG_HOME
+export HOME
+
+${lnav_test} -i ${srcdir}/formats/jsontest/format.json
+
+if ! test -f ${CONFIG_DIR}/.lnav/formats/installed/test_log.json; then
+ echo "Format not installed correctly?"
+ exit 1
+fi
+
+if test x"${TEST_GIT_INSTALL}" = x""; then
+ # Hitting the git repos frequently is slow/noisy
+ exit 0
+fi
+
+${lnav_test} -i extra
+
+if ! test -f ${CONFIG_DIR}/.lnav/remote-config/remote-config.json; then
+ echo "Remote config not downloaded?"
+ exit 1
+fi
+
+if ! test -d ${CONFIG_DIR}/.lnav/formats/https___github_com_PaulWay_lnav_formats_git; then
+ echo "Third-party repo not downloaded?"
+ exit 1
+fi
diff --git a/test/test_format_loader.sh b/test/test_format_loader.sh
new file mode 100644
index 0000000..e5bb422
--- /dev/null
+++ b/test/test_format_loader.sh
@@ -0,0 +1,17 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+run_cap_test ${lnav_test} -W -C \
+ -I ${test_dir}/bad-config-json
+
+if test x"$HAVE_SQLITE3_ERROR_OFFSET" != x""; then
+ run_cap_test env LC_ALL=C ${lnav_test} -W -C \
+ -I ${test_dir}/bad-config
+fi
+
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ";select * from leveltest_log" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_leveltest.0
diff --git a/test/test_grep_proc.sh b/test/test_grep_proc.sh
new file mode 100644
index 0000000..70b19ba
--- /dev/null
+++ b/test/test_grep_proc.sh
@@ -0,0 +1,57 @@
+#! /bin/bash
+
+cat > gp.dat <<EOF
+Hello, World!
+Goodbye, World?
+EOF
+
+grep_slice() {
+ ./drive_grep_proc "$1" "$2" | ./slicer "$2"
+}
+
+grep_capture() {
+ ./drive_grep_proc "$1" "$2" 1>/dev/null
+}
+
+run_test grep_slice 'Hello' gp.dat
+
+check_output "grep_proc didn't find the right match?" <<EOF
+Hello
+EOF
+
+run_test grep_slice '.*' gp.dat
+
+check_output "grep_proc didn't find all lines?" <<EOF
+Hello, World!
+
+
+Goodbye, World?
+
+
+EOF
+
+run_test grep_slice '\w+,' gp.dat
+
+check_output "grep_proc didn't find the right matches?" <<EOF
+Hello,
+Goodbye,
+EOF
+
+run_test grep_slice '\w+.' gp.dat
+
+check_output "grep_proc didn't find multiple matches?" <<EOF
+Hello,
+World!
+Goodbye,
+World?
+EOF
+
+run_test grep_capture '(\w+), World' gp.dat
+
+check_error_output "grep_proc didn't capture matches?" <<EOF
+0(0:5)Hello
+1(0:7)Goodbye
+EOF
+
+check_output "grep_proc didn't capture matches?" <<EOF
+EOF
diff --git a/test/test_grep_proc2.cc b/test/test_grep_proc2.cc
new file mode 100644
index 0000000..11e9d1c
--- /dev/null
+++ b/test/test_grep_proc2.cc
@@ -0,0 +1,150 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+
+#include "config.h"
+#include "grep_proc.hh"
+#include "vis_line.hh"
+
+using namespace std;
+
+static struct {
+ int l_number;
+ const char* l_value;
+} MS_LINES[] = {
+ {10, ""},
+ {11, ""},
+ {12, ""},
+ {13, ""},
+ {0, ""},
+ {1, ""},
+ {2, ""},
+};
+
+class my_source : public grep_proc_source<vis_line_t> {
+public:
+ my_source() : ms_current_line(0){};
+
+ bool grep_value_for_line(vis_line_t line_number, string& value_out)
+ {
+ bool retval = true;
+
+ assert(line_number == MS_LINES[this->ms_current_line].l_number);
+ value_out = MS_LINES[this->ms_current_line].l_value;
+
+ this->ms_current_line += 1;
+
+ return retval;
+ };
+
+ int ms_current_line;
+};
+
+class my_sleeper_source : public grep_proc_source<vis_line_t> {
+ bool grep_value_for_line(vis_line_t line_number, string& value_out)
+ {
+ sleep(1000);
+ return true;
+ };
+};
+
+class my_sink : public grep_proc_sink<vis_line_t> {
+public:
+ my_sink() : ms_finished(false){};
+
+ void grep_match(grep_proc<vis_line_t>& gp,
+ vis_line_t line,
+ int start,
+ int end){};
+
+ void grep_end(grep_proc<vis_line_t>& gp) { this->ms_finished = true; };
+
+ bool ms_finished;
+};
+
+static void
+looper(grep_proc<vis_line_t>& gp)
+{
+ my_sink msink;
+
+ gp.set_sink(&msink);
+
+ while (!msink.ms_finished) {
+ vector<struct pollfd> pollfds;
+
+ gp.update_poll_set(pollfds);
+ poll(&pollfds[0], pollfds.size(), -1);
+
+ gp.check_poll_set(pollfds);
+ }
+}
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+
+ auto code
+ = lnav::pcre2pp::code::from_const("foobar", PCRE2_CASELESS).to_shared();
+
+ auto psuperv = std::make_shared<pollable_supervisor>();
+ {
+ my_source ms;
+ grep_proc<vis_line_t> gp(code, ms, psuperv);
+
+ gp.queue_request(10_vl, 14_vl);
+ gp.queue_request(0_vl, 3_vl);
+ gp.start();
+ looper(gp);
+ }
+
+ {
+ my_sleeper_source mss;
+ grep_proc<vis_line_t>* gp
+ = new grep_proc<vis_line_t>(code, mss, psuperv);
+ int status;
+
+ gp->queue_request();
+ gp->start();
+
+ assert(wait3(&status, WNOHANG, NULL) == 0);
+
+ delete gp;
+
+ assert(wait(&status) == -1);
+ assert(errno == ECHILD);
+ }
+
+ return retval;
+}
diff --git a/test/test_json_format.sh b/test/test_json_format.sh
new file mode 100644
index 0000000..435e8e3
--- /dev/null
+++ b/test/test_json_format.sh
@@ -0,0 +1,156 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+# journald json log format is not working"
+run_cap_test env TZ=UTC ${lnav_test} -n \
+ -I ${test_dir} \
+ ${test_dir}/logfile_journald.json
+
+# json log format is not working"
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ ${test_dir}/logfile_json.json
+
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ':filter-in up service' \
+ ${test_dir}/logfile_json.json
+
+# json log format is not working"
+run_cap_test ${lnav_test} -n -I ${test_dir} \
+ -c ':switch-to-view pretty' \
+ -c ':switch-to-view log' \
+ -c ':switch-to-view pretty' \
+ ${test_dir}/logfile_json.json
+
+# multi-line-format json log format is not working"
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ ${test_dir}/log.clog
+
+# log levels not working"
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ';select * from test_log' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_json.json
+
+# log levels not working" < ${test_dir}/logfile_jso
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ';select log_raw_text from test_log' \
+ -c ':write-raw-to -' \
+ ${test_dir}/logfile_json.json
+
+# write-raw-to with json is not working" <
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ':goto 0' \
+ -c ':mark' \
+ -c ':goto 1' \
+ -c ':mark' \
+ -c ':goto 2' \
+ -c ':mark' \
+ -c ':write-raw-to -' \
+ ${test_dir}/log.clog
+
+# json output not working"
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ';select * from test_log' \
+ -c ':write-json-to -' \
+ ${test_dir}/logfile_json.json
+
+# timestamp-format not working"
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ ${test_dir}/logfile_json2.json
+
+# log levels not working"
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -I ${test_dir} \
+ -c ';select * from json_log2' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_json2.json
+
+# pipe-line-to is not working"
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ":goto 4" \
+ -c ":pipe-line-to sed -e 's/2013//g'" \
+ -c ":switch-to-view text" \
+ ${test_dir}/logfile_json.json
+
+# json log format is not working"
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ ${test_dir}/logfile_nested_json.json
+
+# log levels not working"
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ';select * from ntest_log' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_nested_json.json
+
+# pipe-line-to is not working"
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ":goto 4" \
+ -c ":pipe-line-to sed -e 's/2013//g'" \
+ -c ":switch-to-view text" \
+ ${test_dir}/logfile_nested_json.json
+
+# json log3 format is not working"
+run_cap_test env TZ=UTC ${lnav_test} -n \
+ -I ${test_dir} \
+ ${test_dir}/logfile_json3.json
+
+# json log3 format is not working"
+run_cap_test env TZ=UTC ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ';select * from json_log3' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_json3.json
+
+run_cap_test env TZ=America/New_York ${lnav_test} -n \
+ -I ${test_dir} \
+ ${test_dir}/logfile_json3.json
+
+# json log3 format is not working"
+run_cap_test env TZ=America/New_York ${lnav_test} -n \
+ -I ${test_dir} \
+ -c ';select * from json_log3' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_json3.json
+
+# json log format is not working"
+run_cap_test ${lnav_test} -n \
+ -d /tmp/lnav.err \
+ -I ${test_dir} \
+ ${test_dir}/logfile_invalid_json.json
+
+# json log format is not working"
+run_cap_test ${lnav_test} -n \
+ -d /tmp/lnav.err \
+ -I ${test_dir} \
+ ${test_dir}/logfile_invalid_json2.json
+
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ ${test_dir}/logfile_mixed_json2.json
+
+run_cap_test ${lnav_test} -n \
+ -I ${test_dir} \
+ ${test_dir}/logfile_json_subsec.json
+
+run_cap_test ${lnav_test} -n \
+ ${test_dir}/logfile_bunyan.0
+
+run_cap_test ${lnav_test} -n \
+ ${test_dir}/logfile_cloudflare.json
+
+run_cap_test ${lnav_test} -n \
+ -c ':show-fields RayID' \
+ ${test_dir}/logfile_cloudflare.json
diff --git a/test/test_line_buffer.sh b/test/test_line_buffer.sh
new file mode 100644
index 0000000..43a2580
--- /dev/null
+++ b/test/test_line_buffer.sh
@@ -0,0 +1,78 @@
+#! /bin/bash
+
+cp ${test_dir}/logfile_access_log.1 logfile_changed.0
+chmod u+w logfile_changed.0
+run_test ${lnav_test} -n \
+ -c ":rebuild" \
+ -c ":shexec head -1 ${test_dir}/logfile_access_log.0 > logfile_changed.0" \
+ -c ":rebuild" \
+ logfile_changed.0
+
+check_error_output "line buffer cache flush" <<EOF
+EOF
+
+check_output "line buffer cache flush is not working" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+EOF
+
+run_test ./drive_line_buffer "${top_srcdir}/src/line_buffer.hh"
+
+check_output "Line buffer output doesn't match input?" < \
+ "${top_srcdir}/src/line_buffer.hh"
+
+run_test ./drive_line_buffer < ${top_srcdir}/src/line_buffer.hh
+
+check_output "Line buffer output doesn't match input from pipe?" < \
+ "${top_srcdir}/src/line_buffer.hh"
+
+cat > lb.dat <<EOF
+1
+2
+3
+4
+5
+EOF
+
+LINE_OFF=`grep -b '4' lb.dat | cut -f 1 -d :`
+
+run_test ./drive_line_buffer -o $LINE_OFF lb.dat
+
+check_output "Seeking in the line buffer doesn't work?" <<EOF
+4
+5
+EOF
+
+run_test ./drive_line_buffer -o 4424 -c 1 ${srcdir}/UTF-8-test.txt
+
+check_output "Invalid UTF is not scrubbed?" <<EOF
+2.1.5 5 bytes (U-00200000): "?????" |
+EOF
+
+cat "${top_srcdir}/src/"*.hh "${top_srcdir}/src/"*.cc > lb-2.dat
+grep -b '$' lb-2.dat | cut -f 1 -d : > lb.index
+
+run_test ./drive_line_buffer -i lb.index -n 10 lb-2.dat
+
+check_output "Random reads don't match input?" <<EOF
+All done
+EOF
+
+gzip -c ${test_dir}/logfile_access_log.1 > lb-double.gz
+gzip -c ${test_dir}/logfile_access_log.1 >> lb-double.gz
+run_test ${lnav_test} -n lb-double.gz
+
+gzip -dc lb-double.gz | \
+ check_output "concatenated gzip files don't parse correctly"
+
+> lb-3.gz
+while test $(wc -c < lb-3.gz) -le 5000000 ; do
+ cat lb-2.dat
+done | gzip -c -1 > lb-3.gz
+gzip -dc lb-3.gz > lb-3.dat
+grep -b '$' lb-3.dat | cut -f 1 -d : > lb-3.index
+
+run_test ./drive_line_buffer -i lb-3.index -n 10 lb-3.gz lb-3.dat
+
+check_output "Random gzipped reads don't match input" <<EOF
+All done
+EOF \ No newline at end of file
diff --git a/test/test_line_buffer2.cc b/test/test_line_buffer2.cc
new file mode 100644
index 0000000..07b9765
--- /dev/null
+++ b/test/test_line_buffer2.cc
@@ -0,0 +1,157 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "base/auto_fd.hh"
+#include "config.h"
+#include "line_buffer.hh"
+
+using namespace std;
+
+static const char* TEST_DATA
+ = "Hello, World!\n"
+ "Goodbye, World!\n";
+
+static void
+single_line(const char* data)
+{
+ line_buffer lb;
+ auto_fd pi[2];
+ off_t off = 0;
+
+ assert(auto_fd::pipe(pi) == 0);
+ log_perror(write(pi[1], data, strlen(data)));
+ pi[1].reset();
+
+ lb.set_fd(pi[0]);
+ auto load_result = lb.load_next_line({off});
+ auto li = load_result.unwrap();
+ assert(data[strlen(data) - 1] == '\n' || li.li_partial);
+ assert(li.li_file_range.next_offset() == (off_t) strlen(data));
+ assert(li.li_file_range.fr_size == strlen(data));
+
+ auto next_load_result = lb.load_next_line(li.li_file_range);
+ assert(next_load_result.isOk());
+ assert(next_load_result.unwrap().li_file_range.empty());
+ assert(lb.get_file_size() != -1);
+}
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+
+ single_line("Dexter Morgan");
+ single_line("Rudy Morgan\n");
+
+ {
+ char fn_template[] = "test_line_buffer.XXXXXX";
+
+ auto fd = auto_fd(mkstemp(fn_template));
+ remove(fn_template);
+ line_buffer lb;
+
+ write(fd, TEST_DATA, strlen(TEST_DATA));
+ lseek(fd, SEEK_SET, 0);
+
+ lb.set_fd(fd);
+
+ shared_buffer_ref sbr;
+
+ auto result = lb.read_range({0, 1024});
+
+ assert(result.isErr());
+ }
+
+ {
+ static string first = "Hello";
+ static string second = ", World!";
+ static string third = "Goodbye, World!";
+ static string last = "\n";
+
+ line_buffer lb;
+ auto_fd pi[2];
+ off_t off = 0;
+
+ assert(auto_fd::pipe(pi) == 0);
+ log_perror(write(pi[1], first.c_str(), first.size()));
+ fcntl(pi[0], F_SETFL, O_NONBLOCK);
+
+ lb.set_fd(pi[0]);
+ auto load_result = lb.load_next_line({off});
+ auto li = load_result.unwrap();
+ assert(li.li_partial);
+ assert(li.li_file_range.fr_size == 5);
+ log_perror(write(pi[1], second.c_str(), second.size()));
+ auto load_result2 = lb.load_next_line({off});
+ li = load_result2.unwrap();
+ assert(li.li_partial);
+ assert(li.li_file_range.fr_size == 13);
+ log_perror(write(pi[1], last.c_str(), last.size()));
+ auto load_result3 = lb.load_next_line({off});
+ li = load_result3.unwrap();
+ assert(!li.li_partial);
+ assert(li.li_file_range.fr_size == 14);
+ auto load_result4 = lb.load_next_line(li.li_file_range);
+ li = load_result4.unwrap();
+ auto last_range = li.li_file_range;
+ assert(li.li_partial);
+ assert(li.li_file_range.empty());
+ log_perror(write(pi[1], third.c_str(), third.size()));
+ auto load_result5 = lb.load_next_line(last_range);
+ li = load_result5.unwrap();
+ assert(li.li_partial);
+ assert(li.li_file_range.fr_size == 15);
+ log_perror(write(pi[1], last.c_str(), last.size()));
+ auto load_result6 = lb.load_next_line(last_range);
+ li = load_result6.unwrap();
+ assert(!li.li_partial);
+ assert(li.li_file_range.fr_size == 16);
+
+ auto load_result7 = lb.load_next_line(li.li_file_range);
+ li = load_result7.unwrap();
+ assert(li.li_partial);
+ assert(li.li_file_range.empty());
+ assert(!lb.is_pipe_closed());
+
+ pi[1].reset();
+
+ auto load_result8 = lb.load_next_line(li.li_file_range);
+ li = load_result8.unwrap();
+ assert(!li.li_partial);
+ assert(li.li_file_range.empty());
+ assert(lb.is_pipe_closed());
+ }
+
+ return retval;
+}
diff --git a/test/test_listview.sh b/test/test_listview.sh
new file mode 100644
index 0000000..5529008
--- /dev/null
+++ b/test/test_listview.sh
@@ -0,0 +1,83 @@
+#! /bin/bash
+
+run_test ./scripty -n -e ${srcdir}/listview_output.0 -- \
+ ./drive_listview < /dev/null
+
+on_error_fail_with "listview output does not match?"
+
+run_test ./scripty -n -e ${srcdir}/listview_output.1 -- \
+ ./drive_listview -t 1 < /dev/null
+
+on_error_fail_with "listview didn't move down?"
+
+run_test ./scripty -n -e ${srcdir}/listview_output.2 -- \
+ ./drive_listview -l 1 < /dev/null
+
+on_error_fail_with "Listview didn't move right?"
+
+run_test ./scripty -n -e ${srcdir}/listview_output.3 -- \
+ ./drive_listview -t 1 -l 1 < /dev/null
+
+on_error_fail_with "Listview didn't move left and right?"
+
+run_test ./scripty -n -e ${srcdir}/listview_output.4 -- \
+ ./drive_listview -y 1 -r 50 < /dev/null
+
+on_error_fail_with "Listview doesn't start down one line?"
+
+run_test ./scripty -n -e ${srcdir}/listview_output.5 -- \
+ ./drive_listview -y 1 -r 50 -h -1 < /dev/null
+
+on_error_fail_with "Listview isn't shorter?"
+
+run_test ./scripty -n -e ${srcdir}/listview_output.6 -- \
+ ./drive_listview -y 1 -r 50 -h -1 -t 1 < /dev/null
+
+on_error_fail_with "Listview didn't move down (2)?"
+
+###
+# Cursor mode tests
+###
+
+# Cursor appears on first line
+run_test ./scripty -n -e ${srcdir}/listview_output_cursor.0 -- \
+ ./drive_listview -c < /dev/null
+
+on_error_fail_with "Listview Cursor Mode: Didn't enable (not selectable)"
+
+# Move down within visible area between top (at 0) and tail space
+run_test ./scripty -n -e ${srcdir}/listview_output_cursor.1 -- \
+ ./drive_listview -r 20 -c -k jjjjj < /dev/null
+
+on_error_fail_with "Listview Cursor Mode: Didn-t move cursor down?"
+
+# Move up within visible area between top (at 0) and tail space
+run_test ./scripty -n -e ${srcdir}/listview_output_cursor.2 -- \
+ ./drive_listview -r 20 -c -k jjjjjkk < /dev/null
+
+on_error_fail_with "Listview Cursor Mode: Didn't move cursor up?"
+
+
+# Scroll file when reaching tail space
+run_test ./scripty -n -e ${srcdir}/listview_output_cursor.3 -- \
+ ./drive_listview -r 30 -h 5 -c -k jjjj < /dev/null
+
+on_error_fail_with "Listview Cursor Mode: Didn't scroll down when reaching tail space?"
+
+# Do not scroll up when moving up after reaching tail space
+run_test ./scripty -n -e ${srcdir}/listview_output_cursor.4 -- \
+ ./drive_listview -r 30 -h 5 -c -k jjjjk < /dev/null
+
+on_error_fail_with "Listview Cursor Mode: scrolled when moving up from tail space?"
+
+# Page down move
+run_test ./scripty -n -e ${srcdir}/listview_output_cursor.5 -- \
+ ./drive_listview -r 30 -h 10 -c -k ' ' < /dev/null
+
+on_error_fail_with "Listview Cursor Mode: didn't moved down on page jump?"
+
+# Page up move
+run_test ./scripty -n -e ${srcdir}/listview_output_cursor.6 -- \
+ ./drive_listview -r 30 -h 10 -c -k ' b' < /dev/null
+
+on_error_fail_with "Listview Cursor Mode: didn't moved up on page jump?"
diff --git a/test/test_log_accel.cc b/test/test_log_accel.cc
new file mode 100644
index 0000000..df5625f
--- /dev/null
+++ b/test/test_log_accel.cc
@@ -0,0 +1,70 @@
+/**
+ * Copyright (c) 2014, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+
+#include "config.h"
+#include "log_accel.hh"
+
+static int64_t SIMPLE_TEST_POINTS[] = {90,
+ 80,
+ 40,
+ 30,
+ 20,
+ 10,
+
+ -1};
+
+static log_accel::direction_t SIMPLE_TEST_DIRS[] = {
+ log_accel::A_STEADY,
+ log_accel::A_DECEL,
+ log_accel::A_STEADY,
+ log_accel::A_STEADY,
+ log_accel::A_STEADY,
+ log_accel::A_STEADY,
+ log_accel::A_STEADY,
+};
+
+int
+main(int argc, char* argv[])
+{
+ for (int point = 0; SIMPLE_TEST_POINTS[point] != -1; point++) {
+ log_accel la;
+
+ for (int lpc = point; SIMPLE_TEST_POINTS[lpc] != -1; lpc++) {
+ if (!la.add_point(SIMPLE_TEST_POINTS[lpc])) {
+ break;
+ }
+ }
+
+ assert(SIMPLE_TEST_DIRS[point] == la.get_direction());
+ }
+
+ return EXIT_SUCCESS;
+} \ No newline at end of file
diff --git a/test/test_logfile.sh b/test/test_logfile.sh
new file mode 100644
index 0000000..86e26c9
--- /dev/null
+++ b/test/test_logfile.sh
@@ -0,0 +1,707 @@
+#! /bin/bash
+
+echo ${top_srcdir}
+echo ${top_builddir}
+
+printf '#Date:\t20\x800-2-02\n0\n' | run_cap_test \
+ env TEST_COMMENT="short timestamp" ${lnav_test} -n
+
+printf '000\n000\n#Fields: 0\n0\n#Fields: 0\n0' | run_cap_test \
+ env TEST_COMMENT="invalid w3c log" ${lnav_test} -n
+
+cat > rollover_in.0 <<EOF
+2600/2 0 00:00:00 0:
+00:2 0 00:00:00 0:
+00:2 0 00:00:00 0:
+EOF
+touch -t 200711030923 rollover_in.0
+
+run_cap_test env TEST_COMMENT="invalid date rollover" ${lnav_test} -n rollover_in.0
+
+printf '#Fields: 0\tcs-bytes\n#Fields: 0\n\t0 #\n0' | run_cap_test \
+ env TEST_COMMENT="w3c with dupe #Fields" ${lnav_test} -n
+
+printf '#Fields: \xf9\t)\n0\n' | run_cap_test \
+ env TEST_COMMENT="garbage w3c fields #1" ${lnav_test} -n
+
+run_cap_test env TEST_COMMENT="w3c with bad header" ${lnav_test} -n <<EOF
+#Fields: 0 time
+ 00:00
+#Date:
+EOF
+
+printf '\x2b0\x1b[a' | run_cap_test \
+ env TEST_COMMENT="log line with an ansi escape" ${lnav_test} -n
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT * FROM logline' \
+ ${test_dir}/logfile_block.1
+
+run_test ${lnav_test} -d /tmp/lnav.err -n -w logfile_stdin.0.log \
+ -c ':shexec sleep 1 && touch -t 200711030923 logfile_stdin.0.log' <<EOF
+2013-06-06T19:13:20.123 Hi
+EOF
+
+check_output "piping to stdin is not working?" <<EOF
+2013-06-06T19:13:20.123 Hi
+EOF
+
+if test x"${TSHARK_CMD}" != x""; then
+ run_test env TZ=UTC ${lnav_test} -n ${test_dir}/dhcp.pcapng
+
+ check_output "pcap file is not recognized" <<EOF
+2004-12-05T19:16:24.317 0.0.0.0 → 255.255.255.255 DHCP 314 DHCP Discover - Transaction ID 0x3d1d
+2004-12-05T19:16:24.317 192.168.0.1 → 192.168.0.10 DHCP 342 DHCP Offer - Transaction ID 0x3d1d
+2004-12-05T19:16:24.387 0.0.0.0 → 255.255.255.255 DHCP 314 DHCP Request - Transaction ID 0x3d1e
+2004-12-05T19:16:24.387 192.168.0.1 → 192.168.0.10 DHCP 342 DHCP ACK - Transaction ID 0x3d1e
+EOF
+
+ run_test ${lnav_test} -n ${test_dir}/dhcp-trunc.pcapng
+
+ check_error_output "truncated pcap file is not recognized" <<EOF
+error: unable to open file: {test_dir}/dhcp-trunc.pcapng -- tshark: The file "{test_dir}/dhcp-trunc.pcapng" appears to have been cut short in the middle of a packet.
+EOF
+fi
+
+
+cp ${srcdir}/logfile_syslog.0 truncfile.0
+chmod u+w truncfile.0
+
+run_test ${lnav_test} -d /tmp/lnav.err -n \
+ -c ";update syslog_log set log_mark = 1 where log_line = 1" \
+ -c ":write-to truncfile.0" \
+ -c ":goto 1" \
+ truncfile.0
+
+check_output "truncated log file not detected" <<EOF
+Nov 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt
+EOF
+
+
+if locale -a | grep fr_FR; then
+ cp ${srcdir}/logfile_syslog_fr.0 logfile_syslog_fr_test.0
+ touch -t 200711030923 logfile_syslog_fr_test.0
+ run_test env LC_ALL=fr_FR.UTF-8 ${lnav_test} -n \
+ -c ";SELECT log_time FROM syslog_log" \
+ -c ":write-csv-to -" \
+ logfile_syslog_fr_test.0
+
+ check_output "french locale is not recognized" <<EOF
+log_time
+2007-08-19 11:08:37.000
+EOF
+fi
+
+if test x"${LIBARCHIVE_LIBS}" != x""; then
+ run_test env TMPDIR=tmp ${lnav_test} -n \
+ -c ':config /tuning/archive-manager/min-free-space -1' \
+ ${srcdir}/logfile_syslog.0
+
+ check_error_output "invalid min-free-space allowed?" <<EOF
+✘ error: “-1” is not a valid value for option “/tuning/archive-manager/min-free-space”
+ reason: value must be greater than or equal to 0
+ --> input:1
+ = help: Property Synopsis
+ /tuning/archive-manager/min-free-space <bytes>
+ Description
+ The minimum free space, in bytes, to maintain when unpacking archives
+EOF
+
+ rm -rf tmp/lnav-*
+ if test x"${XZ_CMD}" != x""; then
+ ${XZ_CMD} -z -c ${srcdir}/logfile_syslog.1 > logfile_syslog.1.xz
+
+ run_test env TMPDIR=tmp ${lnav_test} -n \
+ -c ':config /tuning/archive-manager/min-free-space 1125899906842624' \
+ -c ':config /tuning/archive-manager/cache-ttl 1d' \
+ ${srcdir}/logfile_syslog.0
+
+ run_test env TMPDIR=tmp ${lnav_test} -d /tmp/lnav.err -n \
+ logfile_syslog.1.xz
+
+ sed -e "s|lnav-user-[0-9]*-work|lnav-user-NNN-work|g" \
+ -e "s|arc-[0-9a-z]*-logfile|arc-NNN-logfile|g" \
+ -e "s|space on disk \(.*\) is|space on disk (NNN) is|g" \
+ -e "s|${builddir}||g" \
+ `test_err_filename` > test_logfile.big.out
+ mv test_logfile.big.out `test_err_filename`
+ check_error_output "decompression worked?" <<EOF
+✘ error: unable to open file: /logfile_syslog.1.xz
+ reason: available space on disk (NNN) is below the minimum-free threshold (1.0PB). Unable to unpack 'logfile_syslog.1.xz' to 'tmp/lnav-user-NNN-work/archives/arc-NNN-logfile_syslog.1.xz'
+EOF
+
+ run_test env TMPDIR=tmp ${lnav_test} -n \
+ -c ':config /tuning/archive-manager/min-free-space 33554432' \
+ ${srcdir}/logfile_syslog.0
+
+ run_test env TMPDIR=tmp ${lnav_test} -n \
+ logfile_syslog.1.xz
+
+ check_output "decompression not working" <<EOF
+Dec 3 09:23:38 veridian automount[7998]: lookup(file): lookup for foobar failed
+Dec 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt
+Dec 3 09:23:38 veridian automount[7999]: lookup(file): lookup for opt failed
+Jan 3 09:47:02 veridian sudo: timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
+EOF
+ fi
+
+ tar cfz ${builddir}/test-logs.tgz -C ${top_srcdir} test/logfile_access_log.0 test/logfile_access_log.1 test/logfile_empty.0 -C ${builddir}/.. src/lnav
+
+ dd if=test-logs.tgz of=test-logs-trunc.tgz bs=4096 count=20
+
+ mkdir -p tmp
+ run_test env TMPDIR=tmp ${lnav_test} \
+ -c ':config /tuning/archive-manager/cache-ttl 1d' \
+ -n test-logs.tgz
+
+ check_output "archive not unpacked" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
+EOF
+
+ if ! test -f tmp/*/archives/*-test-logs.tgz/test/logfile_access_log.0; then
+ echo "archived file not unpacked"
+ exit 1
+ fi
+
+ if test -w tmp/*/archives/*-test-logs.tgz/test/logfile_access_log.0; then
+ echo "archived file is writable"
+ exit 1
+ fi
+
+ env TMPDIR=tmp ${lnav_test} -d /tmp/lnav.err \
+ -c ':config /tuning/archive-manager/cache-ttl 0d' \
+ -n -q ${srcdir}/logfile_syslog.0
+
+ if test -f tmp/lnav*/archives/*-test-logs.tgz/test/logfile_access_log.0; then
+ echo "archive cache not deleted?"
+ exit 1
+ fi
+
+ run_test env TMPDIR=tmp ${lnav_test} -n\
+ -c ';SELECT view_name, basename(filepath), visible FROM lnav_view_files' \
+ test-logs.tgz
+
+ check_output "archive files not loaded correctly" <<EOF
+view_name basename(filepath) visible
+log logfile_access_log.0 1
+log logfile_access_log.1 1
+EOF
+
+ run_test env TMPDIR=tmp ${lnav_test} -n \
+ test-logs-trunc.tgz
+
+ sed -e "s|${builddir}||g" `test_err_filename` | head -2 \
+ > test_logfile.trunc.out
+ mv test_logfile.trunc.out `test_err_filename`
+ check_error_output "truncated tgz not reported correctly" <<EOF
+✘ error: unable to open file: /test-logs-trunc.tgz
+ reason: failed to extract 'src/lnav' from archive '/test-logs-trunc.tgz' -- truncated gzip input
+EOF
+
+ mkdir -p rotmp
+ chmod ugo-w rotmp
+ run_test env TMPDIR=rotmp ${lnav_test} -n test-logs.tgz
+
+ sed -e "s|lnav-user-[0-9]*-work|lnav-user-NNN-work|g" \
+ -e 's|log\.0 -- .*|log\.0 -- ...|g' \
+ -e "s|arc-[0-9a-z]*-test|arc-NNN-test|g" \
+ -e "s|${builddir}||g" \
+ `test_err_filename` | head -2 \
+ > test_logfile.rotmp.out
+ cp test_logfile.rotmp.out `test_err_filename`
+ check_error_output "archive not unpacked" <<EOF
+✘ error: unable to open file: /test-logs.tgz
+ reason: unable to create directory: rotmp/lnav-user-NNN-work/archives -- Permission denied
+EOF
+fi
+
+touch unreadable.log
+chmod ugo-r unreadable.log
+
+run_test ${lnav_test} -n unreadable.log
+
+sed -e "s|/.*/unreadable.log|unreadable.log|g" `test_err_filename` | head -3 \
+ > test_logfile.unreadable.out
+
+mv test_logfile.unreadable.out `test_err_filename`
+check_error_output "able to read an unreadable log file?" <<EOF
+✘ error: file exists, but is not readable: unreadable.log
+ reason: Permission denied
+EOF
+
+run_test ${lnav_test} -n 'unreadable.*'
+
+check_output "unreadable file was not skipped" <<EOF
+EOF
+
+run_test ./drive_logfile -f syslog_log ${srcdir}/logfile_syslog.0
+
+on_error_fail_with "Didn't infer syslog log format?"
+
+run_test ./drive_logfile -f tcsh_history ${srcdir}/logfile_tcsh_history.0
+
+on_error_fail_with "Didn't infer tcsh-history log format?"
+
+run_test ./drive_logfile -f access_log ${srcdir}/logfile_access_log.0
+
+on_error_fail_with "Didn't infer access_log log format?"
+
+run_test ./drive_logfile -f strace_log ${srcdir}/logfile_strace_log.0
+
+on_error_fail_with "Didn't infer strace_log log format?"
+
+run_test ./drive_logfile -f zblued_log ${srcdir}/logfile_blued.0
+
+on_error_fail_with "Didn't infer blued_log that collides with syslog?"
+
+run_test ./drive_logfile -f bro_http_log ${srcdir}/logfile_bro_http.log.0
+
+on_error_fail_with "Didn't infer bro_http_log log format?"
+
+run_test ./drive_logfile -f bro_conn_log ${srcdir}/logfile_bro_conn.log.0
+
+on_error_fail_with "Didn't infer bro_conn_log log format?"
+
+run_test ./drive_logfile -f w3c_log ${srcdir}/logfile_w3c.0
+
+on_error_fail_with "Didn't infer w3c_log log format?"
+
+
+run_test ./drive_logfile ${srcdir}/logfile_empty.0
+
+on_error_fail_with "Didn't handle empty log?"
+
+
+run_test ./drive_logfile -t -f w3c_log ${srcdir}/logfile_w3c.2
+
+check_output "w3c timestamp interpreted incorrectly?" <<EOF
+Oct 09 16:44:49 2000 -- 000
+Oct 09 16:44:49 2000 -- 000
+Oct 09 16:48:05 2000 -- 000
+Oct 09 16:48:17 2000 -- 000
+Oct 09 16:48:24 2000 -- 000
+Oct 09 16:48:35 2000 -- 000
+Oct 09 16:48:41 2000 -- 000
+Oct 09 16:48:41 2000 -- 000
+Oct 09 16:48:41 2000 -- 000
+Oct 09 16:48:41 2000 -- 000
+Oct 09 16:48:44 2000 -- 000
+Oct 10 16:44:49 2000 -- 000
+Oct 10 16:44:49 2000 -- 000
+Oct 10 16:48:05 2000 -- 000
+EOF
+
+run_test ./drive_logfile -t -f w3c_log ${srcdir}/logfile_w3c.4
+
+check_output "quoted w3c timestamp interpreted incorrectly?" <<EOF
+Jun 28 07:26:35 2017 -- 000
+Jun 26 18:21:17 2017 -- 000
+EOF
+
+cp ${srcdir}/logfile_syslog.0 logfile_syslog_test.0
+touch -t 200711030923 logfile_syslog_test.0
+run_test ./drive_logfile -t -f syslog_log logfile_syslog_test.0
+
+check_output "Syslog timestamp interpreted incorrectly?" <<EOF
+Nov 03 09:23:38 2007 -- 000
+Nov 03 09:23:38 2007 -- 000
+Nov 03 09:23:38 2007 -- 000
+Nov 03 09:47:02 2007 -- 000
+EOF
+
+env TZ=UTC touch -t 200711030923 ${srcdir}/logfile_syslog.1
+run_test ./drive_logfile -t -f syslog_log ${srcdir}/logfile_syslog.1
+
+check_output "Syslog timestamp interpreted incorrectly for year end?" <<EOF
+Dec 03 09:23:38 2006 -- 000
+Dec 03 09:23:38 2006 -- 000
+Dec 03 09:23:38 2006 -- 000
+Jan 03 09:47:02 2007 -- 000
+EOF
+
+touch -t 200711030000 ${srcdir}/logfile_rollover.0
+run_test ./drive_logfile -t -f generic_log ${srcdir}/logfile_rollover.0
+
+check_output "Generic timestamp interpreted incorrectly for day rollover?" <<EOF
+Nov 02 00:00:00 2007 -- 000
+Nov 02 01:00:00 2007 -- 000
+Nov 02 02:00:00 2007 -- 000
+Nov 02 03:00:00 2007 -- 000
+Nov 03 00:00:00 2007 -- 000
+Nov 03 00:01:00 2007 -- 000
+EOF
+
+gzip -c ${srcdir}/logfile_syslog.1 > logfile_syslog.1.gz
+
+run_test ./drive_logfile -t -f syslog_log logfile_syslog.1.gz
+
+check_output "Syslog timestamp incorrect for gzipped file?" <<EOF
+Dec 03 09:23:38 2006 -- 000
+Dec 03 09:23:38 2006 -- 000
+Dec 03 09:23:38 2006 -- 000
+Jan 03 09:47:02 2007 -- 000
+EOF
+
+if [ "$BZIP2_SUPPORT" -eq 1 ] && [ x"$BZIP2_CMD" != x"" ] ; then
+ $BZIP2_CMD -z -c "${srcdir}/logfile_syslog.1" > logfile_syslog.1.bz2
+
+ touch -t 200711030923 logfile_syslog.1.bz2
+ run_test ./drive_logfile -t -f syslog_log logfile_syslog.1.bz2
+
+ check_output "bzip2 file not loaded?" <<EOF
+Dec 03 09:23:38 2006 -- 000
+Dec 03 09:23:38 2006 -- 000
+Dec 03 09:23:38 2006 -- 000
+Jan 03 09:47:02 2007 -- 000
+EOF
+fi
+
+touch -t 201404061109 ${srcdir}/logfile_tcf.1
+run_test ./drive_logfile -t -f tcf_log ${srcdir}/logfile_tcf.1
+
+check_output "TCF timestamp interpreted incorrectly for hour wrap?" <<EOF
+Apr 06 09:59:47 2014 -- 191
+Apr 06 10:30:11 2014 -- 474
+Apr 06 11:01:11 2014 -- 475
+EOF
+
+run_test ${lnav_test} -n ${srcdir}/logfile_tcf.1
+
+check_output "timestamps with no dates are not rewritten?" <<EOF
+TCF 2014-04-06 09:59:47.191234: Server-Properties: {"Name":"TCF Protocol Logger","OSName":"Linux 3.2.0-60-generic","UserName":"xavier","AgentID":"1fde3dd1-d4be-4f79-8090-6f8d212f03bf","TransportName":"TCP","Proxy":"","ValueAdd":"1","Port":"1534"}
+TCF 2014-04-06 10:30:11.474442: 0: ---> C 2 RunControl getChildren null <eom>
+TCF 2014-04-06 11:01:11.475557: 0: <--- R 2 ["P1"] <eom>
+EOF
+
+
+# The TCSH format converts to local time, so we need to specify a TZ
+export TZ="UTC"
+run_test ./drive_logfile -t -f tcsh_history ${srcdir}/logfile_tcsh_history.0
+
+check_output "TCSH timestamp interpreted incorrectly?" <<EOF
+Nov 02 17:59:26 2006 -- 000
+Nov 02 17:59:26 2006 -- 000
+Nov 02 17:59:45 2006 -- 000
+Nov 02 17:59:45 2006 -- 000
+EOF
+
+run_test ./drive_logfile -t -f access_log ${srcdir}/logfile_access_log.0
+
+check_output "access_log timestamp interpreted incorrectly?" <<EOF
+Jul 20 22:59:26 2009 -- 000
+Jul 20 22:59:29 2009 -- 000
+Jul 20 22:59:29 2009 -- 000
+EOF
+
+run_test ./drive_logfile -t -f generic_log ${srcdir}/logfile_tai64n.0
+
+check_output "tai64n timestamps interpreted incorrectly?" <<EOF
+Sep 22 03:31:05 2005 -- 997
+Sep 22 03:31:05 2005 -- 997
+Sep 22 03:31:06 2005 -- 210
+Sep 22 03:31:06 2005 -- 210
+Sep 22 03:31:07 2005 -- 714
+Sep 22 03:31:07 2005 -- 714
+Sep 22 03:31:07 2005 -- 715
+Sep 22 03:31:07 2005 -- 715
+Sep 22 03:31:07 2005 -- 954
+Sep 22 03:31:07 2005 -- 954
+EOF
+
+touch -t 200711030923 ${srcdir}/logfile_strace_log.0
+run_test ./drive_logfile -t -f strace_log ${srcdir}/logfile_strace_log.0
+
+check_output "strace_log timestamp interpreted incorrectly?" <<EOF
+Nov 03 08:09:33 2007 -- 814
+Nov 03 08:09:33 2007 -- 815
+Nov 03 08:09:33 2007 -- 815
+Nov 03 08:09:33 2007 -- 815
+Nov 03 08:09:33 2007 -- 816
+Nov 03 08:09:33 2007 -- 816
+Nov 03 08:09:33 2007 -- 816
+Nov 03 08:09:33 2007 -- 816
+Nov 03 08:09:33 2007 -- 816
+EOF
+
+
+run_test ./drive_logfile -t -f epoch_log ${srcdir}/logfile_epoch.0
+
+check_output "epoch_log timestamp interpreted incorrectly?" <<EOF
+Apr 10 02:58:07 2015 -- 123
+Apr 10 02:58:07 2015 -- 456
+EOF
+
+
+run_test ./drive_logfile -t -f epoch_log ${srcdir}/logfile_epoch.1
+
+check_error_output "epoch" <<EOF
+EOF
+
+check_output "epoch_log timestamp interpreted incorrectly?" <<EOF
+Apr 09 19:58:07 2015 -- 123
+Apr 09 19:58:07 2015 -- 456
+EOF
+
+
+touch -t 201509130923 ${srcdir}/logfile_syslog_with_mixed_times.0
+run_test ./drive_logfile -t -f syslog_log ${srcdir}/logfile_syslog_with_mixed_times.0
+
+check_output "syslog_log with mixed times interpreted incorrectly?" <<EOF
+Sep 13 00:58:45 2015 -- 000
+Sep 13 00:59:30 2015 -- 000
+Sep 13 01:23:54 2015 -- 000
+Sep 13 03:12:04 2015 -- 000
+Sep 13 03:12:04 2015 -- 000
+Sep 13 03:12:04 2015 -- 000
+Sep 13 03:12:04 2015 -- 000
+Sep 13 03:12:58 2015 -- 000
+Sep 13 03:46:03 2015 -- 000
+Sep 13 03:46:03 2015 -- 000
+Sep 13 03:46:03 2015 -- 000
+Sep 13 03:46:03 2015 -- 000
+Sep 13 03:46:03 2015 -- 000
+EOF
+
+
+##
+
+run_test ./drive_logfile -v -f syslog_log ${srcdir}/logfile_syslog.0
+
+check_output "Syslog level interpreted incorrectly?" <<EOF
+error 0x0
+info 0x0
+error 0x0
+info 0x0
+EOF
+
+run_test ./drive_logfile -v -f tcsh_history ${srcdir}/logfile_tcsh_history.0
+
+check_output "TCSH level interpreted incorrectly?" <<EOF
+info 0x0
+info 0x80
+info 0x0
+info 0x80
+EOF
+
+run_test ./drive_logfile -v -f access_log ${srcdir}/logfile_access_log.0
+
+check_output "access_log level interpreted incorrectly?" <<EOF
+info 0x0
+error 0x0
+info 0x0
+EOF
+
+run_test ./drive_logfile -v -f strace_log ${srcdir}/logfile_strace_log.0
+
+check_output "strace_log level interpreted incorrectly?" <<EOF
+info 0x0
+info 0x0
+info 0x0
+error 0x0
+info 0x0
+error 0x0
+info 0x0
+info 0x0
+info 0x0
+EOF
+
+run_test ./drive_logfile -t -f generic_log ${srcdir}/logfile_generic.0
+
+check_output "generic_log timestamp interpreted incorrectly?" <<EOF
+Jul 02 10:22:40 2012 -- 672
+Oct 08 16:56:38 2014 -- 344
+EOF
+
+run_test ./drive_logfile -t -f generic_log ${srcdir}/logfile_generic.3
+
+check_output "generic_log timestamp interpreted incorrectly?" <<EOF
+Jul 02 10:22:40 2012 -- 672
+Oct 08 16:56:38 2014 -- 344
+EOF
+
+run_test ./drive_logfile -v -f generic_log ${srcdir}/logfile_generic.0
+
+check_output "generic_log level interpreted incorrectly?" <<EOF
+debug 0x0
+warning 0x0
+EOF
+
+run_test ./drive_logfile -v -f generic_log ${srcdir}/logfile_generic.1
+
+check_output "generic_log (1) level interpreted incorrectly?" <<EOF
+info 0x0
+error 0x0
+EOF
+
+run_test ./drive_logfile -v -f generic_log ${srcdir}/logfile_generic.2
+
+check_output "generic_log (2) level interpreted incorrectly?" <<EOF
+error 0x0
+error 0x0
+EOF
+
+touch -t 200711030923 ${srcdir}/logfile_glog.0
+run_test ./drive_logfile -t -f glog_log ${srcdir}/logfile_glog.0
+
+check_output "glog_log timestamp interpreted incorrectly?" <<EOF
+May 17 15:04:22 2007 -- 619
+May 17 15:04:22 2007 -- 619
+May 17 15:04:22 2007 -- 619
+May 17 15:04:22 2007 -- 619
+May 17 15:04:22 2007 -- 619
+May 17 15:04:22 2007 -- 619
+May 17 15:04:22 2007 -- 619
+EOF
+
+run_test ./drive_logfile -v -f glog_log ${srcdir}/logfile_glog.0
+
+check_output "glog_log level interpreted incorrectly?" <<EOF
+error 0x0
+info 0x0
+info 0x0
+warning 0x0
+info 0x0
+info 0x0
+error 0x0
+EOF
+
+run_test ./drive_logfile -t -f logfmt_log ${srcdir}/logfile_logfmt.0
+
+check_output "logfmt_log time interpreted incorrectly?" <<EOF
+Sep 15 21:17:10 2021 -- 220
+Sep 15 21:17:11 2021 -- 674
+Sep 15 21:17:11 2021 -- 678
+Sep 15 21:17:11 2021 -- 679
+Sep 15 21:18:20 2021 -- 335
+EOF
+
+run_test ./drive_logfile -v -f logfmt_log ${srcdir}/logfile_logfmt.0
+
+check_output "logfmt_log level interpreted incorrectly?" <<EOF
+error 0x0
+warning 0x0
+info 0x0
+info 0x0
+error 0x0
+EOF
+
+run_test ${lnav_test} -d /tmp/lnav.err -nt -w logfile_stdin.log <<EOF
+Hi
+EOF
+
+check_output "piping to stdin is not working?" <<EOF
+2013-06-06T19:13:20.123 Hi
+2013-06-06T19:13:20.123 ---- END-OF-STDIN ----
+EOF
+
+run_test ${lnav_test} -C ${test_dir}/logfile_bad_access_log.0
+
+sed -ibak -e "s|/.*/logfile_bad_access_log.0|logfile_bad_access_log.0|g" `test_err_filename`
+
+check_error_output "bad access_log line not found?" <<EOF
+error:logfile_bad_access_log.0:1:line did not match format /access_log/regex/std
+error:logfile_bad_access_log.0:1: line -- 192.168.202.254 [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+error:logfile_bad_access_log.0:1:partial match -- 192.168.202.254
+EOF
+
+run_test ${lnav_test} -n -I ${test_dir} ${srcdir}/logfile_w3c.2
+
+check_output "metadata lines not ignored?" <<EOF
+16:44:49 1.1.1.1 [2]USER anonymous 331
+16:44:49 1.1.1.1 [2]PASS - 230
+16:48:05 1.1.1.1 [2]QUIT - 226
+16:48:17 1.1.1.1 [3]USER anonymous 331
+16:48:24 1.1.1.1 [3]PASS user@domain.com 230
+16:48:35 1.1.1.1 [3]sent /user/test.c 226
+16:48:41 1.1.1.1 [3]created readme.txt 226
+16:48:41 1.1.1.1 [3]created fileid.diz 226
+16:48:41 1.1.1.1 [3]created names.dll 226
+16:48:41 1.1.1.1 [3]created TEST.EXE 226
+16:48:44 1.1.1.1 [3]QUIT - 226
+16:44:49 1.1.1.1 [2]USER anonymous 331
+16:44:49 1.1.1.1 [2]PASS - 230
+16:48:05 1.1.1.1 [2]QUIT - 226
+EOF
+
+run_test ${lnav_test} -n -I ${test_dir} ${srcdir}/logfile_w3c.6
+
+check_output "unicode in w3c not working?" <<EOF
+2015-01-13 00:32:17 100.79.192.81 GET /robots.txt - 80 - 157.55.39.146 ÄÖÜäöü\ßßßMözillä/5.0+(compatible;+bingbot/2.0;++http://www.bing.com/bingbot.htm) - 404 0 2 1405 242 283
+EOF
+
+run_test ${lnav_test} -n -I ${test_dir} ${srcdir}/logfile_epoch.0
+
+check_output "rewriting machine-oriented timestamp didn't work?" <<EOF
+2015-04-10 02:58:07.123000 Hello, World!
+2015-04-10 02:58:07.456000 Goodbye, World!
+EOF
+
+run_test ${lnav_test} -n -I ${test_dir} ${srcdir}/logfile_crlf.0
+
+check_output "CR-LF line-endings not handled?" <<EOF
+2012-07-02 10:22:40,672:DEBUG:foo bar baz
+2014-10-08 16:56:38,344:WARN:foo bar baz
+EOF
+
+run_test ${lnav_test} -n -I ${test_dir} \
+ -c ';SELECT count(*) FROM haproxy_log' \
+ ${srcdir}/logfile_haproxy.0
+
+check_output "multi-pattern logs don't work?" <<EOF
+count(*)
+ 17
+EOF
+
+run_test ${lnav_test} -n \
+ ${srcdir}/logfile_syslog_with_header.0
+
+check_output "multi-pattern logs don't work?" <<EOF
+Header1: abc
+Header2: def
+Nov 3 09:23:38 veridian automount[7998]: lookup(file): lookup for foobar failed
+Nov 3 09:23:38 veridian automount[16442]: attempting to mount entry /auto/opt
+Nov 3 09:23:38 veridian automount[7999]: lookup(file): lookup for opt failed
+Nov 3 09:47:02 veridian sudo: timstack : TTY=pts/6 ; PWD=/auto/wstimstack/rpms/lbuild/test ; USER=root ; COMMAND=/usr/bin/tail /var/log/messages
+EOF
+
+run_test ${lnav_test} -n \
+ ${srcdir}/logfile_generic_with_header.0
+
+check_output "multi-pattern logs don't work?" <<EOF
+Header1: abc
+Header2: def
+2012-07-02 10:22:40,672:DEBUG:foo bar baz
+2014-10-08 16:56:38,344:WARN:foo bar baz
+EOF
+
+# XXX get this working...
+# run_test ${lnav_test} -n -I ${test_dir} <(cat ${srcdir}/logfile_access_log.0)
+#
+# check_output "opening a FIFO didn't work?" <<EOF
+# 192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+# 192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+# 192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+# EOF
+
+export YES_COLOR=1
+
+touch -t 202211030923 ${test_dir}/logfile_ansi.1
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT log_time, log_body FROM syslog_log' \
+ ${test_dir}/logfile_ansi.1
+
+run_cap_test ${lnav_test} -n \
+ -c ':switch-to-view pretty' \
+ ${test_dir}/logfile_ansi.1
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT basename(filepath),descriptor,mimetype,content FROM lnav_file_metadata' \
+ logfile_syslog.1.gz
+
+run_cap_test ${lnav_test} -n \
+ -c ':filter-in Air Mob' \
+ ${test_dir}/logfile_ansi.1
diff --git a/test/test_md2attr_line.cc b/test/test_md2attr_line.cc
new file mode 100644
index 0000000..47e2454
--- /dev/null
+++ b/test/test_md2attr_line.cc
@@ -0,0 +1,34 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "doctest/doctest.h"
+#include "md2attr_line.hh"
+
+TEST_CASE("basics") {}
diff --git a/test/test_meta.sh b/test/test_meta.sh
new file mode 100644
index 0000000..caab4f9
--- /dev/null
+++ b/test/test_meta.sh
@@ -0,0 +1,110 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+export HOME="./meta-sessions"
+export XDG_CONFIG_HOME="./meta-sessions/.config"
+rm -rf "./meta-sessions"
+mkdir -p $HOME/.config
+
+# add comment/tag
+run_cap_test ${lnav_test} -n -dln.dbg \
+ -c ":comment Hello, World!" \
+ -c ":tag foo" \
+ -c ":save-session" \
+ -c ":write-screen-to -" \
+ ${test_dir}/logfile_access_log.0
+
+ls -lha meta-sessions
+find meta-sessions
+# cat ln.dbg
+if test ! -d meta-sessions/.config/lnav; then
+ echo "error: configuration not stored in .config/lnav?"
+ exit 1
+fi
+
+if test -d meta-sessions/.lnav; then
+ echo "error: configuration stored in .lnav?"
+ exit 1
+fi
+
+# tag was saved and :write-to displays the comments/tags
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ";UPDATE access_log SET log_mark = 1" \
+ -c ":write-to -" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ":untag #foo" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ":clear-comment" \
+ ${test_dir}/logfile_access_log.0
+
+# search for a tag
+run_cap_test ${lnav_test} -n \
+ -c ":goto 2" \
+ -c "/foo" \
+ -c ":tag #foo" \
+ -c ":goto 0" \
+ -c ":next-mark search" \
+ ${test_dir}/logfile_access_log.0
+
+# query meta columns
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ";SELECT log_line, log_comment, log_tags FROM access_log" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE access_log SET log_tags = json_array('#foo', '#foo') WHERE log_line = 1" \
+ -c ":save-session" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE access_log SET log_comment = 'Goodbye, World!' WHERE log_line = 1" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE access_log SET log_tags = 1 WHERE log_line = 1" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE access_log SET log_tags = json_array('foo') WHERE log_line = 1" \
+ -c ":save-session" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ";SELECT log_tags FROM access_log WHERE log_line = 1" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":tag foo" \
+ -c ":delete-tags #foo" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":tag foo" \
+ -c ";UPDATE access_log SET log_tags = null" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":comment foo" \
+ -c ";UPDATE access_log SET log_comment = null" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -d /tmp/lnav.err -n \
+ -I ${test_dir} \
+ ${test_dir}/logfile_xml_msg.0
+
+run_cap_test ${lnav_test} -n -f- \
+ ${test_dir}/logfile_access_log.0 <<'EOF'
+:comment Hello, **World**!
+
+This is `markdown` now!
+EOF
diff --git a/test/test_mvwattrline.sh b/test/test_mvwattrline.sh
new file mode 100644
index 0000000..c06c3ea
--- /dev/null
+++ b/test/test_mvwattrline.sh
@@ -0,0 +1,6 @@
+#! /bin/bash
+
+run_test ./scripty -n -e ${srcdir}/mvwattrline_output.0 -- \
+ ./drive_mvwattrline < /dev/null
+
+on_error_fail_with "mvwattrline does not work"
diff --git a/test/test_ncurses_unicode.cc b/test/test_ncurses_unicode.cc
new file mode 100644
index 0000000..a43d91c
--- /dev/null
+++ b/test/test_ncurses_unicode.cc
@@ -0,0 +1,40 @@
+
+#include <stdlib.h>
+
+#include "config.h"
+#define _XOPEN_SOURCE_EXTENDED 1
+#include <locale.h>
+
+#if defined HAVE_NCURSESW_CURSES_H
+# include <ncursesw/curses.h>
+#elif defined HAVE_NCURSESW_H
+# include <ncursesw.h>
+#elif defined HAVE_NCURSES_CURSES_H
+# include <ncurses/curses.h>
+#elif defined HAVE_NCURSES_H
+# include <ncurses.h>
+#elif defined HAVE_CURSES_H
+# include <curses.h>
+#else
+# error "SysV or X/Open-compatible Curses header file required"
+#endif
+
+int
+main(int argc, char* argv[])
+{
+ setenv("LANG", "en_US.UTF-8", 1);
+ setlocale(LC_ALL, "");
+
+ WINDOW* stdscr = initscr();
+ cbreak();
+ char buf[1024];
+ FILE* file = fopen(argv[1], "r");
+ int row = 0;
+ while (!feof(file)) {
+ if (fgets(buf, sizeof(buf), file) != nullptr) {
+ mvwaddstr(stdscr, row++, 0, buf);
+ }
+ }
+ getch();
+ endwin();
+}
diff --git a/test/test_pretty_print.sh b/test/test_pretty_print.sh
new file mode 100644
index 0000000..78f0c96
--- /dev/null
+++ b/test/test_pretty_print.sh
@@ -0,0 +1,45 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view pretty" \
+ ${test_dir}/logfile_cxx.0
+
+# check for ipv4 strings
+run_cap_test ${lnav_test} -n -c ":switch-to-view pretty" <<EOF
+2015-04-18T13:16:30.003 8.8.8.8 <foo>8.8.8.8</foo>9 8.8.8.8<1054 198.51.100.1546 544.9.8.7 98.542.241.99 19143.2.5.6
+EOF
+
+cat > test_pretty_in.1 <<EOF
+2015-04-18T13:16:30.003 {"wrapper": {"msg": r"Hello,\nWorld!\n"}}
+EOF
+
+# pretty print can interpret quoted strings correctly
+run_cap_test ${lnav_test} -n -c ":switch-to-view pretty" -d /tmp/lnav.err test_pretty_in.1
+
+cat > test_pretty_in.2 <<EOF
+{"wrapper": [{"message":"\nselect Id from Account where id = \$sfid\n ^\nERROR at Row:1:Column:34\nline 1:34 no viable alternative at character '$'"}]}
+EOF
+
+# pretty print includes leading white space
+run_cap_test ${lnav_test} -n -c ":switch-to-view pretty" test_pretty_in.2
+
+cat > test_pretty_in.3 <<EOF
+Hello\\nWorld\\n
+EOF
+
+run_cap_test ${lnav_test} -d /tmp/lnav.err -n -c ":switch-to-view pretty" test_pretty_in.3
+
+run_cap_test ${lnav_test} -d /tmp/lnav.err -n \
+ -I ${test_dir} \
+ -c ":switch-to-view pretty" \
+ ${test_dir}/logfile_xml_msg.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view pretty" \
+ ${test_dir}/logfile_ansi.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":switch-to-view pretty" \
+ ${test_dir}/textfile_ansi.0
diff --git a/test/test_regex101.sh b/test/test_regex101.sh
new file mode 100644
index 0000000..861bf10
--- /dev/null
+++ b/test/test_regex101.sh
@@ -0,0 +1,77 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+rm -rf regex101-home
+mkdir -p regex101-home
+export HOME=regex101-home
+
+run_cap_test ${lnav_test} -m format syslog_log regex std
+
+run_cap_test ${lnav_test} -m format syslog_log regex std regex101
+
+run_cap_test ${lnav_test} -m format syslog_log regex std regex101 pull
+
+run_cap_test ${lnav_test} -m format syslog_log regex std regex101 delete
+
+run_cap_test env TEST_COMMENT="before import" ${lnav_test} -m regex101 list
+
+run_cap_test ${lnav_test} -m regex101 import
+
+run_cap_test ${lnav_test} -m regex101 import abc def-jkl
+
+run_cap_test ${lnav_test} -m regex101 import https://regex101.com/r/badregex123/1 unit_test_log
+
+# bad regex flavor
+run_cap_test ${lnav_test} -m regex101 import https://regex101.com/r/cvCJNP/1 unit_test_log
+
+run_cap_test ${lnav_test} -m regex101 import https://regex101.com/r/zpEnjV/2 unit_test_log
+
+# a second import should fail since the format file exists now
+run_cap_test ${lnav_test} -m regex101 import https://regex101.com/r/zpEnjV/1 unit_test_log
+
+run_cap_test cat regex101-home/.lnav/formats/installed/unit_test_log.json
+
+run_cap_test env TEST_COMMENT="after import" ${lnav_test} -m regex101 list
+
+run_cap_test ${lnav_test} -m format non-existent regex std regex101 pull
+
+run_cap_test ${lnav_test} -m format bro regex std regex101 pull
+
+run_cap_test ${lnav_test} -m format unit_test_log regex non-existent regex101 pull
+
+run_cap_test ${lnav_test} -m format unit_test_log regex s regex101 pull
+
+run_cap_test ${lnav_test} -m format unit_test_log regex std regex101
+
+run_cap_test ${lnav_test} -m format unit_test_log regex std regex101 pull
+
+cat > regex101-home/.lnav/formats/installed/unit_test_log.regex101-zpEnjV.json <<EOF
+{
+ "unit_test_log": {
+ "regex": {
+ "std": {
+ "pattern": ""
+ }
+ }
+ }
+}
+EOF
+
+run_cap_test env TEST_COMMENT="pull after change" \
+ ${lnav_test} -m format unit_test_log regex std regex101 pull
+
+run_cap_test ${lnav_test} -m format unit_test_log sources
+
+run_cap_test cat regex101-home/.lnav/formats/installed/unit_test_log.regex101-zpEnjV.json
+
+run_cap_test ${lnav_test} -m regex101 import https://regex101.com/r/hGiqBL/2 unit_test_log alt
+
+run_cap_test cat regex101-home/.lnav/formats/installed/unit_test_log.regex101-hGiqBL.json
+
+run_cap_test ${lnav_test} -m format unit_test_log regex std regex101 delete
+
+rm regex101-home/.lnav/formats/installed/unit_test_log.regex101-zpEnjV.json
+
+run_cap_test env TEST_COMMENT="delete after patch removed" \
+ ${lnav_test} -m format unit_test_log regex std regex101 delete
diff --git a/test/test_reltime.cc b/test/test_reltime.cc
new file mode 100644
index 0000000..a526c1d
--- /dev/null
+++ b/test/test_reltime.cc
@@ -0,0 +1,427 @@
+/**
+ * Copyright (c) 2015, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <sys/time.h>
+
+#include "config.h"
+#include "fmt/format.h"
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "doctest/doctest.h"
+#include "relative_time.hh"
+
+using namespace std;
+
+static struct {
+ const char* reltime{nullptr};
+ const char* expected{nullptr};
+ const char* expected_negate{nullptr};
+} TEST_DATA[] = {
+ // { "10 minutes after the next hour", "next 0:10" },
+ {"0s", "0s", "0s"},
+ {"next day", "next day 0:00", "last day 0:00"},
+ {"next month", "next month day 0 0:00", "last month day 0 0:00"},
+ {"next year",
+ "next year month 0 day 0 0:00",
+ "last year month 0 day 0 0:00"},
+ {"previous hour", "last 0:00", "next 0:00"},
+ {"next 10 minutes after the hour", "next 0:10", "last 0:10"},
+ {"1h50m", "1h50m", "-1h-50m"},
+ {"next hour", "next 0:00", "last 0:00"},
+ {"a minute ago", "0:-1", "0:-1"},
+ {"1m ago", "0:-1", "0:-1"},
+ {"a min ago", "0:-1", "0:-1"},
+ {"a m ago", "0:-1", "0:-1"},
+ {"+1 minute ago", "0:-1", "0:-1"},
+ {"-1 minute ago", "0:-1", "0:-1"},
+ {"-1 minute", "-1m", "1m"},
+ {"10 minutes after the hour", "0:10", "0:10"},
+ {"1:40", "1:40", "1:40"},
+ {"01:30", "1:30", "1:30"},
+ {"1pm", "13:00", "13:00"},
+ {"12pm", "12:00", "12:00"},
+ {"00:27:18.567", "0:27:18.567", "0:27:18.567"},
+
+ {},
+};
+
+static struct {
+ const char* reltime;
+ const char* expected_error;
+} BAD_TEST_DATA[] = {
+ {"10am am", "Time has already been set"},
+ {"yesterday today", "Current time reference has already been used"},
+ {"10am 10am", "Time has already been set"},
+ {"ago", "Expecting a time unit"},
+ {"minute", "Expecting a number before time unit"},
+ {"1 2", "No time unit given for the previous number"},
+ {"blah", "Unrecognized input"},
+ {"before", "'before' requires a point in time (e.g. before 10am)"},
+ {"after", "'after' requires a point in time (e.g. after 10am)"},
+ {"before after", "Before/after ranges are not supported yet"},
+
+ {nullptr, nullptr},
+};
+
+TEST_CASE("reltime")
+{
+ time_t base_time = 1317913200;
+ struct exttm base_tm;
+ base_tm.et_tm = *gmtime(&base_time);
+ struct timeval tv;
+ struct exttm tm, tm2;
+ time_t new_time;
+
+ {
+ auto rt_res = relative_time::from_str(
+ string_fragment::from_const("before 2014"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+
+ time_t t_in = 1438948860;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ auto win_opt = rt.window_start(tm);
+ CHECK(!win_opt.has_value());
+ }
+
+ {
+ auto rt_res = relative_time::from_str(
+ string_fragment::from_const("after 2014"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+
+ time_t t_in = 1438948860;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ auto win_opt = rt.window_start(tm);
+ CHECK(win_opt.has_value());
+ }
+
+ {
+ auto rt_res
+ = relative_time::from_str(string_fragment::from_const("after fri"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+
+ time_t t_in = 1438948860;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ auto win_opt = rt.window_start(tm);
+ CHECK(!win_opt.has_value());
+ }
+
+ {
+ auto rt_res = relative_time::from_str(
+ string_fragment::from_const("before fri"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+
+ time_t t_in = 1438948860;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ auto win_opt = rt.window_start(tm);
+ CHECK(!win_opt.has_value());
+ }
+
+ {
+ auto rt_res = relative_time::from_str(
+ string_fragment::from_const("before 12pm"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+
+ time_t t_in = 1438948860;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ auto win_opt = rt.window_start(tm);
+ CHECK(!win_opt.has_value());
+ }
+
+ {
+ auto rt_res = relative_time::from_str(
+ string_fragment::from_const("sun after 1pm"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+
+ time_t t_in = 1615727900;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ auto win_opt = rt.window_start(tm);
+ auto win_tm = *win_opt;
+ CHECK(win_tm.et_tm.tm_year == 121);
+ CHECK(win_tm.et_tm.tm_mon == 2);
+ CHECK(win_tm.et_tm.tm_mday == 14);
+ CHECK(win_tm.et_tm.tm_hour == 13);
+ CHECK(win_tm.et_tm.tm_min == 0);
+ CHECK(win_tm.et_tm.tm_sec == 0);
+ }
+
+ {
+ auto rt_res
+ = relative_time::from_str(string_fragment::from_const("0:05"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+
+ time_t t_in = 5 * 60 + 15;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ auto win_opt = rt.window_start(tm);
+ auto win_tm = *win_opt;
+ CHECK(win_tm.et_tm.tm_sec == 0);
+ CHECK(win_tm.et_tm.tm_min == 5);
+ CHECK(win_tm.et_tm.tm_hour == 0);
+
+ t_in = 4 * 60 + 15;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ win_opt = rt.window_start(tm);
+ CHECK(!win_opt.has_value());
+ }
+
+ {
+ auto rt_res
+ = relative_time::from_str(string_fragment::from_const("mon"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+
+ time_t t_in = 1615841352;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ auto win_opt = rt.window_start(tm);
+ auto win_tm = *win_opt;
+ CHECK(win_tm.et_tm.tm_year == 121);
+ CHECK(win_tm.et_tm.tm_mon == 2);
+ CHECK(win_tm.et_tm.tm_mday == 15);
+ CHECK(win_tm.et_tm.tm_hour == 0);
+ CHECK(win_tm.et_tm.tm_min == 0);
+ CHECK(win_tm.et_tm.tm_sec == 0);
+ }
+
+ {
+ auto rt_res
+ = relative_time::from_str(string_fragment::from_const("tue"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+ CHECK(rt.rt_included_days
+ == std::set<relative_time::token_t>{relative_time::RTT_TUESDAY});
+ }
+
+ {
+ auto rt_res
+ = relative_time::from_str(string_fragment::from_const("1m"));
+
+ CHECK(rt_res.isOk());
+ auto rt = rt_res.unwrap();
+
+ time_t t_in = 30;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ auto win_opt = rt.window_start(tm);
+ auto win_tm = *win_opt;
+ CHECK(win_tm.et_tm.tm_sec == 0);
+ CHECK(win_tm.et_tm.tm_min == 0);
+ CHECK(win_tm.et_tm.tm_hour == 0);
+
+ t_in = 90;
+ memset(&tm, 0, sizeof(tm));
+ tm.et_tm = *gmtime(&t_in);
+ win_opt = rt.window_start(tm);
+ win_tm = *win_opt;
+ CHECK(win_tm.et_tm.tm_sec == 0);
+ CHECK(win_tm.et_tm.tm_min == 1);
+ CHECK(win_tm.et_tm.tm_hour == 0);
+ }
+
+ relative_time rt;
+ for (int lpc = 0; TEST_DATA[lpc].reltime; lpc++) {
+ auto res = relative_time::from_str(
+ string_fragment::from_c_str(TEST_DATA[lpc].reltime));
+ CHECK_MESSAGE(res.isOk(), TEST_DATA[lpc].reltime);
+ rt = res.unwrap();
+ CHECK(std::string(TEST_DATA[lpc].expected) == rt.to_string());
+ rt.negate();
+ CHECK(std::string(TEST_DATA[lpc].expected_negate) == rt.to_string());
+ }
+
+ for (int lpc = 0; BAD_TEST_DATA[lpc].reltime; lpc++) {
+ auto res = relative_time::from_str(
+ string_fragment::from_c_str(BAD_TEST_DATA[lpc].reltime));
+ CHECK(res.isErr());
+ CHECK(res.unwrapErr().pe_msg
+ == string(BAD_TEST_DATA[lpc].expected_error));
+ }
+
+ rt = relative_time::from_str(string_fragment::from_const("")).unwrap();
+ CHECK(rt.empty());
+
+ rt = relative_time::from_str(string_fragment::from_const("a minute ago"))
+ .unwrap();
+ CHECK(rt.rt_field[relative_time::RTF_MINUTES].value == -1);
+ CHECK(rt.is_negative() == true);
+
+ rt = relative_time::from_str(string_fragment::from_const("5 milliseconds"))
+ .unwrap();
+ CHECK(rt.rt_field[relative_time::RTF_MICROSECONDS].value == 5 * 1000);
+
+ rt = relative_time::from_str(string_fragment::from_const("5000 ms ago"))
+ .unwrap();
+ CHECK(rt.rt_field[relative_time::RTF_SECONDS].value == -5);
+
+ rt = relative_time::from_str(
+ string_fragment::from_const("5 hours 20 minutes ago"))
+ .unwrap();
+
+ CHECK(rt.rt_field[relative_time::RTF_HOURS].value == -5);
+ CHECK(rt.rt_field[relative_time::RTF_MINUTES].value == -20);
+
+ rt = relative_time::from_str(
+ string_fragment::from_const("5 hours and 20 minutes ago"))
+ .unwrap();
+
+ CHECK(rt.rt_field[relative_time::RTF_HOURS].value == -5);
+ CHECK(rt.rt_field[relative_time::RTF_MINUTES].value == -20);
+
+ rt = relative_time::from_str(string_fragment::from_const("1:23")).unwrap();
+
+ CHECK(rt.rt_field[relative_time::RTF_HOURS].value == 1);
+ CHECK(rt.rt_field[relative_time::RTF_MINUTES].value == 23);
+ CHECK(rt.is_absolute());
+
+ rt = relative_time::from_str(string_fragment::from_const("1:23:45"))
+ .unwrap();
+
+ CHECK(rt.rt_field[relative_time::RTF_HOURS].value == 1);
+ CHECK(rt.rt_field[relative_time::RTF_MINUTES].value == 23);
+ CHECK(rt.rt_field[relative_time::RTF_SECONDS].value == 45);
+ CHECK(rt.is_absolute());
+
+ tm = base_tm;
+ tm = rt.adjust(tm);
+
+ new_time = timegm(&tm.et_tm);
+ tm.et_tm = *gmtime(&new_time);
+ CHECK(tm.et_tm.tm_hour == 1);
+ CHECK(tm.et_tm.tm_min == 23);
+
+ rt = relative_time::from_str(string_fragment::from_const("5 minutes ago"))
+ .unwrap();
+
+ tm = base_tm;
+ tm = rt.adjust(tm);
+
+ new_time = timegm(&tm.et_tm);
+
+ CHECK(new_time == (base_time - (5 * 60)));
+
+ rt = relative_time::from_str(string_fragment::from_const("today at 4pm"))
+ .unwrap();
+ memset(&tm, 0, sizeof(tm));
+ memset(&tm2, 0, sizeof(tm2));
+ gettimeofday(&tv, nullptr);
+ localtime_r(&tv.tv_sec, &tm.et_tm);
+ localtime_r(&tv.tv_sec, &tm2.et_tm);
+ tm2.et_tm.tm_hour = 16;
+ tm2.et_tm.tm_min = 0;
+ tm2.et_tm.tm_sec = 0;
+ tm = rt.adjust(tm);
+ tm.et_tm.tm_yday = 0;
+ tm2.et_tm.tm_yday = 0;
+ tm.et_tm.tm_wday = 0;
+ tm2.et_tm.tm_wday = 0;
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ tm2.et_tm.tm_gmtoff = 0;
+ tm2.et_tm.tm_zone = nullptr;
+#endif
+ CHECK(tm.et_tm.tm_year == tm2.et_tm.tm_year);
+ CHECK(tm.et_tm.tm_mon == tm2.et_tm.tm_mon);
+ CHECK(tm.et_tm.tm_mday == tm2.et_tm.tm_mday);
+ CHECK(tm.et_tm.tm_hour == tm2.et_tm.tm_hour);
+ CHECK(tm.et_tm.tm_min == tm2.et_tm.tm_min);
+ CHECK(tm.et_tm.tm_sec == tm2.et_tm.tm_sec);
+
+ rt = relative_time::from_str(
+ string_fragment::from_const("yesterday at 4pm"))
+ .unwrap();
+ gettimeofday(&tv, nullptr);
+ localtime_r(&tv.tv_sec, &tm.et_tm);
+ localtime_r(&tv.tv_sec, &tm2.et_tm);
+ tm2.et_tm.tm_mday -= 1;
+ tm2.et_tm.tm_hour = 16;
+ tm2.et_tm.tm_min = 0;
+ tm2.et_tm.tm_sec = 0;
+ tm = rt.adjust(tm);
+ tm.et_tm.tm_yday = 0;
+ tm2.et_tm.tm_yday = 0;
+ tm.et_tm.tm_wday = 0;
+ tm2.et_tm.tm_wday = 0;
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ tm2.et_tm.tm_gmtoff = 0;
+ tm2.et_tm.tm_zone = NULL;
+#endif
+ CHECK(tm.et_tm.tm_year == tm2.et_tm.tm_year);
+ CHECK(tm.et_tm.tm_mon == tm2.et_tm.tm_mon);
+ CHECK(tm.et_tm.tm_mday == tm2.et_tm.tm_mday);
+ CHECK(tm.et_tm.tm_hour == tm2.et_tm.tm_hour);
+ CHECK(tm.et_tm.tm_min == tm2.et_tm.tm_min);
+ CHECK(tm.et_tm.tm_sec == tm2.et_tm.tm_sec);
+
+ rt = relative_time::from_str(string_fragment::from_const("2 days ago"))
+ .unwrap();
+ gettimeofday(&tv, nullptr);
+ localtime_r(&tv.tv_sec, &tm.et_tm);
+ localtime_r(&tv.tv_sec, &tm2.et_tm);
+ tm2.et_tm.tm_mday -= 2;
+ tm2.et_tm.tm_hour = 0;
+ tm2.et_tm.tm_min = 0;
+ tm2.et_tm.tm_sec = 0;
+ tm = rt.adjust(tm);
+ tm.et_tm.tm_yday = 0;
+ tm2.et_tm.tm_yday = 0;
+ tm.et_tm.tm_wday = 0;
+ tm2.et_tm.tm_wday = 0;
+#ifdef HAVE_STRUCT_TM_TM_ZONE
+ tm2.et_tm.tm_gmtoff = 0;
+ tm2.et_tm.tm_zone = nullptr;
+#endif
+ CHECK(tm.et_tm.tm_year == tm2.et_tm.tm_year);
+ CHECK(tm.et_tm.tm_mon == tm2.et_tm.tm_mon);
+ CHECK(tm.et_tm.tm_mday == tm2.et_tm.tm_mday);
+ CHECK(tm.et_tm.tm_hour == tm2.et_tm.tm_hour);
+ CHECK(tm.et_tm.tm_min == tm2.et_tm.tm_min);
+ CHECK(tm.et_tm.tm_sec == tm2.et_tm.tm_sec);
+}
diff --git a/test/test_remote.sh b/test/test_remote.sh
new file mode 100644
index 0000000..946b19f
--- /dev/null
+++ b/test/test_remote.sh
@@ -0,0 +1,119 @@
+#! /bin/bash
+
+echo "Hello, World!" > not:a:remote:file
+
+run_test ${lnav_test} -d /tmp/lnav.err -n \
+ not:a:remote:file
+
+check_output "a file with colons cannot be read?" <<EOF
+Hello, World!
+EOF
+
+run_test ${lnav_test} -d /tmp/lnav.err -Nn \
+ -c ':open not:a:remote:file'
+
+check_output "a file with colons cannot be read?" <<EOF
+Hello, World!
+EOF
+
+mkdir not:a:remote:dir
+echo "Hello, World!" > not:a:remote:dir/file
+
+run_test ${lnav_test} -d /tmp/lnav.err -n \
+ not:a:remote:dir
+
+check_output "a file in a dir with colons cannot be read?" <<EOF
+Hello, World!
+EOF
+
+run_test ${lnav_test} -d /tmp/lnav.err -n \
+ not:a:remote:dir/f*
+
+check_output "a wildcard in a dir with colons cannot be read?" <<EOF
+Hello, World!
+EOF
+if [ -d /home/runner ]; then
+chmod 755 /home/runner
+ls -la /home/runner
+fi
+export HOME=${PWD}/remote
+unset XDG_CONFIG_HOME
+
+rm -rf remote-tmp
+mkdir -p remote-tmp
+export TMPDIR=remote-tmp
+
+cat > remote/sshd_config <<EOF
+Port 2222
+UsePam no
+AuthorizedKeysFile ${PWD}/remote/authorized_keys
+HostKey ${PWD}/remote/ssh_host_rsa_key
+HostKey ${PWD}/remote/ssh_host_dsa_key
+ChallengeResponseAuthentication no
+PidFile ${PWD}/remote/sshd.pid
+EOF
+
+cat > remote/ssh_config <<EOF
+Host *
+Port 2222
+IdentityFile ${PWD}/remote/id_rsa
+StrictHostKeyChecking no
+EOF
+
+SSHD_PATH=$(which sshd)
+echo "ssh path: ${SSHD_PATH}"
+
+trap 'kill $(cat remote/sshd.pid)' EXIT
+
+$SSHD_PATH -E ${PWD}/remote/sshd.log -f remote/sshd_config
+
+${lnav_test} -d /tmp/lnav.err -nN \
+ -c ":config /tuning/remote/ssh/options/F ${PWD}/remote/ssh_config"
+
+run_test ${lnav_test} -d /tmp/lnav.err -n \
+ nonexistent-host:${test_dir}/logfile_access_log.0
+
+sed -e "s|ssh:.*|...|g" `test_err_filename` | head -1 \
+ > test_remote.err
+
+mv test_remote.err `test_err_filename`
+check_error_output "no error for nonexistent-host?" <<EOF
+error: unable to open file: nonexistent-host: -- failed to ssh to host: ...
+EOF
+
+run_test ${lnav_test} -d /tmp/lnav.err -n \
+ localhost:nonexistent-file
+
+cat remote/sshd.log
+check_error_output "no error for nonexistent-file?" <<EOF
+error: unable to open file: localhost:nonexistent-file -- unable to lstat -- ENOENT[2]
+EOF
+
+run_test ${lnav_test} -d /tmp/lnav.err -n \
+ localhost:${test_dir}/logfile_access_log.0
+
+check_output "could not download remote file?" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+EOF
+
+run_test ${lnav_test} -d /tmp/lnav.err -n \
+ "localhost:${test_dir}/logfile_access_log.*"
+
+check_output "could not download remote file?" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
+EOF
+
+run_test ${lnav_test} -d /tmp/lnav.err -n \
+ "localhost:${test_dir}/remote-log-dir"
+
+check_output "could not download remote file?" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkboot.gz HTTP/1.0" 404 46210 "-" "gPXE/0.9.7"
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+10.112.81.15 - - [15/Feb/2013:06:00:31 +0000] "-" 400 0 "-" "-"
+EOF
diff --git a/test/test_scripts.sh b/test/test_scripts.sh
new file mode 100644
index 0000000..cbd43e4
--- /dev/null
+++ b/test/test_scripts.sh
@@ -0,0 +1,51 @@
+#! /bin/bash
+
+lnav_test="${top_builddir}/src/lnav-test"
+
+touch scripts-empty
+
+run_test ${lnav_test} -n -d /tmp/lnav.err \
+ -I ${test_dir} \
+ -f 'multiline-echo' \
+ scripts-empty
+
+check_error_output "multiline-echo has errors?" <<EOF
+EOF
+
+check_output "multiline-echo is not working?" <<EOF
+Hello, World!
+Goodbye, World!
+EOF
+
+run_test ${lnav_test} -n -d /tmp/lnav.err \
+ -I ${test_dir} \
+ -f 'redirecting' \
+ scripts-empty
+
+check_error_output "redirecting has errors?" <<EOF
+EOF
+
+check_output "redirecting is not working?" <<EOF
+Howdy!
+Goodbye, World!
+EOF
+
+diff -w -u - hw.txt <<EOF
+Hello, World!
+HOWDY!
+GOODBYE, WORLD!
+EOF
+
+if test $? -ne 0; then
+ echo "Script output was not redirected?"
+ exit 1
+fi
+
+diff -w -u - hw2.txt <<EOF
+HELLO, WORLD!
+EOF
+
+if test $? -ne 0; then
+ echo "Script output was not redirected?"
+ exit 1
+fi
diff --git a/test/test_sessions.sh b/test/test_sessions.sh
new file mode 100644
index 0000000..db71e5f
--- /dev/null
+++ b/test/test_sessions.sh
@@ -0,0 +1,119 @@
+#! /bin/bash
+
+export HOME="./sessions"
+unset XDG_CONFIG_HOME
+rm -rf "./sessions"
+mkdir -p $HOME
+
+run_cap_test ${lnav_test} -n \
+ -c ":reset-session" \
+ -c ":goto 0" \
+ -c ":hide-file" \
+ -c ":save-session" \
+ ${test_dir}/logfile_access_log.*
+
+# hidden file saved in session
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ ${test_dir}/logfile_access_log.*
+
+# setting log_mark
+run_cap_test ${lnav_test} -nq \
+ -c ":reset-session" \
+ -c ";update access_log set log_mark = 1 where sc_bytes > 60000" \
+ -c ":goto 1" \
+ -c ":partition-name middle" \
+ -c ":save-session" \
+ ${test_dir}/logfile_access_log.0
+
+mkdir -p support-dump
+echo 'Hello' > support-dump/readme
+cp ${test_dir}/logfile_access_log.0 support-dump/
+cp ${test_dir}/logfile_access_log.1 support-dump/
+
+run_cap_test ${lnav_test} -nq \
+ -c ";update access_log set log_mark = 1 where sc_bytes > 60000" \
+ -c ":goto 1" \
+ -c ":hide-file */logfile_access_log.1" \
+ -c ":export-session-to -" \
+ support-dump/logfile_access_log.*
+
+run_cap_test ${lnav_test} -nq \
+ -c ";update access_log set log_mark = 1 where sc_bytes > 60000" \
+ -c ":set-min-log-level debug" \
+ -c ":hide-lines-before 2005" \
+ -c ":hide-lines-after 2030" \
+ -c ":filter-out blah" \
+ -c "/foobar" \
+ -c ":goto 1" \
+ -c ":export-session-to exported-session.0.lnav" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c "|exported-session.0.lnav" \
+ -c ";SELECT * FROM lnav_view_filters" \
+ -c ":write-screen-to -" \
+ -c ";SELECT name,search FROM lnav_views" \
+ -c ":write-screen-to -" \
+ ${test_dir}/logfile_access_log.0
+
+# log mark was not saved in session
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ':write-to -' \
+ ${test_dir}/logfile_access_log.0
+
+# file was not closed
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ":close" \
+ -c ":save-session" \
+ ${test_dir}/logfile_access_log.0
+
+# partition name was not saved in session
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ';select log_line,log_part from access_log' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+# adjust time is not working
+run_cap_test ${lnav_test} -nq \
+ -c ":adjust-log-time 2010-01-01T00:00:00" \
+ -c ":save-session" \
+ ${test_dir}/logfile_access_log.0
+
+# adjust time is not saved in session
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ":test-comment adjust time in session" \
+ ${test_dir}/logfile_access_log.0
+
+# hiding fields failed
+rm -rf ./sessions
+mkdir -p $HOME
+run_cap_test ${lnav_test} -nq -d /tmp/lnav.err \
+ -c ":hide-fields c_ip" \
+ -c ":save-session" \
+ ${test_dir}/logfile_access_log.0
+
+# restoring hidden fields failed
+run_cap_test ${lnav_test} -n \
+ -c ":load-session" \
+ -c ":test-comment restoring hidden fields" \
+ ${test_dir}/logfile_access_log.0
+
+# hiding fields failed
+rm -rf ./sessions
+mkdir -p $HOME
+run_cap_test ${lnav_test} -nq -d /tmp/lnav.err \
+ -c ":hide-lines-before 2009-07-20 22:59:29" \
+ -c ":save-session" \
+ ${test_dir}/logfile_access_log.0
+
+# XXX we don't actually check
+# restoring hidden fields failed
+run_cap_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ":load-session" \
+ -c ":test-comment restore hidden lines" \
+ ${test_dir}/logfile_access_log.0
diff --git a/test/test_shlexer.sh b/test/test_shlexer.sh
new file mode 100644
index 0000000..747de95
--- /dev/null
+++ b/test/test_shlexer.sh
@@ -0,0 +1,32 @@
+#! /bin/bash
+
+export FOO='bar'
+export DEF='xyz'
+
+run_cap_test ./drive_shlexer '$FOO'
+
+run_cap_test ./drive_shlexer '${FOO}'
+
+# run_cap_test ./drive_shlexer '\a'
+
+run_cap_test ./drive_shlexer '\'
+
+run_cap_test ./drive_shlexer "'abc'"
+
+run_cap_test ./drive_shlexer '"def"'
+
+run_cap_test ./drive_shlexer '"'"'"'"'
+
+run_cap_test ./drive_shlexer "'"'"'"'"
+
+run_cap_test ./drive_shlexer '"abc $DEF 123"'
+
+run_cap_test ./drive_shlexer '"abc ${DEF} 123"'
+
+run_cap_test ./drive_shlexer "'abc \$DEF 123'"
+
+run_cap_test ./drive_shlexer 'abc $DEF 123'
+
+run_cap_test ./drive_shlexer '~ foo'
+
+run_cap_test ./drive_shlexer '~nonexistent/bar baz'
diff --git a/test/test_sql.sh b/test/test_sql.sh
new file mode 100644
index 0000000..369351c
--- /dev/null
+++ b/test/test_sql.sh
@@ -0,0 +1,1097 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+lnav_test="${top_builddir}/src/lnav-test"
+unset XDG_CONFIG_HOME
+
+run_cap_test ${lnav_test} -nN \
+ -c ";SELECT 1 = ?"
+
+run_cap_test ${lnav_test} -n \
+ -c ";.read nonexistent-file" \
+ ${test_dir}/logfile_empty.0
+
+run_test ${lnav_test} -n \
+ -c ";.read ${test_dir}/file_for_dot_read.sql" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_syslog.0
+
+check_output ".read did not work?" <<EOF
+log_line,log_body
+1, attempting to mount entry /auto/opt
+EOF
+
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT replicate('foobar', 120)" \
+ ${test_dir}/logfile_empty.0
+
+cp ${srcdir}/logfile_syslog.2 logfile_syslog_test.2
+touch -t 201511030923 logfile_syslog_test.2
+run_test ${lnav_test} -n \
+ -c ";SELECT *, log_msg_schema FROM all_logs" \
+ -c ":write-csv-to -" \
+ logfile_syslog_test.2
+
+check_output "all_logs does not work?" <<EOF
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,log_msg_format,log_msg_schema
+0,<NULL>,2015-11-03 09:23:38.000,0,info,0,<NULL>,<NULL>,<NULL>,# is up,aff2bfc3c61e7b86329b83190f0912b3
+1,<NULL>,2015-11-03 09:23:38.000,0,info,0,<NULL>,<NULL>,<NULL>,# is up,aff2bfc3c61e7b86329b83190f0912b3
+2,<NULL>,2015-11-03 09:23:38.000,0,info,0,<NULL>,<NULL>,<NULL>,# is down,506560b3c73dee057732e69a3c666718
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";SELECT fields FROM logfmt_log" \
+ -c ":write-json-to -" \
+ ${test_dir}/logfile_logfmt.0
+
+check_output "logfmt fields are not handled correctly?" <<EOF
+[
+ {
+ "fields": {
+ "namespace": "inc-1-enh-domain-c14-ns-2",
+ "pod": "hello-inc-1-enh-domain-c14-ns-2-3-d8f465685-k75gp",
+ "reason": "",
+ "status": "Pending"
+ }
+ },
+ {
+ "fields": {
+ "error": "pod inc-1-domain-c14-ns-6/fe-inc-1-domain-c14-ns-6-5-656d9bb695-4584b is not found: PodNotFound",
+ "namespace": "inc-1-domain-c14-ns-6",
+ "pod": "fe-inc-1-domain-c14-ns-6-5-656d9bb695-4584b",
+ "uid": "be2def59-3a08-42fd-8f84-6f64cfcefa93"
+ }
+ },
+ {
+ "fields": {
+ "namespace": "inc-1-domain-c14-ns-6",
+ "pod": "fe-inc-1-domain-c14-ns-6-5-656d9bb695-4584b",
+ "uid": "be2def59-3a08-42fd-8f84-6f64cfcefa93"
+ }
+ },
+ {
+ "fields": {
+ "namespace": "inc-1-domain-c14-ns-6",
+ "pod": "fe-inc-1-domain-c14-ns-6-5-656d9bb695-4584b",
+ "uid": "be2def59-3a08-42fd-8f84-6f64cfcefa93"
+ }
+ },
+ {
+ "fields": {
+ "namespace": "inc-1-enh-domain-c14-ns-2",
+ "pod": "hello-inc-1-enh-domain-c14-ns-2-7-5ddd6bcd69-6rqct",
+ "reason": "",
+ "status": "Pending"
+ }
+ }
+]
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";SELECT sc_substatus FROM w3c_log" \
+ -c ":write-json-to -" \
+ ${test_dir}/logfile_w3c.3
+
+check_output "w3c quoted strings are not handled correctly?" <<EOF
+[
+ {
+ "sc_substatus": 0
+ },
+ {
+ "sc_substatus": 0
+ },
+ {
+ "sc_substatus": null
+ }
+]
+EOF
+
+run_test ${lnav_test} -n \
+ -c ";SELECT cs_headers FROM w3c_log" \
+ -c ":write-json-to -" \
+ ${test_dir}/logfile_w3c.3
+
+check_output "w3c headers are not captured?" <<EOF
+[
+ {
+ "cs_headers": {
+ "User-Agent": "Mozilla/5.0 (Linux; Android 4.4.4; SM-G900V Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.59 Mobile Safari/537.36",
+ "Referer": "http://example.com/Search/SearchResults.pg?informationRecipient.languageCode.c=en",
+ "Host": "xzy.example.com"
+ }
+ },
+ {
+ "cs_headers": {
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/41.0.2227.1 Safari/537.36",
+ "Referer": null,
+ "Host": "example.hello.com"
+ }
+ },
+ {
+ "cs_headers": {
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2062.124 Safari/537.36",
+ "Referer": null,
+ "Host": "hello.example.com"
+ }
+ }
+]
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * FROM generate_series()" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * FROM generate_series(1)" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT 1 AS inum, NULL AS nul, 2.0 AS fnum, 'abc' AS str" \
+ -c ";SELECT \$inum, \$nul, \$fnum, \$str, raise_error('oops!')" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT raise_error('oops!')" \
+ ${test_dir}/logfile_access_log.0
+
+run_test ${lnav_test} -n \
+ -c ";SELECT basename(filepath) as name, content, length(content) FROM lnav_file" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_empty.0
+
+check_output "empty content not handled correctly?" <<EOF
+name,content,length(content)
+logfile_empty.0,,0
+EOF
+
+run_test ${lnav_test} -n \
+ -c ";SELECT distinct xp.node_text FROM lnav_file, xpath('//author', content) as xp" \
+ -c ":write-csv-to -" \
+ ${test_dir}/books.xml
+
+check_output "xpath on file content not working?" <<EOF
+node_text
+"Gambardella, Matthew"
+"Ralls, Kim"
+"Corets, Eva"
+"Randall, Cynthia"
+"Thurman, Paula"
+"Knorr, Stefan"
+"Kress, Peter"
+"O'Brien, Tim"
+"Galos, Mike"
+EOF
+
+gzip -c ${srcdir}/logfile_json.json > logfile_json.json.gz
+dd if=logfile_json.json.gz of=logfile_json-trunc.json.gz bs=64 count=2
+
+# TODO re-enable this
+#run_test ${lnav_test} -n \
+# -c ";SELECT content FROM lnav_file" \
+# logfile_json-trunc.json.gz
+
+#check_error_output "invalid gzip file working?" <<EOF
+#command-option:1: error: unable to uncompress: logfile_json-trunc.json.gz -- buffer error
+#EOF
+
+run_test ${lnav_test} -n \
+ -c ";SELECT jget(rc.content, '/ts') AS ts FROM lnav_file, regexp_capture(lnav_file.content, '.*\n') as rc" \
+ -c ":write-csv-to -" \
+ logfile_json.json.gz
+
+check_output "jget on file content not working?" <<EOF
+ts
+2013-09-06T20:00:48.124817Z
+2013-09-06T20:00:49.124817Z
+2013-09-06T22:00:49.124817Z
+2013-09-06T22:00:59.124817Z
+2013-09-06T22:00:59.124817Z
+2013-09-06T22:00:59.124817Z
+2013-09-06T22:00:59.124817Z
+2013-09-06T22:00:59.124817Z
+2013-09-06T22:01:49.124817Z
+2013-09-06T22:01:49.124817Z
+2013-09-06T22:01:49.124817Z
+2013-09-06T22:01:49.124817Z
+2013-09-06T22:01:49.124817Z
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_file SET filepath='foo' WHERE endswith(filepath, '_log.0')" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c "|rename-stdin" \
+ ${test_dir}/logfile_access_log.0 < /dev/null
+
+run_cap_test ${lnav_test} -n \
+ -c "|rename-stdin foo" \
+ ${test_dir}/logfile_access_log.0 < /dev/null
+
+run_cap_test ${lnav_test} -n \
+ -c "|rename-stdin foo" \
+ -c ";SELECT filepath FROM lnav_file" <<EOF
+Hello, World!
+EOF
+
+run_test ${lnav_test} -n \
+ -c ";SELECT basename(filepath),format,lines,time_offset FROM lnav_file LIMIT 2" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_access_log.0 \
+ ${test_dir}/logfile_access_log.1
+
+check_output "lnav_file table is not working?" <<EOF
+basename(filepath),format,lines,time_offset
+logfile_access_log.0,access_log,3,0
+logfile_access_log.1,access_log,1,0
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_file SET time_offset = 60 * 1000" \
+ ${test_dir}/logfile_access_log.0 \
+ ${test_dir}/logfile_access_log.1
+
+run_test ${lnav_test} -n \
+ -c ";UPDATE lnav_file SET time_offset=14400000 WHERE endswith(filepath, 'logfile_block.1')" \
+ ${test_dir}/logfile_block.1 \
+ ${test_dir}/logfile_block.2
+
+check_output "time_offset in lnav_file table is not reordering?" <<EOF
+Wed May 19 12:00:01 2021 line 1
+/abc/def
+Wed May 19 12:00:02 UTC 2021 line 2
+Wed May 19 12:00:03 2021 line 3
+/ghi/jkl
+Wed May 19 12:00:04 UTC 2021 line 4
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";SELECT * FROM access_log LIMIT 0" \
+ -c ':switch-to-view db' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "output generated for empty result set?" <<EOF
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 2" \
+ -c ";SELECT log_top_line()" \
+ ${test_dir}/logfile_uwsgi.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 2" \
+ -c ";SELECT log_top_line()" \
+ ${test_dir}/logfile_empty.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 2" \
+ -c ";SELECT log_top_datetime()" \
+ ${test_dir}/logfile_uwsgi.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 2" \
+ -c ";SELECT log_top_datetime()" \
+ ${test_dir}/logfile_empty.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * FROM uwsgi_log LIMIT 1" \
+ -c ':switch-to-view db' \
+ ${test_dir}/logfile_uwsgi.0
+
+run_test ${lnav_test} -n \
+ -c ";SELECT s_runtime FROM uwsgi_log LIMIT 5" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_uwsgi.0
+
+check_output "uwsgi scaling not working?" <<EOF
+s_runtime
+0.129
+0.035
+6.8e-05
+0.016
+0.01
+EOF
+
+run_test env TZ=UTC ${lnav_test} -n \
+ -c ";SELECT bro_conn_log.bro_duration as duration, bro_conn_log.bro_uid, group_concat( distinct (bro_method || ' ' || bro_host)) as req from bro_http_log, bro_conn_log where bro_http_log.bro_uid = bro_conn_log.bro_uid group by bro_http_log.bro_uid order by duration desc limit 10" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_bro_http.log.0 ${test_dir}/logfile_bro_conn.log.0
+
+check_output "bro logs are not recognized?" <<EOF
+duration,bro_uid,req
+116.438679,CwFs1P2UcUdlSxD2La,GET www.reddit.com
+115.202498,CdZUPH2DKOE7zzCLE3,GET feeds.bbci.co.uk
+115.121914,CdrfXZ1NOFPEawF218,GET c.thumbs.redditmedia.com
+115.121837,CoX7zA3OJKGUOSCBY2,GET e.thumbs.redditmedia.com
+115.12181,CJxSUgkInyKSHiju1,GET e.thumbs.redditmedia.com
+115.121506,CT0JIh479jXIGt0Po1,GET f.thumbs.redditmedia.com
+115.121339,CJwUi9bdB9c1lLW44,GET f.thumbs.redditmedia.com
+115.119217,C6Q4Vm14ZJIlZhsXqk,GET a.thumbs.redditmedia.com
+72.274459,CbNCgO1MzloHRNeY4f,GET www.google.com
+71.658218,CnGze54kQWWpKqrrZ4,GET ajax.googleapis.com
+EOF
+
+run_test env TZ=UTC ${lnav_test} -n \
+ -c ";SELECT * FROM bro_http_log LIMIT 5" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_bro_http.log.0
+
+check_output "bro logs are not recognized?" <<EOF
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,bro_ts,bro_uid,bro_id_orig_h,bro_id_orig_p,bro_id_resp_h,bro_id_resp_p,bro_trans_depth,bro_method,bro_host,bro_uri,bro_referrer,bro_version,bro_user_agent,bro_request_body_len,bro_response_body_len,bro_status_code,bro_status_msg,bro_info_code,bro_info_msg,bro_tags,bro_username,bro_password,bro_proxied,bro_orig_fuids,bro_orig_filenames,bro_orig_mime_types,bro_resp_fuids,bro_resp_filenames,bro_resp_mime_types
+0,<NULL>,2011-11-03 00:19:26.452,0,info,0,<NULL>,<NULL>,<NULL>,1320279566.452687,CwFs1P2UcUdlSxD2La,192.168.2.76,52026,132.235.215.119,80,1,GET,www.reddit.com,/,<NULL>,1.1,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1,0,109978,200,OK,<NULL>,<NULL>,,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,Ftw3fJ2JJF3ntMTL2,<NULL>,text/html
+1,<NULL>,2011-11-03 00:19:26.831,379,info,0,<NULL>,<NULL>,<NULL>,1320279566.831619,CJxSUgkInyKSHiju1,192.168.2.76,52030,72.21.211.173,80,1,GET,e.thumbs.redditmedia.com,/E-pbDbmiBclPkDaX.jpg,http://www.reddit.com/,1.1,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1,0,2300,200,OK,<NULL>,<NULL>,,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,FFTf9Zdgk3YkfCKo3,<NULL>,image/jpeg
+2,<NULL>,2011-11-03 00:19:26.831,0,info,0,<NULL>,<NULL>,<NULL>,1320279566.831563,CJwUi9bdB9c1lLW44,192.168.2.76,52029,72.21.211.173,80,1,GET,f.thumbs.redditmedia.com,/BP5bQfy4o-C7cF6A.jpg,http://www.reddit.com/,1.1,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1,0,2272,200,OK,<NULL>,<NULL>,,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,FfXtOj3o7aub4vbs2j,<NULL>,image/jpeg
+3,<NULL>,2011-11-03 00:19:26.831,0,info,0,<NULL>,<NULL>,<NULL>,1320279566.831473,CoX7zA3OJKGUOSCBY2,192.168.2.76,52027,72.21.211.173,80,1,GET,e.thumbs.redditmedia.com,/SVUtep3Rhg5FTRn4.jpg,http://www.reddit.com/,1.1,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1,0,2562,200,OK,<NULL>,<NULL>,,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,F21Ybs3PTqS6O4Q2Zh,<NULL>,image/jpeg
+4,<NULL>,2011-11-03 00:19:26.831,0,info,0,<NULL>,<NULL>,<NULL>,1320279566.831643,CT0JIh479jXIGt0Po1,192.168.2.76,52031,72.21.211.173,80,1,GET,f.thumbs.redditmedia.com,/uuy31444rLSyKdHS.jpg,http://www.reddit.com/,1.1,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1,0,1595,200,OK,<NULL>,<NULL>,,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,Fdk0MZ1wQmKWAJ4WH4,<NULL>,image/jpeg
+EOF
+
+run_test env TZ=UTC ${lnav_test} -n \
+ -c ";SELECT * FROM bro_http_log WHERE log_level = 'error'" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_bro_http.log.0
+
+check_output "bro logs are not recognized?" <<EOF
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,bro_ts,bro_uid,bro_id_orig_h,bro_id_orig_p,bro_id_resp_h,bro_id_resp_p,bro_trans_depth,bro_method,bro_host,bro_uri,bro_referrer,bro_version,bro_user_agent,bro_request_body_len,bro_response_body_len,bro_status_code,bro_status_msg,bro_info_code,bro_info_msg,bro_tags,bro_username,bro_password,bro_proxied,bro_orig_fuids,bro_orig_filenames,bro_orig_mime_types,bro_resp_fuids,bro_resp_filenames,bro_resp_mime_types
+118,<NULL>,2011-11-03 00:19:49.337,18,error,0,<NULL>,<NULL>,<NULL>,1320279589.337053,CBHHuR1xFnm5C5CQBc,192.168.2.76,52074,74.125.225.76,80,1,GET,i4.ytimg.com,/vi/gDbg_GeuiSY/hqdefault.jpg,<NULL>,1.1,Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 Firefox/7.0.1,0,893,404,Not Found,<NULL>,<NULL>,,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,<NULL>,F2GiAw3j1m22R2yIg2,<NULL>,image/jpeg
+EOF
+
+run_test ${lnav_test} -n \
+ -c ';select log_time from access_log where log_line > 100000' \
+ -c ':switch-to-view db' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "out-of-range query failed?" <<EOF
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ';select log_time from access_log where log_line > -100000' \
+ ${test_dir}/logfile_access_log.0
+
+run_test ${lnav_test} -n \
+ -c ';select log_time from access_log where log_line < -10000' \
+ -c ':switch-to-view db' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "out-of-range query failed?" <<EOF
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ';select log_time from access_log where log_line > -10000' \
+ ${test_dir}/logfile_access_log.0
+
+run_test ${lnav_test} -n \
+ -c ';select log_time from access_log where log_line < 0' \
+ -c ':switch-to-view db' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "out-of-range query failed?" <<EOF
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ';select log_time from access_log where log_line <= 0' \
+ -c ':switch-to-view db' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';select log_time from access_log where log_line >= 0' \
+ -c ':switch-to-view db' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';select sc_bytes from access_log' \
+ -c ':spectrogram sc_bytes' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';select log_time,sc_bytes from access_log' \
+ -c ':spectrogram sc_byes' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';select log_time,c_ip from access_log' \
+ -c ':spectrogram c_ip' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test env TZ=UTC LC_NUMERIC=C ${lnav_test} -n \
+ -c ':spectrogram bro_response_body_len' \
+ ${test_dir}/logfile_bro_http.log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';select log_time,sc_bytes from access_log order by log_time desc' \
+ -c ':spectrogram sc_bytes' \
+ ${test_dir}/logfile_access_log.0
+
+cp ${srcdir}/logfile_syslog_with_mixed_times.0 logfile_syslog_with_mixed_times_test.0
+touch -t 201511030923 logfile_syslog_with_mixed_times_test.0
+run_test ${lnav_test} -n \
+ -c ";select log_time,log_actual_time from syslog_log" \
+ -c ':write-csv-to -' \
+ logfile_syslog_with_mixed_times_test.0
+
+check_output "log_actual_time column not working" <<EOF
+log_time,log_actual_time
+2015-09-13 00:58:45.000,2015-09-13 00:58:45.000
+2015-09-13 00:59:30.000,2015-09-13 00:59:30.000
+2015-09-13 01:23:54.000,2015-09-13 01:23:54.000
+2015-09-13 03:12:04.000,2015-09-13 03:12:04.000
+2015-09-13 03:12:04.000,2015-09-13 03:12:04.000
+2015-09-13 03:12:04.000,2015-09-13 01:25:39.000
+2015-09-13 03:12:04.000,2015-09-13 03:12:04.000
+2015-09-13 03:12:58.000,2015-09-13 03:12:58.000
+2015-09-13 03:46:03.000,2015-09-13 03:46:03.000
+2015-09-13 03:46:03.000,2015-09-13 03:46:03.000
+2015-09-13 03:46:03.000,2015-09-13 03:46:03.000
+2015-09-13 03:46:03.000,2015-09-13 03:13:16.000
+2015-09-13 03:46:03.000,2015-09-13 03:46:03.000
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";update access_log set log_part = 'middle' where log_line = 1" \
+ -c ';select log_line, log_part from access_log' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "setting log_part is not working" <<EOF
+log_line,log_part
+0,<NULL>
+1,middle
+2,middle
+EOF
+
+run_test ${lnav_test} -n \
+ -c ";update access_log set log_part = 'middle' where log_line = 1" \
+ -c ";update access_log set log_part = NULL where log_line = 1" \
+ -c ';select log_line, log_part from access_log' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "setting log_part is not working" <<EOF
+log_line,log_part
+0,<NULL>
+1,<NULL>
+2,<NULL>
+EOF
+
+run_test ${lnav_test} -n \
+ -c ";update access_log set log_part = 'middle' where log_line = 1" \
+ -c ";update access_log set log_part = NULL where log_line = 2" \
+ -c ';select log_line, log_part from access_log' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "setting log_part is not working" <<EOF
+log_line,log_part
+0,<NULL>
+1,middle
+2,middle
+EOF
+
+
+run_test ${lnav_test} -n \
+ -I "${top_srcdir}/test" \
+ -c ";select * from web_status" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "access_log table is not working" <<EOF
+group_concat(cs_uri_stem),sc_status
+"/vmw/cgi/tramp,/vmw/vSphere/default/vmkernel.gz",200
+/vmw/vSphere/default/vmkboot.gz,404
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";select * from access_log" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "access_log table is not working" <<EOF
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,c_ip,cs_method,cs_referer,cs_uri_query,cs_uri_stem,cs_user_agent,cs_username,cs_version,sc_bytes,sc_status,cs_host
+0,<NULL>,2009-07-20 22:59:26.000,0,info,0,<NULL>,<NULL>,<NULL>,192.168.202.254,GET,-,<NULL>,/vmw/cgi/tramp,gPXE/0.9.7,-,HTTP/1.0,134,200,<NULL>
+1,<NULL>,2009-07-20 22:59:29.000,3000,error,0,<NULL>,<NULL>,<NULL>,192.168.202.254,GET,-,<NULL>,/vmw/vSphere/default/vmkboot.gz,gPXE/0.9.7,-,HTTP/1.0,46210,404,<NULL>
+2,<NULL>,2009-07-20 22:59:29.000,0,info,0,<NULL>,<NULL>,<NULL>,192.168.202.254,GET,-,<NULL>,/vmw/vSphere/default/vmkernel.gz,gPXE/0.9.7,-,HTTP/1.0,78929,200,<NULL>
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";select * from access_log where log_level >= 'warning'" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "loglevel collator is not working" <<EOF
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,c_ip,cs_method,cs_referer,cs_uri_query,cs_uri_stem,cs_user_agent,cs_username,cs_version,sc_bytes,sc_status,cs_host
+1,<NULL>,2009-07-20 22:59:29.000,3000,error,0,<NULL>,<NULL>,<NULL>,192.168.202.254,GET,-,<NULL>,/vmw/vSphere/default/vmkboot.gz,gPXE/0.9.7,-,HTTP/1.0,46210,404,<NULL>
+EOF
+
+
+# XXX The timestamp on the file is used to determine the year for syslog files.
+touch -t 200711030923 ${test_dir}/logfile_syslog.0
+run_test ${lnav_test} -n \
+ -c ";select * from syslog_log" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_syslog.0
+
+check_output "syslog_log table is not working" <<EOF
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,log_hostname,log_msgid,log_pid,log_pri,log_procname,log_struct,log_syslog_tag,syslog_version
+0,<NULL>,2007-11-03 09:23:38.000,0,error,0,<NULL>,<NULL>,<NULL>,veridian,<NULL>,7998,<NULL>,automount,<NULL>,automount[7998],<NULL>
+1,<NULL>,2007-11-03 09:23:38.000,0,info,0,<NULL>,<NULL>,<NULL>,veridian,<NULL>,16442,<NULL>,automount,<NULL>,automount[16442],<NULL>
+2,<NULL>,2007-11-03 09:23:38.000,0,error,0,<NULL>,<NULL>,<NULL>,veridian,<NULL>,7999,<NULL>,automount,<NULL>,automount[7999],<NULL>
+3,<NULL>,2007-11-03 09:47:02.000,1404000,info,0,<NULL>,<NULL>,<NULL>,veridian,<NULL>,<NULL>,<NULL>,sudo,<NULL>,sudo,<NULL>
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";select * from syslog_log where log_time >= NULL" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_syslog.0
+
+check_output "log_time collation failed on null" <<EOF
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";select log_line from syslog_log where log_time >= datetime('2007-11-03T09:47:02.000')" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_syslog.0
+
+check_output "log_time collation is wrong" <<EOF
+log_line
+3
+EOF
+
+
+run_cap_test ${lnav_test} -n \
+ -c ':filter-in sudo' \
+ -c ";select * from logline" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_syslog.0
+
+run_test ${lnav_test} -n \
+ -c ':goto 1' \
+ -c ";select log_line, log_pid, col_0 from logline" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_syslog.1
+
+check_output "logline table is not working" <<EOF
+log_line,log_pid,col_0
+1,16442,/auto/opt
+EOF
+
+run_test ${lnav_test} -n \
+ -c ";select sc_bytes from logline" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "logline table is not working for defined columns" <<EOF
+sc_bytes
+134
+46210
+78929
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ':goto 1' \
+ -c ":summarize col_0" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_syslog.1
+
+check_output "summarize is not working" <<EOF
+c_col_0,count_col_0
+/auto/opt,1
+EOF
+
+
+run_cap_test ${lnav_test} -n \
+ -c ";update access_log set log_mark = 1 where sc_bytes > 60000" \
+ -c ':write-to -' \
+ ${test_dir}/logfile_access_log.0
+
+export SQL_ENV_VALUE="foo bar,baz"
+
+run_test ${lnav_test} -n \
+ -c ';select $SQL_ENV_VALUE as val' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "env vars are not working in SQL" <<EOF
+val
+"foo bar,baz"
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ';SELECT name,value FROM environ WHERE name = "SQL_ENV_VALUE"' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "environ table is not working in SQL" <<EOF
+name,value
+SQL_ENV_VALUE,"foo bar,baz"
+EOF
+
+
+run_cap_test ${lnav_test} -n \
+ -c ';INSERT INTO environ (name) VALUES (null)' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';INSERT INTO environ (name, value) VALUES (null, null)' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';INSERT INTO environ (name, value) VALUES ("", null)' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';INSERT INTO environ (name, value) VALUES ("foo=bar", "bar")' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';INSERT INTO environ (name, value) VALUES ("SQL_ENV_VALUE", "bar")' \
+ ${test_dir}/logfile_access_log.0
+
+run_test ${lnav_test} -n \
+ -c ';INSERT OR IGNORE INTO environ (name, value) VALUES ("SQL_ENV_VALUE", "bar")' \
+ -c ';SELECT * FROM environ WHERE name = "SQL_ENV_VALUE"' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "insert into environ table works" <<EOF
+name,value
+SQL_ENV_VALUE,"foo bar,baz"
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ';REPLACE INTO environ (name, value) VALUES ("SQL_ENV_VALUE", "bar")' \
+ -c ';SELECT * FROM environ WHERE name = "SQL_ENV_VALUE"' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "replace into environ table works" <<EOF
+name,value
+SQL_ENV_VALUE,bar
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ';INSERT INTO environ (name, value) VALUES ("foo_env", "bar")' \
+ -c ';SELECT $foo_env as val' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "insert into environ table does not work" <<EOF
+val
+bar
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ';UPDATE environ SET name="NEW_ENV_VALUE" WHERE name="SQL_ENV_VALUE"' \
+ -c ";SELECT * FROM environ WHERE name like '%ENV_VALUE'" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "update environ table does not work" <<EOF
+name,value
+NEW_ENV_VALUE,"foo bar,baz"
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ';DELETE FROM environ WHERE name="SQL_ENV_VALUE"' \
+ -c ';SELECT * FROM environ WHERE name like "%ENV_VALUE"' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "delete from environ table does not work" <<EOF
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ';DELETE FROM environ' \
+ -c ';SELECT * FROM environ' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "delete environ table does not work" <<EOF
+EOF
+
+
+
+schema_dump() {
+ ${lnav_test} -n -c ';.schema' ${test_dir}/logfile_access_log.0 | head -n21
+}
+
+run_test schema_dump
+
+check_output "schema view is not working" <<EOF
+ATTACH DATABASE '' AS 'main';
+CREATE VIRTUAL TABLE environ USING environ_vtab_impl();
+CREATE VIRTUAL TABLE lnav_static_files USING lnav_static_file_vtab_impl();
+CREATE VIRTUAL TABLE lnav_views USING lnav_views_impl();
+CREATE VIRTUAL TABLE lnav_view_filter_stats USING lnav_view_filter_stats_impl();
+CREATE VIRTUAL TABLE lnav_view_files USING lnav_view_files_impl();
+CREATE VIRTUAL TABLE lnav_view_stack USING lnav_view_stack_impl();
+CREATE VIRTUAL TABLE lnav_view_filters USING lnav_view_filters_impl();
+CREATE VIRTUAL TABLE lnav_file USING lnav_file_impl();
+CREATE VIRTUAL TABLE lnav_file_metadata USING lnav_file_metadata_impl();
+CREATE VIEW lnav_view_filters_and_stats AS
+ SELECT * FROM lnav_view_filters LEFT NATURAL JOIN lnav_view_filter_stats;
+CREATE VIRTUAL TABLE regexp_capture USING regexp_capture_impl();
+CREATE VIRTUAL TABLE regexp_capture_into_json USING regexp_capture_into_json_impl();
+CREATE VIRTUAL TABLE xpath USING xpath_impl();
+CREATE VIRTUAL TABLE fstat USING fstat_impl();
+CREATE TABLE lnav_events (
+ ts TEXT NOT NULL DEFAULT(strftime('%Y-%m-%dT%H:%M:%f', 'now')),
+ content TEXT
+);
+CREATE TABLE http_status_codes
+EOF
+
+
+run_cap_test ${lnav_test} -n \
+ -c ";select * from nonexistent_table" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";delete from access_log" \
+ ${test_dir}/logfile_access_log.0
+
+touch -t 201504070732 ${test_dir}/logfile_pretty.0
+run_test ${lnav_test} -n \
+ -c ":goto 1" \
+ -c ":partition-name middle" \
+ -c ":goto 21" \
+ -c ":partition-name end" \
+ -c ";select log_line,log_part,log_time from syslog_log" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_pretty.0
+
+check_output "partition-name does not work" <<EOF
+log_line,log_part,log_time
+0,<NULL>,2015-04-07 00:49:42.000
+1,middle,2015-04-07 05:49:53.000
+18,middle,2015-04-07 07:31:56.000
+20,middle,2015-04-07 07:31:56.000
+21,end,2015-04-07 07:31:56.000
+22,end,2015-04-07 07:32:56.000
+EOF
+
+run_test ${lnav_test} -n \
+ -c ":goto 1" \
+ -c ":partition-name middle" \
+ -c ":clear-partition" \
+ -c ";select log_line, log_part from access_log" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_access_log.0
+
+check_output "clear-partition does not work" <<EOF
+log_line,log_part
+0,<NULL>
+1,<NULL>
+2,<NULL>
+EOF
+
+run_test ${lnav_test} -n \
+ -c ":goto 1" \
+ -c ":partition-name middle" \
+ -c ":goto 2" \
+ -c ":clear-partition" \
+ -c ";select log_line, log_part from access_log" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_access_log.0
+
+check_output "clear-partition does not work when in the middle of a part" <<EOF
+log_line,log_part
+0,<NULL>
+1,<NULL>
+2,<NULL>
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";SELECT * FROM openam_log" \
+ -c ":write-json-to -" \
+ ${test_dir}/logfile_openam.0
+
+check_output "write-json-to isn't working?" <<EOF
+[
+ {
+ "log_line": 0,
+ "log_part": null,
+ "log_time": "2014-06-15 01:04:52.000",
+ "log_idle_msecs": 0,
+ "log_level": "info",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "contextid": "82e87195d704585501",
+ "data": "http://localhost:8086|/|<samlp:Response xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\" ID=\"s2daac0735bf476f4560aab81104b623bedfb0cbc0\" InResponseTo=\"84cbf2be33f6410bbe55877545a93f02\" Version=\"2.0\" IssueInstant=\"2014-06-15T01:04:52Z\" Destination=\"http://localhost:8086/api/1/rest/admin/org/530e42ccd6f45fd16d0d0717/saml/consume\"><saml:Issuer xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://openam.vagrant.dev/openam</saml:Issuer><samlp:Status xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">\\\\n<samlp:StatusCode xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\"\\\\nValue=\"urn:oasis:names:tc:SAML:2.0:status:Success\">\\\\n</samlp:StatusCode>\\\\n</samlp:Status><saml:Assertion xmlns:saml=\"urn:oasis:names:tc:SAML:2.0:assertion\" ID=\"s2a0bee0da937e236167e99b209802056033816ac2\" IssueInstant=\"2014-06-15T01:04:52Z\" Version=\"2.0\">\\\\n<saml:Issuer>http://openam.vagrant.dev/openam</saml:Issuer><ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">\\\\n<ds:SignedInfo>\\\\n<ds:CanonicalizationMethod Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>\\\\n<ds:SignatureMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#rsa-sha1\"/>\\\\n<ds:Reference URI=\"#s2a0bee0da937e236167e99b209802056033816ac2\">\\\\n<ds:Transforms>\\\\n<ds:Transform Algorithm=\"http://www.w3.org/2000/09/xmldsig#enveloped-signature\"/>\\\\n<ds:Transform Algorithm=\"http://www.w3.org/2001/10/xml-exc-c14n#\"/>\\\\n</ds:Transforms>\\\\n<ds:DigestMethod Algorithm=\"http://www.w3.org/2000/09/xmldsig#sha1\"/>\\\\n<ds:DigestValue>4uSmVzjovUdQd3px/RcnoxQBsqE=</ds:DigestValue>\\\\n</ds:Reference>\\\\n</ds:SignedInfo>\\\\n<ds:SignatureValue>\\\\nhm/grge36uA6j1OWif2bTcvVTwESjmuJa27NxepW0AiV5YlcsHDl7RAIk6k/CjsSero3bxGbm56m\\\\nYncOEi9F1Tu7dS0bfx+vhm/kKTPgwZctf4GWn4qQwP+KeoZywbNj9ShsYJ+zPKzXwN4xBSuPjMxP\\\\nNf5szzjEWpOndQO/uDs=\\\\n</ds:SignatureValue>\\\\n<ds:KeyInfo>\\\\n<ds:X509Data>\\\\n<ds:X509Certificate>\\\\nMIICQDCCAakCBEeNB0swDQYJKoZIhvcNAQEEBQAwZzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCkNh\\\\nbGlmb3JuaWExFDASBgNVBAcTC1NhbnRhIENsYXJhMQwwCgYDVQQKEwNTdW4xEDAOBgNVBAsTB09w\\\\nZW5TU08xDTALBgNVBAMTBHRlc3QwHhcNMDgwMTE1MTkxOTM5WhcNMTgwMTEyMTkxOTM5WjBnMQsw\\\\nCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEUMBIGA1UEBxMLU2FudGEgQ2xhcmExDDAK\\\\nBgNVBAoTA1N1bjEQMA4GA1UECxMHT3BlblNTTzENMAsGA1UEAxMEdGVzdDCBnzANBgkqhkiG9w0B\\\\nAQEFAAOBjQAwgYkCgYEArSQc/U75GB2AtKhbGS5piiLkmJzqEsp64rDxbMJ+xDrye0EN/q1U5Of+\\\\nRkDsaN/igkAvV1cuXEgTL6RlafFPcUX7QxDhZBhsYF9pbwtMzi4A4su9hnxIhURebGEmxKW9qJNY\\\\nJs0Vo5+IgjxuEWnjnnVgHTs1+mq5QYTA7E6ZyL8CAwEAATANBgkqhkiG9w0BAQQFAAOBgQB3Pw/U\\\\nQzPKTPTYi9upbFXlrAKMwtFf2OW4yvGWWvlcwcNSZJmTJ8ARvVYOMEVNbsT4OFcfu2/PeYoAdiDA\\\\ncGy/F2Zuj8XJJpuQRSE6PtQqBuDEHjjmOQJ0rV/r8mO1ZCtHRhpZ5zYRjhRC9eCbjx9VrFax0JDC\\\\n/FfwWigmrW0Y0Q==\\\\n</ds:X509Certificate>\\\\n</ds:X509Data>\\\\n</ds:KeyInfo>\\\\n</ds:Signature><saml:Subject>\\\\n<saml:NameID Format=\"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress\" NameQualifier=\"http://openam.vagrant.dev/openam\">user@example.com</saml:NameID><saml:SubjectConfirmation Method=\"urn:oasis:names:tc:SAML:2.0:cm:bearer\">\\\\n<saml:SubjectConfirmationData InResponseTo=\"84cbf2be33f6410bbe55877545a93f02\" NotOnOrAfter=\"2014-06-15T01:14:52Z\" Recipient=\"http://localhost:8086/api/1/rest/admin/org/530e42ccd6f45fd16d0d0717/saml/consume\"/></saml:SubjectConfirmation>\\\\n</saml:Subject><saml:Conditions NotBefore=\"2014-06-15T00:54:52Z\" NotOnOrAfter=\"2014-06-15T01:14:52Z\">\\\\n<saml:AudienceRestriction>\\\\n<saml:Audience>http://localhost:8086</saml:Audience>\\\\n</saml:AudienceRestriction>\\\\n</saml:Conditions>\\\\n<saml:AuthnStatement AuthnInstant=\"2014-06-15T01:00:25Z\" SessionIndex=\"s2f9b4d4b453d12b40ef3905cc959cdb40579c2301\"><saml:AuthnContext><saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</saml:AuthnContextClassRef></saml:AuthnContext></saml:AuthnStatement></saml:Assertion></samlp:Response>",
+ "domain": "dc=openam",
+ "hostname": "192.168.33.1\t",
+ "ipaddr": "Not Available",
+ "loggedby": "cn=dsameuser,ou=DSAME Users,dc=openam",
+ "loginid": "id=openamuser,ou=user,dc=openam",
+ "messageid": "SAML2-37",
+ "modulename": "SAML2.access",
+ "nameid": "user@example.com"
+ },
+ {
+ "log_line": 1,
+ "log_part": null,
+ "log_time": "2014-06-15 01:04:52.000",
+ "log_idle_msecs": 0,
+ "log_level": "trace",
+ "log_mark": 0,
+ "log_comment": null,
+ "log_tags": null,
+ "log_filters": null,
+ "contextid": "ec5708a7f199678a01",
+ "data": "vagrant|/",
+ "domain": "dc=openam",
+ "hostname": "127.0.1.1\t",
+ "ipaddr": "Not Available",
+ "loggedby": "cn=dsameuser,ou=DSAME Users,dc=openam",
+ "loginid": "cn=dsameuser,ou=DSAME Users,dc=openam",
+ "messageid": "COT-22",
+ "modulename": "COT.access",
+ "nameid": "Not Available"
+ }
+]
+EOF
+
+run_cap_test ${lnav_test} -d "/tmp/lnav.err" -n \
+ -c ";select log_line, col_0 from logline" \
+ ${test_dir}/logfile_for_join.0
+
+run_cap_test ${lnav_test} -d "/tmp/lnav.err" -n \
+ -c ";select col_0 from logline where log_line > 4" \
+ ${test_dir}/logfile_for_join.0
+
+run_test ${lnav_test} -d "/tmp/lnav.err" -n \
+ -c ":goto 1" \
+ -c ":create-logline-table join_group" \
+ -c ":goto 2" \
+ -c ";select logline.log_line as llline, join_group.log_line as jgline from logline, join_group where logline.col_0 = join_group.col_2" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_for_join.0
+
+check_output "create-logline-table is not working" <<EOF
+llline,jgline
+2,1
+2,8
+9,1
+9,8
+EOF
+
+
+run_cap_test ${lnav_test} -n \
+ -c ";select log_body from syslog_log where log_procname = 'automount'" \
+ < ${test_dir}/logfile_syslog.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";select log_body from syslog_log where log_procname = 'sudo'" \
+ < ${test_dir}/logfile_syslog.0
+
+# Create a dummy database for the next couple of tests to consume.
+touch empty
+rm simple-db.db
+run_test ${lnav_test} -n \
+ -c ";ATTACH DATABASE 'simple-db.db' as 'db'" \
+ -c ";CREATE TABLE IF NOT EXISTS db.person ( id integer PRIMARY KEY, first_name text, last_name, age integer )" \
+ -c ";INSERT INTO db.person(id, first_name, last_name, age) VALUES (0, 'Phil', 'Myman', 30)" \
+ -c ";INSERT INTO db.person(id, first_name, last_name, age) VALUES (1, 'Lem', 'Hewitt', 35)" \
+ -c ";DETACH DATABASE 'db'" \
+ empty
+
+check_output "Could not create db?" <<EOF
+EOF
+
+# Test to see if lnav can recognize a sqlite3 db file passed in as an argument.
+run_cap_test ${lnav_test} -n -c ";select * from person order by age asc" \
+ simple-db.db
+
+# Test to see if lnav can recognize a sqlite3 db file passed in as an argument.
+# XXX: Need to pass in a file, otherwise lnav keeps trying to open syslog
+# and we might not have sufficient privileges on the system the tests are being
+# run on.
+run_cap_test ${lnav_test} -n \
+ -c ";attach database 'simple-db.db' as 'db'" \
+ -c ';select * from person order by age asc' \
+ empty
+
+# Test to see if we can attach a database in LNAVSECURE mode.
+export LNAVSECURE=1
+
+run_cap_test ${lnav_test} -n \
+ -c ";attach database 'simple-db.db' as 'db'" \
+ empty
+
+run_cap_test ${lnav_test} -n \
+ -c ";attach database ':memdb:' as 'db'" \
+ empty
+
+run_cap_test ${lnav_test} -n \
+ -c ";attach database '/tmp/memdb' as 'db'" \
+ empty
+
+run_cap_test ${lnav_test} -n \
+ -c ";attach database 'file:memdb?cache=shared' as 'db'" \
+ empty
+
+unset LNAVSECURE
+
+
+touch -t 201503240923 ${test_dir}/logfile_syslog_with_access_log.0
+run_test ${lnav_test} -n -d /tmp/lnav.err \
+ -c ";select * from access_log" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_syslog_with_access_log.0
+
+check_output "access_log not found within syslog file" <<EOF
+log_line,log_part,log_time,log_idle_msecs,log_level,log_mark,log_comment,log_tags,log_filters,c_ip,cs_method,cs_referer,cs_uri_query,cs_uri_stem,cs_user_agent,cs_username,cs_version,sc_bytes,sc_status,cs_host
+1,<NULL>,2015-03-24 14:02:50.000,6927348000,info,0,<NULL>,<NULL>,<NULL>,127.0.0.1,GET,<NULL>,<NULL>,/includes/js/combined-javascript.js,<NULL>,-,HTTP/1.1,65508,200,<NULL>
+2,<NULL>,2015-03-24 14:02:50.000,0,error,0,<NULL>,<NULL>,<NULL>,127.0.0.1,GET,<NULL>,<NULL>,/bad.foo,<NULL>,-,HTTP/1.1,65508,404,<NULL>
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";select log_text from generic_log" \
+ -c ":write-json-to -" \
+ ${test_dir}/logfile_multiline.0
+
+check_output "multiline data is not right?" <<EOF
+[
+ {
+ "log_text": "2009-07-20 22:59:27,672:DEBUG:Hello, World!\n How are you today?"
+ },
+ {
+ "log_text": "2009-07-20 22:59:30,221:ERROR:Goodbye, World!"
+ }
+]
+EOF
+
+run_test ${lnav_test} -n \
+ -c ";select log_text from generic_log where log_line = 1" \
+ -c ":write-json-to -" \
+ ${test_dir}/logfile_multiline.0
+
+check_output "able to select a continued line?" <<EOF
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ":create-search-table search_test1 (\w+), world!" \
+ -c ";select col_0 from search_test1" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_multiline.0
+
+check_output "create-search-table is not working?" <<EOF
+col_0
+Hello
+Goodbye
+EOF
+
+run_test ${lnav_test} -n \
+ -c ":create-search-table search_test1 (\w+), World!" \
+ -c ";select col_0 from search_test1 where log_line > 0" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_multiline.0
+
+check_output "create-search-table is not working with where clause?" <<EOF
+col_0
+Goodbye
+EOF
+
+run_test ${lnav_test} -n \
+ -c ":create-search-table search_test1 (?<word>\w+), World!" \
+ -c ";select word, typeof(word) from search_test1" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_multiline.0
+
+check_output "create-search-table is not working?" <<EOF
+word,typeof(word)
+Hello,text
+Goodbye,text
+EOF
+
+run_test ${lnav_test} -n \
+ -c ":create-search-table search_test1 eth(?<ethnum>\d+)" \
+ -c ";select typeof(ethnum) from search_test1" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_syslog.2
+
+check_output "regex type guessing is not working?" <<EOF
+typeof(ethnum)
+integer
+integer
+integer
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ":delete-search-table search_test1" \
+ ${test_dir}/logfile_multiline.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":create-logline-table search_test1" \
+ -c ":delete-search-table search_test1" \
+ ${test_dir}/logfile_multiline.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":create-search-table search_test1 bad(" \
+ ${test_dir}/logfile_multiline.0
+
+NULL_GRAPH_SELECT_1=$(cat <<EOF
+;SELECT value FROM (
+ SELECT 10 as value
+ UNION ALL SELECT null as value)
+EOF
+)
+
+run_test ${lnav_test} -n \
+ -c "$NULL_GRAPH_SELECT_1" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_multiline.0
+
+check_output "number column with null does not work?" <<EOF
+value
+10
+<NULL>
+EOF
+
+run_test ${lnav_test} -n \
+ -c ";SELECT regexp_capture.content FROM access_log, regexp_capture(access_log.cs_version, 'HTTP/(\d+\.\d+)') WHERE regexp_capture.capture_index = 1" \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "joining log table with regexp_capture is not working?" <<EOF
+content
+1.0
+1.0
+1.0
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT echoln(sc_bytes), 123 FROM access_log' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT lnav_top_file()' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ':switch-to-view db' \
+ -c ';SELECT lnav_top_file()' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -Nn \
+ -c ";select *,case match_index when 2 then replicate('abc', 1000) else '' end from regexp_capture_into_json('10;50;50;50;', '(\d+);')"
diff --git a/test/test_sql_anno.sh b/test/test_sql_anno.sh
new file mode 100644
index 0000000..e09c312
--- /dev/null
+++ b/test/test_sql_anno.sh
@@ -0,0 +1,50 @@
+#! /bin/bash
+
+# basic query
+run_cap_test ./drive_sql_anno "SELECT * FROM FOO"
+
+# no help for keyword flag
+run_cap_test ./drive_sql_anno "TABLE"
+
+# nested function calls
+run_cap_test ./drive_sql_anno "SELECT foo(bar())"
+
+# nested function calls
+run_cap_test ./drive_sql_anno "SELECT foo(bar())" 2
+
+# caret in keyword whitespace
+run_cap_test ./drive_sql_anno "SELECT lower(abc)" 9
+
+# caret in function whitespace
+run_cap_test ./drive_sql_anno "SELECT lower( abc )" 14
+
+# caret in unfinished function call
+run_cap_test ./drive_sql_anno "SELECT lower(abc" 16
+
+# caret on the outer function
+run_cap_test ./drive_sql_anno "SELECT instr(lower(abc), '123')" 9
+
+# caret on a nested function
+run_cap_test ./drive_sql_anno "SELECT instr(lower(abc), '123')" 15
+
+# caret on a flag
+run_cap_test ./drive_sql_anno "SELECT instr(lower(abc), '123') FROM bar" 30
+
+# multiple help hits
+run_cap_test ./drive_sql_anno "CREATE" 2
+
+# string vs ident
+run_cap_test ./drive_sql_anno "SELECT 'hello, world!' FROM \"my table\""
+
+# math
+run_cap_test ./drive_sql_anno "SELECT (1 + 2) AS three"
+
+run_cap_test ./drive_sql_anno "SELECT (1.5 + 2.2) AS decim"
+
+# subqueries
+run_cap_test ./drive_sql_anno "SELECT * FROM (SELECT foo, bar FROM baz)"
+
+run_cap_test ./drive_sql_anno \
+ "SELECT * from vmw_log, regexp_capture(log_body, '--> /SessionStats/SessionPool/Session/(?<line>[abc]+)')"
+
+run_cap_test ./drive_sql_anno "SELECT * FROM foo.bar"
diff --git a/test/test_sql_coll_func.sh b/test/test_sql_coll_func.sh
new file mode 100644
index 0000000..2e61f9a
--- /dev/null
+++ b/test/test_sql_coll_func.sh
@@ -0,0 +1,29 @@
+#! /bin/bash
+
+run_cap_test ./drive_sql "select '192.168.1.10' < '192.168.1.2'"
+
+run_cap_test ./drive_sql "select '192.168.1.10' < '192.168.1.2' collate ipaddress"
+
+run_cap_test ./drive_sql "select '192.168.1.10' < '192.168.1.12' collate ipaddress"
+
+run_cap_test ./drive_sql "select '::ffff:192.168.1.10' = '192.168.1.10' collate ipaddress"
+
+run_cap_test ./drive_sql "select 'fe80::a85f:80b4:5cbe:8691' = 'fe80:0000:0000:0000:a85f:80b4:5cbe:8691' collate ipaddress"
+
+run_cap_test ./drive_sql "select '' < '192.168.1.2' collate ipaddress"
+
+run_cap_test ./drive_sql "select '192.168.1.2' > '' collate ipaddress"
+
+run_cap_test ./drive_sql "select '192.168.1.2' < 'fe80::a85f:80b4:5cbe:8691' collate ipaddress"
+
+run_cap_test ./drive_sql "select 'h9.example.com' < 'h10.example.com' collate ipaddress"
+
+run_cap_test ./drive_sql "select 'file10.txt' < 'file2.txt'"
+
+run_cap_test ./drive_sql "select 'file10.txt' < 'file2.txt' collate naturalcase"
+
+run_cap_test ./drive_sql "select 'w' < 'e' collate loglevel"
+
+run_cap_test ./drive_sql "select 'e' < 'w' collate loglevel"
+
+run_cap_test ./drive_sql "select 'info' collate loglevel between 'trace' and 'fatal'"
diff --git a/test/test_sql_fs_func.sh b/test/test_sql_fs_func.sh
new file mode 100644
index 0000000..e3da92b
--- /dev/null
+++ b/test/test_sql_fs_func.sh
@@ -0,0 +1,53 @@
+#! /bin/bash
+
+run_cap_test ./drive_sql "select readlink('non-existent-link')"
+
+ln -sf sql_fs_readlink_test sql_fs_readlink_test.lnk
+run_cap_test ./drive_sql "select readlink('sql_fs_readlink_test.lnk')"
+rm sql_fs_readlink_test.lnk
+
+run_cap_test ./drive_sql "select realpath('non-existent-path')"
+
+# ln -sf drive_sql sql_fs_realpath_test.lnk
+# run_cap_test ./drive_sql "select realpath('sql_fs_realpath_test.lnk')"
+# rm sql_fs_realpath_test.lnk
+
+run_cap_test ./drive_sql "select basename('')"
+
+run_cap_test ./drive_sql "select basename('/')"
+
+run_cap_test ./drive_sql "select basename('//')"
+
+run_cap_test ./drive_sql "select basename('/foo')"
+
+run_cap_test ./drive_sql "select basename('foo/bar')"
+
+run_cap_test ./drive_sql "select basename('/foo/')"
+
+run_cap_test ./drive_sql "select basename('/foo///')"
+
+run_cap_test ./drive_sql "select basename('foo')"
+
+run_cap_test ./drive_sql "select dirname('')"
+
+run_cap_test ./drive_sql "select dirname('foo')"
+
+run_cap_test ./drive_sql "select dirname('foo///')"
+
+run_cap_test ./drive_sql "select dirname('/foo/bar')"
+
+run_cap_test ./drive_sql "select dirname('/')"
+
+run_cap_test ./drive_sql "select dirname('/foo')"
+
+run_cap_test ./drive_sql "select dirname('/foo//')"
+
+run_cap_test ./drive_sql "select dirname('foo//')"
+
+run_cap_test ./drive_sql "select joinpath()"
+
+run_cap_test ./drive_sql "select joinpath('foo')"
+
+run_cap_test ./drive_sql "select joinpath('foo', 'bar', 'baz')"
+
+run_cap_test ./drive_sql "select joinpath('foo', 'bar', 'baz', '/root')"
diff --git a/test/test_sql_indexes.sh b/test/test_sql_indexes.sh
new file mode 100644
index 0000000..951c7a6
--- /dev/null
+++ b/test/test_sql_indexes.sh
@@ -0,0 +1,45 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+# XXX sqlite reports different results for the "detail" column, so we
+# have to rewrite it.
+run_cap_test ${lnav_test} -n \
+ -c ";EXPLAIN QUERY PLAN SELECT * FROM access_log WHERE log_path GLOB '*/logfile_access_log.*'" \
+ -c ";SELECT \$id, \$parent, \$notused, replace(\$detail, 'SCAN TABLE', 'SCAN')" \
+ ${test_dir}/logfile_access_log.*
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT *,log_unique_path FROM access_log WHERE log_path GLOB '*/logfile_access_log.*'" \
+ ${test_dir}/logfile_access_log.*
+
+run_cap_test ${lnav_test} -n \
+ -c ";EXPLAIN QUERY PLAN SELECT * FROM all_logs WHERE log_format = 'access_log'" \
+ -c ";SELECT \$id, \$parent, \$notused, replace(\$detail, 'SCAN TABLE', 'SCAN')" \
+ ${test_dir}/logfile_access_log.*
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT *,log_format FROM all_logs WHERE log_format = 'access_log'" \
+ ${test_dir}/logfile_access_log.* \
+ ${test_dir}/logfile_procstate.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";EXPLAIN QUERY PLAN SELECT * FROM all_logs WHERE log_level < 'error'" \
+ -c ";SELECT \$id, \$parent, \$notused, replace(\$detail, 'SCAN TABLE', 'SCAN')" \
+ ${test_dir}/logfile_access_log.*
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * FROM all_logs WHERE log_level < 'error'" \
+ ${test_dir}/logfile_access_log.*
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * FROM all_logs WHERE log_level <= 'error'" \
+ ${test_dir}/logfile_access_log.*
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * FROM all_logs WHERE log_level >= 'error'" \
+ ${test_dir}/logfile_access_log.*
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * FROM all_logs WHERE log_level > 'error'" \
+ ${test_dir}/logfile_access_log.*
diff --git a/test/test_sql_json_func.sh b/test/test_sql_json_func.sh
new file mode 100644
index 0000000..0865c45
--- /dev/null
+++ b/test/test_sql_json_func.sh
@@ -0,0 +1,132 @@
+#! /bin/bash
+
+run_cap_test ./drive_sql "select json_concat('[null,', 1.0, 2.0)"
+
+run_cap_test ./drive_sql "select json_concat(json('[null, true, 0]'), 1.0, 2.0)"
+
+run_cap_test ./drive_sql "select json_concat(json('[\"tag0\"]'), 'tag1', 'tag2')"
+
+run_cap_test ./drive_sql "select json_concat(NULL, NULL)"
+
+run_cap_test ./drive_sql "select json_concat(NULL, json('{\"abc\": 1}'))"
+
+run_cap_test ./drive_sql "select json_contains(NULL, 4)"
+
+run_cap_test ./drive_sql "select json_contains('', 4)"
+
+run_cap_test ./drive_sql "select json_contains('null', NULL)"
+
+run_cap_test ./drive_sql "select json_contains('[[0]]', 0)"
+
+run_cap_test ./drive_sql "select json_contains('4', 4)"
+
+run_cap_test ./drive_sql "select json_contains('4', 2)"
+
+run_cap_test env TEST_COMMENT='contains1' ./drive_sql <<EOF
+select json_contains('"hi"', 'hi')
+EOF
+
+run_cap_test env TEST_COMMENT='contains1.5' ./drive_sql <<EOF
+select json_contains('"hi"', 'hi there')
+EOF
+
+run_cap_test env TEST_COMMENT='contains2' ./drive_sql <<EOF
+select json_contains('["hi", "bye"]', 'hola') as res
+EOF
+
+run_cap_test env TEST_COMMENT='contains3' ./drive_sql <<EOF
+select json_contains('["hi", "bye", "solong"]', 'bye') as res
+EOF
+
+run_cap_test env TEST_COMMENT='contains4' ./drive_sql <<EOF
+select json_contains('["hi", "bye", "solong]', 'bye') as res
+EOF
+
+run_cap_test ./drive_sql "select jget()"
+
+run_cap_test ./drive_sql "select jget('[123, true', '/0')"
+
+run_cap_test ./drive_sql "select jget('4', '')"
+
+run_cap_test ./drive_sql "select jget('4', null)"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 40]', '/3')"
+
+run_cap_test ./drive_sql "select typeof(jget('[null, true, 20, 30, 40]', '/3'))"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 40, {\"msg\": \"Hello\"}]', '/5')"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 40, {\"msg\": \"Hello\"}]', '/5/msg')"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 40, {\"msg\": \"Hello\"}]', '')"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 40]', '/abc')"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 40]', '/abc', 1)"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 40]', '/0')"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 40]', '/0/foo')"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 4.0]', '/4')"
+
+run_cap_test ./drive_sql "select typeof(jget('[null, true, 20, 30, 4.0]', '/4'))"
+
+run_cap_test ./drive_sql "select jget('[null, true, 20, 30, 40', '/0/foo')"
+
+run_cap_test ./drive_sql "select json_group_object(key) from (select 1 as key)"
+
+GROUP_SELECT_1=$(cat <<EOF
+SELECT id, json_group_object(key, value) as stack FROM (
+ SELECT 1 as id, 'key1' as key, 10 as value
+ UNION ALL SELECT 1 as id, 'key2' as key, 20 as value
+ UNION ALL SELECT 1 as id, 'key3' as key, 30 as value)
+EOF
+)
+
+run_cap_test ./drive_sql "$GROUP_SELECT_1"
+
+GROUP_SELECT_2=$(cat <<EOF
+SELECT id, json_group_object(key, value) as stack FROM (
+ SELECT 1 as id, 1 as key, 10 as value
+ UNION ALL SELECT 1 as id, 2 as key, null as value
+ UNION ALL SELECT 1 as id, 3 as key, 30.5 as value)
+EOF
+)
+
+run_cap_test ./drive_sql "$GROUP_SELECT_2"
+
+if test x"$HAVE_SQLITE3_VALUE_SUBTYPE" != x""; then
+ GROUP_SELECT_3=$(cat <<EOF
+SELECT id, json_group_object(key, json(value)) as stack FROM (
+ SELECT 1 as id, 1 as key, 10 as value
+ UNION ALL SELECT 1 as id, 2 as key, json_array(1, 2, 3) as value
+ UNION ALL SELECT 1 as id, 3 as key, 30.5 as value)
+EOF
+)
+
+ run_cap_test ./drive_sql "$GROUP_SELECT_3"
+fi
+
+
+GROUP_ARRAY_SELECT_1=$(cat <<EOF
+SELECT json_group_array(value) as stack FROM (
+ SELECT 10 as value
+ UNION ALL SELECT null as value
+ UNION ALL SELECT 'hello' as value)
+EOF
+)
+
+run_cap_test ./drive_sql "$GROUP_ARRAY_SELECT_1"
+
+GROUP_ARRAY_SELECT_2=$(cat <<EOF
+SELECT json_group_array(value, value * 10) as stack FROM (
+ SELECT 10 as value
+ UNION ALL SELECT 20 as value
+ UNION ALL SELECT 30 as value)
+EOF
+)
+
+run_cap_test ./drive_sql "$GROUP_ARRAY_SELECT_2"
+
+run_cap_test ./drive_sql "SELECT json_group_array(column1) FROM (VALUES (1)) WHERE 0" \ No newline at end of file
diff --git a/test/test_sql_regexp.sh b/test/test_sql_regexp.sh
new file mode 100644
index 0000000..23d0425
--- /dev/null
+++ b/test/test_sql_regexp.sh
@@ -0,0 +1,32 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+touch -t 202211030923 ${test_dir}/logfile_syslog.3
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * FROM syslog_log, regexp_capture_into_json(log_body, '"'"'"(?<value>[^"'"'"]+)')" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_syslog.3
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * from regexp_capture_into_json('foo=0x123e;', '(?<key>\w+)=(?<value>[^;]+)')" \
+ ${test_dir}/logfile_syslog.3
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * from regexp_capture_into_json('foo=0x123e;', '(?<key>\w+)=(?<value>[^;]+)', json_object('convert-numbers', json('false')))" \
+ ${test_dir}/logfile_syslog.3
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * from regexp_capture_into_json('foo=0x123e;', '(?<key>\w+)=(?<value>[^;]+)', '{abc')" \
+ ${test_dir}/logfile_syslog.3
+
+run_cap_test ${lnav_test} -n \
+ -c ";SELECT * from regexp_capture_into_json('foo=123e;', '(?<key>\w+)=(?<value>[^;]+)')" \
+ ${test_dir}/logfile_syslog.3
+
+run_cap_test ${lnav_test} -nN \
+ -c ";SELECT * from regexp_capture('abc=def;ghi=jkl;', '^(\w+)=([^;]+);')"
+
+run_cap_test ${lnav_test} -nN \
+ -c ";SELECT * from regexp_capture_into_json('abc=def;ghi=jkl;', '^(\w+)=([^;]+);')"
diff --git a/test/test_sql_search_table.sh b/test/test_sql_search_table.sh
new file mode 100644
index 0000000..bf5e71b
--- /dev/null
+++ b/test/test_sql_search_table.sh
@@ -0,0 +1,28 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT * FROM procstate_procs' \
+ ${test_dir}/logfile_procstate.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT *,log_body FROM vpx_lro_begin' \
+ ${test_dir}/logfile_vpxd.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";select * from vpx_lro_begin where log_line > 3 and lro_id = 'lro-846064'" \
+ -c ";select * from vpx_lro_begin where lro_id = 'lro-846064'" \
+ ${test_dir}/logfile_vpxd.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";select * from procstate_procs where cmd_name = '[kthreadd]'" \
+ -c ";select * from procstate_procs where cmd_name = '[kthreadd]'" \
+ ${test_dir}/logfile_procstate.0
+
+touch -t 202211030923 ${test_dir}/logfile_syslog.3
+
+run_cap_test ${lnav_test} -n \
+ -c ':create-search-table asl_mod ASL Module "(?<name>[^"]+)"' \
+ -c ';SELECT * FROM asl_mod' \
+ ${test_dir}/logfile_syslog.3
diff --git a/test/test_sql_str_func.sh b/test/test_sql_str_func.sh
new file mode 100644
index 0000000..e4cb96b
--- /dev/null
+++ b/test/test_sql_str_func.sh
@@ -0,0 +1,170 @@
+#! /bin/bash
+
+run_cap_test ./drive_sql "select length(gzip(1))"
+
+run_cap_test ./drive_sql "select gunzip(gzip(1))"
+
+run_cap_test ./drive_sql "select humanize_file_size()"
+
+run_cap_test ./drive_sql "select humanize_file_size('abc')"
+
+run_cap_test ./drive_sql "select humanize_file_size(1, 2)"
+
+run_cap_test ./drive_sql "select humanize_file_size(10 * 1000 * 1000)"
+
+run_cap_test ./drive_sql "select startswith('.foo', '.')"
+
+run_cap_test ./drive_sql "select startswith('foo', '.')"
+
+run_cap_test ./drive_sql "select endswith('foo', '.')"
+
+run_cap_test ./drive_sql "select endswith('foo.', '.')"
+
+run_cap_test ./drive_sql "select endswith('foo.txt', '.txt')"
+
+run_cap_test ./drive_sql "select endswith('a', '.txt')"
+
+run_cap_test ./drive_sql "select regexp('abcd', 'abcd')"
+
+run_cap_test ./drive_sql "select regexp('bc', 'abcd')"
+
+run_cap_test ./drive_sql "select regexp('[e-z]+', 'abcd')"
+
+run_cap_test ./drive_sql "select regexp('[e-z]+', 'ea')"
+
+run_cap_test ./drive_sql "select regexp_replace('test 1 2 3', '\\d+', 'N')"
+
+run_cap_test env TEST_COMMENT=regexp_replace_with_bs1 ./drive_sql <<'EOF'
+select regexp_replace('test 1 2 3', '\s+', '{\0}') as repl
+EOF
+
+run_cap_test env TEST_COMMENT=regexp_replace_with_bs2 ./drive_sql <<'EOF'
+select regexp_replace('test 1 2 3', '\w*', '{\0}') as repl
+EOF
+
+run_cap_test ./drive_sql "select regexp_replace('123 abc', '(\w*)', '<\3>') as repl"
+
+run_cap_test env TEST_COMMENT=regexp_replace_with_bs3 ./drive_sql <<'EOF'
+select regexp_replace('123 abc', '(\w*)', '<\\>') as repl
+EOF
+
+run_cap_test ./drive_sql "select regexp_replace('abc: def', '(\w*):\s*(.*)', '\1=\2') as repl"
+
+run_cap_test ./drive_sql "select regexp_match('abc', 'abc')"
+
+run_cap_test ./drive_sql "select regexp_match(null, 'abc')"
+
+run_cap_test ./drive_sql "select regexp_match('abc', null) as result"
+
+run_cap_test ./drive_sql "select typeof(result), result from (select regexp_match('(\d*)abc', 'abc') as result)"
+
+run_cap_test ./drive_sql "select typeof(result), result from (select regexp_match('(\d*)abc(\d*)', 'abc') as result)"
+
+run_cap_test ./drive_sql "select typeof(result), result from (select regexp_match('(\d+)', '123') as result)"
+
+run_cap_test ./drive_sql "select typeof(result), result from (select regexp_match('a(\d+\.\d+)a', 'a123.456a') as result)"
+
+run_cap_test ./drive_sql "select regexp_match('foo=(?<foo>\w+); (\w+)', 'foo=abc; 123') as result"
+
+run_cap_test ./drive_sql "select regexp_match('foo=(?<foo>\w+); (\w+\.\w+)', 'foo=abc; 123.456') as result"
+
+run_cap_test ${lnav_test} -nN \
+ -c ";SELECT regexp_match('^(\w+)=([^;]+);', 'abc=def;ghi=jkl;')"
+
+run_cap_test ./drive_sql "select extract('foo=1') as result"
+
+run_cap_test ./drive_sql "select extract('foo=1; bar=2') as result"
+
+run_cap_test ./drive_sql "select extract(null) as result"
+
+run_cap_test ./drive_sql "select extract(1) as result"
+
+run_cap_test ./drive_sql "select logfmt2json('foo=1 bar=2 baz=2e1 msg=hello') as result"
+
+run_cap_test ./drive_sql "SELECT substr('#foo', range_start) AS value FROM regexp_capture('#foo', '(\w+)') WHERE capture_index = 1"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture('foo bar', '\w+ (\w+)')"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture('foo bar', '\w+ \w+')"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture('foo bar', '\w+ (?<word>\w+)')"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture('foo bar', '(bar)|\w+ (?<word>\w+)')"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture()"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture('foo bar')"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture('foo bar', '(')"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture('1 2 3 45', '(\d+)')"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture('foo foo', '^foo')"
+
+run_cap_test ./drive_sql "SELECT * FROM regexp_capture_into_json('foo=1 bar=2; foo=3 bar=4', 'foo=(\d+) bar=(\d+)')"
+
+run_cap_test ./drive_sql "SELECT encode('foo', 'bar')"
+
+run_cap_test ./drive_sql "SELECT encode('foo', null)"
+
+run_cap_test ./drive_sql "SELECT encode(null, 'base64')"
+
+run_cap_test ./drive_sql "SELECT gunzip(decode(encode(gzip('Hello, World!'), 'base64'), 'base64'))"
+
+#run_cap_test env TEST_COMMENT=invalid_url ./drive_sql <<'EOF'
+#SELECT parse_url('https://bad@[fe::')
+#EOF
+
+run_cap_test env TEST_COMMENT=unsupported_url ./drive_sql <<'EOF'
+SELECT parse_url('https://example.com:100000')
+EOF
+
+run_cap_test env TEST_COMMENT=parse_url1 ./drive_sql <<'EOF'
+SELECT parse_url('https://example.com')
+EOF
+
+run_cap_test env TEST_COMMENT=parse_url2 ./drive_sql <<'EOF'
+SELECT parse_url('https://example.com/')
+EOF
+
+run_cap_test env TEST_COMMENT=parse_url3 ./drive_sql <<'EOF'
+SELECT parse_url('https://example.com/search?flag')
+EOF
+
+run_cap_test env TEST_COMMENT=parse_url4 ./drive_sql <<'EOF'
+SELECT parse_url('https://example.com/search?flag&flag2')
+EOF
+
+run_cap_test env TEST_COMMENT=parse_url5 ./drive_sql <<'EOF'
+SELECT parse_url('https://example.com/search?flag&flag2&=def')
+EOF
+
+run_cap_test env TEST_COMMENT=parse_url6 ./drive_sql <<'EOF'
+SELECT parse_url('https://example.com/sea%26rch?flag&flag2&=def#frag1%20space')
+EOF
+
+
+run_cap_test env TEST_COMMENT=unparse_url3 ./drive_sql <<'EOF'
+SELECT unparse_url(parse_url('https://example.com/search?flag'))
+EOF
+
+run_cap_test env TEST_COMMENT=unparse_url4 ./drive_sql <<'EOF'
+SELECT unparse_url(parse_url('https://example.com/search?flag&flag2'))
+EOF
+
+run_cap_test env TEST_COMMENT=unparse_url5 ./drive_sql <<'EOF'
+SELECT unparse_url(parse_url('https://example.com/search?flag&flag2&=def'))
+EOF
+
+run_cap_test env TEST_COMMENT=unparse_url6 ./drive_sql <<'EOF'
+SELECT unparse_url(parse_url('https://example.com/search?flag&flag2&=def#frag1%20space'))
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT log_body, extract(log_body) from vmw_log' \
+ -c ':write-json-to -' \
+ ${test_dir}/logfile_vmw_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT anonymize(bro_id_resp_h) FROM bro_http_log' \
+ ${test_dir}/logfile_bro_http.log.0
diff --git a/test/test_sql_time_func.sh b/test/test_sql_time_func.sh
new file mode 100644
index 0000000..181dd70
--- /dev/null
+++ b/test/test_sql_time_func.sh
@@ -0,0 +1,71 @@
+#! /bin/bash
+
+# timeslice('blah')
+run_cap_test ./drive_sql "select timeslice('2015-08-07 12:01:00', 'blah')"
+
+# before 12pm
+run_cap_test ./drive_sql "select timeslice('2015-08-07 12:01:00', 'before fri')"
+
+# not before 12pm
+run_cap_test ./drive_sql "select timeslice('2015-08-07 11:59:00', 'after fri')"
+
+# not before 12pm
+run_cap_test ./drive_sql "select timeslice('2015-08-07 11:59:00', 'fri')"
+
+# before 12pm
+run_cap_test ./drive_sql "select timeslice('2015-08-07 12:01:00', 'before 12pm')"
+
+# not before 12pm
+run_cap_test ./drive_sql "select timeslice('2015-08-07 11:59:00', 'before 12pm')"
+
+# after 12pm
+run_cap_test ./drive_sql "select timeslice('2015-08-07 12:01:00', 'after 12pm')"
+
+# not after 12pm
+run_cap_test ./drive_sql "select timeslice('2015-08-07 11:59:00', 'after 12pm')"
+
+# timeslice()
+run_cap_test ./drive_sql "select timeslice()"
+
+# timeslice('2015-02-01T05:10:00')
+run_cap_test ./drive_sql "select timeslice('2015-02-01T05:10:00')"
+
+# timeslice empty
+run_cap_test ./drive_sql "select timeslice('', '')"
+
+# timeslice abs
+run_cap_test ./drive_sql "select timeslice('2015-08-07 12:01:00', '8 am')"
+
+# timeslice abs
+run_cap_test ./drive_sql "select timeslice('2015-08-07 08:00:33', '8 am')"
+
+# timeslice abs
+run_cap_test ./drive_sql "select timeslice('2015-08-07 08:01:33', '8 am')"
+
+# timeslice(null, null)
+run_cap_test ./drive_sql "select timeslice(null, null)"
+
+# timeslice(null)
+run_cap_test ./drive_sql "select timeslice(null)"
+
+# 100ms slice
+run_cap_test ./drive_sql "select timeslice(1616300753.333, '100ms')"
+
+# timeslice 5m
+run_cap_test ./drive_sql "select timeslice('2015-08-07 12:01:00', '5m')"
+
+# timeslice 1d
+run_cap_test ./drive_sql "select timeslice('2015-08-07 12:01:00', '1d')"
+
+# XXX This is wrong...
+# timeslice 1 month
+run_cap_test ./drive_sql "select timeslice('2015-08-07 12:01:00', '1 month')"
+
+# timeslice ms
+run_cap_test ./drive_sql "select timediff('2017-01-02T05:00:00.100', '2017-01-02T05:00:00.000')"
+
+# timeslice day
+run_cap_test ./drive_sql "select timediff('today', 'yesterday')"
+
+# timeslice day
+run_cap_test ./drive_sql "select timediff('foo', 'yesterday')"
diff --git a/test/test_sql_views_vtab.sh b/test/test_sql_views_vtab.sh
new file mode 100644
index 0000000..606a673
--- /dev/null
+++ b/test/test_sql_views_vtab.sh
@@ -0,0 +1,179 @@
+#! /bin/bash
+
+export YES_COLOR=1
+unset XDG_CONFIG_HOME
+
+run_test ${lnav_test} -n \
+ -c ";SELECT view_name,basename(filepath),visible FROM lnav_view_files" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_access_log.*
+
+check_output "lnav_view_files does not work?" <<EOF
+view_name,basename(filepath),visible
+log,logfile_access_log.0,1
+log,logfile_access_log.1,1
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_view_files SET visible=0 WHERE endswith(filepath, 'log.0')" \
+ ${test_dir}/logfile_access_log.*
+
+run_test ${lnav_test} -n \
+ -c ";DELETE FROM lnav_view_stack" \
+ ${test_dir}/logfile_access_log.0
+
+check_output "deleting the view stack does not work?" <<EOF
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_view_stack SET name = 'foo'" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_stack VALUES ('help')" \
+ -c ";DELETE FROM lnav_view_stack WHERE name = 'log'" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters VALUES ('log', 0, 1, 'out', 'regex', '')" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters VALUES ('log', 0, 1, 'out', 'regex', 'abc(')" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters VALUES ('bad', 0, 1, 'out', 'regex', 'abc')" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters VALUES (NULL, 0, 1, 'out', 'regex', 'abc')" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters VALUES ('log', 0 , 1, 'bad', 'regex', 'abc')" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters (view_name, pattern) VALUES ('log', 'vmk')" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters (view_name, pattern, type) VALUES ('log', 'vmk', 'in')" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters (view_name, pattern, type) VALUES ('log', 'vmk', 'in')" \
+ -c ";INSERT INTO lnav_view_filters (view_name, pattern, type) VALUES ('log', 'vmk', 'in')" \
+ -c ';SELECT * FROM lnav_view_filters' \
+ -c ':write-screen-to -' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters (view_name, pattern, type) VALUES ('log', 'vmk', 'in')" \
+ -c ";INSERT INTO lnav_view_filters (view_name, pattern, type) VALUES ('log', 'vmk1', 'in')" \
+ -c ";UPDATE lnav_view_filters SET pattern = 'vmk'" \
+ -c ';SELECT * FROM lnav_view_filters' \
+ -c ':write-screen-to -' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters (view_name, language, pattern) VALUES ('log', 'sql', '1')" \
+ -c ";INSERT INTO lnav_view_filters (view_name, language, pattern) VALUES ('log', 'sql', '1')" \
+ -c ';SELECT * FROM lnav_view_filters' \
+ -c ':write-screen-to -' \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-out vmk" \
+ -c ";DELETE FROM lnav_view_filters" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ":filter-out vmk" \
+ -c ";UPDATE lnav_view_filters SET pattern = 'vmkboot'" \
+ ${test_dir}/logfile_access_log.0
+
+run_test ${lnav_test} -n \
+ -c ":filter-out vmk" \
+ -c ";SELECT * FROM lnav_view_filter_stats" \
+ -c ":write-csv-to -" \
+ ${test_dir}/logfile_access_log.0
+
+check_output "view filter stats is not working?" <<EOF
+view_name,filter_id,hits
+log,1,2
+EOF
+
+run_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_view_filters (view_name, language, pattern) VALUES ('log', 'sql', ':sc_bytes = 134')" \
+ ${test_dir}/logfile_access_log.0
+
+check_output "inserted filter-out did not work?" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:26 +0000] "GET /vmw/cgi/tramp HTTP/1.0" 200 134 "-" "gPXE/0.9.7"
+EOF
+
+run_test ${lnav_test} -n \
+ -c ';DELETE FROM lnav_views' \
+ -c ';SELECT count(*) FROM lnav_views' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "delete from lnav_views table works?" <<EOF
+count(*)
+8
+EOF
+
+
+run_test ${lnav_test} -n \
+ -c ";INSERT INTO lnav_views (name) VALUES ('foo')" \
+ -c ';SELECT count(*) FROM lnav_views' \
+ -c ':write-csv-to -' \
+ ${test_dir}/logfile_access_log.0
+
+check_output "insert into lnav_views table works?" <<EOF
+count(*)
+8
+EOF
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_views SET top = 1 WHERE name = 'log'" \
+ ${test_dir}/logfile_access_log.0
+
+run_test ${lnav_test} -n \
+ -c ";UPDATE lnav_views SET top = inner_height - 1 WHERE name = 'log'" \
+ ${test_dir}/logfile_access_log.0
+
+check_output "updating lnav_views.top using inner_height does not work?" <<EOF
+192.168.202.254 - - [20/Jul/2009:22:59:29 +0000] "GET /vmw/vSphere/default/vmkernel.gz HTTP/1.0" 200 78929 "-" "gPXE/0.9.7"
+EOF
+
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_views SET top_time = 'bad-time' WHERE name = 'log'" \
+ ${test_dir}/logfile_access_log.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_views SET top_time = '2014-10-08T00:00:00' WHERE name = 'log'" \
+ ${test_dir}/logfile_generic.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_views SET search = 'warn' WHERE name = 'log'" \
+ -c ";SELECT search FROM lnav_views WHERE name = 'log'" \
+ ${test_dir}/logfile_generic.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_views SET search = 'warn' WHERE name = 'log'" \
+ -c ":goto 0" \
+ -c ":next-mark search" \
+ ${test_dir}/logfile_generic.0
+
+run_cap_test ${lnav_test} -n \
+ -c ";UPDATE lnav_views SET top_meta = json_object('anchor', '#build') WHERE name = 'text'" \
+ ${top_srcdir}/README.md
+
+run_cap_test ${lnav_test} -n \
+ -c ":goto 5" \
+ -c ";SELECT top_meta FROM lnav_top_view" \
+ -c ":write-json-to -" \
+ ${test_dir}/logfile_xml_msg.0
diff --git a/test/test_sql_xml_func.sh b/test/test_sql_xml_func.sh
new file mode 100644
index 0000000..72f1b0c
--- /dev/null
+++ b/test/test_sql_xml_func.sh
@@ -0,0 +1,11 @@
+#! /bin/bash
+
+run_cap_test ./drive_sql "SELECT * FROM xpath('/abc[', '<abc/>')"
+
+run_cap_test ./drive_sql "SELECT * FROM xpath('/abc', '<abc')"
+
+run_cap_test ./drive_sql "SELECT * FROM xpath('/abc/def', '<abc/>')"
+
+run_cap_test ./drive_sql "SELECT * FROM xpath('/abc/def[@a=\"b\"]', '<abc><def/><def a=\"b\">ghi</def></abc>')"
+
+run_cap_test ./drive_sql "SELECT * FROM xpath('/abc/def', '<abc><def>Hello &gt;</def></abc>')"
diff --git a/test/test_sql_yaml_func.sh b/test/test_sql_yaml_func.sh
new file mode 100644
index 0000000..3daca41
--- /dev/null
+++ b/test/test_sql_yaml_func.sh
@@ -0,0 +1,5 @@
+#! /bin/bash
+
+export YES_COLOR=1
+
+run_cap_test ${lnav_test} -nN -c ";SELECT yaml_to_json('[abc')"
diff --git a/test/test_stubs.cc b/test/test_stubs.cc
new file mode 100644
index 0000000..7d273b3
--- /dev/null
+++ b/test/test_stubs.cc
@@ -0,0 +1,93 @@
+/**
+ * Copyright (c) 2021, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "base/injector.hh"
+#include "bound_tags.hh"
+#include "config.h"
+#include "lnav.hh"
+#include "service_tags.hh"
+#include "spectro_source.hh"
+
+struct lnav_data_t lnav_data;
+
+void
+rebuild_hist()
+{
+}
+
+bool
+setup_logline_table(exec_context& ec)
+{
+ return false;
+}
+
+bool
+rescan_files(bool required)
+{
+ return false;
+}
+
+void
+wait_for_children()
+{
+}
+
+size_t
+rebuild_indexes(nonstd::optional<ui_clock::time_point> deadline)
+{
+ return 0;
+}
+
+void
+rebuild_indexes_repeatedly()
+{
+}
+
+readline_context::command_map_t lnav_commands;
+
+namespace injector {
+
+template<>
+void
+force_linking(lnav_flags_tag anno)
+{
+}
+
+template<>
+void
+force_linking(services::curl_streamer_t anno)
+{
+}
+
+template<>
+void
+force_linking(services::remote_tailer_t anno)
+{
+}
+} // namespace injector
diff --git a/test/test_text_anonymizer.cc b/test/test_text_anonymizer.cc
new file mode 100644
index 0000000..162111f
--- /dev/null
+++ b/test/test_text_anonymizer.cc
@@ -0,0 +1,131 @@
+/**
+ * Copyright (c) 2022, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "config.h"
+
+#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
+#include "doctest/doctest.h"
+#include "text_anonymizer.hh"
+
+TEST_CASE("ipv4")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const("127.0.1.1 says hi"))
+ == "10.0.0.1 says hi");
+ CHECK(ta.next(string_fragment::from_const("127.0.1.1 says hi"))
+ == "10.0.0.1 says hi");
+ CHECK(ta.next(string_fragment::from_const("u'127.0.1.1' says hi"))
+ == "u'10.0.0.1' says hi");
+}
+
+TEST_CASE("ipv6")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(
+ string_fragment::from_const("fe80::1887:2f2d:bc2e:8e41 says hi"))
+ == "2001:db8::1 says hi");
+}
+
+TEST_CASE("url")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const("retrieving https://bob:abc@example.com/fooooooo22/192.168.1.33/barrrrr44?abcdef=foobar&ghijkl=123456&bazzer&ip=192.168.1.2#heading-2")) ==
+ "aback https://meerkat:67c93775f715ab8ab01178caf86713c6@achondroplasia.example.com/abaft22/10.0.0.1/abashed44?aberrant=abhorrent&abiding=123456&abject&ip=10.0.0.2#able-2");
+}
+
+TEST_CASE("email")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const("hello support@lnav.org"))
+ == "aback meerkat@achondroplasia.example.com");
+}
+
+TEST_CASE("symbol")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const(
+ "state is Constants.DOWNLOAD_STARTED"))
+ == "aback is Abandoned.ABASHED_ABERRANT");
+}
+
+TEST_CASE("date")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const("2022-06-02T12:26:22.072Z"))
+ == "2022-06-02T12:26:22.072Z");
+}
+
+TEST_CASE("uuid")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const(
+ "52556d7e-c34d-d7f9-73b6-f52ad939952e"))
+ == "bc8b6954-c2a4-e7f3-0e18-2fa4035db1c9");
+}
+
+TEST_CASE("MAC-address")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const("ether f2:09:1a:a2:e3:e2"))
+ == "aback 00:00:5e:00:53:00");
+}
+
+TEST_CASE("hex-dump")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const("key f2:09:1a:a2"))
+ == "key 68:48:d3:93");
+}
+
+TEST_CASE("cc")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const("cc 6011 1111 1111 1117"))
+ == "cc 1a49 c794 31d9 3eb2");
+ CHECK(ta.next(string_fragment::from_const("cc 6011111111111117"))
+ == "cc 1a49c79431d93eb2");
+}
+
+TEST_CASE("xml")
+{
+ lnav::text_anonymizer ta;
+
+ CHECK(ta.next(string_fragment::from_const("<o:gupdate xmlns:o=\"http://www.google.com/update2/request\" protocol=\"2.0\" version=\"KeystoneDaemon-1.2.0.7709\" ismachine=\"1\" requestid=\"{0DFDBCD1-5E29-4DFC-BD99-31A2397198FE}\">")) ==
+ "<o:gupdate xmlns:o=\"http://achondroplasia.example.com/aback2/abandoned\" protocol=\"2.0\" version=\"Abashed-1.2.0.7709\" ismachine=\"1\" requestid=\"{1ca0a968-cbe9-e75b-d00b-4859609878ea}\">");
+}
diff --git a/test/test_text_file.sh b/test/test_text_file.sh
new file mode 100644
index 0000000..166f790
--- /dev/null
+++ b/test/test_text_file.sh
@@ -0,0 +1,36 @@
+#! /bin/bash
+
+export YES_COLOR=1
+unset XDG_CONFIG_HOME
+
+run_cap_test ${lnav_test} -n \
+ ${top_srcdir}/README.md
+
+run_cap_test ${lnav_test} -n -c ':goto #screenshot' \
+ ${top_srcdir}/README.md
+
+run_cap_test ${lnav_test} -n ${top_srcdir}/README.md#screenshot
+
+# run_cap_test ${lnav_test} -n ${test_dir}/non-existent:4
+
+run_cap_test ${lnav_test} -n ${top_srcdir}/README.md:-4
+
+run_cap_test ${lnav_test} -n \
+ -c ':goto 115' \
+ -c ";SELECT top_meta FROM lnav_views WHERE name = 'text'" \
+ -c ':write-json-to -' \
+ ${top_srcdir}/README.md
+
+run_cap_test ${lnav_test} -n \
+ ${top_srcdir}/src/log_level.cc
+
+cp ${test_dir}/UTF-8-test.txt UTF-8-test.md
+run_cap_test ${lnav_test} -n \
+ UTF-8-test.md
+
+run_cap_test ${lnav_test} -n \
+ -c ';SELECT * FROM lnav_file_metadata' \
+ ${test_dir}/textfile_0.md
+
+run_cap_test ${lnav_test} -n \
+ ${test_dir}/textfile_ansi_expanding.0
diff --git a/test/test_top_status.cc b/test/test_top_status.cc
new file mode 100644
index 0000000..769c346
--- /dev/null
+++ b/test/test_top_status.cc
@@ -0,0 +1,90 @@
+/**
+ * Copyright (c) 2007-2012, Timothy Stack
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * * Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * * Neither the name of Timothy Stack nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <assert.h>
+#include <stdlib.h>
+
+#include "command_executor.hh"
+#include "config.h"
+#include "lnav_config.hh"
+#include "top_status_source.hh"
+
+static time_t current_time = 1;
+
+int
+gettimeofday(struct timeval* tp, void* tzp)
+{
+ tp->tv_sec = current_time;
+ tp->tv_usec = 0;
+
+ return 0;
+}
+
+int
+main(int argc, char* argv[])
+{
+ int retval = EXIT_SUCCESS;
+
+ auto_sqlite3 db;
+
+ if (sqlite3_open(":memory:", db.out()) != SQLITE_OK) {
+ fprintf(stderr, "error: unable to create sqlite memory database\n");
+ exit(EXIT_FAILURE);
+ }
+
+ top_status_source_cfg cfg;
+ top_status_source tss(db, cfg);
+
+ setenv("HOME", "/", 1);
+
+ std::vector<lnav::console::user_message> errors;
+ std::vector<ghc::filesystem::path> paths;
+
+ load_config(paths, errors);
+
+ {
+ status_field& sf
+ = tss.statusview_value_for_field(top_status_source::TSF_TIME);
+ attr_line_t val;
+
+ tss.update_time();
+ val = sf.get_value();
+ assert(val.get_string() == sf.get_value().get_string());
+ current_time += 2;
+ tss.update_time();
+ assert(val.get_string() != sf.get_value().get_string());
+
+ cfg.tssc_clock_format = "abc";
+ tss.update_time();
+ val = sf.get_value();
+ assert(val.get_string() == " abc");
+ }
+
+ return retval;
+}
diff --git a/test/test_tui.sh b/test/test_tui.sh
new file mode 100644
index 0000000..3b6f4e9
--- /dev/null
+++ b/test/test_tui.sh
@@ -0,0 +1,28 @@
+#! /bin/bash
+
+# Unsets the following so it does not show up in the term title
+unset SSH_CONNECTION
+
+lnav_test="${top_builddir}/src/lnav-test"
+export lnav_test
+
+for fn in ${srcdir}/tui-captures/*; do
+ base_fn=`basename $fn`
+ run_test ./scripty -n -e $fn -- ${lnav_test} -H < /dev/null
+
+ case "$base_fn" in
+ tui_echo.0)
+ on_error_log "Skipping $fn"
+ ;;
+ *)
+ on_error_log "TUI test ${fn} does not work?"
+ ;;
+ esac
+done
+
+run_test ./scripty -n -e ${srcdir}/xpath_tui.0 -- \
+ ${lnav_test} -I ${test_dir} \
+ -c ':goto 2' \
+ ${srcdir}/logfile_xml_msg.0
+
+on_error_log "xpath() fields are not working?"
diff --git a/test/test_view_colors.sh b/test/test_view_colors.sh
new file mode 100644
index 0000000..39d02a2
--- /dev/null
+++ b/test/test_view_colors.sh
@@ -0,0 +1,6 @@
+#! /bin/bash
+
+run_test ./scripty -n -e ${srcdir}/view_colors_output.0 -- \
+ ./drive_view_colors < /dev/null
+
+on_error_fail_with "view colors are wrong?"
diff --git a/test/test_vt52_curses.sh b/test/test_vt52_curses.sh
new file mode 100644
index 0000000..946e645
--- /dev/null
+++ b/test/test_vt52_curses.sh
@@ -0,0 +1,6 @@
+#! /bin/bash
+
+run_test ./scripty -n -e ${srcdir}/vt52_curses_input.0 \
+ -- ./drive_vt52_curses < /dev/null
+
+on_error_fail_with "single line vt52 did not work?"
diff --git a/test/textfile_0.md b/test/textfile_0.md
new file mode 100644
index 0000000..377e46f
--- /dev/null
+++ b/test/textfile_0.md
@@ -0,0 +1,9 @@
+{
+ "comment": "This is JSON front-matter"
+}
+
+## Test
+
+* One
+* Two
+* Three
diff --git a/test/textfile_ansi.0 b/test/textfile_ansi.0
new file mode 100644
index 0000000..5371c52
--- /dev/null
+++ b/test/textfile_ansi.0
@@ -0,0 +1 @@
+{ Example: foo, bar: baz }
diff --git a/test/textfile_ansi_expanding.0 b/test/textfile_ansi_expanding.0
new file mode 100644
index 0000000..2fefb6f
--- /dev/null
+++ b/test/textfile_ansi_expanding.0
@@ -0,0 +1,10 @@
+ * Package: sys-libs/glibc-2.36-r7:2.2
+ * Repository: gentoo
+ * Maintainer: toolchain@gentoo.org
+ * USE: abi_x86_64 amd64 caps elibc_glibc kernel_linux multiarch ssp static-libs test userland_GNU
+ * FEATURES: network-sandbox preserve-libs sandbox test userpriv usersandbox
+ * Checking whether python3_11 is suitable ...
+ * >=dev-lang/python-3.11.1-r1:3.11 ...
+ [ ok ]
+ * Using python3.11 to build (via PYTHON_COMPAT iteration)
+>>> Unpacking source...
diff --git a/test/textfile_json_indented.0 b/test/textfile_json_indented.0
new file mode 100644
index 0000000..80b5fb6
--- /dev/null
+++ b/test/textfile_json_indented.0
@@ -0,0 +1,12 @@
+{
+ "foo bar": null,
+ "array": [
+ 1,
+ 2,
+ 3
+ ],
+ "obj": {
+ "one": 1,
+ "two": true
+ }
+}
diff --git a/test/textfile_json_one_line.0 b/test/textfile_json_one_line.0
new file mode 100644
index 0000000..a83fd26
--- /dev/null
+++ b/test/textfile_json_one_line.0
@@ -0,0 +1 @@
+{ "foo bar" : null, "array" : [1, 2, 3], "obj" : { "one" : 1, "two" : true } }
diff --git a/test/textfile_quoted_json.0 b/test/textfile_quoted_json.0
new file mode 100644
index 0000000..cf46f72
--- /dev/null
+++ b/test/textfile_quoted_json.0
@@ -0,0 +1 @@
+'{ "foo bar" : null, "array" : [1, 2, 3], "obj" : { "one" : 1, "two" : true } }'
diff --git a/test/toplevel.lnav b/test/toplevel.lnav
new file mode 100644
index 0000000..b6309c8
--- /dev/null
+++ b/test/toplevel.lnav
@@ -0,0 +1,4 @@
+
+:echo toplevel here $1 $2
+
+|nested.lnav abc $3
diff --git a/test/tui-captures/tui_echo.0 b/test/tui-captures/tui_echo.0
new file mode 100644
index 0000000..962b066
--- /dev/null
+++ b/test/tui-captures/tui_echo.0
@@ -0,0 +1,229 @@
+CSI Don't Send Mouse X & Y
+CSI Don’t Use Cell Motion Mouse Tracking
+CSI Don't ...
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Application cursor keys
+CTRL =
+OSC Set window title: LOG
+S -1 ┋ ┋
+A └ normal, normal, normal
+CSI Erase all
+S 1 ┋ 2013-06-06T12:13:20 PDT ┋
+A └ bg(#008080)
+S 16 ┋ Files :: Text Filters :: Press q to exit ┋
+A └ bg(#000080), bold │ ││
+A ·└ fg(#008080), bg(#000080), underline ││
+A ··└ normal, bg(#000080), bold ││
+A ·······└ normal, fg(#000000), bg(#000080) ││
+A ········└ fg(#000080), bg(#c0c0c0) ││
+A ·········└ fg(#000000), bg(#c0c0c0), bold ││
+A ··········└ fg(#800080), bg(#c0c0c0), underline ││
+A ···········└ normal, fg(#000000), bg(#c0c0c0), bold ││
+A ·······················└ normal, fg(#000000), bg(#c0c0c0) ││
+A ······································································└ bold
+A ·······································································└ normal, fg(#000000), bg(#c0c0c0)
+S 17 ┋ ┋
+S 23 ┋ L0 0% ?:View Help ┋
+A └ normal, bg(#008080)
+S 18 ┋ ┋
+A └ normal, normal
+S 1 ┋ Press ENTER to focus on the breadcrumb bar ┋
+A ·····································└ bg(#008080) │
+A ···············································································└ carriage-return
+S 2 ┋ LOG ❭ ┋
+A └ fg(#000000), bg(#000080), bold
+A ·····└ normal, fg(#008080), bg(#c0c0c0)
+A ······└ fg(#000000), bg(#c0c0c0)
+S 3 ┋ x┋
+A ···············································································├ normal
+A └┛ alt
+A ················································································└ normal
+S 4 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 5 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 6 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 7 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 8 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 9 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 10 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 11 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 12 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 13 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 14 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 15 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 18 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 19 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 20 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 21 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 22 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 22 ┋ ┋
+A └ normal
+OSC Set window title: HELP
+S 2 ┋ HELP ❭⋯❭ ┋
+A ·└ fg(#000000), bg(#000080), bold
+A ······└ normal, fg(#008080), bg(#c0c0c0)
+A ·······└ fg(#000000), bg(#c0c0c0)
+A ········└ fg(#008080), bg(#c0c0c0)
+S 3 ┋ x┋
+A ···············································································└ normal, fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 4 ┋lnav ┋
+A └ fg(#000000), normal, underline
+A ····└ carriage-return
+S 6 ┋A fancy log file viewer for the terminal. ┋
+A └ normal │
+A ·········································└ carriage-return
+S 8 ┋Overview ┋
+A └ underline
+A ········└ carriage-return
+S 10 ┋The Logfile Navigator, lnav, is an enhanced log file viewer that takes ┋
+A └ normal │ │ │
+A ·······················└ bold │
+A ···························└ normal │
+A ······································································└ carriage-return
+S 11 ┋advantage of any semantic information that can be gleaned from the ┋
+A ··································································└ carriage-return
+S 12 ┋files being viewed, such as timestamps and log levels. Using this ┋
+A ·································································└ carriage-return
+S 13 ┋extra semantic information, lnav can do things like interleaving ┋
+A ································································└ carriage-return
+S 14 ┋messages from different files, generate histograms of messages over ┋
+A ···································································└ carriage-return
+S 15 ┋time, and providing hotkeys for navigating through the file. It is ┋
+A ··································································└ carriage-return
+S 16 ┋hoped that these features will allow the user to quickly and x┋
+A └┛ alt
+A ················································································└ normal
+S 17 ┋efficiently zero in on problems. x┋
+A └┛ alt
+A ················································································└ normal
+S 19 ┋Opening Paths/URLs ┋
+A └ underline │
+A ··················└ carriage-return
+S 21 ┋The main arguments to lnav are the local/remote files, directories, ┋
+A └ normal │
+A ···································································└ carriage-return
+S 22 ┋glob patterns, or URLs to be viewed. If no arguments are given, the ┋
+A ···································································└ carriage-return
+S 23 ┋ L0 0% ?:View Help ┋
+A └ fg(#000000), bg(#c0c0c0)
+S 24 ┋ Press e/E to move forward/backward through error messags ┋
+A ·······················└ normal││ │
+A ·····························└ bold │
+A ······························└ normal │
+A ·······························└ bold │
+A ································└ normal │
+A ···············································································└ backspace, backspace
+CSI Set Replace mode
+S 24 ┋ e ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+A └ normal
+K 3a
+CSI Erase Below
+CSI Erase Below
+S 24 ┋: ┋
+A └ normal
+A ·└ normal
+S 23 ┋ Enter an lnav command: (Press CTRL+] to abort) ┋
+A ·└ fg(#000000), bg(#c0c0c0) │ │
+A ·······························└ bold│
+A ·····································└ normal, fg(#000000), bg(#c0c0c0)
+S 23 ┋ ┋
+S 24 ┋ e ┋
+A ·└ normal, normal
+A ··└ normal
+K 65
+S 24 ┋ c ┋
+A ···└ normal
+K 63
+S 24 ┋ h ┋
+A ····└ normal
+K 68
+S 24 ┋ o ┋
+A ·····└ normal
+K 6f
+S 24 ┋ echo ┋
+A ·····└ backspace
+A ····└ backspace
+A ···└ backspace
+A ··└ backspace
+A ·└ fg(#000080)
+A ······└ normal, normal
+K 20
+S 24 ┋ h ┋
+A ·······└ normal
+K 68
+S 24 ┋ i ┋
+A ········└ normal
+K 69
+S 24 ┋ ┋
+A ········└ carriage-return
+CSI Erase Below
+S 24 ┋hi ┋
+A └ normal
+A ··└ normal
+K 0d
+S 23 ┋ L0 0% ┋
+A ··└ backspace
+A ·└ fg(#000000), bg(#c0c0c0)
+S 23 ┋ ?:View Help ┋
+S 24 ┋ ┋
+A ··└ normal, normal
+K 71
+S 24 ┋ⓘ info: executing SQL statement, press CTRL+] to cancel ┋
+A ··└ carriage-return │ │ │
+A ·······································└ fg(#800080), bold, underline
+A ·············································└ normal │
+A ·······················································└ carriage-return
+A └ normal
+OSC Set window title: LOG
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/tui-captures/tui_help.0 b/test/tui-captures/tui_help.0
new file mode 100644
index 0000000..ae13ead
--- /dev/null
+++ b/test/tui-captures/tui_help.0
@@ -0,0 +1,179 @@
+CSI Don't Send Mouse X & Y
+CSI Don’t Use Cell Motion Mouse Tracking
+CSI Don't ...
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Application cursor keys
+CTRL =
+OSC Set window title: LOG
+S -1 ┋ ┋
+A └ normal, normal, normal
+CSI Erase all
+S 1 ┋ 2013-06-06T12:13:20 PDT ┋
+A └ bg(#008080)
+S 16 ┋ Files :: Text Filters :: Press q to exit ┋
+A └ bg(#000080), bold │ ││
+A ·└ fg(#008080), bg(#000080), underline ││
+A ··└ normal, bg(#000080), bold ││
+A ·······└ normal, fg(#000000), bg(#000080) ││
+A ········└ fg(#000080), bg(#c0c0c0) ││
+A ·········└ fg(#000000), bg(#c0c0c0), bold ││
+A ··········└ fg(#800080), bg(#c0c0c0), underline ││
+A ···········└ normal, fg(#000000), bg(#c0c0c0), bold ││
+A ·······················└ normal, fg(#000000), bg(#c0c0c0) ││
+A ······································································└ bold
+A ·······································································└ normal, fg(#000000), bg(#c0c0c0)
+S 17 ┋ ┋
+S 23 ┋ L0 0% ?:View Help ┋
+A └ normal, bg(#008080)
+S 18 ┋ ┋
+A └ normal, normal
+S 1 ┋ Press ENTER to focus on the breadcrumb bar ┋
+A ·····································└ bg(#008080) │
+A ···············································································└ carriage-return
+S 2 ┋ LOG ❭ ┋
+A └ fg(#000000), bg(#000080), bold
+A ·····└ normal, fg(#008080), bg(#c0c0c0)
+A ······└ fg(#000000), bg(#c0c0c0)
+S 3 ┋ x┋
+A ···············································································├ normal
+A └┛ alt
+A ················································································└ normal
+S 4 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 5 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 6 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 7 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 8 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 9 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 10 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 11 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 12 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 13 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 14 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 15 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 18 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 19 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 20 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 21 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 22 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 22 ┋ ┋
+A └ normal
+OSC Set window title: HELP
+S 2 ┋ HELP ❭⋯❭ ┋
+A ·└ fg(#000000), bg(#000080), bold
+A ······└ normal, fg(#008080), bg(#c0c0c0)
+A ·······└ fg(#000000), bg(#c0c0c0)
+A ········└ fg(#008080), bg(#c0c0c0)
+S 3 ┋ x┋
+A ···············································································└ normal, fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 4 ┋lnav ┋
+A └ fg(#000000), normal, underline
+A ····└ carriage-return
+S 6 ┋A fancy log file viewer for the terminal. ┋
+A └ normal │
+A ·········································└ carriage-return
+S 8 ┋Overview ┋
+A └ underline
+A ········└ carriage-return
+S 10 ┋The Logfile Navigator, lnav, is an enhanced log file viewer that takes ┋
+A └ normal │ │ │
+A ·······················└ bold │
+A ···························└ normal │
+A ······································································└ carriage-return
+S 11 ┋advantage of any semantic information that can be gleaned from the ┋
+A ··································································└ carriage-return
+S 12 ┋files being viewed, such as timestamps and log levels. Using this ┋
+A ·································································└ carriage-return
+S 13 ┋extra semantic information, lnav can do things like interleaving ┋
+A ································································└ carriage-return
+S 14 ┋messages from different files, generate histograms of messages over ┋
+A ···································································└ carriage-return
+S 15 ┋time, and providing hotkeys for navigating through the file. It is ┋
+A ··································································└ carriage-return
+S 16 ┋hoped that these features will allow the user to quickly and x┋
+A └┛ alt
+A ················································································└ normal
+S 17 ┋efficiently zero in on problems. x┋
+A └┛ alt
+A ················································································└ normal
+S 19 ┋Opening Paths/URLs ┋
+A └ underline │
+A ··················└ carriage-return
+S 21 ┋The main arguments to lnav are the local/remote files, directories, ┋
+A └ normal │
+A ···································································└ carriage-return
+S 22 ┋glob patterns, or URLs to be viewed. If no arguments are given, the ┋
+A ···································································└ carriage-return
+S 23 ┋ L0 0% ?:View Help ┋
+A └ fg(#000000), bg(#c0c0c0)
+S 24 ┋ Press e/E to move forward/backward through error messags ┋
+A ·······················└ normal││ │
+A ·····························└ bold │
+A ······························└ normal │
+A ·······························└ bold │
+A ································└ normal │
+A ···············································································└ backspace, backspace
+CSI Set Replace mode
+S 24 ┋ e ┋
+CSI Reset Replace mode
+S 24 ┋ ┋
+A ···············································································└ carriage-return
+A └ normal
+K 71
+OSC Set window title: LOG
+S 24 ┋ⓘ info: executing SQL statement, press CTRL+] to cancel ┋
+A ·······································└ fg(#800080), bold, underline
+A ·············································└ normal
+CSI Erase to Right
+S 24 ┋ ┋
+A ·······················································└ carriage-return
+A └ normal
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/update_parser_output.sh b/test/update_parser_output.sh
new file mode 100755
index 0000000..334892b
--- /dev/null
+++ b/test/update_parser_output.sh
@@ -0,0 +1,13 @@
+#!/usr/bin/env bash
+
+test_dir=`dirname $0`
+
+for fn in ${test_dir}/datafile_simple.*; do
+ echo "Checking $fn"
+ ./drive_data_scanner -p $fn
+done
+
+for fn in ${test_dir}/log-samples/sample-*; do
+ echo "Checking $fn"
+ ./drive_data_scanner -p -l $fn
+done
diff --git a/test/view_colors_output.0 b/test/view_colors_output.0
new file mode 100644
index 0000000..cb70c4c
--- /dev/null
+++ b/test/view_colors_output.0
@@ -0,0 +1,54 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+S -1 ┋ ┋
+A └ normal, normal, normal
+CSI Erase all
+S 1 ┋This is line: 0 ┋
+A ···············└ carriage-return
+S 2 ┋This is line: 1 ┋
+A ···············└ carriage-return
+S 3 ┋This is line: 2 ┋
+A ···············└ carriage-return
+S 4 ┋This is line: 3 ┋
+A ···············└ carriage-return
+S 5 ┋This is line: 4 ┋
+A ···············└ carriage-return
+S 6 ┋This is line: 5 ┋
+A ···············└ carriage-return
+S 7 ┋This is line: 6 ┋
+A ···············└ carriage-return
+S 8 ┋This is line: 7 ┋
+A ···············└ carriage-return
+S 9 ┋This is line: 8 ┋
+A ···············└ carriage-return
+S 10 ┋This is line: 9 ┋
+A ···············└ carriage-return
+S 11 ┋This is line: 10 ┋
+A ················└ carriage-return
+S 12 ┋This is line: 11 ┋
+A ················└ carriage-return
+S 13 ┋This is line: 12 ┋
+A ················└ carriage-return
+S 14 ┋This is line: 13 ┋
+A ················└ carriage-return
+S 15 ┋This is line: 14 ┋
+A ················└ carriage-return
+S 16 ┋This is line: 15 ┋
+A ················└ carriage-return
+S 17 ┋before <123> after ┋
+A ········└ fg(#008080), inverse
+A ···········└ normal
+A ··················└ carriage-return
+A └ normal
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/vt52_curses_input.0 b/test/vt52_curses_input.0
new file mode 100644
index 0000000..c8a7ad7
--- /dev/null
+++ b/test/vt52_curses_input.0
@@ -0,0 +1,49 @@
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Erase all
+S 1 ┋Gruß ┋
+K 0d
+S 1 ┋ ┋
+A ····└ carriage-return
+K 0d
+CSI Erase Below
+K 0d
+S 1 ┋de ┋
+K 0d
+S 1 ┋ ┋
+A ··└ carriage-return
+CSI Erase Below
+K 0d
+S 1 ┋1 ┋
+K 0d
+S 1 ┋ 2 ┋
+K 0d
+S 1 ┋ 3 ┋
+K 0d
+S 1 ┋ ┋
+A ···└ carriage-return
+CSI Erase Below
+K 0d
+S 1 ┋abc ┋
+K 0d
+S 1 ┋ ┋
+A ···└ carriage-return
+CSI Erase Below
+K 0d
+CTRL bell
+K 0d
+S 1 ┋acdef ┋
+K 0d
+K 0d
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/test/vt52_curses_input.1 b/test/vt52_curses_input.1
new file mode 100644
index 0000000..9ee3226
--- /dev/null
+++ b/test/vt52_curses_input.1
@@ -0,0 +1,38 @@
+sleep 1.124207
+write 31
+sleep 0.295830
+write 32
+sleep 0.487919
+write 33
+sleep 0.343913
+write 34
+sleep 0.303816
+write 35
+sleep 0.343703
+write 36
+sleep 0.295997
+write 37
+sleep 0.327862
+write 38
+sleep 0.295952
+write 39
+sleep 0.247670
+write 30
+sleep 0.911859
+write 61
+sleep 0.311780
+write 62
+sleep 0.407914
+write 63
+sleep 0.319836
+write 64
+sleep 0.199866
+write 65
+sleep 0.327904
+write 66
+sleep 0.359919
+write 67
+sleep 0.343907
+write 68
+sleep 0.288008
+write 69
diff --git a/test/vt52_curses_output.0 b/test/vt52_curses_output.0
new file mode 100644
index 0000000..b77311a
--- /dev/null
+++ b/test/vt52_curses_output.0
@@ -0,0 +1,39 @@
+read 1b29301b371b5b3f3437681b5b313b3234721b5b6d1b5b346c1b5b481b5b324a477275c39f
+# write 0d
+read 0d
+# write 0d
+read 1b5b4a
+# write 0d
+read 6465
+# write 0d
+read 0d1b5b4a
+# write 0d
+read
+# write 0d
+read
+# write 0d
+read
+# write 0d
+read
+# write 0d
+read
+# write 0d
+read
+# write 0d
+read
+# write 0d
+read
+# write 0d
+read
+# write 0d
+read 616263
+# write 0d
+read 0d1b5b4a
+# write 0d
+read 07
+# write 0d
+read 6163646566
+# write 0d
+read
+# write 0d
+read 1b5b32343b31481b5b324a1b5b3f34376c1b380d1b5b3f316c1b3e
diff --git a/test/vt52_curses_output.1 b/test/vt52_curses_output.1
new file mode 100644
index 0000000..dfbe7dd
--- /dev/null
+++ b/test/vt52_curses_output.1
@@ -0,0 +1,38 @@
+read 1b29301b371b5b3f3437681b5b313b3234721b5b6d1b5b346c1b5b481b5b324a1b5b3542616263
+# write 31
+read 0d
+# write 32
+read 1b5b4a
+# write 33
+read 6465
+# write 34
+read 0d1b5b3138420a1b5b313841
+# write 35
+read 1b5b3138420a1b5b313941310a08
+# write 36
+read 1b5b3138420a1b5b313941320a08
+# write 37
+read 1b5b3138420a1b5b313941330a08
+# write 38
+read 1b5b3138420a1b5b313941340a08
+# write 39
+read 1b5b3138420a1b5b313941350a08
+# write 30
+read 1b5b3138420a1b5b313941360a08
+# write 61
+read 1b5b3138420a1b5b313941370a08
+# write 62
+read 1b5b3138420a1b5b313941380a08
+# write 63
+read 1b5b3138420a1b5b313941390a08
+# write 64
+read 616263
+# write 65
+read 0d1b5b4a
+# write 66
+read 07
+# write 67
+read 6163646566
+# write 68
+read
+# write 69
diff --git a/test/xpath_tui.0 b/test/xpath_tui.0
new file mode 100644
index 0000000..feb93ec
--- /dev/null
+++ b/test/xpath_tui.0
@@ -0,0 +1,458 @@
+CSI Don't Send Mouse X & Y
+CSI Don’t Use Cell Motion Mouse Tracking
+CSI Don't ...
+CTRL Use alt charset
+CTRL save cursor
+CSI Use alternate screen buffer
+CSI set scrolling region 1-24
+S -1 ┋ ┋
+A └ normal
+CSI Reset Replace mode
+CSI Application cursor keys
+CTRL =
+OSC Set window title: LOG
+S -1 ┋ ┋
+A └ normal, normal, normal
+CSI Erase all
+S 1 ┋ 2013-06-06T12:13:20 PDT Press ENTER to focus on the breadcrumb bar ┋
+A └ bg(#008080)
+S 2 ┋ LOG ❭ ┋
+A └ fg(#000000), bg(#000080), bold
+A ·····└ normal, fg(#008080), bg(#c0c0c0)
+A ······└ fg(#000000), bg(#c0c0c0)
+S 3 ┋ x┋
+A ···············································································├ normal
+A └┛ alt
+A ················································································└ normal
+S 4 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 5 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 6 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 7 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 8 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 9 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 10 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 11 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 12 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 13 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 14 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 15 ┋ x┋
+A └┛ alt
+A ················································································└ normal
+S 16 ┋ Files :: Text Filters :: 0 of 1 enabled Press q to exit ┋
+A └ bg(#000080), bold │ ││ ││ ││
+A ·└ fg(#008080), bg(#000080), underline ││
+A ··└ normal, bg(#000080), bold ││ ││
+A ·······└ normal, fg(#000000), bg(#000080) ││
+A ········└ fg(#000080), bg(#c0c0c0) ││
+A ·········└ fg(#000000), bg(#c0c0c0), bold ││
+A ··········└ fg(#800080), bg(#c0c0c0), underline ││
+A ···········└ normal, fg(#000000), bg(#c0c0c0), bold ││
+A ·······················└ normal, fg(#000000), bg(#c0c0c0) ││
+A ··························└ bold│ ││
+A ···························└ normal, fg(#000000), bg(#c0c0c0) ││
+A ·······························└ bold ││
+A ································└ normal, fg(#000000), bg(#c0c0c0) ││
+A ······································································└ bold
+A ·······································································└ normal, fg(#000000), bg(#c0c0c0)
+S 17 ┋ ┋
+S 18 ┋→ ` logfile_xml_msg.0 0.0 B — x┋
+A ··├ fg(#008000), bg(#c0c0c0) │ │ ││
+A └┛ alt │ │ │ ││
+A ···└ fg(#000000), bg(#c0c0c0) │ │ ││
+A ························└ bold│ │ ││
+A ······························└ normal, fg(#000000), bg(#c0c0c0) ││
+A ······································└ fg(#808000), bg(#c0c0c0) ││
+A ···············································································└ normal, fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 19 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 20 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 21 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 22 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 23 ┋ L0 0% ?:View Help ┋
+A └ fg(#000000), normal, bg(#008080)
+S 22 ┋ ┋
+A └ normal, normal
+S 18 ┋ 64.0 B 2020-12-10 06:56:41.061 — 2020-12-10 06:56:41. ┋
+A ··························└ fg(#000000), bg(#c0c0c0), bold │
+A ······························└ normal, fg(#000000), bg(#c0c0c0) │
+A ···············································································└ carriage-return
+S 22 ┋ ┋
+A └ normal, normal
+S 18 ┋ 628 ┋
+A ·························└ fg(#000000), bg(#c0c0c0), bold
+A ····························└ carriage-return
+S 22 ┋ ┋
+A └ normal, normal
+S 16 ┋ 1 ┋
+A ··························└ fg(#000000), bg(#c0c0c0), bold
+S 23 ┋ 25 10 ┋
+A ··└ normal, bg(#008080)
+A ·············└ carriage-return
+S 22 ┋ ┋
+A └ normal, normal
+S 2 ┋ 2020-12-10T06:56:41.061❭xml_msg_log❭logfile_xml_msg.0[0]❭ ┋
+A ······└ fg(#000000), bg(#c0c0c0) ││ ││
+A ·····························└ fg(#008080), bg(#c0c0c0) ││
+A ······························└ fg(#000000), bg(#c0c0c0) ││
+A ·········································└ fg(#008080), bg(#c0c0c0)
+A ··········································└ fg(#000000), bg(#c0c0c0)
+A ······························································└ fg(#008080), bg(#c0c0c0)
+A ·······························································└ carriage-return
+S 3 ┋l[2020-12-10 06:56:41,061] INFO [m:108] Calling 'x' with params: x┋
+A ├ normal │ │ ││
+A └┛ alt │ │ ││
+A ················································└ fg(#008000), bold ││
+A ···················································└ normal ││
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 4 ┋x x┋
+A ├ fg(#000000), normal ││
+A └┛ alt ││
+A ·└ normal ││
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 5 ┋x[2020-12-10 06:56:41,092] DEBUG [connect.client:69] Full request text: x┋
+A ├ fg(#000000), normal ││
+A └┛ alt ││
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 6 ┋x<?xml version='1.0' encoding='iso-8859-2'?> x┋
+A ├ fg(#000000), normal│ ││ │ ││
+A └┛ alt │ ││ ││ ││ │ ││
+A ·······└ fg(#008080)││ ││ │ ││
+A ··············└ normal ││ │ ││
+A ···············└ fg(#008000), bold │ ││
+A ····················└ normal ││ │ ││
+A ·····················└ fg(#008080) │ ││
+A ·····························└ normal │ ││
+A ······························└ fg(#008000), bold ││
+A ··········································└ normal ││
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 7 ┋x<a-request> x┋
+A ├ fg(#000000), normal ││
+A └┛ alt ││
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 8 ┋x <head> ┋
+A ├ fg(#000000), normal
+A └┛ alt │
+A ·········└ carriage-return
+S 9 ┋x x ┋
+A └┛ alt│
+A ······└ carriage-return
+S 10 ┋x </head> ┋
+A └┛ alt │
+A ··········└ carriage-return
+S 11 ┋x <source> ┋
+A └┛ alt │
+A ···········└ carriage-return
+S 12 ┋x x ┋
+A └┛ alt│
+A ······└ carriage-return
+S 13 ┋x </source> ┋
+A └┛ alt │
+A ············└ carriage-return
+S 14 ┋x <request id="1"> ┋
+A └┛ alt │ ││ ││
+A ············└ fg(#008080)
+A ··············└ normal
+A ···············└ fg(#008000), bold
+A ··················└ normal
+A ···················└ carriage-return
+S 15 ┋x <name> ┋
+A └┛ alt │
+A ···········└ carriage-return
+S 22 ┋ ┋
+A └ normal
+CSI set scrolling region 3-20
+S 3 ┋ ┋
+A └ [2M
+CSI set scrolling region 1-24
+CSI Erase Below
+S 2 ┋ 92 ┋
+A ···························└ fg(#000000), bg(#c0c0c0)
+S 2 ┋ 2]❭⋯❭ ┋
+A ······························································└ fg(#008080), bg(#c0c0c0)
+A ·······························································└ fg(#000000), bg(#c0c0c0)
+A ································································└ fg(#008080), bg(#c0c0c0)
+S 3 ┋ x┋
+A ···············································································├ normal
+A └┛ alt
+A ················································································└ normal
+S 6 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 7 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 8 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 9 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 10 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 11 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 12 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 13 ┋ x┋
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 14 ┋x x x┋
+A ├ fg(#000000), normal ││
+A └┛ alt ││
+A ···············································································└ fg(#000000)
+A ················································································└ normal
+A └┛ alt
+A ················································································└ normal
+S 15 ┋x </name> x┋
+A ├ fg(#000000), normal ││
+A └┛ alt ││
+A └┛ alt
+A ················································································└ normal
+S 16 ┋x </request> x┋
+A └┛ alt ││
+A └┛ alt
+A ················································································└ normal
+S 17 ┋x</a-request> x┋
+A └┛ alt ││
+A └┛ alt
+A ················································································└ normal
+S 18 ┋x x┋
+A └┛ alt ││
+A ·└ normal ││
+A └┛ alt
+A ················································································└ normal
+S 19 ┋x[2020-12-10 06:56:41,099] DEBUG [m:85] Full reply text: x┋
+A └┛ alt ││
+A └┛ alt
+A ················································································└ normal
+S 20 ┋x<?xml version='1.0' encoding='iso-8859-2'?> x┋
+A └┛ alt │ ││ ││ ││ │ ││
+A ·······└ fg(#008080)││ ││ │ ││
+A ··············└ normal ││ │ ││
+A ···············└ fg(#008000), bold │ ││
+A ····················└ normal ││ │ ││
+A ·····················└ fg(#008080) │ ││
+A ·····························└ normal │ ││
+A ······························└ fg(#008000), bold ││
+A ··········································└ normal ││
+A └┛ alt
+A ················································································└ normal
+S 21 ┋x<a-reply> x┋
+A └┛ alt ││
+A └┛ alt
+A ················································································└ normal
+S 24 ┋ ┋
+A └ normal
+S 22 ┋ Files :: Text Filters :: 0 of 1 enabled Press TAB to edit ┋
+A └ bg(#008080) ││ ││ │ │
+A ··························└ bold│ │ │
+A ···························└ normal, bg(#008080) │ │
+A ·······························└ bold │ │
+A ································└ normal, bg(#008080) │ │
+A ····································································└ bold
+A ·······································································└ normal, bg(#008080)
+S 23 ┋ L2 58% ?:View Help ┋
+A └ fg(#000000), bg(#c0c0c0)
+S 24 ┋ ┋
+A └ normal, normal
+K 70
+S 4 ┋ Received Time: 2020-12-10T06:56:41.092 -- in the future ┋
+A ················└ bold │ │ │
+A ·······································└ normal │
+A ···········································└ bold │
+A ························································└ carriage-return
+S 5 ┋ Pattern: /xml_msg_log/regex/std = ^\[(?<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2 ┋
+A └ normal ││ │ ││ ││ │││││ │││││ │││││ │││││ │││
+A ···································└ fg(#008080) ││ │││││ │││││ │││││ │││││ │││
+A ····································└ normal ││ │││││ │││││ │││││ │││││ │││
+A ······································└ fg(#008000), bold│ │││││ │││││ │││││ │││
+A ········································└ normal, fg(#008080)│││ │││││ │││││ │││
+A ·········································└ normal ││ │││││ │││││ │││││ │││││ │││
+A ··················································└ fg(#008080)│ │││││ │││││ │││
+A ···················································└ fg(#000080) │││││ │││││ │││
+A ·····················································└ fg(#008000), bold││││ │││
+A ······················································└ normal││ │││││ │││││ │││
+A ·······················································└ fg(#008000), bold││ │││
+A ························································└ normal │││││ │││││ │││
+A ·························································└ fg(#000080) │││││ │││
+A ···························································└ fg(#008000), bold││
+A ····························································└ normal││ │││││ │││
+A ·····························································└ fg(#008000), bold
+A ······························································└ normal │││││ │││
+A ·······························································└ fg(#000080) │││
+A ·································································└ fg(#008000), bold
+A ··································································└ normal││ │││
+A ···································································└ fg(#008000), bold
+A ····································································└ normal │││
+A ·····································································└ fg(#000080)
+A ·······································································└ fg(#008000), bold
+A ········································································└ normal
+A ·········································································└ fg(#008000), bold
+A ··········································································└ normal
+A ···········································································└ fg(#000080)
+A ·············································································└ fg(#008000), bold
+A ··············································································└ normal
+A ···············································································└ carriage-return
+S 6 ┋ Known message fields for table xml_msg_log: ┋
+A ································└ bold ││
+A ···········································└ normal
+A ············································└ carriage-return
+S 7 ┋ t log_time = 2020-12-10 06:56:41,092 ┋
+A └┛ alt │ │
+A ···············└ bold │
+A ···············································································└ carriage-return
+S 8 ┋ t level = DEBUG ┋
+A └ normal │ │
+A └┛ alt │ │
+A ···············└ bold │
+A ···············································································└ carriage-return
+S 9 ┋ t module = connect.client ┋
+A └ normal │ │
+A └┛ alt │ │
+A ···············└ bold │
+A ···············································································└ carriage-return
+S 10 ┋ t line = 69 ┋
+A └ normal │ │
+A └┛ alt │ │
+A ···············└ bold │
+A ···············································································└ carriage-return
+S 11 ┋ t log_body = Full request text: ┋
+A └ normal │ │
+A └┛ alt │ │
+A ···············└ bold │
+A ···············································································└ carriage-return
+S 12 ┋ t msg_data = <?xml version='1.0' encoding='iso-8859-2'?> <a-request> <head> ┋
+A └ normal │ │
+A └┛ alt │ │
+A ···············└ bold │
+A ···············································································└ carriage-return
+S 13 ┋ XML fields: ┋
+A └ normal │
+A ············└ carriage-return
+S 14 ┋ t xpath('/a-request/head/text()', msg_data) = x ┋
+A └┛ alt │
+A ······└ bold │
+A ···············································································└ carriage-return
+S 15 ┋ t xpath('/a-request/request/@id', msg_data) = 1 ┋
+A └ normal │
+A └┛ alt │
+A ······└ bold │
+A ···············································································└ carriage-return
+S 16 ┋ t xpath('/a-request/request/name/text()', msg_data) = x ┋
+A └ normal │
+A └┛ alt │
+A ······└ bold │
+A ···············································································└ carriage-return
+S 17 ┋ t xpath('/a-request/source/text()', msg_data) = x ┋
+A └ normal │
+A └┛ alt │
+A ······└ bold │
+A ···············································································└ carriage-return
+S 18 ┋ No discovered message fields ┋
+A └ normal
+S 19 ┋ <?xml version='1.0' encoding='iso-8859-2'?> ┋
+A ·······└ fg(#008080)││ ││ │
+A ··············└ normal ││ │
+A ···············└ fg(#008000), bold │
+A ····················└ normal ││ │
+A ·····················└ fg(#008080) │
+A ·····························└ normal │
+A ······························└ fg(#008000), bold
+A ··········································└ normal
+S 20 ┋ a-request> ┋
+S 21 ┋ <head> ┋
+A ··········└ carriage-return
+S 24 ┋ ┋
+A └ normal
+K 71
+S 23 ┋ 0 ┋
+A ··└ fg(#000000), bg(#c0c0c0)
+S 23 ┋ 0 ┋
+A ··············└ carriage-return
+S 24 ┋ⓘ info: executing SQL statement, press CTRL+] to cancel ┋
+A └ normal │ │ │
+A ·······································└ fg(#800080), bold, underline
+A ·············································└ normal │
+A ·······················································└ carriage-return
+A └ normal
+CSI Erase all
+CSI Use normal screen buffer
+CTRL restore cursor
+S 24 ┋ ┋
+A └ carriage-return
+CSI Normal cursor keys
+CTRL Normal keypad
diff --git a/tools/Makefile.am b/tools/Makefile.am
new file mode 100644
index 0000000..3dce4e8
--- /dev/null
+++ b/tools/Makefile.am
@@ -0,0 +1,16 @@
+
+all-local: bin2c$(BUILD_EXEEXT)
+
+bin2c$(BUILD_EXEEXT): bin2c.c
+ $(AM_V_CC) $(CC_FOR_BUILD) $(CPPFLAGS_FOR_BUILD) $(LDFLAGS_FOR_BUILD) -g3 -o $@ $? -lz
+
+EXTRA_DIST = \
+ bin2c.c
+
+CLEANFILES = \
+ bin2c$(BUILD_EXEEXT)
+
+dist-hook: bin2c$(BUILD_EXEEXT)
+
+distclean-local:
+ $(RM_V)rm -rf *.dSYM
diff --git a/tools/bin2c.c b/tools/bin2c.c
new file mode 100644
index 0000000..2141bf0
--- /dev/null
+++ b/tools/bin2c.c
@@ -0,0 +1,252 @@
+// bin2c.c
+//
+// convert a binary file into a C source vector
+//
+// THE "BEER-WARE LICENSE" (Revision 3.1415):
+// sandro AT sigala DOT it wrote this file. As long as you retain this notice
+// you can do whatever you want with this stuff. If we meet some day, and you
+// think this stuff is worth it, you can buy me a beer in return. Sandro Sigala
+
+#ifdef __CYGWIN__
+# include <alloca.h>
+#endif
+
+#include <ctype.h>
+#include <fcntl.h>
+#include <getopt.h>
+#include <libgen.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/stat.h>
+#include <unistd.h>
+#include <zlib.h>
+
+#ifndef PATH_MAX
+# define PATH_MAX 1024
+#endif
+
+const char* name = NULL;
+
+static const char* HEADER_FMT
+ = "#ifndef bin2c_%s_h\n"
+ "#define bin2c_%s_h\n"
+ "\n"
+ "#include \"bin2c.hh\"\n"
+ "\n"
+ "extern struct bin_src_file %s%s;\n"
+ "\n"
+ "#endif\n"
+ "\n";
+
+struct file_meta {
+ const char* fm_name;
+ unsigned int fm_compressed_size;
+ unsigned int fm_size;
+};
+
+void
+symname(char* dst, const char* fname)
+{
+ strcpy(dst, fname);
+ for (int lpc = 0; dst[lpc]; lpc++) {
+ if (!isalnum(dst[lpc])) {
+ dst[lpc] = '_';
+ }
+ }
+}
+
+void
+process(struct file_meta* fm, FILE* ofile)
+{
+ struct stat st;
+
+ if (stat(fm->fm_name, &st) == -1) {
+ perror("unable to stat file");
+ exit(1);
+ }
+
+ unsigned char* buf = malloc(st.st_size);
+ unsigned char* dest = malloc(st.st_size);
+
+ int fd = open(fm->fm_name, O_RDONLY);
+ if (fd == -1) {
+ perror("unable to open file");
+ exit(1);
+ }
+
+ int rc;
+ while ((rc = read(fd, &buf[fm->fm_size], (st.st_size - fm->fm_size))) > 0) {
+ fm->fm_size += rc;
+ }
+
+ uLongf destLen = st.st_size;
+ compress(dest, &destLen, buf, st.st_size);
+ fm->fm_compressed_size = destLen;
+
+ int c, col = 1;
+ char sym[1024];
+
+ symname(sym, basename((char*) fm->fm_name));
+ fprintf(ofile, "static const unsigned char %s_data[] = {\n", sym);
+ for (int lpc = 0; lpc < destLen; lpc++) {
+ c = dest[lpc];
+ if (col >= 78 - 6) {
+ fputc('\n', ofile);
+ col = 1;
+ }
+ fprintf(ofile, "0x%.2x, ", c);
+ col += 6;
+ }
+ fprintf(ofile, "0x00\n");
+ fprintf(ofile, "\n};\n");
+
+ free(buf);
+ free(dest);
+}
+
+void
+usage()
+{
+ fprintf(stderr, "usage: bin2c [-n name] <output_file> [input_file1 ...]\n");
+ exit(1);
+}
+
+int
+main(int argc, char** argv)
+{
+ int c;
+
+ while ((c = getopt(argc, argv, "hn:")) != -1) {
+ switch (c) {
+ case 'n':
+ name = optarg;
+ break;
+ default:
+ usage();
+ break;
+ }
+ }
+
+ argc -= optind;
+ argv += optind;
+
+ if (argc < 1) {
+ usage();
+ }
+
+ const char* out_base_name = argv[0];
+ char hname[PATH_MAX], hname_tmp[PATH_MAX], cname[PATH_MAX];
+
+ argc -= 1;
+ argv += 1;
+
+ snprintf(hname, sizeof(hname), "%s.h", out_base_name);
+ snprintf(hname_tmp, sizeof(hname_tmp), "%s.tmp", hname);
+
+ FILE* hfile = fopen(hname_tmp, "w+b");
+ if (hfile == NULL) {
+ fprintf(stderr, "cannot open %s for writing\n", hname_tmp);
+ exit(1);
+ }
+
+ snprintf(cname, sizeof(cname), "%s.cc", out_base_name);
+ FILE* cfile = fopen(cname, "wb");
+ if (cfile == NULL) {
+ fprintf(stderr, "cannot open %s for writing\n", cname);
+ exit(1);
+ }
+
+ char sym[1024];
+ if (name) {
+ strcpy(sym, name);
+ } else {
+ const char* in_base = basename(argv[0]);
+
+ symname(sym, in_base);
+ }
+
+ int array = argc > 1 || name;
+ char trailer[16];
+
+ if (array) {
+ snprintf(trailer, sizeof(trailer), "[%d]", argc);
+ } else {
+ trailer[0] = '\0';
+ }
+ fprintf(hfile, HEADER_FMT, sym, sym, sym, trailer);
+ fflush(hfile);
+ rewind(hfile);
+
+ int same = 1;
+ {
+ FILE* orig_hfile = fopen(hname, "rb");
+ if (orig_hfile == NULL) {
+ same = 0;
+ } else {
+ while (1) {
+ char orig_line[1024], new_line[1024];
+
+ char* orig_res
+ = fgets(orig_line, sizeof(orig_line), orig_hfile);
+ char* new_res = fgets(new_line, sizeof(new_line), hfile);
+
+ if (orig_res == NULL && new_res == NULL) {
+ break;
+ }
+
+ if (orig_res == NULL || new_res == NULL) {
+ same = 0;
+ break;
+ }
+
+ if (strcmp(orig_line, new_line) != 0) {
+ same = 0;
+ break;
+ }
+ }
+ }
+ }
+ fclose(hfile);
+ if (!same) {
+ rename(hname_tmp, hname);
+ } else {
+ remove(hname_tmp);
+ }
+
+ fprintf(cfile, "#include \"bin2c.hh\"\n");
+ fprintf(cfile, "\n");
+
+ struct file_meta* meta = alloca(sizeof(struct file_meta) * argc);
+
+ memset(meta, 0, sizeof(struct file_meta) * argc);
+ for (int lpc = 0; lpc < argc; lpc++) {
+ meta[lpc].fm_name = argv[lpc];
+ process(&meta[lpc], cfile);
+ }
+
+ fprintf(cfile, "struct bin_src_file %s%s = {\n", sym, trailer);
+ for (int lpc = 0; lpc < argc; lpc++) {
+ char sym[1024];
+
+ symname(sym, basename((char*) meta[lpc].fm_name));
+ fprintf(cfile, " ");
+ if (array) {
+ fprintf(cfile, "{ ");
+ }
+ fprintf(cfile,
+ "\"%s\", %s_data, %d, %d",
+ basename((char*) meta[lpc].fm_name),
+ sym,
+ meta[lpc].fm_compressed_size,
+ meta[lpc].fm_size);
+ if (array) {
+ fprintf(cfile, " },");
+ }
+ fprintf(cfile, "\n");
+ }
+ fprintf(cfile, "};\n");
+ fclose(cfile);
+
+ return 0;
+}
diff --git a/update_expected_output.sh b/update_expected_output.sh
new file mode 100755
index 0000000..ad416ee
--- /dev/null
+++ b/update_expected_output.sh
@@ -0,0 +1,99 @@
+#!/usr/bin/env bash
+
+srcdir="$1"
+builddir="$2"
+
+expected_dir="$1/expected"
+expected_am="${expected_dir}/expected.am"
+
+mkdir -p "${expected_dir}"
+
+for fname in $(ls -t ${builddir}/*.cmd); do
+ echo
+ echo "Checking test ${fname}:"
+ echo -n " "
+ cat "${fname}"
+ stem=$(echo $fname | sed -e 's/.cmd$//')
+ exp_stem="${srcdir}/expected/$(basename $stem)"
+
+ echo " \$(srcdir)/%reldir%/$(basename "$stem").out \\" >> "${expected_am}.tmp"
+ echo " \$(srcdir)/%reldir%/$(basename "$stem").err \\" >> "${expected_am}.tmp"
+
+ if ! test -f "${exp_stem}.out"; then
+ printf '\033[0;32mBEGIN\033[0m %s.out\n' "${stem}"
+ cat "${stem}.out"
+ printf '\033[0;32mEND\033[0m %s.out\n' "${stem}"
+ if test x"${AUTO_APPROVE}" = x""; then
+ echo "Expected stdout is missing, update with the above?"
+ select yn in "Yes" "No"; do
+ case $yn in
+ Yes ) cp "${stem}.out" "${exp_stem}.out"; break;;
+ No ) exit;;
+ esac
+ done
+ else
+ cp "${stem}.out" "${exp_stem}.out"
+ fi
+ else
+ if ! cmp "${exp_stem}.out" "${stem}.out"; then
+ diff -u "${exp_stem}.out" "${stem}.out"
+ if test x"${AUTO_APPROVE}" = x""; then
+ echo "Expected stdout is different, update with the above?"
+ select yn in "Yes" "No"; do
+ case $yn in
+ Yes ) cp "${stem}.out" "${exp_stem}.out"; break;;
+ No ) exit;;
+ esac
+ done
+ else
+ cp "${stem}.out" "${exp_stem}.out"
+ fi
+ fi
+ fi
+
+ if ! test -f "${exp_stem}.err"; then
+ printf '\033[0;31mBEGIN\033[0m %s.err\n' "${stem}"
+ cat "${stem}.err"
+ printf '\033[0;31mEND\033[0m %s.err\n' "${stem}"
+ if test x"${AUTO_APPROVE}" = x""; then
+ echo "Expected stderr is missing, update with the above?"
+ select yn in "Yes" "No"; do
+ case $yn in
+ Yes ) cp "${stem}.err" "${exp_stem}.err"; break;;
+ No ) exit;;
+ esac
+ done
+ else
+ cp "${stem}.err" "${exp_stem}.err"
+ fi
+ else
+ if ! cmp "${exp_stem}.err" "${stem}.err"; then
+ diff -u "${exp_stem}.err" "${stem}.err"
+ if test x"${AUTO_APPROVE}" = x""; then
+ echo "Expected stderr is different, update with the above?"
+ select yn in "Yes" "No"; do
+ case $yn in
+ Yes ) cp "${stem}.err" "${exp_stem}.err"; break;;
+ No ) exit;;
+ esac
+ done
+ else
+ cp "${stem}.err" "${exp_stem}.err"
+ fi
+ fi
+ fi
+done
+
+cat > "${expected_am}.new" <<EOF
+
+EXPECTED_FILES = \\
+$(sort "${expected_am}.tmp")
+ \$()
+EOF
+
+if ! cmp "${expected_am}" "${expected_am}.new"; then
+ cp "${expected_am}.new" "${expected_am}"
+fi
+
+rm "${expected_am}.new"
+rm "${expected_am}.tmp"